setAddressDialogOpen(false)}
+ onSubmit={onAddressSubmit}
+ address={address}
+ />
+ >
+ );
+};
+
+export default ClientCard;
diff --git a/src/components/mui/RefundForm/__tests__/refund-form.test.js b/src/components/mui/RefundForm/__tests__/refund-form.test.js
new file mode 100644
index 000000000..7827eb9af
--- /dev/null
+++ b/src/components/mui/RefundForm/__tests__/refund-form.test.js
@@ -0,0 +1,57 @@
+import React from "react";
+import { render, screen, act } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import "@testing-library/jest-dom";
+import RefundForm from "../index";
+
+jest.mock("i18n-react/dist/i18n-react", () => ({
+ __esModule: true,
+ default: { translate: (key) => key }
+}));
+
+describe("RefundForm", () => {
+ it("renders reason and amount fields and submit button", () => {
+ render();
+ expect(screen.getByLabelText(/refund_form\.reason/i)).toBeInTheDocument();
+ expect(
+ screen.getByRole("button", { name: /refund_form\.queue_refund/i })
+ ).toBeInTheDocument();
+ });
+
+ it("calls onSubmit with reason and amount in cents", async () => {
+ const onSubmit = jest.fn();
+ render();
+
+ const reasonField = screen.getByLabelText(/refund_form\.reason/i);
+ const amountField = screen.getByLabelText(/refund_form\.amount/i);
+ const submitButton = screen.getByRole("button", {
+ name: /refund_form\.queue_refund/i
+ });
+
+ await act(async () => {
+ await userEvent.type(reasonField, "Duplicate charge");
+ await userEvent.type(amountField, "10");
+ await userEvent.click(submitButton);
+ });
+
+ expect(onSubmit).toHaveBeenCalledWith(
+ expect.objectContaining({ reason: "Duplicate charge", amount: 1000 }),
+ expect.anything()
+ );
+ });
+
+ it("does not call onSubmit when reason is empty", async () => {
+ const onSubmit = jest.fn();
+ render();
+
+ const submitButton = screen.getByRole("button", {
+ name: /refund_form\.queue_refund/i
+ });
+
+ await act(async () => {
+ await userEvent.click(submitButton);
+ });
+
+ expect(onSubmit).not.toHaveBeenCalled();
+ });
+});
diff --git a/src/components/mui/RefundForm/index.jsx b/src/components/mui/RefundForm/index.jsx
new file mode 100644
index 000000000..b545207e3
--- /dev/null
+++ b/src/components/mui/RefundForm/index.jsx
@@ -0,0 +1,88 @@
+/**
+ * Copyright 2017 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 { FormikProvider, useFormik } from "formik";
+import * as yup from "yup";
+import { Box, Button, Grid2 } from "@mui/material";
+import MuiFormikTextField from "openstack-uicore-foundation/lib/components/mui/formik-inputs/textfield";
+import MuiFormikPriceField from "openstack-uicore-foundation/lib/components/mui/formik-inputs/price-field";
+import InfoNote from "openstack-uicore-foundation/lib/components/mui/info-note";
+
+const RefundForm = ({ onSubmit }) => {
+ const formik = useFormik({
+ initialValues: {
+ reason: "",
+ amount: 0
+ },
+ validationSchema: yup.object({
+ reason: yup
+ .string(T.translate("validation.string"))
+ .required(T.translate("validation.required")),
+ amount: yup
+ .number()
+ .typeError(T.translate("validation.number"))
+ .positive(T.translate("validation.positive"))
+ .required(T.translate("validation.required"))
+ }),
+ onSubmit,
+ validateOnChange: false,
+ enableReinitialize: true
+ });
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default RefundForm;
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 22c593037..45ba6e65c 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -54,6 +54,7 @@
"save": "Save",
"confirm": "Confirm",
"cancel": "Cancel",
+ "undo": "Undo",
"return": "Return",
"ingest": "Ingest",
"save_and_publish": "Save & Publish",
@@ -101,6 +102,7 @@
"number": "Must be a number.",
"non_negative": "Must be a non-negative number.",
"integer": "Must be a integer",
+ "positive": "Must be a positive number.",
"one_option_required": "Must have one option",
"two_decimals": "Max 2 decimal places",
"email": "Wrong email format.",
@@ -2551,6 +2553,8 @@
"add_form_to_cart": "Add Form to Cart",
"add_selected_form": "Add selected form",
"select_addon": "Select Add-on...",
+ "reopen_warning": "Are you sure you want to reopen the cart?",
+ "reopen": "Reopen Cart",
"edit_form": {
"code": "Code",
"description": "Description",
@@ -2604,7 +2608,25 @@
"amount": "Amount",
"details": "Details",
"purchases": "purchases",
- "status_updated": "Status updated successfully."
+ "status_updated": "Status updated successfully.",
+ "order_details": {
+ "order": "Order",
+ "purchase_date": "Purchase Date",
+ "purchased_by": "Purchased by",
+ "show": "Show",
+ "sponsor": "Sponsor",
+ "tier": "Tier",
+ "payment_type": "Payment Type",
+ "payment_status": "Payment Status",
+ "payment_date": "Payment Date",
+ "general_info": "General Info",
+ "payment_info": "Payment Info",
+ "issue_refund": "Issue a Refund",
+ "info_note": "Active order items can be canceled. Canceled items show an undo action to restore them. Refund and payment rows are display-only.",
+ "address_updated": "Address updated successfully",
+ "client_updated": "Client info updated successfully",
+ "order_refunded": "Refund issued successfully."
+ }
},
"mu_tab": {
"alert_info": "Here you can see the status of this Sponsor's Media Uploads. If an additional file upload is required, it must be requested from the specific page where it is needed.",
@@ -4134,5 +4156,30 @@
"resync_helper": "Use the sync icon to re-sync individual room folders to Dropbox.",
"resync_tooltip": "Re-sync Dropbox folder",
"resync_dispatched": "Room re-sync task has been dispatched."
+ },
+ "refund_form": {
+ "reason": "Reason for Refund",
+ "amount": "Amount",
+ "queue_refund": "Queue refund",
+ "info": "If the original payment was made via Stripe, the refund will be automatically queued and processed."
+ },
+ "client_card": {
+ "title": "Client & Address Details",
+ "client": "Client",
+ "address": "Address",
+ "edit_client": "Edit Client",
+ "edit_address": "Edit Address",
+ "company_name": "Company Name",
+ "contact_name": "Contact Name",
+ "contact_email": "Contact Email",
+ "contact_phone": "Contact Phone",
+ "line1": "Address Line 1",
+ "line2": "Address Line 2",
+ "postal_code": "Postal Code",
+ "city": "City",
+ "state": "State",
+ "country": "Country",
+ "save": "Save",
+ "cancel": "Cancel"
}
}
diff --git a/src/pages/sponsors/sponsor-page/tabDefs.js b/src/pages/sponsors/sponsor-page/tabDefs.js
index 1ccb7d88f..e06109643 100644
--- a/src/pages/sponsors/sponsor-page/tabDefs.js
+++ b/src/pages/sponsors/sponsor-page/tabDefs.js
@@ -12,6 +12,7 @@ import SponsorFormsManageItems from "./tabs/sponsor-forms-tab/components/manage-
import SponsorCartTab from "./tabs/sponsor-cart-tab";
import SponsorPurchasesTab from "./tabs/sponsor-purchases-tab";
import SponsorBadgeScans from "./tabs/sponsor-badge-scans";
+import SponsorOrderDetails from "./tabs/sponsor-purchases-tab/sponsor-order-details";
const SponsorFormsRoute = ({ match }) => (
@@ -32,6 +33,25 @@ const SponsorFormsRoute = ({ match }) => (
);
+const SponsorPurchasesRoute = ({ match }) => (
+
+
+
+
+
+
+
+);
+
export const SPONSOR_PAGE_TABS = [
{
labelKey: "edit_sponsor.tab.general",
@@ -77,8 +97,7 @@ export const SPONSOR_PAGE_TABS = [
{
labelKey: "edit_sponsor.tab.purchases",
path: "/purchases",
- exact: true,
- component: SponsorPurchasesTab,
+ component: SponsorPurchasesRoute,
accessRoute: ACCESS_ROUTES.ADMIN_SPONSORS
},
{
diff --git a/src/pages/sponsors/sponsor-page/tabs/sponsor-cart-tab/components/cart-view.js b/src/pages/sponsors/sponsor-page/tabs/sponsor-cart-tab/components/cart-view.js
index 875833845..1257df9c5 100644
--- a/src/pages/sponsors/sponsor-page/tabs/sponsor-cart-tab/components/cart-view.js
+++ b/src/pages/sponsors/sponsor-page/tabs/sponsor-cart-tab/components/cart-view.js
@@ -36,6 +36,7 @@ import {
getSponsorCart,
lockSponsorCartForm,
payWithInvoice,
+ reopenCart,
saveSponsorCartNote,
unlockSponsorCartForm
} from "../../../../../../actions/sponsor-cart-actions";
@@ -44,6 +45,7 @@ import {
SPONSOR_CART_NOTE_TYPES,
SPONSOR_CART_STATUS
} from "../../../../../../utils/constants";
+import showConfirmDialog from "../../../../../../components/mui/showConfirmDialog";
const CartView = ({
cart,
@@ -56,10 +58,12 @@ const CartView = ({
saveSponsorCartNote,
deleteSponsorCartNote,
checkoutCart,
+ reopenCart,
payWithInvoice
}) => {
const cartIsPendingPayment =
cart?.status === SPONSOR_CART_STATUS.PENDING_PAYMENT;
+ const cartIsCheckedOut = cart?.status === SPONSOR_CART_STATUS.CHECKED_OUT;
useEffect(() => {
getSponsorCart();
@@ -69,6 +73,20 @@ const CartView = ({
getSponsorCart(searchTerm);
};
+ const handleReopenCart = async () => {
+ const isConfirmed = await showConfirmDialog({
+ title: T.translate("general.are_you_sure"),
+ text: T.translate("edit_sponsor.cart_tab.reopen_warning"),
+ type: "warning",
+ confirmButtonColor: "#DD6B55",
+ confirmButtonText: T.translate("edit_sponsor.cart_tab.reopen")
+ });
+
+ if (isConfirmed) {
+ reopenCart();
+ }
+ };
+
const handleDelete = (itemId) => {
deleteSponsorCartForm(itemId);
};
@@ -180,12 +198,12 @@ const CartView = ({
mb: 2
}}
>
-
+
{cart && (
{cart?.forms.length} forms in Cart
)}
-
+
+ {(cartIsPendingPayment || cartIsCheckedOut) && (
+
+ )}