Merge pull request #207 from medusajs/fix/discount-usage-limit
fix(medusa): Add usage count and usage limit functionality to discounts
This commit is contained in:
@@ -106,6 +106,8 @@ describe("/store/carts", () => {
|
|||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
const manager = dbConnection.manager;
|
const manager = dbConnection.manager;
|
||||||
|
await manager.query(`DELETE FROM "discount"`);
|
||||||
|
await manager.query(`DELETE FROM "discount_rule"`);
|
||||||
await manager.query(`DELETE FROM "cart"`);
|
await manager.query(`DELETE FROM "cart"`);
|
||||||
await manager.query(`DELETE FROM "customer"`);
|
await manager.query(`DELETE FROM "customer"`);
|
||||||
await manager.query(
|
await manager.query(
|
||||||
@@ -114,6 +116,21 @@ describe("/store/carts", () => {
|
|||||||
await manager.query(`DELETE FROM "region"`);
|
await manager.query(`DELETE FROM "region"`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("fails on apply discount if limit has been reached", async () => {
|
||||||
|
const api = useApi();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.post("/store/carts/test-cart", {
|
||||||
|
discounts: [{ code: "CREATED" }],
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
expect(error.response.status).toEqual(400);
|
||||||
|
expect(error.response.data.message).toEqual(
|
||||||
|
"Discount has been used maximum allowed times"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("updates cart customer id", async () => {
|
it("updates cart customer id", async () => {
|
||||||
const api = useApi();
|
const api = useApi();
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,43 @@
|
|||||||
const { Customer, Region, Cart } = require("@medusajs/medusa");
|
const {
|
||||||
|
Customer,
|
||||||
|
Region,
|
||||||
|
Cart,
|
||||||
|
DiscountRule,
|
||||||
|
Discount,
|
||||||
|
} = require("@medusajs/medusa");
|
||||||
|
|
||||||
module.exports = async (connection, data = {}) => {
|
module.exports = async (connection, data = {}) => {
|
||||||
const manager = connection.manager;
|
const manager = connection.manager;
|
||||||
|
|
||||||
await manager.insert(Region, {
|
const r = manager.create(Region, {
|
||||||
id: "test-region",
|
id: "test-region",
|
||||||
name: "Test Region",
|
name: "Test Region",
|
||||||
currency_code: "usd",
|
currency_code: "usd",
|
||||||
tax_rate: 0,
|
tax_rate: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const d = await manager.create(Discount, {
|
||||||
|
id: "test-discount",
|
||||||
|
code: "CREATED",
|
||||||
|
is_dynamic: false,
|
||||||
|
is_disabled: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const dr = await manager.create(DiscountRule, {
|
||||||
|
id: "test-discount-rule",
|
||||||
|
description: "Created",
|
||||||
|
type: "fixed",
|
||||||
|
value: 10000,
|
||||||
|
allocation: "total",
|
||||||
|
usage_limit: 2,
|
||||||
|
usage_count: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
d.rule = dr;
|
||||||
|
d.regions = [r];
|
||||||
|
|
||||||
|
await manager.save(d);
|
||||||
|
|
||||||
await manager.query(
|
await manager.query(
|
||||||
`UPDATE "country" SET region_id='test-region' WHERE iso_2 = 'us'`
|
`UPDATE "country" SET region_id='test-region' WHERE iso_2 = 'us'`
|
||||||
);
|
);
|
||||||
|
|||||||
+730
-893
File diff suppressed because it is too large
Load Diff
@@ -64,7 +64,9 @@ export default async (req, res) => {
|
|||||||
.required(),
|
.required(),
|
||||||
allocation: Validator.string().required(),
|
allocation: Validator.string().required(),
|
||||||
valid_for: Validator.array().items(Validator.string()),
|
valid_for: Validator.array().items(Validator.string()),
|
||||||
usage_limit: Validator.number().optional(),
|
usage_limit: Validator.number()
|
||||||
|
.positive()
|
||||||
|
.optional(),
|
||||||
})
|
})
|
||||||
.required(),
|
.required(),
|
||||||
is_disabled: Validator.boolean().default(false),
|
is_disabled: Validator.boolean().default(false),
|
||||||
|
|||||||
@@ -64,6 +64,9 @@ export default async (req, res) => {
|
|||||||
value: Validator.number().required(),
|
value: Validator.number().required(),
|
||||||
allocation: Validator.string().required(),
|
allocation: Validator.string().required(),
|
||||||
valid_for: Validator.array().items(Validator.string()),
|
valid_for: Validator.array().items(Validator.string()),
|
||||||
|
usage_limit: Validator.number()
|
||||||
|
.positive()
|
||||||
|
.optional(),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
is_disabled: Validator.boolean().optional(),
|
is_disabled: Validator.boolean().optional(),
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import glob from "glob"
|
import glob from "glob"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { BaseModel } from "medusa-interfaces"
|
|
||||||
import { EntitySchema } from "typeorm"
|
import { EntitySchema } from "typeorm"
|
||||||
import { Lifetime, asClass, asValue } from "awilix"
|
import { asClass, asValue } from "awilix"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registers all models in the model directory
|
* Registers all models in the model directory
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from "typeorm"
|
||||||
|
|
||||||
|
export class discountUsageCount1615970124120 implements MigrationInterface {
|
||||||
|
name = "discountUsageCount1615970124120"
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "discount_rule" ADD "usage_count" integer NOT NULL DEFAULT '0'`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "discount_rule" DROP COLUMN "usage_count"`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
Index,
|
Index,
|
||||||
Column,
|
Column,
|
||||||
PrimaryColumn,
|
PrimaryColumn,
|
||||||
OneToOne,
|
|
||||||
ManyToMany,
|
ManyToMany,
|
||||||
JoinTable,
|
JoinTable,
|
||||||
} from "typeorm"
|
} from "typeorm"
|
||||||
@@ -67,6 +66,9 @@ export class DiscountRule {
|
|||||||
@Column({ nullable: true })
|
@Column({ nullable: true })
|
||||||
usage_limit: number
|
usage_limit: number
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
usage_count: number
|
||||||
|
|
||||||
@CreateDateColumn({ type: "timestamptz" })
|
@CreateDateColumn({ type: "timestamptz" })
|
||||||
created_at: Date
|
created_at: Date
|
||||||
|
|
||||||
@@ -122,6 +124,9 @@ export class DiscountRule {
|
|||||||
* usage_limit:
|
* usage_limit:
|
||||||
* description: "The maximum number of times that a discount can be used."
|
* description: "The maximum number of times that a discount can be used."
|
||||||
* type: integer
|
* type: integer
|
||||||
|
* usage_count:
|
||||||
|
* description: "The number of times a discount has been used."
|
||||||
|
* type: integer
|
||||||
* created_at:
|
* created_at:
|
||||||
* description: "The date with timezone at which the resource was created."
|
* description: "The date with timezone at which the resource was created."
|
||||||
* type: string
|
* type: string
|
||||||
|
|||||||
@@ -1462,6 +1462,28 @@ describe("CartService", () => {
|
|||||||
regions: [{ id: IdMap.getId("bad") }],
|
regions: [{ id: IdMap.getId("bad") }],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
if (code === "limit-reached") {
|
||||||
|
return Promise.resolve({
|
||||||
|
id: IdMap.getId("limit-reached"),
|
||||||
|
code: "limit-reached",
|
||||||
|
regions: [{ id: IdMap.getId("good") }],
|
||||||
|
rule: {
|
||||||
|
usage_count: 2,
|
||||||
|
usage_limit: 2,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (code === "null-count") {
|
||||||
|
return Promise.resolve({
|
||||||
|
id: IdMap.getId("null-count"),
|
||||||
|
code: "null-count",
|
||||||
|
regions: [{ id: IdMap.getId("good") }],
|
||||||
|
rule: {
|
||||||
|
usage_count: null,
|
||||||
|
usage_limit: 2,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
if (code === "FREESHIPPING") {
|
if (code === "FREESHIPPING") {
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
id: IdMap.getId("freeship"),
|
id: IdMap.getId("freeship"),
|
||||||
@@ -1594,6 +1616,43 @@ describe("CartService", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
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: [
|
||||||
|
{
|
||||||
|
id: IdMap.getId("null-count"),
|
||||||
|
code: "null-count",
|
||||||
|
regions: [{ id: IdMap.getId("good") }],
|
||||||
|
rule: {
|
||||||
|
usage_count: 0,
|
||||||
|
usage_limit: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
discount_total: 0,
|
||||||
|
shipping_total: 0,
|
||||||
|
subtotal: 0,
|
||||||
|
tax_total: 0,
|
||||||
|
total: 0,
|
||||||
|
region_id: IdMap.getId("good"),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
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 () => {
|
it("throws if discount is not available in region", async () => {
|
||||||
await expect(
|
await expect(
|
||||||
cartService.update(IdMap.getId("cart"), {
|
cartService.update(IdMap.getId("cart"), {
|
||||||
|
|||||||
@@ -812,6 +812,18 @@ class CartService extends BaseService {
|
|||||||
])
|
])
|
||||||
|
|
||||||
const rule = discount.rule
|
const rule = discount.rule
|
||||||
|
|
||||||
|
// if limit is set and reached, we make an early exit
|
||||||
|
if (rule?.usage_limit) {
|
||||||
|
rule.usage_count = rule.usage_count || 0
|
||||||
|
|
||||||
|
if (rule.usage_limit === rule.usage_count)
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.NOT_ALLOWED,
|
||||||
|
"Discount has been used maximum allowed times"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
let regions = discount.regions
|
let regions = discount.regions
|
||||||
if (discount.parent_discount_id) {
|
if (discount.parent_discount_id) {
|
||||||
const parent = await this.discountService_.retrieve(
|
const parent = await this.discountService_.retrieve(
|
||||||
|
|||||||
@@ -83,7 +83,23 @@ class DiscountService extends BaseService {
|
|||||||
.required(),
|
.required(),
|
||||||
allocation: Validator.string().required(),
|
allocation: Validator.string().required(),
|
||||||
valid_for: Validator.array().optional(),
|
valid_for: Validator.array().optional(),
|
||||||
user_limit: Validator.number().optional(),
|
usage_limit: Validator.number()
|
||||||
|
.positive()
|
||||||
|
.allow(null)
|
||||||
|
.optional(),
|
||||||
|
usage_count: Validator.number()
|
||||||
|
.positive()
|
||||||
|
.optional(),
|
||||||
|
created_at: Validator.date().optional(),
|
||||||
|
updated_at: Validator.date()
|
||||||
|
.allow(null)
|
||||||
|
.optional(),
|
||||||
|
deleted_at: Validator.date()
|
||||||
|
.allow(null)
|
||||||
|
.optional(),
|
||||||
|
metadata: Validator.object()
|
||||||
|
.allow(null)
|
||||||
|
.optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
const { value, error } = schema.validate(discountRule)
|
const { value, error } = schema.validate(discountRule)
|
||||||
|
|||||||
@@ -48,8 +48,12 @@ class OrderSubscriber {
|
|||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
order.discounts.map(async d => {
|
order.discounts.map(async d => {
|
||||||
|
const usageCount = d.rule?.usage_count || 0
|
||||||
return this.discountService_.update(d.id, {
|
return this.discountService_.update(d.id, {
|
||||||
usage_count: d.usage_count + 1,
|
rule: {
|
||||||
|
...d.rule,
|
||||||
|
usage_count: usageCount + 1,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|||||||
+957
-271
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user