diff --git a/packages/admin-next/dashboard/src/hooks/api/payments.tsx b/packages/admin-next/dashboard/src/hooks/api/payments.tsx index e873aef972..9e8141d88f 100644 --- a/packages/admin-next/dashboard/src/hooks/api/payments.tsx +++ b/packages/admin-next/dashboard/src/hooks/api/payments.tsx @@ -8,9 +8,13 @@ import { } from "@tanstack/react-query" import { client, sdk } from "../../lib/client" import { queryClient } from "../../lib/query-client" +import { queryKeysFactory } from "../../lib/query-key-factory" import { PaymentProvidersListRes } from "../../types/api-responses" import { ordersQueryKeys } from "./orders" +const PAYMENT_QUERY_KEY = "payment" as const +export const paymentQueryKeys = queryKeysFactory(PAYMENT_QUERY_KEY) + export const usePaymentProviders = ( query?: Record, options?: Omit< @@ -32,6 +36,28 @@ export const usePaymentProviders = ( return { ...data, ...rest } } +export const usePayment = ( + id: string, + query?: HttpTypes.AdminPaymentFilters, + options?: Omit< + UseQueryOptions< + HttpTypes.AdminPaymentResponse, + Error, + HttpTypes.AdminPaymentResponse, + QueryKey + >, + "queryKey" | "queryFn" + > +) => { + const { data, ...rest } = useQuery({ + queryFn: () => sdk.admin.payment.retrieve(id, query), + queryKey: paymentQueryKeys.detail(id), + ...options, + }) + + return { ...data, ...rest } +} + export const useCapturePayment = ( paymentId: string, options?: UseMutationOptions< @@ -56,3 +82,28 @@ export const useCapturePayment = ( ...options, }) } + +export const useRefundPayment = ( + paymentId: string, + options?: UseMutationOptions< + HttpTypes.AdminPaymentResponse, + Error, + HttpTypes.AdminRefundPayment + > +) => { + return useMutation({ + mutationFn: (payload) => sdk.admin.payment.refund(paymentId, payload), + onSuccess: (data, variables, context) => { + queryClient.invalidateQueries({ + queryKey: ordersQueryKeys.details(), + }) + + queryClient.invalidateQueries({ + queryKey: ordersQueryKeys.lists(), + }) + + options?.onSuccess?.(data, variables, context) + }, + ...options, + }) +} diff --git a/packages/admin-next/dashboard/src/i18n/translations/en.json b/packages/admin-next/dashboard/src/i18n/translations/en.json index 6ba353fa3c..8fa322ca52 100644 --- a/packages/admin-next/dashboard/src/i18n/translations/en.json +++ b/packages/admin-next/dashboard/src/i18n/translations/en.json @@ -826,7 +826,10 @@ "requiresAction": "Requires action" }, "capturePayment": "Payment of {{amount}} will be captured.", - "capturePaymentSuccess": "Payment of {{amount}} successfully captured" + "capturePaymentSuccess": "Payment of {{amount}} successfully captured", + "createRefund": "Create Refund", + "refundPaymentSuccess": "Refund of amount {{amount}} successful", + "createRefundWrongQuantity": "Quantity should be a number between 1 and {{number}}" }, "edits": { diff --git a/packages/admin-next/dashboard/src/lib/client/payments.ts b/packages/admin-next/dashboard/src/lib/client/payments.ts index d17fe09474..fe3668ae31 100644 --- a/packages/admin-next/dashboard/src/lib/client/payments.ts +++ b/packages/admin-next/dashboard/src/lib/client/payments.ts @@ -1,5 +1,5 @@ -import { getRequest } from "./common" import { PaymentProvidersListRes } from "../../types/api-responses" +import { getRequest } from "./common" async function listPaymentProviders(query?: Record) { return getRequest>( diff --git a/packages/admin-next/dashboard/src/providers/router-provider/route-map.tsx b/packages/admin-next/dashboard/src/providers/router-provider/route-map.tsx index f83603d2ef..b2aa98f2b7 100644 --- a/packages/admin-next/dashboard/src/providers/router-provider/route-map.tsx +++ b/packages/admin-next/dashboard/src/providers/router-provider/route-map.tsx @@ -238,6 +238,11 @@ export const RouteMap: RouteObject[] = [ lazy: () => import("../../routes/orders/order-create-return"), }, + { + path: "payments/:paymentId/refund", + lazy: () => + import("../../routes/orders/order-create-refund"), + }, ], }, ], diff --git a/packages/admin-next/dashboard/src/routes/orders/order-create-refund/components/create-refund-form/create-refund-form.tsx b/packages/admin-next/dashboard/src/routes/orders/order-create-refund/components/create-refund-form/create-refund-form.tsx new file mode 100644 index 0000000000..5c4415c0ab --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/orders/order-create-refund/components/create-refund-form/create-refund-form.tsx @@ -0,0 +1,137 @@ +import { zodResolver } from "@hookform/resolvers/zod" +import { HttpTypes } from "@medusajs/types" +import { Button, CurrencyInput, toast } from "@medusajs/ui" +import { useForm } from "react-hook-form" +import { useTranslation } from "react-i18next" +import * as zod from "zod" +import { Form } from "../../../../../components/common/form" +import { RouteDrawer, useRouteModal } from "../../../../../components/modals" +import { useRefundPayment } from "../../../../../hooks/api" +import { getCurrencySymbol } from "../../../../../lib/data/currencies" +import { formatCurrency } from "../../../../../lib/format-currency" + +type CreateRefundFormProps = { + payment: HttpTypes.AdminPayment +} + +const CreateRefundSchema = zod.object({ + amount: zod.number(), +}) + +export const CreateRefundForm = ({ payment }: CreateRefundFormProps) => { + const { t } = useTranslation() + const { handleSuccess } = useRouteModal() + const paymentAmount = payment.amount as unknown as number + + const form = useForm>({ + defaultValues: { + amount: paymentAmount, + }, + resolver: zodResolver(CreateRefundSchema), + }) + + const { mutateAsync, isPending } = useRefundPayment(payment.id) + + const handleSubmit = form.handleSubmit(async (data) => { + await mutateAsync( + { + amount: data.amount, + }, + { + onSuccess: () => { + toast.success( + t("orders.payment.refundPaymentSuccess", { + amount: formatCurrency(data.amount, payment.currency_code), + }) + ) + + handleSuccess() + }, + onError: (error) => { + toast.error(error.message) + }, + } + ) + }) + + return ( + +
+ +
+ { + return ( + + {t("fields.amount")} + + + { + const val = + e.target.value === "" + ? null + : Number(e.target.value) + + onChange(val) + + if (val && !isNaN(val)) { + if (val < 0 || val > paymentAmount) { + form.setError(`amount`, { + type: "manual", + message: t( + "orders.payment.createRefundWrongQuantity", + { number: paymentAmount } + ), + }) + } else { + form.clearErrors(`amount`) + } + } + }} + code={payment.currency_code} + symbol={getCurrencySymbol(payment.currency_code)} + value={field.value} + /> + + + + + ) + }} + /> +
+
+ + +
+ + + + + +
+
+
+
+ ) +} diff --git a/packages/admin-next/dashboard/src/routes/orders/order-create-refund/components/create-refund-form/index.ts b/packages/admin-next/dashboard/src/routes/orders/order-create-refund/components/create-refund-form/index.ts new file mode 100644 index 0000000000..aebb0a966d --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/orders/order-create-refund/components/create-refund-form/index.ts @@ -0,0 +1 @@ +export * from "./create-refund-form" diff --git a/packages/admin-next/dashboard/src/routes/orders/order-create-refund/index.ts b/packages/admin-next/dashboard/src/routes/orders/order-create-refund/index.ts new file mode 100644 index 0000000000..e0c3c04da9 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/orders/order-create-refund/index.ts @@ -0,0 +1 @@ +export { OrderCreateRefund as Component } from "./order-create-refund" diff --git a/packages/admin-next/dashboard/src/routes/orders/order-create-refund/order-create-refund.tsx b/packages/admin-next/dashboard/src/routes/orders/order-create-refund/order-create-refund.tsx new file mode 100644 index 0000000000..53850564bb --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/orders/order-create-refund/order-create-refund.tsx @@ -0,0 +1,26 @@ +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 { CreateRefundForm } from "./components/create-refund-form" + +export const OrderCreateRefund = () => { + const { t } = useTranslation() + const params = useParams() + const { payment, isLoading, isError, error } = usePayment(params.paymentId!) + + if (isError) { + throw error + } + + return ( + + + {t("orders.payment.createRefund")} + + + {!isLoading && payment && } + + ) +} diff --git a/packages/admin-next/dashboard/src/routes/orders/order-detail/components/order-payment-section/order-payment-section.tsx b/packages/admin-next/dashboard/src/routes/orders/order-detail/components/order-payment-section/order-payment-section.tsx index 7edc66c1c3..eb9de63022 100644 --- a/packages/admin-next/dashboard/src/routes/orders/order-detail/components/order-payment-section/order-payment-section.tsx +++ b/packages/admin-next/dashboard/src/routes/orders/order-detail/components/order-payment-section/order-payment-section.tsx @@ -49,6 +49,7 @@ export const OrderPaymentSection = ({ order }: OrderPaymentSectionProps) => {
{ const { t } = useTranslation() - const hasPayment = refund.payment_id !== null const BadgeComponent = ( @@ -93,17 +93,20 @@ const Refund = ({ return (
-
- {hasPayment && } - - {t("orders.payment.refund")} - -
-
- - {format(new Date(refund.created_at), "dd MMM, yyyy, HH:mm:ss")} - +
+
+ +
+
+ + {t("orders.payment.refund")} + + + {format(new Date(refund.created_at), "dd MMM, yyyy, HH:mm:ss")} + +
+
{Render}
@@ -115,10 +118,12 @@ const Refund = ({ } const Payment = ({ + order, payment, refunds, currencyCode, }: { + order: HttpTypes.AdminOrder payment: MedusaPayment refunds: MedusaRefund[] currencyCode: string @@ -135,6 +140,7 @@ const Payment = ({ }), confirmText: t("actions.confirm"), cancelText: t("actions.cancel"), + variant: "confirmation", }) if (!res) { @@ -204,7 +210,7 @@ const Payment = ({ { label: t("orders.payment.refund"), icon: , - to: `/orders/${payment.order_id}/refund?paymentId=${payment.id}`, + to: `/orders/${order.id}/payments/${payment.id}/refund`, disabled: !payment.captured_at, }, ], @@ -236,10 +242,12 @@ const Payment = ({ } const PaymentBreakdown = ({ + order, payments, refunds, currencyCode, }: { + order: HttpTypes.AdminOrder payments: MedusaPayment[] refunds: MedusaRefund[] currencyCode: string @@ -271,6 +279,7 @@ const PaymentBreakdown = ({ return ( refund.payment_id === event.id diff --git a/packages/core/js-sdk/src/admin/payment.ts b/packages/core/js-sdk/src/admin/payment.ts index b5cb048b55..230cdf5107 100644 --- a/packages/core/js-sdk/src/admin/payment.ts +++ b/packages/core/js-sdk/src/admin/payment.ts @@ -8,13 +8,37 @@ export class Payment { this.client = client } + async list(query?: HttpTypes.AdminPaymentFilters, headers?: ClientHeaders) { + return await this.client.fetch( + `/admin/payments`, + { + query, + headers, + } + ) + } + + async retrieve( + id: string, + query?: HttpTypes.AdminPaymentFilters, + headers?: ClientHeaders + ) { + return await this.client.fetch( + `/admin/payments/${id}`, + { + query, + headers, + } + ) + } + async capture( id: string, body: HttpTypes.AdminCapturePayment, query?: SelectParams, headers?: ClientHeaders ) { - return await this.client.fetch<{ payment: HttpTypes.AdminPayment }>( + return await this.client.fetch( `/admin/payments/${id}/capture`, { method: "POST", @@ -24,4 +48,21 @@ export class Payment { } ) } + + async refund( + id: string, + body: HttpTypes.AdminRefundPayment, + query?: SelectParams, + headers?: ClientHeaders + ) { + return await this.client.fetch( + `/admin/payments/${id}/refund`, + { + method: "POST", + headers, + body, + query, + } + ) + } } diff --git a/packages/core/types/src/http/payment/admin.ts b/packages/core/types/src/http/payment/admin.ts index 910ea5d214..a7fa667411 100644 --- a/packages/core/types/src/http/payment/admin.ts +++ b/packages/core/types/src/http/payment/admin.ts @@ -2,6 +2,7 @@ import { BasePayment, BasePaymentCollection, BasePaymentCollectionFilters, + BasePaymentFilters, BasePaymentProvider, BasePaymentProviderFilters, BasePaymentSession, @@ -28,6 +29,16 @@ export interface AdminCapturePayment { amount?: number } +export interface AdminRefundPayment { + amount?: number +} + export interface AdminPaymentResponse { payment: AdminPayment } + +export interface AdminPaymentsResponse { + payments: AdminPayment[] +} + +export interface AdminPaymentFilters extends BasePaymentFilters {} diff --git a/packages/core/types/src/http/payment/common.ts b/packages/core/types/src/http/payment/common.ts index 39474eea05..e5172bad9b 100644 --- a/packages/core/types/src/http/payment/common.ts +++ b/packages/core/types/src/http/payment/common.ts @@ -420,3 +420,7 @@ export interface BasePaymentProviderFilters id?: string | string[] region_id?: string | string[] } + +export interface BasePaymentFilters extends BaseFilterable { + id?: string | string[] +} diff --git a/packages/medusa/src/api/admin/payments/query-config.ts b/packages/medusa/src/api/admin/payments/query-config.ts index a30ae02ac7..1a66f4a9a0 100644 --- a/packages/medusa/src/api/admin/payments/query-config.ts +++ b/packages/medusa/src/api/admin/payments/query-config.ts @@ -9,6 +9,7 @@ export const defaultAdminPaymentFields = [ "captures.amount", "refunds.id", "refunds.amount", + "refunds.payment_id", ] export const listTransformQueryConfig = { diff --git a/packages/modules/payment/src/models/refund.ts b/packages/modules/payment/src/models/refund.ts index f00d2009fa..f325fea17c 100644 --- a/packages/modules/payment/src/models/refund.ts +++ b/packages/modules/payment/src/models/refund.ts @@ -33,6 +33,9 @@ export default class Refund { }) payment!: Rel + @Property({ columnType: "text", nullable: true }) + payment_id: string + @Property({ onCreate: () => new Date(), columnType: "timestamptz",