Add BudgetAmounts

Add CategorizerController
Add ReportController
This commit is contained in:
Steve Schafer 2026-01-17 09:57:29 -07:00
parent 373708f991
commit 09c3d932c2
58 changed files with 1640 additions and 91 deletions

View file

@ -24,14 +24,6 @@
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" path="target/generated-sources/annotations">
<attributes>
<attribute name="ignore_optional_problems" value="true"/>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="m2e-apt" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/test-classes" path="src/test/resources">
<attributes>
<attribute name="test" value="true"/>
@ -39,12 +31,21 @@
<attribute name="optional" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="target/generated-test-sources/test-annotations">
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-17">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" path="target/generated-sources/annotations">
<attributes>
<attribute name="test" value="true"/>
<attribute name="optional" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-17"/>
<classpathentry kind="src" output="target/test-classes" path="target/generated-test-sources/test-annotations">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

View file

@ -1,74 +1,80 @@
<factorypath>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot-starter-web/2.1.12.RELEASE/spring-boot-starter-web-2.1.12.RELEASE.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot-starter/2.1.12.RELEASE/spring-boot-starter-2.1.12.RELEASE.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot/2.1.12.RELEASE/spring-boot-2.1.12.RELEASE.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot-autoconfigure/2.1.12.RELEASE/spring-boot-autoconfigure-2.1.12.RELEASE.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot-starter-logging/2.1.12.RELEASE/spring-boot-starter-logging-2.1.12.RELEASE.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/ch/qos/logback/logback-classic/1.2.3/logback-classic-1.2.3.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/ch/qos/logback/logback-core/1.2.3/logback-core-1.2.3.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/apache/logging/log4j/log4j-to-slf4j/2.11.2/log4j-to-slf4j-2.11.2.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/apache/logging/log4j/log4j-api/2.11.2/log4j-api-2.11.2.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/slf4j/jul-to-slf4j/1.7.30/jul-to-slf4j-1.7.30.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/javax/annotation/javax.annotation-api/1.3.2/javax.annotation-api-1.3.2.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-core/5.1.13.RELEASE/spring-core-5.1.13.RELEASE.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-jcl/5.1.13.RELEASE/spring-jcl-5.1.13.RELEASE.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/yaml/snakeyaml/1.23/snakeyaml-1.23.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot-starter-json/2.1.12.RELEASE/spring-boot-starter-json-2.1.12.RELEASE.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/com/fasterxml/jackson/datatype/jackson-datatype-jdk8/2.9.10/jackson-datatype-jdk8-2.9.10.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/com/fasterxml/jackson/datatype/jackson-datatype-jsr310/2.9.10/jackson-datatype-jsr310-2.9.10.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/com/fasterxml/jackson/module/jackson-module-parameter-names/2.9.10/jackson-module-parameter-names-2.9.10.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot-starter-tomcat/2.1.12.RELEASE/spring-boot-starter-tomcat-2.1.12.RELEASE.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/apache/tomcat/embed/tomcat-embed-core/9.0.30/tomcat-embed-core-9.0.30.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/apache/tomcat/embed/tomcat-embed-el/9.0.30/tomcat-embed-el-9.0.30.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/apache/tomcat/embed/tomcat-embed-websocket/9.0.30/tomcat-embed-websocket-9.0.30.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/hibernate/validator/hibernate-validator/6.0.18.Final/hibernate-validator-6.0.18.Final.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/javax/validation/validation-api/2.0.1.Final/validation-api-2.0.1.Final.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/jboss/logging/jboss-logging/3.3.3.Final/jboss-logging-3.3.3.Final.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/com/fasterxml/classmate/1.4.0/classmate-1.4.0.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-web/5.1.13.RELEASE/spring-web-5.1.13.RELEASE.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-beans/5.1.13.RELEASE/spring-beans-5.1.13.RELEASE.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-webmvc/5.1.13.RELEASE/spring-webmvc-5.1.13.RELEASE.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-context/5.1.13.RELEASE/spring-context-5.1.13.RELEASE.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-expression/5.1.13.RELEASE/spring-expression-5.1.13.RELEASE.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot-starter-data-jpa/2.1.12.RELEASE/spring-boot-starter-data-jpa-2.1.12.RELEASE.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot-starter-aop/2.1.12.RELEASE/spring-boot-starter-aop-2.1.12.RELEASE.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/aspectj/aspectjweaver/1.9.5/aspectjweaver-1.9.5.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/javax/transaction/javax.transaction-api/1.3/javax.transaction-api-1.3.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/javax/xml/bind/jaxb-api/2.3.1/jaxb-api-2.3.1.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/javax/activation/javax.activation-api/1.2.0/javax.activation-api-1.2.0.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/hibernate/hibernate-core/5.3.15.Final/hibernate-core-5.3.15.Final.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/javax/persistence/javax.persistence-api/2.2/javax.persistence-api-2.2.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/javassist/javassist/3.23.2-GA/javassist-3.23.2-GA.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/net/bytebuddy/byte-buddy/1.9.16/byte-buddy-1.9.16.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/antlr/antlr/2.7.7/antlr-2.7.7.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/jboss/jandex/2.0.5.Final/jandex-2.0.5.Final.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/dom4j/dom4j/2.1.1/dom4j-2.1.1.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/hibernate/common/hibernate-commons-annotations/5.0.4.Final/hibernate-commons-annotations-5.0.4.Final.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/glassfish/jaxb/jaxb-runtime/2.3.1/jaxb-runtime-2.3.1.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/glassfish/jaxb/txw2/2.3.1/txw2-2.3.1.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/com/sun/istack/istack-commons-runtime/3.0.7/istack-commons-runtime-3.0.7.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/jvnet/staxex/stax-ex/1.8/stax-ex-1.8.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/com/sun/xml/fastinfoset/FastInfoset/1.2.15/FastInfoset-1.2.15.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/data/spring-data-jpa/2.1.15.RELEASE/spring-data-jpa-2.1.15.RELEASE.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/data/spring-data-commons/2.1.15.RELEASE/spring-data-commons-2.1.15.RELEASE.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-orm/5.1.13.RELEASE/spring-orm-5.1.13.RELEASE.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-tx/5.1.13.RELEASE/spring-tx-5.1.13.RELEASE.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/slf4j/slf4j-api/1.7.30/slf4j-api-1.7.30.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-aspects/5.1.13.RELEASE/spring-aspects-5.1.13.RELEASE.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot-starter-jdbc/2.1.12.RELEASE/spring-boot-starter-jdbc-2.1.12.RELEASE.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/com/zaxxer/HikariCP/3.2.0/HikariCP-3.2.0.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-jdbc/5.1.13.RELEASE/spring-jdbc-5.1.13.RELEASE.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot-starter-security/2.1.12.RELEASE/spring-boot-starter-security-2.1.12.RELEASE.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-aop/5.1.13.RELEASE/spring-aop-5.1.13.RELEASE.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/security/spring-security-config/5.1.7.RELEASE/spring-security-config-5.1.7.RELEASE.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/security/spring-security-core/5.1.7.RELEASE/spring-security-core-5.1.7.RELEASE.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/security/spring-security-web/5.1.7.RELEASE/spring-security-web-5.1.7.RELEASE.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/projectlombok/lombok/1.18.16/lombok-1.18.16.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot-starter-thymeleaf/3.4.7/spring-boot-starter-thymeleaf-3.4.7.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot-starter/3.4.7/spring-boot-starter-3.4.7.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot/3.4.7/spring-boot-3.4.7.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot-autoconfigure/3.4.7/spring-boot-autoconfigure-3.4.7.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot-starter-logging/3.4.7/spring-boot-starter-logging-3.4.7.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/ch/qos/logback/logback-classic/1.5.18/logback-classic-1.5.18.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/ch/qos/logback/logback-core/1.5.18/logback-core-1.5.18.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/apache/logging/log4j/log4j-to-slf4j/2.24.3/log4j-to-slf4j-2.24.3.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/apache/logging/log4j/log4j-api/2.24.3/log4j-api-2.24.3.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/slf4j/jul-to-slf4j/2.0.17/jul-to-slf4j-2.0.17.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/jakarta/annotation/jakarta.annotation-api/2.1.1/jakarta.annotation-api-2.1.1.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-core/6.2.8/spring-core-6.2.8.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-jcl/6.2.8/spring-jcl-6.2.8.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/yaml/snakeyaml/2.3/snakeyaml-2.3.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/thymeleaf/thymeleaf-spring6/3.1.3.RELEASE/thymeleaf-spring6-3.1.3.RELEASE.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/thymeleaf/thymeleaf/3.1.3.RELEASE/thymeleaf-3.1.3.RELEASE.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/attoparser/attoparser/2.0.7.RELEASE/attoparser-2.0.7.RELEASE.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/unbescape/unbescape/1.1.6.RELEASE/unbescape-1.1.6.RELEASE.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot-starter-web/3.4.7/spring-boot-starter-web-3.4.7.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot-starter-json/3.4.7/spring-boot-starter-json-3.4.7.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/com/fasterxml/jackson/datatype/jackson-datatype-jdk8/2.18.4/jackson-datatype-jdk8-2.18.4.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/com/fasterxml/jackson/datatype/jackson-datatype-jsr310/2.18.4/jackson-datatype-jsr310-2.18.4.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/com/fasterxml/jackson/module/jackson-module-parameter-names/2.18.4/jackson-module-parameter-names-2.18.4.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot-starter-tomcat/3.4.7/spring-boot-starter-tomcat-3.4.7.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/apache/tomcat/embed/tomcat-embed-core/10.1.42/tomcat-embed-core-10.1.42.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/apache/tomcat/embed/tomcat-embed-el/10.1.42/tomcat-embed-el-10.1.42.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/apache/tomcat/embed/tomcat-embed-websocket/10.1.42/tomcat-embed-websocket-10.1.42.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-web/6.2.8/spring-web-6.2.8.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-beans/6.2.8/spring-beans-6.2.8.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/io/micrometer/micrometer-observation/1.14.8/micrometer-observation-1.14.8.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/io/micrometer/micrometer-commons/1.14.8/micrometer-commons-1.14.8.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-webmvc/6.2.8/spring-webmvc-6.2.8.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-context/6.2.8/spring-context-6.2.8.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-expression/6.2.8/spring-expression-6.2.8.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot-starter-data-jpa/3.4.7/spring-boot-starter-data-jpa-3.4.7.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/hibernate/orm/hibernate-core/6.6.18.Final/hibernate-core-6.6.18.Final.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/jakarta/persistence/jakarta.persistence-api/3.1.0/jakarta.persistence-api-3.1.0.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/jakarta/transaction/jakarta.transaction-api/2.0.1/jakarta.transaction-api-2.0.1.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/jboss/logging/jboss-logging/3.6.1.Final/jboss-logging-3.6.1.Final.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/hibernate/common/hibernate-commons-annotations/7.0.3.Final/hibernate-commons-annotations-7.0.3.Final.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/io/smallrye/jandex/3.2.0/jandex-3.2.0.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/com/fasterxml/classmate/1.7.0/classmate-1.7.0.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/net/bytebuddy/byte-buddy/1.15.11/byte-buddy-1.15.11.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/jakarta/xml/bind/jakarta.xml.bind-api/4.0.2/jakarta.xml.bind-api-4.0.2.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/jakarta/activation/jakarta.activation-api/2.1.3/jakarta.activation-api-2.1.3.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/glassfish/jaxb/jaxb-runtime/4.0.5/jaxb-runtime-4.0.5.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/glassfish/jaxb/jaxb-core/4.0.5/jaxb-core-4.0.5.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/eclipse/angus/angus-activation/2.0.2/angus-activation-2.0.2.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/glassfish/jaxb/txw2/4.0.5/txw2-4.0.5.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/com/sun/istack/istack-commons-runtime/4.1.2/istack-commons-runtime-4.1.2.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/jakarta/inject/jakarta.inject-api/2.0.1/jakarta.inject-api-2.0.1.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/antlr/antlr4-runtime/4.13.0/antlr4-runtime-4.13.0.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/data/spring-data-jpa/3.4.7/spring-data-jpa-3.4.7.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/data/spring-data-commons/3.4.7/spring-data-commons-3.4.7.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-orm/6.2.8/spring-orm-6.2.8.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-tx/6.2.8/spring-tx-6.2.8.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-aspects/6.2.8/spring-aspects-6.2.8.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/aspectj/aspectjweaver/1.9.24/aspectjweaver-1.9.24.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot-starter-jdbc/3.4.7/spring-boot-starter-jdbc-3.4.7.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/com/zaxxer/HikariCP/5.1.0/HikariCP-5.1.0.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-jdbc/6.2.8/spring-jdbc-6.2.8.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot-starter-security/3.4.7/spring-boot-starter-security-3.4.7.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-aop/6.2.8/spring-aop-6.2.8.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/security/spring-security-config/6.4.7/spring-security-config-6.4.7.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/security/spring-security-core/6.4.7/spring-security-core-6.4.7.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/security/spring-security-crypto/6.4.7/spring-security-crypto-6.4.7.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/security/spring-security-web/6.4.7/spring-security-web-6.4.7.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/thymeleaf/extras/thymeleaf-extras-springsecurity6/3.1.1.RELEASE/thymeleaf-extras-springsecurity6-3.1.1.RELEASE.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/slf4j/slf4j-api/2.0.17/slf4j-api-2.0.17.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/projectlombok/lombok/1.18.38/lombok-1.18.38.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/io/jsonwebtoken/jjwt/0.9.0/jjwt-0.9.0.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/com/fasterxml/jackson/core/jackson-databind/2.9.10.2/jackson-databind-2.9.10.2.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/com/fasterxml/jackson/core/jackson-annotations/2.9.10/jackson-annotations-2.9.10.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/com/fasterxml/jackson/core/jackson-core/2.9.10/jackson-core-2.9.10.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/mysql/mysql-connector-java/8.0.19/mysql-connector-java-8.0.19.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/joda-time/joda-time/2.10.5/joda-time-2.10.5.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/com/fasterxml/jackson/core/jackson-databind/2.18.4/jackson-databind-2.18.4.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/com/fasterxml/jackson/core/jackson-annotations/2.18.4/jackson-annotations-2.18.4.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/com/fasterxml/jackson/core/jackson-core/2.18.4.1/jackson-core-2.18.4.1.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/com/mysql/mysql-connector-j/9.1.0/mysql-connector-j-9.1.0.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/joda-time/joda-time/2.10.9/joda-time-2.10.9.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/apache/commons/commons-csv/1.5/commons-csv-1.5.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/javax/xml/bind/jaxb-api/2.3.0/jaxb-api-2.3.0.jar" enabled="true" runInBatchMode="false"/>
</factorypath>

