Implement orders tab aligned with Android, including scan verify flows.

Add role-based order/deposit lists, AVFoundation QR scanning, verify dialogs, and ViewModel tests so photographers, scenic admins, and store admins match Android behavior.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-06 16:59:53 +08:00
parent 6492f80ef0
commit 31d2b6094b
27 changed files with 4226 additions and 17 deletions

View File

@ -0,0 +1,64 @@
//
// DepositOrderListViewModelTests.swift
// suixinkanTests
//
import XCTest
@testable import suixinkan
@MainActor
final class DepositOrderListViewModelTests: XCTestCase {
override func tearDown() {
AppStore.shared.currentStoreId = 0
AppStore.shared.currentScenicId = 0
super.tearDown()
}
func testStoreFilterMapsPaidStatusToAPI20() {
XCTAssertEqual(DepositOrderListViewModel.apiOrderStatus(forTabIndex: 18), 20)
XCTAssertEqual(DepositOrderListViewModel.apiOrderStatus(forTabIndex: 30), 30)
XCTAssertNil(DepositOrderListViewModel.apiOrderStatus(forTabIndex: 0))
}
func testStoreFilterLabelUsesPaidText() {
let viewModel = DepositOrderListViewModel()
viewModel.onFilterChange(tab: 18)
XCTAssertEqual(viewModel.filterLabel, "已支付")
}
func testPhoneValidation() {
XCTAssertTrue(DepositOrderListViewModel.isValidPhone("13800138000"))
XCTAssertFalse(DepositOrderListViewModel.isValidPhone("12345"))
XCTAssertFalse(DepositOrderListViewModel.isValidPhone("23800138000"))
}
func testStoreIdZeroAbortsWithMessage() async {
AppStore.shared.currentStoreId = 0
let viewModel = DepositOrderListViewModel()
var message: String?
viewModel.onShowMessage = { message = $0 }
let session = MockURLSession(responses: [])
let api = OrderAPI(client: APIClient(environment: .testing, session: session))
await viewModel.refreshOrderList(api: api)
XCTAssertTrue(viewModel.orderList.isEmpty)
XCTAssertEqual(message, "店铺信息异常,请先选择店铺")
}
func testCanOpenDetailOnlyForType19() throws {
let viewModel = DepositOrderListViewModel()
let json = """
{"order_number":"A","order_type":19,"order_status":20}
""".data(using: .utf8)!
let item = try JSONDecoder().decode(StoreOrderItem.self, from: json)
XCTAssertTrue(viewModel.canOpenDetail(for: item))
let otherJSON = """
{"order_number":"B","order_type":4,"order_status":20}
""".data(using: .utf8)!
let other = try JSONDecoder().decode(StoreOrderItem.self, from: otherJSON)
XCTAssertFalse(viewModel.canOpenDetail(for: other))
}
}

View File

@ -0,0 +1,85 @@
//
// OrderListViewModelTests.swift
// suixinkanTests
//
import XCTest
@testable import suixinkan
@MainActor
final class OrderListViewModelTests: XCTestCase {
override func tearDown() {
AppStore.shared.roleCode = ""
AppStore.shared.roleName = ""
AppStore.shared.currentScenicId = 0
super.tearDown()
}
func testOrderStatusQueryMapping() {
XCTAssertNil(OrderListQueryBuilder.orderStatusQuery(tabIndex: 0))
XCTAssertEqual(OrderListQueryBuilder.orderStatusQuery(tabIndex: 18), "18")
XCTAssertEqual(OrderListQueryBuilder.orderStatusQuery(tabIndex: 30), "30")
}
func testIsRefinedQueryMapping() {
XCTAssertNil(OrderListQueryBuilder.isRefinedQuery(rTabIndex: 0))
XCTAssertEqual(OrderListQueryBuilder.isRefinedQuery(rTabIndex: 1), 1)
XCTAssertEqual(OrderListQueryBuilder.isRefinedQuery(rTabIndex: 2), 2)
}
func testPhotographerFilterLabels() {
let viewModel = OrderListViewModel()
viewModel.onFilterChange(orderStatus: 18, isRefined: 0)
XCTAssertEqual(viewModel.filterLabel, "已付定金")
viewModel.onFilterChange(orderStatus: 0, isRefined: 1)
XCTAssertEqual(viewModel.filterLabel, "需要精修")
}
func testRoleRoutingPath() {
AppStore.shared.roleCode = AppRoleCode.scenicAdmin.rawValue
let scenicVM = OrderListViewModel()
XCTAssertEqual(scenicVM.listAPIPath, "/api/app/scenic-admin/order/list")
XCTAssertTrue(scenicVM.isScenicAdminReadOnly)
AppStore.shared.roleCode = AppRoleCode.photographer.rawValue
let photoVM = OrderListViewModel()
XCTAssertEqual(photoVM.listAPIPath, "/api/yf-handset-app/photog/order/listv2")
XCTAssertFalse(photoVM.isScenicAdminReadOnly)
}
func testLoadMoreBoundary() {
let viewModel = OrderListViewModel()
XCTAssertFalse(viewModel.shouldLoadMore(at: 0))
}
func testOrderEntityDecodesPhotoTravel() throws {
let json = """
{
"code": 100000,
"msg": "success",
"data": {
"total": 1,
"list": [{
"order_number": "NO001",
"order_status": 18,
"order_status_name": "",
"actual_pay_amount": "99.00",
"photo_travel": {
"order_photo_num": 10,
"order_video_num": 1,
"need_check_in": true,
"can_cancel_order": true
}
}]
}
}
""".data(using: .utf8)!
let envelope = try JSONDecoder().decode(APIEnvelope<OrderListResponse<OrderEntity>>.self, from: json)
let order = try XCTUnwrap(envelope.data?.list.first)
XCTAssertEqual(order.orderNumber, "NO001")
XCTAssertEqual(order.photoTravel?.orderPhotoNum, 10)
XCTAssertEqual(order.photoTravel?.needCheckIn, true)
}
}