Files

139 lines
5.5 KiB
JavaScript

const ZERO_DECIMAL_CURRENCIES = new Set([
'BIF', 'CLP', 'DJF', 'GNF', 'JPY', 'KMF', 'KRW', 'MGA',
'PYG', 'RWF', 'VND', 'VUV', 'XAF', 'XOF', 'XPF',
]);
const THREE_DECIMAL_CURRENCIES = new Set([
'BHD', 'IQD', 'JOD', 'KWD', 'LYD', 'OMR', 'TND',
]);
const FOUR_DECIMAL_CURRENCIES = new Set(['CLF', 'UYW']);
export function currencyMinorUnit(currency) {
const normalized = String(currency || '').trim().toUpperCase();
if (ZERO_DECIMAL_CURRENCIES.has(normalized)) return 0;
if (THREE_DECIMAL_CURRENCIES.has(normalized)) return 3;
if (FOUR_DECIMAL_CURRENCIES.has(normalized)) return 4;
return 2;
}
function formattedCurrencyAmount(amount, currency, minorUnit = currencyMinorUnit(currency)) {
try {
return new Intl.NumberFormat('zh-CN', {
style: 'currency',
currency,
minimumFractionDigits: minorUnit,
maximumFractionDigits: minorUnit,
}).format(amount);
} catch {
return `${currency} ${amount.toFixed(minorUnit)}`;
}
}
export const PROLITE_PRICE_MULTIPLIER = 5;
export function estimateProliteSeatCost(defaultAmount = {}) {
const rawAmountMinor = defaultAmount.amountMinor;
if (rawAmountMinor === undefined || rawAmountMinor === null || rawAmountMinor === '') return null;
const amountMinor = Number(rawAmountMinor) * PROLITE_PRICE_MULTIPLIER;
const currency = String(defaultAmount.currency || '').trim().toUpperCase();
const minorUnit = Number(defaultAmount.minorUnit);
if (!Number.isFinite(amountMinor) || !Number.isInteger(amountMinor) || !/^[A-Z]{3}$/.test(currency) || !Number.isInteger(minorUnit)) return null;
const amount = amountMinor / (10 ** minorUnit);
return {
estimated: true,
seatType: 'prolite',
priceScope: 'estimated_incremental_prorated_prolite_seat',
amount,
amountMinor,
formattedAmount: formattedCurrencyAmount(amount, currency, minorUnit),
currency,
minorUnit,
multiplier: PROLITE_PRICE_MULTIPLIER,
estimateMethod: 'default_seat_preview_multiplier',
estimateBasis: 'live_incremental_prorated_default_seat',
};
}
export function currentSeatQuantity(payload = {}) {
const value = Number(payload?.current_seat_quantity ?? payload?.currentSeatQuantity);
return Number.isInteger(value) && value >= 1 ? value : null;
}
function normalizedDate(value) {
if (value === undefined || value === null || value === '') return null;
const numeric = typeof value === 'number' || /^\d+$/.test(String(value).trim()) ? Number(value) : null;
const timestamp = Number.isFinite(numeric)
? (numeric < 10_000_000_000 ? numeric * 1000 : numeric)
: Date.parse(String(value));
return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : null;
}
function amountDetails(payload = {}) {
const amountDue = payload?.amount_due && typeof payload.amount_due === 'object' ? payload.amount_due : {};
const amountMinor = Number(amountDue.amount ?? payload?.total_amount ?? payload?.totalAmount);
const currency = String(payload?.currency || amountDue.currency || '').trim().toUpperCase();
if (!Number.isFinite(amountMinor)) return { ok: false, message: 'billing_preview_amount_missing' };
if (!/^[A-Z]{3}$/.test(currency)) return { ok: false, message: 'billing_preview_currency_missing' };
const minorUnit = currencyMinorUnit(currency);
const amount = amountMinor / (10 ** minorUnit);
const formattedAmount = formattedCurrencyAmount(amount, currency, minorUnit);
return { ok: true, amountMinor, amount, currency, minorUnit, formattedAmount };
}
export function normalizeBillingPreview(payload = {}, options = {}) {
const currentSeats = currentSeatQuantity(payload);
if (currentSeats == null) return { ok: false, message: 'billing_preview_current_seats_missing' };
const updatedSeats = Number(options.updatedSeats);
if (!Number.isInteger(updatedSeats) || updatedSeats !== currentSeats + 1) {
return { ok: false, message: 'billing_preview_seat_target_mismatch', currentSeats, expectedUpdatedSeats: currentSeats + 1 };
}
const amount = amountDetails(payload);
if (!amount.ok) return amount;
const proliteEstimate = estimateProliteSeatCost(amount);
const fetchedAt = options.fetchedAt || new Date().toISOString();
const activeUntil = normalizedDate(options.activeUntil);
const renewalDate = normalizedDate(payload?.renewal_date ?? payload?.renewalDate);
const thresholdDays = Math.min(365, Math.max(1, Number(options.thresholdDays) || 7));
const referenceTime = Date.parse(options.now || fetchedAt);
const expiryTime = Date.parse(activeUntil || '');
const remainingSeconds = Number.isFinite(referenceTime) && Number.isFinite(expiryTime)
? Math.ceil((expiryTime - referenceTime) / 1000)
: null;
const remainingDays = remainingSeconds == null ? null : Math.ceil(remainingSeconds / 86400);
const isDelinquent = options.isDelinquent === true;
const willRenew = typeof options.willRenew === 'boolean' ? options.willRenew : null;
const expiryStatus = isDelinquent
? 'delinquent'
: remainingSeconds != null && remainingSeconds <= 0
? 'expired'
: remainingDays != null && remainingDays <= thresholdDays
? 'due_soon'
: willRenew === false
? 'cancelling'
: activeUntil
? 'active'
: 'unknown';
return {
ok: true,
currentSeats,
updatedSeats,
seatType: 'default',
priceScope: 'incremental_prorated_default_seat',
...amount,
proliteEstimate,
activeUntil,
renewalDate,
willRenew,
isDelinquent,
billingPeriod: options.billingPeriod || null,
remainingSeconds,
remainingDays,
thresholdDays,
expiryStatus,
fetchedAt,
};
}