View file

@ -1,4 +1,6 @@
eclipse.preferences.version=1
encoding//src/main/java=UTF-8
encoding//src/main/resources=UTF-8
encoding//src/test/java=UTF-8
encoding//src/test/resources=UTF-8
encoding/<project>=UTF-8

View file

@ -10,5 +10,5 @@ org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled
org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning
org.eclipse.jdt.core.compiler.processAnnotations=enabled
org.eclipse.jdt.core.compiler.release=disabled
org.eclipse.jdt.core.compiler.release=enabled
org.eclipse.jdt.core.compiler.source=17

View file

@ -80,6 +80,11 @@
<artifactId>jaxb-api</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.8.5</version>
</dependency>
</dependencies>
<properties>

View file

@ -0,0 +1,23 @@
package com.stephenschafer.budget;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@ToString
public class BudgetAmounts {
private String year = null;
private Integer categoryId;
private BigDecimal yearBudget;
private Map<Integer, BigDecimal> monthBudgets = new HashMap<>();
public void setMonthBudget(final int monthNum, final BigDecimal amount) {
monthBudgets.put(Integer.valueOf(monthNum), amount);
}
}

View file

@ -0,0 +1,175 @@
package com.stephenschafer.budget;
import java.util.regex.Pattern;
public class CatRegex {
private final Pattern pattern;
private final String category;
private final String source;
private final int priority;
private final String extraDescription;
private final Integer year;
private int id;
public CatRegex(final int id, final Pattern pattern, final String category, final String source,
final int priority, final String extraDescription, final Integer year) {
this.id = id;
this.pattern = pattern;
this.category = category;
this.source = source;
this.priority = priority;
this.extraDescription = extraDescription;
this.year = year;
}
public CatRegex(final String line) {
final String[] parts = line.split(",");
final String category = parts[0].trim();
final String regex = parts[1].trim();
String flagsString = "";
String source = "";
int priority = 0;
String extraDescription = "";
Integer year = null;
if (parts.length > 2) {
flagsString = parts[2].trim();
if (parts.length > 3) {
source = parts[3].trim();
if (parts.length > 4) {
final String priorityString = parts[4].trim();
if (priorityString.length() > 0) {
try {
priority = Integer.parseInt(priorityString);
}
catch (final NumberFormatException e) {
System.out.println(
"Invalid priority in reegex line: " + line + "<br/>");
}
}
if (parts.length > 5) {
extraDescription = parts[5].trim();
if (parts.length > 6) {
final String yearString = parts[6].trim();
if (yearString.length() > 0) {
try {
year = Integer.valueOf(yearString);
}
catch (final NumberFormatException e) {
System.out.println(
"Invalid year in reegex line: " + line + "<br/>");
}
}
if (parts.length > 7) {
System.out.println("Too many parts in " + line);
}
}
}
}
}
}
int flags = 0;
if (flagsString.indexOf("i") >= 0) {
flags |= Pattern.CASE_INSENSITIVE;
}
if (flagsString.indexOf("m") >= 0) {
flags |= Pattern.MULTILINE;
}
pattern = Pattern.compile(regex, flags);
this.category = category;
this.source = source.length() == 0 ? null : source;
this.priority = priority;
this.extraDescription = extraDescription;
this.year = year;
}
public String toLine() {
final StringBuilder sb = new StringBuilder();
sb.append(category);
sb.append(", ");
sb.append(pattern.pattern());
sb.append(", ");
String flags = "";
if ((pattern.flags() & Pattern.CASE_INSENSITIVE) != 0) {
flags += "i";
}
if ((pattern.flags() & Pattern.MULTILINE) != 0) {
flags += "m";
}
sb.append(flags);
sb.append(", ");
sb.append(source == null ? "" : source);
sb.append(", ");
sb.append(priority == 0 ? "" : String.valueOf(priority));
sb.append(", ");
sb.append(extraDescription);
sb.append(", ");
sb.append(year == null ? "" : year.toString());
return sb.toString();
}
public String getSource() {
return source;
}
public Pattern getPattern() {
return pattern;
}
public String getCategory() {
return category;
}
public int getPriority() {
return priority;
}
@Override
public int hashCode() {
int hashCode = 0;
if (source != null) {
hashCode += source.hashCode();
}
return hashCode + pattern.hashCode();
}
@Override
public boolean equals(final Object obj) {
if (obj instanceof CatRegex) {
final CatRegex that = (CatRegex) obj;
if (!source.equals(that.source) || !pattern.equals(that.pattern)) {
return false;
}
}
return true;
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder();
sb.append(category);
sb.append(": ");
sb.append(pattern.toString());
if (source != null && source.trim().length() > 0) {
sb.append(" (" + source + ")");
}
sb.append(", ");
sb.append(priority);
return sb.toString();
}
public String getExtraDescription() {
return extraDescription;
}
public int getId() {
return id;
}
public void setId(final int id) {
this.id = id;
}
public Integer getYear() {
return year;
}
}

