fix(scripts): helcim plan-create payload shape + empty-GET handling

Verified against the live Helcim v2 API during the deploy migration:
- POST /payment-plans requires { paymentPlans: [plan] } wrapper (mirrors
  the POST /subscriptions shape), and response is { data: [plan] }.
- taxType 'customer' rejects as ERR_VALIDATION_FAILED; must be 'no_tax'
  with taxCalculation 'country_province'.
- termLength:1 rejects when termType:'forever' — drop the field.
- GET /subscriptions returns an empty body (not JSON) when no subs exist;
  tolerate that instead of failing with 'Unexpected end of JSON input'.

Plans created in the Helcim account: Monthly=50302, Annual=50303.
This commit is contained in:
Jennie Robinson Faber 2026-04-18 20:58:17 +01:00
parent f8e0cf36ba
commit 549a849bc0

View file

@ -44,8 +44,12 @@ const ANNUAL_PLAN = {
billingPeriod: 'yearly', billingPeriod: 'yearly',
} }
// Shape mirrors the pre-migration legacy plans (verified via backup dump).
// taxType:'customer' + missing taxCalculation/termLength caused ERR_VALIDATION_FAILED.
const HELCIM_PLAN_PAYLOAD_BASE = { const HELCIM_PLAN_PAYLOAD_BASE = {
description: 'Ghost Guild membership', businessName: '',
businessEmail: '',
description: '',
type: 'subscription', type: 'subscription',
status: 'active', status: 'active',
currency: 'CAD', currency: 'CAD',
@ -54,8 +58,10 @@ const HELCIM_PLAN_PAYLOAD_BASE = {
dateBilling: 'Sign-up', dateBilling: 'Sign-up',
termType: 'forever', termType: 'forever',
freeTrialPeriod: 0, freeTrialPeriod: 0,
taxType: 'customer', taxType: 'no_tax',
taxCalculation: 'country_province',
paymentMethod: 'card', paymentMethod: 'card',
isProrated: 'no',
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -87,11 +93,12 @@ async function helcimGet(path) {
const res = await fetch(`${HELCIM_API_BASE}${path}`, { const res = await fetch(`${HELCIM_API_BASE}${path}`, {
headers: helcimHeaders(), headers: helcimHeaders(),
}) })
if (!res.ok) {
const body = await res.text() const body = await res.text()
if (!res.ok) {
throw new Error(`GET ${path} failed (${res.status}): ${body}`) throw new Error(`GET ${path} failed (${res.status}): ${body}`)
} }
return res.json() if (!body.trim()) return []
try { return JSON.parse(body) } catch { return [] }
} }
async function helcimDelete(path) { async function helcimDelete(path) {
@ -280,9 +287,10 @@ async function main() {
console.log(` [DRY RUN] Would POST /payment-plans with:`) console.log(` [DRY RUN] Would POST /payment-plans with:`)
console.log(' ', JSON.stringify(monthlyPayload, null, 4).split('\n').join('\n ')) console.log(' ', JSON.stringify(monthlyPayload, null, 4).split('\n').join('\n '))
} else { } else {
const wrapped = { paymentPlans: [monthlyPayload] }
console.log(` POSTing /payment-plans with:`) console.log(` POSTing /payment-plans with:`)
console.log(' ', JSON.stringify(monthlyPayload, null, 4).split('\n').join('\n ')) console.log(' ', JSON.stringify(wrapped, null, 4).split('\n').join('\n '))
const result = await helcimPost('/payment-plans', monthlyPayload) const result = await helcimPost('/payment-plans', wrapped)
if (!result.ok) { if (!result.ok) {
console.error(` [ERROR] POST failed (${result.status}):`) console.error(` [ERROR] POST failed (${result.status}):`)
console.error(' ', JSON.stringify(result.body, null, 2)) console.error(' ', JSON.stringify(result.body, null, 2))
@ -290,7 +298,7 @@ async function main() {
await mongoose.disconnect() await mongoose.disconnect()
process.exit(1) process.exit(1)
} }
monthlyPlanId = result.body.id monthlyPlanId = result.body.data?.[0]?.id ?? result.body.paymentPlans?.[0]?.id ?? result.body.id
console.log(` [OK] Created Monthly Membership plan id: ${monthlyPlanId}`) console.log(` [OK] Created Monthly Membership plan id: ${monthlyPlanId}`)
} }
console.log() console.log()
@ -313,9 +321,10 @@ async function main() {
console.log(' Note: billingPeriod="yearly" is Helcim v2 convention.') console.log(' Note: billingPeriod="yearly" is Helcim v2 convention.')
console.log(' If 4xx on --confirm, check the Helcim API docs for the correct field value.') console.log(' If 4xx on --confirm, check the Helcim API docs for the correct field value.')
} else { } else {
const wrapped = { paymentPlans: [annualPayload] }
console.log(` POSTing /payment-plans with:`) console.log(` POSTing /payment-plans with:`)
console.log(' ', JSON.stringify(annualPayload, null, 4).split('\n').join('\n ')) console.log(' ', JSON.stringify(wrapped, null, 4).split('\n').join('\n '))
const result = await helcimPost('/payment-plans', annualPayload) const result = await helcimPost('/payment-plans', wrapped)
if (!result.ok) { if (!result.ok) {
console.error(` [ERROR] POST failed (${result.status}):`) console.error(` [ERROR] POST failed (${result.status}):`)
console.error(' ', JSON.stringify(result.body, null, 2)) console.error(' ', JSON.stringify(result.body, null, 2))
@ -324,7 +333,7 @@ async function main() {
await mongoose.disconnect() await mongoose.disconnect()
process.exit(1) process.exit(1)
} }
annualPlanId = result.body.id annualPlanId = result.body.data?.[0]?.id ?? result.body.paymentPlans?.[0]?.id ?? result.body.id
console.log(` [OK] Created Annual Membership plan id: ${annualPlanId}`) console.log(` [OK] Created Annual Membership plan id: ${annualPlanId}`)
} }
console.log() console.log()