/** * This module defines the API endpoints for dealing with Accounts directly, * including any data-transfer objects that are needed. */ module account.api; import handy_http_primitives; import handy_http_data.json; import handy_http_handlers.path_handler; import std.datetime; import profile.service; import profile.data; import account.model; import account.service; import util.money; import util.pagination; import util.data; import account.data; import attachment.data; import attachment.dto; /// The data the API provides for an Account entity. struct AccountResponse { import asdf : serdeTransformOut; ulong id; string createdAt; bool archived; string type; string numberSuffix; string name; Currency currency; string description; @serdeTransformOut!serializeOptional Optional!long currentBalance; static AccountResponse of(in Account account, Optional!long currentBalance) { AccountResponse r; r.id = account.id; r.createdAt = account.createdAt.toISOExtString(); r.archived = account.archived; r.type = account.type.id; r.numberSuffix = account.numberSuffix; r.name = account.name; r.currency = account.currency; r.description = account.description; r.currentBalance = currentBalance; return r; } } void handleGetAccounts(ref ServerHttpRequest request, ref ServerHttpResponse response) { import std.algorithm; import std.array; auto ds = getProfileDataSource(request); auto accounts = ds.getAccountRepository().findAll() .map!(a => AccountResponse.of(a, getBalance(ds, a.id))).array; writeJsonBody(response, accounts); } void handleGetAccount(ref ServerHttpRequest request, ref ServerHttpResponse response) { ulong accountId = request.getPathParamAs!ulong("accountId"); auto ds = getProfileDataSource(request); auto account = ds.getAccountRepository().findById(accountId) .orElseThrow(() => new HttpStatusException(HttpStatus.NOT_FOUND)); writeJsonBody(response, AccountResponse.of(account, getBalance(ds, account.id))); } // The data provided by a user to create a new account. struct AccountCreationPayload { string type; string numberSuffix; string name; string currency; string description; } void handleCreateAccount(ref ServerHttpRequest request, ref ServerHttpResponse response) { auto ds = getProfileDataSource(request); AccountCreationPayload payload = readJsonBodyAs!AccountCreationPayload(request); // TODO: Validate the account creation payload. AccountType type = AccountType.fromId(payload.type); Currency currency = Currency.ofCode(payload.currency); Account account = ds.getAccountRepository().insert( type, payload.numberSuffix, payload.name, currency, payload.description ); writeJsonBody(response, AccountResponse.of(account, getBalance(ds, account.id))); } void handleUpdateAccount(ref ServerHttpRequest request, ref ServerHttpResponse response) { ulong accountId = request.getPathParamAs!ulong("accountId"); AccountCreationPayload payload = readJsonBodyAs!AccountCreationPayload(request); auto ds = getProfileDataSource(request); AccountRepository repo = ds.getAccountRepository(); auto account = repo.findById(accountId).orElseThrow(() => new HttpStatusException(HttpStatus.NOT_FOUND)); Account updated = repo.update(accountId, Account( account.id, account.createdAt, account.archived, AccountType.fromId(payload.type), payload.numberSuffix, payload.name, Currency.ofCode(payload.currency), payload.description )); writeJsonBody(response, AccountResponse.of(updated, getBalance(ds, updated.id))); } void handleDeleteAccount(ref ServerHttpRequest request, ref ServerHttpResponse response) { ulong accountId = request.getPathParamAs!ulong("accountId"); auto ds = getProfileDataSource(request); ds.getAccountRepository().deleteById(accountId); } const PageRequest VALUE_RECORD_DEFAULT_PAGE_REQUEST = PageRequest(1, 10, [Sort("timestamp", SortDir.DESC)]); struct AccountValueRecordResponse { ulong id; string timestamp; ulong accountId; string type; long value; Currency currency; AttachmentResponse[] attachments; static AccountValueRecordResponse of(in AccountValueRecord vr, AttachmentRepository attachmentRepo) { import std.algorithm : map; import std.array : array; return AccountValueRecordResponse( vr.id, vr.timestamp.toISOExtString(), vr.accountId, vr.type, vr.value, vr.currency, attachmentRepo.findAllByValueRecordId(vr.id) .map!(AttachmentResponse.of) .array ); } } void handleGetValueRecords(ref ServerHttpRequest request, ref ServerHttpResponse response) { ulong accountId = request.getPathParamAs!ulong("accountId"); auto ds = getProfileDataSource(request); scope attachmentRepo = ds.getAttachmentRepository(); auto page = ds.getAccountValueRecordRepository() .findAllByAccountId(accountId, PageRequest.parse(request, VALUE_RECORD_DEFAULT_PAGE_REQUEST)) .mapTo!()((vr) => AccountValueRecordResponse.of(vr, attachmentRepo)); writeJsonBody(response, page); } void handleGetValueRecord(ref ServerHttpRequest request, ref ServerHttpResponse response) { ulong accountId = request.getPathParamAs!ulong("accountId"); ulong valueRecordId = request.getPathParamAs!ulong("valueRecordId"); auto ds = getProfileDataSource(request); auto attachmentRepo = ds.getAttachmentRepository(); auto record = ds.getAccountValueRecordRepository().findById(accountId, valueRecordId) .orElseThrow(() => new HttpStatusException(HttpStatus.NOT_FOUND)); writeJsonBody(response, AccountValueRecordResponse.of(record, attachmentRepo)); } struct ValueRecordCreationPayload { string timestamp; string type; long value; } void handleCreateValueRecord(ref ServerHttpRequest request, ref ServerHttpResponse response) { ulong accountId = request.getPathParamAs!ulong("accountId"); ProfileDataSource ds = getProfileDataSource(request); Account account = ds.getAccountRepository().findById(accountId) .orElseThrow(() => new HttpStatusException(HttpStatus.NOT_FOUND)); AccountValueRecordRepository valueRecordRepo = ds.getAccountValueRecordRepository(); AttachmentRepository attachmentRepo = ds.getAttachmentRepository(); auto fullPayload = parseMultipartFilesAndBody!ValueRecordCreationPayload(request); ValueRecordCreationPayload payload = fullPayload.payload; SysTime timestamp = SysTime.fromISOExtString(payload.timestamp); AccountValueRecordType type = AccountValueRecordType.BALANCE; // TODO: Support more types. ulong valueRecordId; ds.doTransaction(() { AccountValueRecord record = valueRecordRepo.insert( timestamp, account.id, type, payload.value, account.currency ); foreach (attachment; fullPayload.files) { ulong attachmentId = attachmentRepo.save( timestamp, attachment.name, attachment.contentType, attachment.content); valueRecordRepo.linkAttachment(record.id, attachmentId); } valueRecordId = record.id; }); writeJsonBody( response, AccountValueRecordResponse.of( valueRecordRepo.findById(accountId, valueRecordId).orElseThrow(), attachmentRepo ) ); } void handleDeleteValueRecord(ref ServerHttpRequest request, ref ServerHttpResponse response) { ulong accountId = request.getPathParamAs!ulong("accountId"); ulong valueRecordId = request.getPathParamAs!ulong("valueRecordId"); ProfileDataSource ds = getProfileDataSource(request); AccountValueRecordRepository valueRecordRepo = ds.getAccountValueRecordRepository(); AttachmentRepository attachmentRepo = ds.getAttachmentRepository(); AccountValueRecord valueRecord = valueRecordRepo.findById(accountId, valueRecordId) .orElseThrow(() => new HttpStatusException(HttpStatus.NOT_FOUND)); ds.doTransaction(() { // First delete all attachments. foreach (a; attachmentRepo.findAllByValueRecordId(valueRecord.id)) { attachmentRepo.remove(a.id); } valueRecordRepo.deleteById(accountId, valueRecordId); }); }