logic : reports #3

Merged
zobrock merged 1 commits from feature/reports into main 2025-10-19 21:01:03 +02:00
23 changed files with 652 additions and 20 deletions
Showing only changes of commit 634ec61805 - Show all commits

View File

@ -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
}

View File

@ -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.")
} }

View File

@ -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>

View File

@ -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>
)

View File

@ -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
)

View File

@ -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>
} }

View File

@ -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
}

View File

@ -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
)

View File

@ -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
}

View File

@ -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)
) )
} }

View File

@ -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)
}
}

View File

@ -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

View File

@ -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
}

View File

@ -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

View File

@ -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()
}
}

View File

@ -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

View File

@ -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))
}
}

View File

@ -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(

View File

@ -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
)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
} }
} }