Skip to content

Commit bcd25e9

Browse files
committed
up
1 parent ec910b5 commit bcd25e9

1 file changed

Lines changed: 182 additions & 0 deletions

File tree

  • specs/002-harden-checkout-tenancy
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
# Phase 2 Tasks — Harden Checkout, Tenancy, Newsletter
2+
3+
Date: 2025-11-13
4+
Branch: 002-harden-checkout-tenancy
5+
6+
This plan breaks the specification into implementable tasks with owners, concrete file targets, validation criteria, and tests. Each task references FR/SC items from the spec. Follow project standards in .github/instructions/* and constitution limits (files <300 lines, functions <50 lines, no any, WCAG AA, multi-tenant isolation).
7+
8+
9+
## 0) Cross-cutting foundations
10+
11+
- T0.1 Add standardized API response helpers (FR-008, FR-021)
12+
- Files: `src/lib/api-response.ts` (new), wire-in in key routes
13+
- Implement success({ data, meta?, message? }), error({ code, message, details? })
14+
- Inject `X-Request-Id` header; expose helper to generate/propagate requestId
15+
- Tests: `tests/unit/api-response.test.ts`
16+
17+
- T0.2 Request context and correlation ID propagation (FR-021)
18+
- Files: `src/lib/request-context.ts` (new) using AsyncLocalStorage to store requestId, storeId
19+
- Adapter in proxy to seed requestId; server routes to pick it up
20+
- Tests: `tests/unit/request-context.test.ts`
21+
22+
- T0.3 Error class hierarchy and mapper (FR-008)
23+
- Files: `src/lib/errors.ts` (new) with typed AppError(code, httpStatus, details)
24+
- Mapper to JSON shape used by api-response
25+
- Tests: `tests/unit/errors.test.ts`
26+
27+
28+
## 1) Secure Checkout hardening (P1)
29+
30+
- T1.1 Enforce authentication and tenant scope in checkout endpoints (FR-001, FR-009)
31+
- Files: `src/app/api/checkout/complete/route.ts`, `src/app/api/checkout/validate/route.ts`
32+
- Use `getServerSession(authOptions)`, reject 401 when not authenticated
33+
- Read storeId from session.user.storeId; remove body storeId usage
34+
- Add `dynamic = 'force-dynamic'` where route reads headers/cookies
35+
- Tests: `tests/integration/checkout-auth.test.ts`
36+
37+
- T1.2 Ignore client monetary fields; recalc all totals server-side (FR-002)
38+
- Files: `src/app/api/checkout/complete/route.ts`, `src/services/checkout-service.ts`
39+
- Remove acceptance of price/subtotal/taxes in request (keep only items[productId, variantId, quantity], addresses, shippingMethod)
40+
- Ensure `validateCart` and total calculation drive amounts exclusively
41+
- Tests: update `src/services/__tests__/checkout-service.test.ts` and add `tests/integration/checkout-recalc.test.ts` (send tampered prices → server ignores)
42+
43+
- T1.3 Validate payment intent/token before order creation (FR-003)
44+
- Files: `src/services/payment-service.ts` (add `preValidatePaymentIntent(paymentIntentId|token, expectedAmount, currency)`),
45+
`src/app/api/checkout/complete/route.ts`
46+
- Flow: compute totals → call preValidate → on success, create order in tx and create payment record with AUTHORIZED/PAID as appropriate
47+
- Tests: `tests/unit/payment-prevalidate.test.ts` (mock Stripe), `tests/integration/checkout-payment-guard.test.ts`
48+
49+
- T1.4 Wrap order+items+inventory+payment record in one atomic transaction (FR-004)
50+
- Files: `src/services/checkout-service.ts` (ensure `$transaction` covers inventory decrement and payment record write)
51+
- Add compensating safeguards: if any write fails, no partial data persists
52+
- Tests: `tests/integration/checkout-atomicity.test.ts` (fail inventory → no order created)
53+
54+
- T1.5 Plan enforcement at checkout (optional guard)
55+
- Files: `src/lib/plan-enforcement.ts` (already exists) — verify check and call before order create; return 402 equivalent error if over limit
56+
- Tests: `tests/unit/plan-enforcement.checkout.test.ts`
57+
58+
59+
## 2) Multi-tenancy and canonical domains (P2)
60+
61+
- T2.1 Store resolution from host + canonical redirect (FR-005, FR-020)
62+
- Schema: add `StoreDomain` model
63+
- `id`, `storeId`, `domain` (unique), `isPrimary` (canonical), timestamps
64+
- Files: `prisma/schema.prisma` (model + `@@unique([domain])`), migration; seed example
65+
- Files: `src/lib/store-resolver.ts` (new): parse host, resolve `storeId` by domain OR `slug.subdomain`
66+
- Proxy: add canonical redirect: if request for subdomain and primary custom domain exists → 308 redirect
67+
- Tests: `tests/unit/store-resolver.test.ts`, `tests/integration/canonical-redirect.test.ts`
68+
69+
- T2.2 Remove hardcoded store IDs and enforce tenant scoping (FR-006, FR-009)
70+
- Sweep: search for `storeId: "..."` literals and replace with session context or resolver
71+
- Ensure Prisma middleware `registerMultiTenantMiddleware` is seeded via request context in API routes
72+
- Files: various services/routes; add minimal `setStoreIdContext()` calls in each API route’s handler entry
73+
- Tests: `tests/integration/tenant-isolation.test.ts` (cross-tenant access must fail)
74+
75+
- T2.3 Per-store dynamic metadata (FR-020)
76+
- Files: add `generateMetadata` in key storefront routes to pull store title/description/social image
77+
- Tests: `tests/integration/metadata-storefront.test.ts`
78+
79+
80+
## 3) Newsletter subscription (P2)
81+
82+
- T3.1 Server Action for subscribe (single opt-in + consent + audit) (FR-007, FR-019)
83+
- Files: `src/app/(storefront)/newsletter/actions.ts` (new, 'use server') with Zod validation
84+
- Write to `Newsletter` with `@@unique([storeId,email])`; respect DNT header (process essential only)
85+
- Create `ConsentRecord` row and `AuditLog` for subscription
86+
- Rate limit: call simple limiter (100 rpm/IP) per store/email
87+
- Tests: `tests/integration/newsletter-action.test.ts` (valid/invalid/duplicate/DNT)
88+
89+
- T3.2 Minimal API endpoint for integrations (optional)
90+
- Files: `src/app/api/subscriptions/route.ts` — wrapper to call server action for external clients
91+
- Tests: `tests/integration/newsletter-api.test.ts`
92+
93+
94+
## 4) API pipeline and consistency (P3)
95+
96+
- T4.1 Apply api-response and errors to selected routes (FR-008, FR-014, FR-015, FR-021)
97+
- Routes: orders, checkout, inventory, products (representative set)
98+
- Ensure `X-Request-Id` set on all responses; proxy seeds it if missing
99+
- Tests: `tests/integration/api-consistency.test.ts`
100+
101+
- T4.2 Proxy security headers/rate limit already present — extend to add requestId seeding
102+
- Files: `proxy.ts` (generate UUID v4 if header missing, store in ALS)
103+
- Tests: `tests/integration/request-id.test.ts`
104+
105+
106+
## 5) Caching and invalidation (P3)
107+
108+
- T5.1 Cache tag util + invalidation (FR-011)
109+
- Files: `src/lib/cache-tags.ts` (new): helpers for tag name construction
110+
- On create/update/delete in product/category/page services → call `revalidateTag(tag, 'max')`
111+
- Tests: `tests/unit/cache-tags.test.ts`
112+
113+
- T5.2 Document phased Cache Components plan (FR-012)
114+
- Files: `docs/cache-components-plan.md` (new) — enablement guardrails and rollout
115+
116+
117+
## 6) CSV export (P3)
118+
119+
- T6.1 Streaming for ≤10k, async for >10k (FR-016)
120+
- Files: `src/services/bulk-export-service.ts`, `src/app/api/orders/route.ts`
121+
- Implement row counting; if >10k, enqueue background job (existing job infra or stub) and notify via email + in-app `Notification`
122+
- Tests: `tests/integration/orders-export.test.ts` (mock dataset sizes)
123+
124+
125+
## 7) Dashboard and accessibility polish (P3)
126+
127+
- T7.1 Redirect unauthenticated dashboard access (FR-017)
128+
- Already handled via proxy; add e2e coverage
129+
- Tests: `tests/e2e/auth-redirect.spec.ts`
130+
131+
- T7.2 Charts/analytics accessible markup (FR-018)
132+
- Files: targeted pages/components under `src/app/(dashboard)`
133+
- Wrap charts in `<figure>`/`<figcaption>` with textual summary; ensure focus rings
134+
- Tests: axe-core scan `tests/integration/a11y-analytics.test.ts`
135+
136+
137+
## 8) Testing and coverage (P3)
138+
139+
- T8.1 Unit tests to ≥80% for services, 100% for utilities (FR-013)
140+
- Add/expand tests covering fraud scenarios, tenancy isolation, newsletter flows, standardized errors
141+
142+
- T8.2 E2E Playwright flows (checkout, tenancy, newsletter, export) (FR-013)
143+
- Files: `tests/e2e/checkout.spec.ts`, `tests/e2e/tenancy.spec.ts`, `tests/e2e/newsletter.spec.ts`, `tests/e2e/export.spec.ts`
144+
145+
146+
## Deliverables checklist (tie to Success Criteria)
147+
148+
- [ ] SC-001 Security review passes: no high-severity findings in checkout/tenancy
149+
- [ ] SC-002 Tenant isolation verified; no hardcoded store IDs
150+
- [ ] SC-003 Checkout E2E validates server-side pricing; failure paths abort cleanly
151+
- [ ] SC-004 Newsletter E2E: consent + audit + idempotency + rate limit + DNT
152+
- [ ] SC-005 Middleware instrumentation confirms tenant scoping
153+
- [ ] SC-006 Migrations apply cleanly; data integrity verified
154+
- [ ] SC-007 API responses standardized + X-Request-Id everywhere
155+
- [ ] SC-008 Cache invalidation proves timely updates
156+
- [ ] SC-009 Coverage thresholds hit; tests in place
157+
- [ ] SC-010 Perf & a11y budgets within thresholds
158+
- [ ] SC-011 CSV export: streamed ≤10k, async >10k with email + in-app
159+
- [ ] SC-012 Dashboard unauthenticated redirect verified
160+
- [ ] SC-013 Per-store metadata present and validated
161+
- [ ] SC-014 Analytics markup accessible
162+
163+
164+
## Execution order (safe incremental rollout)
165+
166+
1) T0 foundations (responses, request context, errors)
167+
2) T1 secure checkout (auth, recalc, payment pre-validate, atomic tx)
168+
3) T2 store resolver + canonical redirect + tenant scoping sweep
169+
4) T3 newsletter server action + endpoint
170+
5) T4 API consistency + requestId propagation in proxy
171+
6) T5 caching utilities + invalidations + docs
172+
7) T6 CSV export streaming/async
173+
8) T7 a11y polish
174+
9) T8 testing/coverage expansion
175+
176+
177+
## Notes and risks
178+
179+
- Payment pre-validation requires Stripe secret key; in dev/tests use mocks guarded by env fallbacks. Create `.env.local` placeholders if missing.
180+
- Adding StoreDomain model introduces a migration; coordinate seed data and dev db push. Ensure backwards-compatible fallbacks (use `Store.slug` for subdomain if no domain records).
181+
- Ensure Next.js 16 async params/searchParams and cookies/headers await semantics are preserved in any touched routes.
182+
- Respect constitution size limits by extracting helper functions to utilities; avoid monolith handlers.

0 commit comments

Comments
 (0)