diff --git a/src/main/kotlin/space/luminic/budgerapp/configs/BearerTokenFilter.kt b/src/main/kotlin/space/luminic/budgerapp/configs/BearerTokenFilter.kt index 842421a..47ce400 100644 --- a/src/main/kotlin/space/luminic/budgerapp/configs/BearerTokenFilter.kt +++ b/src/main/kotlin/space/luminic/budgerapp/configs/BearerTokenFilter.kt @@ -26,7 +26,6 @@ class BearerTokenFilter(private val authService: AuthService) : SecurityContextS override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono { val token = exchange.request.headers.getFirst(HttpHeaders.AUTHORIZATION)?.removePrefix("Bearer ") - logger.info("here ${exchange.request.path.value()}") if (exchange.request.path.value() == "/api/auth/login" || exchange.request.path.value() .startsWith("/api/actuator") ) { diff --git a/src/main/kotlin/space/luminic/budgerapp/controllers/BudgetController.kt b/src/main/kotlin/space/luminic/budgerapp/controllers/BudgetController.kt index 9ace580..4b3ca47 100644 --- a/src/main/kotlin/space/luminic/budgerapp/controllers/BudgetController.kt +++ b/src/main/kotlin/space/luminic/budgerapp/controllers/BudgetController.kt @@ -16,49 +16,49 @@ import space.luminic.budgerapp.models.Budget import space.luminic.budgerapp.models.BudgetDTO import space.luminic.budgerapp.models.Transaction import space.luminic.budgerapp.models.Warn -import space.luminic.budgerapp.services.BudgetService + +import space.luminic.budgerapp.services.FinancialService import java.time.LocalDate @RestController @RequestMapping("/budgets") class BudgetController( - val budgetService: BudgetService + val financialService: FinancialService ) { private val logger = LoggerFactory.getLogger(BudgetController::class.java) @GetMapping fun getBudgets(): Mono> { - return budgetService.getBudgets() + return financialService.getBudgets() } @GetMapping("/{id}") fun getBudget(@PathVariable id: String): Mono { - logger.info("here }") - return budgetService.getBudget(id) + return financialService.getBudget(id) } @GetMapping("/by-dates") fun getBudgetByDate(@RequestParam date: LocalDate): ResponseEntity { - return ResponseEntity.ok(budgetService.getBudgetByDate(date)) + return ResponseEntity.ok(financialService.getBudgetByDate(date)) } @GetMapping("/{id}/categories") fun getBudgetCategories(@PathVariable id: String): ResponseEntity { - return ResponseEntity.ok(budgetService.getBudgetCategories(id)) + return ResponseEntity.ok(financialService.getBudgetCategories(id)) } @GetMapping("/{id}/transactions") fun getBudgetTransactions(@PathVariable id: String):Mono>> { - return budgetService.getBudgetTransactionsByType(id) + return financialService.getBudgetTransactionsByType(id) } @PostMapping("/") fun createBudget(@RequestBody budgetCreationDTO: BudgetCreationDTO): Mono { - return budgetService.createBudget( + return financialService.createBudget( budgetCreationDTO.budget, budgetCreationDTO.createRecurrent ) @@ -66,7 +66,7 @@ class BudgetController( @DeleteMapping("/{id}") fun deleteBudget(@PathVariable id: String): Mono { - return budgetService.deleteBudget(id) + return financialService.deleteBudget(id) } @PostMapping("/{budgetId}/categories/{catId}/limit") @@ -76,7 +76,7 @@ class BudgetController( @RequestBody limit: LimitValue, ): ResponseEntity { return try { - ResponseEntity.ok(budgetService.setCategoryLimit(budgetId, catId, limit.limit)) + ResponseEntity.ok(financialService.setCategoryLimit(budgetId, catId, limit.limit)) } catch (e: Exception) { ResponseEntity.badRequest().body(e.message) } @@ -86,14 +86,17 @@ class BudgetController( @GetMapping("/{id}/warns") fun budgetWarns(@PathVariable id: String, @RequestParam hidden: Boolean? = null): Mono> { - return budgetService.getWarns(id, hidden) + return financialService.getWarns(id, hidden) } @PostMapping("/{id}/warns/{warnId}/hide") fun setWarnHide(@PathVariable id: String, @PathVariable warnId: String): Mono { - return budgetService.hideWarn( warnId) - + return financialService.hideWarn( warnId) + } + @GetMapping("/regencats") + fun regenCats(): Mono{ + return financialService.regenCats() } data class LimitValue( diff --git a/src/main/kotlin/space/luminic/budgerapp/controllers/CategoriesController.kt b/src/main/kotlin/space/luminic/budgerapp/controllers/CategoriesController.kt index ca34976..8c47886 100644 --- a/src/main/kotlin/space/luminic/budgerapp/controllers/CategoriesController.kt +++ b/src/main/kotlin/space/luminic/budgerapp/controllers/CategoriesController.kt @@ -17,8 +17,8 @@ import org.springframework.web.client.HttpClientErrorException import reactor.core.publisher.Mono import space.luminic.budgerapp.models.BudgetCategory import space.luminic.budgerapp.models.Category -import space.luminic.budgerapp.services.BudgetService import space.luminic.budgerapp.services.CategoryService +import space.luminic.budgerapp.services.FinancialService import java.time.LocalDate @@ -26,7 +26,7 @@ import java.time.LocalDate @RequestMapping("/categories") class CategoriesController( private val categoryService: CategoryService, - private val budgetService: BudgetService + private val financialService: FinancialService ) { @@ -72,19 +72,19 @@ class CategoriesController( var dateFrom = LocalDate.parse("2025-01-10") var dateTo = LocalDate.parse("2025-02-09") - return categoryService.getCategoryTransactionPipeline(dateFrom, dateTo) + return financialService.getCategoryTransactionPipeline(dateFrom, dateTo) } @GetMapping("/by-month") fun getCategoriesSumsByMonths(): Mono> { - return categoryService.getCategorySumsPipeline(LocalDate.of(2024, 8, 1), LocalDate.of(2025, 1, 12)) + return financialService.getCategorySumsPipeline(LocalDate.of(2024, 8, 1), LocalDate.of(2025, 1, 12)) } @GetMapping("/by-month2") fun getCategoriesSumsByMonthsV2(): Mono> { - return categoryService.getCategorySummaries(LocalDate.now().minusMonths(6)) + return financialService.getCategorySummaries(LocalDate.now().minusMonths(6)) } diff --git a/src/main/kotlin/space/luminic/budgerapp/controllers/TransactionController.kt b/src/main/kotlin/space/luminic/budgerapp/controllers/TransactionController.kt index 90334d4..5c261d8 100644 --- a/src/main/kotlin/space/luminic/budgerapp/controllers/TransactionController.kt +++ b/src/main/kotlin/space/luminic/budgerapp/controllers/TransactionController.kt @@ -14,13 +14,14 @@ import org.springframework.web.bind.annotation.RestController import org.springframework.web.client.HttpClientErrorException import reactor.core.publisher.Mono import space.luminic.budgerapp.models.Transaction -import space.luminic.budgerapp.services.BudgetService -import space.luminic.budgerapp.services.TransactionService + +import space.luminic.budgerapp.services.FinancialService + @RestController @RequestMapping("/transactions") -class TransactionController(private val transactionService: TransactionService) { +class TransactionController(private val financialService: FinancialService) { @GetMapping @@ -34,7 +35,7 @@ class TransactionController(private val transactionService: TransactionService) ): ResponseEntity { try { return ResponseEntity.ok( - transactionService.getTransactions( + financialService.getTransactions( transactionType = transactionType, categoryType = categoryType, userId = userId, @@ -52,7 +53,7 @@ class TransactionController(private val transactionService: TransactionService) @GetMapping("/{id}") fun getTransaction(@PathVariable id: String): ResponseEntity { try { - return ResponseEntity.ok(transactionService.getTransactionById(id)) + return ResponseEntity.ok(financialService.getTransactionById(id)) } catch (e: Exception) { e.printStackTrace() return ResponseEntity(e.message, HttpStatus.INTERNAL_SERVER_ERROR) @@ -62,7 +63,7 @@ class TransactionController(private val transactionService: TransactionService) @PostMapping fun createTransaction(@RequestBody transaction: Transaction): ResponseEntity { try { - return ResponseEntity.ok(transactionService.createTransaction(transaction)) + return ResponseEntity.ok(financialService.createTransaction(transaction)) } catch (e: Exception) { e.printStackTrace() return ResponseEntity(e.message, HttpStatus.INTERNAL_SERVER_ERROR) @@ -73,7 +74,7 @@ class TransactionController(private val transactionService: TransactionService) @PutMapping("/{id}") fun editTransaction(@PathVariable id: String, @RequestBody transaction: Transaction): ResponseEntity { try { - return ResponseEntity.ok(transactionService.editTransaction(transaction)) + return ResponseEntity.ok(financialService.editTransaction(transaction)) } catch (e: Exception) { e.printStackTrace() return ResponseEntity(e.message, HttpStatus.INTERNAL_SERVER_ERROR) @@ -83,32 +84,30 @@ class TransactionController(private val transactionService: TransactionService) @DeleteMapping("/{id}") fun deleteTransaction(@PathVariable id: String): Mono { - return transactionService.deleteTransaction(id) + return financialService.deleteTransaction(id) } @GetMapping("/{id}/child") fun getChildTransactions(@PathVariable id: String): ResponseEntity { - return ResponseEntity.ok(transactionService.getChildTransaction(id)) + return ResponseEntity.ok(financialService.getChildTransaction(id)) } @GetMapping("/avg-by-category") fun getAvgSums(): ResponseEntity { - return ResponseEntity.ok(transactionService.getAverageSpendingByCategory()) + return ResponseEntity.ok(financialService.getAverageSpendingByCategory()) } - @GetMapping("/types") fun getTypes(): ResponseEntity { return try { - ResponseEntity.ok(transactionService.getTransactionTypes()) + ResponseEntity.ok(financialService.getTransactionTypes()) } catch (e: Exception) { ResponseEntity(HttpClientErrorException(HttpStatus.INTERNAL_SERVER_ERROR), HttpStatus.INTERNAL_SERVER_ERROR) } } - } \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/budgerapp/models/Budget.kt b/src/main/kotlin/space/luminic/budgerapp/models/Budget.kt index 7d71ed4..0ed8e67 100644 --- a/src/main/kotlin/space/luminic/budgerapp/models/Budget.kt +++ b/src/main/kotlin/space/luminic/budgerapp/models/Budget.kt @@ -17,6 +17,7 @@ data class BudgetDTO( var plannedExpenses: MutableList? = null, var plannedIncomes: MutableList? = null, var categories: MutableList = mutableListOf(), + var incomeCategories: MutableList = mutableListOf(), var transactions: MutableList? = null, ) @@ -30,6 +31,7 @@ data class Budget( var dateTo: LocalDate, val createdAt: LocalDateTime = LocalDateTime.now(), var categories: MutableList = mutableListOf(), + var incomeCategories: MutableList = mutableListOf(), ) data class BudgetCategory( diff --git a/src/main/kotlin/space/luminic/budgerapp/services/BudgetService.kt b/src/main/kotlin/space/luminic/budgerapp/services/BudgetService.kt index a240c35..06e97c6 100644 --- a/src/main/kotlin/space/luminic/budgerapp/services/BudgetService.kt +++ b/src/main/kotlin/space/luminic/budgerapp/services/BudgetService.kt @@ -1,547 +1,533 @@ -package space.luminic.budgerapp.services - - - -import org.slf4j.LoggerFactory -import org.springframework.cache.annotation.CacheEvict -import org.springframework.cache.annotation.Cacheable -import org.springframework.context.event.EventListener -import org.springframework.data.domain.Sort -import org.springframework.data.mongodb.core.ReactiveMongoTemplate -import org.springframework.data.mongodb.core.aggregation.Aggregation.group -import org.springframework.data.mongodb.core.aggregation.Aggregation.lookup -import org.springframework.data.mongodb.core.aggregation.Aggregation.match -import org.springframework.data.mongodb.core.aggregation.Aggregation.newAggregation -import org.springframework.data.mongodb.core.aggregation.Aggregation.project -import org.springframework.data.mongodb.core.aggregation.Aggregation.sort -import org.springframework.data.mongodb.core.aggregation.Aggregation.unwind -import org.springframework.data.mongodb.core.query.Criteria -import org.springframework.stereotype.Service -import reactor.core.publisher.Flux -import reactor.core.publisher.Mono -import space.luminic.budgerapp.models.Budget -import space.luminic.budgerapp.models.BudgetCategory -import space.luminic.budgerapp.models.BudgetDTO -import space.luminic.budgerapp.models.BudgetNotFoundException -import space.luminic.budgerapp.models.PushMessage -import space.luminic.budgerapp.models.SortSetting -import space.luminic.budgerapp.models.Transaction -import space.luminic.budgerapp.models.TransactionEvent -import space.luminic.budgerapp.models.TransactionEventType -import space.luminic.budgerapp.models.Warn -import space.luminic.budgerapp.models.WarnSerenity -import space.luminic.budgerapp.repos.BudgetRepo -import space.luminic.budgerapp.repos.WarnRepo -import java.time.LocalDate -import java.util.Optional -import kotlin.collections.get - -@Service -class BudgetService( - val budgetRepo: BudgetRepo, - val warnRepo: WarnRepo, - val transactionService: TransactionService, - val recurrentService: RecurrentService, - val categoryService: CategoryService, - val reactiveMongoTemplate: ReactiveMongoTemplate -) { - - private val logger = LoggerFactory.getLogger(BudgetService::class.java) - - - @EventListener - @CacheEvict(cacheNames = ["budgets"], allEntries = true) - fun handleTransactionEvent(event: TransactionEvent) { - logger.info("Got ${event.eventType} event on transaction ${event.newTransaction.id}") - if (event.newTransaction.category.type.code == "EXPENSE") { - when (event.eventType) { - TransactionEventType.EDIT -> updateBudgetOnEdit(event) - TransactionEventType.CREATE -> updateBudgetOnCreate(event) - TransactionEventType.DELETE -> updateBudgetOnDelete(event) - } - } - -// runBlocking(Dispatchers.IO) { -// updateBudgetWarns( -// budgetRepo.findByDateFromLessThanEqualAndDateToGreaterThan( -// event.newTransaction.date, event.newTransaction.date. -// ).map{it} +//package space.luminic.budgerapp.services +// +// +// +//import org.slf4j.LoggerFactory +//import org.springframework.cache.annotation.CacheEvict +//import org.springframework.cache.annotation.Cacheable +//import org.springframework.context.event.EventListener +//import org.springframework.data.domain.Sort +//import org.springframework.data.mongodb.core.ReactiveMongoTemplate +//import org.springframework.data.mongodb.core.aggregation.Aggregation.group +//import org.springframework.data.mongodb.core.aggregation.Aggregation.lookup +//import org.springframework.data.mongodb.core.aggregation.Aggregation.match +//import org.springframework.data.mongodb.core.aggregation.Aggregation.newAggregation +//import org.springframework.data.mongodb.core.aggregation.Aggregation.project +//import org.springframework.data.mongodb.core.aggregation.Aggregation.sort +//import org.springframework.data.mongodb.core.aggregation.Aggregation.unwind +//import org.springframework.data.mongodb.core.query.Criteria +//import org.springframework.stereotype.Service +//import reactor.core.publisher.Flux +//import reactor.core.publisher.Mono +//import space.luminic.budgerapp.models.Budget +//import space.luminic.budgerapp.models.BudgetCategory +//import space.luminic.budgerapp.models.BudgetDTO +//import space.luminic.budgerapp.models.BudgetNotFoundException +//import space.luminic.budgerapp.models.PushMessage +//import space.luminic.budgerapp.models.SortSetting +//import space.luminic.budgerapp.models.Transaction +//import space.luminic.budgerapp.models.TransactionEvent +//import space.luminic.budgerapp.models.TransactionEventType +//import space.luminic.budgerapp.models.Warn +//import space.luminic.budgerapp.models.WarnSerenity +//import space.luminic.budgerapp.repos.BudgetRepo +//import space.luminic.budgerapp.repos.WarnRepo +//import java.time.LocalDate +//import java.util.Optional +//import kotlin.collections.get +// +//@Service +//class BudgetService( +// val budgetRepo: BudgetRepo, +// val warnRepo: WarnRepo, +// val transactionService: TransactionService, +// val recurrentService: RecurrentService, +// val categoryService: CategoryService, +// val reactiveMongoTemplate: ReactiveMongoTemplate +//) { +// +// private val logger = LoggerFactory.getLogger(BudgetService::class.java) +// +// +// @EventListener +// @CacheEvict(cacheNames = ["budgets"], allEntries = true) +// fun handleTransactionEvent(event: TransactionEvent) { +// logger.info("Got ${event.eventType} event on transaction ${event.newTransaction.id}") +// if (event.newTransaction.category.type.code == "EXPENSE") { +// when (event.eventType) { +// TransactionEventType.EDIT -> updateBudgetOnEdit(event) +// TransactionEventType.CREATE -> updateBudgetOnCreate(event) +// TransactionEventType.DELETE -> updateBudgetOnDelete(event) +// } +// } +// } +// +// fun updateBudgetOnCreate(event: TransactionEvent) { +// budgetRepo.findByDateFromLessThanEqualAndDateToGreaterThanEqual( +// event.newTransaction.date, event.newTransaction.date +// ).flatMap { budget -> +// val categories = categoryService.getBudgetCategories(budget.dateFrom, budget.dateTo) +// logger.info(categories.toString()) +// categories.flatMap { categories -> +// val updatedCategories = when (event.newTransaction.type.code) { +// "PLANNED" -> Flux.fromIterable(budget.categories) +// .map { category -> +// categories[category.category.id]?.let { data -> +// if (category.category.id == event.newTransaction.category.id) { +// category.currentSpent = data["instantAmount"] ?: 0.0 +// category.currentPlanned = data["plannedAmount"] ?: 0.0 +// category.currentLimit += event.newTransaction.amount +// } +// } +// category +// }.collectList() +// +// "INSTANT" -> Flux.fromIterable(budget.categories) +// .map { category -> +// categories[category.category.id]?.let { data -> +// if (category.category.id == event.newTransaction.category.id) { +// category.currentSpent = data["instantAmount"] ?: 0.0 +// category.currentPlanned = data["plannedAmount"] ?: 0.0 +// } +// } +// category +// }.collectList() +// +// else -> Mono.just(budget.categories) // Добавляем обработку типа по умолчанию +// } +// +// updatedCategories.flatMap { updated -> +// budget.categories = updated +// budgetRepo.save(budget) // Сохраняем обновленный бюджет +// } +// } +// }.then() // Гарантируем завершение +// .subscribe() // Запускаем выполнение +// } +// +// +// fun updateBudgetOnEdit(event: TransactionEvent) { +// budgetRepo.findByDateFromLessThanEqualAndDateToGreaterThanEqual( +// event.oldTransaction.date, event.oldTransaction.date +// ).switchIfEmpty( +// Mono.error(BudgetNotFoundException("old budget cannot be null")) +// ).then().subscribe() +// +// budgetRepo.findByDateFromLessThanEqualAndDateToGreaterThanEqual( +// event.newTransaction.date, event.newTransaction.date +// ).flatMap { budget -> +// categoryService.getBudgetCategories(budget.dateFrom, budget.dateTo).flatMap { categories -> +// val updatedCategories = when (event.newTransaction.type.code) { +// "PLANNED" -> Flux.fromIterable(budget.categories) +// .map { category -> +// if (category.category.id == event.newTransaction.category.id) { +// categories[category.category.id]?.let { data -> +// category.currentSpent = data["instantAmount"] ?: 0.0 +// category.currentPlanned = data["plannedAmount"] ?: 0.0 +// category.currentLimit += event.difference!! +// } +// } +// +// category +// }.collectList() +// +// "INSTANT" -> Flux.fromIterable(budget.categories) +// .map { category -> +// categories[category.category.id]?.let { data -> +// if (category.category.id == event.newTransaction.category.id) { +// category.currentSpent = data["instantAmount"] ?: 0.0 +// category.currentPlanned = data["plannedAmount"] ?: 0.0 +// } +// } +// category +// }.collectList() +// +// else -> Mono.just(budget.categories) // Добавляем обработку типа по умолчанию +// } +// +// updatedCategories.flatMap { updated -> +// budget.categories = updated +// budgetRepo.save(budget) // Сохраняем обновленный бюджет +// } +// } +// }.then() // Гарантируем завершение +// .subscribe() // Запускаем выполнение +// } +// +// +// fun updateBudgetOnDelete(event: TransactionEvent) { +// budgetRepo.findByDateFromLessThanEqualAndDateToGreaterThanEqual( +// event.newTransaction.date, event.newTransaction.date +// ).flatMap { budget -> +// categoryService.getBudgetCategories(budget.dateFrom, budget.dateTo).flatMap { categories -> +// val updatedCategories = when (event.newTransaction.type.code) { +// "PLANNED" -> Flux.fromIterable(budget.categories) +// .map { category -> +// categories[category.category.id]?.let { data -> +// if (category.category.id == event.newTransaction.category.id) { +// category.currentSpent = data["instantAmount"] ?: 0.0 +// category.currentPlanned = data["plannedAmount"] ?: 0.0 +// category.currentLimit -= event.newTransaction.amount +// } +// } +// category +// }.collectList() +// +// "INSTANT" -> Flux.fromIterable(budget.categories) +// .map { category -> +// categories[category.category.id]?.let { data -> +// if (category.category.id == event.newTransaction.category.id) { +// category.currentSpent = data["instantAmount"] ?: 0.0 +// category.currentPlanned = data["plannedAmount"] ?: 0.0 +// } +// } +// category +// }.collectList() +// +// else -> Mono.just(budget.categories) // Добавляем обработку типа по умолчанию +// } +// +// updatedCategories.flatMap { updated -> +// budget.categories = updated +// budgetRepo.save(budget) // Сохраняем обновленный бюджет +// } +// } +// }.then() // Гарантируем завершение +// .subscribe() // Запускаем выполнение +// } +// +// +// @Cacheable("budgetsList") +// fun getBudgets(sortSetting: SortSetting? = null): Mono> { +// val sort = if (sortSetting != null) { +// Sort.by(sortSetting.order, sortSetting.by) +// } else { +// Sort.by(Sort.Direction.DESC, "dateFrom") +// } +// +// return budgetRepo.findAll(sort) +// .collectList() // Сбор Flux в Mono> +// } +// +// +// // @Cacheable("budgets", key = "#id") +// fun getBudget(id: String): Mono { +// return budgetRepo.findById(id) +// .flatMap { budget -> +// val budgetDTO = BudgetDTO( +// budget.id, +// budget.name, +// budget.dateFrom, +// budget.dateTo, +// budget.createdAt, +// categories = budget.categories +// ) +// +// logger.info("Fetching categories and transactions") +// val categoriesMono = categoryService.getBudgetCategories(budgetDTO.dateFrom, budgetDTO.dateTo) +// val transactionsMono = +// transactionService.getTransactionsByTypes(budgetDTO.dateFrom, budgetDTO.dateTo) +// +// +// Mono.zip(categoriesMono, transactionsMono) +// .flatMap { tuple -> +// val categories = tuple.t1 +// val transactions = tuple.t2 +// +// +// Flux.fromIterable(budgetDTO.categories) +// .map { category -> +// categories[category.category.id]?.let { data -> +// category.currentSpent = data["instantAmount"] ?: 0.0 +// category.currentPlanned = data["plannedAmount"] ?: 0.0 +// } +// category +// } +// .collectList() +// .map { updatedCategories -> +// budgetDTO.categories = updatedCategories +// budgetDTO.plannedExpenses = transactions["plannedExpenses"] as MutableList +// budgetDTO.plannedIncomes = transactions["plannedIncomes"] as MutableList +// budgetDTO.transactions = transactions["instantTransactions"] as MutableList +// logger.info("here e") +// +// budgetDTO +// } +// } +// } +// .doOnError { error -> +// logger.error("Error fetching budget: ${error.message}", error) +// } +// .switchIfEmpty(Mono.error(BudgetNotFoundException("Budget not found with id: $id"))) +// } +// +// +// @CacheEvict(cacheNames = ["budgets", "budgetsList"], allEntries = true) +// fun createBudget(budget: Budget, createRecurrent: Boolean): Mono { +// return Mono.zip( +// getBudgetByDate(budget.dateFrom).map { Optional.ofNullable(it) } +// .switchIfEmpty(Mono.just(Optional.empty())), +// getBudgetByDate(budget.dateTo).map { Optional.ofNullable(it) } +// .switchIfEmpty(Mono.just(Optional.empty())) +// ).flatMap { tuple -> +// val startBudget = tuple.t1.orElse(null) +// val endBudget = tuple.t2.orElse(null) +// +// // Проверяем, пересекаются ли бюджеты по датам +// if (startBudget != null || endBudget != null) { +// return@flatMap Mono.error(IllegalArgumentException("Бюджет с теми же датами найден")) +// } +// +// // Если createRecurrent=true, создаем рекуррентные транзакции +// val recurrentsCreation = if (createRecurrent) { +// recurrentService.createRecurrentsForBudget(budget) +// } else { +// Mono.empty() +// } +// +// // Создаем бюджет после возможного создания рекуррентных транзакций +// recurrentsCreation.then( +// categoryService.getCategoryTransactionPipeline(budget.dateFrom, budget.dateTo) +// .flatMap { categories -> +// budget.categories = categories +// budgetRepo.save(budget) +// } +// .publishOn(reactor.core.scheduler.Schedulers.boundedElastic()) +// .doOnNext { savedBudget -> +// // Выполнение updateBudgetWarns в фоне +// updateBudgetWarns(budget = savedBudget) +// .doOnError { error -> +// // Логируем ошибку, если произошла +// println("Error during updateBudgetWarns: ${error.message}") +// } +// .subscribe() +// } // ) // } - } - - fun updateBudgetOnCreate(event: TransactionEvent) { - budgetRepo.findByDateFromLessThanEqualAndDateToGreaterThanEqual( - event.newTransaction.date, event.newTransaction.date - ).flatMap { budget -> - val categories = categoryService.getBudgetCategories(budget.dateFrom, budget.dateTo) - logger.info(categories.toString()) - categories.flatMap { categories -> - val updatedCategories = when (event.newTransaction.type.code) { - "PLANNED" -> Flux.fromIterable(budget.categories) - .map { category -> - categories[category.category.id]?.let { data -> - if (category.category.id == event.newTransaction.category.id) { - category.currentSpent = data["instantAmount"] ?: 0.0 - category.currentPlanned = data["plannedAmount"] ?: 0.0 - category.currentLimit += event.newTransaction.amount - } - } - category - }.collectList() - - "INSTANT" -> Flux.fromIterable(budget.categories) - .map { category -> - categories[category.category.id]?.let { data -> - if (category.category.id == event.newTransaction.category.id) { - category.currentSpent = data["instantAmount"] ?: 0.0 - category.currentPlanned = data["plannedAmount"] ?: 0.0 - } - } - category - }.collectList() - - else -> Mono.just(budget.categories) // Добавляем обработку типа по умолчанию - } - - updatedCategories.flatMap { updated -> - budget.categories = updated - budgetRepo.save(budget) // Сохраняем обновленный бюджет - } - } - }.then() // Гарантируем завершение - .subscribe() // Запускаем выполнение - } - - - fun updateBudgetOnEdit(event: TransactionEvent) { - budgetRepo.findByDateFromLessThanEqualAndDateToGreaterThanEqual( - event.oldTransaction.date, event.oldTransaction.date - ).switchIfEmpty( - Mono.error(BudgetNotFoundException("old budget cannot be null")) - ).then().subscribe() - - budgetRepo.findByDateFromLessThanEqualAndDateToGreaterThanEqual( - event.newTransaction.date, event.newTransaction.date - ).flatMap { budget -> - categoryService.getBudgetCategories(budget.dateFrom, budget.dateTo).flatMap { categories -> - val updatedCategories = when (event.newTransaction.type.code) { - "PLANNED" -> Flux.fromIterable(budget.categories) - .map { category -> - if (category.category.id == event.newTransaction.category.id) { - categories[category.category.id]?.let { data -> - category.currentSpent = data["instantAmount"] ?: 0.0 - category.currentPlanned = data["plannedAmount"] ?: 0.0 - category.currentLimit += event.difference!! - } - } - - category - }.collectList() - - "INSTANT" -> Flux.fromIterable(budget.categories) - .map { category -> - categories[category.category.id]?.let { data -> - if (category.category.id == event.newTransaction.category.id) { - category.currentSpent = data["instantAmount"] ?: 0.0 - category.currentPlanned = data["plannedAmount"] ?: 0.0 - } - } - category - }.collectList() - - else -> Mono.just(budget.categories) // Добавляем обработку типа по умолчанию - } - - updatedCategories.flatMap { updated -> - budget.categories = updated - budgetRepo.save(budget) // Сохраняем обновленный бюджет - } - } - }.then() // Гарантируем завершение - .subscribe() // Запускаем выполнение - } - - - fun updateBudgetOnDelete(event: TransactionEvent) { - budgetRepo.findByDateFromLessThanEqualAndDateToGreaterThanEqual( - event.newTransaction.date, event.newTransaction.date - ).flatMap { budget -> - categoryService.getBudgetCategories(budget.dateFrom, budget.dateTo).flatMap { categories -> - val updatedCategories = when (event.newTransaction.type.code) { - "PLANNED" -> Flux.fromIterable(budget.categories) - .map { category -> - categories[category.category.id]?.let { data -> - if (category.category.id == event.newTransaction.category.id) { - category.currentSpent = data["instantAmount"] ?: 0.0 - category.currentPlanned = data["plannedAmount"] ?: 0.0 - category.currentLimit += event.newTransaction.amount - } - } - category - }.collectList() - - "INSTANT" -> Flux.fromIterable(budget.categories) - .map { category -> - categories[category.category.id]?.let { data -> - if (category.category.id == event.newTransaction.category.id) { - category.currentSpent = data["instantAmount"] ?: 0.0 - category.currentPlanned = data["plannedAmount"] ?: 0.0 - } - } - category - }.collectList() - - else -> Mono.just(budget.categories) // Добавляем обработку типа по умолчанию - } - - updatedCategories.flatMap { updated -> - budget.categories = updated - budgetRepo.save(budget) // Сохраняем обновленный бюджет - } - } - }.then() // Гарантируем завершение - .subscribe() // Запускаем выполнение - } - - - @Cacheable("budgetsList") - fun getBudgets(sortSetting: SortSetting? = null): Mono> { - val sort = if (sortSetting != null) { - Sort.by(sortSetting.order, sortSetting.by) - } else { - Sort.by(Sort.Direction.DESC, "dateFrom") - } - - return budgetRepo.findAll(sort) - .collectList() // Сбор Flux в Mono> - } - - - // @Cacheable("budgets", key = "#id") - fun getBudget(id: String): Mono { - logger.info("here b") - return budgetRepo.findById(id) - .flatMap { budget -> - val budgetDTO = BudgetDTO( - budget.id, - budget.name, - budget.dateFrom, - budget.dateTo, - budget.createdAt, - categories = budget.categories - ) - - logger.info("Fetching categories and transactions") - val categoriesMono = categoryService.getBudgetCategories(budgetDTO.dateFrom, budgetDTO.dateTo) - val transactionsMono = - transactionService.getTransactionsByTypes(budgetDTO.dateFrom, budgetDTO.dateTo) - - - Mono.zip(categoriesMono, transactionsMono) - .flatMap { tuple -> - val categories = tuple.t1 - val transactions = tuple.t2 - - - Flux.fromIterable(budgetDTO.categories) - .map { category -> - categories[category.category.id]?.let { data -> - category.currentSpent = data["instantAmount"] ?: 0.0 - category.currentPlanned = data["plannedAmount"] ?: 0.0 - } - category - } - .collectList() - .map { updatedCategories -> - budgetDTO.categories = updatedCategories - budgetDTO.plannedExpenses = transactions["plannedExpenses"] as MutableList - budgetDTO.plannedIncomes = transactions["plannedIncomes"] as MutableList - budgetDTO.transactions = transactions["instantTransactions"] as MutableList - logger.info("here e") - - budgetDTO - } - } - } - .doOnError { error -> - logger.error("Error fetching budget: ${error.message}", error) - } - .switchIfEmpty(Mono.error(BudgetNotFoundException("Budget not found with id: $id"))) - } - - - @CacheEvict(cacheNames = ["budgets", "budgetsList"], allEntries = true) - fun createBudget(budget: Budget, createRecurrent: Boolean): Mono { - return Mono.zip( - getBudgetByDate(budget.dateFrom).map { Optional.ofNullable(it) } - .switchIfEmpty(Mono.just(Optional.empty())), - getBudgetByDate(budget.dateTo).map { Optional.ofNullable(it) } - .switchIfEmpty(Mono.just(Optional.empty())) - ).flatMap { tuple -> - val startBudget = tuple.t1.orElse(null) - val endBudget = tuple.t2.orElse(null) - - // Проверяем, пересекаются ли бюджеты по датам - if (startBudget != null || endBudget != null) { - return@flatMap Mono.error(IllegalArgumentException("Бюджет с теми же датами найден")) - } - - // Если createRecurrent=true, создаем рекуррентные транзакции - val recurrentsCreation = if (createRecurrent) { - recurrentService.createRecurrentsForBudget(budget) - } else { - Mono.empty() - } - - // Создаем бюджет после возможного создания рекуррентных транзакций - recurrentsCreation.then( - categoryService.getCategoryTransactionPipeline(budget.dateFrom, budget.dateTo) - .flatMap { categories -> - budget.categories = categories - budgetRepo.save(budget) - } - .publishOn(reactor.core.scheduler.Schedulers.boundedElastic()) - .doOnNext { savedBudget -> - // Выполнение updateBudgetWarns в фоне - updateBudgetWarns(budget = savedBudget) - .doOnError { error -> - // Логируем ошибку, если произошла - println("Error during updateBudgetWarns: ${error.message}") - } - .subscribe() - } - ) - } - } - - - fun getBudgetByDate(date: LocalDate): Mono { - return budgetRepo.findByDateFromLessThanEqualAndDateToGreaterThanEqual(date, date).switchIfEmpty(Mono.empty()) - } - -// fun getBudgetCategorySQL(id: Int): List? { -// var categories = budgetRepoSql.getBudgetCategory(id) -// for (category in categories) { -// categoryService.getCategoryByName(category.category.name)?.let { category.category = it } -// } -// return categories // } - - fun getBudgetCategories(id: String): Mono> { - return budgetRepo.findById(id).flatMap { budget -> - val lookup = lookup("categories", "category.\$id", "_id", "categoryDetailed") - val unwind = unwind("categoryDetailed") - val projectDouble = project("categoryDetailed", "amount", "date") - .andExpression("{ \$toDouble: \"\$amount\" }").`as`("amount") - val match = match(Criteria.where("date").gte(budget.dateFrom).lt(budget.dateTo)) - val group = group("categoryDetailed").sum("amount").`as`("currentSpent") - val project = project("currentSpent").and("_id").`as`("category") - val sort = sort(Sort.by(Sort.Order.asc("_id"))) - - val aggregation = newAggregation(lookup, unwind, projectDouble, match, group, project, sort) - - reactiveMongoTemplate.aggregate(aggregation, "transactions", BudgetCategory::class.java) - .collectList() // Преобразование результата в список - } - } - - - fun getBudgetTransactionsByType(budgetId: String): Mono>> { - return budgetRepo.findById(budgetId).flatMap { it -> - transactionService.getTransactionsByTypes(it.dateFrom, it.dateTo) - } - } - - - @CacheEvict(cacheNames = ["budgets", "budgetsList"], allEntries = true) - fun deleteBudget(budgetId: String): Mono { - return budgetRepo.findById(budgetId) - .switchIfEmpty(Mono.error(BudgetNotFoundException("Budget with id: $budgetId not found"))) - .flatMap { budget -> - transactionService.getTransactionsToDelete(budget.dateFrom, budget.dateTo) - .flatMapMany { transactions -> - Flux.fromIterable(transactions) - .flatMap { transaction -> - transactionService.deleteTransaction(transaction.id!!) - } - } - .then( - budgetRepo.delete(budget) - ) - } - } - - - @CacheEvict(cacheNames = ["budgets", "budgetsList"], allEntries = true) - fun setCategoryLimit(budgetId: String, catId: String, limit: Double): Mono { - return budgetRepo.findById(budgetId).flatMap { budget -> - val catEdit = budget.categories.firstOrNull { it.category?.id == catId } - ?: return@flatMap Mono.error(Exception("Category not found in the budget")) - - transactionService.calcTransactionsSum(budget, catId, "PLANNED").flatMap { catPlanned -> - if (catPlanned > limit) { - Mono.error(Exception("Limit can't be less than planned expenses on category. Current planned value: $catPlanned")) - } else { - catEdit.currentLimit = limit - budgetRepo.save(budget).flatMap { - updateBudgetWarns(it) - .thenReturn(catEdit) - } - } - } - } - } - - - fun getWarns(budgetId: String, isHide: Boolean? = null): Mono> { - return warnRepo.findAllByBudgetIdAndIsHide(budgetId, isHide == true).collectList() - } - - fun hideWarn(warnId: String): Mono { - return warnRepo.findById(warnId) // Ищем предупреждение - .flatMap { warn -> - warn.isHide = true // Обновляем поле - warnRepo.save(warn) // Сохраняем изменённое предупреждение - } - } - - - fun updateBudgetWarns(budget: Budget? = null): Mono> { - logger.info("STARTED WARNS UPDATE") - - val finalBudgetMono = budget?.let { Mono.just(it) } - ?: return Mono.just(emptyList()) - - return finalBudgetMono.flatMap { finalBudget -> - if (finalBudget.categories.isEmpty()) { - logger.info("No categories found for budget ${finalBudget.id}") - return@flatMap Mono.just(emptyList()) - } - - val averageSumsMono = transactionService.getAverageSpendingByCategory() - val averageIncomeMono = transactionService.getAverageIncome() - val currentBudgetIncomeMono = transactionService.calcTransactionsSum( - finalBudget, transactionType = "PLANNED", categoryType = "INCOME" - ) - val plannedIncomeMono = transactionService.calcTransactionsSum( - finalBudget, categoryType = "INCOME", transactionType = "PLANNED" - ) - val plannedSavingMono = transactionService.calcTransactionsSum( - finalBudget, categoryId = "675850148198643f121e466a", transactionType = "PLANNED" - ) - - Mono.zip( - averageSumsMono, - averageIncomeMono, - currentBudgetIncomeMono, - plannedIncomeMono, - plannedSavingMono - ).flatMap { tuple -> - val averageSums = tuple.t1 - val averageIncome = tuple.t2 - val currentBudgetIncome = tuple.t3 - val plannedIncome = tuple.t4 - val plannedSaving = tuple.t5 - - Flux.fromIterable(finalBudget.categories) - .flatMap { category -> - processCategoryWarnings( - category, - finalBudget, - averageSums, - averageIncome, - currentBudgetIncome, - plannedIncome, - plannedSaving - ) - } - .collectList() - .flatMap { warns -> - warnRepo.saveAll(warns.filterNotNull()).collectList() - } - .doOnSuccess { logger.info("ENDED WARNS UPDATE") } - .map { it.sortedByDescending { warn -> warn.serenity.sort } } - } - }.doOnError { error -> - logger.error("Error updating budget warns: ${error.message}", error) - }.onErrorResume { - Mono.just(emptyList()) // Возвращаем пустой список в случае ошибки - } - } - - - private fun processCategoryWarnings( - category: BudgetCategory, - finalBudget: Budget, - averageSums: Map, - averageIncome: Double, - currentBudgetIncome: Double, - plannedIncome: Double, - plannedSaving: Double - ): Flux { - val warnsForCategory = mutableListOf>() - - val averageSum = averageSums[category.category?.id] ?: 0.0 - val categorySpentRatioInAvgIncome = if (averageIncome > 0.0) averageSum / averageIncome else 0.0 - val projectedAvailableSum = currentBudgetIncome * categorySpentRatioInAvgIncome - val contextAtAvg = "category${category.category?.id}atbudget${finalBudget.id}lessavg" - val lowSavingContext = "savingValueLess10atBudget${finalBudget.id}" - - if (averageSum > category.currentLimit) { - val warnMono = warnRepo.findWarnByContext(contextAtAvg) - .switchIfEmpty( - Mono.just( - Warn( - serenity = WarnSerenity.MAIN, - message = PushMessage( - title = "Внимание на ${category.category?.name}!", - body = "Лимит меньше средних трат (Среднее: ${averageSum.toInt()} ₽ Текущий лимит: ${category.currentLimit.toInt()} ₽)." + - "\nСредняя доля данной категории в доходах: ${(categorySpentRatioInAvgIncome * 100).toInt()}%." + - "\nПроецируется на текущие поступления: ${projectedAvailableSum.toInt()} ₽", - icon = category.category?.icon - ), - budgetId = finalBudget.id!!, - context = contextAtAvg, - isHide = false - ) - ) - ) - warnsForCategory.add(warnMono) - } else { - warnRepo.findWarnByContext(contextAtAvg).flatMap { warnRepo.delete(it).then(Mono.empty()) } - } - - if (category.category?.id == "675850148198643f121e466a") { - val savingRatio = if (plannedIncome > 0.0) category.currentLimit / plannedIncome else 0.0 - if (savingRatio < 0.1) { - val warnMono = warnRepo.findWarnByContext(lowSavingContext) - .switchIfEmpty( - Mono.just( - Warn( - serenity = WarnSerenity.IMPORTANT, - message = PushMessage( - title = "Доля сбережений очень мала!", - body = "Текущие плановые сбережения равны ${plannedSaving.toInt()} (${ - (savingRatio * 100).toInt() - }%)! Исправьте!", - icon = category.category!!.icon - ), - budgetId = finalBudget.id!!, - context = lowSavingContext, - isHide = false - ) - ) - ) - warnsForCategory.add(warnMono) - } else { - warnRepo.findWarnByContext(lowSavingContext) - .flatMap { warnRepo.delete(it).then(Mono.empty()) } - } - } - - return Flux.fromIterable(warnsForCategory).flatMap { it } - } - - -} +// +// +// fun getBudgetByDate(date: LocalDate): Mono { +// return budgetRepo.findByDateFromLessThanEqualAndDateToGreaterThanEqual(date, date).switchIfEmpty(Mono.empty()) +// } +// +// +// +// +// fun getBudgetCategories(id: String): Mono> { +// return budgetRepo.findById(id).flatMap { budget -> +// val lookup = lookup("categories", "category.\$id", "_id", "categoryDetailed") +// val unwind = unwind("categoryDetailed") +// val projectDouble = project("categoryDetailed", "amount", "date") +// .andExpression("{ \$toDouble: \"\$amount\" }").`as`("amount") +// val match = match(Criteria.where("date").gte(budget.dateFrom).lt(budget.dateTo)) +// val group = group("categoryDetailed").sum("amount").`as`("currentSpent") +// val project = project("currentSpent").and("_id").`as`("category") +// val sort = sort(Sort.by(Sort.Order.asc("_id"))) +// +// val aggregation = newAggregation(lookup, unwind, projectDouble, match, group, project, sort) +// +// reactiveMongoTemplate.aggregate(aggregation, "transactions", BudgetCategory::class.java) +// .collectList() // Преобразование результата в список +// } +// } +// +// +// fun getBudgetTransactionsByType(budgetId: String): Mono>> { +// return budgetRepo.findById(budgetId).flatMap { it -> +// transactionService.getTransactionsByTypes(it.dateFrom, it.dateTo) +// } +// } +// +// +// @CacheEvict(cacheNames = ["budgets", "budgetsList"], allEntries = true) +// fun deleteBudget(budgetId: String): Mono { +// return budgetRepo.findById(budgetId) +// .switchIfEmpty(Mono.error(BudgetNotFoundException("Budget with id: $budgetId not found"))) +// .flatMap { budget -> +// transactionService.getTransactionsToDelete(budget.dateFrom, budget.dateTo) +// .flatMapMany { transactions -> +// Flux.fromIterable(transactions) +// .flatMap { transaction -> +// transactionService.deleteTransaction(transaction.id!!) +// } +// } +// .then( +// budgetRepo.delete(budget) +// ) +// } +// } +// +// +// @CacheEvict(cacheNames = ["budgets", "budgetsList"], allEntries = true) +// fun setCategoryLimit(budgetId: String, catId: String, limit: Double): Mono { +// return budgetRepo.findById(budgetId).flatMap { budget -> +// val catEdit = budget.categories.firstOrNull { it.category?.id == catId } +// ?: return@flatMap Mono.error(Exception("Category not found in the budget")) +// +// transactionService.calcTransactionsSum(budget, catId, "PLANNED").flatMap { catPlanned -> +// if (catPlanned > limit) { +// Mono.error(Exception("Limit can't be less than planned expenses on category. Current planned value: $catPlanned")) +// } else { +// catEdit.currentLimit = limit +// budgetRepo.save(budget).flatMap { +// updateBudgetWarns(it) +// .thenReturn(catEdit) +// } +// } +// } +// } +// } +// +// +// fun getWarns(budgetId: String, isHide: Boolean? = null): Mono> { +// return warnRepo.findAllByBudgetIdAndIsHide(budgetId, isHide == true).collectList() +// } +// +// fun hideWarn(warnId: String): Mono { +// return warnRepo.findById(warnId) // Ищем предупреждение +// .flatMap { warn -> +// warn.isHide = true // Обновляем поле +// warnRepo.save(warn) // Сохраняем изменённое предупреждение +// } +// } +// +// +// fun updateBudgetWarns(budget: Budget? = null): Mono> { +// logger.info("STARTED WARNS UPDATE") +// +// val finalBudgetMono = budget?.let { Mono.just(it) } +// ?: return Mono.just(emptyList()) +// +// return finalBudgetMono.flatMap { finalBudget -> +// if (finalBudget.categories.isEmpty()) { +// logger.info("No categories found for budget ${finalBudget.id}") +// return@flatMap Mono.just(emptyList()) +// } +// +// val averageSumsMono = transactionService.getAverageSpendingByCategory() +// val averageIncomeMono = transactionService.getAverageIncome() +// val currentBudgetIncomeMono = transactionService.calcTransactionsSum( +// finalBudget, transactionType = "PLANNED", categoryType = "INCOME" +// ) +// val plannedIncomeMono = transactionService.calcTransactionsSum( +// finalBudget, categoryType = "INCOME", transactionType = "PLANNED" +// ) +// val plannedSavingMono = transactionService.calcTransactionsSum( +// finalBudget, categoryId = "675850148198643f121e466a", transactionType = "PLANNED" +// ) +// +// Mono.zip( +// averageSumsMono, +// averageIncomeMono, +// currentBudgetIncomeMono, +// plannedIncomeMono, +// plannedSavingMono +// ).flatMap { tuple -> +// val averageSums = tuple.t1 +// val averageIncome = tuple.t2 +// val currentBudgetIncome = tuple.t3 +// val plannedIncome = tuple.t4 +// val plannedSaving = tuple.t5 +// +// Flux.fromIterable(finalBudget.categories) +// .flatMap { category -> +// processCategoryWarnings( +// category, +// finalBudget, +// averageSums, +// averageIncome, +// currentBudgetIncome, +// plannedIncome, +// plannedSaving +// ) +// } +// .collectList() +// .flatMap { warns -> +// warnRepo.saveAll(warns.filterNotNull()).collectList() +// } +// .doOnSuccess { logger.info("ENDED WARNS UPDATE") } +// .map { it.sortedByDescending { warn -> warn.serenity.sort } } +// } +// }.doOnError { error -> +// logger.error("Error updating budget warns: ${error.message}", error) +// }.onErrorResume { +// Mono.just(emptyList()) // Возвращаем пустой список в случае ошибки +// } +// } +// +// +// private fun processCategoryWarnings( +// category: BudgetCategory, +// finalBudget: Budget, +// averageSums: Map, +// averageIncome: Double, +// currentBudgetIncome: Double, +// plannedIncome: Double, +// plannedSaving: Double +// ): Flux { +// val warnsForCategory = mutableListOf>() +// +// val averageSum = averageSums[category.category?.id] ?: 0.0 +// val categorySpentRatioInAvgIncome = if (averageIncome > 0.0) averageSum / averageIncome else 0.0 +// val projectedAvailableSum = currentBudgetIncome * categorySpentRatioInAvgIncome +// val contextAtAvg = "category${category.category?.id}atbudget${finalBudget.id}lessavg" +// val lowSavingContext = "savingValueLess10atBudget${finalBudget.id}" +// +// if (averageSum > category.currentLimit) { +// val warnMono = warnRepo.findWarnByContext(contextAtAvg) +// .switchIfEmpty( +// Mono.just( +// Warn( +// serenity = WarnSerenity.MAIN, +// message = PushMessage( +// title = "Внимание на ${category.category?.name}!", +// body = "Лимит меньше средних трат (Среднее: ${averageSum.toInt()} ₽ Текущий лимит: ${category.currentLimit.toInt()} ₽)." + +// "\nСредняя доля данной категории в доходах: ${(categorySpentRatioInAvgIncome * 100).toInt()}%." + +// "\nПроецируется на текущие поступления: ${projectedAvailableSum.toInt()} ₽", +// icon = category.category?.icon +// ), +// budgetId = finalBudget.id!!, +// context = contextAtAvg, +// isHide = false +// ) +// ) +// ) +// warnsForCategory.add(warnMono) +// } else { +// warnRepo.findWarnByContext(contextAtAvg).flatMap { warnRepo.delete(it).then(Mono.empty()) } +// } +// +// if (category.category?.id == "675850148198643f121e466a") { +// val savingRatio = if (plannedIncome > 0.0) category.currentLimit / plannedIncome else 0.0 +// if (savingRatio < 0.1) { +// val warnMono = warnRepo.findWarnByContext(lowSavingContext) +// .switchIfEmpty( +// Mono.just( +// Warn( +// serenity = WarnSerenity.IMPORTANT, +// message = PushMessage( +// title = "Доля сбережений очень мала!", +// body = "Текущие плановые сбережения равны ${plannedSaving.toInt()} (${ +// (savingRatio * 100).toInt() +// }%)! Исправьте!", +// icon = category.category!!.icon +// ), +// budgetId = finalBudget.id!!, +// context = lowSavingContext, +// isHide = false +// ) +// ) +// ) +// warnsForCategory.add(warnMono) +// } else { +// warnRepo.findWarnByContext(lowSavingContext) +// .flatMap { warnRepo.delete(it).then(Mono.empty()) } +// } +// } +// +// return Flux.fromIterable(warnsForCategory).flatMap { it } +// } +// +// +//} diff --git a/src/main/kotlin/space/luminic/budgerapp/services/CategoryService.kt b/src/main/kotlin/space/luminic/budgerapp/services/CategoryService.kt index f7f26b4..219f728 100644 --- a/src/main/kotlin/space/luminic/budgerapp/services/CategoryService.kt +++ b/src/main/kotlin/space/luminic/budgerapp/services/CategoryService.kt @@ -6,6 +6,7 @@ import org.bson.types.ObjectId import org.slf4j.LoggerFactory import org.springframework.cache.annotation.CacheEvict import org.springframework.cache.annotation.Cacheable +import org.springframework.context.ApplicationEventPublisher import org.springframework.data.domain.Sort import org.springframework.data.domain.Sort.Direction import org.springframework.data.mongodb.core.ReactiveMongoTemplate @@ -37,8 +38,9 @@ import java.util.Date @Service class CategoryService( private val categoryRepo: CategoryRepo, - private val transactionService: TransactionService, - private val mongoTemplate: ReactiveMongoTemplate + private val financialService: FinancialService, + private val mongoTemplate: ReactiveMongoTemplate, + private val eventPublisher: ApplicationEventPublisher ) { private val logger = LoggerFactory.getLogger(javaClass) @@ -115,7 +117,7 @@ class CategoryService( return categoryRepo.findById(categoryId).switchIfEmpty( Mono.error(IllegalArgumentException("Category with id: $categoryId not found")) ).flatMap { - transactionService.getTransactions(categoryId = categoryId) + financialService.getTransactions(categoryId = categoryId) .flatMapMany { transactions -> categoryRepo.findByName("Другое").switchIfEmpty( categoryRepo.save( @@ -126,10 +128,10 @@ class CategoryService( icon = "🚮" ) ) - ).flatMapMany { Category -> + ).flatMapMany { category -> Flux.fromIterable(transactions).flatMap { transaction -> - transaction.category = Category // Присваиваем конкретный объект категории - transactionService.editTransaction(transaction) // Сохраняем изменения + transaction.category = category // Присваиваем конкретный объект категории + financialService.editTransaction(transaction) // Сохраняем изменения } } } @@ -140,7 +142,6 @@ class CategoryService( fun getBudgetCategories(dateFrom: LocalDate, dateTo: LocalDate): Mono>> { - logger.info("here cat starts") val pipeline = listOf( Document( "\$lookup", @@ -265,7 +266,6 @@ class CategoryService( ) id to values } - logger.info("here cat ends") Mono.just(categories) } diff --git a/src/main/kotlin/space/luminic/budgerapp/services/FinancialService.kt b/src/main/kotlin/space/luminic/budgerapp/services/FinancialService.kt new file mode 100644 index 0000000..e2adf4b --- /dev/null +++ b/src/main/kotlin/space/luminic/budgerapp/services/FinancialService.kt @@ -0,0 +1,1805 @@ +package space.luminic.budgerapp.services + +import org.bson.BsonNull +import org.bson.Document +import org.bson.types.ObjectId +import org.slf4j.LoggerFactory +import org.springframework.cache.annotation.CacheEvict +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.aggregation.Aggregation.* +import org.springframework.data.mongodb.core.aggregation.DateOperators.DateToString +import org.springframework.data.mongodb.core.query.Criteria +import org.springframework.data.mongodb.core.query.Query +import org.springframework.security.core.context.ReactiveSecurityContextHolder +import org.springframework.stereotype.Service +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono +import space.luminic.budgerapp.models.* +import space.luminic.budgerapp.repos.BudgetRepo +import space.luminic.budgerapp.repos.TransactionRepo +import space.luminic.budgerapp.repos.WarnRepo +import java.time.* +import java.time.temporal.TemporalAdjusters +import java.util.* + +@Service +class FinancialService( + val budgetRepo: BudgetRepo, + val warnRepo: WarnRepo, + val transactionsRepo: TransactionRepo, + val recurrentService: RecurrentService, + val userService: UserService, + val reactiveMongoTemplate: ReactiveMongoTemplate +) { + private val logger = LoggerFactory.getLogger(FinancialService::class.java) + + + fun updateBudgetOnCreate(transaction: Transaction): Mono { + return budgetRepo.findByDateFromLessThanEqualAndDateToGreaterThanEqual( + transaction.date, transaction.date + ).flatMap { budget -> + + val budgetCategory = budget.categories.firstOrNull { it.category.id == transaction.category.id } + if (transaction.category.type.code == "INCOME") { + return@flatMap budgetRepo.save(budget) + } else if (budgetCategory == null) { + return@flatMap Mono.error(RuntimeException("Budget category not found in the budget")) + } + return@flatMap getBudgetSumsByCategory(transaction.category.id!!, budget) + .flatMap { sums -> + budgetCategory.currentPlanned = sums.getDouble("plannedAmount") ?: 0.0 + budgetCategory.currentSpent = sums.getDouble("instantAmount") ?: 0.0 + // При совпадении бюджетов разница просто корректирует лимит + budgetCategory.currentLimit += transaction.amount + logger.info("updateBudgetOnEdit end") + budgetRepo.save(budget).then() + } +// getBudgetCategories(budget.dateFrom, budget.dateTo).flatMap { categories -> +// val updatedCategoriesMono: Mono> = when (transaction.type.code) { +// "PLANNED" -> Flux.fromIterable(budget.categories) +// .map { category -> +// if (category.category.id == transaction.category.id) { +// categories[category.category.id]?.let { data -> +// category.currentSpent = data["instantAmount"] ?: 0.0 +// category.currentPlanned = data["plannedAmount"] ?: 0.0 +// category.currentLimit += transaction.amount +// } +// } +// category +// }.collectList() +// +// "INSTANT" -> Flux.fromIterable(budget.categories) +// .map { category -> +// if (category.category.id == transaction.category.id) { +// categories[category.category.id]?.let { data -> +// category.currentSpent = data["instantAmount"] ?: 0.0 +// category.currentPlanned = data["plannedAmount"] ?: 0.0 +// } +// } +// category +// }.collectList() +// +// else -> Mono.just(budget.categories) // Добавляем обработку типа по умолчанию +// } +// +// updatedCategoriesMono.flatMap { updated -> +// budget.categories = updated.toMutableList() +// budgetRepo.save(budget).then() // Гарантируем завершение сохранения +// } +// } + }.then() // Возвращаем корректный Mono + } + + + fun updateBudgetOnEdit( + oldTransaction: Transaction, + newTransaction: Transaction, + difference: Double + ): Mono { + logger.info("updateBudgetOnEdit start ") + return Mono.zip( + + budgetRepo.findByDateFromLessThanEqualAndDateToGreaterThanEqual(oldTransaction.date, oldTransaction.date) + .map { + logger.info("got old budget") + it + } + .switchIfEmpty(Mono.error(BudgetNotFoundException("Old budget cannot be null"))), + budgetRepo.findByDateFromLessThanEqualAndDateToGreaterThanEqual(newTransaction.date, newTransaction.date) + .map { + logger.info("got new budget") + it + } + .switchIfEmpty(Mono.error(BudgetNotFoundException("New budget cannot be null"))) + ).flatMap { tuple -> + val oldBudget = tuple.t1 + val newBudget = tuple.t2 + val isSameBudget = oldBudget.id == newBudget.id + + if (isSameBudget) { + // Если бюджеты совпадают — обновляем соответствующую категорию в новом (едином) бюджете. + val budgetCategory = + if (newTransaction.category.type.code == "EXPENSE") newBudget.categories.firstOrNull { it.category.id == newTransaction.category.id } else newBudget.incomeCategories.firstOrNull { it.category.id == newTransaction.category.id } + if (budgetCategory == null) { + return@flatMap Mono.error(RuntimeException("Budget category not found in the budget")) + } + + return@flatMap getBudgetSumsByCategory(newTransaction.category.id!!, newBudget) + .flatMap { sums -> + budgetCategory.currentPlanned = sums.getDouble("plannedAmount") ?: 0.0 + budgetCategory.currentSpent = sums.getDouble("instantAmount") ?: 0.0 + // При совпадении бюджетов разница просто корректирует лимит + budgetCategory.currentLimit += difference + logger.info("updateBudgetOnEdit end") + budgetRepo.save(newBudget).then() + } + } else { + // Если бюджеты различаются — отдельно обновляем категории в старом и новом бюджетах. + val oldBudgetCategory = + oldBudget.categories.firstOrNull { it.category.id == oldTransaction.category.id } + val newBudgetCategory = + newBudget.categories.firstOrNull { it.category.id == newTransaction.category.id } + + val oldUpdate: Mono = if (oldBudgetCategory == null) { + Mono.error(RuntimeException("Old budget category not found")) + } else { + getBudgetSumsByCategory(oldTransaction.category.id!!, oldBudget) + .flatMap { sums -> + oldBudgetCategory.currentPlanned = sums.getDouble("plannedAmount") ?: 0.0 + oldBudgetCategory.currentSpent = sums.getDouble("instantAmount") ?: 0.0 + // В старом бюджете вычитаем разницу, так как транзакция перемещается + oldBudgetCategory.currentLimit -= difference + budgetRepo.save(oldBudget).then() + } + } + + val newUpdate: Mono = if (newBudgetCategory == null) { + Mono.error(RuntimeException("New budget category not found")) + } else { + getBudgetSumsByCategory(newTransaction.category.id!!, newBudget) + .flatMap { sums -> + newBudgetCategory.currentPlanned = sums.getDouble("plannedAmount") ?: 0.0 + newBudgetCategory.currentSpent = sums.getDouble("instantAmount") ?: 0.0 + // В новом бюджете прибавляем разницу + newBudgetCategory.currentLimit += difference + budgetRepo.save(newBudget).then() + } + } + logger.info("updateBudgetOnEdit end") + return@flatMap Mono.`when`(oldUpdate, newUpdate).then() + } + } + } + + fun updateBudgetOnDelete(transaction: Transaction): Mono { + return budgetRepo.findByDateFromLessThanEqualAndDateToGreaterThanEqual( + transaction.date, transaction.date + ).flatMap { budget -> + getBudgetCategories(budget.dateFrom, budget.dateTo).flatMap { categories -> + val updatedCategoriesMono: Mono> = when (transaction.type.code) { + "PLANNED" -> Flux.fromIterable(budget.categories) + .map { category -> + if (category.category.id == transaction.category.id) { + categories[category.category.id]?.let { data -> + category.currentSpent = data["instantAmount"] ?: 0.0 + category.currentPlanned = data["plannedAmount"] ?: 0.0 + category.currentLimit -= transaction.amount + } + } + category + }.collectList() + + "INSTANT" -> Flux.fromIterable(budget.categories) + .map { category -> + if (category.category.id == transaction.category.id) { + categories[category.category.id]?.let { data -> + category.currentSpent = data["instantAmount"] ?: 0.0 + category.currentPlanned = data["plannedAmount"] ?: 0.0 + } + } + category + }.collectList() + + else -> Mono.just(budget.categories) + } + + updatedCategoriesMono.flatMap { updated -> + budget.categories = updated.toMutableList() + budgetRepo.save(budget).then() // Гарантируем завершение + } + } + }.then() // Возвращаем корректный Mono + } + + + @Cacheable("budgetsList") + fun getBudgets(sortSetting: SortSetting? = null): Mono> { + val sort = if (sortSetting != null) { + Sort.by(sortSetting.order, sortSetting.by) + } else { + Sort.by(Sort.Direction.DESC, "dateFrom") + } + + return budgetRepo.findAll(sort) + .collectList() // Сбор Flux в Mono> + } + + + // @Cacheable("budgets", key = "#id") + fun getBudget(id: String): Mono { + return budgetRepo.findById(id) + .flatMap { budget -> + val budgetDTO = BudgetDTO( + budget.id, + budget.name, + budget.dateFrom, + budget.dateTo, + budget.createdAt, + categories = budget.categories, + incomeCategories = budget.incomeCategories, + ) + + logger.info("Fetching categories and transactions") + val categoriesMono = getBudgetCategories(budgetDTO.dateFrom, budgetDTO.dateTo) + val transactionsMono = + getTransactionsByTypes(budgetDTO.dateFrom, budgetDTO.dateTo) + + + Mono.zip(categoriesMono, transactionsMono) + .flatMap { tuple -> + val categories = tuple.t1 + val transactions = tuple.t2 + + + Flux.fromIterable(budgetDTO.categories) + .map { category -> + categories[category.category.id]?.let { data -> + category.currentSpent = data["instantAmount"] ?: 0.0 + category.currentPlanned = data["plannedAmount"] ?: 0.0 + } + category + } + .collectList() + .map { updatedCategories -> + budgetDTO.categories = updatedCategories + budgetDTO.plannedExpenses = transactions["plannedExpenses"] as MutableList + budgetDTO.plannedIncomes = transactions["plannedIncomes"] as MutableList + budgetDTO.transactions = transactions["instantTransactions"] as MutableList + + budgetDTO + } + } + } + .doOnError { error -> + logger.error("Error fetching budget: ${error.message}", error) + } + .switchIfEmpty(Mono.error(BudgetNotFoundException("Budget not found with id: $id"))) + } + + + fun regenCats(): Mono { + return budgetRepo.findAll() + .flatMap { budget -> + getCategoryTransactionPipeline(budget.dateFrom, budget.dateTo, "INCOME") + .map { categories -> + budget.incomeCategories = categories + budget + } + .flatMap { updatedBudget -> budgetRepo.save(updatedBudget) } + } + .then() + } + + @CacheEvict(cacheNames = ["budgets", "budgetsList"], allEntries = true) + fun createBudget(budget: Budget, createRecurrent: Boolean): Mono { + return Mono.zip( + getBudgetByDate(budget.dateFrom).map { Optional.ofNullable(it) } + .switchIfEmpty(Mono.just(Optional.empty())), + getBudgetByDate(budget.dateTo).map { Optional.ofNullable(it) } + .switchIfEmpty(Mono.just(Optional.empty())) + ).flatMap { tuple -> + val startBudget = tuple.t1.orElse(null) + val endBudget = tuple.t2.orElse(null) + + // Проверяем, пересекаются ли бюджеты по датам + if (startBudget != null || endBudget != null) { + return@flatMap Mono.error(IllegalArgumentException("Бюджет с теми же датами найден")) + } + + // Если createRecurrent=true, создаем рекуррентные транзакции + val recurrentsCreation = if (createRecurrent) { + recurrentService.createRecurrentsForBudget(budget) + } else { + Mono.empty() + } + + // Создаем бюджет после возможного создания рекуррентных транзакций + recurrentsCreation.then( + getCategoryTransactionPipeline(budget.dateFrom, budget.dateTo) + .flatMap { categories -> + budget.categories = categories + budgetRepo.save(budget) + } + .publishOn(reactor.core.scheduler.Schedulers.boundedElastic()) + .doOnNext { savedBudget -> + // Выполнение updateBudgetWarns в фоне + updateBudgetWarns(budget = savedBudget) + .doOnError { error -> + // Логируем ошибку, если произошла + println("Error during updateBudgetWarns: ${error.message}") + } + .subscribe() + } + ) + } + } + + + fun getBudgetByDate(date: LocalDate): Mono { + return budgetRepo.findByDateFromLessThanEqualAndDateToGreaterThanEqual(date, date).switchIfEmpty(Mono.empty()) + } + + + fun getBudgetCategories(id: String): Mono> { + return budgetRepo.findById(id).flatMap { budget -> + val lookup = lookup("categories", "category.\$id", "_id", "categoryDetailed") + val unwind = unwind("categoryDetailed") + val projectDouble = project("categoryDetailed", "amount", "date") + .andExpression("{ \$toDouble: \"\$amount\" }").`as`("amount") + val match = match(Criteria.where("date").gte(budget.dateFrom).lt(budget.dateTo)) + val group = group("categoryDetailed").sum("amount").`as`("currentSpent") + val project = project("currentSpent").and("_id").`as`("category") + val sort = sort(Sort.by(Sort.Order.asc("_id"))) + + val aggregation = newAggregation(lookup, unwind, projectDouble, match, group, project, sort) + + reactiveMongoTemplate.aggregate(aggregation, "transactions", BudgetCategory::class.java) + .collectList() // Преобразование результата в список + } + } + + + fun getBudgetTransactionsByType(budgetId: String): Mono>> { + return budgetRepo.findById(budgetId).flatMap { it -> + getTransactionsByTypes(it.dateFrom, it.dateTo) + } + } + + + @CacheEvict(cacheNames = ["budgets", "budgetsList"], allEntries = true) + fun deleteBudget(budgetId: String): Mono { + return budgetRepo.findById(budgetId) + .switchIfEmpty(Mono.error(BudgetNotFoundException("Budget with id: $budgetId not found"))) + .flatMap { budget -> + getTransactionsToDelete(budget.dateFrom, budget.dateTo) + .flatMapMany { transactions -> + Flux.fromIterable(transactions) + .flatMap { transaction -> + deleteTransaction(transaction.id!!) + } + } + .then( + budgetRepo.delete(budget) + ) + } + } + + + @CacheEvict(cacheNames = ["budgets", "budgetsList"], allEntries = true) + fun setCategoryLimit(budgetId: String, catId: String, limit: Double): Mono { + return budgetRepo.findById(budgetId).flatMap { budget -> + val catEdit = budget.categories.firstOrNull { it.category.id == catId } + ?: return@flatMap Mono.error(Exception("Category not found in the budget")) + + calcTransactionsSum(budget, catId, "PLANNED").flatMap { catPlanned -> + if (catPlanned > limit) { + Mono.error(Exception("Limit can't be less than planned expenses on category. Current planned value: $catPlanned")) + } else { + catEdit.currentLimit = limit + budgetRepo.save(budget).flatMap { + updateBudgetWarns(it) + .thenReturn(catEdit) + } + } + } + } + } + + + fun getWarns(budgetId: String, isHide: Boolean? = null): Mono> { + return warnRepo.findAllByBudgetIdAndIsHide(budgetId, isHide == true).collectList() + } + + fun hideWarn(warnId: String): Mono { + return warnRepo.findById(warnId) // Ищем предупреждение + .flatMap { warn -> + warn.isHide = true // Обновляем поле + warnRepo.save(warn) // Сохраняем изменённое предупреждение + } + } + + + fun updateBudgetWarns(budget: Budget? = null): Mono> { + logger.info("STARTED WARNS UPDATE") + + val finalBudgetMono = budget?.let { Mono.just(it) } + ?: return Mono.just(emptyList()) + + return finalBudgetMono.flatMap { finalBudget -> + if (finalBudget.categories.isEmpty()) { + logger.info("No categories found for budget ${finalBudget.id}") + return@flatMap Mono.just(emptyList()) + } + + val averageSumsMono = getAverageSpendingByCategory() + val averageIncomeMono = getAverageIncome() + val currentBudgetIncomeMono = calcTransactionsSum( + finalBudget, transactionType = "PLANNED", categoryType = "INCOME" + ) + val plannedIncomeMono = calcTransactionsSum( + finalBudget, categoryType = "INCOME", transactionType = "PLANNED" + ) + val plannedSavingMono = calcTransactionsSum( + finalBudget, categoryId = "675850148198643f121e466a", transactionType = "PLANNED" + ) + + Mono.zip( + averageSumsMono, + averageIncomeMono, + currentBudgetIncomeMono, + plannedIncomeMono, + plannedSavingMono + ).flatMap { tuple -> + val averageSums = tuple.t1 + val averageIncome = tuple.t2 + val currentBudgetIncome = tuple.t3 + val plannedIncome = tuple.t4 + val plannedSaving = tuple.t5 + + Flux.fromIterable(finalBudget.categories) + .flatMap { category -> + processCategoryWarnings( + category, + finalBudget, + averageSums, + averageIncome, + currentBudgetIncome, + plannedIncome, + plannedSaving + ) + } + .collectList() + .flatMap { warns -> + warnRepo.saveAll(warns.filterNotNull()).collectList() + } + .doOnSuccess { logger.info("ENDED WARNS UPDATE") } + .map { it.sortedByDescending { warn -> warn.serenity.sort } } + } + }.doOnError { error -> + logger.error("Error updating budget warns: ${error.message}", error) + }.onErrorResume { + Mono.just(emptyList()) // Возвращаем пустой список в случае ошибки + } + } + + + private fun processCategoryWarnings( + category: BudgetCategory, + finalBudget: Budget, + averageSums: Map, + averageIncome: Double, + currentBudgetIncome: Double, + plannedIncome: Double, + plannedSaving: Double + ): Flux { + val warnsForCategory = mutableListOf>() + + val averageSum = averageSums[category.category.id] ?: 0.0 + val categorySpentRatioInAvgIncome = if (averageIncome > 0.0) averageSum / averageIncome else 0.0 + val projectedAvailableSum = currentBudgetIncome * categorySpentRatioInAvgIncome + val contextAtAvg = "category${category.category.id}atbudget${finalBudget.id}lessavg" + val lowSavingContext = "savingValueLess10atBudget${finalBudget.id}" + + if (averageSum > category.currentLimit) { + val warnMono = warnRepo.findWarnByContext(contextAtAvg) + .switchIfEmpty( + Mono.just( + Warn( + serenity = WarnSerenity.MAIN, + message = PushMessage( + title = "Внимание на ${category.category.name}!", + body = "Лимит меньше средних трат (Среднее: ${averageSum.toInt()} ₽ Текущий лимит: ${category.currentLimit.toInt()} ₽)." + + "\nСредняя доля данной категории в доходах: ${(categorySpentRatioInAvgIncome * 100).toInt()}%." + + "\nПроецируется на текущие поступления: ${projectedAvailableSum.toInt()} ₽", + icon = category.category.icon + ), + budgetId = finalBudget.id!!, + context = contextAtAvg, + isHide = false + ) + ) + ) + warnsForCategory.add(warnMono) + } else { + warnRepo.findWarnByContext(contextAtAvg).flatMap { warnRepo.delete(it).then(Mono.empty()) } + } + + if (category.category.id == "675850148198643f121e466a") { + val savingRatio = if (plannedIncome > 0.0) category.currentLimit / plannedIncome else 0.0 + if (savingRatio < 0.1) { + val warnMono = warnRepo.findWarnByContext(lowSavingContext) + .switchIfEmpty( + Mono.just( + Warn( + serenity = WarnSerenity.IMPORTANT, + message = PushMessage( + title = "Доля сбережений очень мала!", + body = "Текущие плановые сбережения равны ${plannedSaving.toInt()} (${ + (savingRatio * 100).toInt() + }%)! Исправьте!", + icon = category.category.icon + ), + budgetId = finalBudget.id!!, + context = lowSavingContext, + isHide = false + ) + ) + ) + warnsForCategory.add(warnMono) + } else { + warnRepo.findWarnByContext(lowSavingContext) + .flatMap { warnRepo.delete(it).then(Mono.empty()) } + } + } + + return Flux.fromIterable(warnsForCategory).flatMap { it } + } + + @Cacheable("transactions") + fun getTransactions( + dateFrom: LocalDate? = null, + dateTo: LocalDate? = null, + transactionType: String? = null, + isDone: Boolean? = null, + categoryId: String? = null, + categoryType: String? = null, + userId: String? = null, + parentId: String? = null, + isChild: Boolean? = null, + sortSetting: SortSetting? = null, + limit: Int? = null, + offset: Int? = null, + ): Mono> { + val matchCriteria = mutableListOf() + + // Добавляем фильтры + dateFrom?.let { matchCriteria.add(Criteria.where("date").gte(it)) } + dateTo?.let { matchCriteria.add(Criteria.where("date").lt(it)) } + transactionType?.let { matchCriteria.add(Criteria.where("type.code").`is`(it)) } + isDone?.let { matchCriteria.add(Criteria.where("isDone").`is`(it)) } + categoryId?.let { matchCriteria.add(Criteria.where("categoryDetails._id").`is`(it)) } + categoryType?.let { matchCriteria.add(Criteria.where("categoryDetails.type.code").`is`(it)) } + userId?.let { matchCriteria.add(Criteria.where("userDetails._id").`is`(ObjectId(it))) } + parentId?.let { matchCriteria.add(Criteria.where("parentId").`is`(it)) } + isChild?.let { matchCriteria.add(Criteria.where("parentId").exists(it)) } + + // Сборка агрегации + val lookup = lookup("categories", "category.\$id", "_id", "categoryDetails") + val lookupUsers = lookup("users", "user.\$id", "_id", "userDetails") + val match = match(Criteria().andOperator(*matchCriteria.toTypedArray())) + + var sort = sort(Sort.by(Direction.DESC, "date").and(Sort.by(Direction.DESC, "createdAt"))) + + sortSetting?.let { + sort = sort(Sort.by(it.order, it.by).and(Sort.by(Direction.ASC, "createdAt"))) + } + + val aggregationBuilder = mutableListOf( + lookup, + lookupUsers, + match.takeIf { matchCriteria.isNotEmpty() }, + sort, + offset?.let { skip(it.toLong()) }, + limit?.let { limit(it.toLong()) } + ).filterNotNull() + + val aggregation = newAggregation(aggregationBuilder) + + return reactiveMongoTemplate.aggregate( + aggregation, "transactions", Transaction::class.java + ) + .collectList() // Преобразуем Flux в Mono> + .map { it.toMutableList() } + } + + fun getTransactionsToDelete(dateFrom: LocalDate, dateTo: LocalDate): Mono> { + val criteria = Criteria().andOperator( + Criteria.where("date").gte(dateFrom), + Criteria.where("date").lte(dateTo), + Criteria().orOperator( + Criteria.where("type.code").`is`("PLANNED"), + Criteria.where("parentId").exists(true) + ) + ) + +// Пример использования в MongoTemplate: + val query = Query(criteria) + +// Если вы хотите использовать ReactiveMongoTemplate: + return reactiveMongoTemplate.find(query, Transaction::class.java) + .collectList() + .doOnNext { transactions -> println("Found transactions: $transactions") } + } + + + @Cacheable("transactions") + fun getTransactionById(id: String): Mono { + return transactionsRepo.findById(id) + .map { + it + } + .switchIfEmpty( + Mono.error(IllegalArgumentException("Transaction with id: $id not found")) + ) + } + + + @CacheEvict(cacheNames = ["transactions"], allEntries = true) + fun createTransaction(transaction: Transaction): Mono { + return ReactiveSecurityContextHolder.getContext() + .map { it.authentication } + .flatMap { authentication -> + val username = authentication.name + userService.getByUsername(username) + .switchIfEmpty(Mono.error(IllegalArgumentException("User not found for username: $username"))) + .flatMap { user -> + transaction.user = user + transactionsRepo.save(transaction) + .flatMap { savedTransaction -> + updateBudgetOnCreate(savedTransaction) + .thenReturn(savedTransaction) // Ждём выполнения updateBudgetOnCreate перед возвратом + } + } + } + } + + + @CacheEvict(cacheNames = ["transactions", "budgets"], allEntries = true) + fun editTransaction(transaction: Transaction): Mono { + return transactionsRepo.findById(transaction.id!!) + .flatMap { oldStateOfTransaction -> + val changed = compareSumDateDoneIsChanged(oldStateOfTransaction, transaction) + if (!changed) { + return@flatMap transactionsRepo.save(transaction) // Сохраняем, если изменений нет + } + + val amountDifference = transaction.amount - oldStateOfTransaction.amount + + // Обработка дочерней транзакции + handleChildTransaction(oldStateOfTransaction, transaction) + .then(transactionsRepo.save(transaction)) // Сохраняем основную транзакцию + .flatMap { savedTransaction -> + updateBudgetOnEdit(oldStateOfTransaction, savedTransaction, amountDifference) + .thenReturn(savedTransaction) // Ждем выполнения updateBudgetOnEdit и возвращаем транзакцию + } + } + .switchIfEmpty( + Mono.error(IllegalArgumentException("Transaction not found with id: ${transaction.id}")) + ) + } + + private fun handleChildTransaction( + oldTransaction: Transaction, + newTransaction: Transaction + ): Mono { + return transactionsRepo.findByParentId(newTransaction.id!!) + .flatMap { childTransaction -> + logger.info(childTransaction.toString()) + // Если родительская транзакция обновлена, обновляем дочернюю + childTransaction.amount = newTransaction.amount + childTransaction.category = newTransaction.category + childTransaction.comment = newTransaction.comment + childTransaction.user = newTransaction.user + transactionsRepo.save(childTransaction).then() // Убедимся, что это Mono + } + .switchIfEmpty( + Mono.defer { + // Создание новой дочерней транзакции, если требуется + if (!oldTransaction.isDone && newTransaction.isDone) { + val newChildTransaction = newTransaction.copy( + id = null, + type = TransactionType("INSTANT", "Текущие"), + parentId = newTransaction.id + ) + transactionsRepo.save(newChildTransaction) + .flatMap { savedChildTransaction -> + updateBudgetOnDelete(savedChildTransaction) // Гарантируем выполнение + } + } else { + Mono.empty() + } + } + ) + .then( + Mono.defer { + // Удаление дочерней транзакции, если родительская помечена как не выполненная + if (oldTransaction.isDone && !newTransaction.isDone) { + transactionsRepo.findByParentId(newTransaction.id!!) + .flatMap { child -> + deleteTransaction(child.id!!) + }.then() + } else { + Mono.empty() + } + } + ) + } + + + fun compareSumDateDoneIsChanged(t1: Transaction, t2: Transaction): Boolean { + return if (t1.amount != t2.amount) { + true + } else if (t1.date != t2.date) { + true + } else if (t1.isDone != t2.isDone) { + true + } else if (t1.category.id != t2.category.id) { + return true + } else { + return false + } + } + + @CacheEvict(cacheNames = ["transactions"], allEntries = true) + fun deleteTransaction(transactionId: String): Mono { + return transactionsRepo.findById(transactionId) + .flatMap { transactionToDelete -> + transactionsRepo.deleteById(transactionId) // Удаляем транзакцию + .then(updateBudgetOnDelete(transactionToDelete)) + } + + } + + +// @CacheEvict(cacheNames = ["transactions", "childTransactions"], allEntries = true) +// fun setTransactionDone(transaction: Transaction): Transaction { +// val oldStateTransaction = transactionsRepo.findById(transaction.id!!) +// .orElseThrow { RuntimeException("Transaction ${transaction.id} not found") } +// +// if (transaction.isDone) { +// if (oldStateTransaction.isDone) { +// throw RuntimeException("Transaction ${transaction.id} is already done") +// } +// +// // Создание дочерней транзакции +// val childTransaction = transaction.copy( +// id = null, +// type = TransactionType("INSTANT", "Текущие"), +// parentId = transaction.id +// ) +// createTransaction(childTransaction) +// } else { +// // Удаление дочерней транзакции, если она существует +// transactionsRepo.findByParentId(transaction.id!!).getOrNull()?.let { +// deleteTransaction(it.id!!) +// } ?: logger.warn("Child transaction of parent ${transaction.id} not found") +// } +// +// return editTransaction(transaction) +// } + + + @Cacheable("childTransactions", key = "#parentId") + fun getChildTransaction(parentId: String): Mono { + return transactionsRepo.findByParentId(parentId) + } + +// fun getTransactionByOldId(id: Int): Transaction? { +// return transactionsRepo.findByOldId(id).getOrNull() +// } + +// fun transferTransactions(): Mono { +// var transactions = transactionsRepoSQl.getTransactions() +// return transactionsRepo.saveAll(transactions).then() +// } +// + + fun calcTransactionsSum( + budget: Budget, + categoryId: String? = null, + categoryType: String? = null, + transactionType: String? = null, + isDone: Boolean? = null + ): Mono { + val matchCriteria = mutableListOf() + + // Добавляем фильтры + matchCriteria.add(Criteria.where("date").gte(budget.dateFrom)) + matchCriteria.add(Criteria.where("date").lt(budget.dateTo)) + categoryId?.let { matchCriteria.add(Criteria.where("category.\$id").`is`(ObjectId(it))) } + categoryType?.let { matchCriteria.add(Criteria.where("categoryDetails.type.code").`is`(it)) } + transactionType?.let { matchCriteria.add(Criteria.where("type.code").`is`(it)) } + isDone?.let { matchCriteria.add(Criteria.where("isDone").`is`(it)) } + + // Сборка агрегации + val lookup = lookup("categories", "category.\$id", "_id", "categoryDetails") + val unwind = unwind("categoryDetails") + val match = match(Criteria().andOperator(*matchCriteria.toTypedArray())) + val project = project("category").andExpression("{ \$toDouble: \"\$amount\" }").`as`("amount") + val group = group(categoryId ?: "all").sum("amount").`as`("totalSum") + val projectSum = project("totalSum") + val aggregation = newAggregation(lookup, unwind, match, project, group, projectSum) + + return reactiveMongoTemplate.aggregate(aggregation, "transactions", Map::class.java) + .map { result -> + val totalSum = result["totalSum"] + if (totalSum is Double) { + totalSum + } else { + 0.0 + } + } + .reduce(0.0) { acc, sum -> acc + sum } // Суммируем значения, если несколько результатов + } + + + // @Cacheable("transactions") + fun getAverageSpendingByCategory(): Mono> { + val firstDateOfMonth = LocalDate.now().with(TemporalAdjusters.firstDayOfMonth()) + + val lookup = lookup("categories", "category.\$id", "_id", "categoryDetails") + val unwind = unwind("categoryDetails") + val match = match( + Criteria.where("categoryDetails.type.code").`is`("EXPENSE") + .and("type.code").`is`("INSTANT") + .and("date").lt(firstDateOfMonth) + ) + val projectDate = project("_id", "category", "amount", "categoryDetails") + .and(DateToString.dateOf("date").toString("%Y-%m")).`as`("month") + .andExpression("{ \$toDouble: \"\$amount\" }").`as`("amount") + val groupByMonthAndCategory = group("month", "category.\$id").sum("amount").`as`("sum") + val groupByCategory = group("_id.id").avg("sum").`as`("averageAmount") + val project = project() + .and("_id").`as`("category") + .and("averageAmount").`as`("avgAmount") + val sort = sort(Sort.by(Sort.Order.asc("_id"))) + + val aggregation = newAggregation( + lookup, unwind, match, projectDate, groupByMonthAndCategory, groupByCategory, project, sort + ) + + return reactiveMongoTemplate.aggregate(aggregation, "transactions", Map::class.java) + .collectList() + .map { results -> + results.associate { result -> + val category = result["category"]?.toString() ?: "Unknown" + val avgAmount = (result["avgAmount"] as? Double) ?: 0.0 + category to avgAmount + } + } + .defaultIfEmpty(emptyMap()) // Возвращаем пустую карту, если результатов нет + } + + + @Cacheable("transactionTypes") + fun getTransactionTypes(): List { + val types = mutableListOf() + types.add(TransactionType("PLANNED", "Плановые")) + types.add(TransactionType("INSTANT", "Текущие")) + return types + } + + fun getAverageIncome(): Mono { + val lookup = lookup("categories", "category.\$id", "_id", "detailedCategory") + + val unwind = unwind("detailedCategory") + + val match = match( + Criteria.where("detailedCategory.type.code").`is`("INCOME") + .and("type.code").`is`("INSTANT") + .and("isDone").`is`(true) + ) + + val project = project("_id", "category", "detailedCategory") + .and(DateToString.dateOf("date").toString("%Y-%m")).`as`("month") + .andExpression("{ \$toDouble: \"\$amount\" }").`as`("amount") + + val groupByMonth = group("month").sum("amount").`as`("sum") + + val groupForAverage = group("avgIncomeByMonth").avg("sum").`as`("averageAmount") + + val aggregation = newAggregation(lookup, unwind, match, project, groupByMonth, groupForAverage) + + return reactiveMongoTemplate.aggregate(aggregation, "transactions", Map::class.java) + .singleOrEmpty() // Ожидаем только один результат + .map { result -> + result["averageAmount"] as? Double ?: 0.0 + } + .defaultIfEmpty(0.0) // Если результат пустой, возвращаем 0.0 + } + + + fun getTransactionsByTypes(dateFrom: LocalDate, dateTo: LocalDate): Mono>> { + val pipeline = listOf( + Document( + "\$lookup", + Document("from", "categories") + .append("localField", "category.\$id") + .append("foreignField", "_id") + .append("as", "categoryDetailed") + ), + Document( + "\$lookup", + Document("from", "users") + .append("localField", "user.\$id") + .append("foreignField", "_id") + .append("as", "userDetailed") + ), + Document( + "\$unwind", + Document("path", "\$categoryDetailed").append("preserveNullAndEmptyArrays", true) + ), + Document( + "\$unwind", + Document("path", "\$userDetailed").append("preserveNullAndEmptyArrays", true) + ), + Document( + "\$match", + Document( + "\$and", listOf( + Document( + "date", + Document( + "\$gte", + Date.from( + LocalDateTime.of(dateFrom, LocalTime.MIN) + .atZone(ZoneId.systemDefault()) + .withZoneSameInstant(ZoneOffset.UTC) + .toInstant() + ) + ) + ), + Document( + "date", + Document( + "\$lt", + Date.from( + LocalDateTime.of(dateTo, LocalTime.MAX) + .atZone(ZoneId.systemDefault()) + .withZoneSameInstant(ZoneOffset.UTC) + .toInstant() + ) + ) + ) + ) + ) + ), + Document( + "\$facet", + Document( + "plannedExpenses", + listOf( + Document( + "\$match", + Document("type.code", "PLANNED") + .append("categoryDetailed.type.code", "EXPENSE") + ), + Document("\$sort", Document("date", 1).append("_id", 1)) + ) + ) + .append( + "plannedIncomes", + listOf( + Document( + "\$match", + Document("type.code", "PLANNED") + .append("categoryDetailed.type.code", "INCOME") + ), + Document("\$sort", Document("date", 1).append("_id", 1)) + ) + ) + .append( + "instantTransactions", + listOf( + Document("\$match", Document("type.code", "INSTANT")), + Document("\$sort", Document("date", 1).append("_id", 1)) + ) + ) + ) + ) + +// getCategoriesExplainReactive(pipeline) +// .doOnNext { explainResult -> +// logger.info("Explain Result: ${explainResult.toJson()}") +// } +// .subscribe() // Этот вызов лучше оставить только для отладки + return reactiveMongoTemplate.getCollection("transactions") + .flatMapMany { it.aggregate(pipeline, Document::class.java) } + .single() // Получаем только первый результат агрегации + .flatMap { aggregationResult -> + Mono.zip( + extractTransactions(aggregationResult, "plannedExpenses"), + extractTransactions(aggregationResult, "plannedIncomes"), + extractTransactions(aggregationResult, "instantTransactions") + ).map { tuple -> + val plannedExpenses = tuple.t1 + val plannedIncomes = tuple.t2 + val instantTransactions = tuple.t3 + mapOf( + "plannedExpenses" to plannedExpenses, + "plannedIncomes" to plannedIncomes, + "instantTransactions" to instantTransactions + ) + + + } + + } + + + } + + + private fun extractTransactions(aggregationResult: Document, key: String): Mono> { + val resultTransactions = aggregationResult[key] as? List ?: emptyList() + return Flux.fromIterable(resultTransactions) + .map { documentToTransactionMapper(it) } + .collectList() + } + + + private fun documentToTransactionMapper(document: Document): Transaction { + val transactionType = document["type"] as Document + var user: User? = null + + val userDocument = document["userDetailed"] as Document + user = User( + id = (userDocument["_id"] as ObjectId).toString(), + username = userDocument["username"] as String, + firstName = userDocument["firstName"] as String, + tgId = userDocument["tgId"] as String, + tgUserName = userDocument["tgUserName"]?.let { it as String }, + password = null, + isActive = userDocument["isActive"] as Boolean, + regDate = userDocument["regDate"] as Date, + createdAt = userDocument["createdAt"] as Date, + roles = userDocument["roles"] as ArrayList, + ) + + + val categoryDocument = document["categoryDetailed"] as Document + val categoryTypeDocument = categoryDocument["type"] as Document + val category = Category( + id = (categoryDocument["_id"] as ObjectId).toString(), + type = CategoryType(categoryTypeDocument["code"] as String, categoryTypeDocument["name"] as String), + name = categoryDocument["name"] as String, + description = categoryDocument["description"] as String, + icon = categoryDocument["icon"] as String + ) + return Transaction( + (document["_id"] as ObjectId).toString(), + TransactionType( + transactionType["code"] as String, + transactionType["name"] as String + ), + user = user, + category = category, + comment = document["comment"] as String, + date = (document["date"] as Date).toInstant().atZone(ZoneId.systemDefault()).toLocalDate(), + amount = document["amount"] as Double, + isDone = document["isDone"] as Boolean, + parentId = if (document["parentId"] != null) document["parentId"] as String else null, + createdAt = LocalDateTime.ofInstant((document["createdAt"] as Date).toInstant(), ZoneOffset.UTC), + ) + + } + + fun getBudgetSumsByCategory(categoryId: String, budget: Budget): Mono { + logger.info("getting budget sums for category $categoryId") + val pipeline = listOf( + Document( + "\$match", + Document("category.\$id", ObjectId(categoryId)) + .append( + "date", Document( + "\$gte", Date.from( + LocalDateTime.of(budget.dateFrom, LocalTime.MIN) + .atZone(ZoneId.systemDefault()) + .withZoneSameInstant(ZoneOffset.UTC).toInstant() + ) + ) + .append( + "\$lt", Date.from( + LocalDateTime.of(budget.dateTo, LocalTime.MIN) + .atZone(ZoneId.systemDefault()) + .withZoneSameInstant(ZoneOffset.UTC).toInstant() + ) + ) + ) + ), + Document( + "\$group", + Document("_id", BsonNull()) + .append( + "plannedAmount", + Document( + "\$sum", + Document( + "\$cond", + listOf(Document("\$eq", listOf("\$type.code", "PLANNED")), "\$amount", 0.0) + ) + ) + ) + .append( + "instantAmount", + Document( + "\$sum", + Document( + "\$cond", + listOf(Document("\$eq", listOf("\$type.code", "INSTANT")), "\$amount", 0.0) + ) + ) + ) + ), + Document( + "\$project", + Document("_id", 0) + .append("plannedAmount", 1) + .append("instantAmount", 1) + ) + ) + + return reactiveMongoTemplate.getCollection("transactions") // Исправлено на transactions + .flatMapMany { + it.aggregate(pipeline) + }.map { + logger.info("getting budget sums for category $categoryId end") + it + } + .next() // Берём первый документ, а не весь список + } + + fun getBudgetCategories(dateFrom: LocalDate, dateTo: LocalDate): Mono>> { + val pipeline = listOf( + Document( + "\$lookup", + Document("from", "transactions") + .append( + "let", + Document("categoryId", "\$_id") + ) + .append( + "pipeline", listOf( + Document( + "\$match", + Document( + "\$expr", + Document( + "\$and", listOf( + Document("\$eq", listOf("\$category.\$id", "\$\$categoryId")), + Document( + "\$gte", listOf( + "\$date", + Date.from( + LocalDateTime.of(dateFrom, LocalTime.MIN) + .atZone(ZoneId.systemDefault()) + .withZoneSameInstant(ZoneOffset.UTC).toInstant() + ), + ) + ), + Document( + "\$lte", listOf( + "\$date", + Date.from( + LocalDateTime.of(dateTo, LocalTime.MIN) + .atZone(ZoneId.systemDefault()) + .withZoneSameInstant(ZoneOffset.UTC).toInstant() + ) + ) + ) + ) + ) + ) + ), + Document( + "\$group", + Document("_id", "\$type.code") + .append( + "totalAmount", + Document("\$sum", "\$amount") + ) + ) + ) + ) + .append("as", "transactionSums") + ), + Document( + "\$project", + Document("_id", 1L) + .append( + "plannedAmount", + Document( + "\$arrayElemAt", listOf( + Document( + "\$filter", + Document("input", "\$transactionSums") + .append("as", "sum") + .append( + "cond", + Document("\$eq", listOf("\$\$sum._id", "PLANNED")) + ) + ), 0L + ) + ) + ) + .append( + "instantAmount", + Document( + "\$arrayElemAt", listOf( + Document( + "\$filter", + Document("input", "\$transactionSums") + .append("as", "sum") + .append( + "cond", + Document("\$eq", listOf("\$\$sum._id", "INSTANT")) + ) + ), 0L + ) + ) + ) + ), + Document( + "\$addFields", + Document( + "plannedAmount", + Document("\$ifNull", listOf("\$plannedAmount.totalAmount", 0.0)) + ) + .append( + "instantAmount", + Document("\$ifNull", listOf("\$instantAmount.totalAmount", 0.0)) + ) + ) + ) + + + // Анализ плана выполнения (вывод для отладки) +// getCategoriesExplainReactive(pipeline) +// .doOnNext { explainResult -> +// logger.info("Explain Result: ${explainResult.toJson()}") +// } +// .subscribe() // Этот вызов лучше оставить только для отладки +// + + + return reactiveMongoTemplate.getCollection("categories") + .flatMapMany { it.aggregate(pipeline) } + .collectList() + .flatMap { result -> + val categories = result.associate { document -> + val id = document["_id"].toString() + val values = mapOf( + "plannedAmount" to (document["plannedAmount"] as Double? ?: 0.0), + "instantAmount" to (document["instantAmount"] as Double? ?: 0.0) + ) + id to values + } + + Mono.just(categories) + } + } + + + fun getCategoryTransactionPipeline( + dateFrom: LocalDate, + dateTo: LocalDate, + catType: String? = "EXPENSE" + ): Mono> { + val pipeline = listOf( + Document("\$match", Document("type.code", catType)), + Document( + "\$lookup", + Document("from", "transactions") + .append( + "let", + Document("categoryId", "\$_id") + ) + .append( + "pipeline", listOf( + Document( + "\$match", + Document( + "\$expr", + Document( + "\$and", listOf( + Document("\$eq", listOf("\$category.\$id", "\$\$categoryId")), + Document( + "\$gte", listOf( + "\$date", + Date.from( + LocalDateTime.of(dateFrom, LocalTime.MIN) + .atZone(ZoneId.systemDefault()) + .withZoneSameInstant(ZoneOffset.UTC).toInstant() + ) + ) + ), + Document( + "\$lt", listOf( + "\$date", + Date.from( + LocalDateTime.of(dateTo, LocalTime.MIN) + .atZone(ZoneId.systemDefault()) + .withZoneSameInstant(ZoneOffset.UTC).toInstant() + ) + ) + ) + ) + ) + ) + ), + Document( + "\$group", + Document("_id", "\$type.code") + .append( + "totalAmount", + Document("\$sum", "\$amount") + ) + ) + ) + ) + .append("as", "transactionSums") + ), + Document( + "\$project", + Document("_id", 1L) + .append("type", 1L) + .append("name", 1L) + .append("description", 1L) + .append("icon", 1L) + .append( + "plannedAmount", + Document( + "\$arrayElemAt", listOf( + Document( + "\$filter", + Document("input", "\$transactionSums") + .append("as", "sum") + .append( + "cond", + Document("\$eq", listOf("\$\$sum._id", "PLANNED")) + ) + ), 0.0 + ) + ) + ) + .append( + "instantAmount", + Document( + "\$arrayElemAt", listOf( + Document( + "\$filter", + Document("input", "\$transactionSums") + .append("as", "sum") + .append( + "cond", + Document("\$eq", listOf("\$\$sum._id", "INSTANT")) + ) + ), 0.0 + ) + ) + ) + ), + Document( + "\$addFields", + Document( + "plannedAmount", + Document("\$ifNull", listOf("\$plannedAmount.totalAmount", 0.0)) + ) + .append( + "instantAmount", + Document("\$ifNull", listOf("\$instantAmount.totalAmount", 0.0)) + ) + ) + ) + + return reactiveMongoTemplate.getCollection("categories") + .flatMapMany { it.aggregate(pipeline, Document::class.java) } + .map { document -> + val catType = document["type"] as Document + BudgetCategory( + currentSpent = document["instantAmount"] as Double, + currentLimit = document["plannedAmount"] as Double, + currentPlanned = document["plannedAmount"] as Double, + category = Category( + document["_id"].toString(), + type = CategoryType(catType["code"] as String, catType["name"] as String), + name = document["name"] as String, + description = document["description"] as String, + icon = document["icon"] as String + ) + ) + } + .collectList() + .map { it.toMutableList() } + } + + + fun getCategorySumsPipeline(dateFrom: LocalDate, dateTo: LocalDate): Mono> { + 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(dateFrom, 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 reactiveMongoTemplate.getCollection("transactions") + .flatMapMany { it.aggregate(pipeline) } + .map { + it["categoryId"] = it["categoryId"].toString() + it + } + .collectList() + } + + fun getCategorySummaries(dateFrom: LocalDate): Mono> { + val sixMonthsAgo = Date.from( + LocalDateTime.of(dateFrom, LocalTime.MIN) + .atZone(ZoneId.systemDefault()) + .withZoneSameInstant(ZoneOffset.UTC).toInstant() + ) // Пример даты, можно заменить на вычисляемую + + val aggregation = listOf( + // 1. Фильтр за последние 6 месяцев + Document( + "\$match", + Document("date", Document("\$gte", sixMonthsAgo).append("\$lt", Date())).append("type.code", "INSTANT") + ), + + // 2. Группируем по категории + (год, месяц) + Document( + "\$group", Document( + "_id", Document("category", "\$category.\$id") + .append("year", Document("\$year", "\$date")) + .append("month", Document("\$month", "\$date")) + ) + .append("totalAmount", Document("\$sum", "\$amount")) + ), + + // 3. Подтягиваем информацию о категории + Document( + "\$lookup", Document("from", "categories") + .append("localField", "_id.category") + .append("foreignField", "_id") + .append("as", "categoryInfo") + ), + + // 4. Распаковываем массив категорий + Document("\$unwind", "\$categoryInfo"), + + // 5. Фильтруем по типу категории (EXPENSE) +// Document("\$match", Document("categoryInfo.type.code", "EXPENSE")), + + // 6. Группируем обратно по категории, собирая все (год, месяц, total) + Document( + "\$group", Document("_id", "\$_id.category") + .append("categoryName", Document("\$first", "\$categoryInfo.name")) + .append("categoryType", Document("\$first", "\$categoryInfo.type.code")) + .append("categoryIcon", Document("\$first", "\$categoryInfo.icon")) + .append( + "monthlySums", Document( + "\$push", Document("year", "\$_id.year") + .append("month", "\$_id.month") + .append("total", "\$totalAmount") + ) + ) + ), + + // 7. Формируем единый массив из 6 элементов: + // - каждый элемент = {year, month, total}, + // - если нет записей за месяц, ставим total=0 + Document( + "\$project", Document("categoryName", 1) + .append("categoryType", 1) + .append("categoryIcon", 1) + .append( + "monthlySums", Document( + "\$map", Document("input", Document("\$range", listOf(0, 6))) + .append("as", "i") + .append( + "in", Document( + "\$let", Document( + "vars", Document( + "subDate", Document( + "\$dateSubtract", Document("startDate", Date()) + .append("unit", "month") + .append("amount", "$\$i") + ) + ) + ) + .append( + "in", Document("year", Document("\$year", "$\$subDate")) + .append("month", Document("\$month", "$\$subDate")) + .append( + "total", Document( + "\$ifNull", listOf( + Document( + "\$getField", Document("field", "total") + .append( + "input", Document( + "\$arrayElemAt", listOf( + Document( + "\$filter", + Document( + "input", + "\$monthlySums" + ) + .append("as", "ms") + .append( + "cond", Document( + "\$and", listOf( + Document( + "\$eq", + listOf( + "$\$ms.year", + Document( + "\$year", + "$\$subDate" + ) + ) + ), + Document( + "\$eq", + listOf( + "$\$ms.month", + Document( + "\$month", + "$\$subDate" + ) + ) + ) + ) + ) + ) + ), 0.0 + ) + ) + ) + ), 0.0 + ) + ) + ) + ) + ) + ) + ) + ) + ), + + // 8. Сортируем результат по имени категории + Document("\$sort", Document("categoryName", 1)) + ) + + // Выполняем агрегацию + return reactiveMongoTemplate.getCollection("transactions") + .flatMapMany { it.aggregate(aggregation) } + .map { document -> + // Преобразуем _id в строку + document["_id"] = document["_id"].toString() + + // Получаем monthlySums и приводим к изменяемому списку + val monthlySums = (document["monthlySums"] as? List<*>)?.map { monthlySum -> + if (monthlySum is Document) { + // Создаем копию Document, чтобы избежать изменений в исходном списке + Document(monthlySum).apply { + // Добавляем поле date + val date = LocalDate.of(getInteger("year"), getInteger("month"), 1) + this["date"] = date + } + } else { + monthlySum + } + }?.toMutableList() + + // Сортируем monthlySums по полю date + val sortedMonthlySums = monthlySums?.sortedBy { (it as? Document)?.get("date") as? LocalDate } + + // Рассчитываем разницу между текущим и предыдущим месяцем + var previousMonthSum = 0.0 + sortedMonthlySums?.forEach { monthlySum -> + if (monthlySum is Document) { + val currentMonthSum = monthlySum.getDouble("total") ?: 0.0 + + // Рассчитываем разницу в процентах + val difference = if (previousMonthSum != 0.0 && currentMonthSum != 0.0) { + (((currentMonthSum - previousMonthSum) / previousMonthSum) * 100).toInt() + } else { + 0 + } + + // Добавляем поле difference + monthlySum["difference"] = difference + + // Обновляем previousMonthSum для следующей итерации + previousMonthSum = currentMonthSum + } + } + + // Обновляем документ с отсортированными и обновленными monthlySums + document["monthlySums"] = sortedMonthlySums + document + } + .collectList() + } + +} \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/budgerapp/services/TransactionService.kt b/src/main/kotlin/space/luminic/budgerapp/services/TransactionService.kt index 9d4046b..8c3bea4 100644 --- a/src/main/kotlin/space/luminic/budgerapp/services/TransactionService.kt +++ b/src/main/kotlin/space/luminic/budgerapp/services/TransactionService.kt @@ -1,632 +1,632 @@ -package space.luminic.budgerapp.services - - -import org.bson.Document -import org.bson.types.ObjectId -import org.slf4j.LoggerFactory -import org.springframework.cache.annotation.CacheEvict -import org.springframework.cache.annotation.Cacheable -import org.springframework.context.ApplicationEventPublisher -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.aggregation.Aggregation.group -import org.springframework.data.mongodb.core.aggregation.Aggregation.limit -import org.springframework.data.mongodb.core.aggregation.Aggregation.lookup -import org.springframework.data.mongodb.core.aggregation.Aggregation.match -import org.springframework.data.mongodb.core.aggregation.Aggregation.newAggregation -import org.springframework.data.mongodb.core.aggregation.Aggregation.project -import org.springframework.data.mongodb.core.aggregation.Aggregation.skip -import org.springframework.data.mongodb.core.aggregation.Aggregation.sort -import org.springframework.data.mongodb.core.aggregation.Aggregation.unwind -import org.springframework.data.mongodb.core.aggregation.DateOperators.DateToString -import org.springframework.data.mongodb.core.query.Criteria -import org.springframework.data.mongodb.core.query.Query -import org.springframework.security.core.context.ReactiveSecurityContextHolder -import org.springframework.stereotype.Service -import reactor.core.publisher.Flux -import reactor.core.publisher.Mono -import space.luminic.budgerapp.models.Budget -import space.luminic.budgerapp.models.Category -import space.luminic.budgerapp.models.CategoryType -import space.luminic.budgerapp.models.SortSetting -import space.luminic.budgerapp.models.Transaction -import space.luminic.budgerapp.models.TransactionEvent -import space.luminic.budgerapp.models.TransactionEventType -import space.luminic.budgerapp.models.TransactionType -import space.luminic.budgerapp.models.User -import space.luminic.budgerapp.repos.TransactionRepo -import java.time.LocalDate -import java.time.LocalDateTime -import java.time.LocalTime -import java.time.ZoneId -import java.time.ZoneOffset -import java.time.temporal.TemporalAdjusters -import java.util.ArrayList -import java.util.Date - -@Service -class TransactionService( - private val reactiveMongoTemplate: ReactiveMongoTemplate, - val transactionsRepo: TransactionRepo, - val userService: UserService, - private val eventPublisher: ApplicationEventPublisher -) { - - private val logger = LoggerFactory.getLogger(TransactionService::class.java) - - @Cacheable("transactions") - fun getTransactions( - dateFrom: LocalDate? = null, - dateTo: LocalDate? = null, - transactionType: String? = null, - isDone: Boolean? = null, - categoryId: String? = null, - categoryType: String? = null, - userId: String? = null, - parentId: String? = null, - isChild: Boolean? = null, - sortSetting: SortSetting? = null, - limit: Int? = null, - offset: Int? = null, - ): Mono> { - val matchCriteria = mutableListOf() - - // Добавляем фильтры - dateFrom?.let { matchCriteria.add(Criteria.where("date").gte(it)) } - dateTo?.let { matchCriteria.add(Criteria.where("date").lt(it)) } - transactionType?.let { matchCriteria.add(Criteria.where("type.code").`is`(it)) } - isDone?.let { matchCriteria.add(Criteria.where("isDone").`is`(it)) } - categoryId?.let { matchCriteria.add(Criteria.where("categoryDetails._id").`is`(it)) } - categoryType?.let { matchCriteria.add(Criteria.where("categoryDetails.type.code").`is`(it)) } - userId?.let { matchCriteria.add(Criteria.where("userDetails._id").`is`(ObjectId(it))) } - parentId?.let { matchCriteria.add(Criteria.where("parentId").`is`(it)) } - isChild?.let { matchCriteria.add(Criteria.where("parentId").exists(it)) } - - // Сборка агрегации - val lookup = lookup("categories", "category.\$id", "_id", "categoryDetails") - val lookupUsers = lookup("users", "user.\$id", "_id", "userDetails") - val match = match(Criteria().andOperator(*matchCriteria.toTypedArray())) - - var sort = sort(Sort.by(Direction.DESC, "date").and(Sort.by(Direction.DESC, "createdAt"))) - - sortSetting?.let { - sort = sort(Sort.by(it.order, it.by).and(Sort.by(Direction.ASC, "createdAt"))) - } - - val aggregationBuilder = mutableListOf( - lookup, - lookupUsers, - match.takeIf { matchCriteria.isNotEmpty() }, - sort, - offset?.let { skip(it.toLong()) }, - limit?.let { limit(it.toLong()) } - ).filterNotNull() - - val aggregation = newAggregation(aggregationBuilder) - - return reactiveMongoTemplate.aggregate( - aggregation, "transactions", Transaction::class.java - ) - .collectList() // Преобразуем Flux в Mono> - .map { it.toMutableList() } - } - - fun getTransactionsToDelete(dateFrom: LocalDate, dateTo: LocalDate): Mono> { - val criteria = Criteria().andOperator( - Criteria.where("date").gte(dateFrom), - Criteria.where("date").lte(dateTo), - Criteria().orOperator( - Criteria.where("type.code").`is`("PLANNED"), - Criteria.where("parentId").exists(true) - ) - ) - -// Пример использования в MongoTemplate: - val query = Query(criteria) - -// Если вы хотите использовать ReactiveMongoTemplate: - return reactiveMongoTemplate.find(query, Transaction::class.java) - .collectList() - .doOnNext { transactions -> println("Found transactions: $transactions") } - } - - - - @Cacheable("transactions") - fun getTransactionById(id: String): Mono { - return transactionsRepo.findById(id) - .map { - it - } - .switchIfEmpty( - Mono.error(IllegalArgumentException("Transaction with id: $id not found")) - ) - } - - - @CacheEvict(cacheNames = ["transactions"], allEntries = true) - fun createTransaction(transaction: Transaction): Mono { - return ReactiveSecurityContextHolder.getContext() - .map { it.authentication } // Получаем Authentication из SecurityContext - .flatMap { authentication -> - val username = authentication.name // Имя пользователя из токена - // Получаем пользователя и сохраняем транзакцию - userService.getByUsername(username) - .switchIfEmpty(Mono.error(IllegalArgumentException("User not found for username: $username"))) - .flatMap { user -> - transaction.user = user - transactionsRepo.save(transaction) - .doOnNext { savedTransaction -> - // Публикуем событие после сохранения - eventPublisher.publishEvent( - TransactionEvent( - this, - TransactionEventType.CREATE, - newTransaction = savedTransaction, - oldTransaction = savedTransaction - ) - ) - } - .map { it.id!! } // Возвращаем ID сохраненной транзакции - } - } - } - - - @CacheEvict(cacheNames = ["transactions"], allEntries = true) - - fun editTransaction(transaction: Transaction): Mono { - return transactionsRepo.findById(transaction.id!!) - .flatMap { oldStateOfTransaction -> - val changed = compareSumDateDoneIsChanged(oldStateOfTransaction, transaction) - if (!changed) { - return@flatMap transactionsRepo.save(transaction) // Сохраняем, если изменений нет - } - - val amountDifference = transaction.amount - oldStateOfTransaction.amount - - // Обработка дочерней транзакции - handleChildTransaction(oldStateOfTransaction, transaction, amountDifference) - .then(transactionsRepo.save(transaction)) // Сохраняем основную транзакцию - .doOnSuccess { savedTransaction -> - eventPublisher.publishEvent( - TransactionEvent( - this, - TransactionEventType.EDIT, - newTransaction = savedTransaction, - oldTransaction = oldStateOfTransaction, - difference = amountDifference - ) - ) - } - } - .switchIfEmpty( - Mono.error(IllegalArgumentException("Transaction not found with id: ${transaction.id}")) - ) - } - - private fun handleChildTransaction( - oldTransaction: Transaction, - newTransaction: Transaction, - amountDifference: Double - ): Mono { - return transactionsRepo.findByParentId(newTransaction.id!!) - .flatMap { childTransaction -> - logger.info(childTransaction.toString()) - // Если родительская транзакция обновлена, обновляем дочернюю - childTransaction.amount = newTransaction.amount - childTransaction.category = newTransaction.category - childTransaction.comment = newTransaction.comment - childTransaction.user = newTransaction.user - transactionsRepo.save(childTransaction) - } - .switchIfEmpty( - Mono.defer { - // Создание новой дочерней транзакции, если требуется - if (!oldTransaction.isDone && newTransaction.isDone) { - val newChildTransaction = newTransaction.copy( - id = null, - type = TransactionType("INSTANT", "Текущие"), - parentId = newTransaction.id - ) - transactionsRepo.save(newChildTransaction).doOnSuccess { savedChildTransaction -> - eventPublisher.publishEvent( - TransactionEvent( - this, - TransactionEventType.CREATE, - newTransaction = savedChildTransaction, - oldTransaction = oldTransaction, - difference = amountDifference - ) - ) - } - } else Mono.empty() - } - ) - .flatMap { - // Удаление дочерней транзакции, если родительская помечена как не выполненная - if (oldTransaction.isDone && !newTransaction.isDone) { - transactionsRepo.findByParentId(newTransaction.id!!) - .flatMap { child -> - deleteTransaction(child.id!!) - }.then() - } else { - Mono.empty() - } - } - } - - - fun compareSumDateDoneIsChanged(t1: Transaction, t2: Transaction): Boolean { - return if (t1.amount != t2.amount) { - true - } else if (t1.date != t2.date) { - true - } else if (t1.isDone != t2.isDone) { - true - } else if (t1.category.id != t2.category.id) { - return true - } else { - return false - } - } - - @CacheEvict(cacheNames = ["transactions"], allEntries = true) - fun deleteTransaction(transactionId: String): Mono { - return transactionsRepo.findById(transactionId) - .flatMap { transactionToDelete -> - transactionsRepo.deleteById(transactionId) // Удаляем транзакцию - .then( - Mono.fromRunnable { - // Публикуем событие после успешного удаления - eventPublisher.publishEvent( - TransactionEvent( - this, - TransactionEventType.DELETE, - newTransaction = transactionToDelete, - oldTransaction = transactionToDelete - ) - ) - } - ) - } - - } - - -// @CacheEvict(cacheNames = ["transactions", "childTransactions"], allEntries = true) -// fun setTransactionDone(transaction: Transaction): Transaction { -// val oldStateTransaction = transactionsRepo.findById(transaction.id!!) -// .orElseThrow { RuntimeException("Transaction ${transaction.id} not found") } +//package space.luminic.budgerapp.services // -// if (transaction.isDone) { -// if (oldStateTransaction.isDone) { -// throw RuntimeException("Transaction ${transaction.id} is already done") -// } // -// // Создание дочерней транзакции -// val childTransaction = transaction.copy( -// id = null, -// type = TransactionType("INSTANT", "Текущие"), -// parentId = transaction.id -// ) -// createTransaction(childTransaction) -// } else { -// // Удаление дочерней транзакции, если она существует -// transactionsRepo.findByParentId(transaction.id!!).getOrNull()?.let { -// deleteTransaction(it.id!!) -// } ?: logger.warn("Child transaction of parent ${transaction.id} not found") +//import org.bson.Document +//import org.bson.types.ObjectId +//import org.slf4j.LoggerFactory +//import org.springframework.cache.annotation.CacheEvict +//import org.springframework.cache.annotation.Cacheable +//import org.springframework.context.ApplicationEventPublisher +//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.aggregation.Aggregation.group +//import org.springframework.data.mongodb.core.aggregation.Aggregation.limit +//import org.springframework.data.mongodb.core.aggregation.Aggregation.lookup +//import org.springframework.data.mongodb.core.aggregation.Aggregation.match +//import org.springframework.data.mongodb.core.aggregation.Aggregation.newAggregation +//import org.springframework.data.mongodb.core.aggregation.Aggregation.project +//import org.springframework.data.mongodb.core.aggregation.Aggregation.skip +//import org.springframework.data.mongodb.core.aggregation.Aggregation.sort +//import org.springframework.data.mongodb.core.aggregation.Aggregation.unwind +//import org.springframework.data.mongodb.core.aggregation.DateOperators.DateToString +//import org.springframework.data.mongodb.core.query.Criteria +//import org.springframework.data.mongodb.core.query.Query +//import org.springframework.security.core.context.ReactiveSecurityContextHolder +//import org.springframework.stereotype.Service +//import reactor.core.publisher.Flux +//import reactor.core.publisher.Mono +//import space.luminic.budgerapp.models.Budget +//import space.luminic.budgerapp.models.Category +//import space.luminic.budgerapp.models.CategoryType +//import space.luminic.budgerapp.models.SortSetting +//import space.luminic.budgerapp.models.Transaction +//import space.luminic.budgerapp.models.TransactionEvent +//import space.luminic.budgerapp.models.TransactionEventType +//import space.luminic.budgerapp.models.TransactionType +//import space.luminic.budgerapp.models.User +//import space.luminic.budgerapp.repos.TransactionRepo +//import java.time.LocalDate +//import java.time.LocalDateTime +//import java.time.LocalTime +//import java.time.ZoneId +//import java.time.ZoneOffset +//import java.time.temporal.TemporalAdjusters +//import java.util.ArrayList +//import java.util.Date +// +//@Service +//class TransactionService( +// private val reactiveMongoTemplate: ReactiveMongoTemplate, +// val transactionsRepo: TransactionRepo, +// val userService: UserService, +// private val eventPublisher: ApplicationEventPublisher +//) { +// +// private val logger = LoggerFactory.getLogger(TransactionService::class.java) +// +// @Cacheable("transactions") +// fun getTransactions( +// dateFrom: LocalDate? = null, +// dateTo: LocalDate? = null, +// transactionType: String? = null, +// isDone: Boolean? = null, +// categoryId: String? = null, +// categoryType: String? = null, +// userId: String? = null, +// parentId: String? = null, +// isChild: Boolean? = null, +// sortSetting: SortSetting? = null, +// limit: Int? = null, +// offset: Int? = null, +// ): Mono> { +// val matchCriteria = mutableListOf() +// +// // Добавляем фильтры +// dateFrom?.let { matchCriteria.add(Criteria.where("date").gte(it)) } +// dateTo?.let { matchCriteria.add(Criteria.where("date").lt(it)) } +// transactionType?.let { matchCriteria.add(Criteria.where("type.code").`is`(it)) } +// isDone?.let { matchCriteria.add(Criteria.where("isDone").`is`(it)) } +// categoryId?.let { matchCriteria.add(Criteria.where("categoryDetails._id").`is`(it)) } +// categoryType?.let { matchCriteria.add(Criteria.where("categoryDetails.type.code").`is`(it)) } +// userId?.let { matchCriteria.add(Criteria.where("userDetails._id").`is`(ObjectId(it))) } +// parentId?.let { matchCriteria.add(Criteria.where("parentId").`is`(it)) } +// isChild?.let { matchCriteria.add(Criteria.where("parentId").exists(it)) } +// +// // Сборка агрегации +// val lookup = lookup("categories", "category.\$id", "_id", "categoryDetails") +// val lookupUsers = lookup("users", "user.\$id", "_id", "userDetails") +// val match = match(Criteria().andOperator(*matchCriteria.toTypedArray())) +// +// var sort = sort(Sort.by(Direction.DESC, "date").and(Sort.by(Direction.DESC, "createdAt"))) +// +// sortSetting?.let { +// sort = sort(Sort.by(it.order, it.by).and(Sort.by(Direction.ASC, "createdAt"))) // } // -// return editTransaction(transaction) -// } - - - @Cacheable("childTransactions", key = "#parentId") - fun getChildTransaction(parentId: String): Mono { - return transactionsRepo.findByParentId(parentId) - } - -// fun getTransactionByOldId(id: Int): Transaction? { -// return transactionsRepo.findByOldId(id).getOrNull() -// } - -// fun transferTransactions(): Mono { -// var transactions = transactionsRepoSQl.getTransactions() -// return transactionsRepo.saveAll(transactions).then() +// val aggregationBuilder = mutableListOf( +// lookup, +// lookupUsers, +// match.takeIf { matchCriteria.isNotEmpty() }, +// sort, +// offset?.let { skip(it.toLong()) }, +// limit?.let { limit(it.toLong()) } +// ).filterNotNull() +// +// val aggregation = newAggregation(aggregationBuilder) +// +// return reactiveMongoTemplate.aggregate( +// aggregation, "transactions", Transaction::class.java +// ) +// .collectList() // Преобразуем Flux в Mono> +// .map { it.toMutableList() } // } // - - fun calcTransactionsSum( - budget: Budget, - categoryId: String? = null, - categoryType: String? = null, - transactionType: String? = null, - isDone: Boolean? = null - ): Mono { - val matchCriteria = mutableListOf() - - // Добавляем фильтры - matchCriteria.add(Criteria.where("date").gte(budget.dateFrom)) - matchCriteria.add(Criteria.where("date").lt(budget.dateTo)) - categoryId?.let { matchCriteria.add(Criteria.where("category.\$id").`is`(ObjectId(it))) } - categoryType?.let { matchCriteria.add(Criteria.where("categoryDetails.type.code").`is`(it)) } - transactionType?.let { matchCriteria.add(Criteria.where("type.code").`is`(it)) } - isDone?.let { matchCriteria.add(Criteria.where("isDone").`is`(it)) } - - // Сборка агрегации - val lookup = lookup("categories", "category.\$id", "_id", "categoryDetails") - val unwind = unwind("categoryDetails") - val match = match(Criteria().andOperator(*matchCriteria.toTypedArray())) - val project = project("category").andExpression("{ \$toDouble: \"\$amount\" }").`as`("amount") - val group = group(categoryId ?: "all").sum("amount").`as`("totalSum") - val projectSum = project("totalSum") - val aggregation = newAggregation(lookup, unwind, match, project, group, projectSum) - - return reactiveMongoTemplate.aggregate(aggregation, "transactions", Map::class.java) - .map { result -> - val totalSum = result["totalSum"] - if (totalSum is Double) { - totalSum - } else { - 0.0 - } - } - .reduce(0.0) { acc, sum -> acc + sum } // Суммируем значения, если несколько результатов - } - - - // @Cacheable("transactions") - fun getAverageSpendingByCategory(): Mono> { - val firstDateOfMonth = LocalDate.now().with(TemporalAdjusters.firstDayOfMonth()) - - val lookup = lookup("categories", "category.\$id", "_id", "categoryDetails") - val unwind = unwind("categoryDetails") - val match = match( - Criteria.where("categoryDetails.type.code").`is`("EXPENSE") - .and("type.code").`is`("INSTANT") - .and("date").lt(firstDateOfMonth) - ) - val projectDate = project("_id", "category", "amount", "categoryDetails") - .and(DateToString.dateOf("date").toString("%Y-%m")).`as`("month") - .andExpression("{ \$toDouble: \"\$amount\" }").`as`("amount") - val groupByMonthAndCategory = group("month", "category.\$id").sum("amount").`as`("sum") - val groupByCategory = group("_id.id").avg("sum").`as`("averageAmount") - val project = project() - .and("_id").`as`("category") - .and("averageAmount").`as`("avgAmount") - val sort = sort(Sort.by(Sort.Order.asc("_id"))) - - val aggregation = newAggregation( - lookup, unwind, match, projectDate, groupByMonthAndCategory, groupByCategory, project, sort - ) - - return reactiveMongoTemplate.aggregate(aggregation, "transactions", Map::class.java) - .collectList() - .map { results -> - results.associate { result -> - val category = result["category"]?.toString() ?: "Unknown" - val avgAmount = (result["avgAmount"] as? Double) ?: 0.0 - category to avgAmount - } - } - .defaultIfEmpty(emptyMap()) // Возвращаем пустую карту, если результатов нет - } - - - @Cacheable("transactionTypes") - fun getTransactionTypes(): List { - var types = mutableListOf() - types.add(TransactionType("PLANNED", "Плановые")) - types.add(TransactionType("INSTANT", "Текущие")) - return types - } - - fun getAverageIncome(): Mono { - val lookup = lookup("categories", "category.\$id", "_id", "detailedCategory") - - val unwind = unwind("detailedCategory") - - val match = match( - Criteria.where("detailedCategory.type.code").`is`("INCOME") - .and("type.code").`is`("INSTANT") - .and("isDone").`is`(true) - ) - - val project = project("_id", "category", "detailedCategory") - .and(DateToString.dateOf("date").toString("%Y-%m")).`as`("month") - .andExpression("{ \$toDouble: \"\$amount\" }").`as`("amount") - - val groupByMonth = group("month").sum("amount").`as`("sum") - - val groupForAverage = group("avgIncomeByMonth").avg("sum").`as`("averageAmount") - - val aggregation = newAggregation(lookup, unwind, match, project, groupByMonth, groupForAverage) - - return reactiveMongoTemplate.aggregate(aggregation, "transactions", Map::class.java) - .singleOrEmpty() // Ожидаем только один результат - .map { result -> - result["averageAmount"] as? Double ?: 0.0 - } - .defaultIfEmpty(0.0) // Если результат пустой, возвращаем 0.0 - } - - - fun getTransactionsByTypes(dateFrom: LocalDate, dateTo: LocalDate): Mono>> { - logger.info("here tran starts") - val pipeline = listOf( - Document( - "\$lookup", - Document("from", "categories") - .append("localField", "category.\$id") - .append("foreignField", "_id") - .append("as", "categoryDetailed") - ), - Document( - "\$lookup", - Document("from", "users") - .append("localField", "user.\$id") - .append("foreignField", "_id") - .append("as", "userDetailed") - ), - Document( - "\$unwind", - Document("path", "\$categoryDetailed").append("preserveNullAndEmptyArrays", true) - ), - Document( - "\$unwind", - Document("path", "\$userDetailed").append("preserveNullAndEmptyArrays", true) - ), - Document( - "\$match", - Document( - "\$and", listOf( - Document( - "date", - Document( - "\$gte", - Date.from( - LocalDateTime.of(dateFrom, LocalTime.MIN) - .atZone(ZoneId.systemDefault()) - .withZoneSameInstant(ZoneOffset.UTC) - .toInstant() - ) - ) - ), - Document( - "date", - Document( - "\$lt", - Date.from( - LocalDateTime.of(dateTo, LocalTime.MAX) - .atZone(ZoneId.systemDefault()) - .withZoneSameInstant(ZoneOffset.UTC) - .toInstant() - ) - ) - ) - ) - ) - ), - Document( - "\$facet", - Document( - "plannedExpenses", - listOf( - Document( - "\$match", - Document("type.code", "PLANNED") - .append("categoryDetailed.type.code", "EXPENSE") - ), - Document("\$sort", Document("date", 1).append("_id", 1)) - ) - ) - .append( - "plannedIncomes", - listOf( - Document( - "\$match", - Document("type.code", "PLANNED") - .append("categoryDetailed.type.code", "INCOME") - ), - Document("\$sort", Document("date", 1).append("_id", 1)) - ) - ) - .append( - "instantTransactions", - listOf( - Document("\$match", Document("type.code", "INSTANT")), - Document("\$sort", Document("date", 1).append("_id", 1)) - ) - ) - ) - ) - -// getCategoriesExplainReactive(pipeline) -// .doOnNext { explainResult -> -// logger.info("Explain Result: ${explainResult.toJson()}") +// fun getTransactionsToDelete(dateFrom: LocalDate, dateTo: LocalDate): Mono> { +// val criteria = Criteria().andOperator( +// Criteria.where("date").gte(dateFrom), +// Criteria.where("date").lte(dateTo), +// Criteria().orOperator( +// Criteria.where("type.code").`is`("PLANNED"), +// Criteria.where("parentId").exists(true) +// ) +// ) +// +//// Пример использования в MongoTemplate: +// val query = Query(criteria) +// +//// Если вы хотите использовать ReactiveMongoTemplate: +// return reactiveMongoTemplate.find(query, Transaction::class.java) +// .collectList() +// .doOnNext { transactions -> println("Found transactions: $transactions") } +// } +// +// +// +// @Cacheable("transactions") +// fun getTransactionById(id: String): Mono { +// return transactionsRepo.findById(id) +// .map { +// it // } -// .subscribe() // Этот вызов лучше оставить только для отладки - return reactiveMongoTemplate.getCollection("transactions") - .flatMapMany { it.aggregate(pipeline, Document::class.java) } - .single() // Получаем только первый результат агрегации - .flatMap { aggregationResult -> - Mono.zip( - extractTransactions(aggregationResult, "plannedExpenses"), - extractTransactions(aggregationResult, "plannedIncomes"), - extractTransactions(aggregationResult, "instantTransactions") - ).map { tuple -> - val plannedExpenses = tuple.t1 - val plannedIncomes = tuple.t2 - val instantTransactions = tuple.t3 - logger.info("here tran ends") - mapOf( - "plannedExpenses" to plannedExpenses, - "plannedIncomes" to plannedIncomes, - "instantTransactions" to instantTransactions - ) - - - } - - } - - - } - - - private fun extractTransactions(aggregationResult: Document, key: String): Mono> { - val resultTransactions = aggregationResult[key] as? List ?: emptyList() - return Flux.fromIterable(resultTransactions) - .map { documentToTransactionMapper(it) } - .collectList() - } - - - private fun documentToTransactionMapper(document: Document): Transaction { - val transactionType = document["type"] as Document - var user: User? = null - - val userDocument = document["userDetailed"] as Document - user = User( - id = (userDocument["_id"] as ObjectId).toString(), - username = userDocument["username"] as String, - firstName = userDocument["firstName"] as String, - tgId = userDocument["tgId"] as String, - tgUserName = userDocument["tgUserName"]?.let { it as String }, - password = null, - isActive = userDocument["isActive"] as Boolean, - regDate = userDocument["regDate"] as Date, - createdAt = userDocument["createdAt"] as Date, - roles = userDocument["roles"] as ArrayList, - ) - - - val categoryDocument = document["categoryDetailed"] as Document - val categoryTypeDocument = categoryDocument["type"] as Document - val category = Category( - id = (categoryDocument["_id"] as ObjectId).toString(), - type = CategoryType(categoryTypeDocument["code"] as String, categoryTypeDocument["name"] as String), - name = categoryDocument["name"] as String, - description = categoryDocument["description"] as String, - icon = categoryDocument["icon"] as String - ) - return Transaction( - (document["_id"] as ObjectId).toString(), - TransactionType( - transactionType["code"] as String, - transactionType["name"] as String - ), - user = user, - category = category, - comment = document["comment"] as String, - date = (document["date"] as Date).toInstant().atZone(ZoneId.systemDefault()).toLocalDate(), - amount = document["amount"] as Double, - isDone = document["isDone"] as Boolean, - parentId = if (document["parentId"] != null) document["parentId"] as String else null, - createdAt = LocalDateTime.ofInstant((document["createdAt"] as Date).toInstant(), ZoneOffset.UTC), - ) - - } -} \ No newline at end of file +// .switchIfEmpty( +// Mono.error(IllegalArgumentException("Transaction with id: $id not found")) +// ) +// } +// +// +// @CacheEvict(cacheNames = ["transactions"], allEntries = true) +// fun createTransaction(transaction: Transaction): Mono { +// return ReactiveSecurityContextHolder.getContext() +// .map { it.authentication } // Получаем Authentication из SecurityContext +// .flatMap { authentication -> +// val username = authentication.name // Имя пользователя из токена +// // Получаем пользователя и сохраняем транзакцию +// userService.getByUsername(username) +// .switchIfEmpty(Mono.error(IllegalArgumentException("User not found for username: $username"))) +// .flatMap { user -> +// transaction.user = user +// transactionsRepo.save(transaction) +// .doOnNext { savedTransaction -> +// // Публикуем событие после сохранения +// eventPublisher.publishEvent( +// TransactionEvent( +// this, +// TransactionEventType.CREATE, +// newTransaction = savedTransaction, +// oldTransaction = savedTransaction +// ) +// ) +// } +// .map { it.id!! } // Возвращаем ID сохраненной транзакции +// } +// } +// } +// +// +// @CacheEvict(cacheNames = ["transactions"], allEntries = true) +// +// fun editTransaction(transaction: Transaction): Mono { +// return transactionsRepo.findById(transaction.id!!) +// .flatMap { oldStateOfTransaction -> +// val changed = compareSumDateDoneIsChanged(oldStateOfTransaction, transaction) +// if (!changed) { +// return@flatMap transactionsRepo.save(transaction) // Сохраняем, если изменений нет +// } +// +// val amountDifference = transaction.amount - oldStateOfTransaction.amount +// +// // Обработка дочерней транзакции +// handleChildTransaction(oldStateOfTransaction, transaction, amountDifference) +// .then(transactionsRepo.save(transaction)) // Сохраняем основную транзакцию +// .doOnSuccess { savedTransaction -> +// eventPublisher.publishEvent( +// TransactionEvent( +// this, +// TransactionEventType.EDIT, +// newTransaction = savedTransaction, +// oldTransaction = oldStateOfTransaction, +// difference = amountDifference +// ) +// ) +// } +// } +// .switchIfEmpty( +// Mono.error(IllegalArgumentException("Transaction not found with id: ${transaction.id}")) +// ) +// } +// +// private fun handleChildTransaction( +// oldTransaction: Transaction, +// newTransaction: Transaction, +// amountDifference: Double +// ): Mono { +// return transactionsRepo.findByParentId(newTransaction.id!!) +// .flatMap { childTransaction -> +// logger.info(childTransaction.toString()) +// // Если родительская транзакция обновлена, обновляем дочернюю +// childTransaction.amount = newTransaction.amount +// childTransaction.category = newTransaction.category +// childTransaction.comment = newTransaction.comment +// childTransaction.user = newTransaction.user +// transactionsRepo.save(childTransaction) +// } +// .switchIfEmpty( +// Mono.defer { +// // Создание новой дочерней транзакции, если требуется +// if (!oldTransaction.isDone && newTransaction.isDone) { +// val newChildTransaction = newTransaction.copy( +// id = null, +// type = TransactionType("INSTANT", "Текущие"), +// parentId = newTransaction.id +// ) +// transactionsRepo.save(newChildTransaction).doOnSuccess { savedChildTransaction -> +// eventPublisher.publishEvent( +// TransactionEvent( +// this, +// TransactionEventType.CREATE, +// newTransaction = savedChildTransaction, +// oldTransaction = oldTransaction, +// difference = amountDifference +// ) +// ) +// } +// } else Mono.empty() +// } +// ) +// .flatMap { +// // Удаление дочерней транзакции, если родительская помечена как не выполненная +// if (oldTransaction.isDone && !newTransaction.isDone) { +// transactionsRepo.findByParentId(newTransaction.id!!) +// .flatMap { child -> +// deleteTransaction(child.id!!) +// }.then() +// } else { +// Mono.empty() +// } +// } +// } +// +// +// fun compareSumDateDoneIsChanged(t1: Transaction, t2: Transaction): Boolean { +// return if (t1.amount != t2.amount) { +// true +// } else if (t1.date != t2.date) { +// true +// } else if (t1.isDone != t2.isDone) { +// true +// } else if (t1.category.id != t2.category.id) { +// return true +// } else { +// return false +// } +// } +// +// @CacheEvict(cacheNames = ["transactions"], allEntries = true) +// fun deleteTransaction(transactionId: String): Mono { +// return transactionsRepo.findById(transactionId) +// .flatMap { transactionToDelete -> +// transactionsRepo.deleteById(transactionId) // Удаляем транзакцию +// .then( +// Mono.fromRunnable { +// // Публикуем событие после успешного удаления +// eventPublisher.publishEvent( +// TransactionEvent( +// this, +// TransactionEventType.DELETE, +// newTransaction = transactionToDelete, +// oldTransaction = transactionToDelete +// ) +// ) +// } +// ) +// } +// +// } +// +// +//// @CacheEvict(cacheNames = ["transactions", "childTransactions"], allEntries = true) +//// fun setTransactionDone(transaction: Transaction): Transaction { +//// val oldStateTransaction = transactionsRepo.findById(transaction.id!!) +//// .orElseThrow { RuntimeException("Transaction ${transaction.id} not found") } +//// +//// if (transaction.isDone) { +//// if (oldStateTransaction.isDone) { +//// throw RuntimeException("Transaction ${transaction.id} is already done") +//// } +//// +//// // Создание дочерней транзакции +//// val childTransaction = transaction.copy( +//// id = null, +//// type = TransactionType("INSTANT", "Текущие"), +//// parentId = transaction.id +//// ) +//// createTransaction(childTransaction) +//// } else { +//// // Удаление дочерней транзакции, если она существует +//// transactionsRepo.findByParentId(transaction.id!!).getOrNull()?.let { +//// deleteTransaction(it.id!!) +//// } ?: logger.warn("Child transaction of parent ${transaction.id} not found") +//// } +//// +//// return editTransaction(transaction) +//// } +// +// +// @Cacheable("childTransactions", key = "#parentId") +// fun getChildTransaction(parentId: String): Mono { +// return transactionsRepo.findByParentId(parentId) +// } +// +//// fun getTransactionByOldId(id: Int): Transaction? { +//// return transactionsRepo.findByOldId(id).getOrNull() +//// } +// +//// fun transferTransactions(): Mono { +//// var transactions = transactionsRepoSQl.getTransactions() +//// return transactionsRepo.saveAll(transactions).then() +//// } +//// +// +// fun calcTransactionsSum( +// budget: Budget, +// categoryId: String? = null, +// categoryType: String? = null, +// transactionType: String? = null, +// isDone: Boolean? = null +// ): Mono { +// val matchCriteria = mutableListOf() +// +// // Добавляем фильтры +// matchCriteria.add(Criteria.where("date").gte(budget.dateFrom)) +// matchCriteria.add(Criteria.where("date").lt(budget.dateTo)) +// categoryId?.let { matchCriteria.add(Criteria.where("category.\$id").`is`(ObjectId(it))) } +// categoryType?.let { matchCriteria.add(Criteria.where("categoryDetails.type.code").`is`(it)) } +// transactionType?.let { matchCriteria.add(Criteria.where("type.code").`is`(it)) } +// isDone?.let { matchCriteria.add(Criteria.where("isDone").`is`(it)) } +// +// // Сборка агрегации +// val lookup = lookup("categories", "category.\$id", "_id", "categoryDetails") +// val unwind = unwind("categoryDetails") +// val match = match(Criteria().andOperator(*matchCriteria.toTypedArray())) +// val project = project("category").andExpression("{ \$toDouble: \"\$amount\" }").`as`("amount") +// val group = group(categoryId ?: "all").sum("amount").`as`("totalSum") +// val projectSum = project("totalSum") +// val aggregation = newAggregation(lookup, unwind, match, project, group, projectSum) +// +// return reactiveMongoTemplate.aggregate(aggregation, "transactions", Map::class.java) +// .map { result -> +// val totalSum = result["totalSum"] +// if (totalSum is Double) { +// totalSum +// } else { +// 0.0 +// } +// } +// .reduce(0.0) { acc, sum -> acc + sum } // Суммируем значения, если несколько результатов +// } +// +// +// // @Cacheable("transactions") +// fun getAverageSpendingByCategory(): Mono> { +// val firstDateOfMonth = LocalDate.now().with(TemporalAdjusters.firstDayOfMonth()) +// +// val lookup = lookup("categories", "category.\$id", "_id", "categoryDetails") +// val unwind = unwind("categoryDetails") +// val match = match( +// Criteria.where("categoryDetails.type.code").`is`("EXPENSE") +// .and("type.code").`is`("INSTANT") +// .and("date").lt(firstDateOfMonth) +// ) +// val projectDate = project("_id", "category", "amount", "categoryDetails") +// .and(DateToString.dateOf("date").toString("%Y-%m")).`as`("month") +// .andExpression("{ \$toDouble: \"\$amount\" }").`as`("amount") +// val groupByMonthAndCategory = group("month", "category.\$id").sum("amount").`as`("sum") +// val groupByCategory = group("_id.id").avg("sum").`as`("averageAmount") +// val project = project() +// .and("_id").`as`("category") +// .and("averageAmount").`as`("avgAmount") +// val sort = sort(Sort.by(Sort.Order.asc("_id"))) +// +// val aggregation = newAggregation( +// lookup, unwind, match, projectDate, groupByMonthAndCategory, groupByCategory, project, sort +// ) +// +// return reactiveMongoTemplate.aggregate(aggregation, "transactions", Map::class.java) +// .collectList() +// .map { results -> +// results.associate { result -> +// val category = result["category"]?.toString() ?: "Unknown" +// val avgAmount = (result["avgAmount"] as? Double) ?: 0.0 +// category to avgAmount +// } +// } +// .defaultIfEmpty(emptyMap()) // Возвращаем пустую карту, если результатов нет +// } +// +// +// @Cacheable("transactionTypes") +// fun getTransactionTypes(): List { +// var types = mutableListOf() +// types.add(TransactionType("PLANNED", "Плановые")) +// types.add(TransactionType("INSTANT", "Текущие")) +// return types +// } +// +// fun getAverageIncome(): Mono { +// val lookup = lookup("categories", "category.\$id", "_id", "detailedCategory") +// +// val unwind = unwind("detailedCategory") +// +// val match = match( +// Criteria.where("detailedCategory.type.code").`is`("INCOME") +// .and("type.code").`is`("INSTANT") +// .and("isDone").`is`(true) +// ) +// +// val project = project("_id", "category", "detailedCategory") +// .and(DateToString.dateOf("date").toString("%Y-%m")).`as`("month") +// .andExpression("{ \$toDouble: \"\$amount\" }").`as`("amount") +// +// val groupByMonth = group("month").sum("amount").`as`("sum") +// +// val groupForAverage = group("avgIncomeByMonth").avg("sum").`as`("averageAmount") +// +// val aggregation = newAggregation(lookup, unwind, match, project, groupByMonth, groupForAverage) +// +// return reactiveMongoTemplate.aggregate(aggregation, "transactions", Map::class.java) +// .singleOrEmpty() // Ожидаем только один результат +// .map { result -> +// result["averageAmount"] as? Double ?: 0.0 +// } +// .defaultIfEmpty(0.0) // Если результат пустой, возвращаем 0.0 +// } +// +// +// fun getTransactionsByTypes(dateFrom: LocalDate, dateTo: LocalDate): Mono>> { +// logger.info("here tran starts") +// val pipeline = listOf( +// Document( +// "\$lookup", +// Document("from", "categories") +// .append("localField", "category.\$id") +// .append("foreignField", "_id") +// .append("as", "categoryDetailed") +// ), +// Document( +// "\$lookup", +// Document("from", "users") +// .append("localField", "user.\$id") +// .append("foreignField", "_id") +// .append("as", "userDetailed") +// ), +// Document( +// "\$unwind", +// Document("path", "\$categoryDetailed").append("preserveNullAndEmptyArrays", true) +// ), +// Document( +// "\$unwind", +// Document("path", "\$userDetailed").append("preserveNullAndEmptyArrays", true) +// ), +// Document( +// "\$match", +// Document( +// "\$and", listOf( +// Document( +// "date", +// Document( +// "\$gte", +// Date.from( +// LocalDateTime.of(dateFrom, LocalTime.MIN) +// .atZone(ZoneId.systemDefault()) +// .withZoneSameInstant(ZoneOffset.UTC) +// .toInstant() +// ) +// ) +// ), +// Document( +// "date", +// Document( +// "\$lt", +// Date.from( +// LocalDateTime.of(dateTo, LocalTime.MAX) +// .atZone(ZoneId.systemDefault()) +// .withZoneSameInstant(ZoneOffset.UTC) +// .toInstant() +// ) +// ) +// ) +// ) +// ) +// ), +// Document( +// "\$facet", +// Document( +// "plannedExpenses", +// listOf( +// Document( +// "\$match", +// Document("type.code", "PLANNED") +// .append("categoryDetailed.type.code", "EXPENSE") +// ), +// Document("\$sort", Document("date", 1).append("_id", 1)) +// ) +// ) +// .append( +// "plannedIncomes", +// listOf( +// Document( +// "\$match", +// Document("type.code", "PLANNED") +// .append("categoryDetailed.type.code", "INCOME") +// ), +// Document("\$sort", Document("date", 1).append("_id", 1)) +// ) +// ) +// .append( +// "instantTransactions", +// listOf( +// Document("\$match", Document("type.code", "INSTANT")), +// Document("\$sort", Document("date", 1).append("_id", 1)) +// ) +// ) +// ) +// ) +// +//// getCategoriesExplainReactive(pipeline) +//// .doOnNext { explainResult -> +//// logger.info("Explain Result: ${explainResult.toJson()}") +//// } +//// .subscribe() // Этот вызов лучше оставить только для отладки +// return reactiveMongoTemplate.getCollection("transactions") +// .flatMapMany { it.aggregate(pipeline, Document::class.java) } +// .single() // Получаем только первый результат агрегации +// .flatMap { aggregationResult -> +// Mono.zip( +// extractTransactions(aggregationResult, "plannedExpenses"), +// extractTransactions(aggregationResult, "plannedIncomes"), +// extractTransactions(aggregationResult, "instantTransactions") +// ).map { tuple -> +// val plannedExpenses = tuple.t1 +// val plannedIncomes = tuple.t2 +// val instantTransactions = tuple.t3 +// logger.info("here tran ends") +// mapOf( +// "plannedExpenses" to plannedExpenses, +// "plannedIncomes" to plannedIncomes, +// "instantTransactions" to instantTransactions +// ) +// +// +// } +// +// } +// +// +// } +// +// +// private fun extractTransactions(aggregationResult: Document, key: String): Mono> { +// val resultTransactions = aggregationResult[key] as? List ?: emptyList() +// return Flux.fromIterable(resultTransactions) +// .map { documentToTransactionMapper(it) } +// .collectList() +// } +// +// +// private fun documentToTransactionMapper(document: Document): Transaction { +// val transactionType = document["type"] as Document +// var user: User? = null +// +// val userDocument = document["userDetailed"] as Document +// user = User( +// id = (userDocument["_id"] as ObjectId).toString(), +// username = userDocument["username"] as String, +// firstName = userDocument["firstName"] as String, +// tgId = userDocument["tgId"] as String, +// tgUserName = userDocument["tgUserName"]?.let { it as String }, +// password = null, +// isActive = userDocument["isActive"] as Boolean, +// regDate = userDocument["regDate"] as Date, +// createdAt = userDocument["createdAt"] as Date, +// roles = userDocument["roles"] as ArrayList, +// ) +// +// +// val categoryDocument = document["categoryDetailed"] as Document +// val categoryTypeDocument = categoryDocument["type"] as Document +// val category = Category( +// id = (categoryDocument["_id"] as ObjectId).toString(), +// type = CategoryType(categoryTypeDocument["code"] as String, categoryTypeDocument["name"] as String), +// name = categoryDocument["name"] as String, +// description = categoryDocument["description"] as String, +// icon = categoryDocument["icon"] as String +// ) +// return Transaction( +// (document["_id"] as ObjectId).toString(), +// TransactionType( +// transactionType["code"] as String, +// transactionType["name"] as String +// ), +// user = user, +// category = category, +// comment = document["comment"] as String, +// date = (document["date"] as Date).toInstant().atZone(ZoneId.systemDefault()).toLocalDate(), +// amount = document["amount"] as Double, +// isDone = document["isDone"] as Boolean, +// parentId = if (document["parentId"] != null) document["parentId"] as String else null, +// createdAt = LocalDateTime.ofInstant((document["createdAt"] as Date).toInstant(), ZoneOffset.UTC), +// ) +// +// } +//} \ No newline at end of file diff --git a/src/main/resources/application-prod.properties b/src/main/resources/application-prod.properties index 69e8322..4ab26ba 100644 --- a/src/main/resources/application-prod.properties +++ b/src/main/resources/application-prod.properties @@ -5,12 +5,12 @@ spring.application.name=budger-app spring.data.mongodb.uri=mongodb://budger-app:BA1q2w3e4r!@luminic.space:27017/budger-app?authSource=admin&minPoolSize=10&maxPoolSize=100 -logging.level.org.springframework.web=DEBUG -logging.level.org.springframework.data = DEBUG +#logging.level.org.springframework.web=DEBUG +#logging.level.org.springframework.data = DEBUG logging.level.org.springframework.data.mongodb.core.ReactiveMongoTemplate=DEBUG -logging.level.org.springframework.security = DEBUG +#logging.level.org.springframework.security = DEBUG logging.level.org.springframework.data.mongodb.code = DEBUG -logging.level.org.springframework.web.reactive=DEBUG +#logging.level.org.springframework.web.reactive=DEBUG #management.endpoints.web.exposure.include=* #management.endpoint.metrics.access=read_only diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index b804bb6..e1c17cb 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -9,12 +9,12 @@ spring.main.web-application-type=reactive -logging.level.org.springframework.web=DEBUG -logging.level.org.springframework.data = DEBUG -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 +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 server.compression.enabled=true server.compression.mime-types=application/json