+ google drive
This commit is contained in:
@@ -35,6 +35,14 @@ repositories {
|
|||||||
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
// Excel
|
||||||
|
implementation("org.apache.poi:poi-ooxml:5.2.3")
|
||||||
|
|
||||||
|
// Google API
|
||||||
|
implementation("com.google.api-client:google-api-client:2.4.0")
|
||||||
|
implementation("com.google.apis:google-api-services-drive:v3-rev20240123-2.0.0")
|
||||||
|
implementation("com.google.auth:google-auth-library-oauth2-http:1.23.0")
|
||||||
|
|
||||||
// Spring
|
// Spring
|
||||||
implementation("org.springframework.boot:spring-boot-starter-cache")
|
implementation("org.springframework.boot:spring-boot-starter-cache")
|
||||||
implementation("org.springframework.boot:spring-boot-starter-security")
|
implementation("org.springframework.boot:spring-boot-starter-security")
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import space.luminic.finance.dtos.UserDTO.RegisterUserDTO
|
|||||||
import space.luminic.finance.mappers.UserMapper.toDto
|
import space.luminic.finance.mappers.UserMapper.toDto
|
||||||
import space.luminic.finance.mappers.UserMapper.toTelegramMap
|
import space.luminic.finance.mappers.UserMapper.toTelegramMap
|
||||||
import space.luminic.finance.services.AuthService
|
import space.luminic.finance.services.AuthService
|
||||||
|
import space.luminic.finance.services.GoogleDriveService
|
||||||
|
import space.luminic.finance.services.UserService
|
||||||
import java.net.URLDecoder
|
import java.net.URLDecoder
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
@@ -23,6 +25,8 @@ import javax.crypto.spec.SecretKeySpec
|
|||||||
@RequestMapping("/auth")
|
@RequestMapping("/auth")
|
||||||
class AuthController(
|
class AuthController(
|
||||||
private val authService: AuthService,
|
private val authService: AuthService,
|
||||||
|
private val googleDriveService: GoogleDriveService,
|
||||||
|
private val userService: UserService,
|
||||||
@Value("\${telegram.bot.token}") private val botToken: String
|
@Value("\${telegram.bot.token}") private val botToken: String
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@@ -183,4 +187,18 @@ class AuthController(
|
|||||||
|
|
||||||
return authService.getSecurityUser().toDto()
|
return authService.getSecurityUser().toDto()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/me/google-drive")
|
||||||
|
fun linkGoogleDrive(@RequestBody request: UserDTO.GoogleAuthDTO): Map<String, String> {
|
||||||
|
val user = authService.getSecurityUser()
|
||||||
|
val refreshToken = googleDriveService.exchangeCodeForRefreshToken(request.code)
|
||||||
|
|
||||||
|
if (refreshToken.isNotEmpty()) {
|
||||||
|
user.googleRefreshToken = refreshToken
|
||||||
|
userService.update(user)
|
||||||
|
return mapOf("status" to "success", "message" to "Google Drive linked successfully")
|
||||||
|
} else {
|
||||||
|
throw IllegalArgumentException("Failed to exchange code for refresh token")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
package space.luminic.finance.api
|
package space.luminic.finance.api
|
||||||
|
|
||||||
|
import org.springframework.http.HttpHeaders
|
||||||
|
import org.springframework.http.MediaType
|
||||||
|
import org.springframework.http.ResponseEntity
|
||||||
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType
|
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType
|
||||||
import io.swagger.v3.oas.annotations.security.SecurityScheme
|
import io.swagger.v3.oas.annotations.security.SecurityScheme
|
||||||
import org.springframework.web.bind.annotation.*
|
import org.springframework.web.bind.annotation.*
|
||||||
@@ -26,6 +29,18 @@ class TransactionController (
|
|||||||
return transactionService.getTransactions(spaceId, filter).map { it.toDto() }
|
return transactionService.getTransactions(spaceId, filter).map { it.toDto() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/_export")
|
||||||
|
fun exportExcel(@PathVariable spaceId: Int, @RequestBody filter: TransactionService.TransactionsFilter): ResponseEntity<ByteArray> {
|
||||||
|
val excelBytes = transactionService.generateExcel(spaceId, filter)
|
||||||
|
val headers = HttpHeaders()
|
||||||
|
headers.contentType = MediaType.APPLICATION_OCTET_STREAM
|
||||||
|
headers.setContentDispositionFormData("attachment", "transactions.xlsx")
|
||||||
|
|
||||||
|
return ResponseEntity.ok()
|
||||||
|
.headers(headers)
|
||||||
|
.body(excelBytes)
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/{transactionId}")
|
@GetMapping("/{transactionId}")
|
||||||
fun getTransaction(@PathVariable spaceId: Int, @PathVariable transactionId: Int): TransactionDTO {
|
fun getTransaction(@PathVariable spaceId: Int, @PathVariable transactionId: Int): TransactionDTO {
|
||||||
return transactionService.getTransaction(spaceId, transactionId).toDto()
|
return transactionService.getTransaction(spaceId, transactionId).toDto()
|
||||||
|
|||||||
@@ -48,6 +48,9 @@ data class UserDTO(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
data class GoogleAuthDTO(
|
||||||
|
val code: String
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ data class User(
|
|||||||
@CreatedDate val createdAt: Instant? = null,
|
@CreatedDate val createdAt: Instant? = null,
|
||||||
@LastModifiedDate var updatedAt: Instant? = null,
|
@LastModifiedDate var updatedAt: Instant? = null,
|
||||||
var roles: List<String> = listOf(),
|
var roles: List<String> = listOf(),
|
||||||
|
var googleRefreshToken: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import java.time.LocalDateTime
|
|||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
interface SpaceRepo {
|
interface SpaceRepo {
|
||||||
|
fun findAll(): List<Space>
|
||||||
fun findSpacesForScheduling(lastRun: LocalDateTime): List<Space>
|
fun findSpacesForScheduling(lastRun: LocalDateTime): List<Space>
|
||||||
fun findSpacesAvailableForUser(userId: Int): List<Space>
|
fun findSpacesAvailableForUser(userId: Int): List<Space>
|
||||||
fun findSpaceById(id: Int, userId: Int): Space?
|
fun findSpaceById(id: Int, userId: Int): Space?
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ class SpaceRepoImpl(
|
|||||||
username = rs.getString("username"),
|
username = rs.getString("username"),
|
||||||
firstName = rs.getString("first_name"),
|
firstName = rs.getString("first_name"),
|
||||||
password = rs.getString("password"),
|
password = rs.getString("password"),
|
||||||
|
googleRefreshToken = rs.getString("google_refresh_token")
|
||||||
),
|
),
|
||||||
participants = userRepo.findParticipantsBySpace(rs.getInt("id")).toSet(),
|
participants = userRepo.findParticipantsBySpace(rs.getInt("id")).toSet(),
|
||||||
isDeleted = rs.getBoolean("is_deleted"),
|
isDeleted = rs.getBoolean("is_deleted"),
|
||||||
@@ -85,6 +86,21 @@ class SpaceRepoImpl(
|
|||||||
return spaceMap.map { it.value }
|
return spaceMap.map { it.value }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun findAll(): List<Space> {
|
||||||
|
val sql = """
|
||||||
|
select s.*,
|
||||||
|
u.id as owner_id,
|
||||||
|
u.username as username,
|
||||||
|
u.first_name as first_name,
|
||||||
|
u.password as password,
|
||||||
|
u.google_refresh_token as google_refresh_token
|
||||||
|
from finance.spaces s
|
||||||
|
join finance.users u on u.id = s.owner_id
|
||||||
|
where s.is_deleted = false
|
||||||
|
""".trimIndent()
|
||||||
|
return jdbcTemplate.query(sql, spaceRowMapper())
|
||||||
|
}
|
||||||
|
|
||||||
override fun findSpacesForScheduling(lastRun: LocalDateTime): List<Space> {
|
override fun findSpacesForScheduling(lastRun: LocalDateTime): List<Space> {
|
||||||
val sql = """
|
val sql = """
|
||||||
select s.id as s_id,
|
select s.id as s_id,
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ class UserRepoImpl(
|
|||||||
regDate = rs.getDate("reg_date").toLocalDate(),
|
regDate = rs.getDate("reg_date").toLocalDate(),
|
||||||
createdAt = rs.getTimestamp("created_at").toInstant(),
|
createdAt = rs.getTimestamp("created_at").toInstant(),
|
||||||
updatedAt = rs.getTimestamp("updated_at").toInstant(),
|
updatedAt = rs.getTimestamp("updated_at").toInstant(),
|
||||||
roles = (rs.getArray("roles")?.array as? Array<String>)?.toList() ?: emptyList()
|
roles = (rs.getArray("roles")?.array as? Array<String>)?.toList() ?: emptyList(),
|
||||||
|
googleRefreshToken = rs.getString("google_refresh_token")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,7 +59,7 @@ class UserRepoImpl(
|
|||||||
|
|
||||||
override fun create(user: User): User {
|
override fun create(user: User): User {
|
||||||
val sql =
|
val sql =
|
||||||
"insert into finance.users(username, first_name, tg_id, tg_user_name, photo_url, password, is_active, reg_date) values (:username, :firstname, :tg_id, :tg_user_name, :photo_url, :password, :isActive, :regDate) returning ID"
|
"insert into finance.users(username, first_name, tg_id, tg_user_name, photo_url, password, is_active, reg_date, google_refresh_token) values (:username, :firstname, :tg_id, :tg_user_name, :photo_url, :password, :isActive, :regDate, :googleRefreshToken) returning ID"
|
||||||
val params = mapOf(
|
val params = mapOf(
|
||||||
"username" to user.username,
|
"username" to user.username,
|
||||||
"firstname" to user.firstName,
|
"firstname" to user.firstName,
|
||||||
@@ -68,6 +69,7 @@ class UserRepoImpl(
|
|||||||
"password" to user.password,
|
"password" to user.password,
|
||||||
"isActive" to user.isActive,
|
"isActive" to user.isActive,
|
||||||
"regDate" to user.regDate,
|
"regDate" to user.regDate,
|
||||||
|
"googleRefreshToken" to user.googleRefreshToken
|
||||||
)
|
)
|
||||||
val savedId = jdbcTemplate.queryForObject(sql, params, Int::class.java)
|
val savedId = jdbcTemplate.queryForObject(sql, params, Int::class.java)
|
||||||
user.id = savedId
|
user.id = savedId
|
||||||
@@ -75,7 +77,33 @@ class UserRepoImpl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun update(user: User): User {
|
override fun update(user: User): User {
|
||||||
TODO("Not yet implemented")
|
val sql = """
|
||||||
|
update finance.users
|
||||||
|
set username = :username,
|
||||||
|
first_name = :firstname,
|
||||||
|
tg_id = :tg_id,
|
||||||
|
tg_user_name = :tg_user_name,
|
||||||
|
photo_url = :photo_url,
|
||||||
|
password = :password,
|
||||||
|
is_active = :isActive,
|
||||||
|
reg_date = :regDate,
|
||||||
|
google_refresh_token = :googleRefreshToken
|
||||||
|
where id = :id
|
||||||
|
""".trimIndent()
|
||||||
|
val params = mapOf(
|
||||||
|
"id" to user.id,
|
||||||
|
"username" to user.username,
|
||||||
|
"firstname" to user.firstName,
|
||||||
|
"tg_id" to user.tgId,
|
||||||
|
"tg_user_name" to user.tgUserName,
|
||||||
|
"photo_url" to user.photoUrl,
|
||||||
|
"password" to user.password,
|
||||||
|
"isActive" to user.isActive,
|
||||||
|
"regDate" to user.regDate,
|
||||||
|
"googleRefreshToken" to user.googleRefreshToken
|
||||||
|
)
|
||||||
|
jdbcTemplate.update(sql, params)
|
||||||
|
return user
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun deleteById(id: Long) {
|
override fun deleteById(id: Long) {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import space.luminic.finance.models.Transaction
|
|||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
class NotificationServiceImpl(private val userService: UserService, private val bot: Bot,) : NotificationService {
|
class NotificationServiceImpl(private val userService: UserService, private val bot: Bot) : NotificationService {
|
||||||
private val logger = LoggerFactory.getLogger(this.javaClass)
|
private val logger = LoggerFactory.getLogger(this.javaClass)
|
||||||
|
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ class NotificationServiceImpl(private val userService: UserService, private val
|
|||||||
listOf(
|
listOf(
|
||||||
InlineKeyboardButton.WebApp(
|
InlineKeyboardButton.WebApp(
|
||||||
"Открыть в WebApp",
|
"Открыть в WebApp",
|
||||||
WebAppInfo("https://app.luminic.space/transactions")
|
WebAppInfo("https://app.luminic.space/transactions?mode=from_bot")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -113,10 +113,11 @@ class NotificationServiceImpl(private val userService: UserService, private val
|
|||||||
}"
|
}"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (changes.isNotEmpty()) {
|
||||||
var text = "${user.firstName} обновил транзакцию ${tx.comment}\n\n"
|
var text = "${user.firstName} обновил транзакцию ${tx.comment}\n\n"
|
||||||
text += changes.joinToString("\n") { it }
|
text += changes.joinToString("\n") { it }
|
||||||
space.owner.tgId?.let { sendTextMessage(it, text, createWebAppButton(space.id, tx.id)) }
|
space.owner.tgId?.let { sendTextMessage(it, text, createWebAppButton(space.id, tx.id)) }
|
||||||
|
}
|
||||||
} ?: logger.warn("No tx2 provided when update")
|
} ?: logger.warn("No tx2 provided when update")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,11 @@ interface TransactionService {
|
|||||||
filter: TransactionsFilter
|
filter: TransactionsFilter
|
||||||
): List<Transaction>
|
): List<Transaction>
|
||||||
|
|
||||||
|
fun generateExcel(
|
||||||
|
spaceId: Int,
|
||||||
|
filter: TransactionsFilter
|
||||||
|
): ByteArray
|
||||||
|
|
||||||
fun getTransaction(spaceId: Int, transactionId: Int): Transaction
|
fun getTransaction(spaceId: Int, transactionId: Int): Transaction
|
||||||
fun createTransaction(spaceId: Int, transaction: TransactionDTO.CreateTransactionDTO): Int
|
fun createTransaction(spaceId: Int, transaction: TransactionDTO.CreateTransactionDTO): Int
|
||||||
fun batchCreate(spaceId: Int, transactions: List<TransactionDTO.CreateTransactionDTO>, createdById: Int?)
|
fun batchCreate(spaceId: Int, transactions: List<TransactionDTO.CreateTransactionDTO>, createdById: Int?)
|
||||||
|
|||||||
@@ -11,8 +11,10 @@ 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.io.ByteArrayOutputStream
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
|
import org.apache.poi.xssf.usermodel.XSSFWorkbook
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
class TransactionServiceImpl(
|
class TransactionServiceImpl(
|
||||||
@@ -34,6 +36,45 @@ class TransactionServiceImpl(
|
|||||||
return transactions
|
return transactions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun generateExcel(
|
||||||
|
spaceId: Int,
|
||||||
|
filter: TransactionService.TransactionsFilter
|
||||||
|
): ByteArray {
|
||||||
|
val transactions = getTransactions(spaceId, filter)
|
||||||
|
val workbook = XSSFWorkbook()
|
||||||
|
val sheet = workbook.createSheet("Transactions")
|
||||||
|
|
||||||
|
val headerRow = sheet.createRow(0)
|
||||||
|
headerRow.createCell(0).setCellValue("ID")
|
||||||
|
headerRow.createCell(1).setCellValue("Date")
|
||||||
|
headerRow.createCell(2).setCellValue("Type")
|
||||||
|
headerRow.createCell(3).setCellValue("Kind")
|
||||||
|
headerRow.createCell(4).setCellValue("Category")
|
||||||
|
headerRow.createCell(5).setCellValue("Amount")
|
||||||
|
headerRow.createCell(6).setCellValue("Comment")
|
||||||
|
|
||||||
|
var rowNum = 1
|
||||||
|
for (transaction in transactions) {
|
||||||
|
val row = sheet.createRow(rowNum++)
|
||||||
|
row.createCell(0).setCellValue(transaction.id?.toDouble() ?: 0.0)
|
||||||
|
row.createCell(1).setCellValue(transaction.date.toString())
|
||||||
|
row.createCell(2).setCellValue(transaction.type.displayName)
|
||||||
|
row.createCell(3).setCellValue(transaction.kind.displayName)
|
||||||
|
row.createCell(4).setCellValue(transaction.category?.name ?: "")
|
||||||
|
row.createCell(5).setCellValue(transaction.amount.toDouble())
|
||||||
|
row.createCell(6).setCellValue(transaction.comment)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i in 0..6) {
|
||||||
|
sheet.autoSizeColumn(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
val outputStream = ByteArrayOutputStream()
|
||||||
|
workbook.write(outputStream)
|
||||||
|
workbook.close()
|
||||||
|
return outputStream.toByteArray()
|
||||||
|
}
|
||||||
|
|
||||||
override fun getTransaction(
|
override fun getTransaction(
|
||||||
spaceId: Int,
|
spaceId: Int,
|
||||||
transactionId: Int
|
transactionId: Int
|
||||||
@@ -112,7 +153,7 @@ class TransactionServiceImpl(
|
|||||||
val newKind = if (
|
val newKind = if (
|
||||||
!existingTransaction.isDone &&
|
!existingTransaction.isDone &&
|
||||||
transaction.isDone &&
|
transaction.isDone &&
|
||||||
(today.isAfter(transaction.date) || today.isEqual(transaction.date))
|
(today.isAfter(transaction.date) || today.isEqual(transaction.date) || today.isBefore(transaction.date))
|
||||||
) Transaction.TransactionKind.INSTANT else transaction.kind
|
) Transaction.TransactionKind.INSTANT else transaction.kind
|
||||||
val updatedTransaction = Transaction(
|
val updatedTransaction = Transaction(
|
||||||
id = existingTransaction.id,
|
id = existingTransaction.id,
|
||||||
|
|||||||
@@ -40,4 +40,7 @@ class UserService(val userRepo: UserRepo) {
|
|||||||
return userRepo.findAll()
|
return userRepo.findAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun update(user: User): User {
|
||||||
|
return userRepo.update(user)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -12,6 +12,7 @@ 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.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
|
import space.luminic.finance.models.Category
|
||||||
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
|
||||||
@@ -38,9 +39,18 @@ class CategorizeService(
|
|||||||
runCatching {
|
runCatching {
|
||||||
val tx = transactionRepo.findBySpaceIdAndId(job.spaceId, job.txId)
|
val tx = transactionRepo.findBySpaceIdAndId(job.spaceId, job.txId)
|
||||||
?: throw IllegalArgumentException("Transaction ${job.txId} not found")
|
?: throw IllegalArgumentException("Transaction ${job.txId} not found")
|
||||||
|
|
||||||
|
val categories = categoriesRepo.findBySpaceId(job.spaceId).filter {
|
||||||
|
it.type == when (tx.type) {
|
||||||
|
Transaction.TransactionType.INCOME -> Category.CategoryType.INCOME
|
||||||
|
Transaction.TransactionType.EXPENSE -> Category.CategoryType.EXPENSE
|
||||||
|
else -> it.type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val res = gpt.suggestCategory(
|
val res = gpt.suggestCategory(
|
||||||
tx,
|
tx,
|
||||||
categoriesRepo.findBySpaceId(job.spaceId)
|
categories
|
||||||
) // тут твой вызов GPT
|
) // тут твой вызов GPT
|
||||||
var message: TelegramBotResult<Message>? = null
|
var message: TelegramBotResult<Message>? = null
|
||||||
|
|
||||||
@@ -60,7 +70,7 @@ class CategorizeService(
|
|||||||
listOf(
|
listOf(
|
||||||
InlineKeyboardButton.WebApp(
|
InlineKeyboardButton.WebApp(
|
||||||
"Открыть в WebApp",
|
"Открыть в WebApp",
|
||||||
WebAppInfo("https://app.luminic.space/transactions/${tx.id}/edit")
|
WebAppInfo("https://app.luminic.space/transactions/${tx.id}/edit?mode=from_bot")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -85,7 +95,7 @@ class CategorizeService(
|
|||||||
listOf(
|
listOf(
|
||||||
InlineKeyboardButton.WebApp(
|
InlineKeyboardButton.WebApp(
|
||||||
"Открыть в WebApp",
|
"Открыть в WebApp",
|
||||||
WebAppInfo("https://app.luminic.space/transactions/${tx.id}/edit")
|
WebAppInfo("https://app.luminic.space/transactions/${tx.id}/edit?mode=from_bot"),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
@@ -131,7 +141,7 @@ class CategorizeService(
|
|||||||
listOf(
|
listOf(
|
||||||
InlineKeyboardButton.WebApp(
|
InlineKeyboardButton.WebApp(
|
||||||
"Открыть в WebApp",
|
"Открыть в WebApp",
|
||||||
WebAppInfo("https://app.luminic.space/transactions/${tx.id}/edit")
|
WebAppInfo("https://app.luminic.space/transactions/${tx.id}/edit?mode=from_bot"),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -34,10 +34,10 @@ class DeepSeekCategorizationService(
|
|||||||
override fun suggestCategory(tx: Transaction, categories: List<Category>): CategorySuggestion {
|
override fun suggestCategory(tx: Transaction, categories: List<Category>): CategorySuggestion {
|
||||||
val catList = categories.joinToString("\n") { "- ${it.id}: ${it.name}" }
|
val catList = categories.joinToString("\n") { "- ${it.id}: ${it.name}" }
|
||||||
val txInfo = """
|
val txInfo = """
|
||||||
{ \"amount\": ${tx.amount}, \"comment\": \"${tx.comment}\", \"date\":${tx.date}\" }
|
{ \"type\": \"${tx.type.displayName}\", \"amount\": ${tx.amount}, \"comment\": \"${tx.comment}\", \"date\":${tx.date}\" }
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
val prompt = """
|
val prompt = """
|
||||||
Пользователь имеет следующие категории:
|
Пользователь имеет следующие категории (${tx.type.displayName}):
|
||||||
$catList
|
$catList
|
||||||
|
|
||||||
Задача:
|
Задача:
|
||||||
@@ -88,7 +88,7 @@ class DeepSeekCategorizationService(
|
|||||||
override fun analyzePeriod(startDate: LocalDate, endDate: LocalDate, dashboardData: DashboardData): String {
|
override fun analyzePeriod(startDate: LocalDate, endDate: LocalDate, dashboardData: DashboardData): String {
|
||||||
mapper.registerModule(JavaTimeModule());
|
mapper.registerModule(JavaTimeModule());
|
||||||
if (dashboardData.totalIncome == 0 && dashboardData.totalExpense == 0){
|
if (dashboardData.totalIncome == 0 && dashboardData.totalExpense == 0){
|
||||||
return "Начните записывать траты и поступления и мы начнем их анализировать!"
|
return """{ "common": "Начните записывать траты и поступления и мы начнем их анализировать!", "categoryAnalysis": "", "keyInsights": "", "recommendations": "" }"""
|
||||||
} else {
|
} else {
|
||||||
var prompt = """
|
var prompt = """
|
||||||
You are a personal finance analyst.
|
You are a personal finance analyst.
|
||||||
|
|||||||
@@ -22,19 +22,19 @@ class QwenCategorizationService(
|
|||||||
private val mapper = jacksonObjectMapper()
|
private val mapper = jacksonObjectMapper()
|
||||||
private val client = OkHttpClient()
|
private val client = OkHttpClient()
|
||||||
private val logger = LoggerFactory.getLogger(javaClass)
|
private val logger = LoggerFactory.getLogger(javaClass)
|
||||||
|
override fun suggestCategory(tx: Transaction, categories: List<Category>): CategorySuggestion {
|
||||||
override fun suggestCategory(tx: Transaction, categories: List<Category>): CategorySuggestion {
|
|
||||||
val catList = categories.joinToString("\n") { "- ${it.id}: ${it.name}" }
|
val catList = categories.joinToString("\n") { "- ${it.id}: ${it.name}" }
|
||||||
val txInfo = """
|
val txInfo = """
|
||||||
{ \"amount\": ${tx.amount}, \"comment\": \"${tx.comment}\", \"date\":${tx.date}\" }
|
{ \"type\": \"${tx.type.displayName}\", \"amount\": ${tx.amount}, \"comment\": \"${tx.comment}\", \"date\":${tx.date}\" }
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
val prompt = """
|
val prompt = """
|
||||||
Пользователь имеет следующие категории:
|
Пользователь имеет следующие категории (${tx.type.displayName}):
|
||||||
$catList
|
$catList
|
||||||
|
|
||||||
Задача:
|
Задача:
|
||||||
1. Определите наиболее подходящую категорию из списка выше для транзакции пользователя.
|
1. Определите наиболее подходящую категорию из списка выше для транзакции пользователя.
|
||||||
2. Верните ответ в формате: "ID категории – имя категории (вероятность)", например "3 – Продукты (0.87)".
|
2. Верните ответ в формате: "ID категории – имя категории (вероятность)", например "3 – Продукты (0.87)".
|
||||||
|
...
|
||||||
3. Если ни одна категория из списка не подходит, верните: "0 – Другое (вероятность)".
|
3. Если ни одна категория из списка не подходит, верните: "0 – Другое (вероятность)".
|
||||||
|
|
||||||
Ответ должен быть кратким, одной строкой, без дополнительных пояснений.
|
Ответ должен быть кратким, одной строкой, без дополнительных пояснений.
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ class BotService(
|
|||||||
listOf(
|
listOf(
|
||||||
InlineKeyboardButton.WebApp(
|
InlineKeyboardButton.WebApp(
|
||||||
text = "Открыть WebApp",
|
text = "Открыть WebApp",
|
||||||
webApp = WebAppInfo(url = "https://app.luminic.space")
|
webApp = WebAppInfo(url = "https://app.luminic.space?mode=from_bot")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -131,11 +131,18 @@ class BotService(
|
|||||||
when (state?.state) {
|
when (state?.state) {
|
||||||
State.StateCode.SPACE_SELECTED -> {
|
State.StateCode.SPACE_SELECTED -> {
|
||||||
try {
|
try {
|
||||||
val parts = message.text!!.trim().split(" ", limit = 2)
|
var text = message.text!!.trim()
|
||||||
if (parts.isEmpty()) {
|
var type = Transaction.TransactionType.EXPENSE
|
||||||
|
if (text.startsWith("+")) {
|
||||||
|
type = Transaction.TransactionType.INCOME
|
||||||
|
text = text.substring(1).trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
val parts = text.split(" ", limit = 2)
|
||||||
|
if (parts.isEmpty() || parts[0].isEmpty()) {
|
||||||
bot.sendMessage(
|
bot.sendMessage(
|
||||||
chatId = ChatId.fromId(message.chat.id),
|
chatId = ChatId.fromId(message.chat.id),
|
||||||
text = "Введите сумму и комментарий, например: `250 обед`",
|
text = "Введите сумму и комментарий, например: `250 обед` или `+1000 зп` ",
|
||||||
parseMode = ParseMode.MARKDOWN
|
parseMode = ParseMode.MARKDOWN
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -161,7 +168,7 @@ class BotService(
|
|||||||
?: throw IllegalArgumentException("selected space is empty"),
|
?: throw IllegalArgumentException("selected space is empty"),
|
||||||
user.id!!,
|
user.id!!,
|
||||||
TransactionDTO.CreateTransactionDTO(
|
TransactionDTO.CreateTransactionDTO(
|
||||||
Transaction.TransactionType.EXPENSE,
|
type,
|
||||||
Transaction.TransactionKind.INSTANT,
|
Transaction.TransactionKind.INSTANT,
|
||||||
comment = comment,
|
comment = comment,
|
||||||
amount = amount.toBigDecimal(),
|
amount = amount.toBigDecimal(),
|
||||||
@@ -203,7 +210,7 @@ class BotService(
|
|||||||
bot.editMessageText(
|
bot.editMessageText(
|
||||||
chatId = ChatId.Companion.fromId(callbackQuery.message!!.chat.id),
|
chatId = ChatId.Companion.fromId(callbackQuery.message!!.chat.id),
|
||||||
messageId = callbackQuery.message!!.messageId,
|
messageId = callbackQuery.message!!.messageId,
|
||||||
text = "Успешно!\n\nМы готовы принимать Ваши транзакции.\n\nПросто пишите их в формате:\n\n <i>сумма комментарий</i>\n\n <b>Первой обязательно должна быть сумма!</b>",
|
text = "Успешно!\n\nМы готовы принимать Ваши транзакции.\n\nПросто пишите их в формате:\n\n <i>сумма комментарий</i> (расходы)\n\n <i>+сумма комментарий</i> (пополнения)\n\n <b>Первой обязательно должна быть сумма!</b>",
|
||||||
parseMode = ParseMode.HTML,
|
parseMode = ParseMode.HTML,
|
||||||
replyMarkup = buildMenu(callbackQuery.from.id)
|
replyMarkup = buildMenu(callbackQuery.from.id)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ package space.luminic.finance.services.telegram
|
|||||||
|
|
||||||
import space.luminic.finance.dtos.TransactionDTO
|
import space.luminic.finance.dtos.TransactionDTO
|
||||||
|
|
||||||
interface TransactionService {
|
interface
|
||||||
|
TransactionService {
|
||||||
|
|
||||||
fun createTransaction(spaceId: Int, userId: Int, transaction: TransactionDTO.CreateTransactionDTO, chatId: Long, messageId: Long ): Int
|
fun createTransaction(spaceId: Int, userId: Int, transaction: TransactionDTO.CreateTransactionDTO, chatId: Long, messageId: Long ): Int
|
||||||
}
|
}
|
||||||
@@ -35,4 +35,7 @@ spring.flyway.schemas=finance
|
|||||||
spring.jpa.properties.hibernate.default_schema=finance
|
spring.jpa.properties.hibernate.default_schema=finance
|
||||||
spring.jpa.properties.hibernate.default_batch_fetch_size=50
|
spring.jpa.properties.hibernate.default_batch_fetch_size=50
|
||||||
qwen.api_key=sk-991942d15b424cc89513498bb2946045
|
qwen.api_key=sk-991942d15b424cc89513498bb2946045
|
||||||
ds.api_key=sk-b5949728e79747f08af0a1d65bc6a7a2
|
ds.api_key=sk-b5949728e79747f08af0a1d65bc6a7a2\ngoogle.client-id=\ngoogle.client-secret=\ngoogle.redirect-uri=
|
||||||
|
google.client-id=112729998586-q39qsptu67lqeej0356m01e1ghptuajk.apps.googleusercontent.com
|
||||||
|
google.client-secret=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
|
||||||
|
google.redirect-uri=https://app.luminic.space
|
||||||
Reference in New Issue
Block a user