Skip to content

Commit f28a305

Browse files
committed
up
1 parent ead0fae commit f28a305

5 files changed

Lines changed: 296 additions & 4 deletions

File tree

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
name: API Integration Gate
2+
3+
on:
4+
pull_request:
5+
branches:
6+
- main
7+
- '002-harden-checkout-tenancy'
8+
push:
9+
branches:
10+
- '002-harden-checkout-tenancy'
11+
12+
jobs:
13+
api-integration-gate:
14+
runs-on: ubuntu-latest
15+
steps:
16+
- name: Checkout
17+
uses: actions/checkout@v4
18+
19+
- name: Setup Node
20+
uses: actions/setup-node@v4
21+
with:
22+
node-version: '20'
23+
cache: 'npm'
24+
25+
- name: Install dependencies
26+
run: |
27+
npm install --no-audit --no-fund
28+
29+
- name: Prisma generate & db push (SQLite dev)
30+
run: |
31+
npx prisma generate
32+
npx prisma db push
33+
34+
- name: Type check
35+
run: |
36+
npm run type-check || (echo 'Type-check failed' && exit 1)
37+
38+
- name: API route integration test coverage gate
39+
run: |
40+
node scripts/check-api-integration-tests.mjs
41+
42+
- name: Run integration test suite
43+
run: |
44+
npx vitest run tests/integration --reporter verbose
45+
46+
- name: Summary
47+
if: always()
48+
run: |
49+
echo "API integration gate completed"
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
#!/usr/bin/env node
2+
/* eslint-disable no-console */
3+
/**
4+
* CI Gate: Ensure every API route under src/app/api/.../route.ts has at least one integration test.
5+
* Mapping heuristic:
6+
* - For each route file, derive its relative API path (segments before route.ts)
7+
* - Search tests/integration for any test file whose path contains all those segments in order.
8+
* - If none found, mark missing.
9+
* - Exit with non-zero code and JSON summary for missing routes.
10+
*
11+
* This keeps the rule flexible while enforcing coverage presence.
12+
*/
13+
import { promises as fs } from 'fs';
14+
import path from 'path';
15+
16+
const root = process.cwd();
17+
const apiRoot = path.join(root, 'src', 'app', 'api');
18+
const testsRoot = path.join(root, 'tests', 'integration');
19+
20+
async function walk(dir) {
21+
const entries = await fs.readdir(dir, { withFileTypes: true });
22+
const files = [];
23+
for (const e of entries) {
24+
const full = path.join(dir, e.name);
25+
if (e.isDirectory()) files.push(...await walk(full));
26+
else files.push(full);
27+
}
28+
return files;
29+
}
30+
31+
function segmentsForRoute(routeFile) {
32+
// routeFile: .../src/app/api/products/[id]/route.ts
33+
const rel = path.relative(apiRoot, path.dirname(routeFile));
34+
// e.g. products/[id]
35+
return rel.split(path.sep).filter(Boolean);
36+
}
37+
38+
function testMatchesSegments(testPath, segments) {
39+
const lowered = testPath.toLowerCase();
40+
return segments.every(seg => lowered.includes(seg.replace(/\[|\]/g, '').toLowerCase()));
41+
}
42+
43+
async function main() {
44+
// Guard if paths missing
45+
for (const required of [apiRoot, testsRoot]) {
46+
try { await fs.access(required); } catch { console.error(JSON.stringify({ error: `Missing required directory: ${required}` })); process.exit(1); }
47+
}
48+
const routeFiles = (await walk(apiRoot)).filter(f => f.endsWith(path.join('route.ts')));
49+
const testFiles = (await walk(testsRoot)).filter(f => /\.(test|spec)\.(ts|tsx|js|mjs)$/.test(f));
50+
51+
const missing = [];
52+
for (const rf of routeFiles) {
53+
const segs = segmentsForRoute(rf);
54+
const has = testFiles.some(tf => testMatchesSegments(tf, segs));
55+
if (!has) missing.push({ routeFile: path.relative(root, rf), segments: segs });
56+
}
57+
58+
const summary = { totalRoutes: routeFiles.length, missingCount: missing.length, missing };
59+
if (missing.length) {
60+
console.error(JSON.stringify(summary, null, 2));
61+
process.exit(2);
62+
} else {
63+
console.log(JSON.stringify(summary, null, 2));
64+
}
65+
}
66+
67+
main().catch(err => { console.error(JSON.stringify({ error: err.message, stack: err.stack }, null, 2)); process.exit(1); });
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
{
2+
"generatedAt": "2025-11-14T00:00:00.000Z",
3+
"description": "Schema inventory of JSON-encoded array/object fields stored as String requiring normalization to Json type for Harden Checkout, Tenancy, Newsletter (T038).",
4+
"fields": [
5+
{
6+
"model": "Product",
7+
"field": "images",
8+
"currentType": "String (JSON array of blob URLs)",
9+
"semantics": "ARRAY_STRING",
10+
"recommendedType": "Json",
11+
"migrationId": "T038b",
12+
"parseStrategy": "Try JSON.parse; if invalid or empty string, set []",
13+
"risk": "low",
14+
"tests": ["migration parses valid arrays", "empty string becomes []", "invalid JSON causes row flagged"]
15+
},
16+
{
17+
"model": "Product",
18+
"field": "metaKeywords",
19+
"currentType": "String (JSON array)",
20+
"semantics": "ARRAY_STRING",
21+
"recommendedType": "Json",
22+
"migrationId": "T038c",
23+
"parseStrategy": "JSON.parse or [] if falsy",
24+
"risk": "low",
25+
"tests": ["keywords preserved", "null/empty -> []"]
26+
},
27+
{
28+
"model": "ProductVariant",
29+
"field": "options",
30+
"currentType": "String (JSON object)",
31+
"semantics": "OBJECT",
32+
"recommendedType": "Json",
33+
"migrationId": "T038d",
34+
"parseStrategy": "JSON.parse; must be object else {}",
35+
"risk": "medium",
36+
"tests": ["object keys preserved", "non-object becomes {}"]
37+
},
38+
{
39+
"model": "ProductAttribute",
40+
"field": "values",
41+
"currentType": "String (JSON array)",
42+
"semantics": "ARRAY_STRING",
43+
"recommendedType": "Json",
44+
"migrationId": "T038e",
45+
"parseStrategy": "JSON.parse or []",
46+
"risk": "low",
47+
"tests": ["values preserved", "empty -> []"]
48+
},
49+
{
50+
"model": "Review",
51+
"field": "images",
52+
"currentType": "String (JSON array of blob URLs)",
53+
"semantics": "ARRAY_STRING",
54+
"recommendedType": "Json",
55+
"migrationId": "T038f",
56+
"parseStrategy": "JSON.parse or []",
57+
"risk": "low",
58+
"tests": ["images preserved", "empty -> []"]
59+
},
60+
{
61+
"model": "ShippingZone",
62+
"field": "countries",
63+
"currentType": "String (JSON array of ISO country codes)",
64+
"semantics": "ARRAY_STRING",
65+
"recommendedType": "Json",
66+
"migrationId": "T038g",
67+
"parseStrategy": "JSON.parse or []",
68+
"risk": "low",
69+
"tests": ["countries preserved", "invalid -> []"]
70+
},
71+
{
72+
"model": "Page",
73+
"field": "metaKeywords",
74+
"currentType": "String (JSON array)",
75+
"semantics": "ARRAY_STRING",
76+
"recommendedType": "Json",
77+
"migrationId": "T038h",
78+
"parseStrategy": "JSON.parse or []",
79+
"risk": "low",
80+
"tests": ["keywords preserved", "invalid -> []"]
81+
},
82+
{
83+
"model": "EmailTemplate",
84+
"field": "variables",
85+
"currentType": "String (JSON array)",
86+
"semantics": "ARRAY_STRING",
87+
"recommendedType": "Json",
88+
"migrationId": "T038i",
89+
"parseStrategy": "JSON.parse or []",
90+
"risk": "low",
91+
"tests": ["variables preserved", "invalid -> []"]
92+
},
93+
{
94+
"model": "Webhook",
95+
"field": "events",
96+
"currentType": "String (JSON array)",
97+
"semantics": "ARRAY_STRING",
98+
"recommendedType": "Json",
99+
"migrationId": "T038j",
100+
"parseStrategy": "JSON.parse or []",
101+
"risk": "low",
102+
"tests": ["events preserved", "invalid -> []"]
103+
},
104+
{
105+
"model": "Payment",
106+
"field": "metadata",
107+
"currentType": "String? (JSON gateway-specific)",
108+
"semantics": "OBJECT_OPTIONAL",
109+
"recommendedType": "Json?",
110+
"migrationId": "T038k",
111+
"parseStrategy": "If null skip; else JSON.parse; if invalid -> store validation error separately",
112+
"risk": "medium",
113+
"tests": ["metadata preserved", "invalid JSON flagged"]
114+
},
115+
{
116+
"model": "SyncLog",
117+
"field": "metadata",
118+
"currentType": "String? (JSON)",
119+
"semantics": "OBJECT_OPTIONAL",
120+
"recommendedType": "Json?",
121+
"migrationId": "T038l",
122+
"parseStrategy": "If null skip; else JSON.parse or {}",
123+
"risk": "low",
124+
"tests": ["preserves object", "null untouched"]
125+
},
126+
{
127+
"model": "AuditLog",
128+
"field": "changes",
129+
"currentType": "String? (JSON diff object)",
130+
"semantics": "OBJECT_OPTIONAL",
131+
"recommendedType": "Json?",
132+
"migrationId": "T038m",
133+
"parseStrategy": "If null skip; else JSON.parse or {}",
134+
"risk": "medium",
135+
"tests": ["diff object preserved", "invalid -> {}"]
136+
}
137+
],
138+
"summary": {
139+
"arrayFields": 9,
140+
"objectFields": 1,
141+
"optionalObjectFields": 3,
142+
"total": 13,
143+
"nextSteps": [
144+
"Generate Prisma migration altering field types from String to Json/Json?",
145+
"Write backfill script to parse existing data",
146+
"Add integration tests under tests/integration/migrations/json-fields.spec.ts",
147+
"Update application code to treat these fields as typed JSON instead of string",
148+
"Add validation guards when writing to these fields going forward"
149+
]
150+
}
151+
}

