fix recurrents
This commit is contained in:
@@ -43,6 +43,25 @@ class AuthService(
|
||||
}
|
||||
}
|
||||
|
||||
fun tgLogin(tgId: String): Mono<String> {
|
||||
return userRepository.findByTgId(tgId)
|
||||
.switchIfEmpty(Mono.error(AuthException("Invalid credentials")))
|
||||
.flatMap { user ->
|
||||
println("here")
|
||||
val token = jwtUtil.generateToken(user.username)
|
||||
val expireAt = Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24 * 10)
|
||||
tokenService.saveToken(
|
||||
token = token,
|
||||
username = user.username,
|
||||
expiresAt = LocalDateTime.ofInstant(
|
||||
expireAt.toInstant(),
|
||||
ZoneId.systemDefault()
|
||||
)
|
||||
).thenReturn(token)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
fun register(username: String, password: String, firstName: String): Mono<User> {
|
||||
return userRepository.findByUsername(username)
|
||||
.flatMap<User> { Mono.error(IllegalArgumentException("User with username '$username' already exists")) } // Ошибка, если пользователь уже существует
|
||||
|
||||
@@ -8,7 +8,6 @@ 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.*
|
||||
import org.springframework.data.mongodb.core.query.Criteria
|
||||
@@ -16,72 +15,102 @@ import org.springframework.data.mongodb.core.query.isEqualTo
|
||||
import org.springframework.stereotype.Service
|
||||
import reactor.core.publisher.Flux
|
||||
import reactor.core.publisher.Mono
|
||||
import space.luminic.budgerapp.mappers.CategoryMapper
|
||||
import space.luminic.budgerapp.models.*
|
||||
import space.luminic.budgerapp.repos.CategoryRepo
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
import java.time.LocalTime
|
||||
import java.time.ZoneId
|
||||
import java.time.ZoneOffset
|
||||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
|
||||
@Service
|
||||
class CategoryService(
|
||||
private val categoryRepo: CategoryRepo,
|
||||
private val financialService: FinancialService,
|
||||
private val mongoTemplate: ReactiveMongoTemplate,
|
||||
private val eventPublisher: ApplicationEventPublisher
|
||||
) {
|
||||
private val eventPublisher: ApplicationEventPublisher,
|
||||
private val categoryMapper: CategoryMapper,
|
||||
|
||||
) {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(javaClass)
|
||||
|
||||
|
||||
fun getCategory(id: String): Mono<Category> {
|
||||
return categoryRepo.findById(id)
|
||||
}
|
||||
|
||||
|
||||
// @Cacheable("categories")
|
||||
fun getCategories(spaceId: String, type: String? = null, sortBy: String, direction: String): Mono<List<Category>> {
|
||||
fun findCategory(
|
||||
space: Space? = null,
|
||||
id: String? = null,
|
||||
name: String? = null,
|
||||
tagCode: String? = null
|
||||
): Mono<Category> {
|
||||
val lookupSpaces = lookup("spaces", "space.\$id", "_id", "spaceDetails")
|
||||
val unwindSpace = unwind("spaceDetails")
|
||||
val matchCriteria = mutableListOf<Criteria>()
|
||||
|
||||
// Добавляем фильтры
|
||||
matchCriteria.add(Criteria.where("spaceDetails._id").`is`(ObjectId(spaceId)))
|
||||
type?.let { matchCriteria.add(Criteria.where("type.code").isEqualTo(it)) }
|
||||
matchCriteria.add(Criteria.where("spaceDetails._id").`is`(ObjectId(space!!.id)))
|
||||
id?.let { matchCriteria.add(Criteria.where("_id").`is`(ObjectId(id))) }
|
||||
name?.let { matchCriteria.add(Criteria.where("name").isEqualTo(it.trim())) }
|
||||
tagCode?.let { matchCriteria.add(Criteria.where("tags.code").`is`(it)) }
|
||||
|
||||
val match = match(Criteria().andOperator(*matchCriteria.toTypedArray()))
|
||||
|
||||
|
||||
// val project = project("_id", "type", "name", "description", "icon")
|
||||
|
||||
val aggregationBuilder = mutableListOf(
|
||||
lookupSpaces,
|
||||
unwindSpace,
|
||||
match.takeIf { matchCriteria.isNotEmpty() },
|
||||
).filterNotNull()
|
||||
|
||||
val aggregation = newAggregation(aggregationBuilder)
|
||||
return mongoTemplate.aggregate(
|
||||
aggregation, "categories", Document::class.java
|
||||
).next()
|
||||
.map { doc ->
|
||||
categoryMapper.fromDocument(doc)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun getCategories(
|
||||
spaceId: String,
|
||||
type: String? = null,
|
||||
sortBy: String,
|
||||
direction: String,
|
||||
tagCode: String? = null
|
||||
): Mono<List<Category>> {
|
||||
val lookupSpaces = lookup("spaces", "space.\$id", "_id", "spaceDetails")
|
||||
val unwindSpace = unwind("spaceDetails")
|
||||
val matchCriteria = mutableListOf<Criteria>()
|
||||
// Добавляем фильтры
|
||||
matchCriteria.add(Criteria.where("spaceDetails._id").`is`(ObjectId(spaceId)))
|
||||
type?.let { matchCriteria.add(Criteria.where("type.code").isEqualTo(it)) }
|
||||
tagCode?.let { matchCriteria.add(Criteria.where("tags.code").`is`(it)) }
|
||||
|
||||
val match = match(Criteria().andOperator(*matchCriteria.toTypedArray()))
|
||||
|
||||
val sort = sort(Sort.by(direction, sortBy))
|
||||
|
||||
val lookupSpaces = lookup("spaces", "space.\$id", "_id", "spaceDetails")
|
||||
val project = project("_id", "type", "name", "description", "icon")
|
||||
// val project = project("_id", "type", "name", "description", "icon")
|
||||
|
||||
val aggregationBuilder = mutableListOf(
|
||||
|
||||
lookupSpaces,
|
||||
unwindSpace,
|
||||
match.takeIf { matchCriteria.isNotEmpty() },
|
||||
project,
|
||||
// project,
|
||||
sort,
|
||||
).filterNotNull()
|
||||
|
||||
val aggregation = newAggregation(aggregationBuilder)
|
||||
logger.error("STARTED")
|
||||
return mongoTemplate.aggregate(
|
||||
aggregation, "categories", Category::class.java
|
||||
aggregation, "categories", Document::class.java
|
||||
)
|
||||
.collectList() // Преобразуем Flux<Transaction> в Mono<List<Transaction>>
|
||||
.map { it.toMutableList() }
|
||||
}
|
||||
|
||||
|
||||
@Cacheable("getAllCategories")
|
||||
fun getCategories2(type: String? = null, sortBy: String, direction: String): Mono<List<Category>> {
|
||||
return categoryRepo.findAll(Sort.by(if (direction == "ASC") Direction.ASC else Direction.DESC, sortBy))
|
||||
.collectList()
|
||||
|
||||
|
||||
.map { docs ->
|
||||
docs.map { doc ->
|
||||
categoryMapper.fromDocument(doc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Cacheable("categoryTypes")
|
||||
@@ -93,681 +122,44 @@ class CategoryService(
|
||||
}
|
||||
|
||||
|
||||
|
||||
@CacheEvict(cacheNames = ["getAllCategories"], allEntries = true)
|
||||
fun editCategory(category: Category): Mono<Category> {
|
||||
return categoryRepo.findById(category.id!!) // Возвращаем Mono<Category>
|
||||
fun editCategory(space: Space, category: Category): Mono<Category> {
|
||||
return findCategory(space, id = category.id) // Возвращаем Mono<Category>
|
||||
.flatMap { oldCategory ->
|
||||
if (oldCategory.type.code != category.type.code) {
|
||||
return@flatMap Mono.error<Category>(IllegalArgumentException("You cannot change category type"))
|
||||
}
|
||||
category.space = space
|
||||
categoryRepo.save(category) // Сохраняем категорию, если тип не изменился
|
||||
}
|
||||
}
|
||||
|
||||
@CacheEvict(cacheNames = ["getAllCategories"], allEntries = true)
|
||||
// fun deleteCategory(categoryId: String): Mono<String> {
|
||||
// return categoryRepo.findById(categoryId).switchIfEmpty(
|
||||
// Mono.error(IllegalArgumentException("Category with id: $categoryId not found"))
|
||||
// ).flatMap {
|
||||
// financialService.getTransactions(categoryId = categoryId)
|
||||
// .flatMapMany { transactions ->
|
||||
// categoryRepo.findByName("Другое").switchIfEmpty(
|
||||
// categoryRepo.save(
|
||||
// Category(
|
||||
// type = CategoryType("EXPENSE", "Траты"),
|
||||
// name = "Другое",
|
||||
// description = "Категория для других трат",
|
||||
// icon = "🚮"
|
||||
// )
|
||||
// )
|
||||
// ).flatMapMany { category ->
|
||||
// Flux.fromIterable(transactions).flatMap { transaction ->
|
||||
// transaction.category = category // Присваиваем конкретный объект категории
|
||||
// financialService.editTransaction(transaction) // Сохраняем изменения
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// .then(categoryRepo.deleteById(categoryId)) // Удаляем старую категорию
|
||||
// .thenReturn(categoryId) // Возвращаем удалённую категорию
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
fun getBudgetCategories(dateFrom: LocalDate, dateTo: LocalDate): Mono<Map<String, Map<String, Double>>> {
|
||||
val pipeline = listOf(
|
||||
Document(
|
||||
"\$lookup",
|
||||
Document("from", "transactions")
|
||||
.append(
|
||||
"let",
|
||||
Document("categoryId", "\$_id")
|
||||
)
|
||||
.append(
|
||||
"pipeline", listOf(
|
||||
Document(
|
||||
"\$match",
|
||||
Document(
|
||||
"\$expr",
|
||||
Document(
|
||||
"\$and", listOf(
|
||||
Document("\$eq", listOf("\$category.\$id", "\$\$categoryId")),
|
||||
Document(
|
||||
"\$gte", listOf(
|
||||
"\$date",
|
||||
Date.from(
|
||||
LocalDateTime.of(dateFrom, LocalTime.MIN)
|
||||
.atZone(ZoneId.systemDefault())
|
||||
.withZoneSameInstant(ZoneOffset.UTC).toInstant()
|
||||
),
|
||||
)
|
||||
),
|
||||
Document(
|
||||
"\$lte", listOf(
|
||||
"\$date",
|
||||
Date.from(
|
||||
LocalDateTime.of(dateTo, LocalTime.MIN)
|
||||
.atZone(ZoneId.systemDefault())
|
||||
.withZoneSameInstant(ZoneOffset.UTC).toInstant()
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
Document(
|
||||
"\$group",
|
||||
Document("_id", "\$type.code")
|
||||
.append(
|
||||
"totalAmount",
|
||||
Document("\$sum", "\$amount")
|
||||
)
|
||||
fun deleteCategory(space: Space, categoryId: String): Mono<String> {
|
||||
return findCategory(space, id = categoryId).switchIfEmpty(
|
||||
Mono.error(IllegalArgumentException("Category with id: $categoryId not found"))
|
||||
).flatMap {
|
||||
financialService.getTransactions(space.id!!, categoryId = categoryId)
|
||||
.flatMapMany { transactions ->
|
||||
findCategory(space, name = "Другое").switchIfEmpty(
|
||||
categoryRepo.save(
|
||||
Category(
|
||||
space = space,
|
||||
type = CategoryType("EXPENSE", "Траты"),
|
||||
name = "Другое",
|
||||
description = "Категория для других трат",
|
||||
icon = "🚮"
|
||||
)
|
||||
)
|
||||
)
|
||||
.append("as", "transactionSums")
|
||||
),
|
||||
Document(
|
||||
"\$project",
|
||||
Document("_id", 1L)
|
||||
.append(
|
||||
"plannedAmount",
|
||||
Document(
|
||||
"\$arrayElemAt", listOf(
|
||||
Document(
|
||||
"\$filter",
|
||||
Document("input", "\$transactionSums")
|
||||
.append("as", "sum")
|
||||
.append(
|
||||
"cond",
|
||||
Document("\$eq", listOf("\$\$sum._id", "PLANNED"))
|
||||
)
|
||||
), 0L
|
||||
)
|
||||
)
|
||||
)
|
||||
.append(
|
||||
"instantAmount",
|
||||
Document(
|
||||
"\$arrayElemAt", listOf(
|
||||
Document(
|
||||
"\$filter",
|
||||
Document("input", "\$transactionSums")
|
||||
.append("as", "sum")
|
||||
.append(
|
||||
"cond",
|
||||
Document("\$eq", listOf("\$\$sum._id", "INSTANT"))
|
||||
)
|
||||
), 0L
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
Document(
|
||||
"\$addFields",
|
||||
Document(
|
||||
"plannedAmount",
|
||||
Document("\$ifNull", listOf("\$plannedAmount.totalAmount", 0.0))
|
||||
)
|
||||
.append(
|
||||
"instantAmount",
|
||||
Document("\$ifNull", listOf("\$instantAmount.totalAmount", 0.0))
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
// Анализ плана выполнения (вывод для отладки)
|
||||
// getCategoriesExplainReactive(pipeline)
|
||||
// .doOnNext { explainResult ->
|
||||
// logger.info("Explain Result: ${explainResult.toJson()}")
|
||||
// }
|
||||
// .subscribe() // Этот вызов лучше оставить только для отладки
|
||||
//
|
||||
|
||||
|
||||
return mongoTemplate.getCollection("categories")
|
||||
.flatMapMany { it.aggregate(pipeline) }
|
||||
.collectList()
|
||||
.flatMap { result ->
|
||||
val categories = result.associate { document ->
|
||||
val id = document["_id"].toString()
|
||||
val values = mapOf(
|
||||
"plannedAmount" to (document["plannedAmount"] as Double? ?: 0.0),
|
||||
"instantAmount" to (document["instantAmount"] as Double? ?: 0.0)
|
||||
)
|
||||
id to values
|
||||
}
|
||||
|
||||
Mono.just(categories)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun getCategoryTransactionPipeline(dateFrom: LocalDate, dateTo: LocalDate): Mono<MutableList<BudgetCategory>> {
|
||||
val pipeline = listOf(
|
||||
Document("\$match", Document("type.code", "EXPENSE")),
|
||||
Document(
|
||||
"\$lookup",
|
||||
Document("from", "transactions")
|
||||
.append(
|
||||
"let",
|
||||
Document("categoryId", "\$_id")
|
||||
)
|
||||
.append(
|
||||
"pipeline", listOf(
|
||||
Document(
|
||||
"\$match",
|
||||
Document(
|
||||
"\$expr",
|
||||
Document(
|
||||
"\$and", listOf(
|
||||
Document("\$eq", listOf("\$category.\$id", "\$\$categoryId")),
|
||||
Document(
|
||||
"\$gte", listOf(
|
||||
"\$date",
|
||||
Date.from(
|
||||
LocalDateTime.of(dateFrom, LocalTime.MIN)
|
||||
.atZone(ZoneId.systemDefault())
|
||||
.withZoneSameInstant(ZoneOffset.UTC).toInstant()
|
||||
)
|
||||
)
|
||||
),
|
||||
Document(
|
||||
"\$lt", listOf(
|
||||
"\$date",
|
||||
Date.from(
|
||||
LocalDateTime.of(dateTo, LocalTime.MIN)
|
||||
.atZone(ZoneId.systemDefault())
|
||||
.withZoneSameInstant(ZoneOffset.UTC).toInstant()
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
Document(
|
||||
"\$group",
|
||||
Document("_id", "\$type.code")
|
||||
.append(
|
||||
"totalAmount",
|
||||
Document("\$sum", "\$amount")
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
.append("as", "transactionSums")
|
||||
),
|
||||
Document(
|
||||
"\$project",
|
||||
Document("_id", 1L)
|
||||
.append("type", 1L)
|
||||
.append("name", 1L)
|
||||
.append("description", 1L)
|
||||
.append("icon", 1L)
|
||||
.append(
|
||||
"plannedAmount",
|
||||
Document(
|
||||
"\$arrayElemAt", listOf(
|
||||
Document(
|
||||
"\$filter",
|
||||
Document("input", "\$transactionSums")
|
||||
.append("as", "sum")
|
||||
.append(
|
||||
"cond",
|
||||
Document("\$eq", listOf("\$\$sum._id", "PLANNED"))
|
||||
)
|
||||
), 0.0
|
||||
)
|
||||
)
|
||||
)
|
||||
.append(
|
||||
"instantAmount",
|
||||
Document(
|
||||
"\$arrayElemAt", listOf(
|
||||
Document(
|
||||
"\$filter",
|
||||
Document("input", "\$transactionSums")
|
||||
.append("as", "sum")
|
||||
.append(
|
||||
"cond",
|
||||
Document("\$eq", listOf("\$\$sum._id", "INSTANT"))
|
||||
)
|
||||
), 0.0
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
Document(
|
||||
"\$addFields",
|
||||
Document(
|
||||
"plannedAmount",
|
||||
Document("\$ifNull", listOf("\$plannedAmount.totalAmount", 0.0))
|
||||
)
|
||||
.append(
|
||||
"instantAmount",
|
||||
Document("\$ifNull", listOf("\$instantAmount.totalAmount", 0.0))
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
return mongoTemplate.getCollection("categories")
|
||||
.flatMapMany { it.aggregate(pipeline, Document::class.java) }
|
||||
.map { document ->
|
||||
val catType = document["type"] as Document
|
||||
BudgetCategory(
|
||||
currentSpent = document["instantAmount"] as Double,
|
||||
currentLimit = document["plannedAmount"] as Double,
|
||||
currentPlanned = document["plannedAmount"] as Double,
|
||||
category = Category(
|
||||
document["_id"].toString(),
|
||||
type = CategoryType(catType["code"] as String, catType["name"] as String),
|
||||
name = document["name"] as String,
|
||||
description = document["description"] as String,
|
||||
icon = document["icon"] as String
|
||||
)
|
||||
)
|
||||
}
|
||||
.collectList()
|
||||
.map { it.toMutableList() }
|
||||
}
|
||||
|
||||
|
||||
fun getCategorySumsPipeline(dateFrom: LocalDate, dateTo: LocalDate): Mono<List<Document>> {
|
||||
val pipeline = listOf(
|
||||
Document(
|
||||
"\$lookup",
|
||||
Document("from", "categories")
|
||||
.append("localField", "category.\$id")
|
||||
.append("foreignField", "_id")
|
||||
.append("as", "categoryDetails")
|
||||
),
|
||||
Document("\$unwind", "\$categoryDetails"),
|
||||
Document(
|
||||
"\$match",
|
||||
Document(
|
||||
"date",
|
||||
Document(
|
||||
"\$gte", Date.from(
|
||||
LocalDateTime.of(dateFrom, LocalTime.MIN)
|
||||
.atZone(ZoneId.systemDefault())
|
||||
.withZoneSameInstant(ZoneOffset.UTC).toInstant()
|
||||
)
|
||||
)
|
||||
.append(
|
||||
"\$lte", LocalDateTime.of(dateTo, LocalTime.MIN)
|
||||
.atZone(ZoneId.systemDefault())
|
||||
.withZoneSameInstant(ZoneOffset.UTC).toInstant()
|
||||
)
|
||||
)
|
||||
),
|
||||
Document(
|
||||
"\$group",
|
||||
Document(
|
||||
"_id",
|
||||
Document("categoryId", "\$categoryDetails._id")
|
||||
.append("categoryName", "\$categoryDetails.name")
|
||||
.append("year", Document("\$year", "\$date"))
|
||||
.append("month", Document("\$month", "\$date"))
|
||||
)
|
||||
.append("totalAmount", Document("\$sum", "\$amount"))
|
||||
),
|
||||
Document(
|
||||
"\$group",
|
||||
Document("_id", "\$_id.categoryId")
|
||||
.append("categoryName", Document("\$first", "\$_id.categoryName"))
|
||||
.append(
|
||||
"monthlyData",
|
||||
Document(
|
||||
"\$push",
|
||||
Document(
|
||||
"month",
|
||||
Document(
|
||||
"\$concat", listOf(
|
||||
Document("\$toString", "\$_id.year"), "-",
|
||||
Document(
|
||||
"\$cond", listOf(
|
||||
Document("\$lt", listOf("\$_id.month", 10L)),
|
||||
Document(
|
||||
"\$concat", listOf(
|
||||
"0",
|
||||
Document("\$toString", "\$_id.month")
|
||||
)
|
||||
),
|
||||
Document("\$toString", "\$_id.month")
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
.append("totalAmount", "\$totalAmount")
|
||||
)
|
||||
)
|
||||
),
|
||||
Document(
|
||||
"\$addFields",
|
||||
Document(
|
||||
"completeMonthlyData",
|
||||
Document(
|
||||
"\$map",
|
||||
Document("input", Document("\$range", listOf(0L, 6L)))
|
||||
.append("as", "offset")
|
||||
.append(
|
||||
"in",
|
||||
Document(
|
||||
"month",
|
||||
Document(
|
||||
"\$dateToString",
|
||||
Document("format", "%Y-%m")
|
||||
.append(
|
||||
"date",
|
||||
Document(
|
||||
"\$dateAdd",
|
||||
Document("startDate", java.util.Date(1754006400000L))
|
||||
.append("unit", "month")
|
||||
.append(
|
||||
"amount",
|
||||
Document("\$multiply", listOf("\$\$offset", 1L))
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
.append(
|
||||
"totalAmount",
|
||||
Document(
|
||||
"\$let",
|
||||
Document(
|
||||
"vars",
|
||||
Document(
|
||||
"matched",
|
||||
Document(
|
||||
"\$arrayElemAt", listOf(
|
||||
Document(
|
||||
"\$filter",
|
||||
Document("input", "\$monthlyData")
|
||||
.append("as", "data")
|
||||
.append(
|
||||
"cond",
|
||||
Document(
|
||||
"\$eq", listOf(
|
||||
"\$\$data.month",
|
||||
Document(
|
||||
"\$dateToString",
|
||||
Document("format", "%Y-%m")
|
||||
.append(
|
||||
"date",
|
||||
Document(
|
||||
"\$dateAdd",
|
||||
Document(
|
||||
"startDate",
|
||||
java.util.Date(
|
||||
1733011200000L
|
||||
)
|
||||
)
|
||||
.append(
|
||||
"unit",
|
||||
"month"
|
||||
)
|
||||
.append(
|
||||
"amount",
|
||||
Document(
|
||||
"\$multiply",
|
||||
listOf(
|
||||
"\$\$offset",
|
||||
1L
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
), 0L
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
.append(
|
||||
"in",
|
||||
Document("\$ifNull", listOf("\$\$matched.totalAmount", 0L))
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
Document(
|
||||
"\$project",
|
||||
Document("_id", 0L)
|
||||
.append("categoryId", "\$_id")
|
||||
.append("categoryName", "\$categoryName")
|
||||
.append("monthlyData", "\$completeMonthlyData")
|
||||
)
|
||||
)
|
||||
|
||||
return mongoTemplate.getCollection("transactions")
|
||||
.flatMapMany { it.aggregate(pipeline) }
|
||||
.map {
|
||||
it["categoryId"] = it["categoryId"].toString()
|
||||
it
|
||||
}
|
||||
.collectList()
|
||||
}
|
||||
|
||||
fun getCategorySummaries(dateFrom: LocalDate): Mono<List<Document>> {
|
||||
val sixMonthsAgo = Date.from(
|
||||
LocalDateTime.of(dateFrom, LocalTime.MIN)
|
||||
.atZone(ZoneId.systemDefault())
|
||||
.withZoneSameInstant(ZoneOffset.UTC).toInstant()
|
||||
) // Пример даты, можно заменить на вычисляемую
|
||||
|
||||
val aggregation = listOf(
|
||||
// 1. Фильтр за последние 6 месяцев
|
||||
Document(
|
||||
"\$match",
|
||||
Document("date", Document("\$gte", sixMonthsAgo).append("\$lt", Date())).append("type.code", "INSTANT")
|
||||
),
|
||||
|
||||
// 2. Группируем по категории + (год, месяц)
|
||||
Document(
|
||||
"\$group", Document(
|
||||
"_id", Document("category", "\$category.\$id")
|
||||
.append("year", Document("\$year", "\$date"))
|
||||
.append("month", Document("\$month", "\$date"))
|
||||
)
|
||||
.append("totalAmount", Document("\$sum", "\$amount"))
|
||||
),
|
||||
|
||||
// 3. Подтягиваем информацию о категории
|
||||
Document(
|
||||
"\$lookup", Document("from", "categories")
|
||||
.append("localField", "_id.category")
|
||||
.append("foreignField", "_id")
|
||||
.append("as", "categoryInfo")
|
||||
),
|
||||
|
||||
// 4. Распаковываем массив категорий
|
||||
Document("\$unwind", "\$categoryInfo"),
|
||||
|
||||
// 5. Фильтруем по типу категории (EXPENSE)
|
||||
Document("\$match", Document("categoryInfo.type.code", "EXPENSE")),
|
||||
|
||||
// 6. Группируем обратно по категории, собирая все (год, месяц, total)
|
||||
Document(
|
||||
"\$group", Document("_id", "\$_id.category")
|
||||
.append("categoryName", Document("\$first", "\$categoryInfo.name"))
|
||||
.append("categoryType", Document("\$first", "\$categoryInfo.type.code"))
|
||||
.append("categoryIcon", Document("\$first", "\$categoryInfo.icon"))
|
||||
.append(
|
||||
"monthlySums", Document(
|
||||
"\$push", Document("year", "\$_id.year")
|
||||
.append("month", "\$_id.month")
|
||||
.append("total", "\$totalAmount")
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
// 7. Формируем единый массив из 6 элементов:
|
||||
// - каждый элемент = {year, month, total},
|
||||
// - если нет записей за месяц, ставим total=0
|
||||
Document(
|
||||
"\$project", Document("categoryName", 1)
|
||||
.append("categoryType", 1)
|
||||
.append("categoryIcon", 1)
|
||||
.append(
|
||||
"monthlySums", Document(
|
||||
"\$map", Document("input", Document("\$range", listOf(0, 6)))
|
||||
.append("as", "i")
|
||||
.append(
|
||||
"in", Document(
|
||||
"\$let", Document(
|
||||
"vars", Document(
|
||||
"subDate", Document(
|
||||
"\$dateSubtract", Document("startDate", Date())
|
||||
.append("unit", "month")
|
||||
.append("amount", "$\$i")
|
||||
)
|
||||
)
|
||||
)
|
||||
.append(
|
||||
"in", Document("year", Document("\$year", "$\$subDate"))
|
||||
.append("month", Document("\$month", "$\$subDate"))
|
||||
.append(
|
||||
"total", Document(
|
||||
"\$ifNull", listOf(
|
||||
Document(
|
||||
"\$getField", Document("field", "total")
|
||||
.append(
|
||||
"input", Document(
|
||||
"\$arrayElemAt", listOf(
|
||||
Document(
|
||||
"\$filter",
|
||||
Document(
|
||||
"input",
|
||||
"\$monthlySums"
|
||||
)
|
||||
.append("as", "ms")
|
||||
.append(
|
||||
"cond", Document(
|
||||
"\$and", listOf(
|
||||
Document(
|
||||
"\$eq",
|
||||
listOf(
|
||||
"$\$ms.year",
|
||||
Document(
|
||||
"\$year",
|
||||
"$\$subDate"
|
||||
)
|
||||
)
|
||||
),
|
||||
Document(
|
||||
"\$eq",
|
||||
listOf(
|
||||
"$\$ms.month",
|
||||
Document(
|
||||
"\$month",
|
||||
"$\$subDate"
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
), 0.0
|
||||
)
|
||||
)
|
||||
)
|
||||
), 0.0
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
// 8. Сортируем результат по имени категории
|
||||
Document("\$sort", Document("categoryName", 1))
|
||||
)
|
||||
|
||||
// Выполняем агрегацию
|
||||
return mongoTemplate.getCollection("transactions")
|
||||
.flatMapMany { it.aggregate(aggregation) }
|
||||
.map { document ->
|
||||
// Преобразуем _id в строку
|
||||
document["_id"] = document["_id"].toString()
|
||||
|
||||
// Получаем monthlySums и приводим к изменяемому списку
|
||||
val monthlySums = (document["monthlySums"] as? List<*>)?.map { monthlySum ->
|
||||
if (monthlySum is Document) {
|
||||
// Создаем копию Document, чтобы избежать изменений в исходном списке
|
||||
Document(monthlySum).apply {
|
||||
// Добавляем поле date
|
||||
val date = LocalDate.of(getInteger("year"), getInteger("month"), 1)
|
||||
this["date"] = date
|
||||
).flatMapMany { category ->
|
||||
Flux.fromIterable(transactions).flatMap { transaction ->
|
||||
transaction.category = category // Присваиваем конкретный объект категории
|
||||
financialService.editTransaction(transaction) // Сохраняем изменения
|
||||
}
|
||||
} else {
|
||||
monthlySum
|
||||
}
|
||||
}?.toMutableList()
|
||||
|
||||
// Сортируем monthlySums по полю date
|
||||
val sortedMonthlySums = monthlySums?.sortedBy { (it as? Document)?.get("date") as? LocalDate }
|
||||
|
||||
// Рассчитываем разницу между текущим и предыдущим месяцем
|
||||
var previousMonthSum = 0.0
|
||||
sortedMonthlySums?.forEach { monthlySum ->
|
||||
if (monthlySum is Document) {
|
||||
val currentMonthSum = monthlySum.getDouble("total") ?: 0.0
|
||||
|
||||
// Рассчитываем разницу в процентах
|
||||
val difference = if (previousMonthSum != 0.0 && currentMonthSum != 0.0) {
|
||||
(((currentMonthSum - previousMonthSum) / previousMonthSum) * 100).toInt()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
|
||||
// Добавляем поле difference
|
||||
monthlySum["difference"] = difference
|
||||
|
||||
// Обновляем previousMonthSum для следующей итерации
|
||||
previousMonthSum = currentMonthSum
|
||||
}
|
||||
}
|
||||
|
||||
// Обновляем документ с отсортированными и обновленными monthlySums
|
||||
document["monthlySums"] = sortedMonthlySums
|
||||
document
|
||||
}
|
||||
.collectList()
|
||||
.then(categoryRepo.deleteById(categoryId)) // Удаляем старую категорию
|
||||
.thenReturn(categoryId) // Возвращаем удалённую категорию
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -39,7 +39,6 @@ class FinancialService(
|
||||
val recurrentService: RecurrentService,
|
||||
val userService: UserService,
|
||||
val reactiveMongoTemplate: ReactiveMongoTemplate,
|
||||
private val spaceService: SpaceService,
|
||||
private val categoryRepo: CategoryRepo,
|
||||
val transactionsMapper: TransactionsMapper,
|
||||
val budgetMapper: BudgetMapper
|
||||
@@ -197,35 +196,10 @@ class FinancialService(
|
||||
Sort.by(it.order, it.by)
|
||||
} ?: 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 }
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
return findProjectedBudgets(ObjectId(spaceId), sort)
|
||||
}
|
||||
|
||||
|
||||
fun findProjectedBudgets(spaceId: ObjectId, sortRequested: Sort? = null): Mono<List<Budget>> {
|
||||
val lookupCategories = lookup("categories", "categories.category.\$id", "_id", "categoriesDetails")
|
||||
|
||||
@@ -242,7 +216,7 @@ class FinancialService(
|
||||
Sort.by(Direction.DESC, "date").and(Sort.by(Direction.DESC, "createdAt"))
|
||||
)
|
||||
val aggregation =
|
||||
newAggregation(lookupCategories, lookupIncomeCategories, lookupSpace, unwindSpace, matchStage, sort)
|
||||
newAggregation(lookupCategories, lookupIncomeCategories, lookupSpace, unwindSpace, matchStage, sort)
|
||||
|
||||
return reactiveMongoTemplate.aggregate(aggregation, "budgets", Document::class.java).collectList().map { docs ->
|
||||
docs.map { doc ->
|
||||
@@ -268,7 +242,7 @@ class FinancialService(
|
||||
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)
|
||||
val aggregation = newAggregation(lookupCategories, lookupIncomeCategories, lookupSpace, unwindSpace, matchStage)
|
||||
|
||||
return reactiveMongoTemplate.aggregate(aggregation, "budgets", Document::class.java).next().map { doc ->
|
||||
budgetMapper.fromDocument(doc)
|
||||
@@ -380,10 +354,10 @@ class FinancialService(
|
||||
}
|
||||
|
||||
|
||||
fun createBudget(spaceId: String, budget: Budget, createRecurrent: Boolean): Mono<Budget> {
|
||||
return Mono.zip(getBudgetByDate(budget.dateFrom, spaceId).map { Optional.ofNullable(it) }
|
||||
fun createBudget(space: Space, budget: Budget, createRecurrent: Boolean): Mono<Budget> {
|
||||
return Mono.zip(getBudgetByDate(budget.dateFrom, space.id!!).map { Optional.ofNullable(it) }
|
||||
.switchIfEmpty(Mono.just(Optional.empty())),
|
||||
getBudgetByDate(budget.dateTo, spaceId).map { Optional.ofNullable(it) }
|
||||
getBudgetByDate(budget.dateTo, space.id!!).map { Optional.ofNullable(it) }
|
||||
.switchIfEmpty(Mono.just(Optional.empty()))).flatMap { tuple ->
|
||||
val startBudget = tuple.t1.orElse(null)
|
||||
val endBudget = tuple.t2.orElse(null)
|
||||
@@ -394,65 +368,53 @@ class FinancialService(
|
||||
}
|
||||
|
||||
// Получаем Space по spaceId
|
||||
spaceService.getSpace(spaceId)
|
||||
.switchIfEmpty(Mono.error(IllegalArgumentException("Space not found for $spaceId"))).flatMap { space ->
|
||||
// Проверяем, входит ли пользователь в этот Space
|
||||
ReactiveSecurityContextHolder.getContext().flatMap { securityContext ->
|
||||
val username = securityContext.authentication.name
|
||||
userService.getByUsername(username)
|
||||
.switchIfEmpty(Mono.error(IllegalArgumentException("User not found for username: $username")))
|
||||
.flatMap { user ->
|
||||
if (space.users.none { it.id == user.id }) {
|
||||
return@flatMap Mono.error<Budget>(IllegalArgumentException("User does not have access to this space"))
|
||||
}
|
||||
|
||||
// Присваиваем Space бюджету
|
||||
budget.space = space
|
||||
|
||||
// Если createRecurrent=true, создаем рекуррентные транзакции
|
||||
val recurrentsCreation = if (createRecurrent) {
|
||||
recurrentService.createRecurrentsForBudget(space, budget)
|
||||
} else {
|
||||
Mono.empty()
|
||||
}
|
||||
// Присваиваем Space бюджету
|
||||
budget.space = space
|
||||
|
||||
// Создаем бюджет после возможного создания рекуррентных транзакций
|
||||
recurrentsCreation.then(
|
||||
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()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
// Если createRecurrent=true, создаем рекуррентные транзакции
|
||||
val recurrentsCreation = if (createRecurrent) {
|
||||
recurrentService.createRecurrentsForBudget(space, budget)
|
||||
} else {
|
||||
Mono.empty()
|
||||
}
|
||||
|
||||
// Создаем бюджет после возможного создания рекуррентных транзакций
|
||||
recurrentsCreation.then(
|
||||
getCategoryTransactionPipeline(
|
||||
space.id!!,
|
||||
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(
|
||||
space.id!!,
|
||||
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()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun getBudgetByDate(date: LocalDate, spaceId: String): Mono<Budget> {
|
||||
return budgetRepo.findByDateFromLessThanEqualAndDateToGreaterThanEqualAndSpace(date, date, ObjectId(spaceId))
|
||||
.switchIfEmpty(Mono.empty())
|
||||
@@ -665,82 +627,69 @@ class FinancialService(
|
||||
limit: Int? = null,
|
||||
offset: Int? = null,
|
||||
): Mono<MutableList<Transaction>> {
|
||||
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<MutableList<Transaction>>(IllegalArgumentException("User does not have access to this Space"))
|
||||
}
|
||||
|
||||
val matchCriteria = mutableListOf<Criteria>()
|
||||
val matchCriteria = mutableListOf<Criteria>()
|
||||
|
||||
// Добавляем фильтры
|
||||
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)) }
|
||||
// Добавляем фильтры
|
||||
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 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()))
|
||||
val match = match(Criteria().andOperator(*matchCriteria.toTypedArray()))
|
||||
|
||||
var sort =
|
||||
sort(Sort.by(Direction.DESC, "date").and(Sort.by(Direction.DESC, "createdAt")))
|
||||
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")))
|
||||
}
|
||||
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 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)
|
||||
val aggregation = newAggregation(aggregationBuilder)
|
||||
|
||||
return@flatMap reactiveMongoTemplate.aggregate(
|
||||
aggregation, "transactions", Document::class.java
|
||||
).collectList().map { docs ->
|
||||
return reactiveMongoTemplate.aggregate(
|
||||
aggregation, "transactions", Document::class.java
|
||||
).collectList().map { docs ->
|
||||
|
||||
val test = docs.map { doc ->
|
||||
transactionsMapper.fromDocument(doc)
|
||||
}.toMutableList()
|
||||
val test = docs.map { doc ->
|
||||
transactionsMapper.fromDocument(doc)
|
||||
}.toMutableList()
|
||||
|
||||
test
|
||||
}
|
||||
}
|
||||
}
|
||||
test
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun getTransactionByParentId(
|
||||
parentId: String
|
||||
): Mono<Transaction> {
|
||||
@@ -832,26 +781,22 @@ class FinancialService(
|
||||
}
|
||||
|
||||
|
||||
fun createTransaction(spaceId: String, transaction: Transaction): Mono<Transaction> {
|
||||
fun createTransaction(space: Space, transaction: Transaction): Mono<Transaction> {
|
||||
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<Transaction>(IllegalArgumentException("User does not have access to this Space"))
|
||||
}
|
||||
// Привязываем space и user к транзакции
|
||||
transaction.user = user
|
||||
transaction.space = 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<Transaction>(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 перед возвратом
|
||||
}
|
||||
}
|
||||
transactionsRepo.save(transaction).flatMap { savedTransaction ->
|
||||
updateBudgetOnCreate(savedTransaction).thenReturn(savedTransaction) // Ждём выполнения updateBudgetOnCreate перед возвратом
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1669,7 +1614,7 @@ class FinancialService(
|
||||
Document("\$unwind", "\$categoryInfo"),
|
||||
|
||||
// 5. Фильтруем по типу категории (EXPENSE)
|
||||
Document("\$match", Document("categoryInfo.type.code", "EXPENSE")),
|
||||
// Document("\$match", Document("categoryInfo.type.code", "EXPENSE")),
|
||||
|
||||
// 6. Группируем обратно по категории, собирая все (год, месяц, total)
|
||||
Document(
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package space.luminic.budgerapp.services
|
||||
|
||||
|
||||
|
||||
import org.bson.Document
|
||||
import org.bson.types.ObjectId
|
||||
import org.slf4j.LoggerFactory
|
||||
@@ -16,11 +15,11 @@ 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.mappers.RecurrentMapper
|
||||
import space.luminic.budgerapp.models.*
|
||||
import space.luminic.budgerapp.repos.RecurrentRepo
|
||||
import space.luminic.budgerapp.repos.TransactionRepo
|
||||
import java.time.YearMonth
|
||||
import java.time.ZoneId
|
||||
|
||||
|
||||
@Service
|
||||
@@ -29,47 +28,30 @@ class RecurrentService(
|
||||
private val recurrentRepo: RecurrentRepo,
|
||||
private val transactionRepo: TransactionRepo,
|
||||
private val userService: UserService,
|
||||
private val spaceService: SpaceService,
|
||||
private val recurrentMapper: RecurrentMapper,
|
||||
) {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(javaClass)
|
||||
|
||||
|
||||
fun getRecurrents(space: Space): Mono<List<Recurrent>> {
|
||||
fun getRecurrents(spaceId: String): Mono<List<Recurrent>> {
|
||||
val lookupCategories = lookup("categories", "category.\$id", "_id", "categoryDetails")
|
||||
val unwindCategory = unwind("categoryDetails")
|
||||
|
||||
|
||||
val lookupSpace = lookup("spaces", "space.\$id", "_id", "spaceDetails")
|
||||
val unwindSpace = unwind("spaceDetails")
|
||||
val matchStage = match(Criteria.where("spaceDetails._id").`is`(ObjectId(space.id)))
|
||||
val matchStage = match(Criteria.where("spaceDetails._id").`is`(ObjectId(spaceId)))
|
||||
|
||||
val sort =sort(Sort.by(Direction.ASC, "atDay"))
|
||||
val sort = sort(Sort.by(Direction.ASC, "atDay"))
|
||||
val aggregation =
|
||||
newAggregation(lookupCategories, unwindCategory,lookupSpace, unwindSpace, matchStage, sort)
|
||||
newAggregation(lookupCategories, unwindCategory, lookupSpace, unwindSpace, matchStage, sort)
|
||||
// Запрос рекуррентных платежей
|
||||
return reactiveMongoTemplate.aggregate(aggregation, "recurrents", Document::class.java).collectList().map { docs ->
|
||||
docs.map { doc ->
|
||||
val categoryDoc = doc.get("categoryDetails", Document::class.java)
|
||||
val categoryTypeDoc = categoryDoc.get("type", Document::class.java)
|
||||
Recurrent(
|
||||
id = doc.getObjectId("_id").toString(),
|
||||
space = space,
|
||||
atDay = doc.getInteger("atDay"),
|
||||
category = Category(
|
||||
id = categoryDoc.getObjectId("_id").toString(),
|
||||
space = space,
|
||||
type = CategoryType(categoryTypeDoc.getString("code"), categoryTypeDoc.getString("name")),
|
||||
name = categoryDoc.getString("name"),
|
||||
description = categoryDoc.getString("description"),
|
||||
icon = categoryDoc.getString("icon"),
|
||||
),
|
||||
name = doc.getString("name"),
|
||||
description = doc.getString("description"),
|
||||
amount = doc.getInteger("amount"),
|
||||
createdAt = doc.getDate("createdAt"),
|
||||
)
|
||||
}.toList()
|
||||
}
|
||||
return reactiveMongoTemplate.aggregate(aggregation, "recurrents", Document::class.java).collectList()
|
||||
.map { docs ->
|
||||
docs.map { doc ->
|
||||
recurrentMapper.fromDocument(doc)
|
||||
}.toList()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -107,7 +89,7 @@ class RecurrentService(
|
||||
.switchIfEmpty(Mono.error(IllegalArgumentException("User not found for username: $username")))
|
||||
}
|
||||
.flatMapMany { user ->
|
||||
getRecurrents(space) // Теперь это Mono<List<Recurrent>>
|
||||
getRecurrents(space.id!!) // Теперь это Mono<List<Recurrent>>
|
||||
.flatMapMany { Flux.fromIterable(it) } // Преобразуем List<Recurrent> в Flux<Recurrent>
|
||||
.map { recurrent ->
|
||||
// Определяем дату транзакции
|
||||
@@ -115,9 +97,11 @@ class RecurrentService(
|
||||
recurrent.atDay in budget.dateFrom.dayOfMonth..daysInCurrentMonth -> {
|
||||
currentYearMonth.atDay(recurrent.atDay)
|
||||
}
|
||||
|
||||
recurrent.atDay < budget.dateFrom.dayOfMonth -> {
|
||||
currentYearMonth.atDay(recurrent.atDay).plusMonths(1)
|
||||
}
|
||||
|
||||
else -> {
|
||||
val extraDays = recurrent.atDay - daysInCurrentMonth
|
||||
currentYearMonth.plusMonths(1).atDay(extraDays)
|
||||
@@ -154,17 +138,7 @@ class RecurrentService(
|
||||
return recurrentRepo.deleteById(id)
|
||||
}
|
||||
|
||||
fun regenRecurrents(): Mono<List<Recurrent>> {
|
||||
return recurrentRepo.findAll()
|
||||
.flatMap { recurrent ->
|
||||
spaceService.getSpace("67af3c0f652da946a7dd9931")
|
||||
.flatMap { space ->
|
||||
recurrent.space = space
|
||||
recurrentRepo.save(recurrent) // Сохраняем и возвращаем сохраненный объект
|
||||
}
|
||||
}
|
||||
.collectList() // Собираем результаты в список
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -1,14 +1,19 @@
|
||||
package space.luminic.budgerapp.services
|
||||
|
||||
import org.bson.Document
|
||||
import org.bson.types.ObjectId
|
||||
import org.springframework.data.mongodb.core.ReactiveMongoTemplate
|
||||
import org.springframework.data.mongodb.core.aggregation.Aggregation.*
|
||||
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.Category
|
||||
import space.luminic.budgerapp.models.Space
|
||||
import space.luminic.budgerapp.models.SpaceInvite
|
||||
import space.luminic.budgerapp.models.Tag
|
||||
import space.luminic.budgerapp.repos.*
|
||||
import java.time.LocalDateTime
|
||||
import java.util.UUID
|
||||
@@ -22,7 +27,11 @@ class SpaceService(
|
||||
private val reactiveMongoTemplate: ReactiveMongoTemplate,
|
||||
private val categoryRepo: CategoryRepo,
|
||||
private val recurrentRepo: RecurrentRepo,
|
||||
private val transactionRepo: TransactionRepo
|
||||
private val transactionRepo: TransactionRepo,
|
||||
private val financialService: FinancialService,
|
||||
private val categoryService: CategoryService,
|
||||
private val recurrentService: RecurrentService,
|
||||
private val tagRepo: TagRepo
|
||||
) {
|
||||
|
||||
fun isValidRequest(spaceId: String): Mono<Space> {
|
||||
@@ -101,27 +110,29 @@ class SpaceService(
|
||||
val objectId = ObjectId(space.id)
|
||||
|
||||
return Mono.`when`(
|
||||
budgetRepo.findBySpaceId(objectId)
|
||||
.flatMap { budgetRepo.delete(it) }
|
||||
financialService.findProjectedBudgets(objectId)
|
||||
.flatMapMany { Flux.fromIterable(it) }
|
||||
.flatMap { budgetRepo.deleteById(it.id!!) }
|
||||
.then(),
|
||||
|
||||
transactionRepo.findBySpaceId(objectId)
|
||||
.flatMap { transactionRepo.delete(it) }
|
||||
.then(),
|
||||
financialService.getTransactions(objectId.toString())
|
||||
.flatMapMany { Flux.fromIterable(it) }
|
||||
.flatMap { transactionRepo.deleteById(it.id!!) }
|
||||
.then(),
|
||||
|
||||
categoryRepo.findBySpaceId(objectId)
|
||||
.flatMap { categoryRepo.delete(it) }
|
||||
.then(),
|
||||
categoryService.getCategories(objectId.toString(), null, "name", "ASC")
|
||||
.flatMapMany { Flux.fromIterable(it) }
|
||||
.flatMap { categoryRepo.deleteById(it.id!!) }
|
||||
.then(),
|
||||
|
||||
recurrentRepo.findRecurrentsBySpaceId(objectId)
|
||||
.flatMap { recurrentRepo.delete(it) }
|
||||
.then()
|
||||
recurrentService.getRecurrents(objectId.toString())
|
||||
.flatMapMany { Flux.fromIterable(it) }
|
||||
.flatMap { recurrentRepo.deleteById(it.id!!) }
|
||||
.then()
|
||||
).then(spaceRepo.deleteById(space.id!!)) // Исправлено: удаление по ID
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
fun createInviteSpace(spaceId: String): Mono<SpaceInvite> {
|
||||
return ReactiveSecurityContextHolder.getContext()
|
||||
.map { it.authentication }
|
||||
@@ -247,6 +258,106 @@ class SpaceService(
|
||||
}
|
||||
}
|
||||
|
||||
fun findTag(space: Space, tagCode: String): Mono<Tag> {
|
||||
val lookupSpaces = lookup("spaces", "space.\$id", "_id", "spaceDetails")
|
||||
val unwindSpace = unwind("spaceDetails")
|
||||
val matchCriteria = mutableListOf<Criteria>()
|
||||
// Добавляем фильтры
|
||||
matchCriteria.add(Criteria.where("spaceDetails._id").`is`(ObjectId(space.id)))
|
||||
matchCriteria.add(Criteria.where("code").`is`(tagCode))
|
||||
val match = match(Criteria().andOperator(*matchCriteria.toTypedArray()))
|
||||
val aggregationBuilder = mutableListOf(
|
||||
lookupSpaces,
|
||||
unwindSpace,
|
||||
match.takeIf { matchCriteria.isNotEmpty() },
|
||||
).filterNotNull()
|
||||
|
||||
val aggregation = newAggregation(aggregationBuilder)
|
||||
return reactiveMongoTemplate.aggregate(
|
||||
aggregation, "tags", Document::class.java
|
||||
).next()
|
||||
.map { doc ->
|
||||
Tag(
|
||||
id = doc.getObjectId("_id").toString(),
|
||||
space = Space(id = doc.get("spaceDetails", Document::class.java).getObjectId("_id").toString()),
|
||||
code = doc.getString("code"),
|
||||
name = doc.getString("name")
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun createTag(space: Space, tag: Tag): Mono<Tag> {
|
||||
tag.space = space
|
||||
return findTag(space, tag.code)
|
||||
.flatMap { existingTag ->
|
||||
Mono.error<Tag>(IllegalArgumentException("Tag with code ${existingTag.code} already exists"))
|
||||
}
|
||||
.switchIfEmpty(tagRepo.save(tag))
|
||||
}
|
||||
|
||||
fun deleteTag(space: Space, tagCode: String): Mono<Void> {
|
||||
return findTag(space, tagCode)
|
||||
.switchIfEmpty(Mono.error(IllegalArgumentException("Tag with code $tagCode not found")))
|
||||
.flatMap { tag ->
|
||||
categoryService.getCategories(space.id!!, sortBy = "name", direction = "ASC", tagCode = tag.code)
|
||||
.flatMapMany { cats ->
|
||||
Flux.fromIterable(cats)
|
||||
.map { cat ->
|
||||
cat.tags.removeIf { it.code == tagCode } // Изменяем список тегов
|
||||
cat
|
||||
}
|
||||
.flatMap { categoryRepo.save(it) } // Сохраняем обновлённые категории
|
||||
}
|
||||
.then(tagRepo.deleteById(tag.id!!)) // Удаляем тег только после обновления категорий
|
||||
}
|
||||
}
|
||||
|
||||
fun getTags(space: Space): Mono<List<Tag>> {
|
||||
val lookupSpaces = lookup("spaces", "space.\$id", "_id", "spaceDetails")
|
||||
val unwindSpace = unwind("spaceDetails")
|
||||
val matchCriteria = mutableListOf<Criteria>()
|
||||
// Добавляем фильтры
|
||||
matchCriteria.add(Criteria.where("spaceDetails._id").`is`(ObjectId(space.id)))
|
||||
val match = match(Criteria().andOperator(*matchCriteria.toTypedArray()))
|
||||
val aggregationBuilder = mutableListOf(
|
||||
|
||||
lookupSpaces,
|
||||
unwindSpace,
|
||||
match.takeIf { matchCriteria.isNotEmpty() },
|
||||
).filterNotNull()
|
||||
|
||||
val aggregation = newAggregation(aggregationBuilder)
|
||||
return reactiveMongoTemplate.aggregate(
|
||||
aggregation, "tags", Document::class.java
|
||||
)
|
||||
.collectList() // Преобразуем Flux<Transaction> в Mono<List<Transaction>>
|
||||
.map { docs ->
|
||||
docs.map { doc ->
|
||||
Tag(
|
||||
id = doc.getObjectId("_id").toString(),
|
||||
space = Space(id = doc.get("spaceDetails", Document::class.java).getObjectId("_id").toString()),
|
||||
code = doc.getString("code"),
|
||||
name = doc.getString("name")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun regenSpaceCategory(): Mono<Category> {
|
||||
return getSpace("67af3c0f652da946a7dd9931")
|
||||
.flatMap { space ->
|
||||
categoryService.findCategory(id= "677bc767c7857460a491bd4f")
|
||||
.flatMap { category -> // заменил map на flatMap
|
||||
category.space = space
|
||||
category.name = "Сбережения"
|
||||
category.description = "Отчисления в накопления или инвестиционные счета"
|
||||
category.icon = "💰"
|
||||
categoryRepo.save(category) // теперь возвращаем Mono<Category>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fun regenSpaces(): Mono<List<Space>> {
|
||||
// return spaceRepo.findAll()
|
||||
// .flatMap { space ->
|
||||
|
||||
Reference in New Issue
Block a user