View file

@ -0,0 +1,17 @@
package com.stephenschafer.budget;
import java.util.ArrayList;
import java.util.List;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@ToString
public class CategorizationResponse {
String year = null;
List<MultiplyAssignedTransaction> multiplyAssignedTransactions = new ArrayList<>();
List<UnresolvedItem> unassignedTransactions = new ArrayList<>();
}

View file

@ -0,0 +1,362 @@
package com.stephenschafer.budget;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.math.BigDecimal;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.StatementCallback;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@CrossOrigin(origins = "*", maxAge = 3600)
@RestController
public class CategorizerController {
@Autowired
TransactionDao transactionDao;
@Autowired
UserService userService;
@Autowired
JdbcTemplate jdbcTemplate;
@GetMapping("/problems")
@ResponseBody
private ApiResponse<Map<String, CategorizationResponse>> getProblems(
final HttpServletRequest request)
throws SQLException, UnsupportedEncodingException, FileNotFoundException, IOException {
log.debug("GET /problems");
if (!userService.isAuthorized(request)) {
return new ApiResponse<>(HttpStatus.UNAUTHORIZED.value(),
"You are not authorized to do this", null);
}
final Map<String, CategorizationResponse> response = new HashMap<>();
final Set<String> years = getYears();
for (final String year : years) {
final CategorizationResponse yearResponse = new CategorizationResponse();
yearResponse.year = year;
response.put(year, yearResponse);
final String uatSql = Util.getResourceAsString("getTransactionDescriptions.sql") //
.replace("${databaseName}", "budget_" + year);
jdbcTemplate.execute((StatementCallback<Void>) stmt -> {
try (ResultSet rs = stmt.executeQuery(uatSql)) {
while (rs.next()) {
rs.getInt(1);
final java.sql.Date date = rs.getDate(2);
final String source = rs.getString(3);
final String description = rs.getString(4);
final BigDecimal tmpAmount = rs.getBigDecimal(5);
final BigDecimal amount = tmpAmount == null ? new BigDecimal(0) : tmpAmount;
final UnresolvedItem unresolvedItem = new UnresolvedItem(
Integer.parseInt(year), source, description, date, amount);
yearResponse.unassignedTransactions.add(unresolvedItem);
}
}
return null;
});
final String matSql = Util.getResourceAsString("getMultiplyAssignedTransactions.sql") //
.replace("${databaseName}", "budget_" + year);
jdbcTemplate.execute((StatementCallback<Void>) stmt -> {
try (ResultSet rs = stmt.executeQuery(matSql)) {
final List<RegexDisplay> regexes = new ArrayList<>();
Detail detail = null;
while (rs.next()) {
final Detail lastDetail = detail;
int i = 0;
final int transId = rs.getInt(++i);
final java.sql.Date date = rs.getDate(++i);
final String source = rs.getString(++i);
final String description = rs.getString(++i);
final BigDecimal tmpAmount = rs.getBigDecimal(++i);
final BigDecimal amount = tmpAmount == null ? new BigDecimal(0) : tmpAmount;
final int regexId = rs.getInt(++i);
final int categoryId = rs.getInt(++i);
final String expression = rs.getString(++i);
final int flags = rs.getInt(++i);
final String requiredSource = rs.getString(++i);
final int priority = rs.getInt(++i);
final String extraDescription = rs.getString(++i);
final int tmpRequiredYear = rs.getInt(++i);
final Integer requiredYear = rs.wasNull() ? null
: Integer.valueOf(tmpRequiredYear);
final String fqCategoryName = null;
final Regex regex = new Regex(regexId, categoryId, expression, flags,
requiredSource, priority, extraDescription, requiredYear);
final RegexDisplay regexDisplay = new RegexDisplay(regex, fqCategoryName);
regexes.add(regexDisplay);
detail = new Detail(transId, source, description, date, amount);
if (lastDetail != null
&& lastDetail.getTransactionId() != detail.getTransactionId()) {
final MultiplyAssignedTransaction mat = new MultiplyAssignedTransaction();
mat.setRegexes(regexes);
mat.setTransaction(lastDetail);
yearResponse.multiplyAssignedTransactions.add(mat);
regexes.clear();
}
}
if (detail != null) {
final MultiplyAssignedTransaction mat = new MultiplyAssignedTransaction();
mat.setRegexes(regexes);
mat.setTransaction(detail);
yearResponse.multiplyAssignedTransactions.add(mat);
}
}
return null;
});
}
return new ApiResponse<>(HttpStatus.OK.value(), "Problems fetched successfully", response);
}
@GetMapping("/categorize")
@ResponseBody
private ApiResponse<Map<String, CategorizationResponse>> categorize(
final HttpServletRequest request)
throws SQLException, UnsupportedEncodingException, FileNotFoundException, IOException {
log.info("GET /categorize");
if (!userService.isAuthorized(request)) {
return new ApiResponse<>(HttpStatus.UNAUTHORIZED.value(),
"You are not authorized to do this", null);
}
final Map<String, CategorizationResponse> response = new HashMap<>();
final Map<String, List<Detail>> categoryMap = new HashMap<>();
final Set<String> years = getYears();
final List<CatRegex> catRegexes = getCatRegexes();
final Set<UnresolvedItem> unresolvedSet = new HashSet<>();
for (final String year : years) {
final CategorizationResponse yearResponse = new CategorizationResponse();
yearResponse.year = year;
response.put(year, yearResponse);
final Map<Integer, String> extraDescriptionMap = new HashMap<>();
final Map<Integer, Integer> linkedRegexes = new HashMap<>();
jdbcTemplate.execute(Util.getResourceAsString("clearExtraDescriptions.sql") //
.replace("${databaseName}", "budget_" + year));
jdbcTemplate.execute("drop table if exists ${databaseName}.transaction_regex_mtm" //
.replace("${databaseName}", "budget_" + year));
jdbcTemplate.execute(Util.getResourceAsString("createTransactionRegexTable.sql") //
.replace("${databaseName}", "budget_" + year));
final String sql = Util.getResourceAsString("getTransactionDescriptions.sql") //
.replace("${databaseName}", "budget_" + year);
final String insertSql = Util.getResourceAsString("insertTransactionRegex.sql") //
.replace("${databaseName}", "budget_" + year);
jdbcTemplate.execute((StatementCallback<Void>) stmt -> {
try (ResultSet rs = stmt.executeQuery(sql)) {
while (rs.next()) {
final int transactionId = rs.getInt(1);
final java.sql.Date date = rs.getDate(2);
final String source = rs.getString(3);
final String description = rs.getString(4);
final BigDecimal tmpAmount = rs.getBigDecimal(5);
final BigDecimal amount = tmpAmount == null ? new BigDecimal(0) : tmpAmount;
final Detail detail = new Detail(transactionId, source, description, date,
amount);
if (description != null) {
int maxPriority = Integer.MIN_VALUE;
final Map<String, Set<CatRegex>> matchesFound = new HashMap<>();
for (final CatRegex catRegex : catRegexes) {
final String requiredSource = catRegex.getSource();
if (requiredSource == null || requiredSource.equals(source)) {
final Integer requiredYear = catRegex.getYear();
if (requiredYear == null
|| requiredYear.equals(Integer.valueOf(year))) {
final Pattern pattern = catRegex.getPattern();
final Matcher matcher = pattern.matcher(description);
final String category = catRegex.getCategory();
if (matcher.matches()) {
final int priority = catRegex.getPriority();
if (priority > maxPriority) {
maxPriority = priority;
final int regexId = catRegex.getId();
linkedRegexes.put(transactionId, regexId);
}
Set<CatRegex> set = matchesFound.get(category);
if (set == null) {
set = new HashSet<>();
matchesFound.put(category, set);
}
set.add(catRegex);
}
}
}
}
if (matchesFound.isEmpty()) {
final Calendar cal = new GregorianCalendar();
cal.setTime(date);
if (cal.get(Calendar.YEAR) > 2023) {
final UnresolvedItem unresolvedItem = new UnresolvedItem(
Integer.parseInt(year), source, description, date,
amount);
unresolvedSet.add(unresolvedItem);
yearResponse.unassignedTransactions.add(unresolvedItem);
}
}
else if (matchesFound.size() > 1) {
// more than one category matches this description
// find the one(s) with the highest priority
final Map<String, Set<CatRegex>> actualMatchesFound = new HashMap<>();
for (final String category : matchesFound.keySet()) {
final Set<CatRegex> set = matchesFound.get(category);
for (final CatRegex catRegex : set) {
if (catRegex.getPriority() == maxPriority) {
Set<CatRegex> newSet = actualMatchesFound.get(category);
if (newSet == null) {
newSet = new HashSet<>();
actualMatchesFound.put(category, newSet);
}
newSet.add(catRegex);
}
}
}
if (actualMatchesFound.size() > 1) {
// more than one category matches this description and also has the max priority
// print to HTML so user can make the regex more precise
final MultiplyAssignedTransaction multiplyAssignedTransaction = new MultiplyAssignedTransaction();
multiplyAssignedTransaction.transaction = detail;
yearResponse.multiplyAssignedTransactions.add(
multiplyAssignedTransaction);
for (final String category : actualMatchesFound.keySet()) {
final Set<CatRegex> newSet = actualMatchesFound.get(
category);
for (final CatRegex catRegex : newSet) {
multiplyAssignedTransaction.regexes.add(
new RegexDisplay(catRegex));
jdbcTemplate.update(insertSql, catRegex.getId(),
transactionId);
}
}
}
else {
addTransactionToCategory(year, actualMatchesFound, categoryMap,
extraDescriptionMap, detail);
}
}
else {
addTransactionToCategory(year, matchesFound, categoryMap,
extraDescriptionMap, detail);
}
}
}
}
return null;
});
for (final Integer transactionId : extraDescriptionMap.keySet()) {
final String extraDescription = extraDescriptionMap.get(transactionId);
final String setSql = Util.getResourceAsString("setExtraDescription.sql") //
.replace("${databaseName}", "budget_" + year);
jdbcTemplate.update(setSql, extraDescription, transactionId);
}
for (final Integer transactionId : linkedRegexes.keySet()) {
final Integer regexId = linkedRegexes.get(transactionId);
final String linkRegexSql = Util.getResourceAsString("updateRegexLink.sql") //
.replace("${databaseName}", "budget_" + year);
try {
System.out.println("updating year " + year + ", trans " + transactionId
+ " with regex id " + regexId);
jdbcTemplate.update(linkRegexSql, regexId, transactionId);
}
catch (final Exception e) {
e.printStackTrace();
}
}
}
return new ApiResponse<>(HttpStatus.OK.value(), "Categorization fetched successfully",
response);
}
void addTransactionToCategory(final String year, final Map<String, Set<CatRegex>> matchesFound,
final Map<String, List<Detail>> categoryMap,
final Map<Integer, String> extraDescriptionMap, final Detail detail) {
final StringBuilder sb = new StringBuilder();
String sep = "";
for (final String category : matchesFound.keySet()) {
final Set<CatRegex> regexSet = matchesFound.get(category);
List<Detail> details = categoryMap.get(category);
if (details == null) {
details = new ArrayList<>();
categoryMap.put(category, details);
}
details.add(detail);
for (final CatRegex regex : regexSet) {
sb.append(sep);
sep = ", ";
sb.append(regex.getExtraDescription());
}
}
extraDescriptionMap.put(detail.transactionId, sb.toString());
}
Set<String> getYears() throws IOException, SQLException {
final String sql = Util.getResourceAsString("getYears.sql") //
.replace("${databaseName}", "budget");
final List<Integer> list = jdbcTemplate.queryForList(sql, Integer.class);
final Set<String> result = new HashSet<>();
for (final Integer year : list) {
result.add(year.toString());
}
return result;
}
Map<Integer, Category> loadCategories() throws IOException, SQLException {
final Map<Integer, Category> categories = new HashMap<>();
final String sql = Util.getResourceAsString("getCategories.sql") //
.replace("${databaseName}", "budget");
final List<Category> categoryList = jdbcTemplate.query(sql,
(RowMapper<Category>) (rs, rowNum) -> {
final int id = rs.getInt(1);
final int parentId = rs.getInt(2);
final String name = rs.getString(3);
return new Category(id, parentId, name);
});
for (final Category category : categoryList) {
categories.put(Integer.valueOf(category.id), category);
}
return categories;
}
List<CatRegex> getCatRegexes() throws IOException, SQLException {
final Map<Integer, Category> categories = loadCategories();
final String sql = Util.getResourceAsString("getRegexes.sql") //
.replace("${databaseName}", "budget");
final List<CatRegex> catRegexes = jdbcTemplate.query(sql,
(RowMapper<CatRegex>) (resultSet, rowNum) -> {
int i = 0;
final int id = resultSet.getInt(++i);
final int categoryId = resultSet.getInt(++i);
final String regex = resultSet.getString(++i);
final int flags = resultSet.getInt(++i);
final String source = resultSet.getString(++i);
final int priority = resultSet.getInt(++i);
final String description = resultSet.getString(++i);
final int yearInt = resultSet.getInt(++i);
final Integer year = resultSet.wasNull() ? null : Integer.valueOf(yearInt);
final Pattern pattern = Pattern.compile(regex, flags);
final Category category = categories.get(categoryId);
final String categoryName = category == null ? null
: category.getFullName(categories);
final CatRegex catRegex = new CatRegex(id, pattern, categoryName, source, priority,
description, year);
return catRegex;
});
return catRegexes;
}
}

