backend-manager 5.0.117 → 5.0.118
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/manager/events/firestore/payments-webhooks/on-write.js +11 -1
- package/src/manager/index.js +3 -0
- package/src/manager/libraries/payment/processors/chargebee.js +637 -0
- package/src/manager/routes/payments/cancel/processors/chargebee.js +43 -0
- package/src/manager/routes/payments/cancel/processors/paypal.js +2 -1
- package/src/manager/routes/payments/intent/processors/chargebee.js +120 -0
- package/src/manager/routes/payments/portal/processors/chargebee.js +62 -0
- package/src/manager/routes/payments/portal/processors/paypal.js +2 -1
- package/src/manager/routes/payments/refund/processors/chargebee.js +101 -0
- package/src/manager/routes/payments/webhook/processors/chargebee.js +170 -0
- package/templates/backend-manager-config.json +11 -0
- package/test/fixtures/chargebee/invoice-one-time.json +27 -0
- package/test/fixtures/chargebee/subscription-active.json +44 -0
- package/test/fixtures/chargebee/subscription-cancelled.json +42 -0
- package/test/fixtures/chargebee/subscription-in-trial.json +41 -0
- package/test/fixtures/chargebee/subscription-legacy-plan.json +41 -0
- package/test/fixtures/chargebee/subscription-non-renewing.json +41 -0
- package/test/fixtures/chargebee/subscription-paused.json +42 -0
- package/test/fixtures/chargebee/webhook-payment-failed.json +51 -0
- package/test/fixtures/chargebee/webhook-subscription-created.json +47 -0
- package/test/helpers/payment/chargebee/parse-webhook.js +413 -0
- package/test/helpers/payment/chargebee/to-unified-one-time.js +147 -0
- package/test/helpers/payment/chargebee/to-unified-subscription.js +648 -0
package/package.json
CHANGED
|
@@ -321,7 +321,17 @@ function extractCustomerName(resource, resourceType) {
|
|
|
321
321
|
}
|
|
322
322
|
}
|
|
323
323
|
|
|
324
|
-
//
|
|
324
|
+
// Chargebee subscriptions carry shipping_address / billing_address with first_name + last_name
|
|
325
|
+
if (resourceType === 'subscription') {
|
|
326
|
+
const addr = resource.shipping_address || resource.billing_address;
|
|
327
|
+
if (addr?.first_name) {
|
|
328
|
+
const { capitalize } = require('../../../libraries/infer-contact.js');
|
|
329
|
+
return {
|
|
330
|
+
first: capitalize(addr.first_name) || null,
|
|
331
|
+
last: capitalize(addr.last_name) || null,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
}
|
|
325
335
|
|
|
326
336
|
if (!fullName) {
|
|
327
337
|
return null;
|
package/src/manager/index.js
CHANGED
|
@@ -141,6 +141,9 @@ Manager.prototype.init = function (exporter, options) {
|
|
|
141
141
|
// Set PAYPAL_CLIENT_ID from config (clientId is public, not a secret — lives in config, not .env)
|
|
142
142
|
process.env.PAYPAL_CLIENT_ID = process.env.PAYPAL_CLIENT_ID || self.config?.payment?.processors?.paypal?.clientId || '';
|
|
143
143
|
|
|
144
|
+
// Set CHARGEBEE_SITE from config (site is public, not a secret — lives in config, not .env)
|
|
145
|
+
process.env.CHARGEBEE_SITE = process.env.CHARGEBEE_SITE || self.config?.payment?.processors?.chargebee?.site || '';
|
|
146
|
+
|
|
144
147
|
// Resolve legacy paths
|
|
145
148
|
// TODO: Remove this in future versions (after we migrate to removing app.id from config)
|
|
146
149
|
self.config.app = self.config.app || {};
|
|
@@ -0,0 +1,637 @@
|
|
|
1
|
+
const powertools = require('node-powertools');
|
|
2
|
+
|
|
3
|
+
// Epoch zero timestamps (used as default/empty dates)
|
|
4
|
+
const EPOCH_ZERO = powertools.timestamp(new Date(0), { output: 'string' });
|
|
5
|
+
const EPOCH_ZERO_UNIX = powertools.timestamp(EPOCH_ZERO, { output: 'unix' });
|
|
6
|
+
|
|
7
|
+
// Chargebee billing_period_unit → unified frequency map
|
|
8
|
+
const UNIT_TO_FREQUENCY = { year: 'annually', month: 'monthly', week: 'weekly', day: 'daily' };
|
|
9
|
+
|
|
10
|
+
// Valid frequency suffixes for deterministic item_price_id parsing
|
|
11
|
+
const VALID_FREQUENCIES = ['monthly', 'annually', 'weekly', 'daily'];
|
|
12
|
+
|
|
13
|
+
// Cached config
|
|
14
|
+
let cachedConfig = null;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Chargebee shared library
|
|
18
|
+
* Provides API helpers, resource fetching, and unified transformations
|
|
19
|
+
*/
|
|
20
|
+
const Chargebee = {
|
|
21
|
+
/**
|
|
22
|
+
* Initialize or return the Chargebee config
|
|
23
|
+
* API key from CHARGEBEE_API_KEY env; site from CHARGEBEE_SITE env (set by Manager from config)
|
|
24
|
+
* @returns {{ apiKey: string, site: string, baseUrl: string }}
|
|
25
|
+
*/
|
|
26
|
+
init() {
|
|
27
|
+
if (cachedConfig) {
|
|
28
|
+
return cachedConfig;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const apiKey = process.env.CHARGEBEE_API_KEY;
|
|
32
|
+
|
|
33
|
+
if (!apiKey) {
|
|
34
|
+
throw new Error('CHARGEBEE_API_KEY environment variable is required');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const site = process.env.CHARGEBEE_SITE;
|
|
38
|
+
|
|
39
|
+
if (!site) {
|
|
40
|
+
throw new Error('CHARGEBEE_SITE environment variable is required (set from config payment.processors.chargebee.site)');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
cachedConfig = {
|
|
44
|
+
apiKey,
|
|
45
|
+
site,
|
|
46
|
+
baseUrl: `https://${site}.chargebee.com/api/v2`,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return cachedConfig;
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Make an authenticated Chargebee API request
|
|
54
|
+
* Chargebee uses Basic auth (apiKey as username, empty password)
|
|
55
|
+
* POST/PUT bodies use application/x-www-form-urlencoded
|
|
56
|
+
* Responses are JSON wrapped in a type key (e.g., { subscription: {...} })
|
|
57
|
+
*
|
|
58
|
+
* @param {string} endpoint - API path (e.g., '/subscriptions/sub_xxx')
|
|
59
|
+
* @param {object} options - { method, body (object to form-encode), headers }
|
|
60
|
+
* @returns {Promise<object>} Parsed JSON response
|
|
61
|
+
*/
|
|
62
|
+
async request(endpoint, options = {}) {
|
|
63
|
+
const config = this.init();
|
|
64
|
+
const auth = Buffer.from(`${config.apiKey}:`).toString('base64');
|
|
65
|
+
|
|
66
|
+
const fetchOptions = {
|
|
67
|
+
method: options.method || 'GET',
|
|
68
|
+
headers: {
|
|
69
|
+
'Authorization': `Basic ${auth}`,
|
|
70
|
+
...options.headers,
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// Encode body as form data for POST/PUT
|
|
75
|
+
if (options.body && typeof options.body === 'object') {
|
|
76
|
+
fetchOptions.headers['Content-Type'] = 'application/x-www-form-urlencoded';
|
|
77
|
+
fetchOptions.body = encodeFormData(options.body);
|
|
78
|
+
} else if (options.body) {
|
|
79
|
+
fetchOptions.headers['Content-Type'] = 'application/x-www-form-urlencoded';
|
|
80
|
+
fetchOptions.body = options.body;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const response = await fetch(`${config.baseUrl}${endpoint}`, fetchOptions);
|
|
84
|
+
|
|
85
|
+
// 204 No Content
|
|
86
|
+
if (response.status === 204) {
|
|
87
|
+
return {};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const data = await response.json();
|
|
91
|
+
|
|
92
|
+
if (!response.ok) {
|
|
93
|
+
const msg = data.message || data.error_description || JSON.stringify(data);
|
|
94
|
+
throw new Error(`Chargebee API ${response.status}: ${msg}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return data;
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Fetch the latest resource from Chargebee's API
|
|
102
|
+
* Falls back to the raw webhook payload if the API call fails
|
|
103
|
+
*
|
|
104
|
+
* @param {string} resourceType - 'subscription' or 'invoice'
|
|
105
|
+
* @param {string} resourceId - Chargebee resource ID
|
|
106
|
+
* @param {object} rawFallback - Fallback data from webhook payload
|
|
107
|
+
* @param {object} context - Additional context
|
|
108
|
+
* @returns {object} Full Chargebee resource object
|
|
109
|
+
*/
|
|
110
|
+
async fetchResource(resourceType, resourceId, rawFallback, context) {
|
|
111
|
+
try {
|
|
112
|
+
if (resourceType === 'subscription') {
|
|
113
|
+
const result = await this.request(`/subscriptions/${resourceId}`);
|
|
114
|
+
return result.subscription || result;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (resourceType === 'invoice') {
|
|
118
|
+
const result = await this.request(`/invoices/${resourceId}`);
|
|
119
|
+
return result.invoice || result;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
throw new Error(`Unknown resource type: ${resourceType}`);
|
|
123
|
+
} catch (e) {
|
|
124
|
+
// If the API call fails but we have raw webhook data, use it
|
|
125
|
+
if (rawFallback && Object.keys(rawFallback).length > 0) {
|
|
126
|
+
return rawFallback;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
throw e;
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Extract the internal orderId from a Chargebee resource
|
|
135
|
+
* Checks meta_data JSON first (new), then cf_clientorderid (legacy)
|
|
136
|
+
*
|
|
137
|
+
* @param {object} resource - Raw Chargebee resource
|
|
138
|
+
* @returns {string|null}
|
|
139
|
+
*/
|
|
140
|
+
getOrderId(resource) {
|
|
141
|
+
const meta = parseMetaData(resource);
|
|
142
|
+
|
|
143
|
+
if (meta.orderId) {
|
|
144
|
+
return meta.orderId;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Legacy: cf_clientorderid custom field
|
|
148
|
+
return resource.cf_clientorderid || null;
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Extract the UID from a Chargebee resource
|
|
153
|
+
* Checks meta_data JSON first (new), then cf_uid (legacy)
|
|
154
|
+
*
|
|
155
|
+
* @param {object} resource - Raw Chargebee resource
|
|
156
|
+
* @returns {string|null}
|
|
157
|
+
*/
|
|
158
|
+
getUid(resource) {
|
|
159
|
+
const meta = parseMetaData(resource);
|
|
160
|
+
|
|
161
|
+
if (meta.uid) {
|
|
162
|
+
return meta.uid;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Legacy: cf_uid custom field
|
|
166
|
+
return resource.cf_uid || null;
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Extract refund details from a Chargebee payment_refunded webhook payload
|
|
171
|
+
*
|
|
172
|
+
* @param {object} raw - Raw Chargebee webhook payload
|
|
173
|
+
* @returns {{ amount: string|null, currency: string, reason: string|null }}
|
|
174
|
+
*/
|
|
175
|
+
getRefundDetails(raw) {
|
|
176
|
+
const creditNote = raw?.content?.credit_note;
|
|
177
|
+
const transaction = raw?.content?.transaction;
|
|
178
|
+
|
|
179
|
+
// Credit note has the refund amount
|
|
180
|
+
if (creditNote) {
|
|
181
|
+
return {
|
|
182
|
+
amount: creditNote.total ? (creditNote.total / 100).toFixed(2) : null,
|
|
183
|
+
currency: creditNote.currency_code?.toUpperCase() || 'USD',
|
|
184
|
+
reason: creditNote.reason_code || null,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Fall back to transaction
|
|
189
|
+
if (transaction) {
|
|
190
|
+
return {
|
|
191
|
+
amount: transaction.amount ? (transaction.amount / 100).toFixed(2) : null,
|
|
192
|
+
currency: transaction.currency_code?.toUpperCase() || 'USD',
|
|
193
|
+
reason: null,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return { amount: null, currency: 'USD', reason: null };
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Transform a raw Chargebee subscription object into the unified subscription shape
|
|
202
|
+
*
|
|
203
|
+
* @param {object} rawSubscription - Raw Chargebee subscription object
|
|
204
|
+
* @param {object} options
|
|
205
|
+
* @param {object} options.config - BEM config (must contain products array)
|
|
206
|
+
* @param {string} options.eventName - Name of the webhook event
|
|
207
|
+
* @param {string} options.eventId - ID of the webhook event
|
|
208
|
+
* @returns {object} Unified subscription object
|
|
209
|
+
*/
|
|
210
|
+
toUnifiedSubscription(rawSubscription, options) {
|
|
211
|
+
options = options || {};
|
|
212
|
+
const config = options.config || {};
|
|
213
|
+
|
|
214
|
+
const status = resolveStatus(rawSubscription);
|
|
215
|
+
const cancellation = resolveCancellation(rawSubscription);
|
|
216
|
+
const trial = resolveTrial(rawSubscription);
|
|
217
|
+
const frequency = resolveFrequency(rawSubscription);
|
|
218
|
+
const product = resolveProduct(rawSubscription, config);
|
|
219
|
+
const expires = resolveExpires(rawSubscription);
|
|
220
|
+
const startDate = resolveStartDate(rawSubscription);
|
|
221
|
+
const price = resolvePrice(product.id, frequency, config);
|
|
222
|
+
|
|
223
|
+
const meta = parseMetaData(rawSubscription);
|
|
224
|
+
|
|
225
|
+
const now = powertools.timestamp(new Date(), { output: 'string' });
|
|
226
|
+
const nowUNIX = powertools.timestamp(now, { output: 'unix' });
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
product: product,
|
|
230
|
+
status: status,
|
|
231
|
+
expires: expires,
|
|
232
|
+
trial: trial,
|
|
233
|
+
cancellation: cancellation,
|
|
234
|
+
payment: {
|
|
235
|
+
processor: 'chargebee',
|
|
236
|
+
orderId: meta.orderId || rawSubscription.cf_clientorderid || null,
|
|
237
|
+
resourceId: rawSubscription.id || null,
|
|
238
|
+
frequency: frequency,
|
|
239
|
+
price: price,
|
|
240
|
+
startDate: startDate,
|
|
241
|
+
updatedBy: {
|
|
242
|
+
event: {
|
|
243
|
+
name: options.eventName || null,
|
|
244
|
+
id: options.eventId || null,
|
|
245
|
+
},
|
|
246
|
+
date: {
|
|
247
|
+
timestamp: now,
|
|
248
|
+
timestampUNIX: nowUNIX,
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
},
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Transform a raw Chargebee one-time payment resource into a unified shape
|
|
257
|
+
* One-time payments come through as non-recurring invoices
|
|
258
|
+
*
|
|
259
|
+
* @param {object} rawResource - Raw Chargebee invoice resource
|
|
260
|
+
* @param {object} options
|
|
261
|
+
* @returns {object} Unified one-time payment object
|
|
262
|
+
*/
|
|
263
|
+
toUnifiedOneTime(rawResource, options) {
|
|
264
|
+
options = options || {};
|
|
265
|
+
const config = options.config || {};
|
|
266
|
+
|
|
267
|
+
const now = powertools.timestamp(new Date(), { output: 'string' });
|
|
268
|
+
const nowUNIX = powertools.timestamp(now, { output: 'unix' });
|
|
269
|
+
|
|
270
|
+
// Try to resolve product from line items or meta_data
|
|
271
|
+
const meta = parseMetaData(rawResource);
|
|
272
|
+
const productId = meta.productId || null;
|
|
273
|
+
const product = resolveProductOneTime(productId, config);
|
|
274
|
+
const price = resolvePrice(productId, 'once', config);
|
|
275
|
+
|
|
276
|
+
// Resolve status from invoice status
|
|
277
|
+
let status = 'unknown';
|
|
278
|
+
if (rawResource.status === 'paid') {
|
|
279
|
+
status = 'completed';
|
|
280
|
+
} else if (rawResource.status === 'payment_due' || rawResource.status === 'not_paid') {
|
|
281
|
+
status = 'failed';
|
|
282
|
+
} else if (rawResource.status) {
|
|
283
|
+
status = rawResource.status;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
product: product,
|
|
288
|
+
status: status,
|
|
289
|
+
payment: {
|
|
290
|
+
processor: 'chargebee',
|
|
291
|
+
orderId: meta.orderId || rawResource.cf_clientorderid || null,
|
|
292
|
+
resourceId: rawResource.id || null,
|
|
293
|
+
price: price,
|
|
294
|
+
updatedBy: {
|
|
295
|
+
event: {
|
|
296
|
+
name: options.eventName || null,
|
|
297
|
+
id: options.eventId || null,
|
|
298
|
+
},
|
|
299
|
+
date: {
|
|
300
|
+
timestamp: now,
|
|
301
|
+
timestampUNIX: nowUNIX,
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
};
|
|
306
|
+
},
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Build the meta_data JSON string for Chargebee subscriptions
|
|
310
|
+
*
|
|
311
|
+
* @param {string} uid - User's Firebase UID
|
|
312
|
+
* @param {string} orderId - Our internal order ID
|
|
313
|
+
* @param {string} [productId] - Product ID (used for one-time payments)
|
|
314
|
+
* @returns {string} JSON string
|
|
315
|
+
*/
|
|
316
|
+
buildMetaData(uid, orderId, productId) {
|
|
317
|
+
const data = { uid, orderId };
|
|
318
|
+
|
|
319
|
+
if (productId) {
|
|
320
|
+
data.productId = productId;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return JSON.stringify(data);
|
|
324
|
+
},
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Parse the meta_data JSON from a Chargebee resource
|
|
329
|
+
* Falls back to cf_* custom fields for legacy subscriptions
|
|
330
|
+
*
|
|
331
|
+
* @param {object} resource - Chargebee resource
|
|
332
|
+
* @returns {{ uid: string|null, orderId: string|null, productId: string|null }}
|
|
333
|
+
*/
|
|
334
|
+
function parseMetaData(resource) {
|
|
335
|
+
if (!resource) {
|
|
336
|
+
return { uid: null, orderId: null, productId: null };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Try meta_data JSON (new approach)
|
|
340
|
+
const metaData = resource.meta_data;
|
|
341
|
+
|
|
342
|
+
if (metaData) {
|
|
343
|
+
try {
|
|
344
|
+
const parsed = typeof metaData === 'string' ? JSON.parse(metaData) : metaData;
|
|
345
|
+
return {
|
|
346
|
+
uid: parsed.uid || null,
|
|
347
|
+
orderId: parsed.orderId || null,
|
|
348
|
+
productId: parsed.productId || null,
|
|
349
|
+
};
|
|
350
|
+
} catch (e) {
|
|
351
|
+
// Invalid JSON — fall through to legacy
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Legacy: cf_* custom fields
|
|
356
|
+
return {
|
|
357
|
+
uid: resource.cf_uid || null,
|
|
358
|
+
orderId: resource.cf_clientorderid || null,
|
|
359
|
+
productId: null,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Encode an object as application/x-www-form-urlencoded with bracket notation
|
|
365
|
+
* Handles nested objects: { subscription: { plan_id: 'x' } } → subscription[plan_id]=x
|
|
366
|
+
*
|
|
367
|
+
* @param {object} params - Parameters to encode
|
|
368
|
+
* @param {string} [prefix] - Parent key prefix
|
|
369
|
+
* @returns {string} URL-encoded string
|
|
370
|
+
*/
|
|
371
|
+
function encodeFormData(params, prefix) {
|
|
372
|
+
const parts = [];
|
|
373
|
+
|
|
374
|
+
for (const [key, value] of Object.entries(params)) {
|
|
375
|
+
const fullKey = prefix ? `${prefix}[${key}]` : key;
|
|
376
|
+
|
|
377
|
+
if (value === null || value === undefined) {
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (typeof value === 'object' && !Array.isArray(value)) {
|
|
382
|
+
parts.push(encodeFormData(value, fullKey));
|
|
383
|
+
} else if (Array.isArray(value)) {
|
|
384
|
+
for (let i = 0; i < value.length; i++) {
|
|
385
|
+
if (typeof value[i] === 'object') {
|
|
386
|
+
parts.push(encodeFormData(value[i], `${fullKey}[${i}]`));
|
|
387
|
+
} else {
|
|
388
|
+
parts.push(`${encodeURIComponent(`${fullKey}[${i}]`)}=${encodeURIComponent(value[i])}`);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
} else {
|
|
392
|
+
parts.push(`${encodeURIComponent(fullKey)}=${encodeURIComponent(value)}`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return parts.filter(Boolean).join('&');
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Map Chargebee subscription status to unified status
|
|
401
|
+
*
|
|
402
|
+
* | Chargebee Status | Unified Status |
|
|
403
|
+
* |------------------|----------------|
|
|
404
|
+
* | active | active |
|
|
405
|
+
* | in_trial | active |
|
|
406
|
+
* | non_renewing | active |
|
|
407
|
+
* | future | active |
|
|
408
|
+
* | paused | suspended |
|
|
409
|
+
* | cancelled | cancelled |
|
|
410
|
+
* | transferred | cancelled |
|
|
411
|
+
*/
|
|
412
|
+
function resolveStatus(raw) {
|
|
413
|
+
const status = raw.status;
|
|
414
|
+
|
|
415
|
+
if (status === 'active' || status === 'in_trial' || status === 'non_renewing' || status === 'future') {
|
|
416
|
+
return 'active';
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (status === 'paused') {
|
|
420
|
+
return 'suspended';
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// cancelled, transferred, or anything else
|
|
424
|
+
return 'cancelled';
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Resolve cancellation state from Chargebee subscription
|
|
429
|
+
* non_renewing = pending cancellation (cancel at period end)
|
|
430
|
+
* cancelled + cancelled_at = completed cancellation
|
|
431
|
+
*/
|
|
432
|
+
function resolveCancellation(raw) {
|
|
433
|
+
// Pending cancellation: non_renewing status
|
|
434
|
+
if (raw.status === 'non_renewing') {
|
|
435
|
+
const periodEnd = raw.current_term_end
|
|
436
|
+
? powertools.timestamp(new Date(raw.current_term_end * 1000), { output: 'string' })
|
|
437
|
+
: EPOCH_ZERO;
|
|
438
|
+
|
|
439
|
+
return {
|
|
440
|
+
pending: true,
|
|
441
|
+
date: {
|
|
442
|
+
timestamp: periodEnd,
|
|
443
|
+
timestampUNIX: periodEnd !== EPOCH_ZERO
|
|
444
|
+
? powertools.timestamp(periodEnd, { output: 'unix' })
|
|
445
|
+
: EPOCH_ZERO_UNIX,
|
|
446
|
+
},
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Already cancelled
|
|
451
|
+
if (raw.cancelled_at) {
|
|
452
|
+
const cancelledDate = powertools.timestamp(new Date(raw.cancelled_at * 1000), { output: 'string' });
|
|
453
|
+
|
|
454
|
+
return {
|
|
455
|
+
pending: false,
|
|
456
|
+
date: {
|
|
457
|
+
timestamp: cancelledDate,
|
|
458
|
+
timestampUNIX: powertools.timestamp(cancelledDate, { output: 'unix' }),
|
|
459
|
+
},
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// No cancellation
|
|
464
|
+
return {
|
|
465
|
+
pending: false,
|
|
466
|
+
date: {
|
|
467
|
+
timestamp: EPOCH_ZERO,
|
|
468
|
+
timestampUNIX: EPOCH_ZERO_UNIX,
|
|
469
|
+
},
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Resolve trial state from Chargebee subscription
|
|
475
|
+
* Uses trial_start/trial_end unix timestamps (same pattern as Stripe)
|
|
476
|
+
*/
|
|
477
|
+
function resolveTrial(raw) {
|
|
478
|
+
const trialStart = raw.trial_start ? raw.trial_start * 1000 : 0;
|
|
479
|
+
const trialEnd = raw.trial_end ? raw.trial_end * 1000 : 0;
|
|
480
|
+
const activated = !!(trialStart && trialEnd);
|
|
481
|
+
|
|
482
|
+
let trialExpires = { timestamp: EPOCH_ZERO, timestampUNIX: EPOCH_ZERO_UNIX };
|
|
483
|
+
|
|
484
|
+
if (trialEnd) {
|
|
485
|
+
const trialEndDate = powertools.timestamp(new Date(trialEnd), { output: 'string' });
|
|
486
|
+
trialExpires = {
|
|
487
|
+
timestamp: trialEndDate,
|
|
488
|
+
timestampUNIX: powertools.timestamp(trialEndDate, { output: 'unix' }),
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return {
|
|
493
|
+
claimed: activated,
|
|
494
|
+
expires: trialExpires,
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Resolve billing frequency from Chargebee subscription
|
|
500
|
+
*
|
|
501
|
+
* Items model: parse suffix from deterministic item_price_id
|
|
502
|
+
* e.g., "somiibo-pro-monthly" → split('-').pop() → "monthly"
|
|
503
|
+
*
|
|
504
|
+
* Legacy Plans model: use billing_period_unit
|
|
505
|
+
* e.g., "month" → "monthly"
|
|
506
|
+
*/
|
|
507
|
+
function resolveFrequency(raw) {
|
|
508
|
+
// Items model: parse from deterministic item_price_id
|
|
509
|
+
const itemPriceId = raw.subscription_items?.[0]?.item_price_id;
|
|
510
|
+
|
|
511
|
+
if (itemPriceId) {
|
|
512
|
+
const suffix = itemPriceId.split('-').pop();
|
|
513
|
+
|
|
514
|
+
if (VALID_FREQUENCIES.includes(suffix)) {
|
|
515
|
+
return suffix;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Legacy Plans model: use billing_period_unit
|
|
520
|
+
const unit = raw.billing_period_unit;
|
|
521
|
+
|
|
522
|
+
return UNIT_TO_FREQUENCY[unit] || null;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Resolve product by matching Chargebee subscription against config products
|
|
527
|
+
*
|
|
528
|
+
* Items model: subscription_items[0].item_price_id starts with product.chargebee.itemId + '-'
|
|
529
|
+
* e.g., "somiibo-pro-monthly".startsWith("somiibo-pro-") → match
|
|
530
|
+
*
|
|
531
|
+
* Legacy Plans model: plan_id matches product.chargebee.legacyPlanIds[]
|
|
532
|
+
* e.g., "somiibo-premium-monthly-1" in legacyPlanIds → match
|
|
533
|
+
*/
|
|
534
|
+
function resolveProduct(raw, config) {
|
|
535
|
+
const itemPriceId = raw.subscription_items?.[0]?.item_price_id;
|
|
536
|
+
const planId = raw.plan_id;
|
|
537
|
+
|
|
538
|
+
if (!config.payment?.products) {
|
|
539
|
+
return { id: 'basic', name: 'Basic' };
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Items model takes priority — check all products first
|
|
543
|
+
if (itemPriceId) {
|
|
544
|
+
for (const product of config.payment.products) {
|
|
545
|
+
if (product.chargebee?.itemId && itemPriceId.startsWith(product.chargebee.itemId + '-')) {
|
|
546
|
+
return { id: product.id, name: product.name || product.id };
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Legacy Plans model — fallback
|
|
552
|
+
if (planId) {
|
|
553
|
+
for (const product of config.payment.products) {
|
|
554
|
+
if (product.chargebee?.legacyPlanIds?.includes(planId)) {
|
|
555
|
+
return { id: product.id, name: product.name || product.id };
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
return { id: 'basic', name: 'Basic' };
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Resolve product for one-time payments by productId from metadata
|
|
565
|
+
*/
|
|
566
|
+
function resolveProductOneTime(productId, config) {
|
|
567
|
+
if (!productId || !config.payment?.products) {
|
|
568
|
+
return { id: productId || 'unknown', name: 'Unknown' };
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const product = config.payment.products.find(p => p.id === productId);
|
|
572
|
+
|
|
573
|
+
if (!product) {
|
|
574
|
+
return { id: productId, name: productId };
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
return { id: product.id, name: product.name || product.id };
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Resolve subscription expiration from Chargebee data
|
|
582
|
+
* Uses current_term_end (unix timestamp)
|
|
583
|
+
*/
|
|
584
|
+
function resolveExpires(raw) {
|
|
585
|
+
const termEnd = raw.current_term_end;
|
|
586
|
+
|
|
587
|
+
if (!termEnd) {
|
|
588
|
+
return {
|
|
589
|
+
timestamp: EPOCH_ZERO,
|
|
590
|
+
timestampUNIX: EPOCH_ZERO_UNIX,
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const expiresDate = powertools.timestamp(new Date(termEnd * 1000), { output: 'string' });
|
|
595
|
+
|
|
596
|
+
return {
|
|
597
|
+
timestamp: expiresDate,
|
|
598
|
+
timestampUNIX: powertools.timestamp(expiresDate, { output: 'unix' }),
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Resolve subscription start date from Chargebee data
|
|
604
|
+
* Uses started_at or created_at (unix timestamps)
|
|
605
|
+
*/
|
|
606
|
+
function resolveStartDate(raw) {
|
|
607
|
+
const startTs = raw.started_at || raw.created_at;
|
|
608
|
+
|
|
609
|
+
if (!startTs) {
|
|
610
|
+
return {
|
|
611
|
+
timestamp: EPOCH_ZERO,
|
|
612
|
+
timestampUNIX: EPOCH_ZERO_UNIX,
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
const startDate = powertools.timestamp(new Date(startTs * 1000), { output: 'string' });
|
|
617
|
+
|
|
618
|
+
return {
|
|
619
|
+
timestamp: startDate,
|
|
620
|
+
timestampUNIX: powertools.timestamp(startDate, { output: 'unix' }),
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Resolve the display price for a product/frequency from config
|
|
626
|
+
*/
|
|
627
|
+
function resolvePrice(productId, frequency, config) {
|
|
628
|
+
const product = config.payment?.products?.find(p => p.id === productId);
|
|
629
|
+
|
|
630
|
+
if (!product || !product.prices) {
|
|
631
|
+
return 0;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
return product.prices[frequency] || 0;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
module.exports = Chargebee;
|