import { useRef, useState } from 'react'; import { Button } from '../../../components/ui/button.tsx'; import { Alert, AlertDescription } from '../../../components/ui/alert.tsx'; import { useContractorAutopayService, type AgencyAutopayArrangement } from '../../../services/ContractorAutopayService.tsx'; import { relayServerFailure } from '../../../utils/fetchFailureMessage'; import { formatDayMonthYear } from '../../../utils/dateUtils'; import { isSaveRetired, SaveRetiredError, useSaveScope } from '../../../utils/saveOperation'; import { autopayHolder, autopayStorageAvailable, newAutopayOperationId, readAutopayHold, replaceAutopayHold, useAutopayHolds, writeAutopayHold, type AutopayHold, type AutopaySetupResult, } from '../../../utils/autopaySetup'; const money = (cents: number) => new Intl.NumberFormat(undefined, { style: 'currency', currency: 'USD' }).format(cents / 100); /** The formula the practice accepts: its own final Akashic bill, the agency's markup, and fixed services. */ export const autopayFormula = (arrangement: Pick): string => { const markup = arrangement.markup; return 'Your final Akashic bill' + (markup.type === 'percent' ? ' + ' + markup.basisPoints / 100 + '%' : markup.type === 'flat' ? ' + ' + money(markup.cents) : '') + ' each month'; }; export const autopayStatus = (arrangement: AgencyAutopayArrangement): string => arrangement.status === 'active' ? 'Auto-pay active' + (arrangement.card ? ' · ' + arrangement.card.brand + ' ending ' + arrangement.card.last4 : '') + (arrangement.activatedAt ? ' · since ' + formatDayMonthYear(arrangement.activatedAt) : '') : arrangement.status === 'authorizing' ? 'Accepted · waiting for a saved card' : 'Offered · waiting for you'; /** One agency's offer, accepted here by an administrator; the card itself is saved on the agency's Stripe page. */ const AgencyAutopaySection = ({ arrangements, organizationId, viewerId, canManage, mayAccept = true }: { arrangements: AgencyAutopayArrangement[]; organizationId: string; viewerId: string; canManage: boolean; mayAccept?: boolean; }) => { const { acceptArrangement, checkSetup, continueSetup } = useContractorAutopayService(); const [busy, setBusy] = useState(); const [error, setError] = useState(); const [results, setResults] = useState>({}); const [opening] = useState(newAutopayOperationId); const working = useRef(false); const scope = useSaveScope(JSON.stringify([organizationId, viewerId, canManage, mayAccept, opening, arrangements]), () => { working.current = false; setBusy(undefined); setError(undefined); setResults({}); }); useAutopayHolds(); const run = async (arrangement: AgencyAutopayArrangement, action: 'accept' | 'check' | 'continue') => { if (working.current || !canManage || !organizationId || !viewerId) return; const holder = autopayHolder(viewerId, organizationId, arrangement.contractorId); const held = readAutopayHold(holder); const known = results[arrangement.contractorId] ?? arrangement.setup; if (action === 'accept' && (!mayAccept || held || arrangement.status === 'active' || !arrangement.offerIdentity)) return; const operationId = held?.operationId ?? (action === 'accept' ? newAutopayOperationId() : known?.operationId); if (!operationId) { setError('This setup could not be confirmed. Check contractor auto-pay before accepting again.'); return; } const attempt: AutopayHold = { operationId, opening, attempt: newAutopayOperationId(), sent: held?.sent === true || action !== 'accept' }; writeAutopayHold(holder, attempt); working.current = true; setBusy(arrangement.contractorId); setError(undefined); const operation = scope.start(); let sent = false; const transport = { ...operation.transport, organizationId, admit: () => { operation.admit(); const current = readAutopayHold(holder); if (!current || current.operationId !== operationId || current.opening !== opening || current.attempt !== attempt.attempt) throw new SaveRetiredError(); }, onSend: () => { sent = true; replaceAutopayHold(holder, attempt, { ...attempt, sent: true }); }, }; try { const result = await operation.step(() => action === 'accept' ? acceptArrangement(arrangement.contractorId, { operationId, offerIdentity: arrangement.offerIdentity, packageVersion: arrangement.packageVersion }, transport) : action === 'continue' ? continueSetup(arrangement.contractorId, operationId, transport) : checkSetup(arrangement.contractorId, operationId, transport)); operation.admit(); const current = readAutopayHold(holder); if (!current || current.attempt !== attempt.attempt || current.opening !== opening || current.operationId !== operationId) return; setResults((prior) => ({ ...prior, [arrangement.contractorId]: result })); if (((result.state === 'active' || result.state === 'expired') && !result.evidencePending) || (result.state === 'refused' && result.admitted === false)) { replaceAutopayHold(holder, attempt, undefined); } else { replaceAutopayHold(holder, attempt, { ...attempt, operationId: result.operationId, sent: true }); } if (result.state === 'ready' && result.url) { operation.admit(); window.location.assign(result.url); } if (result.state === 'pending') setError('This setup could not be confirmed yet. Check the original setup before accepting again.'); if (result.state === 'refused') setError(result.message || 'This offer could not be accepted. Check its current terms before trying again.'); } catch (failure) { if (!operation.belongs() || isSaveRetired(failure)) return; const admitted = (failure as { body?: { admitted?: unknown } } | null)?.body?.admitted; const refused = action === 'accept' && admitted === false; if ((!sent && !attempt.sent) || refused) replaceAutopayHold(holder, attempt, undefined); setError(!refused && (sent || attempt.sent) ? 'This setup could not be confirmed. Check the original setup before accepting again.' : relayServerFailure(failure, 'The contractor payment page could not be opened.')); } finally { if (!sent && !attempt.sent) replaceAutopayHold(holder, attempt, undefined); if (operation.belongs()) { working.current = false; setBusy(undefined); } operation.retire(); } }; return (
{!autopayStorageAvailable() &&

This browser cannot retain the setup reference after reload. Check contractor auto-pay before accepting again.

} {error && ( {error} )} {arrangements.map((arrangement) => { const held = readAutopayHold(autopayHolder(viewerId, organizationId, arrangement.contractorId)); const result = results[arrangement.contractorId] ?? arrangement.setup; const active = result?.state === 'active' || arrangement.status === 'active'; const pending = Boolean(held || (result ? result.state !== 'expired' && result.state !== 'refused' : arrangement.status === 'authorizing')); return (

{arrangement.contractorName || 'Your contractor'}

{result?.state === 'active' && arrangement.status !== 'active' ? 'Auto-pay active' : autopayStatus(arrangement)}

{result?.state === 'stale' || result?.state === 'needsReview' ?

The original setup needs review before another card can be saved.

: null} {result?.state === 'expired' &&

The previous card setup expired. Review these terms before accepting again.

} {result?.evidencePending &&

The setup record is still being confirmed. This is not a charge confirmation.

}

{autopayFormula(arrangement)}

{arrangement.lines.length > 0 && (
    {arrangement.lines.map((line) => (
  • {line.description} {money(line.monthlyCents)} / month
  • ))}
)}

The contractor charges this on its own Stripe account after each Akashic bill is final. It never changes your Akashic subscription, and changing its markup or services needs your consent again.

{canManage && (!active || held || result?.evidencePending) && (pending ? (
{result?.canContinue && }
) : )}
); })}
); }; export default AgencyAutopaySection;