transactions
deploy to danak / build_and_deploy (push) Has been cancelled

This commit is contained in:
2026-07-04 12:26:30 +03:30
parent 92e97fa9d2
commit 5b6fb16f5c
14 changed files with 435 additions and 70 deletions
@@ -0,0 +1,54 @@
import { Migration } from '@mikro-orm/migrations';
export class Migration20260704140000_addCampaignUserGroupsTable extends Migration {
override async up(): Promise<void> {
this.addSql(`
create table "campaign_user_groups" (
"campaign_id" char(26) not null,
"user_group_id" char(26) not null,
constraint "campaign_user_groups_pkey" primary key ("campaign_id", "user_group_id")
);
`);
this.addSql(`
alter table "campaign_user_groups"
add constraint "campaign_user_groups_campaign_id_foreign"
foreign key ("campaign_id") references "campaigns" ("id") on update cascade on delete cascade;
`);
this.addSql(`
alter table "campaign_user_groups"
add constraint "campaign_user_groups_user_group_id_foreign"
foreign key ("user_group_id") references "user_groups" ("id") on update cascade on delete cascade;
`);
this.addSql(`
insert into "campaign_user_groups" ("campaign_id", "user_group_id")
select c."id", g."id"
from "campaigns" c
cross join lateral jsonb_array_elements_text(c."groups") as gid("id")
inner join "user_groups" g on g."id" = gid."id"
on conflict do nothing;
`);
this.addSql(`alter table "campaigns" drop column "groups";`);
}
override async down(): Promise<void> {
this.addSql(`alter table "campaigns" add column "groups" jsonb not null default '[]';`);
this.addSql(`
update "campaigns" c
set "groups" = coalesce(
(
select jsonb_agg(cug."user_group_id")
from "campaign_user_groups" cug
where cug."campaign_id" = c."id"
),
'[]'::jsonb
);
`);
this.addSql(`drop table if exists "campaign_user_groups" cascade;`);
}
}
@@ -1,4 +1,4 @@
import { Controller, Get, Post, Body, Patch, Param, UseGuards, Delete, Query } from '@nestjs/common';
import { Controller, Get, Post, Body, Patch, Param, UseGuards, Delete, Query, ValidationPipe } from '@nestjs/common';
import { RestaurantsService } from '../providers/restaurants.service';
import { SetupRestaurantDto } from '../dto/setup-restaurant.dto';
import { UpdateRestaurantDto } from '../dto/update-restaurant.dto';
@@ -26,6 +26,7 @@ import { CacheKeyPrefixes } from 'src/common/constants/cache-keys.constant';
import { UpdateRestaurantBgDto } from '../dto/update-restaurant-bg.dto';
import { ChargeRequestDto } from '../dto/charge-request.dto';
import { FinishChargeRequestDto } from '../dto/finish-charge-request.dto';
import { FindRestaurantWalletTransactionsDto } from '../dto/find-restaurant-wallet-transactions.dto';
@ApiTags('restaurants')
@Controller()
@@ -99,6 +100,28 @@ export class RestaurantsController {
return this.restaurantsService.findAllBackgrounds();
}
@UseGuards(AdminAuthGuard)
@ApiBearerAuth()
@Permissions(Permission.UPDATE_RESTAURANT)
@ApiOperation({ summary: 'Get restaurant wallet balance' })
@Get('admin/restaurants/wallet/balance')
getWalletBalance(@RestId() restId: string) {
return this.restaurantsService.getCurrentBalance(restId);
}
@UseGuards(AdminAuthGuard)
@ApiBearerAuth()
@Permissions(Permission.UPDATE_RESTAURANT)
@ApiOperation({ summary: 'Get restaurant wallet transactions with pagination' })
@Get('admin/restaurants/wallet/transactions')
getWalletTransactions(
@RestId() restId: string,
@Query(new ValidationPipe({ transform: true, whitelist: true }))
query: FindRestaurantWalletTransactionsDto,
) {
return this.restaurantsService.getWalletTransactions(restId, query);
}
@UseGuards(AdminAuthGuard)
@ApiBearerAuth()
@Permissions(Permission.UPDATE_RESTAURANT)
@@ -0,0 +1,68 @@
import { IsOptional, IsString, IsNumber, Min, IsIn, IsDateString } from 'class-validator';
import { Type } from 'class-transformer';
import { ApiPropertyOptional } from '@nestjs/swagger';
import {
RestaurantCreditTransactionReason,
RestaurantCreditTransactionType,
} from '../interface/restaurant-credit-transaction.interface';
const sortOrderOptions = ['asc', 'desc'] as const;
type SortOrder = (typeof sortOrderOptions)[number];
export class FindRestaurantWalletTransactionsDto {
@ApiPropertyOptional({ description: 'Page number', type: Number, default: 1, minimum: 1 })
@IsOptional()
@IsNumber()
@Min(1)
@Type(() => Number)
page?: number = 1;
@ApiPropertyOptional({ description: 'Items per page', type: Number, default: 10, minimum: 1 })
@IsOptional()
@IsNumber()
@Min(1)
@Type(() => Number)
limit?: number = 10;
@ApiPropertyOptional({ description: 'Transaction type filter', enum: RestaurantCreditTransactionType })
@IsOptional()
@IsIn(Object.values(RestaurantCreditTransactionType))
type?: RestaurantCreditTransactionType;
@ApiPropertyOptional({ description: 'Transaction reason filter', enum: RestaurantCreditTransactionReason })
@IsOptional()
@IsIn(Object.values(RestaurantCreditTransactionReason))
reason?: RestaurantCreditTransactionReason;
@ApiPropertyOptional({ description: 'Start date filter (ISO 8601 format)', type: String })
@IsOptional()
@IsDateString()
startDate?: string;
@ApiPropertyOptional({ description: 'End date filter (ISO 8601 format)', type: String })
@IsOptional()
@IsDateString()
endDate?: string;
@ApiPropertyOptional({ description: 'Minimum amount filter', type: Number })
@IsOptional()
@IsNumber()
@Type(() => Number)
minAmount?: number;
@ApiPropertyOptional({ description: 'Maximum amount filter', type: Number })
@IsOptional()
@IsNumber()
@Type(() => Number)
maxAmount?: number;
@ApiPropertyOptional({ description: 'Sort field', type: String, default: 'createdAt' })
@IsOptional()
@IsString()
orderBy?: string = 'createdAt';
@ApiPropertyOptional({ description: 'Sort direction', enum: sortOrderOptions, default: 'desc' })
@IsOptional()
@IsIn(sortOrderOptions)
order?: SortOrder = 'desc';
}
@@ -15,6 +15,8 @@ import { RestaurantCreditRequestStatus } from '../interface/restaurant-credit-re
import { CreateExternalInvoice } from '../interface/external-invoice.interface';
import { ChargeRequestDto } from '../dto/charge-request.dto';
import { FinishChargeRequestDto } from '../dto/finish-charge-request.dto';
import { FindRestaurantWalletTransactionsDto } from '../dto/find-restaurant-wallet-transactions.dto';
import { RestaurantCreditTransactionRepository } from '../repositories/restaurant-credit-transaction.repository';
import { RestMessage } from 'src/common/enums/message.enum';
@Injectable()
@@ -25,6 +27,7 @@ export class RestaurantWalletService {
private readonly defaultEm: EntityManager,
private readonly httpService: HttpService,
private readonly configService: ConfigService,
private readonly restaurantCreditTransactionRepository: RestaurantCreditTransactionRepository,
) {}
async getCurrentBalance(restaurantId: string, em: EntityManager = this.defaultEm): Promise<number> {
@@ -37,6 +40,10 @@ export class RestaurantWalletService {
return Number(latestTransaction?.balance ?? 0);
}
getTransactions(restaurantId: string, dto: FindRestaurantWalletTransactionsDto) {
return this.restaurantCreditTransactionRepository.findAllPaginated(restaurantId, dto);
}
async createTransaction(params: {
restaurant: Restaurant;
amount: number;
@@ -35,6 +35,7 @@ import { ActiveRestaurantListItemDto } from '../dto/active-restaurant-list-item.
import { RestaurantWalletService } from './restaurant-wallet.service';
import { ChargeRequestDto } from '../dto/charge-request.dto';
import { FinishChargeRequestDto } from '../dto/finish-charge-request.dto';
import { FindRestaurantWalletTransactionsDto } from '../dto/find-restaurant-wallet-transactions.dto';
@Injectable()
@@ -330,6 +331,15 @@ export class RestaurantsService {
return this.backgroundRepository.findAll();
}
async getCurrentBalance(restId: string): Promise<{ balance: number }> {
const balance = await this.restaurantWalletService.getCurrentBalance(restId);
return { balance };
}
getWalletTransactions(restId: string, dto: FindRestaurantWalletTransactionsDto) {
return this.restaurantWalletService.getTransactions(restId, dto);
}
chargeRequest(restId: string, dto: ChargeRequestDto) {
return this.restaurantWalletService.chargeRequest(restId, dto);
}
@@ -0,0 +1,81 @@
import { Injectable } from '@nestjs/common';
import { FilterQuery } from '@mikro-orm/core';
import { EntityManager, EntityRepository } from '@mikro-orm/postgresql';
import { RestaurantCreditTransaction } from '../entities/restaurant-credit-transaction.entity';
import { FindRestaurantWalletTransactionsDto } from '../dto/find-restaurant-wallet-transactions.dto';
import { PaginatedResult } from 'src/common/interfaces/pagination.interface';
@Injectable()
export class RestaurantCreditTransactionRepository extends EntityRepository<RestaurantCreditTransaction> {
constructor(readonly em: EntityManager) {
super(em, RestaurantCreditTransaction);
}
async findAllPaginated(
restaurantId: string,
dto: FindRestaurantWalletTransactionsDto,
): Promise<PaginatedResult<RestaurantCreditTransaction>> {
const {
page = 1,
limit = 10,
type,
reason,
startDate,
endDate,
minAmount,
maxAmount,
orderBy = 'createdAt',
order = 'desc',
} = dto;
const offset = (page - 1) * limit;
const where: FilterQuery<RestaurantCreditTransaction> = {
restaurant: { id: restaurantId },
};
if (type) {
where.type = type;
}
if (reason) {
where.reason = reason;
}
if (startDate || endDate) {
where.createdAt = {};
if (startDate) {
where.createdAt.$gte = new Date(startDate);
}
if (endDate) {
where.createdAt.$lte = new Date(endDate);
}
}
if (minAmount !== undefined || maxAmount !== undefined) {
where.amount = {};
if (minAmount !== undefined) {
where.amount.$gte = minAmount;
}
if (maxAmount !== undefined) {
where.amount.$lte = maxAmount;
}
}
const [data, total] = await this.findAndCount(where, {
limit,
offset,
orderBy: { [orderBy]: order.toLowerCase() as 'asc' | 'desc' },
});
return {
data,
meta: {
total,
page,
limit,
totalPages: Math.ceil(total / limit),
},
};
}
}
@@ -15,6 +15,7 @@ import { AuthModule } from '../auth/auth.module';
import { UtilsModule } from '../utils/utils.module';
import { Background } from './entities/background.entity';
import { BackgroundRepository } from './repositories/background.repository';
import { RestaurantCreditTransactionRepository } from './repositories/restaurant-credit-transaction.repository';
import { RestaurantCreditTransaction } from './entities/restaurant-credit-transaction.entity';
import { RestaurantCreditRequest } from './entities/restaurant-credit-request.entity';
import { RestaurantWalletService } from './providers/restaurant-wallet.service';
@@ -29,6 +30,7 @@ import { httpConfig } from 'src/config/http.config';
ScheduleService,
RestaurantCrone,
BackgroundRepository,
RestaurantCreditTransactionRepository,
RestaurantWalletService,
],
imports: [
@@ -1,10 +1,11 @@
import { Body, Controller, Delete, Get, Param, Post, UseGuards, ValidationPipe } from '@nestjs/common';
import { ApiBearerAuth, ApiBody, ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger';
import { Body, Controller, Delete, Get, Param, Post, Query, UseGuards, ValidationPipe } from '@nestjs/common';
import { ApiBearerAuth, ApiBody, ApiOperation, ApiParam, ApiQuery, 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 { FindCampaignsDto } from '../dto/find-campaigns.dto';
import { CampaignService } from '../providers/campaign.service';
import { CommonMessage } from 'src/common/enums/message.enum';
@@ -30,9 +31,16 @@ export class CampaignsController {
@ApiBearerAuth()
@Permissions(Permission.MANAGE_USERS)
@Get('admin/campaigns')
@ApiOperation({ summary: 'Get all campaigns' })
findAll(@RestId() restId: string) {
return this.campaignService.findAll(restId);
@ApiOperation({ summary: 'Get paginated list of campaigns' })
@ApiQuery({ name: 'page', required: false, type: Number })
@ApiQuery({ name: 'limit', required: false, type: Number })
@ApiQuery({ name: 'search', required: false, type: String })
@ApiQuery({ name: 'sentType', required: false, enum: ['push', 'sms'] })
@ApiQuery({ name: 'status', required: false, enum: ['sending', 'completed'] })
@ApiQuery({ name: 'orderBy', required: false, type: String })
@ApiQuery({ name: 'order', required: false, enum: ['asc', 'desc'] })
findAll(@Query() dto: FindCampaignsDto, @RestId() restId: string) {
return this.campaignService.findAll(restId, dto);
}
@UseGuards(AdminAuthGuard)
@@ -45,14 +53,5 @@ export class CampaignsController {
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,43 @@
import { IsOptional, IsString, IsNumber, IsEnum } from 'class-validator';
import { ApiPropertyOptional } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { CampaignSentType, CampaignStatus } from '../interface/campaign';
export class FindCampaignsDto {
@IsOptional()
@IsNumber()
@Type(() => Number)
@ApiPropertyOptional({ example: 1, description: 'Page number' })
page?: number;
@IsOptional()
@IsNumber()
@Type(() => Number)
@ApiPropertyOptional({ example: 10, description: 'Items per page' })
limit?: number;
@IsOptional()
@IsString()
@ApiPropertyOptional({ example: 'تخفیف', description: 'Search by title or discount code' })
search?: string;
@IsOptional()
@IsEnum(CampaignSentType)
@ApiPropertyOptional({ enum: CampaignSentType, description: 'Filter by sent type' })
sentType?: CampaignSentType;
@IsOptional()
@IsEnum(CampaignStatus)
@ApiPropertyOptional({ enum: CampaignStatus, description: 'Filter by status' })
status?: CampaignStatus;
@IsOptional()
@IsString()
@ApiPropertyOptional({ example: 'createdAt', description: 'Field to order by' })
orderBy?: string;
@IsOptional()
@IsEnum(['asc', 'desc'])
@ApiPropertyOptional({ enum: ['asc', 'desc'], example: 'desc', description: 'Sort order' })
order?: 'asc' | 'desc';
}
@@ -0,0 +1,12 @@
import { Entity, ManyToOne } from '@mikro-orm/core';
import { Campaign } from './campaign.entity';
import { UserGroup } from './user-group.entity';
@Entity({ tableName: 'campaign_user_groups' })
export class CampaignUserGroup {
@ManyToOne(() => Campaign, { primary: true })
campaign!: Campaign;
@ManyToOne(() => UserGroup, { primary: true })
userGroup!: UserGroup;
}
@@ -1,7 +1,9 @@
import { Entity, Enum, Index, ManyToOne, Property } from '@mikro-orm/core';
import { Collection, Entity, Enum, Index, ManyToMany, ManyToOne, Property } from '@mikro-orm/core';
import { BaseEntity } from '../../../common/entities/base.entity';
import { CampaignSentType, CampaignStatus } from '../interface/campaign';
import { Restaurant } from 'src/modules/restaurants/entities/restaurant.entity';
import { UserGroup } from './user-group.entity';
import { CampaignUserGroup } from './campaign-user-group.entity';
@Entity({ tableName: 'campaigns' })
@Index({ properties: ['restaurant'] })
@@ -12,8 +14,8 @@ export class Campaign extends BaseEntity {
@Property()
title!: string;
@Property({ type: 'json', default: '[]' })
groups: string[] = [];
@ManyToMany({ entity: () => UserGroup, pivotEntity: () => CampaignUserGroup, inversedBy: g => g.campaigns })
groups = new Collection<UserGroup>(this);
@Enum(() => CampaignSentType)
sentType!: CampaignSentType;
@@ -1,7 +1,8 @@
import { Collection, Entity, Index, ManyToOne, OneToMany, Property, Unique } from '@mikro-orm/core';
import { Collection, Entity, Index, ManyToMany, ManyToOne, OneToMany, Property, Unique } from '@mikro-orm/core';
import { BaseEntity } from '../../../common/entities/base.entity';
import { Restaurant } from '../../restaurants/entities/restaurant.entity';
import { UserGroupUser } from './user-group-user.entity';
import { Campaign } from './campaign.entity';
@Entity({ tableName: 'user_groups' })
@Index({ properties: ['restaurant'] })
@@ -18,4 +19,7 @@ export class UserGroup extends BaseEntity {
@OneToMany(() => UserGroupUser, member => member.userGroup)
members = new Collection<UserGroupUser>(this);
@ManyToMany({ entity: () => Campaign, mappedBy: 'groups' })
campaigns = new Collection<Campaign>(this);
}
+56 -49
View File
@@ -3,6 +3,8 @@ 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 { FindCampaignsDto } from '../dto/find-campaigns.dto';
import { PaginatedResult } from 'src/common/interfaces/pagination.interface';
import { RestaurantsService } from 'src/modules/restaurants/providers/restaurants.service';
import { UserGroupRepository } from '../repositories/user-group.repository';
import { UserGroupUserRepository } from '../repositories/user-group-user.repository';
@@ -27,69 +29,79 @@ export class CampaignService {
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)];
const uniqueGroupIds = [...new Set(dto.groups)];
if (uniqueGroups.length === 0) {
if (uniqueGroupIds.length === 0) {
throw new BadRequestException('حداقل یک گروه الزامی است');
}
const validGroupsCount = await this.userGroupRepository.count({
id: { $in: uniqueGroups },
const userGroups = await this.userGroupRepository.find({
id: { $in: uniqueGroupIds },
restaurant: { id: restId },
});
if (validGroupsCount !== uniqueGroups.length) {
if (userGroups.length !== uniqueGroupIds.length) {
throw new BadRequestException('گروه(ها) معتبر نیستند');
}
let smsDispatchPlan: Awaited<ReturnType<typeof this.buildSmsDiscountDispatchPlan>> | undefined;
if (dto.sentType === CampaignSentType.SMS) {
smsDispatchPlan = await this.buildSmsDiscountDispatchPlan(restId, uniqueGroupIds, dto, restaurant.name);
}
const data: RequiredEntityData<Campaign> = {
restaurant,
title: dto.title,
groups: uniqueGroups,
sentType: dto.sentType,
discountCode: dto.discountCode,
status: dto.sentType === CampaignSentType.SMS ? CampaignStatus.SENDING : CampaignStatus.COMPLETED,
totalCount: 0,
totalCost: 0,
status:
dto.sentType === CampaignSentType.SMS
? smsDispatchPlan!.totalCount === 0
? CampaignStatus.COMPLETED
: CampaignStatus.SENDING
: CampaignStatus.COMPLETED,
totalCount: smsDispatchPlan?.totalCount ?? 0,
totalCost: smsDispatchPlan?.totalCost ?? 0,
refunded: 0,
sentCount: 0,
failedCount: 0,
};
const campaign = this.campaignRepository.create(data);
await this.em.persistAndFlush(campaign);
let campaign: Campaign;
if (dto.sentType === CampaignSentType.SMS) {
const smsDispatchPlan = await this.buildSmsDiscountDispatchPlan(
restId,
uniqueGroups,
dto,
restaurant.name,
campaign.id,
);
if (dto.sentType === CampaignSentType.SMS && smsDispatchPlan && smsDispatchPlan.totalCount > 0) {
campaign = await this.em.transactional(async (em) => {
const created = em.create(Campaign, data);
userGroups.forEach(group => created.groups.add(group));
await em.persistAndFlush(created);
campaign.totalCount = smsDispatchPlan.totalCount;
campaign.totalCost = smsDispatchPlan.totalCost;
if (smsDispatchPlan.totalCount === 0) {
campaign.status = CampaignStatus.COMPLETED;
} else {
await this.restaurantWalletService.createTransaction({
restaurant,
amount: smsDispatchPlan.totalCost,
amount: smsDispatchPlan!.totalCost,
type: RestaurantCreditTransactionType.DEBIT,
reason: RestaurantCreditTransactionReason.SMS_SEND,
insufficientBalanceMessage: 'اعتبار کیف پول رستوران برای ارسال کمپین پیامکی کافی نیست',
em,
});
await this.smsQueueService.enqueueBulk(smsDispatchPlan.jobs);
}
return created;
});
const jobs = smsDispatchPlan.jobs.map((job) => ({ ...job, campaignId: campaign.id }));
await this.smsQueueService.enqueueBulk(jobs);
} else {
campaign = this.campaignRepository.create(data);
userGroups.forEach(group => campaign.groups.add(group));
await this.em.persistAndFlush(campaign);
} else if (dto.sentType === CampaignSentType.PUSH) {
await this.runPushDiscountCampaignFlow(restId, uniqueGroups, dto);
if (dto.sentType === CampaignSentType.PUSH) {
await this.runPushDiscountCampaignFlow(restId, uniqueGroupIds, dto);
}
}
return campaign;
@@ -100,7 +112,6 @@ export class CampaignService {
uniqueGroups: string[],
dto: CreateCampaignDto,
restaurantTitle: string,
campaignId: string,
): Promise<{ jobs: DirectSmsQueueJob[]; totalCount: number; totalCost: number }> {
const discountTemplateId = this.configService.get<string>('SMS_PATTERN_DISCOUNT_CODE');
if (!discountTemplateId) {
@@ -138,18 +149,17 @@ export class CampaignService {
}
const jobs: DirectSmsQueueJob[] = [...recipientsByPhone.entries()].map(([phone, firstName]) => ({
phone,
templateId: discountTemplateId,
restaurantId: restId,
quantity: 1,
params: {
name: firstName ?? '',
disc: dto.discountCode,
oca: dto.ocasion,
rest: restaurantTitle,
},
campaignId,
}));
phone,
templateId: discountTemplateId,
restaurantId: restId,
quantity: 1,
params: {
name: firstName ?? '',
disc: dto.discountCode,
oca: dto.ocasion,
rest: restaurantTitle,
},
}));
const totalCount = jobs.length;
const smsUnitPrice = this.getSmsUnitPrice();
@@ -180,18 +190,15 @@ export class CampaignService {
return;
}
async findAll(restId: string): Promise<Campaign[]> {
return this.campaignRepository.find(
{ restaurant: { id: restId } },
{ orderBy: { createdAt: 'desc' } },
);
findAll(restId: string, dto: FindCampaignsDto): Promise<PaginatedResult<Campaign>> {
return this.campaignRepository.findAllPaginated(restId, dto);
}
async findOneOrFail(restId: string, campaignId: string): Promise<Campaign> {
const campaign = await this.campaignRepository.findOne({
id: campaignId,
restaurant: { id: restId },
});
}, { populate: ['groups'] });
if (!campaign) {
throw new NotFoundException('کمپین یافت نشد');
@@ -1,10 +1,63 @@
import { Injectable } from '@nestjs/common';
import { EntityManager, EntityRepository } from '@mikro-orm/postgresql';
import { FilterQuery } from '@mikro-orm/core';
import { Campaign } from '../entities/campaign.entity';
import { CampaignSentType, CampaignStatus } from '../interface/campaign';
import { PaginatedResult } from 'src/common/interfaces/pagination.interface';
type FindCampaignsOpts = {
page?: number;
limit?: number;
search?: string;
orderBy?: string;
order?: 'asc' | 'desc';
sentType?: CampaignSentType;
status?: CampaignStatus;
};
@Injectable()
export class CampaignRepository extends EntityRepository<Campaign> {
constructor(readonly em: EntityManager) {
super(em, Campaign);
}
async findAllPaginated(restId: string, opts: FindCampaignsOpts = {}): Promise<PaginatedResult<Campaign>> {
const { page = 1, limit = 10, search, orderBy = 'createdAt', order = 'desc', sentType, status } = opts;
const offset = (page - 1) * limit;
const where: FilterQuery<Campaign> = { restaurant: { id: restId } };
if (sentType) {
where.sentType = sentType;
}
if (status) {
where.status = status;
}
if (search) {
const pattern = `%${search}%`;
where.$or = [{ title: { $ilike: pattern } }, { discountCode: { $ilike: pattern } }];
}
const [data, total] = await this.findAndCount(where, {
limit,
offset,
populate: ['groups'],
orderBy: { [orderBy]: order.toLowerCase() as 'asc' | 'desc' },
});
const totalPages = Math.ceil(total / limit);
return {
data,
meta: {
total,
page,
limit,
totalPages,
},
};
}
}