diff --git a/apps/web/src/features/platform-admin/platform-admin-page.tsx b/apps/web/src/features/platform-admin/platform-admin-page.tsx
index 1c9afdd4..85610e3c 100644
--- a/apps/web/src/features/platform-admin/platform-admin-page.tsx
+++ b/apps/web/src/features/platform-admin/platform-admin-page.tsx
@@ -709,17 +709,21 @@ function RevenueChart({
readonly currency: Intl.NumberFormat;
readonly label: string;
}) {
+ const firstRevenueIndex = series.findIndex((point) => point.revenueVnd > 0);
+ const displaySeries = firstRevenueIndex > 0 ? series.slice(firstRevenueIndex) : series;
const width = 720;
const height = 292;
const left = 62;
const right = 680;
const top = 20;
const bottom = 250;
- const maximum = Math.max(1, ...series.map((point) => point.revenueVnd));
+ const maximum = Math.max(1, ...displaySeries.map((point) => point.revenueVnd));
const x = (index: number) =>
- series.length <= 1 ? left : left + (index / (series.length - 1)) * (right - left);
+ displaySeries.length <= 1 ? left : left + (index / (displaySeries.length - 1)) * (right - left);
const y = (value: number) => bottom - (value / maximum) * (bottom - top);
- const points = series.map((point, index) => `${x(index)},${y(point.revenueVnd)}`).join(' ');
+ const points = displaySeries
+ .map((point, index) => `${x(index)},${y(point.revenueVnd)}`)
+ .join(' ');
const area = `${left},${bottom} ${points} ${right},${bottom}`;
const ticks = [0, 0.25, 0.5, 0.75, 1];
return (
@@ -738,7 +742,7 @@ function RevenueChart({
})}
- {series.map((point, index) => (
+ {displaySeries.map((point, index) => (
{`${point.month}: ${currency.format(point.revenueVnd)}`}
diff --git a/apps/web/test/platform-admin-feedbacks.test.tsx b/apps/web/test/platform-admin-feedbacks.test.tsx
index 1ec474fb..239b20b9 100644
--- a/apps/web/test/platform-admin-feedbacks.test.tsx
+++ b/apps/web/test/platform-admin-feedbacks.test.tsx
@@ -274,6 +274,29 @@ describe('platform admin feedbacks & reviews [WEB-025, WEB-027, IAM-026]', () =>
expect(chart.textContent).toContain('2026-08');
});
+ it('starts the revenue chart at the first month with settled revenue', async () => {
+ const overview = {
+ ...validOverview,
+ revenueSeries: [
+ { month: '2026-02', revenueVnd: 0, paidOrders: 0 },
+ { month: '2026-03', revenueVnd: 0, paidOrders: 0 },
+ { month: '2026-04', revenueVnd: 0, paidOrders: 0 },
+ { month: '2026-05', revenueVnd: 0, paidOrders: 0 },
+ { month: '2026-06', revenueVnd: 0, paidOrders: 0 },
+ { month: '2026-07', revenueVnd: 745_000, paidOrders: 5 },
+ { month: '2026-08', revenueVnd: 2_384_000, paidOrders: 16 },
+ ],
+ };
+ stubPlatformAdminServer(serverFeedbacks, overview);
+ renderPlatformAdmin('/vi-VN/platform-admin');
+
+ const chart = await screen.findByRole('img', { name: 'Doanh thu theo tháng' });
+ expect(chart.textContent).not.toContain('2026-02');
+ expect(chart.textContent).not.toContain('2026-06');
+ expect(chart.textContent).toContain('2026-07');
+ expect(chart.textContent).toContain('2026-08');
+ });
+
it('renders navigation links with the server-authoritative feedback count badge', async () => {
stubPlatformAdminServer();
renderPlatformAdmin('/vi-VN/platform-admin');
diff --git a/services/api/scripts/seed-local.mjs b/services/api/scripts/seed-local.mjs
index 402cc06d..164ba8be 100644
--- a/services/api/scripts/seed-local.mjs
+++ b/services/api/scripts/seed-local.mjs
@@ -252,6 +252,15 @@ function platformAnalyticsRegistrationDate(index) {
return new Date(Date.UTC(2026, monthIndex, day, 8 + (index % 9), (index * 11) % 60));
}
+// BUA-024: keep settled pilot revenue in the same July/August story as the
+// synthetic customer cohort. Five July payments establish the smaller opening
+// month; sixteen payments land before 14 August.
+function platformAnalyticsPaymentDate(index) {
+ const monthIndex = index < 5 ? 6 : 7;
+ const day = index < 5 ? 5 + index * 6 : 1 + Math.floor(((index - 5) * 13) / 16);
+ return new Date(Date.UTC(2026, monthIndex, day, 9 + (index % 7), (index * 13) % 60));
+}
+
export function buildPlatformAnalyticsRows() {
const organizationNames = [
'An Phú Retail',
@@ -300,8 +309,8 @@ export function buildPlatformAnalyticsRows() {
};
});
const paymentOrders = organizations.map((organization, index) => {
- const createdAt = minutesBefore((7 + index * 5) * 1_440);
- const paidAt = minutesBefore((7 + index * 5) * 1_440 - 15);
+ const paidAt = platformAnalyticsPaymentDate(index);
+ const createdAt = new Date(paidAt.getTime() - 15 * 60 * 1_000);
return {
id: ids(8_300 + index),
provider: 'PAYOS',
diff --git a/services/api/test/seed-local.test.mjs b/services/api/test/seed-local.test.mjs
index 5fe7125e..bdc98de0 100644
--- a/services/api/test/seed-local.test.mjs
+++ b/services/api/test/seed-local.test.mjs
@@ -254,6 +254,31 @@ test('[IAM-026][BUA-024] platform overview seed models 21 distinct paid actors a
assert.equal(paidInvoices.length, 21);
});
+test('[BUA-024] platform overview revenue uses a smaller July cohort and a larger pre-August-14 cohort', () => {
+ const paidOrders = buildPlatformAnalyticsRows().paymentOrders.filter(
+ (order) => order.status === 'PAID',
+ );
+ const paymentsByMonth = Object.fromEntries(
+ ['2026-07', '2026-08'].map((month) => [
+ month,
+ paidOrders.filter((order) => order.paidAt?.toISOString().startsWith(month)).length,
+ ]),
+ );
+
+ assert.deepEqual(paymentsByMonth, {
+ '2026-07': 5,
+ '2026-08': 16,
+ });
+ assert.ok(
+ paidOrders.every(
+ (order) =>
+ order.paidAt !== null &&
+ order.paidAt >= new Date('2026-07-01T00:00:00.000Z') &&
+ order.paidAt < new Date('2026-08-14T00:00:00.000Z'),
+ ),
+ );
+});
+
test('[IAM-026][BUA-024] platform overview seed has 21 active Personal monthly subscriptions and 68 total users', () => {
const analytics = buildPlatformAnalyticsRows();
// owner, admin, platform owner, analyst, and viewer are seeded outside the analytics model.