feat: Implement dynamic product filtering by outlet, enforce purchase total integrity, add concurrency locks for stock updates, and improve item management.
This commit is contained in:
parent
7495578b57
commit
14e79054c0
@ -87,14 +87,30 @@ public function store(): void
|
||||
|
||||
$purchaseItems = $this->loadPurchaseItems();
|
||||
|
||||
DB::transaction(function () use ($purchaseItems) {
|
||||
$purchase = Purchase::create($this->prepareSavedData());
|
||||
// Recalculate total to ensure data integrity
|
||||
$calculatedTotal = $purchaseItems->sum(function ($item) {
|
||||
return $item->unit_price * $item->quantity;
|
||||
});
|
||||
|
||||
// Override the total input with the calculated one to prevent mismatches
|
||||
$this->total = formatCurrencyNumber($calculatedTotal);
|
||||
|
||||
DB::transaction(function () use ($purchaseItems, $calculatedTotal) {
|
||||
$data = $this->prepareSavedData();
|
||||
$data['total'] = $calculatedTotal; // Use strict calculated total
|
||||
|
||||
$purchase = Purchase::create($data);
|
||||
|
||||
$purchase->load('outlet');
|
||||
|
||||
$this->uploadMedia($this->image, $purchase, 'image');
|
||||
|
||||
foreach ($purchaseItems as $item) {
|
||||
// Ensure item integrity before linking
|
||||
if ($item->quantity <= 0 || $item->unit_price <= 0) {
|
||||
continue; // Or throw exception
|
||||
}
|
||||
|
||||
$item->update(['purchase_id' => $purchase->id]);
|
||||
|
||||
$this->increaseOutletStock($purchase->outlet, $item);
|
||||
@ -106,8 +122,15 @@ public function update(): void
|
||||
{
|
||||
$this->validate();
|
||||
|
||||
DB::transaction(function () {
|
||||
$this->purchase->update($this->prepareSavedData());
|
||||
// Ensure total calculation integrity from existing items
|
||||
$calculatedTotal = $this->purchase->items()->sum(DB::raw('unit_price * quantity'));
|
||||
$this->total = formatCurrencyNumber($calculatedTotal);
|
||||
|
||||
DB::transaction(function () use ($calculatedTotal) {
|
||||
$data = $this->prepareSavedData();
|
||||
$data['total'] = $calculatedTotal; // Enforce calculated total
|
||||
|
||||
$this->purchase->update($data);
|
||||
|
||||
$this->syncMedia($this->image, $this->purchase, 'image');
|
||||
$this->uploadMedia($this->image, $this->purchase, 'image');
|
||||
|
||||
@ -40,11 +40,11 @@ public function mount(): void
|
||||
{
|
||||
$this->outlets = auth()->user()->outlets->pluck('name', 'id')->toArray();
|
||||
|
||||
$this->perfumes = Perfume::orderBy('name')->pluck('name', 'id')->toArray();
|
||||
$this->perfumes = Perfume::whereHas('outlets', fn ($query) => $query->where('outlets.id', $this->form->outlet_id))->orderBy('name')->pluck('name', 'id')->toArray();
|
||||
|
||||
$this->bottles = Bottle::orderBy('size')->pluck('name', 'id')->toArray();
|
||||
$this->bottles = Bottle::whereHas('outlets', fn ($query) => $query->where('outlets.id', $this->form->outlet_id))->orderBy('size')->pluck('name', 'id')->toArray();
|
||||
|
||||
$this->products = Product::orderBy('name')->pluck('name', 'id')->toArray();
|
||||
$this->products = Product::whereHas('outlets', fn ($query) => $query->where('outlets.id', $this->form->outlet_id))->orderBy('name')->pluck('name', 'id')->toArray();
|
||||
|
||||
$this->purchaseItems = $this->loadPurchaseItems();
|
||||
|
||||
@ -70,6 +70,13 @@ public function save(): void
|
||||
$this->redirectRoute('studio.manage.purchase.index');
|
||||
}
|
||||
|
||||
public function updatedFormOutletId($value): void
|
||||
{
|
||||
$this->perfumes = Perfume::whereHas('outlets', fn ($query) => $query->where('outlets.id', $value))->orderBy('name')->pluck('name', 'id')->toArray();
|
||||
$this->bottles = Bottle::whereHas('outlets', fn ($query) => $query->where('outlets.id', $value))->orderBy('size')->pluck('name', 'id')->toArray();
|
||||
$this->products = Product::whereHas('outlets', fn ($query) => $query->where('outlets.id', $value))->orderBy('name')->pluck('name', 'id')->toArray();
|
||||
}
|
||||
|
||||
public function render(): View
|
||||
{
|
||||
return view('livewire.studio.manage.purchase.form', [
|
||||
|
||||
@ -11,6 +11,7 @@
|
||||
use App\Traits\Purchase\WithUpdateStock;
|
||||
use Flux\Flux;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Livewire\Attributes\Title;
|
||||
use Livewire\Component;
|
||||
|
||||
@ -23,11 +24,13 @@ public function delete(Purchase $purchase): void
|
||||
{
|
||||
$this->canOrAbort('delete purchase');
|
||||
|
||||
foreach ($purchase->items as $item) {
|
||||
$this->decreaseOutletStock($purchase->outlet, $item);
|
||||
}
|
||||
DB::transaction(function () use ($purchase) {
|
||||
foreach ($purchase->items as $item) {
|
||||
$this->decreaseOutletStock($purchase->outlet, $item);
|
||||
}
|
||||
|
||||
$purchase->delete();
|
||||
$purchase->delete();
|
||||
});
|
||||
|
||||
$this->dispatch('refreshDatatable');
|
||||
|
||||
|
||||
@ -84,7 +84,7 @@ public function addItem(string $type): void
|
||||
'total_price' => $costPrice * $quantity,
|
||||
]);
|
||||
|
||||
$this->purchaseItems->push($item);
|
||||
$this->purchaseItems->prepend($item);
|
||||
|
||||
$this->toast(Str::ucfirst($type).' ditambahkan ke keranjang.', 'Berhasil');
|
||||
}
|
||||
|
||||
@ -27,11 +27,25 @@ public function closeModal()
|
||||
|
||||
public function updateItem(): void
|
||||
{
|
||||
$quantity = parseRupiahToInt($this->form->quantity_edit);
|
||||
|
||||
if ($quantity <= 0) {
|
||||
$this->toast('Jumlah item tidak valid.', 'Gagal', 'danger');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$item = PurchaseItem::find($this->form->item_id);
|
||||
|
||||
if (! $item) {
|
||||
$this->toast('Item tidak found.', 'Gagal', 'danger');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$item->update([
|
||||
'quantity' => parseRupiahToInt($this->form->quantity_edit),
|
||||
'total_price' => (int) $item->purchasable->cost_price * (int) parseRupiahToInt($this->form->quantity_edit),
|
||||
'quantity' => $quantity,
|
||||
'total_price' => (int) $item->purchasable->cost_price * $quantity,
|
||||
]);
|
||||
|
||||
$item->refresh();
|
||||
|
||||
@ -27,7 +27,12 @@ public function increaseOutletStock($outlet, $item): void
|
||||
return;
|
||||
}
|
||||
|
||||
$existing = $outlet->{$relation}()->withPivot('stock')->where("{$relation}.id", $item->purchasable_id)->first();
|
||||
// Lock the row to prevent race conditions
|
||||
$existing = $outlet->{$relation}()
|
||||
->where("{$relation}.id", $item->purchasable_id)
|
||||
->lockForUpdate()
|
||||
->withPivot('stock')
|
||||
->first();
|
||||
|
||||
if ($existing) {
|
||||
$previousStock = $existing->pivot->stock ?? 0;
|
||||
@ -76,7 +81,11 @@ public function decreaseOutletStock($outlet, $item): void
|
||||
return;
|
||||
}
|
||||
|
||||
$existing = $outlet->{$relation}()->withPivot('stock')->where("{$relation}.id", $item->purchasable_id)->first();
|
||||
$existing = $outlet->{$relation}()
|
||||
->where("{$relation}.id", $item->purchasable_id)
|
||||
->lockForUpdate()
|
||||
->withPivot('stock')
|
||||
->first();
|
||||
|
||||
if ($existing) {
|
||||
$currentStock = $existing->pivot->stock ?? 0;
|
||||
@ -84,6 +93,11 @@ public function decreaseOutletStock($outlet, $item): void
|
||||
$outlet->{$relation}()->updateExistingPivot($item->purchasable_id, [
|
||||
'stock' => $newStock,
|
||||
]);
|
||||
} else {
|
||||
// Handle case where stock doesn't exist but we are decreasing?
|
||||
// Should not happen in normal flow, but good to handle.
|
||||
$currentStock = 0;
|
||||
$newStock = 0;
|
||||
}
|
||||
|
||||
StockActivityLogService::log(
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-6">
|
||||
<flux:field>
|
||||
<flux:label>Outlet <span class="text-red-500 ms-1">*</span></flux:label>
|
||||
<flux:select variant="listbox" searchable placeholder="Pilih Outlet" wire:model="form.outlet_id">
|
||||
<flux:select variant="listbox" searchable placeholder="Pilih Outlet" wire:model.live="form.outlet_id">
|
||||
@foreach ($outlets as $key => $value)
|
||||
<flux:select.option value="{{ $key }}">
|
||||
{{ $value }}
|
||||
|
||||
@ -50,7 +50,7 @@
|
||||
<flux:field>
|
||||
<flux:label>Outlet <span class="text-red-500 ms-1">*</span></flux:label>
|
||||
<flux:select variant="listbox" searchable placeholder="Pilih Outlet"
|
||||
wire:model="form.outlet_id">
|
||||
wire:model.live="form.outlet_id">
|
||||
@foreach ($outlets as $key => $value)
|
||||
<flux:select.option value="{{ $key }}">{{ $value }}
|
||||
</flux:select.option>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user