create-cartbase 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/template/app/docs/BUILD-A-STOREFRONT.md +1 -1
- package/template/app/docs/README.md +1 -1
- package/template/app/docs/auth.md +1 -1
- package/template/app/docs/carts.md +3 -3
- package/template/app/docs/categories.md +1 -1
- package/template/app/docs/checkout.md +125 -74
- package/template/app/docs/collections.md +1 -1
- package/template/app/docs/components.md +53 -55
- package/template/app/docs/consent.md +1 -1
- package/template/app/docs/content.md +1 -1
- package/template/app/docs/customers.md +1 -1
- package/template/app/docs/deploy.md +1 -1
- package/template/app/docs/gift-cards.md +1 -1
- package/template/app/docs/integrations.md +1 -1
- package/template/app/docs/menus.md +1 -1
- package/template/app/docs/metaobjects.md +1 -1
- package/template/app/docs/orders.md +1 -1
- package/template/app/docs/platform.md +1 -1
- package/template/app/docs/products.md +1 -1
- package/template/app/docs/redirects.md +1 -1
- package/template/app/docs/regions.md +1 -1
- package/template/app/docs/reviews.md +1 -1
- package/template/app/docs/search.md +1 -1
- package/template/app/docs/subscriptions.md +1 -1
- package/template/app/docs/variables.md +315 -0
- package/template/app/next-env.d.ts +6 -0
- package/template/app/package.json +1 -1
- package/template/app/smoke.mjs +1 -1
- package/template/app/src/app/checkout/checkout-page-client.tsx +26 -10
- package/template/app/src/app/checkout/mypos-demo-tab.tsx +101 -0
- package/template/app/src/app/checkout/page.tsx +48 -51
- package/template/app/src/app/order/[id]/confirmed/page.tsx +5 -4
- package/template/app/tsconfig.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Carts
|
|
1
|
+
# Carts
|
|
2
2
|
|
|
3
3
|
The cart is the storefront's working document: created anonymously, mutated
|
|
4
4
|
through line-item and update calls, completed into an order (see
|
|
@@ -118,8 +118,8 @@ test -n "$CART_ID" && test -n "$REGION_ID"
|
|
|
118
118
|
"item_total": 54, "item_subtotal": 45, "item_tax_total": 9,
|
|
119
119
|
"original_total": 54, "original_subtotal": 45, "original_tax_total": 9,
|
|
120
120
|
"credit_line_total": 0,
|
|
121
|
-
"
|
|
122
|
-
"
|
|
121
|
+
"payment_method_fee_total": 0, // the selected method's own fee — non-zero only with a live method session
|
|
122
|
+
"payment_method_fee_label": null,
|
|
123
123
|
// gift-card tender decoration (totals above NEVER move):
|
|
124
124
|
"gift_cards": [], // [{id, last4, amount}] in apply order
|
|
125
125
|
"gift_card_total": 0, // Σ applied-card coverage
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Checkout
|
|
1
|
+
# Checkout
|
|
2
2
|
|
|
3
3
|
This page is the full checkout knowledge transfer: every listing, the
|
|
4
4
|
orchestrated Buy-click sequence, the amount-sync matrix, dead-PI recovery,
|
|
@@ -23,10 +23,11 @@ listShippingOptions(cart_id) ─┐ (render pickers)
|
|
|
23
23
|
listPaymentProviders(cart_id) ┘
|
|
24
24
|
│ Buy click
|
|
25
25
|
▼
|
|
26
|
-
prepareCheckout(cart, {address, shipping_method_id, carrier_metadata,
|
|
26
|
+
prepareCheckout(cart, {address, shipping_method_id, carrier_metadata,
|
|
27
|
+
payment_provider XOR payment_method_id})
|
|
27
28
|
│ ONE atomic call, compensated on failure
|
|
28
29
|
├─ pp_stripe → stripe.confirmPayment(client_secret) ─┐
|
|
29
|
-
└─
|
|
30
|
+
└─ a payment method (Bank transfer, COD…) ───────────┤
|
|
30
31
|
▼
|
|
31
32
|
completeCart(cart) → {type:"order", order}
|
|
32
33
|
```
|
|
@@ -129,33 +130,64 @@ curl -sf -X POST "$BASE/api/store/shipping-options/$SO_ID/calculate" \
|
|
|
129
130
|
`region_id`: the tenant's enabled providers (global catalog ∩ tenant
|
|
130
131
|
enablement). With it: providers linked to that region. `cart_id` feeds
|
|
131
132
|
the rules engine — storefronts SHOULD pass it during checkout.
|
|
132
|
-
- **Response** — list envelope, full filtered list
|
|
133
|
+
- **Response** — list envelope, full filtered list. Two entry shapes share
|
|
134
|
+
the array: connected PROCESSORS (`{id}` — pp_stripe) and merchant
|
|
135
|
+
payment METHODS (`{payment_method_id, name, kind, instructions,
|
|
136
|
+
fee_amount, fee_label}` — "Bank transfer", the COD method). Methods
|
|
137
|
+
carry no provider id anywhere; a fresh store with nothing configured
|
|
138
|
+
gets an honestly empty list. With `region_id`, BOTH families filter by
|
|
139
|
+
the region's claims (`region_payment_provider` /
|
|
140
|
+
`region_payment_method` — availability is a per-region merchant
|
|
141
|
+
choice):
|
|
133
142
|
|
|
134
143
|
```jsonc
|
|
135
144
|
{
|
|
136
145
|
"payment_providers": [
|
|
137
|
-
{ "id": "
|
|
138
|
-
//
|
|
146
|
+
{ "id": "pp_stripe", "is_enabled": true, "created_at": "…" },
|
|
147
|
+
// one entry PER enabled, region-claimed merchant method:
|
|
148
|
+
{ "payment_method_id": "pm_…", "name": "Bank transfer",
|
|
149
|
+
"kind": "manual", "instructions": "IBAN BG…",
|
|
150
|
+
"fee_amount": null, "fee_label": null },
|
|
151
|
+
{ "payment_method_id": "pm_…", "name": "Cash on delivery",
|
|
152
|
+
"kind": "cod", "instructions": null,
|
|
153
|
+
"fee_amount": 4.99, "fee_label": "COD fee" }
|
|
139
154
|
// pp_giftcard is INTERNAL tender and is never listed
|
|
140
155
|
],
|
|
141
|
-
"count":
|
|
156
|
+
"count": 3, "offset": 0, "limit": 3
|
|
142
157
|
}
|
|
143
158
|
```
|
|
144
159
|
|
|
160
|
+
- **Selecting a method** — initiate the session with the method id alone:
|
|
161
|
+
`POST …/payment-sessions { payment_method_id: "pm_…" }`. The server
|
|
162
|
+
validates it (tenant's, enabled, claimed for the cart's region — 400
|
|
163
|
+
otherwise), the session lands with `provider_id` NULL, and the method's
|
|
164
|
+
identity is snapshotted into `session.data`
|
|
165
|
+
(`payment_method_id/name/kind`) so payment surfaces say "Bank
|
|
166
|
+
transfer" without a join. Render `instructions` to the shopper after
|
|
167
|
+
selection and on the confirmation screen. `fee_amount`/`fee_label` let
|
|
168
|
+
the checkout PREDICT the fee before the session exists — the authority
|
|
169
|
+
stays the server totals (`payment_method_fee_total`).
|
|
145
170
|
- **Errors** — none beyond the standard envelope (empty list when nothing
|
|
146
|
-
is enabled)
|
|
171
|
+
is enabled); initiate 400s on unknown/disabled/foreign method ids and
|
|
172
|
+
`payment_method_not_in_region` for an unclaimed region.
|
|
147
173
|
- **SDK** — `checkout.listPaymentProviders(client, {region_id, cart_id})`.
|
|
148
174
|
- **Components** — payment picker.
|
|
149
|
-
- **Settings** —
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
175
|
+
- **Settings** — Stripe connects in Settings, Payments (provisions
|
|
176
|
+
`pp_stripe` + credentials); payment methods live in the same screen
|
|
177
|
+
(the COD switch + named manual methods, each with optional
|
|
178
|
+
`fee_amount`/`fee_label` and per-region availability); checkout rules
|
|
179
|
+
(`target_type=payment_method`, method entries participate under their
|
|
180
|
+
`payment_method_id`) + `checkout_method_order`.
|
|
153
181
|
|
|
154
182
|
```bash
|
|
155
183
|
curl -sf "$BASE/api/store/payment-providers?cart_id=$CART_ID" \
|
|
156
|
-
-H "x-client-id: $CLIENT_ID" | grep -q '"
|
|
157
|
-
curl -sf "$BASE/api/store/payment-providers?region_id=$REGION_ID&cart_id=$CART_ID" \
|
|
158
|
-
-H "x-client-id: $CLIENT_ID"
|
|
184
|
+
-H "x-client-id: $CLIENT_ID" | grep -q '"payment_method_id"'
|
|
185
|
+
PAY_JSON=$(curl -sf "$BASE/api/store/payment-providers?region_id=$REGION_ID&cart_id=$CART_ID" \
|
|
186
|
+
-H "x-client-id: $CLIENT_ID")
|
|
187
|
+
echo "$PAY_JSON" | grep -q '"payment_method_id"'
|
|
188
|
+
# The seeded Bank transfer method carries the rest of this page.
|
|
189
|
+
PM_ID=$(echo "$PAY_JSON" | grep -o '"payment_method_id":"pm_[^"]*"' | head -1 | cut -d'"' -f4)
|
|
190
|
+
test -n "$PM_ID"
|
|
159
191
|
```
|
|
160
192
|
|
|
161
193
|
---
|
|
@@ -166,8 +198,8 @@ ONE call writes everything the customer toggled on /checkout, in the only
|
|
|
166
198
|
safe order: **address first** (option pricing reads the destination) →
|
|
167
199
|
**shipping method** → **payment collection** at the shipped total →
|
|
168
200
|
**payment session LAST** at the FINAL amount (real Stripe PaymentIntent,
|
|
169
|
-
idempotency key = session id; plain row for
|
|
170
|
-
|
|
201
|
+
idempotency key = session id; a plain NULL-provider row for a payment
|
|
202
|
+
method). A method's fee only applies once its session exists, so amounts
|
|
171
203
|
are re-synced after it. Fully compensated: any failure rolls back session
|
|
172
204
|
→ collection → shipping method → addresses/metadata to the pre-call
|
|
173
205
|
snapshot (recorded in the execution ledger, workflow `prepare-checkout`,
|
|
@@ -178,8 +210,10 @@ states done/reverted; failures also land in `checkout_error_logs`, step
|
|
|
178
210
|
- **Request** (`.strict()`; DTO verbatim from
|
|
179
211
|
`src/lib/checkout-orchestration/prepare.ts`) — all address fields
|
|
180
212
|
required except `address_2`/`company`/`province`; `phone` is required
|
|
181
|
-
(courier recovery channel).
|
|
182
|
-
`
|
|
213
|
+
(courier recovery channel). The tender is exactly ONE of
|
|
214
|
+
`payment_provider` (a connected processor — `pp_stripe`, never
|
|
215
|
+
`pp_giftcard`) or `payment_method_id` (a merchant method from the
|
|
216
|
+
listing):
|
|
183
217
|
|
|
184
218
|
```jsonc
|
|
185
219
|
{
|
|
@@ -195,7 +229,7 @@ states done/reverted; failures also land in `checkout_error_logs`, step
|
|
|
195
229
|
"carrier_metadata": { // optional, opaque per-carrier keys
|
|
196
230
|
"office_code": "X1", "office_name": "Center"
|
|
197
231
|
},
|
|
198
|
-
"
|
|
232
|
+
"payment_method_id": "pm_…", // XOR payment_provider: "pp_stripe"
|
|
199
233
|
"save_payment_method": false // optional — subscription carts only;
|
|
200
234
|
// same semantics + consent duty as on
|
|
201
235
|
// payment-sessions below
|
|
@@ -214,15 +248,17 @@ states done/reverted; failures also land in `checkout_error_logs`, step
|
|
|
214
248
|
{
|
|
215
249
|
"cart_id": "cart_…",
|
|
216
250
|
"payment_collection_id": "pc_…",
|
|
217
|
-
"client_secret": "pi_…_secret_…", // Stripe only; null for
|
|
218
|
-
"provider_id":
|
|
251
|
+
"client_secret": "pi_…_secret_…", // Stripe only; null for method sessions AND zero-remainder carts
|
|
252
|
+
"provider_id": null, // the processor, when one was chosen
|
|
253
|
+
"payment_method_id": "pm_…" // the method, when one was chosen
|
|
219
254
|
}
|
|
220
255
|
```
|
|
221
256
|
|
|
222
257
|
- **Zero-remainder gift path** — when applied gift cards cover the whole
|
|
223
|
-
total, the
|
|
224
|
-
`provider_id` come back null) and the cart
|
|
225
|
-
alone — no Stripe involved
|
|
258
|
+
total, the tender session is skipped entirely (`client_secret`,
|
|
259
|
+
`provider_id` and `payment_method_id` come back null) and the cart
|
|
260
|
+
completes on the gift session alone — no Stripe involved
|
|
261
|
+
([gift-cards.md](gift-cards.md)).
|
|
226
262
|
- **Errors** — 404 `cart_not_found` | `shipping_option_not_found`; 409
|
|
227
263
|
`cart_completed`; 400 `validation_failed` | `invalid_provider`
|
|
228
264
|
(pp_giftcard) | `shipping_price_missing` | `stripe_not_configured`.
|
|
@@ -241,8 +277,8 @@ PREP_JSON=$(curl -sf -X POST "$BASE/api/store/carts/$CART_ID/prepare-checkout" \
|
|
|
241
277
|
"country_code":"bg","phone":"+359888123456"},
|
|
242
278
|
"shipping_method_id":"'"$SO_ID"'",
|
|
243
279
|
"carrier_metadata":{"office_code":"X1"},
|
|
244
|
-
"
|
|
245
|
-
echo "$PREP_JSON" | grep -q '"
|
|
280
|
+
"payment_method_id":"'"$PM_ID"'"}')
|
|
281
|
+
echo "$PREP_JSON" | grep -q '"payment_method_id":"'"$PM_ID"'"'
|
|
246
282
|
echo "$PREP_JSON" | grep -q '"client_secret":null'
|
|
247
283
|
PC_ID=$(echo "$PREP_JSON" | grep -o '"payment_collection_id":"pc_[^"]*"' | cut -d'"' -f4)
|
|
248
284
|
test -n "$PC_ID"
|
|
@@ -270,8 +306,9 @@ changes the total while checkout is mounted (quantity change, gift card
|
|
|
270
306
|
applied/removed, shipping switch).
|
|
271
307
|
|
|
272
308
|
- **Auth** — anon `x-client-id`.
|
|
273
|
-
- **Request** — `{provider_id?}` (`.strict()`; empty
|
|
274
|
-
a DIFFERENT
|
|
309
|
+
- **Request** — `{provider_id? | payment_method_id?}` (`.strict()`; empty
|
|
310
|
+
object fine, never both). Passing a DIFFERENT tender than the pending
|
|
311
|
+
session's forces rotation to it.
|
|
275
312
|
- **Response matrix** (verbatim `src/lib/checkout-orchestration/sync.ts`):
|
|
276
313
|
|
|
277
314
|
| state | response |
|
|
@@ -279,29 +316,31 @@ applied/removed, shipping switch).
|
|
|
279
316
|
| completed cart | `{"synced":false,"reason":"cart-completed"}` |
|
|
280
317
|
| no payment collection | `{"synced":false,"reason":"no_payment_collection"}` |
|
|
281
318
|
| no pending provider session (gift sessions excluded) | `{"synced":false,"reason":"no_pending_session"}` |
|
|
282
|
-
|
|
|
283
|
-
|
|
|
284
|
-
|
|
|
319
|
+
| tender matches, amount current | `{"synced":true,"rotated":false,"client_secret":…,"provider_id":…,"payment_method_id":…}` (no-op) |
|
|
320
|
+
| tender matches, amount drifted | in-place update (Stripe `paymentIntents.update`; plain field for method sessions) → `{"synced":true,"rotated":false,…}` — same secret |
|
|
321
|
+
| tender mismatch OR update refused (terminal PI) | rotation: old session retired (+ PI voided best-effort), fresh session at the new remainder → `{"synced":true,"rotated":true,…}` |
|
|
285
322
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
323
|
+
A session's tender identity is its `provider_id` for processors and its
|
|
324
|
+
snapshot `payment_method_id` for NULL-provider method sessions. Rotation
|
|
325
|
+
retires the old session BEFORE recomputing so session-dependent totals
|
|
326
|
+
(the method fee) settle for the NEW tender; a final resync pass aligns
|
|
327
|
+
collection + session + PI. `client_secret` is null for method sessions.
|
|
290
328
|
|
|
291
329
|
- **Errors** — 404 `cart_not_found`; 400 `validation_failed` |
|
|
292
330
|
`stripe_not_configured`. Failures land in `checkout_error_logs`
|
|
293
331
|
(step `sync-payment-amount`).
|
|
294
|
-
- **SDK** — `checkout.syncPaymentAmount(client, cartId, {provider_id
|
|
332
|
+
- **SDK** — `checkout.syncPaymentAmount(client, cartId, {provider_id?,
|
|
333
|
+
payment_method_id?})`.
|
|
295
334
|
- **Components** — checkout totals watcher (debounced), payment-method
|
|
296
|
-
switcher (pass the new
|
|
335
|
+
switcher (pass the new tender id).
|
|
297
336
|
|
|
298
337
|
```bash
|
|
299
|
-
# No-drift no-op on the prepared
|
|
338
|
+
# No-drift no-op on the prepared method cart: same-session, not rotated.
|
|
300
339
|
SYNC_JSON=$(curl -sf -X POST "$BASE/api/store/carts/$CART_ID/sync-payment-amount" \
|
|
301
340
|
-H "x-client-id: $CLIENT_ID" -H "content-type: application/json" -d '{}')
|
|
302
341
|
echo "$SYNC_JSON" | grep -q '"synced":true'
|
|
303
342
|
echo "$SYNC_JSON" | grep -q '"rotated":false'
|
|
304
|
-
echo "$SYNC_JSON" | grep -q '"
|
|
343
|
+
echo "$SYNC_JSON" | grep -q '"payment_method_id":"pm_'
|
|
305
344
|
```
|
|
306
345
|
|
|
307
346
|
---
|
|
@@ -333,7 +372,7 @@ production reload loop).
|
|
|
333
372
|
- **Components** — Stripe Elements mount error handler.
|
|
334
373
|
|
|
335
374
|
```bash
|
|
336
|
-
#
|
|
375
|
+
# Method session ⇒ documented no-op reason (Stripe-specific rotation
|
|
337
376
|
# needs STRIPE credentials — proven by tests/store/checkout-orchestration-sync.test.ts).
|
|
338
377
|
curl -sf -X POST "$BASE/api/store/carts/$CART_ID/refresh-payment-if-terminal" \
|
|
339
378
|
-H "x-client-id: $CLIENT_ID" | grep -q '"reason":"no-stripe-session"'
|
|
@@ -350,7 +389,7 @@ reservation (kit-aware) → order creation (rows copied cart→order) →
|
|
|
350
389
|
cycle 1 tied to this order, cycle 2 scheduled at the next CHARGE date;
|
|
351
390
|
guests are refused with 400 `customer_required`) → **payment authorization
|
|
352
391
|
LAST** (gift tender redeemed atomically first; real Stripe authorize for
|
|
353
|
-
`pp_stripe`; best-effort stub for
|
|
392
|
+
`pp_stripe`; best-effort stub for method sessions) → `order.placed`
|
|
354
393
|
(+ `subscription.created` per contract) on the durable bus. Any failure
|
|
355
394
|
before authorize compensates fully (order deleted, contracts deleted,
|
|
356
395
|
inventory released, gift tender reversed, cart unlocked) — the cart stays
|
|
@@ -378,9 +417,11 @@ the mandate already exists.
|
|
|
378
417
|
`customer_required` (plan lines on a guest cart — subscribing needs an
|
|
379
418
|
account; normally already refused at the payment-session step).
|
|
380
419
|
- 400 `checkout_method_hidden` — **the checkout-rules security
|
|
381
|
-
boundary**: every live payment session's
|
|
382
|
-
`
|
|
383
|
-
|
|
420
|
+
boundary**: every live payment session's chosen tender (the method's
|
|
421
|
+
`payment_method_id` for NULL-provider sessions, else the provider id;
|
|
422
|
+
internal `pp_giftcard` exempt) and every chosen shipping option is
|
|
423
|
+
re-validated against the live rules with the full cart context. A
|
|
424
|
+
stale session
|
|
384
425
|
that picked a method before a rule started matching, or a hostile
|
|
385
426
|
client that skipped the filtered listings, is rejected here and the
|
|
386
427
|
rejection is recorded in `checkout_error_logs` (step `complete`).
|
|
@@ -395,14 +436,15 @@ the mandate already exists.
|
|
|
395
436
|
`@cartbase/storefront/api/carts`).
|
|
396
437
|
- **Components** — Buy button (orchestrated path), order-confirmation
|
|
397
438
|
page.
|
|
398
|
-
- **Settings** — checkout rules; `accounts_mode`;
|
|
399
|
-
fee exists only while
|
|
400
|
-
cart at prepare
|
|
439
|
+
- **Settings** — checkout rules; `accounts_mode`; the payment method fee
|
|
440
|
+
(**timing**: a method's fee exists only while ITS live session does — it
|
|
441
|
+
appears on the cart at prepare as `payment_method_fee_total`, rides
|
|
442
|
+
`cart.total`, and is carried onto the order via
|
|
401
443
|
`order_summaries.totals`, where waybill COD amounts read it); gift-card
|
|
402
444
|
tender (zero-remainder carts complete on the gift session alone).
|
|
403
445
|
|
|
404
446
|
```bash
|
|
405
|
-
# Complete the prepared
|
|
447
|
+
# Complete the prepared method cart → a real order, no Stripe env needed.
|
|
406
448
|
ORDER_JSON=$(curl -sf -X POST "$BASE/api/store/carts/$CART_ID/complete" \
|
|
407
449
|
-H "x-client-id: $CLIENT_ID" -H "content-type: application/json" -d '{}')
|
|
408
450
|
echo "$ORDER_JSON" | grep -q '"type":"order"'
|
|
@@ -452,16 +494,19 @@ curl -sf -X POST "$BASE/api/store/carts/$CART_ID/refresh-payment-if-terminal" \
|
|
|
452
494
|
|
|
453
495
|
### POST /api/store/payment-collections/:id/payment-sessions
|
|
454
496
|
|
|
455
|
-
- **Purpose** — mint (or repair) the
|
|
456
|
-
|
|
497
|
+
- **Purpose** — mint (or repair) the tender session — idempotent per
|
|
498
|
+
tender. For Stripe the PaymentIntent is minted FIRST (idempotency key
|
|
457
499
|
= session id) so a Stripe session row can never exist without its
|
|
458
500
|
intent; amount drift syncs the PI in place; terminal PIs self-heal by
|
|
459
|
-
rotation.
|
|
501
|
+
rotation. Method sessions share ONE NULL-provider row per collection —
|
|
502
|
+
switching methods updates its snapshot in place.
|
|
460
503
|
- **Auth** — anon `x-client-id`.
|
|
461
|
-
- **Request** — `{provider_id
|
|
462
|
-
|
|
463
|
-
`
|
|
464
|
-
|
|
504
|
+
- **Request** — `{provider_id? XOR payment_method_id?, data?,
|
|
505
|
+
save_payment_method?}` — exactly one tender. Keys the server owns
|
|
506
|
+
(`payment_intent_id`, `client_secret`, `status`, `stripe_customer_id`,
|
|
507
|
+
`setup_future_usage`, and the method snapshot
|
|
508
|
+
`payment_method_id/name/kind`) are stripped from `data` — they cannot
|
|
509
|
+
be forged from the client.
|
|
465
510
|
- **`save_payment_method`** — saves the card for future off-session
|
|
466
511
|
renewal charges: the server resolves the CART'S customer (never a
|
|
467
512
|
client-supplied id), ensures a Stripe Customer for them, and mints the
|
|
@@ -475,21 +520,26 @@ curl -sf -X POST "$BASE/api/store/carts/$CART_ID/refresh-payment-if-terminal" \
|
|
|
475
520
|
the plan selection / next to the payment element (e.g. "Your card will
|
|
476
521
|
be saved for future subscription charges"). Re-initiating an existing
|
|
477
522
|
session with the flag (or after a plan line appears) upgrades the live
|
|
478
|
-
intent in place (same `client_secret`). No
|
|
479
|
-
(COD/manual subscriptions renew offline)
|
|
480
|
-
ordinary checkouts.
|
|
523
|
+
intent in place (same `client_secret`). No card to save on method
|
|
524
|
+
sessions (COD/manual subscriptions renew offline) — the account gate
|
|
525
|
+
still applies. Never set the flag on ordinary checkouts.
|
|
481
526
|
- **Response** — `201 {payment_session}` when created, `200` when the
|
|
482
527
|
existing one was returned/repaired: `{id, provider_id, amount,
|
|
483
528
|
currency_code, status:"pending", authorized_at:null, data}` — for
|
|
484
529
|
Stripe, `data` carries `payment_intent_id` + `client_secret` (mount
|
|
485
530
|
Elements with it); with `save_payment_method` it also carries
|
|
486
|
-
`setup_future_usage: "off_session"` + `stripe_customer_id`.
|
|
487
|
-
`
|
|
531
|
+
`setup_future_usage: "off_session"` + `stripe_customer_id`. For a
|
|
532
|
+
method, `provider_id` is null and `data` carries the snapshot
|
|
533
|
+
(`payment_method_id`, `payment_method_name`, `payment_method_kind`).
|
|
534
|
+
The session `amount` is `collection.amount − gift_card_total` — the
|
|
535
|
+
remainder.
|
|
488
536
|
- **Errors** — 404 `payment_collection_not_found`; 400 `invalid_provider`
|
|
489
|
-
(pp_giftcard
|
|
490
|
-
`
|
|
491
|
-
|
|
492
|
-
|
|
537
|
+
(pp_giftcard, or an id the catalog doesn't know — the dead pp_ ids land
|
|
538
|
+
here) | `payment_provider_disabled` | `payment_provider_not_in_region` |
|
|
539
|
+
`payment_method_not_in_region` | `stripe_not_configured` |
|
|
540
|
+
`customer_required` | `validation_failed`.
|
|
541
|
+
- **SDK** — `checkout.initiatePaymentSession(client, pcId, {provider_id?,
|
|
542
|
+
payment_method_id?, save_payment_method?})`.
|
|
493
543
|
|
|
494
544
|
### POST /api/store/carts/:id/shipping-methods
|
|
495
545
|
|
|
@@ -532,8 +582,9 @@ PC2_ID=$(echo "$PC2_JSON" | grep -o '"id":"pc_[^"]*"' | head -1 | cut -d'"' -f4)
|
|
|
532
582
|
SES_JSON=$(curl -sf -X POST \
|
|
533
583
|
"$BASE/api/store/payment-collections/$PC2_ID/payment-sessions" \
|
|
534
584
|
-H "x-client-id: $CLIENT_ID" -H "content-type: application/json" \
|
|
535
|
-
-d '{"
|
|
536
|
-
echo "$SES_JSON" | grep -q '"provider_id":
|
|
585
|
+
-d '{"payment_method_id":"'"$PM_ID"'"}')
|
|
586
|
+
echo "$SES_JSON" | grep -q '"provider_id":null'
|
|
587
|
+
echo "$SES_JSON" | grep -q '"payment_method_name"'
|
|
537
588
|
echo "$SES_JSON" | grep -q '"status":"pending"'
|
|
538
589
|
|
|
539
590
|
# Error contract: the internal gift tender is not initiable.
|
|
@@ -564,7 +615,7 @@ PC3_ID=$(echo "$PC3_JSON" | grep -o '"id":"pc_[^"]*"' | head -1 | cut -d'"' -f4)
|
|
|
564
615
|
|
|
565
616
|
curl -s -X POST "$BASE/api/store/payment-collections/$PC3_ID/payment-sessions" \
|
|
566
617
|
-H "x-client-id: $CLIENT_ID" -H "content-type: application/json" \
|
|
567
|
-
-d '{"
|
|
618
|
+
-d '{"payment_method_id":"'"$PM_ID"'","save_payment_method":true}' \
|
|
568
619
|
| grep -q '"code":"customer_required"'
|
|
569
620
|
```
|
|
570
621
|
|
|
@@ -597,15 +648,15 @@ rotates only on `succeeded`/`canceled`/`requires_capture`/missing.
|
|
|
597
648
|
exercised by `tests/store/checkout-rules.test.ts`.
|
|
598
649
|
- **`account_required` (403)** — requires `accounts_mode='required'` on
|
|
599
650
|
the store; exercised by `tests/store/customer-accounts-policy.test.ts`.
|
|
600
|
-
- **
|
|
601
|
-
(
|
|
602
|
-
`tests/store/
|
|
603
|
-
`tests/store/checkout-orchestration.test.ts` (fee-inclusive
|
|
604
|
-
prepare with
|
|
651
|
+
- **Payment method fee** — any method may carry `fee_amount`/`fee_label`
|
|
652
|
+
(the COD method included); exercised by
|
|
653
|
+
`tests/store/payment-method-fee.test.ts` (method-switch switches the
|
|
654
|
+
fee) and `tests/store/checkout-orchestration.test.ts` (fee-inclusive
|
|
655
|
+
session on prepare with the COD method).
|
|
605
656
|
|
|
606
657
|
## Cleanup / accretion note
|
|
607
658
|
|
|
608
|
-
This page creates two carts and completes two
|
|
659
|
+
This page creates two carts and completes two method-tender orders on the
|
|
609
660
|
shared dev tenant — the same inert accretion the checkout test suites
|
|
610
661
|
produce (no store-facing delete exists for either; suites always create
|
|
611
662
|
their own carts/orders and never re-read foreign ones).
|