diff --git a/.factorypath b/.factorypath
index 7c47bf9..3c6a756 100644
--- a/.factorypath
+++ b/.factorypath
@@ -1,80 +1,107 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/build b/build
index 39d9343..6997a49 100755
--- a/build
+++ b/build
@@ -4,7 +4,7 @@ ROOT=$(pwd)
./stop
# export JAVA_HOME="/usr/lib/jvm/java-11-openjdk"
mkdir -p logs
-if ! mvn clean package > logs/build.log 2> logs/build.err.log; then
+if ! mvn clean install > logs/build.log 2> logs/build.err.log; then
echo "build failed"
exit 1
fi
diff --git a/eclipse b/eclipse
index 748a886..ce62fe9 100755
--- a/eclipse
+++ b/eclipse
@@ -2,5 +2,5 @@
cd "$(dirname "${BASH_SOURCE[0]}")"
LOG=./logs
mkdir -p $LOG
-/home/eclipse/sts-4.30.0.RELEASE/SpringToolSuite4 -data .. \
+/home/eclipse/sts-5.2.0.RELEASE/SpringToolsForEclipse -data .. \
>$LOG/eclipse-sts.log 2>$LOG/eclipse-sts.err.log &
diff --git a/pom.xml b/pom.xml
index 85f9d64..8646f2f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -12,7 +12,7 @@
org.springframework.boot
spring-boot-starter-parent
- 3.4.7
+ 4.1.0
@@ -62,8 +62,8 @@
0.9.0
- com.mysql
- mysql-connector-j
+ org.mariadb.jdbc
+ mariadb-java-client
runtime
@@ -88,7 +88,6 @@
- 17
@@ -97,6 +96,15 @@
org.springframework.boot
spring-boot-maven-plugin
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+ -proc:full
+
+
+
diff --git a/src/main/java/com/stephenschafer/budget/ReportCategory.java b/src/main/java/com/stephenschafer/budget/ReportCategory.java
index fc94919..67ab5cc 100644
--- a/src/main/java/com/stephenschafer/budget/ReportCategory.java
+++ b/src/main/java/com/stephenschafer/budget/ReportCategory.java
@@ -103,7 +103,9 @@ public class ReportCategory {
public BigDecimal getDetailTotal() {
var amount = new BigDecimal(0);
for (final ReportDetail detail : details) {
- amount = amount.add(detail.amount);
+ if (!detail.hidden) {
+ amount = amount.add(detail.amount);
+ }
}
return amount;
}
@@ -111,10 +113,12 @@ public class ReportCategory {
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);
+ if (!detail.hidden) {
+ final Calendar cal = new GregorianCalendar();
+ cal.setTime(detail.date);
+ if (month == cal.get(Calendar.MONTH)) {
+ amount = amount.add(detail.amount);
+ }
}
}
return amount;
@@ -125,14 +129,16 @@ public class ReportCategory {
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);
+ if (!detail.hidden) {
+ 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));
}
- monthTotals.put(month, monthTotal.add(detail.amount));
}
this.monthTotals = monthTotals;
}
diff --git a/src/main/java/com/stephenschafer/budget/ReportCategoryFilters.java b/src/main/java/com/stephenschafer/budget/ReportCategoryFilters.java
new file mode 100644
index 0000000..4460a19
--- /dev/null
+++ b/src/main/java/com/stephenschafer/budget/ReportCategoryFilters.java
@@ -0,0 +1,18 @@
+package com.stephenschafer.budget;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@Getter
+@AllArgsConstructor
+public class ReportCategoryFilters {
+ final String year;
+ final boolean includeIncome;
+ final boolean includePayments;
+ final boolean includeInvestments;
+ final boolean includeExpenses;
+ final boolean excludePaypalPayments;
+ final boolean excludeWholefoodsPayments;
+ final boolean excludeAmazonPayments;
+ final int monthCount;
+}
diff --git a/src/main/java/com/stephenschafer/budget/ReportController.java b/src/main/java/com/stephenschafer/budget/ReportController.java
index 082fe2f..c05657d 100644
--- a/src/main/java/com/stephenschafer/budget/ReportController.java
+++ b/src/main/java/com/stephenschafer/budget/ReportController.java
@@ -95,6 +95,7 @@ public class ReportController {
@RequestParam(required = false, name = "startDate") final String startDateString,
@RequestParam(required = false, name = "endDate") final String endDateString,
@RequestParam(required = false) final List includes,
+ @RequestParam(required = false) final List excludes,
final HttpServletRequest request)
throws SQLException, UnsupportedEncodingException, FileNotFoundException, IOException {
log.info("GET /report");
@@ -127,18 +128,45 @@ public class ReportController {
else if ("investments".equalsIgnoreCase(include)) {
tmpIncludeInvestments = true;
}
+ else {
+ return new ApiResponse<>(HttpStatus.BAD_REQUEST.value(),
+ "Unrecognized includes value", null);
+ }
}
}
final var includeExpenses = tmpIncludeExpenses;
final var includeIncome = tmpIncludeIncome;
final var includePayments = tmpIncludePayments;
final var includeInvestments = tmpIncludeInvestments;
+ var tmpExcludePaypalPayments = false;
+ var tmpExcludeWholefoodsPayments = false;
+ var tmpExcludeAmazonPayments = false;
+ if (excludes != null && !excludes.isEmpty()) {
+ for (final String exclude : excludes) {
+ if ("paypalPayments".equalsIgnoreCase(exclude)) {
+ tmpExcludePaypalPayments = true;
+ }
+ else if ("wholefoodsPayments".equalsIgnoreCase(exclude)) {
+ tmpExcludeWholefoodsPayments = true;
+ }
+ else if ("amazonPayments".equalsIgnoreCase(exclude)) {
+ tmpExcludeAmazonPayments = true;
+ }
+ else {
+ return new ApiResponse<>(HttpStatus.BAD_REQUEST.value(),
+ "Unrecognized excludes value", null);
+ }
+ }
+ }
+ final var excludePaypalPayments = tmpExcludePaypalPayments;
+ final var excludeWholefoodsPayments = tmpExcludeWholefoodsPayments;
+ final var excludeAmazonPayments = tmpExcludeAmazonPayments;
final DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
final Date startDate;
- final Date endDate;
+ Date tmpEndDate;
try {
startDate = startDateString == null ? null : df.parse(startDateString);
- endDate = endDateString == null ? null : df.parse(endDateString);
+ tmpEndDate = endDateString == null ? null : df.parse(endDateString);
}
catch (final ParseException e) {
return new ApiResponse<>(HttpStatus.BAD_REQUEST.value(), "Invalid date", null);
@@ -153,19 +181,25 @@ public class ReportController {
startMonth = 0;
}
final int endMonth;
- if (endDate != null) {
+ final Date endDate;
+ if (tmpEndDate != null) {
final Calendar endCal = new GregorianCalendar();
- endCal.setTime(endDate);
+ endCal.setTime(tmpEndDate);
endMonth = endCal.get(Calendar.MONTH);
+ endCal.add(Calendar.DAY_OF_MONTH, 1);
+ endDate = endCal.getTime();
}
else {
endMonth = 11;
+ endDate = null;
}
final int monthCount = endMonth - startMonth + 1;
final Map response = new HashMap<>();
for (final String year : years) {
- final var categoryMap = loadCategories(year, includeIncome, includePayments,
- includeInvestments, includeExpenses, monthCount);
+ final var filters = new ReportCategoryFilters(year, includeIncome, includePayments,
+ includeInvestments, includeExpenses, excludePaypalPayments,
+ excludeWholefoodsPayments, excludeAmazonPayments, monthCount);
+ final var categoryMap = loadCategories(filters);
final String createBudgetTableSql = Util.getResourceAsString(
"createBudgetAmountsTable.sql") //
.replace("${ifNotExists}", "if not exists ") //
@@ -188,6 +222,7 @@ public class ReportController {
}
}
});
+ final Map> detailMap = new HashMap<>();
final String sql = Util.getResourceAsString("getTransactionDetail.sql") //
.replace("${databaseName}", "budget_" + year) //
.replace("${regexDatabaseName}", "budget");
@@ -214,7 +249,14 @@ public class ReportController {
category.isIncluded()) {
category.addDetail(detail);
}
+ List detailList = detailMap.get(detail.source);
+ if (detailList == null) {
+ detailList = new ArrayList<>();
+ detailMap.put(detail.source, detailList);
+ }
+ detailList.add(detail);
});
+ hidePaymentDetails(detailMap);
final var rootCategory = new ReportCategory(-1, null, "total", monthCount);
for (final Integer categoryId : categoryMap.keySet()) {
final var category = categoryMap.get(categoryId);
@@ -229,6 +271,41 @@ public class ReportController {
return new ApiResponse<>(HttpStatus.OK.value(), "Report fetched successfully", response);
}
+ static final String[] payerSources = new String[] { "chase", "firstbank" };
+ static final String[] payeeSources = new String[] { "paypal", "amz.sandy.dig.org",
+ "amz.steve.dig.ord", "amz.sandy.retail.ord", "amz.steve.retail.ord" };
+
+ private void hidePaymentDetails(final Map> detailMap) {
+ final List payerDetails = new ArrayList<>();
+ for (final String source : payerSources) {
+ final List details = detailMap.get(source);
+ if (details != null) {
+ for (final ReportDetail detail : details) {
+ payerDetails.add(detail);
+ }
+ }
+ }
+ final List payeeDetails = new ArrayList<>();
+ for (final String source : payeeSources) {
+ final List details = detailMap.get(source);
+ if (details != null) {
+ for (final ReportDetail detail : details) {
+ payeeDetails.add(detail);
+ }
+ }
+ }
+ for (final ReportDetail payeeDetail : payeeDetails) {
+ for (final ReportDetail payerDetail : payerDetails) {
+ if (payeeDetail.isPaidBy(payerDetail)) {
+ payerDetail.setHidden(true);
+ payeeDetail.setPaidByTransactionId(payerDetail.transactionId);
+ payerDetail.setPaidTransactionId(payeeDetail.transactionId);
+ break;
+ }
+ }
+ }
+ }
+
List getYears() throws IOException, SQLException {
final String sql = Util.getResourceAsString("getYears.sql") //
.replace("${databaseName}", "budget");
@@ -244,9 +321,7 @@ public class ReportController {
boolean include(ReportCategory category);
}
- private Map loadCategories(final String year,
- final boolean includeIncome, final boolean includePayments,
- final boolean includeInvestments, final boolean includeExpenses, final int monthCount)
+ private Map loadCategories(final ReportCategoryFilters filters)
throws SQLException, IOException {
ReportCategory unknownCategory = null;
final Map categories = new HashMap<>();
@@ -260,7 +335,7 @@ public class ReportController {
parentId = null;
}
final var name = rs.getString(3);
- final var category = new ReportCategory(id, parentId, name, monthCount);
+ final var category = new ReportCategory(id, parentId, name, filters.monthCount);
return category;
});
for (final ReportCategory category : list) {
@@ -274,29 +349,48 @@ public class ReportController {
category.updateParent(categories);
}
if (unknownCategory == null) {
- unknownCategory = new ReportCategory(-1, null, "unknown", monthCount);
+ unknownCategory = new ReportCategory(-1, null, "unknown", filters.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();
+ final ReportCategory parentCategory = category1.getParent();
+ final String parentCategoryName = parentCategory == null ? null
+ : parentCategory.getName();
if ("income".equals(categoryName) || "refunds".equals(categoryName)) {
- if (!includeIncome) {
+ if (!filters.includeIncome) {
return false;
}
}
else if ("payment".equals(categoryName)) {
- if (!includePayments) {
+ if (!filters.includePayments) {
return false;
}
}
else if ("investment".equals(categoryName)) {
- if (!includeInvestments) {
+ if (!filters.includeInvestments) {
return false;
}
}
- else if (!includeExpenses) {
+ else if ("paypal".equals(categoryName) && "payment".equals(parentCategoryName)) {
+ if (filters.excludePaypalPayments) {
+ return false;
+ }
+ }
+ else if ("wholefoods".equals(categoryName)
+ && "payment".equals(parentCategoryName)) {
+ if (filters.excludeWholefoodsPayments) {
+ return false;
+ }
+ }
+ else if ("amazon".equals(categoryName) && "payment".equals(parentCategoryName)) {
+ if (filters.excludeAmazonPayments) {
+ return false;
+ }
+ }
+ else if (!filters.includeExpenses) {
return false;
}
return true;
diff --git a/src/main/java/com/stephenschafer/budget/ReportDetail.java b/src/main/java/com/stephenschafer/budget/ReportDetail.java
index eb6a8c4..f9a669c 100644
--- a/src/main/java/com/stephenschafer/budget/ReportDetail.java
+++ b/src/main/java/com/stephenschafer/budget/ReportDetail.java
@@ -20,6 +20,9 @@ class ReportDetail implements Comparable {
int flags;
String requiredSource;
String extraDescription;
+ boolean hidden = false;
+ Integer paidByTransactionId = null;
+ Integer paidTransactionId = null;
@Override
public int compareTo(final ReportDetail arg1) {
@@ -77,4 +80,32 @@ class ReportDetail implements Comparable {
public boolean isBefore(final Date endDate) {
return endDate == null || endDate.getTime() > this.date.getTime();
}
+
+ public boolean isPaidBy(final ReportDetail payingDetail) {
+ if (payingDetail.description == null) {
+ return false;
+ }
+ if (payingDetail.description.toLowerCase().indexOf(this.source.toLowerCase()) < 0) {
+ return false;
+ }
+ if (payingDetail.date == null) {
+ return false;
+ }
+ if (this.date == null) {
+ return false;
+ }
+ if (payingDetail.date.getTime() < this.date.getTime()) {
+ return false;
+ }
+ if (payingDetail.amount == null) {
+ return false;
+ }
+ if (this.amount == null) {
+ return false;
+ }
+ if (!payingDetail.amount.equals(this.amount)) {
+ return false;
+ }
+ return true;
+ }
}
\ No newline at end of file
diff --git a/src/main/java/com/stephenschafer/budget/WebSecurityConfig.java b/src/main/java/com/stephenschafer/budget/WebSecurityConfig.java
index dc7cada..0d48953 100644
--- a/src/main/java/com/stephenschafer/budget/WebSecurityConfig.java
+++ b/src/main/java/com/stephenschafer/budget/WebSecurityConfig.java
@@ -27,8 +27,7 @@ public class WebSecurityConfig {
@Bean
AuthenticationManager authenticationManager(final UserDetailsService userDetailsService,
final PasswordEncoder passwordEncoder) {
- final var provider = new DaoAuthenticationProvider();
- provider.setUserDetailsService(userDetailsService);
+ final var provider = new DaoAuthenticationProvider(userDetailsService);
provider.setPasswordEncoder(passwordEncoder);
return new ProviderManager(provider);
}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 21c58ec..f08a0f3 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -1,4 +1,5 @@
-spring.datasource.driver-class-name=com.mysql.jdbc.Driver
+spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
+#com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306?serverTimezone=UTC&useSSL=false
spring.datasource.username=elephant
# spring.datasource.password=CHANGEME