Files
grow-api/src/notifications/salon-notifications.service.ts
T
2026-07-09 20:58:19 +03:30

196 lines
5.6 KiB
TypeScript

import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CampaignType } from '../campaigns/enums/campaign-type.enum';
import { getCampaignTypeDefinition } from '../campaigns/constants/campaign-types.constants';
import { Reserve } from '../reserves/entities/reserve.entity';
import { Salon } from '../salons/entities/salon.entity';
import { buildReserveSmsVariables } from '../sms-settings/utils/build-reserve-sms-variables.util';
import { NotificationScenarioKey } from './enums/notification-scenario-key.enum';
import { NotificationDispatchService } from './notification-dispatch.service';
@Injectable()
export class SalonNotificationsService {
private readonly logger = new Logger(SalonNotificationsService.name);
constructor(
private readonly notificationDispatchService: NotificationDispatchService,
@InjectRepository(Salon)
private readonly salonsRepository: Repository<Salon>,
@InjectRepository(Reserve)
private readonly reservesRepository: Repository<Reserve>,
) {}
async notifyReserveCreated(reserveId: string): Promise<void> {
await this.notifyForReserve(
reserveId,
NotificationScenarioKey.RESERVE_CREATED,
`/work-schedule`,
);
}
async notifyReserveConfirmed(reserveId: string): Promise<void> {
await this.notifyForReserve(
reserveId,
NotificationScenarioKey.RESERVE_CONFIRMED,
`/work-schedule`,
);
}
async notifyReserveCancelled(reserveId: string): Promise<void> {
await this.notifyForReserve(
reserveId,
NotificationScenarioKey.RESERVE_CANCELLED,
`/work-schedule`,
);
}
async notifyReserveRemindersSent(params: {
salonId: string;
sentCount: number;
appointmentDate: string;
}): Promise<void> {
if (params.sentCount <= 0) {
return;
}
const salon = await this.salonsRepository.findOne({
where: { id: params.salonId },
});
if (!salon) {
return;
}
const appointmentDate = this.formatDate(params.appointmentDate);
await this.safeCreate({
salonId: salon.id,
userId: salon.ownerId,
key: NotificationScenarioKey.RESERVE_REMINDER_SENT,
variables: {
salonName: salon.name,
sentCount: params.sentCount.toLocaleString('fa-IR'),
appointmentDate,
},
link: '/work-schedule',
relatedEntityType: 'reserve_reminder',
relatedEntityId: null,
});
}
async notifyCampaignSent(params: {
salon: Salon;
type: CampaignType;
campaignRunId: string;
sentCount: number;
failedCount: number;
targetCount: number;
estimatedRevenue: number;
}): Promise<void> {
const definition = getCampaignTypeDefinition(params.type);
const key =
params.sentCount === 0
? NotificationScenarioKey.CAMPAIGN_FAILED
: NotificationScenarioKey.CAMPAIGN_SENT;
await this.safeCreate({
salonId: params.salon.id,
userId: params.salon.ownerId,
key,
variables: {
salonName: params.salon.name,
campaignLabel: definition.label,
campaignType: params.type,
sentCount: params.sentCount.toLocaleString('fa-IR'),
failedCount: params.failedCount.toLocaleString('fa-IR'),
targetCount: params.targetCount.toLocaleString('fa-IR'),
estimatedRevenue: params.estimatedRevenue.toLocaleString('fa-IR'),
},
link: `/campaigns/results/${params.campaignRunId}`,
relatedEntityType: 'campaign_run',
relatedEntityId: params.campaignRunId,
});
}
async notifyCampaignFailed(params: {
salon: Salon;
type: CampaignType;
targetCount: number;
}): Promise<void> {
const definition = getCampaignTypeDefinition(params.type);
await this.safeCreate({
salonId: params.salon.id,
userId: params.salon.ownerId,
key: NotificationScenarioKey.CAMPAIGN_FAILED,
variables: {
salonName: params.salon.name,
campaignLabel: definition.label,
campaignType: params.type,
targetCount: params.targetCount.toLocaleString('fa-IR'),
},
link: '/campaigns',
relatedEntityType: 'campaign',
relatedEntityId: null,
});
}
private async notifyForReserve(
reserveId: string,
key: NotificationScenarioKey,
link: string,
): Promise<void> {
const reserve = await this.reservesRepository.findOne({
where: { id: reserveId },
relations: {
salon: true,
customer: { user: true },
stylist: { user: true },
},
});
if (!reserve?.salon) {
return;
}
const variables = buildReserveSmsVariables(reserve);
await this.safeCreate({
salonId: reserve.salonId,
userId: reserve.salon.ownerId,
key,
variables,
link,
relatedEntityType: 'reserve',
relatedEntityId: reserve.id,
});
}
private async safeCreate(params: {
salonId: string;
userId: string;
key: NotificationScenarioKey;
variables: Record<string, string>;
link?: string | null;
relatedEntityType?: string | null;
relatedEntityId?: string | null;
}): Promise<void> {
try {
await this.notificationDispatchService.createForSalon(params);
} catch (error) {
this.logger.error(
`Failed to create notification ${params.key} for salon ${params.salonId}`,
error instanceof Error ? error.stack : undefined,
);
}
}
private formatDate(date: string): string {
const parsed = new Date(`${date}T00:00:00`);
if (Number.isNaN(parsed.getTime())) {
return date;
}
return parsed.toLocaleDateString('fa-IR');
}
}