UNPKG

@vendure/core

Version:

A modern, headless ecommerce framework

319 lines 17 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.PromotionService = void 0; const common_1 = require("@nestjs/common"); const generated_types_1 = require("@vendure/common/lib/generated-types"); const unique_1 = require("@vendure/common/lib/unique"); const typeorm_1 = require("typeorm"); const errors_1 = require("../../common/error/errors"); const generated_graphql_admin_errors_1 = require("../../common/error/generated-graphql-admin-errors"); const generated_graphql_shop_errors_1 = require("../../common/error/generated-graphql-shop-errors"); const instrument_decorator_1 = require("../../common/instrument-decorator"); const adjustment_source_1 = require("../../common/types/adjustment-source"); const utils_1 = require("../../common/utils"); const config_service_1 = require("../../config/config.service"); const transactional_connection_1 = require("../../connection/transactional-connection"); const order_entity_1 = require("../../entity/order/order.entity"); const promotion_translation_entity_1 = require("../../entity/promotion/promotion-translation.entity"); const promotion_entity_1 = require("../../entity/promotion/promotion.entity"); const event_bus_1 = require("../../event-bus"); const promotion_event_1 = require("../../event-bus/events/promotion-event"); const config_arg_service_1 = require("../helpers/config-arg/config-arg.service"); const custom_field_relation_service_1 = require("../helpers/custom-field-relation/custom-field-relation.service"); const list_query_builder_1 = require("../helpers/list-query-builder/list-query-builder"); const translatable_saver_1 = require("../helpers/translatable-saver/translatable-saver"); const translator_service_1 = require("../helpers/translator/translator.service"); const channel_service_1 = require("./channel.service"); /** * @description * Contains methods relating to {@link Promotion} entities. * * @docsCategory services */ let PromotionService = class PromotionService { constructor(connection, configService, channelService, listQueryBuilder, configArgService, customFieldRelationService, eventBus, translatableSaver, translator) { this.connection = connection; this.configService = configService; this.channelService = channelService; this.listQueryBuilder = listQueryBuilder; this.configArgService = configArgService; this.customFieldRelationService = customFieldRelationService; this.eventBus = eventBus; this.translatableSaver = translatableSaver; this.translator = translator; this.availableConditions = []; this.availableActions = []; this.availableConditions = this.configService.promotionOptions.promotionConditions || []; this.availableActions = this.configService.promotionOptions.promotionActions || []; } findAll(ctx, options, relations = []) { return this.listQueryBuilder .build(promotion_entity_1.Promotion, options, { where: { deletedAt: (0, typeorm_1.IsNull)() }, channelId: ctx.channelId, relations, ctx, }) .getManyAndCount() .then(([promotions, totalItems]) => { const items = promotions.map(promotion => this.translator.translate(promotion, ctx)); return { items, totalItems, }; }); } async findOne(ctx, adjustmentSourceId, relations = []) { return this.connection .findOneInChannel(ctx, promotion_entity_1.Promotion, adjustmentSourceId, ctx.channelId, { where: { deletedAt: (0, typeorm_1.IsNull)() }, relations, }) .then(promotion => { var _a; return (_a = (promotion && this.translator.translate(promotion, ctx))) !== null && _a !== void 0 ? _a : undefined; }); } getPromotionConditions(ctx) { return this.availableConditions.map(x => x.toGraphQlType(ctx)); } getPromotionActions(ctx) { return this.availableActions.map(x => x.toGraphQlType(ctx)); } async createPromotion(ctx, input) { const conditions = input.conditions.map(c => this.configArgService.parseInput('PromotionCondition', c)); const actions = input.actions.map(a => this.configArgService.parseInput('PromotionAction', a)); this.validateRequiredConditions(conditions, actions); if (conditions.length === 0 && !input.couponCode) { return new generated_graphql_admin_errors_1.MissingConditionsError(); } const newPromotion = await this.translatableSaver.create({ ctx, input, entityType: promotion_entity_1.Promotion, translationType: promotion_translation_entity_1.PromotionTranslation, beforeSave: async (p) => { p.priorityScore = this.calculatePriorityScore(input); p.conditions = conditions; p.actions = actions; await this.channelService.assignToCurrentChannel(p, ctx); }, }); const promotionWithRelations = await this.customFieldRelationService.updateRelations(ctx, promotion_entity_1.Promotion, input, newPromotion); await this.eventBus.publish(new promotion_event_1.PromotionEvent(ctx, promotionWithRelations, 'created', input)); return (0, utils_1.assertFound)(this.findOne(ctx, newPromotion.id)); } async updatePromotion(ctx, input) { const promotion = await this.connection.getEntityOrThrow(ctx, promotion_entity_1.Promotion, input.id, { channelId: ctx.channelId, }); const hasConditions = input.conditions ? input.conditions.length > 0 : promotion.conditions.length > 0; const hasCouponCode = input.couponCode != null ? !!input.couponCode : !!promotion.couponCode; if (!hasConditions && !hasCouponCode) { return new generated_graphql_admin_errors_1.MissingConditionsError(); } const updatedPromotion = await this.translatableSaver.update({ ctx, input, entityType: promotion_entity_1.Promotion, translationType: promotion_translation_entity_1.PromotionTranslation, beforeSave: async (p) => { p.priorityScore = this.calculatePriorityScore(input); if (input.conditions) { p.conditions = input.conditions.map(c => this.configArgService.parseInput('PromotionCondition', c)); } if (input.actions) { p.actions = input.actions.map(a => this.configArgService.parseInput('PromotionAction', a)); } }, }); await this.customFieldRelationService.updateRelations(ctx, promotion_entity_1.Promotion, input, updatedPromotion); await this.eventBus.publish(new promotion_event_1.PromotionEvent(ctx, promotion, 'updated', input)); return (0, utils_1.assertFound)(this.findOne(ctx, updatedPromotion.id)); } async softDeletePromotion(ctx, promotionId) { const promotion = await this.connection.getEntityOrThrow(ctx, promotion_entity_1.Promotion, promotionId); await this.connection .getRepository(ctx, promotion_entity_1.Promotion) .update({ id: promotionId }, { deletedAt: new Date() }); await this.eventBus.publish(new promotion_event_1.PromotionEvent(ctx, promotion, 'deleted', promotionId)); return { result: generated_types_1.DeletionResult.DELETED, }; } async assignPromotionsToChannel(ctx, input) { const promotions = await this.connection.findByIdsInChannel(ctx, promotion_entity_1.Promotion, input.promotionIds, ctx.channelId, {}); for (const promotion of promotions) { await this.channelService.assignToChannels(ctx, promotion_entity_1.Promotion, promotion.id, [input.channelId]); } return promotions.map(p => this.translator.translate(p, ctx)); } async removePromotionsFromChannel(ctx, input) { const promotions = await this.connection.findByIdsInChannel(ctx, promotion_entity_1.Promotion, input.promotionIds, ctx.channelId, {}); for (const promotion of promotions) { await this.channelService.removeFromChannels(ctx, promotion_entity_1.Promotion, promotion.id, [input.channelId]); } return promotions.map(p => this.translator.translate(p, ctx)); } /** * @description * Checks the validity of a coupon code, by checking that it is associated with an existing, * enabled and non-expired Promotion. Additionally, if there is a usage limit on the coupon code, * this method will enforce that limit against the specified Customer. */ async validateCouponCode(ctx, couponCode, customerId) { const promotion = await this.connection.getRepository(ctx, promotion_entity_1.Promotion).findOne({ where: { couponCode, enabled: true, deletedAt: (0, typeorm_1.IsNull)(), channels: { id: ctx.channelId }, }, relations: ['channels'], }); if (!promotion || promotion.couponCode !== couponCode || !promotion.channels.find(c => (0, utils_1.idsAreEqual)(c.id, ctx.channelId))) { return new generated_graphql_shop_errors_1.CouponCodeInvalidError({ couponCode }); } if (promotion.endsAt && +promotion.endsAt < +new Date()) { return new generated_graphql_shop_errors_1.CouponCodeExpiredError({ couponCode }); } if (customerId && promotion.perCustomerUsageLimit != null) { const usageCount = await this.countPromotionUsagesForCustomer(ctx, promotion.id, customerId); if (promotion.perCustomerUsageLimit <= usageCount) { return new generated_graphql_shop_errors_1.CouponCodeLimitError({ couponCode, limit: promotion.perCustomerUsageLimit }); } } if (promotion.usageLimit !== null) { const usageCount = await this.countPromotionUsages(ctx, promotion.id); if (promotion.usageLimit <= usageCount) { return new generated_graphql_shop_errors_1.CouponCodeLimitError({ couponCode, limit: promotion.usageLimit }); } } return promotion; } getActivePromotionsInChannel(ctx) { return this.connection .getRepository(ctx, promotion_entity_1.Promotion) .createQueryBuilder('promotion') .leftJoin('promotion.channels', 'channel') .leftJoinAndSelect('promotion.translations', 'translation') .where('channel.id = :channelId', { channelId: ctx.channelId }) .andWhere('promotion.deletedAt IS NULL') .andWhere('promotion.enabled = :enabled', { enabled: true }) .orderBy('promotion.priorityScore', 'ASC') .getMany() .then(promotions => promotions.map(p => this.translator.translate(p, ctx))); } async getActivePromotionsOnOrder(ctx, orderId) { var _a; const order = await this.connection .getRepository(ctx, order_entity_1.Order) .createQueryBuilder('order') .leftJoinAndSelect('order.promotions', 'promotions') .where('order.id = :orderId', { orderId }) .getOne(); return (_a = order === null || order === void 0 ? void 0 : order.promotions) !== null && _a !== void 0 ? _a : []; } async runPromotionSideEffects(ctx, order, promotionsPre) { const promotionsPost = order.promotions; for (const activePre of promotionsPre) { if (!promotionsPost.find(p => (0, utils_1.idsAreEqual)(p.id, activePre.id))) { // activePre is no longer active, so call onDeactivate await activePre.deactivate(ctx, order); } } for (const activePost of promotionsPost) { if (!promotionsPre.find(p => (0, utils_1.idsAreEqual)(p.id, activePost.id))) { // activePost was not previously active, so call onActivate await activePost.activate(ctx, order); } } } /** * @description * Used internally to associate a Promotion with an Order, once an Order has been placed. * * @deprecated This method is no longer used and will be removed in v2.0 */ async addPromotionsToOrder(ctx, order) { const allPromotionIds = order.discounts.map(a => adjustment_source_1.AdjustmentSource.decodeSourceId(a.adjustmentSource).id); const promotionIds = (0, unique_1.unique)(allPromotionIds); const promotions = await this.connection .getRepository(ctx, promotion_entity_1.Promotion) .find({ where: { id: (0, typeorm_1.In)(promotionIds) } }); order.promotions = promotions; return this.connection.getRepository(ctx, order_entity_1.Order).save(order); } async countPromotionUsagesForCustomer(ctx, promotionId, customerId) { const qb = this.connection .getRepository(ctx, order_entity_1.Order) .createQueryBuilder('order') .leftJoin('order.promotions', 'promotion') .where('promotion.id = :promotionId', { promotionId }) .andWhere('order.customer = :customerId', { customerId }) .andWhere('order.state != :state', { state: 'Cancelled' }) .andWhere('order.active = :active', { active: false }); return qb.getCount(); } async countPromotionUsages(ctx, promotionId) { const qb = this.connection .getRepository(ctx, order_entity_1.Order) .createQueryBuilder('order') .leftJoin('order.promotions', 'promotion') .where('promotion.id = :promotionId', { promotionId }) .andWhere('order.state != :state', { state: 'Cancelled' }) .andWhere('order.active = :active', { active: false }); return qb.getCount(); } calculatePriorityScore(input) { const conditions = input.conditions ? input.conditions.map(c => this.configArgService.getByCode('PromotionCondition', c.code)) : []; const actions = input.actions ? input.actions.map(c => this.configArgService.getByCode('PromotionAction', c.code)) : []; return [...conditions, ...actions].reduce((score, op) => score + op.priorityValue, 0); } validateRequiredConditions(conditions, actions) { const conditionCodes = conditions.reduce((codeMap, { code }) => (Object.assign(Object.assign({}, codeMap), { [code]: code })), {}); for (const { code: actionCode } of actions) { const actionDef = this.configArgService.getByCode('PromotionAction', actionCode); const actionDependencies = actionDef.conditions || []; if (!actionDependencies || actionDependencies.length === 0) { continue; } const missingConditions = actionDependencies.filter(condition => !conditionCodes[condition.code]); if (missingConditions.length) { throw new errors_1.UserInputError('error.conditions-required-for-action', { action: actionCode, conditions: missingConditions.map(c => c.code).join(', '), }); } } } }; exports.PromotionService = PromotionService; exports.PromotionService = PromotionService = __decorate([ (0, common_1.Injectable)(), (0, instrument_decorator_1.Instrument)(), __metadata("design:paramtypes", [transactional_connection_1.TransactionalConnection, config_service_1.ConfigService, channel_service_1.ChannelService, list_query_builder_1.ListQueryBuilder, config_arg_service_1.ConfigArgService, custom_field_relation_service_1.CustomFieldRelationService, event_bus_1.EventBus, translatable_saver_1.TranslatableSaver, translator_service_1.TranslatorService]) ], PromotionService); //# sourceMappingURL=promotion.service.js.map