feat(dashboard,core-flows): ability to refund payment post RMA flow (#8685)
This commit is contained in:
1 parent
cc3e84f081
commit
a7d03ec562
7 files changed
+107
-27
No files matched your search
@@ -837,7 +837,8 @@
|
|||||||
"refundPaymentSuccess": "Refund of amount {{amount}} successful",
|
"refundPaymentSuccess": "Refund of amount {{amount}} successful",
|
||||||
"createRefundWrongQuantity": "Quantity should be a number between 1 and {{number}}",
|
"createRefundWrongQuantity": "Quantity should be a number between 1 and {{number}}",
|
||||||
"refundAmount": "Refund {{ amount }}",
|
"refundAmount": "Refund {{ amount }}",
|
||||||
"paymentLink": "Copy payment link for {{ amount }}"
|
"paymentLink": "Copy payment link for {{ amount }}",
|
||||||
|
"selectPaymentToRefund": "Select payment to refund"
|
||||||
},
|
},
|
||||||
|
|
||||||
"edits": {
|
"edits": {
|
||||||
|
|||||||
@@ -258,7 +258,7 @@ export const RouteMap: RouteObject[] = [
|
|||||||
import("../../routes/orders/order-create-exchange"),
|
import("../../routes/orders/order-create-exchange"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "payments/:paymentId/refund",
|
path: "refund",
|
||||||
lazy: () =>
|
lazy: () =>
|
||||||
import("../../routes/orders/order-create-refund"),
|
import("../../routes/orders/order-create-refund"),
|
||||||
},
|
},
|
||||||
|
|||||||
+73
-10
@@ -1,19 +1,28 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { HttpTypes } from "@medusajs/types"
|
import { HttpTypes } from "@medusajs/types"
|
||||||
import { Button, CurrencyInput, Textarea, toast } from "@medusajs/ui"
|
import {
|
||||||
|
Button,
|
||||||
|
CurrencyInput,
|
||||||
|
Label,
|
||||||
|
Select,
|
||||||
|
Textarea,
|
||||||
|
toast,
|
||||||
|
} from "@medusajs/ui"
|
||||||
|
import { useEffect } from "react"
|
||||||
import { useForm } from "react-hook-form"
|
import { useForm } from "react-hook-form"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { useNavigate, useSearchParams } from "react-router-dom"
|
||||||
import * as zod from "zod"
|
import * as zod from "zod"
|
||||||
import { upperCaseFirst } from "../../../../../../../../core/utils/src/common/upper-case-first"
|
|
||||||
import { Form } from "../../../../../components/common/form"
|
import { Form } from "../../../../../components/common/form"
|
||||||
import { Combobox } from "../../../../../components/inputs/combobox"
|
|
||||||
import { RouteDrawer, useRouteModal } from "../../../../../components/modals"
|
import { RouteDrawer, useRouteModal } from "../../../../../components/modals"
|
||||||
import { useRefundPayment } from "../../../../../hooks/api"
|
import { useRefundPayment } from "../../../../../hooks/api"
|
||||||
import { getCurrencySymbol } from "../../../../../lib/data/currencies"
|
import { getCurrencySymbol } from "../../../../../lib/data/currencies"
|
||||||
import { formatCurrency } from "../../../../../lib/format-currency"
|
import { formatCurrency } from "../../../../../lib/format-currency"
|
||||||
|
import { getLocaleAmount } from "../../../../../lib/money-amount-helpers"
|
||||||
|
import { getPaymentsFromOrder } from "../../../order-detail/components/order-payment-section"
|
||||||
|
|
||||||
type CreateRefundFormProps = {
|
type CreateRefundFormProps = {
|
||||||
payment: HttpTypes.AdminPayment
|
order: HttpTypes.AdminOrder
|
||||||
refundReasons: HttpTypes.AdminRefundReason[]
|
refundReasons: HttpTypes.AdminRefundReason[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,12 +33,17 @@ const CreateRefundSchema = zod.object({
|
|||||||
})
|
})
|
||||||
|
|
||||||
export const CreateRefundForm = ({
|
export const CreateRefundForm = ({
|
||||||
payment,
|
order,
|
||||||
refundReasons,
|
refundReasons,
|
||||||
}: CreateRefundFormProps) => {
|
}: CreateRefundFormProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { handleSuccess } = useRouteModal()
|
const { handleSuccess } = useRouteModal()
|
||||||
const paymentAmount = payment.amount as unknown as number
|
const navigate = useNavigate()
|
||||||
|
const [searchParams] = useSearchParams()
|
||||||
|
const paymentId = searchParams.get("paymentId")
|
||||||
|
const payments = getPaymentsFromOrder(order)
|
||||||
|
const payment = payments.find((p) => p.id === paymentId)!
|
||||||
|
const paymentAmount = (payment?.amount || 0) as number
|
||||||
|
|
||||||
const form = useForm<zod.infer<typeof CreateRefundSchema>>({
|
const form = useForm<zod.infer<typeof CreateRefundSchema>>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -39,7 +53,21 @@ export const CreateRefundForm = ({
|
|||||||
resolver: zodResolver(CreateRefundSchema),
|
resolver: zodResolver(CreateRefundSchema),
|
||||||
})
|
})
|
||||||
|
|
||||||
const { mutateAsync, isPending } = useRefundPayment(payment.id)
|
useEffect(() => {
|
||||||
|
const pendingDifference = order.summary.pending_difference as number
|
||||||
|
const paymentAmount = (payment?.amount || 0) as number
|
||||||
|
const pendingAmount =
|
||||||
|
pendingDifference < 0
|
||||||
|
? Math.min(pendingDifference, paymentAmount)
|
||||||
|
: paymentAmount
|
||||||
|
|
||||||
|
const normalizedAmount =
|
||||||
|
pendingAmount < 0 ? pendingAmount * -1 : pendingAmount
|
||||||
|
|
||||||
|
form.setValue("amount", normalizedAmount as number)
|
||||||
|
}, [payment])
|
||||||
|
|
||||||
|
const { mutateAsync, isPending } = useRefundPayment(payment?.id!)
|
||||||
|
|
||||||
const handleSubmit = form.handleSubmit(async (data) => {
|
const handleSubmit = form.handleSubmit(async (data) => {
|
||||||
await mutateAsync(
|
await mutateAsync(
|
||||||
@@ -52,7 +80,7 @@ export const CreateRefundForm = ({
|
|||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success(
|
toast.success(
|
||||||
t("orders.payment.refundPaymentSuccess", {
|
t("orders.payment.refundPaymentSuccess", {
|
||||||
amount: formatCurrency(data.amount, payment.currency_code),
|
amount: formatCurrency(data.amount, payment?.currency_code!),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -70,6 +98,41 @@ export const CreateRefundForm = ({
|
|||||||
<form onSubmit={handleSubmit} className="flex flex-1 flex-col">
|
<form onSubmit={handleSubmit} className="flex flex-1 flex-col">
|
||||||
<RouteDrawer.Body>
|
<RouteDrawer.Body>
|
||||||
<div className="flex flex-col gap-y-4">
|
<div className="flex flex-col gap-y-4">
|
||||||
|
<Select
|
||||||
|
value={payment?.id}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
navigate(`/orders/${order.id}/refund?paymentId=${value}`, {
|
||||||
|
replace: true,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Label className="font-sans txt-compact-small font-medium mb-[-6px]">
|
||||||
|
{t("orders.payment.selectPaymentToRefund")}
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<Select.Trigger>
|
||||||
|
<Select.Value
|
||||||
|
placeholder={t("orders.payment.selectPaymentToRefund")}
|
||||||
|
/>
|
||||||
|
</Select.Trigger>
|
||||||
|
|
||||||
|
<Select.Content>
|
||||||
|
{payments.map((payment) => (
|
||||||
|
<Select.Item value={payment!.id} key={payment.id}>
|
||||||
|
<span>
|
||||||
|
{getLocaleAmount(
|
||||||
|
payment.amount as number,
|
||||||
|
payment.currency_code
|
||||||
|
)}
|
||||||
|
{" - "}
|
||||||
|
</span>
|
||||||
|
<span>{payment.provider_id}</span>
|
||||||
|
<span> - ({payment.id.replace("pay_", "")})</span>
|
||||||
|
</Select.Item>
|
||||||
|
))}
|
||||||
|
</Select.Content>
|
||||||
|
</Select>
|
||||||
|
|
||||||
<Form.Field
|
<Form.Field
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="amount"
|
name="amount"
|
||||||
@@ -109,8 +172,8 @@ export const CreateRefundForm = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
code={payment.currency_code}
|
code={order.currency_code}
|
||||||
symbol={getCurrencySymbol(payment.currency_code)}
|
symbol={getCurrencySymbol(order.currency_code)}
|
||||||
value={field.value}
|
value={field.value}
|
||||||
/>
|
/>
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
|
|||||||
+8
-8
@@ -2,13 +2,17 @@ import { Heading } from "@medusajs/ui"
|
|||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { useParams } from "react-router-dom"
|
import { useParams } from "react-router-dom"
|
||||||
import { RouteDrawer } from "../../../components/modals"
|
import { RouteDrawer } from "../../../components/modals"
|
||||||
import { usePayment, useRefundReasons } from "../../../hooks/api"
|
import { useOrder, useRefundReasons } from "../../../hooks/api"
|
||||||
|
import { DEFAULT_FIELDS } from "../order-detail/constants"
|
||||||
import { CreateRefundForm } from "./components/create-refund-form"
|
import { CreateRefundForm } from "./components/create-refund-form"
|
||||||
|
|
||||||
export const OrderCreateRefund = () => {
|
export const OrderCreateRefund = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const { payment, isLoading, isError, error } = usePayment(params.paymentId!)
|
const { order } = useOrder(params.id!, {
|
||||||
|
fields: DEFAULT_FIELDS,
|
||||||
|
})
|
||||||
|
|
||||||
const {
|
const {
|
||||||
refund_reasons: refundReasons,
|
refund_reasons: refundReasons,
|
||||||
isLoading: isRefundReasonsLoading,
|
isLoading: isRefundReasonsLoading,
|
||||||
@@ -16,10 +20,6 @@ export const OrderCreateRefund = () => {
|
|||||||
error: refundReasonsError,
|
error: refundReasonsError,
|
||||||
} = useRefundReasons()
|
} = useRefundReasons()
|
||||||
|
|
||||||
if (isError) {
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isRefundReasonsError) {
|
if (isRefundReasonsError) {
|
||||||
throw refundReasonsError
|
throw refundReasonsError
|
||||||
}
|
}
|
||||||
@@ -30,8 +30,8 @@ export const OrderCreateRefund = () => {
|
|||||||
<Heading>{t("orders.payment.createRefund")}</Heading>
|
<Heading>{t("orders.payment.createRefund")}</Heading>
|
||||||
</RouteDrawer.Header>
|
</RouteDrawer.Header>
|
||||||
|
|
||||||
{!isLoading && !isRefundReasonsLoading && payment && refundReasons && (
|
{order && !isRefundReasonsLoading && refundReasons && (
|
||||||
<CreateRefundForm payment={payment} refundReasons={refundReasons} />
|
<CreateRefundForm order={order} refundReasons={refundReasons} />
|
||||||
)}
|
)}
|
||||||
</RouteDrawer>
|
</RouteDrawer>
|
||||||
)
|
)
|
||||||
|
|||||||
+3
-3
@@ -31,11 +31,11 @@ type OrderPaymentSectionProps = {
|
|||||||
order: HttpTypes.AdminOrder
|
order: HttpTypes.AdminOrder
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPaymentsFromOrder = (order: HttpTypes.AdminOrder) => {
|
export const getPaymentsFromOrder = (order: HttpTypes.AdminOrder) => {
|
||||||
return order.payment_collections
|
return order.payment_collections
|
||||||
.map((collection: HttpTypes.AdminPaymentCollection) => collection.payments)
|
.map((collection: HttpTypes.AdminPaymentCollection) => collection.payments)
|
||||||
.flat(1)
|
.flat(1)
|
||||||
.filter(Boolean)
|
.filter(Boolean) as HttpTypes.AdminPayment[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const OrderPaymentSection = ({ order }: OrderPaymentSectionProps) => {
|
export const OrderPaymentSection = ({ order }: OrderPaymentSectionProps) => {
|
||||||
@@ -222,7 +222,7 @@ const Payment = ({
|
|||||||
{
|
{
|
||||||
label: t("orders.payment.refund"),
|
label: t("orders.payment.refund"),
|
||||||
icon: <XCircle />,
|
icon: <XCircle />,
|
||||||
to: `/orders/${order.id}/payments/${payment.id}/refund`,
|
to: `/orders/${order.id}/refund?paymentId=${payment.id}`,
|
||||||
disabled: !payment.captured_at,
|
disabled: !payment.captured_at,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
+15
@@ -212,6 +212,21 @@ export const OrderSummarySection = ({ order }: OrderSummarySectionProps) => {
|
|||||||
{t("orders.payment.markAsPaid")}
|
{t("orders.payment.markAsPaid")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{showRefund && (
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => navigate(`/orders/${order.id}/refund`)}
|
||||||
|
>
|
||||||
|
{t("orders.payment.refundAmount", {
|
||||||
|
amount: getStylizedAmount(
|
||||||
|
(order?.summary?.pending_difference || 0) * -1,
|
||||||
|
order?.currency_code
|
||||||
|
),
|
||||||
|
})}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Container>
|
</Container>
|
||||||
|
|||||||
+5
-4
@@ -57,17 +57,18 @@ export const createOrUpdateOrderPaymentCollectionWorkflow = createWorkflow(
|
|||||||
}).config({ name: "payment-collection-query" })
|
}).config({ name: "payment-collection-query" })
|
||||||
|
|
||||||
const amountPending = transform({ order, input }, ({ order, input }) => {
|
const amountPending = transform({ order, input }, ({ order, input }) => {
|
||||||
const pendingPayment =
|
const amountToCharge = input.amount ?? 0
|
||||||
|
const amountPending =
|
||||||
order.summary.raw_pending_difference ?? order.summary.pending_difference
|
order.summary.raw_pending_difference ?? order.summary.pending_difference
|
||||||
|
|
||||||
if (MathBN.gt(input.amount ?? 0, pendingPayment)) {
|
if (amountToCharge > 0 && MathBN.gt(amountToCharge, amountPending)) {
|
||||||
throw new MedusaError(
|
throw new MedusaError(
|
||||||
MedusaError.Types.NOT_ALLOWED,
|
MedusaError.Types.NOT_ALLOWED,
|
||||||
`Amount cannot be greater than ${pendingPayment}`
|
`Amount cannot be greater than ${amountPending}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return pendingPayment
|
return amountPending
|
||||||
})
|
})
|
||||||
|
|
||||||
const updatedPaymentCollections = when(
|
const updatedPaymentCollections = when(
|
||||||
|
|||||||
Reference in new issue
Block a user