Skip to content
This repository has been archived by the owner on Feb 8, 2025. It is now read-only.

Commit

Permalink
Merge pull request #205 from Kerosene-Labs/BIL-6-update-fixed
Browse files Browse the repository at this point in the history
BIL-6: finishing up updating recurring expenses
  • Loading branch information
hlafaille authored Jan 5, 2025
2 parents 5c33e31 + 26b0c9c commit 84f068c
Show file tree
Hide file tree
Showing 31 changed files with 517 additions and 197 deletions.
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
/mvnw text eol=lf
*.cmd text eol=crlf
*.mp4 filter=lfs diff=lfs merge=lfs -text
2 changes: 2 additions & 0 deletions .github/workflows/develop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ jobs:
working-directory: ./frontend
steps:
- uses: actions/checkout@v4
with:
lfs: true

- name: Cache npm dependencies
uses: actions/cache@v3
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ jobs:
working-directory: ./frontend
steps:
- uses: actions/checkout@v4
with:
lfs: true

- name: Cache npm dependencies
uses: actions/cache@v3
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.kerosenelabs.billtracker.config

import com.kerosenelabs.billtracker.exception.AuthException
import com.kerosenelabs.billtracker.exception.BadRequestException
import com.kerosenelabs.billtracker.exception.UnconfirmedUserException
import com.kerosenelabs.billtracker.model.response.ErrorResponse
import org.springframework.http.HttpStatus
Expand All @@ -19,4 +20,9 @@ class GlobalExceptionHandler {
fun handleUnconfirmedUserException(e: UnconfirmedUserException): ResponseEntity<ErrorResponse> {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(ErrorResponse(e.message.toString()))
}

@ExceptionHandler(BadRequestException::class)
fun handleBadRequestException(e: BadRequestException): ResponseEntity<ErrorResponse> {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ErrorResponse(e.message.toString()))
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.kerosenelabs.billtracker.controller

import com.kerosenelabs.billtracker.entity.UserEntity
import com.kerosenelabs.billtracker.exception.BadRequestException
import com.kerosenelabs.billtracker.model.request.CreateOneOffExpenseRequest
import com.kerosenelabs.billtracker.model.request.CreateRecurringExpenseCreatorRequest
import com.kerosenelabs.billtracker.model.response.GetExpenseEventsResponse
Expand All @@ -10,12 +11,7 @@ import io.swagger.v3.oas.annotations.Parameter
import io.swagger.v3.oas.annotations.tags.Tag
import jakarta.validation.Valid
import org.springframework.http.HttpStatus
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.ResponseStatus
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.bind.annotation.*
import java.util.*

@RestController
Expand Down Expand Up @@ -44,6 +40,36 @@ class ExpensesController(private val expenseService: ExpenseService) {
)
}

@PutMapping("/expenses/recurringCreators/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
fun supersedeRecurringExpenseCreator(
@Parameter(hidden = true) user: UserEntity,
@PathVariable("id", required = true) id: String,
@RequestBody request: CreateRecurringExpenseCreatorRequest
) {
val recurringExpenseEventCreator =
expenseService.getRecurringExpenseEventCreatorsByUser(user).find { it.id == UUID.fromString(id) }
?: throw BadRequestException("Invalid ID")
expenseService.supersedeRecurringExpenseEventCreator(
predecessor = recurringExpenseEventCreator,
amount = request.amount,
description = request.description,
recursEveryCalendarDay = request.recursEveryCalendarDay,
)
}

@DeleteMapping("/expenses/recurringCreators/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
fun deleteRecurringExpenseCreator(
@Parameter(hidden = true) user: UserEntity,
@PathVariable("id", required = true) id: String,
) {
val recurringExpenseEventCreator =
expenseService.getRecurringExpenseEventCreatorsByUser(user).find { it.id == UUID.fromString(id) }
?: throw BadRequestException("Invalid ID")
expenseService.hideRecurringExpenseEventCreator(recurringExpenseEventCreator)
}

@GetMapping("/expenses")
@ResponseStatus(HttpStatus.OK)
fun getExpenses(@Parameter(hidden = true) user: UserEntity): GetExpenseEventsResponse {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ class RecurringExpenseEventCreatorEntity(
@Column(nullable = false) var recursEveryCalendarDay: Int = 0,
@Column(nullable = false) var amount: BigDecimal = BigDecimal.ZERO,
@ManyToOne @JoinColumn(nullable = false) var user: UserEntity = UserEntity(),
@OneToOne var supersededBy: RecurringExpenseEventCreatorEntity? = null,
@OneToOne var predecessor: RecurringExpenseEventCreatorEntity? = null,
@Column(nullable = false) var description: String = "",
@Column(nullable = false) var hidden: Boolean = false,
) {
constructor(
recursEveryCalendarDay: Int, amount: BigDecimal, user: UserEntity, description: String
Expand All @@ -22,7 +23,6 @@ class RecurringExpenseEventCreatorEntity(
recursEveryCalendarDay = recursEveryCalendarDay,
amount = amount,
user = user,
supersededBy = null,
description = description
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.kerosenelabs.billtracker.exception

class BadRequestException(message: String?) : Exception(message)
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ data class RecurringExpenseEventCreator(
var recursEveryCalendarDay: Int,
var amount: BigDecimal,
var description: String,
var predecessor: UUID?,
)
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,13 @@ import org.springframework.data.jpa.repository.Query
import org.springframework.data.repository.query.Param
import java.util.*

interface RecurringExpenseEventCreatorRepository : JpaRepository<RecurringExpenseEventCreatorEntity, UUID>{
@Query("SELECT e FROM RecurringExpenseEventCreatorEntity e WHERE e.user = :user AND (:ids IS NULL OR e.id IN :ids)")
interface RecurringExpenseEventCreatorRepository : JpaRepository<RecurringExpenseEventCreatorEntity, UUID> {
/**
* Queries for Recurring Expense Event Creators that are for the given user, and optionally, find by IDs.
* This method also filters out hidden superseded records.
*/
@Query("SELECT e FROM RecurringExpenseEventCreatorEntity e WHERE e.user = :user AND (:ids IS NULL OR e.id IN :ids) AND e.hidden = false AND e.predecessor IS NOT NULL")
fun findAllByUserAndOptionalIds(
@Param("user") user: UserEntity,
@Param("ids") ids: List<UUID>?
): List<RecurringExpenseEventCreatorEntity>}
@Param("user") user: UserEntity, @Param("ids") ids: List<UUID>?
): List<RecurringExpenseEventCreatorEntity>
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ package com.kerosenelabs.billtracker.service
import com.kerosenelabs.billtracker.entity.ExpenseEventEntity
import com.kerosenelabs.billtracker.entity.RecurringExpenseEventCreatorEntity
import com.kerosenelabs.billtracker.entity.UserEntity
import com.kerosenelabs.billtracker.exception.BadRequestException
import com.kerosenelabs.billtracker.model.expense.ExpenseEvent
import com.kerosenelabs.billtracker.model.expense.ExpenseEventType
import com.kerosenelabs.billtracker.model.expense.RecurringExpenseEventCreator
import com.kerosenelabs.billtracker.repository.ExpenseEventRepository
import com.kerosenelabs.billtracker.repository.RecurringExpenseEventCreatorRepository
import org.postgresql.util.PSQLException
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.dao.DataIntegrityViolationException
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Service
import java.math.BigDecimal
Expand Down Expand Up @@ -53,6 +56,46 @@ class ExpenseService(
)
}

/**
* Supersede a Recurring Expense Event Creator. T
*/
fun supersedeRecurringExpenseEventCreator(
predecessor: RecurringExpenseEventCreatorEntity,
amount: BigDecimal,
recursEveryCalendarDay: Int,
description: String
): RecurringExpenseEventCreatorEntity {
try {
return recurringExpenseEventCreatorRepository.save(
RecurringExpenseEventCreatorEntity(
amount = amount,
user = predecessor.user,
recursEveryCalendarDay = recursEveryCalendarDay,
description = description,
predecessor = predecessor
)
)
} catch (e: DataIntegrityViolationException) {
if (e.message.toString().contains("violates unique constraint")) {
throw BadRequestException("This Recurring Expense Event Creator has already been superseded, you may not supersede it again. To supersede this, find the most recent iteration in the chain and supersede it.")
} else {
throw RuntimeException("Unexpected error: ${e.message}", e)
}
}
}

/**
* Hide (aka Delete) a Recurring Expense Event Creator. This will take it out of the rotation of auto-posting,
* but will leave the record intact for references.
*/
fun hideRecurringExpenseEventCreator(recurringExpenseEventCreator: RecurringExpenseEventCreatorEntity) {
if (recurringExpenseEventCreator.hidden) {
throw BadRequestException("This Recurring Expense Event Creator has already been hidden.")
}
recurringExpenseEventCreator.hidden = true
recurringExpenseEventCreatorRepository.save(recurringExpenseEventCreator)
}

/**
* Helper function to get all expense events.
* @see ExpenseEventEntity
Expand All @@ -66,7 +109,9 @@ class ExpenseService(
* further filter by Recurring Expense Event Creator ID.
* @see RecurringExpenseEventCreatorEntity
*/
fun getRecurringExpenseEventCreatorsByUser(user: UserEntity, ids: Optional<List<UUID>> = Optional.empty()): List<RecurringExpenseEventCreatorEntity> {
fun getRecurringExpenseEventCreatorsByUser(
user: UserEntity, ids: Optional<List<UUID>> = Optional.empty()
): List<RecurringExpenseEventCreatorEntity> {
return recurringExpenseEventCreatorRepository.findAllByUserAndOptionalIds(user, ids.getOrNull())
}

Expand Down Expand Up @@ -94,6 +139,7 @@ class ExpenseService(
description = entity.description,
amount = entity.amount,
recursEveryCalendarDay = entity.recursEveryCalendarDay,
predecessor = entity.predecessor?.id
)
}

Expand All @@ -109,12 +155,14 @@ class ExpenseService(
for (creator in creators) {
if (creator.recursEveryCalendarDay == now.dayOfMonth) {
logger.info("Posting Expense Event: ${creator.amount} - ${creator.description}");
expenseEventRepository.save(ExpenseEventEntity(
amount = creator.amount,
description = creator.description,
recurringExpenseEventCreator = creator,
date = now.atStartOfDay(zoneId).toInstant()
))
expenseEventRepository.save(
ExpenseEventEntity(
amount = creator.amount,
description = creator.description,
recurringExpenseEventCreator = creator,
date = now.atStartOfDay(zoneId).toInstant()
)
)
}
}
}
Expand Down
Binary file added frontend/bun.lockb
Binary file not shown.
2 changes: 1 addition & 1 deletion frontend/src/lib/components/Back.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
<button aria-label="App Drawer Toggle" on:click={() => {history.back()}}>
<span class="font-semibold text-2xl text-neutral-300 hover:text-neutral-400 dark:text-neutral-600 dark:hover:text-neutral-500 transition-colors">&lt;</span>
</button>
<span class="font-bold desktop:text-xl text-neutral-800 dark:text-neutral-400 ">{title}</span>
<span class="font-semibold desktop:text-xl text-neutral-800 dark:text-neutral-400 ">{title}</span>
</div>
<div id="pageContent" class="h-full overflow-y-auto pt-14">
{@render children()}
Expand Down
15 changes: 7 additions & 8 deletions frontend/src/lib/components/IntroductionsCard.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import { onMount } from "svelte";
import Card from "$lib/tk/Card.svelte";
import { goto } from "$app/navigation";
import ETextInput from "$lib/eureka/input/ETextInput.svelte";
import EDateInput from "$lib/eureka/input/EDateInput.svelte";
let firstName: string;
let lastName: string;
Expand Down Expand Up @@ -58,20 +60,17 @@
<Card title="Introductions" subtitle="Tell us a bit about yourself.">
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-4 xl:flex-row">
<LineEdit
<ETextInput
bind:value={firstName}
id="firstName"
label="First Name"
type="text"
></LineEdit>
<LineEdit
></ETextInput>
<ETextInput
bind:value={lastName}
id="lastName"
label="Last Name"
type="text"
></LineEdit>
<LineEdit bind:value={birthday} id="birthday" label="Birthday" type="date"
></LineEdit>
></ETextInput>
<EDateInput bind:value={birthday} id="birthday" label="Birthday"></EDateInput>
</div>
<Button on:click={save}>Save</Button>
</div>
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/lib/components/NavUserCard.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@
<Spinner></Spinner>
</div>
{/if}
<Button on:click={doLogOut}>Log Out</Button>
<div class="pt-2">
<Button on:click={doLogOut}>Log Out</Button>
</div>
</Card>
</div>
22 changes: 12 additions & 10 deletions frontend/src/lib/components/expenses/OneOffCreator.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,33 @@
import { addToToastQueue, ToastType } from "$lib/toast";
import { goto } from "$app/navigation";
import ENumberInput from "$lib/eureka/input/ENumberInput.svelte";
import ETextInput from "$lib/eureka/input/ETextInput.svelte";
import EDateInput from "$lib/eureka/input/EDateInput.svelte";
import EButton from "$lib/eureka/button/EButton.svelte";
let amount: number = 0.0;
let description: string;
let date: string = "";
let date: Date = "";
function createOneOff() {
new ExpensesApi(getPrivateApiConfig())
.createOneOff({
createOneOffExpenseRequest: {
amount,
date: new Date(date),
description: description,
},
description: description
}
})
.then((response) => {
addToToastQueue({
message: "Successfully created one-off expense.",
type: ToastType.SUCCESS,
type: ToastType.SUCCESS
});
goto("/app/expenses");
})
.catch(async (error: ResponseError) => {
await getErrorMessageFromSdk(error).then((msg) =>
addToToastQueue({ message: msg, type: ToastType.ERROR }),
addToToastQueue({ message: msg, type: ToastType.ERROR })
);
});
}
Expand All @@ -44,14 +47,13 @@
<div class="flex h-fit w-full flex-col gap-4 xl:flex-row">
<ENumberInput id="amount" label="Dollars" prefix="$" bind:value={amount}
></ENumberInput>
<LineEdit
<ETextInput
id="description"
type="text"
label="Description"
bind:value={description}
></LineEdit>
<LineEdit id="date" type="date" label="Date" bind:value={date}></LineEdit>
></ETextInput>
<EDateInput id="date" label="Date" bind:value={date}></EDateInput>
</div>
<Button on:click={createOneOff}>Create</Button>
<EButton onclick={createOneOff}>Create</EButton>
</div>
</Card>
Loading

0 comments on commit 84f068c

Please sign in to comment.