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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "epicmerch-mcp",
3
- "version": "1.3.6",
3
+ "version": "1.3.13",
4
4
  "description": "MCP server for EpicMerch — integrates e-commerce into Claude and ChatGPT",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -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`.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: epicmerch
3
- description: EpicMerch umbrella — guided 6-step onboarding wizard for new merchants (ends with a full smoke test against the live API), 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).
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
- Before declaring victory, run an end-to-end smoke test to make sure
136
- the site can actually talk to the API. Catches bugs like wrong field
137
- names, missing variants, stale orderItems shape the kind of thing
138
- that would otherwise show up as a 422 only when a real customer hits
139
- checkout.
140
-
141
- > "Last step — let me run a smoke test to confirm everything works."
142
-
143
- Run `/epicmerch-verify` (or, if not in a slash-command client, fetch
144
- https://api.epicmerch.in/api/skills/epicmerch-verify.md and follow it).
145
- The verify skill does NOT send OTP and does NOT charge real money;
146
- it tests every read-only path and validates the checkout payload
147
- shape, then gives the merchant a 4-line manual recipe for the actual
148
- cart checkout Razorpay test-card flow.
149
-
150
- **If any check fails:** do NOT just print the failure and stop. Route
151
- to `/epicmerch-debug` automatically it has a four-check playbook for
152
- the most common failure modes (OTP not persisting, products not
153
- loading, INSUFFICIENT_STOCK on checkout, cart returning 401) and will
154
- diagnose + fix without further merchant intervention. After debug runs,
155
- re-run `/epicmerch-verify` to confirm the fix held.
156
-
157
- Only proceed to wrap-up when verify is fully green.
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 `/epicmerch-verify` finishes:
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
- Smoke test: all read paths + checkout shape pass
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`, then
174
- follow the manual recipe in Phase 4 of the verify report to test the
175
- final cart checkout Razorpay flow with the test card. Ask me
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
 
@@ -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).',
@@ -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
  ];
@@ -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.countInStock ?? 0) === 0).length,
446
+ outOfStockCount: products.filter((p) => productStock(p) === 0).length,
433
447
  } : { error: (productsR.error || categoriesR.error) };
434
448
 
435
449
  const payment = checkoutR.ok ? {