Add instant payment collection flow aligned with Android.

Implement收款详情与收款记录 pages with pay-code API, QR generation, amount setting, and record grouping so merchants can collect payments from the home quick action.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-07 09:29:03 +08:00
parent 731de7d7f0
commit d8329d19fd
17 changed files with 1548 additions and 1 deletions

View File

@ -0,0 +1,94 @@
//
// PaymentCollectionDetailsViewModelTests.swift
// suixinkanTests
//
import XCTest
@testable import suixinkan
/// ViewModel
@MainActor
final class PaymentCollectionDetailsViewModelTests: XCTestCase {
private var appStore: AppStore!
override func setUp() {
let defaults = UserDefaults(suiteName: "PaymentCollectionDetailsViewModelTests")!
defaults.removePersistentDomain(forName: "PaymentCollectionDetailsViewModelTests")
appStore = AppStore(defaults: defaults)
appStore.userId = "10001"
appStore.currentScenicId = 10
appStore.currentScenicName = "测试景区"
}
override func tearDown() {
appStore.logout()
super.tearDown()
}
func testPaymentAmountValidatorRejectsEmptyAmount() {
XCTAssertEqual(PaymentAmountValidator.validationMessage(for: ""), "请输入收款金额")
}
func testPaymentAmountValidatorRejectsZeroAmount() {
XCTAssertEqual(PaymentAmountValidator.validationMessage(for: "0"), "请输入有效的收款金额")
}
func testPaymentAmountValidatorRejectsMoreThanTwoDecimals() {
XCTAssertEqual(PaymentAmountValidator.validationMessage(for: "1.234"), "金额最多支持两位小数")
}
func testPaymentAmountValidatorAcceptsValidAmount() {
XCTAssertNil(PaymentAmountValidator.validationMessage(for: "12.50"))
}
func testShowSetAmountDialogRequiresPayCode() {
let viewModel = PaymentCollectionDetailsViewModel(appStore: appStore)
var message: String?
viewModel.onShowMessage = { message = $0 }
viewModel.showSetAmountDialog()
XCTAssertEqual(message, "请先获取收款码")
XCTAssertFalse(viewModel.showAmountDialog)
}
func testConfirmAmountBuildsDynamicURL() async throws {
let payCodeJSON = """
{"code":100000,"msg":"success","data":{"static_pay_url":"https://pay.example.com/static","dynamic_pay_url":"https://pay.example.com/dynamic"}}
""".data(using: .utf8)!
let session = MockURLSession(responses: [payCodeJSON])
let api = PaymentAPI(client: APIClient(environment: .testing, session: session))
let viewModel = PaymentCollectionDetailsViewModel(appStore: appStore)
await viewModel.loadPayCode(api: api)
viewModel.updateAmount("88.50")
viewModel.updateRemark("")
viewModel.confirmAmount()
XCTAssertTrue(viewModel.hasPayCode)
XCTAssertFalse(viewModel.showAmountDialog)
}
func testToggleReceiveVoicePersistsInAppStore() {
let viewModel = PaymentCollectionDetailsViewModel(appStore: appStore)
viewModel.refreshLocalState()
XCTAssertFalse(viewModel.isVoiceBroadcastOpen)
viewModel.toggleReceiveVoice()
XCTAssertTrue(appStore.isOpenReceiveVoice)
XCTAssertTrue(viewModel.isVoiceBroadcastOpen)
}
func testLoadPayCodeClearsQRWhenScenicMissing() async {
appStore.currentScenicId = 0
let session = MockURLSession(responses: [])
let api = PaymentAPI(client: APIClient(environment: .testing, session: session))
let viewModel = PaymentCollectionDetailsViewModel(appStore: appStore)
await viewModel.loadPayCode(api: api)
XCTAssertFalse(viewModel.hasPayCode)
XCTAssertEqual(session.requests.count, 0)
}
}

View File

