backend-manager 5.0.86 → 5.0.87

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 (40) hide show
  1. package/CLAUDE.md +53 -1
  2. package/package.json +1 -1
  3. package/src/cli/commands/base-command.js +5 -1
  4. package/src/cli/commands/serve.js +1 -2
  5. package/src/manager/cron/daily/ghostii-auto-publisher.js +10 -19
  6. package/src/manager/events/firestore/payments-webhooks/on-write.js +351 -56
  7. package/src/manager/events/firestore/payments-webhooks/transitions/index.js +148 -0
  8. package/src/manager/events/firestore/payments-webhooks/transitions/one-time/purchase-completed.js +16 -0
  9. package/src/manager/events/firestore/payments-webhooks/transitions/one-time/purchase-failed.js +15 -0
  10. package/src/manager/events/firestore/payments-webhooks/transitions/subscription/cancellation-requested.js +15 -0
  11. package/src/manager/events/firestore/payments-webhooks/transitions/subscription/new-subscription.js +18 -0
  12. package/src/manager/events/firestore/payments-webhooks/transitions/subscription/payment-failed.js +15 -0
  13. package/src/manager/events/firestore/payments-webhooks/transitions/subscription/payment-recovered.js +14 -0
  14. package/src/manager/events/firestore/payments-webhooks/transitions/subscription/plan-changed.js +16 -0
  15. package/src/manager/events/firestore/payments-webhooks/transitions/subscription/subscription-cancelled.js +16 -0
  16. package/src/manager/index.js +26 -36
  17. package/src/manager/libraries/{stripe.js → payment-processors/stripe.js} +57 -2
  18. package/src/manager/libraries/payment-processors/test.js +141 -0
  19. package/src/manager/routes/app/get.js +5 -22
  20. package/src/manager/routes/payments/intent/post.js +38 -23
  21. package/src/manager/routes/payments/intent/processors/stripe.js +96 -48
  22. package/src/manager/routes/payments/intent/processors/test.js +139 -76
  23. package/src/manager/routes/payments/webhook/post.js +14 -5
  24. package/src/manager/routes/payments/webhook/processors/stripe.js +75 -9
  25. package/src/manager/schemas/payments/intent/post.js +1 -1
  26. package/src/test/test-accounts.js +10 -1
  27. package/templates/backend-manager-config.json +16 -4
  28. package/test/events/payments/journey-payments-cancel.js +6 -0
  29. package/test/events/payments/journey-payments-failure.js +114 -0
  30. package/test/events/payments/journey-payments-suspend.js +6 -0
  31. package/test/events/payments/journey-payments-trial.js +12 -0
  32. package/test/events/payments/journey-payments-upgrade.js +17 -0
  33. package/test/fixtures/stripe/checkout-session-completed.json +130 -0
  34. package/test/fixtures/stripe/invoice-payment-failed.json +148 -0
  35. package/test/fixtures/stripe/invoice-subscription-payment-failed.json +28 -0
  36. package/test/helpers/stripe-parse-webhook.js +447 -0
  37. package/test/helpers/stripe-to-unified.js +59 -59
  38. package/test/routes/payments/intent.js +3 -3
  39. package/test/routes/payments/webhook.js +2 -2
  40. package/src/manager/libraries/test.js +0 -27
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Payment transition detection and dispatch
3
+ *
4
+ * Compares subscription state before and after a webhook to detect meaningful
5
+ * transitions (e.g., new subscription, payment failed, cancellation).
6
+ * Dispatches to individual handler files for each transition type.
7
+ */
8
+ const path = require('path');
9
+
10
+ /**
11
+ * Detect what transition occurred based on category and before/after state
12
+ *
13
+ * @param {string} category - 'subscription' or 'one-time'
14
+ * @param {object|null} before - Previous state (null for new users / one-time)
15
+ * @param {object} after - New unified state about to be written
16
+ * @param {string} eventType - Original webhook event type (used for one-time detection)
17
+ * @returns {string|null} Transition name or null if no meaningful change
18
+ */
19
+ function detectTransition(category, before, after, eventType) {
20
+ if (category === 'subscription') {
21
+ return detectSubscriptionTransition(before, after);
22
+ }
23
+
24
+ if (category === 'one-time') {
25
+ return detectOneTimeTransition(eventType);
26
+ }
27
+
28
+ return null;
29
+ }
30
+
31
+ /**
32
+ * Detect subscription state transitions by comparing before and after
33
+ *
34
+ * Checks are ordered by specificity — most specific first to avoid misclassification.
35
+ *
36
+ * @param {object|null} before - Previous users/{uid}.subscription (null/undefined for new users)
37
+ * @param {object} after - New unified subscription
38
+ * @returns {string|null} Transition name
39
+ */
40
+ function detectSubscriptionTransition(before, after) {
41
+ if (!after) {
42
+ return null;
43
+ }
44
+
45
+ const beforeStatus = before?.status;
46
+ const afterStatus = after.status;
47
+
48
+ // 1. new-subscription: basic/null → active paid (handler checks after.trial.claimed for trial info)
49
+ if (isBasicOrNull(before) && afterStatus === 'active' && isPaid(after)) {
50
+ return 'new-subscription';
51
+ }
52
+
53
+ // 2. payment-failed: active → suspended
54
+ if (beforeStatus === 'active' && afterStatus === 'suspended') {
55
+ return 'payment-failed';
56
+ }
57
+
58
+ // 3. payment-recovered: suspended → active
59
+ if (beforeStatus === 'suspended' && afterStatus === 'active') {
60
+ return 'payment-recovered';
61
+ }
62
+
63
+ // 4. cancellation-requested: pending flips from false → true while still active
64
+ if (afterStatus === 'active' && !before?.cancellation?.pending && after.cancellation?.pending) {
65
+ return 'cancellation-requested';
66
+ }
67
+
68
+ // 5. subscription-cancelled: any non-cancelled → cancelled
69
+ if (beforeStatus !== 'cancelled' && afterStatus === 'cancelled') {
70
+ return 'subscription-cancelled';
71
+ }
72
+
73
+ // 6. plan-changed: both active, both paid, different product
74
+ if (
75
+ beforeStatus === 'active'
76
+ && afterStatus === 'active'
77
+ && isPaid(before)
78
+ && isPaid(after)
79
+ && before.product.id !== after.product.id
80
+ ) {
81
+ return 'plan-changed';
82
+ }
83
+
84
+ return null;
85
+ }
86
+
87
+ /**
88
+ * Detect one-time payment transitions from event type
89
+ * Simpler than subscriptions — no before/after comparison needed
90
+ *
91
+ * @param {string} eventType - Webhook event type
92
+ * @returns {string|null} Transition name
93
+ */
94
+ function detectOneTimeTransition(eventType) {
95
+ if (eventType === 'checkout.session.completed') {
96
+ return 'purchase-completed';
97
+ }
98
+
99
+ if (eventType === 'invoice.payment_failed') {
100
+ return 'purchase-failed';
101
+ }
102
+
103
+ return null;
104
+ }
105
+
106
+ /**
107
+ * Dispatch a transition handler (fire-and-forget)
108
+ *
109
+ * @param {string} transitionName - e.g., 'new-subscription', 'payment-failed'
110
+ * @param {string} category - 'subscription' or 'one-time'
111
+ * @param {object} context - Full context passed to the handler
112
+ */
113
+ function dispatch(transitionName, category, context) {
114
+ const { assistant } = context;
115
+
116
+ try {
117
+ const handlerPath = path.join(__dirname, category, `${transitionName}.js`);
118
+ const handler = require(handlerPath);
119
+
120
+ // Fire-and-forget — don't block the main webhook processing
121
+ Promise.resolve(handler(context)).catch((e) => {
122
+ assistant.error(`Transition handler [${category}/${transitionName}] failed: ${e.message}`, e);
123
+ });
124
+ } catch (e) {
125
+ // Handler file doesn't exist or can't be loaded — log but don't fail
126
+ assistant.error(`Transition handler [${category}/${transitionName}] not found: ${e.message}`);
127
+ }
128
+ }
129
+
130
+ // ─── Helpers ───
131
+
132
+ function isBasicOrNull(sub) {
133
+ return !sub || !sub.product || sub.product.id === 'basic';
134
+ }
135
+
136
+ function isPaid(sub) {
137
+ return sub && sub.product && sub.product.id !== 'basic';
138
+ }
139
+
140
+ module.exports = {
141
+ detectTransition,
142
+ detectSubscriptionTransition,
143
+ detectOneTimeTransition,
144
+ dispatch,
145
+ // Exported for testing
146
+ isBasicOrNull,
147
+ isPaid,
148
+ };
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Transition: purchase-completed
3
+ * Triggered when a one-time payment checkout completes (checkout.session.completed with mode=payment)
4
+ *
5
+ * Use cases:
6
+ * - Send purchase receipt/confirmation email
7
+ * - Deliver digital goods or credits
8
+ * - Fire analytics event for purchase
9
+ */
10
+ module.exports = async function ({ before, after, uid, userDoc, admin, assistant, Manager, eventType, eventId }) {
11
+ assistant.log(`Transition [one-time/purchase-completed]: uid=${uid}, resourceId=${after.id}`);
12
+
13
+ // TODO: Send purchase confirmation email
14
+ // TODO: Deliver digital goods
15
+ // TODO: Fire analytics event
16
+ };
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Transition: purchase-failed
3
+ * Triggered when a one-time payment fails (invoice.payment_failed with billing_reason=manual)
4
+ *
5
+ * Use cases:
6
+ * - Send payment failure notification
7
+ * - Include retry link or alternative payment method
8
+ * - Fire analytics event
9
+ */
10
+ module.exports = async function ({ before, after, uid, userDoc, admin, assistant, Manager, eventType, eventId }) {
11
+ assistant.log(`Transition [one-time/purchase-failed]: uid=${uid}, resourceId=${after.id}`);
12
+
13
+ // TODO: Send payment failure email with retry link
14
+ // TODO: Fire analytics event
15
+ };
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Transition: cancellation-requested
3
+ * Triggered when a user requests cancellation at period end (cancellation.pending flips to true)
4
+ *
5
+ * Use cases:
6
+ * - Send cancellation confirmation email with period end date
7
+ * - Include win-back offer or feedback survey link
8
+ * - Fire analytics event for churn intent
9
+ */
10
+ module.exports = async function ({ before, after, uid, userDoc, admin, assistant, Manager, eventType, eventId }) {
11
+ assistant.log(`Transition [subscription/cancellation-requested]: uid=${uid}, product=${after.product.id}, cancelDate=${after.cancellation.date.timestamp}`);
12
+
13
+ // TODO: Send cancellation confirmation email with end date
14
+ // TODO: Fire analytics event
15
+ };
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Transition: new-subscription
3
+ * Triggered when a user subscribes for the first time (basic/null → active paid)
4
+ * Check after.trial.claimed to determine if this is a trial subscription
5
+ *
6
+ * Use cases:
7
+ * - Send order confirmation email with plan details (include trial info if applicable)
8
+ * - Fire analytics event for new subscriber
9
+ * - Update marketing lists
10
+ */
11
+ module.exports = async function ({ before, after, uid, userDoc, admin, assistant, Manager, eventType, eventId }) {
12
+ const isTrial = after.trial?.claimed === true;
13
+
14
+ assistant.log(`Transition [subscription/new-subscription]: uid=${uid}, product=${after.product.id}, frequency=${after.payment.frequency}, trial=${isTrial}`);
15
+
16
+ // TODO: Send order confirmation email (modify content if isTrial)
17
+ // TODO: Fire analytics event
18
+ };
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Transition: payment-failed
3
+ * Triggered when a subscription payment fails (active → suspended)
4
+ *
5
+ * Use cases:
6
+ * - Send payment failure notification email
7
+ * - Include link to update payment method
8
+ * - Fire analytics event for churn risk
9
+ */
10
+ module.exports = async function ({ before, after, uid, userDoc, admin, assistant, Manager, eventType, eventId }) {
11
+ assistant.log(`Transition [subscription/payment-failed]: uid=${uid}, product=${after.product.id}, previousStatus=${before?.status}`);
12
+
13
+ // TODO: Send payment failure email with update-payment link
14
+ // TODO: Fire analytics event
15
+ };
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Transition: payment-recovered
3
+ * Triggered when a suspended subscription is recovered (suspended → active)
4
+ *
5
+ * Use cases:
6
+ * - Send payment recovered confirmation email
7
+ * - Fire analytics event for recovered subscriber
8
+ */
9
+ module.exports = async function ({ before, after, uid, userDoc, admin, assistant, Manager, eventType, eventId }) {
10
+ assistant.log(`Transition [subscription/payment-recovered]: uid=${uid}, product=${after.product.id}`);
11
+
12
+ // TODO: Send payment recovered email
13
+ // TODO: Fire analytics event
14
+ };
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Transition: plan-changed
3
+ * Triggered when a user upgrades or downgrades their plan (product A → product B, both active + paid)
4
+ *
5
+ * Use cases:
6
+ * - Send plan change confirmation email
7
+ * - Include new plan details and what changed
8
+ * - Fire analytics event for upgrade/downgrade
9
+ */
10
+ module.exports = async function ({ before, after, uid, userDoc, admin, assistant, Manager, eventType, eventId }) {
11
+ const direction = after.product.id > before.product.id ? 'upgrade' : 'downgrade';
12
+ assistant.log(`Transition [subscription/plan-changed]: uid=${uid}, ${before.product.id} → ${after.product.id} (${direction})`);
13
+
14
+ // TODO: Send plan change email with new plan details
15
+ // TODO: Fire analytics event
16
+ };
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Transition: subscription-cancelled
3
+ * Triggered when a subscription is fully cancelled (any non-cancelled → cancelled)
4
+ *
5
+ * Use cases:
6
+ * - Send final cancellation email
7
+ * - Include reactivation link or win-back offer
8
+ * - Fire analytics event for churned subscriber
9
+ * - Clean up any subscription-specific resources
10
+ */
11
+ module.exports = async function ({ before, after, uid, userDoc, admin, assistant, Manager, eventType, eventId }) {
12
+ assistant.log(`Transition [subscription/subscription-cancelled]: uid=${uid}, previousProduct=${before?.product?.id}, previousStatus=${before?.status}`);
13
+
14
+ // TODO: Send cancellation email with reactivation link
15
+ // TODO: Fire analytics event
16
+ };
@@ -709,6 +709,14 @@ Manager.prototype.debug = function () {
709
709
  // Setup functions
710
710
  Manager.prototype.setupFunctions = function (exporter, options) {
711
711
  const self = this;
712
+ const resourceZone = options.resourceZone;
713
+
714
+ // Helper to create a function builder with region + runtime options
715
+ function fn(runtimeOptions) {
716
+ return self.libraries.functions
717
+ .runWith(runtimeOptions)
718
+ .region(resourceZone);
719
+ }
712
720
 
713
721
  // Log
714
722
  if (options.log) {
@@ -717,8 +725,7 @@ Manager.prototype.setupFunctions = function (exporter, options) {
717
725
 
718
726
  // Setup functions
719
727
  exporter.bm_api =
720
- self.libraries.functions
721
- .runWith({memory: '256MB', timeoutSeconds: 60 * 5})
728
+ fn({memory: '256MB', timeoutSeconds: 60 * 5})
722
729
  .https.onRequest(async (req, res) => {
723
730
  const route = self.BemRouter(req, res).resolve();
724
731
 
@@ -734,8 +741,7 @@ Manager.prototype.setupFunctions = function (exporter, options) {
734
741
  // Setup legacy functions
735
742
  if (options.setupFunctionsLegacy) {
736
743
  exporter.bm_signUpHandler =
737
- self.libraries.functions
738
- .runWith({memory: '256MB', timeoutSeconds: 60})
744
+ fn({memory: '256MB', timeoutSeconds: 60})
739
745
  .https.onRequest(async (req, res) => {
740
746
  const Module = require(`${_legacy}/actions/sign-up-handler.js`);
741
747
  Module.init(self, { req: req, res: res, });
@@ -750,8 +756,7 @@ Manager.prototype.setupFunctions = function (exporter, options) {
750
756
 
751
757
  // Admin
752
758
  exporter.bm_createPost =
753
- self.libraries.functions
754
- .runWith({memory: '256MB', timeoutSeconds: 60})
759
+ fn({memory: '256MB', timeoutSeconds: 60})
755
760
  .https.onRequest(async (req, res) => {
756
761
  const Module = require(`${_legacy}/admin/create-post.js`);
757
762
  Module.init(self, { req: req, res: res, });
@@ -765,8 +770,7 @@ Manager.prototype.setupFunctions = function (exporter, options) {
765
770
  });
766
771
 
767
772
  exporter.bm_firestoreWrite =
768
- self.libraries.functions
769
- .runWith({memory: '256MB', timeoutSeconds: 60})
773
+ fn({memory: '256MB', timeoutSeconds: 60})
770
774
  .https.onRequest(async (req, res) => {
771
775
  const Module = require(`${_legacy}/admin/firestore-write.js`);
772
776
  Module.init(self, { req: req, res: res, });
@@ -780,8 +784,7 @@ Manager.prototype.setupFunctions = function (exporter, options) {
780
784
  });
781
785
 
782
786
  exporter.bm_getStats =
783
- self.libraries.functions
784
- .runWith({memory: '256MB', timeoutSeconds: 420})
787
+ fn({memory: '256MB', timeoutSeconds: 420})
785
788
  .https.onRequest(async (req, res) => {
786
789
  const Module = require(`${_legacy}/admin/get-stats.js`);
787
790
  Module.init(self, { req: req, res: res, });
@@ -795,8 +798,7 @@ Manager.prototype.setupFunctions = function (exporter, options) {
795
798
  });
796
799
 
797
800
  exporter.bm_sendNotification =
798
- self.libraries.functions
799
- .runWith({memory: '1GB', timeoutSeconds: 420})
801
+ fn({memory: '1GB', timeoutSeconds: 420})
800
802
  .https.onRequest(async (req, res) => {
801
803
  const Module = require(`${_legacy}/admin/send-notification.js`);
802
804
  Module.init(self, { req: req, res: res, });
@@ -810,8 +812,7 @@ Manager.prototype.setupFunctions = function (exporter, options) {
810
812
  });
811
813
 
812
814
  exporter.bm_query =
813
- self.libraries.functions
814
- .runWith({memory: '256MB', timeoutSeconds: 60})
815
+ fn({memory: '256MB', timeoutSeconds: 60})
815
816
  .https.onRequest(async (req, res) => {
816
817
  const Module = require(`${_legacy}/admin/query.js`);
817
818
  Module.init(self, { req: req, res: res, });
@@ -825,8 +826,7 @@ Manager.prototype.setupFunctions = function (exporter, options) {
825
826
  });
826
827
 
827
828
  exporter.bm_createPostHandler =
828
- self.libraries.functions
829
- .runWith({memory: '256MB', timeoutSeconds: 60})
829
+ fn({memory: '256MB', timeoutSeconds: 60})
830
830
  .https.onRequest(async (req, res) => {
831
831
  const Module = require(`${_legacy}/actions/create-post-handler.js`);
832
832
  Module.init(self, { req: req, res: res, });
@@ -840,8 +840,7 @@ Manager.prototype.setupFunctions = function (exporter, options) {
840
840
  });
841
841
 
842
842
  exporter.bm_generateUuid =
843
- self.libraries.functions
844
- .runWith({memory: '256MB', timeoutSeconds: 60})
843
+ fn({memory: '256MB', timeoutSeconds: 60})
845
844
  .https.onRequest(async (req, res) => {
846
845
  const Module = require(`${_legacy}/actions/generate-uuid.js`);
847
846
  Module.init(self, { req: req, res: res, });
@@ -856,8 +855,7 @@ Manager.prototype.setupFunctions = function (exporter, options) {
856
855
 
857
856
  // Test
858
857
  exporter.bm_test_authenticate =
859
- self.libraries.functions
860
- .runWith({memory: '256MB', timeoutSeconds: 60})
858
+ fn({memory: '256MB', timeoutSeconds: 60})
861
859
  .https.onRequest(async (req, res) => {
862
860
  const Module = require(`${_legacy}/test/authenticate.js`);
863
861
  Module.init(self, { req: req, res: res, });
@@ -871,8 +869,7 @@ Manager.prototype.setupFunctions = function (exporter, options) {
871
869
  });
872
870
 
873
871
  exporter.bm_test_webhook =
874
- self.libraries.functions
875
- .runWith({memory: '256MB', timeoutSeconds: 60})
872
+ fn({memory: '256MB', timeoutSeconds: 60})
876
873
  .https.onRequest(async (req, res) => {
877
874
  const Module = require(`${_legacy}/test/webhook.js`);
878
875
  Module.init(self, { req: req, res: res, });
@@ -889,47 +886,40 @@ Manager.prototype.setupFunctions = function (exporter, options) {
889
886
  // Setup identity functions
890
887
  if (options.setupFunctionsIdentity) {
891
888
  exporter.bm_authBeforeCreate =
892
- self.libraries.functions
893
- .runWith({memory: '256MB', timeoutSeconds: 60})
889
+ fn({memory: '256MB', timeoutSeconds: 60})
894
890
  .auth.user()
895
891
  .beforeCreate((user, context) => self.EventMiddleware({ user, context }).run(`${events}/auth/before-create.js`));
896
892
 
897
893
  exporter.bm_authBeforeSignIn =
898
- self.libraries.functions
899
- .runWith({memory: '256MB', timeoutSeconds: 60})
894
+ fn({memory: '256MB', timeoutSeconds: 60})
900
895
  .auth.user()
901
896
  .beforeSignIn((user, context) => self.EventMiddleware({ user, context }).run(`${events}/auth/before-signin.js`));
902
897
  }
903
898
 
904
899
  // Setup events
905
900
  exporter.bm_authOnCreate =
906
- self.libraries.functions
907
- .runWith({memory: '256MB', timeoutSeconds: 60})
901
+ fn({memory: '256MB', timeoutSeconds: 60})
908
902
  .auth.user()
909
903
  .onCreate((user, context) => self.EventMiddleware({ user, context }).run(`${events}/auth/on-create.js`));
910
904
 
911
905
  exporter.bm_authOnDelete =
912
- self.libraries.functions
913
- .runWith({memory: '256MB', timeoutSeconds: 60})
906
+ fn({memory: '256MB', timeoutSeconds: 60})
914
907
  .auth.user()
915
908
  .onDelete((user, context) => self.EventMiddleware({ user, context }).run(`${events}/auth/on-delete.js`));
916
909
 
917
910
  exporter.bm_notificationsOnWrite =
918
- self.libraries.functions
919
- .runWith({memory: '256MB', timeoutSeconds: 60})
911
+ fn({memory: '256MB', timeoutSeconds: 60})
920
912
  .firestore.document('notifications/{token}')
921
913
  .onWrite((change, context) => self.EventMiddleware({ change, context }).run(`${events}/firestore/notifications/on-write.js`));
922
914
 
923
915
  exporter.bm_paymentsWebhookOnWrite =
924
- self.libraries.functions
925
- .runWith({memory: '256MB', timeoutSeconds: 60})
916
+ fn({memory: '256MB', timeoutSeconds: 60})
926
917
  .firestore.document('payments-webhooks/{eventId}')
927
918
  .onWrite((change, context) => self.EventMiddleware({ change, context }).run(`${events}/firestore/payments-webhooks/on-write.js`));
928
919
 
929
920
  // Setup cron jobs
930
921
  exporter.bm_cronDaily =
931
- self.libraries.functions
932
- .runWith({ memory: '256MB', timeoutSeconds: 60 * 5})
922
+ fn({memory: '256MB', timeoutSeconds: 60 * 5})
933
923
  .pubsub.schedule('0 0 * * *')
934
924
  .onRun((context) => self.EventMiddleware({ context }).run(`${cron}/daily.js`));
935
925
  };
@@ -5,7 +5,7 @@ let stripeInstance = null;
5
5
 
6
6
  /**
7
7
  * Stripe shared library
8
- * Provides SDK initialization and unified subscription transformation
8
+ * Provides SDK initialization, resource fetching, and unified transformations
9
9
  */
10
10
  const Stripe = {
11
11
  /**
@@ -27,6 +27,33 @@ const Stripe = {
27
27
  return stripeInstance;
28
28
  },
29
29
 
30
+ /**
31
+ * Fetch the latest resource from Stripe's API
32
+ *
33
+ * @param {string} resourceType - 'subscription' | 'invoice' | 'session'
34
+ * @param {string} resourceId - Stripe resource ID
35
+ * @param {object} rawFallback - Fallback data from webhook payload (unused for Stripe — fetches live)
36
+ * @param {object} context - Additional context (e.g., { admin }) — unused for Stripe
37
+ * @returns {object} Full Stripe resource object
38
+ */
39
+ async fetchResource(resourceType, resourceId, rawFallback, context) {
40
+ const stripe = this.init();
41
+
42
+ if (resourceType === 'subscription') {
43
+ return stripe.subscriptions.retrieve(resourceId);
44
+ }
45
+
46
+ if (resourceType === 'invoice') {
47
+ return stripe.invoices.retrieve(resourceId);
48
+ }
49
+
50
+ if (resourceType === 'session') {
51
+ return stripe.checkout.sessions.retrieve(resourceId);
52
+ }
53
+
54
+ throw new Error(`Unknown resource type: ${resourceType}`);
55
+ },
56
+
30
57
  /**
31
58
  * Transform a raw Stripe subscription object into the unified subscription shape
32
59
  * This produces the exact same object stored in users/{uid}.subscription
@@ -38,7 +65,7 @@ const Stripe = {
38
65
  * @param {string} options.eventId - ID of the webhook event (e.g., 'evt_xxx')
39
66
  * @returns {object} Unified subscription object
40
67
  */
41
- toUnified(rawSubscription, options) {
68
+ toUnifiedSubscription(rawSubscription, options) {
42
69
  options = options || {};
43
70
  const config = options.config || {};
44
71
 
@@ -94,6 +121,34 @@ const Stripe = {
94
121
  },
95
122
  };
96
123
  },
124
+
125
+ /**
126
+ * Transform a raw Stripe one-time payment resource into a unified shape
127
+ * Stub for now — will be fully implemented when one-time purchases are built out
128
+ *
129
+ * @param {object} rawResource - Raw Stripe resource (session, invoice, etc.)
130
+ * @param {object} options
131
+ * @returns {object} Unified one-time payment object
132
+ */
133
+ toUnifiedOneTime(rawResource, options) {
134
+ options = options || {};
135
+
136
+ const now = powertools.timestamp(new Date(), { output: 'string' });
137
+ const nowUNIX = powertools.timestamp(now, { output: 'unix' });
138
+
139
+ return {
140
+ id: rawResource.id || null,
141
+ processor: 'stripe',
142
+ status: rawResource.status || 'unknown',
143
+ raw: rawResource,
144
+ metadata: {
145
+ created: {
146
+ timestamp: now,
147
+ timestampUNIX: nowUNIX,
148
+ },
149
+ },
150
+ };
151
+ },
97
152
  };
98
153
 
99
154
  /**