feat(dashboard,core-flows,types,utils,medusa): Order cancelations will refund payments (#10667)
* feat(order, types): Add Credit Line to order module * chore: add action to inject credit lines * WIP * chore: add fixes + observe * chore: fix balances * chore: add canceled badge * chore: fix i18n schema * chore: remove redunddant query * chore: add changeset * chore: add credit lines for all cancel cases * chore: add accounting total * chore: address review & cleanup
This commit is contained in:
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/dashboard": patch
|
||||||
|
"@medusajs/core-flows": patch
|
||||||
|
"@medusajs/types": patch
|
||||||
|
"@medusajs/utils": patch
|
||||||
|
"@medusajs/medusa": patch
|
||||||
|
"@medusajs/order": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
feat(dashboard,core-flows,types,utils,medusa,order): Order cancelations will refund payments
|
||||||
@@ -343,6 +343,199 @@ medusaIntegrationTestRunner({
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("POST /orders/:id/cancel", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
seeder = await createOrderSeeder({
|
||||||
|
api,
|
||||||
|
container: getContainer(),
|
||||||
|
})
|
||||||
|
order = seeder.order
|
||||||
|
|
||||||
|
order = (await api.get(`/admin/orders/${order.id}`, adminHeaders)).data
|
||||||
|
.order
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should successfully cancel an order and its authorized but not captured payments", async () => {
|
||||||
|
const response = await api.post(
|
||||||
|
`/admin/orders/${order.id}/cancel`,
|
||||||
|
{},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.status).toBe(200)
|
||||||
|
expect(response.data.order).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: order.id,
|
||||||
|
status: "canceled",
|
||||||
|
|
||||||
|
summary: expect.objectContaining({
|
||||||
|
credit_line_total: 106,
|
||||||
|
current_order_total: 0,
|
||||||
|
accounting_total: 0,
|
||||||
|
}),
|
||||||
|
|
||||||
|
payment_collections: [
|
||||||
|
expect.objectContaining({
|
||||||
|
status: "canceled",
|
||||||
|
captured_amount: 0,
|
||||||
|
refunded_amount: 0,
|
||||||
|
amount: 106,
|
||||||
|
payments: [
|
||||||
|
expect.objectContaining({
|
||||||
|
canceled_at: expect.any(String),
|
||||||
|
refunds: [],
|
||||||
|
captures: [],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should successfully cancel an order with a captured payment", async () => {
|
||||||
|
const payment = order.payment_collections[0].payments[0]
|
||||||
|
|
||||||
|
const paymentResponse = await api.post(
|
||||||
|
`/admin/payments/${payment.id}/capture`,
|
||||||
|
undefined,
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(paymentResponse.data.payment).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: payment.id,
|
||||||
|
captured_at: expect.any(String),
|
||||||
|
captures: [
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(String),
|
||||||
|
amount: 106,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
refunds: [],
|
||||||
|
amount: 106,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const response = await api.post(
|
||||||
|
`/admin/orders/${order.id}/cancel`,
|
||||||
|
{},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.status).toBe(200)
|
||||||
|
expect(response.data.order).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: order.id,
|
||||||
|
status: "canceled",
|
||||||
|
|
||||||
|
summary: expect.objectContaining({
|
||||||
|
credit_line_total: 106,
|
||||||
|
current_order_total: 0,
|
||||||
|
accounting_total: 0,
|
||||||
|
}),
|
||||||
|
|
||||||
|
payment_collections: [
|
||||||
|
expect.objectContaining({
|
||||||
|
status: "canceled",
|
||||||
|
captured_amount: 106,
|
||||||
|
refunded_amount: 106,
|
||||||
|
amount: 106,
|
||||||
|
payments: [
|
||||||
|
expect.objectContaining({
|
||||||
|
// canceled_at: expect.any(String),
|
||||||
|
refunds: [
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(String),
|
||||||
|
amount: 106,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
captures: [
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(String),
|
||||||
|
amount: 106,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should successfully cancel an order with a partially captured payment", async () => {
|
||||||
|
const payment = order.payment_collections[0].payments[0]
|
||||||
|
|
||||||
|
const paymentResponse = await api.post(
|
||||||
|
`/admin/payments/${payment.id}/capture`,
|
||||||
|
{ amount: 50 },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(paymentResponse.data.payment).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: payment.id,
|
||||||
|
captured_at: null,
|
||||||
|
captures: [
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(String),
|
||||||
|
amount: 50,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
refunds: [],
|
||||||
|
amount: 106,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const response = await api.post(
|
||||||
|
`/admin/orders/${order.id}/cancel`,
|
||||||
|
{},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.status).toBe(200)
|
||||||
|
expect(response.data.order).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: order.id,
|
||||||
|
status: "canceled",
|
||||||
|
|
||||||
|
summary: expect.objectContaining({
|
||||||
|
credit_line_total: 106,
|
||||||
|
current_order_total: 0,
|
||||||
|
accounting_total: 0,
|
||||||
|
}),
|
||||||
|
|
||||||
|
payment_collections: [
|
||||||
|
expect.objectContaining({
|
||||||
|
status: "canceled",
|
||||||
|
captured_amount: 50,
|
||||||
|
refunded_amount: 50,
|
||||||
|
amount: 106,
|
||||||
|
payments: [
|
||||||
|
expect.objectContaining({
|
||||||
|
// canceled_at: expect.any(String),
|
||||||
|
refunds: [
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(String),
|
||||||
|
amount: 50,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
captures: [
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(String),
|
||||||
|
amount: 50,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe("POST /orders/:id/fulfillments", () => {
|
describe("POST /orders/:id/fulfillments", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const stockChannelOverride = (
|
const stockChannelOverride = (
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -916,6 +916,15 @@
|
|||||||
"list": {
|
"list": {
|
||||||
"noRecordsMessage": "Your orders will show up here."
|
"noRecordsMessage": "Your orders will show up here."
|
||||||
},
|
},
|
||||||
|
"status": {
|
||||||
|
"not_paid": "Not paid",
|
||||||
|
"pending": "Pending",
|
||||||
|
"completed": "Completed",
|
||||||
|
"draft": "Draft",
|
||||||
|
"archived": "Archived",
|
||||||
|
"canceled": "Canceled",
|
||||||
|
"requires_action": "Requires action"
|
||||||
|
},
|
||||||
"summary": {
|
"summary": {
|
||||||
"requestReturn": "Request return",
|
"requestReturn": "Request return",
|
||||||
"allocateItems": "Allocate items",
|
"allocateItems": "Allocate items",
|
||||||
|
|||||||
@@ -1,5 +1,16 @@
|
|||||||
import { TFunction } from "i18next"
|
import { TFunction } from "i18next"
|
||||||
|
|
||||||
|
export const getCanceledOrderStatus = (
|
||||||
|
t: TFunction<"translation">,
|
||||||
|
status: string
|
||||||
|
) => {
|
||||||
|
if (status === "canceled") {
|
||||||
|
return { label: t("orders.status.canceled"), color: "red" }
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
export const getOrderPaymentStatus = (
|
export const getOrderPaymentStatus = (
|
||||||
t: TFunction<"translation">,
|
t: TFunction<"translation">,
|
||||||
status: string
|
status: string
|
||||||
|
|||||||
+19
-1
@@ -1,4 +1,5 @@
|
|||||||
import { XCircle } from "@medusajs/icons"
|
import { XCircle } from "@medusajs/icons"
|
||||||
|
import { HttpTypes } from "@medusajs/types"
|
||||||
import {
|
import {
|
||||||
Container,
|
Container,
|
||||||
Copy,
|
Copy,
|
||||||
@@ -9,13 +10,14 @@ import {
|
|||||||
} from "@medusajs/ui"
|
} from "@medusajs/ui"
|
||||||
import { format } from "date-fns"
|
import { format } from "date-fns"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { isPresent } from "../../../../../../../../core/utils/src/common/is-present"
|
||||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
import { useCancelOrder } from "../../../../../hooks/api/orders"
|
import { useCancelOrder } from "../../../../../hooks/api/orders"
|
||||||
import {
|
import {
|
||||||
|
getCanceledOrderStatus,
|
||||||
getOrderFulfillmentStatus,
|
getOrderFulfillmentStatus,
|
||||||
getOrderPaymentStatus,
|
getOrderPaymentStatus,
|
||||||
} from "../../../../../lib/order-helpers"
|
} from "../../../../../lib/order-helpers"
|
||||||
import { HttpTypes } from "@medusajs/types"
|
|
||||||
|
|
||||||
type OrderGeneralSectionProps = {
|
type OrderGeneralSectionProps = {
|
||||||
order: HttpTypes.AdminOrder
|
order: HttpTypes.AdminOrder
|
||||||
@@ -60,6 +62,7 @@ export const OrderGeneralSection = ({ order }: OrderGeneralSectionProps) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-x-4">
|
<div className="flex items-center gap-x-4">
|
||||||
<div className="flex items-center gap-x-1.5">
|
<div className="flex items-center gap-x-1.5">
|
||||||
|
<OrderBadge order={order} />
|
||||||
<PaymentBadge order={order} />
|
<PaymentBadge order={order} />
|
||||||
<FulfillmentBadge order={order} />
|
<FulfillmentBadge order={order} />
|
||||||
</div>
|
</div>
|
||||||
@@ -108,3 +111,18 @@ const PaymentBadge = ({ order }: { order: HttpTypes.AdminOrder }) => {
|
|||||||
</StatusBadge>
|
</StatusBadge>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const OrderBadge = ({ order }: { order: HttpTypes.AdminOrder }) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const orderStatus = getCanceledOrderStatus(t, order.status)
|
||||||
|
|
||||||
|
if (!isPresent(orderStatus)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StatusBadge color={orderStatus.color} className="text-nowrap">
|
||||||
|
{orderStatus.label}
|
||||||
|
</StatusBadge>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
+20
-5
@@ -1,5 +1,9 @@
|
|||||||
import { ArrowDownRightMini, DocumentText, XCircle } from "@medusajs/icons"
|
import { ArrowDownRightMini, DocumentText, XCircle } from "@medusajs/icons"
|
||||||
import { AdminPaymentCollection, HttpTypes } from "@medusajs/types"
|
import {
|
||||||
|
AdminPayment,
|
||||||
|
AdminPaymentCollection,
|
||||||
|
HttpTypes,
|
||||||
|
} from "@medusajs/types"
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
@@ -14,6 +18,7 @@ import {
|
|||||||
import { format } from "date-fns"
|
import { format } from "date-fns"
|
||||||
import { Trans, useTranslation } from "react-i18next"
|
import { Trans, useTranslation } from "react-i18next"
|
||||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
|
import DisplayId from "../../../../../components/common/display-id/display-id"
|
||||||
import { useCapturePayment } from "../../../../../hooks/api"
|
import { useCapturePayment } from "../../../../../hooks/api"
|
||||||
import { formatCurrency } from "../../../../../lib/format-currency"
|
import { formatCurrency } from "../../../../../lib/format-currency"
|
||||||
import {
|
import {
|
||||||
@@ -22,7 +27,6 @@ import {
|
|||||||
} from "../../../../../lib/money-amount-helpers"
|
} from "../../../../../lib/money-amount-helpers"
|
||||||
import { getOrderPaymentStatus } from "../../../../../lib/order-helpers"
|
import { getOrderPaymentStatus } from "../../../../../lib/order-helpers"
|
||||||
import { getTotalCaptured, getTotalPending } from "../../../../../lib/payment"
|
import { getTotalCaptured, getTotalPending } from "../../../../../lib/payment"
|
||||||
import DisplayId from "../../../../../components/common/display-id/display-id"
|
|
||||||
|
|
||||||
type OrderPaymentSectionProps = {
|
type OrderPaymentSectionProps = {
|
||||||
order: HttpTypes.AdminOrder
|
order: HttpTypes.AdminOrder
|
||||||
@@ -173,9 +177,20 @@ const Payment = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const [status, color] = (
|
const getPaymentStatusAttributes = (payment: AdminPayment) => {
|
||||||
payment.captured_at ? ["Captured", "green"] : ["Pending", "orange"]
|
if (payment.canceled_at) {
|
||||||
) as [string, "green" | "orange"]
|
return ["Canceled", "red"]
|
||||||
|
} else if (payment.captured_at) {
|
||||||
|
return ["Captured", "green"]
|
||||||
|
} else {
|
||||||
|
return ["Pending", "orange"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const [status, color] = getPaymentStatusAttributes(payment) as [
|
||||||
|
string,
|
||||||
|
"green" | "orange" | "red",
|
||||||
|
]
|
||||||
|
|
||||||
const showCapture =
|
const showCapture =
|
||||||
payment.captured_at === null && payment.canceled_at === null
|
payment.captured_at === null && payment.canceled_at === null
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ import {
|
|||||||
PaymentCollectionDTO,
|
PaymentCollectionDTO,
|
||||||
} from "@medusajs/framework/types"
|
} from "@medusajs/framework/types"
|
||||||
import {
|
import {
|
||||||
|
MathBN,
|
||||||
MedusaError,
|
MedusaError,
|
||||||
OrderWorkflowEvents,
|
OrderWorkflowEvents,
|
||||||
|
PaymentCollectionStatus,
|
||||||
deepFlatMap,
|
deepFlatMap,
|
||||||
} from "@medusajs/framework/utils"
|
} from "@medusajs/framework/utils"
|
||||||
import {
|
import {
|
||||||
@@ -17,12 +19,16 @@ import {
|
|||||||
createWorkflow,
|
createWorkflow,
|
||||||
parallelize,
|
parallelize,
|
||||||
transform,
|
transform,
|
||||||
|
when,
|
||||||
} from "@medusajs/framework/workflows-sdk"
|
} from "@medusajs/framework/workflows-sdk"
|
||||||
import { emitEventStep, useRemoteQueryStep } from "../../common"
|
import { emitEventStep, useQueryGraphStep } from "../../common"
|
||||||
|
import { updatePaymentCollectionStep } from "../../payment-collection"
|
||||||
import { cancelPaymentStep } from "../../payment/steps"
|
import { cancelPaymentStep } from "../../payment/steps"
|
||||||
import { deleteReservationsByLineItemsStep } from "../../reservation/steps"
|
import { deleteReservationsByLineItemsStep } from "../../reservation/steps"
|
||||||
import { cancelOrdersStep } from "../steps/cancel-orders"
|
import { cancelOrdersStep } from "../steps/cancel-orders"
|
||||||
import { throwIfOrderIsCancelled } from "../utils/order-validation"
|
import { throwIfOrderIsCancelled } from "../utils/order-validation"
|
||||||
|
import { createOrderRefundCreditLinesWorkflow } from "./payments/create-order-refund-credit-lines"
|
||||||
|
import { refundCapturedPaymentsWorkflow } from "./payments/refund-captured-payments"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This step validates that an order can be canceled.
|
* This step validates that an order can be canceled.
|
||||||
@@ -42,28 +48,6 @@ export const cancelValidateOrder = createStep(
|
|||||||
|
|
||||||
throwIfOrderIsCancelled({ order })
|
throwIfOrderIsCancelled({ order })
|
||||||
|
|
||||||
let refunds = 0
|
|
||||||
let captures = 0
|
|
||||||
|
|
||||||
deepFlatMap(order_, "payment_collections.payments", ({ payments }) => {
|
|
||||||
refunds += payments?.refunds?.length ?? 0
|
|
||||||
captures += payments?.captures?.length ?? 0
|
|
||||||
})
|
|
||||||
|
|
||||||
if (captures > 0) {
|
|
||||||
throw new MedusaError(
|
|
||||||
MedusaError.Types.NOT_ALLOWED,
|
|
||||||
"Order with payment capture(s) cannot be canceled"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (refunds > 0) {
|
|
||||||
throw new MedusaError(
|
|
||||||
MedusaError.Types.NOT_ALLOWED,
|
|
||||||
"Order with payment refund(s) cannot be canceled"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const throwErrorIf = (
|
const throwErrorIf = (
|
||||||
arr: unknown[],
|
arr: unknown[],
|
||||||
pred: (obj: any) => boolean,
|
pred: (obj: any) => boolean,
|
||||||
@@ -90,42 +74,74 @@ export const cancelOrderWorkflowId = "cancel-order"
|
|||||||
export const cancelOrderWorkflow = createWorkflow(
|
export const cancelOrderWorkflow = createWorkflow(
|
||||||
cancelOrderWorkflowId,
|
cancelOrderWorkflowId,
|
||||||
(input: WorkflowData<OrderWorkflow.CancelOrderWorkflowInput>) => {
|
(input: WorkflowData<OrderWorkflow.CancelOrderWorkflowInput>) => {
|
||||||
const order: OrderDTO & { fulfillments: FulfillmentDTO[] } =
|
const orderQuery = useQueryGraphStep({
|
||||||
useRemoteQueryStep({
|
entity: "orders",
|
||||||
entry_point: "orders",
|
fields: [
|
||||||
fields: [
|
"id",
|
||||||
"id",
|
"status",
|
||||||
"status",
|
"items.id",
|
||||||
"items.id",
|
"fulfillments.canceled_at",
|
||||||
"fulfillments.canceled_at",
|
"payment_collections.payments.id",
|
||||||
"payment_collections.payments.id",
|
"payment_collections.payments.amount",
|
||||||
"payment_collections.payments.refunds.id",
|
"payment_collections.payments.refunds.id",
|
||||||
"payment_collections.payments.captures.id",
|
"payment_collections.payments.refunds.amount",
|
||||||
],
|
"payment_collections.payments.captures.id",
|
||||||
variables: { id: input.order_id },
|
"payment_collections.payments.captures.amount",
|
||||||
list: false,
|
],
|
||||||
throw_if_key_not_found: true,
|
filters: { id: input.order_id },
|
||||||
})
|
options: { throwIfKeyNotFound: true },
|
||||||
|
}).config({ name: "get-cart" })
|
||||||
|
|
||||||
|
const order = transform(
|
||||||
|
{ orderQuery },
|
||||||
|
({ orderQuery }) => orderQuery.data[0]
|
||||||
|
)
|
||||||
|
|
||||||
cancelValidateOrder({ order, input })
|
cancelValidateOrder({ order, input })
|
||||||
|
|
||||||
|
const uncapturedPaymentIds = transform({ order }, ({ order }) => {
|
||||||
|
const payments = deepFlatMap(
|
||||||
|
order,
|
||||||
|
"payment_collections.payments",
|
||||||
|
({ payments }) => payments
|
||||||
|
)
|
||||||
|
|
||||||
|
const uncapturedPayments = payments.filter(
|
||||||
|
(payment) => payment.captures.length === 0
|
||||||
|
)
|
||||||
|
|
||||||
|
return uncapturedPayments.map((payment) => payment.id)
|
||||||
|
})
|
||||||
|
|
||||||
|
const creditLineAmount = transform({ order }, ({ order }) => {
|
||||||
|
const payments = deepFlatMap(
|
||||||
|
order,
|
||||||
|
"payment_collections.payments",
|
||||||
|
({ payments }) => payments
|
||||||
|
)
|
||||||
|
|
||||||
|
return payments.reduce(
|
||||||
|
(acc, payment) => MathBN.sum(acc, payment.amount),
|
||||||
|
MathBN.convert(0)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
const lineItemIds = transform({ order }, ({ order }) => {
|
const lineItemIds = transform({ order }, ({ order }) => {
|
||||||
return order.items?.map((i) => i.id)
|
return order.items?.map((i) => i.id)
|
||||||
})
|
})
|
||||||
|
|
||||||
const paymentIds = transform({ order }, ({ order }) => {
|
|
||||||
return deepFlatMap(
|
|
||||||
order,
|
|
||||||
"payment_collections.payments",
|
|
||||||
({ payments }) => {
|
|
||||||
return payments?.id
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
parallelize(
|
parallelize(
|
||||||
|
createOrderRefundCreditLinesWorkflow.runAsStep({
|
||||||
|
input: {
|
||||||
|
order_id: order.id,
|
||||||
|
amount: creditLineAmount,
|
||||||
|
},
|
||||||
|
}),
|
||||||
deleteReservationsByLineItemsStep(lineItemIds),
|
deleteReservationsByLineItemsStep(lineItemIds),
|
||||||
cancelPaymentStep({ paymentIds }),
|
cancelPaymentStep({ paymentIds: uncapturedPaymentIds }),
|
||||||
|
refundCapturedPaymentsWorkflow.runAsStep({
|
||||||
|
input: { order_id: order.id },
|
||||||
|
}),
|
||||||
cancelOrdersStep({ orderIds: [order.id] }),
|
cancelOrdersStep({ orderIds: [order.id] }),
|
||||||
emitEventStep({
|
emitEventStep({
|
||||||
eventName: OrderWorkflowEvents.CANCELED,
|
eventName: OrderWorkflowEvents.CANCELED,
|
||||||
@@ -133,6 +149,19 @@ export const cancelOrderWorkflow = createWorkflow(
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const paymentCollectionids = transform({ order }, ({ order }) =>
|
||||||
|
order.payment_collections?.map((pc) => pc.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
when({ paymentCollectionids }, ({ paymentCollectionids }) => {
|
||||||
|
return !!paymentCollectionids?.length
|
||||||
|
}).then(() => {
|
||||||
|
updatePaymentCollectionStep({
|
||||||
|
selector: { id: paymentCollectionids },
|
||||||
|
update: { status: PaymentCollectionStatus.CANCELED },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
const orderCanceled = createHook("orderCanceled", {
|
const orderCanceled = createHook("orderCanceled", {
|
||||||
order,
|
order,
|
||||||
})
|
})
|
||||||
|
|||||||
+113
@@ -0,0 +1,113 @@
|
|||||||
|
import { BigNumberInput, OrderDTO } from "@medusajs/framework/types"
|
||||||
|
import {
|
||||||
|
ChangeActionType,
|
||||||
|
OrderChangeStatus,
|
||||||
|
OrderChangeType,
|
||||||
|
} from "@medusajs/framework/utils"
|
||||||
|
import {
|
||||||
|
WorkflowData,
|
||||||
|
createStep,
|
||||||
|
createWorkflow,
|
||||||
|
transform,
|
||||||
|
} from "@medusajs/framework/workflows-sdk"
|
||||||
|
import { useQueryGraphStep } from "../../../common"
|
||||||
|
import { confirmOrderChanges } from "../../steps/confirm-order-changes"
|
||||||
|
import { createOrderChangeStep } from "../../steps/create-order-change"
|
||||||
|
import { throwIfOrderIsCancelled } from "../../utils/order-validation"
|
||||||
|
import { createOrderChangeActionsWorkflow } from "../create-order-change-actions"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This step validates that an order refund credit line can be issued
|
||||||
|
*/
|
||||||
|
export const validateOrderRefundCreditLinesStep = createStep(
|
||||||
|
"begin-order-edit-validation",
|
||||||
|
async function ({ order }: { order: OrderDTO }) {
|
||||||
|
throwIfOrderIsCancelled({ order })
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export const createOrderRefundCreditLinesWorkflowId =
|
||||||
|
"create-order-refund-credit-lines"
|
||||||
|
/**
|
||||||
|
* This workflow creates an order refund credit line
|
||||||
|
*/
|
||||||
|
export const createOrderRefundCreditLinesWorkflow = createWorkflow(
|
||||||
|
createOrderRefundCreditLinesWorkflowId,
|
||||||
|
function (
|
||||||
|
input: WorkflowData<{
|
||||||
|
order_id: string
|
||||||
|
created_by?: string
|
||||||
|
amount: BigNumberInput
|
||||||
|
}>
|
||||||
|
) {
|
||||||
|
const orderQuery = useQueryGraphStep({
|
||||||
|
entity: "orders",
|
||||||
|
fields: ["id", "status", "summary", "payment_collections.id"],
|
||||||
|
filters: { id: input.order_id },
|
||||||
|
options: { throwIfKeyNotFound: true },
|
||||||
|
}).config({ name: "get-order" })
|
||||||
|
|
||||||
|
const order = transform(
|
||||||
|
{ orderQuery },
|
||||||
|
({ orderQuery }) => orderQuery.data[0]
|
||||||
|
)
|
||||||
|
|
||||||
|
validateOrderRefundCreditLinesStep({ order })
|
||||||
|
|
||||||
|
const orderChangeInput = transform({ input }, ({ input }) => ({
|
||||||
|
change_type: OrderChangeType.CREDIT_LINE,
|
||||||
|
order_id: input.order_id,
|
||||||
|
created_by: input.created_by,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const createdOrderChange = createOrderChangeStep(orderChangeInput)
|
||||||
|
|
||||||
|
const orderChangeActionInput = transform(
|
||||||
|
{ order, orderChange: createdOrderChange, input },
|
||||||
|
({ order, orderChange, input }) => ({
|
||||||
|
order_change_id: orderChange.id,
|
||||||
|
order_id: order.id,
|
||||||
|
version: orderChange.version,
|
||||||
|
action: ChangeActionType.CREDIT_LINE_ADD,
|
||||||
|
reference: "payment_collection",
|
||||||
|
reference_id: order.payment_collections[0]?.id,
|
||||||
|
amount: input.amount,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
createOrderChangeActionsWorkflow.runAsStep({
|
||||||
|
input: [orderChangeActionInput],
|
||||||
|
})
|
||||||
|
|
||||||
|
const orderChangeQuery = useQueryGraphStep({
|
||||||
|
entity: "order_change",
|
||||||
|
fields: [
|
||||||
|
"id",
|
||||||
|
"status",
|
||||||
|
"change_type",
|
||||||
|
"actions.id",
|
||||||
|
"actions.order_id",
|
||||||
|
"actions.action",
|
||||||
|
"actions.details",
|
||||||
|
"actions.reference",
|
||||||
|
"actions.reference_id",
|
||||||
|
"actions.internal_note",
|
||||||
|
],
|
||||||
|
filters: {
|
||||||
|
order_id: input.order_id,
|
||||||
|
status: [OrderChangeStatus.PENDING],
|
||||||
|
},
|
||||||
|
options: { throwIfKeyNotFound: true },
|
||||||
|
}).config({ name: "order-change-query" })
|
||||||
|
|
||||||
|
const orderChange = transform(
|
||||||
|
{ orderChangeQuery },
|
||||||
|
({ orderChangeQuery }) => orderChangeQuery.data[0]
|
||||||
|
)
|
||||||
|
|
||||||
|
confirmOrderChanges({
|
||||||
|
changes: [orderChange],
|
||||||
|
orderId: order.id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import { PaymentDTO } from "@medusajs/framework/types"
|
||||||
|
import { deepFlatMap, MathBN } from "@medusajs/framework/utils"
|
||||||
|
import {
|
||||||
|
createWorkflow,
|
||||||
|
transform,
|
||||||
|
when,
|
||||||
|
WorkflowData,
|
||||||
|
} from "@medusajs/framework/workflows-sdk"
|
||||||
|
import { useQueryGraphStep } from "../../../common"
|
||||||
|
import { refundPaymentsWorkflow } from "../../../payment"
|
||||||
|
|
||||||
|
export const refundCapturedPaymentsWorkflowId =
|
||||||
|
"refund-captured-payments-workflow"
|
||||||
|
/**
|
||||||
|
* This workflow refunds a payment.
|
||||||
|
*/
|
||||||
|
export const refundCapturedPaymentsWorkflow = createWorkflow(
|
||||||
|
refundCapturedPaymentsWorkflowId,
|
||||||
|
(
|
||||||
|
input: WorkflowData<{
|
||||||
|
order_id: string
|
||||||
|
created_by?: string
|
||||||
|
}>
|
||||||
|
) => {
|
||||||
|
const orderQuery = useQueryGraphStep({
|
||||||
|
entity: "orders",
|
||||||
|
fields: [
|
||||||
|
"id",
|
||||||
|
"status",
|
||||||
|
"summary",
|
||||||
|
"payment_collections.payments.id",
|
||||||
|
"payment_collections.payments.amount",
|
||||||
|
"payment_collections.payments.refunds.id",
|
||||||
|
"payment_collections.payments.refunds.amount",
|
||||||
|
"payment_collections.payments.captures.id",
|
||||||
|
"payment_collections.payments.captures.amount",
|
||||||
|
],
|
||||||
|
filters: { id: input.order_id },
|
||||||
|
options: { throwIfKeyNotFound: true },
|
||||||
|
}).config({ name: "get-order" })
|
||||||
|
|
||||||
|
const order = transform(
|
||||||
|
{ orderQuery },
|
||||||
|
({ orderQuery }) => orderQuery.data[0]
|
||||||
|
)
|
||||||
|
|
||||||
|
const refundPaymentsData = transform(
|
||||||
|
{ order, input },
|
||||||
|
({ order, input }) => {
|
||||||
|
const payments: PaymentDTO[] = deepFlatMap(
|
||||||
|
order,
|
||||||
|
"payment_collections.payments",
|
||||||
|
({ payments }) => payments
|
||||||
|
)
|
||||||
|
|
||||||
|
const capturedPayments = payments.filter(
|
||||||
|
(payment) => payment.captures?.length
|
||||||
|
)
|
||||||
|
|
||||||
|
return capturedPayments
|
||||||
|
.map((payment) => {
|
||||||
|
const capturedAmount = (payment.captures || []).reduce(
|
||||||
|
(acc, capture) => MathBN.sum(acc, capture.amount),
|
||||||
|
MathBN.convert(0)
|
||||||
|
)
|
||||||
|
const refundedAmount = (payment.refunds || []).reduce(
|
||||||
|
(acc, refund) => MathBN.sum(acc, refund.amount),
|
||||||
|
MathBN.convert(0)
|
||||||
|
)
|
||||||
|
|
||||||
|
const amountToRefund = MathBN.sub(capturedAmount, refundedAmount)
|
||||||
|
|
||||||
|
return {
|
||||||
|
payment_id: payment.id,
|
||||||
|
created_by: input.created_by,
|
||||||
|
amount: amountToRefund,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((payment) => MathBN.gt(payment.amount, 0))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const totalCaptured = transform(
|
||||||
|
{ refundPaymentsData },
|
||||||
|
({ refundPaymentsData }) =>
|
||||||
|
refundPaymentsData.reduce(
|
||||||
|
(acc, refundPayment) => MathBN.sum(acc, refundPayment.amount),
|
||||||
|
MathBN.convert(0)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
when({ totalCaptured }, ({ totalCaptured }) => {
|
||||||
|
return !!MathBN.gt(totalCaptured, 0)
|
||||||
|
}).then(() => {
|
||||||
|
refundPaymentsWorkflow.runAsStep({ input: refundPaymentsData })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -27,6 +27,7 @@ export const cancelPaymentStep = createStep(
|
|||||||
: [input.paymentIds]
|
: [input.paymentIds]
|
||||||
|
|
||||||
const promises: Promise<any>[] = []
|
const promises: Promise<any>[] = []
|
||||||
|
|
||||||
for (const id of paymentIds) {
|
for (const id of paymentIds) {
|
||||||
promises.push(
|
promises.push(
|
||||||
paymentModule.cancelPayment(id).catch((e) => {
|
paymentModule.cancelPayment(id).catch((e) => {
|
||||||
@@ -36,6 +37,7 @@ export const cancelPaymentStep = createStep(
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
await promiseAll(promises)
|
await promiseAll(promises)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,3 +2,4 @@ export * from "./authorize-payment-session"
|
|||||||
export * from "./cancel-payment"
|
export * from "./cancel-payment"
|
||||||
export * from "./capture-payment"
|
export * from "./capture-payment"
|
||||||
export * from "./refund-payment"
|
export * from "./refund-payment"
|
||||||
|
export * from "./refund-payments"
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import {
|
||||||
|
BigNumberInput,
|
||||||
|
IPaymentModuleService,
|
||||||
|
Logger,
|
||||||
|
PaymentDTO,
|
||||||
|
} from "@medusajs/framework/types"
|
||||||
|
import {
|
||||||
|
ContainerRegistrationKeys,
|
||||||
|
isObject,
|
||||||
|
Modules,
|
||||||
|
promiseAll,
|
||||||
|
} from "@medusajs/framework/utils"
|
||||||
|
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
|
||||||
|
|
||||||
|
export const refundPaymentsStepId = "refund-payments-step"
|
||||||
|
/**
|
||||||
|
* This step refunds one or more payments.
|
||||||
|
*/
|
||||||
|
export const refundPaymentsStep = createStep(
|
||||||
|
refundPaymentsStepId,
|
||||||
|
async (
|
||||||
|
input: {
|
||||||
|
payment_id: string
|
||||||
|
amount: BigNumberInput
|
||||||
|
created_by?: string
|
||||||
|
}[],
|
||||||
|
{ container }
|
||||||
|
) => {
|
||||||
|
const logger = container.resolve<Logger>(ContainerRegistrationKeys.LOGGER)
|
||||||
|
const paymentModule = container.resolve<IPaymentModuleService>(
|
||||||
|
Modules.PAYMENT
|
||||||
|
)
|
||||||
|
|
||||||
|
const promises: Promise<PaymentDTO | void>[] = []
|
||||||
|
|
||||||
|
for (const refundInput of input) {
|
||||||
|
promises.push(
|
||||||
|
paymentModule.refundPayment(refundInput).catch((e) => {
|
||||||
|
logger.error(
|
||||||
|
`Error was thrown trying to cancel payment - ${refundInput.payment_id} - ${e}`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const successfulRefunds = (await promiseAll(promises)).filter((payment) =>
|
||||||
|
isObject(payment)
|
||||||
|
)
|
||||||
|
|
||||||
|
return new StepResponse(successfulRefunds)
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
export * from "./capture-payment"
|
export * from "./capture-payment"
|
||||||
export * from "./process-payment"
|
export * from "./process-payment"
|
||||||
export * from "./refund-payment"
|
export * from "./refund-payment"
|
||||||
|
export * from "./refund-payments"
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import { BigNumberInput, PaymentDTO } from "@medusajs/framework/types"
|
||||||
|
import { MathBN, MedusaError } from "@medusajs/framework/utils"
|
||||||
|
import {
|
||||||
|
createStep,
|
||||||
|
createWorkflow,
|
||||||
|
transform,
|
||||||
|
WorkflowData,
|
||||||
|
WorkflowResponse,
|
||||||
|
} from "@medusajs/framework/workflows-sdk"
|
||||||
|
import { useQueryGraphStep } from "../../common"
|
||||||
|
import { addOrderTransactionStep } from "../../order"
|
||||||
|
import { refundPaymentsStep } from "../steps/refund-payments"
|
||||||
|
|
||||||
|
type RefundPaymentsInput = {
|
||||||
|
payment_id: string
|
||||||
|
amount: BigNumberInput
|
||||||
|
created_by?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This step validates that the refund is valid for the payment
|
||||||
|
*/
|
||||||
|
export const validatePaymentsRefundStep = createStep(
|
||||||
|
"validate-payments-refund-step",
|
||||||
|
async function ({
|
||||||
|
payments,
|
||||||
|
input,
|
||||||
|
}: {
|
||||||
|
payments: PaymentDTO[]
|
||||||
|
input: RefundPaymentsInput[]
|
||||||
|
}) {
|
||||||
|
const paymentIdAmountMap = new Map<string, BigNumberInput>(
|
||||||
|
input.map(({ payment_id, amount }) => [payment_id, amount])
|
||||||
|
)
|
||||||
|
|
||||||
|
for (const payment of payments) {
|
||||||
|
const capturedAmount = (payment.captures || []).reduce(
|
||||||
|
(acc, capture) => MathBN.sum(acc, capture.amount),
|
||||||
|
MathBN.convert(0)
|
||||||
|
)
|
||||||
|
|
||||||
|
const refundedAmount = (payment.refunds || []).reduce(
|
||||||
|
(acc, capture) => MathBN.sum(acc, capture.amount),
|
||||||
|
MathBN.convert(0)
|
||||||
|
)
|
||||||
|
|
||||||
|
const refundableAmount = MathBN.sub(capturedAmount, refundedAmount)
|
||||||
|
const amountToRefund = paymentIdAmountMap.get(payment.id)!
|
||||||
|
|
||||||
|
if (MathBN.gt(amountToRefund, refundableAmount)) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.INVALID_DATA,
|
||||||
|
`Payment with id ${payment.id} is trying to refund amount greater than the refundable amount`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export const refundPaymentsWorkflowId = "refund-payments-workflow"
|
||||||
|
/**
|
||||||
|
* This workflow refunds a payment.
|
||||||
|
*/
|
||||||
|
export const refundPaymentsWorkflow = createWorkflow(
|
||||||
|
refundPaymentsWorkflowId,
|
||||||
|
(input: WorkflowData<RefundPaymentsInput[]>) => {
|
||||||
|
const paymentIds = transform({ input }, ({ input }) =>
|
||||||
|
input.map((paymentInput) => paymentInput.payment_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
const paymentsQuery = useQueryGraphStep({
|
||||||
|
entity: "payments",
|
||||||
|
fields: [
|
||||||
|
"id",
|
||||||
|
"currency_code",
|
||||||
|
"refunds.id",
|
||||||
|
"refunds.amount",
|
||||||
|
"captures.id",
|
||||||
|
"captures.amount",
|
||||||
|
"payment_collection.order.id",
|
||||||
|
"payment_collection.order.currency_code",
|
||||||
|
],
|
||||||
|
filters: { id: paymentIds },
|
||||||
|
options: { throwIfKeyNotFound: true },
|
||||||
|
}).config({ name: "get-cart" })
|
||||||
|
|
||||||
|
const payments = transform(
|
||||||
|
{ paymentsQuery },
|
||||||
|
({ paymentsQuery }) => paymentsQuery.data
|
||||||
|
)
|
||||||
|
|
||||||
|
validatePaymentsRefundStep({ payments, input })
|
||||||
|
|
||||||
|
const refundedPayments = refundPaymentsStep(input)
|
||||||
|
|
||||||
|
const orderTransactionData = transform(
|
||||||
|
{ payments, input },
|
||||||
|
({ payments, input }) => {
|
||||||
|
const paymentsMap: Record<
|
||||||
|
string,
|
||||||
|
PaymentDTO & {
|
||||||
|
payment_collection: { order: { id: string; currency_code: string } }
|
||||||
|
}
|
||||||
|
> = {}
|
||||||
|
|
||||||
|
for (const payment of payments) {
|
||||||
|
paymentsMap[payment.id] = payment
|
||||||
|
}
|
||||||
|
|
||||||
|
return input.map((paymentInput) => {
|
||||||
|
const payment = paymentsMap[paymentInput.payment_id]!
|
||||||
|
const order = payment.payment_collection.order
|
||||||
|
|
||||||
|
return {
|
||||||
|
order_id: order.id,
|
||||||
|
amount: MathBN.mult(paymentInput.amount, -1),
|
||||||
|
currency_code: payment.currency_code,
|
||||||
|
reference_id: payment.id,
|
||||||
|
reference: "refund",
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
addOrderTransactionStep(orderTransactionData)
|
||||||
|
|
||||||
|
return new WorkflowResponse(refundedPayments)
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -773,6 +773,10 @@ export interface BaseOrder {
|
|||||||
* The order's display ID.
|
* The order's display ID.
|
||||||
*/
|
*/
|
||||||
display_id?: number
|
display_id?: number
|
||||||
|
/**
|
||||||
|
* The order's status.
|
||||||
|
*/
|
||||||
|
status: string
|
||||||
/**
|
/**
|
||||||
* The order's shipping address.
|
* The order's shipping address.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -127,6 +127,18 @@ export type OrderSummaryDTO = {
|
|||||||
* @ignore
|
* @ignore
|
||||||
*/
|
*/
|
||||||
raw_pending_difference: BigNumberRawValue
|
raw_pending_difference: BigNumberRawValue
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The sum difference of all actions
|
||||||
|
*/
|
||||||
|
difference_sum: BigNumberValue
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The raw sum difference of all actions
|
||||||
|
*
|
||||||
|
* @ignore
|
||||||
|
*/
|
||||||
|
raw_difference_sum: BigNumberRawValue
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ type OrderChangeType =
|
|||||||
| "edit"
|
| "edit"
|
||||||
| "transfer"
|
| "transfer"
|
||||||
| "update_order"
|
| "update_order"
|
||||||
|
| "credit_line"
|
||||||
|
|
||||||
/** ADDRESS START */
|
/** ADDRESS START */
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { BigNumberInput } from "../totals"
|
import { BigNumberInput } from "../totals"
|
||||||
|
import { PaymentCollectionStatus } from "./common"
|
||||||
import { PaymentProviderContext } from "./provider"
|
import { PaymentProviderContext } from "./provider"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -77,10 +78,15 @@ export interface PaymentCollectionUpdatableFields {
|
|||||||
region_id?: string
|
region_id?: string
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {The ISO 3 character currency code of the payment collection.
|
* The ISO 3 character currency code of the payment collection.
|
||||||
*/
|
*/
|
||||||
currency_code?: string
|
currency_code?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The status of the payment collection
|
||||||
|
*/
|
||||||
|
status?: PaymentCollectionStatus
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The amount of the payment collection.
|
* The amount of the payment collection.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -27,4 +27,5 @@ export enum OrderChangeType {
|
|||||||
EXCHANGE = "exchange",
|
EXCHANGE = "exchange",
|
||||||
CLAIM = "claim",
|
CLAIM = "claim",
|
||||||
EDIT = "edit",
|
EDIT = "edit",
|
||||||
|
CREDIT_LINE = "credit_line",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ export const defaultAdminRetrieveOrderFields = [
|
|||||||
"*payment_collections",
|
"*payment_collections",
|
||||||
"*payment_collections.payments",
|
"*payment_collections.payments",
|
||||||
"*payment_collections.payments.refunds",
|
"*payment_collections.payments.refunds",
|
||||||
|
"*payment_collections.payments.captures",
|
||||||
]
|
]
|
||||||
|
|
||||||
export const defaultAdminRetrieveOrderChangesFields = [
|
export const defaultAdminRetrieveOrderChangesFields = [
|
||||||
|
|||||||
+34
-11
@@ -51,22 +51,43 @@ describe("Action: Credit Line Add", function () {
|
|||||||
"credit_line_total": 0
|
"credit_line_total": 0
|
||||||
}
|
}
|
||||||
|
|
||||||
Upon adding a credit line, the order total and the pending difference will increase making it possible for the merchant
|
Upon adding a credit line, the current order total will decrease with the difference_sum going in
|
||||||
to request the customer for a payment for an arbitrary reason, or prepare the order balance sheet to then allow
|
the negatives making it possible for the merchant to balance the order to then enable a refund.
|
||||||
the merchant to provide a refund.
|
|
||||||
|
|
||||||
{
|
{
|
||||||
"transaction_total": 0,
|
"transaction_total": 0,
|
||||||
"original_order_total": 30,
|
"original_order_total": 30,
|
||||||
"current_order_total": 60,
|
"current_order_total": 60,
|
||||||
"pending_difference": 60,
|
"pending_difference": 0,
|
||||||
"difference_sum": 30,
|
"difference_sum": -30,
|
||||||
"paid_total": 0,
|
"paid_total": 0,
|
||||||
"refunded_total": 0,
|
"refunded_total": 0,
|
||||||
"credit_line_total": 30
|
"credit_line_total": 30
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
it("should add credit lines", function () {
|
it("should add credit lines", function () {
|
||||||
|
const changesWithoutActions = calculateOrderChange({
|
||||||
|
order: originalOrder,
|
||||||
|
actions: [],
|
||||||
|
options: { addActionReferenceToObject: true },
|
||||||
|
})
|
||||||
|
|
||||||
|
const changesWithoutActionsJSON = JSON.parse(
|
||||||
|
JSON.stringify(changesWithoutActions.summary)
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(changesWithoutActionsJSON).toEqual({
|
||||||
|
transaction_total: 0,
|
||||||
|
original_order_total: 30,
|
||||||
|
current_order_total: 30,
|
||||||
|
pending_difference: 30,
|
||||||
|
difference_sum: 0,
|
||||||
|
paid_total: 0,
|
||||||
|
refunded_total: 0,
|
||||||
|
credit_line_total: 0,
|
||||||
|
accounting_total: 30,
|
||||||
|
})
|
||||||
|
|
||||||
const actions = [
|
const actions = [
|
||||||
{
|
{
|
||||||
action: ChangeActionType.CREDIT_LINE_ADD,
|
action: ChangeActionType.CREDIT_LINE_ADD,
|
||||||
@@ -87,12 +108,13 @@ describe("Action: Credit Line Add", function () {
|
|||||||
expect(sumToJSON).toEqual({
|
expect(sumToJSON).toEqual({
|
||||||
transaction_total: 0,
|
transaction_total: 0,
|
||||||
original_order_total: 30,
|
original_order_total: 30,
|
||||||
current_order_total: 60,
|
current_order_total: 0,
|
||||||
pending_difference: 60,
|
pending_difference: 0,
|
||||||
difference_sum: 30,
|
difference_sum: 0,
|
||||||
paid_total: 0,
|
paid_total: 0,
|
||||||
refunded_total: 0,
|
refunded_total: 0,
|
||||||
credit_line_total: 30,
|
credit_line_total: 30,
|
||||||
|
accounting_total: 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
originalOrder.credit_lines.push({
|
originalOrder.credit_lines.push({
|
||||||
@@ -123,12 +145,13 @@ describe("Action: Credit Line Add", function () {
|
|||||||
expect(sumToJSONSecond).toEqual({
|
expect(sumToJSONSecond).toEqual({
|
||||||
transaction_total: 0,
|
transaction_total: 0,
|
||||||
original_order_total: 30,
|
original_order_total: 30,
|
||||||
current_order_total: 70,
|
current_order_total: -10,
|
||||||
pending_difference: 70,
|
pending_difference: -10,
|
||||||
difference_sum: 30,
|
difference_sum: 0,
|
||||||
paid_total: 0,
|
paid_total: 0,
|
||||||
refunded_total: 0,
|
refunded_total: 0,
|
||||||
credit_line_total: 40,
|
credit_line_total: 40,
|
||||||
|
accounting_total: -10,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ describe("Order Exchange - Actions", function () {
|
|||||||
paid_total: 0,
|
paid_total: 0,
|
||||||
refunded_total: 0,
|
refunded_total: 0,
|
||||||
credit_line_total: 0,
|
credit_line_total: 0,
|
||||||
|
accounting_total: 312.5,
|
||||||
})
|
})
|
||||||
|
|
||||||
const toJson = JSON.parse(JSON.stringify(changes.order.items))
|
const toJson = JSON.parse(JSON.stringify(changes.order.items))
|
||||||
|
|||||||
@@ -3059,6 +3059,7 @@ export default class OrderModuleService<
|
|||||||
transformPropertiesToBigNumber(trxs)
|
transformPropertiesToBigNumber(trxs)
|
||||||
|
|
||||||
const op = isRemoved ? MathBN.sub : MathBN.add
|
const op = isRemoved ? MathBN.sub : MathBN.add
|
||||||
|
|
||||||
for (const trx of trxs) {
|
for (const trx of trxs) {
|
||||||
if (MathBN.gt(trx.amount, 0)) {
|
if (MathBN.gt(trx.amount, 0)) {
|
||||||
summary.totals.paid_total = new BigNumber(
|
summary.totals.paid_total = new BigNumber(
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ export interface OrderSummaryCalculated {
|
|||||||
paid_total: BigNumberInput
|
paid_total: BigNumberInput
|
||||||
refunded_total: BigNumberInput
|
refunded_total: BigNumberInput
|
||||||
credit_line_total: BigNumberInput
|
credit_line_total: BigNumberInput
|
||||||
|
accounting_total: BigNumberInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OrderTransaction {
|
export interface OrderTransaction {
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ OrderChangeProcessing.registerActionType(ChangeActionType.CREDIT_LINE_ADD, {
|
|||||||
existing = {
|
existing = {
|
||||||
id: action.reference_id!,
|
id: action.reference_id!,
|
||||||
order_id: currentOrder.id,
|
order_id: currentOrder.id,
|
||||||
amount: action.amount as number,
|
amount: action.amount!,
|
||||||
|
reference: action.reference,
|
||||||
|
reference_id: action.reference_id,
|
||||||
}
|
}
|
||||||
|
|
||||||
creditLines.push(existing)
|
creditLines.push(existing)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { BigNumberInput, OrderSummaryDTO } from "@medusajs/framework/types"
|
import { BigNumberInput, OrderSummaryDTO } from "@medusajs/framework/types"
|
||||||
import {
|
import {
|
||||||
BigNumber,
|
BigNumber,
|
||||||
|
ChangeActionType,
|
||||||
MathBN,
|
MathBN,
|
||||||
isPresent,
|
isPresent,
|
||||||
transformPropertiesToBigNumber,
|
transformPropertiesToBigNumber,
|
||||||
@@ -63,8 +64,6 @@ export class OrderChangeProcessing {
|
|||||||
MathBN.convert(0)
|
MathBN.convert(0)
|
||||||
)
|
)
|
||||||
|
|
||||||
const currentOrderTotal = MathBN.add(this.order.total ?? 0, creditLineTotal)
|
|
||||||
|
|
||||||
for (const tr of transactions) {
|
for (const tr of transactions) {
|
||||||
if (MathBN.lt(tr.amount, 0)) {
|
if (MathBN.lt(tr.amount, 0)) {
|
||||||
refunded = MathBN.add(refunded, MathBN.abs(tr.amount))
|
refunded = MathBN.add(refunded, MathBN.abs(tr.amount))
|
||||||
@@ -79,12 +78,13 @@ export class OrderChangeProcessing {
|
|||||||
this.summary = {
|
this.summary = {
|
||||||
pending_difference: 0,
|
pending_difference: 0,
|
||||||
difference_sum: 0,
|
difference_sum: 0,
|
||||||
current_order_total: currentOrderTotal,
|
current_order_total: this.order.total ?? 0,
|
||||||
original_order_total: this.order.total ?? 0,
|
original_order_total: this.order.total ?? 0,
|
||||||
transaction_total: transactionTotal,
|
transaction_total: transactionTotal,
|
||||||
paid_total: paid,
|
paid_total: paid,
|
||||||
refunded_total: refunded,
|
refunded_total: refunded,
|
||||||
credit_line_total: creditLineTotal,
|
credit_line_total: creditLineTotal,
|
||||||
|
accounting_total: MathBN.sub(this.order.total ?? 0, creditLineTotal),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,6 +102,11 @@ export class OrderChangeProcessing {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public processActions() {
|
public processActions() {
|
||||||
|
let creditLineTotal = (this.order.credit_lines || []).reduce(
|
||||||
|
(acc, creditLine) => MathBN.add(acc, creditLine.amount),
|
||||||
|
MathBN.convert(0)
|
||||||
|
)
|
||||||
|
|
||||||
for (const action of this.actions) {
|
for (const action of this.actions) {
|
||||||
this.processAction_(action)
|
this.processAction_(action)
|
||||||
}
|
}
|
||||||
@@ -128,30 +133,37 @@ export class OrderChangeProcessing {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.isEventDone(action) && !action.change_id) {
|
if (action.action === ChangeActionType.CREDIT_LINE_ADD) {
|
||||||
summary.difference_sum = MathBN.add(summary.difference_sum, amount)
|
creditLineTotal = MathBN.add(creditLineTotal, amount)
|
||||||
|
} else {
|
||||||
|
if (!this.isEventDone(action) && !action.change_id) {
|
||||||
|
summary.difference_sum = MathBN.add(summary.difference_sum, amount)
|
||||||
|
}
|
||||||
|
|
||||||
|
summary.current_order_total = MathBN.add(
|
||||||
|
summary.current_order_total,
|
||||||
|
amount
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const creditLineTotal = (this.order.credit_lines || []).reduce(
|
|
||||||
(acc, creditLine) => MathBN.add(acc, creditLine.amount),
|
|
||||||
MathBN.convert(0)
|
|
||||||
)
|
|
||||||
|
|
||||||
summary.credit_line_total = creditLineTotal
|
|
||||||
summary.current_order_total = MathBN.add(
|
|
||||||
summary.current_order_total,
|
|
||||||
amount
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const groupSum = MathBN.add(...Object.values(this.groupTotal))
|
const groupSum = MathBN.add(...Object.values(this.groupTotal))
|
||||||
|
|
||||||
summary.difference_sum = MathBN.add(summary.difference_sum, groupSum)
|
summary.difference_sum = MathBN.add(summary.difference_sum, groupSum)
|
||||||
|
summary.credit_line_total = creditLineTotal
|
||||||
|
summary.accounting_total = MathBN.sub(
|
||||||
|
summary.current_order_total,
|
||||||
|
creditLineTotal
|
||||||
|
)
|
||||||
|
|
||||||
summary.transaction_total = MathBN.sum(
|
summary.transaction_total = MathBN.sum(
|
||||||
...this.transactions.map((tr) => tr.amount)
|
...this.transactions.map((tr) => tr.amount)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
summary.current_order_total = MathBN.sub(
|
||||||
|
summary.current_order_total,
|
||||||
|
creditLineTotal
|
||||||
|
)
|
||||||
|
|
||||||
summary.pending_difference = MathBN.sub(
|
summary.pending_difference = MathBN.sub(
|
||||||
summary.current_order_total,
|
summary.current_order_total,
|
||||||
summary.transaction_total
|
summary.transaction_total
|
||||||
@@ -216,6 +228,7 @@ export class OrderChangeProcessing {
|
|||||||
paid_total: new BigNumber(summary.paid_total),
|
paid_total: new BigNumber(summary.paid_total),
|
||||||
refunded_total: new BigNumber(summary.refunded_total),
|
refunded_total: new BigNumber(summary.refunded_total),
|
||||||
credit_line_total: new BigNumber(summary.credit_line_total),
|
credit_line_total: new BigNumber(summary.credit_line_total),
|
||||||
|
accounting_total: new BigNumber(summary.accounting_total),
|
||||||
} as unknown as OrderSummaryDTO
|
} as unknown as OrderSummaryDTO
|
||||||
|
|
||||||
return orderSummary
|
return orderSummary
|
||||||
|
|||||||
Reference in New Issue
Block a user