feat(dashboard,core-flows): ability to refund payment post RMA flow (#8685)
This commit is contained in:
@@ -837,7 +837,8 @@
|
||||
"refundPaymentSuccess": "Refund of amount {{amount}} successful",
|
||||
"createRefundWrongQuantity": "Quantity should be a number between 1 and {{number}}",
|
||||
"refundAmount": "Refund {{ amount }}",
|
||||
"paymentLink": "Copy payment link for {{ amount }}"
|
||||
"paymentLink": "Copy payment link for {{ amount }}",
|
||||
"selectPaymentToRefund": "Select payment to refund"
|
||||
},
|
||||
|
||||
"edits": {
|
||||
|
||||
@@ -258,7 +258,7 @@ export const RouteMap: RouteObject[] = [
|
||||
import("../../routes/orders/order-create-exchange"),
|
||||
},
|
||||
{
|
||||
path: "payments/:paymentId/refund",
|
||||
path: "refund",
|
||||
lazy: () =>
|
||||
import("../../routes/orders/order-create-refund"),
|
||||
},
|
||||
|
||||
@@ -1,19 +1,28 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
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 { useTranslation } from "react-i18next"
|
||||
import { useNavigate, useSearchParams } from "react-router-dom"
|
||||
import * as zod from "zod"
|
||||
import { upperCaseFirst } from "../../../../../../../../core/utils/src/common/upper-case-first"
|
||||
import { Form } from "../../../../../components/common/form"
|
||||
import { Combobox } from "../../../../../components/inputs/combobox"
|
||||
import { RouteDrawer, useRouteModal } from "../../../../../components/modals"
|
||||
import { useRefundPayment } from "../../../../../hooks/api"
|
||||
import { getCurrencySymbol } from "../../../../../lib/data/currencies"
|
||||
import { formatCurrency } from "../../../../../lib/format-currency"
|
||||
import { getLocaleAmount } from "../../../../../lib/money-amount-helpers"
|
||||
import { getPaymentsFromOrder } from "../../../order-detail/components/order-payment-section"
|
||||
|
||||
type CreateRefundFormProps = {
|
||||
payment: HttpTypes.AdminPayment
|
||||
order: HttpTypes.AdminOrder
|
||||
refundReasons: HttpTypes.AdminRefundReason[]
|
||||
}
|
||||
|
||||
@@ -24,12 +33,17 @@ const CreateRefundSchema = zod.object({
|
||||
})
|
||||
|
||||
export const CreateRefundForm = ({
|
||||
payment,
|
||||
order,
|
||||
refundReasons,
|
||||
}: CreateRefundFormProps) => {
|
||||
const { t } = useTranslation()
|
||||
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>>({
|
||||
defaultValues: {
|
||||
@@ -39,7 +53,21 @@ export const CreateRefundForm = ({
|
||||
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) => {
|
||||
await mutateAsync(
|
||||
@@ -52,7 +80,7 @@ export const CreateRefundForm = ({
|
||||
onSuccess: () => {
|
||||
toast.success(
|
||||
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">
|
||||
<RouteDrawer.Body>
|
||||
<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
|
||||
control={form.control}
|
||||
name="amount"
|
||||
@@ -109,8 +172,8 @@ export const CreateRefundForm = ({
|
||||
}
|
||||
}
|
||||
}}
|
||||
code={payment.currency_code}
|
||||
symbol={getCurrencySymbol(payment.currency_code)}
|
||||
code={order.currency_code}
|
||||
symbol={getCurrencySymbol(order.currency_code)}
|
||||
value={field.value}
|
||||
/>
|
||||
</Form.Control>
|
||||
|
||||
@@ -2,13 +2,17 @@ import { Heading } from "@medusajs/ui"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useParams } from "react-router-dom"
|
||||
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"
|
||||
|
||||
export const OrderCreateRefund = () => {
|
||||
const { t } = useTranslation()
|
||||
const params = useParams()
|
||||
const { payment, isLoading, isError, error } = usePayment(params.paymentId!)
|
||||
const { order } = useOrder(params.id!, {
|
||||
fields: DEFAULT_FIELDS,
|
||||
})
|
||||
|
||||
const {
|
||||
refund_reasons: refundReasons,
|
||||
isLoading: isRefundReasonsLoading,
|
||||
@@ -16,10 +20,6 @@ export const OrderCreateRefund = () => {
|
||||
error: refundReasonsError,
|
||||
} = useRefundReasons()
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
if (isRefundReasonsError) {
|
||||
throw refundReasonsError
|
||||
}
|
||||
@@ -30,8 +30,8 @@ export const OrderCreateRefund = () => {
|
||||
<Heading>{t("orders.payment.createRefund")}</Heading>
|
||||
</RouteDrawer.Header>
|
||||
|
||||
{!isLoading && !isRefundReasonsLoading && payment && refundReasons && (
|
||||
<CreateRefundForm payment={payment} refundReasons={refundReasons} />
|
||||
{order && !isRefundReasonsLoading && refundReasons && (
|
||||
<CreateRefundForm order={order} refundReasons={refundReasons} />
|
||||
)}
|
||||
</RouteDrawer>
|
||||
)
|
||||
|
||||
@@ -31,11 +31,11 @@ type OrderPaymentSectionProps = {
|
||||
order: HttpTypes.AdminOrder
|
||||
}
|
||||
|
||||
const getPaymentsFromOrder = (order: HttpTypes.AdminOrder) => {
|
||||
export const getPaymentsFromOrder = (order: HttpTypes.AdminOrder) => {
|
||||
return order.payment_collections
|
||||
.map((collection: HttpTypes.AdminPaymentCollection) => collection.payments)
|
||||
.flat(1)
|
||||
.filter(Boolean)
|
||||
.filter(Boolean) as HttpTypes.AdminPayment[]
|
||||
}
|
||||
|
||||
export const OrderPaymentSection = ({ order }: OrderPaymentSectionProps) => {
|
||||
@@ -222,7 +222,7 @@ const Payment = ({
|
||||
{
|
||||
label: t("orders.payment.refund"),
|
||||
icon: <XCircle />,
|
||||
to: `/orders/${order.id}/payments/${payment.id}/refund`,
|
||||
to: `/orders/${order.id}/refund?paymentId=${payment.id}`,
|
||||
disabled: !payment.captured_at,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -212,6 +212,21 @@ export const OrderSummarySection = ({ order }: OrderSummarySectionProps) => {
|
||||
{t("orders.payment.markAsPaid")}
|
||||
</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>
|
||||
)}
|
||||
</Container>
|
||||
|
||||
@@ -57,17 +57,18 @@ export const createOrUpdateOrderPaymentCollectionWorkflow = createWorkflow(
|
||||
}).config({ name: "payment-collection-query" })
|
||||
|
||||
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
|
||||
|
||||
if (MathBN.gt(input.amount ?? 0, pendingPayment)) {
|
||||
if (amountToCharge > 0 && MathBN.gt(amountToCharge, amountPending)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
`Amount cannot be greater than ${pendingPayment}`
|
||||
`Amount cannot be greater than ${amountPending}`
|
||||
)
|
||||
}
|
||||
|
||||
return pendingPayment
|
||||
return amountPending
|
||||
})
|
||||
|
||||
const updatedPaymentCollections = when(
|
||||
|
||||
Reference in New Issue
Block a user