226 lines
8.4 KiB
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);
|
|
});
|
|
}
|