C1: جدول vehicles (مكافئ CarRegistration عند سيرو) — plate/vin مشفَّران، make/model/year، color+color_hex (لتلوين السيارة في فلاتر)، fuel/owner/ category، is_default، status. سائق قد يملك أكثر من مركبة؛ VehiclesService يضمن افتراضية واحدة دائماً (أول مركبة تلقائياً، حذف الافتراضية يرقّي غيرها). C2: حقول الملف على drivers — gender، national_number (مشفَّر + فهرس أعمى فريد لكل مستأجر، نفس نمط الهاتف)، name_arabic (مشفَّر)، birthdate، address، الرخصة (type/categories/issue/expiry)، rejected_reason. عبر PATCH /drivers/profile. C3: ai_data + user_input (jsonb) على drivers و vehicles — مخرجات Gemini مقابل مدخلات السائق، تُراكَم للمقارنة حقلاً بحقل. C4: vehicle_photo min:2 في كتالوج الوثائق. C5: نوع وثيقة face_liveness (فيديو) — الرفع يقبله بلا قيد mime. قرار: المركبة كيان مستقل لا حقول مسطّحة (سيرو يفصلها بـisDefault). الحقول القديمة على drivers تبقى للتوافق؛ المصدر الجديد vehicles. هجرة: VehiclesAndDriverProfile (جدول vehicles + أعمدة السائق + فهرس فريد جزئي على الرقم الوطني). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
106 lines
3.9 KiB
TypeScript
106 lines
3.9 KiB
TypeScript
import { randomUUID } from 'crypto';
|
|
import { newDb } from 'pg-mem';
|
|
import { DataSource } from 'typeorm';
|
|
import { Vehicle } from './entities/vehicle.entity';
|
|
import { VehiclesService } from './vehicles.service';
|
|
|
|
const TENANT = '11111111-1111-1111-1111-111111111111';
|
|
const DRIVER = '22222222-2222-2222-2222-222222222222';
|
|
|
|
describe('VehiclesService (docs/17 — C1)', () => {
|
|
let ds: DataSource;
|
|
let svc: VehiclesService;
|
|
|
|
beforeEach(async () => {
|
|
const db = newDb({ autoCreateForeignKeyIndices: true });
|
|
db.public.registerFunction({ name: 'version', returns: 'text' as any, implementation: () => 'pg-mem' });
|
|
db.public.registerFunction({
|
|
name: 'current_database',
|
|
returns: 'text' as any,
|
|
implementation: () => 'tripz',
|
|
});
|
|
db.registerExtension('uuid-ossp', (schema) =>
|
|
schema.registerFunction({
|
|
name: 'uuid_generate_v4',
|
|
returns: 'uuid' as any,
|
|
implementation: () => randomUUID(),
|
|
impure: true,
|
|
}),
|
|
);
|
|
await db.public.none(`CREATE EXTENSION "uuid-ossp"`);
|
|
|
|
ds = (await db.adapters.createTypeormDataSource({
|
|
type: 'postgres',
|
|
entities: [Vehicle],
|
|
entityPrefix: 'tripz_',
|
|
})) as DataSource;
|
|
await ds.initialize();
|
|
await ds.synchronize();
|
|
|
|
svc = new VehiclesService(ds.getRepository(Vehicle));
|
|
});
|
|
|
|
afterEach(async () => {
|
|
if (ds?.isInitialized) await ds.destroy();
|
|
});
|
|
|
|
it('أول مركبة تصير الافتراضية تلقائياً', async () => {
|
|
const v = await svc.create(TENANT, DRIVER, { make: 'Toyota', model: 'Corolla' });
|
|
expect(v.is_default).toBe(true);
|
|
expect(v.status).toBe('pending');
|
|
});
|
|
|
|
it('المركبة الثانية ليست افتراضية إلا بطلب صريح', async () => {
|
|
await svc.create(TENANT, DRIVER, { make: 'Toyota' });
|
|
const second = await svc.create(TENANT, DRIVER, { make: 'Kia' });
|
|
expect(second.is_default).toBe(false);
|
|
});
|
|
|
|
it('تعيين افتراضي جديد يلغي القديم — واحدة افتراضية فقط دائماً', async () => {
|
|
const first = await svc.create(TENANT, DRIVER, { make: 'Toyota' });
|
|
const second = await svc.create(TENANT, DRIVER, { make: 'Kia' });
|
|
|
|
await svc.setDefault(TENANT, DRIVER, second.id);
|
|
const list = await svc.list(TENANT, DRIVER);
|
|
const defaults = list.filter((v) => v.is_default);
|
|
expect(defaults).toHaveLength(1);
|
|
expect(defaults[0].id).toBe(second.id);
|
|
expect((await svc.getDefault(TENANT, DRIVER))!.id).toBe(second.id);
|
|
});
|
|
|
|
it('حذف الافتراضية يرقّي مركبة أخرى — لا يبقى السائق بلا افتراضية', async () => {
|
|
const first = await svc.create(TENANT, DRIVER, { make: 'Toyota' });
|
|
await svc.create(TENANT, DRIVER, { make: 'Kia' });
|
|
|
|
await svc.remove(TENANT, DRIVER, first.id);
|
|
const def = await svc.getDefault(TENANT, DRIVER);
|
|
expect(def).not.toBeNull();
|
|
expect(def!.is_default).toBe(true);
|
|
});
|
|
|
|
it('اللوحة والـVIN مشفَّران at-rest، والباقي واضح', async () => {
|
|
const v = await svc.create(TENANT, DRIVER, {
|
|
plate: 'ABC-123',
|
|
vin: '1HGCM82633A004352',
|
|
make: 'Honda',
|
|
color_hex: '#FF0000',
|
|
});
|
|
|
|
const raw = await ds.query(`SELECT plate, vin, make, color_hex FROM tripz_vehicles WHERE id = $1`, [v.id]);
|
|
expect(raw[0].plate).toMatch(/^v1:/); // مشفّر
|
|
expect(raw[0].vin).toMatch(/^v1:/);
|
|
expect(raw[0].make).toBe('Honda'); // غير حسّاس — واضح
|
|
expect(raw[0].color_hex).toBe('#FF0000');
|
|
|
|
// لكنه يُقرأ مفكوكاً عبر الـORM
|
|
const read = await svc.list(TENANT, DRIVER);
|
|
expect(read[0].plate).toBe('ABC-123');
|
|
expect(read[0].vin).toBe('1HGCM82633A004352');
|
|
});
|
|
|
|
it('المستأجرون معزولون', async () => {
|
|
await svc.create(TENANT, DRIVER, { make: 'Toyota' });
|
|
expect(await svc.list('33333333-3333-3333-3333-333333333333', DRIVER)).toHaveLength(0);
|
|
});
|
|
});
|