diff --git a/backend/src/common/seed/seed.service.ts b/backend/src/common/seed/seed.service.ts index 5b67ce7..9f9eea7 100644 --- a/backend/src/common/seed/seed.service.ts +++ b/backend/src/common/seed/seed.service.ts @@ -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++; diff --git a/backend/src/database/seeds/generate-tariff-sql.ts b/backend/src/database/seeds/generate-tariff-sql.ts index 34e26a2..d33994d 100644 --- a/backend/src/database/seeds/generate-tariff-sql.ts +++ b/backend/src/database/seeds/generate-tariff-sql.ts @@ -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, "''"); diff --git a/backend/src/database/seeds/tariffs.sql b/backend/src/database/seeds/tariffs.sql index a52b79b..038b7e7 100644 --- a/backend/src/database/seeds/tariffs.sql +++ b/backend/src/database/seeds/tariffs.sql @@ -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; diff --git a/backend/src/modules/tariff/default-tariffs.ts b/backend/src/modules/tariff/default-tariffs.ts index d48d6fd..f1d6093 100644 --- a/backend/src/modules/tariff/default-tariffs.ts +++ b/backend/src/modules/tariff/default-tariffs.ts @@ -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 = { - [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 = { + [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; diff --git a/backend/src/modules/tariff/tariff-seed.spec.ts b/backend/src/modules/tariff/tariff-seed.spec.ts index 6fc3f5e..d9fe694 100644 --- a/backend/src/modules/tariff/tariff-seed.spec.ts +++ b/backend/src/modules/tariff/tariff-seed.spec.ts @@ -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'); + }); +}); diff --git a/backend/src/modules/tenants/provision-tariffs.spec.ts b/backend/src/modules/tenants/provision-tariffs.spec.ts index 5d94f9b..8adb355 100644 --- a/backend/src/modules/tenants/provision-tariffs.spec.ts +++ b/backend/src/modules/tenants/provision-tariffs.spec.ts @@ -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 () => { diff --git a/backend/src/modules/tenants/tenants.service.ts b/backend/src/modules/tenants/tenants.service.ts index feeaf9f..ad61fd2 100644 --- a/backend/src/modules/tenants/tenants.service.ts +++ b/backend/src/modules/tenants/tenants.service.ts @@ -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 { 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', diff --git a/backend/src/modules/trips/settle-fare.spec.ts b/backend/src/modules/trips/settle-fare.spec.ts index ad9b669..299ee39 100644 --- a/backend/src/modules/trips/settle-fare.spec.ts +++ b/backend/src/modules/trips/settle-fare.spec.ts @@ -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); }); }); diff --git a/backend/src/modules/trips/trips.service.ts b/backend/src/modules/trips/trips.service.ts index ca968c5..f1b0c5f 100644 --- a/backend/src/modules/trips/trips.service.ts +++ b/backend/src/modules/trips/trips.service.ts @@ -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}"`, + ); } // الكوبون يُقيَّم الآن ليعرف الراكب سعره قبل التأكيد، لكنه **لا يُستهلك**