feat(get_user): expose subscription_alerts for admin expiry modals

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-14 16:31:44 +03:30
co-authored by Cursor
parent 9bdecdf00a
commit 9cfd38ef99
+125
View File
@@ -1,8 +1,25 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { iranDaysUntil } from '../common/iran-time.util';
import { PrismaService } from '../prisma/prisma.service';
type UiSetting = { key: string; value: string };
type SubscriptionAlertStatus = 'expiring' | 'expired';
type SubscriptionAlertItem = {
id: string;
name: string;
expires_at: string;
days_left: number;
status: SubscriptionAlertStatus;
};
type SubscriptionAlerts = {
servers: SubscriptionAlertItem[];
plugins: SubscriptionAlertItem[];
supports: SubscriptionAlertItem[];
};
@Injectable()
export class GetUserService {
constructor(private readonly prisma: PrismaService) {}
@@ -22,6 +39,8 @@ export class GetUserService {
throw new NotFoundException('tenant not found for domain');
}
const subscription_alerts = await this.buildSubscriptionAlerts(tenant.id);
const ui = (tenant.appUiConfig as Record<string, any> | null) || null;
const isHamkarUi = tenant.product === 'hamkar' && ui?.kind === 'hamkar';
const hamkarBrand = isHamkarUi ? ui?.brand : null;
@@ -89,6 +108,8 @@ export class GetUserService {
: undefined,
/** Laravel-compatible overlay; Hamkar merges only when present */
ui_settings: uiSettings.length ? uiSettings : undefined,
/** Admin expiry modals — near/expired server, plugin, support */
subscription_alerts,
};
return {
@@ -97,6 +118,110 @@ export class GetUserService {
};
}
private async buildSubscriptionAlerts(tenantId: string): Promise<SubscriptionAlerts> {
const [servers, plugins, supports] = await Promise.all([
this.prisma.serverAccount.findMany({
where: {
tenantId,
isDedicated: false,
expiresAt: { not: null },
},
select: {
id: true,
serverName: true,
expiresAt: true,
alertDaysBefore: true,
},
}),
this.prisma.pluginSubscription.findMany({
where: {
tenantId,
isActive: true,
expiresAt: { not: null },
},
select: {
id: true,
name: true,
expiresAt: true,
alertDaysBefore: true,
},
}),
this.prisma.supportSubscription.findMany({
where: {
tenantId,
isActive: true,
expiresAt: { not: null },
},
select: {
id: true,
name: true,
expiresAt: true,
alertDaysBefore: true,
},
}),
]);
return {
servers: servers
.map((row) =>
this.toAlertItem({
id: row.id,
name: (row.serverName || '').trim() || 'سرور',
expiresAt: row.expiresAt!,
alertDaysBefore: row.alertDaysBefore,
}),
)
.filter((item): item is SubscriptionAlertItem => item != null),
plugins: plugins
.map((row) =>
this.toAlertItem({
id: row.id,
name: (row.name || '').trim() || 'افزونه',
expiresAt: row.expiresAt!,
alertDaysBefore: row.alertDaysBefore,
}),
)
.filter((item): item is SubscriptionAlertItem => item != null),
supports: supports
.map((row) =>
this.toAlertItem({
id: row.id,
name: (row.name || '').trim() || 'پشتیبانی',
expiresAt: row.expiresAt!,
alertDaysBefore: row.alertDaysBefore,
}),
)
.filter((item): item is SubscriptionAlertItem => item != null),
};
}
private toAlertItem(input: {
id: string;
name: string;
expiresAt: Date;
alertDaysBefore: number;
}): SubscriptionAlertItem | null {
const daysLeft = iranDaysUntil(input.expiresAt);
const threshold = Math.max(0, Number(input.alertDaysBefore) || 0);
let status: SubscriptionAlertStatus | null = null;
if (daysLeft < 0) {
status = 'expired';
} else if (daysLeft <= threshold) {
status = 'expiring';
}
if (!status) return null;
return {
id: input.id,
name: input.name,
expires_at: input.expiresAt.toISOString(),
days_left: daysLeft,
status,
};
}
/**
* Web App Manifest برای PWA / ساخت APK — برندینگ همان get_user
* GET /manifest.json?domain=