Skip to content

Commit

Permalink
Merge pull request #162 from DELTSV/expense-v2
Browse files Browse the repository at this point in the history
Add expense in backoffice + mutliple fix
  • Loading branch information
Loic-Vanden-Bossche authored Jul 7, 2024
2 parents d88c575 + 35579df commit 9301d3b
Show file tree
Hide file tree
Showing 24 changed files with 314 additions and 115 deletions.
2 changes: 1 addition & 1 deletion packages/backend/src/main/kotlin/hollybike/api/Api.kt
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ fun Application.api(storageService: StorageService, db: Database) {
val journeyService = JourneyService(db, associationService, storageService, conf.mapBox)
val profileService = ProfileService(db)
val userEventPositionService = UserEventPositionService(db, CoroutineScope(Dispatchers.Default), storageService)
val expenseService = ExpenseService(db, eventService)
val expenseService = ExpenseService(db, eventService, storageService)
val mailSender = attributes.conf.smtp?.let {
MailSender(it.url, it.port, it.username ?: "", it.password ?: "", it.sender)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package hollybike.api.repository

import hollybike.api.signatureService
import hollybike.api.utils.search.Mapper
import kotlinx.datetime.Clock
import org.jetbrains.exposed.dao.IntEntity
Expand All @@ -14,6 +15,7 @@ object Expenses : IntIdTable("expenses", "id_expense") {
val date = timestamp("date").default(Clock.System.now())
val amount = integer("amount")
val event = reference("event", Events)
val proof = varchar("proof", 2_048).nullable().default(null)
}

class Expense(id: EntityID<Int>) : IntEntity(id) {
Expand All @@ -22,6 +24,8 @@ class Expense(id: EntityID<Int>) : IntEntity(id) {
var date by Expenses.date
var amount by Expenses.amount
var event by Event referencedOn Expenses.event
var proof by Expenses.proof
val proofSigned by Expenses.proof.transform({ it }, { it?.let { s -> signatureService.sign(s) } })

companion object: IntEntityClass<Expense>(Expenses)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,7 @@ class EventController(
val (event, callerParticipation) = eventService.getEventWithParticipation(call.user, id.details.id)
?: return@get call.respond(HttpStatusCode.NotFound, "L'évènement n'a pas été trouvé")

val eventExpenses = expenseService.getEventExpense(call.user, event) ?: run {
call.respond(HttpStatusCode.NotFound, "L'évènement n'a pas été trouvé")
return@get
}
val eventExpenses = expenseService.getEventExpense(call.user, event)

eventParticipationService.getParticipationsPreview(call.user, id.details.id)
.onSuccess { (participants, participantsCount) ->
Expand All @@ -127,7 +124,7 @@ class EventController(
val event = eventService.getEvent(call.user, id.id)
?: return@get call.respond(HttpStatusCode.NotFound, "L'évènement n'a pas été trouvé")

call.respond(TEvent(event))
call.respond(TEvent(event, expenseService.authorizeBudget(call.user, event)))
}
}

Expand All @@ -147,7 +144,7 @@ class EventController(
newEvent.endDate,
association
).onSuccess {
call.respond(HttpStatusCode.Created, TEvent(it))
call.respond(HttpStatusCode.Created, TEvent(it, expenseService.authorizeBudget(call.user, it)))
}.onFailure {
eventService.handleEventExceptions(it, call)
}
Expand Down Expand Up @@ -193,9 +190,10 @@ class EventController(
updateEvent.name,
updateEvent.description,
updateEvent.startDate,
updateEvent.endDate
updateEvent.endDate,
updateEvent.budget
).onSuccess {
call.respond(HttpStatusCode.OK, TEvent(it))
call.respond(HttpStatusCode.OK, TEvent(it, expenseService.authorizeBudget(call.user, it)))
}.onFailure {
eventService.handleEventExceptions(it, call)
}
Expand Down Expand Up @@ -258,7 +256,7 @@ class EventController(
image.streamProvider().readBytes(),
contentType.toString()
).onSuccess {
call.respond(TEvent(it))
call.respond(TEvent(it, expenseService.authorizeBudget(call.user, it)))
}.onFailure {
eventService.handleEventExceptions(it, call)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ import io.ktor.server.resources.*
import io.ktor.server.resources.patch
import io.ktor.server.resources.post
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.server.routing.Route
import io.ktor.server.routing.routing

class ExpenseController(
application: Application,
Expand All @@ -40,6 +41,7 @@ class ExpenseController(
createExpense()
updateExpense()
deleteExpense()
uploadExpenseProof()
}
}
}
Expand Down Expand Up @@ -114,4 +116,24 @@ class ExpenseController(
}
}
}

private fun Route.uploadExpenseProof() {
put<Expenses.Id.Proof> {
val expense = expenseService.getExpense(call.user, it.id.id) ?: run {
return@put call.respond(HttpStatusCode.NotFound, "Dépense non trouvé")
}
val contentType = call.request.contentType()
if("image" !in contentType.contentType) {
return@put call.respond(HttpStatusCode.BadRequest)
}
val data = call.receiveStream().readBytes()
expenseService.uploadProof(call.user, expense, data, contentType).onSuccess { e ->
call.respond(TExpense(e))
}.onFailure { err ->
when(err) {
is NotAllowedException -> call.respond(HttpStatusCode.Forbidden)
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import io.ktor.resources.*
@Resource("/expenses")
class Expenses(val api: API) {
@Resource("/{id}")
class Id(val expenses: Expenses, val id: Int)
class Id(val expenses: Expenses, val id: Int) {
@Resource("/proof")
class Proof(val id: Id)
}

@Resource("/meta-data")
class Metadata(val expenses: Expenses)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ class EventService(
}

Result.success(
createdEvent
createdEvent.load(Event::participants)
)
}.apply {
onSuccess { e ->
Expand Down Expand Up @@ -358,6 +358,7 @@ class EventService(
description: String?,
startDate: Instant,
endDate: Instant?,
budget: Int?
): Result<Event> {
checkEventInputDates(startDate, endDate, false).onFailure { return Result.failure(it) }
checkEventTextFields(name, description).onFailure { return Result.failure(it) }
Expand All @@ -370,6 +371,7 @@ class EventService(
this.description = description
this.startDateTime = startDate
this.endDateTime = endDate
this.budget = budget
}

Result.success(event)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import hollybike.api.exceptions.CannotCreateExpenseException
import hollybike.api.exceptions.EventNotFoundException
import hollybike.api.exceptions.NotAllowedException
import hollybike.api.repository.*
import hollybike.api.services.storage.StorageService
import hollybike.api.types.event.participation.EEventRole
import hollybike.api.types.expense.TNewExpense
import hollybike.api.types.expense.TUpdateExpense
Expand All @@ -13,6 +14,7 @@ import hollybike.api.utils.search.Filter
import hollybike.api.utils.search.FilterMode
import hollybike.api.utils.search.SearchParam
import hollybike.api.utils.search.applyParam
import io.ktor.http.*
import org.jetbrains.exposed.dao.load
import org.jetbrains.exposed.dao.with
import org.jetbrains.exposed.sql.Database
Expand All @@ -22,7 +24,8 @@ import org.jetbrains.exposed.sql.transactions.transaction

class ExpenseService(
private val db: Database,
private val eventService: EventService
private val eventService: EventService,
private val storageService: StorageService
) {
private fun authorizeGetOrUpdateOrDelete(caller: User, expense: Expense): Boolean = when (caller.scope) {
EUserScope.Root -> true
Expand All @@ -39,6 +42,12 @@ class ExpenseService(
EUserScope.User -> event.participants.any { it.user.id == caller.id && it.role == EEventRole.Organizer }
}

fun authorizeBudget(caller: User, event: Event): Boolean = when (caller.scope) {
EUserScope.Root -> true
EUserScope.Admin -> event.association.id == caller.association.id
EUserScope.User -> event.participants.any { it.user.id == caller.id && it.role == EEventRole.Organizer }
}

fun getExpense(caller: User, id: Int): Expense? = transaction(db) {
Expense.findById(id)?.load(Expense::event) getIfAllowed caller
}
Expand Down Expand Up @@ -134,4 +143,16 @@ class ExpenseService(
transaction(db) { expense.delete() }
return Result.success(Unit)
}

suspend fun uploadProof(caller: User, expense: Expense, data: ByteArray, contentType: ContentType): Result<Expense> {
if(!authorizeGetOrUpdateOrDelete(caller, expense)) {
return Result.failure(NotAllowedException())
}
val path = "e/${expense.event.id}/e/${expense.id}/p"
storageService.store(data, path, contentType.contentType)
transaction(db) {
expense.proof = path
}
return Result.success(expense)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ data class TEvent(
val budget: Int? = null
) {
constructor(
entity: Event
entity: Event, viewBudget: Boolean
) : this(
id = entity.id.value,
name = entity.name,
Expand All @@ -40,6 +40,6 @@ data class TEvent(
createDateTime = entity.createDateTime,
updateDateTime = entity.updateDateTime,
association = TPartialAssociation(entity.association),
budget = entity.budget
budget = if(viewBudget) entity.budget else null
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,22 @@ data class TEventDetails(
val journey: TJourneyPartial?,
val previewParticipants: List<TEventParticipation>,
val previewParticipantsCount: Long,
val expenses: List<TExpense>,
val totalExpense: Int
val expenses: List<TExpense>? = null,
val totalExpense: Int? = null
) {
constructor(
event: Event,
callerParticipation: EventParticipation?,
participants: List<EventParticipation>,
participantsCount: Long,
expenses: List<Expense>
expenses: List<Expense>?
) : this(
event = TEvent(event),
event = TEvent(event, expenses != null),
callerParticipation = callerParticipation?.let { TEventCallerParticipation(it) },
journey = event.journey?.let { TJourneyPartial(it) },
previewParticipants = participants.map { TEventParticipation(it) },
previewParticipantsCount = participantsCount,
expenses = expenses.map { TExpense(it) },
totalExpense = expenses.sumOf { it.amount }
expenses = expenses?.map { TExpense(it) },
totalExpense = expenses?.sumOf { it.amount }
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ data class TUpdateEvent(
@SerialName("start_date")
val startDate: Instant,
@SerialName("end_date")
val endDate: Instant? = null
val endDate: Instant? = null,
val budget: Int? = null
)
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ data class TExpense(
val name: String,
val description: String? = null,
val date: Instant,
val amount: Int
val amount: Int,
val proof: String? = null
) {
constructor(entity: Expense): this(
entity.id.value,
entity.name,
entity.description,
entity.date,
entity.amount
entity.amount,
entity.proofSigned
)
}
4 changes: 4 additions & 0 deletions packages/backend/src/main/resources/liquibase-changelog.sql
Original file line number Diff line number Diff line change
Expand Up @@ -439,3 +439,7 @@ CREATE TABLE IF NOT EXISTS expenses (

ALTER TABLE events
ADD COLUMN IF NOT EXISTS budget INTEGER DEFAULT NULL;

--changeset denis:16
ALTER TABLE expenses
ADD COLUMN IF NOT EXISTS proof VARCHAR(2048) DEFAULT NULL;
6 changes: 3 additions & 3 deletions packages/frontend/src/associations/AssociationData.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ export function AssociationData(props: AssociationDataProps) {
);
return (
<Card>
<p>Nombre d'utilisateur : { data.data?.total_user }</p>
<p>Nombre d'utilisateurs : { data.data?.total_user }</p>
<p>Nombre d'évènements : { data.data?.total_event }</p>
<p>Nombre de balade : { data.data?.total_event_with_journey }</p>
<p>Nombre de trajet : { data.data?.total_journey }</p>
<p>Nombre de balades : { data.data?.total_event_with_journey }</p>
<p>Nombre de trajets : { data.data?.total_journey }</p>
</Card>
);
}
3 changes: 2 additions & 1 deletion packages/frontend/src/components/Input/FileInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ export function FileInput(props: FileInputProps) {
<input
value={v}
className={"hidden text-gray-600"}
id={id} type={"file"}
id={id}
type={"file"}
placeholder={props.placeholder}
accept={props.accept}
onInput={(e) => {
Expand Down
52 changes: 26 additions & 26 deletions packages/frontend/src/components/Modal/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,31 +12,31 @@ interface ModalProps {
}

export function Modal(props: ModalProps) {
if (props.visible) {
return (
<div
onClick={() => props.setVisible(false)}
className={"fixed top-0 left-0 w-screen h-screen bg-base/30 flex items-center justify-center"}
style={{ zIndex: 10_000 }}
return (
<div
onClick={() => props.setVisible(false)}
className={
clsx(
"fixed top-0 left-0 w-screen h-screen bg-base/30 flex items-center justify-center",
props.visible || "hidden",
)
}
style={{ zIndex: 10_000 }}
>
<Card
className={clsx("relative", props.width ?? "w-3/5")} onClick={(e) => {
e.stopPropagation();
}}
>
<Card
className={clsx("relative", props.width ?? "w-3/5")} onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
>
<div className={"flex gap-4 justify-between"}>
<div/>
<h1 className={"text-lg"}>{ props.title }</h1>
<button className={"right-2 top-2"} onClick={() => props.setVisible(false)}>
<Close/>
</button>
</div>
{ props.children }
</Card>
</div>
);
} else {
return null;
}
<div className={"flex gap-4 justify-between"}>
<div/>
<h1 className={"text-lg"}>{ props.title }</h1>
<button className={"right-2 top-2"} onClick={() => props.setVisible(false)}>
<Close/>
</button>
</div>
{ props.children }
</Card>
</div>
);
}
Loading

0 comments on commit 9301d3b

Please sign in to comment.