package space.luminic.budgerapp.controllers import com.opencsv.CSVWriter import kotlinx.coroutines.reactor.awaitSingle import kotlinx.coroutines.reactor.awaitSingleOrNull import org.apache.commons.io.IOUtils.writer import org.bson.Document import org.slf4j.LoggerFactory import org.springframework.http.HttpHeaders import org.springframework.http.HttpStatus import org.springframework.http.MediaType import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.* import space.luminic.budgerapp.controllers.BudgetController.LimitValue import space.luminic.budgerapp.controllers.dtos.BudgetCreationDTO import space.luminic.budgerapp.models.* import space.luminic.budgerapp.services.* import java.io.ByteArrayOutputStream import java.io.OutputStreamWriter import java.time.LocalDate @RestController @RequestMapping("/spaces") class SpaceController( private val spaceService: SpaceService, private val financialService: FinancialService, private val categoryService: CategoryService, private val recurrentService: RecurrentService, private val authService: AuthService ) { private val log = LoggerFactory.getLogger(SpaceController::class.java) data class SpaceCreateDTO( val name: String, val description: String, val createCategories: Boolean, ) @GetMapping suspend fun getSpaces(): List { return spaceService.getSpaces() } @PostMapping suspend fun createSpace(@RequestBody space: SpaceCreateDTO): Space { return spaceService.createSpace( Space(name = space.name, description = space.description), space.createCategories ) } @GetMapping("{spaceId}") suspend fun getSpace(@PathVariable spaceId: String): Space { return spaceService.getSpace(spaceId) } @DeleteMapping("/{spaceId}") suspend fun deleteSpace(@PathVariable spaceId: String) { val user = authService.getSecurityUser() val space = spaceService.isValidRequest(spaceId, user) return spaceService.deleteSpace(space) } @PostMapping("/{spaceId}/invite") suspend fun inviteSpace(@PathVariable spaceId: String): SpaceInvite { val user = authService.getSecurityUser() spaceService.isValidRequest(spaceId, user) return spaceService.createInviteSpace(spaceId) } @PostMapping("/invite/{code}") suspend fun acceptInvite(@PathVariable code: String): Space { return spaceService.acceptInvite(code) } @DeleteMapping("/{spaceId}/leave") suspend fun leaveSpace(@PathVariable spaceId: String) { val user = authService.getSecurityUser() spaceService.isValidRequest(spaceId, user) return spaceService.leaveSpace(spaceId) } @DeleteMapping("/{spaceId}/members/kick/{username}") suspend fun kickMembers(@PathVariable spaceId: String, @PathVariable username: String) { val user = authService.getSecurityUser() spaceService.isValidRequest(spaceId, user) return spaceService.kickMember(spaceId, username) } // //Budgets API // @GetMapping("/{spaceId}/budgets") suspend fun getBudgets(@PathVariable spaceId: String): List { val user = authService.getSecurityUser() spaceService.isValidRequest(spaceId, user) return financialService.getBudgets(spaceId).awaitSingleOrNull().orEmpty() } @GetMapping("/{spaceId}/budgets/{id}") suspend fun getBudget(@PathVariable spaceId: String, @PathVariable id: String): BudgetDTO? { log.info("Getting budget for spaceId=$spaceId, id=$id") val user = authService.getSecurityUser() spaceService.isValidRequest(spaceId, user) return financialService.getBudget(spaceId, id) } @PostMapping("/{spaceId}/budgets") suspend fun createBudget( @PathVariable spaceId: String, @RequestBody budgetCreationDTO: BudgetCreationDTO, ): Budget? { val user = authService.getSecurityUser() val space = spaceService.isValidRequest(spaceId, user) return financialService.createBudget( space, budgetCreationDTO.budget, budgetCreationDTO.createRecurrent ) } @DeleteMapping("/{spaceId}/budgets/{id}") suspend fun deleteBudget(@PathVariable spaceId: String, @PathVariable id: String) { val user = authService.getSecurityUser() spaceService.isValidRequest(spaceId, user) financialService.deleteBudget(spaceId, id) } @PostMapping("/{spaceId}/budgets/{budgetId}/categories/{catId}/limit") suspend fun setCategoryLimit( @PathVariable spaceId: String, @PathVariable budgetId: String, @PathVariable catId: String, @RequestBody limit: LimitValue, ): BudgetCategory { val user = authService.getSecurityUser() spaceService.isValidRequest(spaceId, user) return financialService.setCategoryLimit(spaceId, budgetId, catId, limit.limit) } // // Transactions API // @GetMapping("/{spaceId}/transactions") fun getTransactions( @PathVariable spaceId: String, @RequestParam(value = "transaction_type") transactionType: String? = null, @RequestParam(value = "category_type") categoryType: String? = null, @RequestParam(value = "user_id") userId: String? = null, @RequestParam(value = "is_child") isChild: Boolean? = null, @RequestParam(value = "limit") limit: Int = 10, @RequestParam(value = "offset") offset: Int = 0 ): ResponseEntity { try { return ResponseEntity.ok( financialService.getTransactions( spaceId = spaceId, transactionType = transactionType, categoryType = categoryType, userId = userId, isChild = isChild, limit = limit, offset = offset ) ) } catch (e: Exception) { e.printStackTrace() return ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR) } } @GetMapping("/{spaceId}/transactions/csv") suspend fun getTransactionsCSV( @PathVariable spaceId: String, @RequestParam(value = "transaction_type") transactionType: String? = null, @RequestParam(value = "category_type") categoryType: String? = null, @RequestParam(value = "user_id") userId: String? = null, @RequestParam(value = "is_child") isChild: Boolean? = null, @RequestParam(value = "limit") limit: Int = 20000, @RequestParam(value = "offset") offset: Int = 0 ): ResponseEntity { try { val bos = ByteArrayOutputStream() val writer = CSVWriter(OutputStreamWriter(bos)) val CSVHeaders = arrayOf("id", "name", "category") writer.writeNext(CSVHeaders) financialService.getTransactions( spaceId = spaceId, transactionType = transactionType, categoryType = categoryType, userId = userId, isChild = isChild, limit = limit, offset = offset ).awaitSingle().map { val data = arrayOf(it.id, it.comment, it.category.name) 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 ): List { val user = authService.getSecurityUser() spaceService.isValidRequest(spaceId, user) return categoryService.getCategories( "67af3c0f652da946a7dd9931", "EXPENSE", sortBy = "name", direction = "ASC", predict = comment ) } @GetMapping("/{spaceId}/transactions/{id}") suspend fun getTransaction( @PathVariable spaceId: String, @PathVariable id: String ): Transaction { return financialService.getTransactionById(id) } @PostMapping("/{spaceId}/transactions") suspend fun createTransaction(@PathVariable spaceId: String, @RequestBody transaction: Transaction): Transaction { val user = authService.getSecurityUser() val space = spaceService.isValidRequest(spaceId, user) return financialService.createTransaction(space, transaction) } @PutMapping("/{spaceId}/transactions/{id}") suspend fun editTransaction( @PathVariable spaceId: String, @PathVariable id: String, @RequestBody transaction: Transaction ): Transaction { val user = authService.getSecurityUser() val space = spaceService.isValidRequest(spaceId, user) transaction.space = space return financialService.editTransaction(transaction, user) } @DeleteMapping("/{spaceId}/transactions/{id}") suspend fun deleteTransaction(@PathVariable spaceId: String, @PathVariable id: String) { val user = authService.getSecurityUser() spaceService.isValidRequest(spaceId, user) val transaction = financialService.getTransactionById(id) financialService.deleteTransaction(transaction) } // // Categories API // @GetMapping("/{spaceId}/categories") suspend fun getCategories( @PathVariable spaceId: String, @RequestParam("type") type: String? = null, @RequestParam("sort") sortBy: String = "name", @RequestParam("direction") direction: String = "ASC" ): List { val user = authService.getSecurityUser() spaceService.isValidRequest(spaceId, user) return categoryService.getCategories(spaceId, type, sortBy, direction) } @GetMapping("/{spaceId}/categories/types") fun getCategoriesTypes(@PathVariable spaceId: String): List { return categoryService.getCategoryTypes() } @PostMapping("/{spaceId}/categories") suspend fun createCategory( @PathVariable spaceId: String, @RequestBody category: Category ): Category { val user = authService.getSecurityUser() val space = spaceService.isValidRequest(spaceId, user) return financialService.createCategory(space, category).awaitSingle() } @PutMapping("/{spaceId}/categories/{categoryId}") suspend fun editCategory( @PathVariable categoryId: String, @RequestBody category: Category, @PathVariable spaceId: String ): Category { val user = authService.getSecurityUser() val space = spaceService.isValidRequest(spaceId, user) return categoryService.editCategory(space, category) } @DeleteMapping("/{spaceId}/categories/{categoryId}") suspend fun deleteCategory(@PathVariable categoryId: String, @PathVariable spaceId: String) { val user = authService.getSecurityUser() val space = spaceService.isValidRequest(spaceId, user) categoryService.deleteCategory(space, categoryId, user) } @GetMapping("/{spaceId}/categories/tags") suspend fun getTags(@PathVariable spaceId: String): List { val user = authService.getSecurityUser() val space = spaceService.isValidRequest(spaceId, user) return spaceService.getTags(space) } @PostMapping("/{spaceId}/categories/tags") suspend fun createTags(@PathVariable spaceId: String, @RequestBody tag: Tag): Tag { val user = authService.getSecurityUser() val space = spaceService.isValidRequest(spaceId, user) return spaceService.createTag(space, tag) } @DeleteMapping("/{spaceId}/categories/tags/{tagId}") suspend fun deleteTags(@PathVariable spaceId: String, @PathVariable tagId: String) { val user = authService.getSecurityUser() val space = spaceService.isValidRequest(spaceId, user) return spaceService.deleteTag(space, tagId) } @GetMapping("/{spaceId}/analytics/by-month") suspend fun getCategoriesSumsByMonthsV2(@PathVariable spaceId: String): List { return financialService.getCategorySummaries(spaceId, LocalDate.now().minusMonths(6)) } // // Recurrents API // @GetMapping("/{spaceId}/recurrents") suspend fun getRecurrents(@PathVariable spaceId: String): List { val user = authService.getSecurityUser() spaceService.isValidRequest(spaceId, user) return recurrentService.getRecurrents(spaceId) } @GetMapping("/{spaceId}/recurrents/{id}") suspend fun getRecurrent(@PathVariable spaceId: String, @PathVariable id: String): Recurrent { val user = authService.getSecurityUser() val space = spaceService.isValidRequest(spaceId, user) return recurrentService.getRecurrentById(space, id).awaitSingle() } @PostMapping("/{spaceId}/recurrent") suspend fun createRecurrent(@PathVariable spaceId: String, @RequestBody recurrent: Recurrent): Recurrent { val user = authService.getSecurityUser() val space = spaceService.isValidRequest(spaceId, user) return recurrentService.createRecurrent(space, recurrent).awaitSingle() } @PutMapping("/{spaceId}/recurrent/{id}") suspend fun editRecurrent( @PathVariable spaceId: String, @PathVariable id: String, @RequestBody recurrent: Recurrent ): Recurrent { val user = authService.getSecurityUser() spaceService.isValidRequest(spaceId, user) return recurrentService.editRecurrent(recurrent).awaitSingle() } @DeleteMapping("/{spaceId}/recurrent/{id}") suspend fun deleteRecurrent(@PathVariable spaceId: String, @PathVariable id: String) { val user = authService.getSecurityUser() spaceService.isValidRequest(spaceId, user) recurrentService.deleteRecurrent(id).awaitSingle() } // @GetMapping("/regen") // fun regenSpaces(): Mono { // return spaceService.regenSpaceCategory() // } }