option('connection'); $batchSize = max(1, (int) $this->option('batch-size')); $resetProgress = (bool) $this->option('reset-progress'); $dryRun = (bool) $this->option('dry-run'); try { DB::connection($connection)->getPdo(); } catch (\Throwable $e) { $this->error("Cannot connect to Asteria DB (connection='{$connection}'): ".$e->getMessage()); return self::FAILURE; } foreach ([ 'sales_flat_order', 'sales_flat_order_item', 'sales_flat_order_address', 'sales_flat_order_payment', ] as $table) { if (! Schema::connection($connection)->hasTable($table)) { $this->error("Asteria table '{$table}' is missing on connection '{$connection}'."); return self::FAILURE; } } if (! Schema::hasColumn('orders', 'migrated_from_asteria_id')) { $this->error('orders.migrated_from_asteria_id is missing. Run php artisan migrate.'); return self::FAILURE; } $channel = core()->getDefaultChannel() ?? core()->getCurrentChannel(); if (! $channel) { $this->error('Bagisto default channel was not found.'); return self::FAILURE; } $reader = new Magento1OrderReader($connection); $lastId = $resetProgress ? 0 : (int) Cache::get(self::PROGRESS_KEY, 0); if ($resetProgress) { Cache::forget(self::PROGRESS_KEY); } if ($lastId > 0) { $this->line("Resuming from Asteria entity_id > {$lastId} (use --reset-progress to restart)."); } $created = 0; $skipped = 0; $itemsImported = 0; $addressesImported = 0; $batchNumber = 0; $this->info($dryRun ? '[DRY RUN] Scanning Magento orders…' : 'Migrating Magento orders…'); do { $orders = $reader->fetchOrders($lastId, $batchSize); if ($orders->isEmpty()) { break; } $batchNumber++; $lastId = (int) $orders->max('entity_id'); $orderIds = $orders->pluck('entity_id')->map(fn ($id) => (int) $id)->all(); $items = $reader->fetchItems($orderIds)->groupBy(fn (array $row) => (int) $row['order_id']); $addresses = $reader->fetchAddresses($orderIds)->groupBy(fn (array $row) => (int) $row['parent_id']); $payments = $reader->fetchPayments($orderIds)->groupBy(fn (array $row) => (int) $row['parent_id']); if ($dryRun) { $created += $orders->count(); $itemsImported += $items->flatten(1)->count(); $addressesImported += $addresses->flatten(1)->count(); $this->line(sprintf( ' Batch #%d: %d orders, %d items, %d addresses (last entity_id=%d) [skipped – dry-run]', $batchNumber, $orders->count(), $items->flatten(1)->count(), $addresses->flatten(1)->count(), $lastId )); continue; } [$customersByAsteriaId, $customersByEmail] = $this->loadCustomers($orders); $productsBySku = $this->loadProducts($items); $batchCreated = 0; $batchSkipped = 0; $batchItems = 0; $batchAddresses = 0; DB::transaction(function () use ( $orders, $items, $addresses, $payments, $channel, $customersByAsteriaId, $customersByEmail, $productsBySku, &$batchCreated, &$batchSkipped, &$batchItems, &$batchAddresses ) { foreach ($orders as $row) { $result = $this->migrateOrder( $row, $items->get((int) $row['entity_id'], collect()), $addresses->get((int) $row['entity_id'], collect()), $payments->get((int) $row['entity_id'], collect())->first(), $channel, $customersByAsteriaId, $customersByEmail, $productsBySku ); $batchCreated += $result['created']; $batchSkipped += $result['skipped']; $batchItems += $result['items']; $batchAddresses += $result['addresses']; } }); Cache::put(self::PROGRESS_KEY, $lastId, now()->addDays(30)); $created += $batchCreated; $skipped += $batchSkipped; $itemsImported += $batchItems; $addressesImported += $batchAddresses; $this->line(sprintf( ' Batch #%d: created=%d skipped=%d items=%d addresses=%d (last entity_id=%d)', $batchNumber, $batchCreated, $batchSkipped, $batchItems, $batchAddresses, $lastId )); Log::info('MigrateAsteriaOrders: batch '.$batchNumber.', last_id='.$lastId); } while ($orders->count() === $batchSize); $this->newLine(); $this->info("Done. Batches: {$batchNumber}, created: {$created}, skipped: {$skipped}, items: {$itemsImported}, addresses: {$addressesImported}."); return self::SUCCESS; } /** * @param array $row * @param Collection> $itemRows * @param Collection> $addressRows * @param array|null $paymentRow * @param array $customersByAsteriaId * @param array $customersByEmail * @param array $productsBySku * @return array{created: int, skipped: int, items: int, addresses: int} */ private function migrateOrder( array $row, $itemRows, $addressRows, ?array $paymentRow, Channel $channel, array $customersByAsteriaId, array $customersByEmail, array $productsBySku ): array { $asteriaId = (int) $row['entity_id']; $incrementId = trim((string) ($row['increment_id'] ?? '')); if ($asteriaId < 1 || $incrementId === '') { return ['created' => 0, 'skipped' => 1, 'items' => 0, 'addresses' => 0]; } $alreadyMigrated = Order::query() ->where('migrated_from_asteria_id', $asteriaId) ->exists(); if ($alreadyMigrated) { return ['created' => 0, 'skipped' => 1, 'items' => 0, 'addresses' => 0]; } $incrementTaken = Order::query() ->where('increment_id', $incrementId) ->exists(); if ($incrementTaken) { return ['created' => 0, 'skipped' => 1, 'items' => 0, 'addresses' => 0]; } $customer = $this->resolveCustomer($row, $customersByAsteriaId, $customersByEmail); $email = strtolower(trim((string) ($row['customer_email'] ?? ''))); if ($email === '' && $customer) { $email = strtolower((string) $customer->email); } $subTotal = $this->money($row['subtotal'] ?? 0); $baseSubTotal = $this->money($row['base_subtotal'] ?? $subTotal); $taxAmount = $this->money($row['tax_amount'] ?? 0); $baseTaxAmount = $this->money($row['base_tax_amount'] ?? $taxAmount); $shippingAmount = $this->money($row['shipping_amount'] ?? 0); $baseShippingAmount = $this->money($row['base_shipping_amount'] ?? $shippingAmount); $shippingTaxAmount = $this->money($row['shipping_tax_amount'] ?? 0); $baseShippingTaxAmount = $this->money($row['base_shipping_tax_amount'] ?? $shippingTaxAmount); $discountAmount = $this->money($row['discount_amount'] ?? 0); $baseDiscountAmount = $this->money($row['base_discount_amount'] ?? $discountAmount); $grandTotal = $this->money($row['grand_total'] ?? 0); $baseGrandTotal = $this->money($row['base_grand_total'] ?? $grandTotal); $subTotalInclTax = array_key_exists('subtotal_incl_tax', $row) ? $this->money($row['subtotal_incl_tax']) : $subTotal + $taxAmount; $baseSubTotalInclTax = array_key_exists('base_subtotal_incl_tax', $row) ? $this->money($row['base_subtotal_incl_tax']) : $baseSubTotal + $baseTaxAmount; $shippingInclTax = array_key_exists('shipping_incl_tax', $row) ? $this->money($row['shipping_incl_tax']) : $shippingAmount + $shippingTaxAmount; $baseShippingInclTax = array_key_exists('base_shipping_incl_tax', $row) ? $this->money($row['base_shipping_incl_tax']) : $baseShippingAmount + $baseShippingTaxAmount; $order = new Order; $order->forceFill([ 'migrated_from_asteria_id' => $asteriaId, 'increment_id' => $incrementId, 'status' => $this->mapStatus($row), 'channel_name' => $channel->name, 'is_guest' => $customer ? 0 : 1, 'customer_email' => $email !== '' ? $email : null, 'customer_first_name' => $this->requiredName($row['customer_firstname'] ?? null, $customer?->first_name ?? $email), 'customer_last_name' => trim((string) ($row['customer_lastname'] ?? '')) ?: ($customer?->last_name ?? '-'), 'customer_id' => $customer?->id, 'customer_type' => $customer ? Customer::class : null, 'channel_id' => $channel->id, 'channel_type' => get_class($channel), 'cart_id' => null, 'shipping_method' => $this->nullableString($row['shipping_method'] ?? null), 'shipping_title' => $this->nullableString($row['shipping_description'] ?? null), 'shipping_description' => $this->nullableString($row['shipping_description'] ?? null), 'coupon_code' => $this->nullableString($row['coupon_code'] ?? null), 'is_gift' => 0, 'total_item_count' => $this->qty($row['total_item_count'] ?? $itemRows->count()), 'total_qty_ordered' => $this->qty($row['total_qty_ordered'] ?? $itemRows->sum(fn (array $item) => (float) ($item['qty_ordered'] ?? 0))), 'base_currency_code' => $this->nullableString($row['base_currency_code'] ?? null) ?? 'USD', 'channel_currency_code' => $this->nullableString($row['store_currency_code'] ?? null) ?? $this->nullableString($row['order_currency_code'] ?? null) ?? 'USD', 'order_currency_code' => $this->nullableString($row['order_currency_code'] ?? null) ?? 'USD', 'grand_total' => $grandTotal, 'base_grand_total' => $baseGrandTotal, 'grand_total_invoiced' => $this->money($row['total_invoiced'] ?? 0), 'base_grand_total_invoiced' => $this->money($row['base_total_invoiced'] ?? 0), 'grand_total_refunded' => $this->money($row['total_refunded'] ?? 0), 'base_grand_total_refunded' => $this->money($row['base_total_refunded'] ?? 0), 'sub_total' => $subTotal, 'base_sub_total' => $baseSubTotal, 'sub_total_incl_tax' => $subTotalInclTax, 'base_sub_total_incl_tax' => $baseSubTotalInclTax, 'sub_total_invoiced' => $this->money($row['subtotal_invoiced'] ?? 0), 'base_sub_total_invoiced' => $this->money($row['base_subtotal_invoiced'] ?? 0), 'sub_total_refunded' => $this->money($row['subtotal_refunded'] ?? 0), 'base_sub_total_refunded' => $this->money($row['base_subtotal_refunded'] ?? 0), 'discount_amount' => $discountAmount, 'base_discount_amount' => $baseDiscountAmount, 'discount_invoiced' => $this->money($row['discount_invoiced'] ?? 0), 'base_discount_invoiced' => $this->money($row['base_discount_invoiced'] ?? 0), 'discount_refunded' => $this->money($row['discount_refunded'] ?? 0), 'base_discount_refunded' => $this->money($row['base_discount_refunded'] ?? 0), 'tax_amount' => $taxAmount, 'base_tax_amount' => $baseTaxAmount, 'tax_amount_invoiced' => $this->money($row['tax_invoiced'] ?? 0), 'base_tax_amount_invoiced' => $this->money($row['base_tax_invoiced'] ?? 0), 'tax_amount_refunded' => $this->money($row['tax_refunded'] ?? 0), 'base_tax_amount_refunded' => $this->money($row['base_tax_refunded'] ?? 0), 'shipping_amount' => $shippingAmount, 'base_shipping_amount' => $baseShippingAmount, 'shipping_amount_incl_tax' => $shippingInclTax, 'base_shipping_amount_incl_tax' => $baseShippingInclTax, 'shipping_invoiced' => $this->money($row['shipping_invoiced'] ?? 0), 'base_shipping_invoiced' => $this->money($row['base_shipping_invoiced'] ?? 0), 'shipping_refunded' => $this->money($row['shipping_refunded'] ?? 0), 'base_shipping_refunded' => $this->money($row['base_shipping_refunded'] ?? 0), 'shipping_tax_amount' => $shippingTaxAmount, 'base_shipping_tax_amount' => $baseShippingTaxAmount, ]); if (! empty($row['created_at'])) { $order->created_at = $row['created_at']; } $order->save(); $this->importPayment($order, $paymentRow); $importedAddresses = $this->importAddresses($order, $addressRows, $customer, $email); $importedItems = $this->importItems($order, $itemRows, $productsBySku); return [ 'created' => 1, 'skipped' => 0, 'items' => $importedItems, 'addresses' => $importedAddresses, ]; } /** * @param array|null $paymentRow */ private function importPayment(Order $order, ?array $paymentRow): void { $magentoMethod = trim((string) ($paymentRow['method'] ?? '')); $additional = [ 'magento_method' => $magentoMethod !== '' ? $magentoMethod : null, ]; if ($paymentRow) { $additional['asteria_payment_id'] = (int) ($paymentRow['entity_id'] ?? 0); foreach (['last_trans_id', 'cc_type', 'cc_last4'] as $key) { $value = $this->nullableString($paymentRow[$key] ?? null); if ($value !== null) { $additional[$key] = $value; } } } $payment = new OrderPayment; $payment->forceFill([ 'order_id' => $order->id, 'method' => $this->mapPaymentMethod($magentoMethod), 'method_title' => $magentoMethod !== '' ? $magentoMethod : null, 'additional' => $additional, ]); $payment->save(); } /** * @param Collection> $addressRows */ private function importAddresses(Order $order, $addressRows, ?Customer $customer, string $email): int { $imported = 0; foreach ($addressRows as $row) { $type = strtolower(trim((string) ($row['address_type'] ?? ''))); $addressType = $type === 'shipping' ? OrderAddress::ADDRESS_TYPE_SHIPPING : OrderAddress::ADDRESS_TYPE_BILLING; $address = new OrderAddress; $address->forceFill([ 'order_id' => $order->id, 'customer_id' => $customer?->id, 'address_type' => $addressType, 'first_name' => $this->requiredName($row['firstname'] ?? null, $order->customer_first_name), 'last_name' => trim((string) ($row['lastname'] ?? '')) ?: $order->customer_last_name, 'company_name' => $this->nullableString($row['company'] ?? null), 'address' => $this->mapStreet($row['street'] ?? null) ?: '-', 'city' => trim((string) ($row['city'] ?? '')) ?: '-', 'state' => $this->nullableString($row['region'] ?? null), 'country' => $this->nullableString($row['country_id'] ?? null), 'postcode' => $this->nullableString($row['postcode'] ?? null), 'email' => $this->nullableString($row['email'] ?? null) ?? ($email !== '' ? $email : null), 'phone' => $this->nullableString($row['telephone'] ?? null), 'additional' => json_encode(['asteria_address_id' => (int) ($row['entity_id'] ?? 0)]), ]); $address->save(); $imported++; } return $imported; } /** * @param Collection> $itemRows * @param array $productsBySku */ private function importItems(Order $order, $itemRows, array $productsBySku): int { if ($itemRows->isEmpty()) { return 0; } $parents = $itemRows->filter(fn (array $row) => empty($row['parent_item_id'])); $children = $itemRows->filter(fn (array $row) => ! empty($row['parent_item_id'])); $idMap = []; $imported = 0; foreach ($parents as $row) { $item = $this->createOrderItem($order, $row, null, $productsBySku); $idMap[(int) $row['item_id']] = $item->id; $imported++; } foreach ($children as $row) { $parentId = $idMap[(int) $row['parent_item_id']] ?? null; $this->createOrderItem($order, $row, $parentId, $productsBySku); $imported++; } return $imported; } /** * @param array $row * @param array $productsBySku */ private function createOrderItem(Order $order, array $row, ?int $parentId, array $productsBySku): OrderItem { $sku = trim((string) ($row['sku'] ?? '')); $product = $sku !== '' ? ($productsBySku[$sku] ?? null) : null; $type = $this->mapProductType($row['product_type'] ?? null, $product); $price = $this->money($row['price'] ?? 0); $basePrice = $this->money($row['base_price'] ?? $price); $total = $this->money($row['row_total'] ?? 0); $baseTotal = $this->money($row['base_row_total'] ?? $total); $taxAmount = $this->money($row['tax_amount'] ?? 0); $baseTaxAmount = $this->money($row['base_tax_amount'] ?? $taxAmount); $item = new OrderItem; $item->forceFill([ 'order_id' => $order->id, 'parent_id' => $parentId, 'sku' => $sku !== '' ? $sku : null, 'type' => $type, 'name' => $this->nullableString($row['name'] ?? null) ?? '-', 'weight' => $this->money($row['weight'] ?? 0), 'total_weight' => $this->money($row['row_weight'] ?? $row['weight'] ?? 0), 'qty_ordered' => $this->qty($row['qty_ordered'] ?? 0), 'qty_shipped' => $this->qty($row['qty_shipped'] ?? 0), 'qty_invoiced' => $this->qty($row['qty_invoiced'] ?? 0), 'qty_canceled' => $this->qty($row['qty_canceled'] ?? 0), 'qty_refunded' => $this->qty($row['qty_refunded'] ?? 0), 'price' => $price, 'base_price' => $basePrice, 'price_incl_tax' => array_key_exists('price_incl_tax', $row) ? $this->money($row['price_incl_tax']) : $price, 'base_price_incl_tax' => array_key_exists('base_price_incl_tax', $row) ? $this->money($row['base_price_incl_tax']) : $basePrice, 'total' => $total, 'base_total' => $baseTotal, 'total_incl_tax' => array_key_exists('row_total_incl_tax', $row) ? $this->money($row['row_total_incl_tax']) : $total + $taxAmount, 'base_total_incl_tax' => array_key_exists('base_row_total_incl_tax', $row) ? $this->money($row['base_row_total_incl_tax']) : $baseTotal + $baseTaxAmount, 'tax_percent' => $this->money($row['tax_percent'] ?? 0), 'tax_amount' => $taxAmount, 'base_tax_amount' => $baseTaxAmount, 'discount_percent' => $this->money($row['discount_percent'] ?? 0), 'discount_amount' => $this->money($row['discount_amount'] ?? 0), 'base_discount_amount' => $this->money($row['base_discount_amount'] ?? 0), 'product_id' => $product?->id, 'product_type' => $product ? get_class($product) : null, 'additional' => [ 'asteria_item_id' => (int) ($row['item_id'] ?? 0), ], ]); $item->save(); return $item; } /** * @param Collection> $orders * @return array{0: array, 1: array} */ private function loadCustomers($orders): array { $asteriaIds = $orders ->pluck('customer_id') ->filter(fn ($id) => (int) $id > 0) ->map(fn ($id) => (int) $id) ->unique() ->values() ->all(); $emails = $orders ->pluck('customer_email') ->map(fn ($email) => strtolower(trim((string) $email))) ->filter() ->unique() ->values() ->all(); $byAsteriaId = []; $byEmail = []; if ($asteriaIds !== []) { foreach (Customer::query()->whereIn('migrated_from_asteria_id', $asteriaIds)->get() as $customer) { $byAsteriaId[(int) $customer->migrated_from_asteria_id] = $customer; } } if ($emails !== []) { $query = Customer::query(); $query->where(function ($inner) use ($emails) { foreach ($emails as $email) { $inner->orWhereRaw('LOWER(email) = ?', [$email]); } }); foreach ($query->get() as $customer) { $byEmail[strtolower((string) $customer->email)] = $customer; } } return [$byAsteriaId, $byEmail]; } /** * @param Collection>> $items * @return array */ private function loadProducts($items): array { $skus = $items ->flatten(1) ->pluck('sku') ->map(fn ($sku) => trim((string) $sku)) ->filter() ->unique() ->values() ->all(); if ($skus === []) { return []; } return Product::query() ->whereIn('sku', $skus) ->get(['id', 'sku', 'type']) ->keyBy('sku') ->all(); } /** * @param array $row * @param array $customersByAsteriaId * @param array $customersByEmail */ private function resolveCustomer(array $row, array $customersByAsteriaId, array $customersByEmail): ?Customer { $asteriaCustomerId = (int) ($row['customer_id'] ?? 0); if ($asteriaCustomerId > 0 && isset($customersByAsteriaId[$asteriaCustomerId])) { return $customersByAsteriaId[$asteriaCustomerId]; } $email = strtolower(trim((string) ($row['customer_email'] ?? ''))); if ($email !== '' && isset($customersByEmail[$email])) { return $customersByEmail[$email]; } return null; } /** * @param array $row */ private function mapStatus(array $row): string { $status = strtolower(trim((string) ($row['status'] ?? ''))); $state = strtolower(trim((string) ($row['state'] ?? ''))); $value = $status !== '' ? $status : $state; return match ($value) { 'complete' => Order::STATUS_COMPLETED, 'canceled', 'cancelled' => Order::STATUS_CANCELED, 'pending_payment', 'payment_review', 'pending_paypal' => Order::STATUS_PENDING_PAYMENT, 'holded' => Order::STATUS_PENDING, 'fraud' => Order::STATUS_FRAUD, 'closed' => Order::STATUS_CLOSED, 'processing' => Order::STATUS_PROCESSING, default => Order::STATUS_PENDING, }; } private function mapPaymentMethod(string $method): string { $method = strtolower(trim($method)); if ($method === '') { return 'unknown'; } if (in_array($method, ['paypal_express', 'paypal_standard'], true) || str_starts_with($method, 'paypaluk_')) { return 'paypal_standard'; } if ($method === 'checkmo') { return 'moneytransfer'; } if ($method === 'cashondelivery') { return 'cashondelivery'; } if (str_starts_with($method, 'klarna')) { return 'klarna'; } if (str_starts_with($method, 'afterpay') || str_starts_with($method, 'clearpay')) { return 'afterpay'; } return $method; } private function mapProductType(mixed $magentoType, ?Product $product): string { if ($product && in_array($product->type, self::BAGISTO_PRODUCT_TYPES, true)) { return $product->type; } $type = strtolower(trim((string) $magentoType)); if (in_array($type, self::BAGISTO_PRODUCT_TYPES, true)) { return $type; } return 'simple'; } private function mapStreet(mixed $value): string { $value = trim((string) $value); if ($value === '') { return ''; } $lines = preg_split("/\r\n|\n|\r/", $value) ?: []; return implode(', ', array_filter(array_map('trim', $lines))); } private function requiredName(mixed $value, string $fallback): string { $value = trim((string) $value); if ($value !== '') { return $value; } $fallback = trim($fallback); if ($fallback !== '') { $local = strstr($fallback, '@', true); return $local !== false && $local !== '' ? $local : $fallback; } return 'Customer'; } private function nullableString(mixed $value): ?string { $value = trim((string) $value); return $value === '' ? null : $value; } private function money(mixed $value): float { return is_numeric($value) ? (float) $value : 0.0; } private function qty(mixed $value): int { return (int) round((float) $value); } }