+ categories sort + analytics

This commit is contained in:
Vladimir Voronin
2025-01-12 14:18:37 +03:00
parent 84f61532d0
commit a0fa275073
14 changed files with 351 additions and 78 deletions

View File

@@ -38,10 +38,11 @@ dependencies {
implementation("org.jetbrains.kotlin:kotlin-reflect") implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.springframework.boot:spring-boot-starter-validation") implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("commons-logging:commons-logging:1.3.4") implementation("commons-logging:commons-logging:1.3.4")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-cache") implementation("org.springframework.boot:spring-boot-starter-cache")
implementation("org.springframework.boot:spring-boot-starter-security") implementation("org.springframework.boot:spring-boot-starter-security")
implementation ("org.springframework.boot:spring-boot-starter-actuator")
implementation("io.jsonwebtoken:jjwt-api:0.11.5") implementation("io.jsonwebtoken:jjwt-api:0.11.5")
implementation("io.jsonwebtoken:jjwt-impl:0.11.5") implementation("io.jsonwebtoken:jjwt-impl:0.11.5")
@@ -53,10 +54,11 @@ dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:1.7.3") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:1.7.3")
implementation("org.springframework.boot:spring-boot-starter-data-mongodb-reactive") implementation("org.springframework.boot:spring-boot-starter-data-mongodb-reactive")
implementation("com.google.code.gson:gson") implementation("com.google.code.gson:gson")
implementation("io.micrometer:micrometer-registry-prometheus")
runtimeOnly("org.postgresql:postgresql")
compileOnly("org.projectlombok:lombok") compileOnly("org.projectlombok:lombok")
annotationProcessor("org.projectlombok:lombok") annotationProcessor("org.projectlombok:lombok")
testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.springframework.boot:spring-boot-starter-test")
@@ -73,3 +75,5 @@ kotlin {
tasks.withType<Test> { tasks.withType<Test> {
useJUnitPlatform() useJUnitPlatform()
} }

View File

@@ -26,7 +26,10 @@ class BearerTokenFilter(private val authService: AuthService) : SecurityContextS
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> { override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
val token = exchange.request.headers.getFirst(HttpHeaders.AUTHORIZATION)?.removePrefix("Bearer ") val token = exchange.request.headers.getFirst(HttpHeaders.AUTHORIZATION)?.removePrefix("Bearer ")
if (exchange.request.path.value() == "/api/auth/login"){ logger.info("here ${exchange.request.path.value()}")
if (exchange.request.path.value() == "/api/auth/login" || exchange.request.path.value()
.startsWith("/api/actuator")
) {
return chain.filter(exchange) return chain.filter(exchange)
} }
@@ -52,8 +55,6 @@ class BearerTokenFilter(private val authService: AuthService) : SecurityContextS
} }
} }

View File

@@ -0,0 +1,12 @@
package space.luminic.budgerapp.configs
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
//@Configuration
//class CommonConfig {
// @Bean
// fun httpTraceRepository(): HttpTraceRepository {
// return InMemoryHttpTraceRepository()
// }
//}

View File

