create discount camp
This commit is contained in:
@@ -0,0 +1,58 @@
|
|||||||
|
import { Body, Controller, Delete, Get, Param, Post, UseGuards, ValidationPipe } from '@nestjs/common';
|
||||||
|
import { ApiBearerAuth, ApiBody, ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger';
|
||||||
|
import { AdminAuthGuard } from 'src/modules/auth/guards/adminAuth.guard';
|
||||||
|
import { Permissions } from 'src/common/decorators/permissions.decorator';
|
||||||
|
import { Permission } from 'src/common/enums/permission.enum';
|
||||||
|
import { RestId } from 'src/common/decorators/rest-id.decorator';
|
||||||
|
import { CreateCampaignDto } from '../dto/create-campaign.dto';
|
||||||
|
import { CampaignService } from '../providers/campaign.service';
|
||||||
|
import { CommonMessage } from 'src/common/enums/message.enum';
|
||||||
|
|
||||||
|
@ApiTags('Campaigns')
|
||||||
|
@Controller()
|
||||||
|
export class CampaignsController {
|
||||||
|
constructor(private readonly campaignService: CampaignService) {}
|
||||||
|
|
||||||
|
@UseGuards(AdminAuthGuard)
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@Permissions(Permission.MANAGE_USERS)
|
||||||
|
@Post('admin/campaigns/discount')
|
||||||
|
@ApiOperation({ summary: 'Create discount campaign' })
|
||||||
|
@ApiBody({ type: CreateCampaignDto })
|
||||||
|
createDiscountCampaign(
|
||||||
|
@Body(new ValidationPipe({ transform: true, whitelist: true })) dto: CreateCampaignDto,
|
||||||
|
@RestId() restId: string,
|
||||||
|
) {
|
||||||
|
return this.campaignService.createDiscountCampaign(restId, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@UseGuards(AdminAuthGuard)
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@Permissions(Permission.MANAGE_USERS)
|
||||||
|
@Get('admin/campaigns')
|
||||||
|
@ApiOperation({ summary: 'Get all campaigns' })
|
||||||
|
findAll(@RestId() restId: string) {
|
||||||
|
return this.campaignService.findAll(restId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@UseGuards(AdminAuthGuard)
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@Permissions(Permission.MANAGE_USERS)
|
||||||
|
@Get('admin/campaigns/:campaignId')
|
||||||
|
@ApiOperation({ summary: 'Get campaign by id' })
|
||||||
|
@ApiParam({ name: 'campaignId', description: 'Campaign ID' })
|
||||||
|
findOne(@Param('campaignId') campaignId: string, @RestId() restId: string) {
|
||||||
|
return this.campaignService.findOneOrFail(restId, campaignId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@UseGuards(AdminAuthGuard)
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@Permissions(Permission.MANAGE_USERS)
|
||||||
|
@Delete('admin/campaigns/:campaignId')
|
||||||
|
@ApiOperation({ summary: 'Delete campaign' })
|
||||||
|
@ApiParam({ name: 'campaignId', description: 'Campaign ID' })
|
||||||
|
async remove(@Param('campaignId') campaignId: string, @RestId() restId: string) {
|
||||||
|
await this.campaignService.remove(restId, campaignId);
|
||||||
|
return { message: CommonMessage.DELETED };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { IsArray, IsEnum, IsNotEmpty, IsString } from 'class-validator';
|
||||||
|
import { CampaignSentType } from '../interface/campaign';
|
||||||
|
|
||||||
|
export class CreateCampaignDto {
|
||||||
|
@ApiProperty({ example: 'کمپین تابستانه' })
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
title!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ type: [String], example: ['01JABC...', '01JDEF...'] })
|
||||||
|
@IsArray()
|
||||||
|
@IsString({ each: true })
|
||||||
|
groups!: string[];
|
||||||
|
|
||||||
|
@ApiProperty({ enum: CampaignSentType, example: CampaignSentType.PUSH })
|
||||||
|
@IsEnum(CampaignSentType)
|
||||||
|
sentType!: CampaignSentType;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'SUMMER20' })
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
discountCode!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'کمپین افتتاحیه' })
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
ocasion!: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { Entity, Enum, Index, ManyToOne, Property } from '@mikro-orm/core';
|
||||||
|
import { BaseEntity } from '../../../common/entities/base.entity';
|
||||||
|
import { CampaignSentType } from '../interface/campaign';
|
||||||
|
import { Restaurant } from 'src/modules/restaurants/entities/restaurant.entity';
|
||||||
|
|
||||||
|
@Entity({ tableName: 'campaigns' })
|
||||||
|
@Index({ properties: ['restaurant'] })
|
||||||
|
export class Campaign extends BaseEntity {
|
||||||
|
@ManyToOne(() => Restaurant)
|
||||||
|
restaurant!: Restaurant;
|
||||||
|
|
||||||
|
@Property()
|
||||||
|
title!: string;
|
||||||
|
|
||||||
|
@Property({ type: 'json', default: '[]' })
|
||||||
|
groups: string[] = [];
|
||||||
|
|
||||||
|
@Enum(() => CampaignSentType)
|
||||||
|
sentType!: CampaignSentType;
|
||||||
|
|
||||||
|
@Property()
|
||||||
|
discountCode!: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export enum CampaignSentType {
|
||||||
|
PUSH = 'push',
|
||||||
|
SMS = 'sms',
|
||||||
|
}
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||||||
|
import { EntityManager, RequiredEntityData } from '@mikro-orm/postgresql';
|
||||||
|
import { Campaign } from '../entities/campaign.entity';
|
||||||
|
import { CampaignRepository } from '../repositories/campaign.repository';
|
||||||
|
import { CreateCampaignDto } from '../dto/create-campaign.dto';
|
||||||
|
import { RestaurantsService } from 'src/modules/restaurants/providers/restaurants.service';
|
||||||
|
import { UserGroupRepository } from '../repositories/user-group.repository';
|
||||||
|
import { UserGroupUserRepository } from '../repositories/user-group-user.repository';
|
||||||
|
import { SmsQueueService } from 'src/modules/notifications/services/sms-queue.service';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { CampaignSentType } from '../interface/campaign';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CampaignService {
|
||||||
|
constructor(
|
||||||
|
private readonly campaignRepository: CampaignRepository,
|
||||||
|
private readonly userGroupRepository: UserGroupRepository,
|
||||||
|
private readonly userGroupUserRepository: UserGroupUserRepository,
|
||||||
|
private readonly restaurantsService: RestaurantsService,
|
||||||
|
private readonly smsQueueService: SmsQueueService,
|
||||||
|
private readonly configService: ConfigService,
|
||||||
|
private readonly em: EntityManager,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async createDiscountCampaign(restId: string, dto: CreateCampaignDto): Promise<Campaign> {
|
||||||
|
const restaurant = await this.restaurantsService.findOneOrFail(restId);
|
||||||
|
const uniqueGroups = [...new Set(dto.groups)];
|
||||||
|
|
||||||
|
if (uniqueGroups.length === 0) {
|
||||||
|
throw new BadRequestException('حداقل یک گروه الزامی است');
|
||||||
|
}
|
||||||
|
|
||||||
|
const validGroupsCount = await this.userGroupRepository.count({
|
||||||
|
id: { $in: uniqueGroups },
|
||||||
|
restaurant: { id: restId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (validGroupsCount !== uniqueGroups.length) {
|
||||||
|
throw new BadRequestException('گروه(ها) معتبر نیستند');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: RequiredEntityData<Campaign> = {
|
||||||
|
restaurant,
|
||||||
|
title: dto.title,
|
||||||
|
groups: uniqueGroups,
|
||||||
|
sentType: dto.sentType,
|
||||||
|
discountCode: dto.discountCode,
|
||||||
|
};
|
||||||
|
|
||||||
|
const campaign = this.campaignRepository.create(data);
|
||||||
|
await this.em.persistAndFlush(campaign);
|
||||||
|
|
||||||
|
if (dto.sentType === CampaignSentType.SMS) {
|
||||||
|
await this.runSmsDiscountCampaignFlow(restId, uniqueGroups, dto, restaurant.name);
|
||||||
|
} else if (dto.sentType === CampaignSentType.PUSH) {
|
||||||
|
await this.runPushDiscountCampaignFlow(restId, uniqueGroups, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
return campaign;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async runSmsDiscountCampaignFlow(
|
||||||
|
restId: string,
|
||||||
|
uniqueGroups: string[],
|
||||||
|
dto: CreateCampaignDto,
|
||||||
|
restaurantTitle: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const discountTemplateId = this.configService.get<string>('SMS_PATTERN_DISCOUNT_CODE');
|
||||||
|
if (!discountTemplateId) {
|
||||||
|
throw new BadRequestException('الگوی پیامک تخفیف تنظیم نشده است');
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupMembers = await this.userGroupUserRepository.find(
|
||||||
|
{
|
||||||
|
userGroup: {
|
||||||
|
id: { $in: uniqueGroups },
|
||||||
|
restaurant: { id: restId },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ populate: ['user'] },
|
||||||
|
);
|
||||||
|
|
||||||
|
const recipientsByPhone = new Map<string, string>();
|
||||||
|
for (const member of groupMembers) {
|
||||||
|
const phone = member.user?.phone;
|
||||||
|
if (!phone) continue;
|
||||||
|
|
||||||
|
const firstName = member.user?.firstName ?? '';
|
||||||
|
const existingName = recipientsByPhone.get(phone);
|
||||||
|
if (!existingName || existingName.trim().length === 0) {
|
||||||
|
recipientsByPhone.set(phone, firstName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recipientsByPhone.size === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.smsQueueService.enqueueBulk(
|
||||||
|
[...recipientsByPhone.entries()].map(([phone, firstName]) => ({
|
||||||
|
phone,
|
||||||
|
templateId: discountTemplateId,
|
||||||
|
restaurantId: restId,
|
||||||
|
quantity: 1,
|
||||||
|
params: {
|
||||||
|
name: firstName ?? '',
|
||||||
|
disc: dto.discountCode,
|
||||||
|
oca: dto.ocasion,
|
||||||
|
rest: restaurantTitle,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async runPushDiscountCampaignFlow(
|
||||||
|
_restId: string,
|
||||||
|
_uniqueGroups: string[],
|
||||||
|
_dto: CreateCampaignDto,
|
||||||
|
): Promise<void> {
|
||||||
|
// Intentionally separated flow: push campaign handling is independent from SMS dispatch.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAll(restId: string): Promise<Campaign[]> {
|
||||||
|
return this.campaignRepository.find(
|
||||||
|
{ restaurant: { id: restId } },
|
||||||
|
{ orderBy: { createdAt: 'desc' } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOneOrFail(restId: string, campaignId: string): Promise<Campaign> {
|
||||||
|
const campaign = await this.campaignRepository.findOne({
|
||||||
|
id: campaignId,
|
||||||
|
restaurant: { id: restId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!campaign) {
|
||||||
|
throw new NotFoundException('کمپین یافت نشد');
|
||||||
|
}
|
||||||
|
|
||||||
|
return campaign;
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove(restId: string, campaignId: string): Promise<void> {
|
||||||
|
const campaign = await this.findOneOrFail(restId, campaignId);
|
||||||
|
campaign.deletedAt = new Date();
|
||||||
|
await this.em.persistAndFlush(campaign);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { EntityManager, EntityRepository } from '@mikro-orm/postgresql';
|
||||||
|
import { Campaign } from '../entities/campaign.entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CampaignRepository extends EntityRepository<Campaign> {
|
||||||
|
constructor(readonly em: EntityManager) {
|
||||||
|
super(em, Campaign);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module, forwardRef } from '@nestjs/common';
|
||||||
import { UserService } from './providers/user.service';
|
import { UserService } from './providers/user.service';
|
||||||
import { UsersController } from './controllers/users.controller';
|
import { UsersController } from './controllers/users.controller';
|
||||||
import { MikroOrmModule } from '@mikro-orm/nestjs';
|
import { MikroOrmModule } from '@mikro-orm/nestjs';
|
||||||
@@ -21,17 +21,23 @@ import { UserGroupRepository } from './repositories/user-group.repository';
|
|||||||
import { UserGroupUserRepository } from './repositories/user-group-user.repository';
|
import { UserGroupUserRepository } from './repositories/user-group-user.repository';
|
||||||
import { UserGroupService } from './providers/user-group.service';
|
import { UserGroupService } from './providers/user-group.service';
|
||||||
import { UserGroupsController } from './controllers/user-groups.controller';
|
import { UserGroupsController } from './controllers/user-groups.controller';
|
||||||
|
import { Campaign } from './entities/campaign.entity';
|
||||||
|
import { CampaignRepository } from './repositories/campaign.repository';
|
||||||
|
import { CampaignService } from './providers/campaign.service';
|
||||||
|
import { CampaignsController } from './controllers/campaigns.controller';
|
||||||
|
import { NotificationsModule } from '../notifications/notifications.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [UserService, WalletService, UserGroupService,
|
providers: [UserService, WalletService, UserGroupService, CampaignService,
|
||||||
UserRepository, WalletTransactionRepository,
|
UserRepository, WalletTransactionRepository,
|
||||||
PointTransactionRepository, UserRestaurantRepository, UserGroupRepository, UserGroupUserRepository, UserRestaurantCrone],
|
PointTransactionRepository, UserRestaurantRepository, UserGroupRepository, UserGroupUserRepository, CampaignRepository, UserRestaurantCrone],
|
||||||
controllers: [UsersController, UserGroupsController],
|
controllers: [UsersController, UserGroupsController, CampaignsController],
|
||||||
imports: [
|
imports: [
|
||||||
MikroOrmModule.forFeature([User, UserAddress, WalletTransaction, PointTransaction, UserRestaurant, UserGroup, UserGroupUser]),
|
MikroOrmModule.forFeature([User, UserAddress, WalletTransaction, PointTransaction, UserRestaurant, UserGroup, UserGroupUser, Campaign]),
|
||||||
JwtModule,
|
JwtModule,
|
||||||
RestaurantsModule,
|
RestaurantsModule,
|
||||||
|
forwardRef(() => NotificationsModule),
|
||||||
],
|
],
|
||||||
exports: [UserService, WalletService, UserGroupService, UserRepository, WalletTransactionRepository, PointTransactionRepository, UserGroupRepository],
|
exports: [UserService, WalletService, UserGroupService, CampaignService, UserRepository, WalletTransactionRepository, PointTransactionRepository, UserGroupRepository, CampaignRepository],
|
||||||
})
|
})
|
||||||
export class UserModule { }
|
export class UserModule { }
|
||||||
|
|||||||
Reference in New Issue
Block a user