feat: 完成9.7线下收款登记与日清
This commit is contained in:
@@ -0,0 +1,275 @@
|
||||
import Foundation
|
||||
|
||||
/// 收款首页线下收款区域的加载状态与业务数据。
|
||||
final class OfflineCollectionHomeViewModel {
|
||||
private(set) var statistics: OfflineCollectionStatisticsResponse?
|
||||
private(set) var isLoading = false
|
||||
private(set) var errorMessage: String?
|
||||
|
||||
let context: OfflineCollectionContext
|
||||
private let api: any OfflineCollectionServing
|
||||
var onStateChange: (() -> Void)?
|
||||
|
||||
init(context: OfflineCollectionContext = .current(), api: any OfflineCollectionServing) {
|
||||
self.context = context
|
||||
self.api = api
|
||||
}
|
||||
|
||||
var todayBusinessDate: String? { statistics?.today.date }
|
||||
var todaySummary: OfflineDailySummary {
|
||||
statistics?.todaySummary ?? .empty(todayBusinessDate ?? "")
|
||||
}
|
||||
var overdueDates: [OfflinePendingDate] {
|
||||
guard let statistics else { return [] }
|
||||
return statistics.pending.dates.filter { $0.date < statistics.today.date }
|
||||
}
|
||||
var earliestOverdueBusinessDate: String? { overdueDates.first?.date }
|
||||
var overdueRecordCount: Int { overdueDates.reduce(0) { $0 + $1.unpaidCount } }
|
||||
var overdueAmountFen: Int { overdueDates.reduce(0) { $0 + $1.unpaidAmountFen } }
|
||||
|
||||
/// 从真实统计接口刷新首页卡片。
|
||||
func load() async {
|
||||
guard context.scenicId > 0 else {
|
||||
errorMessage = OfflineCollectionError.missingScenic.localizedDescription
|
||||
onStateChange?()
|
||||
return
|
||||
}
|
||||
isLoading = true
|
||||
errorMessage = nil
|
||||
onStateChange?()
|
||||
do {
|
||||
statistics = try await api.statistics(scenicId: context.scenicId)
|
||||
} catch {
|
||||
errorMessage = error.localizedDescription
|
||||
}
|
||||
isLoading = false
|
||||
onStateChange?()
|
||||
}
|
||||
}
|
||||
|
||||
/// 线下收款登记页 ViewModel,负责表单校验和登记后的统计刷新。
|
||||
final class OfflineCollectionRegistrationViewModel {
|
||||
private(set) var amountText = ""
|
||||
private(set) var paymentMethod: OfflineCollectionPaymentMethod = .wechat
|
||||
private(set) var isSubmitting = false
|
||||
|
||||
let context: OfflineCollectionContext
|
||||
let api: any OfflineCollectionServing
|
||||
var onStateChange: (() -> Void)?
|
||||
var onShowMessage: ((String) -> Void)?
|
||||
var onRegistrationSuccess: ((OfflineCollectionRegistrationReceipt) -> Void)?
|
||||
|
||||
init(context: OfflineCollectionContext = .current(), api: any OfflineCollectionServing) {
|
||||
self.context = context
|
||||
self.api = api
|
||||
}
|
||||
|
||||
var canSubmit: Bool {
|
||||
!isSubmitting && context.scenicId > 0 && OfflineCollectionMoney.parseFen(amountText) != nil
|
||||
}
|
||||
|
||||
func updateAmount(_ value: String) {
|
||||
guard OfflineCollectionMoney.acceptsEditingText(value) else { return }
|
||||
guard value != amountText else { return }
|
||||
amountText = value
|
||||
onStateChange?()
|
||||
}
|
||||
|
||||
func selectPaymentMethod(_ method: OfflineCollectionPaymentMethod) {
|
||||
guard method != paymentMethod else { return }
|
||||
paymentMethod = method
|
||||
onStateChange?()
|
||||
}
|
||||
|
||||
/// 按后端现有契约提交一笔线下收款。
|
||||
func submit() async {
|
||||
guard !isSubmitting else { return }
|
||||
guard context.scenicId > 0 else {
|
||||
onShowMessage?(OfflineCollectionError.missingScenic.localizedDescription)
|
||||
return
|
||||
}
|
||||
guard let amountFen = OfflineCollectionMoney.parseFen(amountText) else {
|
||||
onShowMessage?(OfflineCollectionError.invalidAmount.localizedDescription)
|
||||
return
|
||||
}
|
||||
|
||||
isSubmitting = true
|
||||
onStateChange?()
|
||||
defer {
|
||||
isSubmitting = false
|
||||
onStateChange?()
|
||||
}
|
||||
|
||||
do {
|
||||
let response = try await api.register(OfflineCollectionRegisterRequest(
|
||||
scenicId: context.scenicId,
|
||||
amount: OfflineCollectionMoney.apiAmount(amountFen),
|
||||
payMethod: paymentMethod.rawValue
|
||||
))
|
||||
let statistics = try? await api.statistics(scenicId: context.scenicId)
|
||||
let responseDate = String(response.createdAt.prefix(10))
|
||||
onRegistrationSuccess?(OfflineCollectionRegistrationReceipt(
|
||||
record: response.record,
|
||||
businessDate: statistics?.today.date ?? responseDate,
|
||||
summary: statistics?.todaySummary
|
||||
))
|
||||
} catch {
|
||||
onShowMessage?(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
func startAnotherRegistration() {
|
||||
amountText = ""
|
||||
onStateChange?()
|
||||
}
|
||||
}
|
||||
|
||||
/// 日清页的补缴呈现状态。
|
||||
enum OfflineSettlementPresentationState: Sendable, Equatable {
|
||||
case idle
|
||||
case processing
|
||||
case success(OfflineSettlementResult)
|
||||
case failed(String, canRetry: Bool)
|
||||
}
|
||||
|
||||
/// 日清页 ViewModel,管理服务端营业日、日期标记、详情请求与补缴快照。
|
||||
final class OfflineCollectionDailyViewModel {
|
||||
private(set) var businessDate: String
|
||||
private(set) var serverToday: String
|
||||
private(set) var summary: OfflineDailySummary
|
||||
private(set) var records: [OfflineCollectionRecord] = []
|
||||
private(set) var pendingDates: Set<String> = []
|
||||
private(set) var isLoading = false
|
||||
private(set) var errorMessage: String?
|
||||
private(set) var settlementState: OfflineSettlementPresentationState = .idle
|
||||
private(set) var preparedRequest: OfflineSettlementRequest?
|
||||
|
||||
let context: OfflineCollectionContext
|
||||
private let api: any OfflineCollectionServing
|
||||
private var detailRequestVersion = 0
|
||||
var onStateChange: (() -> Void)?
|
||||
|
||||
init(businessDate: String, context: OfflineCollectionContext = .current(), api: any OfflineCollectionServing) {
|
||||
self.businessDate = businessDate
|
||||
serverToday = businessDate
|
||||
summary = .empty(businessDate)
|
||||
self.context = context
|
||||
self.api = api
|
||||
}
|
||||
|
||||
var isToday: Bool { businessDate == serverToday }
|
||||
var canSettle: Bool {
|
||||
summary.pendingAmountFen > 0 && summary.pendingCount > 0 && settlementState != .processing && !isLoading
|
||||
}
|
||||
|
||||
/// 首次进入或页面重新出现时刷新统计和当前详情。
|
||||
func refresh() async {
|
||||
guard context.scenicId > 0 else {
|
||||
errorMessage = OfflineCollectionError.missingScenic.localizedDescription
|
||||
onStateChange?()
|
||||
return
|
||||
}
|
||||
isLoading = true
|
||||
errorMessage = nil
|
||||
onStateChange?()
|
||||
do {
|
||||
let statistics = try await api.statistics(scenicId: context.scenicId)
|
||||
serverToday = statistics.today.date
|
||||
pendingDates = Set(statistics.pending.dates.map(\.date))
|
||||
if businessDate.isEmpty || businessDate > serverToday { businessDate = serverToday }
|
||||
await loadDetails(for: businessDate)
|
||||
} catch {
|
||||
isLoading = false
|
||||
errorMessage = error.localizedDescription
|
||||
onStateChange?()
|
||||
}
|
||||
}
|
||||
|
||||
/// 选择任意不晚于服务端今日的日期,并只接受最后一次请求结果。
|
||||
func selectBusinessDate(_ date: String) async {
|
||||
guard date <= serverToday, settlementState != .processing else { return }
|
||||
businessDate = date
|
||||
preparedRequest = nil
|
||||
settlementState = .idle
|
||||
await loadDetails(for: date)
|
||||
}
|
||||
|
||||
private func loadDetails(for date: String) async {
|
||||
detailRequestVersion += 1
|
||||
let version = detailRequestVersion
|
||||
isLoading = true
|
||||
errorMessage = nil
|
||||
records = []
|
||||
summary = .empty(date)
|
||||
onStateChange?()
|
||||
do {
|
||||
let details = try await api.details(scenicId: context.scenicId, date: date)
|
||||
guard version == detailRequestVersion, date == businessDate else { return }
|
||||
summary = details.summary
|
||||
records = details.records(serverToday: serverToday)
|
||||
isLoading = false
|
||||
onStateChange?()
|
||||
} catch is CancellationError {
|
||||
return
|
||||
} catch {
|
||||
guard version == detailRequestVersion, date == businessDate else { return }
|
||||
isLoading = false
|
||||
errorMessage = error.localizedDescription
|
||||
onStateChange?()
|
||||
}
|
||||
}
|
||||
|
||||
func prepareSettlement() throws -> OfflineSettlementRequest {
|
||||
guard canSettle else { throw OfflineCollectionError.noPendingRecords }
|
||||
let request = OfflineSettlementRequest(
|
||||
scenicId: context.scenicId,
|
||||
businessDate: businessDate,
|
||||
amountFen: summary.pendingAmountFen,
|
||||
pendingCount: summary.pendingCount
|
||||
)
|
||||
preparedRequest = request
|
||||
return request
|
||||
}
|
||||
|
||||
func cancelPreparedSettlement() {
|
||||
guard settlementState != .processing else { return }
|
||||
preparedRequest = nil
|
||||
}
|
||||
|
||||
/// 按后端现有契约补缴指定日期;失败时允许用户再次提交。
|
||||
func confirmSettlement() async {
|
||||
guard settlementState != .processing, let request = preparedRequest else { return }
|
||||
settlementState = .processing
|
||||
onStateChange?()
|
||||
do {
|
||||
let result = try await api.supplement(request)
|
||||
preparedRequest = nil
|
||||
settlementState = .success(result)
|
||||
await refreshAfterSettlement()
|
||||
} catch {
|
||||
settlementState = .failed(error.localizedDescription, canRetry: true)
|
||||
onStateChange?()
|
||||
}
|
||||
}
|
||||
|
||||
private func refreshAfterSettlement() async {
|
||||
if let statistics = try? await api.statistics(scenicId: context.scenicId) {
|
||||
serverToday = statistics.today.date
|
||||
pendingDates = Set(statistics.pending.dates.map(\.date))
|
||||
}
|
||||
detailRequestVersion += 1
|
||||
let version = detailRequestVersion
|
||||
if let details = try? await api.details(scenicId: context.scenicId, date: businessDate), version == detailRequestVersion {
|
||||
summary = details.summary
|
||||
records = details.records(serverToday: serverToday)
|
||||
}
|
||||
isLoading = false
|
||||
onStateChange?()
|
||||
}
|
||||
|
||||
func clearSettlementFeedback() {
|
||||
guard settlementState != .processing else { return }
|
||||
settlementState = .idle
|
||||
onStateChange?()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user