Compare commits
6 Commits
recurrents
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| c84f6a3988 | |||
| 195bdd83f0 | |||
| 42cbf30bd8 | |||
| 5803fc208b | |||
| a79dbffe3f | |||
| 9d7c385654 |
@@ -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 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
|
||||
COPY build/libs/luminic-space-v2.jar /app/luminic-space-v2.jar
|
||||
USER app
|
||||
|
||||
ENV JAVA_TOOL_OPTIONS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0"
|
||||
|
||||
@@ -21,9 +21,9 @@ class TransactionController (
|
||||
){
|
||||
|
||||
|
||||
@GetMapping
|
||||
fun getTransactions(@PathVariable spaceId: Int) : List<TransactionDTO>{
|
||||
return transactionService.getTransactions(spaceId, TransactionService.TransactionsFilter(),"date", "DESC").map { it.toDto() }
|
||||
@PostMapping("/_search")
|
||||
fun getTransactions(@PathVariable spaceId: Int, @RequestBody filter: TransactionService.TransactionsFilter) : List<TransactionDTO>{
|
||||
return transactionService.getTransactions(spaceId, filter,"date", "DESC").map { it.toDto() }
|
||||
}
|
||||
|
||||
@GetMapping("/{transactionId}")
|
||||
|
||||
@@ -15,7 +15,7 @@ data class Transaction(
|
||||
val type: TransactionType = TransactionType.EXPENSE,
|
||||
val kind: TransactionKind = TransactionKind.INSTANT,
|
||||
val category: Category? = null,
|
||||
val comment: String,
|
||||
var comment: String,
|
||||
val amount: BigDecimal,
|
||||
val fees: BigDecimal = BigDecimal.ZERO,
|
||||
val date: LocalDate = LocalDate.now(),
|
||||
|
||||
@@ -74,7 +74,7 @@ class RecurrentOperationRepoImpl(
|
||||
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
|
||||
where ro.space_id = :spaceId
|
||||
order by ro.date
|
||||
order by ro.date, ro.id
|
||||
""".trimIndent()
|
||||
val params = mapOf("spaceId" to spaceId)
|
||||
return jdbcTemplate.query(sql, params, operationRowMapper())
|
||||
|
||||
@@ -38,7 +38,8 @@ class SpaceRepoImpl(
|
||||
owner = User(
|
||||
rs.getInt("s_owner_id"),
|
||||
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")),
|
||||
createdAt = rs.getTimestamp("s_created_at").toInstant(),
|
||||
@@ -93,6 +94,7 @@ class SpaceRepoImpl(
|
||||
s.owner_id as s_owner_id,
|
||||
ou.username as s_owner_username,
|
||||
ou.first_name as s_owner_firstname,
|
||||
ou.tg_id as s_owner_tg_id,
|
||||
sp.participants_id as sp_uid,
|
||||
u.username as sp_username,
|
||||
u.first_name as sp_first_name,
|
||||
@@ -114,7 +116,7 @@ class SpaceRepoImpl(
|
||||
where (s.owner_id = :user_id
|
||||
or sp.participants_id = :user_id)
|
||||
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;
|
||||
""".trimMargin()
|
||||
val params = mapOf(
|
||||
@@ -133,6 +135,7 @@ class SpaceRepoImpl(
|
||||
s.owner_id as s_owner_id,
|
||||
ou.username as s_owner_username,
|
||||
ou.first_name as s_owner_firstname,
|
||||
ou.tg_id as s_owner_tg_id,
|
||||
sp.participants_id as sp_uid,
|
||||
u.username as sp_username,
|
||||
u.first_name as sp_first_name,
|
||||
@@ -154,7 +157,7 @@ from finance.spaces s
|
||||
where (s.owner_id = :user_id
|
||||
or sp.participants_id = :user_id)
|
||||
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;
|
||||
""".trimMargin()
|
||||
val params = mapOf(
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
package space.luminic.finance.repos
|
||||
|
||||
import space.luminic.finance.models.Transaction
|
||||
import space.luminic.finance.services.TransactionService
|
||||
|
||||
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 findBySpaceIdAndRecurrentId(spaceId: Int, recurrentId: Int): List<Transaction>
|
||||
fun create(transaction: Transaction, userId: Int): Int
|
||||
fun createBatch(transactions: List<Transaction>, userId: Int)
|
||||
fun update(transaction: Transaction): Int
|
||||
fun updateBatch(transactions: List<Transaction>, userId: Int)
|
||||
fun delete(transactionId: Int)
|
||||
fun deleteByRecurrentId(spaceId: Int, recurrentId: Int)
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import org.springframework.stereotype.Repository
|
||||
import space.luminic.finance.models.Category
|
||||
import space.luminic.finance.models.Transaction
|
||||
import space.luminic.finance.models.User
|
||||
import space.luminic.finance.services.TransactionService
|
||||
|
||||
@Repository
|
||||
class TransactionRepoImpl(
|
||||
@@ -52,8 +53,8 @@ class TransactionRepoImpl(
|
||||
)
|
||||
}
|
||||
|
||||
override fun findAllBySpaceId(spaceId: Int): List<Transaction> {
|
||||
val sql = """
|
||||
override fun findAllBySpaceId(spaceId: Int, filters: TransactionService.TransactionsFilter): List<Transaction> {
|
||||
var sql = """
|
||||
SELECT
|
||||
t.id AS t_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
|
||||
JOIN finance.users u ON u.id = t.created_by_id
|
||||
WHERE t.space_id = :spaceId and t.is_deleted = false
|
||||
ORDER BY t.date, t.id
|
||||
|
||||
""".trimIndent()
|
||||
val params = mapOf(
|
||||
val params = mutableMapOf<String, Any?>(
|
||||
"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())
|
||||
}
|
||||
|
||||
@@ -134,6 +163,49 @@ class TransactionRepoImpl(
|
||||
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 {
|
||||
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 (
|
||||
@@ -211,14 +283,6 @@ class TransactionRepoImpl(
|
||||
}
|
||||
|
||||
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 = """
|
||||
UPDATE finance.transactions
|
||||
@@ -247,6 +311,39 @@ class TransactionRepoImpl(
|
||||
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) {
|
||||
val sql = """
|
||||
update finance.transactions set is_deleted = true where id = :id
|
||||
|
||||
@@ -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()
|
||||
// }
|
||||
//
|
||||
//
|
||||
//}
|
||||
@@ -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()
|
||||
// }
|
||||
//}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import space.luminic.finance.repos.RecurrentOperationRepo
|
||||
import space.luminic.finance.repos.SpaceRepo
|
||||
import space.luminic.finance.repos.TransactionRepo
|
||||
import java.time.LocalDate
|
||||
import kotlin.math.min
|
||||
|
||||
@Service
|
||||
class RecurrentOperationServiceImpl(
|
||||
@@ -22,7 +23,6 @@ class RecurrentOperationServiceImpl(
|
||||
private val spaceRepo: SpaceRepo,
|
||||
private val recurrentOperationRepo: RecurrentOperationRepo,
|
||||
private val categoryService: CategoryService,
|
||||
private val transactionService: TransactionService,
|
||||
private val transactionRepo: TransactionRepo
|
||||
) : RecurrentOperationService {
|
||||
private val logger = LoggerFactory.getLogger(this.javaClass)
|
||||
@@ -61,7 +61,8 @@ class RecurrentOperationServiceImpl(
|
||||
val transactionsToCreate = mutableListOf<Transaction>()
|
||||
serviceScope.launch {
|
||||
runCatching {
|
||||
val date = LocalDate.now()
|
||||
val now = LocalDate.now()
|
||||
val date = now.withDayOfMonth(min(operation.date, now.lengthOfMonth()))
|
||||
for (i in 1..12) {
|
||||
transactionsToCreate.add(
|
||||
Transaction(
|
||||
@@ -99,7 +100,30 @@ class RecurrentOperationServiceImpl(
|
||||
date = operation.date
|
||||
)
|
||||
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) {
|
||||
|
||||
@@ -8,7 +8,8 @@ import org.springframework.stereotype.Service
|
||||
@EnableScheduling
|
||||
@Service
|
||||
class Scheduler(
|
||||
private val recurrentOperationService: RecurrentOperationService
|
||||
private val recurrentOperationService: RecurrentOperationService,
|
||||
private val notificationService: NotificationService
|
||||
) {
|
||||
private val log = LoggerFactory.getLogger(Scheduler::class.java)
|
||||
|
||||
@@ -17,4 +18,10 @@ class Scheduler(
|
||||
log.info("Creating recurrent after 13 month")
|
||||
recurrentOperationService.createRecurrentTransactions()
|
||||
}
|
||||
|
||||
@Scheduled(cron = "0 30 19 * * *")
|
||||
fun sendDailyReminders() {
|
||||
log.info("Sending daily reminders")
|
||||
notificationService.sendDailyReminder()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
// }
|
||||
//}
|
||||
@@ -7,8 +7,13 @@ import java.time.LocalDate
|
||||
interface TransactionService {
|
||||
|
||||
data class TransactionsFilter(
|
||||
val type: Transaction.TransactionType? = null,
|
||||
val kind: Transaction.TransactionKind? = null,
|
||||
val dateFrom: LocalDate? = null,
|
||||
val dateTo: LocalDate? = null,
|
||||
val isDone: Boolean? = null,
|
||||
val offset: Int = 0,
|
||||
val limit: Int = 10,
|
||||
)
|
||||
|
||||
fun getTransactions(
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
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.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
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
@Service
|
||||
class TransactionServiceImpl(
|
||||
@@ -14,14 +20,18 @@ class TransactionServiceImpl(
|
||||
private val transactionRepo: TransactionRepo,
|
||||
private val authService: AuthService,
|
||||
private val categorizeService: CategorizeService,
|
||||
private val notificationService: NotificationService,
|
||||
) : TransactionService {
|
||||
private val logger = LoggerFactory.getLogger(this.javaClass)
|
||||
private val serviceScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
||||
|
||||
override fun getTransactions(
|
||||
spaceId: Int,
|
||||
filter: TransactionService.TransactionsFilter,
|
||||
sortBy: String,
|
||||
sortDirection: String
|
||||
): List<Transaction> {
|
||||
val transactions = transactionRepo.findAllBySpaceId(spaceId)
|
||||
val transactions = transactionRepo.findAllBySpaceId(spaceId, filter)
|
||||
return transactions
|
||||
}
|
||||
|
||||
@@ -53,7 +63,17 @@ class TransactionServiceImpl(
|
||||
date = transaction.date,
|
||||
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?) {
|
||||
@@ -85,6 +105,7 @@ class TransactionServiceImpl(
|
||||
transactionId: Int,
|
||||
transaction: TransactionDTO.UpdateTransactionDTO
|
||||
): Int {
|
||||
val userId = authService.getSecurityUserId()
|
||||
val space = spaceService.getSpace(spaceId, null)
|
||||
val existingTransaction = getTransaction(space.id!!, transactionId)
|
||||
val newCategory = transaction.categoryId?.let { categoryService.getCategory(spaceId, it) }
|
||||
@@ -106,16 +127,37 @@ class TransactionServiceImpl(
|
||||
tgChatId = existingTransaction.tgChatId,
|
||||
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)
|
||||
}
|
||||
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) {
|
||||
val userId = authService.getSecurityUserId()
|
||||
val space = spaceService.getSpace(spaceId, null)
|
||||
getTransaction(space.id!!, transactionId)
|
||||
val tx = getTransaction(space.id!!, 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) {
|
||||
|
||||
@@ -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()
|
||||
// }
|
||||
//
|
||||
//
|
||||
//}
|
||||
@@ -4,7 +4,6 @@ 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
|
||||
@@ -12,10 +11,7 @@ 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
|
||||
@@ -46,7 +42,7 @@ class CategorizeService(
|
||||
tx,
|
||||
categoriesRepo.findBySpaceId(job.spaceId)
|
||||
) // тут твой вызов GPT
|
||||
var unsuccessMessage: TelegramBotResult<Message>? = null
|
||||
var message: TelegramBotResult<Message>? = null
|
||||
|
||||
if (res.categoryId == 0) {
|
||||
if (tx.tgChatId != null && tx.tgMessageId != null) {
|
||||
@@ -56,7 +52,7 @@ class CategorizeService(
|
||||
listOf(ReactionType.Emoji("💔")),
|
||||
isBig = false
|
||||
)
|
||||
unsuccessMessage = bot.sendMessage(
|
||||
message = bot.sendMessage(
|
||||
ChatId.fromId(tx.tgChatId),
|
||||
replyToMessageId = tx.tgMessageId,
|
||||
text = "К сожалению, мы не смогли распознать категорию.\n\nПопробуйте выставить ее самостоятельно.",
|
||||
@@ -81,7 +77,7 @@ class CategorizeService(
|
||||
)
|
||||
val category = categoriesRepo.findBySpaceIdAndId(job.spaceId, res.categoryId)
|
||||
if (category != null) {
|
||||
bot.sendMessage(
|
||||
message = bot.sendMessage(
|
||||
ChatId.fromId(tx.tgChatId),
|
||||
replyToMessageId = tx.tgMessageId,
|
||||
text = "Определили категорию: <b>${category.name}</b>.\n\nЕсли это не так, исправьте это в WebApp.",
|
||||
@@ -98,11 +94,11 @@ class CategorizeService(
|
||||
}
|
||||
}
|
||||
}
|
||||
if (unsuccessMessage != null) {
|
||||
if (message != null) {
|
||||
categoryJobRepo.successJob(
|
||||
job.id,
|
||||
unsuccessMessage.get().chat.id,
|
||||
unsuccessMessage.get().messageId
|
||||
message.get().chat.id,
|
||||
message.get().messageId
|
||||
)
|
||||
} else {
|
||||
categoryJobRepo.successJob(job.id)
|
||||
|
||||
@@ -35,8 +35,8 @@ class DeepSeekCategorizationService(
|
||||
|
||||
Задача:
|
||||
1. Определите наиболее подходящую категорию из списка выше для транзакции пользователя.
|
||||
2. Верните ответ в формате: "ID категории", например "3".
|
||||
3. Если ни одна категория из списка не подходит, верните: "0".
|
||||
2. Верните ответ в формате: ID категории", например 3.
|
||||
3. Если ни одна категория из списка не подходит, верните: 0.
|
||||
|
||||
Ответ должен быть кратким, одной строкой, без дополнительных пояснений.
|
||||
""".trimIndent()
|
||||
|
||||
@@ -16,6 +16,7 @@ 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.context.annotation.Lazy
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import space.luminic.finance.dtos.TransactionDTO
|
||||
@@ -30,10 +31,11 @@ import java.time.LocalDate
|
||||
@Service
|
||||
class BotService(
|
||||
@Value("\${telegram.bot.token}") private val botToken: String,
|
||||
@Value("\${spring.profiles.active}") private val profile: String,
|
||||
private val userService: UserService,
|
||||
@Qualifier("spaceServiceTelegram") private val spaceService: SpaceService,
|
||||
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
|
||||
fun bot(): Bot {
|
||||
val bot = com.github.kotlintelegrambot.bot {
|
||||
logLevel = LogLevel.None
|
||||
logLevel = if (profile == "proc") LogLevel.None else LogLevel.All()
|
||||
token = botToken
|
||||
dispatch {
|
||||
message(Filter.Text) {
|
||||
|
||||
@@ -4,5 +4,5 @@ import space.luminic.finance.models.Space
|
||||
|
||||
interface SpaceService {
|
||||
fun getSpaces(userId: Int): List<Space>
|
||||
fun getSpace(spaceId: Int, userId: Int): Space?
|
||||
fun getSpace(spaceId: Int, userId: Int): Space
|
||||
}
|
||||
@@ -14,7 +14,7 @@ class SpaceServiceImpl(
|
||||
return spaces
|
||||
}
|
||||
|
||||
override fun getSpace(spaceId: Int, userId: Int): Space? {
|
||||
override fun getSpace(spaceId: Int, userId: Int): Space {
|
||||
val space =
|
||||
spaceRepo.findSpaceById(spaceId, userId) ?: throw NotFoundException("Space with id $spaceId not found")
|
||||
return space
|
||||
|
||||
@@ -1,18 +1,29 @@
|
||||
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.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
|
||||
import space.luminic.finance.services.NotificationService
|
||||
import space.luminic.finance.services.TxActionType
|
||||
|
||||
@Service("transactionsServiceTelegram")
|
||||
class TransactionsServiceImpl(
|
||||
private val transactionRepo: TransactionRepo,
|
||||
@Qualifier("spaceServiceTelegram") private val spaceService: SpaceService,
|
||||
private val categoryService: CategoryServiceImpl
|
||||
private val categoryService: CategoryServiceImpl,
|
||||
private val notificationService: NotificationService
|
||||
) : TransactionService {
|
||||
private val logger = LoggerFactory.getLogger(this.javaClass)
|
||||
private val serviceScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
||||
|
||||
|
||||
override fun createTransaction(
|
||||
spaceId: Int,
|
||||
@@ -35,10 +46,17 @@ class TransactionsServiceImpl(
|
||||
tgChatId = chatId,
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user