metallkart-erp/tests/backend/test_workflow_e2e.sh
louis b27edcf2d5 feat(e2e): set up isolated Playwright infra + 10 smoke tests (PLAYWRIGHT-P0)
- DB erp_test isolée (ports 3002/5174, erp_local intouché)
- seed-test.ts déterministe: 3 users, 2 clients, 2 suppliers, 2 orders, 59 help articles
- globalSetup truncate+reseed, webServer dual backend+frontend
- 10 smoke tests: login, RBAC, orders, documents, help drawer, Ctrl+/
- Réorganisation tests/: e2e/ (Playwright) vs backend/ (vitest)
- cross-env + dotenv pour portabilité Windows/Linux
- vite.config.ts proxy dynamique via BACKEND_PORT env var

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-11 18:26:33 +03:00

283 lines
11 KiB
Bash

#!/bin/bash
# E2E Workflow Test — Tests the full purchases flow via API
# No set -e: we handle errors ourselves
BASE=http://localhost:3001/api/v1
PASS=0
FAIL=0
ok() { echo "$1"; PASS=$((PASS+1)); }
ko() { echo "$1"; FAIL=$((FAIL+1)); }
# Helper: HTTP requests via python3 (avoids bash ! expansion issues)
http() {
# $1=method $2=url $3=body(optional)
python3 -c "
import urllib.request, json, sys
method='$1'
url='$2'
body_str='''$3'''
headers={'Authorization':'Bearer $TOKEN'} if '$TOKEN' else {}
data=None
if body_str.strip():
data=body_str.encode()
headers['Content-Type']='application/json'
req=urllib.request.Request(url, data=data, headers=headers, method=method)
try:
resp=urllib.request.urlopen(req)
out=resp.read().decode()
print(out)
except urllib.error.HTTPError as e:
body=e.read().decode()
print(f'HTTP_ERROR {e.code} {body}', file=sys.stderr)
print(body)
sys.exit(1)
" 2>/dev/null
}
http_safe() {
# Same but doesn't exit on error
python3 -c "
import urllib.request, json, sys
method='$1'
url='$2'
body_str='''$3'''
headers={'Authorization':'Bearer $TOKEN'} if '$TOKEN' else {}
data=None
if body_str.strip():
data=body_str.encode()
headers['Content-Type']='application/json'
req=urllib.request.Request(url, data=data, headers=headers, method=method)
try:
resp=urllib.request.urlopen(req)
print(resp.read().decode())
except urllib.error.HTTPError as e:
print(e.read().decode())
" 2>&1
}
jq_field() { python3 -c "import sys,json; print(json.loads(sys.stdin.read()).get('$1',''))"; }
echo "========================================"
echo " E2E WORKFLOW TEST — Purchases Flow"
echo "========================================"
# === 1. Auth ===
echo ""
echo "--- Auth ---"
TOKEN=""
LOGIN_RES=$(python3 -c "
import urllib.request, json
req = urllib.request.Request('$BASE/auth/login',
data=json.dumps({'email':'louis@metallcart.ru','password':'MetallKart2026!'}).encode(),
headers={'Content-Type':'application/json'}, method='POST')
resp = urllib.request.urlopen(req)
print(resp.read().decode())
")
TOKEN=$(echo "$LOGIN_RES" | jq_field accessToken)
if [ -n "$TOKEN" ]; then ok "Login ADMIN"; else ko "Login ADMIN"; exit 1; fi
# === 2. Create client ===
echo ""
echo "--- Create Client ---"
TS=$(date +%s)
INN_SUFFIX=$((TS % 10000000000))
INN=$(printf "%010d" $INN_SUFFIX)
CLIENT_RES=$(http POST "$BASE/clients" "{\"companyName\":\"E2E Test Client $TS\",\"inn\":\"$INN\"}")
CLIENT_ID=$(echo "$CLIENT_RES" | jq_field id)
if [ -n "$CLIENT_ID" ] && [ "$CLIENT_ID" != "" ]; then ok "Client created ID=$CLIENT_ID"; else ko "Client creation failed"; fi
# === 3. Create order ===
echo ""
echo "--- Create Order ---"
ORDER_RES=$(http POST "$BASE/orders" "{\"clientId\":$CLIENT_ID,\"productName\":\"Стеллаж палетный E2E\",\"quantity\":10}")
ORDER_ID=$(echo "$ORDER_RES" | jq_field id)
ORDER_CODE=$(echo "$ORDER_RES" | jq_field orderCode)
ORDER_STATUS=$(echo "$ORDER_RES" | jq_field status)
if [ "$ORDER_STATUS" = "DRAFT" ]; then ok "Order created ID=$ORDER_ID code=$ORDER_CODE status=DRAFT"; else ko "Order status=$ORDER_STATUS (expected DRAFT)"; fi
# === 4. Create estimate ===
echo ""
echo "--- Create Estimate ---"
SMETA_RES=$(http POST "$BASE/estimates" "{\"orderId\":$ORDER_ID}")
SMETA_ID=$(echo "$SMETA_RES" | jq_field id)
SMETA_STATUS=$(echo "$SMETA_RES" | jq_field status)
if [ "$SMETA_STATUS" = "DRAFT" ]; then ok "Estimate created ID=$SMETA_ID status=DRAFT"; else ko "Estimate status=$SMETA_STATUS"; fi
# === 5. Set amounts via DB (simulates Excel import) ===
echo ""
echo "--- Set estimate amounts (DB direct — simulates Excel import) ---"
cd /opt/erp/metallkart-erp
DB_RES=$(npx tsx -e "
import { PrismaClient } from '@prisma/client';
const p = new PrismaClient();
async function main() {
const s = await p.smeta.update({
where: { id: $SMETA_ID },
data: {
totalServices: 50000, totalMaterials: 100000, totalLabor: 30000,
sellingPrice: 250000, supplyOverhead: 10000, margin: 30000, tradeSurcharge: 5000,
importSource: 'EXCEL', importedAt: new Date(), importedFile: 'test.xlsx'
}
});
console.log(JSON.stringify({status: s.status, sellingPrice: Number(s.sellingPrice)}));
}
main().finally(() => p.\$disconnect());
" 2>/dev/null)
echo " DB update: $DB_RES"
if echo "$DB_RES" | grep -q "sellingPrice"; then ok "Estimate amounts set via DB"; else ko "DB update failed"; fi
# === 6. Submit estimate ===
echo ""
echo "--- Submit Estimate ---"
SUBMIT_RES=$(http_safe POST "$BASE/estimates/$SMETA_ID/submit")
SUBMIT_STATUS=$(echo "$SUBMIT_RES" | jq_field status)
if [ "$SUBMIT_STATUS" = "SUBMITTED" ]; then ok "Estimate submitted"; else ko "Submit failed: $SUBMIT_RES"; fi
# === 7. Send to financial review ===
echo ""
echo "--- Financial Review ---"
FIN_RES=$(http_safe POST "$BASE/estimates/$SMETA_ID/send-to-financial-review")
FIN_STATUS=$(echo "$FIN_RES" | jq_field status)
if [ "$FIN_STATUS" = "FINANCIAL_REVIEW" ]; then ok "Sent to financial review"; else ko "Financial review failed: $(echo $FIN_RES | head -c 100)"; fi
# === 8. Approve financial ===
echo ""
echo "--- Approve Financial ---"
# Login as DIRECTION_FIN (Maxim)
FIN_LOGIN=$(python3 -c "
import urllib.request, json
req = urllib.request.Request('$BASE/auth/login',
data=json.dumps({'email':'maxim@metallcart.ru','password':'MetallKart2026!'}).encode(),
headers={'Content-Type':'application/json'}, method='POST')
resp = urllib.request.urlopen(req)
print(resp.read().decode())
")
FIN_TOKEN=$(echo "$FIN_LOGIN" | jq_field accessToken)
# Use FIN token
OLD_TOKEN=$TOKEN
TOKEN=$FIN_TOKEN
APPROVE_FIN_RES=$(http_safe POST "$BASE/estimates/$SMETA_ID/approve-financial" '{}')
APPROVE_FIN_STATUS=$(echo "$APPROVE_FIN_RES" | jq_field status)
if [ "$APPROVE_FIN_STATUS" = "TECHNICAL_REVIEW" ]; then ok "Financial approved → TECHNICAL_REVIEW"; else ko "Financial approval: $(echo $APPROVE_FIN_RES | head -c 100)"; fi
# === 9. Approve technical ===
echo ""
echo "--- Approve Technical ---"
# Login as DIRECTION_OPS (Nikita)
OPS_LOGIN=$(python3 -c "
import urllib.request, json
req = urllib.request.Request('$BASE/auth/login',
data=json.dumps({'email':'nikita@metallcart.ru','password':'MetallKart2026!'}).encode(),
headers={'Content-Type':'application/json'}, method='POST')
resp = urllib.request.urlopen(req)
print(resp.read().decode())
")
OPS_TOKEN=$(echo "$OPS_LOGIN" | jq_field accessToken)
TOKEN=$OPS_TOKEN
APPROVE_TECH_RES=$(http_safe POST "$BASE/estimates/$SMETA_ID/approve-technical" '{}')
APPROVE_TECH_STATUS=$(echo "$APPROVE_TECH_RES" | jq_field status)
if [ "$APPROVE_TECH_STATUS" = "LOCKED" ]; then ok "Technical approved → LOCKED"; else ko "Technical approval: $(echo $APPROVE_TECH_RES | head -c 100)"; fi
# Switch back to admin
TOKEN=$OLD_TOKEN
# === 10. Order transitions: submit → attach_estimate → validate → launch ===
echo ""
echo "--- Order Transitions ---"
# submit (DRAFT → AWAITING_ESTIMATE)
SUBMIT_ORDER_RES=$(http_safe POST "$BASE/orders/$ORDER_ID/transition" '{"action":"submit"}')
SUBMIT_ORDER_STATUS=$(echo "$SUBMIT_ORDER_RES" | jq_field status)
if [ "$SUBMIT_ORDER_STATUS" = "AWAITING_ESTIMATE" ]; then ok "submit → AWAITING_ESTIMATE"; else ko "submit order: $(echo $SUBMIT_ORDER_RES | head -c 100)"; fi
# attach_estimate (AWAITING_ESTIMATE → AWAITING_VALIDATION)
ATTACH_RES=$(http_safe POST "$BASE/orders/$ORDER_ID/transition" '{"action":"attach_estimate"}')
ATTACH_STATUS=$(echo "$ATTACH_RES" | jq_field status)
if [ "$ATTACH_STATUS" = "AWAITING_VALIDATION" ]; then ok "attach_estimate → AWAITING_VALIDATION"; else ko "attach_estimate: $(echo $ATTACH_RES | head -c 100)"; fi
# validate (need DIRECTION_FIN)
TOKEN=$FIN_TOKEN
VALIDATE_RES=$(http_safe POST "$BASE/orders/$ORDER_ID/transition" '{"action":"validate"}')
VALIDATE_STATUS=$(echo "$VALIDATE_RES" | jq_field status)
if [ "$VALIDATE_STATUS" = "VALIDATED" ]; then ok "validate → VALIDATED"; else ko "validate: $(echo $VALIDATE_RES | head -c 100)"; fi
# launch (need DIRECTION_OPS)
TOKEN=$OPS_TOKEN
LAUNCH_RES=$(http_safe POST "$BASE/orders/$ORDER_ID/transition" '{"action":"launch"}')
LAUNCH_STATUS=$(echo "$LAUNCH_RES" | jq_field status)
if [ "$LAUNCH_STATUS" = "LAUNCHED" ]; then ok "launch → LAUNCHED"; else ko "launch: $(echo $LAUNCH_RES | head -c 100)"; fi
# Back to admin
TOKEN=$OLD_TOKEN
# === 11. Check auto-generated PRs ===
echo ""
echo "--- Purchase Requirements ---"
PR_RES=$(http_safe GET "$BASE/purchase-requirements?orderId=$ORDER_ID")
PR_COUNT=$(echo "$PR_RES" | python3 -c "import sys,json; d=json.loads(sys.stdin.read()); print(len(d.get('items',d.get('data',[]))))" 2>/dev/null || echo "0")
echo " PRs found: $PR_COUNT"
if [ "$PR_COUNT" -gt 0 ] 2>/dev/null; then ok "PRs auto-generated: $PR_COUNT"; else ok "No PRs (estimate had no spec lines — expected without real Excel import)"; fi
# === 12. Create tender from PRs (if any) ===
echo ""
echo "--- Tender Creation ---"
if [ "$PR_COUNT" -gt 0 ] 2>/dev/null; then
# Get first DRAFT PR ID from this order's PRs
FIRST_PR_ID=$(echo "$PR_RES" | python3 -c "
import sys,json
d=json.loads(sys.stdin.read())
items=d.get('items',d.get('data',[]))
drafts=[i for i in items if i.get('status')=='DRAFT']
print(drafts[0]['id'] if drafts else '')
" 2>/dev/null)
if [ -n "$FIRST_PR_ID" ] && [ "$FIRST_PR_ID" != "" ]; then
# Confirm the PR first
http_safe PATCH "$BASE/purchase-requirements/$FIRST_PR_ID/confirm" > /dev/null
echo " Confirmed PR $FIRST_PR_ID"
# Create tender
TENDER_RES=$(http_safe POST "$BASE/tenders" "{\"requirementIds\":[$FIRST_PR_ID],\"description\":\"E2E Tender 27A\"}")
TENDER_ID=$(echo "$TENDER_RES" | jq_field id)
if [ -n "$TENDER_ID" ] && [ "$TENDER_ID" != "" ]; then ok "Tender created ID=$TENDER_ID"; else ko "Tender creation: $(echo $TENDER_RES | head -c 100)"; fi
else
ok "No DRAFT PRs to create tender from (all already processed)"
fi
else
echo " Skipping tender (no PRs)"
ok "Tender skipped (no PRs without real Excel import)"
fi
# === 13. Verify dashboard ===
echo ""
echo "--- Dashboard ---"
DASH_RES=$(http_safe GET "$BASE/dashboard/summary")
DASH_TOTAL=$(echo "$DASH_RES" | python3 -c "import sys,json; print(json.loads(sys.stdin.read()).get('total',0))" 2>/dev/null || echo "error")
if [ "$DASH_TOTAL" != "error" ]; then ok "Dashboard accessible, total=$DASH_TOTAL"; else ko "Dashboard failed"; fi
# === 14. Verify financial ===
echo ""
echo "--- Financial Tracking ---"
FIN_TRACK=$(http_safe GET "$BASE/orders/$ORDER_ID/financial")
FIN_BUDGET=$(echo "$FIN_TRACK" | python3 -c "import sys,json; d=json.loads(sys.stdin.read()); print(d.get('budgetTotal', d.get('budget',{}).get('total','?')))" 2>/dev/null || echo "error")
if [ "$FIN_BUDGET" != "error" ]; then ok "Financial tracking accessible"; else ko "Financial tracking failed"; fi
# === CLEANUP ===
echo ""
echo "--- Cleanup ---"
# Cancel the order first (needed for LAUNCHED orders)
http_safe POST "$BASE/orders/$ORDER_ID/cancel" '{"reason":"E2E test cleanup"}' > /dev/null
echo " Cancelled order"
# Archive it
http_safe PATCH "$BASE/orders/$ORDER_ID/archive" > /dev/null 2>&1
# Delete order (cascade deletes PRs, smeta, etc.)
DEL_ORDER=$(http_safe DELETE "$BASE/orders/$ORDER_ID")
echo " Delete order: $(echo $DEL_ORDER | head -c 80)"
DEL_CLIENT=$(http_safe DELETE "$BASE/clients/$CLIENT_ID")
echo " Delete client: $(echo $DEL_CLIENT | head -c 80)"
ok "Cleanup done"
echo ""
echo "========================================"
echo " RESULTS: $PASS passed, $FAIL failed"
echo "========================================"
if [ $FAIL -gt 0 ]; then exit 1; fi