P2: ride types + class-aware matching + round-trip + dispatch + wallet + FCM + WS live location/WebRTC

- ride-types: per-tenant catalog (economy/comfort/electric/family_van/women/scooter), seeded
- matching now class-scoped (geo:drivers:{tenant}:{class}); trips match by requested type
- trips: is_round_trip (doubles distance for fare) + payment_method + wallet settlement on paid
- dispatch: operator creates trip for phone customer (role-guarded)
- wallet: balance + credit/debit + txns + self topup; wallet-paid trips settle rider→driver
- notifications: FCM device tokens + best-effort push (assign event)
- realtime: trip:join, live driver:location broadcast, WebRTC call signaling (offer/answer/ice/end)
- migration InitP2 (ride_types, wallets, wallet_txns, device_tokens, trips.is_round_trip)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-16 17:39:56 +03:00
co-authored by Claude Opus 4.8
parent ccb80d9e53
commit 16df6258c1
30 changed files with 737 additions and 27 deletions
@@ -0,0 +1,71 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
/**
* P2: ride_types, wallets, wallet_txns, device_tokens + trips.is_round_trip.
*/
export class InitP21721500000000 implements MigrationInterface {
public async up(q: QueryRunner): Promise<void> {
await q.query(`ALTER TABLE tripz_trips ADD COLUMN IF NOT EXISTS is_round_trip boolean NOT NULL DEFAULT false`);
await q.query(`
CREATE TABLE IF NOT EXISTS tripz_ride_types (
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
tenant_id uuid NOT NULL,
code varchar NOT NULL,
name_ar varchar NOT NULL,
name_en varchar,
vehicle_kind varchar NOT NULL DEFAULT 'car',
women_only boolean NOT NULL DEFAULT false,
round_trip_supported boolean NOT NULL DEFAULT true,
icon varchar,
sort int NOT NULL DEFAULT 0,
active boolean NOT NULL DEFAULT true,
created_at timestamptz NOT NULL DEFAULT now()
)`);
await q.query(`CREATE UNIQUE INDEX IF NOT EXISTS "UQ_tripz_ride_types_code" ON tripz_ride_types (tenant_id, code)`);
await q.query(`
CREATE TABLE IF NOT EXISTS tripz_wallets (
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
tenant_id uuid NOT NULL,
user_id uuid NOT NULL,
balance numeric(12,3) NOT NULL DEFAULT 0,
currency varchar NOT NULL DEFAULT 'JOD',
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
)`);
await q.query(`CREATE UNIQUE INDEX IF NOT EXISTS "UQ_tripz_wallets_user" ON tripz_wallets (tenant_id, user_id)`);
await q.query(`
CREATE TABLE IF NOT EXISTS tripz_wallet_txns (
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
tenant_id uuid NOT NULL,
wallet_id uuid NOT NULL,
amount numeric(12,3) NOT NULL,
type varchar NOT NULL,
reason varchar NOT NULL,
ref varchar,
created_at timestamptz NOT NULL DEFAULT now()
)`);
await q.query(`CREATE INDEX IF NOT EXISTS "IDX_tripz_wallet_txns_wallet" ON tripz_wallet_txns (tenant_id, wallet_id)`);
await q.query(`
CREATE TABLE IF NOT EXISTS tripz_device_tokens (
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
tenant_id uuid NOT NULL,
user_id uuid NOT NULL,
token varchar NOT NULL UNIQUE,
platform varchar NOT NULL DEFAULT 'android',
created_at timestamptz NOT NULL DEFAULT now()
)`);
await q.query(`CREATE INDEX IF NOT EXISTS "IDX_tripz_device_tokens_user" ON tripz_device_tokens (tenant_id, user_id)`);
}
public async down(q: QueryRunner): Promise<void> {
await q.query(`DROP TABLE IF EXISTS tripz_device_tokens`);
await q.query(`DROP TABLE IF EXISTS tripz_wallet_txns`);
await q.query(`DROP TABLE IF EXISTS tripz_wallets`);
await q.query(`DROP TABLE IF EXISTS tripz_ride_types`);
await q.query(`ALTER TABLE tripz_trips DROP COLUMN IF EXISTS is_round_trip`);
}
}