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 (
+
+
+
+ )
+}
+
+export default OrderRefundForm
diff --git a/packages/admin-next/dashboard/src/routes/orders/order-refund/index.ts b/packages/admin-next/dashboard/src/routes/orders/order-refund/index.ts
new file mode 100644
index 0000000000..e268a5492a
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/orders/order-refund/index.ts
@@ -0,0 +1 @@
+export { OrderRefund as Component } from "./order-refund"
diff --git a/packages/admin-next/dashboard/src/routes/orders/order-refund/order-refund.tsx b/packages/admin-next/dashboard/src/routes/orders/order-refund/order-refund.tsx
new file mode 100644
index 0000000000..354bbe4938
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/orders/order-refund/order-refund.tsx
@@ -0,0 +1,40 @@
+import { Heading } from "@medusajs/ui"
+import { useAdminOrder } from "medusa-react"
+import { useTranslation } from "react-i18next"
+import { useParams, useSearchParams } from "react-router-dom"
+
+import { RouteDrawer } from "../../../components/route-modal"
+import { OrderRefundForm } from "./components/order-refund-form"
+import { orderExpand } from "../order-detail/constants"
+
+export const OrderRefund = () => {
+ const { t } = useTranslation()
+
+ const { id } = useParams()
+ const [searchParams] = useSearchParams()
+
+ const paymentId = searchParams.get("paymentId")
+
+ const { order, isLoading, isError, error } = useAdminOrder(id!, {
+ expand: orderExpand,
+ })
+
+ const ready = !isLoading && order
+
+ if (isError) {
+ throw error
+ }
+
+ const payment = paymentId
+ ? order.payments.find((p) => p.id === paymentId)
+ : undefined
+
+ return (
+
+
+ {t("orders.refund.title")}
+
+ {ready && }
+
+ )
+}
diff --git a/packages/admin-next/dashboard/src/routes/orders/order-refund/schema.ts b/packages/admin-next/dashboard/src/routes/orders/order-refund/schema.ts
new file mode 100644
index 0000000000..737f9ab627
--- /dev/null
+++ b/packages/admin-next/dashboard/src/routes/orders/order-refund/schema.ts
@@ -0,0 +1,18 @@
+import { z } from "zod"
+import i18n from "i18next"
+
+export enum RefundReason {
+ Discount = "discount",
+ Other = "other",
+}
+
+export const CreateRefundSchema = z.object({
+ amount: z.union([z.number(), z.string()]),
+ reason: z.nativeEnum(RefundReason, {
+ errorMap: () => ({
+ message: i18n.t("orders.refund.error.reasonRequired"),
+ }),
+ }),
+ note: z.string().optional(),
+ notification_enabled: z.boolean().optional(),
+})
diff --git a/packages/medusa/src/services/payment-provider.ts b/packages/medusa/src/services/payment-provider.ts
index 8ec3c84762..a917838db9 100644
--- a/packages/medusa/src/services/payment-provider.ts
+++ b/packages/medusa/src/services/payment-provider.ts
@@ -809,6 +809,7 @@ export default class PaymentProviderService extends TransactionBaseService {
const toCreate = {
payment_id: payment.id,
+ order_id: payment.order_id,
amount,
reason,
note,