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:
1 parent
f9a8fcc0bc
commit
a93b025233
10 files changed
+160
-20
No files matched your search
@@ -19,6 +19,7 @@ export * from "./product-types"
|
|||||||
export * from "./product-variants"
|
export * from "./product-variants"
|
||||||
export * from "./products"
|
export * from "./products"
|
||||||
export * from "./promotions"
|
export * from "./promotions"
|
||||||
|
export * from "./refund-reasons"
|
||||||
export * from "./regions"
|
export * from "./regions"
|
||||||
export * from "./reservations"
|
export * from "./reservations"
|
||||||
export * from "./sales-channels"
|
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",
|
"totalRedemptions": "Total Redemptions",
|
||||||
"countries": "Countries",
|
"countries": "Countries",
|
||||||
"paymentProviders": "Payment Providers",
|
"paymentProviders": "Payment Providers",
|
||||||
|
"refundReason": "Refund Reason",
|
||||||
"fulfillmentProviders": "Fulfillment Providers",
|
"fulfillmentProviders": "Fulfillment Providers",
|
||||||
"fulfillmentProvider": "Fulfillment Provider",
|
"fulfillmentProvider": "Fulfillment Provider",
|
||||||
"providers": "Providers",
|
"providers": "Providers",
|
||||||
|
|||||||
+55
-2
@@ -1,10 +1,12 @@
|
|||||||
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, toast } from "@medusajs/ui"
|
import { Button, CurrencyInput, Textarea, toast } from "@medusajs/ui"
|
||||||
import { useForm } from "react-hook-form"
|
import { useForm } from "react-hook-form"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
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"
|
||||||
@@ -12,13 +14,19 @@ import { formatCurrency } from "../../../../../lib/format-currency"
|
|||||||
|
|
||||||
type CreateRefundFormProps = {
|
type CreateRefundFormProps = {
|
||||||
payment: HttpTypes.AdminPayment
|
payment: HttpTypes.AdminPayment
|
||||||
|
refundReasons: HttpTypes.AdminRefundReason[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const CreateRefundSchema = zod.object({
|
const CreateRefundSchema = zod.object({
|
||||||
amount: zod.number(),
|
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 { t } = useTranslation()
|
||||||
const { handleSuccess } = useRouteModal()
|
const { handleSuccess } = useRouteModal()
|
||||||
const paymentAmount = payment.amount as unknown as number
|
const paymentAmount = payment.amount as unknown as number
|
||||||
@@ -26,6 +34,7 @@ export const CreateRefundForm = ({ payment }: CreateRefundFormProps) => {
|
|||||||
const form = useForm<zod.infer<typeof CreateRefundSchema>>({
|
const form = useForm<zod.infer<typeof CreateRefundSchema>>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
amount: paymentAmount,
|
amount: paymentAmount,
|
||||||
|
note: "",
|
||||||
},
|
},
|
||||||
resolver: zodResolver(CreateRefundSchema),
|
resolver: zodResolver(CreateRefundSchema),
|
||||||
})
|
})
|
||||||
@@ -36,6 +45,8 @@ export const CreateRefundForm = ({ payment }: CreateRefundFormProps) => {
|
|||||||
await mutateAsync(
|
await mutateAsync(
|
||||||
{
|
{
|
||||||
amount: data.amount,
|
amount: data.amount,
|
||||||
|
refund_reason_id: data.refund_reason_id,
|
||||||
|
note: data.note,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onSuccess: () => {
|
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>
|
</div>
|
||||||
</RouteDrawer.Body>
|
</RouteDrawer.Body>
|
||||||
|
|
||||||
|
|||||||
+14
-2
@@ -2,25 +2,37 @@ 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 } from "../../../hooks/api"
|
import { usePayment, useRefundReasons } from "../../../hooks/api"
|
||||||
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 { payment, isLoading, isError, error } = usePayment(params.paymentId!)
|
||||||
|
const {
|
||||||
|
refund_reasons: refundReasons,
|
||||||
|
isLoading: isRefundReasonsLoading,
|
||||||
|
isError: isRefundReasonsError,
|
||||||
|
error: refundReasonsError,
|
||||||
|
} = useRefundReasons()
|
||||||
|
|
||||||
if (isError) {
|
if (isError) {
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isRefundReasonsError) {
|
||||||
|
throw refundReasonsError
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RouteDrawer>
|
<RouteDrawer>
|
||||||
<RouteDrawer.Header>
|
<RouteDrawer.Header>
|
||||||
<Heading>{t("orders.payment.createRefund")}</Heading>
|
<Heading>{t("orders.payment.createRefund")}</Heading>
|
||||||
</RouteDrawer.Header>
|
</RouteDrawer.Header>
|
||||||
|
|
||||||
{!isLoading && payment && <CreateRefundForm payment={payment} />}
|
{!isLoading && !isRefundReasonsLoading && payment && refundReasons && (
|
||||||
|
<CreateRefundForm payment={payment} refundReasons={refundReasons} />
|
||||||
|
)}
|
||||||
</RouteDrawer>
|
</RouteDrawer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
+19
-16
@@ -1,4 +1,4 @@
|
|||||||
import { ArrowDownRightMini, XCircle } from "@medusajs/icons"
|
import { ArrowDownRightMini, DocumentText, XCircle } from "@medusajs/icons"
|
||||||
import {
|
import {
|
||||||
Payment as MedusaPayment,
|
Payment as MedusaPayment,
|
||||||
Refund as MedusaRefund,
|
Refund as MedusaRefund,
|
||||||
@@ -80,40 +80,42 @@ const Refund = ({
|
|||||||
refund,
|
refund,
|
||||||
currencyCode,
|
currencyCode,
|
||||||
}: {
|
}: {
|
||||||
refund: MedusaRefund
|
refund: HttpTypes.AdminRefund
|
||||||
currencyCode: string
|
currencyCode: string
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const RefundReasonBadge = refund?.refund_reason && (
|
||||||
const BadgeComponent = (
|
<Badge
|
||||||
<Badge size="2xsmall" className="cursor-default select-none capitalize">
|
size="2xsmall"
|
||||||
{refund.reason}
|
className="cursor-default select-none capitalize"
|
||||||
|
rounded="full"
|
||||||
|
>
|
||||||
|
{refund.refund_reason.label}
|
||||||
</Badge>
|
</Badge>
|
||||||
)
|
)
|
||||||
|
|
||||||
const Render = refund.note ? (
|
const RefundNoteIndicator = refund.note && (
|
||||||
<Tooltip content={refund.note}>{BadgeComponent}</Tooltip>
|
<Tooltip content={refund.note}>
|
||||||
) : (
|
<DocumentText className="text-ui-tag-neutral-icon inline ml-1" />
|
||||||
BadgeComponent
|
</Tooltip>
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
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="flex flex-row">
|
||||||
<div className="self-center pr-3">
|
<div className="self-center pr-3">
|
||||||
<ArrowDownRightMini className="text-ui-fg-muted" />
|
<ArrowDownRightMini className="text-ui-fg-muted" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Text size="small" leading="compact" weight="plus">
|
<Text size="small" leading="compact" weight="plus">
|
||||||
{t("orders.payment.refund")}
|
{t("orders.payment.refund")} {RefundNoteIndicator}
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="small" leading="compact">
|
<Text size="small" leading="compact">
|
||||||
{format(new Date(refund.created_at), "dd MMM, yyyy, HH:mm:ss")}
|
{format(new Date(refund.created_at), "dd MMM, yyyy, HH:mm:ss")}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-end"></div>
|
<div className="flex items-center justify-end">{RefundReasonBadge}</div>
|
||||||
<div className="flex items-center justify-end">{Render}</div>
|
|
||||||
<div className="flex items-center justify-end">
|
<div className="flex items-center justify-end">
|
||||||
<Text size="small" leading="compact">
|
<Text size="small" leading="compact">
|
||||||
- {getLocaleAmount(refund.amount, currencyCode)}
|
- {getLocaleAmount(refund.amount, currencyCode)}
|
||||||
@@ -316,9 +318,10 @@ const Total = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const refunds = payments.map((payment) => payment.refunds).flat(1)
|
||||||
const paid = payments.reduce((acc, payment) => acc + payment.amount, 0)
|
const paid = payments.reduce((acc, payment) => acc + payment.amount, 0)
|
||||||
const refunded = payments.reduce(
|
const refunded = refunds.reduce(
|
||||||
(acc, payment) => acc + (payment.amount_refunded || 0),
|
(acc, refund) => acc + (refund.amount || 0),
|
||||||
0
|
0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ const DEFAULT_RELATIONS = [
|
|||||||
"*payment_collections",
|
"*payment_collections",
|
||||||
"*payment_collections.payments",
|
"*payment_collections.payments",
|
||||||
"*payment_collections.payments.refunds",
|
"*payment_collections.payments.refunds",
|
||||||
|
"*payment_collections.payments.refunds.refund_reason",
|
||||||
]
|
]
|
||||||
|
|
||||||
export const DEFAULT_FIELDS = `${DEFAULT_PROPERTIES.join(
|
export const DEFAULT_FIELDS = `${DEFAULT_PROPERTIES.join(
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { ProductCollection } from "./product-collection"
|
|||||||
import { ProductTag } from "./product-tag"
|
import { ProductTag } from "./product-tag"
|
||||||
import { ProductType } from "./product-type"
|
import { ProductType } from "./product-type"
|
||||||
import { ProductVariant } from "./product-variant"
|
import { ProductVariant } from "./product-variant"
|
||||||
|
import { RefundReason } from "./refund-reasons"
|
||||||
import { Region } from "./region"
|
import { Region } from "./region"
|
||||||
import { Return } from "./return"
|
import { Return } from "./return"
|
||||||
import { ReturnReason } from "./return-reason"
|
import { ReturnReason } from "./return-reason"
|
||||||
@@ -63,6 +64,7 @@ export class Admin {
|
|||||||
public currency: Currency
|
public currency: Currency
|
||||||
public payment: Payment
|
public payment: Payment
|
||||||
public productVariant: ProductVariant
|
public productVariant: ProductVariant
|
||||||
|
public refundReason: RefundReason
|
||||||
|
|
||||||
constructor(client: Client) {
|
constructor(client: Client) {
|
||||||
this.invite = new Invite(client)
|
this.invite = new Invite(client)
|
||||||
@@ -96,5 +98,6 @@ export class Admin {
|
|||||||
this.currency = new Currency(client)
|
this.currency = new Currency(client)
|
||||||
this.payment = new Payment(client)
|
this.payment = new Payment(client)
|
||||||
this.productVariant = new ProductVariant(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,
|
BasePaymentProviderFilters,
|
||||||
BasePaymentSession,
|
BasePaymentSession,
|
||||||
BasePaymentSessionFilters,
|
BasePaymentSessionFilters,
|
||||||
|
BaseRefund,
|
||||||
RefundReason,
|
RefundReason,
|
||||||
} from "./common"
|
} from "./common"
|
||||||
|
|
||||||
@@ -33,6 +34,8 @@ export interface AdminCapturePayment {
|
|||||||
|
|
||||||
export interface AdminRefundPayment {
|
export interface AdminRefundPayment {
|
||||||
amount?: number
|
amount?: number
|
||||||
|
refund_reason_id?: string | null
|
||||||
|
note?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AdminPaymentResponse {
|
export interface AdminPaymentResponse {
|
||||||
@@ -45,6 +48,21 @@ export interface AdminPaymentsResponse {
|
|||||||
|
|
||||||
export interface AdminPaymentFilters extends BasePaymentFilters {}
|
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
|
// Refund reason
|
||||||
|
|
||||||
export interface AdminRefundReason extends RefundReason {}
|
export interface AdminRefundReason extends RefundReason {}
|
||||||
|
|||||||
Reference in new issue
Block a user