feat(payments): log Helcim charge on free-to-paid upgrade
In the Case 1 (free→paid) branch of update-contribution, after the subscription is created and the member row is updated, fetch the newest paid Helcim transaction and upsert a Payment with paymentType=cadence and sendConfirmation=true. Paid→paid (Case 3) is intentionally NOT wired — no new transaction occurs at amount change; the next recurring charge is captured by the reconciliation script.
This commit is contained in:
parent
49cfb47fff
commit
fc09760a41
2 changed files with 38 additions and 0 deletions
|
|
@ -12,7 +12,9 @@ import {
|
||||||
updateHelcimSubscription,
|
updateHelcimSubscription,
|
||||||
cancelHelcimSubscription,
|
cancelHelcimSubscription,
|
||||||
generateIdempotencyKey,
|
generateIdempotencyKey,
|
||||||
|
listHelcimCustomerTransactions,
|
||||||
} from "../../utils/helcim.js";
|
} from "../../utils/helcim.js";
|
||||||
|
import { upsertPaymentFromHelcim } from "../../utils/payments.js";
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -118,6 +120,27 @@ export default defineEventHandler(async (event) => {
|
||||||
{ runValidators: false }
|
{ runValidators: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const txs = await listHelcimCustomerTransactions(customerCode);
|
||||||
|
const latestPaid = txs.find((t) => t.status === 'paid');
|
||||||
|
if (latestPaid) {
|
||||||
|
await upsertPaymentFromHelcim(
|
||||||
|
{
|
||||||
|
_id: member._id,
|
||||||
|
email: member.email,
|
||||||
|
name: member.name,
|
||||||
|
helcimCustomerId: member.helcimCustomerId,
|
||||||
|
helcimSubscriptionId: subscription.id,
|
||||||
|
billingCadence: cadence,
|
||||||
|
},
|
||||||
|
latestPaid,
|
||||||
|
{ paymentType: cadence, sendConfirmation: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[payments] free→paid charge log failed, will be picked up by reconciliation:', err?.message || err);
|
||||||
|
}
|
||||||
|
|
||||||
logContributionChange();
|
logContributionChange();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,9 @@ import {
|
||||||
listHelcimCustomerCards,
|
listHelcimCustomerCards,
|
||||||
createHelcimSubscription,
|
createHelcimSubscription,
|
||||||
cancelHelcimSubscription,
|
cancelHelcimSubscription,
|
||||||
|
listHelcimCustomerTransactions,
|
||||||
} from '../../../server/utils/helcim.js'
|
} from '../../../server/utils/helcim.js'
|
||||||
|
import { upsertPaymentFromHelcim } from '../../../server/utils/payments.js'
|
||||||
import handler from '../../../server/api/members/update-contribution.post.js'
|
import handler from '../../../server/api/members/update-contribution.post.js'
|
||||||
import { createMockEvent } from '../helpers/createMockEvent.js'
|
import { createMockEvent } from '../helpers/createMockEvent.js'
|
||||||
|
|
||||||
|
|
@ -31,6 +33,10 @@ vi.mock('../../../server/utils/helcim.js', () => ({
|
||||||
updateHelcimSubscription: vi.fn(),
|
updateHelcimSubscription: vi.fn(),
|
||||||
cancelHelcimSubscription: vi.fn(),
|
cancelHelcimSubscription: vi.fn(),
|
||||||
generateIdempotencyKey: vi.fn().mockReturnValue('idem-key-123'),
|
generateIdempotencyKey: vi.fn().mockReturnValue('idem-key-123'),
|
||||||
|
listHelcimCustomerTransactions: vi.fn().mockResolvedValue([]),
|
||||||
|
}))
|
||||||
|
vi.mock('../../../server/utils/payments.js', () => ({
|
||||||
|
upsertPaymentFromHelcim: vi.fn().mockResolvedValue({ created: true, payment: { _id: 'p1' } })
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Nitro auto-imports
|
// Nitro auto-imports
|
||||||
|
|
@ -224,6 +230,9 @@ describe('update-contribution endpoint — Case 1 (free→paid)', () => {
|
||||||
listHelcimCustomerCards.mockResolvedValue([{ id: 'card-1' }])
|
listHelcimCustomerCards.mockResolvedValue([{ id: 'card-1' }])
|
||||||
createHelcimSubscription.mockResolvedValue({ data: [{ id: 'sub-new', status: 'active', nextBillingDate: '2026-05-18' }] })
|
createHelcimSubscription.mockResolvedValue({ data: [{ id: 'sub-new', status: 'active', nextBillingDate: '2026-05-18' }] })
|
||||||
Member.findByIdAndUpdate.mockResolvedValue({})
|
Member.findByIdAndUpdate.mockResolvedValue({})
|
||||||
|
listHelcimCustomerTransactions.mockResolvedValueOnce([
|
||||||
|
{ id: 'tx-upgrade', date: '2026-04-20', amount: 15, status: 'paid', currency: 'CAD' }
|
||||||
|
])
|
||||||
|
|
||||||
const event = createMockEvent({
|
const event = createMockEvent({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|
@ -242,6 +251,12 @@ describe('update-contribution endpoint — Case 1 (free→paid)', () => {
|
||||||
{ $set: expect.objectContaining({ billingCadence: 'monthly', contributionAmount: 15, helcimSubscriptionId: 'sub-new' }) },
|
{ $set: expect.objectContaining({ billingCadence: 'monthly', contributionAmount: 15, helcimSubscriptionId: 'sub-new' }) },
|
||||||
{ runValidators: false }
|
{ runValidators: false }
|
||||||
)
|
)
|
||||||
|
expect(listHelcimCustomerTransactions).toHaveBeenCalledWith('code-1')
|
||||||
|
expect(upsertPaymentFromHelcim).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ _id: 'member-c1', helcimSubscriptionId: 'sub-new', billingCadence: 'monthly' }),
|
||||||
|
expect.objectContaining({ id: 'tx-upgrade' }),
|
||||||
|
{ paymentType: 'monthly', sendConfirmation: true }
|
||||||
|
)
|
||||||
expect(result.success).toBe(true)
|
expect(result.success).toBe(true)
|
||||||
expect(result.message).toBe('Successfully upgraded to paid tier')
|
expect(result.message).toBe('Successfully upgraded to paid tier')
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue