feat: rebuild pricing on Siro's live kazan table + close two fare leaks
## الأرقام صارت من جدول `kazan` الحيّ لا من تقدير العمولة 11٪/12٪/14٪ مطابقة حرفياً. وثلاثة تصحيحات على الخام: - **محورا الأردن مقلوبان**: أعمدة الفئات تحمل سعر الدقيقة (0.043) وأعمدة النوافذ سعر الكيلومتر (0.184) — عكس مصر وسوريا. قراءتها كما هي تجعل رحلة 27 كم بأقلّ من دينارين. - **نِسَب الفئات في مصر وسوريا معطوبة**: الكهربائي 0.23 والفان 0.25–0.40 من الاقتصادي، أي فان أرخص من اقتصادي. نِسَب الأردن وحدها متماسكة فاعتُمدت. - سعر الكيلو في مصر 4 جنيهات لا 3.5 (قرار المالك). ## البنية صارت مطابقة لسيرو — وهي ما سيكتب فيه بوت استخبارات الأسعار سعر الكيلو يتغيّر بالفئة فقط (رقم يعرفه السوق)، وسعر الدقيقة بالنافذة فقط. **لا فتحة عدّاد** (`startPrice = 0` في كل صفوف سيرو): الحدّ الأدنى أرضية لا رسم يُضاف — «ما في رحلة أقل من 20 جنيهاً». `fuelPrice` محفوظ كمدخل للبوت. ## تسريبان أغلقهما الاختبار الحيّ (بموافقة المالك) 1. **فئة مجهولة = رحلة مجانية**: `service_class` يأتي من الطلب بلا تحقّق، والاستثناء كان يُبتلع فتُنشأ رحلة بـ`quoted_fare = null` ثم تُسوّى بصفر. صار الطلب يُرفض بـ400 — الفشل الصريح أأمن من الصامت. 2. **`fixed` لا تُزرع**: سعر ثابت بلا جدول خطوط يعني رقماً واحداً لكل الوجهات — أظهر الاختبار رحلة 55.8 كم بـ2.5 ديناراً مقابل 23.2 للاقتصادي. 300 اختبار خضر. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
f58f2e2e2c
commit
e9beeb90b4
@@ -6,6 +6,8 @@ import {
|
||||
buildDefinition,
|
||||
COUNTRY_TARIFFS as countries,
|
||||
SERVICE_CLASSES as ServiceClasses,
|
||||
SEEDED_CLASSES,
|
||||
CLASS_MULTIPLIERS,
|
||||
WINDOWS,
|
||||
countryTariff as byPack,
|
||||
} from './default-tariffs';
|
||||
@@ -34,27 +36,39 @@ describe('تعرفة الانطلاق — الحدود الدنيا بقرار
|
||||
}
|
||||
});
|
||||
|
||||
it('فتحة العدّاد موجودة في كل نافذة ولكل بلد — لا صفر', () => {
|
||||
it('**لا فتحة عدّاد ولا رسم حجز** — الأرضية هي الآلية', () => {
|
||||
// `startPrice = 0` في كل صفوف `kazan`، وقرار المالك أن «ما في رحلة أقل
|
||||
// من 20 جنيهاً» تُنفَّذ كأرضية لا كرسم يُضاف فوق كل أجرة.
|
||||
for (const country of countries) {
|
||||
for (const w of def(country.countryPack, ServiceClasses.ECONOMY).windows) {
|
||||
// الفتحة صفراً كانت تجعل الرحلة القصيرة خاسرة على السائق.
|
||||
expect(w.flag).toBeGreaterThan(0);
|
||||
}
|
||||
const d = def(country.countryPack, ServiceClasses.ECONOMY);
|
||||
expect(d.booking_fee).toBe(0);
|
||||
for (const w of d.windows) expect(w.flag).toBe(0);
|
||||
}
|
||||
});
|
||||
|
||||
it('الأرضية ترفع الرحلة القصيرة فقط — الطويلة تُحاسَب بالعدّاد', () => {
|
||||
const d = def('eg', ServiceClasses.ECONOMY);
|
||||
const short = TariffEngine.quote(d, { distanceKm: 1, durationMin: 3, at: NOON_AMMAN });
|
||||
const long = TariffEngine.quote(d, { distanceKm: 30, durationMin: 40, at: NOON_AMMAN });
|
||||
expect(short.total).toBe(20); // الأرضية
|
||||
expect(long.total).toBeGreaterThan(100); // العدّاد وحده
|
||||
});
|
||||
});
|
||||
|
||||
describe('تعرفة الانطلاق — ترتيب الفئات', () => {
|
||||
// الخلل الذي كسر النسخة السابقة: الفان أرخص من الاقتصادي في مصر وسوريا،
|
||||
// فيصير الخيار العقلاني لكل راكب هو أغلى فئة على المشغّل.
|
||||
// الترتيب من نِسَب `kazan` (صفّ الأردن): الفان **أغلى من VIP** — مركبة
|
||||
// بسبعة مقاعد أعلى كلفة من سيارة أفخم.
|
||||
const ordered = [
|
||||
ServiceClasses.SAVER,
|
||||
ServiceClasses.DELIVERY,
|
||||
ServiceClasses.ECONOMY,
|
||||
ServiceClasses.ELECTRIC,
|
||||
ServiceClasses.LADY,
|
||||
ServiceClasses.ELECTRIC,
|
||||
ServiceClasses.COMFORT,
|
||||
ServiceClasses.VAN,
|
||||
ServiceClasses.VIP,
|
||||
ServiceClasses.VAN,
|
||||
];
|
||||
|
||||
for (const country of countries) {
|
||||
@@ -62,8 +76,8 @@ describe('تعرفة الانطلاق — ترتيب الفئات', () => {
|
||||
const fares = ordered.map(
|
||||
(cls) =>
|
||||
TariffEngine.quote(def(country.countryPack, cls), {
|
||||
distanceKm: 5,
|
||||
durationMin: 15,
|
||||
distanceKm: 40, // مسافة كبيرة تتجاوز الأرضية في كل الفئات
|
||||
durationMin: 50,
|
||||
at: NOON_AMMAN,
|
||||
}).total,
|
||||
);
|
||||
@@ -171,3 +185,65 @@ describe('العمولة', () => {
|
||||
expect(c.amount).toBeCloseTo(q.total * 0.14, 2);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('مطابقة جدول kazan (بيانات سيرو الحيّة)', () => {
|
||||
it('العمولة مطابقة حرفياً: 11٪ سوريا · 12٪ مصر · 14٪ الأردن', () => {
|
||||
expect(byPack('sy').commissionPercent).toBe(11);
|
||||
expect(byPack('eg').commissionPercent).toBe(12);
|
||||
expect(byPack('jo').commissionPercent).toBe(14);
|
||||
});
|
||||
|
||||
it('**محورا الأردن مقروءان بالاتجاه الصحيح** — الكيلو أغلى من الدقيقة', () => {
|
||||
// في `kazan` كان صفّ الأردن مقلوباً: أعمدة الفئات تحمل سعر الدقيقة
|
||||
// (0.043) وأعمدة النوافذ سعر الكيلومتر (0.184). قراءته كما هو تجعل
|
||||
// الكيلومتر بـ0.043 ديناراً — أي رحلة 27 كم بأقلّ من دينارين.
|
||||
const w = def('jo', ServiceClasses.ECONOMY).windows[1]; // normal_day
|
||||
expect(w.per_km).toBe(0.184);
|
||||
expect(w.per_min).toBe(0.043);
|
||||
expect(w.per_km).toBeGreaterThan(w.per_min);
|
||||
});
|
||||
|
||||
it('سعر الكيلو في مصر 4 جنيهات (قرار المالك) لا 3.5', () => {
|
||||
expect(byPack('eg').perKm).toBe(4.0);
|
||||
});
|
||||
|
||||
it('**سعر الكيلو ثابت عبر النوافذ** — الوقت يحرّك الدقيقة لا المسافة', () => {
|
||||
// «الكيلو بأربعة جنيه» رقم يعرفه السوق، فلا يصحّ أن يتذبذب بالساعة.
|
||||
for (const country of countries) {
|
||||
const d = def(country.countryPack, ServiceClasses.ECONOMY);
|
||||
const kms = new Set(d.windows.map((w) => w.per_km));
|
||||
expect(kms.size).toBe(1);
|
||||
// بينما الدقيقة تتغيّر فعلاً بين النوافذ (حيث تختلف أرقام البلد).
|
||||
const mins = new Set(d.windows.map((w) => w.per_min));
|
||||
expect(mins.size).toBeGreaterThan(1);
|
||||
}
|
||||
});
|
||||
|
||||
it('نِسَب الفئات معطوبة في kazan لمصر وسوريا — نِسَب الأردن هي المعتمدة', () => {
|
||||
// الخام: الكهربائي 0.23 والفان 0.25–0.40 من الاقتصادي، أي فان أرخص من
|
||||
// اقتصادي. المعتمد هنا متصاعد ولا ينعكس.
|
||||
expect(CLASS_MULTIPLIERS.van).toBeGreaterThan(CLASS_MULTIPLIERS.economy);
|
||||
expect(CLASS_MULTIPLIERS.electric).toBeGreaterThan(CLASS_MULTIPLIERS.economy);
|
||||
expect(CLASS_MULTIPLIERS.delivery).toBeLessThan(CLASS_MULTIPLIERS.economy);
|
||||
});
|
||||
|
||||
it('سعر الوقود محفوظ لبوت استخبارات الأسعار', () => {
|
||||
expect(byPack('sy').fuelPrice).toBe(110);
|
||||
expect(byPack('eg').fuelPrice).toBe(13);
|
||||
expect(byPack('jo').fuelPrice).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('فئة fixed لا تُزرع', () => {
|
||||
it('**مستثناة من الزرع** — سعر ثابت بلا جدول خطوط تسريب', () => {
|
||||
// أثبت الاختبار الحيّ: رحلة 55.8 كم بـ2.5 ديناراً مقابل 23.2 للاقتصادي.
|
||||
expect(SEEDED_CLASSES).not.toContain(ServiceClasses.FIXED);
|
||||
expect(SEEDED_CLASSES).toHaveLength(8);
|
||||
});
|
||||
|
||||
it('تبقى معرَّفة في الكتالوج — تُفعَّل حين يُبنى جدول الخطوط', () => {
|
||||
expect(Object.values(ServiceClasses)).toContain(ServiceClasses.FIXED);
|
||||
expect(def('jo', ServiceClasses.FIXED).mode).toBe('fixed_quote');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user