diff --git a/.changeset/rare-colts-warn.md b/.changeset/rare-colts-warn.md new file mode 100644 index 0000000000..b026f27934 --- /dev/null +++ b/.changeset/rare-colts-warn.md @@ -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 diff --git a/integration-tests/http/__tests__/exchanges/exchanges.spec.ts b/integration-tests/http/__tests__/exchanges/exchanges.spec.ts index 0e4fbbf1c3..b74a57fb94 100644 --- a/integration-tests/http/__tests__/exchanges/exchanges.spec.ts +++ b/integration-tests/http/__tests__/exchanges/exchanges.spec.ts @@ -22,8 +22,10 @@ medusaIntegrationTestRunner({ let returnReason let inventoryItem let inventoryItemExtra + let inventoryItemExtra2 let location let productExtra + let productExtra2 const shippingProviderId = "manual_test-provider" beforeEach(async () => { @@ -123,6 +125,31 @@ medusaIntegrationTestRunner({ ) ).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 = ( await api.post( "/admin/return-reasons", @@ -269,6 +296,10 @@ medusaIntegrationTestRunner({ await api.get(`/admin/inventory-items?sku=variant-sku`, adminHeaders) ).data.inventory_items[0] + inventoryItemExtra2 = ( + await api.get(`/admin/inventory-items?sku=variant-sku-2`, adminHeaders) + ).data.inventory_items[0] + await api.post( `/admin/inventory-items/${inventoryItemExtra.id}/location-levels`, { @@ -278,6 +309,15 @@ medusaIntegrationTestRunner({ adminHeaders ) + await api.post( + `/admin/inventory-items/${inventoryItemExtra2.id}/location-levels`, + { + location_id: location.id, + stocked_quantity: 2, + }, + adminHeaders + ) + const remoteLink = container.resolve( ContainerRegistrationKeys.REMOTE_LINK ) @@ -323,6 +363,14 @@ medusaIntegrationTestRunner({ 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 @@ -440,6 +488,95 @@ medusaIntegrationTestRunner({ }) 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 () => { let result = await api.post( "/admin/exchanges", diff --git a/integration-tests/http/__tests__/fixtures/order.ts b/integration-tests/http/__tests__/fixtures/order.ts index ff7861543b..a8626cf96c 100644 --- a/integration-tests/http/__tests__/fixtures/order.ts +++ b/integration-tests/http/__tests__/fixtures/order.ts @@ -6,6 +6,7 @@ import { AdminStockLocation, MedusaContainer, } from "@medusajs/types" +import { ContainerRegistrationKeys, Modules } from "@medusajs/utils" import { adminHeaders, generatePublishableKey, @@ -175,6 +176,26 @@ export async function createOrderSeeder({ 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 */ diff --git a/integration-tests/http/__tests__/payment/admin/payment.spec.ts b/integration-tests/http/__tests__/payment/admin/payment.spec.ts index 1c27bcb5c7..b29d9c73d8 100644 --- a/integration-tests/http/__tests__/payment/admin/payment.spec.ts +++ b/integration-tests/http/__tests__/payment/admin/payment.spec.ts @@ -32,7 +32,35 @@ medusaIntegrationTestRunner({ 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 () => { @@ -64,10 +92,6 @@ medusaIntegrationTestRunner({ }) describe("with outstanding amount due to claim", () => { - beforeEach(async () => { - await createClaim({ order }) - }) - it("should capture an authorized payment", async () => { const payment = order.payment_collections[0].payments[0] @@ -189,6 +213,8 @@ medusaIntegrationTestRunner({ adminHeaders ) + await createClaim({ order }) + const refundReason = ( await api.post( `/admin/refund-reasons`, @@ -253,6 +279,8 @@ medusaIntegrationTestRunner({ ) ).data.refund_reason + await createClaim({ order }) + await api.post( `/admin/payments/${payment.id}/refund`, { @@ -311,6 +339,8 @@ medusaIntegrationTestRunner({ adminHeaders ) + await createClaim({ order }) + await api.post( `/admin/payments/${payment.id}/refund`, { amount: 25 }, diff --git a/integration-tests/http/__tests__/returns/returns.spec.ts b/integration-tests/http/__tests__/returns/returns.spec.ts index 78268058d8..d0eba47bcf 100644 --- a/integration-tests/http/__tests__/returns/returns.spec.ts +++ b/integration-tests/http/__tests__/returns/returns.spec.ts @@ -1,9 +1,9 @@ +import { medusaIntegrationTestRunner } from "@medusajs/test-utils" import { ContainerRegistrationKeys, Modules, RuleOperator, } from "@medusajs/utils" -import { medusaIntegrationTestRunner } from "@medusajs/test-utils" import { adminHeaders, createAdminUser, @@ -478,6 +478,16 @@ medusaIntegrationTestRunner({ 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.objectContaining({ id: order.id, diff --git a/integration-tests/modules/__tests__/order/order.spec.ts b/integration-tests/modules/__tests__/order/order.spec.ts index fabde67833..90fefbe851 100644 --- a/integration-tests/modules/__tests__/order/order.spec.ts +++ b/integration-tests/modules/__tests__/order/order.spec.ts @@ -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 { CreateOrderLineItemDTO, IOrderModuleService, OrderDTO, } from "@medusajs/types" +import { + CreateOrderLineItemDTO, + IOrderModuleService, + OrderDTO, +} from "@medusajs/types" 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) @@ -288,6 +298,7 @@ medusaIntegrationTestRunner({ discount_total: 1.1, discount_tax_total: 0.1, original_total: 61, + original_subtotal: 60, original_tax_total: 1, item_total: 50, item_subtotal: 50, @@ -433,6 +444,7 @@ medusaIntegrationTestRunner({ subtotal: 50, total: 50, original_total: 50, + original_subtotal: 50, discount_total: 0, discount_tax_total: 0, discount_subtotal: 0, @@ -490,6 +502,10 @@ medusaIntegrationTestRunner({ precision: 20, value: "0", }, + raw_original_subtotal: { + precision: 20, + value: "50", + }, raw_return_dismissed_total: { precision: 20, value: "0", diff --git a/packages/core/core-flows/src/order/steps/add-order-transaction.ts b/packages/core/core-flows/src/order/steps/add-order-transaction.ts index 6856580c5c..63c18ebb01 100644 --- a/packages/core/core-flows/src/order/steps/add-order-transaction.ts +++ b/packages/core/core-flows/src/order/steps/add-order-transaction.ts @@ -31,24 +31,43 @@ export const addOrderTransactionStep = createStep( return new StepResponse(null) } + const existingQuery: any[] = [] for (const trx of trxsData) { - const existing = await service.listOrderTransactions( - { - order_id: trx.order_id, - reference: trx.reference, - reference_id: trx.reference_id, - }, - { - select: ["id"], - } - ) + existingQuery.push({ + order_id: trx.order_id, + reference: trx.reference, + reference_id: trx.reference_id, + }) + } - if (existing.length) { - return new StepResponse(null) + const existing = await service.listOrderTransactions( + { + $or: existingQuery, + }, + { + select: ["order_id", "reference", "reference_id"], + } + ) + const existingSet = new Set( + 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( (Array.isArray(data) diff --git a/packages/core/core-flows/src/order/utils/order-validation.ts b/packages/core/core-flows/src/order/utils/order-validation.ts index 432222b3c7..787862f399 100644 --- a/packages/core/core-flows/src/order/utils/order-validation.ts +++ b/packages/core/core-flows/src/order/utils/order-validation.ts @@ -9,6 +9,7 @@ import { MedusaError, OrderStatus, arrayDifference, + deepFlatMap, isPresent, } from "@medusajs/framework/utils" @@ -21,6 +22,58 @@ export function throwIfOrderIsCancelled({ order }: { order: OrderDTO }) { } } +export function throwIfManagedItemsNotStockedAtReturnLocation({ + order, + orderReturn, + inputItems, +}: { + order: Pick + orderReturn: Pick + 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({ order, inputItems, diff --git a/packages/core/core-flows/src/order/workflows/claim/claim-request-item-return.ts b/packages/core/core-flows/src/order/workflows/claim/claim-request-item-return.ts index f95c7c3f3d..e2b3fe1334 100644 --- a/packages/core/core-flows/src/order/workflows/claim/claim-request-item-return.ts +++ b/packages/core/core-flows/src/order/workflows/claim/claim-request-item-return.ts @@ -6,7 +6,12 @@ import { OrderWorkflow, ReturnDTO, } from "@medusajs/framework/types" -import { ChangeActionType, OrderChangeStatus } from "@medusajs/framework/utils" +import { + ChangeActionType, + OrderChangeStatus, + deepFlatMap, + isDefined, +} from "@medusajs/framework/utils" import { WorkflowData, WorkflowResponse, @@ -23,6 +28,7 @@ import { updateOrderChangesStep } from "../../steps/update-order-changes" import { throwIfIsCancelled, throwIfItemsDoesNotExistsInOrder, + throwIfManagedItemsNotStockedAtReturnLocation, throwIfOrderChangeIsNotActive, } from "../../utils/order-validation" import { createOrderChangeActionsWorkflow } from "../create-order-change-actions" @@ -105,6 +111,11 @@ export const orderClaimRequestItemReturnValidationStep = createStep( throwIfIsCancelled(orderReturn, "Return") throwIfOrderChangeIsNotActive({ orderChange }) throwIfItemsDoesNotExistsInOrder({ order, inputItems: items }) + throwIfManagedItemsNotStockedAtReturnLocation({ + order, + orderReturn, + inputItems: items, + }) } ) @@ -154,34 +165,23 @@ export const orderClaimRequestItemReturnWorkflow = createWorkflow( }).then(() => { return useRemoteQueryStep({ entry_point: "return", - fields: ["id", "status", "order_id", "canceled_at"], + fields: ["id", "status", "order_id", "location_id", "canceled_at"], variables: { id: orderClaim.return_id }, list: false, throw_if_key_not_found: true, }).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({ 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 }, list: false, throw_if_key_not_found: true, @@ -202,6 +202,51 @@ export const orderClaimRequestItemReturnWorkflow = createWorkflow( 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 }) => { return !!createdReturn?.length }).then(() => { diff --git a/packages/core/core-flows/src/order/workflows/create-or-update-order-payment-collection.ts b/packages/core/core-flows/src/order/workflows/create-or-update-order-payment-collection.ts index 41e4ffe4f6..686f3d6a55 100644 --- a/packages/core/core-flows/src/order/workflows/create-or-update-order-payment-collection.ts +++ b/packages/core/core-flows/src/order/workflows/create-or-update-order-payment-collection.ts @@ -13,8 +13,8 @@ import { } from "@medusajs/framework/workflows-sdk" import { useRemoteQueryStep } from "../../common" import { updatePaymentCollectionStep } from "../../payment-collection" -import { createOrderPaymentCollectionWorkflow } from "./create-order-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. @@ -65,7 +65,7 @@ export const createOrUpdateOrderPaymentCollectionWorkflow = createWorkflow( ) => { const order = useRemoteQueryStep({ entry_point: "order", - fields: ["id", "summary", "currency_code", "region_id"], + fields: ["id", "summary", "total", "currency_code", "region_id"], variables: { id: input.order_id }, throw_if_key_not_found: true, list: false, diff --git a/packages/core/core-flows/src/order/workflows/create-order-credit-lines.ts b/packages/core/core-flows/src/order/workflows/create-order-credit-lines.ts index 43aec6f8f9..ae8d47510a 100644 --- a/packages/core/core-flows/src/order/workflows/create-order-credit-lines.ts +++ b/packages/core/core-flows/src/order/workflows/create-order-credit-lines.ts @@ -90,7 +90,7 @@ export const createOrderCreditLinesWorkflow = createWorkflow( ) => { const orderQuery = useQueryGraphStep({ entity: "orders", - fields: ["id", "status", "summary"], + fields: ["id", "status", "summary", "total"], filters: { id: input.id }, options: { throwIfKeyNotFound: true }, }).config({ name: "get-order" }) diff --git a/packages/core/core-flows/src/order/workflows/create-order-payment-collection.ts b/packages/core/core-flows/src/order/workflows/create-order-payment-collection.ts index c522bf551f..0b44ee649d 100644 --- a/packages/core/core-flows/src/order/workflows/create-order-payment-collection.ts +++ b/packages/core/core-flows/src/order/workflows/create-order-payment-collection.ts @@ -5,8 +5,8 @@ import { createWorkflow, transform, } from "@medusajs/framework/workflows-sdk" -import { createRemoteLinkStep, useRemoteQueryStep } from "../../common" import { createPaymentCollectionsStep } from "../../cart" +import { createRemoteLinkStep, useRemoteQueryStep } from "../../common" /** * The details of the payment collection to create. @@ -27,10 +27,10 @@ export const createOrderPaymentCollectionWorkflowId = /** * This workflow creates a payment collection for an order. It's used by the * [Create Payment Collection Admin API Route](https://docs.medusajs.com/api/admin#payment-collections_postpaymentcollections). - * + * * You can use this workflow within your customizations or your own custom workflows, allowing you to wrap custom logic around * creating a payment collection for an order. - * + * * @example * const { result } = await createOrderPaymentCollectionWorkflow(container) * .run({ @@ -39,19 +39,17 @@ export const createOrderPaymentCollectionWorkflowId = * amount: 10, * } * }) - * + * * @summary - * + * * Create a payment collection for an order. */ export const createOrderPaymentCollectionWorkflow = createWorkflow( createOrderPaymentCollectionWorkflowId, - ( - input: WorkflowData - ) => { + (input: WorkflowData) => { const order = useRemoteQueryStep({ entry_point: "order", - fields: ["id", "summary", "currency_code", "region_id"], + fields: ["id", "summary", "total", "currency_code", "region_id"], variables: { id: input.order_id }, throw_if_key_not_found: true, list: false, diff --git a/packages/core/core-flows/src/order/workflows/exchange/exchange-request-item-return.ts b/packages/core/core-flows/src/order/workflows/exchange/exchange-request-item-return.ts index e2f449c23a..9a9bb4696c 100644 --- a/packages/core/core-flows/src/order/workflows/exchange/exchange-request-item-return.ts +++ b/packages/core/core-flows/src/order/workflows/exchange/exchange-request-item-return.ts @@ -6,14 +6,19 @@ import { OrderWorkflow, ReturnDTO, } from "@medusajs/framework/types" -import { ChangeActionType, OrderChangeStatus } from "@medusajs/framework/utils" import { - WorkflowData, - WorkflowResponse, + ChangeActionType, + deepFlatMap, + isDefined, + OrderChangeStatus, +} from "@medusajs/framework/utils" +import { createStep, createWorkflow, transform, when, + WorkflowData, + WorkflowResponse, } from "@medusajs/framework/workflows-sdk" import { useRemoteQueryStep } from "../../../common" import { updateOrderExchangesStep } from "../../steps/exchange/update-order-exchanges" @@ -23,6 +28,7 @@ import { updateOrderChangesStep } from "../../steps/update-order-changes" import { throwIfIsCancelled, throwIfItemsDoesNotExistsInOrder, + throwIfManagedItemsNotStockedAtReturnLocation, throwIfOrderChangeIsNotActive, } from "../../utils/order-validation" import { createOrderChangeActionsWorkflow } from "../create-order-change-actions" @@ -106,6 +112,11 @@ export const exchangeRequestItemReturnValidationStep = createStep( throwIfIsCancelled(orderReturn, "Return") throwIfOrderChangeIsNotActive({ orderChange }) throwIfItemsDoesNotExistsInOrder({ order, inputItems: items }) + throwIfManagedItemsNotStockedAtReturnLocation({ + order, + orderReturn, + inputItems: items, + }) } ) @@ -144,7 +155,7 @@ export const orderExchangeRequestItemReturnWorkflow = createWorkflow( ): WorkflowResponse { const orderExchange = useRemoteQueryStep({ 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 }, list: false, throw_if_key_not_found: true, @@ -162,27 +173,16 @@ export const orderExchangeRequestItemReturnWorkflow = createWorkflow( }).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({ 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 }, list: false, throw_if_key_not_found: true, @@ -204,6 +204,51 @@ export const orderExchangeRequestItemReturnWorkflow = createWorkflow( 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 }) => { return !!createdReturn?.length }).then(() => { diff --git a/packages/core/core-flows/src/order/workflows/mark-order-fulfillment-as-delivered.ts b/packages/core/core-flows/src/order/workflows/mark-order-fulfillment-as-delivered.ts index d4d38932e3..e787843acc 100644 --- a/packages/core/core-flows/src/order/workflows/mark-order-fulfillment-as-delivered.ts +++ b/packages/core/core-flows/src/order/workflows/mark-order-fulfillment-as-delivered.ts @@ -234,6 +234,7 @@ export const markOrderFulfillmentAsDeliveredWorkflow = createWorkflow( fields: [ "id", "summary", + "total", "currency_code", "region_id", "fulfillments.id", diff --git a/packages/core/core-flows/src/order/workflows/payments/create-order-refund-credit-lines.ts b/packages/core/core-flows/src/order/workflows/payments/create-order-refund-credit-lines.ts index ee276352d2..beac0e7a8f 100644 --- a/packages/core/core-flows/src/order/workflows/payments/create-order-refund-credit-lines.ts +++ b/packages/core/core-flows/src/order/workflows/payments/create-order-refund-credit-lines.ts @@ -42,7 +42,7 @@ export const createOrderRefundCreditLinesWorkflow = createWorkflow( ) { const orderQuery = useQueryGraphStep({ entity: "orders", - fields: ["id", "status", "summary", "payment_collections.id"], + fields: ["id", "status", "summary", "total", "payment_collections.id"], filters: { id: input.order_id }, options: { throwIfKeyNotFound: true }, }).config({ name: "get-order" }) diff --git a/packages/core/core-flows/src/order/workflows/payments/refund-captured-payments.ts b/packages/core/core-flows/src/order/workflows/payments/refund-captured-payments.ts index 93b2af5acc..e65b03b565 100644 --- a/packages/core/core-flows/src/order/workflows/payments/refund-captured-payments.ts +++ b/packages/core/core-flows/src/order/workflows/payments/refund-captured-payments.ts @@ -29,6 +29,7 @@ export const refundCapturedPaymentsWorkflow = createWorkflow( "id", "status", "summary", + "total", "payment_collections.payments.id", "payment_collections.payments.amount", "payment_collections.payments.refunds.id", diff --git a/packages/core/core-flows/src/order/workflows/return/confirm-receive-return-request.ts b/packages/core/core-flows/src/order/workflows/return/confirm-receive-return-request.ts index 7eb51b58d3..8f76a31bde 100644 --- a/packages/core/core-flows/src/order/workflows/return/confirm-receive-return-request.ts +++ b/packages/core/core-flows/src/order/workflows/return/confirm-receive-return-request.ts @@ -34,6 +34,7 @@ import { throwIfIsCancelled, throwIfOrderChangeIsNotActive, } from "../../utils/order-validation" +import { createOrUpdateOrderPaymentCollectionWorkflow } from "../create-or-update-order-payment-collection" /** * The data to validate that a return receival can be confirmed. @@ -56,14 +57,14 @@ export type ConfirmReceiveReturnValidationStepInput = { /** * This step validates that a return receival can be confirmed. * If the order or return is canceled, or the order change is not active, the step will throw an error. - * + * * :::note - * + * * You can retrieve an order, return, and order change details using [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query), * or [useQueryGraphStep](https://docs.medusajs.com/resources/references/medusa-workflows/steps/useQueryGraphStep). - * + * * ::: - * + * * @example * const data = confirmReceiveReturnValidationStep({ * order: { @@ -182,10 +183,10 @@ export const confirmReturnReceiveWorkflowId = "confirm-return-receive" /** * This workflow confirms a return receival request. It's used by the * [Confirm Return Receival Admin API Route](https://docs.medusajs.com/api/admin#returns_postreturnsidreceiveconfirm). - * + * * You can use this workflow within your customizations or your own custom workflows, allowing you * to confirm a return receival in your custom flow. - * + * * @example * const { result } = await confirmReturnReceiveWorkflow(container) * .run({ @@ -193,9 +194,9 @@ export const confirmReturnReceiveWorkflowId = "confirm-return-receive" * return_id: "return_123", * } * }) - * + * * @summary - * + * * Confirm a return receival request. */ export const confirmReturnReceiveWorkflow = createWorkflow( @@ -363,7 +364,15 @@ export const confirmReturnReceiveWorkflow = createWorkflow( orderId: order.id, confirmed_by: input.confirmed_by, }), - adjustInventoryLevelsStep(inventoryAdjustment), + adjustInventoryLevelsStep(inventoryAdjustment) + ) + + parallelize( + createOrUpdateOrderPaymentCollectionWorkflow.runAsStep({ + input: { + order_id: order.id, + }, + }), emitEventStep({ eventName: OrderWorkflowEvents.RETURN_RECEIVED, data: { diff --git a/packages/core/core-flows/src/order/workflows/return/create-complete-return.ts b/packages/core/core-flows/src/order/workflows/return/create-complete-return.ts index ee4ef89172..56d63d105f 100644 --- a/packages/core/core-flows/src/order/workflows/return/create-complete-return.ts +++ b/packages/core/core-flows/src/order/workflows/return/create-complete-return.ts @@ -1,13 +1,13 @@ import { + AdditionalData, BigNumberInput, CreateOrderShippingMethodDTO, FulfillmentWorkflow, OrderDTO, - ReturnDTO, OrderWorkflow, + ReturnDTO, ShippingOptionDTO, WithCalculatedPrice, - AdditionalData, } from "@medusajs/framework/types" import { MathBN, @@ -25,6 +25,7 @@ import { parallelize, transform, } from "@medusajs/framework/workflows-sdk" +import { pricingContextResult } from "../../../cart/utils/schemas" import { createRemoteLinkStep, emitEventStep, @@ -38,7 +39,6 @@ import { throwIfOrderIsCancelled, } from "../../utils/order-validation" import { validateReturnReasons } from "../../utils/validate-return-reason" -import { pricingContextResult } from "../../../cart/utils/schemas" function prepareShippingMethodData({ orderId, @@ -311,11 +311,11 @@ export const createAndCompleteReturnOrderWorkflowId = * @summary * * Create and complete a return for an order. - * + * * @property hooks.setPricingContext - This hook is executed before the return's shipping method is created. You can consume this hook to return any custom context useful for the prices retrieval of the shipping method's option. - * + * * For example, assuming you have the following custom pricing rule: - * + * * ```json * { * "attribute": "location_id", @@ -323,13 +323,13 @@ export const createAndCompleteReturnOrderWorkflowId = * "value": "sloc_123", * } * ``` - * + * * You can consume the `setPricingContext` hook to add the `location_id` context to the prices calculation: - * + * * ```ts * import { createAndCompleteReturnOrderWorkflow } from "@medusajs/medusa/core-flows"; * import { StepResponse } from "@medusajs/workflows-sdk"; - * + * * createAndCompleteReturnOrderWorkflow.hooks.setPricingContext(( * { order, additional_data }, { container } * ) => { @@ -338,13 +338,13 @@ export const createAndCompleteReturnOrderWorkflowId = * }); * }); * ``` - * + * * The price of the shipping method's option will now be retrieved using the context you return. - * + * * :::note - * + * * Learn more about prices calculation context in the [Prices Calculation](https://docs.medusajs.com/resources/commerce-modules/pricing/price-calculation) documentation. - * + * * ::: */ export const createAndCompleteReturnOrderWorkflow = createWorkflow( @@ -423,6 +423,7 @@ export const createAndCompleteReturnOrderWorkflow = createWorkflow( const returnCreated = createCompleteReturnStep({ order_id: input.order_id, + location_id: input.location_id, items: input.items, shipping_method: shippingMethodData, created_by: input.created_by, diff --git a/packages/core/core-flows/src/payment/workflows/refund-payment.ts b/packages/core/core-flows/src/payment/workflows/refund-payment.ts index 9d8b1c9bee..89c406a92c 100644 --- a/packages/core/core-flows/src/payment/workflows/refund-payment.ts +++ b/packages/core/core-flows/src/payment/workflows/refund-payment.ts @@ -146,31 +146,33 @@ export const refundPaymentWorkflow = createWorkflow( const order = useRemoteQueryStep({ entry_point: "order", - fields: ["id", "summary", "currency_code", "region_id"], + fields: ["id", "summary", "total", "currency_code", "region_id"], variables: { id: orderPaymentCollection.order.id }, throw_if_key_not_found: true, list: false, }).config({ name: "order" }) validateRefundStep({ order, payment, amount: input.amount }) - refundPaymentStep(input) + const refundPayment = refundPaymentStep(input) when({ orderPaymentCollection }, ({ orderPaymentCollection }) => { return !!orderPaymentCollection?.order?.id }).then(() => { const orderTransactionData = transform( - { input, payment, orderPaymentCollection }, - ({ input, payment, orderPaymentCollection }) => { - return { - order_id: orderPaymentCollection.order.id, - amount: MathBN.mult( - input.amount ?? payment.raw_amount ?? payment.amount, - -1 - ), - currency_code: payment.currency_code ?? order.currency_code, - reference_id: payment.id, - reference: "refund", - } + { input, refundPayment, orderPaymentCollection, order }, + ({ input, refundPayment, orderPaymentCollection, order }) => { + return refundPayment.refunds?.map((refund) => { + return { + order_id: orderPaymentCollection.order.id, + amount: MathBN.mult( + input.amount ?? refund.raw_amount ?? refund.amount, + -1 + ), + currency_code: refundPayment.currency_code ?? order.currency_code, + reference_id: refund.id, + reference: "refund", + } + }) } ) diff --git a/packages/core/types/src/order/mutations.ts b/packages/core/types/src/order/mutations.ts index e5eeee63db..0e795a630e 100644 --- a/packages/core/types/src/order/mutations.ts +++ b/packages/core/types/src/order/mutations.ts @@ -1578,7 +1578,7 @@ export interface CreateOrderReturnDTO extends BaseOrderBundledActionsDTO { /** * The ID of the location to return the items to. */ - location_id?: string + location_id?: string | null /** * The items of the return. diff --git a/packages/core/types/src/workflow/order/request-item-return.ts b/packages/core/types/src/workflow/order/request-item-return.ts index 256466937b..3626d50886 100644 --- a/packages/core/types/src/workflow/order/request-item-return.ts +++ b/packages/core/types/src/workflow/order/request-item-return.ts @@ -73,6 +73,10 @@ export interface OrderExchangeRequestItemReturnWorkflowInput { * The ID of the return that's associated with the exchange. */ 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. */ @@ -95,6 +99,10 @@ export interface OrderClaimRequestItemReturnWorkflowInput { * The ID of the return that's associated with the claim. */ 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. */ @@ -124,11 +132,11 @@ export interface DeleteRequestItemReturnWorkflowInput { /** * The details of the received item to be removed. - * + * * @property return_id - The ID of the return to remove the item from. * @property action_id - The ID of the action associated with the item to remove. - * Every item has an `actions` property, whose value is an array of actions. - * You can find an action with the name `RECEIVE_RETURN_ITEM` using its `action` property, + * Every item has an `actions` property, whose value is an array of actions. + * You can find an action with the name `RECEIVE_RETURN_ITEM` using its `action` property, * and use the value of its `id` property. */ export interface DeleteRequestItemReceiveReturnWorkflowInput diff --git a/packages/core/utils/src/totals/__tests__/totals.ts b/packages/core/utils/src/totals/__tests__/totals.ts index 9d23d0fcb2..f9dc364efd 100644 --- a/packages/core/utils/src/totals/__tests__/totals.ts +++ b/packages/core/utils/src/totals/__tests__/totals.ts @@ -41,6 +41,7 @@ describe("Total calculation", function () { subtotal: 60, total: 66, original_total: 66, + original_subtotal: 60, discount_total: 0, discount_subtotal: 0, discount_tax_total: 0, @@ -60,6 +61,7 @@ describe("Total calculation", function () { subtotal: 5, total: 7.5, original_total: 7.5, + original_subtotal: 5, discount_total: 0, discount_subtotal: 0, discount_tax_total: 0, @@ -78,6 +80,7 @@ describe("Total calculation", function () { item_tax_total: 8.5, item_discount_total: 0, original_total: 73.5, + original_subtotal: 65, original_tax_total: 8.5, original_item_subtotal: 65, original_item_total: 73.5, @@ -132,6 +135,7 @@ describe("Total calculation", function () { subtotal: 100, total: 99, original_total: 110, + original_subtotal: 100, discount_total: 11, discount_subtotal: 10, discount_tax_total: 1, @@ -146,6 +150,7 @@ describe("Total calculation", function () { discount_subtotal: 10, discount_tax_total: 1, original_total: 110, + original_subtotal: 100, original_tax_total: 10, item_total: 99, item_subtotal: 100, @@ -253,6 +258,7 @@ describe("Total calculation", function () { subtotal: 90, total: 89.1, original_total: 99, + original_subtotal: 90, discount_total: 9.9, discount_subtotal: 9, discount_tax_total: 0.9, @@ -280,6 +286,7 @@ describe("Total calculation", function () { subtotal: 9, total: 6.6, original_total: 9.9, + original_subtotal: 9, discount_total: 3.3, discount_subtotal: 3, discount_tax_total: 0.3, @@ -308,6 +315,7 @@ describe("Total calculation", function () { subtotal: 90, total: 89.1, original_total: 99, + original_subtotal: 90, discount_total: 9.9, discount_subtotal: 9, discount_tax_total: 0.9, @@ -334,6 +342,7 @@ describe("Total calculation", function () { subtotal: 9, total: 6.6, original_total: 9.9, + original_subtotal: 9, discount_total: 3.3, discount_subtotal: 3, discount_tax_total: 0.3, @@ -348,6 +357,7 @@ describe("Total calculation", function () { discount_subtotal: 24, discount_tax_total: 2.4, original_total: 217.8, + original_subtotal: 198, original_tax_total: 19.8, item_total: 95.7, item_subtotal: 99, @@ -431,6 +441,7 @@ describe("Total calculation", function () { subtotal: 90.9090909090909, total: 100, original_total: 100, + original_subtotal: 90.9090909090909, discount_total: 0, discount_tax_total: 0, tax_total: 9.090909090909092, @@ -444,6 +455,7 @@ describe("Total calculation", function () { discount_total: 0, discount_tax_total: 0, original_total: 100, + original_subtotal: 90.9090909090909, original_tax_total: 9.090909090909092, item_total: 100, item_subtotal: 90.9090909090909, @@ -478,6 +490,7 @@ describe("Total calculation", function () { discount_tax_total: 0, tax_total: 10, original_tax_total: 10, + original_subtotal: 100, }, ], total: 110, @@ -487,6 +500,7 @@ describe("Total calculation", function () { discount_total: 0, discount_tax_total: 0, original_total: 110, + original_subtotal: 100, original_tax_total: 10, item_total: 110, item_subtotal: 100, @@ -516,6 +530,7 @@ describe("Total calculation", function () { ], subtotal: 90.9090909090909, total: 100, + original_subtotal: 90.9090909090909, original_total: 100, discount_total: 0, discount_tax_total: 0, @@ -537,6 +552,7 @@ describe("Total calculation", function () { subtotal: 100, total: 110, original_total: 110, + original_subtotal: 100, discount_total: 0, discount_tax_total: 0, tax_total: 10, @@ -550,6 +566,7 @@ describe("Total calculation", function () { discount_total: 0, discount_tax_total: 0, original_total: 210, + original_subtotal: 190.9090909090909, original_tax_total: 19.09090909090909, item_total: 210, item_subtotal: 190.9090909090909, @@ -606,6 +623,7 @@ describe("Total calculation", function () { is_tax_inclusive: true, original_total: 120, + original_subtotal: 100, original_tax_total: 20, discount_subtotal: 8.333333333333334, @@ -637,6 +655,7 @@ describe("Total calculation", function () { original_item_total: 120, original_tax_total: 20, original_total: 120, + original_subtotal: 100, discount_subtotal: 8.333333333333334, discount_tax_total: 1.6666666666666667, @@ -712,6 +731,7 @@ describe("Total calculation", function () { subtotal: 100, total: 88, original_total: 110, + original_subtotal: 100, discount_total: 22, discount_subtotal: 20, discount_tax_total: 2, @@ -739,6 +759,7 @@ describe("Total calculation", function () { subtotal: 25, total: 25.3, original_total: 27.5, + original_subtotal: 25, discount_total: 2.2, discount_subtotal: 2, discount_tax_total: 0.2, @@ -753,6 +774,7 @@ describe("Total calculation", function () { discount_subtotal: 22, discount_tax_total: 2.2, original_total: 137.5, + original_subtotal: 125, original_tax_total: 12.5, item_total: 88, item_subtotal: 100, @@ -836,8 +858,8 @@ describe("Total calculation", function () { tax_lines: [ { rate: 10, - total: 8, - subtotal: 10, + total: 0, + subtotal: 0, }, ], adjustments: [ @@ -847,14 +869,15 @@ describe("Total calculation", function () { total: 22, }, ], - subtotal: 100, - total: 88, - original_total: 110, - discount_total: 22, - discount_subtotal: 20, - discount_tax_total: 2, - tax_total: 8, - original_tax_total: 10, + subtotal: 0, + total: 0, + original_total: 0, + original_subtotal: 0, + discount_total: 0, + discount_subtotal: 0, + discount_tax_total: 0, + tax_total: 0, + original_tax_total: 0, refundable_total_per_unit: 0, refundable_total: 0, fulfilled_total: 88, @@ -865,21 +888,22 @@ describe("Total calculation", function () { write_off_total: 44, }, ], - total: 48, - subtotal: 100, - tax_total: 8, - discount_total: 22, - discount_subtotal: 20, - discount_tax_total: 2, - original_total: 110, - original_tax_total: 10, - item_total: 88, - item_subtotal: 100, - item_tax_total: 8, - item_discount_total: 22, - original_item_total: 110, - original_item_subtotal: 100, - original_item_tax_total: 10, + total: -40, + subtotal: 0, + tax_total: 0, + discount_total: 0, + discount_subtotal: 0, + discount_tax_total: 0, + original_total: 0, + original_subtotal: 0, + original_tax_total: 0, + item_total: 0, + item_subtotal: 0, + item_tax_total: 0, + item_discount_total: 0, + original_item_total: 0, + original_item_subtotal: 0, + original_item_tax_total: 0, fulfilled_total: 88, shipped_total: 88, return_requested_total: 0, @@ -923,6 +947,7 @@ describe("Total calculation", function () { { rate: 19, subtotal: 22.61, + total: 0, }, ], adjustments: [ @@ -935,6 +960,7 @@ describe("Total calculation", function () { subtotal: 119, total: 0, original_total: 141.61, + original_subtotal: 119, discount_total: 141.61, discount_subtotal: 119, discount_tax_total: 22.61, @@ -949,6 +975,7 @@ describe("Total calculation", function () { discount_subtotal: 119, discount_tax_total: 22.61, original_total: 141.61, + original_subtotal: 119, original_tax_total: 22.61, item_total: 0, item_subtotal: 119, @@ -1026,6 +1053,7 @@ describe("Total calculation", function () { discount_total: 0, is_tax_inclusive: true, original_tax_total: 19, + original_subtotal: 100, original_total: 119, quantity: 1, subtotal: 100, @@ -1045,6 +1073,7 @@ describe("Total calculation", function () { original_item_tax_total: 19, original_item_total: 119, original_tax_total: 19, + original_subtotal: 100, original_total: 119, subtotal: 100, tax_total: 19, @@ -1069,6 +1098,7 @@ describe("Total calculation", function () { discount_total: 0, is_tax_inclusive: false, original_tax_total: 22.61, + original_subtotal: 119, original_total: 141.61, quantity: 1, subtotal: 119, @@ -1088,6 +1118,7 @@ describe("Total calculation", function () { original_item_tax_total: 22.61, original_item_total: 141.61, original_tax_total: 22.61, + original_subtotal: 119, original_total: 141.61, subtotal: 119, tax_total: 22.61, @@ -1113,6 +1144,7 @@ describe("Total calculation", function () { is_tax_inclusive: true, original_tax_total: 19, original_total: 119, + original_subtotal: 100, quantity: 1, subtotal: 100, tax_lines: [ @@ -1132,6 +1164,7 @@ describe("Total calculation", function () { discount_total: 0, is_tax_inclusive: false, original_tax_total: 22.61, + original_subtotal: 119, original_total: 141.61, quantity: 1, subtotal: 119, @@ -1150,6 +1183,7 @@ describe("Total calculation", function () { original_item_subtotal: 219, original_item_tax_total: 41.61, original_item_total: 260.61, + original_subtotal: 219, original_tax_total: 41.61, original_total: 260.61, subtotal: 219, @@ -1206,6 +1240,7 @@ describe("Total calculation", function () { discount_total: 119, is_tax_inclusive: true, original_tax_total: 19, + original_subtotal: 100, original_total: 119, quantity: 1, subtotal: 100, @@ -1213,6 +1248,7 @@ describe("Total calculation", function () { { rate: 19, subtotal: 19, + total: 0, }, ], tax_total: 0, @@ -1224,6 +1260,7 @@ describe("Total calculation", function () { original_item_tax_total: 19, original_item_total: 119, original_tax_total: 19, + original_subtotal: 100, original_total: 119, subtotal: 100, tax_total: 0, diff --git a/packages/core/utils/src/totals/adjustment/index.ts b/packages/core/utils/src/totals/adjustment/index.ts index f9e0440264..89af9d2813 100644 --- a/packages/core/utils/src/totals/adjustment/index.ts +++ b/packages/core/utils/src/totals/adjustment/index.ts @@ -4,9 +4,11 @@ import { BigNumber } from "../big-number" import { MathBN } from "../math" export function calculateAdjustmentTotal({ + item, adjustments, taxRate, }: { + item?: { quantity: BigNumberInput } adjustments: Pick[] taxRate?: BigNumberInput }) { @@ -40,9 +42,24 @@ export function calculateAdjustmentTotal({ 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 { adjustmentsTotal, adjustmentsSubtotal, adjustmentsTaxTotal, + adjustmentPerItem, + adjustmentSubtotalPerItem, + adjustmentTaxTotalPerItem, } } diff --git a/packages/core/utils/src/totals/cart/index.ts b/packages/core/utils/src/totals/cart/index.ts index e44969584e..5c3e65b2e4 100644 --- a/packages/core/utils/src/totals/cart/index.ts +++ b/packages/core/utils/src/totals/cart/index.ts @@ -101,7 +101,8 @@ export function decorateCartTotals( let shippingDiscountTotal = MathBN.convert(0) 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 itemTotal = MathBN.convert(itemTotals.total) @@ -208,6 +209,10 @@ export function decorateCartTotals( // TODO: Gift Card calculations const originalTotal = MathBN.add(itemsOriginalTotal, shippingOriginalTotal) + const originalSubtotal = MathBN.add( + itemsOriginalSubtotal, + shippingOriginalSubtotal + ) // TODO: subtract (cart.gift_card_total + cart.gift_card_tax_total) const tempTotal = MathBN.add(subtotal, taxTotal) @@ -231,6 +236,7 @@ export function decorateCartTotals( // cart.gift_card_tax_total = giftCardTotal.tax_total || 0 cart.original_total = new BigNumber(originalTotal) + cart.original_subtotal = new BigNumber(originalSubtotal) cart.original_tax_total = new BigNumber(originalTaxTotal) // cart.original_gift_card_total = @@ -264,5 +270,22 @@ export function decorateCartTotals( 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 } diff --git a/packages/core/utils/src/totals/line-item/index.ts b/packages/core/utils/src/totals/line-item/index.ts index 2e1f2ad744..efa7f0bc6e 100644 --- a/packages/core/utils/src/totals/line-item/index.ts +++ b/packages/core/utils/src/totals/line-item/index.ts @@ -33,6 +33,7 @@ export interface GetItemTotalOutput { unit_price: BigNumber subtotal: BigNumber + original_subtotal: BigNumber total: BigNumber original_total: BigNumber @@ -76,8 +77,7 @@ export function getLineItemsTotals( function setRefundableTotal( item: GetItemTotalInput, discountsTotal: BigNumberInput, - totals: GetItemTotalOutput, - context: GetLineItemsTotalsContext + totals: GetItemTotalOutput ) { const itemDetail = item.detail! const totalReturnedQuantity = MathBN.sum( @@ -127,54 +127,107 @@ function getLineItemTotals( ? MathBN.div(totalItemPrice, MathBN.add(1, sumTaxRate)) : totalItemPrice + // Proportional discounts to current quantity and compute taxes on the current net amount const { adjustmentsTotal: discountsTotal, - adjustmentsSubtotal: discountsSubtotal, - adjustmentsTaxTotal: discountTaxTotal, + adjustmentsSubtotal: discountsSubtotalFull, + adjustmentSubtotalPerItem, } = calculateAdjustmentTotal({ + item, adjustments: item.adjustments || [], 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({ taxLines: item.tax_lines || [], - taxableAmount: MathBN.sub(subtotal, discountsSubtotal), + taxableAmount: MathBN.sub(currentSubtotal, currentDiscountsSubtotal), setTotalField: "total", }) const originalTaxTotal = calculateTaxTotal({ taxLines: item.tax_lines || [], - taxableAmount: subtotal, + taxableAmount: currentSubtotal, 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 = { quantity: item.quantity, unit_price: item.unit_price, - subtotal: new BigNumber(subtotal), + subtotal: new BigNumber(currentSubtotal), + 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( - isTaxInclusive ? totalItemPrice : MathBN.add(subtotal, originalTaxTotal) + isTaxInclusive + ? currentTotalItemPrice + : MathBN.add(currentSubtotal, originalTaxTotal) ), - discount_total: new BigNumber(discountsTotal), - discount_subtotal: new BigNumber(discountsSubtotal), - discount_tax_total: new BigNumber(discountTaxTotal), + // Discount values prorated to the current quantity + discount_subtotal: new BigNumber(currentDiscountsSubtotal), + 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), original_tax_total: new BigNumber(originalTaxTotal), } - if (isDefined(item.detail?.return_requested_quantity)) { - setRefundableTotal(item, discountsTotal, totals, context) + if ( + 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 totalPerUnit = MathBN.div(totals.total, div) + const totalPerUnit = MathBN.div(fullNetTotal, div) const optionalFields = { ...(context.extraQuantityFields ?? {}), @@ -183,10 +236,9 @@ function getLineItemTotals( for (const field in optionalFields) { const totalField = optionalFields[field] - let target = item[totalField] - if (field.includes(".")) { - target = pickValueFromObject(field, item) - } + let target = field.includes(".") + ? pickValueFromObject(field, item) + : item[field] if (!isDefined(target)) { continue diff --git a/packages/core/utils/src/totals/shipping-method/index.ts b/packages/core/utils/src/totals/shipping-method/index.ts index 1c7c68adce..9eb1d0019a 100644 --- a/packages/core/utils/src/totals/shipping-method/index.ts +++ b/packages/core/utils/src/totals/shipping-method/index.ts @@ -20,6 +20,7 @@ export interface GetShippingMethodTotalOutput { amount: BigNumber subtotal: BigNumber + original_subtotal: BigNumber total: BigNumber original_total: BigNumber @@ -96,6 +97,14 @@ export function getShippingMethodTotals( total: new BigNumber( MathBN.sum(MathBN.sub(subtotal, discountsSubtotal), taxTotal) ), + original_subtotal: new BigNumber( + MathBN.sub( + isTaxInclusive + ? shippingMethodAmount + : MathBN.add(subtotal, originalTaxTotal), + originalTaxTotal + ) + ), original_total: new BigNumber( isTaxInclusive ? shippingMethodAmount diff --git a/packages/core/utils/src/totals/tax/index.ts b/packages/core/utils/src/totals/tax/index.ts index de62883e20..a9ceb2c656 100644 --- a/packages/core/utils/src/totals/tax/index.ts +++ b/packages/core/utils/src/totals/tax/index.ts @@ -11,10 +11,6 @@ export function calculateTaxTotal({ taxableAmount: BigNumberInput setTotalField?: string }) { - if (MathBN.lte(taxableAmount, 0)) { - return MathBN.convert(0) - } - let taxTotal = MathBN.convert(0) for (const taxLine of taxLines) { diff --git a/packages/medusa/src/api/admin/claims/validators.ts b/packages/medusa/src/api/admin/claims/validators.ts index af111778f7..d00fb7b15c 100644 --- a/packages/medusa/src/api/admin/claims/validators.ts +++ b/packages/medusa/src/api/admin/claims/validators.ts @@ -128,6 +128,7 @@ export type AdminPostClaimsAddItemsReqSchemaType = z.infer< > export const AdminPostClaimsRequestReturnItemsReqSchema = z.object({ + location_id: z.string().optional(), items: z.array( z.object({ id: z.string(), diff --git a/packages/medusa/src/api/admin/draft-orders/query-config.ts b/packages/medusa/src/api/admin/draft-orders/query-config.ts index 64579ae5d6..e1ccf6fe8f 100644 --- a/packages/medusa/src/api/admin/draft-orders/query-config.ts +++ b/packages/medusa/src/api/admin/draft-orders/query-config.ts @@ -7,6 +7,7 @@ export const defaultAdminListOrderFields = [ "region_id", "*items", "summary", + "total", "metadata", "created_at", "updated_at", @@ -31,6 +32,7 @@ export const defaultAdminOrderFields = [ "*shipping_methods.tax_lines", "*shipping_methods.adjustments", "summary", + "total", "metadata", "created_at", "updated_at", diff --git a/packages/medusa/src/api/admin/exchanges/validators.ts b/packages/medusa/src/api/admin/exchanges/validators.ts index c7e41ecd41..c7fa6bed83 100644 --- a/packages/medusa/src/api/admin/exchanges/validators.ts +++ b/packages/medusa/src/api/admin/exchanges/validators.ts @@ -127,6 +127,7 @@ export type AdminPostExchangesAddItemsReqSchemaType = z.infer< > export const AdminPostExchangesReturnRequestItemsReqSchema = z.object({ + location_id: z.string().optional(), items: z.array( z.object({ id: z.string(), diff --git a/packages/medusa/src/api/admin/orders/query-config.ts b/packages/medusa/src/api/admin/orders/query-config.ts index f94d64483e..88cc00a161 100644 --- a/packages/medusa/src/api/admin/orders/query-config.ts +++ b/packages/medusa/src/api/admin/orders/query-config.ts @@ -4,6 +4,7 @@ export const defaultAdminOrderFields = [ "status", "version", "summary", + "total", "metadata", "created_at", "updated_at", @@ -19,6 +20,7 @@ export const defaultAdminRetrieveOrderFields = [ "discount_total", "discount_tax_total", "original_total", + "original_subtotal", "original_tax_total", "item_total", "item_subtotal", diff --git a/packages/medusa/src/api/store/orders/query-config.ts b/packages/medusa/src/api/store/orders/query-config.ts index 72a2745e01..e495ecef08 100644 --- a/packages/medusa/src/api/store/orders/query-config.ts +++ b/packages/medusa/src/api/store/orders/query-config.ts @@ -27,6 +27,7 @@ export const defaultStoreRetrieveOrderFields = [ "discount_subtotal", "discount_tax_total", "original_total", + "original_subtotal", "original_tax_total", "item_total", "item_subtotal", diff --git a/packages/modules/cart/integration-tests/__tests__/services/cart-module/index.spec.ts b/packages/modules/cart/integration-tests/__tests__/services/cart-module/index.spec.ts index 3be9bdd98d..78ad5eea1d 100644 --- a/packages/modules/cart/integration-tests/__tests__/services/cart-module/index.spec.ts +++ b/packages/modules/cart/integration-tests/__tests__/services/cart-module/index.spec.ts @@ -2934,6 +2934,7 @@ moduleIntegrationTestRunner({ subtotal: 100, total: 0, original_total: 100, + original_subtotal: 100, discount_total: 100, discount_subtotal: 100, discount_tax_total: 0, @@ -2951,6 +2952,10 @@ moduleIntegrationTestRunner({ value: "100", precision: 20, }, + raw_original_subtotal: { + value: "100", + precision: 20, + }, raw_discount_total: { value: "100", precision: 20, @@ -3042,6 +3047,7 @@ moduleIntegrationTestRunner({ subtotal: 400, total: 200, original_total: 400, + original_subtotal: 400, discount_total: 200, discount_subtotal: 200, discount_tax_total: 0, @@ -3059,6 +3065,10 @@ moduleIntegrationTestRunner({ value: "400", precision: 20, }, + raw_original_subtotal: { + value: "400", + precision: 20, + }, raw_discount_total: { value: "200", precision: 20, @@ -3104,6 +3114,7 @@ moduleIntegrationTestRunner({ subtotal: 10, total: 10, original_total: 10, + original_subtotal: 10, discount_total: 0, discount_subtotal: 0, discount_tax_total: 0, @@ -3121,6 +3132,10 @@ moduleIntegrationTestRunner({ value: "10", precision: 20, }, + raw_original_subtotal: { + value: "10", + precision: 20, + }, raw_discount_total: { value: "0", precision: 20, @@ -3166,6 +3181,7 @@ moduleIntegrationTestRunner({ discount_subtotal: 300, discount_tax_total: 0, original_total: 510, + original_subtotal: 510, original_tax_total: 0, item_total: 200, item_subtotal: 500, @@ -3217,6 +3233,10 @@ moduleIntegrationTestRunner({ value: "510", precision: 20, }, + raw_original_subtotal: { + value: "510", + precision: 20, + }, raw_original_tax_total: { value: "0", precision: 20, diff --git a/packages/modules/order/integration-tests/__tests__/create-order.spec.ts b/packages/modules/order/integration-tests/__tests__/create-order.spec.ts index af2840321c..3440440130 100644 --- a/packages/modules/order/integration-tests/__tests__/create-order.spec.ts +++ b/packages/modules/order/integration-tests/__tests__/create-order.spec.ts @@ -227,7 +227,7 @@ moduleIntegrationTestRunner({ const serializedOrder = JSON.parse( JSON.stringify( await service.retrieveOrder(created.id, { - select: ["id", "summary"], + select: ["id", "summary", "total"], }) ) ) @@ -246,7 +246,7 @@ moduleIntegrationTestRunner({ const serializedOrder2 = JSON.parse( JSON.stringify( await service.retrieveOrder(created.id, { - select: ["id", "summary"], + select: ["id", "summary", "total"], }) ) ) @@ -271,7 +271,7 @@ moduleIntegrationTestRunner({ const serializedOrder3 = JSON.parse( JSON.stringify( await service.retrieveOrder(created.id, { - select: ["id", "summary"], + select: ["id", "summary", "total"], }) ) ) @@ -290,7 +290,7 @@ moduleIntegrationTestRunner({ const serializedOrder4 = JSON.parse( JSON.stringify( await service.retrieveOrder(created.id, { - select: ["id", "summary"], + select: ["id", "summary", "total"], }) ) ) diff --git a/packages/modules/order/integration-tests/__tests__/order-edit.spec.ts b/packages/modules/order/integration-tests/__tests__/order-edit.spec.ts index f4522b3c05..74b9a80e85 100644 --- a/packages/modules/order/integration-tests/__tests__/order-edit.spec.ts +++ b/packages/modules/order/integration-tests/__tests__/order-edit.spec.ts @@ -414,7 +414,7 @@ moduleIntegrationTestRunner({ }) const changedOrder = await service.retrieveOrder(createdOrder.id, { - select: ["total", "items.detail", "summary"], + select: ["total", "items.detail", "summary", "total"], relations: ["items"], }) @@ -492,7 +492,7 @@ moduleIntegrationTestRunner({ }) const modified = await service.retrieveOrder(createdOrder.id, { - select: ["total", "items.detail", "summary"], + select: ["total", "items.detail", "summary", "total"], relations: ["items"], }) diff --git a/packages/modules/order/src/services/order-module-service.ts b/packages/modules/order/src/services/order-module-service.ts index 2758a00803..a592ad46e1 100644 --- a/packages/modules/order/src/services/order-module-service.ts +++ b/packages/modules/order/src/services/order-module-service.ts @@ -358,6 +358,7 @@ export default class OrderModuleService "discount_tax_total", "original_total", "original_tax_total", + "pending_difference", "item_total", "item_subtotal", "item_tax_total", @@ -370,9 +371,13 @@ export default class OrderModuleService "original_shipping_tax_total", "original_shipping_subtotal", "original_shipping_total", + "original_total", + "original_subtotal", + "original_tax_total", "credit_line_total", "credit_line_tax_total", "credit_line_subtotal", + "refundable_amount", ] const includeTotals = (config?.select ?? []).some((field) => diff --git a/packages/modules/order/src/utils/calculate-order-change.ts b/packages/modules/order/src/utils/calculate-order-change.ts index 7c7876bd60..84abcd12bf 100644 --- a/packages/modules/order/src/utils/calculate-order-change.ts +++ b/packages/modules/order/src/utils/calculate-order-change.ts @@ -224,7 +224,7 @@ export class OrderChangeProcessing { 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 { const summary_ = this.summary const total = order.total @@ -241,35 +241,6 @@ export class OrderChangeProcessing { 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 }