Compare commits

..

2 Commits

Author SHA1 Message Date
0b54384258 Merge pull request 'init' (#1) from sql into main
Reviewed-on: #1
2025-10-31 15:38:34 +03:00
xds
7972ea0fdf init 2025-10-31 15:31:55 +03:00
117 changed files with 3691 additions and 2013 deletions

31
Dockerfile Normal file
View File

@@ -0,0 +1,31 @@
# ---------- build stage ----------
FROM gradle:jdk17-ubi AS build
WORKDIR /app
COPY gradlew gradlew
COPY gradle gradle
COPY build.gradle.kts settings.gradle.kts ./
COPY src src
RUN ./gradlew --no-daemon dependencies
RUN ./gradlew --no-daemon clean bootJar
# ---------- run stage ----------
FROM eclipse-temurin:17.0.16_8-jre AS runtime
WORKDIR /app
# Create non-root user with a higher UID/GID to avoid conflicts
RUN groupadd --system --gid 1001 app && \
useradd --system --gid app --uid 1001 --shell /bin/bash --create-home app
# Создаём директорию и меняем владельца ДО переключения пользователя
RUN mkdir -p /app/static && chown -R app:app /app
USER app
COPY --from=build /app/build/libs/*.jar /app/app.jar
# Настройки JVM (Java 17)
ENV JAVA_TOOL_OPTIONS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0"
EXPOSE 8080
HEALTHCHECK --interval=20s --timeout=3s --retries=3 CMD wget -qO- http://localhost:8080/actuator/health || exit 1
ENTRYPOINT ["java","-jar","/app/app.jar"]

View File

@@ -1,6 +1,7 @@
plugins { plugins {
kotlin("jvm") version "1.9.25" kotlin("jvm") version "1.9.25"
kotlin("plugin.spring") version "1.9.25" kotlin("plugin.spring") version "1.9.25"
kotlin("plugin.jpa") version "1.9.25"
id("org.springframework.boot") version "3.4.0" id("org.springframework.boot") version "3.4.0"
id("io.spring.dependency-management") version "1.1.6" id("io.spring.dependency-management") version "1.1.6"
kotlin("plugin.serialization") version "2.1.0" kotlin("plugin.serialization") version "2.1.0"
@@ -32,20 +33,21 @@ repositories {
dependencies { dependencies {
// Spring // Spring
// implementation("org.springframework.boot:spring-boot-starter-data-mongodb-reactive")
implementation("org.springframework.boot:spring-boot-starter-data-r2dbc")
implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation("org.springframework.boot:spring-boot-starter-cache") implementation("org.springframework.boot:spring-boot-starter-cache")
implementation("org.springframework.boot:spring-boot-starter-security") implementation("org.springframework.boot:spring-boot-starter-security")
implementation ("org.springframework.boot:spring-boot-starter-actuator") implementation ("org.springframework.boot:spring-boot-starter-actuator")
implementation("org.springframework.boot:spring-boot-starter-validation") implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("org.springframework.boot:spring-boot-starter-actuator") implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("org.springdoc:springdoc-openapi-starter-webflux-ui:2.8.13") implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.13")
implementation("org.springframework.boot:spring-boot-starter-web") // MVC
implementation("org.springframework.boot:spring-boot-starter-jdbc")
implementation("org.postgresql:postgresql:42.7.8")
implementation("io.r2dbc:r2dbc-postgresql") // Аудит Spring Data JPA (@CreatedBy/@CreatedDate)
implementation("org.springframework.data:spring-data-commons")
// Миграции // Миграции
implementation("org.flywaydb:flyway-core") implementation("org.flywaydb:flyway-core:11.14.1")
implementation("org.flywaydb:flyway-database-postgresql:11.14.1")
// jackson для jsonb (если маппишь объекты в json) // jackson для jsonb (если маппишь объекты в json)
implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
@@ -56,6 +58,7 @@ dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:1.7.3") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:1.7.3")
implementation("org.jetbrains.kotlin.plugin.jpa:org.jetbrains.kotlin.plugin.jpa.gradle.plugin:1.9.25")
implementation("io.jsonwebtoken:jjwt-api:0.11.5") implementation("io.jsonwebtoken:jjwt-api:0.11.5")
implementation("io.jsonwebtoken:jjwt-impl:0.11.5") implementation("io.jsonwebtoken:jjwt-impl:0.11.5")

View File

@@ -4,25 +4,17 @@ import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.context.properties.ConfigurationPropertiesScan import org.springframework.boot.context.properties.ConfigurationPropertiesScan
import org.springframework.boot.runApplication import org.springframework.boot.runApplication
import org.springframework.cache.annotation.EnableCaching import org.springframework.cache.annotation.EnableCaching
import org.springframework.data.mongodb.config.EnableMongoAuditing
import org.springframework.data.mongodb.config.EnableReactiveMongoAuditing
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories
import org.springframework.scheduling.annotation.EnableAsync
import org.springframework.scheduling.annotation.EnableScheduling import org.springframework.scheduling.annotation.EnableScheduling
import java.util.TimeZone import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
@SpringBootApplication(scanBasePackages = ["space.luminic.finance"]) @SpringBootApplication(scanBasePackages = ["space.luminic.finance"])
@EnableReactiveMongoAuditing(auditorAwareRef = "coroutineAuditorAware")
@EnableCaching @EnableCaching
@EnableAsync
@EnableScheduling @EnableScheduling
//@EnableConfigurationProperties([TelegramBotProperties::class,) @EnableWebSecurity
@ConfigurationPropertiesScan(basePackages = ["space.luminic.finance"]) @ConfigurationPropertiesScan(basePackages = ["space.luminic.finance"])
@EnableMongoRepositories(basePackages = ["space.luminic.finance.repos"])
class Main class Main
fun main(args: Array<String>) { fun main(args: Array<String>) {
TimeZone.setDefault(TimeZone.getTimeZone("Europe/Moscow"))
runApplication<Main>(*args) runApplication<Main>(*args)
} }

View File

@@ -1,75 +0,0 @@
package space.luminic.finance.api
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType
import io.swagger.v3.oas.annotations.security.SecurityScheme
import jakarta.ws.rs.GET
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 space.luminic.finance.dtos.AccountDTO
import space.luminic.finance.dtos.TransactionDTO
import space.luminic.finance.mappers.AccountMapper.toDto
import space.luminic.finance.mappers.TransactionMapper.toDto
import space.luminic.finance.models.Account
import space.luminic.finance.services.AccountService
@RestController
@RequestMapping("/spaces/{spaceId}/accounts")
@SecurityScheme(
name = "bearerAuth",
type = SecuritySchemeType.HTTP,
bearerFormat = "JWT",
scheme = "bearer"
)
class AccountController(
private val accountService: AccountService
) {
@GetMapping
suspend fun getAccounts(@PathVariable spaceId: String): List<AccountDTO> {
return accountService.getAccounts(spaceId).map { it.toDto() }
}
@GetMapping("/{accountId}")
suspend fun getAccount(@PathVariable spaceId: String, @PathVariable accountId: String): AccountDTO {
return accountService.getAccount(accountId, spaceId).toDto()
}
@GetMapping("/{accountId}/transactions")
suspend fun getAccountTransactions(
@PathVariable spaceId: String,
@PathVariable accountId: String
): List<TransactionDTO> {
return accountService.getAccountTransactions(spaceId, accountId).map { it.toDto() }
}
@PostMapping
suspend fun createAccount(
@PathVariable spaceId: String,
@RequestBody accountDTO: AccountDTO.CreateAccountDTO
): AccountDTO {
return accountService.createAccount(spaceId, accountDTO).toDto()
}
@PutMapping("/{accountId}")
suspend fun updateAccount(
@PathVariable spaceId: String,
@PathVariable accountId: String,
@RequestBody accountDTO: AccountDTO.UpdateAccountDTO
): AccountDTO {
return accountService.updateAccount(spaceId, accountDTO).toDto()
}
@DeleteMapping("/{accountId}")
suspend fun deleteAccount(@PathVariable spaceId: String, @PathVariable accountId: String) {
accountService.deleteAccount(accountId, spaceId)
}
}

View File

@@ -1,23 +1,18 @@
package space.luminic.finance.api package space.luminic.finance.api
import kotlinx.coroutines.reactive.awaitSingle
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.security.core.context.ReactiveSecurityContextHolder
import org.springframework.security.core.context.SecurityContextHolder import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.web.bind.annotation.* import org.springframework.web.bind.annotation.*
import space.luminic.finance.dtos.UserDTO.*
import space.luminic.finance.dtos.UserDTO import space.luminic.finance.dtos.UserDTO
import space.luminic.finance.dtos.UserDTO.AuthUserDTO
import space.luminic.finance.dtos.UserDTO.RegisterUserDTO
import space.luminic.finance.mappers.UserMapper.toDto import space.luminic.finance.mappers.UserMapper.toDto
import space.luminic.finance.services.AuthService import space.luminic.finance.services.AuthService
import space.luminic.finance.services.UserService
import kotlin.jvm.javaClass
import kotlin.to
@RestController @RestController
@RequestMapping("/auth") @RequestMapping("/auth")
class AuthController( class AuthController(
private val userService: UserService,
private val authService: AuthService private val authService: AuthService
) { ) {
@@ -31,26 +26,28 @@ class AuthController(
} }
@PostMapping("/login") @PostMapping("/login")
suspend fun login(@RequestBody request: AuthUserDTO): Map<String, String> { fun login(@RequestBody request: AuthUserDTO): Map<String, String> {
val token = authService.login(request.username, request.password) val token = authService.login(request.username.lowercase(), request.password)
return mapOf("token" to token) return mapOf("token" to token)
} }
@PostMapping("/register") @PostMapping("/register")
suspend fun register(@RequestBody request: RegisterUserDTO): UserDTO { fun register(@RequestBody request: RegisterUserDTO): UserDTO {
return authService.register(request.username, request.password, request.firstName).toDto() return authService.register(request.username, request.password, request.firstName).toDto()
} }
@PostMapping("/tgLogin") @PostMapping("/tgLogin")
suspend fun tgLogin(@RequestHeader("X-Tg-Id") tgId: String): Map<String, String> { fun tgLogin(@RequestHeader("X-Tg-Id") tgId: String): Map<String, String> {
val token = authService.tgLogin(tgId) val token = authService.tgLogin(tgId)
return mapOf("token" to token) return mapOf("token" to token)
} }
@GetMapping("/me") @GetMapping("/me")
suspend fun getMe(): UserDTO { fun getMe(): UserDTO {
val securityContext = ReactiveSecurityContextHolder.getContext().awaitSingle() logger.info("Get Me")
return userService.getByUsername(securityContext.authentication.name).toDto() authService.getSecurityUser()
return authService.getSecurityUser().toDto()
} }
} }

View File

@@ -1,78 +0,0 @@
package space.luminic.finance.api
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType
import io.swagger.v3.oas.annotations.security.SecurityScheme
import org.springframework.web.bind.annotation.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.RequestParam
import org.springframework.web.bind.annotation.RestController
import space.luminic.finance.dtos.BudgetDTO
import space.luminic.finance.dtos.TransactionDTO
import space.luminic.finance.mappers.BudgetMapper.toDto
import space.luminic.finance.mappers.BudgetMapper.toShortDto
import space.luminic.finance.mappers.TransactionMapper.toDto
import space.luminic.finance.models.Budget
import space.luminic.finance.services.BudgetService
@RestController
@RequestMapping("/spaces/{spaceId}/budgets")
@SecurityScheme(
name = "bearerAuth",
type = SecuritySchemeType.HTTP,
bearerFormat = "JWT",
scheme = "bearer"
)
class BudgetController(
private val budgetService: BudgetService
) {
@GetMapping
suspend fun getBudgets(
@PathVariable spaceId: String,
@RequestParam(value = "sort", defaultValue = "dateFrom") sortBy: String,
@RequestParam("direction", defaultValue = "DESC") sortDirection: String
): List<BudgetDTO.BudgetShortInfoDTO> {
return budgetService.getBudgets(spaceId, sortBy, sortDirection).map { it.toShortDto() }
}
@GetMapping("/{budgetId}")
suspend fun getBudgetById(@PathVariable spaceId: String, @PathVariable budgetId: String): BudgetDTO {
return budgetService.getBudget(spaceId, budgetId).toDto()
}
@GetMapping("/{budgetId}/transactions")
suspend fun getBudgetTransactions(
@PathVariable spaceId: String,
@PathVariable budgetId: String
): BudgetDTO.BudgetTransactionsDTO {
return budgetService.getBudgetTransactions(spaceId, budgetId).toDto()
}
@PostMapping
suspend fun createBudget(
@PathVariable spaceId: String,
@RequestBody createBudgetDTO: BudgetDTO.CreateBudgetDTO
): BudgetDTO {
return budgetService.createBudget(spaceId, Budget.BudgetType.SPECIAL, createBudgetDTO).toDto()
}
@PutMapping("/{budgetId}")
suspend fun updateBudget(
@PathVariable spaceId: String,
@PathVariable budgetId: String,
@RequestBody updateBudgetDTO: BudgetDTO.UpdateBudgetDTO
): BudgetDTO {
return budgetService.updateBudget(spaceId, updateBudgetDTO).toDto()
}
@DeleteMapping("/{budgetId}")
suspend fun deleteBudget(@PathVariable spaceId: String, @PathVariable budgetId: String) {
budgetService.deleteBudget(spaceId, budgetId)
}
}

View File

@@ -2,16 +2,10 @@ package space.luminic.finance.api
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType import io.swagger.v3.oas.annotations.enums.SecuritySchemeType
import io.swagger.v3.oas.annotations.security.SecurityScheme import io.swagger.v3.oas.annotations.security.SecurityScheme
import org.springframework.web.bind.annotation.DeleteMapping import org.springframework.web.bind.annotation.*
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 space.luminic.finance.dtos.CategoryDTO import space.luminic.finance.dtos.CategoryDTO
import space.luminic.finance.mappers.CategoryMapper.toDto import space.luminic.finance.mappers.CategoryMapper.toDto
import space.luminic.finance.models.Category
import space.luminic.finance.services.CategoryService import space.luminic.finance.services.CategoryService
@RestController @RestController
@@ -23,43 +17,47 @@ import space.luminic.finance.services.CategoryService
scheme = "bearer" scheme = "bearer"
) )
class CategoryController( class CategoryController(
private val categoryService: CategoryService, private val categoryService: CategoryService
service: CategoryService
) { ) {
@GetMapping @GetMapping
suspend fun getCategories(@PathVariable spaceId: String): List<CategoryDTO> { fun getCategories(@PathVariable spaceId: Int): List<CategoryDTO> {
return categoryService.getCategories(spaceId).map { it.toDto() } return categoryService.getCategories(spaceId).map { it.toDto() }
} }
@GetMapping("/{categoryId}") @GetMapping("/{categoryId}")
suspend fun getCategory(@PathVariable spaceId: String, @PathVariable categoryId: String): CategoryDTO { fun getCategory(@PathVariable spaceId: Int, @PathVariable categoryId: Int): CategoryDTO {
return categoryService.getCategory(spaceId, categoryId).toDto() return categoryService.getCategory(spaceId, categoryId).toDto()
} }
@PostMapping @PostMapping
suspend fun createCategory( fun createCategory(
@PathVariable spaceId: String, @PathVariable spaceId: Int,
@RequestBody categoryDTO: CategoryDTO.CreateCategoryDTO @RequestBody categoryDTO: CategoryDTO.CreateCategoryDTO
): CategoryDTO { ): CategoryDTO {
return categoryService.createCategory(spaceId, categoryDTO).toDto() return categoryService.createCategory(spaceId, categoryDTO).toDto()
} }
@PostMapping("/_many")
fun createManyCategory(@PathVariable spaceId: Int, @RequestBody categoryDTOs: List<Category.CategoryEtalon>): List<CategoryDTO> {
return categoryService.createEtalonCategoriesForSpace(spaceId).map { it.toDto() }
}
@PutMapping("/{categoryId}") @PutMapping("/{categoryId}")
suspend fun updateCategory( fun updateCategory(
@PathVariable spaceId: String, @PathVariable spaceId: Int,
@PathVariable categoryId: String, @PathVariable categoryId: Int,
@RequestBody categoryDTO: CategoryDTO.UpdateCategoryDTO @RequestBody categoryDTO: CategoryDTO.UpdateCategoryDTO
): CategoryDTO { ): CategoryDTO {
return categoryService.updateCategory(spaceId, categoryDTO).toDto() return categoryService.updateCategory(spaceId, categoryId, categoryDTO).toDto()
} }
@DeleteMapping("/{categoryId}") @DeleteMapping("/{categoryId}")
suspend fun deleteCategory(@PathVariable spaceId: String, @PathVariable categoryId: String) { fun deleteCategory(@PathVariable spaceId: Int, @PathVariable categoryId: Int) {
categoryService.deleteCategory(spaceId, categoryId) categoryService.deleteCategory(spaceId, categoryId)
} }
} }

View File

@@ -0,0 +1,19 @@
package space.luminic.finance.api
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import space.luminic.finance.dtos.GoalDTO
import space.luminic.finance.mappers.GoalMapper.toDto
import space.luminic.finance.services.GoalService
@RestController
@RequestMapping("/spaces/{spaceId}/goals")
class GoalController(private val goalService: GoalService) {
@GetMapping
fun findAll(@PathVariable spaceId: Int): List<GoalDTO> {
return goalService.findAllBySpaceId(spaceId).map { it.toDto() }
}
}

View File

@@ -0,0 +1,40 @@
package space.luminic.finance.api
import org.springframework.web.bind.annotation.*
import space.luminic.finance.dtos.RecurrentOperationDTO
import space.luminic.finance.mappers.RecurrentOperationMapper.toDTO
import space.luminic.finance.services.RecurrentOperationService
@RestController
@RequestMapping(value = ["/spaces/{spaceId}/recurrents"])
class RecurrentOperationController(private val recurrentOperationService: RecurrentOperationService) {
@GetMapping
fun findAll(@PathVariable spaceId: Int): List<RecurrentOperationDTO> {
return recurrentOperationService.findBySpaceId(spaceId).map { it.toDTO() }
}
@GetMapping("/{operationId}")
fun findById(@PathVariable spaceId: Int, @PathVariable operationId: Int): RecurrentOperationDTO {
return recurrentOperationService.findBySpaceIdAndId(spaceId, operationId).toDTO()
}
@PostMapping
fun createOperation(@PathVariable spaceId: Int, @RequestBody createOperation: RecurrentOperationDTO.CreateRecurrentOperationDTO): Map<String, Int> {
return mapOf("id" to recurrentOperationService.create(spaceId, createOperation))
}
@PutMapping("/{operationId}")
fun updateOperation(@PathVariable spaceId: Int, @PathVariable operationId: Int, @RequestBody operation: RecurrentOperationDTO.UpdateRecurrentOperationDTO): Map<String, Int> {
recurrentOperationService.update(spaceId, operationId, operation)
return mapOf("id" to operationId)
}
@DeleteMapping("/{operationId}")
fun deleteOperation(@PathVariable spaceId: Int, @PathVariable operationId: Int) {
recurrentOperationService.delete(spaceId, operationId)
}
}

View File

@@ -2,14 +2,7 @@ package space.luminic.finance.api
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType import io.swagger.v3.oas.annotations.enums.SecuritySchemeType
import io.swagger.v3.oas.annotations.security.SecurityScheme import io.swagger.v3.oas.annotations.security.SecurityScheme
import org.springframework.web.bind.annotation.DeleteMapping import org.springframework.web.bind.annotation.*
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 space.luminic.finance.dtos.CurrencyDTO import space.luminic.finance.dtos.CurrencyDTO
import space.luminic.finance.mappers.CurrencyMapper.toDto import space.luminic.finance.mappers.CurrencyMapper.toDto
import space.luminic.finance.services.CurrencyService import space.luminic.finance.services.CurrencyService
@@ -27,27 +20,27 @@ class ReferenceController(
) { ) {
@GetMapping("/currencies") @GetMapping("/currencies")
suspend fun getCurrencies(): List<CurrencyDTO> { fun getCurrencies(): List<CurrencyDTO> {
return currencyService.getCurrencies().map { it.toDto() } return currencyService.getCurrencies().map { it.toDto() }
} }
@GetMapping("/currencies/{currencyCode}") @GetMapping("/currencies/{currencyCode}")
suspend fun getCurrency(@PathVariable currencyCode: String): CurrencyDTO { fun getCurrency(@PathVariable currencyCode: String): CurrencyDTO {
return currencyService.getCurrency(currencyCode).toDto() return currencyService.getCurrency(currencyCode).toDto()
} }
@PostMapping("/currencies") @PostMapping("/currencies")
suspend fun createCurrency(@RequestBody currencyDTO: CurrencyDTO): CurrencyDTO { fun createCurrency(@RequestBody currencyDTO: CurrencyDTO): CurrencyDTO {
return currencyService.createCurrency(currencyDTO).toDto() return currencyService.createCurrency(currencyDTO).toDto()
} }
@PutMapping("/currencies/{currencyCode}") @PutMapping("/currencies/{currencyCode}")
suspend fun updateCurrency(@PathVariable currencyCode: String, @RequestBody currencyDTO: CurrencyDTO): CurrencyDTO { fun updateCurrency(@PathVariable currencyCode: String, @RequestBody currencyDTO: CurrencyDTO): CurrencyDTO {
return currencyService.updateCurrency(currencyDTO).toDto() return currencyService.updateCurrency(currencyDTO).toDto()
} }
@DeleteMapping("/currencies/{currencyCode}") @DeleteMapping("/currencies/{currencyCode}")
suspend fun deleteCurrency(@PathVariable currencyCode: String) { fun deleteCurrency(@PathVariable currencyCode: String) {
currencyService.deleteCurrency(currencyCode) currencyService.deleteCurrency(currencyCode)
} }
} }

View File

@@ -2,17 +2,9 @@ package space.luminic.finance.api
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType import io.swagger.v3.oas.annotations.enums.SecuritySchemeType
import io.swagger.v3.oas.annotations.security.SecurityScheme import io.swagger.v3.oas.annotations.security.SecurityScheme
import org.springframework.web.bind.annotation.DeleteMapping import org.springframework.web.bind.annotation.*
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 space.luminic.finance.dtos.SpaceDTO import space.luminic.finance.dtos.SpaceDTO
import space.luminic.finance.mappers.SpaceMapper.toDto import space.luminic.finance.mappers.SpaceMapper.toDto
import space.luminic.finance.models.Space
import space.luminic.finance.services.SpaceService import space.luminic.finance.services.SpaceService
@RestController @RestController
@@ -29,27 +21,27 @@ class SpaceController(
@GetMapping @GetMapping
suspend fun getSpaces(): List<SpaceDTO> { fun getSpaces(): List<SpaceDTO> {
return spaceService.getSpaces().map { it.toDto() } return spaceService.getSpaces().map { it.toDto() }
} }
@GetMapping("/{spaceId}") @GetMapping("/{spaceId}")
suspend fun getSpace(@PathVariable spaceId: String): SpaceDTO { fun getSpace(@PathVariable spaceId: Int): SpaceDTO {
return spaceService.getSpace(spaceId).toDto() return spaceService.getSpace(spaceId).toDto()
} }
@PostMapping @PostMapping
suspend fun createSpace(@RequestBody space: SpaceDTO.CreateSpaceDTO): SpaceDTO { fun createSpace(@RequestBody space: SpaceDTO.CreateSpaceDTO): Map<String, Int> {
return spaceService.createSpace(space).toDto() return mapOf("id" to spaceService.createSpace(space) )
} }
@PutMapping("/{spaceId}") @PutMapping("/{spaceId}")
suspend fun updateSpace(@PathVariable spaceId: String, @RequestBody space: SpaceDTO.UpdateSpaceDTO): SpaceDTO { fun updateSpace(@PathVariable spaceId: Int, @RequestBody space: SpaceDTO.UpdateSpaceDTO): Map<String, Int> {
return spaceService.updateSpace(spaceId, space).toDto() return mapOf("id" to spaceService.updateSpace(spaceId, space) )
} }
@DeleteMapping("/{spaceId}") @DeleteMapping("/{spaceId}")
suspend fun deleteSpace(@PathVariable spaceId: String) { fun deleteSpace(@PathVariable spaceId: Int) {
spaceService.deleteSpace(spaceId) spaceService.deleteSpace(spaceId)
} }
} }

View File

@@ -2,14 +2,7 @@ package space.luminic.finance.api
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType import io.swagger.v3.oas.annotations.enums.SecuritySchemeType
import io.swagger.v3.oas.annotations.security.SecurityScheme import io.swagger.v3.oas.annotations.security.SecurityScheme
import org.springframework.web.bind.annotation.DeleteMapping import org.springframework.web.bind.annotation.*
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 space.luminic.finance.dtos.TransactionDTO import space.luminic.finance.dtos.TransactionDTO
import space.luminic.finance.mappers.TransactionMapper.toDto import space.luminic.finance.mappers.TransactionMapper.toDto
import space.luminic.finance.services.TransactionService import space.luminic.finance.services.TransactionService
@@ -25,34 +18,32 @@ import space.luminic.finance.services.TransactionService
) )
class TransactionController ( class TransactionController (
private val transactionService: TransactionService, private val transactionService: TransactionService,
service: TransactionService,
transactionService1: TransactionService,
){ ){
@GetMapping @GetMapping
suspend fun getTransactions(@PathVariable spaceId: String) : List<TransactionDTO>{ fun getTransactions(@PathVariable spaceId: Int) : List<TransactionDTO>{
return transactionService.getTransactions(spaceId, TransactionService.TransactionsFilter(),"date", "DESC").map { it.toDto() } return transactionService.getTransactions(spaceId, TransactionService.TransactionsFilter(),"date", "DESC").map { it.toDto() }
} }
@GetMapping("/{transactionId}") @GetMapping("/{transactionId}")
suspend fun getTransaction(@PathVariable spaceId: String, @PathVariable transactionId: String): TransactionDTO { fun getTransaction(@PathVariable spaceId: Int, @PathVariable transactionId: Int): TransactionDTO {
return transactionService.getTransaction(spaceId, transactionId).toDto() return transactionService.getTransaction(spaceId, transactionId).toDto()
} }
@PostMapping @PostMapping
suspend fun createTransaction(@PathVariable spaceId: String, @RequestBody transactionDTO: TransactionDTO.CreateTransactionDTO): TransactionDTO { fun createTransaction(@PathVariable spaceId: Int, @RequestBody transactionDTO: TransactionDTO.CreateTransactionDTO): Map<String, Int> {
return transactionService.createTransaction(spaceId, transactionDTO).toDto() return mapOf("id" to transactionService.createTransaction(spaceId, transactionDTO))
} }
@PutMapping("/{transactionId}") @PutMapping("/{transactionId}")
suspend fun updateTransaction(@PathVariable spaceId: String, @PathVariable transactionId: String, @RequestBody transactionDTO: TransactionDTO.UpdateTransactionDTO): TransactionDTO { fun updateTransaction(@PathVariable spaceId: Int, @PathVariable transactionId: Int, @RequestBody transactionDTO: TransactionDTO.UpdateTransactionDTO): Map<String, Int> {
return transactionService.updateTransaction(spaceId, transactionDTO).toDto() return mapOf("id" to transactionService.updateTransaction(spaceId, transactionId, transactionDTO))
} }
@DeleteMapping("/{transactionId}") @DeleteMapping("/{transactionId}")
suspend fun deleteTransaction(@PathVariable spaceId: String, @PathVariable transactionId: String) { fun deleteTransaction(@PathVariable spaceId: Int, @PathVariable transactionId: Int) {
transactionService.deleteTransaction(spaceId, transactionId) transactionService.deleteTransaction(spaceId, transactionId)
} }

View File

@@ -1,109 +1,112 @@
package space.luminic.finance.api.exceptionHandlers package space.luminic.finance.api.exceptionHandlers
import jakarta.servlet.http.HttpServletRequest
import org.springframework.http.HttpStatus import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
import org.springframework.http.server.reactive.ServerHttpRequest
import org.springframework.web.bind.annotation.ExceptionHandler import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.RestControllerAdvice import org.springframework.web.bind.annotation.RestControllerAdvice
import org.springframework.web.server.ServerWebExchange
import reactor.core.publisher.Mono
import space.luminic.finance.configs.AuthException import space.luminic.finance.configs.AuthException
import space.luminic.finance.models.NotFoundException import space.luminic.finance.models.NotFoundException
@RestControllerAdvice @RestControllerAdvice
class GlobalExceptionHandler { class GlobalExceptionHandler {
data class ErrorResponse(
val timestamp: Long = System.currentTimeMillis(),
val status: Int,
val error: String,
val message: String?,
val path: String,
)
// Изменённый параметр request
fun constructErrorBody( fun constructErrorBody(
e: Exception, e: Exception,
message: String, message: String,
status: HttpStatus, status: HttpStatus,
request: ServerHttpRequest request: HttpServletRequest // <--- Изменённый тип
): Map<String, Any?> { ): ErrorResponse {
val errorResponse = mapOf( return ErrorResponse(
"timestamp" to System.currentTimeMillis(), status = status.value(),
"status" to status.value(), error = message,
"error" to message, message = e.message,
"message" to e.message, path = request.requestURI // <--- Получаем путь через HttpServletRequest
"path" to request.path.value()
) )
return errorResponse
} }
@ExceptionHandler(AuthException::class) @ExceptionHandler(AuthException::class)
// Изменённый параметр exchange
fun handleAuthenticationException( fun handleAuthenticationException(
ex: AuthException, ex: AuthException,
exchange: ServerWebExchange request: HttpServletRequest // <--- Изменённый тип
): Mono<ResponseEntity<Map<String, Any?>>?> { ): ResponseEntity<ErrorResponse>? {
ex.printStackTrace() ex.printStackTrace()
return Mono.just( return ResponseEntity(
ResponseEntity( constructErrorBody(
constructErrorBody( ex,
ex, ex.message.toString(),
ex.message.toString(), HttpStatus.UNAUTHORIZED,
HttpStatus.UNAUTHORIZED, request // <--- Передаём HttpServletRequest
exchange.request ),
), HttpStatus.UNAUTHORIZED HttpStatus.UNAUTHORIZED
)
) )
} }
@ExceptionHandler(NotFoundException::class) @ExceptionHandler(NotFoundException::class)
// Изменённый параметр exchange
fun handleNotFoundException( fun handleNotFoundException(
e: NotFoundException, e: NotFoundException,
exchange: ServerWebExchange request: HttpServletRequest // <--- Изменённый тип
): Mono<ResponseEntity<Map<String, Any?>>?> { ): ResponseEntity<ErrorResponse>? {
e.printStackTrace() e.printStackTrace()
return Mono.just( return ResponseEntity(
ResponseEntity( constructErrorBody(
constructErrorBody( e,
e, e.message.toString(),
e.message.toString(), HttpStatus.NOT_FOUND,
HttpStatus.NOT_FOUND, request // <--- Передаём HttpServletRequest
exchange.request ),
), HttpStatus.NOT_FOUND HttpStatus.NOT_FOUND
)
) )
} }
@ExceptionHandler(IllegalArgumentException::class) @ExceptionHandler(IllegalArgumentException::class)
// Изменённый параметр exchange
fun handleIllegalArgumentException( fun handleIllegalArgumentException(
e: IllegalArgumentException, e: IllegalArgumentException,
exchange: ServerWebExchange request: HttpServletRequest // <--- Изменённый тип
): Mono<ResponseEntity<Map<String, Any?>>?> { ): ResponseEntity<ErrorResponse>? {
e.printStackTrace() e.printStackTrace()
return Mono.just( return ResponseEntity(
ResponseEntity( constructErrorBody(
constructErrorBody( e,
e, e.message.toString(),
e.message.toString(), HttpStatus.BAD_REQUEST,
HttpStatus.BAD_REQUEST, request // <--- Передаём HttpServletRequest
exchange.request ),
), HttpStatus.BAD_REQUEST HttpStatus.BAD_REQUEST
)
) )
} }
@ExceptionHandler(Exception::class) @ExceptionHandler(Exception::class)
// Изменённый параметр exchange
fun handleGenericException( fun handleGenericException(
e: Exception, e: Exception,
exchange: ServerWebExchange request: HttpServletRequest // <--- Изменённый тип
): Mono<out ResponseEntity<out Map<String, Any?>>?> { ): ResponseEntity<ErrorResponse>? {
e.printStackTrace() e.printStackTrace()
return ResponseEntity(
return Mono.just( constructErrorBody(
ResponseEntity( e,
constructErrorBody( e.message.toString(),
e, HttpStatus.INTERNAL_SERVER_ERROR,
e.message.toString(), request // <--- Передаём HttpServletRequest
HttpStatus.INTERNAL_SERVER_ERROR, ),
exchange.request HttpStatus.INTERNAL_SERVER_ERROR
), HttpStatus.INTERNAL_SERVER_ERROR
)
) )
} }
} }

View File

@@ -1,49 +1,37 @@
package space.luminic.finance.api.exceptionHandlers package space.luminic.finance.api.exceptionHandlers
import org.springframework.boot.autoconfigure.web.WebProperties //
import org.springframework.boot.autoconfigure.web.reactive.error.AbstractErrorWebExceptionHandler //@Component
import org.springframework.boot.web.error.ErrorAttributeOptions //@Order(-2)
import org.springframework.boot.web.reactive.error.ErrorAttributes //class GlobalErrorWebExceptionHandler(
import org.springframework.context.ApplicationContext // errorAttributes: ErrorAttributes,
import org.springframework.core.annotation.Order // applicationContext: ApplicationContext,
import org.springframework.http.MediaType // serverCodecConfigurer: ServerCodecConfigurer
import org.springframework.http.codec.ServerCodecConfigurer //) : AbstractErrorWebExceptionHandler(
import org.springframework.stereotype.Component // errorAttributes,
import org.springframework.web.reactive.function.BodyInserters // WebProperties.Resources(),
import org.springframework.web.reactive.function.server.* // applicationContext
import reactor.core.publisher.Mono //) {
//
@Component // init {
@Order(-2) // super.setMessageWriters(serverCodecConfigurer.writers)
class GlobalErrorWebExceptionHandler( // super.setMessageReaders(serverCodecConfigurer.readers)
errorAttributes: ErrorAttributes, // }
applicationContext: ApplicationContext, //
serverCodecConfigurer: ServerCodecConfigurer // override fun getRoutingFunction(errorAttributes: ErrorAttributes): RouterFunction<ServerResponse> {
) : AbstractErrorWebExceptionHandler( // return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse)
errorAttributes, // }
WebProperties.Resources(), //
applicationContext // private fun renderErrorResponse(request: ServerRequest): Mono<ServerResponse> {
) { // val errorAttributesMap = getErrorAttributes(
// request,
init { // ErrorAttributeOptions.of(
super.setMessageWriters(serverCodecConfigurer.writers) // ErrorAttributeOptions.Include.MESSAGE
super.setMessageReaders(serverCodecConfigurer.readers) // )
} // )
// return ServerResponse.status(401)
override fun getRoutingFunction(errorAttributes: ErrorAttributes): RouterFunction<ServerResponse> { // .contentType(MediaType.APPLICATION_JSON)
return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse) // .body(BodyInserters.fromValue(errorAttributesMap))
} // }
//
private fun renderErrorResponse(request: ServerRequest): Mono<ServerResponse> { //
val errorAttributesMap = getErrorAttributes( //}
request,
ErrorAttributeOptions.of(
ErrorAttributeOptions.Include.MESSAGE
)
)
return ServerResponse.status(401)
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(errorAttributesMap))
}
}

View File

@@ -1,56 +1,80 @@
package space.luminic.finance.configs package space.luminic.finance.configs
import kotlinx.coroutines.reactor.mono import jakarta.servlet.FilterChain
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.http.HttpHeaders import org.springframework.http.HttpHeaders
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.authority.SimpleGrantedAuthority import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.core.context.ReactiveSecurityContextHolder import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.core.context.SecurityContextImpl import org.springframework.security.web.util.matcher.AntPathRequestMatcher
import org.springframework.security.web.server.context.SecurityContextServerWebExchangeWebFilter
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
import org.springframework.web.server.ServerWebExchange import org.springframework.web.filter.OncePerRequestFilter
import org.springframework.web.server.WebFilterChain
import reactor.core.publisher.Mono
import space.luminic.finance.services.AuthService import space.luminic.finance.services.AuthService
@Component @Component
class BearerTokenFilter(private val authService: AuthService) : SecurityContextServerWebExchangeWebFilter() { class BearerTokenFilter(
// private val logger = LoggerFactory.getLogger(BearerTokenFilter::class.java) private val authService: AuthService
) : OncePerRequestFilter() {
// Публичные пути — как в твоём WebFlux-фильтре
private val publicMatchers = listOf(
AntPathRequestMatcher("/auth/login", "POST"),
AntPathRequestMatcher("/auth/register", "POST"),
AntPathRequestMatcher("/auth/tgLogin", "POST"),
AntPathRequestMatcher("/actuator/**"),
AntPathRequestMatcher("/static/**"),
AntPathRequestMatcher("/wishlistexternal/**"),
AntPathRequestMatcher("/swagger-ui/**"),
AntPathRequestMatcher("/v3/api-docs/**"),
)
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> { override fun shouldNotFilter(request: HttpServletRequest): Boolean {
val token = exchange.request.headers.getFirst(HttpHeaders.AUTHORIZATION)?.removePrefix("Bearer ") return publicMatchers.any { it.matches(request) }
}
if (exchange.request.path.value() in listOf( override fun doFilterInternal(
"/api/auth/login", request: HttpServletRequest,
"/api/auth/register", response: HttpServletResponse,
"/api/auth/tgLogin" chain: FilterChain
) || exchange.request.path.value().startsWith("/api/actuator") || exchange.request.path.value() ) {
.startsWith("/api/static/") val raw = request.getHeader(HttpHeaders.AUTHORIZATION)
|| exchange.request.path.value() val token = raw?.takeIf { it.startsWith("Bearer ", ignoreCase = true) }?.substring(7)
.startsWith("/api/wishlistexternal/")
|| exchange.request.path.value().startsWith("/api/swagger-ui") || exchange.request.path.value().startsWith("/api/v3/api-docs") if (token.isNullOrBlank()) {
) { unauthorized(response, "Authorization token is missing")
return chain.filter(exchange) return
} }
return if (token != null) { try {
mono { // AuthService.isTokenValid у тебя suspend — в Servlet-фильтре вызываем через runBlocking
val userDetails = authService.isTokenValid(token) // suspend вызов
val authorities = userDetails.roles.map { SimpleGrantedAuthority(it) } val userDetails = authService.isTokenValid(token)
val securityContext = SecurityContextImpl(
UsernamePasswordAuthenticationToken(userDetails.username, null, authorities) val authorities = userDetails.roles.map { SimpleGrantedAuthority(it) }
) val auth = UsernamePasswordAuthenticationToken(userDetails.id, null, authorities)
securityContext
}.flatMap { securityContext -> SecurityContextHolder.getContext().authentication = auth
chain.filter(exchange) chain.doFilter(request, response)
.contextWrite(ReactiveSecurityContextHolder.withSecurityContext(Mono.just(securityContext))) } catch (ex: Exception) {
} ex.printStackTrace()
} else { unauthorized(response, ex.message ?: "Invalid token")
Mono.error(AuthException("Authorization token is missing")) } finally {
// важно не «протекать» аутентификацией за пределы запроса
SecurityContextHolder.clearContext()
} }
} }
private fun unauthorized(response: HttpServletResponse, message: String) {
response.status = HttpServletResponse.SC_UNAUTHORIZED
response.contentType = "application/json"
response.writer.use { out ->
out.write("""{"error":"unauthorized","message":${message.quoteJson()}}""")
}
}
private fun String.quoteJson(): String =
"\"" + this.replace("\\", "\\\\").replace("\"", "\\\"") + "\""
} }
open class AuthException(msg: String) : RuntimeException(msg) open class AuthException(msg: String) : RuntimeException(msg)

View File

@@ -1,9 +1,5 @@
package space.luminic.finance.configs package space.luminic.finance.configs
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
//class CommonConfig { //class CommonConfig {
// @Bean // @Bean
@@ -12,8 +8,8 @@ import org.springframework.context.annotation.Configuration
// } // }
//} //}
//
@ConfigurationProperties(prefix = "nlp") //@ConfigurationProperties(prefix = "nlp")
data class NLPConfig( //data class NLPConfig(
val address: String, // val address: String,
) //)

View File

@@ -0,0 +1,31 @@
package space.luminic.finance.configs
import org.springframework.context.annotation.Configuration
import org.springframework.data.domain.AuditorAware
import org.springframework.stereotype.Component
import space.luminic.finance.models.User
import space.luminic.finance.services.AuthService
import java.util.*
@Configuration
class JpaConfig // Класс для включения аудита
@Component // Помечаем как Spring-компонент
class AuditorAwareImpl(
private val authService: AuthService,
) : AuditorAware<User> { // Убедитесь, что тип возвращаемого ID совпадает (Int)
override fun getCurrentAuditor(): Optional<User> {
return try {
val currentUser = authService.getSecurityUser()
// Проверяем, что пользователь не null перед доступом к id
Optional.of(currentUser) // currentUser.id должен быть Int
} catch (ex: Exception) {
// Логирование (по желанию)
// logger.debug("Authentication not found or invalid, auditor is null", ex)
// Если возникла ошибка при получении пользователя (например, не аутентифицирован), возвращаем Optional.empty()
Optional.empty()
}
}
}

View File

@@ -1,60 +1,62 @@
package space.luminic.finance.configs package space.luminic.finance.configs
import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Configuration
import org.springframework.http.HttpMethod import org.springframework.http.HttpMethod
import org.springframework.security.config.web.server.SecurityWebFiltersOrder import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.web.server.ServerHttpSecurity import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.http.SessionCreationPolicy
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.security.web.server.SecurityWebFilterChain import org.springframework.security.web.SecurityFilterChain
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
import org.springframework.web.cors.CorsConfiguration
import org.springframework.web.cors.CorsConfigurationSource
import org.springframework.web.cors.UrlBasedCorsConfigurationSource
@Configuration @Configuration
@EnableWebSecurity
class SecurityConfig( class SecurityConfig(
private val bearerTokenFilter: BearerTokenFilter, // см. примечание ниже
) { ) {
@Bean
fun securityWebFilterChain(
http: ServerHttpSecurity,
bearerTokenFilter: BearerTokenFilter
): SecurityWebFilterChain {
return http
.csrf { it.disable() }
.cors { it.configurationSource(corsConfigurationSource()) }
.logout { it.disable() } @Bean
.authorizeExchange { fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
it.pathMatchers(HttpMethod.POST, "/auth/login", "/auth/register", "/auth/tgLogin").permitAll() http
it.pathMatchers("/actuator/**", "/static/**").permitAll() .csrf {
it.pathMatchers("/wishlistexternal/**").permitAll() it.disable()
it.pathMatchers("/swagger-ui/**", "/swagger-ui.html", "/v3/api-docs/**").permitAll()
it.anyExchange().authenticated()
} }
.addFilterAt( .cors { it.configurationSource(corsConfigurationSource()) }
bearerTokenFilter, .sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) }
SecurityWebFiltersOrder.AUTHENTICATION .logout { it.disable() }
) // BearerTokenFilter только для authenticated
.build()
}
.authorizeHttpRequests {
it.requestMatchers(HttpMethod.POST, "/auth/login", "/auth/register", "/auth/tgLogin").permitAll()
it.requestMatchers("/actuator/**", "/static/**").permitAll()
it.requestMatchers("/wishlistexternal/**").permitAll()
it.requestMatchers("/swagger-ui/**", "/swagger-ui.html", "/v3/api-docs/**").permitAll()
it.anyRequest().authenticated()
}
@Bean // ваш JWT-фильтр до стандартной аутентификации
fun passwordEncoder(): PasswordEncoder { .addFilterBefore(bearerTokenFilter, UsernamePasswordAuthenticationFilter::class.java)
return BCryptPasswordEncoder()
return http.build()
} }
@Bean @Bean
fun corsConfigurationSource(): org.springframework.web.cors.reactive.CorsConfigurationSource { fun passwordEncoder(): PasswordEncoder = BCryptPasswordEncoder()
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() @Bean
source.registerCorsConfiguration("/**", corsConfig) fun corsConfigurationSource(): CorsConfigurationSource {
val cors = CorsConfiguration().apply {
allowedOrigins = listOf("https://luminic.space", "http://localhost:5173")
allowedMethods = listOf("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH")
allowedHeaders = listOf("*")
allowCredentials = true
}
val source = UrlBasedCorsConfigurationSource()
source.registerCorsConfiguration("/**", cors)
return source return source
} }
} }

View File

@@ -1,37 +0,0 @@
package space.luminic.finance.dtos
import space.luminic.finance.models.Account.AccountType
import space.luminic.finance.models.Currency
import java.math.BigDecimal
import java.time.Instant
data class AccountDTO(
val id: String,
val name: String,
val type: AccountType = AccountType.COLLECTING,
val currencyCode: String,
val currency: CurrencyDTO? = null,
var balance: BigDecimal = BigDecimal.ZERO,
val goal: GoalDTO? = null,
val createdBy: String? = null,
val createdAt: Instant? = null,
val updatedBy: String? = null,
val updatedAt: Instant? = null,
){
data class CreateAccountDTO(
val name: String,
val type: AccountType,
val currencyCode: String,
val amount: BigDecimal,
val goalId: String? = null,
)
data class UpdateAccountDTO(
val id: String,
val name: String,
val type: AccountType,
val currencyCode: String,
val amount: BigDecimal,
val goalId: String? = null,
)
}

View File

@@ -1,69 +0,0 @@
package space.luminic.finance.dtos
import space.luminic.finance.models.Budget
import space.luminic.finance.models.Category
import space.luminic.finance.models.Transaction
import space.luminic.finance.models.Transaction.TransactionKind
import space.luminic.finance.models.Transaction.TransactionType
import space.luminic.finance.models.User
import java.math.BigDecimal
import java.time.Instant
import java.time.LocalDate
data class BudgetDTO(
val id: String?,
val type: Budget.BudgetType,
var name: String,
var description: String? = null,
var dateFrom: LocalDate,
var dateTo: LocalDate,
val isActive: Boolean,
val createdBy: UserDTO?,
val createdAt: Instant,
var updatedBy: UserDTO?,
var updatedAt: Instant,
) {
data class BudgetShortInfoDTO(
val id: String,
val type: Budget.BudgetType,
val name: String,
val description: String?,
val dateFrom: LocalDate,
val dateTo: LocalDate,
val createdBy: String
)
data class BudgetCategoryDto(
val category: CategoryDTO,
val limit: BigDecimal,
val totalPlannedAmount: BigDecimal,
val totalSpendingAmount: BigDecimal
)
data class CreateBudgetDTO(
val name: String,
val description: String? = null,
val dateFrom: LocalDate,
val dateTo: LocalDate
)
data class UpdateBudgetDTO(
val id: String,
val name: String? = null,
val description: String? = null,
val dateFrom: LocalDate? = null,
val dateTo: LocalDate? = null,
)
data class BudgetTransactionsDTO(
val categories: List<BudgetCategoryDto>,
val transactions: List<TransactionDTO>,
val plannedIncomeTransactions: List<TransactionDTO>,
val plannedExpenseTransactions: List<TransactionDTO>,
val instantIncomeTransactions: List<TransactionDTO>,
val instantExpenseTransactions: List<TransactionDTO>
)
}

View File

@@ -4,26 +4,29 @@ import space.luminic.finance.models.Category.CategoryType
import java.time.Instant import java.time.Instant
data class CategoryDTO( data class CategoryDTO(
val id: String, val id: Int,
val type: CategoryType, val type: CategoryType,
val name: String, val name: String,
val description: String,
val icon: String, val icon: String,
val createdBy: UserDTO? = null, val createdBy: UserDTO? = null,
val createdAt: Instant, val createdAt: Instant? = null,
val updatedBy: UserDTO? = null, val updatedBy: UserDTO? = null,
val updatedAt: Instant, val updatedAt: Instant? = null,
) { ) {
data class CreateCategoryDTO( data class CreateCategoryDTO(
val name: String,
val type: CategoryType, val type: CategoryType,
val name: String,
val description: String,
val icon: String, val icon: String,
) )
data class UpdateCategoryDTO( data class UpdateCategoryDTO(
val id: String,
val name: String,
val type: CategoryType, val type: CategoryType,
val name: String,
val description: String,
val icon: String, val icon: String,
) )
} }

View File

@@ -1,20 +1,39 @@
package space.luminic.finance.dtos package space.luminic.finance.dtos
import space.luminic.finance.models.Goal
import space.luminic.finance.models.Goal.GoalType import space.luminic.finance.models.Goal.GoalType
import space.luminic.finance.models.Transaction
import java.math.BigDecimal import java.math.BigDecimal
import java.time.Instant import java.time.Instant
import java.time.LocalDate import java.time.LocalDate
data class GoalDTO( data class GoalDTO(
val id: String, val id: Int,
val type: GoalType, val type: GoalType,
val name: String, val name: String,
val description: String? = null, val description: String? = null,
val amount: BigDecimal, val amount: BigDecimal,
val date: LocalDate, val date: LocalDate,
val components: List<Goal.GoalComponent>,
val transactions: List<Transaction>,
val createdBy: UserDTO, val createdBy: UserDTO,
val createdAt: Instant, val createdAt: Instant,
val updatedBy: UserDTO, val updatedBy: UserDTO? = null,
val updatedAt: Instant, val updatedAt: Instant? = null,
) { ) {
data class CreateGoalDTO(
val type: GoalType,
val name: String,
val description: String?,
val amount: BigDecimal,
val date: LocalDate
)
data class UpdateGoalDTO(
val type: GoalType,
val name: String,
val description: String?,
val amount: BigDecimal,
val date: LocalDate
)
} }

View File

@@ -0,0 +1,29 @@
package space.luminic.finance.dtos
import java.math.BigDecimal
import java.time.Instant
data class RecurrentOperationDTO (
val id: Int,
val category: CategoryDTO,
val name: String,
val amount: BigDecimal,
val date: Int,
val createdBy: UserDTO? = null,
val createdAt: Instant
) {
data class CreateRecurrentOperationDTO (
val categoryId: Int,
val name: String,
val amount: BigDecimal,
val date: Int,
)
data class UpdateRecurrentOperationDTO (
val categoryId: Int,
val name: String,
val amount: BigDecimal,
val date: Int,
)
}

View File

@@ -4,15 +4,29 @@ import space.luminic.finance.models.User
import java.time.Instant import java.time.Instant
data class SpaceDTO( data class SpaceDTO(
val id: String? = null, val id: Int? = null,
val name: String, val name: String,
val owner: UserDTO, val owner: UserDTO,
val participants: List<UserDTO> = emptyList(), val participants: Set<UserDTO> = emptySet(),
val createdBy: UserDTO? = null, val createdBy: UserDTO? = null,
val createdAt: Instant, val createdAt: Instant,
var updatedBy: UserDTO? = null, var updatedBy: UserDTO? = null,
var updatedAt: Instant, var updatedAt: Instant,
) { ) {
data class SpaceShortInfoDTO(
val id: Int,
val name: String,
val isOwner: Boolean,
val owner: User,
val participant: User,
val createdAt: Instant,
val updatedAt: Instant? = null,
val createdBy: User,
val updatedBy: User? = null,
)
data class CreateSpaceDTO( data class CreateSpaceDTO(
val name: String, val name: String,
val createBasicCategories: Boolean = true, val createBasicCategories: Boolean = true,

View File

@@ -0,0 +1,6 @@
package space.luminic.finance.dtos
data class SubscriptionDTO (
val endpoint: String,
val keys: Map<String, String>
)

View File

@@ -1,52 +1,43 @@
package space.luminic.finance.dtos package space.luminic.finance.dtos
import space.luminic.finance.models.Account
import space.luminic.finance.models.Category
import space.luminic.finance.models.Transaction.TransactionKind import space.luminic.finance.models.Transaction.TransactionKind
import space.luminic.finance.models.Transaction.TransactionType import space.luminic.finance.models.Transaction.TransactionType
import space.luminic.finance.models.User
import java.math.BigDecimal import java.math.BigDecimal
import java.time.Instant import java.time.LocalDate
data class TransactionDTO( data class TransactionDTO(
val id: String? = null, val id: Int? = null,
var parentId: String? = null, var parentId: Int? = null,
val type: TransactionType = TransactionType.EXPENSE, val type: TransactionType = TransactionType.EXPENSE,
val kind: TransactionKind = TransactionKind.INSTANT, val kind: TransactionKind = TransactionKind.INSTANT,
val categoryId: String, val category: CategoryDTO,
val category: CategoryDTO? = null,
val comment: String, val comment: String,
val amount: BigDecimal, val amount: BigDecimal,
val fees: BigDecimal = BigDecimal.ZERO, val fees: BigDecimal = BigDecimal.ZERO,
val fromAccount: AccountDTO? = null, val date: LocalDate,
val toAccount: AccountDTO? = null, val isDone: Boolean,
val date: Instant, val createdBy: UserDTO,
val createdBy: String? = null, val updatedBy: UserDTO? = null,
val updatedBy: String? = null,
) { ) {
data class CreateTransactionDTO( data class CreateTransactionDTO(
val type: TransactionType = TransactionType.EXPENSE, val type: TransactionType = TransactionType.EXPENSE,
val kind: TransactionKind = TransactionKind.INSTANT, val kind: TransactionKind = TransactionKind.INSTANT,
val categoryId: String, val categoryId: Int,
val comment: String, val comment: String,
val amount: BigDecimal, val amount: BigDecimal,
val fees: BigDecimal = BigDecimal.ZERO, val fees: BigDecimal = BigDecimal.ZERO,
val fromAccountId: String, val date: LocalDate,
val toAccountId: String? = null,
val date: Instant
) )
data class UpdateTransactionDTO( data class UpdateTransactionDTO(
val id: String,
val type: TransactionType = TransactionType.EXPENSE, val type: TransactionType = TransactionType.EXPENSE,
val kind: TransactionKind = TransactionKind.INSTANT, val kind: TransactionKind = TransactionKind.INSTANT,
val category: String, val categoryId: Int,
val comment: String, val comment: String,
val amount: BigDecimal, val amount: BigDecimal,
val fees: BigDecimal = BigDecimal.ZERO, val fees: BigDecimal = BigDecimal.ZERO,
val fromAccountId: String, val isDone: Boolean,
val toAccountId: String? = null, val date: LocalDate
val date: Instant
) )
} }

View File

@@ -1,9 +1,7 @@
package space.luminic.finance.dtos package space.luminic.finance.dtos
import space.luminic.finance.models.User
data class UserDTO ( data class UserDTO (
var id: String, var id: Int,
val username: String, val username: String,
var firstName: String, var firstName: String,
var tgId: String? = null, var tgId: String? = null,

View File

@@ -1,26 +0,0 @@
package space.luminic.finance.mappers
import space.luminic.finance.dtos.AccountDTO
import space.luminic.finance.mappers.CurrencyMapper.toDto
import space.luminic.finance.mappers.GoalMapper.toDto
import space.luminic.finance.models.Account
object AccountMapper {
fun Account.toDto(): AccountDTO {
return AccountDTO(
id = this.id ?: throw IllegalStateException("Account ID must not be null"),
name = this.name,
type = this.type,
currencyCode = this.currencyCode,
currency = this.currency?.toDto(),
balance = this.amount,
goal = this.goal?.toDto(),
createdBy = this.createdBy?.username,
createdAt = this.createdAt,
updatedBy = this.updatedBy?.username,
updatedAt = this.updatedAt,
)
}
}

View File

@@ -1,88 +0,0 @@
package space.luminic.finance.mappers
import space.luminic.finance.dtos.BudgetDTO
import space.luminic.finance.dtos.TransactionDTO
import space.luminic.finance.mappers.CategoryMapper.toDto
import space.luminic.finance.mappers.TransactionMapper.toDto
import space.luminic.finance.mappers.UserMapper.toDto
import space.luminic.finance.models.Budget
import space.luminic.finance.models.Transaction
import space.luminic.finance.models.Transaction.TransactionKind
import space.luminic.finance.models.Transaction.TransactionType
import java.time.LocalDate
object BudgetMapper {
fun Budget.toDto(): BudgetDTO {
val isActive = this.dateTo.isBefore(LocalDate.now())
return BudgetDTO(
id = this.id,
type = this.type,
name = this.name,
description = this.description,
dateFrom = this.dateFrom,
dateTo = this.dateTo,
isActive = isActive,
createdBy = this.createdBy?.toDto(),
createdAt = this.createdAt?: throw IllegalArgumentException("created at is null"),
updatedBy = this.updatedBy?.toDto(),
updatedAt = this.updatedAt?: throw IllegalArgumentException("updated at is null"),
)
}
fun Budget.toShortDto(): BudgetDTO.BudgetShortInfoDTO = BudgetDTO.BudgetShortInfoDTO(
id = this.id!!,
type = this.type,
name = this.name,
description = this.description,
dateFrom = this.dateFrom,
dateTo = this.dateTo,
createdBy = this.createdBy?.username ?: throw IllegalArgumentException("created by is null"),
)
fun List<Transaction>.toDto(): BudgetDTO.BudgetTransactionsDTO {
val planningSpending = this.filter {
it.type == TransactionType.EXPENSE && it.kind == TransactionKind.PLANNING
}
val planningIncomes = this.filter {
it.type == TransactionType.INCOME && it.kind == TransactionKind.PLANNING
}
val instantSpendingTransactions = this.filter {
it.type == TransactionType.EXPENSE && it.kind == TransactionKind.INSTANT && it.parentId == null }
val instantIncomeTransactions = this.filter {
it.type == TransactionType.INCOME && it.kind == TransactionKind.INSTANT && it.parentId == null
}
val totalPlannedIncome = planningIncomes.sumOf { it.amount }
val totalPlannedSpending = planningSpending.sumOf { it.amount }
val categoriesWithPlannedAmounts = this.categories.map { cat ->
val totalPlannedAmount = planningSpending
.filter { it.id == cat.categoryId }
.sumOf { it.amount }
val totalInstantAmount = instantSpendingTransactions
.filter { it.id == cat.categoryId }
.sumOf { it.amount }
BudgetDTO.BudgetCategoryDto(cat.category?.toDto() ?: throw java.lang.IllegalArgumentException("category is not provided"), cat.limit, totalPlannedAmount, totalInstantAmount)
}
return BudgetDTO.BudgetTransactionsDTO(
categories = categoriesWithPlannedAmounts,
transactions = this.map{it.toDto()},
plannedIncomeTransactions = this.filter { it.kind == TransactionKind.PLANNING && it.type == TransactionType.INCOME }.map{it.toDto()},
plannedExpenseTransactions = this.filter { it.kind == TransactionKind.PLANNING && it.type == TransactionType.EXPENSE }.map{it.toDto()},
instantIncomeTransactions = this.filter { it.kind == TransactionKind.INSTANT && it.type == TransactionType.INCOME }.map{it.toDto()},
instantExpenseTransactions = this.filter { it.kind == TransactionKind.INSTANT && it.type == TransactionType.EXPENSE }.map{it.toDto()},
)
}
}

View File

@@ -11,11 +11,12 @@ object CategoryMapper {
id = this.id ?: throw IllegalArgumentException("category id is not set"), id = this.id ?: throw IllegalArgumentException("category id is not set"),
type = this.type, type = this.type,
name = this.name, name = this.name,
description = this.description,
icon = this.icon, icon = this.icon,
createdBy = this.createdBy?.toDto(), createdBy = this.createdBy?.toDto(),
createdAt = this.createdAt ?: throw IllegalArgumentException("created at is not set"), createdAt = this.createdAt,
updatedBy = this.updatedBy?.toDto(), updatedBy = this.updatedBy?.toDto(),
updatedAt = this.updatedAt ?: throw IllegalArgumentException("updated at is not set"), updatedAt = this.updatedAt
) )

View File

@@ -10,12 +10,13 @@ object GoalMapper {
id = this.id ?: throw IllegalArgumentException("Goal id is not provided"), id = this.id ?: throw IllegalArgumentException("Goal id is not provided"),
type = this.type, type = this.type,
name = this.name, name = this.name,
amount = this.goalAmount, amount = this.amount,
date = this.goalDate, date = this.untilDate,
components = this.components,
transactions = this.transactions,
createdBy = (this.createdBy ?: throw IllegalArgumentException("created by not provided")).toDto(), createdBy = (this.createdBy ?: throw IllegalArgumentException("created by not provided")).toDto(),
createdAt = this.createdAt ?: throw IllegalArgumentException("created at not provided"), createdAt = this.createdAt ?: throw IllegalArgumentException("created at not provided"),
updatedBy = this.updatedBy?.toDto() ?: throw IllegalArgumentException("updated by not provided"), updatedBy = this.updatedBy?.toDto() ,
updatedAt = this.updatedAt ?: throw IllegalArgumentException("updatedAt not provided"), updatedAt = this.updatedAt
) )
} }

View File

@@ -0,0 +1,21 @@
package space.luminic.finance.mappers
import space.luminic.finance.dtos.RecurrentOperationDTO
import space.luminic.finance.mappers.CategoryMapper.toDto
import space.luminic.finance.mappers.UserMapper.toDto
import space.luminic.finance.models.RecurrentOperation
object RecurrentOperationMapper {
fun RecurrentOperation.toDTO(): RecurrentOperationDTO {
return RecurrentOperationDTO(
id = this.id ?: throw IllegalArgumentException("id is null"),
category = this.category.toDto(),
name = this.name,
amount = this.amount,
date = this.date,
createdBy = this.createdBy?.toDto(),
createdAt = this.createdAt ?: throw NullPointerException("created at"),
)
}
}

View File

@@ -8,8 +8,8 @@ object SpaceMapper {
fun Space.toDto() = SpaceDTO( fun Space.toDto() = SpaceDTO(
id = this.id, id = this.id,
name = this.name, name = this.name,
owner = this.owner?.toDto() ?: throw IllegalArgumentException("Owner is not provided"), owner = this.owner.toDto(),
participants = this.participants?.map { it.toDto() } ?: emptyList(), participants = this.participants.map { it.toDto() }.toSet(),
createdBy = this.createdBy?.toDto(), createdBy = this.createdBy?.toDto(),
createdAt = this.createdAt ?: throw IllegalArgumentException("createdAt is not provided"), createdAt = this.createdAt ?: throw IllegalArgumentException("createdAt is not provided"),
updatedBy = this.updatedBy?.toDto(), updatedBy = this.updatedBy?.toDto(),

View File

@@ -1,9 +1,6 @@
package space.luminic.finance.mappers package space.luminic.finance.mappers
import space.luminic.finance.dtos.TransactionDTO import space.luminic.finance.dtos.TransactionDTO
import space.luminic.finance.dtos.TransactionDTO.CreateTransactionDTO
import space.luminic.finance.dtos.TransactionDTO.UpdateTransactionDTO
import space.luminic.finance.mappers.AccountMapper.toDto
import space.luminic.finance.mappers.CategoryMapper.toDto import space.luminic.finance.mappers.CategoryMapper.toDto
import space.luminic.finance.mappers.UserMapper.toDto import space.luminic.finance.mappers.UserMapper.toDto
import space.luminic.finance.models.Transaction import space.luminic.finance.models.Transaction
@@ -11,25 +8,16 @@ import space.luminic.finance.models.Transaction
object TransactionMapper { object TransactionMapper {
fun Transaction.toDto() = TransactionDTO( fun Transaction.toDto() = TransactionDTO(
id = this.id, id = this.id,
parentId = this.parentId, parentId = this.parent?.id,
type = this.type, type = this.type,
kind = this.kind, kind = this.kind,
categoryId = this.categoryId, category = this.category.toDto(),
category = this.category?.toDto(),
comment = this.comment, comment = this.comment,
amount = this.amount, amount = this.amount,
fees = this.fees, fees = this.fees,
fromAccount = this.fromAccount?.toDto(),
toAccount = this.toAccount?.toDto(),
date = this.date, date = this.date,
createdBy = this.createdBy?.username, isDone = this.isDone,
updatedBy = this.updatedBy?.username, createdBy = this.createdBy?.toDto() ?: throw IllegalStateException("created by null"),
updatedBy = this.updatedBy?.toDto()
) )
} }

View File

@@ -13,4 +13,5 @@ object UserMapper {
tgUserName = this.tgUserName, tgUserName = this.tgUserName,
roles = this.roles roles = this.roles
) )
} }

View File

@@ -1,51 +0,0 @@
package space.luminic.finance.models
import org.springframework.data.annotation.CreatedDate
import org.springframework.data.annotation.Id
import org.springframework.data.annotation.LastModifiedDate
import org.springframework.data.annotation.ReadOnlyProperty
import org.springframework.data.relational.core.mapping.Column
import org.springframework.data.relational.core.mapping.Table
import java.math.BigDecimal
import java.time.Instant
@Table(name = "accounts")
data class Account (
@Id val id: Int? = null,
@Column("space_id")
val spaceId: Int,
val name: String,
val type: AccountType = AccountType.COLLECTING,
@Column("currency_code")
val currencyCode: String,
var amount: BigDecimal,
@Column("goal_id")
var goalId: Int? = null,
@Column("is_deleted")
var isDeleted: Boolean = false,
@Column("created_by")
val createdById: String? = null,
@CreatedDate
@Column("created_at")
val createdAt: Instant? = null,
@Column("updated_by")
val updatedById: String? = null,
@LastModifiedDate
@Column("updated_at")
val updatedAt: Instant? = null,
) {
@ReadOnlyProperty var goal: Goal? = null
@ReadOnlyProperty var currency: Currency? = null
@ReadOnlyProperty var transactions: List<Transaction>? = null
@ReadOnlyProperty var createdBy: User? = null
@ReadOnlyProperty var updatedBy: User? = null
enum class AccountType(displayName: String) {
SALARY("Зарплатный"),
COLLECTING("Накопительный"),
LOANS("Долговой"),
}
}

View File

@@ -1,62 +0,0 @@
package space.luminic.finance.models
import org.springframework.data.annotation.CreatedBy
import org.springframework.data.annotation.CreatedDate
import org.springframework.data.annotation.Id
import org.springframework.data.annotation.LastModifiedBy
import org.springframework.data.annotation.LastModifiedDate
import org.springframework.data.annotation.ReadOnlyProperty
import org.springframework.data.relational.core.mapping.Column
import org.springframework.data.relational.core.mapping.Table
import java.math.BigDecimal
import java.time.Instant
import java.time.LocalDate
@Table( "budgets")
data class Budget(
@Id var id: Int? = null,
@Column("space_id")
val spaceId: Int,
val type: BudgetType = BudgetType.SPECIAL,
var name: String,
var description: String? = null,
@Column("date_from")
var dateFrom: LocalDate,
@Column("date_to")
var dateTo: LocalDate,
val transactions: List<Transaction> = listOf(),
val categories: List<BudgetCategory> = listOf(),
@Column("is_deleted")
var isDeleted: Boolean = false,
@CreatedBy
@Column("created_by")
val createdById: Int? = null,
@CreatedDate
@Column("created_at")
var createdAt: Instant? = null,
@LastModifiedBy
@Column("updated_by")
var updatedById: Int? = null,
@LastModifiedDate
@Column("updated_at")
var updatedAt: Instant? = null,
) {
@ReadOnlyProperty var createdBy: User? = null
@ReadOnlyProperty var updatedBy: User? = null
data class BudgetCategory(
val categoryId: String,
val limit: BigDecimal
) {
@ReadOnlyProperty var category: Category? = null
}
enum class BudgetType(val displayName: String) {
MONTHLY("Месячный"),
SPECIAL("Специальный")
}
}

View File

@@ -2,35 +2,24 @@ package space.luminic.finance.models
import org.springframework.data.annotation.CreatedBy import org.springframework.data.annotation.CreatedBy
import org.springframework.data.annotation.CreatedDate import org.springframework.data.annotation.CreatedDate
import org.springframework.data.annotation.Id
import org.springframework.data.annotation.LastModifiedBy import org.springframework.data.annotation.LastModifiedBy
import org.springframework.data.annotation.LastModifiedDate import org.springframework.data.annotation.LastModifiedDate
import org.springframework.data.annotation.ReadOnlyProperty
import org.springframework.data.mongodb.core.mapping.Document
import org.springframework.data.annotation.Transient
import java.time.Instant import java.time.Instant
@Document(collection = "categories")
data class Category( data class Category(
@Id val id: String? = null, var id: Int? = null,
val spaceId: String, val space: Space? = null,
val type: CategoryType, val type: CategoryType,
val name: String, val name: String,
val description: String,
val icon: String, val icon: String,
var isDeleted: Boolean = false, var isDeleted: Boolean = false,
@CreatedBy @CreatedBy var createdBy: User? = null,
val createdById: String? = null, @CreatedDate var createdAt: Instant? = null,
@CreatedDate @LastModifiedBy var updatedBy: User? = null,
val createdAt: Instant? = null, @LastModifiedDate var updatedAt: Instant? = null,
@LastModifiedBy
val updatedById: String? = null,
@LastModifiedDate
val updatedAt: Instant? = null,
) { ) {
@ReadOnlyProperty var createdBy: User? = null
@ReadOnlyProperty var updatedBy: User? = null
enum class CategoryType(val displayName: String) { enum class CategoryType(val displayName: String) {
INCOME("Поступления"), INCOME("Поступления"),
@@ -38,9 +27,8 @@ data class Category(
} }
@Document(collection = "categories_etalon")
data class CategoryEtalon( data class CategoryEtalon(
@Id val id: String? = null, var id: Int? = null,
val type: CategoryType, val type: CategoryType,
val name: String, val name: String,
val icon: String val icon: String

View File

@@ -1,12 +1,9 @@
package space.luminic.finance.models package space.luminic.finance.models
import org.springframework.data.annotation.Id
import org.springframework.data.mongodb.core.mapping.Document
import java.math.BigDecimal
@Document(collection = "currencies_ref")
data class Currency( data class Currency(
@Id val code: String, val code: String,
val name: String, val name: String,
val symbol: String val symbol: String
) )

View File

@@ -1,18 +1,12 @@
package space.luminic.finance.models package space.luminic.finance.models
import org.springframework.data.annotation.Id
import org.springframework.data.annotation.ReadOnlyProperty
import org.springframework.data.mongodb.core.mapping.Document
import java.math.BigDecimal import java.math.BigDecimal
import java.time.LocalDate import java.time.LocalDate
@Document(collection = "currency_rates")
data class CurrencyRate( data class CurrencyRate(
@Id val id: String? = null, var id: Int? = null,
val currencyCode: String, val currency: Currency,
val rate: BigDecimal, val rate: BigDecimal,
val date: LocalDate val date: LocalDate
) )
{
@ReadOnlyProperty var currency: Currency? = null
}

View File

@@ -2,35 +2,43 @@ package space.luminic.finance.models
import org.springframework.data.annotation.CreatedBy import org.springframework.data.annotation.CreatedBy
import org.springframework.data.annotation.CreatedDate import org.springframework.data.annotation.CreatedDate
import org.springframework.data.annotation.Id
import org.springframework.data.annotation.LastModifiedBy import org.springframework.data.annotation.LastModifiedBy
import org.springframework.data.annotation.LastModifiedDate import org.springframework.data.annotation.LastModifiedDate
import org.springframework.data.mongodb.core.mapping.Document
import org.springframework.data.annotation.Transient
import space.luminic.finance.dtos.UserDTO
import java.math.BigDecimal import java.math.BigDecimal
import java.text.Bidi
import java.time.Instant import java.time.Instant
import java.time.LocalDate import java.time.LocalDate
@Document(collection = "goals")
data class Goal( data class Goal(
@Id val id: String? = null, var id: Int? = null,
val spaceId: String, val space: Space? = null,
val type: GoalType, val type: GoalType,
val name: String, val name: String,
val description: String? = null, val description: String? = null,
val goalAmount: BigDecimal, val amount: BigDecimal,
val goalDate: LocalDate, val components: List<GoalComponent> = emptyList(),
@CreatedBy val createdById: String, val transactions: List<Transaction> = emptyList(),
@Transient val createdBy: User? = null, val untilDate: LocalDate,
@CreatedDate val createdAt: Instant? = null, @CreatedBy var createdBy: User? = null,
@LastModifiedBy val updatedById: String,
@Transient val updatedBy: User? = null, @CreatedDate var createdAt: Instant? = null,
@LastModifiedDate val updatedAt: Instant? = null, @LastModifiedBy var updatedBy: User? = null,
@LastModifiedDate var updatedAt: Instant? = null,
) { ) {
var currentAmount: BigDecimal = {
this.transactions.sumOf { it.amount }
} as BigDecimal
data class GoalComponent(
val id: Int? = null,
val name: String,
val amount: BigDecimal,
val isDone: Boolean = false,
val date: LocalDate = LocalDate.now(),
)
enum class GoalType(val displayName: String, val icon: String) { enum class GoalType(val displayName: String, val icon: String) {
AUTO("Авто", "🏎️"), AUTO("Авто", "🏎️"),
VACATION("Отпуск", "🏖️"), VACATION("Отпуск", "🏖️"),

View File

@@ -0,0 +1,17 @@
package space.luminic.finance.models
import java.math.BigDecimal
import java.time.Instant
data class RecurrentOperation(
val id: Int? = null,
val space: Space,
val category: Category,
val name: String,
val amount: BigDecimal,
val date: Int,
val createdBy: User? = null,
val createdAt: Instant? = null,
val updatedBy: User? = null,
val updatedAt: Instant? = null,
)

View File

@@ -2,35 +2,21 @@ package space.luminic.finance.models
import org.springframework.data.annotation.CreatedBy import org.springframework.data.annotation.CreatedBy
import org.springframework.data.annotation.CreatedDate import org.springframework.data.annotation.CreatedDate
import org.springframework.data.annotation.Id
import org.springframework.data.annotation.LastModifiedBy import org.springframework.data.annotation.LastModifiedBy
import org.springframework.data.annotation.LastModifiedDate import org.springframework.data.annotation.LastModifiedDate
import org.springframework.data.annotation.ReadOnlyProperty
import org.springframework.data.mongodb.core.mapping.Document
import org.springframework.data.annotation.Transient
import java.time.Instant import java.time.Instant
data class Space (
@Document(collection = "spaces") var id: Int? = null,
data class Space (
@Id val id: String? = null,
val name: String, val name: String,
val ownerId: String, val owner: User,
val participantsIds: List<String> = emptyList(), var participants: Set<User>,
var isDeleted: Boolean = false, var isDeleted: Boolean = false,
@CreatedBy val createdById: String? = null, @CreatedBy var createdBy: User? = null,
@CreatedDate val createdAt: Instant? = null, @CreatedDate var createdAt: Instant? = null,
@LastModifiedBy val updatedById: String? = null, @LastModifiedBy var updatedBy: User? = null,
@LastModifiedDate var updatedAt: Instant? = null, @LastModifiedDate var updatedAt: Instant ? = null,
) { ) {
@ReadOnlyProperty var owner: User? = null override fun equals(other: Any?) = this === other || (other is Space && id != null && id == other.id)
override fun hashCode() = id?.hashCode() ?: 0
@ReadOnlyProperty var participants: List<User>? = null
@ReadOnlyProperty var createdBy: User? = null
@ReadOnlyProperty var updatedBy: User? = null
} }

View File

@@ -1,23 +1,19 @@
package space.luminic.finance.models package space.luminic.finance.models
import org.springframework.data.annotation.Id import org.springframework.data.annotation.CreatedDate
import org.springframework.data.mongodb.core.mapping.DBRef import org.springframework.data.annotation.LastModifiedDate
import org.springframework.data.mongodb.core.mapping.Document
import java.time.Instant import java.time.Instant
@Document(collection = "subscriptions")
data class Subscription( data class Subscription(
@Id val id: String? = null, var id: Int? = null,
@DBRef val user: User? = null, val user: User,
val endpoint: String, val endpoint: String,
val auth: String, val auth: String,
val p256dh: String, val p256dh: String,
var isActive: Boolean, var isActive: Boolean,
val createdAt: Instant = Instant.now(), @CreatedDate val createdAt: Instant? = null,
) @LastModifiedDate val updatedAt: Instant? = null,
)
data class SubscriptionDTO (
val endpoint: String,
val keys: Map<String, String>
)

View File

@@ -1,17 +1,13 @@
package space.luminic.finance.models package space.luminic.finance.models
import org.springframework.data.annotation.Id import java.time.Instant
import org.springframework.data.mongodb.core.mapping.Document
import java.time.LocalDateTime
@Document(collection = "tokens")
data class Token( data class Token(
@Id var id: Int? = null,
val id: String? = null,
val token: String, val token: String,
val username: String, val user: User,
val issuedAt: LocalDateTime, val issuedAt: Instant = Instant.now(),
val expiresAt: LocalDateTime, val expiresAt: Instant,
val status: TokenStatus = TokenStatus.ACTIVE val status: TokenStatus = TokenStatus.ACTIVE
) { ) {

View File

@@ -2,49 +2,31 @@ package space.luminic.finance.models
import org.springframework.data.annotation.CreatedBy import org.springframework.data.annotation.CreatedBy
import org.springframework.data.annotation.CreatedDate import org.springframework.data.annotation.CreatedDate
import org.springframework.data.annotation.Id
import org.springframework.data.annotation.LastModifiedBy import org.springframework.data.annotation.LastModifiedBy
import org.springframework.data.annotation.LastModifiedDate import org.springframework.data.annotation.LastModifiedDate
import org.springframework.data.annotation.ReadOnlyProperty
import org.springframework.data.mongodb.core.mapping.Document
import org.springframework.data.annotation.Transient
import java.math.BigDecimal import java.math.BigDecimal
import java.time.Instant import java.time.Instant
import java.time.LocalDate
@Document(collection = "transactions")
data class Transaction( data class Transaction(
@Id val id: String? = null, var id: Int? = null,
val spaceId: String, val space: Space? = null,
var parentId: String? = null, var parent: Transaction? = null,
val type: TransactionType = TransactionType.EXPENSE, val type: TransactionType = TransactionType.EXPENSE,
val kind: TransactionKind = TransactionKind.INSTANT, val kind: TransactionKind = TransactionKind.INSTANT,
val categoryId: String, val category: Category,
val comment: String, val comment: String,
val amount: BigDecimal, val amount: BigDecimal,
val fees: BigDecimal = BigDecimal.ZERO, val fees: BigDecimal = BigDecimal.ZERO,
val fromAccountId: String, val date: LocalDate = LocalDate.now(),
val toAccountId: String? = null,
val date: Instant = Instant.now(),
var isDeleted: Boolean = false, var isDeleted: Boolean = false,
@CreatedBy val isDone: Boolean = false,
val createdById: String? = null, @CreatedBy var createdBy: User? = null,
@CreatedDate @CreatedDate var createdAt: Instant? = null,
val createdAt: Instant? = null, @LastModifiedBy var updatedBy: User? = null,
@LastModifiedBy @LastModifiedDate var updatedAt: Instant? = null,
val updatedById: String? = null,
@LastModifiedDate
val updatedAt: Instant? = null,
) { ) {
@ReadOnlyProperty var category: Category? = null
@ReadOnlyProperty var toAccount: Account? = null
@ReadOnlyProperty var fromAccount: Account? = null
@ReadOnlyProperty var createdBy: User? = null
@ReadOnlyProperty var updatedBy: User? = null
enum class TransactionType(val displayName: String) { enum class TransactionType(val displayName: String) {
INCOME("Поступления"), INCOME("Поступления"),

View File

@@ -1,25 +1,24 @@
package space.luminic.finance.models package space.luminic.finance.models
import org.springframework.data.annotation.Id import org.springframework.data.annotation.CreatedDate
import org.springframework.data.mongodb.core.mapping.Document import org.springframework.data.annotation.LastModifiedDate
import org.springframework.data.annotation.Transient import java.time.Instant
import java.time.LocalDate import java.time.LocalDate
import java.time.LocalDateTime
@Document("users")
data class User( data class User(
@Id var id: Int? = null,
var id: String? = null,
val username: String, val username: String,
var firstName: String, var firstName: String,
var tgId: String? = null, var tgId: String? = null,
var tgUserName: String? = null, var tgUserName: String? = null,
var password: String, var password: String? = null,
var isActive: Boolean = true, var isActive: Boolean = true,
var regDate: LocalDate = LocalDate.now(), var regDate: LocalDate = LocalDate.now(),
val createdAt: LocalDateTime = LocalDateTime.now(), @CreatedDate val createdAt: Instant? = null,
var roles: MutableList<String> = mutableListOf(), @LastModifiedDate var updatedAt: Instant? = null,
var roles: List<String> = listOf(),
) )

View File

@@ -1,7 +0,0 @@
package space.luminic.finance.repos
import org.springframework.data.mongodb.repository.ReactiveMongoRepository
import space.luminic.finance.models.Account
interface AccountRepo : ReactiveMongoRepository<Account, String> {
}

View File

@@ -1,14 +0,0 @@
package space.luminic.finance.repos
import org.bson.types.ObjectId
import org.springframework.data.mongodb.repository.ReactiveMongoRepository
import org.springframework.stereotype.Repository
import reactor.core.publisher.Flux
import space.luminic.finance.models.Budget
@Repository
interface BudgetRepo: ReactiveMongoRepository<Budget, String> {
suspend fun findBudgetsBySpaceIdAndIsDeletedFalse(spaceId: String): Flux<Budget>
suspend fun findBudgetsBySpaceIdAndId(spaceId: String, budgetId: String): Flux<Budget>
}

View File

@@ -1,9 +1,16 @@
package space.luminic.finance.repos package space.luminic.finance.repos
import org.springframework.data.mongodb.repository.ReactiveMongoRepository
import space.luminic.finance.models.Category import space.luminic.finance.models.Category
interface CategoryRepo: ReactiveMongoRepository<Category, String> {
interface CategoryRepo {
fun findBySpaceId(spaceId: Int): List<Category>
fun findBySpaceIdAndId(spaceId: Int, id: Int): Category?
fun create(category: Category, createdById: Int): Category
fun update(category: Category, updatedById: Int): Category
fun delete(categoryId: Int)
} }
interface CategoryEtalonRepo: ReactiveMongoRepository<Category.CategoryEtalon, String> interface CategoryEtalonRepo {
fun findAll(): List<Category>
}

View File

@@ -0,0 +1,107 @@
package space.luminic.finance.repos
import org.springframework.dao.DuplicateKeyException
import org.springframework.jdbc.core.RowMapper
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate
import org.springframework.stereotype.Repository
import space.luminic.finance.models.Category
@Repository
class CategoryRepoImpl(
private val jdbcTemplate: NamedParameterJdbcTemplate
) : CategoryRepo {
private fun categoryRowMapper() = RowMapper<Category> { rs, _ ->
Category(
id = rs.getInt("id"),
type = Category.CategoryType.valueOf(rs.getString("type")),
name = rs.getString("name"),
description = rs.getString("description"),
icon = rs.getString("icon"),
isDeleted = rs.getBoolean("is_deleted"),
createdAt = rs.getTimestamp("created_at").toInstant(),
updatedAt = if (rs.getTimestamp("updated_at") != null) rs.getTimestamp("updated_at").toInstant() else null
)
}
override fun findBySpaceId(spaceId: Int): List<Category> {
val query = "select * from finance.categories where space_id = :space_id order by id"
val params = mapOf("space_id" to spaceId)
return jdbcTemplate.query(query, params, categoryRowMapper())
}
override fun findBySpaceIdAndId(spaceId: Int, id: Int): Category? {
val query = "select * from finance.categories where space_id = :space_id and id = :id"
val params = mapOf("id" to id, "space_id" to spaceId)
return jdbcTemplate.query(query, params, categoryRowMapper()).firstOrNull()
}
override fun create(category: Category, createdById: Int): Category {
if (category.id == null) {
val query =
"insert into finance.categories(space_id, type, name, description, icon, is_deleted, created_by_id) values (:space_id, :type, :name, :description, :icon, :isDeleted, :createdById) returning id"
val params = mapOf(
"space_id" to category.space!!.id,
"type" to category.type.name,
"name" to category.name,
"description" to category.description,
"icon" to category.icon,
"isDeleted" to category.isDeleted,
"createdById" to createdById
)
try {
category.id = jdbcTemplate.queryForObject(query, params, Int::class.java)
return category
} catch (ex: DuplicateKeyException) {
throw IllegalArgumentException("You cannot create a category with similar name in one space", ex)
}
} else throw IllegalArgumentException("Category id must by null")
}
override fun update(category: Category, updatedById: Int): Category {
if (category.id != null) {
val query =
"update finance.categories set type=:type, name=:name, description=:description, icon=:icon, is_deleted=:isDeleted , updated_by_id = :updatedById, updated_at = now() where id = :id"
val params = mapOf(
"id" to category.id!!,
"type" to category.type.name,
"name" to category.name,
"description" to category.description,
"icon" to category.icon,
"isDeleted" to category.isDeleted,
"updatedById" to updatedById
)
jdbcTemplate.update(query, params)
return category
} else throw IllegalArgumentException("Category cannot be null")
}
override fun delete(categoryId: Int) {
val query = "update finance.categories set is_deleted=:isDeleted where id = :id"
val params = mapOf(
"id" to categoryId,
"isDeleted" to true
)
jdbcTemplate.update(query, params)
}
}
@Repository
class CategoryEtalonRepoImpl(
private val jdbcTemplate: NamedParameterJdbcTemplate
) : CategoryEtalonRepo {
private val rowMapper = RowMapper { rs, _ ->
Category(
type = Category.CategoryType.valueOf(rs.getString("type")),
name = rs.getString("name"),
description = rs.getString("description"),
icon = rs.getString("icon"),
)
}
override fun findAll(): List<Category> {
val query = "select * from finance.categories_etalon"
return jdbcTemplate.query(query, rowMapper)
}
}

View File

@@ -1,8 +1,21 @@
package space.luminic.finance.repos package space.luminic.finance.repos
import org.springframework.data.mongodb.repository.ReactiveMongoRepository import org.springframework.stereotype.Repository
import space.luminic.finance.models.Currency import space.luminic.finance.models.Currency
import space.luminic.finance.models.CurrencyRate
interface CurrencyRepo: ReactiveMongoRepository<Currency, String> @Repository
interface CurrencyRateRepo: ReactiveMongoRepository<CurrencyRate, String> interface CurrencyRepo {
fun findAll(): List<Currency>
fun findByCode(code: String): Currency?
fun save(currency: Currency): Currency
fun update(currency: Currency)
fun delete(currencyCode: String)
}
//
//interface CurrencyRateRepo {
// fun findByCode(code: String): List<CurrencyRate>
// fun save(currencyRate: CurrencyRate)
// fun update(currencyRate: CurrencyRate)
// fun delete(id: Int)
//}

View File

@@ -0,0 +1,50 @@
package space.luminic.finance.repos
import org.springframework.jdbc.core.RowMapper
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate
import org.springframework.stereotype.Repository
import space.luminic.finance.models.Currency
@Repository
class CurrencyRepoImpl(
private val jdbcTemplate: NamedParameterJdbcTemplate
): CurrencyRepo {
private fun currencyRowMapper() = RowMapper<Currency> { rs, _ ->
Currency(
code = rs.getString("code"),
name = rs.getString("name"),
symbol = rs.getString("symbol"),
)
}
override fun findAll(): List<Currency> {
val sql = "SELECT * FROM finance.currencies_ref"
return jdbcTemplate.query(sql, currencyRowMapper())
}
override fun findByCode(code: String): Currency? {
val sql = "SELECT * FROM finance.currencies_ref WHERE code = :code"
val params = mapOf("code" to code)
return jdbcTemplate.queryForObject(sql, params, Currency::class.java)
}
override fun save(currency: Currency): Currency {
val sql = "insert into finance.currencies_ref (code, name, symbol) values (:code, :name, :symbol)"
val params = mapOf("code" to currency.code, "name" to currency.name, "symbol" to currency.symbol)
jdbcTemplate.update(sql, params)
return currency
}
override fun update(currency: Currency) {
val sql = "update finance.currencies_ref set name=:name, symbol=:symbol where code = :code"
val params = mapOf("name" to currency.code, "symbol" to currency.name)
jdbcTemplate.update(sql, params)
}
override fun delete(currencyCode: String) {
val sql = "delete from finance.currencies_ref where code = :code"
val params = mapOf("code" to currencyCode)
jdbcTemplate.update(sql, params)
}
}

View File

@@ -0,0 +1,20 @@
package space.luminic.finance.repos
import space.luminic.finance.models.Goal
interface GoalRepo {
fun findAllBySpaceId(spaceId: Int) : List<Goal>
fun findBySpaceIdAndId(spaceId: Int, id: Int) : Goal?
fun create(goal: Goal, createdById: Int): Int
fun update(goal: Goal, updatedById: Int)
fun delete(spaceId: Int, id: Int)
fun getComponents(spaceId: Int, goalId: Int): List<Goal.GoalComponent>
fun getComponent(spaceId: Int, goalId: Int, id: Int): Goal.GoalComponent?
fun createComponent(goalId: Int, component: Goal.GoalComponent, createdById: Int): Int
fun updateComponent(goalId: Int, componentId: Int, component: Goal.GoalComponent, updatedById: Int)
fun deleteComponent(goalId: Int, componentId: Int)
fun assignTransaction(goalId: Int, transactionId: Int)
fun refuseTransaction(goalId: Int, transactionId: Int)
}

View File

@@ -0,0 +1,283 @@
package space.luminic.finance.repos
import org.springframework.jdbc.core.RowMapper
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate
import org.springframework.stereotype.Repository
import space.luminic.finance.models.Goal
import space.luminic.finance.models.User
@Repository
class GoalRepoImpl(
private val jdbcTemplate: NamedParameterJdbcTemplate
) : GoalRepo {
private val goalRowMapper = RowMapper { rs, _ ->
Goal(
id = rs.getInt("g_id"),
type = Goal.GoalType.valueOf(rs.getString("g_type")),
name = rs.getString("g_name"),
description = rs.getString("g_description"),
amount = rs.getBigDecimal("g_amount"),
components = listOf(),
transactions = listOf(),
untilDate = rs.getDate("g_until_date").toLocalDate(),
createdBy = User(
id = rs.getInt("created_by_id"),
username = rs.getString("created_by_username"),
firstName = rs.getString("created_by_first_name"),
),
createdAt = rs.getTimestamp("g_created_at").toInstant()
)
}
private val componentRowMapper = RowMapper { rs, _ ->
Goal.GoalComponent(
id = rs.getInt("gc_id"),
name = rs.getString("gc_name"),
amount = rs.getBigDecimal("gc_amount"),
isDone = rs.getBoolean("gc_is_done"),
date = rs.getDate("gc_date").toLocalDate()
)
}
override fun findAllBySpaceId(spaceId: Int): List<Goal> {
val sql = """
select
g.id as g_id,
g.type as g_type,
g.name as g_name,
g.description as g_description,
g.amount as g_amount,
created_by.id as created_by_id,
created_by.username as created_by_username,
created_by.first_name as created_by_first_name,
g.created_at as g_created_at
from finance.goals g
join finance.users created_by on g.created_by_id = created_by.id
where g.space_id = :spaceId
""".trimIndent()
val params = mapOf(
"space_id" to spaceId,
)
return jdbcTemplate.query(sql, params, goalRowMapper)
}
override fun findBySpaceIdAndId(spaceId: Int, id: Int): Goal? {
val sql = """
select
g.id as g_id,
g.type as g_type,
g.name as g_name,
g.description as g_description,
g.amount as g_amount,
created_by.id as created_by_id,
created_by.username as created_by_username,
created_by.first_name as created_by_first_name,
g.created_at as g_created_at
from finance.goals g
join finance.users created_by on g.created_by_id = created_by.id
where g.space_id = :spaceId and g.id = :id
""".trimIndent()
val params = mapOf(
"space_id" to spaceId,
"id" to id,
)
return jdbcTemplate.query(sql, params, goalRowMapper).firstOrNull()
}
override fun create(goal: Goal, createdById: Int): Int {
val sql = """
insert into finance.goals(
type,
name,
description,
amount,
until_date,
created_by_id
) values (
:type,
:name,
:description,
:amount,
:until_date,
:created_by_id
)
returning id
""".trimIndent()
val params = mapOf(
"type" to goal.type,
"name" to goal.name,
"description" to goal.description,
"amount" to goal.amount,
"until_date" to goal.untilDate,
"created_by_id" to createdById
)
return jdbcTemplate.queryForObject(sql, params, Int::class.java)!!
}
override fun update(goal: Goal, updatedById: Int) {
val sql = """
update finance.goals set
type = :type,
name = :name,
description = :description,
amount = :amount,
until_date = :until_date,
updated_by_id = :updated_by_id,
updated_at = now()
where id = :id
""".trimIndent()
val params = mapOf(
"id" to goal.id,
"type" to goal.type.name,
"name" to goal.name,
"description" to goal.description,
"amount" to goal.amount,
"until_date" to goal.untilDate,
"updated_by_id" to updatedById
)
jdbcTemplate.update(sql, params)
}
override fun delete(spaceId: Int, id: Int) {
val sql = """
delete from finance.goals where id = :id
""".trimIndent()
val params = mapOf(
"id" to id
)
jdbcTemplate.update(sql, params)
}
override fun getComponents(
spaceId: Int,
goalId: Int
): List<Goal.GoalComponent> {
val sql = """
select
gc.id as gc_id,
gc.name as gc_name,
gc.amount as gc_amount,
gc.is_done as gc_is_done,
gc.date as gc_date
from finance.goals_components gc
where gc.goal_id = :goal_id
""".trimIndent()
val params = mapOf(
"goal_id" to goalId
)
return jdbcTemplate.query(sql, params, componentRowMapper)
}
override fun getComponent(
spaceId: Int,
goalId: Int,
id: Int
): Goal.GoalComponent? {
val sql = """
select
gc.id as gc_id,
gc.name as gc_name,
gc.amount as gc_amount,
gc.is_done as gc_is_done,
gc.date as gc_date
from finance.goals_components gc
where gc.goal_id = :goal_id and gc.id = :id
""".trimIndent()
val params = mapOf(
"goal_id" to goalId,
"id" to id
)
return jdbcTemplate.query(sql, params, componentRowMapper).firstOrNull()
}
override fun createComponent(goalId: Int, component: Goal.GoalComponent, createdById: Int): Int {
val sql = """
insert into finance.goals_components(
goal_id,
name,
amount,
is_done,
date,
created_by_id
) values (
:goal_id,
:name,
:amount,
:is_done,
:date,
:created_by_id)
returning id
""".trimIndent()
val params = mapOf(
"goal_id" to goalId,
"name" to component.name,
"amount" to component.amount,
"is_done" to component.isDone,
"date" to component.date,
"created_by_id" to createdById
)
return jdbcTemplate.queryForObject(sql, params, Int::class.java)!!
}
override fun updateComponent(goalId: Int, componentId: Int, component: Goal.GoalComponent, updatedById: Int) {
val sql = """
update finance.goals_components set
name = :name,
amount = :amount,
is_done = :is_done,
updated_by_id = :updated_by_id
where goal_id = :goalId and id = :componentId
""".trimIndent()
val params = mapOf(
"goalId" to goalId,
"componentId" to componentId,
"name" to component.name,
"amount" to component.amount,
"is_done" to component.isDone,
"date" to component.date,
"updated_by_id" to updatedById
)
jdbcTemplate.update(sql, params)
}
override fun deleteComponent(goalId: Int, componentId: Int) {
val sql = """
delete from finance.goals_components where goal_id = :goalId and id = :componentId
""".trimIndent()
val params = mapOf(
"goalId" to goalId,
"componentId" to componentId
)
jdbcTemplate.update(sql, params)
}
override fun assignTransaction(goalId: Int, transactionId: Int) {
val sql = """
insert into finance.goals_transactions(goal_id, transactions_id)
values (:goal_id, :transaction_id)
""".trimIndent()
val params = mapOf(
"goal_id" to goalId,
"transaction_id" to transactionId
)
jdbcTemplate.update(sql, params)
}
override fun refuseTransaction(goalId: Int, transactionId: Int) {
val sql = """
delete from finance.goals_transactions where goal_id = :goalId and transactions_id = :transactionId
""".trimIndent()
val params = mapOf(
"goal_id" to goalId,
"transaction_id" to transactionId
)
jdbcTemplate.update(sql, params)
}
}

View File

@@ -0,0 +1,11 @@
package space.luminic.finance.repos
import space.luminic.finance.models.RecurrentOperation
interface RecurrentOperationRepo {
fun findAllBySpaceId(spaceId: Int): List<RecurrentOperation>
fun findBySpaceIdAndId(spaceId: Int, id: Int): RecurrentOperation?
fun create(operation: RecurrentOperation, createdById: Int): Int
fun update(operation: RecurrentOperation, updatedById: Int)
fun delete(id: Int)
}

View File

@@ -0,0 +1,178 @@
package space.luminic.finance.repos
import org.springframework.jdbc.core.RowMapper
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate
import org.springframework.stereotype.Repository
import space.luminic.finance.models.Category
import space.luminic.finance.models.RecurrentOperation
import space.luminic.finance.models.Space
import space.luminic.finance.models.User
@Repository
class RecurrentOperationRepoImpl(
private val jdbcTemplate: NamedParameterJdbcTemplate
) : RecurrentOperationRepo {
private fun operationRowMapper() = RowMapper<RecurrentOperation> { rs, _ ->
RecurrentOperation(
id = rs.getInt("r_id"),
space = Space(
id = rs.getInt("r_space_id"),
name = rs.getString("s_name"),
owner = User(
rs.getInt("s_owner_id"),
username = rs.getString("su_username"),
firstName = rs.getString("su_first_name"),
),
participants = setOf()
),
category = Category(
id = rs.getInt("r_category_id"),
type = Category.CategoryType.valueOf(rs.getString("c_type")),
name = rs.getString("c_name"),
description = rs.getString("c_description"),
icon = rs.getString("c_icon"),
),
name = rs.getString("r_name"),
amount = rs.getBigDecimal("r_amount"),
date = rs.getInt("r_date"),
createdBy = User(
id = rs.getInt("r_created_by_id"),
username = rs.getString("r_created_by_username"),
firstName = rs.getString("r_created_by_first_name"),
),
createdAt = rs.getTimestamp("r_created_at").toInstant()
)
}
override fun findAllBySpaceId(spaceId: Int): List<RecurrentOperation> {
val sql = """
select
ro.id as r_id,
ro.space_id AS r_space_id,
s.name AS s_name,
s.owner_id as s_owner_id,
su.username as su_username,
su.first_name AS su_first_name,
ro.category_id as r_category_id,
c.type AS c_type,
c.name AS c_name,
c.description AS c_description,
c.icon AS c_icon,
ro.name AS r_name,
ro.amount AS r_amount,
ro.date AS r_date,
ro.created_by_id as r_created_by_id,
r_created_by.username as r_created_by_username,
r_created_by.first_name as r_created_by_first_name,
ro.created_at as r_created_at
from finance.recurrent_operations ro
join finance.spaces s on ro.space_id = s.id
join finance.users su on s.owner_id = su.id
join finance.categories c on ro.category_id = c.id
join finance.users r_created_by on ro.created_by_id = r_created_by.id
where ro.space_id = :spaceId
order by ro.date
""".trimIndent()
val params = mapOf("spaceId" to spaceId)
return jdbcTemplate.query(sql, params, operationRowMapper())
}
override fun findBySpaceIdAndId(
spaceId: Int,
id: Int
): RecurrentOperation? {
val sql = """
select
ro.id as r_id,
ro.space_id AS r_space_id,
s.name AS s_name,
s.owner_id as s_owner_id,
su.username as su_username,
su.first_name AS su_first_name,
ro.category_id as r_category_id,
c.type AS c_type,
c.name AS c_name,
c.description AS c_description,
c.icon AS c_icon,
ro.name AS r_name,
ro.amount AS r_amount,
ro.date AS r_date,
ro.created_by_id as r_created_by_id,
r_created_by.username as r_created_by_username,
r_created_by.first_name as r_created_by_first_name,
ro.created_at as r_created_at
from finance.recurrent_operations ro
join finance.spaces s on ro.space_id = s.id
join finance.users su on s.owner_id = su.id
join finance.categories c on ro.category_id = c.id
join finance.users r_created_by on ro.created_by_id = r_created_by.id
where ro.space_id = :spaceId and ro.id = :id
""".trimIndent()
val params = mapOf("spaceId" to spaceId, "id" to id)
return jdbcTemplate.query(sql, params, operationRowMapper()).firstOrNull()
}
override fun create(operation: RecurrentOperation, createdById: Int): Int {
val sql = """
insert into finance.recurrent_operations (
space_id,
category_id,
name,
amount,
date,
created_by_id)
values (
:spaceId,
:categoryId,
:name,
:amount,
:date,
:created_by
)
returning id
""".trimIndent()
val params = mapOf(
"spaceId" to operation.space.id,
"categoryId" to operation.category.id,
"name" to operation.name,
"amount" to operation.amount,
"date" to operation.date,
"created_by" to createdById
)
return jdbcTemplate.queryForObject(sql, params, Int::class.java)!!
}
override fun update(operation: RecurrentOperation, updatedById: Int) {
val sql = """
update finance.recurrent_operations set
category_id = :categoryId,
name = :name,
amount = :amount,
date = :date,
updated_by_id = :updatedBy,
updated_at = now()
where id = :id
""".trimIndent()
val params = mapOf(
"categoryId" to operation.category.id,
"name" to operation.name,
"amount" to operation.amount,
"date" to operation.date,
"updatedBy" to updatedById,
"id" to operation.id
)
jdbcTemplate.update(sql, params)
}
override fun delete(id: Int) {
val sql = """
delete from finance.recurrent_operations
where id = :id
""".trimIndent()
val params = mapOf("id" to id)
jdbcTemplate.update(sql, params)
}
}

View File

@@ -1,8 +1,13 @@
package space.luminic.finance.repos package space.luminic.finance.repos
import org.springframework.data.mongodb.core.ReactiveMongoTemplate import org.springframework.stereotype.Repository
import org.springframework.data.mongodb.repository.ReactiveMongoRepository
import space.luminic.finance.models.Space import space.luminic.finance.models.Space
interface SpaceRepo: ReactiveMongoRepository<Space, String> { @Repository
interface SpaceRepo {
fun findSpacesAvailableForUser(userId: Int): List<Space>
fun findSpaceById(id: Int, userId: Int): Space?
fun create(space: Space, createdById: Int): Int
fun update(space: Space, updatedById: Int): Int
fun delete(id: Int)
} }

View File

@@ -0,0 +1,221 @@
package space.luminic.finance.repos
import org.springframework.jdbc.core.RowMapper
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate
import org.springframework.stereotype.Repository
import space.luminic.finance.dtos.SpaceDTO
import space.luminic.finance.models.Space
import space.luminic.finance.models.User
@Repository
class SpaceRepoImpl(
private val userRepo: UserRepo,
private val jdbcTemplate: NamedParameterJdbcTemplate
) : SpaceRepo {
private fun spaceRowMapper() = RowMapper { rs, _ ->
Space(
id = rs.getInt("id"),
name = rs.getString("name"),
owner = User(
id = rs.getInt("owner_id"),
username = rs.getString("username"),
firstName = rs.getString("first_name"),
password = rs.getString("password"),
),
participants = userRepo.findParticipantsBySpace(rs.getInt("id")).toSet(),
isDeleted = rs.getBoolean("is_deleted"),
createdAt = rs.getTimestamp("created_at").toInstant(),
updatedAt = rs.getTimestamp("updated_at").toInstant()
)
}
private fun shortRowMapper() = RowMapper { rs, _ ->
SpaceDTO.SpaceShortInfoDTO(
id = rs.getInt("s_id"),
name = rs.getString("s_name"),
isOwner = rs.getBoolean("s_is_owner"),
owner = User(
rs.getInt("s_owner_id"),
rs.getString("s_owner_username"),
rs.getString("s_owner_firstname")
),
participant = User(rs.getInt("sp_uid"), rs.getString("sp_username"), rs.getString("sp_first_name")),
createdAt = rs.getTimestamp("s_created_at").toInstant(),
createdBy = User(
rs.getInt("s_created_by"),
rs.getString("s_created_by_username"),
rs.getString("s_created_by_firstname")
),
updatedAt = if (rs.getTimestamp("s_updated_at") != null) rs.getTimestamp("s_updated_at")
.toInstant() else null,
updatedBy = if (rs.getInt("s_updated_by") != 0) User(
rs.getInt("s_updated_by"),
rs.getString("s_updated_by_username"),
rs.getString("s_updated_by_firstname")
) else null
)
}
private fun collectParticipants(spaces: List<SpaceDTO.SpaceShortInfoDTO>): List<Space> {
val spaceMap = mutableMapOf<Int, Space>()
spaces.forEach { row ->
val entity = row.participant
val existing = spaceMap[row.id]
if (existing == null) {
spaceMap[row.id] = Space(
id = row.id,
name = row.name,
owner = row.owner,
participants = setOf(entity),
createdBy = row.createdBy,
createdAt = row.createdAt,
updatedAt = row.updatedAt,
updatedBy = row.updatedBy
)
} else {
spaceMap[row.id] = existing.copy(
participants = existing.participants + entity
)
}
}
return spaceMap.map { it.value }
}
override fun findSpacesAvailableForUser(userId: Int): List<Space> {
val sql = """
select s.id as s_id,
s.name as s_name,
s.created_at as s_created_at,
s.owner_id = :user_id as s_is_owner,
s.owner_id as s_owner_id,
ou.username as s_owner_username,
ou.first_name as s_owner_firstname,
sp.participants_id as sp_uid,
u.username as sp_username,
u.first_name as sp_first_name,
s.created_at as s_created_at,
s.created_by_id as s_created_by,
cau.username as s_created_by_username,
cau.first_name as s_created_by_firstname,
s.updated_at as s_updated_at,
s.updated_by_id as s_updated_by,
uau.username as s_updated_by_username,
uau.first_name as s_updated_by_firstname
from finance.spaces s
join finance.users ou on s.owner_id = ou.id
join finance.spaces_participants sp on sp.space_id = s.id
join finance.users u on sp.participants_id = u.id
left join finance.users cau on s.created_by_id = cau.id
left join finance.users uau on s.updated_by_id = uau.id
where (s.owner_id = :user_id
or sp.participants_id = :user_id)
and s.is_deleted = false
group by s.id, ou.username, ou.first_name, sp.participants_id, u.username, u.first_name, cau.username, cau.first_name,
uau.username, uau.first_name;
""".trimMargin()
val params = mapOf(
"user_id" to userId,
)
val spaces = jdbcTemplate.query(sql, params, shortRowMapper())
return collectParticipants(spaces)
}
override fun findSpaceById(id: Int, userId: Int): Space? {
val sql = """
select s.id as s_id,
s.name as s_name,
s.created_at as s_created_at,
s.owner_id = :user_id as s_is_owner,
s.owner_id as s_owner_id,
ou.username as s_owner_username,
ou.first_name as s_owner_firstname,
sp.participants_id as sp_uid,
u.username as sp_username,
u.first_name as sp_first_name,
s.created_at as s_created_at,
s.created_by_id as s_created_by,
cau.username as s_created_by_username,
cau.first_name as s_created_by_firstname,
s.updated_at as s_updated_at,
s.updated_by_id as s_updated_by,
uau.username as s_updated_byusername,
uau.first_name as s_updated_byfirstname
from finance.spaces s
join finance.users ou on s.owner_id = ou.id
join finance.spaces_participants sp on sp.space_id = s.id
join finance.users u on sp.participants_id = u.id
left join finance.users cau on s.created_by_id = cau.id
left join finance.users uau on s.updated_by_id = uau.id
where (s.owner_id = :user_id
or sp.participants_id = :user_id)
and s.is_deleted = false and s.id = :spaceId
group by s.id, ou.username, ou.first_name, sp.participants_id, u.username, u.first_name, cau.username, cau.first_name,
uau.username, uau.first_name;
""".trimMargin()
val params = mapOf(
"spaceId" to id,
"user_id" to userId,
)
val spaces = jdbcTemplate.query(sql, params, shortRowMapper())
return collectParticipants(spaces).firstOrNull()
}
override fun create(space: Space, createdById: Int): Int {
val sql = """
insert into finance.spaces (name, owner_id, is_deleted, created_by_id) values (:name, :owner_id, :is_deleted, :created_by_id)
RETURNING id
""".trimIndent()
val params = mapOf(
"name" to space.name,
"owner_id" to space.owner.id,
"is_deleted" to false,
"created_by_id" to createdById
)
val createdSpaceId = jdbcTemplate.queryForObject(sql, params, Int::class.java)
// 2) батч-вставка участников (если есть)
if (space.participants.isNotEmpty()) {
val batchParams = space.participants.map { p ->
mapOf(
"spaceId" to createdSpaceId,
"participantId" to requireNotNull(p.id) { "participant.id is null" }
)
}.toTypedArray()
jdbcTemplate.batchUpdate(
"""
INSERT INTO finance.spaces_participants (space_id, participants_id)
VALUES (:spaceId, :participantId)
""".trimIndent(),
batchParams
)
}
return createdSpaceId!!
}
override fun update(space: Space, updatedById: Int): Int {
val sql = """
update finance.spaces set name = :name, updated_by_id = :updated_by_id, updated_at = now() where id = :spaceId
""".trimIndent()
val params = mapOf(
"name" to space.name,
"spaceId" to space.id,
"updated_by_id" to updatedById
)
jdbcTemplate.update(sql, params)
return space.id!!
}
override fun delete(id: Int) {
val sql = """
update finance.spaces set is_deleted = true where id = :id
""".trimIndent()
jdbcTemplate.update(sql, mapOf("id" to id))
}
}

View File

@@ -1,21 +1,7 @@
package space.luminic.finance.repos package space.luminic.finance.repos
import org.bson.types.ObjectId
import org.springframework.data.mongodb.repository.Query
import org.springframework.data.mongodb.repository.ReactiveMongoRepository
import org.springframework.stereotype.Repository import org.springframework.stereotype.Repository
import reactor.core.publisher.Flux
import space.luminic.finance.models.Subscription
@Repository @Repository
interface SubscriptionRepo : ReactiveMongoRepository<Subscription, String> { interface SubscriptionRepo
@Query("{ \$and: [ " +
"{ 'user': { '\$ref': 'users', '\$id': ?0 } }, " +
"{ 'isActive': true } " +
"]}")
fun findByUserIdAndIsActive(userId: ObjectId): Flux<Subscription>
}

View File

@@ -1,15 +1,16 @@
package space.luminic.finance.repos package space.luminic.finance.repos
import org.springframework.data.mongodb.repository.ReactiveMongoRepository
import org.springframework.stereotype.Repository import org.springframework.stereotype.Repository
import reactor.core.publisher.Mono
import space.luminic.finance.models.Token import space.luminic.finance.models.Token
import java.time.LocalDateTime import java.time.Instant
@Repository @Repository
interface TokenRepo: ReactiveMongoRepository<Token, String> { interface TokenRepo {
fun findByToken(token: String): Token?
fun deleteByExpiresAtBefore(dateTime: Instant)
fun findByToken(token: String): Mono<Token> fun create(token: Token): Token
fun update(token: Token): Token
fun delete(id: Int)
fun deleteByExpiresAtBefore(dateTime: LocalDateTime)
} }

View File

@@ -0,0 +1,84 @@
package space.luminic.finance.repos
import org.springframework.jdbc.core.RowMapper
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate
import org.springframework.stereotype.Repository
import space.luminic.finance.models.Token
import java.sql.Timestamp
import java.time.Instant
@Repository
class TokenRepoImpl(
private val userRepo: UserRepo,
private val jdbcTemplate: NamedParameterJdbcTemplate
) : TokenRepo {
private fun tokenRowMapper() = RowMapper { rs, _ ->
Token(
id = rs.getInt("id"),
token = rs.getString("token"),
user = userRepo.findById(rs.getInt("user_id")) ?: throw IllegalArgumentException("User not found"),
issuedAt = rs.getTimestamp("issued_at").toInstant(),
expiresAt = rs.getTimestamp("expires_at").toInstant(),
status = Token.TokenStatus.valueOf(rs.getString("status")),
)
}
override fun findByToken(token: String): Token? {
val sql = "SELECT * FROM finance.tokens WHERE token = :token"
val params = mapOf(
"token" to token,
)
return jdbcTemplate.query(sql, params, tokenRowMapper()).firstOrNull()
}
override fun deleteByExpiresAtBefore(dateTime: Instant) {
val sql = """
update finance.tokens set status = :status where expires_at <= :dateTime
""".trimIndent()
val params = mapOf(
"status" to Token.TokenStatus.ACTIVE,
"dateTime" to dateTime
)
jdbcTemplate.update(sql, params)
}
override fun create(token: Token): Token {
val sql = """
insert into finance.tokens(token, user_id, expires_at, status) values (:token, :userId, :expiresAt, :status)
""".trimIndent()
val params = mapOf(
"token" to token.token,
"userId" to token.user.id,
"expiresAt" to Timestamp.from(token.expiresAt),
"status" to Token.TokenStatus.ACTIVE.name,
)
val createdTokenId = jdbcTemplate.update(sql, params)
token.id = createdTokenId
return token
}
override fun update(token: Token): Token {
val sql = """
update finance.tokens set status = :status where token = :token
""".trimIndent()
val params = mapOf(
"token" to token,
"status" to token.status
)
jdbcTemplate.update(sql, params)
return token
}
override fun delete(id: Int) {
val sql = """
update finance.tokens set status = :status where id = :id
""".trimIndent()
val params = mapOf(
"id" to id,
"status" to Token.TokenStatus.REVOKED
)
jdbcTemplate.update(sql, params)
}
}

View File

@@ -1,7 +1,12 @@
package space.luminic.finance.repos package space.luminic.finance.repos
import org.springframework.data.mongodb.repository.ReactiveMongoRepository
import space.luminic.finance.models.Transaction import space.luminic.finance.models.Transaction
interface TransactionRepo: ReactiveMongoRepository<Transaction, String> { interface TransactionRepo {
fun findAllBySpaceId(spaceId: Int): List<Transaction>
fun findBySpaceIdAndId(spaceId: Int, id: Int): Transaction?
fun create(transaction: Transaction, userId: Int): Int
fun update(transaction: Transaction): Int
fun delete(transactionId: Int)
} }

View File

@@ -0,0 +1,206 @@
package space.luminic.finance.repos
import org.springframework.jdbc.core.RowMapper
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate
import org.springframework.stereotype.Repository
import space.luminic.finance.models.Category
import space.luminic.finance.models.Transaction
import space.luminic.finance.models.User
@Repository
class TransactionRepoImpl(
private val jdbcTemplate: NamedParameterJdbcTemplate,
) : TransactionRepo {
private fun transactionRowMapper() = RowMapper { rs, _ ->
Transaction(
id = rs.getInt("t_id"),
parent = findBySpaceIdAndId(spaceId = rs.getInt("t_space_id"), rs.getInt("t_parent_id")),
type = Transaction.TransactionType.valueOf(rs.getString("t_type")),
kind = Transaction.TransactionKind.valueOf(rs.getString("t_kind")),
category = Category(
id = rs.getInt("c_id"),
type = Category.CategoryType.valueOf(rs.getString("c_type")),
name = rs.getString(("c_name")),
description = rs.getString(("c_description")),
icon = rs.getString("c_icon"),
isDeleted = rs.getBoolean(("c_is_deleted")),
createdAt = rs.getTimestamp("c_created_at").toInstant(),
updatedAt = rs.getTimestamp("c_updated_at").toInstant(),
),
comment = rs.getString("t_comment"),
amount = rs.getBigDecimal("t_amount"),
fees = rs.getBigDecimal("t_fees"),
date = rs.getDate("t_date").toLocalDate(),
isDeleted = rs.getBoolean("t_is_deleted"),
isDone = rs.getBoolean("t_is_done"),
createdBy = User(
id = rs.getInt("u_id"),
username = rs.getString("u_username"),
firstName = rs.getString("u_first_name")
),
createdAt = rs.getTimestamp("t_created_at").toInstant(),
updatedAt = rs.getTimestamp("t_updated_at").toInstant(),
)
}
override fun findAllBySpaceId(spaceId: Int): List<Transaction> {
val sql = """
SELECT
t.id AS t_id,
t.parent_id AS t_parent_id,
t.space_id AS t_space_id,
t.type AS t_type,
t.kind AS t_kind,
t.comment AS t_comment,
t.amount AS t_amount,
t.fees AS t_fees,
t.date AS t_date,
t.is_deleted AS t_is_deleted,
t.is_done AS t_is_done,
t.created_at AS t_created_at,
t.updated_at AS t_updated_at,
c.id AS c_id,
c.type AS c_type,
c.name AS c_name,
c.description AS c_description,
c.icon AS c_icon,
c.is_deleted AS c_is_deleted,
c.created_at AS c_created_at,
c.updated_at AS c_updated_at,
u.id AS u_id,
u.username AS u_username,
u.first_name AS u_first_name
FROM finance.transactions t
JOIN finance.categories c ON t.category_id = c.id
JOIN finance.users u ON u.id = t.created_by_id
WHERE t.space_id = :spaceId and t.is_deleted = false
ORDER BY t.date
""".trimIndent()
val params = mapOf(
"spaceId" to spaceId,
)
return jdbcTemplate.query(sql, params, transactionRowMapper())
}
override fun findBySpaceIdAndId(spaceId: Int, id: Int): Transaction? {
val sql = """SELECT
t.id AS t_id,
t.parent_id AS t_parent_id,
t.space_id AS t_space_id,
t.type AS t_type,
t.kind AS t_kind,
t.comment AS t_comment,
t.amount AS t_amount,
t.fees AS t_fees,
t.date AS t_date,
t.is_deleted AS t_is_deleted,
t.is_done AS t_is_done,
t.created_at AS t_created_at,
t.updated_at AS t_updated_at,
c.id AS c_id,
c.type AS c_type,
c.name AS c_name,
c.description AS c_description,
c.icon AS c_icon,
c.is_deleted AS c_is_deleted,
c.created_at AS c_created_at,
c.updated_at AS c_updated_at,
u.id AS u_id,
u.username AS u_username,
u.first_name AS u_first_name
FROM finance.transactions t
JOIN finance.categories c ON t.category_id = c.id
JOIN finance.users u ON u.id = t.created_by_id
WHERE t.space_id = :spaceId and t.id = :id and t.is_deleted = false""".trimMargin()
val params = mapOf(
"spaceId" to spaceId,
"id" to id,
)
return jdbcTemplate.query(sql, params, transactionRowMapper()).firstOrNull()
}
override fun create(transaction: Transaction, userId: Int): Int {
val sql = """
INSERT INTO finance.transactions (space_id, parent_id, type, kind, category_id, comment, amount, fees, date, is_deleted, is_done, created_by_id) VALUES (
:spaceId,
:parentId,
:type,
:kind,
:categoryId,
:comment,
:amount,
:fees,
:date,
:is_deleted,
:is_done,
:createdById)
returning id
""".trimIndent()
val params = mapOf(
"spaceId" to transaction.space!!.id,
"parentId" to transaction.parent?.id,
"type" to transaction.type.name,
"kind" to transaction.kind.name,
"categoryId" to transaction.category.id,
"comment" to transaction.comment,
"amount" to transaction.amount,
"fees" to transaction.fees,
"date" to transaction.date,
"is_deleted" to transaction.isDeleted,
"is_done" to transaction.isDone,
"createdById" to userId
)
val createdTxId = jdbcTemplate.queryForObject(sql, params, Int::class.java)
transaction.id = createdTxId
return createdTxId!!
}
override fun update(transaction: Transaction): Int {
// val type: TransactionType = TransactionType.EXPENSE,
// val kind: TransactionKind = TransactionKind.INSTANT,
// val category: Int,
// val comment: String,
// val amount: BigDecimal,
// val fees: BigDecimal = BigDecimal.ZERO,
// val isDone: Boolean,
// val date: Instant
val sql = """
UPDATE finance.transactions
set type = :type,
kind = :kind,
category_id = :categoryId,
comment = :comment,
amount = :amount,
fees = :fees,
is_done = :is_done,
date = :date
where id = :id
""".trimIndent()
val params = mapOf(
"id" to transaction.id,
"type" to transaction.type.name,
"kind" to transaction.kind.name,
"categoryId" to transaction.category.id,
"comment" to transaction.comment,
"amount" to transaction.amount,
"fees" to transaction.fees,
"date" to transaction.date,
"is_done" to transaction.isDone,
)
jdbcTemplate.update(sql, params)
return transaction.id!!
}
override fun delete(transactionId: Int) {
val sql = """
update finance.transactions set is_deleted = true where id = :id
""".trimIndent()
val params = mapOf(
"id" to transactionId,
)
jdbcTemplate.update(sql, params)
}
}

View File

@@ -1,19 +1,16 @@
package space.luminic.finance.repos package space.luminic.finance.repos
import org.springframework.data.mongodb.repository.Query
import org.springframework.data.mongodb.repository.ReactiveMongoRepository
import org.springframework.stereotype.Repository import org.springframework.stereotype.Repository
import reactor.core.publisher.Mono
import space.luminic.finance.models.User import space.luminic.finance.models.User
@Repository @Repository
interface UserRepo : ReactiveMongoRepository<User, String> { interface UserRepo {
fun findAll(): List<User>
fun findById(id: Int): User?
@Query(value = "{ 'username': ?0 }", fields = "{ 'password': 0 }") fun findByUsername(username: String): User?
fun findByUsernameWOPassword(username: String): Mono<User> fun findParticipantsBySpace(spaceId: Int): Set<User>
fun findByTgId(tgId: String): User?
fun findByUsername(username: String): Mono<User> fun save(user: User): User
fun update(user: User): User
fun findByTgId(id: String): Mono<User> fun deleteById(id: Long)
} }

View File

@@ -0,0 +1,80 @@
package space.luminic.finance.repos
import org.springframework.jdbc.core.RowMapper
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate
import org.springframework.stereotype.Repository
import space.luminic.finance.models.User
@Repository
class UserRepoImpl(
private val jdbcTemplate: NamedParameterJdbcTemplate
) : UserRepo{
private fun userRowMapper() = RowMapper { rs, _ ->
User(
id = rs.getInt("id"),
username = rs.getString("username"),
firstName = rs.getString("first_name"),
tgId = rs.getString("tg_id"),
tgUserName = rs.getString("tg_user_name"),
password = rs.getString("password"),
isActive = rs.getBoolean("is_active"),
regDate = rs.getDate("reg_date").toLocalDate(),
createdAt = rs.getTimestamp("created_at").toInstant(),
updatedAt = rs.getTimestamp("updated_at").toInstant(),
roles = (rs.getArray("roles")?.array as? Array<String>)?.toList() ?: emptyList()
)
}
override fun findAll(): List<User> {
val sql = "select * from finance.users order by created_at desc"
return jdbcTemplate.query(sql, userRowMapper())
}
override fun findById(id: Int): User? {
val sql = "select * from finance.users where id = :userId"
return jdbcTemplate.queryForObject(sql, mapOf("userId" to id), userRowMapper())
}
override fun findByUsername(username: String): User? {
val sql = "select * from finance.users where username = :username"
return jdbcTemplate.query(sql, mapOf("username" to username), userRowMapper()).firstOrNull()
}
override fun findParticipantsBySpace(spaceId: Int): Set<User> {
val sql = "select * from finance.users u join finance.spaces_participants sp on sp.participants_id = u.id where sp.space_id = :spaceId"
return jdbcTemplate.query(sql, mapOf("spaceId" to spaceId), userRowMapper()).toSet()
}
override fun findByTgId(tgId: String): User? {
val sql = """
select * from finance.users u where tg_id = :tgId
""".trimIndent()
val params = mapOf("tgId" to tgId)
return jdbcTemplate.queryForObject(sql, params, userRowMapper())
}
override fun save(user: User): User {
val sql = "insert into finance.users(username, first_name, tg_id, tg_user_name, password, is_active, reg_date) values (:username, :firstname, :tg_id, :tg_user_name, :password, :isActive, :regDate) returning ID"
val params = mapOf(
"username" to user.username,
"firstname" to user.firstName,
"tg_id" to user.tgId,
"tg_user_name" to user.tgUserName,
"password" to user.password,
"isActive" to user.isActive,
"regDate" to user.regDate,
)
val savedId = jdbcTemplate.queryForObject(sql, params, Int::class.java)
user.id = savedId
return user
}
override fun update(user: User): User {
TODO("Not yet implemented")
}
override fun deleteById(id: Long) {
val sql = "update finance.users set is_active = false where id = :id"
jdbcTemplate.update(sql, mapOf("id" to id))
}
}

View File

@@ -1,14 +0,0 @@
package space.luminic.finance.services
import space.luminic.finance.dtos.AccountDTO
import space.luminic.finance.models.Account
import space.luminic.finance.models.Transaction
interface AccountService {
suspend fun getAccounts(spaceId: String): List<Account>
suspend fun getAccount(spaceId: String, accountId: String): Account
suspend fun getAccountTransactions(spaceId: String, accountId: String): List<Transaction>
suspend fun createAccount(spaceId: String, account: AccountDTO.CreateAccountDTO): Account
suspend fun updateAccount(spaceId: String, account: AccountDTO.UpdateAccountDTO): Account
suspend fun deleteAccount(spaceId: String, accountId: String)
}

View File

@@ -1,119 +0,0 @@
package space.luminic.finance.services
import kotlinx.coroutines.reactive.awaitSingle
import org.bson.Document
import org.bson.types.ObjectId
import org.springframework.data.mongodb.core.ReactiveMongoTemplate
import org.springframework.data.mongodb.core.aggregation.Aggregation.addFields
import org.springframework.data.mongodb.core.aggregation.Aggregation.lookup
import org.springframework.data.mongodb.core.aggregation.Aggregation.match
import org.springframework.data.mongodb.core.aggregation.Aggregation.newAggregation
import org.springframework.data.mongodb.core.aggregation.Aggregation.unwind
import org.springframework.data.mongodb.core.aggregation.AggregationOperation
import org.springframework.data.mongodb.core.aggregation.ConvertOperators
import org.springframework.data.mongodb.core.aggregation.LookupOperation
import org.springframework.data.mongodb.core.query.Criteria
import org.springframework.stereotype.Service
import space.luminic.finance.dtos.AccountDTO
import space.luminic.finance.models.Account
import space.luminic.finance.models.Transaction
import space.luminic.finance.repos.AccountRepo
@Service
class AccountServiceImpl(
private val accountRepo: AccountRepo,
private val mongoTemplate: ReactiveMongoTemplate,
private val spaceService: SpaceService,
private val transactionService: TransactionService
): AccountService {
private fun basicAggregation(spaceId: String): List<AggregationOperation> {
val addFieldsAsOJ = addFields()
.addField("createdByOI")
.withValue(ConvertOperators.valueOf("createdById").convertToObjectId())
.addField("updatedByOI")
.withValue(ConvertOperators.valueOf("updatedById").convertToObjectId())
.build()
val lookupCreatedBy = lookup("users", "createdByOI", "_id", "createdBy")
val unwindCreatedBy = unwind("createdBy")
val lookupUpdatedBy = lookup("users", "updatedByOI", "_id", "updatedBy")
val unwindUpdatedBy = unwind("updatedBy")
val matchCriteria = mutableListOf<Criteria>()
matchCriteria.add(Criteria.where("spaceId").`is`(spaceId))
matchCriteria.add(Criteria.where("isDeleted").`is`(false))
val matchStage = match(Criteria().andOperator(*matchCriteria.toTypedArray()))
return listOf(addFieldsAsOJ, lookupCreatedBy, unwindCreatedBy, lookupUpdatedBy, unwindUpdatedBy, matchStage)
}
override suspend fun getAccounts(spaceId: String): List<Account> {
val basicAggregation = basicAggregation(spaceId)
val aggregation = newAggregation(*basicAggregation.toTypedArray())
return mongoTemplate.aggregate(aggregation, "accounts", Account::class.java)
.collectList()
.awaitSingle()
}
override suspend fun getAccount(
spaceId: String,
accountId: String
): Account {
val basicAggregation = basicAggregation(spaceId)
val matchStage = match (Criteria.where("_id").`is`(ObjectId(accountId)))
val aggregation = newAggregation(matchStage, *basicAggregation.toTypedArray())
return mongoTemplate.aggregate(aggregation, "accounts", Account::class.java)
.awaitSingle()
}
override suspend fun getAccountTransactions(
spaceId: String,
accountId: String
): List<Transaction> {
val space = spaceService.checkSpace(spaceId)
val filter = TransactionService.TransactionsFilter(
accountId = accountId,
)
return transactionService.getTransactions(spaceId, filter, "date", "ASC")
}
override suspend fun createAccount(
spaceId: String,
account: AccountDTO.CreateAccountDTO
): Account {
val createdAccount = Account(
type = account.type,
spaceId = spaceId,
name = account.name,
currencyCode = account.currencyCode,
amount = account.amount,
goalId = account.goalId,
)
return accountRepo.save(createdAccount).awaitSingle()
}
override suspend fun updateAccount(
spaceId: String,
account: AccountDTO.UpdateAccountDTO
): Account {
val existingAccount = getAccount(spaceId, account.id)
val newAccount = existingAccount.copy(
name = account.name,
type = account.type,
currencyCode = account.currencyCode,
amount = account.amount,
goalId = account.goalId,
)
return accountRepo.save(newAccount).awaitSingle()
}
override suspend fun deleteAccount(spaceId: String, accountId: String) {
val existingAccount = getAccount(spaceId, accountId)
existingAccount.isDeleted = true
accountRepo.save(existingAccount).awaitSingle()
}
}

View File

@@ -1,10 +1,7 @@
package space.luminic.finance.services package space.luminic.finance.services
import kotlinx.coroutines.reactive.awaitFirstOrNull
import kotlinx.coroutines.reactor.awaitSingle
import kotlinx.coroutines.reactor.awaitSingleOrNull
import org.springframework.cache.annotation.Cacheable import org.springframework.cache.annotation.Cacheable
import org.springframework.security.core.context.ReactiveSecurityContextHolder import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.core.userdetails.UsernameNotFoundException import org.springframework.security.core.userdetails.UsernameNotFoundException
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
@@ -13,14 +10,13 @@ import space.luminic.finance.models.Token
import space.luminic.finance.models.User import space.luminic.finance.models.User
import space.luminic.finance.repos.UserRepo import space.luminic.finance.repos.UserRepo
import space.luminic.finance.utils.JWTUtil import space.luminic.finance.utils.JWTUtil
import java.time.LocalDateTime import java.time.Instant
import java.time.ZoneId
import java.util.* import java.util.*
@Service @Service
class AuthService( class AuthService(
private val userRepository: UserRepo, private val userRepo: UserRepo,
private val tokenService: TokenService, private val tokenService: TokenService,
private val jwtUtil: JWTUtil, private val jwtUtil: JWTUtil,
private val userService: UserService, private val userService: UserService,
@@ -28,18 +24,28 @@ class AuthService(
) { ) {
private val passwordEncoder = BCryptPasswordEncoder() private val passwordEncoder = BCryptPasswordEncoder()
suspend fun getSecurityUser(): User { fun getSecurityUser(): User {
val securityContextHolder = ReactiveSecurityContextHolder.getContext().awaitSingleOrNull() val securityContextHolder = SecurityContextHolder.getContext()
?: throw AuthException("Authentication failed") ?: throw AuthException("Authentication failed")
val authentication = securityContextHolder.authentication val authentication = securityContextHolder.authentication
val username = authentication.name val username = authentication.name
// Получаем пользователя по имени // Получаем пользователя по имени
return userService.getByUsername(username) return userService.getById(username.toInt())
} }
suspend fun login(username: String, password: String): String { fun getSecurityUserId(): Int {
val user = userRepository.findByUsername(username).awaitFirstOrNull() val securityContextHolder = SecurityContextHolder.getContext()
?: throw AuthException("Authentication failed")
val authentication = securityContextHolder.authentication
val username = authentication.name
// Получаем пользователя по имени
return username.toInt()
}
fun login(username: String, password: String): String {
val user = userRepo.findByUsername(username)
?: throw UsernameNotFoundException("Пользователь не найден") ?: throw UsernameNotFoundException("Пользователь не найден")
return if (passwordEncoder.matches(password, user.password)) { return if (passwordEncoder.matches(password, user.password)) {
val token = jwtUtil.generateToken(user.username) val token = jwtUtil.generateToken(user.username)
@@ -47,10 +53,7 @@ class AuthService(
tokenService.saveToken( tokenService.saveToken(
token = token, token = token,
username = username, username = username,
expiresAt = LocalDateTime.ofInstant( expiresAt = expireAt.toInstant()
expireAt.toInstant(),
ZoneId.systemDefault()
)
) )
token token
} else { } else {
@@ -58,26 +61,23 @@ class AuthService(
} }
} }
suspend fun tgLogin(tgId: String): String { fun tgLogin(tgId: String): String {
val user = val user =
userRepository.findByTgId(tgId).awaitSingleOrNull() ?: throw UsernameNotFoundException("Пользователь не найден") userRepo.findByTgId(tgId) ?: throw UsernameNotFoundException("Пользователь не найден")
val token = jwtUtil.generateToken(user.username) val token = jwtUtil.generateToken(user.username)
val expireAt = Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24 * 10) val expireAt = Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24 * 10)
tokenService.saveToken( tokenService.saveToken(
token = token, token = token,
username = user.username, username = user.username,
expiresAt = LocalDateTime.ofInstant( expiresAt = expireAt.toInstant()
expireAt.toInstant(),
ZoneId.systemDefault()
)
) )
return token return token
} }
suspend fun register(username: String, password: String, firstName: String): User { fun register(username: String, password: String, firstName: String): User {
val user = userRepository.findByUsername(username).awaitSingleOrNull() val user = userRepo.findByUsername(username)
if (user == null) { if (user == null) {
var newUser = User( var newUser = User(
username = username, username = username,
@@ -85,18 +85,18 @@ class AuthService(
firstName = firstName, firstName = firstName,
roles = mutableListOf("USER") roles = mutableListOf("USER")
) )
newUser = userRepository.save(newUser).awaitSingle() newUser = userRepo.save(newUser)
return newUser return newUser
} else throw IllegalArgumentException("Пользователь уже зарегистрирован") } else throw IllegalArgumentException("Пользователь уже зарегистрирован")
} }
@Cacheable(cacheNames = ["tokens"], key = "#token") @Cacheable(cacheNames = ["tokens"], key = "#token")
suspend fun isTokenValid(token: String): User { fun isTokenValid(token: String): User {
val tokenDetails = tokenService.getToken(token).awaitFirstOrNull() ?: throw AuthException("Токен не валиден") val tokenDetails = tokenService.getToken(token)
when { when {
tokenDetails.status == Token.TokenStatus.ACTIVE && tokenDetails.expiresAt.isAfter(LocalDateTime.now()) -> { tokenDetails.status == Token.TokenStatus.ACTIVE && tokenDetails.expiresAt.isAfter(Instant.now()) -> {
return userService.getByUsername(tokenDetails.username) return tokenDetails.user
} }
else -> { else -> {

View File

@@ -1,18 +0,0 @@
package space.luminic.finance.services
import space.luminic.finance.dtos.BudgetDTO.*
import space.luminic.finance.models.Budget
import space.luminic.finance.models.Transaction
interface BudgetService {
suspend fun getBudgets(spaceId: String, sortBy: String, sortDirection: String): List<Budget>
suspend fun getBudget(spaceId: String, budgetId: String): Budget
suspend fun getBudgetTransactions(spaceId: String, budgetId: String): List<Transaction>
suspend fun createBudget(spaceId: String, type: Budget.BudgetType, budgetDto: CreateBudgetDTO): Budget
suspend fun updateBudget(spaceId: String, budgetDto: UpdateBudgetDTO): Budget
suspend fun deleteBudget(spaceId: String, budgetId: String)
}

View File

@@ -1,188 +0,0 @@
package space.luminic.finance.services
import kotlinx.coroutines.reactive.awaitFirstOrNull
import kotlinx.coroutines.reactive.awaitSingle
import org.bson.Document
import org.bson.types.ObjectId
import org.springframework.data.domain.Sort
import org.springframework.data.domain.Sort.Direction
import org.springframework.data.mongodb.core.ReactiveMongoTemplate
import org.springframework.data.mongodb.core.aggregation.Aggregation.*
import org.springframework.data.mongodb.core.aggregation.Aggregation.newAggregation
import org.springframework.data.mongodb.core.aggregation.Aggregation.sort
import org.springframework.data.mongodb.core.aggregation.AggregationOperation
import org.springframework.data.mongodb.core.aggregation.ConvertOperators
import org.springframework.data.mongodb.core.aggregation.LookupOperation
import org.springframework.data.mongodb.core.aggregation.SetOperation.set
import org.springframework.data.mongodb.core.aggregation.UnsetOperation.unset
import org.springframework.data.mongodb.core.aggregation.VariableOperators
import org.springframework.data.mongodb.core.query.Criteria
import org.springframework.stereotype.Service
import space.luminic.finance.dtos.BudgetDTO
import space.luminic.finance.models.Budget
import space.luminic.finance.models.NotFoundException
import space.luminic.finance.models.Transaction
import space.luminic.finance.repos.BudgetRepo
import java.math.BigDecimal
@Service
class BudgetServiceImpl(
private val budgetRepo: BudgetRepo,
private val authService: AuthService,
private val categoryService: CategoryService,
private val mongoTemplate: ReactiveMongoTemplate,
private val spaceService: SpaceService,
private val transactionService: TransactionService,
) : BudgetService {
private fun basicAggregation(spaceId: String): List<AggregationOperation> {
val unwindCategories = unwind("categories", true)
val setCategoryIdOI = set("categories.categoryIdOI")
.toValue(ConvertOperators.valueOf("categories.categoryId").convertToObjectId())
val lookupCategory = lookup(
"categories", // from
"categories.categoryIdOI", // localField
"_id", // foreignField
"joinedCategory" // as
)
val unwindJoinedCategory = unwind("joinedCategory", true)
val setEmbeddedCategory = set("categories.category").toValue("\$joinedCategory")
val unsetTemps = unset("joinedCategory", "categories.categoryIdOI")
val groupBack: AggregationOperation = AggregationOperation {
Document(
"\$group", Document()
.append("_id", "\$_id")
.append("doc", Document("\$first", "\$\$ROOT"))
.append("categories", Document("\$push", "\$categories"))
)
}
val setDocCategories: AggregationOperation = AggregationOperation {
Document("\$set", Document("doc.categories", "\$categories"))
}
val replaceRootDoc = replaceRoot("doc")
val addFieldsAsOJ = addFields()
.addField("createdByOI")
.withValue(ConvertOperators.valueOf("createdById").convertToObjectId())
.addField("updatedByOI")
.withValue(ConvertOperators.valueOf("updatedById").convertToObjectId())
.build()
val lookupCreatedBy = lookup("users", "createdByOI", "_id", "createdBy")
val unwindCreatedBy = unwind("createdBy")
val lookupUpdatedBy = lookup("users", "updatedByOI", "_id", "updatedBy")
val unwindUpdatedBy = unwind("updatedBy")
val matchCriteria = mutableListOf<Criteria>()
matchCriteria.add(Criteria.where("spaceId").`is`(spaceId))
matchCriteria.add(Criteria.where("isDeleted").`is`(false))
val matchStage = match(Criteria().andOperator(*matchCriteria.toTypedArray()))
return listOf(matchStage,
unwindCategories,
setCategoryIdOI,
lookupCategory,
unwindJoinedCategory,
setEmbeddedCategory,
unsetTemps,
groupBack,
setDocCategories,
replaceRootDoc,
addFieldsAsOJ,
lookupCreatedBy,
unwindCreatedBy,
lookupUpdatedBy,
unwindUpdatedBy)
}
override suspend fun getBudgets(spaceId: String, sortBy: String, sortDirection: String): List<Budget> {
require(spaceId.isNotBlank()) { "Space ID must not be blank" }
val allowedSortFields = setOf("dateFrom", "dateTo", "amount", "categoryName", "createdAt")
require(sortBy in allowedSortFields) { "Invalid sort field: $sortBy" }
val direction = when (sortDirection.uppercase()) {
"ASC" -> Direction.ASC
"DESC" -> Direction.DESC
else -> throw IllegalArgumentException("Sort direction must be 'ASC' or 'DESC'")
}
val sort = sort(Sort.by(direction, sortBy))
val basicAggregation = basicAggregation(spaceId)
val aggregation =
newAggregation(
*basicAggregation.toTypedArray(),
sort
)
return mongoTemplate.aggregate(aggregation, "budgets", Budget::class.java)
.collectList()
.awaitSingle()
}
override suspend fun getBudget(spaceId: String, budgetId: String): Budget {
val basicAggregation = basicAggregation(spaceId)
val matchStage = match(Criteria.where("_id").`is`(ObjectId(budgetId)))
val aggregation = newAggregation(matchStage, *basicAggregation.toTypedArray(), )
return mongoTemplate.aggregate(aggregation, "budgets", Budget::class.java).awaitFirstOrNull()
?: throw NotFoundException("Budget not found")
}
override suspend fun getBudgetTransactions(
spaceId: String,
budgetId: String
): List<Transaction> {
spaceService.checkSpace(spaceId)
val budget = getBudget(spaceId, budgetId)
val filter = TransactionService.TransactionsFilter(
dateFrom = budget.dateFrom,
dateTo = budget.dateTo
)
return transactionService.getTransactions(spaceId, filter, "date", "ASC")
}
override suspend fun createBudget(
spaceId: String,
type: Budget.BudgetType,
budgetDto: BudgetDTO.CreateBudgetDTO
): Budget {
val user = authService.getSecurityUser()
val categories = categoryService.getCategories(spaceId)
val budget = Budget(
spaceId = spaceId,
type = type,
name = budgetDto.name,
description = budgetDto.description,
categories = categories.map { Budget.BudgetCategory(it.id!!, BigDecimal.ZERO) },
dateFrom = budgetDto.dateFrom,
dateTo = budgetDto.dateTo
)
return budgetRepo.save(budget).awaitSingle()
}
override suspend fun updateBudget(
spaceId: String,
budgetDto: BudgetDTO.UpdateBudgetDTO
): Budget {
val budget = getBudget(spaceId, budgetDto.id)
budgetDto.name?.let { name -> budget.name = name }
budgetDto.description?.let { description -> budget.description = description }
budgetDto.dateFrom?.let { dateFrom -> budget.dateFrom = dateFrom }
budgetDto.dateTo?.let { dateTo -> budget.dateTo = dateTo }
return budgetRepo.save(budget).awaitSingle()
}
override suspend fun deleteBudget(spaceId: String, budgetId: String) {
val budget = getBudget(spaceId, budgetId)
budget.isDeleted = true
budgetRepo.save(budget).awaitSingle()
}
}

View File

@@ -1,15 +1,13 @@
package space.luminic.finance.services package space.luminic.finance.services
import space.luminic.finance.dtos.BudgetDTO
import space.luminic.finance.dtos.CategoryDTO import space.luminic.finance.dtos.CategoryDTO
import space.luminic.finance.models.Category import space.luminic.finance.models.Category
import space.luminic.finance.models.Space
interface CategoryService { interface CategoryService {
suspend fun getCategories(spaceId: String): List<Category> fun getCategories(spaceId: Int): List<Category>
suspend fun getCategory(spaceId: String, id: String): Category fun getCategory(spaceId: Int, id: Int): Category
suspend fun createCategory(spaceId: String, category: CategoryDTO.CreateCategoryDTO): Category fun createCategory(spaceId: Int, category: CategoryDTO.CreateCategoryDTO): Category
suspend fun updateCategory(spaceId: String,category: CategoryDTO.UpdateCategoryDTO): Category fun createEtalonCategoriesForSpace(spaceId: Int): List<Category>
suspend fun deleteCategory(spaceId: String, id: String) fun updateCategory(spaceId: Int,categoryId:Int, category: CategoryDTO.UpdateCategoryDTO): Category
suspend fun createCategoriesForSpace(spaceId: String): List<Category> fun deleteCategory(spaceId: Int, id: Int)
} }

View File

@@ -1,108 +1,105 @@
package space.luminic.finance.services package space.luminic.finance.services
import kotlinx.coroutines.reactive.awaitSingle import org.springframework.jdbc.core.JdbcTemplate
import org.bson.types.ObjectId
import org.springframework.data.mongodb.core.ReactiveMongoTemplate
import org.springframework.data.mongodb.core.aggregation.Aggregation.addFields
import org.springframework.data.mongodb.core.aggregation.Aggregation.lookup
import org.springframework.data.mongodb.core.aggregation.Aggregation.match
import org.springframework.data.mongodb.core.aggregation.Aggregation.newAggregation
import org.springframework.data.mongodb.core.aggregation.Aggregation.unwind
import org.springframework.data.mongodb.core.aggregation.AggregationOperation
import org.springframework.data.mongodb.core.aggregation.ConvertOperators
import org.springframework.data.mongodb.core.query.Criteria
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Propagation
import org.springframework.transaction.annotation.Transactional
import space.luminic.finance.dtos.CategoryDTO import space.luminic.finance.dtos.CategoryDTO
import space.luminic.finance.models.Category import space.luminic.finance.models.Category
import space.luminic.finance.models.Space import space.luminic.finance.models.NotFoundException
import space.luminic.finance.repos.CategoryEtalonRepo import space.luminic.finance.repos.CategoryEtalonRepo
import space.luminic.finance.repos.CategoryRepo import space.luminic.finance.repos.CategoryRepo
import space.luminic.finance.repos.SpaceRepo
@Service @Service
class CategoryServiceImpl( class CategoryServiceImpl(
private val categoryRepo: CategoryRepo, private val spaceRepo: SpaceRepo,
private val categoryEtalonRepo: CategoryEtalonRepo, private val categoriesRepo: CategoryRepo,
private val reactiveMongoTemplate: ReactiveMongoTemplate, private val categoriesEtalonRepo: CategoryEtalonRepo,
private val authService: AuthService, private val authService: AuthService
) : CategoryService { ) : CategoryService {
override fun getCategories(spaceId: Int): List<Category> {
private fun basicAggregation(spaceId: String): List<AggregationOperation> { val userId = authService.getSecurityUserId()
val addFieldsAsOJ = addFields() val space = spaceRepo.findSpaceById(spaceId, userId)
.addField("createdByOI") return categoriesRepo.findBySpaceId(spaceId)
.withValue(ConvertOperators.valueOf("createdById").convertToObjectId())
.addField("updatedByOI")
.withValue(ConvertOperators.valueOf("updatedById").convertToObjectId())
.build()
val lookupCreatedBy = lookup("users", "createdByOI", "_id", "createdBy")
val unwindCreatedBy = unwind("createdBy")
val lookupUpdatedBy = lookup("users", "updatedByOI", "_id", "updatedBy")
val unwindUpdatedBy = unwind("updatedBy")
val matchCriteria = mutableListOf<Criteria>()
matchCriteria.add(Criteria.where("spaceId").`is`(spaceId))
matchCriteria.add(Criteria.where("isDeleted").`is`(false))
val matchStage = match(Criteria().andOperator(*matchCriteria.toTypedArray()))
return listOf(addFieldsAsOJ, lookupCreatedBy, unwindCreatedBy, lookupUpdatedBy, unwindUpdatedBy, matchStage)
} }
override suspend fun getCategories(spaceId: String): List<Category> { override fun getCategory(spaceId: Int, id: Int): Category {
val basicAggregation = basicAggregation(spaceId) val userId = authService.getSecurityUserId()
val aggregation = newAggregation(*basicAggregation.toTypedArray()) val space = spaceRepo.findSpaceById(spaceId, userId)
return reactiveMongoTemplate.aggregate(aggregation, "categories", Category::class.java).collectList().awaitSingle() return categoriesRepo.findBySpaceIdAndId(spaceId, id)
?: throw NotFoundException("Category with id $id not found")
} }
@Transactional
override suspend fun getCategory(spaceId: String, id: String): Category { override fun createCategory(
val basicAggregation = basicAggregation(spaceId) spaceId: Int,
val match = match(Criteria.where("_id").`is`(ObjectId(id)))
val aggregation = newAggregation(*basicAggregation.toTypedArray(), match)
return reactiveMongoTemplate.aggregate(aggregation, "categories", Category::class.java).awaitSingle()
}
override suspend fun createCategory(
spaceId: String,
category: CategoryDTO.CreateCategoryDTO category: CategoryDTO.CreateCategoryDTO
): Category { ): Category {
val createdCategory = Category( val userId = authService.getSecurityUserId()
spaceId = spaceId, val space = spaceRepo.findSpaceById(spaceId, userId)
type = category.type, val newCategory = Category(
space = space,
name = category.name, name = category.name,
icon = category.icon
)
return categoryRepo.save(createdCategory).awaitSingle()
}
override suspend fun updateCategory(
spaceId: String,
category: CategoryDTO.UpdateCategoryDTO
): Category {
val existingCategory = getCategory(spaceId, category.id)
val updatedCategory = existingCategory.copy(
type = category.type, type = category.type,
name = category.name, description = category.description,
icon = category.icon, icon = category.icon,
) )
return categoryRepo.save(updatedCategory).awaitSingle() return categoriesRepo.create(newCategory, userId)
} }
override suspend fun deleteCategory(spaceId: String, id: String) { @Transactional(propagation = Propagation.NESTED)
val existingCategory = getCategory(spaceId, id) override fun createEtalonCategoriesForSpace(
existingCategory.isDeleted = true spaceId: Int
categoryRepo.save(existingCategory).awaitSingle() ): List<Category> {
} val userId = authService.getSecurityUserId()
val space = spaceRepo.findSpaceById(spaceId, userId)
override suspend fun createCategoriesForSpace(spaceId: String): List<Category> { val categories = categoriesEtalonRepo.findAll()
val etalonCategories = categoryEtalonRepo.findAll().collectList().awaitSingle() val newCategories = mutableListOf<Category>()
val toCreate = etalonCategories.map { categories.forEach { category ->
Category( newCategories.add(
spaceId = spaceId, categoriesRepo.create(
type = it.type, Category(
name = it.name, space = space,
icon = it.icon name = category.name,
description = category.description,
type = category.type,
icon = category.icon,
), userId
)
) )
} }
return categoryRepo.saveAll(toCreate).collectList().awaitSingle()
return newCategories
}
@Transactional
override fun updateCategory(
spaceId: Int,
categoryId: Int,
category: CategoryDTO.UpdateCategoryDTO
): Category {
val userId = authService.getSecurityUserId()
val space = spaceRepo.findSpaceById(spaceId, userId)
val existingCategory = getCategory(spaceId, categoryId)
val newCategory = Category(
id = existingCategory.id,
space = space,
name = category.name,
description = category.description,
type = category.type,
icon = category.icon,
isDeleted = existingCategory.isDeleted,
createdBy = existingCategory.createdBy,
createdAt = existingCategory.createdAt,
)
return categoriesRepo.update(newCategory, userId)
}
@Transactional
override fun deleteCategory(spaceId: Int, id: Int) {
val userId = authService.getSecurityUserId()
val space = spaceRepo.findSpaceById(spaceId, userId)
categoriesRepo.delete(id)
} }

View File

@@ -0,0 +1,108 @@
//package space.luminic.finance.services
//
//import kotlinx.coroutines.reactive.awaitSingle
//import org.bson.types.ObjectId
//import org.springframework.data.mongodb.core.ReactiveMongoTemplate
//import org.springframework.data.mongodb.core.aggregation.Aggregation.addFields
//import org.springframework.data.mongodb.core.aggregation.Aggregation.lookup
//import org.springframework.data.mongodb.core.aggregation.Aggregation.match
//import org.springframework.data.mongodb.core.aggregation.Aggregation.newAggregation
//import org.springframework.data.mongodb.core.aggregation.Aggregation.unwind
//import org.springframework.data.mongodb.core.aggregation.AggregationOperation
//import org.springframework.data.mongodb.core.aggregation.ConvertOperators
//import org.springframework.data.mongodb.core.query.Criteria
//import org.springframework.stereotype.Service
//import space.luminic.finance.dtos.CategoryDTO
//import space.luminic.finance.models.Category
//import space.luminic.finance.repos.CategoryEtalonRepo
//import space.luminic.finance.repos.CategoryRepo
//
//@Service
//class CategoryServiceMongoImpl(
// private val categoryRepo: CategoryRepo,
// private val categoryEtalonRepo: CategoryEtalonRepo,
// private val reactiveMongoTemplate: ReactiveMongoTemplate,
// private val authService: AuthService,
//) : CategoryService {
//
// private fun basicAggregation(spaceId: String): List<AggregationOperation> {
// val addFieldsAsOJ = addFields()
// .addField("createdByOI")
// .withValue(ConvertOperators.valueOf("createdById").convertToObjectId())
// .addField("updatedByOI")
// .withValue(ConvertOperators.valueOf("updatedById").convertToObjectId())
// .build()
// val lookupCreatedBy = lookup("users", "createdByOI", "_id", "createdBy")
// val unwindCreatedBy = unwind("createdBy")
//
// val lookupUpdatedBy = lookup("users", "updatedByOI", "_id", "updatedBy")
// val unwindUpdatedBy = unwind("updatedBy")
// val matchCriteria = mutableListOf<Criteria>()
// matchCriteria.add(Criteria.where("spaceId").`is`(spaceId))
// matchCriteria.add(Criteria.where("isDeleted").`is`(false))
// val matchStage = match(Criteria().andOperator(*matchCriteria.toTypedArray()))
//
// return listOf(addFieldsAsOJ, lookupCreatedBy, unwindCreatedBy, lookupUpdatedBy, unwindUpdatedBy, matchStage)
// }
//
// override suspend fun getCategories(spaceId: String): List<Category> {
// val basicAggregation = basicAggregation(spaceId)
// val aggregation = newAggregation(*basicAggregation.toTypedArray())
// return reactiveMongoTemplate.aggregate(aggregation, "categories", Category::class.java).collectList().awaitSingle()
// }
//
// override suspend fun getCategory(spaceId: String, id: String): Category {
// val basicAggregation = basicAggregation(spaceId)
// val match = match(Criteria.where("_id").`is`(ObjectId(id)))
// val aggregation = newAggregation(*basicAggregation.toTypedArray(), match)
// return reactiveMongoTemplate.aggregate(aggregation, "categories", Category::class.java).awaitSingle()
// }
//
//
// override suspend fun createCategory(
// spaceId: String,
// category: CategoryDTO.CreateCategoryDTO
// ): Category {
// val createdCategory = Category(
// spaceId = spaceId,
// type = category.type,
// name = category.name,
// icon = category.icon
// )
// return categoryRepo.save(createdCategory).awaitSingle()
// }
//
// override suspend fun updateCategory(
// spaceId: String,
// category: CategoryDTO.UpdateCategoryDTO
// ): Category {
// val existingCategory = getCategory(spaceId, category.id)
// val updatedCategory = existingCategory.copy(
// type = category.type,
// name = category.name,
// icon = category.icon,
// )
// return categoryRepo.save(updatedCategory).awaitSingle()
// }
//
// override suspend fun deleteCategory(spaceId: String, id: String) {
// val existingCategory = getCategory(spaceId, id)
// existingCategory.isDeleted = true
// categoryRepo.save(existingCategory).awaitSingle()
// }
//
// override suspend fun createCategoriesForSpace(spaceId: String): List<Category> {
// val etalonCategories = categoryEtalonRepo.findAll().collectList().awaitSingle()
// val toCreate = etalonCategories.map {
// Category(
// spaceId = spaceId,
// type = it.type,
// name = it.name,
// icon = it.icon
// )
// }
// return categoryRepo.saveAll(toCreate).collectList().awaitSingle()
// }
//
//
//}

View File

@@ -1,16 +0,0 @@
package space.luminic.finance.services
import kotlinx.coroutines.reactor.mono
import org.springframework.data.domain.ReactiveAuditorAware
import org.springframework.stereotype.Component
import reactor.core.publisher.Mono
@Component
class CoroutineAuditorAware(
private val authService: AuthService
) : ReactiveAuditorAware<String> {
override fun getCurrentAuditor(): Mono<String> =
mono {
authService.getSecurityUser().id!!
}
}

View File

@@ -6,11 +6,11 @@ import space.luminic.finance.models.CurrencyRate
interface CurrencyService { interface CurrencyService {
suspend fun getCurrencies(): List<Currency> fun getCurrencies(): List<Currency>
suspend fun getCurrency(currencyCode: String): Currency fun getCurrency(currencyCode: String): Currency
suspend fun createCurrency(currency: CurrencyDTO): Currency fun createCurrency(currency: CurrencyDTO): Currency
suspend fun updateCurrency(currency: CurrencyDTO): Currency fun updateCurrency(currency: CurrencyDTO): Currency
suspend fun deleteCurrency(currencyCode: String) fun deleteCurrency(currencyCode: String)
suspend fun createCurrencyRate(currencyCode: String): CurrencyRate fun createCurrencyRate(currencyCode: String): CurrencyRate
} }

View File

@@ -1,11 +1,10 @@
package space.luminic.finance.services package space.luminic.finance.services
import kotlinx.coroutines.reactive.awaitSingle
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import space.luminic.finance.dtos.CurrencyDTO import space.luminic.finance.dtos.CurrencyDTO
import space.luminic.finance.models.Currency import space.luminic.finance.models.Currency
import space.luminic.finance.models.CurrencyRate import space.luminic.finance.models.CurrencyRate
import space.luminic.finance.repos.CurrencyRateRepo import space.luminic.finance.models.NotFoundException
import space.luminic.finance.repos.CurrencyRepo import space.luminic.finance.repos.CurrencyRepo
import java.math.BigDecimal import java.math.BigDecimal
import java.time.LocalDate import java.time.LocalDate
@@ -13,39 +12,39 @@ import java.time.LocalDate
@Service @Service
class CurrencyServiceImpl( class CurrencyServiceImpl(
private val currencyRepo: CurrencyRepo, private val currencyRepo: CurrencyRepo,
private val currencyRateRepo: CurrencyRateRepo
) : CurrencyService { ) : CurrencyService {
override fun getCurrencies(): List<Currency> {
override suspend fun getCurrencies(): List<Currency> { return currencyRepo.findAll()
return currencyRepo.findAll().collectList().awaitSingle()
} }
override suspend fun getCurrency(currencyCode: String): Currency { override fun getCurrency(currencyCode: String): Currency {
return currencyRepo.findById(currencyCode).awaitSingle() return currencyRepo.findByCode(currencyCode)
?: throw NotFoundException("Currency code $currencyCode not found")
} }
override suspend fun createCurrency(currency: CurrencyDTO): Currency { override fun createCurrency(currency: CurrencyDTO): Currency {
val createdCurrency = Currency(currency.code, currency.name, currency.symbol) val currency = Currency(currency.code, currency.name, currency.code)
return currencyRepo.save(createdCurrency).awaitSingle() return currencyRepo.save(currency)
} }
override suspend fun updateCurrency(currency: CurrencyDTO): Currency { override fun updateCurrency(currency: CurrencyDTO): Currency {
val existingCurrency = currencyRepo.findById(currency.code).awaitSingle() getCurrency(currency.code)
val newCurrency = existingCurrency.copy(name = currency.name, symbol = currency.symbol) val updatedCurrency =
return currencyRepo.save(newCurrency).awaitSingle() Currency(
} code = currency.code,
name = currency.name,
override suspend fun deleteCurrency(currencyCode: String) { symbol = currency.symbol
currencyRepo.deleteById(currencyCode).awaitSingle()
}
override suspend fun createCurrencyRate(currencyCode: String): CurrencyRate {
return currencyRateRepo.save(
CurrencyRate(
currencyCode = currencyCode,
rate = BigDecimal(12.0),
date = LocalDate.now(),
) )
).awaitSingle() return currencyRepo.save(updatedCurrency)
}
override fun deleteCurrency(currencyCode: String) {
currencyRepo.delete(currencyCode)
}
override fun createCurrencyRate(currencyCode: String): CurrencyRate {
print("createCurrencyRate")
val currency = getCurrency(currencyCode)
return CurrencyRate(currency = currency, rate = BigDecimal.ZERO, date = LocalDate.now())
} }
} }

View File

@@ -0,0 +1,51 @@
//package space.luminic.finance.services
//
//import kotlinx.coroutines.reactive.awaitSingle
//import org.springframework.stereotype.Service
//import space.luminic.finance.dtos.CurrencyDTO
//import space.luminic.finance.models.Currency
//import space.luminic.finance.models.CurrencyRate
//import space.luminic.finance.repos.CurrencyRateRepo
//import space.luminic.finance.repos.CurrencyRepo
//import java.math.BigDecimal
//import java.time.LocalDate
//
//@Service
//class CurrencyServiceMongoImpl(
// private val currencyRepo: CurrencyRepo,
// private val currencyRateRepo: CurrencyRateRepo
//) : CurrencyService {
//
// override suspend fun getCurrencies(): List<Currency> {
// return currencyRepo.findAll().collectList().awaitSingle()
// }
//
// override suspend fun getCurrency(currencyCode: String): Currency {
// return currencyRepo.findById(currencyCode).awaitSingle()
// }
//
// override suspend fun createCurrency(currency: CurrencyDTO): Currency {
// val createdCurrency = Currency(currency.code, currency.name, currency.symbol)
// return currencyRepo.save(createdCurrency).awaitSingle()
// }
//
// override suspend fun updateCurrency(currency: CurrencyDTO): Currency {
// val existingCurrency = currencyRepo.findById(currency.code).awaitSingle()
// val newCurrency = existingCurrency.copy(name = currency.name, symbol = currency.symbol)
// return currencyRepo.save(newCurrency).awaitSingle()
// }
//
// override suspend fun deleteCurrency(currencyCode: String) {
// currencyRepo.deleteById(currencyCode).awaitSingle()
// }
//
// override suspend fun createCurrencyRate(currencyCode: String): CurrencyRate {
// return currencyRateRepo.save(
// CurrencyRate(
// currencyCode = currencyCode,
// rate = BigDecimal(12.0),
// date = LocalDate.now(),
// )
// ).awaitSingle()
// }
//}

View File

@@ -0,0 +1,22 @@
package space.luminic.finance.services
import space.luminic.finance.dtos.GoalDTO
import space.luminic.finance.models.Goal
interface GoalService {
fun findAllBySpaceId(spaceId: Int): List<Goal>
fun findBySpaceIdAndId(spaceId: Int, id: Int): Goal
fun create(spaceId: Int,goal: GoalDTO.CreateGoalDTO): Int
fun update(spaceId: Int, goalId: Int, goal: GoalDTO.UpdateGoalDTO)
fun delete(spaceId: Int, id: Int)
fun getComponents(spaceId: Int, goalId: Int): List<Goal.GoalComponent>
fun getComponent(spaceId: Int, goalId: Int, id: Int): Goal.GoalComponent?
fun createComponent(spaceId: Int, goalId: Int, component: Goal.GoalComponent): Int
fun updateComponent(spaceId: Int, goalId: Int, component: Goal.GoalComponent)
fun deleteComponent(spaceId: Int, goalId: Int, id: Int)
fun assignTransaction(spaceId: Int, goalId: Int, transactionId: Int)
fun refuseTransaction(spaceId: Int,goalId: Int, transactionId: Int)
}

View File

@@ -0,0 +1,140 @@
package space.luminic.finance.services
import org.springframework.stereotype.Service
import space.luminic.finance.dtos.GoalDTO
import space.luminic.finance.models.Goal
import space.luminic.finance.models.NotFoundException
import space.luminic.finance.repos.GoalRepo
import space.luminic.finance.repos.SpaceRepo
import space.luminic.finance.repos.TransactionRepo
@Service
class GoalServiceImpl(
private val goalRepo: GoalRepo,
private val spaceRepo: SpaceRepo,
private val authService: AuthService,
private val transactionRepo: TransactionRepo
) : GoalService {
override fun findAllBySpaceId(spaceId: Int): List<Goal> {
val userId = authService.getSecurityUserId()
spaceRepo.findSpaceById(spaceId, userId)
return goalRepo.findAllBySpaceId(spaceId)
}
override fun findBySpaceIdAndId(spaceId: Int, id: Int): Goal {
val userId = authService.getSecurityUserId()
spaceRepo.findSpaceById(spaceId, userId)
return goalRepo.findBySpaceIdAndId(spaceId, userId) ?: throw NotFoundException("Goal $id not found")
}
override fun create(spaceId: Int, goal: GoalDTO.CreateGoalDTO): Int {
val userId = authService.getSecurityUserId()
spaceRepo.findSpaceById(spaceId, userId)
val creatingGoal = Goal(
type = goal.type,
name = goal.name,
amount = goal.amount,
untilDate = goal.date
)
return goalRepo.create(creatingGoal, userId)
}
override fun update(spaceId: Int, goalId: Int, goal: GoalDTO.UpdateGoalDTO) {
val userId = authService.getSecurityUserId()
spaceRepo.findSpaceById(spaceId, userId)
val existingGoal =
goalRepo.findBySpaceIdAndId(spaceId, goalId) ?: throw NotFoundException("Goal $goalId not found")
val updatedGoal = existingGoal.copy(
type = goal.type,
name = goal.name,
description = goal.description,
amount = goal.amount,
untilDate = goal.date
)
goalRepo.update(updatedGoal, userId)
}
override fun delete(spaceId: Int, id: Int) {
goalRepo.delete(spaceId, id)
}
override fun getComponents(
spaceId: Int,
goalId: Int
): List<Goal.GoalComponent> {
val userId = authService.getSecurityUserId()
spaceRepo.findSpaceById(spaceId, userId)
goalRepo.findBySpaceIdAndId(spaceId, goalId) ?: throw NotFoundException("Goal $goalId not found")
return goalRepo.getComponents(spaceId, goalId)
}
override fun getComponent(
spaceId: Int,
goalId: Int,
id: Int
): Goal.GoalComponent? {
val userId = authService.getSecurityUserId()
spaceRepo.findSpaceById(spaceId, userId)
goalRepo.findBySpaceIdAndId(spaceId, goalId) ?: throw NotFoundException("Goal $goalId not found")
return goalRepo.getComponent(spaceId, goalId, id)
}
override fun createComponent(
spaceId: Int,
goalId: Int,
component: Goal.GoalComponent
): Int {
val userId = authService.getSecurityUserId()
spaceRepo.findSpaceById(spaceId, userId)
goalRepo.findBySpaceIdAndId(spaceId, goalId) ?: throw NotFoundException("Goal $goalId not found")
return goalRepo.createComponent(goalId, component, userId)
}
override fun updateComponent(
spaceId: Int,
goalId: Int,
component: Goal.GoalComponent
) {
val userId = authService.getSecurityUserId()
spaceRepo.findSpaceById(spaceId, userId)
goalRepo.findBySpaceIdAndId(spaceId, goalId) ?: throw NotFoundException("Goal $goalId not found")
val existingComponent = goalRepo.getComponent(spaceId, goalId, component.id!!)
?: throw NotFoundException("Component $goalId not found")
val updatedComponent = existingComponent.copy(
name = component.name,
amount = component.amount,
isDone = component.isDone,
date = component.date
)
goalRepo.updateComponent(goalId, updatedComponent.id!!, updatedComponent, userId)
}
override fun deleteComponent(spaceId: Int, goalId: Int, id: Int) {
val userId = authService.getSecurityUserId()
spaceRepo.findSpaceById(spaceId, userId)
goalRepo.findBySpaceIdAndId(spaceId, goalId) ?: throw NotFoundException("Goal $goalId not found")
goalRepo.getComponent(spaceId, goalId, id) ?: throw NotFoundException("Component $goalId not found")
goalRepo.deleteComponent(goalId, id)
}
override fun assignTransaction(spaceId: Int, goalId: Int, transactionId: Int) {
val userId = authService.getSecurityUserId()
spaceRepo.findSpaceById(spaceId, userId)
goalRepo.findBySpaceIdAndId(spaceId, goalId) ?: throw NotFoundException("Goal $goalId not found")
transactionRepo.findBySpaceIdAndId(spaceId, transactionId) ?: throw NotFoundException(
"Transaction $transactionId not found"
)
goalRepo.assignTransaction(goalId, transactionId)
}
override fun refuseTransaction(spaceId: Int, goalId: Int, transactionId: Int) {
val userId = authService.getSecurityUserId()
spaceRepo.findSpaceById(spaceId, userId)
goalRepo.findBySpaceIdAndId(spaceId, goalId) ?: throw NotFoundException("Goal $goalId not found")
transactionRepo.findBySpaceIdAndId(spaceId, transactionId) ?: throw NotFoundException(
"Transaction $transactionId not found"
)
goalRepo.refuseTransaction(goalId, transactionId)
}
}

View File

@@ -0,0 +1,13 @@
package space.luminic.finance.services
import space.luminic.finance.dtos.RecurrentOperationDTO
import space.luminic.finance.models.RecurrentOperation
interface RecurrentOperationService {
fun findBySpaceId(spaceId: Int): List<RecurrentOperation>
fun findBySpaceIdAndId(spaceId: Int, id: Int): RecurrentOperation
fun create(spaceId: Int, operation: RecurrentOperationDTO.CreateRecurrentOperationDTO): Int
fun update(spaceId: Int, operationId: Int, operation: RecurrentOperationDTO.UpdateRecurrentOperationDTO)
fun delete(spaceId: Int, id: Int)
}

View File

@@ -0,0 +1,66 @@
package space.luminic.finance.services
import org.springframework.stereotype.Service
import space.luminic.finance.dtos.RecurrentOperationDTO
import space.luminic.finance.models.NotFoundException
import space.luminic.finance.models.RecurrentOperation
import space.luminic.finance.repos.RecurrentOperationRepo
import space.luminic.finance.repos.SpaceRepo
@Service
class RecurrentOperationServiceImpl(
private val authService: AuthService,
private val spaceRepo: SpaceRepo,
private val recurrentOperationRepo: RecurrentOperationRepo,
private val categoryService: CategoryService
): RecurrentOperationService {
override fun findBySpaceId(spaceId: Int): List<RecurrentOperation> {
val userId = authService.getSecurityUserId()
spaceRepo.findSpaceById(spaceId, userId)
return recurrentOperationRepo.findAllBySpaceId(spaceId)
}
override fun findBySpaceIdAndId(
spaceId: Int,
id: Int
): RecurrentOperation {
val userId = authService.getSecurityUserId()
spaceRepo.findSpaceById(spaceId, userId)
return recurrentOperationRepo.findBySpaceIdAndId(spaceId, id) ?: throw NotFoundException("Cannot find recurrent operation with id ${id}")
}
override fun create(spaceId: Int, operation: RecurrentOperationDTO.CreateRecurrentOperationDTO): Int {
val userId = authService.getSecurityUserId()
val space = spaceRepo.findSpaceById(spaceId, userId) ?: throw NotFoundException("Cannot find space with id ${spaceId}")
val category = categoryService.getCategory(spaceId, operation.categoryId)
val creatingOperation = RecurrentOperation(
space = space,
category = category,
name = operation.name,
amount = operation.amount,
date = operation.date
)
return recurrentOperationRepo.create(creatingOperation, userId)
}
override fun update(spaceId: Int, operationId: Int, operation: RecurrentOperationDTO.UpdateRecurrentOperationDTO) {
val userId = authService.getSecurityUserId()
spaceRepo.findSpaceById(spaceId, userId)
val newCategory = categoryService.getCategory(spaceId, operation.categoryId)
val existingOperation = recurrentOperationRepo.findBySpaceIdAndId(spaceId,operationId ) ?: throw NotFoundException("Cannot find operation with id $operationId")
val updatedOperation = existingOperation.copy(
category = newCategory,
name = operation.name,
amount = operation.amount,
date = operation.date
)
recurrentOperationRepo.update(updatedOperation, userId)
}
override fun delete(spaceId: Int, id: Int) {
val userId = authService.getSecurityUserId()
spaceRepo.findSpaceById(spaceId, userId)
recurrentOperationRepo.delete(id)
}
}

View File

@@ -1,16 +1,15 @@
package space.luminic.finance.services package space.luminic.finance.services
import space.luminic.finance.dtos.SpaceDTO import space.luminic.finance.dtos.SpaceDTO
import space.luminic.finance.models.Budget
import space.luminic.finance.models.Space import space.luminic.finance.models.Space
interface SpaceService { interface SpaceService {
suspend fun checkSpace(spaceId: String): Space fun checkSpace(spaceId: Int): Space
suspend fun getSpaces(): List<Space> fun getSpaces(): List<Space>
suspend fun getSpace(id: String): Space fun getSpace(id: Int): Space
suspend fun createSpace(space: SpaceDTO.CreateSpaceDTO): Space fun createSpace(space: SpaceDTO.CreateSpaceDTO): Int
suspend fun updateSpace(spaceId: String, space: SpaceDTO.UpdateSpaceDTO): Space fun updateSpace(spaceId: Int, space: SpaceDTO.UpdateSpaceDTO): Int
suspend fun deleteSpace(spaceId: String) fun deleteSpace(spaceId: Int)
} }

View File

@@ -1,149 +1,75 @@
package space.luminic.finance.services package space.luminic.finance.services
import com.mongodb.client.model.Aggregates.sort import org.springframework.cache.annotation.CacheEvict
import kotlinx.coroutines.reactive.awaitFirst import org.springframework.cache.annotation.Cacheable
import kotlinx.coroutines.reactive.awaitFirstOrNull import org.springframework.jdbc.core.JdbcTemplate
import kotlinx.coroutines.reactive.awaitSingle
import kotlinx.coroutines.reactive.awaitSingleOrNull
import org.bson.types.ObjectId
import org.springframework.data.domain.Sort
import org.springframework.data.mongodb.core.ReactiveMongoTemplate
import org.springframework.data.mongodb.core.aggregation.Aggregation.*
import org.springframework.data.mongodb.core.aggregation.AggregationOperation
import org.springframework.data.mongodb.core.aggregation.ArrayOperators
import org.springframework.data.mongodb.core.aggregation.ConvertOperators
import org.springframework.data.mongodb.core.aggregation.VariableOperators
import org.springframework.data.mongodb.core.query.Criteria
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import space.luminic.finance.dtos.SpaceDTO import space.luminic.finance.dtos.SpaceDTO
import space.luminic.finance.models.Budget
import space.luminic.finance.models.NotFoundException import space.luminic.finance.models.NotFoundException
import space.luminic.finance.models.Space import space.luminic.finance.models.Space
import space.luminic.finance.models.User
import space.luminic.finance.repos.SpaceRepo import space.luminic.finance.repos.SpaceRepo
@Service @Service
class SpaceServiceImpl( class SpaceServiceImpl(
private val authService: AuthService, private val authService: AuthService,
private val spaceRepo: SpaceRepo, private val spaceRepo: SpaceRepo,
private val mongoTemplate: ReactiveMongoTemplate, private val categoryService: CategoryService
) : SpaceService { ) : SpaceService {
override fun checkSpace(spaceId: Int): Space {
private fun basicAggregation(user: User): List<AggregationOperation> { return getSpace(spaceId)
val addFieldsAsOJ = addFields()
.addField("createdByOI")
.withValue(ConvertOperators.valueOf("createdById").convertToObjectId())
.addField("updatedByOI")
.withValue(ConvertOperators.valueOf("updatedById").convertToObjectId())
.build()
val lookupCreatedBy = lookup("users", "createdByOI", "_id", "createdBy")
val unwindCreatedBy = unwind("createdBy")
val lookupUpdatedBy = lookup("users", "updatedByOI", "_id", "updatedBy")
val unwindUpdatedBy = unwind("updatedBy")
val matchCriteria = mutableListOf<Criteria>()
matchCriteria.add(
Criteria().orOperator(
Criteria.where("ownerId").`is`(user.id),
Criteria.where("participantsIds").`is`(user.id)
)
)
matchCriteria.add(Criteria.where("isDeleted").`is`(false))
val matchStage = match(Criteria().andOperator(*matchCriteria.toTypedArray()))
return listOf(addFieldsAsOJ, lookupCreatedBy, unwindCreatedBy, lookupUpdatedBy, unwindUpdatedBy, matchStage)
} }
private fun ownerAndParticipantsLookups(): List<AggregationOperation>{ // @Cacheable(cacheNames = ["spaces"])
val addOwnerAsOJ = addFields() override fun getSpaces(): List<Space> {
.addField("ownerIdAsObjectId") val user = authService.getSecurityUserId()
.withValue(ConvertOperators.valueOf("ownerId").convertToObjectId()) val spaces = spaceRepo.findSpacesAvailableForUser(user)
.addField("participantsIdsAsObjectId") return spaces
.withValue(
VariableOperators.Map.itemsOf("participantsIds")
.`as`("id")
.andApply(
ConvertOperators.valueOf("$\$id").convertToObjectId()
)
)
.build()
val lookupOwner = lookup("users", "ownerIdAsObjectId", "_id", "owner")
val unwindOwner = unwind("owner")
val lookupUsers = lookup("users", "participantsIdsAsObjectId", "_id", "participants")
return listOf(addOwnerAsOJ, lookupOwner, unwindOwner, lookupUsers)
} }
override suspend fun checkSpace(spaceId: String): Space { override fun getSpace(id: Int): Space {
val user = authService.getSecurityUserId()
val space = spaceRepo.findSpaceById(id, user) ?: throw NotFoundException("Space with id $id not found")
return space
}
@Transactional
override fun createSpace(space: SpaceDTO.CreateSpaceDTO): Int {
val user = authService.getSecurityUser() val user = authService.getSecurityUser()
val space = getSpace(spaceId) val creatingSpace = Space(
// Проверяем доступ пользователя к пространству
return if (space.participants!!.none { it.id.toString() == user.id }) {
throw IllegalArgumentException("User does not have access to this Space")
} else space
}
override suspend fun getSpaces(): List<Space> {
val user = authService.getSecurityUser()
val basicAggregation = basicAggregation(user)
val ownerAndParticipantsLookup = ownerAndParticipantsLookups()
val sort = sort(Sort.by(Sort.Direction.DESC, "createdAt"))
val aggregation = newAggregation(
*basicAggregation.toTypedArray(),
*ownerAndParticipantsLookup.toTypedArray(),
sort,
)
return mongoTemplate.aggregate(aggregation, "spaces", Space::class.java).collectList().awaitSingle()
}
override suspend fun getSpace(id: String): Space {
val user = authService.getSecurityUser()
val basicAggregation = basicAggregation(user)
val ownerAndParticipantsLookup = ownerAndParticipantsLookups()
val aggregation = newAggregation(
*basicAggregation.toTypedArray(),
*ownerAndParticipantsLookup.toTypedArray(),
)
return mongoTemplate.aggregate(aggregation, "spaces", Space::class.java).awaitFirstOrNull()
?: throw NotFoundException("Space not found")
}
override suspend fun createSpace(space: SpaceDTO.CreateSpaceDTO): Space {
val owner = authService.getSecurityUser()
val createdSpace = Space(
name = space.name, name = space.name,
ownerId = owner.id!!, owner = user,
participants = setOf(user)
participantsIds = listOf(owner.id!!), )
val userId = authService.getSecurityUserId()
val savedSpace = spaceRepo.create(creatingSpace, userId)
) if (space.createBasicCategories) {
createdSpace.owner = owner categoryService.createEtalonCategoriesForSpace(savedSpace)
createdSpace.participants?.toMutableList()?.add(owner) }
val savedSpace = spaceRepo.save(createdSpace).awaitSingle()
return savedSpace return savedSpace
} }
override suspend fun updateSpace(spaceId: String, space: SpaceDTO.UpdateSpaceDTO): Space { @Transactional
val existingSpace = spaceRepo.findById(spaceId).awaitFirstOrNull() ?: throw NotFoundException("Space not found") override fun updateSpace(
val updatedSpace = existingSpace.copy( spaceId: Int,
space: SpaceDTO.UpdateSpaceDTO
): Int {
val userId = authService.getSecurityUserId()
val existingSpace = getSpace(spaceId)
val updatedSpace = Space(
id = existingSpace.id,
name = space.name, name = space.name,
) owner = existingSpace.owner,
updatedSpace.owner = existingSpace.owner participants = existingSpace.participants,
updatedSpace.participants = existingSpace.participants isDeleted = existingSpace.isDeleted,
return spaceRepo.save(updatedSpace).awaitFirst() createdBy = existingSpace.createdBy,
createdAt = existingSpace.createdAt,
)
return spaceRepo.update(updatedSpace, userId)
} }
@Transactional
override suspend fun deleteSpace(spaceId: String) { override fun deleteSpace(spaceId: Int) {
val space = spaceRepo.findById(spaceId).awaitFirstOrNull() ?: throw NotFoundException("Space not found") spaceRepo.delete(spaceId)
space.isDeleted = true
spaceRepo.save(space).awaitFirst()
} }
} }

View File

@@ -0,0 +1,145 @@
//package space.luminic.finance.services
//
//import com.mongodb.client.model.Aggregates.sort
//import kotlinx.coroutines.reactive.awaitFirst
//import kotlinx.coroutines.reactive.awaitFirstOrNull
//import kotlinx.coroutines.reactive.awaitSingle
//import org.springframework.data.domain.Sort
//import org.springframework.data.mongodb.core.ReactiveMongoTemplate
//import org.springframework.data.mongodb.core.aggregation.Aggregation.*
//import org.springframework.data.mongodb.core.aggregation.AggregationOperation
//
//import org.springframework.data.mongodb.core.aggregation.ConvertOperators
//import org.springframework.data.mongodb.core.aggregation.VariableOperators
//import org.springframework.data.mongodb.core.query.Criteria
//import org.springframework.stereotype.Service
//import space.luminic.finance.dtos.SpaceDTO
//import space.luminic.finance.models.NotFoundException
//import space.luminic.finance.models.Space
//import space.luminic.finance.models.User
//import space.luminic.finance.repos.SpaceRepo
//
//@Service
//class SpaceServiceMongoImpl(
// private val authService: AuthService,
// private val spaceRepo: SpaceRepo,
// private val mongoTemplate: ReactiveMongoTemplate,
//) : SpaceService {
//
// private fun basicAggregation(user: User): List<AggregationOperation> {
// val addFieldsAsOJ = addFields()
// .addField("createdByOI")
// .withValue(ConvertOperators.valueOf("createdById").convertToObjectId())
// .addField("updatedByOI")
// .withValue(ConvertOperators.valueOf("updatedById").convertToObjectId())
// .build()
// val lookupCreatedBy = lookup("users", "createdByOI", "_id", "createdBy")
// val unwindCreatedBy = unwind("createdBy")
//
// val lookupUpdatedBy = lookup("users", "updatedByOI", "_id", "updatedBy")
// val unwindUpdatedBy = unwind("updatedBy")
//
//
//
// val matchCriteria = mutableListOf<Criteria>()
// matchCriteria.add(
// Criteria().orOperator(
// Criteria.where("ownerId").`is`(user.id),
// Criteria.where("participantsIds").`is`(user.id)
// )
// )
// matchCriteria.add(Criteria.where("isDeleted").`is`(false))
// val matchStage = match(Criteria().andOperator(*matchCriteria.toTypedArray()))
//
// return listOf(addFieldsAsOJ, lookupCreatedBy, unwindCreatedBy, lookupUpdatedBy, unwindUpdatedBy, matchStage)
// }
//
// private fun ownerAndParticipantsLookups(): List<AggregationOperation>{
// val addOwnerAsOJ = addFields()
// .addField("ownerIdAsObjectId")
// .withValue(ConvertOperators.valueOf("ownerId").convertToObjectId())
// .addField("participantsIdsAsObjectId")
// .withValue(
// VariableOperators.Map.itemsOf("participantsIds")
// .`as`("id")
// .andApply(
// ConvertOperators.valueOf("$\$id").convertToObjectId()
// )
// )
// .build()
// val lookupOwner = lookup("users", "ownerIdAsObjectId", "_id", "owner")
// val unwindOwner = unwind("owner")
// val lookupUsers = lookup("users", "participantsIdsAsObjectId", "_id", "participants")
// return listOf(addOwnerAsOJ, lookupOwner, unwindOwner, lookupUsers)
// }
//
// override suspend fun checkSpace(spaceId: String): Space {
// val user = authService.getSecurityUser()
// val space = getSpace(spaceId)
//
// // Проверяем доступ пользователя к пространству
// return if (space.participants!!.none { it.id.toString() == user.id }) {
// throw IllegalArgumentException("User does not have access to this Space")
// } else space
// }
//
// override suspend fun getSpaces(): List<Space> {
// val user = authService.getSecurityUser()
// val basicAggregation = basicAggregation(user)
// val ownerAndParticipantsLookup = ownerAndParticipantsLookups()
//
// val sort = sort(Sort.by(Sort.Direction.DESC, "createdAt"))
// val aggregation = newAggregation(
// *basicAggregation.toTypedArray(),
// *ownerAndParticipantsLookup.toTypedArray(),
// sort,
// )
// return mongoTemplate.aggregate(aggregation, "spaces", Space::class.java).collectList().awaitSingle()
// }
//
// override suspend fun getSpace(id: String): Space {
// val user = authService.getSecurityUser()
// val basicAggregation = basicAggregation(user)
// val ownerAndParticipantsLookup = ownerAndParticipantsLookups()
//
// val aggregation = newAggregation(
// *basicAggregation.toTypedArray(),
// *ownerAndParticipantsLookup.toTypedArray(),
// )
// return mongoTemplate.aggregate(aggregation, "spaces", Space::class.java).awaitFirstOrNull()
// ?: throw NotFoundException("Space not found")
//
// }
//
// override suspend fun createSpace(space: SpaceDTO.CreateSpaceDTO): Space {
// val owner = authService.getSecurityUser()
// val createdSpace = Space(
// name = space.name,
// ownerId = owner.id!!,
//
// participantsIds = listOf(owner.id!!),
//
//
// )
// createdSpace.owner = owner
// createdSpace.participants?.toMutableList()?.add(owner)
// val savedSpace = spaceRepo.save(createdSpace).awaitSingle()
// return savedSpace
// }
//
// override suspend fun updateSpace(spaceId: String, space: SpaceDTO.UpdateSpaceDTO): Space {
// val existingSpace = spaceRepo.findById(spaceId).awaitFirstOrNull() ?: throw NotFoundException("Space not found")
// val updatedSpace = existingSpace.copy(
// name = space.name,
// )
// updatedSpace.owner = existingSpace.owner
// updatedSpace.participants = existingSpace.participants
// return spaceRepo.save(updatedSpace).awaitFirst()
// }
//
// override suspend fun deleteSpace(spaceId: String) {
// val space = spaceRepo.findById(spaceId).awaitFirstOrNull() ?: throw NotFoundException("Space not found")
// space.isDeleted = true
// spaceRepo.save(space).awaitFirst()
// }
//}

View File

@@ -1,113 +1,111 @@
package space.luminic.finance.services //package space.luminic.finance.services
//
//
import com.interaso.webpush.VapidKeys //import com.interaso.webpush.VapidKeys
import com.interaso.webpush.WebPushService //import com.interaso.webpush.WebPushService
import kotlinx.coroutines.Dispatchers //import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope //import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch //import kotlinx.coroutines.launch
import kotlinx.coroutines.reactive.awaitSingle //import kotlinx.coroutines.reactive.awaitSingle
import kotlinx.serialization.encodeToString //import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json //import kotlinx.serialization.json.Json
import org.bson.types.ObjectId //import org.slf4j.LoggerFactory
import org.slf4j.LoggerFactory //import org.springframework.dao.DuplicateKeyException
import org.springframework.dao.DuplicateKeyException //import org.springframework.stereotype.Service
import org.springframework.stereotype.Service //import space.luminic.finance.models.PushMessage
import space.luminic.finance.models.PushMessage //import space.luminic.finance.models.Subscription
import space.luminic.finance.models.Subscription //import space.luminic.finance.models.User
import space.luminic.finance.models.SubscriptionDTO //import space.luminic.finance.repos.SubscriptionRepo
import space.luminic.finance.models.User //import space.luminic.finance.services.VapidConstants.VAPID_PRIVATE_KEY
import space.luminic.finance.repos.SubscriptionRepo //import space.luminic.finance.services.VapidConstants.VAPID_PUBLIC_KEY
import space.luminic.finance.services.VapidConstants.VAPID_PRIVATE_KEY //import space.luminic.finance.services.VapidConstants.VAPID_SUBJECT
import space.luminic.finance.services.VapidConstants.VAPID_PUBLIC_KEY //import kotlin.collections.forEach
import space.luminic.finance.services.VapidConstants.VAPID_SUBJECT //import kotlin.jvm.javaClass
import kotlin.collections.forEach //import kotlin.text.orEmpty
import kotlin.jvm.javaClass //
import kotlin.text.orEmpty //object VapidConstants {
// const val VAPID_PUBLIC_KEY =
object VapidConstants { // "BKmMyBUhpkcmzYWcYsjH_spqcy0zf_8eVtZo60f7949TgLztCmv3YD0E_vtV2dTfECQ4sdLdPK3ICDcyOkCqr84"
const val VAPID_PUBLIC_KEY = // const val VAPID_PRIVATE_KEY = "YeJH_0LhnVYN6RdxMidgR6WMYlpGXTJS3HjT9V3NSGI"
"BKmMyBUhpkcmzYWcYsjH_spqcy0zf_8eVtZo60f7949TgLztCmv3YD0E_vtV2dTfECQ4sdLdPK3ICDcyOkCqr84" // const val VAPID_SUBJECT = "mailto:voroninvyu@gmail.com"
const val VAPID_PRIVATE_KEY = "YeJH_0LhnVYN6RdxMidgR6WMYlpGXTJS3HjT9V3NSGI" //}
const val VAPID_SUBJECT = "mailto:voroninvyu@gmail.com" //
} //@Service
//class SubscriptionService(private val subscriptionRepo: SubscriptionRepo) {
@Service //
class SubscriptionService(private val subscriptionRepo: SubscriptionRepo) { // private val logger = LoggerFactory.getLogger(javaClass)
// private val pushService =
private val logger = LoggerFactory.getLogger(javaClass) // WebPushService(
private val pushService = // subject = VAPID_SUBJECT,
WebPushService( // vapidKeys = VapidKeys.fromUncompressedBytes(VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY)
subject = VAPID_SUBJECT, // )
vapidKeys = VapidKeys.fromUncompressedBytes(VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY) //
) //
// suspend fun sendToSpaceOwner(ownerId: String, message: PushMessage) = coroutineScope {
// val ownerTokens = subscriptionRepo.findByUserIdAndIsActive(ObjectId(ownerId)).collectList().awaitSingle()
suspend fun sendToSpaceOwner(ownerId: String, message: PushMessage) = coroutineScope { //
val ownerTokens = subscriptionRepo.findByUserIdAndIsActive(ObjectId(ownerId)).collectList().awaitSingle() // ownerTokens.forEach { token ->
// launch(Dispatchers.IO) { // Теперь мы точно в корутин скоупе
ownerTokens.forEach { token -> // try {
launch(Dispatchers.IO) { // Теперь мы точно в корутин скоупе // sendNotification(token.endpoint, token.p256dh, token.auth, message)
try { // } catch (e: Exception) {
sendNotification(token.endpoint, token.p256dh, token.auth, message) // logger.error("Ошибка при отправке уведомления: ${e.message}", e)
} catch (e: Exception) { // }
logger.error("Ошибка при отправке уведомления: ${e.message}", e) // }
} // }
} // }
} //
} //
// suspend fun sendNotification(endpoint: String, p256dh: String, auth: String, payload: PushMessage) {
// try {
suspend fun sendNotification(endpoint: String, p256dh: String, auth: String, payload: PushMessage) { // pushService.send(
try { // payload = Json.encodeToString(payload),
pushService.send( // endpoint = endpoint,
payload = Json.encodeToString(payload), // p256dh = p256dh,
endpoint = endpoint, // auth = auth
p256dh = p256dh, // )
auth = auth // logger.info("Уведомление успешно отправлено на endpoint: $endpoint")
) //
logger.info("Уведомление успешно отправлено на endpoint: $endpoint") // } catch (e: Exception) {
// logger.error("Ошибка при отправке уведомления на endpoint $endpoint: ${e.message}")
} catch (e: Exception) { // throw e
logger.error("Ошибка при отправке уведомления на endpoint $endpoint: ${e.message}") // }
throw e // }
} //
} //
// suspend fun sendToAll(payload: PushMessage) {
//
suspend fun sendToAll(payload: PushMessage) { // subscriptionRepo.findAll().collectList().awaitSingle().forEach { sub ->
//
subscriptionRepo.findAll().collectList().awaitSingle().forEach { sub -> // try {
// sendNotification(sub.endpoint, sub.p256dh, sub.auth, payload)
try { // } catch (e: Exception) {
sendNotification(sub.endpoint, sub.p256dh, sub.auth, payload) // sub.isActive = false
} catch (e: Exception) { // subscriptionRepo.save(sub).awaitSingle()
sub.isActive = false // }
subscriptionRepo.save(sub).awaitSingle() // }
} // }
} //
} //
// suspend fun subscribe(subscriptionDTO: SubscriptionDTO, user: User): String {
// val subscription = Subscription(
suspend fun subscribe(subscriptionDTO: SubscriptionDTO, user: User): String { // id = null,
val subscription = Subscription( // user = user,
id = null, // endpoint = subscriptionDTO.endpoint,
user = user, // auth = subscriptionDTO.keys["auth"].orEmpty(),
endpoint = subscriptionDTO.endpoint, // p256dh = subscriptionDTO.keys["p256dh"].orEmpty(),
auth = subscriptionDTO.keys["auth"].orEmpty(), // isActive = true
p256dh = subscriptionDTO.keys["p256dh"].orEmpty(), // )
isActive = true //
) // return try {
// val savedSubscription = subscriptionRepo.save(subscription).awaitSingle()
return try { // "Subscription created with ID: ${savedSubscription.id}"
val savedSubscription = subscriptionRepo.save(subscription).awaitSingle() // } catch (e: DuplicateKeyException) {
"Subscription created with ID: ${savedSubscription.id}" // logger.info("Subscription already exists. Skipping.")
} catch (e: DuplicateKeyException) { // "Subscription already exists. Skipping."
logger.info("Subscription already exists. Skipping.") // } catch (e: Exception) {
"Subscription already exists. Skipping." // logger.error("Error while saving subscription: ${e.message}")
} catch (e: Exception) { // throw kotlin.RuntimeException("Error while saving subscription")
logger.error("Error while saving subscription: ${e.message}") // }
throw kotlin.RuntimeException("Error while saving subscription") // }
} //}
}
}

View File

@@ -1,43 +1,51 @@
package space.luminic.finance.services package space.luminic.finance.services
import kotlinx.coroutines.reactor.awaitSingle
import org.springframework.cache.annotation.CacheEvict import org.springframework.cache.annotation.CacheEvict
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import reactor.core.publisher.Mono import space.luminic.finance.configs.AuthException
import space.luminic.finance.models.Token import space.luminic.finance.models.Token
import space.luminic.finance.models.Token.TokenStatus import space.luminic.finance.models.Token.TokenStatus
import space.luminic.finance.repos.TokenRepo import space.luminic.finance.repos.TokenRepo
import java.time.LocalDateTime import java.time.Instant
@Service @Service
class TokenService(private val tokenRepository: TokenRepo) { class TokenService(
private val userService: UserService,
private val tokenRepo: TokenRepo) {
@CacheEvict("tokens", allEntries = true) @CacheEvict("tokens", allEntries = true)
suspend fun saveToken(token: String, username: String, expiresAt: LocalDateTime): Token { fun saveToken(token: String, username: String, expiresAt: Instant): Token {
val user = userService.getByUsername(username)
val newToken = Token( val newToken = Token(
token = token, token = token,
username = username, user = user,
issuedAt = LocalDateTime.now(), issuedAt = Instant.now(),
expiresAt = expiresAt expiresAt = expiresAt
) )
return tokenRepository.save(newToken).awaitSingle() return tokenRepo.create(newToken)
} }
fun getToken(token: String): Mono<Token> { fun getToken(token: String): Token {
return tokenRepository.findByToken(token) return tokenRepo.findByToken(token) ?: throw AuthException("Токен не валиден")
} }
fun revokeToken(token: String) { fun revokeToken(token: String) {
val tokenDetail = val tokenDetail = getToken(token)
tokenRepository.findByToken(token).block()!! val updatedToken = Token(
val updatedToken = tokenDetail.copy(status = TokenStatus.REVOKED) id = tokenDetail.id,
tokenRepository.save(updatedToken).block() token = tokenDetail.token,
user = tokenDetail.user,
status = TokenStatus.REVOKED,
issuedAt = tokenDetail.issuedAt,
expiresAt = tokenDetail.expiresAt
)
tokenRepo.update(updatedToken)
} }
@CacheEvict("tokens", allEntries = true) @CacheEvict("tokens", allEntries = true)
fun deleteExpiredTokens() { fun deleteExpiredTokens() {
tokenRepository.deleteByExpiresAtBefore(LocalDateTime.now()) tokenRepo.deleteByExpiresAtBefore(Instant.now())
} }
} }

View File

@@ -7,14 +7,13 @@ import java.time.LocalDate
interface TransactionService { interface TransactionService {
data class TransactionsFilter( data class TransactionsFilter(
val accountId: String,
val dateFrom: LocalDate? = null, val dateFrom: LocalDate? = null,
val dateTo: LocalDate? = null, val dateTo: LocalDate? = null,
) )
suspend fun getTransactions(spaceId: String, filter: TransactionsFilter, sortBy: String, sortDirection: String): List<Transaction> fun getTransactions(spaceId: Int, filter: TransactionsFilter, sortBy: String, sortDirection: String): List<Transaction>
suspend fun getTransaction(spaceId: String, transactionId: String): Transaction fun getTransaction(spaceId: Int, transactionId: Int): Transaction
suspend fun createTransaction(spaceId: String, transaction: TransactionDTO.CreateTransactionDTO): Transaction fun createTransaction(spaceId: Int, transaction: TransactionDTO.CreateTransactionDTO): Int
suspend fun updateTransaction(spaceId: String, transaction: TransactionDTO.UpdateTransactionDTO): Transaction fun updateTransaction(spaceId: Int, transactionId: Int, transaction: TransactionDTO.UpdateTransactionDTO): Int
suspend fun deleteTransaction(spaceId: String, transactionId: String) fun deleteTransaction(spaceId: Int, transactionId: Int)
} }

View File

@@ -1,20 +1,5 @@
package space.luminic.finance.services package space.luminic.finance.services
import kotlinx.coroutines.reactive.awaitFirstOrNull
import kotlinx.coroutines.reactive.awaitSingle
import org.bson.types.ObjectId
import org.springframework.data.domain.Sort
import org.springframework.data.domain.Sort.Direction
import org.springframework.data.mongodb.core.ReactiveMongoTemplate
import org.springframework.data.mongodb.core.aggregation.Aggregation.addFields
import org.springframework.data.mongodb.core.aggregation.Aggregation.lookup
import org.springframework.data.mongodb.core.aggregation.Aggregation.match
import org.springframework.data.mongodb.core.aggregation.Aggregation.newAggregation
import org.springframework.data.mongodb.core.aggregation.Aggregation.sort
import org.springframework.data.mongodb.core.aggregation.Aggregation.unwind
import org.springframework.data.mongodb.core.aggregation.AggregationOperation
import org.springframework.data.mongodb.core.aggregation.ConvertOperators
import org.springframework.data.mongodb.core.query.Criteria
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import space.luminic.finance.dtos.TransactionDTO import space.luminic.finance.dtos.TransactionDTO
import space.luminic.finance.models.NotFoundException import space.luminic.finance.models.NotFoundException
@@ -23,163 +8,89 @@ import space.luminic.finance.repos.TransactionRepo
@Service @Service
class TransactionServiceImpl( class TransactionServiceImpl(
private val mongoTemplate: ReactiveMongoTemplate, private val spaceService: SpaceService,
private val transactionRepo: TransactionRepo,
private val categoryService: CategoryService, private val categoryService: CategoryService,
private val transactionRepo: TransactionRepo,
private val authService: AuthService,
) : TransactionService { ) : TransactionService {
override fun getTransactions(
spaceId: Int,
private fun basicAggregation(spaceId: String): List<AggregationOperation> {
val addFieldsOI = addFields()
.addField("createdByOI")
.withValue(ConvertOperators.valueOf("createdById").convertToObjectId())
.addField("updatedByOI")
.withValue(ConvertOperators.valueOf("updatedById").convertToObjectId())
.addField("fromAccountIdOI")
.withValue(ConvertOperators.valueOf("fromAccountId").convertToObjectId())
.addField("toAccountIdOI")
.withValue(ConvertOperators.valueOf("toAccountId").convertToObjectId())
.addField("categoryIdOI")
.withValue(ConvertOperators.valueOf("categoryId").convertToObjectId())
.build()
val lookupFromAccount = lookup("accounts", "fromAccountIdOI", "_id", "fromAccount")
val unwindFromAccount = unwind("fromAccount")
val lookupToAccount = lookup("accounts", "toAccountIdOI", "_id", "toAccount")
val unwindToAccount = unwind("toAccount", true)
val lookupCategory = lookup("categories", "categoryIdOI", "_id", "category")
val unwindCategory = unwind("category")
val lookupCreatedBy = lookup("users", "createdByOI", "_id", "createdBy")
val unwindCreatedBy = unwind("createdBy")
val lookupUpdatedBy = lookup("users", "updatedByOI", "_id", "updatedBy")
val unwindUpdatedBy = unwind("updatedBy")
val matchCriteria = mutableListOf<Criteria>()
matchCriteria.add(Criteria.where("spaceId").`is`(spaceId))
val matchStage = match(Criteria().andOperator(*matchCriteria.toTypedArray()))
return listOf(
matchStage,
addFieldsOI,
lookupFromAccount,
unwindFromAccount,
lookupToAccount,
unwindToAccount,
lookupCategory,
unwindCategory,
lookupCreatedBy,
unwindCreatedBy,
lookupUpdatedBy,
unwindUpdatedBy
)
}
override suspend fun getTransactions(
spaceId: String,
filter: TransactionService.TransactionsFilter, filter: TransactionService.TransactionsFilter,
sortBy: String, sortBy: String,
sortDirection: String sortDirection: String
): List<Transaction> { ): List<Transaction> {
val allowedSortFields = setOf("date", "amount", "category.name", "createdAt") return transactionRepo.findAllBySpaceId(spaceId)
require(sortBy in allowedSortFields) { "Invalid sort field: $sortBy" }
val direction = when (sortDirection.uppercase()) {
"ASC" -> Direction.ASC
"DESC" -> Direction.DESC
else -> throw IllegalArgumentException("Sort direction must be 'ASC' or 'DESC'")
}
val basicAggregation = basicAggregation(spaceId)
val sort = sort(Sort.by(direction, sortBy))
val matchCriteria = mutableListOf<Criteria>()
filter.dateFrom?.let { matchCriteria.add(Criteria.where("date").gte(it)) }
filter.dateTo?.let { matchCriteria.add(Criteria.where("date").lte(it)) }
val matchStage = match(Criteria().andOperator(*matchCriteria.toTypedArray()))
val aggregation =
newAggregation(
matchStage,
*basicAggregation.toTypedArray(),
sort
)
return mongoTemplate.aggregate(aggregation, "transactions", Transaction::class.java)
.collectList()
.awaitSingle()
} }
override suspend fun getTransaction( override fun getTransaction(
spaceId: String, spaceId: Int,
transactionId: String transactionId: Int
): Transaction { ): Transaction {
val matchCriteria = mutableListOf<Criteria>() spaceService.getSpace(spaceId)
matchCriteria.add(Criteria.where("spaceId").`is`(spaceId)) return transactionRepo.findBySpaceIdAndId(spaceId, transactionId)
matchCriteria.add(Criteria.where("_id").`is`(ObjectId(transactionId))) ?: throw NotFoundException("Transaction with id $transactionId not found")
val matchStage = match(Criteria().andOperator(*matchCriteria.toTypedArray()))
val aggregation =
newAggregation(
matchStage,
)
return mongoTemplate.aggregate(aggregation, "transactions", Transaction::class.java)
.awaitFirstOrNull() ?: throw NotFoundException("Transaction with ID $transactionId not found")
} }
override suspend fun createTransaction( override fun createTransaction(
spaceId: String, spaceId: Int,
transaction: TransactionDTO.CreateTransactionDTO transaction: TransactionDTO.CreateTransactionDTO
): Transaction { ): Int {
if (transaction.type == Transaction.TransactionType.TRANSFER && transaction.toAccountId == null) { val userId = authService.getSecurityUserId()
throw IllegalArgumentException("Cannot create a transaction with type TRANSFER without a toAccountId") val space = spaceService.getSpace(spaceId)
}
val category = categoryService.getCategory(spaceId, transaction.categoryId) val category = categoryService.getCategory(spaceId, transaction.categoryId)
if (transaction.type != Transaction.TransactionType.TRANSFER && transaction.type.name != category.type.name) {
throw IllegalArgumentException("Transaction type should match with category type")
}
val transaction = Transaction( val transaction = Transaction(
spaceId = spaceId, space = space,
type = transaction.type, type = transaction.type,
kind = transaction.kind, kind = transaction.kind,
categoryId = transaction.categoryId, category = category,
comment = transaction.comment, comment = transaction.comment,
amount = transaction.amount, amount = transaction.amount,
fees = transaction.fees, fees = transaction.fees,
date = transaction.date, date = transaction.date,
fromAccountId = transaction.fromAccountId,
toAccountId = transaction.toAccountId,
) )
return transactionRepo.save(transaction).awaitSingle() return transactionRepo.create(transaction, userId)
} }
override suspend fun updateTransaction( override fun updateTransaction(
spaceId: String, spaceId: Int,
transactionId: Int,
transaction: TransactionDTO.UpdateTransactionDTO transaction: TransactionDTO.UpdateTransactionDTO
): Transaction { ): Int {
if (transaction.type == Transaction.TransactionType.TRANSFER && transaction.toAccountId == null) { val space = spaceService.getSpace(spaceId)
throw IllegalArgumentException("Cannot edit a transaction with type TRANSFER without a toAccountId") val existingTransaction = getTransaction(space.id!!, transactionId)
} val newCategory = categoryService.getCategory(spaceId, transaction.categoryId)
val exitingTx = getTransaction(spaceId, transaction.id) // val id: Int,
val transaction = exitingTx.copy( // val type: TransactionType = TransactionType.EXPENSE,
spaceId = exitingTx.spaceId, // val kind: TransactionKind = TransactionKind.INSTANT,
// val category: Int,
// val comment: String,
// val amount: BigDecimal,
// val fees: BigDecimal = BigDecimal.ZERO,
// val date: Instant
val updatedTransaction = Transaction(
id = existingTransaction.id,
space = existingTransaction.space,
parent = existingTransaction.parent,
type = transaction.type, type = transaction.type,
kind = transaction.kind, kind = transaction.kind,
categoryId = transaction.category, category = newCategory,
comment = transaction.comment, comment = transaction.comment,
amount = transaction.amount, amount = transaction.amount,
fees = transaction.fees, fees = transaction.fees,
date = transaction.date, date = transaction.date,
fromAccountId = transaction.fromAccountId, isDeleted = existingTransaction.isDeleted,
toAccountId = transaction.toAccountId, isDone = transaction.isDone,
createdBy = existingTransaction.createdBy,
createdAt = existingTransaction.createdAt
) )
return transactionRepo.save(transaction).awaitSingle() return transactionRepo.update(updatedTransaction)
} }
override suspend fun deleteTransaction(spaceId: String, transactionId: String) { override fun deleteTransaction(spaceId: Int, transactionId: Int) {
val transaction = getTransaction(spaceId, transactionId) val space = spaceService.getSpace(spaceId)
transaction.isDeleted = true getTransaction(space.id!!, transactionId)
transactionRepo.save(transaction).awaitSingle() transactionRepo.delete(transactionId)
} }
} }

View File

@@ -0,0 +1,185 @@
//package space.luminic.finance.services
//
//import kotlinx.coroutines.reactive.awaitFirstOrNull
//import kotlinx.coroutines.reactive.awaitSingle
//import org.bson.types.ObjectId
//import org.springframework.data.domain.Sort
//import org.springframework.data.domain.Sort.Direction
//import org.springframework.data.mongodb.core.ReactiveMongoTemplate
//import org.springframework.data.mongodb.core.aggregation.Aggregation.addFields
//import org.springframework.data.mongodb.core.aggregation.Aggregation.lookup
//import org.springframework.data.mongodb.core.aggregation.Aggregation.match
//import org.springframework.data.mongodb.core.aggregation.Aggregation.newAggregation
//import org.springframework.data.mongodb.core.aggregation.Aggregation.sort
//import org.springframework.data.mongodb.core.aggregation.Aggregation.unwind
//import org.springframework.data.mongodb.core.aggregation.AggregationOperation
//import org.springframework.data.mongodb.core.aggregation.ConvertOperators
//import org.springframework.data.mongodb.core.query.Criteria
//import org.springframework.stereotype.Service
//import space.luminic.finance.dtos.TransactionDTO
//import space.luminic.finance.models.NotFoundException
//import space.luminic.finance.models.Transaction
//import space.luminic.finance.repos.TransactionRepo
//
//@Service
//class TransactionServiceMongoImpl(
// private val mongoTemplate: ReactiveMongoTemplate,
// private val transactionRepo: TransactionRepo,
// private val categoryService: CategoryService,
//) : TransactionService {
//
//
// private fun basicAggregation(spaceId: String): List<AggregationOperation> {
// val addFieldsOI = addFields()
// .addField("createdByOI")
// .withValue(ConvertOperators.valueOf("createdById").convertToObjectId())
// .addField("updatedByOI")
// .withValue(ConvertOperators.valueOf("updatedById").convertToObjectId())
// .addField("fromAccountIdOI")
// .withValue(ConvertOperators.valueOf("fromAccountId").convertToObjectId())
// .addField("toAccountIdOI")
// .withValue(ConvertOperators.valueOf("toAccountId").convertToObjectId())
// .addField("categoryIdOI")
// .withValue(ConvertOperators.valueOf("categoryId").convertToObjectId())
// .build()
//
// val lookupFromAccount = lookup("accounts", "fromAccountIdOI", "_id", "fromAccount")
// val unwindFromAccount = unwind("fromAccount")
// val lookupToAccount = lookup("accounts", "toAccountIdOI", "_id", "toAccount")
// val unwindToAccount = unwind("toAccount", true)
//
// val lookupCategory = lookup("categories", "categoryIdOI", "_id", "category")
// val unwindCategory = unwind("category")
//
// val lookupCreatedBy = lookup("users", "createdByOI", "_id", "createdBy")
// val unwindCreatedBy = unwind("createdBy")
//
// val lookupUpdatedBy = lookup("users", "updatedByOI", "_id", "updatedBy")
// val unwindUpdatedBy = unwind("updatedBy")
// val matchCriteria = mutableListOf<Criteria>()
// matchCriteria.add(Criteria.where("spaceId").`is`(spaceId))
// val matchStage = match(Criteria().andOperator(*matchCriteria.toTypedArray()))
//
// return listOf(
// matchStage,
// addFieldsOI,
// lookupFromAccount,
// unwindFromAccount,
// lookupToAccount,
// unwindToAccount,
// lookupCategory,
// unwindCategory,
// lookupCreatedBy,
// unwindCreatedBy,
// lookupUpdatedBy,
// unwindUpdatedBy
// )
// }
//
// override suspend fun getTransactions(
// spaceId: String,
// filter: TransactionService.TransactionsFilter,
// sortBy: String,
// sortDirection: String
// ): List<Transaction> {
// val allowedSortFields = setOf("date", "amount", "category.name", "createdAt")
// require(sortBy in allowedSortFields) { "Invalid sort field: $sortBy" }
//
// val direction = when (sortDirection.uppercase()) {
// "ASC" -> Direction.ASC
// "DESC" -> Direction.DESC
// else -> throw IllegalArgumentException("Sort direction must be 'ASC' or 'DESC'")
// }
// val basicAggregation = basicAggregation(spaceId)
//
// val sort = sort(Sort.by(direction, sortBy))
// val matchCriteria = mutableListOf<Criteria>()
// filter.dateFrom?.let { matchCriteria.add(Criteria.where("date").gte(it)) }
// filter.dateTo?.let { matchCriteria.add(Criteria.where("date").lte(it)) }
// val matchStage = match(Criteria().andOperator(*matchCriteria.toTypedArray()))
// val aggregation =
// newAggregation(
// matchStage,
// *basicAggregation.toTypedArray(),
// sort
// )
//
// return mongoTemplate.aggregate(aggregation, "transactions", Transaction::class.java)
// .collectList()
// .awaitSingle()
// }
//
// override suspend fun getTransaction(
// spaceId: String,
// transactionId: String
// ): Transaction {
// val matchCriteria = mutableListOf<Criteria>()
// matchCriteria.add(Criteria.where("spaceId").`is`(spaceId))
// matchCriteria.add(Criteria.where("_id").`is`(ObjectId(transactionId)))
// val matchStage = match(Criteria().andOperator(*matchCriteria.toTypedArray()))
//
// val aggregation =
// newAggregation(
// matchStage,
// )
// return mongoTemplate.aggregate(aggregation, "transactions", Transaction::class.java)
// .awaitFirstOrNull() ?: throw NotFoundException("Transaction with ID $transactionId not found")
// }
//
// override suspend fun createTransaction(
// spaceId: String,
// transaction: TransactionDTO.CreateTransactionDTO
// ): Transaction {
// if (transaction.type == Transaction.TransactionType.TRANSFER && transaction.toAccountId == null) {
// throw IllegalArgumentException("Cannot create a transaction with type TRANSFER without a toAccountId")
// }
// val category = categoryService.getCategory(spaceId, transaction.categoryId)
// if (transaction.type != Transaction.TransactionType.TRANSFER && transaction.type.name != category.type.name) {
// throw IllegalArgumentException("Transaction type should match with category type")
// }
// val transaction = Transaction(
// spaceId = spaceId,
// type = transaction.type,
// kind = transaction.kind,
// categoryId = transaction.categoryId,
// comment = transaction.comment,
// amount = transaction.amount,
// fees = transaction.fees,
// date = transaction.date,
// fromAccountId = transaction.fromAccountId,
// toAccountId = transaction.toAccountId,
// )
// return transactionRepo.save(transaction).awaitSingle()
// }
//
// override suspend fun updateTransaction(
// spaceId: String,
// transaction: TransactionDTO.UpdateTransactionDTO
// ): Transaction {
// if (transaction.type == Transaction.TransactionType.TRANSFER && transaction.toAccountId == null) {
// throw IllegalArgumentException("Cannot edit a transaction with type TRANSFER without a toAccountId")
// }
// val exitingTx = getTransaction(spaceId, transaction.id)
// val transaction = exitingTx.copy(
// spaceId = exitingTx.spaceId,
// type = transaction.type,
// kind = transaction.kind,
// categoryId = transaction.category,
// comment = transaction.comment,
// amount = transaction.amount,
// fees = transaction.fees,
// date = transaction.date,
// fromAccountId = transaction.fromAccountId,
// toAccountId = transaction.toAccountId,
// )
// return transactionRepo.save(transaction).awaitSingle()
// }
//
// override suspend fun deleteTransaction(spaceId: String, transactionId: String) {
// val transaction = getTransaction(spaceId, transactionId)
// transaction.isDeleted = true
// transactionRepo.save(transaction).awaitSingle()
// }
//
//
//}

View File

@@ -1,12 +1,9 @@
package space.luminic.finance.services package space.luminic.finance.services
import kotlinx.coroutines.reactor.awaitSingle
import kotlinx.coroutines.reactor.awaitSingleOrNull
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.cache.annotation.Cacheable import org.springframework.cache.annotation.Cacheable
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import space.luminic.finance.mappers.UserMapper
import space.luminic.finance.models.NotFoundException import space.luminic.finance.models.NotFoundException
import space.luminic.finance.models.User import space.luminic.finance.models.User
import space.luminic.finance.repos.UserRepo import space.luminic.finance.repos.UserRepo
@@ -17,36 +14,30 @@ class UserService(val userRepo: UserRepo) {
@Cacheable("users", key = "#username") @Cacheable("users", key = "#username")
suspend fun getByUsername(username: String): User { fun getByUsername(username: String): User {
return userRepo.findByUsername(username).awaitSingleOrNull() return userRepo.findByUsername(username) ?: throw NotFoundException("User with username: $username not found")
?: throw NotFoundException("User with username: $username not found")
} }
suspend fun getById(id: String): User { fun getById(id: Int): User {
return userRepo.findById(id).awaitSingleOrNull() return userRepo.findById(id) ?: throw NotFoundException("User with id: $id not found")
?: throw NotFoundException("User with id: $id not found")
} }
suspend fun getUserByTelegramId(telegramId: Long): User { fun getUserByTelegramId(telegramId: Long): User {
return userRepo.findByTgId(telegramId.toString()).awaitSingleOrNull() return userRepo.findByTgId(telegramId.toString())?: throw NotFoundException("User with telegramId: $telegramId not found")
?: throw NotFoundException("User with telegramId: $telegramId not found")
} }
@Cacheable("users", key = "#username") @Cacheable("users", key = "#username")
suspend fun getByUserNameWoPass(username: String): User { fun getByUserNameWoPass(username: String): User {
return userRepo.findByUsernameWOPassword(username).awaitSingleOrNull() return userRepo.findByUsername(username) ?: throw NotFoundException("User with username: $username not found")
?: throw NotFoundException("User with username: $username not found")
} }
@Cacheable("usersList") @Cacheable("usersList")
suspend fun getUsers(): List<User> { fun getUsers(): List<User> {
return userRepo.findAll() return userRepo.findAll()
.collectList()
.awaitSingle()
} }
} }

View File

@@ -1,35 +1,28 @@
package space.luminic.finance.utils package space.luminic.finance.utils
import org.slf4j.LoggerFactory //
import org.springframework.scheduling.annotation.Scheduled //@Component
import org.springframework.stereotype.Component //class ScheduledTasks(private val subscriptionService: SubscriptionService,
import space.luminic.finance.models.PushMessage // private val tokenService: TokenService) {
import space.luminic.finance.services.SubscriptionService // private val logger = LoggerFactory.getLogger(ScheduledTasks::class.java)
import space.luminic.finance.services.TokenService //
// @Scheduled(cron = "0 30 19 * * *")
// suspend fun sendNotificationOfMoneyFilling() {
@Component // subscriptionService.sendToAll(
class ScheduledTasks(private val subscriptionService: SubscriptionService, // PushMessage(
private val tokenService: TokenService) { // title = "Время заполнять траты!🤑",
private val logger = LoggerFactory.getLogger(ScheduledTasks::class.java) // body = "Проверь все ли траты внесены!",
// icon = "/apple-touch-icon.png",
@Scheduled(cron = "0 30 19 * * *") // badge = "/apple-touch-icon.png",
suspend fun sendNotificationOfMoneyFilling() { // url = "https://luminic.space/transactions/create"
subscriptionService.sendToAll( // )
PushMessage( // )
title = "Время заполнять траты!🤑", // }
body = "Проверь все ли траты внесены!", //
icon = "/apple-touch-icon.png", // @Scheduled(cron = "0 0 0 * * *")
badge = "/apple-touch-icon.png", // fun deleteExpiredTokens() {
url = "https://luminic.space/transactions/create" // tokenService.deleteExpiredTokens()
) // }
) //
} //
//}
@Scheduled(cron = "0 0 0 * * *")
fun deleteExpiredTokens() {
tokenService.deleteExpiredTokens()
}
}

View File

@@ -11,10 +11,10 @@ spring.data.mongodb.database=budger-app
management.endpoints.web.exposure.include=* management.endpoints.web.exposure.include=*
management.endpoint.health.show-details=always management.endpoint.health.show-details=always
nlp.address=http://127.0.0.1:8000
spring.datasource.url=jdbc:postgresql://213.226.71.138:5432/luminic-space-db
spring.datasource.url=jdbc:postgresql://213.183.51.243/familybudget_app spring.datasource.username=luminicspace
spring.datasource.username=familybudget_app spring.datasource.password=LS1q2w3e4r!
spring.datasource.password=FB1q2w3e4r!
telegram.bot.token = 6972242509:AAGyXuL3T-BNE4XMoo_qvtaYxw_SuiS_dDs telegram.bot.token = 6972242509:AAGyXuL3T-BNE4XMoo_qvtaYxw_SuiS_dDs

View File

@@ -3,13 +3,26 @@ spring.data.mongodb.uri=mongodb://budger-app:BA1q2w3e4r!@luminic.space:27017/bud
logging.level.org.springframework.web=DEBUG logging.level.org.springframework.web=DEBUG
logging.level.org.springframework.data = DEBUG logging.level.org.springframework.data = DEBUG
logging.level.org.springframework.data.mongodb.core.ReactiveMongoTemplate=DEBUG logging.level.org.springframework.data.jpa = DEBUG
#logging.level.org.springframework.data.mongodb.core.ReactiveMongoTemplate=DEBUG
logging.level.org.springframework.security = DEBUG logging.level.org.springframework.security = DEBUG
logging.level.org.springframework.data.mongodb.code = DEBUG #logging.level.org.springframework.data.mongodb.code = DEBUG
logging.level.org.springframework.web.reactive=DEBUG logging.level.org.springframework.web.reactive=DEBUG
logging.level.org.mongodb.driver.protocol.command = DEBUG logging.level.org.mongodb.driver.protocol.command = DEBUG
logging.level.org.springframework.jdbc.core=DEBUG
logging.level.org.springframework.jdbc.core.StatementCreatorUtils=TRACE
logging.level.org.springframework.jdbc=DEBUG
logging.level.org.springframework.jdbc.datasource=DEBUG
logging.level.org.springframework.jdbc.support=DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
management.endpoints.web.exposure.include=* management.endpoints.web.exposure.include=*
management.endpoint.health.show-details=always management.endpoint.health.show-details=always
telegram.bot.token=6972242509:AAGyXuL3T-BNE4XMoo_qvtaYxw_SuiS_dDs telegram.bot.token=6972242509:AAGyXuL3T-BNE4XMoo_qvtaYxw_SuiS_dDs
nlp.address=http://127.0.0.1:8000 nlp.address=http://127.0.0.1:8000
spring.datasource.url=jdbc:postgresql://213.226.71.138:5432/luminic-space-db
spring.datasource.username=luminicspace
spring.datasource.password=LS1q2w3e4r!

View File

@@ -19,3 +19,9 @@ logging.level.org.mongodb.driver.protocol.command = INFO
telegram.bot.token = 6662300972:AAFXjk_h0AUCy4bORC12UcdXbYnh2QSVKAY telegram.bot.token = 6662300972:AAFXjk_h0AUCy4bORC12UcdXbYnh2QSVKAY
nlp.address=https://nlp.luminic.space nlp.address=https://nlp.luminic.space
spring.datasource.url=jdbc:postgresql://213.226.71.138:5432/luminic-space-db
spring.datasource.username=luminicspace
spring.datasource.password=LS1q2w3e4r!

View File

@@ -1,11 +1,11 @@
spring.application.name=budger-app spring.application.name=budger-app
server.port=8082 server.port=8082
#server.servlet.context-path=/api server.servlet.context-path=/api
spring.webflux.base-path=/api #spring.webflux.base-path=/api
spring.profiles.active=prod spring.profiles.active=prod
spring.main.web-application-type=reactive spring.main.web-application-type=servlet
server.compression.enabled=true server.compression.enabled=true
server.compression.mime-types=application/json server.compression.mime-types=application/json
@@ -16,7 +16,7 @@ spring.servlet.multipart.max-request-size=10MB
storage.location: static storage.location: static
spring.jackson.default-property-inclusion=non_null
# Expose prometheus, health, and info endpoints # Expose prometheus, health, and info endpoints
#management.endpoints.web.exposure.include=prometheus,health,info #management.endpoints.web.exposure.include=prometheus,health,info
management.endpoints.web.exposure.include=* management.endpoints.web.exposure.include=*
@@ -25,5 +25,9 @@ management.endpoints.web.exposure.include=*
management.prometheus.metrics.export.enabled=true management.prometheus.metrics.export.enabled=true
telegram.bot.username = expenses_diary_bot telegram.bot.username = expenses_diary_bot
spring.flyway.enabled=true
spring.flyway.locations=classpath:db/migration
spring.flyway.baseline-on-migrate= false
spring.flyway.schemas=finance
spring.jpa.properties.hibernate.default_schema=finance
spring.jpa.properties.hibernate.default_batch_fetch_size=50

View File

@@ -0,0 +1,26 @@
ALTER TABLE finance.users
ALTER COLUMN created_at SEt DEFAULT now(),
ALTER COLUMN updated_at SEt DEFAULT now();
ALTER TABLE finance.spaces
ALTER COLUMN created_at SEt DEFAULT now(),
ALTER COLUMN updated_at SEt DEFAULT now();
ALTER TABLE finance.transactions
ALTER COLUMN type SET DATA TYPE VARCHAR(255),
ALTER COLUMN kind SET DATA TYPE VARCHAR(255),
ALTER COLUMN created_at SET DEFAULT now(),
ALTER COLUMN updated_at SEt DEFAULT now();
ALTER TABLE finance.tokens
ALTER COLUMN status SET DATA TYPE VARCHAR(255),
ALTER COLUMN issued_at SET DEFAULT now();
ALTER TABLE finance.categories_etalon
ALTER COLUMN type SET DATA TYPE VARCHAR(255);

View File

@@ -0,0 +1,2 @@
alter table finance.categories
add column if not exists description varchar(1000);

View File

@@ -0,0 +1,5 @@
alter table finance.categories
add column if not exists description varchar(1000);
alter table finance.categories
alter column type set data type varchar;

Some files were not shown because too many files have changed in this diff Show More