feat(medusa): Implement premises of the creation flow of an order edit (#2187)

**What**
- Implements the admin create end point 
- Service implementation of the create method and the retrieveActive as well as the totals computation
- Improve compute line items
- client
  - medusa-js api
  - medusa-react mutations hooks

**Tests**
- Unit tests of the create end points
- Unit tests of the service create method
- Integration tests for admin that also take into account totals computations
- client
  - medusa-js tests
  - medusa-react hooks tests

FIXES CORE-491
This commit is contained in:
Adrien de Peretti
2022-09-16 08:29:40 +00:00
committed by GitHub
parent 6132711eef
commit f7177c9033
20 changed files with 745 additions and 115 deletions
@@ -115,14 +115,16 @@ describe("[MEDUSA_FF_ORDER_EDITING] /admin/order-edits", () => {
await simpleLineItemFactory(dbConnection, {
id: lineItemUpdateId,
order_id: orderEdit.order_id,
order_id: null,
variant_id: product1.variants[0].id,
unit_price: 1000,
quantity: 2,
})
await simpleLineItemFactory(dbConnection, {
id: lineItemCreateId,
order_id: orderEdit.order_id,
order_id: null,
variant_id: product3.variants[0].id,
unit_price: 100,
quantity: 2,
})
@@ -175,6 +177,13 @@ describe("[MEDUSA_FF_ORDER_EDITING] /admin/order-edits", () => {
removed_items: expect.arrayContaining([
expect.objectContaining({ id: lineItemId2, quantity: 1 }),
]),
shipping_total: 0,
gift_card_total: 0,
gift_card_tax_total: 0,
discount_total: 0,
tax_total: 0,
total: 2200,
subtotal: 2200,
})
)
expect(response.status).toEqual(200)
@@ -292,4 +301,126 @@ describe("[MEDUSA_FF_ORDER_EDITING] /admin/order-edits", () => {
})
})
})
describe("POST /admin/order-edits", () => {
let orderId
const prodId1 = IdMap.getId("prodId1")
const prodId2 = IdMap.getId("prodId2")
const lineItemId1 = IdMap.getId("line-item-1")
const lineItemId2 = IdMap.getId("line-item-2")
beforeEach(async () => {
await adminSeeder(dbConnection)
const product1 = await simpleProductFactory(dbConnection, {
id: prodId1,
})
const product2 = await simpleProductFactory(dbConnection, {
id: prodId2,
})
const order = await simpleOrderFactory(dbConnection, {
email: "test@testson.com",
tax_rate: null,
fulfillment_status: "fulfilled",
payment_status: "captured",
region: {
id: "test-region",
name: "Test region",
tax_rate: 12.5,
},
line_items: [
{
id: lineItemId1,
variant_id: product1.variants[0].id,
quantity: 1,
fulfilled_quantity: 1,
shipped_quantity: 1,
unit_price: 1000,
},
{
id: lineItemId2,
variant_id: product2.variants[0].id,
quantity: 1,
fulfilled_quantity: 1,
shipped_quantity: 1,
unit_price: 1000,
},
],
})
orderId = order.id
})
afterEach(async () => {
const db = useDb()
return await db.teardown()
})
it("creates and order edit", async () => {
const api = useApi()
const response = await api.post(
`/admin/order-edits/`,
{
order_id: orderId,
internal_note: "This is an internal note",
},
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data.order_edit).toEqual(
expect.objectContaining({
order_id: orderId,
created_by: "admin_user",
requested_by: null,
canceled_by: null,
confirmed_by: null,
internal_note: "This is an internal note",
items: expect.arrayContaining([
expect.objectContaining({
id: lineItemId1,
quantity: 1,
fulfilled_quantity: 1,
shipped_quantity: 1,
unit_price: 1000,
}),
expect.objectContaining({
id: lineItemId2,
quantity: 1,
fulfilled_quantity: 1,
shipped_quantity: 1,
unit_price: 1000,
}),
]),
shipping_total: 0,
gift_card_total: 0,
gift_card_tax_total: 0,
discount_total: 0,
tax_total: 0,
total: 2000,
subtotal: 2000,
})
)
})
it("throw an error if an active order edit already exists", async () => {
const api = useApi()
const payload = {
order_id: orderId,
internal_note: "This is an internal note",
}
await api.post(`/admin/order-edits/`, payload, adminHeaders)
const err = await api
.post(`/admin/order-edits/`, payload, adminHeaders)
.catch((e) => e)
expect(err.message).toBe("Request failed with status code 400")
expect(err.response.data.message).toBe(
`An active order edit already exists for the order ${payload.order_id}`
)
})
})
})
@@ -114,14 +114,16 @@ describe("[MEDUSA_FF_ORDER_EDITING] /store/order-edits", () => {
await simpleLineItemFactory(dbConnection, {
id: lineItemUpdateId,
order_id: orderEdit.order_id,
order_id: null,
variant_id: product1.variants[0].id,
unit_price: 1000,
quantity: 2,
})
await simpleLineItemFactory(dbConnection, {
id: lineItemCreateId,
order_id: orderEdit.order_id,
order_id: null,
variant_id: product3.variants[0].id,
unit_price: 100,
quantity: 2,
})
@@ -168,6 +170,13 @@ describe("[MEDUSA_FF_ORDER_EDITING] /store/order-edits", () => {
removed_items: expect.arrayContaining([
expect.objectContaining({ id: lineItemId2, quantity: 1 }),
]),
shipping_total: 0,
gift_card_total: 0,
gift_card_tax_total: 0,
discount_total: 0,
tax_total: 0,
total: 2200,
subtotal: 2200,
})
)
@@ -1,6 +1,7 @@
import {
AdminOrdersEditsRes,
AdminOrderEditDeleteRes
AdminOrderEditDeleteRes,
AdminOrderEditsRes,
AdminPostOrderEditsReq,
} from "@medusajs/medusa"
import { ResponsePromise } from "../../typings"
import BaseResource from "../base"
@@ -9,11 +10,19 @@ class AdminOrderEditsResource extends BaseResource {
retrieve(
id: string,
customHeaders: Record<string, any> = {}
): ResponsePromise<AdminOrdersEditsRes> {
): ResponsePromise<AdminOrderEditsRes> {
const path = `/admin/order-edits/${id}`
return this.client.request("GET", path, undefined, {}, customHeaders)
}
create(
payload: AdminPostOrderEditsReq,
customHeaders: Record<string, any> = {}
): ResponsePromise<AdminOrderEditsRes> {
const path = `/admin/order-edits`
return this.client.request("POST", path, payload, {}, customHeaders)
}
delete(
id: string,
customHeaders: Record<string, any> = {}
@@ -1664,6 +1664,18 @@ export const adminHandlers = [
)
}),
rest.post("/admin/order-edits/", (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
order_edit: {
...fixtures.get("order_edit"),
...(req.body as any),
},
})
)
}),
rest.get("/store/order-edits/:id", (req, res, ctx) => {
const { id } = req.params
return res(
@@ -1,11 +1,29 @@
import {
AdminOrderEditDeleteRes,
AdminOrderEditsRes,
AdminPostOrderEditsReq,
} from "@medusajs/medusa"
import { Response } from "@medusajs/medusa-js"
import { useMutation, UseMutationOptions, useQueryClient } from "react-query"
import { adminOrderEditsKeys } from "."
import { useMedusa } from "../../../contexts/medusa"
import { buildOptions } from "../../utils/buildOptions"
import { useMedusa } from "../../../contexts"
export const useAdminCreateOrderEdit = (
options?: UseMutationOptions<
Response<AdminOrderEditsRes>,
Error,
AdminPostOrderEditsReq
>
) => {
const { client } = useMedusa()
const queryClient = useQueryClient()
return useMutation(
(payload: AdminPostOrderEditsReq) =>
client.admin.orderEdits.create(payload),
buildOptions(queryClient, adminOrderEditsKeys.lists(), options)
)
}
export const useAdminDeleteOrderEdit = (
id: string,
@@ -1,4 +1,4 @@
import { AdminOrdersEditsRes } from "@medusajs/medusa"
import { AdminOrderEditsRes } from "@medusajs/medusa"
import { queryKeysFactory } from "../../utils"
import { UseQueryOptionsWrapper } from "../../../types"
import { Response } from "@medusajs/medusa-js"
@@ -13,7 +13,7 @@ type OrderEditQueryKeys = typeof adminOrderEditsKeys
export const useAdminOrderEdit = (
id: string,
options?: UseQueryOptionsWrapper<
Response<AdminOrdersEditsRes>,
Response<AdminOrderEditsRes>,
Error,
ReturnType<OrderEditQueryKeys["detail"]>
>
@@ -1,7 +1,10 @@
import { useAdminDeleteOrderEdit } from "../../../../src/"
import {
useAdminCreateOrderEdit,
useAdminDeleteOrderEdit,
} from "../../../../src/"
import { renderHook } from "@testing-library/react-hooks"
import { fixtures } from "../../../../mocks/data"
import { createWrapper } from "../../../utils"
import { fixtures } from "../../../../mocks/data"
describe("useAdminDelete hook", () => {
test("Deletes an order edit", async () => {
@@ -11,7 +14,6 @@ describe("useAdminDelete hook", () => {
})
result.current.mutate()
await waitFor(() => result.current.isSuccess)
expect(result.current.data.response.status).toEqual(200)
@@ -24,3 +26,30 @@ describe("useAdminDelete hook", () => {
)
})
})
describe("useAdminCreateOrderEdit hook", () => {
test("Created an order edit", async () => {
const { result, waitFor } = renderHook(() => useAdminCreateOrderEdit(), {
wrapper: createWrapper(),
})
const payload = {
order_id: "ord_1",
internal_note: "This is an internal note",
}
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,
},
})
)
})
})
@@ -0,0 +1,48 @@
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"
describe("POST /admin/order-edits", () => {
describe("successfully create an order edit", () => {
const orderId = IdMap.getId("order-edit-order-id-test")
const internalNote = "test internal note"
let subject
beforeAll(async () => {
subject = await request("POST", "/admin/order-edits", {
payload: {
order_id: orderId,
internal_note: internalNote,
},
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
flags: [OrderEditingFeatureFlag],
})
})
afterAll(async () => {
jest.clearAllMocks()
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("calls order edit service create", () => {
expect(orderEditServiceMock.create).toHaveBeenCalledTimes(1)
expect(orderEditServiceMock.create).toHaveBeenCalledWith(
{
order_id: orderId,
internal_note: internalNote,
},
{
loggedInUserId: IdMap.getId("admin_user"),
}
)
})
})
})
@@ -0,0 +1,96 @@
import { Request, Response } from "express"
import { OrderEditService } from "../../../../services"
import { IsOptional, IsString } from "class-validator"
import { EntityManager } from "typeorm"
/**
* @oas [post] /order-edits
* operationId: "PostOrderEdits"
* summary: "Create an OrderEdit"
* description: "Created a OrderEdit."
* 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.create({ order_id, internal_note })
* .then(({ order_edit }) => {
* console.log(order_edit.id);
* });
* - lang: Shell
* label: cURL
* source: |
* curl --location --request POST 'https://medusa-url.com/admin/order-edits' \
* --header 'Authorization: Bearer {api_token}'
* -d '{ "order_id": "my_order_id", "internal_note": "my_optional_note" }'
* 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 manager = req.scope.resolve("manager") as EntityManager
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 { items } = await orderEditServiceTx.computeLineItems(orderEdit.id)
orderEdit.items = items
orderEdit.removed_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
})
return res.json({ order_edit: orderEdit })
}
export class AdminPostOrderEditsReq {
@IsString()
order_id: string
@IsOptional()
@IsString()
internal_note?: string
}
@@ -60,9 +60,19 @@ export default async (req: Request, res: Response) => {
const retrieveConfig = req.retrieveConfig
const orderEdit = await orderEditService.retrieve(id, retrieveConfig)
const { items, removedItems } = await orderEditService.computeLineItems(id)
orderEdit.items = items
orderEdit.removed_items = removedItems
const totals = await orderEditService.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 res.json({ order_edit: orderEdit })
}
@@ -1,6 +1,9 @@
import { Router } from "express"
import middlewares, { transformQuery } from "../../../middlewares"
import { EmptyQueryParams } from "../../../../types/common"
import middlewares, {
transformBody,
transformQuery,
} from "../../../middlewares"
import { DeleteResponse, EmptyQueryParams } from "../../../../types/common"
import { isFeatureFlagEnabled } from "../../../middlewares/feature-flag-enabled"
import OrderEditingFeatureFlag from "../../../../loaders/feature-flags/order-editing"
import {
@@ -8,7 +11,7 @@ import {
defaultOrderEditRelations,
} from "../../../../types/order-edit"
import { OrderEdit } from "../../../../models"
import { DeleteResponse } from "../../../../types/common"
import { AdminPostOrderEditsReq } from "./create-order-edit"
const route = Router()
@@ -19,6 +22,12 @@ export default (app) => {
route
)
route.post(
"/",
transformBody(AdminPostOrderEditsReq),
middlewares.wrap(require("./create-order-edit").default)
)
route.get(
"/:id",
transformQuery(EmptyQueryParams, {
@@ -34,7 +43,9 @@ export default (app) => {
return app
}
export type AdminOrdersEditsRes = {
export type AdminOrderEditsRes = {
order_edit: OrderEdit
}
export type AdminOrderEditDeleteRes = DeleteResponse
export * from "./create-order-edit"
@@ -54,9 +54,19 @@ export default async (req: Request, res: Response) => {
const retrieveConfig = req.retrieveConfig
const orderEdit = await orderEditService.retrieve(id, retrieveConfig)
const { items, removedItems } = await orderEditService.computeLineItems(id)
orderEdit.items = items
orderEdit.removed_items = removedItems
const totals = await orderEditService.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 res.json({ order_edit: orderEdit })
}
+20 -4
View File
@@ -73,10 +73,14 @@ export class OrderEdit extends SoftDeletableEntity {
canceled_at?: Date
// Computed
subtotal: number
discount_total?: number
tax_total: number
shipping_total: number
discount_total: number
tax_total: number | null
total: number
subtotal: number
gift_card_total: number
gift_card_tax_total: number
difference_due: number
status: OrderEditStatus
@@ -168,12 +172,24 @@ export class OrderEdit extends SoftDeletableEntity {
* type: string
* subtotal:
* type: integer
* description: The subtotal for line items computed from changes.
* description: The total of subtotal
* example: 8000
* discount_total:
* type: integer
* description: The total of discount
* example: 800
* shipping_total:
* type: integer
* description: The total of the shipping amount
* example: 800
* gift_card_total:
* type: integer
* description: The total of the gift card amount
* example: 800
* gift_card_tax_total:
* type: integer
* description: The total of the gift card tax amount
* example: 800
* tax_total:
* type: integer
* description: The total of tax
@@ -2,16 +2,22 @@ import { IdMap } from "medusa-test-utils"
import { MedusaError } from "medusa-core-utils"
export const LineItemServiceMock = {
withTransaction: function() {
withTransaction: function () {
return this
},
create: jest.fn().mockImplementation(data => {
list: jest.fn().mockImplementation((data) => {
return Promise.resolve([])
}),
retrieve: jest.fn().mockImplementation((data) => {
return Promise.resolve({})
}),
create: jest.fn().mockImplementation((data) => {
return Promise.resolve({ ...data })
}),
update: jest.fn().mockImplementation(data => {
update: jest.fn().mockImplementation((data) => {
return Promise.resolve({ ...data })
}),
validate: jest.fn().mockImplementation(data => {
validate: jest.fn().mockImplementation((data) => {
if (data.title === "invalid lineitem") {
throw new Error(`"content" is required`)
}
@@ -30,7 +30,38 @@ export const orderEditServiceMock = {
return Promise.resolve(undefined)
}),
computeLineItems: jest.fn().mockImplementation((orderEdit) => {
return Promise.resolve(orderEdit)
return Promise.resolve({
items: [
{
id: IdMap.getId("existingLine"),
title: "merge line",
description: "This is a new line",
thumbnail: "test-img-yeah.com/thumb",
content: {
unit_price: 123,
variant: {
id: IdMap.getId("can-cover"),
},
product: {
id: IdMap.getId("validId"),
},
quantity: 1,
},
quantity: 10,
},
],
removedItems: [],
})
}),
create: jest.fn().mockImplementation((data, context) => {
return Promise.resolve({
order_id: data.order_id,
internal_note: data.internal_note,
created_by: context.loggedInUserId,
})
}),
getTotals: jest.fn().mockImplementation(() => {
return Promise.resolve({})
}),
delete: jest.fn().mockImplementation((_) => {
return Promise.resolve()
@@ -1,7 +1,16 @@
import { IdMap, MockManager, MockRepository } from "medusa-test-utils"
import { OrderEditService, OrderService } from "../index"
import {
EventBusService,
LineItemService,
OrderEditService,
OrderService,
TotalsService,
} from "../index"
import { OrderEditItemChangeType } from "../../models"
import { OrderServiceMock } from "../__mocks__/order"
import { EventBusServiceMock } from "../__mocks__/event-bus"
import { LineItemServiceMock } from "../__mocks__/line-item"
import { TotalsServiceMock } from "../__mocks__/totals"
const orderEditWithChanges = {
id: IdMap.getId("order-edit-with-changes"),
@@ -48,6 +57,25 @@ const orderEditWithChanges = {
],
}
const lineItemServiceMock = {
...LineItemServiceMock,
list: jest.fn().mockImplementation(() => {
return Promise.resolve([
{
id: IdMap.getId("line-item-1"),
},
{
id: IdMap.getId("line-item-2"),
},
])
}),
retrieve: jest.fn().mockImplementation((id) => {
return Promise.resolve({
id,
})
}),
}
describe("OrderEditService", () => {
const orderEditRepository = MockRepository({
findOneWithRelations: (relations, query) => {
@@ -57,12 +85,21 @@ describe("OrderEditService", () => {
return {}
},
create: (data) => {
return {
...orderEditWithChanges,
...data,
}
},
})
const orderEditService = new OrderEditService({
manager: MockManager,
orderEditRepository,
orderService: OrderServiceMock as unknown as OrderService,
eventBusService: EventBusServiceMock as unknown as EventBusService,
totalsService: TotalsServiceMock as unknown as TotalsService,
lineItemService: lineItemServiceMock as unknown as LineItemService,
})
it("should retrieve an order edit and call the repository with the right arguments", async () => {
@@ -77,12 +114,10 @@ describe("OrderEditService", () => {
})
it("should compute the items from the changes and attach them to the orderEdit", async () => {
const orderEdit = await orderEditService.retrieve(
const { items, removedItems } = await orderEditService.computeLineItems(
IdMap.getId("order-edit-with-changes")
)
const { items, removedItems } = await orderEditService.computeLineItems(
orderEdit.id
)
expect(items.length).toBe(2)
expect(items).toEqual(
expect.arrayContaining([
@@ -104,4 +139,26 @@ describe("OrderEditService", () => {
])
)
})
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"),
internal_note: "internal note",
}
await orderEditService.create(data, {
loggedInUserId: IdMap.getId("admin_user"),
})
expect(orderEditRepository.create).toHaveBeenCalledTimes(1)
expect(orderEditRepository.create).toHaveBeenCalledWith({
order_id: data.order_id,
internal_note: data.internal_note,
created_by: IdMap.getId("admin_user"),
})
expect(EventBusServiceMock.emit).toHaveBeenCalledTimes(1)
expect(EventBusServiceMock.emit).toHaveBeenCalledWith(
OrderEditService.Events.CREATED,
{ id: expect.any(String) }
)
})
})
+185 -42
View File
@@ -5,30 +5,49 @@ import { MedusaError } from "medusa-core-utils"
import { OrderEditRepository } from "../repositories/order-edit"
import {
LineItem,
Order,
OrderEdit,
OrderEditItemChangeType,
OrderEditStatus,
OrderItemChange,
} from "../models"
import { TransactionBaseService } from "../interfaces"
import { OrderService } from "./index"
import {
EventBusService,
LineItemService,
OrderService,
TotalsService,
} from "./index"
import { CreateOrderEditInput } from "../types/order-edit"
type InjectedDependencies = {
manager: EntityManager
orderEditRepository: typeof OrderEditRepository
orderService: OrderService
eventBusService: EventBusService
totalsService: TotalsService
lineItemService: LineItemService
}
export default class OrderEditService extends TransactionBaseService {
static readonly Events = {
CREATED: "order-edit.created",
}
protected transactionManager_: EntityManager | undefined
protected readonly manager_: EntityManager
protected readonly orderEditRepository_: typeof OrderEditRepository
protected readonly orderService_: OrderService
protected readonly lineItemService_: LineItemService
protected readonly eventBusService_: EventBusService
protected readonly totalsService_: TotalsService
constructor({
manager,
orderEditRepository,
orderService,
lineItemService,
eventBusService,
totalsService,
}: InjectedDependencies) {
// eslint-disable-next-line prefer-rest-params
super(arguments[0])
@@ -36,13 +55,17 @@ export default class OrderEditService extends TransactionBaseService {
this.manager_ = manager
this.orderEditRepository_ = orderEditRepository
this.orderService_ = orderService
this.lineItemService_ = lineItemService
this.eventBusService_ = eventBusService
this.totalsService_ = totalsService
}
async retrieve(
orderEditId: string,
config: FindConfig<OrderEdit> = {}
): Promise<OrderEdit | never> {
const orderEditRepository = this.manager_.getCustomRepository(
const manager = this.transactionManager_ ?? this.manager_
const orderEditRepository = manager.getCustomRepository(
this.orderEditRepository_
)
const { relations, ...query } = buildQuery({ id: orderEditId }, config)
@@ -62,58 +85,178 @@ export default class OrderEditService extends TransactionBaseService {
return orderEdit
}
protected async retrieveActive(
orderId: string,
config: FindConfig<OrderEdit> = {}
): Promise<OrderEdit | undefined> {
const manager = this.transactionManager_ ?? this.manager_
const orderEditRepository = manager.getCustomRepository(
this.orderEditRepository_
)
const query = buildQuery({ order_id: orderId }, config)
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", "order"],
relations: [
"changes",
"changes.line_item",
"changes.original_line_item",
"order",
"order.items",
],
select: ["id", "order_id", "changes"],
relations: ["changes", "changes.original_line_item", "changes.line_item"],
})
const originalItems = orderEdit.order.items
const removedItems: LineItem[] = []
const items: LineItem[] = []
const orderEditRemovedItemsMap: Map<string, LineItem> = new Map()
const orderEditUpdatedItemsMap: Map<string, LineItem> = new Map()
const updatedItems = orderEdit.changes
.map((itemChange) => {
if (itemChange.type === OrderEditItemChangeType.ITEM_ADD) {
items.push(itemChange.line_item as LineItem)
return
}
for (const change of orderEdit.changes) {
const lineItemId =
change.type === OrderEditItemChangeType.ITEM_REMOVE
? change.original_line_item_id!
: change.line_item_id!
if (itemChange.type === OrderEditItemChangeType.ITEM_REMOVE) {
removedItems.push({
...itemChange.original_line_item,
id: itemChange.original_line_item_id,
} as LineItem)
return
}
return [itemChange.original_line_item_id as string, itemChange]
const lineItem = await lineItemServiceTx.retrieve(lineItemId!, {
relations: ["tax_lines", "adjustments"],
})
.filter((change) => !!change) as [string, OrderItemChange][]
const orderEditUpdatedChangesMap: Map<string, OrderItemChange> = new Map(
updatedItems
)
originalItems.map((item) => {
const itemChange = orderEditUpdatedChangesMap.get(item.id)
if (itemChange) {
items.push({
...itemChange.line_item,
id: itemChange.original_line_item_id,
} as LineItem)
if (change.type === OrderEditItemChangeType.ITEM_REMOVE) {
orderEditRemovedItemsMap.set(change.original_line_item_id!, lineItem)
continue
}
})
return { items, removedItems }
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
*/
async getTotals(orderEditId: string): Promise<{
shipping_total: number
gift_card_total: number
gift_card_tax_total: number
discount_total: number
tax_total: number | null
subtotal: number
total: number
}> {
const manager = this.transactionManager_ ?? this.manager_
const { order_id } = await this.retrieve(orderEditId, {
select: ["order_id"],
})
const order = await this.orderService_
.withTransaction(manager)
.retrieve(order_id, {
relations: [
"discounts",
"discounts.rule",
"gift_cards",
"region",
"region.tax_rates",
"shipping_methods",
"shipping_methods.tax_lines",
],
})
const { items } = await this.computeLineItems(orderEditId)
const computedOrder = { ...order, items } as Order
const totalsServiceTx = this.totalsService_.withTransaction(manager)
const shipping_total = await totalsServiceTx.getShippingTotal(computedOrder)
const { total: gift_card_total, tax_total: gift_card_tax_total } =
await totalsServiceTx.getGiftCardTotal(computedOrder)
const discount_total = await totalsServiceTx.getDiscountTotal(computedOrder)
const tax_total = await totalsServiceTx.getTaxTotal(computedOrder)
const subtotal = await totalsServiceTx.getSubtotal(computedOrder)
const total = await totalsServiceTx.getTotal(computedOrder)
return {
shipping_total,
gift_card_total,
gift_card_tax_total,
discount_total,
tax_total,
subtotal,
total,
}
}
async create(
data: CreateOrderEditInput,
context: { loggedInUserId: string }
): Promise<OrderEdit> {
return await this.atomicPhase_(async (transactionManager) => {
const activeOrderEdit = await this.retrieveActive(data.order_id)
if (activeOrderEdit) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`An active order edit already exists for the order ${data.order_id}`
)
}
const orderEditRepository = transactionManager.getCustomRepository(
this.orderEditRepository_
)
const orderEditToCreate = orderEditRepository.create({
order_id: data.order_id,
internal_note: data.internal_note,
created_by: context.loggedInUserId,
})
const orderEdit = await orderEditRepository.save(orderEditToCreate)
await this.eventBusService_
.withTransaction(transactionManager)
.emit(OrderEditService.Events.CREATED, { id: orderEdit.id })
return orderEdit
})
}
async delete(orderEditId: string): Promise<void> {
+18 -29
View File
@@ -11,7 +11,6 @@ import {
Order,
OrderStatus,
Payment,
PaymentSession,
PaymentStatus,
Return,
Swap,
@@ -164,9 +163,8 @@ class OrderService extends TransactionBaseService {
const orderRepo = this.manager_.getCustomRepository(this.orderRepository_)
const query = buildQuery(selector, config)
const { select, relations, totalsToSelect } = this.transformQueryForTotals(
config
)
const { select, relations, totalsToSelect } =
this.transformQueryForTotals(config)
if (select && select.length) {
query.select = select
@@ -234,9 +232,8 @@ class OrderService extends TransactionBaseService {
}
}
const { select, relations, totalsToSelect } = this.transformQueryForTotals(
config
)
const { select, relations, totalsToSelect } =
this.transformQueryForTotals(config)
if (select && select.length) {
query.select = select
@@ -254,9 +251,7 @@ class OrderService extends TransactionBaseService {
return [orders, count]
}
protected transformQueryForTotals(
config: FindConfig<Order>
): {
protected transformQueryForTotals(config: FindConfig<Order>): {
relations: string[] | undefined
select: FindConfig<Order>["select"]
totalsToSelect: FindConfig<Order>["select"]
@@ -337,9 +332,8 @@ class OrderService extends TransactionBaseService {
): Promise<Order> {
const orderRepo = this.manager_.getCustomRepository(this.orderRepository_)
const { select, relations, totalsToSelect } = this.transformQueryForTotals(
config
)
const { select, relations, totalsToSelect } =
this.transformQueryForTotals(config)
const query = {
where: { id: orderId },
@@ -378,9 +372,8 @@ class OrderService extends TransactionBaseService {
): Promise<Order> {
const orderRepo = this.manager_.getCustomRepository(this.orderRepository_)
const { select, relations, totalsToSelect } = this.transformQueryForTotals(
config
)
const { select, relations, totalsToSelect } =
this.transformQueryForTotals(config)
const query = {
where: { cart_id: cartId },
@@ -418,9 +411,8 @@ class OrderService extends TransactionBaseService {
): Promise<Order> {
const orderRepo = this.manager_.getCustomRepository(this.orderRepository_)
const { select, relations, totalsToSelect } = this.transformQueryForTotals(
config
)
const { select, relations, totalsToSelect } =
this.transformQueryForTotals(config)
const query = {
where: { external_id: externalId },
@@ -858,9 +850,8 @@ class OrderService extends TransactionBaseService {
.withTransaction(manager)
.createShippingMethod(optionId, data ?? {}, { order, ...config })
const shippingOptionServiceTx = this.shippingOptionService_.withTransaction(
manager
)
const shippingOptionServiceTx =
this.shippingOptionService_.withTransaction(manager)
const methods = [newMethod]
if (shipping_methods.length) {
@@ -1031,9 +1022,8 @@ class OrderService extends TransactionBaseService {
await inventoryServiceTx.adjustInventory(item.variant_id, item.quantity)
}
const paymentProviderServiceTx = this.paymentProviderService_.withTransaction(
manager
)
const paymentProviderServiceTx =
this.paymentProviderService_.withTransaction(manager)
for (const p of order.payments) {
await paymentProviderServiceTx.cancelPayment(p)
}
@@ -1073,9 +1063,8 @@ class OrderService extends TransactionBaseService {
)
}
const paymentProviderServiceTx = this.paymentProviderService_.withTransaction(
manager
)
const paymentProviderServiceTx =
this.paymentProviderService_.withTransaction(manager)
const payments: Payment[] = []
for (const p of order.payments) {
@@ -1228,7 +1217,7 @@ class OrderService extends TransactionBaseService {
const fulfillments = await this.fulfillmentService_
.withTransaction(manager)
.createFulfillment(
(order as unknown) as CreateFulfillmentOrder,
order as unknown as CreateFulfillmentOrder,
itemsToFulfill,
{
metadata,
+13 -13
View File
@@ -243,11 +243,12 @@ class TotalsService extends TransactionBaseService {
TaxInclusivePricingFeatureFlag.key
) && shippingMethod.includes_tax
totals.original_tax_total = await this.taxCalculationStrategy_.calculate(
[],
totals.tax_lines,
calculationContext
)
totals.original_tax_total =
await this.taxCalculationStrategy_.calculate(
[],
totals.tax_lines,
calculationContext
)
totals.tax_total = totals.original_tax_total
if (includesTax) {
@@ -904,11 +905,12 @@ class TotalsService extends TransactionBaseService {
calculationContext
)
calculationContext.allocation_map = {} // Don't account for discounts
lineItemTotals.original_tax_total = await this.taxCalculationStrategy_.calculate(
[lineItem],
lineItemTotals.tax_lines,
calculationContext
)
lineItemTotals.original_tax_total =
await this.taxCalculationStrategy_.calculate(
[lineItem],
lineItemTotals.tax_lines,
calculationContext
)
if (
this.featureFlagRouter_.isFeatureEnabled(
@@ -986,9 +988,7 @@ class TotalsService extends TransactionBaseService {
* @param cartOrOrder - the cart or order to get gift card amount for
* @return the gift card amount applied to the cart or order
*/
async getGiftCardTotal(
cartOrOrder: Cart | Order
): Promise<{
async getGiftCardTotal(cartOrOrder: Cart | Order): Promise<{
total: number
tax_total: number
}> {
+5
View File
@@ -22,3 +22,8 @@ export const defaultOrderEditFields: (keyof OrderEdit)[] = [
"canceled_at",
"internal_note",
]
export type CreateOrderEditInput = {
order_id: string
internal_note?: string
}