feat(dashboard,core-flows,js-sdk,types): ability to mark payment as paid (#8679)
* feat(core-flows): create or update payment collections in RMA flows * chore: change ui to pick payment link from unpaid payment collection * Apply suggestions from code review Co-authored-by: Carlos R. L. Rodrigues <37986729+carlos-r-l-rodrigues@users.noreply.github.com> * chore: fix mathbn * feat(dashboard,core-flows,js-sdk,types): ability to mark payment as paid * chore: add captured bt --------- Co-authored-by: Carlos R. L. Rodrigues <37986729+carlos-r-l-rodrigues@users.noreply.github.com>
This commit is contained in:
1 parent
99eca64c20
commit
8bd284779e
12 files changed
+273
-6
No files matched your search
@@ -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 = (
|
const orderForPayment = (
|
||||||
await api.get(`/admin/orders/${order.id}`, adminHeaders)
|
await api.get(`/admin/orders/${order.id}`, adminHeaders)
|
||||||
).data.order
|
).data.order
|
||||||
@@ -746,12 +747,12 @@ medusaIntegrationTestRunner({
|
|||||||
expect(paymentCollections[0]).toEqual(
|
expect(paymentCollections[0]).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
status: "not_paid",
|
status: "not_paid",
|
||||||
amount: 171.5,
|
amount: paymentDelta,
|
||||||
currency_code: "usd",
|
currency_code: "usd",
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
const paymentCollection = (
|
const createdPaymentCollection = (
|
||||||
await api.post(
|
await api.post(
|
||||||
`/admin/payment-collections`,
|
`/admin/payment-collections`,
|
||||||
{ order_id: order.id, amount: 100 },
|
{ order_id: order.id, amount: 100 },
|
||||||
@@ -759,7 +760,7 @@ medusaIntegrationTestRunner({
|
|||||||
)
|
)
|
||||||
).data.payment_collection
|
).data.payment_collection
|
||||||
|
|
||||||
expect(paymentCollection).toEqual(
|
expect(createdPaymentCollection).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
currency_code: "usd",
|
currency_code: "usd",
|
||||||
amount: 100,
|
amount: 100,
|
||||||
@@ -769,16 +770,35 @@ medusaIntegrationTestRunner({
|
|||||||
|
|
||||||
const deleted = (
|
const deleted = (
|
||||||
await api.delete(
|
await api.delete(
|
||||||
`/admin/payment-collections/${paymentCollections[0].id}`,
|
`/admin/payment-collections/${createdPaymentCollection.id}`,
|
||||||
adminHeaders
|
adminHeaders
|
||||||
)
|
)
|
||||||
).data
|
).data
|
||||||
|
|
||||||
expect(deleted).toEqual({
|
expect(deleted).toEqual({
|
||||||
id: expect.any(String),
|
id: createdPaymentCollection.id,
|
||||||
object: "payment-collection",
|
object: "payment-collection",
|
||||||
deleted: true,
|
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,
|
||||||
|
})
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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 = (
|
export const useDeletePaymentCollection = (
|
||||||
options?: Omit<
|
options?: Omit<
|
||||||
UseMutationOptions<
|
UseMutationOptions<
|
||||||
|
|||||||
@@ -814,6 +814,7 @@
|
|||||||
"totalPaidByCustomer": "Total paid by customer",
|
"totalPaidByCustomer": "Total paid by customer",
|
||||||
"capture": "Capture payment",
|
"capture": "Capture payment",
|
||||||
"refund": "Refund",
|
"refund": "Refund",
|
||||||
|
"markAsPaid": "Mark as paid",
|
||||||
"statusLabel": "Payment status",
|
"statusLabel": "Payment status",
|
||||||
"statusTitle": "Payment Status",
|
"statusTitle": "Payment Status",
|
||||||
"status": {
|
"status": {
|
||||||
@@ -830,6 +831,8 @@
|
|||||||
},
|
},
|
||||||
"capturePayment": "Payment of {{amount}} will be captured.",
|
"capturePayment": "Payment of {{amount}} will be captured.",
|
||||||
"capturePaymentSuccess": "Payment of {{amount}} successfully 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",
|
"createRefund": "Create Refund",
|
||||||
"refundPaymentSuccess": "Refund of amount {{amount}} successful",
|
"refundPaymentSuccess": "Refund of amount {{amount}} successful",
|
||||||
"createRefundWrongQuantity": "Quantity should be a number between 1 and {{number}}",
|
"createRefundWrongQuantity": "Quantity should be a number between 1 and {{number}}",
|
||||||
|
|||||||
+60
@@ -27,18 +27,23 @@ import {
|
|||||||
Heading,
|
Heading,
|
||||||
StatusBadge,
|
StatusBadge,
|
||||||
Text,
|
Text,
|
||||||
|
toast,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
|
usePrompt,
|
||||||
} from "@medusajs/ui"
|
} from "@medusajs/ui"
|
||||||
|
|
||||||
|
import { AdminPaymentCollection } from "../../../../../../../../core/types/dist/http/payment/admin/entities"
|
||||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
import { ButtonMenu } from "../../../../../components/common/button-menu/button-menu.tsx"
|
import { ButtonMenu } from "../../../../../components/common/button-menu/button-menu.tsx"
|
||||||
import { Thumbnail } from "../../../../../components/common/thumbnail"
|
import { Thumbnail } from "../../../../../components/common/thumbnail"
|
||||||
import { useClaims } from "../../../../../hooks/api/claims.tsx"
|
import { useClaims } from "../../../../../hooks/api/claims.tsx"
|
||||||
import { useExchanges } from "../../../../../hooks/api/exchanges.tsx"
|
import { useExchanges } from "../../../../../hooks/api/exchanges.tsx"
|
||||||
import { useOrderPreview } from "../../../../../hooks/api/orders.tsx"
|
import { useOrderPreview } from "../../../../../hooks/api/orders.tsx"
|
||||||
|
import { useMarkPaymentCollectionAsPaid } from "../../../../../hooks/api/payment-collections.tsx"
|
||||||
import { useReservationItems } from "../../../../../hooks/api/reservations"
|
import { useReservationItems } from "../../../../../hooks/api/reservations"
|
||||||
import { useReturns } from "../../../../../hooks/api/returns"
|
import { useReturns } from "../../../../../hooks/api/returns"
|
||||||
import { useDate } from "../../../../../hooks/use-date"
|
import { useDate } from "../../../../../hooks/use-date"
|
||||||
|
import { formatCurrency } from "../../../../../lib/format-currency.ts"
|
||||||
import {
|
import {
|
||||||
getLocaleAmount,
|
getLocaleAmount,
|
||||||
getStylizedAmount,
|
getStylizedAmount,
|
||||||
@@ -54,6 +59,7 @@ type OrderSummarySectionProps = {
|
|||||||
export const OrderSummarySection = ({ order }: OrderSummarySectionProps) => {
|
export const OrderSummarySection = ({ order }: OrderSummarySectionProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const prompt = usePrompt()
|
||||||
|
|
||||||
const { reservations } = useReservationItems(
|
const { reservations } = useReservationItems(
|
||||||
{
|
{
|
||||||
@@ -104,10 +110,54 @@ export const OrderSummarySection = ({ order }: OrderSummarySectionProps) => {
|
|||||||
(pc) => pc.status === "not_paid"
|
(pc) => pc.status === "not_paid"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const { mutateAsync: markAsPaid } = useMarkPaymentCollectionAsPaid(
|
||||||
|
unpaidPaymentCollection?.id!
|
||||||
|
)
|
||||||
|
|
||||||
const showPayment =
|
const showPayment =
|
||||||
unpaidPaymentCollection && (order?.summary?.pending_difference || 0) > 0
|
unpaidPaymentCollection && (order?.summary?.pending_difference || 0) > 0
|
||||||
const showRefund = (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 (
|
return (
|
||||||
<Container className="divide-y divide-dashed p-0">
|
<Container className="divide-y divide-dashed p-0">
|
||||||
<Header order={order} orderPreview={orderPreview} />
|
<Header order={order} orderPreview={orderPreview} />
|
||||||
@@ -152,6 +202,16 @@ export const OrderSummarySection = ({ order }: OrderSummarySectionProps) => {
|
|||||||
order={order}
|
order={order}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{showPayment && (
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => handleMarkAsPaid(unpaidPaymentCollection)}
|
||||||
|
>
|
||||||
|
{t("orders.payment.markAsPaid")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Container>
|
</Container>
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export * from "./exchange/update-exchange-add-item"
|
|||||||
export * from "./exchange/update-exchange-shipping-method"
|
export * from "./exchange/update-exchange-shipping-method"
|
||||||
export * from "./get-order-detail"
|
export * from "./get-order-detail"
|
||||||
export * from "./get-orders-list"
|
export * from "./get-orders-list"
|
||||||
|
export * from "./mark-payment-collection-as-paid"
|
||||||
export * from "./order-edit/begin-order-edit"
|
export * from "./order-edit/begin-order-edit"
|
||||||
export * from "./order-edit/cancel-begin-order-edit"
|
export * from "./order-edit/cancel-begin-order-edit"
|
||||||
export * from "./order-edit/confirm-order-edit-request"
|
export * from "./order-edit/confirm-order-edit-request"
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -60,4 +60,21 @@ export class PaymentCollection {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async markAsPaid(
|
||||||
|
id: string,
|
||||||
|
body: HttpTypes.AdminMarkPaymentCollectionAsPaid,
|
||||||
|
query?: SelectParams,
|
||||||
|
headers?: ClientHeaders
|
||||||
|
) {
|
||||||
|
return await this.client.fetch<HttpTypes.AdminPaymentCollectionResponse>(
|
||||||
|
`/admin/payment-collections/${id}/mark-as-paid`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers,
|
||||||
|
body,
|
||||||
|
query,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -17,3 +17,7 @@ export interface AdminCreatePaymentCollection {
|
|||||||
order_id: string
|
order_id: string
|
||||||
amount?: number
|
amount?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AdminMarkPaymentCollectionAsPaid {
|
||||||
|
order_id: string
|
||||||
|
}
|
||||||
@@ -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<AdminMarkPaymentCollectionPaidType>,
|
||||||
|
res: MedusaResponse<HttpTypes.AdminPaymentCollectionResponse>
|
||||||
|
) => {
|
||||||
|
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 })
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import * as queryConfig from "./query-config"
|
|||||||
import {
|
import {
|
||||||
AdminCreatePaymentCollection,
|
AdminCreatePaymentCollection,
|
||||||
AdminGetPaymentCollectionParams,
|
AdminGetPaymentCollectionParams,
|
||||||
|
AdminMarkPaymentCollectionPaid,
|
||||||
} from "./validators"
|
} from "./validators"
|
||||||
|
|
||||||
export const adminPaymentCollectionsMiddlewares: MiddlewareRoute[] = [
|
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"],
|
method: ["DELETE"],
|
||||||
matcher: "/admin/payment-collections/:id",
|
matcher: "/admin/payment-collections/:id",
|
||||||
|
|||||||
@@ -3,7 +3,11 @@ export const defaultPaymentCollectionFields = [
|
|||||||
"currency_code",
|
"currency_code",
|
||||||
"amount",
|
"amount",
|
||||||
"status",
|
"status",
|
||||||
|
"authorized_amount",
|
||||||
|
"captured_amount",
|
||||||
|
"refunded_amount",
|
||||||
"*payment_sessions",
|
"*payment_sessions",
|
||||||
|
"*payments",
|
||||||
]
|
]
|
||||||
|
|
||||||
export const retrievePaymentCollectionTransformQueryConfig = {
|
export const retrievePaymentCollectionTransformQueryConfig = {
|
||||||
|
|||||||
@@ -15,3 +15,12 @@ export const AdminCreatePaymentCollection = z
|
|||||||
amount: z.number(),
|
amount: z.number(),
|
||||||
})
|
})
|
||||||
.strict()
|
.strict()
|
||||||
|
|
||||||
|
export type AdminMarkPaymentCollectionPaidType = z.infer<
|
||||||
|
typeof AdminMarkPaymentCollectionPaid
|
||||||
|
>
|
||||||
|
export const AdminMarkPaymentCollectionPaid = z
|
||||||
|
.object({
|
||||||
|
order_id: z.string(),
|
||||||
|
})
|
||||||
|
.strict()
|
||||||
Reference in new issue
Block a user