Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
177 changes: 177 additions & 0 deletions .agents/skills/testing-ecommerce-features/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
---
name: testing-ecommerce-features
description: Test ecommerce features (cart, catalog, orders, social commerce, promotions, agent store) end-to-end. Use when verifying middleware integration, persistence patterns, mobile parity, or payment flow changes in ecommerce routers.
---

# Testing Ecommerce Features

## Overview
The ecommerce layer spans 6 TypeScript routers, 1 Go service, 1 Rust service, 1 Python service, 5 Flutter screens, and 5 React Native screens. Each mutation must integrate with the middleware stack and persist to PostgreSQL.

## Environment Setup
- **Repo:** `munisp/agentbanking`
- **Branch:** `production-hardened` is the main development branch
- **No live server** in most sessions — no DATABASE_URL, Redis, Kafka env vars
- **TypeScript check:** `npx tsc --noEmit` (pre-existing errors in react-i18next/@dnd-kit are expected)
- **Test suite:** `npx vitest run` (8 pre-existing failures: db-performance x4, e2e x1, sprint46 x1, sprint95 x1, middleware-runtime x2)
- **Package manager:** pnpm

## Devin Secrets Needed
- `DATABASE_URL` — PostgreSQL connection string (not currently available; testing is shell-based)
- No other secrets required for structural validation

## Key Files
- **TS Routers:** `server/routers/ecommerceOrders.ts`, `ecommerceCatalog.ts`, `ecommerceCart.ts`, `agentStore.ts`, `promotions.ts`, `socialCommerceGateway.ts`
- **Kafka topics:** `server/kafkaClient.ts` (KafkaTopic union type)
- **Rust cart:** `server/ecommerce-cart-rust/src/{main,cart,checkout,offline}.rs`
- **Go catalog:** `server/ecommerce-catalog-go/handlers/handlers.go`
- **Python intelligence:** `server/ecommerce-intelligence-py/main.py`
- **Migration:** `drizzle/drizzle/0044_ecommerce_enhancements.sql`
- **Flutter screens:** `mobile-flutter/mobile-flutter/lib/screens/ecommerce_*.dart`
- **RN screens:** `mobile-rn/mobile-rn/src/screens/Ecommerce*.tsx`
- **Middleware clients:** `server/tbClient.ts`, `server/fluvio.ts`, `server/lakehouse.ts`, `server/redisClient.ts`, `server/middleware/middlewareConnectors.ts`

## Testing Approach (Shell-Based Structural Validation)

### 1. TypeScript Compilation
```bash
npx tsc --noEmit 2>&1 | grep "error TS" | grep -E "ecommerceOrders|ecommerceCatalog|ecommerceCart|agentStore|promotions|socialCommerce|kafkaClient"
```
**Expected:** Zero output (0 errors in modified ecommerce files)

### 2. Middleware Active Calls (NOT Dead Code)
This is the MOST IMPORTANT test. PR #46/#48 had middleware helpers defined but never called. Check that each router has active calls (total - 1 definition >= 3):
```bash
for f in ecommerceCatalog ecommerceCart agentStore promotions socialCommerceGateway ecommerceOrders; do
FILE="server/routers/$f.ts"
TOTAL=$(grep -c "publish.*Middleware(" "$FILE")
ACTIVE=$((TOTAL - 1))
echo "$f: $ACTIVE active calls $([ $ACTIVE -ge 3 ] && echo PASS || echo FAIL)"
done
```

### 3. Fail-Open Pattern
Extract each router's middleware helper body and verify .catch() count >= middleware call count:
```bash
for f in ecommerceCatalog ecommerceCart agentStore promotions socialCommerceGateway ecommerceOrders; do
FILE="server/routers/$f.ts"
HELPER_BODY=$(sed -n '/async function publish.*Middleware/,/^}/p' "$FILE")
MW=$(echo "$HELPER_BODY" | grep -c "publishEvent\|tbCreateTransfer\|publishTxToFluvio\|dapr.publishEvent\|ingestToLakehouse\|cacheSet")
CATCH=$(echo "$HELPER_BODY" | grep -c '\.catch(')
echo "$f: $MW calls, $CATCH catches — $([ $CATCH -ge $MW ] && echo PASS || echo FAIL)"
done
```

