diff --git a/package.json b/package.json
index 6f9e36d..0e4c4f2 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "openstack-uicore-foundation",
- "version": "5.0.17",
+ "version": "5.0.20-beta.4",
"description": "ui reactjs components for openstack marketing site",
"main": "lib/openstack-uicore-foundation.js",
"scripts": {
diff --git a/src/components/index.js b/src/components/index.js
index 6e956bb..cc3dff0 100644
--- a/src/components/index.js
+++ b/src/components/index.js
@@ -76,6 +76,7 @@ export {useSnackbarMessage} from './mui/SnackbarNotification/Context'
export {default as MuiInfiniteTable} from './mui/infinite-table'
export {default as MuiEditableTable} from './mui/editable-table/mui-table-editable'
export {default as MuiTable} from './mui/table/mui-table'
+export {default as MuiSponsorOrderGrid} from './mui/SponsorOrderGrid'
export {TotalRow as MuiTotalRow, NotesRow as MuiNotesRow, FeeRow as MuiFeeRow, PaymentRow as MuiPaymentRow, RefundRow as MuiRefundRow, DiscountRow as MuiDiscountRow} from './mui/table/extra-rows'
export {default as MuiFormikAsyncSelect} from './mui/formik-inputs/mui-formik-async-select'
export {default as MuiFormikCheckboxGroup} from './mui/formik-inputs/mui-formik-checkbox-group'
@@ -115,6 +116,7 @@ export {default as MuiOrderSummary} from './mui/OrderSummary'
export {default as MuiStatusChip} from './mui/StatusChip'
export {default as MuiUploadBtn} from './mui/UploadBtn'
export {default as MuiUploadDialog} from './mui/UploadDialog'
+export {default as MuiInfoNote} from './mui/InfoNote'
// these include 3rd party deps
// export {default as ExtraQuestionsForm } from './extra-questions/index.js';
diff --git a/src/components/mui/FormItemTable/index.js b/src/components/mui/FormItemTable/index.js
index 98f77f5..468789c 100644
--- a/src/components/mui/FormItemTable/index.js
+++ b/src/components/mui/FormItemTable/index.js
@@ -91,7 +91,9 @@ const FormItemTable = ({
(mf) => mf.class_field === "Item" && mf.is_required
);
const hasMissingFields = requiredFields.some((mf) => {
- const value = values[`i-${row.form_item_id}-c-item-f-${mf.type_id}`];
+ const value = values[`i-${row.form_item_id}-c-Item-f-${mf.type_id}`];
+ if (mf.type === "CheckBoxList") return !Array.isArray(value) || value.length === 0;
+ if (mf.type === "CheckBox") return value !== true;
return value === undefined || value === null || value === "";
});
diff --git a/src/components/mui/InfoNote/index.jsx b/src/components/mui/InfoNote/index.jsx
new file mode 100644
index 0000000..413735f
--- /dev/null
+++ b/src/components/mui/InfoNote/index.jsx
@@ -0,0 +1,17 @@
+import React from "react";
+import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";
+import Typography from "@mui/material/Typography";
+import Box from "@mui/material/Box";
+
+const InfoNote = ({ message, sx }) => (
+
+
+
+ {message}
+
+
+);
+
+export default InfoNote;
diff --git a/src/components/mui/SponsorOrderGrid/__tests__/SponsorOrderGrid.test.js b/src/components/mui/SponsorOrderGrid/__tests__/SponsorOrderGrid.test.js
new file mode 100644
index 0000000..763dde3
--- /dev/null
+++ b/src/components/mui/SponsorOrderGrid/__tests__/SponsorOrderGrid.test.js
@@ -0,0 +1,171 @@
+/**
+ * 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)}`
+}));
+
+jest.mock("../../../../utils/constants", () => ({
+ SPONSOR_FORMS_METAFIELD_CLASS: { FORM: "Form", ITEM: "Item" }
+}));
+
+import React from "react";
+import { render, screen, fireEvent } from "@testing-library/react";
+import "@testing-library/jest-dom";
+import SponsorOrderGrid from "../index";
+
+const makeItem = (overrides = {}) => ({
+ line_id: 1,
+ quantity: 1,
+ amount: 10000,
+ current_rate: 5000,
+ canceled_by_id: null,
+ type: { name: "Booth" },
+ meta_fields: [],
+ ...overrides
+});
+
+const makeForm = (overrides = {}) => ({
+ id: 10,
+ code: "GOLD",
+ name: "Gold Sponsor",
+ addon_name: "Premium",
+ discount: null,
+ discount_total: null,
+ items: [makeItem()],
+ ...overrides
+});
+
+const defaultProps = {
+ lines: [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.details")).toBeInTheDocument();
+ expect(screen.getByText("sponsor_order_grid.rate")).toBeInTheDocument();
+ expect(screen.getByText("sponsor_order_grid.amount")).toBeInTheDocument();
+ });
+
+ test("renders item code and name", () => {
+ render();
+ expect(screen.getByText("GOLD")).toBeInTheDocument();
+ expect(screen.getByText("Gold Sponsor")).toBeInTheDocument();
+ });
+
+ test("renders formatted amount and rate", () => {
+ render();
+ expect(screen.getByText("$100.00")).toBeInTheDocument();
+ expect(screen.getByText("$50.00")).toBeInTheDocument();
+ });
+
+ test("renders no-items message when lines is empty", () => {
+ render();
+ expect(screen.getByText("mui_table.no_items")).toBeInTheDocument();
+ });
+
+ test("renders no-items message when lines 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();
+ 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();
+ });
+
+ test("renders action column header when both callbacks are provided", () => {
+ render(
+
+ );
+ expect(screen.getByText("sponsor_order_grid.action")).toBeInTheDocument();
+ });
+
+ test("renders delete button for active item and calls onCancelForm on click", () => {
+ const onCancelForm = jest.fn();
+ render(
+
+ );
+ const deleteButton = screen.getByTestId
+ ? document.querySelector('[data-testid="DeleteIcon"]')
+ : null;
+ const button = document.querySelector("button[aria-label]") || 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 })] })];
+ render(
+
+ );
+ const button = document.querySelector("tbody button");
+ fireEvent.click(button);
+ 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 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();
+ });
+});
diff --git a/src/components/mui/SponsorOrderGrid/index.js b/src/components/mui/SponsorOrderGrid/index.js
new file mode 100644
index 0000000..adf695a
--- /dev/null
+++ b/src/components/mui/SponsorOrderGrid/index.js
@@ -0,0 +1,270 @@
+/**
+ * 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 {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 {currencyAmountFromCents} from "../../../utils/money";
+import {SPONSOR_FORMS_METAFIELD_CLASS} from "../../../utils/constants";
+
+const mapOrderData = (forms, showItemDescription) => {
+ if (!forms) return [];
+
+ return forms.map((form) => ({
+ ...form,
+ items: form.items
+ .filter((it) => it.quantity)
+ .map((it) => {
+ const formMetaFields = it.meta_fields.filter(
+ (mf) => mf.class_field === SPONSOR_FORMS_METAFIELD_CLASS.FORM
+ );
+
+ const itemDetails = [it.type?.name];
+
+ // item details
+ if (showItemDescription) {
+ itemDetails.push(
+ ...formMetaFields.map((mf) => {
+ const val =
+ mf.values?.length > 0
+ ? mf.values.find((v) => v.id === mf.current_value)?.name
+ : mf.current_value;
+ return (
+
+ {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);
+
+ return {
+ id: lineId,
+ code: form.code,
+ name: form.name,
+ rate,
+ addon_name: form.addon_name,
+ item_name: itemDetails,
+ amount,
+ cancelled
+ };
+ })
+ }));
+};
+
+const SponsorOrderGrid = ({
+ lines,
+ notes,
+ payments,
+ refunds,
+ fees,
+ total,
+ amountDue,
+ withDescription = false,
+ onCancelForm,
+ onUndoCancelForm
+ }) => {
+ const data = mapOrderData(lines, withDescription);
+ const showActionCol = onCancelForm && onUndoCancelForm;
+ const trailingCols = showActionCol ? 1 : 0;
+
+ const columns = [
+ {
+ columnKey: "code",
+ header: T.translate("sponsor_order_grid.code")
+ },
+ {
+ columnKey: "name",
+ header: T.translate("sponsor_order_grid.contents")
+ },
+ {
+ 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: "amount",
+ header: T.translate("sponsor_order_grid.amount")
+ }
+ ];
+
+ 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)}>
+
+
+ );
+ }
+ });
+ }
+
+ return (
+
+
+
+
+ {/* TABLE HEADER */}
+
+
+ {columns.map((col) => (
+
+ {col.header}
+
+ ))}
+
+
+
+ {data.map((form) => {
+ const rows = form.items.map((row) => (
+
+ {columns.map((col) => (
+
+ {col.render ? (
+ col.render(row)
+ ) : (
+
+ {row[col.columnKey]}
+
+ )}
+
+ ))}
+
+ ));
+
+ rows.push(
+
+ );
+
+ return rows;
+ })}
+ {fees &&
+ fees.map((fee) => (
+
+ ))}
+ {refunds &&
+ refunds.map((refund) => (
+
+ ))}
+ {payments &&
+ payments.map((payment) => (
+
+ ))}
+ {notes &&
+ notes.map((note) => (
+
+ ))}
+
+
+ {data.length === 0 && (
+
+
+ {T.translate("mui_table.no_items")}
+
+
+ )}
+
+
+
+
+
+ );
+};
+
+export default SponsorOrderGrid;
diff --git a/src/i18n/en.json b/src/i18n/en.json
index a6ccb50..ff5d5eb 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -121,5 +121,17 @@
"button_cta": "PAY {amount}",
"title": "Select a payment method",
"payment_confirmation_error": "Payment confirmation failed"
+ },
+ "sponsor_order_grid": {
+ "code": "Code",
+ "contents": "Contents",
+ "addon": "Add-on",
+ "details": "Details",
+ "discount": "Discount",
+ "amount": "Amount",
+ "amount_due": "AMOUNT DUE",
+ "total": "Total",
+ "rate": "Rate",
+ "action": "Action"
}
}
diff --git a/src/utils/constants.js b/src/utils/constants.js
index e7dbe3e..85e190f 100644
--- a/src/utils/constants.js
+++ b/src/utils/constants.js
@@ -3,6 +3,7 @@ export const TWO_DECIMAL_PLACES = 2;
export const THREE_DECIMAL_PLACES = 3;
export const ONE_CENT = 1n;
export const ZERO_INT = 0;
+export const BPS = 100;
export const CODE_200 = 200;
@@ -79,4 +80,9 @@ export const FILE_UPLOAD_STATUS_BKGR_COLOR = {
};
export const DATE_FORMAT = "MM/DD/YYYY";
-export const DATETIME_FORMAT = "MM/DD/YYYY hh:mm a";
\ No newline at end of file
+export const DATETIME_FORMAT = "MM/DD/YYYY hh:mm a";
+
+export const SPONSOR_FORMS_METAFIELD_CLASS = {
+ FORM: "Form",
+ ITEM: "Item"
+};
\ No newline at end of file
diff --git a/src/utils/money.js b/src/utils/money.js
index 2197534..e4295cd 100644
--- a/src/utils/money.js
+++ b/src/utils/money.js
@@ -12,24 +12,26 @@
* */
import {
- CENTS_FACTOR,
- THREE_DECIMAL_PLACES,
- TWO_DECIMAL_PLACES,
- ZERO_INT,
- ONE_CENT
+ BPS,
+ CENTS_FACTOR,
+ DISCOUNT_TYPES,
+ ONE_CENT,
+ THREE_DECIMAL_PLACES,
+ TWO_DECIMAL_PLACES,
+ ZERO_INT
} from "./constants";
const CURRENCY_SYMBOL = {
- USD: "$",
- EUR: "€",
- GBP: "£",
- CAD: "C$",
- AUD: "A$",
- NZD: "NZ$",
- CHF: "CHF",
- ARS: "AR$",
- BRL: "R$",
- MXN: "MX$",
+ USD: "$",
+ EUR: "€",
+ GBP: "£",
+ CAD: "C$",
+ AUD: "A$",
+ NZD: "NZ$",
+ CHF: "CHF",
+ ARS: "AR$",
+ BRL: "R$",
+ MXN: "MX$",
};
/**
@@ -54,54 +56,54 @@ const CURRENCY_SYMBOL = {
* - Throws on invalid formats (does not silently return 0).
*/
export function amountToCents(amount) {
- if (amount == null) throw new Error("amount is required");
-
- let s = String(amount).trim();
-
- // Normalize common separators:
- // - "1,234.56" => "1234.56" (remove thousands separators)
- // - "1234,56" => "1234.56" (convert decimal comma to dot)
- if (s.includes(",") && s.includes(".")) {
- s = s.replace(/,/g, "");
- } else if (s.includes(",") && !s.includes(".")) {
- s = s.replace(",", ".");
- }
-
- // Validate: digits optionally followed by '.' and more digits
- if (!/^\d+(\.\d+)?$/.test(s)) {
- throw new Error(`Invalid money format: "${amount}"`);
- }
-
- const [intPart, fracRaw = ""] = s.split(".");
-
- // Pad at least 3 fractional digits so we can:
- // - take 2 digits for cents
- // - take the 3rd digit to decide rounding
- const fracPadded = (`${fracRaw }000`);
-
- const tenths = fracPadded[0] ?? "0"; // 1st decimal digit
- const hundredths = fracPadded[1] ?? "0"; // 2nd decimal digit (cents)
- const thousandths = fracPadded[2] ?? "0"; // 3rd decimal digit (rounding decision)
-
- // If there are more than 3 decimals, we track if any non-zero exists after the 3rd.
- // This can matter for policies like bankers rounding; here it's mainly informational.
- const trailing =
- fracRaw.length > THREE_DECIMAL_PLACES
- ? fracRaw.slice(THREE_DECIMAL_PLACES)
- : "";
- const hasTrailingNonZero = /[1-9]/.test(trailing);
-
- // Build cents as integer: (dollars * 100) + (first two decimal digits)
- let cents = BigInt(intPart) * CENTS_FACTOR + BigInt(tenths + hundredths);
-
- // Half-up rounding:
- // - If the 3rd digit is >= 5, round up by 1 cent.
- // - If there are more digits beyond the 3rd, "5xxx" should also round up.
- const roundUp = thousandths > "5" || thousandths === "5" || (thousandths === "5" && hasTrailingNonZero);
-
- if (roundUp) cents += ONE_CENT;
-
- return Number(cents);
+ if (amount == null) throw new Error("amount is required");
+
+ let s = String(amount).trim();
+
+ // Normalize common separators:
+ // - "1,234.56" => "1234.56" (remove thousands separators)
+ // - "1234,56" => "1234.56" (convert decimal comma to dot)
+ if (s.includes(",") && s.includes(".")) {
+ s = s.replace(/,/g, "");
+ } else if (s.includes(",") && !s.includes(".")) {
+ s = s.replace(",", ".");
+ }
+
+ // Validate: digits optionally followed by '.' and more digits
+ if (!/^\d+(\.\d+)?$/.test(s)) {
+ throw new Error(`Invalid money format: "${amount}"`);
+ }
+
+ const [intPart, fracRaw = ""] = s.split(".");
+
+ // Pad at least 3 fractional digits so we can:
+ // - take 2 digits for cents
+ // - take the 3rd digit to decide rounding
+ const fracPadded = (`${fracRaw}000`);
+
+ const tenths = fracPadded[0] ?? "0"; // 1st decimal digit
+ const hundredths = fracPadded[1] ?? "0"; // 2nd decimal digit (cents)
+ const thousandths = fracPadded[2] ?? "0"; // 3rd decimal digit (rounding decision)
+
+ // If there are more than 3 decimals, we track if any non-zero exists after the 3rd.
+ // This can matter for policies like bankers rounding; here it's mainly informational.
+ const trailing =
+ fracRaw.length > THREE_DECIMAL_PLACES
+ ? fracRaw.slice(THREE_DECIMAL_PLACES)
+ : "";
+ const hasTrailingNonZero = /[1-9]/.test(trailing);
+
+ // Build cents as integer: (dollars * 100) + (first two decimal digits)
+ let cents = BigInt(intPart) * CENTS_FACTOR + BigInt(tenths + hundredths);
+
+ // Half-up rounding:
+ // - If the 3rd digit is >= 5, round up by 1 cent.
+ // - If there are more digits beyond the 3rd, "5xxx" should also round up.
+ const roundUp = thousandths > "5" || thousandths === "5" || (thousandths === "5" && hasTrailingNonZero);
+
+ if (roundUp) cents += ONE_CENT;
+
+ return Number(cents);
}
/**
@@ -119,36 +121,36 @@ export function amountToCents(amount) {
* - Always returns a string with exactly 2 decimal places.
*/
export function amountFromCents(cents) {
- let c;
-
- // Normalize input to BigInt safely
- if (typeof cents === "bigint") {
- c = cents;
- } else if (typeof cents === "number") {
- // Ensure it's a safe integer before converting to BigInt
- if (!Number.isSafeInteger(cents)) {
- throw new Error("cents must be a safe integer Number (or pass BigInt/string).");
- }
- c = BigInt(cents);
- } else if (typeof cents === "string") {
- const s = cents.trim();
- if (!/^\d+$/.test(s)) {
- throw new Error("cents string must contain digits only (e.g., '1234').");
- }
- c = BigInt(s);
- } else {
- throw new Error("cents must be a bigint, number, or numeric string.");
+ let c;
+
+ // Normalize input to BigInt safely
+ if (typeof cents === "bigint") {
+ c = cents;
+ } else if (typeof cents === "number") {
+ // Ensure it's a safe integer before converting to BigInt
+ if (!Number.isSafeInteger(cents)) {
+ throw new Error("cents must be a safe integer Number (or pass BigInt/string).");
}
-
- if (c < ZERO_INT) {
- throw new Error("cents must be non-negative.");
+ c = BigInt(cents);
+ } else if (typeof cents === "string") {
+ const s = cents.trim();
+ if (!/^\d+$/.test(s)) {
+ throw new Error("cents string must contain digits only (e.g., '1234').");
}
+ c = BigInt(s);
+ } else {
+ throw new Error("cents must be a bigint, number, or numeric string.");
+ }
+
+ if (c < ZERO_INT) {
+ throw new Error("cents must be non-negative.");
+ }
- const amount = c / CENTS_FACTOR;
- const remainder = c % CENTS_FACTOR;
+ const amount = c / CENTS_FACTOR;
+ const remainder = c % CENTS_FACTOR;
- // Always pad remainder to 2 digits
- return `${amount.toString()}.${remainder.toString().padStart(TWO_DECIMAL_PLACES, "0")}`;
+ // Always pad remainder to 2 digits
+ return `${amount.toString()}.${remainder.toString().padStart(TWO_DECIMAL_PLACES, "0")}`;
}
/**
@@ -156,14 +158,14 @@ export function amountFromCents(cents) {
* @param currency
* @returns {string}
*/
-export function currencyAmountFromCents(cents, currency = "USD"){
- if (typeof cents !== "number" || !Number.isInteger(cents)) {
- throw new Error("cents must be an integer number");
- }
-
- const amount = amountFromCents(cents); // "12.34"
- const symbol = CURRENCY_SYMBOL[currency] ?? "$";
- return `${symbol}${amount}`;
+export function currencyAmountFromCents(cents, currency = "USD") {
+ if (typeof cents !== "number" || !Number.isInteger(cents)) {
+ throw new Error("cents must be an integer number");
+ }
+
+ const amount = amountFromCents(cents); // "12.34"
+ const symbol = CURRENCY_SYMBOL[currency] ?? "$";
+ return `${symbol}${amount}`;
}
/**
@@ -172,17 +174,29 @@ export function currencyAmountFromCents(cents, currency = "USD"){
* @returns {number} - The amount converted to cents (e.g., 30).
*/
export const parsePrice = (priceString) => {
- if (priceString == null) throw new Error("priceString is required");
+ if (priceString == null) throw new Error("priceString is required");
- let s = String(priceString).trim();
+ let s = String(priceString).trim();
- // Reject negatives explicitly (per your requirement).
- if (s.includes("-")) throw new Error("Negative amounts are not allowed");
+ // Reject negatives explicitly (per your requirement).
+ if (s.includes("-")) throw new Error("Negative amounts are not allowed");
- // Keep only digits and separators. Remove currency symbols/letters/spaces.
- s = s.replace(/[^\d.,]/g, "");
- if (!s) throw new Error(`Invalid price: "${priceString}"`);
+ // Keep only digits and separators. Remove currency symbols/letters/spaces.
+ s = s.replace(/[^\d.,]/g, "");
+ if (!s) throw new Error(`Invalid price: "${priceString}"`);
- // Delegate exact cents conversion (no floats)
- return amountToCents(s); // <- your safe BigInt cents converter
+ // Delegate exact cents conversion (no floats)
+ return amountToCents(s); // <- your safe BigInt cents converter
};
+
+/**
+ * Formats a discount value in BPS or CENTS to string.
+ * @param {number} amount - The discount in BPS or CENTS(e.g., 300 = 3%).
+ * @param {string} type - The discount type: "Amount" or "Rate".
+ * @returns {string} - The discount converted to string (e.g., $5).
+ */
+export const formatDiscount = (amount, type) => {
+ if (type === DISCOUNT_TYPES.AMOUNT) return currencyAmountFromCents(amount);
+ if (type === DISCOUNT_TYPES.RATE) return `${amount / BPS}%`; // transform from bps to percentage
+ return "";
+};
\ No newline at end of file
diff --git a/webpack.common.js b/webpack.common.js
index e4966a9..eae3e60 100644
--- a/webpack.common.js
+++ b/webpack.common.js
@@ -140,10 +140,12 @@ module.exports = {
'components/mui/loading-overlay': './src/components/mui/LoadingOverlay/index.jsx',
'components/mui/nav-bar': './src/components/mui/NavBar/index.js',
'components/mui/order-summary': './src/components/mui/OrderSummary/index.jsx',
+ 'components/mui/sponsor-order-grid': './src/components/mui/SponsorOrderGrid/index.js',
'components/mui/status-chip': './src/components/mui/StatusChip/index.js',
'components/mui/stripe-payment': './src/components/mui/StripePayment/index.jsx',
'components/mui/upload-btn': './src/components/mui/UploadBtn/index.js',
'components/mui/upload-dialog': './src/components/mui/UploadDialog/index.js',
+ 'components/mui/info-note': './src/components/mui/InfoNote/index.jsx',
// models
'models/index': './src/models',