filters for transactions;

update transactions when recurrent updated
This commit is contained in:
xds
2025-11-18 00:34:02 +03:00
parent 42cbf30bd8
commit 195bdd83f0
16 changed files with 162 additions and 526 deletions

View File

@@ -21,9 +21,9 @@ class TransactionController (
){ ){
@GetMapping @PostMapping("/_search")
fun getTransactions(@PathVariable spaceId: Int) : List<TransactionDTO>{ fun getTransactions(@PathVariable spaceId: Int, @RequestBody filter: TransactionService.TransactionsFilter) : List<TransactionDTO>{
return transactionService.getTransactions(spaceId, TransactionService.TransactionsFilter(),"date", "DESC").map { it.toDto() } return transactionService.getTransactions(spaceId, filter,"date", "DESC").map { it.toDto() }
} }
@GetMapping("/{transactionId}") @GetMapping("/{transactionId}")

View File

@@ -15,7 +15,7 @@ data class Transaction(
val type: TransactionType = TransactionType.EXPENSE, val type: TransactionType = TransactionType.EXPENSE,
val kind: TransactionKind = TransactionKind.INSTANT, val kind: TransactionKind = TransactionKind.INSTANT,
val category: Category? = null, val category: Category? = null,
val comment: String, var comment: String,
val amount: BigDecimal, val amount: BigDecimal,
val fees: BigDecimal = BigDecimal.ZERO, val fees: BigDecimal = BigDecimal.ZERO,
val date: LocalDate = LocalDate.now(), val date: LocalDate = LocalDate.now(),

View File

@@ -74,7 +74,7 @@ class RecurrentOperationRepoImpl(
join finance.categories c on ro.category_id = c.id join finance.categories c on ro.category_id = c.id
join finance.users r_created_by on ro.created_by_id = r_created_by.id join finance.users r_created_by on ro.created_by_id = r_created_by.id
where ro.space_id = :spaceId where ro.space_id = :spaceId
order by ro.date order by ro.date, ro.id
""".trimIndent() """.trimIndent()
val params = mapOf("spaceId" to spaceId) val params = mapOf("spaceId" to spaceId)
return jdbcTemplate.query(sql, params, operationRowMapper()) return jdbcTemplate.query(sql, params, operationRowMapper())

View File

@@ -1,13 +1,16 @@
package space.luminic.finance.repos package space.luminic.finance.repos
import space.luminic.finance.models.Transaction import space.luminic.finance.models.Transaction
import space.luminic.finance.services.TransactionService
interface TransactionRepo { interface TransactionRepo {
fun findAllBySpaceId(spaceId: Int): List<Transaction> fun findAllBySpaceId(spaceId: Int, filters: TransactionService.TransactionsFilter): List<Transaction>
fun findBySpaceIdAndId(spaceId: Int, id: Int): Transaction? fun findBySpaceIdAndId(spaceId: Int, id: Int): Transaction?
fun findBySpaceIdAndRecurrentId(spaceId: Int, recurrentId: Int): List<Transaction>
fun create(transaction: Transaction, userId: Int): Int fun create(transaction: Transaction, userId: Int): Int
fun createBatch(transactions: List<Transaction>, userId: Int) fun createBatch(transactions: List<Transaction>, userId: Int)
fun update(transaction: Transaction): Int fun update(transaction: Transaction): Int
fun updateBatch(transactions: List<Transaction>, userId: Int)
fun delete(transactionId: Int) fun delete(transactionId: Int)
fun deleteByRecurrentId(spaceId: Int, recurrentId: Int) fun deleteByRecurrentId(spaceId: Int, recurrentId: Int)

View File

@@ -6,6 +6,7 @@ import org.springframework.stereotype.Repository
import space.luminic.finance.models.Category import space.luminic.finance.models.Category
import space.luminic.finance.models.Transaction import space.luminic.finance.models.Transaction
import space.luminic.finance.models.User import space.luminic.finance.models.User
import space.luminic.finance.services.TransactionService
@Repository @Repository
class TransactionRepoImpl( class TransactionRepoImpl(
@@ -52,8 +53,8 @@ class TransactionRepoImpl(
) )
} }
override fun findAllBySpaceId(spaceId: Int): List<Transaction> { override fun findAllBySpaceId(spaceId: Int, filters: TransactionService.TransactionsFilter): List<Transaction> {
val sql = """ var sql = """
SELECT SELECT
t.id AS t_id, t.id AS t_id,
t.parent_id AS t_parent_id, t.parent_id AS t_parent_id,
@@ -86,11 +87,39 @@ class TransactionRepoImpl(
LEFT JOIN finance.categories c ON t.category_id = c.id LEFT JOIN finance.categories c ON t.category_id = c.id
JOIN finance.users u ON u.id = t.created_by_id JOIN finance.users u ON u.id = t.created_by_id
WHERE t.space_id = :spaceId and t.is_deleted = false WHERE t.space_id = :spaceId and t.is_deleted = false
ORDER BY t.date, t.id
""".trimIndent() """.trimIndent()
val params = mapOf( val params = mutableMapOf<String, Any?>(
"spaceId" to spaceId, "spaceId" to spaceId,
"offset" to filters.offset,
"limit" to filters.limit,
) )
filters.type?.let {
sql += " AND t.type = :type"
params.put("type", it.name)
}
filters.kind?.let {
sql += " AND t.kind = :kind"
params.put("kind", it.name)
}
filters.isDone?.let {
sql += " AND t.is_done = :isDone"
params.put("isDone", it)
}
filters.dateFrom?.let {
sql += " AND t.date >= :dateFrom"
params.put("dateFrom", it)
}
filters.dateTo?.let {
sql += " AND t.date <= :dateTo"
params.put("dateTo", it)
}
sql += """
ORDER BY t.date, t.id
OFFSET :offset ROWS
FETCH FIRST :limit ROWS ONLY"""
return jdbcTemplate.query(sql, params, transactionRowMapper()) return jdbcTemplate.query(sql, params, transactionRowMapper())
} }
@@ -134,6 +163,49 @@ class TransactionRepoImpl(
return jdbcTemplate.query(sql, params, transactionRowMapper()).firstOrNull() return jdbcTemplate.query(sql, params, transactionRowMapper()).firstOrNull()
} }
override fun findBySpaceIdAndRecurrentId(
spaceId: Int,
recurrentId: Int
): List<Transaction> {
val sql = """SELECT
t.id AS t_id,
t.parent_id AS t_parent_id,
t.space_id AS t_space_id,
t.type AS t_type,
t.kind AS t_kind,
t.comment AS t_comment,
t.amount AS t_amount,
t.fees AS t_fees,
t.date AS t_date,
t.is_deleted AS t_is_deleted,
t.is_done AS t_is_done,
t.created_at AS t_created_at,
t.updated_at AS t_updated_at,
t.tg_chat_id AS tg_chat_id,
t.tg_message_id AS tg_message_id,
c.id AS c_id,
c.type AS c_type,
c.name AS c_name,
c.description AS c_description,
c.icon AS c_icon,
c.is_deleted AS c_is_deleted,
c.created_at AS c_created_at,
c.updated_at AS c_updated_at,
u.id AS u_id,
u.username AS u_username,
u.first_name AS u_first_name,
t.recurrent_id AS t_recurrent_id
FROM finance.transactions t
LEFT JOIN finance.categories c ON t.category_id = c.id
JOIN finance.users u ON u.id = t.created_by_id
WHERE t.space_id = :spaceId and t.recurrent_id = :recurrentId and t.is_deleted = false""".trimMargin()
val params = mapOf(
"spaceId" to spaceId,
"recurrentId" to recurrentId,
)
return jdbcTemplate.query(sql, params, transactionRowMapper())
}
override fun create(transaction: Transaction, userId: Int): Int { override fun create(transaction: Transaction, userId: Int): Int {
val sql = """ val sql = """
INSERT INTO finance.transactions (space_id, parent_id, type, kind, category_id, comment, amount, fees, date, is_deleted, is_done, created_by_id, tg_chat_id, tg_message_id, recurrent_id) VALUES ( INSERT INTO finance.transactions (space_id, parent_id, type, kind, category_id, comment, amount, fees, date, is_deleted, is_done, created_by_id, tg_chat_id, tg_message_id, recurrent_id) VALUES (
@@ -211,14 +283,6 @@ class TransactionRepoImpl(
} }
override fun update(transaction: Transaction): Int { override fun update(transaction: Transaction): 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 isDone: Boolean,
// val date: Instant
val sql = """ val sql = """
UPDATE finance.transactions UPDATE finance.transactions
@@ -247,6 +311,39 @@ class TransactionRepoImpl(
return transaction.id!! return transaction.id!!
} }
override fun updateBatch(transactions: List<Transaction>, userId: Int) {
val sql = """
UPDATE finance.transactions
set type = :type,
kind = :kind,
category_id = :categoryId,
comment = :comment,
amount = :amount,
fees = :fees,
is_done = :is_done,
date = :date,
updated_by_id = :updatedById,
updated_at = now()
where id = :id
""".trimIndent()
val batchValues = transactions.map { transaction ->
mapOf(
"id" to transaction.id,
"type" to transaction.type.name,
"kind" to transaction.kind.name,
"categoryId" to transaction.category?.id,
"comment" to transaction.comment,
"amount" to transaction.amount,
"fees" to transaction.fees,
"date" to transaction.date,
"is_done" to transaction.isDone,
"updatedById" to userId,
)
}.toTypedArray()
jdbcTemplate.batchUpdate(sql, batchValues)
}
override fun delete(transactionId: Int) { override fun delete(transactionId: Int) {
val sql = """ val sql = """
update finance.transactions set is_deleted = true where id = :id update finance.transactions set is_deleted = true where id = :id

View File

@@ -1,108 +0,0 @@
//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,51 +0,0 @@
//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

@@ -15,6 +15,7 @@ import space.luminic.finance.repos.RecurrentOperationRepo
import space.luminic.finance.repos.SpaceRepo import space.luminic.finance.repos.SpaceRepo
import space.luminic.finance.repos.TransactionRepo import space.luminic.finance.repos.TransactionRepo
import java.time.LocalDate import java.time.LocalDate
import kotlin.math.min
@Service @Service
class RecurrentOperationServiceImpl( class RecurrentOperationServiceImpl(
@@ -22,7 +23,6 @@ class RecurrentOperationServiceImpl(
private val spaceRepo: SpaceRepo, private val spaceRepo: SpaceRepo,
private val recurrentOperationRepo: RecurrentOperationRepo, private val recurrentOperationRepo: RecurrentOperationRepo,
private val categoryService: CategoryService, private val categoryService: CategoryService,
private val transactionService: TransactionService,
private val transactionRepo: TransactionRepo private val transactionRepo: TransactionRepo
) : RecurrentOperationService { ) : RecurrentOperationService {
private val logger = LoggerFactory.getLogger(this.javaClass) private val logger = LoggerFactory.getLogger(this.javaClass)
@@ -61,7 +61,8 @@ class RecurrentOperationServiceImpl(
val transactionsToCreate = mutableListOf<Transaction>() val transactionsToCreate = mutableListOf<Transaction>()
serviceScope.launch { serviceScope.launch {
runCatching { runCatching {
val date = LocalDate.now() val now = LocalDate.now()
val date = now.withDayOfMonth(min(operation.date, now.lengthOfMonth()))
for (i in 1..12) { for (i in 1..12) {
transactionsToCreate.add( transactionsToCreate.add(
Transaction( Transaction(
@@ -99,7 +100,30 @@ class RecurrentOperationServiceImpl(
date = operation.date date = operation.date
) )
recurrentOperationRepo.update(updatedOperation, userId) recurrentOperationRepo.update(updatedOperation, userId)
serviceScope.launch {
val transactionsToUpdate = mutableListOf<Transaction>()
runCatching {
val txs = transactionRepo.findBySpaceIdAndRecurrentId(spaceId, operationId)
txs.forEach {
transactionsToUpdate.add(
it.copy(
type = if (it.category?.type == Category.CategoryType.EXPENSE) Transaction.TransactionType.EXPENSE else Transaction.TransactionType.INCOME,
category = updatedOperation.category,
comment = operation.name,
date = LocalDate.of(
it.date.year,
it.date.monthValue,
min(it.date.lengthOfMonth(), updatedOperation.date)
)
)
)
}
transactionRepo.updateBatch(transactionsToUpdate, userId)
}.onFailure {
logger.error("Error creating recurring operation", it)
}
}
} }
override fun delete(spaceId: Int, id: Int) { override fun delete(spaceId: Int, id: Int) {

View File

@@ -1,145 +0,0 @@
//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

@@ -7,8 +7,13 @@ import java.time.LocalDate
interface TransactionService { interface TransactionService {
data class TransactionsFilter( data class TransactionsFilter(
val type: Transaction.TransactionType? = null,
val kind: Transaction.TransactionKind? = null,
val dateFrom: LocalDate? = null, val dateFrom: LocalDate? = null,
val dateTo: LocalDate? = null, val dateTo: LocalDate? = null,
val isDone: Boolean? = null,
val offset: Int = 0,
val limit: Int = 10,
) )
fun getTransactions( fun getTransactions(

View File

@@ -21,7 +21,7 @@ class TransactionServiceImpl(
sortBy: String, sortBy: String,
sortDirection: String sortDirection: String
): List<Transaction> { ): List<Transaction> {
val transactions = transactionRepo.findAllBySpaceId(spaceId) val transactions = transactionRepo.findAllBySpaceId(spaceId, filter)
return transactions return transactions
} }
@@ -106,9 +106,10 @@ class TransactionServiceImpl(
tgChatId = existingTransaction.tgChatId, tgChatId = existingTransaction.tgChatId,
tgMessageId = existingTransaction.tgMessageId, tgMessageId = existingTransaction.tgMessageId,
) )
if (existingTransaction.category == null && updatedTransaction.category != null) { if ((existingTransaction.category == null && updatedTransaction.category != null) || (existingTransaction.category?.id != updatedTransaction.category?.id)) {
categorizeService.notifyThatCategorySelected(updatedTransaction) categorizeService.notifyThatCategorySelected(updatedTransaction)
} }
return transactionRepo.update(updatedTransaction) return transactionRepo.update(updatedTransaction)
} }

View File

@@ -1,185 +0,0 @@
//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

@@ -4,7 +4,6 @@ import com.github.kotlintelegrambot.Bot
import com.github.kotlintelegrambot.entities.ChatId import com.github.kotlintelegrambot.entities.ChatId
import com.github.kotlintelegrambot.entities.InlineKeyboardMarkup import com.github.kotlintelegrambot.entities.InlineKeyboardMarkup
import com.github.kotlintelegrambot.entities.Message import com.github.kotlintelegrambot.entities.Message
import com.github.kotlintelegrambot.entities.MessageId
import com.github.kotlintelegrambot.entities.ParseMode import com.github.kotlintelegrambot.entities.ParseMode
import com.github.kotlintelegrambot.entities.keyboard.InlineKeyboardButton import com.github.kotlintelegrambot.entities.keyboard.InlineKeyboardButton
import com.github.kotlintelegrambot.entities.keyboard.WebAppInfo import com.github.kotlintelegrambot.entities.keyboard.WebAppInfo
@@ -12,10 +11,7 @@ import com.github.kotlintelegrambot.entities.reaction.ReactionType
import com.github.kotlintelegrambot.types.TelegramBotResult import com.github.kotlintelegrambot.types.TelegramBotResult
import org.springframework.beans.factory.annotation.Qualifier import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.beans.factory.annotation.Value import org.springframework.beans.factory.annotation.Value
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate
import org.springframework.stereotype.Repository
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import space.luminic.finance.models.Transaction import space.luminic.finance.models.Transaction
import space.luminic.finance.repos.CategoryRepo import space.luminic.finance.repos.CategoryRepo
import space.luminic.finance.repos.TransactionRepo import space.luminic.finance.repos.TransactionRepo
@@ -46,7 +42,7 @@ class CategorizeService(
tx, tx,
categoriesRepo.findBySpaceId(job.spaceId) categoriesRepo.findBySpaceId(job.spaceId)
) // тут твой вызов GPT ) // тут твой вызов GPT
var unsuccessMessage: TelegramBotResult<Message>? = null var message: TelegramBotResult<Message>? = null
if (res.categoryId == 0) { if (res.categoryId == 0) {
if (tx.tgChatId != null && tx.tgMessageId != null) { if (tx.tgChatId != null && tx.tgMessageId != null) {
@@ -56,7 +52,7 @@ class CategorizeService(
listOf(ReactionType.Emoji("💔")), listOf(ReactionType.Emoji("💔")),
isBig = false isBig = false
) )
unsuccessMessage = bot.sendMessage( message = bot.sendMessage(
ChatId.fromId(tx.tgChatId), ChatId.fromId(tx.tgChatId),
replyToMessageId = tx.tgMessageId, replyToMessageId = tx.tgMessageId,
text = "К сожалению, мы не смогли распознать категорию.\n\nПопробуйте выставить ее самостоятельно.", text = "К сожалению, мы не смогли распознать категорию.\n\nПопробуйте выставить ее самостоятельно.",
@@ -81,7 +77,7 @@ class CategorizeService(
) )
val category = categoriesRepo.findBySpaceIdAndId(job.spaceId, res.categoryId) val category = categoriesRepo.findBySpaceIdAndId(job.spaceId, res.categoryId)
if (category != null) { if (category != null) {
bot.sendMessage( message = bot.sendMessage(
ChatId.fromId(tx.tgChatId), ChatId.fromId(tx.tgChatId),
replyToMessageId = tx.tgMessageId, replyToMessageId = tx.tgMessageId,
text = "Определили категорию: <b>${category.name}</b>.\n\nЕсли это не так, исправьте это в WebApp.", text = "Определили категорию: <b>${category.name}</b>.\n\nЕсли это не так, исправьте это в WebApp.",
@@ -98,11 +94,11 @@ class CategorizeService(
} }
} }
} }
if (unsuccessMessage != null) { if (message != null) {
categoryJobRepo.successJob( categoryJobRepo.successJob(
job.id, job.id,
unsuccessMessage.get().chat.id, message.get().chat.id,
unsuccessMessage.get().messageId message.get().messageId
) )
} else { } else {
categoryJobRepo.successJob(job.id) categoryJobRepo.successJob(job.id)

View File

@@ -35,8 +35,8 @@ class DeepSeekCategorizationService(
Задача: Задача:
1. Определите наиболее подходящую категорию из списка выше для транзакции пользователя. 1. Определите наиболее подходящую категорию из списка выше для транзакции пользователя.
2. Верните ответ в формате: "ID категории", например "3". 2. Верните ответ в формате: ID категории", например 3.
3. Если ни одна категория из списка не подходит, верните: "0". 3. Если ни одна категория из списка не подходит, верните: 0.
Ответ должен быть кратким, одной строкой, без дополнительных пояснений. Ответ должен быть кратким, одной строкой, без дополнительных пояснений.
""".trimIndent() """.trimIndent()

View File

@@ -119,7 +119,7 @@ class BotService(
@Bean @Bean
fun bot(): Bot { fun bot(): Bot {
val bot = com.github.kotlintelegrambot.bot { val bot = com.github.kotlintelegrambot.bot {
logLevel = LogLevel.None logLevel = LogLevel.All()
token = botToken token = botToken
dispatch { dispatch {
message(Filter.Text) { message(Filter.Text) {

View File

@@ -35,7 +35,6 @@ class TransactionsServiceImpl(
tgChatId = chatId, tgChatId = chatId,
tgMessageId = messageId, tgMessageId = messageId,
) )
print(transaction)
return transactionRepo.create(transaction, userId) return transactionRepo.create(transaction, userId)
} }