diff --git a/tests/account_facts_validation.mjs b/tests/account_facts_validation.mjs index bfe1870c..ff33cce9 100644 --- a/tests/account_facts_validation.mjs +++ b/tests/account_facts_validation.mjs @@ -606,6 +606,7 @@ const driftedTarget = identityDriftRead.accounts.find((item) => item.account_key assert.equal(driftedTarget.data_status, "unavailable"); assert.equal(driftedTarget.identity_mismatch, true); assert.equal(driftedTarget.identity_status, "missing_identity"); +assert.equal(driftedTarget.binding_reason, "identity_mismatch"); assert.equal(driftedTarget.balances.length, 0); await saveAccounts([account, otherAccount]); @@ -741,6 +742,7 @@ await saveAccounts([{ }, otherAccount]); const remappedHistory = await (await getHistory("longbridge", "lb-paper", "USD", sessionHeaders, env)).json(); assert.equal(remappedHistory.identity_mismatch, true); +assert.equal(remappedHistory.binding_reason, "identity_mismatch"); assert.equal(remappedHistory.series.points.length, 0); await saveAccounts([account, otherAccount]); diff --git a/tests/longbridge_financing_validation.mjs b/tests/longbridge_financing_validation.mjs index 62d9c0cd..250b8659 100644 --- a/tests/longbridge_financing_validation.mjs +++ b/tests/longbridge_financing_validation.mjs @@ -297,6 +297,7 @@ const unboundModel = buildAccountFactsReadModel({ storedByAccount: new Map([[`longbridge:${option.key}`, withFinancing]]), }); assert.equal(unboundModel.accounts[0].binding_status, "missing"); +assert.equal(unboundModel.accounts[0].binding_reason, "unbound"); assert.equal(Object.hasOwn(unboundModel.accounts[0], "financing"), false); assert.deepEqual(unboundModel.accounts[0].cash, []); @@ -310,6 +311,7 @@ const mismatchModel = buildAccountFactsReadModel({ storedByAccount: new Map([[`longbridge:${option.key}`, withFinancing]]), }); assert.equal(mismatchModel.accounts[0].identity_mismatch, true); +assert.equal(mismatchModel.accounts[0].binding_reason, "identity_mismatch"); assert.equal(Object.hasOwn(mismatchModel.accounts[0], "financing"), false); const accountFresh = { diff --git a/web/strategy-switch-console/account_facts.js b/web/strategy-switch-console/account_facts.js index 613dda3c..290f223b 100644 --- a/web/strategy-switch-console/account_facts.js +++ b/web/strategy-switch-console/account_facts.js @@ -924,9 +924,17 @@ export function buildAccountFactsReadModel({ const binding = bindingByAccount.get(accountId) || null; const sourceKey = binding ? `${binding.target_id}|${binding.source_binding.id}` : null; let bindingStatus = "missing"; - if (binding && sourceKey && (sourceCounts.get(sourceKey) || 0) > 1) bindingStatus = "duplicate"; - else if (binding && accountFactsOptionMatchesBinding(option, binding)) bindingStatus = "bound"; - else if (binding) bindingStatus = "identity_mismatch"; + let bindingReason = "unbound"; + if (binding && sourceKey && (sourceCounts.get(sourceKey) || 0) > 1) { + bindingStatus = "duplicate"; + bindingReason = "duplicate_source"; + } else if (binding && accountFactsOptionMatchesBinding(option, binding)) { + bindingStatus = "bound"; + bindingReason = "bound"; + } else if (binding) { + bindingStatus = "identity_mismatch"; + bindingReason = "identity_mismatch"; + } const stored = storedByAccount.get(accountId) || null; let projected = null; @@ -965,6 +973,7 @@ export function buildAccountFactsReadModel({ platform, account_key: option.key, binding_status: bindingStatus === "identity_mismatch" ? "missing" : bindingStatus, + binding_reason: bindingReason, identity_status: bindingStatus === "bound" ? "partial_identity" : "missing_identity", ...(projected && platform === ACCOUNT_FACTS_PLATFORM && binding?.physical_account_id ? { identity_source: "protected_configuration" } : {}), @@ -1263,6 +1272,7 @@ export function buildAccountFactsHistoryReadModel({ platform: platform || null, account_key: accountKey || null, binding_status: "missing", + binding_reason: "unbound", identity_status: "missing_identity", identity_mismatch: false, series: projectAccountFactsHistorySeries({ days: [], binding: null, currency }), @@ -1270,10 +1280,13 @@ export function buildAccountFactsHistoryReadModel({ }; } let bindingStatus = "missing"; + let bindingReason = "unbound"; if (binding && accountFactsOptionMatchesBinding(option, binding) && binding.account_key === accountKey) { bindingStatus = "bound"; + bindingReason = "bound"; } else if (binding) { bindingStatus = "identity_mismatch"; + bindingReason = "identity_mismatch"; } const qualified = includeCurrencies && bindingStatus === "bound" && typeof currency === "string" && CURRENCY_RE.test(currency) ? qualifiedHistoryDays(days, binding) : null; @@ -1287,6 +1300,7 @@ export function buildAccountFactsHistoryReadModel({ platform, account_key: accountKey, binding_status: bindingStatus === "identity_mismatch" ? "missing" : bindingStatus, + binding_reason: bindingReason, identity_status: bindingStatus === "bound" ? "partial_identity" : "missing_identity", identity_mismatch: bindingStatus === "identity_mismatch", target_id: bindingStatus === "bound" ? binding.target_id : null, diff --git a/web/strategy-switch-console/frontend/src/api/contracts.ts b/web/strategy-switch-console/frontend/src/api/contracts.ts index e9b4f7c8..b70bc2a6 100644 --- a/web/strategy-switch-console/frontend/src/api/contracts.ts +++ b/web/strategy-switch-console/frontend/src/api/contracts.ts @@ -774,6 +774,7 @@ export interface AccountFactsAccount { platform: string; account_key: string; binding_status: "bound" | "missing" | "duplicate"; + binding_reason?: "bound" | "unbound" | "duplicate_source" | "identity_mismatch"; identity_status: "partial_identity" | "missing_identity"; identity_source?: "protected_configuration"; identity_mismatch: boolean; diff --git a/web/strategy-switch-console/frontend/src/types.ts b/web/strategy-switch-console/frontend/src/types.ts index 4ae0c823..1f9568f3 100644 --- a/web/strategy-switch-console/frontend/src/types.ts +++ b/web/strategy-switch-console/frontend/src/types.ts @@ -82,6 +82,7 @@ export type AccountFactsAccount = { platform: string; account_key: string; binding_status: "bound" | "missing" | "duplicate"; + binding_reason?: "bound" | "unbound" | "duplicate_source" | "identity_mismatch"; identity_status: "partial_identity" | "missing_identity"; identity_mismatch?: boolean; aggregation_status?: "included" | "duplicate" | "conflict" | "unverified"; @@ -193,6 +194,7 @@ export type AccountFactsHistorySnapshot = { platform: string; account_key: string; binding_status: "bound" | "missing" | "duplicate"; + binding_reason?: "bound" | "unbound" | "duplicate_source" | "identity_mismatch"; identity_status: "partial_identity" | "missing_identity"; identity_mismatch?: boolean; aggregation_status?: "included" | "duplicate" | "conflict" | "unverified"; @@ -380,8 +382,8 @@ export function accountFactsDisplayReady(account: AccountFactsAccount | null | u export function accountFactsDetail(account: AccountFactsAccount | null | undefined): string { if (!account) return "尚未取得账户资产资料"; - if (account.identity_mismatch === true) return "账户身份不匹配"; - if (account.binding_status === "duplicate") return "账户资料绑定重复"; + if (account.binding_reason === "identity_mismatch" || account.identity_mismatch === true) return "账户身份不匹配"; + if (account.binding_reason === "duplicate_source" || account.binding_status === "duplicate") return "账户资料绑定重复"; if (account.binding_status === "missing") return "账户资料尚未绑定"; if (account.identity_status !== "partial_identity") return "账户身份待核实"; if (account.data_status === "stale") return "账户资产资料已过期";