mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-06 03:03:39 +00:00
feat(overview): add DJF revenue and payment KPIs
The billing KPI queries fan out per currency with a FILTER (WHERE payment.currency = '...') per column rather than a generic GROUP BY, so DJF needs an explicit third variant at each of the 7 query sites (revenueMtd*, amount*, revenue* pairs) plus the matching DTO fields. The already-generic queries (getBookingsByCurrency, getRevenueByCurrency, company-dashboard's sumPaidSpendByCurrency) needed no change — they GROUP BY currency and pick up DJF on their own. Claude-Session: https://claude.ai/code/session_01CZy77vCWhka3pnmVF9NDkL
This commit is contained in:
@@ -36,6 +36,7 @@ export class OverviewCustomerKpisDto {
|
||||
export class OverviewBillingKpisDto {
|
||||
@ApiProperty() revenueMtdEtb!: number;
|
||||
@ApiProperty() revenueMtdUsd!: number;
|
||||
@ApiProperty() revenueMtdDjf!: number;
|
||||
@ApiProperty() pendingPayments!: number;
|
||||
@ApiProperty() successfulPaymentsMtd!: number;
|
||||
}
|
||||
@@ -84,6 +85,7 @@ export class OverviewPaymentTrendPointDto {
|
||||
@ApiProperty({ example: '2026-06-01' }) date!: string;
|
||||
@ApiProperty() amountEtb!: number;
|
||||
@ApiProperty() amountUsd!: number;
|
||||
@ApiProperty() amountDjf!: number;
|
||||
}
|
||||
|
||||
export class OverviewRecentBookingDto {
|
||||
@@ -113,6 +115,7 @@ export class OverviewPeriodTotalsDto {
|
||||
@ApiProperty() bookingsCreated!: number;
|
||||
@ApiProperty() revenueEtb!: number;
|
||||
@ApiProperty() revenueUsd!: number;
|
||||
@ApiProperty() revenueDjf!: number;
|
||||
@ApiProperty() tons!: number;
|
||||
}
|
||||
|
||||
@@ -120,6 +123,7 @@ export class OverviewRevenueSliceDto {
|
||||
@ApiProperty() label!: string;
|
||||
@ApiProperty() amountEtb!: number;
|
||||
@ApiProperty() amountUsd!: number;
|
||||
@ApiProperty() amountDjf!: number;
|
||||
}
|
||||
|
||||
export class OverviewTonsTrendPointDto {
|
||||
@@ -132,6 +136,7 @@ export class OverviewRevenueFlowDto {
|
||||
@ApiProperty() freightType!: string;
|
||||
@ApiProperty() amountEtb!: number;
|
||||
@ApiProperty() amountUsd!: number;
|
||||
@ApiProperty() amountDjf!: number;
|
||||
}
|
||||
|
||||
export class OverviewHeatmapCellDto {
|
||||
|
||||
@@ -265,6 +265,7 @@ export class OverviewRepository {
|
||||
async getBillingKpis(dirs?: string[]): Promise<{
|
||||
revenueMtdEtb: number;
|
||||
revenueMtdUsd: number;
|
||||
revenueMtdDjf: number;
|
||||
pendingPayments: number;
|
||||
successfulPaymentsMtd: number;
|
||||
}> {
|
||||
@@ -279,6 +280,10 @@ export class OverviewRepository {
|
||||
`COALESCE(SUM(payment.amount) FILTER (WHERE payment.currency = 'USD'), 0)`,
|
||||
"revenueMtdUsd",
|
||||
)
|
||||
.addSelect(
|
||||
`COALESCE(SUM(payment.amount) FILTER (WHERE payment.currency = 'DJF'), 0)`,
|
||||
"revenueMtdDjf",
|
||||
)
|
||||
.addSelect(`COUNT(*)::int`, "successfulPaymentsMtd")
|
||||
.where("payment.status = :status", { status: "success" })
|
||||
.andWhere(
|
||||
@@ -298,6 +303,7 @@ export class OverviewRepository {
|
||||
return {
|
||||
revenueMtdEtb: Number(revenueRow?.revenueMtdEtb ?? 0),
|
||||
revenueMtdUsd: Number(revenueRow?.revenueMtdUsd ?? 0),
|
||||
revenueMtdDjf: Number(revenueRow?.revenueMtdDjf ?? 0),
|
||||
pendingPayments,
|
||||
successfulPaymentsMtd: Number(revenueRow?.successfulPaymentsMtd ?? 0),
|
||||
};
|
||||
@@ -370,7 +376,7 @@ export class OverviewRepository {
|
||||
days: number,
|
||||
dirs?: string[],
|
||||
offsetDays = 0,
|
||||
): Promise<{ date: string; amountEtb: number; amountUsd: number }[]> {
|
||||
): Promise<{ date: string; amountEtb: number; amountUsd: number; amountDjf: number }[]> {
|
||||
const scope = bookingRefScopeSql("payment.ref_id", dirs);
|
||||
const rows = await this.paymentRepository
|
||||
.createQueryBuilder("payment")
|
||||
@@ -386,6 +392,10 @@ export class OverviewRepository {
|
||||
`COALESCE(SUM(payment.amount) FILTER (WHERE payment.currency = 'USD'), 0)`,
|
||||
"amountUsd",
|
||||
)
|
||||
.addSelect(
|
||||
`COALESCE(SUM(payment.amount) FILTER (WHERE payment.currency = 'DJF'), 0)`,
|
||||
"amountDjf",
|
||||
)
|
||||
.where("payment.status = :status", { status: "success" })
|
||||
.andWhere(
|
||||
`COALESCE(payment.paid_at, payment.created_at) >= CURRENT_DATE - :offsetDays::int - :days::int + 1 AND COALESCE(payment.paid_at, payment.created_at) < CURRENT_DATE - :offsetDays::int + 1`,
|
||||
@@ -394,12 +404,13 @@ export class OverviewRepository {
|
||||
.andWhere(scope.sql, scope.params)
|
||||
.groupBy(`COALESCE(payment.paid_at, payment.created_at)::date`)
|
||||
.orderBy(`COALESCE(payment.paid_at, payment.created_at)::date`, "ASC")
|
||||
.getRawMany<{ date: string; amountEtb: string; amountUsd: string }>();
|
||||
.getRawMany<{ date: string; amountEtb: string; amountUsd: string; amountDjf: string }>();
|
||||
|
||||
return rows.map((row) => ({
|
||||
date: row.date,
|
||||
amountEtb: Number(row.amountEtb),
|
||||
amountUsd: Number(row.amountUsd),
|
||||
amountDjf: Number(row.amountDjf),
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -510,7 +521,7 @@ export class OverviewRepository {
|
||||
async getPaymentsByMethod(
|
||||
dirs?: string[],
|
||||
): Promise<
|
||||
{ method: string; count: number; amountEtb: number; amountUsd: number }[]
|
||||
{ method: string; count: number; amountEtb: number; amountUsd: number; amountDjf: number }[]
|
||||
> {
|
||||
const scope = bookingRefScopeSql("payment.ref_id", dirs);
|
||||
const rows = await this.paymentRepository
|
||||
@@ -525,6 +536,10 @@ export class OverviewRepository {
|
||||
`COALESCE(SUM(payment.amount) FILTER (WHERE payment.currency = 'USD' AND payment.status = 'success'), 0)`,
|
||||
"amountUsd",
|
||||
)
|
||||
.addSelect(
|
||||
`COALESCE(SUM(payment.amount) FILTER (WHERE payment.currency = 'DJF' AND payment.status = 'success'), 0)`,
|
||||
"amountDjf",
|
||||
)
|
||||
.where(scope.sql, scope.params)
|
||||
.groupBy("payment.method")
|
||||
.orderBy("count", "DESC")
|
||||
@@ -533,6 +548,7 @@ export class OverviewRepository {
|
||||
count: string;
|
||||
amountEtb: string;
|
||||
amountUsd: string;
|
||||
amountDjf: string;
|
||||
}>();
|
||||
|
||||
return rows.map((row) => ({
|
||||
@@ -540,6 +556,7 @@ export class OverviewRepository {
|
||||
count: Number(row.count),
|
||||
amountEtb: Number(row.amountEtb),
|
||||
amountUsd: Number(row.amountUsd),
|
||||
amountDjf: Number(row.amountDjf),
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -580,6 +597,7 @@ export class OverviewRepository {
|
||||
bookingsCreated: number;
|
||||
revenueEtb: number;
|
||||
revenueUsd: number;
|
||||
revenueDjf: number;
|
||||
tons: number;
|
||||
}> {
|
||||
const bookingScope = directionScopeSql("booking.trade_direction", dirs);
|
||||
@@ -605,13 +623,17 @@ export class OverviewRepository {
|
||||
`COALESCE(SUM(payment.amount) FILTER (WHERE payment.currency = 'USD'), 0)`,
|
||||
"revenueUsd",
|
||||
)
|
||||
.addSelect(
|
||||
`COALESCE(SUM(payment.amount) FILTER (WHERE payment.currency = 'DJF'), 0)`,
|
||||
"revenueDjf",
|
||||
)
|
||||
.where("payment.status = :status", { status: "success" })
|
||||
.andWhere(
|
||||
windowSql("COALESCE(payment.paid_at, payment.created_at)"),
|
||||
{ days, offsetDays },
|
||||
)
|
||||
.andWhere(paymentScope.sql, paymentScope.params)
|
||||
.getRawOne<{ revenueEtb: string; revenueUsd: string }>(),
|
||||
.getRawOne<{ revenueEtb: string; revenueUsd: string; revenueDjf: string }>(),
|
||||
this.cargoRepository
|
||||
.createQueryBuilder("cargo")
|
||||
.leftJoin(Booking, "booking", "booking.id = cargo.booking_id")
|
||||
@@ -626,6 +648,7 @@ export class OverviewRepository {
|
||||
bookingsCreated,
|
||||
revenueEtb: Number(revenueRow?.revenueEtb ?? 0),
|
||||
revenueUsd: Number(revenueRow?.revenueUsd ?? 0),
|
||||
revenueDjf: Number(revenueRow?.revenueDjf ?? 0),
|
||||
tons: Number(tonsRow?.tons ?? 0),
|
||||
};
|
||||
}
|
||||
@@ -634,7 +657,7 @@ export class OverviewRepository {
|
||||
async getRevenueByDirection(
|
||||
days: number,
|
||||
dirs?: string[],
|
||||
): Promise<{ label: string; amountEtb: number; amountUsd: number }[]> {
|
||||
): Promise<{ label: string; amountEtb: number; amountUsd: number; amountDjf: number }[]> {
|
||||
const scope = bookingRefScopeSql("payment.ref_id", dirs);
|
||||
const rows = await this.paymentRepository
|
||||
.createQueryBuilder("payment")
|
||||
@@ -648,6 +671,10 @@ export class OverviewRepository {
|
||||
`COALESCE(SUM(payment.amount) FILTER (WHERE payment.currency = 'USD'), 0)`,
|
||||
"amountUsd",
|
||||
)
|
||||
.addSelect(
|
||||
`COALESCE(SUM(payment.amount) FILTER (WHERE payment.currency = 'DJF'), 0)`,
|
||||
"amountDjf",
|
||||
)
|
||||
.where("payment.status = :status", { status: "success" })
|
||||
.andWhere(
|
||||
`COALESCE(payment.paid_at, payment.created_at) >= CURRENT_DATE - :days::int + 1`,
|
||||
@@ -656,12 +683,13 @@ export class OverviewRepository {
|
||||
.andWhere(scope.sql, scope.params)
|
||||
.andWhere("booking.trade_direction IS NOT NULL")
|
||||
.groupBy("booking.trade_direction")
|
||||
.getRawMany<{ label: string; amountEtb: string; amountUsd: string }>();
|
||||
.getRawMany<{ label: string; amountEtb: string; amountUsd: string; amountDjf: string }>();
|
||||
|
||||
return rows.map((row) => ({
|
||||
label: row.label,
|
||||
amountEtb: Number(row.amountEtb),
|
||||
amountUsd: Number(row.amountUsd),
|
||||
amountDjf: Number(row.amountDjf),
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -669,7 +697,7 @@ export class OverviewRepository {
|
||||
async getRevenueByFreightType(
|
||||
days: number,
|
||||
dirs?: string[],
|
||||
): Promise<{ label: string; amountEtb: number; amountUsd: number }[]> {
|
||||
): Promise<{ label: string; amountEtb: number; amountUsd: number; amountDjf: number }[]> {
|
||||
const scope = bookingRefScopeSql("payment.ref_id", dirs);
|
||||
const rows = await this.paymentRepository
|
||||
.createQueryBuilder("payment")
|
||||
@@ -683,6 +711,10 @@ export class OverviewRepository {
|
||||
`COALESCE(SUM(payment.amount) FILTER (WHERE payment.currency = 'USD'), 0)`,
|
||||
"amountUsd",
|
||||
)
|
||||
.addSelect(
|
||||
`COALESCE(SUM(payment.amount) FILTER (WHERE payment.currency = 'DJF'), 0)`,
|
||||
"amountDjf",
|
||||
)
|
||||
.where("payment.status = :status", { status: "success" })
|
||||
.andWhere(
|
||||
`COALESCE(payment.paid_at, payment.created_at) >= CURRENT_DATE - :days::int + 1`,
|
||||
@@ -691,12 +723,13 @@ export class OverviewRepository {
|
||||
.andWhere(scope.sql, scope.params)
|
||||
.andWhere("booking.freight_type IS NOT NULL")
|
||||
.groupBy("booking.freight_type")
|
||||
.getRawMany<{ label: string; amountEtb: string; amountUsd: string }>();
|
||||
.getRawMany<{ label: string; amountEtb: string; amountUsd: string; amountDjf: string }>();
|
||||
|
||||
return rows.map((row) => ({
|
||||
label: row.label,
|
||||
amountEtb: Number(row.amountEtb),
|
||||
amountUsd: Number(row.amountUsd),
|
||||
amountDjf: Number(row.amountDjf),
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -734,6 +767,7 @@ export class OverviewRepository {
|
||||
freightType: string;
|
||||
amountEtb: number;
|
||||
amountUsd: number;
|
||||
amountDjf: number;
|
||||
}[]
|
||||
> {
|
||||
const scope = bookingRefScopeSql("payment.ref_id", dirs);
|
||||
@@ -750,6 +784,10 @@ export class OverviewRepository {
|
||||
`COALESCE(SUM(payment.amount) FILTER (WHERE payment.currency = 'USD'), 0)`,
|
||||
"amountUsd",
|
||||
)
|
||||
.addSelect(
|
||||
`COALESCE(SUM(payment.amount) FILTER (WHERE payment.currency = 'DJF'), 0)`,
|
||||
"amountDjf",
|
||||
)
|
||||
.where("payment.status = :status", { status: "success" })
|
||||
.andWhere(
|
||||
`COALESCE(payment.paid_at, payment.created_at) >= CURRENT_DATE - :days::int + 1`,
|
||||
@@ -765,6 +803,7 @@ export class OverviewRepository {
|
||||
freightType: string;
|
||||
amountEtb: string;
|
||||
amountUsd: string;
|
||||
amountDjf: string;
|
||||
}>();
|
||||
|
||||
return rows.map((row) => ({
|
||||
@@ -772,6 +811,7 @@ export class OverviewRepository {
|
||||
freightType: row.freightType,
|
||||
amountEtb: Number(row.amountEtb),
|
||||
amountUsd: Number(row.amountUsd),
|
||||
amountDjf: Number(row.amountDjf),
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user