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:
Hamza-Ayed
2026-07-19 01:38:20 +03:00
co-authored by Claude Opus 4.8
parent f58f2e2e2c
commit e9beeb90b4
9 changed files with 250 additions and 165 deletions
+2 -2
View File
@@ -5,7 +5,7 @@ import { RideTypesService } from '../../modules/ride-types/ride-types.service';
import {
buildDefinition,
countryTariff,
SERVICE_CLASSES,
SEEDED_CLASSES,
} from '../../modules/tariff/default-tariffs';
/**
@@ -54,7 +54,7 @@ export class SeedService implements OnModuleInit {
const country = countryTariff(tenant.countryPack);
let created = 0;
let skipped = 0;
for (const serviceClass of Object.values(SERVICE_CLASSES)) {
for (const serviceClass of SEEDED_CLASSES) {
const existing = await this.tariff.getActive(tenant.id, 'default', serviceClass);
if (existing) {
skipped++;
@@ -3,7 +3,7 @@ import { join } from 'path';
import {
buildDefinition,
COUNTRY_TARIFFS,
SERVICE_CLASSES,
SEEDED_CLASSES,
} from '../../modules/tariff/default-tariffs';
/**
@@ -30,7 +30,7 @@ function generateSql(): string {
for (const country of COUNTRY_TARIFFS) {
sql += `-- ============ ${country.countryPack.toUpperCase()} (${country.currency}) ============\n`;
for (const serviceClass of Object.values(SERVICE_CLASSES)) {
for (const serviceClass of SEEDED_CLASSES) {
const definition = buildDefinition(country, serviceClass);
const defJson = JSON.stringify(definition).replace(/'/g, "''");
+24 -60
View File
@@ -15,7 +15,7 @@ SELECT uuid_generate_v4(), t.id, 'default', 'economy',
COALESCE((SELECT MAX(version) FROM tripz_tariffs x
WHERE x.tenant_id = t.id AND x.city = 'default'
AND x.service_class = 'economy'), 0) + 1,
true, '{"currency":"JOD","timezone":"Asia/Amman","mode":"time_and_distance","rounding":{"increment":0.05,"mode":"nearest"},"windows":[{"name":"morning_peak","from":"07:00","to":"09:30","flag":0.488,"per_km":0.35,"per_min":0.075,"per_min_waiting":0.6},{"name":"normal_day","from":"09:30","to":"16:00","flag":0.39,"per_km":0.28,"per_min":0.06,"per_min_waiting":0.48},{"name":"evening_peak","from":"16:00","to":"19:30","flag":0.488,"per_km":0.35,"per_min":0.075,"per_min_waiting":0.6},{"name":"normal_evening","from":"19:30","to":"23:00","flag":0.39,"per_km":0.28,"per_min":0.06,"per_min_waiting":0.48},{"name":"late_night","from":"23:00","to":"07:00","flag":0.448,"per_km":0.322,"per_min":0.069,"per_min_waiting":0.552}],"booking_fee":0.25,"min_fare":1.1,"free_waiting_min":5,"surge":{"enabled":false},"commission":{"percent":14}}'::jsonb
true, '{"currency":"JOD","timezone":"Asia/Amman","mode":"time_and_distance","rounding":{"increment":0.05,"mode":"nearest"},"windows":[{"name":"morning_peak","from":"07:00","to":"09:30","flag":0,"per_km":0.184,"per_min":0.05,"per_min_waiting":0.074},{"name":"normal_day","from":"09:30","to":"16:00","flag":0,"per_km":0.184,"per_min":0.043,"per_min_waiting":0.065},{"name":"evening_peak","from":"16:00","to":"19:30","flag":0,"per_km":0.184,"per_min":0.05,"per_min_waiting":0.074},{"name":"normal_evening","from":"19:30","to":"23:00","flag":0,"per_km":0.184,"per_min":0.043,"per_min_waiting":0.065},{"name":"late_night","from":"23:00","to":"07:00","flag":0,"per_km":0.184,"per_min":0.043,"per_min_waiting":0.065}],"booking_fee":0,"min_fare":1.1,"free_waiting_min":5,"surge":{"enabled":false},"commission":{"percent":14}}'::jsonb
FROM tripz_tenants t WHERE t.country_pack = 'jo';
-- comfort
@@ -27,7 +27,7 @@ SELECT uuid_generate_v4(), t.id, 'default', 'comfort',
COALESCE((SELECT MAX(version) FROM tripz_tariffs x
WHERE x.tenant_id = t.id AND x.city = 'default'
AND x.service_class = 'comfort'), 0) + 1,
true, '{"currency":"JOD","timezone":"Asia/Amman","mode":"time_and_distance","rounding":{"increment":0.05,"mode":"nearest"},"windows":[{"name":"morning_peak","from":"07:00","to":"09:30","flag":0.609,"per_km":0.438,"per_min":0.094,"per_min_waiting":0.6},{"name":"normal_day","from":"09:30","to":"16:00","flag":0.488,"per_km":0.35,"per_min":0.075,"per_min_waiting":0.48},{"name":"evening_peak","from":"16:00","to":"19:30","flag":0.609,"per_km":0.438,"per_min":0.094,"per_min_waiting":0.6},{"name":"normal_evening","from":"19:30","to":"23:00","flag":0.488,"per_km":0.35,"per_min":0.075,"per_min_waiting":0.48},{"name":"late_night","from":"23:00","to":"07:00","flag":0.561,"per_km":0.403,"per_min":0.086,"per_min_waiting":0.552}],"booking_fee":0.25,"min_fare":1.1,"free_waiting_min":5,"surge":{"enabled":false},"commission":{"percent":14}}'::jsonb
true, '{"currency":"JOD","timezone":"Asia/Amman","mode":"time_and_distance","rounding":{"increment":0.05,"mode":"nearest"},"windows":[{"name":"morning_peak","from":"07:00","to":"09:30","flag":0,"per_km":0.239,"per_min":0.05,"per_min_waiting":0.074},{"name":"normal_day","from":"09:30","to":"16:00","flag":0,"per_km":0.239,"per_min":0.043,"per_min_waiting":0.065},{"name":"evening_peak","from":"16:00","to":"19:30","flag":0,"per_km":0.239,"per_min":0.05,"per_min_waiting":0.074},{"name":"normal_evening","from":"19:30","to":"23:00","flag":0,"per_km":0.239,"per_min":0.043,"per_min_waiting":0.065},{"name":"late_night","from":"23:00","to":"07:00","flag":0,"per_km":0.239,"per_min":0.043,"per_min_waiting":0.065}],"booking_fee":0,"min_fare":1.1,"free_waiting_min":5,"surge":{"enabled":false},"commission":{"percent":14}}'::jsonb
FROM tripz_tenants t WHERE t.country_pack = 'jo';
-- lady
@@ -39,7 +39,7 @@ SELECT uuid_generate_v4(), t.id, 'default', 'lady',
COALESCE((SELECT MAX(version) FROM tripz_tariffs x
WHERE x.tenant_id = t.id AND x.city = 'default'
AND x.service_class = 'lady'), 0) + 1,
true, '{"currency":"JOD","timezone":"Asia/Amman","mode":"time_and_distance","rounding":{"increment":0.05,"mode":"nearest"},"windows":[{"name":"morning_peak","from":"07:00","to":"09:30","flag":0.536,"per_km":0.385,"per_min":0.083,"per_min_waiting":0.6},{"name":"normal_day","from":"09:30","to":"16:00","flag":0.429,"per_km":0.308,"per_min":0.066,"per_min_waiting":0.48},{"name":"evening_peak","from":"16:00","to":"19:30","flag":0.536,"per_km":0.385,"per_min":0.083,"per_min_waiting":0.6},{"name":"normal_evening","from":"19:30","to":"23:00","flag":0.429,"per_km":0.308,"per_min":0.066,"per_min_waiting":0.48},{"name":"late_night","from":"23:00","to":"07:00","flag":0.493,"per_km":0.354,"per_min":0.076,"per_min_waiting":0.552}],"booking_fee":0.25,"min_fare":1.1,"free_waiting_min":5,"surge":{"enabled":false},"commission":{"percent":14}}'::jsonb
true, '{"currency":"JOD","timezone":"Asia/Amman","mode":"time_and_distance","rounding":{"increment":0.05,"mode":"nearest"},"windows":[{"name":"morning_peak","from":"07:00","to":"09:30","flag":0,"per_km":0.201,"per_min":0.05,"per_min_waiting":0.074},{"name":"normal_day","from":"09:30","to":"16:00","flag":0,"per_km":0.201,"per_min":0.043,"per_min_waiting":0.065},{"name":"evening_peak","from":"16:00","to":"19:30","flag":0,"per_km":0.201,"per_min":0.05,"per_min_waiting":0.074},{"name":"normal_evening","from":"19:30","to":"23:00","flag":0,"per_km":0.201,"per_min":0.043,"per_min_waiting":0.065},{"name":"late_night","from":"23:00","to":"07:00","flag":0,"per_km":0.201,"per_min":0.043,"per_min_waiting":0.065}],"booking_fee":0,"min_fare":1.1,"free_waiting_min":5,"surge":{"enabled":false},"commission":{"percent":14}}'::jsonb
FROM tripz_tenants t WHERE t.country_pack = 'jo';
-- electric
@@ -51,7 +51,7 @@ SELECT uuid_generate_v4(), t.id, 'default', 'electric',
COALESCE((SELECT MAX(version) FROM tripz_tariffs x
WHERE x.tenant_id = t.id AND x.city = 'default'
AND x.service_class = 'electric'), 0) + 1,
true, '{"currency":"JOD","timezone":"Asia/Amman","mode":"time_and_distance","rounding":{"increment":0.05,"mode":"nearest"},"windows":[{"name":"morning_peak","from":"07:00","to":"09:30","flag":0.512,"per_km":0.368,"per_min":0.079,"per_min_waiting":0.6},{"name":"normal_day","from":"09:30","to":"16:00","flag":0.41,"per_km":0.294,"per_min":0.063,"per_min_waiting":0.48},{"name":"evening_peak","from":"16:00","to":"19:30","flag":0.512,"per_km":0.368,"per_min":0.079,"per_min_waiting":0.6},{"name":"normal_evening","from":"19:30","to":"23:00","flag":0.41,"per_km":0.294,"per_min":0.063,"per_min_waiting":0.48},{"name":"late_night","from":"23:00","to":"07:00","flag":0.471,"per_km":0.338,"per_min":0.072,"per_min_waiting":0.552}],"booking_fee":0.25,"min_fare":1.1,"free_waiting_min":5,"surge":{"enabled":false},"commission":{"percent":14}}'::jsonb
true, '{"currency":"JOD","timezone":"Asia/Amman","mode":"time_and_distance","rounding":{"increment":0.05,"mode":"nearest"},"windows":[{"name":"morning_peak","from":"07:00","to":"09:30","flag":0,"per_km":0.223,"per_min":0.05,"per_min_waiting":0.074},{"name":"normal_day","from":"09:30","to":"16:00","flag":0,"per_km":0.223,"per_min":0.043,"per_min_waiting":0.065},{"name":"evening_peak","from":"16:00","to":"19:30","flag":0,"per_km":0.223,"per_min":0.05,"per_min_waiting":0.074},{"name":"normal_evening","from":"19:30","to":"23:00","flag":0,"per_km":0.223,"per_min":0.043,"per_min_waiting":0.065},{"name":"late_night","from":"23:00","to":"07:00","flag":0,"per_km":0.223,"per_min":0.043,"per_min_waiting":0.065}],"booking_fee":0,"min_fare":1.1,"free_waiting_min":5,"surge":{"enabled":false},"commission":{"percent":14}}'::jsonb
FROM tripz_tenants t WHERE t.country_pack = 'jo';
-- van
@@ -63,7 +63,7 @@ SELECT uuid_generate_v4(), t.id, 'default', 'van',
COALESCE((SELECT MAX(version) FROM tripz_tariffs x
WHERE x.tenant_id = t.id AND x.city = 'default'
AND x.service_class = 'van'), 0) + 1,
true, '{"currency":"JOD","timezone":"Asia/Amman","mode":"time_and_distance","rounding":{"increment":0.05,"mode":"nearest"},"windows":[{"name":"morning_peak","from":"07:00","to":"09:30","flag":0.707,"per_km":0.508,"per_min":0.109,"per_min_waiting":0.6},{"name":"normal_day","from":"09:30","to":"16:00","flag":0.566,"per_km":0.406,"per_min":0.087,"per_min_waiting":0.48},{"name":"evening_peak","from":"16:00","to":"19:30","flag":0.707,"per_km":0.508,"per_min":0.109,"per_min_waiting":0.6},{"name":"normal_evening","from":"19:30","to":"23:00","flag":0.566,"per_km":0.406,"per_min":0.087,"per_min_waiting":0.48},{"name":"late_night","from":"23:00","to":"07:00","flag":0.65,"per_km":0.467,"per_min":0.1,"per_min_waiting":0.552}],"booking_fee":0.25,"min_fare":1.1,"free_waiting_min":5,"surge":{"enabled":false},"commission":{"percent":14}}'::jsonb
true, '{"currency":"JOD","timezone":"Asia/Amman","mode":"time_and_distance","rounding":{"increment":0.05,"mode":"nearest"},"windows":[{"name":"morning_peak","from":"07:00","to":"09:30","flag":0,"per_km":0.278,"per_min":0.05,"per_min_waiting":0.074},{"name":"normal_day","from":"09:30","to":"16:00","flag":0,"per_km":0.278,"per_min":0.043,"per_min_waiting":0.065},{"name":"evening_peak","from":"16:00","to":"19:30","flag":0,"per_km":0.278,"per_min":0.05,"per_min_waiting":0.074},{"name":"normal_evening","from":"19:30","to":"23:00","flag":0,"per_km":0.278,"per_min":0.043,"per_min_waiting":0.065},{"name":"late_night","from":"23:00","to":"07:00","flag":0,"per_km":0.278,"per_min":0.043,"per_min_waiting":0.065}],"booking_fee":0,"min_fare":1.1,"free_waiting_min":5,"surge":{"enabled":false},"commission":{"percent":14}}'::jsonb
FROM tripz_tenants t WHERE t.country_pack = 'jo';
-- delivery
@@ -75,7 +75,7 @@ SELECT uuid_generate_v4(), t.id, 'default', 'delivery',
COALESCE((SELECT MAX(version) FROM tripz_tariffs x
WHERE x.tenant_id = t.id AND x.city = 'default'
AND x.service_class = 'delivery'), 0) + 1,
true, '{"currency":"JOD","timezone":"Asia/Amman","mode":"time_and_distance","rounding":{"increment":0.05,"mode":"nearest"},"windows":[{"name":"morning_peak","from":"07:00","to":"09:30","flag":0.439,"per_km":0.315,"per_min":0.068,"per_min_waiting":0.6},{"name":"normal_day","from":"09:30","to":"16:00","flag":0.351,"per_km":0.252,"per_min":0.054,"per_min_waiting":0.48},{"name":"evening_peak","from":"16:00","to":"19:30","flag":0.439,"per_km":0.315,"per_min":0.068,"per_min_waiting":0.6},{"name":"normal_evening","from":"19:30","to":"23:00","flag":0.351,"per_km":0.252,"per_min":0.054,"per_min_waiting":0.48},{"name":"late_night","from":"23:00","to":"07:00","flag":0.404,"per_km":0.29,"per_min":0.062,"per_min_waiting":0.552}],"booking_fee":0.25,"min_fare":1.1,"free_waiting_min":5,"surge":{"enabled":false},"commission":{"percent":14}}'::jsonb
true, '{"currency":"JOD","timezone":"Asia/Amman","mode":"time_and_distance","rounding":{"increment":0.05,"mode":"nearest"},"windows":[{"name":"morning_peak","from":"07:00","to":"09:30","flag":0,"per_km":0.167,"per_min":0.05,"per_min_waiting":0.074},{"name":"normal_day","from":"09:30","to":"16:00","flag":0,"per_km":0.167,"per_min":0.043,"per_min_waiting":0.065},{"name":"evening_peak","from":"16:00","to":"19:30","flag":0,"per_km":0.167,"per_min":0.05,"per_min_waiting":0.074},{"name":"normal_evening","from":"19:30","to":"23:00","flag":0,"per_km":0.167,"per_min":0.043,"per_min_waiting":0.065},{"name":"late_night","from":"23:00","to":"07:00","flag":0,"per_km":0.167,"per_min":0.043,"per_min_waiting":0.065}],"booking_fee":0,"min_fare":1.1,"free_waiting_min":5,"surge":{"enabled":false},"commission":{"percent":14}}'::jsonb
FROM tripz_tenants t WHERE t.country_pack = 'jo';
-- vip
@@ -87,19 +87,7 @@ SELECT uuid_generate_v4(), t.id, 'default', 'vip',
COALESCE((SELECT MAX(version) FROM tripz_tariffs x
WHERE x.tenant_id = t.id AND x.city = 'default'
AND x.service_class = 'vip'), 0) + 1,
true, '{"currency":"JOD","timezone":"Asia/Amman","mode":"time_and_distance","rounding":{"increment":0.05,"mode":"nearest"},"windows":[{"name":"morning_peak","from":"07:00","to":"09:30","flag":0.853,"per_km":0.613,"per_min":0.131,"per_min_waiting":0.6},{"name":"normal_day","from":"09:30","to":"16:00","flag":0.682,"per_km":0.49,"per_min":0.105,"per_min_waiting":0.48},{"name":"evening_peak","from":"16:00","to":"19:30","flag":0.853,"per_km":0.613,"per_min":0.131,"per_min_waiting":0.6},{"name":"normal_evening","from":"19:30","to":"23:00","flag":0.682,"per_km":0.49,"per_min":0.105,"per_min_waiting":0.48},{"name":"late_night","from":"23:00","to":"07:00","flag":0.785,"per_km":0.564,"per_min":0.121,"per_min_waiting":0.552}],"booking_fee":0.25,"min_fare":1.1,"free_waiting_min":5,"surge":{"enabled":false},"commission":{"percent":14}}'::jsonb
FROM tripz_tenants t WHERE t.country_pack = 'jo';
-- fixed
UPDATE tripz_tariffs SET active = false
WHERE city = 'default' AND service_class = 'fixed' AND active = true
AND tenant_id IN (SELECT id FROM tripz_tenants WHERE country_pack = 'jo');
INSERT INTO tripz_tariffs (id, tenant_id, city, service_class, version, active, definition)
SELECT uuid_generate_v4(), t.id, 'default', 'fixed',
COALESCE((SELECT MAX(version) FROM tripz_tariffs x
WHERE x.tenant_id = t.id AND x.city = 'default'
AND x.service_class = 'fixed'), 0) + 1,
true, '{"currency":"JOD","timezone":"Asia/Amman","mode":"fixed_quote","rounding":{"increment":0.05,"mode":"nearest"},"windows":[{"name":"morning_peak","from":"07:00","to":"09:30","flag":0.488,"per_km":0.35,"per_min":0.075,"per_min_waiting":0.6},{"name":"normal_day","from":"09:30","to":"16:00","flag":0.39,"per_km":0.28,"per_min":0.06,"per_min_waiting":0.48},{"name":"evening_peak","from":"16:00","to":"19:30","flag":0.488,"per_km":0.35,"per_min":0.075,"per_min_waiting":0.6},{"name":"normal_evening","from":"19:30","to":"23:00","flag":0.39,"per_km":0.28,"per_min":0.06,"per_min_waiting":0.48},{"name":"late_night","from":"23:00","to":"07:00","flag":0.448,"per_km":0.322,"per_min":0.069,"per_min_waiting":0.552}],"booking_fee":0,"min_fare":0,"free_waiting_min":5,"surge":{"enabled":false},"commission":{"percent":14},"fixed_fare":2.5}'::jsonb
true, '{"currency":"JOD","timezone":"Asia/Amman","mode":"time_and_distance","rounding":{"increment":0.05,"mode":"nearest"},"windows":[{"name":"morning_peak","from":"07:00","to":"09:30","flag":0,"per_km":0.258,"per_min":0.05,"per_min_waiting":0.074},{"name":"normal_day","from":"09:30","to":"16:00","flag":0,"per_km":0.258,"per_min":0.043,"per_min_waiting":0.065},{"name":"evening_peak","from":"16:00","to":"19:30","flag":0,"per_km":0.258,"per_min":0.05,"per_min_waiting":0.074},{"name":"normal_evening","from":"19:30","to":"23:00","flag":0,"per_km":0.258,"per_min":0.043,"per_min_waiting":0.065},{"name":"late_night","from":"23:00","to":"07:00","flag":0,"per_km":0.258,"per_min":0.043,"per_min_waiting":0.065}],"booking_fee":0,"min_fare":1.1,"free_waiting_min":5,"surge":{"enabled":false},"commission":{"percent":14}}'::jsonb
FROM tripz_tenants t WHERE t.country_pack = 'jo';
-- saver
@@ -111,7 +99,7 @@ SELECT uuid_generate_v4(), t.id, 'default', 'saver',
COALESCE((SELECT MAX(version) FROM tripz_tariffs x
WHERE x.tenant_id = t.id AND x.city = 'default'
AND x.service_class = 'saver'), 0) + 1,
true, '{"currency":"JOD","timezone":"Asia/Amman","mode":"time_and_distance","rounding":{"increment":0.05,"mode":"nearest"},"windows":[{"name":"morning_peak","from":"07:00","to":"09:30","flag":0.414,"per_km":0.298,"per_min":0.064,"per_min_waiting":0.6},{"name":"normal_day","from":"09:30","to":"16:00","flag":0.332,"per_km":0.238,"per_min":0.051,"per_min_waiting":0.48},{"name":"evening_peak","from":"16:00","to":"19:30","flag":0.414,"per_km":0.298,"per_min":0.064,"per_min_waiting":0.6},{"name":"normal_evening","from":"19:30","to":"23:00","flag":0.332,"per_km":0.238,"per_min":0.051,"per_min_waiting":0.48},{"name":"late_night","from":"23:00","to":"07:00","flag":0.381,"per_km":0.274,"per_min":0.059,"per_min_waiting":0.552}],"booking_fee":0.25,"min_fare":1.1,"free_waiting_min":5,"surge":{"enabled":false},"commission":{"percent":14}}'::jsonb
true, '{"currency":"JOD","timezone":"Asia/Amman","mode":"time_and_distance","rounding":{"increment":0.05,"mode":"nearest"},"windows":[{"name":"morning_peak","from":"07:00","to":"09:30","flag":0,"per_km":0.158,"per_min":0.05,"per_min_waiting":0.074},{"name":"normal_day","from":"09:30","to":"16:00","flag":0,"per_km":0.158,"per_min":0.043,"per_min_waiting":0.065},{"name":"evening_peak","from":"16:00","to":"19:30","flag":0,"per_km":0.158,"per_min":0.05,"per_min_waiting":0.074},{"name":"normal_evening","from":"19:30","to":"23:00","flag":0,"per_km":0.158,"per_min":0.043,"per_min_waiting":0.065},{"name":"late_night","from":"23:00","to":"07:00","flag":0,"per_km":0.158,"per_min":0.043,"per_min_waiting":0.065}],"booking_fee":0,"min_fare":1.1,"free_waiting_min":5,"surge":{"enabled":false},"commission":{"percent":14}}'::jsonb
FROM tripz_tenants t WHERE t.country_pack = 'jo';
-- ============ EG (EGP) ============
@@ -124,7 +112,7 @@ SELECT uuid_generate_v4(), t.id, 'default', 'economy',
COALESCE((SELECT MAX(version) FROM tripz_tariffs x
WHERE x.tenant_id = t.id AND x.city = 'default'
AND x.service_class = 'economy'), 0) + 1,
true, '{"currency":"EGP","timezone":"Africa/Cairo","mode":"time_and_distance","rounding":{"increment":1,"mode":"nearest"},"windows":[{"name":"morning_peak","from":"07:00","to":"09:30","flag":8.86,"per_km":6.36,"per_min":1.36,"per_min_waiting":10.91},{"name":"normal_day","from":"09:30","to":"16:00","flag":7.09,"per_km":5.09,"per_min":1.09,"per_min_waiting":8.73},{"name":"evening_peak","from":"16:00","to":"19:30","flag":8.86,"per_km":6.36,"per_min":1.36,"per_min_waiting":10.91},{"name":"normal_evening","from":"19:30","to":"23:00","flag":7.09,"per_km":5.09,"per_min":1.09,"per_min_waiting":8.73},{"name":"late_night","from":"23:00","to":"07:00","flag":8.15,"per_km":5.85,"per_min":1.25,"per_min_waiting":10.04}],"booking_fee":4.55,"min_fare":20,"free_waiting_min":5,"surge":{"enabled":false},"commission":{"percent":12}}'::jsonb
true, '{"currency":"EGP","timezone":"Africa/Cairo","mode":"time_and_distance","rounding":{"increment":1,"mode":"nearest"},"windows":[{"name":"morning_peak","from":"07:00","to":"09:30","flag":0,"per_km":4,"per_min":1.4,"per_min_waiting":2.1},{"name":"normal_day","from":"09:30","to":"16:00","flag":0,"per_km":4,"per_min":0.8,"per_min_waiting":1.2},{"name":"evening_peak","from":"16:00","to":"19:30","flag":0,"per_km":4,"per_min":1.4,"per_min_waiting":2.1},{"name":"normal_evening","from":"19:30","to":"23:00","flag":0,"per_km":4,"per_min":0.8,"per_min_waiting":1.2},{"name":"late_night","from":"23:00","to":"07:00","flag":0,"per_km":4,"per_min":1.5,"per_min_waiting":2.25}],"booking_fee":0,"min_fare":20,"free_waiting_min":5,"surge":{"enabled":false},"commission":{"percent":12}}'::jsonb
FROM tripz_tenants t WHERE t.country_pack = 'eg';
-- comfort
@@ -136,7 +124,7 @@ SELECT uuid_generate_v4(), t.id, 'default', 'comfort',
COALESCE((SELECT MAX(version) FROM tripz_tariffs x
WHERE x.tenant_id = t.id AND x.city = 'default'
AND x.service_class = 'comfort'), 0) + 1,
true, '{"currency":"EGP","timezone":"Africa/Cairo","mode":"time_and_distance","rounding":{"increment":1,"mode":"nearest"},"windows":[{"name":"morning_peak","from":"07:00","to":"09:30","flag":11.08,"per_km":7.95,"per_min":1.7,"per_min_waiting":10.91},{"name":"normal_day","from":"09:30","to":"16:00","flag":8.86,"per_km":6.36,"per_min":1.36,"per_min_waiting":8.73},{"name":"evening_peak","from":"16:00","to":"19:30","flag":11.08,"per_km":7.95,"per_min":1.7,"per_min_waiting":10.91},{"name":"normal_evening","from":"19:30","to":"23:00","flag":8.86,"per_km":6.36,"per_min":1.36,"per_min_waiting":8.73},{"name":"late_night","from":"23:00","to":"07:00","flag":10.19,"per_km":7.32,"per_min":1.57,"per_min_waiting":10.04}],"booking_fee":4.55,"min_fare":20,"free_waiting_min":5,"surge":{"enabled":false},"commission":{"percent":12}}'::jsonb
true, '{"currency":"EGP","timezone":"Africa/Cairo","mode":"time_and_distance","rounding":{"increment":1,"mode":"nearest"},"windows":[{"name":"morning_peak","from":"07:00","to":"09:30","flag":0,"per_km":5.2,"per_min":1.4,"per_min_waiting":2.1},{"name":"normal_day","from":"09:30","to":"16:00","flag":0,"per_km":5.2,"per_min":0.8,"per_min_waiting":1.2},{"name":"evening_peak","from":"16:00","to":"19:30","flag":0,"per_km":5.2,"per_min":1.4,"per_min_waiting":2.1},{"name":"normal_evening","from":"19:30","to":"23:00","flag":0,"per_km":5.2,"per_min":0.8,"per_min_waiting":1.2},{"name":"late_night","from":"23:00","to":"07:00","flag":0,"per_km":5.2,"per_min":1.5,"per_min_waiting":2.25}],"booking_fee":0,"min_fare":20,"free_waiting_min":5,"surge":{"enabled":false},"commission":{"percent":12}}'::jsonb
FROM tripz_tenants t WHERE t.country_pack = 'eg';
-- lady
@@ -148,7 +136,7 @@ SELECT uuid_generate_v4(), t.id, 'default', 'lady',
COALESCE((SELECT MAX(version) FROM tripz_tariffs x
WHERE x.tenant_id = t.id AND x.city = 'default'
AND x.service_class = 'lady'), 0) + 1,
true, '{"currency":"EGP","timezone":"Africa/Cairo","mode":"time_and_distance","rounding":{"increment":1,"mode":"nearest"},"windows":[{"name":"morning_peak","from":"07:00","to":"09:30","flag":9.75,"per_km":7,"per_min":1.5,"per_min_waiting":10.91},{"name":"normal_day","from":"09:30","to":"16:00","flag":7.8,"per_km":5.6,"per_min":1.2,"per_min_waiting":8.73},{"name":"evening_peak","from":"16:00","to":"19:30","flag":9.75,"per_km":7,"per_min":1.5,"per_min_waiting":10.91},{"name":"normal_evening","from":"19:30","to":"23:00","flag":7.8,"per_km":5.6,"per_min":1.2,"per_min_waiting":8.73},{"name":"late_night","from":"23:00","to":"07:00","flag":8.97,"per_km":6.44,"per_min":1.38,"per_min_waiting":10.04}],"booking_fee":4.55,"min_fare":20,"free_waiting_min":5,"surge":{"enabled":false},"commission":{"percent":12}}'::jsonb
true, '{"currency":"EGP","timezone":"Africa/Cairo","mode":"time_and_distance","rounding":{"increment":1,"mode":"nearest"},"windows":[{"name":"morning_peak","from":"07:00","to":"09:30","flag":0,"per_km":4.36,"per_min":1.4,"per_min_waiting":2.1},{"name":"normal_day","from":"09:30","to":"16:00","flag":0,"per_km":4.36,"per_min":0.8,"per_min_waiting":1.2},{"name":"evening_peak","from":"16:00","to":"19:30","flag":0,"per_km":4.36,"per_min":1.4,"per_min_waiting":2.1},{"name":"normal_evening","from":"19:30","to":"23:00","flag":0,"per_km":4.36,"per_min":0.8,"per_min_waiting":1.2},{"name":"late_night","from":"23:00","to":"07:00","flag":0,"per_km":4.36,"per_min":1.5,"per_min_waiting":2.25}],"booking_fee":0,"min_fare":20,"free_waiting_min":5,"surge":{"enabled":false},"commission":{"percent":12}}'::jsonb
FROM tripz_tenants t WHERE t.country_pack = 'eg';
-- electric
@@ -160,7 +148,7 @@ SELECT uuid_generate_v4(), t.id, 'default', 'electric',
COALESCE((SELECT MAX(version) FROM tripz_tariffs x
WHERE x.tenant_id = t.id AND x.city = 'default'
AND x.service_class = 'electric'), 0) + 1,
true, '{"currency":"EGP","timezone":"Africa/Cairo","mode":"time_and_distance","rounding":{"increment":1,"mode":"nearest"},"windows":[{"name":"morning_peak","from":"07:00","to":"09:30","flag":9.31,"per_km":6.68,"per_min":1.43,"per_min_waiting":10.91},{"name":"normal_day","from":"09:30","to":"16:00","flag":7.45,"per_km":5.35,"per_min":1.15,"per_min_waiting":8.73},{"name":"evening_peak","from":"16:00","to":"19:30","flag":9.31,"per_km":6.68,"per_min":1.43,"per_min_waiting":10.91},{"name":"normal_evening","from":"19:30","to":"23:00","flag":7.45,"per_km":5.35,"per_min":1.15,"per_min_waiting":8.73},{"name":"late_night","from":"23:00","to":"07:00","flag":8.56,"per_km":6.15,"per_min":1.32,"per_min_waiting":10.04}],"booking_fee":4.55,"min_fare":20,"free_waiting_min":5,"surge":{"enabled":false},"commission":{"percent":12}}'::jsonb
true, '{"currency":"EGP","timezone":"Africa/Cairo","mode":"time_and_distance","rounding":{"increment":1,"mode":"nearest"},"windows":[{"name":"morning_peak","from":"07:00","to":"09:30","flag":0,"per_km":4.84,"per_min":1.4,"per_min_waiting":2.1},{"name":"normal_day","from":"09:30","to":"16:00","flag":0,"per_km":4.84,"per_min":0.8,"per_min_waiting":1.2},{"name":"evening_peak","from":"16:00","to":"19:30","flag":0,"per_km":4.84,"per_min":1.4,"per_min_waiting":2.1},{"name":"normal_evening","from":"19:30","to":"23:00","flag":0,"per_km":4.84,"per_min":0.8,"per_min_waiting":1.2},{"name":"late_night","from":"23:00","to":"07:00","flag":0,"per_km":4.84,"per_min":1.5,"per_min_waiting":2.25}],"booking_fee":0,"min_fare":20,"free_waiting_min":5,"surge":{"enabled":false},"commission":{"percent":12}}'::jsonb
FROM tripz_tenants t WHERE t.country_pack = 'eg';
-- van
@@ -172,7 +160,7 @@ SELECT uuid_generate_v4(), t.id, 'default', 'van',
COALESCE((SELECT MAX(version) FROM tripz_tariffs x
WHERE x.tenant_id = t.id AND x.city = 'default'
AND x.service_class = 'van'), 0) + 1,
true, '{"currency":"EGP","timezone":"Africa/Cairo","mode":"time_and_distance","rounding":{"increment":1,"mode":"nearest"},"windows":[{"name":"morning_peak","from":"07:00","to":"09:30","flag":12.85,"per_km":9.23,"per_min":1.98,"per_min_waiting":10.91},{"name":"normal_day","from":"09:30","to":"16:00","flag":10.28,"per_km":7.38,"per_min":1.58,"per_min_waiting":8.73},{"name":"evening_peak","from":"16:00","to":"19:30","flag":12.85,"per_km":9.23,"per_min":1.98,"per_min_waiting":10.91},{"name":"normal_evening","from":"19:30","to":"23:00","flag":10.28,"per_km":7.38,"per_min":1.58,"per_min_waiting":8.73},{"name":"late_night","from":"23:00","to":"07:00","flag":11.82,"per_km":8.49,"per_min":1.82,"per_min_waiting":10.04}],"booking_fee":4.55,"min_fare":20,"free_waiting_min":5,"surge":{"enabled":false},"commission":{"percent":12}}'::jsonb
true, '{"currency":"EGP","timezone":"Africa/Cairo","mode":"time_and_distance","rounding":{"increment":1,"mode":"nearest"},"windows":[{"name":"morning_peak","from":"07:00","to":"09:30","flag":0,"per_km":6.04,"per_min":1.4,"per_min_waiting":2.1},{"name":"normal_day","from":"09:30","to":"16:00","flag":0,"per_km":6.04,"per_min":0.8,"per_min_waiting":1.2},{"name":"evening_peak","from":"16:00","to":"19:30","flag":0,"per_km":6.04,"per_min":1.4,"per_min_waiting":2.1},{"name":"normal_evening","from":"19:30","to":"23:00","flag":0,"per_km":6.04,"per_min":0.8,"per_min_waiting":1.2},{"name":"late_night","from":"23:00","to":"07:00","flag":0,"per_km":6.04,"per_min":1.5,"per_min_waiting":2.25}],"booking_fee":0,"min_fare":20,"free_waiting_min":5,"surge":{"enabled":false},"commission":{"percent":12}}'::jsonb
FROM tripz_tenants t WHERE t.country_pack = 'eg';
-- delivery
@@ -184,7 +172,7 @@ SELECT uuid_generate_v4(), t.id, 'default', 'delivery',
COALESCE((SELECT MAX(version) FROM tripz_tariffs x
WHERE x.tenant_id = t.id AND x.city = 'default'
AND x.service_class = 'delivery'), 0) + 1,
true, '{"currency":"EGP","timezone":"Africa/Cairo","mode":"time_and_distance","rounding":{"increment":1,"mode":"nearest"},"windows":[{"name":"morning_peak","from":"07:00","to":"09:30","flag":7.98,"per_km":5.73,"per_min":1.23,"per_min_waiting":10.91},{"name":"normal_day","from":"09:30","to":"16:00","flag":6.38,"per_km":4.58,"per_min":0.98,"per_min_waiting":8.73},{"name":"evening_peak","from":"16:00","to":"19:30","flag":7.98,"per_km":5.73,"per_min":1.23,"per_min_waiting":10.91},{"name":"normal_evening","from":"19:30","to":"23:00","flag":6.38,"per_km":4.58,"per_min":0.98,"per_min_waiting":8.73},{"name":"late_night","from":"23:00","to":"07:00","flag":7.34,"per_km":5.27,"per_min":1.13,"per_min_waiting":10.04}],"booking_fee":4.55,"min_fare":20,"free_waiting_min":5,"surge":{"enabled":false},"commission":{"percent":12}}'::jsonb
true, '{"currency":"EGP","timezone":"Africa/Cairo","mode":"time_and_distance","rounding":{"increment":1,"mode":"nearest"},"windows":[{"name":"morning_peak","from":"07:00","to":"09:30","flag":0,"per_km":3.64,"per_min":1.4,"per_min_waiting":2.1},{"name":"normal_day","from":"09:30","to":"16:00","flag":0,"per_km":3.64,"per_min":0.8,"per_min_waiting":1.2},{"name":"evening_peak","from":"16:00","to":"19:30","flag":0,"per_km":3.64,"per_min":1.4,"per_min_waiting":2.1},{"name":"normal_evening","from":"19:30","to":"23:00","flag":0,"per_km":3.64,"per_min":0.8,"per_min_waiting":1.2},{"name":"late_night","from":"23:00","to":"07:00","flag":0,"per_km":3.64,"per_min":1.5,"per_min_waiting":2.25}],"booking_fee":0,"min_fare":20,"free_waiting_min":5,"surge":{"enabled":false},"commission":{"percent":12}}'::jsonb
FROM tripz_tenants t WHERE t.country_pack = 'eg';
-- vip
@@ -196,19 +184,7 @@ SELECT uuid_generate_v4(), t.id, 'default', 'vip',
COALESCE((SELECT MAX(version) FROM tripz_tariffs x
WHERE x.tenant_id = t.id AND x.city = 'default'
AND x.service_class = 'vip'), 0) + 1,
true, '{"currency":"EGP","timezone":"Africa/Cairo","mode":"time_and_distance","rounding":{"increment":1,"mode":"nearest"},"windows":[{"name":"morning_peak","from":"07:00","to":"09:30","flag":15.51,"per_km":11.14,"per_min":2.39,"per_min_waiting":10.91},{"name":"normal_day","from":"09:30","to":"16:00","flag":12.41,"per_km":8.91,"per_min":1.91,"per_min_waiting":8.73},{"name":"evening_peak","from":"16:00","to":"19:30","flag":15.51,"per_km":11.14,"per_min":2.39,"per_min_waiting":10.91},{"name":"normal_evening","from":"19:30","to":"23:00","flag":12.41,"per_km":8.91,"per_min":1.91,"per_min_waiting":8.73},{"name":"late_night","from":"23:00","to":"07:00","flag":14.27,"per_km":10.25,"per_min":2.2,"per_min_waiting":10.04}],"booking_fee":4.55,"min_fare":20,"free_waiting_min":5,"surge":{"enabled":false},"commission":{"percent":12}}'::jsonb
FROM tripz_tenants t WHERE t.country_pack = 'eg';
-- fixed
UPDATE tripz_tariffs SET active = false
WHERE city = 'default' AND service_class = 'fixed' AND active = true
AND tenant_id IN (SELECT id FROM tripz_tenants WHERE country_pack = 'eg');
INSERT INTO tripz_tariffs (id, tenant_id, city, service_class, version, active, definition)
SELECT uuid_generate_v4(), t.id, 'default', 'fixed',
COALESCE((SELECT MAX(version) FROM tripz_tariffs x
WHERE x.tenant_id = t.id AND x.city = 'default'
AND x.service_class = 'fixed'), 0) + 1,
true, '{"currency":"EGP","timezone":"Africa/Cairo","mode":"fixed_quote","rounding":{"increment":1,"mode":"nearest"},"windows":[{"name":"morning_peak","from":"07:00","to":"09:30","flag":8.86,"per_km":6.36,"per_min":1.36,"per_min_waiting":10.91},{"name":"normal_day","from":"09:30","to":"16:00","flag":7.09,"per_km":5.09,"per_min":1.09,"per_min_waiting":8.73},{"name":"evening_peak","from":"16:00","to":"19:30","flag":8.86,"per_km":6.36,"per_min":1.36,"per_min_waiting":10.91},{"name":"normal_evening","from":"19:30","to":"23:00","flag":7.09,"per_km":5.09,"per_min":1.09,"per_min_waiting":8.73},{"name":"late_night","from":"23:00","to":"07:00","flag":8.15,"per_km":5.85,"per_min":1.25,"per_min_waiting":10.04}],"booking_fee":0,"min_fare":0,"free_waiting_min":5,"surge":{"enabled":false},"commission":{"percent":12},"fixed_fare":45}'::jsonb
true, '{"currency":"EGP","timezone":"Africa/Cairo","mode":"time_and_distance","rounding":{"increment":1,"mode":"nearest"},"windows":[{"name":"morning_peak","from":"07:00","to":"09:30","flag":0,"per_km":5.6,"per_min":1.4,"per_min_waiting":2.1},{"name":"normal_day","from":"09:30","to":"16:00","flag":0,"per_km":5.6,"per_min":0.8,"per_min_waiting":1.2},{"name":"evening_peak","from":"16:00","to":"19:30","flag":0,"per_km":5.6,"per_min":1.4,"per_min_waiting":2.1},{"name":"normal_evening","from":"19:30","to":"23:00","flag":0,"per_km":5.6,"per_min":0.8,"per_min_waiting":1.2},{"name":"late_night","from":"23:00","to":"07:00","flag":0,"per_km":5.6,"per_min":1.5,"per_min_waiting":2.25}],"booking_fee":0,"min_fare":20,"free_waiting_min":5,"surge":{"enabled":false},"commission":{"percent":12}}'::jsonb
FROM tripz_tenants t WHERE t.country_pack = 'eg';
-- saver
@@ -220,7 +196,7 @@ SELECT uuid_generate_v4(), t.id, 'default', 'saver',
COALESCE((SELECT MAX(version) FROM tripz_tariffs x
WHERE x.tenant_id = t.id AND x.city = 'default'
AND x.service_class = 'saver'), 0) + 1,
true, '{"currency":"EGP","timezone":"Africa/Cairo","mode":"time_and_distance","rounding":{"increment":1,"mode":"nearest"},"windows":[{"name":"morning_peak","from":"07:00","to":"09:30","flag":7.53,"per_km":5.41,"per_min":1.16,"per_min_waiting":10.91},{"name":"normal_day","from":"09:30","to":"16:00","flag":6.03,"per_km":4.33,"per_min":0.93,"per_min_waiting":8.73},{"name":"evening_peak","from":"16:00","to":"19:30","flag":7.53,"per_km":5.41,"per_min":1.16,"per_min_waiting":10.91},{"name":"normal_evening","from":"19:30","to":"23:00","flag":6.03,"per_km":4.33,"per_min":0.93,"per_min_waiting":8.73},{"name":"late_night","from":"23:00","to":"07:00","flag":6.93,"per_km":4.98,"per_min":1.07,"per_min_waiting":10.04}],"booking_fee":4.55,"min_fare":20,"free_waiting_min":5,"surge":{"enabled":false},"commission":{"percent":12}}'::jsonb
true, '{"currency":"EGP","timezone":"Africa/Cairo","mode":"time_and_distance","rounding":{"increment":1,"mode":"nearest"},"windows":[{"name":"morning_peak","from":"07:00","to":"09:30","flag":0,"per_km":3.44,"per_min":1.4,"per_min_waiting":2.1},{"name":"normal_day","from":"09:30","to":"16:00","flag":0,"per_km":3.44,"per_min":0.8,"per_min_waiting":1.2},{"name":"evening_peak","from":"16:00","to":"19:30","flag":0,"per_km":3.44,"per_min":1.4,"per_min_waiting":2.1},{"name":"normal_evening","from":"19:30","to":"23:00","flag":0,"per_km":3.44,"per_min":0.8,"per_min_waiting":1.2},{"name":"late_night","from":"23:00","to":"07:00","flag":0,"per_km":3.44,"per_min":1.5,"per_min_waiting":2.25}],"booking_fee":0,"min_fare":20,"free_waiting_min":5,"surge":{"enabled":false},"commission":{"percent":12}}'::jsonb
FROM tripz_tenants t WHERE t.country_pack = 'eg';
-- ============ SY (SYP) ============
@@ -233,7 +209,7 @@ SELECT uuid_generate_v4(), t.id, 'default', 'economy',
COALESCE((SELECT MAX(version) FROM tripz_tariffs x
WHERE x.tenant_id = t.id AND x.city = 'default'
AND x.service_class = 'economy'), 0) + 1,
true, '{"currency":"SYP","timezone":"Asia/Damascus","mode":"time_and_distance","rounding":{"increment":5,"mode":"nearest"},"windows":[{"name":"morning_peak","from":"07:00","to":"09:30","flag":66,"per_km":48,"per_min":10,"per_min_waiting":82},{"name":"normal_day","from":"09:30","to":"16:00","flag":53,"per_km":38,"per_min":8,"per_min_waiting":65},{"name":"evening_peak","from":"16:00","to":"19:30","flag":66,"per_km":48,"per_min":10,"per_min_waiting":82},{"name":"normal_evening","from":"19:30","to":"23:00","flag":53,"per_km":38,"per_min":8,"per_min_waiting":65},{"name":"late_night","from":"23:00","to":"07:00","flag":61,"per_km":44,"per_min":9,"per_min_waiting":75}],"booking_fee":34,"min_fare":150,"free_waiting_min":5,"surge":{"enabled":false},"commission":{"percent":11}}'::jsonb
true, '{"currency":"SYP","timezone":"Asia/Damascus","mode":"time_and_distance","rounding":{"increment":5,"mode":"nearest"},"windows":[{"name":"morning_peak","from":"07:00","to":"09:30","flag":0,"per_km":40,"per_min":10,"per_min_waiting":15},{"name":"normal_day","from":"09:30","to":"16:00","flag":0,"per_km":40,"per_min":9,"per_min_waiting":14},{"name":"evening_peak","from":"16:00","to":"19:30","flag":0,"per_km":40,"per_min":10,"per_min_waiting":15},{"name":"normal_evening","from":"19:30","to":"23:00","flag":0,"per_km":40,"per_min":9,"per_min_waiting":14},{"name":"late_night","from":"23:00","to":"07:00","flag":0,"per_km":40,"per_min":11,"per_min_waiting":17}],"booking_fee":0,"min_fare":150,"free_waiting_min":5,"surge":{"enabled":false},"commission":{"percent":11}}'::jsonb
FROM tripz_tenants t WHERE t.country_pack = 'sy';
-- comfort
@@ -245,7 +221,7 @@ SELECT uuid_generate_v4(), t.id, 'default', 'comfort',
COALESCE((SELECT MAX(version) FROM tripz_tariffs x
WHERE x.tenant_id = t.id AND x.city = 'default'
AND x.service_class = 'comfort'), 0) + 1,
true, '{"currency":"SYP","timezone":"Asia/Damascus","mode":"time_and_distance","rounding":{"increment":5,"mode":"nearest"},"windows":[{"name":"morning_peak","from":"07:00","to":"09:30","flag":83,"per_km":60,"per_min":13,"per_min_waiting":82},{"name":"normal_day","from":"09:30","to":"16:00","flag":66,"per_km":48,"per_min":10,"per_min_waiting":65},{"name":"evening_peak","from":"16:00","to":"19:30","flag":83,"per_km":60,"per_min":13,"per_min_waiting":82},{"name":"normal_evening","from":"19:30","to":"23:00","flag":66,"per_km":48,"per_min":10,"per_min_waiting":65},{"name":"late_night","from":"23:00","to":"07:00","flag":76,"per_km":55,"per_min":12,"per_min_waiting":75}],"booking_fee":34,"min_fare":150,"free_waiting_min":5,"surge":{"enabled":false},"commission":{"percent":11}}'::jsonb
true, '{"currency":"SYP","timezone":"Asia/Damascus","mode":"time_and_distance","rounding":{"increment":5,"mode":"nearest"},"windows":[{"name":"morning_peak","from":"07:00","to":"09:30","flag":0,"per_km":52,"per_min":10,"per_min_waiting":15},{"name":"normal_day","from":"09:30","to":"16:00","flag":0,"per_km":52,"per_min":9,"per_min_waiting":14},{"name":"evening_peak","from":"16:00","to":"19:30","flag":0,"per_km":52,"per_min":10,"per_min_waiting":15},{"name":"normal_evening","from":"19:30","to":"23:00","flag":0,"per_km":52,"per_min":9,"per_min_waiting":14},{"name":"late_night","from":"23:00","to":"07:00","flag":0,"per_km":52,"per_min":11,"per_min_waiting":17}],"booking_fee":0,"min_fare":150,"free_waiting_min":5,"surge":{"enabled":false},"commission":{"percent":11}}'::jsonb
FROM tripz_tenants t WHERE t.country_pack = 'sy';
-- lady
@@ -257,7 +233,7 @@ SELECT uuid_generate_v4(), t.id, 'default', 'lady',
COALESCE((SELECT MAX(version) FROM tripz_tariffs x
WHERE x.tenant_id = t.id AND x.city = 'default'
AND x.service_class = 'lady'), 0) + 1,
true, '{"currency":"SYP","timezone":"Asia/Damascus","mode":"time_and_distance","rounding":{"increment":5,"mode":"nearest"},"windows":[{"name":"morning_peak","from":"07:00","to":"09:30","flag":73,"per_km":53,"per_min":11,"per_min_waiting":82},{"name":"normal_day","from":"09:30","to":"16:00","flag":59,"per_km":42,"per_min":9,"per_min_waiting":65},{"name":"evening_peak","from":"16:00","to":"19:30","flag":73,"per_km":53,"per_min":11,"per_min_waiting":82},{"name":"normal_evening","from":"19:30","to":"23:00","flag":59,"per_km":42,"per_min":9,"per_min_waiting":65},{"name":"late_night","from":"23:00","to":"07:00","flag":67,"per_km":48,"per_min":10,"per_min_waiting":75}],"booking_fee":34,"min_fare":150,"free_waiting_min":5,"surge":{"enabled":false},"commission":{"percent":11}}'::jsonb
true, '{"currency":"SYP","timezone":"Asia/Damascus","mode":"time_and_distance","rounding":{"increment":5,"mode":"nearest"},"windows":[{"name":"morning_peak","from":"07:00","to":"09:30","flag":0,"per_km":44,"per_min":10,"per_min_waiting":15},{"name":"normal_day","from":"09:30","to":"16:00","flag":0,"per_km":44,"per_min":9,"per_min_waiting":14},{"name":"evening_peak","from":"16:00","to":"19:30","flag":0,"per_km":44,"per_min":10,"per_min_waiting":15},{"name":"normal_evening","from":"19:30","to":"23:00","flag":0,"per_km":44,"per_min":9,"per_min_waiting":14},{"name":"late_night","from":"23:00","to":"07:00","flag":0,"per_km":44,"per_min":11,"per_min_waiting":17}],"booking_fee":0,"min_fare":150,"free_waiting_min":5,"surge":{"enabled":false},"commission":{"percent":11}}'::jsonb
FROM tripz_tenants t WHERE t.country_pack = 'sy';
-- electric
@@ -269,7 +245,7 @@ SELECT uuid_generate_v4(), t.id, 'default', 'electric',
COALESCE((SELECT MAX(version) FROM tripz_tariffs x
WHERE x.tenant_id = t.id AND x.city = 'default'
AND x.service_class = 'electric'), 0) + 1,
true, '{"currency":"SYP","timezone":"Asia/Damascus","mode":"time_and_distance","rounding":{"increment":5,"mode":"nearest"},"windows":[{"name":"morning_peak","from":"07:00","to":"09:30","flag":70,"per_km":50,"per_min":11,"per_min_waiting":82},{"name":"normal_day","from":"09:30","to":"16:00","flag":56,"per_km":40,"per_min":9,"per_min_waiting":65},{"name":"evening_peak","from":"16:00","to":"19:30","flag":70,"per_km":50,"per_min":11,"per_min_waiting":82},{"name":"normal_evening","from":"19:30","to":"23:00","flag":56,"per_km":40,"per_min":9,"per_min_waiting":65},{"name":"late_night","from":"23:00","to":"07:00","flag":64,"per_km":46,"per_min":10,"per_min_waiting":75}],"booking_fee":34,"min_fare":150,"free_waiting_min":5,"surge":{"enabled":false},"commission":{"percent":11}}'::jsonb
true, '{"currency":"SYP","timezone":"Asia/Damascus","mode":"time_and_distance","rounding":{"increment":5,"mode":"nearest"},"windows":[{"name":"morning_peak","from":"07:00","to":"09:30","flag":0,"per_km":48,"per_min":10,"per_min_waiting":15},{"name":"normal_day","from":"09:30","to":"16:00","flag":0,"per_km":48,"per_min":9,"per_min_waiting":14},{"name":"evening_peak","from":"16:00","to":"19:30","flag":0,"per_km":48,"per_min":10,"per_min_waiting":15},{"name":"normal_evening","from":"19:30","to":"23:00","flag":0,"per_km":48,"per_min":9,"per_min_waiting":14},{"name":"late_night","from":"23:00","to":"07:00","flag":0,"per_km":48,"per_min":11,"per_min_waiting":17}],"booking_fee":0,"min_fare":150,"free_waiting_min":5,"surge":{"enabled":false},"commission":{"percent":11}}'::jsonb
FROM tripz_tenants t WHERE t.country_pack = 'sy';
-- van
@@ -281,7 +257,7 @@ SELECT uuid_generate_v4(), t.id, 'default', 'van',
COALESCE((SELECT MAX(version) FROM tripz_tariffs x
WHERE x.tenant_id = t.id AND x.city = 'default'
AND x.service_class = 'van'), 0) + 1,
true, '{"currency":"SYP","timezone":"Asia/Damascus","mode":"time_and_distance","rounding":{"increment":5,"mode":"nearest"},"windows":[{"name":"morning_peak","from":"07:00","to":"09:30","flag":96,"per_km":69,"per_min":15,"per_min_waiting":82},{"name":"normal_day","from":"09:30","to":"16:00","flag":77,"per_km":55,"per_min":12,"per_min_waiting":65},{"name":"evening_peak","from":"16:00","to":"19:30","flag":96,"per_km":69,"per_min":15,"per_min_waiting":82},{"name":"normal_evening","from":"19:30","to":"23:00","flag":77,"per_km":55,"per_min":12,"per_min_waiting":65},{"name":"late_night","from":"23:00","to":"07:00","flag":89,"per_km":64,"per_min":14,"per_min_waiting":75}],"booking_fee":34,"min_fare":150,"free_waiting_min":5,"surge":{"enabled":false},"commission":{"percent":11}}'::jsonb
true, '{"currency":"SYP","timezone":"Asia/Damascus","mode":"time_and_distance","rounding":{"increment":5,"mode":"nearest"},"windows":[{"name":"morning_peak","from":"07:00","to":"09:30","flag":0,"per_km":60,"per_min":10,"per_min_waiting":15},{"name":"normal_day","from":"09:30","to":"16:00","flag":0,"per_km":60,"per_min":9,"per_min_waiting":14},{"name":"evening_peak","from":"16:00","to":"19:30","flag":0,"per_km":60,"per_min":10,"per_min_waiting":15},{"name":"normal_evening","from":"19:30","to":"23:00","flag":0,"per_km":60,"per_min":9,"per_min_waiting":14},{"name":"late_night","from":"23:00","to":"07:00","flag":0,"per_km":60,"per_min":11,"per_min_waiting":17}],"booking_fee":0,"min_fare":150,"free_waiting_min":5,"surge":{"enabled":false},"commission":{"percent":11}}'::jsonb
FROM tripz_tenants t WHERE t.country_pack = 'sy';
-- delivery
@@ -293,7 +269,7 @@ SELECT uuid_generate_v4(), t.id, 'default', 'delivery',
COALESCE((SELECT MAX(version) FROM tripz_tariffs x
WHERE x.tenant_id = t.id AND x.city = 'default'
AND x.service_class = 'delivery'), 0) + 1,
true, '{"currency":"SYP","timezone":"Asia/Damascus","mode":"time_and_distance","rounding":{"increment":5,"mode":"nearest"},"windows":[{"name":"morning_peak","from":"07:00","to":"09:30","flag":60,"per_km":43,"per_min":9,"per_min_waiting":82},{"name":"normal_day","from":"09:30","to":"16:00","flag":48,"per_km":34,"per_min":7,"per_min_waiting":65},{"name":"evening_peak","from":"16:00","to":"19:30","flag":60,"per_km":43,"per_min":9,"per_min_waiting":82},{"name":"normal_evening","from":"19:30","to":"23:00","flag":48,"per_km":34,"per_min":7,"per_min_waiting":65},{"name":"late_night","from":"23:00","to":"07:00","flag":55,"per_km":40,"per_min":8,"per_min_waiting":75}],"booking_fee":34,"min_fare":150,"free_waiting_min":5,"surge":{"enabled":false},"commission":{"percent":11}}'::jsonb
true, '{"currency":"SYP","timezone":"Asia/Damascus","mode":"time_and_distance","rounding":{"increment":5,"mode":"nearest"},"windows":[{"name":"morning_peak","from":"07:00","to":"09:30","flag":0,"per_km":36,"per_min":10,"per_min_waiting":15},{"name":"normal_day","from":"09:30","to":"16:00","flag":0,"per_km":36,"per_min":9,"per_min_waiting":14},{"name":"evening_peak","from":"16:00","to":"19:30","flag":0,"per_km":36,"per_min":10,"per_min_waiting":15},{"name":"normal_evening","from":"19:30","to":"23:00","flag":0,"per_km":36,"per_min":9,"per_min_waiting":14},{"name":"late_night","from":"23:00","to":"07:00","flag":0,"per_km":36,"per_min":11,"per_min_waiting":17}],"booking_fee":0,"min_fare":150,"free_waiting_min":5,"surge":{"enabled":false},"commission":{"percent":11}}'::jsonb
FROM tripz_tenants t WHERE t.country_pack = 'sy';
-- vip
@@ -305,19 +281,7 @@ SELECT uuid_generate_v4(), t.id, 'default', 'vip',
COALESCE((SELECT MAX(version) FROM tripz_tariffs x
WHERE x.tenant_id = t.id AND x.city = 'default'
AND x.service_class = 'vip'), 0) + 1,
true, '{"currency":"SYP","timezone":"Asia/Damascus","mode":"time_and_distance","rounding":{"increment":5,"mode":"nearest"},"windows":[{"name":"morning_peak","from":"07:00","to":"09:30","flag":116,"per_km":84,"per_min":18,"per_min_waiting":82},{"name":"normal_day","from":"09:30","to":"16:00","flag":93,"per_km":67,"per_min":14,"per_min_waiting":65},{"name":"evening_peak","from":"16:00","to":"19:30","flag":116,"per_km":84,"per_min":18,"per_min_waiting":82},{"name":"normal_evening","from":"19:30","to":"23:00","flag":93,"per_km":67,"per_min":14,"per_min_waiting":65},{"name":"late_night","from":"23:00","to":"07:00","flag":107,"per_km":77,"per_min":16,"per_min_waiting":75}],"booking_fee":34,"min_fare":150,"free_waiting_min":5,"surge":{"enabled":false},"commission":{"percent":11}}'::jsonb
FROM tripz_tenants t WHERE t.country_pack = 'sy';
-- fixed
UPDATE tripz_tariffs SET active = false
WHERE city = 'default' AND service_class = 'fixed' AND active = true
AND tenant_id IN (SELECT id FROM tripz_tenants WHERE country_pack = 'sy');
INSERT INTO tripz_tariffs (id, tenant_id, city, service_class, version, active, definition)
SELECT uuid_generate_v4(), t.id, 'default', 'fixed',
COALESCE((SELECT MAX(version) FROM tripz_tariffs x
WHERE x.tenant_id = t.id AND x.city = 'default'
AND x.service_class = 'fixed'), 0) + 1,
true, '{"currency":"SYP","timezone":"Asia/Damascus","mode":"fixed_quote","rounding":{"increment":5,"mode":"nearest"},"windows":[{"name":"morning_peak","from":"07:00","to":"09:30","flag":66,"per_km":48,"per_min":10,"per_min_waiting":82},{"name":"normal_day","from":"09:30","to":"16:00","flag":53,"per_km":38,"per_min":8,"per_min_waiting":65},{"name":"evening_peak","from":"16:00","to":"19:30","flag":66,"per_km":48,"per_min":10,"per_min_waiting":82},{"name":"normal_evening","from":"19:30","to":"23:00","flag":53,"per_km":38,"per_min":8,"per_min_waiting":65},{"name":"late_night","from":"23:00","to":"07:00","flag":61,"per_km":44,"per_min":9,"per_min_waiting":75}],"booking_fee":0,"min_fare":0,"free_waiting_min":5,"surge":{"enabled":false},"commission":{"percent":11},"fixed_fare":350}'::jsonb
true, '{"currency":"SYP","timezone":"Asia/Damascus","mode":"time_and_distance","rounding":{"increment":5,"mode":"nearest"},"windows":[{"name":"morning_peak","from":"07:00","to":"09:30","flag":0,"per_km":56,"per_min":10,"per_min_waiting":15},{"name":"normal_day","from":"09:30","to":"16:00","flag":0,"per_km":56,"per_min":9,"per_min_waiting":14},{"name":"evening_peak","from":"16:00","to":"19:30","flag":0,"per_km":56,"per_min":10,"per_min_waiting":15},{"name":"normal_evening","from":"19:30","to":"23:00","flag":0,"per_km":56,"per_min":9,"per_min_waiting":14},{"name":"late_night","from":"23:00","to":"07:00","flag":0,"per_km":56,"per_min":11,"per_min_waiting":17}],"booking_fee":0,"min_fare":150,"free_waiting_min":5,"surge":{"enabled":false},"commission":{"percent":11}}'::jsonb
FROM tripz_tenants t WHERE t.country_pack = 'sy';
-- saver
@@ -329,7 +293,7 @@ SELECT uuid_generate_v4(), t.id, 'default', 'saver',
COALESCE((SELECT MAX(version) FROM tripz_tariffs x
WHERE x.tenant_id = t.id AND x.city = 'default'
AND x.service_class = 'saver'), 0) + 1,
true, '{"currency":"SYP","timezone":"Asia/Damascus","mode":"time_and_distance","rounding":{"increment":5,"mode":"nearest"},"windows":[{"name":"morning_peak","from":"07:00","to":"09:30","flag":57,"per_km":41,"per_min":9,"per_min_waiting":82},{"name":"normal_day","from":"09:30","to":"16:00","flag":45,"per_km":32,"per_min":7,"per_min_waiting":65},{"name":"evening_peak","from":"16:00","to":"19:30","flag":57,"per_km":41,"per_min":9,"per_min_waiting":82},{"name":"normal_evening","from":"19:30","to":"23:00","flag":45,"per_km":32,"per_min":7,"per_min_waiting":65},{"name":"late_night","from":"23:00","to":"07:00","flag":52,"per_km":37,"per_min":8,"per_min_waiting":75}],"booking_fee":34,"min_fare":150,"free_waiting_min":5,"surge":{"enabled":false},"commission":{"percent":11}}'::jsonb
true, '{"currency":"SYP","timezone":"Asia/Damascus","mode":"time_and_distance","rounding":{"increment":5,"mode":"nearest"},"windows":[{"name":"morning_peak","from":"07:00","to":"09:30","flag":0,"per_km":34,"per_min":10,"per_min_waiting":15},{"name":"normal_day","from":"09:30","to":"16:00","flag":0,"per_km":34,"per_min":9,"per_min_waiting":14},{"name":"evening_peak","from":"16:00","to":"19:30","flag":0,"per_km":34,"per_min":10,"per_min_waiting":15},{"name":"normal_evening","from":"19:30","to":"23:00","flag":0,"per_km":34,"per_min":9,"per_min_waiting":14},{"name":"late_night","from":"23:00","to":"07:00","flag":0,"per_km":34,"per_min":11,"per_min_waiting":17}],"booking_fee":0,"min_fare":150,"free_waiting_min":5,"surge":{"enabled":false},"commission":{"percent":11}}'::jsonb
FROM tripz_tenants t WHERE t.country_pack = 'sy';
COMMIT;
+106 -81
View File
@@ -3,33 +3,41 @@ import { TariffDefinition } from './entities/tariff.entity';
/**
* تعرفة الانطلاق الافتراضية لكل بلد — **مصدر الحقيقة الوحيد**.
*
* يستهلكها اثنان: `SeedService` (فيأخذها كل مستأجر جديد تلقائياً) ومولّد
* الـSQL (`scripts/generate-tariff-sql.ts`) لقواعد قائمة. نسختان من هذه
* الأرقام كانتا ستتباعدان بصمت، والتسعير أسوأ مكان يقع فيه هذا.
* يستهلكها ثلاثة: `TenantsService.provision` و`SeedService` (فيأخذها كل
* مستأجر جديد تلقائياً) ومولّد الـSQL للقواعد القائمة. نسختان من هذه
* الأرقام كانتا ستتباعدان بصمت، والتسعير أسوأ مكان يقع فيه ذلك.
*
* هذه تعرفة **بداية** فقط؛ يُفترض أن يحدّثها كرون استخبارات الأسعار بنسخة
* أعلى (`version + 1`) لا أن يكتب فوقها.
* تعرفة **بداية** فقط؛ يُفترض أن يحدّثها بوت استخبارات الأسعار بنسخة أعلى
* (`version + 1`) لا أن يكتب فوقها.
*
* قرارات تحكم الملف (وسبب كلٍّ منها):
* ## الأرقام من جدول `kazan` الحيّ في سيرو (2026-07-19)
*
* 1. **`city = 'default'`** لا اسم البلد — كل الكود يبحث بهذه القيمة
* (`trips.service`: `dto.city ?? 'default'`). النسخة السابقة كتبت
* `'jordan'/'egypt'/'syria'` فما طابقت شيئاً: لا تعرفة ← الراكب بلا
* سعر ← `settleFare` تأخذ `quoted_fare ?? 0` ← **رحلة مجانية وعمولة
* صفر**، بسطر تحذير واحد في اللوج. البلد من `country_pack` للمستأجر.
* لا من تقدير: العمولة 11٪ سوريا · 12٪ مصر · 14٪ الأردن مطابقة حرفياً.
* وثلاثة تصحيحات على البيانات الخام، كلٌّ بسببه:
*
* 2. **النوافذ بالتوقيت المحلي** عبر `timezone` — المحرّك كان يقرأ بـUTC،
* فذروة 16:00–20:00 تشتغل 19:00–23:00 في عمّان.
* 1. **محورا الأردن مقلوبان.** أعمدة الفئات عنده تحمل سعر **الدقيقة**
* (0.043) وأعمدة النوافذ تحمل سعر **الكيلومتر** (0.184) — عكس مصر
* وسوريا. قُرئت بالاتجاه الصحيح هنا؛ قراءتها كما هي تجعل الكيلومتر
* بـ0.043 ديناراً والدقيقة بـ0.184.
*
* 3. **فتحة العدّاد (`flag`) حقيقية** لا صفر — الفتحة تغطي تكلفة بدء
* الرحلة، وإسقاطها يجعل الرحلة القصيرة خاسرة على السائق.
* 2. **نِسَب الفئات في مصر وسوريا معطوبة**: الكهربائي 0.23 والفان 0.25–0.40
* من سعر الاقتصادي — أي أن الفان **أرخص من الاقتصادي**، فيصير الخيار
* العقلاني لكل راكب هو أغلى فئة على المشغّل. نِسَب الأردن وحدها متماسكة
* (0.86 → 1.51 متصاعدة)، فاعتُمدت للبلدان الثلاثة.
*
* 4. **الحدّ الأدنى بقرار المالك**: 1.10 دينار · 20 جنيهاً · 150 ليرة.
* النسخة السابقة اشتقّته `perKm × 2` فأنتجت 0.368 ديناراً — ثلث الحقيقي.
* 3. **سعر الكيلو في مصر 4 جنيهات** لا 3.5 (قرار المالك: هذا المتعارف عليه).
*
* 5. **الفئات مشتقّة بمعاملات** لا أرقاماً مستقلّة — الأرقام الخام السابقة
* جعلت الفان أرخص من الاقتصادي في مصر وسوريا (22 مقابل 29.5 جنيهاً)،
* أي أن كل راكب عاقل يطلب أغلى فئة على المشغّل.
* ## البنية (مطابقة لسيرو — وهي ما سيكتب فيه بوت استخبارات الأسعار)
*
* - **سعر الكيلومتر يتغيّر بالفئة فقط**: رقم ثابت يعرفه السوق («الكيلو
* بأربعة جنيه»)، فلا يصحّ أن يتذبذب بالساعة.
* - **سعر الدقيقة يتغيّر بالنافذة فقط**: الوقت هو ما يزدحم لا المسافة.
* - **لا فتحة عدّاد**: `startPrice = 0` في كل صفوف سيرو، وقرار المالك أن
* **الحدّ الأدنى هو الآلية** («ما في رحلة أقل من 20 جنيهاً») لا رسمٌ
* يُضاف فوق كل أجرة.
*
* `city = 'default'` لا اسم البلد: كل الكود يبحث بهذه القيمة
* (`trips.service`: `dto.city ?? 'default'`)، والبلد من `country_pack`.
*/
export const SERVICE_CLASSES = {
@@ -47,60 +55,82 @@ export const SERVICE_CLASSES = {
export type ServiceClass = (typeof SERVICE_CLASSES)[keyof typeof SERVICE_CLASSES];
/**
* معاملات الفئات نسبةً إلى الاقتصادي. الترتيب هنا **هو** منطق المنتج:
* أوفر < اقتصادي < كهربائي < ليدي < مريح < فان < VIP.
* الفئات التي تُزرع تلقائياً لمستأجر جديد.
*
* **`fixed` مستثناة عمداً.** السعر الثابت بلا جدول خطوط (أصل → وجهة → سعر)
* يعني رقماً واحداً يغطّي كل الوجهات: أظهر الاختبار الحيّ أن رحلة 55.8 كم
* تُسعَّر 2.5 ديناراً مقابل 23.2 للاقتصادي — أي أن أي راكب يكتب
* `service_class: "fixed"` يأخذ رحلة المطار بعُشر ثمنها. تُفعَّل حين يُبنى
* جدول الخطوط، لا قبله.
*/
export const CLASS_MULTIPLIERS: Record<string, number> = {
[SERVICE_CLASSES.SAVER]: 0.85,
[SERVICE_CLASSES.DELIVERY]: 0.9,
[SERVICE_CLASSES.ECONOMY]: 1.0,
[SERVICE_CLASSES.FIXED]: 1.0,
[SERVICE_CLASSES.ELECTRIC]: 1.05,
[SERVICE_CLASSES.LADY]: 1.1,
[SERVICE_CLASSES.COMFORT]: 1.25,
[SERVICE_CLASSES.VAN]: 1.45,
[SERVICE_CLASSES.VIP]: 1.75,
};
export const SEEDED_CLASSES: string[] = Object.values(SERVICE_CLASSES).filter(
(c) => c !== SERVICE_CLASSES.FIXED,
);
/**
* الأساس الاقتصادي بالدينار — مأخوذ من التعرفة العاملة في `SeedService`
* لا من أرقام النظام القديم.
* نِسَب الفئات إلى الاقتصادي — من صفّ الأردن في `kazan` (المجموعة الوحيدة
* المتماسكة). الترتيب هنا **هو** منطق المنتج، وأي رقم يكسره يقلب اختيار
* الراكب رأساً على عقب.
*
* **الفان أغلى من VIP** (1.51 مقابل 1.40): هذا ما تقوله بيانات المالك،
* ومنطقيّ — مركبة بسبعة مقاعد أعلى كلفة من سيارة أفخم.
*/
const JOD_BASE = {
flag: 0.39,
perKm: 0.28,
perMin: 0.06,
waitingPerMin: 0.48,
bookingFee: 0.25,
export const CLASS_MULTIPLIERS: Record<string, number> = {
[SERVICE_CLASSES.SAVER]: 0.86,
[SERVICE_CLASSES.DELIVERY]: 0.91,
[SERVICE_CLASSES.ECONOMY]: 1.0,
[SERVICE_CLASSES.FIXED]: 1.0,
[SERVICE_CLASSES.LADY]: 1.09,
[SERVICE_CLASSES.ELECTRIC]: 1.21,
[SERVICE_CLASSES.COMFORT]: 1.3,
[SERVICE_CLASSES.VIP]: 1.4,
[SERVICE_CLASSES.VAN]: 1.51,
};
/** النوافذ الزمنية — بالتوقيت المحلي، وتغطي اليوم كاملاً بلا ثغرة. */
export const WINDOWS = [
{ name: 'morning_peak', from: '07:00', to: '09:30', rate: 'peak' as const },
{ name: 'normal_day', from: '09:30', to: '16:00', rate: 'normal' as const },
{ name: 'evening_peak', from: '16:00', to: '19:30', rate: 'peak' as const },
{ name: 'normal_evening', from: '19:30', to: '23:00', rate: 'normal' as const },
{ name: 'late_night', from: '23:00', to: '07:00', rate: 'late' as const },
];
export interface CountryTariff {
countryPack: string;
currency: string;
timezone: string;
/** أرضية الأجرة — «ما في رحلة أقل من هذا» (قرار المالك). */
minFare: number;
/** معامل التحويل من الدينار — مشتقّ من الحدّ الأدنى لا من سعر صرف يومي. */
scale: number;
/** سعر الكيلومتر للفئة الاقتصادية؛ باقي الفئات مشتقّة بالنِّسَب. */
perKm: number;
/** سعر الدقيقة لكل نافذة — من أعمدة `normal/peak/lateMinPrice`. */
perMin: { normal: number; peak: number; late: number };
commissionPercent: number;
rounding: { increment: number; mode: 'nearest' | 'up' | 'down' };
/** سعر الخط الثابت الافتراضي (فئة `fixed`) — يضبطه المشغّل لكل خط. */
/**
* سعر الوقود من `kazan.fuelPrice`. لا يدخل الحساب اليوم — يُحفظ لأنه
* **مدخل بوت استخبارات الأسعار**: مقارنة السوق بلا كلفة التشغيل مقارنة
* ناقصة، وهو الرقم الذي يتحرّك بين المراجعات.
*/
fuelPrice: number;
/** سعر الخط الثابت — معرَّف ولا يُزرع (انظر `SEEDED_CLASSES`). */
fixedFare: number;
}
/**
* `scale` مشتقّ من الحدّ الأدنى الذي أقرّه المالك (20 ÷ 1.10 لمصر،
* 150 ÷ 1.10 لسوريا): مرساة واحدة تعني أن كل الأرقام تتحرك معاً حين
* يتغير الحدّ الأدنى، بدل جدولين ينحرف أحدهما عن الآخر.
*/
export const COUNTRY_TARIFFS: CountryTariff[] = [
{
countryPack: 'jo',
currency: 'JOD',
timezone: 'Asia/Amman',
minFare: 1.1,
scale: 1,
commissionPercent: 14.0,
perKm: 0.184, // من `normalMinPrice` — عمود الكيلومتر عند الأردن
// نسبة ذروة الأردن من محوره الأصلي (0.212 ÷ 0.184 = 1.152)، مطبَّقة على
// الدقيقة توحيداً مع مصر وسوريا.
perMin: { normal: 0.043, peak: 0.0495, late: 0.043 },
commissionPercent: 14,
rounding: { increment: 0.05, mode: 'nearest' },
fuelPrice: 1,
fixedFare: 2.5,
},
{
@@ -108,9 +138,11 @@ export const COUNTRY_TARIFFS: CountryTariff[] = [
currency: 'EGP',
timezone: 'Africa/Cairo',
minFare: 20,
scale: 20 / 1.1,
commissionPercent: 12.0,
perKm: 4.0, // قرار المالك — `kazan` يقول 3.5
perMin: { normal: 0.8, peak: 1.4, late: 1.5 },
commissionPercent: 12,
rounding: { increment: 1, mode: 'nearest' },
fuelPrice: 13,
fixedFare: 45,
},
{
@@ -118,22 +150,15 @@ export const COUNTRY_TARIFFS: CountryTariff[] = [
currency: 'SYP',
timezone: 'Asia/Damascus',
minFare: 150,
scale: 150 / 1.1,
commissionPercent: 11.0,
perKm: 40,
perMin: { normal: 9, peak: 10, late: 11 },
commissionPercent: 11,
rounding: { increment: 5, mode: 'nearest' },
fuelPrice: 110,
fixedFare: 350,
},
];
/** النوافذ الزمنية — بالتوقيت المحلي، وتغطي اليوم كاملاً بلا ثغرة. */
export const WINDOWS = [
{ name: 'morning_peak', from: '07:00', to: '09:30', multiplier: 1.25 },
{ name: 'normal_day', from: '09:30', to: '16:00', multiplier: 1.0 },
{ name: 'evening_peak', from: '16:00', to: '19:30', multiplier: 1.25 },
{ name: 'normal_evening', from: '19:30', to: '23:00', multiplier: 1.0 },
{ name: 'late_night', from: '23:00', to: '07:00', multiplier: 1.15 },
];
/** تدوير لخانة معقولة بحجم العملة — 3 كسور بالدينار، صحيح بالليرة. */
function money(value: number, currency: string): number {
const decimals = currency === 'JOD' ? 3 : currency === 'EGP' ? 2 : 0;
@@ -153,14 +178,15 @@ export function buildDefinition(
serviceClass: string,
): TariffDefinition {
const classMultiplier = CLASS_MULTIPLIERS[serviceClass] ?? 1;
const rate = (base: number, windowMultiplier: number) =>
money(base * country.scale * classMultiplier * windowMultiplier, country.currency);
const isFixed = serviceClass === SERVICE_CLASSES.FIXED;
// سعر الكيلومتر ثابت عبر النوافذ — الفئة وحدها تحرّكه.
const perKm = money(country.perKm * classMultiplier, country.currency);
const definition: TariffDefinition = {
currency: country.currency,
// المنطقة الزمنية شرط صحة النوافذ — بدونها يقرأ المحرّك بـUTC.
// المنطقة الزمنية شرط صحة النوافذ — بدونها يقرأ المحرّك بـUTC، فتقع
// ذروة 16:00 فعلياً عند 19:00 في عمّان.
timezone: country.timezone,
mode: isFixed ? 'fixed_quote' : 'time_and_distance',
rounding: country.rounding,
@@ -168,17 +194,18 @@ export function buildDefinition(
name: w.name,
from: w.from,
to: w.to,
flag: rate(JOD_BASE.flag, w.multiplier),
per_km: rate(JOD_BASE.perKm, w.multiplier),
per_min: rate(JOD_BASE.perMin, w.multiplier),
// الانتظار لا يتبع معامل الفئة: دقيقة انتظار السائق واحدة مهما كانت
// فئة السيارة — الفرق في السيارة لا في الوقت.
per_min_waiting: money(
JOD_BASE.waitingPerMin * country.scale * w.multiplier,
country.currency,
),
// لا فتحة عدّاد: `startPrice = 0` في كل صفوف سيرو، والحدّ الأدنى هو
// الآلية بدلاً منها (قرار المالك).
flag: 0,
per_km: perKm,
per_min: money(country.perMin[w.rate], country.currency),
// دقيقة انتظار السائق واحدة مهما كانت فئة المركبة — الفرق في السيارة
// لا في الوقت. الضِّعف ونصف لأن الانتظار تعطيل كامل لا سير.
per_min_waiting: money(country.perMin[w.rate] * 1.5, country.currency),
})),
booking_fee: money(JOD_BASE.bookingFee * country.scale, country.currency),
// الحدّ الأدنى **أرضية** لا رسم يُضاف: رحلة تحته تُرفع إليه، وما فوقه
// يُحاسَب بالعدّاد وحده.
booking_fee: 0,
min_fare: country.minFare,
free_waiting_min: 5,
surge: { enabled: false },
@@ -187,10 +214,8 @@ export function buildDefinition(
if (isFixed) {
definition.fixed_fare = country.fixedFare;
// السعر الثابت لا حدّ أدنى له ولا رسم حجز: المعلَن هو المتفَق عليه،
// ورفعه بحدّ أدنى يعني إعلان سعر وتحصيل غيره.
// السعر المعلَن هو المتفَق عليه؛ رفعه بحدّ أدنى يعني إعلان سعر وتحصيل غيره.
definition.min_fare = 0;
definition.booking_fee = 0;
}
return definition;
+85 -9
View File
@@ -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');
});
});
@@ -1,5 +1,5 @@
import { TenantsService } from './tenants.service';
import { SERVICE_CLASSES } from '../tariff/default-tariffs';
import { SEEDED_CLASSES, SERVICE_CLASSES } from '../tariff/default-tariffs';
/**
* حارس ثغرة كُشفت على السيرفر (2026-07-19): `SeedService` كان يزرع التعرفة
@@ -35,12 +35,19 @@ describe('TenantsService.provision — تعرفة الانطلاق', () => {
return { svc, savedTariffs, tariffRepo };
}
it('**كل فئة خدمة تُزرع** لمستأجر جديد — لا الاقتصادي وحده', async () => {
it('**كل فئة قابلة للزرع تُزرع** لمستأجر جديد — لا الاقتصادي وحده', async () => {
const { svc, savedTariffs } = build();
await svc.provision({ name: 'Test', slug: 'test-jo', countryPack: 'jo' });
const classes = savedTariffs.map((t) => t.service_class).sort();
expect(classes).toEqual(Object.values(SERVICE_CLASSES).sort());
expect(classes).toEqual([...SEEDED_CLASSES].sort());
});
it('**`fixed` لا تُزرع** — سعر ثابت بلا جدول خطوط تسريب', async () => {
const { svc, savedTariffs } = build();
await svc.provision({ name: 'Test2', slug: 'test-jo-2', countryPack: 'jo' });
// الاختبار الحيّ: 55.8 كم بـ2.5 ديناراً مقابل 23.2 للاقتصادي.
expect(savedTariffs.map((t) => t.service_class)).not.toContain(SERVICE_CLASSES.FIXED);
});
it('يزرع بعملة البلد ومنطقته الزمنية — لا افتراض الأردن للجميع', async () => {
@@ -7,7 +7,7 @@ import { Tariff } from '../tariff/entities/tariff.entity';
import {
buildDefinition,
countryTariff,
SERVICE_CLASSES,
SEEDED_CLASSES,
} from '../tariff/default-tariffs';
import { TenantRevenueEntry } from '../tenant-wallet/entities/tenant-ledger.entity';
import { CacheService, CacheKeys, TTL } from '../../common/cache/cache.service';
@@ -150,7 +150,7 @@ export class TenantsService {
private async seedTariffs(tenant: Tenant): Promise<void> {
try {
const country = countryTariff(tenant.countryPack);
const rows = Object.values(SERVICE_CLASSES).map((serviceClass) =>
const rows = SEEDED_CLASSES.map((serviceClass) =>
this.tariffs.create({
tenant_id: tenant.id,
city: 'default',
@@ -102,8 +102,9 @@ describe('settleFare — تسعير النهاية', () => {
expect(out.billed_distance_km).toBe(12);
expect(out.actual_distance_km).toBe(12);
expect(out.distance_ratio).toBe(1.2);
// 0.39 فتحة + 12×0.28 مسافة + 20×0.06 زمن + 0.25 رسم حجز = 5.20
expect(out.final_fare).toBeCloseTo(5.2, 2);
// 12×0.184 مسافة + 20×0.043 زمن = 3.068 → تدوير 0.05 → 3.05
// (بلا فتحة ولا رسم حجز — أرقام `kazan`، والأرضية 1.10 لم تُبلَغ)
expect(out.final_fare).toBeCloseTo(3.05, 2);
// السعر المقفول وقت الطلب (4.0) لم يعد هو الحاكم.
expect(out.final_fare).not.toBe(4.0);
});
@@ -141,8 +142,8 @@ describe('settleFare — تسعير النهاية', () => {
it('الانتظار يُضاف فوق الأجرة المعاد تسعيرها', async () => {
const { svc } = build({ row: { ...baseRow, waiting_charge: 1.2 } });
const out = await settle(svc, snapshot);
// 0.39 + 10×0.28 + 20×0.06 = 4.39 → +1.2 انتظار = 5.59 → تدوير
expect(out.final_fare).toBeGreaterThan(5.5);
// 10×0.184 + 20×0.043 = 2.70، والانتظار 1.2 يُضاف فوقها.
expect(out.final_fare).toBeCloseTo(3.9, 2);
});
});
+14 -2
View File
@@ -204,8 +204,20 @@ export class TripsService {
commissionAmount = c.amount;
commissionRate = c.rate;
}
} catch {
this.logger.warn(`no tariff for ${city}/${serviceClass} — trip without quote`);
} catch (e: any) {
// **رحلة بلا سعر تُرفض ولا تُنشأ** (قرار المالك 2026-07-19).
//
// كان الاستثناء يُبتلع فتُنشأ رحلة بـ`quoted_fare = null`، ثم تأخذ
// `settleFare` القيمة `quoted_fare ?? 0` — أي **رحلة مجانية وعمولة
// صفر**، بسطر تحذير واحد في اللوج. و`service_class` يأتي من الطلب بلا
// تحقّق، فكانت كلمة عشوائية (`"foo"`) تكفي للحصول على رحلة بلا أجرة.
//
// الفشل الصريح هنا أأمن من الصامت: الراكب يرى «الفئة غير متاحة» بدل
// أن يكتشف المشغّل بعد شهر أن رحلاتٍ مرّت بلا تحصيل.
this.logger.error(`no active tariff for ${city}/${serviceClass}: ${e?.message}`);
throw new BadRequestException(
`No pricing available for service class "${serviceClass}" in "${city}"`,
);
}
// الكوبون يُقيَّم الآن ليعرف الراكب سعره قبل التأكيد، لكنه **لا يُستهلك**