feat: add is_affiliate field to order management, including validation, database migration, and UI updates
This commit is contained in:
parent
191743632b
commit
1af4af70fa
@ -33,6 +33,7 @@ public function rules(): array
|
||||
'channel' => ['required', Rule::enum(OrderChannel::class)],
|
||||
'price_type' => ['required', Rule::enum(PriceType::class)],
|
||||
'payment_type' => ['required', Rule::enum(PaymentType::class)],
|
||||
'is_affiliate' => ['nullable', 'boolean'],
|
||||
'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'],
|
||||
@ -63,6 +64,7 @@ public function attributes(): array
|
||||
'channel' => 'channel',
|
||||
'price_type' => 'tipe harga',
|
||||
'payment_type' => 'tipe pembayaran',
|
||||
'is_affiliate' => 'afiliasi',
|
||||
'tiktok_order_id' => 'ID pesanan TikTok Shop',
|
||||
'shopee_order_id' => 'ID pesanan Shopee',
|
||||
'discount' => 'diskon',
|
||||
|
||||
@ -39,6 +39,7 @@ protected function casts(): array
|
||||
'channel' => OrderChannel::class,
|
||||
'price_type' => PriceType::class,
|
||||
'payment_type' => PaymentType::class,
|
||||
'is_affiliate' => 'boolean',
|
||||
'status' => OrderStatus::class,
|
||||
'subtotal' => 'integer',
|
||||
'discount' => 'integer',
|
||||
|
||||
@ -329,6 +329,7 @@ public function create(array $validated, User $user): Order
|
||||
'channel' => $channel,
|
||||
'price_type' => $priceType,
|
||||
'payment_type' => PaymentType::from($validated['payment_type']),
|
||||
'is_affiliate' => $validated['is_affiliate'] ?? false,
|
||||
'status' => OrderStatus::PENDING,
|
||||
'tiktok_order_id' => $validated['tiktok_order_id'] ?? null,
|
||||
'shopee_order_id' => $validated['shopee_order_id'] ?? null,
|
||||
@ -339,6 +340,7 @@ public function create(array $validated, User $user): Order
|
||||
$channel,
|
||||
$totalAmount,
|
||||
$this->lineItemsForSnapshot($draftItems),
|
||||
$validated['is_affiliate'] ?? false,
|
||||
),
|
||||
'total_amount' => $totalAmount,
|
||||
'notes' => $validated['notes'] ?? null,
|
||||
@ -417,6 +419,8 @@ public function update(Order $order, array $validated): void
|
||||
$order->marketing_id = $validated['marketing_id'] ?? null;
|
||||
$order->channel = $channel;
|
||||
$order->price_type = $priceType;
|
||||
$order->payment_type = PaymentType::from($validated['payment_type']);
|
||||
$order->is_affiliate = $validated['is_affiliate'] ?? false;
|
||||
$order->tiktok_order_id = $validated['tiktok_order_id'] ?? null;
|
||||
$order->shopee_order_id = $validated['shopee_order_id'] ?? null;
|
||||
$order->subtotal = $subtotal;
|
||||
@ -425,6 +429,7 @@ public function update(Order $order, array $validated): void
|
||||
$channel,
|
||||
$totalAmount,
|
||||
$this->lineItemsForSnapshot($lineItems),
|
||||
$validated['is_affiliate'] ?? false,
|
||||
);
|
||||
$order->total_amount = $totalAmount;
|
||||
$order->notes = $validated['notes'] ?? null;
|
||||
|
||||
@ -84,7 +84,7 @@ public function updateMarketplace(array $validated): void
|
||||
* @param list<array{quantity: int, subtotal: int}> $lineItems
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
public function buildOrderSnapshot(OrderChannel $channel, int $totalAmount, array $lineItems): ?array
|
||||
public function buildOrderSnapshot(OrderChannel $channel, int $totalAmount, array $lineItems, bool $isAffiliate = false): ?array
|
||||
{
|
||||
$feeSnapshot = $this->feeRulesForChannel($channel);
|
||||
|
||||
@ -92,6 +92,10 @@ public function buildOrderSnapshot(OrderChannel $channel, int $totalAmount, arra
|
||||
return null;
|
||||
}
|
||||
|
||||
if (! $isAffiliate) {
|
||||
$feeSnapshot['fees'] = $this->excludeAffiliateFee($feeSnapshot['fees'], $channel);
|
||||
}
|
||||
|
||||
$calculation = app(MarketplaceFeeCalculator::class)->calculate(
|
||||
$feeSnapshot['fees'],
|
||||
$totalAmount,
|
||||
@ -100,6 +104,7 @@ public function buildOrderSnapshot(OrderChannel $channel, int $totalAmount, arra
|
||||
|
||||
return [
|
||||
'platform' => $feeSnapshot['platform'],
|
||||
'is_affiliate' => $isAffiliate,
|
||||
'fees' => $feeSnapshot['fees'],
|
||||
'base_amount' => $totalAmount,
|
||||
'results' => $calculation['results'],
|
||||
@ -108,6 +113,29 @@ public function buildOrderSnapshot(OrderChannel $channel, int $totalAmount, arra
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, array{scope: string, value_type: string, value: float}> $fees
|
||||
* @return array<string, array{scope: string, value_type: string, value: float}>
|
||||
*/
|
||||
private function excludeAffiliateFee(array $fees, OrderChannel $channel): array
|
||||
{
|
||||
$affiliateKey = match ($channel) {
|
||||
OrderChannel::TIKTOK => 'tiktok_shop_affiliate',
|
||||
OrderChannel::SHOPEE => 'ams_commission_fee',
|
||||
default => null,
|
||||
};
|
||||
|
||||
if ($affiliateKey !== null && isset($fees[$affiliateKey])) {
|
||||
$fees[$affiliateKey] = [
|
||||
'scope' => $fees[$affiliateKey]['scope'],
|
||||
'value_type' => $fees[$affiliateKey]['value_type'],
|
||||
'value' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
return $fees;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{platform: string, fees: array<string, array{scope: string, value_type: string, value: float}>}|null
|
||||
*/
|
||||
|
||||
@ -23,6 +23,7 @@ public function up(): void
|
||||
$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->boolean('is_affiliate')->default(false);
|
||||
$table->string('tiktok_order_id', 100)->nullable();
|
||||
$table->string('shopee_order_id', 100)->nullable();
|
||||
$table->unsignedBigInteger('subtotal');
|
||||
|
||||
@ -104,6 +104,9 @@ function statusVariant(status: string): 'default' | 'secondary' | 'destructive'
|
||||
<Badge v-if="order.shopee_order_id" variant="outline">
|
||||
ID Pesanan: {{ order.shopee_order_id }}
|
||||
</Badge>
|
||||
<Badge v-if="order.is_affiliate" variant="outline">
|
||||
Afiliasi
|
||||
</Badge>
|
||||
</div>
|
||||
<div class="text-muted-foreground space-y-1 text-sm">
|
||||
<p v-if="order.customer">
|
||||
@ -114,15 +117,15 @@ 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>
|
||||
}}</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>
|
||||
}}</strong></span>
|
||||
<span>Total <strong class="text-primary">{{ order.total_amount_formatted
|
||||
}}</strong></span>
|
||||
}}</strong></span>
|
||||
</div>
|
||||
<p v-if="order.notes" class="text-muted-foreground text-sm">
|
||||
{{ order.notes }}
|
||||
|
||||
@ -32,6 +32,7 @@ import {
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { OrderChannel } from '@/constants/order-channel';
|
||||
import { apiFetch } from '@/lib/api';
|
||||
@ -55,6 +56,7 @@ const props = defineProps<{
|
||||
channel: string;
|
||||
price_type: string;
|
||||
payment_type: string;
|
||||
is_affiliate: boolean;
|
||||
tiktok_order_id: string;
|
||||
shopee_order_id: string;
|
||||
discount: string;
|
||||
@ -89,6 +91,7 @@ const form = useForm({
|
||||
channel: 'store',
|
||||
price_type: 'ecer',
|
||||
payment_type: 'cash',
|
||||
is_affiliate: false,
|
||||
tiktok_order_id: '',
|
||||
shopee_order_id: '',
|
||||
discount: '',
|
||||
@ -96,6 +99,7 @@ const form = useForm({
|
||||
});
|
||||
|
||||
const isStoreChannel = computed(() => form.channel === 'store');
|
||||
const isMarketplaceChannel = computed(() => form.channel === 'shopee' || form.channel === 'tiktok');
|
||||
|
||||
function populateForm() {
|
||||
if (!props.initialData) {
|
||||
@ -107,6 +111,7 @@ function populateForm() {
|
||||
form.channel = props.initialData.channel;
|
||||
form.price_type = props.initialData.price_type;
|
||||
form.payment_type = props.initialData.payment_type;
|
||||
form.is_affiliate = props.initialData.is_affiliate;
|
||||
form.tiktok_order_id = props.initialData.tiktok_order_id;
|
||||
form.shopee_order_id = props.initialData.shopee_order_id;
|
||||
form.discount = props.initialData.discount;
|
||||
@ -137,11 +142,17 @@ watch(
|
||||
(channel) => {
|
||||
if (channel === 'shopee') {
|
||||
form.price_type = 'shopee';
|
||||
form.payment_type = 'marketplace';
|
||||
} else if (channel === 'tiktok') {
|
||||
form.price_type = 'tiktok';
|
||||
form.payment_type = 'marketplace';
|
||||
} else if (!props.storePriceTypes.some((type) => type.value === form.price_type)) {
|
||||
form.price_type = 'ecer';
|
||||
}
|
||||
|
||||
if (channel === 'store') {
|
||||
form.is_affiliate = false;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@ -360,6 +371,10 @@ function buildFormData(): FormData {
|
||||
formData.append('price_type', form.price_type);
|
||||
formData.append('payment_type', form.payment_type);
|
||||
|
||||
if (form.channel !== 'store') {
|
||||
formData.append('is_affiliate', form.is_affiliate ? '1' : '0');
|
||||
}
|
||||
|
||||
if (form.channel === 'tiktok' && form.tiktok_order_id) {
|
||||
formData.append('tiktok_order_id', form.tiktok_order_id);
|
||||
}
|
||||
@ -546,7 +561,7 @@ function submit() {
|
||||
|
||||
<Field>
|
||||
<FieldLabel for="payment_type" required>Tipe Pembayaran</FieldLabel>
|
||||
<Select v-model="form.payment_type">
|
||||
<Select v-model="form.payment_type" :disabled="isMarketplaceChannel">
|
||||
<SelectTrigger id="payment_type" class="w-full">
|
||||
<SelectValue placeholder="Pilih tipe pembayaran" />
|
||||
</SelectTrigger>
|
||||
@ -562,6 +577,18 @@ function submit() {
|
||||
<FieldError :errors="formErrors(form, 'payment_type')" />
|
||||
</Field>
|
||||
|
||||
<Field v-if="isMarketplaceChannel">
|
||||
<div class="flex items-center gap-3">
|
||||
<Switch id="is_affiliate" :model-value="form.is_affiliate"
|
||||
@update:model-value="form.is_affiliate = $event" />
|
||||
<FieldLabel for="is_affiliate" class="cursor-pointer">Pesanan Afiliasi</FieldLabel>
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Aktifkan jika pesanan ini melalui afiliasi. Biaya afiliasi akan dikenakan sesuai
|
||||
pengaturan marketplace.
|
||||
</p>
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel for="customer">Pelanggan</FieldLabel>
|
||||
<Select v-model="form.customer_id">
|
||||
|
||||
@ -23,6 +23,7 @@ const initialData = computed(() => ({
|
||||
channel: props.order.channel,
|
||||
price_type: props.order.price_type,
|
||||
payment_type: props.order.payment_type,
|
||||
is_affiliate: props.order.is_affiliate,
|
||||
tiktok_order_id: props.order.tiktok_order_id ?? '',
|
||||
shopee_order_id: props.order.shopee_order_id ?? '',
|
||||
discount: String(props.order.discount),
|
||||
|
||||
@ -43,6 +43,7 @@ export type OrderListItem = {
|
||||
price_type_label: string;
|
||||
payment_type: string;
|
||||
payment_type_label: string;
|
||||
is_affiliate: boolean;
|
||||
status: string;
|
||||
status_label: string;
|
||||
is_editable: boolean;
|
||||
@ -84,6 +85,7 @@ export type OrderEditItem = {
|
||||
channel: string;
|
||||
price_type: string;
|
||||
payment_type: string;
|
||||
is_affiliate: boolean;
|
||||
status: string;
|
||||
tiktok_order_id: string | null;
|
||||
shopee_order_id: string | null;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user