feat: المجموعة H — المواقع في Redis، والقاعدة من الـworker فقط
كان كل نبضة موقع (كل 1-3 ثوانٍ لكل سائق متصل) تكلّف استعلاماً + كتابة صف السائق في Postgres — أثقل حمل كان على القاعدة، وأثقل حتى من سيرو. - H1/H2: نبضة الموقع = Redis فقط، بعتبات سيرو (10م / 1.0 سرعة / 5° اتجاه). سائق واقف = EXPIRE واحد: لا كتابة، لا نقطة مسار، ولا بثّ للراكب - H3: لقطة آخر موقع على drivers (heading/speed/loc_status/loc_updated_at) يكتبها الـworker دورياً. لم نُنشئ جدولاً منفصلاً — drivers أصلاً صف واحد لكل سائق - H4: driver_tracks — تتراكم في قائمة Redis ويُدرجها الـworker بعبارة واحدة - H5: فهرسا available/busy — البحث يمسح المتاحين فقط. السائق يصير busy عند القبول ويعود available عند الإنهاء/الإلغاء - H8: المطابقة والاحتيال و«الرحلات المتاحة» تقرأ الموقع الحيّ من Redis. كشف تزوير الوصول خاصةً يجب ألّا يحكم بلقطة دورية الـworker صار حقيقياً (كان setInterval فارغاً): سياق Nest مستقل، تفريغ كل 5s، قفل يمنع تراكب الدورات، وتفريغة أخيرة عند SIGTERM. مؤجَّل من H: H6 (سلوك السائق) و H7 (ساعات العمل) — تحليلات تُبنى من driver_tracks لاحقاً. هجرة: DriverLocations. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
a83dd7e5e5
commit
0ed3a3160a
@@ -0,0 +1,112 @@
|
||||
import RedisMock from 'ioredis-mock';
|
||||
import { DriverLocationService } from './driver-location.service';
|
||||
import { MatchingService } from '../matching/matching.service';
|
||||
|
||||
const TENANT = 't1';
|
||||
const DRIVER = 'd1';
|
||||
const CLASS = 'economy';
|
||||
|
||||
/** نقطة في وسط عمّان + إزاحة بالأمتار (تقريب كافٍ للاختبار). */
|
||||
const BASE = { lat: 31.9539, lng: 35.9106 };
|
||||
const metersToLat = (m: number) => m / 111_320;
|
||||
|
||||
describe('DriverLocationService', () => {
|
||||
let redis: any;
|
||||
let matching: MatchingService;
|
||||
let svc: DriverLocationService;
|
||||
let driversRepo: any;
|
||||
|
||||
beforeEach(() => {
|
||||
redis = new RedisMock({ keyPrefix: 'tripz:' });
|
||||
matching = new MatchingService(redis);
|
||||
driversRepo = { findOne: jest.fn().mockResolvedValue(null) };
|
||||
svc = new DriverLocationService(redis, driversRepo, matching);
|
||||
});
|
||||
|
||||
it('أول نبضة ذات دلالة وتُخزَّن', async () => {
|
||||
const r = await svc.update(TENANT, DRIVER, CLASS, BASE);
|
||||
expect(r.significant).toBe(true);
|
||||
|
||||
const pos = await svc.get(TENANT, DRIVER);
|
||||
expect(pos!.lat).toBeCloseTo(BASE.lat);
|
||||
expect(pos!.status).toBe('available');
|
||||
});
|
||||
|
||||
it('سائق واقف: النبضة التالية غير ذات دلالة ولا تضيف مساراً', async () => {
|
||||
await svc.update(TENANT, DRIVER, CLASS, BASE);
|
||||
const before = await redis.llen('loc:tracks');
|
||||
|
||||
// إزاحة مترين فقط — تحت عتبة 10م
|
||||
const r = await svc.update(TENANT, DRIVER, CLASS, {
|
||||
lat: BASE.lat + metersToLat(2),
|
||||
lng: BASE.lng,
|
||||
});
|
||||
|
||||
expect(r.significant).toBe(false);
|
||||
expect(await redis.llen('loc:tracks')).toBe(before);
|
||||
});
|
||||
|
||||
it('تحرّك أكثر من 10م = نبضة ذات دلالة + نقطة مسار', async () => {
|
||||
await svc.update(TENANT, DRIVER, CLASS, BASE);
|
||||
const before = await redis.llen('loc:tracks');
|
||||
|
||||
const r = await svc.update(TENANT, DRIVER, CLASS, {
|
||||
lat: BASE.lat + metersToLat(25),
|
||||
lng: BASE.lng,
|
||||
});
|
||||
|
||||
expect(r.significant).toBe(true);
|
||||
expect(await redis.llen('loc:tracks')).toBe(before + 1);
|
||||
});
|
||||
|
||||
it('تغيّر السرعة وحده يكفي رغم الوقوف', async () => {
|
||||
await svc.update(TENANT, DRIVER, CLASS, { ...BASE, speed: 0 });
|
||||
const r = await svc.update(TENANT, DRIVER, CLASS, { ...BASE, speed: 12 });
|
||||
expect(r.significant).toBe(true);
|
||||
});
|
||||
|
||||
it('السائق «متّسخ» بعد نبضة ذات دلالة — ليكتبه الـworker لاحقاً', async () => {
|
||||
await svc.update(TENANT, DRIVER, CLASS, BASE);
|
||||
expect(await redis.smembers(DriverLocationService.DIRTY_KEY)).toContain(`${TENANT}|${DRIVER}`);
|
||||
});
|
||||
|
||||
it('المتاح يظهر في المطابقة، والمشغول يختفي منها', async () => {
|
||||
await svc.update(TENANT, DRIVER, CLASS, BASE);
|
||||
expect(await matching.findNearby(TENANT, CLASS, BASE.lat, BASE.lng)).toHaveLength(1);
|
||||
|
||||
await svc.setAvailability(TENANT, DRIVER, CLASS, 'busy');
|
||||
expect(await matching.findNearby(TENANT, CLASS, BASE.lat, BASE.lng)).toHaveLength(0);
|
||||
|
||||
await svc.setAvailability(TENANT, DRIVER, CLASS, 'available');
|
||||
expect(await matching.findNearby(TENANT, CLASS, BASE.lat, BASE.lng)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('الفصل (off) يزيله من الفهرس ومن Redis', async () => {
|
||||
await svc.update(TENANT, DRIVER, CLASS, BASE);
|
||||
await svc.setAvailability(TENANT, DRIVER, CLASS, 'off');
|
||||
|
||||
expect(await matching.findNearby(TENANT, CLASS, BASE.lat, BASE.lng)).toHaveLength(0);
|
||||
expect(await svc.get(TENANT, DRIVER)).toBeNull();
|
||||
});
|
||||
|
||||
it('عند غياب المفتاح يرجع للقطة القاعدة', async () => {
|
||||
driversRepo.findOne.mockResolvedValue({
|
||||
last_lat: 31.9,
|
||||
last_lng: 35.9,
|
||||
heading: 90,
|
||||
speed: 0,
|
||||
loc_status: 'available',
|
||||
service_class: CLASS,
|
||||
loc_updated_at: new Date(),
|
||||
});
|
||||
|
||||
const pos = await svc.get(TENANT, 'cold-driver');
|
||||
expect(pos!.lat).toBe(31.9);
|
||||
expect(driversRepo.findOne).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('المستأجرون معزولون في المطابقة', async () => {
|
||||
await svc.update('tenant-a', DRIVER, CLASS, BASE);
|
||||
expect(await matching.findNearby('tenant-b', CLASS, BASE.lat, BASE.lng)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user