diff --git a/src/main/kotlin/space/luminic/budgerapp/controllers/BudgetController.kt b/src/main/kotlin/space/luminic/budgerapp/controllers/BudgetController.kt index 82a8c7d..0b8166b 100644 --- a/src/main/kotlin/space/luminic/budgerapp/controllers/BudgetController.kt +++ b/src/main/kotlin/space/luminic/budgerapp/controllers/BudgetController.kt @@ -34,10 +34,10 @@ class BudgetController( return financialService.getBudgets("123") } - @GetMapping("/{id}") - fun getBudget(@PathVariable id: String): Mono { - return financialService.getBudget(id) - } +// @GetMapping("/{id}") +// fun getBudget(@PathVariable id: String): Mono { +// return financialService.getBudget(id) +// } // @GetMapping("/by-dates") @@ -51,10 +51,10 @@ class BudgetController( return ResponseEntity.ok(financialService.getBudgetCategories(id)) } - @GetMapping("/{id}/transactions") - fun getBudgetTransactions(@PathVariable id: String): Mono>> { - return financialService.getBudgetTransactionsByType(id) - } +// @GetMapping("/{id}/transactions") +// fun getBudgetTransactions(@PathVariable id: String): Mono>> { +// return financialService.getBudgetTransactionsByType(id) +// } // @PostMapping("/") // fun createBudget(@RequestBody budgetCreationDTO: BudgetCreationDTO): Mono { @@ -64,23 +64,12 @@ class BudgetController( // ) // } - @DeleteMapping("/{id}") - fun deleteBudget(@PathVariable id: String): Mono { - return financialService.deleteBudget(id) - } +// @DeleteMapping("/{id}") +// fun deleteBudget(@PathVariable id: String): Mono { +// return financialService.deleteBudget(id) +// } + - @PostMapping("/{budgetId}/categories/{catId}/limit") - fun setCategoryLimit( - @PathVariable budgetId: String, - @PathVariable catId: String, - @RequestBody limit: LimitValue, - ): ResponseEntity { - return try { - ResponseEntity.ok(financialService.setCategoryLimit(budgetId, catId, limit.limit)) - } catch (e: Exception) { - ResponseEntity.badRequest().body(e.message) - } - } @GetMapping("/{id}/warns") diff --git a/src/main/kotlin/space/luminic/budgerapp/controllers/SpaceController.kt b/src/main/kotlin/space/luminic/budgerapp/controllers/SpaceController.kt index 62c4d7a..3133b85 100644 --- a/src/main/kotlin/space/luminic/budgerapp/controllers/SpaceController.kt +++ b/src/main/kotlin/space/luminic/budgerapp/controllers/SpaceController.kt @@ -7,6 +7,7 @@ import org.springframework.web.bind.annotation.* import org.springframework.web.client.HttpClientErrorException import reactor.core.publisher.Flux import reactor.core.publisher.Mono +import space.luminic.budgerapp.controllers.BudgetController.LimitValue import space.luminic.budgerapp.controllers.dtos.BudgetCreationDTO import space.luminic.budgerapp.models.* import space.luminic.budgerapp.services.CategoryService @@ -39,7 +40,10 @@ class SpaceController( @PostMapping fun createSpace(@RequestBody space: SpaceCreateDTO): Mono { - return spaceService.createSpace(Space(name=space.name, description = space.description), space.createCategories) + return spaceService.createSpace( + Space(name = space.name, description = space.description), + space.createCategories + ) } @@ -81,21 +85,53 @@ class SpaceController( // //Budgets API // - @GetMapping("{spaceId}/budgets") + @GetMapping("/{spaceId}/budgets") fun getBudgets(@PathVariable spaceId: String): Mono> { return financialService.getBudgets(spaceId) } + @GetMapping("/{spaceId}/budgets/{id}") + fun getBudget(@PathVariable spaceId: String, @PathVariable id: String): Mono { + return spaceService.isValidRequest(spaceId).flatMap { + financialService.getBudget(spaceId, id) + } + + } + @PostMapping("/{spaceId}/budgets") fun createBudget( @PathVariable spaceId: String, @RequestBody budgetCreationDTO: BudgetCreationDTO, ): Mono { - return financialService.createBudget(spaceId, budgetCreationDTO.budget, budgetCreationDTO.createRecurrent) + return spaceService.isValidRequest(spaceId).flatMap { + financialService.createBudget(spaceId, budgetCreationDTO.budget, budgetCreationDTO.createRecurrent) + } + } + @DeleteMapping("/{spaceId}/budgets/{id}") + fun deleteBudget(@PathVariable spaceId: String, @PathVariable id: String): Mono { + return spaceService.isValidRequest(spaceId).flatMap { + financialService.deleteBudget(spaceId, id) + } + } + @PostMapping("/{spaceId}/budgets/{budgetId}/categories/{catId}/limit") + fun setCategoryLimit( + @PathVariable spaceId: String, + @PathVariable budgetId: String, + @PathVariable catId: String, + @RequestBody limit: LimitValue, + ): Mono { + return spaceService.isValidRequest(spaceId).flatMap { + financialService.setCategoryLimit(it.id!!,budgetId, catId, limit.limit) + } + + } + + // // Transactions API + // @GetMapping("/{spaceId}/transactions") fun getTransactions( @PathVariable spaceId: String, @@ -125,7 +161,6 @@ class SpaceController( } - @GetMapping("/{spaceId}/transactions/{id}") fun getTransaction( @PathVariable spaceId: String, @@ -140,31 +175,29 @@ class SpaceController( } @PostMapping("/{spaceId}/transactions") - fun createTransaction(@PathVariable spaceId: String, @RequestBody transaction: Transaction): ResponseEntity { - try { - return ResponseEntity.ok(financialService.createTransaction(spaceId, transaction)) - } catch (e: Exception) { - e.printStackTrace() - return ResponseEntity(e.message, HttpStatus.INTERNAL_SERVER_ERROR) - + fun createTransaction(@PathVariable spaceId: String, @RequestBody transaction: Transaction): Mono { + return spaceService.isValidRequest(spaceId).flatMap { + financialService.createTransaction(spaceId, transaction) } + } @PutMapping("/{spaceId}/transactions/{id}") fun editTransaction( @PathVariable spaceId: String, @PathVariable id: String, @RequestBody transaction: Transaction - ): ResponseEntity { - try { - return ResponseEntity.ok(financialService.editTransaction(transaction)) - } catch (e: Exception) { - e.printStackTrace() - return ResponseEntity(e.message, HttpStatus.INTERNAL_SERVER_ERROR) + ): Mono { + return spaceService.isValidRequest(spaceId).flatMap { + transaction.space = it + financialService.editTransaction(transaction) } + } @DeleteMapping("/{spaceId}/transactions/{id}") fun deleteTransaction(@PathVariable spaceId: String, @PathVariable id: String): Mono { - return financialService.deleteTransaction(id) + return spaceService.isValidRequest(spaceId).flatMap { + financialService.getTransactionById(id).flatMap { financialService.deleteTransaction(it) } + } } // @@ -200,7 +233,7 @@ class SpaceController( @PathVariable spaceId: String, @RequestBody category: Category ): Mono { return spaceService.isValidRequest(spaceId).flatMap { - categoryService.createCategory(it,category) + financialService.createCategory(it, category) } } diff --git a/src/main/kotlin/space/luminic/budgerapp/controllers/TransactionController.kt b/src/main/kotlin/space/luminic/budgerapp/controllers/TransactionController.kt index 51cca00..d29ad98 100644 --- a/src/main/kotlin/space/luminic/budgerapp/controllers/TransactionController.kt +++ b/src/main/kotlin/space/luminic/budgerapp/controllers/TransactionController.kt @@ -83,12 +83,12 @@ class TransactionController(private val financialService: FinancialService) { } } - @DeleteMapping("/{id}") - fun deleteTransaction(@PathVariable id: String): Mono { - - return financialService.deleteTransaction(id) - - } +// @DeleteMapping("/{id}") +// fun deleteTransaction(@PathVariable id: String): Mono { +// +// return financialService.deleteTransaction(id) +// +// } @GetMapping("/{id}/child") diff --git a/src/main/kotlin/space/luminic/budgerapp/mappers/BudgetMapper.kt b/src/main/kotlin/space/luminic/budgerapp/mappers/BudgetMapper.kt new file mode 100644 index 0000000..2acd0ca --- /dev/null +++ b/src/main/kotlin/space/luminic/budgerapp/mappers/BudgetMapper.kt @@ -0,0 +1,55 @@ +package space.luminic.budgerapp.mappers + +import com.mongodb.DBRef +import org.bson.Document +import org.springframework.stereotype.Component +import space.luminic.budgerapp.models.* +import java.time.ZoneId + +@Component +class BudgetMapper : FromDocumentMapper { + override fun fromDocument(document: Document): Budget { + return Budget( + id = document.getObjectId("_id").toString(), + space = Space(id = document.get("spaceDetails", Document::class.java).getObjectId("_id").toString()), + name = document.getString("name"), + dateFrom = document.getDate("dateFrom").toInstant().atZone(ZoneId.systemDefault()).toLocalDate(), + dateTo = document.getDate("dateTo").toInstant().atZone(ZoneId.systemDefault()).toLocalDate(), + categories = document.getList("categories", Document::class.java).map { cat -> + val categoryDetailed = document.getList("categoriesDetails", Document::class.java).first { + it.getObjectId("_id").toString() == cat.get("category", DBRef::class.java).id.toString() + } + BudgetCategory( + category = Category( + id = cat.get("category", DBRef::class.java).id.toString(), + type = CategoryType( + categoryDetailed.get("type", Document::class.java).getString("code"), + categoryDetailed.get("type", Document::class.java).getString("name") + ), + name = categoryDetailed.getString("name"), + description = categoryDetailed.getString("description"), + icon = categoryDetailed.getString("icon") + ), currentLimit = cat.getDouble("currentLimit") + ) + }.toMutableList(), + incomeCategories = document.getList("incomeCategories", Document::class.java).map { cat -> + val categoryDetailed = + document.getList("incomeCategoriesDetails", Document::class.java).first { it -> + it.getObjectId("_id").toString() == cat.get("category", DBRef::class.java).id.toString() + } + BudgetCategory( + category = Category( + id = cat.get("category", DBRef::class.java).id.toString(), + type = CategoryType( + categoryDetailed.get("type", Document::class.java).getString("code"), + categoryDetailed.get("type", Document::class.java).getString("name") + ), + name = categoryDetailed.getString("name"), + description = categoryDetailed.getString("description"), + icon = categoryDetailed.getString("icon") + ), currentLimit = cat.getDouble("currentLimit") + ) + }.toMutableList() + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/budgerapp/mappers/FromDocumentMapper.kt b/src/main/kotlin/space/luminic/budgerapp/mappers/FromDocumentMapper.kt new file mode 100644 index 0000000..9847852 --- /dev/null +++ b/src/main/kotlin/space/luminic/budgerapp/mappers/FromDocumentMapper.kt @@ -0,0 +1,9 @@ +package space.luminic.budgerapp.mappers + +import org.bson.Document +import space.luminic.budgerapp.models.Budget + +interface FromDocumentMapper { + + fun fromDocument(document: Document): Any +} \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/budgerapp/mappers/TransactionsMapper.kt b/src/main/kotlin/space/luminic/budgerapp/mappers/TransactionsMapper.kt new file mode 100644 index 0000000..2961dc5 --- /dev/null +++ b/src/main/kotlin/space/luminic/budgerapp/mappers/TransactionsMapper.kt @@ -0,0 +1,47 @@ +package space.luminic.budgerapp.mappers + +import org.bson.Document +import org.bson.types.ObjectId +import org.springframework.stereotype.Component +import space.luminic.budgerapp.models.* +import java.time.ZoneId + +@Component +class TransactionsMapper : FromDocumentMapper { + + + override fun fromDocument(document: Document): Transaction { + val categoryDocument = document.get("categoryDetails", Document::class.java) + val categoryTypeDocument = categoryDocument["type"] as Document + val spaceDocument = document["spaceDetails"] as Document + val userDocument = document.get("userDetails", Document::class.java) + return Transaction( + id = document.getObjectId("_id").toString(), + type = TransactionType( + document.get("type", Document::class.java).getString("code"), + document.get("type", Document::class.java).getString("name") + ), + space = Space( + id = spaceDocument.getObjectId("_id").toString() + ), + 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 + ), + comment = document.getString("comment"), + date = document.getDate("date").toInstant().atZone(ZoneId.systemDefault()).toLocalDate(), + amount = document.getDouble("amount"), + isDone = document.getBoolean("isDone"), + user = User(userDocument.getObjectId("_id").toString(), userDocument.getString("username"), userDocument.getString("firstName"),), + parentId = document.getString("parentId"), + createdAt = document.getDate("createdAt").toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime(), + ) + + } +} \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/budgerapp/services/CategoryService.kt b/src/main/kotlin/space/luminic/budgerapp/services/CategoryService.kt index e913697..e210520 100644 --- a/src/main/kotlin/space/luminic/budgerapp/services/CategoryService.kt +++ b/src/main/kotlin/space/luminic/budgerapp/services/CategoryService.kt @@ -10,12 +10,7 @@ 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.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.skip -import org.springframework.data.mongodb.core.aggregation.Aggregation.sort +import org.springframework.data.mongodb.core.aggregation.Aggregation.* import org.springframework.data.mongodb.core.query.Criteria import org.springframework.data.mongodb.core.query.isEqualTo import org.springframework.stereotype.Service @@ -47,7 +42,6 @@ class CategoryService( } - // @Cacheable("categories") fun getCategories(spaceId: String, type: String? = null, sortBy: String, direction: String): Mono> { val matchCriteria = mutableListOf() @@ -62,11 +56,13 @@ class CategoryService( val sort = sort(Sort.by(direction, sortBy)) val lookupSpaces = lookup("spaces", "space.\$id", "_id", "spaceDetails") + val project = project("_id", "type", "name", "description", "icon") val aggregationBuilder = mutableListOf( lookupSpaces, match.takeIf { matchCriteria.isNotEmpty() }, + project, sort, ).filterNotNull() @@ -96,11 +92,7 @@ class CategoryService( return types } - @CacheEvict(cacheNames = ["getAllCategories"], allEntries = true) - fun createCategory(space: Space, category: Category): Mono { - category.space = space - return categoryRepo.save(category) - } + @CacheEvict(cacheNames = ["getAllCategories"], allEntries = true) fun editCategory(category: Category): Mono { diff --git a/src/main/kotlin/space/luminic/budgerapp/services/FinancialService.kt b/src/main/kotlin/space/luminic/budgerapp/services/FinancialService.kt index e4b3c26..de5daeb 100644 --- a/src/main/kotlin/space/luminic/budgerapp/services/FinancialService.kt +++ b/src/main/kotlin/space/luminic/budgerapp/services/FinancialService.kt @@ -1,5 +1,6 @@ package space.luminic.budgerapp.services +import com.mongodb.DBRef import org.bson.BsonNull import org.bson.Document import org.bson.types.ObjectId @@ -8,17 +9,19 @@ 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.MongoTemplate import org.springframework.data.mongodb.core.ReactiveMongoTemplate -import org.springframework.data.mongodb.core.aggregation.Aggregation import org.springframework.data.mongodb.core.aggregation.Aggregation.* import org.springframework.data.mongodb.core.aggregation.DateOperators.DateToString +import org.springframework.data.mongodb.core.aggregation.SortOperation 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 reactor.core.scheduler.Schedulers +import space.luminic.budgerapp.mappers.BudgetMapper +import space.luminic.budgerapp.mappers.TransactionsMapper import space.luminic.budgerapp.models.* import space.luminic.budgerapp.repos.BudgetRepo import space.luminic.budgerapp.repos.CategoryRepo @@ -37,14 +40,16 @@ class FinancialService( val userService: UserService, val reactiveMongoTemplate: ReactiveMongoTemplate, private val spaceService: SpaceService, - private val categoryRepo: CategoryRepo + private val categoryRepo: CategoryRepo, + val transactionsMapper: TransactionsMapper, + val budgetMapper: BudgetMapper ) { private val logger = LoggerFactory.getLogger(FinancialService::class.java) fun updateBudgetOnCreate(transaction: Transaction): Mono { - return budgetRepo.findByDateFromLessThanEqualAndDateToGreaterThanEqual( - transaction.date, transaction.date + return findProjectedBudget( + transaction.space!!.id!!, budgetId = null, transaction.date, transaction.date ).flatMap { budget -> val budgetCategory = budget.categories.firstOrNull { it.category.id == transaction.category.id } @@ -53,43 +58,39 @@ class FinancialService( } 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 -> + return@flatMap getBudgetSumsByCategory(transaction.category.id!!, budget).flatMap { sums -> - budgetCategory.currentPlanned = sums.getDouble("plannedAmount") ?: 0.0 - budgetCategory.currentSpent = sums.getDouble("instantAmount") ?: 0.0 - // При совпадении бюджетов разница просто корректирует лимит - if (transaction.type.code == "PLANNED") { - budgetCategory.currentLimit += transaction.amount - } - logger.info("updateBudgetOnCreate end") - budgetRepo.save(budget).then() + budgetCategory.currentPlanned = sums.getDouble("plannedAmount") ?: 0.0 + budgetCategory.currentSpent = sums.getDouble("instantAmount") ?: 0.0 + // При совпадении бюджетов разница просто корректирует лимит + if (transaction.type.code == "PLANNED") { + budgetCategory.currentLimit += transaction.amount } + logger.info("updateBudgetOnCreate end") + budgetRepo.save(budget).then() + } // }.then() // Возвращаем корректный Mono } fun updateBudgetOnEdit( - oldTransaction: Transaction, - newTransaction: Transaction, - difference: Double + 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"))) + findProjectedBudget( + newTransaction.space!!.id!!, budgetId = null, oldTransaction.date, oldTransaction.date + ).map { + logger.info("got old budget") + it + }.switchIfEmpty(Mono.error(BudgetNotFoundException("Old budget cannot be null"))), findProjectedBudget( + newTransaction.space!!.id!!, budgetId = null, 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 @@ -103,17 +104,16 @@ class FinancialService( 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 - // При совпадении бюджетов разница просто корректирует лимит - if (newTransaction.type.code == "PLANNED") { - budgetCategory.currentLimit += difference - } - logger.info("updateBudgetOnEdit end") - budgetRepo.save(newBudget).then() + return@flatMap getBudgetSumsByCategory(newTransaction.category.id!!, newBudget).flatMap { sums -> + budgetCategory.currentPlanned = sums.getDouble("plannedAmount") ?: 0.0 + budgetCategory.currentSpent = sums.getDouble("instantAmount") ?: 0.0 + // При совпадении бюджетов разница просто корректирует лимит + if (newTransaction.type.code == "PLANNED") { + budgetCategory.currentLimit += difference } + logger.info("updateBudgetOnEdit end") + budgetRepo.save(newBudget).then() + } } else { // Если бюджеты различаются — отдельно обновляем категории в старом и новом бюджетах. val oldBudgetCategory = @@ -124,31 +124,29 @@ class FinancialService( 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 - // В старом бюджете вычитаем разницу, так как транзакция перемещается - if (oldTransaction.type.code == "PLANNED") { - oldBudgetCategory.currentLimit -= difference - } - budgetRepo.save(oldBudget).then() + getBudgetSumsByCategory(oldTransaction.category.id!!, oldBudget).flatMap { sums -> + oldBudgetCategory.currentPlanned = sums.getDouble("plannedAmount") ?: 0.0 + oldBudgetCategory.currentSpent = sums.getDouble("instantAmount") ?: 0.0 + // В старом бюджете вычитаем разницу, так как транзакция перемещается + if (oldTransaction.type.code == "PLANNED") { + 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 - // В новом бюджете прибавляем разницу - if (newTransaction.type.code == "PLANNED") { - newBudgetCategory.currentLimit += difference - } - budgetRepo.save(newBudget).then() + getBudgetSumsByCategory(newTransaction.category.id!!, newBudget).flatMap { sums -> + newBudgetCategory.currentPlanned = sums.getDouble("plannedAmount") ?: 0.0 + newBudgetCategory.currentSpent = sums.getDouble("instantAmount") ?: 0.0 + // В новом бюджете прибавляем разницу + if (newTransaction.type.code == "PLANNED") { + newBudgetCategory.currentLimit += difference } + budgetRepo.save(newBudget).then() + } } logger.info("updateBudgetOnEdit end") return@flatMap Mono.`when`(oldUpdate, newUpdate).then() @@ -157,33 +155,31 @@ class FinancialService( } fun updateBudgetOnDelete(transaction: Transaction): Mono { - return budgetRepo.findByDateFromLessThanEqualAndDateToGreaterThanEqual( - transaction.date, transaction.date + return findProjectedBudget( + transaction.space!!.id!!, budgetId = null, transaction.date, transaction.date ).flatMap { budget -> - getBudgetCategories(budget.dateFrom, budget.dateTo).flatMap { categories -> + getBudgetCategories(transaction.space?.id!!, 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 - } + "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() + } + 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 - } + "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() + } + category + }.collectList() else -> Mono.just(budget.categories) } @@ -199,156 +195,196 @@ class FinancialService( fun getBudgets(spaceId: String, sortSetting: SortSetting? = null): Mono> { val sort = sortSetting?.let { Sort.by(it.order, it.by) - } ?: Sort.by(Sort.Direction.DESC, "dateFrom") + } ?: Sort.by(Direction.DESC, "dateFrom") - return ReactiveSecurityContextHolder.getContext() - .map { it.authentication } - .flatMap { authentication -> - val username = authentication.name - spaceService.getSpace(spaceId) - .switchIfEmpty(Mono.error(IllegalArgumentException("Space not found for $spaceId"))) - .flatMap { space -> - userService.getByUsername(username) - .switchIfEmpty(Mono.error(IllegalArgumentException("User not found for username: $username"))) - .flatMap { user -> - val userIds = space.users.mapNotNull { it.id?.toString() } - if (user.id !in userIds) { - Mono.error(IllegalArgumentException("User cannot access this Space")) - } else { - val spaceObjectId = try { - ObjectId(space.id!!) // Преобразуем строку в ObjectId - } catch (e: IllegalArgumentException) { - return@flatMap Mono.error(IllegalArgumentException("Invalid Space ID format: ${space.id}")) - } - - println("Space ID type: ${spaceObjectId::class.java}, value: $spaceObjectId") - // Применяем сортировку к запросу - findProjectedBudgets(spaceObjectId).collectList() + return ReactiveSecurityContextHolder.getContext().map { it.authentication }.flatMap { authentication -> + val username = authentication.name + spaceService.getSpace(spaceId) + .switchIfEmpty(Mono.error(IllegalArgumentException("Space not found for $spaceId"))) + .flatMap { space -> + userService.getByUsername(username) + .switchIfEmpty(Mono.error(IllegalArgumentException("User not found for username: $username"))) + .flatMap { user -> + val userIds = space.users.mapNotNull { it.id } + if (user.id !in userIds) { + Mono.error(IllegalArgumentException("User cannot access this Space")) + } else { + val spaceObjectId = try { + ObjectId(space.id!!) // Преобразуем строку в ObjectId + } catch (e: IllegalArgumentException) { + return@flatMap Mono.error(IllegalArgumentException("Invalid Space ID format: ${space.id}")) } - + println("Space ID type: ${spaceObjectId::class.java}, value: $spaceObjectId") + // Применяем сортировку к запросу + findProjectedBudgets(spaceObjectId, sort) } - } - } - } - fun findProjectedBudgets(spaceId: ObjectId): Flux { - val lookupSpaces = lookup("spaces", "space.\$id", "_id", "spaceDetails") - val matchStage = Aggregation.match(Criteria.where("spaceDetails._id").`is`(spaceId)) + + } + } + } + } + + fun findProjectedBudgets(spaceId: ObjectId, sortRequested: Sort? = null): Mono> { + val lookupCategories = lookup("categories", "categories.category.\$id", "_id", "categoriesDetails") + + + val lookupIncomeCategories = + lookup("categories", "incomeCategories.category.\$id", "_id", "incomeCategoriesDetails") + + val lookupSpace = lookup("spaces", "space.\$id", "_id", "spaceDetails") + val unwindSpace = unwind("spaceDetails") + val matchStage = match(Criteria.where("spaceDetails._id").`is`(spaceId)) // matchCriteria.add(Criteria.where("spaceDetails._id").`is`(ObjectId(spaceId))) - val projectStage = Aggregation.project("_id", "name", "dateFrom", "dateTo") // Оставляем только нужные поля - val sort = sort(Sort.by(Sort.Order.desc("_id"))) - val aggregation = Aggregation.newAggregation(lookupSpaces, matchStage, projectStage, sort) + val projectStage = project("_id", "name", "dateFrom", "dateTo") // Оставляем только нужные поля + val sort = sortRequested?.let { sort(it) } ?: sort( + Sort.by(Direction.DESC, "date").and(Sort.by(Direction.DESC, "createdAt")) + ) + val aggregation = + newAggregation(lookupCategories, lookupIncomeCategories, lookupSpace, unwindSpace, matchStage, sort) - return reactiveMongoTemplate.aggregate(aggregation, "budgets", Budget::class.java) + return reactiveMongoTemplate.aggregate(aggregation, "budgets", Document::class.java).collectList().map { docs -> + docs.map { doc -> + budgetMapper.fromDocument(doc) + }.toMutableList() + } } + fun findProjectedBudget( + spaceId: String, budgetId: String? = null, dateFrom: LocalDate? = null, dateTo: LocalDate? = null + ): Mono { + val lookupCategories = lookup("categories", "categories.category.\$id", "_id", "categoriesDetails") + val unwindCategories = unwind("categoriesDetails") + + val lookupIncomeCategories = + lookup("categories", "incomeCategories.category.\$id", "_id", "incomeCategoriesDetails") + val unwindIncomeCategories = unwind("incomeCategoriesDetails") + val lookupSpace = lookup("spaces", "space.\$id", "_id", "spaceDetails") + val unwindSpace = unwind("spaceDetails") + val matchCriteria = mutableListOf() + budgetId?.let { matchCriteria.add(Criteria.where("_id").`is`(ObjectId(it))) } + dateFrom?.let { matchCriteria.add(Criteria.where("dateFrom").lte(dateTo!!)) } + dateTo?.let { matchCriteria.add(Criteria.where("dateTo").gte(it)) } + matchCriteria.add(Criteria.where("spaceDetails._id").`is`(ObjectId(spaceId))) + val matchStage = match(Criteria().andOperator(*matchCriteria.toTypedArray())) + val aggregation = newAggregation(lookupCategories, lookupIncomeCategories, lookupSpace, unwindSpace, matchStage) + + return reactiveMongoTemplate.aggregate(aggregation, "budgets", Document::class.java).next().map { doc -> + budgetMapper.fromDocument(doc) + } + } + + // @Cacheable("budgets", key = "#id") - fun getBudget(id: String): Mono { - return ReactiveSecurityContextHolder.getContext() - .flatMap { securityContext -> - val username = securityContext.authentication.name - budgetRepo.findById(id) - .flatMap { budget -> - // Проверяем, что пользователь есть в space бюджета - if (!budget.space!!.users.any { it.username == username }) { - return@flatMap Mono.error(IllegalArgumentException("User does not have access to this space")) - } - - // Если доступ есть, продолжаем процесс - val budgetDTO = BudgetDTO( - budget.id, - budget.space, - 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 regenBudgets(): Mono { - return budgetRepo.findAll() + fun getBudget(spaceId: String, id: String): Mono { + return findProjectedBudget( + spaceId, + id + ).switchIfEmpty(Mono.error(IllegalArgumentException("Budget not found for spaceId: $spaceId and budgetId: $id"))) .flatMap { budget -> - spaceService.getSpace("67af3c0f652da946a7dd9931") - .map { space -> - budget.space = space - budget + + // Если доступ есть, продолжаем процесс + val budgetDTO = BudgetDTO( + budget.id, + budget.space, + budget.name, + budget.dateFrom, + budget.dateTo, + budget.createdAt, + categories = budget.categories, + incomeCategories = budget.incomeCategories, + ) + + logger.info("Fetching categories and transactions") + + val categoriesMono = getBudgetCategories(spaceId, budgetDTO.dateFrom, budgetDTO.dateTo) + val transactionsMono = getTransactionsByTypes(spaceId, 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 } - .flatMap { updatedBudget -> budgetRepo.save(updatedBudget) } - } - .then() + } + }.doOnError { error -> + logger.error("Error fetching budget: ${error.message}", error) + }.switchIfEmpty(Mono.error(BudgetNotFoundException("Budget not found with id: $id"))) } - fun regenTransactions(): Mono { - return transactionsRepo.findAll().flatMap { transaction -> - spaceService.getSpace("67af3c0f652da946a7dd9931") - .map { space -> - transaction.space = space - transaction - } - .flatMap { updatedTransaction -> transactionsRepo.save(updatedTransaction) } - } - .then() - } + +// fun regenBudgets(): Mono { +// return budgetRepo.findAll() +// .flatMap { budget -> +// spaceService.getSpace("67af3c0f652da946a7dd9931") +// .map { space -> +// budget.space = space +// budget +// } +// .flatMap { updatedBudget -> budgetRepo.save(updatedBudget) } +// } +// .then() +// } + +// fun regenTransactions(): Mono { +// return transactionsRepo.findAll().flatMap { transaction -> +// spaceService.getSpace("67af3c0f652da946a7dd9931") +// .map { space -> +// transaction.space = space +// transaction +// } +// .flatMap { updatedTransaction -> transactionsRepo.save(updatedTransaction) } +// } +// .then() +// } fun regenCats(): Mono { - return categoryRepo.findBySpaceId(ObjectId("67b352b13384483a1c2282ed")) - .flatMap { cat -> + return categoryRepo.findBySpaceId(ObjectId("67b352b13384483a1c2282ed")).flatMap { cat -> // if (cat.space?.id == "67b352b13384483a1c2282ed") { - categoryRepo.deleteById(cat.id!!) // Возвращаем `Mono` + categoryRepo.deleteById(cat.id!!) // Возвращаем `Mono` // } else { // Mono.empty() // Если не удаляем, возвращаем пустой `Mono` // } - } - .then() // Убедимся, что все операции завершены + }.then() // Убедимся, что все операции завершены } - @CacheEvict(cacheNames = ["budgets", "budgetsList"], allEntries = true) + fun createCategory(space: Space, category: Category): Mono { + category.space = space + return categoryRepo.save(category) + .flatMap { savedCategory -> + findProjectedBudgets(ObjectId(space.id)) + .flatMapMany { Flux.fromIterable(it) } // Преобразуем List в Flux + .flatMap { budget -> + when (savedCategory.type.code) { + "INCOME" -> budget.incomeCategories.add(BudgetCategory(0.0, 0.0, 0.0, savedCategory)) + "EXPENSE" -> budget.categories.add(BudgetCategory(0.0, 0.0, 0.0, savedCategory)) + } + budgetRepo.save(budget) // Сохраняем каждый обновленный бюджет + } + .then(Mono.just(savedCategory)) // Возвращаем сохраненную категорию после обработки всех бюджетов + } + } + + fun createBudget(spaceId: String, budget: Budget, createRecurrent: Boolean): Mono { - return Mono.zip( - getBudgetByDate(budget.dateFrom, spaceId).map { Optional.ofNullable(it) } - .switchIfEmpty(Mono.just(Optional.empty())), + return Mono.zip(getBudgetByDate(budget.dateFrom, spaceId).map { Optional.ofNullable(it) } + .switchIfEmpty(Mono.just(Optional.empty())), getBudgetByDate(budget.dateTo, spaceId).map { Optional.ofNullable(it) } - .switchIfEmpty(Mono.just(Optional.empty())) - ).flatMap { tuple -> + .switchIfEmpty(Mono.just(Optional.empty()))).flatMap { tuple -> val startBudget = tuple.t1.orElse(null) val endBudget = tuple.t2.orElse(null) @@ -358,10 +394,8 @@ class FinancialService( } // Получаем Space по spaceId - return@flatMap spaceService.getSpace(spaceId) - .switchIfEmpty(Mono.error(IllegalArgumentException("Space not found for $spaceId"))) - - .flatMap { space -> + spaceService.getSpace(spaceId) + .switchIfEmpty(Mono.error(IllegalArgumentException("Space not found for $spaceId"))).flatMap { space -> // Проверяем, входит ли пользователь в этот Space ReactiveSecurityContextHolder.getContext().flatMap { securityContext -> val username = securityContext.authentication.name @@ -384,25 +418,37 @@ class FinancialService( // Создаем бюджет после возможного создания рекуррентных транзакций 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() - } - ) + getCategoryTransactionPipeline( + spaceId, + budget.dateFrom, + budget.dateTo + ).flatMap { categories -> + budget.categories = categories + budgetRepo.save(budget) + }.publishOn(Schedulers.boundedElastic()).doOnNext { savedBudget -> + // Выполнение updateBudgetWarns в фоне + updateBudgetWarns(budget = savedBudget).doOnError { error -> + // Логируем ошибку, если произошла + logger.error("Error during updateBudgetWarns: ${error.message}") + }.subscribe() + }).then( + getCategoryTransactionPipeline( + spaceId, + budget.dateFrom, + budget.dateTo, + "INCOME" + ).flatMap { categories -> + budget.incomeCategories = categories + budgetRepo.save(budget) + }.publishOn(Schedulers.boundedElastic()).doOnNext { savedBudget -> + // Выполнение updateBudgetWarns в фоне + updateBudgetWarns(budget = savedBudget).doOnError { error -> + // Логируем ошибку, если произошла + logger.error("Error during updateBudgetWarns: ${error.message}") + }.subscribe() + }) } } - } } } @@ -417,8 +463,9 @@ class FinancialService( 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 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") @@ -431,45 +478,44 @@ class FinancialService( } } - fun getBudgetTransactionsByType(budgetId: String): Mono>> { - return budgetRepo.findById(budgetId).flatMap { it -> - getTransactionsByTypes(it.dateFrom, it.dateTo) - } - } +// 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"))) + fun deleteBudget(spaceId: String, budgetId: String): Mono { + return findProjectedBudget( + spaceId, + budgetId + ).switchIfEmpty(Mono.error(IllegalArgumentException("Budget not found for spaceId: $spaceId and budgetId: $budgetId"))) .flatMap { budget -> - getTransactionsToDelete(budget.dateFrom, budget.dateTo) - .flatMapMany { transactions -> - Flux.fromIterable(transactions) - .flatMap { transaction -> - deleteTransaction(transaction.id!!) - } + getTransactionsToDelete(spaceId, budget.dateFrom, budget.dateTo).flatMapMany { transactions -> + Flux.fromIterable(transactions).flatMap { transaction -> + deleteTransaction(transaction) } - .then( - budgetRepo.delete(budget) - ) + }.then( + budgetRepo.deleteById(budget.id!!) + ) } } - @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 -> + fun setCategoryLimit(spaceId: String, budgetId: String, catId: String, limit: Double): Mono { + return findProjectedBudget(spaceId, budgetId = budgetId).flatMap { budget -> + val catEdit = + budget.categories.firstOrNull { it.category.id == catId } ?: return@flatMap Mono.error( + Exception("Category not found in the budget") + ) + + calcTransactionsSum(spaceId, 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) + updateBudgetWarns(it).thenReturn(catEdit) } } } @@ -492,8 +538,7 @@ class FinancialService( fun updateBudgetWarns(budget: Budget? = null): Mono> { logger.info("STARTED WARNS UPDATE") - val finalBudgetMono = budget?.let { Mono.just(it) } - ?: return Mono.just(emptyList()) + val finalBudgetMono = budget?.let { Mono.just(it) } ?: return Mono.just(emptyList()) return finalBudgetMono.flatMap { finalBudget -> if (finalBudget.categories.isEmpty()) { @@ -504,21 +549,17 @@ class FinancialService( val averageSumsMono = getAverageSpendingByCategory() val averageIncomeMono = getAverageIncome() val currentBudgetIncomeMono = calcTransactionsSum( - finalBudget, transactionType = "PLANNED", categoryType = "INCOME" + budget.space!!.id!!, finalBudget, transactionType = "PLANNED", categoryType = "INCOME" ) val plannedIncomeMono = calcTransactionsSum( - finalBudget, categoryType = "INCOME", transactionType = "PLANNED" + budget.space!!.id!!, finalBudget, categoryType = "INCOME", transactionType = "PLANNED" ) val plannedSavingMono = calcTransactionsSum( - finalBudget, categoryId = "675850148198643f121e466a", transactionType = "PLANNED" + budget.space!!.id!!, finalBudget, categoryId = "675850148198643f121e466a", transactionType = "PLANNED" ) Mono.zip( - averageSumsMono, - averageIncomeMono, - currentBudgetIncomeMono, - plannedIncomeMono, - plannedSavingMono + averageSumsMono, averageIncomeMono, currentBudgetIncomeMono, plannedIncomeMono, plannedSavingMono ).flatMap { tuple -> val averageSums = tuple.t1 val averageIncome = tuple.t2 @@ -526,23 +567,19 @@ class FinancialService( 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") } + 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 -> @@ -571,24 +608,17 @@ class FinancialService( 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 - ) + 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()) } @@ -597,35 +627,29 @@ class FinancialService( 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 - ) + 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()) } + warnRepo.findWarnByContext(lowSavingContext).flatMap { warnRepo.delete(it).then(Mono.empty()) } } } return Flux.fromIterable(warnsForCategory).flatMap { it } } - @Cacheable("transactions") + fun getTransactions( spaceId: String, dateFrom: LocalDate? = null, @@ -641,204 +665,257 @@ class FinancialService( limit: Int? = null, offset: Int? = null, ): Mono> { - return ReactiveSecurityContextHolder.getContext() - .map { it.authentication } - .flatMap { authentication -> - val username = authentication.name - spaceService.getSpace(spaceId) - .switchIfEmpty(Mono.error(IllegalArgumentException("Space not found for $spaceId"))) - .flatMap { space -> - userService.getByUsername(username) - .switchIfEmpty(Mono.error(IllegalArgumentException("User not found for username: $username"))) - .flatMap { user -> - if (space.users.none { it.id.toString() == user.id }) { - return@flatMap Mono.error>(IllegalArgumentException("User does not have access to this Space")) - } - - val matchCriteria = mutableListOf() - - // Добавляем фильтры - matchCriteria.add(Criteria.where("spaceDetails._id").`is`(ObjectId(spaceId))) - 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 lookupSpaces = lookup("spaces", "space.\$id", "_id", "spaceDetails") - 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, - lookupSpaces, - lookupUsers, - match.takeIf { matchCriteria.isNotEmpty() }, - sort, - offset?.let { skip(it.toLong()) }, - limit?.let { limit(it.toLong()) } - ).filterNotNull() - - val aggregation = newAggregation(aggregationBuilder) - - return@flatMap reactiveMongoTemplate.aggregate( - aggregation, "transactions", Transaction::class.java - ) - .collectList() - .map { it.toMutableList() } + return ReactiveSecurityContextHolder.getContext().map { it.authentication }.flatMap { authentication -> + val username = authentication.name + spaceService.getSpace(spaceId) + .switchIfEmpty(Mono.error(IllegalArgumentException("Space not found for $spaceId"))) + .flatMap { space -> + userService.getByUsername(username) + .switchIfEmpty(Mono.error(IllegalArgumentException("User not found for username: $username"))) + .flatMap { user -> + if (space.users.none { it.id.toString() == user.id }) { + return@flatMap Mono.error>(IllegalArgumentException("User does not have access to this Space")) } - } - } + + val matchCriteria = mutableListOf() + + // Добавляем фильтры + matchCriteria.add(Criteria.where("spaceDetails._id").`is`(ObjectId(spaceId))) + 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 unwindCategory = unwind("categoryDetails") + val lookupSpaces = lookup("spaces", "space.\$id", "_id", "spaceDetails") + val unwindSpace = unwind("spaceDetails") + val lookupUsers = lookup("users", "user.\$id", "_id", "userDetails") + val unwindUser = unwind("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, + unwindCategory, + lookupSpaces, + unwindSpace, + lookupUsers, + unwindUser, + match.takeIf { matchCriteria.isNotEmpty() }, + sort, + offset?.let { skip(it.toLong()) }, + limit?.let { limit(it.toLong()) }).filterNotNull() + + val aggregation = newAggregation(aggregationBuilder) + + return@flatMap reactiveMongoTemplate.aggregate( + aggregation, "transactions", Document::class.java + ).collectList().map { docs -> + + val test = docs.map { doc -> + transactionsMapper.fromDocument(doc) + }.toMutableList() + + test + } + } + } + } } - 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) - ) + fun getTransactionByParentId( + parentId: String + ): Mono { + + + // Сборка агрегации + val lookup = lookup("categories", "category.\$id", "_id", "categoryDetails") + val unwindCategory = unwind("categoryDetails") + val lookupSpaces = lookup("spaces", "space.\$id", "_id", "spaceDetails") + val unwindSpace = unwind("spaceDetails") + val lookupUsers = lookup("users", "user.\$id", "_id", "userDetails") + val unwindUser = unwind("userDetails") + + val match = match(Criteria.where("parentId").`is`(parentId)) + + val aggregationBuilder = mutableListOf( + lookup, + unwindCategory, + lookupSpaces, + unwindSpace, + lookupUsers, + unwindUser, + match ) -// Пример использования в MongoTemplate: - val query = Query(criteria) + val aggregation = newAggregation(aggregationBuilder) -// Если вы хотите использовать ReactiveMongoTemplate: - return reactiveMongoTemplate.find(query, Transaction::class.java) - .collectList() - .doOnNext { transactions -> println("Found transactions: $transactions") } + return reactiveMongoTemplate.aggregate( + aggregation, "transactions", Document::class.java + ).map { doc -> + + transactionsMapper.fromDocument(doc) + + }.next() } - @Cacheable("transactions") - fun getTransactionById(id: String): Mono { - return transactionsRepo.findById(id) - .map { - it - } - .switchIfEmpty( - Mono.error(IllegalArgumentException("Transaction with id: $id not found")) + fun getTransactionsToDelete(spaceId: String, dateFrom: LocalDate, dateTo: LocalDate): Mono> { + val matchCriteria = mutableListOf() + val lookupSpaces = lookup("spaces", "space.\$id", "_id", "spaceDetails") + val unwindSpace = unwind("spaceDetails") + val lookupCategory = lookup("categories", "category.\$id", "_id", "categoryDetails") + val unwindCategory = unwind("categoryDetails") + matchCriteria.add(Criteria.where("spaceDetails._id").`is`(ObjectId(spaceId))) + matchCriteria.add(Criteria.where("date").gte(dateFrom)) + matchCriteria.add(Criteria.where("date").lt(dateTo)) + matchCriteria.add( + Criteria().orOperator( + Criteria.where("type.code").`is`("PLANNED"), Criteria.where("parentId").exists(true) ) + ) + val match = match(Criteria().andOperator(*matchCriteria.toTypedArray())) + val project = project("_id", "type", "comment", "date", "amount", "isDone", "categoryDetails") + val aggregationBuilder = mutableListOf( + lookupSpaces, unwindSpace, lookupCategory, unwindCategory, match, project + ) + val aggregation = newAggregation(aggregationBuilder) + + return reactiveMongoTemplate.aggregate(aggregation, "transactions", Document::class.java) + .collectList() // Собирать все результаты в список + .map { docs -> + docs.map { doc -> + transactionsMapper.fromDocument(doc) + } + } } - @CacheEvict(cacheNames = ["transactions"], allEntries = true) - fun createTransaction(spaceId: String, transaction: Transaction): Mono { - return ReactiveSecurityContextHolder.getContext() - .map { it.authentication } - .flatMap { authentication -> - val username = authentication.name - spaceService.getSpace(spaceId) - .switchIfEmpty(Mono.error(IllegalArgumentException("Space not found for $spaceId"))) - .flatMap { space -> - userService.getByUsername(username) - .switchIfEmpty(Mono.error(IllegalArgumentException("User not found for username: $username"))) - .flatMap { user -> - if (space.users.none { it.id.toString() == user.id }) { - return@flatMap Mono.error(IllegalArgumentException("User does not have access to this Space")) - } - // Привязываем space и user к транзакции - transaction.user = user - transaction.space = space + fun getTransactionById(id: String): Mono { + val lookupSpaces = lookup("spaces", "space.\$id", "_id", "spaceDetails") + val unwindSpaces = unwind("spaceDetails") + val lookupCategory = lookup("categories", "category.\$id", "_id", "categoryDetails") + val unwindCategory = unwind("categoryDetails") + val lookupUser = lookup("users", "user.\$id", "_id", "userDetails") + val unwindUser = unwind("userDetails") + val matchCriteria = mutableListOf() + matchCriteria.add(Criteria.where("_id").`is`(ObjectId(id))) + val match = match(Criteria().andOperator(*matchCriteria.toTypedArray())) +// val project = project("_iid", "type", "comment", "date", "amount", "isDone") + val aggregationBuilder = mutableListOf( + lookupSpaces, unwindSpaces, lookupCategory, unwindCategory, lookupUser, unwindUser, match + ) + val aggregation = newAggregation(aggregationBuilder) - transactionsRepo.save(transaction) - .flatMap { savedTransaction -> - updateBudgetOnCreate(savedTransaction) - .thenReturn(savedTransaction) // Ждём выполнения updateBudgetOnCreate перед возвратом - } + return reactiveMongoTemplate.aggregate(aggregation, "transactions", Document::class.java).next().map { doc -> + transactionsMapper.fromDocument(doc) + } + + } + + + fun createTransaction(spaceId: String, transaction: Transaction): Mono { + return ReactiveSecurityContextHolder.getContext().map { it.authentication }.flatMap { authentication -> + val username = authentication.name + spaceService.getSpace(spaceId) + .switchIfEmpty(Mono.error(IllegalArgumentException("Space not found for $spaceId"))) + .flatMap { space -> + userService.getByUsername(username) + .switchIfEmpty(Mono.error(IllegalArgumentException("User not found for username: $username"))) + .flatMap { user -> + if (space.users.none { it.id.toString() == user.id }) { + return@flatMap Mono.error(IllegalArgumentException("User does not have access to this Space")) } - } - } + // Привязываем space и user к транзакции + transaction.user = user + transaction.space = space + + 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 и возвращаем транзакцию - } + return getTransactionById(transaction.id!!).flatMap { oldStateOfTransaction -> + val changed = compareSumDateDoneIsChanged(oldStateOfTransaction, transaction) + if (!changed) { + return@flatMap transactionsRepo.save(transaction) // Сохраняем, если изменений нет } - .switchIfEmpty( - Mono.error(IllegalArgumentException("Transaction not found with id: ${transaction.id}")) - ) + + 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 + 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() - } - } + return getTransactionByParentId( + newTransaction.id!! + ).flatMap { childTransaction -> + logger.info("Updating child: $childTransaction") + val updatedChild = childTransaction.copy( + amount = newTransaction.amount, + category = newTransaction.category, + comment = newTransaction.comment, + user = newTransaction.user ) - .then( - Mono.defer { - // Удаление дочерней транзакции, если родительская помечена как не выполненная - if (oldTransaction.isDone && !newTransaction.isDone) { - transactionsRepo.findByParentId(newTransaction.id!!) - .flatMap { child -> - deleteTransaction(child.id!!) - }.then() - } else { - Mono.empty() - } + transactionsRepo.save(updatedChild).then() + }.switchIfEmpty( + Mono.defer { + if (!oldTransaction.isDone && newTransaction.isDone) { + val newChildTransaction = newTransaction.copy( + id = null, type = TransactionType("INSTANT", "Текущие"), parentId = newTransaction.id + ) + return@defer transactionsRepo.save(newChildTransaction).flatMap { updateBudgetOnCreate(it) } + .then() + } else { + return@defer Mono.empty() // Используем Mono.empty() для пустой операции } - ) + }).then( + Mono.defer { + if (oldTransaction.isDone && !newTransaction.isDone) { + getTransactionByParentId(newTransaction.id!!) + .flatMap { child -> deleteTransaction(child) }.then() + } else { + return@defer Mono.empty() // Вернем пустую операцию, если условия не выполняются + } + }) } @@ -856,14 +933,11 @@ class FinancialService( } } - @CacheEvict(cacheNames = ["transactions"], allEntries = true) - fun deleteTransaction(transactionId: String): Mono { - return transactionsRepo.findById(transactionId) - .flatMap { transactionToDelete -> - transactionsRepo.deleteById(transactionId) // Удаляем транзакцию - .then(updateBudgetOnDelete(transactionToDelete)) - } + fun deleteTransaction(transaction: Transaction): Mono { + return transactionsRepo.deleteById(transaction.id!!) // Удаляем транзакцию + .then(Mono.defer { updateBudgetOnDelete(transaction) }) + .then() // Завершаем Mono, так как нам не нужно возвращать результат } @@ -911,6 +985,7 @@ class FinancialService( // fun calcTransactionsSum( + spaceId: String, budget: Budget, categoryId: String? = null, categoryType: String? = null, @@ -920,6 +995,7 @@ class FinancialService( val matchCriteria = mutableListOf() // Добавляем фильтры + matchCriteria.add(Criteria.where("spaceDetails._id").`is`(ObjectId(spaceId))) 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))) } @@ -930,22 +1006,23 @@ class FinancialService( // Сборка агрегации val lookup = lookup("categories", "category.\$id", "_id", "categoryDetails") val unwind = unwind("categoryDetails") + val lookupSpace = lookup("space", "space.\$id", "_id", "spaceDetails") + val unwindSpace = unwind("spaceDetails") + 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) + val aggregation = newAggregation(lookup, unwind, lookupSpace, unwindSpace, 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 - } + 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 } // Суммируем значения, если несколько результатов + }.reduce(0.0) { acc, sum -> acc + sum } // Суммируем значения, если несколько результатов } @@ -956,34 +1033,29 @@ class FinancialService( 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) + 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 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 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() + 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()) // Возвращаем пустую карту, если результатов нет + }.defaultIfEmpty(emptyMap()) // Возвращаем пустую карту, если результатов нет } @@ -1001,14 +1073,12 @@ class FinancialService( val unwind = unwind("detailedCategory") val match = match( - Criteria.where("detailedCategory.type.code").`is`("INCOME") - .and("type.code").`is`("INSTANT") - .and("isDone").`is`(true) + 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 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") @@ -1020,97 +1090,72 @@ class FinancialService( .singleOrEmpty() // Ожидаем только один результат .map { result -> result["averageAmount"] as? Double ?: 0.0 - } - .defaultIfEmpty(0.0) // Если результат пустой, возвращаем 0.0 + }.defaultIfEmpty(0.0) // Если результат пустой, возвращаем 0.0 } - fun getTransactionsByTypes(dateFrom: LocalDate, dateTo: LocalDate): Mono>> { + fun getTransactionsByTypes( + spaceId: String, dateFrom: LocalDate, dateTo: LocalDate + ): Mono>> { val pipeline = listOf( Document( "\$lookup", - Document("from", "categories") - .append("localField", "category.\$id") - .append("foreignField", "_id") + Document("from", "categories").append("localField", "category.\$id").append("foreignField", "_id") .append("as", "categoryDetailed") - ), - Document( + ), Document( "\$lookup", - Document("from", "users") - .append("localField", "user.\$id") - .append("foreignField", "_id") + Document("from", "spaces").append("localField", "space.\$id").append("foreignField", "_id") + .append("as", "spaceDetailed") + ), 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( + ), Document( + "\$unwind", Document("path", "\$categoryDetailed").append("preserveNullAndEmptyArrays", true) + ), Document( + "\$unwind", Document("path", "\$spaceDetailed").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("spaceDetailed._id", ObjectId(spaceId)), 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( + "date", Document( + "\$lt", Date.from( + LocalDateTime.of(dateTo, LocalTime.MAX).atZone(ZoneId.systemDefault()) + .withZoneSameInstant(ZoneOffset.UTC).toInstant() ) ) ) ) ) - ), - Document( - "\$facet", - Document( - "plannedExpenses", - listOf( + ), 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", "EXPENSE") - ), + 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)) ) ) - .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)) - ) - ) ) ) @@ -1148,27 +1193,26 @@ class FinancialService( private fun extractTransactions(aggregationResult: Document, key: String): Mono> { val resultTransactions = aggregationResult[key] as? List ?: emptyList() - return Flux.fromIterable(resultTransactions) - .map { documentToTransactionMapper(it) } - .collectList() + return Flux.fromIterable(resultTransactions).map { documentToTransactionMapper(it) }.collectList() } private fun documentToTransactionMapper(document: Document): Transaction { val transactionType = document["type"] as Document - var user: User? = null + var user: User? 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, + tgId = userDocument["tgId"] as String?, tgUserName = userDocument["tgUserName"]?.let { it as String }, password = null, isActive = userDocument["isActive"] as Boolean, regDate = (userDocument["regDate"] as Date).toInstant().atZone(ZoneId.systemDefault()).toLocalDate(), - createdAt = (userDocument["createdAt"] as Date).toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime(), + createdAt = (userDocument["createdAt"] as Date).toInstant().atZone(ZoneId.systemDefault()) + .toLocalDateTime(), roles = userDocument["roles"] as ArrayList, ) @@ -1186,8 +1230,7 @@ class FinancialService( (document["_id"] as ObjectId).toString(), null, TransactionType( - transactionType["code"] as String, - transactionType["name"] as String + transactionType["code"] as String, transactionType["name"] as String ), user = user, category = category, @@ -1205,54 +1248,35 @@ class FinancialService( 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() - ) + "\$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( + "\$lt", Date.from( + LocalDateTime.of(budget.dateTo, LocalTime.MIN).atZone(ZoneId.systemDefault()) + .withZoneSameInstant(ZoneOffset.UTC).toInstant() ) ) - .append( - "instantAmount", - Document( - "\$sum", - Document( - "\$cond", - listOf(Document("\$eq", listOf("\$type.code", "INSTANT")), "\$amount", 0.0) - ) + ) + ), Document( + "\$group", Document("_id", BsonNull()).append( + "plannedAmount", Document( + "\$sum", Document( + "\$cond", listOf(Document("\$eq", listOf("\$type.code", "PLANNED")), "\$amount", 0.0) ) ) - ), - Document( - "\$project", - Document("_id", 0) - .append("plannedAmount", 1) - .append("instantAmount", 1) + ).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) ) ) @@ -1262,110 +1286,90 @@ class FinancialService( }.map { logger.info("getting budget sums for category $categoryId end") it - } - .next() // Берём первый документ, а не весь список + }.next() // Берём первый документ, а не весь список } - fun getBudgetCategories(dateFrom: LocalDate, dateTo: LocalDate): Mono>> { + fun getBudgetCategories( + spaceId: String, 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("from", "spaces").append("localField", "space.\$id").append("foreignField", "_id") + .append("as", "spaceDetailed") + ), Document( + "\$unwind", Document("path", "\$spaceDetailed").append("preserveNullAndEmptyArrays", true) + ), Document( + "\$match", Document( + Document("spaceDetailed._id", ObjectId(spaceId)) + ) + ), 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("transactionSums", 1L) + ), Document( + "\$project", Document("_id", 1L).append( + "plannedAmount", Document( + "\$arrayElemAt", listOf( Document( - "\$group", - Document("_id", "\$type.code") - .append( - "totalAmount", - Document("\$sum", "\$amount") - ) - ) + "\$filter", Document("input", "\$transactionSums").append("as", "sum").append( + "cond", Document("\$eq", listOf("\$\$sum._id", "PLANNED")) + ) + ), 0L ) ) - .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 ) ) - .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)) - ) + ), Document( + "\$addFields", Document( + "plannedAmount", Document("\$ifNull", listOf("\$plannedAmount.totalAmount", 0.0)) + ).append( + "instantAmount", Document("\$ifNull", listOf("\$instantAmount.totalAmount", 0.0)) + ) ) ) @@ -1379,9 +1383,7 @@ class FinancialService( // - return reactiveMongoTemplate.getCollection("categories") - .flatMapMany { it.aggregate(pipeline) } - .collectList() + return reactiveMongoTemplate.getCollection("categories").flatMapMany { it.aggregate(pipeline) }.collectList() .flatMap { result -> val categories = result.associate { document -> val id = document["_id"].toString() @@ -1398,120 +1400,90 @@ class FinancialService( fun getCategoryTransactionPipeline( - dateFrom: LocalDate, - dateTo: LocalDate, - catType: String? = "EXPENSE" + spaceId: String, 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("from", "spaces").append("localField", "space.\$id").append("foreignField", "_id") + .append("as", "spaceDetailed") + ), Document( + "\$unwind", Document("path", "\$spaceDetailed").append("preserveNullAndEmptyArrays", true) + ), Document( + "\$match", Document( + Document("spaceDetailed._id", ObjectId(spaceId)) + ) + ), 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( + "\$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") - ) + ) + ), Document( + "\$group", Document("_id", "\$type.code").append( + "totalAmount", Document("\$sum", "\$amount") ) ) ) - .append("as", "transactionSums") - ), - Document( + ).append("as", "transactionSums") + ), Document( "\$project", - Document("_id", 1L) - .append("type", 1L) - .append("name", 1L) - .append("description", 1L) - .append("icon", 1L) + Document("_id", 1L).append("type", 1L).append("name", 1L).append("description", 1L).append("icon", 1L) .append( - "plannedAmount", - Document( + "plannedAmount", Document( "\$arrayElemAt", listOf( Document( - "\$filter", - Document("input", "\$transactionSums") - .append("as", "sum") - .append( - "cond", - Document("\$eq", listOf("\$\$sum._id", "PLANNED")) - ) + "\$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 ) ) ) - .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)) + ), Document( + "\$addFields", Document( + "plannedAmount", Document("\$ifNull", listOf("\$plannedAmount.totalAmount", 0.0)) + ).append( + "instantAmount", Document("\$ifNull", listOf("\$instantAmount.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 -> + .flatMapMany { it.aggregate(pipeline, Document::class.java) }.map { document -> val catType = document["type"] as Document BudgetCategory( currentSpent = document["instantAmount"] as Double, @@ -1525,9 +1497,7 @@ class FinancialService( icon = document["icon"] as String ) ) - } - .collectList() - .map { it.toMutableList() } + }.collectList().map { it.toMutableList() } } @@ -1535,200 +1505,139 @@ class FinancialService( val pipeline = listOf( Document( "\$lookup", - Document("from", "categories") - .append("localField", "category.\$id") - .append("foreignField", "_id") + Document("from", "categories").append("localField", "category.\$id").append("foreignField", "_id") .append("as", "categoryDetails") - ), - Document("\$unwind", "\$categoryDetails"), - Document( - "\$match", - Document( - "date", - Document( + ), Document("\$unwind", "\$categoryDetails"), Document( + "\$match", Document( + "date", Document( "\$gte", Date.from( - LocalDateTime.of(dateFrom, LocalTime.MIN) - .atZone(ZoneId.systemDefault()) + 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() ) - .append( - "\$lte", LocalDateTime.of(dateTo, LocalTime.MIN) - .atZone(ZoneId.systemDefault()) - .withZoneSameInstant(ZoneOffset.UTC).toInstant() - ) ) - ), - Document( - "\$group", - Document( + ), 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( + 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")) + Document("_id", "\$_id.categoryId").append("categoryName", Document("\$first", "\$_id.categoryName")) .append( - "monthlyData", - Document( - "\$push", - Document( - "month", - Document( + "monthlyData", Document( + "\$push", Document( + "month", Document( "\$concat", listOf( - Document("\$toString", "\$_id.year"), "-", - Document( + Document("\$toString", "\$_id.year"), "-", Document( "\$cond", listOf( - Document("\$lt", listOf("\$_id.month", 10L)), - Document( + Document("\$lt", listOf("\$_id.month", 10L)), Document( "\$concat", listOf( - "0", - Document("\$toString", "\$_id.month") + "0", Document("\$toString", "\$_id.month") ) - ), - Document("\$toString", "\$_id.month") + ), Document("\$toString", "\$_id.month") ) ) ) ) - ) - .append("totalAmount", "\$totalAmount") + ).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)) - ) + ), 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", Date(1754006400000L)).append("unit", "month") + .append( + "amount", Document("\$multiply", listOf("\$\$offset", 1L)) ) - ) + ) ) ) - .append( - "totalAmount", - Document( - "\$let", - Document( - "vars", - Document( - "matched", + ).append( + "totalAmount", Document( + "\$let", Document( + "vars", Document( + "matched", Document( + "\$arrayElemAt", listOf( 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 - ) - ) - ) + "\$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", Date( + 1733011200000L + ) + ).append( + "unit", "month" + ).append( + "amount", + Document( + "\$multiply", + listOf( + "\$\$offset", + 1L ) ) + ) ) ) ) ) - ), 0L - ) - ) + ) + ) + ), 0L ) ) - .append( - "in", - Document("\$ifNull", listOf("\$\$matched.totalAmount", 0L)) - ) ) + ).append( + "in", Document("\$ifNull", listOf("\$\$matched.totalAmount", 0L)) ) + ) ) + ) ) ) - ), - Document( + ), Document( "\$project", - Document("_id", 0L) - .append("categoryId", "\$_id") - .append("categoryName", "\$categoryName") + 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() + return reactiveMongoTemplate.getCollection("transactions").flatMapMany { it.aggregate(pipeline) }.map { + it["categoryId"] = it["categoryId"].toString() + it + }.collectList() } fun getCategorySummaries(spaceId: String, dateFrom: LocalDate): Mono> { val sixMonthsAgo = Date.from( - LocalDateTime.of(dateFrom, LocalTime.MIN) - .atZone(ZoneId.systemDefault()) - .withZoneSameInstant(ZoneOffset.UTC).toInstant() + LocalDateTime.of(dateFrom, LocalTime.MIN).atZone(ZoneId.systemDefault()).withZoneSameInstant(ZoneOffset.UTC) + .toInstant() ) // Пример даты, можно заменить на вычисляемую val aggregation = listOf( // 1. Фильтр за последние 6 месяцев Document( - "\$lookup", Document("from", "spaces") - .append("localField", "space.\$id") - .append("foreignField", "_id") + "\$lookup", + Document("from", "spaces").append("localField", "space.\$id").append("foreignField", "_id") .append("as", "spaceInfo") ), @@ -1743,18 +1652,16 @@ class FinancialService( // 2. Группируем по категории + (год, месяц) Document( "\$group", Document( - "_id", Document("category", "\$category.\$id") - .append("year", Document("\$year", "\$date")) + "_id", + Document("category", "\$category.\$id").append("year", Document("\$year", "\$date")) .append("month", Document("\$month", "\$date")) - ) - .append("totalAmount", Document("\$sum", "\$amount")) + ).append("totalAmount", Document("\$sum", "\$amount")) ), // 3. Подтягиваем информацию о категории Document( - "\$lookup", Document("from", "categories") - .append("localField", "_id.category") - .append("foreignField", "_id") + "\$lookup", + Document("from", "categories").append("localField", "_id.category").append("foreignField", "_id") .append("as", "categoryInfo") ), @@ -1766,14 +1673,13 @@ class FinancialService( // 6. Группируем обратно по категории, собирая все (год, месяц, total) Document( - "\$group", Document("_id", "\$_id.category") - .append("categoryName", Document("\$first", "\$categoryInfo.name")) + "\$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( + .append("categoryIcon", Document("\$first", "\$categoryInfo.icon")).append( "monthlySums", Document( - "\$push", Document("year", "\$_id.year") - .append("month", "\$_id.month") + "\$push", + Document("year", "\$_id.year").append("month", "\$_id.month") .append("total", "\$totalAmount") ) ) @@ -1783,81 +1689,71 @@ class FinancialService( // - каждый элемент = {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") - ) - ) + "\$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( + ) + ).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( - "\$getField", Document("field", "total") - .append( - "input", Document( - "\$arrayElemAt", listOf( - Document( - "\$filter", + "\$filter", Document( + "input", "\$monthlySums" + ).append("as", "ms").append( + "cond", Document( + "\$and", listOf( + Document( + "\$eq", + listOf( + "$\$ms.year", Document( - "input", - "\$monthlySums" + "\$year", + "$\$subDate" ) - .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 + ) + ), Document( + "\$eq", + listOf( + "$\$ms.month", + Document( + "\$month", + "$\$subDate" + ) + ) ) ) ) + ) ), 0.0 ) ) ) + ), 0.0 ) + ) ) ) + ) ) ) + ) ), // 8. Сортируем результат по имени категории @@ -1866,8 +1762,7 @@ class FinancialService( // Выполняем агрегацию - return reactiveMongoTemplate.getCollection("transactions") - .flatMapMany { it.aggregate(aggregation) } + return reactiveMongoTemplate.getCollection("transactions").flatMapMany { it.aggregate(aggregation) } .map { document -> // Преобразуем _id в строку document["_id"] = document["_id"].toString() @@ -1913,8 +1808,7 @@ class FinancialService( // Обновляем документ с отсортированными и обновленными monthlySums document["monthlySums"] = sortedMonthlySums document - } - .collectList() + }.collectList() } } \ No newline at end of file