From a2efc0f3495af44d085272118e3aca244a4ca8bc Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Thu, 1 Jan 2026 15:26:05 +0700 Subject: [PATCH] refactor(purchase): merubah total alur belanja, yang awalnya langsung masuk ke stok yang ada di outlet, sekarang masuk ke gudang terleih dahulu --- .../Datatable/Manage/PurchasesTable.php | 9 +- .../Forms/Studio/Manage/PurchaseForm.php | 37 ++--- .../Studio/Manage/Purchase/Create.php | 18 +-- app/Livewire/Studio/Manage/Purchase/Index.php | 2 +- app/Models/Purchase.php | 4 +- app/Traits/Purchase/WithUpdateStock.php | 45 +++--- app/Traits/Purchase/WithWarehouseStock.php | 129 ++++++++++++++++++ ...5_12_28_080030_create_purchases_table.php} | 2 +- ...28_080031_create_purchase_items_table.php} | 0 .../studio/manage/purchase/form.blade.php | 10 +- .../studio/manage/purchase/index.blade.php | 2 +- 11 files changed, 181 insertions(+), 77 deletions(-) create mode 100644 app/Traits/Purchase/WithWarehouseStock.php rename database/migrations/{2025_10_03_080030_create_purchases_table.php => 2025_12_28_080030_create_purchases_table.php} (91%) rename database/migrations/{2025_10_03_080031_create_purchase_items_table.php => 2025_12_28_080031_create_purchase_items_table.php} (100%) diff --git a/app/Livewire/Datatable/Manage/PurchasesTable.php b/app/Livewire/Datatable/Manage/PurchasesTable.php index 0e425ae..ae1e9f1 100644 --- a/app/Livewire/Datatable/Manage/PurchasesTable.php +++ b/app/Livewire/Datatable/Manage/PurchasesTable.php @@ -25,7 +25,7 @@ class PurchasesTable extends DataTableComponent public function columns(): array { return [ - Column::make('Outlet', 'outlet.name')->searchable(), + Column::make('Gudang', 'warehouse.name')->searchable(), Column::make('Nomor Invoice', 'invoice_number')->searchable(), @@ -73,14 +73,13 @@ public function columns(): array return $actions; }) ->html() - ->hideIf(auth()->user()->cannot('update purchase')), + ->hideIf(auth()->user()->cannot('delete purchase')), ]; } public function builder(): Builder { - return Purchase::select('purchases.id', 'outlet_id', 'invoice_number', 'purchase_date', 'total') - ->with(['outlet', 'items', 'items.purchasable']) - ->whereIn('outlet_id', auth()->user()->outlets->pluck('id')->toArray()); + return Purchase::select('purchases.id', 'warehouse_id', 'invoice_number', 'purchase_date', 'total') + ->with(['warehouse', 'items', 'items.purchasable']); } } diff --git a/app/Livewire/Forms/Studio/Manage/PurchaseForm.php b/app/Livewire/Forms/Studio/Manage/PurchaseForm.php index 9d6ec00..fbb0d26 100644 --- a/app/Livewire/Forms/Studio/Manage/PurchaseForm.php +++ b/app/Livewire/Forms/Studio/Manage/PurchaseForm.php @@ -6,14 +6,14 @@ use App\Rules\UnsignedInteger; use App\Traits\Media\WithMediaHandler; use App\Traits\Purchase\WithPurchaseItems; -use App\Traits\Purchase\WithUpdateStock; +use App\Traits\Purchase\WithWarehouseStock; use Illuminate\Support\Facades\DB; use Illuminate\Validation\Rule; use Livewire\Form; class PurchaseForm extends Form { - use WithMediaHandler, WithPurchaseItems, WithUpdateStock; + use WithMediaHandler, WithPurchaseItems, WithWarehouseStock; public ?Purchase $purchase = null; @@ -25,7 +25,7 @@ class PurchaseForm extends Form public string $total = ''; - public string $outlet_id = ''; + public string $warehouse_id = ''; public array $image = []; @@ -51,7 +51,7 @@ public function rules(): array 'invoice_number' => ['nullable', 'string', 'max:50'], 'note' => ['nullable', 'string', 'max:100'], 'purchase_date' => ['required', 'date', 'before_or_equal:today'], - 'outlet_id' => ['required', Rule::exists('outlets', 'id')], + 'warehouse_id' => ['required', Rule::exists('warehouses', 'id')], 'total' => ['required', new UnsignedInteger], 'image' => ['nullable', 'array', 'max:1'], ]; @@ -63,7 +63,7 @@ public function validationAttributes(): array 'invoice_number' => 'nomor invoice', 'note' => 'catatan', 'purchase_date' => 'tanggal belanja', - 'outlet_id' => 'outlet', + 'warehouse_id' => 'gudang', 'image' => 'gambar', ]; } @@ -75,7 +75,7 @@ public function setPurchase(Purchase $purchase): void $this->invoice_number = $purchase->invoice_number; $this->note = $purchase->note; $this->purchase_date = $purchase->purchase_date; - $this->outlet_id = $purchase->outlet_id; + $this->warehouse_id = $purchase->warehouse_id; $this->total = $purchase->total; $this->image = $this->mapMediaCollection($purchase->getMedia('image')); @@ -101,7 +101,7 @@ public function store(): void $purchase = Purchase::create($data); - $purchase->load('outlet'); + $purchase->load('warehouse'); $this->uploadMedia($this->image, $purchase, 'image'); @@ -113,36 +113,17 @@ public function store(): void $item->update(['purchase_id' => $purchase->id]); - $this->increaseOutletStock($purchase->outlet, $item); + $this->increaseWarehouseStock($purchase->warehouse, $item); } }); } - public function update(): void - { - $this->validate(); - - // 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'); - }); - } - private function prepareSavedData(): array { return [ 'invoice_number' => $this->invoice_number, 'note' => $this->note, - 'outlet_id' => $this->outlet_id, + 'warehouse_id' => $this->warehouse_id, 'purchase_date' => $this->purchase_date, 'total' => parseRupiahToInt($this->total), ]; diff --git a/app/Livewire/Studio/Manage/Purchase/Create.php b/app/Livewire/Studio/Manage/Purchase/Create.php index 15eee93..6dfe3a5 100644 --- a/app/Livewire/Studio/Manage/Purchase/Create.php +++ b/app/Livewire/Studio/Manage/Purchase/Create.php @@ -6,6 +6,7 @@ use App\Models\Bottle; use App\Models\Perfume; use App\Models\Product; +use App\Models\Warehouse as WarehouseModel; use App\Traits\Authorization\WithAuthorization; use App\Traits\Components\WithConfirmation; use App\Traits\Components\WithToast; @@ -26,7 +27,7 @@ class Create extends Component public PurchaseForm $form; - public array $outlets = []; + public array $warehouses = []; public array $perfumes = []; @@ -38,13 +39,13 @@ class Create extends Component public function mount(): void { - $this->outlets = auth()->user()->outlets->pluck('name', 'id')->toArray(); + $this->warehouses = WarehouseModel::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->perfumes = Perfume::orderBy('name')->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->bottles = Bottle::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->products = Product::orderBy('name')->pluck('name', 'id')->toArray(); $this->purchaseItems = $this->loadPurchaseItems(); @@ -68,13 +69,6 @@ public function save(): void ], navigate: true); } - 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', [ diff --git a/app/Livewire/Studio/Manage/Purchase/Index.php b/app/Livewire/Studio/Manage/Purchase/Index.php index 651bf32..a7453bd 100644 --- a/app/Livewire/Studio/Manage/Purchase/Index.php +++ b/app/Livewire/Studio/Manage/Purchase/Index.php @@ -27,7 +27,7 @@ public function delete(Purchase $purchase): void DB::transaction(function () use ($purchase) { foreach ($purchase->items as $item) { - $this->decreaseOutletStock($purchase->outlet, $item); + $this->decreaseWarehouseStock($purchase->warehouse, $item); } $purchase->delete(); diff --git a/app/Models/Purchase.php b/app/Models/Purchase.php index 3d5f176..30e2259 100644 --- a/app/Models/Purchase.php +++ b/app/Models/Purchase.php @@ -38,9 +38,9 @@ public function yesterday(Builder $query): void $query->whereDate('created_at', today()->subDay()); } - public function outlet(): BelongsTo + public function warehouse(): BelongsTo { - return $this->belongsTo(Outlet::class); + return $this->belongsTo(Warehouse::class); } public function items(): HasMany diff --git a/app/Traits/Purchase/WithUpdateStock.php b/app/Traits/Purchase/WithUpdateStock.php index 56ab602..5200f56 100644 --- a/app/Traits/Purchase/WithUpdateStock.php +++ b/app/Traits/Purchase/WithUpdateStock.php @@ -9,7 +9,7 @@ trait WithUpdateStock { - public function increaseOutletStock($outlet, $item): void + public function increaseWarehouseStock($warehouse, $item): void { $quantity = $item->quantity; @@ -28,7 +28,7 @@ public function increaseOutletStock($outlet, $item): void } // Lock the row to prevent race conditions - $existing = $outlet->{$relation}() + $existing = $warehouse->{$relation}() ->where("{$relation}.id", $item->purchasable_id) ->lockForUpdate() ->withPivot('stock') @@ -36,22 +36,22 @@ public function increaseOutletStock($outlet, $item): void if ($existing) { $previousStock = $existing->pivot->stock ?? 0; - $outlet->{$relation}()->updateExistingPivot($item->purchasable_id, [ + $warehouse->{$relation}()->updateExistingPivot($item->purchasable_id, [ 'stock' => $previousStock + $quantity, ]); } else { $previousStock = 0; - $outlet->{$relation}()->attach($item->purchasable_id, [ + $warehouse->{$relation}()->attach($item->purchasable_id, [ 'stock' => $quantity, ]); } - StockActivityLogService::log( + StockActivityLogService::logWarehouse( logName: 'Belanja', event: 'Menambah', - description: 'Stok '.$item->purchasable->name.' di '.$outlet->name.' bertambah '.formatCurrencyNumber($quantity), + description: 'Stok '.$item->purchasable->name.' di '.$warehouse->name.' bertambah '.formatCurrencyNumber($quantity), performedOn: $item->purchasable, - outlet: $outlet, + warehouse: $warehouse, quantityChange: $quantity, previousStock: $previousStock, newStock: $previousStock + $quantity, @@ -63,7 +63,7 @@ public function increaseOutletStock($outlet, $item): void ); } - public function decreaseOutletStock($outlet, $item): void + public function decreaseWarehouseStock($warehouse, $item): void { $quantity = $item->quantity; @@ -81,31 +81,32 @@ public function decreaseOutletStock($outlet, $item): void return; } - $existing = $outlet->{$relation}() + $existing = $warehouse->{$relation}() ->where("{$relation}.id", $item->purchasable_id) ->lockForUpdate() ->withPivot('stock') ->first(); + $currentStock = $existing?->pivot->stock ?? 0; + $newStock = max(0, $currentStock - $quantity); + if ($existing) { - $currentStock = $existing->pivot->stock ?? 0; - $newStock = max(0, $currentStock - $quantity); - $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; + if ($newStock === 0) { + $warehouse->{$relation}()->detach($item->purchasable_id); + } else { + $warehouse->{$relation}()->updateExistingPivot( + $item->purchasable_id, + ['stock' => $newStock] + ); + } } - StockActivityLogService::log( + StockActivityLogService::logWarehouse( logName: 'Belanja', event: 'Mengurangi', - description: 'Stok '.$item->purchasable->name.' di '.$outlet->name.' berkurang '.$quantity, + description: 'Stok '.$item->purchasable->name.' di '.$warehouse->name.' berkurang '.$quantity, performedOn: $item->purchasable, - outlet: $outlet, + warehouse: $warehouse, quantityChange: $quantity, previousStock: $currentStock, newStock: $newStock, diff --git a/app/Traits/Purchase/WithWarehouseStock.php b/app/Traits/Purchase/WithWarehouseStock.php new file mode 100644 index 0000000..4c798cb --- /dev/null +++ b/app/Traits/Purchase/WithWarehouseStock.php @@ -0,0 +1,129 @@ +quantity; + + switch ($item->purchasable_type) { + case Perfume::class: + $relation = 'perfumes'; + break; + case Product::class: + $relation = 'products'; + break; + case Bottle::class: + $relation = 'bottles'; + break; + default: + return; + } + + // Lock the row to prevent race conditions + $existing = $warehouse->{$relation}() + ->where("{$relation}.id", $item->purchasable_id) + ->lockForUpdate() + ->withPivot('stock') + ->first(); + + if ($existing) { + $previousStock = $existing->pivot->stock ?? 0; + $warehouse->{$relation}()->updateExistingPivot($item->purchasable_id, [ + 'stock' => $previousStock + $quantity, + ]); + } else { + $previousStock = 0; + $warehouse->{$relation}()->attach($item->purchasable_id, [ + 'stock' => $quantity, + ]); + } + + $purchase = $item->purchase ?? \App\Models\Purchase::find($item->purchase_id); + + if ($purchase) { + StockActivityLogService::purchase( + purchase: $purchase, + product: $item->purchasable, + quantity: $quantity, + previousStock: $previousStock, + newStock: $previousStock + $quantity + ); + } else { + StockActivityLogService::logWarehouse( + logName: StockActivityLogService::LOG_PURCHASE, + event: 'increase', + description: 'Stok '.$item->purchasable->name.' di gudang '.$warehouse->name.' bertambah '.formatCurrencyNumber($quantity), + performedOn: $item->purchasable, + warehouse: $warehouse, + quantityChange: $quantity, + previousStock: $previousStock, + newStock: $previousStock + $quantity, + extraProperties: [ + 'purchasable_id' => $item->purchasable_id, + 'purchasable_type' => $item->purchasable_type, + 'purchase_id' => $item->purchase_id ?? null, + ] + ); + } + } + + public function decreaseWarehouseStock($warehouse, $item): void + { + $quantity = $item->quantity; + + switch ($item->purchasable_type) { + case Perfume::class: + $relation = 'perfumes'; + break; + case Product::class: + $relation = 'products'; + break; + case Bottle::class: + $relation = 'bottles'; + break; + default: + return; + } + + $existing = $warehouse->{$relation}() + ->where("{$relation}.id", $item->purchasable_id) + ->lockForUpdate() + ->withPivot('stock') + ->first(); + + if ($existing) { + $currentStock = $existing->pivot->stock ?? 0; + $newStock = max(0, $currentStock - $quantity); + $warehouse->{$relation}()->updateExistingPivot($item->purchasable_id, [ + 'stock' => $newStock, + ]); + } else { + $currentStock = 0; + $newStock = 0; + } + + StockActivityLogService::logWarehouse( + logName: StockActivityLogService::LOG_PURCHASE, + event: 'decrease', + description: 'Stok '.$item->purchasable->name.' di gudang '.$warehouse->name.' berkurang '.$quantity, + performedOn: $item->purchasable, + warehouse: $warehouse, + quantityChange: -1 * abs($quantity), + previousStock: $currentStock, + newStock: $newStock, + extraProperties: [ + 'purchasable_id' => $item->purchasable_id, + 'purchasable_type' => $item->purchasable_type, + 'purchase_id' => $item->purchase_id, + ] + ); + } +} diff --git a/database/migrations/2025_10_03_080030_create_purchases_table.php b/database/migrations/2025_12_28_080030_create_purchases_table.php similarity index 91% rename from database/migrations/2025_10_03_080030_create_purchases_table.php rename to database/migrations/2025_12_28_080030_create_purchases_table.php index 5b6ca14..34e2630 100644 --- a/database/migrations/2025_10_03_080030_create_purchases_table.php +++ b/database/migrations/2025_12_28_080030_create_purchases_table.php @@ -13,7 +13,7 @@ public function up(): void { Schema::create('purchases', function (Blueprint $table) { $table->id(); - $table->foreignId('outlet_id')->constrained()->cascadeOnDelete(); + $table->foreignId('warehouse_id')->constrained()->cascadeOnDelete(); $table->string('invoice_number', 50)->nullable(); $table->date('purchase_date'); $table->unsignedInteger('total'); diff --git a/database/migrations/2025_10_03_080031_create_purchase_items_table.php b/database/migrations/2025_12_28_080031_create_purchase_items_table.php similarity index 100% rename from database/migrations/2025_10_03_080031_create_purchase_items_table.php rename to database/migrations/2025_12_28_080031_create_purchase_items_table.php diff --git a/resources/views/livewire/studio/manage/purchase/form.blade.php b/resources/views/livewire/studio/manage/purchase/form.blade.php index 9a77f46..506adc2 100644 --- a/resources/views/livewire/studio/manage/purchase/form.blade.php +++ b/resources/views/livewire/studio/manage/purchase/form.blade.php @@ -48,15 +48,15 @@ - Outlet * - - @foreach ($outlets as $key => $value) + Gudang * + + @foreach ($warehouses as $key => $value) {{ $value }} @endforeach - + diff --git a/resources/views/livewire/studio/manage/purchase/index.blade.php b/resources/views/livewire/studio/manage/purchase/index.blade.php index 9d9613b..bd44c89 100644 --- a/resources/views/livewire/studio/manage/purchase/index.blade.php +++ b/resources/views/livewire/studio/manage/purchase/index.blade.php @@ -17,7 +17,7 @@ class="text-sm"> Informasi - Menghapus data belanja otomatis akan menambah stok. + Menghapus data belanja otomatis menghapus stok dari gudang.