Add Transaction routes
This commit is contained in:
parent
f6e0157560
commit
9fc3d1ac1c
12 changed files with 212 additions and 288 deletions
|
@ -10,6 +10,7 @@ import io.ktor.auth.*
|
||||||
import io.ktor.http.*
|
import io.ktor.http.*
|
||||||
import io.ktor.response.*
|
import io.ktor.response.*
|
||||||
import io.ktor.util.pipeline.*
|
import io.ktor.util.pipeline.*
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
suspend inline fun PipelineContext<Unit, ApplicationCall>.requireBudgetWithPermission(
|
suspend inline fun PipelineContext<Unit, ApplicationCall>.requireBudgetWithPermission(
|
||||||
permissionRepository: PermissionRepository,
|
permissionRepository: PermissionRepository,
|
||||||
|
@ -56,3 +57,5 @@ suspend inline fun PipelineContext<Unit, ApplicationCall>.errorResponse(
|
||||||
call.respond(httpStatusCode, ErrorResponse(message))
|
call.respond(httpStatusCode, ErrorResponse(message))
|
||||||
}?: call.respond(httpStatusCode)
|
}?: call.respond(httpStatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun String.toInstant(): Instant = Instant.parse(this)
|
|
@ -36,7 +36,7 @@ fun Application.budgetRoutes(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
post("/{id}") {
|
post("/") {
|
||||||
val session = call.principal<Session>()!!
|
val session = call.principal<Session>()!!
|
||||||
val request = call.receive<BudgetRequest>()
|
val request = call.receive<BudgetRequest>()
|
||||||
if (request.name.isNullOrBlank()) {
|
if (request.name.isNullOrBlank()) {
|
||||||
|
|
|
@ -41,7 +41,7 @@ fun Application.categoryRoutes(
|
||||||
).map { CategoryResponse(it) })
|
).map { CategoryResponse(it) })
|
||||||
}
|
}
|
||||||
|
|
||||||
post("/{id}") {
|
post("/") {
|
||||||
val session = call.principal<Session>()!!
|
val session = call.principal<Session>()!!
|
||||||
val request = call.receive<CategoryRequest>()
|
val request = call.receive<CategoryRequest>()
|
||||||
if (request.title.isNullOrBlank()) {
|
if (request.title.isNullOrBlank()) {
|
||||||
|
|
|
@ -2,17 +2,7 @@ package com.wbrawner.twigs
|
||||||
|
|
||||||
import com.wbrawner.twigs.model.Transaction
|
import com.wbrawner.twigs.model.Transaction
|
||||||
|
|
||||||
data class NewTransactionRequest(
|
data class TransactionRequest(
|
||||||
val title: String,
|
|
||||||
val description: String? = null,
|
|
||||||
val date: String,
|
|
||||||
val amount: Long,
|
|
||||||
val categoryId: String? = null,
|
|
||||||
val expense: Boolean,
|
|
||||||
val budgetId: String
|
|
||||||
)
|
|
||||||
|
|
||||||
data class UpdateTransactionRequest(
|
|
||||||
val title: String? = null,
|
val title: String? = null,
|
||||||
val description: String? = null,
|
val description: String? = null,
|
||||||
val date: String? = null,
|
val date: String? = null,
|
||||||
|
@ -20,7 +10,6 @@ data class UpdateTransactionRequest(
|
||||||
val categoryId: String? = null,
|
val categoryId: String? = null,
|
||||||
val expense: Boolean? = null,
|
val expense: Boolean? = null,
|
||||||
val budgetId: String? = null,
|
val budgetId: String? = null,
|
||||||
val createdBy: String? = null
|
|
||||||
)
|
)
|
||||||
|
|
||||||
data class TransactionResponse(
|
data class TransactionResponse(
|
||||||
|
@ -33,16 +22,18 @@ data class TransactionResponse(
|
||||||
val budgetId: String,
|
val budgetId: String,
|
||||||
val categoryId: String?,
|
val categoryId: String?,
|
||||||
val createdBy: String
|
val createdBy: String
|
||||||
) {
|
)
|
||||||
constructor(transaction: Transaction) : this(
|
|
||||||
transaction.id,
|
data class BalanceResponse(val balance: Long)
|
||||||
transaction.title,
|
|
||||||
transaction.description,
|
fun Transaction.asResponse(): TransactionResponse = TransactionResponse(
|
||||||
transaction.date.toString(),
|
id = id,
|
||||||
transaction.amount,
|
title = title,
|
||||||
transaction.expense,
|
description = description,
|
||||||
transaction.budget!!.id,
|
date = date.toString(),
|
||||||
transaction.category?.id,
|
amount = amount,
|
||||||
transaction.createdBy!!.id
|
expense = expense,
|
||||||
)
|
budgetId = budgetId,
|
||||||
}
|
categoryId = categoryId,
|
||||||
|
createdBy = createdBy
|
||||||
|
)
|
162
api/src/main/kotlin/com/wbrawner/twigs/TransactionRoutes.kt
Normal file
162
api/src/main/kotlin/com/wbrawner/twigs/TransactionRoutes.kt
Normal file
|
@ -0,0 +1,162 @@
|
||||||
|
package com.wbrawner.twigs
|
||||||
|
|
||||||
|
import com.wbrawner.twigs.model.Permission
|
||||||
|
import com.wbrawner.twigs.model.Transaction
|
||||||
|
import com.wbrawner.twigs.storage.PermissionRepository
|
||||||
|
import com.wbrawner.twigs.storage.Session
|
||||||
|
import com.wbrawner.twigs.storage.TransactionRepository
|
||||||
|
import io.ktor.application.*
|
||||||
|
import io.ktor.auth.*
|
||||||
|
import io.ktor.http.*
|
||||||
|
import io.ktor.request.*
|
||||||
|
import io.ktor.response.*
|
||||||
|
import io.ktor.routing.*
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
|
fun Application.transactionRoutes(
|
||||||
|
transactionRepository: TransactionRepository,
|
||||||
|
permissionRepository: PermissionRepository
|
||||||
|
) {
|
||||||
|
routing {
|
||||||
|
route("/api/transactions") {
|
||||||
|
authenticate(optional = false) {
|
||||||
|
get("/") {
|
||||||
|
val session = call.principal<Session>()!!
|
||||||
|
call.respond(transactionRepository.findAll(
|
||||||
|
budgetIds = permissionRepository.findAll(
|
||||||
|
budgetIds = call.request.queryParameters.getAll("budgetIds"),
|
||||||
|
userId = session.userId
|
||||||
|
).map { it.budgetId },
|
||||||
|
categoryIds = call.request.queryParameters.getAll("categoryIds"),
|
||||||
|
from = call.request.queryParameters["from"]?.let { Instant.parse(it) },
|
||||||
|
to = call.request.queryParameters["to"]?.let { Instant.parse(it) },
|
||||||
|
expense = call.request.queryParameters["expense"]?.toBoolean(),
|
||||||
|
).map { it.asResponse() })
|
||||||
|
}
|
||||||
|
|
||||||
|
get("/{id}") {
|
||||||
|
val session = call.principal<Session>()!!
|
||||||
|
val transaction = transactionRepository.findAll(
|
||||||
|
ids = call.parameters.getAll("id"),
|
||||||
|
budgetIds = permissionRepository.findAll(
|
||||||
|
userId = session.userId
|
||||||
|
)
|
||||||
|
.map { it.budgetId }
|
||||||
|
)
|
||||||
|
.map { it.asResponse() }
|
||||||
|
.firstOrNull()
|
||||||
|
transaction?.let {
|
||||||
|
call.respond(it)
|
||||||
|
} ?: errorResponse()
|
||||||
|
}
|
||||||
|
|
||||||
|
get("/sum") {
|
||||||
|
val categoryId = call.request.queryParameters["categoryId"]
|
||||||
|
val budgetId = call.request.queryParameters["budgetId"]
|
||||||
|
val from = call.request.queryParameters["from"]?.toInstant() ?: firstOfMonth.toInstant()
|
||||||
|
val to = call.request.queryParameters["to"]?.toInstant() ?: endOfMonth.toInstant()
|
||||||
|
val balance = if (!categoryId.isNullOrBlank()) {
|
||||||
|
if (!budgetId.isNullOrBlank()) {
|
||||||
|
errorResponse(HttpStatusCode.BadRequest, "budgetId and categoryId cannot be provided together")
|
||||||
|
return@get
|
||||||
|
}
|
||||||
|
transactionRepository.sumByCategory(categoryId, from, to)
|
||||||
|
} else if (!budgetId.isNullOrBlank()) {
|
||||||
|
transactionRepository.sumByBudget(budgetId, from, to)
|
||||||
|
} else {
|
||||||
|
errorResponse(HttpStatusCode.BadRequest, "budgetId or categoryId must be provided to sum")
|
||||||
|
return@get
|
||||||
|
}
|
||||||
|
call.respond(BalanceResponse(balance))
|
||||||
|
}
|
||||||
|
|
||||||
|
post("/") {
|
||||||
|
val session = call.principal<Session>()!!
|
||||||
|
val request = call.receive<TransactionRequest>()
|
||||||
|
if (request.title.isNullOrBlank()) {
|
||||||
|
errorResponse(HttpStatusCode.BadRequest, "Title cannot be null or empty")
|
||||||
|
return@post
|
||||||
|
}
|
||||||
|
if (request.budgetId.isNullOrBlank()) {
|
||||||
|
errorResponse(HttpStatusCode.BadRequest, "Budget ID cannot be null or empty")
|
||||||
|
return@post
|
||||||
|
}
|
||||||
|
requireBudgetWithPermission(
|
||||||
|
permissionRepository,
|
||||||
|
session.userId,
|
||||||
|
request.budgetId,
|
||||||
|
Permission.WRITE
|
||||||
|
) {
|
||||||
|
return@post
|
||||||
|
}
|
||||||
|
call.respond(
|
||||||
|
transactionRepository.save(
|
||||||
|
Transaction(
|
||||||
|
title = request.title,
|
||||||
|
description = request.description,
|
||||||
|
amount = request.amount ?: 0L,
|
||||||
|
expense = request.expense ?: true,
|
||||||
|
budgetId = request.budgetId,
|
||||||
|
categoryId = request.categoryId,
|
||||||
|
createdBy = session.userId,
|
||||||
|
date = request.date?.let { Instant.parse(it) } ?: Instant.now()
|
||||||
|
)
|
||||||
|
).asResponse()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
put("/{id}") {
|
||||||
|
val session = call.principal<Session>()!!
|
||||||
|
val request = call.receive<TransactionRequest>()
|
||||||
|
val transaction = transactionRepository.findAll(ids = call.parameters.getAll("id"))
|
||||||
|
.firstOrNull()
|
||||||
|
?: run {
|
||||||
|
errorResponse()
|
||||||
|
return@put
|
||||||
|
}
|
||||||
|
requireBudgetWithPermission(
|
||||||
|
permissionRepository,
|
||||||
|
session.userId,
|
||||||
|
transaction.budgetId,
|
||||||
|
Permission.WRITE
|
||||||
|
) {
|
||||||
|
return@put
|
||||||
|
}
|
||||||
|
call.respond(
|
||||||
|
transactionRepository.save(
|
||||||
|
Transaction(
|
||||||
|
title = request.title ?: transaction.title,
|
||||||
|
description = request.description ?: transaction.description,
|
||||||
|
amount = request.amount ?: transaction.amount,
|
||||||
|
expense = request.expense ?: transaction.expense,
|
||||||
|
date = request.date?.let { Instant.parse(it) } ?: transaction.date,
|
||||||
|
categoryId = request.categoryId ?: transaction.categoryId,
|
||||||
|
budgetId = request.budgetId ?: transaction.budgetId,
|
||||||
|
createdBy = transaction.createdBy,
|
||||||
|
)
|
||||||
|
).asResponse()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
delete("/{id}") {
|
||||||
|
val session = call.principal<Session>()!!
|
||||||
|
val transaction = transactionRepository.findAll(ids = call.parameters.getAll("id"))
|
||||||
|
.firstOrNull()
|
||||||
|
?: run {
|
||||||
|
errorResponse()
|
||||||
|
return@delete
|
||||||
|
}
|
||||||
|
requireBudgetWithPermission(
|
||||||
|
permissionRepository,
|
||||||
|
session.userId,
|
||||||
|
transaction.budgetId,
|
||||||
|
Permission.WRITE
|
||||||
|
) {
|
||||||
|
return@delete
|
||||||
|
}
|
||||||
|
transactionRepository.delete(transaction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,10 +1,8 @@
|
||||||
package com.wbrawner.twigs.server
|
package com.wbrawner.twigs.server
|
||||||
|
|
||||||
import com.wbrawner.twigs.budgetRoutes
|
import com.wbrawner.twigs.*
|
||||||
import com.wbrawner.twigs.categoryRoutes
|
import com.wbrawner.twigs.model.Transaction
|
||||||
import com.wbrawner.twigs.storage.*
|
import com.wbrawner.twigs.storage.*
|
||||||
import com.wbrawner.twigs.twoWeeksFromNow
|
|
||||||
import com.wbrawner.twigs.userRoutes
|
|
||||||
import io.ktor.application.*
|
import io.ktor.application.*
|
||||||
import io.ktor.auth.*
|
import io.ktor.auth.*
|
||||||
import io.ktor.sessions.*
|
import io.ktor.sessions.*
|
||||||
|
@ -23,6 +21,7 @@ fun Application.module(
|
||||||
categoryRepository: CategoryRepository,
|
categoryRepository: CategoryRepository,
|
||||||
permissionRepository: PermissionRepository,
|
permissionRepository: PermissionRepository,
|
||||||
sessionRepository: SessionRepository,
|
sessionRepository: SessionRepository,
|
||||||
|
transactionRepository: TransactionRepository,
|
||||||
userRepository: UserRepository
|
userRepository: UserRepository
|
||||||
) {
|
) {
|
||||||
install(Sessions) {
|
install(Sessions) {
|
||||||
|
@ -43,6 +42,7 @@ fun Application.module(
|
||||||
}
|
}
|
||||||
budgetRoutes(budgetRepository, permissionRepository)
|
budgetRoutes(budgetRepository, permissionRepository)
|
||||||
categoryRoutes(categoryRepository, permissionRepository)
|
categoryRoutes(categoryRepository, permissionRepository)
|
||||||
|
transactionRoutes(transactionRepository, permissionRepository)
|
||||||
userRoutes(permissionRepository, sessionRepository, userRepository)
|
userRoutes(permissionRepository, sessionRepository, userRepository)
|
||||||
launch {
|
launch {
|
||||||
while (currentCoroutineContext().isActive) {
|
while (currentCoroutineContext().isActive) {
|
||||||
|
|
|
@ -1,209 +0,0 @@
|
||||||
package com.wbrawner.twigs.server.transaction
|
|
||||||
|
|
||||||
import com.wbrawner.twigs.ErrorResponse
|
|
||||||
import com.wbrawner.twigs.server.category.Category
|
|
||||||
import com.wbrawner.twigs.server.category.CategoryRepository
|
|
||||||
import com.wbrawner.twigs.server.currentUser
|
|
||||||
import com.wbrawner.twigs.server.endOfMonth
|
|
||||||
import com.wbrawner.twigs.server.firstOfMonth
|
|
||||||
import com.wbrawner.twigs.server.permission.Permission
|
|
||||||
import com.wbrawner.twigs.server.permission.UserPermission
|
|
||||||
import com.wbrawner.twigs.server.permission.UserPermissionRepository
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import org.springframework.data.domain.PageRequest
|
|
||||||
import org.springframework.data.domain.Sort
|
|
||||||
import org.springframework.http.HttpStatus
|
|
||||||
import org.springframework.http.MediaType
|
|
||||||
import org.springframework.http.ResponseEntity
|
|
||||||
import org.springframework.web.bind.annotation.*
|
|
||||||
import java.time.Instant
|
|
||||||
import java.util.stream.Collectors
|
|
||||||
import javax.transaction.Transactional
|
|
||||||
import kotlin.math.min
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping(path = ["/transactions"])
|
|
||||||
@Transactional
|
|
||||||
open class TransactionController(
|
|
||||||
private val categoryRepository: CategoryRepository,
|
|
||||||
private val transactionRepository: TransactionRepository,
|
|
||||||
private val userPermissionsRepository: UserPermissionRepository
|
|
||||||
) {
|
|
||||||
private val logger = LoggerFactory.getLogger(TransactionController::class.java)
|
|
||||||
@GetMapping(path = [""], produces = [MediaType.APPLICATION_JSON_VALUE])
|
|
||||||
open fun getTransactions(
|
|
||||||
@RequestParam(value = "categoryIds", required = false) categoryIds: List<String>?,
|
|
||||||
@RequestParam(value = "budgetIds", required = false) budgetIds: List<String>?,
|
|
||||||
@RequestParam(value = "from", required = false) from: String?,
|
|
||||||
@RequestParam(value = "to", required = false) to: String?,
|
|
||||||
@RequestParam(value = "count", required = false) count: Int?,
|
|
||||||
@RequestParam(value = "page", required = false) page: Int?,
|
|
||||||
@RequestParam(value = "sortBy", required = false) sortBy: String?,
|
|
||||||
@RequestParam(value = "sortOrder", required = false) sortOrder: Sort.Direction?
|
|
||||||
): ResponseEntity<List<TransactionResponse>> {
|
|
||||||
val userPermissions: List<UserPermission> = if (budgetIds != null && budgetIds.isNotEmpty()) {
|
|
||||||
userPermissionsRepository.findAllByUserAndBudget_IdIn(
|
|
||||||
currentUser,
|
|
||||||
budgetIds,
|
|
||||||
PageRequest.of(page ?: 0, count ?: 1000)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
userPermissionsRepository.findAllByUser(currentUser, null)
|
|
||||||
}
|
|
||||||
val budgets = userPermissions.stream()
|
|
||||||
.map { obj: UserPermission -> obj.budget }
|
|
||||||
.collect(Collectors.toList())
|
|
||||||
var categories: List<Category?>? = null
|
|
||||||
if (categoryIds != null && categoryIds.isNotEmpty()) {
|
|
||||||
categories = categoryRepository.findAllByBudgetInAndIdIn(budgets, categoryIds, null)
|
|
||||||
}
|
|
||||||
val pageRequest = PageRequest.of(
|
|
||||||
min(0, if (page != null) page - 1 else 0),
|
|
||||||
count ?: 1000,
|
|
||||||
sortOrder ?: Sort.Direction.DESC,
|
|
||||||
sortBy ?: "date"
|
|
||||||
)
|
|
||||||
val fromInstant: Instant = try {
|
|
||||||
Instant.parse(from)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
if (e !is NullPointerException) logger.error("Failed to parse '$from' to Instant for 'from' parameter", e)
|
|
||||||
firstOfMonth.toInstant()
|
|
||||||
}
|
|
||||||
val toInstant: Instant = try {
|
|
||||||
Instant.parse(to)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
if (e !is NullPointerException) logger.error("Failed to parse '$to' to Instant for 'to' parameter", e)
|
|
||||||
endOfMonth.toInstant()
|
|
||||||
}
|
|
||||||
val query = if (categories == null) {
|
|
||||||
transactionRepository.findAllByBudgetInAndDateGreaterThanAndDateLessThan(
|
|
||||||
budgets,
|
|
||||||
fromInstant,
|
|
||||||
toInstant,
|
|
||||||
pageRequest
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
transactionRepository.findAllByBudgetInAndCategoryInAndDateGreaterThanAndDateLessThan(
|
|
||||||
budgets,
|
|
||||||
categories,
|
|
||||||
fromInstant,
|
|
||||||
toInstant,
|
|
||||||
pageRequest
|
|
||||||
)
|
|
||||||
}
|
|
||||||
val transactions = query.map { transaction: Transaction -> TransactionResponse(transaction) }
|
|
||||||
return ResponseEntity.ok(transactions)
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping(path = ["/{id}"], produces = [MediaType.APPLICATION_JSON_VALUE])
|
|
||||||
open fun getTransaction(@PathVariable id: String?): ResponseEntity<TransactionResponse> {
|
|
||||||
val budgets = userPermissionsRepository.findAllByUser(currentUser, null)
|
|
||||||
.stream()
|
|
||||||
.map { obj: UserPermission -> obj.budget }
|
|
||||||
.collect(Collectors.toList())
|
|
||||||
val transaction = transactionRepository.findByIdAndBudgetIn(id, budgets).orElse(null)
|
|
||||||
?: return ResponseEntity.notFound().build()
|
|
||||||
return ResponseEntity.ok(TransactionResponse(transaction))
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping(
|
|
||||||
path = [""],
|
|
||||||
consumes = [MediaType.APPLICATION_JSON_VALUE],
|
|
||||||
produces = [MediaType.APPLICATION_JSON_VALUE]
|
|
||||||
)
|
|
||||||
open fun newTransaction(@RequestBody request: NewTransactionRequest): ResponseEntity<Any> {
|
|
||||||
val userResponse = userPermissionsRepository.findByUserAndBudget_Id(currentUser, request.budgetId)
|
|
||||||
.orElse(null) ?: return ResponseEntity.badRequest().body(ErrorResponse("Invalid budget ID"))
|
|
||||||
if (userResponse.permission.isNotAtLeast(Permission.WRITE)) {
|
|
||||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).build()
|
|
||||||
}
|
|
||||||
val budget = userResponse.budget
|
|
||||||
var category: Category? = null
|
|
||||||
if (request.categoryId != null) {
|
|
||||||
category = categoryRepository.findByBudgetAndId(budget, request.categoryId).orElse(null)
|
|
||||||
}
|
|
||||||
return ResponseEntity.ok(
|
|
||||||
TransactionResponse(
|
|
||||||
transactionRepository.save(
|
|
||||||
Transaction(
|
|
||||||
title = request.title,
|
|
||||||
description = request.description,
|
|
||||||
date = Instant.parse(request.date),
|
|
||||||
amount = request.amount,
|
|
||||||
category = category,
|
|
||||||
expense = request.expense,
|
|
||||||
createdBy = currentUser,
|
|
||||||
budget = budget
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@PutMapping(
|
|
||||||
path = ["/{id}"],
|
|
||||||
consumes = [MediaType.APPLICATION_JSON_VALUE],
|
|
||||||
produces = [MediaType.APPLICATION_JSON_VALUE]
|
|
||||||
)
|
|
||||||
open fun updateTransaction(
|
|
||||||
@PathVariable id: String,
|
|
||||||
@RequestBody request: UpdateTransactionRequest
|
|
||||||
): ResponseEntity<Any> {
|
|
||||||
val transaction = transactionRepository.findById(id).orElse(null)
|
|
||||||
?: return ResponseEntity.notFound().build()
|
|
||||||
userPermissionsRepository.findByUserAndBudget_Id(
|
|
||||||
currentUser,
|
|
||||||
transaction.budget!!.id
|
|
||||||
).orElse(null)
|
|
||||||
?: return ResponseEntity.notFound().build()
|
|
||||||
if (request.title != null) {
|
|
||||||
transaction.title = request.title
|
|
||||||
}
|
|
||||||
if (request.description != null) {
|
|
||||||
transaction.description = request.description
|
|
||||||
}
|
|
||||||
if (request.date != null) {
|
|
||||||
transaction.date = Instant.parse(request.date)
|
|
||||||
}
|
|
||||||
if (request.amount != null) {
|
|
||||||
transaction.amount = request.amount
|
|
||||||
}
|
|
||||||
if (request.expense != null) {
|
|
||||||
transaction.expense = request.expense
|
|
||||||
}
|
|
||||||
if (request.budgetId != null) {
|
|
||||||
val newUserPermission =
|
|
||||||
userPermissionsRepository.findByUserAndBudget_Id(currentUser, request.budgetId).orElse(null)
|
|
||||||
if (newUserPermission == null || newUserPermission.permission.isNotAtLeast(Permission.WRITE)) {
|
|
||||||
return ResponseEntity
|
|
||||||
.badRequest()
|
|
||||||
.body(ErrorResponse("Invalid budget"))
|
|
||||||
}
|
|
||||||
transaction.budget = newUserPermission.budget
|
|
||||||
}
|
|
||||||
if (request.categoryId != null) {
|
|
||||||
val category = categoryRepository.findByBudgetAndId(transaction.budget, request.categoryId).orElse(null)
|
|
||||||
?: return ResponseEntity
|
|
||||||
.badRequest()
|
|
||||||
.body(ErrorResponse("Invalid category"))
|
|
||||||
transaction.category = category
|
|
||||||
}
|
|
||||||
return ResponseEntity.ok(TransactionResponse(transactionRepository.save(transaction)))
|
|
||||||
}
|
|
||||||
|
|
||||||
@DeleteMapping(path = ["/{id}"], produces = [MediaType.TEXT_PLAIN_VALUE])
|
|
||||||
open fun deleteTransaction(@PathVariable id: String): ResponseEntity<Void> {
|
|
||||||
val transaction = transactionRepository.findById(id).orElse(null) ?: return ResponseEntity.notFound().build()
|
|
||||||
// Check that the transaction belongs to an budget that the user has access to before deleting it
|
|
||||||
val userPermission = userPermissionsRepository.findByUserAndBudget_Id(
|
|
||||||
currentUser,
|
|
||||||
transaction.budget!!.id
|
|
||||||
).orElse(null)
|
|
||||||
?: return ResponseEntity.notFound().build()
|
|
||||||
if (userPermission.permission.isNotAtLeast(Permission.WRITE)) {
|
|
||||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).build()
|
|
||||||
}
|
|
||||||
transactionRepository.delete(transaction)
|
|
||||||
return ResponseEntity.ok().build()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,41 +0,0 @@
|
||||||
package com.wbrawner.twigs.server.transaction
|
|
||||||
|
|
||||||
import com.wbrawner.twigs.server.budget.Budget
|
|
||||||
import com.wbrawner.twigs.server.category.Category
|
|
||||||
import org.springframework.data.domain.Pageable
|
|
||||||
import org.springframework.data.jpa.repository.Query
|
|
||||||
import org.springframework.data.repository.PagingAndSortingRepository
|
|
||||||
import java.time.Instant
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
interface TransactionRepository : PagingAndSortingRepository<Transaction, String> {
|
|
||||||
fun findByIdAndBudgetIn(id: String?, budgets: List<Budget?>?): Optional<Transaction>
|
|
||||||
fun findAllByBudgetInAndCategoryInAndDateGreaterThanAndDateLessThan(
|
|
||||||
budgets: List<Budget?>?,
|
|
||||||
categories: List<Category?>?,
|
|
||||||
start: Instant?,
|
|
||||||
end: Instant?,
|
|
||||||
pageable: Pageable?
|
|
||||||
): List<Transaction>
|
|
||||||
|
|
||||||
fun findAllByBudgetInAndDateGreaterThanAndDateLessThan(
|
|
||||||
budgets: List<Budget?>?,
|
|
||||||
start: Instant?,
|
|
||||||
end: Instant?,
|
|
||||||
pageable: Pageable?
|
|
||||||
): List<Transaction>
|
|
||||||
|
|
||||||
fun findAllByBudgetAndCategory(budget: Budget?, category: Category?): List<Transaction>
|
|
||||||
|
|
||||||
@Query(
|
|
||||||
nativeQuery = true,
|
|
||||||
value = "SELECT (COALESCE((SELECT SUM(amount) from transaction WHERE Budget_id = :BudgetId AND expense = 0 AND date >= :from AND date <= :to), 0)) - (COALESCE((SELECT SUM(amount) from transaction WHERE Budget_id = :BudgetId AND expense = 1 AND date >= :from AND date <= :to), 0));"
|
|
||||||
)
|
|
||||||
fun sumBalanceByBudgetId(BudgetId: String?, from: Instant?, to: Instant?): Long
|
|
||||||
|
|
||||||
@Query(
|
|
||||||
nativeQuery = true,
|
|
||||||
value = "SELECT (COALESCE((SELECT SUM(amount) from transaction WHERE category_id = :categoryId AND expense = 0 AND date > :start), 0)) - (COALESCE((SELECT SUM(amount) from transaction WHERE category_id = :categoryId AND expense = 1 AND date > :start), 0));"
|
|
||||||
)
|
|
||||||
fun sumBalanceByCategoryId(categoryId: String?, start: Date?): Long
|
|
||||||
}
|
|
|
@ -11,7 +11,7 @@ private val CALENDAR_FIELDS = intArrayOf(
|
||||||
)
|
)
|
||||||
|
|
||||||
val firstOfMonth: Date
|
val firstOfMonth: Date
|
||||||
get() = GregorianCalendar().run {
|
get() = GregorianCalendar(TimeZone.getTimeZone("UTC")).run {
|
||||||
for (calField in CALENDAR_FIELDS) {
|
for (calField in CALENDAR_FIELDS) {
|
||||||
set(calField, getActualMinimum(calField))
|
set(calField, getActualMinimum(calField))
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ val firstOfMonth: Date
|
||||||
}
|
}
|
||||||
|
|
||||||
val endOfMonth: Date
|
val endOfMonth: Date
|
||||||
get() = GregorianCalendar().run {
|
get() = GregorianCalendar(TimeZone.getTimeZone("UTC")).run {
|
||||||
for (calField in CALENDAR_FIELDS) {
|
for (calField in CALENDAR_FIELDS) {
|
||||||
set(calField, getActualMaximum(calField))
|
set(calField, getActualMaximum(calField))
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ val endOfMonth: Date
|
||||||
}
|
}
|
||||||
|
|
||||||
val twoWeeksFromNow: Date
|
val twoWeeksFromNow: Date
|
||||||
get() = GregorianCalendar().run {
|
get() = GregorianCalendar(TimeZone.getTimeZone("UTC")).run {
|
||||||
add(Calendar.DATE, 14)
|
add(Calendar.DATE, 14)
|
||||||
time
|
time
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,8 +9,8 @@ data class Transaction(
|
||||||
val description: String? = null,
|
val description: String? = null,
|
||||||
val date: Instant? = null,
|
val date: Instant? = null,
|
||||||
val amount: Long? = null,
|
val amount: Long? = null,
|
||||||
val category: Category? = null,
|
val categoryId: String? = null,
|
||||||
val expense: Boolean? = null,
|
val expense: Boolean? = null,
|
||||||
val createdBy: User? = null,
|
val createdBy: String,
|
||||||
val budget: Budget? = null
|
val budgetId: String
|
||||||
)
|
)
|
||||||
|
|
|
@ -11,4 +11,3 @@ data class Session(
|
||||||
val token: String = randomString(255),
|
val token: String = randomString(255),
|
||||||
var expiration: Date = twoWeeksFromNow
|
var expiration: Date = twoWeeksFromNow
|
||||||
) : Principal
|
) : Principal
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
package com.wbrawner.twigs.storage
|
||||||
|
|
||||||
|
import com.wbrawner.twigs.model.Transaction
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
|
interface TransactionRepository : Repository<Transaction> {
|
||||||
|
fun findAll(
|
||||||
|
ids: List<String>? = null,
|
||||||
|
budgetIds: List<String>? = null,
|
||||||
|
categoryIds: List<String>? = null,
|
||||||
|
expense: Boolean? = null,
|
||||||
|
from: Instant? = null,
|
||||||
|
to: Instant? = null,
|
||||||
|
): List<Transaction>
|
||||||
|
|
||||||
|
fun sumByBudget(budgetId: String, from: Instant, to: Instant): Long
|
||||||
|
|
||||||
|
fun sumByCategory(categoryId: String, from: Instant, to: Instant): Long
|
||||||
|
}
|
Loading…
Reference in a new issue