Update backend to use firebase-admin SDK for FCM
This commit is contained in:
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user