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:
Riqwan ThamirandCarlos R. L. Rodrigues authored and GitHub committed 2024-08-20 18:40:58 +02:00
1 parent 29830f0077
commit 430d9a38c4
10 files changed
+193 -162

No files matched your search

@@ -557,7 +557,11 @@ medusaIntegrationTestRunner({
adminHeaders adminHeaders
) )
await api.post(`/admin/claims/${claimId2}/request`, {}, adminHeaders) const testRes = await api.post(
`/admin/claims/${claimId2}/request`,
{},
adminHeaders
)
claimId = baseClaim.id claimId = baseClaim.id
item = order.items[0] item = order.items[0]
@@ -682,6 +686,17 @@ medusaIntegrationTestRunner({
await api.get(`/admin/orders/${order.id}`, adminHeaders) await api.get(`/admin/orders/${order.id}`, adminHeaders)
).data.order ).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( const fulfillableItem = fulfillOrder.items.find(
(item) => item.detail.fulfilled_quantity === 0 (item) => item.detail.fulfilled_quantity === 0
) )
@@ -720,13 +735,26 @@ medusaIntegrationTestRunner({
) )
}) })
it("should create a payment collection successfully and throw on multiple", async () => { it("should create a payment collection successfully", async () => {
const paymentDelta = 171.5 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 = ( const paymentCollection = (
await api.post( await api.post(
`/admin/payment-collections`, `/admin/payment-collections`,
{ order_id: order.id }, { order_id: order.id, amount: 100 },
adminHeaders adminHeaders
) )
).data.payment_collection ).data.payment_collection
@@ -734,28 +762,14 @@ medusaIntegrationTestRunner({
expect(paymentCollection).toEqual( expect(paymentCollection).toEqual(
expect.objectContaining({ expect.objectContaining({
currency_code: "usd", currency_code: "usd",
amount: paymentDelta, amount: 100,
payment_sessions: [], 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 = ( const deleted = (
await api.delete( await api.delete(
`/admin/payment-collections/${paymentCollection.id}`, `/admin/payment-collections/${paymentCollections[0].id}`,
adminHeaders adminHeaders
) )
).data ).data
@@ -973,7 +987,8 @@ medusaIntegrationTestRunner({
}, },
adminHeaders adminHeaders
) )
await api.post(
const { response } = await api.post(
`/admin/claims/${baseClaim.id}/request`, `/admin/claims/${baseClaim.id}/request`,
{}, {},
adminHeaders adminHeaders
@@ -1,18 +1,15 @@
import { CheckCircleSolid, SquareTwoStack } from "@medusajs/icons" import { CheckCircleSolid, SquareTwoStack } from "@medusajs/icons"
import { AdminOrder } from "@medusajs/types" import { AdminOrder, AdminPaymentCollection } from "@medusajs/types"
import { Button, toast, Tooltip } from "@medusajs/ui" import { Button, Tooltip } from "@medusajs/ui"
import copy from "copy-to-clipboard" import copy from "copy-to-clipboard"
import React, { useState } from "react" import React, { useState } from "react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import {
useCreatePaymentCollection,
useDeletePaymentCollection,
} from "../../../../../hooks/api"
import { getStylizedAmount } from "../../../../../lib/money-amount-helpers" import { getStylizedAmount } from "../../../../../lib/money-amount-helpers"
export const MEDUSA_BACKEND_URL = __STOREFRONT_URL__ ?? "http://localhost:8000" export const MEDUSA_BACKEND_URL = __STOREFRONT_URL__ ?? "http://localhost:8000"
type CopyPaymentLinkProps = { type CopyPaymentLinkProps = {
paymentCollection: AdminPaymentCollection
order: AdminOrder order: AdminOrder
} }
@@ -20,18 +17,11 @@ type CopyPaymentLinkProps = {
* This component is based on the `button` element and supports all of its props * This component is based on the `button` element and supports all of its props
*/ */
const CopyPaymentLink = React.forwardRef<any, CopyPaymentLinkProps>( const CopyPaymentLink = React.forwardRef<any, CopyPaymentLinkProps>(
({ order }: CopyPaymentLinkProps, ref) => { ({ paymentCollection, order }: CopyPaymentLinkProps, ref) => {
const [isCreating, setIsCreating] = useState(false)
const [url, setUrl] = useState("")
const [done, setDone] = useState(false) const [done, setDone] = useState(false)
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const [text, setText] = useState("CopyPaymentLink") const [text, setText] = useState("CopyPaymentLink")
const { t } = useTranslation() const { t } = useTranslation()
const { mutateAsync: createPaymentCollection } =
useCreatePaymentCollection()
const { mutateAsync: deletePaymentCollection } =
useDeletePaymentCollection()
const copyToClipboard = async ( const copyToClipboard = async (
e: e:
@@ -40,53 +30,11 @@ const CopyPaymentLink = React.forwardRef<any, CopyPaymentLinkProps>(
) => { ) => {
e.stopPropagation() 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) setDone(true)
copy(url) copy(`${MEDUSA_BACKEND_URL}/payment-collection/${paymentCollection.id}`)
setTimeout(() => { setTimeout(() => {
setDone(false) setDone(false)
setUrl("")
}, 2000) }, 2000)
} }
@@ -109,7 +57,6 @@ const CopyPaymentLink = React.forwardRef<any, CopyPaymentLinkProps>(
size="small" size="small"
aria-label="CopyPaymentLink code snippet" aria-label="CopyPaymentLink code snippet"
onClick={copyToClipboard} onClick={copyToClipboard}
isLoading={isCreating}
> >
{done ? ( {done ? (
<CheckCircleSolid className="inline" /> <CheckCircleSolid className="inline" />
@@ -118,7 +65,7 @@ const CopyPaymentLink = React.forwardRef<any, CopyPaymentLinkProps>(
)} )}
{t("orders.payment.paymentLink", { {t("orders.payment.paymentLink", {
amount: getStylizedAmount( amount: getStylizedAmount(
order?.summary?.pending_difference, paymentCollection.amount as number,
order?.currency_code order?.currency_code
), ),
})} })}
@@ -100,18 +100,12 @@ export const OrderSummarySection = ({ order }: OrderSummarySectionProps) => {
return false return false
}, [reservations]) }, [reservations])
// TODO: We need a way to link payment collections to a change order to const unpaidPaymentCollection = order.payment_collections.find(
// accurately differentiate order payments and order change payments (pc) => pc.status === "not_paid"
// This fix should be temporary.
const authorizedPaymentCollection = order.payment_collections.find(
(pc) =>
pc.status === "authorized" &&
pc.amount === order.summary?.pending_difference
) )
const showPayment = const showPayment =
typeof authorizedPaymentCollection === "undefined" && unpaidPaymentCollection && (order?.summary?.pending_difference || 0) > 0
(order?.summary?.pending_difference || 0) > 0
const showRefund = (order?.summary?.pending_difference || 0) < 0 const showRefund = (order?.summary?.pending_difference || 0) < 0
return ( return (
@@ -152,7 +146,12 @@ export const OrderSummarySection = ({ order }: OrderSummarySectionProps) => {
</Button> </Button>
)} )}
{showPayment && <CopyPaymentLink order={order} />} {showPayment && (
<CopyPaymentLink
paymentCollection={unpaidPaymentCollection}
order={order}
/>
)}
</div> </div>
)} )}
</Container> </Container>
@@ -32,6 +32,7 @@ import {
throwIfIsCancelled, throwIfIsCancelled,
throwIfOrderChangeIsNotActive, throwIfOrderChangeIsNotActive,
} from "../../utils/order-validation" } from "../../utils/order-validation"
import { createOrUpdateOrderPaymentCollectionWorkflow } from "../create-or-update-order-payment-collection"
export type ConfirmClaimRequestWorkflowInput = { export type ConfirmClaimRequestWorkflowInput = {
claim_id: string claim_id: string
@@ -385,6 +386,12 @@ export const confirmClaimRequestWorkflow = createWorkflow(
}) })
}) })
createOrUpdateOrderPaymentCollectionWorkflow.runAsStep({
input: {
order_id: order.id,
},
})
return new WorkflowResponse(orderPreview) 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 { Modules } from "@medusajs/utils"
import {
MathBN,
MedusaError,
Modules,
PaymentCollectionStatus,
} from "@medusajs/utils"
import { import {
WorkflowData, WorkflowData,
WorkflowResponse, WorkflowResponse,
createStep,
createWorkflow, createWorkflow,
transform, transform,
} from "@medusajs/workflows-sdk" } from "@medusajs/workflows-sdk"
import { createRemoteLinkStep, useRemoteQueryStep } from "../../common" import { createRemoteLinkStep, useRemoteQueryStep } from "../../common"
import { createPaymentCollectionsStep } from "../../definition" 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 = export const createOrderPaymentCollectionWorkflowId =
"create-order-payment-collection" "create-order-payment-collection"
/** /**
@@ -40,7 +18,7 @@ export const createOrderPaymentCollectionWorkflow = createWorkflow(
( (
input: WorkflowData<{ input: WorkflowData<{
order_id: string order_id: string
amount?: number amount: number
}> }>
) => { ) => {
const order = useRemoteQueryStep({ const order = useRemoteQueryStep({
@@ -51,57 +29,12 @@ export const createOrderPaymentCollectionWorkflow = createWorkflow(
list: false, 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( const paymentCollectionData = transform(
{ order, input }, { order, input },
({ 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 { return {
currency_code: order.currency_code, currency_code: order.currency_code,
amount: input.amount ?? pendingPayment, amount: input.amount,
region_id: order.region_id, region_id: order.region_id,
} }
} }
@@ -32,6 +32,7 @@ import {
throwIfIsCancelled, throwIfIsCancelled,
throwIfOrderChangeIsNotActive, throwIfOrderChangeIsNotActive,
} from "../../utils/order-validation" } from "../../utils/order-validation"
import { createOrUpdateOrderPaymentCollectionWorkflow } from "../create-or-update-order-payment-collection"
export type ConfirmExchangeRequestWorkflowInput = { export type ConfirmExchangeRequestWorkflowInput = {
exchange_id: string exchange_id: string
@@ -381,6 +382,12 @@ export const confirmExchangeRequestWorkflow = createWorkflow(
}) })
}) })
createOrUpdateOrderPaymentCollectionWorkflow.runAsStep({
input: {
order_id: order.id,
},
})
return new WorkflowResponse(orderPreview) return new WorkflowResponse(orderPreview)
} }
) )
@@ -15,6 +15,7 @@ import {
throwIfIsCancelled, throwIfIsCancelled,
throwIfOrderChangeIsNotActive, throwIfOrderChangeIsNotActive,
} from "../../utils/order-validation" } from "../../utils/order-validation"
import { createOrUpdateOrderPaymentCollectionWorkflow } from "../create-or-update-order-payment-collection"
export type ConfirmOrderEditRequestWorkflowInput = { export type ConfirmOrderEditRequestWorkflowInput = {
order_id: string order_id: string
@@ -161,6 +162,12 @@ export const confirmOrderEditRequestWorkflow = createWorkflow(
reserveInventoryStep(formatedInventoryItems) reserveInventoryStep(formatedInventoryItems)
createOrUpdateOrderPaymentCollectionWorkflow.runAsStep({
input: {
order_id: order.id,
},
})
return new WorkflowResponse(orderPreview) return new WorkflowResponse(orderPreview)
} }
) )
@@ -28,6 +28,7 @@ import {
throwIfIsCancelled, throwIfIsCancelled,
throwIfOrderChangeIsNotActive, throwIfOrderChangeIsNotActive,
} from "../../utils/order-validation" } from "../../utils/order-validation"
import { createOrUpdateOrderPaymentCollectionWorkflow } from "../create-or-update-order-payment-collection"
export type ConfirmReturnRequestWorkflowInput = { export type ConfirmReturnRequestWorkflowInput = {
return_id: string return_id: string
@@ -259,6 +260,12 @@ export const confirmReturnRequestWorkflow = createWorkflow(
confirmOrderChanges({ changes: [orderChange], orderId: order.id }) confirmOrderChanges({ changes: [orderChange], orderId: order.id })
) )
createOrUpdateOrderPaymentCollectionWorkflow.runAsStep({
input: {
order_id: order.id,
},
})
return new WorkflowResponse(orderPreview) return new WorkflowResponse(orderPreview)
} }
) )
@@ -12,6 +12,6 @@ export type AdminCreatePaymentCollectionType = z.infer<
export const AdminCreatePaymentCollection = z export const AdminCreatePaymentCollection = z
.object({ .object({
order_id: z.string(), order_id: z.string(),
amount: z.number().optional(), amount: z.number(),
}) })
.strict() .strict()