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.