backend-manager 5.0.73 → 5.0.74

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.
Files changed (50) hide show
  1. package/CLAUDE.md +70 -0
  2. package/README.md +81 -7
  3. package/package.json +1 -1
  4. package/src/manager/cron/daily/reset-usage.js +5 -32
  5. package/src/manager/events/firestore/payments-webhooks/on-write.js +126 -0
  6. package/src/manager/functions/core/actions/api/admin/get-stats.js +3 -3
  7. package/src/manager/functions/core/actions/api/general/add-marketing-contact.js +1 -1
  8. package/src/manager/functions/core/actions/api/user/delete.js +5 -3
  9. package/src/manager/functions/core/actions/api/user/get-subscription-info.js +25 -9
  10. package/src/manager/functions/core/actions/api/user/validate-settings.js +1 -1
  11. package/src/manager/helpers/analytics.js +4 -4
  12. package/src/manager/helpers/api-manager.js +25 -42
  13. package/src/manager/helpers/middleware.js +1 -1
  14. package/src/manager/helpers/usage.js +24 -93
  15. package/src/manager/helpers/user.js +29 -38
  16. package/src/manager/index.js +22 -10
  17. package/src/manager/libraries/stripe.js +293 -0
  18. package/src/manager/routes/admin/stats/get.js +3 -3
  19. package/src/manager/routes/marketing/contact/post.js +1 -1
  20. package/src/manager/routes/payments/intent/post.js +94 -0
  21. package/src/manager/routes/payments/intent/providers/stripe.js +66 -0
  22. package/src/manager/routes/payments/webhook/post.js +87 -0
  23. package/src/manager/routes/payments/webhook/providers/stripe.js +35 -0
  24. package/src/manager/routes/test/schema/post.js +5 -5
  25. package/src/manager/routes/user/delete.js +5 -3
  26. package/src/manager/routes/user/settings/validate/post.js +3 -3
  27. package/src/manager/routes/user/subscription/get.js +25 -9
  28. package/src/manager/schemas/payments/intent/post.js +22 -0
  29. package/src/manager/schemas/payments/webhook/post.js +6 -0
  30. package/src/manager/schemas/test/schema/post.js +1 -1
  31. package/src/test/test-accounts.js +63 -25
  32. package/src/test/utils/firestore-rules-client.js +5 -5
  33. package/templates/backend-manager-config.json +32 -0
  34. package/templates/firestore.rules +1 -1
  35. package/test/_init/accounts-validation.js +3 -3
  36. package/test/functions/user/delete.js +1 -1
  37. package/test/functions/user/get-subscription-info.js +18 -24
  38. package/test/payments/intent.js +104 -0
  39. package/test/payments/journey-payment-cancel.js +166 -0
  40. package/test/payments/journey-payment-suspend.js +162 -0
  41. package/test/payments/journey-payment-trial.js +167 -0
  42. package/test/payments/journey-payment-upgrade.js +136 -0
  43. package/test/payments/webhook.js +128 -0
  44. package/test/routes/test/schema.js +1 -1
  45. package/test/routes/user/delete.js +1 -1
  46. package/test/routes/user/subscription.js +18 -24
  47. package/test/routes/user/user.js +14 -14
  48. package/test/rules/user.js +8 -8
  49. package/src/manager/helpers/subscription-resolver-new.js +0 -827
  50. package/src/manager/helpers/subscription-resolver.js +0 -841
