logic : reports #3
|
|
@ -0,0 +1,50 @@
|
||||||
|
package org.octopus.internal.common
|
||||||
|
|
||||||
|
import org.octopus.internal.common.models.Entry
|
||||||
|
import java.time.YearMonth
|
||||||
|
import java.time.ZoneId
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
|
fun YearMonth.toFirstDayOfMonthDate(zoneId: ZoneId): Date {
|
||||||
|
return Date.from(this.atDay(1).atStartOfDay(zoneId).toInstant())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Date?.isInTargetMonth(yearMonth: YearMonth, zoneId: ZoneId): Boolean {
|
||||||
|
if (this == null) return false
|
||||||
|
return this.toYearMonth(zoneId).isSame(yearMonth)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Date.toYearMonth(zoneId: ZoneId = ZoneId.systemDefault()): YearMonth {
|
||||||
|
return YearMonth.from(this.toInstant().atZone(zoneId))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun YearMonth.isSame(other: YearMonth): Boolean {
|
||||||
|
return this.year == other.year && this.month == other.month
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Date.isSameMonth(other: Date): Boolean {
|
||||||
|
val instant1 = this.toInstant()
|
||||||
|
val instant2 = other.toInstant()
|
||||||
|
|
||||||
|
val zoneId = ZoneId.systemDefault()
|
||||||
|
|
||||||
|
val yearMonth1 = YearMonth.from(instant1.atZone(zoneId))
|
||||||
|
val yearMonth2 = YearMonth.from(instant2.atZone(zoneId))
|
||||||
|
|
||||||
|
return yearMonth1.isSame(yearMonth2)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Entry.isRecurrentActive(yearMonth: YearMonth, zoneId: ZoneId): Boolean {
|
||||||
|
if (this.recurrentMonths?.contains(yearMonth.month) != true) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
val startYearMonth = this.startDate?.toYearMonth(zoneId)
|
||||||
|
val endYearMonth = this.endDate?.toYearMonth(zoneId)
|
||||||
|
|
||||||
|
val afterStart = startYearMonth == null || !yearMonth.isBefore(startYearMonth)
|
||||||
|
|
||||||
|
val beforeEnd = endYearMonth == null || !yearMonth.isAfter(endYearMonth)
|
||||||
|
|
||||||
|
return afterStart && beforeEnd
|
||||||
|
}
|
||||||
|
|
@ -2,5 +2,9 @@ package org.octopus.internal.common.enums
|
||||||
|
|
||||||
enum class EBusinessException(val msg: String) {
|
enum class EBusinessException(val msg: String) {
|
||||||
ENTITY_WITH_ID_NOT_FOUND("%s with id %s not found"),
|
ENTITY_WITH_ID_NOT_FOUND("%s with id %s not found"),
|
||||||
INVALID_REQUEST("Invalid request for %s with reason: %s")
|
INVALID_REQUEST("Invalid request for %s with reason: %s"),
|
||||||
|
STARTING_REPORT_ALREADY_EXISTS("Starting report already exists: %s"),
|
||||||
|
STARTING_REPORT_DOESNT_EXIST("Starting report doesn't exist."),
|
||||||
|
REPORT_END_DATE_LE_START_REPORT("End date is before or the same as the starting month"),
|
||||||
|
REPORT_IS_START_NOT_CLEAR("Report start and attribute start are mismatching.")
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
package org.octopus.internal.common.mappers
|
||||||
|
|
||||||
|
import org.mapstruct.Mapper
|
||||||
|
import org.mapstruct.Mapping
|
||||||
|
import org.octopus.internal.common.models.Entry
|
||||||
|
import org.octopus.internal.common.models.Report
|
||||||
|
import org.octopus.internal.db.entities.EntryEntity
|
||||||
|
import org.octopus.internal.db.entities.ReportEntity
|
||||||
|
|
||||||
|
@Mapper(componentModel = "spring")
|
||||||
|
interface ReportMapper : GenericMapper<Report, ReportEntity>
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
package org.octopus.internal.common.models
|
||||||
|
|
||||||
|
import java.time.YearMonth
|
||||||
|
|
||||||
|
data class DetailedReport (
|
||||||
|
val yearMonth: YearMonth,
|
||||||
|
val report: Report,
|
||||||
|
val entries: List<Entry>
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
package org.octopus.internal.common.models
|
||||||
|
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
|
data class Report(
|
||||||
|
var id: Long,
|
||||||
|
val start: Boolean,
|
||||||
|
val reportMonth: Date,
|
||||||
|
val totalIncomes: Double,
|
||||||
|
val totalOutcomes: Double,
|
||||||
|
val expectedDelta: Double,
|
||||||
|
val expectedCount: Double,
|
||||||
|
val corrective: Double,
|
||||||
|
val realCount: Double
|
||||||
|
)
|
||||||
|
|
@ -8,7 +8,7 @@ import java.util.Date
|
||||||
|
|
||||||
interface EntryJpa : JpaRepository<EntryEntity, Long> {
|
interface EntryJpa : JpaRepository<EntryEntity, Long> {
|
||||||
fun findAllByType(type: EEntryType): MutableList<EntryEntity>
|
fun findAllByType(type: EEntryType): MutableList<EntryEntity>
|
||||||
fun findAllByFixedDateIsNullAndRecurrentIsTrueAndEndDateIsNotNullAndEndDateBefore(endDate: Date): MutableList<EntryEntity>
|
fun findAllByFixedDateIsNullAndRecurrentIsTrueAndEndDateIsNotNullAndEndDateLessThanEqual(endDate: Date): MutableList<EntryEntity>
|
||||||
fun findAllByFixedDateIsNullAndRecurrentIsTrueAndRecurrentMonthsIn(months: List<Month>): MutableList<EntryEntity>
|
fun findAllByFixedDateIsNullAndRecurrentIsTrueAndRecurrentMonthsIn(months: List<Month>): MutableList<EntryEntity>
|
||||||
fun findAllByFixedDateIsNotNullAndFixedDateBefore(fixedDate: Date): MutableList<EntryEntity>
|
fun findAllByFixedDateIsNotNullAndFixedDateLessThanEqual(fixedDate: Date): MutableList<EntryEntity>
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
package org.octopus.internal.db
|
||||||
|
|
||||||
|
import org.octopus.internal.db.entities.ReportEntity
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
|
interface ReportJpa : JpaRepository<ReportEntity, Long> {
|
||||||
|
fun findFirstByStartIsTrue(): ReportEntity?
|
||||||
|
fun findByStartIsFalse(): MutableList<ReportEntity>
|
||||||
|
fun findByStartIsFalseAndReportMonthLessThanEqual(date: Date): MutableList<ReportEntity>
|
||||||
|
fun deleteByReportMonthLessThanEqual(date: Date): Long
|
||||||
|
fun deleteByReportMonthGreaterThan(date: Date): Long
|
||||||
|
fun deleteByReportMonthGreaterThanEqual(date: Date): Long
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
package org.octopus.internal.db.entities
|
||||||
|
|
||||||
|
import jakarta.persistence.Column
|
||||||
|
import jakarta.persistence.Entity
|
||||||
|
import jakarta.persistence.GeneratedValue
|
||||||
|
import jakarta.persistence.GenerationType
|
||||||
|
import jakarta.persistence.Id
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
|
@Entity(name = "reports")
|
||||||
|
data class ReportEntity(
|
||||||
|
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
val id: Long = 0L,
|
||||||
|
val start: Boolean,
|
||||||
|
@Column(unique = true)
|
||||||
|
val reportMonth: Date,
|
||||||
|
val totalIncomes: Double,
|
||||||
|
val totalOutcomes: Double,
|
||||||
|
val expectedDelta: Double,
|
||||||
|
val expectedCount: Double,
|
||||||
|
val corrective: Double,
|
||||||
|
val realCount: Double
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
package org.octopus.internal.repositories
|
||||||
|
|
||||||
|
import org.octopus.internal.common.enums.EEntryType
|
||||||
|
import org.octopus.internal.common.models.Entry
|
||||||
|
import org.octopus.internal.common.models.Report
|
||||||
|
import java.time.Month
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
|
interface ReportRepository {
|
||||||
|
fun getById(id: Long): Report
|
||||||
|
fun createReport(report: Report): Report
|
||||||
|
fun createReports(reports: MutableList<Report>): MutableList<Report>
|
||||||
|
fun getStartingReport(): Report?
|
||||||
|
fun getNonStartingReports(endDate: Date?): MutableList<Report>
|
||||||
|
fun deleteAllReportsBefore(endDate: Date): Long
|
||||||
|
fun deleteAllReportsAfter(endDate: Date): Long
|
||||||
|
fun deleteAllReportsAfterOrAtDate(endDate: Date): Long
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
package org.octopus.internal.repositories.impl
|
package org.octopus.internal.repositories.impl
|
||||||
|
|
||||||
import org.hibernate.type.descriptor.DateTimeUtils
|
|
||||||
import org.octopus.internal.common.enums.EBusinessException
|
import org.octopus.internal.common.enums.EBusinessException
|
||||||
import org.octopus.internal.common.enums.EEntryType
|
import org.octopus.internal.common.enums.EEntryType
|
||||||
import org.octopus.internal.common.exceptions.OctopusPlanningException
|
import org.octopus.internal.common.exceptions.OctopusPlanningException
|
||||||
|
|
@ -14,8 +13,8 @@ import java.util.*
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
class EntryRepositoryImpl(
|
class EntryRepositoryImpl(
|
||||||
protected val jpa: EntryJpa,
|
private val jpa: EntryJpa,
|
||||||
protected val mapper: EntryMapper
|
private val mapper: EntryMapper
|
||||||
) : EntryRepository {
|
) : EntryRepository {
|
||||||
|
|
||||||
override fun getById(id: Long): Entry {
|
override fun getById(id: Long): Entry {
|
||||||
|
|
@ -50,7 +49,7 @@ class EntryRepositoryImpl(
|
||||||
|
|
||||||
override fun getAllRecurrentMonthlyToEndDate(endDate: Date): MutableList<Entry> {
|
override fun getAllRecurrentMonthlyToEndDate(endDate: Date): MutableList<Entry> {
|
||||||
return mapper.toModels(
|
return mapper.toModels(
|
||||||
jpa.findAllByFixedDateIsNullAndRecurrentIsTrueAndEndDateIsNotNullAndEndDateBefore(endDate)
|
jpa.findAllByFixedDateIsNullAndRecurrentIsTrueAndEndDateIsNotNullAndEndDateLessThanEqual(endDate)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -68,7 +67,7 @@ class EntryRepositoryImpl(
|
||||||
|
|
||||||
override fun getAllFixedUpToEndDate(endDate: Date): MutableList<Entry> {
|
override fun getAllFixedUpToEndDate(endDate: Date): MutableList<Entry> {
|
||||||
return mapper.toModels(
|
return mapper.toModels(
|
||||||
jpa.findAllByFixedDateIsNotNullAndFixedDateBefore(endDate)
|
jpa.findAllByFixedDateIsNotNullAndFixedDateLessThanEqual(endDate)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
package org.octopus.internal.repositories.impl
|
||||||
|
|
||||||
|
import org.octopus.internal.common.enums.EBusinessException
|
||||||
|
import org.octopus.internal.common.exceptions.OctopusPlanningException
|
||||||
|
import org.octopus.internal.common.mappers.ReportMapper
|
||||||
|
import org.octopus.internal.common.models.Report
|
||||||
|
import org.octopus.internal.db.ReportJpa
|
||||||
|
import org.octopus.internal.repositories.ReportRepository
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class ReportRepositoryImpl(
|
||||||
|
private val jpa: ReportJpa,
|
||||||
|
private val mapper: ReportMapper
|
||||||
|
) : ReportRepository {
|
||||||
|
|
||||||
|
override fun getById(id: Long): Report {
|
||||||
|
return mapper.toModel(
|
||||||
|
jpa.findById(id).orElseThrow {
|
||||||
|
OctopusPlanningException.create(
|
||||||
|
EBusinessException.ENTITY_WITH_ID_NOT_FOUND,
|
||||||
|
Report::class.java.simpleName,
|
||||||
|
id
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createReport(report: Report): Report {
|
||||||
|
return mapper.toModel(jpa.save(mapper.toEntity(report)))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createReports(reports: MutableList<Report>): MutableList<Report> {
|
||||||
|
return mapper.toModels(jpa.saveAll(mapper.toEntities(reports)))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getStartingReport(): Report? {
|
||||||
|
val entity = jpa.findFirstByStartIsTrue() ?: return null
|
||||||
|
return mapper.toModel(entity)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getNonStartingReports(endDate: Date?): MutableList<Report> {
|
||||||
|
if (endDate != null) {
|
||||||
|
return mapper.toModels(jpa.findByStartIsFalseAndReportMonthLessThanEqual(endDate))
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapper.toModels(jpa.findByStartIsFalse())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deleteAllReportsAfter(endDate: Date): Long {
|
||||||
|
return jpa.deleteByReportMonthGreaterThan(endDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deleteAllReportsAfterOrAtDate(endDate: Date): Long {
|
||||||
|
return jpa.deleteByReportMonthGreaterThanEqual(endDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun deleteAllReportsBefore(endDate: Date): Long {
|
||||||
|
return jpa.deleteByReportMonthLessThanEqual(endDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
package org.octopus.internal.services
|
package org.octopus.internal.services
|
||||||
|
|
||||||
import org.octopus.internal.common.models.Entry
|
import org.octopus.internal.common.models.Entry
|
||||||
import org.octopus.internal.web.utils.dtos.EntryDto
|
import org.octopus.internal.web.dtos.EntryDto
|
||||||
|
|
||||||
interface EntryService {
|
interface EntryService {
|
||||||
fun createEntry(entry: EntryDto): Entry
|
fun createEntry(entry: EntryDto): Entry
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
package org.octopus.internal.services
|
||||||
|
|
||||||
|
import org.octopus.internal.common.models.DetailedReport
|
||||||
|
import org.octopus.internal.common.models.Report
|
||||||
|
import org.octopus.internal.web.dtos.ReportDto
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
|
interface ReportService {
|
||||||
|
fun createReport(reportDto: ReportDto): Report
|
||||||
|
fun generateProjections(endDate: Date): Boolean
|
||||||
|
fun getAllReports(endDate: Date?): List<Report>
|
||||||
|
fun getAllReportsWithAvgCorrection(endDate: Date?): List<Report>
|
||||||
|
fun getDetailedReport(id: Long): DetailedReport
|
||||||
|
fun getAllReportsWithDetails(endDate: Date?): List<DetailedReport>
|
||||||
|
fun updateReport(id: Long, reportDto: ReportDto): Report
|
||||||
|
}
|
||||||
|
|
@ -6,7 +6,7 @@ import org.octopus.internal.common.exceptions.OctopusPlanningException
|
||||||
import org.octopus.internal.common.models.Entry
|
import org.octopus.internal.common.models.Entry
|
||||||
import org.octopus.internal.repositories.EntryRepository
|
import org.octopus.internal.repositories.EntryRepository
|
||||||
import org.octopus.internal.services.EntryService
|
import org.octopus.internal.services.EntryService
|
||||||
import org.octopus.internal.web.utils.dtos.EntryDto
|
import org.octopus.internal.web.dtos.EntryDto
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import java.time.Month
|
import java.time.Month
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,333 @@
|
||||||
|
package org.octopus.internal.services.impl
|
||||||
|
|
||||||
|
import jakarta.transaction.Transactional
|
||||||
|
import org.octopus.internal.common.enums.EBusinessException
|
||||||
|
import org.octopus.internal.common.enums.EEntryType
|
||||||
|
import org.octopus.internal.common.exceptions.OctopusPlanningException
|
||||||
|
import org.octopus.internal.common.isInTargetMonth
|
||||||
|
import org.octopus.internal.common.isRecurrentActive
|
||||||
|
import org.octopus.internal.common.models.DetailedReport
|
||||||
|
import org.octopus.internal.common.models.Report
|
||||||
|
import org.octopus.internal.common.toFirstDayOfMonthDate
|
||||||
|
import org.octopus.internal.common.toYearMonth
|
||||||
|
import org.octopus.internal.repositories.EntryRepository
|
||||||
|
import org.octopus.internal.repositories.ReportRepository
|
||||||
|
import org.octopus.internal.services.ReportService
|
||||||
|
import org.octopus.internal.web.dtos.ReportDto
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import org.springframework.transaction.annotation.Propagation
|
||||||
|
import java.time.YearMonth
|
||||||
|
import java.time.ZoneId
|
||||||
|
import java.time.temporal.ChronoUnit
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class ReportServiceImpl(
|
||||||
|
val repo: ReportRepository,
|
||||||
|
val entryRepo: EntryRepository,
|
||||||
|
private val zoneId: ZoneId = ZoneId.of("UTC") ) : ReportService {
|
||||||
|
|
||||||
|
override fun createReport(reportDto: ReportDto): Report {
|
||||||
|
val startingRepo = repo.getStartingReport()
|
||||||
|
if (startingRepo != null) {
|
||||||
|
throw OctopusPlanningException.create(EBusinessException.STARTING_REPORT_ALREADY_EXISTS, startingRepo)
|
||||||
|
}
|
||||||
|
|
||||||
|
return repo.createReport(Report(
|
||||||
|
id = 0L,
|
||||||
|
start = reportDto.start,
|
||||||
|
reportMonth = reportDto.reportMonth,
|
||||||
|
totalIncomes = 0.0,
|
||||||
|
totalOutcomes = 0.0,
|
||||||
|
expectedDelta = 0.0,
|
||||||
|
expectedCount = reportDto.realCount,
|
||||||
|
corrective = 0.0,
|
||||||
|
realCount = reportDto.realCount
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
override fun generateProjections(endDate: Date): Boolean {
|
||||||
|
val allFixedInValidTime = entryRepo.getAllFixedUpToEndDate(endDate)
|
||||||
|
val allRecurrent = entryRepo.getAllRecurrentMonthlyToEndDate(endDate)
|
||||||
|
|
||||||
|
val lastRealCountReport = repo.getNonStartingReports(endDate)
|
||||||
|
.filter { it.realCount > 0.0 }
|
||||||
|
.maxByOrNull { it.reportMonth }
|
||||||
|
|
||||||
|
|
||||||
|
val loopStartDate: YearMonth
|
||||||
|
if (lastRealCountReport != null) {
|
||||||
|
loopStartDate = lastRealCountReport.reportMonth.toYearMonth(zoneId).plusMonths(1)
|
||||||
|
} else {
|
||||||
|
loopStartDate = repo.getStartingReport()!!.reportMonth.toYearMonth(zoneId).plusMonths(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
val loopEndDate = endDate.toYearMonth(zoneId)
|
||||||
|
if (loopStartDate > loopEndDate) {
|
||||||
|
println("Info: All reports are up to date until the end date. No new projections generated.")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteReportsAfterOrAtWithNewTransaction(loopStartDate.toFirstDayOfMonthDate(zoneId))
|
||||||
|
|
||||||
|
val startingReport = repo.getStartingReport()!!
|
||||||
|
|
||||||
|
var runningProjectedBalance = (lastRealCountReport ?: startingReport).realCount
|
||||||
|
|
||||||
|
var currentMonth = loopStartDate
|
||||||
|
while (currentMonth <= loopEndDate) {
|
||||||
|
var totalIncomes = 0.0
|
||||||
|
var totalOutcomes = 0.0
|
||||||
|
|
||||||
|
allFixedInValidTime.filter {
|
||||||
|
it.fixedDate.isInTargetMonth(currentMonth, zoneId)
|
||||||
|
}.forEach { entry ->
|
||||||
|
when (entry.type) {
|
||||||
|
EEntryType.INCOME -> totalIncomes += entry.amount
|
||||||
|
EEntryType.OUTCOME -> totalOutcomes += entry.amount
|
||||||
|
EEntryType.INVESTMENT -> {} // Ignore investments for direct flow calculation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allRecurrent.filter { entry ->
|
||||||
|
entry.isRecurrentActive(currentMonth, zoneId)
|
||||||
|
}.forEach { entry ->
|
||||||
|
when (entry.type) {
|
||||||
|
EEntryType.INCOME -> totalIncomes += entry.amount
|
||||||
|
EEntryType.OUTCOME -> totalOutcomes += entry.amount
|
||||||
|
EEntryType.INVESTMENT -> {} // Ignore investments for direct flow calculation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val expectedDelta = totalIncomes - totalOutcomes
|
||||||
|
val expectedCount = runningProjectedBalance + expectedDelta
|
||||||
|
|
||||||
|
val reportDate = currentMonth.toFirstDayOfMonthDate(zoneId)
|
||||||
|
|
||||||
|
val projectionReport = Report(
|
||||||
|
id = 0L,
|
||||||
|
start = false,
|
||||||
|
reportMonth = reportDate,
|
||||||
|
totalIncomes = totalIncomes,
|
||||||
|
totalOutcomes = totalOutcomes,
|
||||||
|
expectedDelta = expectedDelta,
|
||||||
|
expectedCount = expectedCount,
|
||||||
|
corrective = 0.0,
|
||||||
|
realCount = 0.0
|
||||||
|
)
|
||||||
|
|
||||||
|
repo.createReport(projectionReport)
|
||||||
|
println("Generated and saved projection for $currentMonth. Expected Balance: $expectedCount")
|
||||||
|
|
||||||
|
runningProjectedBalance = expectedCount
|
||||||
|
currentMonth = currentMonth.plus(1, ChronoUnit.MONTHS)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(Transactional.TxType.REQUIRES_NEW)
|
||||||
|
private fun deleteReportsAfterOrAtWithNewTransaction(cutoffDate: Date) {
|
||||||
|
repo.deleteAllReportsAfterOrAtDate(cutoffDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(Transactional.TxType.REQUIRES_NEW)
|
||||||
|
private fun deleteReportsAfterNewTransaction(cutoffDate: Date) {
|
||||||
|
repo.deleteAllReportsAfter(cutoffDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun getAllReports(endDate: Date?): List<Report> {
|
||||||
|
val startRepo = repo.getStartingReport()
|
||||||
|
?: throw OctopusPlanningException.create(EBusinessException.STARTING_REPORT_DOESNT_EXIST)
|
||||||
|
if (endDate != null && startRepo.reportMonth <= endDate) {
|
||||||
|
throw OctopusPlanningException.create(EBusinessException.REPORT_END_DATE_LE_START_REPORT)
|
||||||
|
}
|
||||||
|
val otherReports = repo.getNonStartingReports(endDate)
|
||||||
|
otherReports.addFirst(startRepo)
|
||||||
|
return otherReports
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getAllReportsWithAvgCorrection(endDate: Date?): List<Report> {
|
||||||
|
val startRepo = repo.getStartingReport()
|
||||||
|
?: throw OctopusPlanningException.create(EBusinessException.STARTING_REPORT_DOESNT_EXIST)
|
||||||
|
if (endDate != null && startRepo.reportMonth <= endDate) {
|
||||||
|
throw OctopusPlanningException.create(EBusinessException.REPORT_END_DATE_LE_START_REPORT)
|
||||||
|
}
|
||||||
|
val otherReports = repo.getNonStartingReports(endDate)
|
||||||
|
otherReports.addFirst(startRepo)
|
||||||
|
|
||||||
|
val correctedReports = otherReports.filter { it.realCount > 0.0 && !it.start }
|
||||||
|
|
||||||
|
val totalCorrection = correctedReports.sumOf { it.corrective }
|
||||||
|
val correctedReportCount = correctedReports.size.toDouble()
|
||||||
|
|
||||||
|
val averageCorrection = if (correctedReportCount > 0) {
|
||||||
|
"%.2f".format(totalCorrection / correctedReportCount).toDouble()
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
val finalReports = mutableListOf<Report>()
|
||||||
|
var runningProjectedBalance = 0.0
|
||||||
|
|
||||||
|
val lastActualReport = otherReports
|
||||||
|
.lastOrNull { it.start || it.realCount > 0.0 }
|
||||||
|
?: startRepo
|
||||||
|
|
||||||
|
runningProjectedBalance = if (lastActualReport.realCount > 0.0) {
|
||||||
|
lastActualReport.realCount
|
||||||
|
} else {
|
||||||
|
lastActualReport.expectedCount
|
||||||
|
}
|
||||||
|
|
||||||
|
for (report in otherReports) {
|
||||||
|
if (report.start || report.realCount > 0.0) {
|
||||||
|
finalReports.add(report)
|
||||||
|
runningProjectedBalance = if (report.realCount > 0.0) report.realCount else report.expectedCount
|
||||||
|
} else {
|
||||||
|
val newExpectedCount = runningProjectedBalance + report.expectedDelta + averageCorrection
|
||||||
|
val correctedReport = report.copy(
|
||||||
|
expectedCount = newExpectedCount
|
||||||
|
)
|
||||||
|
finalReports.add(correctedReport)
|
||||||
|
runningProjectedBalance = newExpectedCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return finalReports
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getDetailedReport(id: Long): DetailedReport {
|
||||||
|
val report = repo.getById(id)
|
||||||
|
val reportYearMonth = report.reportMonth.toYearMonth(zoneId)
|
||||||
|
val reportDate = report.reportMonth
|
||||||
|
|
||||||
|
val allFixed = entryRepo.getAllFixedUpToEndDate(reportDate)
|
||||||
|
val allRecurrent = entryRepo.getAllRecurrentMonthlyToEndDate(reportDate)
|
||||||
|
|
||||||
|
val fixedEntries = allFixed.filter { entry ->
|
||||||
|
entry.fixedDate.isInTargetMonth(reportYearMonth, zoneId)
|
||||||
|
}
|
||||||
|
|
||||||
|
val recurrentEntries = allRecurrent.filter { entry ->
|
||||||
|
entry.isRecurrentActive(reportYearMonth, zoneId)
|
||||||
|
}
|
||||||
|
|
||||||
|
val allEntriesForMonth = fixedEntries + recurrentEntries
|
||||||
|
|
||||||
|
return DetailedReport(
|
||||||
|
yearMonth = reportYearMonth,
|
||||||
|
report = report,
|
||||||
|
entries = allEntriesForMonth
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getAllReportsWithDetails(endDate: Date?): List<DetailedReport> {
|
||||||
|
val effectiveEndDate = determineEffectiveEndDate(endDate)
|
||||||
|
|
||||||
|
val allReports = repo.getNonStartingReports(effectiveEndDate)
|
||||||
|
val allFixed = entryRepo.getAllFixedUpToEndDate(effectiveEndDate)
|
||||||
|
val allRecurrent = entryRepo.getAllRecurrentMonthlyToEndDate(effectiveEndDate)
|
||||||
|
|
||||||
|
return allReports.map { report ->
|
||||||
|
val reportYearMonth = report.reportMonth.toYearMonth(zoneId)
|
||||||
|
|
||||||
|
val fixedEntries = allFixed.filter { entry ->
|
||||||
|
entry.fixedDate.isInTargetMonth(reportYearMonth, zoneId)
|
||||||
|
}
|
||||||
|
|
||||||
|
val recurrentEntries = allRecurrent.filter { entry ->
|
||||||
|
entry.isRecurrentActive(reportYearMonth, zoneId)
|
||||||
|
}
|
||||||
|
|
||||||
|
val allEntriesForMonth = fixedEntries + recurrentEntries
|
||||||
|
|
||||||
|
DetailedReport(
|
||||||
|
yearMonth = reportYearMonth,
|
||||||
|
report = report,
|
||||||
|
entries = allEntriesForMonth
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
override fun updateReport(
|
||||||
|
id: Long,
|
||||||
|
reportDto: ReportDto
|
||||||
|
): Report {
|
||||||
|
val reportToUpdate = repo.getById(id)
|
||||||
|
if (reportDto.start != reportToUpdate.start) {
|
||||||
|
throw OctopusPlanningException.create(EBusinessException.REPORT_IS_START_NOT_CLEAR)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reportDto.start) {
|
||||||
|
val startingReport = repo.createReport(Report(
|
||||||
|
id = reportToUpdate.id,
|
||||||
|
start = true,
|
||||||
|
reportMonth = reportDto.reportMonth,
|
||||||
|
totalIncomes = 0.0,
|
||||||
|
totalOutcomes = 0.0,
|
||||||
|
expectedDelta = 0.0,
|
||||||
|
expectedCount = reportDto.realCount,
|
||||||
|
corrective = 0.0,
|
||||||
|
realCount = reportDto.realCount
|
||||||
|
))
|
||||||
|
|
||||||
|
repo.deleteAllReportsBefore(reportDto.reportMonth)
|
||||||
|
generateProjections(determineEffectiveEndDate(null))
|
||||||
|
return startingReport
|
||||||
|
}
|
||||||
|
|
||||||
|
val correctionAmount = reportDto.realCount - reportToUpdate.expectedCount
|
||||||
|
|
||||||
|
val updatedReport = repo.createReport(Report(
|
||||||
|
id = reportToUpdate.id,
|
||||||
|
start = false,
|
||||||
|
reportMonth = reportToUpdate.reportMonth,
|
||||||
|
totalIncomes = reportToUpdate.totalIncomes,
|
||||||
|
totalOutcomes = reportToUpdate.totalOutcomes,
|
||||||
|
expectedDelta = reportToUpdate.expectedDelta,
|
||||||
|
expectedCount = reportToUpdate.expectedCount, // Retain old expectedCount here
|
||||||
|
corrective = correctionAmount, // Record the correction
|
||||||
|
realCount = reportDto.realCount // Set the new verified amount
|
||||||
|
))
|
||||||
|
|
||||||
|
val cutoffDate = updatedReport.reportMonth
|
||||||
|
val subsequentReports = repo.getNonStartingReports(null)
|
||||||
|
.filter { it.reportMonth.after(cutoffDate) }
|
||||||
|
.sortedBy { it.reportMonth }
|
||||||
|
.toMutableList()
|
||||||
|
|
||||||
|
val reportsToUpdateBatch = mutableListOf<Report>()
|
||||||
|
for (subReport in subsequentReports) {
|
||||||
|
if (subReport.realCount > 0.0) {
|
||||||
|
val correctedExpectedCount = subReport.expectedCount + correctionAmount
|
||||||
|
|
||||||
|
reportsToUpdateBatch.add(subReport.copy(
|
||||||
|
expectedCount = correctedExpectedCount,
|
||||||
|
corrective = subReport.realCount - correctedExpectedCount
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
repo.createReports(reportsToUpdateBatch)
|
||||||
|
deleteReportsAfterNewTransaction(cutoffDate)
|
||||||
|
generateProjections(determineEffectiveEndDate(null))
|
||||||
|
|
||||||
|
return updatedReport
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun determineEffectiveEndDate(inputEndDate: Date?): Date {
|
||||||
|
if (inputEndDate != null) return inputEndDate
|
||||||
|
|
||||||
|
val allIncome = entryRepo.getAllByType(EEntryType.INCOME)
|
||||||
|
val allOutcome = entryRepo.getAllByType(EEntryType.OUTCOME)
|
||||||
|
|
||||||
|
val allDates = (allIncome + allOutcome).mapNotNull { entry -> entry.fixedDate ?: entry.endDate }
|
||||||
|
|
||||||
|
return allDates.maxByOrNull { it } ?: Date()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -7,9 +7,8 @@ import jakarta.validation.Valid
|
||||||
import lombok.AllArgsConstructor
|
import lombok.AllArgsConstructor
|
||||||
import org.octopus.internal.common.models.Entry
|
import org.octopus.internal.common.models.Entry
|
||||||
import org.octopus.internal.services.EntryService
|
import org.octopus.internal.services.EntryService
|
||||||
import org.octopus.internal.web.utils.dtos.EntryDto
|
import org.octopus.internal.web.dtos.EntryDto
|
||||||
import org.octopus.internal.web.utils.responses.WebResponse
|
import org.octopus.internal.web.utils.responses.WebResponse
|
||||||
import org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties.Web
|
|
||||||
import org.springframework.web.bind.annotation.*
|
import org.springframework.web.bind.annotation.*
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
package org.octopus.internal.web.controllers
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag
|
||||||
|
import jakarta.validation.Valid
|
||||||
|
import lombok.AllArgsConstructor
|
||||||
|
import org.octopus.internal.common.models.DetailedReport
|
||||||
|
import org.octopus.internal.common.models.Report
|
||||||
|
import org.octopus.internal.services.ReportService
|
||||||
|
import org.octopus.internal.web.dtos.ReportDto
|
||||||
|
import org.octopus.internal.web.utils.responses.WebResponse
|
||||||
|
import org.springframework.format.annotation.DateTimeFormat
|
||||||
|
import org.springframework.web.bind.annotation.*
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/reports")
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Tag(name = "Reports Management", description = "Operations related to reports.")
|
||||||
|
class ReportController(
|
||||||
|
val reportService: ReportService
|
||||||
|
) {
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
fun createReport(@Valid @RequestBody reportDto: ReportDto): WebResponse<Report> {
|
||||||
|
return WebResponse.ok(reportService.createReport(reportDto))
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/projections")
|
||||||
|
fun generateProjections(@RequestParam("endDate", required = true) @DateTimeFormat(pattern = "yyyy-MM-dd") endDate: Date): WebResponse<Boolean> {
|
||||||
|
return WebResponse.ok(reportService.generateProjections(endDate))
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/resume")
|
||||||
|
fun getAllReports(
|
||||||
|
@RequestParam("endDate", required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") endDate: Date?): WebResponse<List<Report>> {
|
||||||
|
return WebResponse.ok(reportService.getAllReports(endDate))
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
fun getAllReportsWithDetails(
|
||||||
|
@RequestParam("endDate", required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") endDate: Date?): WebResponse<List<DetailedReport>> {
|
||||||
|
return WebResponse.ok(reportService.getAllReportsWithDetails(endDate))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
fun updateReport(@PathVariable("id") id: Long,
|
||||||
|
@Valid @RequestBody reportDto: ReportDto): WebResponse<Report> {
|
||||||
|
return WebResponse.ok(reportService.updateReport(id, reportDto))
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/correction")
|
||||||
|
fun getCorrectionValue(
|
||||||
|
@RequestParam("endDate", required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") endDate: Date?): WebResponse<List<Report>> {
|
||||||
|
return WebResponse.ok(reportService.getAllReportsWithAvgCorrection(endDate))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
package org.octopus.internal.web.utils.dtos
|
package org.octopus.internal.web.dtos
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema
|
import io.swagger.v3.oas.annotations.media.Schema
|
||||||
import jakarta.validation.constraints.DecimalMin
|
import jakarta.validation.constraints.DecimalMin
|
||||||
import org.hibernate.validator.constraints.Length
|
import org.hibernate.validator.constraints.Length
|
||||||
import org.octopus.internal.web.utils.dtos.validators.EntryHexColorValidator
|
import org.octopus.internal.web.dtos.validators.EntryHexColorValidator
|
||||||
import org.octopus.internal.web.utils.dtos.validators.EntryMonthValidator
|
import org.octopus.internal.web.dtos.validators.EntryMonthValidator
|
||||||
import org.octopus.internal.web.utils.dtos.validators.EntryTypeValidator
|
import org.octopus.internal.web.dtos.validators.EntryTypeValidator
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
data class EntryDto(
|
data class EntryDto(
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
package org.octopus.internal.web.dtos
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
|
data class ReportDto(
|
||||||
|
@field:Schema(description = "Indicates if this is the **initial month** for financial projections.", defaultValue = "false")
|
||||||
|
val start: Boolean,
|
||||||
|
|
||||||
|
@field:Schema(description = "The calendar month this report provides information **for**.")
|
||||||
|
val reportMonth: Date,
|
||||||
|
|
||||||
|
@field:Schema(description = "The **actual amount of money observed** (e.g., in a bank account) for the month.")
|
||||||
|
val realCount: Double
|
||||||
|
)
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package org.octopus.internal.web.utils.dtos.validators
|
package org.octopus.internal.web.dtos.validators
|
||||||
|
|
||||||
import jakarta.validation.Constraint
|
import jakarta.validation.Constraint
|
||||||
import jakarta.validation.ConstraintValidator
|
import jakarta.validation.ConstraintValidator
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package org.octopus.internal.web.utils.dtos.validators
|
package org.octopus.internal.web.dtos.validators
|
||||||
|
|
||||||
import jakarta.validation.Constraint
|
import jakarta.validation.Constraint
|
||||||
import jakarta.validation.ConstraintValidator
|
import jakarta.validation.ConstraintValidator
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package org.octopus.internal.web.utils.dtos.validators
|
package org.octopus.internal.web.dtos.validators
|
||||||
|
|
||||||
import jakarta.validation.Constraint
|
import jakarta.validation.Constraint
|
||||||
import jakarta.validation.ConstraintValidator
|
import jakarta.validation.ConstraintValidator
|
||||||
|
|
@ -31,7 +31,11 @@ class BaseAdvice {
|
||||||
private fun deductStatus(ex: EBusinessException): HttpStatus {
|
private fun deductStatus(ex: EBusinessException): HttpStatus {
|
||||||
return when (ex) {
|
return when (ex) {
|
||||||
EBusinessException.ENTITY_WITH_ID_NOT_FOUND -> HttpStatus.NOT_FOUND
|
EBusinessException.ENTITY_WITH_ID_NOT_FOUND -> HttpStatus.NOT_FOUND
|
||||||
|
EBusinessException.STARTING_REPORT_ALREADY_EXISTS -> HttpStatus.CONFLICT
|
||||||
|
EBusinessException.REPORT_END_DATE_LE_START_REPORT,
|
||||||
|
EBusinessException.REPORT_IS_START_NOT_CLEAR,
|
||||||
EBusinessException.INVALID_REQUEST -> HttpStatus.BAD_REQUEST
|
EBusinessException.INVALID_REQUEST -> HttpStatus.BAD_REQUEST
|
||||||
|
EBusinessException.STARTING_REPORT_DOESNT_EXIST -> HttpStatus.NOT_ACCEPTABLE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -122,4 +126,4 @@ class BaseAdvice {
|
||||||
ex.message
|
ex.message
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue