*/ private const ORDER_TABLES = [ 'refund_items', 'refunds', 'invoice_items', 'invoices', 'shipment_items', 'shipments', 'order_comments', 'order_transactions', 'order_payment', 'downloadable_product_download_links', 'downloadable_link_purchased', 'order_items', 'notifications', 'product_ordered_inventories', 'orders', ]; /** * Rows that keep their snapshot but must not block deleting orders. * * @var list */ private const DETACH_ORDER_ID = [ ['bookings', 'order_id'], ['bookings', 'order_item_id'], ['gift_card_usage_logs', 'order_id'], ['mw_growth_value_history', 'order_id'], ['mw_reward_point_history', 'history_order_id'], ['member_log', 'order_id'], ['payment_attempts', 'order_id'], ]; /** @var list */ private const ORDER_ADDRESS_TYPES = [ 'order_billing', 'order_shipping', 'invoice_billing', 'invoice_shipping', ]; public function handle(): int { $dryRun = (bool) $this->option('dry-run'); $tables = $this->existingTables(self::ORDER_TABLES); $this->warn('This clears sales orders so they can be re-imported.'); $this->line('Kept: customers, products, carts, customer groups.'); $this->line('Removed: orders, items, payments, invoices, shipments, refunds, order addresses.'); $this->newLine(); $rows = $this->countRows($tables); $this->table(['table', 'rows'], collect($rows)->map(fn ($count, $table) => [$table, $count])->values()->all()); $total = array_sum($rows); $this->info("Order rows that would be truncated: {$total}"); $addressCount = $this->countOrderAddresses(); if ($addressCount > 0) { $this->comment("addresses (order/invoice): {$addressCount}"); } $detachCounts = $this->countDetachRows(); if ($detachCounts !== []) { $this->newLine(); $this->comment('Related rows whose order_id will be set to NULL:'); $this->table(['table.column', 'rows'], collect($detachCounts)->map(fn ($count, $key) => [$key, $count])->values()->all()); } if ($dryRun) { $this->info('Dry run — nothing written.'); return self::SUCCESS; } if (! $this->option('force') && ! $this->confirm('Truncate order tables now?', false)) { $this->info('Aborted.'); return self::SUCCESS; } $driver = DB::getDriverName(); try { $this->disableForeignKeyChecks($driver); $this->deleteOrderAddresses(); $this->detachHistoricalOrderIds(); foreach ($tables as $table) { $this->emptyTable($driver, $table); } } finally { $this->enableForeignKeyChecks($driver); } Cache::forget(self::PROGRESS_KEY); $this->newLine(); $this->info('Order reset finished. Next:'); $this->line(' php artisan orders:migrate-asteria --reset-progress'); return self::SUCCESS; } /** * @param list $tables * @return list */ private function existingTables(array $tables): array { return array_values(array_filter($tables, fn (string $table) => Schema::hasTable($table))); } /** * @param list $tables * @return array */ private function countRows(array $tables): array { $counts = []; foreach ($tables as $table) { $counts[$table] = (int) DB::table($table)->count(); } return $counts; } /** * @return array */ private function countDetachRows(): array { $counts = []; foreach (self::DETACH_ORDER_ID as [$table, $column]) { if (! Schema::hasTable($table) || ! Schema::hasColumn($table, $column)) { continue; } $count = (int) DB::table($table)->whereNotNull($column)->where($column, '!=', 0)->count(); if ($count > 0) { $counts[$table.'.'.$column] = $count; } } return $counts; } private function countOrderAddresses(): int { if (! Schema::hasTable('addresses')) { return 0; } return (int) $this->orderAddressQuery()->count(); } private function deleteOrderAddresses(): void { if (! Schema::hasTable('addresses')) { return; } $this->orderAddressQuery()->delete(); } private function orderAddressQuery() { $query = DB::table('addresses'); return $query->where(function ($builder) { if (Schema::hasColumn('addresses', 'order_id')) { $builder->orWhereNotNull('order_id'); } if (Schema::hasColumn('addresses', 'address_type')) { $builder->orWhereIn('address_type', self::ORDER_ADDRESS_TYPES); } }); } private function detachHistoricalOrderIds(): void { foreach (self::DETACH_ORDER_ID as [$table, $column]) { if (! Schema::hasTable($table) || ! Schema::hasColumn($table, $column)) { continue; } DB::table($table)->whereNotNull($column)->update([$column => null]); } } private function emptyTable(string $driver, string $table): void { if ($driver === 'mysql') { DB::statement('TRUNCATE TABLE '.$this->quoteTable($table)); return; } DB::table($table)->delete(); if ($driver === 'sqlite') { DB::table('sqlite_sequence')->where('name', DB::getTablePrefix().$table)->delete(); } } private function quoteTable(string $table): string { $name = DB::getTablePrefix().$table; return '`'.str_replace('`', '``', $name).'`'; } private function disableForeignKeyChecks(string $driver): void { if ($driver === 'mysql') { DB::statement('SET FOREIGN_KEY_CHECKS=0'); } elseif ($driver === 'sqlite') { DB::statement('PRAGMA foreign_keys = OFF'); } } private function enableForeignKeyChecks(string $driver): void { if ($driver === 'mysql') { DB::statement('SET FOREIGN_KEY_CHECKS=1'); } elseif ($driver === 'sqlite') { DB::statement('PRAGMA foreign_keys = ON'); } } }