@@ -1,827 +0,0 @@
1
- const moment = require('moment');
2
-
3
- function SubscriptionResolver(Manager, profile, resource) {
4
- const self = this;
5
-
6
- self.Manager = Manager;
7
- self.profile = profile || {};
8
- self.resource = resource || {};
9
-
10
- return self;
11
- }
12
-
13
- SubscriptionResolver.prototype.resolve = function (options) {
14
- const self = this;
15
-
16
- const resolved = {
17
- status: '',
18
- frequency: '',
19
- resource: {
20
- id: '',
21
- },
22
- payment: {
23
- completed: false,
24
- refunded: false,
25
- },
26
- start: {
27
- timestamp: moment(0),
28
- timestampUNIX: moment(0),
29
- },
30
- expires: {
31
- timestamp: moment(0),
32
- timestampUNIX: moment(0),
33
- },
34
- cancelled: {
35
- timestamp: moment(0),
36
- timestampUNIX: moment(0),
37
- },
38
- lastPayment: {
39
- amount: 0,
40
- date: {
41
- timestamp: moment(0),
42
- timestampUNIX: moment(0),
43
- }
44
- },
45
- trial: {
46
- claimed: false,
47
- active: false,
48
- daysLeft: 0,
49
- }
50
- }
51
-
52
- // Set
53
- const profile = self.profile;
54
- const resource = self.resource;
55
-
56
- // Set defaults
57
- profile.type = profile.type || null;
58
- profile.details = profile.details || {};
59
- profile.details.planFrequency = profile.details.planFrequency || null;
60
- profile.authorization = profile.authorization || {};
61
- profile.authorization.status = profile.authorization.status || 'pending';
62
-
63
- // Set
64
- options = options || {};
65
- options.log = typeof options.log === 'undefined' ? false : options.log;
66
- options.resolveProcessor = typeof options.resolveProcessor === 'undefined' ? false : options.resolveProcessor;
67
- options.resolveType = typeof options.resolveType === 'undefined' ? false : options.resolveType;
68
- options.today = typeof options.today === 'undefined' ? moment() : moment(options.today);
69
-
70
- // Set provider if not set
71
- if (!profile.processor) {
72
- /*** PayPal ***/
73
- // Order
74
- if (
75
- resource.purchase_units
76
- ) {
77
- profile.processor = 'paypal';
78
- profile.type = profile.type || 'order';
79
- // Subscription
80
- } else if (
81
- // resource.billing_info
82
- resource.create_time
83
- ) {
84
- profile.processor = 'paypal';
85
- profile.type = profile.type || 'subscription';
86
-
87
- /*** Chargebee ***/
88
- // Order
89
- } else if (
90
- resource.line_items
91
- ) {
92
- profile.processor = 'chargebee';
93
- profile.type = profile.type || 'order';
94
- // Subscription
95
- } else if (
96
- resource.billing_period_unit
97
- ) {
98
- profile.processor = 'chargebee';
99
- profile.type = profile.type || 'subscription';
100
-
101
- /*** Stripe ***/
102
- // Order
103
- } else if (
104
- resource.object === 'charge'
105
- ) {
106
- profile.processor = 'stripe';
107
- profile.type = profile.type || 'order';
108
- // Subscription
109
- } else if (
110
- resource.object === 'subscription'
111
- ) {
112
- profile.processor = 'stripe';
113
- profile.type = profile.type || 'subscription';
114
-
115
- /*** Coinbase ***/
116
- // Order AND Subscription
117
- } else if (
118
- resource.addresses
119
- ) {
120
- profile.processor = 'coinbase';
121
- // profile.type = profile.type || 'subscription';
122
-
123
- /*** Error ***/
124
- } else {
125
- throw new Error('Unable to determine subscription provider');
126
- }
127
- }
128
-
129
- // Set profile.type
130
- if (!profile.type) {
131
- profile.type = profile.type || 'subscription';
132
- }
133
-
134
- // Set processor if needed
135
- if (options.resolveProcessor) {
136
- resolved.processor = profile.processor;
137
- }
138
-
139
- // Set type if needed
140
- if (options.resolveType) {
141
- resolved.type = profile.type;
142
- }
143
-
144
- // Set frequency if order
145
- if (profile.type === 'order') {
146
- resolved.frequency = 'single';
147
- }
148
-
149
- // Log if requested
150
- if (options.log) {
151
- console.log('profile', profile);
152
- console.log('resource', resource);
153
- }
154
-
155
- // Resolve
156
- const processor = self[`resolve_${profile.processor}`];
157
- if (processor) {
158
- processor(profile, resource, resolved, options);
159
- } else {
160
- throw new Error('Unknown processor');
161
- }
162
-
163
- // console.log('---resolved', resolved);
164
-
165
- // Check for frequency
166
- if (!resolved.frequency) {
167
- throw new Error('Unknown frequency');
168
- }
169
-
170
- // console.log('----expires 3', resolved.resource.id, resolved.status, resolved.frequency, resolved.trial.active, resolved.expires.timestamp.toISOString ? resolved.expires.timestamp.toISOString() : resolved.expires.timestamp);
171
- console.log('--- 1', resolved.resource.id, resolved.status, resolved.expires.timestamp);
172
- console.log('---', resolved.trial.active, resolved.trial.claimed, resolved.payment.completed, resolved.lastPayment.amount);
173
-
174
- // If they are not trialing AND there was NEVER any payment sent OR the last payment failed, then set the expiration to 0
175
- if (
176
- !resolved.trial.active && resolved.trial.claimed
177
- && (!resolved.payment.completed || resolved.lastPayment.amount === 0)
178
- // && (resolved.status === 'active' || resolved.status === 'suspended')
179
- ) {
180
- // resolved.expires.timestamp = moment(0);
181
- if (resolved.trial.claimed) {
182
- resolved.status = 'suspended';
183
- } else {
184
-
185
- }
186
- }
187
-
188
- // If they are trialing and the authorization charge is failed, set to suspended
189
- if (
190
- resolved.trial.active
191
- && profile.authorization.status === 'failed'
192
- ) {
193
- resolved.status = 'suspended';
194
- }
195
-
196
- // If they got a refund, set the expiration to 0
197
- if (resolved.payment.refunded) {
198
- // resolved.expires.timestamp = moment(0);
199
- resolved.status = 'suspended';
200
- }
201
-
202
- // If they are suspended, set the expiration to 0
203
- if (resolved.status === 'suspended') {
204
- resolved.expires.timestamp = moment(0);
205
- }
206
- console.log('--- 2', resolved.resource.id, resolved.status, resolved.expires.timestamp);
207
-
208
- // console.log('----expires 4', resolved.resource.id, resolved.status, resolved.frequency, resolved.trial.active, resolved.expires.timestamp.toISOString ? resolved.expires.timestamp.toISOString() : resolved.expires.timestamp);
209
-
210
- // Fix expiry by adding time to the date of last payment
211
- // console.log('----expires 2', resolved.resource.id, resolved.status, resolved.frequency, resolved.trial.active, resolved.expires.timestamp.toISOString ? resolved.expires.timestamp.toISOString() : resolved.expires.timestamp);
212
- if (resolved.status === 'active') {
213
- // Set days left
214
- if (resolved.trial.active) {
215
- resolved.trial.daysLeft = Math.abs(resolved.expires.timestamp.diff(options.today, 'days'));
216
- }
217
-
218
- // Set expiration
219
- resolved.expires.timestamp.add(1, 'year').add(30, 'days');
220
- } else if (resolved.status === 'cancelled') {
221
- // If trial, it's already set to the trial end above
222
- if (!resolved.trial.active) {
223
- // if (!resolved.trial.claimed) {
224
- if (resolved.frequency === 'annually') {
225
- resolved.expires.timestamp.add(1, 'year');
226
- } else if (resolved.frequency === 'monthly') {
227
- resolved.expires.timestamp.add(1, 'month');
228
- } else if (resolved.frequency === 'weekly') {
229
- resolved.expires.timestamp.add(1, 'week');
230
- } else if (resolved.frequency === 'daily') {
231
- resolved.expires.timestamp.add(1, 'day');
232
- }
233
- }
234
- }
235
-
236
- // Fix timestamps
237
- resolved.start.timestampUNIX = resolved.start.timestamp.unix();
238
- resolved.start.timestamp = resolved.start.timestamp.toISOString();
239
-
240
- resolved.expires.timestampUNIX = resolved.expires.timestamp.unix();
241
- resolved.expires.timestamp = resolved.expires.timestamp.toISOString ? resolved.expires.timestamp.toISOString() : resolved.expires.timestamp;
242
-
243
- resolved.cancelled.timestampUNIX = resolved.cancelled.timestamp.unix();
244
- resolved.cancelled.timestamp = resolved.cancelled.timestamp.toISOString();
245
-
246
- // Fix trial days
247
- resolved.trial.daysLeft = resolved.trial.daysLeft < 0 ? 0 : resolved.trial.daysLeft;
248
-
249
- // Set last payment
250
- resolved.lastPayment.date.timestampUNIX = moment(resolved.lastPayment.date.timestamp).unix();
251
- resolved.lastPayment.date.timestamp = resolved.lastPayment.date.timestamp.toISOString();
252
-
253
- // Log if needed
254
- console.log('--- 3', resolved.resource.id, resolved.status, resolved.expires.timestamp);
255
- if (options.log) {
256
- console.log('resolved', resolved);
257
- }
258
-
259
- self.resolved = resolved;
260
- // console.log('----expires 6', resolved.resource.id, resolved.status, resolved.frequency, resolved.trial.active, resolved.expires.timestamp.toISOString ? resolved.expires.timestamp.toISOString() : resolved.expires.timestamp);
261
-
262
- return resolved;
263
- };
264
-
265
- SubscriptionResolver.prototype.resolve_paypal = function (profile, resource, resolved, options) {
266
- const self = this;
267
-
268
- // Set status
269
- /*
270
- subscription: https://developer.paypal.com/docs/api/subscriptions/v1/#subscriptions_get
271
- APPROVAL_PENDING. The subscription is created but not yet approved by the buyer.
272
- APPROVED. The buyer has approved the subscription.
273
- ACTIVE. The subscription is active.
274
- SUSPENDED. The subscription is suspended.
275
- CANCELLED. The subscription is cancelled.
276
- EXPIRED. The subscription is expired.
277
-
278
- order: https://developer.paypal.com/docs/api/orders/v2/#orders_get
279
- CREATED The order was created with the specified context.
280
- SAVED The order was saved and persisted. The order status continues to be in progress until a capture is made with final_capture = true for all purchase units within the order.
281
- APPROVED The customer approved the payment through the PayPal wallet or another form of guest or unbranded payment. For example, a card, bank account, or so on.
282
- VOIDED All purchase units in the order are voided.
283
- COMPLETED The payment was authorized or the authorized payment was captured for the order.
284
- PAYER_ACTION_REQUIRED The order requires an action from the payer (e.g. 3DS authentication). Redirect the payer to the "rel":"payer-action" HATEOAS link returned as part of the response prior to authorizing or capturing the order.
285
- */
286
- if (['ACTIVE'].includes(resource.status)) {
287
- resolved.status = 'active';
288
-
289
- // Check for failed payments
290
- /*
291
- Special condition for PayPal
292
- Because I set the payment_failure_threshold to 0, it will not automatically set the status to suspended.
293
- We must check for failed payments and set the status to suspended if there are any.
294
- */
295
- if ((resource?.billing_info?.failed_payments_count ?? 0) > 0) {
296
- resolved.status = 'suspended';
297
- }
298
- } else if (['SUSPENDED'].includes(resource.status)) {
299
- resolved.status = 'suspended';
300
- } else {
301
- resolved.status = 'cancelled';
302
- }
303
-
304
- // Setup preliminary variables
305
- const order = resource?.purchase_units?.[0]?.payments?.captures?.[0];
306
- const subscription = resource?.billing_info?.last_payment;
307
- const isOrder = !!order;
308
-
309
- // Set resource ID
310
- resolved.resource.id = resource.id;
311
-
312
- // Set start
313
- resolved.start.timestamp = moment(
314
- (
315
- isOrder
316
- // Order
317
- ? (resource?.create_time ?? 0)
318
-
319
- // Subscription
320
- : (resource?.start_time ?? 0)
321
- )
322
- )
323
-
324
- // Set expiration
325
- resolved.expires.timestamp = moment(
326
- (
327
- isOrder
328
- // Order
329
- ? (resource?.create_time ?? 0)
330
-
331
- // Subscription
332
- : (resource?.billing_info?.last_payment?.time ?? 0)
333
- )
334
- )
335
-
336
- // Set cancelled
337
- if (resolved.status === 'cancelled') {
338
- resolved.cancelled.timestamp = moment(
339
- (
340
- isOrder
341
- // Order
342
- ? (resource?.create_time ?? 0)
343
-
344
- // Subscription
345
- : (resource?.status_update_time ?? 0)
346
- )
347
- )
348
- }
349
-
350
- // Set last payment
351
- if (order) {
352
- resolved.lastPayment.amount = parseFloat(
353
- order?.amount?.value ?? '0.00'
354
- );
355
- resolved.lastPayment.date.timestamp = moment(
356
- order.create_time || 0
357
- );
358
- } else if (subscription) {
359
- resolved.lastPayment.amount = parseFloat(subscription.amount.value);
360
- resolved.lastPayment.date.timestamp = moment(subscription.time);
361
- }
362
-
363
- // Get trial
364
- const trialTenure = (resource?.plan?.billing_cycles ?? []).find((cycle) => cycle.tenure_type === 'TRIAL');
365
- const regularTenure = (resource?.plan?.billing_cycles ?? []).find((cycle) => cycle.tenure_type === 'REGULAR');
366
- const trialClaimed = !!trialTenure && parseFloat(trialTenure?.pricing_scheme?.fixed_price?.value ?? '0.00') === 0;
367
-
368
- // Resolve trial
369
- /*
370
- Special condition for PayPal
371
- Because you cannot remove trial on a sub-level, you have to charge a prorated amount for the "trial".
372
- Even if charged, it is still considered a trial period by paypal.
373
- Thus, we must remove the trial indicator if the user has been charged.
374
- */
375
- if (
376
- resolved.status === 'active'
377
- && (trialTenure && regularTenure && regularTenure.total_cycles === 0)
378
- && resolved.lastPayment.amount === 0
379
- ) {
380
- resolved.trial.active = true;
381
-
382
- // Set expiration
383
- resolved.expires.timestamp = moment(
384
- resource?.billing_info?.next_billing_time ?? 0
385
- )
386
-
387
- /*
388
- Special condition for PayPal #2
389
- I want to put the subscription in a suspended state if it's even one day past due
390
- */
391
- const trialLength = trialTenure?.frequency?.interval_count ?? 0;
392
- const daysSinceStart = Math.abs(moment(options.today).diff(moment(resolved.start.timestamp), 'days'));
393
- if (daysSinceStart > trialLength) {
394
- resolved.status = 'suspended';
395
- resolved.trial.active = false;
396
- }
397
- // console.log('----resolved.resource.id', resolved.resource.id);
398
- // console.log('----resolved.start.timestamp', resolved.start.timestamp);
399
- // console.log('----options.today', options.today);
400
- // console.log('======daysSinceStart', daysSinceStart);
401
- // console.log('======trialLength', trialLength);
402
- }
403
- resolved.trial.claimed = trialClaimed;
404
-
405
- // Resolve frequency
406
- const unit = regularTenure?.frequency?.interval_unit;
407
- if (unit === 'YEAR') {
408
- resolved.frequency = 'annually';
409
- } else if (unit === 'MONTH') {
410
- resolved.frequency = 'monthly';
411
- } else if (unit === 'WEEK') {
412
- resolved.frequency = 'weekly';
413
- } else if (unit === 'DAY') {
414
- resolved.frequency = 'daily';
415
- }
416
-
417
- // Set completed
418
- if (!resource.plan) {
419
- resolved.payment.completed = !['CREATED', 'SAVED', 'APPROVED', 'VOIDED', 'PAYER_ACTION_REQUIRED'].includes(resource.status);
420
- } else {
421
- resolved.payment.completed = !['APPROVAL_PENDING', 'APPROVED'].includes(resource.status);
422
- }
423
-
424
- // Check if refunded
425
- if (!resource.plan) {
426
- // resolved.payment.refunded = false; // @@@ TODO: check if this is correct
427
- } else {
428
- const transactions = resource?.transactions ?? [];
429
-
430
- resolved.payment.refunded = transactions.some(t => t.status === 'REFUNDED');
431
- }
432
-
433
- return resolved;
434
- }
435
-
436
- SubscriptionResolver.prototype.resolve_chargebee = function (profile, resource, resolved, options) {
437
- const self = this;
438
-
439
- // Set status
440
- // subscription: https://apidocs.chargebee.com/docs/api/subscriptions?prod_cat_ver=2#subscription_status
441
- // future The subscription is scheduled to start at a future date.
442
- // in_trial The subscription is in trial.
443
- // active The subscription is active and will be charged for automatically based on the items in it.
444
- // non_renewing The subscription will be canceled at the end of the current term.
445
- // paused The subscription is paused. The subscription will not renew while in this state.
446
- // cancelled The subscription has been canceled and is no longer in service.
447
-
448
- // order: https://apidocs.chargebee.com/docs/api/invoices?prod_cat_ver=2#invoice_status
449
- // paid: Indicates a paid invoice.
450
- // posted: Indicates the payment is not yet collected and will be in this state till the due date to indicate the due period.
451
- // payment_due: Indicates the payment is not yet collected and is being retried as per retry settings.
452
- // not_paid: Indicates the payment is not made and all attempts to collect is failed.
453
- // voided: Indicates a voided invoice.
454
- // pending: The invoice is yet to be closed (sent for payment collection). An invoice is generated with this status when it has line items that belong to items that are metered or when the subscription.create_pending_invoicesattribute is set to true.
455
-
456
- if (['in_trial', 'active'].includes(resource.status)) {
457
- resolved.status = 'active';
458
-
459
- // If there's a due invoice, it's suspended
460
- if (resource.total_dues > 0) {
461
- resolved.status = 'suspended';
462
- }
463
- } else if (['paused'].includes(resource.status)) {
464
- resolved.status = 'suspended';
465
- } else {
466
- resolved.status = 'cancelled';
467
- }
468
-
469
- // Setup preliminary variables
470
- const isOrder = profile.type === 'order';
471
-
472
- // Set resource ID
473
- resolved.resource.id = resource.id;
474
-
475
- // Set start
476
- resolved.start.timestamp = moment(
477
- (
478
- isOrder
479
- // Order
480
- ? (resource?.date ?? 0)
481
-
482
- // Subscription
483
- : (resource?.created_at ?? 0)
484
- ) * 1000
485
- )
486
-
487
- // Set expiration
488
- resolved.expires.timestamp = moment(
489
- (
490
- isOrder
491
- // Order
492
- ? (resource?.date ?? 0)
493
-
494
- // Subscription
495
- : (resource?.current_term_start ?? 0)
496
- ) * 1000
497
- )
498
- // console.log('---resolved.expires 1', resolved.expires);
499
- // if (resource.total_dues > 0) {
500
- // resolved.expires.timestamp = moment(0);
501
- // } else {
502
- // resolved.expires.timestamp = moment(
503
- // (
504
- // get(resource, 'current_term_start', 0)
505
- // ) * 1000
506
- // )
507
- // }
508
-
509
- // Set cancelled
510
- if (resolved.status === 'cancelled') {
511
- resolved.cancelled.timestamp = moment(
512
- (
513
- isOrder
514
- // Order
515
- ? (resource?.date ?? 0)
516
-
517
- // Subscription
518
- : (resource?.cancelled_at ?? 0)
519
- ) * 1000
520
- )
521
- }
522
-
523
- // Set last payment
524
- if (
525
- // Order
526
- resource.amount_due > 0
527
-
528
- // Subscription
529
- || resource.total_dues > 0
530
- ) {
531
- resolved.lastPayment.amount = 0;
532
- resolved.lastPayment.date.timestamp = moment(
533
- (
534
- isOrder
535
- // Order
536
- ? (resource.date || 0)
537
-
538
- // Subscription
539
- : (resource.due_since || 0)
540
- ) * 1000
541
- );
542
- } else {
543
- resolved.lastPayment.amount = (
544
- (
545
- isOrder
546
- // Order
547
- ? (resource.amount_paid)
548
-
549
- // Subscription
550
- : (resource.plan_amount)
551
- ) / 100
552
- )
553
- resolved.lastPayment.date.timestamp = moment(
554
- (
555
- isOrder
556
- // Order
557
- ? (resource.date || 0)
558
-
559
- // Subscription
560
- : (resource.current_term_start || 0)
561
- ) * 1000
562
- );
563
- }
564
-
565
- // Get trial
566
- if (resource.status === 'in_trial') {
567
- resolved.trial.active = true;
568
-
569
- // Set expiration
570
- resolved.expires.timestamp = moment(
571
- (
572
- resource?.trial_end ?? 0
573
- ) * 1000
574
- )
575
- }
576
-
577
- // Resolve frequency
578
- const unit = resource?.billing_period_unit;
579
- if (unit === 'year') {
580
- resolved.frequency = 'annually';
581
- } else if (unit === 'month') {
582
- resolved.frequency = 'monthly';
583
- } else if (unit === 'week') {
584
- resolved.frequency = 'weekly';
585
- } else if (unit === 'day') {
586
- resolved.frequency = 'daily';
587
- }
588
-
589
- // Set completed
590
- if (isOrder) {
591
- resolved.payment.completed = !['posted', 'payment_due', 'not_paid', 'voided', 'pending'].includes(resource.status);
592
- } else {
593
- resolved.payment.completed = !['future'].includes(resource.status);
594
- }
595
-
596
- // Check if refunded
597
- if (isOrder) {
598
- resolved.payment.refunded = false; // @@@ TODO: check if this is correct
599
- } else {
600
- const invoices = resource?.invoices ?? [];
601
-
602
- resolved.payment.refunded = invoices.some(invoice => {
603
- const creditNotes = invoice?.invoice?.issued_credit_notes ?? [];
604
- return creditNotes.some(creditNote => {
605
- return creditNote.cn_status === 'refunded'
606
- })
607
- })
608
- }
609
-
610
- // Special chargebee reset lastPayment
611
- // If trial is active OR if it was cancelled after the trial has ended
612
- const trialStart = (resource?.trial_start ?? 0) * 1000;
613
- const trialEnd = (resource?.trial_end ?? 0) * 1000;
614
- const cancelledAt = (resource?.cancelled_at ?? 0) * 1000;
615
- const trialDaysDifference = Math.abs(moment(trialEnd).diff(moment(trialStart), 'days'));
616
- const trialClaimed = !!trialStart && !!trialEnd && trialDaysDifference > 1;
617
- if (
618
- resolved.trial.active
619
- || (trialEnd > 0 && cancelledAt > 0 && cancelledAt === trialEnd)
620
- ) {
621
- resolved.lastPayment.amount = 0;
622
- resolved.lastPayment.date.timestamp = moment(0);
623
- }
624
- resolved.trial.claimed = trialClaimed;
625
-
626
- return resolved;
627
- }
628
-
629
- SubscriptionResolver.prototype.resolve_stripe = function (profile, resource, resolved, options) {
630
- const self = this;
631
-
632
- // Subscription: https://stripe.com/docs/api/subscriptions/object#subscription_object-status
633
- // incomplete
634
- // incomplete_expired
635
- // trialing
636
- // active
637
- // past_due
638
- // canceled
639
- // unpaid
640
-
641
- // Charge: https://stripe.com/docs/api/payment_intents/object#payment_intent_object-status
642
- // requires_payment_method
643
- // requires_confirmation
644
- // requires_action
645
- // processing
646
- // requires_capture
647
- // canceled
648
- // succeeded
649
- // Set status
650
- if (['trialing', 'active'].includes(resource.status)) {
651
- resolved.status = 'active';
652
- } else if (['past_due', 'unpaid'].includes(resource.status)) {
653
- resolved.status = 'suspended';
654
- } else {
655
- resolved.status = 'cancelled';
656
- }
657
-
658
- // Setup preliminary variables
659
- const order = resource.object === 'charge' ? resource : null;
660
- const subscription = resource?.latest_invoice;
661
- const isOrder = !!order;
662
-
663
- // Set resource ID
664
- resolved.resource.id = resource.id;
665
-
666
- // Set start
667
- resolved.start.timestamp = moment(
668
- (
669
- isOrder
670
- // Order
671
- ? (resource?.created ?? 0)
672
-
673
- // Subscription
674
- : (resource?.start_date ?? 0)
675
- ) * 1000
676
- );
677
-
678
- // Set expiration
679
- resolved.expires.timestamp = moment(
680
- (
681
- isOrder
682
- // Order
683
- ? (resource?.created ?? 0)
684
-
685
- // Subscription
686
- : (resource?.current_period_start ?? 0)
687
- ) * 1000
688
- );
689
-
690
- // Set cancelled
691
- if (resolved.status === 'cancelled') {
692
- resolved.cancelled.timestamp = moment(
693
- (
694
- isOrder
695
- // Order
696
- ? (resource?.created ?? 0)
697
-
698
- // Subscription
699
- : (resource?.canceled_at ?? 0)
700
- ) * 1000
701
- )
702
- }
703
-
704
- // Set last payment
705
- // TODO: check if suspended payments are handled correctly when using resource.latest_invoice.amount_paid
706
- if (order) {
707
- resolved.lastPayment.amount = order.amount_captured / 100;
708
- resolved.lastPayment.date.timestamp = moment(
709
- (order.created || 0) * 1000
710
- );
711
- } else if (subscription) {
712
- resolved.lastPayment.amount = subscription.amount_paid / 100;
713
- resolved.lastPayment.date.timestamp = moment(
714
- (subscription.created || 0) * 1000
715
- );
716
- }
717
-
718
- // Get trial
719
- const trialStart = (resource?.trial_start ?? 0) * 1000;
720
- const trialEnd = (resource?.trial_end ?? 0) * 1000;
721
- const trialDaysDifference = Math.abs(moment(trialEnd).diff(moment(trialStart), 'days'));
722
- const trialClaimed = !!trialStart && !!trialEnd && trialDaysDifference > 1;
723
- if (resource.status === 'trialing') {
724
- resolved.trial.active = true;
725
-
726
- // Set expiration
727
- resolved.expires.timestamp = moment(
728
- (
729
- trialEnd
730
- )
731
- )
732
- }
733
- resolved.trial.claimed = trialClaimed;
734
-
735
- // Resolve frequency
736
- const unit = resource?.plan?.interval;
737
- if (unit === 'year') {
738
- resolved.frequency = 'annually';
739
- } else if (unit === 'month') {
740
- resolved.frequency = 'monthly';
741
- } else if (unit === 'week') {
742
- resolved.frequency = 'weekly';
743
- } else if (unit === 'day') {
744
- resolved.frequency = 'daily';
745
- }
746
-
747
- // Set completed
748
- if (resource.object === 'charge') {
749
- resolved.payment.completed = !['requires_payment_method', 'requires_confirmation', 'requires_action', 'processing', 'requires_capture', 'canceled'].includes(resource.status);
750
- } else {
751
- resolved.payment.completed = !['incomplete', 'incomplete_expired'].includes(resource.status);
752
- }
753
-
754
- // Check if refunded
755
- if (resource.object === 'charge') {
756
- resolved.payment.refunded = resource.refunded;
757
- } else {
758
- resolved.payment.refunded = resource?.latest_invoice?.charge?.refunded ?? false;
759
- }
760
-
761
- return resolved;
762
- }
763
-
764
- SubscriptionResolver.prototype.resolve_coinbase = function (profile, resource, resolved, options) {
765
- const self = this;
766
-
767
- // Setup preliminary variables
768
- const isOrder = profile.type === 'order';
769
-
770
- // Set status
771
- resolved.status = 'cancelled';
772
-
773
- // Set resource ID
774
- resolved.resource.id = resource.id;
775
-
776
- // Set start
777
- resolved.start.timestamp = moment(
778
- resource?.created_at ?? 0
779
- );
780
-
781
- // Set expiration
782
- resolved.expires.timestamp = moment(
783
- resource?.created_at ?? 0
784
- );
785
-
786
- // Set cancelled
787
- resolved.cancelled.timestamp = moment(
788
- resource?.created_at ?? 0
789
- )
790
-
791
- // Retrieve last payment
792
- const lastPayment = resource.payments.find(p => p.status === 'CONFIRMED');
793
-
794
- // Set last payment
795
- if (lastPayment) {
796
- resolved.lastPayment.amount = parseFloat(lastPayment.value.local.amount);
797
- resolved.lastPayment.date.timestamp = moment(lastPayment.detected_at);
798
- }
799
-
800
- // Get trial
801
- if (true) {
802
- resolved.trial.active = false;
803
- }
804
- resolved.trial.claimed = false;
805
-
806
- // Resolve frequency
807
- const unit = profile.details.planFrequency;
808
- if (unit) {
809
- resolved.frequency = unit;
810
- } else {
811
- resolved.frequency = 'single';
812
- }
813
-
814
- // Set completed
815
- if (true) {
816
- resolved.payment.completed = !!lastPayment;
817
- }
818
-
819
- // Check if refunded
820
- if (true) {
821
- resolved.payment.refunded = false;
822
- }
823
-
824
- return resolved;
825
- }
826
-
827
- module.exports = SubscriptionResolver;