fix(core-flows,dashboard): handling authorized payment collection on OE (#11958)
* wip: handling authorized payment collection on OE * fix: update condition * fix: condition for creation * chore: remove commented code * chore: changeset, refactor * chore: typo, comments * fix: add a test case * fix: reorg workflows, partially captured * fix: status enum type
This commit is contained in:
1 parent
2270f29ec5
commit
a8513019db
9 files changed
+367
-15
No files matched your search
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/dashboard": patch
|
||||||
|
"@medusajs/core-flows": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix(core-flows,dashboard): handle PaymentCollection managemenet on OrderEdit when there is authorized amount
|
||||||
@@ -8,7 +8,10 @@ import {
|
|||||||
import {
|
import {
|
||||||
adminHeaders,
|
adminHeaders,
|
||||||
createAdminUser,
|
createAdminUser,
|
||||||
|
generatePublishableKey,
|
||||||
|
generateStoreHeaders,
|
||||||
} from "../../../helpers/create-admin-user"
|
} from "../../../helpers/create-admin-user"
|
||||||
|
import { medusaTshirtProduct } from "../../__fixtures__/product"
|
||||||
|
|
||||||
jest.setTimeout(30000)
|
jest.setTimeout(30000)
|
||||||
|
|
||||||
@@ -578,5 +581,134 @@ medusaIntegrationTestRunner({
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("Order Edit Payment Collection", () => {
|
||||||
|
let appContainer
|
||||||
|
let storeHeaders
|
||||||
|
let region, product, salesChannel
|
||||||
|
|
||||||
|
const shippingAddressData = {
|
||||||
|
address_1: "test address 1",
|
||||||
|
address_2: "test address 2",
|
||||||
|
city: "SF",
|
||||||
|
country_code: "US",
|
||||||
|
province: "CA",
|
||||||
|
postal_code: "94016",
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
appContainer = getContainer()
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const publishableKey = await generatePublishableKey(appContainer)
|
||||||
|
storeHeaders = generateStoreHeaders({ publishableKey })
|
||||||
|
|
||||||
|
region = (
|
||||||
|
await api.post(
|
||||||
|
"/admin/regions",
|
||||||
|
{ name: "US", currency_code: "usd", countries: ["us"] },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.region
|
||||||
|
|
||||||
|
product = (
|
||||||
|
await api.post(
|
||||||
|
"/admin/products",
|
||||||
|
{ ...medusaTshirtProduct },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.product
|
||||||
|
|
||||||
|
salesChannel = (
|
||||||
|
await api.post(
|
||||||
|
"/admin/sales-channels",
|
||||||
|
{ name: "Webshop", description: "channel" },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.sales_channel
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should add a create a new payment collection if the order has authorized payment collection", async () => {
|
||||||
|
const cart = (
|
||||||
|
await api.post(
|
||||||
|
`/store/carts`,
|
||||||
|
{
|
||||||
|
currency_code: "usd",
|
||||||
|
sales_channel_id: salesChannel.id,
|
||||||
|
region_id: region.id,
|
||||||
|
shipping_address: shippingAddressData,
|
||||||
|
items: [{ variant_id: product.variants[0].id, quantity: 1 }],
|
||||||
|
},
|
||||||
|
storeHeaders
|
||||||
|
)
|
||||||
|
).data.cart
|
||||||
|
|
||||||
|
const paymentCollection = (
|
||||||
|
await api.post(
|
||||||
|
`/store/payment-collections`,
|
||||||
|
{ cart_id: cart.id },
|
||||||
|
storeHeaders
|
||||||
|
)
|
||||||
|
).data.payment_collection
|
||||||
|
|
||||||
|
await api.post(
|
||||||
|
`/store/payment-collections/${paymentCollection.id}/payment-sessions`,
|
||||||
|
{ provider_id: "pp_system_default" },
|
||||||
|
storeHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
const order = (
|
||||||
|
await api.post(
|
||||||
|
`/store/carts/${cart.id}/complete`,
|
||||||
|
{ cart_id: cart.id },
|
||||||
|
storeHeaders
|
||||||
|
)
|
||||||
|
).data.order
|
||||||
|
|
||||||
|
await api.post(
|
||||||
|
`/admin/order-edits`,
|
||||||
|
{ order_id: order.id, description: "Test" },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
await api.post(
|
||||||
|
`/admin/order-edits/${order.id}/items`,
|
||||||
|
{
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
variant_id: product.variants[0].id,
|
||||||
|
quantity: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
await api.post(
|
||||||
|
`/admin/order-edits/${order.id}/confirm`,
|
||||||
|
{},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
const orderResult = (
|
||||||
|
await api.get(`/admin/orders/${order.id}`, adminHeaders)
|
||||||
|
).data.order
|
||||||
|
|
||||||
|
expect(orderResult.payment_collections).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: paymentCollection.id,
|
||||||
|
status: "canceled",
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(String),
|
||||||
|
status: "not_paid",
|
||||||
|
amount: orderResult.total,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -12,7 +12,9 @@ export const getTotalCaptured = (
|
|||||||
}, 0)
|
}, 0)
|
||||||
|
|
||||||
export const getTotalPending = (paymentCollections: AdminPaymentCollection[]) =>
|
export const getTotalPending = (paymentCollections: AdminPaymentCollection[]) =>
|
||||||
paymentCollections.reduce((acc, paymentCollection) => {
|
paymentCollections
|
||||||
|
.filter((pc) => pc.status !== "canceled")
|
||||||
|
.reduce((acc, paymentCollection) => {
|
||||||
acc +=
|
acc +=
|
||||||
(paymentCollection.amount as number) -
|
(paymentCollection.amount as number) -
|
||||||
(paymentCollection.captured_amount as number)
|
(paymentCollection.captured_amount as number)
|
||||||
|
|||||||
+48
-7
@@ -14,6 +14,7 @@ import {
|
|||||||
import { useRemoteQueryStep } from "../../common"
|
import { useRemoteQueryStep } from "../../common"
|
||||||
import { updatePaymentCollectionStep } from "../../payment-collection"
|
import { updatePaymentCollectionStep } from "../../payment-collection"
|
||||||
import { createOrderPaymentCollectionWorkflow } from "./create-order-payment-collection"
|
import { createOrderPaymentCollectionWorkflow } from "./create-order-payment-collection"
|
||||||
|
import { cancelPaymentCollectionWorkflow } from "../../payment-collection/workflows/cancel-payment-collection"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The details of the order payment collection to create or update.
|
* The details of the order payment collection to create or update.
|
||||||
@@ -88,12 +89,28 @@ export const createOrUpdateOrderPaymentCollectionWorkflow = createWorkflow(
|
|||||||
variables: {
|
variables: {
|
||||||
filters: {
|
filters: {
|
||||||
id: orderPaymentCollectionIds,
|
id: orderPaymentCollectionIds,
|
||||||
status: [PaymentCollectionStatus.NOT_PAID],
|
status: [
|
||||||
|
// To update the collection amoun
|
||||||
|
PaymentCollectionStatus.NOT_PAID,
|
||||||
|
PaymentCollectionStatus.AWAITING,
|
||||||
|
// To cancel the authorized payments and create a new collection
|
||||||
|
PaymentCollectionStatus.AUTHORIZED,
|
||||||
|
PaymentCollectionStatus.PARTIALLY_AUTHORIZED,
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
list: false,
|
list: false,
|
||||||
}).config({ name: "payment-collection-query" })
|
}).config({ name: "payment-collection-query" })
|
||||||
|
|
||||||
|
const shouldRecreate = transform(
|
||||||
|
{ existingPaymentCollection },
|
||||||
|
({ existingPaymentCollection }) =>
|
||||||
|
existingPaymentCollection?.status ===
|
||||||
|
PaymentCollectionStatus.AUTHORIZED ||
|
||||||
|
existingPaymentCollection?.status ===
|
||||||
|
PaymentCollectionStatus.PARTIALLY_AUTHORIZED
|
||||||
|
)
|
||||||
|
|
||||||
const amountPending = transform({ order, input }, ({ order, input }) => {
|
const amountPending = transform({ order, input }, ({ order, input }) => {
|
||||||
const amountToCharge = input.amount ?? 0
|
const amountToCharge = input.amount ?? 0
|
||||||
const amountPending =
|
const amountPending =
|
||||||
@@ -110,9 +127,13 @@ export const createOrUpdateOrderPaymentCollectionWorkflow = createWorkflow(
|
|||||||
})
|
})
|
||||||
|
|
||||||
const updatedPaymentCollections = when(
|
const updatedPaymentCollections = when(
|
||||||
{ existingPaymentCollection, amountPending },
|
{ existingPaymentCollection, amountPending, shouldRecreate },
|
||||||
({ existingPaymentCollection, amountPending }) => {
|
({ existingPaymentCollection, amountPending, shouldRecreate }) => {
|
||||||
return !!existingPaymentCollection?.id && MathBN.gt(amountPending, 0)
|
return (
|
||||||
|
!!existingPaymentCollection?.id &&
|
||||||
|
!shouldRecreate &&
|
||||||
|
MathBN.gt(amountPending, 0)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
).then(() => {
|
).then(() => {
|
||||||
return updatePaymentCollectionStep({
|
return updatePaymentCollectionStep({
|
||||||
@@ -124,9 +145,12 @@ export const createOrUpdateOrderPaymentCollectionWorkflow = createWorkflow(
|
|||||||
})
|
})
|
||||||
|
|
||||||
const createdPaymentCollection = when(
|
const createdPaymentCollection = when(
|
||||||
{ existingPaymentCollection, amountPending },
|
{ existingPaymentCollection, amountPending, shouldRecreate },
|
||||||
({ existingPaymentCollection, amountPending }) => {
|
({ existingPaymentCollection, amountPending, shouldRecreate }) => {
|
||||||
return !!!existingPaymentCollection?.id && MathBN.gt(amountPending, 0)
|
return (
|
||||||
|
(!existingPaymentCollection?.id || shouldRecreate) &&
|
||||||
|
MathBN.gt(amountPending, 0)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
).then(() => {
|
).then(() => {
|
||||||
return createOrderPaymentCollectionWorkflow.runAsStep({
|
return createOrderPaymentCollectionWorkflow.runAsStep({
|
||||||
@@ -137,6 +161,23 @@ export const createOrUpdateOrderPaymentCollectionWorkflow = createWorkflow(
|
|||||||
}) as PaymentCollectionDTO[]
|
}) as PaymentCollectionDTO[]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
when(
|
||||||
|
{ existingPaymentCollection, amountPending, shouldRecreate },
|
||||||
|
({ existingPaymentCollection, amountPending, shouldRecreate }) => {
|
||||||
|
return (
|
||||||
|
!!existingPaymentCollection?.id &&
|
||||||
|
shouldRecreate &&
|
||||||
|
MathBN.gt(amountPending, 0)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
).then(() => {
|
||||||
|
cancelPaymentCollectionWorkflow.runAsStep({
|
||||||
|
input: {
|
||||||
|
payment_collection_id: existingPaymentCollection.id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
const paymentCollections = transform(
|
const paymentCollections = transform(
|
||||||
{ updatedPaymentCollections, createdPaymentCollection },
|
{ updatedPaymentCollections, createdPaymentCollection },
|
||||||
({ updatedPaymentCollections, createdPaymentCollection }) =>
|
({ updatedPaymentCollections, createdPaymentCollection }) =>
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import {
|
|||||||
throwIfIsCancelled,
|
throwIfIsCancelled,
|
||||||
throwIfOrderChangeIsNotActive,
|
throwIfOrderChangeIsNotActive,
|
||||||
} from "../../utils/order-validation"
|
} from "../../utils/order-validation"
|
||||||
import { createOrUpdateOrderPaymentCollectionWorkflow } from "../create-or-update-order-payment-collection"
|
|
||||||
|
|
||||||
function getOrderChangesData({
|
function getOrderChangesData({
|
||||||
input,
|
input,
|
||||||
@@ -153,12 +152,6 @@ export const requestOrderEditRequestWorkflow = createWorkflow(
|
|||||||
const updateOrderChangesData = getOrderChangesData({ input, orderChange })
|
const updateOrderChangesData = getOrderChangesData({ input, orderChange })
|
||||||
updateOrderChangesStep(updateOrderChangesData)
|
updateOrderChangesStep(updateOrderChangesData)
|
||||||
|
|
||||||
createOrUpdateOrderPaymentCollectionWorkflow.runAsStep({
|
|
||||||
input: {
|
|
||||||
order_id: order.id,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return new WorkflowResponse(previewOrderChangeStep(order.id))
|
return new WorkflowResponse(previewOrderChangeStep(order.id))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { IPaymentModuleService, Logger } from "@medusajs/framework/types"
|
||||||
|
import {
|
||||||
|
ContainerRegistrationKeys,
|
||||||
|
Modules,
|
||||||
|
promiseAll,
|
||||||
|
} from "@medusajs/framework/utils"
|
||||||
|
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The data to cancel payments.
|
||||||
|
*/
|
||||||
|
export interface CancelPaymentStepInput {
|
||||||
|
/**
|
||||||
|
* The IDs of the payments to cancel.
|
||||||
|
*/
|
||||||
|
ids: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const cancelPaymentStepId = "cancel-payment"
|
||||||
|
/**
|
||||||
|
* This step cancels one or more authorized payments.
|
||||||
|
*/
|
||||||
|
export const cancelPaymentStep = createStep(
|
||||||
|
cancelPaymentStepId,
|
||||||
|
async (input: CancelPaymentStepInput, { container }) => {
|
||||||
|
const { ids = [] } = input
|
||||||
|
const deleted: string[] = []
|
||||||
|
const logger = container.resolve<Logger>(ContainerRegistrationKeys.LOGGER)
|
||||||
|
const service = container.resolve<IPaymentModuleService>(Modules.PAYMENT)
|
||||||
|
|
||||||
|
if (!ids?.length) {
|
||||||
|
return new StepResponse([], null)
|
||||||
|
}
|
||||||
|
|
||||||
|
const promises: Promise<void>[] = []
|
||||||
|
|
||||||
|
for (const id of ids) {
|
||||||
|
const promise = service
|
||||||
|
.cancelPayment(id)
|
||||||
|
.then((res) => {
|
||||||
|
deleted.push(id)
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
logger.error(
|
||||||
|
`Encountered an error when trying to cancel a payment - ${id} - ${e}`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
promises.push(promise)
|
||||||
|
}
|
||||||
|
|
||||||
|
await promiseAll(promises)
|
||||||
|
|
||||||
|
return new StepResponse(deleted)
|
||||||
|
}
|
||||||
|
)
|
||||||
+117
@@ -0,0 +1,117 @@
|
|||||||
|
import { PaymentCollectionDTO } from "@medusajs/framework/types"
|
||||||
|
import { MedusaError, PaymentCollectionStatus } from "@medusajs/framework/utils"
|
||||||
|
import {
|
||||||
|
WorkflowData,
|
||||||
|
WorkflowResponse,
|
||||||
|
createStep,
|
||||||
|
createWorkflow,
|
||||||
|
transform,
|
||||||
|
} from "@medusajs/framework/workflows-sdk"
|
||||||
|
import { useQueryGraphStep } from "../../common"
|
||||||
|
import { updatePaymentCollectionStep } from "../steps/update-payment-collection"
|
||||||
|
import { cancelPaymentStep } from "../steps/cancel-payment"
|
||||||
|
|
||||||
|
const validatePaymentCollectionCancellationStep = createStep(
|
||||||
|
"validate-payment-collection-cancellation",
|
||||||
|
async (input: { paymentCollection: PaymentCollectionDTO }) => {
|
||||||
|
const { paymentCollection } = input
|
||||||
|
|
||||||
|
if (paymentCollection.status === PaymentCollectionStatus.COMPLETED) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.NOT_ALLOWED,
|
||||||
|
"Cannot cancel a completed payment collection"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (paymentCollection.status == PaymentCollectionStatus.CANCELED) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.NOT_ALLOWED,
|
||||||
|
"Payment collection is already canceled"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The data to cancel a payment collection.
|
||||||
|
*/
|
||||||
|
export interface CancelPaymentCollectionWorkflowInput {
|
||||||
|
/**
|
||||||
|
* The id of the payment collection to cancel.
|
||||||
|
*/
|
||||||
|
payment_collection_id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const cancelPaymentCollectionWorkflowId = "cancel-payment-collection"
|
||||||
|
/**
|
||||||
|
* This workflow cancels a payment collection that is either not paid or authorized.
|
||||||
|
*
|
||||||
|
* Payment colelction that is completed or already canceled cannot be canceled.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const data = cancelPaymentCollectionStep({
|
||||||
|
* payment_collection_id: "paycol_123",
|
||||||
|
* })
|
||||||
|
*/
|
||||||
|
export const cancelPaymentCollectionWorkflow = createWorkflow(
|
||||||
|
cancelPaymentCollectionWorkflowId,
|
||||||
|
(
|
||||||
|
input: WorkflowData<CancelPaymentCollectionWorkflowInput>
|
||||||
|
): WorkflowResponse<PaymentCollectionDTO> => {
|
||||||
|
const paymentCollectionQuery = useQueryGraphStep({
|
||||||
|
entity: "payment_collection",
|
||||||
|
fields: [
|
||||||
|
"id",
|
||||||
|
"status",
|
||||||
|
"payments.id",
|
||||||
|
"payments.captured_at",
|
||||||
|
"captured_amount",
|
||||||
|
],
|
||||||
|
filters: { id: input.payment_collection_id },
|
||||||
|
}).config({ name: "get-payment-collection" })
|
||||||
|
|
||||||
|
const paymentCollection = transform(
|
||||||
|
{ paymentCollectionQuery },
|
||||||
|
({ paymentCollectionQuery }) => paymentCollectionQuery.data[0]
|
||||||
|
)
|
||||||
|
|
||||||
|
validatePaymentCollectionCancellationStep({
|
||||||
|
paymentCollection,
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only cancel authorized payments, not captured payments.
|
||||||
|
*/
|
||||||
|
const authorizedPaymentIds = transform(
|
||||||
|
{ paymentCollection },
|
||||||
|
({ paymentCollection }) =>
|
||||||
|
paymentCollection.payments
|
||||||
|
?.filter((p) => !p.captured_at)
|
||||||
|
.map((p) => p.id) ?? []
|
||||||
|
)
|
||||||
|
|
||||||
|
const status = transform({ paymentCollection }, ({ paymentCollection }) =>
|
||||||
|
paymentCollection.captured_amount > 0
|
||||||
|
? PaymentCollectionStatus.PARTIALLY_CAPTURED
|
||||||
|
: PaymentCollectionStatus.CANCELED
|
||||||
|
)
|
||||||
|
|
||||||
|
const updatedPaymentCollections = updatePaymentCollectionStep({
|
||||||
|
selector: { id: paymentCollection.id },
|
||||||
|
update: {
|
||||||
|
status: status,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
cancelPaymentStep({
|
||||||
|
ids: authorizedPaymentIds,
|
||||||
|
})
|
||||||
|
|
||||||
|
const resultPaymentCollection = transform(
|
||||||
|
{ updatedPaymentCollections },
|
||||||
|
({ updatedPaymentCollections }) => updatedPaymentCollections[0]
|
||||||
|
)
|
||||||
|
|
||||||
|
return new WorkflowResponse(resultPaymentCollection)
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -9,6 +9,7 @@ export type PaymentCollectionStatus =
|
|||||||
| "awaiting"
|
| "awaiting"
|
||||||
| "authorized"
|
| "authorized"
|
||||||
| "partially_authorized"
|
| "partially_authorized"
|
||||||
|
| "partially_captured"
|
||||||
| "canceled"
|
| "canceled"
|
||||||
| "failed"
|
| "failed"
|
||||||
| "completed"
|
| "completed"
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ export enum PaymentCollectionStatus {
|
|||||||
* The payment collection is failed.
|
* The payment collection is failed.
|
||||||
*/
|
*/
|
||||||
FAILED = "failed",
|
FAILED = "failed",
|
||||||
|
/**
|
||||||
|
* The payment collection is partially captured.
|
||||||
|
*/
|
||||||
|
PARTIALLY_CAPTURED = "partially_captured",
|
||||||
/**
|
/**
|
||||||
* The payment collection is completed.
|
* The payment collection is completed.
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in new issue
Block a user