Update backend to use firebase-admin SDK for FCM

This commit is contained in:
Hamza-Ayed
2026-07-19 16:21:43 +03:00
parent 978a7489e6
commit 959e2fa758
76 changed files with 4693 additions and 116 deletions
+255
View File
@@ -0,0 +1,255 @@
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { InjectRepository } from '@nestjs/typeorm';
import { IsNull, LessThan, Repository } from 'typeorm';
import { SurgeState } from './entities/surge-state.entity';
import { TariffService } from './tariff.service';
import { Driver } from '../drivers/entities/driver.entity';
/**
* خدمة التسعير الديناميكي — بوت المضاعف (L4).
*
* تحسب مضاعف الطلب/العرض لكل (مدينة × فئة) وتحدّث:
* 1. `surge_states` — الذاكرة المؤقتة للحالة
* 2. `tariffs.definition.surge.multiplier` — مصدر الحقيقة للمحرك
*
* الجدولة تتم عبر BullMQ (CronModule — O5) لا عبر `setInterval` هنا.
*
* المعادلة (من docs/22):
* ```
* ratio = demand / supply (supply = 0 → ratio = max)
* multiplier = 1 + (ratio - 1) × sensitivity
* multiplier = clamp(multiplier, min, max)
* ```
*/
@Injectable()
export class SurgeService {
private readonly logger = new Logger('Surge');
constructor(
@InjectRepository(SurgeState)
private readonly repo: Repository<SurgeState>,
@InjectRepository(Driver)
private readonly driverRepo: Repository<Driver>,
private readonly tariffService: TariffService,
private readonly config: ConfigService,
) {}
/**
* إعادة حساب المضاعفات لجميع الحالات النشطة.
* ي refresh عرض السائقين من قاعدة البيانات قبل كل حساب.
*/
async recalculateAll(): Promise<{ updated: number; unchanged: number }> {
const cooldownMs = this.config.get<number>('trips.surgeCooldownMs') ?? 300_000;
const sensitivity = this.config.get<number>('trips.surgeSensitivity') ?? 0.3;
const now = new Date();
const cutoff = new Date(now.getTime() - cooldownMs);
// تحديث العرض الفعلي من السائقين المتصلين
await this.refreshSupplyFromDrivers(now);
const states = await this.repo.find({
where: [
{ manual_override: false, last_changed_at: LessThan(cutoff) },
{ manual_override: false, last_changed_at: IsNull() },
],
});
let updated = 0;
let unchanged = 0;
for (const state of states) {
const newMultiplier = this.calculateMultiplier(
state.demand_count,
state.supply_count,
Number(state.min_multiplier),
Number(state.max_multiplier),
sensitivity,
);
const oldMultiplier = Number(state.multiplier);
// لا تحديث لو المضاعف لم يتغيّر بشكل ملموس
if (Math.abs(newMultiplier - oldMultiplier) < 0.01) {
unchanged++;
continue;
}
// تحديث مضاعف التعرفة
await this.tariffService.updateSurgeMultiplier(
state.tenant_id,
state.city,
state.service_class,
newMultiplier,
);
state.multiplier = newMultiplier;
state.last_changed_at = now;
await this.repo.save(state);
this.logger.log(
`${state.city}/${state.service_class}: ${oldMultiplier.toFixed(2)}x → ${newMultiplier.toFixed(2)}x ` +
`(demand=${state.demand_count}, supply=${state.supply_count})`,
);
updated++;
}
if (updated > 0) {
this.logger.log(`surge update: ${updated} changed, ${unchanged} unchanged`);
}
// تصفير العدادات لكل حالة تم حسابها — نافذة القياس = دورة واحدة (docs/22).
for (const state of states) {
state.demand_count = 0;
state.supply_count = 0;
}
if (states.length > 0) {
await this.repo.save(states);
}
return { updated, unchanged };
}
/**
* حساب المضاعف من نسبة الطلب/العرض.
*
* ```
* ratio = demand / max(supply, 1)
* multiplier = 1 + (ratio - 1) × sensitivity
* ```
*
* - لا عرض (0 سائقين) → ratio = max → multiplier = max_multiplier
* - عرض ≥ طلب → multiplier = 1.0 (لا زيادة)
*/
calculateMultiplier(
demand: number,
supply: number,
min: number,
max: number,
sensitivity: number,
): number {
if (demand <= 0) return 1.0;
const effectiveSupply = Math.max(supply, 1);
const ratio = demand / effectiveSupply;
// لا زيادة لو العرض كافٍ أو أكبر
if (ratio <= 1.0) return 1.0;
const raw = 1 + (ratio - 1) * sensitivity;
const clamped = Math.min(max, Math.max(min, raw));
// تقريب لخانتين عشريتين
return Math.round(clamped * 100) / 100;
}
// ─── تحديث البيانات (من بوتات أخرى أو API) ────────────────────────
/** تحديث عدّاد الطلب (يُستدعى عند إلغاء/رفض رحلة). */
async incrementDemand(tenantId: string, city: string, serviceClass: string): Promise<void> {
const state = await this.getOrCreate(tenantId, city, serviceClass);
state.demand_count++;
state.last_sample_at = new Date();
await this.repo.save(state);
}
/** تحديث عدّاد العرض (يُستدعى عند تحديث حالة السائق). */
async updateSupply(tenantId: string, city: string, serviceClass: string, count: number): Promise<void> {
const state = await this.getOrCreate(tenantId, city, serviceClass);
state.supply_count = count;
state.last_sample_at = new Date();
await this.repo.save(state);
}
/** إعادة ضبط العدّادات بعد كل دورة حساب. */
async resetCounters(tenantId: string, city: string, serviceClass: string): Promise<void> {
const state = await this.getOrCreate(tenantId, city, serviceClass);
state.demand_count = 0;
state.supply_count = 0;
state.last_sample_at = new Date();
await this.repo.save(state);
}
// ─── التحكم اليدوي (Admin) ──────────────────────────────────────
/** تجاوز يدوي للمضاعف (مثلاً: حدث أو زحمة غير عادية). */
async manualOverride(
tenantId: string,
city: string,
serviceClass: string,
multiplier: number,
note?: string,
): Promise<SurgeState> {
const state = await this.getOrCreate(tenantId, city, serviceClass);
state.multiplier = multiplier;
state.manual_override = true;
state.manual_note = note ?? null;
state.last_changed_at = new Date();
await this.repo.save(state);
await this.tariffService.updateSurgeMultiplier(
tenantId, city, serviceClass, multiplier,
);
this.logger.log(`manual override: ${city}/${serviceClass} → ${multiplier}x (${note ?? 'no note'})`);
return state;
}
/** إلغاء التحكم اليدوي — يعود البوت يحسب تلقائياً. */
async releaseManualOverride(tenantId: string, city: string, serviceClass: string): Promise<void> {
const state = await this.getOrCreate(tenantId, city, serviceClass);
state.manual_override = false;
state.manual_note = null;
await this.repo.save(state);
}
// ─── الاستعلام ────────────────────────────────────────────────
/** حالة البوت لفئة معينة. */
async getState(tenantId: string, city: string, serviceClass: string): Promise<SurgeState | null> {
return this.repo.findOne({ where: { tenant_id: tenantId, city, service_class: serviceClass } });
}
/** كل الحالات النشطة لمستأجر. */
async listStates(tenantId: string): Promise<SurgeState[]> {
return this.repo.find({ where: { tenant_id: tenantId }, order: { city: 'ASC', service_class: 'ASC' } });
}
// ─── داخلي ────────────────────────────────────────────────────
/**
* تحديث عدد السائقين المتاحين لكل حالة surge من قاعدة البيانات الفعلية
* بدلاً من الاعتماد على عداد ثابت. يُستدعى قبل كل إعادة حساب.
*/
private async refreshSupplyFromDrivers(now: Date): Promise<void> {
const states = await this.repo.find({ where: { manual_override: false } });
for (const state of states) {
const count = await this.driverRepo
.createQueryBuilder('d')
.where('d.tenant_id = :tenantId', { tenantId: state.tenant_id })
.andWhere('d.is_online = true')
.andWhere('d.verification_status = :vs', { vs: 'approved' })
.andWhere('d.service_class = :sc', { sc: state.service_class })
.getCount();
state.supply_count = count;
state.last_sample_at = now;
await this.repo.save(state);
}
}
private async getOrCreate(tenantId: string, city: string, serviceClass: string): Promise<SurgeState> {
let state = await this.repo.findOne({ where: { tenant_id: tenantId, city, service_class: serviceClass } });
if (!state) {
state = this.repo.create({
tenant_id: tenantId,
city,
service_class: serviceClass,
multiplier: 1.0,
max_multiplier: 2.0,
min_multiplier: 1.0,
});
await this.repo.save(state);
}
return state;
}
}