user(); return $user->hasAnyRole(['developer', 'owner']); } private function assertNotPending(Product $product): void { if ($product->status === ProductStatus::PENDING && ! $this->canVerify()) { throw ValidationException::withMessages([ 'status' => 'Produk sedang menunggu verifikasi dan tidak dapat diubah.', ]); } } public function getForEdit(ProductVariant $variant): array { $variant->load([ 'productPrices', 'media', ]); $media = $variant->getMedia('photos'); $photoKeys = $media->pluck('file_name')->toArray(); $photoUrls = $media->map(fn ($m) => $this->s3Service->getTemporaryUrl($m->file_name))->toArray(); return [ 'id' => $variant->id, 'product_id' => $variant->product_id, 'name' => $variant->name, 'stock' => $variant->stock, 'reject_stock' => $variant->reject_stock, 'retail_stock' => $variant->retail_stock, 'photo_keys' => $photoKeys, 'photo_urls' => $photoUrls, 'prices' => $variant->productPrices->map(fn ($p) => [ 'type' => $p->type->value, 'price' => $p->price, ]), ]; } public function update(ProductVariant $variant, array $data): ProductVariant { $this->assertNotPending($variant->product); DB::transaction(function () use ($variant, $data) { $oldData = $variant->only(['stock', 'reject_stock', 'retail_stock']); $variant->update([ 'name' => $data['name'], 'stock' => $data['stock'], 'reject_stock' => $data['reject_stock'], 'retail_stock' => $data['retail_stock'], ]); $this->stockMutationService->recordAdjustment($variant, $oldData, $data, 'Penyesuaian stok saat edit varian'); // Upsert prices instead of delete+recreate $priceRows = collect($data['prices'])->map(fn ($p) => [ 'variant_id' => $variant->id, 'type' => $p['type'], 'price' => $p['price'], 'created_at' => now(), 'updated_at' => now(), ])->toArray(); DB::table('product_prices')->upsert( $priceRows, ['variant_id', 'type'], ['price', 'updated_at'] ); if (! empty($data['photo_keys']) && is_array($data['photo_keys'])) { $variant->clearMediaCollection('photos'); $this->registerPhotos($variant, $data['photo_keys']); } }); NotificationService::notify( roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'], title: 'Varian Diperbarui', body: "Varian \"{$variant->name}\" berhasil diperbarui".' oleh '.auth()->user()->full_name.'.', url: route('admin.master.products.index'), ); return $variant->fresh(); } public function delete(Product $product, ProductVariant $variant): bool { $this->assertNotPending($product); $result = DB::transaction(function () use ($variant) { $variant->productPrices()->delete(); $variant->clearMediaCollection('photos'); return $variant->delete(); }); NotificationService::notify( roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'], title: 'Varian Dihapus', body: "Varian \"{$variant->name}\" dari produk \"{$product->name}\" berhasil dihapus".' oleh '.auth()->user()->full_name.'.', url: route('admin.master.products.index'), ); return $result; } public function registerPhotos(ProductVariant $variant, array $photoKeys): void { foreach ($photoKeys as $order => $s3Key) { $this->registerMedia( model: $variant, s3Key: $s3Key, collectionName: 'photos', orderColumn: $order + 1, ); } } public function getTemporaryUrls(ProductVariant $variant): array { $media = $variant->getMedia('photos'); return $media->map(fn ($m) => $this->s3Service->getTemporaryUrl($m->file_name))->toArray(); } public function transferStock(ProductVariant $variant, array $data): ProductVariant { $quantity = (int) $data['quantity']; $this->assertNotPending($variant->product); if ($variant->stock < $quantity) { throw ValidationException::withMessages([ 'quantity' => "Stok bagus tidak mencukupi. Stok tersedia: {$variant->stock}.", ]); } DB::transaction(function () use ($variant, $quantity, $data) { $stockBefore = $variant->stock; $retailBefore = $variant->retail_stock; $variant->update([ 'stock' => $stockBefore - $quantity, 'retail_stock' => $retailBefore + $quantity, ]); $this->stockMutationService->recordTransfer( model: $variant, quantity: $quantity, fromQuality: 'good', toQuality: 'retail', fromBefore: $stockBefore, toBefore: $retailBefore, description: $data['description'] ?? 'Transfer stok bagus ke stok ecer', ); }); NotificationService::notify( roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'], title: 'Transfer Stok', body: "{$quantity} unit dari varian \"{$variant->name}\" berhasil ditransfer dari stok bagus ke stok ecer".' oleh '.auth()->user()->full_name.'.', url: route('admin.master.products.index'), ); return $variant->fresh(); } }