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:
Riqwan Thamir
2024-08-01 06:46:03 +00:00
committed by GitHub
parent 4a7b2fd625
commit 3169543d56
10 changed files with 160 additions and 30 deletions
@@ -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)
}
@@ -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 (
+3
View File
@@ -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)
}
}
+27
View File
@@ -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,
}
)
}
}
+5 -1
View File
@@ -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
}