'Jumlah harus lebih dari 0.', ]); } $variant = ProductVariant::query()->with('product')->findOrFail($variantId); if ($variant->stock < $quantity) { throw ValidationException::withMessages([ 'quantity' => "Stok bagus tidak mencukupi. Stok saat ini: {$variant->stock} pcs.", ]); } $isOwner = $user->can(Permission::OWNER_VERIFICATIONS_VERIFY->value); if ($isOwner) { $this->executeTransfer($variant, $quantity, $user, $notes); return null; } $existingPending = OwnerVerificationRequest::query() ->where('subject_type', ProductVariant::class) ->where('subject_id', $variant->id) ->where('action', OwnerVerificationAction::RETAIL_STOCK_TRANSFER) ->where('status', OwnerVerificationStatus::PENDING) ->exists(); if ($existingPending) { throw ValidationException::withMessages([ 'quantity' => 'Masih ada pengajuan transfer stok ecer yang menunggu verifikasi untuk varian ini.', ]); } try { $request = OwnerVerificationRequest::create([ 'action' => OwnerVerificationAction::RETAIL_STOCK_TRANSFER, 'status' => OwnerVerificationStatus::PENDING, 'subject_type' => ProductVariant::class, 'subject_id' => $variant->id, 'submitted_by_id' => $user->id, 'payload' => [ 'old' => [ 'stock' => $variant->stock, 'retail_stock' => $variant->retail_stock, ], 'new' => [ 'quantity' => $quantity, 'stock' => $variant->stock - $quantity, 'retail_stock' => $variant->retail_stock + $quantity, 'notes' => $notes, 'variant_name' => $variant->name, 'product_name' => $variant->product?->name ?? '-', ], ], ]); $productName = $variant->product?->name ?? $variant->name; $this->pushNotificationService->sendToRoles( '📦 Transfer Stok Ecer Menunggu Persetujuan Owner', "Pengajuan transfer {$quantity} pcs stok ecer untuk varian '{$variant->name}' menunggu verifikasi owner.", ['owner', 'developer', 'direktur'], route('admin.master.products.index', ['search' => $productName]), ); $this->pushNotificationService->sendToUser( '📤 Pengajuan Transfer Terkirim', "Pengajuan transfer {$quantity} pcs stok ecer untuk varian '{$variant->name}' telah dikirim dan menunggu verifikasi owner.", $user->id, route('admin.master.products.index', ['search' => $productName]), ); return $request; } catch (ValidationException $e) { throw $e; } catch (\Throwable $e) { Log::error('Gagal mengajukan transfer stok ecer: '.$e->getMessage(), [ 'trace' => $e->getTraceAsString(), ]); throw ValidationException::withMessages([ 'system' => 'Terjadi kesalahan pada server. Silakan laporkan masalah ini ke pihak terkait.', ]); } } public function applyRetailStockTransfer(OwnerVerificationRequest $request): void { $payload = $request->payload ?? []; $newData = $payload['new'] ?? []; $quantity = (int) ($newData['quantity'] ?? 0); if ($quantity <= 0) { return; } $variant = ProductVariant::query()->find($request->subject_id); if (! $variant) { return; } $this->executeTransfer($variant, $quantity, $request->submittedBy, $newData['notes'] ?? null); } private function executeTransfer(ProductVariant $variant, int $quantity, User $user, ?string $notes = null): void { DB::transaction(function () use ($variant, $quantity, $user, $notes): void { $variant = ProductVariant::query() ->whereKey($variant->id) ->lockForUpdate() ->firstOrFail(); $stockBefore = $variant->stock; $retailStockBefore = $variant->retail_stock; $variant->decrement('stock', $quantity); $variant->increment('retail_stock', $quantity); RetailStockHistory::create([ 'product_variant_id' => $variant->id, 'user_id' => $user->id, 'quantity' => $quantity, 'stock_before' => $stockBefore, 'retail_stock_before' => $retailStockBefore, 'stock_after' => $stockBefore - $quantity, 'retail_stock_after' => $retailStockBefore + $quantity, 'notes' => $notes, 'created_at' => now(), ]); }); } public function rejectRetailStockTransfer(OwnerVerificationRequest $request): void { // No-op: nothing was changed yet, so nothing to rollback. } }