From 668bbcc1db237dbc75a090efcd646ae5ac0e3647 Mon Sep 17 00:00:00 2001 From: adonis Date: Mon, 17 Aug 2026 21:51:48 +0330 Subject: [PATCH] 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 --- .../deploy-setting-catalog-keys.ts | 199 ++++++++++++++++++ .../program-settings.service.ts | 79 +++++-- 2 files changed, 261 insertions(+), 17 deletions(-) create mode 100644 src/program-settings/deploy-setting-catalog-keys.ts diff --git a/src/program-settings/deploy-setting-catalog-keys.ts b/src/program-settings/deploy-setting-catalog-keys.ts new file mode 100644 index 0000000..ce91707 --- /dev/null +++ b/src/program-settings/deploy-setting-catalog-keys.ts @@ -0,0 +1,199 @@ +/** کلیدهای deploy_settings_const — داخل باندل کامپایل می‌شود تا ایمپورت به فایل prisma وابسته نباشد. */ +export const DEPLOY_SETTING_CATALOG_KEYS = new Set([ + "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", +]); + diff --git a/src/program-settings/program-settings.service.ts b/src/program-settings/program-settings.service.ts index 665c6c3..74dad8c 100644 --- a/src/program-settings/program-settings.service.ts +++ b/src/program-settings/program-settings.service.ts @@ -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(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) { + if (!key || catalogKeys.has(key)) return false; + if (isRuntimeProgramSettingKey(key)) return false; + if (isSecretOrServerOnlySettingKey(key)) return false; + return true; } private async seedEmptyKhanegiTenants() {