epicmerch-mcp 1.3.2 → 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.2",
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",
@@ -22,9 +22,23 @@ export const store = new EpicMerch({
22
22
  apiKey: import.meta.env.VITE_API_KEY,
23
23
  ...(import.meta.env.VITE_API_URL && { baseUrl: import.meta.env.VITE_API_URL }),
24
24
  });
25
+
26
+ // Restore the customer session on page load — CRITICAL for OTP login to
27
+ // actually stick. Without this, the merchant logs in successfully, the
28
+ // token gets saved to localStorage, but the SDK in memory stays
29
+ // unauthenticated. The very next render shows them as logged-out again.
30
+ if (typeof window !== 'undefined') {
31
+ try {
32
+ const saved = localStorage.getItem('customerInfo');
33
+ if (saved) {
34
+ const parsed = JSON.parse(saved);
35
+ if (parsed?.token) store.setCustomerToken(parsed.token);
36
+ }
37
+ } catch (_) { /* localStorage unavailable — silent fallback */ }
38
+ }
25
39
  ```
26
40
 
27
- (For Next.js projects use `process.env.NEXT_PUBLIC_API_KEY` / `NEXT_PUBLIC_API_URL` instead.)
41
+ (For Next.js projects use `process.env.NEXT_PUBLIC_API_KEY` / `NEXT_PUBLIC_API_URL` instead. The session-restore block stays the same — the `typeof window !== 'undefined'` guard prevents SSR crashes.)
28
42
 
29
43
  ## Step 3: Login component
30
44
 
@@ -47,6 +61,8 @@ export default function Login({ onLogin }) {
47
61
  const verify = async () => {
48
62
  const result = await store.auth.verifyOtp(phone, otp, {});
49
63
  if (result.token) {
64
+ // SDK already sets the token internally during verifyOtp; mirror to
65
+ // localStorage so src/lib/epicmerch.js can restore on next page load.
50
66
  store.setCustomerToken(result.token);
51
67
  localStorage.setItem('customerInfo', JSON.stringify(result));
52
68
  onLogin(result);
@@ -57,13 +73,31 @@ export default function Login({ onLogin }) {
57
73
  <div>
58
74
  {step === 'phone' ? (
59
75
  <>
60
- <input value={phone} onChange={e => setPhone(e.target.value)} placeholder="+919876543210" />
76
+ <input
77
+ type="tel"
78
+ value={phone}
79
+ onChange={e => setPhone(e.target.value)}
80
+ placeholder="+919876543210"
81
+ autoComplete="tel"
82
+ />
61
83
  <button onClick={sendOtp}>Send OTP</button>
62
84
  </>
63
85
  ) : (
64
86
  <>
65
- <input value={otp} onChange={e => setOtp(e.target.value)} placeholder="Enter OTP" />
66
- <button onClick={verify}>Verify</button>
87
+ {/* EpicMerch OTPs are 4 digits. inputMode='numeric' pops the
88
+ numeric keypad on mobile; autoComplete='one-time-code' lets
89
+ iOS auto-fill from SMS without leaving the page. */}
90
+ <input
91
+ type="tel"
92
+ inputMode="numeric"
93
+ maxLength={4}
94
+ autoComplete="one-time-code"
95
+ value={otp}
96
+ onChange={e => setOtp(e.target.value.replace(/\D/g, '').slice(0, 4))}
97
+ placeholder="4-digit OTP"
98
+ autoFocus
99
+ />
100
+ <button onClick={verify} disabled={otp.length !== 4}>Verify</button>
67
101
  </>
68
102
  )}
69
103
  </div>
@@ -0,0 +1,228 @@
1
+ ---
2
+ name: epicmerch-debug
3
+ description: Diagnose and fix a broken EpicMerch storefront. Walks through the four most common failure modes — OTP login that "works" but doesn't stick across reloads, products not loading on the site, cart returns 401, INSUFFICIENT_STOCK on checkout — runs targeted checks, and routes to a specific fix.
4
+ ---
5
+
6
+ # EpicMerch Debug
7
+
8
+ You are debugging a storefront that isn't behaving. Don't ask the merchant
9
+ to describe what's wrong in detail — most issues map to a known failure
10
+ mode. Walk through the four checks below in order. The first one that
11
+ hits is almost always the root cause.
12
+
13
+ ## Pre-flight: gather context
14
+
15
+ Before checking anything, get the basics in one parallel batch:
16
+
17
+ 1. Read the project's `.env` (or `.env.local`) and grab `VITE_API_KEY` /
18
+ `NEXT_PUBLIC_API_KEY`. If neither exists, that's the first problem —
19
+ route to `/epicmerch` step 1 to generate one.
20
+ 2. Read `src/lib/epicmerch.js` (or wherever the SDK is initialised). Note
21
+ whether it has the `setCustomerToken` restoration block from
22
+ localStorage (see Check 1 below — if it's missing, that's the cause).
23
+ 3. Call `merchant_get_settings` to confirm the merchant's session is still
24
+ live and the API key actually belongs to this merchant.
25
+ 4. Call `merchant_get_checkout_settings` to confirm a payment processor
26
+ is configured.
27
+
28
+ Print a 4-line summary, then dive into the relevant check.
29
+
30
+ ---
31
+
32
+ ## Check 1 — "I logged in but nothing happened" / "OTP works but doesn't persist"
33
+
34
+ **Symptom.** Merchant enters phone, gets OTP, types OTP, the success
35
+ state briefly flashes — then on next page render OR after a page reload,
36
+ the storefront thinks they're logged out again. Cart returns 401. Order
37
+ button is disabled. They re-login, same thing.
38
+
39
+ **Cause.** The EpicMerch SDK holds the customer token in memory only.
40
+ `store.auth.verifyOtp()` calls `store.setCustomerToken()` internally,
41
+ which is why the first session works — but the token isn't restored from
42
+ localStorage on subsequent SDK initialisations. Login.jsx writes
43
+ `customerInfo` to localStorage; the SDK never reads it back.
44
+
45
+ **Diagnosis.** Read `src/lib/epicmerch.js`. If you do NOT see a block
46
+ like this somewhere after the `new EpicMerch(...)` call:
47
+
48
+ ```js
49
+ if (typeof window !== 'undefined') {
50
+ try {
51
+ const saved = localStorage.getItem('customerInfo');
52
+ if (saved) {
53
+ const parsed = JSON.parse(saved);
54
+ if (parsed?.token) store.setCustomerToken(parsed.token);
55
+ }
56
+ } catch (_) {}
57
+ }
58
+ ```
59
+
60
+ …that's the bug. Confirm by asking the merchant to open DevTools →
61
+ Application → Local Storage, expand their site origin, and check that
62
+ `customerInfo` IS there (it should be — Login.jsx writes it). If the
63
+ key exists but the SDK doesn't know about it, it's this exact bug.
64
+
65
+ **Fix.** Patch `src/lib/epicmerch.js` to add the restoration block
66
+ (verbatim from above, right after `export const store = ...`). Tell the
67
+ merchant to refresh — they should still be logged in from their previous
68
+ session.
69
+
70
+ If they want to log out properly, the matching cleanup is:
71
+
72
+ ```js
73
+ store.clearCustomerToken();
74
+ localStorage.removeItem('customerInfo');
75
+ ```
76
+
77
+ ---
78
+
79
+ ## Check 2 — Products don't load on the site
80
+
81
+ **Symptom.** Storefront renders, but the product grid is empty.
82
+ DevTools → Network shows the call to `/api/products` either 401-ing,
83
+ 403-ing, or returning an empty array.
84
+
85
+ **Cause.** One of three:
86
+ - **401**: invalid or revoked API key — `VITE_API_KEY` in `.env` doesn't
87
+ match any active key on the server.
88
+ - **403**: domain isn't in `merchant.allowedDomains` — the CORS / origin
89
+ check is rejecting the request.
90
+ - **200 with empty `products` array**: API key is fine, but the
91
+ merchant's catalog is genuinely empty.
92
+
93
+ **Diagnosis.**
94
+
95
+ 1. Open DevTools → Network, filter for `/products`, click the request.
96
+ Note the response status.
97
+ 2. Compare the `x-api-key` header value to the merchant's actual key:
98
+ `merchant_list_api_keys()` returns the canonical list. If it's masked,
99
+ generate a fresh one with `merchant_generate_api_key({ name: "Debug" })`
100
+ and use that value in `.env`.
101
+ 3. Note `window.location.origin`. Call `merchant_get_settings()` and
102
+ compare against `merchant.allowedDomains`. If the storefront's origin
103
+ isn't listed, that's the 403.
104
+ 4. Call `merchant_list_products({ limit: 1 })` to confirm the catalog
105
+ actually has products.
106
+
107
+ **Fix.**
108
+ - **Bad API key**: regenerate via `merchant_generate_api_key`, paste the
109
+ new value into `.env`, restart the dev server (`npm run dev`) so Vite
110
+ picks up the new env var.
111
+ - **Domain not allowed**: `merchant_add_allowed_domain({ domain: '<origin>' })`,
112
+ then refresh.
113
+ - **Empty catalog**: route to `/epicmerch` step 3 to add products.
114
+
115
+ ---
116
+
117
+ ## Check 3 — "INSUFFICIENT_STOCK: undefined: need 1, have 0" on checkout
118
+
119
+ **Symptom.** Adding to cart works. Cart shows items. Place Order returns
120
+ HTTP 400 with `{ code: 'INSUFFICIENT_STOCK', message: 'undefined: need 1, have 0' }`.
121
+
122
+ **Cause.** Almost always one of these mismatches between what the
123
+ storefront sends and what the server expects in `orderItems[]`:
124
+
125
+ - The line item uses `productId` field but it resolves to `undefined`
126
+ (variant lookup fails because the size string in `orderItems[].variant`
127
+ doesn't match any row in `product_variants`).
128
+ - The product genuinely has 0 stock for that variant.
129
+ - An old scaffold (pre-1.3.3) sent `orderItems[].productId` as
130
+ `undefined` because it read `v.size` on a variant object where the
131
+ field is actually named `v.variant`.
132
+
133
+ **Diagnosis.**
134
+
135
+ 1. Read `src/components/ProductCard.jsx`. Look for either:
136
+ - `availableSizes.map(v => v.size)` — old, broken (returns undefined)
137
+ - `availableVariants.map(v => v.variant)` — correct
138
+ 2. Read `src/components/Checkout.jsx`. Look at the `orderItems` map. The
139
+ product field should be `productId: i.product._id` (canonical) and
140
+ the variant field should be `variant: i.variant` (a string).
141
+ 3. From the merchant's perspective, open Cart, note the variant
142
+ labels visible. Open DevTools → Application → Local Storage →
143
+ `customerInfo`. Try `store.cart.get()` in the console and inspect
144
+ the result — every line item should have a `variant` string that
145
+ matches an entry in the product's `product_variants` table.
146
+
147
+ **Fix.**
148
+ - If ProductCard reads `v.size` → re-run `/epicmerch-storefront` to
149
+ refresh the scaffold from the 1.3.3+ contract.
150
+ - If Checkout sends `product:` instead of `productId:` in orderItems
151
+ → same, re-scaffold.
152
+ - If the variant truly isn't in the database → the product has stock=0
153
+ for that size, or the variant was never created. Have the merchant
154
+ call `merchant_get_product({ id })` and inspect `.variants`. If
155
+ needed, `merchant_update_product({ id, variants: [...] })` to fix.
156
+
157
+ ---
158
+
159
+ ## Check 4 — Add to cart returns 401 / 403
160
+
161
+ **Symptom.** Customer is "logged in" (UI shows their phone/name), but
162
+ clicking Add to Cart fails. Network shows 401 or 403 on POST `/customer/cart`.
163
+
164
+ **Cause.** The Authorization header isn't being sent. Either:
165
+ - Token was never restored on this page load (Check 1).
166
+ - Token expired (30-minute lifetime by default; SDK can refresh, but the
167
+ merchant might have been idle).
168
+ - Login.jsx didn't actually save the token (look for typo).
169
+
170
+ **Diagnosis.**
171
+
172
+ 1. In the console: `localStorage.getItem('customerInfo')` — is it
173
+ present? Is the `token` field a non-empty string?
174
+ 2. In the console: `store.getCustomerToken?.()` (if exposed) or check
175
+ that subsequent API calls include the Authorization header.
176
+ 3. Call `store.auth.getSession()` — if it returns null, the token has
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.
189
+
190
+ **Fix.**
191
+ - If localStorage is empty: merchant needs to log in again.
192
+ - If the token is there but the SDK doesn't have it: apply Check 1's
193
+ restoration block.
194
+ - If `getSession()` returns null: `store.auth.refreshToken()` — if
195
+ that fails, log out (clear customerInfo) and log in again.
196
+
197
+ ---
198
+
199
+ ## Wrap-up
200
+
201
+ After running through the four checks, print a one-line summary:
202
+
203
+ ```
204
+ ✓ Check 1 — Session restore localStorage block present in src/lib/epicmerch.js
205
+ ✗ Check 2 — Products load API key in .env didn't match any active key — regenerated
206
+ - Check 3 — Order shape skipped (cart works)
207
+ - Check 4 — Cart auth skipped (cart works)
208
+
209
+ Fix applied: rewrote .env with the new API key. Restart `npm run dev`
210
+ to pick up the change.
211
+ ```
212
+
213
+ If everything checks out and the merchant still has an issue, ask them
214
+ to paste the EXACT error message + the response body from DevTools →
215
+ Network. That usually pins it. Don't speculate — get the literal error.
216
+
217
+ ## When to escalate
218
+
219
+ If none of the 4 checks fit the symptom, fall through to
220
+ `merchant_diagnose` for a server-side health snapshot, then route based
221
+ on what it reports:
222
+
223
+ - "Razorpay not configured" → `/epicmerch-razorpay`
224
+ - "No products" → `/epicmerch` step 3 or `/epicmerch-migrate`
225
+ - "No API key" → `/epicmerch` step 1
226
+ - "Domain not allowed" → `merchant_add_allowed_domain`
227
+ - Anything else → surface the diagnostic output to the merchant
228
+ verbatim and let them decide.
@@ -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.
@@ -33,9 +33,12 @@ import { useEffect, useState } from 'react';
33
33
  import { store } from '../lib/epicmerch';
34
34
 
35
35
  export default function Cart({ onCheckout }) {
36
- // store.cart.get() returns { cart, cartCount, cartTotal } — the cart line
37
- // items are on `cart.cart`, NOT `cart.items`. Each line item is shaped
38
- // { product: { _id, name, price, salePrice, images, ... }, qty, variant }.
36
+ // store.cart.get() returns { cart: [...] } — JUST one field. The SDK
37
+ // README mentions cartCount/cartTotal but those aren't actually sent by
38
+ // the server; compute them locally. Each line item is shaped
39
+ // { product: { _id, name, price, originalPrice, salePrice, image, type },
40
+ // qty, variant }. Note `product.price` on cart items is ALREADY the
41
+ // effective price (sale if on sale, else regular).
39
42
  const [cart, setCart] = useState({ cart: [] });
40
43
 
41
44
  useEffect(() => { store.cart.get().then(setCart); }, []);
@@ -105,20 +108,23 @@ export default function Checkout({ cart, onSuccess }) {
105
108
  await loadRazorpay();
106
109
 
107
110
  // Map cart line items to the orderItems shape the API expects.
108
- // Critical fields per the SDK README:
109
- // - `product` (NOT `productId`) — the product _id
111
+ // Critical fields per the SDK contract:
112
+ // - `productId` — the product _id (same field name as cart.add,
113
+ // magic-checkout-init, analytics.track; consistent
114
+ // across every endpoint). Server still accepts the
115
+ // legacy `product` field but `productId` is canonical.
110
116
  // - `name`, `image` — required, the server doesn't re-fetch them
111
117
  // - `variant` — the size STRING, must match what was added to cart
112
118
  // or the variant lookup returns undefined and you'll
113
119
  // see "INSUFFICIENT_STOCK: undefined: need 1, have 0"
114
120
  const items = cart.cart || [];
115
121
  const orderItems = items.map(i => ({
116
- product: i.product._id,
117
- name: i.product.name,
118
- image: i.product.images?.[0] || i.product.image,
119
- qty: i.qty,
120
- price: i.product.salePrice ?? i.product.price,
121
- variant: i.variant,
122
+ productId: i.product._id,
123
+ name: i.product.name,
124
+ image: i.product.images?.[0] || i.product.image,
125
+ qty: i.qty,
126
+ price: i.product.salePrice ?? i.product.price,
127
+ variant: i.variant,
122
128
  }));
123
129
  const total = orderItems.reduce((s, i) => s + i.price * i.qty, 0);
124
130
 
@@ -32,40 +32,42 @@ If `src/components/ProductCard.jsx` already exists, skip. Otherwise write:
32
32
  import { useState } from 'react';
33
33
 
34
34
  export default function ProductCard({ product, onAddToCart }) {
35
- // Variants are stock-tracked per-size. Only show sizes that are in stock
36
- // the API will reject orders for out-of-stock variants with
37
- // "INSUFFICIENT_STOCK: <size>: need N, have 0".
38
- const availableSizes = (product.variants || []).filter(v => v.stock > 0);
39
- const [size, setSize] = useState(availableSizes[0]?.size);
35
+ // ProductVariant rows are { variant: String, stock: Int } note the field
36
+ // is named `variant`, NOT `size`. The "variant" string is whatever the
37
+ // merchant configured (typically a size like 'S'/'M'/'L', but could be
38
+ // a color or material). Only show in-stock options — out-of-stock orders
39
+ // are rejected with "INSUFFICIENT_STOCK: <variant>: need N, have 0".
40
+ const availableVariants = (product.variants || []).filter(v => v.stock > 0);
41
+ const [variant, setVariant] = useState(availableVariants[0]?.variant);
40
42
 
41
43
  // Products on sale return both `price` (original) and `salePrice` (current).
42
44
  // Always display the current price the customer will actually pay.
43
45
  const price = product.salePrice ?? product.price;
44
46
  const image = product.images?.[0] || product.image;
45
- const needsSize = availableSizes.length > 0;
47
+ const hasVariants = availableVariants.length > 0;
46
48
 
47
49
  return (
48
50
  <div className="product-card">
49
51
  <img src={image} alt={product.name} />
50
52
  <h3>{product.name}</h3>
51
53
  <p>₹{price}</p>
52
- {needsSize && (
53
- <div className="size-picker">
54
- {availableSizes.map(v => (
54
+ {hasVariants && (
55
+ <div className="variant-picker">
56
+ {availableVariants.map(v => (
55
57
  <button
56
- key={v.size}
58
+ key={v.variant}
57
59
  type="button"
58
- onClick={() => setSize(v.size)}
59
- className={size === v.size ? 'selected' : ''}
60
+ onClick={() => setVariant(v.variant)}
61
+ className={variant === v.variant ? 'selected' : ''}
60
62
  >
61
- {v.size}
63
+ {v.variant}
62
64
  </button>
63
65
  ))}
64
66
  </div>
65
67
  )}
66
68
  <button
67
- onClick={() => onAddToCart(product._id, size)}
68
- disabled={needsSize && !size}
69
+ onClick={() => onAddToCart(product._id, variant)}
70
+ disabled={hasVariants && !variant}
69
71
  >
70
72
  Add to Cart
71
73
  </button>