specs/002-harden-checkout-tenancy/spec.md

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,10 +112,11 @@ As an admin, I receive consistent API responses and can export orders as CSV; sm
112112

113113
- **FR-001**: Reject unauthenticated checkout attempts; require a valid signed-in session.
114114
- **FR-002**: Recalculate all line items, discounts, shipping, and taxes on the server; ignore client-submitted monetary values.
115-
- **FR-003**: Validate the provided payment intent/token with the payment processor prior to order creation; abort on validation failure.
115+
- **FR-003**: Validate the provided payment intent/token with the payment processor prior to order creation; abort on validation failure.
116116
- **FR-003**: Validate the provided payment intent/token with the payment processor prior to order creation; abort on validation failure.
117117
- Clarification: Payment pre-validation MUST verify that the payment intent/authorization exists and that the expected amount and currency match server-side recalculated totals. The system SHOULD accept processor states such as "AUTHORIZED" or equivalent pre-authorized states for later capture; the implementation MUST document which states are merely validated vs which will be captured as part of order finalization.
118-
- Idempotency & retries: Checkout submissions MUST include an idempotency key (client-provided or server-generated) to prevent duplicate captures. The payment adapter MUST implement safe retry/backoff semantics. In case of payment provider outages, the checkout flow MUST abort without creating an order or mutating inventory; a retryable error should be surfaced to the client and reconciliation attempted by background processes when appropriate.
118+
- Decision: Primary approach is provider-backed idempotency combined with server-side deduplication and mapping. The server MUST record the provider reference (provider idempotency key / intent id) and maintain a durable mapping to the platform's idempotency record to prevent duplicate captures. If a provider does not support idempotency keys, the server should accept a server-generated idempotency key as a fallback.
119+
- Idempotency & retries: Checkout submissions MUST include an idempotency key (provider-backed when available; otherwise server-generated) to prevent duplicate captures. The payment adapter MUST implement safe retry/backoff semantics and surface retryable errors to the client. In case of payment provider outages, the checkout flow MUST abort without creating an order or mutating inventory; a retryable error should be surfaced to the client and reconciliation attempted by background processes when appropriate. The implementation MUST document which provider states count as "validated" (e.g., AUTHORIZED) versus "captured" and include test cases for outage/retry scenarios.
119120
- **FR-004**: Execute order creation, order items, inventory decrement, discount usage mark, and payment record within a single atomic transaction.
120121
- **FR-005**: Resolve the active store from the request domain/subdomain and map it to a tenant identifier; return not-found if no mapping exists. When both a custom domain and subdomain exist, redirect subdomain traffic to the custom domain as the canonical host.
121122
- **FR-005**: Resolve the active store from the request domain/subdomain and map it to a tenant identifier; return not-found if no mapping exists. When both a custom domain and subdomain exist, redirect subdomain traffic to the custom domain as the canonical host.
@@ -132,7 +133,7 @@ As an admin, I receive consistent API responses and can export orders as CSV; sm
132133
- **FR-013**: Achieve and enforce test coverage thresholds (services ≥80%, utilities 100%) with tests for fraud scenarios, tenancy isolation, newsletter flows, and error mapping.
133134
- **FR-014**: Apply an API middleware pipeline covering authentication, rate limiting, request validation, and structured logging that includes a request correlation ID.
134135
- **FR-015**: Ensure REST semantics: PUT requires full resource payload; PATCH allows partial updates; no stray `success` flags; use standardized error payloads.
135-
- **FR-016**: Stream CSV exports for up to 10,000 rows within memory limits; for larger datasets, enqueue a background job and provide a downloadable link upon completion via both email and in-app notification.
136+
- **FR-016**: Stream CSV exports for up to 10,000 rows within explicit memory and time guardrails; for larger datasets, enqueue a background job and provide a downloadable link upon completion via both email and in-app notification.
136137
- **FR-017**: Redirect unauthenticated dashboard access to sign-in; remove any fallback tenant defaults; utilize progressive rendering patterns for lists.
137138
- **FR-018**: Make analytics and chart visualizations accessible with textual summaries or `<figure>`/`<figcaption>` structures; pass accessibility audits.
138139
- **FR-019**: Present a consent banner that stores per-store consent and honors the user’s Do Not Track preference by disabling non-essential tracking.
@@ -171,6 +172,7 @@ As an admin, I receive consistent API responses and can export orders as CSV; sm
171172
- **SC-008**: Cache invalidation test demonstrates timely updates on product/category/page changes.
172173
- **SC-009**: Code coverage thresholds met: ≥80% services, 100% utilities; new tests cover fraud, tenancy, newsletter, and error mapping.
173174
- **SC-010**: Performance and accessibility budgets pass automated checks (e.g., LCP < 2.5s mobile, CLS < 0.1, zero blocking accessibility violations for targeted pages).
175+
- **SC-010a**: API performance: p95 latency for representative interactive API endpoints ≤ 500ms under normal load (excluding background/job processing endpoints).
174176
- **SC-011**: CSV export streams complete within memory limits for ≤10k rows; >10k path enqueues background processing and delivers a downloadable link.
175177
- **SC-012**: Dashboard unauthenticated access redirects to sign-in; no fallback tenant default exists.
176178
- **SC-013**: Per-store metadata present and validated in snapshots (title, description, social previews); optional structured data validated when present.
@@ -190,3 +192,14 @@ As an admin, I receive consistent API responses and can export orders as CSV; sm
190192

