From db0ada5ee81e24f9672da31f25a40810ac203388 Mon Sep 17 00:00:00 2001 From: xds Date: Fri, 28 Feb 2025 01:17:52 +0300 Subject: [PATCH] suspend coroutines --- .../budgerapp/configs/BearerTokenFilter.kt | 44 +- .../budgerapp/controllers/AuthController.kt | 28 +- .../budgerapp/controllers/SpaceController.kt | 242 ++-- .../controllers/SubscriptionController.kt | 23 +- .../luminic/budgerapp/mappers/BudgetMapper.kt | 9 +- .../luminic/budgerapp/services/AuthService.kt | 35 +- .../budgerapp/services/CategoryService.kt | 115 +- .../budgerapp/services/FinancialService.kt | 1153 +++++++++-------- .../budgerapp/services/RecurrentService.kt | 84 +- .../budgerapp/services/SpaceService.kt | 493 ++++--- .../budgerapp/services/SubscriptionService.kt | 26 +- .../budgerapp/services/TokenService.kt | 14 +- .../luminic/budgerapp/services/UserService.kt | 17 +- 13 files changed, 1099 insertions(+), 1184 deletions(-) diff --git a/src/main/kotlin/space/luminic/budgerapp/configs/BearerTokenFilter.kt b/src/main/kotlin/space/luminic/budgerapp/configs/BearerTokenFilter.kt index de92175..196b0fa 100644 --- a/src/main/kotlin/space/luminic/budgerapp/configs/BearerTokenFilter.kt +++ b/src/main/kotlin/space/luminic/budgerapp/configs/BearerTokenFilter.kt @@ -1,16 +1,11 @@ package space.luminic.budgerapp.configs +import kotlinx.coroutines.reactor.mono import org.slf4j.LoggerFactory import org.springframework.http.HttpHeaders -import org.springframework.http.HttpMethod -import org.springframework.http.HttpStatus -import org.springframework.security.authentication.BadCredentialsException import org.springframework.security.authentication.UsernamePasswordAuthenticationToken -import org.springframework.security.core.AuthenticationException -import org.springframework.security.core.GrantedAuthority import org.springframework.security.core.authority.SimpleGrantedAuthority import org.springframework.security.core.context.ReactiveSecurityContextHolder -import org.springframework.security.core.context.SecurityContext import org.springframework.security.core.context.SecurityContextImpl import org.springframework.security.web.server.context.SecurityContextServerWebExchangeWebFilter import org.springframework.stereotype.Component @@ -23,37 +18,36 @@ import space.luminic.budgerapp.services.AuthService class BearerTokenFilter(private val authService: AuthService) : SecurityContextServerWebExchangeWebFilter() { private val logger = LoggerFactory.getLogger(BearerTokenFilter::class.java) + + 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", "/api/auth/tgLogin") || exchange.request.path.value() - .startsWith("/api/actuator") + 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) } return if (token != null) { - authService.isTokenValid(token) - .flatMap { userDetails -> - val authorities = userDetails.roles.map { SimpleGrantedAuthority(it) } - val securityContext = SecurityContextImpl( - UsernamePasswordAuthenticationToken( - userDetails.username, null, authorities - ) - ) - chain.filter(exchange) - .contextWrite(ReactiveSecurityContextHolder.withSecurityContext(Mono.just(securityContext))) - } - .onErrorMap(AuthException::class.java) { ex -> - BadCredentialsException(ex.message ?: "Unauthorized") - } - + mono { + val userDetails = authService.isTokenValid(token) // suspend вызов + val authorities = userDetails.roles.map { SimpleGrantedAuthority(it) } + val securityContext = SecurityContextImpl( + UsernamePasswordAuthenticationToken(userDetails.username, null, authorities) + ) + securityContext + }.flatMap { securityContext -> + chain.filter(exchange) + .contextWrite(ReactiveSecurityContextHolder.withSecurityContext(Mono.just(securityContext))) + } } else { Mono.error(AuthException("Authorization token is missing")) } } - - } diff --git a/src/main/kotlin/space/luminic/budgerapp/controllers/AuthController.kt b/src/main/kotlin/space/luminic/budgerapp/controllers/AuthController.kt index 5589c85..5026624 100644 --- a/src/main/kotlin/space/luminic/budgerapp/controllers/AuthController.kt +++ b/src/main/kotlin/space/luminic/budgerapp/controllers/AuthController.kt @@ -1,6 +1,11 @@ package space.luminic.budgerapp.controllers +import kotlinx.coroutines.reactive.awaitFirst +import kotlinx.coroutines.reactive.awaitSingle +import org.slf4j.LoggerFactory +import org.springframework.security.core.context.ReactiveSecurityContextHolder +import org.springframework.security.core.context.SecurityContextHolder import org.springframework.web.bind.annotation.* import reactor.core.publisher.Mono import space.luminic.budgerapp.models.User @@ -10,16 +15,23 @@ import space.luminic.budgerapp.services.UserService @RestController @RequestMapping("/auth") class AuthController( - - private val userService: UserService, private val authService: AuthService ) { + private val logger = LoggerFactory.getLogger(javaClass) + + @GetMapping("/test") + fun test(): String { + val authentication = SecurityContextHolder.getContext().authentication + logger.info("SecurityContext in controller: $authentication") + return "Hello, ${authentication.name}" + } + @PostMapping("/login") - fun login(@RequestBody request: AuthRequest): Mono> { + suspend fun login(@RequestBody request: AuthRequest): Map { return authService.login(request.username, request.password) - .map { token -> mapOf("token" to token) } + .map { token -> mapOf("token" to token) }.awaitFirst() } @PostMapping("/register") @@ -34,11 +46,9 @@ class AuthController( @GetMapping("/me") - fun getMe(@RequestHeader("Authorization") token: String): Mono { - return authService.isTokenValid(token.removePrefix("Bearer ")) - .flatMap { username -> - userService.getByUserNameWoPass(username.username!!) - } + suspend fun getMe(): User { + val securityContext = ReactiveSecurityContextHolder.getContext().awaitSingle() + return userService.getByUserNameWoPass(securityContext.authentication.name) } } diff --git a/src/main/kotlin/space/luminic/budgerapp/controllers/SpaceController.kt b/src/main/kotlin/space/luminic/budgerapp/controllers/SpaceController.kt index d39ed33..89fbcf2 100644 --- a/src/main/kotlin/space/luminic/budgerapp/controllers/SpaceController.kt +++ b/src/main/kotlin/space/luminic/budgerapp/controllers/SpaceController.kt @@ -1,12 +1,11 @@ package space.luminic.budgerapp.controllers +import kotlinx.coroutines.reactor.awaitSingle +import kotlinx.coroutines.reactor.awaitSingleOrNull import org.bson.Document import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.* -import org.springframework.web.client.HttpClientErrorException -import reactor.core.publisher.Flux -import reactor.core.publisher.Mono import space.luminic.budgerapp.controllers.BudgetController.LimitValue import space.luminic.budgerapp.controllers.dtos.BudgetCreationDTO import space.luminic.budgerapp.models.* @@ -33,13 +32,13 @@ class SpaceController( ) @GetMapping - fun getSpaces(): Mono> { + suspend fun getSpaces(): List { return spaceService.getSpaces() } @PostMapping - fun createSpace(@RequestBody space: SpaceCreateDTO): Mono { + suspend fun createSpace(@RequestBody space: SpaceCreateDTO): Space { return spaceService.createSpace( Space(name = space.name, description = space.description), space.createCategories @@ -48,87 +47,85 @@ class SpaceController( @GetMapping("{spaceId}") - fun getSpace(@PathVariable spaceId: String): Mono { + suspend fun getSpace(@PathVariable spaceId: String): Space { return spaceService.getSpace(spaceId) } @DeleteMapping("/{spaceId}") - fun deleteSpace(@PathVariable spaceId: String): Mono { - return spaceService.isValidRequest(spaceId).flatMap { - spaceService.deleteSpace(it) - } - + suspend fun deleteSpace(@PathVariable spaceId: String) { + return spaceService.deleteSpace(spaceService.isValidRequest(spaceId)) } + @PostMapping("/{spaceId}/invite") - fun inviteSpace(@PathVariable spaceId: String): Mono { + suspend fun inviteSpace(@PathVariable spaceId: String): SpaceInvite { + spaceService.isValidRequest(spaceId) return spaceService.createInviteSpace(spaceId) } @PostMapping("/invite/{code}") - fun acceptInvite(@PathVariable code: String): Mono { + suspend fun acceptInvite(@PathVariable code: String): Space { return spaceService.acceptInvite(code) } @DeleteMapping("/{spaceId}/leave") - fun leaveSpace(@PathVariable spaceId: String): Mono { + suspend fun leaveSpace(@PathVariable spaceId: String) { + spaceService.isValidRequest(spaceId) return spaceService.leaveSpace(spaceId) } @DeleteMapping("/{spaceId}/members/kick/{username}") - fun kickMembers(@PathVariable spaceId: String, @PathVariable username: String): Mono { + suspend fun kickMembers(@PathVariable spaceId: String, @PathVariable username: String) { + spaceService.isValidRequest(spaceId) return spaceService.kickMember(spaceId, username) } // - //Budgets API - // +//Budgets API +// @GetMapping("/{spaceId}/budgets") - fun getBudgets(@PathVariable spaceId: String): Mono> { - return spaceService.isValidRequest(spaceId).flatMap { - financialService.getBudgets(spaceId) - } + suspend fun getBudgets(@PathVariable spaceId: String): List { + spaceService.isValidRequest(spaceId) + return financialService.getBudgets(spaceId).awaitSingleOrNull().orEmpty() + } @GetMapping("/{spaceId}/budgets/{id}") - fun getBudget(@PathVariable spaceId: String, @PathVariable id: String): Mono { - return spaceService.isValidRequest(spaceId).flatMap { - financialService.getBudget(spaceId, id) - } - + suspend fun getBudget(@PathVariable spaceId: String, @PathVariable id: String): BudgetDTO? { + spaceService.isValidRequest(spaceId) + return financialService.getBudget(spaceId, id) } @PostMapping("/{spaceId}/budgets") - fun createBudget( + suspend fun createBudget( @PathVariable spaceId: String, @RequestBody budgetCreationDTO: BudgetCreationDTO, - ): Mono { - return spaceService.isValidRequest(spaceId).flatMap { - financialService.createBudget(it, budgetCreationDTO.budget, budgetCreationDTO.createRecurrent) - } - + ): Budget? { + return financialService.createBudget( + spaceService.isValidRequest(spaceId), + budgetCreationDTO.budget, + budgetCreationDTO.createRecurrent + ) } @DeleteMapping("/{spaceId}/budgets/{id}") - fun deleteBudget(@PathVariable spaceId: String, @PathVariable id: String): Mono { - return spaceService.isValidRequest(spaceId).flatMap { - financialService.deleteBudget(spaceId, id) - } + suspend fun deleteBudget(@PathVariable spaceId: String, @PathVariable id: String) { + spaceService.isValidRequest(spaceId) + financialService.deleteBudget(spaceId, id) + } @PostMapping("/{spaceId}/budgets/{budgetId}/categories/{catId}/limit") - fun setCategoryLimit( + suspend fun setCategoryLimit( @PathVariable spaceId: String, @PathVariable budgetId: String, @PathVariable catId: String, @RequestBody limit: LimitValue, - ): Mono { - return spaceService.isValidRequest(spaceId).flatMap { - financialService.setCategoryLimit(it.id!!, budgetId, catId, limit.limit) - } - + ): BudgetCategory { + spaceService.isValidRequest(spaceId) + return financialService.setCategoryLimit(spaceId, budgetId, catId, limit.limit) } // @@ -164,172 +161,145 @@ class SpaceController( @GetMapping("/{spaceId}/transactions/{id}") - fun getTransaction( + suspend fun getTransaction( @PathVariable spaceId: String, @PathVariable id: String - ): ResponseEntity { - try { - return ResponseEntity.ok(financialService.getTransactionById(id)) - } catch (e: Exception) { - e.printStackTrace() - return ResponseEntity(e.message, HttpStatus.INTERNAL_SERVER_ERROR) - } + ): Transaction { + return financialService.getTransactionById(id) } @PostMapping("/{spaceId}/transactions") - fun createTransaction(@PathVariable spaceId: String, @RequestBody transaction: Transaction): Mono { - return spaceService.isValidRequest(spaceId).flatMap { - financialService.createTransaction(it, transaction) - } + suspend fun createTransaction(@PathVariable spaceId: String, @RequestBody transaction: Transaction): Transaction { + val space = spaceService.isValidRequest(spaceId) + return financialService.createTransaction(space, transaction) + } @PutMapping("/{spaceId}/transactions/{id}") - fun editTransaction( + suspend fun editTransaction( @PathVariable spaceId: String, @PathVariable id: String, @RequestBody transaction: Transaction - ): Mono { - return spaceService.isValidRequest(spaceId).flatMap { - transaction.space = it - financialService.editTransaction(transaction) - } - + ): Transaction { + val space = spaceService.isValidRequest(spaceId) + transaction.space = space + return financialService.editTransaction(transaction) } @DeleteMapping("/{spaceId}/transactions/{id}") - fun deleteTransaction(@PathVariable spaceId: String, @PathVariable id: String): Mono { - return spaceService.isValidRequest(spaceId).flatMap { - financialService.getTransactionById(id).flatMap { financialService.deleteTransaction(it) } - } + suspend fun deleteTransaction(@PathVariable spaceId: String, @PathVariable id: String) { + spaceService.isValidRequest(spaceId) + val transaction = financialService.getTransactionById(id) + financialService.deleteTransaction(transaction) } // - // Categories API - // - - +// Categories API +// @GetMapping("/{spaceId}/categories") - fun getCategories( + suspend fun getCategories( @PathVariable spaceId: String, @RequestParam("type") type: String? = null, @RequestParam("sort") sortBy: String = "name", @RequestParam("direction") direction: String = "ASC" - ): Mono> { - return spaceService.isValidRequest(spaceId).flatMap { - categoryService.getCategories(spaceId, type, sortBy, direction) - } - + ): List { + spaceService.isValidRequest(spaceId) + return categoryService.getCategories(spaceId, type, sortBy, direction).awaitSingleOrNull().orEmpty() } @GetMapping("/{spaceId}/categories/types") - fun getCategoriesTypes(@PathVariable spaceId: String): ResponseEntity { - return try { - ResponseEntity.ok(categoryService.getCategoryTypes()) - } catch (e: Exception) { - ResponseEntity(HttpClientErrorException(HttpStatus.INTERNAL_SERVER_ERROR), HttpStatus.INTERNAL_SERVER_ERROR) - - } + fun getCategoriesTypes(@PathVariable spaceId: String): List { + return categoryService.getCategoryTypes() } @PostMapping("/{spaceId}/categories") - fun createCategory( + suspend fun createCategory( @PathVariable spaceId: String, @RequestBody category: Category - ): Mono { - return spaceService.isValidRequest(spaceId).flatMap { - financialService.createCategory(it, category) - } - + ): Category { + val space = spaceService.isValidRequest(spaceId) + return financialService.createCategory(space, category).awaitSingle() } @PutMapping("/{spaceId}/categories/{categoryId}") - fun editCategory( + suspend fun editCategory( @PathVariable categoryId: String, @RequestBody category: Category, @PathVariable spaceId: String - ): Mono { - return spaceService.isValidRequest(spaceId).flatMap { - categoryService.editCategory(it, category) - } + ): Category { + val space = spaceService.isValidRequest(spaceId) + return categoryService.editCategory(space, category) } @DeleteMapping("/{spaceId}/categories/{categoryId}") - fun deleteCategory(@PathVariable categoryId: String, @PathVariable spaceId: String): Mono { - return spaceService.isValidRequest(spaceId).flatMap { - categoryService.deleteCategory(it, categoryId) - } + suspend fun deleteCategory(@PathVariable categoryId: String, @PathVariable spaceId: String) { + val space = spaceService.isValidRequest(spaceId) + categoryService.deleteCategory(space, categoryId) } @GetMapping("/{spaceId}/categories/tags") - fun getTags(@PathVariable spaceId: String): Mono> { - return spaceService.isValidRequest(spaceId).flatMap { - spaceService.getTags(it) - } + suspend fun getTags(@PathVariable spaceId: String): List { + val space = spaceService.isValidRequest(spaceId) + return spaceService.getTags(space) + } @PostMapping("/{spaceId}/categories/tags") - fun createTags(@PathVariable spaceId: String, @RequestBody tag: Tag): Mono { - return spaceService.isValidRequest(spaceId).flatMap { - spaceService.createTag(it, tag) - } + suspend fun createTags(@PathVariable spaceId: String, @RequestBody tag: Tag): Tag { + val space = spaceService.isValidRequest(spaceId) + return spaceService.createTag(space, tag) + } @DeleteMapping("/{spaceId}/categories/tags/{tagId}") - fun deleteTags(@PathVariable spaceId: String, @PathVariable tagId: String): Mono { - return spaceService.isValidRequest(spaceId).flatMap { - spaceService.deleteTag(it, tagId) - } + suspend fun deleteTags(@PathVariable spaceId: String, @PathVariable tagId: String) { + val space = spaceService.isValidRequest(spaceId) + return spaceService.deleteTag(space, tagId) } @GetMapping("/{spaceId}/analytics/by-month") - fun getCategoriesSumsByMonthsV2(@PathVariable spaceId: String): Mono> { + suspend fun getCategoriesSumsByMonthsV2(@PathVariable spaceId: String): List { return financialService.getCategorySummaries(spaceId, LocalDate.now().minusMonths(6)) } - // - // Recurrents API - // +// +// Recurrents API +// @GetMapping("/{spaceId}/recurrents") - fun getRecurrents(@PathVariable spaceId: String): Mono> { - return spaceService.isValidRequest(spaceId).flatMap { - recurrentService.getRecurrents(it.id!!) - } + suspend fun getRecurrents(@PathVariable spaceId: String): List { + spaceService.isValidRequest(spaceId) + return recurrentService.getRecurrents(spaceId).awaitSingleOrNull().orEmpty() + } @GetMapping("/{spaceId}/recurrents/{id}") - fun getRecurrent(@PathVariable spaceId: String, @PathVariable id: String): Mono { - return spaceService.isValidRequest(spaceId).flatMap { - recurrentService.getRecurrentById(it, id) - } + suspend fun getRecurrent(@PathVariable spaceId: String, @PathVariable id: String): Recurrent { + val space = spaceService.isValidRequest(spaceId) + return recurrentService.getRecurrentById(space, id).awaitSingle() } @PostMapping("/{spaceId}/recurrent") - fun createRecurrent(@PathVariable spaceId: String, @RequestBody recurrent: Recurrent): Mono { - return spaceService.isValidRequest(spaceId).flatMap { - recurrentService.createRecurrent(it, recurrent) - } - + suspend fun createRecurrent(@PathVariable spaceId: String, @RequestBody recurrent: Recurrent): Recurrent { + val space = spaceService.isValidRequest(spaceId) + return recurrentService.createRecurrent(space, recurrent).awaitSingle() } @PutMapping("/{spaceId}/recurrent/{id}") - fun editRecurrent( + suspend fun editRecurrent( @PathVariable spaceId: String, @PathVariable id: String, @RequestBody recurrent: Recurrent - ): Mono { - return spaceService.isValidRequest(spaceId).flatMap { - recurrentService.editRecurrent(recurrent) - } - + ): Recurrent { + spaceService.isValidRequest(spaceId) + return recurrentService.editRecurrent(recurrent).awaitSingle() } @DeleteMapping("/{spaceId}/recurrent/{id}") - fun deleteRecurrent(@PathVariable spaceId: String, @PathVariable id: String): Mono { - return spaceService.isValidRequest(spaceId).flatMap { - recurrentService.deleteRecurrent(id) - } + suspend fun deleteRecurrent(@PathVariable spaceId: String, @PathVariable id: String) { + spaceService.isValidRequest(spaceId) + recurrentService.deleteRecurrent(id).awaitSingle() } // @GetMapping("/regen") diff --git a/src/main/kotlin/space/luminic/budgerapp/controllers/SubscriptionController.kt b/src/main/kotlin/space/luminic/budgerapp/controllers/SubscriptionController.kt index 8661cb5..69f1158 100644 --- a/src/main/kotlin/space/luminic/budgerapp/controllers/SubscriptionController.kt +++ b/src/main/kotlin/space/luminic/budgerapp/controllers/SubscriptionController.kt @@ -1,13 +1,10 @@ package space.luminic.budgerapp.controllers +import kotlinx.coroutines.reactive.awaitSingle import org.springframework.http.ResponseEntity import org.springframework.security.core.Authentication -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.PostMapping -import org.springframework.web.bind.annotation.RequestBody -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RestController -import reactor.core.publisher.Mono +import org.springframework.security.core.context.ReactiveSecurityContextHolder +import org.springframework.web.bind.annotation.* import space.luminic.budgerapp.models.PushMessage import space.luminic.budgerapp.models.SubscriptionDTO import space.luminic.budgerapp.services.SubscriptionService @@ -28,17 +25,13 @@ class SubscriptionController( } @PostMapping("/subscribe") - fun subscribe( + suspend fun subscribe( @RequestBody subscription: SubscriptionDTO, authentication: Authentication - ): Mono { - return userService.getByUserNameWoPass(authentication.name) - .flatMap { user -> - subscriptionService.subscribe(subscription, user) - .thenReturn("Subscription successful") - } - .switchIfEmpty(Mono.just("User not found")) - + ): String { + val securityContext = ReactiveSecurityContextHolder.getContext().awaitSingle() + val user = userService.getByUserNameWoPass(securityContext.authentication.name) + return subscriptionService.subscribe(subscription, user) } @PostMapping("/notifyAll") diff --git a/src/main/kotlin/space/luminic/budgerapp/mappers/BudgetMapper.kt b/src/main/kotlin/space/luminic/budgerapp/mappers/BudgetMapper.kt index 1818da3..c517610 100644 --- a/src/main/kotlin/space/luminic/budgerapp/mappers/BudgetMapper.kt +++ b/src/main/kotlin/space/luminic/budgerapp/mappers/BudgetMapper.kt @@ -11,13 +11,16 @@ class BudgetMapper(private val categoryMapper: CategoryMapper) : FromDocumentMap override fun fromDocument(document: Document): Budget { + val spaceId = document.get("spaceDetails", Document::class.java)?.getObjectId("_id")?.toString() + val categoriesList = document.getList("categories", Document::class.java).orEmpty() + val incomeCategoriesList = document.getList("incomeCategories", Document::class.java).orEmpty() return Budget( id = document.getObjectId("_id").toString(), - space = Space(id = document.get("spaceDetails", Document::class.java).getObjectId("_id").toString()), + space = Space(id=spaceId), name = document.getString("name"), dateFrom = document.getDate("dateFrom").toInstant().atZone(ZoneId.systemDefault()).toLocalDate(), dateTo = document.getDate("dateTo").toInstant().atZone(ZoneId.systemDefault()).toLocalDate(), - categories = document.getList("categories", Document::class.java).map { cat -> + categories = categoriesList.map { cat -> val categoryDetailed = document.getList("categoriesDetails", Document::class.java).first { it.getObjectId("_id").toString() == cat.get("category", DBRef::class.java).id.toString() } @@ -26,7 +29,7 @@ class BudgetMapper(private val categoryMapper: CategoryMapper) : FromDocumentMap currentLimit = cat.getDouble("currentLimit") ) }.toMutableList(), - incomeCategories = document.getList("incomeCategories", Document::class.java).map { cat -> + incomeCategories = incomeCategoriesList.map { cat -> val categoryDetailed = document.getList("incomeCategoriesDetails", Document::class.java).first { it -> it.getObjectId("_id").toString() == cat.get("category", DBRef::class.java).id.toString() diff --git a/src/main/kotlin/space/luminic/budgerapp/services/AuthService.kt b/src/main/kotlin/space/luminic/budgerapp/services/AuthService.kt index fdbfe47..bcbd427 100644 --- a/src/main/kotlin/space/luminic/budgerapp/services/AuthService.kt +++ b/src/main/kotlin/space/luminic/budgerapp/services/AuthService.kt @@ -1,5 +1,6 @@ package space.luminic.budgerapp.services +import kotlinx.coroutines.reactive.awaitFirstOrNull import org.springframework.cache.annotation.Cacheable import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder import org.springframework.stereotype.Service @@ -11,16 +12,17 @@ import space.luminic.budgerapp.repos.UserRepo import space.luminic.budgerapp.utils.JWTUtil import java.time.LocalDateTime import java.time.ZoneId -import java.util.Date +import java.util.* @Service class AuthService( private val userRepository: UserRepo, private val tokenService: TokenService, - private val jwtUtil: JWTUtil + private val jwtUtil: JWTUtil, + private val userService: UserService -) { +) { private val passwordEncoder = BCryptPasswordEncoder() fun login(username: String, password: String): Mono { @@ -82,23 +84,20 @@ class AuthService( ) } - @Cacheable("tokens") - fun isTokenValid(token: String): Mono { - return tokenService.getToken(token) - .flatMap { tokenDetails -> - when { - tokenDetails.status == TokenStatus.ACTIVE && tokenDetails.expiresAt.isAfter(LocalDateTime.now()) -> { - userRepository.findByUsername(tokenDetails.username) - .switchIfEmpty(Mono.error(AuthException("User not found for token"))) - } - else -> { - tokenService.revokeToken(token) - .then(Mono.error(AuthException("Token expired or inactive"))) - } - } + @Cacheable(cacheNames = ["tokens"], key = "#token") + suspend fun isTokenValid(token: String): User { + val tokenDetails = tokenService.getToken(token).awaitFirstOrNull() ?: throw AuthException("Invalid token") + when { + tokenDetails.status == TokenStatus.ACTIVE && tokenDetails.expiresAt.isAfter(LocalDateTime.now()) -> { + return userService.getByUserNameWoPass(tokenDetails.username) } - .switchIfEmpty(Mono.error(AuthException("Token not found"))) + + else -> { + tokenService.revokeToken(tokenDetails.token) + throw AuthException("Token expired or inactive") + } + } } } \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/budgerapp/services/CategoryService.kt b/src/main/kotlin/space/luminic/budgerapp/services/CategoryService.kt index a71e728..29a5ea3 100644 --- a/src/main/kotlin/space/luminic/budgerapp/services/CategoryService.kt +++ b/src/main/kotlin/space/luminic/budgerapp/services/CategoryService.kt @@ -1,22 +1,25 @@ package space.luminic.budgerapp.services +import kotlinx.coroutines.reactive.awaitFirstOrNull +import kotlinx.coroutines.reactive.awaitSingle import org.bson.Document import org.bson.types.ObjectId import org.slf4j.LoggerFactory import org.springframework.cache.annotation.CacheEvict import org.springframework.cache.annotation.Cacheable -import org.springframework.context.ApplicationEventPublisher import org.springframework.data.domain.Sort import org.springframework.data.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.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.models.Category +import space.luminic.budgerapp.models.CategoryType +import space.luminic.budgerapp.models.NotFoundException +import space.luminic.budgerapp.models.Space import space.luminic.budgerapp.repos.BudgetRepo import space.luminic.budgerapp.repos.CategoryRepo @@ -33,13 +36,12 @@ class CategoryService( private val logger = LoggerFactory.getLogger(javaClass) - - fun findCategory( + suspend 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() @@ -51,7 +53,6 @@ class CategoryService( val match = match(Criteria().andOperator(*matchCriteria.toTypedArray())) - // val project = project("_id", "type", "name", "description", "icon") val aggregationBuilder = mutableListOf( @@ -63,10 +64,9 @@ class CategoryService( val aggregation = newAggregation(aggregationBuilder) return mongoTemplate.aggregate( aggregation, "categories", Document::class.java - ).next() - .map { doc -> - categoryMapper.fromDocument(doc) - } + ).map { doc -> + categoryMapper.fromDocument(doc) + }.awaitFirstOrNull() ?: throw NotFoundException("Category not found") } @@ -122,55 +122,56 @@ class CategoryService( @CacheEvict(cacheNames = ["getAllCategories"], allEntries = true) - 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) // Сохраняем категорию, если тип не изменился - } - } + suspend fun editCategory(space: Space, category: Category): Category { + val oldCategory = findCategory(space, id = category.id) - fun deleteCategory(space: Space, categoryId: String): Mono { - return findCategory(space, categoryId).switchIfEmpty( - Mono.error(IllegalArgumentException("Category with id: $categoryId not found")) - ).flatMap { categoryToDelete -> - financialService.getTransactions(space.id!!, categoryId = categoryId) - .flatMapMany { transactions -> - findCategory(space, name = "Другое") - .switchIfEmpty( - categoryRepo.save( - Category( - space = space, - type = CategoryType("EXPENSE", "Траты"), - name = "Другое", - description = "Категория для других трат", - icon = "🚮" - ) - ) - ) - .flatMapMany { otherCategory -> - Flux.fromIterable(transactions).flatMap { transaction -> - transaction.category = otherCategory - financialService.editTransaction(transaction) - } - } - } - .then( - financialService.findProjectedBudgets(ObjectId(space.id)) - .flatMapMany { budgets -> - Flux.fromIterable(budgets).flatMap { budget -> - budget.categories.removeIf { it.category.id == categoryId } - budgetRepo.save(budget) - } - }.collectList() - ) - .then(categoryRepo.deleteById(categoryId)) // Удаление категории - .thenReturn(categoryId) + if (oldCategory.type.code != category.type.code) { + throw IllegalArgumentException("You cannot change category type") } + category.space = space + return categoryRepo.save(category).awaitSingle() // Сохраняем категорию, если тип не изменился } + suspend fun deleteCategory(space: Space, categoryId: String) { + findCategory(space, categoryId) + val transactions = financialService.getTransactions(space.id!!, categoryId = categoryId).awaitSingle() + val otherCategory = try { + findCategory(space, name = "Другое") + } catch (nfe: NotFoundException) { + categoryRepo.save( + Category( + space = space, + type = CategoryType("EXPENSE", "Траты"), + name = "Другое", + description = "Категория для других трат", + icon = "🚮" + ) + ).awaitSingle() + } + transactions.map { transaction -> + transaction.category = otherCategory + financialService.editTransaction(transaction) + } + val budgets = financialService.findProjectedBudgets( + ObjectId(space.id), + projectKeys = arrayOf( + "_id", + "name", + "dateFrom", + "dateTo", + "space", + "spaceDetails", + "categories", + "categoriesDetails", + "incomeCategories", + "incomeCategoriesDetails" + ) + ).awaitSingle() + budgets.map { budget -> + budget.categories.removeIf { it.category.id == categoryId } + budgetRepo.save(budget) + } + categoryRepo.deleteById(categoryId).awaitSingle() + } } diff --git a/src/main/kotlin/space/luminic/budgerapp/services/FinancialService.kt b/src/main/kotlin/space/luminic/budgerapp/services/FinancialService.kt index e18ad24..c9222fc 100644 --- a/src/main/kotlin/space/luminic/budgerapp/services/FinancialService.kt +++ b/src/main/kotlin/space/luminic/budgerapp/services/FinancialService.kt @@ -1,6 +1,16 @@ package space.luminic.budgerapp.services -import com.mongodb.DBRef +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.launch +import kotlinx.coroutines.reactive.asFlow +import kotlinx.coroutines.reactive.awaitFirstOrNull +import kotlinx.coroutines.reactive.awaitSingle +import kotlinx.coroutines.reactor.awaitSingle +import kotlinx.coroutines.reactor.awaitSingleOrNull import org.bson.BsonNull import org.bson.Document import org.bson.types.ObjectId @@ -12,14 +22,11 @@ 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.aggregation.DateOperators.DateToString -import org.springframework.data.mongodb.core.aggregation.SortOperation 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.security.core.context.SecurityContextHolder import org.springframework.stereotype.Service import reactor.core.publisher.Flux import reactor.core.publisher.Mono -import reactor.core.scheduler.Schedulers import space.luminic.budgerapp.mappers.BudgetMapper import space.luminic.budgerapp.mappers.TransactionsMapper import space.luminic.budgerapp.models.* @@ -46,100 +53,120 @@ class FinancialService( private val logger = LoggerFactory.getLogger(FinancialService::class.java) - fun updateBudgetOnCreate(transaction: Transaction): Mono { - return findProjectedBudget( + suspend fun updateBudgetOnCreate(transaction: Transaction) { + val budget = findProjectedBudget( transaction.space!!.id!!, budgetId = null, transaction.date, transaction.date - ).flatMap { budget -> - - val budgetCategory = budget.categories.firstOrNull { it.category.id == transaction.category.id } - if (transaction.category.type.code == "INCOME") { - return@flatMap budgetRepo.save(budget) - } else if (budgetCategory == null) { - return@flatMap Mono.error(RuntimeException("Budget category not found in the budget")) - } - return@flatMap getBudgetSumsByCategory(transaction.category.id!!, budget).flatMap { sums -> - - budgetCategory.currentPlanned = sums.getDouble("plannedAmount") ?: 0.0 - budgetCategory.currentSpent = sums.getDouble("instantAmount") ?: 0.0 - // При совпадении бюджетов разница просто корректирует лимит - if (transaction.type.code == "PLANNED") { - budgetCategory.currentLimit += transaction.amount - } - logger.info("updateBudgetOnCreate end") - budgetRepo.save(budget).then() - } -// - }.then() // Возвращаем корректный Mono + ) + val budgetCategory = budget.categories.firstOrNull { it.category.id == transaction.category.id } + ?: throw NotFoundException("Budget category not found in the budget") + if (transaction.category.type.code == "INCOME") { + budgetRepo.save(budget).awaitSingle() + } + val categorySums = getBudgetSumsByCategory(transaction.category.id!!, budget) + budgetCategory.currentPlanned = categorySums.getDouble("plannedAmount") + budgetCategory.currentSpent = categorySums.getDouble("instantAmount") + // При совпадении бюджетов разница просто корректирует лимит + if (transaction.type.code == "PLANNED") { + budgetCategory.currentLimit += transaction.amount + } + logger.info("updateBudgetOnCreate end") + budgetRepo.save(budget).awaitSingle() } - fun updateBudgetOnEdit( + suspend fun updateBudgetOnEdit( oldTransaction: Transaction, newTransaction: Transaction, difference: Double - ): Mono { + ) = coroutineScope { logger.info("updateBudgetOnEdit start") - return Mono.zip( + val oldBudgetDeffer = async { findProjectedBudget( newTransaction.space?.id!!, budgetId = null, oldTransaction.date, oldTransaction.date - ).switchIfEmpty(Mono.error(BudgetNotFoundException("Old budget not found"))), + ) + } + val newBudgetDeffer = async { findProjectedBudget( newTransaction.space?.id!!, budgetId = null, newTransaction.date, newTransaction.date - ).switchIfEmpty(Mono.error(BudgetNotFoundException("New budget not found"))) - ).flatMap { tuple -> - val oldBudget = tuple.t1 - val newBudget = tuple.t2 - val isSameBudget = oldBudget.id == newBudget.id - - if (isSameBudget) { - updateSameBudget(oldTransaction, newTransaction, difference, newBudget) - } else { - updateDifferentBudgets(oldTransaction, newTransaction, difference, oldBudget, newBudget) - } - }.doOnSuccess { logger.info("updateBudgetOnEdit end") } - } - - private fun updateSameBudget( - oldTransaction: Transaction, newTransaction: Transaction, difference: Double, budget: Budget - ): Mono { - val oldCategory = findBudgetCategory(oldTransaction, budget) - val newCategory = findBudgetCategory(newTransaction, budget) - return if (oldCategory.category.id == newCategory.category.id) { - updateBudgetCategory(newTransaction, newCategory, budget, newTransaction.amount, difference) - .flatMap { Mono.empty() } - - } else { - logger.info("hui" + oldTransaction.category.id + " " + (-oldTransaction.amount).toString()) - Mono.zip( - updateBudgetCategory(oldTransaction, oldCategory, budget, -oldTransaction.amount, -difference), - updateBudgetCategory(newTransaction, newCategory, budget, newTransaction.amount, difference) ) - .flatMap { - val t1 = it.t1 - val t2 = it.t2 + } + val (oldBudget, newBudget) = awaitAll(oldBudgetDeffer, newBudgetDeffer) - logger.info(t1.toString()) - logger.info(t2.toString()) - Mono.empty() - } + val isSameBudget = oldBudget.id == newBudget.id + + if (isSameBudget) { + updateSameBudget(oldTransaction, newTransaction, difference, newBudget) + } else { + updateDifferentBudgets(oldTransaction, newTransaction, difference, oldBudget, newBudget) } } - private fun updateDifferentBudgets( + private suspend fun updateSameBudget( + oldTransaction: Transaction, newTransaction: Transaction, difference: Double, budget: Budget + ) = coroutineScope { + val oldCategory = findBudgetCategory(oldTransaction, budget) + val newCategory = findBudgetCategory(newTransaction, budget) + if (oldCategory.category.id == newCategory.category.id) { + updateBudgetCategory(newTransaction, budget, difference) + + } else { + logger.info("hui" + oldTransaction.category.id + " " + (-oldTransaction.amount).toString()) + val updateOldBudgetCategoryDeffer = + async { + updateBudgetCategory( + oldTransaction, + budget, + -difference, + isCategoryChanged = true, + isOldCategory = true + ) + } + val updateNewBudgetCategoryDeffer = + async { + updateBudgetCategory( + newTransaction, + budget, + difference, + true + ) + } + val (updateOldBudgetCategory, updateNewBudgetCategory) = awaitAll( + updateNewBudgetCategoryDeffer, + updateOldBudgetCategoryDeffer + ) + logger.info(updateOldBudgetCategory.toString()) + logger.info(updateNewBudgetCategory.toString()) + } + } + + + private suspend fun updateDifferentBudgets( oldTransaction: Transaction, newTransaction: Transaction, difference: Double, oldBudget: Budget, newBudget: Budget - ): Mono { + ) = coroutineScope { val oldCategory = findBudgetCategory(oldTransaction, oldBudget) val newCategory = findBudgetCategory(newTransaction, newBudget) - return Mono.`when`( - updateBudgetCategory(oldTransaction, oldCategory, oldBudget, -oldTransaction.amount, -difference), - updateBudgetCategory(newTransaction, newCategory, newBudget, newTransaction.amount, difference) - ) - + if (oldCategory.category.id == newCategory.category.id) { + async { + updateBudgetCategory(oldTransaction, oldBudget, -difference) + updateBudgetCategory(newTransaction, newBudget, difference) + } + } else { + async { + updateBudgetCategory( + oldTransaction, + oldBudget, + -difference, + isCategoryChanged = true, + isOldCategory = true + ) + updateBudgetCategory(newTransaction, newBudget, difference) + } + } } private fun findBudgetCategory(transaction: Transaction, budget: Budget): BudgetCategory { @@ -152,72 +179,86 @@ class FinancialService( } } - private fun updateBudgetCategory( - transaction: Transaction, category: BudgetCategory, budget: Budget, tAmount: Double, difference: Double - ): Mono { - return getBudgetSumsByCategory(transaction.category.id!!, budget) - .defaultIfEmpty(Document("plannedAmount", 0.0).append("instantAmount", 0.0)) // Если данных нет, заполняем 0 - .flatMap { sums -> - category.currentPlanned = sums.getDouble("plannedAmount") ?: 0.0 - category.currentSpent = sums.getDouble("instantAmount") ?: 0.0 - - if (transaction.type.code == "PLANNED") { - category.currentLimit += difference - } - - budgetRepo.save(budget).thenReturn(1.0) // Дожидаемся сохранения, возвращаем 1.0 - } + private suspend fun updateBudgetCategory( + transaction: Transaction, + budget: Budget, + difference: Double, + isCategoryChanged: Boolean = false, + isOldCategory: Boolean = false + ): Double { + val sums = getBudgetSumsByCategory(transaction.category.id!!, budget) + val categoryBudget = budget.categories.firstOrNull { it.category.id == transaction.category.id } + ?: throw NotFoundException("Not found category in budget") + categoryBudget.currentPlanned = sums.getDouble("plannedAmount") + categoryBudget.currentSpent = sums.getDouble("instantAmount") + if (transaction.type.code == "PLANNED") { + if (isCategoryChanged) { + if (isOldCategory) { + categoryBudget.currentLimit -= transaction.amount + } else categoryBudget.currentLimit += transaction.amount + } else categoryBudget.currentLimit += difference + } + budgetRepo.save(budget).awaitSingle() + return 1.0 } - fun updateBudgetOnDelete(transaction: Transaction): Mono { - return findProjectedBudget( - transaction.space!!.id!!, budgetId = null, transaction.date, transaction.date - ).flatMap { budget -> - getBudgetCategories(transaction.space?.id!!, budget.dateFrom, budget.dateTo).flatMap { categories -> - val updatedCategoriesMono: Mono> = when (transaction.type.code) { - "PLANNED" -> Flux.fromIterable(budget.categories).map { category -> - if (category.category.id == transaction.category.id) { - categories[category.category.id]?.let { data -> - category.currentSpent = data["instantAmount"] ?: 0.0 - category.currentPlanned = data["plannedAmount"] ?: 0.0 - category.currentLimit -= transaction.amount - } - } - category - }.collectList() + suspend fun updateBudgetOnDelete(transaction: Transaction) { + // Найдем бюджет по дате + val budget = findProjectedBudget(transaction.space!!.id!!, budgetId = null, transaction.date, transaction.date) - "INSTANT" -> Flux.fromIterable(budget.categories).map { category -> - if (category.category.id == transaction.category.id) { - categories[category.category.id]?.let { data -> - category.currentSpent = data["instantAmount"] ?: 0.0 - category.currentPlanned = data["plannedAmount"] ?: 0.0 - } - } - category - }.collectList() - else -> Mono.just(budget.categories) - } - - updatedCategoriesMono.flatMap { updated -> - budget.categories = updated.toMutableList() - budgetRepo.save(budget).then() // Гарантируем завершение + // Получаем категории бюджета + val budgetCategories = + getBudgetCategories(transaction.space?.id!!, budget.dateFrom, budget.dateTo) + ?: throw NotFoundException("Budget category not found in the budget") + + // Обрабатываем категории в зависимости от типа транзакции + val updatedCategories = when (transaction.type.code) { + "PLANNED" -> budget.categories.map { category -> + if (category.category.id == transaction.category.id) { + budgetCategories[category.category.id]?.let { data -> + category.currentSpent = data["instantAmount"] ?: 0.0 + category.currentPlanned = data["plannedAmount"] ?: 0.0 + category.currentLimit -= transaction.amount // Корректируем лимит по расходу + } } + category // Возвращаем обновленную категорию } - }.then() // Возвращаем корректный Mono + + "INSTANT" -> budget.categories.map { category -> + if (category.category.id == transaction.category.id) { + budgetCategories[category.category.id]?.let { data -> + category.currentSpent = data["instantAmount"] ?: 0.0 + category.currentPlanned = data["plannedAmount"] ?: 0.0 + } + } + category // Возвращаем обновленную категорию + } + + else -> budget.categories + } + + // Сохраняем обновленные категории в бюджете + budget.categories = updatedCategories.toMutableList() // Обновляем категории + budgetRepo.save(budget).awaitSingle() } + fun getBudgets(spaceId: String, sortSetting: SortSetting? = null): Mono> { val sort = sortSetting?.let { Sort.by(it.order, it.by) } ?: Sort.by(Direction.DESC, "dateFrom") - return findProjectedBudgets(ObjectId(spaceId), sort) + return findProjectedBudgets(ObjectId(spaceId), sortRequested = sort) } - fun findProjectedBudgets(spaceId: ObjectId, sortRequested: Sort? = null): Mono> { + fun findProjectedBudgets( + spaceId: ObjectId, + projectKeys: Array = arrayOf("_id", "name", "dateFrom", "dateTo"), + sortRequested: Sort? = null + ): Mono> { val lookupCategories = lookup("categories", "categories.category.\$id", "_id", "categoriesDetails") @@ -228,12 +269,20 @@ class FinancialService( val unwindSpace = unwind("spaceDetails") val matchStage = match(Criteria.where("spaceDetails._id").`is`(spaceId)) // matchCriteria.add(Criteria.where("spaceDetails._id").`is`(ObjectId(spaceId))) - val projectStage = project("_id", "name", "dateFrom", "dateTo") // Оставляем только нужные поля + val projectStage = project(*projectKeys) // Оставляем только нужные поля val sort = sortRequested?.let { sort(it) } ?: sort( 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, + projectStage, + sort + ) return reactiveMongoTemplate.aggregate(aggregation, "budgets", Document::class.java).collectList().map { docs -> docs.map { doc -> @@ -242,15 +291,15 @@ class FinancialService( } } - fun findProjectedBudget( + suspend fun findProjectedBudget( spaceId: String, budgetId: String? = null, dateFrom: LocalDate? = null, dateTo: LocalDate? = null - ): Mono { + ): Budget { val lookupCategories = lookup("categories", "categories.category.\$id", "_id", "categoriesDetails") - val unwindCategories = unwind("categoriesDetails") +// val unwindCategories = unwind("categoriesDetails") val lookupIncomeCategories = lookup("categories", "incomeCategories.category.\$id", "_id", "incomeCategoriesDetails") - val unwindIncomeCategories = unwind("incomeCategoriesDetails") +// val unwindIncomeCategories = unwind("incomeCategoriesDetails") val lookupSpace = lookup("spaces", "space.\$id", "_id", "spaceDetails") val unwindSpace = unwind("spaceDetails") val matchCriteria = mutableListOf() @@ -263,57 +312,45 @@ class FinancialService( return reactiveMongoTemplate.aggregate(aggregation, "budgets", Document::class.java).next().map { doc -> budgetMapper.fromDocument(doc) - } + }.awaitSingleOrNull() ?: throw NotFoundException("Budget not found") } // @Cacheable("budgets", key = "#id") - fun getBudget(spaceId: String, id: String): Mono { - return findProjectedBudget( - spaceId, - id - ).switchIfEmpty(Mono.error(IllegalArgumentException("Budget not found for spaceId: $spaceId and budgetId: $id"))) - .flatMap { budget -> + suspend fun getBudget(spaceId: String, id: String): BudgetDTO = coroutineScope { + val budget = findProjectedBudget(spaceId, id) - // Если доступ есть, продолжаем процесс - val budgetDTO = BudgetDTO( - budget.id, - budget.space, - budget.name, - budget.dateFrom, - budget.dateTo, - budget.createdAt, - categories = budget.categories, - incomeCategories = budget.incomeCategories, - ) + // Если доступ есть, продолжаем процесс + val budgetDTO = BudgetDTO( + budget.id, + budget.space, + budget.name, + budget.dateFrom, + budget.dateTo, + budget.createdAt, + categories = budget.categories, + incomeCategories = budget.incomeCategories, + ) + logger.info("Fetching categories and transactions") - logger.info("Fetching categories and transactions") + val categoriesDeffer = async { getBudgetCategories(spaceId, budgetDTO.dateFrom, budgetDTO.dateTo) } + val transactionsDeffer = async { getTransactionsByTypes(spaceId, budgetDTO.dateFrom, budgetDTO.dateTo) } - val categoriesMono = getBudgetCategories(spaceId, budgetDTO.dateFrom, budgetDTO.dateTo) - val transactionsMono = getTransactionsByTypes(spaceId, budgetDTO.dateFrom, budgetDTO.dateTo) + val categories = categoriesDeffer.await() ?: throw NotFoundException("Categories in null") + val transactions = transactionsDeffer.await() ?: throw NotFoundException("Categories in null") + val updatedCategories = budgetDTO.categories.map { category -> + categories.get(category.category.id)?.let { data -> + category.currentSpent = data["instantAmount"] ?: 0.0 + category.currentPlanned = data["plannedAmount"] ?: 0.0 + } + category // Возвращаем обновленную категорию + } + budgetDTO.categories = updatedCategories.toMutableList() + budgetDTO.plannedExpenses = transactions.get("plannedExpenses") as MutableList + budgetDTO.plannedIncomes = transactions["plannedIncomes"] as MutableList + budgetDTO.transactions = transactions["instantTransactions"] as MutableList - Mono.zip(categoriesMono, transactionsMono).flatMap { tuple -> - val categories = tuple.t1 - val transactions = tuple.t2 - - Flux.fromIterable(budgetDTO.categories).map { category -> - categories[category.category.id]?.let { data -> - category.currentSpent = data["instantAmount"] ?: 0.0 - category.currentPlanned = data["plannedAmount"] ?: 0.0 - } - category - }.collectList().map { updatedCategories -> - budgetDTO.categories = updatedCategories - budgetDTO.plannedExpenses = transactions["plannedExpenses"] as MutableList - budgetDTO.plannedIncomes = transactions["plannedIncomes"] as MutableList - budgetDTO.transactions = transactions["instantTransactions"] as MutableList - - budgetDTO - } - } - }.doOnError { error -> - logger.error("Error fetching budget: ${error.message}", error) - }.switchIfEmpty(Mono.error(BudgetNotFoundException("Budget not found with id: $id"))) + budgetDTO } @@ -357,7 +394,20 @@ class FinancialService( category.space = space return categoryRepo.save(category) .flatMap { savedCategory -> - findProjectedBudgets(ObjectId(space.id)) + findProjectedBudgets( + ObjectId(space.id), projectKeys = arrayOf( + "_id", + "name", + "dateFrom", + "dateTo", + "space", + "spaceDetails", + "categories", + "categoriesDetails", + "incomeCategories", + "incomeCategoriesDetails" + ) + ) .flatMapMany { Flux.fromIterable(it) } // Преобразуем List в Flux .flatMap { budget -> when (savedCategory.type.code) { @@ -371,70 +421,50 @@ class FinancialService( } - 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, 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) + suspend fun createBudget(space: Space, budget: Budget, createRecurrent: Boolean): Budget = coroutineScope { + val startBudgetDeferred = async { getBudgetByDate(budget.dateFrom, space.id!!) } + val endBudgetDeferred = async { getBudgetByDate(budget.dateTo, space.id!!) } - // Проверяем, пересекаются ли бюджеты по датам - if (startBudget != null || endBudget != null) { - return@flatMap Mono.error(IllegalArgumentException("Бюджет с теми же датами найден")) + val (startBudget, endBudget) = awaitAll(startBudgetDeferred, endBudgetDeferred) + + if (startBudget != null || endBudget != null) { + throw IllegalArgumentException("Бюджет с теми же датами найден") + } + budget.space = space + + if (createRecurrent) { + recurrentService.createRecurrentsForBudget(space, budget) + } + val categoriesDeferred = + async { getCategoryTransactionPipeline(space.id!!, budget.dateFrom, budget.dateTo) } + val incomeCategoriesDeferred = + async { getCategoryTransactionPipeline(space.id!!, budget.dateFrom, budget.dateTo, "INCOME") } + budget.categories = categoriesDeferred.await().toMutableList() + val savedBudget = budgetRepo.save(budget).awaitSingle() + launch { + try { + updateBudgetWarns(savedBudget) + } catch (error: Exception) { + logger.error("Error during updateBudgetWarns: ${error.message}") } + } + savedBudget.incomeCategories = incomeCategoriesDeferred.await().toMutableList() - // Получаем Space по spaceId - - - // Присваиваем Space бюджету - budget.space = space - - // Если createRecurrent=true, создаем рекуррентные транзакции - val recurrentsCreation = if (createRecurrent) { - recurrentService.createRecurrentsForBudget(space, budget) - } else { - Mono.empty() + return@coroutineScope budgetRepo.save(savedBudget).awaitSingle().also { + launch { + try { + updateBudgetWarns(it) + } catch (error: Exception) { + logger.error("Error during updateBudgetWarns: ${error.message}") + } } - - // Создаем бюджет после возможного создания рекуррентных транзакций - 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 { + suspend fun getBudgetByDate(date: LocalDate, spaceId: String): Budget? { return budgetRepo.findByDateFromLessThanEqualAndDateToGreaterThanEqualAndSpace(date, date, ObjectId(spaceId)) - .switchIfEmpty(Mono.empty()) + .awaitSingleOrNull() } @@ -464,42 +494,32 @@ class FinancialService( // } - fun deleteBudget(spaceId: String, budgetId: String): Mono { - return findProjectedBudget( - spaceId, - budgetId - ).switchIfEmpty(Mono.error(IllegalArgumentException("Budget not found for spaceId: $spaceId and budgetId: $budgetId"))) - .flatMap { budget -> - getTransactionsToDelete(spaceId, budget.dateFrom, budget.dateTo).flatMapMany { transactions -> - Flux.fromIterable(transactions).flatMap { transaction -> - deleteTransaction(transaction) - } - }.then( - budgetRepo.deleteById(budget.id!!) - ) - } + suspend fun deleteBudget(spaceId: String, budgetId: String) { + val budget = findProjectedBudget(spaceId, budgetId) + getTransactionsToDelete(spaceId, budget.dateFrom, budget.dateTo).map { + deleteTransaction(it) + } + budgetRepo.deleteById(budget.id!!) } - fun setCategoryLimit(spaceId: String, budgetId: String, catId: String, limit: Double): Mono { - return findProjectedBudget(spaceId, budgetId = budgetId).flatMap { budget -> - val catEdit = - budget.categories.firstOrNull { it.category.id == catId } ?: return@flatMap Mono.error( - Exception("Category not found in the budget") - ) - - calcTransactionsSum(spaceId, budget, catId, "PLANNED").flatMap { catPlanned -> - if (catPlanned > limit) { - Mono.error(Exception("Limit can't be less than planned expenses on category. Current planned value: $catPlanned")) - } else { - catEdit.currentLimit = limit - budgetRepo.save(budget).flatMap { - updateBudgetWarns(it).thenReturn(catEdit) - } + suspend fun setCategoryLimit(spaceId: String, budgetId: String, catId: String, limit: Double): BudgetCategory = + coroutineScope { + val budget = findProjectedBudget(spaceId, budgetId = budgetId) + val categoryToEdit = budget.categories.firstOrNull { it.category.id == catId } + ?: throw NotFoundException("Category not found in the budget") + val transactionSumsByCategory = calcTransactionsSum(spaceId, budget, catId, "PLANNED").awaitSingle() + if (transactionSumsByCategory > limit) { + throw IllegalArgumentException("Limit can't be less than planned expenses on category. Current planned value: $transactionSumsByCategory") + } else { + categoryToEdit.currentLimit = limit + launch { + updateBudgetWarns(budget) } + budgetRepo.save(budget).awaitSingle() + categoryToEdit } } - } fun getWarns(budgetId: String, isHide: Boolean? = null): Mono> { return warnRepo.findAllByBudgetIdAndIsHide(budgetId, isHide == true).collectList() @@ -534,11 +554,18 @@ class FinancialService( budget.space!!.id!!, finalBudget, categoryType = "INCOME", transactionType = "PLANNED" ) val plannedSavingMono = calcTransactionsSum( - budget.space!!.id!!, finalBudget, categoryId = "675850148198643f121e466a", transactionType = "PLANNED" + budget.space!!.id!!, + finalBudget, + categoryId = "675850148198643f121e466a", + transactionType = "PLANNED" ) Mono.zip( - averageSumsMono, averageIncomeMono, currentBudgetIncomeMono, plannedIncomeMono, plannedSavingMono + averageSumsMono, + averageIncomeMono, + currentBudgetIncomeMono, + plannedIncomeMono, + plannedSavingMono ).flatMap { tuple -> val averageSums = tuple.t1 val averageIncome = tuple.t2 @@ -621,7 +648,8 @@ class FinancialService( ) warnsForCategory.add(warnMono) } else { - warnRepo.findWarnByContext(lowSavingContext).flatMap { warnRepo.delete(it).then(Mono.empty()) } + warnRepo.findWarnByContext(lowSavingContext) + .flatMap { warnRepo.delete(it).then(Mono.empty()) } } } @@ -707,11 +735,7 @@ class FinancialService( } - fun getTransactionByParentId( - parentId: String - ): Mono { - - + suspend fun getTransactionByParentId(parentId: String): Transaction { // Сборка агрегации val lookup = lookup("categories", "category.\$id", "_id", "categoryDetails") val unwindCategory = unwind("categoryDetails") @@ -731,20 +755,22 @@ class FinancialService( unwindUser, match ) - val aggregation = newAggregation(aggregationBuilder) return reactiveMongoTemplate.aggregate( aggregation, "transactions", Document::class.java ).map { doc -> - transactionsMapper.fromDocument(doc) - }.next() + }.awaitFirstOrNull() ?: throw NotFoundException("Child transaction with parent id $parentId not found") } - fun getTransactionsToDelete(spaceId: String, dateFrom: LocalDate, dateTo: LocalDate): Mono> { + suspend fun getTransactionsToDelete( + spaceId: String, + dateFrom: LocalDate, + dateTo: LocalDate + ): List { val matchCriteria = mutableListOf() val lookupSpaces = lookup("spaces", "space.\$id", "_id", "spaceDetails") val unwindSpace = unwind("spaceDetails") @@ -771,11 +797,11 @@ class FinancialService( docs.map { doc -> transactionsMapper.fromDocument(doc) } - } + }.awaitSingle() } - fun getTransactionById(id: String): Mono { + suspend fun getTransactionById(id: String): Transaction { val lookupSpaces = lookup("spaces", "space.\$id", "_id", "spaceDetails") val unwindSpaces = unwind("spaceDetails") val lookupCategory = lookup("categories", "category.\$id", "_id", "categoryDetails") @@ -791,93 +817,73 @@ class FinancialService( ) val aggregation = newAggregation(aggregationBuilder) - return reactiveMongoTemplate.aggregate(aggregation, "transactions", Document::class.java).next().map { doc -> - transactionsMapper.fromDocument(doc) - } + return reactiveMongoTemplate.aggregate(aggregation, "transactions", Document::class.java).next() + .map { doc -> + transactionsMapper.fromDocument(doc) + }.awaitSingleOrNull() ?: throw NotFoundException("Transaction with id $id not found") } - fun createTransaction(space: Space, transaction: Transaction): Mono { - return ReactiveSecurityContextHolder.getContext().map { it.authentication }.flatMap { authentication -> - val username = authentication.name - 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 перед возвратом - } - } + suspend fun createTransaction(space: Space, transaction: Transaction): Transaction { + val securityContextHolder = SecurityContextHolder.getContext() + val user = userService.getByUserNameWoPass(securityContextHolder.authentication.name) + if (space.users.none { it.id.toString() == user.id }) { + throw IllegalArgumentException("User does not have access to this Space") } + // Привязываем space и user к транзакции + transaction.user = user + transaction.space = space + + val savedTransaction = transactionsRepo.save(transaction).awaitSingle() + updateBudgetOnCreate(savedTransaction) + return savedTransaction } @CacheEvict(cacheNames = ["transactions", "budgets"], allEntries = true) - fun editTransaction(transaction: Transaction): Mono { - return getTransactionById(transaction.id!!).flatMap { oldStateOfTransaction -> - val changed = compareSumDateDoneIsChanged(oldStateOfTransaction, transaction) - if (!changed) { - return@flatMap transactionsRepo.save(transaction) // Сохраняем, если изменений нет - } + suspend fun editTransaction(transaction: Transaction): Transaction { + val oldStateOfTransaction = getTransactionById(transaction.id!!) + val changed = compareSumDateDoneIsChanged(oldStateOfTransaction, transaction) + if (!changed) { + return transactionsRepo.save(transaction).awaitSingle() + } + val amountDifference = transaction.amount - oldStateOfTransaction.amount - val amountDifference = transaction.amount - oldStateOfTransaction.amount - - // Обработка дочерней транзакции + if (oldStateOfTransaction.isDone && oldStateOfTransaction.type.code == "PLANNED") { handleChildTransaction( oldStateOfTransaction, transaction - ).then(transactionsRepo.save(transaction)) // Сохраняем основную транзакцию - .flatMap { savedTransaction -> - updateBudgetOnEdit(oldStateOfTransaction, savedTransaction, amountDifference).thenReturn( - savedTransaction - ) // Ждем выполнения updateBudgetOnEdit и возвращаем транзакцию - } - }.switchIfEmpty( - Mono.error(IllegalArgumentException("Transaction not found with id: ${transaction.id}")) - ) + ) + } + val savedTransaction = transactionsRepo.save(transaction).awaitSingle() + updateBudgetOnEdit(oldStateOfTransaction, savedTransaction, amountDifference) + return savedTransaction } - private fun handleChildTransaction( - oldTransaction: Transaction, newTransaction: Transaction - ): Mono { - return getTransactionByParentId( - newTransaction.id!! - ).flatMap { childTransaction -> - logger.info("Updating child: $childTransaction") - val updatedChild = childTransaction.copy( - amount = newTransaction.amount, - category = newTransaction.category, - comment = newTransaction.comment, - user = newTransaction.user + + private suspend fun handleChildTransaction(oldTransaction: Transaction, newTransaction: Transaction) { + val childTransaction = getTransactionByParentId(newTransaction.id!!) + + logger.info("Updating child: $childTransaction") + val updatedChild = childTransaction.copy( + amount = newTransaction.amount, + category = newTransaction.category, + comment = newTransaction.comment, + user = newTransaction.user + ) + transactionsRepo.save(updatedChild).awaitSingle() + + if (!oldTransaction.isDone && newTransaction.isDone) { + val newChildTransaction = newTransaction.copy( + id = null, type = TransactionType("INSTANT", "Текущие"), parentId = newTransaction.id ) - transactionsRepo.save(updatedChild).then() - }.switchIfEmpty( - Mono.defer { - if (!oldTransaction.isDone && newTransaction.isDone) { - val newChildTransaction = newTransaction.copy( - id = null, type = TransactionType("INSTANT", "Текущие"), parentId = newTransaction.id - ) - return@defer transactionsRepo.save(newChildTransaction).flatMap { updateBudgetOnCreate(it) } - .then() - } else { - return@defer Mono.empty() // Используем Mono.empty() для пустой операции - } - }).then( - Mono.defer { - if (oldTransaction.isDone && !newTransaction.isDone) { - getTransactionByParentId(newTransaction.id!!) - .flatMap { child -> deleteTransaction(child) }.then() - } else { - return@defer Mono.empty() // Вернем пустую операцию, если условия не выполняются - } - }) + transactionsRepo.save(newChildTransaction).awaitSingle() + updateBudgetOnCreate(newChildTransaction) + + } else if (oldTransaction.isDone && !newTransaction.isDone) { + deleteTransaction(childTransaction) + } } @@ -896,10 +902,10 @@ class FinancialService( } - fun deleteTransaction(transaction: Transaction): Mono { - return transactionsRepo.deleteById(transaction.id!!) // Удаляем транзакцию - .then(Mono.defer { updateBudgetOnDelete(transaction) }) - .then() // Завершаем Mono, так как нам не нужно возвращать результат + suspend fun deleteTransaction(transaction: Transaction) = coroutineScope { + transactionsRepo.deleteById(transaction.id!!).awaitSingle() + launch { updateBudgetOnDelete(transaction) } + } @@ -931,10 +937,10 @@ class FinancialService( // } - @Cacheable("childTransactions", key = "#parentId") - fun getChildTransaction(parentId: String): Mono { - return transactionsRepo.findByParentId(parentId) - } +// @Cacheable("childTransactions", key = "#parentId") +// fun getChildTransaction(parentId: String): Mono { +// return transactionsRepo.findByParentId(parentId) +// } // fun getTransactionByOldId(id: Int): Transaction? { // return transactionsRepo.findByOldId(id).getOrNull() @@ -975,7 +981,8 @@ class FinancialService( val project = project("category").andExpression("{ \$toDouble: \"\$amount\" }").`as`("amount") val group = group(categoryId ?: "all").sum("amount").`as`("totalSum") val projectSum = project("totalSum") - val aggregation = newAggregation(lookup, unwind, lookupSpace, unwindSpace, match, project, group, projectSum) + val aggregation = + newAggregation(lookup, unwind, lookupSpace, unwindSpace, match, project, group, projectSum) return reactiveMongoTemplate.aggregate(aggregation, "transactions", Map::class.java).map { result -> val totalSum = result["totalSum"] @@ -995,11 +1002,14 @@ class FinancialService( val lookup = lookup("categories", "category.\$id", "_id", "categoryDetails") val unwind = unwind("categoryDetails") val match = match( - Criteria.where("categoryDetails.type.code").`is`("EXPENSE").and("type.code").`is`("INSTANT").and("date") + Criteria.where("categoryDetails.type.code").`is`("EXPENSE").and("type.code").`is`("INSTANT") + .and("date") .lt(firstDateOfMonth) ) val projectDate = - project("_id", "category", "amount", "categoryDetails").and(DateToString.dateOf("date").toString("%Y-%m")) + project("_id", "category", "amount", "categoryDetails").and( + DateToString.dateOf("date").toString("%Y-%m") + ) .`as`("month").andExpression("{ \$toDouble: \"\$amount\" }").`as`("amount") val groupByMonthAndCategory = group("month", "category.\$id").sum("amount").`as`("sum") val groupByCategory = group("_id.id").avg("sum").`as`("averageAmount") @@ -1035,12 +1045,14 @@ class FinancialService( val unwind = unwind("detailedCategory") val match = match( - Criteria.where("detailedCategory.type.code").`is`("INCOME").and("type.code").`is`("INSTANT").and("isDone") + Criteria.where("detailedCategory.type.code").`is`("INCOME").and("type.code").`is`("INSTANT") + .and("isDone") .`is`(true) ) - val project = project("_id", "category", "detailedCategory").and(DateToString.dateOf("date").toString("%Y-%m")) - .`as`("month").andExpression("{ \$toDouble: \"\$amount\" }").`as`("amount") + val project = + project("_id", "category", "detailedCategory").and(DateToString.dateOf("date").toString("%Y-%m")) + .`as`("month").andExpression("{ \$toDouble: \"\$amount\" }").`as`("amount") val groupByMonth = group("month").sum("amount").`as`("sum") @@ -1056,13 +1068,14 @@ class FinancialService( } - fun getTransactionsByTypes( + suspend fun getTransactionsByTypes( spaceId: String, dateFrom: LocalDate, dateTo: LocalDate - ): Mono>> { + ): Map>? = coroutineScope { val pipeline = listOf( Document( "\$lookup", - Document("from", "categories").append("localField", "category.\$id").append("foreignField", "_id") + Document("from", "categories").append("localField", "category.\$id") + .append("foreignField", "_id") .append("as", "categoryDetailed") ), Document( "\$lookup", @@ -1102,7 +1115,8 @@ class FinancialService( "\$facet", Document( "plannedExpenses", listOf( Document( - "\$match", Document("type.code", "PLANNED").append("categoryDetailed.type.code", "EXPENSE") + "\$match", + Document("type.code", "PLANNED").append("categoryDetailed.type.code", "EXPENSE") ), Document("\$sort", Document("date", 1).append("_id", 1)) ) ).append( @@ -1121,47 +1135,35 @@ class FinancialService( ) ) -// getCategoriesExplainReactive(pipeline) -// .doOnNext { explainResult -> -// logger.info("Explain Result: ${explainResult.toJson()}") -// } -// .subscribe() // Этот вызов лучше оставить только для отладки - return reactiveMongoTemplate.getCollection("transactions") - .flatMapMany { it.aggregate(pipeline, Document::class.java) } - .single() // Получаем только первый результат агрегации - .flatMap { aggregationResult -> - Mono.zip( - extractTransactions(aggregationResult, "plannedExpenses"), - extractTransactions(aggregationResult, "plannedIncomes"), - extractTransactions(aggregationResult, "instantTransactions") - ).map { tuple -> - val plannedExpenses = tuple.t1 - val plannedIncomes = tuple.t2 - val instantTransactions = tuple.t3 - mapOf( - "plannedExpenses" to plannedExpenses, - "plannedIncomes" to plannedIncomes, - "instantTransactions" to instantTransactions - ) - - - } - - } + val collection = reactiveMongoTemplate.getCollection("transactions").awaitSingle() + val aggregationResult = collection.aggregate(pipeline, Document::class.java).awaitSingle() + val plannedExpensesDeffer = async { extractTransactions(aggregationResult, "plannedExpenses") } + val plannedIncomesDeffer = async { extractTransactions(aggregationResult, "plannedIncomes") } + val instantTransactionsDeffer = async { extractTransactions(aggregationResult, "instantTransactions") } + val (plannedExpenses, plannedIncomes, instantTransactions) = awaitAll( + plannedExpensesDeffer, + plannedIncomesDeffer, + instantTransactionsDeffer + ) + mapOf( + "plannedExpenses" to plannedExpenses, + "plannedIncomes" to plannedIncomes, + "instantTransactions" to instantTransactions + ) } - private fun extractTransactions(aggregationResult: Document, key: String): Mono> { + private suspend fun extractTransactions(aggregationResult: Document, key: String): List { val resultTransactions = aggregationResult[key] as? List ?: emptyList() - return Flux.fromIterable(resultTransactions).map { documentToTransactionMapper(it) }.collectList() + return resultTransactions.map { documentToTransactionMapper(it) } } private fun documentToTransactionMapper(document: Document): Transaction { val transactionType = document["type"] as Document - var user: User? + val user: User? val userDocument = document["userDetailed"] as Document user = User( @@ -1172,7 +1174,8 @@ class FinancialService( tgUserName = userDocument["tgUserName"]?.let { it as String }, password = null, isActive = userDocument["isActive"] as Boolean, - regDate = (userDocument["regDate"] as Date).toInstant().atZone(ZoneId.systemDefault()).toLocalDate(), + regDate = (userDocument["regDate"] as Date).toInstant().atZone(ZoneId.systemDefault()) + .toLocalDate(), createdAt = (userDocument["createdAt"] as Date).toInstant().atZone(ZoneId.systemDefault()) .toLocalDateTime(), roles = userDocument["roles"] as ArrayList, @@ -1206,7 +1209,7 @@ class FinancialService( } - fun getBudgetSumsByCategory(categoryId: String, budget: Budget): Mono { + suspend fun getBudgetSumsByCategory(categoryId: String, budget: Budget): Document { logger.info("getting budget sums for category $categoryId") val pipeline = listOf( Document( @@ -1227,13 +1230,15 @@ class FinancialService( "\$group", Document("_id", BsonNull()).append( "plannedAmount", Document( "\$sum", Document( - "\$cond", listOf(Document("\$eq", listOf("\$type.code", "PLANNED")), "\$amount", 0.0) + "\$cond", + listOf(Document("\$eq", listOf("\$type.code", "PLANNED")), "\$amount", 0.0) ) ) ).append( "instantAmount", Document( "\$sum", Document( - "\$cond", listOf(Document("\$eq", listOf("\$type.code", "INSTANT")), "\$amount", 0.0) + "\$cond", + listOf(Document("\$eq", listOf("\$type.code", "INSTANT")), "\$amount", 0.0) ) ) ) @@ -1242,20 +1247,14 @@ class FinancialService( ) ) - return reactiveMongoTemplate.getCollection("transactions") // Исправлено на transactions - .flatMapMany { - val result = it.aggregate(pipeline) - logger.info("aggregate: $pipeline") - result - }.map { - logger.info("getting budget sums for category $categoryId end") - it - }.next() // Берём первый документ, а не весь список + val collection = reactiveMongoTemplate.getCollection("transactions").awaitSingle() + val result = collection.aggregate(pipeline).awaitFirstOrNull() + return result ?: Document("plannedAmount", 0.0).append("instantAmount", 0.0) } - fun getBudgetCategories( + suspend fun getBudgetCategories( spaceId: String, dateFrom: LocalDate, dateTo: LocalDate - ): Mono>> { + ): Map>? { val pipeline = listOf( Document( "\$lookup", @@ -1338,34 +1337,23 @@ class FinancialService( ) - // Анализ плана выполнения (вывод для отладки) -// getCategoriesExplainReactive(pipeline) -// .doOnNext { explainResult -> -// logger.info("Explain Result: ${explainResult.toJson()}") -// } -// .subscribe() // Этот вызов лучше оставить только для отладки -// - - - return reactiveMongoTemplate.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) - } + val collection = reactiveMongoTemplate.getCollection("categories").awaitSingle() + val aggregation = collection.aggregate(pipeline, Document::class.java).asFlow().map { 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 и values в виде пары, чтобы дальше с ними можно было работать + id to values + }.toList().toMap() + return aggregation } - fun getCategoryTransactionPipeline( + suspend fun getCategoryTransactionPipeline( spaceId: String, dateFrom: LocalDate, dateTo: LocalDate, catType: String? = "EXPENSE" - ): Mono> { + ): List { val pipeline = listOf( Document( "\$lookup", @@ -1415,12 +1403,14 @@ class FinancialService( ).append("as", "transactionSums") ), Document( "\$project", - Document("_id", 1L).append("type", 1L).append("name", 1L).append("description", 1L).append("icon", 1L) + 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( + "\$filter", + Document("input", "\$transactionSums").append("as", "sum").append( "cond", Document("\$eq", listOf("\$\$sum._id", "PLANNED")) ) ), 0.0 @@ -1430,7 +1420,8 @@ class FinancialService( "instantAmount", Document( "\$arrayElemAt", listOf( Document( - "\$filter", Document("input", "\$transactionSums").append("as", "sum").append( + "\$filter", + Document("input", "\$transactionSums").append("as", "sum").append( "cond", Document("\$eq", listOf("\$\$sum._id", "INSTANT")) ) ), 0.0 @@ -1446,154 +1437,166 @@ class FinancialService( ) ) - return reactiveMongoTemplate.getCollection("categories") - .flatMapMany { it.aggregate(pipeline, Document::class.java) }.map { document -> - val catType = document["type"] as Document + val collection = reactiveMongoTemplate.getCollection("categories").awaitSingle() + return collection.aggregate(pipeline, Document::class.java) + .asFlow() // Преобразуем Flux в Flow + .map { document -> + val categoryType = 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), + id = document["_id"].toString(), + type = CategoryType(categoryType["code"] as String, categoryType["name"] as String), name = document["name"] as String, description = document["description"] as String, icon = document["icon"] as String ) ) - }.collectList().map { it.toMutableList() } + }.toList() } - 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", 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", 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") - ) - ) +// 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", 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", 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 reactiveMongoTemplate.getCollection("transactions").flatMapMany { it.aggregate(pipeline) }.map { +// it["categoryId"] = it["categoryId"].toString() +// it +// }.collectList() +// } - return reactiveMongoTemplate.getCollection("transactions").flatMapMany { it.aggregate(pipeline) }.map { - it["categoryId"] = it["categoryId"].toString() - it - }.collectList() - } - - fun getCategorySummaries(spaceId: String, dateFrom: LocalDate): Mono> { + suspend fun getCategorySummaries(spaceId: String, dateFrom: LocalDate): List { val sixMonthsAgo = Date.from( - LocalDateTime.of(dateFrom, LocalTime.MIN).atZone(ZoneId.systemDefault()).withZoneSameInstant(ZoneOffset.UTC) + LocalDateTime.of(dateFrom, LocalTime.MIN).atZone(ZoneId.systemDefault()) + .withZoneSameInstant(ZoneOffset.UTC) .toInstant() ) // Пример даты, можно заменить на вычисляемую @@ -1610,7 +1613,10 @@ class FinancialService( Document("\$match", Document("spaceInfo._id", ObjectId(spaceId))), Document( "\$match", - Document("date", Document("\$gte", sixMonthsAgo).append("\$lt", Date())).append("type.code", "INSTANT") + Document("date", Document("\$gte", sixMonthsAgo).append("\$lt", Date())).append( + "type.code", + "INSTANT" + ) ), // 2. Группируем по категории + (год, месяц) @@ -1625,7 +1631,8 @@ class FinancialService( // 3. Подтягиваем информацию о категории Document( "\$lookup", - Document("from", "categories").append("localField", "_id.category").append("foreignField", "_id") + Document("from", "categories").append("localField", "_id.category") + .append("foreignField", "_id") .append("as", "categoryInfo") ), @@ -1638,7 +1645,10 @@ class FinancialService( // 6. Группируем обратно по категории, собирая все (год, месяц, total) Document( "\$group", - Document("_id", "\$_id.category").append("categoryName", Document("\$first", "\$categoryInfo.name")) + 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( @@ -1653,7 +1663,8 @@ class FinancialService( // - каждый элемент = {year, month, total}, // - если нет записей за месяц, ставим total=0 Document( - "\$project", Document("categoryName", 1).append("categoryType", 1).append("categoryIcon", 1).append( + "\$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( @@ -1772,7 +1783,7 @@ class FinancialService( // Обновляем документ с отсортированными и обновленными monthlySums document["monthlySums"] = sortedMonthlySums document - }.collectList() + }.collectList().awaitSingle() } } \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/budgerapp/services/RecurrentService.kt b/src/main/kotlin/space/luminic/budgerapp/services/RecurrentService.kt index 99ba07f..1217151 100644 --- a/src/main/kotlin/space/luminic/budgerapp/services/RecurrentService.kt +++ b/src/main/kotlin/space/luminic/budgerapp/services/RecurrentService.kt @@ -1,6 +1,9 @@ package space.luminic.budgerapp.services +import kotlinx.coroutines.reactive.awaitLast +import kotlinx.coroutines.reactive.awaitSingle +import kotlinx.coroutines.reactor.awaitSingleOrNull import org.bson.Document import org.bson.types.ObjectId import org.slf4j.LoggerFactory @@ -13,7 +16,6 @@ import org.springframework.data.mongodb.core.aggregation.Aggregation.* import org.springframework.data.mongodb.core.query.Criteria 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.* @@ -73,58 +75,42 @@ class RecurrentService( ) } - fun createRecurrentsForBudget(space: Space, budget: Budget): Mono { + suspend fun createRecurrentsForBudget(space: Space, budget: Budget) { val currentYearMonth = YearMonth.of(budget.dateFrom.year, budget.dateFrom.monthValue) val daysInCurrentMonth = currentYearMonth.lengthOfMonth() - val context = ReactiveSecurityContextHolder.getContext() - .doOnNext { println("Security context: $it") } - .switchIfEmpty(Mono.error(IllegalStateException("SecurityContext is empty!"))) + val context = ReactiveSecurityContextHolder.getContext().awaitSingleOrNull() + ?: throw IllegalStateException("SecurityContext is empty!") + val user = userService.getByUserNameWoPass(context.authentication.name) + val recurrents = getRecurrents(space.id!!).awaitSingle() + val transactions = recurrents.map { recurrent -> + val transactionDate = when { + recurrent.atDay in budget.dateFrom.dayOfMonth..daysInCurrentMonth -> { + currentYearMonth.atDay(recurrent.atDay) + } - return context - .map { it.authentication } - .flatMap { authentication -> - val username = authentication.name - userService.getByUsername(username) - .switchIfEmpty(Mono.error(IllegalArgumentException("User not found for username: $username"))) - } - .flatMapMany { user -> - getRecurrents(space.id!!) // Теперь это Mono> - .flatMapMany { Flux.fromIterable(it) } // Преобразуем List в Flux - .map { recurrent -> - // Определяем дату транзакции - val transactionDate = when { - 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) - } - } - - // Создаем транзакцию - Transaction( - space = space, - date = transactionDate, - amount = recurrent.amount.toDouble(), - category = recurrent.category, - isDone = false, - comment = recurrent.name, - user = user, - type = TransactionType("PLANNED", "Запланированные") - ) - } - } - .collectList() // Собираем все транзакции в список - .flatMap { transactions -> - transactionRepo.saveAll(transactions).then() // Сохраняем все транзакции разом и возвращаем Mono + recurrent.atDay < budget.dateFrom.dayOfMonth -> { + currentYearMonth.atDay(recurrent.atDay).plusMonths(1) + } + + else -> { + val extraDays = recurrent.atDay - daysInCurrentMonth + currentYearMonth.plusMonths(1).atDay(extraDays) + } } + // Создаем транзакцию + Transaction( + space = space, + date = transactionDate, + amount = recurrent.amount.toDouble(), + category = recurrent.category, + isDone = false, + comment = recurrent.name, + user = user, + type = TransactionType("PLANNED", "Запланированные") + ) + } + transactionRepo.saveAll(transactions).awaitLast() } @@ -139,6 +125,4 @@ class RecurrentService( } - - } \ 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 dca941b..0ec30b0 100644 --- a/src/main/kotlin/space/luminic/budgerapp/services/SpaceService.kt +++ b/src/main/kotlin/space/luminic/budgerapp/services/SpaceService.kt @@ -1,5 +1,11 @@ package space.luminic.budgerapp.services +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.reactive.awaitFirst +import kotlinx.coroutines.reactive.awaitFirstOrNull +import kotlinx.coroutines.reactive.awaitSingle +import kotlinx.coroutines.reactor.awaitSingleOrNull import org.bson.Document import org.bson.types.ObjectId import org.springframework.data.mongodb.core.ReactiveMongoTemplate @@ -8,8 +14,7 @@ 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.configs.AuthException import space.luminic.budgerapp.models.* import space.luminic.budgerapp.repos.* import java.time.LocalDateTime @@ -30,262 +35,214 @@ class SpaceService( private val tagRepo: TagRepo ) { - fun isValidRequest(spaceId: String): Mono { - return ReactiveSecurityContextHolder.getContext() - .map { it.authentication } - .flatMap { authentication -> - val username = authentication.name - // Получаем пользователя по имени - userService.getByUsername(username) - .switchIfEmpty(Mono.error(IllegalArgumentException("User not found for username: $username"))) - .flatMap { user -> - // Получаем пространство по ID - getSpace(spaceId) - .switchIfEmpty(Mono.error(IllegalArgumentException("Space not found for id: $spaceId"))) - .flatMap { space -> - // Проверяем доступ пользователя к пространству - if (space.users.none { it.id.toString() == user.id }) { - return@flatMap Mono.error(IllegalArgumentException("User does not have access to this Space")) - } - // Если проверка прошла успешно, возвращаем пространство - Mono.just(space) - } - } - } + suspend fun isValidRequest(spaceId: String): Space { + val securityContextHolder = ReactiveSecurityContextHolder.getContext().awaitSingleOrNull() + ?: throw AuthException("Authentication failed") + val authentication = securityContextHolder.authentication + + val username = authentication.name + // Получаем пользователя по имени + val user = userService.getByUsername(username) + val space = getSpace(spaceId) + + // Проверяем доступ пользователя к пространству + return if (space.users.none { it.id.toString() == user.id }) { + throw IllegalArgumentException("User does not have access to this Space") + } else space + } - fun getSpaces(): Mono> { - return ReactiveSecurityContextHolder.getContext() - .map { it.authentication } - .flatMap { authentication -> - val username = authentication.name - userService.getByUsername(username) - .switchIfEmpty(Mono.error(IllegalArgumentException("User not found for username: $username"))) - .flatMap { user -> - val userId = ObjectId(user.id!!) + suspend fun getSpaces(): List { + val securityContext = ReactiveSecurityContextHolder.getContext().awaitSingle() + val authentication = securityContext.authentication + val username = authentication.name + val user = userService.getByUsername(username) - // Поиск пространств пользователя + val userId = ObjectId(user.id!!) + // Агрегация для загрузки владельца и пользователей + val lookupOwner = lookup("users", "owner.\$id", "_id", "ownerDetails") + val unwindOwner = unwind("ownerDetails") - // Агрегация для загрузки владельца и пользователей - val lookupOwner = lookup("users", "owner.\$id", "_id", "ownerDetails") - val unwindOwner = unwind("ownerDetails") - - val lookupUsers = lookup("users", "users.\$id", "_id", "usersDetails") + val lookupUsers = lookup("users", "users.\$id", "_id", "usersDetails") // val unwindUsers = unwind("usersDetails") - val matchStage = match(Criteria.where("usersDetails._id").`is`(userId)) - val aggregation = newAggregation(lookupOwner, unwindOwner, lookupUsers, matchStage) + val matchStage = match(Criteria.where("usersDetails._id").`is`(userId)) + val aggregation = newAggregation(lookupOwner, unwindOwner, lookupUsers, matchStage) - reactiveMongoTemplate.aggregate(aggregation, "spaces", Document::class.java) - .collectList() - .map { docs -> - docs.map { doc -> - val ownerDoc = doc.get("ownerDetails", Document::class.java) - val usersDocList = doc.getList("usersDetails", Document::class.java) + return reactiveMongoTemplate.aggregate(aggregation, "spaces", Document::class.java) + .collectList() + .map { docs -> + docs.map { doc -> + val ownerDoc = doc.get("ownerDetails", Document::class.java) + val usersDocList = doc.getList("usersDetails", Document::class.java) - Space( - id = doc.getObjectId("_id").toString(), - name = doc.getString("name"), - description = doc.getString("description"), - owner = User( - id = ownerDoc.getObjectId("_id").toString(), - username = ownerDoc.getString("username"), - firstName = ownerDoc.getString("firstName") - ), - users = usersDocList.map { userDoc -> - User( - id = userDoc.getObjectId("_id").toString(), - username = userDoc.getString("username"), - firstName = userDoc.getString("firstName") - ) - }.toMutableList() - ) - } - } - - - } - } + Space( + id = doc.getObjectId("_id").toString(), + name = doc.getString("name"), + description = doc.getString("description"), + owner = User( + id = ownerDoc.getObjectId("_id").toString(), + username = ownerDoc.getString("username"), + firstName = ownerDoc.getString("firstName") + ), + users = usersDocList.map { userDoc -> + User( + id = userDoc.getObjectId("_id").toString(), + username = userDoc.getString("username"), + firstName = userDoc.getString("firstName") + ) + }.toMutableList() + ) + } + }.awaitFirst() } - fun getSpace(spaceId: String): Mono { - return spaceRepo.findById(spaceId) - .switchIfEmpty(Mono.error(IllegalArgumentException("SpaceId not found for spaceId: $spaceId"))) + suspend fun getSpace(spaceId: String): Space { + return spaceRepo.findById(spaceId).awaitSingleOrNull() + ?: throw IllegalArgumentException("SpaceId not found for spaceId: $spaceId") } - fun createSpace(space: Space, createCategories: Boolean): Mono { - return ReactiveSecurityContextHolder.getContext() - .map { it.authentication } - .flatMap { authentication -> - val username = authentication.name - userService.getByUsername(username) - .switchIfEmpty(Mono.error(IllegalArgumentException("User not found for username: $username"))) - .flatMap { user -> - space.owner = user - space.users.add(user) - spaceRepo.save(space).flatMap { savedSpace -> - if (!createCategories) { - return@flatMap Mono.just(savedSpace) // Если не нужно создавать категории, просто возвращаем пространство - } + suspend fun createSpace(space: Space, createCategories: Boolean): Space { + val securityContextHolder = ReactiveSecurityContextHolder.getContext().awaitSingleOrNull() + ?: throw AuthException("Authentication failed") + val authentication = securityContextHolder.authentication + val username = authentication.name + val user = userService.getByUsername(username) + + space.owner = user + space.users.add(user) + val savedSpace = spaceRepo.save(space).awaitSingle() + return if (!createCategories) { + savedSpace // Если не нужно создавать категории, просто возвращаем пространство + } else { + val categories = reactiveMongoTemplate.find(Query(), Category::class.java, "categories-etalon") + .map { category -> + category.copy(id = null, space = savedSpace) // Создаем новую копию + } + categoryRepo.saveAll(categories).awaitSingle() + savedSpace + } - reactiveMongoTemplate.find(Query(), Category::class.java, "categories-etalon") - .map { category -> - category.copy(id = null, space = savedSpace) // Создаем новую копию - } - .collectList() // Собираем в список перед сохранением - .flatMap { categoryRepo.saveAll(it).collectList() } // Сохраняем и возвращаем список - .then(Mono.just(savedSpace)) // После сохранения всех категорий, возвращаем пространство - } - } - } } - fun deleteSpace(space: Space): Mono { + suspend fun deleteSpace(space: Space) { val objectId = ObjectId(space.id) - return Mono.`when`( - financialService.findProjectedBudgets(objectId) - .flatMap { budgetRepo.deleteAll(it) }, - - financialService.getTransactions(objectId.toString()) - .flatMap { transactionRepo.deleteAll(it) }, - - categoryService.getCategories(objectId.toString(), null, "name", "ASC") - .flatMap { categoryRepo.deleteAll(it) }, - - recurrentService.getRecurrents(objectId.toString()) - .flatMap { recurrentRepo.deleteAll(it) } - ).then(spaceRepo.deleteById(space.id!!)) // Удаление Space после завершения всех операций - } - - - fun createInviteSpace(spaceId: String): Mono { - return ReactiveSecurityContextHolder.getContext() - .map { it.authentication } - .flatMap { authentication -> - val username = authentication.name - userService.getByUsername(username) - .switchIfEmpty(Mono.error(IllegalArgumentException("User not found for username: $username"))) - .flatMap { user -> - spaceRepo.findById(spaceId) - .switchIfEmpty(Mono.error(IllegalArgumentException("Space not found for id: $spaceId"))) - .flatMap { space -> - if (space.users.none { it.id.toString() == user.id }) { - return@flatMap Mono.error(IllegalArgumentException("User does not have access to this Space")) - } - - val invite = SpaceInvite( - UUID.randomUUID().toString().split("-")[0], - user, - LocalDateTime.now().plusHours(1), - - ) - space.invites.add(invite) - - // Сохраняем изменения и возвращаем созданное приглашение - spaceRepo.save(space).thenReturn(invite) - } - } + coroutineScope { + launch { + val budgets = financialService.findProjectedBudgets(objectId).awaitFirstOrNull().orEmpty() + budgetRepo.deleteAll(budgets).awaitFirstOrNull() } - } - - fun acceptInvite(code: String): Mono { - return ReactiveSecurityContextHolder.getContext() - .map { it.authentication } - .flatMap { authentication -> - val username = authentication.name - userService.getByUsername(username) - .switchIfEmpty(Mono.error(IllegalArgumentException("User not found for username: $username"))) - .flatMap { user -> - spaceRepo.findSpaceByInvites(code) - .switchIfEmpty(Mono.error(IllegalArgumentException("Space with invite code: $code not found"))) - .flatMap { space -> - val invite = space.invites.find { it.code == code } - - // Проверяем, есть ли инвайт и не истек ли он - if (invite == null || invite.activeTill.isBefore(LocalDateTime.now())) { - return@flatMap Mono.error(IllegalArgumentException("Invite is invalid or expired")) - } - - // Проверяем, не является ли пользователь уже участником - if (space.users.any { it.id == user.id }) { - return@flatMap Mono.error(IllegalArgumentException("User is already a member of this Space")) - } - - // Добавляем пользователя и удаляем использованный инвайт - space.users.add(user) - space.invites.remove(invite) - - spaceRepo.save(space) - } - } + launch { + val transactions = financialService.getTransactions(objectId.toString()).awaitFirstOrNull().orEmpty() + transactionRepo.deleteAll(transactions).awaitFirstOrNull() } - } - fun leaveSpace(spaceId: String): Mono { - return ReactiveSecurityContextHolder.getContext() - .map { it.authentication } - .flatMap { authentication -> - val username = authentication.name - userService.getByUsername(username) - .switchIfEmpty(Mono.error(IllegalArgumentException("User not found for username: $username"))) - .flatMap { user -> - spaceRepo.findById(spaceId) - .switchIfEmpty(Mono.error(IllegalArgumentException("Space not found for id: $spaceId"))) - .flatMap { space -> - if (space.users.none { it.id.toString() == user.id }) { - return@flatMap Mono.error(IllegalArgumentException("User does not have access to this Space")) - } - // Удаляем пользователя из массива - space.users.removeIf { it.id == user.id } - // Сохраняем изменения - spaceRepo.save(space).then() // .then() для Mono - } - } + launch { + val categories = + categoryService.getCategories(objectId.toString(), null, "name", "ASC").awaitFirstOrNull().orEmpty() + categoryRepo.deleteAll(categories).awaitFirstOrNull() } - } - fun kickMember(spaceId: String, kickedUsername: String): Mono { - return ReactiveSecurityContextHolder.getContext() - .map { it.authentication } - .flatMap { authentication -> - val username = authentication.name - // Получаем текущего пользователя - userService.getByUsername(username) - .switchIfEmpty(Mono.error(IllegalArgumentException("User not found for username: $username"))) - .flatMap { user -> - // Получаем пользователя, которого нужно исключить - userService.getByUsername(kickedUsername) - .switchIfEmpty(Mono.error(IllegalArgumentException("User not found for username: $kickedUsername"))) - .flatMap { kickedUser -> - // Получаем пространство - spaceRepo.findById(spaceId) - .switchIfEmpty(Mono.error(IllegalArgumentException("Space not found for id: $spaceId"))) - .flatMap { space -> - // Проверяем, является ли текущий пользователь владельцем - if (space.owner?.id != user.id) { - return@flatMap Mono.error(IllegalArgumentException("Only owners allowed for this action")) - } - - // Проверяем, что пользователь, которого нужно исключить, присутствует в списке пользователей - val userToKick = space.users.find { it.username == kickedUsername } - if (userToKick != null) { - // Удаляем пользователя из пространства - space.users.removeIf { it.username == kickedUsername } - // Сохраняем изменения - return@flatMap spaceRepo.save(space).then() - } else { - return@flatMap Mono.error(IllegalArgumentException("User not found in this space")) - } - } - } - } + launch { + val recurrents = recurrentService.getRecurrents(objectId.toString()).awaitFirstOrNull().orEmpty() + recurrentRepo.deleteAll(recurrents).awaitFirstOrNull() } + } + + spaceRepo.deleteById(space.id!!).awaitFirstOrNull() // Удаляем Space после всех операций } - fun findTag(space: Space, tagCode: String): Mono { + + suspend fun createInviteSpace(spaceId: String): SpaceInvite { + val securityContext = ReactiveSecurityContextHolder.getContext().awaitSingleOrNull() + ?: throw AuthException("Authentication failed") + val authentication = securityContext.authentication + val user = userService.getByUsername(authentication.name) + + val space = getSpace(spaceId) + if (space.owner?.id != user.id) { + throw AuthException("Only owner could create invite into space") + } + val invite = SpaceInvite( + UUID.randomUUID().toString().split("-")[0], + user, + LocalDateTime.now().plusHours(1), + + ) + space.invites.add(invite) + spaceRepo.save(space).awaitFirstOrNull() + // Сохраняем изменения и возвращаем созданное приглашение + return invite + } + + + suspend fun acceptInvite(code: String): Space { + val securityContextHolder = ReactiveSecurityContextHolder.getContext().awaitSingleOrNull() + ?: throw AuthException("Authentication failed") + val user = userService.getByUsername(securityContextHolder.authentication.name) + + val space = spaceRepo.findSpaceByInvites(code).awaitFirstOrNull() + ?: throw IllegalArgumentException("Space with invite code: $code not found") + val invite = space.invites.find { it.code == code } + + // Проверяем, есть ли инвайт и не истек ли он + if (invite == null || invite.activeTill.isBefore(LocalDateTime.now())) { + throw IllegalArgumentException("Invite is invalid or expired") + } + + // Проверяем, не является ли пользователь уже участником + if (space.users.any { it.id == user.id }) { + throw IllegalArgumentException("User is already a member of this Space") + } + + // Добавляем пользователя и удаляем использованный инвайт + space.users.add(user) + space.invites.remove(invite) + + return spaceRepo.save(space).awaitFirst() + } + + + suspend fun leaveSpace(spaceId: String) { + val securityContext = ReactiveSecurityContextHolder.getContext().awaitSingleOrNull() + ?: throw AuthException("Authentication failed") + val user = userService.getByUsername(securityContext.authentication.name) + val space = getSpace(spaceId) + // Удаляем пользователя из массива + space.users.removeIf { it.id == user.id } + // Сохраняем изменения + spaceRepo.save(space).awaitFirst() + } + + + suspend fun kickMember(spaceId: String, kickedUsername: String) { + val securityContext = ReactiveSecurityContextHolder.getContext().awaitSingleOrNull() + ?: throw AuthException("Authentication failed") + val currentUser = userService.getByUsername(securityContext.authentication.name) + //проверяем что кикнутый пользователь сушествует + userService.getByUsername(kickedUsername) + val space = getSpace(spaceId) + if (space.owner?.id != currentUser.id) { + throw IllegalArgumentException("Only owners allowed for this action") + } + + // Проверяем, что пользователь, которого нужно исключить, присутствует в списке пользователей + val userToKick = space.users.find { it.username == kickedUsername } + if (userToKick != null) { + // Удаляем пользователя из пространства + space.users.removeIf { it.username == kickedUsername } + // Сохраняем изменения + spaceRepo.save(space).awaitSingle() + } else { + throw IllegalArgumentException("User not found in this space") + } + } + + + suspend fun findTag(space: Space, tagCode: String): Tag? { val lookupSpaces = lookup("spaces", "space.\$id", "_id", "spaceDetails") val unwindSpace = unwind("spaceDetails") val matchCriteria = mutableListOf() @@ -310,37 +267,32 @@ class SpaceService( code = doc.getString("code"), name = doc.getString("name") ) - } - + }.awaitSingleOrNull() } - fun createTag(space: Space, tag: Tag): Mono { + suspend fun createTag(space: Space, tag: Tag): Tag { tag.space = space - return findTag(space, tag.code) - .flatMap { existingTag -> - Mono.error(IllegalArgumentException("Tag with code ${existingTag.code} already exists")) - } - .switchIfEmpty(tagRepo.save(tag)) + val existedTag = findTag(space, tag.code) + return existedTag?.let { + throw IllegalArgumentException("Tag with code ${tag.code} already exists") + } ?: tagRepo.save(tag).awaitFirst() } - 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!!)) // Удаляем тег только после обновления категорий - } + suspend fun deleteTag(space: Space, tagCode: String) { + val existedTag = findTag(space, tagCode) ?: throw NoSuchElementException("Tag with code $tagCode not found") + val categoriesWithTag = + categoryService.getCategories(space.id!!, sortBy = "name", direction = "ASC", tagCode = existedTag.code) + .awaitSingleOrNull().orEmpty() + categoriesWithTag.map { cat -> + cat.tags.removeIf { it.code == tagCode } // Изменяем список тегов + cat + } + categoryRepo.saveAll(categoriesWithTag).awaitFirst() // Сохраняем обновлённые категории + tagRepo.deleteById(existedTag.id!!).awaitFirst() } - fun getTags(space: Space): Mono> { + + suspend fun getTags(space: Space): List { val lookupSpaces = lookup("spaces", "space.\$id", "_id", "spaceDetails") val unwindSpace = unwind("spaceDetails") val matchCriteria = mutableListOf() @@ -363,27 +315,30 @@ class SpaceService( docs.map { doc -> Tag( id = doc.getObjectId("_id").toString(), - space = Space(id = doc.get("spaceDetails", Document::class.java).getObjectId("_id").toString()), + space = Space( + id = doc.get("spaceDetails", Document::class.java).getObjectId("_id").toString() + ), code = doc.getString("code"), name = doc.getString("name") ) } } + .awaitSingleOrNull().orEmpty() } - 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 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() diff --git a/src/main/kotlin/space/luminic/budgerapp/services/SubscriptionService.kt b/src/main/kotlin/space/luminic/budgerapp/services/SubscriptionService.kt index 7b9e322..ee5aa07 100644 --- a/src/main/kotlin/space/luminic/budgerapp/services/SubscriptionService.kt +++ b/src/main/kotlin/space/luminic/budgerapp/services/SubscriptionService.kt @@ -3,6 +3,7 @@ package space.luminic.budgerapp.services import com.interaso.webpush.VapidKeys import com.interaso.webpush.WebPushService +import kotlinx.coroutines.reactive.awaitSingle import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import org.slf4j.LoggerFactory @@ -70,7 +71,7 @@ class SubscriptionService(private val subscriptionRepo: SubscriptionRepo) { } - fun subscribe(subscriptionDTO: SubscriptionDTO, user: User): Mono { + suspend fun subscribe(subscriptionDTO: SubscriptionDTO, user: User): String { val subscription = Subscription( id = null, user = user, @@ -80,18 +81,15 @@ class SubscriptionService(private val subscriptionRepo: SubscriptionRepo) { isActive = true ) - return subscriptionRepo.save(subscription) - .flatMap { savedSubscription -> - Mono.just("Subscription created with ID: ${savedSubscription.id}") - } - .onErrorResume(DuplicateKeyException::class.java) { - logger.info("Subscription already exists. Skipping.") - Mono.just("Subscription already exists. Skipping.") - } - .onErrorResume { e -> - logger.error("Error while saving subscription: ${e.message}") - Mono.error(RuntimeException("Error while saving subscription")) - } + return try { + val savedSubscription = subscriptionRepo.save(subscription).awaitSingle() + "Subscription created with ID: ${savedSubscription.id}" + } catch (e: DuplicateKeyException) { + logger.info("Subscription already exists. Skipping.") + "Subscription already exists. Skipping." + } catch (e: Exception) { + logger.error("Error while saving subscription: ${e.message}") + throw RuntimeException("Error while saving subscription") + } } - } diff --git a/src/main/kotlin/space/luminic/budgerapp/services/TokenService.kt b/src/main/kotlin/space/luminic/budgerapp/services/TokenService.kt index 48f2a5f..e1ec91d 100644 --- a/src/main/kotlin/space/luminic/budgerapp/services/TokenService.kt +++ b/src/main/kotlin/space/luminic/budgerapp/services/TokenService.kt @@ -1,4 +1,5 @@ package space.luminic.budgerapp.services + import org.springframework.cache.annotation.CacheEvict import org.springframework.stereotype.Service import reactor.core.publisher.Mono @@ -25,15 +26,12 @@ class TokenService(private val tokenRepository: TokenRepo) { return tokenRepository.findByToken(token) } - @CacheEvict("tokens", allEntries = true) - fun revokeToken(token: String): Mono { - return tokenRepository.findByToken(token) - .switchIfEmpty(Mono.error(Exception("Token not found"))) - .flatMap { existingToken -> - val updatedToken = existingToken.copy(status = TokenStatus.REVOKED) - tokenRepository.save(updatedToken).then() - } + fun revokeToken(token: String) { + val tokenDetail = + tokenRepository.findByToken(token).block()!! + val updatedToken = tokenDetail.copy(status = TokenStatus.REVOKED) + tokenRepository.save(updatedToken).block() } diff --git a/src/main/kotlin/space/luminic/budgerapp/services/UserService.kt b/src/main/kotlin/space/luminic/budgerapp/services/UserService.kt index 853b530..bd78076 100644 --- a/src/main/kotlin/space/luminic/budgerapp/services/UserService.kt +++ b/src/main/kotlin/space/luminic/budgerapp/services/UserService.kt @@ -1,6 +1,7 @@ package space.luminic.budgerapp.services +import kotlinx.coroutines.reactor.awaitSingleOrNull import org.slf4j.LoggerFactory import org.springframework.cache.annotation.Cacheable import org.springframework.stereotype.Service @@ -14,13 +15,10 @@ class UserService(val userRepo: UserRepo) { val logger = LoggerFactory.getLogger(javaClass) - @Cacheable("users", key = "#username") - fun getByUsername(username: String): Mono { - return userRepo.findByUsernameWOPassword(username).switchIfEmpty( - Mono.error(NotFoundException("User with username: $username not found")) - ) - + suspend fun getByUsername(username: String): User { + return userRepo.findByUsernameWOPassword(username).awaitSingleOrNull() + ?: throw NotFoundException("User with username: $username not found") } fun getById(id: String): Mono { @@ -32,9 +30,10 @@ class UserService(val userRepo: UserRepo) { } - @Cacheable("users", key = "#username") - fun getByUserNameWoPass(username: String): Mono { - return userRepo.findByUsernameWOPassword(username) + @Cacheable("users", key = "#username") + suspend fun getByUserNameWoPass(username: String): User { + return userRepo.findByUsernameWOPassword(username).awaitSingleOrNull() + ?: throw NotFoundException("User with username: $username not found") } @Cacheable("usersList")