384 lines
12 KiB
TypeScript
384 lines
12 KiB
TypeScript
import { MedusaModule, Modules } from "@medusajs/modules-sdk"
|
|
import { DeleteResult, EntityTarget, In, Not } from "typeorm"
|
|
import { dataSource } from "../loaders/database"
|
|
import { featureFlagRouter } from "../loaders/feature-flags"
|
|
import IsolateProductDomainFeatureFlag from "../loaders/feature-flags/isolate-product-domain"
|
|
import {
|
|
Discount,
|
|
DiscountCondition,
|
|
DiscountConditionCustomerGroup,
|
|
DiscountConditionOperator,
|
|
DiscountConditionProduct,
|
|
DiscountConditionProductCollection,
|
|
DiscountConditionProductTag,
|
|
DiscountConditionProductType,
|
|
DiscountConditionType,
|
|
} from "../models"
|
|
import { isString } from "../utils"
|
|
|
|
export enum DiscountConditionJoinTableForeignKey {
|
|
PRODUCT_ID = "product_id",
|
|
PRODUCT_TYPE_ID = "product_type_id",
|
|
PRODUCT_COLLECTION_ID = "product_collection_id",
|
|
PRODUCT_TAG_ID = "product_tag_id",
|
|
CUSTOMER_GROUP_ID = "customer_group_id",
|
|
}
|
|
|
|
type DiscountConditionResourceType = EntityTarget<
|
|
| DiscountConditionProduct
|
|
| DiscountConditionProductType
|
|
| DiscountConditionProductCollection
|
|
| DiscountConditionProductTag
|
|
| DiscountConditionCustomerGroup
|
|
>
|
|
|
|
export const DiscountConditionRepository = dataSource
|
|
.getRepository(DiscountCondition)
|
|
.extend({
|
|
async findOneWithDiscount(
|
|
conditionId: string,
|
|
discountId: string
|
|
): Promise<(DiscountCondition & { discount: Discount }) | undefined> {
|
|
return (await this.createQueryBuilder("condition")
|
|
.leftJoinAndMapOne(
|
|
"condition.discount",
|
|
Discount,
|
|
"discount",
|
|
`condition.discount_rule_id = discount.rule_id and discount.id = :discId and condition.id = :dcId`,
|
|
{ discId: discountId, dcId: conditionId }
|
|
)
|
|
.getOne()) as (DiscountCondition & { discount: Discount }) | undefined
|
|
},
|
|
|
|
getJoinTableResourceIdentifiers(type: string): {
|
|
joinTable: string
|
|
resourceKey: string
|
|
joinTableForeignKey: DiscountConditionJoinTableForeignKey
|
|
conditionTable: DiscountConditionResourceType
|
|
joinTableKey: string
|
|
relatedTable: string
|
|
} {
|
|
let conditionTable: DiscountConditionResourceType =
|
|
DiscountConditionProduct
|
|
|
|
let joinTable = "product"
|
|
let joinTableForeignKey: DiscountConditionJoinTableForeignKey =
|
|
DiscountConditionJoinTableForeignKey.PRODUCT_ID
|
|
let joinTableKey = "id"
|
|
let relatedTable = ""
|
|
|
|
// On the joined table (e.g. `product`), what key should be match on
|
|
// (e.g `type_id` for product types and `id` for products)
|
|
let resourceKey
|
|
|
|
switch (type) {
|
|
case DiscountConditionType.PRODUCTS: {
|
|
resourceKey = "id"
|
|
joinTableForeignKey = DiscountConditionJoinTableForeignKey.PRODUCT_ID
|
|
joinTable = "product"
|
|
|
|
conditionTable = DiscountConditionProduct
|
|
break
|
|
}
|
|
case DiscountConditionType.PRODUCT_TYPES: {
|
|
resourceKey = "type_id"
|
|
joinTableForeignKey =
|
|
DiscountConditionJoinTableForeignKey.PRODUCT_TYPE_ID
|
|
joinTable = "product"
|
|
relatedTable = "types"
|
|
|
|
conditionTable = DiscountConditionProductType
|
|
break
|
|
}
|
|
case DiscountConditionType.PRODUCT_COLLECTIONS: {
|
|
resourceKey = "collection_id"
|
|
joinTableForeignKey =
|
|
DiscountConditionJoinTableForeignKey.PRODUCT_COLLECTION_ID
|
|
joinTable = "product"
|
|
relatedTable = "collections"
|
|
|
|
conditionTable = DiscountConditionProductCollection
|
|
break
|
|
}
|
|
case DiscountConditionType.PRODUCT_TAGS: {
|
|
joinTableKey = "product_id"
|
|
resourceKey = "product_tag_id"
|
|
joinTableForeignKey =
|
|
DiscountConditionJoinTableForeignKey.PRODUCT_TAG_ID
|
|
joinTable = "product_tags"
|
|
relatedTable = "tags"
|
|
|
|
conditionTable = DiscountConditionProductTag
|
|
break
|
|
}
|
|
case DiscountConditionType.CUSTOMER_GROUPS: {
|
|
joinTableKey = "customer_id"
|
|
resourceKey = "customer_group_id"
|
|
joinTable = "customer_group_customers"
|
|
joinTableForeignKey =
|
|
DiscountConditionJoinTableForeignKey.CUSTOMER_GROUP_ID
|
|
|
|
conditionTable = DiscountConditionCustomerGroup
|
|
break
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
|
|
return {
|
|
joinTable,
|
|
joinTableKey,
|
|
resourceKey,
|
|
joinTableForeignKey,
|
|
conditionTable,
|
|
relatedTable,
|
|
}
|
|
},
|
|
|
|
async removeConditionResources(
|
|
id: string,
|
|
type: DiscountConditionType,
|
|
resourceIds: (string | { id: string })[]
|
|
): Promise<DeleteResult | void> {
|
|
const { conditionTable, joinTableForeignKey } =
|
|
this.getJoinTableResourceIdentifiers(type)
|
|
|
|
if (!conditionTable || !joinTableForeignKey) {
|
|
return Promise.resolve()
|
|
}
|
|
|
|
const idsToDelete = resourceIds.map((rId): string => {
|
|
return isString(rId) ? rId : rId.id
|
|
})
|
|
return await this.createQueryBuilder()
|
|
.delete()
|
|
.from(conditionTable)
|
|
.where({ condition_id: id, [joinTableForeignKey]: In(idsToDelete) })
|
|
.execute()
|
|
},
|
|
|
|
async addConditionResources(
|
|
conditionId: string,
|
|
resourceIds: (string | { id: string })[],
|
|
type: DiscountConditionType,
|
|
overrideExisting = false
|
|
): Promise<
|
|
(
|
|
| DiscountConditionProduct
|
|
| DiscountConditionProductType
|
|
| DiscountConditionProductCollection
|
|
| DiscountConditionProductTag
|
|
| DiscountConditionCustomerGroup
|
|
)[]
|
|
> {
|
|
let toInsert: { condition_id: string; [x: string]: string }[] | [] = []
|
|
|
|
const { conditionTable, joinTableForeignKey } =
|
|
this.getJoinTableResourceIdentifiers(type)
|
|
|
|
if (!conditionTable || !joinTableForeignKey) {
|
|
return Promise.resolve([])
|
|
}
|
|
|
|
const idsToInsert = resourceIds.map((rId): string => {
|
|
return isString(rId) ? rId : rId.id
|
|
})
|
|
toInsert = idsToInsert.map((rId) => ({
|
|
condition_id: conditionId,
|
|
[joinTableForeignKey]: rId,
|
|
}))
|
|
|
|
const insertResult = await this.createQueryBuilder()
|
|
.insert()
|
|
.orIgnore(true)
|
|
.into(conditionTable)
|
|
.values(toInsert)
|
|
.execute()
|
|
|
|
if (overrideExisting) {
|
|
await this.createQueryBuilder()
|
|
.delete()
|
|
.from(conditionTable)
|
|
.where({
|
|
condition_id: conditionId,
|
|
[joinTableForeignKey]: Not(In(idsToInsert)),
|
|
})
|
|
.execute()
|
|
}
|
|
|
|
return await this.manager
|
|
.createQueryBuilder(conditionTable, "discon")
|
|
.select()
|
|
.where(insertResult.identifiers)
|
|
.getMany()
|
|
},
|
|
|
|
async queryConditionTable({
|
|
type,
|
|
conditionId,
|
|
resourceId,
|
|
}): Promise<number> {
|
|
const {
|
|
conditionTable,
|
|
joinTable,
|
|
joinTableForeignKey,
|
|
resourceKey,
|
|
joinTableKey,
|
|
relatedTable,
|
|
} = this.getJoinTableResourceIdentifiers(type)
|
|
|
|
if (
|
|
type !== DiscountConditionType.CUSTOMER_GROUPS &&
|
|
featureFlagRouter.isFeatureEnabled(IsolateProductDomainFeatureFlag.key)
|
|
) {
|
|
const module = MedusaModule.getModuleInstance(Modules.PRODUCT)[
|
|
Modules.PRODUCT
|
|
]
|
|
const prop = relatedTable
|
|
const resource = await module.retrieve(resourceId, {
|
|
select: [`${prop ? prop + "." : ""}id`],
|
|
relations: prop ? [prop] : [],
|
|
})
|
|
if (!resource) {
|
|
return 0
|
|
}
|
|
|
|
const relatedResourceIds = prop
|
|
? resource[prop].map((relatedResource) => relatedResource.id)
|
|
: [resource.id]
|
|
|
|
if (!relatedResourceIds.length) {
|
|
return 0
|
|
}
|
|
|
|
return await this.manager
|
|
.createQueryBuilder(conditionTable, "dc")
|
|
.where(
|
|
`dc.condition_id = :conditionId AND dc.${joinTableForeignKey} IN (:...relatedResourceIds)`,
|
|
{
|
|
conditionId,
|
|
relatedResourceIds,
|
|
}
|
|
)
|
|
.getCount()
|
|
}
|
|
|
|
return await this.manager
|
|
.createQueryBuilder(conditionTable, "dc")
|
|
.innerJoin(
|
|
joinTable,
|
|
"resource",
|
|
`dc.${joinTableForeignKey} = resource.${resourceKey} and resource.${joinTableKey} = :resourceId `,
|
|
{
|
|
resourceId,
|
|
}
|
|
)
|
|
.where(`dc.condition_id = :conditionId`, {
|
|
conditionId,
|
|
})
|
|
.getCount()
|
|
},
|
|
|
|
async isValidForProduct(
|
|
discountRuleId: string,
|
|
productId: string
|
|
): Promise<boolean> {
|
|
const discountConditions = await this.createQueryBuilder("discon")
|
|
.select(["discon.id", "discon.type", "discon.operator"])
|
|
.where("discon.discount_rule_id = :discountRuleId", {
|
|
discountRuleId,
|
|
})
|
|
.getMany()
|
|
|
|
// in case of no discount conditions, we assume that the discount
|
|
// is valid for all
|
|
if (!discountConditions.length) {
|
|
return true
|
|
}
|
|
|
|
// retrieve all conditions for each type where condition type id is in jointable (products, product_types, product_collections, product_tags)
|
|
// "E.g. for a given product condition, give me all products affected by it"
|
|
// for each of these types, we check:
|
|
// if condition operation is `in` and the query for conditions defined for the given type is empty, the discount is invalid
|
|
// if condition operation is `not_in` and the query for conditions defined for the given type is not empty, the discount is invalid
|
|
for (const condition of discountConditions) {
|
|
if (condition.type === DiscountConditionType.CUSTOMER_GROUPS) {
|
|
continue
|
|
}
|
|
|
|
const numConditions = await this.queryConditionTable({
|
|
type: condition.type,
|
|
conditionId: condition.id,
|
|
resourceId: productId,
|
|
})
|
|
|
|
if (
|
|
condition.operator === DiscountConditionOperator.IN &&
|
|
numConditions === 0
|
|
) {
|
|
return false
|
|
}
|
|
|
|
if (
|
|
condition.operator === DiscountConditionOperator.NOT_IN &&
|
|
numConditions > 0
|
|
) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
},
|
|
|
|
async canApplyForCustomer(
|
|
discountRuleId: string,
|
|
customerId: string
|
|
): Promise<boolean> {
|
|
const discountConditions = await this.createQueryBuilder("discon")
|
|
.select(["discon.id", "discon.type", "discon.operator"])
|
|
.where("discon.discount_rule_id = :discountRuleId", {
|
|
discountRuleId,
|
|
})
|
|
.andWhere("discon.type = :type", {
|
|
type: DiscountConditionType.CUSTOMER_GROUPS,
|
|
})
|
|
.getMany()
|
|
|
|
// in case of no discount conditions, we assume that the discount
|
|
// is valid for all
|
|
if (!discountConditions.length) {
|
|
return true
|
|
}
|
|
|
|
// retrieve conditions for customer groups
|
|
// for each customer group
|
|
// if condition operation is `in` and the query for customer group conditions is empty, the discount is invalid
|
|
// if condition operation is `not_in` and the query for customer group conditions is not empty, the discount is invalid
|
|
for (const condition of discountConditions) {
|
|
const numConditions = await this.queryConditionTable({
|
|
type: "customer_groups",
|
|
conditionId: condition.id,
|
|
resourceId: customerId,
|
|
})
|
|
|
|
if (
|
|
condition.operator === DiscountConditionOperator.IN &&
|
|
numConditions === 0
|
|
) {
|
|
return false
|
|
}
|
|
|
|
if (
|
|
condition.operator === DiscountConditionOperator.NOT_IN &&
|
|
numConditions > 0
|
|
) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
},
|
|
})
|
|
|
|
export default DiscountConditionRepository
|