chore: أرشفة باك إند NestJS إلى backend-archive
قرار المالك 2026-07-27: الباك إند المعتمد صار باك إند سيرو PHP، ويُنقل إلى هذا المستودع كنسخة جديدة باسم «انطلق» على api.intaleqapp.com. backend/ → backend-archive/ (305 ملفاً، إعادة تسمية بلا تعديل محتوى) + README-ARCHIVE.md يوضّح سبب الأرشفة وسبب عدم الحذف: scripts/e2e-test.mjs هو أدق توثيق سلوكي لدورة الرحلة كاملة، ويبقى مرجعاً عند بناء الوحدات الناقصة في باك إند PHP (المحفظتان · الاستحقاقات · محرّك التسعير · الوحدات). مسارات backend/ في docs/*.md لم تُعدَّل عمداً — سجلّ تاريخي. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
75b80bd299
commit
fff76c949f
+115
@@ -0,0 +1,115 @@
|
||||
{
|
||||
"name": "tripz-e2e-tester",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "tripz-e2e-tester",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"socket.io-client": "^4.7.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@socket.io/component-emitter": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
|
||||
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io-client": {
|
||||
"version": "6.6.6",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.6.tgz",
|
||||
"integrity": "sha512-iY6QdftLQ9pyiPoX082bpf/u1UewnOaJrtJIF9T0++QB34lZrj0uP+Q/bj8AlUsAxqhnkTV2BS8SBZSxOmoV5Q==",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.4.1",
|
||||
"engine.io-parser": "~5.2.1",
|
||||
"ws": "~8.21.0",
|
||||
"xmlhttprequest-ssl": "~2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io-parser": {
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
|
||||
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||
},
|
||||
"node_modules/socket.io-client": {
|
||||
"version": "4.8.3",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz",
|
||||
"integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.4.1",
|
||||
"engine.io-client": "~6.6.1",
|
||||
"socket.io-parser": "~4.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-parser": {
|
||||
"version": "4.2.7",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.7.tgz",
|
||||
"integrity": "sha512-IH/iSeO9T6gz1KkFleGDWkG9N3dl4jXVYUtMhIqH10Md0ttMer8nUNWiP1DKuNrybD2xBrixLJdCC9J6ECoYkg==",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.4.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.21.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz",
|
||||
"integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/xmlhttprequest-ssl": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
|
||||
"integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "tripz-e2e-tester",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"socket.io-client": "^4.7.5"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
import readline from 'readline';
|
||||
import { io } from 'socket.io-client';
|
||||
|
||||
const API_URL = process.env.API_URL || 'http://localhost:4010/api';
|
||||
const TENANT_ID = process.env.TENANT_ID || 'siro';
|
||||
|
||||
async function api(method, path, body = null, token = null) {
|
||||
const options = {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-tenant-id': TENANT_ID,
|
||||
'x-device-id': 'simulated-device-123'
|
||||
}
|
||||
};
|
||||
|
||||
if (token) options.headers['Authorization'] = `Bearer ${token}`;
|
||||
if (body) options.body = JSON.stringify(body);
|
||||
|
||||
const res = await fetch(`${API_URL}${path}`, options);
|
||||
const data = await res.json().catch(() => null);
|
||||
|
||||
if (!res.ok) {
|
||||
console.error(`\n\x1b[31m[API Error] ${method} ${path} -> ${res.status}\x1b[0m`);
|
||||
console.error(JSON.stringify(data, null, 2));
|
||||
throw new Error(`API Error: ${res.status}`);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
console.log('\n==========================================');
|
||||
console.log('🚀 Tripz Backend Automated E2E Simulation');
|
||||
console.log('==========================================\n');
|
||||
|
||||
// 1. Authenticate Demo Driver
|
||||
const driverPhone = '+962790000001';
|
||||
console.log(`[Driver] Authenticating Demo Driver (${driverPhone})...`);
|
||||
const driverAuth = await api('POST', '/auth/verify-otp', { phone: driverPhone, code: '1234' });
|
||||
const driverToken = driverAuth.access_token;
|
||||
console.log('\x1b[32m[Driver] Authenticated successfully!\x1b[0m\n');
|
||||
|
||||
// 2. Authenticate Demo Rider
|
||||
const riderPhone = '+962790000002';
|
||||
console.log(`[Rider] Authenticating Demo Rider (${riderPhone})...`);
|
||||
const riderAuth = await api('POST', '/auth/verify-otp', { phone: riderPhone, code: '1234' });
|
||||
const riderToken = riderAuth.access_token;
|
||||
console.log('\x1b[32m[Rider] Authenticated successfully!\x1b[0m\n');
|
||||
|
||||
// 3. Setup Driver Profile & Go Online
|
||||
console.log(`[Driver] Creating Driver profile...`);
|
||||
await api('POST', '/drivers/apply', { service_class: 'economy', vehicle_make: 'Toyota', vehicle_model: 'Prius', vehicle_plate: '12-3456' }, driverToken);
|
||||
|
||||
console.log(`[Driver] Going Online...`);
|
||||
await api('PATCH', '/drivers/status', { online: true }, driverToken);
|
||||
console.log('\x1b[32m[Driver] Profile created and is now ONLINE.\x1b[0m\n');
|
||||
|
||||
// 4. Connect WebSockets
|
||||
console.log(`[Sockets] Connecting Rider and Driver via WebSockets...`);
|
||||
const WS_URL = API_URL.replace('/api', '');
|
||||
const driverSocket = io(WS_URL, { extraHeaders: { Authorization: `Bearer ${driverToken}` } });
|
||||
const riderSocket = io(WS_URL, { extraHeaders: { Authorization: `Bearer ${riderToken}` } });
|
||||
|
||||
let tripId = null;
|
||||
|
||||
// Sockets Error Logging
|
||||
driverSocket.on('connect_error', (err) => console.error(`\x1b[31m[Driver WS] Error: ${err.message}\x1b[0m`));
|
||||
riderSocket.on('connect_error', (err) => console.error(`\x1b[31m[Rider WS] Error: ${err.message}\x1b[0m`));
|
||||
|
||||
driverSocket.on('connect', () => console.log('\x1b[36m[Driver WS] Connected to Gateway\x1b[0m'));
|
||||
riderSocket.on('connect', () => console.log('\x1b[36m[Rider WS] Connected to Gateway\x1b[0m'));
|
||||
|
||||
driverSocket.on('trip:offer', async (data) => {
|
||||
console.log(`\n\x1b[35m[Driver WS] 🔔 RECEIVED OFFER for Trip ${data.tripId || data.id}!\x1b[0m`);
|
||||
console.log(`Offer Payload:`, JSON.stringify(data, null, 2));
|
||||
tripId = data.tripId || data.id;
|
||||
|
||||
try {
|
||||
console.log(`[Driver] Accepting Trip...`);
|
||||
await api('POST', `/trips/${tripId}/accept`, null, driverToken);
|
||||
console.log(`\x1b[32m[Driver] Trip Accepted successfully!\x1b[0m`);
|
||||
} catch (err) {
|
||||
console.error(`\x1b[31m[Driver] Failed to accept trip!\x1b[0m`);
|
||||
}
|
||||
});
|
||||
|
||||
riderSocket.on('trip:update', (data) => {
|
||||
console.log(`\x1b[33m[Rider WS] Trip status updated: ${data.status}\x1b[0m`);
|
||||
});
|
||||
|
||||
await new Promise(r => setTimeout(r, 2000));
|
||||
|
||||
// 5. Driver Location Pings
|
||||
let driverLat = 32.0645;
|
||||
let driverLng = 36.0827;
|
||||
|
||||
const locationInterval = setInterval(() => {
|
||||
driverLat += 0.0001;
|
||||
driverSocket.emit('driver:location', { lat: driverLat, lng: driverLng, speed: 40, heading: 90 });
|
||||
console.log(`[Driver WS] 📍 Location ping: ${driverLat.toFixed(5)}, ${driverLng.toFixed(5)}`);
|
||||
}, 3000);
|
||||
|
||||
// Send first ping IMMEDIATELY
|
||||
driverSocket.emit('driver:location', { lat: driverLat, lng: driverLng, speed: 40, heading: 90 });
|
||||
console.log(`[Driver WS] 📍 Initial Location ping: ${driverLat.toFixed(5)}, ${driverLng.toFixed(5)}`);
|
||||
|
||||
// Wait for the server to process the location update into Redis
|
||||
await new Promise(r => setTimeout(r, 1500));
|
||||
|
||||
// 6. Rider Requests Trip
|
||||
console.log(`\n[Rider] Requesting Trip from Zarqa...`);
|
||||
const tripRequest = await api('POST', '/trips', {
|
||||
city: 'amman',
|
||||
service_class: 'economy',
|
||||
origin: { lat: 32.0650, lng: 36.0830 },
|
||||
destination: { lat: 32.0700, lng: 36.0900 }
|
||||
}, riderToken);
|
||||
|
||||
console.log(`\x1b[32m[Rider] Trip created: ${tripRequest.trip?.id || tripRequest.id}\x1b[0m`);
|
||||
console.log(`[System] Offered to \x1b[33m${tripRequest.offeredDrivers}\x1b[0m drivers nearby.`);
|
||||
|
||||
// 7. Simulate Trip Flow
|
||||
await new Promise(r => setTimeout(r, 10000));
|
||||
|
||||
if (tripId) {
|
||||
try {
|
||||
console.log(`\n[Driver] Heading to pickup (driver_arriving)...`);
|
||||
await api('PATCH', `/trips/${tripId}/status`, { status: 'driver_arriving' }, driverToken);
|
||||
await new Promise(r => setTimeout(r, 3000));
|
||||
|
||||
console.log(`\n[Driver] Arriving at pickup (driver_arrived)...`);
|
||||
await api('PATCH', `/trips/${tripId}/status`, { status: 'driver_arrived' }, driverToken);
|
||||
await new Promise(r => setTimeout(r, 3000));
|
||||
|
||||
console.log(`\n[Driver] Starting Trip...`);
|
||||
await api('PATCH', `/trips/${tripId}/status`, { status: 'in_progress' }, driverToken);
|
||||
await new Promise(r => setTimeout(r, 5000));
|
||||
|
||||
console.log(`\n[Driver] Completing Trip...`);
|
||||
await api('PATCH', `/trips/${tripId}/status`, { status: 'completed' }, driverToken);
|
||||
|
||||
console.log(`\n\x1b[32m🎉 E2E Simulation Completed Successfully!\x1b[0m\n`);
|
||||
} catch(e) {
|
||||
console.error(`\x1b[31m[Simulation Error] ${e.message}\x1b[0m`);
|
||||
}
|
||||
} else {
|
||||
console.log(`\n\x1b[31mFailed to receive offer!\x1b[0m`);
|
||||
console.log(`[Hint] Check if offeredDrivers was 0. If it was 0, the backend didn't find the driver in Redis. If it was > 0, the backend emitted the socket event but the driver didn't receive it.\n`);
|
||||
}
|
||||
|
||||
clearInterval(locationInterval);
|
||||
driverSocket.disconnect();
|
||||
riderSocket.disconnect();
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error(`\n\x1b[31m[Fatal Error] Simulation Crashed: ${error.message}\x1b[0m\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -0,0 +1,155 @@
|
||||
import { io } from 'socket.io-client';
|
||||
|
||||
const API_URL = process.env.API_URL || 'http://localhost:4010/api';
|
||||
const TENANT_ID = process.env.TENANT_ID || 'siro';
|
||||
const NUM_DRIVERS = parseInt(process.env.NUM_DRIVERS || '100', 10);
|
||||
const TRIP_RATE_SEC = parseInt(process.env.TRIP_RATE_SEC || '2', 10);
|
||||
const DRIVER_PING_INTERVAL_MS = parseInt(process.env.DRIVER_PING_INTERVAL_MS || '3000', 10);
|
||||
|
||||
async function api(method, path, body = null, token = null) {
|
||||
const options = {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-tenant-id': TENANT_ID,
|
||||
'x-device-id': 'stress-test-device'
|
||||
}
|
||||
};
|
||||
if (token) options.headers['Authorization'] = `Bearer ${token}`;
|
||||
if (body) options.body = JSON.stringify(body);
|
||||
|
||||
const res = await fetch(`${API_URL}${path}`, options);
|
||||
const data = await res.json().catch(() => null);
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`API Error: ${method} ${path} -> ${res.status} : ${JSON.stringify(data)}`);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
let drivers = [];
|
||||
let reqCount = 0;
|
||||
let errors = 0;
|
||||
let tripsCompleted = 0;
|
||||
|
||||
async function setupDriver(index) {
|
||||
const phone = `+96279${String(index).padStart(6, '0')}`;
|
||||
try {
|
||||
// Authenticate using dev bypass
|
||||
const auth = await api('POST', '/auth/verify-otp', { phone, code: '1234' });
|
||||
const token = auth.access_token;
|
||||
|
||||
// Auto-create driver profile if missing
|
||||
if (!auth.user.vehicle_plate) {
|
||||
await api('POST', '/drivers/apply', {
|
||||
service_class: 'economy',
|
||||
vehicle_make: 'Toyota',
|
||||
vehicle_model: 'Corolla',
|
||||
vehicle_plate: `${index}-TEST`
|
||||
}, token).catch(() => {});
|
||||
}
|
||||
|
||||
// Go online
|
||||
await api('PATCH', '/drivers/status', { online: true }, token);
|
||||
|
||||
const WS_URL = API_URL.replace('/api', '');
|
||||
const socket = io(WS_URL, { extraHeaders: { Authorization: `Bearer ${token}` }, reconnection: true });
|
||||
|
||||
const driverObj = { phone, token, socket, available: true };
|
||||
drivers.push(driverObj);
|
||||
|
||||
// Initial random location in Amman
|
||||
let lat = 31.95 + (Math.random() * 0.1);
|
||||
let lng = 35.91 + (Math.random() * 0.1);
|
||||
|
||||
// Start pinging location
|
||||
setInterval(() => {
|
||||
lat += 0.0001; // Move slightly
|
||||
socket.emit('driver:location', { lat, lng, speed: 40, heading: 90 });
|
||||
reqCount++;
|
||||
}, DRIVER_PING_INTERVAL_MS);
|
||||
|
||||
// Listen for trip offers
|
||||
socket.on('trip:offer', async (data) => {
|
||||
if (!driverObj.available) return;
|
||||
driverObj.available = false;
|
||||
const tripId = data.tripId || data.id;
|
||||
try {
|
||||
await api('POST', `/trips/${tripId}/accept`, null, token);
|
||||
|
||||
// Simulate trip flow timeline (shortened for stress test)
|
||||
setTimeout(async () => {
|
||||
await api('PATCH', `/trips/${tripId}/status`, { status: 'driver_arrived' }, token).catch(()=>{});
|
||||
await api('PATCH', `/trips/${tripId}/status`, { status: 'in_progress' }, token).catch(()=>{});
|
||||
await api('PATCH', `/trips/${tripId}/status`, { status: 'completed' }, token).catch(()=>{});
|
||||
driverObj.available = true; // Free to take next trip
|
||||
tripsCompleted++;
|
||||
}, 5000);
|
||||
|
||||
} catch (err) {
|
||||
driverObj.available = true;
|
||||
}
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
errors++;
|
||||
}
|
||||
}
|
||||
|
||||
async function spawnRiderAndRequestTrip(index) {
|
||||
const phone = `+96278${String(index).padStart(6, '0')}`;
|
||||
try {
|
||||
const auth = await api('POST', '/auth/verify-otp', { phone, code: '1234' });
|
||||
const token = auth.access_token;
|
||||
|
||||
// Pick a location roughly where drivers are spawned
|
||||
const lat = 31.95 + (Math.random() * 0.1);
|
||||
const lng = 35.91 + (Math.random() * 0.1);
|
||||
|
||||
await api('POST', '/trips', {
|
||||
city: 'amman',
|
||||
service_class: 'economy',
|
||||
origin: { lat, lng },
|
||||
destination: { lat: lat + 0.05, lng: lng + 0.05 }
|
||||
}, token);
|
||||
reqCount++;
|
||||
|
||||
} catch (err) {
|
||||
errors++;
|
||||
}
|
||||
}
|
||||
|
||||
async function start() {
|
||||
console.log(`\x1b[36m==================================================\x1b[0m`);
|
||||
console.log(`🚀 Starting Stress Test`);
|
||||
console.log(`\x1b[33mDrivers:\x1b[0m ${NUM_DRIVERS}`);
|
||||
console.log(`\x1b[33mTrips/Sec:\x1b[0m ${TRIP_RATE_SEC}`);
|
||||
console.log(`\x1b[33mLocation Ping Interval:\x1b[0m ${DRIVER_PING_INTERVAL_MS}ms`);
|
||||
console.log(`\x1b[36m==================================================\x1b[0m\n`);
|
||||
|
||||
// Throttle driver creation so we don't spam the DB at startup
|
||||
for (let i = 0; i < NUM_DRIVERS; i++) {
|
||||
setupDriver(i);
|
||||
if (i % 10 === 0) await new Promise(r => setTimeout(r, 100));
|
||||
}
|
||||
|
||||
console.log(`[System] Finished spawning drivers. Waiting 5s for sockets...`);
|
||||
await new Promise(r => setTimeout(r, 5000));
|
||||
console.log(`[System] Commencing trip requests...`);
|
||||
|
||||
let riderIndex = 0;
|
||||
setInterval(() => {
|
||||
for (let i = 0; i < TRIP_RATE_SEC; i++) {
|
||||
spawnRiderAndRequestTrip(riderIndex++);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
// Monitor performance locally
|
||||
setInterval(() => {
|
||||
const currentRps = reqCount / 5;
|
||||
console.log(`\x1b[32m[Metrics]\x1b[0m Active Drivers: ${drivers.length} | API/WS Requests/sec: ${currentRps.toFixed(1)} | Trips Completed: ${tripsCompleted} | Errors: ${errors}`);
|
||||
reqCount = 0;
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
start();
|
||||
Reference in New Issue
Block a user