*/ public function getPendingVerificationCuttings(User $user): Collection { return Cutting::query() ->with([ 'createdBy.profile', 'rejection.rejectedBy.profile', 'materials.rawMaterialPrice.rawMaterial:id,name,unit', 'results.productVariant.product:id,name', 'results.productVariant:id,product_id,name', ]) ->where('status', CuttingStatus::COMPLETED) ->latest() ->get() ->each(function (Cutting $cutting): void { $this->appendCostPreview($cutting); }); } /** * Get all verified cuttings (stock history). * * @return Collection */ public function getVerifiedCuttings(): Collection { return Cutting::query() ->with([ 'createdBy.profile', 'rejection.rejectedBy.profile', 'materials.rawMaterialPrice.rawMaterial:id,name,unit', 'results.productVariant.product:id,name', 'results.productVariant:id,product_id,name', ]) ->where('status', CuttingStatus::VERIFIED) ->latest() ->limit(50) ->get() ->each(function (Cutting $cutting): void { $this->appendCostPreview($cutting); }); } /** * Verify a completed cutting - adds stock to store. * * @param list|null $results * @param list}>|null $resultPrices */ public function verify( Cutting $cutting, User $user, ?string $verificationNote = null, ?array $results = null, ?array $resultPrices = null, ): void { if ($cutting->status !== CuttingStatus::COMPLETED) { throw ValidationException::withMessages([ 'status' => 'Hanya cutting yang sudah selesai yang dapat diverifikasi.', ]); } DB::transaction(function () use ($cutting, $user, $verificationNote, $results, $resultPrices): void { $cutting->load(['materials.rawMaterialPrice', 'results']); if ($results !== null) { foreach ($results as $item) { $cutting->results() ->where('product_variant_id', $item['product_variant_id']) ->update([ 'warehouse_stock' => $item['warehouse_stock'], 'cutting_reject' => $item['cutting_reject'], ]); } $cutting->load('results'); } $this->applyProductStockOnVerify($cutting); $this->storeResultPrices($cutting, $resultPrices ?? []); if ($verificationNote !== null && trim($verificationNote) !== '') { $cutting->rejection()->create([ 'reason' => trim($verificationNote), 'rejected_by_id' => $user->id, ]); } $cutting->status = CuttingStatus::VERIFIED; $cutting->save(); }); $description = $cutting->description ?? '-'; $this->pushNotificationService->sendToRoles( '📦 Stok Cutting Diverifikasi', "Cutting dengan deskripsi '{$description}' telah diverifikasi dan stok produk telah ditambahkan ke toko.", ['owner', 'developer'], '/admin/manage/stocks', ); } /** * Reject a completed cutting - sends back to in_progress. */ public function reject(Cutting $cutting, User $user, string $reason): void { if ($cutting->status !== CuttingStatus::COMPLETED) { throw ValidationException::withMessages([ 'status' => 'Hanya cutting yang sudah selesai yang dapat ditolak.', ]); } DB::transaction(function () use ($cutting, $user, $reason): void { $cutting->load(['materials.rawMaterialPrice', 'results']); $this->deductRemainingMaterialStock($cutting); $cutting->rejection()->create([ 'reason' => trim($reason), 'rejected_by_id' => $user->id, ]); $cutting->status = CuttingStatus::IN_PROGRESS; $cutting->save(); }); $description = $cutting->description ?? '-'; $this->pushNotificationService->sendToRoles( '📦 Verifikasi Cutting Ditolak', "Cutting dengan deskripsi '{$description}' ditolak dari verifikasi stok dengan alasan: '{$reason}'.", ['owner', 'developer'], '/admin/manage/stocks', ); } private function applyProductStockOnVerify(Cutting $cutting): void { foreach ($cutting->results as $result) { if ($result->warehouse_stock > 0) { ProductVariant::query() ->whereKey($result->product_variant_id) ->increment('stock', $result->warehouse_stock); } if ($result->cutting_reject > 0) { ProductVariant::query() ->whereKey($result->product_variant_id) ->increment('reject_stock', $result->cutting_reject); } } } private function deductRemainingMaterialStock(Cutting $cutting): void { foreach ($cutting->materials as $material) { if ((float) $material->remaining_material > 0) { RawMaterialPrice::query() ->whereKey($material->raw_material_price_id) ->decrement('stock', $material->remaining_material); } } } /** * @param list}> $resultPrices */ private function storeResultPrices(Cutting $cutting, array $resultPrices): void { $costPerUnit = (int) ($cutting->cost_per_unit ?? 0); foreach ($resultPrices as $resultData) { foreach ($resultData['prices'] as $priceData) { if ((int) $priceData['price'] > 0) { CuttingResultPrice::query()->create([ 'cutting_id' => $cutting->id, 'product_variant_id' => $resultData['product_variant_id'], 'price_type' => $priceData['type'], 'price' => (int) $priceData['price'], 'cost_per_unit' => $costPerUnit, ]); } } } } private function appendCostPreview(Cutting $cutting): void { $cutting->setAttribute('total_result_pieces', (int) $cutting->results->sum('cutting_result')); $cutting->setAttribute('total_material_usage', (float) $cutting->materials->sum('material_usage')); $totalMaterialCost = $cutting->total_material_cost ?? 0; $sewingCost = (int) ($cutting->sewing_cost ?? 0); $otherCost = (int) ($cutting->other_cost ?? 0); $totalProductionCost = $totalMaterialCost + $sewingCost + $otherCost; $costPerUnit = $cutting->cost_per_unit ?? 0; $cutting->setAttribute('total_material_cost', $totalMaterialCost); $cutting->setAttribute('total_material_cost_formatted', 'Rp '.number_format($totalMaterialCost, 0, ',', '.')); $cutting->setAttribute('sewing_cost', $sewingCost); $cutting->setAttribute('sewing_cost_formatted', 'Rp '.number_format($sewingCost, 0, ',', '.')); $cutting->setAttribute('other_cost', $otherCost); $cutting->setAttribute('other_cost_formatted', 'Rp '.number_format($otherCost, 0, ',', '.')); $cutting->setAttribute('total_production_cost', $totalProductionCost); $cutting->setAttribute('total_production_cost_formatted', 'Rp '.number_format($totalProductionCost, 0, ',', '.')); $cutting->setAttribute('estimated_cost_per_unit', $costPerUnit); $cutting->setAttribute('estimated_cost_per_unit_formatted', 'Rp '.number_format($costPerUnit, 0, ',', '.')); } }