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) {
|
||||
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> {
|
||||
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 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
|
||||
|
||||
import org.hibernate.type.descriptor.DateTimeUtils
|
||||
import org.octopus.internal.common.enums.EBusinessException
|
||||
import org.octopus.internal.common.enums.EEntryType
|
||||
import org.octopus.internal.common.exceptions.OctopusPlanningException
|
||||
|
|
@ -14,8 +13,8 @@ import java.util.*
|
|||
|
||||
@Component
|
||||
class EntryRepositoryImpl(
|
||||
protected val jpa: EntryJpa,
|
||||
protected val mapper: EntryMapper
|
||||
private val jpa: EntryJpa,
|
||||
private val mapper: EntryMapper
|
||||
) : EntryRepository {
|
||||
|
||||
override fun getById(id: Long): Entry {
|
||||
|
|
@ -50,7 +49,7 @@ class EntryRepositoryImpl(
|
|||
|
||||
override fun getAllRecurrentMonthlyToEndDate(endDate: Date): MutableList<Entry> {
|
||||
return mapper.toModels(
|
||||
jpa.findAllByFixedDateIsNullAndRecurrentIsTrueAndEndDateIsNotNullAndEndDateBefore(endDate)
|
||||
jpa.findAllByFixedDateIsNullAndRecurrentIsTrueAndEndDateIsNotNullAndEndDateLessThanEqual(endDate)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -68,7 +67,7 @@ class EntryRepositoryImpl(
|
|||
|
||||
override fun getAllFixedUpToEndDate(endDate: Date): MutableList<Entry> {
|
||||
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
|
||||
|
||||
import org.octopus.internal.common.models.Entry
|
||||
import org.octopus.internal.web.utils.dtos.EntryDto
|
||||
import org.octopus.internal.web.dtos.EntryDto
|
||||
|
||||
interface EntryService {
|
||||
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.repositories.EntryRepository
|
||||
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 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 org.octopus.internal.common.models.Entry
|
||||
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.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties.Web
|
||||
import org.springframework.web.bind.annotation.*
|
||||
|
||||
@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 jakarta.validation.constraints.DecimalMin
|
||||
import org.hibernate.validator.constraints.Length
|
||||
import org.octopus.internal.web.utils.dtos.validators.EntryHexColorValidator
|
||||
import org.octopus.internal.web.utils.dtos.validators.EntryMonthValidator
|
||||
import org.octopus.internal.web.utils.dtos.validators.EntryTypeValidator
|
||||
import org.octopus.internal.web.dtos.validators.EntryHexColorValidator
|
||||
import org.octopus.internal.web.dtos.validators.EntryMonthValidator
|
||||
import org.octopus.internal.web.dtos.validators.EntryTypeValidator
|
||||
import java.util.Date
|
||||
|
||||
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.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.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.ConstraintValidator
|
||||
|
|
@ -31,7 +31,11 @@ class BaseAdvice {
|
|||
private fun deductStatus(ex: EBusinessException): HttpStatus {
|
||||
return when (ex) {
|
||||
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.STARTING_REPORT_DOESNT_EXIST -> HttpStatus.NOT_ACCEPTABLE
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue