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:
Sebastian Rindom
2021-03-17 14:56:51 +01:00
committed by GitHub
13 changed files with 1857 additions and 1172 deletions
@@ -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();
+30 -2
View File
@@ -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'`
); );
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 -2
View File
@@ -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"`
)
}
}
+6 -1
View File
@@ -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"), {
+12
View File
@@ -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(
+17 -1
View File
@@ -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)
+5 -1
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff