feat(core-flows): create or update payment collections in RMA flows (#8676)
* 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 --------- Co-authored-by: Carlos R. L. Rodrigues <37986729+carlos-r-l-rodrigues@users.noreply.github.com>
This commit is contained in:
@@ -557,7 +557,11 @@ medusaIntegrationTestRunner({
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
await api.post(`/admin/claims/${claimId2}/request`, {}, adminHeaders)
|
||||
const testRes = await api.post(
|
||||
`/admin/claims/${claimId2}/request`,
|
||||
{},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
claimId = baseClaim.id
|
||||
item = order.items[0]
|
||||
@@ -682,6 +686,17 @@ medusaIntegrationTestRunner({
|
||||
await api.get(`/admin/orders/${order.id}`, adminHeaders)
|
||||
).data.order
|
||||
|
||||
const paymentCollections = fulfillOrder.payment_collections
|
||||
|
||||
expect(paymentCollections).toHaveLength(1)
|
||||
expect(paymentCollections[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
status: "not_paid",
|
||||
amount: 171.5,
|
||||
currency_code: "usd",
|
||||
})
|
||||
)
|
||||
|
||||
const fulfillableItem = fulfillOrder.items.find(
|
||||
(item) => item.detail.fulfilled_quantity === 0
|
||||
)
|
||||
@@ -720,13 +735,26 @@ medusaIntegrationTestRunner({
|
||||
)
|
||||
})
|
||||
|
||||
it("should create a payment collection successfully and throw on multiple", async () => {
|
||||
const paymentDelta = 171.5
|
||||
it("should create a payment collection successfully", async () => {
|
||||
const orderForPayment = (
|
||||
await api.get(`/admin/orders/${order.id}`, adminHeaders)
|
||||
).data.order
|
||||
|
||||
const paymentCollections = orderForPayment.payment_collections
|
||||
|
||||
expect(paymentCollections).toHaveLength(1)
|
||||
expect(paymentCollections[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
status: "not_paid",
|
||||
amount: 171.5,
|
||||
currency_code: "usd",
|
||||
})
|
||||
)
|
||||
|
||||
const paymentCollection = (
|
||||
await api.post(
|
||||
`/admin/payment-collections`,
|
||||
{ order_id: order.id },
|
||||
{ order_id: order.id, amount: 100 },
|
||||
adminHeaders
|
||||
)
|
||||
).data.payment_collection
|
||||
@@ -734,28 +762,14 @@ medusaIntegrationTestRunner({
|
||||
expect(paymentCollection).toEqual(
|
||||
expect.objectContaining({
|
||||
currency_code: "usd",
|
||||
amount: paymentDelta,
|
||||
payment_sessions: [],
|
||||
amount: 100,
|
||||
status: "not_paid",
|
||||
})
|
||||
)
|
||||
|
||||
const { response } = await api
|
||||
.post(
|
||||
`/admin/payment-collections`,
|
||||
{ order_id: order.id },
|
||||
adminHeaders
|
||||
)
|
||||
.catch((e) => e)
|
||||
|
||||
expect(response.data).toEqual({
|
||||
type: "not_allowed",
|
||||
message:
|
||||
"Active payment collections were found. Complete existing ones or delete them before proceeding.",
|
||||
})
|
||||
|
||||
const deleted = (
|
||||
await api.delete(
|
||||
`/admin/payment-collections/${paymentCollection.id}`,
|
||||
`/admin/payment-collections/${paymentCollections[0].id}`,
|
||||
adminHeaders
|
||||
)
|
||||
).data
|
||||
@@ -973,7 +987,8 @@ medusaIntegrationTestRunner({
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
await api.post(
|
||||
|
||||
const { response } = await api.post(
|
||||
`/admin/claims/${baseClaim.id}/request`,
|
||||
{},
|
||||
adminHeaders
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
import { CheckCircleSolid, SquareTwoStack } from "@medusajs/icons"
|
||||
import { AdminOrder } from "@medusajs/types"
|
||||
import { Button, toast, Tooltip } from "@medusajs/ui"
|
||||
import { AdminOrder, AdminPaymentCollection } from "@medusajs/types"
|
||||
import { Button, Tooltip } from "@medusajs/ui"
|
||||
import copy from "copy-to-clipboard"
|
||||
import React, { useState } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import {
|
||||
useCreatePaymentCollection,
|
||||
useDeletePaymentCollection,
|
||||
} from "../../../../../hooks/api"
|
||||
import { getStylizedAmount } from "../../../../../lib/money-amount-helpers"
|
||||
|
||||
export const MEDUSA_BACKEND_URL = __STOREFRONT_URL__ ?? "http://localhost:8000"
|
||||
|
||||
type CopyPaymentLinkProps = {
|
||||
paymentCollection: AdminPaymentCollection
|
||||
order: AdminOrder
|
||||
}
|
||||
|
||||
@@ -20,18 +17,11 @@ type CopyPaymentLinkProps = {
|
||||
* This component is based on the `button` element and supports all of its props
|
||||
*/
|
||||
const CopyPaymentLink = React.forwardRef<any, CopyPaymentLinkProps>(
|
||||
({ order }: CopyPaymentLinkProps, ref) => {
|
||||
const [isCreating, setIsCreating] = useState(false)
|
||||
const [url, setUrl] = useState("")
|
||||
({ paymentCollection, order }: CopyPaymentLinkProps, ref) => {
|
||||
const [done, setDone] = useState(false)
|
||||
const [open, setOpen] = useState(false)
|
||||
const [text, setText] = useState("CopyPaymentLink")
|
||||
const { t } = useTranslation()
|
||||
const { mutateAsync: createPaymentCollection } =
|
||||
useCreatePaymentCollection()
|
||||
|
||||
const { mutateAsync: deletePaymentCollection } =
|
||||
useDeletePaymentCollection()
|
||||
|
||||
const copyToClipboard = async (
|
||||
e:
|
||||
@@ -40,53 +30,11 @@ const CopyPaymentLink = React.forwardRef<any, CopyPaymentLinkProps>(
|
||||
) => {
|
||||
e.stopPropagation()
|
||||
|
||||
if (!url?.length) {
|
||||
const activePaymentCollection = order.payment_collections.find(
|
||||
(pc) =>
|
||||
pc.status === "not_paid" &&
|
||||
pc.amount === order.summary?.pending_difference
|
||||
)
|
||||
|
||||
if (!activePaymentCollection) {
|
||||
setIsCreating(true)
|
||||
|
||||
const paymentCollectionsToDelete = order.payment_collections.filter(
|
||||
(pc) => pc.status === "not_paid"
|
||||
)
|
||||
|
||||
const promises = paymentCollectionsToDelete.map((paymentCollection) =>
|
||||
deletePaymentCollection(paymentCollection.id)
|
||||
)
|
||||
|
||||
await Promise.all(promises)
|
||||
|
||||
await createPaymentCollection(
|
||||
{ order_id: order.id },
|
||||
{
|
||||
onSuccess: (data) => {
|
||||
setUrl(
|
||||
`${MEDUSA_BACKEND_URL}/payment-collection/${data.payment_collection.id}`
|
||||
)
|
||||
},
|
||||
onError: (err) => {
|
||||
toast.error(err.message)
|
||||
},
|
||||
onSettled: () => setIsCreating(false),
|
||||
}
|
||||
)
|
||||
} else {
|
||||
setUrl(
|
||||
`${MEDUSA_BACKEND_URL}/payment-collection/${activePaymentCollection.id}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
setDone(true)
|
||||
copy(url)
|
||||
copy(`${MEDUSA_BACKEND_URL}/payment-collection/${paymentCollection.id}`)
|
||||
|
||||
setTimeout(() => {
|
||||
setDone(false)
|
||||
setUrl("")
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
@@ -109,7 +57,6 @@ const CopyPaymentLink = React.forwardRef<any, CopyPaymentLinkProps>(
|
||||
size="small"
|
||||
aria-label="CopyPaymentLink code snippet"
|
||||
onClick={copyToClipboard}
|
||||
isLoading={isCreating}
|
||||
>
|
||||
{done ? (
|
||||
<CheckCircleSolid className="inline" />
|
||||
@@ -118,7 +65,7 @@ const CopyPaymentLink = React.forwardRef<any, CopyPaymentLinkProps>(
|
||||
)}
|
||||
{t("orders.payment.paymentLink", {
|
||||
amount: getStylizedAmount(
|
||||
order?.summary?.pending_difference,
|
||||
paymentCollection.amount as number,
|
||||
order?.currency_code
|
||||
),
|
||||
})}
|
||||
|
||||
@@ -100,18 +100,12 @@ export const OrderSummarySection = ({ order }: OrderSummarySectionProps) => {
|
||||
return false
|
||||
}, [reservations])
|
||||
|
||||
// TODO: We need a way to link payment collections to a change order to
|
||||
// accurately differentiate order payments and order change payments
|
||||
// This fix should be temporary.
|
||||
const authorizedPaymentCollection = order.payment_collections.find(
|
||||
(pc) =>
|
||||
pc.status === "authorized" &&
|
||||
pc.amount === order.summary?.pending_difference
|
||||
const unpaidPaymentCollection = order.payment_collections.find(
|
||||
(pc) => pc.status === "not_paid"
|
||||
)
|
||||
|
||||
const showPayment =
|
||||
typeof authorizedPaymentCollection === "undefined" &&
|
||||
(order?.summary?.pending_difference || 0) > 0
|
||||
unpaidPaymentCollection && (order?.summary?.pending_difference || 0) > 0
|
||||
const showRefund = (order?.summary?.pending_difference || 0) < 0
|
||||
|
||||
return (
|
||||
@@ -152,7 +146,12 @@ export const OrderSummarySection = ({ order }: OrderSummarySectionProps) => {
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{showPayment && <CopyPaymentLink order={order} />}
|
||||
{showPayment && (
|
||||
<CopyPaymentLink
|
||||
paymentCollection={unpaidPaymentCollection}
|
||||
order={order}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Container>
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
throwIfIsCancelled,
|
||||
throwIfOrderChangeIsNotActive,
|
||||
} from "../../utils/order-validation"
|
||||
import { createOrUpdateOrderPaymentCollectionWorkflow } from "../create-or-update-order-payment-collection"
|
||||
|
||||
export type ConfirmClaimRequestWorkflowInput = {
|
||||
claim_id: string
|
||||
@@ -385,6 +386,12 @@ export const confirmClaimRequestWorkflow = createWorkflow(
|
||||
})
|
||||
})
|
||||
|
||||
createOrUpdateOrderPaymentCollectionWorkflow.runAsStep({
|
||||
input: {
|
||||
order_id: order.id,
|
||||
},
|
||||
})
|
||||
|
||||
return new WorkflowResponse(orderPreview)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import { PaymentCollectionDTO } from "@medusajs/types"
|
||||
import { MathBN, MedusaError, PaymentCollectionStatus } from "@medusajs/utils"
|
||||
import {
|
||||
createWorkflow,
|
||||
transform,
|
||||
when,
|
||||
WorkflowData,
|
||||
WorkflowResponse,
|
||||
} from "@medusajs/workflows-sdk"
|
||||
import { useRemoteQueryStep } from "../../common"
|
||||
import { updatePaymentCollectionStep } from "../../payment-collection"
|
||||
import { createOrderPaymentCollectionWorkflow } from "./create-order-payment-collection"
|
||||
|
||||
export const createOrUpdateOrderPaymentCollectionWorkflowId =
|
||||
"create-or-update-order-payment-collection"
|
||||
/**
|
||||
* This workflow creates or updates payment collection for an order.
|
||||
*/
|
||||
export const createOrUpdateOrderPaymentCollectionWorkflow = createWorkflow(
|
||||
createOrUpdateOrderPaymentCollectionWorkflowId,
|
||||
(
|
||||
input: WorkflowData<{
|
||||
order_id: string
|
||||
amount?: number
|
||||
}>
|
||||
) => {
|
||||
const order = useRemoteQueryStep({
|
||||
entry_point: "order",
|
||||
fields: ["id", "summary", "currency_code", "region_id"],
|
||||
variables: { id: input.order_id },
|
||||
throw_if_key_not_found: true,
|
||||
list: false,
|
||||
})
|
||||
|
||||
const orderPaymentCollections = useRemoteQueryStep({
|
||||
entry_point: "order_payment_collection",
|
||||
fields: ["payment_collection_id"],
|
||||
variables: { order_id: order.id },
|
||||
}).config({ name: "order-payment-collection-query" })
|
||||
|
||||
const orderPaymentCollectionIds = transform(
|
||||
{ orderPaymentCollections },
|
||||
({ orderPaymentCollections }) =>
|
||||
orderPaymentCollections.map((opc) => opc.payment_collection_id)
|
||||
)
|
||||
|
||||
const existingPaymentCollection = useRemoteQueryStep({
|
||||
entry_point: "payment_collection",
|
||||
fields: ["id", "status"],
|
||||
variables: {
|
||||
filters: {
|
||||
id: orderPaymentCollectionIds,
|
||||
status: [PaymentCollectionStatus.NOT_PAID],
|
||||
},
|
||||
},
|
||||
list: false,
|
||||
}).config({ name: "payment-collection-query" })
|
||||
|
||||
const amountPending = transform({ order, input }, ({ order, input }) => {
|
||||
const pendingPayment =
|
||||
order.summary.raw_pending_difference ?? order.summary.pending_difference
|
||||
|
||||
if (MathBN.gt(input.amount ?? 0, pendingPayment)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
`Amount cannot be greater than ${pendingPayment}`
|
||||
)
|
||||
}
|
||||
|
||||
return pendingPayment
|
||||
})
|
||||
|
||||
const updatedPaymentCollections = when(
|
||||
{ existingPaymentCollection, amountPending },
|
||||
({ existingPaymentCollection, amountPending }) => {
|
||||
return !!existingPaymentCollection?.id && MathBN.gt(amountPending, 0)
|
||||
}
|
||||
).then(() => {
|
||||
return updatePaymentCollectionStep({
|
||||
selector: { id: existingPaymentCollection.id },
|
||||
update: {
|
||||
amount: amountPending,
|
||||
},
|
||||
}) as PaymentCollectionDTO[]
|
||||
})
|
||||
|
||||
const createdPaymentCollection = when(
|
||||
{ existingPaymentCollection, amountPending },
|
||||
({ existingPaymentCollection, amountPending }) => {
|
||||
return !!!existingPaymentCollection?.id && MathBN.gt(amountPending, 0)
|
||||
}
|
||||
).then(() => {
|
||||
return createOrderPaymentCollectionWorkflow.runAsStep({
|
||||
input: {
|
||||
order_id: order.id,
|
||||
amount: amountPending,
|
||||
},
|
||||
}) as PaymentCollectionDTO[]
|
||||
})
|
||||
|
||||
const paymentCollections = transform(
|
||||
{ updatedPaymentCollections, createdPaymentCollection },
|
||||
({ updatedPaymentCollections, createdPaymentCollection }) =>
|
||||
updatedPaymentCollections || createdPaymentCollection
|
||||
)
|
||||
|
||||
return new WorkflowResponse(paymentCollections)
|
||||
}
|
||||
)
|
||||
@@ -1,35 +1,13 @@
|
||||
import { PaymentCollectionDTO } from "@medusajs/types"
|
||||
import {
|
||||
MathBN,
|
||||
MedusaError,
|
||||
Modules,
|
||||
PaymentCollectionStatus,
|
||||
} from "@medusajs/utils"
|
||||
import { Modules } from "@medusajs/utils"
|
||||
import {
|
||||
WorkflowData,
|
||||
WorkflowResponse,
|
||||
createStep,
|
||||
createWorkflow,
|
||||
transform,
|
||||
} from "@medusajs/workflows-sdk"
|
||||
import { createRemoteLinkStep, useRemoteQueryStep } from "../../common"
|
||||
import { createPaymentCollectionsStep } from "../../definition"
|
||||
|
||||
/**
|
||||
* This step validates that the order doesn't have an active payment collection.
|
||||
*/
|
||||
export const throwIfActivePaymentCollectionExists = createStep(
|
||||
"validate-existing-payment-collection",
|
||||
({ paymentCollection }: { paymentCollection: PaymentCollectionDTO }) => {
|
||||
if (paymentCollection) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
`Active payment collections were found. Complete existing ones or delete them before proceeding.`
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
export const createOrderPaymentCollectionWorkflowId =
|
||||
"create-order-payment-collection"
|
||||
/**
|
||||
@@ -40,7 +18,7 @@ export const createOrderPaymentCollectionWorkflow = createWorkflow(
|
||||
(
|
||||
input: WorkflowData<{
|
||||
order_id: string
|
||||
amount?: number
|
||||
amount: number
|
||||
}>
|
||||
) => {
|
||||
const order = useRemoteQueryStep({
|
||||
@@ -51,57 +29,12 @@ export const createOrderPaymentCollectionWorkflow = createWorkflow(
|
||||
list: false,
|
||||
})
|
||||
|
||||
const orderPaymentCollections = useRemoteQueryStep({
|
||||
entry_point: "order_payment_collection",
|
||||
fields: ["payment_collection_id"],
|
||||
variables: { order_id: order.id },
|
||||
}).config({ name: "order-payment-collection-query" })
|
||||
|
||||
const orderPaymentCollectionIds = transform(
|
||||
{ orderPaymentCollections },
|
||||
({ orderPaymentCollections }) =>
|
||||
orderPaymentCollections.map((opc) => opc.payment_collection_id)
|
||||
)
|
||||
|
||||
const paymentCollection = useRemoteQueryStep({
|
||||
entry_point: "payment_collection",
|
||||
fields: ["id", "status"],
|
||||
variables: {
|
||||
filters: {
|
||||
id: orderPaymentCollectionIds,
|
||||
status: [PaymentCollectionStatus.NOT_PAID],
|
||||
},
|
||||
},
|
||||
list: false,
|
||||
}).config({ name: "payment-collection-query" })
|
||||
|
||||
throwIfActivePaymentCollectionExists({ paymentCollection })
|
||||
|
||||
const paymentCollectionData = transform(
|
||||
{ order, input },
|
||||
({ order, input }) => {
|
||||
const pendingPayment = order.summary.raw_pending_difference
|
||||
|
||||
if (MathBN.lte(pendingPayment, 0)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
`Cannot create a payment collection for amount less than 0`
|
||||
)
|
||||
}
|
||||
|
||||
if (
|
||||
input.amount &&
|
||||
MathBN.gt(input.amount ?? pendingPayment, pendingPayment)
|
||||
) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
`Cannot create a payment collection for amount greater than ${pendingPayment}`
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
currency_code: order.currency_code,
|
||||
amount: input.amount ?? pendingPayment,
|
||||
amount: input.amount,
|
||||
region_id: order.region_id,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
throwIfIsCancelled,
|
||||
throwIfOrderChangeIsNotActive,
|
||||
} from "../../utils/order-validation"
|
||||
import { createOrUpdateOrderPaymentCollectionWorkflow } from "../create-or-update-order-payment-collection"
|
||||
|
||||
export type ConfirmExchangeRequestWorkflowInput = {
|
||||
exchange_id: string
|
||||
@@ -381,6 +382,12 @@ export const confirmExchangeRequestWorkflow = createWorkflow(
|
||||
})
|
||||
})
|
||||
|
||||
createOrUpdateOrderPaymentCollectionWorkflow.runAsStep({
|
||||
input: {
|
||||
order_id: order.id,
|
||||
},
|
||||
})
|
||||
|
||||
return new WorkflowResponse(orderPreview)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
throwIfIsCancelled,
|
||||
throwIfOrderChangeIsNotActive,
|
||||
} from "../../utils/order-validation"
|
||||
import { createOrUpdateOrderPaymentCollectionWorkflow } from "../create-or-update-order-payment-collection"
|
||||
|
||||
export type ConfirmOrderEditRequestWorkflowInput = {
|
||||
order_id: string
|
||||
@@ -161,6 +162,12 @@ export const confirmOrderEditRequestWorkflow = createWorkflow(
|
||||
|
||||
reserveInventoryStep(formatedInventoryItems)
|
||||
|
||||
createOrUpdateOrderPaymentCollectionWorkflow.runAsStep({
|
||||
input: {
|
||||
order_id: order.id,
|
||||
},
|
||||
})
|
||||
|
||||
return new WorkflowResponse(orderPreview)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
throwIfIsCancelled,
|
||||
throwIfOrderChangeIsNotActive,
|
||||
} from "../../utils/order-validation"
|
||||
import { createOrUpdateOrderPaymentCollectionWorkflow } from "../create-or-update-order-payment-collection"
|
||||
|
||||
export type ConfirmReturnRequestWorkflowInput = {
|
||||
return_id: string
|
||||
@@ -259,6 +260,12 @@ export const confirmReturnRequestWorkflow = createWorkflow(
|
||||
confirmOrderChanges({ changes: [orderChange], orderId: order.id })
|
||||
)
|
||||
|
||||
createOrUpdateOrderPaymentCollectionWorkflow.runAsStep({
|
||||
input: {
|
||||
order_id: order.id,
|
||||
},
|
||||
})
|
||||
|
||||
return new WorkflowResponse(orderPreview)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -12,6 +12,6 @@ export type AdminCreatePaymentCollectionType = z.infer<
|
||||
export const AdminCreatePaymentCollection = z
|
||||
.object({
|
||||
order_id: z.string(),
|
||||
amount: z.number().optional(),
|
||||
amount: z.number(),
|
||||
})
|
||||
.strict()
|
||||
|
||||
Reference in New Issue
Block a user