P1 complete: real انطلق maps + Nabeh WhatsApp OTP + chat + ratings + cancel(both) + fraud detection

- maps: real map-saas route/geocode/reverse/places (x-api-key per country) + straight-line fallback
- auth: Redis-backed OTP + Nabeh WhatsApp send (dev mode keeps fixed 1234 bypass)
- chat: per-trip messages (participant-guarded) + socket broadcast
- ratings: post-trip rating + driver avg recompute + no double-rate
- cancel: from rider or driver + stage-based cancel fee + notify both
- fraud: cancel-abuse (soft/hard block via Redis), arrived-far-from-pickup, completed-too-fast → fraud_flags
- migration InitP1b (chat_messages, ratings, fraud_flags, trips.cancelled_by/cancel_fee)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-16 17:12:38 +03:00
co-authored by Claude Opus 4.8
parent 00870a8cc3
commit 65a2e5e275
25 changed files with 948 additions and 53 deletions
@@ -0,0 +1,59 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
/**
* P1b: chat_messages, ratings, fraud_flags + أعمدة إلغاء على trips.
*/
export class InitP1b1721400000000 implements MigrationInterface {
public async up(q: QueryRunner): Promise<void> {
await q.query(`ALTER TABLE tripz_trips ADD COLUMN IF NOT EXISTS cancelled_by varchar`);
await q.query(`ALTER TABLE tripz_trips ADD COLUMN IF NOT EXISTS cancel_fee numeric(12,3)`);
await q.query(`
CREATE TABLE IF NOT EXISTS tripz_chat_messages (
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
tenant_id uuid NOT NULL,
trip_id uuid NOT NULL,
sender_id uuid NOT NULL,
sender_role varchar NOT NULL,
body text NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
)`);
await q.query(`CREATE INDEX IF NOT EXISTS "IDX_tripz_chat_trip" ON tripz_chat_messages (tenant_id, trip_id)`);
await q.query(`
CREATE TABLE IF NOT EXISTS tripz_ratings (
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
tenant_id uuid NOT NULL,
trip_id uuid NOT NULL,
by_user_id uuid NOT NULL,
by_role varchar NOT NULL,
target_driver_id uuid,
stars int NOT NULL,
comment text,
created_at timestamptz NOT NULL DEFAULT now()
)`);
await q.query(`CREATE INDEX IF NOT EXISTS "IDX_tripz_ratings_trip" ON tripz_ratings (tenant_id, trip_id)`);
await q.query(`CREATE INDEX IF NOT EXISTS "IDX_tripz_ratings_driver" ON tripz_ratings (tenant_id, target_driver_id)`);
await q.query(`
CREATE TABLE IF NOT EXISTS tripz_fraud_flags (
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
tenant_id uuid NOT NULL,
subject_type varchar NOT NULL,
subject_id uuid NOT NULL,
trip_id uuid,
reason varchar NOT NULL,
meta jsonb NOT NULL DEFAULT '{}',
created_at timestamptz NOT NULL DEFAULT now()
)`);
await q.query(`CREATE INDEX IF NOT EXISTS "IDX_tripz_fraud_subject" ON tripz_fraud_flags (tenant_id, subject_type, subject_id)`);
}
public async down(q: QueryRunner): Promise<void> {
await q.query(`DROP TABLE IF EXISTS tripz_fraud_flags`);
await q.query(`DROP TABLE IF EXISTS tripz_ratings`);
await q.query(`DROP TABLE IF EXISTS tripz_chat_messages`);
await q.query(`ALTER TABLE tripz_trips DROP COLUMN IF EXISTS cancel_fee`);
await q.query(`ALTER TABLE tripz_trips DROP COLUMN IF EXISTS cancelled_by`);
}
}