diff --git a/packages/admin-next/dashboard/src/hooks/api/payments.tsx b/packages/admin-next/dashboard/src/hooks/api/payments.tsx index f3bab9cafc..e873aef972 100644 --- a/packages/admin-next/dashboard/src/hooks/api/payments.tsx +++ b/packages/admin-next/dashboard/src/hooks/api/payments.tsx @@ -1,6 +1,15 @@ -import { QueryKey, useQuery, UseQueryOptions } from "@tanstack/react-query" +import { HttpTypes } from "@medusajs/types" +import { + QueryKey, + useMutation, + UseMutationOptions, + useQuery, + UseQueryOptions, +} from "@tanstack/react-query" +import { client, sdk } from "../../lib/client" +import { queryClient } from "../../lib/query-client" import { PaymentProvidersListRes } from "../../types/api-responses" -import { client } from "../../lib/client" +import { ordersQueryKeys } from "./orders" export const usePaymentProviders = ( query?: Record, @@ -22,3 +31,28 @@ export const usePaymentProviders = ( return { ...data, ...rest } } + +export const useCapturePayment = ( + paymentId: string, + options?: UseMutationOptions< + HttpTypes.AdminPaymentResponse, + Error, + HttpTypes.AdminCapturePayment + > +) => { + return useMutation({ + mutationFn: (payload) => sdk.admin.payment.capture(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 6a30cb0f6d..6ba353fa3c 100644 --- a/packages/admin-next/dashboard/src/i18n/translations/en.json +++ b/packages/admin-next/dashboard/src/i18n/translations/en.json @@ -809,7 +809,7 @@ "title": "Payments", "isReadyToBeCaptured": "Payment {{id}} is ready to be captured.", "totalPaidByCustomer": "Total paid by customer", - "capture": "Capture", + "capture": "Capture payment", "refund": "Refund", "statusLabel": "Payment status", "statusTitle": "Payment Status", @@ -824,7 +824,9 @@ "refunded": "Refunded", "canceled": "Canceled", "requiresAction": "Requires action" - } + }, + "capturePayment": "Payment of {{amount}} will be captured.", + "capturePaymentSuccess": "Payment of {{amount}} successfully captured" }, "edits": { diff --git a/packages/admin-next/dashboard/src/lib/format-currency.ts b/packages/admin-next/dashboard/src/lib/format-currency.ts new file mode 100644 index 0000000000..9704ef702e --- /dev/null +++ b/packages/admin-next/dashboard/src/lib/format-currency.ts @@ -0,0 +1,7 @@ +export const formatCurrency = (amount: number, currency: string) => { + return new Intl.NumberFormat("en-US", { + style: "currency", + currency, + signDisplay: "always", + }).format(amount) +} 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 afc2d6e652..7edc66c1c3 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 @@ -2,8 +2,8 @@ import { ArrowDownRightMini, XCircle } from "@medusajs/icons" import { Payment as MedusaPayment, Refund as MedusaRefund, - Order, } from "@medusajs/medusa" +import { HttpTypes } from "@medusajs/types" import { Badge, Button, @@ -11,56 +11,60 @@ import { Heading, StatusBadge, Text, + toast, Tooltip, + usePrompt, } from "@medusajs/ui" import { format } from "date-fns" import { useTranslation } from "react-i18next" import { ActionMenu } from "../../../../../components/common/action-menu" +import { useCapturePayment } from "../../../../../hooks/api" +import { formatCurrency } from "../../../../../lib/format-currency" import { getLocaleAmount, getStylizedAmount, } from "../../../../../lib/money-amount-helpers" type OrderPaymentSectionProps = { - order: Order + order: HttpTypes.AdminOrder +} + +const getPaymentsFromOrder = (order: HttpTypes.AdminOrder) => { + return order.payment_collections + .map((collection: HttpTypes.AdminPaymentCollection) => collection.payments) + .flat(1) + .filter(Boolean) } export const OrderPaymentSection = ({ order }: OrderPaymentSectionProps) => { + const payments = getPaymentsFromOrder(order) + + const refunds = payments + .map((payment) => payment?.refunds) + .flat(1) + .filter(Boolean) + return ( -
+
+ - + + ) } -const Header = ({ order }: { order: Order }) => { +const Header = () => { const { t } = useTranslation() - const hasCapturedPayment = order.payments.some((p) => !!p.captured_at) - return (
{t("orders.payment.title")} - , - to: `/orders/${order.id}/refund`, - disabled: !hasCapturedPayment, - }, - ], - }, - ]} - />
) } @@ -120,6 +124,39 @@ const Payment = ({ currencyCode: string }) => { const { t } = useTranslation() + const prompt = usePrompt() + const { mutateAsync } = useCapturePayment(payment.id) + + const handleCapture = async () => { + const res = await prompt({ + title: t("orders.payment.capture"), + description: t("orders.payment.capturePayment", { + amount: formatCurrency(payment.amount, currencyCode), + }), + confirmText: t("actions.confirm"), + cancelText: t("actions.cancel"), + }) + + if (!res) { + return + } + + await mutateAsync( + { amount: payment.amount }, + { + onSuccess: () => { + toast.success( + t("orders.payment.capturePaymentSuccess", { + amount: formatCurrency(payment.amount, currencyCode), + }) + ) + }, + onError: (error) => { + toast.error(error.message) + }, + } + ) + } const [status, color] = ( payment.captured_at ? ["Captured", "green"] : ["Pending", "orange"] @@ -185,7 +222,8 @@ const Payment = ({ })} - diff --git a/packages/admin-next/dashboard/src/routes/orders/order-detail/constants.ts b/packages/admin-next/dashboard/src/routes/orders/order-detail/constants.ts index 59a9eed81c..3f67c6516c 100644 --- a/packages/admin-next/dashboard/src/routes/orders/order-detail/constants.ts +++ b/packages/admin-next/dashboard/src/routes/orders/order-detail/constants.ts @@ -31,6 +31,10 @@ const DEFAULT_RELATIONS = [ "*fulfillments", "*fulfillments.items", "*fulfillments.labels", + "*fulfillments.labels", + "*payment_collections", + "*payment_collections.payments", + "*payment_collections.payments.refunds", ] export const DEFAULT_FIELDS = `${DEFAULT_PROPERTIES.join( diff --git a/packages/admin-next/dashboard/src/routes/orders/order-detail/order-detail.tsx b/packages/admin-next/dashboard/src/routes/orders/order-detail/order-detail.tsx index 6e8eae1d6d..0630c22f17 100644 --- a/packages/admin-next/dashboard/src/routes/orders/order-detail/order-detail.tsx +++ b/packages/admin-next/dashboard/src/routes/orders/order-detail/order-detail.tsx @@ -6,6 +6,7 @@ import { OrderActivitySection } from "./components/order-activity-section" import { OrderCustomerSection } from "./components/order-customer-section" import { OrderFulfillmentSection } from "./components/order-fulfillment-section" import { OrderGeneralSection } from "./components/order-general-section" +import { OrderPaymentSection } from "./components/order-payment-section" import { OrderSummarySection } from "./components/order-summary-section" import { DEFAULT_FIELDS } from "./constants" import { orderLoader } from "./loader" @@ -51,7 +52,7 @@ export const OrderDetail = () => {
- {/* */} + {after.widgets.map((w, i) => { return ( diff --git a/packages/core/js-sdk/src/admin/index.ts b/packages/core/js-sdk/src/admin/index.ts index e24245bc02..9fb4ba7684 100644 --- a/packages/core/js-sdk/src/admin/index.ts +++ b/packages/core/js-sdk/src/admin/index.ts @@ -8,6 +8,7 @@ import { InventoryItem } from "./inventory-item" import { Invite } from "./invite" import { Notification } from "./notification" import { Order } from "./order" +import { Payment } from "./payment" import { PriceList } from "./price-list" import { PricePreference } from "./price-preference" import { Product } from "./product" @@ -59,6 +60,7 @@ export class Admin { public productTag: ProductTag public user: User public currency: Currency + public payment: Payment constructor(client: Client) { this.invite = new Invite(client) @@ -90,5 +92,6 @@ export class Admin { this.productTag = new ProductTag(client) this.user = new User(client) this.currency = new Currency(client) + this.payment = new Payment(client) } } diff --git a/packages/core/js-sdk/src/admin/payment.ts b/packages/core/js-sdk/src/admin/payment.ts new file mode 100644 index 0000000000..b5cb048b55 --- /dev/null +++ b/packages/core/js-sdk/src/admin/payment.ts @@ -0,0 +1,27 @@ +import { HttpTypes, SelectParams } from "@medusajs/types" +import { Client } from "../client" +import { ClientHeaders } from "../types" + +export class Payment { + private client: Client + constructor(client: Client) { + this.client = client + } + + async capture( + id: string, + body: HttpTypes.AdminCapturePayment, + query?: SelectParams, + headers?: ClientHeaders + ) { + return await this.client.fetch<{ payment: HttpTypes.AdminPayment }>( + `/admin/payments/${id}/capture`, + { + method: "POST", + headers, + body, + query, + } + ) + } +} diff --git a/packages/core/types/src/http/order/admin.ts b/packages/core/types/src/http/order/admin.ts index 055bbc6a4f..dce0c8fec0 100644 --- a/packages/core/types/src/http/order/admin.ts +++ b/packages/core/types/src/http/order/admin.ts @@ -1,3 +1,4 @@ +import { AdminPaymentCollection } from "../payment/admin" import { BaseOrder, BaseOrderAddress, @@ -6,7 +7,10 @@ import { BaseOrderShippingMethod, } from "./common" -export interface AdminOrder extends BaseOrder {} +export interface AdminOrder extends BaseOrder { + payment_collections: AdminPaymentCollection[] +} + export interface AdminOrderLineItem extends BaseOrderLineItem {} export interface AdminOrderFilters extends BaseOrderFilters {} export interface AdminOrderAddress extends BaseOrderAddress {} diff --git a/packages/core/types/src/http/payment/admin.ts b/packages/core/types/src/http/payment/admin.ts index 6151efd25d..910ea5d214 100644 --- a/packages/core/types/src/http/payment/admin.ts +++ b/packages/core/types/src/http/payment/admin.ts @@ -1,4 +1,5 @@ import { + BasePayment, BasePaymentCollection, BasePaymentCollectionFilters, BasePaymentProvider, @@ -11,6 +12,7 @@ export interface AdminPaymentProvider extends BasePaymentProvider { is_enabled: boolean } +export interface AdminPayment extends BasePayment {} export interface AdminPaymentCollection extends BasePaymentCollection {} export interface AdminPaymentSession extends BasePaymentSession {} @@ -21,3 +23,11 @@ export interface AdminPaymentProviderFilters export interface AdminPaymentCollectionFilters extends BasePaymentCollectionFilters {} export interface AdminPaymentSessionFilters extends BasePaymentSessionFilters {} + +export interface AdminCapturePayment { + amount?: number +} + +export interface AdminPaymentResponse { + payment: AdminPayment +}