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 <cursoragent@cursor.com>
This commit is contained in:
2026-08-20 12:32:27 +03:30
co-authored by Cursor
parent c0b6ec9262
commit e459b3d862
@@ -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 از اپ خانگی ناموفق بود'),