import { Injectable, BadRequestException, NotFoundException } from '@nestjs/common'; import { DatabaseService } from '../../database/database.service'; import { BreakEvenService } from './breakeven.service'; @Injectable() export class DriversService { constructor( private readonly db: DatabaseService, private readonly breakEven: BreakEvenService, ) {} // ============================================================ // DRIVER SESSION - Daily Login Flow // ============================================================ async startSession(driverId: string): Promise { // All registered drivers can start — no approval gate for MVP const driver = await this.db.queryOne( `SELECT d.*, u.first_name, u.last_name FROM drivers d JOIN users u ON u.id = d.user_id WHERE d.id = $1`, [driverId], ); if (!driver) throw new NotFoundException('Driver not found'); // Check if session already exists today const existing = await this.db.queryOne( `SELECT * FROM driver_sessions WHERE driver_id = $1 AND session_date = CURRENT_DATE`, [driverId], ); if (existing) return { session: existing, breakEven: this.breakEven.calculate(existing) }; // $20 daily fee tracked as debt — deducted from first payouts, no upfront card charge const session = await this.db.queryOne( `INSERT INTO driver_sessions (driver_id, daily_fee, fee_paid) VALUES ($1, $2, FALSE) RETURNING *`, [driverId, 20.00], ); // Mark driver as online and available await this.db.query( `UPDATE driver_locations SET is_online = TRUE, is_available = TRUE, updated_at = NOW() WHERE driver_id = $1`, [driverId], ); return { session, breakEven: this.breakEven.calculate(session) }; } async endSession(driverId: string): Promise { const session = await this.db.queryOne( `UPDATE driver_sessions SET logout_at = NOW(), status = 'inactive' WHERE driver_id = $1 AND session_date = CURRENT_DATE AND status = 'active' RETURNING *`, [driverId], ); await this.db.query( `UPDATE driver_locations SET is_online = FALSE, is_available = FALSE WHERE driver_id = $1`, [driverId], ); return { session, breakEven: session ? this.breakEven.calculate(session) : null }; } async getSession(driverId: string): Promise { const session = await this.db.queryOne( `SELECT * FROM driver_sessions WHERE driver_id = $1 AND session_date = CURRENT_DATE`, [driverId], ); if (!session) return { session: null, breakEven: null }; const earnings = await this.db.queryMany( `SELECT de.*, o.order_number, o.delivered_at FROM driver_earnings de JOIN orders o ON o.id = de.order_id WHERE de.session_id = $1 ORDER BY de.created_at DESC`, [session.id], ); return { session, breakEven: this.breakEven.calculate(session), earnings, }; } // ============================================================ // DRIVER PROFILE & EARNINGS // ============================================================ async getProfile(driverId: string) { return this.db.queryOne( `SELECT d.*, u.first_name, u.last_name, u.email, u.phone, u.avatar_url, z.name AS zone_name FROM drivers d JOIN users u ON u.id = d.user_id LEFT JOIN zones z ON z.id = d.zone_id WHERE d.id = $1`, [driverId], ); } async getEarningsHistory(driverId: string, days: number = 30) { return this.db.queryMany( `SELECT ds.session_date, ds.deliveries_count, ds.delivery_revenue, ds.tips_earned, ds.net_earnings, ds.daily_fee, ds.fee_paid FROM driver_sessions ds WHERE ds.driver_id = $1 AND ds.session_date >= CURRENT_DATE - INTERVAL '${days} days' ORDER BY ds.session_date DESC`, [driverId], ); } async getAnalytics(driverId: string) { const [summary, weekly, hourly] = await Promise.all([ // All-time + period summaries this.db.queryOne( `SELECT COUNT(*) FILTER (WHERE session_date >= CURRENT_DATE - 6) AS sessions_7d, COUNT(*) FILTER (WHERE session_date >= CURRENT_DATE - 29) AS sessions_30d, SUM(deliveries_count) FILTER (WHERE session_date >= CURRENT_DATE - 6) AS trips_7d, SUM(deliveries_count) FILTER (WHERE session_date >= CURRENT_DATE - 29) AS trips_30d, SUM(deliveries_count) AS trips_total, SUM(net_earnings) FILTER (WHERE session_date >= CURRENT_DATE - 6) AS net_7d, SUM(net_earnings) FILTER (WHERE session_date >= CURRENT_DATE - 29) AS net_30d, SUM(net_earnings) AS net_total, SUM(tips_earned) FILTER (WHERE session_date >= CURRENT_DATE - 29) AS tips_30d, SUM(tips_earned) AS tips_total, AVG(CASE WHEN deliveries_count > 0 THEN net_earnings END) AS avg_daily_net FROM driver_sessions WHERE driver_id = $1`, [driverId], ), // Day-of-week breakdown (0=Sun, 6=Sat) this.db.queryMany( `SELECT EXTRACT(DOW FROM session_date) AS dow, TO_CHAR(session_date, 'Dy') AS day_name, AVG(deliveries_count) AS avg_trips, AVG(net_earnings) AS avg_net FROM driver_sessions WHERE driver_id = $1 AND session_date >= CURRENT_DATE - 90 GROUP BY dow, day_name ORDER BY dow`, [driverId], ), // Hour-of-day delivery distribution from driver_earnings this.db.queryMany( `SELECT EXTRACT(HOUR FROM de.created_at) AS hour, COUNT(*) AS deliveries, SUM(de.total) AS revenue FROM driver_earnings de WHERE de.driver_id = $1 AND de.created_at >= NOW() - INTERVAL '90 days' GROUP BY hour ORDER BY hour`, [driverId], ), ]); // Find peak day and peak hour const peakDay = weekly.reduce( (best: any, d: any) => (Number(d.avg_trips) > Number(best?.avg_trips || 0) ? d : best), null, ); const peakHour = hourly.reduce( (best: any, h: any) => (Number(h.deliveries) > Number(best?.deliveries || 0) ? h : best), null, ); return { summary, weekly, hourly, peakDay, peakHour }; } async updateLocation( driverId: string, lng: number, lat: number, heading?: number, speedKmh?: number, ) { return this.db.query( `INSERT INTO driver_locations (driver_id, location, heading, speed_kmh, updated_at) VALUES ($1, ST_GeographyFromText($2), $3, $4, NOW()) ON CONFLICT (driver_id) DO UPDATE SET location = EXCLUDED.location, heading = EXCLUDED.heading, speed_kmh = EXCLUDED.speed_kmh, updated_at = NOW()`, [driverId, `POINT(${lng} ${lat})`, heading, speedKmh], ); } async setAvailability(driverId: string, isAvailable: boolean) { return this.db.query( `UPDATE driver_locations SET is_available = $2 WHERE driver_id = $1`, [driverId, isAvailable], ); } async getNearbyDrivers(lng: number, lat: number, radiusKm: number = 5) { return this.db.queryMany( `SELECT dl.driver_id, u.first_name, d.vehicle_type, d.rating, ST_Distance(dl.location, ST_GeographyFromText($1)) / 1000 AS distance_km, ST_X(dl.location::geometry) AS lng, ST_Y(dl.location::geometry) AS lat FROM driver_locations dl JOIN drivers d ON d.id = dl.driver_id JOIN users u ON u.id = d.user_id WHERE dl.is_online = TRUE AND dl.is_available = TRUE AND ST_DWithin(dl.location, ST_GeographyFromText($1), $2 * 1000) ORDER BY distance_km ASC LIMIT 20`, [`POINT(${lng} ${lat})`, radiusKm], ); } }