From e58e81fd2583acfaf091daff79937934c13fb0ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frane=20Poli=C4=87?= <16856471+fPolic@users.noreply.github.com> Date: Mon, 1 Apr 2024 10:55:42 +0200 Subject: [PATCH] feat(dashboard): admin 3.0 order refund form (#6850) --- .changeset/chilled-nails-walk.md | 5 + .../public/locales/en-US/translation.json | 13 + .../src/providers/router-provider/v1.tsx | 4 + .../order-payment-section.tsx | 8 +- .../routes/orders/order-email/order-email.tsx | 5 +- .../components/order-refund-form/index.ts | 1 + .../order-refund-form/order-refund-form.tsx | 262 ++++++++++++++++++ .../src/routes/orders/order-refund/index.ts | 1 + .../orders/order-refund/order-refund.tsx | 40 +++ .../src/routes/orders/order-refund/schema.ts | 18 ++ .../medusa/src/services/payment-provider.ts | 1 + 11 files changed, 355 insertions(+), 3 deletions(-) create mode 100644 .changeset/chilled-nails-walk.md create mode 100644 packages/admin-next/dashboard/src/routes/orders/order-refund/components/order-refund-form/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/orders/order-refund/components/order-refund-form/order-refund-form.tsx create mode 100644 packages/admin-next/dashboard/src/routes/orders/order-refund/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/orders/order-refund/order-refund.tsx create mode 100644 packages/admin-next/dashboard/src/routes/orders/order-refund/schema.ts diff --git a/.changeset/chilled-nails-walk.md b/.changeset/chilled-nails-walk.md new file mode 100644 index 0000000000..8916146cbb --- /dev/null +++ b/.changeset/chilled-nails-walk.md @@ -0,0 +1,5 @@ +--- +"@medusajs/medusa": patch +--- + +fix(medusa): associate refund with order when calling `refundFromPayment` diff --git a/packages/admin-next/dashboard/public/locales/en-US/translation.json b/packages/admin-next/dashboard/public/locales/en-US/translation.json index 457885be4e..e1c332957a 100644 --- a/packages/admin-next/dashboard/public/locales/en-US/translation.json +++ b/packages/admin-next/dashboard/public/locales/en-US/translation.json @@ -56,6 +56,7 @@ "remove": "Remove", "revoke": "Revoke", "cancel": "Cancel", + "complete": "Complete", "back": "Back", "continue": "Continue", "confirm": "Confirm", @@ -330,6 +331,17 @@ "shippingFromLabel": "Shipping from", "itemsLabel": "Items" }, + "refund": { + "title": "Create Refund", + "sendNotificationHint": "Notify customers about the created refund.", + "systemPayment": "System payment", + "systemPaymentDesc": "One or more of your payments is a system payment. Be aware, that captures and refunds are not handled by Medusa for such payments.", + "error": { + "amountToLarge": "Cannot refund more than the original order amount.", + "amountNegative": "Refund amount must be a positive number.", + "reasonRequired": "Please select a refund reason." + } + }, "customer": { "contactLabel": "Contact", "editEmail": "Edit email", @@ -819,6 +831,7 @@ }, "fields": { "amount": "Amount", + "refundAmount": "Refund amount", "name": "Name", "lastName": "Last Name", "firstName": "First Name", diff --git a/packages/admin-next/dashboard/src/providers/router-provider/v1.tsx b/packages/admin-next/dashboard/src/providers/router-provider/v1.tsx index 0774208109..749b6d720d 100644 --- a/packages/admin-next/dashboard/src/providers/router-provider/v1.tsx +++ b/packages/admin-next/dashboard/src/providers/router-provider/v1.tsx @@ -129,6 +129,10 @@ export const v1Routes: RouteObject[] = [ lazy: () => import("../../routes/orders/order-create-return"), }, + { + path: "refund", + lazy: () => import("../../routes/orders/order-refund"), + }, ], }, ], diff --git a/packages/admin-next/dashboard/src/routes/orders/order-detail/components/order-payment-section/order-payment-section.tsx b/packages/admin-next/dashboard/src/routes/orders/order-detail/components/order-payment-section/order-payment-section.tsx index 071056dbf3..afc2d6e652 100644 --- a/packages/admin-next/dashboard/src/routes/orders/order-detail/components/order-payment-section/order-payment-section.tsx +++ b/packages/admin-next/dashboard/src/routes/orders/order-detail/components/order-payment-section/order-payment-section.tsx @@ -42,6 +42,8 @@ export const OrderPaymentSection = ({ order }: OrderPaymentSectionProps) => { const Header = ({ order }: { order: Order }) => { const { t } = useTranslation() + const hasCapturedPayment = order.payments.some((p) => !!p.captured_at) + return (
{t("orders.payment.title")} @@ -52,7 +54,8 @@ const Header = ({ order }: { order: Order }) => { { label: t("orders.payment.refund"), icon: , - to: "#", // TODO: Go to general refund modal + to: `/orders/${order.id}/refund`, + disabled: !hasCapturedPayment, }, ], }, @@ -164,7 +167,8 @@ const Payment = ({ { label: t("orders.payment.refund"), icon: , - to: "#", // TODO: Go to specific payment refund modal + to: `/orders/${payment.order_id}/refund?paymentId=${payment.id}`, + disabled: !payment.captured_at, }, ], }, diff --git a/packages/admin-next/dashboard/src/routes/orders/order-email/order-email.tsx b/packages/admin-next/dashboard/src/routes/orders/order-email/order-email.tsx index 2d03a5a23b..33c1279a05 100644 --- a/packages/admin-next/dashboard/src/routes/orders/order-email/order-email.tsx +++ b/packages/admin-next/dashboard/src/routes/orders/order-email/order-email.tsx @@ -4,12 +4,15 @@ import { useTranslation } from "react-i18next" import { useParams } from "react-router-dom" import { RouteDrawer } from "../../../components/route-modal" import { EditOrderEmailForm } from "./components/edit-order-email-form" +import { orderExpand } from "../order-detail/constants" export const OrderEmail = () => { const { id } = useParams() const { t } = useTranslation() - const { order, isLoading, isError, error } = useAdminOrder(id!) + const { order, isLoading, isError, error } = useAdminOrder(id!, { + expand: orderExpand, + }) const ready = !isLoading && order diff --git a/packages/admin-next/dashboard/src/routes/orders/order-refund/components/order-refund-form/index.ts b/packages/admin-next/dashboard/src/routes/orders/order-refund/components/order-refund-form/index.ts new file mode 100644 index 0000000000..4d51d6a0e7 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/orders/order-refund/components/order-refund-form/index.ts @@ -0,0 +1 @@ +export * from "./order-refund-form" diff --git a/packages/admin-next/dashboard/src/routes/orders/order-refund/components/order-refund-form/order-refund-form.tsx b/packages/admin-next/dashboard/src/routes/orders/order-refund/components/order-refund-form/order-refund-form.tsx new file mode 100644 index 0000000000..e097905950 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/orders/order-refund/components/order-refund-form/order-refund-form.tsx @@ -0,0 +1,262 @@ +import React, { useMemo } from "react" +import { Order, Payment } from "@medusajs/medusa" +import { + RouteDrawer, + useRouteModal, +} from "../../../../../components/route-modal" +import { + Alert, + Button, + CurrencyInput, + Select, + Switch, + Text, + Textarea, +} from "@medusajs/ui" +import { useTranslation } from "react-i18next" +import { useForm } from "react-hook-form" +import { + adminOrderKeys, + useAdminPaymentsRefundPayment, + useAdminRefundPayment, +} from "medusa-react" +import { z } from "zod" +import { zodResolver } from "@hookform/resolvers/zod" + +import { CreateRefundSchema } from "../../schema" +import { castNumber } from "../../../../../lib/cast-number" +import { Form } from "../../../../../components/common/form" +import { getCurrencySymbol } from "../../../../../lib/currencies" +import { queryClient } from "../../../../../lib/medusa" +import { + getDbAmount, + getPresentationalAmount, +} from "../../../../../lib/money-amount-helpers" + +const reasonOptions = [ + { label: "Discount", value: "discount" }, + { label: "Other", value: "other" }, +] + +type OrderRefundFormProps = { + order: Order + payment?: Payment +} + +export function OrderRefundForm({ order, payment }: OrderRefundFormProps) { + const { t } = useTranslation() + const { handleSuccess } = useRouteModal() + + const isSpecificPaymentRefund = !!payment + + const refundable = useMemo(() => { + return order.paid_total - order.refunded_total + }, [order]) + + const form = useForm>({ + defaultValues: { + amount: isSpecificPaymentRefund + ? getPresentationalAmount(payment!.amount, order.currency_code) + : "", + note: "", + reason: "", + notification_enabled: !order.no_notification, + }, + resolver: zodResolver(CreateRefundSchema), + }) + + let { mutateAsync: createOrderRefund, isLoading: isOrderRefundLoading } = + useAdminRefundPayment(order.id) + let { mutateAsync: createPaymentRefund, isLoading: isPaymentRefundLoading } = + useAdminPaymentsRefundPayment(payment?.id) + + const handleSubmit = form.handleSubmit(async (values) => { + if (values.amount === "" || values.amount === undefined) { + form.setError("amount", { + type: "manual", + message: t("Please enter an amount for refund"), + }) + return + } + + const amount = castNumber(values.amount) + const dbAmount = getDbAmount(amount, order.currency_code) + + if (dbAmount > refundable) { + form.setError("amount", { + type: "manual", + message: t("orders.refund.error.amountToLarge"), + }) + return + } + + if (amount < 0) { + form.setError("amount", { + type: "manual", + message: t("orders.refund.error.amountNegative"), + }) + return + } + + const payload = { + amount: dbAmount, + note: values.note, + reason: values.reason, + no_notification: !values.notification_enabled, + } + + const mutate = isSpecificPaymentRefund + ? createPaymentRefund + : createOrderRefund + + if (isSpecificPaymentRefund) { + delete payload.no_notification + } + + await mutate(payload) + + if (isSpecificPaymentRefund) { + await queryClient.invalidateQueries(adminOrderKeys.detail(order.id)) + } + + handleSuccess() + }) + + const isSystemPayment = order.payments.some((p) => p.provider_id === "system") + + return ( + +
+ +
+ { + return ( + + {t("fields.refundAmount")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.reason")} + + + + + + ) + }} + /> + + { + return ( + + {t("fields.note")} + +