import { PolicyReasons } from './usage'; import { BookingError } from './api'; import { formattingLocale } from './dateTime'; import * as React from 'react'; import { __ } from '@wordpress/i18n'; import type { BookingApi } from './api'; export interface PriceSegment { start: string; end: string; minutes: number; hourly_rate: number; base_amount: number; discount_amount: number; final_amount: number; applied_rule_id: number; } export interface Price { participant_share_applies?: boolean; base_amount: number; discount_amount: number; final_amount: number; currency: string; participant_count: number; eligible_member_count?: number; non_member_count?: number; segments: PriceSegment[]; } // ISO 4217 minor units; display and major-unit input conversion never use floating-point money arithmetic. export function currencyDigits(currency: string): number { return ['BIF', 'CLP', 'DJF', 'GNF', 'ISK', 'JPY', 'KMF', 'KRW', 'PYG', 'RWF', 'UGX', 'UYI', 'VND', 'VUV', 'XAF', 'XOF', 'XPF'].includes(currency) ? 0 : ['BHD', 'IQD', 'JOD', 'KWD', 'LYD', 'OMR', 'TND'].includes(currency) ? 3 : ['CLF', 'UYW'].includes(currency) ? 4 : 2; } export function majorText(minor: number, currency: string): string { const digits = currencyDigits(currency), value = BigInt(Math.abs(minor)), factor = 10n ** BigInt(digits); return (minor < 0 ? '-' : '') + String(value / factor) + (digits ? '.' + String(value % factor).padStart(digits, '0') : ''); } export function minorAmount(value: string, currency: string, credit = false): number { const digits = currencyDigits(currency); const normalized = value.trim().replace(',', '.'); if (!/^\d+(?:\.\d+)?$/.test(normalized)) throw new Error(credit ? __('Enter a positive amount.', 'abece-booking') : __('Enter a non-negative hourly price.', 'abece-booking')); const [whole, fraction = ''] = normalized.split('.'); if (fraction.length > digits) throw new Error(credit ? __('The amount has too many decimal places for this currency.', 'abece-booking') : __('The price has too many decimal places for this currency.', 'abece-booking')); const result = BigInt(whole) * 10n ** BigInt(digits) + BigInt(fraction.padEnd(digits, '0') || '0'); if (result > BigInt(Number.MAX_SAFE_INTEGER)) throw new Error(credit ? __('The credit balance exceeds the supported money range.', 'abece-booking') : __('The configured price exceeds the supported money range.', 'abece-booking')); return Number(result); } export function money(minor: number, currency: string): string { const text = majorText(Math.abs(minor), currency), [whole, fraction] = text.split('.'); const locale = formattingLocale(); const group = new Intl.NumberFormat(locale, { maximumFractionDigits: 0 }).format(BigInt(whole)); const decimal = new Intl.NumberFormat(locale).formatToParts(1.1).find(p => p.type === 'decimal')?.value ?? '.'; return (minor < 0 ? '−' : '') + group + (fraction ? decimal + fraction : '') + ' ' + currency; } export function useQuote(api: BookingApi, route: string, payload: Record, ready: boolean) { const key = ready ? JSON.stringify(payload) : ''; const [revision, refresh] = React.useReducer(n => n + 1, 0); const [state, setState] = React.useState<{ key: string; price: Price | null; error: string; pending: boolean; reasons?: BookingError['data']; }>({ key: '', price: null, error: '', pending: false }); React.useEffect(() => { let alive = true; if (!key) { setState({ key, price: null, error: '', pending: false }); return; } setState({ key, price: null, error: '', pending: true }); const timer = setTimeout(() => { api.request(route, 'POST', JSON.parse(key)).then(price => { if (alive) setState({ key, price, error: '', pending: false }); }).catch(e => { if (alive) setState({ key, price: null, error: e.message, pending: false, reasons: e instanceof BookingError ? e.data : undefined }); }); }, 200); return () => { alive = false; clearTimeout(timer); }; }, [api, route, key, revision]); return { ...(state.key === key ? state : { key, price: null, error: '', pending: !!key }), refresh }; } export function PriceSummary({ price, members = false }: { price: Price; members?: boolean; }) { return
{__('Base price', 'abece-booking')}
{money(price.base_amount, price.currency)}
{members && <>
{__('Eligible members', 'abece-booking')}
{price.eligible_member_count ?? 0} / {price.participant_count}
}{price.discount_amount > 0 && <>
{__('Member benefit', 'abece-booking')}
−{money(price.discount_amount, price.currency)}
}
{__('Amount due', 'abece-booking')}
{money(price.final_amount, price.currency)}
; } export function QuoteView({ quote, members }: { quote: ReturnType; members?: boolean; }) { return
{quote.pending &&

{__('Calculating price…', 'abece-booking')}

}{quote.error && <>

{quote.error}

}{quote.price && <>{false}}
; }