init
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
package space.luminic.finance.services
|
||||
|
||||
import space.luminic.finance.dtos.AccountDTO
|
||||
import space.luminic.finance.models.Account
|
||||
import space.luminic.finance.models.Transaction
|
||||
|
||||
interface AccountService {
|
||||
suspend fun getAccounts(spaceId: String): List<Account>
|
||||
suspend fun getAccount(spaceId: String, accountId: String): Account
|
||||
suspend fun getAccountTransactions(spaceId: String, accountId: String): List<Transaction>
|
||||
suspend fun createAccount(spaceId: String, account: AccountDTO.CreateAccountDTO): Account
|
||||
suspend fun updateAccount(spaceId: String, account: AccountDTO.UpdateAccountDTO): Account
|
||||
suspend fun deleteAccount(spaceId: String, accountId: String)
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package space.luminic.finance.services
|
||||
|
||||
import kotlinx.coroutines.reactive.awaitSingle
|
||||
import org.bson.Document
|
||||
import org.bson.types.ObjectId
|
||||
import org.springframework.data.mongodb.core.ReactiveMongoTemplate
|
||||
import org.springframework.data.mongodb.core.aggregation.Aggregation.addFields
|
||||
import org.springframework.data.mongodb.core.aggregation.Aggregation.lookup
|
||||
import org.springframework.data.mongodb.core.aggregation.Aggregation.match
|
||||
import org.springframework.data.mongodb.core.aggregation.Aggregation.newAggregation
|
||||
import org.springframework.data.mongodb.core.aggregation.Aggregation.unwind
|
||||
import org.springframework.data.mongodb.core.aggregation.AggregationOperation
|
||||
import org.springframework.data.mongodb.core.aggregation.ConvertOperators
|
||||
import org.springframework.data.mongodb.core.aggregation.LookupOperation
|
||||
import org.springframework.data.mongodb.core.query.Criteria
|
||||
import org.springframework.stereotype.Service
|
||||
import space.luminic.finance.dtos.AccountDTO
|
||||
import space.luminic.finance.models.Account
|
||||
import space.luminic.finance.models.Transaction
|
||||
import space.luminic.finance.repos.AccountRepo
|
||||
|
||||
@Service
|
||||
class AccountServiceImpl(
|
||||
private val accountRepo: AccountRepo,
|
||||
private val mongoTemplate: ReactiveMongoTemplate,
|
||||
private val spaceService: SpaceService,
|
||||
private val transactionService: TransactionService
|
||||
): AccountService {
|
||||
|
||||
private fun basicAggregation(spaceId: String): List<AggregationOperation> {
|
||||
val addFieldsAsOJ = addFields()
|
||||
.addField("createdByOI")
|
||||
.withValue(ConvertOperators.valueOf("createdById").convertToObjectId())
|
||||
.addField("updatedByOI")
|
||||
.withValue(ConvertOperators.valueOf("updatedById").convertToObjectId())
|
||||
.build()
|
||||
val lookupCreatedBy = lookup("users", "createdByOI", "_id", "createdBy")
|
||||
val unwindCreatedBy = unwind("createdBy")
|
||||
|
||||
val lookupUpdatedBy = lookup("users", "updatedByOI", "_id", "updatedBy")
|
||||
val unwindUpdatedBy = unwind("updatedBy")
|
||||
|
||||
val matchCriteria = mutableListOf<Criteria>()
|
||||
matchCriteria.add(Criteria.where("spaceId").`is`(spaceId))
|
||||
matchCriteria.add(Criteria.where("isDeleted").`is`(false))
|
||||
val matchStage = match(Criteria().andOperator(*matchCriteria.toTypedArray()))
|
||||
|
||||
return listOf(addFieldsAsOJ, lookupCreatedBy, unwindCreatedBy, lookupUpdatedBy, unwindUpdatedBy, matchStage)
|
||||
}
|
||||
|
||||
|
||||
override suspend fun getAccounts(spaceId: String): List<Account> {
|
||||
val basicAggregation = basicAggregation(spaceId)
|
||||
val aggregation = newAggregation(*basicAggregation.toTypedArray())
|
||||
return mongoTemplate.aggregate(aggregation, "accounts", Account::class.java)
|
||||
.collectList()
|
||||
.awaitSingle()
|
||||
}
|
||||
|
||||
override suspend fun getAccount(
|
||||
spaceId: String,
|
||||
accountId: String
|
||||
): Account {
|
||||
val basicAggregation = basicAggregation(spaceId)
|
||||
val matchStage = match (Criteria.where("_id").`is`(ObjectId(accountId)))
|
||||
val aggregation = newAggregation(matchStage, *basicAggregation.toTypedArray())
|
||||
return mongoTemplate.aggregate(aggregation, "accounts", Account::class.java)
|
||||
.awaitSingle()
|
||||
}
|
||||
|
||||
override suspend fun getAccountTransactions(
|
||||
spaceId: String,
|
||||
accountId: String
|
||||
): List<Transaction> {
|
||||
val space = spaceService.checkSpace(spaceId)
|
||||
val filter = TransactionService.TransactionsFilter(
|
||||
accountId = accountId,
|
||||
)
|
||||
return transactionService.getTransactions(spaceId, filter, "date", "ASC")
|
||||
}
|
||||
|
||||
override suspend fun createAccount(
|
||||
spaceId: String,
|
||||
account: AccountDTO.CreateAccountDTO
|
||||
): Account {
|
||||
val createdAccount = Account(
|
||||
type = account.type,
|
||||
spaceId = spaceId,
|
||||
name = account.name,
|
||||
currencyCode = account.currencyCode,
|
||||
amount = account.amount,
|
||||
goalId = account.goalId,
|
||||
)
|
||||
return accountRepo.save(createdAccount).awaitSingle()
|
||||
}
|
||||
|
||||
override suspend fun updateAccount(
|
||||
spaceId: String,
|
||||
account: AccountDTO.UpdateAccountDTO
|
||||
): Account {
|
||||
val existingAccount = getAccount(spaceId, account.id)
|
||||
val newAccount = existingAccount.copy(
|
||||
name = account.name,
|
||||
type = account.type,
|
||||
currencyCode = account.currencyCode,
|
||||
amount = account.amount,
|
||||
goalId = account.goalId,
|
||||
)
|
||||
return accountRepo.save(newAccount).awaitSingle()
|
||||
}
|
||||
|
||||
override suspend fun deleteAccount(spaceId: String, accountId: String) {
|
||||
val existingAccount = getAccount(spaceId, accountId)
|
||||
|
||||
existingAccount.isDeleted = true
|
||||
accountRepo.save(existingAccount).awaitSingle()
|
||||
|
||||
}
|
||||
}
|
||||
109
src/main/kotlin/space/luminic/finance/services/AuthService.kt
Normal file
109
src/main/kotlin/space/luminic/finance/services/AuthService.kt
Normal file
@@ -0,0 +1,109 @@
|
||||
package space.luminic.finance.services
|
||||
|
||||
import kotlinx.coroutines.reactive.awaitFirstOrNull
|
||||
import kotlinx.coroutines.reactor.awaitSingle
|
||||
import kotlinx.coroutines.reactor.awaitSingleOrNull
|
||||
import org.springframework.cache.annotation.Cacheable
|
||||
import org.springframework.security.core.context.ReactiveSecurityContextHolder
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
|
||||
import org.springframework.stereotype.Service
|
||||
import space.luminic.finance.configs.AuthException
|
||||
import space.luminic.finance.models.Token
|
||||
import space.luminic.finance.models.User
|
||||
import space.luminic.finance.repos.UserRepo
|
||||
import space.luminic.finance.utils.JWTUtil
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneId
|
||||
import java.util.*
|
||||
|
||||
|
||||
@Service
|
||||
class AuthService(
|
||||
private val userRepository: UserRepo,
|
||||
private val tokenService: TokenService,
|
||||
private val jwtUtil: JWTUtil,
|
||||
private val userService: UserService,
|
||||
|
||||
) {
|
||||
private val passwordEncoder = BCryptPasswordEncoder()
|
||||
|
||||
suspend fun getSecurityUser(): User {
|
||||
val securityContextHolder = ReactiveSecurityContextHolder.getContext().awaitSingleOrNull()
|
||||
?: throw AuthException("Authentication failed")
|
||||
val authentication = securityContextHolder.authentication
|
||||
|
||||
val username = authentication.name
|
||||
// Получаем пользователя по имени
|
||||
return userService.getByUsername(username)
|
||||
}
|
||||
|
||||
suspend fun login(username: String, password: String): String {
|
||||
val user = userRepository.findByUsername(username).awaitFirstOrNull()
|
||||
?: throw UsernameNotFoundException("Пользователь не найден")
|
||||
return if (passwordEncoder.matches(password, user.password)) {
|
||||
val token = jwtUtil.generateToken(user.username)
|
||||
val expireAt = Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24 * 10)
|
||||
tokenService.saveToken(
|
||||
token = token,
|
||||
username = username,
|
||||
expiresAt = LocalDateTime.ofInstant(
|
||||
expireAt.toInstant(),
|
||||
ZoneId.systemDefault()
|
||||
)
|
||||
)
|
||||
token
|
||||
} else {
|
||||
throw IllegalArgumentException("Ошибка логина или пароля")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun tgLogin(tgId: String): String {
|
||||
val user =
|
||||
userRepository.findByTgId(tgId).awaitSingleOrNull() ?: throw UsernameNotFoundException("Пользователь не найден")
|
||||
|
||||
val token = jwtUtil.generateToken(user.username)
|
||||
val expireAt = Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24 * 10)
|
||||
tokenService.saveToken(
|
||||
token = token,
|
||||
username = user.username,
|
||||
expiresAt = LocalDateTime.ofInstant(
|
||||
expireAt.toInstant(),
|
||||
ZoneId.systemDefault()
|
||||
)
|
||||
)
|
||||
return token
|
||||
|
||||
}
|
||||
|
||||
suspend fun register(username: String, password: String, firstName: String): User {
|
||||
val user = userRepository.findByUsername(username).awaitSingleOrNull()
|
||||
if (user == null) {
|
||||
var newUser = User(
|
||||
username = username,
|
||||
password = passwordEncoder.encode(password), // Шифрование пароля
|
||||
firstName = firstName,
|
||||
roles = mutableListOf("USER")
|
||||
)
|
||||
newUser = userRepository.save(newUser).awaitSingle()
|
||||
return newUser
|
||||
} else throw IllegalArgumentException("Пользователь уже зарегистрирован")
|
||||
}
|
||||
|
||||
|
||||
@Cacheable(cacheNames = ["tokens"], key = "#token")
|
||||
suspend fun isTokenValid(token: String): User {
|
||||
val tokenDetails = tokenService.getToken(token).awaitFirstOrNull() ?: throw AuthException("Токен не валиден")
|
||||
when {
|
||||
tokenDetails.status == Token.TokenStatus.ACTIVE && tokenDetails.expiresAt.isAfter(LocalDateTime.now()) -> {
|
||||
return userService.getByUsername(tokenDetails.username)
|
||||
}
|
||||
|
||||
else -> {
|
||||
tokenService.revokeToken(tokenDetails.token)
|
||||
throw AuthException("Токен истек или не валиден")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package space.luminic.finance.services
|
||||
|
||||
import space.luminic.finance.dtos.BudgetDTO.*
|
||||
import space.luminic.finance.models.Budget
|
||||
import space.luminic.finance.models.Transaction
|
||||
|
||||
interface BudgetService {
|
||||
|
||||
suspend fun getBudgets(spaceId: String, sortBy: String, sortDirection: String): List<Budget>
|
||||
suspend fun getBudget(spaceId: String, budgetId: String): Budget
|
||||
suspend fun getBudgetTransactions(spaceId: String, budgetId: String): List<Transaction>
|
||||
suspend fun createBudget(spaceId: String, type: Budget.BudgetType, budgetDto: CreateBudgetDTO): Budget
|
||||
suspend fun updateBudget(spaceId: String, budgetDto: UpdateBudgetDTO): Budget
|
||||
suspend fun deleteBudget(spaceId: String, budgetId: String)
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
package space.luminic.finance.services
|
||||
|
||||
import kotlinx.coroutines.reactive.awaitFirstOrNull
|
||||
import kotlinx.coroutines.reactive.awaitSingle
|
||||
import org.bson.Document
|
||||
import org.bson.types.ObjectId
|
||||
import org.springframework.data.domain.Sort
|
||||
import org.springframework.data.domain.Sort.Direction
|
||||
import org.springframework.data.mongodb.core.ReactiveMongoTemplate
|
||||
import org.springframework.data.mongodb.core.aggregation.Aggregation.*
|
||||
import org.springframework.data.mongodb.core.aggregation.Aggregation.newAggregation
|
||||
import org.springframework.data.mongodb.core.aggregation.Aggregation.sort
|
||||
import org.springframework.data.mongodb.core.aggregation.AggregationOperation
|
||||
import org.springframework.data.mongodb.core.aggregation.ConvertOperators
|
||||
import org.springframework.data.mongodb.core.aggregation.LookupOperation
|
||||
import org.springframework.data.mongodb.core.aggregation.SetOperation.set
|
||||
import org.springframework.data.mongodb.core.aggregation.UnsetOperation.unset
|
||||
import org.springframework.data.mongodb.core.aggregation.VariableOperators
|
||||
import org.springframework.data.mongodb.core.query.Criteria
|
||||
import org.springframework.stereotype.Service
|
||||
import space.luminic.finance.dtos.BudgetDTO
|
||||
import space.luminic.finance.models.Budget
|
||||
import space.luminic.finance.models.NotFoundException
|
||||
import space.luminic.finance.models.Transaction
|
||||
import space.luminic.finance.repos.BudgetRepo
|
||||
import java.math.BigDecimal
|
||||
|
||||
@Service
|
||||
class BudgetServiceImpl(
|
||||
private val budgetRepo: BudgetRepo,
|
||||
private val authService: AuthService,
|
||||
private val categoryService: CategoryService,
|
||||
private val mongoTemplate: ReactiveMongoTemplate,
|
||||
private val spaceService: SpaceService,
|
||||
private val transactionService: TransactionService,
|
||||
) : BudgetService {
|
||||
|
||||
private fun basicAggregation(spaceId: String): List<AggregationOperation> {
|
||||
|
||||
val unwindCategories = unwind("categories", true)
|
||||
val setCategoryIdOI = set("categories.categoryIdOI")
|
||||
.toValue(ConvertOperators.valueOf("categories.categoryId").convertToObjectId())
|
||||
val lookupCategory = lookup(
|
||||
"categories", // from
|
||||
"categories.categoryIdOI", // localField
|
||||
"_id", // foreignField
|
||||
"joinedCategory" // as
|
||||
)
|
||||
val unwindJoinedCategory = unwind("joinedCategory", true)
|
||||
val setEmbeddedCategory = set("categories.category").toValue("\$joinedCategory")
|
||||
val unsetTemps = unset("joinedCategory", "categories.categoryIdOI")
|
||||
val groupBack: AggregationOperation = AggregationOperation {
|
||||
Document(
|
||||
"\$group", Document()
|
||||
.append("_id", "\$_id")
|
||||
.append("doc", Document("\$first", "\$\$ROOT"))
|
||||
.append("categories", Document("\$push", "\$categories"))
|
||||
)
|
||||
}
|
||||
val setDocCategories: AggregationOperation = AggregationOperation {
|
||||
Document("\$set", Document("doc.categories", "\$categories"))
|
||||
}
|
||||
val replaceRootDoc = replaceRoot("doc")
|
||||
|
||||
val addFieldsAsOJ = addFields()
|
||||
.addField("createdByOI")
|
||||
.withValue(ConvertOperators.valueOf("createdById").convertToObjectId())
|
||||
.addField("updatedByOI")
|
||||
.withValue(ConvertOperators.valueOf("updatedById").convertToObjectId())
|
||||
.build()
|
||||
val lookupCreatedBy = lookup("users", "createdByOI", "_id", "createdBy")
|
||||
val unwindCreatedBy = unwind("createdBy")
|
||||
|
||||
val lookupUpdatedBy = lookup("users", "updatedByOI", "_id", "updatedBy")
|
||||
val unwindUpdatedBy = unwind("updatedBy")
|
||||
|
||||
|
||||
val matchCriteria = mutableListOf<Criteria>()
|
||||
matchCriteria.add(Criteria.where("spaceId").`is`(spaceId))
|
||||
matchCriteria.add(Criteria.where("isDeleted").`is`(false))
|
||||
val matchStage = match(Criteria().andOperator(*matchCriteria.toTypedArray()))
|
||||
|
||||
return listOf(matchStage,
|
||||
unwindCategories,
|
||||
setCategoryIdOI,
|
||||
lookupCategory,
|
||||
unwindJoinedCategory,
|
||||
setEmbeddedCategory,
|
||||
unsetTemps,
|
||||
groupBack,
|
||||
setDocCategories,
|
||||
replaceRootDoc,
|
||||
addFieldsAsOJ,
|
||||
lookupCreatedBy,
|
||||
unwindCreatedBy,
|
||||
lookupUpdatedBy,
|
||||
unwindUpdatedBy)
|
||||
}
|
||||
|
||||
|
||||
override suspend fun getBudgets(spaceId: String, sortBy: String, sortDirection: String): List<Budget> {
|
||||
|
||||
require(spaceId.isNotBlank()) { "Space ID must not be blank" }
|
||||
|
||||
val allowedSortFields = setOf("dateFrom", "dateTo", "amount", "categoryName", "createdAt")
|
||||
require(sortBy in allowedSortFields) { "Invalid sort field: $sortBy" }
|
||||
|
||||
val direction = when (sortDirection.uppercase()) {
|
||||
"ASC" -> Direction.ASC
|
||||
"DESC" -> Direction.DESC
|
||||
else -> throw IllegalArgumentException("Sort direction must be 'ASC' or 'DESC'")
|
||||
}
|
||||
val sort = sort(Sort.by(direction, sortBy))
|
||||
val basicAggregation = basicAggregation(spaceId)
|
||||
val aggregation =
|
||||
newAggregation(
|
||||
*basicAggregation.toTypedArray(),
|
||||
sort
|
||||
)
|
||||
|
||||
return mongoTemplate.aggregate(aggregation, "budgets", Budget::class.java)
|
||||
.collectList()
|
||||
.awaitSingle()
|
||||
|
||||
}
|
||||
|
||||
override suspend fun getBudget(spaceId: String, budgetId: String): Budget {
|
||||
val basicAggregation = basicAggregation(spaceId)
|
||||
val matchStage = match(Criteria.where("_id").`is`(ObjectId(budgetId)))
|
||||
val aggregation = newAggregation(matchStage, *basicAggregation.toTypedArray(), )
|
||||
return mongoTemplate.aggregate(aggregation, "budgets", Budget::class.java).awaitFirstOrNull()
|
||||
?: throw NotFoundException("Budget not found")
|
||||
|
||||
}
|
||||
|
||||
override suspend fun getBudgetTransactions(
|
||||
spaceId: String,
|
||||
budgetId: String
|
||||
): List<Transaction> {
|
||||
spaceService.checkSpace(spaceId)
|
||||
val budget = getBudget(spaceId, budgetId)
|
||||
val filter = TransactionService.TransactionsFilter(
|
||||
dateFrom = budget.dateFrom,
|
||||
dateTo = budget.dateTo
|
||||
)
|
||||
return transactionService.getTransactions(spaceId, filter, "date", "ASC")
|
||||
|
||||
}
|
||||
|
||||
|
||||
override suspend fun createBudget(
|
||||
spaceId: String,
|
||||
type: Budget.BudgetType,
|
||||
budgetDto: BudgetDTO.CreateBudgetDTO
|
||||
): Budget {
|
||||
val user = authService.getSecurityUser()
|
||||
val categories = categoryService.getCategories(spaceId)
|
||||
val budget = Budget(
|
||||
spaceId = spaceId,
|
||||
type = type,
|
||||
name = budgetDto.name,
|
||||
description = budgetDto.description,
|
||||
categories = categories.map { Budget.BudgetCategory(it.id!!, BigDecimal.ZERO) },
|
||||
dateFrom = budgetDto.dateFrom,
|
||||
dateTo = budgetDto.dateTo
|
||||
)
|
||||
return budgetRepo.save(budget).awaitSingle()
|
||||
}
|
||||
|
||||
override suspend fun updateBudget(
|
||||
spaceId: String,
|
||||
budgetDto: BudgetDTO.UpdateBudgetDTO
|
||||
): Budget {
|
||||
val budget = getBudget(spaceId, budgetDto.id)
|
||||
budgetDto.name?.let { name -> budget.name = name }
|
||||
budgetDto.description?.let { description -> budget.description = description }
|
||||
budgetDto.dateFrom?.let { dateFrom -> budget.dateFrom = dateFrom }
|
||||
budgetDto.dateTo?.let { dateTo -> budget.dateTo = dateTo }
|
||||
|
||||
return budgetRepo.save(budget).awaitSingle()
|
||||
}
|
||||
|
||||
override suspend fun deleteBudget(spaceId: String, budgetId: String) {
|
||||
val budget = getBudget(spaceId, budgetId)
|
||||
budget.isDeleted = true
|
||||
budgetRepo.save(budget).awaitSingle()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package space.luminic.finance.services
|
||||
|
||||
import space.luminic.finance.dtos.BudgetDTO
|
||||
import space.luminic.finance.dtos.CategoryDTO
|
||||
import space.luminic.finance.models.Category
|
||||
import space.luminic.finance.models.Space
|
||||
|
||||
interface CategoryService {
|
||||
suspend fun getCategories(spaceId: String): List<Category>
|
||||
suspend fun getCategory(spaceId: String, id: String): Category
|
||||
suspend fun createCategory(spaceId: String, category: CategoryDTO.CreateCategoryDTO): Category
|
||||
suspend fun updateCategory(spaceId: String,category: CategoryDTO.UpdateCategoryDTO): Category
|
||||
suspend fun deleteCategory(spaceId: String, id: String)
|
||||
suspend fun createCategoriesForSpace(spaceId: String): List<Category>
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package space.luminic.finance.services
|
||||
|
||||
import kotlinx.coroutines.reactive.awaitSingle
|
||||
import org.bson.types.ObjectId
|
||||
import org.springframework.data.mongodb.core.ReactiveMongoTemplate
|
||||
import org.springframework.data.mongodb.core.aggregation.Aggregation.addFields
|
||||
import org.springframework.data.mongodb.core.aggregation.Aggregation.lookup
|
||||
import org.springframework.data.mongodb.core.aggregation.Aggregation.match
|
||||
import org.springframework.data.mongodb.core.aggregation.Aggregation.newAggregation
|
||||
import org.springframework.data.mongodb.core.aggregation.Aggregation.unwind
|
||||
import org.springframework.data.mongodb.core.aggregation.AggregationOperation
|
||||
import org.springframework.data.mongodb.core.aggregation.ConvertOperators
|
||||
import org.springframework.data.mongodb.core.query.Criteria
|
||||
import org.springframework.stereotype.Service
|
||||
import space.luminic.finance.dtos.CategoryDTO
|
||||
import space.luminic.finance.models.Category
|
||||
import space.luminic.finance.models.Space
|
||||
import space.luminic.finance.repos.CategoryEtalonRepo
|
||||
import space.luminic.finance.repos.CategoryRepo
|
||||
|
||||
@Service
|
||||
class CategoryServiceImpl(
|
||||
private val categoryRepo: CategoryRepo,
|
||||
private val categoryEtalonRepo: CategoryEtalonRepo,
|
||||
private val reactiveMongoTemplate: ReactiveMongoTemplate,
|
||||
private val authService: AuthService,
|
||||
) : CategoryService {
|
||||
|
||||
private fun basicAggregation(spaceId: String): List<AggregationOperation> {
|
||||
val addFieldsAsOJ = addFields()
|
||||
.addField("createdByOI")
|
||||
.withValue(ConvertOperators.valueOf("createdById").convertToObjectId())
|
||||
.addField("updatedByOI")
|
||||
.withValue(ConvertOperators.valueOf("updatedById").convertToObjectId())
|
||||
.build()
|
||||
val lookupCreatedBy = lookup("users", "createdByOI", "_id", "createdBy")
|
||||
val unwindCreatedBy = unwind("createdBy")
|
||||
|
||||
val lookupUpdatedBy = lookup("users", "updatedByOI", "_id", "updatedBy")
|
||||
val unwindUpdatedBy = unwind("updatedBy")
|
||||
val matchCriteria = mutableListOf<Criteria>()
|
||||
matchCriteria.add(Criteria.where("spaceId").`is`(spaceId))
|
||||
matchCriteria.add(Criteria.where("isDeleted").`is`(false))
|
||||
val matchStage = match(Criteria().andOperator(*matchCriteria.toTypedArray()))
|
||||
|
||||
return listOf(addFieldsAsOJ, lookupCreatedBy, unwindCreatedBy, lookupUpdatedBy, unwindUpdatedBy, matchStage)
|
||||
}
|
||||
|
||||
override suspend fun getCategories(spaceId: String): List<Category> {
|
||||
val basicAggregation = basicAggregation(spaceId)
|
||||
val aggregation = newAggregation(*basicAggregation.toTypedArray())
|
||||
return reactiveMongoTemplate.aggregate(aggregation, "categories", Category::class.java).collectList().awaitSingle()
|
||||
}
|
||||
|
||||
override suspend fun getCategory(spaceId: String, id: String): Category {
|
||||
val basicAggregation = basicAggregation(spaceId)
|
||||
val match = match(Criteria.where("_id").`is`(ObjectId(id)))
|
||||
val aggregation = newAggregation(*basicAggregation.toTypedArray(), match)
|
||||
return reactiveMongoTemplate.aggregate(aggregation, "categories", Category::class.java).awaitSingle()
|
||||
}
|
||||
|
||||
|
||||
override suspend fun createCategory(
|
||||
spaceId: String,
|
||||
category: CategoryDTO.CreateCategoryDTO
|
||||
): Category {
|
||||
val createdCategory = Category(
|
||||
spaceId = spaceId,
|
||||
type = category.type,
|
||||
name = category.name,
|
||||
icon = category.icon
|
||||
)
|
||||
return categoryRepo.save(createdCategory).awaitSingle()
|
||||
}
|
||||
|
||||
override suspend fun updateCategory(
|
||||
spaceId: String,
|
||||
category: CategoryDTO.UpdateCategoryDTO
|
||||
): Category {
|
||||
val existingCategory = getCategory(spaceId, category.id)
|
||||
val updatedCategory = existingCategory.copy(
|
||||
type = category.type,
|
||||
name = category.name,
|
||||
icon = category.icon,
|
||||
)
|
||||
return categoryRepo.save(updatedCategory).awaitSingle()
|
||||
}
|
||||
|
||||
override suspend fun deleteCategory(spaceId: String, id: String) {
|
||||
val existingCategory = getCategory(spaceId, id)
|
||||
existingCategory.isDeleted = true
|
||||
categoryRepo.save(existingCategory).awaitSingle()
|
||||
}
|
||||
|
||||
override suspend fun createCategoriesForSpace(spaceId: String): List<Category> {
|
||||
val etalonCategories = categoryEtalonRepo.findAll().collectList().awaitSingle()
|
||||
val toCreate = etalonCategories.map {
|
||||
Category(
|
||||
spaceId = spaceId,
|
||||
type = it.type,
|
||||
name = it.name,
|
||||
icon = it.icon
|
||||
)
|
||||
}
|
||||
return categoryRepo.saveAll(toCreate).collectList().awaitSingle()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package space.luminic.finance.services
|
||||
|
||||
import kotlinx.coroutines.reactor.mono
|
||||
import org.springframework.data.domain.ReactiveAuditorAware
|
||||
import org.springframework.stereotype.Component
|
||||
import reactor.core.publisher.Mono
|
||||
|
||||
@Component
|
||||
class CoroutineAuditorAware(
|
||||
private val authService: AuthService
|
||||
) : ReactiveAuditorAware<String> {
|
||||
override fun getCurrentAuditor(): Mono<String> =
|
||||
mono {
|
||||
authService.getSecurityUser().id!!
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package space.luminic.finance.services
|
||||
|
||||
import space.luminic.finance.dtos.CurrencyDTO
|
||||
import space.luminic.finance.models.Currency
|
||||
import space.luminic.finance.models.CurrencyRate
|
||||
|
||||
interface CurrencyService {
|
||||
|
||||
suspend fun getCurrencies(): List<Currency>
|
||||
suspend fun getCurrency(currencyCode: String): Currency
|
||||
suspend fun createCurrency(currency: CurrencyDTO): Currency
|
||||
suspend fun updateCurrency(currency: CurrencyDTO): Currency
|
||||
suspend fun deleteCurrency(currencyCode: String)
|
||||
suspend fun createCurrencyRate(currencyCode: String): CurrencyRate
|
||||
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package space.luminic.finance.services
|
||||
|
||||
import kotlinx.coroutines.reactive.awaitSingle
|
||||
import org.springframework.stereotype.Service
|
||||
import space.luminic.finance.dtos.CurrencyDTO
|
||||
import space.luminic.finance.models.Currency
|
||||
import space.luminic.finance.models.CurrencyRate
|
||||
import space.luminic.finance.repos.CurrencyRateRepo
|
||||
import space.luminic.finance.repos.CurrencyRepo
|
||||
import java.math.BigDecimal
|
||||
import java.time.LocalDate
|
||||
|
||||
@Service
|
||||
class CurrencyServiceImpl(
|
||||
private val currencyRepo: CurrencyRepo,
|
||||
private val currencyRateRepo: CurrencyRateRepo
|
||||
) : CurrencyService {
|
||||
|
||||
override suspend fun getCurrencies(): List<Currency> {
|
||||
return currencyRepo.findAll().collectList().awaitSingle()
|
||||
}
|
||||
|
||||
override suspend fun getCurrency(currencyCode: String): Currency {
|
||||
return currencyRepo.findById(currencyCode).awaitSingle()
|
||||
}
|
||||
|
||||
override suspend fun createCurrency(currency: CurrencyDTO): Currency {
|
||||
val createdCurrency = Currency(currency.code, currency.name, currency.symbol)
|
||||
return currencyRepo.save(createdCurrency).awaitSingle()
|
||||
}
|
||||
|
||||
override suspend fun updateCurrency(currency: CurrencyDTO): Currency {
|
||||
val existingCurrency = currencyRepo.findById(currency.code).awaitSingle()
|
||||
val newCurrency = existingCurrency.copy(name = currency.name, symbol = currency.symbol)
|
||||
return currencyRepo.save(newCurrency).awaitSingle()
|
||||
}
|
||||
|
||||
override suspend fun deleteCurrency(currencyCode: String) {
|
||||
currencyRepo.deleteById(currencyCode).awaitSingle()
|
||||
}
|
||||
|
||||
override suspend fun createCurrencyRate(currencyCode: String): CurrencyRate {
|
||||
return currencyRateRepo.save(
|
||||
CurrencyRate(
|
||||
currencyCode = currencyCode,
|
||||
rate = BigDecimal(12.0),
|
||||
date = LocalDate.now(),
|
||||
)
|
||||
).awaitSingle()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package space.luminic.finance.services
|
||||
|
||||
import space.luminic.finance.dtos.SpaceDTO
|
||||
import space.luminic.finance.models.Budget
|
||||
import space.luminic.finance.models.Space
|
||||
|
||||
interface SpaceService {
|
||||
|
||||
suspend fun checkSpace(spaceId: String): Space
|
||||
suspend fun getSpaces(): List<Space>
|
||||
suspend fun getSpace(id: String): Space
|
||||
suspend fun createSpace(space: SpaceDTO.CreateSpaceDTO): Space
|
||||
suspend fun updateSpace(spaceId: String, space: SpaceDTO.UpdateSpaceDTO): Space
|
||||
suspend fun deleteSpace(spaceId: String)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
package space.luminic.finance.services
|
||||
|
||||
import com.mongodb.client.model.Aggregates.sort
|
||||
import kotlinx.coroutines.reactive.awaitFirst
|
||||
import kotlinx.coroutines.reactive.awaitFirstOrNull
|
||||
import kotlinx.coroutines.reactive.awaitSingle
|
||||
import kotlinx.coroutines.reactive.awaitSingleOrNull
|
||||
import org.bson.types.ObjectId
|
||||
import org.springframework.data.domain.Sort
|
||||
import org.springframework.data.mongodb.core.ReactiveMongoTemplate
|
||||
import org.springframework.data.mongodb.core.aggregation.Aggregation.*
|
||||
import org.springframework.data.mongodb.core.aggregation.AggregationOperation
|
||||
|
||||
import org.springframework.data.mongodb.core.aggregation.ArrayOperators
|
||||
import org.springframework.data.mongodb.core.aggregation.ConvertOperators
|
||||
import org.springframework.data.mongodb.core.aggregation.VariableOperators
|
||||
import org.springframework.data.mongodb.core.query.Criteria
|
||||
import org.springframework.stereotype.Service
|
||||
import space.luminic.finance.dtos.SpaceDTO
|
||||
import space.luminic.finance.models.Budget
|
||||
import space.luminic.finance.models.NotFoundException
|
||||
import space.luminic.finance.models.Space
|
||||
import space.luminic.finance.models.User
|
||||
import space.luminic.finance.repos.SpaceRepo
|
||||
|
||||
@Service
|
||||
class SpaceServiceImpl(
|
||||
private val authService: AuthService,
|
||||
private val spaceRepo: SpaceRepo,
|
||||
private val mongoTemplate: ReactiveMongoTemplate,
|
||||
) : SpaceService {
|
||||
|
||||
private fun basicAggregation(user: User): List<AggregationOperation> {
|
||||
val addFieldsAsOJ = addFields()
|
||||
.addField("createdByOI")
|
||||
.withValue(ConvertOperators.valueOf("createdById").convertToObjectId())
|
||||
.addField("updatedByOI")
|
||||
.withValue(ConvertOperators.valueOf("updatedById").convertToObjectId())
|
||||
.build()
|
||||
val lookupCreatedBy = lookup("users", "createdByOI", "_id", "createdBy")
|
||||
val unwindCreatedBy = unwind("createdBy")
|
||||
|
||||
val lookupUpdatedBy = lookup("users", "updatedByOI", "_id", "updatedBy")
|
||||
val unwindUpdatedBy = unwind("updatedBy")
|
||||
|
||||
|
||||
|
||||
val matchCriteria = mutableListOf<Criteria>()
|
||||
matchCriteria.add(
|
||||
Criteria().orOperator(
|
||||
Criteria.where("ownerId").`is`(user.id),
|
||||
Criteria.where("participantsIds").`is`(user.id)
|
||||
)
|
||||
)
|
||||
matchCriteria.add(Criteria.where("isDeleted").`is`(false))
|
||||
val matchStage = match(Criteria().andOperator(*matchCriteria.toTypedArray()))
|
||||
|
||||
return listOf(addFieldsAsOJ, lookupCreatedBy, unwindCreatedBy, lookupUpdatedBy, unwindUpdatedBy, matchStage)
|
||||
}
|
||||
|
||||
private fun ownerAndParticipantsLookups(): List<AggregationOperation>{
|
||||
val addOwnerAsOJ = addFields()
|
||||
.addField("ownerIdAsObjectId")
|
||||
.withValue(ConvertOperators.valueOf("ownerId").convertToObjectId())
|
||||
.addField("participantsIdsAsObjectId")
|
||||
.withValue(
|
||||
VariableOperators.Map.itemsOf("participantsIds")
|
||||
.`as`("id")
|
||||
.andApply(
|
||||
ConvertOperators.valueOf("$\$id").convertToObjectId()
|
||||
)
|
||||
)
|
||||
.build()
|
||||
val lookupOwner = lookup("users", "ownerIdAsObjectId", "_id", "owner")
|
||||
val unwindOwner = unwind("owner")
|
||||
val lookupUsers = lookup("users", "participantsIdsAsObjectId", "_id", "participants")
|
||||
return listOf(addOwnerAsOJ, lookupOwner, unwindOwner, lookupUsers)
|
||||
}
|
||||
|
||||
override suspend fun checkSpace(spaceId: String): Space {
|
||||
val user = authService.getSecurityUser()
|
||||
val space = getSpace(spaceId)
|
||||
|
||||
// Проверяем доступ пользователя к пространству
|
||||
return if (space.participants!!.none { it.id.toString() == user.id }) {
|
||||
throw IllegalArgumentException("User does not have access to this Space")
|
||||
} else space
|
||||
}
|
||||
|
||||
override suspend fun getSpaces(): List<Space> {
|
||||
val user = authService.getSecurityUser()
|
||||
val basicAggregation = basicAggregation(user)
|
||||
val ownerAndParticipantsLookup = ownerAndParticipantsLookups()
|
||||
|
||||
val sort = sort(Sort.by(Sort.Direction.DESC, "createdAt"))
|
||||
val aggregation = newAggregation(
|
||||
*basicAggregation.toTypedArray(),
|
||||
*ownerAndParticipantsLookup.toTypedArray(),
|
||||
sort,
|
||||
)
|
||||
return mongoTemplate.aggregate(aggregation, "spaces", Space::class.java).collectList().awaitSingle()
|
||||
}
|
||||
|
||||
override suspend fun getSpace(id: String): Space {
|
||||
val user = authService.getSecurityUser()
|
||||
val basicAggregation = basicAggregation(user)
|
||||
val ownerAndParticipantsLookup = ownerAndParticipantsLookups()
|
||||
|
||||
val aggregation = newAggregation(
|
||||
*basicAggregation.toTypedArray(),
|
||||
*ownerAndParticipantsLookup.toTypedArray(),
|
||||
)
|
||||
return mongoTemplate.aggregate(aggregation, "spaces", Space::class.java).awaitFirstOrNull()
|
||||
?: throw NotFoundException("Space not found")
|
||||
|
||||
}
|
||||
|
||||
override suspend fun createSpace(space: SpaceDTO.CreateSpaceDTO): Space {
|
||||
val owner = authService.getSecurityUser()
|
||||
val createdSpace = Space(
|
||||
name = space.name,
|
||||
ownerId = owner.id!!,
|
||||
|
||||
participantsIds = listOf(owner.id!!),
|
||||
|
||||
|
||||
)
|
||||
createdSpace.owner = owner
|
||||
createdSpace.participants?.toMutableList()?.add(owner)
|
||||
val savedSpace = spaceRepo.save(createdSpace).awaitSingle()
|
||||
return savedSpace
|
||||
}
|
||||
|
||||
override suspend fun updateSpace(spaceId: String, space: SpaceDTO.UpdateSpaceDTO): Space {
|
||||
val existingSpace = spaceRepo.findById(spaceId).awaitFirstOrNull() ?: throw NotFoundException("Space not found")
|
||||
val updatedSpace = existingSpace.copy(
|
||||
name = space.name,
|
||||
)
|
||||
updatedSpace.owner = existingSpace.owner
|
||||
updatedSpace.participants = existingSpace.participants
|
||||
return spaceRepo.save(updatedSpace).awaitFirst()
|
||||
}
|
||||
|
||||
override suspend fun deleteSpace(spaceId: String) {
|
||||
val space = spaceRepo.findById(spaceId).awaitFirstOrNull() ?: throw NotFoundException("Space not found")
|
||||
space.isDeleted = true
|
||||
spaceRepo.save(space).awaitFirst()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package space.luminic.finance.services
|
||||
|
||||
|
||||
import com.interaso.webpush.VapidKeys
|
||||
import com.interaso.webpush.WebPushService
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.reactive.awaitSingle
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.bson.types.ObjectId
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.dao.DuplicateKeyException
|
||||
import org.springframework.stereotype.Service
|
||||
import space.luminic.finance.models.PushMessage
|
||||
import space.luminic.finance.models.Subscription
|
||||
import space.luminic.finance.models.SubscriptionDTO
|
||||
import space.luminic.finance.models.User
|
||||
import space.luminic.finance.repos.SubscriptionRepo
|
||||
import space.luminic.finance.services.VapidConstants.VAPID_PRIVATE_KEY
|
||||
import space.luminic.finance.services.VapidConstants.VAPID_PUBLIC_KEY
|
||||
import space.luminic.finance.services.VapidConstants.VAPID_SUBJECT
|
||||
import kotlin.collections.forEach
|
||||
import kotlin.jvm.javaClass
|
||||
import kotlin.text.orEmpty
|
||||
|
||||
object VapidConstants {
|
||||
const val VAPID_PUBLIC_KEY =
|
||||
"BKmMyBUhpkcmzYWcYsjH_spqcy0zf_8eVtZo60f7949TgLztCmv3YD0E_vtV2dTfECQ4sdLdPK3ICDcyOkCqr84"
|
||||
const val VAPID_PRIVATE_KEY = "YeJH_0LhnVYN6RdxMidgR6WMYlpGXTJS3HjT9V3NSGI"
|
||||
const val VAPID_SUBJECT = "mailto:voroninvyu@gmail.com"
|
||||
}
|
||||
|
||||
@Service
|
||||
class SubscriptionService(private val subscriptionRepo: SubscriptionRepo) {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(javaClass)
|
||||
private val pushService =
|
||||
WebPushService(
|
||||
subject = VAPID_SUBJECT,
|
||||
vapidKeys = VapidKeys.fromUncompressedBytes(VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY)
|
||||
)
|
||||
|
||||
|
||||
suspend fun sendToSpaceOwner(ownerId: String, message: PushMessage) = coroutineScope {
|
||||
val ownerTokens = subscriptionRepo.findByUserIdAndIsActive(ObjectId(ownerId)).collectList().awaitSingle()
|
||||
|
||||
ownerTokens.forEach { token ->
|
||||
launch(Dispatchers.IO) { // Теперь мы точно в корутин скоупе
|
||||
try {
|
||||
sendNotification(token.endpoint, token.p256dh, token.auth, message)
|
||||
} catch (e: Exception) {
|
||||
logger.error("Ошибка при отправке уведомления: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
suspend fun sendNotification(endpoint: String, p256dh: String, auth: String, payload: PushMessage) {
|
||||
try {
|
||||
pushService.send(
|
||||
payload = Json.encodeToString(payload),
|
||||
endpoint = endpoint,
|
||||
p256dh = p256dh,
|
||||
auth = auth
|
||||
)
|
||||
logger.info("Уведомление успешно отправлено на endpoint: $endpoint")
|
||||
|
||||
} catch (e: Exception) {
|
||||
logger.error("Ошибка при отправке уведомления на endpoint $endpoint: ${e.message}")
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
suspend fun sendToAll(payload: PushMessage) {
|
||||
|
||||
subscriptionRepo.findAll().collectList().awaitSingle().forEach { sub ->
|
||||
|
||||
try {
|
||||
sendNotification(sub.endpoint, sub.p256dh, sub.auth, payload)
|
||||
} catch (e: Exception) {
|
||||
sub.isActive = false
|
||||
subscriptionRepo.save(sub).awaitSingle()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
suspend fun subscribe(subscriptionDTO: SubscriptionDTO, user: User): String {
|
||||
val subscription = Subscription(
|
||||
id = null,
|
||||
user = user,
|
||||
endpoint = subscriptionDTO.endpoint,
|
||||
auth = subscriptionDTO.keys["auth"].orEmpty(),
|
||||
p256dh = subscriptionDTO.keys["p256dh"].orEmpty(),
|
||||
isActive = true
|
||||
)
|
||||
|
||||
return try {
|
||||
val savedSubscription = subscriptionRepo.save(subscription).awaitSingle()
|
||||
"Subscription created with ID: ${savedSubscription.id}"
|
||||
} catch (e: DuplicateKeyException) {
|
||||
logger.info("Subscription already exists. Skipping.")
|
||||
"Subscription already exists. Skipping."
|
||||
} catch (e: Exception) {
|
||||
logger.error("Error while saving subscription: ${e.message}")
|
||||
throw kotlin.RuntimeException("Error while saving subscription")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package space.luminic.finance.services
|
||||
|
||||
import kotlinx.coroutines.reactor.awaitSingle
|
||||
import org.springframework.cache.annotation.CacheEvict
|
||||
import org.springframework.stereotype.Service
|
||||
import reactor.core.publisher.Mono
|
||||
import space.luminic.finance.models.Token
|
||||
import space.luminic.finance.models.Token.TokenStatus
|
||||
import space.luminic.finance.repos.TokenRepo
|
||||
import java.time.LocalDateTime
|
||||
|
||||
@Service
|
||||
class TokenService(private val tokenRepository: TokenRepo) {
|
||||
|
||||
@CacheEvict("tokens", allEntries = true)
|
||||
suspend fun saveToken(token: String, username: String, expiresAt: LocalDateTime): Token {
|
||||
val newToken = Token(
|
||||
token = token,
|
||||
username = username,
|
||||
issuedAt = LocalDateTime.now(),
|
||||
expiresAt = expiresAt
|
||||
)
|
||||
return tokenRepository.save(newToken).awaitSingle()
|
||||
}
|
||||
|
||||
fun getToken(token: String): Mono<Token> {
|
||||
return tokenRepository.findByToken(token)
|
||||
}
|
||||
|
||||
|
||||
fun revokeToken(token: String) {
|
||||
val tokenDetail =
|
||||
tokenRepository.findByToken(token).block()!!
|
||||
val updatedToken = tokenDetail.copy(status = TokenStatus.REVOKED)
|
||||
tokenRepository.save(updatedToken).block()
|
||||
}
|
||||
|
||||
|
||||
@CacheEvict("tokens", allEntries = true)
|
||||
fun deleteExpiredTokens() {
|
||||
tokenRepository.deleteByExpiresAtBefore(LocalDateTime.now())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package space.luminic.finance.services
|
||||
|
||||
import space.luminic.finance.dtos.TransactionDTO
|
||||
import space.luminic.finance.models.Transaction
|
||||
import java.time.LocalDate
|
||||
|
||||
interface TransactionService {
|
||||
|
||||
data class TransactionsFilter(
|
||||
val accountId: String,
|
||||
val dateFrom: LocalDate? = null,
|
||||
val dateTo: LocalDate? = null,
|
||||
)
|
||||
|
||||
suspend fun getTransactions(spaceId: String, filter: TransactionsFilter, sortBy: String, sortDirection: String): List<Transaction>
|
||||
suspend fun getTransaction(spaceId: String, transactionId: String): Transaction
|
||||
suspend fun createTransaction(spaceId: String, transaction: TransactionDTO.CreateTransactionDTO): Transaction
|
||||
suspend fun updateTransaction(spaceId: String, transaction: TransactionDTO.UpdateTransactionDTO): Transaction
|
||||
suspend fun deleteTransaction(spaceId: String, transactionId: String)
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
package space.luminic.finance.services
|
||||
|
||||
import kotlinx.coroutines.reactive.awaitFirstOrNull
|
||||
import kotlinx.coroutines.reactive.awaitSingle
|
||||
import org.bson.types.ObjectId
|
||||
import org.springframework.data.domain.Sort
|
||||
import org.springframework.data.domain.Sort.Direction
|
||||
import org.springframework.data.mongodb.core.ReactiveMongoTemplate
|
||||
import org.springframework.data.mongodb.core.aggregation.Aggregation.addFields
|
||||
import org.springframework.data.mongodb.core.aggregation.Aggregation.lookup
|
||||
import org.springframework.data.mongodb.core.aggregation.Aggregation.match
|
||||
import org.springframework.data.mongodb.core.aggregation.Aggregation.newAggregation
|
||||
import org.springframework.data.mongodb.core.aggregation.Aggregation.sort
|
||||
import org.springframework.data.mongodb.core.aggregation.Aggregation.unwind
|
||||
import org.springframework.data.mongodb.core.aggregation.AggregationOperation
|
||||
import org.springframework.data.mongodb.core.aggregation.ConvertOperators
|
||||
import org.springframework.data.mongodb.core.query.Criteria
|
||||
import org.springframework.stereotype.Service
|
||||
import space.luminic.finance.dtos.TransactionDTO
|
||||
import space.luminic.finance.models.NotFoundException
|
||||
import space.luminic.finance.models.Transaction
|
||||
import space.luminic.finance.repos.TransactionRepo
|
||||
|
||||
@Service
|
||||
class TransactionServiceImpl(
|
||||
private val mongoTemplate: ReactiveMongoTemplate,
|
||||
private val transactionRepo: TransactionRepo,
|
||||
private val categoryService: CategoryService,
|
||||
) : TransactionService {
|
||||
|
||||
|
||||
private fun basicAggregation(spaceId: String): List<AggregationOperation> {
|
||||
val addFieldsOI = addFields()
|
||||
.addField("createdByOI")
|
||||
.withValue(ConvertOperators.valueOf("createdById").convertToObjectId())
|
||||
.addField("updatedByOI")
|
||||
.withValue(ConvertOperators.valueOf("updatedById").convertToObjectId())
|
||||
.addField("fromAccountIdOI")
|
||||
.withValue(ConvertOperators.valueOf("fromAccountId").convertToObjectId())
|
||||
.addField("toAccountIdOI")
|
||||
.withValue(ConvertOperators.valueOf("toAccountId").convertToObjectId())
|
||||
.addField("categoryIdOI")
|
||||
.withValue(ConvertOperators.valueOf("categoryId").convertToObjectId())
|
||||
.build()
|
||||
|
||||
val lookupFromAccount = lookup("accounts", "fromAccountIdOI", "_id", "fromAccount")
|
||||
val unwindFromAccount = unwind("fromAccount")
|
||||
val lookupToAccount = lookup("accounts", "toAccountIdOI", "_id", "toAccount")
|
||||
val unwindToAccount = unwind("toAccount", true)
|
||||
|
||||
val lookupCategory = lookup("categories", "categoryIdOI", "_id", "category")
|
||||
val unwindCategory = unwind("category")
|
||||
|
||||
val lookupCreatedBy = lookup("users", "createdByOI", "_id", "createdBy")
|
||||
val unwindCreatedBy = unwind("createdBy")
|
||||
|
||||
val lookupUpdatedBy = lookup("users", "updatedByOI", "_id", "updatedBy")
|
||||
val unwindUpdatedBy = unwind("updatedBy")
|
||||
val matchCriteria = mutableListOf<Criteria>()
|
||||
matchCriteria.add(Criteria.where("spaceId").`is`(spaceId))
|
||||
val matchStage = match(Criteria().andOperator(*matchCriteria.toTypedArray()))
|
||||
|
||||
return listOf(
|
||||
matchStage,
|
||||
addFieldsOI,
|
||||
lookupFromAccount,
|
||||
unwindFromAccount,
|
||||
lookupToAccount,
|
||||
unwindToAccount,
|
||||
lookupCategory,
|
||||
unwindCategory,
|
||||
lookupCreatedBy,
|
||||
unwindCreatedBy,
|
||||
lookupUpdatedBy,
|
||||
unwindUpdatedBy
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getTransactions(
|
||||
spaceId: String,
|
||||
filter: TransactionService.TransactionsFilter,
|
||||
sortBy: String,
|
||||
sortDirection: String
|
||||
): List<Transaction> {
|
||||
val allowedSortFields = setOf("date", "amount", "category.name", "createdAt")
|
||||
require(sortBy in allowedSortFields) { "Invalid sort field: $sortBy" }
|
||||
|
||||
val direction = when (sortDirection.uppercase()) {
|
||||
"ASC" -> Direction.ASC
|
||||
"DESC" -> Direction.DESC
|
||||
else -> throw IllegalArgumentException("Sort direction must be 'ASC' or 'DESC'")
|
||||
}
|
||||
val basicAggregation = basicAggregation(spaceId)
|
||||
|
||||
val sort = sort(Sort.by(direction, sortBy))
|
||||
val matchCriteria = mutableListOf<Criteria>()
|
||||
filter.dateFrom?.let { matchCriteria.add(Criteria.where("date").gte(it)) }
|
||||
filter.dateTo?.let { matchCriteria.add(Criteria.where("date").lte(it)) }
|
||||
val matchStage = match(Criteria().andOperator(*matchCriteria.toTypedArray()))
|
||||
val aggregation =
|
||||
newAggregation(
|
||||
matchStage,
|
||||
*basicAggregation.toTypedArray(),
|
||||
sort
|
||||
)
|
||||
|
||||
return mongoTemplate.aggregate(aggregation, "transactions", Transaction::class.java)
|
||||
.collectList()
|
||||
.awaitSingle()
|
||||
}
|
||||
|
||||
override suspend fun getTransaction(
|
||||
spaceId: String,
|
||||
transactionId: String
|
||||
): Transaction {
|
||||
val matchCriteria = mutableListOf<Criteria>()
|
||||
matchCriteria.add(Criteria.where("spaceId").`is`(spaceId))
|
||||
matchCriteria.add(Criteria.where("_id").`is`(ObjectId(transactionId)))
|
||||
val matchStage = match(Criteria().andOperator(*matchCriteria.toTypedArray()))
|
||||
|
||||
val aggregation =
|
||||
newAggregation(
|
||||
matchStage,
|
||||
)
|
||||
return mongoTemplate.aggregate(aggregation, "transactions", Transaction::class.java)
|
||||
.awaitFirstOrNull() ?: throw NotFoundException("Transaction with ID $transactionId not found")
|
||||
}
|
||||
|
||||
override suspend fun createTransaction(
|
||||
spaceId: String,
|
||||
transaction: TransactionDTO.CreateTransactionDTO
|
||||
): Transaction {
|
||||
if (transaction.type == Transaction.TransactionType.TRANSFER && transaction.toAccountId == null) {
|
||||
throw IllegalArgumentException("Cannot create a transaction with type TRANSFER without a toAccountId")
|
||||
}
|
||||
val category = categoryService.getCategory(spaceId, transaction.categoryId)
|
||||
if (transaction.type != Transaction.TransactionType.TRANSFER && transaction.type.name != category.type.name) {
|
||||
throw IllegalArgumentException("Transaction type should match with category type")
|
||||
}
|
||||
val transaction = Transaction(
|
||||
spaceId = spaceId,
|
||||
type = transaction.type,
|
||||
kind = transaction.kind,
|
||||
categoryId = transaction.categoryId,
|
||||
comment = transaction.comment,
|
||||
amount = transaction.amount,
|
||||
fees = transaction.fees,
|
||||
date = transaction.date,
|
||||
fromAccountId = transaction.fromAccountId,
|
||||
toAccountId = transaction.toAccountId,
|
||||
)
|
||||
return transactionRepo.save(transaction).awaitSingle()
|
||||
}
|
||||
|
||||
override suspend fun updateTransaction(
|
||||
spaceId: String,
|
||||
transaction: TransactionDTO.UpdateTransactionDTO
|
||||
): Transaction {
|
||||
if (transaction.type == Transaction.TransactionType.TRANSFER && transaction.toAccountId == null) {
|
||||
throw IllegalArgumentException("Cannot edit a transaction with type TRANSFER without a toAccountId")
|
||||
}
|
||||
val exitingTx = getTransaction(spaceId, transaction.id)
|
||||
val transaction = exitingTx.copy(
|
||||
spaceId = exitingTx.spaceId,
|
||||
type = transaction.type,
|
||||
kind = transaction.kind,
|
||||
categoryId = transaction.category,
|
||||
comment = transaction.comment,
|
||||
amount = transaction.amount,
|
||||
fees = transaction.fees,
|
||||
date = transaction.date,
|
||||
fromAccountId = transaction.fromAccountId,
|
||||
toAccountId = transaction.toAccountId,
|
||||
)
|
||||
return transactionRepo.save(transaction).awaitSingle()
|
||||
}
|
||||
|
||||
override suspend fun deleteTransaction(spaceId: String, transactionId: String) {
|
||||
val transaction = getTransaction(spaceId, transactionId)
|
||||
transaction.isDeleted = true
|
||||
transactionRepo.save(transaction).awaitSingle()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package space.luminic.finance.services
|
||||
|
||||
|
||||
import kotlinx.coroutines.reactor.awaitSingle
|
||||
import kotlinx.coroutines.reactor.awaitSingleOrNull
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.cache.annotation.Cacheable
|
||||
import org.springframework.stereotype.Service
|
||||
import space.luminic.finance.mappers.UserMapper
|
||||
import space.luminic.finance.models.NotFoundException
|
||||
import space.luminic.finance.models.User
|
||||
import space.luminic.finance.repos.UserRepo
|
||||
|
||||
@Service
|
||||
class UserService(val userRepo: UserRepo) {
|
||||
val logger = LoggerFactory.getLogger(javaClass)
|
||||
|
||||
|
||||
@Cacheable("users", key = "#username")
|
||||
suspend fun getByUsername(username: String): User {
|
||||
return userRepo.findByUsername(username).awaitSingleOrNull()
|
||||
?: throw NotFoundException("User with username: $username not found")
|
||||
|
||||
}
|
||||
|
||||
suspend fun getById(id: String): User {
|
||||
return userRepo.findById(id).awaitSingleOrNull()
|
||||
?: throw NotFoundException("User with id: $id not found")
|
||||
}
|
||||
|
||||
suspend fun getUserByTelegramId(telegramId: Long): User {
|
||||
return userRepo.findByTgId(telegramId.toString()).awaitSingleOrNull()
|
||||
?: throw NotFoundException("User with telegramId: $telegramId not found")
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Cacheable("users", key = "#username")
|
||||
suspend fun getByUserNameWoPass(username: String): User {
|
||||
return userRepo.findByUsernameWOPassword(username).awaitSingleOrNull()
|
||||
?: throw NotFoundException("User with username: $username not found")
|
||||
|
||||
}
|
||||
|
||||
@Cacheable("usersList")
|
||||
suspend fun getUsers(): List<User> {
|
||||
return userRepo.findAll()
|
||||
.collectList()
|
||||
.awaitSingle()
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user