init
This commit is contained in:
45
.gitignore
vendored
Normal file
45
.gitignore
vendored
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
.gradle
|
||||||
|
build/
|
||||||
|
!gradle/wrapper/gradle-wrapper.jar
|
||||||
|
!**/src/main/**/build/
|
||||||
|
!**/src/test/**/build/
|
||||||
|
|
||||||
|
### IntelliJ IDEA ###
|
||||||
|
.idea/modules.xml
|
||||||
|
.idea/jarRepositories.xml
|
||||||
|
.idea/compiler.xml
|
||||||
|
.idea/libraries/
|
||||||
|
*.iws
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
out/
|
||||||
|
!**/src/main/**/out/
|
||||||
|
!**/src/test/**/out/
|
||||||
|
|
||||||
|
### Kotlin ###
|
||||||
|
.kotlin
|
||||||
|
|
||||||
|
### Eclipse ###
|
||||||
|
.apt_generated
|
||||||
|
.classpath
|
||||||
|
.factorypath
|
||||||
|
.project
|
||||||
|
.settings
|
||||||
|
.springBeans
|
||||||
|
.sts4-cache
|
||||||
|
bin/
|
||||||
|
!**/src/main/**/bin/
|
||||||
|
!**/src/test/**/bin/
|
||||||
|
|
||||||
|
### NetBeans ###
|
||||||
|
/nbproject/private/
|
||||||
|
/nbbuild/
|
||||||
|
/dist/
|
||||||
|
/nbdist/
|
||||||
|
/.nb-gradle/
|
||||||
|
|
||||||
|
### VS Code ###
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
### Mac OS ###
|
||||||
|
.DS_Store
|
||||||
89
build.gradle.kts
Normal file
89
build.gradle.kts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
plugins {
|
||||||
|
kotlin("jvm") version "1.9.25"
|
||||||
|
kotlin("plugin.spring") version "1.9.25"
|
||||||
|
id("org.springframework.boot") version "3.4.0"
|
||||||
|
id("io.spring.dependency-management") version "1.1.6"
|
||||||
|
kotlin("plugin.serialization") version "2.1.0"
|
||||||
|
id("application")
|
||||||
|
}
|
||||||
|
|
||||||
|
group = "space.luminic"
|
||||||
|
version = "v2"
|
||||||
|
|
||||||
|
application {
|
||||||
|
mainClass.set("space.luminic.finance.MainKt") // Укажи путь к главному классу
|
||||||
|
}
|
||||||
|
|
||||||
|
java {
|
||||||
|
toolchain {
|
||||||
|
languageVersion = JavaLanguageVersion.of(17)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
configurations {
|
||||||
|
compileOnly {
|
||||||
|
extendsFrom(configurations.annotationProcessor.get())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
// 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-security")
|
||||||
|
implementation ("org.springframework.boot:spring-boot-starter-actuator")
|
||||||
|
implementation("org.springframework.boot:spring-boot-starter-validation")
|
||||||
|
implementation("org.springframework.boot:spring-boot-starter-actuator")
|
||||||
|
implementation("org.springdoc:springdoc-openapi-starter-webflux-ui:2.8.13")
|
||||||
|
|
||||||
|
implementation("io.r2dbc:r2dbc-postgresql")
|
||||||
|
// Миграции
|
||||||
|
implementation("org.flywaydb:flyway-core")
|
||||||
|
// jackson для jsonb (если маппишь объекты в json)
|
||||||
|
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
|
||||||
|
|
||||||
|
implementation("commons-logging:commons-logging:1.3.4")
|
||||||
|
|
||||||
|
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
|
||||||
|
implementation("org.jetbrains.kotlin:kotlin-reflect")
|
||||||
|
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-reactor:1.7.3")
|
||||||
|
|
||||||
|
implementation("io.jsonwebtoken:jjwt-api:0.11.5")
|
||||||
|
implementation("io.jsonwebtoken:jjwt-impl:0.11.5")
|
||||||
|
implementation("io.jsonwebtoken:jjwt-jackson:0.11.5")
|
||||||
|
implementation("com.interaso:webpush:1.2.0")
|
||||||
|
|
||||||
|
implementation("io.micrometer:micrometer-registry-prometheus")
|
||||||
|
|
||||||
|
implementation("org.telegram:telegrambots:6.9.7.1")
|
||||||
|
implementation("org.telegram:telegrambots-spring-boot-starter:6.9.7.1")
|
||||||
|
implementation("com.opencsv:opencsv:5.10")
|
||||||
|
|
||||||
|
|
||||||
|
compileOnly("org.projectlombok:lombok")
|
||||||
|
annotationProcessor("org.projectlombok:lombok")
|
||||||
|
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
||||||
|
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
|
||||||
|
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
compilerOptions {
|
||||||
|
freeCompilerArgs.addAll("-Xjsr305=strict")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType<Test> {
|
||||||
|
useJUnitPlatform()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
28
src/main/kotlin/space/luminic/finance/Main.kt
Normal file
28
src/main/kotlin/space/luminic/finance/Main.kt
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package space.luminic.finance
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationPropertiesScan
|
||||||
|
import org.springframework.boot.runApplication
|
||||||
|
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 java.util.TimeZone
|
||||||
|
|
||||||
|
|
||||||
|
@SpringBootApplication(scanBasePackages = ["space.luminic.finance"])
|
||||||
|
@EnableReactiveMongoAuditing(auditorAwareRef = "coroutineAuditorAware")
|
||||||
|
@EnableCaching
|
||||||
|
@EnableAsync
|
||||||
|
@EnableScheduling
|
||||||
|
//@EnableConfigurationProperties([TelegramBotProperties::class,)
|
||||||
|
@ConfigurationPropertiesScan(basePackages = ["space.luminic.finance"])
|
||||||
|
@EnableMongoRepositories(basePackages = ["space.luminic.finance.repos"])
|
||||||
|
class Main
|
||||||
|
|
||||||
|
fun main(args: Array<String>) {
|
||||||
|
TimeZone.setDefault(TimeZone.getTimeZone("Europe/Moscow"))
|
||||||
|
runApplication<Main>(*args)
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
56
src/main/kotlin/space/luminic/finance/api/AuthController.kt
Normal file
56
src/main/kotlin/space/luminic/finance/api/AuthController.kt
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
package space.luminic.finance.api
|
||||||
|
|
||||||
|
|
||||||
|
import kotlinx.coroutines.reactive.awaitSingle
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.security.core.context.ReactiveSecurityContextHolder
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder
|
||||||
|
import org.springframework.web.bind.annotation.*
|
||||||
|
import space.luminic.finance.dtos.UserDTO.*
|
||||||
|
import space.luminic.finance.dtos.UserDTO
|
||||||
|
import space.luminic.finance.mappers.UserMapper.toDto
|
||||||
|
import space.luminic.finance.services.AuthService
|
||||||
|
import space.luminic.finance.services.UserService
|
||||||
|
import kotlin.jvm.javaClass
|
||||||
|
import kotlin.to
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/auth")
|
||||||
|
class AuthController(
|
||||||
|
private val userService: UserService,
|
||||||
|
private val authService: AuthService
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val logger = LoggerFactory.getLogger(javaClass)
|
||||||
|
|
||||||
|
@GetMapping("/test")
|
||||||
|
fun test(): String {
|
||||||
|
val authentication = SecurityContextHolder.getContext().authentication
|
||||||
|
logger.info("SecurityContext in controller: $authentication")
|
||||||
|
return "Hello, ${authentication.name}"
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/login")
|
||||||
|
suspend fun login(@RequestBody request: AuthUserDTO): Map<String, String> {
|
||||||
|
val token = authService.login(request.username, request.password)
|
||||||
|
return mapOf("token" to token)
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/register")
|
||||||
|
suspend fun register(@RequestBody request: RegisterUserDTO): UserDTO {
|
||||||
|
return authService.register(request.username, request.password, request.firstName).toDto()
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/tgLogin")
|
||||||
|
suspend fun tgLogin(@RequestHeader("X-Tg-Id") tgId: String): Map<String, String> {
|
||||||
|
val token = authService.tgLogin(tgId)
|
||||||
|
return mapOf("token" to token)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@GetMapping("/me")
|
||||||
|
suspend fun getMe(): UserDTO {
|
||||||
|
val securityContext = ReactiveSecurityContextHolder.getContext().awaitSingle()
|
||||||
|
return userService.getByUsername(securityContext.authentication.name).toDto()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
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.RestController
|
||||||
|
import space.luminic.finance.dtos.CategoryDTO
|
||||||
|
import space.luminic.finance.mappers.CategoryMapper.toDto
|
||||||
|
import space.luminic.finance.services.CategoryService
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/spaces/{spaceId}/categories")
|
||||||
|
@SecurityScheme(
|
||||||
|
name = "bearerAuth",
|
||||||
|
type = SecuritySchemeType.HTTP,
|
||||||
|
bearerFormat = "JWT",
|
||||||
|
scheme = "bearer"
|
||||||
|
)
|
||||||
|
class CategoryController(
|
||||||
|
private val categoryService: CategoryService,
|
||||||
|
service: CategoryService
|
||||||
|
) {
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
suspend fun getCategories(@PathVariable spaceId: String): List<CategoryDTO> {
|
||||||
|
return categoryService.getCategories(spaceId).map { it.toDto() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{categoryId}")
|
||||||
|
suspend fun getCategory(@PathVariable spaceId: String, @PathVariable categoryId: String): CategoryDTO {
|
||||||
|
return categoryService.getCategory(spaceId, categoryId).toDto()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
suspend fun createCategory(
|
||||||
|
@PathVariable spaceId: String,
|
||||||
|
@RequestBody categoryDTO: CategoryDTO.CreateCategoryDTO
|
||||||
|
): CategoryDTO {
|
||||||
|
return categoryService.createCategory(spaceId, categoryDTO).toDto()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@PutMapping("/{categoryId}")
|
||||||
|
suspend fun updateCategory(
|
||||||
|
@PathVariable spaceId: String,
|
||||||
|
@PathVariable categoryId: String,
|
||||||
|
@RequestBody categoryDTO: CategoryDTO.UpdateCategoryDTO
|
||||||
|
): CategoryDTO {
|
||||||
|
return categoryService.updateCategory(spaceId, categoryDTO).toDto()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@DeleteMapping("/{categoryId}")
|
||||||
|
suspend fun deleteCategory(@PathVariable spaceId: String, @PathVariable categoryId: String) {
|
||||||
|
categoryService.deleteCategory(spaceId, categoryId)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
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.RestController
|
||||||
|
import space.luminic.finance.dtos.CurrencyDTO
|
||||||
|
import space.luminic.finance.mappers.CurrencyMapper.toDto
|
||||||
|
import space.luminic.finance.services.CurrencyService
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/references")
|
||||||
|
@SecurityScheme(
|
||||||
|
name = "bearerAuth",
|
||||||
|
type = SecuritySchemeType.HTTP,
|
||||||
|
bearerFormat = "JWT",
|
||||||
|
scheme = "bearer"
|
||||||
|
)
|
||||||
|
class ReferenceController(
|
||||||
|
private val currencyService: CurrencyService
|
||||||
|
) {
|
||||||
|
|
||||||
|
@GetMapping("/currencies")
|
||||||
|
suspend fun getCurrencies(): List<CurrencyDTO> {
|
||||||
|
return currencyService.getCurrencies().map { it.toDto() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/currencies/{currencyCode}")
|
||||||
|
suspend fun getCurrency(@PathVariable currencyCode: String): CurrencyDTO {
|
||||||
|
return currencyService.getCurrency(currencyCode).toDto()
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/currencies")
|
||||||
|
suspend fun createCurrency(@RequestBody currencyDTO: CurrencyDTO): CurrencyDTO {
|
||||||
|
return currencyService.createCurrency(currencyDTO).toDto()
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/currencies/{currencyCode}")
|
||||||
|
suspend fun updateCurrency(@PathVariable currencyCode: String, @RequestBody currencyDTO: CurrencyDTO): CurrencyDTO {
|
||||||
|
return currencyService.updateCurrency(currencyDTO).toDto()
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/currencies/{currencyCode}")
|
||||||
|
suspend fun deleteCurrency(@PathVariable currencyCode: String) {
|
||||||
|
currencyService.deleteCurrency(currencyCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
55
src/main/kotlin/space/luminic/finance/api/SpaceController.kt
Normal file
55
src/main/kotlin/space/luminic/finance/api/SpaceController.kt
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
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.RestController
|
||||||
|
import space.luminic.finance.dtos.SpaceDTO
|
||||||
|
import space.luminic.finance.mappers.SpaceMapper.toDto
|
||||||
|
import space.luminic.finance.models.Space
|
||||||
|
import space.luminic.finance.services.SpaceService
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/spaces")
|
||||||
|
@SecurityScheme(
|
||||||
|
name = "bearerAuth",
|
||||||
|
type = SecuritySchemeType.HTTP,
|
||||||
|
bearerFormat = "JWT",
|
||||||
|
scheme = "bearer"
|
||||||
|
)
|
||||||
|
class SpaceController(
|
||||||
|
private val spaceService: SpaceService,
|
||||||
|
) {
|
||||||
|
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
suspend fun getSpaces(): List<SpaceDTO> {
|
||||||
|
return spaceService.getSpaces().map { it.toDto() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{spaceId}")
|
||||||
|
suspend fun getSpace(@PathVariable spaceId: String): SpaceDTO {
|
||||||
|
return spaceService.getSpace(spaceId).toDto()
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
suspend fun createSpace(@RequestBody space: SpaceDTO.CreateSpaceDTO): SpaceDTO {
|
||||||
|
return spaceService.createSpace(space).toDto()
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{spaceId}")
|
||||||
|
suspend fun updateSpace(@PathVariable spaceId: String, @RequestBody space: SpaceDTO.UpdateSpaceDTO): SpaceDTO {
|
||||||
|
return spaceService.updateSpace(spaceId, space).toDto()
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{spaceId}")
|
||||||
|
suspend fun deleteSpace(@PathVariable spaceId: String) {
|
||||||
|
spaceService.deleteSpace(spaceId)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
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.RestController
|
||||||
|
import space.luminic.finance.dtos.TransactionDTO
|
||||||
|
import space.luminic.finance.mappers.TransactionMapper.toDto
|
||||||
|
import space.luminic.finance.services.TransactionService
|
||||||
|
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/spaces/{spaceId}/transactions")
|
||||||
|
@SecurityScheme(
|
||||||
|
name = "bearerAuth",
|
||||||
|
type = SecuritySchemeType.HTTP,
|
||||||
|
bearerFormat = "JWT",
|
||||||
|
scheme = "bearer"
|
||||||
|
)
|
||||||
|
class TransactionController (
|
||||||
|
private val transactionService: TransactionService,
|
||||||
|
service: TransactionService,
|
||||||
|
transactionService1: TransactionService,
|
||||||
|
){
|
||||||
|
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
suspend fun getTransactions(@PathVariable spaceId: String) : List<TransactionDTO>{
|
||||||
|
return transactionService.getTransactions(spaceId, TransactionService.TransactionsFilter(),"date", "DESC").map { it.toDto() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{transactionId}")
|
||||||
|
suspend fun getTransaction(@PathVariable spaceId: String, @PathVariable transactionId: String): TransactionDTO {
|
||||||
|
return transactionService.getTransaction(spaceId, transactionId).toDto()
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
suspend fun createTransaction(@PathVariable spaceId: String, @RequestBody transactionDTO: TransactionDTO.CreateTransactionDTO): TransactionDTO {
|
||||||
|
return transactionService.createTransaction(spaceId, transactionDTO).toDto()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@PutMapping("/{transactionId}")
|
||||||
|
suspend fun updateTransaction(@PathVariable spaceId: String, @PathVariable transactionId: String, @RequestBody transactionDTO: TransactionDTO.UpdateTransactionDTO): TransactionDTO {
|
||||||
|
return transactionService.updateTransaction(spaceId, transactionDTO).toDto()
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{transactionId}")
|
||||||
|
suspend fun deleteTransaction(@PathVariable spaceId: String, @PathVariable transactionId: String) {
|
||||||
|
transactionService.deleteTransaction(spaceId, transactionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
package space.luminic.finance.api.exceptionHandlers
|
||||||
|
|
||||||
|
import org.springframework.http.HttpStatus
|
||||||
|
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.RestControllerAdvice
|
||||||
|
import org.springframework.web.server.ServerWebExchange
|
||||||
|
import reactor.core.publisher.Mono
|
||||||
|
import space.luminic.finance.configs.AuthException
|
||||||
|
import space.luminic.finance.models.NotFoundException
|
||||||
|
|
||||||
|
@RestControllerAdvice
|
||||||
|
class GlobalExceptionHandler {
|
||||||
|
|
||||||
|
fun constructErrorBody(
|
||||||
|
e: Exception,
|
||||||
|
message: String,
|
||||||
|
status: HttpStatus,
|
||||||
|
request: ServerHttpRequest
|
||||||
|
): Map<String, Any?> {
|
||||||
|
val errorResponse = mapOf(
|
||||||
|
"timestamp" to System.currentTimeMillis(),
|
||||||
|
"status" to status.value(),
|
||||||
|
"error" to message,
|
||||||
|
"message" to e.message,
|
||||||
|
"path" to request.path.value()
|
||||||
|
)
|
||||||
|
return errorResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(AuthException::class)
|
||||||
|
fun handleAuthenticationException(
|
||||||
|
ex: AuthException,
|
||||||
|
exchange: ServerWebExchange
|
||||||
|
): Mono<ResponseEntity<Map<String, Any?>>?> {
|
||||||
|
ex.printStackTrace()
|
||||||
|
|
||||||
|
return Mono.just(
|
||||||
|
ResponseEntity(
|
||||||
|
constructErrorBody(
|
||||||
|
ex,
|
||||||
|
ex.message.toString(),
|
||||||
|
HttpStatus.UNAUTHORIZED,
|
||||||
|
exchange.request
|
||||||
|
), HttpStatus.UNAUTHORIZED
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ExceptionHandler(NotFoundException::class)
|
||||||
|
fun handleNotFoundException(
|
||||||
|
e: NotFoundException,
|
||||||
|
exchange: ServerWebExchange
|
||||||
|
): Mono<ResponseEntity<Map<String, Any?>>?> {
|
||||||
|
e.printStackTrace()
|
||||||
|
|
||||||
|
return Mono.just(
|
||||||
|
ResponseEntity(
|
||||||
|
constructErrorBody(
|
||||||
|
e,
|
||||||
|
e.message.toString(),
|
||||||
|
HttpStatus.NOT_FOUND,
|
||||||
|
exchange.request
|
||||||
|
), HttpStatus.NOT_FOUND
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(IllegalArgumentException::class)
|
||||||
|
fun handleIllegalArgumentException(
|
||||||
|
e: IllegalArgumentException,
|
||||||
|
exchange: ServerWebExchange
|
||||||
|
): Mono<ResponseEntity<Map<String, Any?>>?> {
|
||||||
|
e.printStackTrace()
|
||||||
|
|
||||||
|
return Mono.just(
|
||||||
|
ResponseEntity(
|
||||||
|
constructErrorBody(
|
||||||
|
e,
|
||||||
|
e.message.toString(),
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
exchange.request
|
||||||
|
), HttpStatus.BAD_REQUEST
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(Exception::class)
|
||||||
|
fun handleGenericException(
|
||||||
|
e: Exception,
|
||||||
|
exchange: ServerWebExchange
|
||||||
|
): Mono<out ResponseEntity<out Map<String, Any?>>?> {
|
||||||
|
e.printStackTrace()
|
||||||
|
|
||||||
|
|
||||||
|
return Mono.just(
|
||||||
|
ResponseEntity(
|
||||||
|
constructErrorBody(
|
||||||
|
e,
|
||||||
|
e.message.toString(),
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
exchange.request
|
||||||
|
), HttpStatus.INTERNAL_SERVER_ERROR
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package space.luminic.finance.api.exceptionHandlers
|
||||||
|
import org.springframework.boot.autoconfigure.web.WebProperties
|
||||||
|
import org.springframework.boot.autoconfigure.web.reactive.error.AbstractErrorWebExceptionHandler
|
||||||
|
import org.springframework.boot.web.error.ErrorAttributeOptions
|
||||||
|
import org.springframework.boot.web.reactive.error.ErrorAttributes
|
||||||
|
import org.springframework.context.ApplicationContext
|
||||||
|
import org.springframework.core.annotation.Order
|
||||||
|
import org.springframework.http.MediaType
|
||||||
|
import org.springframework.http.codec.ServerCodecConfigurer
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import org.springframework.web.reactive.function.BodyInserters
|
||||||
|
import org.springframework.web.reactive.function.server.*
|
||||||
|
import reactor.core.publisher.Mono
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@Order(-2)
|
||||||
|
class GlobalErrorWebExceptionHandler(
|
||||||
|
errorAttributes: ErrorAttributes,
|
||||||
|
applicationContext: ApplicationContext,
|
||||||
|
serverCodecConfigurer: ServerCodecConfigurer
|
||||||
|
) : AbstractErrorWebExceptionHandler(
|
||||||
|
errorAttributes,
|
||||||
|
WebProperties.Resources(),
|
||||||
|
applicationContext
|
||||||
|
) {
|
||||||
|
|
||||||
|
init {
|
||||||
|
super.setMessageWriters(serverCodecConfigurer.writers)
|
||||||
|
super.setMessageReaders(serverCodecConfigurer.readers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getRoutingFunction(errorAttributes: ErrorAttributes): RouterFunction<ServerResponse> {
|
||||||
|
return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderErrorResponse(request: ServerRequest): Mono<ServerResponse> {
|
||||||
|
val errorAttributesMap = getErrorAttributes(
|
||||||
|
request,
|
||||||
|
ErrorAttributeOptions.of(
|
||||||
|
ErrorAttributeOptions.Include.MESSAGE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return ServerResponse.status(401)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.body(BodyInserters.fromValue(errorAttributesMap))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package space.luminic.finance.configs
|
||||||
|
|
||||||
|
import kotlinx.coroutines.reactor.mono
|
||||||
|
import org.springframework.http.HttpHeaders
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
|
||||||
|
import org.springframework.security.core.authority.SimpleGrantedAuthority
|
||||||
|
import org.springframework.security.core.context.ReactiveSecurityContextHolder
|
||||||
|
import org.springframework.security.core.context.SecurityContextImpl
|
||||||
|
import org.springframework.security.web.server.context.SecurityContextServerWebExchangeWebFilter
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import org.springframework.web.server.ServerWebExchange
|
||||||
|
import org.springframework.web.server.WebFilterChain
|
||||||
|
import reactor.core.publisher.Mono
|
||||||
|
import space.luminic.finance.services.AuthService
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class BearerTokenFilter(private val authService: AuthService) : SecurityContextServerWebExchangeWebFilter() {
|
||||||
|
// private val logger = LoggerFactory.getLogger(BearerTokenFilter::class.java)
|
||||||
|
|
||||||
|
|
||||||
|
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
|
||||||
|
val token = exchange.request.headers.getFirst(HttpHeaders.AUTHORIZATION)?.removePrefix("Bearer ")
|
||||||
|
|
||||||
|
if (exchange.request.path.value() in listOf(
|
||||||
|
"/api/auth/login",
|
||||||
|
"/api/auth/register",
|
||||||
|
"/api/auth/tgLogin"
|
||||||
|
) || exchange.request.path.value().startsWith("/api/actuator") || exchange.request.path.value()
|
||||||
|
.startsWith("/api/static/")
|
||||||
|
|| exchange.request.path.value()
|
||||||
|
.startsWith("/api/wishlistexternal/")
|
||||||
|
|| exchange.request.path.value().startsWith("/api/swagger-ui") || exchange.request.path.value().startsWith("/api/v3/api-docs")
|
||||||
|
) {
|
||||||
|
return chain.filter(exchange)
|
||||||
|
}
|
||||||
|
|
||||||
|
return if (token != null) {
|
||||||
|
mono {
|
||||||
|
val userDetails = authService.isTokenValid(token) // suspend вызов
|
||||||
|
val authorities = userDetails.roles.map { SimpleGrantedAuthority(it) }
|
||||||
|
val securityContext = SecurityContextImpl(
|
||||||
|
UsernamePasswordAuthenticationToken(userDetails.username, null, authorities)
|
||||||
|
)
|
||||||
|
securityContext
|
||||||
|
}.flatMap { securityContext ->
|
||||||
|
chain.filter(exchange)
|
||||||
|
.contextWrite(ReactiveSecurityContextHolder.withSecurityContext(Mono.just(securityContext)))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Mono.error(AuthException("Authorization token is missing"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
open class AuthException(msg: String) : RuntimeException(msg)
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
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 {
|
||||||
|
// @Bean
|
||||||
|
// fun httpTraceRepository(): HttpTraceRepository {
|
||||||
|
// return InMemoryHttpTraceRepository()
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
|
|
||||||
|
@ConfigurationProperties(prefix = "nlp")
|
||||||
|
data class NLPConfig(
|
||||||
|
val address: String,
|
||||||
|
)
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package space.luminic.finance.configs
|
||||||
|
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean
|
||||||
|
import org.springframework.context.annotation.Configuration
|
||||||
|
import org.springframework.http.HttpMethod
|
||||||
|
import org.springframework.security.config.web.server.SecurityWebFiltersOrder
|
||||||
|
import org.springframework.security.config.web.server.ServerHttpSecurity
|
||||||
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder
|
||||||
|
import org.springframework.security.web.server.SecurityWebFilterChain
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
class SecurityConfig(
|
||||||
|
|
||||||
|
) {
|
||||||
|
@Bean
|
||||||
|
fun securityWebFilterChain(
|
||||||
|
http: ServerHttpSecurity,
|
||||||
|
bearerTokenFilter: BearerTokenFilter
|
||||||
|
): SecurityWebFilterChain {
|
||||||
|
return http
|
||||||
|
.csrf { it.disable() }
|
||||||
|
.cors { it.configurationSource(corsConfigurationSource()) }
|
||||||
|
|
||||||
|
.logout { it.disable() }
|
||||||
|
.authorizeExchange {
|
||||||
|
it.pathMatchers(HttpMethod.POST, "/auth/login", "/auth/register", "/auth/tgLogin").permitAll()
|
||||||
|
it.pathMatchers("/actuator/**", "/static/**").permitAll()
|
||||||
|
it.pathMatchers("/wishlistexternal/**").permitAll()
|
||||||
|
it.pathMatchers("/swagger-ui/**", "/swagger-ui.html", "/v3/api-docs/**").permitAll()
|
||||||
|
it.anyExchange().authenticated()
|
||||||
|
}
|
||||||
|
.addFilterAt(
|
||||||
|
bearerTokenFilter,
|
||||||
|
SecurityWebFiltersOrder.AUTHENTICATION
|
||||||
|
) // BearerTokenFilter только для authenticated
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
fun passwordEncoder(): PasswordEncoder {
|
||||||
|
return BCryptPasswordEncoder()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
fun corsConfigurationSource(): org.springframework.web.cors.reactive.CorsConfigurationSource {
|
||||||
|
val corsConfig = org.springframework.web.cors.CorsConfiguration()
|
||||||
|
corsConfig.allowedOrigins =
|
||||||
|
listOf("https://luminic.space", "http://localhost:5173") // Ваши разрешённые источники
|
||||||
|
corsConfig.allowedMethods = listOf("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH")
|
||||||
|
corsConfig.allowedHeaders = listOf("*")
|
||||||
|
corsConfig.allowCredentials = true
|
||||||
|
|
||||||
|
val source = org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource()
|
||||||
|
source.registerCorsConfiguration("/**", corsConfig)
|
||||||
|
return source
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package space.luminic.finance.configs
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Value
|
||||||
|
import org.springframework.context.annotation.Configuration
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.nio.file.Paths
|
||||||
|
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
class StorageConfig(@Value("\${storage.location}") location: String) {
|
||||||
|
|
||||||
|
val rootLocation: Path = Paths.get(location)
|
||||||
|
|
||||||
|
init {
|
||||||
|
Files.createDirectories(rootLocation) // Создаем папку, если её нет
|
||||||
|
}
|
||||||
|
}
|
||||||
37
src/main/kotlin/space/luminic/finance/dtos/AccountDTO.kt
Normal file
37
src/main/kotlin/space/luminic/finance/dtos/AccountDTO.kt
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
69
src/main/kotlin/space/luminic/finance/dtos/BudgetDTO.kt
Normal file
69
src/main/kotlin/space/luminic/finance/dtos/BudgetDTO.kt
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
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>
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
29
src/main/kotlin/space/luminic/finance/dtos/CategoryDTO.kt
Normal file
29
src/main/kotlin/space/luminic/finance/dtos/CategoryDTO.kt
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package space.luminic.finance.dtos
|
||||||
|
|
||||||
|
import space.luminic.finance.models.Category.CategoryType
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
|
data class CategoryDTO(
|
||||||
|
val id: String,
|
||||||
|
val type: CategoryType,
|
||||||
|
val name: String,
|
||||||
|
val icon: String,
|
||||||
|
val createdBy: UserDTO? = null,
|
||||||
|
val createdAt: Instant,
|
||||||
|
val updatedBy: UserDTO? = null,
|
||||||
|
val updatedAt: Instant,
|
||||||
|
|
||||||
|
) {
|
||||||
|
data class CreateCategoryDTO(
|
||||||
|
val name: String,
|
||||||
|
val type: CategoryType,
|
||||||
|
val icon: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class UpdateCategoryDTO(
|
||||||
|
val id: String,
|
||||||
|
val name: String,
|
||||||
|
val type: CategoryType,
|
||||||
|
val icon: String,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package space.luminic.finance.dtos
|
||||||
|
|
||||||
|
data class CurrencyDTO(
|
||||||
|
val code: String,
|
||||||
|
val name: String,
|
||||||
|
val symbol: String
|
||||||
|
){
|
||||||
|
}
|
||||||
20
src/main/kotlin/space/luminic/finance/dtos/GoalDTO.kt
Normal file
20
src/main/kotlin/space/luminic/finance/dtos/GoalDTO.kt
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package space.luminic.finance.dtos
|
||||||
|
|
||||||
|
import space.luminic.finance.models.Goal.GoalType
|
||||||
|
import java.math.BigDecimal
|
||||||
|
import java.time.Instant
|
||||||
|
import java.time.LocalDate
|
||||||
|
|
||||||
|
data class GoalDTO(
|
||||||
|
val id: String,
|
||||||
|
val type: GoalType,
|
||||||
|
val name: String,
|
||||||
|
val description: String? = null,
|
||||||
|
val amount: BigDecimal,
|
||||||
|
val date: LocalDate,
|
||||||
|
val createdBy: UserDTO,
|
||||||
|
val createdAt: Instant,
|
||||||
|
val updatedBy: UserDTO,
|
||||||
|
val updatedAt: Instant,
|
||||||
|
) {
|
||||||
|
}
|
||||||
24
src/main/kotlin/space/luminic/finance/dtos/SpaceDTO.kt
Normal file
24
src/main/kotlin/space/luminic/finance/dtos/SpaceDTO.kt
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package space.luminic.finance.dtos
|
||||||
|
|
||||||
|
import space.luminic.finance.models.User
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
|
data class SpaceDTO(
|
||||||
|
val id: String? = null,
|
||||||
|
val name: String,
|
||||||
|
val owner: UserDTO,
|
||||||
|
val participants: List<UserDTO> = emptyList(),
|
||||||
|
val createdBy: UserDTO? = null,
|
||||||
|
val createdAt: Instant,
|
||||||
|
var updatedBy: UserDTO? = null,
|
||||||
|
var updatedAt: Instant,
|
||||||
|
) {
|
||||||
|
data class CreateSpaceDTO(
|
||||||
|
val name: String,
|
||||||
|
val createBasicCategories: Boolean = true,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class UpdateSpaceDTO(
|
||||||
|
val name: String
|
||||||
|
)
|
||||||
|
}
|
||||||
52
src/main/kotlin/space/luminic/finance/dtos/TransactionDTO.kt
Normal file
52
src/main/kotlin/space/luminic/finance/dtos/TransactionDTO.kt
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
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.TransactionType
|
||||||
|
import space.luminic.finance.models.User
|
||||||
|
import java.math.BigDecimal
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
|
data class TransactionDTO(
|
||||||
|
val id: String? = null,
|
||||||
|
var parentId: String? = null,
|
||||||
|
val type: TransactionType = TransactionType.EXPENSE,
|
||||||
|
val kind: TransactionKind = TransactionKind.INSTANT,
|
||||||
|
val categoryId: String,
|
||||||
|
val category: CategoryDTO? = null,
|
||||||
|
val comment: String,
|
||||||
|
val amount: BigDecimal,
|
||||||
|
val fees: BigDecimal = BigDecimal.ZERO,
|
||||||
|
val fromAccount: AccountDTO? = null,
|
||||||
|
val toAccount: AccountDTO? = null,
|
||||||
|
val date: Instant,
|
||||||
|
val createdBy: String? = null,
|
||||||
|
val updatedBy: String? = null,
|
||||||
|
) {
|
||||||
|
data class CreateTransactionDTO(
|
||||||
|
val type: TransactionType = TransactionType.EXPENSE,
|
||||||
|
val kind: TransactionKind = TransactionKind.INSTANT,
|
||||||
|
val categoryId: String,
|
||||||
|
val comment: String,
|
||||||
|
val amount: BigDecimal,
|
||||||
|
val fees: BigDecimal = BigDecimal.ZERO,
|
||||||
|
val fromAccountId: String,
|
||||||
|
val toAccountId: String? = null,
|
||||||
|
val date: Instant
|
||||||
|
)
|
||||||
|
|
||||||
|
data class UpdateTransactionDTO(
|
||||||
|
val id: String,
|
||||||
|
val type: TransactionType = TransactionType.EXPENSE,
|
||||||
|
val kind: TransactionKind = TransactionKind.INSTANT,
|
||||||
|
val category: String,
|
||||||
|
val comment: String,
|
||||||
|
val amount: BigDecimal,
|
||||||
|
val fees: BigDecimal = BigDecimal.ZERO,
|
||||||
|
val fromAccountId: String,
|
||||||
|
val toAccountId: String? = null,
|
||||||
|
val date: Instant
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
30
src/main/kotlin/space/luminic/finance/dtos/UserDTO.kt
Normal file
30
src/main/kotlin/space/luminic/finance/dtos/UserDTO.kt
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package space.luminic.finance.dtos
|
||||||
|
|
||||||
|
import space.luminic.finance.models.User
|
||||||
|
|
||||||
|
data class UserDTO (
|
||||||
|
var id: String,
|
||||||
|
val username: String,
|
||||||
|
var firstName: String,
|
||||||
|
var tgId: String? = null,
|
||||||
|
var tgUserName: String? = null,
|
||||||
|
var roles: List<String>
|
||||||
|
|
||||||
|
) {
|
||||||
|
|
||||||
|
data class AuthUserDTO (
|
||||||
|
var username: String,
|
||||||
|
var password: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class RegisterUserDTO (
|
||||||
|
var username: String,
|
||||||
|
var firstName: String,
|
||||||
|
var password: String,
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
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()},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package space.luminic.finance.mappers
|
||||||
|
|
||||||
|
import space.luminic.finance.dtos.CategoryDTO
|
||||||
|
import space.luminic.finance.mappers.UserMapper.toDto
|
||||||
|
import space.luminic.finance.models.Category
|
||||||
|
|
||||||
|
object CategoryMapper {
|
||||||
|
|
||||||
|
|
||||||
|
fun Category.toDto() = CategoryDTO(
|
||||||
|
id = this.id ?: throw IllegalArgumentException("category id is not set"),
|
||||||
|
type = this.type,
|
||||||
|
name = this.name,
|
||||||
|
icon = this.icon,
|
||||||
|
createdBy = this.createdBy?.toDto(),
|
||||||
|
createdAt = this.createdAt ?: throw IllegalArgumentException("created at is not set"),
|
||||||
|
updatedBy = this.updatedBy?.toDto(),
|
||||||
|
updatedAt = this.updatedAt ?: throw IllegalArgumentException("updated at is not set"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package space.luminic.finance.mappers
|
||||||
|
|
||||||
|
import space.luminic.finance.dtos.CurrencyDTO
|
||||||
|
import space.luminic.finance.models.Currency
|
||||||
|
|
||||||
|
object CurrencyMapper {
|
||||||
|
|
||||||
|
fun Currency.toDto() = CurrencyDTO(
|
||||||
|
code, name, symbol
|
||||||
|
)
|
||||||
|
}
|
||||||
21
src/main/kotlin/space/luminic/finance/mappers/GoalMapper.kt
Normal file
21
src/main/kotlin/space/luminic/finance/mappers/GoalMapper.kt
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package space.luminic.finance.mappers
|
||||||
|
|
||||||
|
import space.luminic.finance.dtos.GoalDTO
|
||||||
|
import space.luminic.finance.mappers.UserMapper.toDto
|
||||||
|
import space.luminic.finance.models.Goal
|
||||||
|
|
||||||
|
object GoalMapper {
|
||||||
|
|
||||||
|
fun Goal.toDto() = GoalDTO(
|
||||||
|
id = this.id ?: throw IllegalArgumentException("Goal id is not provided"),
|
||||||
|
type = this.type,
|
||||||
|
name = this.name,
|
||||||
|
amount = this.goalAmount,
|
||||||
|
date = this.goalDate,
|
||||||
|
createdBy = (this.createdBy ?: throw IllegalArgumentException("created by not provided")).toDto(),
|
||||||
|
createdAt = this.createdAt ?: throw IllegalArgumentException("created at not provided"),
|
||||||
|
updatedBy = this.updatedBy?.toDto() ?: throw IllegalArgumentException("updated by not provided"),
|
||||||
|
updatedAt = this.updatedAt ?: throw IllegalArgumentException("updatedAt not provided"),
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
18
src/main/kotlin/space/luminic/finance/mappers/SpaceMapper.kt
Normal file
18
src/main/kotlin/space/luminic/finance/mappers/SpaceMapper.kt
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package space.luminic.finance.mappers
|
||||||
|
|
||||||
|
import space.luminic.finance.dtos.SpaceDTO
|
||||||
|
import space.luminic.finance.mappers.UserMapper.toDto
|
||||||
|
import space.luminic.finance.models.Space
|
||||||
|
|
||||||
|
object SpaceMapper {
|
||||||
|
fun Space.toDto() = SpaceDTO(
|
||||||
|
id = this.id,
|
||||||
|
name = this.name,
|
||||||
|
owner = this.owner?.toDto() ?: throw IllegalArgumentException("Owner is not provided"),
|
||||||
|
participants = this.participants?.map { it.toDto() } ?: emptyList(),
|
||||||
|
createdBy = this.createdBy?.toDto(),
|
||||||
|
createdAt = this.createdAt ?: throw IllegalArgumentException("createdAt is not provided"),
|
||||||
|
updatedBy = this.updatedBy?.toDto(),
|
||||||
|
updatedAt = this.updatedAt ?: throw IllegalArgumentException("updatedAt is not provided"),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package space.luminic.finance.mappers
|
||||||
|
|
||||||
|
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.UserMapper.toDto
|
||||||
|
import space.luminic.finance.models.Transaction
|
||||||
|
|
||||||
|
object TransactionMapper {
|
||||||
|
fun Transaction.toDto() = TransactionDTO(
|
||||||
|
id = this.id,
|
||||||
|
parentId = this.parentId,
|
||||||
|
type = this.type,
|
||||||
|
kind = this.kind,
|
||||||
|
categoryId = this.categoryId,
|
||||||
|
category = this.category?.toDto(),
|
||||||
|
comment = this.comment,
|
||||||
|
amount = this.amount,
|
||||||
|
fees = this.fees,
|
||||||
|
fromAccount = this.fromAccount?.toDto(),
|
||||||
|
toAccount = this.toAccount?.toDto(),
|
||||||
|
date = this.date,
|
||||||
|
createdBy = this.createdBy?.username,
|
||||||
|
updatedBy = this.updatedBy?.username,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
16
src/main/kotlin/space/luminic/finance/mappers/UserMapper.kt
Normal file
16
src/main/kotlin/space/luminic/finance/mappers/UserMapper.kt
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package space.luminic.finance.mappers
|
||||||
|
|
||||||
|
import space.luminic.finance.dtos.UserDTO
|
||||||
|
import space.luminic.finance.models.User
|
||||||
|
|
||||||
|
object UserMapper {
|
||||||
|
// UserMapping.kt
|
||||||
|
fun User.toDto(): UserDTO = UserDTO(
|
||||||
|
id = this.id!!,
|
||||||
|
username = this.username,
|
||||||
|
firstName = this.firstName,
|
||||||
|
tgId = this.tgId,
|
||||||
|
tgUserName = this.tgUserName,
|
||||||
|
roles = this.roles
|
||||||
|
)
|
||||||
|
}
|
||||||
51
src/main/kotlin/space/luminic/finance/models/Account.kt
Normal file
51
src/main/kotlin/space/luminic/finance/models/Account.kt
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
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("Долговой"),
|
||||||
|
}
|
||||||
|
}
|
||||||
62
src/main/kotlin/space/luminic/finance/models/Budget.kt
Normal file
62
src/main/kotlin/space/luminic/finance/models/Budget.kt
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
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("Специальный")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
49
src/main/kotlin/space/luminic/finance/models/Category.kt
Normal file
49
src/main/kotlin/space/luminic/finance/models/Category.kt
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
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.mongodb.core.mapping.Document
|
||||||
|
import org.springframework.data.annotation.Transient
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
|
|
||||||
|
@Document(collection = "categories")
|
||||||
|
data class Category(
|
||||||
|
@Id val id: String? = null,
|
||||||
|
val spaceId: String,
|
||||||
|
val type: CategoryType,
|
||||||
|
val name: String,
|
||||||
|
val icon: String,
|
||||||
|
var isDeleted: Boolean = false,
|
||||||
|
|
||||||
|
@CreatedBy
|
||||||
|
val createdById: String? = null,
|
||||||
|
@CreatedDate
|
||||||
|
val createdAt: 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) {
|
||||||
|
INCOME("Поступления"),
|
||||||
|
EXPENSE("Расходы")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Document(collection = "categories_etalon")
|
||||||
|
data class CategoryEtalon(
|
||||||
|
@Id val id: String? = null,
|
||||||
|
val type: CategoryType,
|
||||||
|
val name: String,
|
||||||
|
val icon: String
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
13
src/main/kotlin/space/luminic/finance/models/Currency.kt
Normal file
13
src/main/kotlin/space/luminic/finance/models/Currency.kt
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
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(
|
||||||
|
@Id val code: String,
|
||||||
|
val name: String,
|
||||||
|
val symbol: String
|
||||||
|
)
|
||||||
|
|
||||||
18
src/main/kotlin/space/luminic/finance/models/CurrencyRate.kt
Normal file
18
src/main/kotlin/space/luminic/finance/models/CurrencyRate.kt
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
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.time.LocalDate
|
||||||
|
|
||||||
|
@Document(collection = "currency_rates")
|
||||||
|
data class CurrencyRate(
|
||||||
|
@Id val id: String? = null,
|
||||||
|
val currencyCode: String,
|
||||||
|
val rate: BigDecimal,
|
||||||
|
val date: LocalDate
|
||||||
|
)
|
||||||
|
{
|
||||||
|
@ReadOnlyProperty var currency: Currency? = null
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package space.luminic.finance.models
|
||||||
|
|
||||||
|
|
||||||
|
open class NotFoundException(message: String) : Exception(message)
|
||||||
|
open class TelegramBotException(message: String, val chatId: Long) : Exception(message)
|
||||||
40
src/main/kotlin/space/luminic/finance/models/Goal.kt
Normal file
40
src/main/kotlin/space/luminic/finance/models/Goal.kt
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
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.mongodb.core.mapping.Document
|
||||||
|
import org.springframework.data.annotation.Transient
|
||||||
|
import space.luminic.finance.dtos.UserDTO
|
||||||
|
import java.math.BigDecimal
|
||||||
|
import java.text.Bidi
|
||||||
|
import java.time.Instant
|
||||||
|
import java.time.LocalDate
|
||||||
|
|
||||||
|
|
||||||
|
@Document(collection = "goals")
|
||||||
|
data class Goal(
|
||||||
|
@Id val id: String? = null,
|
||||||
|
val spaceId: String,
|
||||||
|
val type: GoalType,
|
||||||
|
val name: String,
|
||||||
|
val description: String? = null,
|
||||||
|
val goalAmount: BigDecimal,
|
||||||
|
val goalDate: LocalDate,
|
||||||
|
@CreatedBy val createdById: String,
|
||||||
|
@Transient val createdBy: User? = null,
|
||||||
|
@CreatedDate val createdAt: Instant? = null,
|
||||||
|
@LastModifiedBy val updatedById: String,
|
||||||
|
@Transient val updatedBy: User? = null,
|
||||||
|
@LastModifiedDate val updatedAt: Instant? = null,
|
||||||
|
) {
|
||||||
|
|
||||||
|
enum class GoalType(val displayName: String, val icon: String) {
|
||||||
|
AUTO("Авто", "🏎️"),
|
||||||
|
VACATION("Отпуск", "🏖️"),
|
||||||
|
GOODS("Покупка", "🛍️"),
|
||||||
|
OTHER("Прочее", "💸")
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/main/kotlin/space/luminic/finance/models/PushMessage.kt
Normal file
20
src/main/kotlin/space/luminic/finance/models/PushMessage.kt
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package space.luminic.finance.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class PushMessage(
|
||||||
|
|
||||||
|
// title: str
|
||||||
|
// body: str
|
||||||
|
// icon: str
|
||||||
|
// badge: str
|
||||||
|
// url: str
|
||||||
|
val title: String,
|
||||||
|
val body: String,
|
||||||
|
val icon: String? = null,
|
||||||
|
val badge: String? = null,
|
||||||
|
val url: String? = null
|
||||||
|
|
||||||
|
)
|
||||||
36
src/main/kotlin/space/luminic/finance/models/Space.kt
Normal file
36
src/main/kotlin/space/luminic/finance/models/Space.kt
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
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.mongodb.core.mapping.Document
|
||||||
|
import org.springframework.data.annotation.Transient
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
|
|
||||||
|
@Document(collection = "spaces")
|
||||||
|
data class Space (
|
||||||
|
@Id val id: String? = null,
|
||||||
|
val name: String,
|
||||||
|
val ownerId: String,
|
||||||
|
val participantsIds: List<String> = emptyList(),
|
||||||
|
var isDeleted: Boolean = false,
|
||||||
|
@CreatedBy val createdById: String? = null,
|
||||||
|
@CreatedDate val createdAt: Instant? = null,
|
||||||
|
@LastModifiedBy val updatedById: String? = null,
|
||||||
|
@LastModifiedDate var updatedAt: Instant? = null,
|
||||||
|
) {
|
||||||
|
@ReadOnlyProperty var owner: User? = null
|
||||||
|
|
||||||
|
@ReadOnlyProperty var participants: List<User>? = null
|
||||||
|
|
||||||
|
@ReadOnlyProperty var createdBy: User? = null
|
||||||
|
|
||||||
|
@ReadOnlyProperty var updatedBy: User? = null
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
23
src/main/kotlin/space/luminic/finance/models/Subscription.kt
Normal file
23
src/main/kotlin/space/luminic/finance/models/Subscription.kt
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package space.luminic.finance.models
|
||||||
|
|
||||||
|
import org.springframework.data.annotation.Id
|
||||||
|
import org.springframework.data.mongodb.core.mapping.DBRef
|
||||||
|
import org.springframework.data.mongodb.core.mapping.Document
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
|
@Document(collection = "subscriptions")
|
||||||
|
data class Subscription(
|
||||||
|
@Id val id: String? = null,
|
||||||
|
@DBRef val user: User? = null,
|
||||||
|
val endpoint: String,
|
||||||
|
val auth: String,
|
||||||
|
val p256dh: String,
|
||||||
|
var isActive: Boolean,
|
||||||
|
val createdAt: Instant = Instant.now(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
data class SubscriptionDTO (
|
||||||
|
val endpoint: String,
|
||||||
|
val keys: Map<String, String>
|
||||||
|
)
|
||||||
22
src/main/kotlin/space/luminic/finance/models/Token.kt
Normal file
22
src/main/kotlin/space/luminic/finance/models/Token.kt
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package space.luminic.finance.models
|
||||||
|
|
||||||
|
import org.springframework.data.annotation.Id
|
||||||
|
import org.springframework.data.mongodb.core.mapping.Document
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
@Document(collection = "tokens")
|
||||||
|
data class Token(
|
||||||
|
@Id
|
||||||
|
val id: String? = null,
|
||||||
|
val token: String,
|
||||||
|
val username: String,
|
||||||
|
val issuedAt: LocalDateTime,
|
||||||
|
val expiresAt: LocalDateTime,
|
||||||
|
val status: TokenStatus = TokenStatus.ACTIVE
|
||||||
|
) {
|
||||||
|
|
||||||
|
enum class TokenStatus {
|
||||||
|
ACTIVE, REVOKED, EXPIRED
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
59
src/main/kotlin/space/luminic/finance/models/Transaction.kt
Normal file
59
src/main/kotlin/space/luminic/finance/models/Transaction.kt
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
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.mongodb.core.mapping.Document
|
||||||
|
import org.springframework.data.annotation.Transient
|
||||||
|
import java.math.BigDecimal
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
|
@Document(collection = "transactions")
|
||||||
|
data class Transaction(
|
||||||
|
@Id val id: String? = null,
|
||||||
|
val spaceId: String,
|
||||||
|
var parentId: String? = null,
|
||||||
|
val type: TransactionType = TransactionType.EXPENSE,
|
||||||
|
val kind: TransactionKind = TransactionKind.INSTANT,
|
||||||
|
val categoryId: String,
|
||||||
|
|
||||||
|
val comment: String,
|
||||||
|
val amount: BigDecimal,
|
||||||
|
val fees: BigDecimal = BigDecimal.ZERO,
|
||||||
|
val fromAccountId: String,
|
||||||
|
val toAccountId: String? = null,
|
||||||
|
val date: Instant = Instant.now(),
|
||||||
|
var isDeleted: Boolean = false,
|
||||||
|
@CreatedBy
|
||||||
|
val createdById: String? = null,
|
||||||
|
@CreatedDate
|
||||||
|
val createdAt: Instant? = null,
|
||||||
|
@LastModifiedBy
|
||||||
|
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) {
|
||||||
|
INCOME("Поступления"),
|
||||||
|
EXPENSE("Расходы"),
|
||||||
|
TRANSFER("Перевод")
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class TransactionKind(val displayName: String) {
|
||||||
|
PLANNING("Плановая"),
|
||||||
|
INSTANT("Текущая")
|
||||||
|
}
|
||||||
|
}
|
||||||
26
src/main/kotlin/space/luminic/finance/models/User.kt
Normal file
26
src/main/kotlin/space/luminic/finance/models/User.kt
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package space.luminic.finance.models
|
||||||
|
|
||||||
|
|
||||||
|
import org.springframework.data.annotation.Id
|
||||||
|
import org.springframework.data.mongodb.core.mapping.Document
|
||||||
|
import org.springframework.data.annotation.Transient
|
||||||
|
import java.time.LocalDate
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
@Document("users")
|
||||||
|
data class User(
|
||||||
|
@Id
|
||||||
|
var id: String? = null,
|
||||||
|
val username: String,
|
||||||
|
var firstName: String,
|
||||||
|
var tgId: String? = null,
|
||||||
|
var tgUserName: String? = null,
|
||||||
|
var password: String,
|
||||||
|
var isActive: Boolean = true,
|
||||||
|
var regDate: LocalDate = LocalDate.now(),
|
||||||
|
val createdAt: LocalDateTime = LocalDateTime.now(),
|
||||||
|
var roles: MutableList<String> = mutableListOf(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package space.luminic.finance.repos
|
||||||
|
|
||||||
|
import org.springframework.data.mongodb.repository.ReactiveMongoRepository
|
||||||
|
import space.luminic.finance.models.Account
|
||||||
|
|
||||||
|
interface AccountRepo : ReactiveMongoRepository<Account, String> {
|
||||||
|
}
|
||||||
14
src/main/kotlin/space/luminic/finance/repos/BudgetRepo.kt
Normal file
14
src/main/kotlin/space/luminic/finance/repos/BudgetRepo.kt
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
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>
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package space.luminic.finance.repos
|
||||||
|
|
||||||
|
import org.springframework.data.mongodb.repository.ReactiveMongoRepository
|
||||||
|
import space.luminic.finance.models.Category
|
||||||
|
|
||||||
|
interface CategoryRepo: ReactiveMongoRepository<Category, String> {
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CategoryEtalonRepo: ReactiveMongoRepository<Category.CategoryEtalon, String>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package space.luminic.finance.repos
|
||||||
|
|
||||||
|
import org.springframework.data.mongodb.repository.ReactiveMongoRepository
|
||||||
|
import space.luminic.finance.models.Currency
|
||||||
|
import space.luminic.finance.models.CurrencyRate
|
||||||
|
|
||||||
|
interface CurrencyRepo: ReactiveMongoRepository<Currency, String>
|
||||||
|
interface CurrencyRateRepo: ReactiveMongoRepository<CurrencyRate, String>
|
||||||
8
src/main/kotlin/space/luminic/finance/repos/SpaceRepo.kt
Normal file
8
src/main/kotlin/space/luminic/finance/repos/SpaceRepo.kt
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
package space.luminic.finance.repos
|
||||||
|
|
||||||
|
import org.springframework.data.mongodb.core.ReactiveMongoTemplate
|
||||||
|
import org.springframework.data.mongodb.repository.ReactiveMongoRepository
|
||||||
|
import space.luminic.finance.models.Space
|
||||||
|
|
||||||
|
interface SpaceRepo: ReactiveMongoRepository<Space, String> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
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 reactor.core.publisher.Flux
|
||||||
|
import space.luminic.finance.models.Subscription
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
interface SubscriptionRepo : ReactiveMongoRepository<Subscription, String> {
|
||||||
|
|
||||||
|
@Query("{ \$and: [ " +
|
||||||
|
"{ 'user': { '\$ref': 'users', '\$id': ?0 } }, " +
|
||||||
|
"{ 'isActive': true } " +
|
||||||
|
"]}")
|
||||||
|
fun findByUserIdAndIsActive(userId: ObjectId): Flux<Subscription>
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
15
src/main/kotlin/space/luminic/finance/repos/TokenRepo.kt
Normal file
15
src/main/kotlin/space/luminic/finance/repos/TokenRepo.kt
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package space.luminic.finance.repos
|
||||||
|
|
||||||
|
import org.springframework.data.mongodb.repository.ReactiveMongoRepository
|
||||||
|
import org.springframework.stereotype.Repository
|
||||||
|
import reactor.core.publisher.Mono
|
||||||
|
import space.luminic.finance.models.Token
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
interface TokenRepo: ReactiveMongoRepository<Token, String> {
|
||||||
|
|
||||||
|
fun findByToken(token: String): Mono<Token>
|
||||||
|
|
||||||
|
fun deleteByExpiresAtBefore(dateTime: LocalDateTime)
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package space.luminic.finance.repos
|
||||||
|
|
||||||
|
import org.springframework.data.mongodb.repository.ReactiveMongoRepository
|
||||||
|
import space.luminic.finance.models.Transaction
|
||||||
|
|
||||||
|
interface TransactionRepo: ReactiveMongoRepository<Transaction, String> {
|
||||||
|
}
|
||||||
19
src/main/kotlin/space/luminic/finance/repos/UserRepo.kt
Normal file
19
src/main/kotlin/space/luminic/finance/repos/UserRepo.kt
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
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 reactor.core.publisher.Mono
|
||||||
|
import space.luminic.finance.models.User
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
interface UserRepo : ReactiveMongoRepository<User, String> {
|
||||||
|
|
||||||
|
|
||||||
|
@Query(value = "{ 'username': ?0 }", fields = "{ 'password': 0 }")
|
||||||
|
fun findByUsernameWOPassword(username: String): Mono<User>
|
||||||
|
|
||||||
|
fun findByUsername(username: String): Mono<User>
|
||||||
|
|
||||||
|
fun findByTgId(id: String): Mono<User>
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
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()
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
109
src/main/kotlin/space/luminic/finance/services/AuthService.kt
Normal file
109
src/main/kotlin/space/luminic/finance/services/AuthService.kt
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
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.security.core.context.ReactiveSecurityContextHolder
|
||||||
|
import org.springframework.security.core.userdetails.UsernameNotFoundException
|
||||||
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import space.luminic.finance.configs.AuthException
|
||||||
|
import space.luminic.finance.models.Token
|
||||||
|
import space.luminic.finance.models.User
|
||||||
|
import space.luminic.finance.repos.UserRepo
|
||||||
|
import space.luminic.finance.utils.JWTUtil
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.time.ZoneId
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class AuthService(
|
||||||
|
private val userRepository: UserRepo,
|
||||||
|
private val tokenService: TokenService,
|
||||||
|
private val jwtUtil: JWTUtil,
|
||||||
|
private val userService: UserService,
|
||||||
|
|
||||||
|
) {
|
||||||
|
private val passwordEncoder = BCryptPasswordEncoder()
|
||||||
|
|
||||||
|
suspend fun getSecurityUser(): User {
|
||||||
|
val securityContextHolder = ReactiveSecurityContextHolder.getContext().awaitSingleOrNull()
|
||||||
|
?: throw AuthException("Authentication failed")
|
||||||
|
val authentication = securityContextHolder.authentication
|
||||||
|
|
||||||
|
val username = authentication.name
|
||||||
|
// Получаем пользователя по имени
|
||||||
|
return userService.getByUsername(username)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun login(username: String, password: String): String {
|
||||||
|
val user = userRepository.findByUsername(username).awaitFirstOrNull()
|
||||||
|
?: throw UsernameNotFoundException("Пользователь не найден")
|
||||||
|
return if (passwordEncoder.matches(password, user.password)) {
|
||||||
|
val token = jwtUtil.generateToken(user.username)
|
||||||
|
val expireAt = Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24 * 10)
|
||||||
|
tokenService.saveToken(
|
||||||
|
token = token,
|
||||||
|
username = username,
|
||||||
|
expiresAt = LocalDateTime.ofInstant(
|
||||||
|
expireAt.toInstant(),
|
||||||
|
ZoneId.systemDefault()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
token
|
||||||
|
} else {
|
||||||
|
throw IllegalArgumentException("Ошибка логина или пароля")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun tgLogin(tgId: String): String {
|
||||||
|
val user =
|
||||||
|
userRepository.findByTgId(tgId).awaitSingleOrNull() ?: throw UsernameNotFoundException("Пользователь не найден")
|
||||||
|
|
||||||
|
val token = jwtUtil.generateToken(user.username)
|
||||||
|
val expireAt = Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24 * 10)
|
||||||
|
tokenService.saveToken(
|
||||||
|
token = token,
|
||||||
|
username = user.username,
|
||||||
|
expiresAt = LocalDateTime.ofInstant(
|
||||||
|
expireAt.toInstant(),
|
||||||
|
ZoneId.systemDefault()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return token
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun register(username: String, password: String, firstName: String): User {
|
||||||
|
val user = userRepository.findByUsername(username).awaitSingleOrNull()
|
||||||
|
if (user == null) {
|
||||||
|
var newUser = User(
|
||||||
|
username = username,
|
||||||
|
password = passwordEncoder.encode(password), // Шифрование пароля
|
||||||
|
firstName = firstName,
|
||||||
|
roles = mutableListOf("USER")
|
||||||
|
)
|
||||||
|
newUser = userRepository.save(newUser).awaitSingle()
|
||||||
|
return newUser
|
||||||
|
} else throw IllegalArgumentException("Пользователь уже зарегистрирован")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Cacheable(cacheNames = ["tokens"], key = "#token")
|
||||||
|
suspend fun isTokenValid(token: String): User {
|
||||||
|
val tokenDetails = tokenService.getToken(token).awaitFirstOrNull() ?: throw AuthException("Токен не валиден")
|
||||||
|
when {
|
||||||
|
tokenDetails.status == Token.TokenStatus.ACTIVE && tokenDetails.expiresAt.isAfter(LocalDateTime.now()) -> {
|
||||||
|
return userService.getByUsername(tokenDetails.username)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
tokenService.revokeToken(tokenDetails.token)
|
||||||
|
throw AuthException("Токен истек или не валиден")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package space.luminic.finance.services
|
||||||
|
|
||||||
|
import space.luminic.finance.dtos.BudgetDTO
|
||||||
|
import space.luminic.finance.dtos.CategoryDTO
|
||||||
|
import space.luminic.finance.models.Category
|
||||||
|
import space.luminic.finance.models.Space
|
||||||
|
|
||||||
|
interface CategoryService {
|
||||||
|
suspend fun getCategories(spaceId: String): List<Category>
|
||||||
|
suspend fun getCategory(spaceId: String, id: String): Category
|
||||||
|
suspend fun createCategory(spaceId: String, category: CategoryDTO.CreateCategoryDTO): Category
|
||||||
|
suspend fun updateCategory(spaceId: String,category: CategoryDTO.UpdateCategoryDTO): Category
|
||||||
|
suspend fun deleteCategory(spaceId: String, id: String)
|
||||||
|
suspend fun createCategoriesForSpace(spaceId: String): List<Category>
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
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.models.Space
|
||||||
|
import space.luminic.finance.repos.CategoryEtalonRepo
|
||||||
|
import space.luminic.finance.repos.CategoryRepo
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class CategoryServiceImpl(
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
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!!
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package space.luminic.finance.services
|
||||||
|
|
||||||
|
import space.luminic.finance.dtos.CurrencyDTO
|
||||||
|
import space.luminic.finance.models.Currency
|
||||||
|
import space.luminic.finance.models.CurrencyRate
|
||||||
|
|
||||||
|
interface CurrencyService {
|
||||||
|
|
||||||
|
suspend fun getCurrencies(): List<Currency>
|
||||||
|
suspend fun getCurrency(currencyCode: String): Currency
|
||||||
|
suspend fun createCurrency(currency: CurrencyDTO): Currency
|
||||||
|
suspend fun updateCurrency(currency: CurrencyDTO): Currency
|
||||||
|
suspend fun deleteCurrency(currencyCode: String)
|
||||||
|
suspend fun createCurrencyRate(currencyCode: String): CurrencyRate
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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 CurrencyServiceImpl(
|
||||||
|
private val currencyRepo: CurrencyRepo,
|
||||||
|
private val currencyRateRepo: CurrencyRateRepo
|
||||||
|
) : CurrencyService {
|
||||||
|
|
||||||
|
override suspend fun getCurrencies(): List<Currency> {
|
||||||
|
return currencyRepo.findAll().collectList().awaitSingle()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getCurrency(currencyCode: String): Currency {
|
||||||
|
return currencyRepo.findById(currencyCode).awaitSingle()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun createCurrency(currency: CurrencyDTO): Currency {
|
||||||
|
val createdCurrency = Currency(currency.code, currency.name, currency.symbol)
|
||||||
|
return currencyRepo.save(createdCurrency).awaitSingle()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun updateCurrency(currency: CurrencyDTO): Currency {
|
||||||
|
val existingCurrency = currencyRepo.findById(currency.code).awaitSingle()
|
||||||
|
val newCurrency = existingCurrency.copy(name = currency.name, symbol = currency.symbol)
|
||||||
|
return currencyRepo.save(newCurrency).awaitSingle()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun deleteCurrency(currencyCode: String) {
|
||||||
|
currencyRepo.deleteById(currencyCode).awaitSingle()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun createCurrencyRate(currencyCode: String): CurrencyRate {
|
||||||
|
return currencyRateRepo.save(
|
||||||
|
CurrencyRate(
|
||||||
|
currencyCode = currencyCode,
|
||||||
|
rate = BigDecimal(12.0),
|
||||||
|
date = LocalDate.now(),
|
||||||
|
)
|
||||||
|
).awaitSingle()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package space.luminic.finance.services
|
||||||
|
|
||||||
|
import space.luminic.finance.dtos.SpaceDTO
|
||||||
|
import space.luminic.finance.models.Budget
|
||||||
|
import space.luminic.finance.models.Space
|
||||||
|
|
||||||
|
interface SpaceService {
|
||||||
|
|
||||||
|
suspend fun checkSpace(spaceId: String): Space
|
||||||
|
suspend fun getSpaces(): List<Space>
|
||||||
|
suspend fun getSpace(id: String): Space
|
||||||
|
suspend fun createSpace(space: SpaceDTO.CreateSpaceDTO): Space
|
||||||
|
suspend fun updateSpace(spaceId: String, space: SpaceDTO.UpdateSpaceDTO): Space
|
||||||
|
suspend fun deleteSpace(spaceId: String)
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
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 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 space.luminic.finance.dtos.SpaceDTO
|
||||||
|
import space.luminic.finance.models.Budget
|
||||||
|
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 SpaceServiceImpl(
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
package space.luminic.finance.services
|
||||||
|
|
||||||
|
|
||||||
|
import com.interaso.webpush.VapidKeys
|
||||||
|
import com.interaso.webpush.WebPushService
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.reactive.awaitSingle
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import org.bson.types.ObjectId
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.dao.DuplicateKeyException
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import space.luminic.finance.models.PushMessage
|
||||||
|
import space.luminic.finance.models.Subscription
|
||||||
|
import space.luminic.finance.models.SubscriptionDTO
|
||||||
|
import space.luminic.finance.models.User
|
||||||
|
import space.luminic.finance.repos.SubscriptionRepo
|
||||||
|
import space.luminic.finance.services.VapidConstants.VAPID_PRIVATE_KEY
|
||||||
|
import space.luminic.finance.services.VapidConstants.VAPID_PUBLIC_KEY
|
||||||
|
import space.luminic.finance.services.VapidConstants.VAPID_SUBJECT
|
||||||
|
import kotlin.collections.forEach
|
||||||
|
import kotlin.jvm.javaClass
|
||||||
|
import kotlin.text.orEmpty
|
||||||
|
|
||||||
|
object VapidConstants {
|
||||||
|
const val VAPID_PUBLIC_KEY =
|
||||||
|
"BKmMyBUhpkcmzYWcYsjH_spqcy0zf_8eVtZo60f7949TgLztCmv3YD0E_vtV2dTfECQ4sdLdPK3ICDcyOkCqr84"
|
||||||
|
const val VAPID_PRIVATE_KEY = "YeJH_0LhnVYN6RdxMidgR6WMYlpGXTJS3HjT9V3NSGI"
|
||||||
|
const val VAPID_SUBJECT = "mailto:voroninvyu@gmail.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class SubscriptionService(private val subscriptionRepo: SubscriptionRepo) {
|
||||||
|
|
||||||
|
private val logger = LoggerFactory.getLogger(javaClass)
|
||||||
|
private val pushService =
|
||||||
|
WebPushService(
|
||||||
|
subject = VAPID_SUBJECT,
|
||||||
|
vapidKeys = VapidKeys.fromUncompressedBytes(VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
suspend fun sendToSpaceOwner(ownerId: String, message: PushMessage) = coroutineScope {
|
||||||
|
val ownerTokens = subscriptionRepo.findByUserIdAndIsActive(ObjectId(ownerId)).collectList().awaitSingle()
|
||||||
|
|
||||||
|
ownerTokens.forEach { token ->
|
||||||
|
launch(Dispatchers.IO) { // Теперь мы точно в корутин скоупе
|
||||||
|
try {
|
||||||
|
sendNotification(token.endpoint, token.p256dh, token.auth, message)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger.error("Ошибка при отправке уведомления: ${e.message}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
suspend fun sendNotification(endpoint: String, p256dh: String, auth: String, payload: PushMessage) {
|
||||||
|
try {
|
||||||
|
pushService.send(
|
||||||
|
payload = Json.encodeToString(payload),
|
||||||
|
endpoint = endpoint,
|
||||||
|
p256dh = p256dh,
|
||||||
|
auth = auth
|
||||||
|
)
|
||||||
|
logger.info("Уведомление успешно отправлено на endpoint: $endpoint")
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger.error("Ошибка при отправке уведомления на endpoint $endpoint: ${e.message}")
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
suspend fun sendToAll(payload: PushMessage) {
|
||||||
|
|
||||||
|
subscriptionRepo.findAll().collectList().awaitSingle().forEach { sub ->
|
||||||
|
|
||||||
|
try {
|
||||||
|
sendNotification(sub.endpoint, sub.p256dh, sub.auth, payload)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
sub.isActive = false
|
||||||
|
subscriptionRepo.save(sub).awaitSingle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
suspend fun subscribe(subscriptionDTO: SubscriptionDTO, user: User): String {
|
||||||
|
val subscription = Subscription(
|
||||||
|
id = null,
|
||||||
|
user = user,
|
||||||
|
endpoint = subscriptionDTO.endpoint,
|
||||||
|
auth = subscriptionDTO.keys["auth"].orEmpty(),
|
||||||
|
p256dh = subscriptionDTO.keys["p256dh"].orEmpty(),
|
||||||
|
isActive = true
|
||||||
|
)
|
||||||
|
|
||||||
|
return try {
|
||||||
|
val savedSubscription = subscriptionRepo.save(subscription).awaitSingle()
|
||||||
|
"Subscription created with ID: ${savedSubscription.id}"
|
||||||
|
} catch (e: DuplicateKeyException) {
|
||||||
|
logger.info("Subscription already exists. Skipping.")
|
||||||
|
"Subscription already exists. Skipping."
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger.error("Error while saving subscription: ${e.message}")
|
||||||
|
throw kotlin.RuntimeException("Error while saving subscription")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package space.luminic.finance.services
|
||||||
|
|
||||||
|
import kotlinx.coroutines.reactor.awaitSingle
|
||||||
|
import org.springframework.cache.annotation.CacheEvict
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import reactor.core.publisher.Mono
|
||||||
|
import space.luminic.finance.models.Token
|
||||||
|
import space.luminic.finance.models.Token.TokenStatus
|
||||||
|
import space.luminic.finance.repos.TokenRepo
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class TokenService(private val tokenRepository: TokenRepo) {
|
||||||
|
|
||||||
|
@CacheEvict("tokens", allEntries = true)
|
||||||
|
suspend fun saveToken(token: String, username: String, expiresAt: LocalDateTime): Token {
|
||||||
|
val newToken = Token(
|
||||||
|
token = token,
|
||||||
|
username = username,
|
||||||
|
issuedAt = LocalDateTime.now(),
|
||||||
|
expiresAt = expiresAt
|
||||||
|
)
|
||||||
|
return tokenRepository.save(newToken).awaitSingle()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getToken(token: String): Mono<Token> {
|
||||||
|
return tokenRepository.findByToken(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun revokeToken(token: String) {
|
||||||
|
val tokenDetail =
|
||||||
|
tokenRepository.findByToken(token).block()!!
|
||||||
|
val updatedToken = tokenDetail.copy(status = TokenStatus.REVOKED)
|
||||||
|
tokenRepository.save(updatedToken).block()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@CacheEvict("tokens", allEntries = true)
|
||||||
|
fun deleteExpiredTokens() {
|
||||||
|
tokenRepository.deleteByExpiresAtBefore(LocalDateTime.now())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package space.luminic.finance.services
|
||||||
|
|
||||||
|
import space.luminic.finance.dtos.TransactionDTO
|
||||||
|
import space.luminic.finance.models.Transaction
|
||||||
|
import java.time.LocalDate
|
||||||
|
|
||||||
|
interface TransactionService {
|
||||||
|
|
||||||
|
data class TransactionsFilter(
|
||||||
|
val accountId: String,
|
||||||
|
val dateFrom: LocalDate? = null,
|
||||||
|
val dateTo: LocalDate? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
suspend fun getTransactions(spaceId: String, filter: TransactionsFilter, sortBy: String, sortDirection: String): List<Transaction>
|
||||||
|
suspend fun getTransaction(spaceId: String, transactionId: String): Transaction
|
||||||
|
suspend fun createTransaction(spaceId: String, transaction: TransactionDTO.CreateTransactionDTO): Transaction
|
||||||
|
suspend fun updateTransaction(spaceId: String, transaction: TransactionDTO.UpdateTransactionDTO): Transaction
|
||||||
|
suspend fun deleteTransaction(spaceId: String, transactionId: String)
|
||||||
|
}
|
||||||
@@ -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 TransactionServiceImpl(
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package space.luminic.finance.services
|
||||||
|
|
||||||
|
|
||||||
|
import kotlinx.coroutines.reactor.awaitSingle
|
||||||
|
import kotlinx.coroutines.reactor.awaitSingleOrNull
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.cache.annotation.Cacheable
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import space.luminic.finance.mappers.UserMapper
|
||||||
|
import space.luminic.finance.models.NotFoundException
|
||||||
|
import space.luminic.finance.models.User
|
||||||
|
import space.luminic.finance.repos.UserRepo
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class UserService(val userRepo: UserRepo) {
|
||||||
|
val logger = LoggerFactory.getLogger(javaClass)
|
||||||
|
|
||||||
|
|
||||||
|
@Cacheable("users", key = "#username")
|
||||||
|
suspend fun getByUsername(username: String): User {
|
||||||
|
return userRepo.findByUsername(username).awaitSingleOrNull()
|
||||||
|
?: throw NotFoundException("User with username: $username not found")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getById(id: String): User {
|
||||||
|
return userRepo.findById(id).awaitSingleOrNull()
|
||||||
|
?: throw NotFoundException("User with id: $id not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getUserByTelegramId(telegramId: Long): User {
|
||||||
|
return userRepo.findByTgId(telegramId.toString()).awaitSingleOrNull()
|
||||||
|
?: throw NotFoundException("User with telegramId: $telegramId not found")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Cacheable("users", key = "#username")
|
||||||
|
suspend fun getByUserNameWoPass(username: String): User {
|
||||||
|
return userRepo.findByUsernameWOPassword(username).awaitSingleOrNull()
|
||||||
|
?: throw NotFoundException("User with username: $username not found")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Cacheable("usersList")
|
||||||
|
suspend fun getUsers(): List<User> {
|
||||||
|
return userRepo.findAll()
|
||||||
|
.collectList()
|
||||||
|
.awaitSingle()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
32
src/main/kotlin/space/luminic/finance/utils/JWTUtil.kt
Normal file
32
src/main/kotlin/space/luminic/finance/utils/JWTUtil.kt
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package space.luminic.finance.utils
|
||||||
|
|
||||||
|
import io.jsonwebtoken.Jwts
|
||||||
|
|
||||||
|
import io.jsonwebtoken.security.Keys
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
|
||||||
|
import space.luminic.finance.services.TokenService
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.time.ZoneId
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class JWTUtil(private val tokenService: TokenService) {
|
||||||
|
|
||||||
|
private val key = Keys.hmacShaKeyFor("MyTusimMyFleximMyEstSilaNasNeVzlomayutEtoNevozmozhno".toByteArray())
|
||||||
|
|
||||||
|
|
||||||
|
fun generateToken(username: String): String {
|
||||||
|
val expireAt = Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24 * 10)
|
||||||
|
val token = Jwts.builder()
|
||||||
|
.setSubject(username)
|
||||||
|
.setIssuedAt(Date())
|
||||||
|
.setExpiration(expireAt) // 10 дней
|
||||||
|
.signWith(key)
|
||||||
|
.compact()
|
||||||
|
return token
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package space.luminic.finance.utils
|
||||||
|
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import space.luminic.finance.models.PushMessage
|
||||||
|
import space.luminic.finance.services.SubscriptionService
|
||||||
|
import space.luminic.finance.services.TokenService
|
||||||
|
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class ScheduledTasks(private val subscriptionService: SubscriptionService,
|
||||||
|
private val tokenService: TokenService) {
|
||||||
|
private val logger = LoggerFactory.getLogger(ScheduledTasks::class.java)
|
||||||
|
|
||||||
|
@Scheduled(cron = "0 30 19 * * *")
|
||||||
|
suspend fun sendNotificationOfMoneyFilling() {
|
||||||
|
subscriptionService.sendToAll(
|
||||||
|
PushMessage(
|
||||||
|
title = "Время заполнять траты!🤑",
|
||||||
|
body = "Проверь все ли траты внесены!",
|
||||||
|
icon = "/apple-touch-icon.png",
|
||||||
|
badge = "/apple-touch-icon.png",
|
||||||
|
url = "https://luminic.space/transactions/create"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Scheduled(cron = "0 0 0 * * *")
|
||||||
|
fun deleteExpiredTokens() {
|
||||||
|
tokenService.deleteExpiredTokens()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
20
src/main/resources/application-dev-local.properties
Normal file
20
src/main/resources/application-dev-local.properties
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
spring.application.name=budger-app
|
||||||
|
|
||||||
|
spring.data.mongodb.host=localhost
|
||||||
|
spring.data.mongodb.port=27018
|
||||||
|
spring.data.mongodb.database=budger-app
|
||||||
|
#spring.data.mongodb.username=budger-app
|
||||||
|
#spring.data.mongodb.password=BA1q2w3e4r!
|
||||||
|
#spring.data.mongodb.authentication-database=admin
|
||||||
|
|
||||||
|
|
||||||
|
management.endpoints.web.exposure.include=*
|
||||||
|
management.endpoint.health.show-details=always
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
spring.datasource.url=jdbc:postgresql://213.183.51.243/familybudget_app
|
||||||
|
spring.datasource.username=familybudget_app
|
||||||
|
spring.datasource.password=FB1q2w3e4r!
|
||||||
|
|
||||||
|
telegram.bot.token = 6972242509:AAGyXuL3T-BNE4XMoo_qvtaYxw_SuiS_dDs
|
||||||
15
src/main/resources/application-dev.properties
Normal file
15
src/main/resources/application-dev.properties
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
spring.application.name=budger-app
|
||||||
|
spring.data.mongodb.uri=mongodb://budger-app:BA1q2w3e4r!@luminic.space:27017/budger-app-v2?authSource=admin&minPoolSize=10&maxPoolSize=100
|
||||||
|
|
||||||
|
logging.level.org.springframework.web=DEBUG
|
||||||
|
logging.level.org.springframework.data = DEBUG
|
||||||
|
logging.level.org.springframework.data.mongodb.core.ReactiveMongoTemplate=DEBUG
|
||||||
|
logging.level.org.springframework.security = DEBUG
|
||||||
|
logging.level.org.springframework.data.mongodb.code = DEBUG
|
||||||
|
logging.level.org.springframework.web.reactive=DEBUG
|
||||||
|
logging.level.org.mongodb.driver.protocol.command = DEBUG
|
||||||
|
|
||||||
|
management.endpoints.web.exposure.include=*
|
||||||
|
management.endpoint.health.show-details=always
|
||||||
|
telegram.bot.token=6972242509:AAGyXuL3T-BNE4XMoo_qvtaYxw_SuiS_dDs
|
||||||
|
nlp.address=http://127.0.0.1:8000
|
||||||
21
src/main/resources/application-prod.properties
Normal file
21
src/main/resources/application-prod.properties
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
spring.application.name=budger-app
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
spring.data.mongodb.uri=mongodb://budger-app:BA1q2w3e4r!@luminic.space:27017/budger-app-v2?authSource=admin&minPoolSize=10&maxPoolSize=100
|
||||||
|
|
||||||
|
|
||||||
|
logging.level.org.springframework.web=INFO
|
||||||
|
logging.level.org.springframework.data = INFO
|
||||||
|
logging.level.org.springframework.data.mongodb.core.ReactiveMongoTemplate=INFO
|
||||||
|
logging.level.org.springframework.security = INFO
|
||||||
|
logging.level.org.springframework.data.mongodb.code = INFO
|
||||||
|
logging.level.org.springframework.web.reactive=INFO
|
||||||
|
logging.level.org.mongodb.driver.protocol.command = INFO
|
||||||
|
|
||||||
|
#management.endpoints.web.exposure.include=*
|
||||||
|
#management.endpoint.metrics.access=read_only
|
||||||
|
|
||||||
|
|
||||||
|
telegram.bot.token = 6662300972:AAFXjk_h0AUCy4bORC12UcdXbYnh2QSVKAY
|
||||||
|
nlp.address=https://nlp.luminic.space
|
||||||
29
src/main/resources/application.properties
Normal file
29
src/main/resources/application.properties
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
spring.application.name=budger-app
|
||||||
|
|
||||||
|
server.port=8082
|
||||||
|
#server.servlet.context-path=/api
|
||||||
|
spring.webflux.base-path=/api
|
||||||
|
|
||||||
|
spring.profiles.active=prod
|
||||||
|
spring.main.web-application-type=reactive
|
||||||
|
|
||||||
|
server.compression.enabled=true
|
||||||
|
server.compression.mime-types=application/json
|
||||||
|
|
||||||
|
# ???????????? ?????? ????? (?? ????????? 1 ??)
|
||||||
|
spring.servlet.multipart.max-file-size=10MB
|
||||||
|
spring.servlet.multipart.max-request-size=10MB
|
||||||
|
|
||||||
|
storage.location: static
|
||||||
|
|
||||||
|
|
||||||
|
# Expose prometheus, health, and info endpoints
|
||||||
|
#management.endpoints.web.exposure.include=prometheus,health,info
|
||||||
|
management.endpoints.web.exposure.include=*
|
||||||
|
|
||||||
|
# Enable Prometheus metrics export
|
||||||
|
management.prometheus.metrics.export.enabled=true
|
||||||
|
|
||||||
|
telegram.bot.username = expenses_diary_bot
|
||||||
|
|
||||||
|
|
||||||
596
src/main/resources/json.json
Normal file
596
src/main/resources/json.json
Normal file
@@ -0,0 +1,596 @@
|
|||||||
|
[{
|
||||||
|
"id": "677bc767c7857460a491bd52",
|
||||||
|
"space": {
|
||||||
|
"id": "67af3c0f652da946a7dd9931",
|
||||||
|
"name": null,
|
||||||
|
"description": null,
|
||||||
|
"owner": null,
|
||||||
|
"users": [],
|
||||||
|
"invites": [],
|
||||||
|
"createdAt": "2025-10-09"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"code": "EXPENSE",
|
||||||
|
"name": "Траты"
|
||||||
|
},
|
||||||
|
"name": "Авто",
|
||||||
|
"description": "Расходы на обслуживание автомобиля, топливо и страховку",
|
||||||
|
"icon": "\uD83D\uDE97",
|
||||||
|
"tags": []
|
||||||
|
}, {
|
||||||
|
"id": "677bc767c7857460a491bd59",
|
||||||
|
"space": {
|
||||||
|
"id": "67af3c0f652da946a7dd9931",
|
||||||
|
"name": null,
|
||||||
|
"description": null,
|
||||||
|
"owner": null,
|
||||||
|
"users": [],
|
||||||
|
"invites": [],
|
||||||
|
"createdAt": "2025-10-09"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"code": "EXPENSE",
|
||||||
|
"name": "Траты"
|
||||||
|
},
|
||||||
|
"name": "Аптеки",
|
||||||
|
"description": "Покупка лекарств и аптечных товаров",
|
||||||
|
"icon": "\uD83D\uDC8A",
|
||||||
|
"tags": []
|
||||||
|
}, {
|
||||||
|
"id": "677bc767c7857460a491bd47",
|
||||||
|
"space": {
|
||||||
|
"id": "67af3c0f652da946a7dd9931",
|
||||||
|
"name": null,
|
||||||
|
"description": null,
|
||||||
|
"owner": null,
|
||||||
|
"users": [],
|
||||||
|
"invites": [],
|
||||||
|
"createdAt": "2025-10-09"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"code": "INCOME",
|
||||||
|
"name": "Поступления"
|
||||||
|
},
|
||||||
|
"name": "Возвраты",
|
||||||
|
"description": "Возврат средств за товары или услуги",
|
||||||
|
"icon": "\uD83D\uDD04",
|
||||||
|
"tags": []
|
||||||
|
}, {
|
||||||
|
"id": "67a1b8e691c96a0f177e09d2",
|
||||||
|
"space": {
|
||||||
|
"id": "67af3c0f652da946a7dd9931",
|
||||||
|
"name": null,
|
||||||
|
"description": null,
|
||||||
|
"owner": null,
|
||||||
|
"users": [],
|
||||||
|
"invites": [],
|
||||||
|
"createdAt": "2025-10-09"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"code": "INCOME",
|
||||||
|
"name": "Поступления"
|
||||||
|
},
|
||||||
|
"name": "Депозиты и счета",
|
||||||
|
"description": "Доходы от размещение денежных средств на депозитах и накопительных счетах",
|
||||||
|
"icon": "\uD83C\uDFA2",
|
||||||
|
"tags": []
|
||||||
|
}, {
|
||||||
|
"id": "677bc767c7857460a491bd54",
|
||||||
|
"space": {
|
||||||
|
"id": "67af3c0f652da946a7dd9931",
|
||||||
|
"name": null,
|
||||||
|
"description": null,
|
||||||
|
"owner": null,
|
||||||
|
"users": [],
|
||||||
|
"invites": [],
|
||||||
|
"createdAt": "2025-10-09"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"code": "EXPENSE",
|
||||||
|
"name": "Траты"
|
||||||
|
},
|
||||||
|
"name": "Дни рождения и подарки",
|
||||||
|
"description": "Покупка подарков и празднование дней рождения",
|
||||||
|
"icon": "\uD83C\uDF81",
|
||||||
|
"tags": []
|
||||||
|
}, {
|
||||||
|
"id": "677bc767c7857460a491bd43",
|
||||||
|
"space": {
|
||||||
|
"id": "67af3c0f652da946a7dd9931",
|
||||||
|
"name": null,
|
||||||
|
"description": null,
|
||||||
|
"owner": null,
|
||||||
|
"users": [],
|
||||||
|
"invites": [],
|
||||||
|
"createdAt": "2025-10-09"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"code": "INCOME",
|
||||||
|
"name": "Поступления"
|
||||||
|
},
|
||||||
|
"name": "Доп.активности",
|
||||||
|
"description": "Доход от подработок, фриланса и других активностей",
|
||||||
|
"icon": "\uD83D\uDCBB",
|
||||||
|
"tags": []
|
||||||
|
}, {
|
||||||
|
"id": "67b83e141fc0575a3f0a383f",
|
||||||
|
"space": {
|
||||||
|
"id": "67af3c0f652da946a7dd9931",
|
||||||
|
"name": null,
|
||||||
|
"description": null,
|
||||||
|
"owner": null,
|
||||||
|
"users": [],
|
||||||
|
"invites": [],
|
||||||
|
"createdAt": "2025-10-09"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"code": "EXPENSE",
|
||||||
|
"name": "Траты"
|
||||||
|
},
|
||||||
|
"name": "Другое",
|
||||||
|
"description": "Категория для других трат",
|
||||||
|
"icon": "\uD83D\uDEAE",
|
||||||
|
"tags": []
|
||||||
|
}, {
|
||||||
|
"id": "677bc767c7857460a491bd4b",
|
||||||
|
"space": {
|
||||||
|
"id": "67af3c0f652da946a7dd9931",
|
||||||
|
"name": null,
|
||||||
|
"description": null,
|
||||||
|
"owner": null,
|
||||||
|
"users": [],
|
||||||
|
"invites": [],
|
||||||
|
"createdAt": "2025-10-09"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"code": "EXPENSE",
|
||||||
|
"name": "Траты"
|
||||||
|
},
|
||||||
|
"name": "ЖКХ",
|
||||||
|
"description": "Оплата коммунальных услуг, электричества, воды и газа",
|
||||||
|
"icon": "\uD83D\uDCA1",
|
||||||
|
"tags": []
|
||||||
|
}, {
|
||||||
|
"id": "677bc767c7857460a491bd41",
|
||||||
|
"space": {
|
||||||
|
"id": "67af3c0f652da946a7dd9931",
|
||||||
|
"name": null,
|
||||||
|
"description": null,
|
||||||
|
"owner": null,
|
||||||
|
"users": [],
|
||||||
|
"invites": [],
|
||||||
|
"createdAt": "2025-10-09"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"code": "INCOME",
|
||||||
|
"name": "Поступления"
|
||||||
|
},
|
||||||
|
"name": "Зарплата",
|
||||||
|
"description": "Регулярный доход от основной работы или должности",
|
||||||
|
"icon": "\uD83D\uDCBC",
|
||||||
|
"tags": []
|
||||||
|
}, {
|
||||||
|
"id": "677bc767c7857460a491bd53",
|
||||||
|
"space": {
|
||||||
|
"id": "67af3c0f652da946a7dd9931",
|
||||||
|
"name": null,
|
||||||
|
"description": null,
|
||||||
|
"owner": null,
|
||||||
|
"users": [],
|
||||||
|
"invites": [],
|
||||||
|
"createdAt": "2025-10-09"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"code": "EXPENSE",
|
||||||
|
"name": "Траты"
|
||||||
|
},
|
||||||
|
"name": "Интернет и связь",
|
||||||
|
"description": "Оплата за мобильную связь, интернет и ТВ",
|
||||||
|
"icon": "\uD83D\uDCE1",
|
||||||
|
"tags": []
|
||||||
|
}, {
|
||||||
|
"id": "677bc767c7857460a491bd4e",
|
||||||
|
"space": {
|
||||||
|
"id": "67af3c0f652da946a7dd9931",
|
||||||
|
"name": null,
|
||||||
|
"description": null,
|
||||||
|
"owner": null,
|
||||||
|
"users": [],
|
||||||
|
"invites": [],
|
||||||
|
"createdAt": "2025-10-09"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"code": "EXPENSE",
|
||||||
|
"name": "Траты"
|
||||||
|
},
|
||||||
|
"name": "Кофе",
|
||||||
|
"description": "Расходы на покупку кофе и других напитков вне дома",
|
||||||
|
"icon": "☕",
|
||||||
|
"tags": []
|
||||||
|
}, {
|
||||||
|
"id": "677bc767c7857460a491bd50",
|
||||||
|
"space": {
|
||||||
|
"id": "67af3c0f652da946a7dd9931",
|
||||||
|
"name": null,
|
||||||
|
"description": null,
|
||||||
|
"owner": null,
|
||||||
|
"users": [],
|
||||||
|
"invites": [],
|
||||||
|
"createdAt": "2025-10-09"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"code": "EXPENSE",
|
||||||
|
"name": "Траты"
|
||||||
|
},
|
||||||
|
"name": "Красота",
|
||||||
|
"description": "Расходы на уход за собой, косметику и услуги красоты",
|
||||||
|
"icon": "\uD83D\uDC84",
|
||||||
|
"tags": []
|
||||||
|
}, {
|
||||||
|
"id": "677bc767c7857460a491bd49",
|
||||||
|
"space": {
|
||||||
|
"id": "67af3c0f652da946a7dd9931",
|
||||||
|
"name": null,
|
||||||
|
"description": null,
|
||||||
|
"owner": null,
|
||||||
|
"users": [],
|
||||||
|
"invites": [],
|
||||||
|
"createdAt": "2025-10-09"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"code": "EXPENSE",
|
||||||
|
"name": "Траты"
|
||||||
|
},
|
||||||
|
"name": "Кредиты и долги",
|
||||||
|
"description": "Платежи по кредитам, займам и другим долгам",
|
||||||
|
"icon": "\uD83D\uDCB3",
|
||||||
|
"tags": [{
|
||||||
|
"code": "loans",
|
||||||
|
"name": "Долги"
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
"id": "677bc767c7857460a491bd46",
|
||||||
|
"space": {
|
||||||
|
"id": "67af3c0f652da946a7dd9931",
|
||||||
|
"name": null,
|
||||||
|
"description": null,
|
||||||
|
"owner": null,
|
||||||
|
"users": [],
|
||||||
|
"invites": [],
|
||||||
|
"createdAt": "2025-10-09"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"code": "INCOME",
|
||||||
|
"name": "Поступления"
|
||||||
|
},
|
||||||
|
"name": "Кэшбек",
|
||||||
|
"description": "Возврат части потраченных денег при покупке товаров и услуг",
|
||||||
|
"icon": "\uD83D\uDCB3",
|
||||||
|
"tags": []
|
||||||
|
}, {
|
||||||
|
"id": "677bc767c7857460a491bd4c",
|
||||||
|
"space": {
|
||||||
|
"id": "67af3c0f652da946a7dd9931",
|
||||||
|
"name": null,
|
||||||
|
"description": null,
|
||||||
|
"owner": null,
|
||||||
|
"users": [],
|
||||||
|
"invites": [],
|
||||||
|
"createdAt": "2025-10-09"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"code": "EXPENSE",
|
||||||
|
"name": "Траты"
|
||||||
|
},
|
||||||
|
"name": "Мебель",
|
||||||
|
"description": "Покупка мебели для дома или квартиры",
|
||||||
|
"icon": "\uD83D\uDECB️",
|
||||||
|
"tags": []
|
||||||
|
}, {
|
||||||
|
"id": "677bc767c7857460a491bd5a",
|
||||||
|
"space": {
|
||||||
|
"id": "67af3c0f652da946a7dd9931",
|
||||||
|
"name": null,
|
||||||
|
"description": null,
|
||||||
|
"owner": null,
|
||||||
|
"users": [],
|
||||||
|
"invites": [],
|
||||||
|
"createdAt": "2025-10-09"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"code": "EXPENSE",
|
||||||
|
"name": "Траты"
|
||||||
|
},
|
||||||
|
"name": "Медицина",
|
||||||
|
"description": "Расходы на медицинские услуги и страховку",
|
||||||
|
"icon": "\uD83C\uDFE5",
|
||||||
|
"tags": []
|
||||||
|
}, {
|
||||||
|
"id": "677bc767c7857460a491bd44",
|
||||||
|
"space": {
|
||||||
|
"id": "67af3c0f652da946a7dd9931",
|
||||||
|
"name": null,
|
||||||
|
"description": null,
|
||||||
|
"owner": null,
|
||||||
|
"users": [],
|
||||||
|
"invites": [],
|
||||||
|
"createdAt": "2025-10-09"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"code": "INCOME",
|
||||||
|
"name": "Поступления"
|
||||||
|
},
|
||||||
|
"name": "Налоговый вычет",
|
||||||
|
"description": "Возврат части уплаченных налогов от государства",
|
||||||
|
"icon": "\uD83C\uDFDB️",
|
||||||
|
"tags": []
|
||||||
|
}, {
|
||||||
|
"id": "677bc767c7857460a491bd5b",
|
||||||
|
"space": {
|
||||||
|
"id": "67af3c0f652da946a7dd9931",
|
||||||
|
"name": null,
|
||||||
|
"description": null,
|
||||||
|
"owner": null,
|
||||||
|
"users": [],
|
||||||
|
"invites": [],
|
||||||
|
"createdAt": "2025-10-09"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"code": "EXPENSE",
|
||||||
|
"name": "Траты"
|
||||||
|
},
|
||||||
|
"name": "Одежда",
|
||||||
|
"description": "Покупка одежды и обуви",
|
||||||
|
"icon": "\uD83D\uDC57",
|
||||||
|
"tags": []
|
||||||
|
}, {
|
||||||
|
"id": "677bc767c7857460a491bd48",
|
||||||
|
"space": {
|
||||||
|
"id": "67af3c0f652da946a7dd9931",
|
||||||
|
"name": null,
|
||||||
|
"description": null,
|
||||||
|
"owner": null,
|
||||||
|
"users": [],
|
||||||
|
"invites": [],
|
||||||
|
"createdAt": "2025-10-09"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"code": "INCOME",
|
||||||
|
"name": "Поступления"
|
||||||
|
},
|
||||||
|
"name": "Остатки",
|
||||||
|
"description": "Нерасходованные средства, оставшиеся с предыдущего периода",
|
||||||
|
"icon": "\uD83D\uDCE6",
|
||||||
|
"tags": []
|
||||||
|
}, {
|
||||||
|
"id": "677bc767c7857460a491bd45",
|
||||||
|
"space": {
|
||||||
|
"id": "67af3c0f652da946a7dd9931",
|
||||||
|
"name": null,
|
||||||
|
"description": null,
|
||||||
|
"owner": null,
|
||||||
|
"users": [],
|
||||||
|
"invites": [],
|
||||||
|
"createdAt": "2025-10-09"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"code": "INCOME",
|
||||||
|
"name": "Поступления"
|
||||||
|
},
|
||||||
|
"name": "Пособия",
|
||||||
|
"description": "Финансовая помощь от государства или других организаций",
|
||||||
|
"icon": "\uD83D\uDCB0",
|
||||||
|
"tags": []
|
||||||
|
}, {
|
||||||
|
"id": "677bc767c7857460a491bd42",
|
||||||
|
"space": {
|
||||||
|
"id": "67af3c0f652da946a7dd9931",
|
||||||
|
"name": null,
|
||||||
|
"description": null,
|
||||||
|
"owner": null,
|
||||||
|
"users": [],
|
||||||
|
"invites": [],
|
||||||
|
"createdAt": "2025-10-09"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"code": "INCOME",
|
||||||
|
"name": "Поступления"
|
||||||
|
},
|
||||||
|
"name": "Премия",
|
||||||
|
"description": "Дополнительное денежное вознаграждение за выполнение задач или проектов",
|
||||||
|
"icon": "\uD83C\uDF89",
|
||||||
|
"tags": []
|
||||||
|
}, {
|
||||||
|
"id": "677bc767c7857460a491bd4a",
|
||||||
|
"space": {
|
||||||
|
"id": "67af3c0f652da946a7dd9931",
|
||||||
|
"name": null,
|
||||||
|
"description": null,
|
||||||
|
"owner": null,
|
||||||
|
"users": [],
|
||||||
|
"invites": [],
|
||||||
|
"createdAt": "2025-10-09"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"code": "EXPENSE",
|
||||||
|
"name": "Траты"
|
||||||
|
},
|
||||||
|
"name": "Продукты",
|
||||||
|
"description": "Расходы на покупку продуктов питания и напитков",
|
||||||
|
"icon": "\uD83C\uDF4E",
|
||||||
|
"tags": []
|
||||||
|
}, {
|
||||||
|
"id": "677bc767c7857460a491bd4d",
|
||||||
|
"space": {
|
||||||
|
"id": "67af3c0f652da946a7dd9931",
|
||||||
|
"name": null,
|
||||||
|
"description": null,
|
||||||
|
"owner": null,
|
||||||
|
"users": [],
|
||||||
|
"invites": [],
|
||||||
|
"createdAt": "2025-10-09"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"code": "EXPENSE",
|
||||||
|
"name": "Траты"
|
||||||
|
},
|
||||||
|
"name": "Прочие услуги",
|
||||||
|
"description": "Прочие расходы на обслуживание и ремонт дома",
|
||||||
|
"icon": "\uD83D\uDD27",
|
||||||
|
"tags": []
|
||||||
|
}, {
|
||||||
|
"id": "677bc767c7857460a491bd56",
|
||||||
|
"space": {
|
||||||
|
"id": "67af3c0f652da946a7dd9931",
|
||||||
|
"name": null,
|
||||||
|
"description": null,
|
||||||
|
"owner": null,
|
||||||
|
"users": [],
|
||||||
|
"invites": [],
|
||||||
|
"createdAt": "2025-10-09"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"code": "EXPENSE",
|
||||||
|
"name": "Траты"
|
||||||
|
},
|
||||||
|
"name": "Путешествия",
|
||||||
|
"description": "Расходы на путешествия, билеты, отели и экскурсии",
|
||||||
|
"icon": "✈️",
|
||||||
|
"tags": []
|
||||||
|
}, {
|
||||||
|
"id": "677bc767c7857460a491bd58",
|
||||||
|
"space": {
|
||||||
|
"id": "67af3c0f652da946a7dd9931",
|
||||||
|
"name": null,
|
||||||
|
"description": null,
|
||||||
|
"owner": null,
|
||||||
|
"users": [],
|
||||||
|
"invites": [],
|
||||||
|
"createdAt": "2025-10-09"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"code": "EXPENSE",
|
||||||
|
"name": "Траты"
|
||||||
|
},
|
||||||
|
"name": "Развлечения",
|
||||||
|
"description": "Расходы на кино, концерты, игры и другие развлечения",
|
||||||
|
"icon": "\uD83C\uDFAE",
|
||||||
|
"tags": []
|
||||||
|
}, {
|
||||||
|
"id": "677bc767c7857460a491bd5d",
|
||||||
|
"space": {
|
||||||
|
"id": "67af3c0f652da946a7dd9931",
|
||||||
|
"name": null,
|
||||||
|
"description": null,
|
||||||
|
"owner": null,
|
||||||
|
"users": [],
|
||||||
|
"invites": [],
|
||||||
|
"createdAt": "2025-10-09"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"code": "EXPENSE",
|
||||||
|
"name": "Траты"
|
||||||
|
},
|
||||||
|
"name": "Ребенок",
|
||||||
|
"description": "Расходы на детей: игрушки, одежда, обучение и уход",
|
||||||
|
"icon": "\uD83E\uDDF8",
|
||||||
|
"tags": []
|
||||||
|
}, {
|
||||||
|
"id": "677bc767c7857460a491bd55",
|
||||||
|
"space": {
|
||||||
|
"id": "67af3c0f652da946a7dd9931",
|
||||||
|
"name": null,
|
||||||
|
"description": null,
|
||||||
|
"owner": null,
|
||||||
|
"users": [],
|
||||||
|
"invites": [],
|
||||||
|
"createdAt": "2025-10-09"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"code": "EXPENSE",
|
||||||
|
"name": "Траты"
|
||||||
|
},
|
||||||
|
"name": "Рестораны и кафе",
|
||||||
|
"description": "Расходы на посещение ресторанов и кафе",
|
||||||
|
"icon": "\uD83C\uDF7D️",
|
||||||
|
"tags": []
|
||||||
|
}, {
|
||||||
|
"id": "677bc767c7857460a491bd4f",
|
||||||
|
"space": {
|
||||||
|
"id": "67af3c0f652da946a7dd9931",
|
||||||
|
"name": null,
|
||||||
|
"description": null,
|
||||||
|
"owner": null,
|
||||||
|
"users": [],
|
||||||
|
"invites": [],
|
||||||
|
"createdAt": "2025-10-09"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"code": "EXPENSE",
|
||||||
|
"name": "Траты"
|
||||||
|
},
|
||||||
|
"name": "Сбережения",
|
||||||
|
"description": "Отчисления в накопления или инвестиционные счета",
|
||||||
|
"icon": "\uD83D\uDCB0",
|
||||||
|
"tags": [{
|
||||||
|
"code": "savings",
|
||||||
|
"name": "Сбережения"
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
"id": "677bc767c7857460a491bd57",
|
||||||
|
"space": {
|
||||||
|
"id": "67af3c0f652da946a7dd9931",
|
||||||
|
"name": null,
|
||||||
|
"description": null,
|
||||||
|
"owner": null,
|
||||||
|
"users": [],
|
||||||
|
"invites": [],
|
||||||
|
"createdAt": "2025-10-09"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"code": "EXPENSE",
|
||||||
|
"name": "Траты"
|
||||||
|
},
|
||||||
|
"name": "Спорт",
|
||||||
|
"description": "Расходы на спортивные мероприятия, тренировки и спорттовары",
|
||||||
|
"icon": "\uD83C\uDFCB️",
|
||||||
|
"tags": []
|
||||||
|
}, {
|
||||||
|
"id": "677bc767c7857460a491bd51",
|
||||||
|
"space": {
|
||||||
|
"id": "67af3c0f652da946a7dd9931",
|
||||||
|
"name": null,
|
||||||
|
"description": null,
|
||||||
|
"owner": null,
|
||||||
|
"users": [],
|
||||||
|
"invites": [],
|
||||||
|
"createdAt": "2025-10-09"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"code": "EXPENSE",
|
||||||
|
"name": "Траты"
|
||||||
|
},
|
||||||
|
"name": "Транспорт",
|
||||||
|
"description": "Расходы на общественный транспорт и такси",
|
||||||
|
"icon": "\uD83D\uDE8C",
|
||||||
|
"tags": []
|
||||||
|
}, {
|
||||||
|
"id": "677bc767c7857460a491bd5c",
|
||||||
|
"space": {
|
||||||
|
"id": "67af3c0f652da946a7dd9931",
|
||||||
|
"name": null,
|
||||||
|
"description": null,
|
||||||
|
"owner": null,
|
||||||
|
"users": [],
|
||||||
|
"invites": [],
|
||||||
|
"createdAt": "2025-10-09"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"code": "EXPENSE",
|
||||||
|
"name": "Траты"
|
||||||
|
},
|
||||||
|
"name": "Электроника",
|
||||||
|
"description": "Покупка гаджетов, бытовой техники и другой электроники",
|
||||||
|
"icon": "\uD83D\uDCF1",
|
||||||
|
"tags": []
|
||||||
|
}]
|
||||||
Reference in New Issue
Block a user