feat(dashboard,core-flows): ability to refund payment post RMA flow (#8685)

This commit is contained in:
Riqwan Thamir authored and GitHub committed 2024-08-21 09:58:09 +02:00
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"),
}, },
@@ -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>
@@ -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>
) )
@@ -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,
}, },
], ],
@@ -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>
@@ -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(