'use strict'; import type { Cents } from '../utils/money.mts'; // CJS module (.cts): Node runs it as CommonJS; the require of the ESM money.mts // works via Node's require(esm). Cast the require result to the module's type so // toCents/fromCents keep their branded Cents/Dollars signatures. const { toCents, fromCents } = require('../utils/money.mts') as typeof import('../utils/money.mts'); type FieldResult = { value: T } | { error: string }; function isPositiveIntegerString(value: unknown): boolean { return /^\d+$/.test(String(value).trim()); } function validateIsoDate(value: unknown, field = 'paid_date'): FieldResult { if (typeof value !== 'string') { return { error: `${field} must be a valid date in YYYY-MM-DD format` }; } const trimmed = value.trim(); const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(trimmed); if (!match) { return { error: `${field} must be a valid date in YYYY-MM-DD format` }; } const year = Number(match[1]); const month = Number(match[2]); const day = Number(match[3]); const date = new Date(Date.UTC(year, month - 1, day)); if ( date.getUTCFullYear() !== year || date.getUTCMonth() !== month - 1 || date.getUTCDate() !== day ) { return { error: `${field} must be a real calendar date in YYYY-MM-DD format` }; } return { value: trimmed }; } /** * Validates a positive dollar amount and converts it to integer cents * (the unit `payments.amount` and related money columns are stored in). */ function validatePositiveAmount(value: unknown, field = 'amount'): FieldResult { const cents = toCents(value as number | string); if (cents === null || !Number.isInteger(cents) || cents <= 0) { return { error: `${field} must be a positive number` }; } return { value: cents }; } interface PaymentRow { amount?: number | null; balance_delta?: number | null; interest_delta?: number | null; [key: string]: unknown; } /** * Converts a payment row's cent columns (amount, balance_delta, interest_delta) * to dollars for API responses. */ function serializePayment(payment: T): T { if (!payment) return payment; const out = { ...payment }; if (out.amount != null) out.amount = fromCents(out.amount); if (out.balance_delta != null) out.balance_delta = fromCents(out.balance_delta); if (out.interest_delta != null) out.interest_delta = fromCents(out.interest_delta); return out; } const PAYMENT_SOURCES = ['manual', 'file_import', 'provider_sync'] as const; function validatePaymentSource(value: unknown, field = 'payment_source'): FieldResult { if (typeof value !== 'string') { return { error: `${field} must be one of: ${PAYMENT_SOURCES.join(', ')}` }; } const source = value.trim(); if (!(PAYMENT_SOURCES as readonly string[]).includes(source)) { return { error: `${field} must be one of: ${PAYMENT_SOURCES.join(', ')}` }; } return { value: source }; } interface ValidatePaymentOptions { requireBillId?: boolean; requireAmount?: boolean; requirePaidDate?: boolean; fieldPrefix?: string; } interface NormalizedPayment { bill_id?: number; amount?: Cents; paid_date?: string; payment_source?: string; } function validatePaymentInput( data: Record, options: ValidatePaymentOptions = {}, ): { normalized: NormalizedPayment } | { error: string; field: string } { const { requireBillId = true, requireAmount = true, requirePaidDate = true, fieldPrefix = '', } = options; const normalized: NormalizedPayment = {}; if (requireBillId || data.bill_id !== undefined) { if (data.bill_id === undefined || data.bill_id === null || data.bill_id === '') { return { error: 'bill_id is required', field: `${fieldPrefix}bill_id` }; } if (!isPositiveIntegerString(data.bill_id)) { return { error: 'bill_id must be a positive integer', field: `${fieldPrefix}bill_id` }; } normalized.bill_id = Number(String(data.bill_id).trim()); } if (requireAmount || data.amount !== undefined) { if (data.amount === undefined || data.amount === null || data.amount === '') { return { error: 'amount is required', field: `${fieldPrefix}amount` }; } const amount = validatePositiveAmount(data.amount, `${fieldPrefix}amount`); if ('error' in amount) return { error: amount.error, field: `${fieldPrefix}amount` }; normalized.amount = amount.value; } if (requirePaidDate || data.paid_date !== undefined) { if (data.paid_date === undefined || data.paid_date === null || data.paid_date === '') { return { error: 'paid_date is required', field: `${fieldPrefix}paid_date` }; } const paidDate = validateIsoDate(data.paid_date, `${fieldPrefix}paid_date`); if ('error' in paidDate) return { error: paidDate.error, field: `${fieldPrefix}paid_date` }; normalized.paid_date = paidDate.value; } if (data.payment_source !== undefined) { const source = validatePaymentSource(data.payment_source, `${fieldPrefix}payment_source`); if ('error' in source) return { error: source.error, field: `${fieldPrefix}payment_source` }; normalized.payment_source = source.value; } return { normalized }; } module.exports = { PAYMENT_SOURCES, serializePayment, validateIsoDate, validatePaymentInput, validatePaymentSource, validatePositiveAmount, };