Files
luminic-back/src/main/kotlin/space/luminic/budgerapp/services/SpaceService.kt
2025-04-07 18:26:23 +03:00

355 lines
15 KiB
Kotlin

package space.luminic.budgerapp.services
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.reactive.awaitFirst
import kotlinx.coroutines.reactive.awaitFirstOrNull
import kotlinx.coroutines.reactive.awaitLast
import kotlinx.coroutines.reactive.awaitSingle
import kotlinx.coroutines.reactor.awaitSingleOrNull
import org.bson.Document
import org.bson.types.ObjectId
import org.springframework.data.mongodb.core.ReactiveMongoTemplate
import org.springframework.data.mongodb.core.aggregation.Aggregation.*
import org.springframework.data.mongodb.core.query.Criteria
import org.springframework.data.mongodb.core.query.Query
import org.springframework.security.core.context.ReactiveSecurityContextHolder
import org.springframework.stereotype.Service
import space.luminic.budgerapp.configs.AuthException
import space.luminic.budgerapp.models.*
import space.luminic.budgerapp.repos.*
import java.time.LocalDateTime
import java.time.ZoneId
import java.util.UUID
@Service
class SpaceService(
private val spaceRepo: SpaceRepo,
private val userService: UserService,
private val budgetRepo: BudgetRepo,
private val reactiveMongoTemplate: ReactiveMongoTemplate,
private val categoryRepo: CategoryRepo,
private val recurrentRepo: RecurrentRepo,
private val transactionRepo: TransactionRepo,
private val financialService: FinancialService,
private val categoryService: CategoryService,
private val recurrentService: RecurrentService,
private val tagRepo: TagRepo
) {
suspend fun isValidRequest(spaceId: String, user: User): Space {
val space = getSpace(spaceId)
// Проверяем доступ пользователя к пространству
return if (space.users.none { it.id.toString() == user.id }) {
throw IllegalArgumentException("User does not have access to this Space")
} else space
}
suspend fun getSpaces(): List<Space> {
val securityContext = ReactiveSecurityContextHolder.getContext().awaitSingle()
val authentication = securityContext.authentication
val username = authentication.name
val user = userService.getByUsername(username)
val userId = ObjectId(user.id!!)
// Агрегация для загрузки владельца и пользователей
val lookupOwner = lookup("users", "owner.\$id", "_id", "ownerDetails")
val unwindOwner = unwind("ownerDetails")
val lookupUsers = lookup("users", "users.\$id", "_id", "usersDetails")
// val unwindUsers = unwind("usersDetails")
val matchStage = match(Criteria.where("usersDetails._id").`is`(userId))
val aggregation = newAggregation(lookupOwner, unwindOwner, lookupUsers, matchStage)
return reactiveMongoTemplate.aggregate(aggregation, "spaces", Document::class.java)
.collectList()
.map { docs ->
docs.map { doc ->
val ownerDoc = doc.get("ownerDetails", Document::class.java)
val usersDocList = doc.getList("usersDetails", Document::class.java)
Space(
id = doc.getObjectId("_id").toString(),
name = doc.getString("name"),
description = doc.getString("description"),
owner = User(
id = ownerDoc.getObjectId("_id").toString(),
username = ownerDoc.getString("username"),
firstName = ownerDoc.getString("firstName")
),
users = usersDocList.map { userDoc ->
User(
id = userDoc.getObjectId("_id").toString(),
username = userDoc.getString("username"),
firstName = userDoc.getString("firstName")
)
}.toMutableList(),
createdAt = doc.getDate("createdAt").toInstant().atZone(ZoneId.systemDefault()).toLocalDate()
)
}
}.awaitFirst()
}
suspend fun getSpace(spaceId: String): Space {
return spaceRepo.findById(spaceId).awaitSingleOrNull()
?: throw IllegalArgumentException("SpaceId not found for spaceId: $spaceId")
}
suspend fun createSpace(space: Space, createCategories: Boolean): Space {
val securityContextHolder = ReactiveSecurityContextHolder.getContext().awaitSingleOrNull()
?: throw AuthException("Authentication failed")
val authentication = securityContextHolder.authentication
val username = authentication.name
val user = userService.getByUsername(username)
space.owner = user
space.users.add(user)
val savedSpace = spaceRepo.save(space).awaitSingle()
return if (!createCategories) {
savedSpace // Если не нужно создавать категории, просто возвращаем пространство
} else {
val categories = reactiveMongoTemplate.find(Query(), Category::class.java, "categories-etalon")
.map { category ->
category.copy(id = null, space = savedSpace) // Создаем новую копию
}
categoryRepo.saveAll(categories).awaitLast()
savedSpace
}
}
suspend fun deleteSpace(space: Space) {
val objectId = ObjectId(space.id)
coroutineScope {
launch {
val budgets = financialService.findProjectedBudgets(objectId).awaitFirstOrNull().orEmpty()
budgetRepo.deleteAll(budgets).awaitFirstOrNull()
}
launch {
val transactions = financialService.getTransactions(objectId.toString()).awaitFirstOrNull().orEmpty()
transactionRepo.deleteAll(transactions).awaitFirstOrNull()
}
launch {
val categories =
categoryService.getCategories(objectId.toString(), null, "name", "ASC")
categoryRepo.deleteAll(categories).awaitFirstOrNull()
}
launch {
val recurrents = recurrentService.getRecurrents(objectId.toString())
recurrentRepo.deleteAll(recurrents).awaitFirstOrNull()
}
}
spaceRepo.deleteById(space.id!!).awaitFirstOrNull() // Удаляем Space после всех операций
}
suspend fun createInviteSpace(spaceId: String): SpaceInvite {
val securityContext = ReactiveSecurityContextHolder.getContext().awaitSingleOrNull()
?: throw AuthException("Authentication failed")
val authentication = securityContext.authentication
val user = userService.getByUsername(authentication.name)
val space = getSpace(spaceId)
if (space.owner?.id != user.id) {
throw AuthException("Only owner could create invite into space")
}
val invite = SpaceInvite(
UUID.randomUUID().toString().split("-")[0],
user,
LocalDateTime.now().plusHours(1),
)
space.invites.add(invite)
spaceRepo.save(space).awaitFirstOrNull()
// Сохраняем изменения и возвращаем созданное приглашение
return invite
}
suspend fun acceptInvite(code: String): Space {
val securityContextHolder = ReactiveSecurityContextHolder.getContext().awaitSingleOrNull()
?: throw AuthException("Authentication failed")
val user = userService.getByUsername(securityContextHolder.authentication.name)
val space = spaceRepo.findSpaceByInvites(code).awaitFirstOrNull()
?: throw IllegalArgumentException("Space with invite code: $code not found")
val invite = space.invites.find { it.code == code }
// Проверяем, есть ли инвайт и не истек ли он
if (invite == null || invite.activeTill.isBefore(LocalDateTime.now())) {
throw IllegalArgumentException("Invite is invalid or expired")
}
// Проверяем, не является ли пользователь уже участником
if (space.users.any { it.id == user.id }) {
throw IllegalArgumentException("User is already a member of this Space")
}
// Добавляем пользователя и удаляем использованный инвайт
space.users.add(user)
space.invites.remove(invite)
return spaceRepo.save(space).awaitFirst()
}
suspend fun leaveSpace(spaceId: String) {
val securityContext = ReactiveSecurityContextHolder.getContext().awaitSingleOrNull()
?: throw AuthException("Authentication failed")
val user = userService.getByUsername(securityContext.authentication.name)
val space = getSpace(spaceId)
// Удаляем пользователя из массива
space.users.removeIf { it.id == user.id }
// Сохраняем изменения
spaceRepo.save(space).awaitFirst()
}
suspend fun kickMember(spaceId: String, kickedUsername: String) {
val securityContext = ReactiveSecurityContextHolder.getContext().awaitSingleOrNull()
?: throw AuthException("Authentication failed")
val currentUser = userService.getByUsername(securityContext.authentication.name)
//проверяем что кикнутый пользователь сушествует
userService.getByUsername(kickedUsername)
val space = getSpace(spaceId)
if (space.owner?.id != currentUser.id) {
throw IllegalArgumentException("Only owners allowed for this action")
}
// Проверяем, что пользователь, которого нужно исключить, присутствует в списке пользователей
val userToKick = space.users.find { it.username == kickedUsername }
if (userToKick != null) {
// Удаляем пользователя из пространства
space.users.removeIf { it.username == kickedUsername }
// Сохраняем изменения
spaceRepo.save(space).awaitSingle()
} else {
throw IllegalArgumentException("User not found in this space")
}
}
suspend fun findTag(space: Space, tagCode: String): Tag? {
val lookupSpaces = lookup("spaces", "space.\$id", "_id", "spaceDetails")
val unwindSpace = unwind("spaceDetails")
val matchCriteria = mutableListOf<Criteria>()
// Добавляем фильтры
matchCriteria.add(Criteria.where("spaceDetails._id").`is`(ObjectId(space.id)))
matchCriteria.add(Criteria.where("code").`is`(tagCode))
val match = match(Criteria().andOperator(*matchCriteria.toTypedArray()))
val aggregationBuilder = mutableListOf(
lookupSpaces,
unwindSpace,
match.takeIf { matchCriteria.isNotEmpty() },
).filterNotNull()
val aggregation = newAggregation(aggregationBuilder)
return reactiveMongoTemplate.aggregate(
aggregation, "tags", Document::class.java
).next()
.map { doc ->
Tag(
id = doc.getObjectId("_id").toString(),
space = Space(id = doc.get("spaceDetails", Document::class.java).getObjectId("_id").toString()),
code = doc.getString("code"),
name = doc.getString("name")
)
}.awaitSingleOrNull()
}
suspend fun createTag(space: Space, tag: Tag): Tag {
tag.space = space
val existedTag = findTag(space, tag.code)
return existedTag?.let {
throw IllegalArgumentException("Tag with code ${tag.code} already exists")
} ?: tagRepo.save(tag).awaitFirst()
}
suspend fun deleteTag(space: Space, tagCode: String) {
val existedTag = findTag(space, tagCode) ?: throw NoSuchElementException("Tag with code $tagCode not found")
val categoriesWithTag =
categoryService.getCategories(space.id!!, sortBy = "name", direction = "ASC", tagCode = existedTag.code)
categoriesWithTag.map { cat ->
cat.tags.removeIf { it.code == tagCode } // Изменяем список тегов
cat
}
categoryRepo.saveAll(categoriesWithTag).awaitFirst() // Сохраняем обновлённые категории
tagRepo.deleteById(existedTag.id!!).awaitFirst()
}
suspend fun getTags(space: Space): List<Tag> {
val lookupSpaces = lookup("spaces", "space.\$id", "_id", "spaceDetails")
val unwindSpace = unwind("spaceDetails")
val matchCriteria = mutableListOf<Criteria>()
// Добавляем фильтры
matchCriteria.add(Criteria.where("spaceDetails._id").`is`(ObjectId(space.id)))
val match = match(Criteria().andOperator(*matchCriteria.toTypedArray()))
val aggregationBuilder = mutableListOf(
lookupSpaces,
unwindSpace,
match.takeIf { matchCriteria.isNotEmpty() },
).filterNotNull()
val aggregation = newAggregation(aggregationBuilder)
return reactiveMongoTemplate.aggregate(
aggregation, "tags", Document::class.java
)
.collectList()
.map { docs ->
docs.map { doc ->
Tag(
id = doc.getObjectId("_id").toString(),
space = Space(
id = doc.get("spaceDetails", Document::class.java).getObjectId("_id").toString()
),
code = doc.getString("code"),
name = doc.getString("name")
)
}
}
.awaitSingleOrNull().orEmpty()
}
// fun regenSpaceCategory(): Mono<Category> {
// return getSpace("67af3c0f652da946a7dd9931")
// .flatMap { space ->
// categoryService.findCategory(id = "677bc767c7857460a491bd4f")
// .flatMap { category -> // заменил map на flatMap
// category.space = space
// category.name = "Сбережения"
// category.description = "Отчисления в накопления или инвестиционные счета"
// category.icon = "💰"
// categoryRepo.save(category) // теперь возвращаем Mono<Category>
// }
// }
// }
// fun regenSpaces(): Mono<List<Space>> {
// return spaceRepo.findAll()
// .flatMap { space ->
// 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()
// }
}