From 95a252dab7f8b434ac87769dc822f61cfbb48079 Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Fri, 9 Jan 2026 20:18:58 +0700 Subject: [PATCH] feat: add voucher discount functionality and update related calculations across orders --- app/Livewire/Datatable/Manage/OrdersTable.php | 18 ++++++++++------- .../Forms/Studio/Manage/OrderForm.php | 5 +++-- app/Livewire/Studio/Dashboard/Analysis.php | 4 ++-- app/Livewire/Studio/Dashboard/Overview.php | 4 ++-- app/Livewire/Studio/Loyalty/Tier.php | 2 +- app/Livewire/Studio/Manage/Order/Index.php | 2 +- app/Livewire/Studio/Manage/Order/Show.php | 3 ++- app/Models/Order.php | 1 + database/factories/OrderFactory.php | 4 ++++ .../2025_10_06_135045_create_orders_table.php | 1 + .../studio/manage/order/form.blade.php | 2 +- .../studio/manage/order/show.blade.php | 6 ++++-- .../Studio/Manage/Order/CreateTest.php | 20 ++++++++++++++----- 13 files changed, 48 insertions(+), 24 deletions(-) diff --git a/app/Livewire/Datatable/Manage/OrdersTable.php b/app/Livewire/Datatable/Manage/OrdersTable.php index 8b44555..d779971 100644 --- a/app/Livewire/Datatable/Manage/OrdersTable.php +++ b/app/Livewire/Datatable/Manage/OrdersTable.php @@ -54,26 +54,29 @@ public function columns(): array ->data( fn ($value, $row) => $row->items->map(fn ($item) => [ 'name' => $item->orderable?->name, - 'quantity' => formatCurrencyNumber($item->quantity, ''), + 'quantity' => $item->quality && $item->quality !== 'Custom' ? $item->quality : formatCurrencyNumber($item->quantity, ''), 'unit_price' => formatCurrencyNumber($item->unit_price, 'Rp'), 'total_price' => formatCurrencyNumber($item->total_price, 'Rp'), + 'has_quality' => $item->quality && $item->quality !== 'Custom', ])->toArray() ) - ->outputFormat( - fn ($index, $value) => " + ->outputFormat(fn ($index, $value) => "
{$value['name']}
-
{$value['quantity']} x {$value['unit_price']}
+ ".($value['has_quality'] + ? "
{$value['quantity']}
" + : "
{$value['quantity']} x {$value['unit_price']}
" + )."
{$value['total_price']}
-
" - ) + + ") ->flexCol(['class' => 'flex-col gap-3']), Column::make('Ringkasan') ->label(function ($value) { $subtotal = formatCurrencyNumber($value->subtotal, 'Rp'); - $discount = formatCurrencyNumber($value->discount, 'Rp'); + $discount = formatCurrencyNumber($value->discount + $value->voucher_discount, 'Rp'); $total = formatCurrencyNumber($value->total, 'Rp'); @@ -151,6 +154,7 @@ public function builder(): Builder 'cogs', 'subtotal', 'discount', + 'voucher_discount', 'total', 'orders.status', 'channel', diff --git a/app/Livewire/Forms/Studio/Manage/OrderForm.php b/app/Livewire/Forms/Studio/Manage/OrderForm.php index 7774f16..126ae41 100644 --- a/app/Livewire/Forms/Studio/Manage/OrderForm.php +++ b/app/Livewire/Forms/Studio/Manage/OrderForm.php @@ -118,7 +118,7 @@ public function store(): array // Wrap the entire operation in a database transaction // Ensures atomicity — if any step fails, all changes are rolled back try { - DB::transaction(function () use (&$order, $invoiceNumber, $cogs, $subtotal, $discount, $total, $items, $pointsEarned) { + DB::transaction(function () use (&$order, $invoiceNumber, $cogs, $subtotal, $manualDiscount, $voucherDiscount, $total, $items, $pointsEarned) { $customer = $this->member_id ? Customer::find($this->member_id) : null; $member = $customer?->user()->with(['membership' => fn ($q) => $q->lockForUpdate()])->first(); @@ -130,7 +130,8 @@ public function store(): array 'invoice_number' => $invoiceNumber, 'cogs' => $cogs, 'subtotal' => $subtotal, - 'discount' => $discount, + 'discount' => $manualDiscount, + 'voucher_discount' => $voucherDiscount, 'total' => $total, 'channel' => (int) $this->channel, 'status' => (int) $this->status, diff --git a/app/Livewire/Studio/Dashboard/Analysis.php b/app/Livewire/Studio/Dashboard/Analysis.php index f8a371f..7e632fd 100644 --- a/app/Livewire/Studio/Dashboard/Analysis.php +++ b/app/Livewire/Studio/Dashboard/Analysis.php @@ -187,7 +187,7 @@ public function render(): View $totalIncome = (clone $orderQuery)->sum('total'); $totalCogs = (clone $orderQuery)->sum('cogs'); - $totalDiscount = (clone $orderQuery)->sum('discount'); + $totalDiscount = (clone $orderQuery)->sum(DB::raw('discount + COALESCE(voucher_discount, 0)')); $expenseQuery = Expense::when(! empty($outletIds), fn ($q) => $q->whereIn('outlet_id', $outletIds)) ->when($dateQuery || ($startDate && $endDate), $filterDate); @@ -389,7 +389,7 @@ public function render(): View $ordersTrend = Order::query() ->when(! empty($outletIds), fn ($q) => $q->whereIn('outlet_id', $outletIds)) ->where('created_at', '>=', now()->subDays(30)->startOfDay()) - ->select(DB::raw('DATE(created_at) as date'), DB::raw('SUM(total) as revenue'), DB::raw('SUM(total - cogs - discount) as gross_profit'), DB::raw('count(*) as count')) + ->select(DB::raw('DATE(created_at) as date'), DB::raw('SUM(total) as revenue'), DB::raw('SUM(total - cogs - discount - COALESCE(voucher_discount,0)) as gross_profit'), DB::raw('count(*) as count')) ->groupBy('date') ->get() ->keyBy('date'); diff --git a/app/Livewire/Studio/Dashboard/Overview.php b/app/Livewire/Studio/Dashboard/Overview.php index 2d5fec5..c731b95 100644 --- a/app/Livewire/Studio/Dashboard/Overview.php +++ b/app/Livewire/Studio/Dashboard/Overview.php @@ -79,11 +79,11 @@ public function render(): View $todayDiscount = Order::when(! empty($outletIds), fn ($q) => $q->whereIn('outlet_id', $outletIds)) ->today() - ->sum('discount'); + ->sum(DB::raw('discount + COALESCE(voucher_discount, 0)')); $yesterdayDiscount = Order::when(! empty($outletIds), fn ($q) => $q->whereIn('outlet_id', $outletIds)) ->yesterday() - ->sum('discount'); + ->sum(DB::raw('discount + COALESCE(voucher_discount, 0)')); $todayVoucher = Order::when(! empty($outletIds), fn ($q) => $q->whereIn('outlet_id', $outletIds)) ->today() diff --git a/app/Livewire/Studio/Loyalty/Tier.php b/app/Livewire/Studio/Loyalty/Tier.php index e806328..91e0937 100644 --- a/app/Livewire/Studio/Loyalty/Tier.php +++ b/app/Livewire/Studio/Loyalty/Tier.php @@ -53,7 +53,7 @@ public function getTopMemberStats($tierId): ?array // Sum total $subtotal = $orders->sum('subtotal'); - $totalDiscount = $orders->sum('discount'); + $totalDiscount = $orders->sum(fn ($o) => ($o->discount ?? 0) + ($o->voucher_discount ?? 0)); $grandTotal = $orders->sum('total'); return [ diff --git a/app/Livewire/Studio/Manage/Order/Index.php b/app/Livewire/Studio/Manage/Order/Index.php index 9570ec6..8f862f0 100644 --- a/app/Livewire/Studio/Manage/Order/Index.php +++ b/app/Livewire/Studio/Manage/Order/Index.php @@ -69,7 +69,7 @@ public function print(Order $order): void outlet_name: $order->outlet?->name ?? '', outlet_address: $order->outlet?->address ?? '', subtotal: $order->subtotal, - discount: $order->discount, + discount: ($order->discount ?? 0) + ($order->voucher_discount ?? 0), total: $order->total, items: $order->items->map(function ($item) { $item->name = $item->orderable?->name; diff --git a/app/Livewire/Studio/Manage/Order/Show.php b/app/Livewire/Studio/Manage/Order/Show.php index f52d525..78a9eba 100644 --- a/app/Livewire/Studio/Manage/Order/Show.php +++ b/app/Livewire/Studio/Manage/Order/Show.php @@ -29,7 +29,7 @@ public function mount(Order $order): void 'date' => formatDateTimeLocalized($order->created_at), 'channel' => $order->channel->label(), 'subtotal' => formatCurrencyNumber($order->subtotal, 'Rp'), - 'discount' => formatCurrencyNumber($order->discount, 'Rp'), + 'discount' => formatCurrencyNumber(($order->discount ?? 0) + ($order->voucher_discount ?? 0), 'Rp'), 'total' => formatCurrencyNumber($order->total, 'Rp'), ]; @@ -37,6 +37,7 @@ public function mount(Order $order): void ->map(fn (OrderItem $item) => [ 'id' => $item->hash, 'name' => $item->orderable?->name, + 'quality' => $item->quality, 'quantity' => $item->quantity, 'unit_price' => formatCurrencyNumber($item->unit_price, 'Rp'), 'total' => formatCurrencyNumber($item->quantity * $item->unit_price, 'Rp'), diff --git a/app/Models/Order.php b/app/Models/Order.php index 67a4a33..86541bb 100644 --- a/app/Models/Order.php +++ b/app/Models/Order.php @@ -28,6 +28,7 @@ protected function casts(): array 'cogs' => 'int', 'subtotal' => 'int', 'discount' => 'int', + 'voucher_discount' => 'int', 'total' => 'int', 'channel' => OrderChannel::class, 'status' => OrderStatus::class, diff --git a/database/factories/OrderFactory.php b/database/factories/OrderFactory.php index 500f367..0aa842a 100644 --- a/database/factories/OrderFactory.php +++ b/database/factories/OrderFactory.php @@ -36,6 +36,7 @@ public function definition(): array 'cogs' => $cogs, 'subtotal' => $subtotal, 'discount' => $discount, + 'voucher_discount' => 0, 'total' => $total, 'channel' => $this->faker->randomElement(OrderChannel::cases()), 'status' => $this->faker->randomElement(OrderStatus::cases()), @@ -138,6 +139,7 @@ public function withAmounts(int $subtotal, ?int $discount = null, ?int $cogs = n return [ 'subtotal' => $subtotal, 'discount' => $discount, + 'voucher_discount' => 0, 'total' => $total, 'cogs' => $cogs, ]; @@ -152,6 +154,7 @@ public function withDiscount(int $discount): Factory return [ 'discount' => $discount, + 'voucher_discount' => 0, 'total' => max(0, $total), ]; }); @@ -164,6 +167,7 @@ public function withoutDiscount(): Factory return [ 'discount' => 0, + 'voucher_discount' => 0, 'total' => $subtotal, ]; }); diff --git a/database/migrations/2025_10_06_135045_create_orders_table.php b/database/migrations/2025_10_06_135045_create_orders_table.php index fbd72e9..42ed769 100644 --- a/database/migrations/2025_10_06_135045_create_orders_table.php +++ b/database/migrations/2025_10_06_135045_create_orders_table.php @@ -23,6 +23,7 @@ public function up(): void $table->unsignedInteger('cogs'); $table->unsignedInteger('subtotal'); $table->unsignedInteger('discount'); + $table->unsignedInteger('voucher_discount')->default(0); $table->unsignedInteger('total'); $table->enum('channel', OrderChannel::values())->default(OrderChannel::OUTLET)->comment(OrderChannel::comment()); $table->enum('status', OrderStatus::values())->default(OrderStatus::PENDING)->comment(OrderStatus::comment()); diff --git a/resources/views/livewire/studio/manage/order/form.blade.php b/resources/views/livewire/studio/manage/order/form.blade.php index aa30514..84ebee4 100644 --- a/resources/views/livewire/studio/manage/order/form.blade.php +++ b/resources/views/livewire/studio/manage/order/form.blade.php @@ -336,7 +336,7 @@ Voucher diurutkan berdasarkan dengan diskon yang paling besar. + wire:model.live.debounce.250ms="form.voucher_id" searchable clearable> @foreach ($vouchers as $key => $name) {{ $name }} diff --git a/resources/views/livewire/studio/manage/order/show.blade.php b/resources/views/livewire/studio/manage/order/show.blade.php index c7be973..11bf775 100644 --- a/resources/views/livewire/studio/manage/order/show.blade.php +++ b/resources/views/livewire/studio/manage/order/show.blade.php @@ -104,13 +104,15 @@
Kuantitas
-

{{ $item['quantity'] }}

+

{{ $item['quality'] ?: $item['quantity'] }} +

Harga
-

{{ $item['unit_price'] }}

+

+ {{ $item['quality'] ? '-' : $item['unit_price'] }}

diff --git a/tests/Feature/Studio/Manage/Order/CreateTest.php b/tests/Feature/Studio/Manage/Order/CreateTest.php index e04ab6b..f7f16be 100644 --- a/tests/Feature/Studio/Manage/Order/CreateTest.php +++ b/tests/Feature/Studio/Manage/Order/CreateTest.php @@ -328,18 +328,23 @@ $this->outlet->products()->attach($product->id, ['stock' => 10]); $memberUser = User::factory()->create(); - $memberUser->customer()->create([ + $customer = $memberUser->customer()->create([ 'name' => 'Voucher User', 'gender' => Gender::MALE->value, ]); - $memberUser->membership()->create(['tier_id' => $this->tier->id]); + $memberUser->membership()->create([ + 'tier_id' => $this->tier->id, + 'total_spending' => 0, + 'reward_points' => 0, + ]); $voucher = Voucher::factory()->create([ 'type' => VoucherType::FIXED->value, 'discount_amount' => 20000, + 'start_date' => now()->subDay()->format('Y-m-d'), // Make it active ]); - // Assign voucher to user + // Assign voucher to user and outlet DB::table('user_voucher')->insert([ 'user_id' => $memberUser->id, 'voucher_id' => $voucher->id, @@ -348,6 +353,8 @@ 'updated_at' => now(), ]); + $voucher->outlets()->attach($this->outlet->id); + OrderItem::factory()->forProduct($product)->create([ 'user_id' => $this->user->id, 'order_id' => null, @@ -360,13 +367,16 @@ ->set('form.outlet_id', $this->outlet->id) ->set('form.member_id', $memberUser->customer->id) ->set('form.voucher_id', $voucher->id) + ->set('form.payment_method', '1') // Cash ->call('save') - ->assertHasNoErrors(); + ->assertHasNoErrors() + ->assertRedirect(); $this->assertDatabaseHas('orders', [ 'voucher_id' => $voucher->id, 'total' => 80000, - 'discount' => 20000, + 'discount' => 0, // Manual discount + 'voucher_discount' => 20000, // Voucher discount ]); // Check voucher used up