// // OfflineCollectionFeatureTests.swift // suixinkanTests // import XCTest @testable import suixinkan /// 线下收款登记、日汇总、失败回滚与幂等补缴流程测试。 final class OfflineCollectionFeatureTests: XCTestCase { private var defaults: UserDefaults! private var calendar: Calendar! private var fixedNow: Date! private var service: OfflineCollectionMockService! private let context = OfflineCollectionContext( collectorId: "U10086", collectorName: "张三", storeId: "S1001", storeName: "那拉提旅拍一店", scenicId: "SC001", scenicName: "那拉提景区" ) override func setUp() { super.setUp() let suiteName = "OfflineCollectionFeatureTests" defaults = UserDefaults(suiteName: suiteName)! defaults.removePersistentDomain(forName: suiteName) calendar = Calendar(identifier: .gregorian) calendar.timeZone = TimeZone(identifier: "Asia/Shanghai")! fixedNow = calendar.date(from: DateComponents( calendar: calendar, timeZone: calendar.timeZone, year: 2026, month: 8, day: 19, hour: 20, minute: 5 ))! let now = fixedNow! service = OfflineCollectionMockService( defaults: defaults, calendar: calendar, nowProvider: { now }, requestDelayNanoseconds: 0 ) } override func tearDown() { defaults.removePersistentDomain(forName: "OfflineCollectionFeatureTests") service = nil calendar = nil fixedNow = nil defaults = nil super.tearDown() } func testMoneyParserUsesIntegerFenAndEnforcesRange() { XCTAssertEqual(OfflineCollectionMoney.parseFen(".5"), 50) XCTAssertEqual(OfflineCollectionMoney.parseFen("299"), 29_900) XCTAssertEqual(OfflineCollectionMoney.parseFen("99999.99"), 9_999_999) XCTAssertNil(OfflineCollectionMoney.parseFen("")) XCTAssertNil(OfflineCollectionMoney.parseFen("0")) XCTAssertNil(OfflineCollectionMoney.parseFen("-1")) XCTAssertNil(OfflineCollectionMoney.parseFen("1.234")) XCTAssertNil(OfflineCollectionMoney.parseFen("100000")) XCTAssertNil(OfflineCollectionMoney.parseFen("1e2")) XCTAssertEqual(OfflineCollectionMoney.display(29_900), "¥299.00") } func testDefaultMockDataContainsTodayAndHistoricalOverdueRecords() { let today = service.getDailySummary(date: "2026-08-19", context: context) let overdue = service.getOverdueSummaries(context: context) XCTAssertEqual(today.totalCount, 3) XCTAssertEqual(today.totalAmountFen, 89_700) XCTAssertEqual(today.pendingAmountFen, 89_700) XCTAssertEqual(overdue.count, 1) XCTAssertEqual(overdue.first?.businessDate, "2026-08-18") XCTAssertEqual(overdue.first?.pendingCount, 2) XCTAssertEqual(overdue.first?.pendingAmountFen, 50_000) XCTAssertEqual(overdue.first?.hasOverdue, true) } func testCreateCollectionUpdatesSummaryAndPersists() async throws { let receipt = try await service.createOfflineCollection( amountFen: 29_900, paymentMethod: .wechat, context: context ) XCTAssertEqual(receipt.record.status, .pending) XCTAssertEqual(receipt.summary.totalCount, 4) XCTAssertEqual(receipt.summary.totalAmountFen, 119_600) XCTAssertEqual(receipt.summary.pendingAmountFen, 119_600) let now = fixedNow! let reloaded = OfflineCollectionMockService( defaults: defaults, calendar: calendar, nowProvider: { now }, requestDelayNanoseconds: 0 ) XCTAssertEqual(reloaded.getDailySummary(date: "2026-08-19", context: context), receipt.summary) } func testSuccessfulSettlementGeneratesIndependentBatchAndSettlesSnapshot() async throws { let records = service.getDailyRecords(date: "2026-08-19", context: context) let request = OfflineSettlementRequest( businessDate: "2026-08-19", recordIds: records.map(\.id), amountFen: records.reduce(0) { $0 + $1.amountFen }, clientRequestId: "LOCAL-SUCCESS-001" ) let batch = try await service.settleDailyCollections(request: request, context: context) let summary = service.getDailySummary(date: "2026-08-19", context: context) let settledRecords = service.getDailyRecords(date: "2026-08-19", context: context) XCTAssertEqual(batch.amountFen, 89_700) XCTAssertEqual(batch.recordCount, 3) XCTAssertEqual(batch.transactionType, "OFFLINE_COLLECTION_SETTLEMENT") XCTAssertEqual(summary.totalAmountFen, summary.settledAmountFen + summary.pendingAmountFen) XCTAssertEqual(summary.pendingAmountFen, 0) XCTAssertTrue(settledRecords.allSatisfy { $0.status == .settled }) XCTAssertTrue(settledRecords.allSatisfy { $0.settlementBatchId == batch.id }) } func testFailedSettlementDoesNotCreateBatchOrChangeRecords() async throws { let before = service.getDailyRecords(date: "2026-08-19", context: context) let request = OfflineSettlementRequest( businessDate: "2026-08-19", recordIds: before.map(\.id), amountFen: before.reduce(0) { $0 + $1.amountFen }, clientRequestId: "LOCAL-FAILURE-001" ) service.simulateNextSettlementFailure() do { _ = try await service.settleDailyCollections(request: request, context: context) XCTFail("应返回 Mock 补缴失败") } catch { XCTAssertEqual(error as? OfflineCollectionError, .settlementFailed) } XCTAssertEqual(service.getDailyRecords(date: "2026-08-19", context: context), before) XCTAssertTrue(service.getSettlementBatches(date: "2026-08-19", context: context).isEmpty) XCTAssertEqual(service.getDailySummary(date: "2026-08-19", context: context).pendingAmountFen, 89_700) } func testSettlementOnlySettlesFrozenRecordSnapshot() async throws { let frozenRecords = service.getDailyRecords(date: "2026-08-19", context: context) let request = OfflineSettlementRequest( businessDate: "2026-08-19", recordIds: frozenRecords.map(\.id), amountFen: frozenRecords.reduce(0) { $0 + $1.amountFen }, clientRequestId: "LOCAL-SNAPSHOT-001" ) let newReceipt = try await service.createOfflineCollection( amountFen: 12_300, paymentMethod: .cash, context: context ) _ = try await service.settleDailyCollections(request: request, context: context) let records = service.getDailyRecords(date: "2026-08-19", context: context) let newRecord = try XCTUnwrap(records.first(where: { $0.id == newReceipt.record.id })) XCTAssertEqual(newRecord.status, .pending) XCTAssertEqual(service.getDailySummary(date: "2026-08-19", context: context).pendingAmountFen, 12_300) } func testSameClientRequestIdDoesNotCreateDuplicateBatch() async throws { let records = service.getDailyRecords(date: "2026-08-19", context: context) let request = OfflineSettlementRequest( businessDate: "2026-08-19", recordIds: records.map(\.id), amountFen: records.reduce(0) { $0 + $1.amountFen }, clientRequestId: "LOCAL-IDEMPOTENT-001" ) let first = try await service.settleDailyCollections(request: request, context: context) let second = try await service.settleDailyCollections(request: request, context: context) XCTAssertEqual(first, second) XCTAssertEqual(service.getSettlementBatches(date: "2026-08-19", context: context).count, 1) } func testRegistrationViewModelValidatesAndSubmitsForm() async { let viewModel = OfflineCollectionRegistrationViewModel(context: context, service: service) var successReceipt: OfflineCollectionRegistrationReceipt? viewModel.onRegistrationSuccess = { successReceipt = $0 } XCTAssertEqual(viewModel.paymentMethod, .wechat) viewModel.updateAmount("299") XCTAssertTrue(viewModel.canSubmit) await viewModel.submit() XCTAssertEqual(successReceipt?.record.amountFen, 29_900) XCTAssertEqual(successReceipt?.record.paymentMethod, .wechat) XCTAssertFalse(viewModel.isSubmitting) XCTAssertEqual(successReceipt?.summary.totalCount, 4) } }