fix(medusa): Add usage_count + usage_limit to discount
This commit is contained in:
@@ -106,6 +106,8 @@ describe("/store/carts", () => {
|
||||
|
||||
afterEach(async () => {
|
||||
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 "customer"`);
|
||||
await manager.query(
|
||||
@@ -114,6 +116,21 @@ describe("/store/carts", () => {
|
||||
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 () => {
|
||||
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 = {}) => {
|
||||
const manager = connection.manager;
|
||||
|
||||
await manager.insert(Region, {
|
||||
const r = manager.create(Region, {
|
||||
id: "test-region",
|
||||
name: "Test Region",
|
||||
currency_code: "usd",
|
||||
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(
|
||||
`UPDATE "country" SET region_id='test-region' WHERE iso_2 = 'us'`
|
||||
);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +0,0 @@
|
||||
module.exports = {
|
||||
testEnvironment: "node",
|
||||
}
|
||||
@@ -64,7 +64,10 @@ export default async (req, res) => {
|
||||
.required(),
|
||||
allocation: Validator.string().required(),
|
||||
valid_for: Validator.array().items(Validator.string()),
|
||||
usage_limit: Validator.number().optional(),
|
||||
usage_limit: Validator.number()
|
||||
.positive()
|
||||
.optional(),
|
||||
usage_count: Validator.number().optional(),
|
||||
})
|
||||
.required(),
|
||||
is_disabled: Validator.boolean().default(false),
|
||||
|
||||
@@ -64,6 +64,9 @@ export default async (req, res) => {
|
||||
value: Validator.number().required(),
|
||||
allocation: Validator.string().required(),
|
||||
valid_for: Validator.array().items(Validator.string()),
|
||||
usage_limit: Validator.number()
|
||||
.positive()
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
is_disabled: Validator.boolean().optional(),
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import glob from "glob"
|
||||
import path from "path"
|
||||
import { BaseModel } from "medusa-interfaces"
|
||||
import { EntitySchema } from "typeorm"
|
||||
import { Lifetime, asClass, asValue } from "awilix"
|
||||
import { asClass, asValue } from "awilix"
|
||||
|
||||
/**
|
||||
* Registers all models in the model directory
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm"
|
||||
|
||||
export class discountUsageCount1615912118791 implements MigrationInterface {
|
||||
name = "discountUsageCount1615912118791"
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "discount_rule" ADD "usage_count" integer`
|
||||
)
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "discount_rule" DROP COLUMN "usage_count"`
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
Index,
|
||||
Column,
|
||||
PrimaryColumn,
|
||||
OneToOne,
|
||||
ManyToMany,
|
||||
JoinTable,
|
||||
} from "typeorm"
|
||||
@@ -67,6 +66,9 @@ export class DiscountRule {
|
||||
@Column({ nullable: true })
|
||||
usage_limit: number
|
||||
|
||||
@Column({ nullable: true })
|
||||
usage_count: number
|
||||
|
||||
@CreateDateColumn({ type: "timestamptz" })
|
||||
created_at: Date
|
||||
|
||||
@@ -122,6 +124,9 @@ export class DiscountRule {
|
||||
* usage_limit:
|
||||
* description: "The maximum number of times that a discount can be used."
|
||||
* type: integer
|
||||
* usage_count:
|
||||
* description: "The number of times a discount has been used."
|
||||
* type: integer
|
||||
* created_at:
|
||||
* description: "The date with timezone at which the resource was created."
|
||||
* type: string
|
||||
|
||||
@@ -1462,6 +1462,28 @@ describe("CartService", () => {
|
||||
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") {
|
||||
return Promise.resolve({
|
||||
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 () => {
|
||||
await expect(
|
||||
cartService.update(IdMap.getId("cart"), {
|
||||
|
||||
@@ -812,6 +812,18 @@ class CartService extends BaseService {
|
||||
])
|
||||
|
||||
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
|
||||
if (discount.parent_discount_id) {
|
||||
const parent = await this.discountService_.retrieve(
|
||||
|
||||
@@ -83,7 +83,12 @@ class DiscountService extends BaseService {
|
||||
.required(),
|
||||
allocation: Validator.string().required(),
|
||||
valid_for: Validator.array().optional(),
|
||||
user_limit: Validator.number().optional(),
|
||||
usage_limit: Validator.number()
|
||||
.min(0)
|
||||
.optional(),
|
||||
usage_count: Validator.number()
|
||||
.positive()
|
||||
.optional(),
|
||||
})
|
||||
|
||||
const { value, error } = schema.validate(discountRule)
|
||||
@@ -276,7 +281,8 @@ class DiscountService extends BaseService {
|
||||
}
|
||||
|
||||
if (rule) {
|
||||
discount.rule = this.validateDiscountRule_(rule)
|
||||
const updated = this.validateDiscountRule_(rule)
|
||||
discount.rule = { ...discount.rule, ...updated }
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(rest)) {
|
||||
|
||||
@@ -48,8 +48,12 @@ class OrderSubscriber {
|
||||
|
||||
await Promise.all(
|
||||
order.discounts.map(async d => {
|
||||
const usageCount = d?.usage_count || 0
|
||||
return this.discountService_.update(d.id, {
|
||||
usage_count: d.usage_count + 1,
|
||||
rule: {
|
||||
...d.rule,
|
||||
usage_count: usageCount + 1,
|
||||
},
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user