// lib/services/market/market_service.dart // // سرویس برای endpoint بازار (market-service) در API gateway جدید: // - GET /market (paginated) → لیست symbolها با داده‌ی OHLCV. // // داده‌ی خام Hlocvt را به مدل قدیمی NewStatisticModel (CategoryList/Content/ // Data) map می‌کنیم تا UI نبض صنعت بدون تغییر کار کند. import 'dart:async'; import 'dart:convert'; import 'dart:developer' as dev; import 'dart:io' show SocketException; // ignore: depend_on_referenced_packages import 'package:http/http.dart' as http; import 'package:didvan/config/auth_config.dart'; import 'package:didvan/models/new_statistic/new_statistics_model.dart'; import 'package:didvan/services/auth/auth_interceptor.dart'; import 'package:didvan/services/auth/token_storage.dart'; class MarketServiceResult { final bool isSuccess; final int? statusCode; final List? lists; final String? errorMessage; const MarketServiceResult({ required this.isSuccess, this.statusCode, this.lists, this.errorMessage, }); } /// نتیجه‌ی دریافت آیتم‌های خام بازار (برای صفحه‌ی «همه»). class MarketItemsResult { final bool isSuccess; final int? statusCode; final List>? items; final String? errorMessage; const MarketItemsResult({ required this.isSuccess, this.statusCode, this.items, this.errorMessage, }); } class MarketService { MarketService._(); static final MarketService instance = MarketService._(); String get _base => AuthConfig.apiBaseUrl; static const _timeout = Duration(seconds: 30); /// دریافت داده‌ی بازار و map به CategoryList ها. Future getMarketData({int limit = 50}) async { await AuthInterceptor.ensureFreshToken(); try { final uri = Uri.parse('$_base/market').replace(queryParameters: { 'limit': limit.toString(), 'page': '1', }); final response = await _withRetry( () => http.get(uri, headers: _headers()).timeout(_timeout), ); dev.log('GET /market -> status=${response.statusCode}, ' 'bodyLen=${response.body.length}', name: 'MarketService'); if (response.statusCode == 200) { final body = json.decode(response.body); final List items = body is Map && body['data'] is List ? body['data'] as List : (body is List ? body : const []); dev.log('GET /market -> parsed ${items.length} items', name: 'MarketService'); final contents = []; for (final raw in items) { if (raw is! Map) continue; final c = _mapToContent(Map.from(raw)); if (c != null) contents.add(c); } // همه را در یک گروه "بازارها" قرار می‌دهیم. (بک‌اند جدید // دسته‌بندی ندارد؛ در صورت افزوده شدن، اینجا گروه‌بندی می‌شود.) final lists = [ CategoryList(category: 1, header: 'بازارها', contents: contents), ]; return MarketServiceResult( isSuccess: true, statusCode: 200, lists: lists, ); } return MarketServiceResult( isSuccess: false, statusCode: response.statusCode, errorMessage: _extractError(response.body) ?? 'خطا در دریافت داده‌ی بازار.', ); } catch (e) { dev.log('getMarketData error: $e', name: 'MarketService'); return const MarketServiceResult( isSuccess: false, errorMessage: 'خطا در ارتباط با سرور.', ); } } /// دریافت آیتم‌های خام بازار (List از Map). برای صفحه‌ی «همه» که خودش /// هر آیتم را به مدل GeneralItem map می‌کند. Future getMarketItems({ int page = 1, int limit = 100, }) async { await AuthInterceptor.ensureFreshToken(); try { final uri = Uri.parse('$_base/market').replace(queryParameters: { 'limit': limit.toString(), 'page': page.toString(), }); final response = await _withRetry( () => http.get(uri, headers: _headers()).timeout(_timeout), ); dev.log('GET /market (items) -> status=${response.statusCode}', name: 'MarketService'); if (response.statusCode == 200) { final body = json.decode(response.body); final List items = body is Map && body['data'] is List ? body['data'] as List : (body is List ? body : const []); final mapped = items .whereType() .map((e) => Map.from(e)) .toList(); return MarketItemsResult( isSuccess: true, statusCode: 200, items: mapped, ); } return MarketItemsResult( isSuccess: false, statusCode: response.statusCode, errorMessage: _extractError(response.body) ?? 'خطا در دریافت داده‌ی بازار.', ); } catch (e) { dev.log('getMarketItems error: $e', name: 'MarketService'); return const MarketItemsResult( isSuccess: false, errorMessage: 'خطا در ارتباط با سرور.', ); } } /// تبدیل یک Hlocvt به Content مدل قدیمی. /// آخرین قیمت = close.last، درصد تغییر از دو close آخر محاسبه می‌شود. Content? _mapToContent(Map json) { try { final closeRaw = json['close']; final closes = (closeRaw is List) ? closeRaw.map((e) => (e as num).toDouble()).toList() : []; String price = '-'; num changePercent = 0; String direction = 'none'; if (closes.isNotEmpty) { final last = closes.last; price = _formatNumber(last); if (closes.length >= 2) { final prev = closes[closes.length - 2]; if (prev != 0) { final change = ((last - prev) / prev) * 100; changePercent = double.parse(change.toStringAsFixed(2)); direction = change > 0 ? 'high' : (change < 0 ? 'low' : 'none'); } } } final symbolId = (json['symbolId'] ?? json['id'] ?? '').toString(); // آخرین 10 قیمت برای نمودار کوچک. final chartPrices = closes.length > 10 ? closes.sublist(closes.length - 10) : closes; return Content( id: symbolId.hashCode, label: symbolId, title: (json['title'] ?? symbolId).toString(), marked: false, data: Data( p: price, dp: changePercent, dt: direction, prices: chartPrices, ), ); } catch (e) { dev.log('mapToContent error: $e', name: 'MarketService'); return null; } } String _formatNumber(double v) { // جداسازی سه‌رقمی ساده. final s = v.toStringAsFixed(v.truncateToDouble() == v ? 0 : 2); final parts = s.split('.'); final intPart = parts[0]; final buf = StringBuffer(); for (var i = 0; i < intPart.length; i++) { if (i > 0 && (intPart.length - i) % 3 == 0) buf.write(','); buf.write(intPart[i]); } return parts.length > 1 ? '${buf.toString()}.${parts[1]}' : buf.toString(); } // ----------------- Helpers ----------------- Map _headers() { final headers = { 'Accept': 'application/json', 'Content-Type': 'application/json', }; final t = TokenStorage.accessToken; if (t != null && t.isNotEmpty) headers['Authorization'] = 'Bearer $t'; return headers; } Future _withRetry(Future Function() body) async { for (var attempt = 0; attempt < 3; attempt++) { try { return await body(); } on SocketException { if (attempt == 2) rethrow; await Future.delayed(Duration(milliseconds: 250 * (attempt + 1))); } on http.ClientException { if (attempt == 2) rethrow; await Future.delayed(Duration(milliseconds: 250 * (attempt + 1))); } on TimeoutException { if (attempt == 2) rethrow; } } throw StateError('retry loop exited without return'); } String? _extractError(String body) { if (body.isEmpty) return null; try { final data = json.decode(body); if (data is Map) { final msg = data['message'] ?? data['error'] ?? data['msg']; if (msg is String && msg.isNotEmpty) return msg; if (msg is List && msg.isNotEmpty) return msg.first.toString(); } } catch (_) {} return null; } }