196 lines
5.6 KiB
TypeScript
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');
|
|
}
|
|
}
|