+ categories sort + analytics
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package space.luminic.budgerapp.services
|
||||
|
||||
|
||||
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.cache.annotation.CacheEvict
|
||||
import org.springframework.cache.annotation.Cacheable
|
||||
@@ -59,27 +60,32 @@ class BudgetService(
|
||||
TransactionEventType.DELETE -> updateBudgetOnDelete(event)
|
||||
}
|
||||
}
|
||||
|
||||
// runBlocking(Dispatchers.IO) {
|
||||
// updateBudgetWarns(
|
||||
// budget = budgetRepo.findByDateFromLessThanEqualAndDateToGreaterThan(
|
||||
// event.newTransaction.date.toLocalDate(), event.newTransaction.date.toLocalDate()
|
||||
// )
|
||||
// budgetRepo.findByDateFromLessThanEqualAndDateToGreaterThan(
|
||||
// event.newTransaction.date, event.newTransaction.date.
|
||||
// ).map{it}
|
||||
// )
|
||||
// }
|
||||
}
|
||||
|
||||
fun updateBudgetOnCreate(event: TransactionEvent) {
|
||||
budgetRepo.findByDateFromLessThanEqualAndDateToGreaterThan(
|
||||
budgetRepo.findByDateFromLessThanEqualAndDateToGreaterThanEqual(
|
||||
event.newTransaction.date, event.newTransaction.date
|
||||
).flatMap { budget ->
|
||||
categoryService.getBudgetCategories(budget.dateFrom, budget.dateTo).flatMap { categories ->
|
||||
val categories = categoryService.getBudgetCategories(budget.dateFrom, budget.dateTo)
|
||||
logger.info(categories.toString())
|
||||
categories.flatMap { categories ->
|
||||
val updatedCategories = when (event.newTransaction.type.code) {
|
||||
"PLANNED" -> Flux.fromIterable(budget.categories)
|
||||
.map { category ->
|
||||
categories[category.category.id]?.let { data ->
|
||||
category.currentSpent = data["instantAmount"] ?: 0.0
|
||||
category.currentPlanned = data["plannedAmount"] ?: 0.0
|
||||
category.currentLimit += event.newTransaction.amount
|
||||
if (category.category.id == event.newTransaction.category.id) {
|
||||
category.currentSpent = data["instantAmount"] ?: 0.0
|
||||
category.currentPlanned = data["plannedAmount"] ?: 0.0
|
||||
category.currentLimit += event.newTransaction.amount
|
||||
}
|
||||
}
|
||||
category
|
||||
}.collectList()
|
||||
@@ -87,8 +93,10 @@ class BudgetService(
|
||||
"INSTANT" -> Flux.fromIterable(budget.categories)
|
||||
.map { category ->
|
||||
categories[category.category.id]?.let { data ->
|
||||
category.currentSpent = data["instantAmount"] ?: 0.0
|
||||
category.currentPlanned = data["plannedAmount"] ?: 0.0
|
||||
if (category.category.id == event.newTransaction.category.id) {
|
||||
category.currentSpent = data["instantAmount"] ?: 0.0
|
||||
category.currentPlanned = data["plannedAmount"] ?: 0.0
|
||||
}
|
||||
}
|
||||
category
|
||||
}.collectList()
|
||||
@@ -107,13 +115,13 @@ class BudgetService(
|
||||
|
||||
|
||||
fun updateBudgetOnEdit(event: TransactionEvent) {
|
||||
budgetRepo.findByDateFromLessThanEqualAndDateToGreaterThan(
|
||||
budgetRepo.findByDateFromLessThanEqualAndDateToGreaterThanEqual(
|
||||
event.oldTransaction.date, event.oldTransaction.date
|
||||
).switchIfEmpty(
|
||||
Mono.error(BudgetNotFoundException("old budget cannot be null"))
|
||||
).then().subscribe()
|
||||
|
||||
budgetRepo.findByDateFromLessThanEqualAndDateToGreaterThan(
|
||||
budgetRepo.findByDateFromLessThanEqualAndDateToGreaterThanEqual(
|
||||
event.newTransaction.date, event.newTransaction.date
|
||||
).flatMap { budget ->
|
||||
categoryService.getBudgetCategories(budget.dateFrom, budget.dateTo).flatMap { categories ->
|
||||
@@ -134,8 +142,10 @@ class BudgetService(
|
||||
"INSTANT" -> Flux.fromIterable(budget.categories)
|
||||
.map { category ->
|
||||
categories[category.category.id]?.let { data ->
|
||||
category.currentSpent = data["instantAmount"] ?: 0.0
|
||||
category.currentPlanned = data["plannedAmount"] ?: 0.0
|
||||
if (category.category.id == event.newTransaction.category.id) {
|
||||
category.currentSpent = data["instantAmount"] ?: 0.0
|
||||
category.currentPlanned = data["plannedAmount"] ?: 0.0
|
||||
}
|
||||
}
|
||||
category
|
||||
}.collectList()
|
||||
@@ -154,7 +164,7 @@ class BudgetService(
|
||||
|
||||
|
||||
fun updateBudgetOnDelete(event: TransactionEvent) {
|
||||
budgetRepo.findByDateFromLessThanEqualAndDateToGreaterThan(
|
||||
budgetRepo.findByDateFromLessThanEqualAndDateToGreaterThanEqual(
|
||||
event.newTransaction.date, event.newTransaction.date
|
||||
).flatMap { budget ->
|
||||
categoryService.getBudgetCategories(budget.dateFrom, budget.dateTo).flatMap { categories ->
|
||||
@@ -162,9 +172,11 @@ class BudgetService(
|
||||
"PLANNED" -> Flux.fromIterable(budget.categories)
|
||||
.map { category ->
|
||||
categories[category.category.id]?.let { data ->
|
||||
category.currentSpent = data["instantAmount"] ?: 0.0
|
||||
category.currentPlanned = data["plannedAmount"] ?: 0.0
|
||||
category.currentLimit += event.newTransaction.amount
|
||||
if (category.category.id == event.newTransaction.category.id) {
|
||||
category.currentSpent = data["instantAmount"] ?: 0.0
|
||||
category.currentPlanned = data["plannedAmount"] ?: 0.0
|
||||
category.currentLimit += event.newTransaction.amount
|
||||
}
|
||||
}
|
||||
category
|
||||
}.collectList()
|
||||
@@ -172,8 +184,10 @@ class BudgetService(
|
||||
"INSTANT" -> Flux.fromIterable(budget.categories)
|
||||
.map { category ->
|
||||
categories[category.category.id]?.let { data ->
|
||||
category.currentSpent = data["instantAmount"] ?: 0.0
|
||||
category.currentPlanned = data["plannedAmount"] ?: 0.0
|
||||
if (category.category.id == event.newTransaction.category.id) {
|
||||
category.currentSpent = data["instantAmount"] ?: 0.0
|
||||
category.currentPlanned = data["plannedAmount"] ?: 0.0
|
||||
}
|
||||
}
|
||||
category
|
||||
}.collectList()
|
||||
@@ -206,6 +220,7 @@ class BudgetService(
|
||||
|
||||
// @Cacheable("budgets", key = "#id")
|
||||
fun getBudget(id: String): Mono<BudgetDTO> {
|
||||
logger.info("here b")
|
||||
return budgetRepo.findById(id)
|
||||
.flatMap { budget ->
|
||||
val budgetDTO = BudgetDTO(
|
||||
@@ -243,6 +258,7 @@ class BudgetService(
|
||||
budgetDTO.plannedExpenses = transactions["plannedExpenses"] as MutableList
|
||||
budgetDTO.plannedIncomes = transactions["plannedIncomes"] as MutableList
|
||||
budgetDTO.transactions = transactions["instantTransactions"] as MutableList
|
||||
logger.info("here e")
|
||||
|
||||
budgetDTO
|
||||
}
|
||||
@@ -301,7 +317,7 @@ class BudgetService(
|
||||
|
||||
|
||||
fun getBudgetByDate(date: LocalDate): Mono<Budget> {
|
||||
return budgetRepo.findByDateFromLessThanEqualAndDateToGreaterThan(date, date).switchIfEmpty(Mono.empty())
|
||||
return budgetRepo.findByDateFromLessThanEqualAndDateToGreaterThanEqual(date, date).switchIfEmpty(Mono.empty())
|
||||
}
|
||||
|
||||
// fun getBudgetCategorySQL(id: Int): List<BudgetCategory>? {
|
||||
@@ -360,7 +376,7 @@ class BudgetService(
|
||||
@CacheEvict(cacheNames = ["budgets", "budgetsList"], allEntries = true)
|
||||
fun setCategoryLimit(budgetId: String, catId: String, limit: Double): Mono<BudgetCategory> {
|
||||
return budgetRepo.findById(budgetId).flatMap { budget ->
|
||||
val catEdit = budget.categories.firstOrNull { it.category.id == catId }
|
||||
val catEdit = budget.categories.firstOrNull { it.category?.id == catId }
|
||||
?: return@flatMap Mono.error<BudgetCategory>(Exception("Category not found in the budget"))
|
||||
|
||||
transactionService.calcTransactionsSum(budget, catId, "PLANNED").flatMap { catPlanned ->
|
||||
@@ -378,9 +394,6 @@ class BudgetService(
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
fun getWarns(budgetId: String, isHide: Boolean? = null): Mono<List<Warn>> {
|
||||
return warnRepo.findAllByBudgetIdAndIsHide(budgetId, isHide == true).collectList()
|
||||
}
|
||||
@@ -469,10 +482,10 @@ class BudgetService(
|
||||
): Flux<Warn> {
|
||||
val warnsForCategory = mutableListOf<Mono<Warn?>>()
|
||||
|
||||
val averageSum = averageSums[category.category.id] ?: 0.0
|
||||
val averageSum = averageSums[category.category?.id] ?: 0.0
|
||||
val categorySpentRatioInAvgIncome = if (averageIncome > 0.0) averageSum / averageIncome else 0.0
|
||||
val projectedAvailableSum = currentBudgetIncome * categorySpentRatioInAvgIncome
|
||||
val contextAtAvg = "category${category.category.id}atbudget${finalBudget.id}lessavg"
|
||||
val contextAtAvg = "category${category.category?.id}atbudget${finalBudget.id}lessavg"
|
||||
val lowSavingContext = "savingValueLess10atBudget${finalBudget.id}"
|
||||
|
||||
if (averageSum > category.currentLimit) {
|
||||
@@ -482,11 +495,11 @@ class BudgetService(
|
||||
Warn(
|
||||
serenity = WarnSerenity.MAIN,
|
||||
message = PushMessage(
|
||||
title = "Внимание на ${category.category.name}!",
|
||||
title = "Внимание на ${category.category?.name}!",
|
||||
body = "Лимит меньше средних трат (Среднее: <b>${averageSum.toInt()} ₽</b> Текущий лимит: <b>${category.currentLimit.toInt()} ₽</b>)." +
|
||||
"\nСредняя доля данной категории в доходах: <b>${(categorySpentRatioInAvgIncome * 100).toInt()}%</b>." +
|
||||
"\nПроецируется на текущие поступления: <b>${projectedAvailableSum.toInt()} ₽</b>",
|
||||
icon = category.category.icon
|
||||
icon = category.category?.icon
|
||||
),
|
||||
budgetId = finalBudget.id!!,
|
||||
context = contextAtAvg,
|
||||
@@ -499,7 +512,7 @@ class BudgetService(
|
||||
warnRepo.findWarnByContext(contextAtAvg).flatMap { warnRepo.delete(it).then(Mono.empty<Warn>()) }
|
||||
}
|
||||
|
||||
if (category.category.id == "675850148198643f121e466a") {
|
||||
if (category.category?.id == "675850148198643f121e466a") {
|
||||
val savingRatio = if (plannedIncome > 0.0) category.currentLimit / plannedIncome else 0.0
|
||||
if (savingRatio < 0.1) {
|
||||
val warnMono = warnRepo.findWarnByContext(lowSavingContext)
|
||||
@@ -512,7 +525,7 @@ class BudgetService(
|
||||
body = "Текущие плановые сбережения равны ${plannedSaving.toInt()} (${
|
||||
(savingRatio * 100).toInt()
|
||||
}%)! Исправьте!",
|
||||
icon = category.category.icon
|
||||
icon = category.category!!.icon
|
||||
),
|
||||
budgetId = finalBudget.id!!,
|
||||
context = lowSavingContext,
|
||||
|
||||
@@ -5,6 +5,8 @@ import org.bson.Document
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.cache.annotation.CacheEvict
|
||||
import org.springframework.cache.annotation.Cacheable
|
||||
import org.springframework.data.domain.Sort
|
||||
import org.springframework.data.domain.Sort.Direction
|
||||
import org.springframework.data.mongodb.core.ReactiveMongoTemplate
|
||||
import org.springframework.stereotype.Service
|
||||
import reactor.core.publisher.Flux
|
||||
@@ -30,9 +32,16 @@ class CategoryService(
|
||||
private val logger = LoggerFactory.getLogger(javaClass)
|
||||
|
||||
|
||||
fun getCategory(id: String): Mono<Category> {
|
||||
return categoryRepo.findById(id)
|
||||
}
|
||||
|
||||
@Cacheable("getAllCategories")
|
||||
fun getCategories(): Mono<List<Category>> {
|
||||
return categoryRepo.findAll().collectList()
|
||||
fun getCategories(sortBy: String, direction: String): Mono<List<Category>> {
|
||||
return categoryRepo.findAll(Sort.by(if (direction == "ASC") Direction.ASC else Direction.DESC, sortBy))
|
||||
.collectList()
|
||||
|
||||
|
||||
}
|
||||
|
||||
@Cacheable("categoryTypes")
|
||||
@@ -43,12 +52,12 @@ class CategoryService(
|
||||
return types
|
||||
}
|
||||
|
||||
@CacheEvict(cacheNames = ["getAllCategories"],allEntries = true)
|
||||
@CacheEvict(cacheNames = ["getAllCategories"], allEntries = true)
|
||||
fun createCategory(category: Category): Mono<Category> {
|
||||
return categoryRepo.save(category)
|
||||
}
|
||||
|
||||
@CacheEvict(cacheNames = ["getAllCategories"],allEntries = true)
|
||||
@CacheEvict(cacheNames = ["getAllCategories"], allEntries = true)
|
||||
fun editCategory(category: Category): Mono<Category> {
|
||||
return categoryRepo.findById(category.id!!) // Возвращаем Mono<Category>
|
||||
.flatMap { oldCategory ->
|
||||
@@ -59,7 +68,7 @@ class CategoryService(
|
||||
}
|
||||
}
|
||||
|
||||
@CacheEvict(cacheNames = ["getAllCategories"],allEntries = true)
|
||||
@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"))
|
||||
@@ -75,9 +84,9 @@ class CategoryService(
|
||||
icon = "🚮"
|
||||
)
|
||||
)
|
||||
).flatMapMany { newCategory ->
|
||||
).flatMapMany { Category ->
|
||||
Flux.fromIterable(transactions).flatMap { transaction ->
|
||||
transaction.category = newCategory // Присваиваем конкретный объект категории
|
||||
transaction.category = Category // Присваиваем конкретный объект категории
|
||||
transactionService.editTransaction(transaction) // Сохраняем изменения
|
||||
}
|
||||
}
|
||||
@@ -118,7 +127,7 @@ class CategoryService(
|
||||
)
|
||||
),
|
||||
Document(
|
||||
"\$lt", listOf(
|
||||
"\$lte", listOf(
|
||||
"\$date",
|
||||
Date.from(
|
||||
LocalDateTime.of(dateTo, LocalTime.MIN)
|
||||
@@ -339,7 +348,7 @@ class CategoryService(
|
||||
currentPlanned = document["plannedAmount"] as Double,
|
||||
category = Category(
|
||||
document["_id"].toString(),
|
||||
CategoryType(catType["code"] as String, catType["name"] as String),
|
||||
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
|
||||
@@ -351,4 +360,190 @@ class CategoryService(
|
||||
}
|
||||
|
||||
|
||||
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(dateTo, LocalTime.MIN)
|
||||
.atZone(ZoneId.systemDefault())
|
||||
.withZoneSameInstant(ZoneOffset.UTC).toInstant()
|
||||
)
|
||||
)
|
||||
.append(
|
||||
"\$lte", LocalDateTime.of(dateTo, LocalTime.MIN)
|
||||
.atZone(ZoneId.systemDefault())
|
||||
.withZoneSameInstant(ZoneOffset.UTC).toInstant()
|
||||
)
|
||||
)
|
||||
),
|
||||
Document(
|
||||
"\$group",
|
||||
Document(
|
||||
"_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()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user