Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9575eb82a2 | |||
| 6e8f5d112b | |||
| 2e054088ce | |||
| 1bcbf5e53a | |||
| 5d9cc167fc | |||
| af7577c65b | |||
| a38d5068e0 | |||
| 711348b386 | |||
| 4d5b89b08c | |||
| ed8965b055 | |||
| 94df5d72c3 | |||
| 98263732ca |
@@ -56,6 +56,9 @@ dependencies {
|
|||||||
implementation("com.google.code.gson:gson")
|
implementation("com.google.code.gson:gson")
|
||||||
implementation("io.micrometer:micrometer-registry-prometheus")
|
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")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,17 +2,23 @@ package space.luminic.budgerapp
|
|||||||
|
|
||||||
|
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationPropertiesScan
|
||||||
|
import org.springframework.boot.context.properties.EnableConfigurationProperties
|
||||||
import org.springframework.boot.runApplication
|
import org.springframework.boot.runApplication
|
||||||
import org.springframework.cache.annotation.EnableCaching
|
import org.springframework.cache.annotation.EnableCaching
|
||||||
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories
|
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories
|
||||||
import org.springframework.scheduling.annotation.EnableAsync
|
import org.springframework.scheduling.annotation.EnableAsync
|
||||||
import org.springframework.scheduling.annotation.EnableScheduling
|
import org.springframework.scheduling.annotation.EnableScheduling
|
||||||
import java.util.TimeZone
|
import space.luminic.budgerapp.configs.NLPConfig
|
||||||
|
import space.luminic.budgerapp.configs.TelegramBotProperties
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
@SpringBootApplication(scanBasePackages = ["space.luminic.budgerapp"])
|
@SpringBootApplication(scanBasePackages = ["space.luminic.budgerapp"])
|
||||||
@EnableCaching
|
@EnableCaching
|
||||||
@EnableAsync
|
@EnableAsync
|
||||||
@EnableScheduling
|
@EnableScheduling
|
||||||
|
//@EnableConfigurationProperties([TelegramBotProperties::class,)
|
||||||
|
@ConfigurationPropertiesScan(basePackages = ["space.luminic.budgerapp"])
|
||||||
@EnableMongoRepositories(basePackages = ["space.luminic.budgerapp.repos"])
|
@EnableMongoRepositories(basePackages = ["space.luminic.budgerapp.repos"])
|
||||||
class BudgerAppApplication
|
class BudgerAppApplication
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,19 @@
|
|||||||
package space.luminic.budgerapp.configs
|
package space.luminic.budgerapp.configs
|
||||||
|
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||||
import org.springframework.context.annotation.Bean
|
import org.springframework.context.annotation.Bean
|
||||||
import org.springframework.context.annotation.Configuration
|
import org.springframework.context.annotation.Configuration
|
||||||
|
|
||||||
//@Configuration
|
|
||||||
//class CommonConfig {
|
//class CommonConfig {
|
||||||
// @Bean
|
// @Bean
|
||||||
// fun httpTraceRepository(): HttpTraceRepository {
|
// fun httpTraceRepository(): HttpTraceRepository {
|
||||||
// return InMemoryHttpTraceRepository()
|
// return InMemoryHttpTraceRepository()
|
||||||
// }
|
// }
|
||||||
//}
|
//}
|
||||||
|
|
||||||
|
|
||||||
|
@ConfigurationProperties(prefix = "nlp")
|
||||||
|
data class NLPConfig(
|
||||||
|
val address: String,
|
||||||
|
)
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package space.luminic.budgerapp.configs
|
||||||
|
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||||
|
import org.springframework.context.annotation.Bean
|
||||||
|
import org.springframework.context.annotation.Configuration
|
||||||
|
import org.telegram.telegrambots.meta.TelegramBotsApi
|
||||||
|
import org.telegram.telegrambots.updatesreceivers.DefaultBotSession
|
||||||
|
import space.luminic.budgerapp.services.BotService
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
class TelegramBotConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
fun telegramBotsApi(myBot: BotService): TelegramBotsApi {
|
||||||
|
val botsApi = TelegramBotsApi(DefaultBotSession::class.java)
|
||||||
|
botsApi.registerBot(myBot)
|
||||||
|
return botsApi
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ConfigurationProperties(prefix = "telegram.bot")
|
||||||
|
data class TelegramBotProperties(
|
||||||
|
val username: String,
|
||||||
|
val token: String,
|
||||||
|
)
|
||||||
@@ -30,18 +30,19 @@ class AuthController(
|
|||||||
|
|
||||||
@PostMapping("/login")
|
@PostMapping("/login")
|
||||||
suspend fun login(@RequestBody request: AuthRequest): Map<String, String> {
|
suspend fun login(@RequestBody request: AuthRequest): Map<String, String> {
|
||||||
return authService.login(request.username, request.password)
|
val token = authService.login(request.username, request.password)
|
||||||
.map { token -> mapOf("token" to token) }.awaitFirst()
|
return mapOf("token" to token)
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/register")
|
@PostMapping("/register")
|
||||||
fun register(@RequestBody request: RegisterRequest): Mono<User> {
|
suspend fun register(@RequestBody request: RegisterRequest): User {
|
||||||
return authService.register(request.username, request.password, request.firstName)
|
return authService.register(request.username, request.password, request.firstName)
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/tgLogin")
|
@PostMapping("/tgLogin")
|
||||||
fun tgLogin(@RequestHeader("X-Tg-Id") tgId: String): Mono<Map<String, String>> {
|
suspend fun tgLogin(@RequestHeader("X-Tg-Id") tgId: String): Map<String, String> {
|
||||||
return authService.tgLogin(tgId).map { token -> mapOf("token" to token) }
|
val token = authService.tgLogin(tgId)
|
||||||
|
return mapOf("token" to token)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,35 @@
|
|||||||
package space.luminic.budgerapp.controllers
|
package space.luminic.budgerapp.controllers
|
||||||
|
|
||||||
import org.springframework.web.bind.annotation.DeleteMapping
|
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.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.RequestMapping
|
||||||
import org.springframework.web.bind.annotation.RestController
|
import org.springframework.web.bind.annotation.RestController
|
||||||
import reactor.core.publisher.Mono
|
import space.luminic.budgerapp.configs.AuthException
|
||||||
import space.luminic.budgerapp.models.Recurrent
|
import space.luminic.budgerapp.services.AuthService
|
||||||
import space.luminic.budgerapp.services.RecurrentService
|
import space.luminic.budgerapp.services.RecurrentService
|
||||||
|
import space.luminic.budgerapp.services.SpaceService
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/recurrents")
|
@RequestMapping("/spaces/{spaceId}")
|
||||||
class RecurrentController(
|
class RecurrentController(
|
||||||
private val recurrentService: RecurrentService
|
private val recurrentService: RecurrentService,
|
||||||
|
private val authService: AuthService,
|
||||||
|
private val spaceService: SpaceService
|
||||||
){
|
){
|
||||||
|
|
||||||
|
// @DeleteMapping("/recurrents/{recurrentId}")
|
||||||
|
// suspend fun delete(
|
||||||
|
// @PathVariable spaceId: String,
|
||||||
|
// @PathVariable recurrentId: String): String {
|
||||||
|
// val user = authService.getSecurityUser()
|
||||||
|
// val space = spaceService.isValidRequest(spaceId, user)
|
||||||
|
// if (space.owner?.id == user.id) {
|
||||||
|
// recurrentService.deleteRecurrent(recurrentId)
|
||||||
|
// return "Cool"
|
||||||
|
// } else {
|
||||||
|
// throw AuthException("Only owners allowed")
|
||||||
|
// }
|
||||||
|
// }
|
||||||
//
|
//
|
||||||
// @GetMapping("/")
|
// @GetMapping("/")
|
||||||
// fun getRecurrents(): Mono<List<Recurrent>> {
|
// fun getRecurrents(): Mono<List<Recurrent>> {
|
||||||
|
|||||||
@@ -1,29 +1,38 @@
|
|||||||
package space.luminic.budgerapp.controllers
|
package space.luminic.budgerapp.controllers
|
||||||
|
|
||||||
|
import com.opencsv.CSVWriter
|
||||||
import kotlinx.coroutines.reactor.awaitSingle
|
import kotlinx.coroutines.reactor.awaitSingle
|
||||||
import kotlinx.coroutines.reactor.awaitSingleOrNull
|
import kotlinx.coroutines.reactor.awaitSingleOrNull
|
||||||
|
import org.apache.commons.io.IOUtils.writer
|
||||||
import org.bson.Document
|
import org.bson.Document
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.http.HttpHeaders
|
||||||
import org.springframework.http.HttpStatus
|
import org.springframework.http.HttpStatus
|
||||||
|
import org.springframework.http.MediaType
|
||||||
import org.springframework.http.ResponseEntity
|
import org.springframework.http.ResponseEntity
|
||||||
import org.springframework.web.bind.annotation.*
|
import org.springframework.web.bind.annotation.*
|
||||||
|
import space.luminic.budgerapp.configs.AuthException
|
||||||
import space.luminic.budgerapp.controllers.BudgetController.LimitValue
|
import space.luminic.budgerapp.controllers.BudgetController.LimitValue
|
||||||
import space.luminic.budgerapp.controllers.dtos.BudgetCreationDTO
|
import space.luminic.budgerapp.controllers.dtos.BudgetCreationDTO
|
||||||
import space.luminic.budgerapp.models.*
|
import space.luminic.budgerapp.models.*
|
||||||
import space.luminic.budgerapp.services.CategoryService
|
import space.luminic.budgerapp.services.*
|
||||||
import space.luminic.budgerapp.services.FinancialService
|
import java.io.ByteArrayOutputStream
|
||||||
import space.luminic.budgerapp.services.RecurrentService
|
import java.io.OutputStreamWriter
|
||||||
import space.luminic.budgerapp.services.SpaceService
|
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
|
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/spaces")
|
@RequestMapping("/spaces")
|
||||||
class SpaceController(
|
class SpaceController(
|
||||||
private val spaceService: SpaceService,
|
private val spaceService: SpaceService,
|
||||||
private val financialService: FinancialService,
|
private val financialService: FinancialService,
|
||||||
private val categoryService: CategoryService,
|
private val categoryService: CategoryService,
|
||||||
private val recurrentService: RecurrentService
|
private val recurrentService: RecurrentService,
|
||||||
|
private val authService: AuthService
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
private val log = LoggerFactory.getLogger(SpaceController::class.java)
|
||||||
|
|
||||||
|
|
||||||
data class SpaceCreateDTO(
|
data class SpaceCreateDTO(
|
||||||
val name: String,
|
val name: String,
|
||||||
@@ -54,13 +63,16 @@ class SpaceController(
|
|||||||
|
|
||||||
@DeleteMapping("/{spaceId}")
|
@DeleteMapping("/{spaceId}")
|
||||||
suspend fun deleteSpace(@PathVariable spaceId: String) {
|
suspend fun deleteSpace(@PathVariable spaceId: String) {
|
||||||
return spaceService.deleteSpace(spaceService.isValidRequest(spaceId))
|
val user = authService.getSecurityUser()
|
||||||
|
val space = spaceService.isValidRequest(spaceId, user)
|
||||||
|
return spaceService.deleteSpace(space)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@PostMapping("/{spaceId}/invite")
|
@PostMapping("/{spaceId}/invite")
|
||||||
suspend fun inviteSpace(@PathVariable spaceId: String): SpaceInvite {
|
suspend fun inviteSpace(@PathVariable spaceId: String): SpaceInvite {
|
||||||
spaceService.isValidRequest(spaceId)
|
val user = authService.getSecurityUser()
|
||||||
|
spaceService.isValidRequest(spaceId, user)
|
||||||
return spaceService.createInviteSpace(spaceId)
|
return spaceService.createInviteSpace(spaceId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,13 +83,15 @@ class SpaceController(
|
|||||||
|
|
||||||
@DeleteMapping("/{spaceId}/leave")
|
@DeleteMapping("/{spaceId}/leave")
|
||||||
suspend fun leaveSpace(@PathVariable spaceId: String) {
|
suspend fun leaveSpace(@PathVariable spaceId: String) {
|
||||||
spaceService.isValidRequest(spaceId)
|
val user = authService.getSecurityUser()
|
||||||
|
spaceService.isValidRequest(spaceId, user)
|
||||||
return spaceService.leaveSpace(spaceId)
|
return spaceService.leaveSpace(spaceId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping("/{spaceId}/members/kick/{username}")
|
@DeleteMapping("/{spaceId}/members/kick/{username}")
|
||||||
suspend fun kickMembers(@PathVariable spaceId: String, @PathVariable username: String) {
|
suspend fun kickMembers(@PathVariable spaceId: String, @PathVariable username: String) {
|
||||||
spaceService.isValidRequest(spaceId)
|
val user = authService.getSecurityUser()
|
||||||
|
spaceService.isValidRequest(spaceId, user)
|
||||||
return spaceService.kickMember(spaceId, username)
|
return spaceService.kickMember(spaceId, username)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,14 +101,17 @@ class SpaceController(
|
|||||||
//
|
//
|
||||||
@GetMapping("/{spaceId}/budgets")
|
@GetMapping("/{spaceId}/budgets")
|
||||||
suspend fun getBudgets(@PathVariable spaceId: String): List<Budget> {
|
suspend fun getBudgets(@PathVariable spaceId: String): List<Budget> {
|
||||||
spaceService.isValidRequest(spaceId)
|
val user = authService.getSecurityUser()
|
||||||
|
spaceService.isValidRequest(spaceId, user)
|
||||||
return financialService.getBudgets(spaceId).awaitSingleOrNull().orEmpty()
|
return financialService.getBudgets(spaceId).awaitSingleOrNull().orEmpty()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{spaceId}/budgets/{id}")
|
@GetMapping("/{spaceId}/budgets/{id}")
|
||||||
suspend fun getBudget(@PathVariable spaceId: String, @PathVariable id: String): BudgetDTO? {
|
suspend fun getBudget(@PathVariable spaceId: String, @PathVariable id: String): BudgetDTO? {
|
||||||
spaceService.isValidRequest(spaceId)
|
log.info("Getting budget for spaceId=$spaceId, id=$id")
|
||||||
|
val user = authService.getSecurityUser()
|
||||||
|
spaceService.isValidRequest(spaceId, user)
|
||||||
return financialService.getBudget(spaceId, id)
|
return financialService.getBudget(spaceId, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,8 +120,10 @@ class SpaceController(
|
|||||||
@PathVariable spaceId: String,
|
@PathVariable spaceId: String,
|
||||||
@RequestBody budgetCreationDTO: BudgetCreationDTO,
|
@RequestBody budgetCreationDTO: BudgetCreationDTO,
|
||||||
): Budget? {
|
): Budget? {
|
||||||
|
val user = authService.getSecurityUser()
|
||||||
|
val space = spaceService.isValidRequest(spaceId, user)
|
||||||
return financialService.createBudget(
|
return financialService.createBudget(
|
||||||
spaceService.isValidRequest(spaceId),
|
space,
|
||||||
budgetCreationDTO.budget,
|
budgetCreationDTO.budget,
|
||||||
budgetCreationDTO.createRecurrent
|
budgetCreationDTO.createRecurrent
|
||||||
)
|
)
|
||||||
@@ -112,7 +131,8 @@ class SpaceController(
|
|||||||
|
|
||||||
@DeleteMapping("/{spaceId}/budgets/{id}")
|
@DeleteMapping("/{spaceId}/budgets/{id}")
|
||||||
suspend fun deleteBudget(@PathVariable spaceId: String, @PathVariable id: String) {
|
suspend fun deleteBudget(@PathVariable spaceId: String, @PathVariable id: String) {
|
||||||
spaceService.isValidRequest(spaceId)
|
val user = authService.getSecurityUser()
|
||||||
|
spaceService.isValidRequest(spaceId, user)
|
||||||
financialService.deleteBudget(spaceId, id)
|
financialService.deleteBudget(spaceId, id)
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -124,7 +144,8 @@ class SpaceController(
|
|||||||
@PathVariable catId: String,
|
@PathVariable catId: String,
|
||||||
@RequestBody limit: LimitValue,
|
@RequestBody limit: LimitValue,
|
||||||
): BudgetCategory {
|
): BudgetCategory {
|
||||||
spaceService.isValidRequest(spaceId)
|
val user = authService.getSecurityUser()
|
||||||
|
spaceService.isValidRequest(spaceId, user)
|
||||||
return financialService.setCategoryLimit(spaceId, budgetId, catId, limit.limit)
|
return financialService.setCategoryLimit(spaceId, budgetId, catId, limit.limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,6 +180,51 @@ class SpaceController(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/transactions/csv")
|
||||||
|
suspend fun getTransactionsCSV(
|
||||||
|
): ResponseEntity<Any> {
|
||||||
|
try {
|
||||||
|
val bos = ByteArrayOutputStream()
|
||||||
|
val writer = CSVWriter(OutputStreamWriter(bos))
|
||||||
|
val CSVHeaders = arrayOf("id", "comment", "category")
|
||||||
|
writer.writeNext(CSVHeaders)
|
||||||
|
financialService.getAllTransactions(
|
||||||
|
).map {
|
||||||
|
val data = arrayOf(it.id, it.comment, it.category.id)
|
||||||
|
writer.writeNext(data)
|
||||||
|
}
|
||||||
|
writer.close()
|
||||||
|
|
||||||
|
val csvData = bos.toByteArray()
|
||||||
|
val headers = HttpHeaders()
|
||||||
|
headers.contentType = MediaType.parseMediaType("text/csv")
|
||||||
|
headers.setContentDispositionFormData("attachment", "pojos.csv")
|
||||||
|
|
||||||
|
return ResponseEntity(csvData, headers, HttpStatus.OK)
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
return ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{spaceId}/category-predict")
|
||||||
|
suspend fun getTransactionCategoryPredict(
|
||||||
|
@PathVariable spaceId: String,
|
||||||
|
@RequestParam comment: String,
|
||||||
|
@RequestParam cloud: Int
|
||||||
|
): List<Category> {
|
||||||
|
val user = authService.getSecurityUser()
|
||||||
|
spaceService.isValidRequest(spaceId, user)
|
||||||
|
return categoryService.getCategories(
|
||||||
|
"67af3c0f652da946a7dd9931",
|
||||||
|
"EXPENSE",
|
||||||
|
sortBy = "name",
|
||||||
|
direction = "ASC",
|
||||||
|
predict = comment,
|
||||||
|
cloud = cloud
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/{spaceId}/transactions/{id}")
|
@GetMapping("/{spaceId}/transactions/{id}")
|
||||||
suspend fun getTransaction(
|
suspend fun getTransaction(
|
||||||
@@ -168,9 +234,11 @@ class SpaceController(
|
|||||||
return financialService.getTransactionById(id)
|
return financialService.getTransactionById(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@PostMapping("/{spaceId}/transactions")
|
@PostMapping("/{spaceId}/transactions")
|
||||||
suspend fun createTransaction(@PathVariable spaceId: String, @RequestBody transaction: Transaction): Transaction {
|
suspend fun createTransaction(@PathVariable spaceId: String, @RequestBody transaction: Transaction): Transaction {
|
||||||
val space = spaceService.isValidRequest(spaceId)
|
val user = authService.getSecurityUser()
|
||||||
|
val space = spaceService.isValidRequest(spaceId, user)
|
||||||
return financialService.createTransaction(space, transaction)
|
return financialService.createTransaction(space, transaction)
|
||||||
|
|
||||||
|
|
||||||
@@ -180,14 +248,16 @@ class SpaceController(
|
|||||||
suspend fun editTransaction(
|
suspend fun editTransaction(
|
||||||
@PathVariable spaceId: String, @PathVariable id: String, @RequestBody transaction: Transaction
|
@PathVariable spaceId: String, @PathVariable id: String, @RequestBody transaction: Transaction
|
||||||
): Transaction {
|
): Transaction {
|
||||||
val space = spaceService.isValidRequest(spaceId)
|
val user = authService.getSecurityUser()
|
||||||
|
val space = spaceService.isValidRequest(spaceId, user)
|
||||||
transaction.space = space
|
transaction.space = space
|
||||||
return financialService.editTransaction(transaction)
|
return financialService.editTransaction(transaction, user)
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping("/{spaceId}/transactions/{id}")
|
@DeleteMapping("/{spaceId}/transactions/{id}")
|
||||||
suspend fun deleteTransaction(@PathVariable spaceId: String, @PathVariable id: String) {
|
suspend fun deleteTransaction(@PathVariable spaceId: String, @PathVariable id: String) {
|
||||||
spaceService.isValidRequest(spaceId)
|
val user = authService.getSecurityUser()
|
||||||
|
spaceService.isValidRequest(spaceId, user)
|
||||||
val transaction = financialService.getTransactionById(id)
|
val transaction = financialService.getTransactionById(id)
|
||||||
financialService.deleteTransaction(transaction)
|
financialService.deleteTransaction(transaction)
|
||||||
}
|
}
|
||||||
@@ -202,8 +272,9 @@ class SpaceController(
|
|||||||
@RequestParam("sort") sortBy: String = "name",
|
@RequestParam("sort") sortBy: String = "name",
|
||||||
@RequestParam("direction") direction: String = "ASC"
|
@RequestParam("direction") direction: String = "ASC"
|
||||||
): List<Category> {
|
): List<Category> {
|
||||||
spaceService.isValidRequest(spaceId)
|
val user = authService.getSecurityUser()
|
||||||
return categoryService.getCategories(spaceId, type, sortBy, direction).awaitSingleOrNull().orEmpty()
|
spaceService.isValidRequest(spaceId, user)
|
||||||
|
return categoryService.getCategories(spaceId, type, sortBy, direction)
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{spaceId}/categories/types")
|
@GetMapping("/{spaceId}/categories/types")
|
||||||
@@ -215,7 +286,8 @@ class SpaceController(
|
|||||||
suspend fun createCategory(
|
suspend fun createCategory(
|
||||||
@PathVariable spaceId: String, @RequestBody category: Category
|
@PathVariable spaceId: String, @RequestBody category: Category
|
||||||
): Category {
|
): Category {
|
||||||
val space = spaceService.isValidRequest(spaceId)
|
val user = authService.getSecurityUser()
|
||||||
|
val space = spaceService.isValidRequest(spaceId, user)
|
||||||
return financialService.createCategory(space, category).awaitSingle()
|
return financialService.createCategory(space, category).awaitSingle()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,33 +297,38 @@ class SpaceController(
|
|||||||
@RequestBody category: Category,
|
@RequestBody category: Category,
|
||||||
@PathVariable spaceId: String
|
@PathVariable spaceId: String
|
||||||
): Category {
|
): Category {
|
||||||
val space = spaceService.isValidRequest(spaceId)
|
val user = authService.getSecurityUser()
|
||||||
|
val space = spaceService.isValidRequest(spaceId, user)
|
||||||
return categoryService.editCategory(space, category)
|
return categoryService.editCategory(space, category)
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping("/{spaceId}/categories/{categoryId}")
|
@DeleteMapping("/{spaceId}/categories/{categoryId}")
|
||||||
suspend fun deleteCategory(@PathVariable categoryId: String, @PathVariable spaceId: String) {
|
suspend fun deleteCategory(@PathVariable categoryId: String, @PathVariable spaceId: String) {
|
||||||
val space = spaceService.isValidRequest(spaceId)
|
val user = authService.getSecurityUser()
|
||||||
categoryService.deleteCategory(space, categoryId)
|
val space = spaceService.isValidRequest(spaceId, user)
|
||||||
|
categoryService.deleteCategory(space, categoryId, user)
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{spaceId}/categories/tags")
|
@GetMapping("/{spaceId}/categories/tags")
|
||||||
suspend fun getTags(@PathVariable spaceId: String): List<Tag> {
|
suspend fun getTags(@PathVariable spaceId: String): List<Tag> {
|
||||||
val space = spaceService.isValidRequest(spaceId)
|
val user = authService.getSecurityUser()
|
||||||
|
val space = spaceService.isValidRequest(spaceId, user)
|
||||||
return spaceService.getTags(space)
|
return spaceService.getTags(space)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/{spaceId}/categories/tags")
|
@PostMapping("/{spaceId}/categories/tags")
|
||||||
suspend fun createTags(@PathVariable spaceId: String, @RequestBody tag: Tag): Tag {
|
suspend fun createTags(@PathVariable spaceId: String, @RequestBody tag: Tag): Tag {
|
||||||
val space = spaceService.isValidRequest(spaceId)
|
val user = authService.getSecurityUser()
|
||||||
|
val space = spaceService.isValidRequest(spaceId, user)
|
||||||
return spaceService.createTag(space, tag)
|
return spaceService.createTag(space, tag)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping("/{spaceId}/categories/tags/{tagId}")
|
@DeleteMapping("/{spaceId}/categories/tags/{tagId}")
|
||||||
suspend fun deleteTags(@PathVariable spaceId: String, @PathVariable tagId: String) {
|
suspend fun deleteTags(@PathVariable spaceId: String, @PathVariable tagId: String) {
|
||||||
val space = spaceService.isValidRequest(spaceId)
|
val user = authService.getSecurityUser()
|
||||||
|
val space = spaceService.isValidRequest(spaceId, user)
|
||||||
return spaceService.deleteTag(space, tagId)
|
return spaceService.deleteTag(space, tagId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,39 +344,49 @@ class SpaceController(
|
|||||||
|
|
||||||
@GetMapping("/{spaceId}/recurrents")
|
@GetMapping("/{spaceId}/recurrents")
|
||||||
suspend fun getRecurrents(@PathVariable spaceId: String): List<Recurrent> {
|
suspend fun getRecurrents(@PathVariable spaceId: String): List<Recurrent> {
|
||||||
spaceService.isValidRequest(spaceId)
|
val user = authService.getSecurityUser()
|
||||||
return recurrentService.getRecurrents(spaceId).awaitSingleOrNull().orEmpty()
|
spaceService.isValidRequest(spaceId, user)
|
||||||
|
return recurrentService.getRecurrents(spaceId)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@GetMapping("/{spaceId}/recurrents/{id}")
|
@GetMapping("/{spaceId}/recurrents/{id}")
|
||||||
suspend fun getRecurrent(@PathVariable spaceId: String, @PathVariable id: String): Recurrent {
|
suspend fun getRecurrent(@PathVariable spaceId: String, @PathVariable id: String): Recurrent {
|
||||||
val space = spaceService.isValidRequest(spaceId)
|
val user = authService.getSecurityUser()
|
||||||
|
val space = spaceService.isValidRequest(spaceId, user)
|
||||||
return recurrentService.getRecurrentById(space, id).awaitSingle()
|
return recurrentService.getRecurrentById(space, id).awaitSingle()
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/{spaceId}/recurrent")
|
@PostMapping("/{spaceId}/recurrents")
|
||||||
suspend fun createRecurrent(@PathVariable spaceId: String, @RequestBody recurrent: Recurrent): Recurrent {
|
suspend fun createRecurrent(@PathVariable spaceId: String, @RequestBody recurrent: Recurrent): Recurrent {
|
||||||
val space = spaceService.isValidRequest(spaceId)
|
val user = authService.getSecurityUser()
|
||||||
|
val space = spaceService.isValidRequest(spaceId, user)
|
||||||
return recurrentService.createRecurrent(space, recurrent).awaitSingle()
|
return recurrentService.createRecurrent(space, recurrent).awaitSingle()
|
||||||
}
|
}
|
||||||
|
|
||||||
@PutMapping("/{spaceId}/recurrent/{id}")
|
@PutMapping("/{spaceId}/recurrents/{id}")
|
||||||
suspend fun editRecurrent(
|
suspend fun editRecurrent(
|
||||||
@PathVariable spaceId: String,
|
@PathVariable spaceId: String,
|
||||||
@PathVariable id: String,
|
@PathVariable id: String,
|
||||||
@RequestBody recurrent: Recurrent
|
@RequestBody recurrent: Recurrent
|
||||||
): Recurrent {
|
): Recurrent {
|
||||||
spaceService.isValidRequest(spaceId)
|
val user = authService.getSecurityUser()
|
||||||
|
spaceService.isValidRequest(spaceId, user)
|
||||||
return recurrentService.editRecurrent(recurrent).awaitSingle()
|
return recurrentService.editRecurrent(recurrent).awaitSingle()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@DeleteMapping("/{spaceId}/recurrent/{id}")
|
@DeleteMapping("/{spaceId}/recurrents/{id}")
|
||||||
suspend fun deleteRecurrent(@PathVariable spaceId: String, @PathVariable id: String) {
|
suspend fun deleteRecurrent(@PathVariable spaceId: String, @PathVariable id: String) {
|
||||||
spaceService.isValidRequest(spaceId)
|
val user = authService.getSecurityUser()
|
||||||
recurrentService.deleteRecurrent(id).awaitSingle()
|
val space = spaceService.isValidRequest(spaceId, user)
|
||||||
|
if (space.owner?.id == user.id) {
|
||||||
|
recurrentService.deleteRecurrent(id)
|
||||||
|
} else {
|
||||||
|
throw AuthException("Only owners allowed")
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// @GetMapping("/regen")
|
// @GetMapping("/regen")
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ class SubscriptionController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/notifyAll")
|
@PostMapping("/notifyAll")
|
||||||
fun notifyAll(@RequestBody payload: PushMessage): ResponseEntity<Any> {
|
suspend fun notifyAll(@RequestBody payload: PushMessage): ResponseEntity<Any> {
|
||||||
return try {
|
return try {
|
||||||
ResponseEntity.ok(subscriptionService.sendToAll(payload))
|
ResponseEntity.ok(subscriptionService.sendToAll(payload))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package space.luminic.budgerapp.controllers
|
|||||||
import org.springframework.web.bind.annotation.*
|
import org.springframework.web.bind.annotation.*
|
||||||
import space.luminic.budgerapp.models.WishList
|
import space.luminic.budgerapp.models.WishList
|
||||||
import space.luminic.budgerapp.models.WishListItem
|
import space.luminic.budgerapp.models.WishListItem
|
||||||
|
import space.luminic.budgerapp.services.AuthService
|
||||||
import space.luminic.budgerapp.services.SpaceService
|
import space.luminic.budgerapp.services.SpaceService
|
||||||
import space.luminic.budgerapp.services.WishListService
|
import space.luminic.budgerapp.services.WishListService
|
||||||
|
|
||||||
@@ -11,24 +12,28 @@ import space.luminic.budgerapp.services.WishListService
|
|||||||
@RequestMapping("/spaces/{spaceId}/wishlists")
|
@RequestMapping("/spaces/{spaceId}/wishlists")
|
||||||
class WishListController(
|
class WishListController(
|
||||||
private val wishListService: WishListService,
|
private val wishListService: WishListService,
|
||||||
private val spaceService: SpaceService
|
private val spaceService: SpaceService,
|
||||||
|
private val authService: AuthService
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
suspend fun findWishList(@PathVariable spaceId: String): List<WishList> {
|
suspend fun findWishList(@PathVariable spaceId: String): List<WishList> {
|
||||||
spaceService.isValidRequest(spaceId)
|
val user = authService.getSecurityUser()
|
||||||
|
spaceService.isValidRequest(spaceId, user)
|
||||||
return wishListService.findWishLists(spaceId)
|
return wishListService.findWishLists(spaceId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{wishListId}")
|
@GetMapping("/{wishListId}")
|
||||||
suspend fun getWishList(@PathVariable spaceId: String, @PathVariable wishListId: String): WishList {
|
suspend fun getWishList(@PathVariable spaceId: String, @PathVariable wishListId: String): WishList {
|
||||||
spaceService.isValidRequest(spaceId)
|
val user = authService.getSecurityUser()
|
||||||
|
spaceService.isValidRequest(spaceId, user)
|
||||||
return wishListService.getList(wishListId)
|
return wishListService.getList(wishListId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping
|
@PostMapping
|
||||||
suspend fun createWishList(@PathVariable spaceId: String, @RequestBody wishList: WishList): WishList {
|
suspend fun createWishList(@PathVariable spaceId: String, @RequestBody wishList: WishList): WishList {
|
||||||
val space = spaceService.isValidRequest(spaceId)
|
val user = authService.getSecurityUser()
|
||||||
|
val space = spaceService.isValidRequest(spaceId, user)
|
||||||
return wishListService.createWishList(space, wishList)
|
return wishListService.createWishList(space, wishList)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,7 +43,8 @@ class WishListController(
|
|||||||
@PathVariable wishListId: String,
|
@PathVariable wishListId: String,
|
||||||
@RequestBody wishList: WishList
|
@RequestBody wishList: WishList
|
||||||
): WishList {
|
): WishList {
|
||||||
spaceService.isValidRequest(spaceId)
|
val user = authService.getSecurityUser()
|
||||||
|
spaceService.isValidRequest(spaceId, user)
|
||||||
return wishListService.updateWishListInfo(wishList)
|
return wishListService.updateWishListInfo(wishList)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,7 +55,8 @@ class WishListController(
|
|||||||
@PathVariable itemId: String,
|
@PathVariable itemId: String,
|
||||||
@RequestBody wishListItem: WishListItem
|
@RequestBody wishListItem: WishListItem
|
||||||
): WishList {
|
): WishList {
|
||||||
spaceService.isValidRequest(spaceId)
|
val user = authService.getSecurityUser()
|
||||||
|
spaceService.isValidRequest(spaceId, user)
|
||||||
return wishListService.updateWishListItemInfo(wishListId, wishListItem)
|
return wishListService.updateWishListItemInfo(wishListId, wishListItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,7 +66,8 @@ class WishListController(
|
|||||||
@PathVariable wishListId: String,
|
@PathVariable wishListId: String,
|
||||||
@RequestBody wishListItem: WishListItem
|
@RequestBody wishListItem: WishListItem
|
||||||
): WishList {
|
): WishList {
|
||||||
spaceService.isValidRequest(spaceId)
|
val user = authService.getSecurityUser()
|
||||||
|
spaceService.isValidRequest(spaceId, user)
|
||||||
return wishListService.addItemToWishList(wishListId, wishListItem)
|
return wishListService.addItemToWishList(wishListId, wishListItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,13 +77,15 @@ class WishListController(
|
|||||||
@PathVariable wishListId: String,
|
@PathVariable wishListId: String,
|
||||||
@PathVariable wishListItemId: String
|
@PathVariable wishListItemId: String
|
||||||
): WishList {
|
): WishList {
|
||||||
spaceService.isValidRequest(spaceId)
|
val user = authService.getSecurityUser()
|
||||||
|
spaceService.isValidRequest(spaceId, user)
|
||||||
return wishListService.removeItemFromWishList(wishListId, wishListItemId)
|
return wishListService.removeItemFromWishList(wishListId, wishListItemId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping("/{wishListId}")
|
@DeleteMapping("/{wishListId}")
|
||||||
suspend fun deleteWishList(@PathVariable spaceId: String, @PathVariable wishListId: String) {
|
suspend fun deleteWishList(@PathVariable spaceId: String, @PathVariable wishListId: String) {
|
||||||
spaceService.isValidRequest(spaceId)
|
val user = authService.getSecurityUser()
|
||||||
|
spaceService.isValidRequest(spaceId, user)
|
||||||
wishListService.deleteWishList(wishListId)
|
wishListService.deleteWishList(wishListId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,7 +94,8 @@ class WishListController(
|
|||||||
@PathVariable spaceId: String, @PathVariable wishListId: String,
|
@PathVariable spaceId: String, @PathVariable wishListId: String,
|
||||||
@PathVariable wishlistItemId: String
|
@PathVariable wishlistItemId: String
|
||||||
): WishList {
|
): WishList {
|
||||||
spaceService.isValidRequest(spaceId)
|
val user = authService.getSecurityUser()
|
||||||
|
spaceService.isValidRequest(spaceId, user)
|
||||||
return wishListService.cancelReserve(wishListId, wishlistItemId)
|
return wishListService.cancelReserve(wishListId, wishlistItemId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -36,7 +36,8 @@ class WishListMapper : FromDocumentMapper {
|
|||||||
reserveDoc.getString("name")
|
reserveDoc.getString("name")
|
||||||
) else null,
|
) else null,
|
||||||
updatedAt = it.getDate("updatedAt").toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime(),
|
updatedAt = it.getDate("updatedAt").toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime(),
|
||||||
createdAt = it.getDate("createdAt").toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime()
|
createdAt = it.getDate("createdAt").toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime(),
|
||||||
|
isActive = it.getBoolean("isActive")
|
||||||
)
|
)
|
||||||
}.toMutableList(),
|
}.toMutableList(),
|
||||||
updatedAt = document.getDate("updatedAt").toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime(),
|
updatedAt = document.getDate("updatedAt").toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime(),
|
||||||
|
|||||||
22
src/main/kotlin/space/luminic/budgerapp/models/BotStates.kt
Normal file
22
src/main/kotlin/space/luminic/budgerapp/models/BotStates.kt
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package space.luminic.budgerapp.models
|
||||||
|
|
||||||
|
import org.springframework.data.annotation.Id
|
||||||
|
import org.springframework.data.mongodb.core.mapping.DBRef
|
||||||
|
import org.springframework.data.mongodb.core.mapping.Document
|
||||||
|
|
||||||
|
enum class BotStates {
|
||||||
|
WAIT_CATEGORY
|
||||||
|
}
|
||||||
|
|
||||||
|
@Document("bot-user-states")
|
||||||
|
data class BotUserState(
|
||||||
|
@Id val id: String? = null,
|
||||||
|
@DBRef val user: User,
|
||||||
|
var data: MutableList<ChatData> = mutableListOf(),
|
||||||
|
)
|
||||||
|
|
||||||
|
data class ChatData (
|
||||||
|
val chatId: String,
|
||||||
|
var state: BotStates,
|
||||||
|
var data: MutableMap<String, String> = mutableMapOf(),
|
||||||
|
)
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package space.luminic.budgerapp.models
|
||||||
|
|
||||||
|
data class CategoryPrediction(val category: String, val weight: Double) {
|
||||||
|
}
|
||||||
@@ -2,3 +2,4 @@ package space.luminic.budgerapp.models
|
|||||||
|
|
||||||
|
|
||||||
open class NotFoundException(message: String) : Exception(message)
|
open class NotFoundException(message: String) : Exception(message)
|
||||||
|
open class TelegramBotException(message: String, val chatId: Long) : Exception(message)
|
||||||
@@ -27,6 +27,7 @@ data class WishListItem(
|
|||||||
var price: Double,
|
var price: Double,
|
||||||
var link: String,
|
var link: String,
|
||||||
var images: MutableList<String> = mutableListOf(),
|
var images: MutableList<String> = mutableListOf(),
|
||||||
|
var isActive: Boolean,
|
||||||
var reservedBy: Reserve? = null,
|
var reservedBy: Reserve? = null,
|
||||||
var updatedAt: LocalDateTime = LocalDateTime.now(),
|
var updatedAt: LocalDateTime = LocalDateTime.now(),
|
||||||
var createdAt: LocalDateTime = LocalDateTime.now()
|
var createdAt: LocalDateTime = LocalDateTime.now()
|
||||||
@@ -35,5 +36,4 @@ data class WishListItem(
|
|||||||
data class Reserve(
|
data class Reserve(
|
||||||
val aid: String,
|
val aid: String,
|
||||||
val name: String? = null,
|
val name: String? = null,
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package space.luminic.budgerapp.repos
|
||||||
|
|
||||||
|
import org.springframework.data.mongodb.repository.ReactiveMongoRepository
|
||||||
|
import space.luminic.budgerapp.models.BotUserState
|
||||||
|
|
||||||
|
interface BotStatesRepo: ReactiveMongoRepository<BotUserState, String> {
|
||||||
|
}
|
||||||
@@ -1,10 +1,21 @@
|
|||||||
package space.luminic.budgerapp.repos
|
package space.luminic.budgerapp.repos
|
||||||
|
|
||||||
|
|
||||||
|
import org.bson.types.ObjectId
|
||||||
|
import org.springframework.data.mongodb.repository.Query
|
||||||
import org.springframework.data.mongodb.repository.ReactiveMongoRepository
|
import org.springframework.data.mongodb.repository.ReactiveMongoRepository
|
||||||
import org.springframework.stereotype.Repository
|
import org.springframework.stereotype.Repository
|
||||||
|
import reactor.core.publisher.Flux
|
||||||
import space.luminic.budgerapp.models.Subscription
|
import space.luminic.budgerapp.models.Subscription
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
interface SubscriptionRepo : ReactiveMongoRepository<Subscription, String> {
|
interface SubscriptionRepo : ReactiveMongoRepository<Subscription, String> {
|
||||||
|
|
||||||
|
@Query("{ \$and: [ " +
|
||||||
|
"{ 'user': { '\$ref': 'users', '\$id': ?0 } }, " +
|
||||||
|
"{ 'isActive': true } " +
|
||||||
|
"]}")
|
||||||
|
fun findByUserIdAndIsActive(userId: ObjectId): Flux<Subscription>
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
package space.luminic.budgerapp.services
|
package space.luminic.budgerapp.services
|
||||||
|
|
||||||
import kotlinx.coroutines.reactive.awaitFirstOrNull
|
import kotlinx.coroutines.reactive.awaitFirstOrNull
|
||||||
|
import kotlinx.coroutines.reactor.awaitSingle
|
||||||
|
import kotlinx.coroutines.reactor.awaitSingleOrNull
|
||||||
import org.springframework.cache.annotation.Cacheable
|
import org.springframework.cache.annotation.Cacheable
|
||||||
|
import org.springframework.security.core.context.ReactiveSecurityContextHolder
|
||||||
|
import org.springframework.security.core.userdetails.UsernameNotFoundException
|
||||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import reactor.core.publisher.Mono
|
|
||||||
import space.luminic.budgerapp.configs.AuthException
|
import space.luminic.budgerapp.configs.AuthException
|
||||||
import space.luminic.budgerapp.models.TokenStatus
|
import space.luminic.budgerapp.models.TokenStatus
|
||||||
import space.luminic.budgerapp.models.User
|
import space.luminic.budgerapp.models.User
|
||||||
@@ -25,10 +28,20 @@ class AuthService(
|
|||||||
) {
|
) {
|
||||||
private val passwordEncoder = BCryptPasswordEncoder()
|
private val passwordEncoder = BCryptPasswordEncoder()
|
||||||
|
|
||||||
fun login(username: String, password: String): Mono<String> {
|
suspend fun getSecurityUser(): User {
|
||||||
return userRepository.findByUsername(username)
|
val securityContextHolder = ReactiveSecurityContextHolder.getContext().awaitSingleOrNull()
|
||||||
.flatMap { user ->
|
?: throw AuthException("Authentication failed")
|
||||||
if (passwordEncoder.matches(password, user.password)) {
|
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 token = jwtUtil.generateToken(user.username!!)
|
||||||
val expireAt = Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24 * 10)
|
val expireAt = Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24 * 10)
|
||||||
tokenService.saveToken(
|
tokenService.saveToken(
|
||||||
@@ -38,18 +51,17 @@ class AuthService(
|
|||||||
expireAt.toInstant(),
|
expireAt.toInstant(),
|
||||||
ZoneId.systemDefault()
|
ZoneId.systemDefault()
|
||||||
)
|
)
|
||||||
).thenReturn(token)
|
)
|
||||||
|
token
|
||||||
} else {
|
} else {
|
||||||
Mono.error(AuthException("Invalid credentials"))
|
throw IllegalArgumentException("Ошибка логина или пароля")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun tgLogin(tgId: String): Mono<String> {
|
suspend fun tgLogin(tgId: String): String {
|
||||||
return userRepository.findByTgId(tgId)
|
val user =
|
||||||
.switchIfEmpty(Mono.error(AuthException("Invalid credentials")))
|
userRepository.findByTgId(tgId).awaitSingleOrNull() ?: throw UsernameNotFoundException("Пользователь не найден")
|
||||||
.flatMap { user ->
|
|
||||||
println("here")
|
|
||||||
val token = jwtUtil.generateToken(user.username!!)
|
val token = jwtUtil.generateToken(user.username!!)
|
||||||
val expireAt = Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24 * 10)
|
val expireAt = Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24 * 10)
|
||||||
tokenService.saveToken(
|
tokenService.saveToken(
|
||||||
@@ -59,35 +71,30 @@ class AuthService(
|
|||||||
expireAt.toInstant(),
|
expireAt.toInstant(),
|
||||||
ZoneId.systemDefault()
|
ZoneId.systemDefault()
|
||||||
)
|
)
|
||||||
).thenReturn(token)
|
)
|
||||||
|
return token
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun register(username: String, password: String, firstName: String): Mono<User> {
|
suspend fun register(username: String, password: String, firstName: String): User {
|
||||||
return userRepository.findByUsername(username)
|
val user = userRepository.findByUsername(username).awaitSingleOrNull()
|
||||||
.flatMap<User> { Mono.error(IllegalArgumentException("User with username '$username' already exists")) } // Ошибка, если пользователь уже существует
|
if (user == null) {
|
||||||
.switchIfEmpty(
|
var newUser = User(
|
||||||
Mono.defer {
|
|
||||||
val newUser = User(
|
|
||||||
username = username,
|
username = username,
|
||||||
password = passwordEncoder.encode(password), // Шифрование пароля
|
password = passwordEncoder.encode(password), // Шифрование пароля
|
||||||
firstName = firstName,
|
firstName = firstName,
|
||||||
roles = mutableListOf("USER")
|
roles = mutableListOf("USER")
|
||||||
)
|
)
|
||||||
userRepository.save(newUser).map { user ->
|
newUser = userRepository.save(newUser).awaitSingle()
|
||||||
user.password = null
|
newUser.password = null
|
||||||
user
|
return newUser
|
||||||
} // Сохранение нового пользователя
|
} else throw IllegalArgumentException("Пользователь уже зарегистрирован")
|
||||||
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Cacheable(cacheNames = ["tokens"], key = "#token")
|
@Cacheable(cacheNames = ["tokens"], key = "#token")
|
||||||
suspend fun isTokenValid(token: String): User {
|
suspend fun isTokenValid(token: String): User {
|
||||||
val tokenDetails = tokenService.getToken(token).awaitFirstOrNull() ?: throw AuthException("Invalid token")
|
val tokenDetails = tokenService.getToken(token).awaitFirstOrNull() ?: throw AuthException("Токен не валиден")
|
||||||
when {
|
when {
|
||||||
tokenDetails.status == TokenStatus.ACTIVE && tokenDetails.expiresAt.isAfter(LocalDateTime.now()) -> {
|
tokenDetails.status == TokenStatus.ACTIVE && tokenDetails.expiresAt.isAfter(LocalDateTime.now()) -> {
|
||||||
return userService.getByUserNameWoPass(tokenDetails.username)
|
return userService.getByUserNameWoPass(tokenDetails.username)
|
||||||
@@ -95,7 +102,7 @@ class AuthService(
|
|||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
tokenService.revokeToken(tokenDetails.token)
|
tokenService.revokeToken(tokenDetails.token)
|
||||||
throw AuthException("Token expired or inactive")
|
throw AuthException("Токен истек или не валиден")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
319
src/main/kotlin/space/luminic/budgerapp/services/BotService.kt
Normal file
319
src/main/kotlin/space/luminic/budgerapp/services/BotService.kt
Normal file
@@ -0,0 +1,319 @@
|
|||||||
|
package space.luminic.budgerapp.services
|
||||||
|
|
||||||
|
import kotlinx.coroutines.reactive.awaitSingle
|
||||||
|
import kotlinx.coroutines.reactor.awaitSingleOrNull
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.bson.Document
|
||||||
|
import org.bson.types.ObjectId
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.data.mongodb.core.ReactiveMongoTemplate
|
||||||
|
import org.springframework.data.mongodb.core.aggregation.Aggregation.*
|
||||||
|
|
||||||
|
import org.springframework.data.mongodb.core.query.Criteria
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import org.telegram.telegrambots.bots.TelegramLongPollingBot
|
||||||
|
import org.telegram.telegrambots.meta.api.methods.send.SendMessage
|
||||||
|
import org.telegram.telegrambots.meta.api.methods.updatingmessages.DeleteMessage
|
||||||
|
import org.telegram.telegrambots.meta.api.methods.updatingmessages.EditMessageReplyMarkup
|
||||||
|
import org.telegram.telegrambots.meta.api.methods.updatingmessages.EditMessageText
|
||||||
|
import org.telegram.telegrambots.meta.api.objects.Update
|
||||||
|
import org.telegram.telegrambots.meta.api.objects.replykeyboard.InlineKeyboardMarkup
|
||||||
|
import org.telegram.telegrambots.meta.api.objects.replykeyboard.buttons.InlineKeyboardButton
|
||||||
|
import org.telegram.telegrambots.meta.exceptions.TelegramApiException
|
||||||
|
import space.luminic.budgerapp.configs.TelegramBotConfig
|
||||||
|
import space.luminic.budgerapp.configs.TelegramBotProperties
|
||||||
|
import space.luminic.budgerapp.models.*
|
||||||
|
import space.luminic.budgerapp.repos.BotStatesRepo
|
||||||
|
import java.time.LocalDate
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class BotService(
|
||||||
|
private val telegramBotProperties: TelegramBotProperties,
|
||||||
|
private val botStatesRepo: BotStatesRepo,
|
||||||
|
private val reactiveMongoTemplate: ReactiveMongoTemplate,
|
||||||
|
private val userService: UserService,
|
||||||
|
private val categoriesService: CategoryService,
|
||||||
|
private val financialService: FinancialService,
|
||||||
|
private val spaceService: SpaceService,
|
||||||
|
private val nlpService: NLPService
|
||||||
|
) : TelegramLongPollingBot(telegramBotProperties.token) {
|
||||||
|
private val logger = LoggerFactory.getLogger(javaClass)
|
||||||
|
|
||||||
|
private suspend fun constructCategoriesButtons(
|
||||||
|
nlp: Boolean, text: String? = null, chatId: Long
|
||||||
|
): InlineKeyboardMarkup {
|
||||||
|
val categories = categoriesService.getCategories(
|
||||||
|
"67af3c0f652da946a7dd9931", "EXPENSE", sortBy = "name", direction = "ASC"
|
||||||
|
)
|
||||||
|
|
||||||
|
val keyboard = InlineKeyboardMarkup()
|
||||||
|
val buttonLines = mutableListOf<MutableList<InlineKeyboardButton>>()
|
||||||
|
val filteredCategories = mutableListOf<Category>()
|
||||||
|
if (nlp) {
|
||||||
|
if (text.isNullOrBlank()) {
|
||||||
|
throw TelegramBotException("Текст не может быть пустым", chatId)
|
||||||
|
}
|
||||||
|
val predictedCategories = nlpService.predictCategory(text, 0)
|
||||||
|
|
||||||
|
|
||||||
|
for (category in predictedCategories) {
|
||||||
|
filteredCategories.add(categories.first { it.id == category.category })
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
filteredCategories.addAll(categories)
|
||||||
|
}
|
||||||
|
filteredCategories.map { category ->
|
||||||
|
val btn = InlineKeyboardButton.builder().text("${category.icon} ${category.name}")
|
||||||
|
.callbackData("category_${category.id}").build()
|
||||||
|
|
||||||
|
if (category.name.length >= 15) {
|
||||||
|
// Если текст длинный, создаём отдельную строку для кнопки
|
||||||
|
buttonLines.add(mutableListOf(btn))
|
||||||
|
} else {
|
||||||
|
var isAdded = false
|
||||||
|
|
||||||
|
// Пытаемся добавить кнопку в существующую строку
|
||||||
|
for (line in buttonLines) {
|
||||||
|
if (line.size < 2 && (line.isEmpty() || line[0].text.length < 14)) {
|
||||||
|
line.add(btn)
|
||||||
|
isAdded = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если не нашли подходящую строку, создаём новую
|
||||||
|
if (!isAdded) {
|
||||||
|
buttonLines.add(mutableListOf(btn))
|
||||||
|
} else {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val allCatsBtn = InlineKeyboardButton.builder().text("Все категории").callbackData("all_cats").build()
|
||||||
|
val backButton = InlineKeyboardButton.builder().text("Отмена").callbackData("cancel").build()
|
||||||
|
|
||||||
|
buttonLines.add(mutableListOf(allCatsBtn))
|
||||||
|
buttonLines.add(mutableListOf(backButton))
|
||||||
|
keyboard.keyboard = buttonLines
|
||||||
|
return keyboard
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun getBotUsername(): String {
|
||||||
|
return telegramBotProperties.username
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onUpdateReceived(update: Update) = runBlocking {
|
||||||
|
logger.info("Received message $update")
|
||||||
|
try {
|
||||||
|
if (update.hasCallbackQuery()) {
|
||||||
|
processCallback(update)
|
||||||
|
} else if (update.hasMessage()) {
|
||||||
|
if (update.message.hasText()) {
|
||||||
|
processMessage(update)
|
||||||
|
} else if (update.message.hasPhoto()) {
|
||||||
|
processPhoto(update)
|
||||||
|
} else if (update.message.hasVideo()) {
|
||||||
|
processVideo(update)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: TelegramBotException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
logger.error(e.message)
|
||||||
|
sendMessage(e.chatId.toString(), "${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun processCallback(update: Update) {
|
||||||
|
val chatId = update.callbackQuery.message.chatId.toString()
|
||||||
|
val tgUserId = update.callbackQuery.from.id
|
||||||
|
val user = userService.getUserByTelegramId(tgUserId) ?: throw TelegramBotException(
|
||||||
|
"User ${update.callbackQuery.from.userName} not found", chatId = update.callbackQuery.message.chatId
|
||||||
|
)
|
||||||
|
val state = getState(user.id!!)
|
||||||
|
if (state != null) {
|
||||||
|
when (state.data.first { it.chatId == chatId }.state) {
|
||||||
|
BotStates.WAIT_CATEGORY -> if (update.callbackQuery.data.startsWith("category_")) {
|
||||||
|
confirmTransaction(
|
||||||
|
chatId, user = userService.getUserByTelegramId(tgUserId)!!, update
|
||||||
|
)
|
||||||
|
} else if (update.callbackQuery.data == "cancel") {
|
||||||
|
finishState(chatId, user)
|
||||||
|
val deleteMsg = DeleteMessage(chatId, update.callbackQuery.message.messageId)
|
||||||
|
execute(deleteMsg)
|
||||||
|
sendMessage(chatId, "Введите сумму и комментарий когда будете готовы.")
|
||||||
|
} else if (update.callbackQuery.data == "all_cats") {
|
||||||
|
val editMessageReplyMarkup = EditMessageReplyMarkup()
|
||||||
|
editMessageReplyMarkup.chatId = chatId
|
||||||
|
editMessageReplyMarkup.messageId = update.callbackQuery.message.messageId
|
||||||
|
editMessageReplyMarkup.replyMarkup = constructCategoriesButtons(false, chatId = chatId.toLong())
|
||||||
|
execute(editMessageReplyMarkup)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun processMessage(update: Update) {
|
||||||
|
val user = userService.getUserByTelegramId(update.message.from.id) ?: throw TelegramBotException(
|
||||||
|
"Мы не знакомы",
|
||||||
|
chatId = update.message.chatId,
|
||||||
|
)
|
||||||
|
getState(user.id!!)?.data?.find { it.chatId == update.message.chatId.toString() }?.let {
|
||||||
|
if (it.state == BotStates.WAIT_CATEGORY) {
|
||||||
|
throw TelegramBotException(
|
||||||
|
"Уже есть открытый выбор категории", update.message.chatId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newExpense(
|
||||||
|
update.message.chatId.toString(),
|
||||||
|
user = user,
|
||||||
|
text = update.message.text,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun processPhoto(update: Update) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun processVideo(update: Update) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sendMessage(chatId: String, text: String) {
|
||||||
|
val message = SendMessage(chatId, text)
|
||||||
|
try {
|
||||||
|
execute(message)
|
||||||
|
} catch (e: TelegramApiException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun newExpense(chatId: String, user: User, text: String) {
|
||||||
|
|
||||||
|
val splitText = text.split(" ")
|
||||||
|
if (splitText.size < 2) {
|
||||||
|
try {
|
||||||
|
throw TelegramBotException("Сумма или комментарий не введены", chatId.toLong())
|
||||||
|
|
||||||
|
} catch (e: TelegramApiException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val sum = try {
|
||||||
|
splitText[0].toInt()
|
||||||
|
} catch (e: NumberFormatException) {
|
||||||
|
throw TelegramBotException("Кажется первый параметр не цифра", chatId.toLong())
|
||||||
|
}
|
||||||
|
val textWOSum = splitText.drop(1)
|
||||||
|
var comment = ""
|
||||||
|
textWOSum.map { word ->
|
||||||
|
comment += "$word "
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
val message = SendMessage()
|
||||||
|
message.chatId = chatId
|
||||||
|
val msg = "Выберите категорию"
|
||||||
|
message.text = msg
|
||||||
|
message.replyMarkup = constructCategoriesButtons(true, text, chatId.toLong())
|
||||||
|
val userState = BotUserState(user = user)
|
||||||
|
val chatData =
|
||||||
|
userState.data.find { it.chatId == chatId } ?: ChatData(chatId, state = BotStates.WAIT_CATEGORY)
|
||||||
|
chatData.data["sum"] = sum.toString()
|
||||||
|
chatData.data["comment"] = comment
|
||||||
|
userState.data.add(chatData)
|
||||||
|
try {
|
||||||
|
execute(message)
|
||||||
|
setState(userState)
|
||||||
|
} catch (e: TelegramApiException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun confirmTransaction(chatId: String, user: User, update: Update) {
|
||||||
|
val state = getState(user.id!!)
|
||||||
|
if (state == null) {
|
||||||
|
sendMessage(chatId, "Не можем найти информацию о сумме и комментарии")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val stateData = state.data.find { it.chatId == chatId }
|
||||||
|
if (stateData == null) {
|
||||||
|
sendMessage(chatId, "Не можем найти информацию о сумме и комментарии")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val category = categoriesService.getCategories(
|
||||||
|
"67af3c0f652da946a7dd9931", "EXPENSE", sortBy = "name", direction = "ASC"
|
||||||
|
).first { it.id == update.callbackQuery.data.split("_")[1] }
|
||||||
|
val space = spaceService.getSpace("67af3c0f652da946a7dd9931")
|
||||||
|
val instantType = financialService.getTransactionTypes().first { it.code == "INSTANT" }
|
||||||
|
val transaction = financialService.createTransaction(
|
||||||
|
space, transaction = Transaction(
|
||||||
|
space = space,
|
||||||
|
type = instantType,
|
||||||
|
user = user,
|
||||||
|
category = category,
|
||||||
|
comment = stateData.data["comment"]!!.trim(),
|
||||||
|
date = LocalDate.now(),
|
||||||
|
amount = stateData.data["sum"]!!.toDouble(),
|
||||||
|
isDone = true,
|
||||||
|
parentId = null,
|
||||||
|
createdAt = LocalDateTime.now()
|
||||||
|
), user = user
|
||||||
|
)
|
||||||
|
val editMsg = EditMessageText()
|
||||||
|
editMsg.chatId = chatId
|
||||||
|
editMsg.messageId = update.callbackQuery.message.messageId
|
||||||
|
editMsg.text = "Успешно создали транзакцию c id ${transaction.id}"
|
||||||
|
try {
|
||||||
|
execute(editMsg)
|
||||||
|
// execute(msg)
|
||||||
|
finishState(chatId, user)
|
||||||
|
} catch (e: TelegramApiException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
logger.error(e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private suspend fun getState(userId: String): BotUserState? {
|
||||||
|
val lookup = lookup("users", "user.\$id", "_id", "userDetails")
|
||||||
|
val unwind = unwind("userDetails")
|
||||||
|
val match = match(Criteria.where("userDetails._id").`is`(ObjectId(userId)))
|
||||||
|
|
||||||
|
val aggregation = newAggregation(lookup, unwind, match)
|
||||||
|
return reactiveMongoTemplate.aggregate(aggregation, "bot-user-states", Document::class.java).next().map { doc ->
|
||||||
|
val dataList = doc.getList("data", Document::class.java)
|
||||||
|
BotUserState(
|
||||||
|
id = doc.getObjectId("_id").toString(),
|
||||||
|
user = User(doc.get("userDetails", Document::class.java).getObjectId("_id").toString()),
|
||||||
|
data = dataList.map {
|
||||||
|
val data = it.get("data", Document::class.java)
|
||||||
|
ChatData(
|
||||||
|
chatId = it.getString("chatId"),
|
||||||
|
state = BotStates.valueOf(it.getString("state")),
|
||||||
|
data = (data.toMap().mapValues { it.value.toString() }.toMutableMap())
|
||||||
|
)
|
||||||
|
}.toMutableList(),
|
||||||
|
)
|
||||||
|
}.awaitSingleOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun setState(userState: BotUserState): BotUserState {
|
||||||
|
val stateToSave = userState.user.id?.let { userId ->
|
||||||
|
getState(userId)?.copy(data = userState.data) ?: BotUserState(user = userState.user, data = userState.data)
|
||||||
|
} ?: BotUserState(user = userState.user, data = userState.data)
|
||||||
|
|
||||||
|
return botStatesRepo.save(stateToSave).awaitSingle()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun finishState(chatId: String, user: User) {
|
||||||
|
val state = getState(user.id!!)
|
||||||
|
state?.data?.removeIf { it.chatId == chatId }
|
||||||
|
state?.let { botStatesRepo.save(state).awaitSingle() }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -3,6 +3,8 @@ package space.luminic.budgerapp.services
|
|||||||
|
|
||||||
import kotlinx.coroutines.reactive.awaitFirstOrNull
|
import kotlinx.coroutines.reactive.awaitFirstOrNull
|
||||||
import kotlinx.coroutines.reactive.awaitSingle
|
import kotlinx.coroutines.reactive.awaitSingle
|
||||||
|
import kotlinx.coroutines.reactor.awaitSingle
|
||||||
|
import kotlinx.coroutines.reactor.awaitSingleOrNull
|
||||||
import org.bson.Document
|
import org.bson.Document
|
||||||
import org.bson.types.ObjectId
|
import org.bson.types.ObjectId
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
@@ -16,10 +18,7 @@ import org.springframework.data.mongodb.core.query.isEqualTo
|
|||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import reactor.core.publisher.Mono
|
import reactor.core.publisher.Mono
|
||||||
import space.luminic.budgerapp.mappers.CategoryMapper
|
import space.luminic.budgerapp.mappers.CategoryMapper
|
||||||
import space.luminic.budgerapp.models.Category
|
import space.luminic.budgerapp.models.*
|
||||||
import space.luminic.budgerapp.models.CategoryType
|
|
||||||
import space.luminic.budgerapp.models.NotFoundException
|
|
||||||
import space.luminic.budgerapp.models.Space
|
|
||||||
import space.luminic.budgerapp.repos.BudgetRepo
|
import space.luminic.budgerapp.repos.BudgetRepo
|
||||||
import space.luminic.budgerapp.repos.CategoryRepo
|
import space.luminic.budgerapp.repos.CategoryRepo
|
||||||
|
|
||||||
@@ -30,7 +29,7 @@ class CategoryService(
|
|||||||
private val mongoTemplate: ReactiveMongoTemplate,
|
private val mongoTemplate: ReactiveMongoTemplate,
|
||||||
private val categoryMapper: CategoryMapper,
|
private val categoryMapper: CategoryMapper,
|
||||||
private val budgetRepo: BudgetRepo,
|
private val budgetRepo: BudgetRepo,
|
||||||
|
private val nlpService: NLPService
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val logger = LoggerFactory.getLogger(javaClass)
|
private val logger = LoggerFactory.getLogger(javaClass)
|
||||||
@@ -69,14 +68,15 @@ class CategoryService(
|
|||||||
}.awaitFirstOrNull() ?: throw NotFoundException("Category not found")
|
}.awaitFirstOrNull() ?: throw NotFoundException("Category not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun getCategories(
|
||||||
fun getCategories(
|
|
||||||
spaceId: String,
|
spaceId: String,
|
||||||
type: String? = null,
|
type: String? = null,
|
||||||
sortBy: String,
|
sortBy: String,
|
||||||
direction: String,
|
direction: String,
|
||||||
tagCode: String? = null
|
tagCode: String? = null,
|
||||||
): Mono<List<Category>> {
|
predict: String? = null,
|
||||||
|
cloud: Int? = null
|
||||||
|
): MutableList<Category> {
|
||||||
val lookupSpaces = lookup("spaces", "space.\$id", "_id", "spaceDetails")
|
val lookupSpaces = lookup("spaces", "space.\$id", "_id", "spaceDetails")
|
||||||
val unwindSpace = unwind("spaceDetails")
|
val unwindSpace = unwind("spaceDetails")
|
||||||
val matchCriteria = mutableListOf<Criteria>()
|
val matchCriteria = mutableListOf<Criteria>()
|
||||||
@@ -101,7 +101,7 @@ class CategoryService(
|
|||||||
).filterNotNull()
|
).filterNotNull()
|
||||||
|
|
||||||
val aggregation = newAggregation(aggregationBuilder)
|
val aggregation = newAggregation(aggregationBuilder)
|
||||||
return mongoTemplate.aggregate(
|
val categories = mongoTemplate.aggregate(
|
||||||
aggregation, "categories", Document::class.java
|
aggregation, "categories", Document::class.java
|
||||||
)
|
)
|
||||||
.collectList() // Преобразуем Flux<Transaction> в Mono<List<Transaction>>
|
.collectList() // Преобразуем Flux<Transaction> в Mono<List<Transaction>>
|
||||||
@@ -109,8 +109,19 @@ class CategoryService(
|
|||||||
docs.map { doc ->
|
docs.map { doc ->
|
||||||
categoryMapper.fromDocument(doc)
|
categoryMapper.fromDocument(doc)
|
||||||
}
|
}
|
||||||
|
}.awaitSingle().toMutableList()
|
||||||
|
|
||||||
|
val predictedCategories = mutableListOf<CategoryPrediction>()
|
||||||
|
if (!predict.isNullOrBlank() && cloud != null) {
|
||||||
|
predictedCategories.addAll(nlpService.predictCategory(predict, cloud))
|
||||||
}
|
}
|
||||||
|
val filteredCategories = mutableListOf<Category>()
|
||||||
|
for (category in predictedCategories) {
|
||||||
|
categories.find { it.id == category.category }?.let { filteredCategories.add(it) }
|
||||||
}
|
}
|
||||||
|
return if (filteredCategories.isEmpty()) categories else filteredCategories
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Cacheable("categoryTypes")
|
@Cacheable("categoryTypes")
|
||||||
fun getCategoryTypes(): List<CategoryType> {
|
fun getCategoryTypes(): List<CategoryType> {
|
||||||
@@ -121,7 +132,6 @@ class CategoryService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@CacheEvict(cacheNames = ["getAllCategories"], allEntries = true)
|
|
||||||
suspend fun editCategory(space: Space, category: Category): Category {
|
suspend fun editCategory(space: Space, category: Category): Category {
|
||||||
val oldCategory = findCategory(space, id = category.id)
|
val oldCategory = findCategory(space, id = category.id)
|
||||||
|
|
||||||
@@ -132,9 +142,10 @@ class CategoryService(
|
|||||||
return categoryRepo.save(category).awaitSingle() // Сохраняем категорию, если тип не изменился
|
return categoryRepo.save(category).awaitSingle() // Сохраняем категорию, если тип не изменился
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun deleteCategory(space: Space, categoryId: String) {
|
suspend fun deleteCategory(space: Space, categoryId: String, author: User) {
|
||||||
findCategory(space, categoryId)
|
findCategory(space, categoryId)
|
||||||
val transactions = financialService.getTransactions(space.id!!, categoryId = categoryId).awaitSingle()
|
val transactions = financialService.getTransactions(space.id!!, categoryId = categoryId).awaitSingle()
|
||||||
|
if (transactions.isNotEmpty()) {
|
||||||
val otherCategory = try {
|
val otherCategory = try {
|
||||||
findCategory(space, name = "Другое")
|
findCategory(space, name = "Другое")
|
||||||
} catch (nfe: NotFoundException) {
|
} catch (nfe: NotFoundException) {
|
||||||
@@ -148,9 +159,12 @@ class CategoryService(
|
|||||||
)
|
)
|
||||||
).awaitSingle()
|
).awaitSingle()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
transactions.map { transaction ->
|
transactions.map { transaction ->
|
||||||
transaction.category = otherCategory
|
transaction.category = otherCategory
|
||||||
financialService.editTransaction(transaction)
|
financialService.editTransaction(transaction, author)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val budgets = financialService.findProjectedBudgets(
|
val budgets = financialService.findProjectedBudgets(
|
||||||
ObjectId(space.id),
|
ObjectId(space.id),
|
||||||
@@ -171,7 +185,7 @@ class CategoryService(
|
|||||||
budget.categories.removeIf { it.category.id == categoryId }
|
budget.categories.removeIf { it.category.id == categoryId }
|
||||||
budgetRepo.save(budget)
|
budgetRepo.save(budget)
|
||||||
}
|
}
|
||||||
categoryRepo.deleteById(categoryId).awaitSingle()
|
categoryRepo.deleteById(categoryId).awaitSingleOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
package space.luminic.budgerapp.services
|
package space.luminic.budgerapp.services
|
||||||
|
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.awaitAll
|
|
||||||
import kotlinx.coroutines.coroutineScope
|
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.toList
|
import kotlinx.coroutines.flow.toList
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.reactive.asFlow
|
import kotlinx.coroutines.reactive.asFlow
|
||||||
import kotlinx.coroutines.reactive.awaitFirstOrNull
|
import kotlinx.coroutines.reactive.awaitFirstOrNull
|
||||||
import kotlinx.coroutines.reactive.awaitSingle
|
import kotlinx.coroutines.reactive.awaitSingle
|
||||||
@@ -35,6 +32,7 @@ import space.luminic.budgerapp.repos.CategoryRepo
|
|||||||
import space.luminic.budgerapp.repos.TransactionRepo
|
import space.luminic.budgerapp.repos.TransactionRepo
|
||||||
import space.luminic.budgerapp.repos.WarnRepo
|
import space.luminic.budgerapp.repos.WarnRepo
|
||||||
import java.time.*
|
import java.time.*
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
import java.time.temporal.TemporalAdjusters
|
import java.time.temporal.TemporalAdjusters
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@@ -48,7 +46,9 @@ class FinancialService(
|
|||||||
val reactiveMongoTemplate: ReactiveMongoTemplate,
|
val reactiveMongoTemplate: ReactiveMongoTemplate,
|
||||||
private val categoryRepo: CategoryRepo,
|
private val categoryRepo: CategoryRepo,
|
||||||
val transactionsMapper: TransactionsMapper,
|
val transactionsMapper: TransactionsMapper,
|
||||||
val budgetMapper: BudgetMapper
|
val budgetMapper: BudgetMapper,
|
||||||
|
private val subscriptionService: SubscriptionService,
|
||||||
|
private val nlpService: NLPService
|
||||||
) {
|
) {
|
||||||
private val logger = LoggerFactory.getLogger(FinancialService::class.java)
|
private val logger = LoggerFactory.getLogger(FinancialService::class.java)
|
||||||
|
|
||||||
@@ -57,8 +57,15 @@ class FinancialService(
|
|||||||
val budget = findProjectedBudget(
|
val budget = findProjectedBudget(
|
||||||
transaction.space!!.id!!, budgetId = null, transaction.date, transaction.date
|
transaction.space!!.id!!, budgetId = null, transaction.date, transaction.date
|
||||||
)
|
)
|
||||||
val budgetCategory = budget.categories.firstOrNull { it.category.id == transaction.category.id }
|
if (transaction.category.type.code == "EXPENSE") {
|
||||||
?: throw NotFoundException("Budget category not found in the budget")
|
var budgetCategory = budget.categories.firstOrNull { it.category.id == transaction.category.id }
|
||||||
|
|
||||||
|
if (budgetCategory == null) {
|
||||||
|
budgetCategory = BudgetCategory(0.0, 0.0, 0.0, transaction.category)
|
||||||
|
budget.categories.add(budgetCategory)
|
||||||
|
budgetRepo.save(budget).awaitSingle()
|
||||||
|
|
||||||
|
}
|
||||||
if (transaction.category.type.code == "INCOME") {
|
if (transaction.category.type.code == "INCOME") {
|
||||||
budgetRepo.save(budget).awaitSingle()
|
budgetRepo.save(budget).awaitSingle()
|
||||||
}
|
}
|
||||||
@@ -72,6 +79,7 @@ class FinancialService(
|
|||||||
logger.info("updateBudgetOnCreate end")
|
logger.info("updateBudgetOnCreate end")
|
||||||
budgetRepo.save(budget).awaitSingle()
|
budgetRepo.save(budget).awaitSingle()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
suspend fun updateBudgetOnEdit(
|
suspend fun updateBudgetOnEdit(
|
||||||
@@ -147,26 +155,22 @@ class FinancialService(
|
|||||||
oldBudget: Budget,
|
oldBudget: Budget,
|
||||||
newBudget: Budget
|
newBudget: Budget
|
||||||
) = coroutineScope {
|
) = coroutineScope {
|
||||||
val oldCategory = findBudgetCategory(oldTransaction, oldBudget)
|
// val oldCategory = findBudgetCategory(oldTransaction, oldBudget)
|
||||||
val newCategory = findBudgetCategory(newTransaction, newBudget)
|
// val newCategory = findBudgetCategory(newTransaction, newBudget)
|
||||||
|
|
||||||
|
|
||||||
if (oldCategory.category.id == newCategory.category.id) {
|
|
||||||
async {
|
|
||||||
updateBudgetCategory(oldTransaction, oldBudget, -difference)
|
|
||||||
updateBudgetCategory(newTransaction, newBudget, difference)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
async {
|
async {
|
||||||
updateBudgetCategory(
|
updateBudgetCategory(
|
||||||
oldTransaction,
|
oldTransaction,
|
||||||
oldBudget,
|
oldBudget,
|
||||||
-difference,
|
-difference,
|
||||||
isCategoryChanged = true,
|
isCategoryChanged = true,
|
||||||
isOldCategory = true
|
isOldCategory = true,
|
||||||
|
isNewBudget = true
|
||||||
)
|
)
|
||||||
updateBudgetCategory(newTransaction, newBudget, difference)
|
updateBudgetCategory(newTransaction, newBudget, difference, isNewBudget = true)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun findBudgetCategory(transaction: Transaction, budget: Budget): BudgetCategory {
|
private suspend fun findBudgetCategory(transaction: Transaction, budget: Budget): BudgetCategory {
|
||||||
@@ -202,8 +206,10 @@ class FinancialService(
|
|||||||
budget: Budget,
|
budget: Budget,
|
||||||
difference: Double,
|
difference: Double,
|
||||||
isCategoryChanged: Boolean = false,
|
isCategoryChanged: Boolean = false,
|
||||||
isOldCategory: Boolean = false
|
isOldCategory: Boolean = false,
|
||||||
|
isNewBudget: Boolean = false
|
||||||
): Double {
|
): Double {
|
||||||
|
return if (transaction.category.type.code == "EXPENSE") {
|
||||||
val sums = getBudgetSumsByCategory(transaction.category.id!!, budget)
|
val sums = getBudgetSumsByCategory(transaction.category.id!!, budget)
|
||||||
val categoryBudget = budget.categories.firstOrNull { it.category.id == transaction.category.id }
|
val categoryBudget = budget.categories.firstOrNull { it.category.id == transaction.category.id }
|
||||||
?: throw NotFoundException("Not found category in budget")
|
?: throw NotFoundException("Not found category in budget")
|
||||||
@@ -214,10 +220,13 @@ class FinancialService(
|
|||||||
if (isOldCategory) {
|
if (isOldCategory) {
|
||||||
categoryBudget.currentLimit -= transaction.amount
|
categoryBudget.currentLimit -= transaction.amount
|
||||||
} else categoryBudget.currentLimit += transaction.amount
|
} else categoryBudget.currentLimit += transaction.amount
|
||||||
|
} else if (isNewBudget) {
|
||||||
|
categoryBudget.currentLimit += transaction.amount
|
||||||
} else categoryBudget.currentLimit += difference
|
} else categoryBudget.currentLimit += difference
|
||||||
}
|
}
|
||||||
budgetRepo.save(budget).awaitSingle()
|
budgetRepo.save(budget).awaitSingle()
|
||||||
return 1.0
|
1.0
|
||||||
|
} else 0.0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -367,6 +376,7 @@ class FinancialService(
|
|||||||
budgetDTO.plannedExpenses = transactions.get("plannedExpenses") as MutableList
|
budgetDTO.plannedExpenses = transactions.get("plannedExpenses") as MutableList
|
||||||
budgetDTO.plannedIncomes = transactions["plannedIncomes"] as MutableList
|
budgetDTO.plannedIncomes = transactions["plannedIncomes"] as MutableList
|
||||||
budgetDTO.transactions = transactions["instantTransactions"] as MutableList
|
budgetDTO.transactions = transactions["instantTransactions"] as MutableList
|
||||||
|
logger.info("got budget for spaceId=$spaceId, id=$id")
|
||||||
|
|
||||||
budgetDTO
|
budgetDTO
|
||||||
}
|
}
|
||||||
@@ -752,8 +762,39 @@ class FinancialService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun getAllTransactions(): List<Transaction>{
|
||||||
|
|
||||||
suspend fun getTransactionByParentId(parentId: String): Transaction {
|
// Сборка агрегации
|
||||||
|
val lookup = lookup("categories", "category.\$id", "_id", "categoryDetails")
|
||||||
|
val unwindCategory = unwind("categoryDetails")
|
||||||
|
val lookupSpaces = lookup("spaces", "space.\$id", "_id", "spaceDetails")
|
||||||
|
val unwindSpace = unwind("spaceDetails")
|
||||||
|
val lookupUsers = lookup("users", "user.\$id", "_id", "userDetails")
|
||||||
|
val unwindUser = unwind("userDetails")
|
||||||
|
val sort =
|
||||||
|
sort(Sort.by(Direction.DESC, "date").and(Sort.by(Direction.DESC, "createdAt")))
|
||||||
|
|
||||||
|
val aggregationBuilder = mutableListOf(
|
||||||
|
lookup,
|
||||||
|
unwindCategory,
|
||||||
|
lookupSpaces,
|
||||||
|
unwindSpace,
|
||||||
|
lookupUsers,
|
||||||
|
unwindUser,
|
||||||
|
sort
|
||||||
|
)
|
||||||
|
|
||||||
|
val aggregation = newAggregation(aggregationBuilder)
|
||||||
|
|
||||||
|
return reactiveMongoTemplate.aggregate(
|
||||||
|
aggregation, "transactions", Document::class.java
|
||||||
|
).collectList().awaitSingle().map { doc ->
|
||||||
|
transactionsMapper.fromDocument(doc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
suspend fun getTransactionByParentId(parentId: String): Transaction? {
|
||||||
// Сборка агрегации
|
// Сборка агрегации
|
||||||
val lookup = lookup("categories", "category.\$id", "_id", "categoryDetails")
|
val lookup = lookup("categories", "category.\$id", "_id", "categoryDetails")
|
||||||
val unwindCategory = unwind("categoryDetails")
|
val unwindCategory = unwind("categoryDetails")
|
||||||
@@ -780,7 +821,7 @@ class FinancialService(
|
|||||||
).map { doc ->
|
).map { doc ->
|
||||||
transactionsMapper.fromDocument(doc)
|
transactionsMapper.fromDocument(doc)
|
||||||
|
|
||||||
}.awaitFirstOrNull() ?: throw NotFoundException("Child transaction with parent id $parentId not found")
|
}.awaitFirstOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -843,24 +884,48 @@ class FinancialService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
suspend fun createTransaction(space: Space, transaction: Transaction): Transaction {
|
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||||
val securityContextHolder = ReactiveSecurityContextHolder.getContext().awaitSingle()
|
|
||||||
val user = userService.getByUserNameWoPass(securityContextHolder.authentication.name)
|
suspend fun createTransaction(space: Space, transaction: Transaction, user: User? = null): Transaction {
|
||||||
if (space.users.none { it.id.toString() == user.id }) {
|
val author = user
|
||||||
|
?: userService.getByUserNameWoPass(
|
||||||
|
ReactiveSecurityContextHolder.getContext().awaitSingle().authentication.name
|
||||||
|
)
|
||||||
|
if (space.users.none { it.id.toString() == author.id }) {
|
||||||
throw IllegalArgumentException("User does not have access to this Space")
|
throw IllegalArgumentException("User does not have access to this Space")
|
||||||
}
|
}
|
||||||
// Привязываем space и user к транзакции
|
// Привязываем space и user к транзакции
|
||||||
transaction.user = user
|
transaction.user = author
|
||||||
transaction.space = space
|
transaction.space = space
|
||||||
|
|
||||||
val savedTransaction = transactionsRepo.save(transaction).awaitSingle()
|
val savedTransaction = transactionsRepo.save(transaction).awaitSingle()
|
||||||
|
|
||||||
updateBudgetOnCreate(savedTransaction)
|
updateBudgetOnCreate(savedTransaction)
|
||||||
|
scope.launch {
|
||||||
|
val dateFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy")
|
||||||
|
val transactionType = if (transaction.type.code == "INSTANT") "текущую" else "плановую"
|
||||||
|
|
||||||
|
subscriptionService.sendToSpaceOwner(
|
||||||
|
space.owner!!.id!!, PushMessage(
|
||||||
|
title = "Новая транзакция в пространстве ${space.name}!",
|
||||||
|
body = "Пользователь ${author.username} создал $transactionType транзакцию на сумму ${transaction.amount.toInt()} с комментарием ${transaction.comment} с датой ${
|
||||||
|
dateFormatter.format(
|
||||||
|
transaction.date
|
||||||
|
)
|
||||||
|
}",
|
||||||
|
url = "https://luminic.space/"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
scope.launch {
|
||||||
|
nlpService.reteach()
|
||||||
|
}
|
||||||
return savedTransaction
|
return savedTransaction
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@CacheEvict(cacheNames = ["transactions", "budgets"], allEntries = true)
|
@CacheEvict(cacheNames = ["transactions", "budgets"], allEntries = true)
|
||||||
suspend fun editTransaction(transaction: Transaction): Transaction {
|
suspend fun editTransaction(transaction: Transaction, author: User): Transaction {
|
||||||
val oldStateOfTransaction = getTransactionById(transaction.id!!)
|
val oldStateOfTransaction = getTransactionById(transaction.id!!)
|
||||||
val changed = compareSumDateDoneIsChanged(oldStateOfTransaction, transaction)
|
val changed = compareSumDateDoneIsChanged(oldStateOfTransaction, transaction)
|
||||||
if (!changed) {
|
if (!changed) {
|
||||||
@@ -868,39 +933,90 @@ class FinancialService(
|
|||||||
}
|
}
|
||||||
val amountDifference = transaction.amount - oldStateOfTransaction.amount
|
val amountDifference = transaction.amount - oldStateOfTransaction.amount
|
||||||
|
|
||||||
if (oldStateOfTransaction.isDone && oldStateOfTransaction.type.code == "PLANNED") {
|
if (oldStateOfTransaction.type.code == "PLANNED") {
|
||||||
handleChildTransaction(
|
handleChildTransaction(
|
||||||
oldStateOfTransaction,
|
oldStateOfTransaction,
|
||||||
transaction
|
transaction
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
val space = transaction.space
|
||||||
val savedTransaction = transactionsRepo.save(transaction).awaitSingle()
|
val savedTransaction = transactionsRepo.save(transaction).awaitSingle()
|
||||||
updateBudgetOnEdit(oldStateOfTransaction, savedTransaction, amountDifference)
|
updateBudgetOnEdit(oldStateOfTransaction, savedTransaction, amountDifference)
|
||||||
|
scope.launch {
|
||||||
|
var whatChanged = "nothing"
|
||||||
|
val dateFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy")
|
||||||
|
val transactionType = if (transaction.type.code == "INSTANT") "текущую" else "плановую"
|
||||||
|
|
||||||
|
val sb = StringBuilder()
|
||||||
|
if (oldStateOfTransaction.amount != transaction.amount) {
|
||||||
|
sb.append("${oldStateOfTransaction.amount} → ${transaction.amount}\n")
|
||||||
|
whatChanged = "main"
|
||||||
|
}
|
||||||
|
if (oldStateOfTransaction.comment != transaction.comment) {
|
||||||
|
sb.append("${oldStateOfTransaction.comment} → ${transaction.comment}\n")
|
||||||
|
whatChanged = "main"
|
||||||
|
|
||||||
|
}
|
||||||
|
if (oldStateOfTransaction.date != transaction.date) {
|
||||||
|
sb.append("${dateFormatter.format(oldStateOfTransaction.date)} → ${dateFormatter.format(transaction.date)}\n")
|
||||||
|
whatChanged = "main"
|
||||||
|
}
|
||||||
|
if (!oldStateOfTransaction.isDone && transaction.isDone) {
|
||||||
|
whatChanged = "done_true"
|
||||||
|
}
|
||||||
|
if (oldStateOfTransaction.isDone && !transaction.isDone) {
|
||||||
|
whatChanged = "done_false"
|
||||||
|
}
|
||||||
|
|
||||||
|
val body: String = when (whatChanged) {
|
||||||
|
"main" -> {
|
||||||
|
"Пользователь ${author.username} изменил $transactionType транзакцию:\n$sb"
|
||||||
|
}
|
||||||
|
|
||||||
|
"done_true" -> {
|
||||||
|
"Пользователь ${author.username} выполнил ${transaction.comment} с суммой ${transaction.amount.toInt()}"
|
||||||
|
}
|
||||||
|
|
||||||
|
"done_false" -> {
|
||||||
|
"Пользователь ${author.username} отменил выполнение ${transaction.comment} с суммой ${transaction.amount.toInt()}"
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> "Изменения не обнаружены, но что то точно изменилось"
|
||||||
|
}
|
||||||
|
|
||||||
|
subscriptionService.sendToSpaceOwner(
|
||||||
|
space?.owner!!.id!!, PushMessage(
|
||||||
|
title = "Новое действие в пространстве ${space.name}!",
|
||||||
|
body = body,
|
||||||
|
url = "https://luminic.space/"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return savedTransaction
|
return savedTransaction
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private suspend fun handleChildTransaction(oldTransaction: Transaction, newTransaction: Transaction) {
|
private suspend fun handleChildTransaction(oldTransaction: Transaction, newTransaction: Transaction) {
|
||||||
val childTransaction = getTransactionByParentId(newTransaction.id!!)
|
|
||||||
|
|
||||||
logger.info("Updating child: $childTransaction")
|
|
||||||
val updatedChild = childTransaction.copy(
|
|
||||||
amount = newTransaction.amount,
|
|
||||||
category = newTransaction.category,
|
|
||||||
comment = newTransaction.comment,
|
|
||||||
user = newTransaction.user
|
|
||||||
)
|
|
||||||
transactionsRepo.save(updatedChild).awaitSingle()
|
|
||||||
|
|
||||||
if (!oldTransaction.isDone && newTransaction.isDone) {
|
if (!oldTransaction.isDone && newTransaction.isDone) {
|
||||||
val newChildTransaction = newTransaction.copy(
|
val newChildTransaction = newTransaction.copy(
|
||||||
id = null, type = TransactionType("INSTANT", "Текущие"), parentId = newTransaction.id
|
id = null, type = TransactionType("INSTANT", "Текущие"), parentId = newTransaction.id
|
||||||
)
|
)
|
||||||
transactionsRepo.save(newChildTransaction).awaitSingle()
|
transactionsRepo.save(newChildTransaction).awaitSingle()
|
||||||
updateBudgetOnCreate(newChildTransaction)
|
updateBudgetOnCreate(newChildTransaction)
|
||||||
|
|
||||||
} else if (oldTransaction.isDone && !newTransaction.isDone) {
|
} else if (oldTransaction.isDone && !newTransaction.isDone) {
|
||||||
deleteTransaction(childTransaction)
|
val childTransaction = getTransactionByParentId(newTransaction.id!!)
|
||||||
|
childTransaction?.let { deleteTransaction(it) }
|
||||||
|
} else {
|
||||||
|
val childTransaction = getTransactionByParentId(newTransaction.id!!)
|
||||||
|
childTransaction?.let {
|
||||||
|
logger.info("Updating child: $it")
|
||||||
|
it.amount = newTransaction.amount
|
||||||
|
it.category = newTransaction.category
|
||||||
|
it.comment = newTransaction.comment
|
||||||
|
it.user = newTransaction.user
|
||||||
|
transactionsRepo.save(it).awaitSingle()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package space.luminic.budgerapp.services
|
||||||
|
|
||||||
|
import kotlinx.coroutines.reactive.awaitSingle
|
||||||
|
import kotlinx.coroutines.reactor.awaitSingleOrNull
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import org.springframework.web.reactive.function.client.WebClient
|
||||||
|
import org.springframework.web.reactive.function.client.bodyToMono
|
||||||
|
import space.luminic.budgerapp.configs.NLPConfig
|
||||||
|
import space.luminic.budgerapp.models.CategoryPrediction
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class NLPService(
|
||||||
|
private val nlpConfig: NLPConfig,
|
||||||
|
private val webClient: WebClient = WebClient.builder()
|
||||||
|
.baseUrl(nlpConfig.address)
|
||||||
|
// .defaultHeader("Authorization", "Bearer YOUR_API_KEY")
|
||||||
|
.build(),
|
||||||
|
) {
|
||||||
|
private val logging = LoggerFactory.getLogger(NLPService::class.java)
|
||||||
|
|
||||||
|
suspend fun reteach() {
|
||||||
|
logging.info("Reteaching NLP")
|
||||||
|
webClient.get().uri("/reteach").retrieve().bodyToMono<Void>().awaitSingleOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun predictCategory(category: String, cloud: Int): List<CategoryPrediction> {
|
||||||
|
val response = webClient.get().uri("/predict?req=$category&cloud=$cloud")
|
||||||
|
.retrieve()
|
||||||
|
.bodyToFlux(CategoryPrediction::class.java)
|
||||||
|
.collectList()
|
||||||
|
.awaitSingle()
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package space.luminic.budgerapp.services
|
package space.luminic.budgerapp.services
|
||||||
|
|
||||||
|
|
||||||
|
import kotlinx.coroutines.reactive.awaitFirstOrNull
|
||||||
import kotlinx.coroutines.reactive.awaitLast
|
import kotlinx.coroutines.reactive.awaitLast
|
||||||
import kotlinx.coroutines.reactive.awaitSingle
|
import kotlinx.coroutines.reactive.awaitSingle
|
||||||
import kotlinx.coroutines.reactor.awaitSingleOrNull
|
import kotlinx.coroutines.reactor.awaitSingleOrNull
|
||||||
@@ -36,7 +37,7 @@ class RecurrentService(
|
|||||||
private val logger = LoggerFactory.getLogger(javaClass)
|
private val logger = LoggerFactory.getLogger(javaClass)
|
||||||
|
|
||||||
|
|
||||||
fun getRecurrents(spaceId: String): Mono<List<Recurrent>> {
|
suspend fun getRecurrents(spaceId: String): List<Recurrent> {
|
||||||
val lookupCategories = lookup("categories", "category.\$id", "_id", "categoryDetails")
|
val lookupCategories = lookup("categories", "category.\$id", "_id", "categoryDetails")
|
||||||
val unwindCategory = unwind("categoryDetails")
|
val unwindCategory = unwind("categoryDetails")
|
||||||
|
|
||||||
@@ -48,12 +49,13 @@ class RecurrentService(
|
|||||||
val aggregation =
|
val aggregation =
|
||||||
newAggregation(lookupCategories, unwindCategory, lookupSpace, unwindSpace, matchStage, sort)
|
newAggregation(lookupCategories, unwindCategory, lookupSpace, unwindSpace, matchStage, sort)
|
||||||
// Запрос рекуррентных платежей
|
// Запрос рекуррентных платежей
|
||||||
return reactiveMongoTemplate.aggregate(aggregation, "recurrents", Document::class.java).collectList()
|
return reactiveMongoTemplate.aggregate(aggregation, "recurrents", Document::class.java)
|
||||||
|
.collectList()
|
||||||
.map { docs ->
|
.map { docs ->
|
||||||
docs.map { doc ->
|
docs.map { doc ->
|
||||||
recurrentMapper.fromDocument(doc)
|
recurrentMapper.fromDocument(doc)
|
||||||
}.toList()
|
|
||||||
}
|
}
|
||||||
|
}.awaitSingle()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -82,7 +84,8 @@ class RecurrentService(
|
|||||||
val context = ReactiveSecurityContextHolder.getContext().awaitSingleOrNull()
|
val context = ReactiveSecurityContextHolder.getContext().awaitSingleOrNull()
|
||||||
?: throw IllegalStateException("SecurityContext is empty!")
|
?: throw IllegalStateException("SecurityContext is empty!")
|
||||||
val user = userService.getByUserNameWoPass(context.authentication.name)
|
val user = userService.getByUserNameWoPass(context.authentication.name)
|
||||||
val recurrents = getRecurrents(space.id!!).awaitSingle()
|
val recurrents = getRecurrents(space.id!!)
|
||||||
|
if (recurrents.isNotEmpty()) {
|
||||||
val transactions = recurrents.map { recurrent ->
|
val transactions = recurrents.map { recurrent ->
|
||||||
val transactionDate = when {
|
val transactionDate = when {
|
||||||
recurrent.atDay in budget.dateFrom.dayOfMonth..daysInCurrentMonth -> {
|
recurrent.atDay in budget.dateFrom.dayOfMonth..daysInCurrentMonth -> {
|
||||||
@@ -113,6 +116,8 @@ class RecurrentService(
|
|||||||
transactionRepo.saveAll(transactions).awaitLast()
|
transactionRepo.saveAll(transactions).awaitLast()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
fun editRecurrent(recurrent: Recurrent): Mono<Recurrent> {
|
fun editRecurrent(recurrent: Recurrent): Mono<Recurrent> {
|
||||||
return if (recurrent.id != null && recurrent.atDay <= 31) recurrentRepo.save(recurrent) else Mono.error(
|
return if (recurrent.id != null && recurrent.atDay <= 31) recurrentRepo.save(recurrent) else Mono.error(
|
||||||
@@ -120,8 +125,8 @@ class RecurrentService(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteRecurrent(id: String): Mono<Void> {
|
suspend fun deleteRecurrent(id: String) {
|
||||||
return recurrentRepo.deleteById(id)
|
recurrentRepo.deleteById(id).awaitFirstOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import kotlinx.coroutines.coroutineScope
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.reactive.awaitFirst
|
import kotlinx.coroutines.reactive.awaitFirst
|
||||||
import kotlinx.coroutines.reactive.awaitFirstOrNull
|
import kotlinx.coroutines.reactive.awaitFirstOrNull
|
||||||
|
import kotlinx.coroutines.reactive.awaitLast
|
||||||
import kotlinx.coroutines.reactive.awaitSingle
|
import kotlinx.coroutines.reactive.awaitSingle
|
||||||
import kotlinx.coroutines.reactor.awaitSingleOrNull
|
import kotlinx.coroutines.reactor.awaitSingleOrNull
|
||||||
import org.bson.Document
|
import org.bson.Document
|
||||||
@@ -18,6 +19,7 @@ import space.luminic.budgerapp.configs.AuthException
|
|||||||
import space.luminic.budgerapp.models.*
|
import space.luminic.budgerapp.models.*
|
||||||
import space.luminic.budgerapp.repos.*
|
import space.luminic.budgerapp.repos.*
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
|
import java.time.ZoneId
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@@ -35,14 +37,9 @@ class SpaceService(
|
|||||||
private val tagRepo: TagRepo
|
private val tagRepo: TagRepo
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun isValidRequest(spaceId: String): Space {
|
|
||||||
val securityContextHolder = ReactiveSecurityContextHolder.getContext().awaitSingleOrNull()
|
|
||||||
?: throw AuthException("Authentication failed")
|
|
||||||
val authentication = securityContextHolder.authentication
|
|
||||||
|
|
||||||
val username = authentication.name
|
|
||||||
// Получаем пользователя по имени
|
suspend fun isValidRequest(spaceId: String, user: User): Space {
|
||||||
val user = userService.getByUsername(username)
|
|
||||||
val space = getSpace(spaceId)
|
val space = getSpace(spaceId)
|
||||||
|
|
||||||
// Проверяем доступ пользователя к пространству
|
// Проверяем доступ пользователя к пространству
|
||||||
@@ -92,7 +89,8 @@ class SpaceService(
|
|||||||
username = userDoc.getString("username"),
|
username = userDoc.getString("username"),
|
||||||
firstName = userDoc.getString("firstName")
|
firstName = userDoc.getString("firstName")
|
||||||
)
|
)
|
||||||
}.toMutableList()
|
}.toMutableList(),
|
||||||
|
createdAt = doc.getDate("createdAt").toInstant().atZone(ZoneId.systemDefault()).toLocalDate()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}.awaitFirst()
|
}.awaitFirst()
|
||||||
@@ -120,7 +118,7 @@ class SpaceService(
|
|||||||
.map { category ->
|
.map { category ->
|
||||||
category.copy(id = null, space = savedSpace) // Создаем новую копию
|
category.copy(id = null, space = savedSpace) // Создаем новую копию
|
||||||
}
|
}
|
||||||
categoryRepo.saveAll(categories).awaitSingle()
|
categoryRepo.saveAll(categories).awaitLast()
|
||||||
savedSpace
|
savedSpace
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,12 +140,12 @@ class SpaceService(
|
|||||||
|
|
||||||
launch {
|
launch {
|
||||||
val categories =
|
val categories =
|
||||||
categoryService.getCategories(objectId.toString(), null, "name", "ASC").awaitFirstOrNull().orEmpty()
|
categoryService.getCategories(objectId.toString(), null, "name", "ASC")
|
||||||
categoryRepo.deleteAll(categories).awaitFirstOrNull()
|
categoryRepo.deleteAll(categories).awaitFirstOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
launch {
|
launch {
|
||||||
val recurrents = recurrentService.getRecurrents(objectId.toString()).awaitFirstOrNull().orEmpty()
|
val recurrents = recurrentService.getRecurrents(objectId.toString())
|
||||||
recurrentRepo.deleteAll(recurrents).awaitFirstOrNull()
|
recurrentRepo.deleteAll(recurrents).awaitFirstOrNull()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -282,7 +280,6 @@ class SpaceService(
|
|||||||
val existedTag = findTag(space, tagCode) ?: throw NoSuchElementException("Tag with code $tagCode not found")
|
val existedTag = findTag(space, tagCode) ?: throw NoSuchElementException("Tag with code $tagCode not found")
|
||||||
val categoriesWithTag =
|
val categoriesWithTag =
|
||||||
categoryService.getCategories(space.id!!, sortBy = "name", direction = "ASC", tagCode = existedTag.code)
|
categoryService.getCategories(space.id!!, sortBy = "name", direction = "ASC", tagCode = existedTag.code)
|
||||||
.awaitSingleOrNull().orEmpty()
|
|
||||||
categoriesWithTag.map { cat ->
|
categoriesWithTag.map { cat ->
|
||||||
cat.tags.removeIf { it.code == tagCode } // Изменяем список тегов
|
cat.tags.removeIf { it.code == tagCode } // Изменяем список тегов
|
||||||
cat
|
cat
|
||||||
|
|||||||
@@ -3,13 +3,16 @@ package space.luminic.budgerapp.services
|
|||||||
|
|
||||||
import com.interaso.webpush.VapidKeys
|
import com.interaso.webpush.VapidKeys
|
||||||
import com.interaso.webpush.WebPushService
|
import com.interaso.webpush.WebPushService
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.reactive.awaitSingle
|
import kotlinx.coroutines.reactive.awaitSingle
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
import org.bson.types.ObjectId
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.dao.DuplicateKeyException
|
import org.springframework.dao.DuplicateKeyException
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import reactor.core.publisher.Mono
|
|
||||||
import space.luminic.budgerapp.models.PushMessage
|
import space.luminic.budgerapp.models.PushMessage
|
||||||
import space.luminic.budgerapp.models.Subscription
|
import space.luminic.budgerapp.models.Subscription
|
||||||
import space.luminic.budgerapp.models.SubscriptionDTO
|
import space.luminic.budgerapp.models.SubscriptionDTO
|
||||||
@@ -36,38 +39,50 @@ class SubscriptionService(private val subscriptionRepo: SubscriptionRepo) {
|
|||||||
vapidKeys = VapidKeys.fromUncompressedBytes(VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY)
|
vapidKeys = VapidKeys.fromUncompressedBytes(VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY)
|
||||||
)
|
)
|
||||||
|
|
||||||
fun sendNotification(endpoint: String, p256dh: String, auth: String, payload: PushMessage): Mono<Void> {
|
|
||||||
return Mono.fromRunnable<Void> {
|
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(
|
pushService.send(
|
||||||
payload = Json.encodeToString(payload),
|
payload = Json.encodeToString(payload),
|
||||||
endpoint = endpoint,
|
endpoint = endpoint,
|
||||||
p256dh = p256dh,
|
p256dh = p256dh,
|
||||||
auth = auth
|
auth = auth
|
||||||
)
|
)
|
||||||
}
|
|
||||||
.doOnSuccess {
|
|
||||||
logger.info("Уведомление успешно отправлено на endpoint: $endpoint")
|
logger.info("Уведомление успешно отправлено на endpoint: $endpoint")
|
||||||
}
|
|
||||||
.doOnError { e ->
|
} catch (e: Exception) {
|
||||||
logger.error("Ошибка при отправке уведомления на endpoint $endpoint: ${e.message}")
|
logger.error("Ошибка при отправке уведомления на endpoint $endpoint: ${e.message}")
|
||||||
}
|
throw e
|
||||||
.onErrorResume { e ->
|
|
||||||
Mono.error(e) // Пробрасываем ошибку дальше, если нужна обработка выше
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun sendToAll(payload: PushMessage): Mono<List<String>> {
|
suspend fun sendToAll(payload: PushMessage) {
|
||||||
return subscriptionRepo.findAll()
|
|
||||||
.flatMap { sub ->
|
subscriptionRepo.findAll().collectList().awaitSingle().forEach { sub ->
|
||||||
|
|
||||||
|
try {
|
||||||
sendNotification(sub.endpoint, sub.p256dh, sub.auth, payload)
|
sendNotification(sub.endpoint, sub.p256dh, sub.auth, payload)
|
||||||
.then(Mono.just("${sub.user?.username} at endpoint ${sub.endpoint}"))
|
} catch (e: Exception) {
|
||||||
.onErrorResume { e ->
|
|
||||||
sub.isActive = false
|
sub.isActive = false
|
||||||
subscriptionRepo.save(sub).then(Mono.empty())
|
subscriptionRepo.save(sub).awaitSingle()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.collectList() // Собираем результаты в список
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package space.luminic.budgerapp.services
|
package space.luminic.budgerapp.services
|
||||||
|
|
||||||
|
import kotlinx.coroutines.reactor.awaitSingle
|
||||||
import org.springframework.cache.annotation.CacheEvict
|
import org.springframework.cache.annotation.CacheEvict
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import reactor.core.publisher.Mono
|
import reactor.core.publisher.Mono
|
||||||
@@ -12,14 +13,14 @@ import java.time.LocalDateTime
|
|||||||
class TokenService(private val tokenRepository: TokenRepo) {
|
class TokenService(private val tokenRepository: TokenRepo) {
|
||||||
|
|
||||||
@CacheEvict("tokens", allEntries = true)
|
@CacheEvict("tokens", allEntries = true)
|
||||||
fun saveToken(token: String, username: String, expiresAt: LocalDateTime): Mono<Token> {
|
suspend fun saveToken(token: String, username: String, expiresAt: LocalDateTime): Token {
|
||||||
val newToken = Token(
|
val newToken = Token(
|
||||||
token = token,
|
token = token,
|
||||||
username = username,
|
username = username,
|
||||||
issuedAt = LocalDateTime.now(),
|
issuedAt = LocalDateTime.now(),
|
||||||
expiresAt = expiresAt
|
expiresAt = expiresAt
|
||||||
)
|
)
|
||||||
return tokenRepository.save(newToken)
|
return tokenRepository.save(newToken).awaitSingle()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getToken(token: String): Mono<Token> {
|
fun getToken(token: String): Mono<Token> {
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ class UserService(val userRepo: UserRepo) {
|
|||||||
.switchIfEmpty(Mono.error(Exception("User not found"))) // Обрабатываем случай, когда пользователь не найден
|
.switchIfEmpty(Mono.error(Exception("User not found"))) // Обрабатываем случай, когда пользователь не найден
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun getUserByTelegramId(telegramId: Long): User? {
|
||||||
|
return userRepo.findByTgId(telegramId.toString()).awaitSingleOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Cacheable("users", key = "#username")
|
@Cacheable("users", key = "#username")
|
||||||
suspend fun getByUserNameWoPass(username: String): User {
|
suspend fun getByUserNameWoPass(username: String): User {
|
||||||
|
|||||||
@@ -48,8 +48,10 @@ class WishListService(
|
|||||||
val match = match(
|
val match = match(
|
||||||
Criteria.where("spaceDetails._id").`is`(ObjectId(spaceId))
|
Criteria.where("spaceDetails._id").`is`(ObjectId(spaceId))
|
||||||
.andOperator(
|
.andOperator(
|
||||||
Criteria.where("ownerDetails._id").`is`(ObjectId(user.id))
|
Criteria().orOperator(
|
||||||
.orOperator(Criteria.where("isPrivate").`is`(false))
|
Criteria.where("ownerDetails._id").`is`(ObjectId(user.id)),
|
||||||
|
Criteria.where("isPrivate").`is`(false)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -60,13 +62,14 @@ class WishListService(
|
|||||||
}.toList()
|
}.toList()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getList(listId: String): WishList {
|
suspend fun getList(listId: String, isActive: Boolean = true): WishList {
|
||||||
val user = userService.getByUserNameWoPass(
|
val user = userService.getByUserNameWoPass(
|
||||||
ReactiveSecurityContextHolder.getContext().awaitSingle().authentication.name
|
ReactiveSecurityContextHolder.getContext().awaitSingle().authentication.name
|
||||||
)
|
)
|
||||||
val match = match(
|
val match = match(
|
||||||
Criteria.where("_id").`is`(ObjectId(listId))
|
Criteria.where("_id").`is`(ObjectId(listId))
|
||||||
.andOperator(Criteria.where("ownerDetails._id").`is`(ObjectId(user.id)))
|
// .andOperator(Criteria.where("ownerDetails._id").`is`(ObjectId(user.id)))
|
||||||
|
// .andOperator(Criteria.where("isActive").`is`(isActive))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,19 @@
|
|||||||
package space.luminic.budgerapp.services
|
package space.luminic.budgerapp.services
|
||||||
|
|
||||||
|
import com.mongodb.client.model.Filters.and
|
||||||
|
import com.mongodb.client.model.Filters.eq
|
||||||
import kotlinx.coroutines.reactor.awaitSingle
|
import kotlinx.coroutines.reactor.awaitSingle
|
||||||
import kotlinx.coroutines.reactor.awaitSingleOrNull
|
import kotlinx.coroutines.reactor.awaitSingleOrNull
|
||||||
import org.bson.Document
|
import org.bson.Document
|
||||||
import org.bson.types.ObjectId
|
import org.bson.types.ObjectId
|
||||||
import org.springframework.data.mongodb.core.ReactiveMongoTemplate
|
import org.springframework.data.mongodb.core.ReactiveMongoTemplate
|
||||||
|
import org.springframework.data.mongodb.core.aggregation.Aggregation
|
||||||
import org.springframework.data.mongodb.core.aggregation.Aggregation.*
|
import org.springframework.data.mongodb.core.aggregation.Aggregation.*
|
||||||
|
import org.springframework.data.mongodb.core.aggregation.AggregationOperation
|
||||||
|
import org.springframework.data.mongodb.core.aggregation.LookupOperation
|
||||||
|
import org.springframework.data.mongodb.core.aggregation.VariableOperators
|
||||||
import org.springframework.data.mongodb.core.query.Criteria
|
import org.springframework.data.mongodb.core.query.Criteria
|
||||||
|
import org.springframework.data.mongodb.core.query.Criteria.expr
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import space.luminic.budgerapp.mappers.WishListMapper
|
import space.luminic.budgerapp.mappers.WishListMapper
|
||||||
import space.luminic.budgerapp.models.NotFoundException
|
import space.luminic.budgerapp.models.NotFoundException
|
||||||
@@ -27,7 +34,32 @@ class WishlistExternalService(
|
|||||||
val unwindSpace = unwind("spaceDetails")
|
val unwindSpace = unwind("spaceDetails")
|
||||||
val lookupOwner = lookup("users", "owner.\$id", "_id", "ownerDetails")
|
val lookupOwner = lookup("users", "owner.\$id", "_id", "ownerDetails")
|
||||||
val unwindOwner = unwind("ownerDetails")
|
val unwindOwner = unwind("ownerDetails")
|
||||||
val lookupItems = lookup("wishlistItems", "items.\$id", "_id", "itemsDetails")
|
// val lookupItems = lookup("wishlistItems", "items.\$id", "_id", "itemsDetails")
|
||||||
|
// Расширенный lookup с фильтром isActive == true
|
||||||
|
val lookupItems = AggregationOperation { context ->
|
||||||
|
Document(
|
||||||
|
"\$lookup", Document()
|
||||||
|
.append("from", "wishlistItems")
|
||||||
|
.append("let", Document("itemIds", "\$items.\$id"))
|
||||||
|
.append("pipeline", listOf(
|
||||||
|
Document(
|
||||||
|
"\$match", Document(
|
||||||
|
"\$expr", Document(
|
||||||
|
"\$and", listOf(
|
||||||
|
Document("\$in", listOf("\$_id", Document("\$map", Document()
|
||||||
|
.append("input", "\$\$itemIds")
|
||||||
|
.append("as", "id")
|
||||||
|
.append("in", Document("\$toObjectId", "\$\$id"))
|
||||||
|
))),
|
||||||
|
Document("\$eq", listOf("\$isActive", true))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
))
|
||||||
|
.append("as", "itemsDetails")
|
||||||
|
)
|
||||||
|
}
|
||||||
val match = match(
|
val match = match(
|
||||||
Criteria.where("_id").`is`(ObjectId(wishListId))
|
Criteria.where("_id").`is`(ObjectId(wishListId))
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -24,14 +24,6 @@ class JWTUtil(private val tokenService: TokenService) {
|
|||||||
.setExpiration(expireAt) // 10 дней
|
.setExpiration(expireAt) // 10 дней
|
||||||
.signWith(key)
|
.signWith(key)
|
||||||
.compact()
|
.compact()
|
||||||
tokenService.saveToken(
|
|
||||||
token,
|
|
||||||
username,
|
|
||||||
LocalDateTime.from(
|
|
||||||
expireAt.toInstant().atZone(ZoneId.systemDefault())
|
|
||||||
.toLocalDateTime()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return token
|
return token
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class ScheduledTasks(private val subscriptionService: SubscriptionService,
|
|||||||
private val logger = LoggerFactory.getLogger(ScheduledTasks::class.java)
|
private val logger = LoggerFactory.getLogger(ScheduledTasks::class.java)
|
||||||
|
|
||||||
@Scheduled(cron = "0 30 19 * * *")
|
@Scheduled(cron = "0 30 19 * * *")
|
||||||
fun sendNotificationOfMoneyFilling() {
|
suspend fun sendNotificationOfMoneyFilling() {
|
||||||
subscriptionService.sendToAll(
|
subscriptionService.sendToAll(
|
||||||
PushMessage(
|
PushMessage(
|
||||||
title = "Время заполнять траты!🤑",
|
title = "Время заполнять траты!🤑",
|
||||||
@@ -23,13 +23,6 @@ class ScheduledTasks(private val subscriptionService: SubscriptionService,
|
|||||||
badge = "/apple-touch-icon.png",
|
badge = "/apple-touch-icon.png",
|
||||||
url = "https://luminic.space/transactions/create"
|
url = "https://luminic.space/transactions/create"
|
||||||
)
|
)
|
||||||
).doOnNext { responses ->
|
|
||||||
responses.forEach { response ->
|
|
||||||
logger.info("Уведомление отправлено: $response")
|
|
||||||
}
|
|
||||||
}.subscribe(
|
|
||||||
{ logger.info("Все уведомления отправлены.") },
|
|
||||||
{ error -> logger.error("Ошибка при отправке уведомлений: ${error.message}", error) }
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,11 +17,4 @@ spring.datasource.url=jdbc:postgresql://213.183.51.243/familybudget_app
|
|||||||
spring.datasource.username=familybudget_app
|
spring.datasource.username=familybudget_app
|
||||||
spring.datasource.password=FB1q2w3e4r!
|
spring.datasource.password=FB1q2w3e4r!
|
||||||
|
|
||||||
|
telegram.bot.token = 6972242509:AAGyXuL3T-BNE4XMoo_qvtaYxw_SuiS_dDs
|
||||||
# ??????? JDBC
|
|
||||||
spring.datasource.driver-class-name=org.postgresql.Driver
|
|
||||||
|
|
||||||
# ????????? Hibernate
|
|
||||||
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
|
|
||||||
spring.jpa.hibernate.ddl-auto=none
|
|
||||||
|
|
||||||
@@ -1,27 +1,9 @@
|
|||||||
spring.application.name=budger-app
|
spring.application.name=budger-app
|
||||||
|
spring.data.mongodb.uri=mongodb://budger-app:BA1q2w3e4r!@luminic.space:27017/budger-app?authSource=admin&minPoolSize=10&maxPoolSize=100
|
||||||
spring.data.mongodb.host=77.222.32.64
|
|
||||||
spring.data.mongodb.port=27017
|
|
||||||
spring.data.mongodb.database=budger-app
|
|
||||||
#spring.data.mongodb.username=budger-app
|
#spring.data.mongodb.username=budger-app
|
||||||
#spring.data.mongodb.password=BA1q2w3e4r!
|
#spring.data.mongodb.password=BA1q2w3e4r!
|
||||||
#spring.data.mongodb.authentication-database=admin
|
#spring.data.mongodb.authentication-database=admin
|
||||||
|
|
||||||
|
|
||||||
management.endpoints.web.exposure.include=*
|
management.endpoints.web.exposure.include=*
|
||||||
management.endpoint.health.show-details=always
|
management.endpoint.health.show-details=always
|
||||||
|
telegram.bot.token=6972242509:AAGyXuL3T-BNE4XMoo_qvtaYxw_SuiS_dDs
|
||||||
|
nlp.address=http://127.0.0.1:8000
|
||||||
|
|
||||||
spring.datasource.url=jdbc:postgresql://213.183.51.243/familybudget_app
|
|
||||||
spring.datasource.username=familybudget_app
|
|
||||||
spring.datasource.password=FB1q2w3e4r!
|
|
||||||
|
|
||||||
|
|
||||||
# ??????? JDBC
|
|
||||||
spring.datasource.driver-class-name=org.postgresql.Driver
|
|
||||||
|
|
||||||
# ????????? Hibernate
|
|
||||||
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
|
|
||||||
spring.jpa.hibernate.ddl-auto=none
|
|
||||||
|
|
||||||
@@ -16,5 +16,5 @@ logging.level.org.springframework.data.mongodb.code = DEBUG
|
|||||||
#management.endpoint.metrics.access=read_only
|
#management.endpoint.metrics.access=read_only
|
||||||
|
|
||||||
|
|
||||||
|
telegram.bot.token = 6662300972:AAFXjk_h0AUCy4bORC12UcdXbYnh2QSVKAY
|
||||||
|
nlp.address=https://nlp.luminic.space
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ spring.webflux.base-path=/api
|
|||||||
spring.profiles.active=prod
|
spring.profiles.active=prod
|
||||||
spring.main.web-application-type=reactive
|
spring.main.web-application-type=reactive
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
logging.level.org.springframework.web=INFO
|
logging.level.org.springframework.web=INFO
|
||||||
logging.level.org.springframework.data = INFO
|
logging.level.org.springframework.data = INFO
|
||||||
logging.level.org.springframework.data.mongodb.core.ReactiveMongoTemplate=INFO
|
logging.level.org.springframework.data.mongodb.core.ReactiveMongoTemplate=INFO
|
||||||
@@ -34,3 +32,6 @@ management.endpoints.web.exposure.include=*
|
|||||||
# Enable Prometheus metrics export
|
# Enable Prometheus metrics export
|
||||||
management.prometheus.metrics.export.enabled=true
|
management.prometheus.metrics.export.enabled=true
|
||||||
|
|
||||||
|
telegram.bot.username = expenses_diary_bot
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user