feat: المجموعة C — بيانات السائق والمركبة (كيان مركبة + ملف السائق)
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>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
4e9d4d9be1
commit
c2a4b9d1a6
@@ -0,0 +1,105 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user