diff --git a/src/main/kotlin/space/luminic/budgerapp/configs/BearerTokenFilter.kt b/src/main/kotlin/space/luminic/budgerapp/configs/BearerTokenFilter.kt index 6e60b82..de92175 100644 --- a/src/main/kotlin/space/luminic/budgerapp/configs/BearerTokenFilter.kt +++ b/src/main/kotlin/space/luminic/budgerapp/configs/BearerTokenFilter.kt @@ -26,7 +26,7 @@ class BearerTokenFilter(private val authService: AuthService) : SecurityContextS override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono { 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) diff --git a/src/main/kotlin/space/luminic/budgerapp/configs/SecurityConfig.kt b/src/main/kotlin/space/luminic/budgerapp/configs/SecurityConfig.kt index 0cd06ed..d7b0f55 100644 --- a/src/main/kotlin/space/luminic/budgerapp/configs/SecurityConfig.kt +++ b/src/main/kotlin/space/luminic/budgerapp/configs/SecurityConfig.kt @@ -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() } diff --git a/src/main/kotlin/space/luminic/budgerapp/controllers/AuthController.kt b/src/main/kotlin/space/luminic/budgerapp/controllers/AuthController.kt index 62bfa8b..d3c04f0 100644 --- a/src/main/kotlin/space/luminic/budgerapp/controllers/AuthController.kt +++ b/src/main/kotlin/space/luminic/budgerapp/controllers/AuthController.kt @@ -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> { + return authService.tgLogin(tgId).map { token -> mapOf("token" to token) } + } + @GetMapping("/me") fun getMe(@RequestHeader("Authorization") token: String): Mono { diff --git a/src/main/kotlin/space/luminic/budgerapp/controllers/SpaceController.kt b/src/main/kotlin/space/luminic/budgerapp/controllers/SpaceController.kt index 3133b85..865e8f0 100644 --- a/src/main/kotlin/space/luminic/budgerapp/controllers/SpaceController.kt +++ b/src/main/kotlin/space/luminic/budgerapp/controllers/SpaceController.kt @@ -87,7 +87,9 @@ class SpaceController( // @GetMapping("/{spaceId}/budgets") fun getBudgets(@PathVariable spaceId: String): Mono> { - 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 { 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 { 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 { 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 { + fun getCategoriesTypes(@PathVariable spaceId: String): ResponseEntity { 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 { - return categoryService.editCategory(category) + @PutMapping("/{spaceId}/categories/{categoryId}") + fun editCategory( + @PathVariable categoryId: String, + @RequestBody category: Category, + @PathVariable spaceId: String + ): Mono { + return spaceService.isValidRequest(spaceId).flatMap { + categoryService.editCategory(it, category) + } } + @DeleteMapping("/{spaceId}/categories/{categoryId}") + fun deleteCategory(@PathVariable categoryId: String, @PathVariable spaceId: String): Mono { + return spaceService.isValidRequest(spaceId).flatMap { + categoryService.deleteCategory(it, categoryId) + } + } - @GetMapping("/by-month") - fun getCategoriesSumsByMonths(): Mono> { - return financialService.getCategorySumsPipeline(LocalDate.of(2024, 8, 1), LocalDate.of(2025, 1, 12)) + @GetMapping("/{spaceId}/categories/tags") + fun getTags(@PathVariable spaceId: String): Mono> { + return spaceService.isValidRequest(spaceId).flatMap { + spaceService.getTags(it) + } + } + + @PostMapping("/{spaceId}/categories/tags") + fun createTags(@PathVariable spaceId: String, @RequestBody tag: Tag): Mono { + return spaceService.isValidRequest(spaceId).flatMap { + spaceService.createTag(it, tag) + } + } + + @DeleteMapping("/{spaceId}/categories/tags/{tagId}") + fun deleteTags(@PathVariable spaceId: String, @PathVariable tagId: String): Mono { + 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> { return spaceService.isValidRequest(spaceId).flatMap { - recurrentService.getRecurrents(it) + recurrentService.getRecurrents(it.id!!) } } @@ -302,8 +332,8 @@ class SpaceController( } } -// @GetMapping("/regen") -// fun regenSpaces(): Mono> { -// return spaceService.regenSpaces() -// } + @GetMapping("/regen") + fun regenSpaces(): Mono { + return spaceService.regenSpaceCategory() + } } \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/budgerapp/controllers/TransactionController.kt b/src/main/kotlin/space/luminic/budgerapp/controllers/TransactionController.kt index d29ad98..a58c178 100644 --- a/src/main/kotlin/space/luminic/budgerapp/controllers/TransactionController.kt +++ b/src/main/kotlin/space/luminic/budgerapp/controllers/TransactionController.kt @@ -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 { - 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 { - 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 { - 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 { - 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 { // -// 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 { +// 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 { +// 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 { +// 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 { +// 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 { +//// +//// return financialService.deleteTransaction(id) +//// +//// } +// +// +// @GetMapping("/{id}/child") +// fun getChildTransactions(@PathVariable id: String): ResponseEntity { +// return ResponseEntity.ok(financialService.getChildTransaction(id)) +// } +// +// @GetMapping("/avg-by-category") +// fun getAvgSums(): ResponseEntity { +// return ResponseEntity.ok(financialService.getAverageSpendingByCategory()) // } - @GetMapping("/{id}/child") - fun getChildTransactions(@PathVariable id: String): ResponseEntity { - return ResponseEntity.ok(financialService.getChildTransaction(id)) - } - - @GetMapping("/avg-by-category") - fun getAvgSums(): ResponseEntity { - return ResponseEntity.ok(financialService.getAverageSpendingByCategory()) - } - - - @GetMapping("/types") + @GetMapping("/types/") fun getTypes(): ResponseEntity { return try { ResponseEntity.ok(financialService.getTransactionTypes()) diff --git a/src/main/kotlin/space/luminic/budgerapp/mappers/BudgetMapper.kt b/src/main/kotlin/space/luminic/budgerapp/mappers/BudgetMapper.kt index 2acd0ca..1818da3 100644 --- a/src/main/kotlin/space/luminic/budgerapp/mappers/BudgetMapper.kt +++ b/src/main/kotlin/space/luminic/budgerapp/mappers/BudgetMapper.kt @@ -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() ) diff --git a/src/main/kotlin/space/luminic/budgerapp/mappers/CategoryMapper.kt b/src/main/kotlin/space/luminic/budgerapp/mappers/CategoryMapper.kt new file mode 100644 index 0000000..023bea4 --- /dev/null +++ b/src/main/kotlin/space/luminic/budgerapp/mappers/CategoryMapper.kt @@ -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, + ) + + } +} \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/budgerapp/mappers/RecurrentMapper.kt b/src/main/kotlin/space/luminic/budgerapp/mappers/RecurrentMapper.kt new file mode 100644 index 0000000..f4c36b7 --- /dev/null +++ b/src/main/kotlin/space/luminic/budgerapp/mappers/RecurrentMapper.kt @@ -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"), + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/budgerapp/models/Category.kt b/src/main/kotlin/space/luminic/budgerapp/models/Category.kt index 9d82db3..beacb53 100644 --- a/src/main/kotlin/space/luminic/budgerapp/models/Category.kt +++ b/src/main/kotlin/space/luminic/budgerapp/models/Category.kt @@ -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 = 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 + } +} diff --git a/src/main/kotlin/space/luminic/budgerapp/models/Tag.kt b/src/main/kotlin/space/luminic/budgerapp/models/Tag.kt new file mode 100644 index 0000000..fa3237e --- /dev/null +++ b/src/main/kotlin/space/luminic/budgerapp/models/Tag.kt @@ -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, +) \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/budgerapp/repos/TagRepo.kt b/src/main/kotlin/space/luminic/budgerapp/repos/TagRepo.kt new file mode 100644 index 0000000..2a7c52c --- /dev/null +++ b/src/main/kotlin/space/luminic/budgerapp/repos/TagRepo.kt @@ -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 { +} \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/budgerapp/repos/UserRepo.kt b/src/main/kotlin/space/luminic/budgerapp/repos/UserRepo.kt index c0e63ce..67d7bcf 100644 --- a/src/main/kotlin/space/luminic/budgerapp/repos/UserRepo.kt +++ b/src/main/kotlin/space/luminic/budgerapp/repos/UserRepo.kt @@ -14,4 +14,6 @@ interface UserRepo : ReactiveMongoRepository { fun findByUsernameWOPassword(username: String): Mono fun findByUsername(username: String): Mono + + fun findByTgId(id: String): Mono } \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/budgerapp/services/AuthService.kt b/src/main/kotlin/space/luminic/budgerapp/services/AuthService.kt index 265fb79..1ec26a5 100644 --- a/src/main/kotlin/space/luminic/budgerapp/services/AuthService.kt +++ b/src/main/kotlin/space/luminic/budgerapp/services/AuthService.kt @@ -43,6 +43,25 @@ class AuthService( } } + fun tgLogin(tgId: String): Mono { + 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 { return userRepository.findByUsername(username) .flatMap { Mono.error(IllegalArgumentException("User with username '$username' already exists")) } // Ошибка, если пользователь уже существует diff --git a/src/main/kotlin/space/luminic/budgerapp/services/CategoryService.kt b/src/main/kotlin/space/luminic/budgerapp/services/CategoryService.kt index e210520..8877a32 100644 --- a/src/main/kotlin/space/luminic/budgerapp/services/CategoryService.kt +++ b/src/main/kotlin/space/luminic/budgerapp/services/CategoryService.kt @@ -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 { return categoryRepo.findById(id) } - - // @Cacheable("categories") - fun getCategories(spaceId: String, type: String? = null, sortBy: String, direction: String): Mono> { + fun findCategory( + space: Space? = null, + id: String? = null, + name: String? = null, + tagCode: String? = null + ): Mono { + val lookupSpaces = lookup("spaces", "space.\$id", "_id", "spaceDetails") + val unwindSpace = unwind("spaceDetails") val matchCriteria = mutableListOf() - // Добавляем фильтры - 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> { + val lookupSpaces = lookup("spaces", "space.\$id", "_id", "spaceDetails") + val unwindSpace = unwind("spaceDetails") + val matchCriteria = mutableListOf() + // Добавляем фильтры + 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 в Mono> - .map { it.toMutableList() } - } - - - @Cacheable("getAllCategories") - fun getCategories2(type: String? = null, sortBy: String, direction: String): Mono> { - 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 { - return categoryRepo.findById(category.id!!) // Возвращаем Mono + fun editCategory(space: Space, category: Category): Mono { + return findCategory(space, id = category.id) // Возвращаем Mono .flatMap { oldCategory -> if (oldCategory.type.code != category.type.code) { return@flatMap Mono.error(IllegalArgumentException("You cannot change category type")) } + category.space = space categoryRepo.save(category) // Сохраняем категорию, если тип не изменился } } - @CacheEvict(cacheNames = ["getAllCategories"], allEntries = true) -// fun deleteCategory(categoryId: String): Mono { -// 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>> { - 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 { + 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> { - 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> { - 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> { - 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) // Возвращаем удалённую категорию + } } diff --git a/src/main/kotlin/space/luminic/budgerapp/services/FinancialService.kt b/src/main/kotlin/space/luminic/budgerapp/services/FinancialService.kt index de5daeb..db1184d 100644 --- a/src/main/kotlin/space/luminic/budgerapp/services/FinancialService.kt +++ b/src/main/kotlin/space/luminic/budgerapp/services/FinancialService.kt @@ -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> { 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 { - return Mono.zip(getBudgetByDate(budget.dateFrom, spaceId).map { Optional.ofNullable(it) } + fun createBudget(space: Space, budget: Budget, createRecurrent: Boolean): Mono { + 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(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 { return budgetRepo.findByDateFromLessThanEqualAndDateToGreaterThanEqualAndSpace(date, date, ObjectId(spaceId)) .switchIfEmpty(Mono.empty()) @@ -665,82 +627,69 @@ class FinancialService( limit: Int? = null, offset: Int? = null, ): Mono> { - 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>(IllegalArgumentException("User does not have access to this Space")) - } - val matchCriteria = mutableListOf() + val matchCriteria = mutableListOf() - // Добавляем фильтры - 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 { @@ -832,26 +781,22 @@ class FinancialService( } - fun createTransaction(spaceId: String, transaction: Transaction): Mono { + fun createTransaction(space: Space, transaction: Transaction): Mono { 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(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(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( diff --git a/src/main/kotlin/space/luminic/budgerapp/services/RecurrentService.kt b/src/main/kotlin/space/luminic/budgerapp/services/RecurrentService.kt index b3c85aa..99ba07f 100644 --- a/src/main/kotlin/space/luminic/budgerapp/services/RecurrentService.kt +++ b/src/main/kotlin/space/luminic/budgerapp/services/RecurrentService.kt @@ -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> { + fun getRecurrents(spaceId: String): Mono> { 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> + getRecurrents(space.id!!) // Теперь это Mono> .flatMapMany { Flux.fromIterable(it) } // Преобразуем List в Flux .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> { - return recurrentRepo.findAll() - .flatMap { recurrent -> - spaceService.getSpace("67af3c0f652da946a7dd9931") - .flatMap { space -> - recurrent.space = space - recurrentRepo.save(recurrent) // Сохраняем и возвращаем сохраненный объект - } - } - .collectList() // Собираем результаты в список - } + } \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/budgerapp/services/SpaceService.kt b/src/main/kotlin/space/luminic/budgerapp/services/SpaceService.kt index 9e3d352..a826701 100644 --- a/src/main/kotlin/space/luminic/budgerapp/services/SpaceService.kt +++ b/src/main/kotlin/space/luminic/budgerapp/services/SpaceService.kt @@ -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 { @@ -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 { return ReactiveSecurityContextHolder.getContext() .map { it.authentication } @@ -247,6 +258,106 @@ class SpaceService( } } + fun findTag(space: Space, tagCode: String): Mono { + val lookupSpaces = lookup("spaces", "space.\$id", "_id", "spaceDetails") + val unwindSpace = unwind("spaceDetails") + val matchCriteria = mutableListOf() + // Добавляем фильтры + 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.space = space + return findTag(space, tag.code) + .flatMap { existingTag -> + Mono.error(IllegalArgumentException("Tag with code ${existingTag.code} already exists")) + } + .switchIfEmpty(tagRepo.save(tag)) + } + + fun deleteTag(space: Space, tagCode: String): Mono { + 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> { + val lookupSpaces = lookup("spaces", "space.\$id", "_id", "spaceDetails") + val unwindSpace = unwind("spaceDetails") + val matchCriteria = mutableListOf() + // Добавляем фильтры + 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 в Mono> + .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 { + 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 + } + } + } + // fun regenSpaces(): Mono> { // return spaceRepo.findAll() // .flatMap { space ->