export const MYSQL_UNSIGNED_INT_MAX = 4_294_967_295; export interface LegacyMoneyOptions { nullable?: boolean; maxCents?: number; field?: string; } function describeField(field: string | undefined): string { return field ? ` for ${field}` : ''; } export function legacyDecimalToCents( value: unknown, options: LegacyMoneyOptions = {} ): number | null { const field = describeField(options.field); if (value === null || value === undefined) { if (options.nullable) { return null; } throw new Error(`Legacy money value${field} cannot be null.`); } if (typeof value !== 'string' && typeof value !== 'number') { throw new Error(`Legacy money value${field} must be a decimal string or number.`); } if (typeof value === 'number' && !Number.isFinite(value)) { throw new Error(`Legacy money value${field} must be finite.`); } const decimal = String(value); const match = /^(\d+)(?:\.(\d{1,2}))?$/.exec(decimal); if (!match) { throw new Error( `Legacy money value${field} must be a non-negative decimal with at most two fractional digits.` ); } const wholeCents = BigInt(match[1]) * 100n; const fraction = (match[2] ?? '').padEnd(2, '0'); const cents = wholeCents + BigInt(fraction || '0'); const maxCents = options.maxCents ?? MYSQL_UNSIGNED_INT_MAX; if (!Number.isSafeInteger(maxCents) || maxCents < 0) { throw new Error('Legacy money maxCents must be a non-negative safe integer.'); } if (cents > BigInt(maxCents)) { throw new Error(`Legacy money value${field} exceeds the target cents column limit.`); } return Number(cents); }