fix(program-settings): skip deploy_const keys in Khanegi import preview

Catalog keys are compiled into the API bundle so import no longer depends on an incomplete templates table or missing JSON on the server.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-17 21:51:48 +03:30
co-authored by Cursor
parent 357c4fef59
commit 668bbcc1db
2 changed files with 261 additions and 17 deletions
@@ -0,0 +1,199 @@
/** کلیدهای deploy_settings_const — داخل باندل کامپایل می‌شود تا ایمپورت به فایل prisma وابسته نباشد. */
export const DEPLOY_SETTING_CATALOG_KEYS = new Set<string>([
"accounting_provider",
"admin_url",
"apk_download_link",
"app_url",
"auto_active_customer",
"balance_report_exclude_customer_id",
"bale_bot_token",
"banner_enabled",
"buy_fee",
"change_mesghal",
"charge_notif",
"chart_id",
"chart_use_local_data",
"chat_api",
"chat_api_key",
"chat_download_url",
"chat_upload_url",
"chat_webhook_base_url",
"chat_webhook_secret",
"close_price",
"customer_direct_sms",
"customer_special_pricing",
"deposit_account_holder",
"deposit_account_number",
"deposit_card_number",
"deposit_method_card_to_card",
"deposit_method_gateway",
"deposit_method_identified",
"deposit_method_shetab",
"deposit_sheba",
"deposit_sms_support_number",
"deposit_support_message_text",
"down_price",
"enable_conditional_order",
"enable_customer_level_profit",
"enable_product_funds",
"endDay",
"external_order_api_enabled",
"external_payment_base_url",
"external_payment_client_id",
"external_payment_client_secret",
"external_payment_enabled",
"external_payment_gateway",
"external_payment_return_url",
"external_payment_webhook_url",
"fee_conditional_order",
"fee_deduct_from_gold",
"fee_free",
"fee_service_charge_percentage",
"gift_for_referral",
"gift_for_referrer",
"group_sms",
"has_chat",
"has_page_earn_money",
"has_page_gift_card",
"has_page_notification",
"has_page_price_trigger",
"invite_link_text",
"is_store",
"jibit_enabled",
"jibit_verification",
"logo_login",
"logo_register",
"main_metal",
"main_product",
"main_product_price",
"main_wallet_products",
"metal_type",
"min_deposit",
"min_first_buy",
"min_withdraw",
"minimum_sell",
"montify_name",
"montzar_base_url",
"montzar_closed",
"montzar_diff",
"montzar_last_product_log",
"montzar_live_price",
"montzar_mazaneh",
"montzar_mobile",
"montzar_open_buy",
"montzar_open_sell",
"montzar_order_response",
"montzar_password",
"montzar_product_id",
"montzar_token",
"montzar_truly_buy_mazaneh",
"montzar_truly_sell_mazaneh",
"name_fa",
"node_client_id",
"node_shared_secret",
"open_register",
"pargar_api_token",
"pargar_item_id_gold",
"pargar_item_id_rial",
"pargar_password",
"pargar_username",
"payping_fin_customer_id",
"payping_fin_sync_enabled",
"payping_fin_token",
"payping_fin_url",
"payping_gateway_items",
"payping_gateway_name",
"payping_round_toman",
"payping_start_url",
"payping_strict_failed_withdraw",
"payping_verify_url",
"permision__enabled",
"price_channel_access",
"price_channel_bale_to",
"price_channel_diff",
"price_channel_enabled",
"price_channel_footer_id",
"price_channel_footer_link",
"price_channel_footer_phone",
"price_channel_message_text",
"price_channel_messengers",
"price_channel_min_seconds",
"price_channel_rubika_to",
"price_channel_telegram_to",
"price_extreme_date",
"price_provider",
"products_site_link_enabled",
"products_site_link_text",
"products_site_link_url",
"profit_gold_api_enabled",
"referral_bonus_amount",
"referral_gift_buy_from_montzar",
"referral_gift_on_first_order",
"referral_id",
"referral_limit",
"referral_show",
"register_jibit_verify",
"register_referral_bonus_message",
"register_referral_sms_to_referrer",
"require_zero_main_gold_before_other_buy",
"rubika_bot_token",
"sell_fee",
"send_to_pargar",
"send_to_tahesab",
"service_charge_percentage",
"show_gold_ounce_price",
"show_home_price_when_closed",
"show_last_price",
"show_product_card_meta",
"show_product_details",
"show_product_trade_status",
"show_to_users_chat",
"sms_notif_bank_card_admin",
"sms_notif_bank_card_customer",
"sms_notif_fund_withdraw_admin",
"sms_notif_fund_withdraw_customer",
"sms_notif_gateway_charge_admin",
"sms_notif_jibit_admin",
"sms_notif_login_otp",
"sms_notif_order_admin",
"sms_notif_order_customer",
"sms_notif_price_trigger",
"sms_notif_register_admin",
"sms_notif_register_customer",
"sms_notif_support_admin",
"sms_notif_wallet_alert",
"sms_notif_withdraw_admin",
"sms_notif_withdraw_otp",
"sms_panel",
"sse_path",
"startDay",
"support_messenger",
"support_messenger_id",
"support_messenger_text",
"tahesab_bank_account",
"tahesab_db",
"tahesab_host",
"tahesab_port",
"tahesab_token",
"telegram_bot_token",
"text_about",
"text_rule",
"title_fav",
"title_pwa",
"tracking_systems",
"tracking_systems_enabled",
"unit",
"up_price",
"wallet_payment_provider",
"wss_enabled",
"zaarbox_api_url",
"zaarbox_price_key",
"zarin_api",
"zarin_gorohe_kar",
"zarin_name_g",
"zarin_noe_kar",
"zarin_noe_kar_tedadi",
"zarin_token",
"zarin_uid_prefix",
]);
@@ -16,6 +16,7 @@ import { PrismaService } from '../prisma/prisma.service';
import { AuditService } from '../audit/audit.service';
import { isSecretOrServerOnlySettingKey } from './secret-setting-keys';
import { isRuntimeProgramSettingKey } from './runtime-setting-keys';
import { DEPLOY_SETTING_CATALOG_KEYS } from './deploy-setting-catalog-keys';
export type ProgramSettingRow = {
id?: string | number;
@@ -187,8 +188,10 @@ export class ProgramSettingsService implements OnModuleInit {
throw new BadRequestException('موبایل ادمین خانگی باید ۱۱ رقم باشد (مثلاً 0912…)');
}
const { apiBase, token } = await this.loginKhanegiAdmin(tenant, mobileNorm, password);
await this.ensureTemplatesSeeded();
const remote = await this.fetchSystemSettings(apiBase, token);
const local = await this.listTenantRows(tenantId);
const catalogKeys = await this.catalogKeySet();
const localByKey = new Map(local.map((row) => [String(row.key), row]));
const remoteByKey = new Map(
remote
@@ -206,6 +209,7 @@ export class ProgramSettingsService implements OnModuleInit {
}> = [];
for (const [key, row] of remoteByKey) {
if (!this.isNewKhanegiCatalogKey(key, catalogKeys)) continue;
const localRow = localByKey.get(key);
decisionsNeeded.push({
key,
@@ -216,15 +220,16 @@ export class ProgramSettingsService implements OnModuleInit {
});
}
const onlySuperAdmin = [...localByKey.keys()].filter((key) => !remoteByKey.has(key));
const onlySuperAdmin = [...catalogKeys].filter((key) => !remoteByKey.has(key));
return {
tenant: this.tenantMeta(tenant),
apiBase,
decisionsNeeded,
onlySuperAdminKeys: onlySuperAdmin,
skippedCatalogCount: [...remoteByKey.keys()].filter((key) => catalogKeys.has(key)).length,
note:
'برای هر کلید یکی از حالت‌ها را بفرستید: none | this | all. کلیدهایی که فقط در سوپرادمین‌اند با مقدار پیش‌فرض قالب روی این پروژه تکمیل می‌شوند.',
'فقط کلیدهایی که در قالب سوپرادمین نیستند. کلیدهای deploy_settings_const از قبل در قالب‌اند و از خانگی بازنویسی نمی‌شوند. کلیدهای فقط-سوپرادمین روی این پروژه با پیش‌فرض قالب تکمیل می‌شوند.',
};
}
@@ -241,11 +246,14 @@ export class ProgramSettingsService implements OnModuleInit {
throw new BadRequestException('موبایل ادمین خانگی باید ۱۱ رقم باشد (مثلاً 0912…)');
}
const { apiBase, token } = await this.loginKhanegiAdmin(tenant, mobileNorm, password);
await this.ensureTemplatesSeeded();
const remote = await this.fetchSystemSettings(apiBase, token);
const catalogKeys = await this.catalogKeySet();
const remoteByKey = new Map(
remote
.map((row) => this.normalizeIncoming(row))
.filter((row) => row.key && !isRuntimeProgramSettingKey(row.key))
.filter((row) => this.isNewKhanegiCatalogKey(row.key, catalogKeys))
.map((row) => [row.key, row]),
);
@@ -443,25 +451,29 @@ export class ProgramSettingsService implements OnModuleInit {
}
async ensureTemplatesSeeded() {
const count = await this.prisma.settingTemplate.count();
if (count > 0) return { seeded: false, count };
const seedPath = this.resolveSeedPath();
if (!seedPath || !existsSync(seedPath)) {
return { seeded: false, count: 0, missingSeed: true };
}
const raw = JSON.parse(readFileSync(seedPath, 'utf8')) as TemplateSeedRow[];
if (!Array.isArray(raw) || !raw.length) {
return { seeded: false, count: 0 };
const raw = this.readSeedRows();
const before = await this.prisma.settingTemplate.count();
if (!raw.length) {
return { seeded: false, count: before, missingSeed: true };
}
let inserted = 0;
await this.prisma.$transaction(async (tx: any) => {
for (const row of raw) {
const key = String(row.key || '').trim();
if (!key || isRuntimeProgramSettingKey(key)) continue;
await tx.settingTemplate.upsert({
where: { key },
create: {
const existing = await tx.settingTemplate.findUnique({ where: { key } });
if (existing) {
if (existing.deletedAt) {
await tx.settingTemplate.update({
where: { key },
data: { deletedAt: null },
});
}
continue;
}
await tx.settingTemplate.create({
data: {
key,
titleFa: row.title_fa || key,
value: row.value == null ? '' : String(row.value),
@@ -472,13 +484,46 @@ export class ProgramSettingsService implements OnModuleInit {
isActive: this.toSmallInt(row.is_active, 1),
isExist: row.is_exist === 0 || row.is_exist === false ? false : true,
},
update: {},
});
inserted += 1;
}
});
const after = await this.prisma.settingTemplate.count();
return { seeded: true, count: after };
return { seeded: inserted > 0 || before === 0, count: after };
}
private readSeedRows(): TemplateSeedRow[] {
const seedPath = this.resolveSeedPath();
if (!seedPath || !existsSync(seedPath)) return [];
try {
const raw = JSON.parse(readFileSync(seedPath, 'utf8')) as TemplateSeedRow[];
return Array.isArray(raw) ? raw : [];
} catch {
return [];
}
}
/** کلیدهای deploy const (داخل باندل) + فایل seed + جدول قالب. */
private async catalogKeySet() {
const keys = new Set<string>(DEPLOY_SETTING_CATALOG_KEYS);
for (const row of this.readSeedRows()) {
const key = String(row.key || '').trim();
if (key) keys.add(key);
}
const rows = await this.prisma.settingTemplate.findMany({
where: { deletedAt: null },
select: { key: true },
});
for (const row of rows as Array<{ key: string }>) keys.add(row.key);
return keys;
}
private isNewKhanegiCatalogKey(key: string, catalogKeys: Set<string>) {
if (!key || catalogKeys.has(key)) return false;
if (isRuntimeProgramSettingKey(key)) return false;
if (isSecretOrServerOnlySettingKey(key)) return false;
return true;
}
private async seedEmptyKhanegiTenants() {