191193
<!-- Clarifications resolved: single opt-in with audit trail; canonical redirect subdomain → custom domain; CSV link via both email and in-app notification. -->
192194

195+
## Clarifications
196+
197+
### Session 2025-11-14
198+
199+
- Q: Payment idempotency & pre-validation strategy → A: Option C - Provider-backed idempotency + server-side deduplication and mapping. Server stores provider reference (intent id / idempotency key) and a durable mapping to prevent duplicate captures; implement retry/backoff semantics; abort checkout and do not mutate inventory if provider unreachable. Implementation MUST document which provider states are "validated" (e.g., AUTHORIZED) vs which are "captured".
200+
- Q: Canonical redirect behavior for non-GET requests → A: Option B - Use HTTP 301 for storefront GET requests; for non-GET requests return a 4xx informative response (e.g., 409/422) with a `Link: <https://{primary-domain}{path}>; rel="canonical"` header and an explanatory body instructing clients to re-submit to the canonical host. Avoid method-changing redirects for non-GETs to preserve safety and make behavior explicit for clients.
201+
- Q: Newsletter consent & audit retention policy → A: Option B - Retain consent and audit records for 3 years. Consent and audit entries MUST be retained for 3 years from creation to support dispute resolution and marketing audit trails; include export tooling to support legal requests and provide an expiry/archival process that removes or anonymizes personal details after retention period unless a legal hold exists.
202+
- Q: CSV export threshold & guardrails → A: Option A - Keep 10,000 rows synchronous threshold with explicit memory/time caps and async fallback. Streaming must enforce per-request memory/time caps (e.g., 200MB heap, 120s) and fall back to async job when exceeded; tests must cover boundary conditions.
203+
- Q: API performance SLA (p95) → A: Option B - p95 ≤ 500ms for representative interactive API endpoints under normal load.
204+
205+

