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:
ghost2023
2026-09-04 11:52:48 +03:00
parent 3734ca3897
commit d8a12939ae
2 changed files with 53 additions and 8 deletions

View File

@@ -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 {

View File

@@ -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),
}));
}