add spaces

This commit is contained in:
xds
2025-02-17 17:58:07 +03:00
parent a5b334f6c2
commit d680345a9f
23 changed files with 1097 additions and 285 deletions

View File

@@ -26,7 +26,7 @@ class BearerTokenFilter(private val authService: AuthService) : SecurityContextS
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> { override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
val token = exchange.request.headers.getFirst(HttpHeaders.AUTHORIZATION)?.removePrefix("Bearer ") 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") .startsWith("/api/actuator")
) { ) {
return chain.filter(exchange) return chain.filter(exchange)

View File

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

View File

@@ -22,6 +22,11 @@ class AuthController(
.map { token -> mapOf("token" to token) } .map { token -> mapOf("token" to token) }
} }
@PostMapping("/register")
fun register(@RequestBody request: RegisterRequest): Mono<User> {
return authService.register(request.username, request.password, request.firstName)
}
@GetMapping("/me") @GetMapping("/me")
fun getMe(@RequestHeader("Authorization") token: String): Mono<User> { fun getMe(@RequestHeader("Authorization") token: String): Mono<User> {
@@ -33,3 +38,4 @@ class AuthController(
} }
data class AuthRequest(val username: String, val password: String) data class AuthRequest(val username: String, val password: String)
data class RegisterRequest(val username: String, val password: String, val firstName: String)

View File

@@ -30,8 +30,8 @@ class BudgetController(
private val logger = LoggerFactory.getLogger(BudgetController::class.java) private val logger = LoggerFactory.getLogger(BudgetController::class.java)
@GetMapping @GetMapping
fun getBudgets(): Mono<MutableList<Budget>> { fun getBudgets(): Mono<List<Budget>> {
return financialService.getBudgets() return financialService.getBudgets("123")
} }
@GetMapping("/{id}") @GetMapping("/{id}")
@@ -40,10 +40,10 @@ class BudgetController(
} }
@GetMapping("/by-dates") // @GetMapping("/by-dates")
fun getBudgetByDate(@RequestParam date: LocalDate): ResponseEntity<Any> { // fun getBudgetByDate(@RequestParam date: LocalDate): ResponseEntity<Any> {
return ResponseEntity.ok(financialService.getBudgetByDate(date)) // return ResponseEntity.ok(financialService.getBudgetByDate(date))
} // }
@GetMapping("/{id}/categories") @GetMapping("/{id}/categories")
@@ -56,13 +56,13 @@ class BudgetController(
return financialService.getBudgetTransactionsByType(id) return financialService.getBudgetTransactionsByType(id)
} }
@PostMapping("/") // @PostMapping("/")
fun createBudget(@RequestBody budgetCreationDTO: BudgetCreationDTO): Mono<Budget> { // fun createBudget(@RequestBody budgetCreationDTO: BudgetCreationDTO): Mono<Budget> {
return financialService.createBudget( // return financialService.createBudget(
budgetCreationDTO.budget, // budgetCreationDTO.budget,
budgetCreationDTO.createRecurrent // budgetCreationDTO.createRecurrent
) // )
} // }
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
fun deleteBudget(@PathVariable id: String): Mono<Void> { fun deleteBudget(@PathVariable id: String): Mono<Void> {
@@ -83,7 +83,6 @@ class BudgetController(
} }
@GetMapping("/{id}/warns") @GetMapping("/{id}/warns")
fun budgetWarns(@PathVariable id: String, @RequestParam hidden: Boolean? = null): Mono<List<Warn>> { fun budgetWarns(@PathVariable id: String, @RequestParam hidden: Boolean? = null): Mono<List<Warn>> {
return financialService.getWarns(id, hidden) return financialService.getWarns(id, hidden)
@@ -93,11 +92,16 @@ class BudgetController(
fun setWarnHide(@PathVariable id: String, @PathVariable warnId: String): Mono<Warn> { fun setWarnHide(@PathVariable id: String, @PathVariable warnId: String): Mono<Warn> {
return financialService.hideWarn(warnId) return financialService.hideWarn(warnId)
} }
//
@GetMapping("/regencats") // @GetMapping("/regencats")
fun regenCats(): Mono<Void>{ // fun regenCats(): Mono<Void> {
return financialService.regenCats() // return financialService.regenCats()
} // }
//
// @GetMapping("/regenSpaces")
// fun regenSpaces(): Mono<Void> {
// return financialService.regenBudgets()
// }
data class LimitValue( data class LimitValue(
var limit: Double var limit: Double

View File

@@ -31,61 +31,61 @@ class CategoriesController(
private val logger = LoggerFactory.getLogger(javaClass) private val logger = LoggerFactory.getLogger(javaClass)
//
@GetMapping() // @GetMapping()
fun getCategories( // fun getCategories(
@RequestParam("type") type: String? = null, // @RequestParam("type") type: String? = null,
@RequestParam("sort") sortBy: String = "name", // @RequestParam("sort") sortBy: String = "name",
@RequestParam("direction") direction: String = "ASC" // @RequestParam("direction") direction: String = "ASC"
): Mono<List<Category>> { // ): Mono<List<Category>> {
return categoryService.getCategories(type, sortBy, direction) // return categoryService.getCategories(type, sortBy, direction)
//
} // }
//
@GetMapping("/types") // @GetMapping("/types")
fun getCategoriesTypes(): ResponseEntity<Any> { // fun getCategoriesTypes(): ResponseEntity<Any> {
return try { // return try {
ResponseEntity.ok(categoryService.getCategoryTypes()) // ResponseEntity.ok(categoryService.getCategoryTypes())
} catch (e: Exception) { // } catch (e: Exception) {
ResponseEntity(HttpClientErrorException(HttpStatus.INTERNAL_SERVER_ERROR), HttpStatus.INTERNAL_SERVER_ERROR) // ResponseEntity(HttpClientErrorException(HttpStatus.INTERNAL_SERVER_ERROR), HttpStatus.INTERNAL_SERVER_ERROR)
//
} // }
} // }
//
@PostMapping() // @PostMapping()
fun createCategory(@RequestBody category: Category): Mono<Category> { // fun createCategory(@RequestBody category: Category): Mono<Category> {
return categoryService.createCategory(category) // return categoryService.createCategory(category)
} // }
//
@PutMapping("/{categoryId}") // @PutMapping("/{categoryId}")
fun editCategory(@PathVariable categoryId: String, @RequestBody category: Category): Mono<Category> { // fun editCategory(@PathVariable categoryId: String, @RequestBody category: Category): Mono<Category> {
return categoryService.editCategory(category) // return categoryService.editCategory(category)
} // }
//
@DeleteMapping("/{categoryId}") //// @DeleteMapping("/{categoryId}")
fun deleteCategory(@PathVariable categoryId: String): Mono<String> { //// fun deleteCategory(@PathVariable categoryId: String): Mono<String> {
return categoryService.deleteCategory(categoryId) //// return categoryService.deleteCategory(categoryId)
} //// }
//
@GetMapping("/test") // @GetMapping("/test")
fun test(): Mono<MutableList<BudgetCategory>> { // fun test(): Mono<MutableList<BudgetCategory>> {
var dateFrom = LocalDate.parse("2025-01-10") // var dateFrom = LocalDate.parse("2025-01-10")
var dateTo = LocalDate.parse("2025-02-09") // var dateTo = LocalDate.parse("2025-02-09")
//
return financialService.getCategoryTransactionPipeline(dateFrom, dateTo) // return financialService.getCategoryTransactionPipeline(dateFrom, dateTo)
//
} // }
//
//
@GetMapping("/by-month") // @GetMapping("/by-month")
fun getCategoriesSumsByMonths(): Mono<List<Document>> { // fun getCategoriesSumsByMonths(): Mono<List<Document>> {
return financialService.getCategorySumsPipeline(LocalDate.of(2024, 8, 1), LocalDate.of(2025, 1, 12)) // return financialService.getCategorySumsPipeline(LocalDate.of(2024, 8, 1), LocalDate.of(2025, 1, 12))
} // }
//
@GetMapping("/by-month2") // @GetMapping("/by-month2")
fun getCategoriesSumsByMonthsV2(): Mono<List<Document>> { // fun getCategoriesSumsByMonthsV2(@RequestParam spaceId: String): Mono<List<Document>> {
return financialService.getCategorySummaries(LocalDate.now().minusMonths(6)) // return financialService.getCategorySummaries(spaceId, LocalDate.now().minusMonths(6))
} // }
} }

View File

@@ -17,33 +17,38 @@ import space.luminic.budgerapp.services.RecurrentService
class RecurrentController ( class RecurrentController (
private val recurrentService: RecurrentService private val recurrentService: RecurrentService
){ ){
//
@GetMapping("/") // @GetMapping("/")
fun getRecurrents(): Mono<List<Recurrent>> { // fun getRecurrents(): Mono<List<Recurrent>> {
return recurrentService.getRecurrents() // return recurrentService.getRecurrents()
} // }
//
//
@GetMapping("/{id}") // @GetMapping("/{id}")
fun getRecurrent(@PathVariable id: String): Mono<Recurrent> { // fun getRecurrent(@PathVariable id: String): Mono<Recurrent> {
return recurrentService.getRecurrentById(id) // return recurrentService.getRecurrentById(id)
} // }
//
@PostMapping("/") // @PostMapping("/")
fun createRecurrent(@RequestBody recurrent: Recurrent): Mono<Recurrent> { // fun createRecurrent(@RequestBody recurrent: Recurrent): Mono<Recurrent> {
return recurrentService.createRecurrent(recurrent) // return recurrentService.createRecurrent(recurrent)
} // }
//
@PutMapping("/{id}") // @PutMapping("/{id}")
fun editRecurrent(@PathVariable id: String, @RequestBody recurrent: Recurrent): Mono<Recurrent> { // fun editRecurrent(@PathVariable id: String, @RequestBody recurrent: Recurrent): Mono<Recurrent> {
return recurrentService.editRecurrent(recurrent) // return recurrentService.editRecurrent(recurrent)
} // }
//
//
@DeleteMapping("/{id}") // @DeleteMapping("/{id}")
fun deleteRecurrent(@PathVariable id: String): Mono<Void> { // fun deleteRecurrent(@PathVariable id: String): Mono<Void> {
return recurrentService.deleteRecurrent(id) // return recurrentService.deleteRecurrent(id)
} // }
//
// @GetMapping("/regen")
// fun regenRecurrents(): Mono<List<Recurrent>> {
// return recurrentService.regenRecurrents()
// }

View File

@@ -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<List<Space>> {
return spaceService.getSpaces()
}
@PostMapping
fun createSpace(@RequestBody space: Space): Mono<Space> {
return spaceService.createSpace(space)
}
@GetMapping("{spaceId}")
fun getSpace(@PathVariable spaceId: String): Mono<Space> {
return spaceService.getSpace(spaceId)
}
@DeleteMapping("/{spaceId}")
fun deleteSpace(@PathVariable spaceId: String): Mono<Void> {
return spaceService.deleteSpace(spaceId)
}
@PostMapping("/{spaceId}/invite")
fun inviteSpace(@PathVariable spaceId: String): Mono<SpaceInvite> {
return spaceService.createInviteSpace(spaceId)
}
@PostMapping("/invite/{code}")
fun acceptInvite(@PathVariable code: String): Mono<Space> {
return spaceService.acceptInvite(code)
}
@DeleteMapping("/{spaceId}/leave")
fun leaveSpace(@PathVariable spaceId: String): Mono<Void> {
return spaceService.leaveSpace(spaceId)
}
@DeleteMapping("/{spaceId}/members/kick/{username}")
fun kickMembers(@PathVariable spaceId: String, @PathVariable username: String): Mono<Void> {
return spaceService.kickMember(spaceId, username)
}
//
//Budgets API
//
@GetMapping("{spaceId}/budgets")
fun getBudgets(@PathVariable spaceId: String): Mono<List<Budget>> {
return financialService.getBudgets(spaceId)
}
@PostMapping("/{spaceId}/budgets")
fun createBudget(
@PathVariable spaceId: String,
@RequestBody budgetCreationDTO: BudgetCreationDTO,
): Mono<Budget> {
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<Any> {
try {
return ResponseEntity.ok(
financialService.getTransactions(
spaceId = spaceId,
transactionType = transactionType,
categoryType = categoryType,
userId = userId,
isChild = isChild,
limit = limit,
offset = offset
)
)
} catch (e: Exception) {
e.printStackTrace()
return ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR)
}
}
@GetMapping("/{spaceId}/transactions/{id}")
fun getTransaction(
@PathVariable spaceId: String,
@PathVariable id: String
): ResponseEntity<Any> {
try {
return ResponseEntity.ok(financialService.getTransactionById(id))
} catch (e: Exception) {
e.printStackTrace()
return ResponseEntity(e.message, HttpStatus.INTERNAL_SERVER_ERROR)
}
}
@PostMapping("/{spaceId}/transactions")
fun createTransaction(@PathVariable spaceId: String, @RequestBody transaction: Transaction): ResponseEntity<Any> {
try {
return ResponseEntity.ok(financialService.createTransaction(spaceId, transaction))
} catch (e: Exception) {
e.printStackTrace()
return ResponseEntity(e.message, HttpStatus.INTERNAL_SERVER_ERROR)
}
}
@PutMapping("/{spaceId}/transactions/{id}")
fun editTransaction(
@PathVariable spaceId: String, @PathVariable id: String, @RequestBody transaction: Transaction
): ResponseEntity<Any> {
try {
return ResponseEntity.ok(financialService.editTransaction(transaction))
} catch (e: Exception) {
e.printStackTrace()
return ResponseEntity(e.message, HttpStatus.INTERNAL_SERVER_ERROR)
}
}
@DeleteMapping("/{spaceId}/transactions/{id}")
fun deleteTransaction(@PathVariable spaceId: String, @PathVariable id: String): Mono<Void> {
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<List<Category>> {
return spaceService.isValidRequest(spaceId).flatMap {
categoryService.getCategories(spaceId, type, sortBy, direction)
}
}
@GetMapping("/{spaceId}/categories/types")
fun getCategoriesTypes(): ResponseEntity<Any> {
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<Category> {
return spaceService.isValidRequest(spaceId).flatMap {
categoryService.createCategory(it,category)
}
}
@PutMapping("/{spaceId}/{categoryId}")
fun editCategory(@PathVariable categoryId: String, @RequestBody category: Category): Mono<Category> {
return categoryService.editCategory(category)
}
@GetMapping("/by-month")
fun getCategoriesSumsByMonths(): Mono<List<Document>> {
return financialService.getCategorySumsPipeline(LocalDate.of(2024, 8, 1), LocalDate.of(2025, 1, 12))
}
@GetMapping("/{spaceId}/analytics/by-month")
fun getCategoriesSumsByMonthsV2(@PathVariable spaceId: String): Mono<List<Document>> {
return financialService.getCategorySummaries(spaceId, LocalDate.now().minusMonths(6))
}
//
// Recurrents API
//
@GetMapping("/{spaceId}/recurrents")
fun getRecurrents(@PathVariable spaceId: String): Mono<List<Recurrent>> {
return spaceService.isValidRequest(spaceId).flatMap {
recurrentService.getRecurrents(it)
}
}
@GetMapping("/{spaceId}/recurrents/{id}")
fun getRecurrent(@PathVariable spaceId: String, @PathVariable id: String): Mono<Recurrent> {
return spaceService.isValidRequest(spaceId).flatMap {
recurrentService.getRecurrentById(it, id)
}
}
@PostMapping("/{spaceId}/recurrent")
fun createRecurrent(@PathVariable spaceId: String, @RequestBody recurrent: Recurrent): Mono<Recurrent> {
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<Recurrent> {
return spaceService.isValidRequest(spaceId).flatMap {
recurrentService.editRecurrent(recurrent)
}
}
@DeleteMapping("/{spaceId}/recurrent/{id}")
fun deleteRecurrent(@PathVariable spaceId: String, @PathVariable id: String): Mono<Void> {
return spaceService.isValidRequest(spaceId).flatMap {
recurrentService.deleteRecurrent(id)
}
}
// @GetMapping("/regen")
// fun regenSpaces(): Mono<List<Space>> {
// return spaceService.regenSpaces()
// }
}

View File

@@ -26,6 +26,7 @@ class TransactionController(private val financialService: FinancialService) {
@GetMapping @GetMapping
fun getTransactions( fun getTransactions(
@RequestParam spaceId: String,
@RequestParam(value = "transaction_type") transactionType: String? = null, @RequestParam(value = "transaction_type") transactionType: String? = null,
@RequestParam(value = "category_type") categoryType: String? = null, @RequestParam(value = "category_type") categoryType: String? = null,
@RequestParam(value = "user_id") userId: String? = null, @RequestParam(value = "user_id") userId: String? = null,
@@ -36,6 +37,7 @@ class TransactionController(private val financialService: FinancialService) {
try { try {
return ResponseEntity.ok( return ResponseEntity.ok(
financialService.getTransactions( financialService.getTransactions(
spaceId = spaceId,
transactionType = transactionType, transactionType = transactionType,
categoryType = categoryType, categoryType = categoryType,
userId = userId, userId = userId,
@@ -61,9 +63,9 @@ class TransactionController(private val financialService: FinancialService) {
} }
@PostMapping @PostMapping
fun createTransaction(@RequestBody transaction: Transaction): ResponseEntity<Any> { fun createTransaction(@RequestParam spaceId: String, @RequestBody transaction: Transaction): ResponseEntity<Any> {
try { try {
return ResponseEntity.ok(financialService.createTransaction(transaction)) return ResponseEntity.ok(financialService.createTransaction(spaceId,transaction))
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
return ResponseEntity(e.message, HttpStatus.INTERNAL_SERVER_ERROR) return ResponseEntity(e.message, HttpStatus.INTERNAL_SERVER_ERROR)
@@ -109,5 +111,10 @@ class TransactionController(private val financialService: FinancialService) {
} }
} }
// @GetMapping("/regenTransactions")
// fun regenTransactions(): Mono<Void> {
// return financialService.regenTransactions()
// }
} }

View File

@@ -10,6 +10,7 @@ import kotlin.collections.mutableListOf
data class BudgetDTO( data class BudgetDTO(
var id: String? = null, var id: String? = null,
var space: Space? = null,
var name: String, var name: String,
var dateFrom: LocalDate, var dateFrom: LocalDate,
var dateTo: LocalDate, var dateTo: LocalDate,
@@ -26,6 +27,7 @@ data class BudgetDTO(
@Document("budgets") @Document("budgets")
data class Budget( data class Budget(
@Id var id: String? = null, @Id var id: String? = null,
@DBRef var space: Space? = null,
var name: String, var name: String,
var dateFrom: LocalDate, var dateFrom: LocalDate,
var dateTo: LocalDate, var dateTo: LocalDate,

View File

@@ -1,6 +1,7 @@
package space.luminic.budgerapp.models package space.luminic.budgerapp.models
import org.springframework.data.annotation.Id import org.springframework.data.annotation.Id
import org.springframework.data.mongodb.core.mapping.DBRef
import org.springframework.data.mongodb.core.mapping.Document import org.springframework.data.mongodb.core.mapping.Document
@@ -8,6 +9,7 @@ import org.springframework.data.mongodb.core.mapping.Document
data class Category( data class Category(
@Id @Id
val id: String? = null, val id: String? = null,
@DBRef var space: Space? = null,
var type: CategoryType, var type: CategoryType,
val name: String, val name: String,
val description: String? = null, val description: String? = null,

View File

@@ -8,6 +8,7 @@ import java.util.Date
@Document(collection = "recurrents") @Document(collection = "recurrents")
data class Recurrent( data class Recurrent(
@Id val id: String? = null, @Id val id: String? = null,
@DBRef var space: Space? = null,
var atDay: Int, var atDay: Int,
@DBRef var category: Category, @DBRef var category: Category,
var name: String, var name: String,

View File

@@ -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<User> = mutableListOf(),
var invites: MutableList<SpaceInvite> = mutableListOf(),
val createdAt: LocalDate = LocalDate.now(),
)
data class SpaceInvite(
val code: String,
@DBRef val fromUser: User,
val activeTill: LocalDateTime,
)

View File

@@ -12,6 +12,7 @@ import java.util.Date
@Document(collection = "transactions") @Document(collection = "transactions")
data class Transaction( data class Transaction(
@Id var id: String? = null, @Id var id: String? = null,
@DBRef var space: Space? = null,
var type: TransactionType, var type: TransactionType,
@DBRef var user: User?=null, @DBRef var user: User?=null,
@DBRef var category: Category, @DBRef var category: Category,

View File

@@ -4,6 +4,8 @@ import com.fasterxml.jackson.annotation.JsonIgnore
import jakarta.validation.constraints.NotBlank import jakarta.validation.constraints.NotBlank
import org.springframework.data.annotation.Id import org.springframework.data.annotation.Id
import org.springframework.data.mongodb.core.mapping.Document import org.springframework.data.mongodb.core.mapping.Document
import java.time.LocalDate
import java.time.LocalDateTime
import java.util.Date import java.util.Date
@Document("users") @Document("users")
@@ -17,8 +19,8 @@ data class User (
@JsonIgnore // Скрывает пароль при сериализации @JsonIgnore // Скрывает пароль при сериализации
var password: String? = null, var password: String? = null,
var isActive: Boolean = true, var isActive: Boolean = true,
var regDate: Date? = null, var regDate: LocalDate = LocalDate.now(),
val createdAt: Date? = null, val createdAt: LocalDateTime = LocalDateTime.now(),
var roles: MutableList<String> = mutableListOf(), var roles: MutableList<String> = mutableListOf(),
) )

View File

@@ -1,7 +1,10 @@
package space.luminic.budgerapp.repos package space.luminic.budgerapp.repos
import org.bson.types.ObjectId
import org.springframework.data.domain.Sort import org.springframework.data.domain.Sort
import org.springframework.data.mongodb.repository.Query
import org.springframework.data.mongodb.repository.ReactiveMongoRepository import org.springframework.data.mongodb.repository.ReactiveMongoRepository
import org.springframework.stereotype.Repository import org.springframework.stereotype.Repository
import reactor.core.publisher.Flux import reactor.core.publisher.Flux
@@ -16,4 +19,10 @@ interface BudgetRepo: ReactiveMongoRepository<Budget, String> {
fun findByDateFromLessThanEqualAndDateToGreaterThanEqual(dateOne: LocalDate, dateTwo: LocalDate): Mono<Budget> fun findByDateFromLessThanEqualAndDateToGreaterThanEqual(dateOne: LocalDate, dateTwo: LocalDate): Mono<Budget>
@Query("{'dateFrom': {'\$lte': ?0}, 'dateTo': {'\$gte': ?1}, 'space': ?2}")
fun findByDateFromLessThanEqualAndDateToGreaterThanEqualAndSpace(dateOne: LocalDate, dateTwo: LocalDate, spaceId: ObjectId): Mono<Budget>
@Query("{ 'space': { '\$ref': 'spaces','\$id': ?0 } }")
fun findBySpaceId(spaceId: ObjectId, sort: Sort): Flux<Budget>
} }

View File

@@ -1,9 +1,17 @@
package space.luminic.budgerapp.repos 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.data.mongodb.repository.ReactiveMongoRepository
import org.springframework.stereotype.Repository 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.Recurrent
import space.luminic.budgerapp.models.Space
@Repository @Repository
interface RecurrentRepo: ReactiveMongoRepository<Recurrent, String> { interface RecurrentRepo: ReactiveMongoRepository<Recurrent, String> {
@Query("{ 'space': { '\$ref': 'spaces', '\$id': ?0 } }")
fun findRecurrentsBySpaceId(spaceID: ObjectId): Flux<Recurrent>
} }

View File

@@ -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<Space, String> {
@Query("{ 'users': { '\$ref': 'users', '\$id': ?0 } }")
fun findByArrayElement(userId: ObjectId): Flux<Space>
@Query("{ 'invites.code': ?0 }") // Исправленный путь, чтобы находить вложенные документы
fun findSpaceByInvites(code: String): Mono<Space>
}

View File

@@ -43,6 +43,26 @@ class AuthService(
} }
} }
fun register(username: String, password: String, firstName: String): Mono<User> {
return userRepository.findByUsername(username)
.flatMap<User> { Mono.error(IllegalArgumentException("User with username '$username' already exists")) } // Ошибка, если пользователь уже существует
.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") @Cacheable("tokens")
fun isTokenValid(token: String): Mono<User> { fun isTokenValid(token: String): Mono<User> {
return tokenService.getToken(token) return tokenService.getToken(token)

View File

@@ -21,11 +21,7 @@ import org.springframework.data.mongodb.core.query.isEqualTo
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import reactor.core.publisher.Flux import reactor.core.publisher.Flux
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
import space.luminic.budgerapp.models.BudgetCategory import space.luminic.budgerapp.models.*
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.repos.CategoryRepo import space.luminic.budgerapp.repos.CategoryRepo
import java.time.LocalDate import java.time.LocalDate
import java.time.LocalDateTime import java.time.LocalDateTime
@@ -51,10 +47,11 @@ class CategoryService(
} }
// @Cacheable("categories") // @Cacheable("categories")
fun getCategories(type: String? = null, sortBy: String, direction: String): Mono<List<Category>> { fun getCategories(spaceId: String, type: String? = null, sortBy: String, direction: String): Mono<List<Category>> {
val matchCriteria = mutableListOf<Criteria>() val matchCriteria = mutableListOf<Criteria>()
// Добавляем фильтры // Добавляем фильтры
matchCriteria.add(Criteria.where("spaceDetails._id").`is`(ObjectId(spaceId)))
type?.let { matchCriteria.add(Criteria.where("type.code").isEqualTo(it)) } type?.let { matchCriteria.add(Criteria.where("type.code").isEqualTo(it)) }
val match = match(Criteria().andOperator(*matchCriteria.toTypedArray())) val match = match(Criteria().andOperator(*matchCriteria.toTypedArray()))
@@ -62,16 +59,17 @@ class CategoryService(
val sort = sort(Sort.by(direction, sortBy)) val sort = sort(Sort.by(direction, sortBy))
val lookupSpaces = lookup("spaces", "space.\$id", "_id", "spaceDetails")
val aggregationBuilder = mutableListOf( val aggregationBuilder = mutableListOf(
lookupSpaces,
match.takeIf { matchCriteria.isNotEmpty() }, match.takeIf { matchCriteria.isNotEmpty() },
sort, sort,
).filterNotNull() ).filterNotNull()
val aggregation = newAggregation(aggregationBuilder) val aggregation = newAggregation(aggregationBuilder)
logger.error("STARTED")
return mongoTemplate.aggregate( return mongoTemplate.aggregate(
aggregation, "categories", Category::class.java aggregation, "categories", Category::class.java
) )
@@ -97,7 +95,8 @@ class CategoryService(
} }
@CacheEvict(cacheNames = ["getAllCategories"], allEntries = true) @CacheEvict(cacheNames = ["getAllCategories"], allEntries = true)
fun createCategory(category: Category): Mono<Category> { fun createCategory(space: Space, category: Category): Mono<Category> {
category.space = space
return categoryRepo.save(category) return categoryRepo.save(category)
} }
@@ -113,32 +112,32 @@ class CategoryService(
} }
@CacheEvict(cacheNames = ["getAllCategories"], allEntries = true) @CacheEvict(cacheNames = ["getAllCategories"], allEntries = true)
fun deleteCategory(categoryId: String): Mono<String> { // fun deleteCategory(categoryId: String): Mono<String> {
return categoryRepo.findById(categoryId).switchIfEmpty( // return categoryRepo.findById(categoryId).switchIfEmpty(
Mono.error(IllegalArgumentException("Category with id: $categoryId not found")) // Mono.error(IllegalArgumentException("Category with id: $categoryId not found"))
).flatMap { // ).flatMap {
financialService.getTransactions(categoryId = categoryId) // financialService.getTransactions(categoryId = categoryId)
.flatMapMany { transactions -> // .flatMapMany { transactions ->
categoryRepo.findByName("Другое").switchIfEmpty( // categoryRepo.findByName("Другое").switchIfEmpty(
categoryRepo.save( // categoryRepo.save(
Category( // Category(
type = CategoryType("EXPENSE", "Траты"), // type = CategoryType("EXPENSE", "Траты"),
name = "Другое", // name = "Другое",
description = "Категория для других трат", // description = "Категория для других трат",
icon = "🚮" // icon = "🚮"
) // )
) // )
).flatMapMany { category -> // ).flatMapMany { category ->
Flux.fromIterable(transactions).flatMap { transaction -> // Flux.fromIterable(transactions).flatMap { transaction ->
transaction.category = category // Присваиваем конкретный объект категории // transaction.category = category // Присваиваем конкретный объект категории
financialService.editTransaction(transaction) // Сохраняем изменения // financialService.editTransaction(transaction) // Сохраняем изменения
} // }
} // }
} // }
.then(categoryRepo.deleteById(categoryId)) // Удаляем старую категорию // .then(categoryRepo.deleteById(categoryId)) // Удаляем старую категорию
.thenReturn(categoryId) // Возвращаем удалённую категорию // .thenReturn(categoryId) // Возвращаем удалённую категорию
} // }
} // }
fun getBudgetCategories(dateFrom: LocalDate, dateTo: LocalDate): Mono<Map<String, Map<String, Double>>> { fun getBudgetCategories(dateFrom: LocalDate, dateTo: LocalDate): Mono<Map<String, Map<String, Double>>> {

View File

@@ -8,6 +8,7 @@ import org.springframework.cache.annotation.CacheEvict
import org.springframework.cache.annotation.Cacheable import org.springframework.cache.annotation.Cacheable
import org.springframework.data.domain.Sort import org.springframework.data.domain.Sort
import org.springframework.data.domain.Sort.Direction 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.ReactiveMongoTemplate
import org.springframework.data.mongodb.core.aggregation.Aggregation.* import org.springframework.data.mongodb.core.aggregation.Aggregation.*
import org.springframework.data.mongodb.core.aggregation.DateOperators.DateToString import org.springframework.data.mongodb.core.aggregation.DateOperators.DateToString
@@ -19,6 +20,7 @@ import reactor.core.publisher.Flux
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
import space.luminic.budgerapp.models.* import space.luminic.budgerapp.models.*
import space.luminic.budgerapp.repos.BudgetRepo import space.luminic.budgerapp.repos.BudgetRepo
import space.luminic.budgerapp.repos.CategoryRepo
import space.luminic.budgerapp.repos.TransactionRepo import space.luminic.budgerapp.repos.TransactionRepo
import space.luminic.budgerapp.repos.WarnRepo import space.luminic.budgerapp.repos.WarnRepo
import java.time.* import java.time.*
@@ -32,7 +34,9 @@ class FinancialService(
val transactionsRepo: TransactionRepo, val transactionsRepo: TransactionRepo,
val recurrentService: RecurrentService, val recurrentService: RecurrentService,
val userService: UserService, 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) private val logger = LoggerFactory.getLogger(FinancialService::class.java)
@@ -191,24 +195,58 @@ class FinancialService(
}.then() // Возвращаем корректный Mono<Void> }.then() // Возвращаем корректный Mono<Void>
} }
@Cacheable("budgetsList") fun getBudgets(spaceId: String, sortSetting: SortSetting? = null): Mono<List<Budget>> {
fun getBudgets(sortSetting: SortSetting? = null): Mono<MutableList<Budget>> { val sort = sortSetting?.let {
val sort = if (sortSetting != null) { Sort.by(it.order, it.by)
Sort.by(sortSetting.order, sortSetting.by) } ?: Sort.by(Sort.Direction.DESC, "dateFrom")
return ReactiveSecurityContextHolder.getContext()
.map { it.authentication }
.flatMap { authentication ->
val username = authentication.name
spaceService.getSpace(spaceId)
.switchIfEmpty(Mono.error(IllegalArgumentException("Space not found for $spaceId")))
.flatMap { space ->
userService.getByUsername(username)
.switchIfEmpty(Mono.error(IllegalArgumentException("User not found for username: $username")))
.flatMap { user ->
val userIds = space.users.mapNotNull { it.id?.toString() }
if (user.id !in userIds) {
Mono.error(IllegalArgumentException("User cannot access this Space"))
} else { } else {
Sort.by(Sort.Direction.DESC, "dateFrom") val spaceObjectId = try {
ObjectId(space.id!!) // Преобразуем строку в ObjectId
} catch (e: IllegalArgumentException) {
return@flatMap Mono.error(IllegalArgumentException("Invalid Space ID format: ${space.id}"))
} }
return budgetRepo.findAll(sort) println("Space ID type: ${spaceObjectId::class.java}, value: $spaceObjectId")
.collectList() // Сбор Flux<Budget> в Mono<List<Budget>> // Применяем сортировку к запросу
budgetRepo.findBySpaceId(spaceObjectId, sort).collectList()
}
}
}
}
} }
// @Cacheable("budgets", key = "#id") // @Cacheable("budgets", key = "#id")
fun getBudget(id: String): Mono<BudgetDTO> { fun getBudget(id: String): Mono<BudgetDTO> {
return budgetRepo.findById(id) return ReactiveSecurityContextHolder.getContext()
.flatMap { securityContext ->
val username = securityContext.authentication.name
budgetRepo.findById(id)
.flatMap { budget -> .flatMap { budget ->
// Проверяем, что пользователь есть в space бюджета
if (!budget.space!!.users.any { it.username == username }) {
return@flatMap Mono.error(IllegalArgumentException("User does not have access to this space"))
}
// Если доступ есть, продолжаем процесс
val budgetDTO = BudgetDTO( val budgetDTO = BudgetDTO(
budget.id, budget.id,
budget.space,
budget.name, budget.name,
budget.dateFrom, budget.dateFrom,
budget.dateTo, budget.dateTo,
@@ -219,16 +257,13 @@ class FinancialService(
logger.info("Fetching categories and transactions") logger.info("Fetching categories and transactions")
val categoriesMono = getBudgetCategories(budgetDTO.dateFrom, budgetDTO.dateTo) val categoriesMono = getBudgetCategories(budgetDTO.dateFrom, budgetDTO.dateTo)
val transactionsMono = val transactionsMono = getTransactionsByTypes(budgetDTO.dateFrom, budgetDTO.dateTo)
getTransactionsByTypes(budgetDTO.dateFrom, budgetDTO.dateTo)
Mono.zip(categoriesMono, transactionsMono) Mono.zip(categoriesMono, transactionsMono)
.flatMap { tuple -> .flatMap { tuple ->
val categories = tuple.t1 val categories = tuple.t1
val transactions = tuple.t2 val transactions = tuple.t2
Flux.fromIterable(budgetDTO.categories) Flux.fromIterable(budgetDTO.categories)
.map { category -> .map { category ->
categories[category.category.id]?.let { data -> categories[category.category.id]?.let { data ->
@@ -253,13 +288,15 @@ class FinancialService(
} }
.switchIfEmpty(Mono.error(BudgetNotFoundException("Budget not found with id: $id"))) .switchIfEmpty(Mono.error(BudgetNotFoundException("Budget not found with id: $id")))
} }
}
fun regenCats(): Mono<Void> {
fun regenBudgets(): Mono<Void> {
return budgetRepo.findAll() return budgetRepo.findAll()
.flatMap { budget -> .flatMap { budget ->
getCategoryTransactionPipeline(budget.dateFrom, budget.dateTo, "INCOME") spaceService.getSpace("67af3c0f652da946a7dd9931")
.map { categories -> .map { space ->
budget.incomeCategories = categories budget.space = space
budget budget
} }
.flatMap { updatedBudget -> budgetRepo.save(updatedBudget) } .flatMap { updatedBudget -> budgetRepo.save(updatedBudget) }
@@ -267,12 +304,38 @@ class FinancialService(
.then() .then()
} }
fun regenTransactions(): Mono<Void> {
return transactionsRepo.findAll().flatMap { transaction ->
spaceService.getSpace("67af3c0f652da946a7dd9931")
.map { space ->
transaction.space = space
transaction
}
.flatMap { updatedTransaction -> transactionsRepo.save(updatedTransaction) }
}
.then()
}
fun regenCats(): Mono<Void> {
return categoryRepo.findAll()// Получаем список категорий
.flatMap { cat ->
spaceService.getSpace("67af3c0f652da946a7dd9931") // Получаем space
.map { space ->
cat.space = space // Привязываем пространство к категории
cat
}
}
.flatMap { updatedCategory -> categoryRepo.save(updatedCategory) } // Сохраняем в БД
.then() // Завершаем Mono<Void>
}
@CacheEvict(cacheNames = ["budgets", "budgetsList"], allEntries = true) @CacheEvict(cacheNames = ["budgets", "budgetsList"], allEntries = true)
fun createBudget(budget: Budget, createRecurrent: Boolean): Mono<Budget> { fun createBudget(spaceId: String, budget: Budget, createRecurrent: Boolean): Mono<Budget> {
return Mono.zip( return Mono.zip(
getBudgetByDate(budget.dateFrom).map { Optional.ofNullable(it) } getBudgetByDate(budget.dateFrom, spaceId).map { Optional.ofNullable(it) }
.switchIfEmpty(Mono.just(Optional.empty())), .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())) .switchIfEmpty(Mono.just(Optional.empty()))
).flatMap { tuple -> ).flatMap { tuple ->
val startBudget = tuple.t1.orElse(null) val startBudget = tuple.t1.orElse(null)
@@ -283,9 +346,27 @@ class FinancialService(
return@flatMap Mono.error<Budget>(IllegalArgumentException("Бюджет с теми же датами найден")) return@flatMap Mono.error<Budget>(IllegalArgumentException("Бюджет с теми же датами найден"))
} }
// Получаем Space по spaceId
return@flatMap spaceService.getSpace(spaceId)
.switchIfEmpty(Mono.error(IllegalArgumentException("Space not found for $spaceId")))
.flatMap { space ->
// Проверяем, входит ли пользователь в этот Space
ReactiveSecurityContextHolder.getContext().flatMap { securityContext ->
val username = securityContext.authentication.name
userService.getByUsername(username)
.switchIfEmpty(Mono.error(IllegalArgumentException("User not found for username: $username")))
.flatMap { user ->
if (space.users.none { it.id == user.id }) {
return@flatMap Mono.error<Budget>(IllegalArgumentException("User does not have access to this space"))
}
// Присваиваем Space бюджету
budget.space = space
// Если createRecurrent=true, создаем рекуррентные транзакции // Если createRecurrent=true, создаем рекуррентные транзакции
val recurrentsCreation = if (createRecurrent) { val recurrentsCreation = if (createRecurrent) {
recurrentService.createRecurrentsForBudget(budget) recurrentService.createRecurrentsForBudget(space, budget)
} else { } else {
Mono.empty() Mono.empty()
} }
@@ -311,8 +392,13 @@ class FinancialService(
} }
} }
fun getBudgetByDate(date: LocalDate): Mono<Budget> { }
return budgetRepo.findByDateFromLessThanEqualAndDateToGreaterThanEqual(date, date).switchIfEmpty(Mono.empty()) }
}
fun getBudgetByDate(date: LocalDate, spaceId: String): Mono<Budget> {
return budgetRepo.findByDateFromLessThanEqualAndDateToGreaterThanEqualAndSpace(date, date, ObjectId(spaceId))
.switchIfEmpty(Mono.empty())
} }
@@ -530,6 +616,7 @@ class FinancialService(
@Cacheable("transactions") @Cacheable("transactions")
fun getTransactions( fun getTransactions(
spaceId: String,
dateFrom: LocalDate? = null, dateFrom: LocalDate? = null,
dateTo: LocalDate? = null, dateTo: LocalDate? = null,
transactionType: String? = null, transactionType: String? = null,
@@ -543,25 +630,46 @@ class FinancialService(
limit: Int? = null, limit: Int? = null,
offset: Int? = null, offset: Int? = null,
): Mono<MutableList<Transaction>> { ): Mono<MutableList<Transaction>> {
return ReactiveSecurityContextHolder.getContext()
.map { it.authentication }
.flatMap { authentication ->
val username = authentication.name
spaceService.getSpace(spaceId)
.switchIfEmpty(Mono.error(IllegalArgumentException("Space not found for $spaceId")))
.flatMap { space ->
userService.getByUsername(username)
.switchIfEmpty(Mono.error(IllegalArgumentException("User not found for username: $username")))
.flatMap { user ->
if (space.users.none { it.id.toString() == user.id }) {
return@flatMap Mono.error<MutableList<Transaction>>(IllegalArgumentException("User does not have access to this Space"))
}
val matchCriteria = mutableListOf<Criteria>() val matchCriteria = mutableListOf<Criteria>()
// Добавляем фильтры // Добавляем фильтры
matchCriteria.add(Criteria.where("spaceDetails._id").`is`(ObjectId(spaceId)))
dateFrom?.let { matchCriteria.add(Criteria.where("date").gte(it)) } dateFrom?.let { matchCriteria.add(Criteria.where("date").gte(it)) }
dateTo?.let { matchCriteria.add(Criteria.where("date").lt(it)) } dateTo?.let { matchCriteria.add(Criteria.where("date").lt(it)) }
transactionType?.let { matchCriteria.add(Criteria.where("type.code").`is`(it)) } transactionType?.let { matchCriteria.add(Criteria.where("type.code").`is`(it)) }
isDone?.let { matchCriteria.add(Criteria.where("isDone").`is`(it)) } isDone?.let { matchCriteria.add(Criteria.where("isDone").`is`(it)) }
categoryId?.let { matchCriteria.add(Criteria.where("categoryDetails._id").`is`(it)) } categoryId?.let { matchCriteria.add(Criteria.where("categoryDetails._id").`is`(it)) }
categoryType?.let { matchCriteria.add(Criteria.where("categoryDetails.type.code").`is`(it)) } categoryType?.let {
matchCriteria.add(
Criteria.where("categoryDetails.type.code").`is`(it)
)
}
userId?.let { matchCriteria.add(Criteria.where("userDetails._id").`is`(ObjectId(it))) } userId?.let { matchCriteria.add(Criteria.where("userDetails._id").`is`(ObjectId(it))) }
parentId?.let { matchCriteria.add(Criteria.where("parentId").`is`(it)) } parentId?.let { matchCriteria.add(Criteria.where("parentId").`is`(it)) }
isChild?.let { matchCriteria.add(Criteria.where("parentId").exists(it)) } isChild?.let { matchCriteria.add(Criteria.where("parentId").exists(it)) }
// Сборка агрегации // Сборка агрегации
val lookup = lookup("categories", "category.\$id", "_id", "categoryDetails") 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 lookupUsers = lookup("users", "user.\$id", "_id", "userDetails")
val match = match(Criteria().andOperator(*matchCriteria.toTypedArray())) val match = match(Criteria().andOperator(*matchCriteria.toTypedArray()))
var sort = sort(Sort.by(Direction.DESC, "date").and(Sort.by(Direction.DESC, "createdAt"))) var sort =
sort(Sort.by(Direction.DESC, "date").and(Sort.by(Direction.DESC, "createdAt")))
sortSetting?.let { sortSetting?.let {
sort = sort(Sort.by(it.order, it.by).and(Sort.by(Direction.ASC, "createdAt"))) sort = sort(Sort.by(it.order, it.by).and(Sort.by(Direction.ASC, "createdAt")))
@@ -569,6 +677,7 @@ class FinancialService(
val aggregationBuilder = mutableListOf( val aggregationBuilder = mutableListOf(
lookup, lookup,
lookupSpaces,
lookupUsers, lookupUsers,
match.takeIf { matchCriteria.isNotEmpty() }, match.takeIf { matchCriteria.isNotEmpty() },
sort, sort,
@@ -578,12 +687,15 @@ class FinancialService(
val aggregation = newAggregation(aggregationBuilder) val aggregation = newAggregation(aggregationBuilder)
return reactiveMongoTemplate.aggregate( return@flatMap reactiveMongoTemplate.aggregate(
aggregation, "transactions", Transaction::class.java aggregation, "transactions", Transaction::class.java
) )
.collectList() // Преобразуем Flux<Transaction> в Mono<List<Transaction>> .collectList()
.map { it.toMutableList() } .map { it.toMutableList() }
} }
}
}
}
fun getTransactionsToDelete(dateFrom: LocalDate, dateTo: LocalDate): Mono<List<Transaction>> { fun getTransactionsToDelete(dateFrom: LocalDate, dateTo: LocalDate): Mono<List<Transaction>> {
val criteria = Criteria().andOperator( val criteria = Criteria().andOperator(
@@ -618,15 +730,24 @@ class FinancialService(
@CacheEvict(cacheNames = ["transactions"], allEntries = true) @CacheEvict(cacheNames = ["transactions"], allEntries = true)
fun createTransaction(transaction: Transaction): Mono<Transaction> { fun createTransaction(spaceId: String, transaction: Transaction): Mono<Transaction> {
return ReactiveSecurityContextHolder.getContext() return ReactiveSecurityContextHolder.getContext()
.map { it.authentication } .map { it.authentication }
.flatMap { authentication -> .flatMap { authentication ->
val username = authentication.name val username = authentication.name
spaceService.getSpace(spaceId)
.switchIfEmpty(Mono.error(IllegalArgumentException("Space not found for $spaceId")))
.flatMap { space ->
userService.getByUsername(username) userService.getByUsername(username)
.switchIfEmpty(Mono.error(IllegalArgumentException("User not found for username: $username"))) .switchIfEmpty(Mono.error(IllegalArgumentException("User not found for username: $username")))
.flatMap { user -> .flatMap { user ->
if (space.users.none { it.id.toString() == user.id }) {
return@flatMap Mono.error<Transaction>(IllegalArgumentException("User does not have access to this Space"))
}
// Привязываем space и user к транзакции
transaction.user = user transaction.user = user
transaction.space = space
transactionsRepo.save(transaction) transactionsRepo.save(transaction)
.flatMap { savedTransaction -> .flatMap { savedTransaction ->
updateBudgetOnCreate(savedTransaction) updateBudgetOnCreate(savedTransaction)
@@ -635,6 +756,7 @@ class FinancialService(
} }
} }
} }
}
@CacheEvict(cacheNames = ["transactions", "budgets"], allEntries = true) @CacheEvict(cacheNames = ["transactions", "budgets"], allEntries = true)
@@ -1034,8 +1156,8 @@ class FinancialService(
tgUserName = userDocument["tgUserName"]?.let { it as String }, tgUserName = userDocument["tgUserName"]?.let { it as String },
password = null, password = null,
isActive = userDocument["isActive"] as Boolean, isActive = userDocument["isActive"] as Boolean,
regDate = userDocument["regDate"] as Date, regDate = userDocument["regDate"] as LocalDate,
createdAt = userDocument["createdAt"] as Date, createdAt = userDocument["createdAt"] as LocalDateTime,
roles = userDocument["roles"] as ArrayList<String>, roles = userDocument["roles"] as ArrayList<String>,
) )
@@ -1051,6 +1173,7 @@ class FinancialService(
) )
return Transaction( return Transaction(
(document["_id"] as ObjectId).toString(), (document["_id"] as ObjectId).toString(),
null,
TransactionType( TransactionType(
transactionType["code"] as String, transactionType["code"] as String,
transactionType["name"] as String transactionType["name"] as String
@@ -1582,7 +1705,7 @@ class FinancialService(
.collectList() .collectList()
} }
fun getCategorySummaries(dateFrom: LocalDate): Mono<List<Document>> { fun getCategorySummaries(spaceId: String, dateFrom: LocalDate): Mono<List<Document>> {
val sixMonthsAgo = Date.from( val sixMonthsAgo = Date.from(
LocalDateTime.of(dateFrom, LocalTime.MIN) LocalDateTime.of(dateFrom, LocalTime.MIN)
.atZone(ZoneId.systemDefault()) .atZone(ZoneId.systemDefault())
@@ -1591,6 +1714,16 @@ class FinancialService(
val aggregation = listOf( val aggregation = listOf(
// 1. Фильтр за последние 6 месяцев // 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( Document(
"\$match", "\$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")
@@ -1720,6 +1853,7 @@ class FinancialService(
Document("\$sort", Document("categoryName", 1)) Document("\$sort", Document("categoryName", 1))
) )
// Выполняем агрегацию // Выполняем агрегацию
return reactiveMongoTemplate.getCollection("transactions") return reactiveMongoTemplate.getCollection("transactions")
.flatMapMany { it.aggregate(aggregation) } .flatMapMany { it.aggregate(aggregation) }

View File

@@ -1,17 +1,14 @@
package space.luminic.budgerapp.services package space.luminic.budgerapp.services
import org.bson.types.ObjectId
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.cache.annotation.CacheEvict import org.springframework.cache.annotation.CacheEvict
import org.springframework.cache.annotation.Cacheable import org.springframework.cache.annotation.Cacheable
import org.springframework.security.core.context.ReactiveSecurityContextHolder import org.springframework.security.core.context.ReactiveSecurityContextHolder
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
import space.luminic.budgerapp.models.Budget import space.luminic.budgerapp.models.*
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.repos.RecurrentRepo import space.luminic.budgerapp.repos.RecurrentRepo
import space.luminic.budgerapp.repos.TransactionRepo import space.luminic.budgerapp.repos.TransactionRepo
import java.time.YearMonth import java.time.YearMonth
@@ -22,31 +19,40 @@ class RecurrentService(
private val recurrentRepo: RecurrentRepo, private val recurrentRepo: RecurrentRepo,
private val transactionRepo: TransactionRepo, private val transactionRepo: TransactionRepo,
private val userService: UserService, private val userService: UserService,
private val spaceService: SpaceService,
) { ) {
private val logger = LoggerFactory.getLogger(javaClass) private val logger = LoggerFactory.getLogger(javaClass)
@Cacheable("recurrentsList") @Cacheable("recurrentsList")
fun getRecurrents(): Mono<List<Recurrent>> { fun getRecurrents(space: Space): Mono<List<Recurrent>> {
return recurrentRepo.findAll().collectList()
// Запрос рекуррентных платежей
return recurrentRepo.findRecurrentsBySpaceId(ObjectId(space.id))
.collectList() // Преобразуем Flux<Recurrent> в Mono<List<Recurrent>>
} }
@Cacheable("recurrents", key = "#id") @Cacheable("recurrents", key = "#id")
fun getRecurrentById(id: String): Mono<Recurrent> { fun getRecurrentById(space: Space, id: String): Mono<Recurrent> {
// Запрос рекуррентных платежей
return recurrentRepo.findById(id) return recurrentRepo.findById(id)
.switchIfEmpty(Mono.error(NotFoundException("Recurrent with id: $id not found"))) .switchIfEmpty(Mono.error(NotFoundException("Recurrent with id: $id not found")))
} }
@CacheEvict(cacheNames = ["recurrentsList", "recurrents"]) @CacheEvict(cacheNames = ["recurrentsList", "recurrents"])
fun createRecurrent(recurrent: Recurrent): Mono<Recurrent> { fun createRecurrent(space: Space, recurrent: Recurrent): Mono<Recurrent> {
return if (recurrent.id == null && recurrent.atDay <= 31) recurrentRepo.save(recurrent) else Mono.error( 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") RuntimeException("Cannot create recurrent with id or date cannot be higher than 31")
) )
} }
@CacheEvict(cacheNames = ["recurrentsList", "recurrents"]) @CacheEvict(cacheNames = ["recurrentsList", "recurrents"])
fun createRecurrentsForBudget(budget: Budget): Mono<Void> { fun createRecurrentsForBudget(space: Space, budget: Budget): Mono<Void> {
val currentYearMonth = YearMonth.of(budget.dateFrom.year, budget.dateFrom.monthValue) val currentYearMonth = YearMonth.of(budget.dateFrom.year, budget.dateFrom.monthValue)
val daysInCurrentMonth = currentYearMonth.lengthOfMonth() val daysInCurrentMonth = currentYearMonth.lengthOfMonth()
val context = ReactiveSecurityContextHolder.getContext() val context = ReactiveSecurityContextHolder.getContext()
@@ -63,7 +69,7 @@ class RecurrentService(
.switchIfEmpty(Mono.error(IllegalArgumentException("User not found for username: $username"))) .switchIfEmpty(Mono.error(IllegalArgumentException("User not found for username: $username")))
} }
.flatMapMany { user -> .flatMapMany { user ->
recurrentRepo.findAll() recurrentRepo.findRecurrentsBySpaceId(ObjectId(space.id))
.map { recurrent -> .map { recurrent ->
// Определяем дату транзакции // Определяем дату транзакции
val transactionDate = when { val transactionDate = when {
@@ -111,7 +117,17 @@ class RecurrentService(
return recurrentRepo.deleteById(id) return recurrentRepo.deleteById(id)
} }
fun regenRecurrents(): Mono<List<Recurrent>> {
return recurrentRepo.findAll()
.flatMap { recurrent ->
spaceService.getSpace("67af3c0f652da946a7dd9931")
.flatMap { space ->
recurrent.space = space
recurrentRepo.save(recurrent) // Сохраняем и возвращаем сохраненный объект
}
}
.collectList() // Собираем результаты в список
}
} }

View File

@@ -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<Space> {
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<Space>(IllegalArgumentException("User does not have access to this Space"))
}
// Если проверка прошла успешно, возвращаем пространство
Mono.just(space)
}
}
}
}
fun getSpaces(): Mono<List<Space>> {
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<List<Space>>
}
}
fun getSpace(spaceId: String): Mono<Space> {
return spaceRepo.findById(spaceId)
.switchIfEmpty(Mono.error(IllegalArgumentException("SpaceId not found for spaceId: $spaceId")))
}
fun createSpace(space: Space): Mono<Space> {
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<Void> {
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<SpaceInvite> {
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<SpaceInvite>(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<Space> {
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<Space>(IllegalArgumentException("Invite is invalid or expired"))
}
// Проверяем, не является ли пользователь уже участником
if (space.users.any { it.id == user.id }) {
return@flatMap Mono.error<Space>(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<Void> {
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<Void>(IllegalArgumentException("User does not have access to this Space"))
}
// Удаляем пользователя из массива
space.users.removeIf { it.id == user.id }
// Сохраняем изменения
spaceRepo.save(space).then() // .then() для Mono<Void>
}
}
}
}
fun kickMember(spaceId: String, kickedUsername: String): Mono<Void> {
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<Void>(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<Void>(IllegalArgumentException("User not found in this space"))
}
}
}
}
}
}
// fun regenSpaces(): Mono<List<Space>> {
// return spaceRepo.findAll()
// .flatMap { space ->
// userService.getUsers()
// .flatMap { users ->
// if (users.isEmpty()) {
// return@flatMap Mono.error<Space>(IllegalStateException("No users found"))
// }
// val updatedSpace = space.copy(owner = users.first()) // Создаем копию (если `Space` data class)
// spaceRepo.save(updatedSpace)
// }
// }
// .collectList()
// }
}

View File

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