init
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
package space.luminic.budgerapp
|
||||
|
||||
import com.interaso.webpush.VapidKeys
|
||||
//import org.apache.tomcat.util.codec.binary.Base64.encodeBase64
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.runApplication
|
||||
import org.springframework.cache.annotation.EnableCaching
|
||||
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories
|
||||
import org.springframework.scheduling.annotation.EnableAsync
|
||||
import org.springframework.web.reactive.config.EnableWebFlux
|
||||
import java.security.Security
|
||||
import java.util.TimeZone
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableCaching
|
||||
@EnableAsync
|
||||
@EnableMongoRepositories(basePackages = ["space.luminic.budgerapp.repos"])
|
||||
class BudgerAppApplication
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
TimeZone.setDefault(TimeZone.getTimeZone("Europe/Moscow"))
|
||||
runApplication<BudgerAppApplication>(*args)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package space.luminic.budgerapp.configs
|
||||
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor
|
||||
import java.util.concurrent.Executor
|
||||
//
|
||||
//@Configuration
|
||||
//class AsyncConfig {
|
||||
// @Bean(name = ["taskExecutor"])
|
||||
// fun taskExecutor(): Executor {
|
||||
// val executor = ThreadPoolTaskExecutor()
|
||||
// executor.corePoolSize = 5
|
||||
// executor.maxPoolSize = 10
|
||||
// executor.setQueueCapacity(25)
|
||||
// executor.setThreadNamePrefix("Async-")
|
||||
// executor.initialize()
|
||||
// return executor
|
||||
// }
|
||||
//}
|
||||
@@ -0,0 +1,60 @@
|
||||
package space.luminic.budgerapp.configs
|
||||
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.http.HttpHeaders
|
||||
import org.springframework.http.HttpMethod
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.security.authentication.BadCredentialsException
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
|
||||
import org.springframework.security.core.AuthenticationException
|
||||
import org.springframework.security.core.GrantedAuthority
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority
|
||||
import org.springframework.security.core.context.ReactiveSecurityContextHolder
|
||||
import org.springframework.security.core.context.SecurityContext
|
||||
import org.springframework.security.core.context.SecurityContextImpl
|
||||
import org.springframework.security.web.server.context.SecurityContextServerWebExchangeWebFilter
|
||||
import org.springframework.stereotype.Component
|
||||
import org.springframework.web.server.ServerWebExchange
|
||||
import org.springframework.web.server.WebFilterChain
|
||||
import reactor.core.publisher.Mono
|
||||
import space.luminic.budgerapp.services.AuthService
|
||||
|
||||
@Component
|
||||
class BearerTokenFilter(private val authService: AuthService) : SecurityContextServerWebExchangeWebFilter() {
|
||||
private val logger = LoggerFactory.getLogger(BearerTokenFilter::class.java)
|
||||
|
||||
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
|
||||
val token = exchange.request.headers.getFirst(HttpHeaders.AUTHORIZATION)?.removePrefix("Bearer ")
|
||||
|
||||
if (exchange.request.path.value() == "/api/auth/login"){
|
||||
return chain.filter(exchange)
|
||||
}
|
||||
|
||||
return if (token != null) {
|
||||
authService.isTokenValid(token)
|
||||
.flatMap { userDetails ->
|
||||
val authorities = userDetails.roles.map { SimpleGrantedAuthority(it) }
|
||||
val securityContext = SecurityContextImpl(
|
||||
UsernamePasswordAuthenticationToken(
|
||||
userDetails.username, null, authorities
|
||||
)
|
||||
)
|
||||
chain.filter(exchange)
|
||||
.contextWrite(ReactiveSecurityContextHolder.withSecurityContext(Mono.just(securityContext)))
|
||||
}
|
||||
.onErrorMap(AuthException::class.java) { ex ->
|
||||
BadCredentialsException(ex.message ?: "Unauthorized")
|
||||
}
|
||||
|
||||
} else {
|
||||
Mono.error(AuthException("Authorization token is missing"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
open class AuthException(msg: String) : RuntimeException(msg)
|
||||
@@ -0,0 +1,18 @@
|
||||
package space.luminic.budgerapp.configs
|
||||
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.web.reactive.config.CorsRegistry
|
||||
import org.springframework.web.reactive.config.WebFluxConfigurer
|
||||
|
||||
@Configuration
|
||||
class CorsConfig : WebFluxConfigurer {
|
||||
|
||||
override fun addCorsMappings(registry: CorsRegistry) {
|
||||
registry.addMapping("/**") // Разрешить все пути
|
||||
.allowedOrigins("http://localhost:5173") // Разрешить домен localhost:5173
|
||||
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH") // Разрешить методы
|
||||
.allowedHeaders("*") // Разрешить все заголовки
|
||||
.allowCredentials(true) // Разрешить передачу cookies
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
//package space.luminic.budgerapp.configs
|
||||
//import org.springframework.http.HttpHeaders
|
||||
//import org.springframework.http.HttpStatus
|
||||
//import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
|
||||
//import org.springframework.security.core.context.SecurityContextHolder
|
||||
//import org.springframework.stereotype.Component
|
||||
//import org.springframework.web.server.ServerWebExchange
|
||||
//import org.springframework.web.server.WebFilter
|
||||
//import org.springframework.web.server.WebFilterChain
|
||||
//import reactor.core.publisher.Mono
|
||||
//import space.luminic.budgerapp.services.AuthService
|
||||
//import space.luminic.budgerapp.services.TokenService
|
||||
//import space.luminic.budgerapp.utils.JWTUtil
|
||||
//
|
||||
//@Component
|
||||
//class JWTAuthFilter(
|
||||
// private val jwtUtil: JWTUtil,
|
||||
// private val authService: AuthService
|
||||
//) : WebFilter {
|
||||
//
|
||||
// override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
|
||||
// val authHeader = exchange.request.headers.getFirst(HttpHeaders.AUTHORIZATION)
|
||||
//
|
||||
// if (authHeader != null && authHeader.startsWith("Bearer ")) {
|
||||
// val token = authHeader.substring(7)
|
||||
// return Mono.just(token)
|
||||
// .filter { authService.isTokenValid(it) }
|
||||
// .flatMap { validToken ->
|
||||
// val username = jwtUtil.validateToken(validToken)
|
||||
// if (username != null) {
|
||||
// val auth = UsernamePasswordAuthenticationToken(username, null, emptyList())
|
||||
// SecurityContextHolder.getContext().authentication = auth
|
||||
// }
|
||||
// chain.filter(exchange)
|
||||
// }
|
||||
// .onErrorResume {
|
||||
// exchange.response.statusCode = HttpStatus.UNAUTHORIZED
|
||||
// exchange.response.setComplete()
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return chain.filter(exchange)
|
||||
// }
|
||||
//}
|
||||
@@ -0,0 +1,33 @@
|
||||
//package space.luminic.budgerapp.configs
|
||||
//
|
||||
//import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
|
||||
//import org.springframework.security.authentication.ReactiveAuthenticationManager
|
||||
//import org.springframework.security.core.Authentication
|
||||
//import org.springframework.security.core.userdetails.ReactiveUserDetailsService
|
||||
//import org.springframework.security.crypto.password.PasswordEncoder
|
||||
//import org.springframework.stereotype.Component
|
||||
//import reactor.core.publisher.Mono
|
||||
//import space.luminic.budgerapp.services.CustomReactiveUserDetailsService
|
||||
//import space.luminic.budgerapp.services.TokenService
|
||||
//import space.luminic.budgerapp.utils.JWTUtil
|
||||
//
|
||||
//@Component
|
||||
//class JWTReactiveAuthenticationManager(
|
||||
// private val passwordEncoder: PasswordEncoder,
|
||||
// private val userDetailsService: CustomReactiveUserDetailsService
|
||||
//) : ReactiveAuthenticationManager {
|
||||
//
|
||||
//
|
||||
// override fun authenticate(authentication: Authentication): Mono<Authentication> {
|
||||
// val username = authentication.principal as String
|
||||
// val password = authentication.credentials as String
|
||||
//
|
||||
// return userDetailsService.findByUsername(username)
|
||||
// .filter { userDetails -> password == passwordEncoder.encode(userDetails.password) } // Пример проверки пароля
|
||||
// .map { userDetails ->
|
||||
// UsernamePasswordAuthenticationToken(userDetails, null, userDetails.authorities)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//
|
||||
//}
|
||||
@@ -0,0 +1,62 @@
|
||||
package space.luminic.budgerapp.configs
|
||||
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.http.HttpMethod
|
||||
import org.springframework.security.config.web.server.SecurityWebFiltersOrder
|
||||
import org.springframework.security.config.web.server.ServerHttpSecurity
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
|
||||
import org.springframework.security.crypto.password.PasswordEncoder
|
||||
import org.springframework.security.web.server.SecurityWebFilterChain
|
||||
import space.luminic.budgerapp.controllers.CustomAuthenticationEntryPoint
|
||||
|
||||
|
||||
import space.luminic.budgerapp.services.AuthService
|
||||
|
||||
@Configuration
|
||||
class SecurityConfig(
|
||||
private val authService: AuthService
|
||||
) {
|
||||
@Bean
|
||||
fun securityWebFilterChain(
|
||||
http: ServerHttpSecurity,
|
||||
bearerTokenFilter: BearerTokenFilter,
|
||||
customAuthenticationEntryPoint: CustomAuthenticationEntryPoint
|
||||
): SecurityWebFilterChain {
|
||||
return http
|
||||
.csrf { it.disable() }
|
||||
.cors { it.configurationSource(corsConfigurationSource()) }
|
||||
|
||||
.logout { it.disable() }
|
||||
.authorizeExchange {
|
||||
it.pathMatchers(HttpMethod.POST, "/auth/login").permitAll()
|
||||
it.pathMatchers("/actuator/**").permitAll()
|
||||
it.anyExchange().authenticated()
|
||||
}
|
||||
.addFilterAt(
|
||||
bearerTokenFilter,
|
||||
SecurityWebFiltersOrder.AUTHENTICATION
|
||||
) // BearerTokenFilter только для authenticated
|
||||
.build()
|
||||
}
|
||||
|
||||
|
||||
@Bean
|
||||
fun passwordEncoder(): PasswordEncoder {
|
||||
return BCryptPasswordEncoder()
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun corsConfigurationSource(): org.springframework.web.cors.reactive.CorsConfigurationSource {
|
||||
val corsConfig = org.springframework.web.cors.CorsConfiguration()
|
||||
corsConfig.allowedOrigins =
|
||||
listOf("https://luminic.space", "http://localhost:5173") // Ваши разрешённые источники
|
||||
corsConfig.allowedMethods = listOf("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH")
|
||||
corsConfig.allowedHeaders = listOf("*")
|
||||
corsConfig.allowCredentials = true
|
||||
|
||||
val source = org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource()
|
||||
source.registerCorsConfiguration("/**", corsConfig)
|
||||
return source
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package space.luminic.budgerapp.controllers
|
||||
import org.springframework.http.ResponseEntity
|
||||
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
|
||||
import org.springframework.security.core.Authentication
|
||||
|
||||
import org.springframework.web.bind.annotation.*
|
||||
import reactor.core.publisher.Mono
|
||||
//import space.luminic.budgerapp.configs.JWTReactiveAuthenticationManager
|
||||
import space.luminic.budgerapp.models.User
|
||||
import space.luminic.budgerapp.services.AuthService
|
||||
import space.luminic.budgerapp.services.UserService
|
||||
import space.luminic.budgerapp.utils.JWTUtil
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/auth")
|
||||
class AuthController(
|
||||
// private val authenticationManager: JWTReactiveAuthenticationManager,
|
||||
private val jwtUtil: JWTUtil,
|
||||
private val userService: UserService,
|
||||
private val authService: AuthService
|
||||
) {
|
||||
|
||||
@PostMapping("/login")
|
||||
fun login(@RequestBody request: AuthRequest): Mono<Map<String, String>> {
|
||||
return authService.login(request.username, request.password)
|
||||
.map { token -> mapOf("token" to token) }
|
||||
}
|
||||
|
||||
|
||||
@GetMapping("/me")
|
||||
fun getMe(@RequestHeader("Authorization") token: String): Mono<User> {
|
||||
return authService.isTokenValid(token.removePrefix("Bearer "))
|
||||
.flatMap { username ->
|
||||
userService.getByUserNameWoPass(username.username)
|
||||
}
|
||||
// return mapOf(
|
||||
// "username" to authentication.name,
|
||||
// "details" to authentication.details,
|
||||
// "roles" to authentication.authorities.map { it.authority }
|
||||
// )
|
||||
}
|
||||
}
|
||||
|
||||
data class AuthRequest(val username: String, val password: String)
|
||||
@@ -0,0 +1,112 @@
|
||||
package space.luminic.budgerapp.controllers
|
||||
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.DeleteMapping
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.PathVariable
|
||||
import org.springframework.web.bind.annotation.PostMapping
|
||||
import org.springframework.web.bind.annotation.RequestBody
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RequestParam
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import reactor.core.publisher.Mono
|
||||
import space.luminic.budgerapp.controllers.dtos.BudgetCreationDTO
|
||||
import space.luminic.budgerapp.models.Budget
|
||||
import space.luminic.budgerapp.models.BudgetDTO
|
||||
import space.luminic.budgerapp.models.Transaction
|
||||
import space.luminic.budgerapp.models.Warn
|
||||
|
||||
import space.luminic.budgerapp.services.BudgetService
|
||||
import space.luminic.budgerapp.services.CacheInspector
|
||||
import java.math.BigDecimal
|
||||
import java.sql.Date
|
||||
import java.time.LocalDate
|
||||
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/budgets")
|
||||
class BudgetController(
|
||||
val budgetService: BudgetService
|
||||
) {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(BudgetController::class.java)
|
||||
|
||||
@GetMapping
|
||||
fun getBudgets(): Mono<MutableList<Budget>> {
|
||||
return budgetService.getBudgets()
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
fun getBudget(@PathVariable id: String): Mono<BudgetDTO> {
|
||||
// logger.info(cacheInspector.getCacheContent("budgets").toString())
|
||||
return budgetService.getBudget(id)
|
||||
}
|
||||
|
||||
|
||||
@GetMapping("/by-dates")
|
||||
fun getBudgetByDate(@RequestParam date: LocalDate): ResponseEntity<Any> {
|
||||
return ResponseEntity.ok(budgetService.getBudgetByDate(date))
|
||||
}
|
||||
|
||||
|
||||
@GetMapping("/{id}/categories")
|
||||
fun getBudgetCategories(@PathVariable id: String): ResponseEntity<Any> {
|
||||
return ResponseEntity.ok(budgetService.getBudgetCategories(id))
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/transactions")
|
||||
fun getBudgetTransactions(@PathVariable id: String):Mono<Map<String,List<Transaction>>> {
|
||||
return budgetService.getBudgetTransactionsByType(id)
|
||||
}
|
||||
|
||||
@PostMapping("/")
|
||||
fun createBudget(@RequestBody budgetCreationDTO: BudgetCreationDTO): Mono<Budget> {
|
||||
return budgetService.createBudget(
|
||||
budgetCreationDTO.budget,
|
||||
budgetCreationDTO.createRecurrent
|
||||
)
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
fun deleteBudget(@PathVariable id: String): Mono<Void> {
|
||||
return budgetService.deleteBudget(id)
|
||||
}
|
||||
|
||||
@PostMapping("/{budgetId}/categories/{catId}/limit")
|
||||
fun setCategoryLimit(
|
||||
@PathVariable budgetId: String,
|
||||
@PathVariable catId: String,
|
||||
@RequestBody limit: LimitValue,
|
||||
): ResponseEntity<Any> {
|
||||
return try {
|
||||
ResponseEntity.ok(budgetService.setCategoryLimit(budgetId, catId, limit.limit))
|
||||
} catch (e: Exception) {
|
||||
ResponseEntity.badRequest().body(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/recalc-categories")
|
||||
fun recalcCategories(): Mono<Void> {
|
||||
return budgetService.recalcBudgetCategory()
|
||||
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/warns")
|
||||
fun budgetWarns(@PathVariable id: String, @RequestParam hidden: Boolean? = null): Mono<List<Warn>> {
|
||||
return budgetService.getWarns(id, hidden)
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/warns/{warnId}/hide")
|
||||
fun setWarnHide(@PathVariable id: String, @PathVariable warnId: String): Mono<Warn> {
|
||||
return budgetService.hideWarn(id, warnId)
|
||||
|
||||
|
||||
}
|
||||
|
||||
data class LimitValue(
|
||||
var limit: Double
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package space.luminic.budgerapp.controllers
|
||||
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.DeleteMapping
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.PathVariable
|
||||
import org.springframework.web.bind.annotation.PostMapping
|
||||
import org.springframework.web.bind.annotation.PutMapping
|
||||
import org.springframework.web.bind.annotation.RequestBody
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import org.springframework.web.client.HttpClientErrorException
|
||||
import reactor.core.publisher.Mono
|
||||
import space.luminic.budgerapp.models.Category
|
||||
import space.luminic.budgerapp.services.CategoryService
|
||||
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/categories")
|
||||
class CategoriesController(
|
||||
private val categoryService: CategoryService
|
||||
) {
|
||||
|
||||
|
||||
private val logger = LoggerFactory.getLogger(javaClass)
|
||||
|
||||
@GetMapping()
|
||||
fun getCategories(): ResponseEntity<Any> {
|
||||
return try {
|
||||
ResponseEntity.ok(categoryService.getCategories())
|
||||
} catch (e: Exception) {
|
||||
ResponseEntity(HttpClientErrorException(HttpStatus.INTERNAL_SERVER_ERROR), HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@GetMapping("/types")
|
||||
fun getCategoriesTypes(): ResponseEntity<Any> {
|
||||
return try {
|
||||
ResponseEntity.ok(categoryService.getCategoryTypes())
|
||||
}catch (e: Exception) {
|
||||
ResponseEntity(HttpClientErrorException(HttpStatus.INTERNAL_SERVER_ERROR), HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping()
|
||||
fun createCategory(@RequestBody category: Category): Mono<Category> {
|
||||
return categoryService.createCategory(category)
|
||||
}
|
||||
|
||||
@PutMapping("/{categoryId}")
|
||||
fun editCategory(@PathVariable categoryId: String, @RequestBody category: Category): Mono<Category> {
|
||||
return categoryService.editCategory(category)
|
||||
}
|
||||
|
||||
@DeleteMapping("/{categoryId}")
|
||||
fun deleteCategory(@PathVariable categoryId: String): Mono<String> {
|
||||
return categoryService.deleteCategory(categoryId)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package space.luminic.budgerapp.controllers
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.security.core.AuthenticationException
|
||||
import org.springframework.security.web.server.ServerAuthenticationEntryPoint
|
||||
import org.springframework.stereotype.Component
|
||||
import org.springframework.web.server.ServerWebExchange
|
||||
import reactor.core.publisher.Mono
|
||||
import java.util.*
|
||||
|
||||
@Component
|
||||
class CustomAuthenticationEntryPoint : ServerAuthenticationEntryPoint {
|
||||
override fun commence(
|
||||
exchange: ServerWebExchange,
|
||||
ex: AuthenticationException
|
||||
): Mono<Void> {
|
||||
val response = exchange.response
|
||||
response.statusCode = HttpStatus.UNAUTHORIZED
|
||||
response.headers.contentType = MediaType.APPLICATION_JSON
|
||||
|
||||
val body = mapOf(
|
||||
"timestamp" to Date(),
|
||||
"status" to HttpStatus.UNAUTHORIZED.value(),
|
||||
"error" to "Unauthorized",
|
||||
"message" to ex.message,
|
||||
"path" to exchange.request.path.value()
|
||||
)
|
||||
|
||||
val buffer = response.bufferFactory().wrap(ObjectMapper().writeValueAsBytes(body))
|
||||
return response.writeWith(Mono.just(buffer))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package space.luminic.budgerapp.controllers
|
||||
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest
|
||||
import org.springframework.security.core.AuthenticationException
|
||||
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice
|
||||
import org.springframework.web.reactive.function.server.ServerRequest
|
||||
import org.springframework.web.server.ServerWebExchange
|
||||
import reactor.core.publisher.Mono
|
||||
import space.luminic.budgerapp.configs.AuthException
|
||||
import space.luminic.budgerapp.models.NotFoundException
|
||||
|
||||
@RestControllerAdvice
|
||||
class GlobalExceptionHandler {
|
||||
|
||||
fun constructErrorBody(
|
||||
e: Exception,
|
||||
message: String,
|
||||
status: HttpStatus,
|
||||
request: ServerHttpRequest
|
||||
): Map<String, Any?> {
|
||||
val errorResponse = mapOf(
|
||||
"timestamp" to System.currentTimeMillis(),
|
||||
"status" to status.value(),
|
||||
"error" to message,
|
||||
"message" to e.message,
|
||||
"path" to request.path.value()
|
||||
)
|
||||
return errorResponse
|
||||
}
|
||||
|
||||
@ExceptionHandler(AuthException::class)
|
||||
fun handleAuthenticationException(
|
||||
ex: AuthException,
|
||||
exchange: ServerWebExchange
|
||||
): Mono<ResponseEntity<Map<String, Any?>>?> {
|
||||
ex.printStackTrace()
|
||||
|
||||
return Mono.just(
|
||||
ResponseEntity(
|
||||
constructErrorBody(
|
||||
ex,
|
||||
ex.message.toString(),
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
exchange.request
|
||||
), HttpStatus.UNAUTHORIZED
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ExceptionHandler(NotFoundException::class)
|
||||
fun handleNotFoundException(
|
||||
e: NotFoundException,
|
||||
exchange: ServerWebExchange
|
||||
): Mono<ResponseEntity<Map<String, Any?>>?> {
|
||||
e.printStackTrace()
|
||||
|
||||
return Mono.just(
|
||||
ResponseEntity(
|
||||
constructErrorBody(
|
||||
e,
|
||||
e.message.toString(),
|
||||
HttpStatus.NOT_FOUND,
|
||||
exchange.request
|
||||
), HttpStatus.NOT_FOUND
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@ExceptionHandler(IllegalArgumentException::class)
|
||||
fun handleIllegalArgumentException(
|
||||
e: IllegalArgumentException,
|
||||
exchange: ServerWebExchange
|
||||
): Mono<ResponseEntity<Map<String, Any?>>?> {
|
||||
e.printStackTrace()
|
||||
val errorBody = mapOf("error" to e.message.orEmpty())
|
||||
return Mono.just(
|
||||
ResponseEntity(
|
||||
constructErrorBody(
|
||||
e,
|
||||
e.message.toString(),
|
||||
HttpStatus.BAD_REQUEST,
|
||||
exchange.request
|
||||
), HttpStatus.BAD_REQUEST
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception::class)
|
||||
fun handleGenericException(
|
||||
e: Exception,
|
||||
exchange: ServerWebExchange
|
||||
): Mono<out ResponseEntity<out Map<String, Any?>>?> {
|
||||
e.printStackTrace()
|
||||
// val errorBody = mapOf("error" to "An unexpected error occurred")
|
||||
val errorResponse = mapOf(
|
||||
"timestamp" to System.currentTimeMillis(),
|
||||
"status" to HttpStatus.INTERNAL_SERVER_ERROR.value(),
|
||||
"error" to "Internal Server Error",
|
||||
"message" to e.message,
|
||||
"path" to exchange.request.path.value()
|
||||
)
|
||||
return Mono.just(
|
||||
ResponseEntity(
|
||||
constructErrorBody(
|
||||
e,
|
||||
e.message.toString(),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
exchange.request
|
||||
), HttpStatus.INTERNAL_SERVER_ERROR
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package space.luminic.budgerapp.controllers
|
||||
import org.springframework.boot.autoconfigure.web.WebProperties
|
||||
import org.springframework.boot.autoconfigure.web.reactive.error.AbstractErrorWebExceptionHandler
|
||||
import org.springframework.boot.web.reactive.error.ErrorAttributes
|
||||
import org.springframework.context.ApplicationContext
|
||||
import org.springframework.core.annotation.Order
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.stereotype.Component
|
||||
import org.springframework.web.reactive.function.BodyInserters
|
||||
import org.springframework.web.reactive.function.server.*
|
||||
import org.springframework.web.reactive.result.view.ViewResolver
|
||||
import org.springframework.http.codec.ServerCodecConfigurer
|
||||
import reactor.core.publisher.Mono
|
||||
|
||||
@Component
|
||||
@Order(-2)
|
||||
class GlobalErrorWebExceptionHandler(
|
||||
errorAttributes: ErrorAttributes,
|
||||
applicationContext: ApplicationContext,
|
||||
serverCodecConfigurer: ServerCodecConfigurer
|
||||
) : AbstractErrorWebExceptionHandler(
|
||||
errorAttributes,
|
||||
WebProperties.Resources(),
|
||||
applicationContext
|
||||
) {
|
||||
|
||||
init {
|
||||
super.setMessageWriters(serverCodecConfigurer.writers)
|
||||
super.setMessageReaders(serverCodecConfigurer.readers)
|
||||
}
|
||||
|
||||
override fun getRoutingFunction(errorAttributes: ErrorAttributes): RouterFunction<ServerResponse> {
|
||||
return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse)
|
||||
}
|
||||
|
||||
private fun renderErrorResponse(request: ServerRequest): Mono<ServerResponse> {
|
||||
val errorAttributesMap = getErrorAttributes(
|
||||
request,
|
||||
org.springframework.boot.web.error.ErrorAttributeOptions.of(
|
||||
org.springframework.boot.web.error.ErrorAttributeOptions.Include.MESSAGE
|
||||
)
|
||||
)
|
||||
return ServerResponse.status(401)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body(BodyInserters.fromValue(errorAttributesMap))
|
||||
}
|
||||
|
||||
private fun getHttpStatus(errorAttributes: Map<String, Any>): Int {
|
||||
return errorAttributes["status"] as Int
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package space.luminic.budgerapp.controllers
|
||||
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.DeleteMapping
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.PathVariable
|
||||
import org.springframework.web.bind.annotation.PostMapping
|
||||
import org.springframework.web.bind.annotation.PutMapping
|
||||
import org.springframework.web.bind.annotation.RequestBody
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import reactor.core.publisher.Mono
|
||||
import space.luminic.budgerapp.models.Recurrent
|
||||
import space.luminic.budgerapp.services.RecurrentService
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/recurrents")
|
||||
class RecurrentController (
|
||||
private val recurrentService: RecurrentService
|
||||
){
|
||||
|
||||
@GetMapping("/")
|
||||
fun getRecurrents(): Mono<List<Recurrent>> {
|
||||
return recurrentService.getRecurrents()
|
||||
}
|
||||
|
||||
|
||||
@GetMapping("/{id}")
|
||||
fun getRecurrent(@PathVariable id: String): Mono<Recurrent> {
|
||||
return recurrentService.getRecurrentById(id)
|
||||
}
|
||||
|
||||
@PostMapping("/")
|
||||
fun createRecurrent(@RequestBody recurrent: Recurrent): Mono<Recurrent> {
|
||||
return recurrentService.createRecurrent(recurrent)
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
fun editRecurrent(@PathVariable id: String, @RequestBody recurrent: Recurrent): Mono<Recurrent> {
|
||||
return recurrentService.editRecurrent(recurrent)
|
||||
}
|
||||
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
fun deleteRecurrent(@PathVariable id: String): Mono<Void> {
|
||||
return recurrentService.deleteRecurrent(id)
|
||||
}
|
||||
|
||||
|
||||
@GetMapping("/transfer")
|
||||
fun transferRecurrents() : ResponseEntity<Any> {
|
||||
return ResponseEntity.ok(recurrentService.transferRecurrents())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package space.luminic.budgerapp.controllers
|
||||
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/push")
|
||||
class SubsController {
|
||||
|
||||
|
||||
|
||||
@GetMapping("/vapid")
|
||||
fun getVapid(): ResponseEntity<String> {
|
||||
return ResponseEntity.ok("BKmMyBUhpkcmzYWcYsjH_spqcy0zf_8eVtZo60f7949TgLztCmv3YD0E_vtV2dTfECQ4sdLdPK3ICDcyOkCqr84")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package space.luminic.budgerapp.controllers
|
||||
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.security.core.Authentication
|
||||
import org.springframework.web.bind.annotation.PostMapping
|
||||
import org.springframework.web.bind.annotation.RequestBody
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import reactor.core.publisher.Mono
|
||||
import space.luminic.budgerapp.models.PushMessage
|
||||
import space.luminic.budgerapp.models.SubscriptionDTO
|
||||
import space.luminic.budgerapp.services.SubscriptionService
|
||||
import space.luminic.budgerapp.services.UserService
|
||||
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/subscriptions")
|
||||
class SubscriptionController(
|
||||
private val subscriptionService: SubscriptionService,
|
||||
private val userService: UserService
|
||||
) {
|
||||
|
||||
|
||||
@PostMapping("/subscribe")
|
||||
fun subscribe(
|
||||
@RequestBody subscription: SubscriptionDTO,
|
||||
authentication: Authentication
|
||||
): Mono<String> {
|
||||
return userService.getByUserNameWoPass(authentication.name)
|
||||
.flatMap { user ->
|
||||
subscriptionService.subscribe(subscription, user)
|
||||
.thenReturn("Subscription successful")
|
||||
}
|
||||
.switchIfEmpty(Mono.just("User not found"))
|
||||
|
||||
}
|
||||
|
||||
@PostMapping("/notifyAll")
|
||||
fun notifyAll(@RequestBody payload: PushMessage): ResponseEntity<Any> {
|
||||
return try {
|
||||
ResponseEntity.ok(subscriptionService.sendToAll(payload))
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
return ResponseEntity.badRequest().body(e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
package space.luminic.budgerapp.controllers
|
||||
|
||||
import org.springframework.data.mongodb.core.aggregation.AggregationResults
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.security.core.Authentication
|
||||
import org.springframework.web.bind.annotation.DeleteMapping
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.PatchMapping
|
||||
import org.springframework.web.bind.annotation.PathVariable
|
||||
import org.springframework.web.bind.annotation.PostMapping
|
||||
import org.springframework.web.bind.annotation.PutMapping
|
||||
import org.springframework.web.bind.annotation.RequestBody
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RequestParam
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import org.springframework.web.client.HttpClientErrorException
|
||||
import reactor.core.publisher.Mono
|
||||
import space.luminic.budgerapp.models.Transaction
|
||||
import space.luminic.budgerapp.services.BudgetService
|
||||
import space.luminic.budgerapp.services.TransactionService
|
||||
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/transactions")
|
||||
class TransactionController(private val transactionService: TransactionService, val budgetService: BudgetService) {
|
||||
|
||||
|
||||
@GetMapping
|
||||
fun getTransactions(
|
||||
@RequestParam(value = "transaction_type") transactionType: String? = null,
|
||||
@RequestParam(value = "category_type") categoryType: String? = null,
|
||||
@RequestParam(value = "user_id") userId: String? = null,
|
||||
@RequestParam(value = "is_child") isChild: Boolean? = null,
|
||||
@RequestParam(value = "limit") limit: Int = 10,
|
||||
@RequestParam(value = "offset") offset: Int = 0
|
||||
): ResponseEntity<Any> {
|
||||
try {
|
||||
return ResponseEntity.ok(
|
||||
transactionService.getTransactions(
|
||||
transactionType = transactionType,
|
||||
categoryType = categoryType,
|
||||
userId = userId,
|
||||
isChild = isChild,
|
||||
limit = limit,
|
||||
offset = offset
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
return ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
fun getTransaction(@PathVariable id: String): ResponseEntity<Any> {
|
||||
try {
|
||||
return ResponseEntity.ok(transactionService.getTransactionById(id))
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
return ResponseEntity(e.message, HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
fun createTransaction(@RequestBody transaction: Transaction): ResponseEntity<Any> {
|
||||
try {
|
||||
return ResponseEntity.ok(transactionService.createTransaction(transaction))
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
return ResponseEntity(e.message, HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
fun editTransaction(@PathVariable id: String, @RequestBody transaction: Transaction): ResponseEntity<Any> {
|
||||
try {
|
||||
return ResponseEntity.ok(transactionService.editTransaction(transaction))
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
return ResponseEntity(e.message, HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
}
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
fun deleteTransaction(@PathVariable id: String): Mono<Void> {
|
||||
|
||||
return transactionService.deleteTransaction(id)
|
||||
|
||||
}
|
||||
|
||||
|
||||
// @PatchMapping("/{id}/set-done")
|
||||
// fun setTransactionDoneStatus(@PathVariable id: String, @RequestBody transaction: Transaction): ResponseEntity<Any> {
|
||||
// try {
|
||||
// transactionService.setTransactionDone(transaction)
|
||||
// return ResponseEntity.ok(true)
|
||||
// } catch (e: Exception) {
|
||||
// e.printStackTrace()
|
||||
// return ResponseEntity(e.message, HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
// }
|
||||
// }
|
||||
|
||||
@GetMapping("/{id}/child")
|
||||
fun getChildTransactions(@PathVariable id: String): ResponseEntity<Any> {
|
||||
return ResponseEntity.ok(transactionService.getChildTransaction(id))
|
||||
}
|
||||
|
||||
@GetMapping("/avg-by-category")
|
||||
fun getAvgSums(): ResponseEntity<Any> {
|
||||
return ResponseEntity.ok(transactionService.getAverageSpendingByCategory())
|
||||
}
|
||||
|
||||
// @GetMapping("/_transfer")
|
||||
// fun transfer(): ResponseEntity<Any> {
|
||||
// return ResponseEntity.ok(transactionService.transferTransactions())
|
||||
// }
|
||||
|
||||
@GetMapping("/types")
|
||||
fun getTypes(): ResponseEntity<Any> {
|
||||
return try {
|
||||
ResponseEntity.ok(transactionService.getTransactionTypes())
|
||||
} catch (e: Exception) {
|
||||
ResponseEntity(HttpClientErrorException(HttpStatus.INTERNAL_SERVER_ERROR), HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
}
|
||||
}
|
||||
|
||||
// @GetMapping("/test")
|
||||
// fun getSome(): List<Map<*, *>?> {
|
||||
// return transactionService.getPlannedForBudget(budgetService.getBudget("675ae567f232c35272f8a692")!!)
|
||||
// }
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package space.luminic.budgerapp.controllers
|
||||
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import reactor.core.publisher.Mono
|
||||
import space.luminic.budgerapp.models.Budget
|
||||
import space.luminic.budgerapp.models.Category
|
||||
import space.luminic.budgerapp.models.Transaction
|
||||
import space.luminic.budgerapp.services.TransferService
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/transfer")
|
||||
class TransferController(private val transferService: TransferService) {
|
||||
|
||||
@GetMapping("/transactions")
|
||||
fun transferTransactions(): Mono<List<Transaction>> {
|
||||
return transferService.getTransactions()
|
||||
}
|
||||
|
||||
@GetMapping("/categories")
|
||||
fun transferCategories(): Mono<List<Category>> {
|
||||
return transferService.getCategories()
|
||||
}
|
||||
|
||||
@GetMapping("/budgets")
|
||||
fun budgets(): Mono<List<Budget>> {
|
||||
return transferService.transferBudgets()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package space.luminic.budgerapp.controllers
|
||||
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.http.ResponseEntity
|
||||
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 reactor.core.publisher.Mono
|
||||
import space.luminic.budgerapp.models.User
|
||||
import space.luminic.budgerapp.services.UserService
|
||||
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/users")
|
||||
class UsersController(val userService: UserService) {
|
||||
|
||||
val logger = LoggerFactory.getLogger(javaClass)
|
||||
|
||||
@GetMapping("/{id}")
|
||||
fun getUser(@PathVariable id: String): Mono<User> {
|
||||
return userService.getById(id)
|
||||
}
|
||||
|
||||
@GetMapping("/")
|
||||
fun getUsers(): Mono<List<User>> {
|
||||
// return ResponseEntity.ok("teset")
|
||||
return userService.getUsers()
|
||||
}
|
||||
//
|
||||
// @GetMapping("/regen")
|
||||
// fun regenUsers(): ResponseEntity<Any> {
|
||||
// return ResponseEntity.ok(userService.regenPass())
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package space.luminic.budgerapp.controllers.dtos
|
||||
|
||||
import space.luminic.budgerapp.models.Budget
|
||||
|
||||
data class BudgetCreationDTO(
|
||||
val budget: Budget,
|
||||
val createRecurrent: Boolean = false
|
||||
)
|
||||
42
src/main/kotlin/space/luminic/budgerapp/models/Budget.kt
Normal file
42
src/main/kotlin/space/luminic/budgerapp/models/Budget.kt
Normal file
@@ -0,0 +1,42 @@
|
||||
package space.luminic.budgerapp.models
|
||||
|
||||
import org.springframework.data.annotation.Id
|
||||
import org.springframework.data.mongodb.core.mapping.DBRef
|
||||
import org.springframework.data.mongodb.core.mapping.Document
|
||||
import java.math.BigDecimal
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
|
||||
|
||||
data class BudgetDTO(
|
||||
var id: String? = null,
|
||||
var name: String,
|
||||
var dateFrom: LocalDate,
|
||||
var dateTo: LocalDate,
|
||||
val createdAt: LocalDateTime = LocalDateTime.now(),
|
||||
var plannedExpenses: MutableList<Transaction>? = null,
|
||||
var plannedIncomes: MutableList<Transaction>? = null,
|
||||
var categories: MutableList<BudgetCategory> = mutableListOf(),
|
||||
var transactions: MutableList<Transaction>? = null,
|
||||
|
||||
)
|
||||
|
||||
|
||||
@Document("budgets")
|
||||
data class Budget(
|
||||
@Id var id: String? = null,
|
||||
var name: String,
|
||||
var dateFrom: LocalDate,
|
||||
var dateTo: LocalDate,
|
||||
val createdAt: LocalDateTime = LocalDateTime.now(),
|
||||
var categories: MutableList<BudgetCategory> = mutableListOf(),
|
||||
)
|
||||
|
||||
data class BudgetCategory(
|
||||
@Transient var currentSpent: Double? = null,
|
||||
var currentLimit: Double,
|
||||
@Transient var currentPlanned: Double? = null,
|
||||
@DBRef var category: Category
|
||||
)
|
||||
|
||||
class BudgetNotFoundException(message: String) : NotFoundException(message)
|
||||
21
src/main/kotlin/space/luminic/budgerapp/models/Category.kt
Normal file
21
src/main/kotlin/space/luminic/budgerapp/models/Category.kt
Normal file
@@ -0,0 +1,21 @@
|
||||
package space.luminic.budgerapp.models
|
||||
|
||||
import org.springframework.data.annotation.Id
|
||||
import org.springframework.data.mongodb.core.mapping.Document
|
||||
|
||||
|
||||
@Document("categories")
|
||||
data class Category(
|
||||
@Id
|
||||
val id: String? = null,
|
||||
var type: CategoryType,
|
||||
val name: String,
|
||||
val description: String? = null,
|
||||
val icon: String? = null
|
||||
)
|
||||
|
||||
|
||||
data class CategoryType(
|
||||
val code: String,
|
||||
val name: String? = null
|
||||
)
|
||||
@@ -0,0 +1,4 @@
|
||||
package space.luminic.budgerapp.models
|
||||
|
||||
|
||||
open class NotFoundException(message: String) : Exception(message)
|
||||
@@ -0,0 +1,20 @@
|
||||
package space.luminic.budgerapp.models
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
|
||||
@Serializable
|
||||
data class PushMessage(
|
||||
|
||||
// title: str
|
||||
// body: str
|
||||
// icon: str
|
||||
// badge: str
|
||||
// url: str
|
||||
val title: String,
|
||||
val body: String,
|
||||
val icon: String? = null,
|
||||
val budge: String? = null,
|
||||
val url: String? = null
|
||||
|
||||
)
|
||||
17
src/main/kotlin/space/luminic/budgerapp/models/Recurrent.kt
Normal file
17
src/main/kotlin/space/luminic/budgerapp/models/Recurrent.kt
Normal file
@@ -0,0 +1,17 @@
|
||||
package space.luminic.budgerapp.models
|
||||
|
||||
import org.springframework.data.annotation.Id
|
||||
import org.springframework.data.mongodb.core.mapping.DBRef
|
||||
import org.springframework.data.mongodb.core.mapping.Document
|
||||
import java.util.Date
|
||||
|
||||
@Document(collection = "recurrents")
|
||||
data class Recurrent(
|
||||
@Id val id: String? = null,
|
||||
var atDay: Int,
|
||||
@DBRef var category: Category,
|
||||
var name: String,
|
||||
var description: String,
|
||||
var amount: Int,
|
||||
var createdAt: Date = Date()
|
||||
)
|
||||
13
src/main/kotlin/space/luminic/budgerapp/models/Sort.kt
Normal file
13
src/main/kotlin/space/luminic/budgerapp/models/Sort.kt
Normal file
@@ -0,0 +1,13 @@
|
||||
package space.luminic.budgerapp.models
|
||||
|
||||
import org.springframework.data.domain.Sort.Direction
|
||||
|
||||
|
||||
enum class SortTypes {
|
||||
ASC, DESC
|
||||
}
|
||||
|
||||
data class SortSetting(
|
||||
val by: String,
|
||||
val order: Direction
|
||||
)
|
||||
@@ -0,0 +1,23 @@
|
||||
package space.luminic.budgerapp.models
|
||||
|
||||
import org.springframework.data.annotation.Id
|
||||
import org.springframework.data.mongodb.core.mapping.DBRef
|
||||
import org.springframework.data.mongodb.core.mapping.Document
|
||||
import java.util.Date
|
||||
|
||||
@Document(collection = "subscriptions")
|
||||
data class Subscription(
|
||||
@Id val id: String? = null,
|
||||
@DBRef val user: User? = null,
|
||||
val endpoint: String,
|
||||
val auth: String,
|
||||
val p256dh: String,
|
||||
var isActive: Boolean,
|
||||
val createdAt: Date = Date()
|
||||
)
|
||||
|
||||
|
||||
data class SubscriptionDTO (
|
||||
val endpoint: String,
|
||||
val keys: Map<String, String>
|
||||
)
|
||||
20
src/main/kotlin/space/luminic/budgerapp/models/Token.kt
Normal file
20
src/main/kotlin/space/luminic/budgerapp/models/Token.kt
Normal file
@@ -0,0 +1,20 @@
|
||||
package space.luminic.budgerapp.models
|
||||
|
||||
import org.springframework.data.annotation.Id
|
||||
import org.springframework.data.mongodb.core.mapping.Document
|
||||
import java.time.LocalDateTime
|
||||
|
||||
@Document(collection = "tokens")
|
||||
data class Token(
|
||||
@Id
|
||||
val id: String? = null,
|
||||
val token: String,
|
||||
val username: String,
|
||||
val issuedAt: LocalDateTime,
|
||||
val expiresAt: LocalDateTime,
|
||||
val status: TokenStatus = TokenStatus.ACTIVE
|
||||
)
|
||||
|
||||
enum class TokenStatus {
|
||||
ACTIVE, REVOKED, EXPIRED
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package space.luminic.budgerapp.models
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat
|
||||
import org.springframework.data.annotation.Id
|
||||
import org.springframework.data.mongodb.core.mapping.DBRef
|
||||
import org.springframework.data.mongodb.core.mapping.Document
|
||||
import java.math.BigDecimal
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
import java.util.Date
|
||||
|
||||
@Document(collection = "transactions")
|
||||
data class Transaction(
|
||||
@Id var id: String? = null,
|
||||
var type: TransactionType,
|
||||
@DBRef var user: User?=null,
|
||||
@DBRef var category: Category,
|
||||
var comment: String,
|
||||
val date: LocalDate,
|
||||
var amount: Double,
|
||||
val isDone: Boolean,
|
||||
var parentId: String? = null,
|
||||
var createdAt: LocalDateTime? = LocalDateTime.now(),
|
||||
)
|
||||
|
||||
data class TransactionType(
|
||||
val code: String,
|
||||
val name: String
|
||||
)
|
||||
@@ -0,0 +1,20 @@
|
||||
package space.luminic.budgerapp.models
|
||||
|
||||
import org.springframework.context.ApplicationEvent
|
||||
import java.math.BigDecimal
|
||||
|
||||
enum class TransactionEventType {
|
||||
CREATE, EDIT, DELETE
|
||||
}
|
||||
|
||||
class TransactionEvent(
|
||||
source: Any,
|
||||
val eventType: TransactionEventType,
|
||||
val oldTransaction: Transaction,
|
||||
val newTransaction: Transaction,
|
||||
var difference: Double? = null,
|
||||
) : ApplicationEvent(source) {
|
||||
override fun toString(): String {
|
||||
return "${eventType}, ${oldTransaction}, ${newTransaction}, $difference"
|
||||
}
|
||||
}
|
||||
26
src/main/kotlin/space/luminic/budgerapp/models/User.kt
Normal file
26
src/main/kotlin/space/luminic/budgerapp/models/User.kt
Normal file
@@ -0,0 +1,26 @@
|
||||
package space.luminic.budgerapp.models
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
import jakarta.validation.constraints.NotBlank
|
||||
import org.springframework.data.annotation.Id
|
||||
import org.springframework.data.mongodb.core.mapping.Document
|
||||
import java.util.Date
|
||||
|
||||
@Document("users")
|
||||
data class User (
|
||||
@Id
|
||||
var id: String? = null,
|
||||
@field:NotBlank val username: String,
|
||||
var firstName: String,
|
||||
var tgId: String? = null,
|
||||
var tgUserName: String? = null,
|
||||
@JsonIgnore // Скрывает пароль при сериализации
|
||||
var password: String? = null,
|
||||
var isActive: Boolean = true,
|
||||
var regDate: Date? = null,
|
||||
val createdAt: Date? = null,
|
||||
var roles: MutableList<String> = mutableListOf(),
|
||||
)
|
||||
|
||||
|
||||
|
||||
22
src/main/kotlin/space/luminic/budgerapp/models/Warn.kt
Normal file
22
src/main/kotlin/space/luminic/budgerapp/models/Warn.kt
Normal file
@@ -0,0 +1,22 @@
|
||||
package space.luminic.budgerapp.models
|
||||
|
||||
import org.bson.types.ObjectId
|
||||
import org.springframework.data.annotation.Id
|
||||
import org.springframework.data.mongodb.core.mapping.DBRef
|
||||
import org.springframework.data.mongodb.core.mapping.Document
|
||||
|
||||
enum class WarnSerenity(val level: String, val sort: Int) {
|
||||
OTHER("Другое", -10),
|
||||
MAIN("Основное", 0),
|
||||
IMPORTANT("Важное", 10)
|
||||
}
|
||||
|
||||
@Document(collection = "warns")
|
||||
data class Warn(
|
||||
@Id var id: String? = null,
|
||||
var serenity: WarnSerenity,
|
||||
var message: PushMessage,
|
||||
var budgetId: String,
|
||||
var context: String,
|
||||
var isHide: Boolean
|
||||
)
|
||||
25
src/main/kotlin/space/luminic/budgerapp/repos/BudgetRepo.kt
Normal file
25
src/main/kotlin/space/luminic/budgerapp/repos/BudgetRepo.kt
Normal file
@@ -0,0 +1,25 @@
|
||||
package space.luminic.budgerapp.repos
|
||||
|
||||
|
||||
import org.springframework.data.domain.Sort
|
||||
import org.springframework.data.mongodb.core.aggregation.SortOperation
|
||||
import org.springframework.data.mongodb.repository.MongoRepository
|
||||
import org.springframework.data.mongodb.repository.Query
|
||||
import org.springframework.data.mongodb.repository.ReactiveMongoRepository
|
||||
import org.springframework.data.repository.PagingAndSortingRepository
|
||||
import org.springframework.stereotype.Repository
|
||||
import reactor.core.publisher.Flux
|
||||
import reactor.core.publisher.Mono
|
||||
import space.luminic.budgerapp.models.Budget
|
||||
import java.time.LocalDate
|
||||
import java.util.Date
|
||||
import java.util.Optional
|
||||
|
||||
@Repository
|
||||
interface BudgetRepo: ReactiveMongoRepository<Budget, String> {
|
||||
|
||||
override fun findAll(sort: Sort): Flux<Budget>
|
||||
|
||||
fun findByDateFromLessThanEqualAndDateToGreaterThan(dateOne: LocalDate, dateTwo: LocalDate): Mono<Budget>
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package space.luminic.budgerapp.repos
|
||||
|
||||
import org.springframework.data.mongodb.repository.MongoRepository
|
||||
import org.springframework.data.mongodb.repository.ReactiveMongoRepository
|
||||
import org.springframework.stereotype.Repository
|
||||
import reactor.core.publisher.Mono
|
||||
import space.luminic.budgerapp.models.Category
|
||||
import java.util.Optional
|
||||
|
||||
@Repository
|
||||
interface CategoryRepo : ReactiveMongoRepository<Category, String> {
|
||||
|
||||
|
||||
fun findByName(name: String): Mono<Category>
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package space.luminic.budgerapp.repos
|
||||
|
||||
import org.springframework.data.mongodb.repository.MongoRepository
|
||||
import org.springframework.stereotype.Repository
|
||||
import space.luminic.budgerapp.models.Category
|
||||
|
||||
@Repository
|
||||
interface CategoryRepoOld: MongoRepository<Category, String> {
|
||||
|
||||
fun findByName(name: String): Category?
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package space.luminic.budgerapp.repos
|
||||
|
||||
import org.bson.types.ObjectId
|
||||
import org.springframework.data.mongodb.repository.MongoRepository
|
||||
import org.springframework.data.mongodb.repository.ReactiveMongoRepository
|
||||
import org.springframework.stereotype.Repository
|
||||
import space.luminic.budgerapp.models.Recurrent
|
||||
|
||||
@Repository
|
||||
interface RecurrentRepo: ReactiveMongoRepository<Recurrent, String> {
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package space.luminic.budgerapp.repos
|
||||
|
||||
|
||||
import org.springframework.data.mongodb.repository.MongoRepository
|
||||
import org.springframework.data.mongodb.repository.ReactiveMongoRepository
|
||||
import org.springframework.stereotype.Repository
|
||||
import space.luminic.budgerapp.models.Subscription
|
||||
|
||||
@Repository
|
||||
interface SubscriptionRepo : ReactiveMongoRepository<Subscription, String> {
|
||||
}
|
||||
17
src/main/kotlin/space/luminic/budgerapp/repos/TokenRepo.kt
Normal file
17
src/main/kotlin/space/luminic/budgerapp/repos/TokenRepo.kt
Normal file
@@ -0,0 +1,17 @@
|
||||
package space.luminic.budgerapp.repos
|
||||
|
||||
import org.springframework.data.mongodb.repository.MongoRepository
|
||||
import org.springframework.data.mongodb.repository.ReactiveMongoRepository
|
||||
import org.springframework.stereotype.Repository
|
||||
import reactor.core.publisher.Mono
|
||||
import space.luminic.budgerapp.models.Token
|
||||
import space.luminic.budgerapp.models.TokenStatus
|
||||
import java.time.LocalDateTime
|
||||
|
||||
@Repository
|
||||
interface TokenRepo: ReactiveMongoRepository<Token, String> {
|
||||
|
||||
fun findByToken(token: String): Mono<Token>
|
||||
fun findByUsernameAndStatus(username: String, status: TokenStatus): Mono<List<Token>>
|
||||
fun deleteByExpiresAtBefore(dateTime: LocalDateTime)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package space.luminic.budgerapp.repos
|
||||
|
||||
import org.bson.types.ObjectId
|
||||
import org.springframework.data.mongodb.repository.MongoRepository
|
||||
import org.springframework.data.mongodb.repository.ReactiveMongoRepository
|
||||
import org.springframework.stereotype.Repository
|
||||
import space.luminic.budgerapp.models.Transaction
|
||||
import java.util.Optional
|
||||
|
||||
|
||||
@Repository
|
||||
interface TransactionReactiveRepo: ReactiveMongoRepository<Transaction, String> {
|
||||
|
||||
|
||||
|
||||
|
||||
// fun findByOldId(transactionId: Int): Optional<Transaction>
|
||||
|
||||
fun findByParentId(parentId: String): Optional<Transaction>
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package space.luminic.budgerapp.repos
|
||||
|
||||
import org.bson.types.ObjectId
|
||||
import org.springframework.data.mongodb.repository.MongoRepository
|
||||
import org.springframework.data.mongodb.repository.ReactiveMongoRepository
|
||||
import org.springframework.stereotype.Repository
|
||||
import reactor.core.publisher.Mono
|
||||
import space.luminic.budgerapp.models.Transaction
|
||||
import java.util.Optional
|
||||
|
||||
|
||||
@Repository
|
||||
interface TransactionRepo: ReactiveMongoRepository<Transaction, String> {
|
||||
|
||||
|
||||
|
||||
|
||||
// fun findByOldId(transactionId: Int): Optional<Transaction>
|
||||
|
||||
fun findByParentId(parentId: String): Mono<Transaction>
|
||||
}
|
||||
19
src/main/kotlin/space/luminic/budgerapp/repos/UserRepo.kt
Normal file
19
src/main/kotlin/space/luminic/budgerapp/repos/UserRepo.kt
Normal file
@@ -0,0 +1,19 @@
|
||||
package space.luminic.budgerapp.repos
|
||||
|
||||
import org.springframework.data.mongodb.repository.MongoRepository
|
||||
import org.springframework.data.mongodb.repository.Query
|
||||
import org.springframework.data.mongodb.repository.ReactiveMongoRepository
|
||||
import org.springframework.stereotype.Repository
|
||||
import reactor.core.publisher.Mono
|
||||
import space.luminic.budgerapp.models.User
|
||||
import java.util.Optional
|
||||
|
||||
@Repository
|
||||
interface UserRepo : ReactiveMongoRepository<User, String> {
|
||||
|
||||
|
||||
@Query(value = "{ 'username': ?0 }", fields = "{ 'password': 0 }")
|
||||
fun findByUsernameWOPassword(username: String): Mono<User>
|
||||
|
||||
fun findByUsername(username: String): Mono<User>
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package space.luminic.budgerapp.repos
|
||||
|
||||
import org.springframework.data.mongodb.repository.MongoRepository
|
||||
import space.luminic.budgerapp.models.User
|
||||
|
||||
interface UserRepoOld: MongoRepository<User, String> {
|
||||
|
||||
fun findByUsername(username: String): User?
|
||||
}
|
||||
19
src/main/kotlin/space/luminic/budgerapp/repos/WarnRepo.kt
Normal file
19
src/main/kotlin/space/luminic/budgerapp/repos/WarnRepo.kt
Normal file
@@ -0,0 +1,19 @@
|
||||
package space.luminic.budgerapp.repos
|
||||
|
||||
import org.springframework.data.mongodb.repository.MongoRepository
|
||||
import org.springframework.data.mongodb.repository.ReactiveMongoRepository
|
||||
import org.springframework.stereotype.Repository
|
||||
import reactor.core.publisher.Flux
|
||||
import reactor.core.publisher.Mono
|
||||
import space.luminic.budgerapp.models.Warn
|
||||
|
||||
@Repository
|
||||
interface WarnRepo : ReactiveMongoRepository<Warn, String> {
|
||||
|
||||
fun findAllByBudgetIdAndIsHide(budgetId: String, isHide: Boolean): Flux<Warn>
|
||||
fun deleteAllByBudgetId(budgetId: String)
|
||||
|
||||
fun findWarnByContext(context: String): Mono<Warn>
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package space.luminic.budgerapp.repos.sqlrepo
|
||||
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.jdbc.core.JdbcTemplate
|
||||
import org.springframework.jdbc.core.RowMapper
|
||||
import org.springframework.stereotype.Repository
|
||||
import space.luminic.budgerapp.controllers.dtos.BudgetCreationDTO
|
||||
import space.luminic.budgerapp.models.Budget
|
||||
import space.luminic.budgerapp.models.BudgetCategory
|
||||
import space.luminic.budgerapp.models.Category
|
||||
import space.luminic.budgerapp.models.CategoryType
|
||||
import space.luminic.budgerapp.models.Transaction
|
||||
import space.luminic.budgerapp.models.TransactionType
|
||||
import space.luminic.budgerapp.services.CategoryService
|
||||
import space.luminic.budgerapp.services.UserService
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
import java.time.LocalTime
|
||||
import java.time.ZoneId
|
||||
import java.time.ZoneOffset
|
||||
|
||||
|
||||
@Repository
|
||||
class BudgetRepoSQL(
|
||||
val jdbcTemplate: JdbcTemplate,
|
||||
val userService: UserService,
|
||||
val categoryService: CategoryService,
|
||||
val transactionsRepoSQl: TransactionsRepoSQl
|
||||
) {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(BudgetRepoSQL::class.java)
|
||||
|
||||
fun getBudgetsIds(): List<Int> {
|
||||
return jdbcTemplate.queryForList("SELECT id FROM budger.budgets", Int::class.java)
|
||||
}
|
||||
|
||||
fun getBudgets(): List<BudgetCreationDTO> {
|
||||
val sql = "SELECT * FROM budger.budgets"
|
||||
return jdbcTemplate.query(sql, RowMapper { rs, _ ->
|
||||
BudgetCreationDTO(
|
||||
Budget(
|
||||
// id = rs.getString("id"),
|
||||
name = rs.getString("name"),
|
||||
dateFrom = rs.getDate("date_from").toLocalDate(),
|
||||
dateTo = rs.getDate("date_to").toLocalDate(),
|
||||
createdAt = LocalDateTime.of(rs.getDate("created_at").toLocalDate(), LocalTime.NOON),
|
||||
), false
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
// fun getBudgetCategory(id: Int): List<BudgetCategory> {
|
||||
// val sql = "SELECT c.*, \n" +
|
||||
// " COALESCE(SUM(CASE WHEN t.transaction_type_code = 'INSTANT' THEN t.amount END), 0) AS total_instant_expenses,\n" +
|
||||
// " COALESCE(SUM(CASE WHEN t.transaction_type_code = 'PLANNED' THEN t.amount END), 0) AS total_planned_expenses,\n" +
|
||||
// " COALESCE(bcs.category_setting_value, 0) AS current_limit,\n" +
|
||||
// " COALESCE((SELECT AVG(amount) FROM budger.transactions where category_id=c.id AND transaction_type_code='INSTANT'),0) as average\n" +
|
||||
// "FROM\n" +
|
||||
// " budger.budget_category_settings bcs\n" +
|
||||
// "JOIN\n" +
|
||||
// " budger.budgets b ON bcs.budget_id = b.id\n" +
|
||||
// "LEFT JOIN\n" +
|
||||
// " budger.transactions t ON bcs.category_id = t.category_id\n" +
|
||||
// " AND t.date BETWEEN b.date_from AND b.date_to\n" +
|
||||
// "JOIN\n" +
|
||||
// " budger.categories c ON bcs.category_id = c.id\n" +
|
||||
// "WHERE\n" +
|
||||
// " b.id = ? -- Укажите ID бюджета, для которого нужно вывести данные\n" +
|
||||
// "GROUP BY\n" +
|
||||
// " c.id, bcs.category_setting_value\n" +
|
||||
// "ORDER BY\n" +
|
||||
// " c.id;"
|
||||
// return jdbcTemplate.query(sql, RowMapper { rs, _ ->
|
||||
// BudgetCategory(
|
||||
// currentSpent = rs.getBigDecimal("total_instant_expenses"),
|
||||
// currentLimit = rs.getBigDecimal("current_limit"),
|
||||
// category = categoryService.getCategoryByName(rs.getString("name"))
|
||||
// )
|
||||
// }, id)
|
||||
// }
|
||||
|
||||
fun getBudgetTransactions(id: Int, transactionType: String?, categoryType: String?): List<Transaction> {
|
||||
val whereConditions = mutableListOf<String>()
|
||||
val parameters = mutableListOf<Any>()
|
||||
|
||||
// Базовое условие
|
||||
whereConditions.add("b.id = ?")
|
||||
parameters.add(id)
|
||||
|
||||
// Условие для transactionType, если указано
|
||||
if (transactionType != null) {
|
||||
whereConditions.add("t.transaction_type_code = ?")
|
||||
parameters.add(transactionType)
|
||||
}
|
||||
|
||||
// Условие для categoryType, если указано
|
||||
if (categoryType != null) {
|
||||
whereConditions.add("c.type_code = ?")
|
||||
parameters.add(categoryType)
|
||||
}
|
||||
|
||||
// Генерация WHERE
|
||||
val whereClause = if (whereConditions.isNotEmpty()) "WHERE ${whereConditions.joinToString(" AND ")}" else ""
|
||||
|
||||
// SQL запрос
|
||||
val sql = """
|
||||
SELECT
|
||||
tt.code as tt_code,
|
||||
tt.name as tt_name,
|
||||
t.comment,
|
||||
t.date,
|
||||
t.amount,
|
||||
t.is_done,
|
||||
t.created_at,
|
||||
u.username,
|
||||
c.name as c_name,
|
||||
t.id
|
||||
FROM budger.transactions t
|
||||
JOIN budger.transaction_types tt ON tt.code = t.transaction_type_code
|
||||
JOIN budger.categories c ON c.id = t.category_id
|
||||
JOIN budger.category_types ct ON ct.code = c.type_code
|
||||
JOIN budger.budgets b ON t.date BETWEEN b.date_from AND b.date_to
|
||||
LEFT JOIN budger.users u ON t.user_id = u.id
|
||||
$whereClause
|
||||
ORDER BY t.date DESC, c.name, t.id
|
||||
""".trimIndent()
|
||||
logger.info(parameters.toTypedArray().toString())
|
||||
// Выполнение запроса
|
||||
|
||||
|
||||
return jdbcTemplate.query(
|
||||
sql, parameters.toTypedArray(),
|
||||
transactionsRepoSQl.transactionRowMapper()
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package space.luminic.budgerapp.repos.sqlrepo
|
||||
|
||||
import org.springframework.jdbc.core.JdbcTemplate
|
||||
import org.springframework.jdbc.core.RowMapper
|
||||
import org.springframework.stereotype.Repository
|
||||
import space.luminic.budgerapp.models.Category
|
||||
import space.luminic.budgerapp.models.CategoryType
|
||||
import space.luminic.budgerapp.models.Transaction
|
||||
import space.luminic.budgerapp.models.TransactionType
|
||||
import java.time.LocalDate
|
||||
import java.time.ZoneId
|
||||
|
||||
@Repository
|
||||
class CategoriesRepoSQL(private val jdbcTemplate: JdbcTemplate) {
|
||||
|
||||
|
||||
fun getCategories(): List<Category> {
|
||||
val sql = "SELECT * FROM budger.categories"
|
||||
return jdbcTemplate.query(sql, categoriesRowMapper())
|
||||
|
||||
|
||||
}
|
||||
|
||||
fun categoriesRowMapper() = RowMapper { rs, _ ->
|
||||
val category = Category(
|
||||
type = if (rs.getString("type_code") == "EXPENSE") CategoryType("EXPENSE", "Траты") else CategoryType(
|
||||
"INCOME",
|
||||
"Поступления"
|
||||
),
|
||||
name = rs.getString("name"),
|
||||
description = rs.getString("description"),
|
||||
icon = rs.getString("icon")
|
||||
)
|
||||
|
||||
return@RowMapper category
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package space.luminic.budgerapp.repos.sqlrepo
|
||||
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.jdbc.core.JdbcTemplate
|
||||
import org.springframework.jdbc.core.RowMapper
|
||||
import org.springframework.stereotype.Repository
|
||||
import space.luminic.budgerapp.models.Recurrent
|
||||
import space.luminic.budgerapp.models.Transaction
|
||||
import space.luminic.budgerapp.models.TransactionType
|
||||
import space.luminic.budgerapp.services.CategoryService
|
||||
|
||||
|
||||
@Repository
|
||||
class RecurrentRepoSQL(
|
||||
private val jdbcTemplate: JdbcTemplate,
|
||||
private val categoryService: CategoryService
|
||||
) {
|
||||
private val logger = LoggerFactory.getLogger(RecurrentRepoSQL::class.java)
|
||||
|
||||
fun getRecurrents(): List<Recurrent> {
|
||||
return jdbcTemplate.query("SELECT r.*, c.name as c_name FROM budger.recurrent_payments r join budger.categories c on r.category_id = c.id ", recurrentRowMapper())
|
||||
}
|
||||
|
||||
|
||||
fun recurrentRowMapper() = RowMapper { rs, _ ->
|
||||
|
||||
val recurrent = Recurrent(
|
||||
atDay = rs.getInt("at_day"),
|
||||
category = categoryService.getCategoryByName(rs.getString("c_name")).block()!!,
|
||||
name = rs.getString("name"),
|
||||
description = rs.getString("description"),
|
||||
amount = rs.getInt("amount")
|
||||
)
|
||||
logger.info(recurrent.toString())
|
||||
return@RowMapper recurrent
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package space.luminic.budgerapp.repos.sqlrepo
|
||||
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.jdbc.core.JdbcTemplate
|
||||
import org.springframework.jdbc.core.RowMapper
|
||||
import org.springframework.stereotype.Repository
|
||||
import space.luminic.budgerapp.models.Category
|
||||
import space.luminic.budgerapp.models.Transaction
|
||||
import space.luminic.budgerapp.models.TransactionType
|
||||
import space.luminic.budgerapp.models.User
|
||||
import space.luminic.budgerapp.repos.CategoryRepoOld
|
||||
import space.luminic.budgerapp.repos.UserRepoOld
|
||||
import space.luminic.budgerapp.services.CategoryService
|
||||
import space.luminic.budgerapp.services.UserService
|
||||
import java.sql.SQLException
|
||||
import java.time.LocalDate
|
||||
import java.time.ZoneId
|
||||
|
||||
|
||||
@Repository
|
||||
class TransactionsRepoSQl(
|
||||
private val jdbcTemplate: JdbcTemplate,
|
||||
private val userRepoOld: UserRepoOld,
|
||||
private val categoryRepoOld: CategoryRepoOld
|
||||
) {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(TransactionsRepoSQl::class.java)
|
||||
|
||||
|
||||
fun getTransactions(): List<Transaction> {
|
||||
|
||||
|
||||
return jdbcTemplate.query(
|
||||
"SELECT tt.code as tt_code, tt.name as tt_name, u.username, c.name as c_name, t.comment, t.date, t.amount, t.is_done, t.created_at, t.id" +
|
||||
" FROM budger.transactions t" +
|
||||
" JOIN budger.transaction_types tt on t.transaction_type_code = tt.code " +
|
||||
" JOIN budger.categories c on t.category_id = c.id" +
|
||||
" LEFT JOIN budger.users u on t.user_id = u.id", transactionRowMapper()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
fun transactionRowMapper() = RowMapper { rs, _ ->
|
||||
|
||||
|
||||
val transaction = Transaction(
|
||||
type = TransactionType(rs.getString("tt_code"), rs.getString("tt_name")),
|
||||
user = rs.getString("username")
|
||||
?.let { userRepoOld.findByUsername(it) }
|
||||
?: userRepoOld.findByUsername("voroninv"),
|
||||
category = categoryRepoOld.findByName(rs.getString("c_name"))!!,
|
||||
comment = rs.getString("comment"),
|
||||
date = rs.getDate("date").toLocalDate(),
|
||||
amount = rs.getDouble("amount"),
|
||||
isDone = rs.getBoolean("is_done"),
|
||||
createdAt = rs.getTimestamp("created_at").toInstant()
|
||||
.atZone(ZoneId.systemDefault())
|
||||
.toLocalDateTime()
|
||||
)
|
||||
logger.info(transaction.toString())
|
||||
return@RowMapper transaction
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package space.luminic.budgerapp.services
|
||||
|
||||
import org.springframework.cache.annotation.Cacheable
|
||||
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
|
||||
import org.springframework.security.crypto.password.PasswordEncoder
|
||||
import org.springframework.stereotype.Service
|
||||
import reactor.core.publisher.Mono
|
||||
import space.luminic.budgerapp.configs.AuthException
|
||||
import space.luminic.budgerapp.models.Token
|
||||
import space.luminic.budgerapp.models.TokenStatus
|
||||
import space.luminic.budgerapp.models.User
|
||||
import space.luminic.budgerapp.repos.TokenRepo
|
||||
import space.luminic.budgerapp.repos.UserRepo
|
||||
import space.luminic.budgerapp.utils.JWTUtil
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneId
|
||||
import java.util.Date
|
||||
import java.util.UUID
|
||||
|
||||
|
||||
@Service
|
||||
class AuthService(
|
||||
private val userRepository: UserRepo,
|
||||
private val tokenRepo: TokenRepo,
|
||||
private val jwtUtil: JWTUtil
|
||||
|
||||
) {
|
||||
private val passwordEncoder = BCryptPasswordEncoder()
|
||||
|
||||
fun login(username: String, password: String): Mono<String> {
|
||||
return userRepository.findByUsername(username)
|
||||
.flatMap { user ->
|
||||
if (passwordEncoder.matches(password, user.password)) {
|
||||
val token = jwtUtil.generateToken(user.username)
|
||||
val expireAt = Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24 * 10)
|
||||
tokenRepo.save(
|
||||
Token(
|
||||
token = token,
|
||||
username = username,
|
||||
issuedAt = LocalDateTime.now(),
|
||||
expiresAt = LocalDateTime.ofInstant(
|
||||
expireAt.toInstant(),
|
||||
ZoneId.systemDefault()
|
||||
)
|
||||
)
|
||||
)
|
||||
.thenReturn(token)
|
||||
} else {
|
||||
Mono.error(AuthException("Invalid credentials"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Cacheable("tokens")
|
||||
fun isTokenValid(token: String): Mono<User> {
|
||||
// print("checking token: $token")
|
||||
return tokenRepo.findByToken(token)
|
||||
.flatMap {
|
||||
if (it.status == TokenStatus.ACTIVE &&
|
||||
it.expiresAt.isAfter(LocalDateTime.now())
|
||||
) {
|
||||
userRepository.findByUsername(it.username)
|
||||
} else {
|
||||
Mono.error(AuthException("Token expired"))
|
||||
}
|
||||
}.switchIfEmpty(Mono.error(AuthException("User not found")))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,616 @@
|
||||
package space.luminic.budgerapp.services
|
||||
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.cache.annotation.CacheEvict
|
||||
import org.springframework.cache.annotation.Cacheable
|
||||
import org.springframework.context.event.EventListener
|
||||
import org.springframework.data.domain.Sort
|
||||
import org.springframework.data.domain.Sort.Direction
|
||||
import org.springframework.stereotype.Service
|
||||
import space.luminic.budgerapp.models.Budget
|
||||
import space.luminic.budgerapp.models.BudgetCategory
|
||||
import space.luminic.budgerapp.models.Transaction
|
||||
import org.springframework.data.mongodb.core.aggregation.Aggregation.newAggregation
|
||||
import org.springframework.data.mongodb.core.MongoTemplate
|
||||
import org.springframework.data.mongodb.core.ReactiveMongoTemplate
|
||||
import org.springframework.data.mongodb.core.aggregation.Aggregation.group
|
||||
import org.springframework.data.mongodb.core.aggregation.Aggregation.lookup
|
||||
import org.springframework.data.mongodb.core.aggregation.Aggregation.match
|
||||
import org.springframework.data.mongodb.core.aggregation.Aggregation.project
|
||||
import org.springframework.data.mongodb.core.aggregation.Aggregation.sort
|
||||
import org.springframework.data.mongodb.core.aggregation.Aggregation.unwind
|
||||
import org.springframework.data.mongodb.core.query.Criteria
|
||||
import reactor.core.publisher.Flux
|
||||
import reactor.core.publisher.Mono
|
||||
import space.luminic.budgerapp.models.BudgetDTO
|
||||
import space.luminic.budgerapp.models.BudgetNotFoundException
|
||||
import space.luminic.budgerapp.models.PushMessage
|
||||
import space.luminic.budgerapp.models.SortSetting
|
||||
import space.luminic.budgerapp.models.TransactionEvent
|
||||
import space.luminic.budgerapp.models.TransactionEventType
|
||||
|
||||
import space.luminic.budgerapp.models.Warn
|
||||
import space.luminic.budgerapp.models.WarnSerenity
|
||||
import space.luminic.budgerapp.repos.BudgetRepo
|
||||
import space.luminic.budgerapp.repos.WarnRepo
|
||||
|
||||
import java.time.LocalDate
|
||||
|
||||
|
||||
import java.util.Optional
|
||||
import kotlin.collections.get
|
||||
|
||||
@Service
|
||||
class BudgetService(
|
||||
val budgetRepo: BudgetRepo,
|
||||
val warnRepo: WarnRepo,
|
||||
val transactionService: TransactionService,
|
||||
val recurrentService: RecurrentService,
|
||||
val categoryService: CategoryService,
|
||||
val reactiveMongoTemplate: ReactiveMongoTemplate
|
||||
) {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(BudgetService::class.java)
|
||||
|
||||
|
||||
@EventListener
|
||||
@CacheEvict(cacheNames = ["budgets"], allEntries = true)
|
||||
fun handleTransactionEvent(event: TransactionEvent) {
|
||||
logger.info("Got ${event.eventType} event on transaction ${event.newTransaction.id}")
|
||||
if (event.newTransaction.category.type?.code == "EXPENSE") {
|
||||
when (event.eventType) {
|
||||
TransactionEventType.EDIT -> updateBudgetOnEdit(event)
|
||||
TransactionEventType.CREATE -> updateBudgetOnCreate(event)
|
||||
TransactionEventType.DELETE -> updateBudgetOnDelete(event)
|
||||
}
|
||||
}
|
||||
// runBlocking(Dispatchers.IO) {
|
||||
// updateBudgetWarns(
|
||||
// budget = budgetRepo.findByDateFromLessThanEqualAndDateToGreaterThan(
|
||||
// event.newTransaction.date.toLocalDate(), event.newTransaction.date.toLocalDate()
|
||||
// )
|
||||
// )
|
||||
// }
|
||||
}
|
||||
|
||||
fun updateBudgetOnCreate(event: TransactionEvent) {
|
||||
budgetRepo.findByDateFromLessThanEqualAndDateToGreaterThan(
|
||||
event.newTransaction.date, event.newTransaction.date
|
||||
).flatMap { budget ->
|
||||
categoryService.getBudgetCategories(budget.dateFrom, budget.dateTo).flatMap { categories ->
|
||||
val updatedCategories = when (event.newTransaction.type.code) {
|
||||
"PLANNED" -> Flux.fromIterable(budget.categories)
|
||||
.map { category ->
|
||||
categories[category.category.id]?.let { data ->
|
||||
category.currentSpent = data["instantAmount"] ?: 0.0
|
||||
category.currentPlanned = data["plannedAmount"] ?: 0.0
|
||||
category.currentLimit += event.newTransaction.amount
|
||||
}
|
||||
category
|
||||
}.collectList()
|
||||
|
||||
"INSTANT" -> Flux.fromIterable(budget.categories)
|
||||
.map { category ->
|
||||
categories[category.category.id]?.let { data ->
|
||||
category.currentSpent = data["instantAmount"] ?: 0.0
|
||||
category.currentPlanned = data["plannedAmount"] ?: 0.0
|
||||
}
|
||||
category
|
||||
}.collectList()
|
||||
|
||||
else -> Mono.just(budget.categories) // Добавляем обработку типа по умолчанию
|
||||
}
|
||||
|
||||
updatedCategories.flatMap { updated ->
|
||||
budget.categories = updated
|
||||
budgetRepo.save(budget) // Сохраняем обновленный бюджет
|
||||
}
|
||||
}
|
||||
}.then() // Гарантируем завершение
|
||||
.subscribe() // Запускаем выполнение
|
||||
}
|
||||
|
||||
|
||||
fun updateBudgetOnEdit(event: TransactionEvent) {
|
||||
budgetRepo.findByDateFromLessThanEqualAndDateToGreaterThan(
|
||||
event.oldTransaction.date, event.oldTransaction.date
|
||||
).switchIfEmpty(
|
||||
Mono.error(BudgetNotFoundException("old budget cannot be null"))
|
||||
).then().subscribe()
|
||||
|
||||
budgetRepo.findByDateFromLessThanEqualAndDateToGreaterThan(
|
||||
event.newTransaction.date, event.newTransaction.date
|
||||
).flatMap { budget ->
|
||||
categoryService.getBudgetCategories(budget.dateFrom, budget.dateTo).flatMap { categories ->
|
||||
val updatedCategories = when (event.newTransaction.type.code) {
|
||||
"PLANNED" -> Flux.fromIterable(budget.categories)
|
||||
.map { category ->
|
||||
if (category.category.id == event.newTransaction.category.id) {
|
||||
categories[category.category.id]?.let { data ->
|
||||
category.currentSpent = data["instantAmount"] ?: 0.0
|
||||
category.currentPlanned = data["plannedAmount"] ?: 0.0
|
||||
category.currentLimit += event.difference!!
|
||||
}
|
||||
}
|
||||
|
||||
category
|
||||
}.collectList()
|
||||
|
||||
"INSTANT" -> Flux.fromIterable(budget.categories)
|
||||
.map { category ->
|
||||
categories[category.category.id]?.let { data ->
|
||||
category.currentSpent = data["instantAmount"] ?: 0.0
|
||||
category.currentPlanned = data["plannedAmount"] ?: 0.0
|
||||
}
|
||||
category
|
||||
}.collectList()
|
||||
|
||||
else -> Mono.just(budget.categories) // Добавляем обработку типа по умолчанию
|
||||
}
|
||||
|
||||
updatedCategories.flatMap { updated ->
|
||||
budget.categories = updated
|
||||
budgetRepo.save(budget) // Сохраняем обновленный бюджет
|
||||
}
|
||||
}
|
||||
}.then() // Гарантируем завершение
|
||||
.subscribe() // Запускаем выполнение
|
||||
}
|
||||
|
||||
|
||||
fun updateBudgetOnDelete(event: TransactionEvent) {
|
||||
budgetRepo.findByDateFromLessThanEqualAndDateToGreaterThan(
|
||||
event.newTransaction.date, event.newTransaction.date
|
||||
).flatMap { budget ->
|
||||
categoryService.getBudgetCategories(budget.dateFrom, budget.dateTo).flatMap { categories ->
|
||||
val updatedCategories = when (event.newTransaction.type.code) {
|
||||
"PLANNED" -> Flux.fromIterable(budget.categories)
|
||||
.map { category ->
|
||||
categories[category.category.id]?.let { data ->
|
||||
category.currentSpent = data["instantAmount"] ?: 0.0
|
||||
category.currentPlanned = data["plannedAmount"] ?: 0.0
|
||||
category.currentLimit += event.newTransaction.amount
|
||||
}
|
||||
category
|
||||
}.collectList()
|
||||
|
||||
"INSTANT" -> Flux.fromIterable(budget.categories)
|
||||
.map { category ->
|
||||
categories[category.category.id]?.let { data ->
|
||||
category.currentSpent = data["instantAmount"] ?: 0.0
|
||||
category.currentPlanned = data["plannedAmount"] ?: 0.0
|
||||
}
|
||||
category
|
||||
}.collectList()
|
||||
|
||||
else -> Mono.just(budget.categories) // Добавляем обработку типа по умолчанию
|
||||
}
|
||||
|
||||
updatedCategories.flatMap { updated ->
|
||||
budget.categories = updated
|
||||
budgetRepo.save(budget) // Сохраняем обновленный бюджет
|
||||
}
|
||||
}
|
||||
}.then() // Гарантируем завершение
|
||||
.subscribe() // Запускаем выполнение
|
||||
}
|
||||
|
||||
|
||||
fun updateBudgetTransactions(budget: Budget): Budget {
|
||||
// budget.plannedExpenses = getBudgetTransactions(
|
||||
// budget = budget,
|
||||
// transactionType = "PLANNED",
|
||||
// categoryType = "EXPENSE",
|
||||
// sortBy = SortSetting("date", Direction.ASC)
|
||||
// )
|
||||
// budget.plannedIncomes = getBudgetTransactions(
|
||||
// budget = budget,
|
||||
// transactionType = "PLANNED",
|
||||
// categoryType = "INCOME",
|
||||
// sortBy = SortSetting("date", Direction.ASC)
|
||||
// )
|
||||
return budget
|
||||
}
|
||||
|
||||
|
||||
@Cacheable("budgetsList")
|
||||
fun getBudgets(sortSetting: SortSetting? = null): Mono<MutableList<Budget>> {
|
||||
val sort = if (sortSetting != null) {
|
||||
Sort.by(sortSetting.order, sortSetting.by)
|
||||
} else {
|
||||
Sort.by(Sort.Direction.DESC, "dateFrom")
|
||||
}
|
||||
|
||||
return budgetRepo.findAll(sort)
|
||||
.collectList() // Сбор Flux<Budget> в Mono<List<Budget>>
|
||||
}
|
||||
|
||||
|
||||
// @Cacheable("budgets", key = "#id")
|
||||
fun getBudget(id: String): Mono<BudgetDTO> {
|
||||
return budgetRepo.findById(id)
|
||||
.flatMap { budget ->
|
||||
val budgetDTO = BudgetDTO(
|
||||
budget.id,
|
||||
budget.name,
|
||||
budget.dateFrom,
|
||||
budget.dateTo,
|
||||
budget.createdAt,
|
||||
categories = budget.categories
|
||||
)
|
||||
|
||||
logger.info("Fetching categories and transactions")
|
||||
val categoriesMono = categoryService.getBudgetCategories(budgetDTO.dateFrom, budgetDTO.dateTo)
|
||||
val transactionsMono =
|
||||
transactionService.getTransactionsByTypes(budgetDTO.dateFrom, budgetDTO.dateTo)
|
||||
|
||||
|
||||
Mono.zip(categoriesMono, transactionsMono)
|
||||
.flatMap { tuple ->
|
||||
val categories = tuple.t1
|
||||
val transactions = tuple.t2
|
||||
|
||||
|
||||
Flux.fromIterable(budgetDTO.categories)
|
||||
.map { category ->
|
||||
categories[category.category.id]?.let { data ->
|
||||
category.currentSpent = data["instantAmount"] ?: 0.0
|
||||
category.currentPlanned = data["plannedAmount"] ?: 0.0
|
||||
}
|
||||
category
|
||||
}
|
||||
.collectList()
|
||||
.map { updatedCategories ->
|
||||
budgetDTO.categories = updatedCategories
|
||||
budgetDTO.plannedExpenses = transactions["plannedExpenses"] as MutableList
|
||||
budgetDTO.plannedIncomes = transactions["plannedIncomes"] as MutableList
|
||||
budgetDTO.transactions = transactions["instantTransactions"] as MutableList
|
||||
|
||||
budgetDTO
|
||||
}
|
||||
}
|
||||
}
|
||||
.doOnError { error ->
|
||||
logger.error("Error fetching budget: ${error.message}", error)
|
||||
}
|
||||
.switchIfEmpty(Mono.error(BudgetNotFoundException("Budget not found with id: $id")))
|
||||
}
|
||||
|
||||
|
||||
// fun transferBudgets() {
|
||||
// val budgets = getBudgets()
|
||||
// budgetRepo.saveAll<Budget>(budgets)
|
||||
//
|
||||
// }
|
||||
//
|
||||
// fun getBudgets(): List<Budget> {
|
||||
// val budgetIds = budgetRepoSql.getBudgetsIds()
|
||||
// var budgets = mutableListOf<Budget>()
|
||||
// budgetIds.forEach { budgetId ->
|
||||
// budgets.add(getBudgetSQL(budgetId)!!)
|
||||
// }
|
||||
// return budgets
|
||||
// }
|
||||
|
||||
|
||||
@CacheEvict(cacheNames = ["budgets", "budgetsList"], allEntries = true)
|
||||
fun createBudget(budget: Budget, createRecurrent: Boolean): Mono<Budget> {
|
||||
return Mono.zip(
|
||||
getBudgetByDate(budget.dateFrom).map { Optional.ofNullable(it) }
|
||||
.switchIfEmpty(Mono.just(Optional.empty())),
|
||||
getBudgetByDate(budget.dateTo).map { Optional.ofNullable(it) }
|
||||
.switchIfEmpty(Mono.just(Optional.empty()))
|
||||
).flatMap { tuple ->
|
||||
val startBudget = tuple.t1.orElse(null)
|
||||
val endBudget = tuple.t2.orElse(null)
|
||||
|
||||
// Проверяем, пересекаются ли бюджеты по датам
|
||||
if (startBudget != null || endBudget != null) {
|
||||
return@flatMap Mono.error<Budget>(IllegalArgumentException("Бюджет с теми же датами найден"))
|
||||
}
|
||||
|
||||
// Если createRecurrent=true, создаем рекуррентные транзакции
|
||||
val recurrentsCreation = if (createRecurrent) {
|
||||
recurrentService.createRecurrentsForBudget(budget)
|
||||
} else {
|
||||
Mono.empty()
|
||||
}
|
||||
|
||||
// Создаем бюджет после возможного создания рекуррентных транзакций
|
||||
recurrentsCreation.then(
|
||||
categoryService.getCategoryTransactionPipeline(budget.dateFrom, budget.dateTo)
|
||||
.flatMap { categories ->
|
||||
budget.categories = categories
|
||||
budgetRepo.save(budget)
|
||||
}
|
||||
.doOnNext { savedBudget ->
|
||||
// Выполнение updateBudgetWarns в фоне
|
||||
updateBudgetWarns(budget = savedBudget)
|
||||
.doOnError { error ->
|
||||
// Логируем ошибку, если произошла
|
||||
println("Error during updateBudgetWarns: ${error.message}")
|
||||
}
|
||||
.subscribe()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun getBudgetByDate(date: LocalDate): Mono<Budget> {
|
||||
return budgetRepo.findByDateFromLessThanEqualAndDateToGreaterThan(date, date).switchIfEmpty(Mono.empty())
|
||||
}
|
||||
|
||||
// fun getBudgetCategorySQL(id: Int): List<BudgetCategory>? {
|
||||
// var categories = budgetRepoSql.getBudgetCategory(id)
|
||||
// for (category in categories) {
|
||||
// categoryService.getCategoryByName(category.category.name)?.let { category.category = it }
|
||||
// }
|
||||
// return categories
|
||||
// }
|
||||
|
||||
fun getBudgetCategories(id: String): Mono<List<BudgetCategory>> {
|
||||
return budgetRepo.findById(id).flatMap { budget ->
|
||||
val lookup = lookup("categories", "category.\$id", "_id", "categoryDetailed")
|
||||
val unwind = unwind("categoryDetailed")
|
||||
val projectDouble = project("categoryDetailed", "amount", "date")
|
||||
.andExpression("{ \$toDouble: \"\$amount\" }").`as`("amount")
|
||||
val match = match(Criteria.where("date").gte(budget.dateFrom).lt(budget.dateTo))
|
||||
val group = group("categoryDetailed").sum("amount").`as`("currentSpent")
|
||||
val project = project("currentSpent").and("_id").`as`("category")
|
||||
val sort = sort(Sort.by(Sort.Order.asc("_id")))
|
||||
|
||||
val aggregation = newAggregation(lookup, unwind, projectDouble, match, group, project, sort)
|
||||
|
||||
reactiveMongoTemplate.aggregate(aggregation, "transactions", BudgetCategory::class.java)
|
||||
.collectList() // Преобразование результата в список
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun getBudgetTransactionsByType(budgetId: String): Mono<Map<String, List<Transaction>>> {
|
||||
return budgetRepo.findById(budgetId).flatMap { it ->
|
||||
transactionService.getTransactionsByTypes(it.dateFrom, it.dateTo)
|
||||
}
|
||||
}
|
||||
|
||||
fun getBudgetTransactions(
|
||||
budget: Budget,
|
||||
transactionType: String? = null,
|
||||
isDone: Boolean? = null,
|
||||
categoryType: String? = null,
|
||||
sortBy: SortSetting? = null
|
||||
): Mono<MutableList<Transaction>> {
|
||||
val defaultSort = SortSetting("date", Sort.Direction.ASC) // Сортировка по умолчанию
|
||||
|
||||
return transactionService.getTransactions(
|
||||
dateFrom = budget.dateFrom,
|
||||
dateTo = budget.dateTo,
|
||||
transactionType = transactionType,
|
||||
isDone = isDone,
|
||||
categoryType = categoryType,
|
||||
sortSetting = sortBy ?: defaultSort
|
||||
).onErrorResume { e ->
|
||||
Mono.error(RuntimeException("Error fetching transactions: ${e.message}", e))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@CacheEvict(cacheNames = ["budgets", "budgetsList"], allEntries = true)
|
||||
fun deleteBudget(budgetId: String): Mono<Void> {
|
||||
return budgetRepo.findById(budgetId)
|
||||
.switchIfEmpty(Mono.error(BudgetNotFoundException("Budget with id: $budgetId not found")))
|
||||
.flatMap { budget ->
|
||||
transactionService.getTransactionsToDelete(budget.dateFrom, budget.dateTo)
|
||||
.flatMapMany { transactions ->
|
||||
Flux.fromIterable(transactions)
|
||||
.flatMap { transaction ->
|
||||
transactionService.deleteTransaction(transaction.id!!)
|
||||
}
|
||||
}
|
||||
.then(
|
||||
budgetRepo.delete(budget)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@CacheEvict(cacheNames = ["budgets", "budgetsList"], allEntries = true)
|
||||
fun setCategoryLimit(budgetId: String, catId: String, limit: Double): Mono<BudgetCategory> {
|
||||
return budgetRepo.findById(budgetId).flatMap { budget ->
|
||||
val catEdit = budget.categories.firstOrNull { it.category.id == catId }
|
||||
?: return@flatMap Mono.error<BudgetCategory>(Exception("Category not found in the budget"))
|
||||
|
||||
transactionService.calcTransactionsSum(budget, catId, "PLANNED").flatMap { catPlanned ->
|
||||
if (catPlanned > limit) {
|
||||
Mono.error(Exception("Limit can't be less than planned expenses on category. Current planned value: $catPlanned"))
|
||||
} else {
|
||||
catEdit.currentLimit = limit
|
||||
budgetRepo.save(budget).flatMap {
|
||||
updateBudgetWarns(it)
|
||||
.thenReturn(catEdit)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun recalcBudgetCategory(): Mono<Void> {
|
||||
return budgetRepo.findAll(Sort.by(Direction.DESC, "dateFrom")) // Получаем все бюджеты
|
||||
// .flatMapIterable { budgets -> budgets } // Преобразуем Flux<Budget> в Flux<Budget> (итерация по бюджетам)
|
||||
// .flatMap { budget ->
|
||||
// logger.warn("HERE $budget")
|
||||
// Flux.fromIterable(budget.categories) // Преобразуем категории в поток
|
||||
// .flatMap { category ->
|
||||
// logger.warn("HERE $category")
|
||||
// Mono.zip(
|
||||
// transactionService.calcTransactionsSum(budget, category.category.id!!, "PLANNED"),
|
||||
// transactionService.calcTransactionsSum(budget, category.category.id!!, "INSTANT", isDone = true),
|
||||
// transactionService.calcTransactionsSum(budget, category.category.id!!, "PLANNED")
|
||||
// ).map { (plannedSum, spentSum, limitSum) ->
|
||||
// category.currentPlanned = plannedSum
|
||||
// category.currentSpent = spentSum
|
||||
// category.currentLimit = limitSum
|
||||
// }
|
||||
// }
|
||||
// .then() // Завершаем поток категорий
|
||||
// .flatMap { updateBudgetWarns(budgetId = budget.id!!) } // Обновляем предупреждения
|
||||
// }
|
||||
// .collectList() // Собираем все бюджеты
|
||||
// .flatMap { budgets -> budgetRepo.saveAll(budgets).collectList() } // Сохраняем все бюджеты
|
||||
.then() // Завершаем метод
|
||||
}
|
||||
|
||||
|
||||
fun getWarns(budgetId: String, isHide: Boolean? = null): Mono<List<Warn>> {
|
||||
return warnRepo.findAllByBudgetIdAndIsHide(budgetId, isHide == true).collectList()
|
||||
}
|
||||
|
||||
fun hideWarn(budgetId: String, warnId: String): Mono<Warn> {
|
||||
return warnRepo.findById(warnId) // Ищем предупреждение
|
||||
.flatMap { warn ->
|
||||
warn.isHide = true // Обновляем поле
|
||||
warnRepo.save(warn) // Сохраняем изменённое предупреждение
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun updateBudgetWarns(budget: Budget? = null): Mono<List<Warn>> {
|
||||
logger.info("STARTED WARNS UPDATE")
|
||||
|
||||
val finalBudgetMono = budget?.let { Mono.just(it) }
|
||||
?: return Mono.just(emptyList())
|
||||
|
||||
return finalBudgetMono.flatMap { finalBudget ->
|
||||
if (finalBudget.categories.isEmpty()) {
|
||||
logger.info("No categories found for budget ${finalBudget.id}")
|
||||
return@flatMap Mono.just(emptyList<Warn>())
|
||||
}
|
||||
|
||||
val averageSumsMono = transactionService.getAverageSpendingByCategory()
|
||||
val averageIncomeMono = transactionService.getAverageIncome()
|
||||
val currentBudgetIncomeMono = transactionService.calcTransactionsSum(
|
||||
finalBudget, transactionType = "PLANNED", categoryType = "INCOME"
|
||||
)
|
||||
val plannedIncomeMono = transactionService.calcTransactionsSum(
|
||||
finalBudget, categoryType = "INCOME", transactionType = "PLANNED"
|
||||
)
|
||||
val plannedSavingMono = transactionService.calcTransactionsSum(
|
||||
finalBudget, categoryId = "675850148198643f121e466a", transactionType = "PLANNED"
|
||||
)
|
||||
|
||||
Mono.zip(
|
||||
averageSumsMono,
|
||||
averageIncomeMono,
|
||||
currentBudgetIncomeMono,
|
||||
plannedIncomeMono,
|
||||
plannedSavingMono
|
||||
).flatMap { tuple ->
|
||||
val averageSums = tuple.t1
|
||||
val averageIncome = tuple.t2
|
||||
val currentBudgetIncome = tuple.t3
|
||||
val plannedIncome = tuple.t4
|
||||
val plannedSaving = tuple.t5
|
||||
|
||||
Flux.fromIterable(finalBudget.categories)
|
||||
.flatMap { category ->
|
||||
processCategoryWarnings(
|
||||
category,
|
||||
finalBudget,
|
||||
averageSums,
|
||||
averageIncome,
|
||||
currentBudgetIncome,
|
||||
plannedIncome,
|
||||
plannedSaving
|
||||
)
|
||||
}
|
||||
.collectList()
|
||||
.flatMap { warns ->
|
||||
warnRepo.saveAll(warns.filterNotNull()).collectList()
|
||||
}
|
||||
.doOnSuccess { logger.info("ENDED WARNS UPDATE") }
|
||||
.map { it.sortedByDescending { warn -> warn.serenity.sort } }
|
||||
}
|
||||
}.doOnError { error ->
|
||||
logger.error("Error updating budget warns: ${error.message}", error)
|
||||
}.onErrorResume {
|
||||
Mono.just(emptyList()) // Возвращаем пустой список в случае ошибки
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun processCategoryWarnings(
|
||||
category: BudgetCategory,
|
||||
finalBudget: Budget,
|
||||
averageSums: Map<String, Double>,
|
||||
averageIncome: Double,
|
||||
currentBudgetIncome: Double,
|
||||
plannedIncome: Double,
|
||||
plannedSaving: Double
|
||||
): Flux<Warn> {
|
||||
val warnsForCategory = mutableListOf<Mono<Warn?>>()
|
||||
|
||||
val averageSum = averageSums[category.category.id] ?: 0.0
|
||||
val categorySpentRatioInAvgIncome = if (averageIncome > 0.0) averageSum / averageIncome else 0.0
|
||||
val projectedAvailableSum = currentBudgetIncome * categorySpentRatioInAvgIncome
|
||||
val contextAtAvg = "category${category.category.id}atbudget${finalBudget.id}lessavg"
|
||||
val lowSavingContext = "savingValueLess10atBudget${finalBudget.id}"
|
||||
|
||||
if (averageSum > category.currentLimit) {
|
||||
val warnMono = warnRepo.findWarnByContext(contextAtAvg)
|
||||
.switchIfEmpty(
|
||||
Mono.just(
|
||||
Warn(
|
||||
serenity = WarnSerenity.MAIN,
|
||||
message = PushMessage(
|
||||
title = "Внимание на ${category.category.name}!",
|
||||
body = "Лимит меньше средних трат (Среднее: <b>${averageSum.toInt()} ₽</b> Текущий лимит: <b>${category.currentLimit.toInt()} ₽</b>)." +
|
||||
"\nСредняя доля данной категории в доходах: <b>${(categorySpentRatioInAvgIncome * 100).toInt()}%</b>." +
|
||||
"\nПроецируется на текущие поступления: <b>${projectedAvailableSum.toInt()} ₽</b>",
|
||||
icon = category.category.icon
|
||||
),
|
||||
budgetId = finalBudget.id!!,
|
||||
context = contextAtAvg,
|
||||
isHide = false
|
||||
)
|
||||
)
|
||||
)
|
||||
warnsForCategory.add(warnMono)
|
||||
} else {
|
||||
warnRepo.findWarnByContext(contextAtAvg).flatMap { warnRepo.delete(it).then(Mono.empty<Warn>()) }
|
||||
}
|
||||
|
||||
if (category.category.id == "675850148198643f121e466a") {
|
||||
val savingRatio = if (plannedIncome > 0.0) category.currentLimit / plannedIncome else 0.0
|
||||
if (savingRatio < 0.1) {
|
||||
val warnMono = warnRepo.findWarnByContext(lowSavingContext)
|
||||
.switchIfEmpty(
|
||||
Mono.just(
|
||||
Warn(
|
||||
serenity = WarnSerenity.IMPORTANT,
|
||||
message = PushMessage(
|
||||
title = "Доля сбережений очень мала!",
|
||||
body = "Текущие плановые сбережения равны ${plannedSaving.toInt()} (${
|
||||
(savingRatio * 100).toInt()
|
||||
}%)! Исправьте!",
|
||||
icon = category.category.icon
|
||||
),
|
||||
budgetId = finalBudget.id!!,
|
||||
context = lowSavingContext,
|
||||
isHide = false
|
||||
)
|
||||
)
|
||||
)
|
||||
warnsForCategory.add(warnMono)
|
||||
} else {
|
||||
warnRepo.findWarnByContext(lowSavingContext)
|
||||
.flatMap { warnRepo.delete(it).then(Mono.empty<Warn>()) }
|
||||
}
|
||||
}
|
||||
|
||||
return Flux.fromIterable(warnsForCategory).flatMap { it }
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package space.luminic.budgerapp.services
|
||||
|
||||
import org.springframework.cache.CacheManager
|
||||
import org.springframework.stereotype.Service
|
||||
|
||||
@Service
|
||||
class CacheInspector(private val cacheManager: CacheManager) {
|
||||
|
||||
fun getCacheContent(cacheName: String): Map<Any, Any>? {
|
||||
val cache = cacheManager.getCache(cacheName)
|
||||
if (cache != null && cache is org.springframework.cache.concurrent.ConcurrentMapCache) {
|
||||
return cache.nativeCache as Map<Any, Any>
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,362 @@
|
||||
package space.luminic.budgerapp.services
|
||||
|
||||
|
||||
import org.bson.Document
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.cache.annotation.CacheEvict
|
||||
import org.springframework.cache.annotation.Cacheable
|
||||
|
||||
import org.springframework.data.mongodb.core.ReactiveMongoTemplate
|
||||
|
||||
import org.springframework.stereotype.Service
|
||||
import reactor.core.publisher.Flux
|
||||
import reactor.core.publisher.Mono
|
||||
import space.luminic.budgerapp.models.BudgetCategory
|
||||
import space.luminic.budgerapp.models.Category
|
||||
import space.luminic.budgerapp.models.CategoryType
|
||||
import space.luminic.budgerapp.repos.CategoryRepo
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
import java.time.LocalTime
|
||||
import java.time.ZoneId
|
||||
import java.time.ZoneOffset
|
||||
import java.util.Date
|
||||
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
|
||||
@Service
|
||||
class CategoryService(
|
||||
private val categoryRepo: CategoryRepo,
|
||||
private val transactionService: TransactionService,
|
||||
private val mongoTemplate: ReactiveMongoTemplate
|
||||
) {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(javaClass)
|
||||
|
||||
fun getCategoryByName(name: String): Mono<Category> {
|
||||
return categoryRepo.findByName(name)
|
||||
|
||||
}
|
||||
|
||||
@Cacheable("getAllCategories")
|
||||
fun getCategories(): Mono<List<Category>> {
|
||||
return categoryRepo.findAll().collectList()
|
||||
}
|
||||
|
||||
@Cacheable("categoryTypes")
|
||||
fun getCategoryTypes(): List<CategoryType> {
|
||||
var types = mutableListOf<CategoryType>()
|
||||
types.add(CategoryType("EXPENSE", "Траты"))
|
||||
types.add(CategoryType("INCOME", "Поступления"))
|
||||
return types
|
||||
}
|
||||
|
||||
@CacheEvict(cacheNames = ["getAllCategories"],allEntries = true)
|
||||
fun createCategory(category: Category): Mono<Category> {
|
||||
return categoryRepo.save(category)
|
||||
}
|
||||
|
||||
@CacheEvict(cacheNames = ["getAllCategories"],allEntries = true)
|
||||
fun editCategory(category: Category): Mono<Category> {
|
||||
return categoryRepo.findById(category.id!!) // Возвращаем Mono<Category>
|
||||
.flatMap { oldCategory ->
|
||||
if (oldCategory.type.code != category.type.code) {
|
||||
return@flatMap Mono.error<Category>(IllegalArgumentException("You cannot change category type"))
|
||||
}
|
||||
categoryRepo.save(category) // Сохраняем категорию, если тип не изменился
|
||||
}
|
||||
}
|
||||
|
||||
@CacheEvict(cacheNames = ["getAllCategories"],allEntries = true)
|
||||
fun deleteCategory(categoryId: String): Mono<String> {
|
||||
return categoryRepo.findById(categoryId).switchIfEmpty(
|
||||
Mono.error(IllegalArgumentException("Category with id: $categoryId not found"))
|
||||
).flatMap {
|
||||
transactionService.getTransactions(categoryId = categoryId)
|
||||
.flatMapMany { transactions ->
|
||||
categoryRepo.findByName("Другое").switchIfEmpty(
|
||||
categoryRepo.save(
|
||||
Category(
|
||||
type = CategoryType("EXPENSE", "Траты"),
|
||||
name = "Другое",
|
||||
description = "Категория для других трат",
|
||||
icon = "🚮"
|
||||
)
|
||||
)
|
||||
).flatMapMany { newCategory ->
|
||||
Flux.fromIterable(transactions).flatMap { transaction ->
|
||||
transaction.category = newCategory // Присваиваем конкретный объект категории
|
||||
transactionService.editTransaction(transaction) // Сохраняем изменения
|
||||
}
|
||||
}
|
||||
}
|
||||
.then(categoryRepo.deleteById(categoryId)) // Удаляем старую категорию
|
||||
.thenReturn(categoryId) // Возвращаем удалённую категорию
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun getBudgetCategories(dateFrom: LocalDate, dateTo: LocalDate): Mono<Map<String, Map<String, Double>>> {
|
||||
logger.info("here cat starts")
|
||||
val pipeline = listOf(
|
||||
Document(
|
||||
"\$lookup",
|
||||
Document("from", "transactions")
|
||||
.append(
|
||||
"let",
|
||||
Document("categoryId", "\$_id")
|
||||
)
|
||||
.append(
|
||||
"pipeline", listOf(
|
||||
Document(
|
||||
"\$match",
|
||||
Document(
|
||||
"\$expr",
|
||||
Document(
|
||||
"\$and", listOf(
|
||||
Document("\$eq", listOf("\$category.\$id", "\$\$categoryId")),
|
||||
Document(
|
||||
"\$gte", listOf(
|
||||
"\$date",
|
||||
Date.from(
|
||||
LocalDateTime.of(dateFrom, LocalTime.MIN)
|
||||
.atZone(ZoneId.systemDefault())
|
||||
.withZoneSameInstant(ZoneOffset.UTC).toInstant()
|
||||
),
|
||||
)
|
||||
),
|
||||
Document(
|
||||
"\$lt", listOf(
|
||||
"\$date",
|
||||
Date.from(
|
||||
LocalDateTime.of(dateTo, LocalTime.MIN)
|
||||
.atZone(ZoneId.systemDefault())
|
||||
.withZoneSameInstant(ZoneOffset.UTC).toInstant()
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
Document(
|
||||
"\$group",
|
||||
Document("_id", "\$type.code")
|
||||
.append(
|
||||
"totalAmount",
|
||||
Document("\$sum", "\$amount")
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
.append("as", "transactionSums")
|
||||
),
|
||||
Document(
|
||||
"\$project",
|
||||
Document("_id", 1L)
|
||||
.append(
|
||||
"plannedAmount",
|
||||
Document(
|
||||
"\$arrayElemAt", listOf(
|
||||
Document(
|
||||
"\$filter",
|
||||
Document("input", "\$transactionSums")
|
||||
.append("as", "sum")
|
||||
.append(
|
||||
"cond",
|
||||
Document("\$eq", listOf("\$\$sum._id", "PLANNED"))
|
||||
)
|
||||
), 0L
|
||||
)
|
||||
)
|
||||
)
|
||||
.append(
|
||||
"instantAmount",
|
||||
Document(
|
||||
"\$arrayElemAt", listOf(
|
||||
Document(
|
||||
"\$filter",
|
||||
Document("input", "\$transactionSums")
|
||||
.append("as", "sum")
|
||||
.append(
|
||||
"cond",
|
||||
Document("\$eq", listOf("\$\$sum._id", "INSTANT"))
|
||||
)
|
||||
), 0L
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
Document(
|
||||
"\$addFields",
|
||||
Document(
|
||||
"plannedAmount",
|
||||
Document("\$ifNull", listOf("\$plannedAmount.totalAmount", 0.0))
|
||||
)
|
||||
.append(
|
||||
"instantAmount",
|
||||
Document("\$ifNull", listOf("\$instantAmount.totalAmount", 0.0))
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
// Анализ плана выполнения (вывод для отладки)
|
||||
// getCategoriesExplainReactive(pipeline)
|
||||
// .doOnNext { explainResult ->
|
||||
// logger.info("Explain Result: ${explainResult.toJson()}")
|
||||
// }
|
||||
// .subscribe() // Этот вызов лучше оставить только для отладки
|
||||
//
|
||||
|
||||
|
||||
return mongoTemplate.getCollection("categories")
|
||||
.flatMapMany { it.aggregate(pipeline) }
|
||||
.collectList()
|
||||
.flatMap { result ->
|
||||
val categories = result.associate { document ->
|
||||
val id = document["_id"].toString()
|
||||
val values = mapOf(
|
||||
"plannedAmount" to (document["plannedAmount"] as Double? ?: 0.0),
|
||||
"instantAmount" to (document["instantAmount"] as Double? ?: 0.0)
|
||||
)
|
||||
id to values
|
||||
}
|
||||
logger.info("here cat ends")
|
||||
|
||||
Mono.just(categories)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun getCategoryTransactionPipeline(dateFrom: LocalDate, dateTo: LocalDate): Mono<MutableList<BudgetCategory>> {
|
||||
val pipeline = listOf(
|
||||
Document("\$match", Document("type.code", "EXPENSE")),
|
||||
Document(
|
||||
"\$lookup",
|
||||
Document("from", "transactions")
|
||||
.append(
|
||||
"let",
|
||||
Document("categoryId", "\$_id")
|
||||
)
|
||||
.append(
|
||||
"pipeline", listOf(
|
||||
Document(
|
||||
"\$match",
|
||||
Document(
|
||||
"\$expr",
|
||||
Document(
|
||||
"\$and", listOf(
|
||||
Document("\$eq", listOf("\$category.\$id", "\$\$categoryId")),
|
||||
Document(
|
||||
"\$gte", listOf(
|
||||
"\$date",
|
||||
Date.from(
|
||||
LocalDateTime.of(dateFrom, LocalTime.MIN)
|
||||
.atZone(ZoneId.systemDefault())
|
||||
.withZoneSameInstant(ZoneOffset.UTC).toInstant()
|
||||
)
|
||||
)
|
||||
),
|
||||
Document(
|
||||
"\$lt", listOf(
|
||||
"\$date",
|
||||
Date.from(
|
||||
LocalDateTime.of(dateTo, LocalTime.MIN)
|
||||
.atZone(ZoneId.systemDefault())
|
||||
.withZoneSameInstant(ZoneOffset.UTC).toInstant()
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
Document(
|
||||
"\$group",
|
||||
Document("_id", "\$type.code")
|
||||
.append(
|
||||
"totalAmount",
|
||||
Document("\$sum", "\$amount")
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
.append("as", "transactionSums")
|
||||
),
|
||||
Document(
|
||||
"\$project",
|
||||
Document("_id", 1L)
|
||||
.append("type", 1L)
|
||||
.append("name", 1L)
|
||||
.append("description", 1L)
|
||||
.append("icon", 1L)
|
||||
.append(
|
||||
"plannedAmount",
|
||||
Document(
|
||||
"\$arrayElemAt", listOf(
|
||||
Document(
|
||||
"\$filter",
|
||||
Document("input", "\$transactionSums")
|
||||
.append("as", "sum")
|
||||
.append(
|
||||
"cond",
|
||||
Document("\$eq", listOf("\$\$sum._id", "PLANNED"))
|
||||
)
|
||||
), 0.0
|
||||
)
|
||||
)
|
||||
)
|
||||
.append(
|
||||
"instantAmount",
|
||||
Document(
|
||||
"\$arrayElemAt", listOf(
|
||||
Document(
|
||||
"\$filter",
|
||||
Document("input", "\$transactionSums")
|
||||
.append("as", "sum")
|
||||
.append(
|
||||
"cond",
|
||||
Document("\$eq", listOf("\$\$sum._id", "INSTANT"))
|
||||
)
|
||||
), 0.0
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
Document(
|
||||
"\$addFields",
|
||||
Document(
|
||||
"plannedAmount",
|
||||
Document("\$ifNull", listOf("\$plannedAmount.totalAmount", 0.0))
|
||||
)
|
||||
.append(
|
||||
"instantAmount",
|
||||
Document("\$ifNull", listOf("\$instantAmount.totalAmount", 0.0))
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
return mongoTemplate.getCollection("categories")
|
||||
.flatMapMany { it.aggregate(pipeline, Document::class.java) }
|
||||
.map { document ->
|
||||
val catType = document["type"] as Document
|
||||
BudgetCategory(
|
||||
currentSpent = document["instantAmount"] as Double,
|
||||
currentLimit = document["plannedAmount"] as Double,
|
||||
currentPlanned = document["plannedAmount"] as Double,
|
||||
category = Category(
|
||||
document["_id"].toString(),
|
||||
CategoryType(catType["code"] as String, catType["name"] as String),
|
||||
name = document["name"] as String,
|
||||
description = document["description"] as String,
|
||||
icon = document["icon"] as String
|
||||
)
|
||||
)
|
||||
}
|
||||
.collectList()
|
||||
.map { it.toMutableList() }
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package space.luminic.budgerapp.services
|
||||
import org.springframework.security.core.userdetails.ReactiveUserDetailsService
|
||||
import org.springframework.security.core.userdetails.UserDetails
|
||||
import org.springframework.security.core.userdetails.UserDetailsService
|
||||
import org.springframework.stereotype.Service
|
||||
import reactor.core.publisher.Mono
|
||||
|
||||
@Service
|
||||
class CustomReactiveUserDetailsService(
|
||||
private val userDetailsService: UserDetailsService // Ваш синхронный сервис
|
||||
) : ReactiveUserDetailsService {
|
||||
|
||||
override fun findByUsername(username: String): Mono<UserDetails> {
|
||||
return Mono.fromCallable { userDetailsService.loadUserByUsername(username) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package space.luminic.budgerapp.services
|
||||
|
||||
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.cache.annotation.CacheEvict
|
||||
import org.springframework.cache.annotation.Cacheable
|
||||
import org.springframework.security.core.context.ReactiveSecurityContextHolder
|
||||
import org.springframework.stereotype.Service
|
||||
import reactor.core.publisher.Flux
|
||||
import reactor.core.publisher.Mono
|
||||
import space.luminic.budgerapp.models.Budget
|
||||
import space.luminic.budgerapp.models.NotFoundException
|
||||
import space.luminic.budgerapp.models.Recurrent
|
||||
import space.luminic.budgerapp.models.Transaction
|
||||
import space.luminic.budgerapp.models.TransactionType
|
||||
import space.luminic.budgerapp.repos.RecurrentRepo
|
||||
import space.luminic.budgerapp.repos.TransactionRepo
|
||||
import space.luminic.budgerapp.repos.sqlrepo.RecurrentRepoSQL
|
||||
import java.math.BigDecimal
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
import java.time.LocalTime
|
||||
import java.time.YearMonth
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
|
||||
|
||||
@Service
|
||||
class RecurrentService(
|
||||
private val recurrentRepo: RecurrentRepo,
|
||||
private val recurrentRepoSQL: RecurrentRepoSQL,
|
||||
private val transactionRepo: TransactionRepo,
|
||||
private val userService: UserService,
|
||||
) {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(javaClass)
|
||||
|
||||
@Cacheable("recurrentsList")
|
||||
fun getRecurrents(): Mono<List<Recurrent>> {
|
||||
return recurrentRepo.findAll().collectList()
|
||||
}
|
||||
|
||||
@Cacheable("recurrents", key = "#id")
|
||||
fun getRecurrentById(id: String): Mono<Recurrent> {
|
||||
return recurrentRepo.findById(id)
|
||||
.switchIfEmpty(Mono.error(NotFoundException("Recurrent with id: $id not found")))
|
||||
}
|
||||
|
||||
|
||||
@CacheEvict(cacheNames = ["recurrentsList", "recurrents"])
|
||||
fun createRecurrent(recurrent: Recurrent): Mono<Recurrent> {
|
||||
return if (recurrent.id == null && recurrent.atDay <= 31) recurrentRepo.save(recurrent) else Mono.error(
|
||||
RuntimeException("Cannot create recurrent with id or date cannot be higher than 31")
|
||||
)
|
||||
}
|
||||
|
||||
@CacheEvict(cacheNames = ["recurrentsList", "recurrents"])
|
||||
fun createRecurrentsForBudget(budget: Budget): Mono<Void> {
|
||||
val currentYearMonth = YearMonth.of(budget.dateFrom.year, budget.dateFrom.monthValue)
|
||||
val daysInCurrentMonth = currentYearMonth.lengthOfMonth()
|
||||
val context = ReactiveSecurityContextHolder.getContext()
|
||||
.doOnNext { println("Security context: $it") }
|
||||
.switchIfEmpty(Mono.error(IllegalStateException("SecurityContext is empty!")))
|
||||
return context
|
||||
.map {
|
||||
logger.debug(it.authentication.name)
|
||||
it.authentication
|
||||
}
|
||||
.flatMap { authentication ->
|
||||
val username = authentication.name
|
||||
userService.getByUsername(username)
|
||||
.switchIfEmpty(Mono.error(IllegalArgumentException("User not found for username: $username")))
|
||||
}
|
||||
.flatMapMany { user ->
|
||||
recurrentRepo.findAll()
|
||||
.map { recurrent ->
|
||||
// Определяем дату транзакции
|
||||
val transactionDate = when {
|
||||
recurrent.atDay <= daysInCurrentMonth && recurrent.atDay >= budget.dateFrom.dayOfMonth -> {
|
||||
currentYearMonth.atDay(recurrent.atDay)
|
||||
}
|
||||
|
||||
recurrent.atDay < budget.dateFrom.dayOfMonth -> {
|
||||
currentYearMonth.atDay(recurrent.atDay).plusMonths(1)
|
||||
}
|
||||
|
||||
else -> {
|
||||
val extraDays = recurrent.atDay - daysInCurrentMonth
|
||||
currentYearMonth.plusMonths(1).atDay(extraDays)
|
||||
}
|
||||
}
|
||||
|
||||
// Создаем транзакцию
|
||||
Transaction(
|
||||
date = transactionDate,
|
||||
amount = recurrent.amount.toDouble(),
|
||||
category = recurrent.category,
|
||||
isDone = false,
|
||||
comment = recurrent.name,
|
||||
user = user,
|
||||
type = TransactionType("PLANNED", "Запланированные")
|
||||
)
|
||||
}
|
||||
}
|
||||
.collectList() // Собираем все транзакции в список
|
||||
.flatMap { transactions ->
|
||||
transactionRepo.saveAll(transactions) // Сохраняем все транзакции разом
|
||||
.then() // Возвращаем Mono<Void>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun editRecurrent(recurrent: Recurrent): Mono<Recurrent> {
|
||||
return if (recurrent.id != null && recurrent.atDay <= 31) recurrentRepo.save(recurrent) else Mono.error(
|
||||
RuntimeException("Cannot edit recurrent without id or date cannot be higher than 31")
|
||||
)
|
||||
}
|
||||
|
||||
fun deleteRecurrent(id: String): Mono<Void> {
|
||||
return recurrentRepo.deleteById(id)
|
||||
}
|
||||
|
||||
|
||||
fun transferRecurrents() {
|
||||
recurrentRepo.saveAll(recurrentRepoSQL.getRecurrents()).then().subscribe()
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package space.luminic.budgerapp.services
|
||||
|
||||
import com.interaso.webpush.VapidKeys
|
||||
import com.interaso.webpush.WebPushService
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.encodeToString
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.dao.DuplicateKeyException
|
||||
|
||||
|
||||
import org.springframework.stereotype.Service
|
||||
import reactor.core.publisher.Mono
|
||||
|
||||
import space.luminic.budgerapp.models.PushMessage
|
||||
import space.luminic.budgerapp.models.Subscription
|
||||
import space.luminic.budgerapp.models.SubscriptionDTO
|
||||
import space.luminic.budgerapp.models.User
|
||||
import space.luminic.budgerapp.repos.SubscriptionRepo
|
||||
import space.luminic.budgerapp.services.VapidConstants.VAPID_PRIVATE_KEY
|
||||
import space.luminic.budgerapp.services.VapidConstants.VAPID_PUBLIC_KEY
|
||||
import space.luminic.budgerapp.services.VapidConstants.VAPID_SUBJECT
|
||||
import java.security.GeneralSecurityException
|
||||
|
||||
object VapidConstants {
|
||||
const val VAPID_PUBLIC_KEY =
|
||||
"BKmMyBUhpkcmzYWcYsjH_spqcy0zf_8eVtZo60f7949TgLztCmv3YD0E_vtV2dTfECQ4sdLdPK3ICDcyOkCqr84"
|
||||
const val VAPID_PRIVATE_KEY = "YeJH_0LhnVYN6RdxMidgR6WMYlpGXTJS3HjT9V3NSGI"
|
||||
const val VAPID_SUBJECT = "mailto:voroninvyu@gmail.com"
|
||||
}
|
||||
|
||||
@Service
|
||||
class SubscriptionService(private val subscriptionRepo: SubscriptionRepo) {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(javaClass)
|
||||
private val pushService =
|
||||
WebPushService(
|
||||
subject = VAPID_SUBJECT,
|
||||
vapidKeys = VapidKeys.fromUncompressedBytes(VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY)
|
||||
)
|
||||
|
||||
fun sendNotification(endpoint: String, p256dh: String, auth: String, payload: PushMessage): Mono<Void> {
|
||||
return Mono.fromRunnable<Void> {
|
||||
pushService.send(
|
||||
payload = Json.encodeToString(payload),
|
||||
endpoint = endpoint,
|
||||
p256dh = p256dh,
|
||||
auth = auth
|
||||
)
|
||||
}
|
||||
.doOnSuccess {
|
||||
logger.info("Уведомление успешно отправлено на endpoint: $endpoint")
|
||||
}
|
||||
.doOnError { e ->
|
||||
logger.error("Ошибка при отправке уведомления на endpoint $endpoint: ${e.message}")
|
||||
}
|
||||
.onErrorResume { e ->
|
||||
Mono.error(e) // Пробрасываем ошибку дальше, если нужна обработка выше
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun sendToAll(payload: PushMessage): Mono<List<String>> {
|
||||
return subscriptionRepo.findAll()
|
||||
.flatMap { sub ->
|
||||
sendNotification(sub.endpoint, sub.p256dh, sub.auth, payload)
|
||||
.then(Mono.just("${sub.user?.username} at endpoint ${sub.endpoint}"))
|
||||
.onErrorResume { e ->
|
||||
sub.isActive = false
|
||||
subscriptionRepo.save(sub).then(Mono.empty())
|
||||
}
|
||||
}
|
||||
.collectList() // Собираем результаты в список
|
||||
}
|
||||
|
||||
|
||||
fun subscribe(subscriptionDTO: SubscriptionDTO, user: User): Mono<String> {
|
||||
val subscription = Subscription(
|
||||
id = null,
|
||||
user = user,
|
||||
endpoint = subscriptionDTO.endpoint,
|
||||
auth = subscriptionDTO.keys["auth"].orEmpty(),
|
||||
p256dh = subscriptionDTO.keys["p256dh"].orEmpty(),
|
||||
isActive = true
|
||||
)
|
||||
|
||||
return subscriptionRepo.save(subscription)
|
||||
.flatMap { savedSubscription ->
|
||||
Mono.just("Subscription created with ID: ${savedSubscription.id}")
|
||||
}
|
||||
.onErrorResume(DuplicateKeyException::class.java) {
|
||||
logger.info("Subscription already exists. Skipping.")
|
||||
Mono.just("Subscription already exists. Skipping.")
|
||||
}
|
||||
.onErrorResume { e ->
|
||||
logger.error("Error while saving subscription: ${e.message}")
|
||||
Mono.error(RuntimeException("Error while saving subscription"))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package space.luminic.budgerapp.services
|
||||
import org.springframework.cache.annotation.CacheEvict
|
||||
import org.springframework.cache.annotation.Cacheable
|
||||
import org.springframework.stereotype.Service
|
||||
import reactor.core.publisher.Mono
|
||||
import space.luminic.budgerapp.models.Token
|
||||
import space.luminic.budgerapp.models.TokenStatus
|
||||
import space.luminic.budgerapp.repos.TokenRepo
|
||||
import java.time.LocalDateTime
|
||||
|
||||
@Service
|
||||
class TokenService(private val tokenRepository: TokenRepo) {
|
||||
|
||||
@CacheEvict("tokens", allEntries = true)
|
||||
fun saveToken(token: String, username: String, expiresAt: LocalDateTime) {
|
||||
val newToken = Token(
|
||||
token = token,
|
||||
username = username,
|
||||
issuedAt = LocalDateTime.now(),
|
||||
expiresAt = expiresAt
|
||||
)
|
||||
tokenRepository.save(newToken)
|
||||
}
|
||||
|
||||
@CacheEvict("tokens", allEntries = true)
|
||||
fun revokeToken(token: String): Mono<Void> {
|
||||
return tokenRepository.findByToken(token)
|
||||
.flatMap { existingToken ->
|
||||
val updatedToken = existingToken.copy(status = TokenStatus.REVOKED)
|
||||
tokenRepository.save(updatedToken).then()
|
||||
}
|
||||
.switchIfEmpty(Mono.error(Exception("Token not found")))
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@CacheEvict("tokens", allEntries = true)
|
||||
fun deleteExpiredTokens() {
|
||||
tokenRepository.deleteByExpiresAtBefore(LocalDateTime.now())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,784 @@
|
||||
package space.luminic.budgerapp.services
|
||||
|
||||
import com.mongodb.client.model.Aggregates.addFields
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.bson.Document
|
||||
import org.bson.types.ObjectId
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.cache.annotation.CacheEvict
|
||||
import org.springframework.cache.annotation.Cacheable
|
||||
import org.springframework.context.ApplicationEventPublisher
|
||||
import org.springframework.data.domain.Sort
|
||||
import org.springframework.data.domain.Sort.Direction
|
||||
import org.springframework.data.mongodb.MongoExpression
|
||||
|
||||
import org.springframework.data.mongodb.core.aggregation.Aggregation.newAggregation
|
||||
import org.springframework.data.mongodb.core.MongoTemplate
|
||||
import org.springframework.data.mongodb.core.ReactiveMongoTemplate
|
||||
import org.springframework.data.mongodb.core.aggregation.AddFieldsOperation
|
||||
import org.springframework.data.mongodb.core.aggregation.Aggregation
|
||||
import org.springframework.data.mongodb.core.aggregation.Aggregation.ROOT
|
||||
import org.springframework.data.mongodb.core.aggregation.Aggregation.group
|
||||
import org.springframework.data.mongodb.core.aggregation.Aggregation.lookup
|
||||
import org.springframework.data.mongodb.core.aggregation.Aggregation.match
|
||||
import org.springframework.data.mongodb.core.aggregation.Aggregation.project
|
||||
import org.springframework.data.mongodb.core.aggregation.Aggregation.sort
|
||||
import org.springframework.data.mongodb.core.aggregation.Aggregation.unwind
|
||||
import org.springframework.data.mongodb.core.aggregation.Aggregation.addFields
|
||||
import org.springframework.data.mongodb.core.aggregation.Aggregation.limit
|
||||
import org.springframework.data.mongodb.core.aggregation.Aggregation.skip
|
||||
import org.springframework.data.mongodb.core.aggregation.AggregationExpression
|
||||
import org.springframework.data.mongodb.core.aggregation.AggregationResults
|
||||
import org.springframework.data.mongodb.core.aggregation.ArrayOperators
|
||||
import org.springframework.data.mongodb.core.aggregation.ArrayOperators.Filter.filter
|
||||
import org.springframework.data.mongodb.core.aggregation.ConditionalOperators
|
||||
import org.springframework.data.mongodb.core.aggregation.ConditionalOperators.IfNull.ifNull
|
||||
import org.springframework.data.mongodb.core.aggregation.DateOperators
|
||||
import org.springframework.data.mongodb.core.aggregation.DateOperators.DateToString
|
||||
import org.springframework.data.mongodb.core.aggregation.LookupOperation
|
||||
import org.springframework.data.mongodb.core.aggregation.MatchOperation
|
||||
|
||||
|
||||
import org.springframework.data.mongodb.core.query.Criteria
|
||||
import org.springframework.data.mongodb.core.query.Query
|
||||
import org.springframework.data.mongodb.core.query.update
|
||||
import org.springframework.security.core.context.ReactiveSecurityContextHolder
|
||||
|
||||
|
||||
import org.springframework.stereotype.Service
|
||||
import reactor.core.publisher.Flux
|
||||
import reactor.core.publisher.Mono
|
||||
import space.luminic.budgerapp.models.Budget
|
||||
import space.luminic.budgerapp.models.Category
|
||||
import space.luminic.budgerapp.models.CategoryType
|
||||
|
||||
import space.luminic.budgerapp.models.SortSetting
|
||||
import space.luminic.budgerapp.models.SortTypes
|
||||
import space.luminic.budgerapp.models.Transaction
|
||||
import space.luminic.budgerapp.models.TransactionEvent
|
||||
import space.luminic.budgerapp.models.TransactionEventType
|
||||
import space.luminic.budgerapp.models.TransactionType
|
||||
import space.luminic.budgerapp.models.User
|
||||
import space.luminic.budgerapp.repos.TransactionRepo
|
||||
|
||||
import java.math.BigDecimal
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
import java.time.LocalTime
|
||||
import java.time.ZoneId
|
||||
import java.time.ZoneOffset
|
||||
import java.time.ZonedDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.temporal.TemporalAdjusters
|
||||
import java.util.ArrayList
|
||||
import java.util.Arrays
|
||||
import java.util.Date
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
|
||||
@Service
|
||||
class TransactionService(
|
||||
private val mongoTemplate: MongoTemplate,
|
||||
private val reactiveMongoTemplate: ReactiveMongoTemplate,
|
||||
val transactionsRepo: TransactionRepo,
|
||||
val userService: UserService,
|
||||
private val eventPublisher: ApplicationEventPublisher
|
||||
) {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(TransactionService::class.java)
|
||||
|
||||
@Cacheable("transactions")
|
||||
fun getTransactions(
|
||||
dateFrom: LocalDate? = null,
|
||||
dateTo: LocalDate? = null,
|
||||
transactionType: String? = null,
|
||||
isDone: Boolean? = null,
|
||||
categoryId: String? = null,
|
||||
categoryType: String? = null,
|
||||
userId: String? = null,
|
||||
parentId: String? = null,
|
||||
isChild: Boolean? = null,
|
||||
sortSetting: SortSetting? = null,
|
||||
limit: Int? = null,
|
||||
offset: Int? = null,
|
||||
): Mono<MutableList<Transaction>> {
|
||||
val matchCriteria = mutableListOf<Criteria>()
|
||||
|
||||
// Добавляем фильтры
|
||||
dateFrom?.let { matchCriteria.add(Criteria.where("date").gte(it)) }
|
||||
dateTo?.let { matchCriteria.add(Criteria.where("date").lt(it)) }
|
||||
transactionType?.let { matchCriteria.add(Criteria.where("type.code").`is`(it)) }
|
||||
isDone?.let { matchCriteria.add(Criteria.where("isDone").`is`(it)) }
|
||||
categoryId?.let { matchCriteria.add(Criteria.where("categoryDetails._id").`is`(it)) }
|
||||
categoryType?.let { matchCriteria.add(Criteria.where("categoryDetails.type.code").`is`(it)) }
|
||||
userId?.let { matchCriteria.add(Criteria.where("userDetails._id").`is`(ObjectId(it))) }
|
||||
parentId?.let { matchCriteria.add(Criteria.where("parentId").`is`(it)) }
|
||||
isChild?.let { matchCriteria.add(Criteria.where("parentId").exists(it)) }
|
||||
|
||||
// Сборка агрегации
|
||||
val lookup = lookup("categories", "category.\$id", "_id", "categoryDetails")
|
||||
val lookupUsers = lookup("users", "user.\$id", "_id", "userDetails")
|
||||
val match = match(Criteria().andOperator(*matchCriteria.toTypedArray()))
|
||||
|
||||
var sort = sort(Sort.by(Direction.DESC, "date").and(Sort.by(Direction.DESC, "createdAt")))
|
||||
|
||||
sortSetting?.let {
|
||||
sort = sort(Sort.by(it.order, it.by).and(Sort.by(Direction.ASC, "createdAt")))
|
||||
}
|
||||
|
||||
val aggregationBuilder = mutableListOf(
|
||||
lookup,
|
||||
lookupUsers,
|
||||
match.takeIf { matchCriteria.isNotEmpty() },
|
||||
sort,
|
||||
offset?.let { skip(it.toLong()) },
|
||||
limit?.let { limit(it.toLong()) }
|
||||
).filterNotNull()
|
||||
|
||||
val aggregation = newAggregation(aggregationBuilder)
|
||||
|
||||
return reactiveMongoTemplate.aggregate(
|
||||
aggregation, "transactions", Transaction::class.java
|
||||
)
|
||||
.collectList() // Преобразуем Flux<Transaction> в Mono<List<Transaction>>
|
||||
.map { it.toMutableList() }
|
||||
}
|
||||
|
||||
fun getTransactionsToDelete(dateFrom: LocalDate, dateTo: LocalDate): Mono<List<Transaction>> {
|
||||
val criteria = Criteria().andOperator(
|
||||
Criteria.where("date").gte(dateFrom),
|
||||
Criteria.where("date").lte(dateTo),
|
||||
Criteria().orOperator(
|
||||
Criteria.where("type.code").`is`("PLANNED"),
|
||||
Criteria.where("parentId").exists(true)
|
||||
)
|
||||
)
|
||||
|
||||
// Пример использования в MongoTemplate:
|
||||
val query = Query(criteria)
|
||||
|
||||
// Если вы хотите использовать ReactiveMongoTemplate:
|
||||
return reactiveMongoTemplate.find(query, Transaction::class.java)
|
||||
.collectList()
|
||||
.doOnNext { transactions -> println("Found transactions: $transactions") }
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Cacheable("transactions")
|
||||
fun getTransactionById(id: String): Mono<Transaction> {
|
||||
return transactionsRepo.findById(id)
|
||||
.map {
|
||||
it
|
||||
}
|
||||
.switchIfEmpty(
|
||||
Mono.error(IllegalArgumentException("Transaction with id: $id not found"))
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@CacheEvict(cacheNames = ["transactions"], allEntries = true)
|
||||
fun createTransaction(transaction: Transaction): Mono<String> {
|
||||
return ReactiveSecurityContextHolder.getContext()
|
||||
.map { it.authentication } // Получаем Authentication из SecurityContext
|
||||
.flatMap { authentication ->
|
||||
val username = authentication.name // Имя пользователя из токена
|
||||
// Получаем пользователя и сохраняем транзакцию
|
||||
userService.getByUsername(username)
|
||||
.switchIfEmpty(Mono.error(IllegalArgumentException("User not found for username: $username")))
|
||||
.flatMap { user ->
|
||||
transaction.user = user
|
||||
transactionsRepo.save(transaction)
|
||||
.doOnNext { savedTransaction ->
|
||||
// Публикуем событие после сохранения
|
||||
eventPublisher.publishEvent(
|
||||
TransactionEvent(
|
||||
this,
|
||||
TransactionEventType.CREATE,
|
||||
newTransaction = savedTransaction,
|
||||
oldTransaction = savedTransaction
|
||||
)
|
||||
)
|
||||
}
|
||||
.map { it.id!! } // Возвращаем ID сохраненной транзакции
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@CacheEvict(cacheNames = ["transactions"], allEntries = true)
|
||||
|
||||
fun editTransaction(transaction: Transaction): Mono<Transaction> {
|
||||
return transactionsRepo.findById(transaction.id!!)
|
||||
.flatMap { oldStateOfTransaction ->
|
||||
val changed = compareSumDateDoneIsChanged(oldStateOfTransaction, transaction)
|
||||
if (!changed) {
|
||||
return@flatMap transactionsRepo.save(transaction) // Сохраняем, если изменений нет
|
||||
}
|
||||
|
||||
val amountDifference = transaction.amount - oldStateOfTransaction.amount
|
||||
|
||||
// Обработка дочерней транзакции
|
||||
handleChildTransaction(oldStateOfTransaction, transaction, amountDifference)
|
||||
.then(transactionsRepo.save(transaction)) // Сохраняем основную транзакцию
|
||||
.doOnSuccess { savedTransaction ->
|
||||
eventPublisher.publishEvent(
|
||||
TransactionEvent(
|
||||
this,
|
||||
TransactionEventType.EDIT,
|
||||
newTransaction = savedTransaction,
|
||||
oldTransaction = oldStateOfTransaction,
|
||||
difference = amountDifference
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
.switchIfEmpty(
|
||||
Mono.error(IllegalArgumentException("Transaction not found with id: ${transaction.id}"))
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleChildTransaction(
|
||||
oldTransaction: Transaction,
|
||||
newTransaction: Transaction,
|
||||
amountDifference: Double
|
||||
): Mono<Void> {
|
||||
return transactionsRepo.findByParentId(newTransaction.id!!)
|
||||
.flatMap { childTransaction ->
|
||||
logger.info(childTransaction.toString())
|
||||
// Если родительская транзакция обновлена, обновляем дочернюю
|
||||
childTransaction.amount = newTransaction.amount
|
||||
childTransaction.category = newTransaction.category
|
||||
childTransaction.comment = newTransaction.comment
|
||||
childTransaction.user = newTransaction.user
|
||||
transactionsRepo.save(childTransaction)
|
||||
}
|
||||
.switchIfEmpty(
|
||||
Mono.defer {
|
||||
// Создание новой дочерней транзакции, если требуется
|
||||
if (!oldTransaction.isDone && newTransaction.isDone) {
|
||||
val newChildTransaction = newTransaction.copy(
|
||||
id = null,
|
||||
type = TransactionType("INSTANT", "Текущие"),
|
||||
parentId = newTransaction.id
|
||||
)
|
||||
transactionsRepo.save(newChildTransaction).doOnSuccess { savedChildTransaction ->
|
||||
eventPublisher.publishEvent(
|
||||
TransactionEvent(
|
||||
this,
|
||||
TransactionEventType.CREATE,
|
||||
newTransaction = savedChildTransaction,
|
||||
oldTransaction = oldTransaction,
|
||||
difference = amountDifference
|
||||
)
|
||||
)
|
||||
}
|
||||
} else Mono.empty()
|
||||
}
|
||||
)
|
||||
.flatMap {
|
||||
// Удаление дочерней транзакции, если родительская помечена как не выполненная
|
||||
if (oldTransaction.isDone && !newTransaction.isDone) {
|
||||
transactionsRepo.findByParentId(newTransaction.id!!)
|
||||
.flatMap { child ->
|
||||
deleteTransaction(child.id!!)
|
||||
}.then()
|
||||
} else {
|
||||
Mono.empty()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun compareSumDateDoneIsChanged(t1: Transaction, t2: Transaction): Boolean {
|
||||
return if (t1.amount != t2.amount) {
|
||||
true
|
||||
} else if (t1.date != t2.date) {
|
||||
true
|
||||
} else if (t1.isDone != t2.isDone) {
|
||||
true
|
||||
} else if (t1.category.id != t2.category.id) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@CacheEvict(cacheNames = ["transactions"], allEntries = true)
|
||||
fun deleteTransaction(transactionId: String): Mono<Void> {
|
||||
return transactionsRepo.findById(transactionId)
|
||||
.flatMap { transactionToDelete ->
|
||||
transactionsRepo.deleteById(transactionId) // Удаляем транзакцию
|
||||
.then(
|
||||
Mono.fromRunnable<Void> {
|
||||
// Публикуем событие после успешного удаления
|
||||
eventPublisher.publishEvent(
|
||||
TransactionEvent(
|
||||
this,
|
||||
TransactionEventType.DELETE,
|
||||
newTransaction = transactionToDelete,
|
||||
oldTransaction = transactionToDelete
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// @CacheEvict(cacheNames = ["transactions", "childTransactions"], allEntries = true)
|
||||
// fun setTransactionDone(transaction: Transaction): Transaction {
|
||||
// val oldStateTransaction = transactionsRepo.findById(transaction.id!!)
|
||||
// .orElseThrow { RuntimeException("Transaction ${transaction.id} not found") }
|
||||
//
|
||||
// if (transaction.isDone) {
|
||||
// if (oldStateTransaction.isDone) {
|
||||
// throw RuntimeException("Transaction ${transaction.id} is already done")
|
||||
// }
|
||||
//
|
||||
// // Создание дочерней транзакции
|
||||
// val childTransaction = transaction.copy(
|
||||
// id = null,
|
||||
// type = TransactionType("INSTANT", "Текущие"),
|
||||
// parentId = transaction.id
|
||||
// )
|
||||
// createTransaction(childTransaction)
|
||||
// } else {
|
||||
// // Удаление дочерней транзакции, если она существует
|
||||
// transactionsRepo.findByParentId(transaction.id!!).getOrNull()?.let {
|
||||
// deleteTransaction(it.id!!)
|
||||
// } ?: logger.warn("Child transaction of parent ${transaction.id} not found")
|
||||
// }
|
||||
//
|
||||
// return editTransaction(transaction)
|
||||
// }
|
||||
|
||||
|
||||
@Cacheable("childTransactions", key = "#parentId")
|
||||
fun getChildTransaction(parentId: String): Mono<Transaction> {
|
||||
return transactionsRepo.findByParentId(parentId)
|
||||
}
|
||||
|
||||
// fun getTransactionByOldId(id: Int): Transaction? {
|
||||
// return transactionsRepo.findByOldId(id).getOrNull()
|
||||
// }
|
||||
|
||||
// fun transferTransactions(): Mono<Void> {
|
||||
// var transactions = transactionsRepoSQl.getTransactions()
|
||||
// return transactionsRepo.saveAll(transactions).then()
|
||||
// }
|
||||
//
|
||||
|
||||
fun calcTransactionsSum(
|
||||
budget: Budget,
|
||||
categoryId: String? = null,
|
||||
categoryType: String? = null,
|
||||
transactionType: String? = null,
|
||||
isDone: Boolean? = null
|
||||
): Mono<Double> {
|
||||
val matchCriteria = mutableListOf<Criteria>()
|
||||
|
||||
// Добавляем фильтры
|
||||
matchCriteria.add(Criteria.where("date").gte(budget.dateFrom))
|
||||
matchCriteria.add(Criteria.where("date").lt(budget.dateTo))
|
||||
categoryId?.let { matchCriteria.add(Criteria.where("category.\$id").`is`(ObjectId(it))) }
|
||||
categoryType?.let { matchCriteria.add(Criteria.where("categoryDetails.type.code").`is`(it)) }
|
||||
transactionType?.let { matchCriteria.add(Criteria.where("type.code").`is`(it)) }
|
||||
isDone?.let { matchCriteria.add(Criteria.where("isDone").`is`(it)) }
|
||||
|
||||
// Сборка агрегации
|
||||
val lookup = lookup("categories", "category.\$id", "_id", "categoryDetails")
|
||||
val unwind = unwind("categoryDetails")
|
||||
val match = match(Criteria().andOperator(*matchCriteria.toTypedArray()))
|
||||
val project = project("category").andExpression("{ \$toDouble: \"\$amount\" }").`as`("amount")
|
||||
val group = group(categoryId ?: "all").sum("amount").`as`("totalSum")
|
||||
val projectSum = project("totalSum")
|
||||
val aggregation = newAggregation(lookup, unwind, match, project, group, projectSum)
|
||||
|
||||
return reactiveMongoTemplate.aggregate(aggregation, "transactions", Map::class.java)
|
||||
.map { result ->
|
||||
val totalSum = result["totalSum"]
|
||||
if (totalSum is Double) {
|
||||
totalSum
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
.reduce(0.0) { acc, sum -> acc + sum } // Суммируем значения, если несколько результатов
|
||||
}
|
||||
|
||||
|
||||
// @Cacheable("transactions")
|
||||
fun getAverageSpendingByCategory(): Mono<Map<String, Double>> {
|
||||
val firstDateOfMonth = LocalDate.now().with(TemporalAdjusters.firstDayOfMonth())
|
||||
|
||||
val lookup = lookup("categories", "category.\$id", "_id", "categoryDetails")
|
||||
val unwind = unwind("categoryDetails")
|
||||
val match = match(
|
||||
Criteria.where("categoryDetails.type.code").`is`("EXPENSE")
|
||||
.and("type.code").`is`("INSTANT")
|
||||
.and("date").lt(firstDateOfMonth)
|
||||
)
|
||||
val projectDate = project("_id", "category", "amount", "categoryDetails")
|
||||
.and(DateToString.dateOf("date").toString("%Y-%m")).`as`("month")
|
||||
.andExpression("{ \$toDouble: \"\$amount\" }").`as`("amount")
|
||||
val groupByMonthAndCategory = group("month", "category.\$id").sum("amount").`as`("sum")
|
||||
val groupByCategory = group("_id.id").avg("sum").`as`("averageAmount")
|
||||
val project = project()
|
||||
.and("_id").`as`("category")
|
||||
.and("averageAmount").`as`("avgAmount")
|
||||
val sort = sort(Sort.by(Sort.Order.asc("_id")))
|
||||
|
||||
val aggregation = newAggregation(
|
||||
lookup, unwind, match, projectDate, groupByMonthAndCategory, groupByCategory, project, sort
|
||||
)
|
||||
|
||||
return reactiveMongoTemplate.aggregate(aggregation, "transactions", Map::class.java)
|
||||
.collectList()
|
||||
.map { results ->
|
||||
results.associate { result ->
|
||||
val category = result["category"]?.toString() ?: "Unknown"
|
||||
val avgAmount = (result["avgAmount"] as? Double) ?: 0.0
|
||||
category to avgAmount
|
||||
}
|
||||
}
|
||||
.defaultIfEmpty(emptyMap()) // Возвращаем пустую карту, если результатов нет
|
||||
}
|
||||
|
||||
|
||||
@Cacheable("transactionTypes")
|
||||
fun getTransactionTypes(): List<TransactionType> {
|
||||
var types = mutableListOf<TransactionType>()
|
||||
types.add(TransactionType("PLANNED", "Плановые"))
|
||||
types.add(TransactionType("INSTANT", "Текущие"))
|
||||
return types
|
||||
}
|
||||
|
||||
fun getAverageIncome(): Mono<Double> {
|
||||
val lookup = lookup("categories", "category.\$id", "_id", "detailedCategory")
|
||||
|
||||
val unwind = unwind("detailedCategory")
|
||||
|
||||
val match = match(
|
||||
Criteria.where("detailedCategory.type.code").`is`("INCOME")
|
||||
.and("type.code").`is`("INSTANT")
|
||||
.and("isDone").`is`(true)
|
||||
)
|
||||
|
||||
val project = project("_id", "category", "detailedCategory")
|
||||
.and(DateToString.dateOf("date").toString("%Y-%m")).`as`("month")
|
||||
.andExpression("{ \$toDouble: \"\$amount\" }").`as`("amount")
|
||||
|
||||
val groupByMonth = group("month").sum("amount").`as`("sum")
|
||||
|
||||
val groupForAverage = group("avgIncomeByMonth").avg("sum").`as`("averageAmount")
|
||||
|
||||
val aggregation = newAggregation(lookup, unwind, match, project, groupByMonth, groupForAverage)
|
||||
|
||||
return reactiveMongoTemplate.aggregate(aggregation, "transactions", Map::class.java)
|
||||
.singleOrEmpty() // Ожидаем только один результат
|
||||
.map { result ->
|
||||
result["averageAmount"] as? Double ?: 0.0
|
||||
}
|
||||
.defaultIfEmpty(0.0) // Если результат пустой, возвращаем 0.0
|
||||
}
|
||||
|
||||
|
||||
fun getTransactionsByTypes(dateFrom: LocalDate, dateTo: LocalDate): Mono<Map<String, List<Transaction>>> {
|
||||
logger.info("here tran starts")
|
||||
val pipeline = listOf(
|
||||
Document(
|
||||
"\$lookup",
|
||||
Document("from", "categories")
|
||||
.append("localField", "category.\$id")
|
||||
.append("foreignField", "_id")
|
||||
.append("as", "categoryDetailed")
|
||||
),
|
||||
Document(
|
||||
"\$lookup",
|
||||
Document("from", "users")
|
||||
.append("localField", "user.\$id")
|
||||
.append("foreignField", "_id")
|
||||
.append("as", "userDetailed")
|
||||
),
|
||||
Document(
|
||||
"\$unwind",
|
||||
Document("path", "\$categoryDetailed").append("preserveNullAndEmptyArrays", true)
|
||||
),
|
||||
Document(
|
||||
"\$unwind",
|
||||
Document("path", "\$userDetailed").append("preserveNullAndEmptyArrays", true)
|
||||
),
|
||||
Document(
|
||||
"\$match",
|
||||
Document(
|
||||
"\$and", listOf(
|
||||
Document(
|
||||
"date",
|
||||
Document(
|
||||
"\$gte",
|
||||
Date.from(
|
||||
LocalDateTime.of(dateFrom, LocalTime.MIN)
|
||||
.atZone(ZoneId.systemDefault())
|
||||
.withZoneSameInstant(ZoneOffset.UTC)
|
||||
.toInstant()
|
||||
)
|
||||
)
|
||||
),
|
||||
Document(
|
||||
"date",
|
||||
Document(
|
||||
"\$lt",
|
||||
Date.from(
|
||||
LocalDateTime.of(dateTo, LocalTime.MAX)
|
||||
.atZone(ZoneId.systemDefault())
|
||||
.withZoneSameInstant(ZoneOffset.UTC)
|
||||
.toInstant()
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
Document(
|
||||
"\$facet",
|
||||
Document(
|
||||
"plannedExpenses",
|
||||
listOf(
|
||||
Document(
|
||||
"\$match",
|
||||
Document("type.code", "PLANNED")
|
||||
.append("categoryDetailed.type.code", "EXPENSE")
|
||||
),
|
||||
Document("\$sort", Document("date", 1).append("_id", 1))
|
||||
)
|
||||
)
|
||||
.append(
|
||||
"plannedIncomes",
|
||||
listOf(
|
||||
Document(
|
||||
"\$match",
|
||||
Document("type.code", "PLANNED")
|
||||
.append("categoryDetailed.type.code", "INCOME")
|
||||
),
|
||||
Document("\$sort", Document("date", 1).append("_id", 1))
|
||||
)
|
||||
)
|
||||
.append(
|
||||
"instantTransactions",
|
||||
listOf(
|
||||
Document("\$match", Document("type.code", "INSTANT")),
|
||||
Document("\$sort", Document("date", 1).append("_id", 1))
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
// getCategoriesExplainReactive(pipeline)
|
||||
// .doOnNext { explainResult ->
|
||||
// logger.info("Explain Result: ${explainResult.toJson()}")
|
||||
// }
|
||||
// .subscribe() // Этот вызов лучше оставить только для отладки
|
||||
return reactiveMongoTemplate.getCollection("transactions")
|
||||
.flatMapMany { it.aggregate(pipeline, Document::class.java) }
|
||||
.single() // Получаем только первый результат агрегации
|
||||
.flatMap { aggregationResult ->
|
||||
Mono.zip(
|
||||
extractTransactions(aggregationResult, "plannedExpenses"),
|
||||
extractTransactions(aggregationResult, "plannedIncomes"),
|
||||
extractTransactions(aggregationResult, "instantTransactions")
|
||||
).map { tuple ->
|
||||
val plannedExpenses = tuple.t1
|
||||
val plannedIncomes = tuple.t2
|
||||
val instantTransactions = tuple.t3
|
||||
logger.info("here tran ends")
|
||||
mapOf(
|
||||
"plannedExpenses" to plannedExpenses,
|
||||
"plannedIncomes" to plannedIncomes,
|
||||
"instantTransactions" to instantTransactions
|
||||
)
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
fun getCategoriesExplainReactive(pipeline: List<Document>): Mono<Document> {
|
||||
val command = Document("aggregate", "transactions")
|
||||
.append("pipeline", pipeline)
|
||||
.append("explain", true)
|
||||
|
||||
return reactiveMongoTemplate.executeCommand(command)
|
||||
}
|
||||
|
||||
private fun extractTransactions(aggregationResult: Document, key: String): Mono<List<Transaction>> {
|
||||
val resultTransactions = aggregationResult[key] as? List<Document> ?: emptyList()
|
||||
return Flux.fromIterable(resultTransactions)
|
||||
.map { documentToTransactionMapper(it) }
|
||||
.collectList()
|
||||
}
|
||||
|
||||
|
||||
private fun documentToTransactionMapper(document: Document): Transaction {
|
||||
val transactionType = document["type"] as Document
|
||||
var user: User? = null
|
||||
|
||||
val userDocument = document["userDetailed"] as Document
|
||||
user = User(
|
||||
id = (userDocument["_id"] as ObjectId).toString(),
|
||||
username = userDocument["username"] as String,
|
||||
firstName = userDocument["firstName"] as String,
|
||||
tgId = userDocument["tgId"] as String,
|
||||
tgUserName = userDocument["tgUserName"]?.let { it as String },
|
||||
password = null,
|
||||
isActive = userDocument["isActive"] as Boolean,
|
||||
regDate = userDocument["regDate"] as Date,
|
||||
createdAt = userDocument["createdAt"] as Date,
|
||||
roles = userDocument["roles"] as ArrayList<String>,
|
||||
)
|
||||
|
||||
|
||||
val categoryDocument = document["categoryDetailed"] as Document
|
||||
val categoryTypeDocument = categoryDocument["type"] as Document
|
||||
val category = Category(
|
||||
id = (categoryDocument["_id"] as ObjectId).toString(),
|
||||
type = CategoryType(categoryTypeDocument["code"] as String, categoryTypeDocument["name"] as String),
|
||||
name = categoryDocument["name"] as String,
|
||||
description = categoryDocument["description"] as String,
|
||||
icon = categoryDocument["icon"] as String
|
||||
)
|
||||
return Transaction(
|
||||
(document["_id"] as ObjectId).toString(),
|
||||
TransactionType(
|
||||
transactionType["code"] as String,
|
||||
transactionType["name"] as String
|
||||
),
|
||||
user = user!!,
|
||||
category = category,
|
||||
comment = document["comment"] as String,
|
||||
date = (document["date"] as Date).toInstant().atZone(ZoneId.systemDefault()).toLocalDate(),
|
||||
amount = document["amount"] as Double,
|
||||
isDone = document["isDone"] as Boolean,
|
||||
parentId = if (document["parentId"] != null) document["parentId"] as String else null,
|
||||
createdAt = LocalDateTime.ofInstant((document["createdAt"] as Date).toInstant(), ZoneOffset.UTC),
|
||||
)
|
||||
|
||||
}
|
||||
// fun getPlannedForBudget(budget: Budget, transactionType: String? = null): List<Map<String, Any>> {
|
||||
// // 1) $lookup: "categories"
|
||||
// val lookupCategories = Aggregation.lookup(
|
||||
// "categories",
|
||||
// "category.\$id",
|
||||
// "_id",
|
||||
// "categoryDetailed"
|
||||
// )
|
||||
//
|
||||
// // 2) $lookup: "budgets" (pipeline + let)
|
||||
// val matchBudgetsDoc = Document(
|
||||
// "\$expr", Document(
|
||||
// "\$and", listOf(
|
||||
// Document("\$gte", listOf("\$\$transactionDate", "\$dateFrom")),
|
||||
// Document("\$lt", listOf("\$\$transactionDate", "\$dateTo"))
|
||||
// )
|
||||
// )
|
||||
// )
|
||||
// val matchBudgetsOp = MatchOperation(matchBudgetsDoc)
|
||||
//
|
||||
// val lookupBudgets = LookupOperation.newLookup()
|
||||
// .from("budgets")
|
||||
// .letValueOf("transactionDate").bindTo("date")
|
||||
// .pipeline(matchBudgetsOp)
|
||||
// .`as`("budgetDetails")
|
||||
//
|
||||
// // 3) $unwind
|
||||
// val unwindCategory = Aggregation.unwind("categoryDetailed")
|
||||
// val unwindBudget = Aggregation.unwind("budgetDetails")
|
||||
//
|
||||
// // 4) $match: диапазон дат
|
||||
// val matchDates = Aggregation.match(
|
||||
// Criteria("date")
|
||||
// .gte(budget.dateFrom)
|
||||
// .lt(budget.dateTo)
|
||||
// )
|
||||
//
|
||||
// // 5) $facet (разные ветки: plannedExpenses, plannedExpensesSum, ...)
|
||||
// // plannedExpenses
|
||||
// val plannedExpensesMatch = Aggregation.match(
|
||||
// Criteria().andOperator(
|
||||
// Criteria("type.code").`is`("PLANNED"),
|
||||
// Criteria("categoryDetailed.type.code").`is`("EXPENSE")
|
||||
// )
|
||||
// )
|
||||
// val plannedExpensesPipeline = listOf(plannedExpensesMatch)
|
||||
//
|
||||
// // plannedExpensesSum
|
||||
// val plannedExpensesSumPipeline = listOf(
|
||||
// plannedExpensesMatch,
|
||||
// group(null).`as`("_id").sum("amount").`as`("sum"),
|
||||
// project("sum").andExclude("_id")
|
||||
// )
|
||||
//
|
||||
// // plannedIncome
|
||||
// val plannedIncomeMatch = Aggregation.match(
|
||||
// Criteria().andOperator(
|
||||
// Criteria("type.code").`is`("PLANNED"),
|
||||
// Criteria("categoryDetailed.type.code").`is`("INCOME")
|
||||
// )
|
||||
// )
|
||||
// val plannedIncomePipeline = listOf(plannedIncomeMatch)
|
||||
//
|
||||
// // plannedIncomeSum
|
||||
// val plannedIncomeSumPipeline = listOf(
|
||||
// plannedIncomeMatch,
|
||||
// group().`as`("_id").sum("amount").`as`("sum"),
|
||||
// project("sum").andExclude("_id")
|
||||
// )
|
||||
//
|
||||
// // instantTransactions
|
||||
// val instantTransactionsMatch = Aggregation.match(
|
||||
// Criteria("type.code").`is`("INSTANT")
|
||||
// )
|
||||
// val instantTransactionsProject = Aggregation.project(
|
||||
// "_id", "type", "comment", "user", "amount", "date",
|
||||
// "category", "isDone", "createdAt", "parentId"
|
||||
// )
|
||||
// val instantTransactionsPipeline = listOf(instantTransactionsMatch, instantTransactionsProject)
|
||||
//
|
||||
// val facetStage = Aggregation.facet(*plannedExpensesPipeline.toTypedArray()).`as`("plannedExpenses")
|
||||
// .and(*plannedExpensesSumPipeline.toTypedArray()).`as`("plannedExpensesSum")
|
||||
// .and(*plannedIncomePipeline.toTypedArray()).`as`("plannedIncome")
|
||||
// .and(*plannedIncomeSumPipeline.toTypedArray()).`as`("plannedIncomeSum")
|
||||
// .and(*instantTransactionsPipeline.toTypedArray()).`as`("instantTransactions")
|
||||
//
|
||||
// // 6) $set: вытаскиваем суммы из массивов
|
||||
// val setStage = AddFieldsOperation.builder()
|
||||
// .addField("plannedExpensesSum").withValue(
|
||||
// ArrayOperators.ArrayElemAt.arrayOf("\$plannedExpensesSum.sum").elementAt(0)
|
||||
// )
|
||||
// .addField("plannedIncomeSum").withValue(
|
||||
// ArrayOperators.ArrayElemAt.arrayOf("\$plannedIncomeSum.sum").elementAt(0)
|
||||
// )
|
||||
// .build()
|
||||
//
|
||||
// // Собираем все стадии
|
||||
// val aggregation = Aggregation.newAggregation(
|
||||
// lookupCategories,
|
||||
// lookupBudgets,
|
||||
// unwindCategory,
|
||||
// unwindBudget,
|
||||
// matchDates,
|
||||
// facetStage,
|
||||
// setStage
|
||||
// )
|
||||
//
|
||||
// val results = mongoTemplate.aggregate(aggregation, "transactions", Map::class.java)
|
||||
// return results.mappedResults
|
||||
// }
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package space.luminic.budgerapp.services
|
||||
|
||||
import org.springframework.stereotype.Service
|
||||
import reactor.core.publisher.Flux
|
||||
import reactor.core.publisher.Mono
|
||||
import space.luminic.budgerapp.models.Budget
|
||||
import space.luminic.budgerapp.models.Category
|
||||
import space.luminic.budgerapp.models.Transaction
|
||||
import space.luminic.budgerapp.repos.BudgetRepo
|
||||
import space.luminic.budgerapp.repos.CategoryRepo
|
||||
import space.luminic.budgerapp.repos.TransactionRepo
|
||||
import space.luminic.budgerapp.repos.sqlrepo.BudgetRepoSQL
|
||||
import space.luminic.budgerapp.repos.sqlrepo.CategoriesRepoSQL
|
||||
import space.luminic.budgerapp.repos.sqlrepo.TransactionsRepoSQl
|
||||
|
||||
@Service
|
||||
class TransferService(
|
||||
private val transactionsRepoSQl: TransactionsRepoSQl,
|
||||
private val categoriesRepoSQL: CategoriesRepoSQL,
|
||||
private val budgetRepoSQL: BudgetRepoSQL,
|
||||
private val categoryRepo: CategoryRepo,
|
||||
private val transactionRepo: TransactionRepo,
|
||||
private val budgetService: BudgetService
|
||||
) {
|
||||
|
||||
fun getTransactions(): Mono<List<Transaction>> {
|
||||
val transactions = transactionsRepoSQl.getTransactions()
|
||||
return transactionRepo.saveAll(transactions).collectList()
|
||||
}
|
||||
|
||||
|
||||
fun getCategories(): Mono<List<Category>> {
|
||||
val categories = categoriesRepoSQL.getCategories()
|
||||
|
||||
return Flux.fromIterable(categories)
|
||||
.flatMap { category -> categoryRepo.save(category) }
|
||||
.collectList() // Преобразуем Flux<Category> в Mono<List<Category>>
|
||||
}
|
||||
|
||||
|
||||
fun transferBudgets(): Mono<List<Budget>> {
|
||||
val budgets = budgetRepoSQL.getBudgets()
|
||||
return Flux.fromIterable(budgets)
|
||||
.flatMap { budget ->
|
||||
budgetService.createBudget(budget.budget, budget.createRecurrent)
|
||||
}.collectList()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package space.luminic.budgerapp.services
|
||||
|
||||
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.cache.annotation.Cacheable
|
||||
import org.springframework.security.crypto.password.PasswordEncoder
|
||||
import org.springframework.stereotype.Service
|
||||
import reactor.core.publisher.Mono
|
||||
import space.luminic.budgerapp.models.NotFoundException
|
||||
import space.luminic.budgerapp.models.User
|
||||
import space.luminic.budgerapp.repos.UserRepo
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
|
||||
@Service
|
||||
class UserService(val userRepo: UserRepo, val passwordEncoder: PasswordEncoder) {
|
||||
val logger = LoggerFactory.getLogger(javaClass)
|
||||
|
||||
|
||||
// fun regenPass(): List<User>? {
|
||||
// var users = getUsers()!!.toMutableList()
|
||||
// for (user in users) {
|
||||
// user.password = passwordEncoder.encode(user.password)
|
||||
// }
|
||||
// userRepo.saveAll<User>(users)
|
||||
// return users
|
||||
// }
|
||||
|
||||
@Cacheable("users", key = "#username")
|
||||
fun getByUsername(username: String): Mono<User> {
|
||||
return userRepo.findByUsernameWOPassword(username).switchIfEmpty(
|
||||
Mono.error(NotFoundException("User with username: $username not found"))
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
fun getById(id: String): Mono<User> {
|
||||
return userRepo.findById(id)
|
||||
.map { user ->
|
||||
user.apply { password = null } // Убираем пароль
|
||||
}
|
||||
.switchIfEmpty(Mono.error(Exception("User not found"))) // Обрабатываем случай, когда пользователь не найден
|
||||
}
|
||||
|
||||
|
||||
@Cacheable("users", key = "#username")
|
||||
fun getByUserNameWoPass(username: String): Mono<User> {
|
||||
return userRepo.findByUsernameWOPassword(username)
|
||||
}
|
||||
|
||||
fun getUsers(): Mono<List<User>> {
|
||||
return userRepo.findAll()
|
||||
.map { user -> user.apply { password = null } } // Убираем пароль
|
||||
.collectList() // Преобразуем Flux<User> в Mono<List<User>>
|
||||
.doOnNext { logger.debug("Users fetched successfully: ${it.size} users found") }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package space.luminic.budgerapp.utils
|
||||
|
||||
import com.fasterxml.jackson.core.JsonParser
|
||||
import com.fasterxml.jackson.databind.DeserializationContext
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.Date
|
||||
|
||||
//class DateSerializer : JsonDeserializer<LocalDateTime>() {
|
||||
//
|
||||
// private val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") // Пример формата
|
||||
//
|
||||
// override fun deserialize(p: JsonParser, ctxt: DeserializationContext): LocalDateTime {
|
||||
// val dateAsString = p.text
|
||||
// var date = formatter.parse(dateAsString)
|
||||
// return LocalDateTime.from(date, LocalDateTime.MIN ) // Преобразуем строку в LocalDateTime
|
||||
// }
|
||||
//}
|
||||
54
src/main/kotlin/space/luminic/budgerapp/utils/JWTUtil.kt
Normal file
54
src/main/kotlin/space/luminic/budgerapp/utils/JWTUtil.kt
Normal file
@@ -0,0 +1,54 @@
|
||||
package space.luminic.budgerapp.utils
|
||||
|
||||
import io.jsonwebtoken.Jwts
|
||||
|
||||
import io.jsonwebtoken.security.Keys
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
import space.luminic.budgerapp.services.TokenService
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneId
|
||||
import java.util.*
|
||||
|
||||
@Component
|
||||
class JWTUtil(private val tokenService: TokenService) {
|
||||
|
||||
private val key = Keys.hmacShaKeyFor("MyTusimMyFleximMyEstSilaNasNeVzlomayutEtoNevozmozhno".toByteArray())
|
||||
|
||||
|
||||
fun generateToken(username: String): String {
|
||||
val expireAt = Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24 * 10)
|
||||
val token = Jwts.builder()
|
||||
.setSubject(username)
|
||||
.setIssuedAt(Date())
|
||||
.setExpiration(expireAt) // 10 дней
|
||||
.signWith(key)
|
||||
.compact()
|
||||
tokenService.saveToken(
|
||||
token,
|
||||
username,
|
||||
LocalDateTime.from(
|
||||
expireAt.toInstant().atZone(ZoneId.systemDefault())
|
||||
.toLocalDateTime()
|
||||
)
|
||||
)
|
||||
return token
|
||||
|
||||
}
|
||||
|
||||
fun validateToken(token: String): String? {
|
||||
return try {
|
||||
val claims = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token)
|
||||
claims.body.subject
|
||||
} catch (e: io.jsonwebtoken.ExpiredJwtException) {
|
||||
println("Token expired: ${e.message}")
|
||||
null
|
||||
} catch (e: io.jsonwebtoken.SignatureException) {
|
||||
println("Invalid token signature: ${e.message}")
|
||||
null
|
||||
} catch (e: Exception) {
|
||||
println("Token validation error: ${e.message}")
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
24
src/main/resources/application-dev.properties
Normal file
24
src/main/resources/application-dev.properties
Normal file
@@ -0,0 +1,24 @@
|
||||
spring.application.name=budger-app
|
||||
|
||||
spring.data.mongodb.host=127.0.0.1
|
||||
spring.data.mongodb.port=27017
|
||||
spring.data.mongodb.database=budger-app
|
||||
|
||||
|
||||
management.endpoints.web.exposure.include=*
|
||||
management.endpoint.health.show-details=always
|
||||
|
||||
|
||||
|
||||
spring.datasource.url=jdbc:postgresql://213.183.51.243/familybudget_app
|
||||
spring.datasource.username=familybudget_app
|
||||
spring.datasource.password=FB1q2w3e4r!
|
||||
|
||||
|
||||
# ??????? JDBC
|
||||
spring.datasource.driver-class-name=org.postgresql.Driver
|
||||
|
||||
# ????????? Hibernate
|
||||
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
|
||||
spring.jpa.hibernate.ddl-auto=none
|
||||
|
||||
25
src/main/resources/application-prod.properties
Normal file
25
src/main/resources/application-prod.properties
Normal file
@@ -0,0 +1,25 @@
|
||||
spring.application.name=budger-app
|
||||
|
||||
|
||||
spring.data.mongodb.host=213.226.71.138
|
||||
spring.data.mongodb.port=27017
|
||||
spring.data.mongodb.database=budger-app
|
||||
spring.data.mongodb.username=budger-app
|
||||
spring.data.mongodb.password=BA1q2w3e4r!
|
||||
spring.data.mongodb.authentication-database=admin
|
||||
|
||||
|
||||
spring.datasource.url=jdbc:postgresql://213.183.51.243/familybudget_app
|
||||
spring.datasource.username=familybudget_app
|
||||
spring.datasource.password=FB1q2w3e4r!
|
||||
|
||||
|
||||
logging.level.org.springframework.web=INFO
|
||||
logging.level.org.springframework.data = INFO
|
||||
logging.level.org.springframework.data.mongodb.core.ReactiveMongoTemplate=INFO
|
||||
logging.level.org.springframework.security = INFO
|
||||
logging.level.org.springframework.data.mongodb.code = INFO
|
||||
logging.level.org.springframework.web.reactive=INFO
|
||||
|
||||
|
||||
|
||||
29
src/main/resources/application.properties
Normal file
29
src/main/resources/application.properties
Normal file
@@ -0,0 +1,29 @@
|
||||
spring.application.name=budger-app
|
||||
|
||||
server.port=8082
|
||||
#server.servlet.context-path=/api
|
||||
spring.webflux.base-path=/api
|
||||
|
||||
spring.profiles.active=prod
|
||||
spring.main.web-application-type=reactive
|
||||
|
||||
|
||||
|
||||
logging.level.org.springframework.web=DEBUG
|
||||
logging.level.org.springframework.data = DEBUG
|
||||
logging.level.org.springframework.data.mongodb.core.ReactiveMongoTemplate=DEBUG
|
||||
logging.level.org.springframework.security = DEBUG
|
||||
logging.level.org.springframework.data.mongodb.code = DEBUG
|
||||
logging.level.org.springframework.web.reactive=DEBUG
|
||||
|
||||
server.compression.enabled=true
|
||||
server.compression.mime-types=application/json
|
||||
|
||||
|
||||
# ??????? JDBC
|
||||
spring.datasource.driver-class-name=org.postgresql.Driver
|
||||
|
||||
# ????????? Hibernate
|
||||
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
|
||||
spring.jpa.hibernate.ddl-auto=none
|
||||
|
||||
Reference in New Issue
Block a user