Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2452e5935f |
8
.idea/.gitignore
generated
vendored
8
.idea/.gitignore
generated
vendored
@@ -1,8 +0,0 @@
|
|||||||
# Default ignored files
|
|
||||||
/shelf/
|
|
||||||
/workspace.xml
|
|
||||||
# Editor-based HTTP Client requests
|
|
||||||
/httpRequests/
|
|
||||||
# Datasource local storage ignored files
|
|
||||||
/dataSources/
|
|
||||||
/dataSources.local.xml
|
|
||||||
@@ -35,14 +35,6 @@ 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")
|
||||||
@@ -76,7 +68,6 @@ dependencies {
|
|||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.0")
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.0")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:1.7.3")
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:1.7.3")
|
||||||
implementation("org.jetbrains.kotlin.plugin.jpa:org.jetbrains.kotlin.plugin.jpa.gradle.plugin:1.9.25")
|
implementation("org.jetbrains.kotlin.plugin.jpa:org.jetbrains.kotlin.plugin.jpa.gradle.plugin:1.9.25")
|
||||||
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.17.0")
|
|
||||||
|
|
||||||
implementation("io.jsonwebtoken:jjwt-api:0.11.5")
|
implementation("io.jsonwebtoken:jjwt-api:0.11.5")
|
||||||
implementation("io.jsonwebtoken:jjwt-impl:0.11.5")
|
implementation("io.jsonwebtoken:jjwt-impl:0.11.5")
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
./gradlew bootJar || exit 1
|
./gradlew bootJar || exit 1
|
||||||
|
|
||||||
scp build/libs/luminic-space-v2.jar root@31.59.58.220:/root/luminic/app/back
|
scp build/libs/luminic-space-v2.jar root@213.226.71.138:/root/luminic/space/back
|
||||||
|
|
||||||
ssh root@31.59.58.220 "
|
ssh root@213.226.71.138 "
|
||||||
cd /root/luminic/app/back &&
|
cd /root/luminic/space/back &&
|
||||||
docker compose up -d --build &&
|
docker compose up -d --build &&
|
||||||
docker restart back-app-1
|
docker restart back-app-1
|
||||||
"
|
"
|
||||||
@@ -13,8 +13,6 @@ 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
|
||||||
@@ -25,8 +23,6 @@ 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
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@@ -187,18 +183,4 @@ 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,35 +0,0 @@
|
|||||||
package space.luminic.finance.api
|
|
||||||
|
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType
|
|
||||||
import io.swagger.v3.oas.annotations.security.SecurityScheme
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping
|
|
||||||
import org.springframework.web.bind.annotation.PathVariable
|
|
||||||
import org.springframework.web.bind.annotation.RequestMapping
|
|
||||||
import org.springframework.web.bind.annotation.RequestParam
|
|
||||||
import org.springframework.web.bind.annotation.RestController
|
|
||||||
import space.luminic.finance.dtos.DashboardDataDTO
|
|
||||||
import space.luminic.finance.mappers.DashboardDataMapper.toDto
|
|
||||||
import space.luminic.finance.models.DashboardData
|
|
||||||
import space.luminic.finance.services.DashboardService
|
|
||||||
import java.time.LocalDate
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/spaces/{spaceId}/dashboard")
|
|
||||||
@SecurityScheme(
|
|
||||||
name = "bearerAuth",
|
|
||||||
type = SecuritySchemeType.HTTP,
|
|
||||||
bearerFormat = "JWT",
|
|
||||||
scheme = "bearer"
|
|
||||||
)
|
|
||||||
class DashboardController(private val dashboardService: DashboardService) {
|
|
||||||
@GetMapping
|
|
||||||
fun getDashboardData(
|
|
||||||
@PathVariable spaceId: Int,
|
|
||||||
@RequestParam startDate: LocalDate,
|
|
||||||
@RequestParam endDate: LocalDate
|
|
||||||
): DashboardDataDTO {
|
|
||||||
return dashboardService.getDashboardData(spaceId, startDate, endDate).toDto()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
package space.luminic.finance.api
|
|
||||||
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping
|
|
||||||
import org.springframework.web.bind.annotation.PathVariable
|
|
||||||
import org.springframework.web.bind.annotation.RequestMapping
|
|
||||||
import org.springframework.web.bind.annotation.RestController
|
|
||||||
import space.luminic.finance.dtos.GoalDTO
|
|
||||||
import space.luminic.finance.mappers.GoalMapper.toDto
|
|
||||||
import space.luminic.finance.services.GoalService
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/spaces/{spaceId}/goals")
|
|
||||||
class GoalController(private val goalService: GoalService) {
|
|
||||||
|
|
||||||
@GetMapping
|
|
||||||
fun findAll(@PathVariable spaceId: Int): List<GoalDTO> {
|
|
||||||
return goalService.findAllBySpaceId(spaceId).map { it.toDto() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package space.luminic.finance.api
|
||||||
|
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping
|
||||||
|
import org.springframework.web.bind.annotation.RestController
|
||||||
|
import space.luminic.finance.dtos.TargetDTO
|
||||||
|
import space.luminic.finance.mappers.TargetMapper.toDto
|
||||||
|
import space.luminic.finance.services.TargetService
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/spaces/{spaceId}/targets")
|
||||||
|
class TargetController(private val targetService: TargetService) {
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
fun findAll(@PathVariable spaceId: Int): List<TargetDTO> {
|
||||||
|
return targetService.findAllBySpaceId(spaceId).map { it.toDto() }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,5 @@
|
|||||||
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,19 +23,7 @@ class TransactionController (
|
|||||||
|
|
||||||
@PostMapping("/_search")
|
@PostMapping("/_search")
|
||||||
fun getTransactions(@PathVariable spaceId: Int, @RequestBody filter: TransactionService.TransactionsFilter) : List<TransactionDTO>{
|
fun getTransactions(@PathVariable spaceId: Int, @RequestBody filter: TransactionService.TransactionsFilter) : List<TransactionDTO>{
|
||||||
return transactionService.getTransactions(spaceId, filter).map { it.toDto() }
|
return transactionService.getTransactions(spaceId, filter,"date", "DESC").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}")
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ class SecurityConfig(
|
|||||||
@Bean
|
@Bean
|
||||||
fun corsConfigurationSource(): CorsConfigurationSource {
|
fun corsConfigurationSource(): CorsConfigurationSource {
|
||||||
val cors = CorsConfiguration().apply {
|
val cors = CorsConfiguration().apply {
|
||||||
allowedOrigins = listOf("https://app.luminic.space", "http://localhost:5173", "http://localhost:5174")
|
allowedOrigins = listOf("https://app.luminic.space", "http://localhost:5173")
|
||||||
allowedMethods = listOf("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH")
|
allowedMethods = listOf("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH")
|
||||||
allowedHeaders = listOf("*")
|
allowedHeaders = listOf("*")
|
||||||
allowCredentials = true
|
allowCredentials = true
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
package space.luminic.finance.dtos
|
|
||||||
|
|
||||||
import space.luminic.finance.models.AISummaryData
|
|
||||||
import java.time.LocalDate
|
|
||||||
|
|
||||||
data class DashboardDataDTO(
|
|
||||||
val analyzedText: AISummaryData? = null,
|
|
||||||
val totalExpense: Int,
|
|
||||||
val totalIncome: Int,
|
|
||||||
val balance: Int,
|
|
||||||
val categories: List<DashboardCategoryDTO>,
|
|
||||||
val upcomingTransactions: List<TransactionDTO>,
|
|
||||||
val recentTransactions: List<TransactionDTO>,
|
|
||||||
val weeks: List<DashboardWeeksDTO>
|
|
||||||
)
|
|
||||||
|
|
||||||
data class DashboardWeeksDTO(
|
|
||||||
val startDate: LocalDate,
|
|
||||||
val endDate: LocalDate,
|
|
||||||
val expenseSum: Int,
|
|
||||||
val categories: List<WeekCategoryDTO>
|
|
||||||
)
|
|
||||||
|
|
||||||
data class WeekCategoryDTO(
|
|
||||||
val categoryId: Int?,
|
|
||||||
val categoryName: String?,
|
|
||||||
val categoryIcon: String?,
|
|
||||||
val sum: Int? = 0
|
|
||||||
)
|
|
||||||
data class DashboardCategoryDTO (
|
|
||||||
val category: CategoryDTO,
|
|
||||||
val currentPeriodAmount: Int,
|
|
||||||
val previousPeriodAmount: Int,
|
|
||||||
val changeDiff: Double,
|
|
||||||
val changeDiffPercentage: Double,
|
|
||||||
)
|
|
||||||
@@ -8,7 +8,6 @@ data class SpaceDTO(
|
|||||||
val name: String,
|
val name: String,
|
||||||
val owner: UserDTO,
|
val owner: UserDTO,
|
||||||
val participants: Set<UserDTO> = emptySet(),
|
val participants: Set<UserDTO> = emptySet(),
|
||||||
val isGoogleDriveConnected: Boolean = false,
|
|
||||||
val createdBy: UserDTO? = null,
|
val createdBy: UserDTO? = null,
|
||||||
val createdAt: Instant,
|
val createdAt: Instant,
|
||||||
var updatedBy: UserDTO? = null,
|
var updatedBy: UserDTO? = null,
|
||||||
|
|||||||
@@ -1,36 +1,37 @@
|
|||||||
package space.luminic.finance.dtos
|
package space.luminic.finance.dtos
|
||||||
|
|
||||||
import space.luminic.finance.models.Goal
|
import space.luminic.finance.models.Target
|
||||||
import space.luminic.finance.models.Goal.GoalType
|
import space.luminic.finance.models.Target.TargetType
|
||||||
import space.luminic.finance.models.Transaction
|
import space.luminic.finance.models.Transaction
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
|
|
||||||
data class GoalDTO(
|
data class TargetDTO(
|
||||||
val id: Int,
|
val id: Int,
|
||||||
val type: GoalType,
|
val type: TargetType,
|
||||||
val name: String,
|
val name: String,
|
||||||
val description: String? = null,
|
val description: String? = null,
|
||||||
val amount: BigDecimal,
|
val amount: BigDecimal,
|
||||||
|
val currentAmount: BigDecimal,
|
||||||
val date: LocalDate,
|
val date: LocalDate,
|
||||||
val components: List<Goal.GoalComponent>,
|
val components: List<Target.TargetComponent>,
|
||||||
val transactions: List<Transaction>,
|
val transactions: List<Transaction>,
|
||||||
val createdBy: UserDTO,
|
val createdBy: UserDTO,
|
||||||
val createdAt: Instant,
|
val createdAt: Instant,
|
||||||
val updatedBy: UserDTO? = null,
|
val updatedBy: UserDTO? = null,
|
||||||
val updatedAt: Instant? = null,
|
val updatedAt: Instant? = null,
|
||||||
) {
|
) {
|
||||||
data class CreateGoalDTO(
|
data class CreateTargetDTO(
|
||||||
val type: GoalType,
|
val type: TargetType,
|
||||||
val name: String,
|
val name: String,
|
||||||
val description: String?,
|
val description: String?,
|
||||||
val amount: BigDecimal,
|
val amount: BigDecimal,
|
||||||
val date: LocalDate
|
val date: LocalDate
|
||||||
)
|
)
|
||||||
|
|
||||||
data class UpdateGoalDTO(
|
data class UpdateTargetDTO(
|
||||||
val type: GoalType,
|
val type: TargetType,
|
||||||
val name: String,
|
val name: String,
|
||||||
val description: String?,
|
val description: String?,
|
||||||
val amount: BigDecimal,
|
val amount: BigDecimal,
|
||||||
@@ -9,8 +9,7 @@ data class UserDTO(
|
|||||||
var tgId: Long? = null,
|
var tgId: Long? = null,
|
||||||
var tgUserName: String? = null,
|
var tgUserName: String? = null,
|
||||||
var photoUrl: String? = null,
|
var photoUrl: String? = null,
|
||||||
var roles: List<String>,
|
var roles: List<String>
|
||||||
var isGoogleDriveConnected: Boolean = false
|
|
||||||
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@@ -49,9 +48,6 @@ data class UserDTO(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
data class GoogleAuthDTO(
|
|
||||||
val code: String
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
package space.luminic.finance.mappers
|
|
||||||
|
|
||||||
import space.luminic.finance.dtos.DashboardCategoryDTO
|
|
||||||
import space.luminic.finance.dtos.DashboardDataDTO
|
|
||||||
import space.luminic.finance.dtos.DashboardWeeksDTO
|
|
||||||
import space.luminic.finance.dtos.WeekCategoryDTO
|
|
||||||
import space.luminic.finance.mappers.CategoryMapper.toDto
|
|
||||||
import space.luminic.finance.mappers.TransactionMapper.toDto
|
|
||||||
import space.luminic.finance.models.DashboardCategory
|
|
||||||
import space.luminic.finance.models.DashboardData
|
|
||||||
import space.luminic.finance.models.DashboardWeeks
|
|
||||||
import space.luminic.finance.models.WeekCategory
|
|
||||||
|
|
||||||
object DashboardDataMapper {
|
|
||||||
fun DashboardData.toDto() = DashboardDataDTO(
|
|
||||||
analyzedText = this.analyzedText,
|
|
||||||
totalExpense = this.totalExpense,
|
|
||||||
totalIncome = this.totalIncome,
|
|
||||||
balance = this.balance,
|
|
||||||
categories = this.categories.map { it.toDto() },
|
|
||||||
upcomingTransactions = this.upcomingTransactions.map { it.toDto() },
|
|
||||||
recentTransactions = this.recentTransactions.map { it.toDto() },
|
|
||||||
weeks = this.weeks.map { it.toDto() },
|
|
||||||
)
|
|
||||||
fun DashboardWeeks.toDto() = DashboardWeeksDTO(
|
|
||||||
startDate = this.startDate,
|
|
||||||
endDate = this.endDate,
|
|
||||||
expenseSum = this.expenseSum,
|
|
||||||
categories = this.categories.map { it.toDto() },
|
|
||||||
)
|
|
||||||
fun WeekCategory.toDto() = WeekCategoryDTO(
|
|
||||||
categoryId = this.categoryId,
|
|
||||||
categoryName = this.categoryName,
|
|
||||||
categoryIcon = this.categoryIcon,
|
|
||||||
sum = this.sum
|
|
||||||
)
|
|
||||||
fun DashboardCategory.toDto() = DashboardCategoryDTO(
|
|
||||||
category = this.category.toDto(),
|
|
||||||
currentPeriodAmount = this.currentPeriodAmount,
|
|
||||||
previousPeriodAmount = this.previousPeriodAmount,
|
|
||||||
changeDiff = this.changeDiff,
|
|
||||||
changeDiffPercentage = this.changeDiffPercentage,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@@ -10,7 +10,6 @@ object SpaceMapper {
|
|||||||
name = this.name,
|
name = this.name,
|
||||||
owner = this.owner.toDto(),
|
owner = this.owner.toDto(),
|
||||||
participants = this.participants.map { it.toDto() }.toSet(),
|
participants = this.participants.map { it.toDto() }.toSet(),
|
||||||
isGoogleDriveConnected = this.owner.googleRefreshToken != null,
|
|
||||||
createdBy = this.createdBy?.toDto(),
|
createdBy = this.createdBy?.toDto(),
|
||||||
createdAt = this.createdAt ?: throw IllegalArgumentException("createdAt is not provided"),
|
createdAt = this.createdAt ?: throw IllegalArgumentException("createdAt is not provided"),
|
||||||
updatedBy = this.updatedBy?.toDto(),
|
updatedBy = this.updatedBy?.toDto(),
|
||||||
|
|||||||
@@ -1,22 +1,26 @@
|
|||||||
package space.luminic.finance.mappers
|
package space.luminic.finance.mappers
|
||||||
|
|
||||||
import space.luminic.finance.dtos.GoalDTO
|
import space.luminic.finance.dtos.TargetDTO
|
||||||
import space.luminic.finance.mappers.UserMapper.toDto
|
import space.luminic.finance.mappers.UserMapper.toDto
|
||||||
import space.luminic.finance.models.Goal
|
import space.luminic.finance.models.Target
|
||||||
|
|
||||||
object GoalMapper {
|
object TargetMapper {
|
||||||
|
|
||||||
fun Goal.toDto() = GoalDTO(
|
fun Target.toDto() = TargetDTO(
|
||||||
id = this.id ?: throw IllegalArgumentException("Goal id is not provided"),
|
id = this.id ?: throw IllegalArgumentException("Target id is not provided"),
|
||||||
type = this.type,
|
type = this.type,
|
||||||
name = this.name,
|
name = this.name,
|
||||||
|
description = this.description,
|
||||||
amount = this.amount,
|
amount = this.amount,
|
||||||
|
currentAmount = this.currentAmount,
|
||||||
date = this.untilDate,
|
date = this.untilDate,
|
||||||
components = this.components,
|
components = this.components,
|
||||||
transactions = this.transactions,
|
transactions = this.transactions,
|
||||||
createdBy = (this.createdBy ?: throw IllegalArgumentException("created by not provided")).toDto(),
|
createdBy = (this.createdBy ?: throw IllegalArgumentException("created by not provided")).toDto(),
|
||||||
createdAt = this.createdAt ?: throw IllegalArgumentException("created at not provided"),
|
createdAt = this.createdAt ?: throw IllegalArgumentException("created at not provided"),
|
||||||
updatedBy = this.updatedBy?.toDto() ,
|
updatedBy = this.updatedBy?.toDto(),
|
||||||
updatedAt = this.updatedAt
|
updatedAt = this.updatedAt,
|
||||||
)
|
|
||||||
|
|
||||||
|
)
|
||||||
}
|
}
|
||||||
@@ -13,8 +13,7 @@ object UserMapper {
|
|||||||
tgId = this.tgId,
|
tgId = this.tgId,
|
||||||
tgUserName = this.tgUserName,
|
tgUserName = this.tgUserName,
|
||||||
photoUrl = this.photoUrl,
|
photoUrl = this.photoUrl,
|
||||||
roles = this.roles,
|
roles = this.roles
|
||||||
isGoogleDriveConnected = this.googleRefreshToken != null
|
|
||||||
)
|
)
|
||||||
|
|
||||||
fun TelegramAuthDTO.toTelegramMap(): Map<String, String> =
|
fun TelegramAuthDTO.toTelegramMap(): Map<String, String> =
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
package space.luminic.finance.models
|
|
||||||
|
|
||||||
import kotlinx.serialization.SerialName
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import lombok.AllArgsConstructor
|
|
||||||
import java.time.LocalDate
|
|
||||||
|
|
||||||
data class DashboardData(
|
|
||||||
var analyzedText: AISummaryData? = null,
|
|
||||||
val totalExpense: Int,
|
|
||||||
val totalIncome: Int,
|
|
||||||
val balance: Int,
|
|
||||||
val prevTotalExpense: Int,
|
|
||||||
val prevTotalIncome: Int,
|
|
||||||
val prevBalance: Int,
|
|
||||||
val prevCurIncomeChange: Double,
|
|
||||||
val prevCurExpenseChange: Double,
|
|
||||||
val categories: List<DashboardCategory>,
|
|
||||||
val upcomingTransactions: List<Transaction>,
|
|
||||||
val recentTransactions: List<Transaction>,
|
|
||||||
val weeks: List<DashboardWeeks>
|
|
||||||
)
|
|
||||||
|
|
||||||
@AllArgsConstructor
|
|
||||||
data class AISummaryData(
|
|
||||||
//) "Общая оценка периода"
|
|
||||||
//2) "Анализ по категориям"
|
|
||||||
//3) "Ключевые инсайты"
|
|
||||||
//4) "Рекомендации"
|
|
||||||
val common: String,
|
|
||||||
val categoryAnalysis: String,
|
|
||||||
val keyInsights: String,
|
|
||||||
val recommendations: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class DashboardWeeks(
|
|
||||||
val startDate: LocalDate,
|
|
||||||
val endDate: LocalDate,
|
|
||||||
val expenseSum: Int,
|
|
||||||
val categories: List<WeekCategory>
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class WeekCategory(
|
|
||||||
@SerialName("category_id") val categoryId: Int?,
|
|
||||||
@SerialName("category_name") val categoryName: String?,
|
|
||||||
@SerialName("category_icon") val categoryIcon: String?,
|
|
||||||
val sum: Int? = 0
|
|
||||||
)
|
|
||||||
data class DashboardCategory(
|
|
||||||
val category: Category,
|
|
||||||
val currentPeriodAmount: Int,
|
|
||||||
val previousPeriodAmount: Int,
|
|
||||||
val changeDiff: Double,
|
|
||||||
val changeDiffPercentage: Double,
|
|
||||||
)
|
|
||||||
@@ -8,30 +8,30 @@ import java.math.BigDecimal
|
|||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
|
|
||||||
data class Goal(
|
data class Target(
|
||||||
var id: Int? = null,
|
var id: Int? = null,
|
||||||
val space: Space? = null,
|
val space: Space? = null,
|
||||||
val type: GoalType,
|
val type: TargetType,
|
||||||
val name: String,
|
val name: String,
|
||||||
val description: String? = null,
|
val description: String? = null,
|
||||||
val amount: BigDecimal,
|
val amount: BigDecimal,
|
||||||
val components: List<GoalComponent> = emptyList(),
|
val components: List<TargetComponent> = emptyList(),
|
||||||
val transactions: List<Transaction> = emptyList(),
|
val transactions: List<Transaction> = emptyList(),
|
||||||
val untilDate: LocalDate,
|
val untilDate: LocalDate,
|
||||||
@CreatedBy var createdBy: User? = null,
|
var createdBy: User? = null,
|
||||||
|
|
||||||
@CreatedDate var createdAt: Instant? = null,
|
var createdAt: Instant? = null,
|
||||||
@LastModifiedBy var updatedBy: User? = null,
|
var updatedBy: User? = null,
|
||||||
|
|
||||||
@LastModifiedDate var updatedAt: Instant? = null,
|
var updatedAt: Instant? = null,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
var currentAmount: BigDecimal = {
|
var currentAmount: BigDecimal = {
|
||||||
this.transactions.sumOf { it.amount }
|
this.transactions.sumOf { it.amount }
|
||||||
} as BigDecimal
|
} as BigDecimal
|
||||||
|
|
||||||
|
|
||||||
data class GoalComponent(
|
data class TargetComponent(
|
||||||
val id: Int? = null,
|
val id: Int? = null,
|
||||||
val name: String,
|
val name: String,
|
||||||
val amount: BigDecimal,
|
val amount: BigDecimal,
|
||||||
@@ -39,8 +39,9 @@ data class Goal(
|
|||||||
val date: LocalDate = LocalDate.now(),
|
val date: LocalDate = LocalDate.now(),
|
||||||
)
|
)
|
||||||
|
|
||||||
enum class GoalType(val displayName: String, val icon: String) {
|
enum class TargetType(val displayName: String, val icon: String) {
|
||||||
AUTO("Авто", "🏎️"),
|
AUTO("Авто", "🏎️"),
|
||||||
|
LEISURE("Досуг", "💃"),
|
||||||
VACATION("Отпуск", "🏖️"),
|
VACATION("Отпуск", "🏖️"),
|
||||||
GOODS("Покупка", "🛍️"),
|
GOODS("Покупка", "🛍️"),
|
||||||
OTHER("Прочее", "💸")
|
OTHER("Прочее", "💸")
|
||||||
@@ -20,7 +20,6 @@ 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,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
package space.luminic.finance.repos
|
|
||||||
|
|
||||||
import space.luminic.finance.models.AISummaryData
|
|
||||||
import space.luminic.finance.models.DashboardData
|
|
||||||
import java.time.LocalDate
|
|
||||||
import java.time.LocalDateTime
|
|
||||||
|
|
||||||
interface DashboardRepo {
|
|
||||||
fun getData(spaceId: Int, startDate: LocalDate, endDate: LocalDate): DashboardData
|
|
||||||
fun savePeriodAnalyze(spaceId: Int, startDate: LocalDate, endDate: LocalDate, analyzedText: String)
|
|
||||||
fun getPeriodAnalyzedText(spaceId: Int, startDate: LocalDate, endDate: LocalDate): AISummaryData?
|
|
||||||
fun getAnalyzeLastRun(): LocalDateTime?
|
|
||||||
fun createRun(spaces: List<Int>)
|
|
||||||
}
|
|
||||||
@@ -1,367 +0,0 @@
|
|||||||
package space.luminic.finance.repos
|
|
||||||
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import org.springframework.dao.EmptyResultDataAccessException
|
|
||||||
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate
|
|
||||||
import org.springframework.stereotype.Repository
|
|
||||||
import space.luminic.finance.models.*
|
|
||||||
import space.luminic.finance.services.TransactionService
|
|
||||||
import java.time.LocalDate
|
|
||||||
import java.time.LocalDateTime
|
|
||||||
|
|
||||||
@Repository
|
|
||||||
class DashboardRepoImpl(
|
|
||||||
private val transactionRepo: TransactionRepo,
|
|
||||||
private val jdbcTemplate: NamedParameterJdbcTemplate
|
|
||||||
) : DashboardRepo {
|
|
||||||
override fun getData(
|
|
||||||
spaceId: Int,
|
|
||||||
startDate: LocalDate,
|
|
||||||
endDate: LocalDate
|
|
||||||
): DashboardData {
|
|
||||||
val getSumsSql = """
|
|
||||||
WITH bounds AS (SELECT :startDate::date AS cur_start,
|
|
||||||
:endDate::date AS cur_end,
|
|
||||||
(:startDate::date - INTERVAL '1 month') AS prev_start,
|
|
||||||
(:endDate::date - INTERVAL '1 month') AS prev_end),
|
|
||||||
sums_cur_period AS (SELECT COALESCE(SUM(amount) FILTER (WHERE type = 'EXPENSE'), 0) AS sum_expense,
|
|
||||||
COALESCE(SUM(amount) FILTER (WHERE type = 'INCOME'), 0) AS sum_income
|
|
||||||
FROM finance.transactions
|
|
||||||
CROSS JOIN bounds b
|
|
||||||
WHERE space_id = :spaceId
|
|
||||||
AND kind = 'INSTANT'
|
|
||||||
AND is_deleted = false
|
|
||||||
AND date BETWEEN b.cur_start AND b.cur_end),
|
|
||||||
sums_prev_period AS (SELECT COALESCE(SUM(amount) FILTER (WHERE type = 'EXPENSE'), 0) AS sum_expense,
|
|
||||||
COALESCE(SUM(amount) FILTER (WHERE type = 'INCOME'), 0) AS sum_income
|
|
||||||
FROM finance.transactions
|
|
||||||
CROSS JOIN bounds b
|
|
||||||
WHERE space_id = :spaceId
|
|
||||||
AND kind = 'INSTANT'
|
|
||||||
AND is_deleted = false
|
|
||||||
AND date BETWEEN b.prev_start AND b.prev_end)
|
|
||||||
SELECT cur.sum_expense AS cur_sum_expense,
|
|
||||||
cur.sum_income AS cur_sum_income,
|
|
||||||
cur.sum_income - cur.sum_expense AS cur_balance,
|
|
||||||
prev.sum_expense AS prev_sum_expense,
|
|
||||||
prev.sum_income AS prev_sum_income,
|
|
||||||
prev.sum_income - prev.sum_expense AS prev_balance,
|
|
||||||
CASE
|
|
||||||
WHEN prev.sum_expense != 0 THEN (cur.sum_expense / prev.sum_expense) * 100
|
|
||||||
ELSE 0 END as prev_cur_expense_change,
|
|
||||||
CASE
|
|
||||||
WHEN prev.sum_income != 0 THEN (cur.sum_income / prev.sum_income) * 100
|
|
||||||
ELSE 0 END as prev_cur_income_change
|
|
||||||
FROM sums_cur_period cur
|
|
||||||
CROSS JOIN sums_prev_period prev;
|
|
||||||
""".trimIndent()
|
|
||||||
val params = mutableMapOf<String, Any>(
|
|
||||||
"spaceId" to spaceId,
|
|
||||||
"startDate" to startDate,
|
|
||||||
"endDate" to endDate,
|
|
||||||
)
|
|
||||||
val expenseIncomeSumResult = jdbcTemplate.queryForObject(getSumsSql, params) { rs, _ ->
|
|
||||||
mapOf(
|
|
||||||
"cur_sum_expense" to rs.getDouble("cur_sum_expense"),
|
|
||||||
"cur_sum_income" to rs.getDouble("cur_sum_income"),
|
|
||||||
"cur_balance" to rs.getDouble("cur_balance"),
|
|
||||||
"prev_sum_expense" to rs.getDouble("prev_sum_expense"),
|
|
||||||
"prev_sum_income" to rs.getDouble("prev_sum_income"),
|
|
||||||
"prev_balance" to rs.getDouble("prev_balance"),
|
|
||||||
"prev_cur_expense_change" to rs.getDouble("prev_cur_expense_change"),
|
|
||||||
"prev_cur_income_change" to rs.getDouble("prev_cur_income_change"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
val getCatsSql = """
|
|
||||||
WITH bounds AS (
|
|
||||||
SELECT
|
|
||||||
:startDate::date AS cur_start,
|
|
||||||
:endDate::date AS cur_end,
|
|
||||||
(:startDate::date - INTERVAL '1 month') AS prev_start,
|
|
||||||
(:endDate::date - INTERVAL '1 month') AS prev_end
|
|
||||||
),
|
|
||||||
agg AS (
|
|
||||||
SELECT
|
|
||||||
c.*,
|
|
||||||
COALESCE(
|
|
||||||
SUM(
|
|
||||||
CASE
|
|
||||||
WHEN t.date BETWEEN b.cur_start AND b.cur_end
|
|
||||||
THEN t.amount
|
|
||||||
END
|
|
||||||
),
|
|
||||||
0
|
|
||||||
) AS sum_this_period,
|
|
||||||
|
|
||||||
COALESCE(
|
|
||||||
SUM(
|
|
||||||
CASE
|
|
||||||
WHEN t.date BETWEEN b.prev_start AND b.prev_end
|
|
||||||
THEN t.amount
|
|
||||||
END
|
|
||||||
),
|
|
||||||
0
|
|
||||||
) AS sum_previous_period
|
|
||||||
FROM finance.categories c
|
|
||||||
CROSS JOIN bounds b
|
|
||||||
LEFT JOIN finance.transactions t
|
|
||||||
ON t.category_id = c.id
|
|
||||||
where t.space_id = :spaceId and t.kind = 'INSTANT'
|
|
||||||
AND t.is_deleted = false
|
|
||||||
|
|
||||||
GROUP BY
|
|
||||||
c.id, c.name
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
agg.*,
|
|
||||||
|
|
||||||
agg.sum_this_period - agg.sum_previous_period AS diff_period,
|
|
||||||
CASE
|
|
||||||
WHEN agg.sum_previous_period = 0 THEN NULL -- или 0, если так удобнее
|
|
||||||
ELSE (agg.sum_this_period::numeric / agg.sum_previous_period::numeric - 1) * 100
|
|
||||||
END AS diff_percent
|
|
||||||
FROM agg
|
|
||||||
ORDER BY agg.name;
|
|
||||||
""".trimIndent()
|
|
||||||
val resultCatsSql = jdbcTemplate.query(getCatsSql, params) { rs, _ ->
|
|
||||||
DashboardCategory(
|
|
||||||
Category(
|
|
||||||
id = rs.getInt("id"),
|
|
||||||
type = Category.CategoryType.valueOf(rs.getString("type")),
|
|
||||||
name = rs.getString("name"),
|
|
||||||
description = rs.getString("description"),
|
|
||||||
icon = rs.getString("icon"),
|
|
||||||
isDeleted = rs.getBoolean("is_deleted"),
|
|
||||||
createdAt = rs.getTimestamp("created_at").toInstant(),
|
|
||||||
updatedAt = if (rs.getTimestamp("updated_at") != null) rs.getTimestamp("updated_at")
|
|
||||||
.toInstant() else null
|
|
||||||
),
|
|
||||||
rs.getInt("sum_this_period"),
|
|
||||||
rs.getInt("sum_previous_period"),
|
|
||||||
rs.getDouble("diff_period"),
|
|
||||||
rs.getDouble("diff_percent"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
val weeksSql = """WITH bounds AS (
|
|
||||||
SELECT
|
|
||||||
date_trunc('week', current_date)::date AS cur_week_start
|
|
||||||
),
|
|
||||||
weeks AS (
|
|
||||||
-- 4 недели: текущая + 3 предыдущие
|
|
||||||
SELECT
|
|
||||||
(cur_week_start - (n * INTERVAL '1 week'))::date AS week_start
|
|
||||||
FROM bounds,
|
|
||||||
generate_series(0, 3) AS g(n)
|
|
||||||
),
|
|
||||||
tx AS (
|
|
||||||
SELECT
|
|
||||||
date_trunc('week', t.date)::date AS week_start,
|
|
||||||
t.amount,
|
|
||||||
c.id AS category_id,
|
|
||||||
c.name AS category_name,
|
|
||||||
c.icon AS category_icon
|
|
||||||
FROM finance.transactions t
|
|
||||||
JOIN finance.categories c ON c.id = t.category_id
|
|
||||||
JOIN bounds b ON TRUE
|
|
||||||
WHERE t.type = 'EXPENSE'
|
|
||||||
AND t.kind = 'INSTANT'
|
|
||||||
AND t.is_deleted = false
|
|
||||||
AND t.date >= b.cur_week_start - INTERVAL '3 weeks'
|
|
||||||
AND t.date < b.cur_week_start + INTERVAL '1 week'
|
|
||||||
),
|
|
||||||
weekly_by_cat AS (
|
|
||||||
SELECT
|
|
||||||
w.week_start,
|
|
||||||
tx.category_id,
|
|
||||||
tx.category_name,
|
|
||||||
tx.category_icon,
|
|
||||||
SUM(tx.amount) AS category_sum
|
|
||||||
FROM weeks w
|
|
||||||
LEFT JOIN tx
|
|
||||||
ON tx.week_start = w.week_start
|
|
||||||
GROUP BY
|
|
||||||
w.week_start, tx.category_id, tx.category_name, tx.category_icon
|
|
||||||
),
|
|
||||||
weekly_totals AS (
|
|
||||||
SELECT
|
|
||||||
week_start,
|
|
||||||
COALESCE(SUM(category_sum), 0) AS week_expense_sum
|
|
||||||
FROM weekly_by_cat
|
|
||||||
GROUP BY week_start
|
|
||||||
),
|
|
||||||
ranked AS (
|
|
||||||
SELECT
|
|
||||||
wbc.*,
|
|
||||||
ROW_NUMBER() OVER (
|
|
||||||
PARTITION BY week_start
|
|
||||||
ORDER BY category_sum DESC
|
|
||||||
) AS rn
|
|
||||||
FROM weekly_by_cat wbc
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
w.week_start,
|
|
||||||
(w.week_start + INTERVAL '6 days')::date AS week_end,
|
|
||||||
wt.week_expense_sum,
|
|
||||||
CASE
|
|
||||||
WHEN COUNT(r.*) FILTER (WHERE r.rn <= 5) = 0 THEN '[]'::jsonb
|
|
||||||
ELSE jsonb_agg(
|
|
||||||
jsonb_build_object(
|
|
||||||
'category_id', r.category_id,
|
|
||||||
'category_name', r.category_name,
|
|
||||||
'category_icon', r.category_icon,
|
|
||||||
'sum', r.category_sum
|
|
||||||
)
|
|
||||||
ORDER BY r.category_sum DESC
|
|
||||||
)
|
|
||||||
END AS top_5_categories
|
|
||||||
FROM weeks w
|
|
||||||
LEFT JOIN weekly_totals wt ON wt.week_start = w.week_start
|
|
||||||
LEFT JOIN ranked r
|
|
||||||
ON r.week_start = w.week_start
|
|
||||||
AND r.rn <= 5
|
|
||||||
GROUP BY
|
|
||||||
w.week_start,
|
|
||||||
wt.week_expense_sum
|
|
||||||
ORDER BY
|
|
||||||
w.week_start"""
|
|
||||||
val weeks = jdbcTemplate.query(weeksSql, params) { rs, _ ->
|
|
||||||
val weekStart = rs.getDate("week_start").toLocalDate()
|
|
||||||
val weekEnd = rs.getDate("week_end").toLocalDate()
|
|
||||||
|
|
||||||
val expenseSum = rs.getBigDecimal("week_expense_sum")?.toInt() ?: 0
|
|
||||||
// или getInt, если в БД тип точно int/numeric(…)
|
|
||||||
|
|
||||||
val categoriesJson = rs.getString("top_5_categories") ?: "[]"
|
|
||||||
|
|
||||||
val categories: List<WeekCategory> =
|
|
||||||
Json.decodeFromString(categoriesJson) // тип T выведется из переменной
|
|
||||||
|
|
||||||
DashboardWeeks(
|
|
||||||
startDate = weekStart,
|
|
||||||
endDate = weekEnd,
|
|
||||||
expenseSum = expenseSum,
|
|
||||||
categories = categories
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return DashboardData(
|
|
||||||
totalExpense = expenseIncomeSumResult?.get("cur_sum_expense")?.toInt() ?: 0,
|
|
||||||
totalIncome = expenseIncomeSumResult?.get("cur_sum_income")?.toInt() ?: 0,
|
|
||||||
balance = expenseIncomeSumResult?.get("cur_balance")?.toInt() ?: 0,
|
|
||||||
prevTotalExpense = expenseIncomeSumResult?.get("cur_balance")?.toInt() ?: 0,
|
|
||||||
prevTotalIncome = expenseIncomeSumResult?.get("cur_balance")?.toInt() ?: 0,
|
|
||||||
prevBalance = expenseIncomeSumResult?.get("cur_balance")?.toInt() ?: 0,
|
|
||||||
prevCurIncomeChange = expenseIncomeSumResult?.get("cur_balance") ?: 0.0,
|
|
||||||
prevCurExpenseChange = expenseIncomeSumResult?.get("cur_balance") ?: 0.0,
|
|
||||||
categories = resultCatsSql,
|
|
||||||
upcomingTransactions = transactionRepo.findAllBySpaceId(
|
|
||||||
spaceId,
|
|
||||||
TransactionService.TransactionsFilter(
|
|
||||||
kind = Transaction.TransactionKind.PLANNING,
|
|
||||||
dateFrom = startDate,
|
|
||||||
dateTo = endDate,
|
|
||||||
isDone = false,
|
|
||||||
limit = 5
|
|
||||||
)
|
|
||||||
),
|
|
||||||
recentTransactions = transactionRepo.findAllBySpaceId(
|
|
||||||
spaceId,
|
|
||||||
TransactionService.TransactionsFilter(
|
|
||||||
kind = Transaction.TransactionKind.INSTANT,
|
|
||||||
dateFrom = startDate,
|
|
||||||
dateTo = endDate,
|
|
||||||
limit = 5,
|
|
||||||
sorts = listOf(mapOf("sortBy" to "date", "sortDirection" to "DESC"))
|
|
||||||
)
|
|
||||||
),
|
|
||||||
weeks = weeks,
|
|
||||||
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun savePeriodAnalyze(
|
|
||||||
spaceId: Int,
|
|
||||||
startDate: LocalDate,
|
|
||||||
endDate: LocalDate,
|
|
||||||
analyzedText: String
|
|
||||||
) {
|
|
||||||
val sql = """
|
|
||||||
INSERT INTO finance.period_analyze (
|
|
||||||
space_id,
|
|
||||||
period_start,
|
|
||||||
period_end,
|
|
||||||
analyze_text
|
|
||||||
)
|
|
||||||
VALUES (
|
|
||||||
:spaceId,
|
|
||||||
:periodStart,
|
|
||||||
:periodEnd,
|
|
||||||
:analyzeText::json
|
|
||||||
)
|
|
||||||
ON CONFLICT (space_id, period_start, period_end)
|
|
||||||
DO UPDATE SET
|
|
||||||
analyze_text = EXCLUDED.analyze_text::json,
|
|
||||||
last_analyze_at = now();
|
|
||||||
""".trimIndent()
|
|
||||||
val params = mapOf(
|
|
||||||
"spaceId" to spaceId,
|
|
||||||
"periodStart" to startDate,
|
|
||||||
"periodEnd" to endDate,
|
|
||||||
"analyzeText" to analyzedText
|
|
||||||
)
|
|
||||||
jdbcTemplate.update(sql, params)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getPeriodAnalyzedText(
|
|
||||||
spaceId: Int,
|
|
||||||
startDate: LocalDate,
|
|
||||||
endDate: LocalDate
|
|
||||||
): AISummaryData? {
|
|
||||||
val sql =
|
|
||||||
"""SELECT analyze_text->>'common' AS common,
|
|
||||||
analyze_text->>'categoryAnalysis' AS category_analysis,
|
|
||||||
analyze_text->>'keyInsights' AS key_insights,
|
|
||||||
analyze_text->>'recommendations' AS recommendations
|
|
||||||
from finance.period_analyze WHERE space_id = :spaceId AND period_start = :periodStart AND period_end = :periodEnd"""
|
|
||||||
val params = mapOf(
|
|
||||||
"spaceId" to spaceId,
|
|
||||||
"periodStart" to startDate,
|
|
||||||
"periodEnd" to endDate
|
|
||||||
|
|
||||||
)
|
|
||||||
return try {
|
|
||||||
return jdbcTemplate
|
|
||||||
.query(sql, params) { rs, _ ->
|
|
||||||
AISummaryData(
|
|
||||||
common = rs.getString("common"),
|
|
||||||
categoryAnalysis = rs.getString("category_analysis"),
|
|
||||||
keyInsights = rs.getString("key_insights"),
|
|
||||||
recommendations = rs.getString("recommendations")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.firstOrNull()
|
|
||||||
} catch (e: EmptyResultDataAccessException) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getAnalyzeLastRun(): LocalDateTime? {
|
|
||||||
val sql = """
|
|
||||||
SELECT MAX(run_at) AS last_run
|
|
||||||
FROM finance.analyze_runs
|
|
||||||
"""
|
|
||||||
|
|
||||||
return jdbcTemplate
|
|
||||||
.query(sql) { rs, _ ->
|
|
||||||
rs.getTimestamp("last_run")?.toLocalDateTime()
|
|
||||||
}
|
|
||||||
.firstOrNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createRun(spaces: List<Int>) {
|
|
||||||
val sql = """INSERT INTO finance.analyze_runs VALUES (now(), :spaces);"""
|
|
||||||
val params = mapOf(
|
|
||||||
"spaces" to spaces.joinToString(",")
|
|
||||||
)
|
|
||||||
jdbcTemplate.update(sql, params)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
package space.luminic.finance.repos
|
|
||||||
|
|
||||||
import space.luminic.finance.models.Goal
|
|
||||||
|
|
||||||
interface GoalRepo {
|
|
||||||
|
|
||||||
fun findAllBySpaceId(spaceId: Int) : List<Goal>
|
|
||||||
fun findBySpaceIdAndId(spaceId: Int, id: Int) : Goal?
|
|
||||||
fun create(goal: Goal, createdById: Int): Int
|
|
||||||
fun update(goal: Goal, updatedById: Int)
|
|
||||||
fun delete(spaceId: Int, id: Int)
|
|
||||||
fun getComponents(spaceId: Int, goalId: Int): List<Goal.GoalComponent>
|
|
||||||
fun getComponent(spaceId: Int, goalId: Int, id: Int): Goal.GoalComponent?
|
|
||||||
fun createComponent(goalId: Int, component: Goal.GoalComponent, createdById: Int): Int
|
|
||||||
fun updateComponent(goalId: Int, componentId: Int, component: Goal.GoalComponent, updatedById: Int)
|
|
||||||
fun deleteComponent(goalId: Int, componentId: Int)
|
|
||||||
|
|
||||||
fun assignTransaction(goalId: Int, transactionId: Int)
|
|
||||||
fun refuseTransaction(goalId: Int, transactionId: Int)
|
|
||||||
}
|
|
||||||
@@ -73,7 +73,7 @@ class RecurrentOperationRepoImpl(
|
|||||||
join finance.users su on s.owner_id = su.id
|
join finance.users su on s.owner_id = su.id
|
||||||
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 and ro.is_deleted = false
|
where ro.space_id = :spaceId
|
||||||
order by ro.date, ro.id
|
order by ro.date, ro.id
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
val params = mapOf("spaceId" to spaceId)
|
val params = mapOf("spaceId" to spaceId)
|
||||||
@@ -109,7 +109,7 @@ class RecurrentOperationRepoImpl(
|
|||||||
join finance.users su on s.owner_id = su.id
|
join finance.users su on s.owner_id = su.id
|
||||||
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 and ro.id = :id and ro.is_deleted = false;
|
where ro.space_id = :spaceId and ro.id = :id
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
val params = mapOf("spaceId" to spaceId, "id" to id)
|
val params = mapOf("spaceId" to spaceId, "id" to id)
|
||||||
return jdbcTemplate.query(sql, params, operationRowMapper()).firstOrNull()
|
return jdbcTemplate.query(sql, params, operationRowMapper()).firstOrNull()
|
||||||
@@ -143,7 +143,7 @@ class RecurrentOperationRepoImpl(
|
|||||||
join finance.users su on s.owner_id = su.id
|
join finance.users su on s.owner_id = su.id
|
||||||
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.date = :date and ro.is_deleted = false
|
where ro.date = :date
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
val params = mapOf( "date" to date)
|
val params = mapOf( "date" to date)
|
||||||
return jdbcTemplate.query(sql, params, operationRowMapper())
|
return jdbcTemplate.query(sql, params, operationRowMapper())
|
||||||
@@ -203,8 +203,7 @@ class RecurrentOperationRepoImpl(
|
|||||||
|
|
||||||
override fun delete(id: Int) {
|
override fun delete(id: Int) {
|
||||||
val sql = """
|
val sql = """
|
||||||
update finance.recurrent_operations
|
delete from finance.recurrent_operations
|
||||||
set is_deleted = true
|
|
||||||
where id = :id
|
where id = :id
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
val params = mapOf("id" to id)
|
val params = mapOf("id" to id)
|
||||||
|
|||||||
@@ -2,12 +2,9 @@ package space.luminic.finance.repos
|
|||||||
|
|
||||||
import org.springframework.stereotype.Repository
|
import org.springframework.stereotype.Repository
|
||||||
import space.luminic.finance.models.Space
|
import space.luminic.finance.models.Space
|
||||||
import java.time.LocalDateTime
|
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
interface SpaceRepo {
|
interface SpaceRepo {
|
||||||
fun findAll(): 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?
|
||||||
fun create(space: Space, createdById: Int): Int
|
fun create(space: Space, createdById: Int): Int
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import org.springframework.stereotype.Repository
|
|||||||
import space.luminic.finance.dtos.SpaceDTO
|
import space.luminic.finance.dtos.SpaceDTO
|
||||||
import space.luminic.finance.models.Space
|
import space.luminic.finance.models.Space
|
||||||
import space.luminic.finance.models.User
|
import space.luminic.finance.models.User
|
||||||
import java.time.LocalDateTime
|
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
class SpaceRepoImpl(
|
class SpaceRepoImpl(
|
||||||
@@ -23,7 +22,6 @@ 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"),
|
||||||
@@ -40,9 +38,7 @@ 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"),
|
|
||||||
googleRefreshToken = rs.getString("s_owner_google_refresh_token")
|
|
||||||
),
|
),
|
||||||
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(),
|
||||||
@@ -87,60 +83,6 @@ 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> {
|
|
||||||
val sql = """
|
|
||||||
select s.id as s_id,
|
|
||||||
s.name as s_name,
|
|
||||||
s.created_at as s_created_at,
|
|
||||||
true as s_is_owner,
|
|
||||||
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,
|
|
||||||
ou.google_refresh_token as s_owner_google_refresh_token,
|
|
||||||
sp.participants_id as sp_uid,
|
|
||||||
u.username as sp_username,
|
|
||||||
u.first_name as sp_first_name,
|
|
||||||
s.created_at as s_created_at,
|
|
||||||
s.created_by_id as s_created_by,
|
|
||||||
cau.username as s_created_by_username,
|
|
||||||
cau.first_name as s_created_by_firstname,
|
|
||||||
s.updated_at as s_updated_at,
|
|
||||||
s.updated_by_id as s_updated_by,
|
|
||||||
uau.username as s_updated_by_username,
|
|
||||||
uau.first_name as s_updated_by_firstname
|
|
||||||
|
|
||||||
from finance.spaces s
|
|
||||||
join finance.users ou on s.owner_id = ou.id
|
|
||||||
join finance.spaces_participants sp on sp.space_id = s.id
|
|
||||||
join finance.users u on sp.participants_id = u.id
|
|
||||||
left join finance.users cau on s.created_by_id = cau.id
|
|
||||||
left join finance.users uau on s.updated_by_id = uau.id
|
|
||||||
left join finance.transactions t on t.space_id = s.id
|
|
||||||
where s.is_deleted = false and t.created_at >= :lastRun
|
|
||||||
group by s.id, ou.username, ou.first_name, ou.tg_id, ou.google_refresh_token, sp.participants_id, u.username, u.first_name, cau.username, cau.first_name,
|
|
||||||
uau.username, uau.first_name;
|
|
||||||
""".trimMargin()
|
|
||||||
val params = mapOf("lastRun" to lastRun)
|
|
||||||
val spaces = jdbcTemplate.query(sql, params, shortRowMapper())
|
|
||||||
return collectParticipants(spaces)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
override fun findSpacesAvailableForUser(userId: Int): List<Space> {
|
override fun findSpacesAvailableForUser(userId: Int): List<Space> {
|
||||||
val sql = """
|
val sql = """
|
||||||
@@ -151,8 +93,6 @@ 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,
|
|
||||||
ou.google_refresh_token as s_owner_google_refresh_token,
|
|
||||||
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,
|
||||||
@@ -174,7 +114,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, ou.tg_id, ou.google_refresh_token, sp.participants_id, u.username, u.first_name, cau.username, cau.first_name,
|
group by s.id, ou.username, ou.first_name, 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(
|
||||||
@@ -193,8 +133,6 @@ 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,
|
|
||||||
ou.google_refresh_token as s_owner_google_refresh_token,
|
|
||||||
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,
|
||||||
@@ -216,7 +154,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, ou.tg_id, ou.google_refresh_token, sp.participants_id, u.username, u.first_name, cau.username, cau.first_name,
|
group by s.id, ou.username, ou.first_name, 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(
|
||||||
|
|||||||
20
src/main/kotlin/space/luminic/finance/repos/TargetRepo.kt
Normal file
20
src/main/kotlin/space/luminic/finance/repos/TargetRepo.kt
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package space.luminic.finance.repos
|
||||||
|
|
||||||
|
import space.luminic.finance.models.Target
|
||||||
|
|
||||||
|
interface TargetRepo {
|
||||||
|
|
||||||
|
fun findAllBySpaceId(spaceId: Int) : List<Target>
|
||||||
|
fun findBySpaceIdAndId(spaceId: Int, id: Int) : Target?
|
||||||
|
fun create(target: Target, createdById: Int): Int
|
||||||
|
fun update(target: Target, updatedById: Int)
|
||||||
|
fun delete(spaceId: Int, id: Int)
|
||||||
|
fun getComponents(spaceId: Int, targetId: Int): List<Target.TargetComponent>
|
||||||
|
fun getComponent(spaceId: Int, targetId: Int, id: Int): Target.TargetComponent?
|
||||||
|
fun createComponent(targetId: Int, component: Target.TargetComponent, createdById: Int): Int
|
||||||
|
fun updateComponent(targetId: Int, componentId: Int, component: Target.TargetComponent, updatedById: Int)
|
||||||
|
fun deleteComponent(targetId: Int, componentId: Int)
|
||||||
|
|
||||||
|
fun assignTransaction(targetId: Int, transactionId: Int)
|
||||||
|
fun refuseTransaction(targetId: Int, transactionId: Int)
|
||||||
|
}
|
||||||
@@ -3,17 +3,17 @@ package space.luminic.finance.repos
|
|||||||
import org.springframework.jdbc.core.RowMapper
|
import org.springframework.jdbc.core.RowMapper
|
||||||
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate
|
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate
|
||||||
import org.springframework.stereotype.Repository
|
import org.springframework.stereotype.Repository
|
||||||
import space.luminic.finance.models.Goal
|
import space.luminic.finance.models.Target
|
||||||
import space.luminic.finance.models.User
|
import space.luminic.finance.models.User
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
class GoalRepoImpl(
|
class TargetRepoImpl(
|
||||||
private val jdbcTemplate: NamedParameterJdbcTemplate
|
private val jdbcTemplate: NamedParameterJdbcTemplate
|
||||||
) : GoalRepo {
|
) : TargetRepo {
|
||||||
private val goalRowMapper = RowMapper { rs, _ ->
|
private val targetRowMapper = RowMapper { rs, _ ->
|
||||||
Goal(
|
Target(
|
||||||
id = rs.getInt("g_id"),
|
id = rs.getInt("g_id"),
|
||||||
type = Goal.GoalType.valueOf(rs.getString("g_type")),
|
type = Target.TargetType.valueOf(rs.getString("g_type")),
|
||||||
name = rs.getString("g_name"),
|
name = rs.getString("g_name"),
|
||||||
description = rs.getString("g_description"),
|
description = rs.getString("g_description"),
|
||||||
amount = rs.getBigDecimal("g_amount"),
|
amount = rs.getBigDecimal("g_amount"),
|
||||||
@@ -30,7 +30,7 @@ class GoalRepoImpl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val componentRowMapper = RowMapper { rs, _ ->
|
private val componentRowMapper = RowMapper { rs, _ ->
|
||||||
Goal.GoalComponent(
|
Target.TargetComponent(
|
||||||
id = rs.getInt("gc_id"),
|
id = rs.getInt("gc_id"),
|
||||||
name = rs.getString("gc_name"),
|
name = rs.getString("gc_name"),
|
||||||
amount = rs.getBigDecimal("gc_amount"),
|
amount = rs.getBigDecimal("gc_amount"),
|
||||||
@@ -39,7 +39,7 @@ class GoalRepoImpl(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun findAllBySpaceId(spaceId: Int): List<Goal> {
|
override fun findAllBySpaceId(spaceId: Int): List<Target> {
|
||||||
val sql = """
|
val sql = """
|
||||||
select
|
select
|
||||||
g.id as g_id,
|
g.id as g_id,
|
||||||
@@ -51,7 +51,7 @@ class GoalRepoImpl(
|
|||||||
created_by.username as created_by_username,
|
created_by.username as created_by_username,
|
||||||
created_by.first_name as created_by_first_name,
|
created_by.first_name as created_by_first_name,
|
||||||
g.created_at as g_created_at
|
g.created_at as g_created_at
|
||||||
from finance.goals g
|
from finance.targets g
|
||||||
join finance.users created_by on g.created_by_id = created_by.id
|
join finance.users created_by on g.created_by_id = created_by.id
|
||||||
where g.space_id = :spaceId
|
where g.space_id = :spaceId
|
||||||
|
|
||||||
@@ -60,10 +60,10 @@ class GoalRepoImpl(
|
|||||||
val params = mapOf(
|
val params = mapOf(
|
||||||
"space_id" to spaceId,
|
"space_id" to spaceId,
|
||||||
)
|
)
|
||||||
return jdbcTemplate.query(sql, params, goalRowMapper)
|
return jdbcTemplate.query(sql, params, targetRowMapper)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun findBySpaceIdAndId(spaceId: Int, id: Int): Goal? {
|
override fun findBySpaceIdAndId(spaceId: Int, id: Int): Target? {
|
||||||
val sql = """
|
val sql = """
|
||||||
select
|
select
|
||||||
g.id as g_id,
|
g.id as g_id,
|
||||||
@@ -75,7 +75,7 @@ class GoalRepoImpl(
|
|||||||
created_by.username as created_by_username,
|
created_by.username as created_by_username,
|
||||||
created_by.first_name as created_by_first_name,
|
created_by.first_name as created_by_first_name,
|
||||||
g.created_at as g_created_at
|
g.created_at as g_created_at
|
||||||
from finance.goals g
|
from finance.targets g
|
||||||
join finance.users created_by on g.created_by_id = created_by.id
|
join finance.users created_by on g.created_by_id = created_by.id
|
||||||
where g.space_id = :spaceId and g.id = :id
|
where g.space_id = :spaceId and g.id = :id
|
||||||
|
|
||||||
@@ -85,12 +85,12 @@ class GoalRepoImpl(
|
|||||||
"space_id" to spaceId,
|
"space_id" to spaceId,
|
||||||
"id" to id,
|
"id" to id,
|
||||||
)
|
)
|
||||||
return jdbcTemplate.query(sql, params, goalRowMapper).firstOrNull()
|
return jdbcTemplate.query(sql, params, targetRowMapper).firstOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun create(goal: Goal, createdById: Int): Int {
|
override fun create(target: Target, createdById: Int): Int {
|
||||||
val sql = """
|
val sql = """
|
||||||
insert into finance.goals(
|
insert into finance.targets(
|
||||||
type,
|
type,
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
@@ -108,19 +108,19 @@ class GoalRepoImpl(
|
|||||||
returning id
|
returning id
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
val params = mapOf(
|
val params = mapOf(
|
||||||
"type" to goal.type,
|
"type" to target.type,
|
||||||
"name" to goal.name,
|
"name" to target.name,
|
||||||
"description" to goal.description,
|
"description" to target.description,
|
||||||
"amount" to goal.amount,
|
"amount" to target.amount,
|
||||||
"until_date" to goal.untilDate,
|
"until_date" to target.untilDate,
|
||||||
"created_by_id" to createdById
|
"created_by_id" to createdById
|
||||||
)
|
)
|
||||||
return jdbcTemplate.queryForObject(sql, params, Int::class.java)!!
|
return jdbcTemplate.queryForObject(sql, params, Int::class.java)!!
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun update(goal: Goal, updatedById: Int) {
|
override fun update(target: Target, updatedById: Int) {
|
||||||
val sql = """
|
val sql = """
|
||||||
update finance.goals set
|
update finance.targets set
|
||||||
type = :type,
|
type = :type,
|
||||||
name = :name,
|
name = :name,
|
||||||
description = :description,
|
description = :description,
|
||||||
@@ -131,12 +131,12 @@ class GoalRepoImpl(
|
|||||||
where id = :id
|
where id = :id
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
val params = mapOf(
|
val params = mapOf(
|
||||||
"id" to goal.id,
|
"id" to target.id,
|
||||||
"type" to goal.type.name,
|
"type" to target.type.name,
|
||||||
"name" to goal.name,
|
"name" to target.name,
|
||||||
"description" to goal.description,
|
"description" to target.description,
|
||||||
"amount" to goal.amount,
|
"amount" to target.amount,
|
||||||
"until_date" to goal.untilDate,
|
"until_date" to target.untilDate,
|
||||||
"updated_by_id" to updatedById
|
"updated_by_id" to updatedById
|
||||||
|
|
||||||
)
|
)
|
||||||
@@ -145,7 +145,7 @@ class GoalRepoImpl(
|
|||||||
|
|
||||||
override fun delete(spaceId: Int, id: Int) {
|
override fun delete(spaceId: Int, id: Int) {
|
||||||
val sql = """
|
val sql = """
|
||||||
delete from finance.goals where id = :id
|
delete from finance.targets where id = :id
|
||||||
|
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
val params = mapOf(
|
val params = mapOf(
|
||||||
@@ -156,8 +156,8 @@ class GoalRepoImpl(
|
|||||||
|
|
||||||
override fun getComponents(
|
override fun getComponents(
|
||||||
spaceId: Int,
|
spaceId: Int,
|
||||||
goalId: Int
|
targetId: Int
|
||||||
): List<Goal.GoalComponent> {
|
): List<Target.TargetComponent> {
|
||||||
val sql = """
|
val sql = """
|
||||||
select
|
select
|
||||||
gc.id as gc_id,
|
gc.id as gc_id,
|
||||||
@@ -165,11 +165,11 @@ class GoalRepoImpl(
|
|||||||
gc.amount as gc_amount,
|
gc.amount as gc_amount,
|
||||||
gc.is_done as gc_is_done,
|
gc.is_done as gc_is_done,
|
||||||
gc.date as gc_date
|
gc.date as gc_date
|
||||||
from finance.goals_components gc
|
from finance.targets_components gc
|
||||||
where gc.goal_id = :goal_id
|
where gc.target_id = :target_id
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
val params = mapOf(
|
val params = mapOf(
|
||||||
"goal_id" to goalId
|
"target_id" to targetId
|
||||||
)
|
)
|
||||||
return jdbcTemplate.query(sql, params, componentRowMapper)
|
return jdbcTemplate.query(sql, params, componentRowMapper)
|
||||||
|
|
||||||
@@ -177,9 +177,9 @@ class GoalRepoImpl(
|
|||||||
|
|
||||||
override fun getComponent(
|
override fun getComponent(
|
||||||
spaceId: Int,
|
spaceId: Int,
|
||||||
goalId: Int,
|
targetId: Int,
|
||||||
id: Int
|
id: Int
|
||||||
): Goal.GoalComponent? {
|
): Target.TargetComponent? {
|
||||||
val sql = """
|
val sql = """
|
||||||
select
|
select
|
||||||
gc.id as gc_id,
|
gc.id as gc_id,
|
||||||
@@ -187,27 +187,27 @@ class GoalRepoImpl(
|
|||||||
gc.amount as gc_amount,
|
gc.amount as gc_amount,
|
||||||
gc.is_done as gc_is_done,
|
gc.is_done as gc_is_done,
|
||||||
gc.date as gc_date
|
gc.date as gc_date
|
||||||
from finance.goals_components gc
|
from finance.targets_components gc
|
||||||
where gc.goal_id = :goal_id and gc.id = :id
|
where gc.target_id = :target_id and gc.id = :id
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
val params = mapOf(
|
val params = mapOf(
|
||||||
"goal_id" to goalId,
|
"target_id" to targetId,
|
||||||
"id" to id
|
"id" to id
|
||||||
)
|
)
|
||||||
return jdbcTemplate.query(sql, params, componentRowMapper).firstOrNull()
|
return jdbcTemplate.query(sql, params, componentRowMapper).firstOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createComponent(goalId: Int, component: Goal.GoalComponent, createdById: Int): Int {
|
override fun createComponent(targetId: Int, component: Target.TargetComponent, createdById: Int): Int {
|
||||||
val sql = """
|
val sql = """
|
||||||
insert into finance.goals_components(
|
insert into finance.targets_components(
|
||||||
goal_id,
|
target_id,
|
||||||
name,
|
name,
|
||||||
amount,
|
amount,
|
||||||
is_done,
|
is_done,
|
||||||
date,
|
date,
|
||||||
created_by_id
|
created_by_id
|
||||||
) values (
|
) values (
|
||||||
:goal_id,
|
:target_id,
|
||||||
:name,
|
:name,
|
||||||
:amount,
|
:amount,
|
||||||
:is_done,
|
:is_done,
|
||||||
@@ -216,7 +216,7 @@ class GoalRepoImpl(
|
|||||||
returning id
|
returning id
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
val params = mapOf(
|
val params = mapOf(
|
||||||
"goal_id" to goalId,
|
"target_id" to targetId,
|
||||||
"name" to component.name,
|
"name" to component.name,
|
||||||
"amount" to component.amount,
|
"amount" to component.amount,
|
||||||
"is_done" to component.isDone,
|
"is_done" to component.isDone,
|
||||||
@@ -226,17 +226,17 @@ class GoalRepoImpl(
|
|||||||
return jdbcTemplate.queryForObject(sql, params, Int::class.java)!!
|
return jdbcTemplate.queryForObject(sql, params, Int::class.java)!!
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun updateComponent(goalId: Int, componentId: Int, component: Goal.GoalComponent, updatedById: Int) {
|
override fun updateComponent(targetId: Int, componentId: Int, component: Target.TargetComponent, updatedById: Int) {
|
||||||
val sql = """
|
val sql = """
|
||||||
update finance.goals_components set
|
update finance.targets_components set
|
||||||
name = :name,
|
name = :name,
|
||||||
amount = :amount,
|
amount = :amount,
|
||||||
is_done = :is_done,
|
is_done = :is_done,
|
||||||
updated_by_id = :updated_by_id
|
updated_by_id = :updated_by_id
|
||||||
where goal_id = :goalId and id = :componentId
|
where target_id = :targetId and id = :componentId
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
val params = mapOf(
|
val params = mapOf(
|
||||||
"goalId" to goalId,
|
"targetId" to targetId,
|
||||||
"componentId" to componentId,
|
"componentId" to componentId,
|
||||||
"name" to component.name,
|
"name" to component.name,
|
||||||
"amount" to component.amount,
|
"amount" to component.amount,
|
||||||
@@ -247,35 +247,35 @@ class GoalRepoImpl(
|
|||||||
jdbcTemplate.update(sql, params)
|
jdbcTemplate.update(sql, params)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun deleteComponent(goalId: Int, componentId: Int) {
|
override fun deleteComponent(targetId: Int, componentId: Int) {
|
||||||
val sql = """
|
val sql = """
|
||||||
delete from finance.goals_components where goal_id = :goalId and id = :componentId
|
delete from finance.targets_components where target_id = :targetId and id = :componentId
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
val params = mapOf(
|
val params = mapOf(
|
||||||
"goalId" to goalId,
|
"targetId" to targetId,
|
||||||
"componentId" to componentId
|
"componentId" to componentId
|
||||||
)
|
)
|
||||||
jdbcTemplate.update(sql, params)
|
jdbcTemplate.update(sql, params)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun assignTransaction(goalId: Int, transactionId: Int) {
|
override fun assignTransaction(targetId: Int, transactionId: Int) {
|
||||||
val sql = """
|
val sql = """
|
||||||
insert into finance.goals_transactions(goal_id, transactions_id)
|
insert into finance.targets_transactions(target_id, transactions_id)
|
||||||
values (:goal_id, :transaction_id)
|
values (:targetId, :transaction_id)
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
val params = mapOf(
|
val params = mapOf(
|
||||||
"goal_id" to goalId,
|
"targetId" to targetId,
|
||||||
"transaction_id" to transactionId
|
"transaction_id" to transactionId
|
||||||
)
|
)
|
||||||
jdbcTemplate.update(sql, params)
|
jdbcTemplate.update(sql, params)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun refuseTransaction(goalId: Int, transactionId: Int) {
|
override fun refuseTransaction(targetId: Int, transactionId: Int) {
|
||||||
val sql = """
|
val sql = """
|
||||||
delete from finance.goals_transactions where goal_id = :goalId and transactions_id = :transactionId
|
delete from finance.targets_transactions where target_id = :goalId and transactions_id = :transactionId
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
val params = mapOf(
|
val params = mapOf(
|
||||||
"goal_id" to goalId,
|
"target_id" to targetId,
|
||||||
"transaction_id" to transactionId
|
"transaction_id" to transactionId
|
||||||
)
|
)
|
||||||
jdbcTemplate.update(sql, params)
|
jdbcTemplate.update(sql, params)
|
||||||
@@ -16,6 +16,4 @@ interface TransactionRepo {
|
|||||||
|
|
||||||
fun setCategory(txId:Int, categoryId: Int)
|
fun setCategory(txId:Int, categoryId: Int)
|
||||||
|
|
||||||
fun findByDateAndKind(date: java.time.LocalDate, kind: Transaction.TransactionKind): List<Transaction>
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -4,10 +4,11 @@ import org.springframework.jdbc.core.RowMapper
|
|||||||
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate
|
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate
|
||||||
import org.springframework.stereotype.Repository
|
import org.springframework.stereotype.Repository
|
||||||
import space.luminic.finance.models.Category
|
import space.luminic.finance.models.Category
|
||||||
import space.luminic.finance.models.Space
|
|
||||||
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
|
import space.luminic.finance.services.TransactionService
|
||||||
|
import java.time.LocalDate
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
class TransactionRepoImpl(
|
class TransactionRepoImpl(
|
||||||
@@ -31,7 +32,6 @@ class TransactionRepoImpl(
|
|||||||
) else null
|
) else null
|
||||||
Transaction(
|
Transaction(
|
||||||
id = rs.getInt("t_id"),
|
id = rs.getInt("t_id"),
|
||||||
space = Space(id = rs.getInt("t_space_id"), name = "", owner = User(0, "", ""), participants = emptySet()),
|
|
||||||
parent = parent,
|
parent = parent,
|
||||||
type = Transaction.TransactionType.valueOf(rs.getString("t_type")),
|
type = Transaction.TransactionType.valueOf(rs.getString("t_type")),
|
||||||
kind = Transaction.TransactionKind.valueOf(rs.getString("t_kind")),
|
kind = Transaction.TransactionKind.valueOf(rs.getString("t_kind")),
|
||||||
@@ -100,18 +100,10 @@ class TransactionRepoImpl(
|
|||||||
sql += " AND t.type = :type"
|
sql += " AND t.type = :type"
|
||||||
params.put("type", it.name)
|
params.put("type", it.name)
|
||||||
}
|
}
|
||||||
filters.query?.let {
|
|
||||||
sql += " AND lower(t.comment) LIKE ('%${it.lowercase()}%')"
|
|
||||||
params["query"] = it.lowercase()
|
|
||||||
}
|
|
||||||
filters.kind?.let {
|
filters.kind?.let {
|
||||||
sql += " AND t.kind = :kind"
|
sql += " AND t.kind = :kind"
|
||||||
params.put("kind", it.name)
|
params.put("kind", it.name)
|
||||||
}
|
}
|
||||||
filters.categoriesIds?.let {
|
|
||||||
sql += " AND t.category_id in (:categoriesIds)"
|
|
||||||
params.put("categoriesIds", it)
|
|
||||||
}
|
|
||||||
filters.isDone?.let {
|
filters.isDone?.let {
|
||||||
sql += " AND t.is_done = :isDone"
|
sql += " AND t.is_done = :isDone"
|
||||||
params.put("isDone", it)
|
params.put("isDone", it)
|
||||||
@@ -119,20 +111,16 @@ class TransactionRepoImpl(
|
|||||||
filters.dateFrom?.let {
|
filters.dateFrom?.let {
|
||||||
sql += " AND t.date >= :dateFrom"
|
sql += " AND t.date >= :dateFrom"
|
||||||
params.put("dateFrom", it)
|
params.put("dateFrom", it)
|
||||||
|
} ?: {
|
||||||
|
sql += " AND t.date >= :dateFrom"
|
||||||
|
params.put("dateFrom", LocalDate.now().minusMonths(1))
|
||||||
}
|
}
|
||||||
filters.dateTo?.let {
|
filters.dateTo?.let {
|
||||||
sql += " AND t.date <= :dateTo"
|
sql += " AND t.date <= :dateTo"
|
||||||
params.put("dateTo", it)
|
params.put("dateTo", it)
|
||||||
}
|
}
|
||||||
sql += if (filters.sorts.isNotEmpty()) {
|
|
||||||
var orderStatement = " ORDER BY "
|
|
||||||
orderStatement += filters.sorts.joinToString(",") { map ->
|
|
||||||
map.entries.joinToString(" ") { (_, v) -> v }
|
|
||||||
}
|
|
||||||
orderStatement
|
|
||||||
} else " ORDER BY t.date DESC, t.id"
|
|
||||||
|
|
||||||
sql += """
|
sql += """
|
||||||
|
ORDER BY t.date, t.id
|
||||||
OFFSET :offset ROWS
|
OFFSET :offset ROWS
|
||||||
FETCH FIRST :limit ROWS ONLY"""
|
FETCH FIRST :limit ROWS ONLY"""
|
||||||
|
|
||||||
@@ -393,44 +381,4 @@ class TransactionRepoImpl(
|
|||||||
)
|
)
|
||||||
jdbcTemplate.update(sql, params)
|
jdbcTemplate.update(sql, params)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun findByDateAndKind(date: java.time.LocalDate, kind: Transaction.TransactionKind): 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.date = :date and t.kind = :kind and t.is_deleted = false""".trimMargin()
|
|
||||||
val params = mapOf(
|
|
||||||
"date" to date,
|
|
||||||
"kind" to kind.name,
|
|
||||||
)
|
|
||||||
return jdbcTemplate.query(sql, params, transactionRowMapper())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -23,8 +23,7 @@ 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")
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,7 +58,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, google_refresh_token) values (:username, :firstname, :tg_id, :tg_user_name, :photo_url, :password, :isActive, :regDate, :googleRefreshToken) returning ID"
|
"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"
|
||||||
val params = mapOf(
|
val params = mapOf(
|
||||||
"username" to user.username,
|
"username" to user.username,
|
||||||
"firstname" to user.firstName,
|
"firstname" to user.firstName,
|
||||||
@@ -69,7 +68,6 @@ 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
|
||||||
@@ -77,33 +75,7 @@ class UserRepoImpl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun update(user: User): User {
|
override fun update(user: User): User {
|
||||||
val sql = """
|
TODO("Not yet implemented")
|
||||||
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) {
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
package space.luminic.finance.services
|
|
||||||
|
|
||||||
import space.luminic.finance.models.DashboardData
|
|
||||||
import java.time.LocalDate
|
|
||||||
|
|
||||||
interface DashboardService {
|
|
||||||
fun getDashboardData(spaceId: Int, startDate: LocalDate, endDate: LocalDate): DashboardData
|
|
||||||
fun analyzePeriodScheduled()
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
package space.luminic.finance.services
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
|
||||||
import org.springframework.beans.factory.annotation.Qualifier
|
|
||||||
import org.springframework.stereotype.Service
|
|
||||||
import space.luminic.finance.models.AISummaryData
|
|
||||||
import space.luminic.finance.models.DashboardData
|
|
||||||
import space.luminic.finance.repos.DashboardRepo
|
|
||||||
import space.luminic.finance.services.gpt.GptClient
|
|
||||||
import java.time.LocalDate
|
|
||||||
|
|
||||||
@Service
|
|
||||||
class DashboardServiceImpl(
|
|
||||||
private val authService: AuthService,
|
|
||||||
private val spaceService: SpaceService,
|
|
||||||
private val dashboardRepo: DashboardRepo,
|
|
||||||
@Qualifier("dsCategorizationService") private val gptClient: GptClient
|
|
||||||
) : DashboardService {
|
|
||||||
|
|
||||||
private val om = ObjectMapper()
|
|
||||||
override fun getDashboardData(spaceId: Int, startDate: LocalDate, endDate: LocalDate): DashboardData {
|
|
||||||
val userId = authService.getSecurityUserId()
|
|
||||||
spaceService.getSpace(spaceId, userId)
|
|
||||||
val data = dashboardRepo.getData(spaceId, startDate, endDate)
|
|
||||||
data.analyzedText = dashboardRepo.getPeriodAnalyzedText(spaceId, startDate, endDate)
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun analyzePeriodScheduled() {
|
|
||||||
val today = LocalDate.now()
|
|
||||||
|
|
||||||
|
|
||||||
val startDate = if (today.dayOfMonth < 10) {
|
|
||||||
today.minusMonths(1).withDayOfMonth(10)
|
|
||||||
} else {
|
|
||||||
today.withDayOfMonth(10)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
val endDate = if (today.dayOfMonth >= 10) {
|
|
||||||
today.plusMonths(1).withDayOfMonth(9)
|
|
||||||
} else {
|
|
||||||
today.withDayOfMonth(9)
|
|
||||||
}
|
|
||||||
val lastRun = dashboardRepo.getAnalyzeLastRun()
|
|
||||||
val spaces = spaceService.getSpacesForScheduling(lastRun)
|
|
||||||
spaces.forEach { space ->
|
|
||||||
val data = dashboardRepo.getData(space.id!!, startDate, endDate)
|
|
||||||
dashboardRepo.savePeriodAnalyze(
|
|
||||||
space.id!!,
|
|
||||||
startDate,
|
|
||||||
endDate,
|
|
||||||
gptClient.analyzePeriod(startDate, endDate, data)
|
|
||||||
)
|
|
||||||
dashboardRepo.createRun(spaces.map { it.id!! })
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
package space.luminic.finance.services
|
|
||||||
|
|
||||||
import space.luminic.finance.dtos.GoalDTO
|
|
||||||
import space.luminic.finance.models.Goal
|
|
||||||
|
|
||||||
interface GoalService {
|
|
||||||
fun findAllBySpaceId(spaceId: Int): List<Goal>
|
|
||||||
fun findBySpaceIdAndId(spaceId: Int, id: Int): Goal
|
|
||||||
fun create(spaceId: Int,goal: GoalDTO.CreateGoalDTO): Int
|
|
||||||
fun update(spaceId: Int, goalId: Int, goal: GoalDTO.UpdateGoalDTO)
|
|
||||||
fun delete(spaceId: Int, id: Int)
|
|
||||||
|
|
||||||
fun getComponents(spaceId: Int, goalId: Int): List<Goal.GoalComponent>
|
|
||||||
fun getComponent(spaceId: Int, goalId: Int, id: Int): Goal.GoalComponent?
|
|
||||||
fun createComponent(spaceId: Int, goalId: Int, component: Goal.GoalComponent): Int
|
|
||||||
fun updateComponent(spaceId: Int, goalId: Int, component: Goal.GoalComponent)
|
|
||||||
fun deleteComponent(spaceId: Int, goalId: Int, id: Int)
|
|
||||||
|
|
||||||
fun assignTransaction(spaceId: Int, goalId: Int, transactionId: Int)
|
|
||||||
fun refuseTransaction(spaceId: Int,goalId: Int, transactionId: Int)
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
package space.luminic.finance.services
|
|
||||||
|
|
||||||
import org.springframework.stereotype.Service
|
|
||||||
import space.luminic.finance.dtos.GoalDTO
|
|
||||||
import space.luminic.finance.models.Goal
|
|
||||||
import space.luminic.finance.models.NotFoundException
|
|
||||||
import space.luminic.finance.repos.GoalRepo
|
|
||||||
import space.luminic.finance.repos.SpaceRepo
|
|
||||||
import space.luminic.finance.repos.TransactionRepo
|
|
||||||
|
|
||||||
@Service
|
|
||||||
class GoalServiceImpl(
|
|
||||||
private val goalRepo: GoalRepo,
|
|
||||||
private val spaceRepo: SpaceRepo,
|
|
||||||
private val authService: AuthService,
|
|
||||||
private val transactionRepo: TransactionRepo
|
|
||||||
) : GoalService {
|
|
||||||
override fun findAllBySpaceId(spaceId: Int): List<Goal> {
|
|
||||||
val userId = authService.getSecurityUserId()
|
|
||||||
spaceRepo.findSpaceById(spaceId, userId)
|
|
||||||
return goalRepo.findAllBySpaceId(spaceId)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun findBySpaceIdAndId(spaceId: Int, id: Int): Goal {
|
|
||||||
val userId = authService.getSecurityUserId()
|
|
||||||
spaceRepo.findSpaceById(spaceId, userId)
|
|
||||||
return goalRepo.findBySpaceIdAndId(spaceId, userId) ?: throw NotFoundException("Goal $id not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun create(spaceId: Int, goal: GoalDTO.CreateGoalDTO): Int {
|
|
||||||
val userId = authService.getSecurityUserId()
|
|
||||||
spaceRepo.findSpaceById(spaceId, userId)
|
|
||||||
val creatingGoal = Goal(
|
|
||||||
type = goal.type,
|
|
||||||
name = goal.name,
|
|
||||||
amount = goal.amount,
|
|
||||||
untilDate = goal.date
|
|
||||||
)
|
|
||||||
return goalRepo.create(creatingGoal, userId)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun update(spaceId: Int, goalId: Int, goal: GoalDTO.UpdateGoalDTO) {
|
|
||||||
val userId = authService.getSecurityUserId()
|
|
||||||
spaceRepo.findSpaceById(spaceId, userId)
|
|
||||||
val existingGoal =
|
|
||||||
goalRepo.findBySpaceIdAndId(spaceId, goalId) ?: throw NotFoundException("Goal $goalId not found")
|
|
||||||
val updatedGoal = existingGoal.copy(
|
|
||||||
type = goal.type,
|
|
||||||
name = goal.name,
|
|
||||||
description = goal.description,
|
|
||||||
amount = goal.amount,
|
|
||||||
untilDate = goal.date
|
|
||||||
)
|
|
||||||
goalRepo.update(updatedGoal, userId)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun delete(spaceId: Int, id: Int) {
|
|
||||||
goalRepo.delete(spaceId, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getComponents(
|
|
||||||
spaceId: Int,
|
|
||||||
goalId: Int
|
|
||||||
): List<Goal.GoalComponent> {
|
|
||||||
val userId = authService.getSecurityUserId()
|
|
||||||
spaceRepo.findSpaceById(spaceId, userId)
|
|
||||||
goalRepo.findBySpaceIdAndId(spaceId, goalId) ?: throw NotFoundException("Goal $goalId not found")
|
|
||||||
return goalRepo.getComponents(spaceId, goalId)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getComponent(
|
|
||||||
spaceId: Int,
|
|
||||||
goalId: Int,
|
|
||||||
id: Int
|
|
||||||
): Goal.GoalComponent? {
|
|
||||||
val userId = authService.getSecurityUserId()
|
|
||||||
spaceRepo.findSpaceById(spaceId, userId)
|
|
||||||
goalRepo.findBySpaceIdAndId(spaceId, goalId) ?: throw NotFoundException("Goal $goalId not found")
|
|
||||||
return goalRepo.getComponent(spaceId, goalId, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createComponent(
|
|
||||||
spaceId: Int,
|
|
||||||
goalId: Int,
|
|
||||||
component: Goal.GoalComponent
|
|
||||||
): Int {
|
|
||||||
val userId = authService.getSecurityUserId()
|
|
||||||
spaceRepo.findSpaceById(spaceId, userId)
|
|
||||||
goalRepo.findBySpaceIdAndId(spaceId, goalId) ?: throw NotFoundException("Goal $goalId not found")
|
|
||||||
return goalRepo.createComponent(goalId, component, userId)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun updateComponent(
|
|
||||||
spaceId: Int,
|
|
||||||
goalId: Int,
|
|
||||||
component: Goal.GoalComponent
|
|
||||||
) {
|
|
||||||
val userId = authService.getSecurityUserId()
|
|
||||||
spaceRepo.findSpaceById(spaceId, userId)
|
|
||||||
goalRepo.findBySpaceIdAndId(spaceId, goalId) ?: throw NotFoundException("Goal $goalId not found")
|
|
||||||
val existingComponent = goalRepo.getComponent(spaceId, goalId, component.id!!)
|
|
||||||
?: throw NotFoundException("Component $goalId not found")
|
|
||||||
val updatedComponent = existingComponent.copy(
|
|
||||||
name = component.name,
|
|
||||||
amount = component.amount,
|
|
||||||
isDone = component.isDone,
|
|
||||||
date = component.date
|
|
||||||
)
|
|
||||||
goalRepo.updateComponent(goalId, updatedComponent.id!!, updatedComponent, userId)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun deleteComponent(spaceId: Int, goalId: Int, id: Int) {
|
|
||||||
val userId = authService.getSecurityUserId()
|
|
||||||
spaceRepo.findSpaceById(spaceId, userId)
|
|
||||||
goalRepo.findBySpaceIdAndId(spaceId, goalId) ?: throw NotFoundException("Goal $goalId not found")
|
|
||||||
goalRepo.getComponent(spaceId, goalId, id) ?: throw NotFoundException("Component $goalId not found")
|
|
||||||
goalRepo.deleteComponent(goalId, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun assignTransaction(spaceId: Int, goalId: Int, transactionId: Int) {
|
|
||||||
val userId = authService.getSecurityUserId()
|
|
||||||
spaceRepo.findSpaceById(spaceId, userId)
|
|
||||||
goalRepo.findBySpaceIdAndId(spaceId, goalId) ?: throw NotFoundException("Goal $goalId not found")
|
|
||||||
transactionRepo.findBySpaceIdAndId(spaceId, transactionId) ?: throw NotFoundException(
|
|
||||||
"Transaction $transactionId not found"
|
|
||||||
)
|
|
||||||
goalRepo.assignTransaction(goalId, transactionId)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun refuseTransaction(spaceId: Int, goalId: Int, transactionId: Int) {
|
|
||||||
val userId = authService.getSecurityUserId()
|
|
||||||
spaceRepo.findSpaceById(spaceId, userId)
|
|
||||||
goalRepo.findBySpaceIdAndId(spaceId, goalId) ?: throw NotFoundException("Goal $goalId not found")
|
|
||||||
transactionRepo.findBySpaceIdAndId(spaceId, transactionId) ?: throw NotFoundException(
|
|
||||||
"Transaction $transactionId not found"
|
|
||||||
)
|
|
||||||
goalRepo.refuseTransaction(goalId, transactionId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
package space.luminic.finance.services
|
|
||||||
|
|
||||||
import com.google.api.client.auth.oauth2.AuthorizationCodeFlow
|
|
||||||
import com.google.api.client.auth.oauth2.BearerToken
|
|
||||||
import com.google.api.client.auth.oauth2.Credential
|
|
||||||
import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow
|
|
||||||
import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets
|
|
||||||
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential
|
|
||||||
import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport
|
|
||||||
import com.google.api.client.http.ByteArrayContent
|
|
||||||
import com.google.api.client.json.gson.GsonFactory
|
|
||||||
import com.google.api.services.drive.Drive
|
|
||||||
import com.google.api.services.drive.DriveScopes
|
|
||||||
import com.google.api.services.drive.model.File
|
|
||||||
import org.springframework.beans.factory.annotation.Value
|
|
||||||
import org.springframework.stereotype.Service
|
|
||||||
|
|
||||||
@Service
|
|
||||||
class GoogleDriveService(
|
|
||||||
@Value("\${google.client-id:}") private val clientId: String,
|
|
||||||
@Value("\${google.client-secret:}") private val clientSecret: String,
|
|
||||||
@Value("\${google.redirect-uri:}") private val redirectUri: String
|
|
||||||
) {
|
|
||||||
private val jsonFactory = GsonFactory.getDefaultInstance()
|
|
||||||
private val httpTransport = GoogleNetHttpTransport.newTrustedTransport()
|
|
||||||
|
|
||||||
fun exchangeCodeForRefreshToken(authCode: String): String {
|
|
||||||
if (clientId.isEmpty() || clientSecret.isEmpty()) return ""
|
|
||||||
|
|
||||||
val flow = GoogleAuthorizationCodeFlow.Builder(
|
|
||||||
httpTransport,
|
|
||||||
jsonFactory,
|
|
||||||
clientId,
|
|
||||||
clientSecret,
|
|
||||||
listOf(DriveScopes.DRIVE_FILE)
|
|
||||||
)
|
|
||||||
.setAccessType("offline")
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val response = flow.newTokenRequest(authCode).setRedirectUri(redirectUri).execute()
|
|
||||||
return response.refreshToken ?: ""
|
|
||||||
}
|
|
||||||
|
|
||||||
fun uploadExcelFile(refreshToken: String, fileName: String, content: ByteArray) {
|
|
||||||
if (refreshToken.isEmpty() || clientId.isEmpty() || clientSecret.isEmpty()) return
|
|
||||||
|
|
||||||
val credential = GoogleCredential.Builder()
|
|
||||||
.setTransport(httpTransport)
|
|
||||||
.setJsonFactory(jsonFactory)
|
|
||||||
.setClientSecrets(clientId, clientSecret)
|
|
||||||
.build()
|
|
||||||
.setRefreshToken(refreshToken)
|
|
||||||
|
|
||||||
val driveService = Drive.Builder(httpTransport, jsonFactory, credential)
|
|
||||||
.setApplicationName("Luminic Space")
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val fileMetadata = File()
|
|
||||||
fileMetadata.name = fileName
|
|
||||||
fileMetadata.mimeType = "application/vnd.google-apps.spreadsheet"
|
|
||||||
|
|
||||||
val mediaContent = ByteArrayContent("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", content)
|
|
||||||
|
|
||||||
driveService.files().create(fileMetadata, mediaContent)
|
|
||||||
.setFields("id")
|
|
||||||
.execute()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
package space.luminic.finance.services
|
|
||||||
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import org.springframework.scheduling.annotation.Scheduled
|
|
||||||
import org.springframework.stereotype.Service
|
|
||||||
import space.luminic.finance.repos.SpaceRepo
|
|
||||||
import java.time.LocalDate
|
|
||||||
|
|
||||||
@Service
|
|
||||||
class MonthlyExportScheduler(
|
|
||||||
private val spaceRepo: SpaceRepo,
|
|
||||||
private val transactionService: TransactionService,
|
|
||||||
private val googleDriveService: GoogleDriveService
|
|
||||||
) {
|
|
||||||
private val logger = LoggerFactory.getLogger(javaClass)
|
|
||||||
|
|
||||||
// Каждое 9 число в 23:59
|
|
||||||
@Scheduled(cron = "0 59 23 9 * ?")
|
|
||||||
fun exportMonthlyTransactions() {
|
|
||||||
logger.info("Starting monthly transaction export to Google Drive")
|
|
||||||
|
|
||||||
val spaces = spaceRepo.findAll()
|
|
||||||
val endDate = LocalDate.now() // 9th of current month
|
|
||||||
val startDate = endDate.minusMonths(1).plusDays(1) // 10th of previous month
|
|
||||||
|
|
||||||
val filter = TransactionService.TransactionsFilter(
|
|
||||||
dateFrom = startDate,
|
|
||||||
dateTo = endDate
|
|
||||||
)
|
|
||||||
|
|
||||||
for (space in spaces) {
|
|
||||||
val owner = space.owner
|
|
||||||
val refreshToken = owner.googleRefreshToken
|
|
||||||
|
|
||||||
if (refreshToken != null && refreshToken.isNotEmpty()) {
|
|
||||||
logger.info("Exporting for space \${space.id} (\${space.name}), owner \${owner.username}")
|
|
||||||
try {
|
|
||||||
val excelBytes = transactionService.generateExcel(space.id!!, filter)
|
|
||||||
val fileName = "Выгрузка_\${space.name}_\${startDate}_\${endDate}.xlsx"
|
|
||||||
googleDriveService.uploadExcelFile(refreshToken, fileName, excelBytes)
|
|
||||||
logger.info("Successfully exported to Google Drive for space \${space.id}")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
logger.error("Failed to export space \${space.id} to Google Drive", e)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logger.debug("Skipping space \${space.id} because owner hasn't linked Google Drive")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
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 sendPlannedTransactionsReminder()
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
@@ -1,179 +0,0 @@
|
|||||||
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 space.luminic.finance.repos.SpaceRepo
|
|
||||||
import space.luminic.finance.repos.TransactionRepo
|
|
||||||
import java.time.LocalDate
|
|
||||||
import java.time.format.DateTimeFormatter
|
|
||||||
|
|
||||||
@Service
|
|
||||||
class NotificationServiceImpl(
|
|
||||||
private val userService: UserService,
|
|
||||||
private val bot: Bot,
|
|
||||||
@Lazy private val transactionRepo: TransactionRepo,
|
|
||||||
@Lazy private val spaceRepo: SpaceRepo
|
|
||||||
) : 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?mode=from_bot")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
override fun sendDailyReminder() {
|
|
||||||
val text = "🤑 Время заполнять траты!"
|
|
||||||
val users = userService.getUsers()
|
|
||||||
|
|
||||||
for (user in users) {
|
|
||||||
user.tgId?.let {
|
|
||||||
sendTextMessage(it, text, createWebAppButton())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun sendPlannedTransactionsReminder() {
|
|
||||||
val today = LocalDate.now()
|
|
||||||
val plannedTxs = transactionRepo.findByDateAndKind(today, Transaction.TransactionKind.PLANNING)
|
|
||||||
|
|
||||||
if (plannedTxs.isEmpty()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Группируем по spaceId. У нас нет Space объекта целиком в ответе findByDateAndKind,
|
|
||||||
// но есть t_space_id в маппере (хотя в модели Transaction.space может быть null).
|
|
||||||
// В RepoImpl маппер заполняет space.id
|
|
||||||
val groupedBySpace = plannedTxs.filter { it.space?.id != null }.groupBy { it.space!!.id!! }
|
|
||||||
|
|
||||||
for ((spaceId, txs) in groupedBySpace) {
|
|
||||||
// Чтобы получить владельца, нужно загрузить Space
|
|
||||||
// Используем системного пользователя или просто id для получения
|
|
||||||
val space = spaceRepo.findSpaceById(spaceId, txs.first().createdBy?.id ?: 0)
|
|
||||||
if (space?.owner?.tgId != null) {
|
|
||||||
val txList = txs.joinToString("\n") { "- ${it.comment}: ${it.amount}" }
|
|
||||||
val text = "📅 На сегодня запланированы транзакции:\n\n$txList"
|
|
||||||
sendTextMessage(space.owner.tgId!!, text, createWebAppButton(spaceId))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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")
|
|
||||||
)
|
|
||||||
}"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (changes.isNotEmpty()) {
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,8 +6,6 @@ import kotlinx.coroutines.SupervisorJob
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import org.springframework.transaction.annotation.Isolation
|
|
||||||
import org.springframework.transaction.annotation.Transactional
|
|
||||||
import space.luminic.finance.dtos.RecurrentOperationDTO
|
import space.luminic.finance.dtos.RecurrentOperationDTO
|
||||||
import space.luminic.finance.models.Category
|
import space.luminic.finance.models.Category
|
||||||
import space.luminic.finance.models.NotFoundException
|
import space.luminic.finance.models.NotFoundException
|
||||||
@@ -74,7 +72,7 @@ class RecurrentOperationServiceImpl(
|
|||||||
category = category,
|
category = category,
|
||||||
comment = creatingOperation.name,
|
comment = creatingOperation.name,
|
||||||
amount = creatingOperation.amount,
|
amount = creatingOperation.amount,
|
||||||
date = if (now.dayOfMonth < 10 && operation.date > 10) date.plusMonths((i-1).toLong()) else date.plusMonths(i.toLong()),
|
date = date.plusMonths(i.toLong()),
|
||||||
recurrentId = createdRecurrentId
|
recurrentId = createdRecurrentId
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -112,7 +110,7 @@ class RecurrentOperationServiceImpl(
|
|||||||
type = if (it.category?.type == Category.CategoryType.EXPENSE) Transaction.TransactionType.EXPENSE else Transaction.TransactionType.INCOME,
|
type = if (it.category?.type == Category.CategoryType.EXPENSE) Transaction.TransactionType.EXPENSE else Transaction.TransactionType.INCOME,
|
||||||
category = updatedOperation.category,
|
category = updatedOperation.category,
|
||||||
comment = operation.name,
|
comment = operation.name,
|
||||||
amount = operation.amount,
|
amount = updatedOperation.amount,
|
||||||
date = LocalDate.of(
|
date = LocalDate.of(
|
||||||
it.date.year,
|
it.date.year,
|
||||||
it.date.monthValue,
|
it.date.monthValue,
|
||||||
@@ -128,11 +126,10 @@ class RecurrentOperationServiceImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
|
|
||||||
override fun delete(spaceId: Int, id: Int) {
|
override fun delete(spaceId: Int, id: Int) {
|
||||||
val userId = authService.getSecurityUserId()
|
val userId = authService.getSecurityUserId()
|
||||||
spaceRepo.findSpaceById(spaceId, userId)?: throw NotFoundException("Cannot find space with id $id")
|
spaceRepo.findSpaceById(spaceId, userId)
|
||||||
transactionRepo.deleteByRecurrentId(spaceId, id)
|
|
||||||
recurrentOperationRepo.delete(id)
|
recurrentOperationRepo.delete(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,14 +4,11 @@ import org.slf4j.LoggerFactory
|
|||||||
import org.springframework.scheduling.annotation.EnableScheduling
|
import org.springframework.scheduling.annotation.EnableScheduling
|
||||||
import org.springframework.scheduling.annotation.Scheduled
|
import org.springframework.scheduling.annotation.Scheduled
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
@EnableScheduling
|
@EnableScheduling
|
||||||
@Service
|
@Service
|
||||||
class Scheduler(
|
class Scheduler(
|
||||||
private val recurrentOperationService: RecurrentOperationService,
|
private val recurrentOperationService: RecurrentOperationService
|
||||||
private val notificationService: NotificationService,
|
|
||||||
private val dashboardService: DashboardService
|
|
||||||
) {
|
) {
|
||||||
private val log = LoggerFactory.getLogger(Scheduler::class.java)
|
private val log = LoggerFactory.getLogger(Scheduler::class.java)
|
||||||
|
|
||||||
@@ -20,22 +17,4 @@ class Scheduler(
|
|||||||
log.info("Creating recurrent after 13 month")
|
log.info("Creating recurrent after 13 month")
|
||||||
recurrentOperationService.createRecurrentTransactions()
|
recurrentOperationService.createRecurrentTransactions()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Scheduled(cron = "0 30 16 * * *")
|
|
||||||
fun sendDailyReminders() {
|
|
||||||
log.info("Sending daily reminders")
|
|
||||||
notificationService.sendDailyReminder()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Scheduled(cron = "0 0 9 * * *")
|
|
||||||
fun sendPlannedTransactionsReminders() {
|
|
||||||
log.info("Sending planned transactions reminders")
|
|
||||||
notificationService.sendPlannedTransactionsReminder()
|
|
||||||
}
|
|
||||||
|
|
||||||
// @Scheduled(cron = "0 0 */3 * * *")
|
|
||||||
@Scheduled(fixedRate = 3, timeUnit =TimeUnit.HOURS)
|
|
||||||
fun analyzePeriodScheduled() {
|
|
||||||
dashboardService.analyzePeriodScheduled()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -2,10 +2,9 @@ package space.luminic.finance.services
|
|||||||
|
|
||||||
import space.luminic.finance.dtos.SpaceDTO
|
import space.luminic.finance.dtos.SpaceDTO
|
||||||
import space.luminic.finance.models.Space
|
import space.luminic.finance.models.Space
|
||||||
import java.time.LocalDateTime
|
|
||||||
|
|
||||||
interface SpaceService {
|
interface SpaceService {
|
||||||
fun getSpacesForScheduling(lastRun: LocalDateTime? = null): List<Space>
|
|
||||||
fun checkSpace(spaceId: Int): Space
|
fun checkSpace(spaceId: Int): Space
|
||||||
fun getSpaces(): List<Space>
|
fun getSpaces(): List<Space>
|
||||||
fun getSpace(id: Int, userId: Int?): Space
|
fun getSpace(id: Int, userId: Int?): Space
|
||||||
|
|||||||
@@ -6,9 +6,6 @@ import space.luminic.finance.dtos.SpaceDTO
|
|||||||
import space.luminic.finance.models.NotFoundException
|
import space.luminic.finance.models.NotFoundException
|
||||||
import space.luminic.finance.models.Space
|
import space.luminic.finance.models.Space
|
||||||
import space.luminic.finance.repos.SpaceRepo
|
import space.luminic.finance.repos.SpaceRepo
|
||||||
import java.time.Instant
|
|
||||||
import java.time.LocalDateTime
|
|
||||||
import java.time.ZoneOffset
|
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
class SpaceServiceImpl(
|
class SpaceServiceImpl(
|
||||||
@@ -16,11 +13,6 @@ class SpaceServiceImpl(
|
|||||||
private val spaceRepo: SpaceRepo,
|
private val spaceRepo: SpaceRepo,
|
||||||
private val categoryService: CategoryService
|
private val categoryService: CategoryService
|
||||||
) : SpaceService {
|
) : SpaceService {
|
||||||
override fun getSpacesForScheduling(lastRun: LocalDateTime?): List<Space> {
|
|
||||||
val lastRunDate = lastRun ?: LocalDateTime.ofInstant(Instant.EPOCH, ZoneOffset.UTC)
|
|
||||||
return spaceRepo.findSpacesForScheduling(lastRunDate)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun checkSpace(spaceId: Int): Space {
|
override fun checkSpace(spaceId: Int): Space {
|
||||||
return getSpace(spaceId, null)
|
return getSpace(spaceId, null)
|
||||||
}
|
}
|
||||||
@@ -74,7 +66,6 @@ class SpaceServiceImpl(
|
|||||||
)
|
)
|
||||||
return spaceRepo.update(updatedSpace, userId)
|
return spaceRepo.update(updatedSpace, userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
override fun deleteSpace(spaceId: Int) {
|
override fun deleteSpace(spaceId: Int) {
|
||||||
spaceRepo.delete(spaceId)
|
spaceRepo.delete(spaceId)
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package space.luminic.finance.services
|
||||||
|
|
||||||
|
import space.luminic.finance.dtos.TargetDTO
|
||||||
|
import space.luminic.finance.models.Target
|
||||||
|
|
||||||
|
interface TargetService {
|
||||||
|
fun findAllBySpaceId(spaceId: Int): List<Target>
|
||||||
|
fun findBySpaceIdAndId(spaceId: Int, id: Int): Target
|
||||||
|
fun create(spaceId: Int,target: TargetDTO.CreateTargetDTO): Int
|
||||||
|
fun update(spaceId: Int, targetId: Int, target: TargetDTO.UpdateTargetDTO)
|
||||||
|
fun delete(spaceId: Int, id: Int)
|
||||||
|
|
||||||
|
fun getComponents(spaceId: Int, targetId: Int): List<Target.TargetComponent>
|
||||||
|
fun getComponent(spaceId: Int, targetId: Int, id: Int): Target.TargetComponent?
|
||||||
|
fun createComponent(spaceId: Int, targetId: Int, component: Target.TargetComponent): Int
|
||||||
|
fun updateComponent(spaceId: Int, targetId: Int, component: Target.TargetComponent)
|
||||||
|
fun deleteComponent(spaceId: Int, targetId: Int, id: Int)
|
||||||
|
|
||||||
|
fun assignTransaction(spaceId: Int, targetId: Int, transactionId: Int)
|
||||||
|
fun refuseTransaction(spaceId: Int,targetId: Int, transactionId: Int)
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
package space.luminic.finance.services
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import space.luminic.finance.dtos.TargetDTO
|
||||||
|
import space.luminic.finance.models.Target
|
||||||
|
import space.luminic.finance.models.NotFoundException
|
||||||
|
import space.luminic.finance.repos.TargetRepo
|
||||||
|
import space.luminic.finance.repos.SpaceRepo
|
||||||
|
import space.luminic.finance.repos.TransactionRepo
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class TargetServiceImpl(
|
||||||
|
private val targetRepo: TargetRepo,
|
||||||
|
private val spaceRepo: SpaceRepo,
|
||||||
|
private val authService: AuthService,
|
||||||
|
private val transactionRepo: TransactionRepo
|
||||||
|
) : TargetService {
|
||||||
|
override fun findAllBySpaceId(spaceId: Int): List<Target> {
|
||||||
|
val userId = authService.getSecurityUserId()
|
||||||
|
spaceRepo.findSpaceById(spaceId, userId)
|
||||||
|
return targetRepo.findAllBySpaceId(spaceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun findBySpaceIdAndId(spaceId: Int, id: Int): Target {
|
||||||
|
val userId = authService.getSecurityUserId()
|
||||||
|
spaceRepo.findSpaceById(spaceId, userId)
|
||||||
|
return targetRepo.findBySpaceIdAndId(spaceId, userId) ?: throw NotFoundException("Goal $id not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun create(spaceId: Int, target: TargetDTO.CreateTargetDTO): Int {
|
||||||
|
val userId = authService.getSecurityUserId()
|
||||||
|
spaceRepo.findSpaceById(spaceId, userId)
|
||||||
|
val creatingTarget = Target(
|
||||||
|
type = target.type,
|
||||||
|
name = target.name,
|
||||||
|
amount = target.amount,
|
||||||
|
untilDate = target.date
|
||||||
|
)
|
||||||
|
return targetRepo.create(creatingTarget, userId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun update(spaceId: Int, targetId: Int, target: TargetDTO.UpdateTargetDTO) {
|
||||||
|
val userId = authService.getSecurityUserId()
|
||||||
|
spaceRepo.findSpaceById(spaceId, userId)
|
||||||
|
val existingGoal =
|
||||||
|
targetRepo.findBySpaceIdAndId(spaceId, targetId) ?: throw NotFoundException("Goal $targetId not found")
|
||||||
|
val updatedGoal = existingGoal.copy(
|
||||||
|
type = target.type,
|
||||||
|
name = target.name,
|
||||||
|
description = target.description,
|
||||||
|
amount = target.amount,
|
||||||
|
untilDate = target.date
|
||||||
|
)
|
||||||
|
targetRepo.update(updatedGoal, userId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun delete(spaceId: Int, id: Int) {
|
||||||
|
targetRepo.delete(spaceId, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getComponents(
|
||||||
|
spaceId: Int,
|
||||||
|
targetId: Int
|
||||||
|
): List<Target.TargetComponent> {
|
||||||
|
val userId = authService.getSecurityUserId()
|
||||||
|
spaceRepo.findSpaceById(spaceId, userId)
|
||||||
|
targetRepo.findBySpaceIdAndId(spaceId, targetId) ?: throw NotFoundException("Goal $targetId not found")
|
||||||
|
return targetRepo.getComponents(spaceId, targetId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getComponent(
|
||||||
|
spaceId: Int,
|
||||||
|
targetId: Int,
|
||||||
|
id: Int
|
||||||
|
): Target.TargetComponent? {
|
||||||
|
val userId = authService.getSecurityUserId()
|
||||||
|
spaceRepo.findSpaceById(spaceId, userId)
|
||||||
|
targetRepo.findBySpaceIdAndId(spaceId, targetId) ?: throw NotFoundException("Target $targetId not found")
|
||||||
|
return targetRepo.getComponent(spaceId, targetId, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createComponent(
|
||||||
|
spaceId: Int,
|
||||||
|
targetId: Int,
|
||||||
|
component: Target.TargetComponent
|
||||||
|
): Int {
|
||||||
|
val userId = authService.getSecurityUserId()
|
||||||
|
spaceRepo.findSpaceById(spaceId, userId)
|
||||||
|
targetRepo.findBySpaceIdAndId(spaceId, targetId) ?: throw NotFoundException("Target $targetId not found")
|
||||||
|
return targetRepo.createComponent(targetId, component, userId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateComponent(
|
||||||
|
spaceId: Int,
|
||||||
|
targetId: Int,
|
||||||
|
component: Target.TargetComponent
|
||||||
|
) {
|
||||||
|
val userId = authService.getSecurityUserId()
|
||||||
|
spaceRepo.findSpaceById(spaceId, userId)
|
||||||
|
targetRepo.findBySpaceIdAndId(spaceId, targetId) ?: throw NotFoundException("Target $targetId not found")
|
||||||
|
val existingComponent = targetRepo.getComponent(spaceId, targetId, component.id!!)
|
||||||
|
?: throw NotFoundException("Component $targetId not found")
|
||||||
|
val updatedComponent = existingComponent.copy(
|
||||||
|
name = component.name,
|
||||||
|
amount = component.amount,
|
||||||
|
isDone = component.isDone,
|
||||||
|
date = component.date
|
||||||
|
)
|
||||||
|
targetRepo.updateComponent(targetId, updatedComponent.id!!, updatedComponent, userId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deleteComponent(spaceId: Int, targetId: Int, id: Int) {
|
||||||
|
val userId = authService.getSecurityUserId()
|
||||||
|
spaceRepo.findSpaceById(spaceId, userId)
|
||||||
|
targetRepo.findBySpaceIdAndId(spaceId, targetId) ?: throw NotFoundException("Target $targetId not found")
|
||||||
|
targetRepo.getComponent(spaceId, targetId, id) ?: throw NotFoundException("Component $targetId not found")
|
||||||
|
targetRepo.deleteComponent(targetId, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun assignTransaction(spaceId: Int, targetId: Int, transactionId: Int) {
|
||||||
|
val userId = authService.getSecurityUserId()
|
||||||
|
spaceRepo.findSpaceById(spaceId, userId)
|
||||||
|
targetRepo.findBySpaceIdAndId(spaceId, targetId) ?: throw NotFoundException("Target $targetId not found")
|
||||||
|
transactionRepo.findBySpaceIdAndId(spaceId, transactionId) ?: throw NotFoundException(
|
||||||
|
"Transaction $transactionId not found"
|
||||||
|
)
|
||||||
|
targetRepo.assignTransaction(targetId, transactionId)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun refuseTransaction(spaceId: Int, targetId: Int, transactionId: Int) {
|
||||||
|
val userId = authService.getSecurityUserId()
|
||||||
|
spaceRepo.findSpaceById(spaceId, userId)
|
||||||
|
targetRepo.findBySpaceIdAndId(spaceId, targetId) ?: throw NotFoundException("Target $targetId not found")
|
||||||
|
transactionRepo.findBySpaceIdAndId(spaceId, transactionId) ?: throw NotFoundException(
|
||||||
|
"Transaction $transactionId not found"
|
||||||
|
)
|
||||||
|
targetRepo.refuseTransaction(targetId, transactionId)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,41 +7,22 @@ import java.time.LocalDate
|
|||||||
interface TransactionService {
|
interface TransactionService {
|
||||||
|
|
||||||
data class TransactionsFilter(
|
data class TransactionsFilter(
|
||||||
val query : String? = null,
|
|
||||||
val type: Transaction.TransactionType? = null,
|
val type: Transaction.TransactionType? = null,
|
||||||
val kind: Transaction.TransactionKind? = null,
|
val kind: Transaction.TransactionKind? = null,
|
||||||
val categoriesIds: Set<Int>? = null,
|
|
||||||
val dateFrom: LocalDate? = null,
|
val dateFrom: LocalDate? = null,
|
||||||
val dateTo: LocalDate? = null,
|
val dateTo: LocalDate? = null,
|
||||||
val isDone: Boolean? = null,
|
val isDone: Boolean? = null,
|
||||||
val offset: Int = 0,
|
val offset: Int = 0,
|
||||||
val limit: Int = 10,
|
val limit: Int = 10,
|
||||||
val sorts: List<Map<String, String>> = listOf(
|
|
||||||
mapOf(
|
|
||||||
"sortBy" to "t.created_at",
|
|
||||||
"sortDirection" to SortDirection.DESC.name
|
|
||||||
),
|
|
||||||
mapOf(
|
|
||||||
"sortBy" to "t.id",
|
|
||||||
"sortDirection" to SortDirection.ASC.name
|
|
||||||
)
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
enum class SortDirection {
|
|
||||||
ASC, DESC
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getTransactions(
|
fun getTransactions(
|
||||||
spaceId: Int,
|
spaceId: Int,
|
||||||
filter: TransactionsFilter
|
filter: TransactionsFilter,
|
||||||
|
sortBy: String,
|
||||||
|
sortDirection: String
|
||||||
): 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?)
|
||||||
|
|||||||
@@ -1,20 +1,11 @@
|
|||||||
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.io.ByteArrayOutputStream
|
|
||||||
import java.time.LocalDate
|
|
||||||
import java.time.LocalDateTime
|
|
||||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook
|
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
class TransactionServiceImpl(
|
class TransactionServiceImpl(
|
||||||
@@ -23,59 +14,17 @@ 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,
|
||||||
|
sortDirection: String
|
||||||
): List<Transaction> {
|
): List<Transaction> {
|
||||||
val transactions = transactionRepo.findAllBySpaceId(spaceId, filter)
|
val transactions = transactionRepo.findAllBySpaceId(spaceId, filter)
|
||||||
return transactions
|
return transactions
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun generateExcel(
|
|
||||||
spaceId: Int,
|
|
||||||
filter: TransactionService.TransactionsFilter
|
|
||||||
): ByteArray {
|
|
||||||
val fullFilter = filter.copy(limit = 10000, offset = 0)
|
|
||||||
val transactions = getTransactions(spaceId, fullFilter)
|
|
||||||
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
|
||||||
@@ -104,17 +53,7 @@ class TransactionServiceImpl(
|
|||||||
date = transaction.date,
|
date = transaction.date,
|
||||||
recurrentId = transaction.recurrentId,
|
recurrentId = transaction.recurrentId,
|
||||||
)
|
)
|
||||||
val createdTx = transactionRepo.create(transaction, userId)
|
return 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?) {
|
||||||
@@ -146,22 +85,15 @@ 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) }
|
||||||
val today = LocalDate.now()
|
|
||||||
val newKind = if (
|
|
||||||
!existingTransaction.isDone &&
|
|
||||||
transaction.isDone &&
|
|
||||||
(today.isAfter(transaction.date) || today.isEqual(transaction.date) || today.isBefore(transaction.date))
|
|
||||||
) Transaction.TransactionKind.INSTANT else transaction.kind
|
|
||||||
val updatedTransaction = Transaction(
|
val updatedTransaction = Transaction(
|
||||||
id = existingTransaction.id,
|
id = existingTransaction.id,
|
||||||
space = existingTransaction.space,
|
space = existingTransaction.space,
|
||||||
parent = existingTransaction.parent,
|
parent = existingTransaction.parent,
|
||||||
type = transaction.type,
|
type = transaction.type,
|
||||||
kind = newKind,
|
kind = transaction.kind,
|
||||||
category = newCategory,
|
category = newCategory,
|
||||||
comment = transaction.comment,
|
comment = transaction.comment,
|
||||||
amount = transaction.amount,
|
amount = transaction.amount,
|
||||||
@@ -177,34 +109,14 @@ class TransactionServiceImpl(
|
|||||||
if ((existingTransaction.category == null && updatedTransaction.category != null) || (existingTransaction.category?.id != updatedTransaction.category?.id)) {
|
if ((existingTransaction.category == null && updatedTransaction.category != null) || (existingTransaction.category?.id != updatedTransaction.category?.id)) {
|
||||||
categorizeService.notifyThatCategorySelected(updatedTransaction)
|
categorizeService.notifyThatCategorySelected(updatedTransaction)
|
||||||
}
|
}
|
||||||
val updatedTx = transactionRepo.update(updatedTransaction)
|
|
||||||
serviceScope.launch {
|
|
||||||
runCatching {
|
|
||||||
|
|
||||||
notificationService.sendTXNotification(
|
return transactionRepo.update(updatedTransaction)
|
||||||
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)
|
||||||
val tx = getTransaction(space.id!!, transactionId)
|
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) {
|
||||||
|
|||||||
@@ -40,7 +40,4 @@ class UserService(val userRepo: UserRepo) {
|
|||||||
return userRepo.findAll()
|
return userRepo.findAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun update(user: User): User {
|
|
||||||
return userRepo.update(user)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -12,7 +12,6 @@ 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
|
||||||
@@ -39,18 +38,9 @@ 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,
|
||||||
categories
|
categoriesRepo.findBySpaceId(job.spaceId)
|
||||||
) // тут твой вызов GPT
|
) // тут твой вызов GPT
|
||||||
var message: TelegramBotResult<Message>? = null
|
var message: TelegramBotResult<Message>? = null
|
||||||
|
|
||||||
@@ -95,7 +85,7 @@ class CategorizeService(
|
|||||||
listOf(
|
listOf(
|
||||||
InlineKeyboardButton.WebApp(
|
InlineKeyboardButton.WebApp(
|
||||||
"Открыть в WebApp",
|
"Открыть в WebApp",
|
||||||
WebAppInfo("https://app.luminic.space/transactions/${tx.id}/edit?mode=from_bot"),
|
WebAppInfo("https://app.luminic.space/transactions/${tx.id}/edit?mode=from_bot")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
@@ -141,7 +131,7 @@ class CategorizeService(
|
|||||||
listOf(
|
listOf(
|
||||||
InlineKeyboardButton.WebApp(
|
InlineKeyboardButton.WebApp(
|
||||||
"Открыть в WebApp",
|
"Открыть в WebApp",
|
||||||
WebAppInfo("https://app.luminic.space/transactions/${tx.id}/edit?mode=from_bot"),
|
WebAppInfo("https://app.luminic.space/transactions/${tx.id}/edit?mode=from_bot")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
package space.luminic.finance.services.gpt
|
package space.luminic.finance.services.gpt
|
||||||
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
|
|
||||||
import okhttp3.*
|
import okhttp3.*
|
||||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
import com.fasterxml.jackson.module.kotlin.readValue
|
import com.fasterxml.jackson.module.kotlin.readValue
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
@@ -14,10 +11,7 @@ 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.Category
|
||||||
import space.luminic.finance.models.DashboardData
|
|
||||||
import space.luminic.finance.models.Transaction
|
import space.luminic.finance.models.Transaction
|
||||||
import java.time.LocalDate
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
|
|
||||||
@Service("dsCategorizationService")
|
@Service("dsCategorizationService")
|
||||||
@@ -27,17 +21,16 @@ class DeepSeekCategorizationService(
|
|||||||
|
|
||||||
private val endpoint = "https://api.deepseek.com/v1"
|
private val endpoint = "https://api.deepseek.com/v1"
|
||||||
private val mapper = jacksonObjectMapper()
|
private val mapper = jacksonObjectMapper()
|
||||||
|
private val client = OkHttpClient()
|
||||||
private val client = OkHttpClient().newBuilder().callTimeout(5, TimeUnit.MINUTES).readTimeout(1, TimeUnit.MINUTES).build()
|
|
||||||
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 = """
|
||||||
{ \"type\": \"${tx.type.displayName}\", \"amount\": ${tx.amount}, \"comment\": \"${tx.comment}\", \"date\":${tx.date}\" }
|
{ \"amount\": ${tx.amount}, \"comment\": \"${tx.comment}\", \"date\":${tx.date}\" }
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
val prompt = """
|
val prompt = """
|
||||||
Пользователь имеет следующие категории (${tx.type.displayName}):
|
Пользователь имеет следующие категории:
|
||||||
$catList
|
$catList
|
||||||
|
|
||||||
Задача:
|
Задача:
|
||||||
@@ -81,181 +74,7 @@ class DeepSeekCategorizationService(
|
|||||||
|
|
||||||
// val (idStr, name, confStr) = match.destructured
|
// val (idStr, name, confStr) = match.destructured
|
||||||
val idStr = text
|
val idStr = text
|
||||||
return CategorySuggestion(idStr.toInt())
|
return CategorySuggestion(idStr.toInt(), )
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun analyzePeriod(startDate: LocalDate, endDate: LocalDate, dashboardData: DashboardData): String {
|
|
||||||
mapper.registerModule(JavaTimeModule());
|
|
||||||
if (dashboardData.totalIncome == 0 && dashboardData.totalExpense == 0){
|
|
||||||
return """{ "common": "Начните записывать траты и поступления и мы начнем их анализировать!", "categoryAnalysis": "", "keyInsights": "", "recommendations": "" }"""
|
|
||||||
} else {
|
|
||||||
var prompt = """
|
|
||||||
You are a personal finance analyst.
|
|
||||||
Your task is to analyze the user’s budget period and provide:
|
|
||||||
1) A summary,
|
|
||||||
2) Key insights,
|
|
||||||
3) Explanations of deviations,
|
|
||||||
4) Actionable recommendations.
|
|
||||||
|
|
||||||
The answer MUST be written in Russian.
|
|
||||||
|
|
||||||
IMPORTANT: The user’s budget is calculated in custom periods, not strict calendar months. A period is usually about a month, for example: starts on 10 November and ends on 9 December. The period may be ongoing or already finished.
|
|
||||||
|
|
||||||
Here is the data for the current budget period:
|
|
||||||
|
|
||||||
Period label (for display, may be month-like): {{period}}
|
|
||||||
Current income: {{income}}
|
|
||||||
Current expense: {{expense}}
|
|
||||||
Net result: {{net}}
|
|
||||||
|
|
||||||
Comparison with the previous period:
|
|
||||||
- Income: {{income_change_percent}}%
|
|
||||||
- Expense: {{expense_change_percent}}%
|
|
||||||
- Net result change: {{net_change_value}} ({{net_change_percent}}%)
|
|
||||||
|
|
||||||
Expense categories:
|
|
||||||
{{categories_list}}
|
|
||||||
|
|
||||||
(format for categories_list example:
|
|
||||||
"Еда — план {{plan}}, факт {{actual}}, отклонение {{diff}} ({{diff_percent}}%)"
|
|
||||||
one category per line)
|
|
||||||
|
|
||||||
Response formatting requirements:
|
|
||||||
|
|
||||||
1. The final response MUST be valid json.:
|
|
||||||
2. Use a HTML text formatting like (<b>, <i>, <u> only) in sections.
|
|
||||||
|
|
||||||
"common" - Общая оценка периода
|
|
||||||
"categoryAnalysis" - Анализ по категориям
|
|
||||||
"keyInsights" - Ключевые инсайты
|
|
||||||
"recommendations" - Рекомендации
|
|
||||||
|
|
||||||
{ "common": "string",
|
|
||||||
"categoryAnalysis": "string",
|
|
||||||
"keyInsights": "string",
|
|
||||||
"recommendations": "string",
|
|
||||||
}
|
|
||||||
|
|
||||||
Content requirements for each section (in Russian):
|
|
||||||
|
|
||||||
Section 1: "Общая оценка периода"
|
|
||||||
- Give a short 1–2 sentence overview of how the budget period is going financially (баланс, общая тенденция, спокойный/напряжённый период и т.п.).
|
|
||||||
- Mention overall direction compared to the previous period (лучше/хуже/на том же уровне), but do NOT restate all numeric values exactly.
|
|
||||||
- If the period is still ongoing, clearly indicate that the evaluation is preliminary (e.g. "на данный момент", "пока", "в текущем периоде").
|
|
||||||
|
|
||||||
Section 2: "Анализ по категориям"
|
|
||||||
- Explain which 2–4 categories had the biggest impact on the budget (самые крупные, самые проблемные или самые улучшившиеся).
|
|
||||||
- Опиши, как изменились привычки в этих категориях (больше/меньше, чем обычно или чем в прошлом периоде, почему это может быть так).
|
|
||||||
- Use a list-style text with "-" at the start of lines for key category observations.
|
|
||||||
|
|
||||||
Section 3: "Ключевые инсайты"
|
|
||||||
- Highlight 2–4 important insights about the user’s behavior and spending patterns in this period.
|
|
||||||
- For each insight, briefly explain the possible cause or context (например, рост доставки еды, больше поездок, крупные единовременные покупки).
|
|
||||||
- Use "-" bullets (text-only, not HTML list tags).
|
|
||||||
|
|
||||||
Section 4: "Рекомендации"
|
|
||||||
- Provide 3–5 concrete, actionable recommendations the user can apply in the next period or in the remaining part of the current period.
|
|
||||||
- Recommendations must be practical actions (e.g., "ограничить спонтанные покупки по одной категории", "установить лимит", "вынести обязательные платежи в начало периода").
|
|
||||||
- VERY IMPORTANT: Do NOT repeat exact numeric values from the input in the recommendations. Focus on actions and относительные формулировки (уменьшить, зафиксировать, перенести, разделить и т.п.).
|
|
||||||
- Use "-" bullets (text-only, not HTML list tags).
|
|
||||||
|
|
||||||
General style requirements:
|
|
||||||
- The tone should be friendly, professional, and supportive, without blaming the user.
|
|
||||||
- Keep the writing structured, concise, and avoid filler.
|
|
||||||
- Do NOT invent completely unrealistic events; base your reasoning on the provided data patterns.
|
|
||||||
|
|
||||||
CONTEXT:
|
|
||||||
today: {{TODAY}}
|
|
||||||
period starts: {{START_DATE}}
|
|
||||||
period ends: {{END_DATE}}
|
|
||||||
|
|
||||||
Dates are provided in ISO format (YYYY-MM-DD).
|
|
||||||
|
|
||||||
PERIOD STATUS LOGIC (VERY IMPORTANT):
|
|
||||||
- If {{TODAY}} < {{START_DATE}}:
|
|
||||||
- Treat the period as future/planned.
|
|
||||||
- Do NOT write as if the period has already started or finished.
|
|
||||||
- Use formulations like "запланированный период", "ожидаемые траты".
|
|
||||||
- If {{START_DATE}} <= {{TODAY}} <= {{END_DATE}}:
|
|
||||||
- Treat the period as ongoing.
|
|
||||||
- Do NOT say or imply that the period has already finished or "завершился".
|
|
||||||
- Avoid phrases like "по итогам периода" or "период завершился".
|
|
||||||
- Use formulations like "в текущем периоде", "на данный момент", "пока".
|
|
||||||
- If {{TODAY}} > {{END_DATE}}:
|
|
||||||
- Treat the period as completed.
|
|
||||||
- You MAY use phrases like "по итогам периода", "период завершился", "за весь период".
|
|
||||||
|
|
||||||
REMINDER:
|
|
||||||
- Always check the relationship between today, period start and period end when choosing wording (ongoing vs completed vs future).
|
|
||||||
- The final response MUST be in Russian.
|
|
||||||
- The final response MUST be valid json with fields:
|
|
||||||
"common" - Общая оценка периода
|
|
||||||
"categoryAnalysis" - Анализ по категориям
|
|
||||||
"keyInsights" - Ключевые инсайты
|
|
||||||
"recommendations" - Рекомендации
|
|
||||||
|
|
||||||
{ "common": "string",
|
|
||||||
"categoryAnalysis": "string",
|
|
||||||
"keyInsights": "string",
|
|
||||||
"recommendations": "string",
|
|
||||||
}
|
|
||||||
""".trimIndent()
|
|
||||||
prompt = prompt.replace("{{TODAY}}", LocalDate.now().toString())
|
|
||||||
prompt = prompt.replace("{{period}}", "$startDate - $endDate")
|
|
||||||
prompt = prompt.replace("{{income}}", dashboardData.totalIncome.toString())
|
|
||||||
prompt = prompt.replace("{{expense}}", dashboardData.totalExpense.toString())
|
|
||||||
prompt = prompt.replace("{{net}}", dashboardData.balance.toString())
|
|
||||||
prompt = prompt.replace("{{income_change_percent}}", dashboardData.prevCurIncomeChange.toString())
|
|
||||||
prompt = prompt.replace("{{expense_change_percent}}", dashboardData.prevCurExpenseChange.toString())
|
|
||||||
prompt =
|
|
||||||
prompt.replace("{{net_change_value}}", (dashboardData.balance - dashboardData.prevBalance).toString())
|
|
||||||
prompt = prompt.replace(
|
|
||||||
"{{net_change_percent}}", if (dashboardData.prevBalance != 0)
|
|
||||||
((dashboardData.balance / dashboardData.prevBalance) * 100).toString() else "0"
|
|
||||||
)
|
|
||||||
prompt = prompt.replace(
|
|
||||||
"{{categories_list}}",
|
|
||||||
dashboardData.categories.joinToString(",") { mapper.writeValueAsString(it) })
|
|
||||||
|
|
||||||
prompt = prompt.replace("{{START_DATE}}", startDate.toString())
|
|
||||||
prompt = prompt.replace("{{END_DATE}}", endDate.toString())
|
|
||||||
val body = mapOf(
|
|
||||||
"model" to "deepseek-chat",
|
|
||||||
"messages" to listOf(
|
|
||||||
// mapOf("role" to "assistant", "content" to prompt),
|
|
||||||
mapOf("role" to "user", "content" to prompt)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
val jsonBody = mapper.writeValueAsString(body)
|
|
||||||
val requestBody = jsonBody.toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull())
|
|
||||||
val request = Request.Builder()
|
|
||||||
.url("${endpoint}/chat/completions")
|
|
||||||
.addHeader("Authorization", "Bearer $apiKey")
|
|
||||||
.post(requestBody)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
logger.info("start analyze period $startDate - $endDate ")
|
|
||||||
client.newCall(request).execute().use { response ->
|
|
||||||
if (!response.isSuccessful) error("Qwen error: ${response.code} ${response.body?.string()}")
|
|
||||||
|
|
||||||
val bodyStr = response.body?.string().orEmpty()
|
|
||||||
|
|
||||||
// Берём content из choices[0].message.content
|
|
||||||
val root = mapper.readTree(bodyStr)
|
|
||||||
val text = root["choices"]?.get(0)?.get("message")?.get("content")?.asText()
|
|
||||||
?: error("No choices[0].message.content in response")
|
|
||||||
|
|
||||||
// Парсим "ID – Название (вероятность)"
|
|
||||||
// val regex = Regex("""^\s*(\d+)\s*[–-]\s*(.+?)\s*\((0(?:\.\d+)?|1(?:\.0)?)\)\s*$""")
|
|
||||||
// val match = regex.find(text.trim()) ?: error("Bad format: '$text'")
|
|
||||||
|
|
||||||
// val (idStr, name, confStr) = match.destructured
|
|
||||||
logger.debug("got anazyled text: $text")
|
|
||||||
val idStr = text.replace("```json", "").replace("```", "")
|
|
||||||
logger.info("stopped analyze period $startDate - $endDate ")
|
|
||||||
return idStr
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,10 @@
|
|||||||
package space.luminic.finance.services.gpt
|
package space.luminic.finance.services.gpt
|
||||||
|
|
||||||
import space.luminic.finance.models.Category
|
import space.luminic.finance.models.Category
|
||||||
import space.luminic.finance.models.DashboardData
|
|
||||||
import space.luminic.finance.models.Transaction
|
import space.luminic.finance.models.Transaction
|
||||||
import java.time.LocalDate
|
|
||||||
|
|
||||||
data class CategorySuggestion(val categoryId: Int, val categoryName: String? = null, val confidence: Double? = null)
|
data class CategorySuggestion(val categoryId: Int, val categoryName: String? = null, val confidence: Double? = null)
|
||||||
|
|
||||||
interface GptClient {
|
interface GptClient {
|
||||||
fun suggestCategory(tx: Transaction, categories: List<Category>): CategorySuggestion
|
fun suggestCategory(tx: Transaction, categories: List<Category>): CategorySuggestion
|
||||||
fun analyzePeriod(startDate: LocalDate, endDate: LocalDate, dashboardData: DashboardData): String
|
|
||||||
}
|
}
|
||||||
@@ -3,15 +3,15 @@ package space.luminic.finance.services.gpt
|
|||||||
|
|
||||||
import okhttp3.*
|
import okhttp3.*
|
||||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
|
import com.fasterxml.jackson.module.kotlin.readValue
|
||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
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.Category
|
||||||
import space.luminic.finance.models.DashboardData
|
|
||||||
import space.luminic.finance.models.Transaction
|
import space.luminic.finance.models.Transaction
|
||||||
import java.time.LocalDate
|
|
||||||
|
|
||||||
@Service("qwenCategorizationService")
|
@Service("qwenCategorizationService")
|
||||||
class QwenCategorizationService(
|
class QwenCategorizationService(
|
||||||
@@ -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 {
|
|
||||||
val catList = categories.joinToString("\n") { "- ${it.id}: ${it.name}" }
|
|
||||||
val txInfo = """
|
|
||||||
{ \"type\": \"${tx.type.displayName}\", \"amount\": ${tx.amount}, \"comment\": \"${tx.comment}\", \"date\":${tx.date}\" }
|
|
||||||
""".trimIndent()
|
|
||||||
val prompt = """
|
|
||||||
Пользователь имеет следующие категории (${tx.type.displayName}):
|
|
||||||
$catList
|
|
||||||
|
|
||||||
Задача:
|
override fun suggestCategory(tx: Transaction, categories: List<Category>): CategorySuggestion {
|
||||||
1. Определите наиболее подходящую категорию из списка выше для транзакции пользователя.
|
val catList = categories.joinToString("\n") { "- ${it.id}: ${it.name}" }
|
||||||
2. Верните ответ в формате: "ID категории – имя категории (вероятность)", например "3 – Продукты (0.87)".
|
val txInfo = """
|
||||||
...
|
{ \"amount\": ${tx.amount}, \"comment\": \"${tx.comment}\", \"date\":${tx.date}\" }
|
||||||
|
""".trimIndent()
|
||||||
|
val prompt = """
|
||||||
|
Пользователь имеет следующие категории:
|
||||||
|
$catList
|
||||||
|
|
||||||
|
Задача:
|
||||||
|
1. Определите наиболее подходящую категорию из списка выше для транзакции пользователя.
|
||||||
|
2. Верните ответ в формате: "ID категории – имя категории (вероятность)", например "3 – Продукты (0.87)".
|
||||||
3. Если ни одна категория из списка не подходит, верните: "0 – Другое (вероятность)".
|
3. Если ни одна категория из списка не подходит, верните: "0 – Другое (вероятность)".
|
||||||
|
|
||||||
Ответ должен быть кратким, одной строкой, без дополнительных пояснений.
|
Ответ должен быть кратким, одной строкой, без дополнительных пояснений.
|
||||||
@@ -75,8 +75,4 @@ override fun suggestCategory(tx: Transaction, categories: List<Category>): Categ
|
|||||||
return CategorySuggestion(idStr.toInt(), name, confStr.toDouble())
|
return CategorySuggestion(idStr.toInt(), name, confStr.toDouble())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun analyzePeriod(startDate: LocalDate, endDate: LocalDate, dashboardData: DashboardData): String {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -16,7 +16,6 @@ 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
|
||||||
@@ -31,11 +30,10 @@ 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,
|
||||||
@Lazy @Qualifier("transactionsServiceTelegram") private val transactionService: TransactionService
|
@Qualifier("transactionsServiceTelegram") private val transactionService: TransactionService
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
|
||||||
@@ -110,18 +108,18 @@ class BotService(
|
|||||||
listOf(
|
listOf(
|
||||||
InlineKeyboardButton.WebApp(
|
InlineKeyboardButton.WebApp(
|
||||||
text = "Открыть WebApp",
|
text = "Открыть WebApp",
|
||||||
webApp = WebAppInfo(url = "https://app.luminic.space?mode=from_bot")
|
webApp = WebAppInfo(url = "https://app.luminic.space")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
return InlineKeyboardMarkup.create(keyboard)
|
return InlineKeyboardMarkup.Companion.create(keyboard)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
fun bot(): Bot {
|
fun bot(): Bot {
|
||||||
val bot = com.github.kotlintelegrambot.bot {
|
val bot = com.github.kotlintelegrambot.bot {
|
||||||
logLevel = if (profile == "prod") LogLevel.None else LogLevel.All()
|
logLevel = LogLevel.None
|
||||||
token = botToken
|
token = botToken
|
||||||
dispatch {
|
dispatch {
|
||||||
message(Filter.Text) {
|
message(Filter.Text) {
|
||||||
@@ -131,18 +129,11 @@ class BotService(
|
|||||||
when (state?.state) {
|
when (state?.state) {
|
||||||
State.StateCode.SPACE_SELECTED -> {
|
State.StateCode.SPACE_SELECTED -> {
|
||||||
try {
|
try {
|
||||||
var text = message.text!!.trim()
|
val parts = message.text!!.trim().split(" ", limit = 2)
|
||||||
var type = Transaction.TransactionType.EXPENSE
|
if (parts.isEmpty()) {
|
||||||
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 обед` или `+1000 зп` ",
|
text = "Введите сумму и комментарий, например: `250 обед`",
|
||||||
parseMode = ParseMode.MARKDOWN
|
parseMode = ParseMode.MARKDOWN
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -168,7 +159,7 @@ class BotService(
|
|||||||
?: throw IllegalArgumentException("selected space is empty"),
|
?: throw IllegalArgumentException("selected space is empty"),
|
||||||
user.id!!,
|
user.id!!,
|
||||||
TransactionDTO.CreateTransactionDTO(
|
TransactionDTO.CreateTransactionDTO(
|
||||||
type,
|
Transaction.TransactionType.EXPENSE,
|
||||||
Transaction.TransactionKind.INSTANT,
|
Transaction.TransactionKind.INSTANT,
|
||||||
comment = comment,
|
comment = comment,
|
||||||
amount = amount.toBigDecimal(),
|
amount = amount.toBigDecimal(),
|
||||||
@@ -210,7 +201,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 <i>+сумма комментарий</i> (пополнения)\n\n <b>Первой обязательно должна быть сумма!</b>",
|
text = "Успешно!\n\nМы готовы принимать Ваши транзакции.\n\nПросто пишите их в формате:\n\n <i>сумма комментарий</i>\n\n <b>Первой обязательно должна быть сумма!</b>",
|
||||||
parseMode = ParseMode.HTML,
|
parseMode = ParseMode.HTML,
|
||||||
replyMarkup = buildMenu(callbackQuery.from.id)
|
replyMarkup = buildMenu(callbackQuery.from.id)
|
||||||
)
|
)
|
||||||
@@ -246,9 +237,9 @@ class BotService(
|
|||||||
)
|
)
|
||||||
|
|
||||||
} catch (e: NotFoundException) {
|
} catch (e: NotFoundException) {
|
||||||
bot.sendMessage(ChatId.fromId(message.chat.id), text = "Кажется, мы еще не знакомы.")
|
bot.sendMessage(ChatId.Companion.fromId(message.chat.id), text = "Кажется, мы еще не знакомы.")
|
||||||
bot.sendMessage(ChatId.fromId(message.chat.id), text = "Давайте зарегистрируемся? ")
|
bot.sendMessage(ChatId.Companion.fromId(message.chat.id), text = "Давайте зарегистрируемся? ")
|
||||||
bot.sendMessage(ChatId.fromId(message.chat.id), text = "")
|
bot.sendMessage(ChatId.Companion.fromId(message.chat.id), text = "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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?
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ package space.luminic.finance.services.telegram
|
|||||||
|
|
||||||
import space.luminic.finance.dtos.TransactionDTO
|
import space.luminic.finance.dtos.TransactionDTO
|
||||||
|
|
||||||
interface
|
interface TransactionService {
|
||||||
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
|
||||||
}
|
}
|
||||||
@@ -1,29 +1,18 @@
|
|||||||
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,
|
||||||
@@ -32,31 +21,23 @@ class TransactionsServiceImpl(
|
|||||||
chatId: Long,
|
chatId: Long,
|
||||||
messageId: Long
|
messageId: Long
|
||||||
): Int {
|
): Int {
|
||||||
val space = spaceService.getSpace(spaceId, userId)
|
val space = spaceService.getSpace(spaceId, userId)
|
||||||
val category = transaction.categoryId?.let { categoryService.getCategory(spaceId, it) }
|
val category = transaction.categoryId?.let { categoryService.getCategory(spaceId, it) }
|
||||||
val transaction = Transaction(
|
val transaction = Transaction(
|
||||||
space = space,
|
space = space,
|
||||||
type = transaction.type,
|
type = transaction.type,
|
||||||
kind = transaction.kind,
|
kind = transaction.kind,
|
||||||
category = category,
|
category = category,
|
||||||
comment = transaction.comment,
|
comment = transaction.comment,
|
||||||
amount = transaction.amount,
|
amount = transaction.amount,
|
||||||
fees = transaction.fees,
|
fees = transaction.fees,
|
||||||
date = transaction.date,
|
date = transaction.date,
|
||||||
tgChatId = chatId,
|
tgChatId = chatId,
|
||||||
tgMessageId = messageId,
|
tgMessageId = messageId,
|
||||||
)
|
)
|
||||||
serviceScope.launch {
|
return transactionRepo.create(transaction, userId)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,20 +1,19 @@
|
|||||||
spring.application.name=budger-app
|
spring.application.name=budger-app
|
||||||
spring.data.mongodb.uri=mongodb://budger-app:BA1q2w3e4r!@luminic.space:27017/budger-app-v2?authSource=admin&minPoolSize=10&maxPoolSize=100
|
spring.data.mongodb.uri=mongodb://budger-app:BA1q2w3e4r!@luminic.space:27017/budger-app-v2?authSource=admin&minPoolSize=10&maxPoolSize=100
|
||||||
|
|
||||||
logging.level.org.springframework.web=DEBUG
|
logging.level.org.springframework.web=DEBUG
|
||||||
logging.level.org.springframework.data=DEBUG
|
logging.level.org.springframework.data = DEBUG
|
||||||
logging.level.org.springframework.data.jpa=DEBUG
|
logging.level.org.springframework.data.jpa = DEBUG
|
||||||
#logging.level.org.springframework.data.mongodb.core.ReactiveMongoTemplate=DEBUG
|
#logging.level.org.springframework.data.mongodb.core.ReactiveMongoTemplate=DEBUG
|
||||||
logging.level.org.springframework.security=DEBUG
|
logging.level.org.springframework.security = DEBUG
|
||||||
#logging.level.org.springframework.data.mongodb.code = DEBUG
|
#logging.level.org.springframework.data.mongodb.code = DEBUG
|
||||||
logging.level.org.springframework.web.reactive=DEBUG
|
logging.level.org.springframework.web.reactive=DEBUG
|
||||||
logging.level.org.mongodb.driver.protocol.command=DEBUG
|
logging.level.org.mongodb.driver.protocol.command = DEBUG
|
||||||
logging.level.org.springframework.jdbc.core=INFO
|
logging.level.org.springframework.jdbc.core=INFO
|
||||||
logging.level.org.springframework.jdbc.core.StatementCreatorUtils=INFO
|
logging.level.org.springframework.jdbc.core.StatementCreatorUtils=INFO
|
||||||
logging.level.org.springframework.jdbc=INFO
|
logging.level.org.springframework.jdbc=INFO
|
||||||
logging.level.org.springframework.jdbc.datasource=INFO
|
logging.level.org.springframework.jdbc.datasource=INFO
|
||||||
logging.level.org.springframework.jdbc.support=INFO
|
logging.level.org.springframework.jdbc.support=INFO
|
||||||
logging.level.okhttp3=INFO
|
|
||||||
logging.level.space.luminic=DEBUG
|
|
||||||
|
|
||||||
management.endpoints.web.exposure.include=*
|
management.endpoints.web.exposure.include=*
|
||||||
management.endpoint.health.show-details=always
|
management.endpoint.health.show-details=always
|
||||||
@@ -24,6 +23,6 @@ nlp.address=http://127.0.0.1:8000
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
spring.datasource.url=jdbc:postgresql://31.59.58.220:5432/luminic-space-db
|
spring.datasource.url=jdbc:postgresql://213.226.71.138:5432/luminic-space-db
|
||||||
spring.datasource.username=luminicspace
|
spring.datasource.username=luminicspace
|
||||||
spring.datasource.password=LS1q2w3e4r!
|
spring.datasource.password=LS1q2w3e4r!
|
||||||
@@ -35,7 +35,4 @@ 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\ngoogle.client-id=\ngoogle.client-secret=\ngoogle.redirect-uri=
|
ds.api_key=sk-b5949728e79747f08af0a1d65bc6a7a2
|
||||||
google.client-id=112729998586-q39qsptu67lqeej0356m01e1ghptuajk.apps.googleusercontent.com
|
|
||||||
google.client-secret=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
|
|
||||||
google.redirect-uri=https://app.luminic.space
|
|
||||||
@@ -1,14 +1,59 @@
|
|||||||
create table if not exists finance.period_analyze
|
DROP table if exists finance.goals cascade;
|
||||||
|
DROP table if exists finance.goals_components cascade;
|
||||||
|
DROP table if exists finance.goals_transactions cascade;
|
||||||
|
|
||||||
|
DROP table if exists finance.targets cascade;
|
||||||
|
DROP table if exists finance.targets_components cascade;
|
||||||
|
DROP table if exists finance.targets_transactions cascade;
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE finance.targets
|
||||||
(
|
(
|
||||||
id INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL,
|
id integer generated by default as identity not null,
|
||||||
space_id integer not null,
|
space_id INTEGER,
|
||||||
period_start date not null,
|
type SMALLINT,
|
||||||
period_end date not null,
|
name VARCHAR(255),
|
||||||
analyze_text text not null,
|
description VARCHAR(255),
|
||||||
first_analyze_at timestamp without time zone not null default now(),
|
amount DECIMAL,
|
||||||
last_analyze_at timestamp without time zone not null default now(),
|
until_date date,
|
||||||
CONSTRAINT pk_period_analyze PRIMARY KEY (id)
|
created_by_id INTEGER,
|
||||||
|
created_at TIMESTAMP WITHOUT TIME ZONE,
|
||||||
|
updated_by_id INTEGER,
|
||||||
|
updated_at TIMESTAMP WITHOUT TIME ZONE,
|
||||||
|
CONSTRAINT pk_targets PRIMARY KEY (id)
|
||||||
);
|
);
|
||||||
|
|
||||||
ALTER TABLE finance.period_analyze
|
|
||||||
ADD CONSTRAINT FK_PERIOD_ANALYZE_ON_SPACE FOREIGN KEY (space_id) REFERENCES finance.spaces (id);
|
ALTER TABLE finance.targets
|
||||||
|
ADD CONSTRAINT FK_TARGETS_ON_CREATEDBY FOREIGN KEY (created_by_id) REFERENCES finance.users (id);
|
||||||
|
|
||||||
|
ALTER TABLE finance.targets
|
||||||
|
ADD CONSTRAINT FK_TARGETS_ON_SPACE FOREIGN KEY (space_id) REFERENCES finance.spaces (id);
|
||||||
|
|
||||||
|
ALTER TABLE finance.targets
|
||||||
|
ADD CONSTRAINT FK_TARGETS_ON_UPDATEDBY FOREIGN KEY (updated_by_id) REFERENCES finance.users (id);
|
||||||
|
|
||||||
|
CREATE TABLE finance.targets_transactions
|
||||||
|
(
|
||||||
|
target_id INTEGER NOT NULL,
|
||||||
|
transactions_id INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
ALTER TABLE finance.targets_transactions
|
||||||
|
ADD CONSTRAINT fk_targettx_on_target FOREIGN KEY (target_id) REFERENCES finance.targets (id);
|
||||||
|
|
||||||
|
ALTER TABLE finance.targets_transactions
|
||||||
|
ADD CONSTRAINT fk_targettx_on_tx FOREIGN KEY (transactions_id) REFERENCES finance.transactions (id);
|
||||||
|
|
||||||
|
create table if not exists finance.targets_components
|
||||||
|
(
|
||||||
|
id INTEGER GENERATED BY DEFAULT AS IDENTITY NOT NULL,
|
||||||
|
target_id INTEGER NOT NULL,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
amount NUMERIC NOT NULL,
|
||||||
|
is_done BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
date TIMESTAMP WITH TIME ZONE NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
alter table finance.targets_components
|
||||||
|
add constraint fk_target_on_components foreign key (target_id) references finance.targets (id);
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
CREATE UNIQUE INDEX IF NOT EXISTS period_analyze_unique_idx
|
|
||||||
ON finance.period_analyze (space_id, period_start, period_end);
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
alter table finance.period_analyze
|
|
||||||
alter column analyze_text set data type json USING analyze_text::json;
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
create table if not exists finance.analyze_runs (
|
|
||||||
run_at timestamp without time zone primary key,
|
|
||||||
involved_spaces varchar not null
|
|
||||||
)
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
alter table finance.recurrent_operations
|
|
||||||
add column is_deleted boolean default false;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
ALTER TABLE finance.users ADD COLUMN google_refresh_token VARCHAR(255);
|
|
||||||
Reference in New Issue
Block a user