implementd adding and removal of tags to/from transactions

Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
This commit is contained in:
2026-04-14 21:44:13 +02:00
parent 9d9e2ed50b
commit f6b854a227
6 changed files with 134 additions and 14 deletions
@@ -8,6 +8,8 @@ import java.util.List;
import java.util.Set;
public interface AccountDb {
void dropTransactionTag(long transactionId, String tag);
Collection<Account> listAccounts(long userId);
Account loadAccount(long accountId);
@@ -41,6 +41,30 @@ public class AccountingModule extends BaseHandler implements AccountingService {
ModuleRegistry.add(this);
}
@Override
public boolean doDelete(Path path, HttpExchange ex) throws IOException {
addCors(ex);
try {
Optional<Token> token = SessionToken.from(ex).map(Token::of);
var user = userService().loadUser(token);
if (user.isEmpty()) return unauthorized(ex);
var head = path.pop();
return switch (head) {
case TRANSACTION -> {
try {
var transaction = accountDb.loadTransaction(Long.parseLong(path.pop()));
yield dropTransaction(transaction, user.get(), path, ex);
} catch (NumberFormatException ignored) {
yield super.doDelete(path,ex);
}
}
case null, default -> super.doDelete(path, ex);
};
} catch (UmbrellaException e){
return send(ex,e);
}
}
@Override
public boolean doGet(Path path, HttpExchange ex) throws IOException {
addCors(ex);
@@ -114,6 +138,25 @@ public class AccountingModule extends BaseHandler implements AccountingService {
}
}
public boolean dropTransaction(Transaction transaction, UmbrellaUser user, Path path, HttpExchange ex) throws IOException {
var head = path.pop();
return switch (head){
case TAG -> dropTransactionTag(user,transaction,ex);
case null, default -> super.doDelete(path,ex);
};
}
private boolean dropTransactionTag(UmbrellaUser user, Transaction transaction, HttpExchange ex) throws IOException {
LOG.log(WARNING,"Missing permission check in AccountModule.dropTransactionTag!");
var json = json(ex);
if (!json.has(Field.TAG)) throw missingField(Field.TAG);
var tag = json.getString(Field.TAG);
if (tag.isBlank()) throw invalidField(Field.TAG,"non-empty");
accountDb.dropTransactionTag(transaction.id(),tag);
transaction.tags().remove(tag);
return sendContent(ex, transaction);
}
private boolean getAccount(UmbrellaUser user, long accountId, HttpExchange ex) throws IOException {
LOG.log(WARNING,"Missing authorization check in AccountingModule.getAccount(…)!");
var account = accountDb.loadAccount(accountId);
@@ -164,6 +207,7 @@ public class AccountingModule extends BaseHandler implements AccountingService {
if (json.has(Field.DESTINATION)) transaction.destination(IdOrString.of(json.getString(Field.DESTINATION)));
if (json.has(Field.PURPOSE)) transaction.purpose(json.getString(Field.PURPOSE));
if (json.has(Field.SOURCE)) transaction.source(IdOrString.of(json.getString(Field.SOURCE)));
if (json.has(Field.TAG)) transaction.tags().add(json.getString(Field.TAG));
return sendContent(ex,accountDb.save(transaction));
}
@@ -6,7 +6,7 @@ import static de.srsoftware.tools.jdbc.Condition.*;
import static de.srsoftware.tools.jdbc.Query.*;
import static de.srsoftware.tools.jdbc.Query.SelectQuery.ALL;
import static de.srsoftware.umbrella.accounting.Constants.*;
import static de.srsoftware.umbrella.core.constants.Field.ID;
import static de.srsoftware.umbrella.core.constants.Field.*;
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.*;
import static de.srsoftware.umbrella.core.model.Translatable.t;
import static java.text.MessageFormat.format;
@@ -117,6 +117,20 @@ public class SqliteDb extends BaseDb implements AccountDb {
}
}
@Override
public void dropTransactionTag(long transactionId, String tag) {
try {
var tagIds = new HashSet<Long>();
var rs = select(ID).from(TABLE_TAGS).where(TAG, equal(tag)).exec(db);
while (rs.next()) tagIds.add(rs.getLong(1));
rs.close();
delete().from(TABLE_TAGS_TRANSACTIONS).where(TRANSACTION_ID, equal(transactionId)).where(TAG_ID, in(tagIds.toArray())).execute(db);
} catch (SQLException e) {
throw failedToDropObject(tag);
}
}
@Override
public HashSet<Account> listAccounts(long userId) {
try {
@@ -169,7 +183,7 @@ public class SqliteDb extends BaseDb implements AccountDb {
public List<Transaction> loadTransactions(Account account) {
try {
var transactions = new HashMap<Long,Transaction>();
var rs = select(ALL).from(TABLE_TRANSACTIONS).where(Field.ACCOUNT,equal(account.id())).sort(Field.TIMESTAMP).exec(db);
var rs = select(ALL).from(TABLE_TRANSACTIONS).where(Field.ACCOUNT,equal(account.id())).exec(db);
while (rs.next()) {
var transaction = Transaction.of(rs);
transactions.put(transaction.id(),transaction);
@@ -182,7 +196,7 @@ public class SqliteDb extends BaseDb implements AccountDb {
if (transaction != null) transaction.tags().add(rs.getString(Field.TAG));
}
rs.close();
return new ArrayList<>(transactions.values());
return transactions.values().stream().sorted(Comparator.comparing(Transaction::date)).toList();
} catch (SQLException e) {
throw failedToLoadMembers(account);
}
@@ -231,7 +245,7 @@ public class SqliteDb extends BaseDb implements AccountDb {
}
private Transaction saveTags(Transaction transaction) {
var remaining = new HashSet<String>(transaction.tags());
var remaining = new HashSet<>(transaction.tags());
var existingTags = new HashMap<String,Long>();
try {
var rs = select(ALL).from(TABLE_TAGS).where(Field.TAG,in(transaction.tags().toArray())).exec(db);