feat: optimize product variant handling with bulk insertions and adjustments; enhance stock mutation recording
This commit is contained in:
parent
44dab8271f
commit
549e3e3bca
@ -15,7 +15,7 @@ protected function handleAction(callable $action, string $successMessage, string
|
||||
$action();
|
||||
Inertia::flash('toast', ['type' => 'success', 'message' => $successMessage]);
|
||||
|
||||
return to_route($redirectRoute, $parameters);
|
||||
return to_route($redirectRoute);
|
||||
} catch (ValidationException $e) {
|
||||
$firstError = collect($e->errors())->flatten()->first();
|
||||
Inertia::flash('toast', ['type' => 'error', 'message' => $firstError ?? 'Terjadi kesalahan.']);
|
||||
|
||||
@ -3,7 +3,6 @@
|
||||
namespace App\Services\Admin\Master\Product;
|
||||
|
||||
use App\Models\Product;
|
||||
use App\Models\ProductPrice;
|
||||
use App\Models\ProductVariant;
|
||||
use App\Services\NotificationService;
|
||||
use App\Services\S3PresignedService;
|
||||
@ -90,31 +89,60 @@ public function create(array $data): Product
|
||||
|
||||
$useSamePrice = $data['use_same_price'] ?? false;
|
||||
|
||||
foreach ($data['variants'] as $index => $variantData) {
|
||||
$variant = $product->productVariants()->create([
|
||||
'name' => $variantData['name'],
|
||||
'stock' => $variantData['stock'],
|
||||
'reject_stock' => $variantData['reject_stock'],
|
||||
'retail_stock' => $variantData['retail_stock'],
|
||||
]);
|
||||
// Bulk insert variants
|
||||
$now = now();
|
||||
$variantRows = collect($data['variants'])->map(fn ($v) => [
|
||||
'product_id' => $product->id,
|
||||
'name' => $v['name'],
|
||||
'stock' => $v['stock'],
|
||||
'reject_stock' => $v['reject_stock'],
|
||||
'retail_stock' => $v['retail_stock'],
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
])->toArray();
|
||||
|
||||
$prices = $useSamePrice
|
||||
? $data['shared_prices']
|
||||
: $variantData['prices'];
|
||||
DB::table('product_variants')->insert($variantRows);
|
||||
|
||||
// Map variant name -> variant ID
|
||||
$insertedVariants = ProductVariant::where('product_id', $product->id)->get();
|
||||
$variantMap = $insertedVariants->mapWithKeys(fn ($v) => [$v->name => $v->id]);
|
||||
|
||||
// Bulk insert prices
|
||||
$priceRows = [];
|
||||
foreach ($data['variants'] as $variantData) {
|
||||
$variantId = $variantMap[$variantData['name']];
|
||||
$prices = $useSamePrice ? $data['shared_prices'] : $variantData['prices'];
|
||||
|
||||
foreach ($prices as $priceData) {
|
||||
ProductPrice::create([
|
||||
'variant_id' => $variant->id,
|
||||
$priceRows[] = [
|
||||
'variant_id' => $variantId,
|
||||
'type' => $priceData['type'],
|
||||
'price' => $priceData['price'],
|
||||
]);
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if ($priceRows !== []) {
|
||||
DB::table('product_prices')->insert($priceRows);
|
||||
}
|
||||
|
||||
// Bulk record stock mutations
|
||||
$stockDataMap = [];
|
||||
foreach ($data['variants'] as $variantData) {
|
||||
$variantId = $variantMap[$variantData['name']];
|
||||
$stockDataMap[$variantId] = $variantData;
|
||||
}
|
||||
|
||||
$this->stockMutationService->recordBulkInitial($insertedVariants, $stockDataMap, 'Stok awal saat pembuatan varian');
|
||||
|
||||
// Register photos (per-variant, S3 operation)
|
||||
foreach ($data['variants'] as $variantData) {
|
||||
if (! empty($variantData['photo_keys']) && is_array($variantData['photo_keys'])) {
|
||||
$variant = $insertedVariants->firstWhere('name', $variantData['name']);
|
||||
$this->variantService->registerPhotos($variant, $variantData['photo_keys']);
|
||||
}
|
||||
|
||||
$this->stockMutationService->recordInitial($variant, $variantData, 'Stok awal saat pembuatan varian');
|
||||
}
|
||||
|
||||
return $product;
|
||||
@ -180,12 +208,15 @@ public function update(Product $product, array $data): Product
|
||||
$product->categories()->sync($data['category_ids']);
|
||||
|
||||
$useSamePrice = $data['use_same_price'] ?? false;
|
||||
$now = now();
|
||||
|
||||
// Identify existing vs new variants
|
||||
$existingVariantIds = collect($data['variants'])
|
||||
->pluck('id')
|
||||
->filter()
|
||||
->toArray();
|
||||
|
||||
// Delete removed variants (cascade prices + media)
|
||||
$product->productVariants()
|
||||
->whereNotIn('id', $existingVariantIds)
|
||||
->each(function (ProductVariant $variant) {
|
||||
@ -194,44 +225,162 @@ public function update(Product $product, array $data): Product
|
||||
$variant->delete();
|
||||
});
|
||||
|
||||
// Bulk load all existing variants with media (1 query instead of N)
|
||||
$existingVariantsMap = ProductVariant::whereIn('id', $existingVariantIds)
|
||||
->with('media')
|
||||
->get()
|
||||
->mapWithKeys(fn ($v) => [$v->id => $v]);
|
||||
|
||||
// Collect old stock data + identify changed variants
|
||||
$oldStockDataMap = [];
|
||||
$changedIds = [];
|
||||
foreach ($data['variants'] as $variantData) {
|
||||
$variantId = $variantData['id'] ?? null;
|
||||
if (! $variantId || ! isset($existingVariantsMap[$variantId])) {
|
||||
continue;
|
||||
}
|
||||
$variant = $existingVariantsMap[$variantId];
|
||||
$oldStockDataMap[$variantId] = $variant->only(['stock', 'reject_stock', 'retail_stock']);
|
||||
|
||||
if ($variant->name !== $variantData['name']
|
||||
|| $variant->stock != $variantData['stock']
|
||||
|| $variant->reject_stock != $variantData['reject_stock']
|
||||
|| $variant->retail_stock != $variantData['retail_stock']) {
|
||||
$changedIds[] = $variantId;
|
||||
}
|
||||
}
|
||||
|
||||
// Bulk update changed variants (only changed ones, not all)
|
||||
if ($changedIds !== []) {
|
||||
$changedUpdates = collect($data['variants'])
|
||||
->filter(fn ($v) => isset($v['id']) && in_array($v['id'], $changedIds));
|
||||
|
||||
foreach ($changedUpdates as $variantData) {
|
||||
$existingVariantsMap[$variantData['id']]->update([
|
||||
'name' => $variantData['name'],
|
||||
'stock' => $variantData['stock'],
|
||||
'reject_stock' => $variantData['reject_stock'],
|
||||
'retail_stock' => $variantData['retail_stock'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Bulk create new variants
|
||||
$newVariantsData = collect($data['variants'])->filter(fn ($v) => ! isset($v['id']));
|
||||
$newVariantIdMap = [];
|
||||
|
||||
if ($newVariantsData->isNotEmpty()) {
|
||||
$newVariantRows = $newVariantsData->map(fn ($v) => [
|
||||
'product_id' => $product->id,
|
||||
'name' => $v['name'],
|
||||
'stock' => $v['stock'],
|
||||
'reject_stock' => $v['reject_stock'],
|
||||
'retail_stock' => $v['retail_stock'],
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
])->toArray();
|
||||
|
||||
DB::table('product_variants')->insert($newVariantRows);
|
||||
|
||||
// Map name -> id for new variants
|
||||
$newlyCreated = ProductVariant::where('product_id', $product->id)
|
||||
->whereIn('name', $newVariantsData->pluck('name')->toArray())
|
||||
->get();
|
||||
|
||||
$newVariantIdMap = $newlyCreated->mapWithKeys(fn ($v) => [$v->name => $v->id])->toArray();
|
||||
}
|
||||
|
||||
// Build variant_id lookup: existing by id, new by name
|
||||
$variantIdLookup = [];
|
||||
foreach ($data['variants'] as $variantData) {
|
||||
$variantId = $variantData['id'] ?? $newVariantIdMap[$variantData['name']] ?? null;
|
||||
if ($variantId) {
|
||||
$variant = $product->productVariants()->findOrFail($variantId);
|
||||
$oldData = $variant->only(['stock', 'reject_stock', 'retail_stock']);
|
||||
$variant->update([
|
||||
'name' => $variantData['name'],
|
||||
'stock' => $variantData['stock'],
|
||||
'reject_stock' => $variantData['reject_stock'],
|
||||
'retail_stock' => $variantData['retail_stock'],
|
||||
]);
|
||||
$this->stockMutationService->recordAdjustment($variant, $oldData, $variantData, 'Penyesuaian stok saat edit varian');
|
||||
} else {
|
||||
$variant = $product->productVariants()->create([
|
||||
'name' => $variantData['name'],
|
||||
'stock' => $variantData['stock'],
|
||||
'reject_stock' => $variantData['reject_stock'],
|
||||
'retail_stock' => $variantData['retail_stock'],
|
||||
]);
|
||||
$this->stockMutationService->recordInitial($variant, $variantData, 'Stok awal saat pembuatan varian');
|
||||
$variantIdLookup[$variantData['name']] = $variantId;
|
||||
}
|
||||
}
|
||||
|
||||
// Upsert all prices (1 query)
|
||||
$priceRows = [];
|
||||
foreach ($data['variants'] as $variantData) {
|
||||
$variantId = $variantData['id'] ?? $newVariantIdMap[$variantData['name']] ?? null;
|
||||
if (! $variantId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$variant->productPrices()->delete();
|
||||
|
||||
$prices = $useSamePrice
|
||||
? $data['shared_prices']
|
||||
: $variantData['prices'];
|
||||
|
||||
$prices = $useSamePrice ? $data['shared_prices'] : $variantData['prices'];
|
||||
foreach ($prices as $priceData) {
|
||||
ProductPrice::create([
|
||||
'variant_id' => $variant->id,
|
||||
$priceRows[] = [
|
||||
'variant_id' => $variantId,
|
||||
'type' => $priceData['type'],
|
||||
'price' => $priceData['price'],
|
||||
]);
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if ($priceRows !== []) {
|
||||
DB::table('product_prices')->upsert(
|
||||
$priceRows,
|
||||
['variant_id', 'type'],
|
||||
['price', 'updated_at']
|
||||
);
|
||||
}
|
||||
|
||||
// Bulk record stock mutations for new variants
|
||||
if ($newVariantIdMap !== []) {
|
||||
$newStockDataMap = [];
|
||||
foreach ($newVariantsData as $variantData) {
|
||||
$variantId = $newVariantIdMap[$variantData['name']] ?? null;
|
||||
if ($variantId) {
|
||||
$newStockDataMap[$variantId] = $variantData;
|
||||
}
|
||||
}
|
||||
|
||||
if (! empty($variantData['photo_keys']) && is_array($variantData['photo_keys'])) {
|
||||
$newVariantModels = ProductVariant::whereIn('id', array_values($newVariantIdMap))->get();
|
||||
$this->stockMutationService->recordBulkInitial($newVariantModels, $newStockDataMap, 'Stok awal saat pembuatan varian');
|
||||
}
|
||||
|
||||
// Bulk record stock mutations for changed existing variants
|
||||
if ($changedIds !== []) {
|
||||
$newDataForExisting = [];
|
||||
foreach ($data['variants'] as $variantData) {
|
||||
$variantId = $variantData['id'] ?? null;
|
||||
if ($variantId && in_array($variantId, $changedIds)) {
|
||||
$newDataForExisting[$variantId] = $variantData;
|
||||
}
|
||||
}
|
||||
|
||||
$changedModels = collect($changedIds)->map(fn ($id) => $existingVariantsMap[$id])->filter();
|
||||
$this->stockMutationService->recordBulkAdjustment(
|
||||
$changedModels,
|
||||
$oldStockDataMap,
|
||||
$newDataForExisting,
|
||||
'Penyesuaian stok saat edit varian'
|
||||
);
|
||||
}
|
||||
|
||||
// Register photos (per-variant, S3 operation) - only if changed
|
||||
foreach ($data['variants'] as $variantData) {
|
||||
if (empty($variantData['photo_keys']) || ! is_array($variantData['photo_keys'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$variantId = $variantData['id'] ?? $newVariantIdMap[$variantData['name']] ?? null;
|
||||
if (! $variantId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$variant = $existingVariantsMap[$variantId] ?? ProductVariant::find($variantId);
|
||||
if (! $variant) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Compare existing photo keys vs new ones
|
||||
$existingKeys = $variant->getMedia('photos')->pluck('file_name')->sort()->values()->toArray();
|
||||
$newKeys = collect($variantData['photo_keys'])->sort()->values()->toArray();
|
||||
|
||||
if ($existingKeys !== $newKeys) {
|
||||
$variant->clearMediaCollection('photos');
|
||||
$this->variantService->registerPhotos($variant, $variantData['photo_keys']);
|
||||
}
|
||||
|
||||
@ -3,7 +3,6 @@
|
||||
namespace App\Services\Admin\Master\Product;
|
||||
|
||||
use App\Models\Product;
|
||||
use App\Models\ProductPrice;
|
||||
use App\Models\ProductVariant;
|
||||
use App\Services\Concerns\RegistersMedia;
|
||||
use App\Services\NotificationService;
|
||||
@ -23,7 +22,10 @@ public function __construct(
|
||||
|
||||
public function getForEdit(ProductVariant $variant): array
|
||||
{
|
||||
$variant->load('productPrices');
|
||||
$variant->load([
|
||||
'productPrices',
|
||||
'media',
|
||||
]);
|
||||
|
||||
$media = $variant->getMedia('photos');
|
||||
$photoKeys = $media->pluck('file_name')->toArray();
|
||||
@ -59,15 +61,20 @@ public function update(ProductVariant $variant, array $data): ProductVariant
|
||||
|
||||
$this->stockMutationService->recordAdjustment($variant, $oldData, $data, 'Penyesuaian stok saat edit varian');
|
||||
|
||||
$variant->productPrices()->delete();
|
||||
// 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();
|
||||
|
||||
foreach ($data['prices'] as $priceData) {
|
||||
ProductPrice::create([
|
||||
'variant_id' => $variant->id,
|
||||
'type' => $priceData['type'],
|
||||
'price' => $priceData['price'],
|
||||
]);
|
||||
}
|
||||
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');
|
||||
|
||||
@ -2,9 +2,12 @@
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\ProductVariant;
|
||||
use App\Models\StockMutation;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class StockMutationService
|
||||
{
|
||||
@ -61,6 +64,77 @@ public function recordAdjustment(Model $model, array $oldData, array $newData, s
|
||||
}
|
||||
}
|
||||
|
||||
public function recordBulkInitial(Collection $variants, array $stockDataMap, string $description = 'Stok awal'): void
|
||||
{
|
||||
$userId = auth()->id();
|
||||
$now = now();
|
||||
$mutations = [];
|
||||
|
||||
foreach ($variants as $variant) {
|
||||
$stockData = $stockDataMap[$variant->id] ?? [];
|
||||
foreach (self::QUALITY_MAP as $field => $quality) {
|
||||
$quantity = (int) ($stockData[$field] ?? 0);
|
||||
if ($quantity > 0) {
|
||||
$mutations[] = [
|
||||
'user_id' => $userId,
|
||||
'stockable_type' => ProductVariant::class,
|
||||
'stockable_id' => $variant->id,
|
||||
'type' => 'in',
|
||||
'quantity' => $quantity,
|
||||
'stock_before' => 0,
|
||||
'stock_after' => $quantity,
|
||||
'stock_quality' => $quality,
|
||||
'description' => $description,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($mutations !== []) {
|
||||
DB::table('stock_mutations')->insert($mutations);
|
||||
}
|
||||
}
|
||||
|
||||
public function recordBulkAdjustment(Collection $variants, array $oldDataMap, array $newDataMap, string $description = 'Penyesuaian stok'): void
|
||||
{
|
||||
$userId = auth()->id();
|
||||
$now = now();
|
||||
$mutations = [];
|
||||
|
||||
foreach ($variants as $variant) {
|
||||
$oldData = $oldDataMap[$variant->id] ?? [];
|
||||
$newData = $newDataMap[$variant->id] ?? [];
|
||||
|
||||
foreach (self::QUALITY_MAP as $field => $quality) {
|
||||
$old = (int) ($oldData[$field] ?? 0);
|
||||
$new = (int) ($newData[$field] ?? 0);
|
||||
$diff = $new - $old;
|
||||
|
||||
if ($diff !== 0) {
|
||||
$mutations[] = [
|
||||
'user_id' => $userId,
|
||||
'stockable_type' => ProductVariant::class,
|
||||
'stockable_id' => $variant->id,
|
||||
'type' => $diff > 0 ? 'in' : 'out',
|
||||
'quantity' => $diff,
|
||||
'stock_before' => $old,
|
||||
'stock_after' => $new,
|
||||
'stock_quality' => $quality,
|
||||
'description' => $description,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($mutations !== []) {
|
||||
DB::table('stock_mutations')->insert($mutations);
|
||||
}
|
||||
}
|
||||
|
||||
public function recordTransfer(
|
||||
Model $model,
|
||||
int $quantity,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user