feat: add a line item to an order edit (#2243)

**What**
- Implement adding a line item to order (edit)

**How**
- _by implementing the following "flow"_
  - generate a line item
  - computing line item adjustments for that line item
  - creating tax lines
  - creating a change record

**Testing**
- **_integration tests_**
  - check if line item and order item change objects are created (with correct tax lines)
  - line item adjustments are generated if
    - fixed discount is applied to cart
    - percentage discount is applied
- **_unit tests_** 
  - ensure that methods from Inventory, LineItem, LineItemAdjustment etc. services are called 

---

RESOLVES CORE-495
This commit is contained in:
Frane Polić
2022-09-28 13:51:13 +02:00
committed by GitHub
parent 474e97252c
commit 884f36e8a8
19 changed files with 679 additions and 80 deletions

View File

@@ -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)

View File

@@ -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<string, any> = {}
): ResponsePromise<AdminOrderEditsRes> {
const path = `/admin/order-edits/${id}/items`
return this.client.request("POST", path, payload, {}, customHeaders)
}
deleteItemChange(
orderEditId: string,
itemChangeId: string,

View File

@@ -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),

View File

@@ -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<AdminOrderEditsRes>,
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<Response<AdminOrderEditsRes>, Error>

View File

@@ -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(

View File

@@ -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"

View File

@@ -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<string, unknown> | undefined
}

View File

@@ -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

View File

@@ -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"

View File

@@ -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()
}),

View File

@@ -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", () => {

View File

@@ -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)
})
})

View File

@@ -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"

View File

@@ -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<LineItem, "id" | "item_id">
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
})
}

View File

@@ -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<LineItem>,
config: FindConfig<LineItem> = {
skip: 0,
take: 50,
@@ -208,6 +207,7 @@ class LineItemService extends BaseService {
includes_tax?: boolean
metadata?: Record<string, unknown>
customer_id?: string
order_edit_id?: string
cart?: Cart
} = {}
): Promise<LineItem> {
@@ -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_
)

View File

@@ -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"

View File

@@ -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<void> {
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

View File

@@ -55,7 +55,7 @@ export type Selector<TEntity> = {
| DateComparisonOperator
| StringComparisonOperator
| NumericalComparisonOperator
| FindOperator<TEntity[key][] | string[]>
| FindOperator<TEntity[key][] | string | string[]>
}
export type TotalField =

View File

@@ -9,6 +9,13 @@ export type CreateOrderEditInput = {
internal_note?: string
}
export type AddOrderEditLineItemInput = {
quantity: number
variant_id: string
metadata?: Record<string, unknown>
}
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",
]