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:
94
suixinkanTests/PaymentCollectionDetailsViewModelTests.swift
Normal file
94
suixinkanTests/PaymentCollectionDetailsViewModelTests.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
72
suixinkanTests/PaymentCollectionRecordViewModelTests.swift
Normal file
72
suixinkanTests/PaymentCollectionRecordViewModelTests.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
53
suixinkanTests/PaymentModelsTests.swift
Normal file
53
suixinkanTests/PaymentModelsTests.swift
Normal 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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user