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 { PaymentProvidersListRes } from "../../types/api-responses"
|
||||||
import { client } from "../../lib/client"
|
import { ordersQueryKeys } from "./orders"
|
||||||
|
|
||||||
export const usePaymentProviders = (
|
export const usePaymentProviders = (
|
||||||
query?: Record<string, any>,
|
query?: Record<string, any>,
|
||||||
@@ -22,3 +31,28 @@ export const usePaymentProviders = (
|
|||||||
|
|
||||||
return { ...data, ...rest }
|
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",
|
"title": "Payments",
|
||||||
"isReadyToBeCaptured": "Payment {{id}} is ready to be captured.",
|
"isReadyToBeCaptured": "Payment {{id}} is ready to be captured.",
|
||||||
"totalPaidByCustomer": "Total paid by customer",
|
"totalPaidByCustomer": "Total paid by customer",
|
||||||
"capture": "Capture",
|
"capture": "Capture payment",
|
||||||
"refund": "Refund",
|
"refund": "Refund",
|
||||||
"statusLabel": "Payment status",
|
"statusLabel": "Payment status",
|
||||||
"statusTitle": "Payment Status",
|
"statusTitle": "Payment Status",
|
||||||
@@ -824,7 +824,9 @@
|
|||||||
"refunded": "Refunded",
|
"refunded": "Refunded",
|
||||||
"canceled": "Canceled",
|
"canceled": "Canceled",
|
||||||
"requiresAction": "Requires action"
|
"requiresAction": "Requires action"
|
||||||
}
|
},
|
||||||
|
"capturePayment": "Payment of {{amount}} will be captured.",
|
||||||
|
"capturePaymentSuccess": "Payment of {{amount}} successfully captured"
|
||||||
},
|
},
|
||||||
|
|
||||||
"edits": {
|
"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 {
|
import {
|
||||||
Payment as MedusaPayment,
|
Payment as MedusaPayment,
|
||||||
Refund as MedusaRefund,
|
Refund as MedusaRefund,
|
||||||
Order,
|
|
||||||
} from "@medusajs/medusa"
|
} from "@medusajs/medusa"
|
||||||
|
import { HttpTypes } from "@medusajs/types"
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
@@ -11,56 +11,60 @@ import {
|
|||||||
Heading,
|
Heading,
|
||||||
StatusBadge,
|
StatusBadge,
|
||||||
Text,
|
Text,
|
||||||
|
toast,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
|
usePrompt,
|
||||||
} from "@medusajs/ui"
|
} from "@medusajs/ui"
|
||||||
import { format } from "date-fns"
|
import { format } from "date-fns"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
|
import { useCapturePayment } from "../../../../../hooks/api"
|
||||||
|
import { formatCurrency } from "../../../../../lib/format-currency"
|
||||||
import {
|
import {
|
||||||
getLocaleAmount,
|
getLocaleAmount,
|
||||||
getStylizedAmount,
|
getStylizedAmount,
|
||||||
} from "../../../../../lib/money-amount-helpers"
|
} from "../../../../../lib/money-amount-helpers"
|
||||||
|
|
||||||
type OrderPaymentSectionProps = {
|
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) => {
|
export const OrderPaymentSection = ({ order }: OrderPaymentSectionProps) => {
|
||||||
|
const payments = getPaymentsFromOrder(order)
|
||||||
|
|
||||||
|
const refunds = payments
|
||||||
|
.map((payment) => payment?.refunds)
|
||||||
|
.flat(1)
|
||||||
|
.filter(Boolean)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container className="divide-y divide-dashed p-0">
|
<Container className="divide-y divide-dashed p-0">
|
||||||
<Header order={order} />
|
<Header />
|
||||||
|
|
||||||
<PaymentBreakdown
|
<PaymentBreakdown
|
||||||
payments={order.payments}
|
payments={payments}
|
||||||
refunds={order.refunds}
|
refunds={refunds}
|
||||||
currencyCode={order.currency_code}
|
currencyCode={order.currency_code}
|
||||||
/>
|
/>
|
||||||
<Total payments={order.payments} currencyCode={order.currency_code} />
|
|
||||||
|
<Total payments={payments} currencyCode={order.currency_code} />
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const Header = ({ order }: { order: Order }) => {
|
const Header = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const hasCapturedPayment = order.payments.some((p) => !!p.captured_at)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between px-6 py-4">
|
<div className="flex items-center justify-between px-6 py-4">
|
||||||
<Heading level="h2">{t("orders.payment.title")}</Heading>
|
<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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -120,6 +124,39 @@ const Payment = ({
|
|||||||
currencyCode: string
|
currencyCode: string
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
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] = (
|
const [status, color] = (
|
||||||
payment.captured_at ? ["Captured", "green"] : ["Pending", "orange"]
|
payment.captured_at ? ["Captured", "green"] : ["Pending", "orange"]
|
||||||
@@ -185,7 +222,8 @@ const Payment = ({
|
|||||||
})}
|
})}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
<Button size="small" variant="secondary">
|
|
||||||
|
<Button size="small" variant="secondary" onClick={handleCapture}>
|
||||||
{t("orders.payment.capture")}
|
{t("orders.payment.capture")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -31,6 +31,10 @@ const DEFAULT_RELATIONS = [
|
|||||||
"*fulfillments",
|
"*fulfillments",
|
||||||
"*fulfillments.items",
|
"*fulfillments.items",
|
||||||
"*fulfillments.labels",
|
"*fulfillments.labels",
|
||||||
|
"*fulfillments.labels",
|
||||||
|
"*payment_collections",
|
||||||
|
"*payment_collections.payments",
|
||||||
|
"*payment_collections.payments.refunds",
|
||||||
]
|
]
|
||||||
|
|
||||||
export const DEFAULT_FIELDS = `${DEFAULT_PROPERTIES.join(
|
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 { OrderCustomerSection } from "./components/order-customer-section"
|
||||||
import { OrderFulfillmentSection } from "./components/order-fulfillment-section"
|
import { OrderFulfillmentSection } from "./components/order-fulfillment-section"
|
||||||
import { OrderGeneralSection } from "./components/order-general-section"
|
import { OrderGeneralSection } from "./components/order-general-section"
|
||||||
|
import { OrderPaymentSection } from "./components/order-payment-section"
|
||||||
import { OrderSummarySection } from "./components/order-summary-section"
|
import { OrderSummarySection } from "./components/order-summary-section"
|
||||||
import { DEFAULT_FIELDS } from "./constants"
|
import { DEFAULT_FIELDS } from "./constants"
|
||||||
import { orderLoader } from "./loader"
|
import { orderLoader } from "./loader"
|
||||||
@@ -51,7 +52,7 @@ export const OrderDetail = () => {
|
|||||||
<div className="flex w-full flex-col gap-y-3">
|
<div className="flex w-full flex-col gap-y-3">
|
||||||
<OrderGeneralSection order={order} />
|
<OrderGeneralSection order={order} />
|
||||||
<OrderSummarySection order={order} />
|
<OrderSummarySection order={order} />
|
||||||
{/* <OrderPaymentSection order={order} />*/}
|
<OrderPaymentSection order={order} />
|
||||||
<OrderFulfillmentSection order={order} />
|
<OrderFulfillmentSection order={order} />
|
||||||
{after.widgets.map((w, i) => {
|
{after.widgets.map((w, i) => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { InventoryItem } from "./inventory-item"
|
|||||||
import { Invite } from "./invite"
|
import { Invite } from "./invite"
|
||||||
import { Notification } from "./notification"
|
import { Notification } from "./notification"
|
||||||
import { Order } from "./order"
|
import { Order } from "./order"
|
||||||
|
import { Payment } from "./payment"
|
||||||
import { PriceList } from "./price-list"
|
import { PriceList } from "./price-list"
|
||||||
import { PricePreference } from "./price-preference"
|
import { PricePreference } from "./price-preference"
|
||||||
import { Product } from "./product"
|
import { Product } from "./product"
|
||||||
@@ -59,6 +60,7 @@ export class Admin {
|
|||||||
public productTag: ProductTag
|
public productTag: ProductTag
|
||||||
public user: User
|
public user: User
|
||||||
public currency: Currency
|
public currency: Currency
|
||||||
|
public payment: Payment
|
||||||
|
|
||||||
constructor(client: Client) {
|
constructor(client: Client) {
|
||||||
this.invite = new Invite(client)
|
this.invite = new Invite(client)
|
||||||
@@ -90,5 +92,6 @@ export class Admin {
|
|||||||
this.productTag = new ProductTag(client)
|
this.productTag = new ProductTag(client)
|
||||||
this.user = new User(client)
|
this.user = new User(client)
|
||||||
this.currency = new Currency(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 {
|
import {
|
||||||
BaseOrder,
|
BaseOrder,
|
||||||
BaseOrderAddress,
|
BaseOrderAddress,
|
||||||
@@ -6,7 +7,10 @@ import {
|
|||||||
BaseOrderShippingMethod,
|
BaseOrderShippingMethod,
|
||||||
} from "./common"
|
} from "./common"
|
||||||
|
|
||||||
export interface AdminOrder extends BaseOrder {}
|
export interface AdminOrder extends BaseOrder {
|
||||||
|
payment_collections: AdminPaymentCollection[]
|
||||||
|
}
|
||||||
|
|
||||||
export interface AdminOrderLineItem extends BaseOrderLineItem {}
|
export interface AdminOrderLineItem extends BaseOrderLineItem {}
|
||||||
export interface AdminOrderFilters extends BaseOrderFilters {}
|
export interface AdminOrderFilters extends BaseOrderFilters {}
|
||||||
export interface AdminOrderAddress extends BaseOrderAddress {}
|
export interface AdminOrderAddress extends BaseOrderAddress {}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
BasePayment,
|
||||||
BasePaymentCollection,
|
BasePaymentCollection,
|
||||||
BasePaymentCollectionFilters,
|
BasePaymentCollectionFilters,
|
||||||
BasePaymentProvider,
|
BasePaymentProvider,
|
||||||
@@ -11,6 +12,7 @@ export interface AdminPaymentProvider extends BasePaymentProvider {
|
|||||||
is_enabled: boolean
|
is_enabled: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AdminPayment extends BasePayment {}
|
||||||
export interface AdminPaymentCollection extends BasePaymentCollection {}
|
export interface AdminPaymentCollection extends BasePaymentCollection {}
|
||||||
export interface AdminPaymentSession extends BasePaymentSession {}
|
export interface AdminPaymentSession extends BasePaymentSession {}
|
||||||
|
|
||||||
@@ -21,3 +23,11 @@ export interface AdminPaymentProviderFilters
|
|||||||
export interface AdminPaymentCollectionFilters
|
export interface AdminPaymentCollectionFilters
|
||||||
extends BasePaymentCollectionFilters {}
|
extends BasePaymentCollectionFilters {}
|
||||||
export interface AdminPaymentSessionFilters extends BasePaymentSessionFilters {}
|
export interface AdminPaymentSessionFilters extends BasePaymentSessionFilters {}
|
||||||
|
|
||||||
|
export interface AdminCapturePayment {
|
||||||
|
amount?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminPaymentResponse {
|
||||||
|
payment: AdminPayment
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user