|
|
|
@@ -21,7 +21,20 @@ import {
|
|
|
|
|
iranYmd,
|
|
|
|
|
} from '../common/iran-time.util';
|
|
|
|
|
|
|
|
|
|
export type ExpiryLevel = 'dedicated' | 'healthy' | 'warning' | 'critical' | 'unknown';
|
|
|
|
|
export type ExpiryLevel =
|
|
|
|
|
| 'dedicated'
|
|
|
|
|
| 'healthy'
|
|
|
|
|
| 'warning'
|
|
|
|
|
| 'critical'
|
|
|
|
|
| 'unknown';
|
|
|
|
|
|
|
|
|
|
const EXPIRY_RANK: Record<ExpiryLevel, number> = {
|
|
|
|
|
critical: 0,
|
|
|
|
|
warning: 1,
|
|
|
|
|
unknown: 2,
|
|
|
|
|
healthy: 3,
|
|
|
|
|
dedicated: 4,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
@Injectable()
|
|
|
|
|
export class ServersService implements OnModuleInit, OnModuleDestroy {
|
|
|
|
@@ -35,7 +48,6 @@ export class ServersService implements OnModuleInit, OnModuleDestroy {
|
|
|
|
|
) {}
|
|
|
|
|
|
|
|
|
|
onModuleInit() {
|
|
|
|
|
// Check once shortly after boot, then every 6 hours
|
|
|
|
|
setTimeout(() => void this.runExpiryAlerts(), 15_000);
|
|
|
|
|
this.alertTimer = setInterval(
|
|
|
|
|
() => void this.runExpiryAlerts(),
|
|
|
|
@@ -48,18 +60,21 @@ export class ServersService implements OnModuleInit, OnModuleDestroy {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async listTenants() {
|
|
|
|
|
await this.ensureAccountsForAllTenants();
|
|
|
|
|
void this.runExpiryAlerts();
|
|
|
|
|
|
|
|
|
|
const rows = await this.prisma.tenant.findMany({
|
|
|
|
|
where: { isActive: true },
|
|
|
|
|
orderBy: [{ product: 'asc' }, { nameFa: 'asc' }],
|
|
|
|
|
include: { serverAccount: true },
|
|
|
|
|
include: {
|
|
|
|
|
serverAccounts: { orderBy: { createdAt: 'asc' } },
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return rows.map((tenant) => {
|
|
|
|
|
const account = tenant.serverAccount!;
|
|
|
|
|
const summary = this.toSummary(account);
|
|
|
|
|
const servers = tenant.serverAccounts.map((account) =>
|
|
|
|
|
this.toSummary(account),
|
|
|
|
|
);
|
|
|
|
|
const worst = this.pickWorstSummary(servers);
|
|
|
|
|
return {
|
|
|
|
|
id: tenant.id,
|
|
|
|
|
product: tenant.product,
|
|
|
|
@@ -69,15 +84,35 @@ export class ServersService implements OnModuleInit, OnModuleDestroy {
|
|
|
|
|
domain: tenant.domain,
|
|
|
|
|
avatarUrl: tenant.avatarUrl,
|
|
|
|
|
isActive: tenant.isActive,
|
|
|
|
|
server: summary,
|
|
|
|
|
serversCount: servers.length,
|
|
|
|
|
servers,
|
|
|
|
|
/** Worst status across servers — used for card color on list/home */
|
|
|
|
|
server: worst,
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async getExpiringSoon(limit = 5) {
|
|
|
|
|
const rows = await this.listTenants();
|
|
|
|
|
return rows
|
|
|
|
|
.filter((r) => !r.server.isDedicated && r.server.expiresAt)
|
|
|
|
|
const flat = [];
|
|
|
|
|
|
|
|
|
|
for (const tenant of rows) {
|
|
|
|
|
for (const accountSummary of tenant.servers) {
|
|
|
|
|
if (accountSummary.isDedicated) continue;
|
|
|
|
|
flat.push({
|
|
|
|
|
id: tenant.id,
|
|
|
|
|
product: tenant.product,
|
|
|
|
|
slug: tenant.slug,
|
|
|
|
|
nameFa: tenant.nameFa,
|
|
|
|
|
domain: tenant.domain,
|
|
|
|
|
serverAccountId: accountSummary.id,
|
|
|
|
|
server: accountSummary,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return flat
|
|
|
|
|
.filter((r) => r.server.expiresAt || r.server.expiryLevel === 'unknown')
|
|
|
|
|
.sort((a, b) => {
|
|
|
|
|
const ae = a.server.expiresAt ? Date.parse(a.server.expiresAt) : Infinity;
|
|
|
|
|
const be = b.server.expiresAt ? Date.parse(b.server.expiresAt) : Infinity;
|
|
|
|
@@ -90,9 +125,42 @@ export class ServersService implements OnModuleInit, OnModuleDestroy {
|
|
|
|
|
.slice(0, limit);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async getAccount(tenantId: string) {
|
|
|
|
|
async getTenantServers(tenantId: string) {
|
|
|
|
|
const tenant = await this.requireTenant(tenantId);
|
|
|
|
|
const account = await this.ensureAccount(tenant.id);
|
|
|
|
|
const accounts = await this.prisma.serverAccount.findMany({
|
|
|
|
|
where: { tenantId: tenant.id },
|
|
|
|
|
orderBy: { createdAt: 'asc' },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
tenant: {
|
|
|
|
|
id: tenant.id,
|
|
|
|
|
product: tenant.product,
|
|
|
|
|
slug: tenant.slug,
|
|
|
|
|
nameFa: tenant.nameFa,
|
|
|
|
|
nameEn: tenant.nameEn,
|
|
|
|
|
domain: tenant.domain,
|
|
|
|
|
avatarUrl: tenant.avatarUrl,
|
|
|
|
|
},
|
|
|
|
|
servers: accounts.map((account) => ({
|
|
|
|
|
account: this.publicAccount(account),
|
|
|
|
|
summary: this.toSummary(account),
|
|
|
|
|
})),
|
|
|
|
|
planOptions: PLAN_MONTHS.map((m) => ({
|
|
|
|
|
months: m,
|
|
|
|
|
label: this.planLabel(m),
|
|
|
|
|
})),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** @deprecated alias kept for old clients — same as getTenantServers */
|
|
|
|
|
async getAccount(tenantId: string) {
|
|
|
|
|
return this.getTenantServers(tenantId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async getServerAccount(accountId: string) {
|
|
|
|
|
const account = await this.requireAccount(accountId);
|
|
|
|
|
const tenant = await this.requireTenant(account.tenantId);
|
|
|
|
|
const charges = await this.prisma.serverCharge.findMany({
|
|
|
|
|
where: { serverAccountId: account.id },
|
|
|
|
|
orderBy: { createdAt: 'desc' },
|
|
|
|
@@ -119,12 +187,48 @@ export class ServersService implements OnModuleInit, OnModuleDestroy {
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async updateAccount(
|
|
|
|
|
async createServerAccount(
|
|
|
|
|
tenantId: string,
|
|
|
|
|
dto: UpdateServerAccountDto,
|
|
|
|
|
userId?: string,
|
|
|
|
|
) {
|
|
|
|
|
const account = await this.ensureAccount(tenantId);
|
|
|
|
|
const tenant = await this.requireTenant(tenantId);
|
|
|
|
|
const created = await this.prisma.serverAccount.create({
|
|
|
|
|
data: {
|
|
|
|
|
tenantId: tenant.id,
|
|
|
|
|
isDedicated: dto.isDedicated ?? false,
|
|
|
|
|
serverName: dto.serverName?.trim() || null,
|
|
|
|
|
specs: dto.specs?.trim() || null,
|
|
|
|
|
description: dto.description?.trim() || null,
|
|
|
|
|
priceRials: dto.priceRials ?? null,
|
|
|
|
|
alertDaysBefore: dto.alertDaysBefore ?? 3,
|
|
|
|
|
planMonths: dto.planMonths ?? null,
|
|
|
|
|
chargedAt: dto.chargedAt ? new Date(dto.chargedAt) : null,
|
|
|
|
|
expiresAt:
|
|
|
|
|
dto.isDedicated !== true && dto.chargedAt && dto.planMonths
|
|
|
|
|
? this.addMonths(new Date(dto.chargedAt), dto.planMonths)
|
|
|
|
|
: null,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await this.audit.log({
|
|
|
|
|
action: 'server.account.create',
|
|
|
|
|
entityType: 'server_account',
|
|
|
|
|
entityId: created.id,
|
|
|
|
|
tenantId,
|
|
|
|
|
userId,
|
|
|
|
|
after: this.publicAccount(created) as unknown as Prisma.InputJsonValue,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return this.getServerAccount(created.id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async updateServerAccount(
|
|
|
|
|
accountId: string,
|
|
|
|
|
dto: UpdateServerAccountDto,
|
|
|
|
|
userId?: string,
|
|
|
|
|
) {
|
|
|
|
|
const account = await this.requireAccount(accountId);
|
|
|
|
|
const before = { ...account };
|
|
|
|
|
|
|
|
|
|
if (dto.planMonths != null && !PLAN_MONTHS.includes(dto.planMonths as any)) {
|
|
|
|
@@ -135,11 +239,6 @@ export class ServersService implements OnModuleInit, OnModuleDestroy {
|
|
|
|
|
let chargedAt = account.chargedAt;
|
|
|
|
|
let planMonths = account.planMonths;
|
|
|
|
|
|
|
|
|
|
if (dto.isDedicated === true) {
|
|
|
|
|
// Dedicated: keep history but stop managing/alerting on current period fields optionally
|
|
|
|
|
// We keep expiresAt as-is for record, but alerts skip dedicated.
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (dto.chargedAt !== undefined) {
|
|
|
|
|
chargedAt = dto.chargedAt ? new Date(dto.chargedAt) : null;
|
|
|
|
|
}
|
|
|
|
@@ -147,7 +246,6 @@ export class ServersService implements OnModuleInit, OnModuleDestroy {
|
|
|
|
|
planMonths = dto.planMonths;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Recalc expiry when charge date or plan changes (and not dedicated-only clear)
|
|
|
|
|
if (
|
|
|
|
|
dto.isDedicated !== true &&
|
|
|
|
|
(dto.chargedAt !== undefined || dto.planMonths !== undefined) &&
|
|
|
|
@@ -157,10 +255,6 @@ export class ServersService implements OnModuleInit, OnModuleDestroy {
|
|
|
|
|
expiresAt = this.addMonths(chargedAt, planMonths);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (dto.isDedicated === true) {
|
|
|
|
|
// Clearing managed alert state when switching to dedicated
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const updated = await this.prisma.serverAccount.update({
|
|
|
|
|
where: { id: account.id },
|
|
|
|
|
data: {
|
|
|
|
@@ -187,13 +281,10 @@ export class ServersService implements OnModuleInit, OnModuleDestroy {
|
|
|
|
|
? undefined
|
|
|
|
|
: dto.description?.trim() || null,
|
|
|
|
|
alertDaysBefore: dto.alertDaysBefore ?? undefined,
|
|
|
|
|
...(dto.isDedicated === true
|
|
|
|
|
? { lastAlertDate: null }
|
|
|
|
|
: {}),
|
|
|
|
|
...(dto.isDedicated === true ? { lastAlertDate: null } : {}),
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// If we toggled off dedicated and have charge+plan, ensure expiresAt
|
|
|
|
|
if (
|
|
|
|
|
updated.isDedicated === false &&
|
|
|
|
|
updated.chargedAt &&
|
|
|
|
@@ -213,25 +304,62 @@ export class ServersService implements OnModuleInit, OnModuleDestroy {
|
|
|
|
|
action: 'server.account.update',
|
|
|
|
|
entityType: 'server_account',
|
|
|
|
|
entityId: updated.id,
|
|
|
|
|
tenantId,
|
|
|
|
|
tenantId: updated.tenantId,
|
|
|
|
|
userId,
|
|
|
|
|
before: this.publicAccount(before) as unknown as Prisma.InputJsonValue,
|
|
|
|
|
after: this.publicAccount(updated) as unknown as Prisma.InputJsonValue,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const tenant = await this.requireTenant(tenantId);
|
|
|
|
|
const tenant = await this.requireTenant(updated.tenantId);
|
|
|
|
|
if (before.isDedicated !== updated.isDedicated) {
|
|
|
|
|
const label = this.serverLabel(updated);
|
|
|
|
|
this.notify.notifyAdmins(
|
|
|
|
|
updated.isDedicated
|
|
|
|
|
? `سوپرادمین: سرور «${tenant.nameFa}» روی حالت اختصاصی قرار گرفت.`
|
|
|
|
|
: `سوپرادمین: سرور «${tenant.nameFa}» از حالت اختصاصی خارج شد (مدیریت با ما).`,
|
|
|
|
|
? `سوپرادمین: سرور «${tenant.nameFa}» / ${label} روی حالت اختصاصی قرار گرفت.`
|
|
|
|
|
: `سوپرادمین: سرور «${tenant.nameFa}» / ${label} از حالت اختصاصی خارج شد (مدیریت با ما).`,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return this.getAccount(tenantId);
|
|
|
|
|
return this.getServerAccount(updated.id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async renew(tenantId: string, dto: RenewServerDto, userId?: string) {
|
|
|
|
|
/** Legacy: update first/only account by tenant id */
|
|
|
|
|
async updateAccount(
|
|
|
|
|
tenantId: string,
|
|
|
|
|
dto: UpdateServerAccountDto,
|
|
|
|
|
userId?: string,
|
|
|
|
|
) {
|
|
|
|
|
const accounts = await this.prisma.serverAccount.findMany({
|
|
|
|
|
where: { tenantId },
|
|
|
|
|
orderBy: { createdAt: 'asc' },
|
|
|
|
|
take: 1,
|
|
|
|
|
});
|
|
|
|
|
if (!accounts.length) {
|
|
|
|
|
return this.createServerAccount(tenantId, dto, userId);
|
|
|
|
|
}
|
|
|
|
|
return this.updateServerAccount(accounts[0].id, dto, userId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async deleteServerAccount(accountId: string, userId?: string) {
|
|
|
|
|
const account = await this.requireAccount(accountId);
|
|
|
|
|
const tenantId = account.tenantId;
|
|
|
|
|
const before = this.publicAccount(account);
|
|
|
|
|
|
|
|
|
|
await this.prisma.serverAccount.delete({ where: { id: account.id } });
|
|
|
|
|
|
|
|
|
|
await this.audit.log({
|
|
|
|
|
action: 'server.account.delete',
|
|
|
|
|
entityType: 'server_account',
|
|
|
|
|
entityId: accountId,
|
|
|
|
|
tenantId,
|
|
|
|
|
userId,
|
|
|
|
|
before: before as unknown as Prisma.InputJsonValue,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return this.getTenantServers(tenantId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async renewAccount(accountId: string, dto: RenewServerDto, userId?: string) {
|
|
|
|
|
if (!PLAN_MONTHS.includes(dto.planMonths as any)) {
|
|
|
|
|
throw new BadRequestException('مدت شارژ نامعتبر است');
|
|
|
|
|
}
|
|
|
|
@@ -239,12 +367,12 @@ export class ServersService implements OnModuleInit, OnModuleDestroy {
|
|
|
|
|
throw new BadRequestException('قیمت نامعتبر است');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const tenant = await this.requireTenant(tenantId);
|
|
|
|
|
const account = await this.ensureAccount(tenantId);
|
|
|
|
|
const account = await this.requireAccount(accountId);
|
|
|
|
|
const tenant = await this.requireTenant(account.tenantId);
|
|
|
|
|
|
|
|
|
|
if (account.isDedicated) {
|
|
|
|
|
throw new BadRequestException(
|
|
|
|
|
'این نسخه سرور اختصاصی است؛ شارژ مدیریتشده لازم نیست. ابتدا سوئیچ اختصاصی را خاموش کنید.',
|
|
|
|
|
'این سرور اختصاصی است؛ شارژ مدیریتشده لازم نیست. ابتدا سوئیچ اختصاصی را خاموش کنید.',
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -254,7 +382,7 @@ export class ServersService implements OnModuleInit, OnModuleDestroy {
|
|
|
|
|
}
|
|
|
|
|
const expiresAt = this.addMonths(chargedAt, dto.planMonths);
|
|
|
|
|
|
|
|
|
|
const [updated] = await this.prisma.$transaction([
|
|
|
|
|
await this.prisma.$transaction([
|
|
|
|
|
this.prisma.serverAccount.update({
|
|
|
|
|
where: { id: account.id },
|
|
|
|
|
data: {
|
|
|
|
@@ -282,7 +410,7 @@ export class ServersService implements OnModuleInit, OnModuleDestroy {
|
|
|
|
|
action: 'server.renew',
|
|
|
|
|
entityType: 'server_account',
|
|
|
|
|
entityId: account.id,
|
|
|
|
|
tenantId,
|
|
|
|
|
tenantId: tenant.id,
|
|
|
|
|
userId,
|
|
|
|
|
after: {
|
|
|
|
|
chargedAt: chargedAt.toISOString(),
|
|
|
|
@@ -293,11 +421,25 @@ export class ServersService implements OnModuleInit, OnModuleDestroy {
|
|
|
|
|
} as unknown as Prisma.InputJsonValue,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const label = this.serverLabel(account);
|
|
|
|
|
this.notify.notifyAdmins(
|
|
|
|
|
`سوپرادمین: سرور «${tenant.nameFa}» شارژ/تمدید شد (${this.planLabel(dto.planMonths)}، تا ${formatIranDate(expiresAt)}).`,
|
|
|
|
|
`سوپرادمین: سرور «${tenant.nameFa}» / ${label} شارژ/تمدید شد (${this.planLabel(dto.planMonths)}، تا ${formatIranDate(expiresAt)}).`,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return this.getAccount(tenantId);
|
|
|
|
|
return this.getServerAccount(account.id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Legacy renew by tenant (first account) */
|
|
|
|
|
async renew(tenantId: string, dto: RenewServerDto, userId?: string) {
|
|
|
|
|
const accounts = await this.prisma.serverAccount.findMany({
|
|
|
|
|
where: { tenantId },
|
|
|
|
|
orderBy: { createdAt: 'asc' },
|
|
|
|
|
take: 1,
|
|
|
|
|
});
|
|
|
|
|
if (!accounts.length) {
|
|
|
|
|
throw new BadRequestException('ابتدا یک سرور برای این نسخه ثبت کنید');
|
|
|
|
|
}
|
|
|
|
|
return this.renewAccount(accounts[0].id, dto, userId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async runExpiryAlerts() {
|
|
|
|
@@ -337,8 +479,9 @@ export class ServersService implements OnModuleInit, OnModuleDestroy {
|
|
|
|
|
? 'امروز منقضی میشود'
|
|
|
|
|
: `${daysLeft} روز تا پایان اعتبار`;
|
|
|
|
|
|
|
|
|
|
const label = this.serverLabel(account);
|
|
|
|
|
this.notify.notifyAdmins(
|
|
|
|
|
`هشدار سرور: «${account.tenant.nameFa}» (${account.tenant.domain}) — ${when} (انقضا: ${formatIranDate(account.expiresAt)}). لطفاً شارژ/تمدید کنید.`,
|
|
|
|
|
`هشدار سرور: «${account.tenant.nameFa}» / ${label} (${account.tenant.domain}) — ${when} (انقضا: ${formatIranDate(account.expiresAt)}). لطفاً شارژ/تمدید کنید.`,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
await this.prisma.serverAccount.update({
|
|
|
|
@@ -356,40 +499,86 @@ export class ServersService implements OnModuleInit, OnModuleDestroy {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private toSummary(account: ServerAccount) {
|
|
|
|
|
if (account.isDedicated) {
|
|
|
|
|
private pickWorstSummary(
|
|
|
|
|
servers: Array<ReturnType<ServersService['toSummary']>>,
|
|
|
|
|
) {
|
|
|
|
|
if (!servers.length) {
|
|
|
|
|
return {
|
|
|
|
|
isDedicated: true,
|
|
|
|
|
id: null as string | null,
|
|
|
|
|
isDedicated: false,
|
|
|
|
|
serverName: null as string | null,
|
|
|
|
|
specs: null as string | null,
|
|
|
|
|
priceRials: null as number | null,
|
|
|
|
|
chargedAt: null as string | null,
|
|
|
|
|
planMonths: null as number | null,
|
|
|
|
|
expiresAt: null as string | null,
|
|
|
|
|
daysLeft: null as number | null,
|
|
|
|
|
expiryLevel: 'unknown' as ExpiryLevel,
|
|
|
|
|
expiryLabel: 'بدون سرور',
|
|
|
|
|
planLabel: null as string | null,
|
|
|
|
|
serversCount: 0,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let worst = servers[0];
|
|
|
|
|
for (const s of servers.slice(1)) {
|
|
|
|
|
if (EXPIRY_RANK[s.expiryLevel] < EXPIRY_RANK[worst.expiryLevel]) {
|
|
|
|
|
worst = s;
|
|
|
|
|
} else if (
|
|
|
|
|
EXPIRY_RANK[s.expiryLevel] === EXPIRY_RANK[worst.expiryLevel] &&
|
|
|
|
|
s.expiresAt &&
|
|
|
|
|
worst.expiresAt &&
|
|
|
|
|
Date.parse(s.expiresAt) < Date.parse(worst.expiresAt)
|
|
|
|
|
) {
|
|
|
|
|
worst = s;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const names = servers
|
|
|
|
|
.map((s) => s.serverName)
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
.slice(0, 2) as string[];
|
|
|
|
|
const nameHint = names.length ? ` · ${names.join('، ')}` : '';
|
|
|
|
|
const countHint =
|
|
|
|
|
servers.length > 1 ? ` · ${servers.length} سرور${nameHint}` : '';
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...worst,
|
|
|
|
|
expiryLabel: `${worst.expiryLabel}${countHint}`,
|
|
|
|
|
serversCount: servers.length,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private toSummary(account: ServerAccount) {
|
|
|
|
|
const base = {
|
|
|
|
|
id: account.id,
|
|
|
|
|
isDedicated: account.isDedicated,
|
|
|
|
|
serverName: account.serverName,
|
|
|
|
|
specs: account.specs,
|
|
|
|
|
priceRials: account.priceRials,
|
|
|
|
|
chargedAt: account.chargedAt?.toISOString() ?? null,
|
|
|
|
|
planMonths: account.planMonths,
|
|
|
|
|
expiresAt: account.expiresAt?.toISOString() ?? null,
|
|
|
|
|
daysLeft: null as number | null,
|
|
|
|
|
expiryLevel: 'dedicated' as ExpiryLevel,
|
|
|
|
|
expiryLabel: 'اختصاصی',
|
|
|
|
|
planLabel: account.planMonths
|
|
|
|
|
? this.planLabel(account.planMonths)
|
|
|
|
|
: null,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (account.isDedicated) {
|
|
|
|
|
return {
|
|
|
|
|
...base,
|
|
|
|
|
daysLeft: null as number | null,
|
|
|
|
|
expiryLevel: 'dedicated' as ExpiryLevel,
|
|
|
|
|
expiryLabel: 'اختصاصی',
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!account.expiresAt) {
|
|
|
|
|
return {
|
|
|
|
|
isDedicated: false,
|
|
|
|
|
serverName: account.serverName,
|
|
|
|
|
specs: account.specs,
|
|
|
|
|
priceRials: account.priceRials,
|
|
|
|
|
chargedAt: account.chargedAt?.toISOString() ?? null,
|
|
|
|
|
planMonths: account.planMonths,
|
|
|
|
|
expiresAt: null,
|
|
|
|
|
...base,
|
|
|
|
|
daysLeft: null,
|
|
|
|
|
expiryLevel: 'unknown' as ExpiryLevel,
|
|
|
|
|
expiryLabel: 'بدون تاریخ اعتبار',
|
|
|
|
|
planLabel: account.planMonths
|
|
|
|
|
? this.planLabel(account.planMonths)
|
|
|
|
|
: null,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -411,17 +600,10 @@ export class ServersService implements OnModuleInit, OnModuleDestroy {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
isDedicated: false,
|
|
|
|
|
serverName: account.serverName,
|
|
|
|
|
specs: account.specs,
|
|
|
|
|
priceRials: account.priceRials,
|
|
|
|
|
chargedAt: account.chargedAt?.toISOString() ?? null,
|
|
|
|
|
planMonths: account.planMonths,
|
|
|
|
|
expiresAt: account.expiresAt.toISOString(),
|
|
|
|
|
...base,
|
|
|
|
|
daysLeft,
|
|
|
|
|
expiryLevel,
|
|
|
|
|
expiryLabel,
|
|
|
|
|
planLabel: account.planMonths ? this.planLabel(account.planMonths) : null,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -443,6 +625,10 @@ export class ServersService implements OnModuleInit, OnModuleDestroy {
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private serverLabel(account: Pick<ServerAccount, 'serverName' | 'id'>) {
|
|
|
|
|
return account.serverName?.trim() || `سرور ${account.id.slice(-6)}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private planLabel(months: number) {
|
|
|
|
|
switch (months) {
|
|
|
|
|
case 1:
|
|
|
|
@@ -462,37 +648,18 @@ export class ServersService implements OnModuleInit, OnModuleDestroy {
|
|
|
|
|
const d = new Date(from.getTime());
|
|
|
|
|
const day = d.getDate();
|
|
|
|
|
d.setMonth(d.getMonth() + months);
|
|
|
|
|
// Handle month-end overflow (e.g. Jan 31 + 1 month)
|
|
|
|
|
if (d.getDate() < day) {
|
|
|
|
|
d.setDate(0);
|
|
|
|
|
}
|
|
|
|
|
return d;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async ensureAccountsForAllTenants() {
|
|
|
|
|
const tenants = await this.prisma.tenant.findMany({
|
|
|
|
|
where: { serverAccount: null },
|
|
|
|
|
select: { id: true },
|
|
|
|
|
});
|
|
|
|
|
for (const tenant of tenants) {
|
|
|
|
|
await this.ensureAccount(tenant.id);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async ensureAccount(tenantId: string) {
|
|
|
|
|
await this.requireTenant(tenantId);
|
|
|
|
|
const existing = await this.prisma.serverAccount.findUnique({
|
|
|
|
|
where: { tenantId },
|
|
|
|
|
});
|
|
|
|
|
if (existing) return existing;
|
|
|
|
|
|
|
|
|
|
return this.prisma.serverAccount.create({
|
|
|
|
|
data: {
|
|
|
|
|
tenantId,
|
|
|
|
|
isDedicated: false,
|
|
|
|
|
alertDaysBefore: 3,
|
|
|
|
|
},
|
|
|
|
|
private async requireAccount(accountId: string) {
|
|
|
|
|
const account = await this.prisma.serverAccount.findUnique({
|
|
|
|
|
where: { id: accountId },
|
|
|
|
|
});
|
|
|
|
|
if (!account) throw new NotFoundException('سرور یافت نشد');
|
|
|
|
|
return account;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async requireTenant(tenantId: string) {
|
|
|
|
|