epicmerch-mcp 1.3.6 → 1.3.12

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.12",
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`.
@@ -197,6 +197,7 @@ The merchant said something specific. Match against the table below, pick the cl
197
197
  | "show me my store / products / orders / sales", "manage my store from chat" | `/epicmerch-merchant` (first-time) or `/epicmerch-merchant-agent` (ongoing) |
198
198
  | "migrate from Shopify", "import my Shopify catalog" | `/epicmerch-migrate` |
199
199
  | "my login isn't sticking", "products aren't loading", "checkout fails", "insufficient stock undefined", "something's broken" | `/epicmerch-debug` |
200
+ | "test everything", "does checkout actually work", "full end-to-end test", "run a real order test" | `/epicmerch-e2e` |
200
201
 
201
202
  ### Hand-off (or inline)
202
203
 
@@ -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 ? {