From 040da34ff7f5a864919dc50b60d3544fc312c6b8 Mon Sep 17 00:00:00 2001 From: xds Date: Thu, 16 Oct 2025 15:06:20 +0300 Subject: [PATCH] init --- .gitignore | 45 ++ build.gradle.kts | 89 +++ src/main/kotlin/space/luminic/finance/Main.kt | 28 + .../luminic/finance/api/AccountController.kt | 75 +++ .../luminic/finance/api/AuthController.kt | 56 ++ .../luminic/finance/api/BudgetController.kt | 78 +++ .../luminic/finance/api/CategoryController.kt | 65 ++ .../finance/api/ReferenceController.kt | 53 ++ .../luminic/finance/api/SpaceController.kt | 55 ++ .../finance/api/TransactionController.kt | 60 ++ .../api/exceptionHandlers/ExceptionHandler.kt | 109 ++++ .../GlobalExceptionHandlerV2.kt | 49 ++ .../finance/configs/BearerTokenFilter.kt | 56 ++ .../luminic/finance/configs/CommonConfig.kt | 19 + .../luminic/finance/configs/SecurityConfig.kt | 60 ++ .../luminic/finance/configs/StorageConfig.kt | 18 + .../space/luminic/finance/dtos/AccountDTO.kt | 37 ++ .../space/luminic/finance/dtos/BudgetDTO.kt | 69 ++ .../space/luminic/finance/dtos/CategoryDTO.kt | 29 + .../space/luminic/finance/dtos/CurrencyDTO.kt | 8 + .../space/luminic/finance/dtos/GoalDTO.kt | 20 + .../space/luminic/finance/dtos/SpaceDTO.kt | 24 + .../luminic/finance/dtos/TransactionDTO.kt | 52 ++ .../space/luminic/finance/dtos/UserDTO.kt | 30 + .../luminic/finance/mappers/AccountMapper.kt | 26 + .../luminic/finance/mappers/BudgetMapper.kt | 88 +++ .../luminic/finance/mappers/CategoryMapper.kt | 22 + .../luminic/finance/mappers/CurrencyMapper.kt | 11 + .../luminic/finance/mappers/GoalMapper.kt | 21 + .../luminic/finance/mappers/SpaceMapper.kt | 18 + .../finance/mappers/TransactionMapper.kt | 35 + .../luminic/finance/mappers/UserMapper.kt | 16 + .../space/luminic/finance/models/Account.kt | 51 ++ .../space/luminic/finance/models/Budget.kt | 62 ++ .../space/luminic/finance/models/Category.kt | 49 ++ .../space/luminic/finance/models/Currency.kt | 13 + .../luminic/finance/models/CurrencyRate.kt | 18 + .../luminic/finance/models/Exceptions.kt | 5 + .../space/luminic/finance/models/Goal.kt | 40 ++ .../luminic/finance/models/PushMessage.kt | 20 + .../space/luminic/finance/models/Space.kt | 36 ++ .../luminic/finance/models/Subscription.kt | 23 + .../space/luminic/finance/models/Token.kt | 22 + .../luminic/finance/models/Transaction.kt | 59 ++ .../space/luminic/finance/models/User.kt | 26 + .../luminic/finance/repos/AccountRepo.kt | 7 + .../space/luminic/finance/repos/BudgetRepo.kt | 14 + .../luminic/finance/repos/CategoryRepo.kt | 9 + .../luminic/finance/repos/CurrencyRepo.kt | 8 + .../space/luminic/finance/repos/SpaceRepo.kt | 8 + .../luminic/finance/repos/SubscriptionRepo.kt | 21 + .../space/luminic/finance/repos/TokenRepo.kt | 15 + .../luminic/finance/repos/TransactionRepo.kt | 7 + .../space/luminic/finance/repos/UserRepo.kt | 19 + .../finance/services/AccountService.kt | 14 + .../finance/services/AccountServiceImpl.kt | 119 ++++ .../luminic/finance/services/AuthService.kt | 109 ++++ .../luminic/finance/services/BudgetService.kt | 18 + .../finance/services/BudgetServiceImpl.kt | 188 ++++++ .../finance/services/CategoryService.kt | 15 + .../finance/services/CategoryServiceImpl.kt | 109 ++++ .../finance/services/CoroutineAuditorAware.kt | 16 + .../finance/services/CurrencyService.kt | 16 + .../finance/services/CurrencyServiceImpl.kt | 51 ++ .../luminic/finance/services/SpaceService.kt | 16 + .../finance/services/SpaceServiceImpl.kt | 149 +++++ .../finance/services/SubscriptionService.kt | 113 ++++ .../luminic/finance/services/TokenService.kt | 43 ++ .../finance/services/TransactionService.kt | 20 + .../services/TransactionServiceImpl.kt | 185 ++++++ .../luminic/finance/services/UserService.kt | 52 ++ .../space/luminic/finance/utils/JWTUtil.kt | 32 + .../luminic/finance/utils/ScheduledTasks.kt | 35 + .../application-dev-local.properties | 20 + src/main/resources/application-dev.properties | 15 + .../resources/application-prod.properties | 21 + src/main/resources/application.properties | 29 + src/main/resources/json.json | 596 ++++++++++++++++++ 78 files changed, 3934 insertions(+) create mode 100644 .gitignore create mode 100644 build.gradle.kts create mode 100644 src/main/kotlin/space/luminic/finance/Main.kt create mode 100644 src/main/kotlin/space/luminic/finance/api/AccountController.kt create mode 100644 src/main/kotlin/space/luminic/finance/api/AuthController.kt create mode 100644 src/main/kotlin/space/luminic/finance/api/BudgetController.kt create mode 100644 src/main/kotlin/space/luminic/finance/api/CategoryController.kt create mode 100644 src/main/kotlin/space/luminic/finance/api/ReferenceController.kt create mode 100644 src/main/kotlin/space/luminic/finance/api/SpaceController.kt create mode 100644 src/main/kotlin/space/luminic/finance/api/TransactionController.kt create mode 100644 src/main/kotlin/space/luminic/finance/api/exceptionHandlers/ExceptionHandler.kt create mode 100644 src/main/kotlin/space/luminic/finance/api/exceptionHandlers/GlobalExceptionHandlerV2.kt create mode 100644 src/main/kotlin/space/luminic/finance/configs/BearerTokenFilter.kt create mode 100644 src/main/kotlin/space/luminic/finance/configs/CommonConfig.kt create mode 100644 src/main/kotlin/space/luminic/finance/configs/SecurityConfig.kt create mode 100644 src/main/kotlin/space/luminic/finance/configs/StorageConfig.kt create mode 100644 src/main/kotlin/space/luminic/finance/dtos/AccountDTO.kt create mode 100644 src/main/kotlin/space/luminic/finance/dtos/BudgetDTO.kt create mode 100644 src/main/kotlin/space/luminic/finance/dtos/CategoryDTO.kt create mode 100644 src/main/kotlin/space/luminic/finance/dtos/CurrencyDTO.kt create mode 100644 src/main/kotlin/space/luminic/finance/dtos/GoalDTO.kt create mode 100644 src/main/kotlin/space/luminic/finance/dtos/SpaceDTO.kt create mode 100644 src/main/kotlin/space/luminic/finance/dtos/TransactionDTO.kt create mode 100644 src/main/kotlin/space/luminic/finance/dtos/UserDTO.kt create mode 100644 src/main/kotlin/space/luminic/finance/mappers/AccountMapper.kt create mode 100644 src/main/kotlin/space/luminic/finance/mappers/BudgetMapper.kt create mode 100644 src/main/kotlin/space/luminic/finance/mappers/CategoryMapper.kt create mode 100644 src/main/kotlin/space/luminic/finance/mappers/CurrencyMapper.kt create mode 100644 src/main/kotlin/space/luminic/finance/mappers/GoalMapper.kt create mode 100644 src/main/kotlin/space/luminic/finance/mappers/SpaceMapper.kt create mode 100644 src/main/kotlin/space/luminic/finance/mappers/TransactionMapper.kt create mode 100644 src/main/kotlin/space/luminic/finance/mappers/UserMapper.kt create mode 100644 src/main/kotlin/space/luminic/finance/models/Account.kt create mode 100644 src/main/kotlin/space/luminic/finance/models/Budget.kt create mode 100644 src/main/kotlin/space/luminic/finance/models/Category.kt create mode 100644 src/main/kotlin/space/luminic/finance/models/Currency.kt create mode 100644 src/main/kotlin/space/luminic/finance/models/CurrencyRate.kt create mode 100644 src/main/kotlin/space/luminic/finance/models/Exceptions.kt create mode 100644 src/main/kotlin/space/luminic/finance/models/Goal.kt create mode 100644 src/main/kotlin/space/luminic/finance/models/PushMessage.kt create mode 100644 src/main/kotlin/space/luminic/finance/models/Space.kt create mode 100644 src/main/kotlin/space/luminic/finance/models/Subscription.kt create mode 100644 src/main/kotlin/space/luminic/finance/models/Token.kt create mode 100644 src/main/kotlin/space/luminic/finance/models/Transaction.kt create mode 100644 src/main/kotlin/space/luminic/finance/models/User.kt create mode 100644 src/main/kotlin/space/luminic/finance/repos/AccountRepo.kt create mode 100644 src/main/kotlin/space/luminic/finance/repos/BudgetRepo.kt create mode 100644 src/main/kotlin/space/luminic/finance/repos/CategoryRepo.kt create mode 100644 src/main/kotlin/space/luminic/finance/repos/CurrencyRepo.kt create mode 100644 src/main/kotlin/space/luminic/finance/repos/SpaceRepo.kt create mode 100644 src/main/kotlin/space/luminic/finance/repos/SubscriptionRepo.kt create mode 100644 src/main/kotlin/space/luminic/finance/repos/TokenRepo.kt create mode 100644 src/main/kotlin/space/luminic/finance/repos/TransactionRepo.kt create mode 100644 src/main/kotlin/space/luminic/finance/repos/UserRepo.kt create mode 100644 src/main/kotlin/space/luminic/finance/services/AccountService.kt create mode 100644 src/main/kotlin/space/luminic/finance/services/AccountServiceImpl.kt create mode 100644 src/main/kotlin/space/luminic/finance/services/AuthService.kt create mode 100644 src/main/kotlin/space/luminic/finance/services/BudgetService.kt create mode 100644 src/main/kotlin/space/luminic/finance/services/BudgetServiceImpl.kt create mode 100644 src/main/kotlin/space/luminic/finance/services/CategoryService.kt create mode 100644 src/main/kotlin/space/luminic/finance/services/CategoryServiceImpl.kt create mode 100644 src/main/kotlin/space/luminic/finance/services/CoroutineAuditorAware.kt create mode 100644 src/main/kotlin/space/luminic/finance/services/CurrencyService.kt create mode 100644 src/main/kotlin/space/luminic/finance/services/CurrencyServiceImpl.kt create mode 100644 src/main/kotlin/space/luminic/finance/services/SpaceService.kt create mode 100644 src/main/kotlin/space/luminic/finance/services/SpaceServiceImpl.kt create mode 100644 src/main/kotlin/space/luminic/finance/services/SubscriptionService.kt create mode 100644 src/main/kotlin/space/luminic/finance/services/TokenService.kt create mode 100644 src/main/kotlin/space/luminic/finance/services/TransactionService.kt create mode 100644 src/main/kotlin/space/luminic/finance/services/TransactionServiceImpl.kt create mode 100644 src/main/kotlin/space/luminic/finance/services/UserService.kt create mode 100644 src/main/kotlin/space/luminic/finance/utils/JWTUtil.kt create mode 100644 src/main/kotlin/space/luminic/finance/utils/ScheduledTasks.kt create mode 100644 src/main/resources/application-dev-local.properties create mode 100644 src/main/resources/application-dev.properties create mode 100644 src/main/resources/application-prod.properties create mode 100644 src/main/resources/application.properties create mode 100644 src/main/resources/json.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b1dff0d --- /dev/null +++ b/.gitignore @@ -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 \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..fdeb014 --- /dev/null +++ b/build.gradle.kts @@ -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 { + useJUnitPlatform() +} + + diff --git a/src/main/kotlin/space/luminic/finance/Main.kt b/src/main/kotlin/space/luminic/finance/Main.kt new file mode 100644 index 0000000..95e7a32 --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/Main.kt @@ -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) { + TimeZone.setDefault(TimeZone.getTimeZone("Europe/Moscow")) + runApplication
(*args) +} diff --git a/src/main/kotlin/space/luminic/finance/api/AccountController.kt b/src/main/kotlin/space/luminic/finance/api/AccountController.kt new file mode 100644 index 0000000..05306f5 --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/api/AccountController.kt @@ -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 { + 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 { + 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) + } + + +} \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/finance/api/AuthController.kt b/src/main/kotlin/space/luminic/finance/api/AuthController.kt new file mode 100644 index 0000000..0e37688 --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/api/AuthController.kt @@ -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 { + 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 { + 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() + } +} diff --git a/src/main/kotlin/space/luminic/finance/api/BudgetController.kt b/src/main/kotlin/space/luminic/finance/api/BudgetController.kt new file mode 100644 index 0000000..9942f44 --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/api/BudgetController.kt @@ -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 { + 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) + } +} \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/finance/api/CategoryController.kt b/src/main/kotlin/space/luminic/finance/api/CategoryController.kt new file mode 100644 index 0000000..f920c1c --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/api/CategoryController.kt @@ -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 { + 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) + } +} \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/finance/api/ReferenceController.kt b/src/main/kotlin/space/luminic/finance/api/ReferenceController.kt new file mode 100644 index 0000000..c1f5f8d --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/api/ReferenceController.kt @@ -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 { + 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) + } +} \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/finance/api/SpaceController.kt b/src/main/kotlin/space/luminic/finance/api/SpaceController.kt new file mode 100644 index 0000000..a4a8972 --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/api/SpaceController.kt @@ -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 { + 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) + } +} \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/finance/api/TransactionController.kt b/src/main/kotlin/space/luminic/finance/api/TransactionController.kt new file mode 100644 index 0000000..1270dae --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/api/TransactionController.kt @@ -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{ + 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) + } + + +} \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/finance/api/exceptionHandlers/ExceptionHandler.kt b/src/main/kotlin/space/luminic/finance/api/exceptionHandlers/ExceptionHandler.kt new file mode 100644 index 0000000..aed7415 --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/api/exceptionHandlers/ExceptionHandler.kt @@ -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 { + 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>?> { + 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>?> { + 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>?> { + 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>?> { + e.printStackTrace() + + + return Mono.just( + ResponseEntity( + constructErrorBody( + e, + e.message.toString(), + HttpStatus.INTERNAL_SERVER_ERROR, + exchange.request + ), HttpStatus.INTERNAL_SERVER_ERROR + ) + ) + } +} diff --git a/src/main/kotlin/space/luminic/finance/api/exceptionHandlers/GlobalExceptionHandlerV2.kt b/src/main/kotlin/space/luminic/finance/api/exceptionHandlers/GlobalExceptionHandlerV2.kt new file mode 100644 index 0000000..f930942 --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/api/exceptionHandlers/GlobalExceptionHandlerV2.kt @@ -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 { + return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse) + } + + private fun renderErrorResponse(request: ServerRequest): Mono { + val errorAttributesMap = getErrorAttributes( + request, + ErrorAttributeOptions.of( + ErrorAttributeOptions.Include.MESSAGE + ) + ) + return ServerResponse.status(401) + .contentType(MediaType.APPLICATION_JSON) + .body(BodyInserters.fromValue(errorAttributesMap)) + } + + +} diff --git a/src/main/kotlin/space/luminic/finance/configs/BearerTokenFilter.kt b/src/main/kotlin/space/luminic/finance/configs/BearerTokenFilter.kt new file mode 100644 index 0000000..448966e --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/configs/BearerTokenFilter.kt @@ -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 { + 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) \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/finance/configs/CommonConfig.kt b/src/main/kotlin/space/luminic/finance/configs/CommonConfig.kt new file mode 100644 index 0000000..b2ef99e --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/configs/CommonConfig.kt @@ -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, +) \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/finance/configs/SecurityConfig.kt b/src/main/kotlin/space/luminic/finance/configs/SecurityConfig.kt new file mode 100644 index 0000000..03d7f2f --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/configs/SecurityConfig.kt @@ -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 + } +} diff --git a/src/main/kotlin/space/luminic/finance/configs/StorageConfig.kt b/src/main/kotlin/space/luminic/finance/configs/StorageConfig.kt new file mode 100644 index 0000000..b96b6eb --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/configs/StorageConfig.kt @@ -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) // Создаем папку, если её нет + } +} \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/finance/dtos/AccountDTO.kt b/src/main/kotlin/space/luminic/finance/dtos/AccountDTO.kt new file mode 100644 index 0000000..178a642 --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/dtos/AccountDTO.kt @@ -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, + ) +} diff --git a/src/main/kotlin/space/luminic/finance/dtos/BudgetDTO.kt b/src/main/kotlin/space/luminic/finance/dtos/BudgetDTO.kt new file mode 100644 index 0000000..2b04669 --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/dtos/BudgetDTO.kt @@ -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, + val transactions: List, + val plannedIncomeTransactions: List, + val plannedExpenseTransactions: List, + val instantIncomeTransactions: List, + val instantExpenseTransactions: List + + ) +} + diff --git a/src/main/kotlin/space/luminic/finance/dtos/CategoryDTO.kt b/src/main/kotlin/space/luminic/finance/dtos/CategoryDTO.kt new file mode 100644 index 0000000..a5c864d --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/dtos/CategoryDTO.kt @@ -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, + ) +} \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/finance/dtos/CurrencyDTO.kt b/src/main/kotlin/space/luminic/finance/dtos/CurrencyDTO.kt new file mode 100644 index 0000000..fefafd9 --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/dtos/CurrencyDTO.kt @@ -0,0 +1,8 @@ +package space.luminic.finance.dtos + +data class CurrencyDTO( + val code: String, + val name: String, + val symbol: String +){ +} \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/finance/dtos/GoalDTO.kt b/src/main/kotlin/space/luminic/finance/dtos/GoalDTO.kt new file mode 100644 index 0000000..9b0f706 --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/dtos/GoalDTO.kt @@ -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, +) { +} \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/finance/dtos/SpaceDTO.kt b/src/main/kotlin/space/luminic/finance/dtos/SpaceDTO.kt new file mode 100644 index 0000000..e048a07 --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/dtos/SpaceDTO.kt @@ -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 = 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 + ) +} diff --git a/src/main/kotlin/space/luminic/finance/dtos/TransactionDTO.kt b/src/main/kotlin/space/luminic/finance/dtos/TransactionDTO.kt new file mode 100644 index 0000000..add02dc --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/dtos/TransactionDTO.kt @@ -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 + ) + +} diff --git a/src/main/kotlin/space/luminic/finance/dtos/UserDTO.kt b/src/main/kotlin/space/luminic/finance/dtos/UserDTO.kt new file mode 100644 index 0000000..fc4b45b --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/dtos/UserDTO.kt @@ -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 + +) { + + data class AuthUserDTO ( + var username: String, + var password: String, + ) + + data class RegisterUserDTO ( + var username: String, + var firstName: String, + var password: String, + + ) + +} + + + diff --git a/src/main/kotlin/space/luminic/finance/mappers/AccountMapper.kt b/src/main/kotlin/space/luminic/finance/mappers/AccountMapper.kt new file mode 100644 index 0000000..497ec15 --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/mappers/AccountMapper.kt @@ -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, + ) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/finance/mappers/BudgetMapper.kt b/src/main/kotlin/space/luminic/finance/mappers/BudgetMapper.kt new file mode 100644 index 0000000..368af34 --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/mappers/BudgetMapper.kt @@ -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.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()}, + ) + } + + + + + +} diff --git a/src/main/kotlin/space/luminic/finance/mappers/CategoryMapper.kt b/src/main/kotlin/space/luminic/finance/mappers/CategoryMapper.kt new file mode 100644 index 0000000..e4a4215 --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/mappers/CategoryMapper.kt @@ -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"), + ) + + +} \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/finance/mappers/CurrencyMapper.kt b/src/main/kotlin/space/luminic/finance/mappers/CurrencyMapper.kt new file mode 100644 index 0000000..dab202f --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/mappers/CurrencyMapper.kt @@ -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 + ) +} \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/finance/mappers/GoalMapper.kt b/src/main/kotlin/space/luminic/finance/mappers/GoalMapper.kt new file mode 100644 index 0000000..4af6363 --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/mappers/GoalMapper.kt @@ -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"), + ) + +} \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/finance/mappers/SpaceMapper.kt b/src/main/kotlin/space/luminic/finance/mappers/SpaceMapper.kt new file mode 100644 index 0000000..4945ca5 --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/mappers/SpaceMapper.kt @@ -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"), + ) +} \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/finance/mappers/TransactionMapper.kt b/src/main/kotlin/space/luminic/finance/mappers/TransactionMapper.kt new file mode 100644 index 0000000..2123377 --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/mappers/TransactionMapper.kt @@ -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, + ) + + + + + + + +} diff --git a/src/main/kotlin/space/luminic/finance/mappers/UserMapper.kt b/src/main/kotlin/space/luminic/finance/mappers/UserMapper.kt new file mode 100644 index 0000000..762dd83 --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/mappers/UserMapper.kt @@ -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 + ) +} \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/finance/models/Account.kt b/src/main/kotlin/space/luminic/finance/models/Account.kt new file mode 100644 index 0000000..2eae024 --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/models/Account.kt @@ -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? = null + @ReadOnlyProperty var createdBy: User? = null + @ReadOnlyProperty var updatedBy: User? = null + + + enum class AccountType(displayName: String) { + SALARY("Зарплатный"), + COLLECTING("Накопительный"), + LOANS("Долговой"), + } +} diff --git a/src/main/kotlin/space/luminic/finance/models/Budget.kt b/src/main/kotlin/space/luminic/finance/models/Budget.kt new file mode 100644 index 0000000..7e6147d --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/models/Budget.kt @@ -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 = listOf(), + val categories: List = 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("Специальный") + } +} + + + diff --git a/src/main/kotlin/space/luminic/finance/models/Category.kt b/src/main/kotlin/space/luminic/finance/models/Category.kt new file mode 100644 index 0000000..b84fa79 --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/models/Category.kt @@ -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 + ) + +} \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/finance/models/Currency.kt b/src/main/kotlin/space/luminic/finance/models/Currency.kt new file mode 100644 index 0000000..cd5e938 --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/models/Currency.kt @@ -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 +) + diff --git a/src/main/kotlin/space/luminic/finance/models/CurrencyRate.kt b/src/main/kotlin/space/luminic/finance/models/CurrencyRate.kt new file mode 100644 index 0000000..57fe427 --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/models/CurrencyRate.kt @@ -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 +} \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/finance/models/Exceptions.kt b/src/main/kotlin/space/luminic/finance/models/Exceptions.kt new file mode 100644 index 0000000..818d418 --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/models/Exceptions.kt @@ -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) \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/finance/models/Goal.kt b/src/main/kotlin/space/luminic/finance/models/Goal.kt new file mode 100644 index 0000000..0a852bf --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/models/Goal.kt @@ -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("Прочее", "💸") + } +} \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/finance/models/PushMessage.kt b/src/main/kotlin/space/luminic/finance/models/PushMessage.kt new file mode 100644 index 0000000..c9afe1f --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/models/PushMessage.kt @@ -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 + +) \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/finance/models/Space.kt b/src/main/kotlin/space/luminic/finance/models/Space.kt new file mode 100644 index 0000000..899bc31 --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/models/Space.kt @@ -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 = 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? = null + + @ReadOnlyProperty var createdBy: User? = null + + @ReadOnlyProperty var updatedBy: User? = null + + + +} \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/finance/models/Subscription.kt b/src/main/kotlin/space/luminic/finance/models/Subscription.kt new file mode 100644 index 0000000..c7c7229 --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/models/Subscription.kt @@ -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 +) diff --git a/src/main/kotlin/space/luminic/finance/models/Token.kt b/src/main/kotlin/space/luminic/finance/models/Token.kt new file mode 100644 index 0000000..ec1a5ab --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/models/Token.kt @@ -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 + } + +} \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/finance/models/Transaction.kt b/src/main/kotlin/space/luminic/finance/models/Transaction.kt new file mode 100644 index 0000000..5133ca8 --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/models/Transaction.kt @@ -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("Текущая") + } +} \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/finance/models/User.kt b/src/main/kotlin/space/luminic/finance/models/User.kt new file mode 100644 index 0000000..74f9c6b --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/models/User.kt @@ -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 = mutableListOf(), +) + + + diff --git a/src/main/kotlin/space/luminic/finance/repos/AccountRepo.kt b/src/main/kotlin/space/luminic/finance/repos/AccountRepo.kt new file mode 100644 index 0000000..e9948b6 --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/repos/AccountRepo.kt @@ -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 { +} \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/finance/repos/BudgetRepo.kt b/src/main/kotlin/space/luminic/finance/repos/BudgetRepo.kt new file mode 100644 index 0000000..3718a8e --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/repos/BudgetRepo.kt @@ -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 { + + suspend fun findBudgetsBySpaceIdAndIsDeletedFalse(spaceId: String): Flux + suspend fun findBudgetsBySpaceIdAndId(spaceId: String, budgetId: String): Flux +} \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/finance/repos/CategoryRepo.kt b/src/main/kotlin/space/luminic/finance/repos/CategoryRepo.kt new file mode 100644 index 0000000..1e054f9 --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/repos/CategoryRepo.kt @@ -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 { +} + +interface CategoryEtalonRepo: ReactiveMongoRepository \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/finance/repos/CurrencyRepo.kt b/src/main/kotlin/space/luminic/finance/repos/CurrencyRepo.kt new file mode 100644 index 0000000..912d212 --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/repos/CurrencyRepo.kt @@ -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 +interface CurrencyRateRepo: ReactiveMongoRepository \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/finance/repos/SpaceRepo.kt b/src/main/kotlin/space/luminic/finance/repos/SpaceRepo.kt new file mode 100644 index 0000000..ee6ef6e --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/repos/SpaceRepo.kt @@ -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 { +} \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/finance/repos/SubscriptionRepo.kt b/src/main/kotlin/space/luminic/finance/repos/SubscriptionRepo.kt new file mode 100644 index 0000000..73c0262 --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/repos/SubscriptionRepo.kt @@ -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 { + + @Query("{ \$and: [ " + + "{ 'user': { '\$ref': 'users', '\$id': ?0 } }, " + + "{ 'isActive': true } " + + "]}") + fun findByUserIdAndIsActive(userId: ObjectId): Flux + + +} \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/finance/repos/TokenRepo.kt b/src/main/kotlin/space/luminic/finance/repos/TokenRepo.kt new file mode 100644 index 0000000..127ce93 --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/repos/TokenRepo.kt @@ -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 { + + fun findByToken(token: String): Mono + + fun deleteByExpiresAtBefore(dateTime: LocalDateTime) +} diff --git a/src/main/kotlin/space/luminic/finance/repos/TransactionRepo.kt b/src/main/kotlin/space/luminic/finance/repos/TransactionRepo.kt new file mode 100644 index 0000000..cf82f06 --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/repos/TransactionRepo.kt @@ -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 { +} \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/finance/repos/UserRepo.kt b/src/main/kotlin/space/luminic/finance/repos/UserRepo.kt new file mode 100644 index 0000000..cd3f7f8 --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/repos/UserRepo.kt @@ -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 { + + + @Query(value = "{ 'username': ?0 }", fields = "{ 'password': 0 }") + fun findByUsernameWOPassword(username: String): Mono + + fun findByUsername(username: String): Mono + + fun findByTgId(id: String): Mono +} \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/finance/services/AccountService.kt b/src/main/kotlin/space/luminic/finance/services/AccountService.kt new file mode 100644 index 0000000..df31b74 --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/services/AccountService.kt @@ -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 + suspend fun getAccount(spaceId: String, accountId: String): Account + suspend fun getAccountTransactions(spaceId: String, accountId: String): List + 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) +} \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/finance/services/AccountServiceImpl.kt b/src/main/kotlin/space/luminic/finance/services/AccountServiceImpl.kt new file mode 100644 index 0000000..1a39d30 --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/services/AccountServiceImpl.kt @@ -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 { + 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() + 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 { + 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 { + 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() + + } +} \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/finance/services/AuthService.kt b/src/main/kotlin/space/luminic/finance/services/AuthService.kt new file mode 100644 index 0000000..174d012 --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/services/AuthService.kt @@ -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("Токен истек или не валиден") + } + } + } + +} \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/finance/services/BudgetService.kt b/src/main/kotlin/space/luminic/finance/services/BudgetService.kt new file mode 100644 index 0000000..8e0d2ea --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/services/BudgetService.kt @@ -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 + suspend fun getBudget(spaceId: String, budgetId: String): Budget + suspend fun getBudgetTransactions(spaceId: String, budgetId: String): List + 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) + + + +} \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/finance/services/BudgetServiceImpl.kt b/src/main/kotlin/space/luminic/finance/services/BudgetServiceImpl.kt new file mode 100644 index 0000000..eeda2e7 --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/services/BudgetServiceImpl.kt @@ -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 { + + 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() + 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 { + + 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 { + 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() + } +} \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/finance/services/CategoryService.kt b/src/main/kotlin/space/luminic/finance/services/CategoryService.kt new file mode 100644 index 0000000..23e3940 --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/services/CategoryService.kt @@ -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 + 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 +} \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/finance/services/CategoryServiceImpl.kt b/src/main/kotlin/space/luminic/finance/services/CategoryServiceImpl.kt new file mode 100644 index 0000000..865a935 --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/services/CategoryServiceImpl.kt @@ -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 { + 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() + 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 { + 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 { + 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() + } + + +} \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/finance/services/CoroutineAuditorAware.kt b/src/main/kotlin/space/luminic/finance/services/CoroutineAuditorAware.kt new file mode 100644 index 0000000..e912647 --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/services/CoroutineAuditorAware.kt @@ -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 { + override fun getCurrentAuditor(): Mono = + mono { + authService.getSecurityUser().id!! + } +} \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/finance/services/CurrencyService.kt b/src/main/kotlin/space/luminic/finance/services/CurrencyService.kt new file mode 100644 index 0000000..13949ec --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/services/CurrencyService.kt @@ -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 + 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 + +} \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/finance/services/CurrencyServiceImpl.kt b/src/main/kotlin/space/luminic/finance/services/CurrencyServiceImpl.kt new file mode 100644 index 0000000..d7c92e7 --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/services/CurrencyServiceImpl.kt @@ -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 { + 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() + } +} \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/finance/services/SpaceService.kt b/src/main/kotlin/space/luminic/finance/services/SpaceService.kt new file mode 100644 index 0000000..508a835 --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/services/SpaceService.kt @@ -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 + 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) + +} \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/finance/services/SpaceServiceImpl.kt b/src/main/kotlin/space/luminic/finance/services/SpaceServiceImpl.kt new file mode 100644 index 0000000..4647851 --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/services/SpaceServiceImpl.kt @@ -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 { + 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() + 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{ + 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 { + 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() + } +} \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/finance/services/SubscriptionService.kt b/src/main/kotlin/space/luminic/finance/services/SubscriptionService.kt new file mode 100644 index 0000000..addb344 --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/services/SubscriptionService.kt @@ -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") + } + } +} diff --git a/src/main/kotlin/space/luminic/finance/services/TokenService.kt b/src/main/kotlin/space/luminic/finance/services/TokenService.kt new file mode 100644 index 0000000..82b0a03 --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/services/TokenService.kt @@ -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 { + 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()) + } +} \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/finance/services/TransactionService.kt b/src/main/kotlin/space/luminic/finance/services/TransactionService.kt new file mode 100644 index 0000000..646cac2 --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/services/TransactionService.kt @@ -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 + 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) +} \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/finance/services/TransactionServiceImpl.kt b/src/main/kotlin/space/luminic/finance/services/TransactionServiceImpl.kt new file mode 100644 index 0000000..a7d8a32 --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/services/TransactionServiceImpl.kt @@ -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 { + 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() + 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 { + 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() + 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() + 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() + } + + +} \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/finance/services/UserService.kt b/src/main/kotlin/space/luminic/finance/services/UserService.kt new file mode 100644 index 0000000..6d95a50 --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/services/UserService.kt @@ -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 { + return userRepo.findAll() + .collectList() + .awaitSingle() + } + +} \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/finance/utils/JWTUtil.kt b/src/main/kotlin/space/luminic/finance/utils/JWTUtil.kt new file mode 100644 index 0000000..45c7a1d --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/utils/JWTUtil.kt @@ -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 + + } + + +} diff --git a/src/main/kotlin/space/luminic/finance/utils/ScheduledTasks.kt b/src/main/kotlin/space/luminic/finance/utils/ScheduledTasks.kt new file mode 100644 index 0000000..a7e8b01 --- /dev/null +++ b/src/main/kotlin/space/luminic/finance/utils/ScheduledTasks.kt @@ -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() + } + + +} \ No newline at end of file diff --git a/src/main/resources/application-dev-local.properties b/src/main/resources/application-dev-local.properties new file mode 100644 index 0000000..2b67d00 --- /dev/null +++ b/src/main/resources/application-dev-local.properties @@ -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 \ No newline at end of file diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties new file mode 100644 index 0000000..74fe26d --- /dev/null +++ b/src/main/resources/application-dev.properties @@ -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 \ No newline at end of file diff --git a/src/main/resources/application-prod.properties b/src/main/resources/application-prod.properties new file mode 100644 index 0000000..7cc1984 --- /dev/null +++ b/src/main/resources/application-prod.properties @@ -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 diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..92caee5 --- /dev/null +++ b/src/main/resources/application.properties @@ -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 + + diff --git a/src/main/resources/json.json b/src/main/resources/json.json new file mode 100644 index 0000000..e43ee60 --- /dev/null +++ b/src/main/resources/json.json @@ -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": [] +}]