### 4. Rust Cart Persistence
```bash
# DashMap must be fully eliminated
grep -rn "DashMap\|dashmap" server/ecommerce-cart-rust/src/
# Should return 0 matches

# PostgreSQL must be present
grep -c "PgPool" server/ecommerce-cart-rust/src/main.rs # >= 3
grep -c "sqlx::query" server/ecommerce-cart-rust/src/cart.rs # >= 10
grep -c "sqlx::query" server/ecommerce-cart-rust/src/checkout.rs # >= 5
grep -c "sqlx::query" server/ecommerce-cart-rust/src/offline.rs # >= 5
```

### 5. KafkaTopic Union
All topics used in publishEvent calls must exist in the KafkaTopic union in `server/kafkaClient.ts`:
```bash
USED=$(grep -ohP 'publishEvent\("([^"]+)"' server/routers/ecommerce*.ts server/routers/agentStore.ts server/routers/promotions.ts server/routers/socialCommerceGateway.ts | sed 's/publishEvent("//;s/"//' | sort -u)
for topic in $USED; do
[ "$topic" = "pubsub" ] && continue
grep -q "\"$topic\"" server/kafkaClient.ts && echo "PASS: $topic" || echo "FAIL: $topic"
done
```

### 6. TigerBeetle Type Safety
socialCommerceGateway previously used BigInt() which mismatched TBTransferRequest (expects string/number):
```bash
grep -c "BigInt" server/routers/socialCommerceGateway.ts
# Must be 0
```

### 7. ecommerceOrders Key Features
```bash
# New endpoints exist
for ep in recoverAbandonedCarts releaseExpiredReservations checkoutFlashSale getDeliveryTracking updateDeliveryTracking convertOrderCurrency; do
grep -q "$ep:" server/routers/ecommerceOrders.ts && echo "PASS: $ep" || echo "FAIL: $ep"
done

# Payment verification wired to createFromCart
grep -A 200 "createFromCart:" server/routers/ecommerceOrders.ts | head -200 | grep -c "verifyPaymentGateway" # >= 1

# Notifications wired to updateStatus
grep -A 200 "updateStatus:" server/routers/ecommerceOrders.ts | head -200 | grep -c "sendOrderNotification" # >= 1

# Flash sale FOR UPDATE locking
grep -A 50 "checkoutFlashSale:" server/routers/ecommerceOrders.ts | head -50 | grep -c "FOR UPDATE" # >= 1
```

### 8. Go Catalog Events
```bash
grep -c "publishCatalogEvent.*inventory" server/ecommerce-catalog-go/handlers/handlers.go
# >= 3 (reserved, released, deducted)
```

### 9. Python Innovation Endpoints
```bash
for ep in "checkout-adjust" "offline-bundle" "barcode-to-cart"; do
grep -q "$ep" server/ecommerce-intelligence-py/main.py && echo "PASS: $ep" || echo "FAIL: $ep"
done
```

### 10. Migration DDL
```bash
TABLE_COUNT=$(grep -ci "CREATE TABLE" drizzle/drizzle/0044_ecommerce_enhancements.sql)
INDEX_COUNT=$(grep -ci "CREATE INDEX" drizzle/drizzle/0044_ecommerce_enhancements.sql)
echo "Tables: $TABLE_COUNT (expect 12), Indexes: $INDEX_COUNT (expect 11)"
```

### 11. Mobile Screens (Not Scaffolds)
Scaffold screens had 0 API calls and ~131/163 lines. Real screens should have >= 2 API calls:
```bash
for f in ecommerce_shopping_cart_screen ecommerce_product_catalog_screen ecommerce_checkout_screen; do
FILE="mobile-flutter/mobile-flutter/lib/screens/$f.dart"
API=$(grep -c "ApiService\|\.get(\|\.post(" "$FILE")
echo "Flutter $f: $API API calls (>=2 = real)"
done
for f in EcommerceShoppingCartScreen EcommerceProductCatalogScreen EcommerceCheckoutScreen; do
FILE="mobile-rn/mobile-rn/src/screens/$f.tsx"
API=$(grep -c "apiService\.\|\.get(\|\.post(" "$FILE")
echo "RN $f: $API API calls (>=2 = real)"
done
```

## Common Issues

### Dead middleware code (PR #46/#48 bug)
The `publishXxxMiddleware()` helper can be defined but never called from mutation handlers. Always verify active call count, not just that the function exists. Count total references minus 1 (the definition).

### BigInt vs String in TigerBeetle
`TBTransferRequest` expects `debitAccountId: string` and `creditAccountId: string`. Using `BigInt()` causes TS2322. Use `String()` instead.

### agentStore input field names
- `createDeliveryZone` uses `input.zoneName` (not `input.name`)
- `createPaymentSplit` uses `input.orderTotal` (not `input.orderAmount`)
Check Zod schema before referencing input fields.

