Files
medusa-store/integration-tests/api/__tests__/admin/order-edit.js
Frane Polić 884f36e8a8 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
2022-09-28 11:51:13 +00:00

2133 lines
62 KiB
JavaScript

const path = require("path")
const startServerWithEnvironment =
require("../../../helpers/start-server-with-environment").default
const { useApi } = require("../../../helpers/use-api")
const { useDb } = require("../../../helpers/use-db")
const adminSeeder = require("../../helpers/admin-seeder")
const {
simpleOrderEditFactory,
} = require("../../factories/simple-order-edit-factory")
const { IdMap } = require("medusa-test-utils")
const {
simpleOrderItemChangeFactory,
} = require("../../factories/simple-order-item-change-factory")
const {
simpleLineItemFactory,
simpleProductFactory,
simpleOrderFactory,
simpleDiscountFactory,
simpleCartFactory,
simpleRegionFactory,
} = require("../../factories")
const { OrderEditItemChangeType, OrderEdit } = require("@medusajs/medusa")
jest.setTimeout(50000)
const adminHeaders = {
headers: {
Authorization: "Bearer test_token",
},
}
describe("[MEDUSA_FF_ORDER_EDITING] /admin/order-edits", () => {
let medusaProcess
let dbConnection
const adminUserId = "admin_user"
beforeAll(async () => {
const cwd = path.resolve(path.join(__dirname, "..", ".."))
const [process, connection] = await startServerWithEnvironment({
cwd,
env: { MEDUSA_FF_ORDER_EDITING: true },
verbose: false,
})
dbConnection = connection
medusaProcess = process
})
afterAll(async () => {
const db = useDb()
await db.shutdown()
medusaProcess.kill()
})
describe("GET /admin/order-edits/:id", () => {
const orderEditId = IdMap.getId("order-edit-1")
const prodId1 = IdMap.getId("prodId1")
const prodId2 = IdMap.getId("prodId2")
const prodId3 = IdMap.getId("prodId3")
const changeUpdateId = IdMap.getId("order-edit-1-change-update")
const changeCreateId = IdMap.getId("order-edit-1-change-create")
const changeRemoveId = IdMap.getId("order-edit-1-change-remove")
const lineItemId1 = IdMap.getId("line-item-1")
const lineItemId2 = IdMap.getId("line-item-2")
const lineItemCreateId = IdMap.getId("line-item-create")
const lineItemUpdateId = IdMap.getId("line-item-update")
beforeEach(async () => {
await adminSeeder(dbConnection)
const product1 = await simpleProductFactory(dbConnection, {
id: prodId1,
})
const product2 = await simpleProductFactory(dbConnection, {
id: prodId2,
})
const product3 = await simpleProductFactory(dbConnection, {
id: prodId3,
})
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,
tax_lines: [
{
rate: 10,
code: "code1",
name: "code1",
},
],
},
{
id: lineItemId2,
variant_id: product2.variants[0].id,
quantity: 1,
fulfilled_quantity: 1,
shipped_quantity: 1,
unit_price: 1000,
tax_lines: [
{
rate: 10,
code: "code2",
name: "code2",
},
],
},
],
})
const orderEdit = await simpleOrderEditFactory(dbConnection, {
id: orderEditId,
order_id: order.id,
created_by: "admin_user",
internal_note: "test internal note",
})
await simpleLineItemFactory(dbConnection, {
id: lineItemUpdateId,
order_id: null,
variant_id: product1.variants[0].id,
unit_price: 1000,
quantity: 2,
})
await simpleLineItemFactory(dbConnection, {
id: lineItemCreateId,
order_id: null,
variant_id: product3.variants[0].id,
unit_price: 100,
quantity: 2,
})
await simpleOrderItemChangeFactory(dbConnection, {
id: changeCreateId,
type: OrderEditItemChangeType.ITEM_ADD,
line_item_id: lineItemCreateId,
order_edit_id: orderEdit.id,
})
await simpleOrderItemChangeFactory(dbConnection, {
id: changeUpdateId,
type: OrderEditItemChangeType.ITEM_UPDATE,
line_item_id: lineItemUpdateId,
original_line_item_id: lineItemId1,
order_edit_id: orderEdit.id,
})
await simpleOrderItemChangeFactory(dbConnection, {
id: changeRemoveId,
type: OrderEditItemChangeType.ITEM_REMOVE,
original_line_item_id: lineItemId2,
order_edit_id: orderEdit.id,
})
})
afterEach(async () => {
const db = useDb()
return await db.teardown()
})
it("gets order edit", async () => {
const api = useApi()
const response = await api.get(
`/admin/order-edits/${orderEditId}`,
adminHeaders
)
expect(response.data.order_edit).toEqual(
expect.objectContaining({
id: orderEditId,
created_by: "admin_user",
requested_by: null,
canceled_by: null,
confirmed_by: null,
internal_note: "test internal note",
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: 0,
subtotal: 0,
})
)
expect(response.status).toEqual(200)
})
})
describe("DELETE /admin/order-edits/:id", () => {
beforeEach(async () => {
await adminSeeder(dbConnection)
})
afterEach(async () => {
const db = useDb()
return await db.teardown()
})
it("deletes order edit", async () => {
const { id } = await simpleOrderEditFactory(dbConnection, {
created_by: adminUserId,
})
const api = useApi()
const response = await api.delete(
`/admin/order-edits/${id}`,
adminHeaders
)
const orderEdit = await dbConnection.manager.findOne(OrderEdit, {
where: { id },
withDeleted: true,
})
expect(orderEdit).toBeUndefined()
expect(response.status).toEqual(200)
expect(response.data).toEqual({
id,
object: "order_edit",
deleted: true,
})
})
it("deletes already removed order edit", async () => {
const { id } = await simpleOrderEditFactory(dbConnection, {
created_by: adminUserId,
})
const api = useApi()
const response = await api.delete(
`/admin/order-edits/${id}`,
adminHeaders
)
const idempontentResponse = await api.delete(
`/admin/order-edits/${id}`,
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data).toEqual({
id,
object: "order_edit",
deleted: true,
})
expect(idempontentResponse.status).toEqual(200)
expect(idempontentResponse.data).toEqual({
id,
object: "order_edit",
deleted: true,
})
})
test.each([
[
"requested",
{
requested_at: new Date(),
requested_by: adminUserId,
},
],
[
"confirmed",
{
confirmed_at: new Date(),
confirmed_by: adminUserId,
},
],
[
"declined",
{
declined_at: new Date(),
declined_by: adminUserId,
},
],
[
"canceled",
{
canceled_at: new Date(),
canceled_by: adminUserId,
},
],
])("fails to delete order edit with status %s", async (status, data) => {
expect.assertions(2)
const { id } = await simpleOrderEditFactory(dbConnection, {
created_by: adminUserId,
...data,
})
const api = useApi()
await api
.delete(`/admin/order-edits/${id}`, adminHeaders)
.catch((err) => {
expect(err.response.status).toEqual(400)
expect(err.response.data.message).toEqual(
`Cannot delete order edit with status ${status}`
)
})
})
})
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")
const confirmedOrderEditId = IdMap.getId("confirmed-order-edit")
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,
tax_lines: [
{
item_id: lineItemId1,
rate: 10,
code: "default",
name: "default",
},
],
},
{
id: lineItemId2,
variant_id: product2.variants[0].id,
quantity: 1,
fulfilled_quantity: 1,
shipped_quantity: 1,
unit_price: 1000,
tax_lines: [
{
item_id: lineItemId2,
rate: 10,
code: "default",
name: "default",
},
],
},
],
})
orderId = order.id
})
afterEach(async () => {
const db = useDb()
return await db.teardown()
})
it("creates an 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",
// The items are cloned from the items of the order
items: expect.arrayContaining([
expect.objectContaining({
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: 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: 200,
subtotal: 2000,
total: 2200,
})
)
})
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}`
)
})
it("created a new order edit if none are active anymore", async () => {
const api = useApi()
await simpleOrderEditFactory(dbConnection, {
id: confirmedOrderEditId,
order_id: orderId,
internal_note: "test",
confirmed_at: new Date(),
created_by: "admin_user",
})
const payload = {
order_id: orderId,
internal_note: "This is an internal note",
}
const response = await api.post(
`/admin/order-edits/`,
payload,
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",
// The items are cloned from the items of the order
items: expect.arrayContaining([
expect.objectContaining({
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: 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: 200,
subtotal: 2000,
total: 2200,
})
)
})
})
describe("POST /admin/order-edits/:id/request", () => {
let orderEditId
let orderEditIdNoChanges
beforeEach(async () => {
await adminSeeder(dbConnection)
const product1 = await simpleProductFactory(dbConnection)
const { id, order_id } = await simpleOrderEditFactory(dbConnection, {
created_by: "admin_user",
})
const noChangesEdit = await simpleOrderEditFactory(dbConnection, {
created_by: "admin_user",
})
await simpleLineItemFactory(dbConnection, {
order_id: order_id,
variant_id: product1.variants[0].id,
})
await simpleOrderItemChangeFactory(dbConnection, {
order_edit_id: id,
type: "item_add",
})
orderEditId = id
orderEditIdNoChanges = noChangesEdit.id
})
afterEach(async () => {
const db = useDb()
return await db.teardown()
})
it("requests order edit", async () => {
const api = useApi()
const result = await api.post(
`/admin/order-edits/${orderEditId}/request`,
{},
adminHeaders
)
expect(result.status).toEqual(200)
expect(result.data.order_edit).toEqual(
expect.objectContaining({
id: orderEditId,
requested_at: expect.any(String),
requested_by: "admin_user",
status: "requested",
})
)
})
it("fails to request an order edit with no changes", async () => {
expect.assertions(2)
const api = useApi()
try {
await api.post(
`/admin/order-edits/${orderEditIdNoChanges}/request`,
{},
adminHeaders
)
} catch (err) {
expect(err.response.status).toEqual(400)
expect(err.response.data.message).toEqual(
"Cannot request a confirmation on an edit with no changes"
)
}
})
it("requests order edit", async () => {
const api = useApi()
const result = await api
.post(`/admin/order-edits/${orderEditId}/request`, {}, adminHeaders)
.catch((err) => console.log(err))
expect(result.status).toEqual(200)
expect(result.data.order_edit).toEqual(
expect.objectContaining({
id: orderEditId,
requested_at: expect.any(String),
requested_by: "admin_user",
status: "requested",
})
)
})
})
describe("POST /admin/order-edits/:id", () => {
let orderEditId
const prodId1 = IdMap.getId("prodId1")
const lineItemId1 = IdMap.getId("line-item-1")
const orderId1 = IdMap.getId("order-id-1")
beforeEach(async () => {
await adminSeeder(dbConnection)
const product1 = await simpleProductFactory(dbConnection, {
id: prodId1,
})
const order = await simpleOrderFactory(dbConnection, {
id: orderId1,
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,
tax_lines: [
{
rate: 10,
code: "code1",
name: "code1",
},
],
},
],
})
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 () => {
const db = useDb()
return await db.teardown()
})
it("updates an order edit", async () => {
const api = useApi()
const response = await api.post(
`/admin/order-edits/${orderEditId}`,
{ internal_note: "changed note" },
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,
internal_note: "changed note",
items: expect.arrayContaining([
expect.objectContaining({
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,
}),
]),
}),
]),
discount_total: 0,
gift_card_total: 0,
gift_card_tax_total: 0,
shipping_total: 0,
subtotal: 1000,
tax_total: 100,
total: 1100,
})
)
})
})
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")
const orderEditId = IdMap.getId("order-edit-1")
const orderEditId2 = IdMap.getId("order-edit-2")
const prodId1 = IdMap.getId("prodId1")
const lineItemId1 = IdMap.getId("line-item-1")
const changeUpdateId = IdMap.getId("order-id-1-change-1")
const changeUpdateId2 = IdMap.getId("order-id-1-change-2")
const lineItemUpdateId = IdMap.getId("line-item-1-update")
const lineItemUpdateId2 = IdMap.getId("line-item-1-update-2")
beforeEach(async () => {
await adminSeeder(dbConnection)
product = await simpleProductFactory(dbConnection, {
id: prodId1,
})
await simpleOrderFactory(dbConnection, {
id: orderId1,
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: product.variants[0].id,
quantity: 1,
fulfilled_quantity: 1,
shipped_quantity: 1,
unit_price: 1000,
},
],
})
await simpleLineItemFactory(dbConnection, {
id: lineItemUpdateId,
order_id: null,
variant_id: product.variants[0].id,
unit_price: 100,
quantity: 2,
})
await simpleLineItemFactory(dbConnection, {
id: lineItemUpdateId2,
order_id: null,
variant_id: product.variants[0].id,
unit_price: 100,
quantity: 2,
})
})
afterEach(async () => {
const db = useDb()
return await db.teardown()
})
it("deletes an item change from an order edit", async () => {
await simpleLineItemFactory(dbConnection, {
id: lineItemUpdateId,
order_id: null,
variant_id: product.variants[0].id,
unit_price: 100,
quantity: 2,
})
await simpleOrderEditFactory(dbConnection, {
id: orderEditId,
order_id: orderId1,
created_by: "admin_user",
internal_note: "test internal note",
})
await simpleOrderItemChangeFactory(dbConnection, {
id: changeUpdateId,
type: OrderEditItemChangeType.ITEM_UPDATE,
line_item_id: lineItemUpdateId,
original_line_item_id: lineItemId1,
order_edit_id: orderEditId,
})
const api = useApi()
let res = await api.get(`/admin/order-edits/${orderEditId}`, adminHeaders)
expect(res.status).toEqual(200)
expect(res.data.order_edit.changes.length).toBe(1)
res = await api.delete(
`/admin/order-edits/${orderEditId}/changes/${changeUpdateId}`,
adminHeaders
)
expect(res.status).toEqual(200)
expect(res.data).toEqual(
expect.objectContaining({
id: changeUpdateId,
object: "item_change",
deleted: true,
})
)
res = await api.get(`/admin/order-edits/${orderEditId}`, adminHeaders)
expect(res.status).toEqual(200)
expect(res.data.order_edit.changes.length).toBe(0)
})
it("return invalid error if the item change does not belong to the order edit", async () => {
await simpleOrderEditFactory(dbConnection, {
id: orderEditId,
order_id: orderId1,
created_by: "admin_user",
internal_note: "test internal note 2",
})
await simpleOrderItemChangeFactory(dbConnection, {
id: changeUpdateId,
type: OrderEditItemChangeType.ITEM_UPDATE,
line_item_id: lineItemUpdateId,
original_line_item_id: lineItemId1,
order_edit_id: orderEditId,
})
await simpleOrderEditFactory(dbConnection, {
id: orderEditId2,
order_id: orderId1,
created_by: "admin_user",
internal_note: "test internal note 2",
})
await simpleOrderItemChangeFactory(dbConnection, {
id: changeUpdateId2,
type: OrderEditItemChangeType.ITEM_UPDATE,
line_item_id: lineItemUpdateId2,
original_line_item_id: lineItemId1,
order_edit_id: orderEditId2,
})
const api = useApi()
const response = await api
.delete(
`/admin/order-edits/${orderEditId}/changes/${changeUpdateId2}`,
adminHeaders
)
.catch((e) => e)
expect(response.response.status).toEqual(400)
expect(response.response.data.message).toEqual(
`The item change you are trying to delete doesn't belong to the OrderEdit with id: ${orderEditId}.`
)
})
it("return an error if the order edit is confirmed", async () => {
await simpleOrderEditFactory(dbConnection, {
id: orderEditId,
order_id: orderId1,
created_by: "admin_user",
internal_note: "test internal note 3",
confirmed_at: new Date(),
})
await simpleOrderItemChangeFactory(dbConnection, {
id: changeUpdateId,
type: OrderEditItemChangeType.ITEM_UPDATE,
line_item_id: lineItemUpdateId,
original_line_item_id: lineItemId1,
order_edit_id: orderEditId,
})
const api = useApi()
const response = await api
.delete(
`/admin/order-edits/${orderEditId}/changes/${changeUpdateId}`,
adminHeaders
)
.catch((e) => e)
expect(response.response.status).toEqual(400)
expect(response.response.data.message).toEqual(
"Cannot delete and item change from a confirmed order edit"
)
})
it("return an error if the order edit is canceled", async () => {
await simpleOrderEditFactory(dbConnection, {
id: orderEditId,
order_id: orderId1,
created_by: "admin_user",
internal_note: "test internal note 4",
canceled_at: new Date(),
})
await simpleOrderItemChangeFactory(dbConnection, {
id: changeUpdateId,
type: OrderEditItemChangeType.ITEM_UPDATE,
line_item_id: lineItemUpdateId,
original_line_item_id: lineItemId1,
order_edit_id: orderEditId,
})
const api = useApi()
const response = await api
.delete(
`/admin/order-edits/${orderEditId}/changes/${changeUpdateId}`,
adminHeaders
)
.catch((e) => e)
expect(response.response.status).toEqual(400)
expect(response.response.data.message).toEqual(
"Cannot delete and item change from a canceled order edit"
)
})
})
describe("POST /admin/order-edits/:id", () => {
const cancellableEditId = IdMap.getId("order-edit-1")
const canceledEditId = IdMap.getId("order-edit-2")
const confirmedEditId = IdMap.getId("order-edit-3")
beforeEach(async () => {
await adminSeeder(dbConnection)
await simpleOrderEditFactory(dbConnection, {
id: cancellableEditId,
created_by: "admin_user",
internal_note: "test internal note",
})
await simpleOrderEditFactory(dbConnection, {
id: canceledEditId,
canceled_at: new Date(),
canceled_by: "admin_user",
created_by: "admin_user",
})
await simpleOrderEditFactory(dbConnection, {
id: confirmedEditId,
confirmed_at: new Date(),
confirmed_by: "admin_user",
created_by: "admin_user",
internal_note: "test internal note",
})
})
afterEach(async () => {
const db = useDb()
return await db.teardown()
})
it("cancels an order edit", async () => {
const api = useApi()
const response = await api.post(
`/admin/order-edits/${cancellableEditId}/cancel`,
{},
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data.order_edit).toEqual(
expect.objectContaining({
id: cancellableEditId,
created_by: "admin_user",
canceled_by: "admin_user",
canceled_at: expect.any(String),
status: "canceled",
})
)
})
it("cancels an already cancelled order edit", async () => {
expect.assertions(2)
const api = useApi()
const response = await api.post(
`/admin/order-edits/${canceledEditId}/cancel`,
{},
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data.order_edit).toEqual(
expect.objectContaining({
id: canceledEditId,
created_by: "admin_user",
canceled_by: "admin_user",
canceled_at: expect.any(String),
status: "canceled",
})
)
})
it("cancels an already cancelled order edit", async () => {
expect.assertions(2)
const api = useApi()
try {
await api.post(
`/admin/order-edits/${confirmedEditId}/cancel`,
{},
adminHeaders
)
} catch (err) {
expect(err.response.status).toEqual(400)
expect(err.response.data.message).toEqual(
"Cannot cancel order edit with status confirmed"
)
}
})
})
describe("POST /admin/order-edits/:id/items/:item_id", () => {
let product, product2
const orderId = IdMap.getId("order-1")
const prodId1 = IdMap.getId("product-1")
const prodId2 = IdMap.getId("product-2")
const lineItemId1 = IdMap.getId("line-item-1")
const lineItemId2 = IdMap.getId("line-item-2")
beforeEach(async () => {
await adminSeeder(dbConnection)
product = await simpleProductFactory(dbConnection, {
id: prodId1,
})
product2 = await simpleProductFactory(dbConnection, {
id: prodId2,
})
await simpleOrderFactory(dbConnection, {
id: orderId,
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: product.variants[0].id,
quantity: 1,
fulfilled_quantity: 1,
shipped_quantity: 1,
unit_price: 1000,
tax_lines: [
{
item_id: lineItemId1,
rate: 12.5,
code: "default",
name: "default",
},
],
},
{
id: lineItemId2,
variant_id: product2.variants[0].id,
quantity: 1,
fulfilled_quantity: 1,
shipped_quantity: 1,
unit_price: 1000,
tax_lines: [
{
item_id: lineItemId2,
rate: 12.5,
code: "default",
name: "default",
},
],
},
],
})
})
afterEach(async () => {
const db = useDb()
return await db.teardown()
})
it("creates an order edit item change of type update on line item update", async () => {
const api = useApi()
const {
data: { order_edit },
} = await api.post(
`/admin/order-edits/`,
{
order_id: orderId,
internal_note: "This is an internal note",
},
adminHeaders
)
const orderEditId = order_edit.id
const updateItemId = order_edit.items.find(
(item) => item.original_item_id === lineItemId1
).id
const response = await api.post(
`/admin/order-edits/${orderEditId}/items/${updateItemId}`,
{ quantity: 2 },
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data.order_edit.changes).toHaveLength(1)
expect(response.data.order_edit).toEqual(
expect.objectContaining({
id: orderEditId,
changes: expect.arrayContaining([
expect.objectContaining({
id: expect.any(String),
created_at: expect.any(String),
updated_at: expect.any(String),
deleted_at: null,
type: "item_update",
order_edit_id: orderEditId,
original_line_item_id: lineItemId1,
line_item_id: expect.any(String),
line_item: expect.objectContaining({
id: expect.any(String),
created_at: expect.any(String),
updated_at: expect.any(String),
original_item_id: lineItemId1,
order_edit_id: orderEditId,
cart_id: null,
order_id: null,
swap_id: null,
claim_order_id: null,
title: expect.any(String),
description: "",
thumbnail: "",
is_return: false,
is_giftcard: false,
should_merge: true,
allow_discounts: true,
has_shipping: null,
unit_price: 1000,
variant_id: expect.any(String),
quantity: 2,
fulfilled_quantity: 1,
returned_quantity: null,
shipped_quantity: 1,
metadata: null,
variant: expect.any(Object),
}),
original_line_item: expect.objectContaining({
id: lineItemId1,
created_at: expect.any(String),
updated_at: expect.any(String),
cart_id: null,
order_id: orderId,
swap_id: null,
claim_order_id: null,
title: expect.any(String),
description: "",
thumbnail: "",
is_return: false,
is_giftcard: false,
should_merge: true,
allow_discounts: true,
has_shipping: null,
unit_price: 1000,
variant_id: expect.any(String),
quantity: 1,
fulfilled_quantity: 1,
returned_quantity: null,
shipped_quantity: 1,
metadata: null,
variant: expect.any(Object),
}),
}),
]),
status: "created",
order_id: orderId,
internal_note: "This is an internal note",
created_by: "admin_user",
items: expect.arrayContaining([
expect.objectContaining({
id: expect.any(String),
original_item_id: lineItemId1,
order_edit_id: orderEditId,
cart_id: null,
order_id: null,
swap_id: null,
claim_order_id: null,
title: expect.any(String),
is_return: false,
is_giftcard: false,
should_merge: true,
allow_discounts: true,
has_shipping: null,
unit_price: 1000,
variant_id: expect.any(String),
quantity: 2,
fulfilled_quantity: 1,
returned_quantity: null,
shipped_quantity: 1,
metadata: null,
tax_lines: expect.arrayContaining([
expect.objectContaining({
rate: 12.5,
name: "default",
code: "default",
}),
]),
}),
expect.objectContaining({
id: expect.any(String),
original_item_id: lineItemId2,
order_edit_id: orderEditId,
cart_id: null,
order_id: null,
swap_id: null,
claim_order_id: null,
title: expect.any(String),
is_return: false,
is_giftcard: false,
should_merge: true,
allow_discounts: true,
has_shipping: null,
unit_price: 1000,
variant_id: expect.any(String),
quantity: 1,
fulfilled_quantity: 1,
returned_quantity: null,
shipped_quantity: 1,
metadata: null,
tax_lines: expect.arrayContaining([
expect.objectContaining({
rate: 12.5,
name: "default",
code: "default",
}),
]),
}),
]),
discount_total: 0,
gift_card_total: 0,
gift_card_tax_total: 0,
shipping_total: 0,
subtotal: 3000,
tax_total: 375,
total: 3375,
})
)
})
it("update an exising order edit item change of type update on multiple line item update", async () => {
const api = useApi()
const {
data: { order_edit },
} = await api.post(
`/admin/order-edits/`,
{
order_id: orderId,
internal_note: "This is an internal note",
},
adminHeaders
)
const orderEditId = order_edit.id
const updateItemId = order_edit.items.find(
(item) => item.original_item_id === lineItemId1
).id
await api.post(
`/admin/order-edits/${orderEditId}/items/${updateItemId}`,
{ quantity: 2 },
adminHeaders
)
const response = await api.post(
`/admin/order-edits/${orderEditId}/items/${updateItemId}`,
{ quantity: 3 },
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data.order_edit.changes).toHaveLength(1)
expect(response.data.order_edit).toEqual(
expect.objectContaining({
id: orderEditId,
changes: expect.arrayContaining([
expect.objectContaining({
id: expect.any(String),
created_at: expect.any(String),
updated_at: expect.any(String),
deleted_at: null,
type: "item_update",
order_edit_id: orderEditId,
original_line_item_id: lineItemId1,
line_item_id: expect.any(String),
line_item: expect.objectContaining({
id: expect.any(String),
created_at: expect.any(String),
updated_at: expect.any(String),
original_item_id: lineItemId1,
order_edit_id: orderEditId,
cart_id: null,
order_id: null,
swap_id: null,
claim_order_id: null,
title: expect.any(String),
description: "",
thumbnail: "",
is_return: false,
is_giftcard: false,
should_merge: true,
allow_discounts: true,
has_shipping: null,
unit_price: 1000,
variant_id: expect.any(String),
quantity: 3,
fulfilled_quantity: 1,
returned_quantity: null,
shipped_quantity: 1,
metadata: null,
variant: expect.any(Object),
}),
original_line_item: expect.objectContaining({
id: lineItemId1,
created_at: expect.any(String),
updated_at: expect.any(String),
cart_id: null,
order_id: orderId,
swap_id: null,
claim_order_id: null,
title: expect.any(String),
description: "",
thumbnail: "",
is_return: false,
is_giftcard: false,
should_merge: true,
allow_discounts: true,
has_shipping: null,
unit_price: 1000,
variant_id: expect.any(String),
quantity: 1,
fulfilled_quantity: 1,
returned_quantity: null,
shipped_quantity: 1,
metadata: null,
variant: expect.any(Object),
}),
}),
]),
status: "created",
order_id: orderId,
internal_note: "This is an internal note",
created_by: "admin_user",
items: expect.arrayContaining([
expect.objectContaining({
id: expect.any(String),
original_item_id: lineItemId1,
order_edit_id: orderEditId,
cart_id: null,
order_id: null,
swap_id: null,
claim_order_id: null,
title: expect.any(String),
is_return: false,
is_giftcard: false,
should_merge: true,
allow_discounts: true,
has_shipping: null,
unit_price: 1000,
variant_id: expect.any(String),
quantity: 3,
fulfilled_quantity: 1,
returned_quantity: null,
shipped_quantity: 1,
metadata: null,
tax_lines: expect.arrayContaining([
expect.objectContaining({
rate: 12.5,
name: "default",
code: "default",
}),
]),
}),
expect.objectContaining({
id: expect.any(String),
original_item_id: lineItemId2,
order_edit_id: orderEditId,
cart_id: null,
order_id: null,
swap_id: null,
claim_order_id: null,
title: expect.any(String),
is_return: false,
is_giftcard: false,
should_merge: true,
allow_discounts: true,
has_shipping: null,
unit_price: 1000,
variant_id: expect.any(String),
quantity: 1,
fulfilled_quantity: 1,
returned_quantity: null,
shipped_quantity: 1,
metadata: null,
tax_lines: expect.arrayContaining([
expect.objectContaining({
rate: 12.5,
name: "default",
code: "default",
}),
]),
}),
]),
discount_total: 0,
gift_card_total: 0,
gift_card_tax_total: 0,
shipping_total: 0,
subtotal: 4000,
tax_total: 500,
total: 4500,
})
)
})
it("update an exising order edit item change of type update on multiple line item update with correct totals including discounts", async () => {
const api = useApi()
const region = await simpleRegionFactory(dbConnection, { tax_rate: 10 })
const discountCode = "FIX_DISCOUNT"
const discount = await simpleDiscountFactory(dbConnection, {
code: discountCode,
rule: {
type: "fixed",
allocation: "total",
value: 2000,
},
regions: [region.id],
})
const cart = await simpleCartFactory(dbConnection, {
email: "adrien@test.com",
region: region.id,
line_items: [
{
id: lineItemId1,
variant_id: product.variants[0].id,
quantity: 1,
unit_price: 1000,
},
{
id: lineItemId2,
variant_id: product2.variants[0].id,
quantity: 1,
unit_price: 1000,
},
],
})
await api.post(`/store/carts/${cart.id}`, {
discounts: [{ code: discountCode }],
})
await api.post(`/store/carts/${cart.id}/payment-sessions`)
const completeRes = await api.post(`/store/carts/${cart.id}/complete`)
const order = completeRes.data.data
const {
data: { order_edit },
} = await api.post(
`/admin/order-edits/`,
{
order_id: order.id,
internal_note: "This is an internal note",
},
adminHeaders
)
const orderEditId = order_edit.id
const updateItemId = order_edit.items.find(
(item) => item.original_item_id === lineItemId1
).id
let response = await api.post(
`/admin/order-edits/${orderEditId}/items/${updateItemId}`,
{ quantity: 2 },
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data.order_edit.changes).toHaveLength(1)
let item1 = response.data.order_edit.items.find(
(item) => item.original_item_id === lineItemId1
)
expect(item1.adjustments).toHaveLength(1)
let item2 = response.data.order_edit.items.find(
(item) => item.original_item_id === lineItemId2
)
expect(item2.adjustments).toHaveLength(1)
expect(response.data.order_edit).toEqual(
expect.objectContaining({
id: orderEditId,
changes: expect.arrayContaining([
expect.objectContaining({
id: expect.any(String),
created_at: expect.any(String),
updated_at: expect.any(String),
deleted_at: null,
type: "item_update",
order_edit_id: orderEditId,
original_line_item_id: lineItemId1,
line_item_id: expect.any(String),
}),
]),
status: "created",
order_id: order.id,
internal_note: "This is an internal note",
created_by: "admin_user",
items: expect.arrayContaining([
expect.objectContaining({
id: expect.any(String),
original_item_id: lineItemId1,
order_edit_id: orderEditId,
cart_id: null,
order_id: null,
swap_id: null,
claim_order_id: null,
title: expect.any(String),
is_return: false,
is_giftcard: false,
should_merge: true,
allow_discounts: true,
has_shipping: null,
unit_price: 1000,
variant_id: expect.any(String),
quantity: 2,
fulfilled_quantity: null,
returned_quantity: null,
shipped_quantity: null,
metadata: null,
tax_lines: expect.arrayContaining([
expect.objectContaining({
rate: 10,
}),
]),
adjustments: expect.arrayContaining([
expect.objectContaining({
discount_id: discount.id,
amount: 1333,
}),
]),
}),
expect.objectContaining({
id: expect.any(String),
original_item_id: lineItemId2,
order_edit_id: orderEditId,
cart_id: null,
order_id: null,
swap_id: null,
claim_order_id: null,
title: expect.any(String),
is_return: false,
is_giftcard: false,
should_merge: true,
allow_discounts: true,
has_shipping: null,
unit_price: 1000,
variant_id: expect.any(String),
quantity: 1,
fulfilled_quantity: null,
returned_quantity: null,
shipped_quantity: null,
metadata: null,
tax_lines: expect.arrayContaining([
expect.objectContaining({
rate: 10,
}),
]),
adjustments: expect.arrayContaining([
expect.objectContaining({
discount_id: discount.id,
amount: 667,
}),
]),
}),
]),
discount_total: 2000,
gift_card_total: 0,
gift_card_tax_total: 0,
shipping_total: 0,
subtotal: 3000,
tax_total: 100,
total: 1100,
})
)
response = await api.post(
`/admin/order-edits/${orderEditId}/items/${updateItemId}`,
{ quantity: 3 },
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data.order_edit.changes).toHaveLength(1)
item1 = response.data.order_edit.items.find(
(item) => item.original_item_id === lineItemId1
)
expect(item1.adjustments).toHaveLength(1)
item2 = response.data.order_edit.items.find(
(item) => item.original_item_id === lineItemId2
)
expect(item2.adjustments).toHaveLength(1)
expect(response.data.order_edit).toEqual(
expect.objectContaining({
id: orderEditId,
changes: expect.arrayContaining([
expect.objectContaining({
id: expect.any(String),
created_at: expect.any(String),
updated_at: expect.any(String),
deleted_at: null,
type: "item_update",
order_edit_id: orderEditId,
original_line_item_id: lineItemId1,
line_item_id: expect.any(String),
}),
]),
status: "created",
order_id: order.id,
internal_note: "This is an internal note",
created_by: "admin_user",
items: expect.arrayContaining([
expect.objectContaining({
id: expect.any(String),
original_item_id: lineItemId1,
order_edit_id: orderEditId,
cart_id: null,
order_id: null,
swap_id: null,
claim_order_id: null,
title: expect.any(String),
is_return: false,
is_giftcard: false,
should_merge: true,
allow_discounts: true,
has_shipping: null,
unit_price: 1000,
variant_id: expect.any(String),
quantity: 3,
fulfilled_quantity: null,
returned_quantity: null,
shipped_quantity: null,
metadata: null,
tax_lines: expect.arrayContaining([
expect.objectContaining({
rate: 10,
}),
]),
adjustments: expect.arrayContaining([
expect.objectContaining({
discount_id: discount.id,
amount: 1500,
}),
]),
}),
expect.objectContaining({
id: expect.any(String),
original_item_id: lineItemId2,
order_edit_id: orderEditId,
cart_id: null,
order_id: null,
swap_id: null,
claim_order_id: null,
title: expect.any(String),
is_return: false,
is_giftcard: false,
should_merge: true,
allow_discounts: true,
has_shipping: null,
unit_price: 1000,
variant_id: expect.any(String),
quantity: 1,
fulfilled_quantity: null,
returned_quantity: null,
shipped_quantity: null,
metadata: null,
tax_lines: expect.arrayContaining([
expect.objectContaining({
rate: 10,
}),
]),
adjustments: expect.arrayContaining([
expect.objectContaining({
discount_id: discount.id,
amount: 500,
}),
]),
}),
]),
discount_total: 2000,
gift_card_total: 0,
gift_card_tax_total: 0,
shipping_total: 0,
subtotal: 4000,
tax_total: 200,
total: 2200,
})
)
})
})
})