Files
baya-monorepo/client/src/components/CancellationPolicyDisclosure/CancellationPolicyDisclosure.tsx
T
2026-08-02 23:12:44 +03:30

116 lines
5.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { FunctionComponent } from 'react';
import { useLocale, useTranslations } from 'next-intl';
import { Divider, Paper, Stack, Typography } from '@mui/material';
import PriceBreakdown from '@/components/PriceBreakdown';
import StatusChip from '@/components/StatusChip';
import AppAlert from '@/components/common/AppAlert';
import AppIcon from '@/components/common/AppIcon';
import RefundEtaBanner from '@/components/RefundEtaBanner';
import { CANCELLATION_LEAD_HOURS } from '@/constants';
import { formatShamsiDate } from '@/utils';
import type { CancellationPolicyPreview } from '@/services/refunds/types';
export interface CancellationPolicyDisclosureProps {
preview: CancellationPolicyPreview;
}
/** Rounds an already-0100 percent value for display — a small display number, never money, so JS math is
* safe. Never re-multiply by 100: the server (and the mock) already serve this scale. */
function toPercent(percent: number): number {
return Math.round(percent);
}
/**
* The pre-confirm cancellation disclosure (f10) — the whole point of the cancel screen: the applicable
* policy tier (label off the `cancellation_policy_code` i18n key, never the raw code), the **refund % + fee
* %**, the concrete refund-vs-fee split (reusing `PriceBreakdown`, which reconciles to the rial), the
* multi-session refundable/locked breakdown, the admin-approved reality, and a per-channel ETA preview
* (BNPL surfaces the ~710-day window honestly). Display-only — the reason field, acknowledgement and
* confirm live in the page; shared so the confirm dialog and any future per-session cancel reuse it.
* @component CancellationPolicyDisclosure
*/
const CancellationPolicyDisclosure: FunctionComponent<CancellationPolicyDisclosureProps> = ({ preview }) => {
const t = useTranslations('refunds');
const locale = useLocale();
const refundPercent = toPercent(preview.refundPercentageApplied);
const feePercent = toPercent(preview.feePercentage);
const isMultiSession = preview.sessions.length > 1;
return (
<Stack sx={{ gap: 2.5 }} data-testid="cancellation-disclosure" data-policy-code={preview.cancellationPolicyCode}>
<Paper elevation={0} sx={{ p: 2.5, border: '1px solid', borderColor: 'divider', borderRadius: 'var(--bal-radius-md)' }}>
<Stack sx={{ gap: 1 }}>
<Stack direction="row" sx={{ alignItems: 'center', gap: 1, flexWrap: 'wrap' }}>
<Typography variant="subtitle1" sx={{ fontWeight: 800 }}>
{t(`policy_${preview.cancellationPolicyCode}`)}
</Typography>
<StatusChip
status={refundPercent > 0 ? 'active' : 'rejected'}
label={t('refund_percent', { percent: refundPercent })}
/>
</Stack>
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
{t(`lead_${preview.leadTimeLabel}`, { hours: CANCELLATION_LEAD_HOURS })}
</Typography>
<Typography variant="body2" sx={{ color: 'var(--bal-money-emphasis)', fontWeight: 500 }}>
{t('fee_percent', { percent: feePercent })}
</Typography>
</Stack>
</Paper>
<PriceBreakdown
rows={[
{ key: 'refund', label: t('row_refund'), amountIrr: preview.refundAmountIrr },
{ key: 'fee', label: t('row_fee'), amountIrr: preview.feeAmountIrr },
]}
totalLabel={t('row_refundable_total')}
totalAmountIrr={preview.refundableAmountIrr}
/>
{isMultiSession && (
<Paper elevation={0} sx={{ p: 2.5, border: '1px solid', borderColor: 'divider', borderRadius: 'var(--bal-radius-md)' }}>
<Stack sx={{ gap: 1.25 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 700 }}>
{t('sessions_title')}
</Typography>
<Divider />
{preview.sessions.map((session) => (
<Stack
key={session.bookingSessionId}
data-session-id={session.bookingSessionId}
data-refundable={session.refundable}
direction="row"
sx={{ justifyContent: 'space-between', alignItems: 'center', gap: 2, opacity: session.refundable ? 1 : 0.6 }}
>
<Typography variant="body2">
{`${session.sessionIndex}. ${formatShamsiDate(session.scheduledDate, locale)}`}
</Typography>
<StatusChip
status={session.refundable ? 'active' : 'neutral'}
label={session.refundable ? t('session_refundable') : t(`reason_${session.reasonCode}`)}
/>
</Stack>
))}
</Stack>
</Paper>
)}
<RefundEtaBanner channel={preview.refundChannel} eta={preview.expectedCustomerRefundEta} />
<AppAlert
severity="info"
variant="outlined"
icon={<AppIcon icon="info" size={20} color="var(--bal-info)" />}
data-testid="admin-approval-explainer"
sx={{ marginY: 0 }}
>
{t('admin_approval_explainer')}
</AppAlert>
</Stack>
);
};
export default CancellationPolicyDisclosure;