feat: Add DiscountConditions (#1230)
* feat: Add DiscountCondition entity + Join table per relation (#1146) * feat: Convert DiscountService to TypeScript (#1149) * feat: Add DiscountRepository + bulk insert and remove (#1156) * feat: Add `conditions` to payload in `POST /discounts` and `POST /discounts/:id` (#1170) * feat: Add DiscountRuleCondition entity * fix relation * fix join key * Add discount rule condition repo * add join table per relation * Convert DiscountService to TypeScript * feat: Add DiscountConditionRepository * Add migration + remove use of valid_for * revert changes to files, not done yet * init work on create discount endpoint * Add conditions to create discount endpoint * Add conditions to update discount endpoint * Add unique constraint to discount condition * integration tests passing * fix imports of models * fix tests (excluding totals calculations) * Fix commented code * add unique constraint on discount condition * Add generic way of generating retrieve configs * Requested changes + ExactlyOne validator * Remove isLocal flag from error handler * Use postgres error constant * remove commented code * feat: Add `isValidForProduct` to check if Discount is valid for a given Product (#1172) * feat: Add `canApplyForCustomer` to check if Discount is valid for customer groups (#1212) * feat: Add `calculateDiscountForLineItem` (#1224) * feat: Adds discount condition test factory (#1228) * Remove use of valid_for * Tests passing * Remove valid_for form relations * Add integration tests for applying discounts to cart
This commit is contained in:
@@ -1,13 +1,12 @@
|
||||
import { IsNumber, IsOptional, IsString } from "class-validator"
|
||||
import { Type } from "class-transformer"
|
||||
import { IsNumber, IsOptional, IsString } from "class-validator"
|
||||
import omit from "lodash/omit"
|
||||
|
||||
import { validator } from "../../../../utils/validator"
|
||||
import { CustomerGroupService } from "../../../../services"
|
||||
import { CustomerGroup } from "../../../../models/customer-group"
|
||||
import { FindConfig } from "../../../../types/common"
|
||||
import { defaultAdminCustomerGroupsRelations } from "."
|
||||
import { CustomerGroup } from "../../../../models/customer-group"
|
||||
import { CustomerGroupService } from "../../../../services"
|
||||
import { FindConfig } from "../../../../types/common"
|
||||
import { FilterableCustomerGroupProps } from "../../../../types/customer-groups"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
|
||||
/**
|
||||
* @oas [get] /customer-groups
|
||||
|
||||
@@ -48,7 +48,7 @@ describe("POST /admin/discounts/:discount_id/regions/:region_id", () => {
|
||||
"metadata",
|
||||
"valid_duration",
|
||||
],
|
||||
relations: ["rule", "parent_discount", "regions", "rule.valid_for"],
|
||||
relations: ["rule", "parent_discount", "regions", "rule.conditions"],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ describe("POST /admin/discounts/:discount_id/products/:product_id", () => {
|
||||
"metadata",
|
||||
"valid_duration",
|
||||
],
|
||||
relations: ["rule", "parent_discount", "regions", "rule.valid_for"],
|
||||
relations: ["rule", "parent_discount", "regions", "rule.conditions"],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -166,6 +166,49 @@ describe("POST /admin/discounts", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("fails on xor constraint for conditions", () => {
|
||||
let subject
|
||||
|
||||
beforeAll(async () => {
|
||||
subject = await request("POST", "/admin/discounts", {
|
||||
payload: {
|
||||
code: "TEST",
|
||||
rule: {
|
||||
description: "Test",
|
||||
type: "fixed",
|
||||
value: 10,
|
||||
allocation: "total",
|
||||
conditions: [
|
||||
{
|
||||
products: ["product1"],
|
||||
operator: "in",
|
||||
product_types: ["producttype1"],
|
||||
},
|
||||
],
|
||||
},
|
||||
starts_at: "02/02/2021 13:45",
|
||||
is_dynamic: true,
|
||||
valid_duration: "P1Y2M03DT04H05M",
|
||||
},
|
||||
adminSession: {
|
||||
jwt: {
|
||||
userId: IdMap.getId("admin_user"),
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("returns 400", () => {
|
||||
expect(subject.status).toEqual(400)
|
||||
})
|
||||
|
||||
it("returns error", () => {
|
||||
expect(subject.body.message).toEqual(
|
||||
`Only one of products, product_types is allowed, Only one of product_types, products is allowed`
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("fails on invalid date intervals", () => {
|
||||
let subject
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ const defaultRelations = [
|
||||
"rule",
|
||||
"parent_discount",
|
||||
"regions",
|
||||
"rule.valid_for",
|
||||
"rule.conditions",
|
||||
]
|
||||
|
||||
describe("GET /admin/discounts/:discount_id", () => {
|
||||
|
||||
@@ -24,7 +24,7 @@ const defaultRelations = [
|
||||
"rule",
|
||||
"parent_discount",
|
||||
"regions",
|
||||
"rule.valid_for",
|
||||
"rule.conditions",
|
||||
]
|
||||
|
||||
describe("DELETE /admin/discounts/:discount_id/regions/region_id", () => {
|
||||
|
||||
@@ -24,7 +24,7 @@ const defaultRelations = [
|
||||
"rule",
|
||||
"parent_discount",
|
||||
"regions",
|
||||
"rule.valid_for",
|
||||
"rule.conditions",
|
||||
]
|
||||
|
||||
describe("DELETE /admin/discounts/:discount_id/products/:product_id", () => {
|
||||
|
||||
@@ -11,11 +11,16 @@ import {
|
||||
IsString,
|
||||
ValidateNested,
|
||||
} from "class-validator"
|
||||
import { defaultAdminDiscountsRelations } from "."
|
||||
import { defaultAdminDiscountsFields, defaultAdminDiscountsRelations } from "."
|
||||
import { Discount } from "../../../../models/discount"
|
||||
import { DiscountConditionOperator } from "../../../../models/discount-condition"
|
||||
import DiscountService from "../../../../services/discount"
|
||||
import { IsGreaterThan } from "../../../../utils/validators/greater-than"
|
||||
import { AdminUpsertConditionsReq } from "../../../../types/discount"
|
||||
import { getRetrieveConfig } from "../../../../utils/get-query-config"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
import { IsGreaterThan } from "../../../../utils/validators/greater-than"
|
||||
import { IsISO8601Duration } from "../../../../utils/validators/iso8601-duration"
|
||||
import { AdminPostDiscountsDiscountParams } from "./update-discount"
|
||||
/**
|
||||
* @oas [post] /discounts
|
||||
* operationId: "PostDiscounts"
|
||||
@@ -74,16 +79,30 @@ import { IsISO8601Duration } from "../../../../utils/validators/iso8601-duration
|
||||
* discount:
|
||||
* $ref: "#/components/schemas/discount"
|
||||
*/
|
||||
|
||||
export default async (req, res) => {
|
||||
const validated = await validator(AdminPostDiscountsReq, req.body)
|
||||
|
||||
const discountService: DiscountService = req.scope.resolve("discountService")
|
||||
const created = await discountService.create(validated)
|
||||
const discount = await discountService.retrieve(
|
||||
created.id,
|
||||
defaultAdminDiscountsRelations
|
||||
console.log(validated.rule.conditions)
|
||||
|
||||
const validatedParams = await validator(
|
||||
AdminPostDiscountsDiscountParams,
|
||||
req.query
|
||||
)
|
||||
|
||||
const discountService: DiscountService = req.scope.resolve("discountService")
|
||||
|
||||
const created = await discountService.create(validated)
|
||||
|
||||
const config = getRetrieveConfig<Discount>(
|
||||
defaultAdminDiscountsFields,
|
||||
defaultAdminDiscountsRelations,
|
||||
validatedParams?.fields?.split(",") as (keyof Discount)[],
|
||||
validatedParams?.expand?.split(",")
|
||||
)
|
||||
|
||||
const discount = await discountService.retrieve(created.id, config)
|
||||
|
||||
res.status(200).json({ discount })
|
||||
}
|
||||
|
||||
@@ -132,7 +151,7 @@ export class AdminPostDiscountsReq {
|
||||
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
metadata?: object
|
||||
metadata?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export class AdminPostDiscountsDiscountRule {
|
||||
@@ -153,6 +172,22 @@ export class AdminPostDiscountsDiscountRule {
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
valid_for?: string[]
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => AdminCreateCondition)
|
||||
conditions?: AdminCreateCondition[]
|
||||
}
|
||||
|
||||
export class AdminCreateCondition extends AdminUpsertConditionsReq {
|
||||
@IsString()
|
||||
operator: DiscountConditionOperator
|
||||
}
|
||||
|
||||
export class AdminPostDiscountsParams {
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
expand?: string[]
|
||||
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
fields?: string[]
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
IsOptional,
|
||||
IsString,
|
||||
} from "class-validator"
|
||||
import { defaultAdminDiscountsFields, defaultAdminDiscountsRelations } from "."
|
||||
import DiscountService from "../../../../services/discount"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
/**
|
||||
@@ -16,6 +17,7 @@ import { validator } from "../../../../utils/validator"
|
||||
* parameters:
|
||||
* - (path) id=* {string} The id of the Discount to create the dynamic code from."
|
||||
* - (body) code=* {string} The unique code that will be used to redeem the Discount.
|
||||
* - (body) usage_limit=* {number} amount of times the discount can be applied
|
||||
* - (body) metadata {object} An optional set of key-value paris to hold additional information.
|
||||
* tags:
|
||||
* - Discount
|
||||
@@ -44,7 +46,8 @@ export default async (req, res) => {
|
||||
)
|
||||
|
||||
const discount = await discountService.retrieve(created.id, {
|
||||
relations: ["rule", "rule.valid_for", "regions"],
|
||||
select: defaultAdminDiscountsFields,
|
||||
relations: defaultAdminDiscountsRelations,
|
||||
})
|
||||
|
||||
res.status(200).json({ discount })
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { defaultAdminDiscountsFields, defaultAdminDiscountsRelations } from "."
|
||||
import DiscountService from "../../../../services/discount"
|
||||
|
||||
/**
|
||||
@@ -28,7 +29,8 @@ export default async (req, res) => {
|
||||
await discountService.deleteDynamicCode(discount_id, code)
|
||||
|
||||
const discount = await discountService.retrieve(discount_id, {
|
||||
relations: ["rule", "rule.valid_for", "regions"],
|
||||
select: defaultAdminDiscountsFields,
|
||||
relations: defaultAdminDiscountsRelations,
|
||||
})
|
||||
|
||||
res.status(200).json({ discount })
|
||||
|
||||
@@ -62,7 +62,7 @@ export default (app) => {
|
||||
return app
|
||||
}
|
||||
|
||||
export const defaultAdminDiscountsFields = [
|
||||
export const defaultAdminDiscountsFields: (keyof Discount)[] = [
|
||||
"id",
|
||||
"code",
|
||||
"is_dynamic",
|
||||
@@ -84,7 +84,7 @@ export const defaultAdminDiscountsRelations = [
|
||||
"rule",
|
||||
"parent_discount",
|
||||
"regions",
|
||||
"rule.valid_for",
|
||||
"rule.conditions",
|
||||
]
|
||||
|
||||
export type AdminDiscountsRes = {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Type, Transform } from "class-transformer"
|
||||
import { Transform, Type } from "class-transformer"
|
||||
import {
|
||||
IsBoolean,
|
||||
IsEnum,
|
||||
IsInt,
|
||||
IsOptional,
|
||||
IsString,
|
||||
@@ -9,12 +8,10 @@ import {
|
||||
} from "class-validator"
|
||||
import _, { pickBy } from "lodash"
|
||||
import { defaultAdminDiscountsFields, defaultAdminDiscountsRelations } from "."
|
||||
import {
|
||||
AllocationType,
|
||||
DiscountRuleType,
|
||||
} from "../../../../models/discount-rule"
|
||||
import { Discount } from "../../../.."
|
||||
import DiscountService from "../../../../services/discount"
|
||||
import { DateComparisonOperator } from "../../../../types/common"
|
||||
import { FindConfig } from "../../../../types/common"
|
||||
import { AdminGetDiscountsDiscountRuleParams } from "../../../../types/discount"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
/**
|
||||
* @oas [get] /discounts
|
||||
@@ -46,7 +43,7 @@ export default async (req, res) => {
|
||||
|
||||
const discountService: DiscountService = req.scope.resolve("discountService")
|
||||
|
||||
const listConfig = {
|
||||
const listConfig: FindConfig<Discount> = {
|
||||
select: defaultAdminDiscountsFields,
|
||||
relations: defaultAdminDiscountsRelations,
|
||||
skip: validated.offset,
|
||||
@@ -69,17 +66,12 @@ export default async (req, res) => {
|
||||
})
|
||||
}
|
||||
|
||||
class AdminGetDiscountsDiscountRuleParams {
|
||||
@IsOptional()
|
||||
@IsEnum(DiscountRuleType)
|
||||
type: DiscountRuleType
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(AllocationType)
|
||||
allocation: AllocationType
|
||||
}
|
||||
|
||||
export class AdminGetDiscountsParams {
|
||||
@ValidateNested()
|
||||
@IsOptional()
|
||||
@Type(() => AdminGetDiscountsDiscountRuleParams)
|
||||
rule?: AdminGetDiscountsDiscountRuleParams
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
q?: string
|
||||
@@ -94,11 +86,6 @@ export class AdminGetDiscountsParams {
|
||||
@Transform(({ value }) => value === "true")
|
||||
is_disabled?: boolean
|
||||
|
||||
@ValidateNested()
|
||||
@IsOptional()
|
||||
@Type(() => AdminGetDiscountsDiscountRuleParams)
|
||||
rule?: AdminGetDiscountsDiscountRuleParams
|
||||
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
|
||||
@@ -12,7 +12,11 @@ import {
|
||||
ValidateNested,
|
||||
} from "class-validator"
|
||||
import { defaultAdminDiscountsFields, defaultAdminDiscountsRelations } from "."
|
||||
import { Discount } from "../../../../models/discount"
|
||||
import { DiscountConditionOperator } from "../../../../models/discount-condition"
|
||||
import DiscountService from "../../../../services/discount"
|
||||
import { AdminUpsertConditionsReq } from "../../../../types/discount"
|
||||
import { getRetrieveConfig } from "../../../../utils/get-query-config"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
import { IsGreaterThan } from "../../../../utils/validators/greater-than"
|
||||
import { IsISO8601Duration } from "../../../../utils/validators/iso8601-duration"
|
||||
@@ -73,12 +77,24 @@ export default async (req, res) => {
|
||||
const { discount_id } = req.params
|
||||
|
||||
const validated = await validator(AdminPostDiscountsDiscountReq, req.body)
|
||||
|
||||
const validatedParams = await validator(
|
||||
AdminPostDiscountsDiscountParams,
|
||||
req.query
|
||||
)
|
||||
|
||||
const discountService: DiscountService = req.scope.resolve("discountService")
|
||||
|
||||
await discountService.update(discount_id, validated)
|
||||
const discount = await discountService.retrieve(discount_id, {
|
||||
select: defaultAdminDiscountsFields,
|
||||
relations: defaultAdminDiscountsRelations,
|
||||
})
|
||||
|
||||
const config = getRetrieveConfig<Discount>(
|
||||
defaultAdminDiscountsFields,
|
||||
defaultAdminDiscountsRelations,
|
||||
validatedParams?.fields?.split(",") as (keyof Discount)[],
|
||||
validatedParams?.expand?.split(",")
|
||||
)
|
||||
|
||||
const discount = await discountService.retrieve(discount_id, config)
|
||||
|
||||
res.status(200).json({ discount })
|
||||
}
|
||||
@@ -128,7 +144,7 @@ export class AdminPostDiscountsDiscountReq {
|
||||
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
metadata?: object
|
||||
metadata?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export class AdminUpdateDiscountRule {
|
||||
@@ -151,8 +167,29 @@ export class AdminUpdateDiscountRule {
|
||||
@IsNotEmpty()
|
||||
allocation: string
|
||||
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
@IsString({ each: true })
|
||||
valid_for?: string[]
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => AdminUpsertCondition)
|
||||
conditions?: AdminUpsertCondition[]
|
||||
}
|
||||
|
||||
export class AdminUpsertCondition extends AdminUpsertConditionsReq {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
id?: string
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
operator: DiscountConditionOperator
|
||||
}
|
||||
|
||||
export class AdminPostDiscountsDiscountParams {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
expand?: string
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
fields?: string
|
||||
}
|
||||
|
||||
@@ -56,7 +56,6 @@ export default async (req, res) => {
|
||||
relations: [
|
||||
"discounts",
|
||||
"discounts.rule",
|
||||
"discounts.rule.valid_for",
|
||||
"shipping_methods",
|
||||
"region",
|
||||
"items",
|
||||
|
||||
@@ -8,7 +8,6 @@ const defaultRelations = [
|
||||
"shipping_address",
|
||||
"discounts",
|
||||
"discounts.rule",
|
||||
"discounts.rule.valid_for",
|
||||
"shipping_methods",
|
||||
"payments",
|
||||
"fulfillments",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Router } from "express"
|
||||
import { Order } from "../../../.."
|
||||
import middlewares from "../../../middlewares"
|
||||
import { DeleteResponse, PaginatedResponse } from "../../../../types/common"
|
||||
import "reflect-metadata"
|
||||
import { Order } from "../../../.."
|
||||
import { DeleteResponse, PaginatedResponse } from "../../../../types/common"
|
||||
import middlewares from "../../../middlewares"
|
||||
|
||||
const route = Router()
|
||||
|
||||
@@ -231,7 +231,6 @@ export const defaultAdminOrdersRelations = [
|
||||
"shipping_address",
|
||||
"discounts",
|
||||
"discounts.rule",
|
||||
"discounts.rule.valid_for",
|
||||
"shipping_methods",
|
||||
"payments",
|
||||
"fulfillments",
|
||||
@@ -332,7 +331,6 @@ export const allowedAdminOrdersRelations = [
|
||||
"shipping_address",
|
||||
"discounts",
|
||||
"discounts.rule",
|
||||
"discounts.rule.valid_for",
|
||||
"shipping_methods",
|
||||
"payments",
|
||||
"fulfillments",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { defaultAdminTaxRatesFields, defaultAdminTaxRatesRelations } from "../"
|
||||
import { pick } from "lodash"
|
||||
import { FindConfig } from "../../../../../types/common"
|
||||
import { defaultAdminTaxRatesFields, defaultAdminTaxRatesRelations } from "../"
|
||||
import { TaxRate } from "../../../../.."
|
||||
import { FindConfig } from "../../../../../types/common"
|
||||
|
||||
export function pickByConfig<T>(
|
||||
obj: T | T[],
|
||||
@@ -64,7 +64,9 @@ export function getListConfig(
|
||||
expandFields = expand
|
||||
}
|
||||
|
||||
const orderBy = order ?? { created_at: "DESC" }
|
||||
const orderBy: Record<string, "DESC" | "ASC"> = order ?? {
|
||||
created_at: "DESC",
|
||||
}
|
||||
|
||||
return {
|
||||
select: includeFields.length ? includeFields : defaultAdminTaxRatesFields,
|
||||
|
||||
@@ -125,23 +125,25 @@ export const defaultStoreCartRelations = [
|
||||
"shipping_methods.shipping_option",
|
||||
"discounts",
|
||||
"discounts.rule",
|
||||
"discounts.rule.valid_for",
|
||||
]
|
||||
|
||||
export type StoreCartsRes = {
|
||||
cart: Omit<Cart, "refundable_amount" | "refunded_total">
|
||||
}
|
||||
|
||||
export type StoreCompleteCartRes = {
|
||||
type: "cart"
|
||||
data: Cart
|
||||
} | {
|
||||
type: "order"
|
||||
data: Order
|
||||
} | {
|
||||
type: "swap"
|
||||
data: Swap
|
||||
}
|
||||
export type StoreCompleteCartRes =
|
||||
| {
|
||||
type: "cart"
|
||||
data: Cart
|
||||
}
|
||||
| {
|
||||
type: "order"
|
||||
data: Order
|
||||
}
|
||||
| {
|
||||
type: "swap"
|
||||
data: Swap
|
||||
}
|
||||
|
||||
export type StoreCartsDeleteRes = DeleteResponse
|
||||
|
||||
|
||||
@@ -39,7 +39,6 @@ export const defaultStoreOrdersRelations = [
|
||||
"shipping_methods",
|
||||
"discounts",
|
||||
"discounts.rule",
|
||||
"discounts.rule.valid_for",
|
||||
"customer",
|
||||
"payments",
|
||||
"region",
|
||||
@@ -79,7 +78,6 @@ export const allowedStoreOrdersRelations = [
|
||||
"shipping_methods",
|
||||
"discounts",
|
||||
"discounts.rule",
|
||||
"discounts.rule.valid_for",
|
||||
"customer",
|
||||
"payments",
|
||||
"region",
|
||||
|
||||
Reference in New Issue
Block a user