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:
@@ -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)
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
}),
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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_
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -55,7 +55,7 @@ export type Selector<TEntity> = {
|
||||
| DateComparisonOperator
|
||||
| StringComparisonOperator
|
||||
| NumericalComparisonOperator
|
||||
| FindOperator<TEntity[key][] | string[]>
|
||||
| FindOperator<TEntity[key][] | string | string[]>
|
||||
}
|
||||
|
||||
export type TotalField =
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user