//package space.luminic.budgerapp.services // // //import org.bson.Document //import org.bson.types.ObjectId //import org.slf4j.LoggerFactory //import org.springframework.cache.annotation.CacheEvict //import org.springframework.cache.annotation.Cacheable //import org.springframework.context.ApplicationEventPublisher //import org.springframework.data.domain.Sort //import org.springframework.data.domain.Sort.Direction //import org.springframework.data.mongodb.core.ReactiveMongoTemplate //import org.springframework.data.mongodb.core.aggregation.Aggregation.group //import org.springframework.data.mongodb.core.aggregation.Aggregation.limit //import org.springframework.data.mongodb.core.aggregation.Aggregation.lookup //import org.springframework.data.mongodb.core.aggregation.Aggregation.match //import org.springframework.data.mongodb.core.aggregation.Aggregation.newAggregation //import org.springframework.data.mongodb.core.aggregation.Aggregation.project //import org.springframework.data.mongodb.core.aggregation.Aggregation.skip //import org.springframework.data.mongodb.core.aggregation.Aggregation.sort //import org.springframework.data.mongodb.core.aggregation.Aggregation.unwind //import org.springframework.data.mongodb.core.aggregation.DateOperators.DateToString //import org.springframework.data.mongodb.core.query.Criteria //import org.springframework.data.mongodb.core.query.Query //import org.springframework.security.core.context.ReactiveSecurityContextHolder //import org.springframework.stereotype.Service //import reactor.core.publisher.Flux //import reactor.core.publisher.Mono //import space.luminic.budgerapp.models.Budget //import space.luminic.budgerapp.models.Category //import space.luminic.budgerapp.models.CategoryType //import space.luminic.budgerapp.models.SortSetting //import space.luminic.budgerapp.models.Transaction //import space.luminic.budgerapp.models.TransactionEvent //import space.luminic.budgerapp.models.TransactionEventType //import space.luminic.budgerapp.models.TransactionType //import space.luminic.budgerapp.models.User //import space.luminic.budgerapp.repos.TransactionRepo //import java.time.LocalDate //import java.time.LocalDateTime //import java.time.LocalTime //import java.time.ZoneId //import java.time.ZoneOffset //import java.time.temporal.TemporalAdjusters //import java.util.ArrayList //import java.util.Date // //@Service //class TransactionService( // private val reactiveMongoTemplate: ReactiveMongoTemplate, // val transactionsRepo: TransactionRepo, // val userService: UserService, // private val eventPublisher: ApplicationEventPublisher //) { // // private val logger = LoggerFactory.getLogger(TransactionService::class.java) // // @Cacheable("transactions") // fun getTransactions( // dateFrom: LocalDate? = null, // dateTo: LocalDate? = null, // transactionType: String? = null, // isDone: Boolean? = null, // categoryId: String? = null, // categoryType: String? = null, // userId: String? = null, // parentId: String? = null, // isChild: Boolean? = null, // sortSetting: SortSetting? = null, // limit: Int? = null, // offset: Int? = null, // ): Mono> { // val matchCriteria = mutableListOf() // // // Добавляем фильтры // dateFrom?.let { matchCriteria.add(Criteria.where("date").gte(it)) } // dateTo?.let { matchCriteria.add(Criteria.where("date").lt(it)) } // transactionType?.let { matchCriteria.add(Criteria.where("type.code").`is`(it)) } // isDone?.let { matchCriteria.add(Criteria.where("isDone").`is`(it)) } // categoryId?.let { matchCriteria.add(Criteria.where("categoryDetails._id").`is`(it)) } // categoryType?.let { matchCriteria.add(Criteria.where("categoryDetails.type.code").`is`(it)) } // userId?.let { matchCriteria.add(Criteria.where("userDetails._id").`is`(ObjectId(it))) } // parentId?.let { matchCriteria.add(Criteria.where("parentId").`is`(it)) } // isChild?.let { matchCriteria.add(Criteria.where("parentId").exists(it)) } // // // Сборка агрегации // val lookup = lookup("categories", "category.\$id", "_id", "categoryDetails") // val lookupUsers = lookup("users", "user.\$id", "_id", "userDetails") // val match = match(Criteria().andOperator(*matchCriteria.toTypedArray())) // // var sort = sort(Sort.by(Direction.DESC, "date").and(Sort.by(Direction.DESC, "createdAt"))) // // sortSetting?.let { // sort = sort(Sort.by(it.order, it.by).and(Sort.by(Direction.ASC, "createdAt"))) // } // // val aggregationBuilder = mutableListOf( // lookup, // lookupUsers, // match.takeIf { matchCriteria.isNotEmpty() }, // sort, // offset?.let { skip(it.toLong()) }, // limit?.let { limit(it.toLong()) } // ).filterNotNull() // // val aggregation = newAggregation(aggregationBuilder) // // return reactiveMongoTemplate.aggregate( // aggregation, "transactions", Transaction::class.java // ) // .collectList() // Преобразуем Flux в Mono> // .map { it.toMutableList() } // } // // fun getTransactionsToDelete(dateFrom: LocalDate, dateTo: LocalDate): Mono> { // val criteria = Criteria().andOperator( // Criteria.where("date").gte(dateFrom), // Criteria.where("date").lte(dateTo), // Criteria().orOperator( // Criteria.where("type.code").`is`("PLANNED"), // Criteria.where("parentId").exists(true) // ) // ) // //// Пример использования в MongoTemplate: // val query = Query(criteria) // //// Если вы хотите использовать ReactiveMongoTemplate: // return reactiveMongoTemplate.find(query, Transaction::class.java) // .collectList() // .doOnNext { transactions -> println("Found transactions: $transactions") } // } // // // // @Cacheable("transactions") // fun getTransactionById(id: String): Mono { // return transactionsRepo.findById(id) // .map { // it // } // .switchIfEmpty( // Mono.error(IllegalArgumentException("Transaction with id: $id not found")) // ) // } // // // @CacheEvict(cacheNames = ["transactions"], allEntries = true) // fun createTransaction(transaction: Transaction): Mono { // return ReactiveSecurityContextHolder.getContext() // .map { it.authentication } // Получаем Authentication из SecurityContext // .flatMap { authentication -> // val username = authentication.name // Имя пользователя из токена // // Получаем пользователя и сохраняем транзакцию // userService.getByUsername(username) // .switchIfEmpty(Mono.error(IllegalArgumentException("User not found for username: $username"))) // .flatMap { user -> // transaction.user = user // transactionsRepo.save(transaction) // .doOnNext { savedTransaction -> // // Публикуем событие после сохранения // eventPublisher.publishEvent( // TransactionEvent( // this, // TransactionEventType.CREATE, // newTransaction = savedTransaction, // oldTransaction = savedTransaction // ) // ) // } // .map { it.id!! } // Возвращаем ID сохраненной транзакции // } // } // } // // // @CacheEvict(cacheNames = ["transactions"], allEntries = true) // // fun editTransaction(transaction: Transaction): Mono { // return transactionsRepo.findById(transaction.id!!) // .flatMap { oldStateOfTransaction -> // val changed = compareSumDateDoneIsChanged(oldStateOfTransaction, transaction) // if (!changed) { // return@flatMap transactionsRepo.save(transaction) // Сохраняем, если изменений нет // } // // val amountDifference = transaction.amount - oldStateOfTransaction.amount // // // Обработка дочерней транзакции // handleChildTransaction(oldStateOfTransaction, transaction, amountDifference) // .then(transactionsRepo.save(transaction)) // Сохраняем основную транзакцию // .doOnSuccess { savedTransaction -> // eventPublisher.publishEvent( // TransactionEvent( // this, // TransactionEventType.EDIT, // newTransaction = savedTransaction, // oldTransaction = oldStateOfTransaction, // difference = amountDifference // ) // ) // } // } // .switchIfEmpty( // Mono.error(IllegalArgumentException("Transaction not found with id: ${transaction.id}")) // ) // } // // private fun handleChildTransaction( // oldTransaction: Transaction, // newTransaction: Transaction, // amountDifference: Double // ): Mono { // return transactionsRepo.findByParentId(newTransaction.id!!) // .flatMap { childTransaction -> // logger.info(childTransaction.toString()) // // Если родительская транзакция обновлена, обновляем дочернюю // childTransaction.amount = newTransaction.amount // childTransaction.category = newTransaction.category // childTransaction.comment = newTransaction.comment // childTransaction.user = newTransaction.user // transactionsRepo.save(childTransaction) // } // .switchIfEmpty( // Mono.defer { // // Создание новой дочерней транзакции, если требуется // if (!oldTransaction.isDone && newTransaction.isDone) { // val newChildTransaction = newTransaction.copy( // id = null, // type = TransactionType("INSTANT", "Текущие"), // parentId = newTransaction.id // ) // transactionsRepo.save(newChildTransaction).doOnSuccess { savedChildTransaction -> // eventPublisher.publishEvent( // TransactionEvent( // this, // TransactionEventType.CREATE, // newTransaction = savedChildTransaction, // oldTransaction = oldTransaction, // difference = amountDifference // ) // ) // } // } else Mono.empty() // } // ) // .flatMap { // // Удаление дочерней транзакции, если родительская помечена как не выполненная // if (oldTransaction.isDone && !newTransaction.isDone) { // transactionsRepo.findByParentId(newTransaction.id!!) // .flatMap { child -> // deleteTransaction(child.id!!) // }.then() // } else { // Mono.empty() // } // } // } // // // fun compareSumDateDoneIsChanged(t1: Transaction, t2: Transaction): Boolean { // return if (t1.amount != t2.amount) { // true // } else if (t1.date != t2.date) { // true // } else if (t1.isDone != t2.isDone) { // true // } else if (t1.category.id != t2.category.id) { // return true // } else { // return false // } // } // // @CacheEvict(cacheNames = ["transactions"], allEntries = true) // fun deleteTransaction(transactionId: String): Mono { // return transactionsRepo.findById(transactionId) // .flatMap { transactionToDelete -> // transactionsRepo.deleteById(transactionId) // Удаляем транзакцию // .then( // Mono.fromRunnable { // // Публикуем событие после успешного удаления // eventPublisher.publishEvent( // TransactionEvent( // this, // TransactionEventType.DELETE, // newTransaction = transactionToDelete, // oldTransaction = transactionToDelete // ) // ) // } // ) // } // // } // // //// @CacheEvict(cacheNames = ["transactions", "childTransactions"], allEntries = true) //// fun setTransactionDone(transaction: Transaction): Transaction { //// val oldStateTransaction = transactionsRepo.findById(transaction.id!!) //// .orElseThrow { RuntimeException("Transaction ${transaction.id} not found") } //// //// if (transaction.isDone) { //// if (oldStateTransaction.isDone) { //// throw RuntimeException("Transaction ${transaction.id} is already done") //// } //// //// // Создание дочерней транзакции //// val childTransaction = transaction.copy( //// id = null, //// type = TransactionType("INSTANT", "Текущие"), //// parentId = transaction.id //// ) //// createTransaction(childTransaction) //// } else { //// // Удаление дочерней транзакции, если она существует //// transactionsRepo.findByParentId(transaction.id!!).getOrNull()?.let { //// deleteTransaction(it.id!!) //// } ?: logger.warn("Child transaction of parent ${transaction.id} not found") //// } //// //// return editTransaction(transaction) //// } // // // @Cacheable("childTransactions", key = "#parentId") // fun getChildTransaction(parentId: String): Mono { // return transactionsRepo.findByParentId(parentId) // } // //// fun getTransactionByOldId(id: Int): Transaction? { //// return transactionsRepo.findByOldId(id).getOrNull() //// } // //// fun transferTransactions(): Mono { //// var transactions = transactionsRepoSQl.getTransactions() //// return transactionsRepo.saveAll(transactions).then() //// } //// // // fun calcTransactionsSum( // budget: Budget, // categoryId: String? = null, // categoryType: String? = null, // transactionType: String? = null, // isDone: Boolean? = null // ): Mono { // val matchCriteria = mutableListOf() // // // Добавляем фильтры // matchCriteria.add(Criteria.where("date").gte(budget.dateFrom)) // matchCriteria.add(Criteria.where("date").lt(budget.dateTo)) // categoryId?.let { matchCriteria.add(Criteria.where("category.\$id").`is`(ObjectId(it))) } // categoryType?.let { matchCriteria.add(Criteria.where("categoryDetails.type.code").`is`(it)) } // transactionType?.let { matchCriteria.add(Criteria.where("type.code").`is`(it)) } // isDone?.let { matchCriteria.add(Criteria.where("isDone").`is`(it)) } // // // Сборка агрегации // val lookup = lookup("categories", "category.\$id", "_id", "categoryDetails") // val unwind = unwind("categoryDetails") // val match = match(Criteria().andOperator(*matchCriteria.toTypedArray())) // val project = project("category").andExpression("{ \$toDouble: \"\$amount\" }").`as`("amount") // val group = group(categoryId ?: "all").sum("amount").`as`("totalSum") // val projectSum = project("totalSum") // val aggregation = newAggregation(lookup, unwind, match, project, group, projectSum) // // return reactiveMongoTemplate.aggregate(aggregation, "transactions", Map::class.java) // .map { result -> // val totalSum = result["totalSum"] // if (totalSum is Double) { // totalSum // } else { // 0.0 // } // } // .reduce(0.0) { acc, sum -> acc + sum } // Суммируем значения, если несколько результатов // } // // // // @Cacheable("transactions") // fun getAverageSpendingByCategory(): Mono> { // val firstDateOfMonth = LocalDate.now().with(TemporalAdjusters.firstDayOfMonth()) // // val lookup = lookup("categories", "category.\$id", "_id", "categoryDetails") // val unwind = unwind("categoryDetails") // val match = match( // Criteria.where("categoryDetails.type.code").`is`("EXPENSE") // .and("type.code").`is`("INSTANT") // .and("date").lt(firstDateOfMonth) // ) // val projectDate = project("_id", "category", "amount", "categoryDetails") // .and(DateToString.dateOf("date").toString("%Y-%m")).`as`("month") // .andExpression("{ \$toDouble: \"\$amount\" }").`as`("amount") // val groupByMonthAndCategory = group("month", "category.\$id").sum("amount").`as`("sum") // val groupByCategory = group("_id.id").avg("sum").`as`("averageAmount") // val project = project() // .and("_id").`as`("category") // .and("averageAmount").`as`("avgAmount") // val sort = sort(Sort.by(Sort.Order.asc("_id"))) // // val aggregation = newAggregation( // lookup, unwind, match, projectDate, groupByMonthAndCategory, groupByCategory, project, sort // ) // // return reactiveMongoTemplate.aggregate(aggregation, "transactions", Map::class.java) // .collectList() // .map { results -> // results.associate { result -> // val category = result["category"]?.toString() ?: "Unknown" // val avgAmount = (result["avgAmount"] as? Double) ?: 0.0 // category to avgAmount // } // } // .defaultIfEmpty(emptyMap()) // Возвращаем пустую карту, если результатов нет // } // // // @Cacheable("transactionTypes") // fun getTransactionTypes(): List { // var types = mutableListOf() // types.add(TransactionType("PLANNED", "Плановые")) // types.add(TransactionType("INSTANT", "Текущие")) // return types // } // // fun getAverageIncome(): Mono { // val lookup = lookup("categories", "category.\$id", "_id", "detailedCategory") // // val unwind = unwind("detailedCategory") // // val match = match( // Criteria.where("detailedCategory.type.code").`is`("INCOME") // .and("type.code").`is`("INSTANT") // .and("isDone").`is`(true) // ) // // val project = project("_id", "category", "detailedCategory") // .and(DateToString.dateOf("date").toString("%Y-%m")).`as`("month") // .andExpression("{ \$toDouble: \"\$amount\" }").`as`("amount") // // val groupByMonth = group("month").sum("amount").`as`("sum") // // val groupForAverage = group("avgIncomeByMonth").avg("sum").`as`("averageAmount") // // val aggregation = newAggregation(lookup, unwind, match, project, groupByMonth, groupForAverage) // // return reactiveMongoTemplate.aggregate(aggregation, "transactions", Map::class.java) // .singleOrEmpty() // Ожидаем только один результат // .map { result -> // result["averageAmount"] as? Double ?: 0.0 // } // .defaultIfEmpty(0.0) // Если результат пустой, возвращаем 0.0 // } // // // fun getTransactionsByTypes(dateFrom: LocalDate, dateTo: LocalDate): Mono>> { // logger.info("here tran starts") // val pipeline = listOf( // Document( // "\$lookup", // Document("from", "categories") // .append("localField", "category.\$id") // .append("foreignField", "_id") // .append("as", "categoryDetailed") // ), // Document( // "\$lookup", // Document("from", "users") // .append("localField", "user.\$id") // .append("foreignField", "_id") // .append("as", "userDetailed") // ), // Document( // "\$unwind", // Document("path", "\$categoryDetailed").append("preserveNullAndEmptyArrays", true) // ), // Document( // "\$unwind", // Document("path", "\$userDetailed").append("preserveNullAndEmptyArrays", true) // ), // Document( // "\$match", // Document( // "\$and", listOf( // Document( // "date", // Document( // "\$gte", // Date.from( // LocalDateTime.of(dateFrom, LocalTime.MIN) // .atZone(ZoneId.systemDefault()) // .withZoneSameInstant(ZoneOffset.UTC) // .toInstant() // ) // ) // ), // Document( // "date", // Document( // "\$lt", // Date.from( // LocalDateTime.of(dateTo, LocalTime.MAX) // .atZone(ZoneId.systemDefault()) // .withZoneSameInstant(ZoneOffset.UTC) // .toInstant() // ) // ) // ) // ) // ) // ), // Document( // "\$facet", // Document( // "plannedExpenses", // listOf( // Document( // "\$match", // Document("type.code", "PLANNED") // .append("categoryDetailed.type.code", "EXPENSE") // ), // Document("\$sort", Document("date", 1).append("_id", 1)) // ) // ) // .append( // "plannedIncomes", // listOf( // Document( // "\$match", // Document("type.code", "PLANNED") // .append("categoryDetailed.type.code", "INCOME") // ), // Document("\$sort", Document("date", 1).append("_id", 1)) // ) // ) // .append( // "instantTransactions", // listOf( // Document("\$match", Document("type.code", "INSTANT")), // Document("\$sort", Document("date", 1).append("_id", 1)) // ) // ) // ) // ) // //// getCategoriesExplainReactive(pipeline) //// .doOnNext { explainResult -> //// logger.info("Explain Result: ${explainResult.toJson()}") //// } //// .subscribe() // Этот вызов лучше оставить только для отладки // return reactiveMongoTemplate.getCollection("transactions") // .flatMapMany { it.aggregate(pipeline, Document::class.java) } // .single() // Получаем только первый результат агрегации // .flatMap { aggregationResult -> // Mono.zip( // extractTransactions(aggregationResult, "plannedExpenses"), // extractTransactions(aggregationResult, "plannedIncomes"), // extractTransactions(aggregationResult, "instantTransactions") // ).map { tuple -> // val plannedExpenses = tuple.t1 // val plannedIncomes = tuple.t2 // val instantTransactions = tuple.t3 // logger.info("here tran ends") // mapOf( // "plannedExpenses" to plannedExpenses, // "plannedIncomes" to plannedIncomes, // "instantTransactions" to instantTransactions // ) // // // } // // } // // // } // // // private fun extractTransactions(aggregationResult: Document, key: String): Mono> { // val resultTransactions = aggregationResult[key] as? List ?: emptyList() // return Flux.fromIterable(resultTransactions) // .map { documentToTransactionMapper(it) } // .collectList() // } // // // private fun documentToTransactionMapper(document: Document): Transaction { // val transactionType = document["type"] as Document // var user: User? = null // // val userDocument = document["userDetailed"] as Document // user = User( // id = (userDocument["_id"] as ObjectId).toString(), // username = userDocument["username"] as String, // firstName = userDocument["firstName"] as String, // tgId = userDocument["tgId"] as String, // tgUserName = userDocument["tgUserName"]?.let { it as String }, // password = null, // isActive = userDocument["isActive"] as Boolean, // regDate = userDocument["regDate"] as Date, // createdAt = userDocument["createdAt"] as Date, // roles = userDocument["roles"] as ArrayList, // ) // // // val categoryDocument = document["categoryDetailed"] as Document // val categoryTypeDocument = categoryDocument["type"] as Document // val category = Category( // id = (categoryDocument["_id"] as ObjectId).toString(), // type = CategoryType(categoryTypeDocument["code"] as String, categoryTypeDocument["name"] as String), // name = categoryDocument["name"] as String, // description = categoryDocument["description"] as String, // icon = categoryDocument["icon"] as String // ) // return Transaction( // (document["_id"] as ObjectId).toString(), // TransactionType( // transactionType["code"] as String, // transactionType["name"] as String // ), // user = user, // category = category, // comment = document["comment"] as String, // date = (document["date"] as Date).toInstant().atZone(ZoneId.systemDefault()).toLocalDate(), // amount = document["amount"] as Double, // isDone = document["isDone"] as Boolean, // parentId = if (document["parentId"] != null) document["parentId"] as String else null, // createdAt = LocalDateTime.ofInstant((document["createdAt"] as Date).toInstant(), ZoneOffset.UTC), // ) // // } //}