diff --git a/integration-tests/http/__tests__/claims/claims.spec.ts b/integration-tests/http/__tests__/claims/claims.spec.ts
index 5e7d79f655..7960ff7509 100644
--- a/integration-tests/http/__tests__/claims/claims.spec.ts
+++ b/integration-tests/http/__tests__/claims/claims.spec.ts
@@ -735,7 +735,8 @@ medusaIntegrationTestRunner({
)
})
- it("should create a payment collection successfully", async () => {
+ it("should create a payment collection successfully & mark as paid", async () => {
+ const paymentDelta = 171.5
const orderForPayment = (
await api.get(`/admin/orders/${order.id}`, adminHeaders)
).data.order
@@ -746,12 +747,12 @@ medusaIntegrationTestRunner({
expect(paymentCollections[0]).toEqual(
expect.objectContaining({
status: "not_paid",
- amount: 171.5,
+ amount: paymentDelta,
currency_code: "usd",
})
)
- const paymentCollection = (
+ const createdPaymentCollection = (
await api.post(
`/admin/payment-collections`,
{ order_id: order.id, amount: 100 },
@@ -759,7 +760,7 @@ medusaIntegrationTestRunner({
)
).data.payment_collection
- expect(paymentCollection).toEqual(
+ expect(createdPaymentCollection).toEqual(
expect.objectContaining({
currency_code: "usd",
amount: 100,
@@ -769,16 +770,35 @@ medusaIntegrationTestRunner({
const deleted = (
await api.delete(
- `/admin/payment-collections/${paymentCollections[0].id}`,
+ `/admin/payment-collections/${createdPaymentCollection.id}`,
adminHeaders
)
).data
expect(deleted).toEqual({
- id: expect.any(String),
+ id: createdPaymentCollection.id,
object: "payment-collection",
deleted: true,
})
+
+ const finalPaymentCollection = (
+ await api.post(
+ `/admin/payment-collections/${paymentCollections[0].id}/mark-as-paid`,
+ { order_id: order.id },
+ adminHeaders
+ )
+ ).data.payment_collection
+
+ expect(finalPaymentCollection).toEqual(
+ expect.objectContaining({
+ currency_code: "usd",
+ amount: paymentDelta,
+ status: "authorized",
+ authorized_amount: paymentDelta,
+ captured_amount: paymentDelta,
+ refunded_amount: 0,
+ })
+ )
})
})
diff --git a/packages/admin-next/dashboard/src/hooks/api/payment-collections.tsx b/packages/admin-next/dashboard/src/hooks/api/payment-collections.tsx
index 8164cda952..557ff4d457 100644
--- a/packages/admin-next/dashboard/src/hooks/api/payment-collections.tsx
+++ b/packages/admin-next/dashboard/src/hooks/api/payment-collections.tsx
@@ -35,6 +35,32 @@ export const useCreatePaymentCollection = (
})
}
+export const useMarkPaymentCollectionAsPaid = (
+ paymentCollectionId: string,
+ options?: UseMutationOptions<
+ HttpTypes.AdminPaymentCollectionResponse,
+ Error,
+ HttpTypes.AdminMarkPaymentCollectionAsPaid
+ >
+) => {
+ return useMutation({
+ mutationFn: (payload) =>
+ sdk.admin.paymentCollection.markAsPaid(paymentCollectionId, payload),
+ onSuccess: (data, variables, context) => {
+ queryClient.invalidateQueries({
+ queryKey: ordersQueryKeys.all,
+ })
+
+ queryClient.invalidateQueries({
+ queryKey: paymentCollectionQueryKeys.all,
+ })
+
+ options?.onSuccess?.(data, variables, context)
+ },
+ ...options,
+ })
+}
+
export const useDeletePaymentCollection = (
options?: Omit<
UseMutationOptions<
diff --git a/packages/admin-next/dashboard/src/i18n/translations/en.json b/packages/admin-next/dashboard/src/i18n/translations/en.json
index 4c890b0318..d3b6133df3 100644
--- a/packages/admin-next/dashboard/src/i18n/translations/en.json
+++ b/packages/admin-next/dashboard/src/i18n/translations/en.json
@@ -814,6 +814,7 @@
"totalPaidByCustomer": "Total paid by customer",
"capture": "Capture payment",
"refund": "Refund",
+ "markAsPaid": "Mark as paid",
"statusLabel": "Payment status",
"statusTitle": "Payment Status",
"status": {
@@ -830,6 +831,8 @@
},
"capturePayment": "Payment of {{amount}} will be captured.",
"capturePaymentSuccess": "Payment of {{amount}} successfully captured",
+ "markAsPaidPayment": "Payment of {{amount}} will be marked as paid.",
+ "markAsPaidPaymentSuccess": "Payment of {{amount}} successfully marked as paid",
"createRefund": "Create Refund",
"refundPaymentSuccess": "Refund of amount {{amount}} successful",
"createRefundWrongQuantity": "Quantity should be a number between 1 and {{number}}",
diff --git a/packages/admin-next/dashboard/src/routes/orders/order-detail/components/order-summary-section/order-summary-section.tsx b/packages/admin-next/dashboard/src/routes/orders/order-detail/components/order-summary-section/order-summary-section.tsx
index 31658f7984..c932fc11e9 100644
--- a/packages/admin-next/dashboard/src/routes/orders/order-detail/components/order-summary-section/order-summary-section.tsx
+++ b/packages/admin-next/dashboard/src/routes/orders/order-detail/components/order-summary-section/order-summary-section.tsx
@@ -27,18 +27,23 @@ import {
Heading,
StatusBadge,
Text,
+ toast,
Tooltip,
+ usePrompt,
} from "@medusajs/ui"
+import { AdminPaymentCollection } from "../../../../../../../../core/types/dist/http/payment/admin/entities"
import { ActionMenu } from "../../../../../components/common/action-menu"
import { ButtonMenu } from "../../../../../components/common/button-menu/button-menu.tsx"
import { Thumbnail } from "../../../../../components/common/thumbnail"
import { useClaims } from "../../../../../hooks/api/claims.tsx"
import { useExchanges } from "../../../../../hooks/api/exchanges.tsx"
import { useOrderPreview } from "../../../../../hooks/api/orders.tsx"
+import { useMarkPaymentCollectionAsPaid } from "../../../../../hooks/api/payment-collections.tsx"
import { useReservationItems } from "../../../../../hooks/api/reservations"
import { useReturns } from "../../../../../hooks/api/returns"
import { useDate } from "../../../../../hooks/use-date"
+import { formatCurrency } from "../../../../../lib/format-currency.ts"
import {
getLocaleAmount,
getStylizedAmount,
@@ -54,6 +59,7 @@ type OrderSummarySectionProps = {
export const OrderSummarySection = ({ order }: OrderSummarySectionProps) => {
const { t } = useTranslation()
const navigate = useNavigate()
+ const prompt = usePrompt()
const { reservations } = useReservationItems(
{
@@ -104,10 +110,54 @@ export const OrderSummarySection = ({ order }: OrderSummarySectionProps) => {
(pc) => pc.status === "not_paid"
)
+ const { mutateAsync: markAsPaid } = useMarkPaymentCollectionAsPaid(
+ unpaidPaymentCollection?.id!
+ )
+
const showPayment =
unpaidPaymentCollection && (order?.summary?.pending_difference || 0) > 0
const showRefund = (order?.summary?.pending_difference || 0) < 0
+ const handleMarkAsPaid = async (
+ paymentCollection: AdminPaymentCollection
+ ) => {
+ const res = await prompt({
+ title: t("orders.payment.markAsPaid"),
+ description: t("orders.payment.markAsPaidPayment", {
+ amount: formatCurrency(
+ paymentCollection.amount as number,
+ order.currency_code
+ ),
+ }),
+ confirmText: t("actions.confirm"),
+ cancelText: t("actions.cancel"),
+ variant: "confirmation",
+ })
+
+ if (!res) {
+ return
+ }
+
+ await markAsPaid(
+ { order_id: order.id },
+ {
+ onSuccess: () => {
+ toast.success(
+ t("orders.payment.markAsPaidPaymentSuccess", {
+ amount: formatCurrency(
+ paymentCollection.amount as number,
+ order.currency_code
+ ),
+ })
+ )
+ },
+ onError: (error) => {
+ toast.error(error.message)
+ },
+ }
+ )
+ }
+
return (
@@ -152,6 +202,16 @@ export const OrderSummarySection = ({ order }: OrderSummarySectionProps) => {
order={order}
/>
)}
+
+ {showPayment && (
+
+ )}
)}
diff --git a/packages/core/core-flows/src/order/workflows/index.ts b/packages/core/core-flows/src/order/workflows/index.ts
index 92273db68e..f6e6294c9c 100644
--- a/packages/core/core-flows/src/order/workflows/index.ts
+++ b/packages/core/core-flows/src/order/workflows/index.ts
@@ -41,6 +41,7 @@ export * from "./exchange/update-exchange-add-item"
export * from "./exchange/update-exchange-shipping-method"
export * from "./get-order-detail"
export * from "./get-orders-list"
+export * from "./mark-payment-collection-as-paid"
export * from "./order-edit/begin-order-edit"
export * from "./order-edit/cancel-begin-order-edit"
export * from "./order-edit/confirm-order-edit-request"
diff --git a/packages/core/core-flows/src/order/workflows/mark-payment-collection-as-paid.ts b/packages/core/core-flows/src/order/workflows/mark-payment-collection-as-paid.ts
new file mode 100644
index 0000000000..d02029183a
--- /dev/null
+++ b/packages/core/core-flows/src/order/workflows/mark-payment-collection-as-paid.ts
@@ -0,0 +1,79 @@
+import { PaymentCollectionDTO } from "@medusajs/types"
+import { MedusaError } from "@medusajs/utils"
+import {
+ WorkflowData,
+ WorkflowResponse,
+ createStep,
+ createWorkflow,
+} from "@medusajs/workflows-sdk"
+import { useRemoteQueryStep } from "../../common"
+import { createPaymentSessionsWorkflow } from "../../definition"
+import {
+ authorizePaymentSessionStep,
+ capturePaymentWorkflow,
+} from "../../payment"
+
+/**
+ * This step validates that the payment collection is not_paid
+ */
+export const throwUnlessPaymentCollectionNotPaid = createStep(
+ "validate-existing-payment-collection",
+ ({ paymentCollection }: { paymentCollection: PaymentCollectionDTO }) => {
+ if (paymentCollection.status !== "not_paid") {
+ throw new MedusaError(
+ MedusaError.Types.NOT_ALLOWED,
+ `Can only mark 'not_paid' payment collection as paid`
+ )
+ }
+ }
+)
+
+const systemPaymentProviderId = "pp_system_default"
+export const markPaymentCollectionAsPaidId = "mark-payment-collection-as-paid"
+/**
+ * This workflow marks a payment collection for an order as paid.
+ */
+export const markPaymentCollectionAsPaid = createWorkflow(
+ markPaymentCollectionAsPaidId,
+ (
+ input: WorkflowData<{
+ payment_collection_id: string
+ order_id: string
+ captured_by?: string
+ }>
+ ) => {
+ const paymentCollection = useRemoteQueryStep({
+ entry_point: "payment_collection",
+ fields: ["id", "status", "amount"],
+ variables: { id: input.payment_collection_id },
+ throw_if_key_not_found: true,
+ list: false,
+ })
+
+ throwUnlessPaymentCollectionNotPaid({ paymentCollection })
+
+ const paymentSession = createPaymentSessionsWorkflow.runAsStep({
+ input: {
+ payment_collection_id: paymentCollection.id,
+ provider_id: systemPaymentProviderId,
+ data: {},
+ context: {},
+ },
+ })
+
+ const payment = authorizePaymentSessionStep({
+ id: paymentSession.id,
+ context: { order_id: input.order_id },
+ })
+
+ capturePaymentWorkflow.runAsStep({
+ input: {
+ payment_id: payment.id,
+ captured_by: input.captured_by,
+ amount: paymentCollection.amount,
+ },
+ })
+
+ return new WorkflowResponse(payment)
+ }
+)
diff --git a/packages/core/js-sdk/src/admin/payment-collection.ts b/packages/core/js-sdk/src/admin/payment-collection.ts
index ca8dacc0cf..12551002a7 100644
--- a/packages/core/js-sdk/src/admin/payment-collection.ts
+++ b/packages/core/js-sdk/src/admin/payment-collection.ts
@@ -60,4 +60,21 @@ export class PaymentCollection {
}
)
}
+
+ async markAsPaid(
+ id: string,
+ body: HttpTypes.AdminMarkPaymentCollectionAsPaid,
+ query?: SelectParams,
+ headers?: ClientHeaders
+ ) {
+ return await this.client.fetch(
+ `/admin/payment-collections/${id}/mark-as-paid`,
+ {
+ method: "POST",
+ headers,
+ body,
+ query,
+ }
+ )
+ }
}
diff --git a/packages/core/types/src/http/payment/admin/payloads.ts b/packages/core/types/src/http/payment/admin/payloads.ts
index 3396965b1a..b407662567 100644
--- a/packages/core/types/src/http/payment/admin/payloads.ts
+++ b/packages/core/types/src/http/payment/admin/payloads.ts
@@ -17,3 +17,7 @@ export interface AdminCreatePaymentCollection {
order_id: string
amount?: number
}
+
+export interface AdminMarkPaymentCollectionAsPaid {
+ order_id: string
+}
diff --git a/packages/medusa/src/api/admin/payment-collections/[id]/mark-as-paid/route.ts b/packages/medusa/src/api/admin/payment-collections/[id]/mark-as-paid/route.ts
new file mode 100644
index 0000000000..46767f49b7
--- /dev/null
+++ b/packages/medusa/src/api/admin/payment-collections/[id]/mark-as-paid/route.ts
@@ -0,0 +1,32 @@
+import { markPaymentCollectionAsPaid } from "@medusajs/core-flows"
+import { HttpTypes } from "@medusajs/types"
+import {
+ AuthenticatedMedusaRequest,
+ MedusaResponse,
+} from "../../../../../types/routing"
+import { refetchEntity } from "../../../../utils/refetch-entity"
+import { AdminMarkPaymentCollectionPaidType } from "../../validators"
+
+export const POST = async (
+ req: AuthenticatedMedusaRequest,
+ res: MedusaResponse
+) => {
+ const { id } = req.params
+
+ await markPaymentCollectionAsPaid(req.scope).run({
+ input: {
+ ...req.body,
+ payment_collection_id: id,
+ captured_by: req.auth_context.actor_id,
+ },
+ })
+
+ const paymentCollection = await refetchEntity(
+ "payment_collection",
+ id,
+ req.scope,
+ req.remoteQueryConfig.fields
+ )
+
+ res.status(200).json({ payment_collection: paymentCollection })
+}
diff --git a/packages/medusa/src/api/admin/payment-collections/middlewares.ts b/packages/medusa/src/api/admin/payment-collections/middlewares.ts
index 4d8400dad6..9ab5f65199 100644
--- a/packages/medusa/src/api/admin/payment-collections/middlewares.ts
+++ b/packages/medusa/src/api/admin/payment-collections/middlewares.ts
@@ -5,6 +5,7 @@ import * as queryConfig from "./query-config"
import {
AdminCreatePaymentCollection,
AdminGetPaymentCollectionParams,
+ AdminMarkPaymentCollectionPaid,
} from "./validators"
export const adminPaymentCollectionsMiddlewares: MiddlewareRoute[] = [
@@ -19,6 +20,17 @@ export const adminPaymentCollectionsMiddlewares: MiddlewareRoute[] = [
),
],
},
+ {
+ method: ["POST"],
+ matcher: "/admin/payment-collections/:id/mark-as-paid",
+ middlewares: [
+ validateAndTransformBody(AdminMarkPaymentCollectionPaid),
+ validateAndTransformQuery(
+ AdminGetPaymentCollectionParams,
+ queryConfig.retrievePaymentCollectionTransformQueryConfig
+ ),
+ ],
+ },
{
method: ["DELETE"],
matcher: "/admin/payment-collections/:id",
diff --git a/packages/medusa/src/api/admin/payment-collections/query-config.ts b/packages/medusa/src/api/admin/payment-collections/query-config.ts
index 5dc3aa9d48..00372fff77 100644
--- a/packages/medusa/src/api/admin/payment-collections/query-config.ts
+++ b/packages/medusa/src/api/admin/payment-collections/query-config.ts
@@ -3,7 +3,11 @@ export const defaultPaymentCollectionFields = [
"currency_code",
"amount",
"status",
+ "authorized_amount",
+ "captured_amount",
+ "refunded_amount",
"*payment_sessions",
+ "*payments",
]
export const retrievePaymentCollectionTransformQueryConfig = {
diff --git a/packages/medusa/src/api/admin/payment-collections/validators.ts b/packages/medusa/src/api/admin/payment-collections/validators.ts
index d9d6c9913f..e3b424f979 100644
--- a/packages/medusa/src/api/admin/payment-collections/validators.ts
+++ b/packages/medusa/src/api/admin/payment-collections/validators.ts
@@ -15,3 +15,12 @@ export const AdminCreatePaymentCollection = z
amount: z.number(),
})
.strict()
+
+export type AdminMarkPaymentCollectionPaidType = z.infer<
+ typeof AdminMarkPaymentCollectionPaid
+>
+export const AdminMarkPaymentCollectionPaid = z
+ .object({
+ order_id: z.string(),
+ })
+ .strict()