Add Mellat e-payment, videos/instructions CMS, and legacy domain lookup.

Store gateway credentials per business, initiate/verify Mellat at checkout, and resolve old CMS ids from domains in the migrate modal.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-13 09:51:17 +03:30
co-authored by Cursor
parent 513673a7da
commit d5e502cf77
44 changed files with 9303 additions and 996 deletions
@@ -10,13 +10,21 @@ import {
DEFAULT_BUSINESS_SETTINGS,
DEFAULT_BUSINESS_THEME_MODE,
DEFAULT_ENABLED_BUSINESS_MODULES,
DEFAULT_EPAYMENT_SETTINGS,
DEFAULT_HOME_CHARTS,
DEFAULT_MELLAT_GATEWAY_SETTINGS,
DEFAULT_ORDER_PROCESS_STEPS,
DEFAULT_STUB_GATEWAY_SETTINGS,
EPaymentSettings,
HOME_CHART_IDS,
MellatGatewaySettings,
PAYMENT_GATEWAY_IDS,
type DashboardLocale,
type DashboardThemeMode,
type HomeChartId,
type PaymentGatewayId,
OrderProcessStep,
StubGatewaySettings,
} from './business-settings.types';
import {
defaultOrderStepColor,
@@ -80,6 +88,190 @@ function readBoolean(value: unknown, fallback: boolean) {
return typeof value === 'boolean' ? value : fallback;
}
function readString(value: unknown, fallback = '') {
return typeof value === 'string' ? value.trim() : fallback;
}
function isPaymentGatewayId(value: unknown): value is PaymentGatewayId {
return (
typeof value === 'string' &&
(PAYMENT_GATEWAY_IDS as readonly string[]).includes(value)
);
}
function readMellatGateway(value: unknown): MellatGatewaySettings {
const source = isRecord(value) ? value : {};
return {
enabled: readBoolean(
source.enabled,
DEFAULT_MELLAT_GATEWAY_SETTINGS.enabled,
),
terminalId: readString(
source.terminalId,
DEFAULT_MELLAT_GATEWAY_SETTINGS.terminalId,
),
username: readString(
source.username,
DEFAULT_MELLAT_GATEWAY_SETTINGS.username,
),
password:
typeof source.password === 'string'
? source.password
: DEFAULT_MELLAT_GATEWAY_SETTINGS.password,
};
}
function readStubGateway(value: unknown): StubGatewaySettings {
const source = isRecord(value) ? value : {};
return {
enabled: readBoolean(
source.enabled,
DEFAULT_STUB_GATEWAY_SETTINGS.enabled,
),
};
}
export function normalizeEPaymentSettings(value: unknown): EPaymentSettings {
const source = isRecord(value) ? value : {};
const gateways = isRecord(source.gateways) ? source.gateways : {};
const defaultGateway = isPaymentGatewayId(source.defaultGateway)
? source.defaultGateway
: null;
return {
enabled: readBoolean(source.enabled, DEFAULT_EPAYMENT_SETTINGS.enabled),
defaultGateway,
gateways: {
mellat: readMellatGateway(gateways.mellat),
sep: readStubGateway(gateways.sep),
snappay: readStubGateway(gateways.snappay),
digipay: readStubGateway(gateways.digipay),
zarinpal: readStubGateway(gateways.zarinpal),
},
};
}
/** Merge e-payment patch; empty mellat password keeps the current secret. */
export function mergeEPaymentSettings(
current: EPaymentSettings,
patch: EPaymentSettings | undefined,
): EPaymentSettings {
if (!patch) return current;
const nextPassword = patch.gateways.mellat.password;
const password =
typeof nextPassword === 'string' && nextPassword.length > 0
? nextPassword
: current.gateways.mellat.password;
return {
enabled: patch.enabled,
defaultGateway: patch.defaultGateway,
gateways: {
mellat: {
enabled: patch.gateways.mellat.enabled,
terminalId: patch.gateways.mellat.terminalId.trim(),
username: patch.gateways.mellat.username.trim(),
password,
},
sep: { enabled: patch.gateways.sep.enabled },
snappay: { enabled: patch.gateways.snappay.enabled },
digipay: { enabled: patch.gateways.digipay.enabled },
zarinpal: { enabled: patch.gateways.zarinpal.enabled },
},
};
}
/** Redact gateway secrets for dashboard API responses. */
export function sanitizeBusinessSettingsForClient(
settings: BusinessSettings,
): BusinessSettings {
const mellat = settings.store.ePayment.gateways.mellat;
return {
...settings,
store: {
...settings.store,
ePayment: {
...settings.store.ePayment,
gateways: {
...settings.store.ePayment.gateways,
mellat: {
...mellat,
password: '',
// Clients treat non-empty passwordSet via a parallel field — see
// get/update response shaping in BusinessSettingsService.
},
},
},
},
};
}
export function mellatPasswordSet(settings: BusinessSettings): boolean {
return settings.store.ePayment.gateways.mellat.password.length > 0;
}
/** Whether a gateway is enabled and has enough credentials to initiate. */
export function isGatewayReady(
settings: EPaymentSettings,
gatewayId: PaymentGatewayId,
): boolean {
if (!settings.enabled) return false;
switch (gatewayId) {
case 'mellat': {
const g = settings.gateways.mellat;
return (
g.enabled &&
g.terminalId.length > 0 &&
g.username.length > 0 &&
g.password.length > 0
);
}
case 'sep':
case 'snappay':
case 'digipay':
case 'zarinpal':
return false;
default:
return false;
}
}
export function listPublicPaymentGateways(settings: EPaymentSettings) {
const labels: Record<
PaymentGatewayId,
{ label: string; labelFa: string }
> = {
mellat: { label: 'Bank Mellat', labelFa: 'بانک ملت' },
sep: { label: 'SEP (Saman)', labelFa: 'سپ (سامان)' },
snappay: { label: 'Snapp Pay', labelFa: 'اسنپ‌پی' },
digipay: { label: 'DigiPay', labelFa: 'دیجی‌پی' },
zarinpal: { label: 'Zarinpal', labelFa: 'زرین‌پال' },
};
const gateways = PAYMENT_GATEWAY_IDS.filter((id) =>
isGatewayReady(settings, id),
).map((id) => ({
id,
...labels[id],
}));
let defaultGateway = settings.defaultGateway;
if (defaultGateway && !gateways.some((g) => g.id === defaultGateway)) {
defaultGateway = null;
}
if (!defaultGateway && gateways.length) {
defaultGateway = gateways[0].id;
}
return {
enabled: settings.enabled && gateways.length > 0,
defaultGateway,
gateways,
};
}
function readOrderProcessSteps(value: unknown): OrderProcessStep[] {
if (!Array.isArray(value)) {
return DEFAULT_ORDER_PROCESS_STEPS;
@@ -152,6 +344,7 @@ export function normalizeBusinessSettings(raw: unknown): BusinessSettings {
DEFAULT_BUSINESS_SETTINGS.store.onlineSellEnabled,
),
orderProcessSteps: readOrderProcessSteps(store.orderProcessSteps),
ePayment: normalizeEPaymentSettings(store.ePayment),
},
modules: {
// Missing `modules` → all enabled (legacy). Explicit `{ enabled: [] }` stays empty.
@@ -194,6 +387,10 @@ export function mergeBusinessSettings(
patch.store?.onlineSellEnabled ?? current.store.onlineSellEnabled,
orderProcessSteps:
patch.store?.orderProcessSteps ?? current.store.orderProcessSteps,
ePayment: mergeEPaymentSettings(
current.store.ePayment,
patch.store?.ePayment,
),
},
modules: {
enabled: patch.modules?.enabled ?? current.modules.enabled,