WIP: Add Drafts, Templates, and Recurring Transactions #45
|
|
@ -0,0 +1,46 @@
|
||||||
|
module util.validation.common;
|
||||||
|
|
||||||
|
import handy_http_primitives.optional;
|
||||||
|
import std.datetime;
|
||||||
|
|
||||||
|
struct ValidationError {
|
||||||
|
string field;
|
||||||
|
string message;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ValidationRule(T) {
|
||||||
|
Optional!ValidationError validate(in T payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidationError[] applyValidationRules(T)(ValidationRule!(T)[] rules, in T payload) {
|
||||||
|
import std.array;
|
||||||
|
auto app = appender!(ValidationError[]);
|
||||||
|
foreach (rule; rules) {
|
||||||
|
auto result = rule.validate(payload);
|
||||||
|
if (result) app ~= result.value;
|
||||||
|
}
|
||||||
|
return app[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper functions:
|
||||||
|
|
||||||
|
void validateTags(in string[] tags) {
|
||||||
|
import std.regex;
|
||||||
|
import handy_http_primitives: HttpStatus, HttpStatusException;
|
||||||
|
foreach (tag; tags) {
|
||||||
|
import std.regex;
|
||||||
|
auto r = ctRegex!(`^[a-z0-9-_]{3,32}$`);
|
||||||
|
if (!matchFirst(tag, r)) {
|
||||||
|
throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Invalid tag: \"" ~ tag ~ "\".");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SysTime validateTimestampFormat(string timestampStr) {
|
||||||
|
import handy_http_primitives: HttpStatus, HttpStatusException;
|
||||||
|
try {
|
||||||
|
return SysTime.fromISOExtString(timestampStr, UTC());
|
||||||
|
} catch (TimeException e) {
|
||||||
|
throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Invalid timestamp format. Expected ISO-8601 datetime.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
module util.validation.draft;
|
||||||
|
|
||||||
|
import handy_http_primitives;
|
||||||
|
import std.datetime;
|
||||||
|
|
||||||
|
import transaction.data;
|
||||||
|
import transaction.dto;
|
||||||
|
import util.validation.common;
|
||||||
|
import account.data;
|
||||||
|
|
||||||
|
void validateDraftPayload(
|
||||||
|
TransactionVendorRepository vendorRepo,
|
||||||
|
TransactionCategoryRepository categoryRepo,
|
||||||
|
AccountRepository accountRepo,
|
||||||
|
in TransactionDraftPayload payload
|
||||||
|
) {
|
||||||
|
if (payload.amount && !payload.currencyCode) {
|
||||||
|
throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Currency is required when saving an amount.");
|
||||||
|
}
|
||||||
|
if (payload.amount && payload.amount.value == 0) {
|
||||||
|
throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Amount should be greater than 0.");
|
||||||
|
}
|
||||||
|
if (payload.timestamp) {
|
||||||
|
validateTimestampFormat(payload.timestamp.value);
|
||||||
|
}
|
||||||
|
if (payload.vendorId && !vendorRepo.existsById(payload.vendorId.value)) {
|
||||||
|
throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Vendor doesn't exist.");
|
||||||
|
}
|
||||||
|
if (payload.categoryId && !categoryRepo.existsById(payload.categoryId.value)) {
|
||||||
|
throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Category doesn't exist.");
|
||||||
|
}
|
||||||
|
if (payload.creditedAccountId && !accountRepo.existsById(payload.creditedAccountId.value)) {
|
||||||
|
throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Credited account doesn't exist.");
|
||||||
|
}
|
||||||
|
if (payload.debitedAccountId && !accountRepo.existsById(payload.debitedAccountId.value)) {
|
||||||
|
throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Debited account doesn't exist.");
|
||||||
|
}
|
||||||
|
validateTags(payload.tags);
|
||||||
|
if (payload.lineItems.length > 0) {
|
||||||
|
long lineItemsTotal = 0;
|
||||||
|
foreach (lineItem; payload.lineItems) {
|
||||||
|
if (lineItem.categoryId && !categoryRepo.existsById(lineItem.categoryId.value)) {
|
||||||
|
throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Line item's category doesn't exist.");
|
||||||
|
}
|
||||||
|
if (lineItem.quantity == 0) {
|
||||||
|
throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Line item's quantity should greater than zero.");
|
||||||
|
}
|
||||||
|
for (ulong i = 0; i < lineItem.quantity; i++) {
|
||||||
|
lineItemsTotal += lineItem.valuePerItem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (payload.amount && lineItemsTotal != payload.amount.value) {
|
||||||
|
throw new HttpStatusException(
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
"Total of all line items doesn't equal the transaction's total."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
module util.validation.transaction;
|
||||||
|
|
||||||
|
import handy_http_primitives;
|
||||||
|
import std.array;
|
||||||
|
import std.datetime;
|
||||||
|
|
||||||
|
import util.validation.common;
|
||||||
|
import transaction.dto;
|
||||||
|
import transaction.data;
|
||||||
|
import account.data;
|
||||||
|
|
||||||
|
// class AtLeastOneLinkedAccountRule : ValidationRule!AddTransactionPayload {
|
||||||
|
// Optional!ValidationError validate(in AddTransactionPayload payload) {
|
||||||
|
// if (!payload.creditedAccountId && !payload.debitedAccountId) {
|
||||||
|
// return ValidationError("creditedAccountId", "At least one account must be linked.").toOptional();
|
||||||
|
// }
|
||||||
|
// return Optional!ValidationError.empty();
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
void validateTransactionPayload(
|
||||||
|
TransactionVendorRepository vendorRepo,
|
||||||
|
TransactionCategoryRepository categoryRepo,
|
||||||
|
AccountRepository accountRepo,
|
||||||
|
in AddTransactionPayload payload
|
||||||
|
) {
|
||||||
|
if (!payload.creditedAccountId && !payload.debitedAccountId) {
|
||||||
|
throw new HttpStatusException(HttpStatus.BAD_REQUEST, "At least one account must be linked.");
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
payload.creditedAccountId &&
|
||||||
|
payload.debitedAccountId &&
|
||||||
|
payload.creditedAccountId.value == payload.debitedAccountId.value
|
||||||
|
) {
|
||||||
|
throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Cannot link the same account as both credit and debit.");
|
||||||
|
}
|
||||||
|
if (payload.amount == 0) {
|
||||||
|
throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Amount should be greater than 0.");
|
||||||
|
}
|
||||||
|
SysTime now = Clock.currTime(UTC());
|
||||||
|
SysTime timestamp = validateTimestampFormat(payload.timestamp);
|
||||||
|
if (timestamp > now) {
|
||||||
|
throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Cannot create transaction in the future.");
|
||||||
|
}
|
||||||
|
if (payload.vendorId && !vendorRepo.existsById(payload.vendorId.value)) {
|
||||||
|
throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Vendor doesn't exist.");
|
||||||
|
}
|
||||||
|
if (payload.categoryId && !categoryRepo.existsById(payload.categoryId.value)) {
|
||||||
|
throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Category doesn't exist.");
|
||||||
|
}
|
||||||
|
if (payload.creditedAccountId && !accountRepo.existsById(payload.creditedAccountId.value)) {
|
||||||
|
throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Credited account doesn't exist.");
|
||||||
|
}
|
||||||
|
if (payload.debitedAccountId && !accountRepo.existsById(payload.debitedAccountId.value)) {
|
||||||
|
throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Debited account doesn't exist.");
|
||||||
|
}
|
||||||
|
validateTags(payload.tags);
|
||||||
|
if (payload.lineItems.length > 0) {
|
||||||
|
long lineItemsTotal = 0;
|
||||||
|
foreach (lineItem; payload.lineItems) {
|
||||||
|
if (lineItem.categoryId && !categoryRepo.existsById(lineItem.categoryId.value)) {
|
||||||
|
throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Line item's category doesn't exist.");
|
||||||
|
}
|
||||||
|
if (lineItem.quantity == 0) {
|
||||||
|
throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Line item's quantity should greater than zero.");
|
||||||
|
}
|
||||||
|
for (ulong i = 0; i < lineItem.quantity; i++) {
|
||||||
|
lineItemsTotal += lineItem.valuePerItem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (lineItemsTotal != payload.amount) {
|
||||||
|
throw new HttpStatusException(
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
"Total of all line items doesn't equal the transaction's total."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue