6 Commits

Author SHA1 Message Date
xds
c84f6a3988 + notifications 2025-11-20 14:54:00 +03:00
xds
195bdd83f0 filters for transactions;
update transactions when recurrent updated
2025-11-18 00:34:02 +03:00
42cbf30bd8 Update Dockerfile 2025-11-17 15:11:46 +03:00
5803fc208b Update Dockerfile 2025-11-17 15:10:40 +03:00
a79dbffe3f recurrents 2025-11-17 15:10:25 +03:00
9d7c385654 Merge pull request 'recurrents' (#2) from recurrents into main
Reviewed-on: #2
2025-11-17 15:03:32 +03:00
23 changed files with 427 additions and 556 deletions

View File

@@ -5,7 +5,6 @@ USER root
RUN apt-get update && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/* RUN apt-get update && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/*
RUN groupadd --system --gid 1001 app && useradd --system --gid app --uid 1001 --shell /bin/bash --create-home app RUN groupadd --system --gid 1001 app && useradd --system --gid app --uid 1001 --shell /bin/bash --create-home app
RUN mkdir -p /app/static && chown -R app:app /app RUN mkdir -p /app/static && chown -R app:app /app
COPY build/libs/luminic-space-v2.jar /app/luminic-space-v2.jar
USER app USER app
ENV JAVA_TOOL_OPTIONS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0" ENV JAVA_TOOL_OPTIONS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0"

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

@@ -38,7 +38,8 @@ class SpaceRepoImpl(
owner = User( owner = User(
rs.getInt("s_owner_id"), rs.getInt("s_owner_id"),
rs.getString("s_owner_username"), rs.getString("s_owner_username"),
rs.getString("s_owner_firstname") rs.getString("s_owner_firstname"),
tgId = rs.getLong("s_owner_tg_id"),
), ),
participant = User(rs.getInt("sp_uid"), rs.getString("sp_username"), rs.getString("sp_first_name")), participant = User(rs.getInt("sp_uid"), rs.getString("sp_username"), rs.getString("sp_first_name")),
createdAt = rs.getTimestamp("s_created_at").toInstant(), createdAt = rs.getTimestamp("s_created_at").toInstant(),
@@ -93,6 +94,7 @@ class SpaceRepoImpl(
s.owner_id as s_owner_id, s.owner_id as s_owner_id,
ou.username as s_owner_username, ou.username as s_owner_username,
ou.first_name as s_owner_firstname, ou.first_name as s_owner_firstname,
ou.tg_id as s_owner_tg_id,
sp.participants_id as sp_uid, sp.participants_id as sp_uid,
u.username as sp_username, u.username as sp_username,
u.first_name as sp_first_name, u.first_name as sp_first_name,
@@ -114,7 +116,7 @@ class SpaceRepoImpl(
where (s.owner_id = :user_id where (s.owner_id = :user_id
or sp.participants_id = :user_id) or sp.participants_id = :user_id)
and s.is_deleted = false and s.is_deleted = false
group by s.id, ou.username, ou.first_name, sp.participants_id, u.username, u.first_name, cau.username, cau.first_name, group by s.id, ou.username, ou.first_name, ou.tg_id, sp.participants_id, u.username, u.first_name, cau.username, cau.first_name,
uau.username, uau.first_name; uau.username, uau.first_name;
""".trimMargin() """.trimMargin()
val params = mapOf( val params = mapOf(
@@ -133,6 +135,7 @@ class SpaceRepoImpl(
s.owner_id as s_owner_id, s.owner_id as s_owner_id,
ou.username as s_owner_username, ou.username as s_owner_username,
ou.first_name as s_owner_firstname, ou.first_name as s_owner_firstname,
ou.tg_id as s_owner_tg_id,
sp.participants_id as sp_uid, sp.participants_id as sp_uid,
u.username as sp_username, u.username as sp_username,
u.first_name as sp_first_name, u.first_name as sp_first_name,
@@ -154,7 +157,7 @@ from finance.spaces s
where (s.owner_id = :user_id where (s.owner_id = :user_id
or sp.participants_id = :user_id) or sp.participants_id = :user_id)
and s.is_deleted = false and s.id = :spaceId and s.is_deleted = false and s.id = :spaceId
group by s.id, ou.username, ou.first_name, sp.participants_id, u.username, u.first_name, cau.username, cau.first_name, group by s.id, ou.username, ou.first_name, ou.tg_id, sp.participants_id, u.username, u.first_name, cau.username, cau.first_name,
uau.username, uau.first_name; uau.username, uau.first_name;
""".trimMargin() """.trimMargin()
val params = mapOf( val params = mapOf(

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

@@ -0,0 +1,19 @@
package space.luminic.finance.services
import com.github.kotlintelegrambot.entities.ReplyMarkup
import com.github.kotlintelegrambot.entities.inputmedia.MediaGroup
import space.luminic.finance.models.Space
import space.luminic.finance.models.Transaction
interface NotificationService {
fun sendDailyReminder()
fun sendTXNotification(action: TxActionType, space: Space, userId: Int, tx: Transaction, tx2: Transaction? = null)
fun sendTextMessage(chatId: Long, message: String, replyMarkup: ReplyMarkup? = null)
fun sendMediaGroup(chatId: Long, group: MediaGroup)
}
enum class TxActionType {
CREATE,
UPDATE,
DELETE,
}

View File

@@ -0,0 +1,145 @@
package space.luminic.finance.services
import com.github.kotlintelegrambot.Bot
import com.github.kotlintelegrambot.entities.ChatId
import com.github.kotlintelegrambot.entities.InlineKeyboardMarkup
import com.github.kotlintelegrambot.entities.ReplyMarkup
import com.github.kotlintelegrambot.entities.inputmedia.MediaGroup
import com.github.kotlintelegrambot.entities.keyboard.InlineKeyboardButton
import com.github.kotlintelegrambot.entities.keyboard.WebAppInfo
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import org.springframework.context.annotation.Lazy
import space.luminic.finance.models.Space
import space.luminic.finance.models.Transaction
import java.time.format.DateTimeFormatter
@Service
class NotificationServiceImpl(private val userService: UserService, private val bot: Bot,) : NotificationService {
private val logger = LoggerFactory.getLogger(this.javaClass)
private fun createWebAppButton(spaceId: Int? = null, txId: Int? = null): InlineKeyboardMarkup =
spaceId?.let { spaceId ->
txId?.let { txId ->
InlineKeyboardMarkup.create(
listOf(
InlineKeyboardButton.WebApp(
"Открыть в WebApp",
WebAppInfo("https://app.luminic.space/transactions/${txId}/edit?mode=from_bot&space=${spaceId}")
)
)
)
} ?: InlineKeyboardMarkup.create(
listOf(
InlineKeyboardButton.WebApp(
"Открыть в WebApp",
WebAppInfo("https://app.luminic.space/transactions?mode=from_bot&space=${spaceId}")
)
)
)
} ?: InlineKeyboardMarkup.create(
listOf(
InlineKeyboardButton.WebApp(
"Открыть в WebApp",
WebAppInfo("https://app.luminic.space/transactions")
)
)
)
override fun sendDailyReminder() {
val text = "🤑 Время заполнять траты!"
val users = userService.getUsers()
for (user in users) {
user.tgId?.let {
sendTextMessage(it, text, createWebAppButton())
}
}
}
override fun sendTXNotification(
action: TxActionType,
space: Space,
userId: Int,
tx: Transaction,
tx2: Transaction?
) {
val user = userService.getById(userId)
when (action) {
TxActionType.CREATE -> {
val text = "${user.firstName} создал транзакцию ${tx.comment} c суммой ${tx.amount} и датой ${tx.date}"
space.owner.tgId?.let { sendTextMessage(it, text, createWebAppButton(space.id, tx.id)) }
}
TxActionType.UPDATE -> {
tx2?.let { tx2 ->
val changes = mutableListOf<String>()
if (tx.type != tx2.type) {
changes.add("Тип: ${tx.type.name}${tx2.type.name}")
}
if (tx.kind != tx2.kind) {
changes.add("Вид: ${tx.kind.name}${tx2.kind.name}")
}
if (tx.category != tx2.category) {
tx.category?.let { oldCategory ->
tx2.category?.let { newCategory ->
if (oldCategory.id != newCategory.id) {
changes.add("Категория: ${oldCategory.name}${newCategory.name}")
}
} ?: changes.add("Удалена категория. Прежняя: ${oldCategory.name}")
} ?: {
tx2.category?.let { newCategory ->
changes.add("Установлена новая категория ${newCategory.name}")
}
}
}
if (tx.comment != tx2.comment) {
changes.add("Комментарий: ${tx.comment}${tx2.comment}")
}
if (tx.amount != tx2.amount) {
changes.add("Сумма: ${tx.amount}${tx2.amount}")
}
if (tx.date.toEpochDay() != tx2.date.toEpochDay()) {
changes.add(
"Сумма: ${
tx.date.format(DateTimeFormatter.ofPattern("dd.MM.yyyy"))
} ${
tx2.date.format(
DateTimeFormatter.ofPattern("dd.MM.yyyy")
)
}"
)
}
var text = "${user.firstName} обновил транзакцию ${tx.comment}\n\n"
text += changes.joinToString("\n") { it }
space.owner.tgId?.let { sendTextMessage(it, text, createWebAppButton(space.id, tx.id)) }
} ?: logger.warn("No tx2 provided when update")
}
TxActionType.DELETE -> {
val text = "${user.firstName} удалил транзакцию ${tx.comment} c суммой ${tx.amount} и датой ${tx.date}"
space.owner.tgId?.let { sendTextMessage(it, text, createWebAppButton(space.id, tx.id)) }
}
}
}
override fun sendTextMessage(
chatId: Long,
message: String,
replyMarkup: ReplyMarkup?
) {
bot.sendMessage(ChatId.fromId(chatId), message, replyMarkup = replyMarkup)
}
override fun sendMediaGroup(
chatId: Long,
group: MediaGroup
) {
TODO("Not yet implemented")
}
}

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

@@ -8,7 +8,8 @@ import org.springframework.stereotype.Service
@EnableScheduling @EnableScheduling
@Service @Service
class Scheduler( class Scheduler(
private val recurrentOperationService: RecurrentOperationService private val recurrentOperationService: RecurrentOperationService,
private val notificationService: NotificationService
) { ) {
private val log = LoggerFactory.getLogger(Scheduler::class.java) private val log = LoggerFactory.getLogger(Scheduler::class.java)
@@ -17,4 +18,10 @@ class Scheduler(
log.info("Creating recurrent after 13 month") log.info("Creating recurrent after 13 month")
recurrentOperationService.createRecurrentTransactions() recurrentOperationService.createRecurrentTransactions()
} }
@Scheduled(cron = "0 30 19 * * *")
fun sendDailyReminders() {
log.info("Sending daily reminders")
notificationService.sendDailyReminder()
}
} }

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

@@ -1,11 +1,17 @@
package space.luminic.finance.services 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 org.springframework.stereotype.Service
import space.luminic.finance.dtos.TransactionDTO import space.luminic.finance.dtos.TransactionDTO
import space.luminic.finance.models.NotFoundException import space.luminic.finance.models.NotFoundException
import space.luminic.finance.models.Transaction import space.luminic.finance.models.Transaction
import space.luminic.finance.repos.TransactionRepo import space.luminic.finance.repos.TransactionRepo
import space.luminic.finance.services.gpt.CategorizeService import space.luminic.finance.services.gpt.CategorizeService
import java.time.format.DateTimeFormatter
@Service @Service
class TransactionServiceImpl( class TransactionServiceImpl(
@@ -14,14 +20,18 @@ class TransactionServiceImpl(
private val transactionRepo: TransactionRepo, private val transactionRepo: TransactionRepo,
private val authService: AuthService, private val authService: AuthService,
private val categorizeService: CategorizeService, private val categorizeService: CategorizeService,
private val notificationService: NotificationService,
) : TransactionService { ) : TransactionService {
private val logger = LoggerFactory.getLogger(this.javaClass)
private val serviceScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
override fun getTransactions( override fun getTransactions(
spaceId: Int, spaceId: Int,
filter: TransactionService.TransactionsFilter, filter: TransactionService.TransactionsFilter,
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
} }
@@ -53,7 +63,17 @@ class TransactionServiceImpl(
date = transaction.date, date = transaction.date,
recurrentId = transaction.recurrentId, recurrentId = transaction.recurrentId,
) )
return transactionRepo.create(transaction, userId) val createdTx = transactionRepo.create(transaction, userId)
serviceScope.launch {
runCatching {
if (space.owner.id != userId) {
notificationService.sendTXNotification(TxActionType.CREATE, space, userId, transaction)
}
}.onFailure {
logger.error("Error while creating transaction", it)
}
}
return createdTx
} }
override fun batchCreate(spaceId: Int, transactions: List<TransactionDTO.CreateTransactionDTO>, createdById: Int?) { override fun batchCreate(spaceId: Int, transactions: List<TransactionDTO.CreateTransactionDTO>, createdById: Int?) {
@@ -85,6 +105,7 @@ class TransactionServiceImpl(
transactionId: Int, transactionId: Int,
transaction: TransactionDTO.UpdateTransactionDTO transaction: TransactionDTO.UpdateTransactionDTO
): Int { ): Int {
val userId = authService.getSecurityUserId()
val space = spaceService.getSpace(spaceId, null) val space = spaceService.getSpace(spaceId, null)
val existingTransaction = getTransaction(space.id!!, transactionId) val existingTransaction = getTransaction(space.id!!, transactionId)
val newCategory = transaction.categoryId?.let { categoryService.getCategory(spaceId, it) } val newCategory = transaction.categoryId?.let { categoryService.getCategory(spaceId, it) }
@@ -106,16 +127,37 @@ 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) val updatedTx = transactionRepo.update(updatedTransaction)
serviceScope.launch {
runCatching {
notificationService.sendTXNotification(
TxActionType.UPDATE,
space,
userId,
existingTransaction,
updatedTransaction
)
}.onFailure {
logger.error("Error while send transaction update notification", it)
}
}
return updatedTx
} }
override fun deleteTransaction(spaceId: Int, transactionId: Int) { override fun deleteTransaction(spaceId: Int, transactionId: Int) {
val userId = authService.getSecurityUserId()
val space = spaceService.getSpace(spaceId, null) val space = spaceService.getSpace(spaceId, null)
getTransaction(space.id!!, transactionId) val tx = getTransaction(space.id!!, transactionId)
transactionRepo.delete(transactionId) transactionRepo.delete(transactionId)
serviceScope.launch {
runCatching {
notificationService.sendTXNotification(TxActionType.DELETE, space, userId, tx)
}.onFailure { logger.error("Error while transaction delete notification", it) }
}
} }
override fun deleteByRecurrentId(spaceId: Int, recurrentId: Int) { override fun deleteByRecurrentId(spaceId: Int, recurrentId: Int) {

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

@@ -16,6 +16,7 @@ import com.github.kotlintelegrambot.logging.LogLevel
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.context.annotation.Bean import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Lazy
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional import org.springframework.transaction.annotation.Transactional
import space.luminic.finance.dtos.TransactionDTO import space.luminic.finance.dtos.TransactionDTO
@@ -30,10 +31,11 @@ import java.time.LocalDate
@Service @Service
class BotService( class BotService(
@Value("\${telegram.bot.token}") private val botToken: String, @Value("\${telegram.bot.token}") private val botToken: String,
@Value("\${spring.profiles.active}") private val profile: String,
private val userService: UserService, private val userService: UserService,
@Qualifier("spaceServiceTelegram") private val spaceService: SpaceService, @Qualifier("spaceServiceTelegram") private val spaceService: SpaceService,
private val botRepo: BotRepo, private val botRepo: BotRepo,
@Qualifier("transactionsServiceTelegram") private val transactionService: TransactionService @Lazy @Qualifier("transactionsServiceTelegram") private val transactionService: TransactionService
) { ) {
@@ -113,13 +115,13 @@ class BotService(
) )
) )
return InlineKeyboardMarkup.Companion.create(keyboard) return InlineKeyboardMarkup.create(keyboard)
} }
@Bean @Bean
fun bot(): Bot { fun bot(): Bot {
val bot = com.github.kotlintelegrambot.bot { val bot = com.github.kotlintelegrambot.bot {
logLevel = LogLevel.None logLevel = if (profile == "proc") LogLevel.None else LogLevel.All()
token = botToken token = botToken
dispatch { dispatch {
message(Filter.Text) { message(Filter.Text) {

View File

@@ -4,5 +4,5 @@ import space.luminic.finance.models.Space
interface SpaceService { interface SpaceService {
fun getSpaces(userId: Int): List<Space> fun getSpaces(userId: Int): List<Space>
fun getSpace(spaceId: Int, userId: Int): Space? fun getSpace(spaceId: Int, userId: Int): Space
} }

View File

@@ -14,7 +14,7 @@ class SpaceServiceImpl(
return spaces return spaces
} }
override fun getSpace(spaceId: Int, userId: Int): Space? { override fun getSpace(spaceId: Int, userId: Int): Space {
val space = val space =
spaceRepo.findSpaceById(spaceId, userId) ?: throw NotFoundException("Space with id $spaceId not found") spaceRepo.findSpaceById(spaceId, userId) ?: throw NotFoundException("Space with id $spaceId not found")
return space return space

View File

@@ -1,18 +1,29 @@
package space.luminic.finance.services.telegram package space.luminic.finance.services.telegram
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Qualifier import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import space.luminic.finance.dtos.TransactionDTO import space.luminic.finance.dtos.TransactionDTO
import space.luminic.finance.models.Transaction import space.luminic.finance.models.Transaction
import space.luminic.finance.repos.TransactionRepo import space.luminic.finance.repos.TransactionRepo
import space.luminic.finance.services.CategoryServiceImpl import space.luminic.finance.services.CategoryServiceImpl
import space.luminic.finance.services.NotificationService
import space.luminic.finance.services.TxActionType
@Service("transactionsServiceTelegram") @Service("transactionsServiceTelegram")
class TransactionsServiceImpl( class TransactionsServiceImpl(
private val transactionRepo: TransactionRepo, private val transactionRepo: TransactionRepo,
@Qualifier("spaceServiceTelegram") private val spaceService: SpaceService, @Qualifier("spaceServiceTelegram") private val spaceService: SpaceService,
private val categoryService: CategoryServiceImpl private val categoryService: CategoryServiceImpl,
private val notificationService: NotificationService
) : TransactionService { ) : TransactionService {
private val logger = LoggerFactory.getLogger(this.javaClass)
private val serviceScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
override fun createTransaction( override fun createTransaction(
spaceId: Int, spaceId: Int,
@@ -35,10 +46,17 @@ class TransactionsServiceImpl(
tgChatId = chatId, tgChatId = chatId,
tgMessageId = messageId, tgMessageId = messageId,
) )
print(transaction) serviceScope.launch {
runCatching {
if (space.owner.id != userId) {
notificationService.sendTXNotification(TxActionType.CREATE, space, userId, transaction)
}
}.onFailure {
logger.error("Error while transaction notification", it)
}
}
return transactionRepo.create(transaction, userId) return transactionRepo.create(transaction, userId)
} }
} }