Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ commands:
checkout-suite:
steps:
- run:
name: Verify deployment suite v2.0.6
name: Verify deployment suite v2.0.10
command: |
set -euo pipefail
git init --quiet /tmp/tc-deploy-scripts
git -C /tmp/tc-deploy-scripts remote add origin https://github.com/topcoder-platform/tc-deploy-scripts.git
git -C /tmp/tc-deploy-scripts fetch --quiet --depth 1 origin refs/tags/v2.0.6
git -C /tmp/tc-deploy-scripts fetch --quiet --depth 1 origin refs/tags/v2.0.10
git -C /tmp/tc-deploy-scripts checkout --quiet --detach FETCH_HEAD
test "$(git -C /tmp/tc-deploy-scripts rev-parse HEAD)" = 26aaf607463d4a271c0099854c1731214215a9c5
test "$(git -C /tmp/tc-deploy-scripts rev-parse HEAD)" = e81a6772a33819c1b57c88a95d04494e59c559b8
jobs:
build:
executor: reviewed
Expand Down
13 changes: 13 additions & 0 deletions SALES.md
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,19 @@ and request parameters remain compatible; no database migration or new configura
empty matrices. Missing source columns omit this optional map; the UI reports unavailable data.
`customerRevenue` retains all categories, including unknowns, and revenue charts remain unchanged.

- PM-6740 adds optional `customerRevenueByOfferingType`, keyed by `AI Training`,
`Crowd Powered Outcomes`, `Crowd Powered Innovation`, `Flexible Talent`, `TopGear`
and `TaaS Ops`. Each value contains `customerRevenue` and, when Account Category
is available, `customerRevenueByAccountCategory` with the same structure as the
top-level datasets. Offering matching ignores case and hyphen/space differences;
`TopGear` includes `TopGear Ops`. The existing blank-offering corrections are
applied before filtering: TopGear → TopGear Ops, Multiple App Enhancement(s) 5 →
Flexible Talent. Populated offerings remain authoritative. Rows are filtered
before customer aggregation, retaining credits, all customers and zero-filled
months. Missing offering/customer columns omit the map; valid selections without
rows return empty matrices. ALL uses the unchanged top-level datasets, retaining
unknown/other offerings. Chart breakdown selection does not change these scopes.

Tests: `pnpm test --runInBand sales-dashboard`. To include PostgreSQL aggregation tests, point
`SALES_DASHBOARD_TEST_DB_URL` at a disposable database named `sales_dashboard_test`; the suite
creates and removes its own test schemas. Tests exercise duplicate payments, members recurring
Expand Down
6 changes: 3 additions & 3 deletions deploy/MIGRATION.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# OIDC deployment migration

