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 { UsersController } from './controllers/users.controller';
|
||||
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 { UserGroupService } from './providers/user-group.service';
|
||||
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({
|
||||
providers: [UserService, WalletService, UserGroupService,
|
||||
providers: [UserService, WalletService, UserGroupService, CampaignService,
|
||||
UserRepository, WalletTransactionRepository,
|
||||
PointTransactionRepository, UserRestaurantRepository, UserGroupRepository, UserGroupUserRepository, UserRestaurantCrone],
|
||||
controllers: [UsersController, UserGroupsController],
|
||||
PointTransactionRepository, UserRestaurantRepository, UserGroupRepository, UserGroupUserRepository, CampaignRepository, UserRestaurantCrone],
|
||||
controllers: [UsersController, UserGroupsController, CampaignsController],
|
||||
imports: [
|
||||
MikroOrmModule.forFeature([User, UserAddress, WalletTransaction, PointTransaction, UserRestaurant, UserGroup, UserGroupUser]),
|
||||
MikroOrmModule.forFeature([User, UserAddress, WalletTransaction, PointTransaction, UserRestaurant, UserGroup, UserGroupUser, Campaign]),
|
||||
JwtModule,
|
||||
RestaurantsModule,
|
||||
forwardRef(() => NotificationsModule),
|
||||
],
|
||||
exports: [UserService, WalletService, UserGroupService, UserRepository, WalletTransactionRepository, PointTransactionRepository, UserGroupRepository],
|
||||
exports: [UserService, WalletService, UserGroupService, CampaignService, UserRepository, WalletTransactionRepository, PointTransactionRepository, UserGroupRepository, CampaignRepository],
|
||||
})
|
||||
export class UserModule { }
|
||||
|
||||
Reference in New Issue
Block a user