From 9cfd38ef99d6cba8b7e876cf19444caa4a0fb936 Mon Sep 17 00:00:00 2001 From: Mahdi Date: Fri, 14 Aug 2026 16:31:44 +0330 Subject: [PATCH] feat(get_user): expose subscription_alerts for admin expiry modals Co-authored-by: Cursor --- src/get-user/get-user.service.ts | 125 +++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) diff --git a/src/get-user/get-user.service.ts b/src/get-user/get-user.service.ts index 79e718c..fe2da4c 100644 --- a/src/get-user/get-user.service.ts +++ b/src/get-user/get-user.service.ts @@ -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 | 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 { + 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=