diff --git a/app/Console/Commands/FixDataCommand.php b/app/Console/Commands/FixDataCommand.php new file mode 100644 index 0000000..82cebfb --- /dev/null +++ b/app/Console/Commands/FixDataCommand.php @@ -0,0 +1,104 @@ +glob($fixPath.'/*.php')) + ->map(fn ($file) => basename($file)) + ->filter(fn ($file) => $file !== 'BaseDataFix.php') + ->toArray(); + + $fixes = []; + + foreach ($files as $file) { + require_once $fixPath.'/'.$file; + + $className = 'App\\DataFixes\\'.str_replace('.php', '', $file); + + if (! class_exists($className)) { + continue; + } + + $instance = new $className; + + if ($instance instanceof BaseDataFix) { + $fixes[$instance->key()] = $instance; + } + } + + $only = $this->option('only') ? explode(',', $this->option('only')) : null; + $skip = $this->option('skip') ? explode(',', $this->option('skip')) : []; + + $this->newLine(); + $this->info('=== Fix Data ==='); + $this->newLine(); + + $summary = []; + $totalFixed = 0; + $totalSkipped = 0; + + foreach ($fixes as $key => $fix) { + if ($only !== null && ! in_array($key, $only)) { + continue; + } + + if (in_array($key, $skip)) { + $this->line("SKIP {$key} — {$fix->description()}"); + + continue; + } + + $fix->setCommand($this); + $this->info("Running: {$fix->description()}"); + + $result = $fix->fix(); + + $fixed = $result['fixed'] ?? 0; + $skipped = $result['skipped'] ?? 0; + $total = $result['total'] ?? 0; + + $summary[$key] = [ + 'Fix' => $key, + 'Total' => $total, + 'Fixed' => $fixed, + 'Skipped' => $skipped, + ]; + + $totalFixed += $fixed; + $totalSkipped += $skipped; + + $this->line(" OK Fixed: {$fixed}, Skipped: {$skipped}, Total: {$total}"); + $this->newLine(); + } + + $this->info('=== Summary ==='); + $this->info("Total fixed: {$totalFixed}"); + $this->info("Total skipped: {$totalSkipped}"); + $this->newLine(); + + if (! empty($summary)) { + $this->table( + ['Fix', 'Total', 'Fixed', 'Skipped'], + array_values($summary) + ); + } + + return Command::SUCCESS; + } +} diff --git a/app/DataFixes/BaseDataFix.php b/app/DataFixes/BaseDataFix.php new file mode 100644 index 0000000..9e154d7 --- /dev/null +++ b/app/DataFixes/BaseDataFix.php @@ -0,0 +1,28 @@ +command = $command; + + return $this; + } + + abstract public function key(): string; + + abstract public function description(): string; + + abstract public function fix(): array; + + protected function info(string $message): void + { + $this->command->line(" {$message}"); + } +} diff --git a/app/DataFixes/FixCuttingsCost.php b/app/DataFixes/FixCuttingsCost.php new file mode 100644 index 0000000..41f6abd --- /dev/null +++ b/app/DataFixes/FixCuttingsCost.php @@ -0,0 +1,85 @@ +get(); + + $fixed = 0; + $skipped = 0; + $details = []; + + foreach ($cuttings as $cutting) { + $oldTotal = $cutting->total_material_cost; + $oldPerUnit = $cutting->cost_per_unit; + + $totalMaterialCost = 0; + foreach ($cutting->cuttingMaterials as $material) { + $price = $material->rawMaterialPrice; + if ($price) { + $totalMaterialCost += $price->price * ($material->material_usage ?? 0); + } + } + + $cuttingResult = $cutting->cuttingResults->sum('cutting_result'); + $costPerUnit = 0; + if ($cuttingResult > 0) { + $costPerUnit = (int) ($totalMaterialCost / $cuttingResult); + } + + if ($oldTotal === $totalMaterialCost && $oldPerUnit === $costPerUnit) { + $skipped++; + + continue; + } + + $cutting->update([ + 'total_material_cost' => $totalMaterialCost, + 'cost_per_unit' => $costPerUnit, + ]); + + $details[] = [ + 'ID' => $cutting->id, + 'Description' => $cutting->description ?? '-', + 'Old Total' => number_format($oldTotal ?? 0), + 'New Total' => number_format($totalMaterialCost), + 'Old/Unit' => number_format($oldPerUnit ?? 0), + 'New/Unit' => number_format($costPerUnit), + ]; + + $fixed++; + } + + if (! empty($details)) { + $this->command->newLine(); + $this->command->table( + ['ID', 'Description', 'Old Total', 'New Total', 'Old/Unit', 'New/Unit'], + $details + ); + } + + return [ + 'fixed' => $fixed, + 'skipped' => $skipped, + 'total' => $cuttings->count(), + ]; + } +} diff --git a/app/DataFixes/FixOrderItemsUnitPrice.php b/app/DataFixes/FixOrderItemsUnitPrice.php new file mode 100644 index 0000000..0edc299 --- /dev/null +++ b/app/DataFixes/FixOrderItemsUnitPrice.php @@ -0,0 +1,126 @@ +with(['order', 'productVariant.productPrices']) + ->get(); + + $fixed = 0; + $skipped = 0; + $notFound = 0; + $details = []; + + foreach ($items as $item) { + $order = $item->order; + if (! $order) { + $skipped++; + + continue; + } + + $priceType = $this->resolvePriceType($order->price_type->value, $item->stock_quality->value); + $productPrices = $item->productVariant?->productPrices; + + if (! $productPrices) { + $notFound++; + $details[] = [ + 'Order' => $order->order_number, + 'Variant' => $item->product_variant_id, + 'Qty' => $item->quantity, + 'Old Price' => 0, + 'New Price' => 'N/A', + 'Status' => 'No prices found', + ]; + + continue; + } + + $correctPrice = $productPrices->firstWhere('type', $priceType); + + if (! $correctPrice || $correctPrice->price <= 0) { + $notFound++; + $details[] = [ + 'Order' => $order->order_number, + 'Variant' => $item->product_variant_id, + 'Qty' => $item->quantity, + 'Old Price' => 0, + 'New Price' => 'N/A', + 'Status' => "No {$priceType} price", + ]; + + continue; + } + + $newSubtotal = $correctPrice->price * $item->quantity; + + $item->update([ + 'unit_price' => $correctPrice->price, + 'subtotal' => $newSubtotal, + ]); + + $details[] = [ + 'Order' => $order->order_number, + 'Variant' => $item->product_variant_id, + 'Qty' => $item->quantity, + 'Old Price' => 0, + 'New Price' => number_format($correctPrice->price), + 'Status' => 'Fixed', + ]; + + $fixed++; + } + + if (! empty($details)) { + $this->command->newLine(); + $this->command->table( + ['Order', 'Variant', 'Qty', 'Old Price', 'New Price', 'Status'], + $details + ); + } + + return [ + 'fixed' => $fixed, + 'skipped' => $skipped, + 'not_found' => $notFound, + 'total' => $items->count(), + ]; + } + + private function resolvePriceType(string $orderPriceType, string $stockQuality): string + { + if ($stockQuality === ProductStockQuality::REJECT->value) { + return PriceType::REJECT->value; + } + + $map = [ + PriceType::DISTRIBUTOR->value => PriceType::DISTRIBUTOR->value, + PriceType::AGEN->value => PriceType::AGEN->value, + PriceType::SUB_AGEN->value => PriceType::SUB_AGEN->value, + PriceType::WHOLESALE->value => PriceType::WHOLESALE->value, + PriceType::RETAIL->value => PriceType::RETAIL->value, + PriceType::TIKTOK->value => PriceType::TIKTOK->value, + PriceType::SHOPEE->value => PriceType::SHOPEE->value, + ]; + + return $map[$orderPriceType] ?? PriceType::RETAIL->value; + } +} diff --git a/app/DataFixes/FixOrdersCogs.php b/app/DataFixes/FixOrdersCogs.php new file mode 100644 index 0000000..89618f6 --- /dev/null +++ b/app/DataFixes/FixOrdersCogs.php @@ -0,0 +1,102 @@ +where('status', '!=', 'cancelled') + ->with(['orderItems.productVariant.productPrices']) + ->get(); + + $fixed = 0; + $skipped = 0; + $noItems = 0; + $details = []; + + foreach ($orders as $order) { + if ($order->orderItems->isEmpty()) { + $noItems++; + + continue; + } + + $totalCogs = 0; + $hasValidCapital = true; + + foreach ($order->orderItems as $item) { + $capitalPrice = $item->productVariant?->productPrices + ->firstWhere('type', 'capital'); + + if (! $capitalPrice || $capitalPrice->price <= 0) { + $hasValidCapital = false; + break; + } + + $totalCogs += $capitalPrice->price * $item->quantity; + } + + if (! $hasValidCapital) { + $skipped++; + $details[] = [ + 'Order' => $order->order_number, + 'Items' => $order->orderItems->count(), + 'Old COGS' => '0', + 'New COGS' => 'N/A', + 'Status' => 'Missing capital price', + ]; + + continue; + } + + if ($totalCogs === 0) { + $skipped++; + + continue; + } + + $order->update([ + 'cogs' => $totalCogs, + ]); + + $details[] = [ + 'Order' => $order->order_number, + 'Items' => $order->orderItems->count(), + 'Old COGS' => '0', + 'New COGS' => number_format($totalCogs), + 'Status' => 'Fixed', + ]; + + $fixed++; + } + + if (! empty($details)) { + $this->command->newLine(); + $this->command->table( + ['Order', 'Items', 'Old COGS', 'New COGS', 'Status'], + $details + ); + } + + return [ + 'fixed' => $fixed, + 'skipped' => $skipped, + 'no_items' => $noItems, + 'total' => $orders->count(), + ]; + } +} diff --git a/app/DataFixes/FixOrdersNegoPrice.php b/app/DataFixes/FixOrdersNegoPrice.php new file mode 100644 index 0000000..40aafcf --- /dev/null +++ b/app/DataFixes/FixOrdersNegoPrice.php @@ -0,0 +1,75 @@ +', 0) + ->where('status', '!=', 'cancelled') + ->get(); + + $fixed = 0; + $skipped = 0; + $details = []; + + foreach ($orders as $order) { + $oldNego = $order->nego_price; + $oldTotal = $order->total_amount; + + $correctNego = $order->subtotal - $order->discount - $order->total_amount; + + if ($correctNego < 0) { + $correctNego = 0; + } + + if ($oldNego === $correctNego) { + $skipped++; + + continue; + } + + $order->update([ + 'nego_price' => $correctNego, + ]); + + $details[] = [ + 'Order' => $order->order_number, + 'Subtotal' => number_format($order->subtotal), + 'Discount' => number_format($order->discount), + 'Old Nego' => number_format($oldNego), + 'New Nego' => number_format($correctNego), + 'Total' => number_format($oldTotal), + ]; + + $fixed++; + } + + if (! empty($details)) { + $this->command->newLine(); + $this->command->table( + ['Order', 'Subtotal', 'Discount', 'Old Nego', 'New Nego', 'Total'], + $details + ); + } + + return [ + 'fixed' => $fixed, + 'skipped' => $skipped, + 'total' => $orders->count(), + ]; + } +} diff --git a/app/Models/Cutting.php b/app/Models/Cutting.php index 255bd6d..d843114 100644 --- a/app/Models/Cutting.php +++ b/app/Models/Cutting.php @@ -98,7 +98,7 @@ public function cuttingResults(): HasMany { return $this->hasMany(CuttingResult::class); } - + public function registerMediaCollections(): void { $this->addMediaCollection('photos');