feat: line item adjustments (#1319)

* add: crud services + model + totals

* fix: enforce unique constraint on line item adjustment model and update service (#1241)

* add: unique constraint on model + fix service

* fix: unique constraint

* fix: add cascade on delete + fix discount relation

* fix: remove optional unique prop

* add: tests for ensuring line item adjustment db constraints (#1279)

* add: tests for ensuring db constraints

* fix: use given when then

* feat: adjust cart to include line item adjustments (#1242)

* fix: cart service + cart tests

* fix: remaining tests

* fix: swap tests

* fix: add relationship + fix oas

* refactor: applyDiscount

* fix: refactor applyDiscount and fix + add unit tests

* fix: plugins tests

* feat: line item adjustments draft orders (#1243)

* fix: draft order tests

* fix: constraint

* fix: wrong variable name

* fix: unique constraint

* progress: add tests

* fix: add cascade on delete + fix discount relation

* fix: remove optional unique prop

* fix: cart removeLineItem + tests

* fix: cart unit tests

* fix: update snapshot

* remove: verbose option

* rename arg

Co-authored-by: Sebastian Rindom <skrindom@gmail.com>

* add: create adjustments for swap additional_items

* add: create adjustments for return lines

* fix: unit test for creating adjustment for additional_items

* fix: create adjustments only for non return items + no deletion when item is a return item

* add: integration tests

* refactor: use refreshAdjustments method

* refactor test

Co-authored-by: Sebastian Rindom <skrindom@gmail.com>

Co-authored-by: Sebastian Rindom <skrindom@gmail.com>

Co-authored-by: Sebastian Rindom <skrindom@gmail.com>
This commit is contained in:
Zakaria El Asri
2022-04-12 15:49:31 +02:00
committed by GitHub
co-authored by Sebastian Rindom
parent 607a382b4e
commit 1cfeb5dbd8
45 changed files with 3290 additions and 581 deletions
@@ -297,6 +297,62 @@ describe("/admin/draft-orders", () => {
)
})
it("creates a draft order with discount and line item", async () => {
const api = useApi()
const payload = {
email: "oli@test.dk",
shipping_address: "oli-shipping",
discounts: [{ code: "TEST" }],
items: [
{
variant_id: "test-variant",
quantity: 2,
metadata: {},
},
],
region_id: "test-region",
customer_id: "oli-test",
shipping_methods: [
{
option_id: "test-option",
},
],
}
const response = await api
.post("/admin/draft-orders", payload, {
headers: {
Authorization: "Bearer test_token",
},
})
.catch((err) => {
console.log(err)
})
const draftOrder = response.data.draft_order
const lineItemId = draftOrder.cart.items[0].id
expect(response.status).toEqual(200)
expect(draftOrder.cart.items).toEqual(
expect.arrayContaining([
expect.objectContaining({
variant_id: "test-variant",
unit_price: 8000,
quantity: 2,
adjustments: expect.arrayContaining([
expect.objectContaining({
item_id: lineItemId,
amount: 1600,
description: "discount",
discount_id: "test-discount",
}),
]),
}),
])
)
})
it("creates a draft order with created shipping address", async () => {
const api = useApi()
@@ -1555,6 +1555,116 @@ describe("/admin/orders", () => {
expect(response.status).toEqual(200)
})
describe("Given an existing discount order", () => {
describe("When a store operator attemps to create a swap form the discount order", () => {
it("Then should successfully create the swap", async () => {
const api = useApi()
const response = await api.post(
"/admin/orders/test-order/swaps",
{
return_items: [
{
item_id: "test-item",
quantity: 1,
},
],
additional_items: [{ variant_id: "test-variant-2", quantity: 1 }],
},
{
headers: {
authorization: "Bearer test_token",
},
}
)
const swapCartId = response.data.order.swaps[0].cart_id
const swapCartRes = await api.get(`/store/carts/${swapCartId}`, {
headers: {
authorization: "Bearer test_token",
},
})
const cart = swapCartRes.data.cart
expect(response.status).toEqual(200)
expect(cart.items.length).toEqual(2)
expect(cart.items).toEqual(
expect.arrayContaining([
expect.objectContaining({
unit_price: -8000,
adjustments: [
expect.objectContaining({
amount: -800,
}),
],
}),
expect.objectContaining({
unit_price: 8000,
adjustments: [
expect.objectContaining({
amount: 800,
}),
],
}),
])
)
expect(cart.total).toEqual(0)
})
})
describe("And given a swap cart", () => {
describe("When a line item is added to the swap cart", () => {
it("Then should not delete existing return line item adjustments", async () => {
const api = useApi()
const createSwapRes = await api.post(
"/admin/orders/test-order/swaps",
{
return_items: [
{
item_id: "test-item",
quantity: 1,
},
],
additional_items: [{ variant_id: "test-variant", quantity: 1 }],
},
{
headers: {
authorization: "Bearer test_token",
},
}
)
const swapCartId = createSwapRes.data.order.swaps[0].cart_id
const response = await api.post(
`/store/carts/${swapCartId}/line-items`,
{
variant_id: "test-variant-2",
quantity: 1,
},
{
headers: {
authorization: "Bearer test_token",
},
}
)
const cart = response.data.cart
const items = cart.items
const [returnItem] = items.filter((i) => i.is_return)
expect(returnItem.adjustments).toEqual([
expect.objectContaining({
amount: -800,
}),
])
expect(cart.total).toBe(7200)
})
})
})
})
it("creates a swap with custom shipping options", async () => {
const api = useApi()
@@ -35,6 +35,7 @@ exports[`Claims creates a replace claim 1`] = `
Object {
"additional_items": Array [
Object {
"adjustments": Array [],
"allow_discounts": true,
"cart_id": null,
"claim_order_id": StringMatching /\\^claim_\\*/,
@@ -0,0 +1,222 @@
const path = require("path")
const { LineItemAdjustment } = require("@medusajs/medusa")
const setupServer = require("../../../helpers/setup-server")
const { useApi } = require("../../../helpers/use-api")
const { initDb, useDb } = require("../../../helpers/use-db")
const cartSeeder = require("../../helpers/cart-seeder")
const { simpleCartFactory, simpleLineItemFactory } = require("../../factories")
const {
simpleDiscountFactory,
} = require("../../factories/simple-discount-factory")
jest.setTimeout(30000)
describe("Line Item Adjustments", () => {
let dbConnection
let medusaProcess
const doAfterEach = async () => {
const db = useDb()
return await db.teardown()
}
beforeAll(async () => {
const cwd = path.resolve(path.join(__dirname, "..", ".."))
try {
dbConnection = await initDb({ cwd })
medusaProcess = await setupServer({ cwd })
} catch (error) {
console.log(error)
}
})
afterAll(async () => {
const db = useDb()
await db.shutdown()
medusaProcess.kill()
})
describe("Tests database constraints", () => {
let cart,
discount,
lineItemId = "line-test"
beforeEach(async () => {
try {
await cartSeeder(dbConnection)
discount = await simpleDiscountFactory(dbConnection, {
code: "MEDUSATEST",
id: "discount-test",
rule: {
value: 100,
type: "fixed",
allocation: "total",
},
regions: ["test-region"],
})
cart = await simpleCartFactory(
dbConnection,
{
customer: "test-customer",
id: "cart-test",
line_items: [
{
id: lineItemId,
variant_id: "test-variant",
cart_id: "cart-test",
unit_price: 1000,
quantity: 1,
adjustments: [
{
amount: 10,
discount_id: discount.id,
description: "discount",
item_id: lineItemId,
},
],
},
],
region: "test-region",
shipping_address: {
address_1: "test",
country_code: "us",
first_name: "chris",
last_name: "rock",
postal_code: "101",
},
shipping_methods: [
{
shipping_option: "test-option",
},
],
},
100
)
} catch (err) {
console.log(err)
throw err
}
})
afterEach(async () => {
await doAfterEach()
})
afterEach(async () => {
await doAfterEach()
})
describe("Given an existing line item, a discount, and a line item adjustment for both", () => {
describe("When creating an adjustment for another line item w. same discount", () => {
test("Then should create an adjustment", async () => {
const createLineItemWithAdjustment = async () => {
return await simpleLineItemFactory(dbConnection, {
id: "line-test-2",
variant_id: "test-variant-quantity",
cart_id: "test-cart",
unit_price: 1000,
quantity: 1,
adjustments: [
{
amount: 10,
discount_id: discount.id,
description: "discount",
item_id: "line-test-2",
},
],
})
}
expect(createLineItemWithAdjustment()).resolves.toEqual(
expect.anything()
)
})
})
describe("When creating an adjustment for another line item w. null discount", () => {
test("Then should create an adjustment", async () => {
const createAdjustmentNullDiscount = async () => {
return await dbConnection.manager.insert(LineItemAdjustment, {
id: "lia-1",
item_id: lineItemId,
amount: 35,
description: "custom discount",
discount_id: null,
})
}
expect(createAdjustmentNullDiscount()).resolves.toEqual(
expect.anything()
)
})
})
describe("When creating multiple adjustments w. a null discount_id", () => {
test("Then should create multiple adjustments", async () => {
const createAdjustmentsNullDiscount = async () => {
await dbConnection.manager.insert(LineItemAdjustment, {
id: "lia-1",
item_id: lineItemId,
amount: 35,
description: "custom discount",
discount_id: null,
})
return await dbConnection.manager.insert(LineItemAdjustment, {
id: "lia-2",
item_id: lineItemId,
amount: 100,
description: "custom discount",
discount_id: null,
})
}
expect(createAdjustmentsNullDiscount()).resolves.toEqual(
expect.anything()
)
})
})
describe("When creating an adjustment w. for same line item and different discount", () => {
test("Then should create an adjustment", async () => {
const createAdjustment = async () => {
await simpleDiscountFactory(dbConnection, {
code: "ANOTHER",
id: "discount-2",
rule: {
value: 10,
type: "percentage",
allocation: "item",
},
regions: ["test-region"],
})
return await dbConnection.manager.insert(LineItemAdjustment, {
id: "lia-1",
item_id: lineItemId,
amount: 10,
description: "discount",
discount_id: "discount-2",
})
}
expect(createAdjustment()).resolves.toEqual(expect.anything())
})
})
describe("When creating an adjustment w. existing line item and discount pair", () => {
test("Then should throw a duplicate error", async () => {
const createDuplicateAdjustment = async () =>
await dbConnection.manager.insert(LineItemAdjustment, {
id: "lia-1",
item_id: lineItemId,
amount: 20,
description: "discount",
discount_id: discount.id,
})
expect(createDuplicateAdjustment()).rejects.toEqual(
expect.objectContaining({ code: "23505" })
)
})
})
})
})
})
@@ -259,6 +259,14 @@ const createReturnableOrder = async (dbConnection, options) => {
variant_id: "test-variant",
quantity: 2,
unit_price: 1000,
adjustments: [
{
amount: 200,
discount_code: "TESTCODE",
description: "discount",
item_id: "test-item",
},
],
tax_lines: [
{
name: "default",
+557 -4
View File
@@ -16,7 +16,7 @@ const { initDb, useDb } = require("../../../helpers/use-db")
const cartSeeder = require("../../helpers/cart-seeder")
const productSeeder = require("../../helpers/product-seeder")
const swapSeeder = require("../../helpers/swap-seeder")
const { simpleCartFactory } = require("../../factories")
const { simpleCartFactory, simpleLineItemFactory } = require("../../factories")
const {
simpleDiscountFactory,
} = require("../../factories/simple-discount-factory")
@@ -235,6 +235,131 @@ describe("/store/carts", () => {
unit_price: 1000,
variant_id: "test-variant-quantity",
quantity: 1,
adjustments: [],
}),
])
})
it("adds line item to cart containing a total fixed discount", async () => {
const api = useApi()
const response = await api
.post(
"/store/carts/test-cart-w-total-fixed-discount/line-items",
{
variant_id: "test-variant-quantity",
quantity: 2,
},
{ withCredentials: true }
)
.catch((err) => console.log(err))
expect(response.data.cart.items).toEqual([
expect.objectContaining({
cart_id: "test-cart-w-total-fixed-discount",
unit_price: 1000,
variant_id: "test-variant-quantity",
quantity: 2,
adjustments: [
expect.objectContaining({
amount: 100,
discount_id: "total-fixed-100",
description: "discount",
}),
],
}),
])
})
it("adds line item to cart containing a total percentage discount", async () => {
const api = useApi()
const response = await api
.post(
"/store/carts/test-cart-w-total-percentage-discount/line-items",
{
variant_id: "test-variant-quantity",
quantity: 2,
},
{ withCredentials: true }
)
.catch((err) => console.log(err))
expect(response.data.cart.items).toEqual([
expect.objectContaining({
cart_id: "test-cart-w-total-percentage-discount",
unit_price: 1000,
variant_id: "test-variant-quantity",
quantity: 2,
adjustments: [
expect.objectContaining({
amount: 200,
discount_id: "10Percent",
description: "discount",
}),
],
}),
])
})
it("adds line item to cart containing an item fixed discount", async () => {
const api = useApi()
const response = await api
.post(
"/store/carts/test-cart-w-item-fixed-discount/line-items",
{
variant_id: "test-variant-quantity",
quantity: 2,
},
{ withCredentials: true }
)
.catch((err) => console.log(err))
expect(response.data.cart.items).toEqual([
expect.objectContaining({
cart_id: "test-cart-w-item-fixed-discount",
unit_price: 1000,
variant_id: "test-variant-quantity",
quantity: 2,
adjustments: [
expect.objectContaining({
amount: 400,
discount_id: "item-fixed-200",
description: "discount",
}),
],
}),
])
})
it("adds line item to cart containing an item percentage discount", async () => {
const api = useApi()
const response = await api
.post(
"/store/carts/test-cart-w-item-percentage-discount/line-items",
{
variant_id: "test-variant-quantity",
quantity: 2,
},
{ withCredentials: true }
)
.catch((err) => console.log(err))
expect(response.data.cart.items).toEqual([
expect.objectContaining({
cart_id: "test-cart-w-item-percentage-discount",
unit_price: 1000,
variant_id: "test-variant-quantity",
quantity: 2,
adjustments: [
expect.objectContaining({
amount: 300,
discount_id: "item-percentage-15",
description: "discount",
}),
],
}),
])
})
@@ -242,7 +367,6 @@ describe("/store/carts", () => {
it("adds line item to cart time limited sale", async () => {
const api = useApi()
// Add standard line item to cart
const response = await api
.post(
"/store/carts/test-cart/line-items",
@@ -305,7 +429,6 @@ describe("/store/carts", () => {
it("adds line item with quantity to cart with quantity discount", async () => {
const api = useApi()
// Add standard line item to cart
const response = await api
.post(
"/store/carts/test-cart/line-items",
@@ -330,7 +453,6 @@ describe("/store/carts", () => {
it("adds line item with quantity to cart with quantity discount no ceiling", async () => {
const api = useApi()
// Add standard line item to cart
const response = await api
.post(
"/store/carts/test-cart/line-items",
@@ -351,6 +473,421 @@ describe("/store/carts", () => {
}),
])
})
describe("ensures correct line item adjustment generation", () => {
const discountData = {
code: "MEDUSA185DKK",
id: "medusa-185",
rule: {
allocation: "total",
type: "fixed",
value: 185,
},
regions: ["test-region"],
}
let discountCart, discount
beforeEach(async () => {
try {
discount = await simpleDiscountFactory(
dbConnection,
discountData,
100
)
discountCart = await simpleCartFactory(
dbConnection,
{
id: "discount-cart",
customer: "test-customer",
region: "test-region",
shipping_address: {
address_1: "next door",
first_name: "lebron",
last_name: "james",
country_code: "dk",
postal_code: "100",
},
line_items: [
{
id: "test-li",
variant_id: "test-variant",
quantity: 1,
unit_price: 100,
adjustments: [
{
amount: 185,
description: "discount",
discount_id: "medusa-185",
},
],
},
],
shipping_methods: [
{
shipping_option: "test-option",
price: 1000,
},
],
},
100
)
await dbConnection.manager
.createQueryBuilder()
.relation(Cart, "discounts")
.of(discountCart)
.add(discount)
} catch (err) {
console.log(err)
}
})
afterEach(async () => {
await doAfterEach()
})
it("updates an old line item adjustment when a new line item is added to a discount cart", async () => {
const api = useApi()
const response = await api
.post(
"/store/carts/discount-cart/line-items",
{
quantity: 1,
variant_id: "test-variant-quantity",
},
{
withCredentials: true,
}
)
.catch((err) => console.log(err))
expect(response.data.cart.items.length).toEqual(2)
expect(response.data.cart.items).toEqual(
expect.arrayContaining([
expect.objectContaining({
adjustments: [
expect.objectContaining({
item_id: "test-li",
amount: 17,
discount_id: "medusa-185",
}),
],
}),
expect.objectContaining({
adjustments: [
expect.objectContaining({
amount: 168,
discount_id: "medusa-185",
}),
],
}),
])
)
})
it("updates an existing item adjustment when a line item is updated", async () => {
const api = useApi()
await simpleLineItemFactory(
dbConnection,
{
id: "line-item-2",
cart_id: discountCart.id,
variant_id: "test-variant-quantity",
unit_price: 950,
quantity: 1,
adjustments: [
{
id: "lia-2",
amount: 92,
description: "discount",
discount_id: "medusa-185",
},
],
},
100
)
const response = await api
.post(
"/store/carts/discount-cart/line-items/line-item-2",
{
quantity: 2,
},
{
withCredentials: true,
}
)
.catch((err) => console.log(err))
expect(response.data.cart.items.length).toEqual(2)
expect(response.data.cart.items).toEqual(
expect.arrayContaining([
expect.objectContaining({
adjustments: [
expect.objectContaining({
item_id: "test-li",
discount_id: "medusa-185",
amount: 9,
}),
],
}),
expect.objectContaining({
adjustments: [
expect.objectContaining({
item_id: "line-item-2",
amount: 176,
discount_id: "medusa-185",
}),
],
}),
])
)
})
it("updates an existing item adjustment when a line item is deleted from a discount cart", async () => {
const api = useApi()
await simpleLineItemFactory(
dbConnection,
{
id: "line-item-2",
cart_id: discountCart.id,
variant_id: "test-variant-quantity",
unit_price: 1000,
quantity: 1,
adjustments: [
{
id: "lia-2",
amount: 93,
description: "discount",
discount_id: "medusa-185",
},
],
},
100
)
const response = await api
.delete("/store/carts/discount-cart/line-items/test-li", {
withCredentials: true,
})
.catch((err) => console.log(err))
expect(response.data.cart.items.length).toEqual(1)
expect(response.data.cart.items).toEqual([
expect.objectContaining({
adjustments: [
expect.objectContaining({
item_id: "line-item-2",
amount: 185,
discount_id: "medusa-185",
}),
],
}),
])
})
})
})
describe("POST /store/carts/:id/line-items/:line_id", () => {
beforeEach(async () => {
try {
await cartSeeder(dbConnection)
await swapSeeder(dbConnection)
} catch (err) {
console.log(err)
throw err
}
})
afterEach(async () => {
await doAfterEach()
})
it("updates line item of cart", async () => {
const api = useApi()
const response = await api
.post(
"/store/carts/test-cart-3/line-items/test-item3/",
{
quantity: 3,
},
{ withCredentials: true }
)
.catch((err) => console.log(err))
expect(response.data.cart.items).toEqual([
expect.objectContaining({
cart_id: "test-cart-3",
unit_price: 8000,
variant_id: "test-variant-sale-cg",
quantity: 3,
adjustments: [],
}),
])
})
it("updates line item of a cart containing a total fixed discount", async () => {
const api = useApi()
await simpleLineItemFactory(dbConnection, {
id: "test-li-disc",
allow_discounts: true,
title: "Line Item Disc",
thumbnail: "https://test.js/1234",
unit_price: 1000,
quantity: 1,
variant_id: "test-variant-quantity",
cart_id: "test-cart-w-total-fixed-discount",
})
const response = await api
.post(
`store/carts/test-cart-w-total-fixed-discount/line-items/test-li-disc`,
{
quantity: 3,
},
{ withCredentials: true }
)
.catch((err) => console.log(err))
expect(response.data.cart.items).toEqual([
expect.objectContaining({
cart_id: "test-cart-w-total-fixed-discount",
unit_price: 1000,
variant_id: "test-variant-quantity",
quantity: 3,
adjustments: [
expect.objectContaining({
amount: 100,
discount_id: "total-fixed-100",
description: "discount",
}),
],
}),
])
})
it("updates line item of a cart containing a total percentage discount", async () => {
const api = useApi()
await simpleLineItemFactory(dbConnection, {
id: "test-li-disc",
allow_discounts: true,
title: "Line Item Disc",
thumbnail: "https://test.js/1234",
unit_price: 1000,
quantity: 1,
variant_id: "test-variant-quantity",
cart_id: "test-cart-w-total-percentage-discount",
})
const response = await api
.post(
"/store/carts/test-cart-w-total-percentage-discount/line-items/test-li-disc",
{
quantity: 10,
},
{ withCredentials: true }
)
.catch((err) => console.log(err))
expect(response.data.cart.items).toEqual([
expect.objectContaining({
cart_id: "test-cart-w-total-percentage-discount",
unit_price: 1000,
variant_id: "test-variant-quantity",
quantity: 10,
adjustments: [
expect.objectContaining({
amount: 1000,
discount_id: "10Percent",
description: "discount",
}),
],
}),
])
})
it("updates line item of a cart containing an item fixed discount", async () => {
const api = useApi()
await simpleLineItemFactory(dbConnection, {
id: "test-li-disc",
allow_discounts: true,
title: "Line Item Disc",
thumbnail: "https://test.js/1234",
unit_price: 1000,
quantity: 1,
variant_id: "test-variant-quantity",
cart_id: "test-cart-w-item-fixed-discount",
})
const response = await api
.post(
"/store/carts/test-cart-w-item-fixed-discount/line-items/test-li-disc",
{
quantity: 4,
},
{ withCredentials: true }
)
.catch((err) => console.log(err))
expect(response.data.cart.items).toEqual([
expect.objectContaining({
cart_id: "test-cart-w-item-fixed-discount",
unit_price: 1000,
variant_id: "test-variant-quantity",
quantity: 4,
adjustments: [
expect.objectContaining({
amount: 800,
discount_id: "item-fixed-200",
description: "discount",
}),
],
}),
])
})
it("updates line item of a cart containing an item percentage discount", async () => {
const api = useApi()
await simpleLineItemFactory(dbConnection, {
id: "test-li-disc",
allow_discounts: true,
title: "Line Item Disc",
thumbnail: "https://test.js/1234",
unit_price: 1000,
quantity: 1,
variant_id: "test-variant-quantity",
cart_id: "test-cart-w-item-percentage-discount",
})
const response = await api
.post(
"/store/carts/test-cart-w-item-percentage-discount/line-items/test-li-disc",
{
quantity: 3,
},
{ withCredentials: true }
)
.catch((err) => console.log(err))
expect(response.data.cart.items).toEqual([
expect.objectContaining({
cart_id: "test-cart-w-item-percentage-discount",
unit_price: 1000,
variant_id: "test-variant-quantity",
quantity: 3,
adjustments: [
expect.objectContaining({
amount: 450,
discount_id: "item-percentage-15",
description: "discount",
}),
],
}),
])
})
})
describe("POST /store/carts/:id", () => {
@@ -1202,6 +1739,22 @@ describe("/store/carts", () => {
.catch((err) => console.log(err))
// Ensure that the discount is only applied to the standard item
const itemId = cartWithGiftcard.data.cart.items[0].id
expect(cartWithGiftcard.data.cart.items).toEqual(
expect.arrayContaining([
expect.objectContaining({
variant_id: "test-variant",
quantity: 1,
adjustments: [
expect.objectContaining({
discount_id: "10Percent",
amount: 100,
item_id: itemId,
}),
],
}),
])
)
expect(cartWithGiftcard.data.cart.total).toBe(1900) // 1000 (giftcard) + 900 (standard item with 10% discount)
expect(cartWithGiftcard.data.cart.discount_total).toBe(100)
expect(cartWithGiftcard.status).toEqual(200)
@@ -1,6 +1,6 @@
import { Connection } from "typeorm"
import faker from "faker"
import { LineItem, LineItemTaxLine } from "@medusajs/medusa"
import { LineItem, LineItemAdjustment, LineItemTaxLine } from "@medusajs/medusa"
type TaxLineFactoryData = {
rate: number
@@ -8,6 +8,11 @@ type TaxLineFactoryData = {
name: string
}
type LineItemAdjustmentFactoryData = Omit<LineItemAdjustment, "discount_id"> & {
discount_id: string
discount_code: string
}
export type LineItemFactoryData = {
id?: string
cart_id?: string
@@ -24,6 +29,7 @@ export type LineItemFactoryData = {
shipped_quantity?: boolean
returned_quantity?: boolean
tax_lines?: TaxLineFactoryData[]
adjustments: LineItemAdjustmentFactoryData[]
}
export const simpleLineItemFactory = async (
@@ -63,6 +69,7 @@ export const simpleLineItemFactory = async (
fulfilled_quantity: data.fulfilled_quantity || null,
shipped_quantity: data.shipped_quantity || null,
returned_quantity: data.returned_quantity || null,
adjustments: data.adjustments,
})
const line = await manager.save(toSave)
@@ -101,7 +101,17 @@ export const simpleOrderFactory = async (
await simpleShippingMethodFactory(connection, { ...sm, order_id: order.id })
}
const items = data.line_items
const items = data.line_items.map((item) => {
let adjustments = item?.adjustments || []
return {
...item,
adjustments: adjustments.map((adj) => ({
...adj,
discount_id: discounts.find((d) => d.code === adj?.discount_code),
})),
}
})
for (const item of items) {
await simpleLineItemFactory(connection, { ...item, order_id: id })
}
@@ -144,6 +144,69 @@ module.exports = async (connection, data = {}) => {
tenPercent.rule = tenPercentRule
await manager.save(tenPercent)
const totalFixed100Rule = manager.create(DiscountRule, {
id: "total-fixed-100-rule",
description: "Fixed 100 total",
type: "fixed",
value: 100,
allocation: "total",
})
const totalFixed100 = manager.create(Discount, {
id: "total-fixed-100",
code: "FIXED100",
is_dynamic: false,
is_disabled: false,
starts_at: tenDaysAgo,
ends_at: tenDaysFromToday,
})
totalFixed100.regions = [r]
totalFixed100.rule = totalFixed100Rule
await manager.save(totalFixed100)
const itemFixed200Rule = manager.create(DiscountRule, {
id: "item-fixed-200-rule",
description: "Item 200 fixed",
type: "fixed",
value: 200,
allocation: "item",
})
const itemFixed200 = manager.create(Discount, {
id: "item-fixed-200",
code: "FIXED200",
is_dynamic: false,
is_disabled: false,
starts_at: tenDaysAgo,
ends_at: tenDaysFromToday,
})
itemFixed200.regions = [r]
itemFixed200.rule = itemFixed200Rule
await manager.save(itemFixed200)
const itemPerc15Rule = manager.create(DiscountRule, {
id: "item-percentage-15-rule",
description: "Item 15 percentage",
type: "percentage",
value: 15,
allocation: "item",
})
const itemPerc15 = manager.create(Discount, {
id: "item-percentage-15",
code: "15PERCENT",
is_dynamic: false,
is_disabled: false,
starts_at: tenDaysAgo,
ends_at: tenDaysFromToday,
})
itemPerc15.regions = [r]
itemPerc15.rule = itemPerc15Rule
await manager.save(itemPerc15)
const dUsageLimit = await manager.create(Discount, {
id: "test-discount-usage-limit",
code: "SPENT",
@@ -570,6 +633,74 @@ module.exports = async (connection, data = {}) => {
await manager.save(cart)
const cartWithTotalFixedDiscount = manager.create(Cart, {
id: "test-cart-w-total-fixed-discount",
customer_id: "some-customer",
email: "some-customer@email.com",
discounts: [totalFixed100],
shipping_address: {
id: "test-shipping-address",
first_name: "lebron",
country_code: "us",
},
region_id: "test-region",
currency_code: "usd",
items: [],
})
await manager.save(cartWithTotalFixedDiscount)
const cartWithItemFixedDiscount = manager.create(Cart, {
id: "test-cart-w-item-fixed-discount",
customer_id: "some-customer",
email: "some-customer@email.com",
discounts: [itemFixed200],
shipping_address: {
id: "test-shipping-address",
first_name: "lebron",
country_code: "us",
},
region_id: "test-region",
currency_code: "usd",
items: [],
})
await manager.save(cartWithItemFixedDiscount)
const cartWithTotalPercDiscount = manager.create(Cart, {
id: "test-cart-w-total-percentage-discount",
customer_id: "some-customer",
email: "some-customer@email.com",
discounts: [tenPercent],
shipping_address: {
id: "test-shipping-address",
first_name: "lebron",
country_code: "us",
},
region_id: "test-region",
currency_code: "usd",
items: [],
})
await manager.save(cartWithTotalPercDiscount)
const cartWithItemPercDiscount = manager.create(Cart, {
id: "test-cart-w-item-percentage-discount",
customer_id: "some-customer",
email: "some-customer@email.com",
discounts: [itemPerc15],
shipping_address: {
id: "test-shipping-address",
first_name: "lebron",
country_code: "us",
},
region_id: "test-region",
currency_code: "usd",
items: [],
})
await manager.save(cartWithItemPercDiscount)
const cart2 = manager.create(Cart, {
id: "test-cart-2",
customer_id: "some-customer",
@@ -173,6 +173,14 @@ module.exports = async (connection, data = {}) => {
quantity: 1,
variant_id: "test-variant",
order_id: "test-order",
adjustments: [
{
amount: 800,
discount_id: "test-discount",
description: "discount",
item_id: "test-item",
},
],
})
await manager.save(li)
+10 -1
View File
@@ -349,7 +349,7 @@ const createSwap = async (options, manager) => {
is_disabled: false,
rule: dRule,
})
await manager.save(discount)
let discountDb = await manager.save(discount)
const cart = manager.create(Cart, {
id: `${swapId}-cart`,
@@ -396,6 +396,14 @@ const createSwap = async (options, manager) => {
thumbnail: "https://test.js/1234",
unit_price: 8000,
quantity: 1,
adjustments: [
{
amount: -800,
description: "discount",
discount_id: discountDb.id,
item_id: `${swapId}-return-item-1`,
},
],
variant_id: "test-variant",
order_id: "order-with-swap",
cart_id: cart.id,
@@ -419,6 +427,7 @@ const createSwap = async (options, manager) => {
const return_item1 = manager.create(LineItem, {
...li,
is_return: true,
unit_price: -1 * li.unit_price,
})
+3 -3
View File
@@ -8,16 +8,16 @@
"build": "babel src -d dist --extensions \".ts,.js\""
},
"dependencies": {
"@medusajs/medusa": "1.2.1-dev-1648026403166",
"@medusajs/medusa": "1.2.1-dev-1649181615374",
"faker": "^5.5.3",
"medusa-interfaces": "1.2.1-dev-1648026403166",
"medusa-interfaces": "1.2.1-dev-1649181615374",
"typeorm": "^0.2.31"
},
"devDependencies": {
"@babel/cli": "^7.12.10",
"@babel/core": "^7.12.10",
"@babel/node": "^7.12.10",
"babel-preset-medusa-package": "1.1.19-dev-1648026403166",
"babel-preset-medusa-package": "1.1.19-dev-1649181615374",
"jest": "^26.6.3"
}
}
+35 -35
View File
@@ -1301,10 +1301,10 @@
"@jridgewell/resolve-uri" "^3.0.3"
"@jridgewell/sourcemap-codec" "^1.4.10"
"@medusajs/medusa-cli@1.2.1-dev-1648026403166":
version "1.2.1-dev-1648026403166"
resolved "http://localhost:4873/@medusajs%2fmedusa-cli/-/medusa-cli-1.2.1-dev-1648026403166.tgz#1dc2ad62ba11421d43357596c61eb0b76d3bc69e"
integrity sha512-TScUnBuxI5V5cB6gAAGeGOZU8lXYHIhozpelDcYibZqaHEIwMNs1ekzRYhfMjYowSFCMLlzCsHFUh+gqU/6WSw==
"@medusajs/medusa-cli@1.2.1-dev-1649181615374":
version "1.2.1-dev-1649181615374"
resolved "http://localhost:4873/@medusajs%2fmedusa-cli/-/medusa-cli-1.2.1-dev-1649181615374.tgz#1ea9014e3ec9813a52457b0d6e2fc6bb64d3bfd6"
integrity sha512-8m6Z1ZZqstZKaAaKoFS3v3IzI7BFhcBgpF+iCSRuJoXltQgzVQOAxXuPjkRoi+m1ZZ+Yi/YYEzKmNQ99vmXisQ==
dependencies:
"@babel/polyfill" "^7.8.7"
"@babel/runtime" "^7.9.6"
@@ -1322,8 +1322,8 @@
is-valid-path "^0.1.1"
joi-objectid "^3.0.1"
meant "^1.0.1"
medusa-core-utils "1.1.31-dev-1648026403166"
medusa-telemetry "0.0.11-dev-1648026403166"
medusa-core-utils "1.1.31-dev-1649181615374"
medusa-telemetry "0.0.11-dev-1649181615374"
netrc-parser "^3.1.6"
open "^8.0.6"
ora "^5.4.1"
@@ -1337,13 +1337,13 @@
winston "^3.3.3"
yargs "^15.3.1"
"@medusajs/medusa@1.2.1-dev-1648026403166":
version "1.2.1-dev-1648026403166"
resolved "http://localhost:4873/@medusajs%2fmedusa/-/medusa-1.2.1-dev-1648026403166.tgz#f6e8fab09ac73b38ed79e15d2080e7ecad80fd99"
integrity sha512-ufYVgcKo+bsAzSnZA3FVLmxseDwnSKkSbIdktQUSZGMJTRvN/+Wcqnjdf59oH11jqqBApJ8T77Nvd/ABasjTWw==
"@medusajs/medusa@1.2.1-dev-1649181615374":
version "1.2.1-dev-1649181615374"
resolved "http://localhost:4873/@medusajs%2fmedusa/-/medusa-1.2.1-dev-1649181615374.tgz#6a62f8628b84b47a8717e9e0c276f3a9c2e376ce"
integrity sha512-eiCGE6JqYuP7GCzTBGg5LI9U0uQ0wlsR+NuMZVEwldj+xc7qwMjBJwUA7gc58gBv6JesfMYj3VZmJComN4+7Bg==
dependencies:
"@hapi/joi" "^16.1.8"
"@medusajs/medusa-cli" "1.2.1-dev-1648026403166"
"@medusajs/medusa-cli" "1.2.1-dev-1649181615374"
"@types/lodash" "^4.14.168"
awilix "^4.2.3"
body-parser "^1.19.0"
@@ -1367,8 +1367,8 @@
joi "^17.3.0"
joi-objectid "^3.0.1"
jsonwebtoken "^8.5.1"
medusa-core-utils "1.1.31-dev-1648026403166"
medusa-test-utils "1.1.37-dev-1648026403166"
medusa-core-utils "1.1.31-dev-1649181615374"
medusa-test-utils "1.1.37-dev-1649181615374"
morgan "^1.9.1"
multer "^1.4.2"
passport "^0.4.0"
@@ -2010,10 +2010,10 @@ babel-preset-jest@^26.6.2:
babel-plugin-jest-hoist "^26.6.2"
babel-preset-current-node-syntax "^1.0.0"
babel-preset-medusa-package@1.1.19-dev-1648026403166:
version "1.1.19-dev-1648026403166"
resolved "http://localhost:4873/babel-preset-medusa-package/-/babel-preset-medusa-package-1.1.19-dev-1648026403166.tgz#e834a06c2283d5b8709d45de1e4ff559dfd23944"
integrity sha512-HjbSvyeLU9ez0dgBv4+SDX2/nXaskN5Bip7nF36F/fnufw/5FtJ28Lyw30VWz6i5Ci991Zmadfq6TNUZnKgNHw==
babel-preset-medusa-package@1.1.19-dev-1649181615374:
version "1.1.19-dev-1649181615374"
resolved "http://localhost:4873/babel-preset-medusa-package/-/babel-preset-medusa-package-1.1.19-dev-1649181615374.tgz#2f13d52fedd336ad4b4c0602b3bf4696d2d08db7"
integrity sha512-N4XL7rTmNM2W+iRR92xvU4bKadP25lY5QR3vndxTxsLNSgcR5tLjKLO/4j7AqiFvcthbE8cF1TcdECH5aJfSuA==
dependencies:
"@babel/plugin-proposal-class-properties" "^7.12.1"
"@babel/plugin-proposal-decorators" "^7.12.1"
@@ -5156,25 +5156,25 @@ media-typer@0.3.0:
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
medusa-core-utils@1.1.31-dev-1648026403166:
version "1.1.31-dev-1648026403166"
resolved "http://localhost:4873/medusa-core-utils/-/medusa-core-utils-1.1.31-dev-1648026403166.tgz#0f3bde6fc0a77d3027d80a4ca5e1a1ce2426ad21"
integrity sha512-B5eFj3dH8g/HgNd8p8ChwR4bpydl0dqyBJllyvqS1AnFpjW7BcXpO0ZPkdYrIHflQL9e2OiIq3A2Lm6ZsYCI1A==
medusa-core-utils@1.1.31-dev-1649181615374:
version "1.1.31-dev-1649181615374"
resolved "http://localhost:4873/medusa-core-utils/-/medusa-core-utils-1.1.31-dev-1649181615374.tgz#60416bba53eaba607d77ca36789aa23756b8db0f"
integrity sha512-w5nusocZweIrAFJ6sl4hD/mN+UtNjz39IIfXukekyJByg3wpv4P+vsW3XdrFTX5OLvKVxuzjl3B2zeZUmdgSKg==
dependencies:
joi "^17.3.0"
joi-objectid "^3.0.1"
medusa-interfaces@1.2.1-dev-1648026403166:
version "1.2.1-dev-1648026403166"
resolved "http://localhost:4873/medusa-interfaces/-/medusa-interfaces-1.2.1-dev-1648026403166.tgz#29742128c852c967a94df1e00f0c8dacccff6b86"
integrity sha512-VRnFIFqPeOd0TcPMlng1kIzjhT7AtJzNqIDmKDxeJeE1MqU89g6YME/yQfCPzTXKNMdwN+8Xx+E5gANNn5i1QA==
medusa-interfaces@1.2.1-dev-1649181615374:
version "1.2.1-dev-1649181615374"
resolved "http://localhost:4873/medusa-interfaces/-/medusa-interfaces-1.2.1-dev-1649181615374.tgz#0b664f4e3e8e61b67108a41c8f0f9dd58a947075"
integrity sha512-JRD773nZnxjn/2oNrgb/zXn+scBoNHpW97YQYW7+LFX1JBYafzyGQW3vWTFX4X+q08ehz4dc21CgoMeYco8yvQ==
dependencies:
medusa-core-utils "1.1.31-dev-1648026403166"
medusa-core-utils "1.1.31-dev-1649181615374"
medusa-telemetry@0.0.11-dev-1648026403166:
version "0.0.11-dev-1648026403166"
resolved "http://localhost:4873/medusa-telemetry/-/medusa-telemetry-0.0.11-dev-1648026403166.tgz#dd66a411a214b953f8b6673f59c0b02dd78ec99f"
integrity sha512-SH07C6rOQyx7bHH4/2TzmBgzgMY8fLiE7PH7dk6BGwY8bjx9w6DvF9CvhdwxFnFnzDTed/cjkE/cHNkQFkjSrQ==
medusa-telemetry@0.0.11-dev-1649181615374:
version "0.0.11-dev-1649181615374"
resolved "http://localhost:4873/medusa-telemetry/-/medusa-telemetry-0.0.11-dev-1649181615374.tgz#3f4c366ea8d0d0fdde9b289f7e771bb27adae56d"
integrity sha512-RMJR3/qlTb1nV05RnBnX1bNOvYyeuXf4owxLlfbWzKAZirWQ5LAC2GikEGGbHGbw7UgiLgQtu4Rnmg2Uye+VcA==
dependencies:
axios "^0.21.1"
axios-retry "^3.1.9"
@@ -5186,13 +5186,13 @@ medusa-telemetry@0.0.11-dev-1648026403166:
remove-trailing-slash "^0.1.1"
uuid "^8.3.2"
medusa-test-utils@1.1.37-dev-1648026403166:
version "1.1.37-dev-1648026403166"
resolved "http://localhost:4873/medusa-test-utils/-/medusa-test-utils-1.1.37-dev-1648026403166.tgz#f32f0a04d7d38f6a84e58a2937bd026767ea5d1f"
integrity sha512-VQCuRal14tl/ZLsN6Ueh+z3FBgQEXgsLLSJxfpsrmABKTgoHONhkoe7UyXxxUozXwPWWqqtHhy80zfCIlfHF+w==
medusa-test-utils@1.1.37-dev-1649181615374:
version "1.1.37-dev-1649181615374"
resolved "http://localhost:4873/medusa-test-utils/-/medusa-test-utils-1.1.37-dev-1649181615374.tgz#079c16a791d47c52072c6f0837d0a827208bc9cc"
integrity sha512-hj3iNZsIA01l7qAZrOgt+kT8PDkXKoW4CEL3bhVfIUEwsdv9jID7FGdTIN/7G3diioTypvrVcqRrp0uiWdgp+Q==
dependencies:
"@babel/plugin-transform-classes" "^7.9.5"
medusa-core-utils "1.1.31-dev-1648026403166"
medusa-core-utils "1.1.31-dev-1649181615374"
randomatic "^3.1.1"
merge-descriptors@1.0.1:
@@ -427,6 +427,7 @@ Object {
"idempotency_key": null,
"items": Array [
Object {
"adjustments": Array [],
"allow_discounts": true,
"cart_id": null,
"claim_order_id": null,
@@ -761,6 +762,7 @@ Object {
"idempotency_key": null,
"items": Array [
Object {
"adjustments": Array [],
"allow_discounts": true,
"cart_id": null,
"claim_order_id": null,
@@ -982,6 +984,7 @@ Object {
"idempotency_key": null,
"items": Array [
Object {
"adjustments": Array [],
"allow_discounts": true,
"cart_id": null,
"claim_order_id": null,
@@ -1248,6 +1251,7 @@ Object {
"idempotency_key": null,
"items": Array [
Object {
"adjustments": Array [],
"allow_discounts": true,
"cart_id": null,
"claim_order_id": null,
@@ -1569,6 +1573,7 @@ Object {
"idempotency_key": null,
"items": Array [
Object {
"adjustments": Array [],
"allow_discounts": true,
"cart_id": null,
"claim_order_id": null,
@@ -107,6 +107,7 @@ describe("medusa-plugin-sendgrid", () => {
},
items: [
{
adjustments: [],
created_at: expect.any(Date),
updated_at: expect.any(Date),
order_id: expect.any(String),
@@ -227,6 +228,7 @@ describe("medusa-plugin-sendgrid", () => {
},
items: [
{
adjustments: [],
created_at: expect.any(Date),
updated_at: expect.any(Date),
order_id: expect.any(String),
@@ -285,6 +287,7 @@ describe("medusa-plugin-sendgrid", () => {
},
items: [
{
adjustments: [],
created_at: expect.any(Date),
updated_at: expect.any(Date),
order_id: expect.any(String),
+5 -5
View File
@@ -8,18 +8,18 @@
"build": "babel src -d dist --extensions \".ts,.js\""
},
"dependencies": {
"@medusajs/medusa": "1.1.60-dev-1642337635041",
"@medusajs/medusa": "1.2.1-dev-1649009241281",
"faker": "^5.5.3",
"medusa-fulfillment-webshipper": "1.1.35-dev-1642337635041",
"medusa-interfaces": "1.1.32-dev-1642337635041",
"medusa-plugin-sendgrid": "1.1.36-dev-1642337635041",
"medusa-fulfillment-webshipper": "1.2.1-dev-1649009241281",
"medusa-interfaces": "1.2.1-dev-1649009241281",
"medusa-plugin-sendgrid": "1.2.1-dev-1649009241281",
"typeorm": "^0.2.31"
},
"devDependencies": {
"@babel/cli": "^7.12.10",
"@babel/core": "^7.12.10",
"@babel/node": "^7.12.10",
"babel-preset-medusa-package": "1.1.19-dev-1642337635041",
"babel-preset-medusa-package": "1.1.19-dev-1649009241281",
"jest": "^26.6.3"
}
}
+46 -46
View File
@@ -1268,10 +1268,10 @@
"@types/yargs" "^15.0.0"
chalk "^4.0.0"
"@medusajs/medusa-cli@1.1.25-dev-1642337635041":
version "1.1.25-dev-1642337635041"
resolved "http://localhost:4873/@medusajs%2fmedusa-cli/-/medusa-cli-1.1.25-dev-1642337635041.tgz#e3ca33f453446dddc4922f42b4254995de9874b5"
integrity sha512-8pA5iBMglgrdPRSEZLvjovJczib5zk9qlQJsewlSrw/aolwnusukEE7vBfjAiUXAZKngMx6BnYoORlWSV88wdA==
"@medusajs/medusa-cli@1.2.1-dev-1649009241281":
version "1.2.1-dev-1649009241281"
resolved "http://localhost:4873/@medusajs%2fmedusa-cli/-/medusa-cli-1.2.1-dev-1649009241281.tgz#e0cc327224af6067013e0eba1b882b3f9f0fd0e3"
integrity sha512-3949nJfrBIguXrKDwiKjQ10XxZ06ccTShbcOUsXkIuGIMOHBZ+AuhjErCAnXS3H/JSIO93lnQKVJ8DWSkf9BGQ==
dependencies:
"@babel/polyfill" "^7.8.7"
"@babel/runtime" "^7.9.6"
@@ -1289,8 +1289,8 @@
is-valid-path "^0.1.1"
joi-objectid "^3.0.1"
meant "^1.0.1"
medusa-core-utils "1.1.31-dev-1642337635041"
medusa-telemetry "0.0.11-dev-1642337635041"
medusa-core-utils "1.1.31-dev-1649009241281"
medusa-telemetry "0.0.11-dev-1649009241281"
netrc-parser "^3.1.6"
open "^8.0.6"
ora "^5.4.1"
@@ -1304,13 +1304,13 @@
winston "^3.3.3"
yargs "^15.3.1"
"@medusajs/medusa@1.1.60-dev-1642337635041":
version "1.1.60-dev-1642337635041"
resolved "http://localhost:4873/@medusajs%2fmedusa/-/medusa-1.1.60-dev-1642337635041.tgz#011d64b9b85a3f66e01f3755788370aa444a2e27"
integrity sha512-MBef46mJZMSyE3+dcijMqjFZbNo7AL60mP/PwkOy4W1d/t2MZ+GOBIufLAVxVw7SaiSjAGpQDmCrUOqQrIhIdg==
"@medusajs/medusa@1.2.1-dev-1649009241281":
version "1.2.1-dev-1649009241281"
resolved "http://localhost:4873/@medusajs%2fmedusa/-/medusa-1.2.1-dev-1649009241281.tgz#ed063a9abee468cd71b5d0201ea4f32dd2a59775"
integrity sha512-uNVPZq4rVOLRRP9yTeYWPn9xsLfhh7mgRexMjRL/7QmKTQmspBL/pbjdRk0WCBSSf3o5SFiOV0/n69Oxg6Oaig==
dependencies:
"@hapi/joi" "^16.1.8"
"@medusajs/medusa-cli" "1.1.25-dev-1642337635041"
"@medusajs/medusa-cli" "1.2.1-dev-1649009241281"
"@types/lodash" "^4.14.168"
awilix "^4.2.3"
body-parser "^1.19.0"
@@ -1334,8 +1334,8 @@
joi "^17.3.0"
joi-objectid "^3.0.1"
jsonwebtoken "^8.5.1"
medusa-core-utils "1.1.31-dev-1642337635041"
medusa-test-utils "1.1.35-dev-1642337635041"
medusa-core-utils "1.1.31-dev-1649009241281"
medusa-test-utils "1.1.37-dev-1649009241281"
morgan "^1.9.1"
multer "^1.4.2"
passport "^0.4.0"
@@ -1995,10 +1995,10 @@ babel-preset-jest@^26.6.2:
babel-plugin-jest-hoist "^26.6.2"
babel-preset-current-node-syntax "^1.0.0"
babel-preset-medusa-package@1.1.19-dev-1642337635041:
version "1.1.19-dev-1642337635041"
resolved "http://localhost:4873/babel-preset-medusa-package/-/babel-preset-medusa-package-1.1.19-dev-1642337635041.tgz#24c6f2db887f35c358270d85cc0f6b6c19f459fe"
integrity sha512-HtrHI6oWUEMyTK9MHKzCg7hzZHgi8+uCIQyTQIY5SYFRA6kZiNl54Wzai/BY26F1j5g0sVNfqmFXdBd53GEz7g==
babel-preset-medusa-package@1.1.19-dev-1649009241281:
version "1.1.19-dev-1649009241281"
resolved "http://localhost:4873/babel-preset-medusa-package/-/babel-preset-medusa-package-1.1.19-dev-1649009241281.tgz#253cb95840d3e122a45e2f72e15f623afa57ae61"
integrity sha512-UFH2A+rYUc33d2f+xEVCQOOmcm0RSSPlnTeSVV5bM8SoS4MXgekPdBO+sMjKi/bLbHJncKaapCm/HoYZ19ZJHg==
dependencies:
"@babel/plugin-proposal-class-properties" "^7.12.1"
"@babel/plugin-proposal-decorators" "^7.12.1"
@@ -5138,48 +5138,48 @@ media-typer@0.3.0:
resolved "http://localhost:4873/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
medusa-core-utils@1.1.31-dev-1642337635041:
version "1.1.31-dev-1642337635041"
resolved "http://localhost:4873/medusa-core-utils/-/medusa-core-utils-1.1.31-dev-1642337635041.tgz#d6928f377320ab851eff1750e13e44e2e366c9cb"
integrity sha512-m0VXTM3QwKi0oRk2Q4mqn6sK07xJgiwyz3rs/0gHTjl88B8k75XugbriFceLPEYqHs6AjP6nSNiRDqdRJqUsMg==
medusa-core-utils@1.1.31-dev-1649009241281:
version "1.1.31-dev-1649009241281"
resolved "http://localhost:4873/medusa-core-utils/-/medusa-core-utils-1.1.31-dev-1649009241281.tgz#1ff37cdd9057c5716124f250c0b4b261ef8c7cde"
integrity sha512-nYcgpTtUSGAFG00m1rKcWUgnlmL2zu5aOYl5Fas/re+hhc1fhi+jwHUMigK0c28t71RwgtaeJFNKgQNgxli+2A==
dependencies:
joi "^17.3.0"
joi-objectid "^3.0.1"
medusa-fulfillment-webshipper@1.1.35-dev-1642337635041:
version "1.1.35-dev-1642337635041"
resolved "http://localhost:4873/medusa-fulfillment-webshipper/-/medusa-fulfillment-webshipper-1.1.35-dev-1642337635041.tgz#ab4b0a23488094b39582d1c1d1c69e29713b464d"
integrity sha512-P1gRvnJcs5Svgmvb44Qp1Oph9TqtUQsEeawhX7u+3dSz9qDnJ1lo1XB1tBttpCP9qepPe3nc0Sk7mEwlq8eM6Q==
medusa-fulfillment-webshipper@1.2.1-dev-1649009241281:
version "1.2.1-dev-1649009241281"
resolved "http://localhost:4873/medusa-fulfillment-webshipper/-/medusa-fulfillment-webshipper-1.2.1-dev-1649009241281.tgz#d199209df0d3683a53829565713b857bac284282"
integrity sha512-dtMBJ3mObQS2uGKClga66qDOGTKjOenzLOdjScurCwheQqi5a+EOyUrZSssE4oQEMMRsuY+L/MTkcLyKr4f+HA==
dependencies:
axios "^0.20.0"
body-parser "^1.19.0"
cors "^2.8.5"
express "^4.17.1"
medusa-core-utils "1.1.31-dev-1642337635041"
medusa-core-utils "1.1.31-dev-1649009241281"
medusa-interfaces@1.1.32-dev-1642337635041:
version "1.1.32-dev-1642337635041"
resolved "http://localhost:4873/medusa-interfaces/-/medusa-interfaces-1.1.32-dev-1642337635041.tgz#8179a344a41e1163502b4ee8af5b294443067f99"
integrity sha512-I7Wp+BEgzJ2wLkGBoDe6VhAlB8lJRMAHJ7wGzWfCMiG3Dj0PX2KE2HZDT+PA5S7IIXHWh9c0n37/cHZDcIF5Hw==
medusa-interfaces@1.2.1-dev-1649009241281:
version "1.2.1-dev-1649009241281"
resolved "http://localhost:4873/medusa-interfaces/-/medusa-interfaces-1.2.1-dev-1649009241281.tgz#77b6c906b805e6b1fb092fb7632a7ad8ecbafec5"
integrity sha512-tgDptu8k7aWCKBo1m8RxcxcffsVjy2e2M80rfvTd81gjC3EtGBGxDmiwNGJoaWkFq+Matq2wve2l92xVA2t8uQ==
dependencies:
medusa-core-utils "1.1.31-dev-1642337635041"
medusa-core-utils "1.1.31-dev-1649009241281"
medusa-plugin-sendgrid@1.1.36-dev-1642337635041:
version "1.1.36-dev-1642337635041"
resolved "http://localhost:4873/medusa-plugin-sendgrid/-/medusa-plugin-sendgrid-1.1.36-dev-1642337635041.tgz#ae4d1d23167daf3c45ced085f3079ed21d5e876d"
integrity sha512-nJBq0x9h9fMsPfW4BnGzvk8yWpuk1v90zeXP85cDhFeAAWJ50o2zF7HsNcJawPfhKooAF+3kRDApR8706gUyqQ==
medusa-plugin-sendgrid@1.2.1-dev-1649009241281:
version "1.2.1-dev-1649009241281"
resolved "http://localhost:4873/medusa-plugin-sendgrid/-/medusa-plugin-sendgrid-1.2.1-dev-1649009241281.tgz#362fafa54658c641fb482346c0ba6f28007bbbf3"
integrity sha512-2C7mfwR+HZSjm/Y3TGHHr7dJAoNhwUvEVZs2dpJCEfmpbsR2f6TG3J5e+kidubQ4NeCmHdrXGPmjS4lbH1tnhQ==
dependencies:
"@babel/plugin-transform-classes" "^7.9.5"
"@sendgrid/mail" "^7.1.1"
body-parser "^1.19.0"
express "^4.17.1"
medusa-core-utils "1.1.31-dev-1642337635041"
medusa-test-utils "1.1.35-dev-1642337635041"
medusa-core-utils "1.1.31-dev-1649009241281"
medusa-test-utils "1.1.37-dev-1649009241281"
medusa-telemetry@0.0.11-dev-1642337635041:
version "0.0.11-dev-1642337635041"
resolved "http://localhost:4873/medusa-telemetry/-/medusa-telemetry-0.0.11-dev-1642337635041.tgz#290bb20c1712adf99ec5d2f6871b200106203bc0"
integrity sha512-LlJVgdKAQFrp6ULW8D+Qvb/VSEgbQbKjEeoMnUwDaMMswGehsFUXsqBzs1LRo5nKmpLaPvSorw4OMvktR+RxTQ==
medusa-telemetry@0.0.11-dev-1649009241281:
version "0.0.11-dev-1649009241281"
resolved "http://localhost:4873/medusa-telemetry/-/medusa-telemetry-0.0.11-dev-1649009241281.tgz#76077de2a5b53f754f1a55701d892716be67876d"
integrity sha512-ZEWmHZI9RByg1Tm8sOOxVCs+dkr2LCvElXqFsJfgIoTdjvGS96tfG1pbod2LHD5rcBY2aXDSXBRf0pA1xRtvsQ==
dependencies:
axios "^0.21.1"
axios-retry "^3.1.9"
@@ -5191,13 +5191,13 @@ medusa-telemetry@0.0.11-dev-1642337635041:
remove-trailing-slash "^0.1.1"
uuid "^8.3.2"
medusa-test-utils@1.1.35-dev-1642337635041:
version "1.1.35-dev-1642337635041"
resolved "http://localhost:4873/medusa-test-utils/-/medusa-test-utils-1.1.35-dev-1642337635041.tgz#1daf7f6a671e2fffe43ab349d795581870104a4a"
integrity sha512-pCpMjmo0VyYbSRu/Ga/GdirBscJtS4HK4HtfXepMrut7Yg/kV+GpW/fFtBFkiqGDBpCWYLopzZAdRw+qpdpusA==
medusa-test-utils@1.1.37-dev-1649009241281:
version "1.1.37-dev-1649009241281"
resolved "http://localhost:4873/medusa-test-utils/-/medusa-test-utils-1.1.37-dev-1649009241281.tgz#59c9ac8fddf31a410347dac5ef147758a61f13da"
integrity sha512-+3c+2DB3UpJxyIt6LvWCR9aBtXwrABXV4GhUYW6toGl19JE2lZ9PBL3GSVmGi7etZ7Xv7tR9bwZXZURvKMr8Hg==
dependencies:
"@babel/plugin-transform-classes" "^7.9.5"
medusa-core-utils "1.1.31-dev-1642337635041"
medusa-core-utils "1.1.31-dev-1649009241281"
randomatic "^3.1.1"
merge-descriptors@1.0.1:
@@ -46,11 +46,17 @@ export default (app) => {
return app
}
export const defaultAdminDraftOrdersRelations = ["order", "cart"]
export const defaultAdminDraftOrdersRelations = [
"order",
"cart",
"cart.items",
"cart.items.adjustments",
]
export const defaultAdminDraftOrdersCartRelations = [
"region",
"items",
"items.adjustments",
"payment",
"shipping_address",
"billing_address",
@@ -5,12 +5,15 @@ import { SwapServiceMock } from "../../../../../services/__mocks__/swap"
const defaultRelations = [
"order",
"additional_items",
"additional_items.adjustments",
"return_order",
"fulfillments",
"payment",
"shipping_address",
"shipping_methods",
"cart",
"cart.items",
"cart.items.adjustments",
]
const defaultFields = [
@@ -24,12 +24,15 @@ export default (app) => {
export const defaultAdminSwapRelations = [
"order",
"additional_items",
"additional_items.adjustments",
"return_order",
"fulfillments",
"payment",
"shipping_address",
"shipping_methods",
"cart",
"cart.items",
"cart.items.adjustments",
]
export const defaultAdminSwapFields = [
@@ -115,6 +115,7 @@ export const defaultStoreCartRelations = [
"gift_cards",
"region",
"items",
"items.adjustments",
"payment",
"shipping_address",
"billing_address",
+1
View File
@@ -14,6 +14,7 @@ export * from "./models/product-type-tax-rate"
export * from "./models/tax-rate"
export * from "./models/shipping-method-tax-line"
export * from "./models/line-item-tax-line"
export * from "./models/line-item-adjustment"
export * from "./models/address"
export * from "./models/cart"
export * from "./models/claim-image"
@@ -0,0 +1,39 @@
import { MigrationInterface, QueryRunner } from "typeorm"
export class addLineItemAdjustments1648600574750 implements MigrationInterface {
name = "addLineItemAdjustments1648600574750"
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "line_item_adjustment" ("id" character varying NOT NULL, "item_id" character varying NOT NULL, "description" character varying NOT NULL, "discount_id" character varying, "amount" integer NOT NULL, "metadata" jsonb, CONSTRAINT "PK_2b1360103753df2dc8257c2c8c3" PRIMARY KEY ("id"))`
)
await queryRunner.query(
`CREATE INDEX "IDX_be9aea2ccf3567007b6227da4d" ON "line_item_adjustment" ("item_id") `
)
await queryRunner.query(
`CREATE INDEX "IDX_2f41b20a71f30e60471d7e3769" ON "line_item_adjustment" ("discount_id") `
)
await queryRunner.query(
`CREATE UNIQUE INDEX "IDX_bf701b88d2041392a288785ada" ON "line_item_adjustment" ("discount_id", "item_id") WHERE "discount_id" IS NOT NULL`
)
await queryRunner.query(
`ALTER TABLE "line_item_adjustment" ADD CONSTRAINT "FK_be9aea2ccf3567007b6227da4d2" FOREIGN KEY ("item_id") REFERENCES "line_item"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
)
await queryRunner.query(
`ALTER TABLE "line_item_adjustment" ADD CONSTRAINT "FK_2f41b20a71f30e60471d7e3769c" FOREIGN KEY ("discount_id") REFERENCES "discount"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`
)
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "line_item_adjustment" DROP CONSTRAINT "FK_2f41b20a71f30e60471d7e3769c"`
)
await queryRunner.query(
`ALTER TABLE "line_item_adjustment" DROP CONSTRAINT "FK_be9aea2ccf3567007b6227da4d2"`
)
await queryRunner.query(`DROP INDEX "IDX_bf701b88d2041392a288785ada"`)
await queryRunner.query(`DROP INDEX "IDX_2f41b20a71f30e60471d7e3769"`)
await queryRunner.query(`DROP INDEX "IDX_be9aea2ccf3567007b6227da4d"`)
await queryRunner.query(`DROP TABLE "line_item_adjustment"`)
}
}
@@ -0,0 +1,61 @@
import {
Entity,
BeforeInsert,
Index,
Column,
ManyToOne,
JoinColumn,
PrimaryColumn,
OneToOne,
Unique,
} from "typeorm"
import { ulid } from "ulid"
import { DbAwareColumn } from "../utils/db-aware-column"
import { Discount } from "./discount"
import { LineItem } from "./line-item"
@Entity()
@Index(["discount_id", "item_id"], {
unique: true,
where: `"discount_id" IS NOT NULL`,
})
export class LineItemAdjustment {
@PrimaryColumn()
id: string
@Index()
@Column()
item_id: string
@ManyToOne(
() => LineItem,
(li) => li.adjustments,
{ onDelete: "CASCADE" }
)
@JoinColumn({ name: "item_id" })
item: LineItem
@Column()
description: string
@ManyToOne(() => Discount)
@JoinColumn({ name: "discount_id" })
discount: Discount
@Index()
@Column({ nullable: true })
discount_id: string
@Column({ type: "int" })
amount: number
@DbAwareColumn({ type: "jsonb", nullable: true })
metadata: any
@BeforeInsert()
private beforeInsert() {
if (this.id) return
const id = ulid()
this.id = `lia_${id}`
}
}
+4
View File
@@ -20,6 +20,7 @@ import { Cart } from "./cart"
import { Order } from "./order"
import { ClaimOrder } from "./claim-order"
import { ProductVariant } from "./product-variant"
import { LineItemAdjustment } from "./line-item-adjustment"
@Check(`"fulfilled_quantity" <= "quantity"`)
@Check(`"shipped_quantity" <= "fulfilled_quantity"`)
@@ -65,6 +66,9 @@ export class LineItem {
@OneToMany(() => LineItemTaxLine, (tl) => tl.item, { cascade: ["insert"] })
tax_lines: LineItemTaxLine[]
@OneToMany(() => LineItemAdjustment, (lia) => lia.item, { cascade: ["insert"] })
adjustments: LineItemAdjustment[]
@Column()
title: string
@@ -0,0 +1,6 @@
import { EntityRepository, Repository } from "typeorm"
import { LineItemAdjustment } from "../models/line-item-adjustment"
@EntityRepository(LineItemAdjustment)
export class LineItemAdjustmentRepository extends Repository<LineItemAdjustment> {}
@@ -17,6 +17,7 @@ export class LineItemRepository extends Repository<LineItem> {
): Promise<(LineItem & { return_item: ReturnItem })[]> {
const qb = this.createQueryBuilder("li")
.leftJoinAndSelect(`li.tax_lines`, "tax_lines")
.leftJoinAndSelect(`li.adjustments`, "adjustments")
.leftJoinAndMapOne(
`li.return_item`,
ReturnItem,
+13 -10
View File
@@ -210,7 +210,7 @@ export const CartServiceMock = {
withTransaction: function() {
return this
},
updatePaymentSession: jest.fn().mockImplementation(data => {
updatePaymentSession: jest.fn().mockImplementation((data) => {
return Promise.resolve()
}),
authorizePayment: jest.fn().mockImplementation((id, data) => {
@@ -223,13 +223,13 @@ export const CartServiceMock = {
}
return Promise.resolve(carts.testCart)
}),
refreshPaymentSession: jest.fn().mockImplementation(data => {
refreshPaymentSession: jest.fn().mockImplementation((data) => {
return Promise.resolve()
}),
update: jest.fn().mockImplementation(data => {
update: jest.fn().mockImplementation((data) => {
return Promise.resolve()
}),
create: jest.fn().mockImplementation(data => {
create: jest.fn().mockImplementation((data) => {
if (data.region_id === IdMap.getId("testRegion")) {
return Promise.resolve(carts.regionCart)
}
@@ -238,7 +238,7 @@ export const CartServiceMock = {
}
return Promise.resolve(carts.regionCart)
}),
retrieve: jest.fn().mockImplementation(cartId => {
retrieve: jest.fn().mockImplementation((cartId) => {
if (cartId === IdMap.getId("fr-cart")) {
return Promise.resolve(carts.frCart)
}
@@ -326,20 +326,20 @@ export const CartServiceMock = {
applyDiscount: jest.fn().mockImplementation((cartId, code) => {
return Promise.resolve()
}),
setPaymentSession: jest.fn().mockImplementation(cartId => {
setPaymentSession: jest.fn().mockImplementation((cartId) => {
return Promise.resolve()
}),
setPaymentSessions: jest.fn().mockImplementation(cartId => {
setPaymentSessions: jest.fn().mockImplementation((cartId) => {
return Promise.resolve()
}),
setShippingOptions: jest.fn().mockImplementation(cartId => {
setShippingOptions: jest.fn().mockImplementation((cartId) => {
return Promise.resolve()
}),
decorate: jest.fn().mockImplementation(cart => {
decorate: jest.fn().mockImplementation((cart) => {
cart.decorated = true
return cart
}),
addShippingMethod: jest.fn().mockImplementation(cartId => {
addShippingMethod: jest.fn().mockImplementation((cartId) => {
return Promise.resolve()
}),
retrieveShippingOption: jest.fn().mockImplementation((cartId, optionId) => {
@@ -378,6 +378,9 @@ export const CartServiceMock = {
}
}
}),
refreshAdjustments_: jest.fn().mockImplementation((cart) => {
return Promise.resolve({})
}),
}
const mock = jest.fn().mockImplementation(() => {
@@ -0,0 +1,68 @@
import { IdMap } from "medusa-test-utils"
import { MedusaError } from "medusa-core-utils"
export const LineItemAdjustmentServiceMock = {
withTransaction: function() {
return this
},
create: jest.fn().mockImplementation((data) => {
return Promise.resolve({ ...data })
}),
update: jest.fn().mockImplementation((data) => {
return Promise.resolve({ ...data })
}),
validate: jest.fn().mockImplementation((data) => {
if (data.title === "invalid lineitem") {
throw new Error(`"content" is required`)
}
return data
}),
delete: jest.fn().mockImplementation((data) => {
return Promise.resolve({})
}),
createAdjustmentForLineItem: jest
.fn()
.mockImplementation((cart, lineItem) => {
return Promise.resolve({
item_id: lineItem.id,
amount: 1000,
discount_id: "disc_2",
id: "lia-1",
description: "discount",
})
}),
createAdjustments: jest.fn().mockImplementation((cart, lineItem) => {
if (lineItem) {
return Promise.resolve({
item_id: lineItem.id,
amount: 1000,
discount_id: "disc_2",
id: "lia-1",
description: "discount",
})
}
return Promise.resolve([
{
item_id: "li-1",
amount: 200,
discount_id: "disc_2",
id: "lia-1",
description: "discount",
},
{
item_id: "li-3",
amount: 100,
discount_id: "disc_3",
id: "lia-2",
description: "discount",
},
])
}),
}
const mock = jest.fn().mockImplementation(() => {
return LineItemAdjustmentServiceMock
})
export default mock
+195 -111
View File
@@ -3,6 +3,7 @@ import { MedusaError } from "medusa-core-utils"
import { IdMap, MockManager, MockRepository } from "medusa-test-utils"
import CartService from "../cart"
import { InventoryServiceMock } from "../__mocks__/inventory"
import { LineItemAdjustmentServiceMock } from "../__mocks__/line-item-adjustment"
const eventBusService = {
emit: jest.fn(),
@@ -347,6 +348,7 @@ describe("CartService", () => {
findOneWithRelations: (rels, q) => {
if (q.where.id === IdMap.getId("cartWithLine")) {
return Promise.resolve({
id: IdMap.getId("cartWithLine"),
items: [
{
id: IdMap.getId("merger"),
@@ -359,6 +361,7 @@ describe("CartService", () => {
})
}
return Promise.resolve({
id: IdMap.getId("emptyCart"),
shipping_methods: [
{
shipping_option: {
@@ -378,13 +381,14 @@ describe("CartService", () => {
eventBusService,
shippingOptionService,
inventoryService,
lineItemAdjustmentService: LineItemAdjustmentServiceMock,
})
beforeEach(() => {
jest.clearAllMocks()
})
it("successfully creates new line item", async () => {
it("creates a new line item and emits a created event", async () => {
const lineItem = {
title: "New Line",
description: "This is a new line",
@@ -393,7 +397,6 @@ describe("CartService", () => {
unit_price: 123,
quantity: 10,
}
await cartService.addLineItem(IdMap.getId("emptyCart"), _.clone(lineItem))
expect(eventBusService.emit).toHaveBeenCalledTimes(1)
@@ -408,6 +411,15 @@ describe("CartService", () => {
has_shipping: false,
cart_id: IdMap.getId("emptyCart"),
})
expect(
LineItemAdjustmentServiceMock.createAdjustments
).toHaveBeenCalledTimes(1)
expect(
LineItemAdjustmentServiceMock.createAdjustments
).toHaveBeenCalledWith(
expect.objectContaining({ id: IdMap.getId("emptyCart") })
)
})
it("successfully creates new line item with shipping", async () => {
@@ -440,6 +452,15 @@ describe("CartService", () => {
has_shipping: false,
cart_id: IdMap.getId("emptyCart"),
})
expect(
LineItemAdjustmentServiceMock.createAdjustments
).toHaveBeenCalledTimes(1)
expect(
LineItemAdjustmentServiceMock.createAdjustments
).toHaveBeenCalledWith(
expect.objectContaining({ id: IdMap.getId("emptyCart") })
)
})
it("successfully merges existing line item", async () => {
@@ -462,6 +483,20 @@ describe("CartService", () => {
quantity: 2,
}
)
expect(LineItemAdjustmentServiceMock.delete).toHaveBeenCalledTimes(1)
expect(LineItemAdjustmentServiceMock.delete).toHaveBeenCalledWith({
item_id: [IdMap.getId("merger")],
})
expect(
LineItemAdjustmentServiceMock.createAdjustments
).toHaveBeenCalledTimes(1)
expect(
LineItemAdjustmentServiceMock.createAdjustments
).toHaveBeenCalledWith(
expect.objectContaining({ id: IdMap.getId("cartWithLine") })
)
})
it("throws if inventory isn't covered", async () => {
@@ -559,6 +594,7 @@ describe("CartService", () => {
lineItemService,
shippingOptionService,
eventBusService,
lineItemAdjustmentService: LineItemAdjustmentServiceMock,
})
beforeEach(() => {
@@ -576,6 +612,22 @@ describe("CartService", () => {
IdMap.getId("itemToRemove")
)
expect(LineItemAdjustmentServiceMock.delete).toHaveBeenCalledTimes(1)
expect(LineItemAdjustmentServiceMock.delete).toHaveBeenCalledWith({
item_id: [IdMap.getId("itemToRemove")],
})
expect(
LineItemAdjustmentServiceMock.createAdjustments
).toHaveBeenCalledTimes(1)
expect(
LineItemAdjustmentServiceMock.createAdjustments
).toHaveBeenCalledWith(
expect.objectContaining({
items: [{ id: IdMap.getId("itemToRemove") }],
})
)
expect(eventBusService.emit).toHaveBeenCalledTimes(1)
expect(eventBusService.emit).toHaveBeenCalledWith(
"cart.updated",
@@ -598,6 +650,22 @@ describe("CartService", () => {
profile_id: IdMap.getId("prevPro"),
},
})
expect(LineItemAdjustmentServiceMock.delete).toHaveBeenCalledTimes(1)
expect(LineItemAdjustmentServiceMock.delete).toHaveBeenCalledWith({
item_id: [IdMap.getId("itemToRemove")],
})
expect(
LineItemAdjustmentServiceMock.createAdjustments
).toHaveBeenCalledTimes(1)
expect(
LineItemAdjustmentServiceMock.createAdjustments
).toHaveBeenCalledWith(
expect.objectContaining({
items: [expect.objectContaining({ id: IdMap.getId("itemToRemove") })],
})
)
})
it("resolves if line item is not in cart", async () => {
@@ -666,7 +734,13 @@ describe("CartService", () => {
describe("updateLineItem", () => {
const lineItemService = {
update: jest.fn(),
update: jest.fn().mockImplementation(() =>
Promise.resolve({
id: IdMap.getId("existing"),
variant_id: IdMap.getId("good"),
quantity: 1,
})
),
withTransaction: function() {
return this
},
@@ -692,6 +766,7 @@ describe("CartService", () => {
})
}
return Promise.resolve({
id: IdMap.getId("cartWithLine"),
items: [
{
id: IdMap.getId("existing"),
@@ -709,6 +784,7 @@ describe("CartService", () => {
lineItemService,
eventBusService,
inventoryService,
lineItemAdjustmentService: LineItemAdjustmentServiceMock,
})
beforeEach(() => {
@@ -733,6 +809,20 @@ describe("CartService", () => {
IdMap.getId("existing"),
{ quantity: 2 }
)
expect(LineItemAdjustmentServiceMock.delete).toHaveBeenCalledTimes(1)
expect(LineItemAdjustmentServiceMock.delete).toHaveBeenCalledWith({
item_id: [IdMap.getId("existing")],
})
expect(
LineItemAdjustmentServiceMock.createAdjustments
).toHaveBeenCalledTimes(1)
expect(
LineItemAdjustmentServiceMock.createAdjustments
).toHaveBeenCalledWith(
expect.objectContaining({ id: IdMap.getId("cartWithLine") })
)
})
it("throws if inventory isn't covered", async () => {
@@ -1599,6 +1689,18 @@ describe("CartService", () => {
},
],
region_id: IdMap.getId("good"),
items: [
{
id: "li1",
quantity: 2,
unit_price: 1000,
},
{
id: "li2",
quantity: 1,
unit_price: 500,
},
],
})
}
if (q.where.id === "with-d-and-customer") {
@@ -1619,6 +1721,18 @@ describe("CartService", () => {
id: IdMap.getId("cart"),
discounts: [],
region_id: IdMap.getId("good"),
items: [
{
id: "li1",
quantity: 2,
unit_price: 1000,
},
{
id: "li2",
quantity: 1,
unit_price: 500,
},
],
})
},
})
@@ -1756,6 +1870,11 @@ describe("CartService", () => {
}
return Promise.resolve(false)
}),
validateDiscountForCartOrThrow: jest
.fn()
.mockImplementation((cart, discount) => {
return Promise.resolve({ hasErrors: () => false })
}),
}
const cartService = new CartService({
@@ -1764,6 +1883,7 @@ describe("CartService", () => {
cartRepository,
discountService,
eventBusService,
lineItemAdjustmentService: LineItemAdjustmentServiceMock,
})
beforeEach(async () => {
@@ -1793,6 +1913,18 @@ describe("CartService", () => {
subtotal: 0,
tax_total: 0,
total: 0,
items: [
{
id: "li1",
quantity: 2,
unit_price: 1000,
},
{
id: "li2",
quantity: 1,
unit_price: 500,
},
],
discounts: [
{
id: IdMap.getId("10off"),
@@ -1804,6 +1936,20 @@ describe("CartService", () => {
},
],
})
expect(LineItemAdjustmentServiceMock.delete).toHaveBeenCalledTimes(1)
expect(LineItemAdjustmentServiceMock.delete).toHaveBeenCalledWith({
item_id: ["li1", "li2"],
})
expect(
LineItemAdjustmentServiceMock.createAdjustments
).toHaveBeenCalledTimes(1)
expect(
LineItemAdjustmentServiceMock.createAdjustments
).toHaveBeenCalledWith(
expect.objectContaining({ id: IdMap.getId("cart") })
)
})
it("successfully applies discount to cart and removes old one", async () => {
@@ -1820,6 +1966,18 @@ describe("CartService", () => {
subtotal: 0,
tax_total: 0,
total: 0,
items: [
{
id: "li1",
quantity: 2,
unit_price: 1000,
},
{
id: "li2",
quantity: 1,
unit_price: 500,
},
],
discounts: [
{
id: IdMap.getId("10off"),
@@ -1831,6 +1989,20 @@ describe("CartService", () => {
},
],
})
expect(LineItemAdjustmentServiceMock.delete).toHaveBeenCalledTimes(1)
expect(LineItemAdjustmentServiceMock.delete).toHaveBeenCalledWith({
item_id: ["li1", "li2"],
})
expect(
LineItemAdjustmentServiceMock.createAdjustments
).toHaveBeenCalledTimes(1)
expect(
LineItemAdjustmentServiceMock.createAdjustments
).toHaveBeenCalledWith(
expect.objectContaining({ id: IdMap.getId("cart") })
)
})
it("successfully applies free shipping", async () => {
@@ -1860,32 +2032,16 @@ describe("CartService", () => {
},
},
],
discount_total: 0,
shipping_total: 0,
subtotal: 0,
tax_total: 0,
total: 0,
region_id: IdMap.getId("good"),
})
})
it("successfully applies discount with usage count null", async () => {
await cartService.update(IdMap.getId("cart"), {
discounts: [{ code: "null-count" }],
})
expect(discountService.retrieveByCode).toHaveBeenCalledTimes(1)
expect(cartRepository.save).toHaveBeenCalledTimes(1)
expect(cartRepository.save).toHaveBeenCalledWith({
id: IdMap.getId("cart"),
discounts: [
items: [
{
id: IdMap.getId("null-count"),
code: "null-count",
regions: [{ id: IdMap.getId("good") }],
usage_count: 0,
usage_limit: 2,
rule: {},
id: "li1",
quantity: 2,
unit_price: 1000,
},
{
id: "li2",
quantity: 1,
unit_price: 500,
},
],
discount_total: 0,
@@ -1895,92 +2051,20 @@ describe("CartService", () => {
total: 0,
region_id: IdMap.getId("good"),
})
})
it("successfully applies valid discount with expiration date to cart", async () => {
await cartService.update(IdMap.getId("fr-cart"), {
discounts: [
{
code: "ValidDiscount",
},
],
expect(LineItemAdjustmentServiceMock.delete).toHaveBeenCalledTimes(1)
expect(LineItemAdjustmentServiceMock.delete).toHaveBeenCalledWith({
item_id: ["li1", "li2"],
})
expect(eventBusService.emit).toHaveBeenCalledTimes(1)
expect(eventBusService.emit).toHaveBeenCalledWith(
"cart.updated",
expect.any(Object)
expect(
LineItemAdjustmentServiceMock.createAdjustments
).toHaveBeenCalledTimes(1)
expect(
LineItemAdjustmentServiceMock.createAdjustments
).toHaveBeenCalledWith(
expect.objectContaining({ id: IdMap.getId("cart") })
)
expect(cartRepository.save).toHaveBeenCalledTimes(1)
expect(cartRepository.save).toHaveBeenCalledWith({
id: IdMap.getId("cart"),
region_id: IdMap.getId("good"),
discount_total: 0,
shipping_total: 0,
subtotal: 0,
tax_total: 0,
total: 0,
discounts: [
{
id: IdMap.getId("10off"),
code: "10%OFF",
regions: [{ id: IdMap.getId("good") }],
rule: {
type: "percentage",
},
starts_at: expect.any(Date),
ends_at: expect.any(Date),
},
],
})
})
it("throws if discount is applied too before it's valid", async () => {
await expect(
cartService.update(IdMap.getId("cart"), {
discounts: [{ code: "EarlyDiscount" }],
})
).rejects.toThrow("Discount is not valid yet")
})
it("throws if expired discount is applied", async () => {
await expect(
cartService.update(IdMap.getId("cart"), {
discounts: [{ code: "ExpiredDiscount" }],
})
).rejects.toThrow("Discount is expired")
})
it("throws if expired dynamic discount is applied", async () => {
await expect(
cartService.update(IdMap.getId("cart"), {
discounts: [{ code: "ExpiredDynamicDiscount" }],
})
).rejects.toThrow("Discount is expired")
})
it("throws if expired dynamic discount is applied after ends_at", async () => {
await expect(
cartService.update(IdMap.getId("cart"), {
discounts: [{ code: "ExpiredDynamicDiscountEndDate" }],
})
).rejects.toThrow("Discount is expired")
})
it("throws if discount limit is reached", async () => {
await expect(
cartService.update(IdMap.getId("cart"), {
discounts: [{ code: "limit-reached" }],
})
).rejects.toThrow("Discount has been used maximum allowed times")
})
it("throws if discount is not available in region", async () => {
await expect(
cartService.update(IdMap.getId("cart"), {
discounts: [{ code: "US10" }],
})
).rejects.toThrow("The discount is not available in current region")
})
it("successfully applies discount with a check for customer applicableness", async () => {
@@ -682,6 +682,472 @@ describe("DiscountService", () => {
})
})
describe("validateDiscountForCartOrThrow", () => {
const getCart = (id) => {
if (id === "with-d") {
return {
id: "cart",
discounts: [
{
code: "1234",
rule: {
type: "fixed",
},
},
{
code: "FS1234",
rule: {
type: "free_shipping",
},
},
],
region_id: "good",
items: [
{
id: "li1",
quantity: 2,
unit_price: 1000,
},
{
id: "li2",
quantity: 1,
unit_price: 500,
},
],
}
}
if (id === "with-d-and-customer") {
return {
id: "with-d-and-customer",
discounts: [
{
code: "ApplicableForCustomer",
rule: {
type: "fixed",
},
},
],
region_id: "good",
customer_id: "test-customer",
}
}
return {
id: "cart",
discounts: [
{
code: "CODE",
rule: {
type: "percentage",
},
},
],
region_id: "good",
items: [
{
id: "li1",
quantity: 2,
unit_price: 1000,
},
{
id: "li2",
quantity: 1,
unit_price: 500,
},
],
}
}
let discountService
beforeEach(async () => {
discountService = new DiscountService({})
const hasReachedLimitMock = jest.fn().mockImplementation(() => false)
const isDisabledMock = jest.fn().mockImplementation(() => false)
const isValidForRegionMock = jest
.fn()
.mockImplementation(() => Promise.resolve(true))
const canApplyForCustomerMock = jest.fn().mockImplementation(() => {
return Promise.resolve(true)
})
discountService.hasReachedLimit = hasReachedLimitMock
discountService.isDisabled = isDisabledMock
discountService.canApplyForCustomer = canApplyForCustomerMock
discountService.isValidForRegion = isValidForRegionMock
})
it("calls all validation methods when cart includes a customer_id and doesn't throw", async () => {
const discount = {
id: "10off",
code: "10%OFF",
regions: [{ id: "good" }],
rule: {
id: "10off-rule",
type: "percentage",
},
}
const cart = getCart("with-d-and-customer")
const result = await discountService.validateDiscountForCartOrThrow(
cart,
discount
)
expect(result).toBeUndefined()
expect(discountService.hasReachedLimit).toHaveBeenCalledTimes(1)
expect(discountService.hasReachedLimit).toHaveBeenCalledWith(discount)
expect(discountService.isDisabled).toHaveBeenCalledTimes(1)
expect(discountService.isDisabled).toHaveBeenCalledWith(discount)
expect(discountService.isValidForRegion).toHaveBeenCalledTimes(1)
expect(discountService.isValidForRegion).toHaveBeenCalledWith(
discount,
cart.region_id
)
expect(discountService.canApplyForCustomer).toHaveBeenCalledTimes(1)
expect(discountService.canApplyForCustomer).toHaveBeenCalledWith(
discount.rule.id,
cart.customer_id
)
})
it("throws when hasReachedLimit returns true", async () => {
const discount = {}
const cart = getCart("with-d-and-customer")
discountService.hasReachedLimit = jest.fn().mockImplementation(() => true)
expect(
discountService.validateDiscountForCartOrThrow(cart, discount)
).rejects.toThrow({
message: "Discount has been used maximum allowed times",
})
})
it("throws when hasNotStarted returns true", async () => {
const discount = {}
const cart = getCart("with-d-and-customer")
discountService.hasNotStarted = jest.fn().mockImplementation(() => true)
expect(
discountService.validateDiscountForCartOrThrow(cart, discount)
).rejects.toThrow({
message: "Discount is not valid yet",
})
})
it("throws when hasExpired returns true", async () => {
const discount = {}
const cart = getCart("with-d-and-customer")
discountService.hasExpired = jest.fn().mockImplementation(() => true)
expect(
discountService.validateDiscountForCartOrThrow(cart, discount)
).rejects.toThrow({
message: "Discount is expired",
})
})
it("throws when isDisabled returns true", async () => {
const discount = {}
const cart = getCart("with-d-and-customer")
discountService.isDisabled = jest.fn().mockImplementation(() => true)
expect(
discountService.validateDiscountForCartOrThrow(cart, discount)
).rejects.toThrow({
message: "The discount code is disabled",
})
})
it("throws when isValidForRegion returns false", async () => {
const discount = {}
const cart = getCart("with-d-and-customer")
discountService.isValidForRegion = jest
.fn()
.mockImplementation(() => Promise.resolve(false))
expect(
discountService.validateDiscountForCartOrThrow(cart, discount)
).rejects.toThrow({
message: "The discount is not available in current region",
})
})
it("throws when canApplyForCustomer returns false", async () => {
const discount = { rule: { id: "" } }
const cart = getCart("with-d-and-customer")
discountService.canApplyForCustomer = jest
.fn()
.mockImplementation(() => Promise.resolve(false))
expect(
discountService.validateDiscountForCartOrThrow(cart, discount)
).rejects.toThrow({
message: "Discount is not valid for customer",
})
})
})
describe("hasReachedLimit", () => {
const discountService = new DiscountService({})
it("returns true if discount limit is reached", () => {
const discount = {
id: "limit-reached",
code: "limit-reached",
regions: [{ id: "good" }],
rule: {},
usage_count: 2,
usage_limit: 2,
}
const hasReachedLimit = discountService.hasReachedLimit(discount)
expect(hasReachedLimit).toBe(true)
})
it("returns false if discount limit is not reached", () => {
const discount = {
id: "limit-reached",
code: "limit-reached",
regions: [{ id: "good" }],
rule: {},
usage_count: 1,
usage_limit: 100,
}
const hasReachedLimit = discountService.hasReachedLimit(discount)
expect(hasReachedLimit).toBe(false)
})
it("returns false if discount limit is not set", () => {
const discount = {
id: "limit-reached",
code: "limit-reached",
regions: [{ id: "good" }],
rule: {},
usage_count: 1,
}
const hasReachedLimit = discountService.hasReachedLimit(discount)
expect(hasReachedLimit).toBe(false)
})
})
describe("isDisabled", () => {
const discountService = new DiscountService({})
it("returns false if discount not disabled", async () => {
const discount = {
id: "10off",
code: "10%OFF",
regions: [{ id: "good" }],
rule: {
id: "10off-rule",
type: "percentage",
},
is_disabled: false,
}
const isDisabled = discountService.isDisabled(discount)
expect(isDisabled).toBe(false)
})
it("returns true if discount is disabled", async () => {
const discount = {
id: "10off",
code: "10%OFF",
regions: [{ id: "good" }],
rule: {
id: "10off-rule",
type: "percentage",
},
is_disabled: true,
}
const isDisabled = discountService.isDisabled(discount)
expect(isDisabled).toBe(true)
})
})
describe("hasNotStarted", () => {
const discountService = new DiscountService({})
it("returns true if discount has a future starts_at date", async () => {
const discount = {
id: "10off",
code: "10%OFF",
regions: [{ id: "good" }],
rule: {
type: "percentage",
},
starts_at: getOffsetDate(1),
ends_at: getOffsetDate(10),
}
const hasNotStarted = discountService.hasNotStarted(discount)
expect(hasNotStarted).toBe(true)
})
it("returns false if discount has a past starts_at date", async () => {
const discount = {
id: "10off",
code: "10%OFF",
regions: [{ id: "good" }],
rule: {
type: "percentage",
},
starts_at: getOffsetDate(-2),
ends_at: getOffsetDate(10),
}
const hasNotStarted = discountService.hasNotStarted(discount)
expect(hasNotStarted).toBe(false)
})
})
describe("hasExpired", () => {
const discountService = new DiscountService({})
it("returns false if discount has a future ends_at date", async () => {
const discount = {
id: "10off",
code: "10%OFF",
regions: [{ id: "good" }],
rule: {
type: "percentage",
},
ends_at: getOffsetDate(10),
starts_at: getOffsetDate(-1),
}
const hasExpired = discountService.hasExpired(discount)
expect(hasExpired).toBe(false)
})
it("returns true if discount has a past ends_at date", async () => {
const discount = {
id: "10off",
code: "10%OFF",
regions: [{ id: "good" }],
rule: {
type: "percentage",
},
starts_at: getOffsetDate(-10),
ends_at: getOffsetDate(-1),
}
const hasExpired = discountService.hasExpired(discount)
expect(hasExpired).toBe(true)
})
})
describe("isValidForRegion", () => {
const retrieveMock = jest.fn().mockImplementation((id) => {
if (id === "parent-discount-us") {
return Promise.resolve({
id,
regions: [{ id: "us" }],
rule: {
id: "10off-rule",
type: "percentage",
},
})
} else if (id === "parent-discount-dk") {
return Promise.resolve({
id,
regions: [{ id: "dk" }],
rule: {
id: "10off-rule",
type: "percentage",
},
})
}
})
const discountService = new DiscountService({})
discountService.retrieve = retrieveMock
beforeEach(() => {
jest.clearAllMocks()
})
it("returns false if discount is not available in a given region", async () => {
const discount = {
id: "10off",
code: "10%OFF",
regions: [{ id: "us" }],
rule: {
id: "10off-rule",
type: "percentage",
},
}
const isValidForRegion = await discountService.isValidForRegion(
discount,
"dk"
)
expect(retrieveMock).toBeCalledTimes(0)
expect(isValidForRegion).toBe(false)
})
it("returns true if discount is available in a given region", async () => {
const discount = {
id: "10off",
code: "10%OFF",
regions: [{ id: "us" }],
rule: {
id: "10off-rule",
type: "percentage",
},
}
const isValidForRegion = await discountService.isValidForRegion(
discount,
"us"
)
expect(retrieveMock).toBeCalledTimes(0)
expect(isValidForRegion).toBe(true)
})
it("returns false if discount has a parent discount and is not available in region", async () => {
const discount = {
id: "10off",
code: "10%OFF",
parent_discount_id: "parent-discount-us",
}
const isValidForRegion = await discountService.isValidForRegion(
discount,
"dk"
)
expect(retrieveMock).toBeCalledTimes(1)
expect(retrieveMock).toBeCalledWith(discount.parent_discount_id, {
relations: ["rule", "regions"],
})
expect(isValidForRegion).toBe(false)
})
it("returns true if discount has a parent discount and is available in region", async () => {
const discount = {
id: "10off",
code: "10%OFF",
parent_discount_id: "parent-discount-dk",
}
const isValidForRegion = await discountService.isValidForRegion(
discount,
"dk"
)
expect(retrieveMock).toBeCalledTimes(1)
expect(retrieveMock).toBeCalledWith(discount.parent_discount_id, {
relations: ["rule", "regions"],
})
expect(isValidForRegion).toBe(true)
})
})
describe("canApplyForCustomer", () => {
const discountConditionRepository = {
canApplyForCustomer: jest
@@ -736,3 +1202,9 @@ describe("DiscountService", () => {
})
})
})
const getOffsetDate = (offset) => {
const date = new Date()
date.setDate(date.getDate() + offset)
return date
}
@@ -12,22 +12,22 @@ const eventBusService = {
describe("DraftOrderService", () => {
const totalsService = {
getTotal: o => {
getTotal: (o) => {
return o.total || 0
},
getSubtotal: o => {
getSubtotal: (o) => {
return o.subtotal || 0
},
getTaxTotal: o => {
getTaxTotal: (o) => {
return o.tax_total || 0
},
getDiscountTotal: o => {
getDiscountTotal: (o) => {
return o.discount_total || 0
},
getShippingTotal: o => {
getShippingTotal: (o) => {
return o.shipping_total || 0
},
getGiftCardTotal: o => {
getGiftCardTotal: (o) => {
return o.gift_card_total || 0
},
}
@@ -83,7 +83,7 @@ describe("DraftOrderService", () => {
}
const cartService = {
create: jest.fn().mockImplementation(data =>
create: jest.fn().mockImplementation((data) =>
Promise.resolve({
id: "test-cart",
...data,
@@ -110,16 +110,16 @@ describe("DraftOrderService", () => {
}
const addressRepository = MockRepository({
create: addr => ({
create: (addr) => ({
...addr,
}),
})
const draftOrderRepository = MockRepository({
create: d => ({
create: (d) => ({
...d,
}),
save: d => ({
save: (d) => ({
id: "test-draft-order",
...d,
}),
@@ -170,7 +170,13 @@ describe("DraftOrderService", () => {
"test-variant",
"test-region",
2,
{ metadata: {}, unit_price: undefined }
{
metadata: {},
unit_price: undefined,
cart: expect.objectContaining({
id: "test-cart",
}),
}
)
expect(lineItemService.create).toHaveBeenCalledTimes(1)
@@ -228,14 +234,14 @@ describe("DraftOrderService", () => {
}
const draftOrderRepository = MockRepository({
create: d => ({
create: (d) => ({
...d,
}),
save: d => ({
save: (d) => ({
id: "test-draft-order",
...d,
}),
findOne: q => {
findOne: (q) => {
switch (q.where.id) {
case "completed":
return Promise.resolve(completedOrder)
@@ -0,0 +1,314 @@
import LineItemAdjustmentService from "../line-item-adjustment"
import { MockManager, MockRepository, IdMap } from "medusa-test-utils"
import { EventBusServiceMock } from "../__mocks__/event-bus"
import { DiscountServiceMock } from "../__mocks__/discount"
import { In } from "typeorm"
describe("LineItemAdjustmentService", () => {
describe("list", () => {
const lineItemAdjustmentRepo = MockRepository({
find: (q) => {
return Promise.resolve([
{
id: "lia-1",
description: "discount",
amount: 1000,
item: "li-1",
discount_id: "disc_1",
},
])
},
})
const lineItemAdjustmentService = new LineItemAdjustmentService({
manager: MockManager,
lineItemAdjustmentRepository: lineItemAdjustmentRepo,
})
beforeAll(async () => {
jest.clearAllMocks()
})
it("calls lineItemAdjustment list method", async () => {
await lineItemAdjustmentService.list(
{ item_id: "li-1" },
{
relations: ["item"],
}
)
expect(lineItemAdjustmentRepo.find).toHaveBeenCalledTimes(1)
expect(lineItemAdjustmentRepo.find).toHaveBeenCalledWith({
where: {
item_id: "li-1",
},
relations: ["item"],
})
})
})
describe("retrieve", () => {
const lineItemAdjustmentRepo = MockRepository({
findOne: (q) => {
switch (q.where.id) {
case "lia-1":
return Promise.resolve({
id: "lia-1",
description: "discount",
})
default:
return Promise.resolve()
}
},
})
const lineItemAdjustmentService = new LineItemAdjustmentService({
manager: MockManager,
lineItemAdjustmentRepository: lineItemAdjustmentRepo,
})
beforeAll(async () => {
jest.clearAllMocks()
})
it("calls lineItemAdjustment retrieve method", async () => {
await lineItemAdjustmentService.retrieve("lia-1", {
relations: ["item"],
})
expect(lineItemAdjustmentRepo.findOne).toHaveBeenCalledTimes(1)
expect(lineItemAdjustmentRepo.findOne).toHaveBeenCalledWith({
where: { id: "lia-1" },
relations: ["item"],
})
})
it("fails when lineItemAdjustment is not found", async () => {
await expect(
lineItemAdjustmentService.retrieve("not-existing")
).rejects.toThrow(
`Line item adjustment with id: not-existing was not found`
)
})
})
describe("create", () => {
const lineItemAdjustment = {
id: "lia-1",
amount: 2000,
description: "discount",
item_id: "li-3",
discount_id: "disc_999",
}
const lineItemAdjustmentRepo = MockRepository({
create: (f) => Promise.resolve(lineItemAdjustment),
save: (f) => Promise.resolve(lineItemAdjustment),
})
const lineItemAdjustmentService = new LineItemAdjustmentService({
manager: MockManager,
lineItemAdjustmentRepository: lineItemAdjustmentRepo,
eventBusService: EventBusServiceMock,
})
beforeAll(async () => {
jest.clearAllMocks()
})
it("calls lineItemAdjustment create method", async () => {
await lineItemAdjustmentService.create({
amount: 2000,
description: "discount",
item_id: "li-3",
discount_id: "disc_999",
})
expect(lineItemAdjustmentRepo.create).toHaveBeenCalledTimes(1)
expect(lineItemAdjustmentRepo.create).toHaveBeenCalledWith({
amount: 2000,
description: "discount",
item_id: "li-3",
discount_id: "disc_999",
})
expect(lineItemAdjustmentRepo.save).toHaveBeenCalledTimes(1)
expect(lineItemAdjustmentRepo.save).toHaveBeenCalledWith({
id: "lia-1",
amount: 2000,
description: "discount",
item_id: "li-3",
discount_id: "disc_999",
})
})
})
describe("update", () => {
const lineItemAdjustment = { id: "lia-1" }
const lineItemAdjustmentRepo = MockRepository({
findOne: (f) => Promise.resolve(lineItemAdjustment),
save: (f) => Promise.resolve(lineItemAdjustment),
})
const lineItemAdjustmentService = new LineItemAdjustmentService({
manager: MockManager,
lineItemAdjustmentRepository: lineItemAdjustmentRepo,
eventBusService: EventBusServiceMock,
})
beforeAll(async () => {
jest.clearAllMocks()
})
it("calls lineItemAdjustment uppdate method", async () => {
await lineItemAdjustmentService.update("lia-1", {
amount: 6000,
})
expect(lineItemAdjustmentRepo.save).toHaveBeenCalledTimes(1)
expect(lineItemAdjustmentRepo.save).toHaveBeenCalledWith({
...lineItemAdjustment,
})
})
})
describe("delete", () => {
beforeAll(async () => {
jest.clearAllMocks()
})
describe("delete by line item adjustment id", () => {
const lineItemAdjustment = { id: "lia-1", item_id: "li-1" }
const lineItemAdjustmentRepo = MockRepository({
find: (f) => Promise.resolve(lineItemAdjustment),
})
const lineItemAdjustmentService = new LineItemAdjustmentService({
manager: MockManager,
lineItemAdjustmentRepository: lineItemAdjustmentRepo,
eventBusService: EventBusServiceMock,
})
it("calls lineItemAdjustment delete method with the right params", async () => {
await lineItemAdjustmentService.delete("lia-1")
expect(lineItemAdjustmentRepo.find).toHaveBeenCalledTimes(1)
expect(lineItemAdjustmentRepo.find).toHaveBeenCalledWith({
where: {
id: "lia-1",
},
})
expect(lineItemAdjustmentRepo.remove).toHaveBeenCalledTimes(1)
expect(lineItemAdjustmentRepo.remove).toHaveBeenCalledWith(
lineItemAdjustment
)
})
})
describe("delete by item ids", () => {
const lineItemAdjustment = [
{ id: "lia-1", item: "li-1" },
{ id: "lia-2", item_id: "li-2" },
]
const lineItemAdjustmentRepo = MockRepository({
find: (f) => Promise.resolve(lineItemAdjustment),
})
const lineItemAdjustmentService = new LineItemAdjustmentService({
manager: MockManager,
lineItemAdjustmentRepository: lineItemAdjustmentRepo,
eventBusService: EventBusServiceMock,
})
it("calls lineItemAdjustment delete method with the right query", async () => {
const query = { item_id: ["li-1", "li-2", "li-3"] }
await lineItemAdjustmentService.delete(query)
expect(lineItemAdjustmentRepo.find).toHaveBeenCalledTimes(1)
expect(lineItemAdjustmentRepo.find).toHaveBeenCalledWith({
where: {
item_id: In(query.item_id),
},
})
expect(lineItemAdjustmentRepo.remove).toHaveBeenCalledTimes(1)
expect(lineItemAdjustmentRepo.remove).toHaveBeenCalledWith(
lineItemAdjustment
)
})
})
})
describe("createAdjustments", () => {
beforeEach(async () => {
jest.clearAllMocks()
})
const lineItemAdjustmentRepo = MockRepository({
find: (f) => Promise.resolve(lineItemAdjustment),
})
const lineItemAdjustmentService = new LineItemAdjustmentService({
manager: MockManager,
lineItemAdjustmentRepository: lineItemAdjustmentRepo,
discountService: DiscountServiceMock,
eventBusService: EventBusServiceMock,
})
lineItemAdjustmentService.createAdjustmentForLineItem = jest
.fn()
.mockImplementation(() => {
return Promise.resolve({
item_id: "li-1",
amount: 1000,
discount_id: "disc-1",
id: "lia-1",
description: "discount",
})
})
it("calls createAdjustmentForLineItem once when given a line item", () => {
const cart = {
id: "cart1",
discounts: ["disc-1"],
items: [{ id: "li-1" }],
},
lineItem = { id: "li-1" }
lineItemAdjustmentService.createAdjustments(cart, lineItem)
expect(
lineItemAdjustmentService.createAdjustmentForLineItem
).toHaveBeenCalledTimes(1)
expect(
lineItemAdjustmentService.createAdjustmentForLineItem
).toHaveBeenCalledWith(cart, lineItem)
})
it("calls createAdjustmentForLineItem 3 times when given a cart containing 3 line items", () => {
const cart = {
id: "cart1",
discounts: ["disc-1"],
items: [
{
id: "li-2",
},
{
id: "li-3",
},
{
id: "li-4",
},
],
}
lineItemAdjustmentService.createAdjustments(cart)
expect(
lineItemAdjustmentService.createAdjustmentForLineItem
).toHaveBeenCalledTimes(3)
expect(
lineItemAdjustmentService.createAdjustmentForLineItem
).toHaveBeenNthCalledWith(1, cart, { id: "li-2" })
})
})
})
+42 -21
View File
@@ -2,10 +2,11 @@ import paymentService from "medusa-interfaces/dist/payment-service"
import { IdMap, MockRepository, MockManager } from "medusa-test-utils"
import SwapService from "../swap"
import { InventoryServiceMock } from "../__mocks__/inventory"
import { LineItemAdjustmentServiceMock } from "../__mocks__/line-item-adjustment"
const eventBusService = {
emit: jest.fn(),
withTransaction: function () {
withTransaction: function() {
return this
},
}
@@ -166,7 +167,7 @@ describe("SwapService", () => {
Promise.resolve({ id: "cart", items: [{ id: "test-item" }] })
),
update: jest.fn().mockReturnValue(Promise.resolve()),
withTransaction: function () {
withTransaction: function() {
return this
},
}
@@ -178,7 +179,7 @@ describe("SwapService", () => {
const customShippingOptionService = {
create: jest.fn().mockReturnValue(Promise.resolve({ id: "cso-test" })),
update: jest.fn().mockReturnValue(Promise.resolve()),
withTransaction: function () {
withTransaction: function() {
return this
},
}
@@ -188,7 +189,7 @@ describe("SwapService", () => {
update: jest.fn().mockImplementation((d) => Promise.resolve(d)),
retrieve: () => Promise.resolve({}),
createReturnLines: jest.fn(() => Promise.resolve()),
withTransaction: function () {
withTransaction: function() {
return this
},
}
@@ -200,6 +201,7 @@ describe("SwapService", () => {
cartService,
lineItemService,
customShippingOptionService,
lineItemAdjustmentService: LineItemAdjustmentServiceMock,
})
it("finds swap and calls return create cart", async () => {
@@ -219,6 +221,7 @@ describe("SwapService", () => {
"order.claims",
"order.claims.additional_items",
"additional_items",
"additional_items.variant",
"return_order",
"return_order.items",
"return_order.shipping_method",
@@ -251,6 +254,24 @@ describe("SwapService", () => {
expect(cartService.create).toHaveBeenCalledTimes(1)
// expect(cartService.update).toHaveBeenCalledTimes(1)
expect(lineItemService.update).toHaveBeenCalledTimes(1)
expect(lineItemService.update).toHaveBeenCalledWith("test", {
cart_id: "cart",
})
expect(
LineItemAdjustmentServiceMock.createAdjustmentForLineItem
).toHaveBeenCalledTimes(1)
expect(
LineItemAdjustmentServiceMock.createAdjustmentForLineItem
).toHaveBeenCalledWith(
{ id: "cart" },
{
id: "test",
data: "lines",
}
)
expect(swapRepo.save).toHaveBeenCalledTimes(1)
expect(swapRepo.save).toHaveBeenCalledWith({
...existing,
@@ -330,7 +351,7 @@ describe("SwapService", () => {
const swapRepo = MockRepository()
const returnService = {
create: jest.fn().mockReturnValue(Promise.resolve({ id: "ret" })),
withTransaction: function () {
withTransaction: function() {
return this
},
}
@@ -431,7 +452,7 @@ describe("SwapService", () => {
{ items: [{ item_id: "1234", quantity: 2 }], data: "new" },
])
),
withTransaction: function () {
withTransaction: function() {
return this
},
}
@@ -452,7 +473,7 @@ describe("SwapService", () => {
const lineItemService = {
update: jest.fn(),
retrieve: () => Promise.resolve({}),
withTransaction: function () {
withTransaction: function() {
return this
},
}
@@ -541,7 +562,7 @@ describe("SwapService", () => {
})
}
}),
withTransaction: function () {
withTransaction: function() {
return this
},
}
@@ -595,14 +616,14 @@ describe("SwapService", () => {
data: "new",
})
}),
withTransaction: function () {
withTransaction: function() {
return this
},
}
const eventBusService = {
emit: jest.fn().mockReturnValue(Promise.resolve()),
withTransaction: function () {
withTransaction: function() {
return this
},
}
@@ -643,7 +664,7 @@ describe("SwapService", () => {
const lineItemService = {
update: jest.fn(),
retrieve: () => Promise.resolve({}),
withTransaction: function () {
withTransaction: function() {
return this
},
}
@@ -655,7 +676,7 @@ describe("SwapService", () => {
.mockReturnValue(
Promise.resolve({ id: "cart", items: [{ id: "test-item" }] })
),
withTransaction: function () {
withTransaction: function() {
return this
},
}
@@ -730,7 +751,7 @@ describe("SwapService", () => {
const eventBusService = {
emit: jest.fn().mockReturnValue(Promise.resolve()),
withTransaction: function () {
withTransaction: function() {
return this
},
}
@@ -745,7 +766,7 @@ describe("SwapService", () => {
updateShippingMethod: () => {
return Promise.resolve()
},
withTransaction: function () {
withTransaction: function() {
return this
},
}
@@ -760,7 +781,7 @@ describe("SwapService", () => {
update: () => {
return Promise.resolve()
},
withTransaction: function () {
withTransaction: function() {
return this
},
}
@@ -775,14 +796,14 @@ describe("SwapService", () => {
cancelPayment: jest.fn(() => {
return Promise.resolve()
}),
withTransaction: function () {
withTransaction: function() {
return this
},
}
const inventoryService = {
...InventoryServiceMock,
withTransaction: function () {
withTransaction: function() {
return this
},
}
@@ -904,7 +925,7 @@ describe("SwapService", () => {
describe("success", () => {
const eventBusService = {
emit: jest.fn().mockReturnValue(Promise.resolve()),
withTransaction: function () {
withTransaction: function() {
return this
},
}
@@ -916,7 +937,7 @@ describe("SwapService", () => {
refundPayment: jest.fn((g) =>
g[0].id === "good" ? Promise.resolve() : Promise.reject()
),
withTransaction: function () {
withTransaction: function() {
return this
},
}
@@ -1044,7 +1065,7 @@ describe("SwapService", () => {
const eventBusService = {
emit: jest.fn().mockReturnValue(Promise.resolve()),
withTransaction: function () {
withTransaction: function() {
return this
},
}
@@ -1107,7 +1128,7 @@ describe("SwapService", () => {
const paymentProviderService = {
cancelPayment: jest.fn(() => Promise.resolve({})),
withTransaction: function () {
withTransaction: function() {
return this
},
}
+209 -158
View File
@@ -55,136 +55,180 @@ const discounts = {
},
}
const applyDiscount = (cart, discount) => {
let newCart = { ...cart }
if (newCart.items) {
newCart.items = cart.items.map((item) => {
return {
...item,
adjustments: [
{
item_id: item.id,
amount: calculateAdjustment(newCart, item, discount),
description: "discount",
discount_id: discount.id,
},
],
}
})
}
return newCart
}
const calculateAdjustment = (cart, lineItem, discount) => {
let amount = discount.rule.value * lineItem.quantity
let lineItemPrice = lineItem.unit_price * lineItem.quantity
if (discount.rule.type === "fixed" && discount.rule.allocation === "total") {
let subtotal = cart.items.reduce(
(total, item) => total + item.unit_price * item.quantity,
0
)
const nominator = Math.min(discount.rule.value, subtotal)
amount = Math.round((lineItemPrice / subtotal) * nominator)
} else if (discount.rule.type === "percentage") {
amount = Math.round((lineItemPrice * discount.rule.value) / 100)
}
return amount > lineItemPrice ? lineItemPrice : amount
}
describe("TotalsService", () => {
const container = {
taxProviderService: {},
taxCalculationStrategy: {},
}
// TODO: Redo tests to include new line item adjustments
describe("getAllocationItemDiscounts", () => {
let res
// describe("getAllocationItemDiscounts", () => {
// let res
const totalsService = new TotalsService(container)
// const totalsService = new TotalsService(container)
beforeEach(() => {
jest.clearAllMocks()
})
// beforeEach(() => {
// jest.clearAllMocks()
// })
it("calculates item with percentage discount", async () => {
const cart = {
items: [
{
id: "test",
allow_discounts: true,
unit_price: 10,
quantity: 10,
variant: {
id: "testv",
product_id: "testp",
},
adjustments: [{ amount: 10 }],
},
],
}
// it("calculates item with percentage discount", async () => {
// const cart = {
// items: [
// {
// id: "test",
// allow_discounts: true,
// unit_price: 10,
// quantity: 10,
// variant: {
// id: "testv",
// product_id: "testp",
// },
// },
// ],
// }
const discount = {
rule: {
type: "percentage",
value: 10,
},
}
// const discount = {
// rule: {
// type: "percentage",
// value: 10,
// },
// }
res = totalsService.getAllocationItemDiscounts(discount, cart)
// res = totalsService.getAllocationItemDiscounts(discount, cart)
expect(res).toEqual([
{
lineItem: {
id: "test",
allow_discounts: true,
unit_price: 10,
quantity: 10,
variant: {
id: "testv",
product_id: "testp",
},
adjustments: [{ amount: 10 }],
},
variant: "testv",
amount: 10,
},
])
})
// expect(res).toEqual([
// {
// lineItem: {
// id: "test",
// allow_discounts: true,
// unit_price: 10,
// quantity: 10,
// variant: {
// id: "testv",
// product_id: "testp",
// },
// },
// variant: "testv",
// amount: 10,
// },
// ])
// })
it("calculates item with fixed discount", async () => {
const cart = {
items: [
{
id: "exists",
allow_discounts: true,
unit_price: 10,
variant: {
id: "testv",
product_id: "testp",
},
quantity: 10,
adjustments: [{ amount: 90 }],
},
],
}
// it("calculates item with fixed discount", async () => {
// const cart = {
// items: [
// {
// id: "exists",
// allow_discounts: true,
// unit_price: 10,
// variant: {
// id: "testv",
// product_id: "testp",
// },
// quantity: 10,
// },
// ],
// }
const discount = {
rule: {
type: "fixed",
value: 9,
// TODO: Add conditions relation
},
}
// const discount = {
// rule: {
// type: "fixed",
// value: 9,
// },
// }
res = totalsService.getAllocationItemDiscounts(discount, cart)
// res = totalsService.getAllocationItemDiscounts(discount, cart)
expect(res).toEqual([
{
lineItem: {
id: "exists",
allow_discounts: true,
unit_price: 10,
variant: {
id: "testv",
product_id: "testp",
},
quantity: 10,
adjustments: [{ amount: 90 }],
},
variant: "testv",
amount: 90,
},
])
})
// expect(res).toEqual([
// {
// lineItem: {
// id: "exists",
// allow_discounts: true,
// unit_price: 10,
// variant: {
// id: "testv",
// product_id: "testp",
// },
// quantity: 10,
// },
// variant: "testv",
// amount: 90,
// },
// ])
// })
// not relevant anymore
// it("does not apply discount if no valid variants are provided", async () => {
// const cart = {
// items: [
// {
// id: "exists",
// allow_discounts: true,
// unit_price: 10,
// variant: {
// id: "testv",
// product_id: "testp",
// },
// quantity: 10,
// },
// ],
// }
// it("does not apply discount if no valid variants are provided", async () => {
// const cart = {
// items: [
// {
// id: "exists",
// allow_discounts: true,
// unit_price: 10,
// variant: {
// id: "testv",
// product_id: "testp",
// },
// quantity: 10,
// },
// ],
// }
// const discount = {
// rule: {
// type: "fixed",
// value: 9,
// // TODO: Add conditions relation
// },
// }
// res = totalsService.getAllocationItemDiscounts(discount, cart)
// const discount = {
// rule: {
// type: "fixed",
// value: 9,
// },
// }
// res = totalsService.getAllocationItemDiscounts(discount, cart)
// expect(res).toEqual([])
// })
// })
// expect(res).toEqual([])
// })
})
describe("getDiscountTotal", () => {
let res
@@ -223,32 +267,36 @@ describe("TotalsService", () => {
discountCart.discounts = []
})
it("calculate total precentage discount", async () => {
it("calculate total percentage discount", async () => {
discountCart.discounts.push(discounts.total10Percent)
res = totalsService.getDiscountTotal(discountCart)
let cart = applyDiscount(discountCart, discounts.total10Percent)
res = totalsService.getDiscountTotal(cart)
expect(res).toEqual(28)
})
// TODO: Redo tests to include new line item adjustments
// it("calculate item fixed discount", async () => {
// discountCart.discounts.push(discounts.item2Fixed)
// res = totalsService.getDiscountTotal(discountCart)
it("calculate item fixed discount", async () => {
discountCart.discounts.push(discounts.item2Fixed)
let cart = applyDiscount(discountCart, discounts.item2Fixed)
res = totalsService.getDiscountTotal(cart)
// expect(res).toEqual(20)
// })
expect(res).toEqual(40)
})
// it("calculate item percentage discount", async () => {
// discountCart.discounts.push(discounts.item10Percent)
// res = totalsService.getDiscountTotal(discountCart)
it("calculate item percentage discount", async () => {
discountCart.discounts.push(discounts.item10Percent)
let cart = applyDiscount(discountCart, discounts.item10Percent)
res = totalsService.getDiscountTotal(cart)
// expect(res).toEqual(10)
// })
expect(res).toEqual(28)
})
it("calculate total fixed discount", async () => {
discountCart.discounts.push(discounts.total10Fixed)
res = totalsService.getDiscountTotal(discountCart)
let cart = applyDiscount(discountCart, discounts.total10Fixed)
res = totalsService.getDiscountTotal(cart)
expect(res).toEqual(10)
})
@@ -388,43 +436,45 @@ describe("TotalsService", () => {
// expect(res).toEqual(1244)
// })
// it("calculates refund with item fixed discount", async () => {
// orderToRefund.discounts.push(discounts.item2Fixed)
// res = totalsService.getRefundTotal(orderToRefund, [
// {
// id: "line2",
// unit_price: 100,
// allow_discounts: true,
// variant: {
// id: "variant",
// product_id: "testp2",
// },
// quantity: 10,
// returned_quantity: 0,
// },
// ])
it("calculates refund with item fixed discount", async () => {
orderToRefund.discounts.push(discounts.item2Fixed)
let order = applyDiscount(orderToRefund, discounts.item2Fixed)
res = totalsService.getRefundTotal(order, [
{
id: "line2",
unit_price: 100,
allow_discounts: true,
variant: {
id: "variant",
product_id: "testp2",
},
quantity: 10,
returned_quantity: 0,
},
])
// expect(res).toEqual(1225)
// })
expect(res).toEqual(1225)
})
// it("calculates refund with item percentage discount", async () => {
// orderToRefund.discounts.push(discounts.item10Percent)
// res = totalsService.getRefundTotal(orderToRefund, [
// {
// id: "line2",
// unit_price: 100,
// allow_discounts: true,
// variant: {
// id: "variant",
// product_id: "testp2",
// },
// quantity: 10,
// returned_quantity: 0,
// },
// ])
it("calculates refund with item percentage discount", async () => {
orderToRefund.discounts.push(discounts.item10Percent)
let order = applyDiscount(orderToRefund, discounts.item10Percent)
res = totalsService.getRefundTotal(order, [
{
id: "line2",
unit_price: 100,
allow_discounts: true,
variant: {
id: "variant",
product_id: "testp2",
},
quantity: 10,
returned_quantity: 0,
},
])
// expect(res).toEqual(1125)
// })
expect(res).toEqual(1125)
})
it("throws if line items to return is not in order", async () => {
const work = () =>
@@ -444,6 +494,7 @@ describe("TotalsService", () => {
expect(work).toThrow("Line item does not exist on order")
})
})
describe("getShippingTotal", () => {
let res
const totalsService = new TotalsService(container)
+83 -99
View File
@@ -21,12 +21,10 @@ import {
LineItemUpdate,
} from "../types/cart"
import { FindConfig, TotalField } from "../types/common"
import CustomShippingOptionService from "./custom-shipping-option"
import CustomerService from "./customer"
import DiscountService from "./discount"
import EventBusService from "./event-bus"
import GiftCardService from "./gift-card"
import InventoryService from "./inventory"
import LineItemService from "./line-item"
import PaymentProviderService from "./payment-provider"
import ProductService from "./product"
@@ -35,6 +33,9 @@ import RegionService from "./region"
import ShippingOptionService from "./shipping-option"
import TaxProviderService from "./tax-provider"
import TotalsService from "./totals"
import InventoryService from "./inventory"
import CustomShippingOptionService from "./custom-shipping-option"
import LineItemAdjustmentService from "./line-item-adjustment"
type CartConstructorProps = {
manager: EntityManager
@@ -56,6 +57,7 @@ type CartConstructorProps = {
totalsService: TotalsService
inventoryService: InventoryService
customShippingOptionService: CustomShippingOptionService
lineItemAdjustmentService: LineItemAdjustmentService
priceSelectionStrategy: IPriceSelectionStrategy
}
@@ -92,6 +94,7 @@ class CartService extends BaseService {
private paymentSessionRepository_: typeof PaymentSessionRepository
private inventoryService_: InventoryService
private customShippingOptionService_: CustomShippingOptionService
private lineItemAdjustmentService_: LineItemAdjustmentService
private priceSelectionStrategy_: IPriceSelectionStrategy
constructor({
@@ -114,6 +117,7 @@ class CartService extends BaseService {
paymentSessionRepository,
inventoryService,
customShippingOptionService,
lineItemAdjustmentService,
priceSelectionStrategy,
}: CartConstructorProps) {
super()
@@ -137,6 +141,7 @@ class CartService extends BaseService {
this.inventoryService_ = inventoryService
this.customShippingOptionService_ = customShippingOptionService
this.taxProviderService_ = taxProviderService
this.lineItemAdjustmentService_ = lineItemAdjustmentService
this.priceSelectionStrategy_ = priceSelectionStrategy
}
@@ -165,6 +170,7 @@ class CartService extends BaseService {
giftCardService: this.giftCardService_,
inventoryService: this.inventoryService_,
customShippingOptionService: this.customShippingOptionService_,
lineItemAdjustmentService: this.lineItemAdjustmentService_,
priceSelectionStrategy: this.priceSelectionStrategy_,
})
@@ -457,7 +463,12 @@ class CartService extends BaseService {
await this.lineItemService_.withTransaction(manager).delete(lineItem.id)
const result = await this.retrieve(cartId)
const result = await this.retrieve(cartId, {
relations: ["items", "discounts", "discounts.rule"],
})
await this.refreshAdjustments_(result)
// Notify subscribers
await this.eventBus_
.withTransaction(manager)
@@ -512,9 +523,12 @@ class CartService extends BaseService {
relations: [
"shipping_methods",
"items",
"items.adjustments",
"payment_sessions",
"items.variant",
"items.variant.product",
"discounts",
"discounts.rule",
],
})
@@ -570,10 +584,16 @@ class CartService extends BaseService {
}
}
const result = await this.retrieve(cartId)
const result = await this.retrieve(cartId, {
relations: ["items", "discounts", "discounts.rule"],
})
await this.refreshAdjustments_(result)
await this.eventBus_
.withTransaction(manager)
.emit(CartService.Events.UPDATED, result)
return result
})
}
@@ -592,7 +612,7 @@ class CartService extends BaseService {
): Promise<Cart> {
return this.atomicPhase_(async (manager: EntityManager) => {
const cart = await this.retrieve(cartId, {
relations: ["items", "payment_sessions"],
relations: ["items", "items.adjustments", "payment_sessions"],
})
// Ensure that the line item exists in the cart
@@ -621,8 +641,13 @@ class CartService extends BaseService {
.withTransaction(manager)
.update(lineItemId, lineItemUpdate)
const result = await this.retrieve(cartId, {
relations: ["items", "discounts", "discounts.rule"],
})
await this.refreshAdjustments_(result)
// Update the line item
const result = await this.retrieve(cartId)
await this.eventBus_
.withTransaction(manager)
.emit(CartService.Events.UPDATED, result)
@@ -1004,109 +1029,51 @@ class CartService extends BaseService {
* @return the result of the update operation
*/
async applyDiscount(cart: Cart, discountCode: string): Promise<void> {
const discount = await this.discountService_.retrieveByCode(discountCode, [
"rule",
"regions",
])
if (cart.customer_id) {
const canApply = await this.discountService_.canApplyForCustomer(
discount.rule.id,
cart.customer_id
return this.atomicPhase_(async (manager) => {
const discount = await this.discountService_.retrieveByCode(
discountCode,
["rule", "regions"]
)
if (!canApply) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"Discount is not valid for customer"
)
await this.discountService_.validateDiscountForCartOrThrow(cart, discount)
const rule = discount.rule
// if discount is already there, we simply resolve
if (cart.discounts.find(({ id }) => id === discount.id)) {
return Promise.resolve()
}
}
const rule = discount.rule
const toParse = [...cart.discounts, discount]
// if limit is set and reached, we make an early exit
if (discount.usage_limit) {
discount.usage_count = discount.usage_count || 0
if (discount.usage_count >= discount.usage_limit) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"Discount has been used maximum allowed times"
)
}
}
const today = new Date()
if (discount.starts_at > today) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"Discount is not valid yet"
)
}
if (discount.ends_at && discount.ends_at < today) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"Discount is expired"
)
}
let regions = discount.regions
if (discount.parent_discount_id) {
const parent = await this.discountService_.retrieve(
discount.parent_discount_id,
{
relations: ["rule", "regions"],
}
)
regions = parent.regions
}
if (discount.is_disabled) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"The discount code is disabled"
)
}
if (!regions.find(({ id }) => id === cart.region_id)) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"The discount is not available in current region"
)
}
// if discount is already there, we simply resolve
if (cart.discounts.find(({ id }) => id === discount.id)) {
return Promise.resolve()
}
const toParse = [...cart.discounts, discount]
let sawNotShipping = false
const newDiscounts = toParse.map((d) => {
const drule = d.rule
switch (drule.type) {
case "free_shipping":
if (d.rule.type === rule.type) {
return discount
}
return d
default:
if (!sawNotShipping) {
sawNotShipping = true
if (rule.type !== "free_shipping") {
let sawNotShipping = false
const newDiscounts = toParse.map((d) => {
const drule = d.rule
switch (drule.type) {
case "free_shipping":
if (d.rule.type === rule.type) {
return discount
}
return d
}
return null
default:
if (!sawNotShipping) {
sawNotShipping = true
if (rule.type !== "free_shipping") {
return discount
}
return d
}
return null
}
})
cart.discounts = newDiscounts.filter(Boolean) as Discount[]
// ignore if free shipping
if (rule.type !== "free_shipping" && cart?.items) {
await this.refreshAdjustments_(cart)
}
})
cart.discounts = newDiscounts.filter(Boolean) as Discount[]
}
/**
@@ -1888,6 +1855,23 @@ class CartService extends BaseService {
})
}
async refreshAdjustments_(cart: Cart): Promise<void> {
return this.atomicPhase_(async (manager: EntityManager) => {
const nonReturnLines = cart.items.filter((item) => !item.is_return)
const nonReturnLineIDs = nonReturnLines.map((i) => i.id)
// delete all old non return line item adjustments
await this.lineItemAdjustmentService_.withTransaction(manager).delete({
item_id: nonReturnLineIDs,
})
// potentially create/update line item adjustments
await this.lineItemAdjustmentService_
.withTransaction(manager)
.createAdjustments(cart)
})
}
/**
* Dedicated method to delete metadata for a cart.
* @param cartId - the cart to delete metadata from.
+94
View File
@@ -30,6 +30,7 @@ import {
UpdateDiscountInput,
UpsertDiscountConditionInput,
} from "../types/discount"
import { isFuture, isPast } from "../utils/date-helpers"
import { formatException, PostgresError } from "../utils/exception-formatter"
/**
@@ -735,6 +736,99 @@ class DiscountService extends BaseService {
return adjustment >= fullItemPrice ? fullItemPrice : adjustment
}
async validateDiscountForCartOrThrow(
cart: Cart,
discount: Discount
): Promise<void> {
if (this.hasReachedLimit(discount)) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"Discount has been used maximum allowed times"
)
}
if (this.hasNotStarted(discount)) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"Discount is not valid yet"
)
}
if (this.hasExpired(discount)) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"Discount is expired"
)
}
if (this.isDisabled(discount)) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"The discount code is disabled"
)
}
const isValidForRegion = await this.isValidForRegion(
discount,
cart.region_id
)
if (!isValidForRegion) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"The discount is not available in current region"
)
}
if (cart.customer_id) {
const canApplyForCustomer = await this.canApplyForCustomer(
discount.rule.id,
cart.customer_id
)
if (!canApplyForCustomer) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"Discount is not valid for customer"
)
}
}
}
hasReachedLimit(discount: Discount): boolean {
const count = discount.usage_count || 0
const limit = discount.usage_limit
return !!limit && count >= limit
}
hasNotStarted(discount: Discount): boolean {
return isFuture(discount.starts_at)
}
hasExpired(discount: Discount): boolean {
return discount.ends_at && isPast(discount.ends_at)
}
isDisabled(discount: Discount): boolean {
return discount.is_disabled
}
async isValidForRegion(
discount: Discount,
region_id: string
): Promise<boolean> {
let regions = discount.regions
if (discount.parent_discount_id) {
const parent = await this.retrieve(discount.parent_discount_id, {
relations: ["rule", "regions"],
})
regions = parent.regions
}
return regions.find(({ id }) => id === region_id) !== undefined
}
async canApplyForCustomer(
discountRuleId: string,
customerId: string | undefined
@@ -290,6 +290,7 @@ class DraftOrderService extends BaseService {
.generate(item.variant_id, data.region_id, item.quantity, {
metadata: item?.metadata || {},
unit_price: item.unit_price,
cart: createdCart,
})
await this.lineItemService_.withTransaction(manager).create({
@@ -0,0 +1,303 @@
import { EntityManager } from "typeorm"
import { BaseService } from "medusa-interfaces"
import { MedusaError } from "medusa-core-utils"
import { LineItemAdjustmentRepository } from "../repositories/line-item-adjustment"
import { FindConfig } from "../types/common"
import { LineItemAdjustment } from "../models/line-item-adjustment"
import { FilterableLineItemAdjustmentProps } from "../types/line-item-adjustment"
import { LineItem } from "../models/line-item"
import { Cart } from "../models/cart"
import DiscountService from "./discount"
import { ProductVariant } from "../models/product-variant"
type LineItemAdjustmentServiceProps = {
manager: EntityManager
lineItemAdjustmentRepository: typeof LineItemAdjustmentRepository
discountService: DiscountService
}
type AdjustmentContext = {
variant: ProductVariant
}
type GeneratedAdjustment = Omit<LineItem, "id" | "item_id">
/**
* Provides layer to manipulate line item adjustments.
* @extends BaseService
*/
class LineItemAdjustmentService extends BaseService {
private manager_: EntityManager
private lineItemAdjustmentRepo_: typeof LineItemAdjustmentRepository
private discountService: DiscountService
constructor({
manager,
lineItemAdjustmentRepository,
discountService,
}: LineItemAdjustmentServiceProps) {
super()
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
* @param config - the config to retrieve the line item adjustment by
* @return the line item adjustment.
*/
async retrieve(
id: string,
config: FindConfig<LineItemAdjustment> = {}
): Promise<LineItemAdjustment> {
const lineItemAdjustmentRepo: LineItemAdjustmentRepository =
this.manager_.getCustomRepository(this.lineItemAdjustmentRepo_)
const query = this.buildQuery_({ id }, config)
const lineItemAdjustment = await lineItemAdjustmentRepo.findOne(query)
if (!lineItemAdjustment) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Line item adjustment with id: ${id} was not found`
)
}
return lineItemAdjustment
}
/**
* Creates a line item adjustment
* @param data - the line item adjustment to create
* @return line item adjustment
*/
async create(data: Partial<LineItemAdjustment>): Promise<LineItemAdjustment> {
return await this.atomicPhase_(async (manager: EntityManager) => {
const lineItemAdjustmentRepo: LineItemAdjustmentRepository =
manager.getCustomRepository(this.lineItemAdjustmentRepo_)
const lineItemAdjustment = await lineItemAdjustmentRepo.create(data)
return await lineItemAdjustmentRepo.save(lineItemAdjustment)
})
}
/**
* Creates a line item adjustment
* @param id - the line item adjustment id to update
* @param data - the line item adjustment to create
* @return line item adjustment
*/
async update(
id: string,
data: Partial<LineItemAdjustment>
): Promise<LineItemAdjustment> {
return await this.atomicPhase_(async (manager: EntityManager) => {
const lineItemAdjustmentRepo: LineItemAdjustmentRepository =
manager.getCustomRepository(this.lineItemAdjustmentRepo_)
const lineItemAdjustment = await this.retrieve(id)
const { metadata, ...rest } = data
if (metadata) {
lineItemAdjustment.metadata = this.setMetadata_(
lineItemAdjustment,
metadata
)
}
for (const [key, value] of Object.entries(rest)) {
lineItemAdjustment[key] = value
}
const result = await lineItemAdjustmentRepo.save(lineItemAdjustment)
return result
})
}
/**
* Lists line item adjustments
* @param selector - the query object for find
* @param config - the config to be used for find
* @return the result of the find operation
*/
async list(
selector: FilterableLineItemAdjustmentProps = {},
config: FindConfig<LineItemAdjustment> = { skip: 0, take: 20 }
): Promise<LineItemAdjustment[]> {
const lineItemAdjustmentRepo = this.manager_.getCustomRepository(
this.lineItemAdjustmentRepo_
)
const query = this.buildQuery_(selector, config)
return await lineItemAdjustmentRepo.find(query)
}
/**
* Deletes line item adjustments matching a selector
* @param selectorOrId - the query object for find or the line item adjustment id
* @return the result of the delete operation
*/
async delete(
selectorOrId: string | FilterableLineItemAdjustmentProps
): Promise<void> {
return this.atomicPhase_(async (manager) => {
const lineItemAdjustmentRepo: LineItemAdjustmentRepository =
manager.getCustomRepository(this.lineItemAdjustmentRepo_)
if (typeof selectorOrId === "string") {
return await this.delete({ id: selectorOrId })
}
const query = this.buildQuery_(selectorOrId)
const lineItemAdjustments = await lineItemAdjustmentRepo.find(query)
await lineItemAdjustmentRepo.remove(lineItemAdjustments)
return Promise.resolve()
})
}
/**
* Creates adjustment for a line item
* @param cart - the cart object holding discounts
* @param generatedLineItem - the line item for which a line item adjustment might be created
* @param context - the line item for which a line item adjustment might be created
* @return a line item adjustment or undefined if no adjustment was created
*/
async generateAdjustments(
cart: Cart,
generatedLineItem: LineItem,
context: AdjustmentContext
): Promise<GeneratedAdjustment[]> {
return this.atomicPhase_(async (manager) => {
// if lineItem should not be discounted
// or lineItem is a return line item
// or the cart does not have any discounts
// then do nothing
if (
!generatedLineItem.allow_discounts ||
generatedLineItem.is_return ||
!cart?.discounts?.length
) {
return []
}
const [discount] = cart.discounts.filter(
(d) => d.rule.type !== "free_shipping"
)
// if no discount is applied to the cart then return
if (!discount) {
return []
}
const lineItemProduct = context.variant.product_id
const isValid = await this.discountService
.withTransaction(manager)
.validateDiscountForProduct(discount.rule_id, lineItemProduct)
// if discount is not valid for line item, then do nothing
if (!isValid) {
return []
}
const amount = await this.discountService.calculateDiscountForLineItem(
discount.id,
generatedLineItem,
cart
)
// if discounted amount is 0, then do nothing
if (amount === 0) {
return []
}
const adjustments = [
{
amount,
discount_id: discount.id,
description: "discount",
},
]
return adjustments
})
}
/**
* Creates adjustment for a line item
* @param cart - the cart object holding discounts
* @param lineItem - the line item for which a line item adjustment might be created
* @return a line item adjustment or undefined if no adjustment was created
*/
async createAdjustmentForLineItem(
cart: Cart,
lineItem: LineItem
): Promise<LineItemAdjustment[]> {
const adjustments = await this.generateAdjustments(cart, lineItem, {
variant: lineItem.variant,
})
const createdAdjustments: LineItemAdjustment[] = []
for (const adjustment of adjustments) {
const created = await this.create({
item_id: lineItem.id,
...adjustment,
})
createdAdjustments.push(created)
}
return createdAdjustments
}
/**
* Creates adjustment for a line item
* @param cart - the cart object holding discounts
* @param lineItem - the line item for which a line item adjustment might be created
* @return if a lineItem was given, returns a line item adjustment or undefined if no adjustment was created
* otherwise returns an array of line item adjustments for each line item in the cart
*/
async createAdjustments(
cart: Cart,
lineItem?: LineItem
): Promise<LineItemAdjustment[] | LineItemAdjustment[][]> {
if (lineItem) {
return await this.createAdjustmentForLineItem(cart, lineItem)
}
if (!cart.items) {
return []
}
return await Promise.all(
cart.items.map((li) => this.createAdjustmentForLineItem(cart, li))
)
}
}
export default LineItemAdjustmentService
+25 -6
View File
@@ -14,6 +14,7 @@ class LineItemService extends BaseService {
productService,
regionService,
cartRepository,
lineItemAdjustmentService,
}) {
super()
@@ -37,6 +38,8 @@ class LineItemService extends BaseService {
/** @private @const {CartRepository} */
this.cartRepository_ = cartRepository
this.lineItemAdjustmentService_ = lineItemAdjustmentService
}
withTransaction(transactionManager) {
@@ -52,6 +55,7 @@ class LineItemService extends BaseService {
productService: this.productService_,
regionService: this.regionService_,
cartRepository: this.cartRepository_,
lineItemAdjustmentService: this.lineItemAdjustmentService_,
})
cloned.transactionManager_ = transactionManager
@@ -131,13 +135,21 @@ class LineItemService extends BaseService {
})
}),
metadata: i.metadata,
adjustments: i.adjustments.map((adjustment) => {
return {
amount: -1 * adjustment.amount,
description: adjustment.description,
discount_id: adjustment.discount_id,
metadata: adjustment.metadata,
}
}),
})
)
return await lineItemRepo.save(toCreate)
}
async generate(variantId, regionId, quantity, config = {}) {
async generate(variantId, regionId, quantity, context = {}) {
return this.atomicPhase_(async (manager) => {
const variant = await this.productVariantService_
.withTransaction(manager)
@@ -153,14 +165,14 @@ class LineItemService extends BaseService {
let price
let shouldMerge = true
if (config.unit_price !== undefined && config.unit_price !== null) {
if (context.unit_price !== undefined && context.unit_price !== null) {
// if custom unit_price, we ensure positive values
// and we choose to not merge the items
shouldMerge = false
if (config.unit_price < 0) {
if (context.unit_price < 0) {
price = 0
} else {
price = config.unit_price
price = context.unit_price
}
} else {
price = await this.productVariantService_
@@ -168,7 +180,7 @@ class LineItemService extends BaseService {
.getRegionPrice(variant.id, {
regionId: region.id,
quantity: quantity,
customer_id: config.customer_id,
customer_id: context.customer_id,
include_discount_prices: true,
})
}
@@ -182,10 +194,17 @@ class LineItemService extends BaseService {
quantity: quantity || 1,
allow_discounts: variant.product.discountable,
is_giftcard: variant.product.is_giftcard,
metadata: config?.metadata || {},
metadata: context?.metadata || {},
should_merge: shouldMerge,
}
if (context.cart) {
const adjustments = await this.lineItemAdjustmentService_
.withTransaction(manager)
.generateAdjustments(context.cart, toCreate, { variant })
toCreate.adjustments = adjustments
}
return toCreate
})
}
+3
View File
@@ -298,12 +298,15 @@ class OrderService extends BaseService {
const relationSet = new Set(relations)
relationSet.add("items")
relationSet.add("items.tax_lines")
relationSet.add("items.adjustments")
relationSet.add("swaps")
relationSet.add("swaps.additional_items")
relationSet.add("swaps.additional_items.tax_lines")
relationSet.add("swaps.additional_items.adjustments")
relationSet.add("claims")
relationSet.add("claims.additional_items")
relationSet.add("claims.additional_items.tax_lines")
relationSet.add("claims.additional_items.adjustments")
relationSet.add("discounts")
relationSet.add("discounts.rule")
relationSet.add("gift_cards")
+10
View File
@@ -33,6 +33,7 @@ class SwapService extends BaseService {
orderService,
inventoryService,
customShippingOptionService,
lineItemAdjustmentService,
}) {
super()
@@ -77,6 +78,9 @@ class SwapService extends BaseService {
/** @private @const {CustomShippingOptionService} */
this.customShippingOptionService_ = customShippingOptionService
/** @private @const {LineItemAdjustmentService} */
this.lineItemAdjustmentService_ = lineItemAdjustmentService
}
withTransaction(transactionManager) {
@@ -99,6 +103,7 @@ class SwapService extends BaseService {
inventoryService: this.inventoryService_,
fulfillmentService: this.fulfillmentService_,
customShippingOptionService: this.customShippingOptionService_,
lineItemAdjustmentService: this.lineItemAdjustmentService_,
})
cloned.transactionManager_ = transactionManager
@@ -549,6 +554,7 @@ class SwapService extends BaseService {
"order.claims",
"order.claims.additional_items",
"additional_items",
"additional_items.variant",
"return_order",
"return_order.items",
"return_order.shipping_method",
@@ -605,6 +611,10 @@ class SwapService extends BaseService {
await this.lineItemService_.withTransaction(manager).update(item.id, {
cart_id: cart.id,
})
// we generate adjustments in case the cart has any discounts that should be applied to the additional items
await this.lineItemAdjustmentService_
.withTransaction(manager)
.createAdjustmentForLineItem(cart, item)
}
// If the swap has a return shipping method the price has to be added to
+55 -65
View File
@@ -200,8 +200,6 @@ class TotalsService extends BaseService {
calculationContext
)
console.log(orderLines)
taxLines = orderLines.filter((ol) => {
if ("shipping_method_id" in ol) {
return ol.shipping_method_id === shippingMethod.id
@@ -465,6 +463,7 @@ class TotalsService extends BaseService {
const discountAmount =
(allocationMap[lineItem.id]?.discount?.unit_amount || 0) *
lineItem.quantity
const lineSubtotal =
lineItem.unit_price * lineItem.quantity - discountAmount
@@ -589,11 +588,46 @@ class TotalsService extends BaseService {
discount: Discount,
cart: Cart | Order
): LineDiscount[] {
const discounts: LineDiscount[] = []
// TODO: Add line item adjustments
const discounts: LineDiscount[] = cart.items.map((item) => ({
lineItem: item,
variant: item.variant.id,
amount: this.getLineItemDiscountAdjustment(item, discount),
}))
return discounts
}
getLineItemDiscountAdjustment(
lineItem: LineItem,
discount: Discount
): number {
const matchingDiscount = lineItem.adjustments.find(
(adjustment) => adjustment.discount_id === discount.id
)
if (!matchingDiscount) {
return 0
}
return matchingDiscount.amount
}
getLineItemAdjustmentsTotal(cartOrOrder: Cart | Order): number {
if (!cartOrOrder?.items?.length) {
return 0
}
return cartOrOrder.items.reduce(
(total, item) =>
total +
item.adjustments?.reduce(
(total, adjustment) => total + adjustment.amount,
0
) || 0,
0
)
}
/**
* Returns the discount amount allocated to the line items of an order.
* @param cartOrOrder - the cart or order to get line discount allocations for
@@ -605,10 +639,6 @@ class TotalsService extends BaseService {
cartOrOrder: Cart | Order,
discount: Discount
): LineDiscountAmount[] {
const subtotal = this.getSubtotal(cartOrOrder, {
excludeNonDiscounts: true,
})
let merged: LineItem[] = [...cartOrOrder.items]
// merge items from order with items from order swaps
@@ -624,43 +654,22 @@ class TotalsService extends BaseService {
}
}
const { type, allocation, value } = discount.rule
if (allocation === "total") {
let percentage = 0
if (type === "percentage") {
percentage = value / 100
} else if (type === "fixed") {
// If the fixed discount exceeds the subtotal we should
// calculate a 100% discount
const nominator = Math.min(value, subtotal)
percentage = nominator / subtotal
}
return merged.map((item) => {
const lineTotal = item.unit_price * item.quantity
return {
item,
amount: item.allow_discounts ? lineTotal * percentage : 0,
}
})
} else if (allocation === "item") {
const allocationDiscounts = this.getAllocationItemDiscounts(
discount,
cartOrOrder
return merged.map((item) => {
const adjustments = item?.adjustments || []
const discountAdjustments = adjustments.filter(
(adjustment) => adjustment.discount_id === discount.id
)
return merged.map((item) => {
const discounted = allocationDiscounts.find(
(a) => a.lineItem.id === item.id
)
return {
item,
amount: discounted ? discounted.amount : 0,
}
})
}
return merged.map((i) => ({ item: i, amount: 0 }))
return {
item,
amount: item.allow_discounts
? discountAdjustments.reduce(
(total, adjustment) => total + adjustment.amount,
0
)
: 0,
}
})
}
/**
@@ -865,32 +874,13 @@ class TotalsService extends BaseService {
return 0
}
const { type, allocation, value } = discount.rule
let toReturn = 0
if (type === "percentage" && allocation === "total") {
toReturn = (subtotal / 100) * value
} else if (type === "percentage" && allocation === "item") {
const itemPercentageDiscounts = this.getAllocationItemDiscounts(
discount,
cartOrOrder
)
toReturn = _.sumBy(itemPercentageDiscounts, (d) => d.amount)
} else if (type === "fixed" && allocation === "total") {
toReturn = value
} else if (type === "fixed" && allocation === "item") {
const itemFixedDiscounts = this.getAllocationItemDiscounts(
discount,
cartOrOrder
)
toReturn = _.sumBy(itemFixedDiscounts, (d) => d.amount)
}
const discountTotal = this.getLineItemAdjustmentsTotal(cartOrOrder)
if (subtotal < 0) {
return this.rounded(Math.max(subtotal, toReturn))
return this.rounded(Math.max(subtotal, discountTotal))
}
return this.rounded(Math.min(subtotal, toReturn))
return this.rounded(Math.min(subtotal, discountTotal))
}
/**
@@ -0,0 +1,27 @@
import { ValidateNested } from "class-validator"
import { IsType } from "../utils/validators/is-type"
import { DateComparisonOperator, StringComparisonOperator } from "./common"
export class FilterableLineItemAdjustmentProps {
@ValidateNested()
@IsType([String, [String], StringComparisonOperator])
id?: string | string[] | StringComparisonOperator
@ValidateNested()
@IsType([String, [String]])
item_id?: string | string[]
@ValidateNested()
@IsType([String, [String]])
description?: string | string[]
@ValidateNested()
@IsType([String, [String]])
resource_id?: string | string[]
@IsType([DateComparisonOperator])
created_at?: DateComparisonOperator
@IsType([DateComparisonOperator])
updated_at?: DateComparisonOperator
}
+11
View File
@@ -0,0 +1,11 @@
import { isDate } from "lodash"
export const isPast = (date: Date | null) => {
const now = new Date()
return isDate(date) && now > date
}
export const isFuture = (date: Date | null) => {
const now = new Date()
return isDate(date) && date > now
}