diff --git a/CHANGELOG.md b/CHANGELOG.md index 967b303..ccecdf2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,29 @@ +## 0.8 +- Cache accounts & currencies on startup + - Added `Restrr#getAccounts` + - Added `Restrr#getCurrencies` +- Added entity-specific Ids: + - Added `EntityId` methods: `get`, `retrieve` + - Added `AccountId` + - Added `TransactionId` + - Added `CurrencyId` + - Added `UserId` + - Added `PartialSessionId` + - Entities now feature the corresponding `EntityId` instead of a `Id (int)` +- Added `Account` + - Added `AccountRoutes` + - Added `Account` methods: `delete`, `update`, `retrieveAllTransactions` + - Added `Restrr` methods: `createAccount`, `retrieveAccountById`, `retrieveAllAccounts` +- Added `Transaction` + - Added `TransactionRoutes` + - Added `Transaction` methods: `delete`, `update` + - Added `Restrr` methods: `createTransaction`, `retrieveTransactionById`, `retrieveAllTransactions` +- Fixed `Session#delete` using a wrong route +- Unified entity deletion +- Implemented actual `RestrrError` error codes + - Added `ErrorResponse#apiCode` + - Made `ErrorResponse#reference` `dynamic` + ## 0.7 - Restructured package (many breaking changes!) - Split package into `api` (abstraction) and `internal` (implementation) diff --git a/lib/restrr.dart b/lib/restrr.dart index 1ada730..76e7f98 100644 --- a/lib/restrr.dart +++ b/lib/restrr.dart @@ -10,8 +10,11 @@ export 'src/api/entities/currency/currency.dart'; export 'src/api/entities/currency/custom_currency.dart'; export 'src/api/entities/session/partial_session.dart'; export 'src/api/entities/session/session.dart'; -export 'src/api/entities/user.dart'; +export 'src/api/entities/account.dart'; export 'src/api/entities/restrr_entity.dart'; +export 'src/api/entities/transaction/transaction.dart'; +export 'src/api/entities/transaction/transaction_type.dart'; +export 'src/api/entities/user.dart'; /* [ /src/api/events ] */ export 'src/api/events/ready_event.dart'; diff --git a/lib/src/api/entities/account.dart b/lib/src/api/entities/account.dart new file mode 100644 index 0000000..1b6c1c9 --- /dev/null +++ b/lib/src/api/entities/account.dart @@ -0,0 +1,22 @@ +import '../../../restrr.dart'; + +abstract class AccountId extends EntityId {} + +abstract class Account extends RestrrEntity { + @override + AccountId get id; + + String get name; + String? get description; + String? get iban; + int get balance; + int get originalBalance; + CurrencyId get currencyId; + DateTime get createdAt; + + Future delete(); + + Future update({String? name, String? description, String? iban, int? originalBalance, Id? currencyId}); + + Future> retrieveAllTransactions({int page = 1, int limit = 25, bool forceRetrieve = false}); +} diff --git a/lib/src/api/entities/currency/currency.dart b/lib/src/api/entities/currency/currency.dart index 1b56076..14cbf6b 100644 --- a/lib/src/api/entities/currency/currency.dart +++ b/lib/src/api/entities/currency/currency.dart @@ -1,6 +1,11 @@ import '../../../../restrr.dart'; -abstract class Currency extends RestrrEntity { +abstract class CurrencyId extends EntityId {} + +abstract class Currency extends RestrrEntity { + @override + CurrencyId get id; + String get name; String get symbol; int get decimalPlaces; diff --git a/lib/src/api/entities/currency/custom_currency.dart b/lib/src/api/entities/currency/custom_currency.dart index 9446ccf..3062765 100644 --- a/lib/src/api/entities/currency/custom_currency.dart +++ b/lib/src/api/entities/currency/custom_currency.dart @@ -1,7 +1,7 @@ import 'package:restrr/restrr.dart'; abstract class CustomCurrency extends Currency { - int? get user; + UserId? get userId; bool isCreatedBy(User user); diff --git a/lib/src/api/entities/restrr_entity.dart b/lib/src/api/entities/restrr_entity.dart index f047462..c3c7b08 100644 --- a/lib/src/api/entities/restrr_entity.dart +++ b/lib/src/api/entities/restrr_entity.dart @@ -2,11 +2,33 @@ import '../../../restrr.dart'; typedef Id = int; +abstract class EntityId { + Restrr get api; + Id get value; + + E? get(); + Future retrieve({forceRetrieve = false}); + + @override + String toString() => value.toString(); + + @override + bool operator ==(Object other) { + if (other is EntityId) { + return other.value == value; + } + return false; + } + + @override + int get hashCode => value.hashCode; +} + /// The base class for all Restrr entities. /// This simply provides a reference to the Restrr instance. -abstract class RestrrEntity { +abstract class RestrrEntity> { /// A reference to the Restrr instance. Restrr get api; - Id get id; + ID get id; } diff --git a/lib/src/api/entities/session/partial_session.dart b/lib/src/api/entities/session/partial_session.dart index 67af7a8..347f5c2 100644 --- a/lib/src/api/entities/session/partial_session.dart +++ b/lib/src/api/entities/session/partial_session.dart @@ -1,6 +1,11 @@ import '../../../../restrr.dart'; -abstract class PartialSession extends RestrrEntity { +abstract class PartialSessionId extends EntityId {} + +abstract class PartialSession extends RestrrEntity { + @override + PartialSessionId get id; + String? get name; DateTime get createdAt; DateTime get expiresAt; diff --git a/lib/src/api/entities/transaction/transaction.dart b/lib/src/api/entities/transaction/transaction.dart new file mode 100644 index 0000000..9899f89 --- /dev/null +++ b/lib/src/api/entities/transaction/transaction.dart @@ -0,0 +1,33 @@ +import 'package:restrr/restrr.dart'; + +abstract class TransactionId extends EntityId {} + +abstract class Transaction extends RestrrEntity { + @override + TransactionId get id; + + AccountId? get sourceId; + AccountId? get destinationId; + int get amount; + CurrencyId get currencyId; + String get name; + String? get description; + EntityId? get budgetId; + DateTime get createdAt; + DateTime get executedAt; + + TransactionType get type; + + Future delete(); + + Future update({ + Id? sourceId, + Id? destinationId, + int? amount, + Id? currencyId, + String? name, + String? description, + Id? budgetId, + DateTime? executedAt, + }); +} diff --git a/lib/src/api/entities/transaction/transaction_type.dart b/lib/src/api/entities/transaction/transaction_type.dart new file mode 100644 index 0000000..8385bb7 --- /dev/null +++ b/lib/src/api/entities/transaction/transaction_type.dart @@ -0,0 +1 @@ +enum TransactionType { deposit, withdrawal, transfer } diff --git a/lib/src/api/entities/user.dart b/lib/src/api/entities/user.dart index 54d1ca2..d67f468 100644 --- a/lib/src/api/entities/user.dart +++ b/lib/src/api/entities/user.dart @@ -1,6 +1,8 @@ import '../../../restrr.dart'; -abstract class User extends RestrrEntity { +abstract class UserId extends EntityId {} + +abstract class User extends RestrrEntity { String get username; String? get email; String? get displayName; diff --git a/lib/src/api/requests/route.dart b/lib/src/api/requests/route.dart index 40644c0..e80f908 100644 --- a/lib/src/api/requests/route.dart +++ b/lib/src/api/requests/route.dart @@ -79,12 +79,17 @@ class CompiledRoute { if (bearerToken != null) { headers['Authorization'] = 'Bearer $bearerToken'; } - return dio.fetch(RequestOptions( - path: compiledRoute, - headers: headers, - data: body, - method: baseRoute.method, - baseUrl: _buildBaseUrl(routeOptions, baseRoute.isVersioned))); + return dio + .fetch(RequestOptions( + path: compiledRoute, + headers: headers, + data: body, + method: baseRoute.method, + baseUrl: _buildBaseUrl(routeOptions, baseRoute.isVersioned))) + .then((response) { + Restrr.log.info('${baseRoute.method} $compiledRoute => ${response.statusCode} ${response.statusMessage}'); + return response; + }); } String _buildBaseUrl(RouteOptions options, bool isVersioned) { diff --git a/lib/src/api/requests/route_definitions.dart b/lib/src/api/requests/route_definitions.dart index 7797b0f..157d6b8 100644 --- a/lib/src/api/requests/route_definitions.dart +++ b/lib/src/api/requests/route_definitions.dart @@ -27,6 +27,17 @@ class UserRoutes { static final Route create = Route.post('/user/register'); } +class AccountRoutes { + const AccountRoutes._(); + + static final Route getAll = Route.get('/account'); + static final Route getById = Route.get('/account/{accountId}'); + static final Route deleteById = Route.delete('/account/{accountId}'); + static final Route patchById = Route.patch('/account/{accountId}'); + static final Route getTransactions = Route.get('/account/{accountId}/transactions'); + static final Route create = Route.post('/account'); +} + class CurrencyRoutes { const CurrencyRoutes._(); @@ -34,5 +45,15 @@ class CurrencyRoutes { static final Route create = Route.post('/currency'); static final Route getById = Route.get('/currency/{currencyId}'); static final Route deleteById = Route.delete('/currency/{currencyId}'); - static final Route updateById = Route.patch('/currency/{currencyId}'); + static final Route patchById = Route.patch('/currency/{currencyId}'); +} + +class TransactionRoutes { + const TransactionRoutes._(); + + static final Route getAll = Route.get('/transaction'); + static final Route getById = Route.get('/transaction/{transactionId}'); + static final Route deleteById = Route.delete('/transaction/{transactionId}'); + static final Route patchById = Route.patch('/transaction/{transactionId}'); + static final Route create = Route.post('/transaction'); } diff --git a/lib/src/api/restrr.dart b/lib/src/api/restrr.dart index 45484cc..9422530 100644 --- a/lib/src/api/restrr.dart +++ b/lib/src/api/restrr.dart @@ -60,12 +60,41 @@ abstract class Restrr { Future deleteAllSessions(); + /* Accounts */ + + Future createAccount( + {required String name, required int originalBalance, required Id currencyId, String? description, String? iban}); + + List getAccounts(); + + Future retrieveAccountById(Id id, {bool forceRetrieve = false}); + + Future> retrieveAllAccounts({int page = 1, int limit = 25, bool forceRetrieve = false}); + /* Currencies */ Future createCurrency( {required String name, required String symbol, required int decimalPlaces, String? isoCode}); + List getCurrencies(); + Future retrieveCurrencyById(Id id, {bool forceRetrieve = false}); Future> retrieveAllCurrencies({int page = 1, int limit = 25, bool forceRetrieve = false}); + + /* Transactions */ + + Future createTransaction( + {required int amount, + required Id currencyId, + required DateTime executedAt, + required String name, + String? description, + Id? sourceId, + Id? destinationId, + Id? budgetId}); + + Future retrieveTransactionById(Id id, {bool forceRetrieve = false}); + + Future> retrieveAllTransactions({int page = 1, int limit = 25, bool forceRetrieve = false}); } diff --git a/lib/src/api/restrr_builder.dart b/lib/src/api/restrr_builder.dart index 0ef5eca..4ce2b31 100644 --- a/lib/src/api/restrr_builder.dart +++ b/lib/src/api/restrr_builder.dart @@ -1,6 +1,7 @@ import '../../restrr.dart'; import '../internal/requests/responses/rest_response.dart'; import '../internal/restrr_impl.dart'; +import '../internal/utils/request_utils.dart'; /// A builder for creating a new [Restrr] instance. /// The [Restrr] instance is created by calling [create]. @@ -28,7 +29,7 @@ class RestrrBuilder { 'session_name': sessionName, }, noAuth: true, - mapper: (json) => apiImpl.entityBuilder.buildSession(json)); + mapper: (json) => apiImpl.entityBuilder.buildPartialSession(json)); }); } @@ -37,11 +38,12 @@ class RestrrBuilder { return apiImpl.requestHandler.apiRequest( route: SessionRoutes.refresh.compile(), bearerTokenOverride: sessionToken, - mapper: (json) => apiImpl.entityBuilder.buildSession(json)); + mapper: (json) => apiImpl.entityBuilder.buildPartialSession(json)); }); } - Future _handleAuthProcess({required Future> Function(RestrrImpl) authFunction}) async { + Future _handleAuthProcess( + {required Future> Function(RestrrImpl) authFunction}) async { // check if the URI is valid and the API is healthy final ServerInfo statusResponse = await Restrr.checkUri(uri, isWeb: options.isWeb); Restrr.log.config('Host: $uri, API v${statusResponse.apiVersion}'); @@ -59,6 +61,24 @@ class RestrrBuilder { throw ArgumentError('The response data is not a session'); } apiImpl.session = response.data! as Session; + + // Retrieve all accounts & currencies to make them available in the cache + try { + final List accounts = + await RequestUtils.fetchAllPaginated(apiImpl, await apiImpl.retrieveAllAccounts(limit: 50)); + Restrr.log.info('Cached ${accounts.length} account(s)'); + } catch (e) { + Restrr.log.warning('Failed to cache accounts: $e'); + } + + try { + final List currencies = await RequestUtils.fetchAllPaginated( + apiImpl, await apiImpl.retrieveAllCurrencies(limit: 50)); + Restrr.log.info('Cached ${currencies.length} currencies'); + } catch (e) { + Restrr.log.warning('Failed to cache currencies: $e'); + } + apiImpl.eventHandler.fire(ReadyEvent(api: apiImpl)); return apiImpl; } diff --git a/lib/src/internal/cache/batch_cache_view.dart b/lib/src/internal/cache/batch_cache_view.dart index d66af69..404dff6 100644 --- a/lib/src/internal/cache/batch_cache_view.dart +++ b/lib/src/internal/cache/batch_cache_view.dart @@ -2,16 +2,16 @@ import 'package:restrr/src/internal/restrr_impl.dart'; import '../../../restrr.dart'; -class BatchCacheView { +class BatchCacheView, ID extends EntityId> { final RestrrImpl api; BatchCacheView(this.api); - List? _lastSnapshot; + List? _lastSnapshot; - List? get() => _lastSnapshot; + List? get() => _lastSnapshot; - void update(List value) => _lastSnapshot = value; + void update(List value) => _lastSnapshot = value; void clear() => _lastSnapshot = null; diff --git a/lib/src/internal/cache/cache_view.dart b/lib/src/internal/cache/cache_view.dart index 9e84476..763833e 100644 --- a/lib/src/internal/cache/cache_view.dart +++ b/lib/src/internal/cache/cache_view.dart @@ -1,11 +1,12 @@ import '../../../restrr.dart'; import '../restrr_impl.dart'; -class EntityCacheView extends MapCacheView { - EntityCacheView(RestrrImpl api) : super(api, valueFunction: (entity) => entity.id); +class EntityCacheView, ID extends EntityId> extends MapCacheView { + EntityCacheView(RestrrImpl api) : super(api, valueFunction: (entity) => entity.id.value); } -class PageCacheView extends MapCacheView<(int, int), Paginated> { +class PageCacheView, ID extends EntityId> + extends MapCacheView<(int, int), Paginated> { PageCacheView(RestrrImpl api) : super(api, valueFunction: (page) => (page.pageNumber, page.limit)); } @@ -19,8 +20,12 @@ abstract class MapCacheView { V? get(K key) => _cache[key]; + List getAll() => _cache.values.toList(); + V cache(V value) => _cache[valueFunction.call(value)] = value; + V? remove(K key) => _cache.remove(key); + void clear() => _cache.clear(); bool contains(K key) => _cache.containsKey(key); diff --git a/lib/src/internal/entities/account_impl.dart b/lib/src/internal/entities/account_impl.dart new file mode 100644 index 0000000..b904f18 --- /dev/null +++ b/lib/src/internal/entities/account_impl.dart @@ -0,0 +1,91 @@ +import 'package:restrr/src/internal/entities/restrr_entity_impl.dart'; + +import '../../../restrr.dart'; +import '../requests/responses/rest_response.dart'; +import '../utils/request_utils.dart'; + +class AccountIdImpl extends IdImpl implements AccountId { + const AccountIdImpl({required super.api, required super.value}); + + @override + Account? get() => api.accountCache.get(value); + + @override + Future retrieve({forceRetrieve = false}) { + return RequestUtils.getOrRetrieveSingle( + key: this, + cacheView: api.accountCache, + compiledRoute: AccountRoutes.getById.compile(params: [value]), + mapper: (json) => api.entityBuilder.buildAccount(json), + forceRetrieve: forceRetrieve); + } +} + +class AccountImpl extends RestrrEntityImpl implements Account { + @override + final String name; + @override + final String? description; + @override + final String? iban; + @override + final int balance; + @override + final int originalBalance; + @override + final CurrencyId currencyId; + @override + final DateTime createdAt; + + const AccountImpl({ + required super.api, + required super.id, + required this.name, + required this.description, + required this.iban, + required this.balance, + required this.originalBalance, + required this.currencyId, + required this.createdAt, + }); + + @override + Future delete() => RequestUtils.deleteSingle( + compiledRoute: AccountRoutes.deleteById.compile(params: [id.value]), + api: api, + key: id, + cacheView: api.transactionCache); + + @override + Future update( + {String? name, String? description, String? iban, int? originalBalance, Id? currencyId}) async { + if (name == null && description == null && iban == null && originalBalance == null && currencyId == null) { + throw ArgumentError('At least one field must be set'); + } + final RestResponse response = await api.requestHandler.apiRequest( + route: AccountRoutes.patchById.compile(params: [id.value]), + mapper: (json) => api.entityBuilder.buildAccount(json), + body: { + if (name != null) 'name': name, + if (description != null) 'description': description, + if (iban != null) 'iban': iban, + if (originalBalance != null) 'original_balance': originalBalance, + if (currencyId != null) 'currency_id': currencyId, + }); + if (response.hasError) { + throw response.error!; + } + return response.data!; + } + + @override + Future> retrieveAllTransactions({int page = 1, int limit = 25, bool forceRetrieve = false}) { + return RequestUtils.getOrRetrievePage( + pageCache: api.transactionPageCache, + compiledRoute: AccountRoutes.getTransactions.compile(params: [id.value]), + page: page, + limit: limit, + mapper: (json) => api.entityBuilder.buildTransaction(json), + forceRetrieve: forceRetrieve); + } +} diff --git a/lib/src/internal/entities/currency/currency_impl.dart b/lib/src/internal/entities/currency/currency_impl.dart index 37dbd5e..a1f60b0 100644 --- a/lib/src/internal/entities/currency/currency_impl.dart +++ b/lib/src/internal/entities/currency/currency_impl.dart @@ -1,7 +1,24 @@ import 'package:restrr/restrr.dart'; import 'package:restrr/src/internal/entities/restrr_entity_impl.dart'; -class CurrencyImpl extends RestrrEntityImpl implements Currency { +import '../../utils/request_utils.dart'; + +class CurrencyIdImpl extends IdImpl implements CurrencyId { + const CurrencyIdImpl({required super.api, required super.value}); + + @override + Currency? get() => api.currencyCache.get(value); + + @override + Future retrieve({forceRetrieve = false}) => RequestUtils.getOrRetrieveSingle( + key: this, + cacheView: api.currencyCache, + compiledRoute: CurrencyRoutes.getById.compile(params: [value]), + mapper: (json) => api.entityBuilder.buildCurrency(json), + forceRetrieve: forceRetrieve); +} + +class CurrencyImpl extends RestrrEntityImpl implements Currency { @override final String name; @override diff --git a/lib/src/internal/entities/currency/custom_currency_impl.dart b/lib/src/internal/entities/currency/custom_currency_impl.dart index 67874eb..286b387 100644 --- a/lib/src/internal/entities/currency/custom_currency_impl.dart +++ b/lib/src/internal/entities/currency/custom_currency_impl.dart @@ -1,11 +1,12 @@ import 'package:restrr/restrr.dart'; import 'package:restrr/src/internal/requests/responses/rest_response.dart'; +import '../../utils/request_utils.dart'; import 'currency_impl.dart'; class CustomCurrencyImpl extends CurrencyImpl implements CustomCurrency { @override - final int? user; + final UserId? userId; const CustomCurrencyImpl({ required super.api, @@ -14,18 +15,18 @@ class CustomCurrencyImpl extends CurrencyImpl implements CustomCurrency { required super.symbol, required super.isoCode, required super.decimalPlaces, - required this.user, + required this.userId, }); @override - bool isCreatedBy(User user) => this.user == user.id; + bool isCreatedBy(User user) => userId?.value == user.id.value; @override - Future delete() async { - final RestResponse response = - await api.requestHandler.noResponseApiRequest(route: CurrencyRoutes.deleteById.compile(params: [id])); - return response.hasData && response.data!; - } + Future delete() => RequestUtils.deleteSingle( + compiledRoute: CurrencyRoutes.deleteById.compile(params: [id.value]), + api: api, + key: id, + cacheView: api.transactionCache); @override Future update({String? name, String? symbol, String? isoCode, int? decimalPlaces}) async { @@ -33,7 +34,7 @@ class CustomCurrencyImpl extends CurrencyImpl implements CustomCurrency { throw ArgumentError('At least one field must be set'); } final RestResponse response = await api.requestHandler.apiRequest( - route: CurrencyRoutes.updateById.compile(params: [id]), + route: CurrencyRoutes.patchById.compile(params: [id.value]), mapper: (json) => api.entityBuilder.buildCurrency(json), body: { if (name != null) 'name': name, diff --git a/lib/src/internal/entities/restrr_entity_impl.dart b/lib/src/internal/entities/restrr_entity_impl.dart index ba948cf..1f0e0c3 100644 --- a/lib/src/internal/entities/restrr_entity_impl.dart +++ b/lib/src/internal/entities/restrr_entity_impl.dart @@ -1,11 +1,20 @@ import '../../../restrr.dart'; import '../restrr_impl.dart'; -class RestrrEntityImpl implements RestrrEntity { +abstract class IdImpl implements EntityId { @override final RestrrImpl api; @override - final Id id; + final Id value; + + const IdImpl({required this.api, required this.value}); +} + +class RestrrEntityImpl> implements RestrrEntity { + @override + final RestrrImpl api; + @override + final ID id; const RestrrEntityImpl({ required this.api, diff --git a/lib/src/internal/entities/session/partial_session_impl.dart b/lib/src/internal/entities/session/partial_session_impl.dart index b14643f..be9d633 100644 --- a/lib/src/internal/entities/session/partial_session_impl.dart +++ b/lib/src/internal/entities/session/partial_session_impl.dart @@ -1,8 +1,24 @@ import 'package:restrr/src/internal/entities/restrr_entity_impl.dart'; import '../../../../restrr.dart'; +import '../../utils/request_utils.dart'; -class PartialSessionImpl extends RestrrEntityImpl implements PartialSession { +class PartialSessionIdImpl extends IdImpl implements PartialSessionId { + const PartialSessionIdImpl({required super.api, required super.value}); + + @override + PartialSession? get() => api.sessionCache.get(value); + + @override + Future retrieve({forceRetrieve = false}) => RequestUtils.getOrRetrieveSingle( + key: this, + cacheView: api.sessionCache, + compiledRoute: SessionRoutes.getById.compile(params: [value]), + mapper: (json) => api.entityBuilder.buildPartialSession(json), + forceRetrieve: forceRetrieve); +} + +class PartialSessionImpl extends RestrrEntityImpl implements PartialSession { @override final String? name; @override @@ -22,8 +38,9 @@ class PartialSessionImpl extends RestrrEntityImpl implements PartialSession { }); @override - Future delete() async { - final response = await api.requestHandler.noResponseApiRequest(route: SessionRoutes.getById.compile(params: [id])); - return response.hasData && response.data!; - } + Future delete() => RequestUtils.deleteSingle( + compiledRoute: SessionRoutes.deleteById.compile(params: [id.value]), + api: api, + key: id, + cacheView: api.transactionCache); } diff --git a/lib/src/internal/entities/session/session_impl.dart b/lib/src/internal/entities/session/session_impl.dart index 6cb5cfc..59afc64 100644 --- a/lib/src/internal/entities/session/session_impl.dart +++ b/lib/src/internal/entities/session/session_impl.dart @@ -6,13 +6,12 @@ class SessionImpl extends PartialSessionImpl implements Session { @override final String token; - const SessionImpl({ - required super.api, - required super.id, - required super.name, - required super.createdAt, - required super.expiresAt, - required super.user, - required this.token - }); -} \ No newline at end of file + const SessionImpl( + {required super.api, + required super.id, + required super.name, + required super.createdAt, + required super.expiresAt, + required super.user, + required this.token}); +} diff --git a/lib/src/internal/entities/transaction_impl.dart b/lib/src/internal/entities/transaction_impl.dart new file mode 100644 index 0000000..ba33fb4 --- /dev/null +++ b/lib/src/internal/entities/transaction_impl.dart @@ -0,0 +1,109 @@ +import 'package:restrr/src/internal/entities/restrr_entity_impl.dart'; + +import '../../../restrr.dart'; +import '../requests/responses/rest_response.dart'; +import '../utils/request_utils.dart'; + +class TransactionIdImpl extends IdImpl implements TransactionId { + const TransactionIdImpl({required super.api, required super.value}); + + @override + Transaction? get() => api.transactionCache.get(value); + + @override + Future retrieve({forceRetrieve = false}) => RequestUtils.getOrRetrieveSingle( + key: this, + cacheView: api.transactionCache, + compiledRoute: TransactionRoutes.getById.compile(params: [value]), + mapper: (json) => api.entityBuilder.buildTransaction(json), + forceRetrieve: forceRetrieve); +} + +class TransactionImpl extends RestrrEntityImpl implements Transaction { + @override + final AccountId? sourceId; + @override + final AccountId? destinationId; + @override + final int amount; + @override + final CurrencyId currencyId; + @override + final String name; + @override + final String? description; + @override + final EntityId? budgetId; + @override + final DateTime createdAt; + @override + final DateTime executedAt; + + const TransactionImpl({ + required super.api, + required super.id, + required this.sourceId, + required this.destinationId, + required this.amount, + required this.currencyId, + required this.name, + required this.description, + required this.budgetId, + required this.createdAt, + required this.executedAt, + }) : assert(sourceId != null || destinationId != null); + + @override + TransactionType get type { + if (sourceId != null && destinationId != null) { + return TransactionType.transfer; + } + if (sourceId != null) { + return TransactionType.withdrawal; + } + return TransactionType.deposit; + } + + @override + Future delete() => RequestUtils.deleteSingle( + compiledRoute: TransactionRoutes.deleteById.compile(params: [id.value]), + api: api, + key: id, + cacheView: api.transactionCache); + + @override + Future update( + {Id? sourceId, + Id? destinationId, + int? amount, + Id? currencyId, + String? name, + String? description, + Id? budgetId, + DateTime? executedAt}) async { + if (sourceId == null && + destinationId == null && + amount == null && + currencyId == null && + name == null && + description == null && + budgetId == null && + executedAt == null) { + throw ArgumentError('At least one field must be set'); + } + final RestResponse response = await api.requestHandler.apiRequest( + route: TransactionRoutes.patchById.compile(params: [id.value]), + mapper: (json) => api.entityBuilder.buildTransaction(json), + body: { + if (sourceId != null) 'source_id': sourceId, + if (destinationId != null) 'destination_id': destinationId, + if (amount != null) 'amount': amount, + if (currencyId != null) 'currency_id': currencyId, + if (name != null) 'name': name, + if (description != null) 'description': description, + if (budgetId != null) 'budget_id': budgetId, + if (executedAt != null) 'executed_at': executedAt.toUtc().toIso8601String(), + }); + return response.data!; + } +} diff --git a/lib/src/internal/entities/user_impl.dart b/lib/src/internal/entities/user_impl.dart index 9660399..c3fb5d3 100644 --- a/lib/src/internal/entities/user_impl.dart +++ b/lib/src/internal/entities/user_impl.dart @@ -1,8 +1,26 @@ import 'package:restrr/src/internal/entities/restrr_entity_impl.dart'; import '../../../restrr.dart'; +import '../utils/request_utils.dart'; -class UserImpl extends RestrrEntityImpl implements User { +class UserIdImpl extends IdImpl implements UserId { + const UserIdImpl({required super.api, required super.value}); + + @override + User? get() => api.userCache.get(value); + + @override + Future retrieve({forceRetrieve = false}) { + return RequestUtils.getOrRetrieveSingle( + key: this, + cacheView: api.userCache, + compiledRoute: UserRoutes.getSelf.compile(), + mapper: (json) => api.entityBuilder.buildUser(json), + forceRetrieve: forceRetrieve); + } +} + +class UserImpl extends RestrrEntityImpl implements User { @override final String username; @override diff --git a/lib/src/internal/entity_builder.dart b/lib/src/internal/entity_builder.dart index 0776d56..40a65c6 100644 --- a/lib/src/internal/entity_builder.dart +++ b/lib/src/internal/entity_builder.dart @@ -1,10 +1,12 @@ import 'package:restrr/restrr.dart'; import 'package:restrr/src/internal/restrr_impl.dart'; +import 'entities/account_impl.dart'; import 'entities/currency/currency_impl.dart'; import 'entities/currency/custom_currency_impl.dart'; import 'entities/session/partial_session_impl.dart'; import 'entities/session/session_impl.dart'; +import 'entities/transaction_impl.dart'; import 'entities/user_impl.dart'; /// Defines how to build entities from JSON responses. @@ -16,7 +18,7 @@ class EntityBuilder { Currency buildCurrency(Map json) { CurrencyImpl currency = CurrencyImpl( api: api, - id: json['id'], + id: CurrencyIdImpl(api: api, value: json['id']), name: json['name'], symbol: json['symbol'], decimalPlaces: json['decimal_places'], @@ -32,16 +34,16 @@ class EntityBuilder { symbol: currency.symbol, isoCode: currency.isoCode, decimalPlaces: currency.decimalPlaces, - user: json['user'], + userId: UserIdImpl(api: api, value: json['user']), ); } return api.currencyCache.cache(currency); } - PartialSession buildSession(Map json) { + PartialSession buildPartialSession(Map json) { PartialSessionImpl session = PartialSessionImpl( api: api, - id: json['id'], + id: PartialSessionIdImpl(api: api, value: json['id']), name: json['name'], createdAt: DateTime.parse(json['created_at']), expiresAt: DateTime.parse(json['expires_at']), @@ -58,13 +60,45 @@ class EntityBuilder { token: json['token'], ); } - return session; + return api.sessionCache.cache(session); + } + + Account buildAccount(Map json) { + final AccountImpl account = AccountImpl( + api: api, + id: AccountIdImpl(api: api, value: json['id']), + name: json['name'], + description: json['description'], + iban: json['iban'], + balance: json['balance'], + originalBalance: json['original_balance'], + currencyId: CurrencyIdImpl(api: api, value: json['currency_id']), + createdAt: DateTime.parse(json['created_at']), + ); + return api.accountCache.cache(account); + } + + Transaction buildTransaction(Map json) { + final TransactionImpl transaction = TransactionImpl( + api: api, + id: TransactionIdImpl(api: api, value: json['id']), + sourceId: json['source_id'] != null ? AccountIdImpl(api: api, value: json['source_id']) : null, + destinationId: json['destination_id'] != null ? AccountIdImpl(api: api, value: json['destination_id']) : null, + amount: json['amount'], + currencyId: CurrencyIdImpl(api: api, value: json['currency_id']), + name: json['name'], + description: json['description'], + budgetId: null, // TODO: implement budgets + createdAt: DateTime.parse(json['created_at']), + executedAt: DateTime.parse(json['executed_at']), + ); + return api.transactionCache.cache(transaction); } User buildUser(Map json) { final UserImpl user = UserImpl( api: api, - id: json['id'], + id: UserIdImpl(api: api, value: json['id']), username: json['username'], email: json['email'], displayName: json['display_name'], diff --git a/lib/src/internal/requests/responses/error_response.dart b/lib/src/internal/requests/responses/error_response.dart index 856b87e..1fea4e2 100644 --- a/lib/src/internal/requests/responses/error_response.dart +++ b/lib/src/internal/requests/responses/error_response.dart @@ -2,24 +2,45 @@ import 'package:restrr/src/internal/requests/restrr_errors.dart'; class ErrorResponse { final String details; - final String? reference; + final dynamic reference; final RestrrError? error; + final ApiCode apiCode; - ErrorResponse({ + const ErrorResponse({ required this.details, required this.reference, required this.error, + required this.apiCode, }); static ErrorResponse? tryFromJson(Map? json) { - if (json == null || json.isEmpty || json['details'] == null) { + final ApiCode? apiCode = ApiCode.tryFromJson(json?['api_code']); + if (json == null || json.isEmpty || apiCode == null || json['details'] == null) { return null; } - final RestrrError? error = RestrrError.fromStatusMessage(json['details']); + final RestrrError? error = RestrrError.fromCode(apiCode.code); return ErrorResponse( details: json['details'], reference: json['reference'], error: error, + apiCode: apiCode, + ); + } +} + +class ApiCode { + final int code; + final String message; + + const ApiCode(this.code, this.message); + + static ApiCode? tryFromJson(Map? json) { + if (json == null || json.isEmpty || json['code'] == null) { + return null; + } + return ApiCode( + json['code'], + json['message'], ); } } diff --git a/lib/src/internal/requests/restrr_errors.dart b/lib/src/internal/requests/restrr_errors.dart index 63df3d9..c01d27c 100644 --- a/lib/src/internal/requests/restrr_errors.dart +++ b/lib/src/internal/requests/restrr_errors.dart @@ -2,17 +2,34 @@ import 'package:restrr/restrr.dart'; import 'package:restrr/src/internal/requests/responses/rest_response.dart'; enum RestrrError { - unknown(0xFFF0, 'Unknown error occurred'), - internalServerError(0xFFF1, 'Internal server error occurred'), - serviceUnavailable(0xFFF3, 'Service is unavailable'), - - invalidSession(0x0001, 'Session expired or invalid!'), - sessionLimitReached(0x0002, 'Session limit reached!'), - signedIn(0x0003, 'User is signed in.'), - invalidCredentials(0x0004, 'Invalid credentials provided.'), - resourceNotFound(0x0005, 'Resource not found.'), - unauthorized(0x0006, 'Unauthorized.'), - noTokenProvided(0x0007, 'No token provided.'), + /* Not thrown by the server directly, but still server-related */ + internalServerError(0000, 'Internal server error!'), + serviceUnavailable(0001, 'Service unavailable!'), + + /* Authentication errors */ + invalidSession(1000, 'Invalid session!'), + sessionLimitReached(1001, 'Session limit reached!'), + invalidCredentials(1002, 'Invalid credentials provided!'), + unauthorized(1004, 'Unauthorized!'), + noTokenProvided(1006, 'No bearer token provided!'), + + /* Client errors */ + resourceNotFound(1100, 'Requested resource was not found!'), + serializationError(1101, 'Serialization error!'), + missingPermissions(1102, 'Missing permissions!'), + + /* Validation errors */ + jsonPayloadValidationError(1200, 'JSON payload validation error!'), + genericValidationError(1201, 'Validation error!'), + + /* Server errors */ + entityError(1300, "DB-Entity error!"), + dbError(1301, "Database error!"), + redisError(1302, "Redis error!"), + + /* Misc errors */ + actixError(9000, "Actix error!"), + unknown(9999, 'Unknown error!'), ; final int code; @@ -22,9 +39,9 @@ enum RestrrError { // TODO: replace this with status code in the future - static RestrrError? fromStatusMessage(String message) { + static RestrrError? fromCode(int errorCode) { for (RestrrError value in RestrrError.values) { - if (value.message == message) { + if (value.code == errorCode) { return value; } } diff --git a/lib/src/internal/restrr_impl.dart b/lib/src/internal/restrr_impl.dart index cf364f9..645f36d 100644 --- a/lib/src/internal/restrr_impl.dart +++ b/lib/src/internal/restrr_impl.dart @@ -1,9 +1,14 @@ import 'package:restrr/src/internal/cache/cache_view.dart'; +import 'package:restrr/src/internal/entities/account_impl.dart'; +import 'package:restrr/src/internal/entities/currency/currency_impl.dart'; +import 'package:restrr/src/internal/entities/transaction_impl.dart'; +import 'package:restrr/src/internal/entities/user_impl.dart'; import 'package:restrr/src/internal/requests/responses/rest_response.dart'; import 'package:restrr/src/internal/utils/request_utils.dart'; import '../../restrr.dart'; import '../api/events/event_handler.dart'; +import 'entities/session/partial_session_impl.dart'; import 'entity_builder.dart'; class RestrrImpl implements Restrr { @@ -18,12 +23,16 @@ class RestrrImpl implements Restrr { /* Caches */ - late final EntityCacheView currencyCache = EntityCacheView(this); - late final EntityCacheView sessionCache = EntityCacheView(this); - late final EntityCacheView userCache = EntityCacheView(this); + late final EntityCacheView currencyCache = EntityCacheView(this); + late final EntityCacheView sessionCache = EntityCacheView(this); + late final EntityCacheView accountCache = EntityCacheView(this); + late final EntityCacheView transactionCache = EntityCacheView(this); + late final EntityCacheView userCache = EntityCacheView(this); - late final PageCacheView currencyPageCache = PageCacheView(this); - late final PageCacheView sessionPageCache = PageCacheView(this); + late final PageCacheView currencyPageCache = PageCacheView(this); + late final PageCacheView sessionPageCache = PageCacheView(this); + late final PageCacheView accountPageCache = PageCacheView(this); + late final PageCacheView transactionPageCache = PageCacheView(this); RestrrImpl({required this.routeOptions, required Map eventMap, this.options = const RestrrOptions()}) : eventHandler = RestrrEventHandler(eventMap); @@ -41,12 +50,7 @@ class RestrrImpl implements Restrr { @override Future retrieveSelf({bool forceRetrieve = false}) async { - return RequestUtils.getOrRetrieveSingle( - key: selfUser.id, - cacheView: userCache, - compiledRoute: UserRoutes.getSelf.compile(), - mapper: (json) => entityBuilder.buildUser(json), - forceRetrieve: forceRetrieve); + return UserIdImpl(api: this, value: session.user.id.value).retrieve(forceRetrieve: forceRetrieve); } /* Sessions */ @@ -57,18 +61,13 @@ class RestrrImpl implements Restrr { key: session.id, cacheView: sessionCache, compiledRoute: SessionRoutes.getCurrent.compile(), - mapper: (json) => entityBuilder.buildSession(json), + mapper: (json) => entityBuilder.buildPartialSession(json), forceRetrieve: forceRetrieve); } @override Future retrieveSessionById(Id id, {bool forceRetrieve = false}) { - return RequestUtils.getOrRetrieveSingle( - key: id, - cacheView: sessionCache, - compiledRoute: SessionRoutes.getById.compile(params: [id]), - mapper: (json) => entityBuilder.buildSession(json), - forceRetrieve: forceRetrieve); + return PartialSessionIdImpl(api: this, value: id).retrieve(forceRetrieve: forceRetrieve); } @override @@ -78,7 +77,7 @@ class RestrrImpl implements Restrr { compiledRoute: SessionRoutes.getAll.compile(), page: page, limit: limit, - mapper: (json) => entityBuilder.buildSession(json), + mapper: (json) => entityBuilder.buildPartialSession(json), forceRetrieve: forceRetrieve); } @@ -100,6 +99,48 @@ class RestrrImpl implements Restrr { return response.hasData && response.data!; } + /* Accounts */ + + @override + Future createAccount( + {required String name, + required int originalBalance, + required Id currencyId, + String? description, + String? iban}) async { + final RestResponse response = await requestHandler + .apiRequest(route: AccountRoutes.create.compile(), mapper: (json) => entityBuilder.buildAccount(json), body: { + 'name': name, + 'original_balance': originalBalance, + 'currency_id': currencyId, + if (description != null) 'description': description, + if (iban != null) 'iban': iban + }); + if (response.hasError) { + throw response.error!; + } + return accountCache.cache(response.data!); + } + + @override + List getAccounts() => accountCache.getAll(); + + @override + Future retrieveAccountById(Id id, {bool forceRetrieve = false}) async { + return AccountIdImpl(api: this, value: id).retrieve(forceRetrieve: forceRetrieve); + } + + @override + Future> retrieveAllAccounts({int page = 1, int limit = 25, bool forceRetrieve = false}) async { + return RequestUtils.getOrRetrievePage( + pageCache: accountPageCache, + compiledRoute: AccountRoutes.getAll.compile(), + page: page, + limit: limit, + mapper: (json) => entityBuilder.buildAccount(json), + forceRetrieve: forceRetrieve); + } + /* Currencies */ @override @@ -118,18 +159,15 @@ class RestrrImpl implements Restrr { throw response.error!; } // invalidate cache - currencyCache.clear(); - return response.data!; + return currencyCache.cache(response.data!); } + @override + List getCurrencies() => currencyCache.getAll(); + @override Future retrieveCurrencyById(Id id, {bool forceRetrieve = false}) async { - return RequestUtils.getOrRetrieveSingle( - key: id, - cacheView: currencyCache, - compiledRoute: CurrencyRoutes.getById.compile(params: [id]), - mapper: (json) => entityBuilder.buildCurrency(json), - forceRetrieve: forceRetrieve); + return CurrencyIdImpl(api: this, value: id).retrieve(forceRetrieve: forceRetrieve); } @override @@ -142,4 +180,57 @@ class RestrrImpl implements Restrr { mapper: (json) => entityBuilder.buildCurrency(json), forceRetrieve: forceRetrieve); } + + /* Transactions */ + + @override + Future createTransaction( + {required int amount, + required Id currencyId, + required DateTime executedAt, + required String name, + String? description, + Id? sourceId, + Id? destinationId, + Id? budgetId}) async { + if (sourceId == null && destinationId == null) { + throw ArgumentError('Either source or destination must be set!'); + } + final RestResponse response = await requestHandler.apiRequest( + route: TransactionRoutes.create.compile(), + mapper: (json) => entityBuilder.buildTransaction(json), + body: { + 'amount': amount, + 'currency_id': currencyId, + 'executed_at': executedAt.toUtc().toIso8601String(), + 'name': name, + if (description != null) 'description': description, + if (sourceId != null) 'source_id': sourceId, + if (destinationId != null) 'destination_id': destinationId, + if (budgetId != null) 'budget_id': budgetId + }); + if (response.hasError) { + throw response.error!; + } + // invalidate cache + transactionCache.clear(); + return response.data!; + } + + @override + Future retrieveTransactionById(Id id, {bool forceRetrieve = false}) async { + return TransactionIdImpl(api: this, value: id).retrieve(forceRetrieve: forceRetrieve); + } + + @override + Future> retrieveAllTransactions( + {int page = 1, int limit = 25, bool forceRetrieve = false}) async { + return RequestUtils.getOrRetrievePage( + pageCache: transactionPageCache, + compiledRoute: TransactionRoutes.getAll.compile(), + page: page, + limit: limit, + mapper: (json) => entityBuilder.buildTransaction(json), + forceRetrieve: forceRetrieve); + } } diff --git a/lib/src/internal/utils/request_utils.dart b/lib/src/internal/utils/request_utils.dart index 1ce6a5d..738dea1 100644 --- a/lib/src/internal/utils/request_utils.dart +++ b/lib/src/internal/utils/request_utils.dart @@ -8,17 +8,48 @@ import '../requests/responses/rest_response.dart'; class RequestUtils { const RequestUtils._(); - static Future getOrRetrieveSingle( - {required Id key, - required EntityCacheView cacheView, + static Future> fetchAllPaginated, ID extends EntityId>( + Restrr api, Paginated firstBatch, + {Duration? delay}) async { + final List all = [...firstBatch.items]; + Paginated current = firstBatch; + while (current.hasNext) { + if (delay != null) { + await Future.delayed(delay); + } + final Paginated next = await current.nextPage!.call(api); + current = next; + all.addAll(next.items); + } + return all; + } + + static Future deleteSingle, ID extends EntityId>( + {required CompiledRoute compiledRoute, + required Restrr api, + required EntityId key, + required EntityCacheView cacheView, + bool noAuth = false}) async { + final RestResponse response = await RequestHandler.noResponseRequest( + route: compiledRoute, routeOptions: api.routeOptions, bearerToken: noAuth ? null : api.session.token); + if (response.hasData && response.data!) { + cacheView.remove(key.value); + return true; + } + return false; + } + + static Future getOrRetrieveSingle, ID extends EntityId>( + {required EntityId key, + required EntityCacheView cacheView, required CompiledRoute compiledRoute, - required T Function(dynamic) mapper, + required E Function(dynamic) mapper, bool forceRetrieve = false, bool noAuth = false}) async { - if (!forceRetrieve && cacheView.contains(key)) { - return cacheView.get(key)!; + if (!forceRetrieve && cacheView.contains(key.value)) { + return cacheView.get(key.value)!; } - final RestResponse response = await RequestHandler.request( + final RestResponse response = await RequestHandler.request( route: compiledRoute, routeOptions: cacheView.api.routeOptions, bearerToken: noAuth ? null : cacheView.api.session.token, @@ -29,16 +60,16 @@ class RequestUtils { return response.data!; } - static Future> getOrRetrieveMulti( - {required BatchCacheView batchCache, + static Future> getOrRetrieveMulti, ID extends EntityId>( + {required BatchCacheView batchCache, required CompiledRoute compiledRoute, - required T Function(dynamic) mapper, + required E Function(dynamic) mapper, bool forceRetrieve = false, bool noAuth = false}) async { if (!forceRetrieve && batchCache.hasSnapshot) { return batchCache.get()!; } - final RestResponse> response = await RequestHandler.multiRequest( + final RestResponse> response = await RequestHandler.multiRequest( route: compiledRoute, routeOptions: batchCache.api.routeOptions, bearerToken: noAuth ? null : batchCache.api.session.token, @@ -46,15 +77,15 @@ class RequestUtils { if (response.hasError) { throw response.error!; } - final List remote = response.data!; + final List remote = response.data!; batchCache.update(remote); return remote; } - static Future> getOrRetrievePage( - {required PageCacheView pageCache, + static Future> getOrRetrievePage, ID extends EntityId>( + {required PageCacheView pageCache, required CompiledRoute compiledRoute, - required T Function(dynamic) mapper, + required E Function(dynamic) mapper, required int page, required int limit, bool forceRetrieve = false, @@ -62,7 +93,7 @@ class RequestUtils { if (!forceRetrieve && pageCache.contains((page, limit))) { return pageCache.get((page, limit))!; } - final RestResponse> response = await RequestHandler.paginatedRequest( + final RestResponse> response = await RequestHandler.paginatedRequest( route: compiledRoute, routeOptions: pageCache.api.routeOptions, page: page, @@ -72,7 +103,7 @@ class RequestUtils { if (response.hasError) { throw response.error!; } - final Paginated remote = (response as PaginatedResponse).toPage(); + final Paginated remote = (response as PaginatedResponse).toPage(); pageCache.cache(remote); return remote; } diff --git a/pubspec.yaml b/pubspec.yaml index c43bc0c..21954d7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: restrr description: Dart package which allows to communicate with the financrr REST API. -version: 0.7.0 +version: 0.8.0 repository: https://github.com/financrr/restrr environment: diff --git a/test/restrr_entity_test.dart b/test/restrr_entity_test.dart index 00046d7..5a12a20 100644 --- a/test/restrr_entity_test.dart +++ b/test/restrr_entity_test.dart @@ -38,6 +38,35 @@ const String sessionJson = ''' } '''; +const String accountJson = ''' +{ + "id": 1, + "name": "Cash", + "description": "Cash in hand", + "iban": null, + "balance": 0, + "original_balance": 0, + "currency_id": 1, + "created_at": "+002024-02-17T20:48:43.391176000Z" +} +'''; + +const String transactionJson = ''' +{ + "id": 1, + "source_id": 1, + "destination_id": null, + "amount": 100, + "currency_id": 1, + "name": "Initial balance", + "description": "Initial balance", + "budget_id": null, + "created_at": "+002024-02-17T20:48:43.391176000Z", + "executed_at": "+002024-02-17T20:48:43.391176000Z" +} +'''; + + void main() { late RestrrImpl api; @@ -47,31 +76,57 @@ void main() { }); group('[EntityBuilder] ', () { - test('.buildUser', () async { - final User user = api.entityBuilder.buildUser(jsonDecode(userJson)); - expect(user.id, 1); - expect(user.username, 'admin'); - expect(user.email, null); - expect(user.createdAt, DateTime.parse('+002024-02-17T20:48:43.391176000Z')); - expect(user.isAdmin, true); - }); - test('.buildCurrency', () { final Currency currency = api.entityBuilder.buildCurrency(jsonDecode(currencyJson)); - expect(currency.id, 1); + expect(currency.id.value, 1); expect(currency.name, 'US Dollar'); expect(currency.symbol, '\$'); expect(currency.isoCode, 'USD'); expect(currency.decimalPlaces, 2); }); - test('.buildSession', () { - final Session session = api.entityBuilder.buildSession(jsonDecode(sessionJson)) as Session; - expect(session.id, 1); + test('.buildPartialSession', () { + final Session session = api.entityBuilder.buildPartialSession(jsonDecode(sessionJson)) as Session; + expect(session.id.value, 1); expect(session.token, 'abc'); - expect(session.user.id, 1); + expect(session.user.id.value, 1); expect(session.createdAt, DateTime.parse('+002024-02-17T20:48:43.391176000Z')); expect(session.expiresAt, DateTime.parse('+002024-02-17T20:48:43.391176000Z')); }); + + test('.buildAccount', () { + final Account account = api.entityBuilder.buildAccount(jsonDecode(accountJson)); + expect(account.id.value, 1); + expect(account.name, 'Cash'); + expect(account.description, 'Cash in hand'); + expect(account.iban, null); + expect(account.balance, 0); + expect(account.originalBalance, 0); + expect(account.currencyId.value, 1); + expect(account.createdAt, DateTime.parse('+002024-02-17T20:48:43.391176000Z')); + }); + + test('.buildTransaction', () { + final Transaction transaction = api.entityBuilder.buildTransaction(jsonDecode(transactionJson)); + expect(transaction.id.value, 1); + expect(transaction.sourceId?.value, 1); + expect(transaction.destinationId?.value, null); + expect(transaction.amount, 100); + expect(transaction.currencyId.value, 1); + expect(transaction.name, 'Initial balance'); + expect(transaction.description, 'Initial balance'); + expect(transaction.budgetId, null); + expect(transaction.createdAt, DateTime.parse('+002024-02-17T20:48:43.391176000Z')); + expect(transaction.executedAt, DateTime.parse('+002024-02-17T20:48:43.391176000Z')); + }); + + test('.buildUser', () { + final User user = api.entityBuilder.buildUser(jsonDecode(userJson)); + expect(user.id.value, 1); + expect(user.username, 'admin'); + expect(user.email, null); + expect(user.createdAt, DateTime.parse('+002024-02-17T20:48:43.391176000Z')); + expect(user.isAdmin, true); + }); }); }