init
This commit is contained in:
@@ -0,0 +1,784 @@
|
||||
package space.luminic.budgerapp.services
|
||||
|
||||
import com.mongodb.client.model.Aggregates.addFields
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.runBlocking
|
||||
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.MongoExpression
|
||||
|
||||
import org.springframework.data.mongodb.core.aggregation.Aggregation.newAggregation
|
||||
import org.springframework.data.mongodb.core.MongoTemplate
|
||||
import org.springframework.data.mongodb.core.ReactiveMongoTemplate
|
||||
import org.springframework.data.mongodb.core.aggregation.AddFieldsOperation
|
||||
import org.springframework.data.mongodb.core.aggregation.Aggregation
|
||||
import org.springframework.data.mongodb.core.aggregation.Aggregation.ROOT
|
||||
import org.springframework.data.mongodb.core.aggregation.Aggregation.group
|
||||
import org.springframework.data.mongodb.core.aggregation.Aggregation.lookup
|
||||
import org.springframework.data.mongodb.core.aggregation.Aggregation.match
|
||||
import org.springframework.data.mongodb.core.aggregation.Aggregation.project
|
||||
import org.springframework.data.mongodb.core.aggregation.Aggregation.sort
|
||||
import org.springframework.data.mongodb.core.aggregation.Aggregation.unwind
|
||||
import org.springframework.data.mongodb.core.aggregation.Aggregation.addFields
|
||||
import org.springframework.data.mongodb.core.aggregation.Aggregation.limit
|
||||
import org.springframework.data.mongodb.core.aggregation.Aggregation.skip
|
||||
import org.springframework.data.mongodb.core.aggregation.AggregationExpression
|
||||
import org.springframework.data.mongodb.core.aggregation.AggregationResults
|
||||
import org.springframework.data.mongodb.core.aggregation.ArrayOperators
|
||||
import org.springframework.data.mongodb.core.aggregation.ArrayOperators.Filter.filter
|
||||
import org.springframework.data.mongodb.core.aggregation.ConditionalOperators
|
||||
import org.springframework.data.mongodb.core.aggregation.ConditionalOperators.IfNull.ifNull
|
||||
import org.springframework.data.mongodb.core.aggregation.DateOperators
|
||||
import org.springframework.data.mongodb.core.aggregation.DateOperators.DateToString
|
||||
import org.springframework.data.mongodb.core.aggregation.LookupOperation
|
||||
import org.springframework.data.mongodb.core.aggregation.MatchOperation
|
||||
|
||||
|
||||
import org.springframework.data.mongodb.core.query.Criteria
|
||||
import org.springframework.data.mongodb.core.query.Query
|
||||
import org.springframework.data.mongodb.core.query.update
|
||||
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.SortTypes
|
||||
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.math.BigDecimal
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
import java.time.LocalTime
|
||||
import java.time.ZoneId
|
||||
import java.time.ZoneOffset
|
||||
import java.time.ZonedDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.temporal.TemporalAdjusters
|
||||
import java.util.ArrayList
|
||||
import java.util.Arrays
|
||||
import java.util.Date
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
|
||||
@Service
|
||||
class TransactionService(
|
||||
private val mongoTemplate: MongoTemplate,
|
||||
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
|
||||
)
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
fun getCategoriesExplainReactive(pipeline: List<Document>): Mono<Document> {
|
||||
val command = Document("aggregate", "transactions")
|
||||
.append("pipeline", pipeline)
|
||||
.append("explain", true)
|
||||
|
||||
return reactiveMongoTemplate.executeCommand(command)
|
||||
}
|
||||
|
||||
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),
|
||||
)
|
||||
|
||||
}
|
||||
// fun getPlannedForBudget(budget: Budget, transactionType: String? = null): List<Map<String, Any>> {
|
||||
// // 1) $lookup: "categories"
|
||||
// val lookupCategories = Aggregation.lookup(
|
||||
// "categories",
|
||||
// "category.\$id",
|
||||
// "_id",
|
||||
// "categoryDetailed"
|
||||
// )
|
||||
//
|
||||
// // 2) $lookup: "budgets" (pipeline + let)
|
||||
// val matchBudgetsDoc = Document(
|
||||
// "\$expr", Document(
|
||||
// "\$and", listOf(
|
||||
// Document("\$gte", listOf("\$\$transactionDate", "\$dateFrom")),
|
||||
// Document("\$lt", listOf("\$\$transactionDate", "\$dateTo"))
|
||||
// )
|
||||
// )
|
||||
// )
|
||||
// val matchBudgetsOp = MatchOperation(matchBudgetsDoc)
|
||||
//
|
||||
// val lookupBudgets = LookupOperation.newLookup()
|
||||
// .from("budgets")
|
||||
// .letValueOf("transactionDate").bindTo("date")
|
||||
// .pipeline(matchBudgetsOp)
|
||||
// .`as`("budgetDetails")
|
||||
//
|
||||
// // 3) $unwind
|
||||
// val unwindCategory = Aggregation.unwind("categoryDetailed")
|
||||
// val unwindBudget = Aggregation.unwind("budgetDetails")
|
||||
//
|
||||
// // 4) $match: диапазон дат
|
||||
// val matchDates = Aggregation.match(
|
||||
// Criteria("date")
|
||||
// .gte(budget.dateFrom)
|
||||
// .lt(budget.dateTo)
|
||||
// )
|
||||
//
|
||||
// // 5) $facet (разные ветки: plannedExpenses, plannedExpensesSum, ...)
|
||||
// // plannedExpenses
|
||||
// val plannedExpensesMatch = Aggregation.match(
|
||||
// Criteria().andOperator(
|
||||
// Criteria("type.code").`is`("PLANNED"),
|
||||
// Criteria("categoryDetailed.type.code").`is`("EXPENSE")
|
||||
// )
|
||||
// )
|
||||
// val plannedExpensesPipeline = listOf(plannedExpensesMatch)
|
||||
//
|
||||
// // plannedExpensesSum
|
||||
// val plannedExpensesSumPipeline = listOf(
|
||||
// plannedExpensesMatch,
|
||||
// group(null).`as`("_id").sum("amount").`as`("sum"),
|
||||
// project("sum").andExclude("_id")
|
||||
// )
|
||||
//
|
||||
// // plannedIncome
|
||||
// val plannedIncomeMatch = Aggregation.match(
|
||||
// Criteria().andOperator(
|
||||
// Criteria("type.code").`is`("PLANNED"),
|
||||
// Criteria("categoryDetailed.type.code").`is`("INCOME")
|
||||
// )
|
||||
// )
|
||||
// val plannedIncomePipeline = listOf(plannedIncomeMatch)
|
||||
//
|
||||
// // plannedIncomeSum
|
||||
// val plannedIncomeSumPipeline = listOf(
|
||||
// plannedIncomeMatch,
|
||||
// group().`as`("_id").sum("amount").`as`("sum"),
|
||||
// project("sum").andExclude("_id")
|
||||
// )
|
||||
//
|
||||
// // instantTransactions
|
||||
// val instantTransactionsMatch = Aggregation.match(
|
||||
// Criteria("type.code").`is`("INSTANT")
|
||||
// )
|
||||
// val instantTransactionsProject = Aggregation.project(
|
||||
// "_id", "type", "comment", "user", "amount", "date",
|
||||
// "category", "isDone", "createdAt", "parentId"
|
||||
// )
|
||||
// val instantTransactionsPipeline = listOf(instantTransactionsMatch, instantTransactionsProject)
|
||||
//
|
||||
// val facetStage = Aggregation.facet(*plannedExpensesPipeline.toTypedArray()).`as`("plannedExpenses")
|
||||
// .and(*plannedExpensesSumPipeline.toTypedArray()).`as`("plannedExpensesSum")
|
||||
// .and(*plannedIncomePipeline.toTypedArray()).`as`("plannedIncome")
|
||||
// .and(*plannedIncomeSumPipeline.toTypedArray()).`as`("plannedIncomeSum")
|
||||
// .and(*instantTransactionsPipeline.toTypedArray()).`as`("instantTransactions")
|
||||
//
|
||||
// // 6) $set: вытаскиваем суммы из массивов
|
||||
// val setStage = AddFieldsOperation.builder()
|
||||
// .addField("plannedExpensesSum").withValue(
|
||||
// ArrayOperators.ArrayElemAt.arrayOf("\$plannedExpensesSum.sum").elementAt(0)
|
||||
// )
|
||||
// .addField("plannedIncomeSum").withValue(
|
||||
// ArrayOperators.ArrayElemAt.arrayOf("\$plannedIncomeSum.sum").elementAt(0)
|
||||
// )
|
||||
// .build()
|
||||
//
|
||||
// // Собираем все стадии
|
||||
// val aggregation = Aggregation.newAggregation(
|
||||
// lookupCategories,
|
||||
// lookupBudgets,
|
||||
// unwindCategory,
|
||||
// unwindBudget,
|
||||
// matchDates,
|
||||
// facetStage,
|
||||
// setStage
|
||||
// )
|
||||
//
|
||||
// val results = mongoTemplate.aggregate(aggregation, "transactions", Map::class.java)
|
||||
// return results.mappedResults
|
||||
// }
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user