@@ -35,7 +35,7 @@ class BudgetController(
@GetMapping("/{id}") @GetMapping("/{id}")
fun getBudget(@PathVariable id: String): Mono<BudgetDTO> { fun getBudget(@PathVariable id: String): Mono<BudgetDTO> {
logger.info("here }")
return budgetService.getBudget(id) return budgetService.getBudget(id)
} }

View File

@@ -1,5 +1,6 @@
package space.luminic.budgerapp.controllers package space.luminic.budgerapp.controllers
import org.bson.Document
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.http.HttpStatus import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
@@ -10,25 +11,33 @@ import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.PutMapping import org.springframework.web.bind.annotation.PutMapping
import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController import org.springframework.web.bind.annotation.RestController
import org.springframework.web.client.HttpClientErrorException import org.springframework.web.client.HttpClientErrorException
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
import space.luminic.budgerapp.models.BudgetCategory
import space.luminic.budgerapp.models.Category import space.luminic.budgerapp.models.Category
import space.luminic.budgerapp.services.BudgetService
import space.luminic.budgerapp.services.CategoryService import space.luminic.budgerapp.services.CategoryService
import java.time.LocalDate
@RestController @RestController
@RequestMapping("/categories") @RequestMapping("/categories")
class CategoriesController( class CategoriesController(
private val categoryService: CategoryService private val categoryService: CategoryService,
private val budgetService: BudgetService
) { ) {
private val logger = LoggerFactory.getLogger(javaClass) private val logger = LoggerFactory.getLogger(javaClass)
@GetMapping() @GetMapping()
fun getCategories(): Mono<List<Category>> { fun getCategories(
return categoryService.getCategories() @RequestParam("sort") sortBy: String = "name",
@RequestParam("direction") direction: String = "ASC"
): Mono<List<Category>> {
return categoryService.getCategories(sortBy, direction)
} }
@@ -57,4 +66,19 @@ class CategoriesController(
return categoryService.deleteCategory(categoryId) return categoryService.deleteCategory(categoryId)
} }
@GetMapping("/test")
fun test(): Mono<MutableList<BudgetCategory>> {
var dateFrom = LocalDate.parse("2025-01-10")
var dateTo = LocalDate.parse("2025-02-09")
return categoryService.getCategoryTransactionPipeline(dateFrom, dateTo)
}
@GetMapping("/by-month")
fun getCategoriesSumsByMonths(): Mono<List<Document>> {
return categoryService.getCategorySumsPipeline(LocalDate.of(2024, 8, 1), LocalDate.of(2025, 1, 12))
}
} }

View File

@@ -3,9 +3,9 @@ package space.luminic.budgerapp.models
import org.springframework.data.annotation.Id import org.springframework.data.annotation.Id
import org.springframework.data.mongodb.core.mapping.DBRef import org.springframework.data.mongodb.core.mapping.DBRef
import org.springframework.data.mongodb.core.mapping.Document import org.springframework.data.mongodb.core.mapping.Document
import java.math.BigDecimal
import java.time.LocalDate import java.time.LocalDate
import java.time.LocalDateTime import java.time.LocalDateTime
import kotlin.collections.mutableListOf
data class BudgetDTO( data class BudgetDTO(
@@ -19,7 +19,7 @@ data class BudgetDTO(
var categories: MutableList<BudgetCategory> = mutableListOf(), var categories: MutableList<BudgetCategory> = mutableListOf(),
var transactions: MutableList<Transaction>? = null, var transactions: MutableList<Transaction>? = null,
) )
@Document("budgets") @Document("budgets")

View File

@@ -14,6 +14,6 @@ interface BudgetRepo: ReactiveMongoRepository<Budget, String> {
override fun findAll(sort: Sort): Flux<Budget> override fun findAll(sort: Sort): Flux<Budget>
fun findByDateFromLessThanEqualAndDateToGreaterThan(dateOne: LocalDate, dateTwo: LocalDate): Mono<Budget> fun findByDateFromLessThanEqualAndDateToGreaterThanEqual(dateOne: LocalDate, dateTwo: LocalDate): Mono<Budget>
} }

View File

@@ -10,4 +10,6 @@ interface CategoryRepo : ReactiveMongoRepository<Category, String> {
fun findByName(name: String): Mono<Category> fun findByName(name: String): Mono<Category>
} }

View File

