feat(medusa): refactor the way the order edit handle the items (#2255)

* feat(medusa): Reftor the way the order edit works
This commit is contained in:
Adrien de Peretti
2022-09-26 16:01:20 +02:00
committed by GitHub
parent 7e56935e7a
commit d138baf460
25 changed files with 704 additions and 360 deletions

View File

@@ -94,6 +94,13 @@ describe("[MEDUSA_FF_ORDER_EDITING] /admin/order-edits", () => {
fulfilled_quantity: 1,
shipped_quantity: 1,
unit_price: 1000,
tax_lines: [
{
rate: 10,
code: "code1",
name: "code1",
},
],
},
{
id: lineItemId2,
@@ -102,6 +109,13 @@ describe("[MEDUSA_FF_ORDER_EDITING] /admin/order-edits", () => {
fulfilled_quantity: 1,
shipped_quantity: 1,
unit_price: 1000,
tax_lines: [
{
rate: 10,
code: "code2",
name: "code2",
},
],
},
],
})
@@ -170,20 +184,42 @@ describe("[MEDUSA_FF_ORDER_EDITING] /admin/order-edits", () => {
canceled_by: null,
confirmed_by: null,
internal_note: "test internal note",
items: expect.arrayContaining([
expect.objectContaining({ id: lineItemCreateId, quantity: 2 }),
expect.objectContaining({ id: lineItemId1, quantity: 2 }),
]),
removed_items: expect.arrayContaining([
expect.objectContaining({ id: lineItemId2, quantity: 1 }),
items: expect.arrayContaining([]),
changes: expect.arrayContaining([
expect.objectContaining({
type: "item_add",
order_edit_id: orderEditId,
original_line_item_id: null,
line_item_id: lineItemCreateId,
line_item: expect.any(Object),
original_line_item: null,
}),
expect.objectContaining({
type: "item_update",
order_edit_id: orderEditId,
original_line_item_id: lineItemId1,
line_item_id: lineItemUpdateId,
line_item: expect.any(Object),
original_line_item: expect.any(Object),
}),
expect.objectContaining({
type: "item_remove",
order_edit_id: orderEditId,
original_line_item_id: lineItemId2,
line_item_id: null,
line_item: null,
original_line_item: expect.any(Object),
}),
]),
// Items are cloned during the creation which explain why it is 0 for a fake order edit since it does
// not use the logic of the service. Must be check in another test
shipping_total: 0,
gift_card_total: 0,
gift_card_tax_total: 0,
discount_total: 0,
tax_total: 0,
total: 2200,
subtotal: 2200,
total: 0,
subtotal: 0,
})
)
expect(response.status).toEqual(200)
@@ -344,6 +380,14 @@ describe("[MEDUSA_FF_ORDER_EDITING] /admin/order-edits", () => {
fulfilled_quantity: 1,
shipped_quantity: 1,
unit_price: 1000,
tax_lines: [
{
item_id: lineItemId1,
rate: 10,
code: "default",
name: "default",
},
],
},
{
id: lineItemId2,
@@ -352,6 +396,14 @@ describe("[MEDUSA_FF_ORDER_EDITING] /admin/order-edits", () => {
fulfilled_quantity: 1,
shipped_quantity: 1,
unit_price: 1000,
tax_lines: [
{
item_id: lineItemId2,
rate: 10,
code: "default",
name: "default",
},
],
},
],
})
@@ -363,7 +415,7 @@ describe("[MEDUSA_FF_ORDER_EDITING] /admin/order-edits", () => {
return await db.teardown()
})
it("creates and order edit", async () => {
it("creates an order edit", async () => {
const api = useApi()
const response = await api.post(
@@ -384,29 +436,46 @@ describe("[MEDUSA_FF_ORDER_EDITING] /admin/order-edits", () => {
canceled_by: null,
confirmed_by: null,
internal_note: "This is an internal note",
// The items are cloned from the items of the order
items: expect.arrayContaining([
expect.objectContaining({
id: lineItemId1,
id: expect.not.stringContaining(lineItemId1),
order_id: null,
order_edit_id: expect.any(String),
original_item_id: lineItemId1,
quantity: 1,
fulfilled_quantity: 1,
shipped_quantity: 1,
unit_price: 1000,
tax_lines: expect.arrayContaining([
expect.objectContaining({
rate: 10,
}),
]),
}),
expect.objectContaining({
id: lineItemId2,
id: expect.not.stringContaining(lineItemId2),
order_id: null,
order_edit_id: expect.any(String),
original_item_id: lineItemId2,
quantity: 1,
fulfilled_quantity: 1,
shipped_quantity: 1,
unit_price: 1000,
tax_lines: expect.arrayContaining([
expect.objectContaining({
rate: 10,
}),
]),
}),
]),
shipping_total: 0,
gift_card_total: 0,
gift_card_tax_total: 0,
discount_total: 0,
tax_total: 0,
total: 2000,
tax_total: 200,
subtotal: 2000,
total: 2200,
})
)
})
@@ -461,29 +530,46 @@ describe("[MEDUSA_FF_ORDER_EDITING] /admin/order-edits", () => {
canceled_by: null,
confirmed_by: null,
internal_note: "This is an internal note",
// The items are cloned from the items of the order
items: expect.arrayContaining([
expect.objectContaining({
id: lineItemId1,
id: expect.not.stringContaining(lineItemId1),
order_id: null,
order_edit_id: expect.any(String),
original_item_id: lineItemId1,
quantity: 1,
fulfilled_quantity: 1,
shipped_quantity: 1,
unit_price: 1000,
tax_lines: expect.arrayContaining([
expect.objectContaining({
rate: 10,
}),
]),
}),
expect.objectContaining({
id: lineItemId2,
id: expect.not.stringContaining(lineItemId2),
order_id: null,
order_edit_id: expect.any(String),
original_item_id: lineItemId2,
quantity: 1,
fulfilled_quantity: 1,
shipped_quantity: 1,
unit_price: 1000,
tax_lines: expect.arrayContaining([
expect.objectContaining({
rate: 10,
}),
]),
}),
]),
shipping_total: 0,
gift_card_total: 0,
gift_card_tax_total: 0,
discount_total: 0,
tax_total: 0,
total: 2000,
tax_total: 200,
subtotal: 2000,
total: 2200,
})
)
})
@@ -581,7 +667,7 @@ describe("[MEDUSA_FF_ORDER_EDITING] /admin/order-edits", () => {
})
describe("POST /admin/order-edits/:id", () => {
const orderEditId = IdMap.getId("order-edit-1")
let orderEditId
const prodId1 = IdMap.getId("prodId1")
const lineItemId1 = IdMap.getId("line-item-1")
const orderId1 = IdMap.getId("order-id-1")
@@ -612,16 +698,27 @@ describe("[MEDUSA_FF_ORDER_EDITING] /admin/order-edits", () => {
fulfilled_quantity: 1,
shipped_quantity: 1,
unit_price: 1000,
tax_lines: [
{
rate: 10,
code: "code1",
name: "code1",
},
],
},
],
})
await simpleOrderEditFactory(dbConnection, {
id: orderEditId,
order_id: order.id,
created_by: "admin_user",
internal_note: "test internal note",
})
const api = useApi()
const response = await api.post(
`/admin/order-edits/`,
{
order_id: orderId1,
internal_note: "This is an internal note",
},
adminHeaders
)
orderEditId = response.data.order_edit.id
})
afterEach(async () => {
@@ -647,25 +744,29 @@ describe("[MEDUSA_FF_ORDER_EDITING] /admin/order-edits", () => {
canceled_by: null,
confirmed_by: null,
internal_note: "changed note",
/*
* Computed items are appended to the response
*/
items: [
items: expect.arrayContaining([
expect.objectContaining({
id: lineItemId1,
order_id: orderId1,
order_id: null,
order_edit_id: orderEditId,
original_item_id: lineItemId1,
quantity: 1,
fulfilled_quantity: 1,
shipped_quantity: 1,
unit_price: 1000,
tax_lines: expect.arrayContaining([
expect.objectContaining({
rate: 10,
}),
]),
}),
],
/*
* Computed totals are appended to the response
*/
]),
discount_total: 0,
gift_card_total: 0,
gift_card_tax_total: 0,
shipping_total: 0,
subtotal: 1000,
tax_total: 0,
total: 1000,
tax_total: 100,
total: 1100,
})
)
})

