Feat:discount expiration date (#403)
* discount expiration validation and testing * integration testing * double quotes * add iso8601 package * api testing * add joi validation of start and end dates as well as valid_duration * valid_duration column * service testing * discount validation in services * integration test with invalid date interval * include valid_duration when fetching a discount * rename variable for clarity * add test for dynamic discount with expiration date * remove debug code * adjust tests to reflect valid_duration being included in default fields * additional discount update validation * fixed failing test * set ends_at on dynamic discount creation * discount integration tests * removed unused console.log * removed validation of dynamic discounts by duration and added ends_at to dynamic discount creation * integration tests for dynamic discount with and without duration * optional valid duration for dynamic discounts * allow nullable dynamic discount durations * expect assertions * fix unit test after change to dynamic discounts without duration * change to date instead of string * add assertions * error handling * addressed feedback
This commit is contained in:
@@ -63,6 +63,7 @@
|
||||
"glob": "^7.1.6",
|
||||
"ioredis": "^4.17.3",
|
||||
"ioredis-mock": "^5.6.0",
|
||||
"iso8601-duration": "^1.3.0",
|
||||
"joi": "^17.3.0",
|
||||
"joi-objectid": "^3.0.1",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
|
||||
@@ -46,6 +46,7 @@ describe("POST /admin/discounts/:discount_id/regions/:region_id", () => {
|
||||
"updated_at",
|
||||
"deleted_at",
|
||||
"metadata",
|
||||
"valid_duration",
|
||||
],
|
||||
relations: ["rule", "parent_discount", "regions", "rule.valid_for"],
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ describe("POST /admin/discounts/:discount_id/variants/:variant_id", () => {
|
||||
"updated_at",
|
||||
"deleted_at",
|
||||
"metadata",
|
||||
"valid_duration",
|
||||
],
|
||||
relations: ["rule", "parent_discount", "regions", "rule.valid_for"],
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ describe("POST /admin/discounts", () => {
|
||||
value: 10,
|
||||
allocation: "total",
|
||||
},
|
||||
starts_at: "02/02/2021 13:45",
|
||||
ends_at: "03/14/2021 04:30",
|
||||
},
|
||||
adminSession: {
|
||||
jwt: {
|
||||
@@ -39,12 +41,99 @@ describe("POST /admin/discounts", () => {
|
||||
value: 10,
|
||||
allocation: "total",
|
||||
},
|
||||
starts_at: new Date("02/02/2021 13:45"),
|
||||
ends_at: new Date("03/14/2021 04:30"),
|
||||
is_disabled: false,
|
||||
is_dynamic: false,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("unsuccessful creation with dynamic discount using an invalid iso8601 duration", () => {
|
||||
let subject
|
||||
|
||||
beforeAll(async () => {
|
||||
jest.clearAllMocks()
|
||||
subject = await request("POST", "/admin/discounts", {
|
||||
payload: {
|
||||
code: "TEST",
|
||||
rule: {
|
||||
description: "Test",
|
||||
type: "fixed",
|
||||
value: 10,
|
||||
allocation: "total",
|
||||
},
|
||||
starts_at: "02/02/2021 13:45",
|
||||
is_dynamic: true,
|
||||
valid_duration: "PaMT2D",
|
||||
},
|
||||
adminSession: {
|
||||
jwt: {
|
||||
userId: IdMap.getId("admin_user"),
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("returns 400", () => {
|
||||
expect(subject.status).toEqual(400)
|
||||
})
|
||||
|
||||
it("returns error", () => {
|
||||
expect(subject.body.message[0].message).toEqual(
|
||||
`"valid_duration" must be a valid ISO 8601 duration`
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("successful creation with dynamic discount", () => {
|
||||
let subject
|
||||
|
||||
beforeAll(async () => {
|
||||
jest.clearAllMocks()
|
||||
subject = await request("POST", "/admin/discounts", {
|
||||
payload: {
|
||||
code: "TEST",
|
||||
rule: {
|
||||
description: "Test",
|
||||
type: "fixed",
|
||||
value: 10,
|
||||
allocation: "total",
|
||||
},
|
||||
starts_at: "02/02/2021 13:45",
|
||||
is_dynamic: true,
|
||||
valid_duration: "P1Y2M03DT04H05M",
|
||||
},
|
||||
adminSession: {
|
||||
jwt: {
|
||||
userId: IdMap.getId("admin_user"),
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("returns 200", () => {
|
||||
expect(subject.status).toEqual(200)
|
||||
})
|
||||
|
||||
it("calls service create", () => {
|
||||
expect(DiscountServiceMock.create).toHaveBeenCalledTimes(1)
|
||||
expect(DiscountServiceMock.create).toHaveBeenCalledWith({
|
||||
code: "TEST",
|
||||
rule: {
|
||||
description: "Test",
|
||||
type: "fixed",
|
||||
value: 10,
|
||||
allocation: "total",
|
||||
},
|
||||
starts_at: new Date("02/02/2021 13:45"),
|
||||
is_disabled: false,
|
||||
is_dynamic: true,
|
||||
valid_duration: "P1Y2M03DT04H05M",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("fails on invalid data", () => {
|
||||
let subject
|
||||
|
||||
@@ -74,4 +163,84 @@ describe("POST /admin/discounts", () => {
|
||||
expect(subject.body.message[0].message).toEqual(`"rule.type" is required`)
|
||||
})
|
||||
})
|
||||
|
||||
describe("fails on invalid date intervals", () => {
|
||||
let subject
|
||||
|
||||
beforeAll(async () => {
|
||||
subject = await request("POST", "/admin/discounts", {
|
||||
payload: {
|
||||
code: "TEST",
|
||||
rule: {
|
||||
description: "Test",
|
||||
type: "fixed",
|
||||
value: 10,
|
||||
allocation: "total",
|
||||
},
|
||||
ends_at: "02/02/2021",
|
||||
starts_at: "03/14/2021",
|
||||
},
|
||||
adminSession: {
|
||||
jwt: {
|
||||
userId: IdMap.getId("admin_user"),
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("returns 400", () => {
|
||||
expect(subject.status).toEqual(400)
|
||||
})
|
||||
|
||||
it("returns error", () => {
|
||||
expect(subject.body.message[0].message).toEqual(
|
||||
`"ends_at" must be greater than "ref:starts_at"`
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("succesfully creates a dynamic discount without setting valid duration", () => {
|
||||
let subject
|
||||
|
||||
beforeAll(async () => {
|
||||
jest.clearAllMocks()
|
||||
subject = await request("POST", "/admin/discounts", {
|
||||
payload: {
|
||||
code: "TEST",
|
||||
is_dynamic: true,
|
||||
rule: {
|
||||
description: "Test",
|
||||
type: "fixed",
|
||||
value: 10,
|
||||
allocation: "total",
|
||||
},
|
||||
starts_at: "03/14/2021 14:30",
|
||||
},
|
||||
adminSession: {
|
||||
jwt: {
|
||||
userId: IdMap.getId("admin_user"),
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("returns 200", () => {
|
||||
expect(subject.status).toEqual(200)
|
||||
})
|
||||
|
||||
it("returns error", () => {
|
||||
expect(DiscountServiceMock.create).toHaveBeenCalledWith({
|
||||
code: "TEST",
|
||||
is_dynamic: true,
|
||||
is_disabled: false,
|
||||
rule: {
|
||||
description: "Test",
|
||||
type: "fixed",
|
||||
value: 10,
|
||||
allocation: "total",
|
||||
},
|
||||
starts_at: new Date("03/14/2021 14:30"),
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -17,6 +17,7 @@ const defaultFields = [
|
||||
"updated_at",
|
||||
"deleted_at",
|
||||
"metadata",
|
||||
"valid_duration",
|
||||
]
|
||||
|
||||
const defaultRelations = [
|
||||
|
||||
@@ -17,6 +17,7 @@ const defaultFields = [
|
||||
"updated_at",
|
||||
"deleted_at",
|
||||
"metadata",
|
||||
"valid_duration",
|
||||
]
|
||||
|
||||
const defaultRelations = [
|
||||
|
||||
@@ -17,6 +17,7 @@ const defaultFields = [
|
||||
"updated_at",
|
||||
"deleted_at",
|
||||
"metadata",
|
||||
"valid_duration",
|
||||
]
|
||||
|
||||
const defaultRelations = [
|
||||
|
||||
@@ -7,6 +7,7 @@ describe("POST /admin/discounts", () => {
|
||||
let subject
|
||||
|
||||
beforeAll(async () => {
|
||||
jest.clearAllMocks()
|
||||
subject = await request(
|
||||
"POST",
|
||||
`/admin/discounts/${IdMap.getId("total10")}`,
|
||||
@@ -50,4 +51,139 @@ describe("POST /admin/discounts", () => {
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("unsuccessful update with dynamic discount using an invalid iso8601 duration", () => {
|
||||
let subject
|
||||
|
||||
beforeAll(async () => {
|
||||
jest.clearAllMocks()
|
||||
subject = await request(
|
||||
"POST",
|
||||
`/admin/discounts/${IdMap.getId("total10")}`,
|
||||
{
|
||||
payload: {
|
||||
code: "10TOTALOFF",
|
||||
rule: {
|
||||
id: "1234",
|
||||
type: "fixed",
|
||||
value: 10,
|
||||
allocation: "total",
|
||||
},
|
||||
starts_at: "02/02/2021 13:45",
|
||||
is_dynamic: true,
|
||||
valid_duration: "PaMT2D",
|
||||
},
|
||||
adminSession: {
|
||||
jwt: {
|
||||
userId: IdMap.getId("admin_user"),
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it("returns 400", () => {
|
||||
expect(subject.status).toEqual(400)
|
||||
})
|
||||
|
||||
it("returns error", () => {
|
||||
expect(subject.body.message[0].message).toEqual(
|
||||
`"valid_duration" must be a valid ISO 8601 duration`
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("successful update with dynamic discount", () => {
|
||||
let subject
|
||||
|
||||
beforeAll(async () => {
|
||||
jest.clearAllMocks()
|
||||
subject = await request(
|
||||
"POST",
|
||||
`/admin/discounts/${IdMap.getId("total10")}`,
|
||||
{
|
||||
payload: {
|
||||
code: "10TOTALOFF",
|
||||
rule: {
|
||||
id: "1234",
|
||||
type: "fixed",
|
||||
value: 10,
|
||||
allocation: "total",
|
||||
},
|
||||
starts_at: "02/02/2021 13:45",
|
||||
is_dynamic: true,
|
||||
valid_duration: "P1Y2M03DT04H05M",
|
||||
},
|
||||
adminSession: {
|
||||
jwt: {
|
||||
userId: IdMap.getId("admin_user"),
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it("returns 200", () => {
|
||||
expect(subject.status).toEqual(200)
|
||||
})
|
||||
|
||||
it("calls service update", () => {
|
||||
expect(DiscountServiceMock.update).toHaveBeenCalledTimes(1)
|
||||
expect(DiscountServiceMock.update).toHaveBeenCalledWith(
|
||||
IdMap.getId("total10"),
|
||||
{
|
||||
code: "10TOTALOFF",
|
||||
rule: {
|
||||
id: "1234",
|
||||
type: "fixed",
|
||||
value: 10,
|
||||
allocation: "total",
|
||||
},
|
||||
starts_at: new Date("02/02/2021 13:45"),
|
||||
is_dynamic: true,
|
||||
valid_duration: "P1Y2M03DT04H05M",
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("fails on invalid date intervals", () => {
|
||||
let subject
|
||||
|
||||
beforeAll(async () => {
|
||||
jest.clearAllMocks()
|
||||
subject = await request(
|
||||
"POST",
|
||||
`/admin/discounts/${IdMap.getId("total10")}`,
|
||||
{
|
||||
payload: {
|
||||
code: "10TOTALOFF",
|
||||
rule: {
|
||||
id: "1234",
|
||||
type: "fixed",
|
||||
value: 10,
|
||||
allocation: "total",
|
||||
},
|
||||
ends_at: "02/02/2021",
|
||||
starts_at: "03/14/2021",
|
||||
},
|
||||
adminSession: {
|
||||
jwt: {
|
||||
userId: IdMap.getId("admin_user"),
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it("returns 400", () => {
|
||||
expect(subject.status).toEqual(400)
|
||||
})
|
||||
|
||||
it("returns error", () => {
|
||||
expect(subject.body.message[0].message).toEqual(
|
||||
`"ends_at" must be greater than "ref:starts_at"`
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -71,7 +71,13 @@ export default async (req, res) => {
|
||||
.required(),
|
||||
is_disabled: Validator.boolean().default(false),
|
||||
starts_at: Validator.date().optional(),
|
||||
ends_at: Validator.date().optional(),
|
||||
ends_at: Validator.date()
|
||||
.greater(Validator.ref("starts_at"))
|
||||
.optional(),
|
||||
valid_duration: Validator.string()
|
||||
.isoDuration()
|
||||
.allow(null)
|
||||
.optional(),
|
||||
usage_limit: Validator.number()
|
||||
.positive()
|
||||
.optional(),
|
||||
|
||||
@@ -37,9 +37,9 @@ export default async (req, res) => {
|
||||
|
||||
try {
|
||||
const discountService = req.scope.resolve("discountService")
|
||||
await discountService.createDynamicCode(discount_id, value)
|
||||
const created = await discountService.createDynamicCode(discount_id, value)
|
||||
|
||||
const discount = await discountService.retrieve(discount_id, {
|
||||
const discount = await discountService.retrieve(created.id, {
|
||||
relations: ["rule", "rule.valid_for", "regions"],
|
||||
})
|
||||
|
||||
|
||||
@@ -74,6 +74,7 @@ export const defaultFields = [
|
||||
"updated_at",
|
||||
"deleted_at",
|
||||
"metadata",
|
||||
"valid_duration",
|
||||
]
|
||||
|
||||
export const defaultRelations = [
|
||||
|
||||
@@ -68,7 +68,16 @@ export default async (req, res) => {
|
||||
.optional(),
|
||||
is_disabled: Validator.boolean().optional(),
|
||||
starts_at: Validator.date().optional(),
|
||||
ends_at: Validator.date().optional(),
|
||||
ends_at: Validator.when("starts_at", {
|
||||
not: undefined,
|
||||
then: Validator.date()
|
||||
.greater(Validator.ref("starts_at"))
|
||||
.optional(),
|
||||
otherwise: Validator.date().optional(),
|
||||
}),
|
||||
valid_duration: Validator.string()
|
||||
.isoDuration().allow(null)
|
||||
.optional(),
|
||||
usage_limit: Validator.number()
|
||||
.positive()
|
||||
.optional(),
|
||||
@@ -78,6 +87,7 @@ export default async (req, res) => {
|
||||
})
|
||||
|
||||
const { value, error } = schema.validate(req.body)
|
||||
|
||||
if (error) {
|
||||
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import {MigrationInterface, QueryRunner} from "typeorm";
|
||||
|
||||
export class validDurationForDiscount1631696624528 implements MigrationInterface {
|
||||
name = 'validDurationForDiscount1631696624528'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "discount" ADD "valid_duration" character varying`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "discount" DROP COLUMN "valid_duration"`);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -44,7 +44,7 @@ export class Discount {
|
||||
|
||||
@Column({ nullable: true })
|
||||
parent_discount_id: string
|
||||
|
||||
|
||||
@ManyToOne(() => Discount)
|
||||
@JoinColumn({ name: "parent_discount_id" })
|
||||
parent_discount: Discount
|
||||
@@ -58,6 +58,9 @@ export class Discount {
|
||||
@Column({ type: resolveDbType("timestamptz"), nullable: true })
|
||||
ends_at: Date
|
||||
|
||||
@Column({ nullable: true })
|
||||
valid_duration: string
|
||||
|
||||
@ManyToMany(() => Region, { cascade: true })
|
||||
@JoinTable({
|
||||
name: "discount_regions",
|
||||
|
||||
@@ -1471,6 +1471,12 @@ describe("CartService", () => {
|
||||
})
|
||||
|
||||
describe("applyDiscount", () => {
|
||||
const getOffsetDate = offset => {
|
||||
const date = new Date()
|
||||
date.setDate(date.getDate() + offset)
|
||||
return date
|
||||
}
|
||||
|
||||
const cartRepository = MockRepository({
|
||||
findOneWithRelations: (rels, q) => {
|
||||
if (q.where.id === IdMap.getId("with-d")) {
|
||||
@@ -1538,6 +1544,69 @@ describe("CartService", () => {
|
||||
},
|
||||
})
|
||||
}
|
||||
if (code === "EarlyDiscount") {
|
||||
return Promise.resolve({
|
||||
id: IdMap.getId("10off"),
|
||||
code: "10%OFF",
|
||||
regions: [{ id: IdMap.getId("good") }],
|
||||
rule: {
|
||||
type: "percentage",
|
||||
},
|
||||
starts_at: getOffsetDate(1),
|
||||
ends_at: getOffsetDate(10),
|
||||
})
|
||||
}
|
||||
if (code === "ExpiredDiscount") {
|
||||
return Promise.resolve({
|
||||
id: IdMap.getId("10off"),
|
||||
code: "10%OFF",
|
||||
regions: [{ id: IdMap.getId("good") }],
|
||||
rule: {
|
||||
type: "percentage",
|
||||
},
|
||||
ends_at: getOffsetDate(-1),
|
||||
starts_at: getOffsetDate(-10),
|
||||
})
|
||||
}
|
||||
if (code === "ExpiredDynamicDiscount") {
|
||||
return Promise.resolve({
|
||||
id: IdMap.getId("10off"),
|
||||
code: "10%OFF",
|
||||
is_dynamic: true,
|
||||
regions: [{ id: IdMap.getId("good") }],
|
||||
rule: {
|
||||
type: "percentage",
|
||||
},
|
||||
starts_at: getOffsetDate(-10),
|
||||
ends_at: getOffsetDate(-1),
|
||||
})
|
||||
}
|
||||
if (code === "ExpiredDynamicDiscountEndDate") {
|
||||
return Promise.resolve({
|
||||
id: IdMap.getId("10off"),
|
||||
is_dynamic: true,
|
||||
code: "10%OFF",
|
||||
regions: [{ id: IdMap.getId("good") }],
|
||||
rule: {
|
||||
type: "percentage",
|
||||
},
|
||||
starts_at: getOffsetDate(-10),
|
||||
ends_at: getOffsetDate(-3),
|
||||
valid_duration: "P0Y0M1D",
|
||||
})
|
||||
}
|
||||
if (code === "ValidDiscount") {
|
||||
return Promise.resolve({
|
||||
id: IdMap.getId("10off"),
|
||||
code: "10%OFF",
|
||||
regions: [{ id: IdMap.getId("good") }],
|
||||
rule: {
|
||||
type: "percentage",
|
||||
},
|
||||
starts_at: getOffsetDate(-10),
|
||||
ends_at: getOffsetDate(10),
|
||||
})
|
||||
}
|
||||
return Promise.resolve({
|
||||
id: IdMap.getId("10off"),
|
||||
code: "10%OFF",
|
||||
@@ -1688,6 +1757,76 @@ describe("CartService", () => {
|
||||
})
|
||||
})
|
||||
|
||||
it("successfully applies valid discount with expiration date to cart", async () => {
|
||||
await cartService.update(IdMap.getId("fr-cart"), {
|
||||
discounts: [
|
||||
{
|
||||
code: "ValidDiscount",
|
||||
},
|
||||
],
|
||||
})
|
||||
expect(eventBusService.emit).toHaveBeenCalledTimes(1)
|
||||
expect(eventBusService.emit).toHaveBeenCalledWith(
|
||||
"cart.updated",
|
||||
expect.any(Object)
|
||||
)
|
||||
|
||||
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"), {
|
||||
|
||||
@@ -55,6 +55,74 @@ describe("DiscountService", () => {
|
||||
|
||||
expect(discountRepository.save).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("successfully creates discount with start and end dates", async () => {
|
||||
await discountService.create({
|
||||
code: "test",
|
||||
rule: {
|
||||
type: "percentage",
|
||||
allocation: "total",
|
||||
value: 20,
|
||||
},
|
||||
starts_at: new Date("03/14/2021"),
|
||||
ends_at: new Date("03/15/2021"),
|
||||
regions: [IdMap.getId("france")],
|
||||
})
|
||||
|
||||
expect(discountRuleRepository.create).toHaveBeenCalledTimes(1)
|
||||
expect(discountRuleRepository.create).toHaveBeenCalledWith({
|
||||
type: "percentage",
|
||||
allocation: "total",
|
||||
value: 20,
|
||||
})
|
||||
|
||||
expect(discountRuleRepository.save).toHaveBeenCalledTimes(1)
|
||||
|
||||
expect(discountRepository.create).toHaveBeenCalledTimes(1)
|
||||
expect(discountRepository.create).toHaveBeenCalledWith({
|
||||
code: "TEST",
|
||||
rule: expect.anything(),
|
||||
regions: [{ id: IdMap.getId("france") }],
|
||||
starts_at: new Date("03/14/2021"),
|
||||
ends_at: new Date("03/15/2021"),
|
||||
})
|
||||
|
||||
expect(discountRepository.save).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("successfully creates discount with start date and a valid duration", async () => {
|
||||
await discountService.create({
|
||||
code: "test",
|
||||
rule: {
|
||||
type: "percentage",
|
||||
allocation: "total",
|
||||
value: 20,
|
||||
},
|
||||
starts_at: new Date("03/14/2021"),
|
||||
valid_duration: "P0Y0M1D",
|
||||
regions: [IdMap.getId("france")],
|
||||
})
|
||||
|
||||
expect(discountRuleRepository.create).toHaveBeenCalledTimes(1)
|
||||
expect(discountRuleRepository.create).toHaveBeenCalledWith({
|
||||
type: "percentage",
|
||||
allocation: "total",
|
||||
value: 20,
|
||||
})
|
||||
|
||||
expect(discountRuleRepository.save).toHaveBeenCalledTimes(1)
|
||||
|
||||
expect(discountRepository.create).toHaveBeenCalledTimes(1)
|
||||
expect(discountRepository.create).toHaveBeenCalledWith({
|
||||
code: "TEST",
|
||||
rule: expect.anything(),
|
||||
regions: [{ id: IdMap.getId("france") }],
|
||||
starts_at: new Date("03/14/2021"),
|
||||
valid_duration: "P0Y0M1D",
|
||||
})
|
||||
|
||||
expect(discountRepository.save).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("retrieve", () => {
|
||||
@@ -376,6 +444,7 @@ describe("DiscountService", () => {
|
||||
id: "parent",
|
||||
is_dynamic: true,
|
||||
rule_id: "parent_rule",
|
||||
valid_duration: "P1Y",
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -412,6 +481,8 @@ describe("DiscountService", () => {
|
||||
rule_id: "parent_rule",
|
||||
parent_discount_id: "parent",
|
||||
code: "HI",
|
||||
usage_limit: undefined,
|
||||
ends_at: expect.any(Date),
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -887,6 +887,21 @@ class CartService extends BaseService {
|
||||
)
|
||||
}
|
||||
|
||||
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(
|
||||
|
||||
@@ -2,6 +2,8 @@ import _ from "lodash"
|
||||
import randomize from "randomatic"
|
||||
import { BaseService } from "medusa-interfaces"
|
||||
import { Validator, MedusaError } from "medusa-core-utils"
|
||||
import { MedusaErrorCodes } from "medusa-core-utils/dist/errors"
|
||||
import { parse, toSeconds } from "iso8601-duration"
|
||||
import { Brackets, ILike } from "typeorm"
|
||||
|
||||
/**
|
||||
@@ -270,6 +272,15 @@ class DiscountService extends BaseService {
|
||||
|
||||
const { rule, metadata, regions, ...rest } = update
|
||||
|
||||
if (rest.ends_at) {
|
||||
if (discount.starts_at >= new Date(update.ends_at)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`"ends_at" must be greater than "starts_at"`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (regions) {
|
||||
discount.regions = await Promise.all(
|
||||
regions.map(regionId => this.regionService_.retrieve(regionId))
|
||||
@@ -329,6 +340,13 @@ class DiscountService extends BaseService {
|
||||
usage_limit: discount.usage_limit,
|
||||
}
|
||||
|
||||
if (discount.valid_duration) {
|
||||
const lastValidDate = new Date()
|
||||
lastValidDate.setSeconds(
|
||||
lastValidDate.getSeconds() + toSeconds(parse(discount.valid_duration))
|
||||
)
|
||||
toCreate.ends_at = lastValidDate
|
||||
}
|
||||
const created = await discountRepo.create(toCreate)
|
||||
const result = await discountRepo.save(created)
|
||||
return result
|
||||
|
||||
Reference in New Issue
Block a user