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 +22 -0
- package/package.json +1 -1
- package/skills/epicmerch-auth.md +38 -4
- package/skills/epicmerch-debug.md +228 -0
- package/skills/epicmerch-e2e.md +203 -0
- package/skills/epicmerch-orders.md +17 -11
- package/skills/epicmerch-products.md +17 -15
- package/skills/epicmerch-storefront.md +85 -30
- package/skills/epicmerch-verify.md +274 -0
- package/skills/epicmerch.md +258 -85
- package/src/index.js +19 -1
- package/src/install.js +101 -1
- package/src/prompts/index.js +43 -0
- package/src/resources/examples/orders.md +90 -27
- package/src/resources/examples/payments.md +64 -23
- package/src/resources/sdk-guide.md +69 -34
- package/src/tools/developer.js +127 -0
- package/src/tools/merchant.js +36 -12
- package/src/tools/scaffold.js +83 -30
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
package/skills/epicmerch-auth.md
CHANGED
|
@@ -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
|
|
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
|
-
|
|
66
|
-
|
|
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
|
|
37
|
-
//
|
|
38
|
-
//
|
|
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
|
|
109
|
-
// - `
|
|
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
|
-
|
|
117
|
-
name:
|
|
118
|
-
image:
|
|
119
|
-
qty:
|
|
120
|
-
price:
|
|
121
|
-
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
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
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
|
-
{
|
|
53
|
-
<div className="
|
|
54
|
-
{
|
|
54
|
+
{hasVariants && (
|
|
55
|
+
<div className="variant-picker">
|
|
56
|
+
{availableVariants.map(v => (
|
|
55
57
|
<button
|
|
56
|
-
key={v.
|
|
58
|
+
key={v.variant}
|
|
57
59
|
type="button"
|
|
58
|
-
onClick={() =>
|
|
59
|
-
className={
|
|
60
|
+
onClick={() => setVariant(v.variant)}
|
|
61
|
+
className={variant === v.variant ? 'selected' : ''}
|
|
60
62
|
>
|
|
61
|
-
{v.
|
|
63
|
+
{v.variant}
|
|
62
64
|
</button>
|
|
63
65
|
))}
|
|
64
66
|
</div>
|
|
65
67
|
)}
|
|
66
68
|
<button
|
|
67
|
-
onClick={() => onAddToCart(product._id,
|
|
68
|
-
disabled={
|
|
69
|
+
onClick={() => onAddToCart(product._id, variant)}
|
|
70
|
+
disabled={hasVariants && !variant}
|
|
69
71
|
>
|
|
70
72
|
Add to Cart
|
|
71
73
|
</button>
|