import 'dart:convert'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:http/http.dart'; import 'package:logger/logger.dart'; import 'package:seshat/config/constants.dart'; import 'package:seshat/domain/models/accounting.dart'; import 'package:seshat/domain/models/bal.dart'; import 'package:seshat/domain/models/bal_stats.dart'; import 'package:seshat/domain/models/book.dart'; import 'package:seshat/domain/models/book_instance.dart'; import 'package:seshat/domain/models/owner.dart'; import 'package:seshat/domain/models/search_result.dart'; import 'package:seshat/utils/command.dart'; import 'package:seshat/utils/result.dart'; extension StringExtension on String { String capitalize() { return "${this[0].toUpperCase()}${this.substring(1).toLowerCase()}"; } } class ApiClient { ApiClient({String? host, int? port}); late final Command0 load; String? token; bool isReady = false; FlutterSecureStorage? _secureStorage; Logger log = Logger( printer: PrettyPrinter( colors: true, lineLength: 100, methodCount: 0, dateTimeFormat: DateTimeFormat.dateAndTime, ), ); Future _initStore() async { _secureStorage ??= const FlutterSecureStorage(); } Future> _getHeaders([ Map? additionalHeaders, ]) async { await _initStore(); final token = await _secureStorage!.read(key: "token"); final headers = {"Authorization": "Bearer $token", ...?additionalHeaders}; return headers; } /* * ======================== * =====< Accounting >===== * ======================== */ Future> getAccounting(int balId) async { final url = "https://$apiBasePath/bal/$balId/accounting"; log.i("Fetching: getAccounting ($url)"); final client = Client(); try { final headers = await _getHeaders(); final response = await client.get(Uri.parse(url), headers: headers); switch (response.statusCode) { case 200: final json = jsonDecode(response.body); return Result.ok(Accounting.fromJSON(json)); case 403: throw "You don't own the specified BAL"; case 404: throw "No BAL with this is exists in database"; default: throw "Unknown error of code ${response.statusCode.toString()}"; } } catch (e) { log.e(e.toString()); return Result.error(Exception(e)); } finally { client.close(); } } Future> returnToId(int balId, int ownerId, String type) async { final url = "https://$apiBasePath/bal/${balId.toString()}/accounting/return/${ownerId.toString()}"; log.i("Fetching: returnToId ($url)"); final client = Client(); try { final headers = await _getHeaders({"Content-Type": "application/json"}); final body = jsonEncode({"return_type": type.capitalize()}); final response = await client.post( Uri.parse(url), headers: headers, body: body, ); switch (response.statusCode) { case 200: return Result.ok(response); case 403: throw "You don't own the specified BAL or owner"; case 404: throw "No BAL or owner with this id exists in database"; case 409: throw "Books and money have already been returned, or there was nothing to return"; default: throw "Unknown error of code ${response.statusCode.toString()}"; } } catch (e) { log.e(e.toString()); return Result.error(Exception(e)); } finally { client.close(); } } /* * ================= * =====[ BAL ]===== * ================= */ Future> getBalStats(int id) async { final url = "https://$apiBasePath/bal/${id.toString()}/stats"; log.i("Fetching: getBalStats ($url)"); final client = Client(); try { final headers = await _getHeaders(); final response = await client.get(Uri.parse(url), headers: headers); switch (response.statusCode) { case 200: final json = jsonDecode(response.body); return Result.ok(BalStats.fromJSON(json)); case 403: throw "You don't own the specified BAL"; case 404: throw "No BAL with this id exists in the database"; case 409: throw "The specified BAL is not ended yet"; default: throw "Unknown error with code ${response.statusCode.toString()}"; } } catch (e) { log.e(e.toString()); return Result.error(Exception(e)); } finally { client.close(); } } Future> stopBal(int id) async { final url = "https://$apiBasePath/bal/${id.toString()}/stop"; log.i("Fetching: stopBal ($url)"); final client = Client(); try { final headers = await _getHeaders(); final response = await client.post(Uri.parse(url), headers: headers); switch (response.statusCode) { case 200: final json = jsonDecode(response.body); return Result.ok(Bal.fromJSON(json)); case 403: throw "You don't own the specified BAL"; case 404: throw "No BAL with specified ID found"; case 409: throw "Selected BAL was not on ongoing state"; default: throw "Unknown error with code ${response.statusCode.toString()}"; } } catch (e) { log.e(e.toString()); return Result.error(Exception(e)); } finally { client.close(); } } Future> startBal(int id) async { final url = "https://$apiBasePath/bal/${id.toString()}/start"; log.i("Fetching: startBal ($url)"); final client = Client(); try { final headers = await _getHeaders(); final response = await client.post(Uri.parse(url), headers: headers); switch (response.statusCode) { case 200: final json = jsonDecode(response.body); return Result.ok(Bal.fromJSON(json)); case 403: throw "You don't own the specified BAL"; case 404: throw "No BAL with specified ID found"; case 409: throw "Cannot have multiple BAl ongoing at the same time!"; default: throw "Unknown error with code ${response.statusCode.toString()}"; } } catch (e) { log.e(e.toString()); return Result.error(Exception(e)); } finally { client.close(); } } Future> editBal( int id, String name, DateTime start, DateTime end, ) async { final url = "https://$apiBasePath/bal/${id.toString()}"; log.i("Fetching: editBal ($url)"); final client = Client(); try { final headers = await _getHeaders({"Content-Type": "application/json"}); final body = { "name": name, "start_timestamp": (start.millisecondsSinceEpoch / 1000).round(), "end_timestamp": (end.millisecondsSinceEpoch / 1000).round(), }; final response = await client.patch( Uri.parse(url), headers: headers, body: jsonEncode(body), ); switch (response.statusCode) { case 200: final json = jsonDecode(response.body); return Result.ok(Bal.fromJSON(json)); case 403: throw "You don't own the specified BAL"; case 404: throw "No bal with specified id"; default: throw "Unknown error with code ${response.statusCode.toString()}"; } } catch (e) { log.e(e.toString()); return Result.error(Exception(e)); } finally { client.close(); } } Future> getBalById(int id) async { final url = "https://$apiBasePath/bal/${id.toString()}"; log.i("Fetching: getBalById ($url)"); final client = Client(); try { final headers = await _getHeaders(); final response = await client.get(Uri.parse(url), headers: headers); switch (response.statusCode) { case 200: final json = jsonDecode(response.body); return Result.ok(Bal.fromJSON(json)); case 403: throw "You don't own the specified BAL"; case 404: throw "No BAL with this id found in database"; default: throw "Unknown error"; } } catch (e) { log.e(e.toString()); return Result.error(Exception(e)); } finally { client.close(); } } Future> addBal(String name, DateTime start, DateTime end) async { final url = "https://$apiBasePath/bal"; log.i("Fetching: addBal ($url)"); final client = Client(); try { final headers = await _getHeaders({"Content-Type": "application/json"}); final body = { "name": name, "start_timestamp": (start.millisecondsSinceEpoch / 1000).round(), "end_timestamp": (end.millisecondsSinceEpoch / 1000).round(), }; final response = await client.post( Uri.parse(url), headers: headers, body: jsonEncode(body), ); switch (response.statusCode) { case 201: final json = jsonDecode(response.body); return Result.ok(Bal.fromJSON(json)); case 400: throw "Time cannot go backwards"; default: throw "Unknown error with code ${response.statusCode.toString()}"; } } catch (e) { log.e(e.toString()); return Result.error(Exception(e)); } finally { client.close(); } } Future>> getBals() async { final url = "https://$apiBasePath/bals"; log.i("Fetching: getBals ($url)"); final client = Client(); try { final headers = await _getHeaders(); final response = await client.get(Uri.parse(url), headers: headers); switch (response.statusCode) { case 200: final json = jsonDecode(response.body) as List; return Result.ok( json.map((element) => Bal.fromJSON(element)).toList(), ); default: throw "Unknown error with code ${response.statusCode.toString()}"; } } catch (e) { log.e(e.toString()); return Result.error(Exception(e)); } finally { client.close(); } } Future> getCurrentBal() async { final url = "https://$apiBasePath/bal/current"; log.i("Fetching: getCurrentBal ($url)"); final client = Client(); try { final headers = await _getHeaders(); final response = await client.get(Uri.parse(url), headers: headers); if (response.statusCode == 200) { final json = jsonDecode(response.body); return Result.ok(Bal.fromJSON(json)); } else if (response.statusCode == 404) { return Result.ok(null); } else { throw Exception("Something went wrong"); } } catch (e) { return Result.error(Exception(e)); } finally { client.close(); } } /* * =================== * =====[ BOOKS ]===== * =================== */ Future> getBookById(int id) async { final url = "https://$apiBasePath/book/id/${id.toString()}"; log.i("Fetching: getBookById ($url)"); final client = Client(); try { final headers = await _getHeaders(); final response = await client.get(Uri.parse(url), headers: headers); switch (response.statusCode) { case 200: final json = jsonDecode(response.body); return Result.ok(Book.fromJSON(json)); case 404: throw "No book with this id exists in database"; default: throw "Unknown error with code ${response.statusCode.toString()}"; } } catch (e) { log.e(e.toString()); return Result.error(Exception("API $e")); } finally { client.close(); } } Future> getBookByEAN(String ean) async { final url = "https://$apiBasePath/book/ean/$ean"; log.i("Fetching: getBookByEan ($url)"); final client = Client(); try { final headers = await _getHeaders(); final response = await client.get(Uri.parse(url), headers: headers); switch (response.statusCode) { case 200: final json = jsonDecode(response.body); return Result.ok(Book.fromJSON(json)); case 404: throw "No book with this EAN found in the database of BNF"; default: throw "Unknown error with code ${response.statusCode.toString()}"; } } catch (e) { log.e(e.toString()); return Result.error(Exception("API $e")); } finally { client.close(); } } /* * ============================= * =====[ BOOKS INSTANCES ]===== * ============================= */ Future>> getBookInstanceBySearch( int balId, String title, String author, ) async { final url = "https://$apiBasePath/bal/${balId.toString()}/search"; log.i("Fetching: getBookInstancesBySearch ($url)"); final client = Client(); try { final headers = await _getHeaders({"Content-Type": "application/json"}); final body = jsonEncode({"title": title, "author": author}); final response = await client.post( Uri.parse(url), headers: headers, body: body, ); switch (response.statusCode) { case 200: final json = jsonDecode(response.body) as List; return Result.ok( json.map((el) => SearchResult.fromJSON(el)).toList(), ); case 403: throw "You do not own the BAL"; default: throw "Unknown error with code ${response.statusCode.toString()}"; } } catch (e) { log.e(e.toString()); return Result.error(Exception("API $e")); } finally { client.close(); } } Future>> getBookInstanceByEAN( int balId, int ean, ) async { final url = "https://$apiBasePath/bal/${balId.toString()}/ean/${ean.toString()}/book_instances"; log.i("Fetching: getBookInstancesByEan ($url)"); final client = Client(); try { final headers = await _getHeaders(); final response = await client.get(Uri.parse(url), headers: headers); switch (response.statusCode) { case 200: final json = jsonDecode(response.body) as List; return Result.ok( json.map((el) => BookInstance.fromJSON(el)).toList(), ); case 403: throw "You do not own the BAL"; default: throw "Unknown error with code ${response.statusCode.toString()}"; } } catch (e) { log.e(e.toString()); return Result.error(Exception("API $e")); } finally { client.close(); } } Future> sellBooks(Map books) async { final url = "https://$apiBasePath/book_instance/sell/bulk"; log.i("Fetching: sellBooks ($url)"); final client = Client(); try { final headers = await _getHeaders({"Content-Type": "application/json"}); final body = jsonEncode(books); final response = await client.post( Uri.parse(url), headers: headers, body: body, ); switch (response.statusCode) { case 200: return Result.ok(response.statusCode); case 403: throw "You don't own one of the specified books"; case 404: throw "One of the books was not found"; case 409: throw "One of the books wasn't available"; default: throw "Unknown error with code ${response.statusCode.toString()}"; } } catch (e) { log.e(e.toString()); return Result.error(Exception(e)); } finally { client.close(); } } Future> sendBook( Book book, Owner owner, Bal bal, double price, ) async { final url = "https://$apiBasePath/book_instance"; log.i("Fetching: sendBook ($url)"); final client = Client(); try { final headers = await _getHeaders({"Content-Type": "application/json"}); final body = jsonEncode({ "bal_id": bal.id, "book_id": book.id, "owner_id": owner.id, "price": price, }); final response = await client.post( Uri.parse(url), headers: headers, body: body, ); switch (response.statusCode) { case 201: final json = jsonDecode(response.body); return Result.ok(BookInstance.fromJSON(json)); case 403: throw "You don't own that book instance"; default: throw "Unknown error with code ${response.statusCode.toString()}"; } } catch (e) { return Result.error(Exception(e)); } finally { client.close(); } } /* * ==================== * =====[ OWNERS ]===== * ==================== */ Future> getOwnerById(int id) async { final url = "https://$apiBasePath/owner/${id.toString()}"; log.i("Fetching: getOwnerById ($url)"); final client = Client(); try { final headers = await _getHeaders(); final response = await client.get(Uri.parse(url), headers: headers); switch (response.statusCode) { case 200: final json = jsonDecode(response.body); return Result.ok(Owner.fromJSON(json)); case 403: throw "You do not own specified owner"; case 404: throw "No owner with this id exists"; default: throw "Unknown error with code ${response.statusCode.toString()}"; } } catch (e) { log.e(e.toString()); return Result.error(Exception("API $e")); } finally { client.close(); } } Future> getSectionOwner() async { final url = "https://$apiBasePath/owner/self"; log.i("Fetching: getSectionOwner ($url)"); final client = Client(); try { final headers = await _getHeaders(); final response = await client.get(Uri.parse(url), headers: headers); switch (response.statusCode) { case 200: final json = jsonDecode(response.body); return Result.ok(Owner.fromJSON(json)); default: throw "Unknown error with code ${response.statusCode.toString()}"; } } catch (e) { log.e(e.toString()); return Result.error(Exception(e)); } finally { client.close(); } } /// Call on `/owners` to get a list of all [Owner]s Future>> getOwners() async { final url = "https://$apiBasePath/owners"; log.i("Fetching: getOwners ($url)"); final client = Client(); try { final headers = await _getHeaders(); final response = await client.get(Uri.parse(url), headers: headers); switch (response.statusCode) { case 200: final json = jsonDecode(response.body) as List; return Result.ok( json.map((element) => Owner.fromJSON(element)).toList(), ); default: throw "Unknown error with code ${response.statusCode.toString()}"; } } on Exception catch (error) { return Result.error(error); } finally { client.close(); } } /// Adds an owner to the database Future> addOwner( String firstName, String lastName, String contact, ) async { final url = "https://$apiBasePath/owner"; log.i("Fetching: addOwner ($url)"); final client = Client(); try { final headers = await _getHeaders({"Content-Type": "application/json"}); final body = { "first_name": firstName, "last_name": lastName, "contact": contact, }; final response = await client.post( Uri.parse(url), headers: headers, body: jsonEncode(body), ); switch (response.statusCode) { case 201: final json = jsonDecode(response.body); return Result.ok(Owner.fromJSON(json)); default: throw "Unknown error with code ${response.statusCode.toString()}"; } } catch (e) { log.e(e.toString()); return Result.error(Exception(e)); } finally { client.close(); } } }