feat: introduce PaymentType enum and integrate payment type selection in order management, including validation and UI updates

This commit is contained in:
Yoga Pangestu 2026-06-18 00:33:39 +07:00
parent 91f7858cfe
commit 191743632b
12 changed files with 104 additions and 2 deletions

25
app/Enums/PaymentType.php Normal file
View File

@ -0,0 +1,25 @@
<?php
namespace App\Enums;
use App\Traits\ProvidesEnumOptions;
enum PaymentType: string
{
use ProvidesEnumOptions;
case CASH = 'cash';
case TRANSFER = 'transfer';
case QRIS = 'qris';
case MARKETPLACE = 'marketplace';
public function label(): string
{
return match ($this) {
self::CASH => 'Tunai',
self::TRANSFER => 'Transfer',
self::QRIS => 'Qris',
self::MARKETPLACE => 'Marketplace',
};
}
}

View File

@ -4,6 +4,7 @@
use App\Enums\OrderChannel;
use App\Enums\OrderStatus;
use App\Enums\PaymentType;
use App\Http\Controllers\Concerns\FlashesEntityMessage;
use App\Http\Controllers\Concerns\ParsesDataTableQuery;
use App\Http\Controllers\Controller;
@ -45,6 +46,7 @@ public function create(Request $request): Response
'catalog' => $this->orderService->catalogItems(user: $user),
'channels' => OrderChannel::selectOptions(),
'storePriceTypes' => $this->orderService->storePriceTypeOptions(),
'paymentTypes' => PaymentType::selectOptions(),
'draftItems' => $this->orderService->draftItemsForUser($user),
]);
}
@ -73,6 +75,7 @@ public function edit(Order $order): Response|RedirectResponse
'catalog' => $this->orderService->catalogItems($order),
'channels' => OrderChannel::selectOptions(),
'storePriceTypes' => $this->orderService->storePriceTypeOptions(),
'paymentTypes' => PaymentType::selectOptions(),
]);
}

View File

@ -3,6 +3,7 @@
namespace App\Http\Requests\Admin\Manage;
use App\Enums\OrderChannel;
use App\Enums\PaymentType;
use App\Enums\Permission;
use App\Enums\PriceType;
use App\Models\Order;
@ -31,6 +32,7 @@ public function rules(): array
'marketing_id' => ['nullable', 'integer', Rule::exists('users', 'id')->whereNull('deleted_at')],
'channel' => ['required', Rule::enum(OrderChannel::class)],
'price_type' => ['required', Rule::enum(PriceType::class)],
'payment_type' => ['required', Rule::enum(PaymentType::class)],
'tiktok_order_id' => [Rule::requiredIf(fn () => $this->channel === OrderChannel::TIKTOK->value), 'string', 'max:100'],
'shopee_order_id' => [Rule::requiredIf(fn () => $this->channel === OrderChannel::SHOPEE->value), 'string', 'max:100'],
'discount' => ['nullable', 'integer', 'min:0'],
@ -60,6 +62,7 @@ public function attributes(): array
'marketing_id' => 'marketing',
'channel' => 'channel',
'price_type' => 'tipe harga',
'payment_type' => 'tipe pembayaran',
'tiktok_order_id' => 'ID pesanan TikTok Shop',
'shopee_order_id' => 'ID pesanan Shopee',
'discount' => 'diskon',

View File

@ -135,6 +135,7 @@ public static function labelForReferenceType(?string $referenceType): string
Expense::class => 'Pengeluaran',
EmployeeAdvance::class => 'Kasbon Pegawai',
Payroll::class => 'Gaji Pegawai',
Order::class => 'Pesanan',
default => class_basename($referenceType),
};
}

View File

