import { describe, it, expect, vi, beforeEach } from 'vitest' import Payment from '../../../server/models/payment.js' import { upsertPaymentFromHelcim } from '../../../server/utils/payments.js' vi.mock('../../../server/models/payment.js', () => ({ default: { findOne: vi.fn(), create: vi.fn() } })) const sendMock = vi.fn().mockResolvedValue({ data: {}, error: null }) vi.mock('resend', () => ({ Resend: function MockResend() { this.emails = { send: (...args) => sendMock(...args) } } })) const memberDoc = { _id: 'member-1', email: 'test@example.com', name: 'Test Member', helcimCustomerId: 'CST123', helcimSubscriptionId: 'sub-1', billingCadence: 'monthly' } const paidTx = { id: 'tx-100', date: '2026-04-20T10:00:00Z', amount: 15, status: 'paid', currency: 'CAD' } describe('upsertPaymentFromHelcim', () => { beforeEach(() => { vi.clearAllMocks() }) it('inserts a new Payment when helcimTransactionId is unseen', async () => { Payment.findOne.mockResolvedValue(null) Payment.create.mockResolvedValue({ _id: 'p1', helcimTransactionId: 'tx-100' }) const result = await upsertPaymentFromHelcim(memberDoc, paidTx) expect(result.created).toBe(true) expect(result.payment._id).toBe('p1') expect(Payment.create).toHaveBeenCalledWith(expect.objectContaining({ memberId: 'member-1', helcimTransactionId: 'tx-100', helcimCustomerId: 'CST123', helcimSubscriptionId: 'sub-1', amount: 15, currency: 'CAD', paymentType: 'monthly', status: 'success', rawHelcim: paidTx })) }) it('does NOT overwrite an existing Payment', async () => { const existing = { _id: 'p1', helcimTransactionId: 'tx-100', receiptIssued: true, receiptId: 'R-2026-001' } Payment.findOne.mockResolvedValue(existing) const result = await upsertPaymentFromHelcim(memberDoc, paidTx) expect(result.created).toBe(false) expect(result.payment).toBe(existing) expect(Payment.create).not.toHaveBeenCalled() }) it("maps helcim status 'refunded' to 'refunded'", async () => { Payment.findOne.mockResolvedValue(null) Payment.create.mockResolvedValue({ _id: 'p2' }) await upsertPaymentFromHelcim(memberDoc, { ...paidTx, id: 'tx-101', status: 'refunded' }) expect(Payment.create).toHaveBeenCalledWith(expect.objectContaining({ status: 'refunded' })) }) it("maps helcim status 'failed' to 'failed'", async () => { Payment.findOne.mockResolvedValue(null) Payment.create.mockResolvedValue({ _id: 'p3' }) await upsertPaymentFromHelcim(memberDoc, { ...paidTx, id: 'tx-102', status: 'failed' }) expect(Payment.create).toHaveBeenCalledWith(expect.objectContaining({ status: 'failed' })) }) it("skips helcim status 'other' and returns created:false, payment:null", async () => { const result = await upsertPaymentFromHelcim(memberDoc, { ...paidTx, id: 'tx-103', status: 'other' }) expect(result).toEqual({ created: false, payment: null }) expect(Payment.findOne).not.toHaveBeenCalled() expect(Payment.create).not.toHaveBeenCalled() }) it('uses opts.paymentType when provided, overriding memberDoc.billingCadence', async () => { Payment.findOne.mockResolvedValue(null) Payment.create.mockResolvedValue({ _id: 'p4' }) await upsertPaymentFromHelcim(memberDoc, paidTx, { paymentType: 'annual' }) expect(Payment.create).toHaveBeenCalledWith(expect.objectContaining({ paymentType: 'annual' })) }) it('falls back to memberDoc.billingCadence when opts.paymentType is absent', async () => { Payment.findOne.mockResolvedValue(null) Payment.create.mockResolvedValue({ _id: 'p5' }) await upsertPaymentFromHelcim({ ...memberDoc, billingCadence: 'annual' }, paidTx) expect(Payment.create).toHaveBeenCalledWith(expect.objectContaining({ paymentType: 'annual' })) }) it('sends Resend confirmation when sendConfirmation:true AND created:true', async () => { Payment.findOne.mockResolvedValue(null) Payment.create.mockResolvedValue({ _id: 'p6' }) await upsertPaymentFromHelcim(memberDoc, paidTx, { sendConfirmation: true }) expect(sendMock).toHaveBeenCalledTimes(1) const emailArg = sendMock.mock.calls[0][0] expect(emailArg.to).toContain('test@example.com') expect(emailArg.text).toContain('Baby Ghosts Studio Development Fund') expect(emailArg.text).toContain('not an official donation receipt') expect(emailArg.text).toContain('available starting later in 2026') }) it('does NOT send confirmation when sendConfirmation:true but created:false', async () => { Payment.findOne.mockResolvedValue({ _id: 'existing' }) await upsertPaymentFromHelcim(memberDoc, paidTx, { sendConfirmation: true }) expect(sendMock).not.toHaveBeenCalled() }) it('does NOT send confirmation when sendConfirmation is absent/false', async () => { Payment.findOne.mockResolvedValue(null) Payment.create.mockResolvedValue({ _id: 'p7' }) await upsertPaymentFromHelcim(memberDoc, paidTx) expect(sendMock).not.toHaveBeenCalled() }) it('swallows Resend send errors (logging must not break payment flow)', async () => { Payment.findOne.mockResolvedValue(null) Payment.create.mockResolvedValue({ _id: 'p8' }) sendMock.mockRejectedValueOnce(new Error('SMTP blew up')) const result = await upsertPaymentFromHelcim(memberDoc, paidTx, { sendConfirmation: true }) expect(result.created).toBe(true) expect(result.payment._id).toBe('p8') }) })