UNPKG

@accounter/server

Version:
93 lines 4.22 kB
/** * Transaction Aggregator * * Aggregates multiple transactions from a single charge into a unified representation * for matching purposes. Handles fee exclusion, currency validation, business ID * validation, amount summation, date selection, and description concatenation. */ /** * Aggregate multiple transactions into a single representation * * Per specification (section 4.2): * 1. Exclude transactions where is_fee = true * 2. If multiple currencies exist: throw error * 3. If multiple non-null business IDs exist: throw error * 4. Amount: sum of all amounts * 5. Currency: the common currency * 6. Business ID: the single non-null business ID (or null if all null) * 7. Date: earliest event_date * 8. Description: concatenate all source_description values with line breaks * * @param transactions - Array of transactions from a charge * @returns Aggregated transaction data * * @throws {Error} If transactions array is empty * @throws {Error} If no non-fee transactions exist after filtering * @throws {Error} If multiple different currencies exist * @throws {Error} If multiple different non-null business IDs exist * * @example * const aggregated = aggregateTransactions([ * { amount: 100, currency: 'USD', business_id: 'b1', event_date: new Date('2024-01-15'), ... }, * { amount: 50, currency: 'USD', business_id: 'b1', event_date: new Date('2024-01-20'), ... } * ]); * // Returns: { amount: 150, currency: 'USD', businessId: 'b1', date: Date('2024-01-15'), ... } */ export function aggregateTransactions(transactions) { // Validate non-empty input if (!transactions || transactions.length === 0) { throw new Error('Cannot aggregate transactions: array is empty'); } // Filter out fee transactions const nonFeeTransactions = transactions.filter(t => t.is_fee !== true); // Validate we have transactions after filtering if (nonFeeTransactions.length === 0) { throw new Error('Cannot aggregate transactions: all transactions are marked as fees'); } // Validate single currency const currencies = new Set(nonFeeTransactions.map(t => t.currency)); if (currencies.size > 1) { throw new Error(`Cannot aggregate transactions: multiple currencies found (${Array.from(currencies).join(', ')})`); } // Validate single non-null business ID const businessIds = nonFeeTransactions .map(t => t.business_id) .filter((id) => id !== null && id !== undefined); const uniqueBusinessIds = new Set(businessIds); if (uniqueBusinessIds.size > 1) { throw new Error(`Cannot aggregate transactions: multiple business IDs found (${Array.from(uniqueBusinessIds).join(', ')})`); } // Sum amounts const totalAmount = nonFeeTransactions.reduce((sum, t) => sum + Number(t.amount), 0); // Get common currency (safe since we validated single currency) const currency = nonFeeTransactions[0].currency; // Get business ID (single non-null or null if all null) const businessId = uniqueBusinessIds.size === 1 ? Array.from(uniqueBusinessIds)[0] : null; // Get earliest event_date const earliestDate = nonFeeTransactions.reduce((earliest, t) => { return t.event_date < earliest ? t.event_date : earliest; }, nonFeeTransactions[0].event_date); // Get earliest debit_date const earliestDebitDate = nonFeeTransactions.reduce((earliest, t) => { const debitDate = t.debit_timestamp ?? t.debit_date; if (debitDate === null) return earliest; if (earliest === null) return debitDate; return debitDate < earliest ? debitDate : earliest; }, null); // Concatenate descriptions with line breaks, filtering out nulls const descriptions = nonFeeTransactions .map(t => t.source_description) .filter((desc) => desc !== null && desc !== undefined && desc.trim() !== ''); const description = descriptions.length > 0 ? descriptions.join('\n') : ''; return { amount: totalAmount, currency, businessId, date: earliestDate, description, debitDate: earliestDebitDate, }; } //# sourceMappingURL=transaction-aggregator.js.map