@ -4,6 +4,7 @@
use App\Enums\OrderChannel;
use App\Enums\OrderStatus;
use App\Enums\PaymentType;
use App\Enums\PriceType;
use App\Models\Concerns\InteractsWithActivityLog;
use Illuminate\Database\Eloquent\Attributes\Appends;
@ -22,6 +23,7 @@
'total_amount_formatted',
'created_at_formatted',
'channel_label',
'payment_type_label',
'price_type_label',
'status_label',
])]
@ -36,6 +38,7 @@ protected function casts(): array
return [
'channel' => OrderChannel::class,
'price_type' => PriceType::class,
'payment_type' => PaymentType::class,
'status' => OrderStatus::class,
'subtotal' => 'integer',
'discount' => 'integer',
@ -97,6 +100,13 @@ public function totalAmountFormatted(): Attribute
);
}
public function paymentTypeLabel(): Attribute
{
return Attribute::make(
get: fn () => $this->payment_type->label(),
);
}
public function priceTypeLabel(): Attribute
{
return Attribute::make(

View File

@ -4,6 +4,7 @@
use App\Enums\OrderChannel;
use App\Enums\OrderStatus;
use App\Enums\PaymentType;
use App\Enums\PriceType;
use App\Models\Customer;
use App\Models\Order;
@ -12,6 +13,7 @@
use App\Models\ProductPrice;
use App\Models\ProductVariant;
use App\Models\User;
use App\Services\Finance\CashService;
use App\Services\System\PushNotificationService;
use App\Services\System\Setting\MarketplaceService;
use App\Support\Media\MediaPresenter;
@ -27,6 +29,7 @@ class OrderService
public function __construct(
private readonly MarketplaceService $marketplaceService,
private readonly PushNotificationService $pushNotificationService,
private readonly CashService $cashService,
) {}
/**
@ -325,6 +328,7 @@ public function create(array $validated, User $user): Order
'marketing_id' => $validated['marketing_id'] ?? null,
'channel' => $channel,
'price_type' => $priceType,
'payment_type' => PaymentType::from($validated['payment_type']),
'status' => OrderStatus::PENDING,
'tiktok_order_id' => $validated['tiktok_order_id'] ?? null,
'shopee_order_id' => $validated['shopee_order_id'] ?? null,
@ -340,6 +344,18 @@ public function create(array $validated, User $user): Order
'notes' => $validated['notes'] ?? null,
]);
if ($order->payment_type === PaymentType::CASH) {
$cashTransaction = $this->cashService->recordIncoming(
$order,
$totalAmount,
"Pembayaran pesanan {$order->order_number}",
$user,
);
$order->cash_transaction_id = $cashTransaction->id;
$order->save();
}
foreach ($draftItems as $item) {
$variant = ProductVariant::query()->with('product')->lockForUpdate()->find($item->product_variant_id);
if ($variant === null) {
@ -453,6 +469,10 @@ public function delete(Order $order): void
}
}
if ($order->cashTransaction) {
$this->cashService->deleteReferencedTransaction($order->cashTransaction);
}
$order->items()->delete();
$order->delete();
});
@ -480,6 +500,11 @@ public function transitionStatus(Order $order, OrderStatus $status): void
foreach ($order->items as $item) {
$this->incrementStock($item);
}
if ($order->cashTransaction) {
$this->cashService->deleteReferencedTransaction($order->cashTransaction);
$order->cash_transaction_id = null;
}
}
$order->status = $status;

View File

@ -2,6 +2,7 @@
use App\Enums\OrderChannel;
use App\Enums\OrderStatus;
use App\Enums\PaymentType;
use App\Enums\PriceType;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
@ -21,6 +22,7 @@ public function up(): void
$table->enum('channel', array_column(OrderChannel::cases(), 'value'));
$table->enum('price_type', array_column(PriceType::cases(), 'value'));
$table->enum('status', array_column(OrderStatus::cases(), 'value'))->default(OrderStatus::PENDING->value);
$table->enum('payment_type', array_column(PaymentType::cases(), 'value'))->default(PaymentType::CASH->value);
$table->string('tiktok_order_id', 100)->nullable();
$table->string('shopee_order_id', 100)->nullable();
$table->unsignedBigInteger('subtotal');

View File

@ -114,9 +114,11 @@ function statusVariant(status: string): 'default' | 'secondary' | 'destructive'
</div>
<div class="flex flex-wrap gap-x-4 gap-y-1 text-sm">
<span>Tipe Harga <strong class="text-foreground">{{ order.price_type_label
}}</strong></span>
}}</strong></span>
<span>Pembayaran <strong class="text-foreground">{{ order.payment_type_label
}}</strong></span>
<span>Subtotal <strong class="text-foreground">{{ order.subtotal_formatted
}}</strong></span>
}}</strong></span>
<span>Diskon <strong class="text-foreground">{{ order.discount_formatted
}}</strong></span>
<span>Total <strong class="text-primary">{{ order.total_amount_formatted

View File

@ -48,11 +48,13 @@ const props = defineProps<{
catalog: OrderCatalogItem[];
channels: EnumOption[];
storePriceTypes: EnumOption[];
paymentTypes: EnumOption[];
initialData?: {
customer_id: string;
marketing_id: string;
channel: string;
price_type: string;
payment_type: string;
tiktok_order_id: string;
shopee_order_id: string;
discount: string;
@ -86,6 +88,7 @@ const form = useForm({
marketing_id: defaultMarketingId.value || 'none',
channel: 'store',
price_type: 'ecer',
payment_type: 'cash',
tiktok_order_id: '',
shopee_order_id: '',
discount: '',
@ -103,6 +106,7 @@ function populateForm() {
form.marketing_id = props.initialData.marketing_id || 'none';
form.channel = props.initialData.channel;
form.price_type = props.initialData.price_type;
form.payment_type = props.initialData.payment_type;
form.tiktok_order_id = props.initialData.tiktok_order_id;
form.shopee_order_id = props.initialData.shopee_order_id;
form.discount = props.initialData.discount;
@ -354,6 +358,7 @@ function buildFormData(): FormData {
formData.append('channel', form.channel);
formData.append('price_type', form.price_type);
formData.append('payment_type', form.payment_type);
if (form.channel === 'tiktok' && form.tiktok_order_id) {
formData.append('tiktok_order_id', form.tiktok_order_id);
@ -539,6 +544,24 @@ function submit() {
<FieldError :errors="formErrors(form, 'shopee_order_id')" />
</Field>
<Field>
<FieldLabel for="payment_type" required>Tipe Pembayaran</FieldLabel>
<Select v-model="form.payment_type">
<SelectTrigger id="payment_type" class="w-full">
<SelectValue placeholder="Pilih tipe pembayaran" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem v-for="pt in paymentTypes" :key="pt.value"
:value="pt.value">
{{ pt.label }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<FieldError :errors="formErrors(form, 'payment_type')" />
</Field>
<Field>
<FieldLabel for="customer">Pelanggan</FieldLabel>
<Select v-model="form.customer_id">

View File

@ -12,6 +12,7 @@ defineProps<{
catalog: OrderCatalogItem[];
channels: EnumOption[];
storePriceTypes: EnumOption[];
paymentTypes: EnumOption[];
draftItems: OrderCartItem[];
}>();
</script>
@ -41,6 +42,7 @@ defineProps<{
:catalog="catalog"
:channels="channels"
:store-price-types="storePriceTypes"
:payment-types="paymentTypes"
:draft-items="draftItems"
submit-url="/admin/manage/orders"
method="post"

View File

@ -14,6 +14,7 @@ const props = defineProps<{
catalog: OrderCatalogItem[];
channels: EnumOption[];
storePriceTypes: EnumOption[];
paymentTypes: EnumOption[];
}>();
const initialData = computed(() => ({
@ -21,6 +22,7 @@ const initialData = computed(() => ({
marketing_id: props.order.marketing_id ? String(props.order.marketing_id) : '',
channel: props.order.channel,
price_type: props.order.price_type,
payment_type: props.order.payment_type,
tiktok_order_id: props.order.tiktok_order_id ?? '',
shopee_order_id: props.order.shopee_order_id ?? '',
discount: String(props.order.discount),
@ -64,6 +66,7 @@ const initialData = computed(() => ({
:catalog="catalog"
:channels="channels"
:store-price-types="storePriceTypes"
:payment-types="paymentTypes"
:initial-data="initialData"
:submit-url="`/admin/manage/orders/${order.id}`"
method="put"

View File

@ -41,6 +41,8 @@ export type OrderListItem = {
channel_label: string;
price_type: string;
price_type_label: string;
payment_type: string;
payment_type_label: string;
status: string;
status_label: string;
is_editable: boolean;
@ -81,6 +83,7 @@ export type OrderEditItem = {
marketing_id: number | null;
channel: string;
price_type: string;
payment_type: string;
status: string;
tiktok_order_id: string | null;
shopee_order_id: string | null;