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,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)
}
}