module transaction.service; import handy_http_primitives; import std.datetime; import transaction.api; import transaction.model; import transaction.data; import transaction.dto; import profile.data; import account.model; import account.data; import util.money; import util.pagination; // Transactions Services Page!TransactionsListItem getTransactions(ProfileDataSource ds, in PageRequest pageRequest) { Page!TransactionsListItem page = ds.getTransactionRepository() .findAll(pageRequest); return page; } TransactionDetail getTransaction(ProfileDataSource ds, ulong transactionId) { return ds.getTransactionRepository().findById(transactionId) .orElseThrow(() => new HttpStatusException(HttpStatus.NOT_FOUND)); } TransactionDetail addTransaction(ProfileDataSource ds, in AddTransactionPayload payload) { TransactionVendorRepository vendorRepo = ds.getTransactionVendorRepository(); TransactionCategoryRepository categoryRepo = ds.getTransactionCategoryRepository(); AccountRepository accountRepo = ds.getAccountRepository(); // Validate transaction details: if (payload.creditedAccountId.isNull && payload.debitedAccountId.isNull) { throw new HttpStatusException(HttpStatus.BAD_REQUEST, "At least one account must be linked."); } if ( !payload.creditedAccountId.isNull && !payload.debitedAccountId.isNull && payload.creditedAccountId.get == payload.debitedAccountId.get ) { 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 = SysTime.fromISOExtString(payload.timestamp, UTC()); if (timestamp > now) { throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Cannot create transaction in the future."); } if (!payload.vendorId.isNull && !vendorRepo.existsById(payload.vendorId.get)) { throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Vendor doesn't exist."); } if (!payload.categoryId.isNull && !categoryRepo.existsById(payload.categoryId.get)) { throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Category doesn't exist."); } if (!payload.creditedAccountId.isNull && !accountRepo.existsById(payload.creditedAccountId.get)) { throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Credited account doesn't exist."); } if (!payload.debitedAccountId.isNull && !accountRepo.existsById(payload.debitedAccountId.get)) { throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Debited account doesn't exist."); } foreach (tag; payload.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 ~ "\"."); } } if (payload.lineItems.length > 0) { long lineItemsTotal = 0; foreach (lineItem; payload.lineItems) { if (!lineItem.categoryId.isNull && !categoryRepo.existsById(lineItem.categoryId.get)) { 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." ); } } // Add the transaction: ulong txnId; ds.doTransaction(() { TransactionRepository txRepo = ds.getTransactionRepository(); TransactionDetail txn = txRepo.insert(payload); AccountJournalEntryRepository jeRepo = ds.getAccountJournalEntryRepository(); if (!payload.creditedAccountId.isNull) { jeRepo.insert( timestamp, payload.creditedAccountId.get, txn.id, txn.amount, AccountJournalEntryType.CREDIT, txn.currency ); } if (!payload.debitedAccountId.isNull) { jeRepo.insert( timestamp, payload.debitedAccountId.get, txn.id, txn.amount, AccountJournalEntryType.DEBIT, txn.currency ); } TransactionTagRepository tagRepo = ds.getTransactionTagRepository(); tagRepo.updateTags(txn.id, payload.tags); txnId = txn.id; }); return ds.getTransactionRepository().findById(txnId).orElseThrow(); } // Vendors Services TransactionVendor[] getAllVendors(ProfileDataSource ds) { return ds.getTransactionVendorRepository().findAll(); } TransactionVendor getVendor(ProfileDataSource ds, ulong vendorId) { return ds.getTransactionVendorRepository().findById(vendorId) .orElseThrow(() => new HttpStatusException(HttpStatus.NOT_FOUND)); } TransactionVendor createVendor(ProfileDataSource ds, in VendorPayload payload) { auto vendorRepo = ds.getTransactionVendorRepository(); if (vendorRepo.existsByName(payload.name)) { throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Vendor name is already in use."); } return vendorRepo.insert(payload.name, payload.description); } TransactionVendor updateVendor(ProfileDataSource ds, ulong vendorId, in VendorPayload payload) { TransactionVendorRepository repo = ds.getTransactionVendorRepository(); TransactionVendor existingVendor = repo.findById(vendorId) .orElseThrow(() => new HttpStatusException(HttpStatus.NOT_FOUND)); if (payload.name != existingVendor.name && repo.existsByName(payload.name)) { throw new HttpStatusException( HttpStatus.BAD_REQUEST, "Vendor name is already in use." ); } return repo.updateById(vendorId, payload.name, payload.description); } void deleteVendor(ProfileDataSource ds, ulong vendorId) { ds.getTransactionVendorRepository().deleteById(vendorId); }