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:
Riqwan Thamir
2024-08-08 23:19:08 +02:00
committed by GitHub
parent f9a8fcc0bc
commit a93b025233
10 changed files with 160 additions and 20 deletions
@@ -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",
@@ -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>
@@ -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>
)
}
@@ -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(
+3
View File
@@ -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 {}