import Foundation /// 注销条件中的单个服务端阻断项;保留未知 code/action,不能据此绕过服务端限制。 struct StoreAccountDeregistrationBlocker: Decodable, Equatable, Sendable { /// 稳定业务错误码。 let code: String /// 供用户阅读的阻断说明。 let message: String /// 后端建议动作;客户端仅对已确认的动作提供说明。 let action: String /// 已知资产确认问题集中展示在资产卡,其余阻断仍逐条展示并阻止继续。 var isAssetConfirmation: Bool { ["WALLET_WAIVER_MISSING", "POINTS_WAIVER_MISSING", "WAIVER_STALE"].contains(code) } /// 未实现跳转的动作仍展示处理建议,不悄悄丢弃该阻断项。 var guidance: String { switch action { case "complete_orders": "请先处理未履约订单或带单,然后刷新条件。" case "wait_risk_window": "请等待业务风险期结束,然后刷新条件。" case "confirm_wallet_waiver": "请单独确认放弃现金余额。" case "confirm_points_waiver": "请单独确认放弃积分。" default: "请按上述说明处理;如不清楚如何操作,请联系管理员或客服。" } } } /// 2026-08-28 真实 eligibility 响应;必需的金额、确认及权限字段缺失时解码失败。 struct StoreAccountDeregistrationEligibility: Decodable, Equatable, Sendable { /// 服务端是否允许申请,不能单独替代完整阻断检查。 let canApply: Bool /// 当前门店用户身份 ID。 let storeUserID: Int /// 对应的财务身份 ID。 let financeIdentityID: Int /// 现金余额,单位分,避免浮点运算。 let walletBalanceFen: Int64 /// 服务端金额展示字符串。 let walletBalance: String /// 积分余额。 let pointsBalance: Int64 /// 服务端当前余额快照是否已有现金放弃确认。 let walletWaived: Bool /// 服务端当前余额快照是否已有积分放弃确认。 let pointsWaived: Bool /// 未履约主订单及带单数量。 let unfulfilledCount: Int /// 尚在处理的交付任务数量。 let fulfillmentInProgressCount: Int /// 最后业务风险结束时间;尚未确认时区,按服务端原文展示。 let riskEndAt: String? /// 业务风险等待截止时间,不是注销冷静期截止时间。 let eligibleAt: String? /// 服务端业务风险等待时长。 let riskWindowHours: Int /// 全部阻断原因,包含客户端尚不认识的新错误码。 let blockers: [StoreAccountDeregistrationBlocker] /// 当前注销记录;已实测草稿、冷静期和主动撤销,其他状态保留原始结构。 let deregister: StoreAccountDeregistrationJSON /// 资产之外尚需处理的业务条件,包含客户端不认识的阻断代码。 var businessBlockers: [StoreAccountDeregistrationBlocker] { blockers.filter { !$0.isAssetConfirmation } } /// 两步页面可以开始资产确认的前提,不替代提交前的完整服务端核验。 var permitsAssetConfirmation: Bool { permitsPreparation && businessBlockers.isEmpty && storeUserID > 0 && financeIdentityID > 0 && walletBalanceFen >= 0 && pointsBalance >= 0 && unfulfilledCount == 0 && fulfillmentInProgressCount == 0 } /// 字段名对齐真实响应,不接受缺失字段的乐观默认值。 enum CodingKeys: String, CodingKey { case canApply = "can_apply", storeUserID = "store_user_id", financeIdentityID = "finance_identity_id" case walletBalanceFen = "wallet_balance_fen", walletBalance = "wallet_balance", pointsBalance = "points_balance" case walletWaived = "wallet_waived", pointsWaived = "points_waived" case unfulfilledCount = "unfulfilled_count", fulfillmentInProgressCount = "fulfillment_in_progress_count" case riskEndAt = "risk_end_at", eligibleAt = "eligible_at", riskWindowHours = "risk_window_hours" case blockers, deregister } /// 没有记录、未提交草稿或已撤销时可准备申请,其他条件仍以服务端核验为准。 var permitsPreparation: Bool { StoreAccountDeregistrationStatus(deregister: deregister).permitsPreparation } /// 完整条件满足才允许提交;有矛盾或未知状态则拒绝,截止时间不由本机推断。 var permitsApplication: Bool { canApply && blockers.isEmpty && walletWaived && pointsWaived && walletBalanceFen >= 0 && pointsBalance >= 0 && unfulfilledCount == 0 && fulfillmentInProgressCount == 0 && storeUserID > 0 && permitsPreparation } /// 确认弹窗期间两种资产或财务身份发生变化,必须重新展示并确认。 func hasSameAssets(as other: Self) -> Bool { storeUserID == other.storeUserID && financeIdentityID == other.financeIdentityID && walletBalanceFen == other.walletBalanceFen && pointsBalance == other.pointsBalance } } /// status 的实测结构:0 草稿、1 冷静期、9 已撤销;缺失或未知状态不默认放行。 struct StoreAccountDeregistrationStatus: Decodable, Equatable, Sendable { /// 保留原始记录,尚不推断未观测的阻断和完成状态值。 let deregister: StoreAccountDeregistrationJSON /// 2026-08-28 两次资产确认后的真实草稿形态;不能仅凭 status=0 忽略矛盾的提交或终态字段。 var isUnsubmittedDraft: Bool { guard case let .object(record) = deregister, case let .number(id) = record["id"], id > 0, id <= Decimal(Int64.max), id == Decimal(NSDecimalNumber(decimal: id).int64Value), record["status"] == .number(0), case let .string(label) = record["status_label"], !label.isEmpty, case .string = record["reason"], record["apply_time"] == .null, record["cooling_until"] == .null, record["remaining_seconds"] == .number(0), record["cancel_time"] == .null, record["blocked_code"] == .string(""), record["blocked_reason"] == .string(""), record["completed_at"] == .null else { return false } return true } /// 实测冷静期;即使剩余秒数为 0,也须等待服务端复核,不能视为完成。 var isCooling: Bool { guard let record = validatedRecord else { return false } return record["status"] == .number(1) && nonemptyString(record["apply_time"]) != nil && nonemptyString(record["cooling_until"]) != nil && record["cancel_time"] == .null && record["completed_at"] == .null && record["blocked_code"] == .string("") && record["blocked_reason"] == .string("") } /// 已撤销必须同时有撤销时间,不能只因剩余秒数为零或仍保留冷静期截止时间而推断。 var isCancelled: Bool { guard let record = validatedRecord else { return false } return record["status"] == .number(9) && nonemptyString(record["apply_time"]) != nil && nonemptyString(record["cancel_time"]) != nil && record["completed_at"] == .null && record["remaining_seconds"] == .number(0) } /// 用于核对取消前后是否为同一申请,不包含账号凭证。 var recordID: String? { guard let record = validatedRecord, case let .number(id) = record["id"] else { return nil } return NSDecimalNumber(decimal: id).stringValue } /// 识别同一次已撤销记录;避免重新申请时用旧撤销响应清除新的提交意图。 var cancellationFingerprint: String? { guard isCancelled, let recordID, let time = nonemptyString(validatedRecord?["cancel_time"]) else { return nil } return recordID + "|" + time } /// 按服务端原文展示,不推断时间字符串的时区。 var coolingUntil: String? { nonemptyString(validatedRecord?["cooling_until"]) } /// 服务端查询时的剩余秒数;客户端不据此宣布注销完成。 var remainingSeconds: Int64? { guard let record = validatedRecord, case let .number(seconds) = record["remaining_seconds"] else { return nil } return NSDecimalNumber(decimal: seconds).int64Value } /// 没有申请、未提交草稿或已撤销允许恢复业务;准备新申请仍须重新核验资产及条件。 var permitsPreparation: Bool { deregister == .null || isUnsubmittedDraft || isCancelled } private var validatedRecord: [String: StoreAccountDeregistrationJSON]? { guard case let .object(record) = deregister, case let .number(id) = record["id"], id > 0, isInteger(id), case let .number(seconds) = record["remaining_seconds"], seconds >= 0, isInteger(seconds), case let .string(label) = record["status_label"], !label.isEmpty, case .string = record["reason"], case .string = record["blocked_code"], case .string = record["blocked_reason"] else { return nil } for key in ["apply_time", "cooling_until", "cancel_time", "completed_at"] { guard record[key] == .null || nonemptyString(record[key]) != nil else { return nil } } return record } private func nonemptyString(_ value: StoreAccountDeregistrationJSON?) -> String? { guard case let .string(text) = value, !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return nil } return text } private func isInteger(_ value: Decimal) -> Bool { value <= Decimal(Int64.max) && value >= Decimal(Int64.min) && value == Decimal(NSDecimalNumber(decimal: value).int64Value) } } /// 必须分别确认的两种资产。 enum StoreAccountDeregistrationAsset: Sendable { case wallet case points }