import { Injectable, Logger } from '@nestjs/common'; import { isPermanentBillingPlan } from '@mento/shared'; import { SmsGatewayService } from '../sms-billing/sms-gateway.service'; import { PrismaService } from '../prisma/prisma.service'; import { newAccountSms, subscriptionEndedSms, type NewAccountSource, } from './platform-admin-alert'; @Injectable() export class PlatformAdminAlertService { private readonly logger = new Logger(PlatformAdminAlertService.name); constructor( private readonly prisma: PrismaService, private readonly sms: SmsGatewayService, ) {} /** ساخت حساب جدید؛ خطای پیامک جلوی ثبت‌نام یا ساخت محیط را نمی‌گیرد. */ async notifyNewAccount(input: { source: NewAccountSource; organizationName: string; ownerName: string; mobile: string | null; }): Promise { try { const delivered = await this.sendToAdmins(newAccountSms(input)); if (!delivered) { this.logger.warn(`new account SMS was not delivered (${input.source})`); } } catch (error) { this.logger.warn( `new account SMS failed: ${error instanceof Error ? error.message : 'unknown'}`, ); } } /** * یک‌بار هنگام تمام شدن اشتراک. اگر ارسال نشود پرچم خالی می‌ماند تا بعداً دوباره تلاش شود. */ async notifySubscriptionEnded(billingAccountId: string): Promise { const row = await this.prisma.billingAccount.findUnique({ where: { id: billingAccountId }, select: { id: true, planCode: true, status: true, expiresAt: true, expiryNotifiedAt: true, owner: { select: { fullName: true, mobile: true, memberships: { where: { role: 'owner', isActive: true }, select: { organization: { select: { name: true } } }, }, }, }, }, }); if (!row || row.expiryNotifiedAt) return; if (row.status === 'suspended') return; if (isPermanentBillingPlan(row.planCode)) { await this.prisma.billingAccount.update({ where: { id: row.id }, data: { expiryNotifiedAt: new Date() }, }); return; } if (row.expiresAt.getTime() > Date.now() && row.status !== 'expired') return; const claimed = await this.prisma.billingAccount.updateMany({ where: { id: row.id, expiryNotifiedAt: null }, data: { status: 'expired', expiryNotifiedAt: new Date() }, }); if (claimed.count !== 1) return; try { const delivered = await this.sendToAdmins( subscriptionEndedSms({ organizationNames: row.owner.memberships.map((item) => item.organization.name), ownerName: row.owner.fullName, mobile: row.owner.mobile, planCode: row.planCode, expiresAt: row.expiresAt, }), ); if (!delivered) { await this.prisma.billingAccount.update({ where: { id: row.id }, data: { expiryNotifiedAt: null }, }); } } catch (error) { this.logger.warn( `subscription SMS failed ${row.id}: ${error instanceof Error ? error.message : 'unknown'}`, ); await this.prisma.billingAccount .update({ where: { id: row.id }, data: { expiryNotifiedAt: null }, }) .catch(() => undefined); } } /** true یعنی حداقل یک پیامک رفت، یا مدیری با موبایل فعال وجود نداشت. */ private async sendToAdmins(text: string): Promise { const admins = await this.prisma.platformAdmin.findMany({ where: { isActive: true, mobile: { not: null } }, select: { mobile: true }, }); const mobiles = [ ...new Set( admins .map((admin) => admin.mobile?.trim() ?? '') .filter((mobile) => mobile.length > 0), ), ]; if (!mobiles.length) { this.logger.warn('platform admin SMS skipped: no active admin mobile'); return true; } let delivered = false; for (const mobile of mobiles) { try { const result = await this.sms.send({ phone: mobile, text, source: 'platform-admin', }); if (result.ok) delivered = true; else { this.logger.warn( `platform admin SMS failed to=…${mobile.slice(-4)} reason=${result.reason ?? 'unknown'}`, ); } } catch (error) { this.logger.warn( `platform admin SMS failed to=…${mobile.slice(-4)}: ${ error instanceof Error ? error.message : 'unknown' }`, ); } } return delivered; } }