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:
@@ -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,
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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<
|
||||
|
||||
@@ -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}}",
|
||||
|
||||
@@ -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 (
|
||||
<Container className="divide-y divide-dashed p-0">
|
||||
<Header order={order} orderPreview={orderPreview} />
|
||||
@@ -152,6 +202,16 @@ export const OrderSummarySection = ({ order }: OrderSummarySectionProps) => {
|
||||
order={order}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showPayment && (
|
||||
<Button
|
||||
size="small"
|
||||
variant="secondary"
|
||||
onClick={() => handleMarkAsPaid(unpaidPaymentCollection)}
|
||||
>
|
||||
{t("orders.payment.markAsPaid")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Container>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
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 {
|
||||
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",
|
||||
|
||||
@@ -3,7 +3,11 @@ export const defaultPaymentCollectionFields = [
|
||||
"currency_code",
|
||||
"amount",
|
||||
"status",
|
||||
"authorized_amount",
|
||||
"captured_amount",
|
||||
"refunded_amount",
|
||||
"*payment_sessions",
|
||||
"*payments",
|
||||
]
|
||||
|
||||
export const retrievePaymentCollectionTransformQueryConfig = {
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user