backend-manager 5.0.203 → 5.1.0
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 +56 -0
- package/CLAUDE.md +43 -1501
- package/TODO-CHARGEBLAST.md +32 -0
- package/TODO-email-auth.md +14 -0
- package/docs/admin-post-route.md +24 -0
- package/docs/ai-library.md +23 -0
- package/docs/architecture.md +31 -0
- package/docs/auth-hooks.md +74 -0
- package/docs/cli-firestore-auth.md +59 -0
- package/docs/cli-logs.md +67 -0
- package/docs/code-patterns.md +67 -0
- package/docs/common-operations.md +64 -0
- package/docs/directory-structure.md +119 -0
- package/docs/environment-detection.md +7 -0
- package/docs/file-naming.md +11 -0
- package/docs/marketing-campaigns.md +244 -0
- package/docs/marketing-fields.md +25 -0
- package/docs/mcp.md +95 -0
- package/docs/payment-system.md +325 -0
- package/docs/response-headers.md +7 -0
- package/docs/routes.md +126 -0
- package/docs/sanitization.md +61 -0
- package/docs/schemas.md +39 -0
- package/docs/stripe-webhook-forwarding.md +18 -0
- package/docs/testing.md +129 -0
- package/docs/usage-rate-limiting.md +67 -0
- package/package.json +8 -4
- package/src/defaults/CHANGELOG.md +15 -0
- package/src/defaults/CLAUDE.md +8 -4
- package/src/defaults/docs/README.md +17 -0
- package/src/defaults/test/README.md +33 -0
- package/src/manager/events/cron/daily/marketing-newsletter-generate.js +48 -8
- package/src/manager/functions/core/actions/api/admin/create-post.js +3 -27
- package/src/manager/helpers/utilities.js +21 -0
- package/src/manager/index.js +1 -1
- package/src/manager/libraries/ai/index.js +162 -0
- package/src/manager/libraries/ai/providers/anthropic.js +193 -0
- package/src/manager/libraries/ai/providers/claude-code.js +206 -0
- package/src/manager/libraries/ai/providers/openai.js +934 -0
- package/src/manager/libraries/disposable-domains.json +2 -0
- package/src/manager/libraries/email/generators/lib/filter.js +179 -0
- package/src/manager/libraries/email/generators/lib/image-host.js +231 -0
- package/src/manager/libraries/email/generators/lib/mjml-template.js +83 -0
- package/src/manager/libraries/email/generators/lib/structure.js +278 -0
- package/src/manager/libraries/email/generators/lib/svg-illustrator.js +184 -0
- package/src/manager/libraries/email/generators/lib/templates/classic-schema.js +63 -0
- package/src/manager/libraries/email/generators/lib/templates/clean.js +82 -0
- package/src/manager/libraries/email/generators/lib/templates/editorial-helpers.js +100 -0
- package/src/manager/libraries/email/generators/lib/templates/editorial.js +317 -0
- package/src/manager/libraries/email/generators/lib/templates/field-report-helpers.js +138 -0
- package/src/manager/libraries/email/generators/lib/templates/field-report.js +497 -0
- package/src/manager/libraries/email/generators/lib/templates/index.js +28 -0
- package/src/manager/libraries/email/generators/lib/templates/shared.js +534 -0
- package/src/manager/libraries/email/generators/newsletter.js +377 -95
- package/src/manager/libraries/email/marketing/index.js +5 -2
- package/src/manager/libraries/email/providers/beehiiv.js +7 -3
- package/src/manager/libraries/openai.js +13 -932
- package/src/manager/routes/admin/post/deduplicate-image-alts.js +52 -0
- package/src/manager/routes/admin/post/post.js +10 -17
- package/templates/_.env +4 -0
- package/templates/_.gitignore +1 -0
- package/templates/backend-manager-config.json +48 -4
- package/test/helpers/slugify.js +394 -0
- package/test/marketing/fixtures/clean.json +31 -0
- package/test/marketing/fixtures/editorial.json +31 -0
- package/test/marketing/fixtures/field-report.json +54 -0
- package/test/marketing/newsletter-generate.js +731 -0
- package/test/marketing/newsletter-templates.js +512 -0
- package/test/routes/admin/deduplicate-image-alts.js +190 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
# Payment System
|
|
2
|
+
|
|
3
|
+
This document covers the full payment system: pipeline architecture, subscription model + statuses, transition handlers, processor interface, product configuration, and the test processor.
|
|
4
|
+
|
|
5
|
+
## Pipeline
|
|
6
|
+
|
|
7
|
+
The payment system follows a linear pipeline: **Intent → Webhook → On-Write → Transition**.
|
|
8
|
+
|
|
9
|
+
1. **Intent** (`POST /payments/intent`): Client requests a payment session. BEM validates the product, generates an order ID (`XXXX-XXXX-XXXX`), and delegates to the processor module (e.g., Stripe creates a Checkout Session). Saves to `payments-intents/{orderId}`.
|
|
10
|
+
|
|
11
|
+
2. **Webhook** (`POST /payments/webhook?processor=X&key=Y`): Processor sends event data. BEM parses and categorizes the event (`subscription` or `one-time`), extracts the UID, and saves to `payments-webhooks/{eventId}` with `status: 'pending'`.
|
|
12
|
+
|
|
13
|
+
3. **On-Write** (Firestore trigger on `payments-webhooks/{eventId}`): Fetches the latest resource from the processor API (not stale webhook data), transforms it into a unified object, detects state transitions, dispatches handlers, tracks analytics, and writes to `users/{uid}.subscription` (subscriptions) and `payments-orders/{orderId}`.
|
|
14
|
+
|
|
15
|
+
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.
|
|
16
|
+
|
|
17
|
+
## 3-Layer Architecture
|
|
18
|
+
|
|
19
|
+
The payment system is cleanly separated into three independent layers:
|
|
20
|
+
|
|
21
|
+
| Layer | Purpose | Tests |
|
|
22
|
+
|-------|---------|-------|
|
|
23
|
+
| **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.) |
|
|
24
|
+
| **Unified pipeline** (processor-agnostic) | Transition detection, Firestore writes, analytics | Journey tests (`journey-payments-*.js`) |
|
|
25
|
+
| **Transition handlers** (fire-and-forget) | Emails, notifications, side effects | Skipped during tests unless `TEST_EXTENDED_MODE` |
|
|
26
|
+
|
|
27
|
+
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:
|
|
28
|
+
- Adding a new processor = implement the processor interface (below). The pipeline handles the rest.
|
|
29
|
+
- Journey tests use the `test` processor but exercise the full unified pipeline end-to-end.
|
|
30
|
+
- Processor-specific tests only need to verify correct transformation to the unified shape.
|
|
31
|
+
|
|
32
|
+
## Subscription Statuses
|
|
33
|
+
|
|
34
|
+
| Status | Meaning | User can delete account? |
|
|
35
|
+
|--------|---------|--------------------------|
|
|
36
|
+
| `active` | Subscription is current and valid (includes trialing) | No (unless `product.id === 'basic'`) |
|
|
37
|
+
| `suspended` | Payment failed (Stripe: `past_due`, `unpaid`) | No |
|
|
38
|
+
| `cancelled` | Subscription terminated (Stripe: `canceled`, `incomplete`, `incomplete_expired`) | Yes |
|
|
39
|
+
|
|
40
|
+
### Stripe Status Mapping
|
|
41
|
+
|
|
42
|
+
| Stripe Status | `subscription.status` | Notes |
|
|
43
|
+
|---|---|---|
|
|
44
|
+
| `active` | `active` | Normal active subscription |
|
|
45
|
+
| `trialing` | `active` | `trial.claimed = true` |
|
|
46
|
+
| `past_due` | `suspended` | Payment failed, retrying |
|
|
47
|
+
| `unpaid` | `suspended` | Payment failed |
|
|
48
|
+
| `canceled` | `cancelled` | Subscription terminated |
|
|
49
|
+
| `incomplete` | `cancelled` | Never completed initial payment |
|
|
50
|
+
| `incomplete_expired` | `cancelled` | Expired before completion |
|
|
51
|
+
| `active` + `cancel_at_period_end` | `active` | `cancellation.pending = true` |
|
|
52
|
+
|
|
53
|
+
## Unified Subscription Object (`users/{uid}.subscription`)
|
|
54
|
+
|
|
55
|
+
```javascript
|
|
56
|
+
subscription: {
|
|
57
|
+
product: {
|
|
58
|
+
id: 'basic', // product ID from config ('basic', 'premium', etc.)
|
|
59
|
+
name: 'Basic', // display name from config
|
|
60
|
+
},
|
|
61
|
+
status: 'active', // 'active' | 'suspended' | 'cancelled'
|
|
62
|
+
expires: { timestamp, timestampUNIX },
|
|
63
|
+
trial: {
|
|
64
|
+
claimed: false, // has user EVER used a trial
|
|
65
|
+
expires: { timestamp, timestampUNIX },
|
|
66
|
+
},
|
|
67
|
+
cancellation: {
|
|
68
|
+
pending: false, // true = cancel at period end
|
|
69
|
+
date: { timestamp, timestampUNIX },
|
|
70
|
+
},
|
|
71
|
+
payment: {
|
|
72
|
+
processor: null, // 'stripe' | 'paypal' | etc.
|
|
73
|
+
orderId: null, // BEM order ID (e.g., '1234-5678-9012')
|
|
74
|
+
resourceId: null, // provider subscription ID (e.g., 'sub_xxx')
|
|
75
|
+
frequency: null, // 'monthly' | 'annually' | 'weekly' | 'daily'
|
|
76
|
+
price: 0, // resolved from config (number, e.g., 4.99)
|
|
77
|
+
startDate: { timestamp, timestampUNIX },
|
|
78
|
+
updatedBy: {
|
|
79
|
+
event: { name: null, id: null },
|
|
80
|
+
date: { timestamp, timestampUNIX },
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Access Check Patterns
|
|
87
|
+
|
|
88
|
+
```javascript
|
|
89
|
+
// Is premium (paid)?
|
|
90
|
+
user.subscription.status === 'active' && user.subscription.product.id !== 'basic'
|
|
91
|
+
|
|
92
|
+
// Is on trial?
|
|
93
|
+
user.subscription.trial.claimed && user.subscription.status === 'active'
|
|
94
|
+
|
|
95
|
+
// Has pending cancellation?
|
|
96
|
+
user.subscription.cancellation.pending === true
|
|
97
|
+
|
|
98
|
+
// Payment failed?
|
|
99
|
+
user.subscription.status === 'suspended'
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## resolveSubscription(account)
|
|
103
|
+
|
|
104
|
+
`User.resolveSubscription(account)` is a static method on the User helper that derives calculated subscription fields from raw account data. It returns only fields that require derivation logic — raw data (product.id, status, trial, cancellation) lives on the account object directly.
|
|
105
|
+
|
|
106
|
+
```javascript
|
|
107
|
+
const User = require('backend-manager/src/manager/helpers/user');
|
|
108
|
+
|
|
109
|
+
const resolved = User.resolveSubscription(account);
|
|
110
|
+
// Returns: { plan, active, trialing, cancelling }
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
| Field | Type | Description |
|
|
114
|
+
|-------|------|-------------|
|
|
115
|
+
| `plan` | `string` | Effective plan ID the user has access to RIGHT NOW (`'basic'` if cancelled/suspended) |
|
|
116
|
+
| `active` | `boolean` | User has active access (active, trialing, or cancelling) |
|
|
117
|
+
| `trialing` | `boolean` | In an active trial (status `'active'` + `trial.claimed` + unexpired `trial.expires`) |
|
|
118
|
+
| `cancelling` | `boolean` | Cancellation pending (status `'active'` + `cancellation.pending` + NOT trialing) |
|
|
119
|
+
|
|
120
|
+
Accepts either a raw Firestore account object or a resolved `User` instance (checks both `account.subscription` and `account.properties.subscription`).
|
|
121
|
+
|
|
122
|
+
**Unified with web-manager**: The same function exists as `auth.resolveSubscription(account)` in web-manager (`modules/auth.js`) with identical logic and return shape.
|
|
123
|
+
|
|
124
|
+
**Use this instead of manual access checks** — it centralizes all the derivation logic in one place:
|
|
125
|
+
|
|
126
|
+
```javascript
|
|
127
|
+
// ✅ PREFERRED — use resolveSubscription
|
|
128
|
+
const resolved = User.resolveSubscription(user);
|
|
129
|
+
if (resolved.active) { /* has access */ }
|
|
130
|
+
|
|
131
|
+
// ❌ AVOID — manual checks that duplicate logic
|
|
132
|
+
if (user.subscription.status === 'active' && user.subscription.product.id !== 'basic') { /* ... */ }
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Transition Handlers
|
|
136
|
+
|
|
137
|
+
When a webhook changes a subscription or processes a one-time payment, BEM detects the state transition and dispatches to a handler file. Handlers are fire-and-forget (non-blocking) — they run after the transition is detected but before or during the Firestore writes. Handler failures never block webhook processing.
|
|
138
|
+
|
|
139
|
+
Handlers are skipped during tests unless `TEST_EXTENDED_MODE` is set.
|
|
140
|
+
|
|
141
|
+
### Transition Detection
|
|
142
|
+
|
|
143
|
+
The `transitions/index.js` module compares the **before** state (current `users/{uid}.subscription`) with the **after** state (new unified subscription) to detect what changed.
|
|
144
|
+
|
|
145
|
+
### Subscription Transitions
|
|
146
|
+
|
|
147
|
+
| Transition | Before → After | File |
|
|
148
|
+
|---|---|---|
|
|
149
|
+
| `new-subscription` | basic/null → active paid | `transitions/subscription/new-subscription.js` |
|
|
150
|
+
| `payment-failed` | active → suspended | `transitions/subscription/payment-failed.js` |
|
|
151
|
+
| `payment-recovered` | suspended → active | `transitions/subscription/payment-recovered.js` |
|
|
152
|
+
| `cancellation-requested` | pending=false → pending=true | `transitions/subscription/cancellation-requested.js` |
|
|
153
|
+
| `subscription-cancelled` | non-cancelled → cancelled | `transitions/subscription/subscription-cancelled.js` |
|
|
154
|
+
| `plan-changed` | active product A → active product B | `transitions/subscription/plan-changed.js` |
|
|
155
|
+
|
|
156
|
+
Note: Trials are NOT a separate transition. The `new-subscription` handler checks `after.trial.claimed` to determine if the subscription started with a trial.
|
|
157
|
+
|
|
158
|
+
### One-Time Transitions
|
|
159
|
+
|
|
160
|
+
| Transition | Event Type | File |
|
|
161
|
+
|---|---|---|
|
|
162
|
+
| `purchase-completed` | `checkout.session.completed` | `transitions/one-time/purchase-completed.js` |
|
|
163
|
+
| `purchase-failed` | `invoice.payment_failed` | `transitions/one-time/purchase-failed.js` |
|
|
164
|
+
|
|
165
|
+
### Handler Interface
|
|
166
|
+
|
|
167
|
+
All handlers are in `src/manager/events/firestore/payments-webhooks/transitions/` and export a single async function:
|
|
168
|
+
|
|
169
|
+
```javascript
|
|
170
|
+
module.exports = async function ({ before, after, uid, userDoc, admin, assistant, Manager, eventType, eventId }) {
|
|
171
|
+
// before: previous subscription state (null for new/one-time)
|
|
172
|
+
// after: new unified state (subscription or one-time)
|
|
173
|
+
// userDoc: full user document data
|
|
174
|
+
// eventType: original webhook event type (e.g., 'customer.subscription.updated')
|
|
175
|
+
// eventId: webhook event ID
|
|
176
|
+
};
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Creating a New Transition Handler
|
|
180
|
+
|
|
181
|
+
1. Add detection logic in `transitions/index.js` (in priority order)
|
|
182
|
+
2. Create handler file in `transitions/{category}/{name}.js`
|
|
183
|
+
3. Handler receives full context — use `assistant.log()` for logging, `Manager.project.apiUrl` for API calls
|
|
184
|
+
|
|
185
|
+
## Processor Interface
|
|
186
|
+
|
|
187
|
+
Each processor implements three modules:
|
|
188
|
+
|
|
189
|
+
**Intent processor** (`routes/payments/intent/processors/{processor}.js`):
|
|
190
|
+
|
|
191
|
+
```javascript
|
|
192
|
+
module.exports = {
|
|
193
|
+
async createIntent({ uid, orderId, product, productId, frequency, trial, confirmationUrl, cancelUrl, Manager, assistant }) {
|
|
194
|
+
return { id, url, raw };
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
**Webhook processor** (`routes/payments/webhook/processors/{processor}.js`):
|
|
200
|
+
|
|
201
|
+
```javascript
|
|
202
|
+
module.exports = {
|
|
203
|
+
isSupported(eventType) { return boolean; },
|
|
204
|
+
parseWebhook(req) { return { eventId, eventType, category, resourceType, resourceId, raw, uid }; },
|
|
205
|
+
};
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
**Cancel processor** (`routes/payments/cancel/processors/{processor}.js`):
|
|
209
|
+
|
|
210
|
+
```javascript
|
|
211
|
+
module.exports = {
|
|
212
|
+
async cancelAtPeriodEnd({ resourceId, uid, subscription, assistant }) { /* cancel at end of period */ },
|
|
213
|
+
};
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
**Refund processor** (`routes/payments/refund/processors/{processor}.js`):
|
|
217
|
+
|
|
218
|
+
```javascript
|
|
219
|
+
module.exports = {
|
|
220
|
+
async processRefund({ resourceId, uid, subscription, assistant }) {
|
|
221
|
+
return { amount, currency, full };
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
**Portal processor** (`routes/payments/portal/processors/{processor}.js`):
|
|
227
|
+
|
|
228
|
+
```javascript
|
|
229
|
+
module.exports = {
|
|
230
|
+
async createPortalSession({ resourceId, uid, returnUrl, assistant }) {
|
|
231
|
+
return { url };
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
**Shared library** (`libraries/payment/processors/{processor}.js`):
|
|
237
|
+
|
|
238
|
+
```javascript
|
|
239
|
+
module.exports = {
|
|
240
|
+
init() { /* return SDK instance */ },
|
|
241
|
+
async fetchResource(resourceType, resourceId, rawFallback, context) { /* return resource */ },
|
|
242
|
+
getOrderId(resource) { /* return orderId string or null */ },
|
|
243
|
+
toUnifiedSubscription(rawSubscription, options) { /* return unified object */ },
|
|
244
|
+
toUnifiedOneTime(rawResource, options) { /* return unified object */ },
|
|
245
|
+
};
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## Product Resolution
|
|
249
|
+
|
|
250
|
+
Products are resolved differently per processor, but always end up matching a product in `config.payment.products`:
|
|
251
|
+
|
|
252
|
+
| Processor | Resolution chain | Stable ID |
|
|
253
|
+
|-----------|-----------------|-----------|
|
|
254
|
+
| **Stripe** | `sub.items.data[0].price.product` or `raw.plan.product` → match `product.stripe.productId` or `legacyProductIds` | `prod_xxx` |
|
|
255
|
+
| **PayPal** | `sub → plan_id → plan → product_id` → match `product.paypal.productId` | PayPal catalog product ID |
|
|
256
|
+
| **Test** | Uses `product.stripe.productId` in Stripe-shaped data | Same as Stripe |
|
|
257
|
+
|
|
258
|
+
Falls back to `{ id: 'basic' }` if no match found.
|
|
259
|
+
|
|
260
|
+
## Processor-Specific Details
|
|
261
|
+
|
|
262
|
+
**Stripe:** Uses `metadata.uid` and `metadata.orderId` on subscriptions for UID/order resolution.
|
|
263
|
+
|
|
264
|
+
**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.
|
|
265
|
+
|
|
266
|
+
## Product Configuration
|
|
267
|
+
|
|
268
|
+
Products are defined in `backend-manager-config.json` under `payment.products`:
|
|
269
|
+
|
|
270
|
+
```javascript
|
|
271
|
+
payment: {
|
|
272
|
+
processors: {
|
|
273
|
+
stripe: { publishableKey: 'pk_live_...' },
|
|
274
|
+
paypal: { clientId: 'ARvf...' },
|
|
275
|
+
},
|
|
276
|
+
products: [
|
|
277
|
+
{
|
|
278
|
+
id: 'basic', // Free tier (no prices, no processor keys)
|
|
279
|
+
name: 'Basic',
|
|
280
|
+
type: 'subscription',
|
|
281
|
+
limits: { requests: 100 },
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
id: 'premium', // Paid subscription
|
|
285
|
+
name: 'Premium',
|
|
286
|
+
type: 'subscription',
|
|
287
|
+
limits: { requests: 1000 },
|
|
288
|
+
trial: { days: 14 },
|
|
289
|
+
prices: { monthly: 4.99, annually: 49.99 }, // Flat numbers; also supports 'weekly' and 'daily'
|
|
290
|
+
stripe: { productId: 'prod_xxx', legacyProductIds: ['prod_OLD'] },
|
|
291
|
+
paypal: { productId: 'PROD-abc123' },
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
id: 'credits-100', // One-time purchase
|
|
295
|
+
name: '100 Credits',
|
|
296
|
+
type: 'one-time',
|
|
297
|
+
prices: { once: 9.99 },
|
|
298
|
+
stripe: { productId: 'prod_yyy' },
|
|
299
|
+
paypal: { productId: null },
|
|
300
|
+
},
|
|
301
|
+
],
|
|
302
|
+
}
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
Key rules:
|
|
306
|
+
- `prices` contains **flat numbers only** — no processor-specific IDs
|
|
307
|
+
- Processor IDs live at the product level: `stripe: { productId }`, `paypal: { productId }`
|
|
308
|
+
- `stripe.productId` is stable — never changes even when prices change
|
|
309
|
+
- `stripe.legacyProductIds` maps old pre-migration Stripe products to this product
|
|
310
|
+
- Price IDs (Stripe `price_xxx`, PayPal plan IDs) are **resolved at runtime** by matching amount + interval against active prices on the processor's product
|
|
311
|
+
- `basic` product has no `prices` and no processor keys — it's the free tier
|
|
312
|
+
- `archived: true` stops offering a product to new subscribers while keeping it resolvable for existing ones
|
|
313
|
+
|
|
314
|
+
## Firestore Collections
|
|
315
|
+
|
|
316
|
+
| Collection | Key | Purpose |
|
|
317
|
+
|---|---|---|
|
|
318
|
+
| `payments-intents/{orderId}` | Order ID | Intent metadata (processor, product, status) |
|
|
319
|
+
| `payments-webhooks/{eventId}` | Processor event ID | Webhook processing state + transition result |
|
|
320
|
+
| `payments-orders/{orderId}` | Order ID | Unified order data (single source of truth for orders) |
|
|
321
|
+
| `users/{uid}.subscription` | User UID | Current subscription state (subscriptions only) |
|
|
322
|
+
|
|
323
|
+
## Test Processor
|
|
324
|
+
|
|
325
|
+
The `test` processor generates Stripe-shaped data and auto-fires webhooks to the local server. Only available in non-production environments. Use `processor: 'test'` in intent requests during testing. The test webhook processor delegates to Stripe's parser since it generates Stripe-shaped payloads.
|
package/docs/routes.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# Creating Routes, API Commands, Events, Cron Jobs
|
|
2
|
+
|
|
3
|
+
Recipes for building consumer-side routes plus BEM-side API commands, event handlers, and cron jobs. See also [docs/schemas.md](schemas.md) for schema definitions, [docs/auth-hooks.md](auth-hooks.md) for auth lifecycle hooks, and [docs/common-operations.md](common-operations.md) for inside-the-handler patterns.
|
|
4
|
+
|
|
5
|
+
## New API Command
|
|
6
|
+
|
|
7
|
+
Create `src/manager/functions/core/actions/api/{category}/{action}.js`:
|
|
8
|
+
|
|
9
|
+
```javascript
|
|
10
|
+
function Module() {}
|
|
11
|
+
|
|
12
|
+
Module.prototype.main = function () {
|
|
13
|
+
const self = this;
|
|
14
|
+
const Manager = self.Manager;
|
|
15
|
+
const Api = self.Api;
|
|
16
|
+
const assistant = self.assistant;
|
|
17
|
+
const payload = self.payload;
|
|
18
|
+
|
|
19
|
+
return new Promise(async function(resolve, reject) {
|
|
20
|
+
// Validate input
|
|
21
|
+
if (!payload.data.payload.requiredField) {
|
|
22
|
+
return reject(assistant.errorify('Missing required field', { code: 400 }));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Business logic here
|
|
26
|
+
const result = { success: true };
|
|
27
|
+
|
|
28
|
+
// Log and return
|
|
29
|
+
assistant.log('Action completed', result);
|
|
30
|
+
return resolve({ data: result });
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
module.exports = Module;
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## New Route (Consumer Project)
|
|
38
|
+
|
|
39
|
+
Create `routes/{name}/index.js`:
|
|
40
|
+
|
|
41
|
+
```javascript
|
|
42
|
+
function Route() {}
|
|
43
|
+
|
|
44
|
+
Route.prototype.main = async function (assistant) {
|
|
45
|
+
const Manager = assistant.Manager;
|
|
46
|
+
const usage = assistant.usage;
|
|
47
|
+
const user = assistant.usage.user;
|
|
48
|
+
const analytics = assistant.analytics;
|
|
49
|
+
const settings = assistant.settings;
|
|
50
|
+
|
|
51
|
+
// Check authentication if needed
|
|
52
|
+
if (!user.authenticated) {
|
|
53
|
+
return assistant.respond('Authentication required', { code: 401 });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Track usage
|
|
57
|
+
await usage.validate('requests');
|
|
58
|
+
usage.increment('requests');
|
|
59
|
+
await usage.update();
|
|
60
|
+
|
|
61
|
+
// Send response
|
|
62
|
+
assistant.respond({ success: true, data: settings });
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
module.exports = Route;
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## New Event Handler
|
|
69
|
+
|
|
70
|
+
Create `src/manager/functions/core/events/{type}/{event}.js`:
|
|
71
|
+
|
|
72
|
+
```javascript
|
|
73
|
+
function Module() {}
|
|
74
|
+
|
|
75
|
+
Module.prototype.init = function (Manager, payload) {
|
|
76
|
+
const self = this;
|
|
77
|
+
self.Manager = Manager;
|
|
78
|
+
self.assistant = Manager.Assistant();
|
|
79
|
+
self.libraries = Manager.libraries;
|
|
80
|
+
self.user = payload.user;
|
|
81
|
+
self.context = payload.context;
|
|
82
|
+
return self;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
Module.prototype.main = function () {
|
|
86
|
+
const self = this;
|
|
87
|
+
const Manager = self.Manager;
|
|
88
|
+
const assistant = self.assistant;
|
|
89
|
+
|
|
90
|
+
return new Promise(async function(resolve, reject) {
|
|
91
|
+
const { admin } = self.libraries;
|
|
92
|
+
|
|
93
|
+
assistant.log('Event triggered', self.user);
|
|
94
|
+
|
|
95
|
+
// Event logic here
|
|
96
|
+
|
|
97
|
+
return resolve(self);
|
|
98
|
+
});
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
module.exports = Module;
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## New Cron Job (Consumer Project)
|
|
105
|
+
|
|
106
|
+
Create `hooks/cron/daily/{job}.js`:
|
|
107
|
+
|
|
108
|
+
```javascript
|
|
109
|
+
function Job() {}
|
|
110
|
+
|
|
111
|
+
Job.prototype.main = function () {
|
|
112
|
+
const self = this;
|
|
113
|
+
const Manager = self.Manager;
|
|
114
|
+
const assistant = self.assistant;
|
|
115
|
+
|
|
116
|
+
return new Promise(async function(resolve, reject) {
|
|
117
|
+
assistant.log('Running daily job...');
|
|
118
|
+
|
|
119
|
+
// Job logic here
|
|
120
|
+
|
|
121
|
+
return resolve();
|
|
122
|
+
});
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
module.exports = Job;
|
|
126
|
+
```
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Sanitization (XSS Prevention)
|
|
2
|
+
|
|
3
|
+
BEM automatically sanitizes all incoming request data — stripping HTML tags and trimming whitespace from every string field. This happens in the middleware pipeline before route handlers execute, so **routes receive clean data by default**.
|
|
4
|
+
|
|
5
|
+
## How It Works
|
|
6
|
+
|
|
7
|
+
1. **Schema fields**: Sanitized per-field during the middleware pipeline. Fields can opt out with `sanitize: false` in the schema.
|
|
8
|
+
2. **Non-schema fields** (when `setupSettings: false` or `includeNonSchemaSettings: true`): All strings are sanitized with no opt-out.
|
|
9
|
+
3. The middleware uses `Manager.Utilities().sanitize()` under the hood.
|
|
10
|
+
|
|
11
|
+
## Schema Opt-Out
|
|
12
|
+
|
|
13
|
+
For fields that legitimately need HTML (rich text, email templates, etc.), set `sanitize: false` in the schema:
|
|
14
|
+
|
|
15
|
+
```javascript
|
|
16
|
+
// This field will NOT be sanitized — raw HTML is preserved
|
|
17
|
+
htmlContent: {
|
|
18
|
+
types: ['string'],
|
|
19
|
+
default: '',
|
|
20
|
+
sanitize: false,
|
|
21
|
+
},
|
|
22
|
+
// This field IS sanitized (default behavior, no flag needed)
|
|
23
|
+
name: {
|
|
24
|
+
types: ['string'],
|
|
25
|
+
default: '',
|
|
26
|
+
},
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Route-Level Opt-Out
|
|
30
|
+
|
|
31
|
+
Disable sanitization entirely for a route (rare — only for routes that handle raw HTML everywhere):
|
|
32
|
+
|
|
33
|
+
```javascript
|
|
34
|
+
// In functions/index.js
|
|
35
|
+
Manager.Middleware(req, res).run('my-route', { sanitize: false });
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Manual Sanitization (Outside Middleware)
|
|
39
|
+
|
|
40
|
+
For cron jobs, event handlers, or anywhere outside the request pipeline, use `utilities.sanitize()` directly:
|
|
41
|
+
|
|
42
|
+
```javascript
|
|
43
|
+
// Available in route context
|
|
44
|
+
const clean = utilities.sanitize(untrustedData);
|
|
45
|
+
|
|
46
|
+
// Or via Manager
|
|
47
|
+
const clean = Manager.Utilities().sanitize(untrustedData);
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Accepts any data type — strings, objects, arrays, primitives. Walks objects/arrays recursively, strips HTML from strings, passes everything else through unchanged.
|
|
51
|
+
|
|
52
|
+
## Route Handler Context
|
|
53
|
+
|
|
54
|
+
The middleware injects these into every route handler:
|
|
55
|
+
|
|
56
|
+
```javascript
|
|
57
|
+
module.exports = async ({ Manager, assistant, analytics, usage, user, settings, libraries, utilities }) => {
|
|
58
|
+
// settings — already sanitized by middleware
|
|
59
|
+
// utilities — Manager.Utilities() instance for manual sanitization
|
|
60
|
+
};
|
|
61
|
+
```
|
package/docs/schemas.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Schemas
|
|
2
|
+
|
|
3
|
+
Schemas define and validate the payload your routes accept. Schema names should match the route name (e.g. route `myEndpoint` ↔ schema `myEndpoint`).
|
|
4
|
+
|
|
5
|
+
## New Schema (Consumer Project)
|
|
6
|
+
|
|
7
|
+
Create `schemas/{name}/index.js`:
|
|
8
|
+
|
|
9
|
+
```javascript
|
|
10
|
+
module.exports = function (assistant, settings, options) {
|
|
11
|
+
const user = options.user;
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
defaults: {
|
|
15
|
+
fieldName: {
|
|
16
|
+
types: ['string'],
|
|
17
|
+
default: 'default value',
|
|
18
|
+
required: false,
|
|
19
|
+
},
|
|
20
|
+
numericField: {
|
|
21
|
+
types: ['number'],
|
|
22
|
+
default: 10,
|
|
23
|
+
min: 1,
|
|
24
|
+
max: 100,
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
// Override for premium users
|
|
28
|
+
premium: {
|
|
29
|
+
numericField: {
|
|
30
|
+
max: 1000,
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Field Sanitization
|
|
38
|
+
|
|
39
|
+
By default, every string field in a schema is sanitized (HTML tags stripped, whitespace trimmed) during the middleware pipeline. To preserve raw HTML (rich text, email templates), set `sanitize: false` on the field. See [docs/sanitization.md](sanitization.md).
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Stripe Webhook Forwarding
|
|
2
|
+
|
|
3
|
+
BEM auto-starts Stripe CLI webhook forwarding when running `npx mgr serve` or `npx mgr emulator`. This forwards Stripe test webhooks to the local server so the full payment pipeline works end-to-end during development.
|
|
4
|
+
|
|
5
|
+
**Requirements:**
|
|
6
|
+
- `STRIPE_SECRET_KEY` set in `functions/.env`
|
|
7
|
+
- `BACKEND_MANAGER_KEY` set in `functions/.env`
|
|
8
|
+
- [Stripe CLI](https://stripe.com/docs/stripe-cli) installed
|
|
9
|
+
|
|
10
|
+
**Standalone usage:**
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npx mgr stripe
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
If any prerequisite is missing, webhook forwarding is silently skipped with an info message.
|
|
17
|
+
|
|
18
|
+
The forwarding URL is: `http://localhost:{hostingPort}/backend-manager/payments/webhook?processor=stripe&key={BACKEND_MANAGER_KEY}`
|