diff --git a/server/api/internal/reconcile-payments.post.js b/server/api/internal/reconcile-payments.post.js index 9c0686f..0fe90ae 100644 --- a/server/api/internal/reconcile-payments.post.js +++ b/server/api/internal/reconcile-payments.post.js @@ -65,37 +65,56 @@ export default defineEventHandler(async (event) => { let skipped = 0 let memberErrors = 0 - for (const member of members) { + async function processMember(member) { let txs try { txs = await listTransactionsWithRetry(member.helcimCustomerId) } catch (err) { - memberErrors++ console.error(`[reconcile] member=${member._id}: ${err?.message || err}`) - continue + return { error: true } } + const result = { error: false, txExamined: 0, created: 0, existed: 0, skipped: 0 } + for (const tx of txs) { - txExamined++ + result.txExamined++ if (!RECONCILABLE_STATUSES.has(tx?.status)) { - skipped++ + result.skipped++ continue } if (!apply) { const existing = await Payment.findOne({ helcimTransactionId: tx.id }) - if (existing) existed++ - else created++ + if (existing) result.existed++ + else result.created++ continue } // Note: deliberately NOT passing sendConfirmation — cron back-fills must // not re-send confirmation emails for transactions the member has already // been notified about (or that pre-date Mongo Payment tracking entirely). - const result = await upsertPaymentFromHelcim(member, tx) - if (result.created) created++ - else if (result.payment) existed++ - else skipped++ + const upsertResult = await upsertPaymentFromHelcim(member, tx) + if (upsertResult.created) result.created++ + else if (upsertResult.payment) result.existed++ + else result.skipped++ + } + + return result + } + + const CHUNK_SIZE = 8 + for (let i = 0; i < members.length; i += CHUNK_SIZE) { + const chunk = members.slice(i, i + CHUNK_SIZE) + const results = await Promise.all(chunk.map((m) => processMember(m))) + for (const r of results) { + if (r.error) { + memberErrors++ + continue + } + txExamined += r.txExamined + created += r.created + existed += r.existed + skipped += r.skipped } } diff --git a/tests/server/api/reconcile-payments-route.test.js b/tests/server/api/reconcile-payments-route.test.js index 9622153..d8eb78d 100644 --- a/tests/server/api/reconcile-payments-route.test.js +++ b/tests/server/api/reconcile-payments-route.test.js @@ -162,12 +162,12 @@ describe('POST /api/internal/reconcile-payments', () => { { _id: 'm3', helcimCustomerId: 'cust-3' } ])) // m1 succeeds first try, m2 fails all 3 retries, m3 succeeds first try. - listHelcimCustomerTransactions - .mockResolvedValueOnce([{ id: 'tx1', status: 'paid', amount: 5 }]) - .mockRejectedValueOnce(new Error('helcim 503')) - .mockRejectedValueOnce(new Error('helcim 503')) - .mockRejectedValueOnce(new Error('helcim 503')) - .mockResolvedValueOnce([{ id: 'tx3', status: 'paid', amount: 7 }]) + // Keyed by customerCode so it works regardless of call order (chunked Promise.all). + listHelcimCustomerTransactions.mockImplementation((customerCode) => { + if (customerCode === 'cust-1') return Promise.resolve([{ id: 'tx1', status: 'paid', amount: 5 }]) + if (customerCode === 'cust-3') return Promise.resolve([{ id: 'tx3', status: 'paid', amount: 7 }]) + return Promise.reject(new Error('helcim 503')) + }) upsertPaymentFromHelcim.mockResolvedValue({ created: true, payment: { _id: 'p' } }) vi.useFakeTimers()