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:
1 parent
607a382b4e
commit
1cfeb5dbd8
45 files changed
+3290
-581
No files matched your search
@@ -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",
|
||||
|
||||
@@ -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}`
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
@@ -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" })
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user