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 Thamir
2024-08-20 18:40:58 +02:00
committed by GitHub
parent 29830f0077
commit 430d9a38c4
10 changed files with 193 additions and 162 deletions

View File

@@ -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

View File

@@ -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
),
})}

View File

@@ -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>

View File

@@ -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)
}
)

View File

@@ -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)
}
)

View File

@@ -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,
}
}

View File

@@ -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)
}
)

View File

@@ -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)
}
)

View File

@@ -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)
}
)

View File

@@ -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()