diff --git a/integration-tests/api/__tests__/admin/order-edit.js b/integration-tests/api/__tests__/admin/order-edit.js index bb6dfc6772..19d35cdab8 100644 --- a/integration-tests/api/__tests__/admin/order-edit.js +++ b/integration-tests/api/__tests__/admin/order-edit.js @@ -17,8 +17,8 @@ const { simpleProductFactory, simpleOrderFactory, simpleDiscountFactory, - simpleRegionFactory, simpleCartFactory, + simpleRegionFactory, } = require("../../factories") const { OrderEditItemChangeType, OrderEdit } = require("@medusajs/medusa") @@ -775,6 +775,327 @@ describe("[MEDUSA_FF_ORDER_EDITING] /admin/order-edits", () => { }) }) + describe("POST /admin/order-edits/:id/items", () => { + const orderEditId = IdMap.getId("order-edit-1") + const prodId1 = IdMap.getId("prodId1") + const lineItemId1 = IdMap.getId("line-item-1") + const orderId1 = IdMap.getId("order-id-1") + const toBeAddedVariantId = IdMap.getId("variant id") + + beforeEach(async () => { + await adminSeeder(dbConnection) + + const product1 = await simpleProductFactory(dbConnection, { + id: prodId1, + }) + + const toBeAddedProduct = await simpleProductFactory(dbConnection, { + variants: [ + { + id: toBeAddedVariantId, + prices: [{ currency: "usd", amount: 200 }], + }, + ], + }) + + const order = await simpleOrderFactory(dbConnection, { + id: orderId1, + fulfillment_status: "fulfilled", + payment_status: "captured", + region: { + id: "test-region", + name: "Test region", + tax_rate: 12.5, + }, + }) + + await simpleOrderEditFactory(dbConnection, { + id: orderEditId, + order_id: order.id, + created_by: "admin_user", + }) + }) + + afterEach(async () => { + const db = useDb() + return await db.teardown() + }) + + it("creates line item that will be added to the order", async () => { + const api = useApi() + + const response = await api.post( + `/admin/order-edits/${orderEditId}/items`, + { variant_id: toBeAddedVariantId, quantity: 2 }, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.order_edit).toEqual( + expect.objectContaining({ + id: orderEditId, + created_by: "admin_user", + requested_by: null, + canceled_by: null, + confirmed_by: null, + // "Add item" change has been created + changes: [ + expect.objectContaining({ + type: "item_add", + order_edit_id: orderEditId, + original_line_item_id: null, + line_item_id: expect.any(String), + }), + ], + items: expect.arrayContaining([ + expect.objectContaining({ + variant: expect.objectContaining({ id: toBeAddedVariantId }), + quantity: 2, + order_id: null, // <-- NOT associated with the order at this point + tax_lines: [ + expect.objectContaining({ + rate: 12.5, + name: "default", + code: "default", + }), + ], + }), + ]), + /* + * Computed totals are appended to the response + */ + discount_total: 0, + gift_card_total: 0, + gift_card_tax_total: 0, + shipping_total: 0, + subtotal: 2 * 200, + tax_total: 0.125 * 2 * 200, + total: 400 + 50, + }) + ) + }) + + it("adding line item to the order edit will create adjustments percentage discount", async () => { + const api = useApi() + + const region = await simpleRegionFactory(dbConnection, { tax_rate: 10 }) + + const initialProduct = await simpleProductFactory(dbConnection, { + variants: [{ id: "initial-variant" }], + }) + + const toBeAddedProduct = await simpleProductFactory(dbConnection, { + variants: [ + { + id: toBeAddedVariantId, + prices: [{ currency: "usd", amount: 200 }], + }, + ], + }) + + const discount = await simpleDiscountFactory(dbConnection, { + code: "20PERCENT", + rule: { + type: "percentage", + allocation: "item", + value: 20, + }, + regions: [region.id], + }) + + const cart = await simpleCartFactory(dbConnection, { + email: "testy@test.com", + region: region.id, + line_items: [ + { variant_id: initialProduct.variants[0].id, quantity: 1 }, + ], + }) + + // Apply the discount on the cart and complete the cart to create an order. + + await api.post(`/store/carts/${cart.id}`, { + discounts: [{ code: "20PERCENT" }], + }) + + await api.post(`/store/carts/${cart.id}/payment-sessions`) + + const completeRes = await api.post(`/store/carts/${cart.id}/complete`) + + const orderWithDiscount = completeRes.data.data + + // Create an order edit for the created order + + const { + data: { order_edit }, + } = await api.post( + `/admin/order-edits/`, + { + order_id: orderWithDiscount.id, + }, + adminHeaders + ) + + const response = await api.post( + `/admin/order-edits/${order_edit.id}/items`, + { variant_id: toBeAddedVariantId, quantity: 2 }, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.order_edit).toEqual( + expect.objectContaining({ + order_id: orderWithDiscount.id, + items: expect.arrayContaining([ + // New line item + expect.objectContaining({ + adjustments: [ + expect.objectContaining({ + discount_id: discount.id, + amount: 80, + }), + ], + tax_lines: [expect.objectContaining({ rate: 10 })], + unit_price: 200, + quantity: 2, + }), + // Already existing line item + expect.objectContaining({ + adjustments: [ + expect.objectContaining({ + discount_id: discount.id, + amount: 20, + }), + ], + tax_lines: [expect.objectContaining({ rate: 10 })], + unit_price: 100, + quantity: 1, + variant: expect.objectContaining({ + id: initialProduct.variants[0].id, + }), + }), + ]), + gift_card_total: 0, + gift_card_tax_total: 0, + shipping_total: 0, + subtotal: 500, // 1 * 100$ + 2 * 200$ + discount_total: 100, // discount === 20% + tax_total: 40, // tax rate === 10% + total: 440, + }) + ) + }) + + it("adding line item to the order edit will create adjustments for fixed discount case", async () => { + const api = useApi() + + const region = await simpleRegionFactory(dbConnection, { tax_rate: 10 }) + + const initialProduct = await simpleProductFactory(dbConnection, { + variants: [{ id: "initial-variant" }], + }) + + const toBeAddedProduct = await simpleProductFactory(dbConnection, { + variants: [ + { + id: toBeAddedVariantId, + prices: [{ currency: "usd", amount: 200 }], + }, + ], + }) + + const discount = await simpleDiscountFactory(dbConnection, { + code: "30FIXED", + rule: { + type: "fixed", + value: 30, + }, + regions: [region.id], + }) + + const cart = await simpleCartFactory(dbConnection, { + email: "testy@test.com", + region: region.id, + line_items: [ + { variant_id: initialProduct.variants[0].id, quantity: 1 }, + ], + }) + + // Apply the discount on the cart and complete the cart to create an order. + + await api.post(`/store/carts/${cart.id}`, { + discounts: [{ code: "30FIXED" }], + }) + + await api.post(`/store/carts/${cart.id}/payment-sessions`) + + const completeRes = await api.post(`/store/carts/${cart.id}/complete`) + + const orderWithDiscount = completeRes.data.data + + // all fixed discount is allocated to single initial line item + expect(orderWithDiscount.items[0].adjustments[0].amount).toEqual(30) + + // Create an order edit for the created order + + const { + data: { order_edit }, + } = await api.post( + `/admin/order-edits/`, + { + order_id: orderWithDiscount.id, + }, + adminHeaders + ) + + const response = await api.post( + `/admin/order-edits/${order_edit.id}/items`, + { variant_id: toBeAddedVariantId, quantity: 2 }, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.order_edit).toEqual( + expect.objectContaining({ + order_id: orderWithDiscount.id, + items: expect.arrayContaining([ + // New line item + expect.objectContaining({ + adjustments: [ + expect.objectContaining({ + discount_id: discount.id, + amount: 24, + }), + ], + unit_price: 200, + quantity: 2, + }), + // Already existing line item + expect.objectContaining({ + adjustments: [ + expect.objectContaining({ + discount_id: discount.id, + amount: 6, + }), + ], + unit_price: 100, + quantity: 1, + variant: expect.objectContaining({ + id: initialProduct.variants[0].id, + }), + }), + ]), + gift_card_total: 0, + gift_card_tax_total: 0, + shipping_total: 0, + subtotal: 500, // 1 * 100$ + 2 * 200$ + discount_total: 30, // discount === fixed 30 + tax_total: 47, // tax rate === 10% + total: 470 + 47, + }) + ) + }) + }) + describe("DELETE /admin/order-edits/:id/changes/:change_id", () => { let product const orderId1 = IdMap.getId("order-id-1") @@ -1575,17 +1896,12 @@ describe("[MEDUSA_FF_ORDER_EDITING] /admin/order-edits", () => { (item) => item.original_item_id === lineItemId1 ).id - await api.post( + let response = await api.post( `/admin/order-edits/${orderEditId}/items/${updateItemId}`, { quantity: 2 }, adminHeaders ) - let response = await api.get( - `/admin/order-edits/${orderEditId}?expand=changes,items,items.tax_lines,items.adjustments`, - adminHeaders - ) - expect(response.status).toEqual(200) expect(response.data.order_edit.changes).toHaveLength(1) @@ -1696,17 +2012,12 @@ describe("[MEDUSA_FF_ORDER_EDITING] /admin/order-edits", () => { }) ) - await api.post( + response = await api.post( `/admin/order-edits/${orderEditId}/items/${updateItemId}`, { quantity: 3 }, adminHeaders ) - response = await api.get( - `/admin/order-edits/${orderEditId}?expand=changes,items,items.tax_lines,items.adjustments`, - adminHeaders - ) - expect(response.status).toEqual(200) expect(response.data.order_edit.changes).toHaveLength(1) diff --git a/packages/medusa-js/src/resources/admin/order-edits.ts b/packages/medusa-js/src/resources/admin/order-edits.ts index 37c5eb2f37..4c88586bd0 100644 --- a/packages/medusa-js/src/resources/admin/order-edits.ts +++ b/packages/medusa-js/src/resources/admin/order-edits.ts @@ -2,9 +2,10 @@ import { AdminOrderEditDeleteRes, AdminOrderEditItemChangeDeleteRes, AdminOrderEditsRes, - AdminPostOrderEditsEditLineItemsLineItemReq, AdminPostOrderEditsOrderEditReq, AdminPostOrderEditsReq, + AdminPostOrderEditsEditLineItemsReq, + AdminPostOrderEditsEditLineItemsLineItemReq, } from "@medusajs/medusa" import { ResponsePromise } from "../../typings" import BaseResource from "../base" @@ -43,6 +44,15 @@ class AdminOrderEditsResource extends BaseResource { return this.client.request("DELETE", path, undefined, {}, customHeaders) } + addLineItem( + id: string, + payload: AdminPostOrderEditsEditLineItemsReq, + customHeaders: Record = {} + ): ResponsePromise { + const path = `/admin/order-edits/${id}/items` + return this.client.request("POST", path, payload, {}, customHeaders) + } + deleteItemChange( orderEditId: string, itemChangeId: string, diff --git a/packages/medusa-react/mocks/handlers/admin.ts b/packages/medusa-react/mocks/handlers/admin.ts index 490fa6c6e3..d24ed25867 100644 --- a/packages/medusa-react/mocks/handlers/admin.ts +++ b/packages/medusa-react/mocks/handlers/admin.ts @@ -1705,6 +1705,15 @@ export const adminHandlers = [ ) }), + rest.post("/admin/order-edits/:id/items", (req, res, ctx) => { + return res( + ctx.status(200), + ctx.json({ + order_edit: { ...fixtures.get("order_edit"), ...(req.body as any) }, + }) + ) + }), + rest.post("/admin/order-edits/:id/request", (req, res, ctx) => { return res( ctx.status(200), diff --git a/packages/medusa-react/src/hooks/admin/order-edits/mutations.ts b/packages/medusa-react/src/hooks/admin/order-edits/mutations.ts index 1315abe330..6cdddf370b 100644 --- a/packages/medusa-react/src/hooks/admin/order-edits/mutations.ts +++ b/packages/medusa-react/src/hooks/admin/order-edits/mutations.ts @@ -8,6 +8,7 @@ import { AdminPostOrderEditsEditLineItemsLineItemReq, AdminPostOrderEditsOrderEditReq, AdminPostOrderEditsReq, + AdminPostOrderEditsEditLineItemsReq, } from "@medusajs/medusa" import { buildOptions } from "../../utils/buildOptions" @@ -114,6 +115,27 @@ export const useAdminUpdateOrderEdit = ( ) } +export const useAdminOrderEditLineItem = ( + id: string, + options?: UseMutationOptions< + Response, + Error, + AdminPostOrderEditsEditLineItemsReq + > +) => { + const { client } = useMedusa() + const queryClient = useQueryClient() + return useMutation( + (payload: AdminPostOrderEditsEditLineItemsReq) => + client.admin.orderEdits.addLineItem(id, payload), + buildOptions( + queryClient, + [adminOrderEditsKeys.lists(), adminOrderEditsKeys.detail(id)], + options + ) + ) +} + export const useAdminRequestOrderEditConfirmation = ( id: string, options?: UseMutationOptions, Error> diff --git a/packages/medusa-react/test/hooks/admin/order-edits/mutations.test.ts b/packages/medusa-react/test/hooks/admin/order-edits/mutations.test.ts index 615be4ec00..5dd792e15c 100644 --- a/packages/medusa-react/test/hooks/admin/order-edits/mutations.test.ts +++ b/packages/medusa-react/test/hooks/admin/order-edits/mutations.test.ts @@ -6,6 +6,8 @@ import { useAdminDeleteOrderEditItemChange, useAdminOrderEditUpdateLineItem, useAdminRequestOrderEditConfirmation, + useAdminOrderEditLineItem, + useAdminCancelOrderEdit, useAdminUpdateOrderEdit, } from "../../../../src/" import { fixtures } from "../../../../mocks/data" @@ -163,6 +165,36 @@ describe("useAdminRequestOrderEditConfirmation hook", () => { }) }) +describe("useAdminOrderEditLineItem hook", () => { + test("Created an order edit line item", async () => { + const { result, waitFor } = renderHook( + () => useAdminOrderEditLineItem(fixtures.get("order_edit").id), + { + wrapper: createWrapper(), + } + ) + + const payload = { + variant_id: "var_1", + quantity: 2, + } + + result.current.mutate(payload) + + await waitFor(() => result.current.isSuccess) + + expect(result.current.data.response.status).toEqual(200) + expect(result.current.data).toEqual( + expect.objectContaining({ + order_edit: { + ...fixtures.get("order_edit"), + ...payload, + }, + }) + ) + }) +}) + describe("useAdminCancelOrderEdit hook", () => { test("cancel an order edit", async () => { const { result, waitFor } = renderHook( diff --git a/packages/medusa/src/api/routes/admin/draft-orders/create-line-item.ts b/packages/medusa/src/api/routes/admin/draft-orders/create-line-item.ts index 48d69bdabd..580dfa91a7 100644 --- a/packages/medusa/src/api/routes/admin/draft-orders/create-line-item.ts +++ b/packages/medusa/src/api/routes/admin/draft-orders/create-line-item.ts @@ -3,13 +3,7 @@ import { DraftOrderService, LineItemService, } from "../../../../services" -import { - IsBoolean, - IsInt, - IsObject, - IsOptional, - IsString, -} from "class-validator" +import { IsInt, IsObject, IsOptional, IsString } from "class-validator" import { defaultAdminDraftOrdersCartFields, defaultAdminDraftOrdersCartRelations, @@ -17,7 +11,6 @@ import { } from "." import { EntityManager } from "typeorm" -import { FlagRouter } from "../../../../utils/flag-router" import { MedusaError } from "medusa-core-utils" import { validator } from "../../../../utils/validator" diff --git a/packages/medusa/src/api/routes/admin/order-edits/add-line-item.ts b/packages/medusa/src/api/routes/admin/order-edits/add-line-item.ts new file mode 100644 index 0000000000..f33e89adce --- /dev/null +++ b/packages/medusa/src/api/routes/admin/order-edits/add-line-item.ts @@ -0,0 +1,101 @@ +import { Request, Response } from "express" +import { IsInt, IsOptional, IsString } from "class-validator" +import { EntityManager } from "typeorm" + +import { OrderEditService } from "../../../../services" +import { + defaultOrderEditFields, + defaultOrderEditRelations, +} from "../../../../types/order-edit" + +/** + * @oas [post] /order-edits/{id}/items + * operationId: "PostOrderEditsEditLineItems" + * summary: "Add an line item to an order (edit)" + * description: "Create an OrderEdit LineItem." + * parameters: + * - (path) id=* {string} The ID of the Order Edit. + * x-authenticated: true + * x-codeSamples: + * - lang: JavaScript + * label: JS Client + * source: | + * import Medusa from "@medusajs/medusa-js" + * const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 }) + * // must be previously logged in or use api token + * medusa.admin.orderEdit.addLineItem(order_edit_id, { variant_id, quantity }) + * .then(({ order_edit }) => { + * console.log(order_edit.id) + * }) + * - lang: Shell + * label: cURL + * source: | + * curl --location --request POST 'https://medusa-url.com/admin/order-edits/{id}/items' \ + * --header 'Authorization: Bearer {api_token}' + * -d '{ "variant_id": "some_variant_id", "quantity": 3 }' + * security: + * - api_token: [] + * - cookie_auth: [] + * tags: + * - OrderEdit + * responses: + * 200: + * description: OK + * content: + * application/json: + * schema: + * properties: + * order_edit: + * $ref: "#/components/schemas/order_edit" + * "400": + * $ref: "#/components/responses/400_error" + * "401": + * $ref: "#/components/responses/unauthorized" + * "404": + * $ref: "#/components/responses/not_found_error" + * "409": + * $ref: "#/components/responses/invalid_state_error" + * "422": + * $ref: "#/components/responses/invalid_request_error" + * "500": + * $ref: "#/components/responses/500_error" + */ +export default async (req: Request, res: Response) => { + const orderEditService = req.scope.resolve( + "orderEditService" + ) as OrderEditService + + const { id } = req.params + + const manager = req.scope.resolve("manager") as EntityManager + + const data = req.validatedBody as AdminPostOrderEditsEditLineItemsReq + + await manager.transaction(async (transactionManager) => { + await orderEditService + .withTransaction(transactionManager) + .addLineItem(id, data) + }) + + let orderEdit = await orderEditService.retrieve(id, { + select: defaultOrderEditFields, + relations: defaultOrderEditRelations, + }) + + orderEdit = await orderEditService.decorateTotals(orderEdit) + + res.status(200).send({ + order_edit: orderEdit, + }) +} + +export class AdminPostOrderEditsEditLineItemsReq { + @IsString() + variant_id: string + + @IsInt() + quantity: number + + @IsOptional() + metadata?: Record | undefined +} diff --git a/packages/medusa/src/api/routes/admin/order-edits/delete-order-edit.ts b/packages/medusa/src/api/routes/admin/order-edits/delete-order-edit.ts index c56ee93812..8ee7cc85bd 100644 --- a/packages/medusa/src/api/routes/admin/order-edits/delete-order-edit.ts +++ b/packages/medusa/src/api/routes/admin/order-edits/delete-order-edit.ts @@ -8,7 +8,7 @@ import { OrderEditService } from "../../../../services" * description: "Deletes an Order Edit" * x-authenticated: true * parameters: - * - (path) id=* {string} The ID of the Note to delete. + * - (path) id=* {string} The ID of the Order Edit to delete. * x-codeSamples: * - lang: JavaScript * label: JS Client diff --git a/packages/medusa/src/api/routes/admin/order-edits/index.ts b/packages/medusa/src/api/routes/admin/order-edits/index.ts index aebae77e07..6b1dcd7142 100644 --- a/packages/medusa/src/api/routes/admin/order-edits/index.ts +++ b/packages/medusa/src/api/routes/admin/order-edits/index.ts @@ -14,6 +14,7 @@ import { import { OrderEdit } from "../../../../models" import { AdminPostOrderEditsOrderEditReq } from "./update-order-edit" import { AdminPostOrderEditsReq } from "./create-order-edit" +import { AdminPostOrderEditsEditLineItemsReq } from "./add-line-item" import { AdminPostOrderEditsEditLineItemsLineItemReq } from "./update-order-edit-line-item" const route = Router() @@ -52,6 +53,12 @@ export default (app) => { middlewares.wrap(require("./cancel-order-edit").default) ) + route.post( + "/:id/items", + transformBody(AdminPostOrderEditsEditLineItemsReq), + middlewares.wrap(require("./add-line-item").default) + ) + route.delete("/:id", middlewares.wrap(require("./delete-order-edit").default)) route.delete( @@ -86,3 +93,5 @@ export type AdminOrderEditItemChangeDeleteRes = { export * from "./update-order-edit" export * from "./update-order-edit-line-item" export * from "./create-order-edit" + +export * from "./add-line-item" diff --git a/packages/medusa/src/services/__mocks__/order-edit-item-change.js b/packages/medusa/src/services/__mocks__/order-edit-item-change.js index 54504992ab..bda9e2e870 100644 --- a/packages/medusa/src/services/__mocks__/order-edit-item-change.js +++ b/packages/medusa/src/services/__mocks__/order-edit-item-change.js @@ -10,6 +10,9 @@ export const orderEditItemChangeServiceMock = { order_edit_id: orderEditId, }) }), + create: jest.fn().mockImplementation((data) => { + return Promise.resolve(data) + }), delete: jest.fn().mockImplementation(() => { return Promise.resolve() }), diff --git a/packages/medusa/src/services/__tests__/line-item.js b/packages/medusa/src/services/__tests__/line-item.js index 757a91eae1..306c5ac25a 100644 --- a/packages/medusa/src/services/__tests__/line-item.js +++ b/packages/medusa/src/services/__tests__/line-item.js @@ -4,7 +4,6 @@ import LineItemService from "../line-item" import { PricingServiceMock } from "../__mocks__/pricing" import { ProductVariantServiceMock } from "../__mocks__/product-variant" import { RegionServiceMock } from "../__mocks__/region" - ;[true, false].forEach((isTaxInclusiveEnabled) => { describe(`tax inclusive flag set to: ${isTaxInclusiveEnabled}`, () => { describe("LineItemService", () => { diff --git a/packages/medusa/src/services/__tests__/order-edit.ts b/packages/medusa/src/services/__tests__/order-edit.ts index 75409da521..90cf0ed91c 100644 --- a/packages/medusa/src/services/__tests__/order-edit.ts +++ b/packages/medusa/src/services/__tests__/order-edit.ts @@ -77,6 +77,17 @@ const orderEditWithChanges = { ], } +const orderEditWithAddedLineItem = { + id: IdMap.getId("order-edit-with-changes"), + order: { + id: IdMap.getId("order-edit-change"), + cart: { + discounts: [{ rule: {} }], + }, + region: { id: IdMap.getId("test-region") }, + }, +} + const lineItemServiceMock = { ...LineItemServiceMock, list: jest.fn().mockImplementation(() => { @@ -179,9 +190,9 @@ describe("OrderEditService", () => { lineItemService: lineItemServiceMock as unknown as LineItemService, orderEditItemChangeService: orderEditItemChangeServiceMock as unknown as OrderEditItemChangeService, - taxProviderService: taxProviderServiceMock as unknown as TaxProviderService, lineItemAdjustmentService: LineItemAdjustmentServiceMock as unknown as LineItemAdjustmentService, + taxProviderService: taxProviderServiceMock as unknown as TaxProviderService, }) it("should retrieve an order edit and call the repository with the right arguments", async () => { @@ -392,4 +403,20 @@ describe("OrderEditService", () => { ) }) }) + + it("should add a line item to an order edit", async () => { + jest + .spyOn(orderEditService, "refreshAdjustments") + .mockImplementation(async () => {}) + + await orderEditService.addLineItem(IdMap.getId("order-edit-with-changes"), { + variant_id: IdMap.getId("to-be-added-variant"), + quantity: 3, + }) + + expect(LineItemServiceMock.generate).toHaveBeenCalledTimes(1) + expect(orderEditService.refreshAdjustments).toHaveBeenCalledTimes(1) + expect(taxProviderServiceMock.createTaxLines).toHaveBeenCalledTimes(1) + expect(orderEditItemChangeServiceMock.create).toHaveBeenCalledTimes(1) + }) }) diff --git a/packages/medusa/src/services/index.ts b/packages/medusa/src/services/index.ts index eb320bae2c..942367a7ac 100644 --- a/packages/medusa/src/services/index.ts +++ b/packages/medusa/src/services/index.ts @@ -16,6 +16,7 @@ export { default as GiftCardService } from "./gift-card" export { default as IdempotencyKeyService } from "./idempotency-key" export { default as InventoryService } from "./inventory" export { default as LineItemService } from "./line-item" +export { default as LineItemAdjustmentService } from "./line-item-adjustment" export { default as MiddlewareService } from "./middleware" export { default as NoteService } from "./note" export { default as NotificationService } from "./notification" diff --git a/packages/medusa/src/services/line-item-adjustment.ts b/packages/medusa/src/services/line-item-adjustment.ts index 694d2b6927..31e1f7ddfb 100644 --- a/packages/medusa/src/services/line-item-adjustment.ts +++ b/packages/medusa/src/services/line-item-adjustment.ts @@ -1,6 +1,6 @@ import { MedusaError } from "medusa-core-utils" -import { BaseService } from "medusa-interfaces" import { EntityManager, In } from "typeorm" + import { Cart, DiscountRuleType, @@ -12,6 +12,8 @@ import { LineItemAdjustmentRepository } from "../repositories/line-item-adjustme import { FindConfig } from "../types/common" import { FilterableLineItemAdjustmentProps } from "../types/line-item-adjustment" import DiscountService from "./discount" +import { TransactionBaseService } from "../interfaces" +import { buildQuery, setMetadata } from "../utils" type LineItemAdjustmentServiceProps = { manager: EntityManager @@ -23,46 +25,36 @@ type AdjustmentContext = { variant: ProductVariant } -type GeneratedAdjustment = Omit +type GeneratedAdjustment = { + amount: number + discount_id: string + description: string +} /** * Provides layer to manipulate line item adjustments. * @extends BaseService */ -class LineItemAdjustmentService extends BaseService { - private manager_: EntityManager - private lineItemAdjustmentRepo_: typeof LineItemAdjustmentRepository - private discountService: DiscountService +class LineItemAdjustmentService extends TransactionBaseService { + protected readonly manager_: EntityManager + protected transactionManager_: EntityManager | undefined + + private readonly lineItemAdjustmentRepo_: typeof LineItemAdjustmentRepository + private readonly discountService: DiscountService constructor({ manager, lineItemAdjustmentRepository, discountService, }: LineItemAdjustmentServiceProps) { - super() + // eslint-disable-next-line prefer-rest-params + super(arguments[0]) + this.manager_ = manager this.lineItemAdjustmentRepo_ = lineItemAdjustmentRepository this.discountService = discountService } - withTransaction( - transactionManager: EntityManager - ): LineItemAdjustmentService { - if (!transactionManager) { - return this - } - - const cloned = new LineItemAdjustmentService({ - manager: transactionManager, - lineItemAdjustmentRepository: this.lineItemAdjustmentRepo_, - discountService: this.discountService, - }) - - cloned.transactionManager_ = transactionManager - - return cloned - } - /** * Retrieves a line item adjustment by id. * @param id - the id of the line item adjustment to retrieve @@ -76,7 +68,7 @@ class LineItemAdjustmentService extends BaseService { const lineItemAdjustmentRepo: LineItemAdjustmentRepository = this.manager_.getCustomRepository(this.lineItemAdjustmentRepo_) - const query = this.buildQuery_({ id }, config) + const query = buildQuery({ id }, config) const lineItemAdjustment = await lineItemAdjustmentRepo.findOne(query) if (!lineItemAdjustment) { @@ -124,10 +116,7 @@ class LineItemAdjustmentService extends BaseService { const { metadata, ...rest } = data if (metadata) { - lineItemAdjustment.metadata = this.setMetadata_( - lineItemAdjustment, - metadata - ) + lineItemAdjustment.metadata = setMetadata(lineItemAdjustment, metadata) } for (const [key, value] of Object.entries(rest)) { @@ -153,7 +142,7 @@ class LineItemAdjustmentService extends BaseService { this.lineItemAdjustmentRepo_ ) - const query = this.buildQuery_(selector, config) + const query = buildQuery(selector, config) return await lineItemAdjustmentRepo.find(query) } @@ -172,15 +161,15 @@ class LineItemAdjustmentService extends BaseService { if (typeof selectorOrIds === "string" || Array.isArray(selectorOrIds)) { const ids = typeof selectorOrIds === "string" ? [selectorOrIds] : selectorOrIds - return await lineItemAdjustmentRepo.delete({ id: In(ids) }) + await lineItemAdjustmentRepo.delete({ id: In(ids) }) + return } - const query = this.buildQuery_(selectorOrIds) + const query = buildQuery(selectorOrIds) const lineItemAdjustments = await lineItemAdjustmentRepo.find(query) await lineItemAdjustmentRepo.remove(lineItemAdjustments) - return }) } diff --git a/packages/medusa/src/services/line-item.ts b/packages/medusa/src/services/line-item.ts index e2c3bee9c3..e33b54810f 100644 --- a/packages/medusa/src/services/line-item.ts +++ b/packages/medusa/src/services/line-item.ts @@ -2,23 +2,22 @@ import { MedusaError } from "medusa-core-utils" import { BaseService } from "medusa-interfaces" import { EntityManager, In } from "typeorm" import { DeepPartial } from "typeorm/common/DeepPartial" -import TaxInclusivePricingFeatureFlag from "../loaders/feature-flags/tax-inclusive-pricing" -import { LineItemTaxLine } from "../models" -import { Cart } from "../models/cart" -import { LineItem } from "../models/line-item" -import { LineItemAdjustment } from "../models/line-item-adjustment" + import { CartRepository } from "../repositories/cart" import { LineItemRepository } from "../repositories/line-item" import { LineItemTaxLineRepository } from "../repositories/line-item-tax-line" -import { FindConfig } from "../types/common" +import { Cart, LineItemTaxLine, LineItem, LineItemAdjustment } from "../models" +import { FindConfig, Selector } from "../types/common" import { FlagRouter } from "../utils/flag-router" +import LineItemAdjustmentService from "./line-item-adjustment" +import OrderEditingFeatureFlag from "../loaders/feature-flags/order-editing" +import TaxInclusivePricingFeatureFlag from "../loaders/feature-flags/tax-inclusive-pricing" import { PricingService, ProductService, ProductVariantService, RegionService, } from "./index" -import LineItemAdjustmentService from "./line-item-adjustment" type InjectedDependencies = { manager: EntityManager @@ -99,7 +98,7 @@ class LineItemService extends BaseService { } async list( - selector, + selector: Selector, config: FindConfig = { skip: 0, take: 50, @@ -208,6 +207,7 @@ class LineItemService extends BaseService { includes_tax?: boolean metadata?: Record customer_id?: string + order_edit_id?: string cart?: Cart } = {} ): Promise { @@ -267,6 +267,12 @@ class LineItemService extends BaseService { rawLineItem.includes_tax = unitPriceIncludesTax } + if ( + this.featureFlagRouter_.isFeatureEnabled(OrderEditingFeatureFlag.key) + ) { + rawLineItem.order_edit_id = context.order_edit_id || null + } + const lineItemRepo = transactionManager.getCustomRepository( this.lineItemRepository_ ) diff --git a/packages/medusa/src/services/order-edit-item-change.ts b/packages/medusa/src/services/order-edit-item-change.ts index f2f0e050e1..85b9f3e3dc 100644 --- a/packages/medusa/src/services/order-edit-item-change.ts +++ b/packages/medusa/src/services/order-edit-item-change.ts @@ -1,6 +1,6 @@ import { TransactionBaseService } from "../interfaces" import { OrderItemChangeRepository } from "../repositories/order-item-change" -import { EntityManager, In } from "typeorm" +import { DeepPartial, EntityManager, In } from "typeorm" import { EventBusService, LineItemService } from "./index" import { FindConfig, Selector } from "../types/common" import { OrderItemChange } from "../models" diff --git a/packages/medusa/src/services/order-edit.ts b/packages/medusa/src/services/order-edit.ts index d27c7dfd0f..2a6f5a249d 100644 --- a/packages/medusa/src/services/order-edit.ts +++ b/packages/medusa/src/services/order-edit.ts @@ -1,7 +1,8 @@ import { EntityManager, IsNull } from "typeorm" +import { MedusaError } from "medusa-core-utils" + import { FindConfig } from "../types/common" import { buildQuery, isDefined } from "../utils" -import { MedusaError } from "medusa-core-utils" import { OrderEditRepository } from "../repositories/order-edit" import { Cart, @@ -13,26 +14,30 @@ import { import { TransactionBaseService } from "../interfaces" import { EventBusService, + LineItemAdjustmentService, LineItemService, OrderEditItemChangeService, OrderService, TaxProviderService, TotalsService, } from "./index" -import { CreateOrderEditInput, UpdateOrderEditInput } from "../types/order-edit" -import region from "./region" -import LineItemAdjustmentService from "./line-item-adjustment" +import { + AddOrderEditLineItemInput, + CreateOrderEditInput, + UpdateOrderEditInput, +} from "../types/order-edit" type InjectedDependencies = { manager: EntityManager orderEditRepository: typeof OrderEditRepository + orderService: OrderService - eventBusService: EventBusService totalsService: TotalsService lineItemService: LineItemService - orderEditItemChangeService: OrderEditItemChangeService - lineItemAdjustmentService: LineItemAdjustmentService + eventBusService: EventBusService taxProviderService: TaxProviderService + lineItemAdjustmentService: LineItemAdjustmentService + orderEditItemChangeService: OrderEditItemChangeService } export default class OrderEditService extends TransactionBaseService { @@ -44,16 +49,18 @@ export default class OrderEditService extends TransactionBaseService { CANCELED: "order-edit.canceled", } - protected transactionManager_: EntityManager | undefined protected readonly manager_: EntityManager + protected transactionManager_: EntityManager | undefined + protected readonly orderEditRepository_: typeof OrderEditRepository + protected readonly orderService_: OrderService + protected readonly totalsService_: TotalsService protected readonly lineItemService_: LineItemService protected readonly eventBusService_: EventBusService - protected readonly totalsService_: TotalsService - protected readonly orderEditItemChangeService_: OrderEditItemChangeService - protected readonly lineItemAdjustmentService_: LineItemAdjustmentService protected readonly taxProviderService_: TaxProviderService + protected readonly lineItemAdjustmentService_: LineItemAdjustmentService + protected readonly orderEditItemChangeService_: OrderEditItemChangeService constructor({ manager, @@ -459,6 +466,78 @@ export default class OrderEditService extends TransactionBaseService { return orderEdit } + async addLineItem( + orderEditId: string, + data: AddOrderEditLineItemInput + ): Promise { + return await this.atomicPhase_(async (manager) => { + const lineItemServiceTx = this.lineItemService_.withTransaction(manager) + + const orderEdit = await this.retrieve(orderEditId, { + relations: ["order", "order.region"], + }) + + if (!OrderEditService.isOrderEditActive(orderEdit)) { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + `Can not add an item to the edit with status ${orderEdit.status}` + ) + } + + const regionId = orderEdit.order.region_id + + /** + * Create new line item and refresh adjustments for all cloned order edit items + */ + + const lineItemData = await lineItemServiceTx.generate( + data.variant_id, + regionId, + data.quantity, + { + customer_id: orderEdit.order.customer_id, + metadata: data.metadata, + order_edit_id: orderEditId, + } + ) + + let lineItem = await lineItemServiceTx.create(lineItemData) + lineItem = await lineItemServiceTx.retrieve(lineItem.id) + + await this.refreshAdjustments(orderEditId) + + /** + * Generate a change record + */ + + await this.orderEditItemChangeService_.withTransaction(manager).create({ + type: OrderEditItemChangeType.ITEM_ADD, + line_item_id: lineItem.id, + order_edit_id: orderEditId, + }) + + /** + * Compute tax lines + */ + + const localCart = { + ...orderEdit.order, + object: "cart", + items: [lineItem], + } as unknown as Cart + + const calcContext = await this.totalsService_ + .withTransaction(manager) + .getCalculationContext(localCart, { + exclude_shipping: true, + }) + + await this.taxProviderService_ + .withTransaction(manager) + .createTaxLines([lineItem], calcContext) + }) + } + async deleteItemChange( orderEditId: string, itemChangeId: string diff --git a/packages/medusa/src/types/common.ts b/packages/medusa/src/types/common.ts index d3175f2c6e..e4bb854f89 100644 --- a/packages/medusa/src/types/common.ts +++ b/packages/medusa/src/types/common.ts @@ -55,7 +55,7 @@ export type Selector = { | DateComparisonOperator | StringComparisonOperator | NumericalComparisonOperator - | FindOperator + | FindOperator } export type TotalField = diff --git a/packages/medusa/src/types/order-edit.ts b/packages/medusa/src/types/order-edit.ts index dc2a7c5547..993f0c5eba 100644 --- a/packages/medusa/src/types/order-edit.ts +++ b/packages/medusa/src/types/order-edit.ts @@ -9,6 +9,13 @@ export type CreateOrderEditInput = { internal_note?: string } +export type AddOrderEditLineItemInput = { + quantity: number + variant_id: string + + metadata?: Record +} + export type CreateOrderEditItemChangeInput = { type: OrderEditItemChangeType order_edit_id: string @@ -21,6 +28,7 @@ export const defaultOrderEditRelations: string[] = [ "changes.line_item", "changes.original_line_item", "items", + "items.adjustments", "items.tax_lines", ]