+
{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..103abc87 100644
--- a/src/components/mui/SponsorOrderGrid/index.js
+++ b/src/components/mui/SponsorOrderGrid/index.js
@@ -13,22 +13,30 @@
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) => ({
@@ -36,233 +44,289 @@ const mapOrderData = (forms, showItemDescription) => {
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 (
-
- {mf.name}: {val}
-
- );
- })
- );
-
- itemDetails.push(
); // spacer
- itemDetails.push(
-
- {T.translate("sponsor_order_grid.total")}: {it.quantity}
-
- );
- }
-
const amount = currencyAmountFromCents(it.amount || 0);
const lineId = it.line_id;
const cancelled = !!it.canceled_by_id;
- const rate = currencyAmountFromCents(it.current_rate || 0);
+ const type = cancelled ? SPONSOR_ORDER_GRID_ITEM_TYPES.CANCELLED : SPONSOR_ORDER_GRID_ITEM_TYPES.CHARGE;
return {
id: lineId,
- code: form.code,
- name: form.name,
- rate,
- addon_name: form.addon_name,
- item_name: itemDetails,
+ formCode: form.code,
+ itemName: it.type?.name,
+ itemCode: it.type?.code,
+ quantity: it.quantity,
+ type,
amount,
- cancelled
+ amountValue: it.amount,
+ cancelled,
+ cancelledBy: T.translate("sponsor_order_grid.cancelled_by", {
+ user: it.canceled_by_full_name,
+ date: formatEpoch(it.canceled_at)
+ }),
};
})
}));
};
const SponsorOrderGrid = ({
- lines,
- notes,
- payments,
- refunds,
- fees,
- total,
- amountDue,
- withDescription = false,
+ title = T.translate("sponsor_order_grid.title"),
+ order,
+ withReconciliation = false,
+ withCancelledItemsHeader = false,
onCancelForm,
onUndoCancelForm
}) => {
- const data = mapOrderData(lines, withDescription);
- const showActionCol = onCancelForm && onUndoCancelForm;
- const trailingCols = showActionCol ? 1 : 0;
+
+ const {
+ forms,
+ fees,
+ payments,
+ refunds,
+ notes,
+ total,
+ retained,
+ credited_to_payment_method: credited,
+ cancelled_total: cancelledTotal,
+ refunds_total: refundsTotal
+ } = order;
+ const data = mapOrderData(forms);
+ const cancelledItems = data.flatMap((form) => form.items.filter((it) => it.cancelled));
+ const canCancel = onCancelForm && onUndoCancelForm;
+ const trailingCols = canCancel ? 1 : 0;
+ let balance = 0;
+
+ const calculateBalance = (rowAmount, op = 1) => {
+ balance = balance + (rowAmount * op);
+ return balance;
+ }
const columns = [
{
- columnKey: "code",
+ columnKey: "formCode",
header: T.translate("sponsor_order_grid.code")
},
{
- columnKey: "name",
- header: T.translate("sponsor_order_grid.contents")
+ columnKey: "type",
+ header: T.translate("sponsor_order_grid.type"),
+ render: (row) => ()
},
{
- columnKey: "addon_name",
- header: T.translate("sponsor_order_grid.addon")
- },
- {
- columnKey: "item_name",
- header: T.translate("sponsor_order_grid.details")
- },
- {
- columnKey: "rate",
- header: T.translate("sponsor_order_grid.rate")
+ columnKey: "details",
+ header: T.translate("sponsor_order_grid.details"),
+ render: (row) => (
+ <>
+
+ {row.itemName} - {T.translate("sponsor_order_grid.total")}: {row.quantity}
+
+ {row.cancelled &&
+
+ {row.cancelledBy}
+
+ }
+ >
+ )
},
{
columnKey: "amount",
- header: T.translate("sponsor_order_grid.amount")
+ header: T.translate("sponsor_order_grid.amount"),
+ align: "right",
+ strikethrough: true,
}
];
- if (showActionCol) {
- columns.push({
- columnKey: "actions",
- header: T.translate("sponsor_order_grid.action"),
- align: "center",
- render: (row) => {
- if (row.cancelled) {
- return (
- onUndoCancelForm(row)}>
- {" "}
- {T.translate("general.undo").toUpperCase()}
-
- );
- }
-
- return (
- onCancelForm(row)}>
-
-
- );
- }
- });
- }
+ const colCount = columns.length + 1 + trailingCols; // 1 for balance, 1 for action col
return (
-
-
-
-
- {/* TABLE HEADER */}
-
-
- {columns.map((col) => (
-
- {col.header}
-
- ))}
-
-
-
- {data.map((form) => {
- const rows = form.items.map((row) => (
-
- {columns.map((col) => (
+
+
+ {title && (
+
+ {title}
+
+ )}
+ {withCancelledItemsHeader && (
+
+ )}
+
+
+ {canCancel && (
+
+ )}
+
+
+ {/* TABLE HEADER */}
+
+
+ {columns.map((col) => (
+
+ {col.header}
+
+ ))}
+
+ {T.translate("sponsor_order_grid.balance")}
+
+ {canCancel && (
+
+ {T.translate("sponsor_order_grid.action")}
+
+ )}
+
+
+
+ {data.map((form) => {
+ const rows = form.items.map((row) => (
+
+ {(() => {
+ const cols = columns.map((col) => (
{col.render ? (
col.render(row)
) : (
-
- {row[col.columnKey]}
-
+ row[col.columnKey]
)}
- ))}
-
- ));
+ ));
+
+ // BALANCE COLUMN
+ cols.push(
+
+
+
+ )
+
+ // ACTION COLUMN
+ if (canCancel) {
+ cols.push(
+
+ {row.cancelled ? (
+ onUndoCancelForm(row)}>
+
+
+ ) : (
+ onCancelForm(row)}>
+
+
+ )}
+
+ )
+ }
+
+ return cols;
+ })()}
+
+
+ ));
- rows.push(
-
- );
+ const discountCents = form.discount_in_cents ?? 0;
+ rows.push(
+
+ );
+
+ return rows;
+ })}
+ {fees && fees.map((fee) => (
+
+ ))}
+ {refunds && refunds.map((refund) => (
+
+ ))}
+ {payments && payments.map((payment) => (
+
+ ))}
+ {notes && notes.map((note) => (
+
+ ))}
- return rows;
- })}
- {fees &&
- fees.map((fee) => (
-
- ))}
- {refunds &&
- refunds.map((refund) => (
-
- ))}
- {payments &&
- payments.map((payment) => (
-
- ))}
- {notes &&
- notes.map((note) => (
-
- ))}
+ {/* When using reconciliation, we show the total at the end */}
+ {!withReconciliation &&
+ }
+ {data.length === 0 && (
+
+
+ {T.translate("mui_table.no_items")}
+
+
+ )}
+
+
+
- {data.length === 0 && (
-
-
- {T.translate("mui_table.no_items")}
-
-
- )}
-
-
-
-
+ {withReconciliation &&
+
+
+
+
+
+ }
);
};
diff --git a/src/components/mui/__tests__/payment-row.test.js b/src/components/mui/__tests__/payment-row.test.js
index 754d7f99..08adb812 100644
--- a/src/components/mui/__tests__/payment-row.test.js
+++ b/src/components/mui/__tests__/payment-row.test.js
@@ -45,7 +45,7 @@ describe("PaymentRow", () => {
test("renders the formatted payment amount", () => {
renderInTable({ payment: { method: "Visa", amount: 2000, created: PAYMENT_TIMESTAMP } });
- expect(screen.getByText("-$20.00")).toBeInTheDocument();
+ expect(screen.getByText("$20.00")).toBeInTheDocument();
});
test("renders the payment method", () => {
diff --git a/src/components/mui/__tests__/refund-row.test.js b/src/components/mui/__tests__/refund-row.test.js
index 594e852e..11bc5496 100644
--- a/src/components/mui/__tests__/refund-row.test.js
+++ b/src/components/mui/__tests__/refund-row.test.js
@@ -42,7 +42,7 @@ describe("RefundRow", () => {
test("renders the formatted refund amount", () => {
renderInTable({ refund: { reason: "Duplicate", status: "completed", amount: 3000 } });
- expect(screen.getByText("-$30.00")).toBeInTheDocument();
+ expect(screen.getByText("$30.00")).toBeInTheDocument();
});
test("renders the refund reason", () => {
diff --git a/src/components/mui/__tests__/total-row.test.js b/src/components/mui/__tests__/total-row.test.js
index 5fcc3100..a278922f 100644
--- a/src/components/mui/__tests__/total-row.test.js
+++ b/src/components/mui/__tests__/total-row.test.js
@@ -31,38 +31,38 @@ const renderInTable = (props) =>
render(
);
describe("TotalRow", () => {
test("renders 'TOTAL' label key in first column", () => {
- renderInTable({ targetCol: "quantity", total: 42 });
+ renderInTable({ total: 4200 });
expect(screen.getByText("mui_table.total")).toBeInTheDocument();
});
- test("renders total value in the targetCol", () => {
- renderInTable({ targetCol: "quantity", total: 42 });
- expect(screen.getByText("42")).toBeInTheDocument();
+ test("renders formatted total amount in cents", () => {
+ renderInTable({ total: 4200 });
+ expect(screen.getByText("$42.00")).toBeInTheDocument();
});
- test("renders correct number of cells (one per column)", () => {
- const { container } = renderInTable({ targetCol: "quantity", total: 10 });
+ test("renders correct number of cells based on colGap", () => {
+ const { container } = renderInTable({ total: 1000, colGap: columns.length - 2 });
expect(container.querySelectorAll("td")).toHaveLength(columns.length);
});
test("renders extra trailing cells when trailing prop is provided", () => {
const { container } = renderInTable({
- targetCol: "quantity",
- total: 10,
+ total: 1000,
+ colGap: columns.length - 2,
trailing: 2
});
expect(container.querySelectorAll("td")).toHaveLength(columns.length + 2);
});
- test("renders string totals", () => {
- renderInTable({ targetCol: "price", total: "$1,234" });
- expect(screen.getByText("$1,234")).toBeInTheDocument();
+ test("renders negative total with sign", () => {
+ renderInTable({ total: -5000 });
+ expect(screen.getByText("-$50.00")).toBeInTheDocument();
});
});
diff --git a/src/components/mui/table/extra-rows/DiscountRow.jsx b/src/components/mui/table/extra-rows/DiscountRow.jsx
index 4e0d84b5..27fe1a79 100644
--- a/src/components/mui/table/extra-rows/DiscountRow.jsx
+++ b/src/components/mui/table/extra-rows/DiscountRow.jsx
@@ -17,40 +17,48 @@ import TableRow from "@mui/material/TableRow";
import TableCell from "@mui/material/TableCell";
import Typography from "@mui/material/Typography";
import { currencyAmountFromCents } from "../../../../utils/money";
+import {SPONSOR_ORDER_GRID_ITEM_TYPES} from "../../../../utils/constants";
+import TransactionType from "../../SponsorOrderGrid/components/TransactionType";
+import BalanceValue from "../../SponsorOrderGrid/components/BalanceValue";
-const DiscountRow = ({ discount, discountTotal, colGap = 2, trailing = 0 }) => {
+const DiscountRow = ({ discount, discountCents, balance, colGap = 0, trailing = 0 }) => {
- if (discountTotal === 0) return null;
+ if (!discountCents) return null;
return (
{T.translate("mui_table.dis")}
-
- {T.translate("mui_table.discount")}
+
+
+ {T.translate("mui_table.discount")}
+
+
+
+
+
+ {discount}
{[...Array(colGap)].map((_, i) => (
// eslint-disable-next-line react/no-array-index-key
))}
-
-
- {discount}
-
-
-
+
- -{currencyAmountFromCents(discountTotal)}
+ {currencyAmountFromCents(discountCents)}
+
+
+
{[...Array(trailing)].map((_, i) => (
// eslint-disable-next-line react/no-array-index-key
diff --git a/src/components/mui/table/extra-rows/FeeRow.jsx b/src/components/mui/table/extra-rows/FeeRow.jsx
index e4914acd..7d2a9899 100644
--- a/src/components/mui/table/extra-rows/FeeRow.jsx
+++ b/src/components/mui/table/extra-rows/FeeRow.jsx
@@ -17,15 +17,21 @@ import TableRow from "@mui/material/TableRow";
import TableCell from "@mui/material/TableCell";
import Typography from "@mui/material/Typography";
import { currencyAmountFromCents } from "../../../../utils/money";
+import TransactionType from "../../SponsorOrderGrid/components/TransactionType";
+import {SPONSOR_ORDER_GRID_ITEM_TYPES} from "../../../../utils/constants";
+import BalanceValue from "../../SponsorOrderGrid/components/BalanceValue";
-const FeeRow = ({ fee, colGap = 3, trailing = 0 }) => {
+const FeeRow = ({ fee, balance, colGap = 0, trailing = 0 }) => {
if (!fee) return null;
return (
{T.translate("mui_table.payfee")}
-
+
+
+
+
{fee.title}
@@ -33,14 +39,14 @@ const FeeRow = ({ fee, colGap = 3, trailing = 0 }) => {
// eslint-disable-next-line react/no-array-index-key
))}
-
-
+
+
{currencyAmountFromCents(fee.amount)}
+
+
+
{[...Array(trailing)].map((_, i) => (
// eslint-disable-next-line react/no-array-index-key
diff --git a/src/components/mui/table/extra-rows/NotesRow.jsx b/src/components/mui/table/extra-rows/NotesRow.jsx
index a71d4d4e..8951b6d4 100644
--- a/src/components/mui/table/extra-rows/NotesRow.jsx
+++ b/src/components/mui/table/extra-rows/NotesRow.jsx
@@ -24,8 +24,8 @@ const NotesRow = ({ colCount, note, showCode = false }) => {
{showCode && (
{T.translate("mui_table.note")}
)}
-
-
+
+
{note}
diff --git a/src/components/mui/table/extra-rows/PaymentRow.jsx b/src/components/mui/table/extra-rows/PaymentRow.jsx
index dc61d88f..a151c0d2 100644
--- a/src/components/mui/table/extra-rows/PaymentRow.jsx
+++ b/src/components/mui/table/extra-rows/PaymentRow.jsx
@@ -18,9 +18,11 @@ import TableRow from "@mui/material/TableRow";
import TableCell from "@mui/material/TableCell";
import Typography from "@mui/material/Typography";
import { currencyAmountFromCents } from "../../../../utils/money";
-import { DATETIME_FORMAT, MILLISECONDS_IN_SECOND } from "../../../../utils/constants";
+import {DATETIME_FORMAT, MILLISECONDS_IN_SECOND, SPONSOR_ORDER_GRID_ITEM_TYPES} from "../../../../utils/constants";
+import TransactionType from "../../SponsorOrderGrid/components/TransactionType";
+import BalanceValue from "../../SponsorOrderGrid/components/BalanceValue";
-const PaymentRow = ({ payment, colGap = 1, trailing = 0 }) => {
+const PaymentRow = ({ payment, balance, colGap = 0, trailing = 0 }) => {
if (!payment) return null;
@@ -28,20 +30,20 @@ const PaymentRow = ({ payment, colGap = 1, trailing = 0 }) => {
{T.translate("mui_table.pay")}
-
- {T.translate("mui_table.payment")}
-
+
+
+ {T.translate("mui_table.payment")}
+
+
-
+
{T.translate("mui_table.paid_via")} {payment.method}
-
-
-
+
{moment(payment.created * MILLISECONDS_IN_SECOND).format(DATETIME_FORMAT)}
@@ -49,14 +51,17 @@ const PaymentRow = ({ payment, colGap = 1, trailing = 0 }) => {
// eslint-disable-next-line react/no-array-index-key
))}
-
+
- -{currencyAmountFromCents(payment.amount)}
+ {currencyAmountFromCents(payment.amount)}
+
+
+
{[...Array(trailing)].map((_, i) => (
// eslint-disable-next-line react/no-array-index-key
diff --git a/src/components/mui/table/extra-rows/RefundRow.jsx b/src/components/mui/table/extra-rows/RefundRow.jsx
index 9b3e9e27..8970cd4a 100644
--- a/src/components/mui/table/extra-rows/RefundRow.jsx
+++ b/src/components/mui/table/extra-rows/RefundRow.jsx
@@ -17,8 +17,12 @@ import TableRow from "@mui/material/TableRow";
import TableCell from "@mui/material/TableCell";
import Typography from "@mui/material/Typography";
import { currencyAmountFromCents } from "../../../../utils/money";
+import {DATETIME_FORMAT, MILLISECONDS_IN_SECOND, SPONSOR_ORDER_GRID_ITEM_TYPES} from "../../../../utils/constants";
+import TransactionType from "../../SponsorOrderGrid/components/TransactionType";
+import BalanceValue from "../../SponsorOrderGrid/components/BalanceValue";
+import moment from "moment-timezone";
-const RefundRow = ({ refund, colGap = 1, trailing = 0 }) => {
+const RefundRow = ({ refund, balance, colGap = 0, trailing = 0 }) => {
if (!refund) return null;
@@ -26,20 +30,20 @@ const RefundRow = ({ refund, colGap = 1, trailing = 0 }) => {
{T.translate("mui_table.ref")}
-
- {T.translate("mui_table.refund")}
-
+
+
+ {T.translate("mui_table.refund")}
+
+
-
+
{refund.reason}
-
-
-
+
{refund.status}
@@ -47,14 +51,17 @@ const RefundRow = ({ refund, colGap = 1, trailing = 0 }) => {
// eslint-disable-next-line react/no-array-index-key
))}
-
+
- -{currencyAmountFromCents(refund.amount)}
+ {currencyAmountFromCents(refund.amount)}
+
+
+
{[...Array(trailing)].map((_, i) => (
// eslint-disable-next-line react/no-array-index-key
diff --git a/src/components/mui/table/extra-rows/TotalRow.jsx b/src/components/mui/table/extra-rows/TotalRow.jsx
index 41bceb6c..f2a2b5c1 100644
--- a/src/components/mui/table/extra-rows/TotalRow.jsx
+++ b/src/components/mui/table/extra-rows/TotalRow.jsx
@@ -14,32 +14,33 @@
import React from "react";
import TableCell from "@mui/material/TableCell";
import TableRow from "@mui/material/TableRow";
+import Typography from "@mui/material/Typography";
import T from "i18n-react/dist/i18n-react";
+import {currencyAmountFromCents} from "../../../../utils/money";
-const TotalRow = ({ columns, targetCol, total, trailing = 0, label = null, rowSx = {} }) => {
-
+const TotalRow = ({total, colGap = 3, trailing = 0, label = null, rowSx = {}}) => {
const totalLabel = label || T.translate("mui_table.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 (
- {columns.map((col, i) => {
- if (i === 0)
- return (
-
- {totalLabel}
-
- );
- if (col.columnKey === targetCol)
- return (
-
- {total}
-
- );
- return ;
- })}
+
+ {totalLabel}
+
+ {[...Array(colGap)].map((_, i) => (
+ // eslint-disable-next-line react/no-array-index-key
+
+ ))}
+
+ {totalStr}
+
{[...Array(trailing)].map((_, i) => (
// eslint-disable-next-line react/no-array-index-key
-
+
))}
);
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 4c3f4abd..b90b191e 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -128,16 +128,26 @@
"payment_confirmation_error": "Payment confirmation failed"
},
"sponsor_order_grid": {
+ "title": "Order Items Ledger",
"code": "Code",
- "contents": "Contents",
+ "type": "Type",
"addon": "Add-on",
"details": "Details",
+ "balance": "Balance",
"discount": "Discount",
"amount": "Amount",
"amount_due": "AMOUNT DUE",
"total": "Total",
"rate": "Rate",
- "action": "Action"
+ "action": "Action",
+ "cancel_info_note": "Active order items can be canceled. Canceled items show an undo action to restore them. Refund and payment rows are display-only.",
+ "cancelled_by": "Cancelled {date} by {user}",
+ "cancelled_items": "Cancelled items ({count}):",
+ "reconciliation": "Reconciliation",
+ "cancelled": "Cancelled",
+ "refunded": "Refunded",
+ "retained": "Retained as cancellation fee",
+ "credited": "Credited to Payment Method"
},
"upload_input_v3": {
"no_post_url": "No Post URL",
diff --git a/src/utils/__tests__/money.test.js b/src/utils/__tests__/money.test.js
index 2b1c8b4a..b186ba9d 100644
--- a/src/utils/__tests__/money.test.js
+++ b/src/utils/__tests__/money.test.js
@@ -120,15 +120,15 @@ describe("parsePrice()", () => {
});
describe("currencyAmountFromCents (integration, no mocks)", () => {
- it("throws if cents is not a number", () => {
- expect(() => currencyAmountFromCents("10")).toThrow("cents must be an integer number");
- expect(() => currencyAmountFromCents(null)).toThrow("cents must be an integer number");
- expect(() => currencyAmountFromCents(undefined)).toThrow("cents must be an integer number");
+ it("returns error string if cents is not a number", () => {
+ expect(currencyAmountFromCents("10")).toBe("!ERROR");
+ expect(currencyAmountFromCents(null)).toBe("!ERROR");
+ expect(currencyAmountFromCents(undefined)).toBe("!ERROR");
});
- it("throws if cents is not an integer", () => {
- expect(() => currencyAmountFromCents(10.5)).toThrow("cents must be an integer number");
- expect(() => currencyAmountFromCents(NaN)).toThrow("cents must be an integer number");
+ it("returns error string if cents is not an integer", () => {
+ expect(currencyAmountFromCents(10.5)).toBe("!ERROR");
+ expect(currencyAmountFromCents(NaN)).toBe("!ERROR");
});
it("formats USD by default", () => {
@@ -153,6 +153,6 @@ describe("currencyAmountFromCents (integration, no mocks)", () => {
});
it("handles negative cents", () => {
- expect( () => currencyAmountFromCents(-123, "USD")).toThrow("cents must be non-negative.");
+ expect(currencyAmountFromCents(-123, "USD")).toBe("!ERROR");
});
});
diff --git a/src/utils/constants.js b/src/utils/constants.js
index 85e190fc..02d6d521 100644
--- a/src/utils/constants.js
+++ b/src/utils/constants.js
@@ -85,4 +85,12 @@ export const DATETIME_FORMAT = "MM/DD/YYYY hh:mm a";
export const SPONSOR_FORMS_METAFIELD_CLASS = {
FORM: "Form",
ITEM: "Item"
+};
+
+export const SPONSOR_ORDER_GRID_ITEM_TYPES = {
+ CHARGE: "Charge",
+ PAYMENT: "Payment",
+ REFUND: "Refund",
+ DISCOUNT: "Discount",
+ CANCELLED: "Cancelled"
};
\ No newline at end of file
diff --git a/src/utils/money.js b/src/utils/money.js
index e4295cdf..7ec987a8 100644
--- a/src/utils/money.js
+++ b/src/utils/money.js
@@ -159,13 +159,22 @@ export function amountFromCents(cents) {
* @returns {string}
*/
export function currencyAmountFromCents(cents, currency = "USD") {
+ let result = "!ERROR";
+
if (typeof cents !== "number" || !Number.isInteger(cents)) {
- throw new Error("cents must be an integer number");
+ console.error("ERROR - currencyAmountFromCents: cents must be an integer number");
+ return result;
+ }
+
+ try {
+ const amount = amountFromCents(cents); // "12.34"
+ const symbol = CURRENCY_SYMBOL[currency] ?? "$";
+ result = `${symbol}${amount}`;
+ } catch (e) {
+ console.error(`ERROR - currencyAmountFromCents: ${e}`);
}
- const amount = amountFromCents(cents); // "12.34"
- const symbol = CURRENCY_SYMBOL[currency] ?? "$";
- return `${symbol}${amount}`;
+ return result;
}
/**