specs/002-harden-checkout-tenancy/tasks.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,18 @@ Total tasks: 42 (grouped by phase and user story)
6363
- [ ] T040 Background job infra validation: confirm existing job processing infra (worker/queue); if missing, add lightweight dev stub and update `src/services/export-service.ts` to support stub in dev/test
6464
- [ ] T041 Payment pre-validation robustness: add idempotency key handling, retry/backoff policy, and tests for provider outages (update `src/services/payments/intent-validator.ts` and tests)
6565
- [ ] T042 REST audit: scan `src/app/api/**/route.ts` for REST violations (PUT/PATCH misuse, stray `success` flags). Produce per-endpoint remediation tasks with deadlines and CI gating to ensure fixes are applied before merge.
66+
- [ ] T038b Migrate `Product.images` from `String` JSON array to `Json` type; write migration + backfill parser.
67+
- [ ] T038c Migrate `Product.metaKeywords` from `String` JSON array to `Json` type.
68+
- [ ] T038d Migrate `ProductVariant.options` from `String` JSON object to `Json` type.
69+
- [ ] T038e Migrate `ProductAttribute.values` from `String` JSON array to `Json` type.
70+
- [ ] T038f Migrate `Review.images` from `String` JSON array to `Json` type.
71+
- [ ] T038g Migrate `ShippingZone.countries` from `String` JSON array to `Json` type.
72+
- [ ] T038h Migrate `Page.metaKeywords` from `String` JSON array to `Json` type.
73+
- [ ] T038i Migrate `EmailTemplate.variables` from `String` JSON array to `Json` type.
74+
- [ ] T038j Migrate `Webhook.events` from `String` JSON array to `Json` type.
75+
- [ ] T038k Migrate optional `Payment.metadata` from `String?` to `Json?` with validation of malformed rows.
76+
- [ ] T038l Migrate optional `SyncLog.metadata` from `String?` to `Json?`.
77+
- [ ] T038m Migrate optional `AuditLog.changes` from `String?` to `Json?` and ensure diff object preserved.
6678

6779
## Additional Tasks (Governance & CI)
6880
- [ ] T043 Percy + Visual Regression: Integrate Percy visual regression for critical pages and UI components (dashboard, checkout, product list). Add Percy job to feature CI and create snapshot baselines. Map to constitution visual regression requirements.
@@ -89,7 +101,7 @@ Total tasks: 42 (grouped by phase and user story)
89101
Path to generated tasks.md: `specs/002-harden-checkout-tenancy/tasks.md`
90102

91103
Summary:
92-
- Total tasks: 42
104+
- Total tasks: 55
93105
- Tasks per story (approx): US1:6, US2:6, US3:5, US4:5, Foundational:6, Setup:3, Final/Polish:7
94106
- Parallel opportunities: Foundational helpers (T004–T009) and docs/tests (T033–T036)
95107
- Suggested MVP: US1 minimal slice (T010, T011, T013)

0 commit comments

Comments
 (0)