diff --git a/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountDb.java b/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountDb.java index 9b813ec9..550e2c97 100644 --- a/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountDb.java +++ b/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountDb.java @@ -1,7 +1,9 @@ package de.srsoftware.umbrella.accounting; import de.srsoftware.umbrella.core.model.Account; +import de.srsoftware.umbrella.core.model.Transaction; public interface AccountDb { Account save(Account account); + Transaction save(Transaction transaction); } 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 262061e5..c5c809ca 100644 --- a/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountingModule.java +++ b/accounting/src/main/java/de/srsoftware/umbrella/accounting/AccountingModule.java @@ -12,15 +12,20 @@ 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.Token; +import de.srsoftware.umbrella.core.model.Transaction; import de.srsoftware.umbrella.core.model.UmbrellaUser; import org.json.JSONObject; import java.io.IOException; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneOffset; import java.util.Optional; import static de.srsoftware.tools.Optionals.nullIfEmpty; import static de.srsoftware.umbrella.accounting.Constants.CONFIG_DATABASE; import static de.srsoftware.umbrella.core.ConnectionProvider.connect; +import static de.srsoftware.umbrella.core.ModuleRegistry.userService; import static de.srsoftware.umbrella.core.constants.Path.JSON; import static de.srsoftware.umbrella.core.constants.Path.SEARCH; import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.invalidField; @@ -42,7 +47,7 @@ public class AccountingModule extends BaseHandler implements AccountingService { addCors(ex); try { Optional token = SessionToken.from(ex).map(Token::of); - var user = ModuleRegistry.userService().loadUser(token); + var user = userService().loadUser(token); if (user.isEmpty()) return unauthorized(ex); var head = path.pop(); return switch (head) { @@ -60,13 +65,49 @@ public class AccountingModule extends BaseHandler implements AccountingService { var json = json(ex); if (!json.has(Field.ACCOUNT)) throw missingField(Field.ACCOUNT); if (!(json.get(Field.ACCOUNT) instanceof JSONObject acc)) throw invalidField(Field.ACCOUNT,JSON); - // TODO more tests + + if (!json.has(Field.DATE)) throw missingField(Field.DATE); + var date = LocalDate.parse(json.getString(Field.DATE)); + var dateTime = LocalDateTime.now().withYear(date.getYear()).withMonth(date.getMonthValue()).withDayOfMonth(date.getDayOfMonth()); + if (!json.has(Field.AMOUNT)) throw missingField(Field.AMOUNT); + if (!(json.get(Field.AMOUNT) instanceof Number amount)) throw invalidField(Field.AMOUNT,Text.NUMBER); + + var purpose = json.has(Field.PURPOSE) ? json.getString(Field.PURPOSE) : null; + + if (!json.has(Field.SOURCE)) throw missingField(Field.SOURCE); + if (!(json.get(Field.SOURCE) instanceof JSONObject sourceData)) throw invalidField(Field.SOURCE,JSON); + if (!json.has(Field.DESTINATION)) throw missingField(Field.DESTINATION); + if (!(json.get(Field.DESTINATION) instanceof JSONObject destinationData)) throw invalidField(Field.DESTINATION,JSON); + + String source = null, destination = null; + + if (sourceData.has(Field.USER_ID)) { + if (!(sourceData.get(Field.USER_ID) instanceof Number uid)) throw invalidField(String.join(".",Field.SOURCE,Field.USER_ID),Text.NUMBER); + var u = userService().loadUser(uid.longValue()); + source = ""+u.id(); + } else { + if (!sourceData.has(Field.DISPLAY)) throw missingField(String.join(".",Field.SOURCE,Field.DISPLAY)); + source = sourceData.getString(Field.DISPLAY); + if (source.isBlank()) throw invalidField(String.join(".",Field.SOURCE,Field.DISPLAY),Text.STRING); + } + + if (destinationData.has(Field.USER_ID)) { + if (!(destinationData.get(Field.USER_ID) instanceof Number uid)) throw invalidField(String.join(".",Field.DESTINATION,Field.USER_ID),Text.NUMBER); + var u = userService().loadUser(uid.longValue()); + destination = ""+u; + } else { + if (!destinationData.has(Field.DISPLAY)) throw missingField(String.join(".",Field.DESTINATION,Field.DISPLAY)); + destination = destinationData.getString(Field.DISPLAY); + if (destination.isBlank()) throw invalidField(String.join(".",Field.DESTINATION,Field.DISPLAY),Text.STRING); + } + Long accountId = null; if (acc.has(Field.ID)) { if (!(acc.get(Field.ID) instanceof Number accId)) throw invalidField(String.join(".",Field.ACCOUNT,Field.ID), Text.NUMBER); if (accId.longValue() != 0) accountId = accId.longValue(); } + Account newAccount = null; if (accountId == null){ if (!acc.has(Field.NAME)) throw missingField(String.join(".",Field.ACCOUNT,Field.NAME)); var accountName = acc.getString(Field.NAME); @@ -74,12 +115,12 @@ public class AccountingModule extends BaseHandler implements AccountingService { var currency = acc.has(Field.CURRENCY) ? nullIfEmpty(acc.getString(Field.CURRENCY)) : null; - var account = accountDb.save(new Account(0, accountName, currency, user.id())); - accountId = account.id(); + newAccount = accountDb.save(new Account(0, accountName, currency, user.id())); + accountId = newAccount.id(); } - // TODO: save entry + var transaction = accountDb.save(new Transaction(accountId,dateTime,source,destination,amount.doubleValue(),purpose)); - return notFound(ex); + return sendContent(ex,newAccount != null ? newAccount : transaction); } } 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 ad144e19..46dbd4d3 100644 --- a/accounting/src/main/java/de/srsoftware/umbrella/accounting/SqliteDb.java +++ b/accounting/src/main/java/de/srsoftware/umbrella/accounting/SqliteDb.java @@ -4,9 +4,11 @@ import de.srsoftware.tools.jdbc.Query; import de.srsoftware.umbrella.core.BaseDb; import de.srsoftware.umbrella.core.constants.Field; 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 static de.srsoftware.tools.NotImplemented.notImplemented; import static de.srsoftware.umbrella.accounting.Constants.TABLE_ACCOUNTS; @@ -85,4 +87,17 @@ public class SqliteDb extends BaseDb implements AccountDb { throw notImplemented(this,"save(account)"); } } + + @Override + public Transaction save(Transaction transaction) { + 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(),transaction.destination(),transaction.amount(),transaction.purpose()) + .execute(db).close(); + return transaction; + } 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 485a151b..53600c1d 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 @@ -45,6 +45,7 @@ public class Field { public static final String DELIVERY_DATE = "delivery_date"; public static final String DESCRIPTION = "description"; public static final String DESTINATION = "destination"; + public static final String DISPLAY = "display"; public static final String DOCUMENT = "document"; public static final String DOCUMENT_ID = "document_id"; public static final String DOC_TYPE_ID = "document_type_id"; @@ -129,6 +130,7 @@ public class Field { public static final String PROJECT = "project"; public static final String PROJECT_ID = "project_id"; public static final String PROPERTIES = "properties"; + public static final String PURPOSE = "purpose"; public static final String RECEIVERS = "receivers"; public static final String REDIRECT = "redirect"; diff --git a/core/src/main/java/de/srsoftware/umbrella/core/model/Account.java b/core/src/main/java/de/srsoftware/umbrella/core/model/Account.java index 1abb8365..a3f19152 100644 --- a/core/src/main/java/de/srsoftware/umbrella/core/model/Account.java +++ b/core/src/main/java/de/srsoftware/umbrella/core/model/Account.java @@ -1,7 +1,25 @@ package de.srsoftware.umbrella.core.model; -public record Account(long id, String name, String currency, long ownerId) { +import de.srsoftware.tools.Mappable; +import de.srsoftware.umbrella.core.constants.Field; + +import java.util.Map; + +import static de.srsoftware.umbrella.core.ModuleRegistry.userService; + +public record Account(long id, String name, String currency, long ownerId) implements Mappable { public Account withId(long newId) { return new Account(newId,name,currency,ownerId); } + + @Override + public Map toMap() { + var owner = userService().loadUser(ownerId); + return Map.of( + Field.ID, id, + Field.NAME, name, + Field.CURRENCY, currency, + Field.OWNER, owner.toMap() + ); + } } 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 new file mode 100644 index 00000000..f2cd50af --- /dev/null +++ b/core/src/main/java/de/srsoftware/umbrella/core/model/Transaction.java @@ -0,0 +1,38 @@ +package de.srsoftware.umbrella.core.model; + +import de.srsoftware.tools.Mappable; +import de.srsoftware.umbrella.core.constants.Field; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.Map; + +import static de.srsoftware.umbrella.core.ModuleRegistry.userService; + +public record Transaction(long accountId, LocalDateTime date, String source, String destination, double amount, String purpose) implements Mappable { + @Override + public Map toMap() { + var source = this.source; + try { + var userId = Long.parseLong(source); + var user = userService().loadUser(userId); + source = user.name(); + } catch (NumberFormatException ignored) {} + + var destination = this.destination; + try { + var userId = Long.parseLong(destination); + var user = userService().loadUser(userId); + destination = user.name(); + } catch (NumberFormatException ignored) {} + + return Map.of( + Field.ID, accountId, + Field.DATE, date.toLocalDate(), + Field.SOURCE, source, + Field.DESTINATION, destination, + Field.AMOUNT, amount, + Field.PURPOSE, purpose + ); + } +} diff --git a/frontend/src/routes/accounting/index.svelte b/frontend/src/routes/accounting/index.svelte index 517d60bd..08882cde 100644 --- a/frontend/src/routes/accounting/index.svelte +++ b/frontend/src/routes/accounting/index.svelte @@ -7,6 +7,8 @@ function newAccount(){ router.navigate('/accounting/new'); } + +