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();
|
$purchaseItems = $this->loadPurchaseItems();
|
||||||
|
|
||||||
DB::transaction(function () use ($purchaseItems) {
|
// Recalculate total to ensure data integrity
|
||||||
$purchase = Purchase::create($this->prepareSavedData());
|
$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');
|
$purchase->load('outlet');
|
||||||
|
|
||||||
$this->uploadMedia($this->image, $purchase, 'image');
|
$this->uploadMedia($this->image, $purchase, 'image');
|
||||||
|
|
||||||
foreach ($purchaseItems as $item) {
|
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]);
|
$item->update(['purchase_id' => $purchase->id]);
|
||||||
|
|
||||||
$this->increaseOutletStock($purchase->outlet, $item);
|
$this->increaseOutletStock($purchase->outlet, $item);
|
||||||
@ -106,8 +122,15 @@ public function update(): void
|
|||||||
{
|
{
|
||||||
$this->validate();
|
$this->validate();
|
||||||
|
|
||||||
DB::transaction(function () {
|
// Ensure total calculation integrity from existing items
|
||||||
$this->purchase->update($this->prepareSavedData());
|
$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->syncMedia($this->image, $this->purchase, 'image');
|
||||||
$this->uploadMedia($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->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();
|
$this->purchaseItems = $this->loadPurchaseItems();
|
||||||
|
|
||||||
@ -70,6 +70,13 @@ public function save(): void
|
|||||||
$this->redirectRoute('studio.manage.purchase.index');
|
$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
|
public function render(): View
|
||||||
{
|
{
|
||||||
return view('livewire.studio.manage.purchase.form', [
|
return view('livewire.studio.manage.purchase.form', [
|
||||||
|
|||||||
@ -11,6 +11,7 @@
|
|||||||
use App\Traits\Purchase\WithUpdateStock;
|
use App\Traits\Purchase\WithUpdateStock;
|
||||||
use Flux\Flux;
|
use Flux\Flux;
|
||||||
use Illuminate\Contracts\View\View;
|
use Illuminate\Contracts\View\View;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
use Livewire\Attributes\Title;
|
use Livewire\Attributes\Title;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
||||||
@ -23,11 +24,13 @@ public function delete(Purchase $purchase): void
|
|||||||
{
|
{
|
||||||
$this->canOrAbort('delete purchase');
|
$this->canOrAbort('delete purchase');
|
||||||
|
|
||||||
|
DB::transaction(function () use ($purchase) {
|
||||||
foreach ($purchase->items as $item) {
|
foreach ($purchase->items as $item) {
|
||||||
$this->decreaseOutletStock($purchase->outlet, $item);
|
$this->decreaseOutletStock($purchase->outlet, $item);
|
||||||
}
|
}
|
||||||
|
|
||||||
$purchase->delete();
|
$purchase->delete();
|
||||||
|
});
|
||||||
|
|
||||||
$this->dispatch('refreshDatatable');
|
$this->dispatch('refreshDatatable');
|
||||||
|
|
||||||
|
|||||||
@ -84,7 +84,7 @@ public function addItem(string $type): void
|
|||||||
'total_price' => $costPrice * $quantity,
|
'total_price' => $costPrice * $quantity,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->purchaseItems->push($item);
|
$this->purchaseItems->prepend($item);
|
||||||
|
|
||||||
$this->toast(Str::ucfirst($type).' ditambahkan ke keranjang.', 'Berhasil');
|
$this->toast(Str::ucfirst($type).' ditambahkan ke keranjang.', 'Berhasil');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,11 +27,25 @@ public function closeModal()
|
|||||||
|
|
||||||
public function updateItem(): void
|
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);
|
$item = PurchaseItem::find($this->form->item_id);
|
||||||
|
|
||||||
|
if (! $item) {
|
||||||
|
$this->toast('Item tidak found.', 'Gagal', 'danger');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$item->update([
|
$item->update([
|
||||||
'quantity' => parseRupiahToInt($this->form->quantity_edit),
|
'quantity' => $quantity,
|
||||||
'total_price' => (int) $item->purchasable->cost_price * (int) parseRupiahToInt($this->form->quantity_edit),
|
'total_price' => (int) $item->purchasable->cost_price * $quantity,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$item->refresh();
|
$item->refresh();
|
||||||
|
|||||||
@ -27,7 +27,12 @@ public function increaseOutletStock($outlet, $item): void
|
|||||||
return;
|
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) {
|
if ($existing) {
|
||||||
$previousStock = $existing->pivot->stock ?? 0;
|
$previousStock = $existing->pivot->stock ?? 0;
|
||||||
@ -76,7 +81,11 @@ public function decreaseOutletStock($outlet, $item): void
|
|||||||
return;
|
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) {
|
if ($existing) {
|
||||||
$currentStock = $existing->pivot->stock ?? 0;
|
$currentStock = $existing->pivot->stock ?? 0;
|
||||||
@ -84,6 +93,11 @@ public function decreaseOutletStock($outlet, $item): void
|
|||||||
$outlet->{$relation}()->updateExistingPivot($item->purchasable_id, [
|
$outlet->{$relation}()->updateExistingPivot($item->purchasable_id, [
|
||||||
'stock' => $newStock,
|
'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(
|
StockActivityLogService::log(
|
||||||
|
|||||||
@ -15,7 +15,7 @@
|
|||||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-6">
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-6">
|
||||||
<flux:field>
|
<flux:field>
|
||||||
<flux:label>Outlet <span class="text-red-500 ms-1">*</span></flux:label>
|
<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)
|
@foreach ($outlets as $key => $value)
|
||||||
<flux:select.option value="{{ $key }}">
|
<flux:select.option value="{{ $key }}">
|
||||||
{{ $value }}
|
{{ $value }}
|
||||||
|
|||||||
@ -50,7 +50,7 @@
|
|||||||
<flux:field>
|
<flux:field>
|
||||||
<flux:label>Outlet <span class="text-red-500 ms-1">*</span></flux:label>
|
<flux:label>Outlet <span class="text-red-500 ms-1">*</span></flux:label>
|
||||||
<flux:select variant="listbox" searchable placeholder="Pilih Outlet"
|
<flux:select variant="listbox" searchable placeholder="Pilih Outlet"
|
||||||
wire:model="form.outlet_id">
|
wire:model.live="form.outlet_id">
|
||||||
@foreach ($outlets as $key => $value)
|
@foreach ($outlets as $key => $value)
|
||||||
<flux:select.option value="{{ $key }}">{{ $value }}
|
<flux:select.option value="{{ $key }}">{{ $value }}
|
||||||
</flux:select.option>
|
</flux:select.option>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user