refund roadmap pr-1 and pr-2

This commit is contained in:
2026-05-11 13:04:20 +07:00
parent d2b0a780d5
commit 54f4569107
36 changed files with 5750 additions and 19 deletions
+93
View File
@@ -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)
* - 36 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 (0100). */
refundPercentage: number;
/** Label untuk UI. */
label: string;
}
const TIERS: RefundTier[] = [
{ minDaysBefore: 7, refundPercentage: 80, label: "≥ 7 hari sebelum berangkat" },
{ minDaysBefore: 3, refundPercentage: 50, label: "36 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,
};
}