@ -0,0 +1,72 @@
//
// PaymentCollectionRecordViewModelTests.swift
// suixinkanTests
//
import XCTest
@testable import suixinkan
/// ViewModel
@MainActor
final class PaymentCollectionRecordViewModelTests: XCTestCase {
func testPaymentRecordGrouperGroupsByCreateDate() {
let response = RepaymentCollectionRecordResponse(
analyse: [
RepaymentCollectionRecordAnalyseItem(date: "2024-01-15", orderCount: 2, orderAmountSum: "150.00"),
RepaymentCollectionRecordAnalyseItem(date: "2024-01-14", orderCount: 1, orderAmountSum: "80.00"),
],
list: [
RepaymentCollectionRecordItem(
orderNumber: "NO001",
userPhone: "13800138000",
orderAmount: "100.00",
createDate: "2024-01-15",
createTime: "10:00:00"
),
RepaymentCollectionRecordItem(
orderNumber: "NO002",
userPhone: "13800138001",
orderAmount: "50.00",
createDate: "2024-01-15",
createTime: "11:00:00"
),
RepaymentCollectionRecordItem(
orderNumber: "NO003",
userPhone: "13800138002",
orderAmount: "80.00",
createDate: "2024-01-14",
createTime: "09:00:00"
),
]
)
let groups = PaymentRecordGrouper.group(response: response)
XCTAssertEqual(groups.count, 2)
XCTAssertEqual(groups[0].analyse.date, "2024-01-15")
XCTAssertEqual(groups[0].items.count, 2)
XCTAssertEqual(groups[1].analyse.date, "2024-01-14")
XCTAssertEqual(groups[1].items.count, 1)
}
func testLoadRecordUsesAPIAndGroupsResult() async {
let defaults = UserDefaults(suiteName: "PaymentCollectionRecordViewModelTests")!
defaults.removePersistentDomain(forName: "PaymentCollectionRecordViewModelTests")
let appStore = AppStore(defaults: defaults)
appStore.currentScenicId = 10
let recordJSON = """
{"code":100000,"msg":"success","data":{"analyse":[{"date":"2024-01-15","order_count":1,"order_amount_sum":"50.00"}],"list":[{"order_number":"NO001","user_phone":"13800138000","order_amount":"50.00","create_date":"2024-01-15","create_time":"10:00:00"}]}}
""".data(using: .utf8)!
let session = MockURLSession(responses: [recordJSON])
let api = PaymentAPI(client: APIClient(environment: .testing, session: session))
let viewModel = PaymentCollectionRecordViewModel(appStore: appStore)
await viewModel.loadRecord(api: api)
XCTAssertEqual(viewModel.groups.count, 1)
XCTAssertEqual(viewModel.groups[0].items.first?.orderNumber, "NO001")
XCTAssertTrue(session.requests.first?.url?.absoluteString.contains("photo-scan-order-list") == true)
}
}

View File

@ -0,0 +1,53 @@
//
// PaymentModelsTests.swift
// suixinkanTests
//
import XCTest
@testable import suixinkan
///
final class PaymentModelsTests: XCTestCase {
func testPayCodeResponseDecodesSnakeCaseFields() throws {
let json = """
{
"static_pay_url": "https://pay.example.com/static",
"dynamic_pay_url": "https://pay.example.com/dynamic"
}
""".data(using: .utf8)!
let response = try JSONDecoder().decode(PayCodeResponse.self, from: json)
XCTAssertEqual(response.staticPayURL, "https://pay.example.com/static")
XCTAssertEqual(response.dynamicPayURL, "https://pay.example.com/dynamic")
}
func testRepaymentCollectionRecordResponseDecodesGroupedFields() throws {
let json = """
{
"analyse": [
{
"date": "2024-01-15",
"order_count": 2,
"order_amount_sum": "100.00"
}
],
"list": [
{
"order_number": "NO001",
"user_phone": "13800138000",
"order_amount": "50.00",
"create_date": "2024-01-15",
"create_time": "10:00:00"
}
]
}
""".data(using: .utf8)!
let response = try JSONDecoder().decode(RepaymentCollectionRecordResponse.self, from: json)
XCTAssertEqual(response.analyse.count, 1)
XCTAssertEqual(response.analyse[0].orderCount, 2)
XCTAssertEqual(response.list[0].orderNumber, "NO001")
}
}