diff --git a/package.json b/package.json index dfe67991..ad48be5a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openstack-uicore-foundation", - "version": "5.0.32", + "version": "5.0.33-beta.1", "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..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; } /**