fix(utils,core-flows): subtotal calculation and returns location (#13497)

* fix(utils,core-flows): subtotal calculation and returns location

* changeset

* fix test

* var

* rm extra field from test

* fix original total

* fix partial refunds and pending difference

* fix test

* fix test

* test

* extract to util

* original total and update payment when receive return

* original_subtotal

* default fields

* test

* calculate pending difference

* revert claims test

* pending difference

* creadit line fix

* if
This commit is contained in:
Carlos R. L. Rodrigues
2025-09-18 17:50:40 +02:00
committed by GitHub
parent 4736c58da5
commit 9563ee446f
37 changed files with 746 additions and 204 deletions
+9
View File
@@ -0,0 +1,9 @@
---
"@medusajs/utils": patch
"@medusajs/types": patch
"@medusajs/core-flows": patch
"@medusajs/order": patch
"@medusajs/medusa": patch
---
fix(utils): subtotal calculation discounting returned items
@@ -22,8 +22,10 @@ medusaIntegrationTestRunner({
let returnReason let returnReason
let inventoryItem let inventoryItem
let inventoryItemExtra let inventoryItemExtra
let inventoryItemExtra2
let location let location
let productExtra let productExtra
let productExtra2
const shippingProviderId = "manual_test-provider" const shippingProviderId = "manual_test-provider"
beforeEach(async () => { beforeEach(async () => {
@@ -123,6 +125,31 @@ medusaIntegrationTestRunner({
) )
).data.product ).data.product
productExtra2 = (
await api.post(
"/admin/products",
{
title: "Extra product 2, same price",
shipping_profile_id: shippingProfile.id,
options: [{ title: "size", values: ["large", "small"] }],
variants: [
{
title: "my variant 2",
sku: "variant-sku-2",
options: { size: "large" },
prices: [
{
currency_code: "usd",
amount: 25,
},
],
},
],
},
adminHeaders
)
).data.product
returnReason = ( returnReason = (
await api.post( await api.post(
"/admin/return-reasons", "/admin/return-reasons",
@@ -269,6 +296,10 @@ medusaIntegrationTestRunner({
await api.get(`/admin/inventory-items?sku=variant-sku`, adminHeaders) await api.get(`/admin/inventory-items?sku=variant-sku`, adminHeaders)
).data.inventory_items[0] ).data.inventory_items[0]
inventoryItemExtra2 = (
await api.get(`/admin/inventory-items?sku=variant-sku-2`, adminHeaders)
).data.inventory_items[0]
await api.post( await api.post(
`/admin/inventory-items/${inventoryItemExtra.id}/location-levels`, `/admin/inventory-items/${inventoryItemExtra.id}/location-levels`,
{ {
@@ -278,6 +309,15 @@ medusaIntegrationTestRunner({
adminHeaders adminHeaders
) )
await api.post(
`/admin/inventory-items/${inventoryItemExtra2.id}/location-levels`,
{
location_id: location.id,
stocked_quantity: 2,
},
adminHeaders
)
const remoteLink = container.resolve( const remoteLink = container.resolve(
ContainerRegistrationKeys.REMOTE_LINK ContainerRegistrationKeys.REMOTE_LINK
) )
@@ -323,6 +363,14 @@ medusaIntegrationTestRunner({
inventory_item_id: inventoryItemExtra.id, inventory_item_id: inventoryItemExtra.id,
}, },
}, },
{
[Modules.PRODUCT]: {
variant_id: productExtra2.variants[0].id,
},
[Modules.INVENTORY]: {
inventory_item_id: inventoryItemExtra2.id,
},
},
]) ])
// create reservation for inventory item that is initially on the order // create reservation for inventory item that is initially on the order
@@ -440,6 +488,95 @@ medusaIntegrationTestRunner({
}) })
describe("Exchanges lifecycle", () => { describe("Exchanges lifecycle", () => {
it("test full exchange flow", async () => {
const orderBefore = (
await api.get(`/admin/orders/${order.id}`, adminHeaders)
).data.order
let result = await api.post(
"/admin/exchanges",
{
order_id: order.id,
description: "Test",
},
adminHeaders
)
expect(result.data.exchange.created_by).toEqual(expect.any(String))
const exchangeId = result.data.exchange.id
const item = order.items[0]
result = await api.post(
`/admin/exchanges/${exchangeId}/inbound/items`,
{
items: [
{
id: item.id,
reason_id: returnReason.id,
quantity: 2,
},
],
},
adminHeaders
)
// New Item
result = await api.post(
`/admin/exchanges/${exchangeId}/outbound/items`,
{
items: [
{
variant_id: productExtra2.variants[0].id,
quantity: 2,
},
],
},
adminHeaders
)
result = await api.post(
`/admin/exchanges/${exchangeId}/request`,
{},
adminHeaders
)
const returnId = result.data.exchange.return_id
result = (await api.get(`/admin/orders/${order.id}`, adminHeaders)).data
.order
expect(orderBefore.total).toBe(61)
expect(result.total).toBe(112)
// receive return
await api.post(`/admin/returns/${returnId}/receive`, {}, adminHeaders)
await api.post(
`/admin/returns/${returnId}/receive-items`,
{
items: [
{
id: item.id,
quantity: 2,
},
],
},
adminHeaders
)
await api.post(
`/admin/returns/${returnId}/receive/confirm`,
{},
adminHeaders
)
result = (await api.get(`/admin/orders/${order.id}`, adminHeaders)).data
.order
expect(orderBefore.total).toBe(61)
expect(result.total).toBe(62) // +1 is from taxes of the new item
})
it("Full flow with 2 orders", async () => { it("Full flow with 2 orders", async () => {
let result = await api.post( let result = await api.post(
"/admin/exchanges", "/admin/exchanges",
@@ -6,6 +6,7 @@ import {
AdminStockLocation, AdminStockLocation,
MedusaContainer, MedusaContainer,
} from "@medusajs/types" } from "@medusajs/types"
import { ContainerRegistrationKeys, Modules } from "@medusajs/utils"
import { import {
adminHeaders, adminHeaders,
generatePublishableKey, generatePublishableKey,
@@ -175,6 +176,26 @@ export async function createOrderSeeder({
adminHeaders adminHeaders
) )
const remoteLink = container.resolve(ContainerRegistrationKeys.LINK)
await remoteLink.create([
{
[Modules.SALES_CHANNEL]: {
sales_channel_id: salesChannel.id,
},
[Modules.STOCK_LOCATION]: {
stock_location_id: stockLocation.id,
},
},
{
[Modules.PRODUCT]: {
variant_id: product.variants[0].id,
},
[Modules.INVENTORY]: {
inventory_item_id: inventoryItem.id,
},
},
])
/** /**
* Create shipping options for each shipping profile provided * Create shipping options for each shipping profile provided
*/ */
@@ -32,7 +32,35 @@ medusaIntegrationTestRunner({
adminHeaders adminHeaders
) )
await api.post(`/admin/claims/${claim.id}/request`, {}, adminHeaders) const createdClaim = await api.post(
`/admin/claims/${claim.id}/request`,
{},
adminHeaders
)
const returnOrder = createdClaim.data.return
const returnId = returnOrder.id
await api.post(`/admin/returns/${returnId}/receive`, {}, adminHeaders)
let lineItem = returnOrder.items[0].item
await api.post(
`/admin/returns/${returnId}/receive-items`,
{
items: [
{
id: lineItem.id,
quantity: returnOrder.items[0].quantity,
},
],
},
adminHeaders
)
await api.post(
`/admin/returns/${returnId}/receive/confirm`,
{},
adminHeaders
)
} }
beforeEach(async () => { beforeEach(async () => {
@@ -64,10 +92,6 @@ medusaIntegrationTestRunner({
}) })
describe("with outstanding amount due to claim", () => { describe("with outstanding amount due to claim", () => {
beforeEach(async () => {
await createClaim({ order })
})
it("should capture an authorized payment", async () => { it("should capture an authorized payment", async () => {
const payment = order.payment_collections[0].payments[0] const payment = order.payment_collections[0].payments[0]
@@ -189,6 +213,8 @@ medusaIntegrationTestRunner({
adminHeaders adminHeaders
) )
await createClaim({ order })
const refundReason = ( const refundReason = (
await api.post( await api.post(
`/admin/refund-reasons`, `/admin/refund-reasons`,
@@ -253,6 +279,8 @@ medusaIntegrationTestRunner({
) )
).data.refund_reason ).data.refund_reason
await createClaim({ order })
await api.post( await api.post(
`/admin/payments/${payment.id}/refund`, `/admin/payments/${payment.id}/refund`,
{ {
@@ -311,6 +339,8 @@ medusaIntegrationTestRunner({
adminHeaders adminHeaders
) )
await createClaim({ order })
await api.post( await api.post(
`/admin/payments/${payment.id}/refund`, `/admin/payments/${payment.id}/refund`,
{ amount: 25 }, { amount: 25 },
@@ -1,9 +1,9 @@
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
import { import {
ContainerRegistrationKeys, ContainerRegistrationKeys,
Modules, Modules,
RuleOperator, RuleOperator,
} from "@medusajs/utils" } from "@medusajs/utils"
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
import { import {
adminHeaders, adminHeaders,
createAdminUser, createAdminUser,
@@ -478,6 +478,16 @@ medusaIntegrationTestRunner({
adminHeaders adminHeaders
) )
expect(result.data.order_preview.summary).toEqual(
expect.objectContaining({
transaction_total: 0,
current_order_total: 61,
pending_difference: 11,
paid_total: 0,
refunded_total: 0,
})
)
expect(result.data.order_preview).toEqual( expect(result.data.order_preview).toEqual(
expect.objectContaining({ expect.objectContaining({
id: order.id, id: order.id,
@@ -1,8 +1,18 @@
import { createOrderChangeWorkflow, createOrderWorkflow, } from "@medusajs/core-flows" import {
createOrderChangeWorkflow,
createOrderWorkflow,
} from "@medusajs/core-flows"
import { medusaIntegrationTestRunner } from "@medusajs/test-utils" import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
import { CreateOrderLineItemDTO, IOrderModuleService, OrderDTO, } from "@medusajs/types" import {
CreateOrderLineItemDTO,
IOrderModuleService,
OrderDTO,
} from "@medusajs/types"
import { Modules, ProductStatus } from "@medusajs/utils" import { Modules, ProductStatus } from "@medusajs/utils"
import { adminHeaders, createAdminUser, } from "../../../helpers/create-admin-user" import {
adminHeaders,
createAdminUser,
} from "../../../helpers/create-admin-user"
jest.setTimeout(50000) jest.setTimeout(50000)
@@ -288,6 +298,7 @@ medusaIntegrationTestRunner({
discount_total: 1.1, discount_total: 1.1,
discount_tax_total: 0.1, discount_tax_total: 0.1,
original_total: 61, original_total: 61,
original_subtotal: 60,
original_tax_total: 1, original_tax_total: 1,
item_total: 50, item_total: 50,
item_subtotal: 50, item_subtotal: 50,
@@ -433,6 +444,7 @@ medusaIntegrationTestRunner({
subtotal: 50, subtotal: 50,
total: 50, total: 50,
original_total: 50, original_total: 50,
original_subtotal: 50,
discount_total: 0, discount_total: 0,
discount_tax_total: 0, discount_tax_total: 0,
discount_subtotal: 0, discount_subtotal: 0,
@@ -490,6 +502,10 @@ medusaIntegrationTestRunner({
precision: 20, precision: 20,
value: "0", value: "0",
}, },
raw_original_subtotal: {
precision: 20,
value: "50",
},
raw_return_dismissed_total: { raw_return_dismissed_total: {
precision: 20, precision: 20,
value: "0", value: "0",
@@ -31,24 +31,43 @@ export const addOrderTransactionStep = createStep(
return new StepResponse(null) return new StepResponse(null)
} }
const existingQuery: any[] = []
for (const trx of trxsData) { for (const trx of trxsData) {
const existing = await service.listOrderTransactions( existingQuery.push({
{ order_id: trx.order_id,
order_id: trx.order_id, reference: trx.reference,
reference: trx.reference, reference_id: trx.reference_id,
reference_id: trx.reference_id, })
}, }
{
select: ["id"],
}
)
if (existing.length) { const existing = await service.listOrderTransactions(
return new StepResponse(null) {
$or: existingQuery,
},
{
select: ["order_id", "reference", "reference_id"],
}
)
const existingSet = new Set<string>(
existing.map(
(trx) => `${trx.order_id}-${trx.reference}-${trx.reference_id}`
)
)
const selectedData: CreateOrderTransactionDTO[] = []
for (const trx of trxsData) {
if (
!existingSet.has(`${trx.order_id}-${trx.reference}-${trx.reference_id}`)
) {
selectedData.push(trx)
} }
} }
const created = await service.addOrderTransactions(trxsData) if (!selectedData.length) {
return new StepResponse(null)
}
const created = await service.addOrderTransactions(selectedData)
return new StepResponse( return new StepResponse(
(Array.isArray(data) (Array.isArray(data)
@@ -9,6 +9,7 @@ import {
MedusaError, MedusaError,
OrderStatus, OrderStatus,
arrayDifference, arrayDifference,
deepFlatMap,
isPresent, isPresent,
} from "@medusajs/framework/utils" } from "@medusajs/framework/utils"
@@ -21,6 +22,58 @@ export function throwIfOrderIsCancelled({ order }: { order: OrderDTO }) {
} }
} }
export function throwIfManagedItemsNotStockedAtReturnLocation({
order,
orderReturn,
inputItems,
}: {
order: Pick<OrderDTO, "items">
orderReturn: Pick<ReturnDTO, "location_id">
inputItems: OrderWorkflow.CreateOrderFulfillmentWorkflowInput["items"]
}) {
if (!orderReturn?.location_id) {
return
}
const inputItemIds = new Set(inputItems.map((i) => i.id))
const requestedOrderItems = order.items?.filter((oi: any) =>
inputItemIds.has(oi.id)
)
const invalidManagedItems: string[] = []
for (const orderItem of requestedOrderItems ?? []) {
const variant = (orderItem as any)?.variant
if (!variant?.manage_inventory) {
continue
}
let hasStockAtLocation = false
deepFlatMap(
orderItem,
"variant.inventory_items.inventory.location_levels",
({ location_levels }) => {
if (location_levels?.location_id === orderReturn.location_id) {
hasStockAtLocation = true
}
}
)
if (!hasStockAtLocation) {
invalidManagedItems.push(orderItem.id)
}
}
if (invalidManagedItems.length) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Cannot request item return at location ${
orderReturn.location_id
} for managed inventory items: ${invalidManagedItems.join(", ")}`
)
}
}
export function throwIfItemsDoesNotExistsInOrder({ export function throwIfItemsDoesNotExistsInOrder({
order, order,
inputItems, inputItems,
@@ -6,7 +6,12 @@ import {
OrderWorkflow, OrderWorkflow,
ReturnDTO, ReturnDTO,
} from "@medusajs/framework/types" } from "@medusajs/framework/types"
import { ChangeActionType, OrderChangeStatus } from "@medusajs/framework/utils" import {
ChangeActionType,
OrderChangeStatus,
deepFlatMap,
isDefined,
} from "@medusajs/framework/utils"
import { import {
WorkflowData, WorkflowData,
WorkflowResponse, WorkflowResponse,
@@ -23,6 +28,7 @@ import { updateOrderChangesStep } from "../../steps/update-order-changes"
import { import {
throwIfIsCancelled, throwIfIsCancelled,
throwIfItemsDoesNotExistsInOrder, throwIfItemsDoesNotExistsInOrder,
throwIfManagedItemsNotStockedAtReturnLocation,
throwIfOrderChangeIsNotActive, throwIfOrderChangeIsNotActive,
} from "../../utils/order-validation" } from "../../utils/order-validation"
import { createOrderChangeActionsWorkflow } from "../create-order-change-actions" import { createOrderChangeActionsWorkflow } from "../create-order-change-actions"
@@ -105,6 +111,11 @@ export const orderClaimRequestItemReturnValidationStep = createStep(
throwIfIsCancelled(orderReturn, "Return") throwIfIsCancelled(orderReturn, "Return")
throwIfOrderChangeIsNotActive({ orderChange }) throwIfOrderChangeIsNotActive({ orderChange })
throwIfItemsDoesNotExistsInOrder({ order, inputItems: items }) throwIfItemsDoesNotExistsInOrder({ order, inputItems: items })
throwIfManagedItemsNotStockedAtReturnLocation({
order,
orderReturn,
inputItems: items,
})
} }
) )
@@ -154,34 +165,23 @@ export const orderClaimRequestItemReturnWorkflow = createWorkflow(
}).then(() => { }).then(() => {
return useRemoteQueryStep({ return useRemoteQueryStep({
entry_point: "return", entry_point: "return",
fields: ["id", "status", "order_id", "canceled_at"], fields: ["id", "status", "order_id", "location_id", "canceled_at"],
variables: { id: orderClaim.return_id }, variables: { id: orderClaim.return_id },
list: false, list: false,
throw_if_key_not_found: true, throw_if_key_not_found: true,
}).config({ name: "return-query" }) as ReturnDTO }).config({ name: "return-query" }) as ReturnDTO
}) })
const createdReturn = when({ orderClaim }, ({ orderClaim }) => {
return !orderClaim.return_id
}).then(() => {
return createReturnsStep([
{
order_id: orderClaim.order_id,
claim_id: orderClaim.id,
},
])
})
const orderReturn: ReturnDTO = transform(
{ createdReturn, existingOrderReturn },
({ createdReturn, existingOrderReturn }) => {
return existingOrderReturn ?? (createdReturn?.[0] as ReturnDTO)
}
)
const order: OrderDTO = useRemoteQueryStep({ const order: OrderDTO = useRemoteQueryStep({
entry_point: "orders", entry_point: "orders",
fields: ["id", "status", "items.*"], fields: [
"id",
"status",
"items.*",
"items.variant.manage_inventory",
"items.variant.inventory_items.inventory_item_id",
"items.variant.inventory_items.inventory.location_levels.location_id",
],
variables: { id: orderClaim.order_id }, variables: { id: orderClaim.order_id },
list: false, list: false,
throw_if_key_not_found: true, throw_if_key_not_found: true,
@@ -202,6 +202,51 @@ export const orderClaimRequestItemReturnWorkflow = createWorkflow(
name: "order-change-query", name: "order-change-query",
}) })
const pickItemLocationId = transform(
{ order, input },
({ order, input }) => {
if (input.location_id) {
return input.location_id
}
// pick the first item location
const item = order?.items?.find(
(item) => item.id === input.items[0].id
) as any
let locationId: string | undefined
deepFlatMap(
item,
"variant.inventory_items.inventory.location_levels",
({ location_levels }) => {
if (!locationId && isDefined(location_levels?.location_id)) {
locationId = location_levels.location_id
}
}
)
return locationId
}
)
const createdReturn = when({ orderClaim }, ({ orderClaim }) => {
return !orderClaim.return_id
}).then(() => {
return createReturnsStep([
{
order_id: orderClaim.order_id,
claim_id: orderClaim.id,
location_id: pickItemLocationId,
},
])
})
const orderReturn: ReturnDTO = transform(
{ createdReturn, existingOrderReturn },
({ createdReturn, existingOrderReturn }) => {
return existingOrderReturn ?? (createdReturn?.[0] as ReturnDTO)
}
)
when({ createdReturn }, ({ createdReturn }) => { when({ createdReturn }, ({ createdReturn }) => {
return !!createdReturn?.length return !!createdReturn?.length
}).then(() => { }).then(() => {
@@ -13,8 +13,8 @@ import {
} from "@medusajs/framework/workflows-sdk" } from "@medusajs/framework/workflows-sdk"
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 { cancelPaymentCollectionWorkflow } from "../../payment-collection/workflows/cancel-payment-collection" import { cancelPaymentCollectionWorkflow } from "../../payment-collection/workflows/cancel-payment-collection"
import { createOrderPaymentCollectionWorkflow } from "./create-order-payment-collection"
/** /**
* The details of the order payment collection to create or update. * The details of the order payment collection to create or update.
@@ -65,7 +65,7 @@ export const createOrUpdateOrderPaymentCollectionWorkflow = createWorkflow(
) => { ) => {
const order = useRemoteQueryStep({ const order = useRemoteQueryStep({
entry_point: "order", entry_point: "order",
fields: ["id", "summary", "currency_code", "region_id"], fields: ["id", "summary", "total", "currency_code", "region_id"],
variables: { id: input.order_id }, variables: { id: input.order_id },
throw_if_key_not_found: true, throw_if_key_not_found: true,
list: false, list: false,
@@ -90,7 +90,7 @@ export const createOrderCreditLinesWorkflow = createWorkflow(
) => { ) => {
const orderQuery = useQueryGraphStep({ const orderQuery = useQueryGraphStep({
entity: "orders", entity: "orders",
fields: ["id", "status", "summary"], fields: ["id", "status", "summary", "total"],
filters: { id: input.id }, filters: { id: input.id },
options: { throwIfKeyNotFound: true }, options: { throwIfKeyNotFound: true },
}).config({ name: "get-order" }) }).config({ name: "get-order" })
@@ -5,8 +5,8 @@ import {
createWorkflow, createWorkflow,
transform, transform,
} from "@medusajs/framework/workflows-sdk" } from "@medusajs/framework/workflows-sdk"
import { createRemoteLinkStep, useRemoteQueryStep } from "../../common"
import { createPaymentCollectionsStep } from "../../cart" import { createPaymentCollectionsStep } from "../../cart"
import { createRemoteLinkStep, useRemoteQueryStep } from "../../common"
/** /**
* The details of the payment collection to create. * The details of the payment collection to create.
@@ -46,12 +46,10 @@ export const createOrderPaymentCollectionWorkflowId =
*/ */
export const createOrderPaymentCollectionWorkflow = createWorkflow( export const createOrderPaymentCollectionWorkflow = createWorkflow(
createOrderPaymentCollectionWorkflowId, createOrderPaymentCollectionWorkflowId,
( (input: WorkflowData<CreateOrderPaymentCollectionWorkflowInput>) => {
input: WorkflowData<CreateOrderPaymentCollectionWorkflowInput>
) => {
const order = useRemoteQueryStep({ const order = useRemoteQueryStep({
entry_point: "order", entry_point: "order",
fields: ["id", "summary", "currency_code", "region_id"], fields: ["id", "summary", "total", "currency_code", "region_id"],
variables: { id: input.order_id }, variables: { id: input.order_id },
throw_if_key_not_found: true, throw_if_key_not_found: true,
list: false, list: false,
@@ -6,14 +6,19 @@ import {
OrderWorkflow, OrderWorkflow,
ReturnDTO, ReturnDTO,
} from "@medusajs/framework/types" } from "@medusajs/framework/types"
import { ChangeActionType, OrderChangeStatus } from "@medusajs/framework/utils"
import { import {
WorkflowData, ChangeActionType,
WorkflowResponse, deepFlatMap,
isDefined,
OrderChangeStatus,
} from "@medusajs/framework/utils"
import {
createStep, createStep,
createWorkflow, createWorkflow,
transform, transform,
when, when,
WorkflowData,
WorkflowResponse,
} from "@medusajs/framework/workflows-sdk" } from "@medusajs/framework/workflows-sdk"
import { useRemoteQueryStep } from "../../../common" import { useRemoteQueryStep } from "../../../common"
import { updateOrderExchangesStep } from "../../steps/exchange/update-order-exchanges" import { updateOrderExchangesStep } from "../../steps/exchange/update-order-exchanges"
@@ -23,6 +28,7 @@ import { updateOrderChangesStep } from "../../steps/update-order-changes"
import { import {
throwIfIsCancelled, throwIfIsCancelled,
throwIfItemsDoesNotExistsInOrder, throwIfItemsDoesNotExistsInOrder,
throwIfManagedItemsNotStockedAtReturnLocation,
throwIfOrderChangeIsNotActive, throwIfOrderChangeIsNotActive,
} from "../../utils/order-validation" } from "../../utils/order-validation"
import { createOrderChangeActionsWorkflow } from "../create-order-change-actions" import { createOrderChangeActionsWorkflow } from "../create-order-change-actions"
@@ -106,6 +112,11 @@ export const exchangeRequestItemReturnValidationStep = createStep(
throwIfIsCancelled(orderReturn, "Return") throwIfIsCancelled(orderReturn, "Return")
throwIfOrderChangeIsNotActive({ orderChange }) throwIfOrderChangeIsNotActive({ orderChange })
throwIfItemsDoesNotExistsInOrder({ order, inputItems: items }) throwIfItemsDoesNotExistsInOrder({ order, inputItems: items })
throwIfManagedItemsNotStockedAtReturnLocation({
order,
orderReturn,
inputItems: items,
})
} }
) )
@@ -144,7 +155,7 @@ export const orderExchangeRequestItemReturnWorkflow = createWorkflow(
): WorkflowResponse<OrderPreviewDTO> { ): WorkflowResponse<OrderPreviewDTO> {
const orderExchange = useRemoteQueryStep({ const orderExchange = useRemoteQueryStep({
entry_point: "order_exchange", entry_point: "order_exchange",
fields: ["id", "order_id", "return_id", "canceled_at"], fields: ["id", "order_id", "return_id", "location_id", "canceled_at"],
variables: { id: input.exchange_id }, variables: { id: input.exchange_id },
list: false, list: false,
throw_if_key_not_found: true, throw_if_key_not_found: true,
@@ -162,27 +173,16 @@ export const orderExchangeRequestItemReturnWorkflow = createWorkflow(
}).config({ name: "return-query" }) as ReturnDTO }).config({ name: "return-query" }) as ReturnDTO
}) })
const createdReturn = when({ orderExchange }, ({ orderExchange }) => {
return !orderExchange.return_id
}).then(() => {
return createReturnsStep([
{
order_id: orderExchange.order_id,
exchange_id: orderExchange.id,
},
])
})
const orderReturn: ReturnDTO = transform(
{ createdReturn, existingOrderReturn, orderExchange },
({ createdReturn, existingOrderReturn, orderExchange }) => {
return existingOrderReturn ?? (createdReturn?.[0] as ReturnDTO)
}
)
const order: OrderDTO = useRemoteQueryStep({ const order: OrderDTO = useRemoteQueryStep({
entry_point: "orders", entry_point: "orders",
fields: ["id", "status", "items.*"], fields: [
"id",
"status",
"items.*",
"items.variant.manage_inventory",
"items.variant.inventory_items.inventory_item_id",
"items.variant.inventory_items.inventory.location_levels.location_id",
],
variables: { id: orderExchange.order_id }, variables: { id: orderExchange.order_id },
list: false, list: false,
throw_if_key_not_found: true, throw_if_key_not_found: true,
@@ -204,6 +204,51 @@ export const orderExchangeRequestItemReturnWorkflow = createWorkflow(
status: [OrderChangeStatus.PENDING, OrderChangeStatus.REQUESTED], status: [OrderChangeStatus.PENDING, OrderChangeStatus.REQUESTED],
}) })
const pickItemLocationId = transform(
{ order, input },
({ order, input }) => {
if (input.location_id) {
return input.location_id
}
// pick the first item location
const item = order?.items?.find(
(item) => item.id === input.items[0].id
) as any
let locationId: string | undefined
deepFlatMap(
item,
"variant.inventory_items.inventory.location_levels",
({ location_levels }) => {
if (!locationId && isDefined(location_levels?.location_id)) {
locationId = location_levels.location_id
}
}
)
return locationId
}
)
const createdReturn = when({ orderExchange }, ({ orderExchange }) => {
return !orderExchange.return_id
}).then(() => {
return createReturnsStep([
{
order_id: orderExchange.order_id,
location_id: pickItemLocationId,
exchange_id: orderExchange.id,
},
])
})
const orderReturn: ReturnDTO = transform(
{ createdReturn, existingOrderReturn, orderExchange },
({ createdReturn, existingOrderReturn, orderExchange }) => {
return existingOrderReturn ?? (createdReturn?.[0] as ReturnDTO)
}
)
when({ createdReturn }, ({ createdReturn }) => { when({ createdReturn }, ({ createdReturn }) => {
return !!createdReturn?.length return !!createdReturn?.length
}).then(() => { }).then(() => {
@@ -234,6 +234,7 @@ export const markOrderFulfillmentAsDeliveredWorkflow = createWorkflow(
fields: [ fields: [
"id", "id",
"summary", "summary",
"total",
"currency_code", "currency_code",
"region_id", "region_id",
"fulfillments.id", "fulfillments.id",
@@ -42,7 +42,7 @@ export const createOrderRefundCreditLinesWorkflow = createWorkflow(
) { ) {
const orderQuery = useQueryGraphStep({ const orderQuery = useQueryGraphStep({
entity: "orders", entity: "orders",
fields: ["id", "status", "summary", "payment_collections.id"], fields: ["id", "status", "summary", "total", "payment_collections.id"],
filters: { id: input.order_id }, filters: { id: input.order_id },
options: { throwIfKeyNotFound: true }, options: { throwIfKeyNotFound: true },
}).config({ name: "get-order" }) }).config({ name: "get-order" })
@@ -29,6 +29,7 @@ export const refundCapturedPaymentsWorkflow = createWorkflow(
"id", "id",
"status", "status",
"summary", "summary",
"total",
"payment_collections.payments.id", "payment_collections.payments.id",
"payment_collections.payments.amount", "payment_collections.payments.amount",
"payment_collections.payments.refunds.id", "payment_collections.payments.refunds.id",
@@ -34,6 +34,7 @@ import {
throwIfIsCancelled, throwIfIsCancelled,
throwIfOrderChangeIsNotActive, throwIfOrderChangeIsNotActive,
} from "../../utils/order-validation" } from "../../utils/order-validation"
import { createOrUpdateOrderPaymentCollectionWorkflow } from "../create-or-update-order-payment-collection"
/** /**
* The data to validate that a return receival can be confirmed. * The data to validate that a return receival can be confirmed.
@@ -363,7 +364,15 @@ export const confirmReturnReceiveWorkflow = createWorkflow(
orderId: order.id, orderId: order.id,
confirmed_by: input.confirmed_by, confirmed_by: input.confirmed_by,
}), }),
adjustInventoryLevelsStep(inventoryAdjustment), adjustInventoryLevelsStep(inventoryAdjustment)
)
parallelize(
createOrUpdateOrderPaymentCollectionWorkflow.runAsStep({
input: {
order_id: order.id,
},
}),
emitEventStep({ emitEventStep({
eventName: OrderWorkflowEvents.RETURN_RECEIVED, eventName: OrderWorkflowEvents.RETURN_RECEIVED,
data: { data: {
@@ -1,13 +1,13 @@
import { import {
AdditionalData,
BigNumberInput, BigNumberInput,
CreateOrderShippingMethodDTO, CreateOrderShippingMethodDTO,
FulfillmentWorkflow, FulfillmentWorkflow,
OrderDTO, OrderDTO,
ReturnDTO,
OrderWorkflow, OrderWorkflow,
ReturnDTO,
ShippingOptionDTO, ShippingOptionDTO,
WithCalculatedPrice, WithCalculatedPrice,
AdditionalData,
} from "@medusajs/framework/types" } from "@medusajs/framework/types"
import { import {
MathBN, MathBN,
@@ -25,6 +25,7 @@ import {
parallelize, parallelize,
transform, transform,
} from "@medusajs/framework/workflows-sdk" } from "@medusajs/framework/workflows-sdk"
import { pricingContextResult } from "../../../cart/utils/schemas"
import { import {
createRemoteLinkStep, createRemoteLinkStep,
emitEventStep, emitEventStep,
@@ -38,7 +39,6 @@ import {
throwIfOrderIsCancelled, throwIfOrderIsCancelled,
} from "../../utils/order-validation" } from "../../utils/order-validation"
import { validateReturnReasons } from "../../utils/validate-return-reason" import { validateReturnReasons } from "../../utils/validate-return-reason"
import { pricingContextResult } from "../../../cart/utils/schemas"
function prepareShippingMethodData({ function prepareShippingMethodData({
orderId, orderId,
@@ -423,6 +423,7 @@ export const createAndCompleteReturnOrderWorkflow = createWorkflow(
const returnCreated = createCompleteReturnStep({ const returnCreated = createCompleteReturnStep({
order_id: input.order_id, order_id: input.order_id,
location_id: input.location_id,
items: input.items, items: input.items,
shipping_method: shippingMethodData, shipping_method: shippingMethodData,
created_by: input.created_by, created_by: input.created_by,
@@ -146,31 +146,33 @@ export const refundPaymentWorkflow = createWorkflow(
const order = useRemoteQueryStep({ const order = useRemoteQueryStep({
entry_point: "order", entry_point: "order",
fields: ["id", "summary", "currency_code", "region_id"], fields: ["id", "summary", "total", "currency_code", "region_id"],
variables: { id: orderPaymentCollection.order.id }, variables: { id: orderPaymentCollection.order.id },
throw_if_key_not_found: true, throw_if_key_not_found: true,
list: false, list: false,
}).config({ name: "order" }) }).config({ name: "order" })
validateRefundStep({ order, payment, amount: input.amount }) validateRefundStep({ order, payment, amount: input.amount })
refundPaymentStep(input) const refundPayment = refundPaymentStep(input)
when({ orderPaymentCollection }, ({ orderPaymentCollection }) => { when({ orderPaymentCollection }, ({ orderPaymentCollection }) => {
return !!orderPaymentCollection?.order?.id return !!orderPaymentCollection?.order?.id
}).then(() => { }).then(() => {
const orderTransactionData = transform( const orderTransactionData = transform(
{ input, payment, orderPaymentCollection }, { input, refundPayment, orderPaymentCollection, order },
({ input, payment, orderPaymentCollection }) => { ({ input, refundPayment, orderPaymentCollection, order }) => {
return { return refundPayment.refunds?.map((refund) => {
order_id: orderPaymentCollection.order.id, return {
amount: MathBN.mult( order_id: orderPaymentCollection.order.id,
input.amount ?? payment.raw_amount ?? payment.amount, amount: MathBN.mult(
-1 input.amount ?? refund.raw_amount ?? refund.amount,
), -1
currency_code: payment.currency_code ?? order.currency_code, ),
reference_id: payment.id, currency_code: refundPayment.currency_code ?? order.currency_code,
reference: "refund", reference_id: refund.id,
} reference: "refund",
}
})
} }
) )
+1 -1
View File
@@ -1578,7 +1578,7 @@ export interface CreateOrderReturnDTO extends BaseOrderBundledActionsDTO {
/** /**
* The ID of the location to return the items to. * The ID of the location to return the items to.
*/ */
location_id?: string location_id?: string | null
/** /**
* The items of the return. * The items of the return.
@@ -73,6 +73,10 @@ export interface OrderExchangeRequestItemReturnWorkflowInput {
* The ID of the return that's associated with the exchange. * The ID of the return that's associated with the exchange.
*/ */
return_id: string return_id: string
/**
* The ID of the location to return the items to.
*/
location_id?: string
/** /**
* The ID of the exchange to add the inbound items to. * The ID of the exchange to add the inbound items to.
*/ */
@@ -95,6 +99,10 @@ export interface OrderClaimRequestItemReturnWorkflowInput {
* The ID of the return that's associated with the claim. * The ID of the return that's associated with the claim.
*/ */
return_id: string return_id: string
/**
* The ID of the location to return the items to.
*/
location_id?: string
/** /**
* The ID of the claim to add the items to. * The ID of the claim to add the items to.
*/ */
@@ -41,6 +41,7 @@ describe("Total calculation", function () {
subtotal: 60, subtotal: 60,
total: 66, total: 66,
original_total: 66, original_total: 66,
original_subtotal: 60,
discount_total: 0, discount_total: 0,
discount_subtotal: 0, discount_subtotal: 0,
discount_tax_total: 0, discount_tax_total: 0,
@@ -60,6 +61,7 @@ describe("Total calculation", function () {
subtotal: 5, subtotal: 5,
total: 7.5, total: 7.5,
original_total: 7.5, original_total: 7.5,
original_subtotal: 5,
discount_total: 0, discount_total: 0,
discount_subtotal: 0, discount_subtotal: 0,
discount_tax_total: 0, discount_tax_total: 0,
@@ -78,6 +80,7 @@ describe("Total calculation", function () {
item_tax_total: 8.5, item_tax_total: 8.5,
item_discount_total: 0, item_discount_total: 0,
original_total: 73.5, original_total: 73.5,
original_subtotal: 65,
original_tax_total: 8.5, original_tax_total: 8.5,
original_item_subtotal: 65, original_item_subtotal: 65,
original_item_total: 73.5, original_item_total: 73.5,
@@ -132,6 +135,7 @@ describe("Total calculation", function () {
subtotal: 100, subtotal: 100,
total: 99, total: 99,
original_total: 110, original_total: 110,
original_subtotal: 100,
discount_total: 11, discount_total: 11,
discount_subtotal: 10, discount_subtotal: 10,
discount_tax_total: 1, discount_tax_total: 1,
@@ -146,6 +150,7 @@ describe("Total calculation", function () {
discount_subtotal: 10, discount_subtotal: 10,
discount_tax_total: 1, discount_tax_total: 1,
original_total: 110, original_total: 110,
original_subtotal: 100,
original_tax_total: 10, original_tax_total: 10,
item_total: 99, item_total: 99,
item_subtotal: 100, item_subtotal: 100,
@@ -253,6 +258,7 @@ describe("Total calculation", function () {
subtotal: 90, subtotal: 90,
total: 89.1, total: 89.1,
original_total: 99, original_total: 99,
original_subtotal: 90,
discount_total: 9.9, discount_total: 9.9,
discount_subtotal: 9, discount_subtotal: 9,
discount_tax_total: 0.9, discount_tax_total: 0.9,
@@ -280,6 +286,7 @@ describe("Total calculation", function () {
subtotal: 9, subtotal: 9,
total: 6.6, total: 6.6,
original_total: 9.9, original_total: 9.9,
original_subtotal: 9,
discount_total: 3.3, discount_total: 3.3,
discount_subtotal: 3, discount_subtotal: 3,
discount_tax_total: 0.3, discount_tax_total: 0.3,
@@ -308,6 +315,7 @@ describe("Total calculation", function () {
subtotal: 90, subtotal: 90,
total: 89.1, total: 89.1,
original_total: 99, original_total: 99,
original_subtotal: 90,
discount_total: 9.9, discount_total: 9.9,
discount_subtotal: 9, discount_subtotal: 9,
discount_tax_total: 0.9, discount_tax_total: 0.9,
@@ -334,6 +342,7 @@ describe("Total calculation", function () {
subtotal: 9, subtotal: 9,
total: 6.6, total: 6.6,
original_total: 9.9, original_total: 9.9,
original_subtotal: 9,
discount_total: 3.3, discount_total: 3.3,
discount_subtotal: 3, discount_subtotal: 3,
discount_tax_total: 0.3, discount_tax_total: 0.3,
@@ -348,6 +357,7 @@ describe("Total calculation", function () {
discount_subtotal: 24, discount_subtotal: 24,
discount_tax_total: 2.4, discount_tax_total: 2.4,
original_total: 217.8, original_total: 217.8,
original_subtotal: 198,
original_tax_total: 19.8, original_tax_total: 19.8,
item_total: 95.7, item_total: 95.7,
item_subtotal: 99, item_subtotal: 99,
@@ -431,6 +441,7 @@ describe("Total calculation", function () {
subtotal: 90.9090909090909, subtotal: 90.9090909090909,
total: 100, total: 100,
original_total: 100, original_total: 100,
original_subtotal: 90.9090909090909,
discount_total: 0, discount_total: 0,
discount_tax_total: 0, discount_tax_total: 0,
tax_total: 9.090909090909092, tax_total: 9.090909090909092,
@@ -444,6 +455,7 @@ describe("Total calculation", function () {
discount_total: 0, discount_total: 0,
discount_tax_total: 0, discount_tax_total: 0,
original_total: 100, original_total: 100,
original_subtotal: 90.9090909090909,
original_tax_total: 9.090909090909092, original_tax_total: 9.090909090909092,
item_total: 100, item_total: 100,
item_subtotal: 90.9090909090909, item_subtotal: 90.9090909090909,
@@ -478,6 +490,7 @@ describe("Total calculation", function () {
discount_tax_total: 0, discount_tax_total: 0,
tax_total: 10, tax_total: 10,
original_tax_total: 10, original_tax_total: 10,
original_subtotal: 100,
}, },
], ],
total: 110, total: 110,
@@ -487,6 +500,7 @@ describe("Total calculation", function () {
discount_total: 0, discount_total: 0,
discount_tax_total: 0, discount_tax_total: 0,
original_total: 110, original_total: 110,
original_subtotal: 100,
original_tax_total: 10, original_tax_total: 10,
item_total: 110, item_total: 110,
item_subtotal: 100, item_subtotal: 100,
@@ -516,6 +530,7 @@ describe("Total calculation", function () {
], ],
subtotal: 90.9090909090909, subtotal: 90.9090909090909,
total: 100, total: 100,
original_subtotal: 90.9090909090909,
original_total: 100, original_total: 100,
discount_total: 0, discount_total: 0,
discount_tax_total: 0, discount_tax_total: 0,
@@ -537,6 +552,7 @@ describe("Total calculation", function () {
subtotal: 100, subtotal: 100,
total: 110, total: 110,
original_total: 110, original_total: 110,
original_subtotal: 100,
discount_total: 0, discount_total: 0,
discount_tax_total: 0, discount_tax_total: 0,
tax_total: 10, tax_total: 10,
@@ -550,6 +566,7 @@ describe("Total calculation", function () {
discount_total: 0, discount_total: 0,
discount_tax_total: 0, discount_tax_total: 0,
original_total: 210, original_total: 210,
original_subtotal: 190.9090909090909,
original_tax_total: 19.09090909090909, original_tax_total: 19.09090909090909,
item_total: 210, item_total: 210,
item_subtotal: 190.9090909090909, item_subtotal: 190.9090909090909,
@@ -606,6 +623,7 @@ describe("Total calculation", function () {
is_tax_inclusive: true, is_tax_inclusive: true,
original_total: 120, original_total: 120,
original_subtotal: 100,
original_tax_total: 20, original_tax_total: 20,
discount_subtotal: 8.333333333333334, discount_subtotal: 8.333333333333334,
@@ -637,6 +655,7 @@ describe("Total calculation", function () {
original_item_total: 120, original_item_total: 120,
original_tax_total: 20, original_tax_total: 20,
original_total: 120, original_total: 120,
original_subtotal: 100,
discount_subtotal: 8.333333333333334, discount_subtotal: 8.333333333333334,
discount_tax_total: 1.6666666666666667, discount_tax_total: 1.6666666666666667,
@@ -712,6 +731,7 @@ describe("Total calculation", function () {
subtotal: 100, subtotal: 100,
total: 88, total: 88,
original_total: 110, original_total: 110,
original_subtotal: 100,
discount_total: 22, discount_total: 22,
discount_subtotal: 20, discount_subtotal: 20,
discount_tax_total: 2, discount_tax_total: 2,
@@ -739,6 +759,7 @@ describe("Total calculation", function () {
subtotal: 25, subtotal: 25,
total: 25.3, total: 25.3,
original_total: 27.5, original_total: 27.5,
original_subtotal: 25,
discount_total: 2.2, discount_total: 2.2,
discount_subtotal: 2, discount_subtotal: 2,
discount_tax_total: 0.2, discount_tax_total: 0.2,
@@ -753,6 +774,7 @@ describe("Total calculation", function () {
discount_subtotal: 22, discount_subtotal: 22,
discount_tax_total: 2.2, discount_tax_total: 2.2,
original_total: 137.5, original_total: 137.5,
original_subtotal: 125,
original_tax_total: 12.5, original_tax_total: 12.5,
item_total: 88, item_total: 88,
item_subtotal: 100, item_subtotal: 100,
@@ -836,8 +858,8 @@ describe("Total calculation", function () {
tax_lines: [ tax_lines: [
{ {
rate: 10, rate: 10,
total: 8, total: 0,
subtotal: 10, subtotal: 0,
}, },
], ],
adjustments: [ adjustments: [
@@ -847,14 +869,15 @@ describe("Total calculation", function () {
total: 22, total: 22,
}, },
], ],
subtotal: 100, subtotal: 0,
total: 88, total: 0,
original_total: 110, original_total: 0,
discount_total: 22, original_subtotal: 0,
discount_subtotal: 20, discount_total: 0,
discount_tax_total: 2, discount_subtotal: 0,
tax_total: 8, discount_tax_total: 0,
original_tax_total: 10, tax_total: 0,
original_tax_total: 0,
refundable_total_per_unit: 0, refundable_total_per_unit: 0,
refundable_total: 0, refundable_total: 0,
fulfilled_total: 88, fulfilled_total: 88,
@@ -865,21 +888,22 @@ describe("Total calculation", function () {
write_off_total: 44, write_off_total: 44,
}, },
], ],
total: 48, total: -40,
subtotal: 100, subtotal: 0,
tax_total: 8, tax_total: 0,
discount_total: 22, discount_total: 0,
discount_subtotal: 20, discount_subtotal: 0,
discount_tax_total: 2, discount_tax_total: 0,
original_total: 110, original_total: 0,
original_tax_total: 10, original_subtotal: 0,
item_total: 88, original_tax_total: 0,
item_subtotal: 100, item_total: 0,
item_tax_total: 8, item_subtotal: 0,
item_discount_total: 22, item_tax_total: 0,
original_item_total: 110, item_discount_total: 0,
original_item_subtotal: 100, original_item_total: 0,
original_item_tax_total: 10, original_item_subtotal: 0,
original_item_tax_total: 0,
fulfilled_total: 88, fulfilled_total: 88,
shipped_total: 88, shipped_total: 88,
return_requested_total: 0, return_requested_total: 0,
@@ -923,6 +947,7 @@ describe("Total calculation", function () {
{ {
rate: 19, rate: 19,
subtotal: 22.61, subtotal: 22.61,
total: 0,
}, },
], ],
adjustments: [ adjustments: [
@@ -935,6 +960,7 @@ describe("Total calculation", function () {
subtotal: 119, subtotal: 119,
total: 0, total: 0,
original_total: 141.61, original_total: 141.61,
original_subtotal: 119,
discount_total: 141.61, discount_total: 141.61,
discount_subtotal: 119, discount_subtotal: 119,
discount_tax_total: 22.61, discount_tax_total: 22.61,
@@ -949,6 +975,7 @@ describe("Total calculation", function () {
discount_subtotal: 119, discount_subtotal: 119,
discount_tax_total: 22.61, discount_tax_total: 22.61,
original_total: 141.61, original_total: 141.61,
original_subtotal: 119,
original_tax_total: 22.61, original_tax_total: 22.61,
item_total: 0, item_total: 0,
item_subtotal: 119, item_subtotal: 119,
@@ -1026,6 +1053,7 @@ describe("Total calculation", function () {
discount_total: 0, discount_total: 0,
is_tax_inclusive: true, is_tax_inclusive: true,
original_tax_total: 19, original_tax_total: 19,
original_subtotal: 100,
original_total: 119, original_total: 119,
quantity: 1, quantity: 1,
subtotal: 100, subtotal: 100,
@@ -1045,6 +1073,7 @@ describe("Total calculation", function () {
original_item_tax_total: 19, original_item_tax_total: 19,
original_item_total: 119, original_item_total: 119,
original_tax_total: 19, original_tax_total: 19,
original_subtotal: 100,
original_total: 119, original_total: 119,
subtotal: 100, subtotal: 100,
tax_total: 19, tax_total: 19,
@@ -1069,6 +1098,7 @@ describe("Total calculation", function () {
discount_total: 0, discount_total: 0,
is_tax_inclusive: false, is_tax_inclusive: false,
original_tax_total: 22.61, original_tax_total: 22.61,
original_subtotal: 119,
original_total: 141.61, original_total: 141.61,
quantity: 1, quantity: 1,
subtotal: 119, subtotal: 119,
@@ -1088,6 +1118,7 @@ describe("Total calculation", function () {
original_item_tax_total: 22.61, original_item_tax_total: 22.61,
original_item_total: 141.61, original_item_total: 141.61,
original_tax_total: 22.61, original_tax_total: 22.61,
original_subtotal: 119,
original_total: 141.61, original_total: 141.61,
subtotal: 119, subtotal: 119,
tax_total: 22.61, tax_total: 22.61,
@@ -1113,6 +1144,7 @@ describe("Total calculation", function () {
is_tax_inclusive: true, is_tax_inclusive: true,
original_tax_total: 19, original_tax_total: 19,
original_total: 119, original_total: 119,
original_subtotal: 100,
quantity: 1, quantity: 1,
subtotal: 100, subtotal: 100,
tax_lines: [ tax_lines: [
@@ -1132,6 +1164,7 @@ describe("Total calculation", function () {
discount_total: 0, discount_total: 0,
is_tax_inclusive: false, is_tax_inclusive: false,
original_tax_total: 22.61, original_tax_total: 22.61,
original_subtotal: 119,
original_total: 141.61, original_total: 141.61,
quantity: 1, quantity: 1,
subtotal: 119, subtotal: 119,
@@ -1150,6 +1183,7 @@ describe("Total calculation", function () {
original_item_subtotal: 219, original_item_subtotal: 219,
original_item_tax_total: 41.61, original_item_tax_total: 41.61,
original_item_total: 260.61, original_item_total: 260.61,
original_subtotal: 219,
original_tax_total: 41.61, original_tax_total: 41.61,
original_total: 260.61, original_total: 260.61,
subtotal: 219, subtotal: 219,
@@ -1206,6 +1240,7 @@ describe("Total calculation", function () {
discount_total: 119, discount_total: 119,
is_tax_inclusive: true, is_tax_inclusive: true,
original_tax_total: 19, original_tax_total: 19,
original_subtotal: 100,
original_total: 119, original_total: 119,
quantity: 1, quantity: 1,
subtotal: 100, subtotal: 100,
@@ -1213,6 +1248,7 @@ describe("Total calculation", function () {
{ {
rate: 19, rate: 19,
subtotal: 19, subtotal: 19,
total: 0,
}, },
], ],
tax_total: 0, tax_total: 0,
@@ -1224,6 +1260,7 @@ describe("Total calculation", function () {
original_item_tax_total: 19, original_item_tax_total: 19,
original_item_total: 119, original_item_total: 119,
original_tax_total: 19, original_tax_total: 19,
original_subtotal: 100,
original_total: 119, original_total: 119,
subtotal: 100, subtotal: 100,
tax_total: 0, tax_total: 0,
@@ -4,9 +4,11 @@ import { BigNumber } from "../big-number"
import { MathBN } from "../math" import { MathBN } from "../math"
export function calculateAdjustmentTotal({ export function calculateAdjustmentTotal({
item,
adjustments, adjustments,
taxRate, taxRate,
}: { }: {
item?: { quantity: BigNumberInput }
adjustments: Pick<AdjustmentLineDTO, "amount" | "is_tax_inclusive">[] adjustments: Pick<AdjustmentLineDTO, "amount" | "is_tax_inclusive">[]
taxRate?: BigNumberInput taxRate?: BigNumberInput
}) { }) {
@@ -40,9 +42,24 @@ export function calculateAdjustmentTotal({
adj["total"] = new BigNumber(adjustmentsTotal) adj["total"] = new BigNumber(adjustmentsTotal)
} }
const quantity = item?.quantity || MathBN.convert(1)
let adjustmentPerItem = MathBN.convert(0)
let adjustmentSubtotalPerItem = MathBN.convert(0)
let adjustmentTaxTotalPerItem = MathBN.convert(0)
if (!MathBN.eq(quantity, 0)) {
adjustmentPerItem = MathBN.div(adjustmentsTotal, quantity)
adjustmentSubtotalPerItem = MathBN.div(adjustmentsSubtotal, quantity)
adjustmentTaxTotalPerItem = MathBN.div(adjustmentsTaxTotal, quantity)
}
return { return {
adjustmentsTotal, adjustmentsTotal,
adjustmentsSubtotal, adjustmentsSubtotal,
adjustmentsTaxTotal, adjustmentsTaxTotal,
adjustmentPerItem,
adjustmentSubtotalPerItem,
adjustmentTaxTotalPerItem,
} }
} }
+24 -1
View File
@@ -101,7 +101,8 @@ export function decorateCartTotals(
let shippingDiscountTotal = MathBN.convert(0) let shippingDiscountTotal = MathBN.convert(0)
const cartItems = items.map((item, index) => { const cartItems = items.map((item, index) => {
const itemTotals = Object.assign(item, itemsTotals[item.id ?? index] ?? {}) const rawTotals = itemsTotals[item.id ?? index] ?? {}
const itemTotals = Object.assign(item, rawTotals)
const itemSubtotal = itemTotals.subtotal const itemSubtotal = itemTotals.subtotal
const itemTotal = MathBN.convert(itemTotals.total) const itemTotal = MathBN.convert(itemTotals.total)
@@ -208,6 +209,10 @@ export function decorateCartTotals(
// TODO: Gift Card calculations // TODO: Gift Card calculations
const originalTotal = MathBN.add(itemsOriginalTotal, shippingOriginalTotal) const originalTotal = MathBN.add(itemsOriginalTotal, shippingOriginalTotal)
const originalSubtotal = MathBN.add(
itemsOriginalSubtotal,
shippingOriginalSubtotal
)
// TODO: subtract (cart.gift_card_total + cart.gift_card_tax_total) // TODO: subtract (cart.gift_card_total + cart.gift_card_tax_total)
const tempTotal = MathBN.add(subtotal, taxTotal) const tempTotal = MathBN.add(subtotal, taxTotal)
@@ -231,6 +236,7 @@ export function decorateCartTotals(
// cart.gift_card_tax_total = giftCardTotal.tax_total || 0 // cart.gift_card_tax_total = giftCardTotal.tax_total || 0
cart.original_total = new BigNumber(originalTotal) cart.original_total = new BigNumber(originalTotal)
cart.original_subtotal = new BigNumber(originalSubtotal)
cart.original_tax_total = new BigNumber(originalTaxTotal) cart.original_tax_total = new BigNumber(originalTaxTotal)
// cart.original_gift_card_total = // cart.original_gift_card_total =
@@ -264,5 +270,22 @@ export function decorateCartTotals(
cart.original_shipping_total = new BigNumber(shippingOriginalTotal) cart.original_shipping_total = new BigNumber(shippingOriginalTotal)
} }
// Calculate pending return total
if (cart.summary) {
const pendingReturnTotal = MathBN.sum(
0,
...(cart.items?.map((item) => item.return_requested_total ?? 0) ?? [0])
)
const pendingDifference = new BigNumber(
MathBN.sub(
MathBN.sub(cart.total, pendingReturnTotal),
cart.summary?.transaction_total ?? 0
)
)
cart.summary.pending_difference = pendingDifference
}
return cart return cart
} }
@@ -33,6 +33,7 @@ export interface GetItemTotalOutput {
unit_price: BigNumber unit_price: BigNumber
subtotal: BigNumber subtotal: BigNumber
original_subtotal: BigNumber
total: BigNumber total: BigNumber
original_total: BigNumber original_total: BigNumber
@@ -76,8 +77,7 @@ export function getLineItemsTotals(
function setRefundableTotal( function setRefundableTotal(
item: GetItemTotalInput, item: GetItemTotalInput,
discountsTotal: BigNumberInput, discountsTotal: BigNumberInput,
totals: GetItemTotalOutput, totals: GetItemTotalOutput
context: GetLineItemsTotalsContext
) { ) {
const itemDetail = item.detail! const itemDetail = item.detail!
const totalReturnedQuantity = MathBN.sum( const totalReturnedQuantity = MathBN.sum(
@@ -127,54 +127,107 @@ function getLineItemTotals(
? MathBN.div(totalItemPrice, MathBN.add(1, sumTaxRate)) ? MathBN.div(totalItemPrice, MathBN.add(1, sumTaxRate))
: totalItemPrice : totalItemPrice
// Proportional discounts to current quantity and compute taxes on the current net amount
const { const {
adjustmentsTotal: discountsTotal, adjustmentsTotal: discountsTotal,
adjustmentsSubtotal: discountsSubtotal, adjustmentsSubtotal: discountsSubtotalFull,
adjustmentsTaxTotal: discountTaxTotal, adjustmentSubtotalPerItem,
} = calculateAdjustmentTotal({ } = calculateAdjustmentTotal({
item,
adjustments: item.adjustments || [], adjustments: item.adjustments || [],
taxRate: sumTaxRate, taxRate: sumTaxRate,
}) })
const itemDetail = item.detail!
const totalReturnedQuantity = MathBN.sum(
itemDetail?.return_received_quantity ?? 0,
itemDetail?.return_dismissed_quantity ?? 0
)
const currentQuantity = MathBN.sub(item.quantity, totalReturnedQuantity)
const currentTotalItemPrice = MathBN.mult(item.unit_price, currentQuantity)
const currentSubtotal = isTaxInclusive
? MathBN.div(currentTotalItemPrice, MathBN.add(1, sumTaxRate))
: currentTotalItemPrice
const currentDiscountsSubtotal = MathBN.mult(
adjustmentSubtotalPerItem ?? 0,
currentQuantity
)
const taxTotal = calculateTaxTotal({ const taxTotal = calculateTaxTotal({
taxLines: item.tax_lines || [], taxLines: item.tax_lines || [],
taxableAmount: MathBN.sub(subtotal, discountsSubtotal), taxableAmount: MathBN.sub(currentSubtotal, currentDiscountsSubtotal),
setTotalField: "total", setTotalField: "total",
}) })
const originalTaxTotal = calculateTaxTotal({ const originalTaxTotal = calculateTaxTotal({
taxLines: item.tax_lines || [], taxLines: item.tax_lines || [],
taxableAmount: subtotal, taxableAmount: currentSubtotal,
setTotalField: "subtotal", setTotalField: "subtotal",
}) })
// Compute full-quantity net total after discounts and taxes to derive per-unit totals
const fullDiscountedTaxable = MathBN.sub(subtotal, discountsSubtotalFull ?? 0)
const taxTotalFull = calculateTaxTotal({
taxLines: item.tax_lines || [],
taxableAmount: fullDiscountedTaxable,
})
const fullNetTotal = MathBN.sum(fullDiscountedTaxable, taxTotalFull)
const totals: GetItemTotalOutput = { const totals: GetItemTotalOutput = {
quantity: item.quantity, quantity: item.quantity,
unit_price: item.unit_price, unit_price: item.unit_price,
subtotal: new BigNumber(subtotal), subtotal: new BigNumber(currentSubtotal),
total: new BigNumber( total: new BigNumber(
MathBN.sum(MathBN.sub(subtotal, discountsSubtotal), taxTotal) MathBN.sum(
MathBN.sub(currentSubtotal, currentDiscountsSubtotal),
taxTotal
)
),
original_subtotal: new BigNumber(
MathBN.sub(
isTaxInclusive
? currentTotalItemPrice
: MathBN.add(currentSubtotal, originalTaxTotal),
originalTaxTotal
)
), ),
original_total: new BigNumber( original_total: new BigNumber(
isTaxInclusive ? totalItemPrice : MathBN.add(subtotal, originalTaxTotal) isTaxInclusive
? currentTotalItemPrice
: MathBN.add(currentSubtotal, originalTaxTotal)
), ),
discount_total: new BigNumber(discountsTotal), // Discount values prorated to the current quantity
discount_subtotal: new BigNumber(discountsSubtotal), discount_subtotal: new BigNumber(currentDiscountsSubtotal),
discount_tax_total: new BigNumber(discountTaxTotal), discount_tax_total: new BigNumber(MathBN.sub(originalTaxTotal, taxTotal)),
discount_total: new BigNumber(
MathBN.add(
currentDiscountsSubtotal,
MathBN.sub(originalTaxTotal, taxTotal)
)
),
tax_total: new BigNumber(taxTotal), tax_total: new BigNumber(taxTotal),
original_tax_total: new BigNumber(originalTaxTotal), original_tax_total: new BigNumber(originalTaxTotal),
} }
if (isDefined(item.detail?.return_requested_quantity)) { if (
setRefundableTotal(item, discountsTotal, totals, context) isDefined(item.detail?.return_requested_quantity) ||
isDefined(item.detail?.return_received_quantity) ||
isDefined(item.detail?.return_dismissed_quantity)
) {
setRefundableTotal(item, discountsTotal, totals)
} }
// Per-unit total should be based on full-quantity net total to support lifecycle totals consistently
const div = MathBN.eq(item.quantity, 0) ? 1 : item.quantity const div = MathBN.eq(item.quantity, 0) ? 1 : item.quantity
const totalPerUnit = MathBN.div(totals.total, div) const totalPerUnit = MathBN.div(fullNetTotal, div)
const optionalFields = { const optionalFields = {
...(context.extraQuantityFields ?? {}), ...(context.extraQuantityFields ?? {}),
@@ -183,10 +236,9 @@ function getLineItemTotals(
for (const field in optionalFields) { for (const field in optionalFields) {
const totalField = optionalFields[field] const totalField = optionalFields[field]
let target = item[totalField] let target = field.includes(".")
if (field.includes(".")) { ? pickValueFromObject(field, item)
target = pickValueFromObject(field, item) : item[field]
}
if (!isDefined(target)) { if (!isDefined(target)) {
continue continue
@@ -20,6 +20,7 @@ export interface GetShippingMethodTotalOutput {
amount: BigNumber amount: BigNumber
subtotal: BigNumber subtotal: BigNumber
original_subtotal: BigNumber
total: BigNumber total: BigNumber
original_total: BigNumber original_total: BigNumber
@@ -96,6 +97,14 @@ export function getShippingMethodTotals(
total: new BigNumber( total: new BigNumber(
MathBN.sum(MathBN.sub(subtotal, discountsSubtotal), taxTotal) MathBN.sum(MathBN.sub(subtotal, discountsSubtotal), taxTotal)
), ),
original_subtotal: new BigNumber(
MathBN.sub(
isTaxInclusive
? shippingMethodAmount
: MathBN.add(subtotal, originalTaxTotal),
originalTaxTotal
)
),
original_total: new BigNumber( original_total: new BigNumber(
isTaxInclusive isTaxInclusive
? shippingMethodAmount ? shippingMethodAmount
@@ -11,10 +11,6 @@ export function calculateTaxTotal({
taxableAmount: BigNumberInput taxableAmount: BigNumberInput
setTotalField?: string setTotalField?: string
}) { }) {
if (MathBN.lte(taxableAmount, 0)) {
return MathBN.convert(0)
}
let taxTotal = MathBN.convert(0) let taxTotal = MathBN.convert(0)
for (const taxLine of taxLines) { for (const taxLine of taxLines) {
@@ -128,6 +128,7 @@ export type AdminPostClaimsAddItemsReqSchemaType = z.infer<
> >
export const AdminPostClaimsRequestReturnItemsReqSchema = z.object({ export const AdminPostClaimsRequestReturnItemsReqSchema = z.object({
location_id: z.string().optional(),
items: z.array( items: z.array(
z.object({ z.object({
id: z.string(), id: z.string(),
@@ -7,6 +7,7 @@ export const defaultAdminListOrderFields = [
"region_id", "region_id",
"*items", "*items",
"summary", "summary",
"total",
"metadata", "metadata",
"created_at", "created_at",
"updated_at", "updated_at",
@@ -31,6 +32,7 @@ export const defaultAdminOrderFields = [
"*shipping_methods.tax_lines", "*shipping_methods.tax_lines",
"*shipping_methods.adjustments", "*shipping_methods.adjustments",
"summary", "summary",
"total",
"metadata", "metadata",
"created_at", "created_at",
"updated_at", "updated_at",
@@ -127,6 +127,7 @@ export type AdminPostExchangesAddItemsReqSchemaType = z.infer<
> >
export const AdminPostExchangesReturnRequestItemsReqSchema = z.object({ export const AdminPostExchangesReturnRequestItemsReqSchema = z.object({
location_id: z.string().optional(),
items: z.array( items: z.array(
z.object({ z.object({
id: z.string(), id: z.string(),
@@ -4,6 +4,7 @@ export const defaultAdminOrderFields = [
"status", "status",
"version", "version",
"summary", "summary",
"total",
"metadata", "metadata",
"created_at", "created_at",
"updated_at", "updated_at",
@@ -19,6 +20,7 @@ export const defaultAdminRetrieveOrderFields = [
"discount_total", "discount_total",
"discount_tax_total", "discount_tax_total",
"original_total", "original_total",
"original_subtotal",
"original_tax_total", "original_tax_total",
"item_total", "item_total",
"item_subtotal", "item_subtotal",
@@ -27,6 +27,7 @@ export const defaultStoreRetrieveOrderFields = [
"discount_subtotal", "discount_subtotal",
"discount_tax_total", "discount_tax_total",
"original_total", "original_total",
"original_subtotal",
"original_tax_total", "original_tax_total",
"item_total", "item_total",
"item_subtotal", "item_subtotal",
@@ -2934,6 +2934,7 @@ moduleIntegrationTestRunner<ICartModuleService>({
subtotal: 100, subtotal: 100,
total: 0, total: 0,
original_total: 100, original_total: 100,
original_subtotal: 100,
discount_total: 100, discount_total: 100,
discount_subtotal: 100, discount_subtotal: 100,
discount_tax_total: 0, discount_tax_total: 0,
@@ -2951,6 +2952,10 @@ moduleIntegrationTestRunner<ICartModuleService>({
value: "100", value: "100",
precision: 20, precision: 20,
}, },
raw_original_subtotal: {
value: "100",
precision: 20,
},
raw_discount_total: { raw_discount_total: {
value: "100", value: "100",
precision: 20, precision: 20,
@@ -3042,6 +3047,7 @@ moduleIntegrationTestRunner<ICartModuleService>({
subtotal: 400, subtotal: 400,
total: 200, total: 200,
original_total: 400, original_total: 400,
original_subtotal: 400,
discount_total: 200, discount_total: 200,
discount_subtotal: 200, discount_subtotal: 200,
discount_tax_total: 0, discount_tax_total: 0,
@@ -3059,6 +3065,10 @@ moduleIntegrationTestRunner<ICartModuleService>({
value: "400", value: "400",
precision: 20, precision: 20,
}, },
raw_original_subtotal: {
value: "400",
precision: 20,
},
raw_discount_total: { raw_discount_total: {
value: "200", value: "200",
precision: 20, precision: 20,
@@ -3104,6 +3114,7 @@ moduleIntegrationTestRunner<ICartModuleService>({
subtotal: 10, subtotal: 10,
total: 10, total: 10,
original_total: 10, original_total: 10,
original_subtotal: 10,
discount_total: 0, discount_total: 0,
discount_subtotal: 0, discount_subtotal: 0,
discount_tax_total: 0, discount_tax_total: 0,
@@ -3121,6 +3132,10 @@ moduleIntegrationTestRunner<ICartModuleService>({
value: "10", value: "10",
precision: 20, precision: 20,
}, },
raw_original_subtotal: {
value: "10",
precision: 20,
},
raw_discount_total: { raw_discount_total: {
value: "0", value: "0",
precision: 20, precision: 20,
@@ -3166,6 +3181,7 @@ moduleIntegrationTestRunner<ICartModuleService>({
discount_subtotal: 300, discount_subtotal: 300,
discount_tax_total: 0, discount_tax_total: 0,
original_total: 510, original_total: 510,
original_subtotal: 510,
original_tax_total: 0, original_tax_total: 0,
item_total: 200, item_total: 200,
item_subtotal: 500, item_subtotal: 500,
@@ -3217,6 +3233,10 @@ moduleIntegrationTestRunner<ICartModuleService>({
value: "510", value: "510",
precision: 20, precision: 20,
}, },
raw_original_subtotal: {
value: "510",
precision: 20,
},
raw_original_tax_total: { raw_original_tax_total: {
value: "0", value: "0",
precision: 20, precision: 20,
@@ -227,7 +227,7 @@ moduleIntegrationTestRunner<IOrderModuleService>({
const serializedOrder = JSON.parse( const serializedOrder = JSON.parse(
JSON.stringify( JSON.stringify(
await service.retrieveOrder(created.id, { await service.retrieveOrder(created.id, {
select: ["id", "summary"], select: ["id", "summary", "total"],
}) })
) )
) )
@@ -246,7 +246,7 @@ moduleIntegrationTestRunner<IOrderModuleService>({
const serializedOrder2 = JSON.parse( const serializedOrder2 = JSON.parse(
JSON.stringify( JSON.stringify(
await service.retrieveOrder(created.id, { await service.retrieveOrder(created.id, {
select: ["id", "summary"], select: ["id", "summary", "total"],
}) })
) )
) )
@@ -271,7 +271,7 @@ moduleIntegrationTestRunner<IOrderModuleService>({
const serializedOrder3 = JSON.parse( const serializedOrder3 = JSON.parse(
JSON.stringify( JSON.stringify(
await service.retrieveOrder(created.id, { await service.retrieveOrder(created.id, {
select: ["id", "summary"], select: ["id", "summary", "total"],
}) })
) )
) )
@@ -290,7 +290,7 @@ moduleIntegrationTestRunner<IOrderModuleService>({
const serializedOrder4 = JSON.parse( const serializedOrder4 = JSON.parse(
JSON.stringify( JSON.stringify(
await service.retrieveOrder(created.id, { await service.retrieveOrder(created.id, {
select: ["id", "summary"], select: ["id", "summary", "total"],
}) })
) )
) )
@@ -414,7 +414,7 @@ moduleIntegrationTestRunner<IOrderModuleService>({
}) })
const changedOrder = await service.retrieveOrder(createdOrder.id, { const changedOrder = await service.retrieveOrder(createdOrder.id, {
select: ["total", "items.detail", "summary"], select: ["total", "items.detail", "summary", "total"],
relations: ["items"], relations: ["items"],
}) })
@@ -492,7 +492,7 @@ moduleIntegrationTestRunner<IOrderModuleService>({
}) })
const modified = await service.retrieveOrder(createdOrder.id, { const modified = await service.retrieveOrder(createdOrder.id, {
select: ["total", "items.detail", "summary"], select: ["total", "items.detail", "summary", "total"],
relations: ["items"], relations: ["items"],
}) })
@@ -358,6 +358,7 @@ export default class OrderModuleService
"discount_tax_total", "discount_tax_total",
"original_total", "original_total",
"original_tax_total", "original_tax_total",
"pending_difference",
"item_total", "item_total",
"item_subtotal", "item_subtotal",
"item_tax_total", "item_tax_total",
@@ -370,9 +371,13 @@ export default class OrderModuleService
"original_shipping_tax_total", "original_shipping_tax_total",
"original_shipping_subtotal", "original_shipping_subtotal",
"original_shipping_total", "original_shipping_total",
"original_total",
"original_subtotal",
"original_tax_total",
"credit_line_total", "credit_line_total",
"credit_line_tax_total", "credit_line_tax_total",
"credit_line_subtotal", "credit_line_subtotal",
"refundable_amount",
] ]
const includeTotals = (config?.select ?? []).some((field) => const includeTotals = (config?.select ?? []).some((field) =>
@@ -224,7 +224,7 @@ export class OrderChangeProcessing {
return orderSummary return orderSummary
} }
// Calculate the order summary from a calculated order including taxes // Returns the order summary from a calculated order including taxes
public getSummaryFromOrder(order: OrderDTO): OrderSummaryDTO { public getSummaryFromOrder(order: OrderDTO): OrderSummaryDTO {
const summary_ = this.summary const summary_ = this.summary
const total = order.total const total = order.total
@@ -241,35 +241,6 @@ export class OrderChangeProcessing {
orderSummary.accounting_total = orderSummary.current_order_total orderSummary.accounting_total = orderSummary.current_order_total
orderSummary.pending_difference = MathBN.sub(
orderSummary.current_order_total,
orderSummary.transaction_total
)
// return total becomes pending difference
for (const item of order.items ?? []) {
const item_ = item as any
;[
"return_requested_total",
"return_received_total",
// TODO: revisit this when we settle on which dismissed items need to be refunded
// "return_dismissed_total",
].forEach((returnTotalKey) => {
const returnTotal = item_[returnTotalKey]
if (MathBN.gt(returnTotal, 0)) {
orderSummary.pending_difference = MathBN.sub(
orderSummary.pending_difference,
returnTotal
)
}
})
}
orderSummary.pending_difference = new BigNumber(
orderSummary.pending_difference
)
return orderSummary return orderSummary
} }