### Multi-line middleware calls
`tbCreateTransfer({...}).catch()` spans 4-5 lines. Single-line grep for `.catch()` will miss it. Use `sed` to extract the helper body first, then count within the block.

### Rust DashMap → PgPool migration
After migration, check both directions:
1. **Negative:** 0 DashMap/dashmap references in src/
2. **Positive:** PgPool in AppState + sqlx::query calls in every handler

### Pre-existing failures
These are known and unrelated to ecommerce:
1. `db-performance.test.ts` x4 (needs PgBouncer)
2. `e2e/critical-flows.spec.ts` x1 (needs live server)
3. `sprint46.test.ts` x1 (currency count)
4. `sprint95.test.ts` x1 (router count hardcoded)
5. `middleware-integration-runtime.test.ts` x2 (needs live Redis)

## Recording
No recording needed — all validation is shell-based. Only record if testing involves browser UI interactions (e.g., testing the PWA storefront or checkout flow with a live server).
115 changes: 115 additions & 0 deletions drizzle/drizzle/0044_full_platform_production_hardening.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
-- Full Platform Production Hardening Migration
-- Covers: compliance screening, service state, stablecoin enhancements,
-- DDoS shield persistence, Permify integration, marketplace integration

-- ── Compliance Screening ──────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS compliance_screening_results (
id SERIAL PRIMARY KEY,
screening_type TEXT NOT NULL,
subject_name TEXT NOT NULL,
subject_id TEXT,
result TEXT NOT NULL,
risk_score REAL DEFAULT 0,
matched_lists TEXT[],
details JSONB,
screened_by TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE IF NOT EXISTS sar_reports (
id SERIAL PRIMARY KEY,
reference TEXT UNIQUE NOT NULL,
agent_code TEXT NOT NULL,
subject_name TEXT NOT NULL,
subject_id TEXT,
suspicious_activity TEXT NOT NULL,
amount NUMERIC,
currency TEXT DEFAULT 'NGN',
narrative TEXT NOT NULL,
supporting_tx_refs TEXT[],
status TEXT DEFAULT 'filed',
filed_at TIMESTAMPTZ DEFAULT NOW(),
submitted_to_nfiu_at TIMESTAMPTZ,
reviewed_at TIMESTAMPTZ
);

CREATE TABLE IF NOT EXISTS sanctions_list_cache (
id SERIAL PRIMARY KEY,
list_name TEXT NOT NULL,
entry_name TEXT NOT NULL,
entry_id TEXT,
country TEXT,
aliases TEXT[],
list_type TEXT,
updated_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE IF NOT EXISTS pep_database (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
country TEXT,
position TEXT,
risk_level TEXT DEFAULT 'medium',
aliases TEXT[],
active BOOLEAN DEFAULT TRUE,
updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- ── Polyglot Service State (shared across Go/Rust/Python) ─────────────────────
CREATE TABLE IF NOT EXISTS service_state (
key TEXT PRIMARY KEY,
value JSONB NOT NULL,
service TEXT NOT NULL,
updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- ── DDoS Shield Persistence ───────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS ddos_ip_reputations (
ip TEXT PRIMARY KEY,
reputation_score REAL NOT NULL,
total_requests BIGINT DEFAULT 0,
blocked_requests BIGINT DEFAULT 0,
last_seen TIMESTAMPTZ DEFAULT NOW(),
country TEXT,
is_tor BOOLEAN DEFAULT FALSE
);

CREATE TABLE IF NOT EXISTS ddos_permanent_blocklist (
ip TEXT PRIMARY KEY,
reason TEXT NOT NULL,
blocked_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE IF NOT EXISTS ddos_threat_intel (
ip TEXT PRIMARY KEY,
threat_type TEXT NOT NULL,
confidence REAL,
source TEXT,
first_seen TIMESTAMPTZ DEFAULT NOW(),
last_seen TIMESTAMPTZ DEFAULT NOW()
);

-- ── Permify Relationship Tuples (local cache for audit) ──────────────────────
CREATE TABLE IF NOT EXISTS permify_relationship_audit (
id SERIAL PRIMARY KEY,
entity_type TEXT NOT NULL,
entity_id TEXT NOT NULL,
relation TEXT NOT NULL,
subject_type TEXT NOT NULL,
subject_id TEXT NOT NULL,
action TEXT NOT NULL DEFAULT 'write',
created_at TIMESTAMPTZ DEFAULT NOW()
);

-- ── Indexes ──────────────────────────────────────────────────────────────────
CREATE INDEX IF NOT EXISTS idx_screening_results_name ON compliance_screening_results(subject_name);
CREATE INDEX IF NOT EXISTS idx_screening_results_type ON compliance_screening_results(screening_type, created_at);
CREATE INDEX IF NOT EXISTS idx_sar_reports_ref ON sar_reports(reference);
CREATE INDEX IF NOT EXISTS idx_sar_reports_status ON sar_reports(status, filed_at);
CREATE INDEX IF NOT EXISTS idx_sanctions_entry_name ON sanctions_list_cache(entry_name);
CREATE INDEX IF NOT EXISTS idx_sanctions_list_name ON sanctions_list_cache(list_name);
CREATE INDEX IF NOT EXISTS idx_pep_name ON pep_database(name);
CREATE INDEX IF NOT EXISTS idx_service_state_service ON service_state(service);
CREATE INDEX IF NOT EXISTS idx_ddos_ip_last_seen ON ddos_ip_reputations(last_seen);
CREATE INDEX IF NOT EXISTS idx_ddos_blocklist_blocked ON ddos_permanent_blocklist(blocked_at);
CREATE INDEX IF NOT EXISTS idx_permify_audit_entity ON permify_relationship_audit(entity_type, entity_id);
7 changes: 7 additions & 0 deletions drizzle/drizzle/meta/_journal.json
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,13 @@
"when": 1779465600000,
"tag": "0043_tigerbeetle_bidir_persistence",
"breakpoints": true
},
{
"idx": 42,
"version": "7",
"when": 1782320100000,
"tag": "0044_full_platform_production_hardening",
"breakpoints": true
}
]
}
21 changes: 20 additions & 1 deletion mobile-rn/mobile-rn/src/screens/AIMonitoringDashboardScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,25 @@ const AIMonitoringDashboardScreen: React.FC = () => {

useEffect(() => { load(); }, [load]);

const handleCreate = useCallback(async (data: Record<string, unknown>) => {
try {
await apiClient.post('/dashboard/create', data);
load(); // Refresh list
} catch (e: any) {
setError(e?.message ?? 'Create failed');
}
}, [load]);

const handleDelete = useCallback(async (id: string | number) => {
try {
await apiClient.delete(`/dashboard/${id}`);
setItems(prev => prev.filter(item => item.id !== id));
} catch (e: any) {
setError(e?.message ?? 'Delete failed');
}
}, []);


const filtered = items.filter(item => {
if (!search) return true;
const q = search.toLowerCase();
Expand Down Expand Up @@ -70,7 +89,7 @@ const AIMonitoringDashboardScreen: React.FC = () => {

return (
<View style={styles.container}>
<Text style={styles.header}>A I Monitoring</Text>
<Text style={styles.header}>A I Monitoring Dashboard</Text>

{/* Summary */}
<View style={styles.summaryRow}>
Expand Down
23 changes: 21 additions & 2 deletions mobile-rn/mobile-rn/src/screens/AnalyticsDashboardScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const AnalyticsDashboardScreen: React.FC = () => {
const load = useCallback(async () => {
try {
setError('');
const { data } = await apiClient.get('/dashboard/list?page=1&limit=50');
const { data } = await apiClient.get('/analytics/list?page=1&limit=50');
setItems(data?.items ?? data?.data ?? []);
} catch (e: any) {
setError(e?.message ?? 'Failed to load');
Expand All @@ -36,6 +36,25 @@ const AnalyticsDashboardScreen: React.FC = () => {

useEffect(() => { load(); }, [load]);

const handleCreate = useCallback(async (data: Record<string, unknown>) => {
try {
await apiClient.post('/analytics/create', data);
load(); // Refresh list
} catch (e: any) {
setError(e?.message ?? 'Create failed');
}
}, [load]);

const handleDelete = useCallback(async (id: string | number) => {
try {
await apiClient.delete(`/analytics/${id}`);
setItems(prev => prev.filter(item => item.id !== id));
} catch (e: any) {
setError(e?.message ?? 'Delete failed');
}
}, []);


const filtered = items.filter(item => {
if (!search) return true;
const q = search.toLowerCase();
Expand Down Expand Up @@ -70,7 +89,7 @@ const AnalyticsDashboardScreen: React.FC = () => {

return (
<View style={styles.container}>
<Text style={styles.header}>Analytics</Text>
<Text style={styles.header}>Analytics Dashboard</Text>

{/* Summary */}
<View style={styles.summaryRow}>
Expand Down
Loading
Loading