This change replaces the Auth0 credential broker and SSM-to-shell deployment path with [tc-deploy-scripts v2.0.6](https://github.com/topcoder-platform/tc-deploy-scripts/releases/tag/v2.0.6), fetched by tag and verified against commit `26aaf607463d4a271c0099854c1731214215a9c5`. Build and deployment run in separate jobs. The deployer cannot read Parameter Store, Secrets Manager or decrypt KMS data, including through another assumed role. ECS continues resolving the existing runtime secret references using the execution role.
This change replaces the Auth0 credential broker and SSM-to-shell deployment path with [tc-deploy-scripts v2.0.10](https://github.com/topcoder-platform/tc-deploy-scripts/releases/tag/v2.0.10), fetched by tag and verified against commit `e81a6772a33819c1b57c88a95d04494e59c559b8`. Build and deployment run in separate jobs. The deployer cannot read Parameter Store, Secrets Manager or decrypt KMS data, including through another assumed role. ECS continues resolving the existing runtime secret references using the execution role.

## Production target

Expand Down Expand Up @@ -28,10 +28,10 @@ IAM templates and detailed AWS baselines are maintained in the private [v6-servi

## Dev branch refresh

This PR targets `develop` and preserves its current application/dependency fixes. The shared suite is v2.0.6 (92 offline tests); service-specific adapters and their tests remain part of review. Use the current readiness record above for the remaining merge/deployment gates. Updating these files does not provision AWS or CircleCI resources.
This PR targets `develop` and preserves its current application/dependency fixes. The shared suite is v2.0.10 (92 offline tests); service-specific adapters and their tests remain part of review. Use the current readiness record above for the remaining merge/deployment gates. Updating these files does not provision AWS or CircleCI resources.

Source checkout uses the public HTTPS repository and verifies the exact CircleCI commit, avoiding dependency on failing SSH checkout keys. No GitHub token is added to build or deployment contexts.

## Reviewed health-check bootstrap

The release wrapper passes the environment-specific health-check JSON to v2.0.6. A missing check is added with the new scanned image; the old image is not restarted with a new probe. An identical existing check is preserved, and a different existing check stops the release. Role, runtime-secret reference, scan, concurrency and healthy-digest checks remain enforced.
The release wrapper passes the environment-specific health-check JSON to v2.0.10. A missing check is added with the new scanned image; the old image is not restarted with a new probe. An identical existing check is preserved, and a different existing check stops the release. Role, runtime-secret reference, scan, concurrency and healthy-digest checks remain enforced.
35 changes: 28 additions & 7 deletions src/reports/dashboard/sales-dashboard.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,18 @@ export const SALES_ACCOUNT_CATEGORIES = [
] as const;
export type SalesAccountCategory = (typeof SALES_ACCOUNT_CATEGORIES)[number];

/** Customer revenue filters; TopGear includes Salesforce's TopGear Ops offering. */
export const SALES_REVENUE_OFFERING_TYPES = [
"AI Training",
"Crowd Powered Outcomes",
"Crowd Powered Innovation",
"Flexible Talent",
"TopGear",
"TaaS Ops",
] as const;
export type SalesRevenueOfferingType =
(typeof SALES_REVENUE_OFFERING_TYPES)[number];

/** One zero-filled monthly chart, with nullable values for unknown percentages. */
export class SalesDashboardChartDto {
@ApiProperty() id: string;
Expand All @@ -128,8 +140,20 @@ export class SalesDashboardChartDto {
}>;
}

/** Complete customer revenue matrices for one offering scope (or all offerings). */
export class SalesCustomerRevenueDto {
/** All customer names from the revenue report, without top-category/Other grouping. */
@ApiPropertyOptional({ type: SalesDashboardChartDto })
customerRevenue?: SalesDashboardChartDto;
/** Full customer matrices filtered by Account Category, independent of chart breakdown. */
@ApiPropertyOptional({ type: Object })
customerRevenueByAccountCategory?: Partial<
Record<SalesAccountCategory, SalesDashboardChartDto>
>;
}

/** Independently loaded section; notices expose missing source data without fabricating metrics. */
export class SalesDashboardDto {
export class SalesDashboardDto extends SalesCustomerRevenueDto {
@ApiProperty({ type: [SalesDashboardChartDto] })
charts: SalesDashboardChartDto[];
/** Unfiltered source categories so selecting one value does not remove the other choices. */
Expand All @@ -143,12 +167,9 @@ export class SalesDashboardDto {
/** Full customer series for the payments matrix, before top-five/Other grouping. */
@ApiPropertyOptional({ type: SalesDashboardChartDto })
customerPayments?: SalesDashboardChartDto;
/** All customer names from the revenue report, without top-category/Other grouping. */
@ApiPropertyOptional({ type: SalesDashboardChartDto })
customerRevenue?: SalesDashboardChartDto;
/** Full customer matrices filtered by Account Category, independent of chart breakdown. */
/** Offering-scoped customer matrices, including intersections with Account Category. */
@ApiPropertyOptional({ type: Object })
customerRevenueByAccountCategory?: Partial<
Record<SalesAccountCategory, SalesDashboardChartDto>
customerRevenueByOfferingType?: Partial<
Record<SalesRevenueOfferingType, SalesCustomerRevenueDto>
>;
}
109 changes: 73 additions & 36 deletions src/reports/dashboard/sales-dashboard.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ import { ConfigService } from "@nestjs/config";
import { Pool } from "pg";
import { SqlLoaderService } from "../../common/sql-loader.service";
import { DbService } from "../../db/db.service";
import { customerTypeLabel } from "../sales/sales-category.utils";
import {
categoryValue,
customerTypeLabel,
} from "../sales/sales-category.utils";
import { SalesRowDto } from "../sales/sales-reports.dto";
import {
DashboardDateFilter,
Expand All @@ -26,6 +29,8 @@ import {
} from "./dashboard-reports.service";
import {
SALES_ACCOUNT_CATEGORIES,
SALES_REVENUE_OFFERING_TYPES,
SalesCustomerRevenueDto,
SalesDashboardChartDto,
SalesDashboardDto,
SalesDashboardQueryDto,
Expand Down Expand Up @@ -541,43 +546,70 @@ export class SalesDashboardService implements OnModuleDestroy {
]
.map((label) => categoryColumn(report, label))
.find((index) => index >= 0) ?? -1);
const customerRevenue =
customerIndex >= 0
? revenueBreakdown(
range,
entries,
(entry) =>
categoryLabel(entry.row, customerIndex) || "Unknown customer",
{
id: "revenue-customer-all",
title: "Revenue Customer-Wise",
description: `All customers by ${report.columns[customerIndex].label}, with monthly revenue and annual totals.`,
},
Infinity,
)
: undefined;
const accountCategoryIndex = categoryColumn(report, /^account category$/i);
const customerRevenueByAccountCategory =
customerIndex >= 0 && accountCategoryIndex >= 0
// Build each matrix from matching detail rows, so customers with multiple
// offerings contribute only the selected offering's recognized revenue.
const customerMatrices = (
scopedEntries: RevenueEntry[],
): SalesCustomerRevenueDto => {
if (customerIndex < 0) return {};
const matrix = (rows: RevenueEntry[], description: string) =>
revenueBreakdown(
range,
rows,
(entry) =>
categoryLabel(entry.row, customerIndex) || "Unknown customer",
{
id: "revenue-customer-all",
title: "Revenue Customer-Wise",
description,
},
Infinity,
);
return {
customerRevenue: matrix(
scopedEntries,
`All customers by ${report.columns[customerIndex].label}, with monthly revenue and annual totals.`,
),
customerRevenueByAccountCategory:
accountCategoryIndex >= 0
? Object.fromEntries(
SALES_ACCOUNT_CATEGORIES.map((category) => [
category,
matrix(
scopedEntries.filter(
(entry) =>
categoryLabel(entry.row, accountCategoryIndex)
.replace(/\s*-\s*ignore$/i, "")
.toLowerCase() === category.toLowerCase(),
),
`Customer revenue for ${category}.`,
),
]),
)
: undefined,
};
};
const { customerRevenue, customerRevenueByAccountCategory } =
customerMatrices(entries);
const offeringIndex = columns.offering
? report.columns.findIndex((column) => column.id === columns.offering)
: categoryColumn(report, CATEGORY_LABELS.offering);
const customerRevenueByOfferingType =
customerIndex >= 0 && offeringIndex >= 0
? Object.fromEntries(
SALES_ACCOUNT_CATEGORIES.map((category) => [
category,
revenueBreakdown(
range,
entries.filter(
(entry) =>
categoryLabel(entry.row, accountCategoryIndex)
.replace(/\s*-\s*ignore$/i, "")
.toLowerCase() === category.toLowerCase(),
),
(entry) =>
categoryLabel(entry.row, customerIndex) || "Unknown customer",
{
id: "revenue-customer-all",
title: "Revenue Customer-Wise",
description: `Customer revenue for ${category}.`,
},
Infinity,
SALES_REVENUE_OFFERING_TYPES.map((offering) => [
offering,
customerMatrices(
entries.filter((entry) => {
const label = categoryValue(
offeringLabel(entry.row, offeringIndex, projectIndex),
);
return (
(label === "topgear ops" ? "topgear" : label) ===
categoryValue(offering)
);
}),
),
]),
)
Expand All @@ -590,6 +622,10 @@ export class SalesDashboardService implements OnModuleDestroy {
...charts,
...(customerRevenue ? [customerRevenue] : []),
...Object.values(customerRevenueByAccountCategory ?? {}),
...Object.values(customerRevenueByOfferingType ?? {}).flatMap((scope) => [
...(scope.customerRevenue ? [scope.customerRevenue] : []),
...Object.values(scope.customerRevenueByAccountCategory ?? {}),
]),
].forEach((chart) =>
chart.months.forEach((month) =>
Object.keys(month.values).forEach((key) => {
Expand All @@ -609,6 +645,7 @@ export class SalesDashboardService implements OnModuleDestroy {
charts,
customerRevenue,
customerRevenueByAccountCategory,
customerRevenueByOfferingType,
notices,
filterOptions: filtered.options,
};
Expand Down
Loading
Loading