recurrents
This commit is contained in:
@@ -65,7 +65,7 @@ class AuthService(
|
||||
|
||||
fun tgAuth(tgUser: UserDTO.TelegramAuthDTO): String {
|
||||
val user: User = try {
|
||||
tgLogin(tgUser.id)
|
||||
tgLogin(tgUser.id!!)
|
||||
} catch (e: NotFoundException) {
|
||||
registerTg(tgUser)
|
||||
}
|
||||
|
||||
@@ -10,4 +10,6 @@ interface RecurrentOperationService {
|
||||
fun create(spaceId: Int, operation: RecurrentOperationDTO.CreateRecurrentOperationDTO): Int
|
||||
fun update(spaceId: Int, operationId: Int, operation: RecurrentOperationDTO.UpdateRecurrentOperationDTO)
|
||||
fun delete(spaceId: Int, id: Int)
|
||||
|
||||
fun createRecurrentTransactions()
|
||||
}
|
||||
@@ -1,19 +1,33 @@
|
||||
package space.luminic.finance.services
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.stereotype.Service
|
||||
import space.luminic.finance.dtos.RecurrentOperationDTO
|
||||
import space.luminic.finance.models.Category
|
||||
import space.luminic.finance.models.NotFoundException
|
||||
import space.luminic.finance.models.RecurrentOperation
|
||||
import space.luminic.finance.models.Transaction
|
||||
import space.luminic.finance.repos.RecurrentOperationRepo
|
||||
import space.luminic.finance.repos.SpaceRepo
|
||||
import space.luminic.finance.repos.TransactionRepo
|
||||
import java.time.LocalDate
|
||||
|
||||
@Service
|
||||
class RecurrentOperationServiceImpl(
|
||||
private val authService: AuthService,
|
||||
private val spaceRepo: SpaceRepo,
|
||||
private val recurrentOperationRepo: RecurrentOperationRepo,
|
||||
private val categoryService: CategoryService
|
||||
): RecurrentOperationService {
|
||||
private val categoryService: CategoryService,
|
||||
private val transactionService: TransactionService,
|
||||
private val transactionRepo: TransactionRepo
|
||||
) : RecurrentOperationService {
|
||||
private val logger = LoggerFactory.getLogger(this.javaClass)
|
||||
private val serviceScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
||||
|
||||
override fun findBySpaceId(spaceId: Int): List<RecurrentOperation> {
|
||||
val userId = authService.getSecurityUserId()
|
||||
spaceRepo.findSpaceById(spaceId, userId)
|
||||
@@ -26,12 +40,14 @@ class RecurrentOperationServiceImpl(
|
||||
): RecurrentOperation {
|
||||
val userId = authService.getSecurityUserId()
|
||||
spaceRepo.findSpaceById(spaceId, userId)
|
||||
return recurrentOperationRepo.findBySpaceIdAndId(spaceId, id) ?: throw NotFoundException("Cannot find recurrent operation with id ${id}")
|
||||
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 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,
|
||||
@@ -40,14 +56,42 @@ class RecurrentOperationServiceImpl(
|
||||
amount = operation.amount,
|
||||
date = operation.date
|
||||
)
|
||||
return recurrentOperationRepo.create(creatingOperation, userId)
|
||||
|
||||
val createdRecurrentId = recurrentOperationRepo.create(creatingOperation, userId)
|
||||
val transactionsToCreate = mutableListOf<Transaction>()
|
||||
serviceScope.launch {
|
||||
runCatching {
|
||||
val date = LocalDate.now()
|
||||
for (i in 1..12) {
|
||||
transactionsToCreate.add(
|
||||
Transaction(
|
||||
space = space,
|
||||
type = if (category.type == Category.CategoryType.EXPENSE) Transaction.TransactionType.EXPENSE else Transaction.TransactionType.INCOME,
|
||||
kind = Transaction.TransactionKind.PLANNING,
|
||||
category = category,
|
||||
comment = creatingOperation.name,
|
||||
amount = creatingOperation.amount,
|
||||
date = date.plusMonths(i.toLong()),
|
||||
recurrentId = createdRecurrentId
|
||||
)
|
||||
)
|
||||
}
|
||||
transactionRepo.createBatch(transactionsToCreate, userId)
|
||||
// transactionService.batchCreate(spaceId, transactionsToCreate, userId)
|
||||
}.onFailure {
|
||||
logger.error("Error creating recurring operation", it)
|
||||
}
|
||||
}
|
||||
|
||||
return createdRecurrentId
|
||||
}
|
||||
|
||||
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 existingOperation = recurrentOperationRepo.findBySpaceIdAndId(spaceId, operationId)
|
||||
?: throw NotFoundException("Cannot find operation with id $operationId")
|
||||
val updatedOperation = existingOperation.copy(
|
||||
category = newCategory,
|
||||
name = operation.name,
|
||||
@@ -63,4 +107,25 @@ class RecurrentOperationServiceImpl(
|
||||
spaceRepo.findSpaceById(spaceId, userId)
|
||||
recurrentOperationRepo.delete(id)
|
||||
}
|
||||
|
||||
override fun createRecurrentTransactions() {
|
||||
val today = LocalDate.now()
|
||||
val recurrents = recurrentOperationRepo.findByDate(today.dayOfMonth)
|
||||
recurrents.forEach {
|
||||
transactionRepo.create(
|
||||
Transaction(
|
||||
space = it.space,
|
||||
type = if (it.category.type == Category.CategoryType.EXPENSE) Transaction.TransactionType.EXPENSE else Transaction.TransactionType.INCOME,
|
||||
kind = Transaction.TransactionKind.PLANNING,
|
||||
category = it.category,
|
||||
comment = it.name,
|
||||
amount = it.amount,
|
||||
date = today.plusMonths(13),
|
||||
recurrentId = it.id
|
||||
), it.createdBy?.id!!
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
20
src/main/kotlin/space/luminic/finance/services/Scheduler.kt
Normal file
20
src/main/kotlin/space/luminic/finance/services/Scheduler.kt
Normal file
@@ -0,0 +1,20 @@
|
||||
package space.luminic.finance.services
|
||||
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.scheduling.annotation.EnableScheduling
|
||||
import org.springframework.scheduling.annotation.Scheduled
|
||||
import org.springframework.stereotype.Service
|
||||
|
||||
@EnableScheduling
|
||||
@Service
|
||||
class Scheduler(
|
||||
private val recurrentOperationService: RecurrentOperationService
|
||||
) {
|
||||
private val log = LoggerFactory.getLogger(Scheduler::class.java)
|
||||
|
||||
@Scheduled(cron = "0 0 3 * * *")
|
||||
fun createRecurrentAfter13Month() {
|
||||
log.info("Creating recurrent after 13 month")
|
||||
recurrentOperationService.createRecurrentTransactions()
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ interface SpaceService {
|
||||
|
||||
fun checkSpace(spaceId: Int): Space
|
||||
fun getSpaces(): List<Space>
|
||||
fun getSpace(id: Int): Space
|
||||
fun getSpace(id: Int, userId: Int?): Space
|
||||
fun createSpace(space: SpaceDTO.CreateSpaceDTO): Int
|
||||
fun updateSpace(spaceId: Int, space: SpaceDTO.UpdateSpaceDTO): Int
|
||||
fun deleteSpace(spaceId: Int)
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
package space.luminic.finance.services
|
||||
|
||||
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
|
||||
@@ -17,7 +14,7 @@ class SpaceServiceImpl(
|
||||
private val categoryService: CategoryService
|
||||
) : SpaceService {
|
||||
override fun checkSpace(spaceId: Int): Space {
|
||||
return getSpace(spaceId)
|
||||
return getSpace(spaceId, null)
|
||||
}
|
||||
|
||||
// @Cacheable(cacheNames = ["spaces"])
|
||||
@@ -27,8 +24,9 @@ class SpaceServiceImpl(
|
||||
return spaces
|
||||
}
|
||||
|
||||
override fun getSpace(id: Int): Space {
|
||||
val user = authService.getSecurityUserId()
|
||||
|
||||
override fun getSpace(id: Int, userId: Int?): Space {
|
||||
val user = userId ?: authService.getSecurityUserId()
|
||||
val space = spaceRepo.findSpaceById(id, user) ?: throw NotFoundException("Space with id $id not found")
|
||||
return space
|
||||
|
||||
@@ -56,7 +54,7 @@ class SpaceServiceImpl(
|
||||
space: SpaceDTO.UpdateSpaceDTO
|
||||
): Int {
|
||||
val userId = authService.getSecurityUserId()
|
||||
val existingSpace = getSpace(spaceId)
|
||||
val existingSpace = getSpace(spaceId, null)
|
||||
val updatedSpace = Space(
|
||||
id = existingSpace.id,
|
||||
name = space.name,
|
||||
|
||||
@@ -11,9 +11,17 @@ interface TransactionService {
|
||||
val dateTo: LocalDate? = null,
|
||||
)
|
||||
|
||||
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 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 batchCreate(spaceId: Int, transactions: List<TransactionDTO.CreateTransactionDTO>, createdById: Int?)
|
||||
fun updateTransaction(spaceId: Int, transactionId: Int, transaction: TransactionDTO.UpdateTransactionDTO): Int
|
||||
fun deleteTransaction(spaceId: Int, transactionId: Int)
|
||||
fun deleteByRecurrentId(spaceId: Int, recurrentId: Int)
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import space.luminic.finance.dtos.TransactionDTO
|
||||
import space.luminic.finance.models.NotFoundException
|
||||
import space.luminic.finance.models.Transaction
|
||||
import space.luminic.finance.repos.TransactionRepo
|
||||
import space.luminic.finance.services.gpt.CategorizeService
|
||||
|
||||
@Service
|
||||
class TransactionServiceImpl(
|
||||
@@ -12,6 +13,7 @@ class TransactionServiceImpl(
|
||||
private val categoryService: CategoryService,
|
||||
private val transactionRepo: TransactionRepo,
|
||||
private val authService: AuthService,
|
||||
private val categorizeService: CategorizeService,
|
||||
) : TransactionService {
|
||||
override fun getTransactions(
|
||||
spaceId: Int,
|
||||
@@ -19,14 +21,15 @@ class TransactionServiceImpl(
|
||||
sortBy: String,
|
||||
sortDirection: String
|
||||
): List<Transaction> {
|
||||
return transactionRepo.findAllBySpaceId(spaceId)
|
||||
val transactions = transactionRepo.findAllBySpaceId(spaceId)
|
||||
return transactions
|
||||
}
|
||||
|
||||
override fun getTransaction(
|
||||
spaceId: Int,
|
||||
transactionId: Int
|
||||
): Transaction {
|
||||
spaceService.getSpace(spaceId)
|
||||
spaceService.getSpace(spaceId, null)
|
||||
return transactionRepo.findBySpaceIdAndId(spaceId, transactionId)
|
||||
?: throw NotFoundException("Transaction with id $transactionId not found")
|
||||
}
|
||||
@@ -36,8 +39,9 @@ class TransactionServiceImpl(
|
||||
transaction: TransactionDTO.CreateTransactionDTO
|
||||
): Int {
|
||||
val userId = authService.getSecurityUserId()
|
||||
val space = spaceService.getSpace(spaceId)
|
||||
val category = categoryService.getCategory(spaceId, transaction.categoryId)
|
||||
val space = spaceService.getSpace(spaceId, null)
|
||||
|
||||
val category = transaction.categoryId?.let { categoryService.getCategory(spaceId, it) }
|
||||
val transaction = Transaction(
|
||||
space = space,
|
||||
type = transaction.type,
|
||||
@@ -47,26 +51,43 @@ class TransactionServiceImpl(
|
||||
amount = transaction.amount,
|
||||
fees = transaction.fees,
|
||||
date = transaction.date,
|
||||
recurrentId = transaction.recurrentId,
|
||||
)
|
||||
return transactionRepo.create(transaction, userId)
|
||||
}
|
||||
|
||||
override fun batchCreate(spaceId: Int, transactions: List<TransactionDTO.CreateTransactionDTO>, createdById: Int?) {
|
||||
val userId = createdById ?: authService.getSecurityUserId()
|
||||
val space = spaceService.getSpace(spaceId, userId)
|
||||
val transactionsToCreate = mutableListOf<Transaction>()
|
||||
transactions.forEach { transaction ->
|
||||
val category = transaction.categoryId?.let { categoryService.getCategory(spaceId, it) }
|
||||
transactionsToCreate.add(
|
||||
Transaction(
|
||||
space = space,
|
||||
type = transaction.type,
|
||||
kind = transaction.kind,
|
||||
category = category,
|
||||
comment = transaction.comment,
|
||||
amount = transaction.amount,
|
||||
fees = transaction.fees,
|
||||
date = transaction.date,
|
||||
recurrentId = transaction.recurrentId,
|
||||
)
|
||||
)
|
||||
}
|
||||
transactionRepo.createBatch(transactionsToCreate, userId)
|
||||
|
||||
}
|
||||
|
||||
override fun updateTransaction(
|
||||
spaceId: Int,
|
||||
transactionId: Int,
|
||||
transaction: TransactionDTO.UpdateTransactionDTO
|
||||
): Int {
|
||||
val space = spaceService.getSpace(spaceId)
|
||||
val space = spaceService.getSpace(spaceId, null)
|
||||
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 newCategory = transaction.categoryId?.let { categoryService.getCategory(spaceId, it) }
|
||||
val updatedTransaction = Transaction(
|
||||
id = existingTransaction.id,
|
||||
space = existingTransaction.space,
|
||||
@@ -81,16 +102,24 @@ class TransactionServiceImpl(
|
||||
isDeleted = existingTransaction.isDeleted,
|
||||
isDone = transaction.isDone,
|
||||
createdBy = existingTransaction.createdBy,
|
||||
createdAt = existingTransaction.createdAt
|
||||
|
||||
createdAt = existingTransaction.createdAt,
|
||||
tgChatId = existingTransaction.tgChatId,
|
||||
tgMessageId = existingTransaction.tgMessageId,
|
||||
)
|
||||
if (existingTransaction.category == null && updatedTransaction.category != null) {
|
||||
categorizeService.notifyThatCategorySelected(updatedTransaction)
|
||||
}
|
||||
return transactionRepo.update(updatedTransaction)
|
||||
}
|
||||
|
||||
override fun deleteTransaction(spaceId: Int, transactionId: Int) {
|
||||
val space = spaceService.getSpace(spaceId)
|
||||
val space = spaceService.getSpace(spaceId, null)
|
||||
getTransaction(space.id!!, transactionId)
|
||||
transactionRepo.delete(transactionId)
|
||||
}
|
||||
|
||||
override fun deleteByRecurrentId(spaceId: Int, recurrentId: Int) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package space.luminic.finance.services.gpt
|
||||
|
||||
import org.springframework.scheduling.annotation.Scheduled
|
||||
import org.springframework.stereotype.Service
|
||||
|
||||
@Service
|
||||
class CategorizeBotScheduler(
|
||||
private val seeder: CategoryJobSeeder,
|
||||
private val picker: CategoryJobRepo,
|
||||
private val service: CategorizeService
|
||||
) {
|
||||
|
||||
|
||||
@Scheduled(cron = "* * * * * *")
|
||||
fun work() {
|
||||
val jobs = picker.pickBatch(limit = 50)
|
||||
if (jobs.isEmpty()) return
|
||||
service.processBatch(jobs)
|
||||
}
|
||||
|
||||
@Scheduled(cron = "* * * * * *")
|
||||
fun createJob(){
|
||||
seeder.seedMissing()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package space.luminic.finance.services.gpt
|
||||
|
||||
import com.github.kotlintelegrambot.Bot
|
||||
import com.github.kotlintelegrambot.entities.ChatId
|
||||
import com.github.kotlintelegrambot.entities.InlineKeyboardMarkup
|
||||
import com.github.kotlintelegrambot.entities.Message
|
||||
import com.github.kotlintelegrambot.entities.MessageId
|
||||
import com.github.kotlintelegrambot.entities.ParseMode
|
||||
import com.github.kotlintelegrambot.entities.keyboard.InlineKeyboardButton
|
||||
import com.github.kotlintelegrambot.entities.keyboard.WebAppInfo
|
||||
import com.github.kotlintelegrambot.entities.reaction.ReactionType
|
||||
import com.github.kotlintelegrambot.types.TelegramBotResult
|
||||
import org.springframework.beans.factory.annotation.Qualifier
|
||||
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.transaction.annotation.Transactional
|
||||
import space.luminic.finance.models.Transaction
|
||||
import space.luminic.finance.repos.CategoryRepo
|
||||
import space.luminic.finance.repos.TransactionRepo
|
||||
|
||||
enum class JobStatus { NEW, PROCESSING, DONE, FAILED }
|
||||
|
||||
data class CategoryResult(val categoryId: Int)
|
||||
|
||||
|
||||
@Service
|
||||
class CategorizeService(
|
||||
private val transactionRepo: TransactionRepo,
|
||||
@Qualifier("dsCategorizationService") private val gpt: GptClient,
|
||||
@Value("\${app.categorize.parallel:4}") private val parallel: Int,
|
||||
private val categoriesRepo: CategoryRepo,
|
||||
private val categoryJobRepo: CategoryJobRepo,
|
||||
private val bot: Bot
|
||||
) {
|
||||
private val exec = java.util.concurrent.Executors.newFixedThreadPool(parallel)
|
||||
|
||||
fun processBatch(jobs: List<CategoryJob>) {
|
||||
jobs.forEach { job ->
|
||||
exec.submit {
|
||||
runCatching {
|
||||
val tx = transactionRepo.findBySpaceIdAndId(job.spaceId, job.txId)
|
||||
?: throw IllegalArgumentException("Transaction ${job.txId} not found")
|
||||
val res = gpt.suggestCategory(
|
||||
tx,
|
||||
categoriesRepo.findBySpaceId(job.spaceId)
|
||||
) // тут твой вызов GPT
|
||||
var unsuccessMessage: TelegramBotResult<Message>? = null
|
||||
|
||||
if (res.categoryId == 0) {
|
||||
if (tx.tgChatId != null && tx.tgMessageId != null) {
|
||||
bot.setMessageReaction(
|
||||
ChatId.fromId(tx.tgChatId),
|
||||
tx.tgMessageId,
|
||||
listOf(ReactionType.Emoji("💔")),
|
||||
isBig = false
|
||||
)
|
||||
unsuccessMessage = bot.sendMessage(
|
||||
ChatId.fromId(tx.tgChatId),
|
||||
replyToMessageId = tx.tgMessageId,
|
||||
text = "К сожалению, мы не смогли распознать категорию.\n\nПопробуйте выставить ее самостоятельно.",
|
||||
replyMarkup = InlineKeyboardMarkup.create(
|
||||
listOf(
|
||||
InlineKeyboardButton.WebApp(
|
||||
"Открыть в WebApp",
|
||||
WebAppInfo("https://app.luminic.space/transactions/${tx.id}/edit")
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
transactionRepo.setCategory(job.txId, res.categoryId)
|
||||
if (tx.tgChatId != null && tx.tgMessageId != null) {
|
||||
bot.setMessageReaction(
|
||||
ChatId.fromId(tx.tgChatId),
|
||||
tx.tgMessageId,
|
||||
listOf(ReactionType.Emoji("👌")),
|
||||
isBig = false
|
||||
)
|
||||
val category = categoriesRepo.findBySpaceIdAndId(job.spaceId, res.categoryId)
|
||||
if (category != null) {
|
||||
bot.sendMessage(
|
||||
ChatId.fromId(tx.tgChatId),
|
||||
replyToMessageId = tx.tgMessageId,
|
||||
text = "Определили категорию: <b>${category.name}</b>.\n\nЕсли это не так, исправьте это в WebApp.",
|
||||
replyMarkup = InlineKeyboardMarkup.create(
|
||||
listOf(
|
||||
InlineKeyboardButton.WebApp(
|
||||
"Открыть в WebApp",
|
||||
WebAppInfo("https://app.luminic.space/transactions/${tx.id}/edit")
|
||||
)
|
||||
)
|
||||
),
|
||||
parseMode = ParseMode.HTML
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (unsuccessMessage != null) {
|
||||
categoryJobRepo.successJob(
|
||||
job.id,
|
||||
unsuccessMessage.get().chat.id,
|
||||
unsuccessMessage.get().messageId
|
||||
)
|
||||
} else {
|
||||
categoryJobRepo.successJob(job.id)
|
||||
}
|
||||
}.onFailure { e ->
|
||||
print(e.localizedMessage)
|
||||
categoryJobRepo.failJob(job.id, e.localizedMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun notifyThatCategorySelected(tx: Transaction) {
|
||||
val job = categoryJobRepo.getJobByTxId(tx.id!!)
|
||||
|
||||
job?.let {
|
||||
if (tx.tgChatId != null && tx.tgMessageId != null) {
|
||||
bot.setMessageReaction(
|
||||
ChatId.fromId(tx.tgChatId),
|
||||
tx.tgMessageId,
|
||||
listOf(ReactionType.Emoji("👌"))
|
||||
)
|
||||
}
|
||||
if (it.chatId != null && it.messageId != null) {
|
||||
bot.editMessageText(
|
||||
ChatId.fromId(it.chatId),
|
||||
messageId = it.messageId,
|
||||
text = "Выбрана: <b>${tx.category!!.name}</b>.\n\nЕсли это не так, измените это в WebApp.",
|
||||
replyMarkup = InlineKeyboardMarkup.create(
|
||||
listOf(
|
||||
InlineKeyboardButton.WebApp(
|
||||
"Открыть в WebApp",
|
||||
WebAppInfo("https://app.luminic.space/transactions/${tx.id}/edit")
|
||||
)
|
||||
)
|
||||
),
|
||||
parseMode = ParseMode.HTML
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package space.luminic.finance.services.gpt
|
||||
|
||||
import com.github.kotlintelegrambot.Bot
|
||||
import com.github.kotlintelegrambot.entities.ChatId
|
||||
import com.github.kotlintelegrambot.entities.reaction.ReactionType
|
||||
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate
|
||||
import org.springframework.stereotype.Repository
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
|
||||
data class CategoryJob(
|
||||
val id: Long,
|
||||
val spaceId: Int,
|
||||
val txId: Int,
|
||||
val attempts: Int,
|
||||
val chatId: Long? = null,
|
||||
val messageId: Long? = null
|
||||
)
|
||||
|
||||
@Repository
|
||||
class CategoryJobRepo(
|
||||
private val np: NamedParameterJdbcTemplate,
|
||||
private val bot: Bot
|
||||
) {
|
||||
|
||||
@Transactional
|
||||
fun pickBatch(limit: Int = 50): List<CategoryJob> {
|
||||
val selectSql = """
|
||||
SELECT cj.id as cj_id, cj.tx_id as cj_tx_id, cj.attempts as cj_attempts, s.id as s_id
|
||||
FROM finance.category_jobs cj
|
||||
JOIN finance.transactions t on cj.tx_id = t.id
|
||||
JOIN finance.spaces s on t.space_id = s.id
|
||||
WHERE status IN ('NEW', 'FAILED')
|
||||
ORDER BY cj.created_at
|
||||
FOR UPDATE SKIP LOCKED
|
||||
LIMIT :limit
|
||||
""".trimIndent()
|
||||
|
||||
val jobs = np.query(selectSql, mapOf("limit" to limit)) { rs, _ ->
|
||||
CategoryJob(
|
||||
id = rs.getLong("cj_id"),
|
||||
spaceId = rs.getInt("s_id"),
|
||||
txId = rs.getInt("cj_tx_id"),
|
||||
attempts = rs.getInt("cj_attempts")
|
||||
)
|
||||
}
|
||||
|
||||
if (jobs.isNotEmpty()) {
|
||||
val updateSql = """
|
||||
UPDATE finance.category_jobs
|
||||
SET status = 'PROCESSING',
|
||||
attempts = attempts + 1,
|
||||
started_at = NOW()
|
||||
WHERE id IN (:ids)
|
||||
""".trimIndent()
|
||||
np.update(updateSql, mapOf("ids" to jobs.map { it.id }))
|
||||
}
|
||||
return jobs
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun successJob(id: Long, chatId: Long? = null, messageId: Long? = null) {
|
||||
val sql =
|
||||
"""UPDATE finance.category_jobs SET status = 'DONE', finished_at = now(), tg_chat_id = :chatId, tg_message_id = :messageId WHERE id = :id """.trimIndent()
|
||||
np.update(sql, mapOf("id" to id, "chatId" to chatId, "messageId" to messageId))
|
||||
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun failJob(id: Long, errorMessage: String?) {
|
||||
val sql =
|
||||
"""UPDATE finance.category_jobs SET status = 'FAILED', last_error = :message WHERE id = :id """.trimIndent()
|
||||
np.update(sql, mapOf("id" to id, "errorMessage" to errorMessage))
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun getJobByTxId(txId: Int): CategoryJob? {
|
||||
val selectSql = """
|
||||
SELECT cj.id as cj_id, cj.tx_id as cj_tx_id, cj.attempts as cj_attempts, s.id as s_id, cj.tg_chat_id as cj_chat_id, cj.tg_message_id as cj_message_id
|
||||
FROM finance.category_jobs cj
|
||||
JOIN finance.transactions t on cj.tx_id = t.id
|
||||
JOIN finance.spaces s on t.space_id = s.id
|
||||
WHERE cj.tx_id = :txId
|
||||
""".trimIndent()
|
||||
val jobs = np.query(selectSql, mapOf("txId" to txId), { rs, _ ->
|
||||
CategoryJob(
|
||||
id = rs.getLong("cj_id"),
|
||||
spaceId = rs.getInt("s_id"),
|
||||
txId = rs.getInt("cj_tx_id"),
|
||||
attempts = rs.getInt("cj_attempts"),
|
||||
chatId = rs.getLong("cj_chat_id"),
|
||||
messageId = rs.getLong("cj_message_id")
|
||||
)
|
||||
})
|
||||
return jobs.firstOrNull()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package space.luminic.finance.services.gpt
|
||||
|
||||
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate
|
||||
import org.springframework.stereotype.Repository
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
|
||||
@Repository
|
||||
class CategoryJobSeeder(private val np: NamedParameterJdbcTemplate) {
|
||||
|
||||
/**
|
||||
* Создаёт задачи для всех транзакций без категории.
|
||||
* Ограничь лимит, чтобы не захлестнуть очередь.
|
||||
*/
|
||||
@Transactional
|
||||
fun seedMissing(limit: Int = 1000) : Int {
|
||||
val sql = """
|
||||
INSERT INTO finance.category_jobs (tx_id)
|
||||
SELECT t.id
|
||||
FROM finance.transactions t
|
||||
WHERE t.category_id IS NULL
|
||||
AND t.is_deleted = false
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM finance.category_jobs j WHERE j.tx_id = t.id
|
||||
)
|
||||
ORDER BY t.date DESC
|
||||
LIMIT :limit
|
||||
ON CONFLICT (tx_id) DO NOTHING
|
||||
""".trimIndent()
|
||||
return np.update(sql, mapOf("limit" to limit))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package space.luminic.finance.services.gpt
|
||||
|
||||
|
||||
import okhttp3.*
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.beans.factory.annotation.Qualifier
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.stereotype.Service
|
||||
import space.luminic.finance.models.Category
|
||||
import space.luminic.finance.models.Transaction
|
||||
|
||||
|
||||
@Service("dsCategorizationService")
|
||||
class DeepSeekCategorizationService(
|
||||
@Value("\${ds.api_key}") private val apiKey: String,
|
||||
) : GptClient {
|
||||
|
||||
private val endpoint = "https://api.deepseek.com/v1"
|
||||
private val mapper = jacksonObjectMapper()
|
||||
private val client = OkHttpClient()
|
||||
private val logger = LoggerFactory.getLogger(javaClass)
|
||||
|
||||
override fun suggestCategory(tx: Transaction, categories: List<Category>): CategorySuggestion {
|
||||
val catList = categories.joinToString("\n") { "- ${it.id}: ${it.name}" }
|
||||
val txInfo = """
|
||||
{ \"amount\": ${tx.amount}, \"comment\": \"${tx.comment}\", \"date\":${tx.date}\" }
|
||||
""".trimIndent()
|
||||
val prompt = """
|
||||
Пользователь имеет следующие категории:
|
||||
$catList
|
||||
|
||||
Задача:
|
||||
1. Определите наиболее подходящую категорию из списка выше для транзакции пользователя.
|
||||
2. Верните ответ в формате: "ID категории", например "3".
|
||||
3. Если ни одна категория из списка не подходит, верните: "0".
|
||||
|
||||
Ответ должен быть кратким, одной строкой, без дополнительных пояснений.
|
||||
""".trimIndent()
|
||||
|
||||
val body = mapOf(
|
||||
"model" to "deepseek-chat",
|
||||
"messages" to listOf(
|
||||
mapOf("role" to "assistant", "content" to prompt),
|
||||
mapOf("role" to "user", "content" to txInfo)
|
||||
)
|
||||
)
|
||||
val jsonBody = mapper.writeValueAsString(body)
|
||||
val requestBody = jsonBody.toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull())
|
||||
|
||||
val request = Request.Builder()
|
||||
.url("${endpoint}/chat/completions")
|
||||
.addHeader("Authorization", "Bearer $apiKey")
|
||||
.post(requestBody)
|
||||
.build()
|
||||
println(request)
|
||||
logger.info(request.toString())
|
||||
client.newCall(request).execute().use { response ->
|
||||
if (!response.isSuccessful) error("Qwen error: ${response.code} ${response.body?.string()}")
|
||||
|
||||
val bodyStr = response.body?.string().orEmpty()
|
||||
|
||||
// Берём content из choices[0].message.content
|
||||
val root = mapper.readTree(bodyStr)
|
||||
val text = root["choices"]?.get(0)?.get("message")?.get("content")?.asText()
|
||||
?: error("No choices[0].message.content in response")
|
||||
|
||||
// Парсим "ID – Название (вероятность)"
|
||||
// val regex = Regex("""^\s*(\d+)\s*[–-]\s*(.+?)\s*\((0(?:\.\d+)?|1(?:\.0)?)\)\s*$""")
|
||||
// val match = regex.find(text.trim()) ?: error("Bad format: '$text'")
|
||||
|
||||
// val (idStr, name, confStr) = match.destructured
|
||||
val idStr = text
|
||||
return CategorySuggestion(idStr.toInt(), )
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package space.luminic.finance.services.gpt
|
||||
|
||||
import space.luminic.finance.models.Category
|
||||
import space.luminic.finance.models.Transaction
|
||||
|
||||
data class CategorySuggestion(val categoryId: Int, val categoryName: String? = null, val confidence: Double? = null)
|
||||
|
||||
interface GptClient {
|
||||
fun suggestCategory(tx: Transaction, categories: List<Category>): CategorySuggestion
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package space.luminic.finance.services.gpt
|
||||
|
||||
|
||||
import okhttp3.*
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.beans.factory.annotation.Qualifier
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.stereotype.Service
|
||||
import space.luminic.finance.models.Category
|
||||
import space.luminic.finance.models.Transaction
|
||||
|
||||
@Service("qwenCategorizationService")
|
||||
class QwenCategorizationService(
|
||||
@Value("\${qwen.api_key}") private val apiKey: String,
|
||||
) : GptClient {
|
||||
|
||||
private val endpoint = "https://dashscope-intl.aliyuncs.com/compatible-mode/v1"
|
||||
private val mapper = jacksonObjectMapper()
|
||||
private val client = OkHttpClient()
|
||||
private val logger = LoggerFactory.getLogger(javaClass)
|
||||
|
||||
override fun suggestCategory(tx: Transaction, categories: List<Category>): CategorySuggestion {
|
||||
val catList = categories.joinToString("\n") { "- ${it.id}: ${it.name}" }
|
||||
val txInfo = """
|
||||
{ \"amount\": ${tx.amount}, \"comment\": \"${tx.comment}\", \"date\":${tx.date}\" }
|
||||
""".trimIndent()
|
||||
val prompt = """
|
||||
Пользователь имеет следующие категории:
|
||||
$catList
|
||||
|
||||
Задача:
|
||||
1. Определите наиболее подходящую категорию из списка выше для транзакции пользователя.
|
||||
2. Верните ответ в формате: "ID категории – имя категории (вероятность)", например "3 – Продукты (0.87)".
|
||||
3. Если ни одна категория из списка не подходит, верните: "0 – Другое (вероятность)".
|
||||
|
||||
Ответ должен быть кратким, одной строкой, без дополнительных пояснений.
|
||||
""".trimIndent()
|
||||
|
||||
val body = mapOf(
|
||||
"model" to "qwen-plus",
|
||||
"messages" to listOf(
|
||||
mapOf("role" to "assistant", "content" to prompt),
|
||||
mapOf("role" to "user", "content" to txInfo)
|
||||
)
|
||||
)
|
||||
val jsonBody = mapper.writeValueAsString(body)
|
||||
val requestBody = jsonBody.toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull())
|
||||
|
||||
val request = Request.Builder()
|
||||
.url("${endpoint}/chat/completions")
|
||||
.addHeader("Authorization", "Bearer $apiKey")
|
||||
.post(requestBody)
|
||||
.build()
|
||||
println(request)
|
||||
logger.info(request.toString())
|
||||
client.newCall(request).execute().use { response ->
|
||||
if (!response.isSuccessful) error("Qwen error: ${response.code} ${response.body?.string()}")
|
||||
|
||||
val bodyStr = response.body?.string().orEmpty()
|
||||
|
||||
// Берём content из choices[0].message.content
|
||||
val root = mapper.readTree(bodyStr)
|
||||
val text = root["choices"]?.get(0)?.get("message")?.get("content")?.asText()
|
||||
?: error("No choices[0].message.content in response")
|
||||
|
||||
// Парсим "ID – Название (вероятность)"
|
||||
val regex = Regex("""^\s*(\d+)\s*[–-]\s*(.+?)\s*\((0(?:\.\d+)?|1(?:\.0)?)\)\s*$""")
|
||||
val match = regex.find(text.trim()) ?: error("Bad format: '$text'")
|
||||
|
||||
val (idStr, name, confStr) = match.destructured
|
||||
return CategorySuggestion(idStr.toInt(), name, confStr.toDouble())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
package space.luminic.finance.services.telegram
|
||||
|
||||
import com.github.kotlintelegrambot.Bot
|
||||
import com.github.kotlintelegrambot.dispatch
|
||||
import com.github.kotlintelegrambot.dispatcher.callbackQuery
|
||||
import com.github.kotlintelegrambot.dispatcher.command
|
||||
import com.github.kotlintelegrambot.dispatcher.message
|
||||
import com.github.kotlintelegrambot.entities.ChatId
|
||||
import com.github.kotlintelegrambot.entities.InlineKeyboardMarkup
|
||||
import com.github.kotlintelegrambot.entities.ParseMode
|
||||
import com.github.kotlintelegrambot.entities.keyboard.InlineKeyboardButton
|
||||
import com.github.kotlintelegrambot.entities.keyboard.WebAppInfo
|
||||
import com.github.kotlintelegrambot.entities.reaction.ReactionType
|
||||
import com.github.kotlintelegrambot.extensions.filters.Filter
|
||||
import com.github.kotlintelegrambot.logging.LogLevel
|
||||
import org.springframework.beans.factory.annotation.Qualifier
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import space.luminic.finance.dtos.TransactionDTO
|
||||
import space.luminic.finance.models.NotFoundException
|
||||
import space.luminic.finance.models.State
|
||||
import space.luminic.finance.models.Transaction
|
||||
import space.luminic.finance.models.User
|
||||
import space.luminic.finance.repos.BotRepo
|
||||
import space.luminic.finance.services.UserService
|
||||
import java.time.LocalDate
|
||||
|
||||
@Service
|
||||
class BotService(
|
||||
@Value("\${telegram.bot.token}") private val botToken: String,
|
||||
private val userService: UserService,
|
||||
@Qualifier("spaceServiceTelegram") private val spaceService: SpaceService,
|
||||
private val botRepo: BotRepo,
|
||||
@Qualifier("transactionsServiceTelegram") private val transactionService: TransactionService
|
||||
) {
|
||||
|
||||
|
||||
private fun buildSpaceSelector(userId: Int): InlineKeyboardMarkup {
|
||||
val spaces = spaceService.getSpaces(userId)
|
||||
|
||||
val keyboard = mutableListOf<List<InlineKeyboardButton>>()
|
||||
val row = mutableListOf<InlineKeyboardButton>()
|
||||
if (spaces.isNotEmpty()) {
|
||||
for ((index, space) in spaces.withIndex()) {
|
||||
val button =
|
||||
InlineKeyboardButton.CallbackData(text = space.name, callbackData = "select_space_${space.id}")
|
||||
|
||||
row.add(button)
|
||||
|
||||
// Если 2 кнопки в строке — отправляем строку и очищаем
|
||||
if (row.size == 2) {
|
||||
keyboard.add(ArrayList(row))
|
||||
row.clear()
|
||||
}
|
||||
}
|
||||
|
||||
// Если осталась 1 кнопка — добавляем последнюю строку
|
||||
if (row.isNotEmpty()) {
|
||||
keyboard.add(ArrayList(row))
|
||||
}
|
||||
} else {
|
||||
row.add(InlineKeyboardButton.CallbackData("Создать пространство!", callbackData = "create_space"))
|
||||
keyboard.add(ArrayList(row))
|
||||
}
|
||||
|
||||
return InlineKeyboardMarkup.Companion.create(keyboard)
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun selectSpace(tgUserId: Long, selectedSpaceId: Int) {
|
||||
val user = userService.getUserByTelegramId(tgUserId)
|
||||
botRepo.setState(
|
||||
user.id!!, State.StateCode.SPACE_SELECTED, mapOf(
|
||||
"selected_space" to selectedSpaceId.toString(),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun buildRegister() {
|
||||
|
||||
}
|
||||
|
||||
private fun buildMenu(tgUserId: Long): InlineKeyboardMarkup {
|
||||
val user = userService.getUserByTelegramId(tgUserId)
|
||||
val userId = requireNotNull(user.id) { "User must have id" }
|
||||
|
||||
val state = botRepo.getState(tgUserId)
|
||||
|
||||
val spaceId = state?.data?.get("selected_space")?.toIntOrNull()
|
||||
val space = spaceId?.let { id -> spaceService.getSpace(spaceId, userId) }
|
||||
|
||||
val keyboard = mutableListOf<List<InlineKeyboardButton>>()
|
||||
|
||||
// Кнопка с названием выбранного space (или плейсхолдером)
|
||||
keyboard.add(
|
||||
listOf(
|
||||
InlineKeyboardButton.CallbackData(
|
||||
text = space?.name ?: "Select space",
|
||||
"select_space"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
// Если нужен второй баттон — сделай другой смысл/текст; иначе этот блок можно убрать
|
||||
keyboard.add(
|
||||
listOf(
|
||||
InlineKeyboardButton.WebApp(
|
||||
text = "Открыть WebApp",
|
||||
webApp = WebAppInfo(url = "https://app.luminic.space")
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
return InlineKeyboardMarkup.Companion.create(keyboard)
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun bot(): Bot {
|
||||
val bot = com.github.kotlintelegrambot.bot {
|
||||
logLevel = LogLevel.None
|
||||
token = botToken
|
||||
dispatch {
|
||||
message(Filter.Text) {
|
||||
val fromId = message.from?.id ?: throw IllegalArgumentException("user is empty")
|
||||
val user = userService.getUserByTelegramId(fromId)
|
||||
val state = botRepo.getState(message.from?.id ?: throw IllegalArgumentException("user is empty"))
|
||||
when (state?.state) {
|
||||
State.StateCode.SPACE_SELECTED -> {
|
||||
try {
|
||||
val parts = message.text!!.trim().split(" ", limit = 2)
|
||||
if (parts.isEmpty()) {
|
||||
bot.sendMessage(
|
||||
chatId = ChatId.fromId(message.chat.id),
|
||||
text = "Введите сумму и комментарий, например: `250 обед`",
|
||||
parseMode = ParseMode.MARKDOWN
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
val amount = parts[0].toIntOrNull()
|
||||
?: throw IllegalArgumentException("Сумма транзакции не число!")
|
||||
if (amount <= 0) {
|
||||
throw IllegalArgumentException("Сумма не может быть меньше 1.")
|
||||
}
|
||||
val comment = parts.getOrNull(1)?.trim().orEmpty()
|
||||
if (comment.isEmpty()) throw IllegalArgumentException("Комментарий не может быть пустым.")
|
||||
|
||||
|
||||
// bot.sendMessage(
|
||||
// chatId = ChatId.fromId(message.chat.id),
|
||||
// text = "Принято: сумма = $amount, комментарий = \"$comment\""
|
||||
// )
|
||||
|
||||
try {
|
||||
transactionService.createTransaction(
|
||||
state.data["selected_space"]?.toInt()
|
||||
?: throw IllegalArgumentException("selected space is empty"),
|
||||
user.id!!,
|
||||
TransactionDTO.CreateTransactionDTO(
|
||||
Transaction.TransactionType.EXPENSE,
|
||||
Transaction.TransactionKind.INSTANT,
|
||||
comment = comment,
|
||||
amount = amount.toBigDecimal(),
|
||||
date = LocalDate.now(),
|
||||
),
|
||||
message.chat.id,
|
||||
message.messageId
|
||||
)
|
||||
bot.setMessageReaction(
|
||||
chatId = ChatId.fromId(message.chat.id),
|
||||
messageId = message.messageId,
|
||||
reaction = listOf(ReactionType.Emoji("🤝")),
|
||||
isBig = false
|
||||
)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
bot.sendMessage(
|
||||
ChatId.Companion.fromId(message.chat.id),
|
||||
text = "Кажется у вас не выбран Space",
|
||||
replyMarkup = buildSpaceSelector(user.id!!)
|
||||
)
|
||||
}
|
||||
} catch (e: IllegalArgumentException) {
|
||||
bot.sendMessage(
|
||||
chatId = ChatId.Companion.fromId(message.chat.id),
|
||||
text = "Ошибка: ${e.message}"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
callbackQuery {
|
||||
if (callbackQuery.data.startsWith("select_space_")) {
|
||||
val spaceId = callbackQuery.data.substringAfter("select_space_").toInt()
|
||||
println(spaceId)
|
||||
try {
|
||||
selectSpace(callbackQuery.from.id, spaceId)
|
||||
bot.editMessageText(
|
||||
chatId = ChatId.Companion.fromId(callbackQuery.message!!.chat.id),
|
||||
messageId = callbackQuery.message!!.messageId,
|
||||
text = "Успешно!\n\nМы готовы принимать Ваши транзакции.\n\nПросто пишите их в формате:\n\n <i>сумма комментарий</i>\n\n <b>Первой обязательно должна быть сумма!</b>",
|
||||
parseMode = ParseMode.HTML,
|
||||
replyMarkup = buildMenu(callbackQuery.from.id)
|
||||
)
|
||||
} catch (e: NotFoundException) {
|
||||
e.printStackTrace()
|
||||
bot.sendMessage(
|
||||
ChatId.Companion.fromId(callbackQuery.message!!.chat.id),
|
||||
text = "Мы кажется не знакомы"
|
||||
)
|
||||
}
|
||||
|
||||
} else if (callbackQuery.data.equals("select_space", ignoreCase = true)) {
|
||||
bot.editMessageText(
|
||||
ChatId.Companion.fromId(callbackQuery.message!!.chat.id),
|
||||
callbackQuery.message!!.messageId,
|
||||
text = "Выберите новое пространство",
|
||||
replyMarkup = buildSpaceSelector(
|
||||
userService.getUserByTelegramId(callbackQuery.from.id).id!!
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
command("start") {
|
||||
val user: User
|
||||
try {
|
||||
user = userService.getUserByTelegramId(
|
||||
message.from?.id ?: throw IllegalArgumentException("User not found")
|
||||
)
|
||||
bot.sendMessage(
|
||||
ChatId.Companion.fromId(message.chat.id),
|
||||
text = "Привет!\n\nРады тебя снова видеть!\n\nНачнем с выбора пространства:",
|
||||
replyMarkup = buildSpaceSelector(user.id!!)
|
||||
)
|
||||
|
||||
} catch (e: NotFoundException) {
|
||||
bot.sendMessage(ChatId.Companion.fromId(message.chat.id), text = "Кажется, мы еще не знакомы.")
|
||||
bot.sendMessage(ChatId.Companion.fromId(message.chat.id), text = "Давайте зарегистрируемся? ")
|
||||
bot.sendMessage(ChatId.Companion.fromId(message.chat.id), text = "")
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
bot.startPolling()
|
||||
return bot
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package space.luminic.finance.services.telegram
|
||||
|
||||
import space.luminic.finance.models.Space
|
||||
|
||||
interface SpaceService {
|
||||
fun getSpaces(userId: Int): List<Space>
|
||||
fun getSpace(spaceId: Int, userId: Int): Space?
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package space.luminic.finance.services.telegram
|
||||
|
||||
import org.springframework.stereotype.Service
|
||||
import space.luminic.finance.models.NotFoundException
|
||||
import space.luminic.finance.models.Space
|
||||
import space.luminic.finance.repos.SpaceRepo
|
||||
|
||||
@Service("spaceServiceTelegram")
|
||||
class SpaceServiceImpl(
|
||||
private val spaceRepo: SpaceRepo
|
||||
) : SpaceService {
|
||||
override fun getSpaces(userId: Int): List<Space> {
|
||||
val spaces = spaceRepo.findSpacesAvailableForUser(userId)
|
||||
return spaces
|
||||
}
|
||||
|
||||
override fun getSpace(spaceId: Int, userId: Int): Space? {
|
||||
val space =
|
||||
spaceRepo.findSpaceById(spaceId, userId) ?: throw NotFoundException("Space with id $spaceId not found")
|
||||
return space
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package space.luminic.finance.services.telegram
|
||||
|
||||
import space.luminic.finance.dtos.TransactionDTO
|
||||
|
||||
interface TransactionService {
|
||||
|
||||
fun createTransaction(spaceId: Int, userId: Int, transaction: TransactionDTO.CreateTransactionDTO, chatId: Long, messageId: Long ): Int
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package space.luminic.finance.services.telegram
|
||||
|
||||
import org.springframework.beans.factory.annotation.Qualifier
|
||||
import org.springframework.stereotype.Service
|
||||
import space.luminic.finance.dtos.TransactionDTO
|
||||
import space.luminic.finance.models.Transaction
|
||||
import space.luminic.finance.repos.TransactionRepo
|
||||
import space.luminic.finance.services.CategoryServiceImpl
|
||||
|
||||
@Service("transactionsServiceTelegram")
|
||||
class TransactionsServiceImpl(
|
||||
private val transactionRepo: TransactionRepo,
|
||||
@Qualifier("spaceServiceTelegram") private val spaceService: SpaceService,
|
||||
private val categoryService: CategoryServiceImpl
|
||||
): TransactionService {
|
||||
|
||||
override fun createTransaction(
|
||||
spaceId: Int,
|
||||
userId: Int,
|
||||
transaction: TransactionDTO.CreateTransactionDTO,
|
||||
chatId: Long,
|
||||
messageId: Long
|
||||
): Int {
|
||||
val space = spaceService.getSpace(spaceId, userId)
|
||||
val category = transaction.categoryId?.let { categoryService.getCategory(spaceId, it) }
|
||||
val transaction = Transaction(
|
||||
space = space,
|
||||
type = transaction.type,
|
||||
kind = transaction.kind,
|
||||
category = category,
|
||||
comment = transaction.comment,
|
||||
amount = transaction.amount,
|
||||
fees = transaction.fees,
|
||||
date = transaction.date,
|
||||
tgChatId = chatId,
|
||||
tgMessageId = messageId,
|
||||
)
|
||||
print(transaction)
|
||||
return transactionRepo.create(transaction, userId)
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user