From d4c7011e21b9b011882884124995f25efb915cbd Mon Sep 17 00:00:00 2001 From: Chris Chudzicki Date: Thu, 6 Aug 2026 16:40:17 -0400 Subject: [PATCH] Update @mitodl/mitxonline-api-axios to 2026.8.6 Three required-field additions in the regenerated client are breaking: - `ManagerEnrollmentCode` gains `email_status` and `email_status_event_timestamp` - `OrganizationPage` gains `sso_organization_id: string | null` - `b2bManagerOrganizationsList` gains a positional `sso_organization_id` parameter ahead of `options` (our only caller passes no arguments, so it is unaffected) Everything else in the release is additive and currently unused: `OrderStatus`/`ordersStatusRetrieve`, `b2bServiceOrganizationManagerCheck`, `EmailStatusEnum`, `anonymous_id` on baskets, and optional `LegalAddress` fields. `Basket.user`/`BasketWithProduct.user` also loosened to `number | null | undefined`; nothing reads `.user`. Give the two factories the new fields. `OrganizationCards.test.tsx` built eleven `OrganizationPage` literals by hand; route them through the factory the file already uses elsewhere so the next required field does not break them again. Every field except `sso_organization_id` was already explicitly overridden and `mergeOverrides` overwrites arrays rather than merging, so the objects are unchanged. Drop the `Analytics/orgUuid.ts` shim, which carried a TODO to delete itself once this client landed, and read `org.sso_organization_id` directly. Behavior is unchanged: all four analytics queries are gated on `enabled: analyticsAvailable`, which requires a truthy `orgUuid`, so an absent, null, or empty UUID still reads as "analytics unavailable" and never reaches the API with `undefined` in the path. Pin the version exactly rather than with a caret, matching `@mitodl/mit-learn-api-axios` and `@mitodl/smoot-design`. A caret on a date-versioned generated client lets a plain `yarn install` pull one that adds required fields, which is exactly the breakage above. Co-Authored-By: Claude Opus 5 (1M context) --- frontends/api/package.json | 2 +- .../test-utils/factories/contracts.ts | 2 + .../test-utils/factories/organization.ts | 2 + frontends/main/package.json | 2 +- .../DashboardPage/Analytics/orgUuid.ts | 28 ------- .../DashboardPage/AnalyticsContent.test.tsx | 14 +--- .../DashboardPage/AnalyticsContent.tsx | 8 +- .../OrganizationCards.test.tsx | 74 ++++++++++--------- yarn.lock | 12 +-- 9 files changed, 59 insertions(+), 85 deletions(-) delete mode 100644 frontends/main/src/app-pages/DashboardPage/Analytics/orgUuid.ts diff --git a/frontends/api/package.json b/frontends/api/package.json index 7598ba0093..83612e3dac 100644 --- a/frontends/api/package.json +++ b/frontends/api/package.json @@ -35,7 +35,7 @@ }, "dependencies": { "@mitodl/mit-learn-api-axios": "2026.7.22", - "@mitodl/mitxonline-api-axios": "2026.7.22", + "@mitodl/mitxonline-api-axios": "2026.8.6", "@tanstack/react-query": "^5.66.0", "axios": "^1.12.2", "tiny-invariant": "^1.3.3" diff --git a/frontends/api/src/mitxonline/test-utils/factories/contracts.ts b/frontends/api/src/mitxonline/test-utils/factories/contracts.ts index 0be1c360fc..1b1b71cb75 100644 --- a/frontends/api/src/mitxonline/test-utils/factories/contracts.ts +++ b/frontends/api/src/mitxonline/test-utils/factories/contracts.ts @@ -52,6 +52,8 @@ const contractCode = ( redeemed_by: isRedeemed ? faker.internet.email() : null, redeemed_on: isRedeemed ? faker.date.past().toISOString() : null, last_sent: null, + email_status: null, + email_status_event_timestamp: null, ...overrides, } } diff --git a/frontends/api/src/mitxonline/test-utils/factories/organization.ts b/frontends/api/src/mitxonline/test-utils/factories/organization.ts index bec2eb1d07..8be6c6735e 100644 --- a/frontends/api/src/mitxonline/test-utils/factories/organization.ts +++ b/frontends/api/src/mitxonline/test-utils/factories/organization.ts @@ -12,6 +12,7 @@ const organization = ( description: faker.lorem.paragraph(), logo: faker.image.url(), slug: faker.lorem.slug(), + sso_organization_id: faker.string.uuid(), contracts: [], }, overrides, @@ -24,6 +25,7 @@ const organization = ( description: merged.description!, logo: merged.logo!, slug: merged.slug!, + sso_organization_id: merged.sso_organization_id ?? null, contracts: merged.contracts!, } } diff --git a/frontends/main/package.json b/frontends/main/package.json index 25042ae7c4..d5b847325f 100644 --- a/frontends/main/package.json +++ b/frontends/main/package.json @@ -18,7 +18,7 @@ "@mitodl/arithmix": "^0.2.2", "@mitodl/course-search-utils": "^3.5.2", "@mitodl/hacksnack": "^0.1.0", - "@mitodl/mitxonline-api-axios": "2026.7.22", + "@mitodl/mitxonline-api-axios": "2026.8.6", "@mitodl/smoot-design": "6.31.1", "@mui/base": "5.0.0-beta.70", "@mui/material": "^6.4.5", diff --git a/frontends/main/src/app-pages/DashboardPage/Analytics/orgUuid.ts b/frontends/main/src/app-pages/DashboardPage/Analytics/orgUuid.ts deleted file mode 100644 index 5c90cf37ef..0000000000 --- a/frontends/main/src/app-pages/DashboardPage/Analytics/orgUuid.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { OrganizationPage } from "@mitodl/mitxonline-api-axios/v2" - -/** - * The Keycloak organization UUID, which is what the analytics API keys every - * org endpoint on — it is the only identifier stable across the JWT, MITx - * Online and StarRocks (see mitodl/ol-analytics-api#13). - * - * MITx Online exposes it on `OrganizationPageSerializer` as - * `sso_organization_id` (mitodl/mitxonline#3789). That change has not been cut - * into a release of `@mitodl/mitxonline-api-axios` yet, so the generated - * `OrganizationPage` type does not declare the field and we have to read it - * off the wire ourselves. - * - * Returning `null` when it is absent is the load-bearing part: it is exactly - * what happens when mit-learn is pointed at a MITx Online deploy that predates - * that PR, and it must surface as "analytics unavailable for this org" rather - * than as a request to the analytics API with `undefined` in the path. - * - * TODO: delete this and read `org.sso_organization_id` directly once the - * regenerated client is picked up in `frontends/api/package.json`. - */ -const getOrgUuid = (org: OrganizationPage | undefined): string | null => { - const value = (org as { sso_organization_id?: unknown } | undefined) - ?.sso_organization_id - return typeof value === "string" && value.length > 0 ? value : null -} - -export { getOrgUuid } diff --git a/frontends/main/src/app-pages/DashboardPage/AnalyticsContent.test.tsx b/frontends/main/src/app-pages/DashboardPage/AnalyticsContent.test.tsx index 9a7566a523..489e40963f 100644 --- a/frontends/main/src/app-pages/DashboardPage/AnalyticsContent.test.tsx +++ b/frontends/main/src/app-pages/DashboardPage/AnalyticsContent.test.tsx @@ -53,23 +53,15 @@ const managerOrgsUrl = urls.organization.managerOrganizationsList() const ORG_UUID = "3fa85f64-5717-4562-b3fc-2c963f66afa6" -/** - * `sso_organization_id` is not on the generated `OrganizationPage` type yet - * (mitodl/mitxonline#3789 has not been released into the client), so it is - * spliced on here the same way it arrives on the wire. - */ const orgWithUuid = ( overrides: Partial = {}, ssoOrganizationId: string | null = ORG_UUID, -) => { - const org = factories.organizations.organization({ +) => + factories.organizations.organization({ contracts: [factories.contracts.contract()], + sso_organization_id: ssoOrganizationId, ...overrides, }) - return ssoOrganizationId - ? { ...org, sso_organization_id: ssoOrganizationId } - : org -} const setManagerOrgs = (orgs: unknown[]) => { setMockResponse.get(managerOrgsUrl, { diff --git a/frontends/main/src/app-pages/DashboardPage/AnalyticsContent.tsx b/frontends/main/src/app-pages/DashboardPage/AnalyticsContent.tsx index 62caacb278..ef4c36f041 100644 --- a/frontends/main/src/app-pages/DashboardPage/AnalyticsContent.tsx +++ b/frontends/main/src/app-pages/DashboardPage/AnalyticsContent.tsx @@ -23,7 +23,6 @@ import EngagementTrendChart from "./Analytics/EngagementTrendChart" import ProgramFunnelChart from "./Analytics/ProgramFunnelChart" import SectionHeader from "./Analytics/SectionHeader" import SectionTruncation from "./Analytics/SectionTruncation" -import { getOrgUuid } from "./Analytics/orgUuid" /** * Org-scoped B2B analytics, the reporting half of the org-manager dashboard @@ -154,7 +153,12 @@ const AnalyticsContentInternal: React.FC = ({ } = useQuery(managerOrganizationQueries.managerOrganizationsList()) const org = managerOrgs?.find(matchOrganizationBySlug(orgSlug)) - const orgUuid = getOrgUuid(org) + // The Keycloak organization UUID, which is what the analytics API keys every + // org endpoint on (see mitodl/ol-analytics-api#13). It is null on orgs whose + // MITx Online record predates the field, and absent altogether against a + // deploy that predates mitodl/mitxonline#3789 — both must read as "analytics + // unavailable" rather than a request with `undefined` in the path. + const orgUuid = org?.sso_organization_id ?? null const analyticsAvailable = isAnalyticsConfigured() && !!orgUuid // Per-section page size. Raised only by that section's "Show all", so diff --git a/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/OrganizationCards.test.tsx b/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/OrganizationCards.test.tsx index 071ae7bac7..11d7580ece 100644 --- a/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/OrganizationCards.test.tsx +++ b/frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/OrganizationCards.test.tsx @@ -26,22 +26,24 @@ describe("OrganizationCards", () => { count: number, withContracts: boolean = true, ): OrganizationPage[] => { - return Array.from({ length: count }, (_, i) => ({ - id: i + 1, - name: `Test Organization ${i + 1}`, - description: `Description for org ${i + 1}`, - logo: `https://example.com/logo${i + 1}.png`, - slug: `org-test-${i + 1}`, - contracts: withContracts - ? [ - mitxOnlineFactories.contracts.contract({ - id: i + 1, - name: `Contract ${i + 1}`, - organization: i + 1, - }), - ] - : [], - })) + return Array.from({ length: count }, (_, i) => + mitxOnlineFactories.organizations.organization({ + id: i + 1, + name: `Test Organization ${i + 1}`, + description: `Description for org ${i + 1}`, + logo: `https://example.com/logo${i + 1}.png`, + slug: `org-test-${i + 1}`, + contracts: withContracts + ? [ + mitxOnlineFactories.contracts.contract({ + id: i + 1, + name: `Contract ${i + 1}`, + organization: i + 1, + }), + ] + : [], + }), + ) } const setup = ({ organizations = [] }: SetupOptions = {}) => { @@ -81,7 +83,7 @@ describe("OrganizationCards", () => { }) it("displays organization logo", async () => { - const organization: OrganizationPage = { + const organization = mitxOnlineFactories.organizations.organization({ id: 1, name: "Test Organization", description: "Test Description", @@ -94,7 +96,7 @@ describe("OrganizationCards", () => { name: "Contract 1", }), ], - } + }) setup({ organizations: [organization] }) const image = await screen.findByAltText("") @@ -108,7 +110,7 @@ describe("OrganizationCards", () => { describe("when organization has contracts", () => { it("renders contract cards with correct information", async () => { - const organization: OrganizationPage = { + const organization = mitxOnlineFactories.organizations.organization({ id: 1, name: "Test Organization", description: "Test description", @@ -126,7 +128,7 @@ describe("OrganizationCards", () => { organization: 1, }), ], - } + }) setup({ organizations: [organization] }) @@ -160,14 +162,14 @@ describe("OrganizationCards", () => { organization: 1, slug: "contract-2", }) - const organization: OrganizationPage = { + const organization = mitxOnlineFactories.organizations.organization({ id: 1, name: "Test Organization", description: "Test description", logo: "https://example.com/logo.png", slug: "org-test-org", contracts: [contract1, contract2], - } + }) setup({ organizations: [organization] }) @@ -192,7 +194,7 @@ describe("OrganizationCards", () => { }) it("renders cards for both mobile and desktop screen sizes", async () => { - const organization: OrganizationPage = { + const organization = mitxOnlineFactories.organizations.organization({ id: 1, name: "Test Organization", description: "Test description", @@ -205,7 +207,7 @@ describe("OrganizationCards", () => { organization: 1, }), ], - } + }) setup({ organizations: [organization] }) @@ -217,14 +219,14 @@ describe("OrganizationCards", () => { describe("when organization has no matching contracts", () => { it("renders organization header but no contract cards", async () => { - const organization: OrganizationPage = { + const organization = mitxOnlineFactories.organizations.organization({ id: 1, name: "Test Organization", description: "Test description", logo: "https://example.com/logo.png", slug: "org-test-org", contracts: [], // No contracts for this organization - } + }) setup({ organizations: [organization] }) @@ -245,7 +247,7 @@ describe("OrganizationCards", () => { describe("with multiple organizations", () => { it("renders all organizations with their respective contracts", async () => { - const org1: OrganizationPage = { + const org1 = mitxOnlineFactories.organizations.organization({ id: 1, name: "Organization One", description: "Test description", @@ -265,8 +267,8 @@ describe("OrganizationCards", () => { slug: "org1-contract-2", }), ], - } - const org2: OrganizationPage = { + }) + const org2 = mitxOnlineFactories.organizations.organization({ id: 2, name: "Organization Two", description: "Test description", @@ -280,7 +282,7 @@ describe("OrganizationCards", () => { slug: "org2-contract-1", }), ], - } + }) setup({ organizations: [org1, org2] }) @@ -368,14 +370,14 @@ describe("OrganizationCards", () => { organization: 1, slug: "test-contract", }) - const organization: OrganizationPage = { + const organization = mitxOnlineFactories.organizations.organization({ id: 1, name: "Test Organization", description: "Test description", logo: "https://example.com/logo.png", slug: "org-my-company", contracts: [contract], - } + }) setup({ organizations: [organization] }) @@ -395,14 +397,14 @@ describe("OrganizationCards", () => { organization: 1, slug: "test-contract", }) - const organization: OrganizationPage = { + const organization = mitxOnlineFactories.organizations.organization({ id: 1, name: "Test Organization", description: "Test description", logo: "https://example.com/logo.png", slug: "my-company", // No 'org-' prefix contracts: [contract], - } + }) setup({ organizations: [organization] }) @@ -416,14 +418,14 @@ describe("OrganizationCards", () => { }) it("filters contracts correctly when none match organization ID", async () => { - const organization: OrganizationPage = { + const organization = mitxOnlineFactories.organizations.organization({ id: 1, name: "Test Organization", description: "Test description", logo: "https://example.com/logo.png", slug: "org-test-org", contracts: [], // No contracts for this organization - } + }) setup({ organizations: [organization] }) diff --git a/yarn.lock b/yarn.lock index dc85f1a362..40ad192596 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3377,13 +3377,13 @@ __metadata: languageName: node linkType: hard -"@mitodl/mitxonline-api-axios@npm:2026.7.22": - version: 2026.7.22 - resolution: "@mitodl/mitxonline-api-axios@npm:2026.7.22" +"@mitodl/mitxonline-api-axios@npm:2026.8.6": + version: 2026.8.6 + resolution: "@mitodl/mitxonline-api-axios@npm:2026.8.6" dependencies: "@types/node": "npm:^20.11.19" axios: "npm:^1.6.5" - checksum: 10/67f8a535f6dc109b6e4c3eaa05beb96ee87e17609142b406ec3218d84d1a39272e2ef07aacca59bb58611490ef9aa1d5e521d1f1b5659e4f799faf5420bf1e95 + checksum: 10/88e68269e1b9e5fb2e79d97ea7232e131dfc4a73d0cb6b0193b2a5cbc24af5eef8f3dbd4f9a63347fbcbd715364bf54b48a2eb9d32b71c7c18186d96ac4d3e8c languageName: node linkType: hard @@ -9304,7 +9304,7 @@ __metadata: dependencies: "@faker-js/faker": "npm:^10.0.0" "@mitodl/mit-learn-api-axios": "npm:2026.7.22" - "@mitodl/mitxonline-api-axios": "npm:2026.7.22" + "@mitodl/mitxonline-api-axios": "npm:2026.8.6" "@tanstack/react-query": "npm:^5.66.0" "@testing-library/react": "npm:^16.3.0" axios: "npm:^1.12.2" @@ -16639,7 +16639,7 @@ __metadata: "@mitodl/arithmix": "npm:^0.2.2" "@mitodl/course-search-utils": "npm:^3.5.2" "@mitodl/hacksnack": "npm:^0.1.0" - "@mitodl/mitxonline-api-axios": "npm:2026.7.22" + "@mitodl/mitxonline-api-axios": "npm:2026.8.6" "@mitodl/smoot-design": "npm:6.31.1" "@mui/base": "npm:5.0.0-beta.70" "@mui/material": "npm:^6.4.5"