// // StatisticsViewModelTests.swift // suixinkanTests // import XCTest @testable import suixinkan @MainActor /// 数据统计 ViewModel 测试。 final class StatisticsViewModelTests: XCTestCase { override func tearDown() { AppStore.shared.roleCode = "" AppStore.shared.roleName = "" AppStore.shared.currentScenicId = 0 AppStore.shared.currentStoreId = 0 super.tearDown() } func testStatisticsSummaryDecodesRefundAmountAsString() throws { let json = """ { "code": 100000, "msg": "success", "data": { "order_amount_sum": "0.00", "order_count": 0, "order_price_avg": "0.00", "received_amount_sum": "0.00", "refund_amount_sum": "0.00" } } """.data(using: .utf8)! let envelope = try JSONDecoder().decode(APIEnvelope.self, from: json) XCTAssertEqual(envelope.data?.refundAmountSum, "0.00") XCTAssertEqual(envelope.data?.orderAmountSum, "0.00") } func testRangeValueMapping() { let viewModel = StatisticsViewModel() XCTAssertEqual(viewModel.rangeValue(for: "今日"), "1") XCTAssertEqual(viewModel.rangeValue(for: "昨日"), "2") XCTAssertEqual(viewModel.rangeValue(for: "7日"), "3") XCTAssertEqual(viewModel.rangeValue(for: "本月"), "4") } func testFormattedSelectedPeriodTimeForToday() { let viewModel = StatisticsViewModel() let text = viewModel.formattedSelectedPeriodTime(for: "今日") let today = formattedToday() XCTAssertEqual(text, "\(today)至\(today)") } func testFormattedSelectedPeriodTimeForThisMonth() { let viewModel = StatisticsViewModel() let text = viewModel.formattedSelectedPeriodTime(for: "本月") XCTAssertTrue(text.contains("至")) XCTAssertTrue(text.hasPrefix(firstDayOfMonth())) } func testUpdateRangeByShortcutAllClearsTimes() async { let summaryJSON = """ {"code":100000,"msg":"success","data":{"order_amount_sum":"0","order_count":0,"order_price_avg":"0","received_amount_sum":"0","refund_amount_sum":0}} """.data(using: .utf8)! let listJSON = """ {"code":100000,"msg":"success","data":{"total":0,"data":[]}} """.data(using: .utf8)! AppStore.shared.currentScenicId = 10 let session = MockURLSession(responses: [listJSON]) let api = StatisticsAPI(client: APIClient(environment: .testing, session: session)) let viewModel = StatisticsViewModel() await viewModel.updateRangeByShortcut("全部", api: api) XCTAssertEqual(viewModel.statisticsRangeText, "全部") XCTAssertEqual(viewModel.listStartTime, "") XCTAssertEqual(viewModel.listEndTime, "") XCTAssertEqual(session.requests.count, 1) XCTAssertEqual(session.requests[0].url?.path, "/api/yf-handset-app/photog/analyse/user/daily") let queryItems = URLComponents(url: session.requests[0].url!, resolvingAgainstBaseURL: false)?.queryItems ?? [] XCTAssertEqual(queryItems.first(where: { $0.name == "start_time" })?.value, "") XCTAssertEqual(queryItems.first(where: { $0.name == "end_time" })?.value, "") } func testPhotographerSummaryAndDailyRequests() async throws { let summaryJSON = """ {"code":100000,"msg":"success","data":{"order_amount_sum":"100","order_count":2,"order_price_avg":"50","received_amount_sum":"90","refund_amount_sum":10}} """.data(using: .utf8)! let listJSON = """ {"code":100000,"msg":"success","data":{"total":1,"data":[{"date":"2025-12-09","order_count":2,"order_price":"50","refund":"10","received":"90"}]}} """.data(using: .utf8)! AppStore.shared.currentScenicId = 10 let session = MockURLSession(responses: [summaryJSON, listJSON]) let api = StatisticsAPI(client: APIClient(environment: .testing, session: session)) let viewModel = StatisticsViewModel() await viewModel.loadStatistics("7日", api: api) await viewModel.updateRangeByShortcut("今日", api: api) XCTAssertEqual(viewModel.statistics?.orderAmountSum, "100") XCTAssertEqual(viewModel.statisticsList.count, 1) XCTAssertEqual(session.requests.map { $0.url?.path }, [ "/api/yf-handset-app/photog/analyse/user", "/api/yf-handset-app/photog/analyse/user/daily", ]) XCTAssertTrue(session.requests[0].url?.query?.contains("range=3") == true) XCTAssertTrue(session.requests[1].url?.query?.contains("paeg_size=15") == true) XCTAssertTrue(session.requests[1].url?.query?.contains("scenic_id=10") == true) } func testScenicAdminUsesScenicAdminEndpoints() async { let summaryJSON = """ {"code":100000,"msg":"success","data":{"order_amount_sum":"0","order_count":0,"order_price_avg":"0","received_amount_sum":"0","refund_amount_sum":0}} """.data(using: .utf8)! let listJSON = """ {"code":100000,"msg":"success","data":{"total":0,"data":[]}} """.data(using: .utf8)! AppStore.shared.roleCode = AppRoleCode.scenicAdmin.rawValue AppStore.shared.currentScenicId = 20 let session = MockURLSession(responses: [summaryJSON, listJSON]) let api = StatisticsAPI(client: APIClient(environment: .testing, session: session)) let viewModel = StatisticsViewModel() await viewModel.loadStatistics("今日", api: api) await viewModel.updateRangeByShortcut("今日", api: api) XCTAssertEqual(session.requests[0].url?.path, "/api/app/scenic-admin/analyse") XCTAssertEqual(session.requests[1].url?.path, "/api/app/scenic-admin/analyse/daily") } func testStoreAdminUsesStoreEndpoints() async { let summaryJSON = """ {"code":100000,"msg":"success","data":{"order_amount_sum":"0","order_count":0,"order_price_avg":"0","received_amount_sum":"0","refund_amount_sum":0}} """.data(using: .utf8)! let listJSON = """ {"code":100000,"msg":"success","data":{"total":0,"data":[]}} """.data(using: .utf8)! AppStore.shared.roleCode = AppRoleCode.storeAdmin.rawValue AppStore.shared.currentStoreId = 30 let session = MockURLSession(responses: [summaryJSON, listJSON]) let api = StatisticsAPI(client: APIClient(environment: .testing, session: session)) let viewModel = StatisticsViewModel() await viewModel.loadStatistics("今日", api: api) await viewModel.updateRangeByShortcut("今日", api: api) XCTAssertEqual(session.requests[0].url?.path, "/api/app/store/analyse") XCTAssertTrue(session.requests[0].url?.query?.contains("store_id=30") == true) XCTAssertEqual(session.requests[1].url?.path, "/api/app/store/analyse/daily") } func testStoreAdminWithoutStoreIdDoesNotRequest() async { AppStore.shared.roleCode = AppRoleCode.storeAdmin.rawValue AppStore.shared.currentStoreId = 0 let session = MockURLSession(responses: []) let api = StatisticsAPI(client: APIClient(environment: .testing, session: session)) let viewModel = StatisticsViewModel() var message: String? viewModel.onShowMessage = { message = $0 } await viewModel.loadStatistics("今日", api: api) XCTAssertEqual(message, "请先选择店铺") XCTAssertEqual(session.requests.count, 0) } func testLoadMoreStopsWhenReachedTotal() async { let page1JSON = """ {"code":100000,"msg":"success","data":{"total":1,"data":[{"date":"2025-12-09","order_count":1,"order_price":"10","refund":"0","received":"10"}]}} """.data(using: .utf8)! AppStore.shared.currentScenicId = 10 let session = MockURLSession(responses: [page1JSON]) let api = StatisticsAPI(client: APIClient(environment: .testing, session: session)) let viewModel = StatisticsViewModel() await viewModel.updateRangeByShortcut("今日", api: api) XCTAssertEqual(viewModel.statisticsList.count, 1) XCTAssertFalse(viewModel.canLoadMore) await viewModel.loadMore(api: api) XCTAssertEqual(viewModel.listCurrentPage, 1) XCTAssertEqual(session.requests.count, 1) } private func formattedToday() -> String { let formatter = DateFormatter() formatter.calendar = Calendar(identifier: .gregorian) formatter.locale = Locale(identifier: "zh_CN") formatter.dateFormat = "yyyy/MM/dd" return formatter.string(from: Date()) } private func firstDayOfMonth() -> String { let formatter = DateFormatter() formatter.calendar = Calendar(identifier: .gregorian) formatter.locale = Locale(identifier: "zh_CN") formatter.dateFormat = "yyyy/MM/dd" var calendar = Calendar(identifier: .gregorian) let components = calendar.dateComponents([.year, .month], from: Date()) let firstDay = calendar.date(from: components) ?? Date() return formatter.string(from: firstDay) } }