/** * 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 profile.api : PROFILE_PATH; import account.model; import account.service; import account.dto; import util.money; import util.pagination; import util.data; import account.data; import attachment.data; import attachment.dto; const ACCOUNT_PATH = PROFILE_PATH ~ "/accounts/:accountId:ulong"; @PathMapping(HttpMethod.GET, PROFILE_PATH ~ "/accounts") 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); } @PathMapping(HttpMethod.GET, ACCOUNT_PATH) 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))); } @PathMapping(HttpMethod.POST, PROFILE_PATH ~ "/accounts") 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))); } @PathMapping(HttpMethod.PUT, ACCOUNT_PATH) 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))); } @PathMapping(HttpMethod.DELETE, ACCOUNT_PATH) void handleDeleteAccount(ref ServerHttpRequest request, ref ServerHttpResponse response) { ulong accountId = request.getPathParamAs!ulong("accountId"); auto ds = getProfileDataSource(request); ds.getAccountRepository().deleteById(accountId); } @PathMapping(HttpMethod.GET, ACCOUNT_PATH ~ "/balance") void handleGetAccountBalance(ref ServerHttpRequest request, ref ServerHttpResponse response) { ulong accountId = request.getPathParamAs!ulong("accountId"); auto ds = getProfileDataSource(request); SysTime timestamp = Clock.currTime(UTC()); string providedTimestamp = request.getParamAs!string("timestamp"); if (providedTimestamp != null && providedTimestamp.length > 0) { timestamp = SysTime.fromISOExtString(providedTimestamp); } Optional!long balance = getBalance(ds, accountId, timestamp); if (balance.isNull) { response.writeBodyString("{\"balance\": null}", ContentTypes.APPLICATION_JSON); } else { import std.conv : to; response.writeBodyString("{\"balance\": " ~ balance.value.to!string ~ "}", ContentTypes.APPLICATION_JSON); } } @PathMapping(HttpMethod.GET, ACCOUNT_PATH ~ "/history") void handleGetAccountHistory(ref ServerHttpRequest request, ref ServerHttpResponse response) { ulong accountId = request.getPathParamOrThrow!ulong("accountId"); PageRequest pagination = PageRequest.parse(request, PageRequest(1, 10, [Sort("timestamp", SortDir.DESC)])); auto ds = getProfileDataSource(request); AccountRepository accountRepo = ds.getAccountRepository(); auto page = accountRepo.getHistory(accountId, pagination); writeHistoryResponse(response, page); } private string serializeAccountHistoryItem(in AccountHistoryItemResponse i) { import asdf : serializeToJson; if (i.type == AccountHistoryItemType.JournalEntry) { return serializeToJson(cast(AccountHistoryJournalEntryItemResponse) i); } else if (i.type == AccountHistoryItemType.ValueRecord) { return serializeToJson(cast(AccountHistoryValueRecordItemResponse) i); } else { return serializeToJson(i); } } private void writeHistoryResponse(ref ServerHttpResponse response, in Page!AccountHistoryItemResponse page) { // Manual serialization of response due to inheritance structure. import asdf; import std.json; string initialJsonObj = serializeToJson(page); JSONValue obj = parseJSON(initialJsonObj); obj.object["items"] = JSONValue.emptyArray; foreach (item; page.items) { string initialItemJson = serializeAccountHistoryItem(item); obj.object["items"].array ~= parseJSON(initialItemJson); } string jsonStr = obj.toJSON(); response.writeBodyString(jsonStr, ContentTypes.APPLICATION_JSON); } @PathMapping(HttpMethod.GET, PROFILE_PATH ~ "/account-balances") void handleGetTotalBalances(ref ServerHttpRequest request, ref ServerHttpResponse response) { auto ds = getProfileDataSource(request); auto balances = getTotalBalanceForAllAccounts(ds); writeJsonBody(response, balances); } // Value records: const PageRequest VALUE_RECORD_DEFAULT_PAGE_REQUEST = PageRequest(1, 10, [Sort("timestamp", SortDir.DESC)]); @PathMapping(HttpMethod.GET, ACCOUNT_PATH ~ "/value-records") 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); } @PathMapping(HttpMethod.GET, ACCOUNT_PATH ~ "/value-records/:valueRecordId:ulong") 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)); } @PathMapping(HttpMethod.POST, ACCOUNT_PATH ~ "/value-records") 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 ) ); } @PathMapping(HttpMethod.DELETE, ACCOUNT_PATH ~ "/value-records/:valueRecordId:ulong") 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); }); }