feat(dashboard,js-sdk,types): add ability to capture payment from order page (#8368)
what: - adds ability to capture payment from order page bugs found: - when capturing payment of one order, other orders statuses get affected from "captured" to "partially captured". Will investigate this separately. https://github.com/user-attachments/assets/0a1beac2-74fc-4803-8528-8de5913964d4
This commit is contained in:
@@ -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<string, any>,
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
export const formatCurrency = (amount: number, currency: string) => {
|
||||
return new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency,
|
||||
signDisplay: "always",
|
||||
}).format(amount)
|
||||
}
|
||||
+62
-24
@@ -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 (
|
||||
<Container className="divide-y divide-dashed p-0">
|
||||
<Header order={order} />
|
||||
<Header />
|
||||
|
||||
<PaymentBreakdown
|
||||
payments={order.payments}
|
||||
refunds={order.refunds}
|
||||
payments={payments}
|
||||
refunds={refunds}
|
||||
currencyCode={order.currency_code}
|
||||
/>
|
||||
<Total payments={order.payments} currencyCode={order.currency_code} />
|
||||
|
||||
<Total payments={payments} currencyCode={order.currency_code} />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Header = ({ order }: { order: Order }) => {
|
||||
const Header = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const hasCapturedPayment = order.payments.some((p) => !!p.captured_at)
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<Heading level="h2">{t("orders.payment.title")}</Heading>
|
||||
<ActionMenu
|
||||
groups={[
|
||||
{
|
||||
actions: [
|
||||
{
|
||||
label: t("orders.payment.refund"),
|
||||
icon: <ArrowDownRightMini />,
|
||||
to: `/orders/${order.id}/refund`,
|
||||
disabled: !hasCapturedPayment,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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 = ({
|
||||
})}
|
||||
</Text>
|
||||
</div>
|
||||
<Button size="small" variant="secondary">
|
||||
|
||||
<Button size="small" variant="secondary" onClick={handleCapture}>
|
||||
{t("orders.payment.capture")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 = () => {
|
||||
<div className="flex w-full flex-col gap-y-3">
|
||||
<OrderGeneralSection order={order} />
|
||||
<OrderSummarySection order={order} />
|
||||
{/* <OrderPaymentSection order={order} />*/}
|
||||
<OrderPaymentSection order={order} />
|
||||
<OrderFulfillmentSection order={order} />
|
||||
{after.widgets.map((w, i) => {
|
||||
return (
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user