dstpabuaran.com/app/Services/Admin/Master/Product/ProductVariantService.php
Yoga Pangestu 85556f8779 feat: enhance product management with approval and rejection workflows
- Added approval and rejection functionality for products, including new routes and methods in the ProductController.
- Implemented UI changes to display product status (pending, rejected) with appropriate badges and actions.
- Introduced a RejectDialog component for providing rejection reasons.
- Updated product columns to handle new actions for approving and rejecting products.
- Enhanced variant management to restrict actions based on product status.
- Refactored various components to improve code organization and readability.
2026-08-07 11:23:41 +07:00

196 lines
6.5 KiB
PHP

<?php
namespace App\Services\Admin\Master\Product;
use App\Enums\ProductStatus;
use App\Models\Product;
use App\Models\ProductVariant;
use App\Services\Concerns\RegistersMedia;
use App\Services\NotificationService;
use App\Services\S3PresignedService;
use App\Services\StockMutationService;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
class ProductVariantService
{
use RegistersMedia;
public function __construct(
private S3PresignedService $s3Service,
private StockMutationService $stockMutationService,
) {}
private function canVerify(): bool
{
$user = auth()->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();
}
}