mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-06 04:13:37 +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 {
|
export class OverviewBillingKpisDto {
|
||||||
@ApiProperty() revenueMtdEtb!: number;
|
@ApiProperty() revenueMtdEtb!: number;
|
||||||
@ApiProperty() revenueMtdUsd!: number;
|
@ApiProperty() revenueMtdUsd!: number;
|
||||||
|
@ApiProperty() revenueMtdDjf!: number;
|
||||||
@ApiProperty() pendingPayments!: number;
|
@ApiProperty() pendingPayments!: number;
|
||||||
@ApiProperty() successfulPaymentsMtd!: number;
|
@ApiProperty() successfulPaymentsMtd!: number;
|
||||||
}
|
}
|
||||||
@@ -84,6 +85,7 @@ export class OverviewPaymentTrendPointDto {
|
|||||||
@ApiProperty({ example: '2026-06-01' }) date!: string;
|
@ApiProperty({ example: '2026-06-01' }) date!: string;
|
||||||
@ApiProperty() amountEtb!: number;
|
@ApiProperty() amountEtb!: number;
|
||||||
@ApiProperty() amountUsd!: number;
|
@ApiProperty() amountUsd!: number;
|
||||||
|
@ApiProperty() amountDjf!: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class OverviewRecentBookingDto {
|
export class OverviewRecentBookingDto {
|
||||||
@@ -113,6 +115,7 @@ export class OverviewPeriodTotalsDto {
|
|||||||
@ApiProperty() bookingsCreated!: number;
|
@ApiProperty() bookingsCreated!: number;
|
||||||
@ApiProperty() revenueEtb!: number;
|
@ApiProperty() revenueEtb!: number;
|
||||||
@ApiProperty() revenueUsd!: number;
|
@ApiProperty() revenueUsd!: number;
|
||||||
|
@ApiProperty() revenueDjf!: number;
|
||||||
@ApiProperty() tons!: number;
|
@ApiProperty() tons!: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,6 +123,7 @@ export class OverviewRevenueSliceDto {
|
|||||||
@ApiProperty() label!: string;
|
@ApiProperty() label!: string;
|
||||||
@ApiProperty() amountEtb!: number;
|
@ApiProperty() amountEtb!: number;
|
||||||
@ApiProperty() amountUsd!: number;
|
@ApiProperty() amountUsd!: number;
|
||||||
|
@ApiProperty() amountDjf!: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class OverviewTonsTrendPointDto {
|
export class OverviewTonsTrendPointDto {
|
||||||
@@ -132,6 +136,7 @@ export class OverviewRevenueFlowDto {
|
|||||||
@ApiProperty() freightType!: string;
|
@ApiProperty() freightType!: string;
|
||||||
@ApiProperty() amountEtb!: number;
|
@ApiProperty() amountEtb!: number;
|
||||||
@ApiProperty() amountUsd!: number;
|
@ApiProperty() amountUsd!: number;
|
||||||
|
@ApiProperty() amountDjf!: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class OverviewHeatmapCellDto {
|
export class OverviewHeatmapCellDto {
|
||||||
|
|||||||
@@ -265,6 +265,7 @@ export class OverviewRepository {
|
|||||||
async getBillingKpis(dirs?: string[]): Promise<{
|
async getBillingKpis(dirs?: string[]): Promise<{
|
||||||
revenueMtdEtb: number;
|
revenueMtdEtb: number;
|
||||||
revenueMtdUsd: number;
|
revenueMtdUsd: number;
|
||||||
|
revenueMtdDjf: number;
|
||||||
pendingPayments: number;
|
pendingPayments: number;
|
||||||
successfulPaymentsMtd: number;
|
successfulPaymentsMtd: number;
|
||||||
}> {
|
}> {
|
||||||
@@ -279,6 +280,10 @@ export class OverviewRepository {
|
|||||||
`COALESCE(SUM(payment.amount) FILTER (WHERE payment.currency = 'USD'), 0)`,
|
`COALESCE(SUM(payment.amount) FILTER (WHERE payment.currency = 'USD'), 0)`,
|
||||||
"revenueMtdUsd",
|
"revenueMtdUsd",
|
||||||
)
|
)
|
||||||
|
.addSelect(
|
||||||
|
`COALESCE(SUM(payment.amount) FILTER (WHERE payment.currency = 'DJF'), 0)`,
|
||||||
|
"revenueMtdDjf",
|
||||||
|
)
|
||||||
.addSelect(`COUNT(*)::int`, "successfulPaymentsMtd")
|
.addSelect(`COUNT(*)::int`, "successfulPaymentsMtd")
|
||||||
.where("payment.status = :status", { status: "success" })
|
.where("payment.status = :status", { status: "success" })
|
||||||
.andWhere(
|
.andWhere(
|
||||||
@@ -298,6 +303,7 @@ export class OverviewRepository {
|
|||||||
return {
|
return {
|
||||||
revenueMtdEtb: Number(revenueRow?.revenueMtdEtb ?? 0),
|
revenueMtdEtb: Number(revenueRow?.revenueMtdEtb ?? 0),
|
||||||
revenueMtdUsd: Number(revenueRow?.revenueMtdUsd ?? 0),
|
revenueMtdUsd: Number(revenueRow?.revenueMtdUsd ?? 0),
|
||||||
|
revenueMtdDjf: Number(revenueRow?.revenueMtdDjf ?? 0),
|
||||||
pendingPayments,
|
pendingPayments,
|
||||||
successfulPaymentsMtd: Number(revenueRow?.successfulPaymentsMtd ?? 0),
|
successfulPaymentsMtd: Number(revenueRow?.successfulPaymentsMtd ?? 0),
|
||||||
};
|
};
|
||||||
@@ -370,7 +376,7 @@ export class OverviewRepository {
|
|||||||
days: number,
|
days: number,
|
||||||
dirs?: string[],
|
dirs?: string[],
|
||||||
offsetDays = 0,
|
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 scope = bookingRefScopeSql("payment.ref_id", dirs);
|
||||||
const rows = await this.paymentRepository
|
const rows = await this.paymentRepository
|
||||||
.createQueryBuilder("payment")
|
.createQueryBuilder("payment")
|
||||||
@@ -386,6 +392,10 @@ export class OverviewRepository {
|
|||||||
`COALESCE(SUM(payment.amount) FILTER (WHERE payment.currency = 'USD'), 0)`,
|
`COALESCE(SUM(payment.amount) FILTER (WHERE payment.currency = 'USD'), 0)`,
|
||||||
"amountUsd",
|
"amountUsd",
|
||||||
)
|
)
|
||||||
|
.addSelect(
|
||||||
|
`COALESCE(SUM(payment.amount) FILTER (WHERE payment.currency = 'DJF'), 0)`,
|
||||||
|
"amountDjf",
|
||||||
|
)
|
||||||
.where("payment.status = :status", { status: "success" })
|
.where("payment.status = :status", { status: "success" })
|
||||||
.andWhere(
|
.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`,
|
`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)
|
.andWhere(scope.sql, scope.params)
|
||||||
.groupBy(`COALESCE(payment.paid_at, payment.created_at)::date`)
|
.groupBy(`COALESCE(payment.paid_at, payment.created_at)::date`)
|
||||||
.orderBy(`COALESCE(payment.paid_at, payment.created_at)::date`, "ASC")
|
.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) => ({
|
return rows.map((row) => ({
|
||||||
date: row.date,
|
date: row.date,
|
||||||
amountEtb: Number(row.amountEtb),
|
amountEtb: Number(row.amountEtb),
|
||||||
amountUsd: Number(row.amountUsd),
|
amountUsd: Number(row.amountUsd),
|
||||||
|
amountDjf: Number(row.amountDjf),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -510,7 +521,7 @@ export class OverviewRepository {
|
|||||||
async getPaymentsByMethod(
|
async getPaymentsByMethod(
|
||||||
dirs?: string[],
|
dirs?: string[],
|
||||||
): Promise<
|
): 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 scope = bookingRefScopeSql("payment.ref_id", dirs);
|
||||||
const rows = await this.paymentRepository
|
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)`,
|
`COALESCE(SUM(payment.amount) FILTER (WHERE payment.currency = 'USD' AND payment.status = 'success'), 0)`,
|
||||||
"amountUsd",
|
"amountUsd",
|
||||||
)
|
)
|
||||||
|
.addSelect(
|
||||||
|
`COALESCE(SUM(payment.amount) FILTER (WHERE payment.currency = 'DJF' AND payment.status = 'success'), 0)`,
|
||||||
|
"amountDjf",
|
||||||
|
)
|
||||||
.where(scope.sql, scope.params)
|
.where(scope.sql, scope.params)
|
||||||
.groupBy("payment.method")
|
.groupBy("payment.method")
|
||||||
.orderBy("count", "DESC")
|
.orderBy("count", "DESC")
|
||||||
@@ -533,6 +548,7 @@ export class OverviewRepository {
|
|||||||
count: string;
|
count: string;
|
||||||
amountEtb: string;
|
amountEtb: string;
|
||||||
amountUsd: string;
|
amountUsd: string;
|
||||||
|
amountDjf: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
return rows.map((row) => ({
|
return rows.map((row) => ({
|
||||||
@@ -540,6 +556,7 @@ export class OverviewRepository {
|
|||||||
count: Number(row.count),
|
count: Number(row.count),
|
||||||
amountEtb: Number(row.amountEtb),
|
amountEtb: Number(row.amountEtb),
|
||||||
amountUsd: Number(row.amountUsd),
|
amountUsd: Number(row.amountUsd),
|
||||||
|
amountDjf: Number(row.amountDjf),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -580,6 +597,7 @@ export class OverviewRepository {
|
|||||||
bookingsCreated: number;
|
bookingsCreated: number;
|
||||||
revenueEtb: number;
|
revenueEtb: number;
|
||||||
revenueUsd: number;
|
revenueUsd: number;
|
||||||
|
revenueDjf: number;
|
||||||
tons: number;
|
tons: number;
|
||||||
}> {
|
}> {
|
||||||
const bookingScope = directionScopeSql("booking.trade_direction", dirs);
|
const bookingScope = directionScopeSql("booking.trade_direction", dirs);
|
||||||
@@ -605,13 +623,17 @@ export class OverviewRepository {
|
|||||||
`COALESCE(SUM(payment.amount) FILTER (WHERE payment.currency = 'USD'), 0)`,
|
`COALESCE(SUM(payment.amount) FILTER (WHERE payment.currency = 'USD'), 0)`,
|
||||||
"revenueUsd",
|
"revenueUsd",
|
||||||
)
|
)
|
||||||
|
.addSelect(
|
||||||
|
`COALESCE(SUM(payment.amount) FILTER (WHERE payment.currency = 'DJF'), 0)`,
|
||||||
|
"revenueDjf",
|
||||||
|
)
|
||||||
.where("payment.status = :status", { status: "success" })
|
.where("payment.status = :status", { status: "success" })
|
||||||
.andWhere(
|
.andWhere(
|
||||||
windowSql("COALESCE(payment.paid_at, payment.created_at)"),
|
windowSql("COALESCE(payment.paid_at, payment.created_at)"),
|
||||||
{ days, offsetDays },
|
{ days, offsetDays },
|
||||||
)
|
)
|
||||||
.andWhere(paymentScope.sql, paymentScope.params)
|
.andWhere(paymentScope.sql, paymentScope.params)
|
||||||
.getRawOne<{ revenueEtb: string; revenueUsd: string }>(),
|
.getRawOne<{ revenueEtb: string; revenueUsd: string; revenueDjf: string }>(),
|
||||||
this.cargoRepository
|
this.cargoRepository
|
||||||
.createQueryBuilder("cargo")
|
.createQueryBuilder("cargo")
|
||||||
.leftJoin(Booking, "booking", "booking.id = cargo.booking_id")
|
.leftJoin(Booking, "booking", "booking.id = cargo.booking_id")
|
||||||
@@ -626,6 +648,7 @@ export class OverviewRepository {
|
|||||||
bookingsCreated,
|
bookingsCreated,
|
||||||
revenueEtb: Number(revenueRow?.revenueEtb ?? 0),
|
revenueEtb: Number(revenueRow?.revenueEtb ?? 0),
|
||||||
revenueUsd: Number(revenueRow?.revenueUsd ?? 0),
|
revenueUsd: Number(revenueRow?.revenueUsd ?? 0),
|
||||||
|
revenueDjf: Number(revenueRow?.revenueDjf ?? 0),
|
||||||
tons: Number(tonsRow?.tons ?? 0),
|
tons: Number(tonsRow?.tons ?? 0),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -634,7 +657,7 @@ export class OverviewRepository {
|
|||||||
async getRevenueByDirection(
|
async getRevenueByDirection(
|
||||||
days: number,
|
days: number,
|
||||||
dirs?: string[],
|
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 scope = bookingRefScopeSql("payment.ref_id", dirs);
|
||||||
const rows = await this.paymentRepository
|
const rows = await this.paymentRepository
|
||||||
.createQueryBuilder("payment")
|
.createQueryBuilder("payment")
|
||||||
@@ -648,6 +671,10 @@ export class OverviewRepository {
|
|||||||
`COALESCE(SUM(payment.amount) FILTER (WHERE payment.currency = 'USD'), 0)`,
|
`COALESCE(SUM(payment.amount) FILTER (WHERE payment.currency = 'USD'), 0)`,
|
||||||
"amountUsd",
|
"amountUsd",
|
||||||
)
|
)
|
||||||
|
.addSelect(
|
||||||
|
`COALESCE(SUM(payment.amount) FILTER (WHERE payment.currency = 'DJF'), 0)`,
|
||||||
|
"amountDjf",
|
||||||
|
)
|
||||||
.where("payment.status = :status", { status: "success" })
|
.where("payment.status = :status", { status: "success" })
|
||||||
.andWhere(
|
.andWhere(
|
||||||
`COALESCE(payment.paid_at, payment.created_at) >= CURRENT_DATE - :days::int + 1`,
|
`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(scope.sql, scope.params)
|
||||||
.andWhere("booking.trade_direction IS NOT NULL")
|
.andWhere("booking.trade_direction IS NOT NULL")
|
||||||
.groupBy("booking.trade_direction")
|
.groupBy("booking.trade_direction")
|
||||||
.getRawMany<{ label: string; amountEtb: string; amountUsd: string }>();
|
.getRawMany<{ label: string; amountEtb: string; amountUsd: string; amountDjf: string }>();
|
||||||
|
|
||||||
return rows.map((row) => ({
|
return rows.map((row) => ({
|
||||||
label: row.label,
|
label: row.label,
|
||||||
amountEtb: Number(row.amountEtb),
|
amountEtb: Number(row.amountEtb),
|
||||||
amountUsd: Number(row.amountUsd),
|
amountUsd: Number(row.amountUsd),
|
||||||
|
amountDjf: Number(row.amountDjf),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -669,7 +697,7 @@ export class OverviewRepository {
|
|||||||
async getRevenueByFreightType(
|
async getRevenueByFreightType(
|
||||||
days: number,
|
days: number,
|
||||||
dirs?: string[],
|
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 scope = bookingRefScopeSql("payment.ref_id", dirs);
|
||||||
const rows = await this.paymentRepository
|
const rows = await this.paymentRepository
|
||||||
.createQueryBuilder("payment")
|
.createQueryBuilder("payment")
|
||||||
@@ -683,6 +711,10 @@ export class OverviewRepository {
|
|||||||
`COALESCE(SUM(payment.amount) FILTER (WHERE payment.currency = 'USD'), 0)`,
|
`COALESCE(SUM(payment.amount) FILTER (WHERE payment.currency = 'USD'), 0)`,
|
||||||
"amountUsd",
|
"amountUsd",
|
||||||
)
|
)
|
||||||
|
.addSelect(
|
||||||
|
`COALESCE(SUM(payment.amount) FILTER (WHERE payment.currency = 'DJF'), 0)`,
|
||||||
|
"amountDjf",
|
||||||
|
)
|
||||||
.where("payment.status = :status", { status: "success" })
|
.where("payment.status = :status", { status: "success" })
|
||||||
.andWhere(
|
.andWhere(
|
||||||
`COALESCE(payment.paid_at, payment.created_at) >= CURRENT_DATE - :days::int + 1`,
|
`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(scope.sql, scope.params)
|
||||||
.andWhere("booking.freight_type IS NOT NULL")
|
.andWhere("booking.freight_type IS NOT NULL")
|
||||||
.groupBy("booking.freight_type")
|
.groupBy("booking.freight_type")
|
||||||
.getRawMany<{ label: string; amountEtb: string; amountUsd: string }>();
|
.getRawMany<{ label: string; amountEtb: string; amountUsd: string; amountDjf: string }>();
|
||||||
|
|
||||||
return rows.map((row) => ({
|
return rows.map((row) => ({
|
||||||
label: row.label,
|
label: row.label,
|
||||||
amountEtb: Number(row.amountEtb),
|
amountEtb: Number(row.amountEtb),
|
||||||
amountUsd: Number(row.amountUsd),
|
amountUsd: Number(row.amountUsd),
|
||||||
|
amountDjf: Number(row.amountDjf),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -734,6 +767,7 @@ export class OverviewRepository {
|
|||||||
freightType: string;
|
freightType: string;
|
||||||
amountEtb: number;
|
amountEtb: number;
|
||||||
amountUsd: number;
|
amountUsd: number;
|
||||||
|
amountDjf: number;
|
||||||
}[]
|
}[]
|
||||||
> {
|
> {
|
||||||
const scope = bookingRefScopeSql("payment.ref_id", dirs);
|
const scope = bookingRefScopeSql("payment.ref_id", dirs);
|
||||||
@@ -750,6 +784,10 @@ export class OverviewRepository {
|
|||||||
`COALESCE(SUM(payment.amount) FILTER (WHERE payment.currency = 'USD'), 0)`,
|
`COALESCE(SUM(payment.amount) FILTER (WHERE payment.currency = 'USD'), 0)`,
|
||||||
"amountUsd",
|
"amountUsd",
|
||||||
)
|
)
|
||||||
|
.addSelect(
|
||||||
|
`COALESCE(SUM(payment.amount) FILTER (WHERE payment.currency = 'DJF'), 0)`,
|
||||||
|
"amountDjf",
|
||||||
|
)
|
||||||
.where("payment.status = :status", { status: "success" })
|
.where("payment.status = :status", { status: "success" })
|
||||||
.andWhere(
|
.andWhere(
|
||||||
`COALESCE(payment.paid_at, payment.created_at) >= CURRENT_DATE - :days::int + 1`,
|
`COALESCE(payment.paid_at, payment.created_at) >= CURRENT_DATE - :days::int + 1`,
|
||||||
@@ -765,6 +803,7 @@ export class OverviewRepository {
|
|||||||
freightType: string;
|
freightType: string;
|
||||||
amountEtb: string;
|
amountEtb: string;
|
||||||
amountUsd: string;
|
amountUsd: string;
|
||||||
|
amountDjf: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
return rows.map((row) => ({
|
return rows.map((row) => ({
|
||||||
@@ -772,6 +811,7 @@ export class OverviewRepository {
|
|||||||
freightType: row.freightType,
|
freightType: row.freightType,
|
||||||
amountEtb: Number(row.amountEtb),
|
amountEtb: Number(row.amountEtb),
|
||||||
amountUsd: Number(row.amountUsd),
|
amountUsd: Number(row.amountUsd),
|
||||||
|
amountDjf: Number(row.amountDjf),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user