feat: add voucher discount functionality and update related calculations across orders
This commit is contained in:
parent
c0fe92f0d2
commit
95a252dab7
@ -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) => "
|
||||
<div class='text-[13px] leading-tight mb-1'>
|
||||
<div class='font-bold text-gray-800 dark:text-white'>{$value['name']}</div>
|
||||
<div class='text-gray-400 text-[12px]'>{$value['quantity']} x {$value['unit_price']}</div>
|
||||
".($value['has_quality']
|
||||
? "<div class='text-gray-400 text-[12px]'>{$value['quantity']}</div>"
|
||||
: "<div class='text-gray-400 text-[12px]'>{$value['quantity']} x {$value['unit_price']}</div>"
|
||||
)."
|
||||
<div class='text-gray-400 text-[12px]'>{$value['total_price']}</div>
|
||||
</div>"
|
||||
)
|
||||
</div>
|
||||
")
|
||||
->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',
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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 [
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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'),
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
];
|
||||
});
|
||||
|
||||
@ -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());
|
||||
|
||||
@ -336,7 +336,7 @@
|
||||
<flux:description>Voucher diurutkan berdasarkan dengan diskon yang paling besar.
|
||||
</flux:description>
|
||||
<flux:select variant="listbox" placeholder="Pilih Voucher"
|
||||
wire:model="form.voucher_id" searchable clearable>
|
||||
wire:model.live.debounce.250ms="form.voucher_id" searchable clearable>
|
||||
@foreach ($vouchers as $key => $name)
|
||||
<flux:select.option value="{{ $key }}" key="{{ $key }}">
|
||||
{{ $name }}
|
||||
|
||||
@ -104,13 +104,15 @@
|
||||
<h5 class="sm:hidden text-xs font-medium text-gray-500 uppercase dark:text-zinc-400">
|
||||
Kuantitas
|
||||
</h5>
|
||||
<p class="text-gray-800 dark:text-zinc-100">{{ $item['quantity'] }}</p>
|
||||
<p class="text-gray-800 dark:text-zinc-100">{{ $item['quality'] ?: $item['quantity'] }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="sm:hidden text-xs font-medium text-gray-500 uppercase dark:text-zinc-400">
|
||||
Harga
|
||||
</h5>
|
||||
<p class="text-gray-800 dark:text-zinc-100">{{ $item['unit_price'] }}</p>
|
||||
<p class="text-gray-800 dark:text-zinc-100">
|
||||
{{ $item['quality'] ? '-' : $item['unit_price'] }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="sm:hidden text-xs font-medium text-gray-500 uppercase dark:text-zinc-400">
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user