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:
Hamza-Ayed
2026-07-27 04:54:35 +03:00
co-authored by Claude Opus 5
parent 75b80bd299
commit fff76c949f
307 changed files with 47 additions and 0 deletions
+115
View File
@@ -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"
}
}
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"name": "tripz-e2e-tester",
"version": "1.0.0",
"type": "module",
"dependencies": {
"socket.io-client": "^4.7.5"
}
}
+162
View File
@@ -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();
+155
View File
@@ -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();