feat(platform): text admins when an account opens or a subscription ends
Signup and admin-created accounts notify platform managers, and an ended plan sends one SMS instead of repeating for old expirations. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<boolean> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user