feat(dashboard,js-sdk,types): ability to add refund reason and note (#8466)
what: - adds ability to add refund reason and note to a refund <img width="524" alt="Screenshot 2024-08-06 at 13 06 59" src="https://github.com/user-attachments/assets/51537e9b-170b-4dd6-9de5-6bdea5e26822"> <img width="1090" alt="Screenshot 2024-08-06 at 12 57 18" src="https://github.com/user-attachments/assets/70bc84a4-5ebf-43e9-8416-370fd37ba615"> <img width="247" alt="Screenshot 2024-08-06 at 13 08 46" src="https://github.com/user-attachments/assets/b1dc1d83-7fb8-4af5-9a5b-fddb63ff1812">
This commit is contained in:
@@ -19,6 +19,7 @@ export * from "./product-types"
|
||||
export * from "./product-variants"
|
||||
export * from "./products"
|
||||
export * from "./promotions"
|
||||
export * from "./refund-reasons"
|
||||
export * from "./regions"
|
||||
export * from "./reservations"
|
||||
export * from "./sales-channels"
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { QueryKey, useQuery, UseQueryOptions } from "@tanstack/react-query"
|
||||
import { sdk } from "../../lib/client"
|
||||
import { queryKeysFactory } from "../../lib/query-key-factory"
|
||||
|
||||
const REFUND_REASON_QUERY_KEY = "refund-reason" as const
|
||||
export const paymentQueryKeys = queryKeysFactory(REFUND_REASON_QUERY_KEY)
|
||||
|
||||
export const useRefundReasons = (
|
||||
query?: HttpTypes.RefundReasonFilters,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
HttpTypes.RefundReasonsResponse,
|
||||
Error,
|
||||
HttpTypes.RefundReasonsResponse,
|
||||
QueryKey
|
||||
>,
|
||||
"queryKey" | "queryFn"
|
||||
>
|
||||
) => {
|
||||
const { data, ...rest } = useQuery({
|
||||
queryFn: () => sdk.admin.refundReason.list(query),
|
||||
queryKey: [],
|
||||
...options,
|
||||
})
|
||||
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
@@ -2336,6 +2336,7 @@
|
||||
"totalRedemptions": "Total Redemptions",
|
||||
"countries": "Countries",
|
||||
"paymentProviders": "Payment Providers",
|
||||
"refundReason": "Refund Reason",
|
||||
"fulfillmentProviders": "Fulfillment Providers",
|
||||
"fulfillmentProvider": "Fulfillment Provider",
|
||||
"providers": "Providers",
|
||||
|
||||
+55
-2
@@ -1,10 +1,12 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { Button, CurrencyInput, toast } from "@medusajs/ui"
|
||||
import { Button, CurrencyInput, Textarea, toast } from "@medusajs/ui"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { useTranslation } from "react-i18next"
|
||||
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"
|
||||
@@ -12,13 +14,19 @@ import { formatCurrency } from "../../../../../lib/format-currency"
|
||||
|
||||
type CreateRefundFormProps = {
|
||||
payment: HttpTypes.AdminPayment
|
||||
refundReasons: HttpTypes.AdminRefundReason[]
|
||||
}
|
||||
|
||||
const CreateRefundSchema = zod.object({
|
||||
amount: zod.number(),
|
||||
refund_reason_id: zod.string().nullish(),
|
||||
note: zod.string().optional(),
|
||||
})
|
||||
|
||||
export const CreateRefundForm = ({ payment }: CreateRefundFormProps) => {
|
||||
export const CreateRefundForm = ({
|
||||
payment,
|
||||
refundReasons,
|
||||
}: CreateRefundFormProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { handleSuccess } = useRouteModal()
|
||||
const paymentAmount = payment.amount as unknown as number
|
||||
@@ -26,6 +34,7 @@ export const CreateRefundForm = ({ payment }: CreateRefundFormProps) => {
|
||||
const form = useForm<zod.infer<typeof CreateRefundSchema>>({
|
||||
defaultValues: {
|
||||
amount: paymentAmount,
|
||||
note: "",
|
||||
},
|
||||
resolver: zodResolver(CreateRefundSchema),
|
||||
})
|
||||
@@ -36,6 +45,8 @@ export const CreateRefundForm = ({ payment }: CreateRefundFormProps) => {
|
||||
await mutateAsync(
|
||||
{
|
||||
amount: data.amount,
|
||||
refund_reason_id: data.refund_reason_id,
|
||||
note: data.note,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
@@ -109,6 +120,48 @@ export const CreateRefundForm = ({ payment }: CreateRefundFormProps) => {
|
||||
)
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* TODO: Bring this back when we have a refund reason management UI */}
|
||||
{/* <Form.Field
|
||||
control={form.control}
|
||||
name="refund_reason_id"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>{t("fields.refundReason")}</Form.Label>
|
||||
|
||||
<Form.Control>
|
||||
<Combobox
|
||||
{...field}
|
||||
options={refundReasons.map((pp) => ({
|
||||
label: upperCaseFirst(pp.label),
|
||||
value: pp.id,
|
||||
}))}
|
||||
/>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/> */}
|
||||
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name={`note`}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>{t("fields.note")}</Form.Label>
|
||||
|
||||
<Form.Control>
|
||||
<Textarea {...field} />
|
||||
</Form.Control>
|
||||
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</RouteDrawer.Body>
|
||||
|
||||
|
||||
+14
-2
@@ -2,25 +2,37 @@ import { Heading } from "@medusajs/ui"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useParams } from "react-router-dom"
|
||||
import { RouteDrawer } from "../../../components/modals"
|
||||
import { usePayment } from "../../../hooks/api"
|
||||
import { usePayment, useRefundReasons } from "../../../hooks/api"
|
||||
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 {
|
||||
refund_reasons: refundReasons,
|
||||
isLoading: isRefundReasonsLoading,
|
||||
isError: isRefundReasonsError,
|
||||
error: refundReasonsError,
|
||||
} = useRefundReasons()
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
if (isRefundReasonsError) {
|
||||
throw refundReasonsError
|
||||
}
|
||||
|
||||
return (
|
||||
<RouteDrawer>
|
||||
<RouteDrawer.Header>
|
||||
<Heading>{t("orders.payment.createRefund")}</Heading>
|
||||
</RouteDrawer.Header>
|
||||
|
||||
{!isLoading && payment && <CreateRefundForm payment={payment} />}
|
||||
{!isLoading && !isRefundReasonsLoading && payment && refundReasons && (
|
||||
<CreateRefundForm payment={payment} refundReasons={refundReasons} />
|
||||
)}
|
||||
</RouteDrawer>
|
||||
)
|
||||
}
|
||||
|
||||
+19
-16
@@ -1,4 +1,4 @@
|
||||
import { ArrowDownRightMini, XCircle } from "@medusajs/icons"
|
||||
import { ArrowDownRightMini, DocumentText, XCircle } from "@medusajs/icons"
|
||||
import {
|
||||
Payment as MedusaPayment,
|
||||
Refund as MedusaRefund,
|
||||
@@ -80,40 +80,42 @@ const Refund = ({
|
||||
refund,
|
||||
currencyCode,
|
||||
}: {
|
||||
refund: MedusaRefund
|
||||
refund: HttpTypes.AdminRefund
|
||||
currencyCode: string
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const BadgeComponent = (
|
||||
<Badge size="2xsmall" className="cursor-default select-none capitalize">
|
||||
{refund.reason}
|
||||
const RefundReasonBadge = refund?.refund_reason && (
|
||||
<Badge
|
||||
size="2xsmall"
|
||||
className="cursor-default select-none capitalize"
|
||||
rounded="full"
|
||||
>
|
||||
{refund.refund_reason.label}
|
||||
</Badge>
|
||||
)
|
||||
|
||||
const Render = refund.note ? (
|
||||
<Tooltip content={refund.note}>{BadgeComponent}</Tooltip>
|
||||
) : (
|
||||
BadgeComponent
|
||||
const RefundNoteIndicator = refund.note && (
|
||||
<Tooltip content={refund.note}>
|
||||
<DocumentText className="text-ui-tag-neutral-icon inline ml-1" />
|
||||
</Tooltip>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="bg-ui-bg-subtle text-ui-fg-subtle grid grid-cols-[1fr_1fr_1fr_1fr_20px] items-center gap-x-4 px-6 py-4">
|
||||
<div className="bg-ui-bg-subtle text-ui-fg-subtle grid grid-cols-[1fr_1fr_1fr_20px] items-center gap-x-4 px-6 py-4">
|
||||
<div className="flex flex-row">
|
||||
<div className="self-center pr-3">
|
||||
<ArrowDownRightMini className="text-ui-fg-muted" />
|
||||
</div>
|
||||
<div>
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{t("orders.payment.refund")}
|
||||
{t("orders.payment.refund")} {RefundNoteIndicator}
|
||||
</Text>
|
||||
<Text size="small" leading="compact">
|
||||
{format(new Date(refund.created_at), "dd MMM, yyyy, HH:mm:ss")}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-end"></div>
|
||||
<div className="flex items-center justify-end">{Render}</div>
|
||||
<div className="flex items-center justify-end">{RefundReasonBadge}</div>
|
||||
<div className="flex items-center justify-end">
|
||||
<Text size="small" leading="compact">
|
||||
- {getLocaleAmount(refund.amount, currencyCode)}
|
||||
@@ -316,9 +318,10 @@ const Total = ({
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const refunds = payments.map((payment) => payment.refunds).flat(1)
|
||||
const paid = payments.reduce((acc, payment) => acc + payment.amount, 0)
|
||||
const refunded = payments.reduce(
|
||||
(acc, payment) => acc + (payment.amount_refunded || 0),
|
||||
const refunded = refunds.reduce(
|
||||
(acc, refund) => acc + (refund.amount || 0),
|
||||
0
|
||||
)
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ const DEFAULT_RELATIONS = [
|
||||
"*payment_collections",
|
||||
"*payment_collections.payments",
|
||||
"*payment_collections.payments.refunds",
|
||||
"*payment_collections.payments.refunds.refund_reason",
|
||||
]
|
||||
|
||||
export const DEFAULT_FIELDS = `${DEFAULT_PROPERTIES.join(
|
||||
|
||||
@@ -18,6 +18,7 @@ import { ProductCollection } from "./product-collection"
|
||||
import { ProductTag } from "./product-tag"
|
||||
import { ProductType } from "./product-type"
|
||||
import { ProductVariant } from "./product-variant"
|
||||
import { RefundReason } from "./refund-reasons"
|
||||
import { Region } from "./region"
|
||||
import { Return } from "./return"
|
||||
import { ReturnReason } from "./return-reason"
|
||||
@@ -63,6 +64,7 @@ export class Admin {
|
||||
public currency: Currency
|
||||
public payment: Payment
|
||||
public productVariant: ProductVariant
|
||||
public refundReason: RefundReason
|
||||
|
||||
constructor(client: Client) {
|
||||
this.invite = new Invite(client)
|
||||
@@ -96,5 +98,6 @@ export class Admin {
|
||||
this.currency = new Currency(client)
|
||||
this.payment = new Payment(client)
|
||||
this.productVariant = new ProductVariant(client)
|
||||
this.refundReason = new RefundReason(client)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { Client } from "../client"
|
||||
import { ClientHeaders } from "../types"
|
||||
|
||||
export class RefundReason {
|
||||
private client: Client
|
||||
constructor(client: Client) {
|
||||
this.client = client
|
||||
}
|
||||
|
||||
async list(query?: HttpTypes.RefundReasonFilters, headers?: ClientHeaders) {
|
||||
return await this.client.fetch<HttpTypes.RefundReasonsResponse>(
|
||||
`/admin/refund-reasons`,
|
||||
{
|
||||
query,
|
||||
headers,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
BasePaymentProviderFilters,
|
||||
BasePaymentSession,
|
||||
BasePaymentSessionFilters,
|
||||
BaseRefund,
|
||||
RefundReason,
|
||||
} from "./common"
|
||||
|
||||
@@ -33,6 +34,8 @@ export interface AdminCapturePayment {
|
||||
|
||||
export interface AdminRefundPayment {
|
||||
amount?: number
|
||||
refund_reason_id?: string | null
|
||||
note?: string | null
|
||||
}
|
||||
|
||||
export interface AdminPaymentResponse {
|
||||
@@ -45,6 +48,21 @@ export interface AdminPaymentsResponse {
|
||||
|
||||
export interface AdminPaymentFilters extends BasePaymentFilters {}
|
||||
|
||||
// Refund
|
||||
|
||||
export interface AdminRefund extends BaseRefund {}
|
||||
export interface RefundFilters extends BaseFilterable<AdminRefund> {
|
||||
id?: string | string[]
|
||||
}
|
||||
|
||||
export interface AdminRefundResponse {
|
||||
refund_reason: AdminRefund
|
||||
}
|
||||
|
||||
export interface AdminRefundsResponse {
|
||||
refund_reasons: AdminRefund[]
|
||||
}
|
||||
|
||||
// Refund reason
|
||||
|
||||
export interface AdminRefundReason extends RefundReason {}
|
||||
|
||||
Reference in New Issue
Block a user