From 9fc3d1ac1ce58a6f13e2eb22cdb51bbb06086c21 Mon Sep 17 00:00:00 2001 From: William Brawner Date: Thu, 5 Aug 2021 21:00:23 -0600 Subject: [PATCH] Add Transaction routes --- .../kotlin/com/wbrawner/twigs/ApiUtils.kt | 3 + .../kotlin/com/wbrawner/twigs/BudgetRoutes.kt | 2 +- .../com/wbrawner/twigs/CategoryRoutes.kt | 2 +- .../com/wbrawner/twigs/TransactionApi.kt | 41 ++-- .../com/wbrawner/twigs/TransactionRoutes.kt | 162 ++++++++++++++ .../com/wbrawner/twigs/server/Application.kt | 8 +- .../transaction/TransactionController.kt | 209 ------------------ .../transaction/TransactionRepository.kt | 41 ---- .../main/kotlin/com/wbrawner/twigs/Utils.kt | 6 +- .../com/wbrawner/twigs/model/Transaction.kt | 6 +- .../com/wbrawner/twigs/storage/Session.kt | 1 - .../twigs/storage/TransactionRepository.kt | 19 ++ 12 files changed, 212 insertions(+), 288 deletions(-) create mode 100644 api/src/main/kotlin/com/wbrawner/twigs/TransactionRoutes.kt delete mode 100644 app/src/main/kotlin/com/wbrawner/twigs/server/transaction/TransactionController.kt delete mode 100644 app/src/main/kotlin/com/wbrawner/twigs/server/transaction/TransactionRepository.kt create mode 100644 storage/src/main/kotlin/com/wbrawner/twigs/storage/TransactionRepository.kt diff --git a/api/src/main/kotlin/com/wbrawner/twigs/ApiUtils.kt b/api/src/main/kotlin/com/wbrawner/twigs/ApiUtils.kt index c73667f..42e280a 100644 --- a/api/src/main/kotlin/com/wbrawner/twigs/ApiUtils.kt +++ b/api/src/main/kotlin/com/wbrawner/twigs/ApiUtils.kt @@ -10,6 +10,7 @@ import io.ktor.auth.* import io.ktor.http.* import io.ktor.response.* import io.ktor.util.pipeline.* +import java.time.Instant suspend inline fun PipelineContext.requireBudgetWithPermission( permissionRepository: PermissionRepository, @@ -56,3 +57,5 @@ suspend inline fun PipelineContext.errorResponse( call.respond(httpStatusCode, ErrorResponse(message)) }?: call.respond(httpStatusCode) } + +fun String.toInstant(): Instant = Instant.parse(this) \ No newline at end of file diff --git a/api/src/main/kotlin/com/wbrawner/twigs/BudgetRoutes.kt b/api/src/main/kotlin/com/wbrawner/twigs/BudgetRoutes.kt index c9ca460..b9d1fd1 100644 --- a/api/src/main/kotlin/com/wbrawner/twigs/BudgetRoutes.kt +++ b/api/src/main/kotlin/com/wbrawner/twigs/BudgetRoutes.kt @@ -36,7 +36,7 @@ fun Application.budgetRoutes( } } - post("/{id}") { + post("/") { val session = call.principal()!! val request = call.receive() if (request.name.isNullOrBlank()) { diff --git a/api/src/main/kotlin/com/wbrawner/twigs/CategoryRoutes.kt b/api/src/main/kotlin/com/wbrawner/twigs/CategoryRoutes.kt index 96bec11..af55259 100644 --- a/api/src/main/kotlin/com/wbrawner/twigs/CategoryRoutes.kt +++ b/api/src/main/kotlin/com/wbrawner/twigs/CategoryRoutes.kt @@ -41,7 +41,7 @@ fun Application.categoryRoutes( ).map { CategoryResponse(it) }) } - post("/{id}") { + post("/") { val session = call.principal()!! val request = call.receive() if (request.title.isNullOrBlank()) { diff --git a/api/src/main/kotlin/com/wbrawner/twigs/TransactionApi.kt b/api/src/main/kotlin/com/wbrawner/twigs/TransactionApi.kt index f1fb3ec..786f687 100644 --- a/api/src/main/kotlin/com/wbrawner/twigs/TransactionApi.kt +++ b/api/src/main/kotlin/com/wbrawner/twigs/TransactionApi.kt @@ -2,17 +2,7 @@ package com.wbrawner.twigs import com.wbrawner.twigs.model.Transaction -data class NewTransactionRequest( - 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( +data class TransactionRequest( val title: String? = null, val description: String? = null, val date: String? = null, @@ -20,7 +10,6 @@ data class UpdateTransactionRequest( val categoryId: String? = null, val expense: Boolean? = null, val budgetId: String? = null, - val createdBy: String? = null ) data class TransactionResponse( @@ -33,16 +22,18 @@ data class TransactionResponse( val budgetId: String, val categoryId: String?, val createdBy: String -) { - constructor(transaction: Transaction) : this( - transaction.id, - transaction.title, - transaction.description, - transaction.date.toString(), - transaction.amount, - transaction.expense, - transaction.budget!!.id, - transaction.category?.id, - transaction.createdBy!!.id - ) -} \ No newline at end of file +) + +data class BalanceResponse(val balance: Long) + +fun Transaction.asResponse(): TransactionResponse = TransactionResponse( + id = id, + title = title, + description = description, + date = date.toString(), + amount = amount, + expense = expense, + budgetId = budgetId, + categoryId = categoryId, + createdBy = createdBy +) \ No newline at end of file diff --git a/api/src/main/kotlin/com/wbrawner/twigs/TransactionRoutes.kt b/api/src/main/kotlin/com/wbrawner/twigs/TransactionRoutes.kt new file mode 100644 index 0000000..95fa7c1 --- /dev/null +++ b/api/src/main/kotlin/com/wbrawner/twigs/TransactionRoutes.kt @@ -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()!! + 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()!! + 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()!! + val request = call.receive() + 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()!! + val request = call.receive() + 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()!! + 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) + } + } + } + } +} diff --git a/app/src/main/kotlin/com/wbrawner/twigs/server/Application.kt b/app/src/main/kotlin/com/wbrawner/twigs/server/Application.kt index 03375d0..4b91504 100644 --- a/app/src/main/kotlin/com/wbrawner/twigs/server/Application.kt +++ b/app/src/main/kotlin/com/wbrawner/twigs/server/Application.kt @@ -1,10 +1,8 @@ package com.wbrawner.twigs.server -import com.wbrawner.twigs.budgetRoutes -import com.wbrawner.twigs.categoryRoutes +import com.wbrawner.twigs.* +import com.wbrawner.twigs.model.Transaction import com.wbrawner.twigs.storage.* -import com.wbrawner.twigs.twoWeeksFromNow -import com.wbrawner.twigs.userRoutes import io.ktor.application.* import io.ktor.auth.* import io.ktor.sessions.* @@ -23,6 +21,7 @@ fun Application.module( categoryRepository: CategoryRepository, permissionRepository: PermissionRepository, sessionRepository: SessionRepository, + transactionRepository: TransactionRepository, userRepository: UserRepository ) { install(Sessions) { @@ -43,6 +42,7 @@ fun Application.module( } budgetRoutes(budgetRepository, permissionRepository) categoryRoutes(categoryRepository, permissionRepository) + transactionRoutes(transactionRepository, permissionRepository) userRoutes(permissionRepository, sessionRepository, userRepository) launch { while (currentCoroutineContext().isActive) { diff --git a/app/src/main/kotlin/com/wbrawner/twigs/server/transaction/TransactionController.kt b/app/src/main/kotlin/com/wbrawner/twigs/server/transaction/TransactionController.kt deleted file mode 100644 index 081e40e..0000000 --- a/app/src/main/kotlin/com/wbrawner/twigs/server/transaction/TransactionController.kt +++ /dev/null @@ -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?, - @RequestParam(value = "budgetIds", required = false) budgetIds: List?, - @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> { - val userPermissions: List = 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? = 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 { - 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 { - 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 { - 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 { - 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() - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/wbrawner/twigs/server/transaction/TransactionRepository.kt b/app/src/main/kotlin/com/wbrawner/twigs/server/transaction/TransactionRepository.kt deleted file mode 100644 index 6d04868..0000000 --- a/app/src/main/kotlin/com/wbrawner/twigs/server/transaction/TransactionRepository.kt +++ /dev/null @@ -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 { - fun findByIdAndBudgetIn(id: String?, budgets: List?): Optional - fun findAllByBudgetInAndCategoryInAndDateGreaterThanAndDateLessThan( - budgets: List?, - categories: List?, - start: Instant?, - end: Instant?, - pageable: Pageable? - ): List - - fun findAllByBudgetInAndDateGreaterThanAndDateLessThan( - budgets: List?, - start: Instant?, - end: Instant?, - pageable: Pageable? - ): List - - fun findAllByBudgetAndCategory(budget: Budget?, category: Category?): List - - @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 -} \ No newline at end of file diff --git a/core/src/main/kotlin/com/wbrawner/twigs/Utils.kt b/core/src/main/kotlin/com/wbrawner/twigs/Utils.kt index 7f72d8a..fca5151 100644 --- a/core/src/main/kotlin/com/wbrawner/twigs/Utils.kt +++ b/core/src/main/kotlin/com/wbrawner/twigs/Utils.kt @@ -11,7 +11,7 @@ private val CALENDAR_FIELDS = intArrayOf( ) val firstOfMonth: Date - get() = GregorianCalendar().run { + get() = GregorianCalendar(TimeZone.getTimeZone("UTC")).run { for (calField in CALENDAR_FIELDS) { set(calField, getActualMinimum(calField)) } @@ -19,7 +19,7 @@ val firstOfMonth: Date } val endOfMonth: Date - get() = GregorianCalendar().run { + get() = GregorianCalendar(TimeZone.getTimeZone("UTC")).run { for (calField in CALENDAR_FIELDS) { set(calField, getActualMaximum(calField)) } @@ -27,7 +27,7 @@ val endOfMonth: Date } val twoWeeksFromNow: Date - get() = GregorianCalendar().run { + get() = GregorianCalendar(TimeZone.getTimeZone("UTC")).run { add(Calendar.DATE, 14) time } diff --git a/core/src/main/kotlin/com/wbrawner/twigs/model/Transaction.kt b/core/src/main/kotlin/com/wbrawner/twigs/model/Transaction.kt index 592ae20..3a17b43 100644 --- a/core/src/main/kotlin/com/wbrawner/twigs/model/Transaction.kt +++ b/core/src/main/kotlin/com/wbrawner/twigs/model/Transaction.kt @@ -9,8 +9,8 @@ data class Transaction( val description: String? = null, val date: Instant? = null, val amount: Long? = null, - val category: Category? = null, + val categoryId: String? = null, val expense: Boolean? = null, - val createdBy: User? = null, - val budget: Budget? = null + val createdBy: String, + val budgetId: String ) diff --git a/storage/src/main/kotlin/com/wbrawner/twigs/storage/Session.kt b/storage/src/main/kotlin/com/wbrawner/twigs/storage/Session.kt index 1dbbf06..f4593ef 100644 --- a/storage/src/main/kotlin/com/wbrawner/twigs/storage/Session.kt +++ b/storage/src/main/kotlin/com/wbrawner/twigs/storage/Session.kt @@ -11,4 +11,3 @@ data class Session( val token: String = randomString(255), var expiration: Date = twoWeeksFromNow ) : Principal - diff --git a/storage/src/main/kotlin/com/wbrawner/twigs/storage/TransactionRepository.kt b/storage/src/main/kotlin/com/wbrawner/twigs/storage/TransactionRepository.kt new file mode 100644 index 0000000..3d1b985 --- /dev/null +++ b/storage/src/main/kotlin/com/wbrawner/twigs/storage/TransactionRepository.kt @@ -0,0 +1,19 @@ +package com.wbrawner.twigs.storage + +import com.wbrawner.twigs.model.Transaction +import java.time.Instant + +interface TransactionRepository : Repository { + fun findAll( + ids: List? = null, + budgetIds: List? = null, + categoryIds: List? = null, + expense: Boolean? = null, + from: Instant? = null, + to: Instant? = null, + ): List + + fun sumByBudget(budgetId: String, from: Instant, to: Instant): Long + + fun sumByCategory(categoryId: String, from: Instant, to: Instant): Long +} \ No newline at end of file