finnow/finnow-api/source/account/api.d

226 lines
8.4 KiB
D

/**
* 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);
});
}