Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 37 additions & 20 deletions lib/core/exchange/data/datasources/bullbitcoin_api_datasource.dart
Original file line number Diff line number Diff line change
Expand Up @@ -238,28 +238,45 @@ class BullbitcoinApiDatasource implements BitcoinPriceDatasource {
);
}

Future<List<OrderModel>> listOrderSummaries({required String apiKey}) async {
final resp = await _http.post(
_ordersPath,
data: {
'jsonrpc': '2.0',
'id': '0',
'method': 'listOrderSummaries',
'params': {
"sortBy": {"id": "createdAt", "sort": "desc"},
"paginator": {"page": 1, "pageSize": 50},
Future<List<OrderModel>> listOrderSummaries({
required String apiKey,
int pageSize = 50,
}) async {
final ordersById = <String, OrderModel>{};

for (var page = 1; ; page++) {
final resp = await _http.post(
_ordersPath,
data: {
'jsonrpc': '2.0',
'id': '0',
'method': 'listOrderSummaries',
'params': {
'sortBy': {'id': 'createdAt', 'sort': 'desc'},
'paginator': {'page': page, 'pageSize': pageSize},
},
},
},
options: Options(headers: {'X-API-Key': apiKey}),
);
if (resp.statusCode != 200) {
throw Exception('Failed to list order summaries');
options: Options(headers: {'X-API-Key': apiKey}),
);
if (resp.statusCode != 200) {
throw Exception('Failed to list order summaries');
}

final elements = resp.data['result']['elements'] as List<dynamic>?;
if (elements == null || elements.isEmpty) break;

final previousLength = ordersById.length;
for (final element in elements) {
final order = OrderModel.fromJson(element as Map<String, dynamic>);
ordersById.putIfAbsent(order.orderId, () => order);
}

if (elements.length < pageSize || ordersById.length == previousLength) {
break;
}
}
final elements = resp.data['result']['elements'] as List<dynamic>?;
if (elements == null) return [];
return elements
.map((e) => OrderModel.fromJson(e as Map<String, dynamic>))
.toList();

return ordersById.values.toList();
}

Future<OrderModel> refreshOrder({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import 'package:bb_mobile/core/exchange/data/datasources/bullbitcoin_api_datasource.dart';
import 'package:dio/dio.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';

class _MockDio extends Mock implements Dio {}

Map<String, dynamic> _orderJson(String id) => {
'orderId': id,
'orderType': 'Buy Bitcoin',
'orderNumber': int.parse(id.substring(1)),
'exchangeRateAmount': 100000,
'exchangeRateCurrency': 'CAD',
'payinAmount': 100,
'payinCurrency': 'CAD',
'payoutAmount': 0.001,
'payoutCurrency': 'BTC',
'orderStatus': 'Completed',
'payinStatus': 'Completed',
'payoutStatus': 'Completed',
'createdAt': '2026-07-29T12:00:00.000Z',
'message': <String, dynamic>{'code': '', 'message': ''},
'payinMethod': 'CAD Balance',
'payoutMethod': 'Bitcoin',
'triggerType': 'MANUAL',
'confirmationDeadline': '2026-07-29T12:05:00.000Z',
};

void main() {
late _MockDio dio;
late BullbitcoinApiDatasource datasource;
late List<int> requestedPages;

setUp(() {
dio = _MockDio();
datasource = BullbitcoinApiDatasource(bullbitcoinApiHttpClient: dio);
requestedPages = [];
});

void stubPages(Map<int, List<Map<String, dynamic>>> pages) {
when(
() => dio.post<dynamic>(
any(),
data: any(named: 'data'),
options: any(named: 'options'),
),
).thenAnswer((invocation) async {
final data = invocation.namedArguments[#data] as Map<String, dynamic>;
final params = data['params'] as Map<String, dynamic>;
final paginator = params['paginator'] as Map<String, dynamic>;
final page = paginator['page'] as int;
requestedPages.add(page);

return Response<dynamic>(
requestOptions: RequestOptions(path: '/ak/api-orders'),
statusCode: 200,
data: {
'result': {'elements': pages[page] ?? <Map<String, dynamic>>[]},
},
);
});
}

test('loads every page instead of stopping after the first one', () async {
stubPages({
1: [_orderJson('o1'), _orderJson('o2')],
2: [_orderJson('o3')],
});

final orders = await datasource.listOrderSummaries(
apiKey: 'key',
pageSize: 2,
);

expect(orders.map((order) => order.orderId), ['o1', 'o2', 'o3']);
expect(requestedPages, [1, 2]);
});

test('stops when the backend repeats a full page', () async {
final repeatedPage = [_orderJson('o1'), _orderJson('o2')];
stubPages({1: repeatedPage, 2: repeatedPage});

final orders = await datasource.listOrderSummaries(
apiKey: 'key',
pageSize: 2,
);

expect(orders.map((order) => order.orderId), ['o1', 'o2']);
expect(requestedPages, [1, 2]);
});
}
Loading