This commit is contained in:
xds
2025-10-31 15:31:55 +03:00
parent 040da34ff7
commit 7972ea0fdf
117 changed files with 3691 additions and 2013 deletions

View File

@@ -1,14 +0,0 @@
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)
}

View File

@@ -1,119 +0,0 @@
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()
}
}

View File

@@ -1,10 +1,7 @@
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.context.SecurityContextHolder
import org.springframework.security.core.userdetails.UsernameNotFoundException
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.stereotype.Service
@@ -13,14 +10,13 @@ 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.time.Instant
import java.util.*
@Service
class AuthService(
private val userRepository: UserRepo,
private val userRepo: UserRepo,
private val tokenService: TokenService,
private val jwtUtil: JWTUtil,
private val userService: UserService,
@@ -28,18 +24,28 @@ class AuthService(
) {
private val passwordEncoder = BCryptPasswordEncoder()
suspend fun getSecurityUser(): User {
val securityContextHolder = ReactiveSecurityContextHolder.getContext().awaitSingleOrNull()
fun getSecurityUser(): User {
val securityContextHolder = SecurityContextHolder.getContext()
?: throw AuthException("Authentication failed")
val authentication = securityContextHolder.authentication
val username = authentication.name
// Получаем пользователя по имени
return userService.getByUsername(username)
return userService.getById(username.toInt())
}
suspend fun login(username: String, password: String): String {
val user = userRepository.findByUsername(username).awaitFirstOrNull()
fun getSecurityUserId(): Int {
val securityContextHolder = SecurityContextHolder.getContext()
?: throw AuthException("Authentication failed")
val authentication = securityContextHolder.authentication
val username = authentication.name
// Получаем пользователя по имени
return username.toInt()
}
fun login(username: String, password: String): String {
val user = userRepo.findByUsername(username)
?: throw UsernameNotFoundException("Пользователь не найден")
return if (passwordEncoder.matches(password, user.password)) {
val token = jwtUtil.generateToken(user.username)
@@ -47,10 +53,7 @@ class AuthService(
tokenService.saveToken(
token = token,
username = username,
expiresAt = LocalDateTime.ofInstant(
expireAt.toInstant(),
ZoneId.systemDefault()
)
expiresAt = expireAt.toInstant()
)
token
} else {
@@ -58,26 +61,23 @@ class AuthService(
}
}
suspend fun tgLogin(tgId: String): String {
fun tgLogin(tgId: String): String {
val user =
userRepository.findByTgId(tgId).awaitSingleOrNull() ?: throw UsernameNotFoundException("Пользователь не найден")
userRepo.findByTgId(tgId) ?: 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()
)
expiresAt = expireAt.toInstant()
)
return token
}
suspend fun register(username: String, password: String, firstName: String): User {
val user = userRepository.findByUsername(username).awaitSingleOrNull()
fun register(username: String, password: String, firstName: String): User {
val user = userRepo.findByUsername(username)
if (user == null) {
var newUser = User(
username = username,
@@ -85,18 +85,18 @@ class AuthService(
firstName = firstName,
roles = mutableListOf("USER")
)
newUser = userRepository.save(newUser).awaitSingle()
newUser = userRepo.save(newUser)
return newUser
} else throw IllegalArgumentException("Пользователь уже зарегистрирован")
}
@Cacheable(cacheNames = ["tokens"], key = "#token")
suspend fun isTokenValid(token: String): User {
val tokenDetails = tokenService.getToken(token).awaitFirstOrNull() ?: throw AuthException("Токен не валиден")
fun isTokenValid(token: String): User {
val tokenDetails = tokenService.getToken(token)
when {
tokenDetails.status == Token.TokenStatus.ACTIVE && tokenDetails.expiresAt.isAfter(LocalDateTime.now()) -> {
return userService.getByUsername(tokenDetails.username)
tokenDetails.status == Token.TokenStatus.ACTIVE && tokenDetails.expiresAt.isAfter(Instant.now()) -> {
return tokenDetails.user
}
else -> {

View File

@@ -1,18 +0,0 @@
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)
}

View File

@@ -1,188 +0,0 @@
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()
}
}

View File

@@ -1,15 +1,13 @@
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>
fun getCategories(spaceId: Int): List<Category>
fun getCategory(spaceId: Int, id: Int): Category
fun createCategory(spaceId: Int, category: CategoryDTO.CreateCategoryDTO): Category
fun createEtalonCategoriesForSpace(spaceId: Int): List<Category>
fun updateCategory(spaceId: Int,categoryId:Int, category: CategoryDTO.UpdateCategoryDTO): Category
fun deleteCategory(spaceId: Int, id: Int)
}

View File

@@ -1,108 +1,105 @@
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.jdbc.core.JdbcTemplate
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Propagation
import org.springframework.transaction.annotation.Transactional
import space.luminic.finance.dtos.CategoryDTO
import space.luminic.finance.models.Category
import space.luminic.finance.models.Space
import space.luminic.finance.models.NotFoundException
import space.luminic.finance.repos.CategoryEtalonRepo
import space.luminic.finance.repos.CategoryRepo
import space.luminic.finance.repos.SpaceRepo
@Service
class CategoryServiceImpl(
private val categoryRepo: CategoryRepo,
private val categoryEtalonRepo: CategoryEtalonRepo,
private val reactiveMongoTemplate: ReactiveMongoTemplate,
private val authService: AuthService,
private val spaceRepo: SpaceRepo,
private val categoriesRepo: CategoryRepo,
private val categoriesEtalonRepo: CategoryEtalonRepo,
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 fun getCategories(spaceId: Int): List<Category> {
val userId = authService.getSecurityUserId()
val space = spaceRepo.findSpaceById(spaceId, userId)
return categoriesRepo.findBySpaceId(spaceId)
}
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 fun getCategory(spaceId: Int, id: Int): Category {
val userId = authService.getSecurityUserId()
val space = spaceRepo.findSpaceById(spaceId, userId)
return categoriesRepo.findBySpaceIdAndId(spaceId, id)
?: throw NotFoundException("Category with id $id not found")
}
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,
@Transactional
override fun createCategory(
spaceId: Int,
category: CategoryDTO.CreateCategoryDTO
): Category {
val createdCategory = Category(
spaceId = spaceId,
type = category.type,
val userId = authService.getSecurityUserId()
val space = spaceRepo.findSpaceById(spaceId, userId)
val newCategory = Category(
space = space,
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,
description = category.description,
icon = category.icon,
)
return categoryRepo.save(updatedCategory).awaitSingle()
return categoriesRepo.create(newCategory, userId)
}
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
@Transactional(propagation = Propagation.NESTED)
override fun createEtalonCategoriesForSpace(
spaceId: Int
): List<Category> {
val userId = authService.getSecurityUserId()
val space = spaceRepo.findSpaceById(spaceId, userId)
val categories = categoriesEtalonRepo.findAll()
val newCategories = mutableListOf<Category>()
categories.forEach { category ->
newCategories.add(
categoriesRepo.create(
Category(
space = space,
name = category.name,
description = category.description,
type = category.type,
icon = category.icon,
), userId
)
)
}
return categoryRepo.saveAll(toCreate).collectList().awaitSingle()
return newCategories
}
@Transactional
override fun updateCategory(
spaceId: Int,
categoryId: Int,
category: CategoryDTO.UpdateCategoryDTO
): Category {
val userId = authService.getSecurityUserId()
val space = spaceRepo.findSpaceById(spaceId, userId)
val existingCategory = getCategory(spaceId, categoryId)
val newCategory = Category(
id = existingCategory.id,
space = space,
name = category.name,
description = category.description,
type = category.type,
icon = category.icon,
isDeleted = existingCategory.isDeleted,
createdBy = existingCategory.createdBy,
createdAt = existingCategory.createdAt,
)
return categoriesRepo.update(newCategory, userId)
}
@Transactional
override fun deleteCategory(spaceId: Int, id: Int) {
val userId = authService.getSecurityUserId()
val space = spaceRepo.findSpaceById(spaceId, userId)
categoriesRepo.delete(id)
}

View File

@@ -0,0 +1,108 @@
//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.repos.CategoryEtalonRepo
//import space.luminic.finance.repos.CategoryRepo
//
//@Service
//class CategoryServiceMongoImpl(
// 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()
// }
//
//
//}

View File

@@ -1,16 +0,0 @@
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!!
}
}

View File

@@ -6,11 +6,11 @@ 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
fun getCurrencies(): List<Currency>
fun getCurrency(currencyCode: String): Currency
fun createCurrency(currency: CurrencyDTO): Currency
fun updateCurrency(currency: CurrencyDTO): Currency
fun deleteCurrency(currencyCode: String)
fun createCurrencyRate(currencyCode: String): CurrencyRate
}

View File

@@ -1,11 +1,10 @@
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.models.NotFoundException
import space.luminic.finance.repos.CurrencyRepo
import java.math.BigDecimal
import java.time.LocalDate
@@ -13,39 +12,39 @@ 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 fun getCurrencies(): List<Currency> {
return currencyRepo.findAll()
}
override suspend fun getCurrency(currencyCode: String): Currency {
return currencyRepo.findById(currencyCode).awaitSingle()
override fun getCurrency(currencyCode: String): Currency {
return currencyRepo.findByCode(currencyCode)
?: throw NotFoundException("Currency code $currencyCode not found")
}
override suspend fun createCurrency(currency: CurrencyDTO): Currency {
val createdCurrency = Currency(currency.code, currency.name, currency.symbol)
return currencyRepo.save(createdCurrency).awaitSingle()
override fun createCurrency(currency: CurrencyDTO): Currency {
val currency = Currency(currency.code, currency.name, currency.code)
return currencyRepo.save(currency)
}
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(),
override fun updateCurrency(currency: CurrencyDTO): Currency {
getCurrency(currency.code)
val updatedCurrency =
Currency(
code = currency.code,
name = currency.name,
symbol = currency.symbol
)
).awaitSingle()
return currencyRepo.save(updatedCurrency)
}
override fun deleteCurrency(currencyCode: String) {
currencyRepo.delete(currencyCode)
}
override fun createCurrencyRate(currencyCode: String): CurrencyRate {
print("createCurrencyRate")
val currency = getCurrency(currencyCode)
return CurrencyRate(currency = currency, rate = BigDecimal.ZERO, date = LocalDate.now())
}
}

View File

@@ -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 CurrencyServiceMongoImpl(
// 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()
// }
//}

View File

@@ -0,0 +1,22 @@
package space.luminic.finance.services
import space.luminic.finance.dtos.GoalDTO
import space.luminic.finance.models.Goal
interface GoalService {
fun findAllBySpaceId(spaceId: Int): List<Goal>
fun findBySpaceIdAndId(spaceId: Int, id: Int): Goal
fun create(spaceId: Int,goal: GoalDTO.CreateGoalDTO): Int
fun update(spaceId: Int, goalId: Int, goal: GoalDTO.UpdateGoalDTO)
fun delete(spaceId: Int, id: Int)
fun getComponents(spaceId: Int, goalId: Int): List<Goal.GoalComponent>
fun getComponent(spaceId: Int, goalId: Int, id: Int): Goal.GoalComponent?
fun createComponent(spaceId: Int, goalId: Int, component: Goal.GoalComponent): Int
fun updateComponent(spaceId: Int, goalId: Int, component: Goal.GoalComponent)
fun deleteComponent(spaceId: Int, goalId: Int, id: Int)
fun assignTransaction(spaceId: Int, goalId: Int, transactionId: Int)
fun refuseTransaction(spaceId: Int,goalId: Int, transactionId: Int)
}

View File

@@ -0,0 +1,140 @@
package space.luminic.finance.services
import org.springframework.stereotype.Service
import space.luminic.finance.dtos.GoalDTO
import space.luminic.finance.models.Goal
import space.luminic.finance.models.NotFoundException
import space.luminic.finance.repos.GoalRepo
import space.luminic.finance.repos.SpaceRepo
import space.luminic.finance.repos.TransactionRepo
@Service
class GoalServiceImpl(
private val goalRepo: GoalRepo,
private val spaceRepo: SpaceRepo,
private val authService: AuthService,
private val transactionRepo: TransactionRepo
) : GoalService {
override fun findAllBySpaceId(spaceId: Int): List<Goal> {
val userId = authService.getSecurityUserId()
spaceRepo.findSpaceById(spaceId, userId)
return goalRepo.findAllBySpaceId(spaceId)
}
override fun findBySpaceIdAndId(spaceId: Int, id: Int): Goal {
val userId = authService.getSecurityUserId()
spaceRepo.findSpaceById(spaceId, userId)
return goalRepo.findBySpaceIdAndId(spaceId, userId) ?: throw NotFoundException("Goal $id not found")
}
override fun create(spaceId: Int, goal: GoalDTO.CreateGoalDTO): Int {
val userId = authService.getSecurityUserId()
spaceRepo.findSpaceById(spaceId, userId)
val creatingGoal = Goal(
type = goal.type,
name = goal.name,
amount = goal.amount,
untilDate = goal.date
)
return goalRepo.create(creatingGoal, userId)
}
override fun update(spaceId: Int, goalId: Int, goal: GoalDTO.UpdateGoalDTO) {
val userId = authService.getSecurityUserId()
spaceRepo.findSpaceById(spaceId, userId)
val existingGoal =
goalRepo.findBySpaceIdAndId(spaceId, goalId) ?: throw NotFoundException("Goal $goalId not found")
val updatedGoal = existingGoal.copy(
type = goal.type,
name = goal.name,
description = goal.description,
amount = goal.amount,
untilDate = goal.date
)
goalRepo.update(updatedGoal, userId)
}
override fun delete(spaceId: Int, id: Int) {
goalRepo.delete(spaceId, id)
}
override fun getComponents(
spaceId: Int,
goalId: Int
): List<Goal.GoalComponent> {
val userId = authService.getSecurityUserId()
spaceRepo.findSpaceById(spaceId, userId)
goalRepo.findBySpaceIdAndId(spaceId, goalId) ?: throw NotFoundException("Goal $goalId not found")
return goalRepo.getComponents(spaceId, goalId)
}
override fun getComponent(
spaceId: Int,
goalId: Int,
id: Int
): Goal.GoalComponent? {
val userId = authService.getSecurityUserId()
spaceRepo.findSpaceById(spaceId, userId)
goalRepo.findBySpaceIdAndId(spaceId, goalId) ?: throw NotFoundException("Goal $goalId not found")
return goalRepo.getComponent(spaceId, goalId, id)
}
override fun createComponent(
spaceId: Int,
goalId: Int,
component: Goal.GoalComponent
): Int {
val userId = authService.getSecurityUserId()
spaceRepo.findSpaceById(spaceId, userId)
goalRepo.findBySpaceIdAndId(spaceId, goalId) ?: throw NotFoundException("Goal $goalId not found")
return goalRepo.createComponent(goalId, component, userId)
}
override fun updateComponent(
spaceId: Int,
goalId: Int,
component: Goal.GoalComponent
) {
val userId = authService.getSecurityUserId()
spaceRepo.findSpaceById(spaceId, userId)
goalRepo.findBySpaceIdAndId(spaceId, goalId) ?: throw NotFoundException("Goal $goalId not found")
val existingComponent = goalRepo.getComponent(spaceId, goalId, component.id!!)
?: throw NotFoundException("Component $goalId not found")
val updatedComponent = existingComponent.copy(
name = component.name,
amount = component.amount,
isDone = component.isDone,
date = component.date
)
goalRepo.updateComponent(goalId, updatedComponent.id!!, updatedComponent, userId)
}
override fun deleteComponent(spaceId: Int, goalId: Int, id: Int) {
val userId = authService.getSecurityUserId()
spaceRepo.findSpaceById(spaceId, userId)
goalRepo.findBySpaceIdAndId(spaceId, goalId) ?: throw NotFoundException("Goal $goalId not found")
goalRepo.getComponent(spaceId, goalId, id) ?: throw NotFoundException("Component $goalId not found")
goalRepo.deleteComponent(goalId, id)
}
override fun assignTransaction(spaceId: Int, goalId: Int, transactionId: Int) {
val userId = authService.getSecurityUserId()
spaceRepo.findSpaceById(spaceId, userId)
goalRepo.findBySpaceIdAndId(spaceId, goalId) ?: throw NotFoundException("Goal $goalId not found")
transactionRepo.findBySpaceIdAndId(spaceId, transactionId) ?: throw NotFoundException(
"Transaction $transactionId not found"
)
goalRepo.assignTransaction(goalId, transactionId)
}
override fun refuseTransaction(spaceId: Int, goalId: Int, transactionId: Int) {
val userId = authService.getSecurityUserId()
spaceRepo.findSpaceById(spaceId, userId)
goalRepo.findBySpaceIdAndId(spaceId, goalId) ?: throw NotFoundException("Goal $goalId not found")
transactionRepo.findBySpaceIdAndId(spaceId, transactionId) ?: throw NotFoundException(
"Transaction $transactionId not found"
)
goalRepo.refuseTransaction(goalId, transactionId)
}
}

View File

@@ -0,0 +1,13 @@
package space.luminic.finance.services
import space.luminic.finance.dtos.RecurrentOperationDTO
import space.luminic.finance.models.RecurrentOperation
interface RecurrentOperationService {
fun findBySpaceId(spaceId: Int): List<RecurrentOperation>
fun findBySpaceIdAndId(spaceId: Int, id: Int): RecurrentOperation
fun create(spaceId: Int, operation: RecurrentOperationDTO.CreateRecurrentOperationDTO): Int
fun update(spaceId: Int, operationId: Int, operation: RecurrentOperationDTO.UpdateRecurrentOperationDTO)
fun delete(spaceId: Int, id: Int)
}

View File

@@ -0,0 +1,66 @@
package space.luminic.finance.services
import org.springframework.stereotype.Service
import space.luminic.finance.dtos.RecurrentOperationDTO
import space.luminic.finance.models.NotFoundException
import space.luminic.finance.models.RecurrentOperation
import space.luminic.finance.repos.RecurrentOperationRepo
import space.luminic.finance.repos.SpaceRepo
@Service
class RecurrentOperationServiceImpl(
private val authService: AuthService,
private val spaceRepo: SpaceRepo,
private val recurrentOperationRepo: RecurrentOperationRepo,
private val categoryService: CategoryService
): RecurrentOperationService {
override fun findBySpaceId(spaceId: Int): List<RecurrentOperation> {
val userId = authService.getSecurityUserId()
spaceRepo.findSpaceById(spaceId, userId)
return recurrentOperationRepo.findAllBySpaceId(spaceId)
}
override fun findBySpaceIdAndId(
spaceId: Int,
id: Int
): RecurrentOperation {
val userId = authService.getSecurityUserId()
spaceRepo.findSpaceById(spaceId, userId)
return recurrentOperationRepo.findBySpaceIdAndId(spaceId, id) ?: throw NotFoundException("Cannot find recurrent operation with id ${id}")
}
override fun create(spaceId: Int, operation: RecurrentOperationDTO.CreateRecurrentOperationDTO): Int {
val userId = authService.getSecurityUserId()
val space = spaceRepo.findSpaceById(spaceId, userId) ?: throw NotFoundException("Cannot find space with id ${spaceId}")
val category = categoryService.getCategory(spaceId, operation.categoryId)
val creatingOperation = RecurrentOperation(
space = space,
category = category,
name = operation.name,
amount = operation.amount,
date = operation.date
)
return recurrentOperationRepo.create(creatingOperation, userId)
}
override fun update(spaceId: Int, operationId: Int, operation: RecurrentOperationDTO.UpdateRecurrentOperationDTO) {
val userId = authService.getSecurityUserId()
spaceRepo.findSpaceById(spaceId, userId)
val newCategory = categoryService.getCategory(spaceId, operation.categoryId)
val existingOperation = recurrentOperationRepo.findBySpaceIdAndId(spaceId,operationId ) ?: throw NotFoundException("Cannot find operation with id $operationId")
val updatedOperation = existingOperation.copy(
category = newCategory,
name = operation.name,
amount = operation.amount,
date = operation.date
)
recurrentOperationRepo.update(updatedOperation, userId)
}
override fun delete(spaceId: Int, id: Int) {
val userId = authService.getSecurityUserId()
spaceRepo.findSpaceById(spaceId, userId)
recurrentOperationRepo.delete(id)
}
}

View File

@@ -1,16 +1,15 @@
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)
fun checkSpace(spaceId: Int): Space
fun getSpaces(): List<Space>
fun getSpace(id: Int): Space
fun createSpace(space: SpaceDTO.CreateSpaceDTO): Int
fun updateSpace(spaceId: Int, space: SpaceDTO.UpdateSpaceDTO): Int
fun deleteSpace(spaceId: Int)
}

View File

@@ -1,149 +1,75 @@
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.cache.annotation.CacheEvict
import org.springframework.cache.annotation.Cacheable
import org.springframework.jdbc.core.JdbcTemplate
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
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,
private val categoryService: CategoryService
) : 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)
override fun checkSpace(spaceId: Int): Space {
return getSpace(spaceId)
}
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)
// @Cacheable(cacheNames = ["spaces"])
override fun getSpaces(): List<Space> {
val user = authService.getSecurityUserId()
val spaces = spaceRepo.findSpacesAvailableForUser(user)
return spaces
}
override suspend fun checkSpace(spaceId: String): Space {
override fun getSpace(id: Int): Space {
val user = authService.getSecurityUserId()
val space = spaceRepo.findSpaceById(id, user) ?: throw NotFoundException("Space with id $id not found")
return space
}
@Transactional
override fun createSpace(space: SpaceDTO.CreateSpaceDTO): Int {
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(
val creatingSpace = 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()
owner = user,
participants = setOf(user)
)
val userId = authService.getSecurityUserId()
val savedSpace = spaceRepo.create(creatingSpace, userId)
if (space.createBasicCategories) {
categoryService.createEtalonCategoriesForSpace(savedSpace)
}
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(
@Transactional
override fun updateSpace(
spaceId: Int,
space: SpaceDTO.UpdateSpaceDTO
): Int {
val userId = authService.getSecurityUserId()
val existingSpace = getSpace(spaceId)
val updatedSpace = Space(
id = existingSpace.id,
name = space.name,
)
updatedSpace.owner = existingSpace.owner
updatedSpace.participants = existingSpace.participants
return spaceRepo.save(updatedSpace).awaitFirst()
owner = existingSpace.owner,
participants = existingSpace.participants,
isDeleted = existingSpace.isDeleted,
createdBy = existingSpace.createdBy,
createdAt = existingSpace.createdAt,
)
return spaceRepo.update(updatedSpace, userId)
}
override suspend fun deleteSpace(spaceId: String) {
val space = spaceRepo.findById(spaceId).awaitFirstOrNull() ?: throw NotFoundException("Space not found")
space.isDeleted = true
spaceRepo.save(space).awaitFirst()
@Transactional
override fun deleteSpace(spaceId: Int) {
spaceRepo.delete(spaceId)
}
}

View File

@@ -0,0 +1,145 @@
//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 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.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.NotFoundException
//import space.luminic.finance.models.Space
//import space.luminic.finance.models.User
//import space.luminic.finance.repos.SpaceRepo
//
//@Service
//class SpaceServiceMongoImpl(
// 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()
// }
//}

View File

@@ -1,113 +1,111 @@
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")
}
}
}
//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.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.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")
// }
// }
//}

View File

@@ -1,43 +1,51 @@
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.configs.AuthException
import space.luminic.finance.models.Token
import space.luminic.finance.models.Token.TokenStatus
import space.luminic.finance.repos.TokenRepo
import java.time.LocalDateTime
import java.time.Instant
@Service
class TokenService(private val tokenRepository: TokenRepo) {
class TokenService(
private val userService: UserService,
private val tokenRepo: TokenRepo) {
@CacheEvict("tokens", allEntries = true)
suspend fun saveToken(token: String, username: String, expiresAt: LocalDateTime): Token {
fun saveToken(token: String, username: String, expiresAt: Instant): Token {
val user = userService.getByUsername(username)
val newToken = Token(
token = token,
username = username,
issuedAt = LocalDateTime.now(),
user = user,
issuedAt = Instant.now(),
expiresAt = expiresAt
)
return tokenRepository.save(newToken).awaitSingle()
return tokenRepo.create(newToken)
}
fun getToken(token: String): Mono<Token> {
return tokenRepository.findByToken(token)
fun getToken(token: String): Token {
return tokenRepo.findByToken(token) ?: throw AuthException("Токен не валиден")
}
fun revokeToken(token: String) {
val tokenDetail =
tokenRepository.findByToken(token).block()!!
val updatedToken = tokenDetail.copy(status = TokenStatus.REVOKED)
tokenRepository.save(updatedToken).block()
val tokenDetail = getToken(token)
val updatedToken = Token(
id = tokenDetail.id,
token = tokenDetail.token,
user = tokenDetail.user,
status = TokenStatus.REVOKED,
issuedAt = tokenDetail.issuedAt,
expiresAt = tokenDetail.expiresAt
)
tokenRepo.update(updatedToken)
}
@CacheEvict("tokens", allEntries = true)
fun deleteExpiredTokens() {
tokenRepository.deleteByExpiresAtBefore(LocalDateTime.now())
tokenRepo.deleteByExpiresAtBefore(Instant.now())
}
}

View File

@@ -7,14 +7,13 @@ 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)
fun getTransactions(spaceId: Int, filter: TransactionsFilter, sortBy: String, sortDirection: String): List<Transaction>
fun getTransaction(spaceId: Int, transactionId: Int): Transaction
fun createTransaction(spaceId: Int, transaction: TransactionDTO.CreateTransactionDTO): Int
fun updateTransaction(spaceId: Int, transactionId: Int, transaction: TransactionDTO.UpdateTransactionDTO): Int
fun deleteTransaction(spaceId: Int, transactionId: Int)
}

View File

@@ -1,20 +1,5 @@
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
@@ -23,163 +8,89 @@ import space.luminic.finance.repos.TransactionRepo
@Service
class TransactionServiceImpl(
private val mongoTemplate: ReactiveMongoTemplate,
private val transactionRepo: TransactionRepo,
private val spaceService: SpaceService,
private val categoryService: CategoryService,
private val transactionRepo: TransactionRepo,
private val authService: AuthService,
) : 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,
override fun getTransactions(
spaceId: Int,
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()
return transactionRepo.findAllBySpaceId(spaceId)
}
override suspend fun getTransaction(
spaceId: String,
transactionId: String
override fun getTransaction(
spaceId: Int,
transactionId: Int
): 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")
spaceService.getSpace(spaceId)
return transactionRepo.findBySpaceIdAndId(spaceId, transactionId)
?: throw NotFoundException("Transaction with id $transactionId not found")
}
override suspend fun createTransaction(
spaceId: String,
override fun createTransaction(
spaceId: Int,
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")
}
): Int {
val userId = authService.getSecurityUserId()
val space = spaceService.getSpace(spaceId)
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,
space = space,
type = transaction.type,
kind = transaction.kind,
categoryId = transaction.categoryId,
category = category,
comment = transaction.comment,
amount = transaction.amount,
fees = transaction.fees,
date = transaction.date,
fromAccountId = transaction.fromAccountId,
toAccountId = transaction.toAccountId,
)
return transactionRepo.save(transaction).awaitSingle()
return transactionRepo.create(transaction, userId)
}
override suspend fun updateTransaction(
spaceId: String,
override fun updateTransaction(
spaceId: Int,
transactionId: Int,
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,
): Int {
val space = spaceService.getSpace(spaceId)
val existingTransaction = getTransaction(space.id!!, transactionId)
val newCategory = categoryService.getCategory(spaceId, transaction.categoryId)
// val id: Int,
// val type: TransactionType = TransactionType.EXPENSE,
// val kind: TransactionKind = TransactionKind.INSTANT,
// val category: Int,
// val comment: String,
// val amount: BigDecimal,
// val fees: BigDecimal = BigDecimal.ZERO,
// val date: Instant
val updatedTransaction = Transaction(
id = existingTransaction.id,
space = existingTransaction.space,
parent = existingTransaction.parent,
type = transaction.type,
kind = transaction.kind,
categoryId = transaction.category,
category = newCategory,
comment = transaction.comment,
amount = transaction.amount,
fees = transaction.fees,
date = transaction.date,
fromAccountId = transaction.fromAccountId,
toAccountId = transaction.toAccountId,
isDeleted = existingTransaction.isDeleted,
isDone = transaction.isDone,
createdBy = existingTransaction.createdBy,
createdAt = existingTransaction.createdAt
)
return transactionRepo.save(transaction).awaitSingle()
return transactionRepo.update(updatedTransaction)
}
override suspend fun deleteTransaction(spaceId: String, transactionId: String) {
val transaction = getTransaction(spaceId, transactionId)
transaction.isDeleted = true
transactionRepo.save(transaction).awaitSingle()
override fun deleteTransaction(spaceId: Int, transactionId: Int) {
val space = spaceService.getSpace(spaceId)
getTransaction(space.id!!, transactionId)
transactionRepo.delete(transactionId)
}
}

View File

@@ -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 TransactionServiceMongoImpl(
// 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()
// }
//
//
//}

View File

@@ -1,12 +1,9 @@
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
@@ -17,36 +14,30 @@ class UserService(val userRepo: UserRepo) {
@Cacheable("users", key = "#username")
suspend fun getByUsername(username: String): User {
return userRepo.findByUsername(username).awaitSingleOrNull()
?: throw NotFoundException("User with username: $username not found")
fun getByUsername(username: String): User {
return userRepo.findByUsername(username) ?: 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")
fun getById(id: Int): User {
return userRepo.findById(id) ?: 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")
fun getUserByTelegramId(telegramId: Long): User {
return userRepo.findByTgId(telegramId.toString())?: 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")
fun getByUserNameWoPass(username: String): User {
return userRepo.findByUsername(username) ?: throw NotFoundException("User with username: $username not found")
}
@Cacheable("usersList")
suspend fun getUsers(): List<User> {
fun getUsers(): List<User> {
return userRepo.findAll()
.collectList()
.awaitSingle()
}
}