View File

@@ -21,12 +21,6 @@ const { OrderEditItemChangeType } = require("@medusajs/medusa")
jest.setTimeout(30000)
const adminHeaders = {
headers: {
Authorization: "Bearer test_token",
},
}
describe("[MEDUSA_FF_ORDER_EDITING] /store/order-edits", () => {
let medusaProcess
let dbConnection
@@ -93,6 +87,13 @@ describe("[MEDUSA_FF_ORDER_EDITING] /store/order-edits", () => {
fulfilled_quantity: 1,
shipped_quantity: 1,
unit_price: 1000,
tax_lines: [
{
rate: 10,
code: "code1",
name: "code1",
},
],
},
{
id: lineItemId2,
@@ -101,6 +102,13 @@ describe("[MEDUSA_FF_ORDER_EDITING] /store/order-edits", () => {
fulfilled_quantity: 1,
shipped_quantity: 1,
unit_price: 1000,
tax_lines: [
{
rate: 10,
code: "code2",
name: "code2",
},
],
},
],
})
@@ -162,24 +170,44 @@ describe("[MEDUSA_FF_ORDER_EDITING] /store/order-edits", () => {
expect(response.data.order_edit).toEqual(
expect.objectContaining({
id: orderEditId,
requested_by: null,
items: expect.arrayContaining([
expect.objectContaining({ id: lineItemCreateId, quantity: 2 }),
expect.objectContaining({ id: lineItemId1, quantity: 2 }),
]),
removed_items: expect.arrayContaining([
expect.objectContaining({ id: lineItemId2, quantity: 1 }),
items: expect.arrayContaining([]),
changes: expect.arrayContaining([
expect.objectContaining({
type: "item_add",
order_edit_id: orderEditId,
original_line_item_id: null,
line_item_id: lineItemCreateId,
line_item: expect.any(Object),
original_line_item: null,
}),
expect.objectContaining({
type: "item_update",
order_edit_id: orderEditId,
original_line_item_id: lineItemId1,
line_item_id: lineItemUpdateId,
line_item: expect.any(Object),
original_line_item: expect.any(Object),
}),
expect.objectContaining({
type: "item_remove",
order_edit_id: orderEditId,
original_line_item_id: lineItemId2,
line_item_id: null,
line_item: null,
original_line_item: expect.any(Object),
}),
]),
// Items are cloned during the creation which explain why it is 0 for a fake order edit since it does
// not use the logic of the service. Must be check in another test
shipping_total: 0,
gift_card_total: 0,
gift_card_tax_total: 0,
discount_total: 0,
tax_total: 0,
total: 2200,
subtotal: 2200,
total: 0,
subtotal: 0,
})
)
expect(response.data.order_edit.internal_note).not.toBeDefined()
expect(response.data.order_edit.created_by).not.toBeDefined()
expect(response.data.order_edit.canceled_by).not.toBeDefined()

View File

@@ -33,6 +33,7 @@ describe("POST /admin/order-edits", () => {
})
it("calls order edit service create", () => {
expect(orderEditServiceMock.decorateTotals).toHaveBeenCalledTimes(1)
expect(orderEditServiceMock.create).toHaveBeenCalledTimes(1)
expect(orderEditServiceMock.create).toHaveBeenCalledWith(
{

View File

@@ -33,7 +33,7 @@ describe("GET /admin/order-edits/:id", () => {
select: defaultOrderEditFields,
relations: defaultOrderEditRelations,
})
expect(orderEditServiceMock.decorateLineItemsAndTotals).toHaveBeenCalledTimes(1)
expect(orderEditServiceMock.decorateTotals).toHaveBeenCalledTimes(1)
})
it("returns order", () => {

View File

@@ -9,14 +9,18 @@ describe("GET /admin/order-edits/:id", () => {
let subject
beforeAll(async () => {
subject = await request("POST", `/admin/order-edits/${orderEditId}/request`, {
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
subject = await request(
"POST",
`/admin/order-edits/${orderEditId}/request`,
{
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
},
flags: [OrderEditingFeatureFlag],
})
flags: [OrderEditingFeatureFlag],
}
)
})
afterAll(() => {
@@ -25,15 +29,20 @@ describe("GET /admin/order-edits/:id", () => {
it("calls orderEditService requestConfirmation", () => {
expect(orderEditServiceMock.requestConfirmation).toHaveBeenCalledTimes(1)
expect(orderEditServiceMock.requestConfirmation).toHaveBeenCalledWith(orderEditId, {loggedInUser: IdMap.getId("admin_user")})
expect(orderEditServiceMock.requestConfirmation).toHaveBeenCalledWith(
orderEditId,
{ loggedInUser: IdMap.getId("admin_user") }
)
})
it("returns updated orderEdit", () => {
expect(subject.body.order_edit).toEqual(expect.objectContaining({
id: orderEditId,
requested_at: expect.any(String),
requested_by: IdMap.getId("admin_user")
}))
expect(subject.body.order_edit).toEqual(
expect.objectContaining({
id: orderEditId,
requested_at: expect.any(String),
requested_by: IdMap.getId("admin_user"),
})
)
})
})
})

View File

@@ -2,6 +2,10 @@ import { Request, Response } from "express"
import { OrderEditService } from "../../../../services"
import { IsOptional, IsString } from "class-validator"
import { EntityManager } from "typeorm"
import {
defaultOrderEditFields,
defaultOrderEditRelations,
} from "../../../../types/order-edit"
/**
* @oas [post] /order-edits
@@ -62,13 +66,19 @@ export default async (req: Request, res: Response) => {
const data = req.validatedBody as AdminPostOrderEditsReq
const loggedInUserId = (req.user?.id ?? req.user?.userId) as string
const orderEdit = await manager.transaction(async (transactionManager) => {
const orderEditServiceTx =
orderEditService.withTransaction(transactionManager)
const orderEdit = await orderEditServiceTx.create(data, { loggedInUserId })
const createdOrderEdit = await manager.transaction(
async (transactionManager) => {
return await orderEditService
.withTransaction(transactionManager)
.create(data, { loggedInUserId })
}
)
return await orderEditServiceTx.decorateLineItemsAndTotals(orderEdit)
let orderEdit = await orderEditService.retrieve(createdOrderEdit.id, {
select: defaultOrderEditFields,
relations: defaultOrderEditRelations,
})
orderEdit = await orderEditService.decorateTotals(orderEdit)
return res.json({ order_edit: orderEdit })
}

View File

@@ -60,8 +60,7 @@ export default async (req: Request, res: Response) => {
const retrieveConfig = req.retrieveConfig
let orderEdit = await orderEditService.retrieve(id, retrieveConfig)
orderEdit = await orderEditService.decorateLineItemsAndTotals(orderEdit)
orderEdit = await orderEditService.decorateTotals(orderEdit)
return res.json({ order_edit: orderEdit })
}

View File

@@ -3,6 +3,10 @@ import { Request, Response } from "express"
import { EntityManager } from "typeorm"
import { OrderEditService } from "../../../../services"
import {
defaultOrderEditFields,
defaultOrderEditRelations,
} from "../../../../types/order-edit"
/**
* @oas [post] /order-edits/{id}
@@ -71,26 +75,19 @@ export default async (req: Request, res: Response) => {
const manager: EntityManager = req.scope.resolve("manager")
const orderEdit = await manager.transaction(async (transactionManager) => {
const orderEditServiceTx =
orderEditService.withTransaction(transactionManager)
const updatedOrderEdit = await manager.transaction(
async (transactionManager) => {
return await orderEditService
.withTransaction(transactionManager)
.update(id, validatedBody)
}
)
const _orderEdit = await orderEditServiceTx.update(id, validatedBody)
const { items } = await orderEditServiceTx.computeLineItems(_orderEdit.id)
_orderEdit.items = items
const totals = await orderEditServiceTx.getTotals(_orderEdit.id)
_orderEdit.discount_total = totals.discount_total
_orderEdit.gift_card_total = totals.gift_card_total
_orderEdit.gift_card_tax_total = totals.gift_card_tax_total
_orderEdit.shipping_total = totals.shipping_total
_orderEdit.subtotal = totals.subtotal
_orderEdit.tax_total = totals.tax_total
_orderEdit.total = totals.total
return _orderEdit
let orderEdit = await orderEditService.retrieve(updatedOrderEdit.id, {
select: defaultOrderEditFields,
relations: defaultOrderEditRelations,
})
orderEdit = await orderEditService.decorateTotals(orderEdit)
res.status(200).json({ order_edit: orderEdit })
}

View File

@@ -2,11 +2,6 @@ import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import { orderEditServiceMock } from "../../../../../services/__mocks__/order-edit"
import OrderEditingFeatureFlag from "../../../../../loaders/feature-flags/order-editing"
import {
defaultOrderEditFields,
defaultOrderEditRelations,
} from "../../../../../types/order-edit"
import { storeOrderEditNotAllowedFields } from "../index"
describe("GET /store/order-edits/:id", () => {
describe("successfully gets an order edit", () => {
@@ -18,10 +13,14 @@ describe("GET /store/order-edits/:id", () => {
}
beforeAll(async () => {
subject = await request("POST", `/store/order-edits/${orderEditId}/decline`, {
payload,
flags: [OrderEditingFeatureFlag],
})
subject = await request(
"POST",
`/store/order-edits/${orderEditId}/decline`,
{
payload,
flags: [OrderEditingFeatureFlag],
}
)
})
afterAll(() => {
@@ -30,8 +29,11 @@ describe("GET /store/order-edits/:id", () => {
it("calls orderService decline", () => {
expect(orderEditServiceMock.decline).toHaveBeenCalledTimes(1)
expect(orderEditServiceMock.decline).toHaveBeenCalledWith(orderEditId, { declinedReason: "test", loggedInUser: undefined})
expect(orderEditServiceMock.decorateLineItemsAndTotals).toHaveBeenCalledTimes(1)
expect(orderEditServiceMock.decline).toHaveBeenCalledWith(orderEditId, {
declinedReason: "test",
loggedInUser: undefined,
})
expect(orderEditServiceMock.decorateTotals).toHaveBeenCalledTimes(1)
})
it("returns orderEdit", () => {

View File

@@ -3,10 +3,9 @@ import { request } from "../../../../../helpers/test-request"
import { orderEditServiceMock } from "../../../../../services/__mocks__/order-edit"
import OrderEditingFeatureFlag from "../../../../../loaders/feature-flags/order-editing"
import {
defaultOrderEditFields,
defaultOrderEditRelations,
defaultStoreOrderEditFields,
defaultStoreOrderEditRelations,
} from "../../../../../types/order-edit"
import { storeOrderEditNotAllowedFields } from "../index"
describe("GET /store/order-edits/:id", () => {
describe("successfully gets an order edit", () => {
@@ -26,14 +25,10 @@ describe("GET /store/order-edits/:id", () => {
it("calls orderService retrieve", () => {
expect(orderEditServiceMock.retrieve).toHaveBeenCalledTimes(1)
expect(orderEditServiceMock.retrieve).toHaveBeenCalledWith(orderEditId, {
select: defaultOrderEditFields.filter(
(field) => !storeOrderEditNotAllowedFields.includes(field)
),
relations: defaultOrderEditRelations.filter(
(field) => !storeOrderEditNotAllowedFields.includes(field)
),
select: defaultStoreOrderEditFields,
relations: defaultStoreOrderEditRelations,
})
expect(orderEditServiceMock.decorateLineItemsAndTotals).toHaveBeenCalledTimes(1)
expect(orderEditServiceMock.decorateTotals).toHaveBeenCalledTimes(1)
})
it("returns order", () => {

View File

@@ -2,6 +2,10 @@ import { IsOptional, IsString } from "class-validator"
import { Request, Response } from "express"
import { EntityManager } from "typeorm"
import { OrderEditService } from "../../../../services"
import {
defaultStoreOrderEditFields,
defaultStoreOrderEditRelations,
} from "../../../../types/order-edit"
/**
* @oas [post] /order-edits/{id}/decline
@@ -71,9 +75,12 @@ export default async (req: Request, res: Response) => {
loggedInUser: userId,
})
})
let orderEdit = await orderEditService.retrieve(id)
orderEdit = await orderEditService.decorateLineItemsAndTotals(orderEdit)
let orderEdit = await orderEditService.retrieve(id, {
select: defaultStoreOrderEditFields,
relations: defaultStoreOrderEditRelations,
})
orderEdit = await orderEditService.decorateTotals(orderEdit)
res.status(200).json({ order_edit: orderEdit })
}

View File

@@ -54,8 +54,7 @@ export default async (req: Request, res: Response) => {
const retrieveConfig = req.retrieveConfig
let orderEdit = await orderEditService.retrieve(id, retrieveConfig)
orderEdit = await orderEditService.decorateLineItemsAndTotals(orderEdit)
orderEdit = await orderEditService.decorateTotals(orderEdit)
return res.json({ order_edit: orderEdit })
}

View File

@@ -7,8 +7,8 @@ import { EmptyQueryParams } from "../../../../types/common"
import { isFeatureFlagEnabled } from "../../../middlewares/feature-flag-enabled"
import OrderEditingFeatureFlag from "../../../../loaders/feature-flags/order-editing"
import {
defaultOrderEditFields,
defaultOrderEditRelations,
defaultStoreOrderEditFields,
defaultStoreOrderEditRelations,
} from "../../../../types/order-edit"
import { OrderEdit } from "../../../../models"
import { StorePostOrderEditsOrderEditDecline } from "./decline-order-edit"
@@ -25,13 +25,9 @@ export default (app) => {
route.get(
"/:id",
transformQuery(EmptyQueryParams, {
defaultRelations: defaultOrderEditRelations.filter(
(field) => !storeOrderEditNotAllowedFields.includes(field)
),
defaultFields: defaultOrderEditFields.filter(
(field) => !storeOrderEditNotAllowedFields.includes(field)
),
allowedFields: defaultOrderEditFields,
defaultRelations: defaultStoreOrderEditRelations,
defaultFields: defaultStoreOrderEditFields,
allowedFields: defaultStoreOrderEditFields,
isList: false,
}),
middlewares.wrap(require("./get-order-edit").default)
@@ -54,10 +50,3 @@ export type StoreOrderEditsRes = {
}
export * from "./decline-order-edit"
export const storeOrderEditNotAllowedFields = [
"internal_note",
"created_by",
"confirmed_by",
"canceled_by",
]

View File

@@ -0,0 +1,50 @@
import { MigrationInterface, QueryRunner } from "typeorm"
import OrderEditingFeatureFlag from "../loaders/feature-flags/order-editing"
export const featureFlag = OrderEditingFeatureFlag.key
export class lineItemOriginalItemRelation1663059812400
implements MigrationInterface
{
name = "lineItemOriginalItemRelation1663059812400"
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "line_item"
ADD COLUMN IF NOT EXISTS original_item_id character varying,
ADD COLUMN IF NOT EXISTS order_edit_id character varying`
)
await queryRunner.query(
`ALTER TABLE "line_item"
ADD CONSTRAINT "line_item_original_item_fk" FOREIGN KEY ("original_item_id") REFERENCES "line_item" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION`
)
await queryRunner.query(
`ALTER TABLE "line_item"
ADD CONSTRAINT "line_item_order_edit_fk" FOREIGN KEY ("order_edit_id") REFERENCES "order_edit" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION`
)
await queryRunner.query(
`CREATE UNIQUE INDEX "unique_li_original_item_id_order_edit_id" ON "line_item" ("order_edit_id", "original_item_id") WHERE original_item_id IS NOT NULL AND order_edit_id IS NOT NULL`
)
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`DROP INDEX IF EXISTS "unique_li_original_item_id_order_edit_id"`
)
await queryRunner.query(
`ALTER TABLE "line_item" DROP CONSTRAINT "line_item_original_item_fk"`
)
await queryRunner.query(
`ALTER TABLE "line_item" DROP CONSTRAINT "line_item_order_edit_fk"`
)
await queryRunner.query(
`ALTER TABLE "line_item" DROP COLUMN "original_item_id"`
)
await queryRunner.query(
`ALTER TABLE "line_item" DROP COLUMN "order_edit_id"`
)
}
}

View File

@@ -9,7 +9,7 @@ import {
OneToMany,
} from "typeorm"
import { BaseEntity } from "../interfaces/models/base-entity"
import { BaseEntity } from "../interfaces"
import { Cart } from "./cart"
import { ClaimOrder } from "./claim-order"
import { DbAwareColumn } from "../utils/db-aware-column"
@@ -18,14 +18,30 @@ import { LineItemTaxLine } from "./line-item-tax-line"
import { Order } from "./order"
import { ProductVariant } from "./product-variant"
import { Swap } from "./swap"
import { generateEntityId } from "../utils/generate-entity-id"
import { FeatureFlagColumn } from "../utils/feature-flag-decorators"
import { generateEntityId } from "../utils"
import {
FeatureFlagClassDecorators,
FeatureFlagColumn,
FeatureFlagDecorators,
} from "../utils/feature-flag-decorators"
import TaxInclusivePricingFeatureFlag from "../loaders/feature-flags/tax-inclusive-pricing"
import OrderEditingFeatureFlag from "../loaders/feature-flags/order-editing"
import { OrderEdit } from "./order-edit"
@Check(`"fulfilled_quantity" <= "quantity"`)
@Check(`"shipped_quantity" <= "fulfilled_quantity"`)
@Check(`"returned_quantity" <= "quantity"`)
@Check(`"quantity" > 0`)
@FeatureFlagClassDecorators(OrderEditingFeatureFlag.key, [
Index(
"unique_li_original_item_id_order_edit_id",
["order_edit_id", "original_item_id"],
{
unique: true,
where: "WHERE original_item_id IS NOT NULL AND order_edit_id IS NOT NULL",
}
),
])
@Entity()
export class LineItem extends BaseEntity {
@Index()
@@ -68,6 +84,27 @@ export class LineItem extends BaseEntity {
})
adjustments: LineItemAdjustment[]
@FeatureFlagColumn(OrderEditingFeatureFlag.key, {
nullable: true,
type: "varchar",
})
original_item_id?: string | null
@FeatureFlagColumn(OrderEditingFeatureFlag.key, {
nullable: true,
type: "varchar",
})
order_edit_id?: string | null
@FeatureFlagDecorators(OrderEditingFeatureFlag.key, [
ManyToOne(
() => OrderEdit,
(orderEdit) => orderEdit.items
),
JoinColumn({ name: "order_edit_id" }),
])
order_edit?: OrderEdit | null
@Column()
title: string
@@ -283,6 +320,15 @@ export class LineItem extends BaseEntity {
* includes_tax:
* description: "[EXPERIMENTAL] Indicates if the line item unit_price include tax"
* type: boolean
* original_item_id:
* description: "[EXPERIMENTAL] The id of the original line item"
* type: string
* order_edit_id:
* description: "[EXPERIMENTAL] The ID of the order edit to which a cloned item belongs"
* type: string
* order_edit:
* description: "[EXPERIMENTAL] The order edit joined"
* type: object
* created_at:
* type: string
* description: "The date with timezone at which the resource was created."

View File

@@ -2,7 +2,6 @@ import {
AfterLoad,
BeforeInsert,
Column,
CreateDateColumn,
JoinColumn,
ManyToOne,
OneToMany,
@@ -72,6 +71,9 @@ export class OrderEdit extends BaseEntity {
@Column({ type: resolveDbType("timestamptz"), nullable: true })
canceled_at?: Date
@OneToMany(() => LineItem, (lineItem) => lineItem.order_edit)
items: LineItem[]
// Computed
shipping_total: number
discount_total: number
@@ -85,9 +87,6 @@ export class OrderEdit extends BaseEntity {
status: OrderEditStatus
items: LineItem[]
removed_items: LineItem[]
@BeforeInsert()
private beforeInsert(): void {
this.id = generateEntityId(this.id, "oe")
@@ -207,9 +206,4 @@ export class OrderEdit extends BaseEntity {
* description: Computed line items from the changes.
* items:
* $ref: "#/components/schemas/line_item"
* removed_items:
* type: array
* description: Computed line items from the changes that have been marked as deleted.
* items:
* $ref: "#/components/schemas/line_item"
*/

View File

@@ -1,68 +1,5 @@
import { EntityRepository, FindManyOptions, Repository } from "typeorm"
import { OrderEdit } from "../models/order-edit"
import { flatten, groupBy, merge } from "lodash"
import { EntityRepository, Repository } from "typeorm"
import { OrderEdit } from "../models"
@EntityRepository(OrderEdit)
export class OrderEditRepository extends Repository<OrderEdit> {
public async findWithRelations(
relations: (keyof OrderEdit | string)[] = [],
idsOrOptionsWithoutRelations:
| Omit<FindManyOptions<OrderEdit>, "relations">
| string[] = {}
): Promise<[OrderEdit[], number]> {
let entities: OrderEdit[] = []
let count
if (Array.isArray(idsOrOptionsWithoutRelations)) {
entities = await this.findByIds(idsOrOptionsWithoutRelations)
count = idsOrOptionsWithoutRelations.length
} else {
const [results, resultCount] = await this.findAndCount(
idsOrOptionsWithoutRelations
)
entities = results
count = resultCount
}
const entitiesIds = entities.map(({ id }) => id)
const groupedRelations = {}
for (const rel of relations) {
const [topLevel] = rel.split(".")
if (groupedRelations[topLevel]) {
groupedRelations[topLevel].push(rel)
} else {
groupedRelations[topLevel] = [rel]
}
}
const entitiesIdsWithRelations = await Promise.all(
Object.entries(groupedRelations).map(async ([_, rels]) => {
return this.findByIds(entitiesIds, {
select: ["id"],
relations: rels as string[],
})
})
).then(flatten)
const entitiesAndRelations = entitiesIdsWithRelations.concat(entities)
const entitiesAndRelationsById = groupBy(entitiesAndRelations, "id")
return [
Object.values(entitiesAndRelationsById).map((v) => merge({}, ...v)),
count,
]
}
public async findOneWithRelations(
relations: Array<keyof OrderEdit> = [],
optionsWithoutRelations: Omit<FindManyOptions<OrderEdit>, "relations"> = {}
): Promise<OrderEdit> {
// Limit 1
optionsWithoutRelations.take = 1
const [result] = await this.findWithRelations(
relations,
optionsWithoutRelations
)
return result[0]
}
}
export class OrderEditRepository extends Repository<OrderEdit> {}

View File

@@ -92,7 +92,7 @@ export const orderEditServiceMock = {
delete: jest.fn().mockImplementation((_) => {
return Promise.resolve()
}),
decorateLineItemsAndTotals: jest.fn().mockImplementation((orderEdit) => {
decorateTotals: jest.fn().mockImplementation((orderEdit) => {
const withLineItems = computeLineItems(orderEdit)
return Promise.resolve({
...withLineItems,

View File

@@ -0,0 +1,17 @@
export const taxProviderServiceMock = {
withTransaction: function () {
return this
},
createTaxLines: jest.fn().mockImplementation((order, calculationContext) => {
return Promise.resolve()
}),
clearLineItemsTaxLines: jest.fn().mockImplementation((_) => {
return Promise.resolve()
}),
}
const mock = jest.fn().mockImplementation(() => {
return taxProviderServiceMock
})
export default mock

View File

@@ -1,6 +1,10 @@
import { IdMap, MockManager, MockRepository } from "medusa-test-utils"
import { FlagRouter } from "../../utils/flag-router"
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", () => {
@@ -517,5 +521,106 @@ describe("LineItemService", () => {
})
})
})
describe("clone", () => {
const buildLineItem = (id) => ({
id,
original_item_id: id,
swap_id: "test",
order_id: "test",
tax_lines: [
{
rate: 10,
item_id: id,
},
],
adjustments: [
{
amount: 10,
item_id: id,
},
],
})
const buildExpectedLineItem = (id) =>
expect.objectContaining({
original_item_id: id,
swap_id: undefined,
claim_order_id: undefined,
cart_id: undefined,
order_edit_id: undefined,
order_id: "test",
tax_lines: expect.arrayContaining([
expect.objectContaining({
rate: 10,
}),
]),
adjustments: expect.arrayContaining([
expect.objectContaining({
amount: 10,
}),
]),
})
const lineItemRepository = MockRepository({
create: (data) => data,
save: (data) => data,
find: (selector) => {
return selector.where.id.value.map(buildLineItem)
},
})
const featureFlagRouter = new FlagRouter({})
const lineItemService = new LineItemService({
manager: MockManager,
pricingService: PricingServiceMock,
lineItemRepository,
productVariantService: ProductVariantServiceMock,
regionService: RegionServiceMock,
cartRepository: MockRepository,
featureFlagRouter,
})
beforeEach(async () => {
jest.clearAllMocks()
})
it("successfully clone line items with tax lines and adjustments", async () => {
const lineItemId1 = IdMap.getId("line-item-1")
const lineItemId2 = IdMap.getId("line-item-2")
await lineItemService.cloneTo([lineItemId1, lineItemId2], {
order_id: "test",
})
expect(lineItemRepository.save).toHaveBeenCalledTimes(1)
expect(lineItemRepository.create).toHaveBeenCalledTimes(1)
expect(lineItemRepository.create).toHaveBeenCalledWith(
expect.arrayContaining([
buildExpectedLineItem(lineItemId1),
buildExpectedLineItem(lineItemId2),
])
)
expect(lineItemRepository.save).toHaveBeenCalledWith(
expect.arrayContaining([
buildExpectedLineItem(lineItemId1),
buildExpectedLineItem(lineItemId2),
])
)
})
it("throw on clone line items if none of the foreign keys is specified", async () => {
const lineItemId1 = IdMap.getId("line-item-1")
const lineItemId2 = IdMap.getId("line-item-2")
const err = await lineItemService
.cloneTo([lineItemId1, lineItemId2])
.catch((e) => e)
expect(err.message).toBe(
"Unable to clone a line item that is not attached to at least one of: order_edit, order, swap, claim or cart."
)
})
})
})
})

View File

@@ -5,6 +5,7 @@ import {
OrderEditItemChangeService,
OrderEditService,
OrderService,
TaxProviderService,
TotalsService,
} from "../index"
import { OrderEditItemChangeType, OrderEditStatus } from "../../models"
@@ -13,6 +14,9 @@ import { EventBusServiceMock } from "../__mocks__/event-bus"
import { LineItemServiceMock } from "../__mocks__/line-item"
import { TotalsServiceMock } from "../__mocks__/totals"
import { orderEditItemChangeServiceMock } from "../__mocks__/order-edit-item-change"
import { taxProviderServiceMock } from "../__mocks__/tax-provider"
import { LineItemAdjustmentServiceMock } from "../__mocks__/line-item-adjustment"
import LineItemAdjustmentService from "../line-item-adjustment"
const orderEditToUpdate = {
id: IdMap.getId("order-edit-to-update"),
@@ -80,6 +84,7 @@ const lineItemServiceMock = {
id,
})
}),
cloneTo: () => [],
}
describe("OrderEditService", () => {
@@ -88,7 +93,10 @@ describe("OrderEditService", () => {
})
const orderEditRepository = MockRepository({
findOneWithRelations: (relations, query) => {
findOne: (query) => {
if (query?.where?.id === IdMap.getId("order-edit-to-update")) {
return orderEditToUpdate
}
if (query?.where?.id === IdMap.getId("order-edit-with-changes")) {
return orderEditWithChanges
}
@@ -117,7 +125,7 @@ describe("OrderEditService", () => {
}
}
return {}
return
},
create: (data) => {
return {
@@ -136,17 +144,17 @@ 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,
})
it("should retrieve an order edit and call the repository with the right arguments", async () => {
await orderEditService.retrieve(IdMap.getId("order-edit-with-changes"))
expect(orderEditRepository.findOneWithRelations).toHaveBeenCalledTimes(1)
expect(orderEditRepository.findOneWithRelations).toHaveBeenCalledWith(
undefined,
{
where: { id: IdMap.getId("order-edit-with-changes") },
}
)
expect(orderEditRepository.findOne).toHaveBeenCalledTimes(1)
expect(orderEditRepository.findOne).toHaveBeenCalledWith({
where: { id: IdMap.getId("order-edit-with-changes") },
})
})
it("should update an order edit with the right arguments", async () => {
@@ -155,37 +163,11 @@ describe("OrderEditService", () => {
})
expect(orderEditRepository.save).toHaveBeenCalledTimes(1)
expect(orderEditRepository.save).toHaveBeenCalledWith({
id: IdMap.getId("order-edit-to-update"),
internal_note: "test note",
})
})
it("should compute the items from the changes and attach them to the orderEdit", async () => {
const { items, removedItems } = await orderEditService.computeLineItems(
IdMap.getId("order-edit-with-changes")
)
expect(items.length).toBe(2)
expect(items).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: IdMap.getId("line-item-2"),
}),
expect.objectContaining({
id: IdMap.getId("line-item-3"),
}),
])
)
expect(removedItems.length).toBe(1)
expect(removedItems).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: IdMap.getId("line-item-1"),
}),
])
)
})
it("should create an order edit and call the repository with the right arguments as well as the event bus service", async () => {
const data = {
order_id: IdMap.getId("order-edit-order-id"),
@@ -267,7 +249,9 @@ describe("OrderEditService", () => {
let result
beforeEach(async () => {
result = await orderEditService.requestConfirmation(orderEditId, {loggedInUser: userId})
result = await orderEditService.requestConfirmation(orderEditId, {
loggedInUser: userId,
})
})
it("sets fields correctly for update", async () => {
@@ -283,13 +267,12 @@ describe("OrderEditService", () => {
requested_at: expect.any(Date),
requested_by: userId,
})
expect(EventBusServiceMock.emit).toHaveBeenCalledWith(
OrderEditService.Events.REQUESTED,
{ id: orderEditId }
)
})
})
describe("requested edit", () => {
@@ -298,7 +281,9 @@ describe("OrderEditService", () => {
let result
beforeEach(async () => {
result = await orderEditService.requestConfirmation(orderEditId, {loggedInUser: userId})
result = await orderEditService.requestConfirmation(orderEditId, {
loggedInUser: userId,
})
})
afterEach(() => {

View File

@@ -1,6 +1,6 @@
import { MedusaError } from "medusa-core-utils"
import { BaseService } from "medusa-interfaces"
import { EntityManager } from "typeorm"
import { EntityManager, In } from "typeorm"
import { DeepPartial } from "typeorm/common/DeepPartial"
import TaxInclusivePricingFeatureFlag from "../loaders/feature-flags/tax-inclusive-pricing"
import { LineItemTaxLine } from "../models"
@@ -363,6 +363,77 @@ class LineItemService extends BaseService {
return itemTaxLineRepo.create(args)
}
async cloneTo(
ids: string | string[],
data: DeepPartial<LineItem> = {},
options: { setOriginalLineItemId?: boolean } = {
setOriginalLineItemId: true,
}
): Promise<LineItem[]> {
ids = typeof ids === "string" ? [ids] : ids
return await this.atomicPhase_(async (manager) => {
let lineItems: DeepPartial<LineItem>[] = await this.list(
{
id: In(ids as string[]),
},
{
relations: ["tax_lines", "adjustments"],
}
)
const lineItemRepository = manager.getCustomRepository(
this.lineItemRepository_
)
const {
order_id,
swap_id,
claim_order_id,
cart_id,
order_edit_id,
...lineItemData
} = data
if (
!order_id &&
!swap_id &&
!claim_order_id &&
!cart_id &&
!order_edit_id
) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Unable to clone a line item that is not attached to at least one of: order_edit, order, swap, claim or cart."
)
}
lineItems = lineItems.map((item) => ({
...item,
...lineItemData,
id: undefined,
order_id,
swap_id,
claim_order_id,
cart_id,
order_edit_id,
original_item_id: options?.setOriginalLineItemId ? item.id : undefined,
tax_lines: item.tax_lines?.map((tax_line) => ({
...tax_line,
id: undefined,
item_id: undefined,
})),
adjustments: item.adjustments?.map((adj) => ({
...adj,
id: undefined,
item_id: undefined,
})),
}))
const clonedLineItemEntities = lineItemRepository.create(lineItems)
return await lineItemRepository.save(clonedLineItemEntities)
})
}
}
export default LineItemService

View File

@@ -3,22 +3,18 @@ import { FindConfig } from "../types/common"
import { buildQuery, isDefined } from "../utils"
import { MedusaError } from "medusa-core-utils"
import { OrderEditRepository } from "../repositories/order-edit"
import {
LineItem,
Order,
OrderEdit,
OrderEditItemChangeType,
OrderEditStatus,
} from "../models"
import { Order, OrderEdit, OrderEditStatus } from "../models"
import { TransactionBaseService } from "../interfaces"
import {
EventBusService,
LineItemService,
OrderEditItemChangeService,
OrderService,
TaxProviderService,
TotalsService,
} from "./index"
import { CreateOrderEditInput, UpdateOrderEditInput } from "../types/order-edit"
import LineItemAdjustmentService from "./line-item-adjustment"
type InjectedDependencies = {
manager: EntityManager
@@ -28,6 +24,8 @@ type InjectedDependencies = {
totalsService: TotalsService
lineItemService: LineItemService
orderEditItemChangeService: OrderEditItemChangeService
lineItemAdjustmentService: LineItemAdjustmentService
taxProviderService: TaxProviderService
}
export default class OrderEditService extends TransactionBaseService {
@@ -46,6 +44,8 @@ export default class OrderEditService extends TransactionBaseService {
protected readonly eventBusService_: EventBusService
protected readonly totalsService_: TotalsService
protected readonly orderEditItemChangeService_: OrderEditItemChangeService
protected readonly lineItemAdjustmentService_: LineItemAdjustmentService
protected readonly taxProviderService_: TaxProviderService
constructor({
manager,
@@ -55,6 +55,8 @@ export default class OrderEditService extends TransactionBaseService {
eventBusService,
totalsService,
orderEditItemChangeService,
lineItemAdjustmentService,
taxProviderService,
}: InjectedDependencies) {
// eslint-disable-next-line prefer-rest-params
super(arguments[0])
@@ -66,6 +68,8 @@ export default class OrderEditService extends TransactionBaseService {
this.eventBusService_ = eventBusService
this.totalsService_ = totalsService
this.orderEditItemChangeService_ = orderEditItemChangeService
this.lineItemAdjustmentService_ = lineItemAdjustmentService
this.taxProviderService_ = taxProviderService
}
async retrieve(
@@ -76,12 +80,9 @@ export default class OrderEditService extends TransactionBaseService {
const orderEditRepository = manager.getCustomRepository(
this.orderEditRepository_
)
const { relations, ...query } = buildQuery({ id: orderEditId }, config)
const orderEdit = await orderEditRepository.findOneWithRelations(
relations as (keyof OrderEdit)[],
query
)
const query = buildQuery({ id: orderEditId }, config)
const orderEdit = await orderEditRepository.findOne(query)
if (!orderEdit) {
throw new MedusaError(
@@ -114,80 +115,6 @@ export default class OrderEditService extends TransactionBaseService {
return await orderEditRepository.findOne(query)
}
/**
* Compute line items across order and order edit
* - if an item have been removed, it will appear in the removedItems collection and will not appear in the item collection
* - if an item have been updated, it will appear in the item collection with id being the id of the original item and the rest of the data being the data of the new item generated from the update
* - if an item have been added, it will appear in the item collection with id being the id of the new item and the rest of the data being the data of the new item generated from the add
* @param orderEditId
*/
async computeLineItems(
orderEditId: string
): Promise<{ items: LineItem[]; removedItems: LineItem[] }> {
const manager = this.transactionManager_ ?? this.manager_
const lineItemServiceTx = this.lineItemService_.withTransaction(manager)
const orderEdit = await this.retrieve(orderEditId, {
select: ["id", "order_id", "changes"],
relations: ["changes", "changes.original_line_item", "changes.line_item"],
})
const items: LineItem[] = []
const orderEditRemovedItemsMap: Map<string, LineItem> = new Map()
const orderEditUpdatedItemsMap: Map<string, LineItem> = new Map()
for (const change of orderEdit.changes) {
const lineItemId =
change.type === OrderEditItemChangeType.ITEM_REMOVE
? change.original_line_item_id!
: change.line_item_id!
const lineItem = await lineItemServiceTx.retrieve(lineItemId!, {
relations: ["tax_lines", "adjustments"],
})
if (change.type === OrderEditItemChangeType.ITEM_REMOVE) {
orderEditRemovedItemsMap.set(change.original_line_item_id!, lineItem)
continue
}
if (change.type === OrderEditItemChangeType.ITEM_ADD) {
items.push(lineItem)
continue
}
orderEditUpdatedItemsMap.set(change.original_line_item_id!, {
...lineItem,
id: change.original_line_item_id!,
} as LineItem)
}
const originalLineItems = await this.lineItemService_
.withTransaction(manager)
.list(
{
order_id: orderEdit.order_id,
},
{
relations: ["tax_lines", "adjustments"],
}
)
for (const originalLineItem of originalLineItems) {
const itemRemoved = orderEditRemovedItemsMap.get(originalLineItem.id)
if (itemRemoved) {
continue
}
const updatedLineItem = orderEditUpdatedItemsMap.get(originalLineItem.id)
const lineItem = updatedLineItem ?? originalLineItem
items.push(lineItem)
}
return { items, removedItems: [...orderEditRemovedItemsMap.values()] }
}
/**
* Compute and return the different totals from the order edit id
* @param orderEditId
@@ -202,8 +129,9 @@ export default class OrderEditService extends TransactionBaseService {
total: number
}> {
const manager = this.transactionManager_ ?? this.manager_
const { order_id } = await this.retrieve(orderEditId, {
select: ["order_id"],
const { order_id, items } = await this.retrieve(orderEditId, {
select: ["id", "order_id", "items"],
relations: ["items", "items.tax_lines", "items.adjustments"],
})
const order = await this.orderService_
.withTransaction(manager)
@@ -218,7 +146,6 @@ export default class OrderEditService extends TransactionBaseService {
"shipping_methods.tax_lines",
],
})
const { items } = await this.computeLineItems(orderEditId)
const computedOrder = { ...order, items } as Order
const totalsServiceTx = this.totalsService_.withTransaction(manager)
@@ -267,6 +194,22 @@ export default class OrderEditService extends TransactionBaseService {
const orderEdit = await orderEditRepository.save(orderEditToCreate)
const lineItemServiceTx =
this.lineItemService_.withTransaction(transactionManager)
const orderLineItems = await lineItemServiceTx.list(
{
order_id: data.order_id,
},
{
select: ["id"],
}
)
const lineItemIds = orderLineItems.map(({ id }) => id)
await lineItemServiceTx.cloneTo(lineItemIds, {
order_edit_id: orderEdit.id,
})
await this.eventBusService_
.withTransaction(transactionManager)
.emit(OrderEditService.Events.CREATED, { id: orderEdit.id })
@@ -304,13 +247,13 @@ export default class OrderEditService extends TransactionBaseService {
})
}
async delete(orderEditId: string): Promise<void> {
async delete(id: string): Promise<void> {
return await this.atomicPhase_(async (manager) => {
const orderEditRepo = manager.getCustomRepository(
this.orderEditRepository_
)
const edit = await orderEditRepo.findOne({ where: { id: orderEditId } })
const edit = await this.retrieve(id).catch(() => void 0)
if (!edit) {
return
@@ -323,6 +266,7 @@ export default class OrderEditService extends TransactionBaseService {
)
}
await this.deleteClonedItems(id)
await orderEditRepo.remove(edit)
})
}
@@ -370,19 +314,6 @@ export default class OrderEditService extends TransactionBaseService {
})
}
async decorateLineItemsAndTotals(orderEdit: OrderEdit): Promise<OrderEdit> {
const lineItemDecoratedOrderEdit = await this.decorateLineItems(orderEdit)
return await this.decorateTotals(lineItemDecoratedOrderEdit)
}
async decorateLineItems(orderEdit: OrderEdit): Promise<OrderEdit> {
const { items, removedItems } = await this.computeLineItems(orderEdit.id)
orderEdit.items = items
orderEdit.removed_items = removedItems
return orderEdit
}
async decorateTotals(orderEdit: OrderEdit): Promise<OrderEdit> {
const totals = await this.getTotals(orderEdit.id)
orderEdit.discount_total = totals.discount_total
@@ -467,4 +398,41 @@ export default class OrderEditService extends TransactionBaseService {
return orderEdit
})
}
protected async deleteClonedItems(orderEditId: string): Promise<void> {
const manager = this.transactionManager_ ?? this.manager_
const lineItemServiceTx = this.lineItemService_.withTransaction(manager)
const lineItemAdjustmentServiceTx =
this.lineItemAdjustmentService_.withTransaction(manager)
const taxProviderServiceTs =
this.taxProviderService_.withTransaction(manager)
const clonedLineItems = await lineItemServiceTx.list(
{
order_edit_id: orderEditId,
},
{
select: ["id", "tax_lines", "adjustments"],
relations: ["tax_lines", "adjustments"],
}
)
const clonedItemIds = clonedLineItems.map((item) => item.id)
await Promise.all(
[
taxProviderServiceTs.clearLineItemsTaxLines(clonedItemIds),
clonedItemIds.map((id) => {
return lineItemAdjustmentServiceTx.delete({
item_id: id,
})
}),
].flat()
)
await Promise.all(
clonedItemIds.map((id) => {
return lineItemServiceTx.delete(id)
})
)
}
}

View File

@@ -4,14 +4,22 @@ export type UpdateOrderEditInput = {
internal_note?: string
}
export type CreateOrderEditInput = {
order_id: string
internal_note?: string
}
export const defaultOrderEditRelations: string[] = [
"changes",
"changes.line_item",
"changes.original_line_item",
"items",
"items.tax_lines",
]
export const defaultOrderEditFields: (keyof OrderEdit)[] = [
"id",
"items",
"changes",
"order_id",
"created_by",
@@ -27,7 +35,16 @@ export const defaultOrderEditFields: (keyof OrderEdit)[] = [
"internal_note",
]
export type CreateOrderEditInput = {
order_id: string
internal_note?: string
}
export const storeOrderEditNotAllowedFields = [
"internal_note",
"created_by",
"confirmed_by",
"canceled_by",
]
export const defaultStoreOrderEditRelations = defaultOrderEditRelations.filter(
(field) => !storeOrderEditNotAllowedFields.includes(field)
)
export const defaultStoreOrderEditFields = defaultOrderEditFields.filter(
(field) => !storeOrderEditNotAllowedFields.includes(field)
)

View File

@@ -50,6 +50,23 @@ export function FeatureFlagDecorators(
}
}
export function FeatureFlagClassDecorators(
featureFlag: string,
decorators: ClassDecorator[]
): ClassDecorator {
return function (target) {
setImmediate_((): any => {
if (!featureFlagRouter.isFeatureEnabled(featureFlag)) {
return
}
decorators.forEach((decorator: ClassDecorator) => {
decorator(target)
})
})
}
}
export function FeatureFlagEntity(
featureFlag: string,
name?: string,