Add order tail flows and migrate queue, message, settlement, and audit modules.

Wire deposit orders, historical shooting, and multi-travel uploads into Orders, and replace home placeholders for queue management, message center, scenic settlement, and withdrawal audit.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-25 13:39:02 +08:00
parent fb8430889b
commit 1a0d1c25f4
63 changed files with 9823 additions and 58 deletions

View File

@ -0,0 +1,167 @@
//
// MessageCenterViewModelTests.swift
// suixinkanTests
//
// Created by Codex on 2026/6/25.
//
import XCTest
@testable import suixinkan
@MainActor
///
final class MessageCenterViewModelTests: XCTestCase {
func testReloadFirstPageMapsMessagesAndPaginationState() async {
let api = MessageCenterMock()
api.listResponses = [
MessageListResponse(hasMore: true, lastId: 101, items: [
MessageEntity(id: 101, type: 1, typeName: "", title: "新订单提醒", content: "有新订单", pushAt: "2026-06-25 10:00", isRead: false),
MessageEntity(id: 100, type: 2, typeName: "核销", title: "", content: "核销完成", pushAt: "2026-06-25 09:00", isRead: true)
])
]
let viewModel = MessageCenterViewModel()
viewModel.selectedFilter = .unread
await viewModel.reloadFirstPage(api: api)
XCTAssertEqual(api.listRequests.map(\.unread), [1])
XCTAssertEqual(viewModel.messages.map(\.id), ["msg_101", "msg_100"])
XCTAssertEqual(viewModel.messages[0].title, "新订单提醒")
XCTAssertEqual(viewModel.messages[0].type, .order)
XCTAssertEqual(viewModel.messages[1].title, "核销")
XCTAssertEqual(viewModel.messages[1].type, .writeOff)
XCTAssertEqual(viewModel.unreadCount, 1)
XCTAssertTrue(viewModel.hasMoreMessages)
XCTAssertEqual(viewModel.lastId, 101)
}
func testLoadMoreDeduplicatesSortsAndStopsAtTail() async {
let api = MessageCenterMock()
api.listResponses = [
MessageListResponse(hasMore: true, lastId: 101, items: [
MessageEntity(id: 101, type: 1, title: "A", content: "A", pushAt: "2026-06-25 10:00"),
MessageEntity(id: 100, type: 3, title: "B", content: "B", pushAt: "2026-06-25 09:00")
]),
MessageListResponse(hasMore: false, lastId: 99, items: [
MessageEntity(id: 100, type: 3, title: "B2", content: "B2", pushAt: "2026-06-25 09:00"),
MessageEntity(id: 99, type: 0, title: "C", content: "C", pushAt: "2026-06-24 08:00")
])
]
let viewModel = MessageCenterViewModel()
await viewModel.reloadFirstPage(api: api)
await viewModel.loadMore(api: api)
await viewModel.loadMore(api: api)
XCTAssertEqual(api.listRequests.map(\.lastId), [0, 101])
XCTAssertEqual(viewModel.messages.map(\.id), ["msg_101", "msg_100", "msg_99"])
XCTAssertEqual(viewModel.messages[1].title, "B2")
XCTAssertFalse(viewModel.hasMoreMessages)
XCTAssertEqual(viewModel.lastId, 99)
}
func testReloadFailureClearsStaleMessagesAndPaging() async {
let api = MessageCenterMock()
api.listResponses = [
MessageListResponse(hasMore: true, lastId: 101, items: [
MessageEntity(id: 101, type: 1, title: "A")
])
]
let viewModel = MessageCenterViewModel()
await viewModel.reloadFirstPage(api: api)
api.listError = TestError.sample
await viewModel.reloadFirstPage(api: api)
XCTAssertTrue(viewModel.messages.isEmpty)
XCTAssertFalse(viewModel.hasMoreMessages)
XCTAssertEqual(viewModel.lastId, 0)
XCTAssertTrue(viewModel.loadFailed)
}
func testMarkReadAllReadAndDeleteUpdateOnlyAfterSuccess() async throws {
let api = MessageCenterMock()
api.listResponses = [
MessageListResponse(items: [
MessageEntity(id: 101, type: 1, title: "A", isRead: false),
MessageEntity(id: 100, type: 0, title: "B", isRead: false)
])
]
let viewModel = MessageCenterViewModel()
await viewModel.reloadFirstPage(api: api)
try await viewModel.markAsRead(api: api, item: try XCTUnwrap(viewModel.messages.first))
XCTAssertEqual(api.readIds, [101])
XCTAssertEqual(viewModel.messages.map(\.isRead), [true, false])
api.readIds = []
try await viewModel.markAllAsRead(api: api)
XCTAssertEqual(api.readIds, [100])
XCTAssertTrue(viewModel.messages.allSatisfy(\.isRead))
let item = try XCTUnwrap(viewModel.messages.first)
try await viewModel.deleteMessage(api: api, item: item)
XCTAssertEqual(api.deleteIds, [101])
XCTAssertFalse(viewModel.messages.contains(where: { $0.id == item.id }))
}
func testFailedReadAndDeleteKeepLocalState() async throws {
let api = MessageCenterMock()
api.listResponses = [MessageListResponse(items: [MessageEntity(id: 101, type: 1, title: "A", isRead: false)])]
let viewModel = MessageCenterViewModel()
await viewModel.reloadFirstPage(api: api)
let item = try XCTUnwrap(viewModel.messages.first)
api.readError = TestError.sample
await XCTAssertThrowsErrorAsync(try await viewModel.markAsRead(api: api, item: item))
XCTAssertFalse(try XCTUnwrap(viewModel.messages.first).isRead)
api.readError = nil
api.deleteError = TestError.sample
await XCTAssertThrowsErrorAsync(try await viewModel.deleteMessage(api: api, item: item))
XCTAssertEqual(viewModel.messages.map(\.id), [item.id])
}
}
@MainActor
private final class MessageCenterMock: MessageCenterServing {
var listResponses: [MessageListResponse] = []
var listError: Error?
var readError: Error?
var deleteError: Error?
var readIds: [Int] = []
var deleteIds: [Int] = []
private(set) var listRequests: [(lastId: Int, limit: Int, unread: Int)] = []
func messageList(lastId: Int, limit: Int, unread: Int) async throws -> MessageListResponse {
listRequests.append((lastId, limit, unread))
if let listError { throw listError }
return listResponses.isEmpty ? MessageListResponse() : listResponses.removeFirst()
}
func messageRead(id: Int) async throws {
if let readError { throw readError }
readIds.append(id)
}
func messageDelete(id: Int) async throws {
if let deleteError { throw deleteError }
deleteIds.append(id)
}
}
private enum TestError: LocalizedError {
case sample
var errorDescription: String? { "测试错误" }
}
private func XCTAssertThrowsErrorAsync(
_ expression: @autoclosure () async throws -> Void,
file: StaticString = #filePath,
line: UInt = #line
) async {
do {
try await expression()
XCTFail("Expected error", file: file, line: line)
} catch {}
}