fix recurrents

This commit is contained in:
xds
2025-02-21 01:44:03 +03:00
parent bf2dfca1cc
commit 363d926443
17 changed files with 618 additions and 1034 deletions

View File

@@ -26,7 +26,7 @@ class BearerTokenFilter(private val authService: AuthService) : SecurityContextS
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
val token = exchange.request.headers.getFirst(HttpHeaders.AUTHORIZATION)?.removePrefix("Bearer ")
if (exchange.request.path.value() in listOf("/api/auth/login","/api/auth/register") || exchange.request.path.value()
if (exchange.request.path.value() in listOf("/api/auth/login","/api/auth/register", "/api/auth/tgLogin") || exchange.request.path.value()
.startsWith("/api/actuator")
) {
return chain.filter(exchange)

View File

@@ -25,7 +25,7 @@ class SecurityConfig(
.logout { it.disable() }
.authorizeExchange {
it.pathMatchers(HttpMethod.POST, "/auth/login", "/auth/register").permitAll()
it.pathMatchers(HttpMethod.POST, "/auth/login", "/auth/register", "/auth/tgLogin").permitAll()
it.pathMatchers("/actuator/**").permitAll()
it.anyExchange().authenticated()
}

View File

@@ -27,6 +27,11 @@ class AuthController(
return authService.register(request.username, request.password, request.firstName)
}
@PostMapping("/tgLogin")
fun tgLogin(@RequestHeader("X-Tg-Id") tgId: String): Mono<Map<String, String>> {
return authService.tgLogin(tgId).map { token -> mapOf("token" to token) }
}
@GetMapping("/me")
fun getMe(@RequestHeader("Authorization") token: String): Mono<User> {

View File

@@ -87,7 +87,9 @@ class SpaceController(
//
@GetMapping("/{spaceId}/budgets")
fun getBudgets(@PathVariable spaceId: String): Mono<List<Budget>> {
return financialService.getBudgets(spaceId)
return spaceService.isValidRequest(spaceId).flatMap {
financialService.getBudgets(spaceId)
}
}
@GetMapping("/{spaceId}/budgets/{id}")
@@ -104,7 +106,7 @@ class SpaceController(
@RequestBody budgetCreationDTO: BudgetCreationDTO,
): Mono<Budget> {
return spaceService.isValidRequest(spaceId).flatMap {
financialService.createBudget(spaceId, budgetCreationDTO.budget, budgetCreationDTO.createRecurrent)
financialService.createBudget(it, budgetCreationDTO.budget, budgetCreationDTO.createRecurrent)
}
}
@@ -124,7 +126,7 @@ class SpaceController(
@RequestBody limit: LimitValue,
): Mono<BudgetCategory> {
return spaceService.isValidRequest(spaceId).flatMap {
financialService.setCategoryLimit(it.id!!,budgetId, catId, limit.limit)
financialService.setCategoryLimit(it.id!!, budgetId, catId, limit.limit)
}
}
@@ -177,7 +179,7 @@ class SpaceController(
@PostMapping("/{spaceId}/transactions")
fun createTransaction(@PathVariable spaceId: String, @RequestBody transaction: Transaction): Mono<Transaction> {
return spaceService.isValidRequest(spaceId).flatMap {
financialService.createTransaction(spaceId, transaction)
financialService.createTransaction(it, transaction)
}
}
@@ -219,7 +221,7 @@ class SpaceController(
}
@GetMapping("/{spaceId}/categories/types")
fun getCategoriesTypes(): ResponseEntity<Any> {
fun getCategoriesTypes(@PathVariable spaceId: String): ResponseEntity<Any> {
return try {
ResponseEntity.ok(categoryService.getCategoryTypes())
} catch (e: Exception) {
@@ -238,15 +240,43 @@ class SpaceController(
}
@PutMapping("/{spaceId}/{categoryId}")
fun editCategory(@PathVariable categoryId: String, @RequestBody category: Category): Mono<Category> {
return categoryService.editCategory(category)
@PutMapping("/{spaceId}/categories/{categoryId}")
fun editCategory(
@PathVariable categoryId: String,
@RequestBody category: Category,
@PathVariable spaceId: String
): Mono<Category> {
return spaceService.isValidRequest(spaceId).flatMap {
categoryService.editCategory(it, category)
}
}
@DeleteMapping("/{spaceId}/categories/{categoryId}")
fun deleteCategory(@PathVariable categoryId: String, @PathVariable spaceId: String): Mono<String> {
return spaceService.isValidRequest(spaceId).flatMap {
categoryService.deleteCategory(it, categoryId)
}
}
@GetMapping("/by-month")
fun getCategoriesSumsByMonths(): Mono<List<Document>> {
return financialService.getCategorySumsPipeline(LocalDate.of(2024, 8, 1), LocalDate.of(2025, 1, 12))
@GetMapping("/{spaceId}/categories/tags")
fun getTags(@PathVariable spaceId: String): Mono<List<Tag>> {
return spaceService.isValidRequest(spaceId).flatMap {
spaceService.getTags(it)
}
}
@PostMapping("/{spaceId}/categories/tags")
fun createTags(@PathVariable spaceId: String, @RequestBody tag: Tag): Mono<Tag> {
return spaceService.isValidRequest(spaceId).flatMap {
spaceService.createTag(it, tag)
}
}
@DeleteMapping("/{spaceId}/categories/tags/{tagId}")
fun deleteTags(@PathVariable spaceId: String, @PathVariable tagId: String): Mono<Void> {
return spaceService.isValidRequest(spaceId).flatMap {
spaceService.deleteTag(it, tagId)
}
}
@GetMapping("/{spaceId}/analytics/by-month")
@@ -262,7 +292,7 @@ class SpaceController(
@GetMapping("/{spaceId}/recurrents")
fun getRecurrents(@PathVariable spaceId: String): Mono<List<Recurrent>> {
return spaceService.isValidRequest(spaceId).flatMap {
recurrentService.getRecurrents(it)
recurrentService.getRecurrents(it.id!!)
}
}
@@ -302,8 +332,8 @@ class SpaceController(
}
}
// @GetMapping("/regen")
// fun regenSpaces(): Mono<List<Space>> {
// return spaceService.regenSpaces()
// }
@GetMapping("/regen")
fun regenSpaces(): Mono<Category> {
return spaceService.regenSpaceCategory()
}
}

View File

@@ -23,86 +23,86 @@ import space.luminic.budgerapp.services.FinancialService
@RequestMapping("/transactions")
class TransactionController(private val financialService: FinancialService) {
@GetMapping
fun getTransactions(
@RequestParam spaceId: String,
@RequestParam(value = "transaction_type") transactionType: String? = null,
@RequestParam(value = "category_type") categoryType: String? = null,
@RequestParam(value = "user_id") userId: String? = null,
@RequestParam(value = "is_child") isChild: Boolean? = null,
@RequestParam(value = "limit") limit: Int = 10,
@RequestParam(value = "offset") offset: Int = 0
): ResponseEntity<Any> {
try {
return ResponseEntity.ok(
financialService.getTransactions(
spaceId = spaceId,
transactionType = transactionType,
categoryType = categoryType,
userId = userId,
isChild = isChild,
limit = limit,
offset = offset
)
)
} catch (e: Exception) {
e.printStackTrace()
return ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR)
}
}
@GetMapping("/{id}")
fun getTransaction(@PathVariable id: String): ResponseEntity<Any> {
try {
return ResponseEntity.ok(financialService.getTransactionById(id))
} catch (e: Exception) {
e.printStackTrace()
return ResponseEntity(e.message, HttpStatus.INTERNAL_SERVER_ERROR)
}
}
@PostMapping
fun createTransaction(@RequestParam spaceId: String, @RequestBody transaction: Transaction): ResponseEntity<Any> {
try {
return ResponseEntity.ok(financialService.createTransaction(spaceId,transaction))
} catch (e: Exception) {
e.printStackTrace()
return ResponseEntity(e.message, HttpStatus.INTERNAL_SERVER_ERROR)
}
}
@PutMapping("/{id}")
fun editTransaction(@PathVariable id: String, @RequestBody transaction: Transaction): ResponseEntity<Any> {
try {
return ResponseEntity.ok(financialService.editTransaction(transaction))
} catch (e: Exception) {
e.printStackTrace()
return ResponseEntity(e.message, HttpStatus.INTERNAL_SERVER_ERROR)
}
}
// @DeleteMapping("/{id}")
// fun deleteTransaction(@PathVariable id: String): Mono<Void> {
//
// return financialService.deleteTransaction(id)
// @GetMapping
// fun getTransactions(
// @RequestParam spaceId: String,
// @RequestParam(value = "transaction_type") transactionType: String? = null,
// @RequestParam(value = "category_type") categoryType: String? = null,
// @RequestParam(value = "user_id") userId: String? = null,
// @RequestParam(value = "is_child") isChild: Boolean? = null,
// @RequestParam(value = "limit") limit: Int = 10,
// @RequestParam(value = "offset") offset: Int = 0
// ): ResponseEntity<Any> {
// try {
// return ResponseEntity.ok(
// financialService.getTransactions(
// spaceId = spaceId,
// transactionType = transactionType,
// categoryType = categoryType,
// userId = userId,
// isChild = isChild,
// limit = limit,
// offset = offset
// )
// )
// } catch (e: Exception) {
// e.printStackTrace()
// return ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR)
// }
// }
//
// @GetMapping("/{id}")
// fun getTransaction(@PathVariable id: String): ResponseEntity<Any> {
// try {
// return ResponseEntity.ok(financialService.getTransactionById(id))
// } catch (e: Exception) {
// e.printStackTrace()
// return ResponseEntity(e.message, HttpStatus.INTERNAL_SERVER_ERROR)
// }
// }
//
// @PostMapping
// fun createTransaction(@RequestParam spaceId: String, @RequestBody transaction: Transaction): ResponseEntity<Any> {
// try {
// return ResponseEntity.ok(financialService.createTransaction(spaceId,transaction))
// } catch (e: Exception) {
// e.printStackTrace()
// return ResponseEntity(e.message, HttpStatus.INTERNAL_SERVER_ERROR)
//
// }
// }
//
// @PutMapping("/{id}")
// fun editTransaction(@PathVariable id: String, @RequestBody transaction: Transaction): ResponseEntity<Any> {
// try {
// return ResponseEntity.ok(financialService.editTransaction(transaction))
// } catch (e: Exception) {
// e.printStackTrace()
// return ResponseEntity(e.message, HttpStatus.INTERNAL_SERVER_ERROR)
// }
// }
//
//// @DeleteMapping("/{id}")
//// fun deleteTransaction(@PathVariable id: String): Mono<Void> {
////
//// return financialService.deleteTransaction(id)
////
//// }
//
//
// @GetMapping("/{id}/child")
// fun getChildTransactions(@PathVariable id: String): ResponseEntity<Any> {
// return ResponseEntity.ok(financialService.getChildTransaction(id))
// }
//
// @GetMapping("/avg-by-category")
// fun getAvgSums(): ResponseEntity<Any> {
// return ResponseEntity.ok(financialService.getAverageSpendingByCategory())
// }
@GetMapping("/{id}/child")
fun getChildTransactions(@PathVariable id: String): ResponseEntity<Any> {
return ResponseEntity.ok(financialService.getChildTransaction(id))
}
@GetMapping("/avg-by-category")
fun getAvgSums(): ResponseEntity<Any> {
return ResponseEntity.ok(financialService.getAverageSpendingByCategory())
}
@GetMapping("/types")
@GetMapping("/types/")
fun getTypes(): ResponseEntity<Any> {
return try {
ResponseEntity.ok(financialService.getTransactionTypes())

View File

@@ -7,9 +7,11 @@ import space.luminic.budgerapp.models.*
import java.time.ZoneId
@Component
class BudgetMapper : FromDocumentMapper {
class BudgetMapper(private val categoryMapper: CategoryMapper) : FromDocumentMapper {
override fun fromDocument(document: Document): Budget {
return Budget(
return Budget(
id = document.getObjectId("_id").toString(),
space = Space(id = document.get("spaceDetails", Document::class.java).getObjectId("_id").toString()),
name = document.getString("name"),
@@ -20,16 +22,8 @@ class BudgetMapper : FromDocumentMapper {
it.getObjectId("_id").toString() == cat.get("category", DBRef::class.java).id.toString()
}
BudgetCategory(
category = Category(
id = cat.get("category", DBRef::class.java).id.toString(),
type = CategoryType(
categoryDetailed.get("type", Document::class.java).getString("code"),
categoryDetailed.get("type", Document::class.java).getString("name")
),
name = categoryDetailed.getString("name"),
description = categoryDetailed.getString("description"),
icon = categoryDetailed.getString("icon")
), currentLimit = cat.getDouble("currentLimit")
category = categoryMapper.fromDocument(categoryDetailed),
currentLimit = cat.getDouble("currentLimit")
)
}.toMutableList(),
incomeCategories = document.getList("incomeCategories", Document::class.java).map { cat ->
@@ -38,16 +32,8 @@ class BudgetMapper : FromDocumentMapper {
it.getObjectId("_id").toString() == cat.get("category", DBRef::class.java).id.toString()
}
BudgetCategory(
category = Category(
id = cat.get("category", DBRef::class.java).id.toString(),
type = CategoryType(
categoryDetailed.get("type", Document::class.java).getString("code"),
categoryDetailed.get("type", Document::class.java).getString("name")
),
name = categoryDetailed.getString("name"),
description = categoryDetailed.getString("description"),
icon = categoryDetailed.getString("icon")
), currentLimit = cat.getDouble("currentLimit")
category = categoryMapper.fromDocument(categoryDetailed),
currentLimit = cat.getDouble("currentLimit")
)
}.toMutableList()
)

View File

@@ -0,0 +1,37 @@
package space.luminic.budgerapp.mappers
import org.bson.Document
import org.bson.types.ObjectId
import org.springframework.stereotype.Component
import space.luminic.budgerapp.models.Category
import space.luminic.budgerapp.models.CategoryTag
import space.luminic.budgerapp.models.CategoryType
import space.luminic.budgerapp.models.Space
@Component
class CategoryMapper : FromDocumentMapper {
override fun fromDocument(document: Document): Category {
val categoryTypeDocument = document["type"] as Document
val spaceDocument = document.get("spaceDetails", Document::class.java) ?: Document()
val spaceId = spaceDocument.get("_id", String::class.java)?:null
val tags = document.getList("tags", Document::class.java) ?: emptyList()
val categoryTags = tags.map { tag ->
CategoryTag(tag.getString("code"), tag.getString("name"))
}.toMutableSet()
return Category(
id = (document["_id"] as ObjectId).toString(),
space = Space(id = spaceId),
type = CategoryType(
categoryTypeDocument["code"] as String,
categoryTypeDocument["name"] as String
),
name = document["name"] as String,
description = document["description"] as String,
icon = document["icon"] as String,
tags = categoryTags,
)
}
}

View File

@@ -0,0 +1,34 @@
package space.luminic.budgerapp.mappers
import org.bson.Document
import org.springframework.stereotype.Component
import space.luminic.budgerapp.models.Category
import space.luminic.budgerapp.models.CategoryType
import space.luminic.budgerapp.models.Recurrent
import space.luminic.budgerapp.models.Space
@Component
class RecurrentMapper: FromDocumentMapper {
override fun fromDocument(document: Document): Recurrent {
val categoryDoc = document.get("categoryDetails", Document::class.java)
val categoryTypeDoc = categoryDoc.get("type", Document::class.java)
val spaceDocument = document.get("spaceDetails", Document::class.java)
return Recurrent(
id = document.getObjectId("_id").toString(),
space = null,
atDay = document.getInteger("atDay"),
category = Category(
id = categoryDoc.getObjectId("_id").toString(),
space = null,
type = CategoryType(categoryTypeDoc.getString("code"), categoryTypeDoc.getString("name")),
name = categoryDoc.getString("name"),
description = categoryDoc.getString("description"),
icon = categoryDoc.getString("icon"),
),
name = document.getString("name"),
description = document.getString("description"),
amount = document.getInteger("amount"),
createdAt = document.getDate("createdAt"),
)
}
}

View File

@@ -11,9 +11,10 @@ data class Category(
val id: String? = null,
@DBRef var space: Space? = null,
var type: CategoryType,
val name: String,
val description: String? = null,
val icon: String? = null
var name: String,
var description: String? = null,
var icon: String? = null,
var tags: MutableSet<CategoryTag> = mutableSetOf(),
)
@@ -21,3 +22,28 @@ data class CategoryType(
val code: String,
val name: String? = null
)
data class CategoryTag (
val code: String,
val name: String,
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as CategoryTag
if (code != other.code) return false
if (name != other.name) return false
return true
}
override fun hashCode(): Int {
var result = code.hashCode()
result = 31 * result + name.hashCode()
return result
}
}

View File

@@ -0,0 +1,16 @@
package space.luminic.budgerapp.models
import org.springframework.data.annotation.Id
import org.springframework.data.mongodb.core.mapping.DBRef
import org.springframework.data.mongodb.core.mapping.Document
@Document(collection = "tags")
data class Tag(
@Id var id: String? = null,
@DBRef var space: Space? = null,
var code: String,
var name: String,
)

View File

@@ -0,0 +1,7 @@
package space.luminic.budgerapp.repos
import org.springframework.data.mongodb.repository.ReactiveMongoRepository
import space.luminic.budgerapp.models.Tag
interface TagRepo : ReactiveMongoRepository<Tag, String> {
}

View File

@@ -14,4 +14,6 @@ interface UserRepo : ReactiveMongoRepository<User, String> {
fun findByUsernameWOPassword(username: String): Mono<User>
fun findByUsername(username: String): Mono<User>
fun findByTgId(id: String): Mono<User>
}

View File

@@ -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")) } // Ошибка, если пользователь уже существует

View File

@@ -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) // Возвращаем удалённую категорию
}
}

View File

@@ -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(

View File

@@ -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() // Собираем результаты в список
}
}

View File

@@ -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 ->