From 508c05da225f3f09a341537c37d7b2ad83849300 Mon Sep 17 00:00:00 2001 From: Martin Berger <> Date: Sun, 12 Feb 2023 12:09:21 +0100 Subject: [PATCH] Added functionality to fetch account statement as MT940 from LNBits wallet transactions --- README.md | 2 +- config/lnbits.properties.example | 4 + pom.xml | 5 + src/main/java/lnbits/LnTransaction.java | 64 ++++++++ src/main/java/lnbits/LnbitsHelper.java | 150 ++++++++++++++++++ src/main/java/lnbits/WalletDetails.java | 16 ++ .../fuelifints/dataaccess/Banking2Access.java | 7 +- .../java/net/petafuel/mt94x/Mt940Helper.java | 37 +++++ third_party/README.md | 35 ++-- 9 files changed, 303 insertions(+), 17 deletions(-) create mode 100644 config/lnbits.properties.example create mode 100644 src/main/java/lnbits/LnTransaction.java create mode 100644 src/main/java/lnbits/LnbitsHelper.java create mode 100644 src/main/java/lnbits/WalletDetails.java create mode 100644 src/main/java/net/petafuel/mt94x/Mt940Helper.java diff --git a/README.md b/README.md index d365a57..dd86a2d 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Fueli FinTS is a server implementing the financial transaction services (FinTS) 4. Added example configuration files and db script to set up a rudimentary database. 5. Some minor refactoring and cleanups -**Please find a complete list of all changes to the original implementation by this fork with this [Github Compare](https://github.com/petafuel/FueliFinTS/compare/main...drmartinberger:main) view.** +**Please find a complete list of all changes to the original implementation by this fork with this [Github Compare](https://github.com/petafuel/FueliFinTS/compare/main...drmartinberger:lnbits-via-mt940) view.** # Setup Steps diff --git a/config/lnbits.properties.example b/config/lnbits.properties.example new file mode 100644 index 0000000..771dd90 --- /dev/null +++ b/config/lnbits.properties.example @@ -0,0 +1,4 @@ +lnbitsUrl = +uriWallet = /api/v1/wallet +uriPayments = /api/v1/payments +xApiKey = \ No newline at end of file diff --git a/pom.xml b/pom.xml index 4a70060..f590131 100644 --- a/pom.xml +++ b/pom.xml @@ -119,6 +119,11 @@ log4j-slf4j-impl 2.9.1 + + com.prowidesoftware + pw-swift-core + SRU2022-9.3.12 + clean install diff --git a/src/main/java/lnbits/LnTransaction.java b/src/main/java/lnbits/LnTransaction.java new file mode 100644 index 0000000..f68dd33 --- /dev/null +++ b/src/main/java/lnbits/LnTransaction.java @@ -0,0 +1,64 @@ +package lnbits; + +import com.prowidesoftware.swift.model.field.Field61; +import com.prowidesoftware.swift.model.field.Field86; +import org.apache.commons.lang3.StringUtils; +import org.codehaus.jackson.annotate.JsonIgnoreProperties; +import org.codehaus.jackson.annotate.JsonProperty; + +import java.text.SimpleDateFormat; +import java.util.Date; + + +@JsonIgnoreProperties(ignoreUnknown = true) +public class LnTransaction { + public LnTransaction() { super(); } + + public Field61 toField61() { + // id: max 16 chars + String id = StringUtils.left(checkingId, 7) + "..." + StringUtils.right(checkingId, 6); + return new Field61() + .setValueDate(new SimpleDateFormat("yyMMdd").format(getTime())) + .setDebitCreditMark(this.amount >= 0 ? "C" : "D") + .setAmount(Math.abs(this.amount+this.fee)/1000L) + .setTransactionType("NMSC") + .setIdentificationCode(id); + } + + public Field86 toField86(){ + return new Field86().setComponent1(this.memo); + } + + public Long getAmount() { + return amount; + } + + public Long getFee() { + return fee; + } + + public Date getTime() { + return new Date(this.time*1000); + } + + public Boolean getPending() { return pending; } + + @JsonProperty("amount") + private Long amount; + @JsonProperty("fee") + private Long fee; + + @JsonProperty("memo") + private String memo; + + // time in seconds since epoch + @JsonProperty("time") + private Long time; + + @JsonProperty("checking_id") + private String checkingId; + + @JsonProperty("pending") + private Boolean pending; + +} diff --git a/src/main/java/lnbits/LnbitsHelper.java b/src/main/java/lnbits/LnbitsHelper.java new file mode 100644 index 0000000..978ca5b --- /dev/null +++ b/src/main/java/lnbits/LnbitsHelper.java @@ -0,0 +1,150 @@ +package lnbits; + +import com.prowidesoftware.swift.model.field.*; +import org.apache.http.HttpEntity; +import org.apache.http.HttpHeaders; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.util.EntityUtils; +import org.codehaus.jackson.map.ObjectMapper; +import org.codehaus.jackson.type.TypeReference; + +import java.io.BufferedInputStream; +import java.io.FileInputStream; +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Properties; +import java.util.stream.Collectors; + +public class LnbitsHelper { + private static String lnbitsUrl; + + private static String uriWallet; + private static String uriPayments; + + private static String xApiKey; + + private static void loadConfiguration() { + Properties properties = new Properties(); + try { + BufferedInputStream stream = new BufferedInputStream(new FileInputStream("config/lnbits.properties")); + properties.load(stream); + stream.close(); + + lnbitsUrl = properties.getProperty("lnbitsUrl"); + uriWallet = properties.getProperty("uriWallet"); + uriPayments = properties.getProperty("uriPayments"); + xApiKey = properties.getProperty("xApiKey"); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + public static Collection getMt940Fields(java.util.Date from, java.util.Date to) throws IOException { + loadConfiguration(); + + Collection mt940Fields = new ArrayList<>(); + Long currentBalance = getCurrentBalance(); + Collection afterFromSettledPayments = getPayments() + .stream() + .filter(tx -> tx.getTime().after(from) && !tx.getPending()) + .collect(Collectors.toList()); + Long fromBalance = currentBalance - afterFromSettledPayments + .stream() + .map(tx -> tx.getAmount() + tx.getFee()) + .reduce(0L, Long::sum); + Collection fromToPayments = afterFromSettledPayments + .stream() + .filter(tx -> tx.getTime().before(to)) + .collect(Collectors.toList()); + Long toBalance = fromBalance + fromToPayments + .stream() + .map(tx -> tx.getAmount() + tx.getFee()) + .reduce(0L, Long::sum); + // initialBalance + String simpleFrom = new SimpleDateFormat("yyMMdd").format(from); + mt940Fields.add(new Field60F() + .setDCMark("C") + .setDate(simpleFrom) + .setCurrency("EUR") + .setAmount(fromBalance/1000L) + ); + + for (LnTransaction tx : fromToPayments) { + mt940Fields.add(tx.toField61()); + mt940Fields.add(tx.toField86()); + } + + // finalBalance + String simpleTo = new SimpleDateFormat("yyMMdd").format(from); + mt940Fields.add(new Field62F() + .setDCMark("C") + .setDate(simpleTo) + .setCurrency("EUR") + .setAmount(toBalance/1000L) + ); + + return mt940Fields; + } + + private static Collection getPayments() throws IOException { + CloseableHttpClient httpClient = HttpClients.createDefault(); + try { + HttpGet request = new HttpGet(lnbitsUrl + uriPayments); + request.addHeader("X-API-KEY", xApiKey); + request.addHeader(HttpHeaders.ACCEPT, "application/json"); + + CloseableHttpResponse response = httpClient.execute(request); + try { + HttpEntity entity = response.getEntity(); + if (entity != null && response.getStatusLine().getStatusCode() == 200) { + String result = EntityUtils.toString(entity); + return new ObjectMapper().readValue(result, new TypeReference>(){}); + } else { + throw new RuntimeException("Request failed"); + } + } finally { + response.close(); + } + } catch (ClientProtocolException e) { + throw new RuntimeException(e); + } catch (IOException e) { + throw new RuntimeException(e); + } finally { + httpClient.close(); + } + } + private static Long getCurrentBalance() throws IOException { + CloseableHttpClient httpClient = HttpClients.createDefault(); + try { + HttpGet request = new HttpGet(lnbitsUrl + uriWallet); + request.addHeader("X-API-KEY", xApiKey); + request.addHeader(HttpHeaders.ACCEPT, "application/json"); + + CloseableHttpResponse response = httpClient.execute(request); + try { + HttpEntity entity = response.getEntity(); + if (entity != null && response.getStatusLine().getStatusCode() == 200) { + String result = EntityUtils.toString(entity); + WalletDetails walletDetails = new ObjectMapper().readValue(result, WalletDetails.class); + return walletDetails.getBalance(); + } else { + throw new RuntimeException("Request failed"); + } + } finally { + response.close(); + } + } catch (ClientProtocolException e) { + throw new RuntimeException(e); + } catch (IOException e) { + throw new RuntimeException(e); + } finally { + httpClient.close(); + } + } +} diff --git a/src/main/java/lnbits/WalletDetails.java b/src/main/java/lnbits/WalletDetails.java new file mode 100644 index 0000000..3f601a0 --- /dev/null +++ b/src/main/java/lnbits/WalletDetails.java @@ -0,0 +1,16 @@ +package lnbits; + +import org.codehaus.jackson.annotate.JsonIgnoreProperties; +import org.codehaus.jackson.annotate.JsonProperty; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class WalletDetails { + public WalletDetails() { super(); } + + public Long getBalance() { + return balance; + } + + @JsonProperty("balance") + private Long balance; +} diff --git a/src/main/java/net/petafuel/fuelifints/dataaccess/Banking2Access.java b/src/main/java/net/petafuel/fuelifints/dataaccess/Banking2Access.java index 8bb54db..4368996 100644 --- a/src/main/java/net/petafuel/fuelifints/dataaccess/Banking2Access.java +++ b/src/main/java/net/petafuel/fuelifints/dataaccess/Banking2Access.java @@ -20,6 +20,7 @@ import net.petafuel.jsepa.exception.SEPAParsingException; import net.petafuel.jsepa.model.*; import net.petafuel.jsepa.util.BankDateCalculator; import net.petafuel.jsepa.util.SepaUtils; +import net.petafuel.mt94x.Mt940Helper; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -1311,7 +1312,11 @@ public class Banking2Access implements DataAccessFacade { */ @Override public byte[] getGebuchteUmsaetze(String kontonummer, Date vonDatum, Date bisDatum, LegitimationInfo legitimationsInfo) { - return database.getGebuchteUmsaetze(kontonummer, vonDatum, bisDatum, legitimationsInfo); + try { + return Mt940Helper.getMT940s(kontonummer, vonDatum, bisDatum); + } catch (IOException e) { + throw new RuntimeException(e); + } } /** diff --git a/src/main/java/net/petafuel/mt94x/Mt940Helper.java b/src/main/java/net/petafuel/mt94x/Mt940Helper.java new file mode 100644 index 0000000..0b54984 --- /dev/null +++ b/src/main/java/net/petafuel/mt94x/Mt940Helper.java @@ -0,0 +1,37 @@ +package net.petafuel.mt94x; + +import com.prowidesoftware.swift.model.BIC; +import com.prowidesoftware.swift.model.field.*; +import com.prowidesoftware.swift.model.mt.mt9xx.MT940; +import lnbits.LnbitsHelper; + +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.Date; + +public class Mt940Helper { + public static byte[] getMT940s(String kontonummer, java.util.Date vonDatum, java.util.Date bisDatum) throws IOException { + if(vonDatum == null) { + vonDatum = new Date(0); + } + if(bisDatum == null) { + bisDatum = new Date(); + } + return getMT940(kontonummer, vonDatum, bisDatum).getBytes(); + } + + private static String getMT940(String kontonummer, java.util.Date vonDatum, java.util.Date bisDatum) throws IOException { + String simpleDate = new SimpleDateFormat("yyMMdd").format(bisDatum); + MT940 a = new MT940(); + a.setSender(new BIC("NOOODE00BIC")); + a.setReceiver(new BIC("NOOODE00BIC")); + a.addField(new Field20(simpleDate)); // Transaktionsnummer + a.addField(new Field21("NONREF")); // Bezugsreferenznummer + a.addField(new Field25(kontonummer)); // Kontobezeichnung + a.addField(new Field28C((simpleDate))); // Auszugsnummer + for(Field f : LnbitsHelper.getMt940Fields(vonDatum, bisDatum)) { + a.addField(f); + } + return a.message(); + } +} diff --git a/third_party/README.md b/third_party/README.md index fdad58c..b976295 100644 --- a/third_party/README.md +++ b/third_party/README.md @@ -1,6 +1,6 @@ # Third Party Licenses -Fueli FinTS is build on top of other open source projects. Their licence files are included in the source code's 'third_party' folder. The third party software included and/or used by Fueli FinTS is: (in alphabetical order) +This fork of Fueli FinTS is build on top of other open source projects. Their licence files are included in the source code's 'third_party' folder. The third party software included and/or used by Fueli FinTS is: (in alphabetical order) - Apache Commons Lang Copyright 2001-2021 The Apache Software Foundation @@ -58,27 +58,32 @@ Fueli FinTS is build on top of other open source projects. Their licence files a - MySQL Connector/J Copyright (c) 1998, 2021 Oracle and/or its affiliates. GNU General Public License Version 2.0 - https://dev.mysql.com/doc/connector-j/5.1/en/ + https://dev.mysql.com/doc/connector-j/8.0/en/ - OWASP Dependency-Check Copyright (c) 2012-2021 Jeremy Long. All Rights Reserved. Apache-2.0 License https://owasp.org/www-project-dependency-check/ -- petaFuel DBUtils - Copyright 2021 petaFuel GmbH - DBUtils Software License Agreement - https://github.com/petafuel +# The following commented libraries are no longer publicly available: +[//]: # (- petaFuel DBUtils ) +[//]: # ( Copyright 2021 petaFuel GmbH ) +[//]: # ( DBUtils Software License Agreement ) +[//]: # ( https://github.com/petafuel) +[//]: # () +[//]: # (- petaFuel JSEPA ) +[//]: # ( Copyright 2021 petaFuel GmbH ) +[//]: # ( JSEPA Software License Agreement ) +[//]: # ( https://github.com/petafuel) +[//]: # () +[//]: # (- petaFuel Mt94x ) +[//]: # ( Copyright 2021 petaFuel GmbH ) +[//]: # ( Mt94x Software License Agreement ) +[//]: # ( https://github.com/petafuel) -- petaFuel JSEPA - Copyright 2021 petaFuel GmbH - JSEPA Software License Agreement - https://github.com/petafuel - -- petaFuel Mt94x - Copyright 2021 petaFuel GmbH - Mt94x Software License Agreement - https://github.com/petafuel +- Prowide Core + Apache-2.0 License + https://www.prowidesoftware.com/products/core - RESTEasy Apache-2.0 License