refund roadmap pr-1 and pr-2
This commit is contained in:
@@ -31,4 +31,6 @@ export const LIMITS = {
|
||||
MAX_BANK_ACCOUNT_NUMBER_LENGTH: 32,
|
||||
MAX_REJECTION_REASON_LENGTH: 500,
|
||||
NIK_LENGTH: 16,
|
||||
/** Catatan laporan dari peserta/organizer + catatan admin pada refund. */
|
||||
MAX_REFUND_NOTE_LENGTH: 1000,
|
||||
} as const;
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* Refund policy hardcoded untuk MVP (PR-R3). Akan jadi data-driven di PR-R5.
|
||||
*
|
||||
* Aturan: hitung persentase refund berdasarkan jarak hari ke tanggal berangkat
|
||||
* (UTC calendar day). Selalu integer rupiah — pakai Math.floor supaya tidak
|
||||
* ada sub-rupiah dan total refund tidak pernah melebihi nominal yang dibayar.
|
||||
*
|
||||
* Tier:
|
||||
* - ≥7 hari sebelum berangkat → 80% refund (organizer ambil 20% admin fee)
|
||||
* - 3–6 hari sebelum berangkat → 50% refund
|
||||
* - <3 hari sebelum berangkat / sudah lewat → 0% (tidak ada refund)
|
||||
*/
|
||||
|
||||
import { utcStartOfDay } from "@/lib/trip-dates";
|
||||
|
||||
export interface RefundTier {
|
||||
/** Minimum jumlah hari sebelum berangkat untuk tier ini. */
|
||||
minDaysBefore: number;
|
||||
/** Persentase nominal yang di-refund (0–100). */
|
||||
refundPercentage: number;
|
||||
/** Label untuk UI. */
|
||||
label: string;
|
||||
}
|
||||
|
||||
const TIERS: RefundTier[] = [
|
||||
{ minDaysBefore: 7, refundPercentage: 80, label: "≥ 7 hari sebelum berangkat" },
|
||||
{ minDaysBefore: 3, refundPercentage: 50, label: "3–6 hari sebelum berangkat" },
|
||||
{ minDaysBefore: 0, refundPercentage: 0, label: "Kurang dari 3 hari / sudah lewat" },
|
||||
];
|
||||
|
||||
export function getRefundPolicyTiers(): RefundTier[] {
|
||||
return TIERS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Jumlah hari kalender UTC dari sekarang ke tanggal berangkat. Negative kalau
|
||||
* tanggal sudah lewat. Pakai start-of-day UTC supaya jam tidak mempengaruhi.
|
||||
*/
|
||||
export function daysUntilDeparture(
|
||||
departureDate: Date,
|
||||
now: Date = new Date()
|
||||
): number {
|
||||
const todayMs = utcStartOfDay(now).getTime();
|
||||
const depMs = utcStartOfDay(departureDate).getTime();
|
||||
const oneDayMs = 24 * 60 * 60 * 1000;
|
||||
return Math.floor((depMs - todayMs) / oneDayMs);
|
||||
}
|
||||
|
||||
/** Tier aktif untuk jumlah hari yang diberikan. */
|
||||
export function getTierForDays(days: number): RefundTier {
|
||||
for (const tier of TIERS) {
|
||||
if (days >= tier.minDaysBefore) {
|
||||
return tier;
|
||||
}
|
||||
}
|
||||
return TIERS[TIERS.length - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Hitung nominal refund (IDR integer) berdasarkan harga booking dan jarak ke
|
||||
* tanggal berangkat. Floor supaya tidak pernah > bookingAmount.
|
||||
*/
|
||||
export function calculateRefundAmount(
|
||||
bookingAmount: number,
|
||||
days: number
|
||||
): number {
|
||||
if (bookingAmount <= 0) return 0;
|
||||
const tier = getTierForDays(days);
|
||||
return Math.floor((bookingAmount * tier.refundPercentage) / 100);
|
||||
}
|
||||
|
||||
export interface RefundPreview {
|
||||
days: number;
|
||||
tier: RefundTier;
|
||||
refundAmount: number;
|
||||
bookingAmount: number;
|
||||
}
|
||||
|
||||
/** Bundle lengkap untuk display di UI — preview cancel booking. */
|
||||
export function previewRefund(
|
||||
bookingAmount: number,
|
||||
departureDate: Date,
|
||||
now: Date = new Date()
|
||||
): RefundPreview {
|
||||
const days = daysUntilDeparture(departureDate, now);
|
||||
const tier = getTierForDays(days);
|
||||
return {
|
||||
days,
|
||||
tier,
|
||||
refundAmount: calculateRefundAmount(bookingAmount, days),
|
||||
bookingAmount,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user