value => 'stock', ProductStockQuality::REJECT->value => 'reject_stock', ]; public function __construct( private S3PresignedService $s3Service = new S3PresignedService, ) {} public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc'): LengthAwarePaginator { $paginator = Restock::query() ->select('id', 'created_by_id', 'subtotal', 'total', 'notes', 'stock_type', 'created_at') ->with([ 'createdBy:id', 'createdBy.userProfile:id,user_id,full_name', 'restockItems:id,restock_id,product_variant_id,quantity,unit_price,subtotal', 'restockItems.productVariant:id,product_id,name,stock,reject_stock,retail_stock', 'restockItems.productVariant.product:id,name', ]) ->when($search, function ($q) use ($search) { $q->whereHas('restockItems.productVariant.product', fn ($sq) => $sq->where('name', 'like', "%{$search}%")) ->orWhere('notes', 'like', "%{$search}%"); }) ->orderBy($sort, $direction) ->paginate($perPage); $paginator->getCollection()->each(function (Restock $restock) { $restock->restockItems->each(function (RestockItem $item) { if (! $item->productVariant) { return; } $media = $item->productVariant->getFirstMedia('photos'); $item->productVariant->photo_url = $media ? $this->s3Service->getTemporaryUrl($media->file_name) : null; }); }); return $paginator; } public function getForCreate(): array { return [ 'products' => Product::query() ->select('id', 'name', 'status') ->with([ 'productVariants:id,product_id,name,stock,reject_stock', 'productVariants.productPrices:id,variant_id,type,price', ]) ->orderBy('name') ->get() ->each(function (Product $product) { $product->productVariants->each(function (ProductVariant $variant) { $media = $variant->getFirstMedia('photos'); $variant->photo_url = $media ? $this->s3Service->getTemporaryUrl($media->file_name) : null; $capitalPrice = $variant->productPrices ->first(fn ($price) => $price->type === PriceType::CAPITAL); $variant->capital_price = $capitalPrice?->price ?? 0; }); }), ]; } public function getForEdit(Restock $restock): array { $restock->load('restockItems.productVariant.product'); $media = $restock->getFirstMedia('photos'); return [ 'id' => $restock->id, 'stock_type' => $restock->stock_type->value, 'notes' => $restock->notes, 'photo_key' => $media?->file_name, 'photo_url' => $media ? $this->s3Service->getTemporaryUrl($media->file_name) : null, 'items' => $restock->restockItems->map(fn (RestockItem $item) => [ 'id' => $item->id, 'product_variant_id' => $item->product_variant_id, 'quantity' => $item->quantity, 'unit_price' => $item->unit_price, ])->values(), ]; } public function create(array $data): Restock { return DB::transaction(function () use ($data) { $now = now(); $subtotal = 0; $stockType = $data['stock_type'] ?? ProductStockQuality::GOOD->value; $itemRows = $this->buildItemRows($data['items'], $now, $subtotal); $restock = Restock::create([ 'created_by_id' => auth()->id(), 'subtotal' => $subtotal, 'total' => $subtotal, 'notes' => $data['notes'] ?? null, 'stock_type' => $stockType, ]); foreach ($itemRows as &$row) { $row['restock_id'] = $restock->id; } DB::table('restock_items')->insert($itemRows); $this->applyStock($data['items'], $stockType, 1); $this->syncPhoto($restock, $data); NotificationService::notify( roles: ['Owner', 'Developer', 'Admin Toko'], title: 'Restock Baru', body: 'Restock '.($stockType === ProductStockQuality::GOOD->value ? 'produk' : 'reject').' sebesar Rp '.number_format($subtotal, 0, ',', '.').' berhasil dicatat oleh '.auth()->user()->full_name.'.', url: route('admin.manage.restocks.index'), ); return $restock; }); } public function update(Restock $restock, array $data): Restock { return DB::transaction(function () use ($restock, $data) { $restock->load('restockItems'); $restock->restockItems->each(function (RestockItem $item) use ($restock) { $this->adjustVariantStock($item->product_variant_id, $item->quantity, -1, $restock->stock_type->value); }); $restock->restockItems()->delete(); $now = now(); $subtotal = 0; $stockType = $data['stock_type'] ?? $restock->stock_type->value; $itemRows = $this->buildItemRows($data['items'], $now, $subtotal); foreach ($itemRows as &$row) { $row['restock_id'] = $restock->id; } DB::table('restock_items')->insert($itemRows); $restock->update([ 'subtotal' => $subtotal, 'total' => $subtotal, 'notes' => $data['notes'] ?? null, 'stock_type' => $stockType, ]); $this->applyStock($data['items'], $stockType, 1); $this->syncPhoto($restock, $data); return $restock; }); } public function delete(Restock $restock): bool { return DB::transaction(function () use ($restock) { $restock->load('restockItems'); $restock->restockItems->each(function (RestockItem $item) use ($restock) { $this->adjustVariantStock($item->product_variant_id, $item->quantity, -1, $restock->stock_type->value); }); $restock->restockItems()->delete(); $restock->clearMediaCollection('photos'); $restock->delete(); return true; }); } private function buildItemRows(array $items, $now, int &$subtotal): array { $variantIds = collect($items)->pluck('product_variant_id')->unique()->all(); $capitalPrices = ProductVariant::query() ->whereKey($variantIds) ->with('productPrices:id,variant_id,type,price') ->get() ->mapWithKeys(function (ProductVariant $variant) { $capitalPrice = $variant->productPrices ->first(fn ($price) => $price->type === PriceType::CAPITAL); return [$variant->id => $capitalPrice?->price ?? 0]; }); return collect($items)->map(function ($item) use ($now, $capitalPrices, &$subtotal) { $quantity = (int) $item['quantity']; $unitPrice = (int) ($capitalPrices[$item['product_variant_id']] ?? 0); $itemSubtotal = $unitPrice * $quantity; $subtotal += $itemSubtotal; return [ 'restock_id' => null, 'user_id' => auth()->id(), 'product_variant_id' => $item['product_variant_id'], 'quantity' => $quantity, 'unit_price' => $unitPrice, 'subtotal' => $itemSubtotal, 'created_at' => $now, 'updated_at' => $now, ]; })->toArray(); } private function applyStock(array $items, string $stockType, int $sign): void { foreach ($items as $item) { $this->adjustVariantStock($item['product_variant_id'], $item['quantity'], $sign, $stockType); } } private function adjustVariantStock(int $variantId, int $quantity, int $sign, string $stockType): void { $field = self::QUALITY_STOCK_MAP[$stockType] ?? 'stock'; if ($sign > 0) { ProductVariant::whereKey($variantId)->increment($field, $quantity); } else { ProductVariant::whereKey($variantId)->decrement($field, $quantity); } } private function syncPhoto(Restock $restock, array $data): void { if (! array_key_exists('photo_key', $data)) { return; } $currentKey = $restock->getFirstMedia('photos')?->file_name; if ($data['photo_key'] === $currentKey) { return; } $restock->clearMediaCollection('photos'); if (! empty($data['photo_key'])) { $this->registerMedia( model: $restock, s3Key: $data['photo_key'], collectionName: 'photos', orderColumn: 1, ); } } }