feat(payments): add upsertPaymentFromHelcim helper with idempotent insert
Takes a Member doc + a normalized Helcim transaction and inserts a Payment row if helcimTransactionId is unseen. Maps helcim status paid→success, refunded→refunded, failed→failed; skips 'other'. opts.paymentType overrides the cadence fallback for mid-flight cadence changes. opts.sendConfirmation triggers a Resend payment-confirmation email ONLY on new inserts — swallows send failures so email trouble cannot break the upstream payment flow. The Resend template lives in server/emails/paymentConfirmation.js. It is CRA-safe (charity name + 'not an official donation receipt / tax receipts available later in 2026' disclaimer) so it can be used in either Task 8 branch without copy changes.
This commit is contained in:
parent
bf5a333117
commit
be7145f96c
3 changed files with 238 additions and 0 deletions
63
server/utils/payments.js
Normal file
63
server/utils/payments.js
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import { Resend } from 'resend'
|
||||
import Payment from '../models/payment.js'
|
||||
import { paymentConfirmationEmail } from '../emails/paymentConfirmation.js'
|
||||
|
||||
const resend = new Resend(process.env.RESEND_API_KEY)
|
||||
|
||||
function mapStatus(helcimStatus) {
|
||||
if (helcimStatus === 'paid') return 'success'
|
||||
if (helcimStatus === 'refunded') return 'refunded'
|
||||
if (helcimStatus === 'failed') return 'failed'
|
||||
return null
|
||||
}
|
||||
|
||||
export async function upsertPaymentFromHelcim(memberDoc, helcimTx, opts = {}) {
|
||||
const status = mapStatus(helcimTx?.status)
|
||||
if (!status) return { created: false, payment: null }
|
||||
|
||||
const existing = await Payment.findOne({ helcimTransactionId: helcimTx.id })
|
||||
if (existing) return { created: false, payment: existing }
|
||||
|
||||
const paymentType = opts.paymentType || memberDoc.billingCadence
|
||||
|
||||
const fields = {
|
||||
memberId: memberDoc._id,
|
||||
helcimTransactionId: helcimTx.id,
|
||||
helcimCustomerId: memberDoc.helcimCustomerId || null,
|
||||
helcimSubscriptionId: memberDoc.helcimSubscriptionId || null,
|
||||
amount: helcimTx.amount,
|
||||
currency: helcimTx.currency || 'CAD',
|
||||
paymentDate: helcimTx.date ? new Date(helcimTx.date) : new Date(),
|
||||
paymentType,
|
||||
status,
|
||||
rawHelcim: helcimTx
|
||||
}
|
||||
|
||||
let payment
|
||||
try {
|
||||
payment = await Payment.create(fields)
|
||||
} catch (err) {
|
||||
if (err?.code === 11000) {
|
||||
const racer = await Payment.findOne({ helcimTransactionId: helcimTx.id })
|
||||
return { created: false, payment: racer }
|
||||
}
|
||||
throw err
|
||||
}
|
||||
|
||||
if (opts.sendConfirmation) {
|
||||
try {
|
||||
await resend.emails.send(
|
||||
paymentConfirmationEmail({
|
||||
member: memberDoc,
|
||||
amount: fields.amount,
|
||||
paymentDate: fields.paymentDate,
|
||||
transactionId: fields.helcimTransactionId
|
||||
})
|
||||
)
|
||||
} catch (err) {
|
||||
console.error('[payments] failed to send payment confirmation email:', err?.message || err)
|
||||
}
|
||||
}
|
||||
|
||||
return { created: true, payment }
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue