// // OfflineCollectionViewModels.swift // suixinkan // import Foundation /// 收款首页线下收款区域的 ViewModel。 final class OfflineCollectionHomeViewModel { private(set) var todaySummary: OfflineDailySummary private(set) var overdueSummaries: [OfflineDailySummary] = [] let context: OfflineCollectionContext let service: OfflineCollectionMockService var onStateChange: (() -> Void)? /// 使用当前账号上下文创建首页 ViewModel。 init( context: OfflineCollectionContext = .current(), service: OfflineCollectionMockService = .shared ) { self.context = context self.service = service todaySummary = OfflineDailySummary.make(businessDate: service.todayBusinessDate, records: []) load() } /// 当前今日营业日。 var todayBusinessDate: String { service.todayBusinessDate } /// 最早一个需要处理的逾期营业日。 var earliestOverdueBusinessDate: String? { overdueSummaries.first?.businessDate } /// 历史逾期记录的合计笔数。 var overdueRecordCount: Int { overdueSummaries.reduce(0) { $0 + $1.pendingCount } } /// 历史逾期记录的合计待补缴金额。 var overdueAmountFen: Int { overdueSummaries.reduce(0) { $0 + $1.pendingAmountFen } } /// 从本地 Mock 服务重新读取今日和逾期汇总。 func load() { todaySummary = service.getDailySummary(date: todayBusinessDate, context: context) overdueSummaries = service.getOverdueSummaries(context: context) onStateChange?() } } /// 线下收款登记页 ViewModel,管理表单校验、提交防重和成功反馈。 final class OfflineCollectionRegistrationViewModel { private(set) var amountText = "" private(set) var paymentMethod: OfflineCollectionPaymentMethod? = .wechat private(set) var isSubmitting = false private(set) var receipt: OfflineCollectionRegistrationReceipt? let context: OfflineCollectionContext let service: OfflineCollectionMockService var onStateChange: (() -> Void)? var onShowMessage: ((String) -> Void)? var onRegistrationSuccess: ((OfflineCollectionRegistrationReceipt) -> Void)? /// 使用指定收款上下文和 Mock 服务创建登记 ViewModel。 init( context: OfflineCollectionContext = .current(), service: OfflineCollectionMockService = .shared ) { self.context = context self.service = service } /// 当前金额和收款方式是否允许提交。 var canSubmit: Bool { !isSubmitting && OfflineCollectionMoney.parseFen(amountText) != nil && paymentMethod != nil } /// 更新金额输入,仅接受数字、单个小数点和最多两位小数。 func updateAmount(_ value: String) { guard OfflineCollectionMoney.acceptsEditingText(value) else { return } amountText = value receipt = nil onStateChange?() } /// 选择线下收款方式。 func selectPaymentMethod(_ method: OfflineCollectionPaymentMethod) { paymentMethod = method receipt = nil onStateChange?() } /// 提交一笔线下收款登记,提交中的重复调用会被忽略。 func submit() async { guard !isSubmitting else { return } guard let amountFen = OfflineCollectionMoney.parseFen(amountText) else { onShowMessage?(OfflineCollectionError.invalidAmount.localizedDescription) return } guard let paymentMethod else { onShowMessage?("请选择收款方式") return } isSubmitting = true onStateChange?() defer { isSubmitting = false onStateChange?() } do { let result = try await service.createOfflineCollection( amountFen: amountFen, paymentMethod: paymentMethod, context: context ) receipt = result onRegistrationSuccess?(result) } catch { onShowMessage?(error.localizedDescription) } } /// 登记成功后开始下一笔,清空金额并保留本次收款方式。 func startAnotherRegistration() { amountText = "" receipt = nil onStateChange?() } } /// 日清页的补缴处理状态。 enum OfflineSettlementPresentationState: Sendable, Equatable { case idle case processing case success(OfflineSettlementBatch) case failed(String) } /// 线下收款日清页 ViewModel,负责单营业日汇总、快照冻结和全额补缴。 final class OfflineCollectionDailyViewModel { private(set) var businessDate: String private(set) var summary: OfflineDailySummary private(set) var records: [OfflineCollectionRecord] = [] private(set) var batches: [OfflineSettlementBatch] = [] private(set) var availableBusinessDates: [String] = [] private(set) var settlementState: OfflineSettlementPresentationState = .idle private(set) var preparedRequest: OfflineSettlementRequest? let context: OfflineCollectionContext let service: OfflineCollectionMockService var onStateChange: (() -> Void)? /// 使用指定营业日、收款上下文和 Mock 服务创建日清 ViewModel。 init( businessDate: String, context: OfflineCollectionContext = .current(), service: OfflineCollectionMockService = .shared ) { self.businessDate = businessDate self.context = context self.service = service summary = OfflineDailySummary.make(businessDate: businessDate, records: []) load() } /// 当前查看的是否为今日营业日。 var isToday: Bool { businessDate == service.todayBusinessDate } /// 当前是否可以发起补缴。 var canSettle: Bool { summary.pendingAmountFen > 0 && summary.pendingCount > 0 && settlementState != .processing } /// 从本地 Mock 服务加载当前营业日的全部数据。 func load() { records = service.getDailyRecords(date: businessDate, context: context) summary = service.getDailySummary(date: businessDate, context: context) batches = service.getSettlementBatches(date: businessDate, context: context) availableBusinessDates = service.getAvailableBusinessDates(context: context) onStateChange?() } /// 切换至已存在数据或今日的营业日。 func selectBusinessDate(_ date: String) { guard availableBusinessDates.contains(date), settlementState != .processing else { return } businessDate = date preparedRequest = nil settlementState = .idle load() } /// 按页面当前待补缴记录冻结一份不可编辑的补缴快照。 func prepareSettlement() throws -> OfflineSettlementRequest { guard canSettle else { throw OfflineCollectionError.noPendingRecords } let pendingRecords = records.filter { $0.status == .pending || $0.status == .overdue } let amountFen = pendingRecords.reduce(0) { $0 + $1.amountFen } guard pendingRecords.count == summary.pendingCount, amountFen == summary.pendingAmountFen else { throw OfflineCollectionError.summaryMismatch } let request = OfflineSettlementRequest( businessDate: businessDate, recordIds: pendingRecords.map(\.id), amountFen: amountFen, clientRequestId: "LOCAL-\(UUID().uuidString.uppercased())" ) preparedRequest = request return request } /// 取消未提交的补缴确认快照。 func cancelPreparedSettlement() { guard settlementState != .processing else { return } preparedRequest = nil } /// 提交已冻结的补缴快照,失败后保留快照以便原样重试。 func confirmSettlement() async { guard settlementState != .processing else { return } guard let request = preparedRequest else { settlementState = .failed(OfflineCollectionError.noPendingRecords.localizedDescription) onStateChange?() return } settlementState = .processing onStateChange?() do { let batch = try await service.settleDailyCollections(request: request, context: context) preparedRequest = nil settlementState = .success(batch) load() } catch { settlementState = .failed(error.localizedDescription) load() } } /// 将成功或失败反馈恢复为可操作状态。 func clearSettlementFeedback() { guard settlementState != .processing else { return } settlementState = .idle onStateChange?() } /// 开发环境下将下一次 Mock 补缴设为失败。 func simulateNextSettlementFailure() { service.simulateNextSettlementFailure() } }