diff --git a/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountingModule.java b/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountingModule.java index 8345d494..1a8dff2c 100644 --- a/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountingModule.java +++ b/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountingModule.java @@ -25,10 +25,7 @@ import de.srsoftware.umbrella.core.model.*; import java.io.IOException; import java.time.LocalDate; import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; +import java.util.*; import org.json.JSONArray; import org.json.JSONObject; @@ -180,10 +177,13 @@ public class AccountingModule extends BaseHandler implements AccountingService { accountId = newAccount.id(); } - var transaction = accountDb.save(new Transaction(accountId,dateTime,source,destination,amount.doubleValue(),purpose)); + var tagList = json.has(Field.TAGS) && json.get(Field.TAGS) instanceof JSONArray t ? t : List.of(); + var tags = new HashSet(); + tagList.forEach(e -> tags.add(e.toString())); + + + var transaction = accountDb.save(new Transaction(0,accountId,dateTime,source,destination,amount.doubleValue(),purpose,tags)); - var tags = json.has(Field.TAGS) && json.get(Field.TAGS) instanceof JSONArray t ? t : null; - if (tags != null) LOG.log(WARNING, "Tagging transactions not implemented!"); return sendContent(ex,newAccount != null ? newAccount : transaction); } diff --git a/accounting/src/main/java/de/srsoftware/umbrella/accounting/Constants.java b/accounting/src/main/java/de/srsoftware/umbrella/accounting/Constants.java index 1bc7de26..eb281211 100644 --- a/accounting/src/main/java/de/srsoftware/umbrella/accounting/Constants.java +++ b/accounting/src/main/java/de/srsoftware/umbrella/accounting/Constants.java @@ -5,5 +5,7 @@ public class Constants { public static final String CONFIG_DATABASE = "umbrella.modules.accounting.database"; public static final String TABLE_ACCOUNTS = "accounts"; + public static final String TABLE_TAGS = "tag"; + public static final String TABLE_TAGS_TRANSACTIONS = "tags_transactions"; public static final String TABLE_TRANSACTIONS = "transactions"; } diff --git a/accounting/src/main/java/de/srsoftware/umbrella/accounting/SqliteDb.java b/accounting/src/main/java/de/srsoftware/umbrella/accounting/SqliteDb.java index 227c083a..1eab87b5 100644 --- a/accounting/src/main/java/de/srsoftware/umbrella/accounting/SqliteDb.java +++ b/accounting/src/main/java/de/srsoftware/umbrella/accounting/SqliteDb.java @@ -3,10 +3,9 @@ package de.srsoftware.umbrella.accounting; import static de.srsoftware.tools.NotImplemented.notImplemented; 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.tools.jdbc.Query.select; -import static de.srsoftware.umbrella.accounting.Constants.TABLE_ACCOUNTS; -import static de.srsoftware.umbrella.accounting.Constants.TABLE_TRANSACTIONS; +import static de.srsoftware.umbrella.accounting.Constants.*; import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.*; import static java.text.MessageFormat.format; @@ -15,12 +14,14 @@ import de.srsoftware.tools.jdbc.Query; import de.srsoftware.umbrella.core.BaseDb; import de.srsoftware.umbrella.core.constants.Field; import de.srsoftware.umbrella.core.constants.Text; +import de.srsoftware.umbrella.core.exceptions.UmbrellaException; import de.srsoftware.umbrella.core.model.Account; import de.srsoftware.umbrella.core.model.Transaction; import java.sql.Connection; import java.sql.SQLException; import java.time.ZoneOffset; import java.util.ArrayList; +import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -36,6 +37,8 @@ public class SqliteDb extends BaseDb implements AccountDb { case 0: createAccountsTable(); createTransactionsTable(); + createTagsTable(); + createTagsTransactionsTable(); } return setCurrentVersion(1); } @@ -59,9 +62,45 @@ public class SqliteDb extends BaseDb implements AccountDb { } } + private void createTagsTable(){ + var sql = """ + CREATE TABLE IF NOT EXISTS {0} ( + {1} INTEGER PRIMARY KEY, + {2} VARCHAR(32) UNIQUE NOT NULL + ); + """; + try { + sql = format(sql,TABLE_TAGS,Field.ID,Field.TAG); + var stmt = db.prepareStatement(sql); + stmt.execute(); + stmt.close(); + } catch (SQLException e) { + throw failedToCreateTable(TABLE_TAGS).causedBy(e); + } + } + + private void createTagsTransactionsTable(){ + var sql = """ + CREATE TABLE IF NOT EXISTS {0} ( + {1} LONG NOT NULL, + {2} LONG NOT NULL, + PRIMARY KEY({1}, {2}) + ); + """; + try { + sql = format(sql,TABLE_TAGS_TRANSACTIONS,Field.TRANSACTION_ID,Field.TAG_ID); + var stmt = db.prepareStatement(sql); + stmt.execute(); + stmt.close(); + } catch (SQLException e) { + throw failedToCreateTable(TABLE_TAGS).causedBy(e); + } + } + private void createTransactionsTable() { var sql = """ CREATE TABLE IF NOT EXISTS {0} ( + {7} INTEGER PRIMARY KEY, {1} LONG NOT NULL, {2} LONG NOT NULL, {3} VARCHAR(255) NOT NULL, @@ -70,7 +109,7 @@ public class SqliteDb extends BaseDb implements AccountDb { {6} TEXT );"""; try { - sql = format(sql,TABLE_TRANSACTIONS,Field.ACCOUNT,Field.TIMESTAMP,Field.SOURCE,Field.DESTINATION, Field.AMOUNT,Field.DESCRIPTION); + sql = format(sql,TABLE_TRANSACTIONS,Field.ACCOUNT,Field.TIMESTAMP,Field.SOURCE,Field.DESTINATION, Field.AMOUNT,Field.DESCRIPTION, Field.ID); var stmt = db.prepareStatement(sql); stmt.execute(); stmt.close(); @@ -144,13 +183,49 @@ public class SqliteDb extends BaseDb implements AccountDb { @Override public Transaction save(Transaction transaction) { + if (transaction.id() == 0) { + try { + var timestamp = transaction.date().toEpochSecond(ZoneOffset.UTC); + var rs = Query.insertInto(TABLE_TRANSACTIONS, Field.ACCOUNT, Field.TIMESTAMP, Field.SOURCE, Field.DESTINATION, Field.AMOUNT, Field.DESCRIPTION) + .values(transaction.accountId(), timestamp, transaction.source().value(), transaction.destination().value(), transaction.amount(), transaction.purpose()) + .execute(db).getGeneratedKeys(); + if (rs.next()) transaction = transaction.withId(rs.getLong(1)); + rs.close(); + } catch (SQLException e) { + throw failedToStoreObject(transaction); + } + } else { // TODO : implement update + throw UmbrellaException.failedToStoreObject(transaction); + } + saveTags(transaction); + return transaction; + } + + private void saveTags(Transaction transaction) { + var remaining = new HashSet(transaction.tags()); + var existingTags = new HashMap(); try { - var timestamp = transaction.date().toEpochSecond(ZoneOffset.UTC); - Query.replaceInto(TABLE_TRANSACTIONS,Field.ACCOUNT,Field.TIMESTAMP,Field.SOURCE,Field.DESTINATION, Field.AMOUNT,Field.DESCRIPTION) - .values(transaction.accountId(),timestamp,transaction.source().value(),transaction.destination().value(),transaction.amount(),transaction.purpose()) - .execute(db).close(); - return transaction; - } catch (SQLException e) { + var rs = select(ALL).from(TABLE_TAGS).where(Field.TAG,in(transaction.tags().toArray())).exec(db); + while (rs.next()) existingTags.put(rs.getString(Field.TAG), rs.getLong(Field.ID)); + rs.close(); + } catch (SQLException e){ + throw failedToLoadMembers(transaction); + } + remaining.removeAll(existingTags.keySet()); + for (var tag : remaining) { + try { + var rs = insertInto(TABLE_TAGS, Field.TAG).values(tag).execute(db).getGeneratedKeys(); + if (rs.next()) existingTags.put(tag,rs.getLong(1)); + rs.close(); + } catch (SQLException e){ + throw failedToStoreObject(tag); + } + } + try { + var query = replaceInto(TABLE_TAGS_TRANSACTIONS, Field.TRANSACTION_ID, Field.TAG_ID); + for (var tag_id : existingTags.values()) query.values(transaction.id(), tag_id); + query.execute(db).close(); + } catch (SQLException e){ throw failedToStoreObject(transaction); } } diff --git a/core/src/main/java/de/srsoftware/umbrella/core/constants/Field.java b/core/src/main/java/de/srsoftware/umbrella/core/constants/Field.java index 77550b1c..a8ca1d2d 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/constants/Field.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/constants/Field.java @@ -157,6 +157,7 @@ public class Field { public static final String TAG = "tag"; public static final String TAGS = "tags"; public static final String TAG_COLORS = "tag_colors"; + public static final String TAG_ID = "tag_id"; public static final String TASK = "task"; public static final String TASK_IDS = "task_ids"; public static final String TASKS = "tasks"; @@ -175,6 +176,7 @@ public class Field { public static final String TO = "to"; public static final String TOKEN = "token"; public static final String TOTAL_PRIO = "total_prio"; + public static final String TRANSACTION_ID = "transaction_id"; public static final String TRANSACTIONS = "transactions"; public static final String TYPE = "type"; public static final String TYPE_ID = "type_id"; diff --git a/core/src/main/java/de/srsoftware/umbrella/core/model/Transaction.java b/core/src/main/java/de/srsoftware/umbrella/core/model/Transaction.java index 49524300..d5979b61 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/model/Transaction.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/model/Transaction.java @@ -7,9 +7,11 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.time.LocalDateTime; import java.time.ZoneOffset; +import java.util.HashSet; import java.util.Map; +import java.util.Set; -public record Transaction(long accountId, LocalDateTime date, IdOrString source, IdOrString destination, double amount, String purpose) implements Mappable { +public record Transaction(long id, long accountId, LocalDateTime date, IdOrString source, IdOrString destination, double amount, String purpose, Set tags) implements Mappable { public static Transaction of(ResultSet rs) throws SQLException { @@ -20,12 +22,14 @@ public record Transaction(long accountId, LocalDateTime date, IdOrString source, var destination = IdOrString.of(rs.getString(Field.DESTINATION)); var amount = rs.getDouble(Field.AMOUNT); var purpose = rs.getString(Field.DESCRIPTION); - return new Transaction(accountId,date,source,destination,amount,purpose); + var id = rs.getLong(Field.ID); + return new Transaction(id, accountId, date, source, destination, amount, purpose, new HashSet<>()); } @Override public Map toMap() { return Map.of( + Field.ID, id, Field.ACCOUNT, accountId, Field.DATE, date.toLocalDate(), Field.SOURCE, source.toMap(), @@ -34,4 +38,8 @@ public record Transaction(long accountId, LocalDateTime date, IdOrString source, Field.PURPOSE, purpose ); } + + public Transaction withId(long id) { + return new Transaction(id, accountId, date, source, destination, amount, purpose, new HashSet<>(tags)); + } }