epicmerch-mcp 1.3.6 → 1.3.13
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/README.md +22 -0
- package/package.json +1 -1
- package/skills/epicmerch-debug.md +11 -0
- package/skills/epicmerch-e2e.md +203 -0
- package/skills/epicmerch-verify.md +53 -0
- package/skills/epicmerch.md +62 -34
- package/src/prompts/index.js +13 -0
- package/src/tools/developer.js +127 -0
- package/src/tools/merchant.js +15 -1
package/README.md
CHANGED
|
@@ -156,6 +156,28 @@ merchant_diagnose({})
|
|
|
156
156
|
|
|
157
157
|
Returns store name + currency, product/category/out-of-stock counts, payment + logistics configuration status, API key info, a 0-100 readiness score, and a prioritised list of next steps. Use this to drive "what's still missing?" conversations.
|
|
158
158
|
|
|
159
|
+
## Debugging a broken storefront
|
|
160
|
+
|
|
161
|
+
Three layers, from quickest to deepest:
|
|
162
|
+
|
|
163
|
+
| Tool | What it does | When to use |
|
|
164
|
+
|------|--------------|-------------|
|
|
165
|
+
| `merchant_diagnose({})` | Server-side health snapshot (above) | "Is my store set up correctly?" |
|
|
166
|
+
| `/epicmerch-verify` skill | Smoke-tests every read path + validates the checkout payload shape against the live API, WITHOUT sending OTP or charging a card. Ends with a manual Razorpay-test-card recipe. | "Does my site actually load products / will checkout work?" — run after scaffolding |
|
|
167
|
+
| `/epicmerch-debug` skill | Four-check playbook for the most common failures: (1) OTP login that doesn't persist across reloads, (2) products not loading, (3) `INSUFFICIENT_STOCK: undefined` on checkout, (4) cart returning 401. Diagnoses + applies the fix. | "Something's broken and I don't know why" |
|
|
168
|
+
|
|
169
|
+
The wizard (`/epicmerch`) auto-runs `/epicmerch-verify` as its final step and auto-routes to `/epicmerch-debug` if any check fails — so most issues surface and get fixed before you ever see them.
|
|
170
|
+
|
|
171
|
+
### Error responses are structured
|
|
172
|
+
|
|
173
|
+
Every API error returns `{ success: false, code, message, hint?, items? }`.
|
|
174
|
+
Branch on `code` (stable) rather than substring-matching `message` (human-readable, may change). The SDK README has the full code table + rate-limit table. The most useful for debugging:
|
|
175
|
+
|
|
176
|
+
- `INVALID_ORDER_ITEM` — an `orderItems[]` entry is missing `productId`; the response's `received` field shows exactly what you sent.
|
|
177
|
+
- `INSUFFICIENT_STOCK` — `items[]` lists each shortfall as `{ product, variant, requested, available }`.
|
|
178
|
+
- `PRODUCT_NOT_FOUND` — the `productId` doesn't match any product in this store.
|
|
179
|
+
- `PAYMENT_NOT_CONFIGURED` — no Razorpay keys yet (run `/epicmerch-razorpay`).
|
|
180
|
+
|
|
159
181
|
## Shopify migration (MCP tool)
|
|
160
182
|
|
|
161
183
|
Migrate a Shopify store to EpicMerch in two steps:
|
package/package.json
CHANGED
|
@@ -175,6 +175,17 @@ clicking Add to Cart fails. Network shows 401 or 403 on POST `/customer/cart`.
|
|
|
175
175
|
that subsequent API calls include the Authorization header.
|
|
176
176
|
3. Call `store.auth.getSession()` — if it returns null, the token has
|
|
177
177
|
expired or wasn't restored. `store.auth.refreshToken()` can extend it.
|
|
178
|
+
4. **Confirm against the live API.** Grab the token from
|
|
179
|
+
`localStorage.customerInfo.token` and the store's API key, then:
|
|
180
|
+
```
|
|
181
|
+
store_cart_get({ apiKey: "<KEY>", customerToken: "<TOKEN>" })
|
|
182
|
+
```
|
|
183
|
+
- `httpStatus: 200` → the token IS valid; the bug is purely client-side
|
|
184
|
+
(the SDK isn't restoring it — apply Check 1's fix to src/lib/epicmerch.js).
|
|
185
|
+
- `httpStatus: 401` → the token is expired or invalid; the customer
|
|
186
|
+
needs to log in again, or refreshToken() needs wiring.
|
|
187
|
+
This pins whether the problem is "token bad" vs "SDK not using a good
|
|
188
|
+
token" — the two have completely different fixes.
|
|
178
189
|
|
|
179
190
|
**Fix.**
|
|
180
191
|
- If localStorage is empty: merchant needs to log in again.
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: epicmerch-e2e
|
|
3
|
+
description: Full end-to-end smoke test against the LIVE EpicMerch API — creates a throwaway test product, adds it to a cart, creates an order (exercising stock reservation + Razorpay order creation), verifies every response shape, then cleans everything up. Proves the entire merchant→customer pipeline works without spending money or leaving junk data. The deepest verification available.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# EpicMerch End-to-End Smoke Test
|
|
7
|
+
|
|
8
|
+
This runs the **complete pipeline** against the live API with real data:
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
create product → add to cart → get cart → create order → cancel order → delete product
|
|
12
|
+
(merchant) (customer) (customer) (customer) (customer) (merchant)
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
If every step returns a good response, the whole store genuinely works —
|
|
16
|
+
not just the read paths. Nothing is charged (the order is created UNPAID
|
|
17
|
+
and cancelled), and nothing is left behind (the test product is deleted).
|
|
18
|
+
|
|
19
|
+
Use this when a merchant wants real confidence ("does checkout actually
|
|
20
|
+
work?"), or after any change to the order/cart/product APIs. It's heavier
|
|
21
|
+
than `/epicmerch-verify` (it writes + deletes data), so it's opt-in.
|
|
22
|
+
|
|
23
|
+
**Fully automated — no real SMS.** The customer session comes from the
|
|
24
|
+
genuine OTP login flow against the server's configured test phone
|
|
25
|
+
(`E2E_TEST_PHONE`), which skips the actual SMS and seeds a secret code
|
|
26
|
+
(`E2E_TEST_OTP`). So the test exercises the REAL `/auth/otp/send` +
|
|
27
|
+
`/auth/otp/verify` endpoints with zero SMS cost.
|
|
28
|
+
|
|
29
|
+
## Prerequisites
|
|
30
|
+
|
|
31
|
+
1. **Merchant MCP session** — you call `merchant_create_product` /
|
|
32
|
+
`merchant_delete_product` with the merchant's OAuth token (already
|
|
33
|
+
present if the MCP is connected).
|
|
34
|
+
2. **A store API key** — `merchant_list_api_keys` (or generate a temp one
|
|
35
|
+
with `merchant_generate_api_key({ name: 'E2E test' })` and delete it
|
|
36
|
+
after).
|
|
37
|
+
3. **A customer token** — obtained via the real OTP login flow against
|
|
38
|
+
the configured test phone (the server seeds the code, no SMS sent):
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
store_auth_send_otp({ apiKey: "<KEY>", phone: "<E2E_TEST_PHONE>" })
|
|
42
|
+
store_auth_verify_otp({ apiKey: "<KEY>", phone: "<E2E_TEST_PHONE>", otp: "<E2E_TEST_OTP>" })
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`store_auth_verify_otp`'s `response.token` is your customer token —
|
|
46
|
+
call it `<CT>`. This exercises the genuine `/auth/otp/send` +
|
|
47
|
+
`/auth/otp/verify` endpoints, so it ALSO verifies that login works,
|
|
48
|
+
not just cart/order. `<E2E_TEST_PHONE>` and `<E2E_TEST_OTP>` are the
|
|
49
|
+
values the server admin configured in its environment.
|
|
50
|
+
|
|
51
|
+
If `store_auth_send_otp` returns a normal SMS channel (not the
|
|
52
|
+
`e2e-test` channel) it means this server doesn't have the test phone
|
|
53
|
+
configured — ask the operator to set `E2E_TEST_PHONE` / `E2E_TEST_OTP`,
|
|
54
|
+
or run `/epicmerch-verify` instead (no token needed).
|
|
55
|
+
|
|
56
|
+
Call the customer token `<CT>` and the API key `<KEY>` below.
|
|
57
|
+
|
|
58
|
+
Confirm payments are configured first: `merchant_get_checkout_settings`.
|
|
59
|
+
If Razorpay isn't set up, the order step will return
|
|
60
|
+
`PAYMENT_NOT_CONFIGURED` — that's still a useful result (route to
|
|
61
|
+
`/epicmerch-razorpay`), but tell the merchant up front.
|
|
62
|
+
|
|
63
|
+
## The test
|
|
64
|
+
|
|
65
|
+
Run these steps in order. STOP and report at the first failure — a later
|
|
66
|
+
step depends on the earlier ones. Always run the cleanup steps (5, 6)
|
|
67
|
+
even if an earlier step failed, so you don't leave a test product or a
|
|
68
|
+
stuck reservation behind.
|
|
69
|
+
|
|
70
|
+
### Step 1 — Create a throwaway test product
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
merchant_create_product({
|
|
74
|
+
name: "__E2E_TEST__ (safe to delete)",
|
|
75
|
+
type: "Test",
|
|
76
|
+
description: "Automated end-to-end test product. Delete if you see it.",
|
|
77
|
+
price: 1,
|
|
78
|
+
stock: 5,
|
|
79
|
+
variants: [{ variant: "TEST", stock: 5 }],
|
|
80
|
+
images: ["https://via.placeholder.com/300"]
|
|
81
|
+
})
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Grab the new product's `_id` (call it `<PID>`). Price ₹1 + a single
|
|
85
|
+
`TEST` variant keeps it cheap and unambiguous.
|
|
86
|
+
|
|
87
|
+
> ✓ Step 1 — test product created (`<PID>`)
|
|
88
|
+
|
|
89
|
+
If the server only creates a placeholder and needs a follow-up update
|
|
90
|
+
(some versions do), chain `merchant_update_product({ id: <PID>, ... })`
|
|
91
|
+
with the same fields, then re-fetch with `merchant_get_product({ id: <PID> })`
|
|
92
|
+
to confirm the variant + stock landed.
|
|
93
|
+
|
|
94
|
+
### Step 2 — Add it to the cart
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
store_cart_add({ apiKey: "<KEY>", customerToken: "<CT>", productId: "<PID>", qty: 1, variant: "TEST" })
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Expect `httpStatus: 200`, body `{ message: 'Added to cart', cartCount: N }`.
|
|
101
|
+
|
|
102
|
+
> ✓ Step 2 — added to cart
|
|
103
|
+
|
|
104
|
+
### Step 3 — Fetch the cart and verify the shape
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
store_cart_get({ apiKey: "<KEY>", customerToken: "<CT>" })
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Confirm `response.cart` contains the test item shaped
|
|
111
|
+
`{ product: { _id: "<PID>", name, price, image, ... }, qty: 1, variant: "TEST" }`.
|
|
112
|
+
This is the authoritative cart shape — if the scaffolded `Cart.jsx`
|
|
113
|
+
reads anything different, FIX the scaffold to match.
|
|
114
|
+
|
|
115
|
+
> ✓ Step 3 — cart returns the item with the expected shape
|
|
116
|
+
|
|
117
|
+
### Step 4 — Create the order (the real checkout pipeline)
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
store_order_create({
|
|
121
|
+
apiKey: "<KEY>",
|
|
122
|
+
customerToken: "<CT>",
|
|
123
|
+
orderItems: [{ productId: "<PID>", name: "__E2E_TEST__ (safe to delete)", image: "https://via.placeholder.com/300", qty: 1, price: 1, variant: "TEST" }],
|
|
124
|
+
shippingAddress: { fullName: "E2E Test", address: "1 Test St", city: "Mumbai", state: "MH", postalCode: "400001", country: "India", phone: "+919999999999" },
|
|
125
|
+
paymentMethod: "Razorpay",
|
|
126
|
+
totalPrice: 1
|
|
127
|
+
})
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
This exercises the FULL pipeline: stock reservation (SELECT FOR UPDATE),
|
|
131
|
+
order row creation, and Razorpay order creation. **It does not charge** —
|
|
132
|
+
no payment is captured. Confirm `response` contains:
|
|
133
|
+
- `orderId` (the EpicMerch order)
|
|
134
|
+
- `razorpayOrderId` + `razorpayKeyId` + `amount` + `currency` + `merchantName`
|
|
135
|
+
(if Razorpay is configured)
|
|
136
|
+
|
|
137
|
+
Grab `response.orderId` as `<OID>`.
|
|
138
|
+
|
|
139
|
+
If you get `code: INSUFFICIENT_STOCK` here, the variant string in
|
|
140
|
+
orderItems didn't match the cart/product variant — compare against
|
|
141
|
+
Step 3's response. If `code: INVALID_ORDER_ITEM`, the orderItems shape
|
|
142
|
+
is wrong (missing productId). If `code: PAYMENT_NOT_CONFIGURED`, route
|
|
143
|
+
to `/epicmerch-razorpay`.
|
|
144
|
+
|
|
145
|
+
> ✓ Step 4 — order created (`<OID>`), Razorpay order + key returned
|
|
146
|
+
|
|
147
|
+
### Step 5 — Cancel the order (cleanup, releases the reservation)
|
|
148
|
+
|
|
149
|
+
```
|
|
150
|
+
store_order_cancel({ apiKey: "<KEY>", customerToken: "<CT>", orderId: "<OID>" })
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
This frees the stock reservation immediately (otherwise it'd auto-expire
|
|
154
|
+
in ~15 min).
|
|
155
|
+
|
|
156
|
+
> ✓ Step 5 — test order cancelled, stock released
|
|
157
|
+
|
|
158
|
+
### Step 6 — Delete the test product
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
merchant_delete_product({ id: "<PID>" })
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Also remove it from the cart if it lingers:
|
|
165
|
+
`store_cart_remove({ apiKey, customerToken, productId: "<PID>", variant: "TEST" })`.
|
|
166
|
+
|
|
167
|
+
If you generated a temp API key in prerequisites, delete it too:
|
|
168
|
+
`merchant_delete_api_key({ id: "<temp_key_id>" })`.
|
|
169
|
+
|
|
170
|
+
> ✓ Step 6 — test product + temp key deleted
|
|
171
|
+
|
|
172
|
+
## Final report
|
|
173
|
+
|
|
174
|
+
```
|
|
175
|
+
EpicMerch End-to-End Test — <merchantEmail>
|
|
176
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
177
|
+
✓ 1. Create product __E2E_TEST__ (₹1, variant TEST)
|
|
178
|
+
✓ 2. Add to cart cartCount: 1
|
|
179
|
+
✓ 3. Get cart shape matches Cart.jsx contract
|
|
180
|
+
✓ 4. Create order orderId + razorpayOrderId returned (UNPAID)
|
|
181
|
+
✓ 5. Cancel order reservation released
|
|
182
|
+
✓ 6. Cleanup product + temp key deleted
|
|
183
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
184
|
+
Full pipeline verified. The only thing not exercised is the actual
|
|
185
|
+
Razorpay card capture — test that once in a browser with test card
|
|
186
|
+
4111 1111 1111 1111. Everything up to the charge is confirmed working.
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
If any step failed, the report should show ✗ on that step with the exact
|
|
190
|
+
error code/message, and the cleanup steps should still have run. Never
|
|
191
|
+
leave a `__E2E_TEST__` product in the merchant's live catalog.
|
|
192
|
+
|
|
193
|
+
## Safety notes
|
|
194
|
+
|
|
195
|
+
- The test product is named `__E2E_TEST__ (safe to delete)` so it's
|
|
196
|
+
obvious in the dashboard if cleanup ever fails.
|
|
197
|
+
- Price ₹1 + stock 5 keeps it harmless even if a real customer somehow
|
|
198
|
+
saw it during the brief window it exists.
|
|
199
|
+
- The order is never paid, so no money moves and no real fulfilment is
|
|
200
|
+
triggered.
|
|
201
|
+
- Always cancel the order AND delete the product, even on partial
|
|
202
|
+
failure — orphaned reservations hold stock, orphaned test products
|
|
203
|
+
clutter the catalog.
|
|
@@ -105,6 +105,59 @@ merchant_delete_api_key({ id: "<temp_key_id>" })
|
|
|
105
105
|
|
|
106
106
|
---
|
|
107
107
|
|
|
108
|
+
## Phase 2.5 — Live cart round-trip (the authoritative cart-shape check)
|
|
109
|
+
|
|
110
|
+
**This is the most reliable way to get the cart contract right.** Instead
|
|
111
|
+
of trusting docs or controller reads, actually add an item to a cart and
|
|
112
|
+
fetch it back through the MCP, then read the REAL response shape. The
|
|
113
|
+
scaffolded `Cart.jsx` must parse exactly what comes back here.
|
|
114
|
+
|
|
115
|
+
This needs a **customer token** (a JWT from a storefront login), which
|
|
116
|
+
costs one OTP. If the merchant can provide one, do this — it's worth it.
|
|
117
|
+
If not, skip to Phase 3 (the structural check) and note the cart shape
|
|
118
|
+
wasn't verified against a live session.
|
|
119
|
+
|
|
120
|
+
Ask:
|
|
121
|
+
|
|
122
|
+
> "To verify the cart end-to-end I need a customer token. Open your
|
|
123
|
+
> storefront, log in once, then in DevTools → Application → Local Storage
|
|
124
|
+
> copy the `token` value from `customerInfo`. Paste it here — or say
|
|
125
|
+
> 'skip' and I'll validate the shape structurally instead."
|
|
126
|
+
|
|
127
|
+
If they provide a token (call it `<CUSTOMER_TOKEN>`) and you have an API
|
|
128
|
+
key from Phase 2 (`<KEY>`) plus a product id (`<FIRST_PRODUCT_ID>`) with
|
|
129
|
+
an in-stock variant (`<VARIANT>`):
|
|
130
|
+
|
|
131
|
+
1. **Add to cart:**
|
|
132
|
+
```
|
|
133
|
+
store_cart_add({ apiKey: "<KEY>", customerToken: "<CUSTOMER_TOKEN>", productId: "<FIRST_PRODUCT_ID>", qty: 1, variant: "<VARIANT>" })
|
|
134
|
+
```
|
|
135
|
+
Expect `httpStatus: 200` and a body like `{ message: 'Added to cart', cartCount: N }`. If you get 401 → the token is expired/wrong. If 404 → the productId is wrong.
|
|
136
|
+
|
|
137
|
+
2. **Fetch the cart:**
|
|
138
|
+
```
|
|
139
|
+
store_cart_get({ apiKey: "<KEY>", customerToken: "<CUSTOMER_TOKEN>" })
|
|
140
|
+
```
|
|
141
|
+
Read `response.cart`. This is the AUTHORITATIVE shape. Confirm each line item is `{ product: { _id, name, price, originalPrice, salePrice, image, type }, qty, variant }`.
|
|
142
|
+
|
|
143
|
+
3. **Cross-check against the scaffold.** Open the project's
|
|
144
|
+
`src/components/Cart.jsx` and confirm it reads:
|
|
145
|
+
- `cart.cart` (the array) — NOT `cart.items`
|
|
146
|
+
- `item.product._id`, `item.product.name`, `item.product.image`
|
|
147
|
+
- `item.product.salePrice ?? item.product.price`
|
|
148
|
+
- `item.variant`
|
|
149
|
+
If the scaffold reads anything the live response doesn't have, FIX the
|
|
150
|
+
scaffold to match the live shape — the live response wins, always.
|
|
151
|
+
|
|
152
|
+
4. **Clean up** so you don't leave a junk item in the cart:
|
|
153
|
+
```
|
|
154
|
+
store_cart_remove({ apiKey: "<KEY>", customerToken: "<CUSTOMER_TOKEN>", productId: "<FIRST_PRODUCT_ID>", variant: "<VARIANT>" })
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Report: `✓ Live cart round-trip — add → get → shape matches Cart.jsx → cleaned up`.
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
108
161
|
## Phase 3 — Checkout payload shape check (no actual order created)
|
|
109
162
|
|
|
110
163
|
This catches the contract bugs that have historically caused live `INSUFFICIENT_STOCK: undefined` or `IDEMPOTENCY_KEY_MISMATCH` failures. We BUILD the orderItems payload that the scaffolded `Checkout.jsx` would build, then validate its shape against the API contract — without actually calling `orders.create`.
|
package/skills/epicmerch.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: epicmerch
|
|
3
|
-
description: EpicMerch umbrella — guided 6-step onboarding wizard for new merchants
|
|
3
|
+
description: EpicMerch umbrella — guided 6-step onboarding wizard for new merchants that ALWAYS ends by auto-running a live end-to-end order test (/epicmerch-e2e), plus an intent router for merchants who already know what they want (scaffold a storefront, set up payments, add a Buy Now button, manage a store, migrate from Shopify, verify the store). Verification is the non-negotiable definition of done for any build.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# EpicMerch — the umbrella
|
|
@@ -21,6 +21,18 @@ If ambiguous, ask ONE disambiguating question: *"Want me to walk you through a f
|
|
|
21
21
|
|
|
22
22
|
---
|
|
23
23
|
|
|
24
|
+
## DEFINITION OF DONE — read this before you finish (applies to BOTH modes)
|
|
25
|
+
|
|
26
|
+
**`/epicmerch` is not complete until a live end-to-end test has run and passed.**
|
|
27
|
+
|
|
28
|
+
This holds no matter how the build happened — the wizard's scaffold step, an intent-mode `/epicmerch-storefront`, OR a fully custom build you designed through brainstorming (e.g. a themed multi-page store). However you got here, the LAST thing you do before telling the merchant "your store is set up" is run **`/epicmerch-e2e`** (full pipeline) — falling back to `/epicmerch-verify` only if the server's test-login isn't configured.
|
|
29
|
+
|
|
30
|
+
Common trap: a design-heavy request ("build me a saree store") pulls you into scope/theme/spec brainstorming and a custom build, and the flow ends at "looks good!" without ever testing against the live API. **Don't let that happen.** After the storefront files are written and the `.env` has a real API key, run the e2e/verify gate. A store that was never tested against the live API is not done — that's exactly how the cart-shape / productId / out-of-stock bugs reached real merchants.
|
|
31
|
+
|
|
32
|
+
If you realize you're wrapping up `/epicmerch` and haven't run the test → run it now.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
24
36
|
## Step A — Wizard mode (guided onboarding)
|
|
25
37
|
|
|
26
38
|
The merchant either said nothing or asked to "start". Walk them through the 5 steps below **in order**. After each step, briefly recap (`✓ done` or `✗ skipped — <reason>`) and move on. Don't dump all 5 questions at once; one step at a time.
|
|
@@ -130,35 +142,48 @@ Ask:
|
|
|
130
142
|
- Yes → run `/epicmerch-magic-checkout`. (That skill verifies Razorpay is configured first; if step 2 set it up, this just works.)
|
|
131
143
|
- No / skip → `✗ Skipped.` (They can always add it later by saying *"add a Buy Now button"*.)
|
|
132
144
|
|
|
133
|
-
### Wizard step 6/6 — Verify the storefront
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
145
|
+
### Wizard step 6/6 — Verify the storefront (MANDATORY — always runs)
|
|
146
|
+
|
|
147
|
+
**This step is NOT optional and is NOT skippable.** Whether the store was
|
|
148
|
+
built by the wizard's scaffold step OR by a custom/brainstormed build,
|
|
149
|
+
the final action of `/epicmerch` is ALWAYS to run a live test against the
|
|
150
|
+
API. Do not print a "your store is set up" wrap-up until this has run and
|
|
151
|
+
passed. If you find yourself about to end the `/epicmerch` flow without
|
|
152
|
+
having run a verification, STOP and run it now.
|
|
153
|
+
|
|
154
|
+
> "Last step — let me run a live end-to-end test to confirm everything actually works."
|
|
155
|
+
|
|
156
|
+
**Run `/epicmerch-e2e` automatically.** It's the deepest check: it gets a
|
|
157
|
+
customer session, creates a throwaway test product, adds it to a cart,
|
|
158
|
+
creates a real order (exercising stock reservation + Razorpay order
|
|
159
|
+
creation, no charge), verifies every response shape, then cleans up
|
|
160
|
+
(cancels the order, deletes the product). This proves the WHOLE pipeline,
|
|
161
|
+
not just read paths.
|
|
162
|
+
|
|
163
|
+
`/epicmerch-e2e` gets its customer token via the server's env-gated test
|
|
164
|
+
phone (`store_auth_send_otp` + `store_auth_verify_otp` against
|
|
165
|
+
`E2E_TEST_PHONE`), so it needs no OTP/SMS and no manual login.
|
|
166
|
+
|
|
167
|
+
**Graceful fallback** — if `/epicmerch-e2e`'s token step fails because the
|
|
168
|
+
server doesn't have `E2E_TEST_PHONE`/`E2E_TEST_OTP` configured (you'll see
|
|
169
|
+
a normal SMS channel instead of `e2e-test`, or a 401 on verify), fall back
|
|
170
|
+
to `/epicmerch-verify` — it tests every read path + the checkout payload
|
|
171
|
+
shape with no token needed, then gives a manual cart→checkout test recipe.
|
|
172
|
+
Tell the merchant: *"Ran the read-path + payload checks; to enable the
|
|
173
|
+
fully-automated order test, ask your EpicMerch operator to set
|
|
174
|
+
E2E_TEST_PHONE/E2E_TEST_OTP on the server."*
|
|
175
|
+
|
|
176
|
+
**If any check fails:** do NOT just print the failure and stop. Route to
|
|
177
|
+
`/epicmerch-debug` automatically — its four-check playbook (OTP not
|
|
178
|
+
persisting, products not loading, INSUFFICIENT_STOCK on checkout, cart
|
|
179
|
+
401) diagnoses + fixes without further merchant intervention. Then re-run
|
|
180
|
+
the test to confirm the fix held.
|
|
181
|
+
|
|
182
|
+
Only proceed to wrap-up when the test is fully green.
|
|
158
183
|
|
|
159
184
|
### Wizard wrap-up
|
|
160
185
|
|
|
161
|
-
Print a one-block recap after
|
|
186
|
+
Print a one-block recap after the step-6 test finishes:
|
|
162
187
|
|
|
163
188
|
```
|
|
164
189
|
━━━ Your store is set up ━━━
|
|
@@ -168,13 +193,13 @@ Print a one-block recap after `/epicmerch-verify` finishes:
|
|
|
168
193
|
✓ Catalog: <N> products
|
|
169
194
|
✓ Storefront: scaffolded into <project> (or "skipped — no project")
|
|
170
195
|
✗ Buy Now button: skipped (or ✓ if they added it)
|
|
171
|
-
✓
|
|
196
|
+
✓ End-to-end test: product → cart → order → cleanup all passed
|
|
197
|
+
(or "✓ Read-path + payload checks passed; order test skipped —
|
|
198
|
+
E2E_TEST_PHONE not configured on server")
|
|
172
199
|
|
|
173
|
-
Next: restart your dev server (`npm run dev`) to pick up `.env
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
about analytics, notifications, or abandoned-cart messages whenever
|
|
177
|
-
you're ready.
|
|
200
|
+
Next: restart your dev server (`npm run dev`) to pick up `.env`. Your
|
|
201
|
+
store works end-to-end. Ask me about analytics, notifications, or
|
|
202
|
+
abandoned-cart messages whenever you're ready.
|
|
178
203
|
```
|
|
179
204
|
|
|
180
205
|
---
|
|
@@ -197,6 +222,7 @@ The merchant said something specific. Match against the table below, pick the cl
|
|
|
197
222
|
| "show me my store / products / orders / sales", "manage my store from chat" | `/epicmerch-merchant` (first-time) or `/epicmerch-merchant-agent` (ongoing) |
|
|
198
223
|
| "migrate from Shopify", "import my Shopify catalog" | `/epicmerch-migrate` |
|
|
199
224
|
| "my login isn't sticking", "products aren't loading", "checkout fails", "insufficient stock undefined", "something's broken" | `/epicmerch-debug` |
|
|
225
|
+
| "test everything", "does checkout actually work", "full end-to-end test", "run a real order test" | `/epicmerch-e2e` |
|
|
200
226
|
|
|
201
227
|
### Hand-off (or inline)
|
|
202
228
|
|
|
@@ -222,11 +248,13 @@ If the fetch fails, fall back to the high-level intent using the MCP tools you h
|
|
|
222
248
|
|
|
223
249
|
Briefly recap and ask if anything else is needed. One short sentence, not three follow-ups.
|
|
224
250
|
|
|
251
|
+
**If the sub-flow built or changed the storefront** (`/epicmerch-storefront`, `/epicmerch-orders`, `/epicmerch-products`, `/epicmerch-magic-checkout`, or a custom build), run the **Definition of Done** gate above before recapping — `/epicmerch-e2e` (or `/epicmerch-verify` fallback). Don't say "done" on anything that touched checkout without testing it live. (Pure config sub-flows like Razorpay/Stripe setup don't need the storefront e2e, but a quick `merchant_diagnose` confirms they took.)
|
|
252
|
+
|
|
225
253
|
Examples:
|
|
226
254
|
|
|
227
255
|
> "Razorpay is now live on your store. Want a Buy Now button too?" *(routes to `/epicmerch-magic-checkout`)*
|
|
228
256
|
|
|
229
|
-
> "Storefront scaffolded. Restart your dev server (`npm run dev`) to load `.env`. Need anything else — payments, Shopify migration, analytics?"
|
|
257
|
+
> "Storefront scaffolded and end-to-end tested (product → cart → order all pass). Restart your dev server (`npm run dev`) to load `.env`. Need anything else — payments, Shopify migration, analytics?"
|
|
230
258
|
|
|
231
259
|
---
|
|
232
260
|
|
package/src/prompts/index.js
CHANGED
|
@@ -218,6 +218,19 @@ export const prompts = [
|
|
|
218
218
|
},
|
|
219
219
|
|
|
220
220
|
// ─── Verification ──────────────────────────────────────────────────────
|
|
221
|
+
{
|
|
222
|
+
name: 'epicmerch-e2e',
|
|
223
|
+
description: 'Full end-to-end smoke test against the LIVE API — creates a throwaway test product, adds it to a cart, creates an order (exercises stock reservation + Razorpay order creation), verifies every response, then deletes the product + cancels the order. Proves the entire merchant→customer pipeline works without charging money or leaving junk data. Deeper than /epicmerch-verify; needs a customer token (one OTP).',
|
|
224
|
+
handler: async () => ({
|
|
225
|
+
messages: [{
|
|
226
|
+
role: 'user',
|
|
227
|
+
content: {
|
|
228
|
+
type: 'text',
|
|
229
|
+
text: `Run the full EpicMerch end-to-end smoke test. Fetch https://api.epicmerch.in/api/skills/epicmerch-e2e.md if needed and follow every step: get a customer token via the real OTP login flow against the configured test phone (store_auth_send_otp + store_auth_verify_otp with E2E_TEST_PHONE / E2E_TEST_OTP — no SMS sent), create a throwaway test product (merchant_create_product), add it to cart (store_cart_add), get the cart and verify the shape (store_cart_get), create the order (store_order_create — exercises stock reservation + Razorpay order, does NOT charge), then ALWAYS clean up — cancel the order (store_order_cancel) and delete the test product (merchant_delete_product). Print the consolidated checklist at the end.`,
|
|
230
|
+
},
|
|
231
|
+
}],
|
|
232
|
+
}),
|
|
233
|
+
},
|
|
221
234
|
{
|
|
222
235
|
name: 'epicmerch-verify',
|
|
223
236
|
description: 'Smoke-test every storefront API the merchant\'s site loads — confirms products / categories / search / single product / checkout payload shape all work against the live server, BEFORE a real customer hits a 422 at checkout. Skips OTP (costs money) and real Razorpay charges (covers with a manual test-card recipe).',
|
package/src/tools/developer.js
CHANGED
|
@@ -7,6 +7,34 @@ const __dir = dirname(fileURLToPath(import.meta.url));
|
|
|
7
7
|
const readExample = (name) =>
|
|
8
8
|
readFileSync(join(__dir, '../resources/examples', `${name}.md`), 'utf-8');
|
|
9
9
|
|
|
10
|
+
// Base URL for direct customer-scoped calls (cart/order). These endpoints
|
|
11
|
+
// need BOTH the store's x-api-key (identifies the merchant/tenant) AND a
|
|
12
|
+
// customer JWT (identifies the shopper) — a different auth combo than the
|
|
13
|
+
// MCP merchant session carries — so the cart tools below take those
|
|
14
|
+
// explicitly and make a raw fetch. This hits the live endpoint EXACTLY as
|
|
15
|
+
// the storefront does, so the response shape they return is the real,
|
|
16
|
+
// authoritative one the scaffolded Cart.jsx / Checkout.jsx must match.
|
|
17
|
+
const API_BASE = (process.env.EPICMERCH_API_URL || 'https://api.epicmerch.in/api').replace(/\/$/, '');
|
|
18
|
+
|
|
19
|
+
async function customerCartFetch(path, { apiKey, customerToken, method = 'GET', body } = {}) {
|
|
20
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
21
|
+
if (apiKey) headers['x-api-key'] = apiKey;
|
|
22
|
+
if (customerToken) headers['Authorization'] = `Bearer ${customerToken}`;
|
|
23
|
+
const res = await fetch(`${API_BASE}${path}`, {
|
|
24
|
+
method,
|
|
25
|
+
headers,
|
|
26
|
+
...(body ? { body: JSON.stringify(body) } : {}),
|
|
27
|
+
});
|
|
28
|
+
let parsed = null;
|
|
29
|
+
try { parsed = await res.json(); } catch (_) { /* non-JSON body */ }
|
|
30
|
+
// Return BOTH the HTTP status and the parsed body so the assistant can
|
|
31
|
+
// see exactly what the storefront would receive — status code AND shape.
|
|
32
|
+
return { httpStatus: res.status, ok: res.ok, response: parsed };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const CUSTOMER_TOKEN_HELP =
|
|
36
|
+
'Get a customer token from your storefront: log in once (one OTP), then in DevTools → Application → Local Storage read `customerInfo` and copy its `.token` value.';
|
|
37
|
+
|
|
10
38
|
export function developerTools(client) {
|
|
11
39
|
return {
|
|
12
40
|
async store_get_config(_args) {
|
|
@@ -14,6 +42,97 @@ export function developerTools(client) {
|
|
|
14
42
|
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
15
43
|
},
|
|
16
44
|
|
|
45
|
+
// ── Live cart verification ──────────────────────────────────────────
|
|
46
|
+
// Use these to STOP GUESSING the cart contract. Add an item, fetch the
|
|
47
|
+
// cart, and read the real `response.cart` shape — then write Cart.jsx /
|
|
48
|
+
// Checkout.jsx to match it exactly. Returns { httpStatus, ok, response }.
|
|
49
|
+
|
|
50
|
+
async store_cart_add({ apiKey, customerToken, productId, qty = 1, variant } = {}) {
|
|
51
|
+
if (!apiKey || !customerToken || !productId) {
|
|
52
|
+
return { isError: true, content: [{ type: 'text', text: `store_cart_add needs apiKey + customerToken + productId. ${CUSTOMER_TOKEN_HELP}` }] };
|
|
53
|
+
}
|
|
54
|
+
const body = { productId, qty };
|
|
55
|
+
if (variant) body.variant = variant;
|
|
56
|
+
const result = await customerCartFetch('/customer/cart', { apiKey, customerToken, method: 'POST', body });
|
|
57
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
async store_cart_get({ apiKey, customerToken } = {}) {
|
|
61
|
+
if (!apiKey || !customerToken) {
|
|
62
|
+
return { isError: true, content: [{ type: 'text', text: `store_cart_get needs apiKey + customerToken. ${CUSTOMER_TOKEN_HELP}` }] };
|
|
63
|
+
}
|
|
64
|
+
const result = await customerCartFetch('/customer/cart', { apiKey, customerToken });
|
|
65
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
async store_cart_remove({ apiKey, customerToken, productId, variant } = {}) {
|
|
69
|
+
if (!apiKey || !customerToken || !productId) {
|
|
70
|
+
return { isError: true, content: [{ type: 'text', text: `store_cart_remove needs apiKey + customerToken + productId. ${CUSTOMER_TOKEN_HELP}` }] };
|
|
71
|
+
}
|
|
72
|
+
const qs = variant ? `?variant=${encodeURIComponent(variant)}` : '';
|
|
73
|
+
const result = await customerCartFetch(`/customer/cart/${productId}${qs}`, { apiKey, customerToken, method: 'DELETE' });
|
|
74
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
// ── Live OTP login (for exercising the REAL auth flow) ──────────────
|
|
78
|
+
// These hit /auth/otp/send + /auth/otp/verify exactly as the storefront
|
|
79
|
+
// Login.jsx does. Pair with the server's env-gated E2E_TEST_PHONE bypass
|
|
80
|
+
// to get a customer token through the genuine login path (no SMS cost)
|
|
81
|
+
// — catches login-flow bugs that merchant_create_test_session skips.
|
|
82
|
+
|
|
83
|
+
async store_auth_send_otp({ apiKey, phone } = {}) {
|
|
84
|
+
if (!apiKey || !phone) {
|
|
85
|
+
return { isError: true, content: [{ type: 'text', text: 'store_auth_send_otp needs apiKey + phone. For automated tests, use the server-configured E2E_TEST_PHONE so no real SMS is sent.' }] };
|
|
86
|
+
}
|
|
87
|
+
const result = await customerCartFetch('/auth/otp/send', { apiKey, method: 'POST', body: { phone } });
|
|
88
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
async store_auth_verify_otp({ apiKey, phone, otp, profile = {} } = {}) {
|
|
92
|
+
if (!apiKey || !phone || !otp) {
|
|
93
|
+
return { isError: true, content: [{ type: 'text', text: 'store_auth_verify_otp needs apiKey + phone + otp. Returns { token, user } on success — that token is the customer JWT for cart/order calls.' }] };
|
|
94
|
+
}
|
|
95
|
+
const result = await customerCartFetch('/auth/otp/verify', { apiKey, method: 'POST', body: { phone, otp, profile } });
|
|
96
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
// ── Live order verification ─────────────────────────────────────────
|
|
100
|
+
// store_order_create exercises the FULL checkout pipeline against the
|
|
101
|
+
// live API: stock reservation + order row + Razorpay order creation.
|
|
102
|
+
// It does NOT charge — the Razorpay order is created but no payment is
|
|
103
|
+
// captured (that needs the hosted widget + a real card). So the order
|
|
104
|
+
// sits UNPAID; its stock reservation expires automatically, or you can
|
|
105
|
+
// release it immediately with store_order_cancel. Use this to confirm
|
|
106
|
+
// the orders.create response shape (razorpayOrderId/razorpayKeyId/
|
|
107
|
+
// amount/currency/merchantName) end-to-end without spending a rupee.
|
|
108
|
+
|
|
109
|
+
async store_order_create({ apiKey, customerToken, orderItems, shippingAddress, paymentMethod = 'Razorpay', totalPrice } = {}) {
|
|
110
|
+
if (!apiKey || !customerToken || !Array.isArray(orderItems) || orderItems.length === 0) {
|
|
111
|
+
return { isError: true, content: [{ type: 'text', text: `store_order_create needs apiKey + customerToken + a non-empty orderItems array. Each item: { productId, name, image, qty, price, variant }. ${CUSTOMER_TOKEN_HELP}` }] };
|
|
112
|
+
}
|
|
113
|
+
const body = { orderItems, shippingAddress, paymentMethod, totalPrice };
|
|
114
|
+
const result = await customerCartFetch('/customer/orders', { apiKey, customerToken, method: 'POST', body });
|
|
115
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
async store_order_get({ apiKey, customerToken, orderId } = {}) {
|
|
119
|
+
if (!apiKey || !customerToken || !orderId) {
|
|
120
|
+
return { isError: true, content: [{ type: 'text', text: `store_order_get needs apiKey + customerToken + orderId. ${CUSTOMER_TOKEN_HELP}` }] };
|
|
121
|
+
}
|
|
122
|
+
const result = await customerCartFetch(`/customer/orders/${orderId}`, { apiKey, customerToken });
|
|
123
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
async store_order_cancel({ apiKey, customerToken, orderId } = {}) {
|
|
127
|
+
if (!apiKey || !customerToken || !orderId) {
|
|
128
|
+
return { isError: true, content: [{ type: 'text', text: `store_order_cancel needs apiKey + customerToken + orderId. ${CUSTOMER_TOKEN_HELP}` }] };
|
|
129
|
+
}
|
|
130
|
+
// PUT /customer/orders/:id/cancel — releases the stock reservation
|
|
131
|
+
// on an unpaid order. Use this to clean up after an E2E test order.
|
|
132
|
+
const result = await customerCartFetch(`/customer/orders/${orderId}/cancel`, { apiKey, customerToken, method: 'PUT' });
|
|
133
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
134
|
+
},
|
|
135
|
+
|
|
17
136
|
async store_list_products({ category, keyword, sort, page, limit } = {}) {
|
|
18
137
|
const p = new URLSearchParams();
|
|
19
138
|
if (category) p.set('type', category);
|
|
@@ -62,4 +181,12 @@ export const developerToolDefs = [
|
|
|
62
181
|
{ name: 'store_search_products', description: 'Search products by keyword.', schema: { query: { type: 'string' }, category: { type: 'string' }, page: { type: 'number' }, limit: { type: 'number' } } },
|
|
63
182
|
{ name: 'store_list_categories', description: 'List all visible product categories.', schema: {} },
|
|
64
183
|
{ name: 'store_get_integration_guide', description: 'Get code examples for integrating a specific EpicMerch module. module: auth | products | orders | payments', schema: { module: { type: 'string' } } },
|
|
184
|
+
{ name: 'store_cart_add', description: 'Add an item to a customer cart on the LIVE store, to verify the real cart contract before writing Cart.jsx/Checkout.jsx. Returns { httpStatus, ok, response }. Requires apiKey (store public key) + customerToken (a customer JWT from a storefront login — localStorage.customerInfo.token) + productId (+ optional qty, variant). Pair with store_cart_get to see how the added item comes back.', schema: { apiKey: { type: 'string' }, customerToken: { type: 'string' }, productId: { type: 'string' }, qty: { type: 'number' }, variant: { type: 'string' } } },
|
|
185
|
+
{ name: 'store_cart_get', description: 'Fetch a customer cart from the LIVE store. Returns { httpStatus, ok, response }. Inspect response.cart to read the EXACT line-item shape your Cart.jsx must parse — { product: { _id, name, price, originalPrice, salePrice, image, type }, qty, variant }. Stops you guessing the shape from docs. Requires apiKey + customerToken.', schema: { apiKey: { type: 'string' }, customerToken: { type: 'string' } } },
|
|
186
|
+
{ name: 'store_cart_remove', description: 'Remove an item from a customer cart on the LIVE store (clean up after a verification add). Requires apiKey + customerToken + productId (+ optional variant to disambiguate sizes).', schema: { apiKey: { type: 'string' }, customerToken: { type: 'string' }, productId: { type: 'string' }, variant: { type: 'string' } } },
|
|
187
|
+
{ name: 'store_auth_send_otp', description: 'Send an OTP via the LIVE store auth flow (POST /auth/otp/send) — exercises the REAL login path. For automated tests, use a phone configured as the server\'s E2E_TEST_PHONE so no real SMS is sent (the server seeds the test code instead). Requires apiKey + phone.', schema: { apiKey: { type: 'string' }, phone: { type: 'string' } } },
|
|
188
|
+
{ name: 'store_auth_verify_otp', description: 'Verify an OTP via the LIVE store auth flow (POST /auth/otp/verify) and get a customer token. Returns { httpStatus, ok, response } where response.token is the customer JWT to use with store_cart_* / store_order_*. With the E2E_TEST_PHONE bypass, pass the configured E2E_TEST_OTP. Requires apiKey + phone + otp.', schema: { apiKey: { type: 'string' }, phone: { type: 'string' }, otp: { type: 'string' }, profile: { type: 'object' } } },
|
|
189
|
+
{ name: 'store_order_create', description: 'Create an order on the LIVE store to verify the full checkout pipeline (stock reservation + order + Razorpay order creation). Does NOT charge — the order is UNPAID and its reservation auto-expires (or cancel it with store_order_cancel). Returns { httpStatus, ok, response } — confirm response has orderId, razorpayOrderId, razorpayKeyId, amount, currency, merchantName. Requires apiKey + customerToken + orderItems [{ productId, name, image, qty, price, variant }] + shippingAddress + totalPrice.', schema: { apiKey: { type: 'string' }, customerToken: { type: 'string' }, orderItems: { type: 'array' }, shippingAddress: { type: 'object' }, paymentMethod: { type: 'string' }, totalPrice: { type: 'number' } } },
|
|
190
|
+
{ name: 'store_order_get', description: 'Fetch a customer order by id from the LIVE store. Returns { httpStatus, ok, response }. Requires apiKey + customerToken + orderId.', schema: { apiKey: { type: 'string' }, customerToken: { type: 'string' }, orderId: { type: 'string' } } },
|
|
191
|
+
{ name: 'store_order_cancel', description: 'Cancel an unpaid order on the LIVE store — releases its stock reservation. Use to clean up after an E2E test order. Requires apiKey + customerToken + orderId.', schema: { apiKey: { type: 'string' }, customerToken: { type: 'string' }, orderId: { type: 'string' } } },
|
|
65
192
|
];
|
package/src/tools/merchant.js
CHANGED
|
@@ -426,10 +426,24 @@ export function merchantTools(client, session) {
|
|
|
426
426
|
hasLogo: Boolean(profile?.logo ?? profile?.user?.logo),
|
|
427
427
|
} : { error: profileR.error };
|
|
428
428
|
|
|
429
|
+
// Total available stock for a product. The Prisma API returns `stock`
|
|
430
|
+
// (NOT the legacy Mongo field `countInStock` — reading that made EVERY
|
|
431
|
+
// product look out-of-stock). For products with variants, the true
|
|
432
|
+
// availability is the sum of per-variant stock; the server derives
|
|
433
|
+
// `stock` from that on write, but we sum defensively in case the
|
|
434
|
+
// top-level value is stale. Falls back to countInStock only for very
|
|
435
|
+
// old API shapes.
|
|
436
|
+
const productStock = (p) => {
|
|
437
|
+
if (Array.isArray(p.variants) && p.variants.length > 0) {
|
|
438
|
+
return p.variants.reduce((sum, v) => sum + (Number(v.stock) || 0), 0);
|
|
439
|
+
}
|
|
440
|
+
return Number(p.stock ?? p.countInStock ?? 0) || 0;
|
|
441
|
+
};
|
|
442
|
+
|
|
429
443
|
const catalog = productsR.ok && categoriesR.ok ? {
|
|
430
444
|
productCount: products.length,
|
|
431
445
|
categoryCount: cats.length,
|
|
432
|
-
outOfStockCount: products.filter((p) => (p
|
|
446
|
+
outOfStockCount: products.filter((p) => productStock(p) === 0).length,
|
|
433
447
|
} : { error: (productsR.error || categoriesR.error) };
|
|
434
448
|
|
|
435
449
|
const payment = checkoutR.ok ? {
|