Skip to content
Snippets Groups Projects
Commit 9df0b910 authored by Robert Goldmann's avatar Robert Goldmann
Browse files

Fixed #526 - duplicated recurring transactions after old database is migrated to BudgetMaster 2.4.0

parent a0da964c
Branches
Tags
No related merge requests found
Pipeline #3576 passed
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
<groupId>de.deadlocker8</groupId> <groupId>de.deadlocker8</groupId>
<artifactId>BudgetMaster</artifactId> <artifactId>BudgetMaster</artifactId>
<version>2.4.0</version> <version>2.4.1</version>
<name>BudgetMaster</name> <name>BudgetMaster</name>
<repositories> <repositories>
...@@ -70,7 +70,7 @@ ...@@ -70,7 +70,7 @@
<app.versionDate>${maven.build.timestamp}</app.versionDate> <app.versionDate>${maven.build.timestamp}</app.versionDate>
<maven.build.timestamp.format>dd.MM.yy</maven.build.timestamp.format> <maven.build.timestamp.format>dd.MM.yy</maven.build.timestamp.format>
<app.versionCode>23</app.versionCode> <app.versionCode>24</app.versionCode>
<app.author>Robert Goldmann</app.author> <app.author>Robert Goldmann</app.author>
<project.outputDirectory>build/${project.version}</project.outputDirectory> <project.outputDirectory>build/${project.version}</project.outputDirectory>
......
...@@ -28,6 +28,7 @@ public class Settings ...@@ -28,6 +28,7 @@ public class Settings
private Integer autoBackupDays; private Integer autoBackupDays;
private AutoBackupTime autoBackupTime; private AutoBackupTime autoBackupTime;
private Integer autoBackupFilesToKeep; private Integer autoBackupFilesToKeep;
private Integer installedVersionCode;
public Settings() public Settings()
{ {
...@@ -48,6 +49,7 @@ public class Settings ...@@ -48,6 +49,7 @@ public class Settings
defaultSettings.setAutoBackupDays(1); defaultSettings.setAutoBackupDays(1);
defaultSettings.setAutoBackupTime(AutoBackupTime.TIME_00); defaultSettings.setAutoBackupTime(AutoBackupTime.TIME_00);
defaultSettings.setAutoBackupFilesToKeep(3); defaultSettings.setAutoBackupFilesToKeep(3);
defaultSettings.setInstalledVersionCode(0);
return defaultSettings; return defaultSettings;
} }
...@@ -186,6 +188,16 @@ public class Settings ...@@ -186,6 +188,16 @@ public class Settings
this.autoBackupFilesToKeep = autoBackupFilesToKeep; this.autoBackupFilesToKeep = autoBackupFilesToKeep;
} }
public Integer getInstalledVersionCode()
{
return installedVersionCode;
}
public void setInstalledVersionCode(Integer installedVersionCode)
{
this.installedVersionCode = installedVersionCode;
}
@Override @Override
public String toString() public String toString()
{ {
...@@ -203,6 +215,7 @@ public class Settings ...@@ -203,6 +215,7 @@ public class Settings
", autoBackupDays=" + autoBackupDays + ", autoBackupDays=" + autoBackupDays +
", autoBackupTime=" + autoBackupTime + ", autoBackupTime=" + autoBackupTime +
", autoBackupFilesToKeep=" + autoBackupFilesToKeep + ", autoBackupFilesToKeep=" + autoBackupFilesToKeep +
", installedVersionCode=" + installedVersionCode +
'}'; '}';
} }
} }
\ No newline at end of file
...@@ -77,6 +77,10 @@ public class SettingsService ...@@ -77,6 +77,10 @@ public class SettingsService
{ {
settings.setAutoBackupFilesToKeep(defaultSettings.getAutoBackupFilesToKeep()); settings.setAutoBackupFilesToKeep(defaultSettings.getAutoBackupFilesToKeep());
} }
if(settings.getInstalledVersionCode() == null)
{
settings.setInstalledVersionCode(defaultSettings.getInstalledVersionCode());
}
} }
@SuppressWarnings("OptionalGetWithoutIsPresent") @SuppressWarnings("OptionalGetWithoutIsPresent")
......
...@@ -11,6 +11,7 @@ import org.springframework.data.jpa.repository.Query; ...@@ -11,6 +11,7 @@ import org.springframework.data.jpa.repository.Query;
import java.util.List; import java.util.List;
@SuppressWarnings("SqlResolve")
public interface TransactionRepository extends JpaRepository<Transaction, Integer>, JpaSpecificationExecutor<Transaction> public interface TransactionRepository extends JpaRepository<Transaction, Integer>, JpaSpecificationExecutor<Transaction>
{ {
List<Transaction> findAllByAccountAndDateBetweenOrderByDateDesc(Account account, DateTime startDate, DateTime endDate); List<Transaction> findAllByAccountAndDateBetweenOrderByDateDesc(Account account, DateTime startDate, DateTime endDate);
......
package de.deadlocker8.budgetmaster.utils.eventlistener;
import de.deadlocker8.budgetmaster.repeating.RepeatingOption;
import de.deadlocker8.budgetmaster.repeating.RepeatingOptionRepository;
import de.deadlocker8.budgetmaster.repeating.RepeatingTransactionUpdater;
import de.deadlocker8.budgetmaster.settings.SettingsService;
import de.deadlocker8.budgetmaster.transactions.Transaction;
import de.deadlocker8.budgetmaster.transactions.TransactionRepository;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.EntityManager;
import javax.persistence.Query;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
@Component
public class DateRepair
{
private static final Logger LOGGER = LoggerFactory.getLogger(DateRepair.class);
private static final int DATE_REPAIR_VERSION_CODE = 23;
private final TransactionRepository transactionRepository;
private final RepeatingOptionRepository repeatingOptionRepository;
private final RepeatingTransactionUpdater repeatingTransactionUpdater;
private final SettingsService settingsService;
private final EntityManager entityManager;
@Autowired
public DateRepair(TransactionRepository transactionRepository, RepeatingOptionRepository repeatingOptionRepository, RepeatingTransactionUpdater repeatingTransactionUpdater, SettingsService settingsService, EntityManager entityManager)
{
this.transactionRepository = transactionRepository;
this.repeatingOptionRepository = repeatingOptionRepository;
this.repeatingTransactionUpdater = repeatingTransactionUpdater;
this.settingsService = settingsService;
this.entityManager = entityManager;
}
@EventListener
@Transactional
@Order(1)
public void onApplicationEvent(ApplicationStartedEvent event)
{
checkForPossibleDuplicates();
if(settingsService.getSettings().getInstalledVersionCode() <= DATE_REPAIR_VERSION_CODE)
{
deleteOldRepeatingTransactions();
repairTransactionDates();
repairRepeatingOptionsDates();
LOGGER.debug("Re-created repeating transactions");
repeatingTransactionUpdater.updateRepeatingTransactions(DateTime.now());
}
}
private void deleteOldRepeatingTransactions()
{
int numberOfDeletedItems = 0;
for(RepeatingOption repeatingOption : repeatingOptionRepository.findAll())
{
final List<Transaction> referringTransactions = repeatingOption.getReferringTransactions();
final List<Transaction> transactionsSorted = referringTransactions.stream()
.sorted(Comparator.comparing(Transaction::getDate))
.collect(Collectors.toList());
final List<Transaction> transactions = transactionsSorted.subList(1, transactionsSorted.size());
numberOfDeletedItems += transactions.size();
for(Transaction transaction : transactions)
{
final Query nativeQuery = entityManager.createNativeQuery("DELETE FROM `transaction` WHERE id=:ID");
nativeQuery.setParameter("ID", transaction.getID());
nativeQuery.executeUpdate();
}
}
LOGGER.debug(MessageFormat.format("Deleted {0} wrong repeating transactions", numberOfDeletedItems));
}
private void checkForPossibleDuplicates()
{
final List<Transaction> transactions = transactionRepository.findAll();
final List<Transaction> alreadyScanned = new ArrayList<>();
final List<List<Transaction>> duplicated = new ArrayList<>();
for(Transaction transaction : transactions)
{
for(Transaction scannedTransaction : alreadyScanned)
{
if(isPossibleDuplicate(transaction, scannedTransaction))
{
final ArrayList<Transaction> entry = new ArrayList<>();
entry.add(scannedTransaction);
entry.add(transaction);
duplicated.add(entry);
}
}
alreadyScanned.add(transaction);
}
if(!duplicated.isEmpty())
{
final List<List<Integer>> duplicatedIDs = duplicated.stream()
.map(entry -> entry.stream().map(Transaction::getID).collect(Collectors.toList()))
.collect(Collectors.toList());
LOGGER.warn(MessageFormat.format("Found {0} possible duplicated transactions: {1}", duplicated.size(), duplicatedIDs));
}
}
private boolean isPossibleDuplicate(Transaction transaction1, Transaction transaction2)
{
if(transaction1.getRepeatingOption() == null || transaction2.getRepeatingOption() == null)
{
return false;
}
if(!transaction1.getRepeatingOption().equals(transaction2.getRepeatingOption()))
{
return false;
}
return areDatesSimilar(transaction1.getDate(), transaction2.getDate());
}
private boolean areDatesSimilar(DateTime date1, DateTime date2)
{
if(date1.getYear() != date2.getYear())
{
return false;
}
if(date1.getMonthOfYear() != date2.getMonthOfYear())
{
return false;
}
return Math.abs(date1.getDayOfMonth() - date2.getDayOfMonth()) <= 1;
}
private void repairTransactionDates()
{
int numberOfRepairedTransactions = 0;
final List<Transaction> transactions = transactionRepository.findAll();
for(Transaction transaction : transactions)
{
final DateTime date = transaction.getDate();
if(dateNeedsToBeRepaired(date))
{
final DateTime fixedDate = getRepairedDate(date);
transaction.setDate(fixedDate);
numberOfRepairedTransactions++;
}
}
LOGGER.debug(MessageFormat.format("Repaired {0}/{1} transaction dates", numberOfRepairedTransactions, transactions.size()));
}
private void repairRepeatingOptionsDates()
{
int numberOfRepairedItems = 0;
final List<RepeatingOption> repeatingOptions = repeatingOptionRepository.findAll();
for(RepeatingOption repeatingOption : repeatingOptions)
{
final DateTime date = repeatingOption.getStartDate();
if(dateNeedsToBeRepaired(date))
{
final DateTime fixedDate = getRepairedDate(date);
repeatingOption.setStartDate(fixedDate);
numberOfRepairedItems++;
}
}
LOGGER.debug(MessageFormat.format("Repaired {0}/{1} repeating option dates", numberOfRepairedItems, repeatingOptions.size()));
}
private DateTime getRepairedDate(DateTime date)
{
return date.plusHours(6)
.withZone(DateTimeZone.UTC)
.withHourOfDay(0)
.withMinuteOfHour(0)
.withSecondOfMinute(0)
.withMillisOfSecond(0);
}
private boolean dateNeedsToBeRepaired(DateTime date)
{
if(date.getHourOfDay() != 0)
{
return true;
}
if(date.getMinuteOfHour() != 0)
{
return true;
}
if(date.getSecondOfMinute() != 0)
{
return true;
}
return date.getMillisOfSecond() != 0;
}
}
package de.deadlocker8.budgetmaster.utils.eventlistener;
import de.deadlocker8.budgetmaster.Build;
import de.deadlocker8.budgetmaster.settings.SettingsService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.text.MessageFormat;
@Component
public class UpdateInstalledVersion
{
private static final Logger LOGGER = LoggerFactory.getLogger(UpdateInstalledVersion.class);
private final SettingsService settingsService;
@Autowired
public UpdateInstalledVersion(SettingsService settingsService)
{
this.settingsService = settingsService;
}
@EventListener
@Transactional
@Order(2)
public void onApplicationEvent(ApplicationStartedEvent event)
{
final Build build = Build.getInstance();
final int runningVersionCode = Integer.parseInt(build.getVersionCode());
LOGGER.debug(MessageFormat.format("Updated installedVersionCode to {0}", runningVersionCode));
settingsService.getSettings().setInstalledVersionCode(runningVersionCode);
}
}
\ No newline at end of file
...@@ -24,6 +24,7 @@ ...@@ -24,6 +24,7 @@
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/> <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
<input type="hidden" name="ID" value="${settings.getID()?c}"> <input type="hidden" name="ID" value="${settings.getID()?c}">
<input type="hidden" name="lastBackupReminderDate" value="${dateService.getLongDateString(settings.getLastBackupReminderDate())}"> <input type="hidden" name="lastBackupReminderDate" value="${dateService.getLongDateString(settings.getLastBackupReminderDate())}">
<input type="hidden" name="installedVersionCode" value="${settings.getInstalledVersionCode()}">
<#-- password --> <#-- password -->
<div class="row"> <div class="row">
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment