From a9611eee235db038002a057e2cacfb580661dd23 Mon Sep 17 00:00:00 2001 From: William Brawner Date: Wed, 15 Sep 2021 14:03:50 -0600 Subject: [PATCH] Implement recurring transactions --- .../kotlin/com/wbrawner/twigs/ApiUtils.kt | 9 +- .../twigs/RecurringTransactionRoutes.kt | 182 +++++++++--------- .../twigs/RecurringTransactionsApi.kt | 11 +- app/build.gradle.kts | 2 +- .../com/wbrawner/twigs/server/Application.kt | 9 +- .../main/kotlin/com/wbrawner/twigs/Utils.kt | 15 +- .../twigs/model/RecurringTransaction.kt | 42 ++-- .../db/JdbcRecurringTransactionRepository.kt | 58 ++++++ .../com/wbrawner/twigs/db/JdbcRepository.kt | 4 +- .../twigs/db/JdbcSessionRepository.kt | 2 +- .../twigs/db/JdbcTransactionRepository.kt | 3 +- .../storage/RecurringTransactionRepository.kt | 1 + .../FakeRecurringTransactionsRepository.kt | 6 +- 13 files changed, 201 insertions(+), 143 deletions(-) create mode 100644 db/src/main/kotlin/com/wbrawner/twigs/db/JdbcRecurringTransactionRepository.kt diff --git a/api/src/main/kotlin/com/wbrawner/twigs/ApiUtils.kt b/api/src/main/kotlin/com/wbrawner/twigs/ApiUtils.kt index a364dd4..fff5f72 100644 --- a/api/src/main/kotlin/com/wbrawner/twigs/ApiUtils.kt +++ b/api/src/main/kotlin/com/wbrawner/twigs/ApiUtils.kt @@ -10,15 +10,18 @@ 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, userId: String, - budgetId: String, + budgetId: String?, permission: Permission, otherwise: () -> Unit ) { + if (budgetId.isNullOrBlank()) { + errorResponse(HttpStatusCode.BadRequest, "budgetId is required") + return + } permissionRepository.findAll( userId = userId, budgetIds = listOf(budgetId) @@ -57,5 +60,3 @@ 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/RecurringTransactionRoutes.kt b/api/src/main/kotlin/com/wbrawner/twigs/RecurringTransactionRoutes.kt index 08582a7..67745fc 100644 --- a/api/src/main/kotlin/com/wbrawner/twigs/RecurringTransactionRoutes.kt +++ b/api/src/main/kotlin/com/wbrawner/twigs/RecurringTransactionRoutes.kt @@ -1,82 +1,80 @@ package com.wbrawner.twigs import com.wbrawner.twigs.model.Permission +import com.wbrawner.twigs.model.RecurringTransaction import com.wbrawner.twigs.model.Session -import com.wbrawner.twigs.model.Transaction import com.wbrawner.twigs.storage.PermissionRepository -import com.wbrawner.twigs.storage.TransactionRepository +import com.wbrawner.twigs.storage.RecurringTransactionRepository 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 io.ktor.util.pipeline.* import java.time.Instant fun Application.recurringTransactionRoutes( - transactionRepository: TransactionRepository, + recurringTransactionRepository: RecurringTransactionRepository, permissionRepository: PermissionRepository ) { + suspend fun PipelineContext.recurringTransactionAfterPermissionCheck( + id: String?, + userId: String, + success: suspend (RecurringTransaction) -> Unit + ) { + if (id.isNullOrBlank()) { + errorResponse(HttpStatusCode.BadRequest, "id is required") + return + } + val recurringTransaction = recurringTransactionRepository.findAll(ids = listOf(id)).firstOrNull() + ?: run { + errorResponse() + return + } + requireBudgetWithPermission( + permissionRepository, + userId, + recurringTransaction.budgetId, + Permission.WRITE + ) { + application.log.info("No permissions on budget ${recurringTransaction.budgetId}.") + return + } + success(recurringTransaction) + } + routing { route("/api/recurringtransactions") { authenticate(optional = false) { get { val session = call.principal()!! + val budgetId = call.request.queryParameters["budgetId"] + requireBudgetWithPermission( + permissionRepository, + session.userId, + budgetId, + Permission.WRITE + ) { + return@get + } 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() }) + recurringTransactionRepository.findAll( + budgetId = budgetId!! + ).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 - val to = call.request.queryParameters["to"]?.toInstant() ?: endOfMonth - 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 + recurringTransactionAfterPermissionCheck(call.parameters["id"]!!, session.userId) { + call.respond(it.asResponse()) } - call.respond(BalanceResponse(balance)) } post { val session = call.principal()!! - val request = call.receive() + val request = call.receive() if (request.title.isNullOrBlank()) { errorResponse(HttpStatusCode.BadRequest, "Title cannot be null or empty") return@post @@ -94,8 +92,8 @@ fun Application.recurringTransactionRoutes( return@post } call.respond( - transactionRepository.save( - Transaction( + recurringTransactionRepository.save( + RecurringTransaction( title = request.title, description = request.description, amount = request.amount ?: 0L, @@ -103,7 +101,9 @@ fun Application.recurringTransactionRoutes( budgetId = request.budgetId, categoryId = request.categoryId, createdBy = session.userId, - date = request.date?.let { Instant.parse(it) } ?: Instant.now() + start = request.start?.toInstant() ?: Instant.now(), + finish = request.finish?.toInstant(), + frequency = request.frequency.asFrequency() ) ).asResponse() ) @@ -111,59 +111,49 @@ fun Application.recurringTransactionRoutes( put("/{id}") { val session = call.principal()!! - val request = call.receive() - val transaction = transactionRepository.findAll(ids = call.parameters.getAll("id")) - .firstOrNull() - ?: run { - errorResponse() - return@put + val request = call.receive() + recurringTransactionAfterPermissionCheck( + call.parameters["id"]!!, + session.userId + ) { recurringTransaction -> + if (request.budgetId != recurringTransaction.budgetId) { + requireBudgetWithPermission( + permissionRepository, + session.userId, + request.budgetId, + Permission.WRITE + ) { + return@recurringTransactionAfterPermissionCheck + } } - requireBudgetWithPermission( - permissionRepository, - session.userId, - transaction.budgetId, - Permission.WRITE - ) { - return@put + call.respond( + recurringTransactionRepository.save( + recurringTransaction.copy( + title = request.title ?: recurringTransaction.title, + description = request.description ?: recurringTransaction.description, + amount = request.amount ?: recurringTransaction.amount, + expense = request.expense ?: recurringTransaction.expense, + categoryId = request.categoryId ?: recurringTransaction.categoryId, + budgetId = request.budgetId ?: recurringTransaction.budgetId, + start = request.start?.toInstant() ?: recurringTransaction.start, + finish = request.finish?.toInstant() ?: recurringTransaction.finish, + frequency = request.frequency.asFrequency() + ) + ).asResponse() + ) } - call.respond( - transactionRepository.save( - transaction.copy( - 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 + recurringTransactionAfterPermissionCheck(call.parameters["id"]!!, session.userId) { + val response = if (recurringTransactionRepository.delete(it)) { + HttpStatusCode.NoContent + } else { + HttpStatusCode.InternalServerError } - requireBudgetWithPermission( - permissionRepository, - session.userId, - transaction.budgetId, - Permission.WRITE - ) { - return@delete + call.respond(response) } - val response = if (transactionRepository.delete(transaction)) { - HttpStatusCode.NoContent - } else { - HttpStatusCode.InternalServerError - } - call.respond(response) } } } diff --git a/api/src/main/kotlin/com/wbrawner/twigs/RecurringTransactionsApi.kt b/api/src/main/kotlin/com/wbrawner/twigs/RecurringTransactionsApi.kt index 2f2e8b0..3d89b64 100644 --- a/api/src/main/kotlin/com/wbrawner/twigs/RecurringTransactionsApi.kt +++ b/api/src/main/kotlin/com/wbrawner/twigs/RecurringTransactionsApi.kt @@ -11,6 +11,9 @@ data class RecurringTransactionRequest( val categoryId: String? = null, val expense: Boolean? = null, val budgetId: String? = null, + val frequency: String, + val start: String? = null, + val finish: String? = null, ) @Serializable @@ -18,7 +21,9 @@ data class RecurringTransactionResponse( val id: String, val title: String?, val description: String?, -// val frequency: FrequencyResponse, + val frequency: String, + val start: String, + val finish: String? = null, val amount: Long?, val expense: Boolean?, val budgetId: String, @@ -30,7 +35,9 @@ fun RecurringTransaction.asResponse(): RecurringTransactionResponse = RecurringT id = id, title = title, description = description, -// frequency = date.toString(), + frequency = frequency.toString(), + start = start.toString(), + finish = finish.toString(), amount = amount, expense = expense, budgetId = budgetId, diff --git a/app/build.gradle.kts b/app/build.gradle.kts index aaf3f90..8fc98c8 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -91,7 +91,7 @@ tasks.register("package") { tasks.register("publish") { dependsOn(":app:package") doLast { - var command = listOf("caprover", "deploy", "-t", "build/${tarFile.name}", "-n", "wbrawner", "-a", "twigs") + var command = listOf("caprover", "deploy", "-t", "build/${tarFile.name}", "-n", "wbrawner", "-a", "twigs-dev") command = if (System.getProperty("os.name").toLowerCase(Locale.ROOT).contains("windows")) { listOf("powershell", "-Command") + command } else { 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 ce82ab6..d2647d9 100644 --- a/app/src/main/kotlin/com/wbrawner/twigs/server/Application.kt +++ b/app/src/main/kotlin/com/wbrawner/twigs/server/Application.kt @@ -24,7 +24,7 @@ import kotlin.time.ExperimentalTime fun main(args: Array): Unit = io.ktor.server.cio.EngineMain.main(args) -private const val DATABASE_VERSION = 1 +private const val DATABASE_VERSION = 2 @ExperimentalTime fun Application.module() { @@ -44,7 +44,7 @@ fun Application.module() { budgetRepository = JdbcBudgetRepository(it), categoryRepository = JdbcCategoryRepository(it), permissionRepository = JdbcPermissionRepository(it), -// recurringTransactionRepository = Fa, + recurringTransactionRepository = JdbcRecurringTransactionRepository(it), sessionRepository = JdbcSessionRepository(it), transactionRepository = JdbcTransactionRepository(it), userRepository = JdbcUserRepository(it) @@ -58,7 +58,7 @@ fun Application.moduleWithDependencies( budgetRepository: BudgetRepository, categoryRepository: CategoryRepository, permissionRepository: PermissionRepository, -// recurringTransactionRepository: RecurringTransactionRepository, + recurringTransactionRepository: RecurringTransactionRepository, sessionRepository: SessionRepository, transactionRepository: TransactionRepository, userRepository: UserRepository @@ -112,6 +112,7 @@ fun Application.moduleWithDependencies( } budgetRoutes(budgetRepository, permissionRepository) categoryRoutes(categoryRepository, permissionRepository) + recurringTransactionRoutes(recurringTransactionRepository, permissionRepository) transactionRoutes(transactionRepository, permissionRepository) userRoutes(permissionRepository, sessionRepository, userRepository) webRoutes() @@ -134,7 +135,7 @@ fun Application.moduleWithDependencies( } val jobs = listOf( SessionCleanupJob(sessionRepository), -// RecurringTransactionProcessingJob(recurringTransactionRepository, transactionRepository) + RecurringTransactionProcessingJob(recurringTransactionRepository, transactionRepository) ) while (currentCoroutineContext().isActive) { delay(Duration.hours(24)) diff --git a/core/src/main/kotlin/com/wbrawner/twigs/Utils.kt b/core/src/main/kotlin/com/wbrawner/twigs/Utils.kt index 45c6d82..866db44 100644 --- a/core/src/main/kotlin/com/wbrawner/twigs/Utils.kt +++ b/core/src/main/kotlin/com/wbrawner/twigs/Utils.kt @@ -1,15 +1,16 @@ package com.wbrawner.twigs import at.favre.lib.crypto.bcrypt.BCrypt +import com.wbrawner.twigs.model.Frequency import java.time.Instant import java.util.* private val CALENDAR_FIELDS = intArrayOf( - Calendar.MILLISECOND, - Calendar.SECOND, - Calendar.MINUTE, - Calendar.HOUR_OF_DAY, - Calendar.DATE + Calendar.MILLISECOND, + Calendar.SECOND, + Calendar.MINUTE, + Calendar.HOUR_OF_DAY, + Calendar.DATE ) val firstOfMonth: Instant @@ -46,3 +47,7 @@ fun randomString(length: Int = 32): String { lateinit var salt: String fun String.hash(): String = String(BCrypt.withDefaults().hash(10, salt.toByteArray(), this.toByteArray())) + +fun String.toInstant(): Instant = Instant.parse(this) + +fun String.asFrequency(): Frequency = Frequency.parse(this) diff --git a/core/src/main/kotlin/com/wbrawner/twigs/model/RecurringTransaction.kt b/core/src/main/kotlin/com/wbrawner/twigs/model/RecurringTransaction.kt index d7b00d8..da53688 100644 --- a/core/src/main/kotlin/com/wbrawner/twigs/model/RecurringTransaction.kt +++ b/core/src/main/kotlin/com/wbrawner/twigs/model/RecurringTransaction.kt @@ -12,7 +12,7 @@ data class RecurringTransaction( val description: String? = null, val frequency: Frequency, val start: Instant, - val end: Instant? = null, + val finish: Instant? = null, val amount: Long, val expense: Boolean, val createdBy: String, @@ -37,6 +37,8 @@ sealed class Frequency { abstract val time: Time data class Daily(override val count: Int, override val time: Time) : Frequency() { + override fun toString(): String = "D;$count;$time" + companion object { fun parse(s: String): Daily { require(s[0] == 'D') { "Invalid format for Daily: $s" } @@ -51,6 +53,7 @@ sealed class Frequency { } data class Weekly(override val count: Int, val daysOfWeek: Set, override val time: Time) : Frequency() { + override fun toString(): String = "W;$count;${daysOfWeek.joinToString(",")};$time" companion object { fun parse(s: String): Weekly { require(s[0] == 'W') { "Invalid format for Weekly: $s" } @@ -70,6 +73,7 @@ sealed class Frequency { val dayOfMonth: DayOfMonth, override val time: Time ) : Frequency() { + override fun toString(): String = "M;$count;$dayOfMonth;$time" companion object { fun parse(s: String): Monthly { require(s[0] == 'M') { "Invalid format for Monthly: $s" } @@ -85,13 +89,16 @@ sealed class Frequency { } data class Yearly(override val count: Int, val dayOfYear: MonthDay, override val time: Time) : Frequency() { + override fun toString(): String = "Y;$count;%02d-%02d;$time".format(dayOfYear.monthValue, dayOfYear.dayOfMonth) companion object { fun parse(s: String): Yearly { require(s[0] == 'Y') { "Invalid format for Yearly: $s" } return with(s.split(';')) { Yearly( get(1).toInt(), - MonthDay.parse(get(2)), + with(get(2).split("-")) { + MonthDay.of(get(0).toInt(), get(1).toInt()) + }, Time.parse(get(3)) ) } @@ -101,13 +108,6 @@ sealed class Frequency { fun instant(now: Instant): Instant = Instant.parse(now.toString().split("T")[0] + "T" + time.toString() + "Z") - override fun toString(): String = when (this) { - is Daily -> "D;$count;$time" - is Weekly -> "W;$count;${daysOfWeek.joinToString(",")};$time" - is Monthly -> "M;$count;$dayOfMonth;$time" - is Yearly -> "Y;$count;$dayOfYear;$time" - } - companion object { fun parse(s: String): Frequency = when (s[0]) { 'D' -> Daily.parse(s) @@ -119,7 +119,7 @@ sealed class Frequency { } } -data class Time(val hours: Int, val minutes: Int, val seconds: Int, val milliseconds: Int) { +data class Time(val hours: Int, val minutes: Int, val seconds: Int) { override fun toString(): String { val s = StringBuilder() if (hours < 10) { @@ -136,28 +136,18 @@ data class Time(val hours: Int, val minutes: Int, val seconds: Int, val millisec s.append("0") } s.append(seconds) - s.append(".") - if (milliseconds < 100) { - s.append("0") - } - if (milliseconds < 10) { - s.append("0") - } - s.append(milliseconds) return s.toString() } companion object { fun parse(s: String): Time { - require(s.length < 12) { "Invalid time format: $s. Time should be formatted as HH:mm:ss.SSS" } - require(s[3] == ':') { "Invalid time format: $s. Time should be formatted as HH:mm:ss.SSS" } - require(s[6] == ':') { "Invalid time format: $s. Time should be formatted as HH:mm:ss.SSS" } - require(s[9] == '.') { "Invalid time format: $s. Time should be formatted as HH:mm:ss.SSS" } + require(s.length < 9) { "Invalid time format: $s. Time should be formatted as HH:mm:ss" } + require(s[2] == ':') { "Invalid time format: $s. Time should be formatted as HH:mm:ss" } + require(s[5] == ':') { "Invalid time format: $s. Time should be formatted as HH:mm:ss" } return Time( - s.substring(0, 3).toInt(), - s.substring(4, 6).toInt(), - s.substring(7, 9).toInt(), - s.substring(10).toInt() + s.substring(0, 2).toInt(), + s.substring(3, 5).toInt(), + s.substring(7).toInt(), ) } } diff --git a/db/src/main/kotlin/com/wbrawner/twigs/db/JdbcRecurringTransactionRepository.kt b/db/src/main/kotlin/com/wbrawner/twigs/db/JdbcRecurringTransactionRepository.kt new file mode 100644 index 0000000..a7b4693 --- /dev/null +++ b/db/src/main/kotlin/com/wbrawner/twigs/db/JdbcRecurringTransactionRepository.kt @@ -0,0 +1,58 @@ +package com.wbrawner.twigs.db + +import com.wbrawner.twigs.asFrequency +import com.wbrawner.twigs.model.RecurringTransaction +import com.wbrawner.twigs.storage.RecurringTransactionRepository +import java.sql.ResultSet +import java.time.Instant +import javax.sql.DataSource + +class JdbcRecurringTransactionRepository(dataSource: DataSource) : + JdbcRepository(dataSource), + RecurringTransactionRepository { + override val tableName: String = TABLE_RECURRING_TRANSACTION + override val fields: Map Any?> = Fields.values().associateWith { it.entityField } + override val conflictFields: Collection = listOf(ID) + + override suspend fun findAll(now: Instant): List = dataSource.connection.use { conn -> + conn.executeQuery("SELECT * FROM $tableName WHERE ${Fields.START.name.lowercase()} < ?", listOf(now)) + } + + override suspend fun findAll(budgetId: String): List = dataSource.connection.use { conn -> + if (budgetId.isBlank()) throw IllegalArgumentException("budgetId cannot be null") + conn.executeQuery("SELECT * FROM $tableName WHERE ${Fields.BUDGET_ID.name.lowercase()} = ?", listOf(budgetId)) + } + + override fun ResultSet.toEntity(): RecurringTransaction = RecurringTransaction( + id = getString(ID), + title = getString(Fields.TITLE.name.lowercase()), + description = getString(Fields.DESCRIPTION.name.lowercase()), + frequency = getString(Fields.FREQUENCY.name.lowercase()).asFrequency(), + start = getInstant(Fields.START.name.lowercase())!!, + finish = getInstant(Fields.FINISH.name.lowercase()), + amount = getLong(Fields.AMOUNT.name.lowercase()), + expense = getBoolean(Fields.EXPENSE.name.lowercase()), + createdBy = getString(Fields.CREATED_BY.name.lowercase()), + categoryId = getString(Fields.CATEGORY_ID.name.lowercase()), + budgetId = getString(Fields.BUDGET_ID.name.lowercase()), + ) + + enum class Fields(val entityField: (RecurringTransaction) -> Any?) { + TITLE({ it.title }), + DESCRIPTION({ it.description }), + FREQUENCY({ it.frequency }), + START({ it.start }), + FINISH({ it.finish }), + LAST_RUN({ it.lastRun }), + AMOUNT({ it.amount }), + EXPENSE({ it.expense }), + CREATED_BY({ it.createdBy }), + CATEGORY_ID({ it.categoryId }), + BUDGET_ID({ it.budgetId }), + } + + companion object { + const val TABLE_RECURRING_TRANSACTION = "recurring_transactions" + } +} + diff --git a/db/src/main/kotlin/com/wbrawner/twigs/db/JdbcRepository.kt b/db/src/main/kotlin/com/wbrawner/twigs/db/JdbcRepository.kt index f06c620..fcd9c73 100644 --- a/db/src/main/kotlin/com/wbrawner/twigs/db/JdbcRepository.kt +++ b/db/src/main/kotlin/com/wbrawner/twigs/db/JdbcRepository.kt @@ -1,6 +1,7 @@ package com.wbrawner.twigs.db import com.wbrawner.twigs.Identifiable +import com.wbrawner.twigs.model.Frequency import com.wbrawner.twigs.storage.Repository import org.slf4j.LoggerFactory import java.sql.Connection @@ -101,6 +102,7 @@ abstract class JdbcRepository>(protected val dataS is Long -> setLong(index + 1, param) is String -> setString(index + 1, param) is Enum<*> -> setString(index + 1, param.name) + is Frequency -> setString(index + 1, param.toString()) null -> setNull(index + 1, NULL) else -> throw Error("Unhandled parameter type: ${param.javaClass.name}") } @@ -117,4 +119,4 @@ private val dateFormatter = DateTimeFormatterBuilder() .toFormatter() .withZone(ZoneId.of("UTC")) -fun ResultSet.getInstant(column: String): Instant = dateFormatter.parse(getString(column), Instant::from) +fun ResultSet.getInstant(column: String): Instant? = getString(column)?.let { dateFormatter.parse(it, Instant::from) } diff --git a/db/src/main/kotlin/com/wbrawner/twigs/db/JdbcSessionRepository.kt b/db/src/main/kotlin/com/wbrawner/twigs/db/JdbcSessionRepository.kt index ebe61b9..6ddd56c 100644 --- a/db/src/main/kotlin/com/wbrawner/twigs/db/JdbcSessionRepository.kt +++ b/db/src/main/kotlin/com/wbrawner/twigs/db/JdbcSessionRepository.kt @@ -30,7 +30,7 @@ class JdbcSessionRepository(dataSource: DataSource) : JdbcRepository Any?) { diff --git a/db/src/main/kotlin/com/wbrawner/twigs/db/JdbcTransactionRepository.kt b/db/src/main/kotlin/com/wbrawner/twigs/db/JdbcTransactionRepository.kt index ebbe362..372bc45 100644 --- a/db/src/main/kotlin/com/wbrawner/twigs/db/JdbcTransactionRepository.kt +++ b/db/src/main/kotlin/com/wbrawner/twigs/db/JdbcTransactionRepository.kt @@ -86,7 +86,7 @@ class JdbcTransactionRepository(dataSource: DataSource) : id = getString(ID), title = getString(Fields.TITLE.name.lowercase()), description = getString(Fields.DESCRIPTION.name.lowercase()), - date = getInstant(Fields.DATE.name.lowercase()), + date = getInstant(Fields.DATE.name.lowercase())!!, amount = getLong(Fields.AMOUNT.name.lowercase()), expense = getBoolean(Fields.EXPENSE.name.lowercase()), createdBy = getString(Fields.CREATED_BY.name.lowercase()), @@ -109,4 +109,3 @@ class JdbcTransactionRepository(dataSource: DataSource) : const val TABLE_TRANSACTION = "transactions" } } - diff --git a/storage/src/main/kotlin/com/wbrawner/twigs/storage/RecurringTransactionRepository.kt b/storage/src/main/kotlin/com/wbrawner/twigs/storage/RecurringTransactionRepository.kt index 5126991..ad23331 100644 --- a/storage/src/main/kotlin/com/wbrawner/twigs/storage/RecurringTransactionRepository.kt +++ b/storage/src/main/kotlin/com/wbrawner/twigs/storage/RecurringTransactionRepository.kt @@ -5,4 +5,5 @@ import java.time.Instant interface RecurringTransactionRepository : Repository { suspend fun findAll(now: Instant): List + suspend fun findAll(budgetId: String): List } \ No newline at end of file diff --git a/testhelpers/src/main/kotlin/com/wbrawner/twigs/test/helpers/repository/FakeRecurringTransactionsRepository.kt b/testhelpers/src/main/kotlin/com/wbrawner/twigs/test/helpers/repository/FakeRecurringTransactionsRepository.kt index ec4d3be..0d6468d 100644 --- a/testhelpers/src/main/kotlin/com/wbrawner/twigs/test/helpers/repository/FakeRecurringTransactionsRepository.kt +++ b/testhelpers/src/main/kotlin/com/wbrawner/twigs/test/helpers/repository/FakeRecurringTransactionsRepository.kt @@ -6,6 +6,10 @@ import java.time.Instant class FakeRecurringTransactionsRepository : FakeRepository(), RecurringTransactionRepository { override suspend fun findAll(now: Instant): List = entities.filter { - (it.start == now || it.start.isBefore(now)) && it.end?.isAfter(now) ?: true + (it.start == now || it.start.isBefore(now)) && it.finish?.isAfter(now) ?: true + } + + override suspend fun findAll(budgetId: String): List = entities.filter { + it.budgetId == budgetId } } \ No newline at end of file