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
@@ -70,4 +70,75 @@ export class TenantWalletController {
body.currency || 'JOD',
);
}
// ─── تقارير المستأجر (P4) ───────────────────────────────────────
/** إيراد حسب مزوّد الدفع (cash / cliq / syriatel / mtn / ...). */
@Get('report/by-provider')
reportByProvider(
@CurrentUser() user: AuthUser,
@Query('days') days?: string,
) {
const n = Number(days);
return this.wallet.revenueByProvider(
user.tenantId,
Number.isFinite(n) && n > 0 ? Math.min(n, 365) : 30,
);
}
/** إيراد يومي — سلسلة زمنية. */
@Get('report/by-day')
reportByDay(
@CurrentUser() user: AuthUser,
@Query('days') days?: string,
@Query('currency') currency?: string,
) {
const n = Number(days);
return this.wallet.revenueByDay(
user.tenantId,
Number.isFinite(n) && n > 0 ? Math.min(n, 365) : 30,
currency || 'JOD',
);
}
/** ملخّص الرحلات المالي (GMV + عمولة + خصومات). */
@Get('report/trips')
reportTrips(
@CurrentUser() user: AuthUser,
@Query('days') days?: string,
) {
const n = Number(days);
return this.wallet.tripRevenueSummary(
user.tenantId,
Number.isFinite(n) && n > 0 ? Math.min(n, 365) : 30,
);
}
/** ملخّص سحوبات السائقين. */
@Get('report/payouts')
reportPayouts(
@CurrentUser() user: AuthUser,
@Query('days') days?: string,
) {
const n = Number(days);
return this.wallet.payoutSummary(
user.tenantId,
Number.isFinite(n) && n > 0 ? Math.min(n, 365) : 30,
);
}
/** التقرير الشامل — يجمع كل شيء في طلب واحد. */
@Get('report/full')
reportFull(
@CurrentUser() user: AuthUser,
@Query('days') days?: string,
@Query('currency') currency?: string,
) {
const n = Number(days);
return this.wallet.fullReport(
user.tenantId,
Number.isFinite(n) && n > 0 ? Math.min(n, 365) : 30,
currency || 'JOD',
);
}
}
@@ -4,11 +4,22 @@ import {
TenantRevenueEntry,
TenantPendingEntry,
} from './entities/tenant-ledger.entity';
import { Payment } from '../payments/entities/payment.entity';
import { Payout } from '../payments/entities/payout.entity';
import { Trip } from '../trips/entities/trip.entity';
import { TenantWalletService } from './tenant-wallet.service';
import { TenantWalletController } from './tenant-wallet.controller';
@Module({
imports: [TypeOrmModule.forFeature([TenantRevenueEntry, TenantPendingEntry])],
imports: [
TypeOrmModule.forFeature([
TenantRevenueEntry,
TenantPendingEntry,
Payment,
Payout,
Trip,
]),
],
controllers: [TenantWalletController],
providers: [TenantWalletService],
exports: [TenantWalletService],
@@ -7,6 +7,9 @@ import {
TenantPendingEntry,
LedgerReason,
} from './entities/tenant-ledger.entity';
import { Payment } from '../payments/entities/payment.entity';
import { Payout } from '../payments/entities/payout.entity';
import { Trip } from '../trips/entities/trip.entity';
import { TenantWalletService } from './tenant-wallet.service';
const TENANT = '11111111-1111-1111-1111-111111111111';
@@ -49,6 +52,9 @@ describe('TenantWalletService — محفظتا المستأجر (docs/24)', () =
wallet = new TenantWalletService(
ds.getRepository(TenantRevenueEntry),
ds.getRepository(TenantPendingEntry),
{} as any, // Payment repo (not used in these tests)
{} as any, // Payout repo (not used in these tests)
{} as any, // Trip repo (not used in these tests)
);
});
@@ -7,6 +7,9 @@ import {
TenantLedgerBase,
LedgerReason,
} from './entities/tenant-ledger.entity';
import { Payment } from '../payments/entities/payment.entity';
import { Payout } from '../payments/entities/payout.entity';
import { Trip } from '../trips/entities/trip.entity';
/** أي الدفترين — يُمرَّر صراحةً في كل نداء فلا يوجد افتراض صامت. */
export type Book = 'revenue' | 'pending';
@@ -36,6 +39,12 @@ export class TenantWalletService {
private readonly revenue: Repository<TenantRevenueEntry>,
@InjectRepository(TenantPendingEntry)
private readonly pending: Repository<TenantPendingEntry>,
@InjectRepository(Payment)
private readonly payments: Repository<Payment>,
@InjectRepository(Payout)
private readonly payouts: Repository<Payout>,
@InjectRepository(Trip)
private readonly trips: Repository<Trip>,
) {}
/**
@@ -192,4 +201,138 @@ export class TenantWalletService {
}
static readonly Reason = LedgerReason;
// ─── تقارير المستأجر (P4) ─────────────────────────────────────────────
/**
* إيراد حسب مزوّد الدفع — يجيب «بأي وسيلة دفع دفع الراكب/السائق؟».
* يقرأ من `pay_payments` (المدفوعات الناجحة فقط).
*/
async revenueByProvider(tenantId: string, days = 30) {
const since = new Date(Date.now() - days * 86400_000);
const rows = await this.payments
.createQueryBuilder('p')
.select('p.provider', 'provider')
.addSelect('p.purpose', 'purpose')
.addSelect('COUNT(*)', 'count')
.addSelect('COALESCE(SUM(p.amount), 0)', 'total_amount')
.where('p.tenant_id = :tenantId AND p.status = :status', { tenantId, status: 'success' })
.andWhere('p.created_at >= :since', { since })
.groupBy('p.provider')
.addGroupBy('p.purpose')
.orderBy('total_amount', 'DESC')
.getRawMany();
return rows.map((r: any) => ({
provider: r.provider,
purpose: r.purpose,
count: Number(r.count),
total_amount: Number(r.total_amount),
}));
}
/**
* إيراد يومي — سلسلة زمنية للإيراد.
* يقرأ من دفتر الإيرادات (العمولات + شحن السائقين + رسوم المعاملات).
*/
async revenueByDay(tenantId: string, days = 30, currency = 'JOD') {
const since = new Date(Date.now() - days * 86400_000);
const rows = await this.revenue
.createQueryBuilder('l')
.select("DATE_TRUNC('day', l.created_at)", 'date')
.addSelect('COUNT(*)', 'count')
.addSelect('COALESCE(SUM(l.amount), 0)', 'total')
.where('l.tenant_id = :tenantId AND l.currency = :currency', { tenantId, currency })
.andWhere('l.created_at >= :since', { since })
.groupBy("DATE_TRUNC('day', l.created_at)")
.orderBy("DATE_TRUNC('day', l.created_at)", 'ASC')
.getRawMany();
return rows.map((r: any) => ({
date: r.date,
count: Number(r.count),
total: Number(r.total),
}));
}
/**
* ملخّص الرحلات المالي — GMV + عمولة + خصومات + كوبونات.
* يقرأ من جدول الرحلات المكتملة والمدفوعة.
*/
async tripRevenueSummary(tenantId: string, days = 30) {
const since = new Date(Date.now() - days * 86400_000);
const row = await this.trips
.createQueryBuilder('t')
.select('COUNT(*)', 'total_trips')
.addSelect("COUNT(*) FILTER (WHERE t.status = 'completed')", 'completed_trips')
.addSelect("COALESCE(SUM(t.final_fare) FILTER (WHERE t.status = 'completed'), 0)", 'gmv')
.addSelect("COALESCE(SUM(t.commission_amount) FILTER (WHERE t.status = 'completed'), 0)", 'commission')
.addSelect("COALESCE(SUM(t.coupon_discount) FILTER (WHERE t.status = 'completed'), 0)", 'coupon_discount')
.addSelect("COALESCE(SUM(t.destination_match_discount) FILTER (WHERE t.status = 'completed'), 0)", 'destination_discount')
.addSelect("COALESCE(SUM(t.cancel_fee) FILTER (WHERE t.status IN ('cancelled','expired') AND t.cancel_fee > 0), 0)", 'cancel_fees')
.where('t.tenant_id = :tenantId', { tenantId })
.andWhere('t.created_at >= :since', { since })
.getRawOne();
return {
total_trips: Number(row?.total_trips ?? 0),
completed_trips: Number(row?.completed_trips ?? 0),
gmv: Number(row?.gmv ?? 0),
commission: Number(row?.commission ?? 0),
coupon_discount: Number(row?.coupon_discount ?? 0),
destination_discount: Number(row?.destination_discount ?? 0),
cancel_fees: Number(row?.cancel_fees ?? 0),
net_revenue: Number(row?.commission ?? 0) - Number(row?.coupon_discount ?? 0),
};
}
/**
* ملخّص سحوبات السائقين — pending / processing / paid / failed.
*/
async payoutSummary(tenantId: string, days = 30) {
const since = new Date(Date.now() - days * 86400_000);
const rows = await this.payouts
.createQueryBuilder('p')
.select('p.status', 'status')
.addSelect('COUNT(*)', 'count')
.addSelect('COALESCE(SUM(p.amount), 0)', 'total_amount')
.where('p.tenant_id = :tenantId', { tenantId })
.andWhere('p.created_at >= :since', { since })
.groupBy('p.status')
.getRawMany();
const summary: Record<string, { count: number; total_amount: number }> = {};
for (const r of rows) {
summary[r.status] = {
count: Number(r.count),
total_amount: Number(r.total_amount),
};
}
return summary;
}
/**
* التقرير الشامل (P4) — يجمع كل شيء في طلب واحد.
*/
async fullReport(tenantId: string, days = 30, currency = 'JOD') {
const [summary, byReason, byProvider, byDay, tripSummary, payoutSummary] = await Promise.all([
this.summary(tenantId, currency),
this.revenueByReason(tenantId, days, currency),
this.revenueByProvider(tenantId, days),
this.revenueByDay(tenantId, days, currency),
this.tripRevenueSummary(tenantId, days),
this.payoutSummary(tenantId, days),
]);
return {
period_days: days,
currency,
wallet: summary,
revenue_by_reason: byReason,
revenue_by_provider: byProvider,
revenue_by_day: byDay,
trips: tripSummary,
payouts: payoutSummary,
};
}
}