From e459b3d86231dff759849831840e6d5b904f2856 Mon Sep 17 00:00:00 2001 From: adonis Date: Thu, 20 Aug 2026 12:32:27 +0330 Subject: [PATCH] fix(import): stop re-asking sms_charge and deleted fee alias Import preview listed sms_charge_* and service_charge_percentage, but applyImport skipped every out-of-catalog key via isSystemProgramSettingKey, so "all projects" never persisted and the modal returned every time. Co-authored-by: Cursor --- .../program-settings.service.ts | 68 ++++++++++++++++--- 1 file changed, 58 insertions(+), 10 deletions(-) diff --git a/src/program-settings/program-settings.service.ts b/src/program-settings/program-settings.service.ts index a3439dc..c7c5076 100644 --- a/src/program-settings/program-settings.service.ts +++ b/src/program-settings/program-settings.service.ts @@ -206,11 +206,17 @@ export class ProgramSettingsService implements OnModuleInit { const local = await this.listTenantRows(tenantId); const catalogKeys = this.deployCatalogKeySet(); const seedCanEdit = this.deployCatalogCanEditMap(); + const knownTemplates = await this.prisma.settingTemplate.findMany({ + where: { deletedAt: null }, + select: { key: true }, + }); + const knownTemplateKeys = new Set(knownTemplates.map((t: { key: string }) => t.key)); const localByKey = new Map(local.map((row) => [String(row.key), row])); const remoteByKey = new Map( remote .map((row) => this.normalizeIncoming(row)) .filter((row) => row.key && !isRuntimeProgramSettingKey(row.key)) + .filter((row) => !this.isImportDecisionExcluded(row.key)) .map((row) => [row.key, row]), ); @@ -223,7 +229,9 @@ export class ProgramSettingsService implements OnModuleInit { }> = []; for (const [key, row] of remoteByKey) { - if (catalogKeys.has(key)) continue; + // قالب deploy یا قالب DB قبلاً ساخته‌شده → بدون سؤال + if (catalogKeys.has(key) || knownTemplateKeys.has(key)) continue; + if (DEPLOY_SETTING_EDITABLE_KEYS.has(key)) continue; const localRow = localByKey.get(key); const khanegiValue = String(row.value ?? ''); const superadminValue = localRow ? String(localRow.value ?? '') : null; @@ -272,10 +280,17 @@ export class ProgramSettingsService implements OnModuleInit { await this.ensureTemplatesSeeded(); const remote = await this.fetchSystemSettings(apiBase, token); const catalogKeys = this.deployCatalogKeySet(); + const knownTemplates = await this.prisma.settingTemplate.findMany({ + where: { deletedAt: null }, + select: { key: true }, + }); + const knownTemplateKeys = new Set(knownTemplates.map((t: { key: string }) => t.key)); const remoteByKey = new Map( remote .map((row) => this.normalizeIncoming(row)) .filter((row) => row.key && !isRuntimeProgramSettingKey(row.key)) + .filter((row) => !this.isImportDecisionExcluded(row.key)) + .filter((row) => !DEPLOY_SETTING_EDITABLE_KEYS.has(row.key)) .map((row) => [row.key, row]), ); @@ -283,7 +298,7 @@ export class ProgramSettingsService implements OnModuleInit { for (const d of decisions || []) { const key = String(d.key || '').trim(); const scope = d.scope; - if (!key) continue; + if (!key || this.isImportDecisionExcluded(key)) continue; if (scope !== 'none' && scope !== 'this' && scope !== 'all') { throw new BadRequestException(`scope نامعتبر برای ${key}`); } @@ -302,10 +317,17 @@ export class ProgramSettingsService implements OnModuleInit { await this.ensureTemplateDefaultsForTenant(tx, tenantId); for (const [key, row] of remoteByKey) { - if (!this.isSystemProgramSettingKey(key)) continue; - const inCatalog = catalogKeys.has(key); - const scope = inCatalog ? 'this' : (decisionMap.get(key) ?? 'this'); - if (scope === 'none') continue; + const inCatalog = catalogKeys.has(key) || knownTemplateKeys.has(key); + let scope: ImportDecisionScope | undefined; + if (inCatalog) { + scope = 'this'; + } else if (decisionMap.has(key)) { + scope = decisionMap.get(key); + } else { + // خارج از قالب و بدون تصمیم کاربر → نادیده + continue; + } + if (!scope || scope === 'none') continue; const targetIds = scope === 'all' ? khanegiTenantIds : [tenantId]; for (const tid of targetIds) { @@ -318,12 +340,23 @@ export class ProgramSettingsService implements OnModuleInit { if (inCatalog) continue; + // خارج از قالب + تصمیم this/all → قالب بساز تا مدال دوباره نیاید const tpl = await tx.settingTemplate.findUnique({ where: { key } }); + const tplData = this.templateDataFromRow({ + ...row, + key, + value: '', + can_edit: 0, + }); if (!tpl) { - await tx.settingTemplate.create({ - data: this.templateDataFromRow(row), + await tx.settingTemplate.create({ data: tplData }); + } else if (tpl.deletedAt) { + await tx.settingTemplate.update({ + where: { key }, + data: { ...tplData, deletedAt: null }, }); } + knownTemplateKeys.add(key); } await tx.tenant.update({ @@ -553,6 +586,19 @@ export class ProgramSettingsService implements OnModuleInit { return DEPLOY_SETTING_CATALOG_KEYS.has(k); } + /** + * کلیدهایی که نباید در مدال تصمیم ایمپورت بیایند / نوشته شوند: + * - alias حذف‌شده کارمزد + * - شارژ پیامک (مالک: پنل SMS سوپرادمین، نه تنظیمات پیشرفته پروژه) + */ + private isImportDecisionExcluded(keyRaw: string): boolean { + const key = String(keyRaw || '').trim(); + if (!key) return true; + if (key === 'service_charge_percentage') return true; + if (key.startsWith('sms_charge_')) return true; + return false; + } + /** حذف can_edit=1 از قالب و همهٔ پروژه‌های خانگی در DB سوپرادمین */ async purgeEditableFromSuperAdmin() { const keys = [...DEPLOY_SETTING_EDITABLE_KEYS]; @@ -904,7 +950,8 @@ export class ProgramSettingsService implements OnModuleInit { }); const items = this.asSettingsArray(data) .filter((row) => this.isSystemSetting(row)) - .filter((row) => !isRuntimeProgramSettingKey(String(row.key || ''))); + .filter((row) => !isRuntimeProgramSettingKey(String(row.key || ''))) + .filter((row) => !this.isImportDecisionExcluded(String(row.key || ''))); if (items.length) return items; } catch (error) { const status = axios.isAxiosError(error) ? error.response?.status : undefined; @@ -923,7 +970,8 @@ export class ProgramSettingsService implements OnModuleInit { }); return this.asSettingsArray(data) .filter((row) => this.isSystemSetting(row)) - .filter((row) => !isRuntimeProgramSettingKey(String(row.key || ''))); + .filter((row) => !isRuntimeProgramSettingKey(String(row.key || ''))) + .filter((row) => !this.isImportDecisionExcluded(String(row.key || ''))); } catch (error) { throw new BadGatewayException( this.axiosMessage(error, 'خواندن جدول settings از اپ خانگی ناموفق بود'),