backend-manager 5.0.104 → 5.0.106
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/CHANGELOG.md +31 -0
- package/CLAUDE.md +114 -24
- package/README.md +42 -1
- package/TODO-PAYMENT-v2.md +5 -2
- package/package.json +1 -1
- package/src/cli/commands/deploy.js +2 -4
- package/src/cli/commands/emulator.js +30 -1
- package/src/cli/commands/test.js +33 -2
- package/src/manager/events/firestore/payments-webhooks/on-write.js +17 -3
- package/src/manager/events/firestore/payments-webhooks/transitions/index.js +6 -0
- package/src/manager/index.js +5 -2
- package/src/manager/libraries/payment/processors/paypal.js +588 -0
- package/src/manager/libraries/{payment-processors → payment/processors}/stripe.js +87 -18
- package/src/manager/libraries/{payment-processors → payment/processors}/test.js +15 -8
- package/src/manager/routes/payments/cancel/processors/paypal.js +30 -0
- package/src/manager/routes/payments/cancel/processors/stripe.js +1 -1
- package/src/manager/routes/payments/cancel/processors/test.js +4 -6
- package/src/manager/routes/payments/intent/post.js +3 -3
- package/src/manager/routes/payments/intent/processors/paypal.js +150 -0
- package/src/manager/routes/payments/intent/processors/stripe.js +3 -5
- package/src/manager/routes/payments/intent/processors/test.js +12 -13
- package/src/manager/routes/payments/portal/processors/paypal.js +24 -0
- package/src/manager/routes/payments/portal/processors/stripe.js +1 -1
- package/src/manager/routes/payments/refund/post.js +85 -0
- package/src/manager/routes/payments/refund/processors/paypal.js +117 -0
- package/src/manager/routes/payments/refund/processors/stripe.js +103 -0
- package/src/manager/routes/payments/refund/processors/test.js +98 -0
- package/src/manager/routes/payments/trial-eligibility/get.js +29 -0
- package/src/manager/routes/payments/webhook/processors/paypal.js +137 -0
- package/src/manager/schemas/payments/refund/post.js +18 -0
- package/src/manager/schemas/payments/trial-eligibility/get.js +5 -0
- package/src/test/test-accounts.js +46 -0
- package/templates/backend-manager-config.json +20 -24
- package/templates/firestore.rules +0 -2
- package/test/events/payments/journey-payments-cancel.js +3 -3
- package/test/events/payments/journey-payments-failure.js +1 -1
- package/test/events/payments/journey-payments-one-time.js +1 -1
- package/test/events/payments/journey-payments-plan-change.js +4 -4
- package/test/events/payments/journey-payments-suspend.js +3 -3
- package/test/events/payments/journey-payments-trial.js +2 -2
- package/test/fixtures/paypal/order-approved.json +62 -0
- package/test/fixtures/paypal/order-completed.json +110 -0
- package/test/fixtures/paypal/subscription-active.json +76 -0
- package/test/fixtures/paypal/subscription-cancelled.json +50 -0
- package/test/fixtures/paypal/subscription-suspended.json +65 -0
- package/test/helpers/payment/paypal/parse-webhook.js +539 -0
- package/test/helpers/payment/paypal/to-unified-one-time.js +382 -0
- package/test/helpers/payment/paypal/to-unified-subscription.js +820 -0
- package/test/helpers/{stripe-parse-webhook.js → payment/stripe/parse-webhook.js} +4 -4
- package/test/helpers/{stripe-to-unified-one-time.js → payment/stripe/to-unified-one-time.js} +8 -6
- package/test/helpers/{stripe-to-unified.js → payment/stripe/to-unified-subscription.js} +40 -33
- package/test/routes/payments/refund.js +174 -0
- package/test/routes/payments/trial-eligibility.js +71 -0
- package/src/manager/libraries/payment-processors/resolve-price-id.js +0 -19
- /package/src/manager/libraries/{payment-processors → payment}/order-id.js +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,37 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|
|
14
14
|
- `Fixed` for any bug fixes.
|
|
15
15
|
- `Security` in case of vulnerabilities.
|
|
16
16
|
|
|
17
|
+
# [5.0.104] - 2026-03-02
|
|
18
|
+
### Added
|
|
19
|
+
- `POST /payments/cancel`: cancels subscription at period end via processor abstraction (Stripe sets `cancel_at_period_end=true`; test processor writes webhook directly into the Firestore pipeline).
|
|
20
|
+
- `POST /payments/portal`: creates Stripe Billing Portal session with cancellation disabled (users must use the cancel endpoint).
|
|
21
|
+
- Payment transition pipeline: `transitions/index.js` detects all subscription state changes (new-subscription, payment-failed, payment-recovered, cancellation-requested, subscription-cancelled, plan-changed) and one-time transitions (purchase-completed, purchase-failed). Handlers fire-and-forget, send transactional emails.
|
|
22
|
+
- Payment analytics: `analytics.js` tracks GA4 payment events for all transitions (non-blocking, skipped in tests).
|
|
23
|
+
- Shared payment processor libraries: `payment/processors/stripe.js` (toUnifiedSubscription, toUnifiedOneTime, resolveCustomer, resolvePriceId, fetchResource), `payment/processors/paypal.js`, `payment/processors/test.js`, `payment/order-id.js`.
|
|
24
|
+
- `Email` library (`libraries/email.js`): shared transactional email via SendGrid, used by transition handlers and admin routes.
|
|
25
|
+
- `infer-contact.js` library: infers user name from payment processor data, auto-fills on first purchase.
|
|
26
|
+
- `routes/user/data-request/` (get/post/delete): GDPR data request endpoints.
|
|
27
|
+
- `cron/daily/data-requests.js`: daily cron to process pending GDPR data requests.
|
|
28
|
+
- CLI commands: `auth` (get/list/delete/set-claims), `firestore` (get/set/query/delete), `firebase-init`, `emulator` (renamed from `emulators`).
|
|
29
|
+
- `setup-tests/firestore-indexes-required.js`: validates required Firestore indexes exist before tests run.
|
|
30
|
+
- Comprehensive payment test suite: journey tests for one-time purchase, one-time failure, payment failure, plan change, cancel endpoint; route validation tests for cancel and portal; unit tests for `toUnifiedOneTime()`, `stripe-parse-webhook`, `infer-contact`, `email`; real Stripe CLI fixtures.
|
|
31
|
+
- Dedicated isolated test accounts for every mutating payment test (no shared state between tests).
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
- `admin/email/post.js`, `general/email/post.js`: refactored to delegate to shared Email library (~400 lines removed from each).
|
|
35
|
+
- `marketing/contact/post.js`, `api/general/add-marketing-contact.js`: delegate to infer-contact + marketing library.
|
|
36
|
+
- `user/signup/post.js`: rewritten with new middleware pattern.
|
|
37
|
+
- `auth/on-create.js`: simplified, inline logic moved to middleware.
|
|
38
|
+
- `api/admin/send-email.js`: removed `ensureUnique` and SendGrid contact name lookup (handled by Email library).
|
|
39
|
+
- All admin routes: middleware pattern cleanup.
|
|
40
|
+
- `config.payment.products` now supports `type: 'one-time'` products with `prices.once` key.
|
|
41
|
+
- Test runner: improved discovery, filtering, and output formatting.
|
|
42
|
+
|
|
43
|
+
### Removed
|
|
44
|
+
- `src/manager/libraries/stripe.js`, `src/manager/libraries/test.js`: replaced by `payment/processors/` shared libs.
|
|
45
|
+
- `REFACTOR-BEM-API.md`, `REFACTOR-MIDDLEWARE.md`, `REFACTOR-PAYMENT.md`: work completed, files deleted.
|
|
46
|
+
- `bin/bem`: replaced by `bin/backend-manager`.
|
|
47
|
+
|
|
17
48
|
# [5.0.84] - 2026-02-19
|
|
18
49
|
### BREAKING
|
|
19
50
|
- Moved `config.products` to `config.payment.products`. All product lookups now use `config.payment.products`.
|
package/CLAUDE.md
CHANGED
|
@@ -59,11 +59,12 @@ src/
|
|
|
59
59
|
utilities.js # Batch operations
|
|
60
60
|
metadata.js # Timestamps/tags
|
|
61
61
|
libraries/
|
|
62
|
-
payment
|
|
63
|
-
stripe.js # Stripe SDK init, fetchResource, toUnified*
|
|
64
|
-
test.js # Test processor (delegates to Stripe shapes)
|
|
62
|
+
payment/ # Shared payment utilities
|
|
65
63
|
order-id.js # Order ID generation (XXXX-XXXX-XXXX)
|
|
66
|
-
|
|
64
|
+
processors/ # Payment processor libraries
|
|
65
|
+
stripe.js # Stripe SDK init, fetchResource, toUnified*, resolvePriceId
|
|
66
|
+
paypal.js # PayPal fetchResource, toUnified* (custom_id parsing)
|
|
67
|
+
test.js # Test processor (delegates to Stripe shapes)
|
|
67
68
|
functions/core/ # Built-in functions
|
|
68
69
|
actions/
|
|
69
70
|
api.js # Main bm_api handler
|
|
@@ -88,12 +89,28 @@ src/
|
|
|
88
89
|
post.js # Intent creation orchestrator
|
|
89
90
|
processors/ # Per-processor intent creators
|
|
90
91
|
stripe.js # Stripe Checkout Session creation
|
|
92
|
+
paypal.js # PayPal subscription + one-time order creation
|
|
91
93
|
test.js # Test processor (auto-fires webhooks)
|
|
92
94
|
webhook/ # POST /payments/webhook
|
|
93
95
|
post.js # Webhook ingestion + Firestore write
|
|
94
96
|
processors/ # Per-processor webhook parsers
|
|
95
97
|
stripe.js # Stripe event parsing + categorization
|
|
98
|
+
paypal.js # PayPal event parsing + categorization
|
|
96
99
|
test.js # Test processor (delegates to Stripe)
|
|
100
|
+
cancel/ # POST /payments/cancel
|
|
101
|
+
processors/
|
|
102
|
+
stripe.js # Stripe cancel_at_period_end
|
|
103
|
+
paypal.js # PayPal subscription cancel
|
|
104
|
+
test.js # Test cancel (writes webhook doc)
|
|
105
|
+
refund/ # POST /payments/refund
|
|
106
|
+
processors/
|
|
107
|
+
stripe.js # Stripe refund + immediate cancel
|
|
108
|
+
paypal.js # PayPal refund + cancel
|
|
109
|
+
test.js # Test refund (writes webhook doc)
|
|
110
|
+
portal/ # POST /payments/portal
|
|
111
|
+
processors/
|
|
112
|
+
stripe.js # Stripe billing portal URL
|
|
113
|
+
paypal.js # PayPal management URL
|
|
97
114
|
schemas/ # Built-in schemas
|
|
98
115
|
cli/
|
|
99
116
|
index.js # CLI entry point
|
|
@@ -421,6 +438,15 @@ npx bm test # Terminal 2 - runs tests
|
|
|
421
438
|
npx bm test
|
|
422
439
|
```
|
|
423
440
|
|
|
441
|
+
### Log Files
|
|
442
|
+
Both `npx bm emulator` and `npx bm test` automatically save all output to log files in the project directory while still streaming to the console:
|
|
443
|
+
- **`emulator.log`** — Full emulator output (Firebase emulator + Cloud Functions logs)
|
|
444
|
+
- **`test.log`** — Test runner output (when running against an existing emulator)
|
|
445
|
+
|
|
446
|
+
When `npx bm test` starts its own emulator, logs go to `emulator.log` (since it delegates to the emulator command). When running against an already-running emulator, logs go to `test.log`.
|
|
447
|
+
|
|
448
|
+
These files are overwritten on each run and are gitignored (`*.log`). Use them to search for errors, debug webhook pipelines, or review full function output after a test run.
|
|
449
|
+
|
|
424
450
|
### Filtering Tests
|
|
425
451
|
```bash
|
|
426
452
|
npx bm test rules/ # Run rules tests (both BEM and project)
|
|
@@ -772,9 +798,24 @@ The payment system follows a linear pipeline: **Intent → Webhook → On-Write
|
|
|
772
798
|
|
|
773
799
|
4. **Transitions** (fire-and-forget): Handler files run asynchronously after detection. Failures never block webhook processing. Skipped during tests unless `TEST_EXTENDED_MODE` is set.
|
|
774
800
|
|
|
801
|
+
### 3-Layer Architecture
|
|
802
|
+
|
|
803
|
+
The payment system is cleanly separated into three independent layers:
|
|
804
|
+
|
|
805
|
+
| Layer | Purpose | Tests |
|
|
806
|
+
|-------|---------|-------|
|
|
807
|
+
| **Processor input** (Stripe, PayPal, Test) | Parse raw webhooks + transform to unified shape | Helper tests per processor (`payment/stripe/to-unified-subscription.js`, `payment/paypal/to-unified-one-time.js`, etc.) |
|
|
808
|
+
| **Unified pipeline** (processor-agnostic) | Transition detection, Firestore writes, analytics | Journey tests (`journey-payments-*.js`) |
|
|
809
|
+
| **Transition handlers** (fire-and-forget) | Emails, notifications, side effects | Skipped during tests unless `TEST_EXTENDED_MODE` |
|
|
810
|
+
|
|
811
|
+
Each processor transforms its raw data into the **same unified shape**. Once data enters the pipeline, the code doesn't know or care which processor it came from. This means:
|
|
812
|
+
- Adding a new processor = implement the processor interface (below). The pipeline handles the rest.
|
|
813
|
+
- Journey tests use the `test` processor but exercise the full unified pipeline end-to-end.
|
|
814
|
+
- Processor-specific tests only need to verify correct transformation to the unified shape.
|
|
815
|
+
|
|
775
816
|
### Processor Interface
|
|
776
817
|
|
|
777
|
-
Each processor implements
|
|
818
|
+
Each processor implements three modules:
|
|
778
819
|
|
|
779
820
|
**Intent processor** (`routes/payments/intent/processors/{processor}.js`):
|
|
780
821
|
```javascript
|
|
@@ -793,25 +834,73 @@ module.exports = {
|
|
|
793
834
|
};
|
|
794
835
|
```
|
|
795
836
|
|
|
796
|
-
**
|
|
837
|
+
**Cancel processor** (`routes/payments/cancel/processors/{processor}.js`):
|
|
838
|
+
```javascript
|
|
839
|
+
module.exports = {
|
|
840
|
+
async cancelAtPeriodEnd({ resourceId, uid, subscription, assistant }) { /* cancel at end of period */ },
|
|
841
|
+
};
|
|
842
|
+
```
|
|
843
|
+
|
|
844
|
+
**Refund processor** (`routes/payments/refund/processors/{processor}.js`):
|
|
845
|
+
```javascript
|
|
846
|
+
module.exports = {
|
|
847
|
+
async processRefund({ resourceId, uid, subscription, assistant }) {
|
|
848
|
+
return { amount, currency, full };
|
|
849
|
+
},
|
|
850
|
+
};
|
|
851
|
+
```
|
|
852
|
+
|
|
853
|
+
**Portal processor** (`routes/payments/portal/processors/{processor}.js`):
|
|
854
|
+
```javascript
|
|
855
|
+
module.exports = {
|
|
856
|
+
async createPortalSession({ resourceId, uid, returnUrl, assistant }) {
|
|
857
|
+
return { url };
|
|
858
|
+
},
|
|
859
|
+
};
|
|
860
|
+
```
|
|
861
|
+
|
|
862
|
+
**Shared library** (`libraries/payment/processors/{processor}.js`):
|
|
797
863
|
```javascript
|
|
798
864
|
module.exports = {
|
|
799
865
|
init() { /* return SDK instance */ },
|
|
800
866
|
async fetchResource(resourceType, resourceId, rawFallback, context) { /* return resource */ },
|
|
867
|
+
getOrderId(resource) { /* return orderId string or null */ },
|
|
801
868
|
toUnifiedSubscription(rawSubscription, options) { /* return unified object */ },
|
|
802
869
|
toUnifiedOneTime(rawResource, options) { /* return unified object */ },
|
|
803
870
|
};
|
|
804
871
|
```
|
|
805
872
|
|
|
873
|
+
### Product Resolution
|
|
874
|
+
|
|
875
|
+
Products are resolved differently per processor, but always end up matching a product in `config.payment.products`:
|
|
876
|
+
|
|
877
|
+
| Processor | Resolution chain | Stable ID |
|
|
878
|
+
|-----------|-----------------|-----------|
|
|
879
|
+
| **Stripe** | `sub.items.data[0].price.product` or `raw.plan.product` → match `product.stripe.productId` or `legacyProductIds` | `prod_xxx` |
|
|
880
|
+
| **PayPal** | `sub → plan_id → plan → product_id` → match `product.paypal.productId` | PayPal catalog product ID |
|
|
881
|
+
| **Test** | Uses `product.stripe.productId` in Stripe-shaped data | Same as Stripe |
|
|
882
|
+
|
|
883
|
+
Falls back to `{ id: 'basic' }` if no match found.
|
|
884
|
+
|
|
885
|
+
### Processor-Specific Details
|
|
886
|
+
|
|
887
|
+
**Stripe:** Uses `metadata.uid` and `metadata.orderId` on subscriptions for UID/order resolution.
|
|
888
|
+
|
|
889
|
+
**PayPal:** Uses `custom_id` field on subscriptions with format `uid:{uid},orderId:{orderId}`. Product resolution fetches the plan from the subscription, then gets `product_id` from the plan. Plans are scoped by `product_id` query param to avoid cross-brand matches on shared PayPal accounts.
|
|
890
|
+
|
|
806
891
|
### Product Configuration
|
|
807
892
|
|
|
808
893
|
Products are defined in `backend-manager-config.json` under `payment.products`:
|
|
809
894
|
|
|
810
895
|
```javascript
|
|
811
896
|
payment: {
|
|
897
|
+
processors: {
|
|
898
|
+
stripe: { publishableKey: 'pk_live_...' },
|
|
899
|
+
paypal: { clientId: 'ARvf...' },
|
|
900
|
+
},
|
|
812
901
|
products: [
|
|
813
902
|
{
|
|
814
|
-
id: 'basic', // Free tier (no prices)
|
|
903
|
+
id: 'basic', // Free tier (no prices, no processor keys)
|
|
815
904
|
name: 'Basic',
|
|
816
905
|
type: 'subscription',
|
|
817
906
|
limits: { requests: 100 },
|
|
@@ -821,30 +910,31 @@ payment: {
|
|
|
821
910
|
name: 'Premium',
|
|
822
911
|
type: 'subscription',
|
|
823
912
|
limits: { requests: 1000 },
|
|
824
|
-
trial: { days: 14 },
|
|
825
|
-
prices: {
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
},
|
|
913
|
+
trial: { days: 14 },
|
|
914
|
+
prices: { monthly: 4.99, annually: 49.99 }, // Flat numbers only
|
|
915
|
+
stripe: { productId: 'prod_xxx', legacyProductIds: ['prod_OLD'] },
|
|
916
|
+
paypal: { productId: 'PROD-abc123' },
|
|
829
917
|
},
|
|
830
918
|
{
|
|
831
919
|
id: 'credits-100', // One-time purchase
|
|
832
920
|
name: '100 Credits',
|
|
833
921
|
type: 'one-time',
|
|
834
|
-
prices: {
|
|
835
|
-
|
|
836
|
-
},
|
|
922
|
+
prices: { once: 9.99 },
|
|
923
|
+
stripe: { productId: 'prod_yyy' },
|
|
924
|
+
paypal: { productId: null },
|
|
837
925
|
},
|
|
838
926
|
],
|
|
839
927
|
}
|
|
840
928
|
```
|
|
841
929
|
|
|
842
930
|
Key rules:
|
|
843
|
-
- `
|
|
844
|
-
-
|
|
845
|
-
-
|
|
846
|
-
-
|
|
847
|
-
- `
|
|
931
|
+
- `prices` contains **flat numbers only** — no processor-specific IDs
|
|
932
|
+
- Processor IDs live at the product level: `stripe: { productId }`, `paypal: { productId }`
|
|
933
|
+
- `stripe.productId` is stable — never changes even when prices change
|
|
934
|
+
- `stripe.legacyProductIds` maps old pre-migration Stripe products to this product
|
|
935
|
+
- Price IDs (Stripe `price_xxx`, PayPal plan IDs) are **resolved at runtime** by matching amount + interval against active prices on the processor's product
|
|
936
|
+
- `basic` product has no `prices` and no processor keys — it's the free tier
|
|
937
|
+
- `archived: true` stops offering a product to new subscribers while keeping it resolvable for existing ones
|
|
848
938
|
|
|
849
939
|
### Firestore Collections
|
|
850
940
|
|
|
@@ -900,10 +990,10 @@ The `test` processor generates Stripe-shaped data and auto-fires webhooks to the
|
|
|
900
990
|
| Webhook processing (on-write) | `src/manager/events/firestore/payments-webhooks/on-write.js` |
|
|
901
991
|
| Payment analytics | `src/manager/events/firestore/payments-webhooks/analytics.js` |
|
|
902
992
|
| Transition detection | `src/manager/events/firestore/payments-webhooks/transitions/index.js` |
|
|
903
|
-
| Payment processor libraries | `src/manager/libraries/payment
|
|
904
|
-
| Stripe library | `src/manager/libraries/payment
|
|
905
|
-
|
|
|
906
|
-
| Order ID generator | `src/manager/libraries/payment
|
|
993
|
+
| Payment processor libraries | `src/manager/libraries/payment/processors/` |
|
|
994
|
+
| Stripe library | `src/manager/libraries/payment/processors/stripe.js` |
|
|
995
|
+
| PayPal library | `src/manager/libraries/payment/processors/paypal.js` |
|
|
996
|
+
| Order ID generator | `src/manager/libraries/payment/order-id.js` |
|
|
907
997
|
| Test accounts | `src/test/test-accounts.js` |
|
|
908
998
|
|
|
909
999
|
## Environment Detection
|
package/README.md
CHANGED
|
@@ -793,6 +793,14 @@ npx bm test project:rules/ # Only project's rules tests
|
|
|
793
793
|
npx bm test user/ admin/ # Multiple paths
|
|
794
794
|
```
|
|
795
795
|
|
|
796
|
+
### Log Files
|
|
797
|
+
|
|
798
|
+
Both `npx bm emulator` and `npx bm test` automatically save all output to log files in the project directory:
|
|
799
|
+
- **`emulator.log`** — Full emulator + Cloud Functions output
|
|
800
|
+
- **`test.log`** — Test runner output (when running against an existing emulator)
|
|
801
|
+
|
|
802
|
+
Logs are overwritten on each run. Use them to debug failing tests or review function output.
|
|
803
|
+
|
|
796
804
|
### Test Locations
|
|
797
805
|
|
|
798
806
|
- **BEM core tests:** `test/`
|
|
@@ -871,7 +879,7 @@ See `CLAUDE.md` for complete test API documentation.
|
|
|
871
879
|
|
|
872
880
|
## Subscription System
|
|
873
881
|
|
|
874
|
-
BEM includes a built-in payment/subscription system with Stripe
|
|
882
|
+
BEM includes a built-in payment/subscription system with Stripe and PayPal integration.
|
|
875
883
|
|
|
876
884
|
### Subscription Statuses
|
|
877
885
|
|
|
@@ -894,6 +902,39 @@ BEM includes a built-in payment/subscription system with Stripe integration (ext
|
|
|
894
902
|
| `incomplete_expired` | `cancelled` | Expired before completion |
|
|
895
903
|
| `active` + `cancel_at_period_end` | `active` | `cancellation.pending = true` |
|
|
896
904
|
|
|
905
|
+
### PayPal Status Mapping
|
|
906
|
+
|
|
907
|
+
| PayPal Status | `subscription.status` | Notes |
|
|
908
|
+
|---|---|---|
|
|
909
|
+
| `ACTIVE` | `active` | Normal active subscription |
|
|
910
|
+
| `SUSPENDED` | `suspended` | Payment failed or manually suspended |
|
|
911
|
+
| `CANCELLED` | `cancelled` | Subscription terminated |
|
|
912
|
+
| `EXPIRED` | `cancelled` | Billing cycles completed |
|
|
913
|
+
|
|
914
|
+
### Product Configuration
|
|
915
|
+
|
|
916
|
+
Products are defined in `config.payment.products` with flat prices and per-processor IDs:
|
|
917
|
+
|
|
918
|
+
```javascript
|
|
919
|
+
payment: {
|
|
920
|
+
products: [
|
|
921
|
+
{ id: 'basic', name: 'Basic', type: 'subscription', limits: { requests: 10 } },
|
|
922
|
+
{
|
|
923
|
+
id: 'plus', name: 'Plus', type: 'subscription',
|
|
924
|
+
limits: { requests: 100 }, trial: { days: 14 },
|
|
925
|
+
prices: { monthly: 28, annually: 276 },
|
|
926
|
+
stripe: { productId: 'prod_xxx' },
|
|
927
|
+
paypal: { productId: 'PROD-abc123' },
|
|
928
|
+
},
|
|
929
|
+
{
|
|
930
|
+
id: 'boost', name: 'Boost Pack', type: 'one-time',
|
|
931
|
+
prices: { once: 9.99 },
|
|
932
|
+
stripe: { productId: 'prod_yyy' },
|
|
933
|
+
},
|
|
934
|
+
],
|
|
935
|
+
}
|
|
936
|
+
```
|
|
937
|
+
|
|
897
938
|
### Unified Subscription Object
|
|
898
939
|
|
|
899
940
|
The same subscription shape is stored in `users/{uid}.subscription` and `payments-orders/{orderId}.subscription`:
|
package/TODO-PAYMENT-v2.md
CHANGED
|
@@ -24,9 +24,12 @@ Note: * for managing and cancelling, they should be under a sinlge button/dropdo
|
|
|
24
24
|
* we need to make it hard to cancel, buried in some info, and make it more prominent to manage it, since we want to encourage users to manage their subscription rather than cancelling it.
|
|
25
25
|
|
|
26
26
|
payments/refund
|
|
27
|
-
* takes a subscription id and requests a refund
|
|
27
|
+
* takes a subscription id and requests a refund to the payment processor
|
|
28
28
|
* we can only refund the most recent payment and we can only refund if the subscription is cancelled.
|
|
29
|
-
* we can only refund if the most recent payment in FULL was made less than 7 days ago, otherwise we can only refund a prorated amount
|
|
29
|
+
* we can only refund if the most recent payment in FULL was made less than 7 days ago, otherwise we can only refund a prorated amount based on how much time is left
|
|
30
|
+
* i want the subscription to be immediately revoked upon refund request
|
|
31
|
+
* generally, we dont modify the subscription DURING the http endpoint (such as payment intent, or cancel), rather we WAIT FOR THE WEBHOOOK. Can we do that here???
|
|
32
|
+
|
|
30
33
|
payments/reactivate
|
|
31
34
|
* takes a subscription id and reactivates a cancelled subscription. this can only be done if the subscription is still within its billing period, otherwise the user would have to create a new subscription.
|
|
32
35
|
payments/upgrade
|
package/package.json
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
const BaseCommand = require('./base-command');
|
|
2
|
-
const chalk = require('chalk');
|
|
3
2
|
const powertools = require('node-powertools');
|
|
4
3
|
|
|
5
4
|
class DeployCommand extends BaseCommand {
|
|
@@ -7,9 +6,8 @@ class DeployCommand extends BaseCommand {
|
|
|
7
6
|
const self = this.main;
|
|
8
7
|
|
|
9
8
|
// Quick check that not using local packages
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
if (hasLocal) {
|
|
9
|
+
const allDeps = JSON.stringify(self.packageJSON.dependencies || {}) + JSON.stringify(self.packageJSON.devDependencies || {});
|
|
10
|
+
if (allDeps.includes('file:')) {
|
|
13
11
|
this.logError(`Please remove local packages before deploying!`);
|
|
14
12
|
return;
|
|
15
13
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const BaseCommand = require('./base-command');
|
|
2
2
|
const path = require('path');
|
|
3
|
+
const fs = require('fs');
|
|
3
4
|
const chalk = require('chalk');
|
|
4
5
|
const jetpack = require('fs-jetpack');
|
|
5
6
|
const JSON5 = require('json5');
|
|
@@ -60,9 +61,37 @@ class EmulatorCommand extends BaseCommand {
|
|
|
60
61
|
// Use double quotes for command wrapper since the command may contain single quotes (JSON strings)
|
|
61
62
|
const emulatorCommand = `BEM_TESTING=true firebase emulators:exec --only functions,firestore,auth,database,hosting,pubsub --ui "${command}"`;
|
|
62
63
|
|
|
64
|
+
// Set up log file in the project directory
|
|
65
|
+
const logPath = path.join(projectDir, 'emulator.log');
|
|
66
|
+
const logStream = fs.createWriteStream(logPath, { flags: 'w' });
|
|
67
|
+
const stripAnsi = (str) => str.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, '');
|
|
68
|
+
|
|
69
|
+
this.log(chalk.gray(` Logs saving to: ${logPath}\n`));
|
|
70
|
+
|
|
63
71
|
await powertools.execute(emulatorCommand, {
|
|
64
|
-
log:
|
|
72
|
+
log: false,
|
|
65
73
|
cwd: projectDir,
|
|
74
|
+
config: {
|
|
75
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
76
|
+
env: { ...process.env, FORCE_COLOR: '1' },
|
|
77
|
+
},
|
|
78
|
+
}, (child) => {
|
|
79
|
+
// Tee stdout to both console and log file (strip ANSI codes for clean log)
|
|
80
|
+
child.stdout.on('data', (data) => {
|
|
81
|
+
process.stdout.write(data);
|
|
82
|
+
logStream.write(stripAnsi(data.toString()));
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Tee stderr to both console and log file (strip ANSI codes for clean log)
|
|
86
|
+
child.stderr.on('data', (data) => {
|
|
87
|
+
process.stderr.write(data);
|
|
88
|
+
logStream.write(stripAnsi(data.toString()));
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Clean up log stream when child exits
|
|
92
|
+
child.on('close', () => {
|
|
93
|
+
logStream.end();
|
|
94
|
+
});
|
|
66
95
|
});
|
|
67
96
|
}
|
|
68
97
|
|
package/src/cli/commands/test.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const BaseCommand = require('./base-command');
|
|
2
2
|
const path = require('path');
|
|
3
|
+
const fs = require('fs');
|
|
3
4
|
const chalk = require('chalk');
|
|
4
5
|
const jetpack = require('fs-jetpack');
|
|
5
6
|
const JSON5 = require('json5');
|
|
@@ -176,15 +177,45 @@ class TestCommand extends BaseCommand {
|
|
|
176
177
|
* Run tests directly (emulator already running)
|
|
177
178
|
*/
|
|
178
179
|
async runTestsDirectly(testCommand, functionsDir, emulatorPorts) {
|
|
180
|
+
const projectDir = this.main.firebaseProjectPath;
|
|
181
|
+
|
|
179
182
|
this.log(chalk.gray(` Hosting: http://127.0.0.1:${emulatorPorts.hosting}`));
|
|
180
183
|
this.log(chalk.gray(` Firestore: 127.0.0.1:${emulatorPorts.firestore}`));
|
|
181
184
|
this.log(chalk.gray(` Auth: 127.0.0.1:${emulatorPorts.auth}`));
|
|
182
|
-
this.log(chalk.gray(` UI: http://127.0.0.1:${emulatorPorts.ui}
|
|
185
|
+
this.log(chalk.gray(` UI: http://127.0.0.1:${emulatorPorts.ui}`));
|
|
186
|
+
|
|
187
|
+
// Set up log file in the project directory
|
|
188
|
+
const logPath = path.join(projectDir, 'test.log');
|
|
189
|
+
const logStream = fs.createWriteStream(logPath, { flags: 'w' });
|
|
190
|
+
const stripAnsi = (str) => str.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, '');
|
|
191
|
+
|
|
192
|
+
this.log(chalk.gray(` Logs saving to: ${logPath}\n`));
|
|
183
193
|
|
|
184
194
|
try {
|
|
185
195
|
await powertools.execute(testCommand, {
|
|
186
|
-
log:
|
|
196
|
+
log: false,
|
|
187
197
|
cwd: functionsDir,
|
|
198
|
+
config: {
|
|
199
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
200
|
+
env: { ...process.env, FORCE_COLOR: '1' },
|
|
201
|
+
},
|
|
202
|
+
}, (child) => {
|
|
203
|
+
// Tee stdout to both console and log file (strip ANSI codes for clean log)
|
|
204
|
+
child.stdout.on('data', (data) => {
|
|
205
|
+
process.stdout.write(data);
|
|
206
|
+
logStream.write(stripAnsi(data.toString()));
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Tee stderr to both console and log file (strip ANSI codes for clean log)
|
|
210
|
+
child.stderr.on('data', (data) => {
|
|
211
|
+
process.stderr.write(data);
|
|
212
|
+
logStream.write(stripAnsi(data.toString()));
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Clean up log stream when child exits
|
|
216
|
+
child.on('close', () => {
|
|
217
|
+
logStream.end();
|
|
218
|
+
});
|
|
188
219
|
});
|
|
189
220
|
} catch (error) {
|
|
190
221
|
process.exit(1);
|
|
@@ -55,7 +55,7 @@ module.exports = async ({ assistant, change, context }) => {
|
|
|
55
55
|
// Load the shared library for this processor
|
|
56
56
|
let library;
|
|
57
57
|
try {
|
|
58
|
-
library = require(`../../../libraries/payment
|
|
58
|
+
library = require(`../../../libraries/payment/processors/${processor}.js`);
|
|
59
59
|
} catch (e) {
|
|
60
60
|
throw new Error(`Unknown processor library: ${processor}`);
|
|
61
61
|
}
|
|
@@ -72,8 +72,8 @@ module.exports = async ({ assistant, change, context }) => {
|
|
|
72
72
|
const nowUNIX = powertools.timestamp(now, { output: 'unix' });
|
|
73
73
|
const webhookReceivedUNIX = dataAfter.metadata?.received?.timestampUNIX || nowUNIX;
|
|
74
74
|
|
|
75
|
-
// Extract orderId from resource
|
|
76
|
-
const orderId = resource
|
|
75
|
+
// Extract orderId from resource (processor-agnostic)
|
|
76
|
+
const orderId = library.getOrderId(resource);
|
|
77
77
|
|
|
78
78
|
// Process the payment event (subscription or one-time)
|
|
79
79
|
if (category !== 'subscription' && category !== 'one-time') {
|
|
@@ -243,6 +243,20 @@ function extractCustomerName(resource, resourceType) {
|
|
|
243
243
|
fullName = resource.customer_name;
|
|
244
244
|
}
|
|
245
245
|
|
|
246
|
+
// PayPal orders have payer.name
|
|
247
|
+
if (resourceType === 'order') {
|
|
248
|
+
const givenName = resource.payer?.name?.given_name;
|
|
249
|
+
const surname = resource.payer?.name?.surname;
|
|
250
|
+
|
|
251
|
+
if (givenName) {
|
|
252
|
+
const { capitalize } = require('../../../libraries/infer-contact.js');
|
|
253
|
+
return {
|
|
254
|
+
first: capitalize(givenName) || null,
|
|
255
|
+
last: capitalize(surname) || null,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
246
260
|
// Subscriptions only have customer ID, no name
|
|
247
261
|
|
|
248
262
|
if (!fullName) {
|
|
@@ -92,6 +92,7 @@ function detectSubscriptionTransition(before, after) {
|
|
|
92
92
|
* @returns {string|null} Transition name
|
|
93
93
|
*/
|
|
94
94
|
function detectOneTimeTransition(eventType) {
|
|
95
|
+
// Stripe
|
|
95
96
|
if (eventType === 'checkout.session.completed') {
|
|
96
97
|
return 'purchase-completed';
|
|
97
98
|
}
|
|
@@ -100,6 +101,11 @@ function detectOneTimeTransition(eventType) {
|
|
|
100
101
|
return 'purchase-failed';
|
|
101
102
|
}
|
|
102
103
|
|
|
104
|
+
// PayPal
|
|
105
|
+
if (eventType === 'CHECKOUT.ORDER.APPROVED') {
|
|
106
|
+
return 'purchase-completed';
|
|
107
|
+
}
|
|
108
|
+
|
|
103
109
|
return null;
|
|
104
110
|
}
|
|
105
111
|
|
package/src/manager/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Libraries
|
|
2
2
|
const path = require('path');
|
|
3
|
-
const {
|
|
3
|
+
const { mergeWith, isArray } = require('lodash');
|
|
4
4
|
const jetpack = require('fs-jetpack');
|
|
5
5
|
const JSON5 = require('json5');
|
|
6
6
|
const EventEmitter = require('events');
|
|
@@ -129,10 +129,13 @@ Manager.prototype.init = function (exporter, options) {
|
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
// Load config
|
|
132
|
-
|
|
132
|
+
// Use mergeWith to replace arrays instead of merging by index
|
|
133
|
+
// (lodash merge merges arrays positionally, causing template defaults to bleed into project values)
|
|
134
|
+
self.config = mergeWith(
|
|
133
135
|
{},
|
|
134
136
|
requireJSON5(BEM_CONFIG_TEMPLATE_PATH, true),
|
|
135
137
|
requireJSON5(self.project.backendManagerConfigPath, true),
|
|
138
|
+
(_objValue, srcValue) => isArray(srcValue) ? srcValue : undefined,
|
|
136
139
|
);
|
|
137
140
|
|
|
138
141
|
// Resolve legacy paths
|