Files
luminic-back/src/main/kotlin/space/luminic/budgerapp/services/TransactionService.kt
2025-02-11 13:46:50 +03:00

632 lines
29 KiB
Kotlin

//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<MutableList<Transaction>> {
// val matchCriteria = mutableListOf<Criteria>()
//
// // Добавляем фильтры
// 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<Transaction> в Mono<List<Transaction>>
// .map { it.toMutableList() }
// }
//
// fun getTransactionsToDelete(dateFrom: LocalDate, dateTo: LocalDate): Mono<List<Transaction>> {
// 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<Transaction> {
// 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<String> {
// 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<Transaction> {
// 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<Void> {
// 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<Void> {
// return transactionsRepo.findById(transactionId)
// .flatMap { transactionToDelete ->
// transactionsRepo.deleteById(transactionId) // Удаляем транзакцию
// .then(
// Mono.fromRunnable<Void> {
// // Публикуем событие после успешного удаления
// 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<Transaction> {
// return transactionsRepo.findByParentId(parentId)
// }
//
//// fun getTransactionByOldId(id: Int): Transaction? {
//// return transactionsRepo.findByOldId(id).getOrNull()
//// }
//
//// fun transferTransactions(): Mono<Void> {
//// 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<Double> {
// val matchCriteria = mutableListOf<Criteria>()
//
// // Добавляем фильтры
// 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<Map<String, Double>> {
// 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<TransactionType> {
// var types = mutableListOf<TransactionType>()
// types.add(TransactionType("PLANNED", "Плановые"))
// types.add(TransactionType("INSTANT", "Текущие"))
// return types
// }
//
// fun getAverageIncome(): Mono<Double> {
// 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<Map<String, List<Transaction>>> {
// 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<List<Transaction>> {
// val resultTransactions = aggregationResult[key] as? List<Document> ?: 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<String>,
// )
//
//
// 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),
// )
//
// }
//}