*/ 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', ]) ->completed() ->latest() ->get() ->each(function (Cutting $cutting): void { $this->appendCostPreview($cutting); }); } /** * Get all cuttings pending owner approval. * * @return Collection */ public function getPendingApprovalCuttings(User $user): Collection { return Cutting::query() ->with([ 'createdBy.profile', 'submittedBy.profile', 'rejection.rejectedBy.profile', 'materials.rawMaterialPrice.rawMaterial:id,name,unit', 'results.productVariant.product:id,name', 'results.productVariant:id,product_id,name', 'resultPrices.productVariant:id,product_id,name', ]) ->pendingVerification() ->latest() ->get() ->each(function (Cutting $cutting): void { $this->appendCostPreview($cutting); }); } /** * Submit verification for a completed cutting - saves data but does NOT add stock yet. * * @param list|null $results * @param list}>|null $resultPrices */ public function submitVerification( 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.', ]); } try { 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->storeResultPrices($cutting, $resultPrices ?? []); if ($verificationNote !== null && trim($verificationNote) !== '') { $cutting->rejection()->create([ 'reason' => trim($verificationNote), 'rejected_by_id' => $user->id, ]); } $cutting->submitted_by_id = $user->id; $cutting->status = CuttingStatus::PENDING_VERIFICATION; $cutting->save(); }); } catch (ValidationException $e) { throw $e; } catch (\Throwable $e) { Log::error('Gagal mengajukan verifikasi stok: '.$e->getMessage(), [ 'trace' => $e->getTraceAsString(), ]); throw ValidationException::withMessages([ 'system' => 'Terjadi kesalahan pada server. Silakan laporkan masalah ini ke pihak terkait.', ]); } $description = $cutting->description ?? '-'; $this->pushNotificationService->sendToRoles( '📦 Verifikasi Stok Menunggu Persetujuan', "Cutting dengan deskripsi '{$description}' telah diajukan verifikasi dan menunggu persetujuan owner.", ['owner', 'developer'], route('admin.manage.owner_verifications.index'), ); } /** * Owner approves verification - adds stock to store. */ public function approveVerification( Cutting $cutting, User $user, ?string $approvalNote = null, ): void { if ($cutting->status !== CuttingStatus::PENDING_VERIFICATION) { throw ValidationException::withMessages([ 'status' => 'Hanya cutting yang menunggu verifikasi yang dapat disetujui.', ]); } try { DB::transaction(function () use ($cutting, $user, $approvalNote): void { $cutting->load(['materials.rawMaterialPrice', 'results', 'resultPrices']); $this->applyProductStockOnVerify($cutting); $this->applyResultPricesToProducts($cutting); if ($approvalNote !== null && trim($approvalNote) !== '') { $cutting->rejection()->create([ 'reason' => trim($approvalNote), 'rejected_by_id' => $user->id, ]); } $cutting->status = CuttingStatus::VERIFIED; $cutting->save(); }); } catch (ValidationException $e) { throw $e; } catch (\Throwable $e) { Log::error('Gagal menyetujui verifikasi stok: '.$e->getMessage(), [ 'trace' => $e->getTraceAsString(), ]); throw ValidationException::withMessages([ 'system' => 'Terjadi kesalahan pada server. Silakan laporkan masalah ini ke pihak terkait.', ]); } $description = $cutting->description ?? '-'; $this->pushNotificationService->sendToRoles( '📦 Stok Cutting Diverifikasi', "Cutting dengan deskripsi '{$description}' telah disetujui owner dan stok produk telah ditambahkan ke toko.", ['owner', 'developer'], route('admin.manage.owner_verifications.index'), ); } /** * Owner rejects verification - sends back to completed. */ public function rejectVerification( Cutting $cutting, User $user, string $reason, ): void { if ($cutting->status !== CuttingStatus::PENDING_VERIFICATION) { throw ValidationException::withMessages([ 'status' => 'Hanya cutting yang menunggu verifikasi yang dapat ditolak.', ]); } try { DB::transaction(function () use ($cutting, $user, $reason): void { $cutting->rejection()->create([ 'reason' => trim($reason), 'rejected_by_id' => $user->id, ]); $cutting->status = CuttingStatus::COMPLETED; $cutting->save(); }); } catch (ValidationException $e) { throw $e; } catch (\Throwable $e) { Log::error('Gagal menolak verifikasi stok: '.$e->getMessage(), [ 'trace' => $e->getTraceAsString(), ]); throw ValidationException::withMessages([ 'system' => 'Terjadi kesalahan pada server. Silakan laporkan masalah ini ke pihak terkait.', ]); } $description = $cutting->description ?? '-'; $this->pushNotificationService->sendToRoles( '📦 Verifikasi Cutting Ditolak Owner', "Cutting dengan deskripsi '{$description}' ditolak oleh owner dengan alasan: '{$reason}'.", ['owner', 'developer'], route('admin.manage.owner_verifications.index'), ); if ($cutting->submitted_by_id) { $this->pushNotificationService->sendToUser( '📦 Verifikasi Cutting Ditolak Owner', "Cutting dengan deskripsi '{$description}' yang Anda verifikasi ditolak oleh owner dengan alasan: '{$reason}'.", $cutting->submitted_by_id, route('admin.manage.owner_verifications.index'), ); } } 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); } } } /** * @param list}> $resultPrices */ private function storeResultPrices(Cutting $cutting, array $resultPrices): void { // Extract harga_modal from the first variant's prices and set as cost_per_unit $costPerUnit = 0; foreach ($resultPrices as $resultData) { foreach ($resultData['prices'] as $priceData) { if ($priceData['type'] === 'harga_modal' && (int) $priceData['price'] > 0) { $costPerUnit = (int) $priceData['price']; break 2; } } } if ($costPerUnit > 0) { $cutting->cost_per_unit = $costPerUnit; } else { $costPerUnit = (int) ($cutting->cost_per_unit ?? 0); } foreach ($resultPrices as $resultData) { foreach ($resultData['prices'] as $priceData) { if ((int) $priceData['price'] > 0) { // Store/update to cutting_result_prices CuttingResultPrice::query()->updateOrCreate( [ 'cutting_id' => $cutting->id, 'product_variant_id' => $resultData['product_variant_id'], 'price_type' => $priceData['type'], ], [ 'price' => (int) $priceData['price'], 'cost_per_unit' => $costPerUnit, ], ); } } } } /** * Update product_prices from cutting result prices (called on owner approval). */ private function applyResultPricesToProducts(Cutting $cutting): void { $resultPrices = $cutting->resultPrices()->with('productVariant')->get(); foreach ($resultPrices as $resultPrice) { if ($resultPrice->price > 0) { ProductPrice::query()->updateOrCreate( [ 'variant_id' => $resultPrice->product_variant_id, 'type' => $resultPrice->price_type->value, ], [ 'price' => $resultPrice->price, ], ); } } } 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, ',', '.')); } }