Skip to content
Closed
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
10 changes: 2 additions & 8 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,9 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
libcairo2-dev \
poppler-utils \
default-jre && \
# chromium 150.0.7871.46 crashes on startup in headless mode (Debian bug
# #1141488), breaking all chromedriver sessions. Pin the last working
# build from snapshot.debian.org; unpin once a fixed version ships.
echo "deb [check-valid-until=no] http://snapshot.debian.org/archive/debian-security/20260628T000000Z/ trixie-security main" >> /etc/apt/sources.list && \
echo "deb http://deb.debian.org/debian/ sid main" >> /etc/apt/sources.list && \
apt-get update -qqy && \
apt-get install -qqy --no-install-recommends \
chromium=149.0.7827.196-1~deb13u1 \
chromium-common=149.0.7827.196-1~deb13u1 \
chromium-driver=149.0.7827.196-1~deb13u1
apt-get install -qqy --no-install-recommends chromium chromium-driver

FROM base AS deps

Expand Down
12 changes: 12 additions & 0 deletions RELEASE.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
Release Notes
=============

Version 0.78.3
--------------

- Take the canonical page URL from learn_url (#3863)
- Bound Renovate to the service versions we actually run (#3865)
- Add refunds to the receipt page (#3823)
- vector search: add course metadata to context (#3852)
- Revert "Pin chromium to 149.0.7827.196 to avoid broken 150 headless build (#3584)" (#3878)
- Move the shared media players into page-components (#3875)
- vector search: embedding call request hedging (#3853)
- Update dependency youtube-transcript-api to >=1.2.4,<1.3 (#3861)

Version 0.78.2 (Released September 01, 2026)
--------------

Expand Down
2 changes: 1 addition & 1 deletion docker-compose.services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ services:
start_period: 30s

qdrant:
image: qdrant/qdrant:latest
image: qdrant/qdrant:v1.18.3
ports:
- "6333:6333"
volumes:
Expand Down
2 changes: 1 addition & 1 deletion frontends/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
},
"dependencies": {
"@mitodl/mit-learn-api-axios": "2026.8.17",
"@mitodl/mitxonline-api-axios": "2026.8.31",
"@mitodl/mitxonline-api-axios": "2026.8.31-1",
"@tanstack/react-query": "^5.66.0",
"axios": "^1.12.2",
"tiny-invariant": "^1.3.3"
Expand Down
26 changes: 25 additions & 1 deletion frontends/api/src/mitxonline/hooks/orders/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,27 @@
import { orderQueries, orderKeys } from "./queries"
import { useMutation, useQueryClient } from "@tanstack/react-query"
import { ordersApi } from "../../clients"
import type { RefundRequestRequest } from "@mitodl/mitxonline-api-axios/v2"
import type { MutationHookOptions } from "../../../mutations/mutationMeta"

export { orderQueries, orderKeys }
/**
* Submit a learner's refund request for an order.
*
* Invalidates the order's receipt, since a successful request moves its
* `refund_status` to `requested` and the card rendering it has to follow.
*/
const useCreateRefundRequest = ({ meta }: MutationHookOptions = {}) => {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (opts: RefundRequestRequest) =>
ordersApi.ordersRefundRequestsCreate({ RefundRequestRequest: opts }),
onSettled: (_data, _error, opts) => {
queryClient.invalidateQueries({
queryKey: orderKeys.receipt(opts.order),
})
},
meta,
})
}

export { orderQueries, orderKeys, useCreateRefundRequest }
25 changes: 23 additions & 2 deletions frontends/api/src/mitxonline/test-utils/factories/orders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ import type {
PaginatedOrderHistoryList,
Product,
RedeemedDiscount,
RefundRequest,
TransactionLine,
} from "@mitodl/mitxonline-api-axios/v2"
import { RefundStatusEnum } from "@mitodl/mitxonline-api-axios/v2"

const transactionLine = (
overrides: Partial<TransactionLine> = {},
Expand All @@ -26,6 +28,8 @@ const transactionLine = (
total_paid: faker.commerce.price({ min: 50, max: 500 }),
discount: "0.00",
price: faker.commerce.price({ min: 50, max: 500 }),
// MITx Online course runs almost always carry an audit track.
has_free_audit: true,
...overrides,
})

Expand Down Expand Up @@ -85,14 +89,30 @@ const order = (overrides: Partial<Order> = {}): Order => ({
lines: [transactionLine()],
discounts: [],
refunds: [],
refund_eligible: false,
// Default to the state a freshly purchased order is in, so tests opt into
// the other refund states rather than out of an arbitrary one.
refund_eligible: true,
refund_status: RefundStatusEnum.Eligible,
refund_deadline: faker.date.future().toISOString(),
refund_requested_on: null,
refund_reviewed_on: null,
reference_number: faker.string.alphanumeric(10),
created_on: faker.date.past().toISOString(),
transactions: orderTransactions(),
street_address: orderStreetAddress(),
...overrides,
})

const refundRequest = (
overrides: Partial<RefundRequest> = {},
): RefundRequest => ({
order: faker.number.int(),
refund_reason: "course_not_as_expected",
refund_reason_text: "",
consent_given: true,
...overrides,
})

/**
* The default `purchasable_object` has only an `id`, which matches no variant —
* pass a shaped object (with `course`, or neither `course` nor `run_tag`) when the
Expand Down Expand Up @@ -135,7 +155,7 @@ const orderHistory = (overrides: Partial<OrderHistory> = {}): OrderHistory => ({
created_on: faker.date.past().toISOString(),
titles: [],
updated_on: faker.date.past().toISOString(),
refund_eligible: false,
refund_eligible: true,
...overrides,
})

Expand All @@ -159,5 +179,6 @@ export {
line,
product,
redeemedDiscount,
refundRequest,
transactionLine,
}
1 change: 1 addition & 0 deletions frontends/api/src/mitxonline/test-utils/urls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ const orders = {
`${getApiBaseUrl()}/api/v0/orders/receipt/${orderId}/`,
historyList: (params?: OrdersApiOrdersHistoryListRequest) =>
`${getApiBaseUrl()}/api/v0/orders/history/${queryify(params)}`,
refundRequests: () => `${getApiBaseUrl()}/api/v0/orders/refund-requests/`,
}

const verifiedProgramEnrollments = {
Expand Down
4 changes: 2 additions & 2 deletions frontends/main/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@
"@mitodl/arithmix": "^0.2.5",
"@mitodl/course-search-utils": "^3.8.0",
"@mitodl/hacksnack": "^0.1.2",
"@mitodl/mitxonline-api-axios": "2026.8.31",
"@mitodl/smoot-design": "6.33.4",
"@mitodl/mitxonline-api-axios": "2026.8.31-1",
"@mitodl/smoot-design": "6.34.0",
"@mui/base": "5.0.0-beta.70",
"@mui/material": "^6.4.5",
"@mui/material-nextjs": "^6.4.3",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -270,16 +270,14 @@ export const EnrolledCourseCard = ({
const mitxOnlineUser = useQuery(mitxUserQueries.me())
const isStaff = mitxOnlineUser.data?.is_staff
/**
* Only verified enrollments can have a receipt, so the lookup is skipped
* entirely for audit ones. Every card shares one `orders/history` query (same
* cache key), so this is a single request for the whole dashboard rather than
* one per card.
* Enrollment mode does not decide this: a refund returns the learner to audit,
* and the receipt is where they confirm it went through. Whether an order
* exists is the only question, and the lookup answers it.
*
* Every card shares one `orders/history` query (same cache key), so this is a
* single request for the whole dashboard rather than one per card.
*/
const receiptResolution = useOrderIdForRun(
isVerifiedEnrollmentMode(enrollment?.enrollment_mode)
? (run?.id ?? null)
: null,
)
const receiptResolution = useOrderIdForRun(run?.id ?? null)
const title = isCompact ? course.title : run?.title || course.title
const coursewareUrl = run?.courseware_url
const certificateLink = getCertificateLink(
Expand Down Expand Up @@ -484,7 +482,6 @@ export const EnrolledCourseCard = ({
)

const receiptMenuItem = getReceiptMenuItem(
enrollment?.enrollment_mode,
receiptResolution,
receiptByRunView(run.id),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
} from "@/test-utils"
import { makeRequest } from "api/test-utils"
import * as mitxonline from "api/mitxonline-test-utils"
import { setupOrderHistory } from "./test-utils"
import { ProgramAsCourseCard } from "./ProgramAsCourseCard"
import { waitFor } from "@testing-library/react"
import invariant from "tiny-invariant"
Expand Down Expand Up @@ -91,6 +92,9 @@ describe("ProgramAsCourseCard", () => {
mitxonline.urls.userMe.get(),
mitxonline.factories.user.user(),
)
// Every enrollment card looks up order history to decide whether to offer a
// Receipt item, regardless of enrollment mode.
setupOrderHistory()

return {
courseProgram: program,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,11 @@ export const ProgramEnrollmentCard = ({
programEnrollment.enrollment_mode,
)
/**
* Skipped for audit enrollments, which never have a receipt. Shares one
* `orders/history` query with every other card on the dashboard.
* Not gated on enrollment mode: a refund returns the learner to audit, and the
* receipt is where they confirm it went through. Shares one `orders/history`
* query with every other card on the dashboard.
*/
const receiptResolution = useOrderIdForProgram(
upgradedAndIncomplete ? programId : null,
)
const receiptResolution = useOrderIdForProgram(programId)
const displayMode = program.display_mode
const titleSection = (
<Stack gap="12px">
Expand Down Expand Up @@ -125,7 +124,6 @@ export const ProgramEnrollmentCard = ({
})
}
const receiptMenuItem = getReceiptMenuItem(
programEnrollment.enrollment_mode,
receiptResolution,
receiptByProgramView(programId),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,35 +13,15 @@ const resolution = (
})

describe("getReceiptMenuItem", () => {
test("returns null when enrollment mode is undefined", () => {
expect(
getReceiptMenuItem(undefined, resolution({ orderId: 87 }), RESOLVER_HREF),
).toBeNull()
})

test("returns null for audit enrollments, since auditing is free", () => {
expect(
getReceiptMenuItem("audit", resolution({ orderId: 87 }), RESOLVER_HREF),
).toBeNull()
})

test("returns null while the order lookup is still pending", () => {
expect(
getReceiptMenuItem(
"verified",
resolution({ isPending: true }),
RESOLVER_HREF,
),
getReceiptMenuItem(resolution({ isPending: true }), RESOLVER_HREF),
).toBeNull()
})

test("links straight to the resolved receipt", () => {
expect(
getReceiptMenuItem(
"verified",
resolution({ orderId: 87 }),
RESOLVER_HREF,
),
getReceiptMenuItem(resolution({ orderId: 87 }), RESOLVER_HREF),
).toEqual(
expect.objectContaining({
key: "receipt",
Expand All @@ -51,14 +31,23 @@ describe("getReceiptMenuItem", () => {
)
})

/**
* A refund moves the learner back to the audit track, and the receipt is where
* they see that it came through. Enrollment mode is not consulted at all; the
* order lookup is the only thing that decides.
*/
test("still links once the order has been refunded and the learner is auditing", () => {
expect(
getReceiptMenuItem(resolution({ orderId: 87 }), RESOLVER_HREF),
).toEqual(expect.objectContaining({ href: "/receipt/87" }))
})

/**
* The lookup succeeded and found nothing — e.g. verified via a program purchase
* that upgraded an existing audit enrollment, which creates no order.
*/
test("hides the item when the lookup found no order", () => {
expect(
getReceiptMenuItem("verified", resolution(), RESOLVER_HREF),
).toBeNull()
expect(getReceiptMenuItem(resolution(), RESOLVER_HREF)).toBeNull()
})

/**
Expand All @@ -68,11 +57,7 @@ describe("getReceiptMenuItem", () => {
*/
test("falls back to the resolver route when the lookup failed", () => {
expect(
getReceiptMenuItem(
"verified",
resolution({ isError: true }),
RESOLVER_HREF,
),
getReceiptMenuItem(resolution({ isError: true }), RESOLVER_HREF),
).toEqual(
expect.objectContaining({
key: "receipt",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { SimpleMenuItem } from "ol-components"
import { isVerifiedEnrollmentMode } from "@/common/mitxonline"
import { receiptView } from "@/common/urls"
import type { OrderIdResolution } from "@/common/mitxonline/useOrderIdForResource"

/**
* The "Receipt" item for a dashboard card, or null when there is nothing to link
* to.
*
* Verified track alone is not enough — a program purchase can upgrade an existing
* audit enrollment without creating any order, leaving no receipt.
* Whether an order exists is the only thing that decides this. Enrollment mode
* does not: a refund moves the learner back to audit, and that is exactly when
* they want the receipt. A learner who reached the verified track without
* paying — a program purchase can upgrade an audit enrollment without creating
* an order — has no matching order, so the lookup below already hides the item.
*
* The three reasons `orderId` can be null are deliberately not equivalent:
*
Expand All @@ -20,11 +22,9 @@ import type { OrderIdResolution } from "@/common/mitxonline/useOrderIdForResourc
* - looked up and found nothing — genuinely no receipt, so hide the item
*/
const getReceiptMenuItem = (
enrollmentMode: string | null | undefined,
resolution: OrderIdResolution,
resolverHref: string,
): SimpleMenuItem | null => {
if (!enrollmentMode || !isVerifiedEnrollmentMode(enrollmentMode)) return null
if (resolution.isPending) return null

const href =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,11 @@ const makeGrade = factories.enrollment.grade
const makeContract = factories.contracts.contract

/**
* Mock the order history that verified enrollment cards fetch to decide whether to
* show a "Receipt" item. Required in any suite rendering a verified enrollment,
* or the unmocked request fails the test. Defaults to an empty history (no
* receipt); pass `runId`/`programId` to make one resolve.
* Mock the order history that enrollment cards fetch to decide whether to show a
* "Receipt" item. Required in any suite rendering an enrollment card, whatever
* its mode — a refunded order leaves the learner auditing and still has a
* receipt — or the unmocked request fails the test. Defaults to an empty history
* (no receipt); pass `runId`/`programId` to make one resolve.
*/
const setupOrderHistory = ({
runId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const makeMockPlayer = () => ({

const mockHandles: MockPlayerHandle[] = []

jest.mock("@/app-pages/VideoPlaylistCollectionPage/VideoJsPlayer", () => ({
jest.mock("@/page-components/VideoPlayer/VideoJsPlayer", () => ({
__esModule: true,
default: ({
sources,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { ActionButton, VisuallyHidden } from "@mitodl/smoot-design"
import { useWindowDimensions } from "ol-utilities"
import type { VideoResource } from "api/v1"
import MITOpenLearningLogo from "@/public/images/mit-open-learning-logo.svg"
import VideoJsPlayer from "@/app-pages/VideoPlaylistCollectionPage/VideoJsPlayer"
import VideoJsPlayer from "@/page-components/VideoPlayer/VideoJsPlayer"
import type Player from "video.js/dist/types/player"
import { FocusTrap } from "@mui/base/FocusTrap"
import { usePostHog } from "posthog-js/react"
Expand Down
Loading
Loading