-
Notifications
You must be signed in to change notification settings - Fork 0
Add Indian Bonds module and Flutter preview app with mock NSE CBRICS transport #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| # Indian Bond Module Demo (Flutter) | ||
|
|
||
| This repository now includes a runnable Flutter entrypoint that launches the NSE CBRICS Bonds preview UI using local mock data. | ||
|
|
||
| ## Run locally | ||
|
|
||
| 1. Ensure Flutter SDK is installed (`flutter --version`). | ||
| 2. In this repository root, generate platform folders (only first time): | ||
|
|
||
| ```bash | ||
| flutter create . | ||
| ``` | ||
|
|
||
| 3. Get dependencies: | ||
|
|
||
| ```bash | ||
| flutter pub get | ||
| ``` | ||
|
|
||
| 4. Run app: | ||
|
|
||
| ```bash | ||
| flutter run | ||
| ``` | ||
|
|
||
| The app boots to `BondPreviewPage`, so it works without backend APIs. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| # Bond Module (Indian Market via NSE CBRICS) | ||
|
|
||
| This module provides a practical baseline implementation for trading Indian bonds inside a Flutter app. | ||
|
|
||
| ## What is included | ||
|
|
||
| - Domain entities for `Bond` and `BondOrder` | ||
| - Repository contract and implementation | ||
| - `PlaceBondOrder` use case for validation before OMS submission | ||
| - Data source for NSE CBRICS OMS-style REST endpoints | ||
| - Mock transport for local feature preview without backend | ||
| - Basic controller for loading instruments/orders and placing orders | ||
| - Starter UI: | ||
| - `BondPage` instrument list with pull-to-refresh | ||
| - `BondOrderTicket` order entry widget embedded per bond row | ||
| - `BondPreviewPage` for quick local demo | ||
|
|
||
| ## Expected NSE CBRICS endpoints | ||
|
|
||
| - `GET /bonds` -> `{ "data": [ ...bond ] }` | ||
| - `POST /orders` -> returns either `{ "data": { ...order } }` or `{ ...order }` | ||
| - `GET /orders/open` -> `{ "data": [ ...order ] }` | ||
|
|
||
| ## Example wiring (real backend) | ||
|
|
||
| ```dart | ||
| final dataSource = NseCbricsDataSource( | ||
| baseUrl: 'https://your-nse-cbrics-host/api/v1', | ||
| apiKey: 'YOUR_API_KEY', | ||
| http: yourHttpTransport, | ||
| ); | ||
|
|
||
| final repository = BondRepositoryImpl(dataSource); | ||
| final controller = BondController(repository); | ||
|
|
||
| MaterialApp(home: BondPage(controller: controller)); | ||
| ``` | ||
|
|
||
| ## Preview wiring (no backend) | ||
|
|
||
| ```dart | ||
| MaterialApp(home: BondPreviewPage()); | ||
| ``` | ||
|
|
||
| `BondPreviewPage` uses `MockNseCbricsTransport` with in-memory sample bonds and orders so you can verify UI and order flow quickly. | ||
|
|
||
| ## Next steps for production | ||
|
|
||
| - Add auth token refresh and request signing required by your broker | ||
| - Add order modification/cancel flows | ||
| - Add lot-size, tick-size, and risk-rule validation from live exchange masters | ||
| - Add pagination, bond search, and filter chips for yields/maturity buckets | ||
| - Add tests (unit + widget + API contract) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| import 'dart:convert'; | ||
|
|
||
| import 'nse_cbrics_datasource.dart'; | ||
|
|
||
| /// Local in-memory transport to preview the Bonds module UI without backend connectivity. | ||
| class MockNseCbricsTransport implements HttpTransport { | ||
| final List<Map<String, dynamic>> _orders = []; | ||
|
|
||
| @override | ||
| Future<String> get(String path, {Map<String, String>? headers}) async { | ||
| if (path.endsWith('/bonds')) { | ||
| return jsonEncode({ | ||
| 'data': [ | ||
| { | ||
| 'isin': 'INE123A08018', | ||
| 'symbol': 'GSEC_2033_7.26', | ||
| 'issuer': 'Government of India', | ||
| 'maturityDate': '2033-04-06', | ||
| 'couponRate': 7.26, | ||
| 'faceValue': 100, | ||
| 'category': 'G-Sec', | ||
| }, | ||
| { | ||
| 'isin': 'INE987B07042', | ||
| 'symbol': 'SDL_MH_2031_7.10', | ||
| 'issuer': 'State of Maharashtra', | ||
| 'maturityDate': '2031-11-15', | ||
| 'couponRate': 7.10, | ||
| 'faceValue': 100, | ||
| 'category': 'SDL', | ||
| }, | ||
| ], | ||
| }); | ||
| } | ||
|
|
||
| if (path.endsWith('/orders/open')) { | ||
| return jsonEncode({'data': _orders}); | ||
| } | ||
|
|
||
| throw UnsupportedError('GET not supported in mock transport for path: $path'); | ||
| } | ||
|
|
||
| @override | ||
| Future<String> post( | ||
| String path, { | ||
| Map<String, String>? headers, | ||
| Object? body, | ||
| }) async { | ||
| if (!path.endsWith('/orders')) { | ||
| throw UnsupportedError('POST not supported in mock transport for path: $path'); | ||
| } | ||
|
|
||
| final request = jsonDecode(body as String) as Map<String, dynamic>; | ||
| final order = { | ||
| 'orderId': 'MOCK-${_orders.length + 1}', | ||
| 'isin': request['isin'], | ||
| 'side': request['side'], | ||
| 'type': request['type'], | ||
| 'status': 'OPEN', | ||
| 'quantity': request['quantity'], | ||
| 'filledQuantity': 0, | ||
| 'limitPrice': request['limitPrice'], | ||
| 'createdAt': DateTime.now().toUtc().toIso8601String(), | ||
| }; | ||
|
|
||
| _orders.insert(0, order); | ||
| return jsonEncode({'data': order}); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| import 'dart:convert'; | ||
|
|
||
| import '../models/bond_model.dart'; | ||
| import '../models/bond_order_model.dart'; | ||
| import '../../domain/entities/bond_order.dart'; | ||
|
|
||
| abstract class HttpTransport { | ||
| Future<String> get(String path, {Map<String, String>? headers}); | ||
|
|
||
| Future<String> post( | ||
| String path, { | ||
| Map<String, String>? headers, | ||
| Object? body, | ||
| }); | ||
| } | ||
|
|
||
| class NseCbricsDataSource { | ||
| final String baseUrl; | ||
| final String apiKey; | ||
| final HttpTransport http; | ||
|
|
||
| const NseCbricsDataSource({ | ||
| required this.baseUrl, | ||
| required this.apiKey, | ||
| required this.http, | ||
| }); | ||
|
|
||
| Map<String, String> get _headers => { | ||
| 'Content-Type': 'application/json', | ||
| 'X-API-KEY': apiKey, | ||
| }; | ||
|
|
||
| Future<List<BondModel>> fetchBonds() async { | ||
| final raw = await http.get('$baseUrl/bonds', headers: _headers); | ||
| final payload = jsonDecode(raw) as Map<String, dynamic>; | ||
| final items = payload['data'] as List<dynamic>; | ||
|
|
||
|
Comment on lines
+35
to
+37
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Guard response envelope shape before casting
Suggested defensive parsing+ Map<String, dynamic> _decodeObject(String raw) {
+ final decoded = jsonDecode(raw);
+ if (decoded is! Map<String, dynamic>) {
+ throw const FormatException('Expected JSON object response');
+ }
+ return decoded;
+ }
+
+ List<Map<String, dynamic>> _readDataList(Map<String, dynamic> payload) {
+ final data = payload['data'];
+ if (data is! List) {
+ throw const FormatException('Expected `data` to be a list');
+ }
+ return data.map((e) {
+ if (e is! Map<String, dynamic>) {
+ throw const FormatException('Expected list items to be JSON objects');
+ }
+ return e;
+ }).toList(growable: false);
+ }
...
- final payload = jsonDecode(raw) as Map<String, dynamic>;
- final items = payload['data'] as List<dynamic>;
+ final payload = _decodeObject(raw);
+ final items = _readDataList(payload);
...
- final payload = jsonDecode(raw) as Map<String, dynamic>;
- final orderMap = (payload['data'] ?? payload) as Map<String, dynamic>;
+ final payload = _decodeObject(raw);
+ final data = payload['data'];
+ if (data is! Map<String, dynamic>) {
+ throw const FormatException('Expected `data` to be an order object');
+ }
+ final orderMap = data;
...
- final payload = jsonDecode(raw) as Map<String, dynamic>;
- final items = payload['data'] as List<dynamic>;
+ final payload = _decodeObject(raw);
+ final items = _readDataList(payload);Also applies to: 62-64, 69-70 🤖 Prompt for AI Agents |
||
| return items | ||
| .map((item) => BondModel.fromJson(item as Map<String, dynamic>)) | ||
| .toList(); | ||
| } | ||
|
|
||
| Future<BondOrderModel> submitOrder({ | ||
| required String isin, | ||
| required BondOrderSide side, | ||
| required BondOrderType type, | ||
| required int quantity, | ||
| double? limitPrice, | ||
| }) async { | ||
| final raw = await http.post( | ||
| '$baseUrl/orders', | ||
| headers: _headers, | ||
| body: jsonEncode({ | ||
| 'isin': isin, | ||
| 'side': side.name.toUpperCase(), | ||
| 'type': type.name.toUpperCase(), | ||
| 'quantity': quantity, | ||
| if (limitPrice != null) 'limitPrice': limitPrice, | ||
| }), | ||
| ); | ||
|
|
||
| final payload = jsonDecode(raw) as Map<String, dynamic>; | ||
| final orderMap = (payload['data'] ?? payload) as Map<String, dynamic>; | ||
| return BondOrderModel.fromJson(orderMap); | ||
| } | ||
|
|
||
| Future<List<BondOrderModel>> fetchOpenOrders() async { | ||
| final raw = await http.get('$baseUrl/orders/open', headers: _headers); | ||
| final payload = jsonDecode(raw) as Map<String, dynamic>; | ||
| final items = payload['data'] as List<dynamic>; | ||
|
|
||
| return items | ||
| .map((item) => BondOrderModel.fromJson(item as Map<String, dynamic>)) | ||
| .toList(); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| import '../../domain/entities/bond.dart'; | ||
|
|
||
| class BondModel extends Bond { | ||
| const BondModel({ | ||
| required super.isin, | ||
| required super.symbol, | ||
| required super.issuer, | ||
| required super.maturityDate, | ||
| required super.couponRate, | ||
| required super.faceValue, | ||
| required super.category, | ||
| }); | ||
|
|
||
| factory BondModel.fromJson(Map<String, dynamic> json) { | ||
| return BondModel( | ||
| isin: json['isin'] as String, | ||
| symbol: json['symbol'] as String, | ||
| issuer: json['issuer'] as String, | ||
| maturityDate: DateTime.parse(json['maturityDate'] as String), | ||
| couponRate: (json['couponRate'] as num).toDouble(), | ||
| faceValue: (json['faceValue'] as num).toDouble(), | ||
| category: json['category'] as String, | ||
| ); | ||
|
Comment on lines
+14
to
+23
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: cat -n lib/features/bonds/data/models/bond_model.dart | head -50Repository: SumitUni7/Test-web Length of output: 974 🏁 Script executed: # Search for other model files with fromJson patterns
find lib -name "*_model.dart" -type f | head -20Repository: SumitUni7/Test-web Length of output: 161 🏁 Script executed: # Search for usages of BondModel.fromJson
rg "BondModel\.fromJson|\.fromJson.*bond" lib/Repository: SumitUni7/Test-web Length of output: 302 🏁 Script executed: # Check the Bond entity
find lib -name "bond.dart" -path "*/domain/entities/*" | xargs catRepository: SumitUni7/Test-web Length of output: 469 🏁 Script executed: cat -n lib/features/bonds/data/models/bond_order_model.dartRepository: SumitUni7/Test-web Length of output: 2642 🏁 Script executed: cat -n lib/features/bonds/data/datasources/nse_cbrics_datasource.dart | head -100Repository: SumitUni7/Test-web Length of output: 2697 Harden JSON parsing in BondModel.fromJson to match BondOrderModel patterns. The current implementation uses unsafe casts that fail with opaque runtime errors on malformed API payloads. BondOrderModel in the same codebase demonstrates the pattern: validate fields and throw contextual errors (see enum decoders). BondModel should follow suit—add validation for strings, dates, and numeric fields so failures are diagnosable rather than cryptic Specifically:
Add field-level validation helpers (following BondOrderModel's pattern) to throw 🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,70 @@ | ||||||||||||||||||||||||||||||||||||||||||||
| import '../../domain/entities/bond_order.dart'; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| class BondOrderModel extends BondOrder { | ||||||||||||||||||||||||||||||||||||||||||||
| const BondOrderModel({ | ||||||||||||||||||||||||||||||||||||||||||||
| required super.orderId, | ||||||||||||||||||||||||||||||||||||||||||||
| required super.isin, | ||||||||||||||||||||||||||||||||||||||||||||
| required super.side, | ||||||||||||||||||||||||||||||||||||||||||||
| required super.type, | ||||||||||||||||||||||||||||||||||||||||||||
| required super.status, | ||||||||||||||||||||||||||||||||||||||||||||
| required super.quantity, | ||||||||||||||||||||||||||||||||||||||||||||
| required super.filledQuantity, | ||||||||||||||||||||||||||||||||||||||||||||
| super.limitPrice, | ||||||||||||||||||||||||||||||||||||||||||||
| required super.createdAt, | ||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| factory BondOrderModel.fromJson(Map<String, dynamic> json) { | ||||||||||||||||||||||||||||||||||||||||||||
| return BondOrderModel( | ||||||||||||||||||||||||||||||||||||||||||||
| orderId: json['orderId'] as String, | ||||||||||||||||||||||||||||||||||||||||||||
| isin: json['isin'] as String, | ||||||||||||||||||||||||||||||||||||||||||||
| side: _decodeSide(json['side'] as String), | ||||||||||||||||||||||||||||||||||||||||||||
| type: _decodeType(json['type'] as String), | ||||||||||||||||||||||||||||||||||||||||||||
| status: _decodeStatus((json['status'] as String?) ?? 'PENDING'), | ||||||||||||||||||||||||||||||||||||||||||||
| quantity: (json['quantity'] as num).toInt(), | ||||||||||||||||||||||||||||||||||||||||||||
| filledQuantity: ((json['filledQuantity'] as num?) ?? 0).toInt(), | ||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+23
to
+24
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Prevent silent quantity truncation during JSON decode. Line 23 and Line 24 use Proposed fix factory BondOrderModel.fromJson(Map<String, dynamic> json) {
return BondOrderModel(
orderId: json['orderId'] as String,
isin: json['isin'] as String,
side: _decodeSide(json['side'] as String),
type: _decodeType(json['type'] as String),
status: _decodeStatus((json['status'] as String?) ?? 'PENDING'),
- quantity: (json['quantity'] as num).toInt(),
- filledQuantity: ((json['filledQuantity'] as num?) ?? 0).toInt(),
+ quantity: _asStrictInt(json['quantity'], 'quantity'),
+ filledQuantity: _asStrictInt(json['filledQuantity'] ?? 0, 'filledQuantity'),
limitPrice: (json['limitPrice'] as num?)?.toDouble(),
createdAt: DateTime.parse(json['createdAt'] as String),
);
}
+
+ static int _asStrictInt(Object? value, String field) {
+ if (value is int) return value;
+ if (value is num && value == value.roundToDouble()) return value.toInt();
+ throw ArgumentError.value(value, field, 'Expected an integer value');
+ }📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||
| limitPrice: (json['limitPrice'] as num?)?.toDouble(), | ||||||||||||||||||||||||||||||||||||||||||||
| createdAt: DateTime.parse(json['createdAt'] as String), | ||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| static BondOrderSide _decodeSide(String side) { | ||||||||||||||||||||||||||||||||||||||||||||
| switch (side.toUpperCase()) { | ||||||||||||||||||||||||||||||||||||||||||||
| case 'BUY': | ||||||||||||||||||||||||||||||||||||||||||||
| return BondOrderSide.buy; | ||||||||||||||||||||||||||||||||||||||||||||
| case 'SELL': | ||||||||||||||||||||||||||||||||||||||||||||
| return BondOrderSide.sell; | ||||||||||||||||||||||||||||||||||||||||||||
| default: | ||||||||||||||||||||||||||||||||||||||||||||
| throw ArgumentError.value(side, 'side', 'Unsupported side'); | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| static BondOrderType _decodeType(String type) { | ||||||||||||||||||||||||||||||||||||||||||||
| switch (type.toUpperCase()) { | ||||||||||||||||||||||||||||||||||||||||||||
| case 'MARKET': | ||||||||||||||||||||||||||||||||||||||||||||
| return BondOrderType.market; | ||||||||||||||||||||||||||||||||||||||||||||
| case 'LIMIT': | ||||||||||||||||||||||||||||||||||||||||||||
| return BondOrderType.limit; | ||||||||||||||||||||||||||||||||||||||||||||
| default: | ||||||||||||||||||||||||||||||||||||||||||||
| throw ArgumentError.value(type, 'type', 'Unsupported order type'); | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| static BondOrderStatus _decodeStatus(String status) { | ||||||||||||||||||||||||||||||||||||||||||||
| switch (status.toUpperCase()) { | ||||||||||||||||||||||||||||||||||||||||||||
| case 'PENDING': | ||||||||||||||||||||||||||||||||||||||||||||
| return BondOrderStatus.pending; | ||||||||||||||||||||||||||||||||||||||||||||
| case 'OPEN': | ||||||||||||||||||||||||||||||||||||||||||||
| return BondOrderStatus.open; | ||||||||||||||||||||||||||||||||||||||||||||
| case 'PARTIALLY_FILLED': | ||||||||||||||||||||||||||||||||||||||||||||
| return BondOrderStatus.partiallyFilled; | ||||||||||||||||||||||||||||||||||||||||||||
| case 'FILLED': | ||||||||||||||||||||||||||||||||||||||||||||
| return BondOrderStatus.filled; | ||||||||||||||||||||||||||||||||||||||||||||
| case 'CANCELLED': | ||||||||||||||||||||||||||||||||||||||||||||
| return BondOrderStatus.cancelled; | ||||||||||||||||||||||||||||||||||||||||||||
| case 'REJECTED': | ||||||||||||||||||||||||||||||||||||||||||||
| return BondOrderStatus.rejected; | ||||||||||||||||||||||||||||||||||||||||||||
| default: | ||||||||||||||||||||||||||||||||||||||||||||
| return BondOrderStatus.pending; | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| import '../../domain/entities/bond.dart'; | ||
| import '../../domain/entities/bond_order.dart'; | ||
| import '../../domain/repositories/bond_repository.dart'; | ||
| import '../datasources/nse_cbrics_datasource.dart'; | ||
|
|
||
| class BondRepositoryImpl implements BondRepository { | ||
| final NseCbricsDataSource _dataSource; | ||
|
|
||
| const BondRepositoryImpl(this._dataSource); | ||
|
|
||
| @override | ||
| Future<List<Bond>> getAvailableBonds() => _dataSource.fetchBonds(); | ||
|
|
||
| @override | ||
| Future<List<BondOrder>> getOpenOrders() => _dataSource.fetchOpenOrders(); | ||
|
|
||
| @override | ||
| Future<BondOrder> placeOrder({ | ||
| required String isin, | ||
| required BondOrderSide side, | ||
| required BondOrderType type, | ||
| required int quantity, | ||
| double? limitPrice, | ||
| }) { | ||
| return _dataSource.submitOrder( | ||
| isin: isin, | ||
| side: side, | ||
| type: type, | ||
| quantity: quantity, | ||
| limitPrice: limitPrice, | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| class Bond { | ||
| final String isin; | ||
| final String symbol; | ||
| final String issuer; | ||
| final DateTime maturityDate; | ||
| final double couponRate; | ||
| final double faceValue; | ||
| final String category; | ||
|
|
||
| const Bond({ | ||
| required this.isin, | ||
| required this.symbol, | ||
| required this.issuer, | ||
| required this.maturityDate, | ||
| required this.couponRate, | ||
| required this.faceValue, | ||
| required this.category, | ||
| }); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: SumitUni7/Test-web
Length of output: 2697
🏁 Script executed:
Repository: SumitUni7/Test-web
Length of output: 984
🏁 Script executed:
Repository: SumitUni7/Test-web
Length of output: 1824
🏁 Script executed:
Repository: SumitUni7/Test-web
Length of output: 548
🏁 Script executed:
Repository: SumitUni7/Test-web
Length of output: 44
🏁 Script executed:
Repository: SumitUni7/Test-web
Length of output: 1246
🏁 Script executed:
Repository: SumitUni7/Test-web
Length of output: 972
🏁 Script executed:
Repository: SumitUni7/Test-web
Length of output: 1068
🏁 Script executed:
Repository: SumitUni7/Test-web
Length of output: 128
🏁 Script executed:
Repository: SumitUni7/Test-web
Length of output: 44
🏁 Script executed:
Repository: SumitUni7/Test-web
Length of output: 44
🏁 Script executed:
Repository: SumitUni7/Test-web
Length of output: 130
🏁 Script executed:
Repository: SumitUni7/Test-web
Length of output: 851
🏁 Script executed:
Repository: SumitUni7/Test-web
Length of output: 308
HttpTransportmust expose HTTP status codes, not only response body.The datasource cannot currently distinguish successful HTTP responses from error responses (4xx/5xx), so failed requests will be passed to
jsonDecode()as if they were valid. This affects all three methods:fetchBonds()(line 34),submitOrder()(line 50), andfetchOpenOrders()(line 68).Change the interface to return a response object with both status code and body:
Suggested fix
abstract class HttpTransport { - Future<String> get(String path, {Map<String, String>? headers}); + Future<HttpResponse> get(String path, {Map<String, String>? headers}); - Future<String> post( + Future<HttpResponse> post( String path, { Map<String, String>? headers, Object? body, }); } + +class HttpResponse { + final int statusCode; + final String body; + + const HttpResponse({required this.statusCode, required this.body}); +}Then validate status before parsing:
Apply the same pattern to
submitOrder()andfetchOpenOrders().📝 Committable suggestion
🤖 Prompt for AI Agents