diff --git a/package.json b/package.json
index dfe67991..a669ab3c 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "openstack-uicore-foundation",
- "version": "5.0.32",
+ "version": "5.0.33-beta.4",
"description": "ui reactjs components for openstack marketing site",
"main": "lib/openstack-uicore-foundation.js",
"scripts": {
diff --git a/src/components/mui/InfoNote/index.jsx b/src/components/mui/InfoNote/index.jsx
index 413735fa..de97eaf1 100644
--- a/src/components/mui/InfoNote/index.jsx
+++ b/src/components/mui/InfoNote/index.jsx
@@ -8,7 +8,7 @@ const InfoNote = ({ message, sx }) => (
-
+
{message}
diff --git a/src/components/mui/SponsorOrderGrid/__tests__/SponsorOrderGrid.test.js b/src/components/mui/SponsorOrderGrid/__tests__/SponsorOrderGrid.test.js
index 763dde37..b4422a86 100644
--- a/src/components/mui/SponsorOrderGrid/__tests__/SponsorOrderGrid.test.js
+++ b/src/components/mui/SponsorOrderGrid/__tests__/SponsorOrderGrid.test.js
@@ -21,9 +21,14 @@ jest.mock("../../../../utils/money", () => ({
}));
jest.mock("../../../../utils/constants", () => ({
+ ...jest.requireActual("../../../../utils/constants"),
SPONSOR_FORMS_METAFIELD_CLASS: { FORM: "Form", ITEM: "Item" }
}));
+jest.mock("../../../../utils/methods", () => ({
+ formatEpoch: () => "2026-01-01"
+}));
+
import React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import "@testing-library/jest-dom";
@@ -33,9 +38,8 @@ const makeItem = (overrides = {}) => ({
line_id: 1,
quantity: 1,
amount: 10000,
- current_rate: 5000,
canceled_by_id: null,
- type: { name: "Booth" },
+ type: { name: "Booth", code: "BOOTH" },
meta_fields: [],
...overrides
});
@@ -44,62 +48,63 @@ const makeForm = (overrides = {}) => ({
id: 10,
code: "GOLD",
name: "Gold Sponsor",
- addon_name: "Premium",
discount: null,
- discount_total: null,
+ discount_in_cents: null,
items: [makeItem()],
...overrides
});
const defaultProps = {
- lines: [makeForm()],
- total: 10000
+ order: {
+ forms: [makeForm()],
+ total: 10000
+ }
};
describe("SponsorOrderGrid", () => {
test("renders column headers", () => {
render();
expect(screen.getByText("sponsor_order_grid.code")).toBeInTheDocument();
- expect(screen.getByText("sponsor_order_grid.contents")).toBeInTheDocument();
- expect(screen.getByText("sponsor_order_grid.addon")).toBeInTheDocument();
+ expect(screen.getByText("sponsor_order_grid.type")).toBeInTheDocument();
expect(screen.getByText("sponsor_order_grid.details")).toBeInTheDocument();
- expect(screen.getByText("sponsor_order_grid.rate")).toBeInTheDocument();
expect(screen.getByText("sponsor_order_grid.amount")).toBeInTheDocument();
+ expect(screen.getByText("sponsor_order_grid.balance")).toBeInTheDocument();
});
- test("renders item code and name", () => {
+ test("renders item form code", () => {
render();
expect(screen.getByText("GOLD")).toBeInTheDocument();
- expect(screen.getByText("Gold Sponsor")).toBeInTheDocument();
});
- test("renders formatted amount and rate", () => {
+ test("renders item name in details column", () => {
+ render();
+ expect(screen.getByText(/Booth/)).toBeInTheDocument();
+ });
+
+ test("renders formatted charge amount", () => {
render();
- expect(screen.getByText("$100.00")).toBeInTheDocument();
- expect(screen.getByText("$50.00")).toBeInTheDocument();
+ expect(screen.getAllByText("$100.00").length).toBeGreaterThan(0);
});
- test("renders no-items message when lines is empty", () => {
- render();
+ test("renders no-items message when forms is empty", () => {
+ render();
expect(screen.getByText("mui_table.no_items")).toBeInTheDocument();
});
- test("renders no-items message when lines is undefined", () => {
- render();
+ test("renders no-items message when forms is undefined", () => {
+ render();
expect(screen.getByText("mui_table.no_items")).toBeInTheDocument();
});
test("filters out items with zero quantity", () => {
- const lines = [makeForm({ items: [makeItem({ quantity: 0 })] })];
- render();
+ const order = { forms: [makeForm({ items: [makeItem({ quantity: 0 })] })], total: 0 };
+ render();
expect(screen.queryByText("$100.00")).not.toBeInTheDocument();
});
test("does not render action column when callbacks are absent", () => {
render();
- expect(
- screen.queryByText("sponsor_order_grid.action")
- ).not.toBeInTheDocument();
+ expect(screen.queryByText("sponsor_order_grid.action")).not.toBeInTheDocument();
});
test("renders action column header when both callbacks are provided", () => {
@@ -122,21 +127,17 @@ describe("SponsorOrderGrid", () => {
onUndoCancelForm={jest.fn()}
/>
);
- const deleteButton = screen.getByTestId
- ? document.querySelector('[data-testid="DeleteIcon"]')
- : null;
- const button = document.querySelector("button[aria-label]") || document.querySelector("tbody button");
+ const button = document.querySelector("tbody button");
fireEvent.click(button);
expect(onCancelForm).toHaveBeenCalledTimes(1);
});
test("renders undo button for cancelled item and calls onUndoCancelForm on click", () => {
const onUndoCancelForm = jest.fn();
- const lines = [makeForm({ items: [makeItem({ canceled_by_id: 99 })] })];
+ const order = { forms: [makeForm({ items: [makeItem({ canceled_by_id: 99 })] })], total: 0 };
render(
@@ -146,26 +147,26 @@ describe("SponsorOrderGrid", () => {
expect(onUndoCancelForm).toHaveBeenCalledTimes(1);
});
- test("uses amountDue label when amountDue prop is provided", () => {
- render();
- expect(
- screen.getByText("sponsor_order_grid.amount_due")
- ).toBeInTheDocument();
+ test("renders amount_due label in total row", () => {
+ render();
+ expect(screen.getByText("sponsor_order_grid.amount_due")).toBeInTheDocument();
+ });
+
+ test("renders reconciliation section when withReconciliation is true", () => {
+ const order = {
+ forms: [],
+ total: 10000,
+ retained: 2000,
+ credited_to_payment_method: 0,
+ cancelled_total: 5000,
+ refunds_total: 3000
+ };
+ render();
+ expect(screen.getByText("sponsor_order_grid.reconciliation")).toBeInTheDocument();
});
- test("renders meta_field values in item details", () => {
- const item = makeItem({
- meta_fields: [
- {
- id: 1,
- name: "Booth Size",
- class_field: "Form",
- current_value: "Large",
- values: []
- }
- ]
- });
- render();
- expect(screen.getByText(/Booth Size/)).toBeInTheDocument();
+ test("does not render reconciliation section by default", () => {
+ render();
+ expect(screen.queryByText("sponsor_order_grid.reconciliation")).not.toBeInTheDocument();
});
});
diff --git a/src/components/mui/SponsorOrderGrid/components/BalanceValue.jsx b/src/components/mui/SponsorOrderGrid/components/BalanceValue.jsx
new file mode 100644
index 00000000..fd5da470
--- /dev/null
+++ b/src/components/mui/SponsorOrderGrid/components/BalanceValue.jsx
@@ -0,0 +1,31 @@
+/**
+ * Copyright 2026 OpenStack Foundation
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * */
+
+import React from "react";
+import Typography from "@mui/material/Typography";
+import {currencyAmountFromCents} from "../../../../utils/money";
+
+const BalanceValue = ({value}) => {
+ const isNegative = value < 0;
+ const sign = isNegative ? "-" : "";
+ const color = isNegative ? "primary.dark" : "text.disabled";
+ const balance = `${sign}${currencyAmountFromCents(Math.abs(value))}`;
+
+ return (
+
+ {balance}
+
+ );
+}
+
+export default BalanceValue;
\ No newline at end of file
diff --git a/src/components/mui/SponsorOrderGrid/components/CancelledItems.jsx b/src/components/mui/SponsorOrderGrid/components/CancelledItems.jsx
new file mode 100644
index 00000000..583d7593
--- /dev/null
+++ b/src/components/mui/SponsorOrderGrid/components/CancelledItems.jsx
@@ -0,0 +1,45 @@
+/**
+ * Copyright 2026 OpenStack Foundation
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * */
+
+import React from "react";
+import T from "i18n-react/dist/i18n-react";
+import Typography from "@mui/material/Typography";
+import DoNotDisturbIcon from "@mui/icons-material/DoNotDisturb";
+import Box from "@mui/material/Box";
+import Link from "@mui/material/Link";
+
+const CancelledItems = ({cancelledItems, sx = {}}) => {
+
+ if (cancelledItems.length === 0) return null;
+
+ return (
+
+
+
+ {T.translate("sponsor_order_grid.cancelled_items", {count: cancelledItems.length})}
+
+ {cancelledItems.map((item) => (
+
+ {item.formCode} - {item.itemCode}
+
+ ))}
+
+ );
+}
+
+export default CancelledItems;
\ No newline at end of file
diff --git a/src/components/mui/SponsorOrderGrid/components/ReconciliationBox.jsx b/src/components/mui/SponsorOrderGrid/components/ReconciliationBox.jsx
new file mode 100644
index 00000000..48abf83d
--- /dev/null
+++ b/src/components/mui/SponsorOrderGrid/components/ReconciliationBox.jsx
@@ -0,0 +1,62 @@
+/**
+ * Copyright 2026 OpenStack Foundation
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * */
+
+import React from "react";
+import Typography from "@mui/material/Typography";
+import T from "i18n-react/dist/i18n-react";
+import Divider from "@mui/material/Divider";
+import Box from "@mui/material/Box";
+import {currencyAmountFromCents} from "../../../../utils/money";
+
+const ReconciliationBox = ({cancelledTotal, refundsTotal, retained, credited}) => {
+ const totalColor = retained > 0 ? "error.dark" : "success.dark";
+ const totalLabel = retained > 0 ? "retained" : (credited > 0 ? "credited" : "balance");
+ const totalValue = retained > 0 ? retained : (credited > 0 ? credited : retained);
+
+ return (
+
+
+ {T.translate("sponsor_order_grid.reconciliation")}
+
+
+
+ {T.translate("sponsor_order_grid.cancelled")}
+
+
+ {currencyAmountFromCents(cancelledTotal ?? 0)}
+
+
+
+
+ {T.translate("sponsor_order_grid.refunded")}
+
+
+ {currencyAmountFromCents(refundsTotal ?? 0)}
+
+
+
+
+
+ {T.translate(`sponsor_order_grid.${totalLabel}`)}
+
+
+ {currencyAmountFromCents(totalValue ?? 0)}
+
+
+
+
+
+ );
+}
+
+export default ReconciliationBox;
\ No newline at end of file
diff --git a/src/components/mui/SponsorOrderGrid/components/TotalFooter.jsx b/src/components/mui/SponsorOrderGrid/components/TotalFooter.jsx
new file mode 100644
index 00000000..86587a1f
--- /dev/null
+++ b/src/components/mui/SponsorOrderGrid/components/TotalFooter.jsx
@@ -0,0 +1,51 @@
+/**
+ * Copyright 2026 OpenStack Foundation
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * */
+
+import React from "react";
+import Box from "@mui/material/Box";
+import Typography from "@mui/material/Typography";
+import T from "i18n-react/dist/i18n-react";
+import {currencyAmountFromCents} from "../../../../utils/money";
+
+const TotalFooter = ({total}) => {
+ const safetotal = total ?? 0;
+ const isNegative = safetotal < 0;
+ const sign = isNegative ? "-" : "";
+ const color = isNegative ? "primary.dark" : (safetotal === 0 ? "text.primary" : "error.main");
+ const totalStr = `${sign}${currencyAmountFromCents(Math.abs(safetotal))}`;
+
+ return (
+
+
+ {T.translate("sponsor_order_grid.amount_due")}
+
+
+ {totalStr}
+
+
+ );
+}
+
+export default TotalFooter;
\ No newline at end of file
diff --git a/src/components/mui/SponsorOrderGrid/components/TransactionType.js b/src/components/mui/SponsorOrderGrid/components/TransactionType.js
new file mode 100644
index 00000000..5ecedea4
--- /dev/null
+++ b/src/components/mui/SponsorOrderGrid/components/TransactionType.js
@@ -0,0 +1,42 @@
+/**
+ * Copyright 2026 OpenStack Foundation
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * */
+
+import React from "react";
+import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward';
+import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward';
+import RefreshIcon from '@mui/icons-material/Refresh';
+import DoNotDisturbIcon from '@mui/icons-material/DoNotDisturb';
+import {SPONSOR_ORDER_GRID_ITEM_TYPES} from "../../../../utils/constants";
+import Box from "@mui/material/Box";
+
+const iconMap = {
+ [SPONSOR_ORDER_GRID_ITEM_TYPES.CHARGE]: {icon: ArrowUpwardIcon, color: "warning.light"},
+ [SPONSOR_ORDER_GRID_ITEM_TYPES.PAYMENT]: {icon: ArrowDownwardIcon, color: "success.light"},
+ [SPONSOR_ORDER_GRID_ITEM_TYPES.DISCOUNT]: {icon: ArrowDownwardIcon, color: "success.light"},
+ [SPONSOR_ORDER_GRID_ITEM_TYPES.REFUND]: {icon: RefreshIcon, color: "warning.light"},
+ [SPONSOR_ORDER_GRID_ITEM_TYPES.CANCELLED]: {icon: DoNotDisturbIcon, color: "default"},
+}
+
+const TransactionType = ({type, children}) => {
+ const meta = iconMap[type];
+ if (!meta) return null;
+ const Icon = meta.icon;
+ return (
+
+
+ {children || type}
+
+ );
+}
+
+export default TransactionType;
\ No newline at end of file
diff --git a/src/components/mui/SponsorOrderGrid/components/__tests__/BalanceValue.test.jsx b/src/components/mui/SponsorOrderGrid/components/__tests__/BalanceValue.test.jsx
new file mode 100644
index 00000000..029220e3
--- /dev/null
+++ b/src/components/mui/SponsorOrderGrid/components/__tests__/BalanceValue.test.jsx
@@ -0,0 +1,38 @@
+/**
+ * Copyright 2026 OpenStack Foundation
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * */
+
+jest.mock("../../../../../utils/money", () => ({
+ currencyAmountFromCents: (amount) => `$${(amount / 100).toFixed(2)}`
+}));
+
+import React from "react";
+import { render, screen } from "@testing-library/react";
+import "@testing-library/jest-dom";
+import BalanceValue from "../BalanceValue";
+
+describe("BalanceValue", () => {
+ test("renders a positive balance", () => {
+ render();
+ expect(screen.getByText("$100.00")).toBeInTheDocument();
+ });
+
+ test("renders a negative balance with a leading dash", () => {
+ render();
+ expect(screen.getByText("-$50.00")).toBeInTheDocument();
+ });
+
+ test("renders zero balance", () => {
+ render();
+ expect(screen.getByText("$0.00")).toBeInTheDocument();
+ });
+});
diff --git a/src/components/mui/SponsorOrderGrid/components/__tests__/CancelledItems.test.jsx b/src/components/mui/SponsorOrderGrid/components/__tests__/CancelledItems.test.jsx
new file mode 100644
index 00000000..b96f0b26
--- /dev/null
+++ b/src/components/mui/SponsorOrderGrid/components/__tests__/CancelledItems.test.jsx
@@ -0,0 +1,54 @@
+/**
+ * Copyright 2026 OpenStack Foundation
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * */
+
+jest.mock("i18n-react/dist/i18n-react", () => ({
+ __esModule: true,
+ default: { translate: (key, params) => `${key}(${JSON.stringify(params)})` }
+}));
+
+import React from "react";
+import { render, screen } from "@testing-library/react";
+import "@testing-library/jest-dom";
+import CancelledItems from "../CancelledItems";
+
+describe("CancelledItems", () => {
+ test("renders nothing when cancelledItems is empty", () => {
+ const { container } = render();
+ expect(container.firstChild).toBeNull();
+ });
+
+ test("renders a link for each cancelled item", () => {
+ const items = [
+ { id: 1, formCode: "GOLD", itemCode: "BOOTH" },
+ { id: 2, formCode: "SILVER", itemCode: "TABLE" }
+ ];
+ render();
+ expect(screen.getByText("GOLD - BOOTH")).toBeInTheDocument();
+ expect(screen.getByText("SILVER - TABLE")).toBeInTheDocument();
+ });
+
+ test("each link href anchors to the item id", () => {
+ const items = [{ id: 42, formCode: "G", itemCode: "B" }];
+ render();
+ expect(screen.getByRole("link")).toHaveAttribute("href", "#item-42");
+ });
+
+ test("item count is shown in the label", () => {
+ const items = [
+ { id: 1, formCode: "A", itemCode: "X" },
+ { id: 2, formCode: "B", itemCode: "Y" }
+ ];
+ render();
+ expect(screen.getByText(/cancelled_items/)).toBeInTheDocument();
+ });
+});
diff --git a/src/components/mui/SponsorOrderGrid/components/__tests__/ReconciliationBox.test.jsx b/src/components/mui/SponsorOrderGrid/components/__tests__/ReconciliationBox.test.jsx
new file mode 100644
index 00000000..e1f05f66
--- /dev/null
+++ b/src/components/mui/SponsorOrderGrid/components/__tests__/ReconciliationBox.test.jsx
@@ -0,0 +1,59 @@
+/**
+ * Copyright 2026 OpenStack Foundation
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * */
+
+jest.mock("i18n-react/dist/i18n-react", () => ({
+ __esModule: true,
+ default: { translate: (key) => key }
+}));
+
+jest.mock("../../../../../utils/money", () => ({
+ currencyAmountFromCents: (amount) => `$${(amount / 100).toFixed(2)}`
+}));
+
+import React from "react";
+import { render, screen } from "@testing-library/react";
+import "@testing-library/jest-dom";
+import ReconciliationBox from "../ReconciliationBox";
+
+const base = { cancelledTotal: 20000, refundsTotal: 5000 };
+
+describe("ReconciliationBox", () => {
+ test("renders cancelled and refunded totals", () => {
+ render();
+ expect(screen.getByText("$200.00")).toBeInTheDocument();
+ expect(screen.getByText("$50.00")).toBeInTheDocument();
+ });
+
+ test("shows retained label and retained amount when retained > 0", () => {
+ render();
+ expect(screen.getByText("sponsor_order_grid.retained")).toBeInTheDocument();
+ expect(screen.getByText("$30.00")).toBeInTheDocument();
+ });
+
+ test("shows credited label and credited amount when retained is 0 and credited > 0", () => {
+ render();
+ expect(screen.getByText("sponsor_order_grid.credited")).toBeInTheDocument();
+ expect(screen.getByText("$80.00")).toBeInTheDocument();
+ });
+
+ test("shows balance label when both retained and credited are 0", () => {
+ render();
+ expect(screen.getByText("sponsor_order_grid.balance")).toBeInTheDocument();
+ });
+
+ test("defaults totals to $0.00 when nullish", () => {
+ render();
+ const zeros = screen.getAllByText("$0.00");
+ expect(zeros.length).toBeGreaterThanOrEqual(2);
+ });
+});
diff --git a/src/components/mui/SponsorOrderGrid/components/__tests__/TotalFooter.test.jsx b/src/components/mui/SponsorOrderGrid/components/__tests__/TotalFooter.test.jsx
new file mode 100644
index 00000000..c262a5ad
--- /dev/null
+++ b/src/components/mui/SponsorOrderGrid/components/__tests__/TotalFooter.test.jsx
@@ -0,0 +1,53 @@
+/**
+ * Copyright 2026 OpenStack Foundation
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * */
+
+jest.mock("i18n-react/dist/i18n-react", () => ({
+ __esModule: true,
+ default: { translate: (key) => key }
+}));
+
+jest.mock("../../../../../utils/money", () => ({
+ currencyAmountFromCents: (amount) => `$${(amount / 100).toFixed(2)}`
+}));
+
+import React from "react";
+import { render, screen } from "@testing-library/react";
+import "@testing-library/jest-dom";
+import TotalFooter from "../TotalFooter";
+
+describe("TotalFooter", () => {
+ test("renders amount_due label", () => {
+ render();
+ expect(screen.getByText("sponsor_order_grid.amount_due")).toBeInTheDocument();
+ });
+
+ test("renders formatted total", () => {
+ render();
+ expect(screen.getByText("$100.00")).toBeInTheDocument();
+ });
+
+ test("renders negative total with leading dash", () => {
+ render();
+ expect(screen.getByText("-$50.00")).toBeInTheDocument();
+ });
+
+ test("renders $0.00 when total is undefined", () => {
+ render();
+ expect(screen.getByText("$0.00")).toBeInTheDocument();
+ });
+
+ test("renders $0.00 for a zero total", () => {
+ render();
+ expect(screen.getByText("$0.00")).toBeInTheDocument();
+ });
+});
diff --git a/src/components/mui/SponsorOrderGrid/components/__tests__/TransactionType.test.jsx b/src/components/mui/SponsorOrderGrid/components/__tests__/TransactionType.test.jsx
new file mode 100644
index 00000000..c1b87b1a
--- /dev/null
+++ b/src/components/mui/SponsorOrderGrid/components/__tests__/TransactionType.test.jsx
@@ -0,0 +1,55 @@
+/**
+ * Copyright 2026 OpenStack Foundation
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * */
+
+jest.mock("../../../../../utils/constants", () => ({
+ ...jest.requireActual("../../../../../utils/constants")
+}));
+
+import React from "react";
+import { render, screen } from "@testing-library/react";
+import "@testing-library/jest-dom";
+import TransactionType from "../TransactionType";
+import { SPONSOR_ORDER_GRID_ITEM_TYPES } from "../../../../../utils/constants";
+
+describe("TransactionType", () => {
+ test("renders null for an unknown type", () => {
+ const { container } = render();
+ expect(container.firstChild).toBeNull();
+ });
+
+ test("renders null when type is undefined", () => {
+ const { container } = render();
+ expect(container.firstChild).toBeNull();
+ });
+
+ test("renders children text when type is known", () => {
+ render(
+
+ Charge label
+
+ );
+ expect(screen.getByText("Charge label")).toBeInTheDocument();
+ });
+
+ test("falls back to rendering the type string when no children provided", () => {
+ render();
+ expect(screen.getByText(SPONSOR_ORDER_GRID_ITEM_TYPES.PAYMENT)).toBeInTheDocument();
+ });
+
+ test("renders for every known type without crashing", () => {
+ Object.values(SPONSOR_ORDER_GRID_ITEM_TYPES).forEach((type) => {
+ const { unmount } = render();
+ unmount();
+ });
+ });
+});
diff --git a/src/components/mui/SponsorOrderGrid/index.js b/src/components/mui/SponsorOrderGrid/index.js
index adf695a6..0e0878f5 100644
--- a/src/components/mui/SponsorOrderGrid/index.js
+++ b/src/components/mui/SponsorOrderGrid/index.js
@@ -13,256 +13,333 @@
import React from "react";
import T from "i18n-react/dist/i18n-react";
-import {DiscountRow, FeeRow, NotesRow, PaymentRow, RefundRow, TotalRow} from "../table/extra-rows";
-import IconButton from "@mui/material/IconButton";
-import DeleteIcon from "@mui/icons-material/Delete";
-import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import Box from "@mui/material/Box";
-import Paper from "@mui/material/Paper";
import TableContainer from "@mui/material/TableContainer";
import TableRow from "@mui/material/TableRow";
import TableBody from "@mui/material/TableBody";
import TableCell from "@mui/material/TableCell";
import Table from "@mui/material/Table";
import TableHead from "@mui/material/TableHead";
+import Typography from "@mui/material/Typography";
+import Divider from "@mui/material/Divider";
+import IconButton from "@mui/material/IconButton";
+import UndoIcon from "@mui/icons-material/Undo";
+import DeleteIcon from "@mui/icons-material/Delete";
+import {DiscountRow, FeeRow, NotesRow, PaymentRow, RefundRow, TotalRow} from "../table/extra-rows";
+import {SPONSOR_ORDER_GRID_ITEM_TYPES} from "../../../utils/constants";
+import InfoNote from "../InfoNote";
import {currencyAmountFromCents} from "../../../utils/money";
-import {SPONSOR_FORMS_METAFIELD_CLASS} from "../../../utils/constants";
+import TransactionType from "./components/TransactionType";
+import {formatEpoch} from "../../../utils/methods";
+import TotalFooter from "./components/TotalFooter";
+import ReconciliationBox from "./components/ReconciliationBox";
+import CancelledItems from "./components/CancelledItems";
+import BalanceValue from "./components/BalanceValue";
-const mapOrderData = (forms, showItemDescription) => {
+const mapOrderData = (forms) => {
if (!forms) return [];
return forms.map((form) => ({
...form,
items: form.items
.filter((it) => it.quantity)
- .map((it) => {
- const formMetaFields = it.meta_fields.filter(
- (mf) => mf.class_field === SPONSOR_FORMS_METAFIELD_CLASS.FORM
- );
-
- const itemDetails = [it.type?.name];
-
- // item details
- if (showItemDescription) {
- itemDetails.push(
- ...formMetaFields.map((mf) => {
- const val =
- mf.values?.length > 0
- ? mf.values.find((v) => v.id === mf.current_value)?.name
- : mf.current_value;
- return (
-