From d680345a9ff833e3ca254411a2e0445298530340 Mon Sep 17 00:00:00 2001 From: xds Date: Mon, 17 Feb 2025 17:58:07 +0300 Subject: [PATCH] add spaces --- .../budgerapp/configs/BearerTokenFilter.kt | 2 +- .../budgerapp/configs/SecurityConfig.kt | 2 +- .../budgerapp/controllers/AuthController.kt | 6 + .../budgerapp/controllers/BudgetController.kt | 50 +-- .../controllers/CategoriesController.kt | 110 ++--- .../controllers/RecurrentController.kt | 59 +-- .../budgerapp/controllers/SpaceController.kt | 265 ++++++++++++ .../controllers/TransactionController.kt | 11 +- .../space/luminic/budgerapp/models/Budget.kt | 2 + .../luminic/budgerapp/models/Category.kt | 2 + .../luminic/budgerapp/models/Recurrent.kt | 1 + .../space/luminic/budgerapp/models/Space.kt | 25 ++ .../luminic/budgerapp/models/Transaction.kt | 1 + .../space/luminic/budgerapp/models/User.kt | 6 +- .../luminic/budgerapp/repos/BudgetRepo.kt | 9 + .../luminic/budgerapp/repos/RecurrentRepo.kt | 8 + .../luminic/budgerapp/repos/SpaceRepo.kt | 21 + .../luminic/budgerapp/services/AuthService.kt | 20 + .../budgerapp/services/CategoryService.kt | 71 ++-- .../budgerapp/services/FinancialService.kt | 384 ++++++++++++------ .../budgerapp/services/RecurrentService.kt | 42 +- .../budgerapp/services/SpaceService.kt | 233 +++++++++++ src/main/resources/persprof.json | 52 +++ 23 files changed, 1097 insertions(+), 285 deletions(-) create mode 100644 src/main/kotlin/space/luminic/budgerapp/controllers/SpaceController.kt create mode 100644 src/main/kotlin/space/luminic/budgerapp/models/Space.kt create mode 100644 src/main/kotlin/space/luminic/budgerapp/repos/SpaceRepo.kt create mode 100644 src/main/kotlin/space/luminic/budgerapp/services/SpaceService.kt create mode 100644 src/main/resources/persprof.json diff --git a/src/main/kotlin/space/luminic/budgerapp/configs/BearerTokenFilter.kt b/src/main/kotlin/space/luminic/budgerapp/configs/BearerTokenFilter.kt index 47ce400..6e60b82 100644 --- a/src/main/kotlin/space/luminic/budgerapp/configs/BearerTokenFilter.kt +++ b/src/main/kotlin/space/luminic/budgerapp/configs/BearerTokenFilter.kt @@ -26,7 +26,7 @@ class BearerTokenFilter(private val authService: AuthService) : SecurityContextS override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono { val token = exchange.request.headers.getFirst(HttpHeaders.AUTHORIZATION)?.removePrefix("Bearer ") - if (exchange.request.path.value() == "/api/auth/login" || exchange.request.path.value() + if (exchange.request.path.value() in listOf("/api/auth/login","/api/auth/register") || exchange.request.path.value() .startsWith("/api/actuator") ) { return chain.filter(exchange) diff --git a/src/main/kotlin/space/luminic/budgerapp/configs/SecurityConfig.kt b/src/main/kotlin/space/luminic/budgerapp/configs/SecurityConfig.kt index 05e2e41..0cd06ed 100644 --- a/src/main/kotlin/space/luminic/budgerapp/configs/SecurityConfig.kt +++ b/src/main/kotlin/space/luminic/budgerapp/configs/SecurityConfig.kt @@ -25,7 +25,7 @@ class SecurityConfig( .logout { it.disable() } .authorizeExchange { - it.pathMatchers(HttpMethod.POST, "/auth/login").permitAll() + it.pathMatchers(HttpMethod.POST, "/auth/login", "/auth/register").permitAll() it.pathMatchers("/actuator/**").permitAll() it.anyExchange().authenticated() } diff --git a/src/main/kotlin/space/luminic/budgerapp/controllers/AuthController.kt b/src/main/kotlin/space/luminic/budgerapp/controllers/AuthController.kt index c38d517..62bfa8b 100644 --- a/src/main/kotlin/space/luminic/budgerapp/controllers/AuthController.kt +++ b/src/main/kotlin/space/luminic/budgerapp/controllers/AuthController.kt @@ -22,6 +22,11 @@ class AuthController( .map { token -> mapOf("token" to token) } } + @PostMapping("/register") + fun register(@RequestBody request: RegisterRequest): Mono { + return authService.register(request.username, request.password, request.firstName) + } + @GetMapping("/me") fun getMe(@RequestHeader("Authorization") token: String): Mono { @@ -33,3 +38,4 @@ class AuthController( } data class AuthRequest(val username: String, val password: String) +data class RegisterRequest(val username: String, val password: String, val firstName: String) \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/budgerapp/controllers/BudgetController.kt b/src/main/kotlin/space/luminic/budgerapp/controllers/BudgetController.kt index 4b3ca47..1691c35 100644 --- a/src/main/kotlin/space/luminic/budgerapp/controllers/BudgetController.kt +++ b/src/main/kotlin/space/luminic/budgerapp/controllers/BudgetController.kt @@ -30,20 +30,20 @@ class BudgetController( private val logger = LoggerFactory.getLogger(BudgetController::class.java) @GetMapping - fun getBudgets(): Mono> { - return financialService.getBudgets() + fun getBudgets(): Mono> { + return financialService.getBudgets("123") } @GetMapping("/{id}") - fun getBudget(@PathVariable id: String): Mono { - return financialService.getBudget(id) + fun getBudget(@PathVariable id: String): Mono { + return financialService.getBudget(id) } - @GetMapping("/by-dates") - fun getBudgetByDate(@RequestParam date: LocalDate): ResponseEntity { - return ResponseEntity.ok(financialService.getBudgetByDate(date)) - } +// @GetMapping("/by-dates") +// fun getBudgetByDate(@RequestParam date: LocalDate): ResponseEntity { +// return ResponseEntity.ok(financialService.getBudgetByDate(date)) +// } @GetMapping("/{id}/categories") @@ -52,17 +52,17 @@ class BudgetController( } @GetMapping("/{id}/transactions") - fun getBudgetTransactions(@PathVariable id: String):Mono>> { + fun getBudgetTransactions(@PathVariable id: String): Mono>> { return financialService.getBudgetTransactionsByType(id) } - @PostMapping("/") - fun createBudget(@RequestBody budgetCreationDTO: BudgetCreationDTO): Mono { - return financialService.createBudget( - budgetCreationDTO.budget, - budgetCreationDTO.createRecurrent - ) - } +// @PostMapping("/") +// fun createBudget(@RequestBody budgetCreationDTO: BudgetCreationDTO): Mono { +// return financialService.createBudget( +// budgetCreationDTO.budget, +// budgetCreationDTO.createRecurrent +// ) +// } @DeleteMapping("/{id}") fun deleteBudget(@PathVariable id: String): Mono { @@ -83,7 +83,6 @@ class BudgetController( } - @GetMapping("/{id}/warns") fun budgetWarns(@PathVariable id: String, @RequestParam hidden: Boolean? = null): Mono> { return financialService.getWarns(id, hidden) @@ -91,13 +90,18 @@ class BudgetController( @PostMapping("/{id}/warns/{warnId}/hide") fun setWarnHide(@PathVariable id: String, @PathVariable warnId: String): Mono { - return financialService.hideWarn( warnId) - } - - @GetMapping("/regencats") - fun regenCats(): Mono{ - return financialService.regenCats() + return financialService.hideWarn(warnId) } +// +// @GetMapping("/regencats") +// fun regenCats(): Mono { +// return financialService.regenCats() +// } +// +// @GetMapping("/regenSpaces") +// fun regenSpaces(): Mono { +// return financialService.regenBudgets() +// } data class LimitValue( var limit: Double diff --git a/src/main/kotlin/space/luminic/budgerapp/controllers/CategoriesController.kt b/src/main/kotlin/space/luminic/budgerapp/controllers/CategoriesController.kt index 8c47886..f730e6d 100644 --- a/src/main/kotlin/space/luminic/budgerapp/controllers/CategoriesController.kt +++ b/src/main/kotlin/space/luminic/budgerapp/controllers/CategoriesController.kt @@ -31,61 +31,61 @@ class CategoriesController( private val logger = LoggerFactory.getLogger(javaClass) - - @GetMapping() - fun getCategories( - @RequestParam("type") type: String? = null, - @RequestParam("sort") sortBy: String = "name", - @RequestParam("direction") direction: String = "ASC" - ): Mono> { - return categoryService.getCategories(type, sortBy, direction) - - } - - @GetMapping("/types") - fun getCategoriesTypes(): ResponseEntity { - return try { - ResponseEntity.ok(categoryService.getCategoryTypes()) - } catch (e: Exception) { - ResponseEntity(HttpClientErrorException(HttpStatus.INTERNAL_SERVER_ERROR), HttpStatus.INTERNAL_SERVER_ERROR) - - } - } - - @PostMapping() - fun createCategory(@RequestBody category: Category): Mono { - return categoryService.createCategory(category) - } - - @PutMapping("/{categoryId}") - fun editCategory(@PathVariable categoryId: String, @RequestBody category: Category): Mono { - return categoryService.editCategory(category) - } - - @DeleteMapping("/{categoryId}") - fun deleteCategory(@PathVariable categoryId: String): Mono { - return categoryService.deleteCategory(categoryId) - } - - @GetMapping("/test") - fun test(): Mono> { - var dateFrom = LocalDate.parse("2025-01-10") - var dateTo = LocalDate.parse("2025-02-09") - - return financialService.getCategoryTransactionPipeline(dateFrom, dateTo) - - } - - - @GetMapping("/by-month") - fun getCategoriesSumsByMonths(): Mono> { - return financialService.getCategorySumsPipeline(LocalDate.of(2024, 8, 1), LocalDate.of(2025, 1, 12)) - } - - @GetMapping("/by-month2") - fun getCategoriesSumsByMonthsV2(): Mono> { - return financialService.getCategorySummaries(LocalDate.now().minusMonths(6)) - } +// +// @GetMapping() +// fun getCategories( +// @RequestParam("type") type: String? = null, +// @RequestParam("sort") sortBy: String = "name", +// @RequestParam("direction") direction: String = "ASC" +// ): Mono> { +// return categoryService.getCategories(type, sortBy, direction) +// +// } +// +// @GetMapping("/types") +// fun getCategoriesTypes(): ResponseEntity { +// return try { +// ResponseEntity.ok(categoryService.getCategoryTypes()) +// } catch (e: Exception) { +// ResponseEntity(HttpClientErrorException(HttpStatus.INTERNAL_SERVER_ERROR), HttpStatus.INTERNAL_SERVER_ERROR) +// +// } +// } +// +// @PostMapping() +// fun createCategory(@RequestBody category: Category): Mono { +// return categoryService.createCategory(category) +// } +// +// @PutMapping("/{categoryId}") +// fun editCategory(@PathVariable categoryId: String, @RequestBody category: Category): Mono { +// return categoryService.editCategory(category) +// } +// +//// @DeleteMapping("/{categoryId}") +//// fun deleteCategory(@PathVariable categoryId: String): Mono { +//// return categoryService.deleteCategory(categoryId) +//// } +// +// @GetMapping("/test") +// fun test(): Mono> { +// var dateFrom = LocalDate.parse("2025-01-10") +// var dateTo = LocalDate.parse("2025-02-09") +// +// return financialService.getCategoryTransactionPipeline(dateFrom, dateTo) +// +// } +// +// +// @GetMapping("/by-month") +// fun getCategoriesSumsByMonths(): Mono> { +// return financialService.getCategorySumsPipeline(LocalDate.of(2024, 8, 1), LocalDate.of(2025, 1, 12)) +// } +// +// @GetMapping("/by-month2") +// fun getCategoriesSumsByMonthsV2(@RequestParam spaceId: String): Mono> { +// return financialService.getCategorySummaries(spaceId, LocalDate.now().minusMonths(6)) +// } } \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/budgerapp/controllers/RecurrentController.kt b/src/main/kotlin/space/luminic/budgerapp/controllers/RecurrentController.kt index a116818..a758e8e 100644 --- a/src/main/kotlin/space/luminic/budgerapp/controllers/RecurrentController.kt +++ b/src/main/kotlin/space/luminic/budgerapp/controllers/RecurrentController.kt @@ -17,33 +17,38 @@ import space.luminic.budgerapp.services.RecurrentService class RecurrentController ( private val recurrentService: RecurrentService ){ - - @GetMapping("/") - fun getRecurrents(): Mono> { - return recurrentService.getRecurrents() - } - - - @GetMapping("/{id}") - fun getRecurrent(@PathVariable id: String): Mono { - return recurrentService.getRecurrentById(id) - } - - @PostMapping("/") - fun createRecurrent(@RequestBody recurrent: Recurrent): Mono { - return recurrentService.createRecurrent(recurrent) - } - - @PutMapping("/{id}") - fun editRecurrent(@PathVariable id: String, @RequestBody recurrent: Recurrent): Mono { - return recurrentService.editRecurrent(recurrent) - } - - - @DeleteMapping("/{id}") - fun deleteRecurrent(@PathVariable id: String): Mono { - return recurrentService.deleteRecurrent(id) - } +// +// @GetMapping("/") +// fun getRecurrents(): Mono> { +// return recurrentService.getRecurrents() +// } +// +// +// @GetMapping("/{id}") +// fun getRecurrent(@PathVariable id: String): Mono { +// return recurrentService.getRecurrentById(id) +// } +// +// @PostMapping("/") +// fun createRecurrent(@RequestBody recurrent: Recurrent): Mono { +// return recurrentService.createRecurrent(recurrent) +// } +// +// @PutMapping("/{id}") +// fun editRecurrent(@PathVariable id: String, @RequestBody recurrent: Recurrent): Mono { +// return recurrentService.editRecurrent(recurrent) +// } +// +// +// @DeleteMapping("/{id}") +// fun deleteRecurrent(@PathVariable id: String): Mono { +// return recurrentService.deleteRecurrent(id) +// } +// +// @GetMapping("/regen") +// fun regenRecurrents(): Mono> { +// return recurrentService.regenRecurrents() +// } diff --git a/src/main/kotlin/space/luminic/budgerapp/controllers/SpaceController.kt b/src/main/kotlin/space/luminic/budgerapp/controllers/SpaceController.kt new file mode 100644 index 0000000..bc03c90 --- /dev/null +++ b/src/main/kotlin/space/luminic/budgerapp/controllers/SpaceController.kt @@ -0,0 +1,265 @@ +package space.luminic.budgerapp.controllers + +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.dtos.BudgetCreationDTO +import space.luminic.budgerapp.models.* +import space.luminic.budgerapp.services.CategoryService +import space.luminic.budgerapp.services.FinancialService +import space.luminic.budgerapp.services.RecurrentService +import space.luminic.budgerapp.services.SpaceService +import java.time.LocalDate + +@RestController +@RequestMapping("/spaces") +class SpaceController( + private val spaceService: SpaceService, + private val financialService: FinancialService, + private val categoryService: CategoryService, + private val recurrentService: RecurrentService +) { + + + @GetMapping + fun getSpaces(): Mono> { + return spaceService.getSpaces() + } + + + @PostMapping + fun createSpace(@RequestBody space: Space): Mono { + return spaceService.createSpace(space) + } + + + @GetMapping("{spaceId}") + fun getSpace(@PathVariable spaceId: String): Mono { + return spaceService.getSpace(spaceId) + } + + + @DeleteMapping("/{spaceId}") + fun deleteSpace(@PathVariable spaceId: String): Mono { + return spaceService.deleteSpace(spaceId) + } + + @PostMapping("/{spaceId}/invite") + fun inviteSpace(@PathVariable spaceId: String): Mono { + return spaceService.createInviteSpace(spaceId) + } + + @PostMapping("/invite/{code}") + fun acceptInvite(@PathVariable code: String): Mono { + return spaceService.acceptInvite(code) + } + + @DeleteMapping("/{spaceId}/leave") + fun leaveSpace(@PathVariable spaceId: String): Mono { + return spaceService.leaveSpace(spaceId) + } + + @DeleteMapping("/{spaceId}/members/kick/{username}") + fun kickMembers(@PathVariable spaceId: String, @PathVariable username: String): Mono { + return spaceService.kickMember(spaceId, username) + } + + + // + //Budgets API + // + @GetMapping("{spaceId}/budgets") + fun getBudgets(@PathVariable spaceId: String): Mono> { + return financialService.getBudgets(spaceId) + } + + @PostMapping("/{spaceId}/budgets") + fun createBudget( + @PathVariable spaceId: String, + @RequestBody budgetCreationDTO: BudgetCreationDTO, + ): Mono { + return financialService.createBudget(spaceId, budgetCreationDTO.budget, budgetCreationDTO.createRecurrent) + } + + + // Transactions API + @GetMapping("/{spaceId}/transactions") + fun getTransactions( + @PathVariable spaceId: String, + @RequestParam(value = "transaction_type") transactionType: String? = null, + @RequestParam(value = "category_type") categoryType: String? = null, + @RequestParam(value = "user_id") userId: String? = null, + @RequestParam(value = "is_child") isChild: Boolean? = null, + @RequestParam(value = "limit") limit: Int = 10, + @RequestParam(value = "offset") offset: Int = 0 + ): ResponseEntity { + try { + return ResponseEntity.ok( + financialService.getTransactions( + spaceId = spaceId, + transactionType = transactionType, + categoryType = categoryType, + userId = userId, + isChild = isChild, + limit = limit, + offset = offset + ) + ) + } catch (e: Exception) { + e.printStackTrace() + return ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR) + } + } + + @GetMapping("/{spaceId}/transactions/{id}") + 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) + } + } + + @PostMapping("/{spaceId}/transactions") + fun createTransaction(@PathVariable spaceId: String, @RequestBody transaction: Transaction): ResponseEntity { + try { + return ResponseEntity.ok(financialService.createTransaction(spaceId, transaction)) + } catch (e: Exception) { + e.printStackTrace() + return ResponseEntity(e.message, HttpStatus.INTERNAL_SERVER_ERROR) + + } + } + + @PutMapping("/{spaceId}/transactions/{id}") + fun editTransaction( + @PathVariable spaceId: String, @PathVariable id: String, @RequestBody transaction: Transaction + ): ResponseEntity { + try { + return ResponseEntity.ok(financialService.editTransaction(transaction)) + } catch (e: Exception) { + e.printStackTrace() + return ResponseEntity(e.message, HttpStatus.INTERNAL_SERVER_ERROR) + } + } + + @DeleteMapping("/{spaceId}/transactions/{id}") + fun deleteTransaction(@PathVariable spaceId: String, @PathVariable id: String): Mono { + return financialService.deleteTransaction(id) + } + + // + // Categories API + // + + + @GetMapping("/{spaceId}/categories") + 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) + } + + } + + @GetMapping("/{spaceId}/categories/types") + fun getCategoriesTypes(): ResponseEntity { + return try { + ResponseEntity.ok(categoryService.getCategoryTypes()) + } catch (e: Exception) { + ResponseEntity(HttpClientErrorException(HttpStatus.INTERNAL_SERVER_ERROR), HttpStatus.INTERNAL_SERVER_ERROR) + + } + } + + @PostMapping("/{spaceId}/categories") + fun createCategory( + @PathVariable spaceId: String, @RequestBody category: Category + ): Mono { + return spaceService.isValidRequest(spaceId).flatMap { + categoryService.createCategory(it,category) + } + + } + + @PutMapping("/{spaceId}/{categoryId}") + fun editCategory(@PathVariable categoryId: String, @RequestBody category: Category): Mono { + return categoryService.editCategory(category) + } + + + @GetMapping("/by-month") + fun getCategoriesSumsByMonths(): Mono> { + return financialService.getCategorySumsPipeline(LocalDate.of(2024, 8, 1), LocalDate.of(2025, 1, 12)) + } + + @GetMapping("/{spaceId}/analytics/by-month") + fun getCategoriesSumsByMonthsV2(@PathVariable spaceId: String): Mono> { + return financialService.getCategorySummaries(spaceId, LocalDate.now().minusMonths(6)) + } + + + // + // Recurrents API + // + + @GetMapping("/{spaceId}/recurrents") + fun getRecurrents(@PathVariable spaceId: String): Mono> { + return spaceService.isValidRequest(spaceId).flatMap { + recurrentService.getRecurrents(it) + } + } + + + @GetMapping("/{spaceId}/recurrents/{id}") + fun getRecurrent(@PathVariable spaceId: String, @PathVariable id: String): Mono { + return spaceService.isValidRequest(spaceId).flatMap { + recurrentService.getRecurrentById(it, id) + } + } + + @PostMapping("/{spaceId}/recurrent") + fun createRecurrent(@PathVariable spaceId: String, @RequestBody recurrent: Recurrent): Mono { + return spaceService.isValidRequest(spaceId).flatMap { + recurrentService.createRecurrent(it, recurrent) + } + + } + + @PutMapping("/{spaceId}/recurrent/{id}") + fun editRecurrent( + @PathVariable spaceId: String, + @PathVariable id: String, + @RequestBody recurrent: Recurrent + ): Mono { + return spaceService.isValidRequest(spaceId).flatMap { + recurrentService.editRecurrent(recurrent) + } + + } + + + @DeleteMapping("/{spaceId}/recurrent/{id}") + fun deleteRecurrent(@PathVariable spaceId: String, @PathVariable id: String): Mono { + return spaceService.isValidRequest(spaceId).flatMap { + recurrentService.deleteRecurrent(id) + } + } + +// @GetMapping("/regen") +// fun regenSpaces(): Mono> { +// return spaceService.regenSpaces() +// } +} \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/budgerapp/controllers/TransactionController.kt b/src/main/kotlin/space/luminic/budgerapp/controllers/TransactionController.kt index 5c261d8..51cca00 100644 --- a/src/main/kotlin/space/luminic/budgerapp/controllers/TransactionController.kt +++ b/src/main/kotlin/space/luminic/budgerapp/controllers/TransactionController.kt @@ -26,6 +26,7 @@ class TransactionController(private val financialService: FinancialService) { @GetMapping fun getTransactions( + @RequestParam spaceId: String, @RequestParam(value = "transaction_type") transactionType: String? = null, @RequestParam(value = "category_type") categoryType: String? = null, @RequestParam(value = "user_id") userId: String? = null, @@ -36,6 +37,7 @@ class TransactionController(private val financialService: FinancialService) { try { return ResponseEntity.ok( financialService.getTransactions( + spaceId = spaceId, transactionType = transactionType, categoryType = categoryType, userId = userId, @@ -61,9 +63,9 @@ class TransactionController(private val financialService: FinancialService) { } @PostMapping - fun createTransaction(@RequestBody transaction: Transaction): ResponseEntity { + fun createTransaction(@RequestParam spaceId: String, @RequestBody transaction: Transaction): ResponseEntity { try { - return ResponseEntity.ok(financialService.createTransaction(transaction)) + return ResponseEntity.ok(financialService.createTransaction(spaceId,transaction)) } catch (e: Exception) { e.printStackTrace() return ResponseEntity(e.message, HttpStatus.INTERNAL_SERVER_ERROR) @@ -109,5 +111,10 @@ class TransactionController(private val financialService: FinancialService) { } } +// @GetMapping("/regenTransactions") +// fun regenTransactions(): Mono { +// return financialService.regenTransactions() +// } + } \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/budgerapp/models/Budget.kt b/src/main/kotlin/space/luminic/budgerapp/models/Budget.kt index 0ed8e67..5f225fe 100644 --- a/src/main/kotlin/space/luminic/budgerapp/models/Budget.kt +++ b/src/main/kotlin/space/luminic/budgerapp/models/Budget.kt @@ -10,6 +10,7 @@ import kotlin.collections.mutableListOf data class BudgetDTO( var id: String? = null, + var space: Space? = null, var name: String, var dateFrom: LocalDate, var dateTo: LocalDate, @@ -26,6 +27,7 @@ data class BudgetDTO( @Document("budgets") data class Budget( @Id var id: String? = null, + @DBRef var space: Space? = null, var name: String, var dateFrom: LocalDate, var dateTo: LocalDate, diff --git a/src/main/kotlin/space/luminic/budgerapp/models/Category.kt b/src/main/kotlin/space/luminic/budgerapp/models/Category.kt index 2b3e6fc..9d82db3 100644 --- a/src/main/kotlin/space/luminic/budgerapp/models/Category.kt +++ b/src/main/kotlin/space/luminic/budgerapp/models/Category.kt @@ -1,6 +1,7 @@ package space.luminic.budgerapp.models import org.springframework.data.annotation.Id +import org.springframework.data.mongodb.core.mapping.DBRef import org.springframework.data.mongodb.core.mapping.Document @@ -8,6 +9,7 @@ import org.springframework.data.mongodb.core.mapping.Document data class Category( @Id val id: String? = null, + @DBRef var space: Space? = null, var type: CategoryType, val name: String, val description: String? = null, diff --git a/src/main/kotlin/space/luminic/budgerapp/models/Recurrent.kt b/src/main/kotlin/space/luminic/budgerapp/models/Recurrent.kt index 441d8b4..e5754c1 100644 --- a/src/main/kotlin/space/luminic/budgerapp/models/Recurrent.kt +++ b/src/main/kotlin/space/luminic/budgerapp/models/Recurrent.kt @@ -8,6 +8,7 @@ import java.util.Date @Document(collection = "recurrents") data class Recurrent( @Id val id: String? = null, + @DBRef var space: Space? = null, var atDay: Int, @DBRef var category: Category, var name: String, diff --git a/src/main/kotlin/space/luminic/budgerapp/models/Space.kt b/src/main/kotlin/space/luminic/budgerapp/models/Space.kt new file mode 100644 index 0000000..d1f96fc --- /dev/null +++ b/src/main/kotlin/space/luminic/budgerapp/models/Space.kt @@ -0,0 +1,25 @@ +package space.luminic.budgerapp.models + +import org.springframework.data.annotation.Id +import org.springframework.data.mongodb.core.mapping.DBRef +import org.springframework.data.mongodb.core.mapping.Document +import java.time.LocalDate +import java.time.LocalDateTime + + +@Document("spaces") +data class Space ( + @Id var id: String? = null, + var name: String? = null, + var description: String? = null, + @DBRef var owner: User? = null, + @DBRef val users: MutableList = mutableListOf(), + var invites: MutableList = mutableListOf(), + val createdAt: LocalDate = LocalDate.now(), +) + +data class SpaceInvite( + val code: String, + @DBRef val fromUser: User, + val activeTill: LocalDateTime, +) \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/budgerapp/models/Transaction.kt b/src/main/kotlin/space/luminic/budgerapp/models/Transaction.kt index 083d69c..ca90c0e 100644 --- a/src/main/kotlin/space/luminic/budgerapp/models/Transaction.kt +++ b/src/main/kotlin/space/luminic/budgerapp/models/Transaction.kt @@ -12,6 +12,7 @@ import java.util.Date @Document(collection = "transactions") data class Transaction( @Id var id: String? = null, + @DBRef var space: Space? = null, var type: TransactionType, @DBRef var user: User?=null, @DBRef var category: Category, diff --git a/src/main/kotlin/space/luminic/budgerapp/models/User.kt b/src/main/kotlin/space/luminic/budgerapp/models/User.kt index 1067991..6cf7a52 100644 --- a/src/main/kotlin/space/luminic/budgerapp/models/User.kt +++ b/src/main/kotlin/space/luminic/budgerapp/models/User.kt @@ -4,6 +4,8 @@ import com.fasterxml.jackson.annotation.JsonIgnore import jakarta.validation.constraints.NotBlank import org.springframework.data.annotation.Id import org.springframework.data.mongodb.core.mapping.Document +import java.time.LocalDate +import java.time.LocalDateTime import java.util.Date @Document("users") @@ -17,8 +19,8 @@ data class User ( @JsonIgnore // Скрывает пароль при сериализации var password: String? = null, var isActive: Boolean = true, - var regDate: Date? = null, - val createdAt: Date? = null, + var regDate: LocalDate = LocalDate.now(), + val createdAt: LocalDateTime = LocalDateTime.now(), var roles: MutableList = mutableListOf(), ) diff --git a/src/main/kotlin/space/luminic/budgerapp/repos/BudgetRepo.kt b/src/main/kotlin/space/luminic/budgerapp/repos/BudgetRepo.kt index 03c548a..ba9d93b 100644 --- a/src/main/kotlin/space/luminic/budgerapp/repos/BudgetRepo.kt +++ b/src/main/kotlin/space/luminic/budgerapp/repos/BudgetRepo.kt @@ -1,7 +1,10 @@ package space.luminic.budgerapp.repos + +import org.bson.types.ObjectId import org.springframework.data.domain.Sort +import org.springframework.data.mongodb.repository.Query import org.springframework.data.mongodb.repository.ReactiveMongoRepository import org.springframework.stereotype.Repository import reactor.core.publisher.Flux @@ -16,4 +19,10 @@ interface BudgetRepo: ReactiveMongoRepository { fun findByDateFromLessThanEqualAndDateToGreaterThanEqual(dateOne: LocalDate, dateTwo: LocalDate): Mono + @Query("{'dateFrom': {'\$lte': ?0}, 'dateTo': {'\$gte': ?1}, 'space': ?2}") + fun findByDateFromLessThanEqualAndDateToGreaterThanEqualAndSpace(dateOne: LocalDate, dateTwo: LocalDate, spaceId: ObjectId): Mono + + + @Query("{ 'space': { '\$ref': 'spaces','\$id': ?0 } }") + fun findBySpaceId(spaceId: ObjectId, sort: Sort): Flux } \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/budgerapp/repos/RecurrentRepo.kt b/src/main/kotlin/space/luminic/budgerapp/repos/RecurrentRepo.kt index 843c55c..e88c2a8 100644 --- a/src/main/kotlin/space/luminic/budgerapp/repos/RecurrentRepo.kt +++ b/src/main/kotlin/space/luminic/budgerapp/repos/RecurrentRepo.kt @@ -1,9 +1,17 @@ package space.luminic.budgerapp.repos +import org.bson.types.ObjectId +import org.springframework.data.mongodb.repository.Query import org.springframework.data.mongodb.repository.ReactiveMongoRepository import org.springframework.stereotype.Repository +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono import space.luminic.budgerapp.models.Recurrent +import space.luminic.budgerapp.models.Space @Repository interface RecurrentRepo: ReactiveMongoRepository { + + @Query("{ 'space': { '\$ref': 'spaces', '\$id': ?0 } }") + fun findRecurrentsBySpaceId(spaceID: ObjectId): Flux } \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/budgerapp/repos/SpaceRepo.kt b/src/main/kotlin/space/luminic/budgerapp/repos/SpaceRepo.kt new file mode 100644 index 0000000..fb4a037 --- /dev/null +++ b/src/main/kotlin/space/luminic/budgerapp/repos/SpaceRepo.kt @@ -0,0 +1,21 @@ +package space.luminic.budgerapp.repos + +import org.bson.types.ObjectId +import org.springframework.data.mongodb.repository.Query +import org.springframework.data.mongodb.repository.ReactiveMongoRepository +import org.springframework.stereotype.Repository +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono +import space.luminic.budgerapp.models.Space +import space.luminic.budgerapp.models.User + + +@Repository +interface SpaceRepo: ReactiveMongoRepository { + + @Query("{ 'users': { '\$ref': 'users', '\$id': ?0 } }") + fun findByArrayElement(userId: ObjectId): Flux + + @Query("{ 'invites.code': ?0 }") // Исправленный путь, чтобы находить вложенные документы + fun findSpaceByInvites(code: String): Mono +} \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/budgerapp/services/AuthService.kt b/src/main/kotlin/space/luminic/budgerapp/services/AuthService.kt index 2f4104e..265fb79 100644 --- a/src/main/kotlin/space/luminic/budgerapp/services/AuthService.kt +++ b/src/main/kotlin/space/luminic/budgerapp/services/AuthService.kt @@ -43,6 +43,26 @@ class AuthService( } } + fun register(username: String, password: String, firstName: String): Mono { + return userRepository.findByUsername(username) + .flatMap { Mono.error(IllegalArgumentException("User with username '$username' already exists")) } // Ошибка, если пользователь уже существует + .switchIfEmpty( + Mono.defer { + val newUser = User( + username = username, + password = passwordEncoder.encode(password), // Шифрование пароля + firstName = firstName, + roles = mutableListOf("USER") + ) + userRepository.save(newUser).map { user -> + user.password = null + user + } // Сохранение нового пользователя + + } + ) + } + @Cacheable("tokens") fun isTokenValid(token: String): Mono { return tokenService.getToken(token) diff --git a/src/main/kotlin/space/luminic/budgerapp/services/CategoryService.kt b/src/main/kotlin/space/luminic/budgerapp/services/CategoryService.kt index 219f728..757a68f 100644 --- a/src/main/kotlin/space/luminic/budgerapp/services/CategoryService.kt +++ b/src/main/kotlin/space/luminic/budgerapp/services/CategoryService.kt @@ -21,11 +21,7 @@ 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.models.BudgetCategory -import space.luminic.budgerapp.models.Category -import space.luminic.budgerapp.models.CategoryType -import space.luminic.budgerapp.models.SortSetting -import space.luminic.budgerapp.models.Transaction +import space.luminic.budgerapp.models.* import space.luminic.budgerapp.repos.CategoryRepo import java.time.LocalDate import java.time.LocalDateTime @@ -50,11 +46,12 @@ class CategoryService( return categoryRepo.findById(id) } -// @Cacheable("categories") - fun getCategories(type: String? = null, sortBy: String, direction: String): Mono> { + // @Cacheable("categories") + fun getCategories(spaceId: String, type: String? = null, sortBy: String, direction: String): Mono> { val matchCriteria = mutableListOf() // Добавляем фильтры + matchCriteria.add(Criteria.where("spaceDetails._id").`is`(ObjectId(spaceId))) type?.let { matchCriteria.add(Criteria.where("type.code").isEqualTo(it)) } val match = match(Criteria().andOperator(*matchCriteria.toTypedArray())) @@ -62,16 +59,17 @@ class CategoryService( val sort = sort(Sort.by(direction, sortBy)) + val lookupSpaces = lookup("spaces", "space.\$id", "_id", "spaceDetails") val aggregationBuilder = mutableListOf( - + lookupSpaces, match.takeIf { matchCriteria.isNotEmpty() }, sort, ).filterNotNull() val aggregation = newAggregation(aggregationBuilder) - + logger.error("STARTED") return mongoTemplate.aggregate( aggregation, "categories", Category::class.java ) @@ -97,7 +95,8 @@ class CategoryService( } @CacheEvict(cacheNames = ["getAllCategories"], allEntries = true) - fun createCategory(category: Category): Mono { + fun createCategory(space: Space, category: Category): Mono { + category.space = space return categoryRepo.save(category) } @@ -113,32 +112,32 @@ class CategoryService( } @CacheEvict(cacheNames = ["getAllCategories"], allEntries = true) - fun deleteCategory(categoryId: String): Mono { - return categoryRepo.findById(categoryId).switchIfEmpty( - Mono.error(IllegalArgumentException("Category with id: $categoryId not found")) - ).flatMap { - financialService.getTransactions(categoryId = categoryId) - .flatMapMany { transactions -> - categoryRepo.findByName("Другое").switchIfEmpty( - categoryRepo.save( - Category( - type = CategoryType("EXPENSE", "Траты"), - name = "Другое", - description = "Категория для других трат", - icon = "🚮" - ) - ) - ).flatMapMany { category -> - Flux.fromIterable(transactions).flatMap { transaction -> - transaction.category = category // Присваиваем конкретный объект категории - financialService.editTransaction(transaction) // Сохраняем изменения - } - } - } - .then(categoryRepo.deleteById(categoryId)) // Удаляем старую категорию - .thenReturn(categoryId) // Возвращаем удалённую категорию - } - } +// fun deleteCategory(categoryId: String): Mono { +// return categoryRepo.findById(categoryId).switchIfEmpty( +// Mono.error(IllegalArgumentException("Category with id: $categoryId not found")) +// ).flatMap { +// financialService.getTransactions(categoryId = categoryId) +// .flatMapMany { transactions -> +// categoryRepo.findByName("Другое").switchIfEmpty( +// categoryRepo.save( +// Category( +// type = CategoryType("EXPENSE", "Траты"), +// name = "Другое", +// description = "Категория для других трат", +// icon = "🚮" +// ) +// ) +// ).flatMapMany { category -> +// Flux.fromIterable(transactions).flatMap { transaction -> +// transaction.category = category // Присваиваем конкретный объект категории +// financialService.editTransaction(transaction) // Сохраняем изменения +// } +// } +// } +// .then(categoryRepo.deleteById(categoryId)) // Удаляем старую категорию +// .thenReturn(categoryId) // Возвращаем удалённую категорию +// } +// } fun getBudgetCategories(dateFrom: LocalDate, dateTo: LocalDate): Mono>> { diff --git a/src/main/kotlin/space/luminic/budgerapp/services/FinancialService.kt b/src/main/kotlin/space/luminic/budgerapp/services/FinancialService.kt index 7170f4a..7be5ea3 100644 --- a/src/main/kotlin/space/luminic/budgerapp/services/FinancialService.kt +++ b/src/main/kotlin/space/luminic/budgerapp/services/FinancialService.kt @@ -8,6 +8,7 @@ import org.springframework.cache.annotation.CacheEvict import org.springframework.cache.annotation.Cacheable import org.springframework.data.domain.Sort import org.springframework.data.domain.Sort.Direction +import org.springframework.data.mongodb.core.MongoTemplate import org.springframework.data.mongodb.core.ReactiveMongoTemplate import org.springframework.data.mongodb.core.aggregation.Aggregation.* import org.springframework.data.mongodb.core.aggregation.DateOperators.DateToString @@ -19,6 +20,7 @@ import reactor.core.publisher.Flux import reactor.core.publisher.Mono import space.luminic.budgerapp.models.* import space.luminic.budgerapp.repos.BudgetRepo +import space.luminic.budgerapp.repos.CategoryRepo import space.luminic.budgerapp.repos.TransactionRepo import space.luminic.budgerapp.repos.WarnRepo import java.time.* @@ -32,7 +34,9 @@ class FinancialService( val transactionsRepo: TransactionRepo, val recurrentService: RecurrentService, val userService: UserService, - val reactiveMongoTemplate: ReactiveMongoTemplate + val reactiveMongoTemplate: ReactiveMongoTemplate, + private val spaceService: SpaceService, + private val categoryRepo: CategoryRepo ) { private val logger = LoggerFactory.getLogger(FinancialService::class.java) @@ -191,75 +195,108 @@ class FinancialService( }.then() // Возвращаем корректный Mono } - @Cacheable("budgetsList") - fun getBudgets(sortSetting: SortSetting? = null): Mono> { - val sort = if (sortSetting != null) { - Sort.by(sortSetting.order, sortSetting.by) - } else { - Sort.by(Sort.Direction.DESC, "dateFrom") - } + fun getBudgets(spaceId: String, sortSetting: SortSetting? = null): Mono> { + val sort = sortSetting?.let { + Sort.by(it.order, it.by) + } ?: Sort.by(Sort.Direction.DESC, "dateFrom") - return budgetRepo.findAll(sort) - .collectList() // Сбор Flux в Mono> + return ReactiveSecurityContextHolder.getContext() + .map { it.authentication } + .flatMap { authentication -> + val username = authentication.name + spaceService.getSpace(spaceId) + .switchIfEmpty(Mono.error(IllegalArgumentException("Space not found for $spaceId"))) + .flatMap { space -> + userService.getByUsername(username) + .switchIfEmpty(Mono.error(IllegalArgumentException("User not found for username: $username"))) + .flatMap { user -> + val userIds = space.users.mapNotNull { it.id?.toString() } + if (user.id !in userIds) { + Mono.error(IllegalArgumentException("User cannot access this Space")) + } else { + val spaceObjectId = try { + ObjectId(space.id!!) // Преобразуем строку в ObjectId + } catch (e: IllegalArgumentException) { + return@flatMap Mono.error(IllegalArgumentException("Invalid Space ID format: ${space.id}")) + } + + println("Space ID type: ${spaceObjectId::class.java}, value: $spaceObjectId") + // Применяем сортировку к запросу + budgetRepo.findBySpaceId(spaceObjectId, sort).collectList() + } + + + } + } + } } // @Cacheable("budgets", key = "#id") fun getBudget(id: String): Mono { - return budgetRepo.findById(id) - .flatMap { budget -> - val budgetDTO = BudgetDTO( - budget.id, - budget.name, - budget.dateFrom, - budget.dateTo, - budget.createdAt, - categories = budget.categories, - incomeCategories = budget.incomeCategories, - ) + return ReactiveSecurityContextHolder.getContext() + .flatMap { securityContext -> + val username = securityContext.authentication.name + budgetRepo.findById(id) + .flatMap { budget -> + // Проверяем, что пользователь есть в space бюджета + if (!budget.space!!.users.any { it.username == username }) { + return@flatMap Mono.error(IllegalArgumentException("User does not have access to this space")) + } - logger.info("Fetching categories and transactions") - val categoriesMono = getBudgetCategories(budgetDTO.dateFrom, budgetDTO.dateTo) - val transactionsMono = - getTransactionsByTypes(budgetDTO.dateFrom, budgetDTO.dateTo) + // Если доступ есть, продолжаем процесс + 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") + val categoriesMono = getBudgetCategories(budgetDTO.dateFrom, budgetDTO.dateTo) + val transactionsMono = getTransactionsByTypes(budgetDTO.dateFrom, budgetDTO.dateTo) - Mono.zip(categoriesMono, transactionsMono) - .flatMap { tuple -> - val categories = tuple.t1 - val transactions = tuple.t2 + 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 - 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 + budgetDTO + } } } + .doOnError { error -> + logger.error("Error fetching budget: ${error.message}", error) + } + .switchIfEmpty(Mono.error(BudgetNotFoundException("Budget not found with id: $id"))) } - .doOnError { error -> - logger.error("Error fetching budget: ${error.message}", error) - } - .switchIfEmpty(Mono.error(BudgetNotFoundException("Budget not found with id: $id"))) } - fun regenCats(): Mono { + + fun regenBudgets(): Mono { return budgetRepo.findAll() .flatMap { budget -> - getCategoryTransactionPipeline(budget.dateFrom, budget.dateTo, "INCOME") - .map { categories -> - budget.incomeCategories = categories + spaceService.getSpace("67af3c0f652da946a7dd9931") + .map { space -> + budget.space = space budget } .flatMap { updatedBudget -> budgetRepo.save(updatedBudget) } @@ -267,12 +304,38 @@ class FinancialService( .then() } + fun regenTransactions(): Mono { + return transactionsRepo.findAll().flatMap { transaction -> + spaceService.getSpace("67af3c0f652da946a7dd9931") + .map { space -> + transaction.space = space + transaction + } + .flatMap { updatedTransaction -> transactionsRepo.save(updatedTransaction) } + } + .then() + } + + + fun regenCats(): Mono { + return categoryRepo.findAll()// Получаем список категорий + .flatMap { cat -> + spaceService.getSpace("67af3c0f652da946a7dd9931") // Получаем space + .map { space -> + cat.space = space // Привязываем пространство к категории + cat + } + } + .flatMap { updatedCategory -> categoryRepo.save(updatedCategory) } // Сохраняем в БД + .then() // Завершаем Mono + } + @CacheEvict(cacheNames = ["budgets", "budgetsList"], allEntries = true) - fun createBudget(budget: Budget, createRecurrent: Boolean): Mono { + fun createBudget(spaceId: String, budget: Budget, createRecurrent: Boolean): Mono { return Mono.zip( - getBudgetByDate(budget.dateFrom).map { Optional.ofNullable(it) } + getBudgetByDate(budget.dateFrom, spaceId).map { Optional.ofNullable(it) } .switchIfEmpty(Mono.just(Optional.empty())), - getBudgetByDate(budget.dateTo).map { Optional.ofNullable(it) } + getBudgetByDate(budget.dateTo, spaceId).map { Optional.ofNullable(it) } .switchIfEmpty(Mono.just(Optional.empty())) ).flatMap { tuple -> val startBudget = tuple.t1.orElse(null) @@ -283,36 +346,59 @@ class FinancialService( return@flatMap Mono.error(IllegalArgumentException("Бюджет с теми же датами найден")) } - // Если createRecurrent=true, создаем рекуррентные транзакции - val recurrentsCreation = if (createRecurrent) { - recurrentService.createRecurrentsForBudget(budget) - } else { - Mono.empty() - } + // Получаем Space по spaceId + return@flatMap spaceService.getSpace(spaceId) + .switchIfEmpty(Mono.error(IllegalArgumentException("Space not found for $spaceId"))) - // Создаем бюджет после возможного создания рекуррентных транзакций - recurrentsCreation.then( - getCategoryTransactionPipeline(budget.dateFrom, budget.dateTo) - .flatMap { categories -> - budget.categories = categories - budgetRepo.save(budget) - } - .publishOn(reactor.core.scheduler.Schedulers.boundedElastic()) - .doOnNext { savedBudget -> - // Выполнение updateBudgetWarns в фоне - updateBudgetWarns(budget = savedBudget) - .doOnError { error -> - // Логируем ошибку, если произошла - println("Error during updateBudgetWarns: ${error.message}") + .flatMap { space -> + // Проверяем, входит ли пользователь в этот Space + ReactiveSecurityContextHolder.getContext().flatMap { securityContext -> + val username = securityContext.authentication.name + userService.getByUsername(username) + .switchIfEmpty(Mono.error(IllegalArgumentException("User not found for username: $username"))) + .flatMap { user -> + if (space.users.none { it.id == user.id }) { + return@flatMap Mono.error(IllegalArgumentException("User does not have access to this space")) + } + + // Присваиваем Space бюджету + budget.space = space + + // Если createRecurrent=true, создаем рекуррентные транзакции + val recurrentsCreation = if (createRecurrent) { + recurrentService.createRecurrentsForBudget(space, budget) + } else { + Mono.empty() + } + + // Создаем бюджет после возможного создания рекуррентных транзакций + recurrentsCreation.then( + getCategoryTransactionPipeline(budget.dateFrom, budget.dateTo) + .flatMap { categories -> + budget.categories = categories + budgetRepo.save(budget) + } + .publishOn(reactor.core.scheduler.Schedulers.boundedElastic()) + .doOnNext { savedBudget -> + // Выполнение updateBudgetWarns в фоне + updateBudgetWarns(budget = savedBudget) + .doOnError { error -> + // Логируем ошибку, если произошла + println("Error during updateBudgetWarns: ${error.message}") + } + .subscribe() + } + ) } - .subscribe() } - ) + + } } } - fun getBudgetByDate(date: LocalDate): Mono { - return budgetRepo.findByDateFromLessThanEqualAndDateToGreaterThanEqual(date, date).switchIfEmpty(Mono.empty()) + fun getBudgetByDate(date: LocalDate, spaceId: String): Mono { + return budgetRepo.findByDateFromLessThanEqualAndDateToGreaterThanEqualAndSpace(date, date, ObjectId(spaceId)) + .switchIfEmpty(Mono.empty()) } @@ -530,6 +616,7 @@ class FinancialService( @Cacheable("transactions") fun getTransactions( + spaceId: String, dateFrom: LocalDate? = null, dateTo: LocalDate? = null, transactionType: String? = null, @@ -543,46 +630,71 @@ class FinancialService( limit: Int? = null, offset: Int? = null, ): Mono> { - val matchCriteria = mutableListOf() + return ReactiveSecurityContextHolder.getContext() + .map { it.authentication } + .flatMap { authentication -> + val username = authentication.name + spaceService.getSpace(spaceId) + .switchIfEmpty(Mono.error(IllegalArgumentException("Space not found for $spaceId"))) + .flatMap { space -> + userService.getByUsername(username) + .switchIfEmpty(Mono.error(IllegalArgumentException("User not found for username: $username"))) + .flatMap { user -> + if (space.users.none { it.id.toString() == user.id }) { + return@flatMap Mono.error>(IllegalArgumentException("User does not have access to this Space")) + } - // Добавляем фильтры - dateFrom?.let { matchCriteria.add(Criteria.where("date").gte(it)) } - dateTo?.let { matchCriteria.add(Criteria.where("date").lt(it)) } - transactionType?.let { matchCriteria.add(Criteria.where("type.code").`is`(it)) } - isDone?.let { matchCriteria.add(Criteria.where("isDone").`is`(it)) } - categoryId?.let { matchCriteria.add(Criteria.where("categoryDetails._id").`is`(it)) } - categoryType?.let { matchCriteria.add(Criteria.where("categoryDetails.type.code").`is`(it)) } - userId?.let { matchCriteria.add(Criteria.where("userDetails._id").`is`(ObjectId(it))) } - parentId?.let { matchCriteria.add(Criteria.where("parentId").`is`(it)) } - isChild?.let { matchCriteria.add(Criteria.where("parentId").exists(it)) } + val matchCriteria = mutableListOf() - // Сборка агрегации - val lookup = lookup("categories", "category.\$id", "_id", "categoryDetails") - val lookupUsers = lookup("users", "user.\$id", "_id", "userDetails") - val match = match(Criteria().andOperator(*matchCriteria.toTypedArray())) + // Добавляем фильтры + matchCriteria.add(Criteria.where("spaceDetails._id").`is`(ObjectId(spaceId))) + dateFrom?.let { matchCriteria.add(Criteria.where("date").gte(it)) } + dateTo?.let { matchCriteria.add(Criteria.where("date").lt(it)) } + transactionType?.let { matchCriteria.add(Criteria.where("type.code").`is`(it)) } + isDone?.let { matchCriteria.add(Criteria.where("isDone").`is`(it)) } + categoryId?.let { matchCriteria.add(Criteria.where("categoryDetails._id").`is`(it)) } + categoryType?.let { + matchCriteria.add( + Criteria.where("categoryDetails.type.code").`is`(it) + ) + } + userId?.let { matchCriteria.add(Criteria.where("userDetails._id").`is`(ObjectId(it))) } + parentId?.let { matchCriteria.add(Criteria.where("parentId").`is`(it)) } + isChild?.let { matchCriteria.add(Criteria.where("parentId").exists(it)) } - var sort = sort(Sort.by(Direction.DESC, "date").and(Sort.by(Direction.DESC, "createdAt"))) + // Сборка агрегации + val lookup = lookup("categories", "category.\$id", "_id", "categoryDetails") + val lookupSpaces = lookup("spaces", "space.\$id", "_id", "spaceDetails") + val lookupUsers = lookup("users", "user.\$id", "_id", "userDetails") + val match = match(Criteria().andOperator(*matchCriteria.toTypedArray())) - sortSetting?.let { - sort = sort(Sort.by(it.order, it.by).and(Sort.by(Direction.ASC, "createdAt"))) - } + var sort = + sort(Sort.by(Direction.DESC, "date").and(Sort.by(Direction.DESC, "createdAt"))) - val aggregationBuilder = mutableListOf( - lookup, - lookupUsers, - match.takeIf { matchCriteria.isNotEmpty() }, - sort, - offset?.let { skip(it.toLong()) }, - limit?.let { limit(it.toLong()) } - ).filterNotNull() + sortSetting?.let { + sort = sort(Sort.by(it.order, it.by).and(Sort.by(Direction.ASC, "createdAt"))) + } - val aggregation = newAggregation(aggregationBuilder) + val aggregationBuilder = mutableListOf( + lookup, + lookupSpaces, + lookupUsers, + match.takeIf { matchCriteria.isNotEmpty() }, + sort, + offset?.let { skip(it.toLong()) }, + limit?.let { limit(it.toLong()) } + ).filterNotNull() - return reactiveMongoTemplate.aggregate( - aggregation, "transactions", Transaction::class.java - ) - .collectList() // Преобразуем Flux в Mono> - .map { it.toMutableList() } + val aggregation = newAggregation(aggregationBuilder) + + return@flatMap reactiveMongoTemplate.aggregate( + aggregation, "transactions", Transaction::class.java + ) + .collectList() + .map { it.toMutableList() } + } + } + } } fun getTransactionsToDelete(dateFrom: LocalDate, dateTo: LocalDate): Mono> { @@ -618,19 +730,29 @@ class FinancialService( @CacheEvict(cacheNames = ["transactions"], allEntries = true) - fun createTransaction(transaction: Transaction): Mono { + fun createTransaction(spaceId: String, 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 -> - transaction.user = user - transactionsRepo.save(transaction) - .flatMap { savedTransaction -> - updateBudgetOnCreate(savedTransaction) - .thenReturn(savedTransaction) // Ждём выполнения updateBudgetOnCreate перед возвратом + spaceService.getSpace(spaceId) + .switchIfEmpty(Mono.error(IllegalArgumentException("Space not found for $spaceId"))) + .flatMap { space -> + userService.getByUsername(username) + .switchIfEmpty(Mono.error(IllegalArgumentException("User not found for username: $username"))) + .flatMap { user -> + if (space.users.none { it.id.toString() == user.id }) { + return@flatMap Mono.error(IllegalArgumentException("User does not have access to this Space")) + } + // Привязываем space и user к транзакции + transaction.user = user + transaction.space = space + + transactionsRepo.save(transaction) + .flatMap { savedTransaction -> + updateBudgetOnCreate(savedTransaction) + .thenReturn(savedTransaction) // Ждём выполнения updateBudgetOnCreate перед возвратом + } } } } @@ -1034,8 +1156,8 @@ class FinancialService( tgUserName = userDocument["tgUserName"]?.let { it as String }, password = null, isActive = userDocument["isActive"] as Boolean, - regDate = userDocument["regDate"] as Date, - createdAt = userDocument["createdAt"] as Date, + regDate = userDocument["regDate"] as LocalDate, + createdAt = userDocument["createdAt"] as LocalDateTime, roles = userDocument["roles"] as ArrayList, ) @@ -1051,6 +1173,7 @@ class FinancialService( ) return Transaction( (document["_id"] as ObjectId).toString(), + null, TransactionType( transactionType["code"] as String, transactionType["name"] as String @@ -1582,7 +1705,7 @@ class FinancialService( .collectList() } - fun getCategorySummaries(dateFrom: LocalDate): Mono> { + fun getCategorySummaries(spaceId: String, dateFrom: LocalDate): Mono> { val sixMonthsAgo = Date.from( LocalDateTime.of(dateFrom, LocalTime.MIN) .atZone(ZoneId.systemDefault()) @@ -1591,6 +1714,16 @@ class FinancialService( val aggregation = listOf( // 1. Фильтр за последние 6 месяцев + Document( + "\$lookup", Document("from", "spaces") + .append("localField", "space.\$id") + .append("foreignField", "_id") + .append("as", "spaceInfo") + ), + + // 4. Распаковываем массив категорий + Document("\$unwind", "\$spaceInfo"), + Document("\$match", Document("spaceInfo._id", ObjectId(spaceId))), Document( "\$match", Document("date", Document("\$gte", sixMonthsAgo).append("\$lt", Date())).append("type.code", "INSTANT") @@ -1720,6 +1853,7 @@ class FinancialService( Document("\$sort", Document("categoryName", 1)) ) + // Выполняем агрегацию return reactiveMongoTemplate.getCollection("transactions") .flatMapMany { it.aggregate(aggregation) } diff --git a/src/main/kotlin/space/luminic/budgerapp/services/RecurrentService.kt b/src/main/kotlin/space/luminic/budgerapp/services/RecurrentService.kt index 2090c7f..e806293 100644 --- a/src/main/kotlin/space/luminic/budgerapp/services/RecurrentService.kt +++ b/src/main/kotlin/space/luminic/budgerapp/services/RecurrentService.kt @@ -1,17 +1,14 @@ package space.luminic.budgerapp.services +import org.bson.types.ObjectId import org.slf4j.LoggerFactory import org.springframework.cache.annotation.CacheEvict import org.springframework.cache.annotation.Cacheable import org.springframework.security.core.context.ReactiveSecurityContextHolder import org.springframework.stereotype.Service import reactor.core.publisher.Mono -import space.luminic.budgerapp.models.Budget -import space.luminic.budgerapp.models.NotFoundException -import space.luminic.budgerapp.models.Recurrent -import space.luminic.budgerapp.models.Transaction -import space.luminic.budgerapp.models.TransactionType +import space.luminic.budgerapp.models.* import space.luminic.budgerapp.repos.RecurrentRepo import space.luminic.budgerapp.repos.TransactionRepo import java.time.YearMonth @@ -22,31 +19,40 @@ class RecurrentService( private val recurrentRepo: RecurrentRepo, private val transactionRepo: TransactionRepo, private val userService: UserService, + private val spaceService: SpaceService, ) { private val logger = LoggerFactory.getLogger(javaClass) @Cacheable("recurrentsList") - fun getRecurrents(): Mono> { - return recurrentRepo.findAll().collectList() + fun getRecurrents(space: Space): Mono> { + + // Запрос рекуррентных платежей + return recurrentRepo.findRecurrentsBySpaceId(ObjectId(space.id)) + .collectList() // Преобразуем Flux в Mono> } + @Cacheable("recurrents", key = "#id") - fun getRecurrentById(id: String): Mono { + fun getRecurrentById(space: Space, id: String): Mono { + // Запрос рекуррентных платежей return recurrentRepo.findById(id) .switchIfEmpty(Mono.error(NotFoundException("Recurrent with id: $id not found"))) } @CacheEvict(cacheNames = ["recurrentsList", "recurrents"]) - fun createRecurrent(recurrent: Recurrent): Mono { - return if (recurrent.id == null && recurrent.atDay <= 31) recurrentRepo.save(recurrent) else Mono.error( + fun createRecurrent(space: Space, recurrent: Recurrent): Mono { + return if (recurrent.id == null && recurrent.atDay <= 31) { + recurrent.space = space + recurrentRepo.save(recurrent) + } else Mono.error( RuntimeException("Cannot create recurrent with id or date cannot be higher than 31") ) } @CacheEvict(cacheNames = ["recurrentsList", "recurrents"]) - fun createRecurrentsForBudget(budget: Budget): Mono { + fun createRecurrentsForBudget(space: Space, budget: Budget): Mono { val currentYearMonth = YearMonth.of(budget.dateFrom.year, budget.dateFrom.monthValue) val daysInCurrentMonth = currentYearMonth.lengthOfMonth() val context = ReactiveSecurityContextHolder.getContext() @@ -63,7 +69,7 @@ class RecurrentService( .switchIfEmpty(Mono.error(IllegalArgumentException("User not found for username: $username"))) } .flatMapMany { user -> - recurrentRepo.findAll() + recurrentRepo.findRecurrentsBySpaceId(ObjectId(space.id)) .map { recurrent -> // Определяем дату транзакции val transactionDate = when { @@ -111,7 +117,17 @@ class RecurrentService( return recurrentRepo.deleteById(id) } - + fun regenRecurrents(): Mono> { + return recurrentRepo.findAll() + .flatMap { recurrent -> + spaceService.getSpace("67af3c0f652da946a7dd9931") + .flatMap { space -> + recurrent.space = space + recurrentRepo.save(recurrent) // Сохраняем и возвращаем сохраненный объект + } + } + .collectList() // Собираем результаты в список + } } \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/budgerapp/services/SpaceService.kt b/src/main/kotlin/space/luminic/budgerapp/services/SpaceService.kt new file mode 100644 index 0000000..1b63add --- /dev/null +++ b/src/main/kotlin/space/luminic/budgerapp/services/SpaceService.kt @@ -0,0 +1,233 @@ +package space.luminic.budgerapp.services + +import org.bson.types.ObjectId +import org.springframework.data.domain.Sort +import org.springframework.data.domain.Sort.Direction +import org.springframework.security.core.context.ReactiveSecurityContextHolder +import org.springframework.stereotype.Service +import reactor.core.publisher.Mono +import space.luminic.budgerapp.models.Space +import space.luminic.budgerapp.models.SpaceInvite +import space.luminic.budgerapp.models.Transaction +import space.luminic.budgerapp.repos.BudgetRepo +import space.luminic.budgerapp.repos.SpaceRepo +import space.luminic.budgerapp.repos.UserRepo +import java.time.LocalDateTime +import java.util.UUID + +@Service +class SpaceService( + private val spaceRepo: SpaceRepo, + private val userService: UserService, + private val budgetRepo: BudgetRepo, + private val userRepo: UserRepo +) { + + 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 + 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")) + } + // Если проверка прошла успешно, возвращаем пространство + Mono.just(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"))) + .flatMapMany { user -> + spaceRepo.findByArrayElement(ObjectId(user.id!!)) + } + .collectList() // Возвращаем Mono> + } + } + + fun getSpace(spaceId: String): Mono { + return spaceRepo.findById(spaceId) + .switchIfEmpty(Mono.error(IllegalArgumentException("SpaceId not found for spaceId: $spaceId"))) + } + + fun createSpace(space: Space): 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) + } + } + } + + fun deleteSpace(spaceId: String): Mono { + return budgetRepo.findBySpaceId(ObjectId(spaceId), Sort.by(Direction.DESC, "dateFrom")) + .flatMap { budget -> + budgetRepo.delete(budget) // Удаляем все бюджеты, связанные с этим Space + } + .then(spaceRepo.deleteById(spaceId)) // Затем удаляем сам 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) + } + } + } + } + + + 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) + } + } + } + } + + 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 + } + } + } + } + + 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")) + } + } + } + } + } + } + +// fun regenSpaces(): Mono> { +// return spaceRepo.findAll() +// .flatMap { space -> +// userService.getUsers() +// .flatMap { users -> +// if (users.isEmpty()) { +// return@flatMap Mono.error(IllegalStateException("No users found")) +// } +// val updatedSpace = space.copy(owner = users.first()) // Создаем копию (если `Space` data class) +// spaceRepo.save(updatedSpace) +// } +// } +// .collectList() +// } + +} \ No newline at end of file diff --git a/src/main/resources/persprof.json b/src/main/resources/persprof.json new file mode 100644 index 0000000..425c40f --- /dev/null +++ b/src/main/resources/persprof.json @@ -0,0 +1,52 @@ +[ + { + "$lookup": { + "from": "categories", + "localField": "category.$id", + "foreignField": "_id", + "as": "categoryDetails" + } + }, + { + "$lookup": { + "from": "spaces", + "localField": "space.$id", + "foreignField": "_id", + "as": "spaceDetails" + } + }, + { + "$lookup": { + "from": "users", + "localField": "user.$id", + "foreignField": "_id", + "as": "userDetails" + } + }, + { + "$match": { + "$and": [ + { + "spaceDetails._id": { + "$oid": "67af52e8b0aa7b0f7f74b491" + } + }, + { + "type.code": "INSTANT" + } + ] + } + }, + { + "$sort": { + "date": -1, + "createdAt": -1 + } + }, + { + "$skip": 0 + }, + { + "$limit": 10 + } +] \ No newline at end of file