249 lines
8.8 KiB
Swift
249 lines
8.8 KiB
Swift
//
|
|
// 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()
|
|
}
|
|
}
|