View file

@ -1,5 +1,7 @@
package com.stephenschafer.budget;
import java.util.Map;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
@ -17,6 +19,19 @@ public class Category {
this(intValue, category.getParentCategoryId(), category.getName());
}
String getFullName(final Map<Integer, Category> categories) {
final StringBuilder sb = new StringBuilder();
if (parentCategoryId != null) {
final Category parentCategory = categories.get(parentCategoryId);
if (parentCategory != null) {
sb.append(parentCategory.getFullName(categories));
sb.append(".");
}
}
sb.append(name);
return sb.toString();
}
Integer id;
Integer parentCategoryId;
String name;

View file

@ -79,7 +79,7 @@ public class CategoryDaoImpl implements CategoryDao {
@Override
public List<Category> getCategoryAncestry(final int categoryId) {
return getCategoryAncestry(null, categoryId);
return getCategoryAncestry(new ArrayList<>(), categoryId);
}
private List<Category> getCategoryAncestry(final List<Category> ancestry,

View file

@ -13,7 +13,8 @@ public class CustomCorsConfiguration implements CorsConfigurationSource {
@Override
public CorsConfiguration getCorsConfiguration(final HttpServletRequest request) {
final CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("http://localhost:3000"));
config.setAllowedOrigins(
List.of("http://localhost:3000", "http://localhost:3001", "http://kirk:3001"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
config.setAllowedHeaders(List.of("*"));
return config;

View file

@ -0,0 +1,21 @@
package com.stephenschafer.budget;
import java.math.BigDecimal;
import java.util.Date;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@AllArgsConstructor
@ToString
class Detail {
int transactionId;
String source;
String description;
Date date;
BigDecimal amount;
}

View file

@ -19,8 +19,8 @@ import io.jsonwebtoken.SignatureAlgorithm;
@Component
public class JwtTokenUtil implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@Serial
private static final long serialVersionUID = 1L;
public String getUsernameFromToken(final String token) {
return getClaimFromToken(token, Claims::getSubject);
@ -51,11 +51,13 @@ public class JwtTokenUtil implements Serializable {
private String doGenerateToken(final String subject) {
final Claims claims = Jwts.claims().setSubject(subject);
claims.put("scopes", Arrays.asList(new SimpleGrantedAuthority("ROLE_ADMIN")));
return Jwts.builder().setClaims(claims).setIssuer("http://stephenschafer.com").setIssuedAt(
new Date(System.currentTimeMillis())).setExpiration(
new Date(System.currentTimeMillis()
+ ACCESS_TOKEN_VALIDITY_SECONDS * 1000L)).signWith(SignatureAlgorithm.HS256,
SIGNING_KEY).compact();
return Jwts.builder() //
.setClaims(claims) //
.setIssuer("http://stephenschafer.com") //
.setIssuedAt(new Date(System.currentTimeMillis())) //
.setExpiration(
new Date(System.currentTimeMillis() + ACCESS_TOKEN_VALIDITY_SECONDS * 1000L)) //
.signWith(SignatureAlgorithm.HS256, SIGNING_KEY).compact();
}
public Boolean validateToken(final String token, final UserDetails userDetails) {

View file

@ -0,0 +1,16 @@
package com.stephenschafer.budget;
import java.util.ArrayList;
import java.util.List;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@ToString
public class MultiplyAssignedTransaction {
Detail transaction = null;
List<RegexDisplay> regexes = new ArrayList<>();
}

View file

@ -7,6 +7,7 @@ import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
@ -90,6 +91,27 @@ public class RegexController {
return new ApiResponse<>(HttpStatus.OK.value(), "Regex retrieved successfully", regex);
}
@DeleteMapping("/regex/{regexId}")
@ResponseBody
public ApiResponse<Regex> deleteRegex(@PathVariable(required = true) final Integer regexId,
final HttpServletRequest request) {
log.info("DELETE /regex/" + regexId);
if (!userService.isAuthorized(request)) {
return new ApiResponse<>(HttpStatus.UNAUTHORIZED.value(),
"You are not authorized to do this", null);
}
if (regexId == null) {
return new ApiResponse<>(HttpStatus.BAD_REQUEST.value(), "Regex ID not specified",
null);
}
final Optional<Regex> optionalRegex = regexDao.getById(regexId.intValue());
if (!optionalRegex.isPresent()) {
return new ApiResponse<>(HttpStatus.NOT_FOUND.value(), "Regex ID not found", null);
}
regexDao.deleteById(regexId.intValue());
return new ApiResponse<>(HttpStatus.OK.value(), "Regex deleted successfully", null);
}
@GetMapping("/regexes/category/{categoryName}")
@ResponseBody
public ApiResponse<List<Regex>> getRegexesByCategory(

View file

@ -15,6 +15,13 @@ public class RegexDisplay {
regex.priority, regex.description, regex.year);
}
public RegexDisplay(final CatRegex catRegex) {
this(catRegex.getId(), (Integer) null, catRegex.getCategory(),
catRegex.getPattern().pattern(), catRegex.getPattern().flags(),
catRegex.getSource(), catRegex.getPriority(), catRegex.getExtraDescription(),
catRegex.getYear());
}
Integer id;
Integer categoryId;
String fqCategoryName;

View file

@ -0,0 +1,16 @@
package com.stephenschafer.budget;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@AllArgsConstructor
@ToString
public class Report {
String year = null;
int monthCount = 0;
ReportCategory rootCategory = null;
}

View file

@ -0,0 +1,232 @@
package com.stephenschafer.budget;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@ToString
public class ReportCategory {
private final Integer id;
private final Integer parentId;
private final String name;
@JsonIgnore
private ReportCategory parent;
private boolean included;
private List<ReportDetail> details = new ArrayList<>();
private Map<Integer, BigDecimal> monthTotals = null;
private Map<Integer, BigDecimal> monthGrandTotals = null;
@JsonIgnore
private final Map<Integer, ReportCategory> children = new HashMap<>();
private final int monthCount;
private final BudgetAmounts budgetAmounts = new BudgetAmounts();
public ReportCategory(final Integer id, final Integer parentId, final String name,
final int monthCount) {
this.id = id;
this.parentId = parentId;
this.name = name;
this.monthCount = monthCount;
}
public void updateParent(final Map<Integer, ReportCategory> categories) {
if (parentId == null) {
parent = null;
}
else {
parent = categories.get(parentId);
parent.addChild(this);
}
}
public void addChild(final ReportCategory category) {
children.put(category.id, category);
}
@JsonIgnore
public ReportCategory getParent() {
return parent;
}
public void setParent(final ReportCategory parent) {
this.parent = parent;
}
public Integer getId() {
return id;
}
public Integer getParentId() {
return parentId;
}
public String getName() {
return name;
}
public String getQualifiedName() {
final StringBuilder sb = new StringBuilder();
if (parent != null) {
sb.append(parent.getQualifiedName());
sb.append(".");
}
sb.append(name);
return sb.toString();
}
public List<ReportDetail> getDetails() {
return details;
}
public void setDetails(final List<ReportDetail> details) {
this.details = details;
}
public void addDetail(final ReportDetail detail) {
details.add(detail);
}
public Map<Integer, ReportCategory> getChildren() {
return children;
}
public BigDecimal getDetailTotal() {
var amount = new BigDecimal(0);
for (final ReportDetail detail : details) {
amount = amount.add(detail.amount);
}
return amount;
}
public BigDecimal getMonthTotal(final int month) {
var amount = new BigDecimal(0);
for (final ReportDetail detail : details) {
final Calendar cal = new GregorianCalendar();
cal.setTime(detail.date);
if (month == cal.get(Calendar.MONTH)) {
amount = amount.add(detail.amount);
}
}
return amount;
}
public Map<Integer, BigDecimal> getMonthTotals() {
Map<Integer, BigDecimal> monthTotals = this.monthTotals;
if (monthTotals == null) {
monthTotals = new HashMap<>();
for (final ReportDetail detail : details) {
final Calendar cal = new GregorianCalendar();
cal.setTime(detail.date);
final int month = cal.get(Calendar.MONTH);
BigDecimal monthTotal = monthTotals.get(month);
if (monthTotal == null) {
monthTotal = new BigDecimal(0);
}
monthTotals.put(month, monthTotal.add(detail.amount));
}
this.monthTotals = monthTotals;
}
return monthTotals;
}
public BigDecimal getGrandTotal() {
var total = getDetailTotal();
for (final Integer categoryId : children.keySet()) {
final var category = children.get(categoryId);
total = total.add(category.getGrandTotal());
}
return total;
}
public BigDecimal getGrandAverage() {
final BigDecimal grandTotal = getGrandTotal();
if (monthCount == 0) {
return new BigDecimal(0);
}
return grandTotal.divide(new BigDecimal(monthCount), RoundingMode.HALF_DOWN);
}
public Map<Integer, BigDecimal> getMonthGrandTotals() {
Map<Integer, BigDecimal> monthGrandTotals = this.monthGrandTotals;
if (monthGrandTotals == null) {
monthGrandTotals = this.getMonthTotals();
for (final Integer categoryId : children.keySet()) {
final var childCategory = children.get(categoryId);
final Map<Integer, BigDecimal> childMonthGrandTotals = childCategory.getMonthGrandTotals();
for (final Integer monthNum : childMonthGrandTotals.keySet()) {
final BigDecimal childMonthGrandTotal = childMonthGrandTotals.get(monthNum);
if (childMonthGrandTotal != null) {
final BigDecimal previousValue = monthGrandTotals.containsKey(monthNum)
? monthGrandTotals.get(monthNum)
: new BigDecimal(0);
monthGrandTotals.put(monthNum, previousValue.add(childMonthGrandTotal));
}
}
}
this.monthGrandTotals = monthGrandTotals;
}
return monthGrandTotals;
}
public int getLargestMonth() {
final Map<Integer, BigDecimal> monthGrandTotals = this.getMonthGrandTotals();
BigDecimal max = new BigDecimal(0);
int maxMonth = -1;
int maxCount = 0;
for (int month = 0; month < 12; month++) {
BigDecimal monthAmount = monthGrandTotals.get(month);
if (monthAmount == null) {
monthAmount = new BigDecimal(0);
}
if (monthAmount.compareTo(max) == 0) {
maxCount++;
}
else if (monthAmount.compareTo(max) > 0) {
max = monthAmount;
maxMonth = month;
maxCount = 1;
}
}
return maxCount > 1 ? -1 : maxMonth;
}
public List<ReportCategory> getChildCategories() {
final List<ReportCategory> categories = new ArrayList<>();
for (final Integer categoryId : children.keySet()) {
final var category = children.get(categoryId);
categories.add(category);
}
categories.sort((arg0, arg1) -> {
final var name0 = arg0.getName();
final var name1 = arg1.getName();
final var comparison = name0.compareTo(name1);
if (comparison != 0) {
return comparison;
}
final var id0 = arg0.getId();
final var id1 = arg1.getId();
return id0.compareTo(id1);
});
return categories;
}
public boolean isIncluded() {
return included;
}
public void setIncluded(final boolean include) {
this.included = include;
}
}

View file

@ -0,0 +1,272 @@
package com.stephenschafer.budget;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.math.BigDecimal;
import java.sql.SQLException;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowCallbackHandler;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@CrossOrigin(origins = "*", maxAge = 3600)
@RestController
public class ReportController {
@Autowired
TransactionDao transactionDao;
@Autowired
UserService userService;
@Autowired
JdbcTemplate jdbcTemplate;
@PutMapping("/budget")
@ResponseBody
public ApiResponse<Report> putBudget(@RequestBody final Map<String, Report> reports,
final HttpServletRequest request) {
if (!userService.isAuthorized(request)) {
return new ApiResponse<>(HttpStatus.UNAUTHORIZED.value(),
"You are not authorized to do this", null);
}
categoryDao.update(report);
return new ApiResponse<>(HttpStatus.OK.value(), "Category updated successfully", report);
}
@GetMapping("/report")
@ResponseBody
private ApiResponse<Map<String, Report>> report(
@RequestParam(required = false) List<String> years,
@RequestParam(required = false, name = "startDate") final String startDateString,
@RequestParam(required = false, name = "endDate") final String endDateString,
@RequestParam(required = false) final List<String> includes,
final HttpServletRequest request)
throws SQLException, UnsupportedEncodingException, FileNotFoundException, IOException {
log.info("GET /report");
if (!userService.isAuthorized(request)) {
return new ApiResponse<>(HttpStatus.UNAUTHORIZED.value(),
"You are not authorized to do this", null);
}
if (years == null) {
years = getYears();
}
var tmpIncludeExpenses = true;
var tmpIncludeIncome = true;
var tmpIncludePayments = false;
var tmpIncludeInvestments = false;
if (includes != null && !includes.isEmpty()) {
tmpIncludeExpenses = false;
tmpIncludeIncome = false;
tmpIncludePayments = false;
tmpIncludeInvestments = false;
for (final String include : includes) {
if ("expenses".equalsIgnoreCase(include)) {
tmpIncludeExpenses = true;
}
else if ("income".equalsIgnoreCase(include)) {
tmpIncludeIncome = true;
}
else if ("payments".equalsIgnoreCase(include)) {
tmpIncludePayments = true;
}
else if ("investments".equalsIgnoreCase(include)) {
tmpIncludeInvestments = true;
}
}
}
final var includeExpenses = tmpIncludeExpenses;
final var includeIncome = tmpIncludeIncome;
final var includePayments = tmpIncludePayments;
final var includeInvestments = tmpIncludeInvestments;
final DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
final Date startDate;
final Date endDate;
try {
startDate = startDateString == null ? null : df.parse(startDateString);
endDate = endDateString == null ? null : df.parse(endDateString);
}
catch (final ParseException e) {
return new ApiResponse<>(HttpStatus.BAD_REQUEST.value(), "Invalid date", null);
}
final int startMonth;
if (startDate != null) {
final Calendar startCal = new GregorianCalendar();
startCal.setTime(startDate);
startMonth = startCal.get(Calendar.MONTH);
}
else {
startMonth = 0;
}
final int endMonth;
if (endDate != null) {
final Calendar endCal = new GregorianCalendar();
endCal.setTime(endDate);
endMonth = endCal.get(Calendar.MONTH);
}
else {
endMonth = 11;
}
final int monthCount = endMonth - startMonth + 1;
final Map<String, Report> response = new HashMap<>();
for (final String year : years) {
final var categoryMap = loadCategories(year, includeIncome, includePayments,
includeInvestments, includeExpenses, monthCount);
final String createBudgetTableSql = Util.getResourceAsString(
"createIfNotExistBudgetAmount.sql");
jdbcTemplate.update(createBudgetTableSql);
final String getBudgetAmountsSql = Util.getResourceAsString("getBudgetAmounts.sql");
jdbcTemplate.query(getBudgetAmountsSql, new Object[] {}, new int[] {},
(RowCallbackHandler) rs -> {
var i = 0;
final int categoryId = rs.getInt(++i);
final var category = categoryMap.get(categoryId);
if (category != null) {
final BudgetAmounts budgetAmounts = category.getBudgetAmounts();
budgetAmounts.setCategoryId(categoryId);
budgetAmounts.setYear(year);
budgetAmounts.setYearBudget(rs.getBigDecimal(++i));
for (int monthIndex = 0; monthIndex < 12; monthIndex++) {
budgetAmounts.setMonthBudget(monthIndex, rs.getBigDecimal(++i));
}
}
});
final String sql = Util.getResourceAsString("getTransactionDetail.sql") //
.replace("${databaseName}", "budget_" + year) //
.replace("${regexDatabaseName}", "budget");
jdbcTemplate.query(sql, new Object[] {}, new int[] {}, (RowCallbackHandler) rs -> {
final var detail = new ReportDetail();
var i = 0;
detail.transactionId = rs.getInt(++i);
detail.date = rs.getDate(++i);
detail.source = rs.getString(++i);
detail.description = rs.getString(++i);
detail.amount = rs.getBigDecimal(++i);
if (rs.wasNull()) {
detail.amount = new BigDecimal(0);
}
final var categoryId = rs.getInt(++i);
detail.regex = rs.getString(++i);
detail.flags = rs.getInt(++i);
detail.requiredSource = rs.getString(++i);
detail.extraDescription = rs.getString(++i);
final var category = categoryMap.get(categoryId);
if (category != null && //
detail.isAfter(startDate) && //
detail.isBefore(endDate) && //
category.isIncluded()) {
category.addDetail(detail);
}
});
final var rootCategory = new ReportCategory(-1, null, "total", monthCount);
for (final Integer categoryId : categoryMap.keySet()) {
final var category = categoryMap.get(categoryId);
if ((category != null) && (category.getParent() == null)) {
rootCategory.addChild(category);
}
}
final Report report = new Report(year, monthCount, rootCategory);
response.put(year, report);
}
return new ApiResponse<>(HttpStatus.OK.value(), "Report fetched successfully", response);
}
List<String> getYears() throws IOException, SQLException {
final String sql = Util.getResourceAsString("getYears.sql") //
.replace("${databaseName}", "budget");
final List<Integer> list = jdbcTemplate.queryForList(sql, Integer.class);
final List<String> result = new ArrayList<>();
for (final Integer year : list) {
result.add(year.toString());
}
return result;
}
interface CategoryFilter {
boolean include(ReportCategory category);
}
private Map<Integer, ReportCategory> loadCategories(final String year,
final boolean includeIncome, final boolean includePayments,
final boolean includeInvestments, final boolean includeExpenses, final int monthCount)
throws SQLException, IOException {
ReportCategory unknownCategory = null;
final Map<Integer, ReportCategory> categories = new HashMap<>();
final var sql = Util.getResourceAsString("getCategories.sql").replace("${databaseName}",
"budget");
final List<ReportCategory> list = jdbcTemplate.query(sql,
(RowMapper<ReportCategory>) (rs, rowNum) -> {
final Integer id = rs.getInt(1);
Integer parentId = rs.getInt(2);
if (rs.wasNull()) {
parentId = null;
}
final var name = rs.getString(3);
final var category = new ReportCategory(id, parentId, name, monthCount);
return category;
});
for (final ReportCategory category : list) {
categories.put(category.getId(), category);
if ("unknown".equals(category.getName()) && category.getParentId() == null) {
unknownCategory = category;
}
}
for (final Integer categoryId : categories.keySet()) {
final var category = categories.get(categoryId);
category.updateParent(categories);
}
if (unknownCategory == null) {
unknownCategory = new ReportCategory(-1, null, "unknown", monthCount);
categories.put(unknownCategory.getId(), unknownCategory);
}
for (final Integer categoryId : categories.keySet()) {
final var category = categories.get(categoryId);
final CategoryFilter filter = category1 -> {
final String categoryName = category1.getName();
if ("income".equals(categoryName) || "refunds".equals(categoryName)) {
if (!includeIncome) {
return false;
}
}
else if ("payment".equals(categoryName)) {
if (!includePayments) {
return false;
}
}
else if ("investment".equals(categoryName)) {
if (!includeInvestments) {
return false;
}
}
else if (!includeExpenses) {
return false;
}
return true;
};
final boolean included = filter.include(category);
category.setIncluded(included);
}
return categories;
}
}

View file

@ -0,0 +1,80 @@
package com.stephenschafer.budget;
import java.math.BigDecimal;
import java.util.Date;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@ToString
class ReportDetail implements Comparable<ReportDetail> {
int transactionId;
String source;
String description;
Date date;
BigDecimal amount;
String regex;
int flags;
String requiredSource;
String extraDescription;
@Override
public int compareTo(final ReportDetail arg1) {
final var arg0 = this;
int comparison;
if (arg0.date == null) {
if (arg1.date != null) {
return -1;
}
}
else if (arg1.date == null) {
return 1;
}
else {
comparison = arg0.date.compareTo(arg1.date);
if (comparison != 0) {
return comparison;
}
}
if (arg0.source == null) {
if (arg1.source != null) {
return -1;
}
}
else if (arg1.source == null) {
return 1;
}
else {
comparison = arg0.source.compareTo(arg1.source);
if (comparison != 0) {
return comparison;
}
}
if (arg0.description == null) {
if (arg1.description != null) {
return -1;
}
}
else if (arg1.description == null) {
return 1;
}
else {
comparison = arg0.description.compareTo(arg1.description);
if (comparison != 0) {
return comparison;
}
}
return 0;
}
public boolean isAfter(final Date startDate) {
return startDate == null || startDate.getTime() <= this.date.getTime();
}
public boolean isBefore(final Date endDate) {
return endDate == null || endDate.getTime() > this.date.getTime();
}
}

View file

@ -0,0 +1,61 @@
package com.stephenschafer.budget;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@AllArgsConstructor
@ToString
class UnresolvedItem implements Comparable<UnresolvedItem> {
private final int year;
private final String source;
private final String description;
private final java.sql.Date date;
private final BigDecimal amount;
@Override
public int compareTo(final UnresolvedItem that) {
if (this.year < that.year) {
return -1;
}
if (this.year > that.year) {
return 1;
}
String thisSource = source;
if (thisSource == null) {
thisSource = "";
}
String thatSource = that.source;
if (thatSource == null) {
thatSource = "";
}
final int comparison = thisSource.compareTo(thatSource);
if (comparison != 0) {
return comparison;
}
String thisdescription = description;
if (thisdescription == null) {
thisdescription = "";
}
String thatdescription = that.description;
if (thatdescription == null) {
thatdescription = "";
}
return thisdescription.compareTo(thatdescription);
}
public String replace(final String string) {
final DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
return string.replace("${year}", String.valueOf(year)).replace("${source}", source).replace(
"${description}", description).replace("${date}", df.format(date)).replace("${amount}",
amount.setScale(2, RoundingMode.HALF_UP).toPlainString());
}
}

View file

@ -1,4 +1,22 @@
package com.stephenschafer.budget;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
public class Util {
public static String getResourceAsString(final String resourceName) throws IOException {
final ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
final StringBuilder sb = new StringBuilder();
try (final Reader reader = new InputStreamReader(
classLoader.getResourceAsStream(resourceName))) {
final char[] buffer = new char[0x1000];
int charsRead = reader.read(buffer);
while (charsRead >= 0) {
sb.append(buffer, 0, charsRead);
charsRead = reader.read(buffer);
}
}
return sb.toString();
}
}

View file

@ -0,0 +1,3 @@
select category_id, year_amount, jan_amount, feb_amount, mar_amount, apr_amount, may_amount, jun_amount,
jul_amount, aug_amount, sep_amount, oct_amount, nov_amount, dec_amount
from ${databaseName}.budget_amount

View file

@ -18,3 +18,6 @@ spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.Ph
#server.ssl.key-alias=timesheet
#server.ssl.key-password=foobar
#server.ssl.enabled=true
# this is necessary to see sql exceptions
logging.level.com.stephenschafer.budget=DEBUG

View file

@ -0,0 +1 @@
update ${databaseName}.transaction set extra_description = '', regex_id = null

View file

@ -0,0 +1,6 @@
create table ${databaseName}.category (
id int not null primary key auto_increment,
parent_category_id int,
name text not null,
constraint unique key (parent_category_id, name)
)

View file

@ -0,0 +1,16 @@
create table if not exists ${databaseName}.budget_amount (
category_id int not null primary key,
year_amount decimal(10,2),
jan_amount decimal(10,2),
feb_amount decimal(10,2),
mar_amount decimal(10,2),
apr_amount decimal(10,2),
may_amount decimal(10,2),
jun_amount decimal(10,2),
jul_amount decimal(10,2),
aug_amount decimal(10,2),
sep_amount decimal(10,2),
oct_amount decimal(10,2),
nov_amount decimal(10,2),
dec_amount decimal(10,2)
)

View file

@ -0,0 +1,10 @@
create table ${databaseName}.regex (
id int not null primary key auto_increment,
category_id int,
regex text not null,
flags int,
source varchar(32),
priority int,
description text,
year int
)

View file

@ -0,0 +1,5 @@
create table ${databaseName}.transaction_regex_mtm (
regex_id int not null,
transaction_id int not null,
primary key (transaction_id, regex_id)
)

View file

@ -0,0 +1,12 @@
create table ${databaseName}.transaction (
id int not null primary key auto_increment,
source varchar(32),
unique_identifier varchar(64),
type varchar(64),
description text,
extra_description text,
date date,
amount decimal(10,2),
optional int default 0,
regex_id int
)

View file

@ -0,0 +1,3 @@
create table ${databaseName}.years (
year int not null primary key
)

View file

@ -0,0 +1 @@
select table_rows from information_schema.tables where table_schema = ? and table_name = ?

View file

@ -0,0 +1,5 @@
select
id,
parent_category_id,
name
from ${databaseName}.category

View file

@ -0,0 +1,8 @@
select
transaction_date,
description,
category,
type,
amount
from ${databaseName}.chase

View file

@ -0,0 +1 @@
select id from ${databaseName}.category where name = ? and parent_category_id = ?

View file

@ -0,0 +1,7 @@
select
status,
date,
description,
debit,
credit
from ${databaseName}.citi

View file

@ -0,0 +1,15 @@
select
m.monetary_component_type_code as type,
i.${productNameCol} as name,
i.digital_order_item_id as order_id,
i.order_date as date,
sum(m.transaction_amount) as amount
from ${databaseName}.${digOrdItems} i
left outer join ${databaseName}.${digOrders} o on o.order_id = i.order_id
left outer join ${databaseName}.${digOrdersMonetary} m on m.digital_order_item_id = i.digital_order_item_id
group by
i.${productNameCol},
i.digital_order_item_id,
i.order_date,
m.monetary_component_type_code

View file

@ -0,0 +1,14 @@
select
m.monetary_component_type as type,
i.${productNameCol} as name,
i.digital_order_item_id as order_id,
i.order_date as date,
-sum(m.transaction_amount) as amount
from ${databaseName}.${digOrdItems} i
inner join ${databaseName}.${digOrdReturnsMonetary} m on m.digital_order_item_id = i.digital_order_item_id
group by
i.${productNameCol},
i.digital_order_item_id,
i.order_date,
m.monetary_component_type

View file

@ -0,0 +1,7 @@
select
trans_date,
post_date,
description,
amount,
category
from ${databaseName}.discover

View file

@ -0,0 +1,6 @@
select
date,
description,
type,
amount
from ${databaseName}.first_bank

View file

@ -0,0 +1,5 @@
select t.id, t.date, t.source, t.description, t.amount,
r.id, r.category_id, r.regex, r.flags, r.source, r.priority, r.description, r.year
from ${databaseName}.transaction t
inner join ${databaseName}.transaction_regex_mtm tr on tr.transaction_id = t.id
inner join budget.regex r on r.id = tr.regex_id

View file

@ -0,0 +1,13 @@
select
date,
time,
time_zone,
name,
type,
status,
currency,
amount,
receipt_id,
balance
from ${databaseName}.paypal

View file

@ -0,0 +1,10 @@
select
id,
category_id,
regex,
flags,
source,
priority,
description,
year
from ${databaseName}.regex

View file

@ -0,0 +1,8 @@
select
website as website,
product_name as name,
order_id as order_id,
date(order_date) as date,
total_owed as amount
from ${databaseName}.${retailOrders} o

View file

@ -0,0 +1 @@
select id from ${databaseName}.category where name = ? and parent_category_id is null

View file

@ -0,0 +1,3 @@
select t.id, t.date, t.source, t.description, t.amount
from ${databaseName}.transaction t
where t.regex_id is null

View file

@ -0,0 +1,3 @@
select t.id, t.date, t.source, t.description, t.amount, r.category_id, r.regex, r.flags, r.source, r.description
from ${databaseName}.transaction t
inner join ${regexDatabaseName}.regex r on r.id = t.regex_id

View file

@ -0,0 +1 @@
select regex_id from ${databaseName}.transaction_regex_mtm where transaction_id = ?

View file

@ -0,0 +1,3 @@
select
year
from ${databaseName}.years

View file

@ -0,0 +1 @@
insert into ${databaseName}.category (name, parent_category_id) values (?, ?)

View file

@ -0,0 +1,9 @@
insert into ${databaseName}.regex (
category_id,
regex,
flags,
source,
priority,
description,
year
) values (?, ?, ?, ?, ?, ?, ?)

View file

@ -0,0 +1 @@
insert into ${databaseName}.category (name) values (?)

View file

@ -0,0 +1,8 @@
insert into ${databaseName}.transaction (
source,
unique_identifier,
type,
description,
date,
amount
) values (?, ?, ?, ?, ?, ?)

View file

@ -0,0 +1 @@
insert into ${databaseName}.transaction_regex_mtm (regex_id, transaction_id) values (?, ?)

View file

@ -0,0 +1,3 @@
insert into ${databaseName}.years (
year
) values (?)

View file

@ -0,0 +1 @@
update ${databaseName}.transaction set extra_description = ? where id = ?

View file

@ -0,0 +1 @@
update ${databaseName}.transaction set regex_id = ? where id = ?