app working with sandbox
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
/// Thin HTTP client for the personal ingtrak FastAPI backend.
|
||||
library;
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../models/bank.dart';
|
||||
import '../utils/constants.dart';
|
||||
|
||||
/// Talks to Enable Banking via the local ingtrak API (no app-user auth).
|
||||
class IngtrakApi {
|
||||
IngtrakApi({http.Client? client, this.baseUrl = kApiBaseUrl})
|
||||
: _client = client ?? http.Client();
|
||||
|
||||
final http.Client _client;
|
||||
final String baseUrl;
|
||||
|
||||
Uri _uri(String path, [Map<String, String>? query]) {
|
||||
final normalized = baseUrl.endsWith('/')
|
||||
? baseUrl.substring(0, baseUrl.length - 1)
|
||||
: baseUrl;
|
||||
return Uri.parse('$normalized$path').replace(queryParameters: query);
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _decodeObject(http.Response response) {
|
||||
final decoded = jsonDecode(response.body);
|
||||
if (decoded is! Map<String, dynamic>) {
|
||||
throw const IngtrakApiException('Unexpected response shape');
|
||||
}
|
||||
return Future.value(decoded);
|
||||
}
|
||||
|
||||
void _ensureOk(http.Response response) {
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) return;
|
||||
throw IngtrakApiException(
|
||||
_messageFromBody(response) ??
|
||||
'Request failed (${response.statusCode})',
|
||||
statusCode: response.statusCode,
|
||||
);
|
||||
}
|
||||
|
||||
String? _messageFromBody(http.Response response) {
|
||||
try {
|
||||
final decoded = jsonDecode(response.body);
|
||||
if (decoded is Map<String, dynamic>) {
|
||||
final detail = decoded['detail'];
|
||||
if (detail is String) return detail;
|
||||
if (detail is Map) {
|
||||
final nested = detail['enablebanking'];
|
||||
if (nested is Map && nested['message'] is String) {
|
||||
return nested['message'] as String;
|
||||
}
|
||||
if (detail['message'] is String) {
|
||||
return detail['message'] as String;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_) {
|
||||
// Fall through to generic message.
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<bool> health() async {
|
||||
final response = await _client.get(
|
||||
_uri('/health'),
|
||||
headers: const {'Accept': 'application/json'},
|
||||
);
|
||||
_ensureOk(response);
|
||||
final body = await _decodeObject(response);
|
||||
return body['status'] == 'ok';
|
||||
}
|
||||
|
||||
/// `POST /api/connect` → open [ConnectStart.authUrl] in a browser.
|
||||
Future<ConnectStart> startConnect({
|
||||
String? aspspName,
|
||||
String? aspspCountry,
|
||||
}) async {
|
||||
final payload = <String, String>{};
|
||||
if (aspspName != null && aspspName.isNotEmpty) {
|
||||
payload['aspspName'] = aspspName;
|
||||
}
|
||||
if (aspspCountry != null && aspspCountry.isNotEmpty) {
|
||||
payload['aspspCountry'] = aspspCountry;
|
||||
}
|
||||
|
||||
final response = await _client.post(
|
||||
_uri('/api/connect'),
|
||||
headers: const {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: jsonEncode(payload),
|
||||
);
|
||||
_ensureOk(response);
|
||||
return ConnectStart.fromJson(await _decodeObject(response));
|
||||
}
|
||||
|
||||
/// `POST /api/connect/complete` after the deep-link redirect.
|
||||
Future<ConnectStatus> completeConnect({
|
||||
required String code,
|
||||
String? state,
|
||||
}) async {
|
||||
final payload = <String, String>{'code': code};
|
||||
if (state != null && state.isNotEmpty) {
|
||||
payload['state'] = state;
|
||||
}
|
||||
|
||||
final response = await _client.post(
|
||||
_uri('/api/connect/complete'),
|
||||
headers: const {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: jsonEncode(payload),
|
||||
);
|
||||
_ensureOk(response);
|
||||
return ConnectStatus.fromJson(await _decodeObject(response));
|
||||
}
|
||||
|
||||
Future<ConnectStatus> fetchStatus() async {
|
||||
final response = await _client.get(
|
||||
_uri('/api/connect/status'),
|
||||
headers: const {'Accept': 'application/json'},
|
||||
);
|
||||
_ensureOk(response);
|
||||
return ConnectStatus.fromJson(await _decodeObject(response));
|
||||
}
|
||||
|
||||
Future<BankBalance> fetchBalance() async {
|
||||
final response = await _client.get(
|
||||
_uri('/api/balance'),
|
||||
headers: const {'Accept': 'application/json'},
|
||||
);
|
||||
_ensureOk(response);
|
||||
return BankBalance.fromJson(await _decodeObject(response));
|
||||
}
|
||||
|
||||
Future<List<BankTransaction>> fetchTransactions({int days = 90}) async {
|
||||
final response = await _client.get(
|
||||
_uri('/api/transactions', {'days': '$days'}),
|
||||
headers: const {'Accept': 'application/json'},
|
||||
);
|
||||
_ensureOk(response);
|
||||
|
||||
final decoded = jsonDecode(response.body);
|
||||
if (decoded is! List) {
|
||||
throw const IngtrakApiException('Unexpected transactions response');
|
||||
}
|
||||
|
||||
return decoded
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(BankTransaction.fromJson)
|
||||
.toList(growable: false);
|
||||
}
|
||||
|
||||
void close() => _client.close();
|
||||
}
|
||||
|
||||
class IngtrakApiException implements Exception {
|
||||
const IngtrakApiException(this.message, {this.statusCode});
|
||||
|
||||
final String message;
|
||||
final int? statusCode;
|
||||
|
||||
bool get isNotLinked => statusCode == 409;
|
||||
|
||||
@override
|
||||
String toString() => message;
|
||||
}
|
||||
Reference in New Issue
Block a user