added some feature for plugin

This commit is contained in:
2026-09-08 18:00:56 +03:30
parent 67a03d9593
commit 58744d0f91
7 changed files with 147 additions and 7 deletions
+2
View File
@@ -1,8 +1,10 @@
import { Module } from '@nestjs/common';
import { NotificationsModule } from '../notifications/notifications.module';
import { GetUserController } from './get-user.controller';
import { GetUserService } from './get-user.service';
@Module({
imports: [NotificationsModule],
controllers: [GetUserController],
providers: [GetUserService],
exports: [GetUserService],
+8 -1
View File
@@ -4,6 +4,7 @@ import { PrismaService } from '../prisma/prisma.service';
import { applyKhanegiNavSample } from './khanegi-nav-sample';
import { rewriteLegacyUploadUrl } from '../uploads/uploads.util';
import { PLUGIN_STORE_SETTING_KEY } from '../program-settings/runtime-setting-keys';
import { RuntimeSettingsService } from '../notifications/runtime-settings.service';
import {
findTenantForHost as matchTenantForHost,
normalizeTenantHost,
@@ -29,7 +30,10 @@ type SubscriptionAlerts = {
@Injectable()
export class GetUserService {
constructor(private readonly prisma: PrismaService) {}
constructor(
private readonly prisma: PrismaService,
private readonly runtimeSettings: RuntimeSettingsService,
) {}
async resolveByHost(hostHeader?: string, domainQuery?: string) {
const host = normalizeTenantHost(domainQuery || hostHeader || '');
@@ -89,6 +93,8 @@ export class GetUserService {
tenant.backgroundColor ||
null
const khanegiDeposit = isKhanegi ? this.runtimeSettings.getKhanegiDeposit() : null;
const branding = {
id: tenant.id,
name: tenant.nameEn || tenant.slug,
@@ -120,6 +126,7 @@ export class GetUserService {
plugin_store_enabled: isKhanegi
? await this.isPluginStoreEnabled(tenant.id)
: false,
...(khanegiDeposit || {}),
};
return {
@@ -0,0 +1,28 @@
import { IsOptional, IsString, MaxLength } from 'class-validator';
export class UpdateKhanegiDepositDto {
@IsOptional()
@IsString()
@MaxLength(64)
sms_charge_card_number?: string;
@IsOptional()
@IsString()
@MaxLength(64)
sms_charge_sheba?: string;
@IsOptional()
@IsString()
@MaxLength(120)
sms_charge_account_holder?: string;
@IsOptional()
@IsString()
@MaxLength(4000)
sms_charge_note?: string;
@IsOptional()
@IsString()
@MaxLength(4000)
plugin_charge_note?: string;
}
@@ -3,6 +3,7 @@ import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { PermissionsGuard } from '../auth/permissions.guard';
import { RequirePermissions } from '../auth/permissions.decorator';
import { UpdateAdminSmsDto } from './dto/update-admin-sms.dto';
import { UpdateKhanegiDepositDto } from './dto/update-khanegi-deposit.dto';
import { RuntimeSettingsService } from './runtime-settings.service';
@Controller('settings')
@@ -22,4 +23,16 @@ export class RuntimeSettingsController {
this.settings.setAdminSmsEnabled(dto.enabled);
return this.settings.getPublic();
}
@Get('khanegi-deposit')
@RequirePermissions('khanegi:read')
getKhanegiDeposit() {
return this.settings.getKhanegiDeposit();
}
@Patch('khanegi-deposit')
@RequirePermissions('khanegi:write')
setKhanegiDeposit(@Body() dto: UpdateKhanegiDepositDto) {
return this.settings.setKhanegiDeposit(dto);
}
}
+67 -2
View File
@@ -2,13 +2,33 @@ import { Injectable, Logger } from '@nestjs/common';
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
import { join } from 'path';
export type KhanegiDeposit = {
sms_charge_card_number: string;
sms_charge_sheba: string;
sms_charge_account_holder: string;
sms_charge_note: string;
plugin_charge_note: string;
};
type RuntimeSettings = {
/** When false, SuperAdmin stops SMS alerts to managers */
adminSmsEnabled: boolean;
khanegiDeposit: KhanegiDeposit;
};
const DEPOSIT_DEFAULTS: KhanegiDeposit = {
sms_charge_card_number: '6219861961033817',
sms_charge_sheba: 'IR080560611828005603604201',
sms_charge_account_holder: 'حسین منتظر',
sms_charge_note:
'لطفا مبلغ مورد نظر خود جهت شارژ سامانه پیامکی را به شماره کارت یا حساب زیر واریز نمایید و تصویر فیش پرداختی را برای پشتیبانی بفرستید.',
plugin_charge_note:
'لطفا مبلغ افزونه را به شماره کارت یا حساب زیر واریز نمایید و تصویر فیش را از تب درخواست‌ها ارسال کنید.',
};
const DEFAULTS: RuntimeSettings = {
adminSmsEnabled: true,
khanegiDeposit: { ...DEPOSIT_DEFAULTS },
};
@Injectable()
@@ -28,6 +48,30 @@ export class RuntimeSettingsService {
return next;
}
getKhanegiDeposit(): KhanegiDeposit {
return { ...this.read().khanegiDeposit };
}
setKhanegiDeposit(patch: Partial<KhanegiDeposit>) {
const current = this.read();
const next: RuntimeSettings = {
...current,
khanegiDeposit: this.normalizeDeposit({ ...current.khanegiDeposit, ...patch }),
};
this.write(next);
return next.khanegiDeposit;
}
pluginStoreDeposit() {
const d = this.getKhanegiDeposit();
return {
card_number: d.sms_charge_card_number,
sheba: d.sms_charge_sheba,
account_holder: d.sms_charge_account_holder,
note: d.plugin_charge_note || d.sms_charge_note,
};
}
getPublic() {
try {
return { adminSmsEnabled: this.getAdminSmsEnabled() };
@@ -37,21 +81,42 @@ export class RuntimeSettingsService {
}
}
private normalizeDeposit(raw?: Partial<KhanegiDeposit> | null): KhanegiDeposit {
const pick = (key: keyof KhanegiDeposit) => {
const value = String(raw?.[key] ?? '').trim();
return value || DEPOSIT_DEFAULTS[key];
};
return {
sms_charge_card_number: pick('sms_charge_card_number'),
sms_charge_sheba: pick('sms_charge_sheba'),
sms_charge_account_holder: pick('sms_charge_account_holder'),
sms_charge_note: String(raw?.sms_charge_note ?? DEPOSIT_DEFAULTS.sms_charge_note),
plugin_charge_note: String(raw?.plugin_charge_note ?? DEPOSIT_DEFAULTS.plugin_charge_note),
};
}
private read(): RuntimeSettings {
if (this.cache) return this.cache;
try {
if (!existsSync(this.filePath)) {
this.cache = { ...DEFAULTS };
this.cache = {
adminSmsEnabled: DEFAULTS.adminSmsEnabled,
khanegiDeposit: { ...DEPOSIT_DEFAULTS },
};
return this.cache;
}
const raw = JSON.parse(readFileSync(this.filePath, 'utf8')) as Partial<RuntimeSettings>;
this.cache = {
adminSmsEnabled: raw.adminSmsEnabled !== false,
khanegiDeposit: this.normalizeDeposit(raw.khanegiDeposit),
};
return this.cache;
} catch (error: any) {
this.logger.warn(`Failed to read runtime settings: ${error?.message || error}`);
this.cache = { ...DEFAULTS };
this.cache = {
adminSmsEnabled: DEFAULTS.adminSmsEnabled,
khanegiDeposit: { ...DEPOSIT_DEFAULTS },
};
return this.cache;
}
}
+7 -2
View File
@@ -21,6 +21,7 @@ import { PrismaService } from '../prisma/prisma.service';
import { AuditService } from '../audit/audit.service';
import { AdminNotifyService } from '../notifications/admin-notify.service';
import { CentralSmsService } from '../notifications/central-sms.service';
import { RuntimeSettingsService } from '../notifications/runtime-settings.service';
import { ProgramSettingsService } from '../program-settings/program-settings.service';
import { PLUGIN_STORE_SETTING_KEY } from '../program-settings/runtime-setting-keys';
import { DEPLOY_SETTING_CATALOG_KEYS } from '../program-settings/deploy-setting-catalog-keys';
@@ -59,6 +60,7 @@ export class PluginStoreService implements OnModuleInit, OnModuleDestroy {
private readonly sms: CentralSmsService,
private readonly programSettings: ProgramSettingsService,
private readonly config: ConfigService,
private readonly runtime: RuntimeSettingsService,
) {}
onModuleInit() {
@@ -558,11 +560,14 @@ export class PluginStoreService implements OnModuleInit, OnModuleDestroy {
async remoteListRequests(tenantId: string, product: string) {
await this.assertRemoteKhanegi(tenantId, product);
try {
return await this.buildRemoteRequestRows(tenantId);
return {
requests: await this.buildRemoteRequestRows(tenantId),
deposit: this.runtime.pluginStoreDeposit(),
};
} catch (error: any) {
if (error instanceof ForbiddenException) throw error;
this.logger.warn(`remoteListRequests skipped: ${error?.message || error}`);
return [];
return { requests: [], deposit: this.runtime.pluginStoreDeposit() };
}
}
+22 -2
View File
@@ -8,6 +8,7 @@ import { Prisma, SmsAccount, SmsLedgerType } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import { AuditService } from '../audit/audit.service';
import { AdminNotifyService } from '../notifications/admin-notify.service';
import { RuntimeSettingsService } from '../notifications/runtime-settings.service';
import { iranYmd } from '../common/iran-time.util';
import {
ChargeSmsWalletDto,
@@ -27,6 +28,7 @@ export class SmsService {
private readonly prisma: PrismaService,
private readonly audit: AuditService,
private readonly notify: AdminNotifyService,
private readonly runtime: RuntimeSettingsService,
) {}
async listTenants() {
@@ -374,7 +376,7 @@ export class SmsService {
async remoteOverview(tenantId: string) {
const account = await this.ensureAccount(tenantId);
const usage = await this.computeUsageStats(account.id);
const charge = this.chargeInfoOf(account);
const charge = await this.chargeInfoForRemote(tenantId, account);
return {
wallet: {
@@ -417,7 +419,8 @@ export class SmsService {
}
async remoteChargeInfo(tenantId: string) {
const charge = await this.getChargeInfo(tenantId);
const account = await this.ensureAccount(tenantId);
const charge = await this.chargeInfoForRemote(tenantId, account);
return {
card: charge.card,
iban: charge.iban,
@@ -894,6 +897,23 @@ export class SmsService {
);
}
private async chargeInfoForRemote(tenantId: string, account: SmsAccount) {
const tenant = await this.prisma.tenant.findUnique({
where: { id: tenantId },
select: { product: true },
});
if (tenant?.product === 'khanegi') {
const deposit = this.runtime.getKhanegiDeposit();
return {
card: deposit.sms_charge_card_number,
iban: deposit.sms_charge_sheba,
accountHolder: deposit.sms_charge_account_holder,
instruction: deposit.sms_charge_note,
};
}
return this.chargeInfoOf(account);
}
private chargeInfoOf(account: SmsAccount) {
return {
card: account.chargeCard || '',