@@ -1,6 +1,7 @@
package space.luminic.budgerapp.services package space.luminic.budgerapp.services
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.cache.annotation.CacheEvict import org.springframework.cache.annotation.CacheEvict
import org.springframework.cache.annotation.Cacheable import org.springframework.cache.annotation.Cacheable
@@ -59,27 +60,32 @@ class BudgetService(
TransactionEventType.DELETE -> updateBudgetOnDelete(event) TransactionEventType.DELETE -> updateBudgetOnDelete(event)
} }
} }
// runBlocking(Dispatchers.IO) { // runBlocking(Dispatchers.IO) {
// updateBudgetWarns( // updateBudgetWarns(
// budget = budgetRepo.findByDateFromLessThanEqualAndDateToGreaterThan( // budgetRepo.findByDateFromLessThanEqualAndDateToGreaterThan(
// event.newTransaction.date.toLocalDate(), event.newTransaction.date.toLocalDate() // event.newTransaction.date, event.newTransaction.date.
// ) // ).map{it}
// ) // )
// } // }
} }
fun updateBudgetOnCreate(event: TransactionEvent) { fun updateBudgetOnCreate(event: TransactionEvent) {
budgetRepo.findByDateFromLessThanEqualAndDateToGreaterThan( budgetRepo.findByDateFromLessThanEqualAndDateToGreaterThanEqual(
event.newTransaction.date, event.newTransaction.date event.newTransaction.date, event.newTransaction.date
).flatMap { budget -> ).flatMap { budget ->
categoryService.getBudgetCategories(budget.dateFrom, budget.dateTo).flatMap { categories -> val categories = categoryService.getBudgetCategories(budget.dateFrom, budget.dateTo)
logger.info(categories.toString())
categories.flatMap { categories ->
val updatedCategories = when (event.newTransaction.type.code) { val updatedCategories = when (event.newTransaction.type.code) {
"PLANNED" -> Flux.fromIterable(budget.categories) "PLANNED" -> Flux.fromIterable(budget.categories)
.map { category -> .map { category ->
categories[category.category.id]?.let { data -> categories[category.category.id]?.let { data ->
category.currentSpent = data["instantAmount"] ?: 0.0 if (category.category.id == event.newTransaction.category.id) {
category.currentPlanned = data["plannedAmount"] ?: 0.0 category.currentSpent = data["instantAmount"] ?: 0.0
category.currentLimit += event.newTransaction.amount category.currentPlanned = data["plannedAmount"] ?: 0.0
category.currentLimit += event.newTransaction.amount
}
} }
category category
}.collectList() }.collectList()
@@ -87,8 +93,10 @@ class BudgetService(
"INSTANT" -> Flux.fromIterable(budget.categories) "INSTANT" -> Flux.fromIterable(budget.categories)
.map { category -> .map { category ->
categories[category.category.id]?.let { data -> categories[category.category.id]?.let { data ->
category.currentSpent = data["instantAmount"] ?: 0.0 if (category.category.id == event.newTransaction.category.id) {
category.currentPlanned = data["plannedAmount"] ?: 0.0 category.currentSpent = data["instantAmount"] ?: 0.0
category.currentPlanned = data["plannedAmount"] ?: 0.0
}
} }
category category
}.collectList() }.collectList()
@@ -107,13 +115,13 @@ class BudgetService(
fun updateBudgetOnEdit(event: TransactionEvent) { fun updateBudgetOnEdit(event: TransactionEvent) {
budgetRepo.findByDateFromLessThanEqualAndDateToGreaterThan( budgetRepo.findByDateFromLessThanEqualAndDateToGreaterThanEqual(
event.oldTransaction.date, event.oldTransaction.date event.oldTransaction.date, event.oldTransaction.date
).switchIfEmpty( ).switchIfEmpty(
Mono.error(BudgetNotFoundException("old budget cannot be null")) Mono.error(BudgetNotFoundException("old budget cannot be null"))
).then().subscribe() ).then().subscribe()
budgetRepo.findByDateFromLessThanEqualAndDateToGreaterThan( budgetRepo.findByDateFromLessThanEqualAndDateToGreaterThanEqual(
event.newTransaction.date, event.newTransaction.date event.newTransaction.date, event.newTransaction.date
).flatMap { budget -> ).flatMap { budget ->
categoryService.getBudgetCategories(budget.dateFrom, budget.dateTo).flatMap { categories -> categoryService.getBudgetCategories(budget.dateFrom, budget.dateTo).flatMap { categories ->
@@ -134,8 +142,10 @@ class BudgetService(
"INSTANT" -> Flux.fromIterable(budget.categories) "INSTANT" -> Flux.fromIterable(budget.categories)
.map { category -> .map { category ->
categories[category.category.id]?.let { data -> categories[category.category.id]?.let { data ->
category.currentSpent = data["instantAmount"] ?: 0.0 if (category.category.id == event.newTransaction.category.id) {
category.currentPlanned = data["plannedAmount"] ?: 0.0 category.currentSpent = data["instantAmount"] ?: 0.0
category.currentPlanned = data["plannedAmount"] ?: 0.0
}
} }
category category
}.collectList() }.collectList()
@@ -154,7 +164,7 @@ class BudgetService(
fun updateBudgetOnDelete(event: TransactionEvent) { fun updateBudgetOnDelete(event: TransactionEvent) {
budgetRepo.findByDateFromLessThanEqualAndDateToGreaterThan( budgetRepo.findByDateFromLessThanEqualAndDateToGreaterThanEqual(
event.newTransaction.date, event.newTransaction.date event.newTransaction.date, event.newTransaction.date
).flatMap { budget -> ).flatMap { budget ->
categoryService.getBudgetCategories(budget.dateFrom, budget.dateTo).flatMap { categories -> categoryService.getBudgetCategories(budget.dateFrom, budget.dateTo).flatMap { categories ->
@@ -162,9 +172,11 @@ class BudgetService(
"PLANNED" -> Flux.fromIterable(budget.categories) "PLANNED" -> Flux.fromIterable(budget.categories)
.map { category -> .map { category ->
categories[category.category.id]?.let { data -> categories[category.category.id]?.let { data ->
category.currentSpent = data["instantAmount"] ?: 0.0 if (category.category.id == event.newTransaction.category.id) {
category.currentPlanned = data["plannedAmount"] ?: 0.0 category.currentSpent = data["instantAmount"] ?: 0.0
category.currentLimit += event.newTransaction.amount category.currentPlanned = data["plannedAmount"] ?: 0.0
category.currentLimit += event.newTransaction.amount
}
} }
category category
}.collectList() }.collectList()
@@ -172,8 +184,10 @@ class BudgetService(
"INSTANT" -> Flux.fromIterable(budget.categories) "INSTANT" -> Flux.fromIterable(budget.categories)
.map { category -> .map { category ->
categories[category.category.id]?.let { data -> categories[category.category.id]?.let { data ->
category.currentSpent = data["instantAmount"] ?: 0.0 if (category.category.id == event.newTransaction.category.id) {
category.currentPlanned = data["plannedAmount"] ?: 0.0 category.currentSpent = data["instantAmount"] ?: 0.0
category.currentPlanned = data["plannedAmount"] ?: 0.0
}
} }
category category
}.collectList() }.collectList()
@@ -206,6 +220,7 @@ class BudgetService(
// @Cacheable("budgets", key = "#id") // @Cacheable("budgets", key = "#id")
fun getBudget(id: String): Mono<BudgetDTO> { fun getBudget(id: String): Mono<BudgetDTO> {
logger.info("here b")
return budgetRepo.findById(id) return budgetRepo.findById(id)
.flatMap { budget -> .flatMap { budget ->
val budgetDTO = BudgetDTO( val budgetDTO = BudgetDTO(
@@ -243,6 +258,7 @@ class BudgetService(
budgetDTO.plannedExpenses = transactions["plannedExpenses"] as MutableList budgetDTO.plannedExpenses = transactions["plannedExpenses"] as MutableList
budgetDTO.plannedIncomes = transactions["plannedIncomes"] as MutableList budgetDTO.plannedIncomes = transactions["plannedIncomes"] as MutableList
budgetDTO.transactions = transactions["instantTransactions"] as MutableList budgetDTO.transactions = transactions["instantTransactions"] as MutableList
logger.info("here e")
budgetDTO budgetDTO
} }
@@ -301,7 +317,7 @@ class BudgetService(
fun getBudgetByDate(date: LocalDate): Mono<Budget> { fun getBudgetByDate(date: LocalDate): Mono<Budget> {
return budgetRepo.findByDateFromLessThanEqualAndDateToGreaterThan(date, date).switchIfEmpty(Mono.empty()) return budgetRepo.findByDateFromLessThanEqualAndDateToGreaterThanEqual(date, date).switchIfEmpty(Mono.empty())
} }
// fun getBudgetCategorySQL(id: Int): List<BudgetCategory>? { // fun getBudgetCategorySQL(id: Int): List<BudgetCategory>? {
@@ -360,7 +376,7 @@ class BudgetService(
@CacheEvict(cacheNames = ["budgets", "budgetsList"], allEntries = true) @CacheEvict(cacheNames = ["budgets", "budgetsList"], allEntries = true)
fun setCategoryLimit(budgetId: String, catId: String, limit: Double): Mono<BudgetCategory> { fun setCategoryLimit(budgetId: String, catId: String, limit: Double): Mono<BudgetCategory> {
return budgetRepo.findById(budgetId).flatMap { budget -> return budgetRepo.findById(budgetId).flatMap { budget ->
val catEdit = budget.categories.firstOrNull { it.category.id == catId } val catEdit = budget.categories.firstOrNull { it.category?.id == catId }
?: return@flatMap Mono.error<BudgetCategory>(Exception("Category not found in the budget")) ?: return@flatMap Mono.error<BudgetCategory>(Exception("Category not found in the budget"))
transactionService.calcTransactionsSum(budget, catId, "PLANNED").flatMap { catPlanned -> transactionService.calcTransactionsSum(budget, catId, "PLANNED").flatMap { catPlanned ->
@@ -378,9 +394,6 @@ class BudgetService(
} }
fun getWarns(budgetId: String, isHide: Boolean? = null): Mono<List<Warn>> { fun getWarns(budgetId: String, isHide: Boolean? = null): Mono<List<Warn>> {
return warnRepo.findAllByBudgetIdAndIsHide(budgetId, isHide == true).collectList() return warnRepo.findAllByBudgetIdAndIsHide(budgetId, isHide == true).collectList()
} }
@@ -469,10 +482,10 @@ class BudgetService(
): Flux<Warn> { ): Flux<Warn> {
val warnsForCategory = mutableListOf<Mono<Warn?>>() val warnsForCategory = mutableListOf<Mono<Warn?>>()
val averageSum = averageSums[category.category.id] ?: 0.0 val averageSum = averageSums[category.category?.id] ?: 0.0
val categorySpentRatioInAvgIncome = if (averageIncome > 0.0) averageSum / averageIncome else 0.0 val categorySpentRatioInAvgIncome = if (averageIncome > 0.0) averageSum / averageIncome else 0.0
val projectedAvailableSum = currentBudgetIncome * categorySpentRatioInAvgIncome val projectedAvailableSum = currentBudgetIncome * categorySpentRatioInAvgIncome
val contextAtAvg = "category${category.category.id}atbudget${finalBudget.id}lessavg" val contextAtAvg = "category${category.category?.id}atbudget${finalBudget.id}lessavg"
val lowSavingContext = "savingValueLess10atBudget${finalBudget.id}" val lowSavingContext = "savingValueLess10atBudget${finalBudget.id}"
if (averageSum > category.currentLimit) { if (averageSum > category.currentLimit) {
@@ -482,11 +495,11 @@ class BudgetService(
Warn( Warn(
serenity = WarnSerenity.MAIN, serenity = WarnSerenity.MAIN,
message = PushMessage( message = PushMessage(
title = "Внимание на ${category.category.name}!", title = "Внимание на ${category.category?.name}!",
body = "Лимит меньше средних трат (Среднее: <b>${averageSum.toInt()} ₽</b> Текущий лимит: <b>${category.currentLimit.toInt()} ₽</b>)." + body = "Лимит меньше средних трат (Среднее: <b>${averageSum.toInt()} ₽</b> Текущий лимит: <b>${category.currentLimit.toInt()} ₽</b>)." +
"\nСредняя доля данной категории в доходах: <b>${(categorySpentRatioInAvgIncome * 100).toInt()}%</b>." + "\nСредняя доля данной категории в доходах: <b>${(categorySpentRatioInAvgIncome * 100).toInt()}%</b>." +
"\nПроецируется на текущие поступления: <b>${projectedAvailableSum.toInt()} ₽</b>", "\nПроецируется на текущие поступления: <b>${projectedAvailableSum.toInt()} ₽</b>",
icon = category.category.icon icon = category.category?.icon
), ),
budgetId = finalBudget.id!!, budgetId = finalBudget.id!!,
context = contextAtAvg, context = contextAtAvg,
@@ -499,7 +512,7 @@ class BudgetService(
warnRepo.findWarnByContext(contextAtAvg).flatMap { warnRepo.delete(it).then(Mono.empty<Warn>()) } warnRepo.findWarnByContext(contextAtAvg).flatMap { warnRepo.delete(it).then(Mono.empty<Warn>()) }
} }
if (category.category.id == "675850148198643f121e466a") { if (category.category?.id == "675850148198643f121e466a") {
val savingRatio = if (plannedIncome > 0.0) category.currentLimit / plannedIncome else 0.0 val savingRatio = if (plannedIncome > 0.0) category.currentLimit / plannedIncome else 0.0
if (savingRatio < 0.1) { if (savingRatio < 0.1) {
val warnMono = warnRepo.findWarnByContext(lowSavingContext) val warnMono = warnRepo.findWarnByContext(lowSavingContext)
@@ -512,7 +525,7 @@ class BudgetService(
body = "Текущие плановые сбережения равны ${plannedSaving.toInt()} (${ body = "Текущие плановые сбережения равны ${plannedSaving.toInt()} (${
(savingRatio * 100).toInt() (savingRatio * 100).toInt()
}%)! Исправьте!", }%)! Исправьте!",
icon = category.category.icon icon = category.category!!.icon
), ),
budgetId = finalBudget.id!!, budgetId = finalBudget.id!!,
context = lowSavingContext, context = lowSavingContext,

View File

@@ -5,6 +5,8 @@ import org.bson.Document
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.cache.annotation.CacheEvict import org.springframework.cache.annotation.CacheEvict
import org.springframework.cache.annotation.Cacheable import org.springframework.cache.annotation.Cacheable
import org.springframework.data.domain.Sort
import org.springframework.data.domain.Sort.Direction
import org.springframework.data.mongodb.core.ReactiveMongoTemplate import org.springframework.data.mongodb.core.ReactiveMongoTemplate
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import reactor.core.publisher.Flux import reactor.core.publisher.Flux
@@ -30,9 +32,16 @@ class CategoryService(
private val logger = LoggerFactory.getLogger(javaClass) private val logger = LoggerFactory.getLogger(javaClass)
fun getCategory(id: String): Mono<Category> {
return categoryRepo.findById(id)
}
@Cacheable("getAllCategories") @Cacheable("getAllCategories")
fun getCategories(): Mono<List<Category>> { fun getCategories(sortBy: String, direction: String): Mono<List<Category>> {
return categoryRepo.findAll().collectList() return categoryRepo.findAll(Sort.by(if (direction == "ASC") Direction.ASC else Direction.DESC, sortBy))
.collectList()
} }
@Cacheable("categoryTypes") @Cacheable("categoryTypes")
@@ -43,12 +52,12 @@ class CategoryService(
return types return types
} }
@CacheEvict(cacheNames = ["getAllCategories"],allEntries = true) @CacheEvict(cacheNames = ["getAllCategories"], allEntries = true)
fun createCategory(category: Category): Mono<Category> { fun createCategory(category: Category): Mono<Category> {
return categoryRepo.save(category) return categoryRepo.save(category)
} }
@CacheEvict(cacheNames = ["getAllCategories"],allEntries = true) @CacheEvict(cacheNames = ["getAllCategories"], allEntries = true)
fun editCategory(category: Category): Mono<Category> { fun editCategory(category: Category): Mono<Category> {
return categoryRepo.findById(category.id!!) // Возвращаем Mono<Category> return categoryRepo.findById(category.id!!) // Возвращаем Mono<Category>
.flatMap { oldCategory -> .flatMap { oldCategory ->
@@ -59,7 +68,7 @@ class CategoryService(
} }
} }
@CacheEvict(cacheNames = ["getAllCategories"],allEntries = true) @CacheEvict(cacheNames = ["getAllCategories"], allEntries = true)
fun deleteCategory(categoryId: String): Mono<String> { fun deleteCategory(categoryId: String): Mono<String> {
return categoryRepo.findById(categoryId).switchIfEmpty( return categoryRepo.findById(categoryId).switchIfEmpty(
Mono.error(IllegalArgumentException("Category with id: $categoryId not found")) Mono.error(IllegalArgumentException("Category with id: $categoryId not found"))
@@ -75,9 +84,9 @@ class CategoryService(
icon = "🚮" icon = "🚮"
) )
) )
).flatMapMany { newCategory -> ).flatMapMany { Category ->
Flux.fromIterable(transactions).flatMap { transaction -> Flux.fromIterable(transactions).flatMap { transaction ->
transaction.category = newCategory // Присваиваем конкретный объект категории transaction.category = Category // Присваиваем конкретный объект категории
transactionService.editTransaction(transaction) // Сохраняем изменения transactionService.editTransaction(transaction) // Сохраняем изменения
} }
} }
@@ -118,7 +127,7 @@ class CategoryService(
) )
), ),
Document( Document(
"\$lt", listOf( "\$lte", listOf(
"\$date", "\$date",
Date.from( Date.from(
LocalDateTime.of(dateTo, LocalTime.MIN) LocalDateTime.of(dateTo, LocalTime.MIN)
@@ -339,7 +348,7 @@ class CategoryService(
currentPlanned = document["plannedAmount"] as Double, currentPlanned = document["plannedAmount"] as Double,
category = Category( category = Category(
document["_id"].toString(), document["_id"].toString(),
CategoryType(catType["code"] as String, catType["name"] as String), type = CategoryType(catType["code"] as String, catType["name"] as String),
name = document["name"] as String, name = document["name"] as String,
description = document["description"] as String, description = document["description"] as String,
icon = document["icon"] as String icon = document["icon"] as String
@@ -351,4 +360,190 @@ class CategoryService(
} }
fun getCategorySumsPipeline(dateFrom: LocalDate, dateTo: LocalDate): Mono<List<Document>> {
val pipeline = listOf(
Document(
"\$lookup",
Document("from", "categories")
.append("localField", "category.\$id")
.append("foreignField", "_id")
.append("as", "categoryDetails")
),
Document("\$unwind", "\$categoryDetails"),
Document(
"\$match",
Document(
"date",
Document(
"\$gte", Date.from(
LocalDateTime.of(dateTo, LocalTime.MIN)
.atZone(ZoneId.systemDefault())
.withZoneSameInstant(ZoneOffset.UTC).toInstant()
)
)
.append(
"\$lte", LocalDateTime.of(dateTo, LocalTime.MIN)
.atZone(ZoneId.systemDefault())
.withZoneSameInstant(ZoneOffset.UTC).toInstant()
)
)
),
Document(
"\$group",
Document(
"_id",
Document("categoryId", "\$categoryDetails._id")
.append("categoryName", "\$categoryDetails.name")
.append("year", Document("\$year", "\$date"))
.append("month", Document("\$month", "\$date"))
)
.append("totalAmount", Document("\$sum", "\$amount"))
),
Document(
"\$group",
Document("_id", "\$_id.categoryId")
.append("categoryName", Document("\$first", "\$_id.categoryName"))
.append(
"monthlyData",
Document(
"\$push",
Document(
"month",
Document(
"\$concat", listOf(
Document("\$toString", "\$_id.year"), "-",
Document(
"\$cond", listOf(
Document("\$lt", listOf("\$_id.month", 10L)),
Document(
"\$concat", listOf(
"0",
Document("\$toString", "\$_id.month")
)
),
Document("\$toString", "\$_id.month")
)
)
)
)
)
.append("totalAmount", "\$totalAmount")
)
)
),
Document(
"\$addFields",
Document(
"completeMonthlyData",
Document(
"\$map",
Document("input", Document("\$range", listOf(0L, 6L)))
.append("as", "offset")
.append(
"in",
Document(
"month",
Document(
"\$dateToString",
Document("format", "%Y-%m")
.append(
"date",
Document(
"\$dateAdd",
Document("startDate", java.util.Date(1754006400000L))
.append("unit", "month")
.append(
"amount",
Document("\$multiply", listOf("\$\$offset", 1L))
)
)
)
)
)
.append(
"totalAmount",
Document(
"\$let",
Document(
"vars",
Document(
"matched",
Document(
"\$arrayElemAt", listOf(
Document(
"\$filter",
Document("input", "\$monthlyData")
.append("as", "data")
.append(
"cond",
Document(
"\$eq", listOf(
"\$\$data.month",
Document(
"\$dateToString",
Document("format", "%Y-%m")
.append(
"date",
Document(
"\$dateAdd",
Document(
"startDate",
java.util.Date(
1733011200000L
)
)
.append(
"unit",
"month"
)
.append(
"amount",
Document(
"\$multiply",
listOf(
"\$\$offset",
1L
)
)
)
)
)
)
)
)
)
), 0L
)
)
)
)
.append(
"in",
Document("\$ifNull", listOf("\$\$matched.totalAmount", 0L))
)
)
)
)
)
)
),
Document(
"\$project",
Document("_id", 0L)
.append("categoryId", "\$_id")
.append("categoryName", "\$categoryName")
.append("monthlyData", "\$completeMonthlyData")
)
)
return mongoTemplate.getCollection("transactions")
.flatMapMany { it.aggregate(pipeline) }
.map {
it["categoryId"] = it["categoryId"].toString()
it
}
.collectList()
}
} }

View File

@@ -0,0 +1,27 @@
spring.application.name=budger-app
spring.data.mongodb.host=localhost
spring.data.mongodb.port=27018
spring.data.mongodb.database=budger-app
#spring.data.mongodb.username=budger-app
#spring.data.mongodb.password=BA1q2w3e4r!
#spring.data.mongodb.authentication-database=admin
management.endpoints.web.exposure.include=*
management.endpoint.health.show-details=always
spring.datasource.url=jdbc:postgresql://213.183.51.243/familybudget_app
spring.datasource.username=familybudget_app
spring.datasource.password=FB1q2w3e4r!
# ??????? JDBC
spring.datasource.driver-class-name=org.postgresql.Driver
# ????????? Hibernate
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.hibernate.ddl-auto=none

View File

@@ -1,11 +1,11 @@
spring.application.name=budger-app spring.application.name=budger-app
spring.data.mongodb.host=213.226.71.138 spring.data.mongodb.host=77.222.32.64
spring.data.mongodb.port=27017 spring.data.mongodb.port=27017
spring.data.mongodb.database=budger-app spring.data.mongodb.database=budger-app
spring.data.mongodb.username=budger-app #spring.data.mongodb.username=budger-app
spring.data.mongodb.password=BA1q2w3e4r! #spring.data.mongodb.password=BA1q2w3e4r!
spring.data.mongodb.authentication-database=admin #spring.data.mongodb.authentication-database=admin
management.endpoints.web.exposure.include=* management.endpoints.web.exposure.include=*

View File

@@ -1,25 +1,20 @@
spring.application.name=budger-app spring.application.name=budger-app
spring.data.mongodb.host=127.0.0.1
spring.data.mongodb.port=27017 spring.data.mongodb.uri=mongodb://budger-app:BA1q2w3e4r!@luminic.space:27017/budger-app?authSource=admin&minPoolSize=10&maxPoolSize=100
spring.data.mongodb.database=budger-app
spring.data.mongodb.username=budger-app
spring.data.mongodb.password=BA1q2w3e4r!
spring.data.mongodb.authentication-database=admin
spring.datasource.url=jdbc:postgresql://213.183.51.243/familybudget_app logging.level.org.springframework.web=DEBUG
spring.datasource.username=familybudget_app logging.level.org.springframework.data = DEBUG
spring.datasource.password=FB1q2w3e4r! logging.level.org.springframework.data.mongodb.core.ReactiveMongoTemplate=DEBUG
logging.level.org.springframework.security = DEBUG
logging.level.org.springframework.data.mongodb.code = DEBUG
logging.level.org.springframework.web.reactive=DEBUG
#management.endpoints.web.exposure.include=*
#management.endpoint.metrics.access=read_only
logging.level.org.springframework.web=INFO
logging.level.org.springframework.data = INFO
logging.level.org.springframework.data.mongodb.core.ReactiveMongoTemplate=INFO
logging.level.org.springframework.security = INFO
logging.level.org.springframework.data.mongodb.code = INFO
logging.level.org.springframework.web.reactive=INFO<EFBFBD>

View File

@@ -4,7 +4,7 @@ server.port=8082
#server.servlet.context-path=/api #server.servlet.context-path=/api
spring.webflux.base-path=/api spring.webflux.base-path=/api
spring.profiles.active=dev spring.profiles.active=prod
spring.main.web-application-type=reactive spring.main.web-application-type=reactive
@@ -20,10 +20,10 @@ server.compression.enabled=true
server.compression.mime-types=application/json server.compression.mime-types=application/json
# ??????? JDBC # Expose prometheus, health, and info endpoints
spring.datasource.driver-class-name=org.postgresql.Driver #management.endpoints.web.exposure.include=prometheus,health,info
management.endpoints.web.exposure.include=*
# ????????? Hibernate # Enable Prometheus metrics export
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect management.prometheus.metrics.export.enabled=true
spring.jpa.hibernate.ddl-auto=none