-
Notifications
You must be signed in to change notification settings - Fork 0
Allow one sponsoring to target multiple teams #264
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -76,8 +76,8 @@ class Team : BasicEntity, Blockable { | |
| @OneToOne(cascade = [ALL], orphanRemoval = true, mappedBy = "team", fetch = LAZY) | ||
| var invoice: TeamEntryFeeInvoice? = null | ||
|
|
||
| @OneToMany(cascade = [ALL], orphanRemoval = true, mappedBy = "team") | ||
| var sponsoring: MutableList<Sponsoring> = ArrayList() | ||
| @ManyToMany(cascade = [ALL], mappedBy = "teams") | ||
| var sponsorings: MutableList<Sponsoring> = ArrayList() | ||
|
|
||
| @OneToMany(cascade = [ALL], orphanRemoval = true, mappedBy = "team") | ||
| var challenges: MutableList<Challenge> = ArrayList() | ||
|
|
@@ -115,7 +115,7 @@ class Team : BasicEntity, Blockable { | |
| fun invite(email: EmailAddress): Invitation { | ||
| if (isInvited(email)) throw DomainException("User $email already is invited to this team") | ||
| val invitation = Invitation(email, this) | ||
| if(this.isFull()) throw DomainException("Team is already full") | ||
| if (this.isFull()) throw DomainException("Team is already full") | ||
| this.invitations.add(invitation) | ||
| return invitation | ||
| } | ||
|
|
@@ -165,8 +165,8 @@ class Team : BasicEntity, Blockable { | |
| this.invitations.forEach { it.team = null } | ||
| this.invitations.clear() | ||
|
|
||
| this.sponsoring.forEach { it.team = null } | ||
| this.sponsoring.clear() | ||
| this.sponsorings.forEach {it.teams.clear() } | ||
| this.sponsorings.clear() | ||
|
|
||
| this.challenges.forEach { it.team = null } | ||
| this.challenges.clear() | ||
|
|
@@ -177,7 +177,7 @@ class Team : BasicEntity, Blockable { | |
| } | ||
|
|
||
| fun raisedAmountFromSponsorings(): Money { | ||
| return this.sponsoring.billableAmount() | ||
| return this.sponsorings.billableAmount() | ||
| } | ||
|
|
||
| @Deprecated("The naming on this is bad as the current distance is actually the farthest distance during the event. This will be renamed") | ||
|
|
@@ -191,7 +191,7 @@ class Team : BasicEntity, Blockable { | |
| |<b>Challenges</b> | ||
| |${challenges.toEmailListing()} | ||
| |<b>Kilometerspenden / Donations per km</b> | ||
| |${this.sponsoring.toEmailListing()} | ||
| |${this.sponsorings.toEmailListing()} | ||
| """.trimMargin("|") | ||
| } | ||
|
|
||
|
|
@@ -201,7 +201,12 @@ class Team : BasicEntity, Blockable { | |
| } | ||
|
|
||
| private fun Sponsoring.toEmailListing(): String { | ||
| return "<b>Name</b> ${this.sponsor?.firstname} ${this.sponsor?.lastname} <b>Status</b> ${this.status} <b>Betrag pro km</b> ${this.amountPerKm.display()} <b>Limit</b> ${this.limit.display()} <b>Gereiste KM</b> ${this.team?.getCurrentDistance()} <b>Spendenversprechen</b> ${this.billableAmount().display()}" | ||
| var result = "<b>Name</b> ${this.sponsor?.firstname} ${this.sponsor?.lastname} <b>Status</b> ${this.status} <b>Betrag pro km</b> ${this.amountPerKm.display()} <b>Limit</b> ${this.limit.display()} <b>Spendenversprechen</b> ${this.billableAmount().display()}" | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need to check whether the visualization is correct this way. We should also add a ticket for HTML encoding the content. Team names can include any HTML which is displayed here... |
||
| for(team in teams) { | ||
| // TODO: HTML encode team name | ||
| result += "<br/><b>Team ${team.name} <b>Gereiste KM</b> ${team.getCurrentDistance()} " | ||
| } | ||
| return result | ||
| } | ||
|
|
||
| @JvmName("challengeToEmailListing") | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -118,7 +118,9 @@ class SponsoringInvoice : Invoice { | |
| } | ||
|
|
||
| private fun Sponsoring.toEmailListing(): String { | ||
| return "<b>Team-ID</b> ${this.team?.id} <b>Teamname</b> ${this.team?.name} <b>Status</b> ${this.status} <b>Betrag pro km</b> ${this.amountPerKm.display()} <b>Limit</b> ${this.limit.display()} <b>Gereiste KM</b> ${this.team?.getCurrentDistance()} <b>Spendenversprechen</b> ${this.billableAmount().display()}" | ||
| // TODO: implement with teams | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm honestly not sure how this should be correctly implemented. We need to check who this is sent to? The sponsor? Each team? Based on this we either list all teams here (if sent to sponsor) or add a team to the function parameters and only show this team's name+id+distance+km+billable amount. And not all other teams they're not interested in. |
||
| return "" | ||
| //return "<b>Team-ID</b> ${this.team?.id} <b>Teamname</b> ${this.team?.name} <b>Status</b> ${this.status} <b>Betrag pro km</b> ${this.amountPerKm.display()} <b>Limit</b> ${this.limit.display()} <b>Gereiste KM</b> ${this.team?.getCurrentDistance()} <b>Spendenversprechen</b> ${this.billableAmount().display()}" | ||
| } | ||
|
|
||
| @JvmName("challengeToEmailListing") | ||
|
|
@@ -135,7 +137,7 @@ class SponsoringInvoice : Invoice { | |
| return when (sponsor) { | ||
| is UnregisteredSponsor -> { | ||
| val fromChallenges = this.challenges.flatMap { it.team?.members?.map { EmailAddress(it.email) } ?: listOf() } | ||
| val fromSponsorings = this.sponsorings.flatMap { it.team?.members?.map { EmailAddress(it.email) } ?: listOf() } | ||
| val fromSponsorings = this.sponsorings.flatMap { it.teams?.flatMap { t -> t.members?.map { m -> EmailAddress(m.email) } ?: listOf() } } | ||
| val fromUnregistered: Iterable<EmailAddress> = if (this.unregisteredSponsor?.email != null) { | ||
| try { | ||
| listOf(EmailAddress(this.unregisteredSponsor!!.email!!)) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,9 +18,10 @@ interface SponsoringInvoiceRepository : CrudRepository<SponsoringInvoice, Long> | |
| @Query(""" | ||
| select distinct i | ||
| from SponsoringInvoice i | ||
| left join i.sponsorings s on s.team.id = :teamId | ||
| left join i.challenges c on c.team.id = :teamId | ||
| where s.id is not null or c.id is not null | ||
| left join i.sponsorings s | ||
| where c.id is not null | ||
| and exists (SELECT 1 FROM Sponsoring sp JOIN sp.teams t WHERE sp.id = s.id AND t.id = :teamId) | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This SQL query is kind of untested. It might also be better to use |
||
| """) | ||
| fun findByTeamId(@Param("teamId") teamId: Long): Iterable<SponsoringInvoice> | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,7 @@ package backend.model.sponsoring | |
|
|
||
| import backend.exceptions.DomainException | ||
| import backend.model.BasicEntity | ||
| import backend.model.event.Event | ||
| import backend.model.event.Team | ||
| import backend.model.media.Media | ||
| import backend.model.misc.EmailAddress | ||
|
|
@@ -12,6 +13,7 @@ import backend.util.euroOf | |
| import org.javamoney.moneta.Money | ||
| import org.slf4j.Logger | ||
| import org.slf4j.LoggerFactory | ||
| import java.util.HashSet | ||
| import javax.persistence.* | ||
| import javax.persistence.CascadeType.PERSIST | ||
|
|
||
|
|
@@ -25,7 +27,7 @@ class Sponsoring : BasicEntity, Billable { | |
| var contract: Media? = null | ||
|
|
||
| var status: SponsoringStatus = ACCEPTED | ||
| private set (value) { | ||
| private set(value) { | ||
| checkTransition(from = field, to = value) | ||
| field = value | ||
| } | ||
|
|
@@ -37,15 +39,18 @@ class Sponsoring : BasicEntity, Billable { | |
| lateinit var limit: Money | ||
| private set | ||
|
|
||
| @ManyToOne | ||
| var team: Team? = null | ||
| @ManyToMany | ||
| var teams: MutableSet<Team> = HashSet() | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I chose a set for now. We might also want to use a list instead. I want to make sure that each team can only be once in there, so a Set felt better as a starting point. |
||
|
|
||
| @ManyToOne(cascade = [PERSIST]) | ||
| private var unregisteredSponsor: UnregisteredSponsor? = null | ||
|
|
||
| @ManyToOne | ||
| private var registeredSponsor: Sponsor? = null | ||
|
|
||
| @ManyToOne | ||
| var event: Event? = null | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is new because the event was initially read from the single team's event field. However, we theoretically might have 0 teams and thus still need to store the event relation somewhere. |
||
|
|
||
| var sponsor: ISponsor? | ||
| get() = this.unregisteredSponsor as? ISponsor ?: this.registeredSponsor | ||
| private set(value) {} | ||
|
|
@@ -55,18 +60,20 @@ class Sponsoring : BasicEntity, Billable { | |
| */ | ||
| private constructor() : super() | ||
|
|
||
| constructor(sponsor: Sponsor, team: Team, amountPerKm: Money, limit: Money) : this() { | ||
| constructor(event: Event, sponsor: Sponsor, teams: MutableSet<Team>, amountPerKm: Money, limit: Money) : this() { | ||
| this.event = event | ||
| this.registeredSponsor = sponsor | ||
| this.team = team | ||
| this.teams = teams | ||
| this.amountPerKm = amountPerKm | ||
| this.limit = limit | ||
| this.contract = null | ||
| this.sponsor?.sponsorings?.add(this) | ||
| } | ||
|
|
||
| constructor(unregisteredSponsor: UnregisteredSponsor, team: Team, amountPerKm: Money, limit: Money) : this() { | ||
| constructor(event: Event, unregisteredSponsor: UnregisteredSponsor, teams: MutableSet<Team>, amountPerKm: Money, limit: Money) : this() { | ||
| this.event = event | ||
| this.unregisteredSponsor = unregisteredSponsor | ||
| this.team = team | ||
| this.teams = teams | ||
| this.amountPerKm = amountPerKm | ||
| this.limit = limit | ||
| this.status = ACCEPTED | ||
|
|
@@ -97,10 +104,15 @@ class Sponsoring : BasicEntity, Billable { | |
| this.registeredSponsor = null | ||
| } | ||
|
|
||
| @Suppress("UNUSED") //Used by Spring @PreAuthorize | ||
| fun isTeamMember(username: String): Boolean { | ||
| return this.teams?.any { it.isMember(username) } | ||
| } | ||
|
|
||
| @Suppress("UNUSED") //Used by Spring @PreAuthorize | ||
| fun checkWithdrawPermissions(username: String): Boolean { | ||
| return when { | ||
| this.unregisteredSponsor != null -> this.team!!.isMember(username) | ||
| this.unregisteredSponsor != null -> this.unregisteredSponsor!!.team?.isMember(username) == true | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm a bit unsure whether this is correct. Can the one who created the sponsoring (so We need to check this. |
||
| this.registeredSponsor != null -> EmailAddress(this.registeredSponsor!!.email) == EmailAddress(username) | ||
| else -> throw Exception("Error checking withdrawal permissions") | ||
| } | ||
|
|
@@ -141,8 +153,9 @@ class Sponsoring : BasicEntity, Billable { | |
| } | ||
|
|
||
| override fun billableAmount(): Money { | ||
|
|
||
| val distance: Double = team?.getCurrentDistance() ?: run { | ||
| val distance: Double = if (teams.isNotEmpty()) | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need to implement the billing calculation across multiple teams here. |
||
| teams?.sumByDouble { it.getCurrentDistance() } | ||
| else run { | ||
| logger.warn("No team for sponsoring $id found. Using 0.0 for currentDistance") | ||
| return@run 0.0 | ||
| } | ||
|
|
@@ -161,7 +174,7 @@ class Sponsoring : BasicEntity, Billable { | |
| } | ||
|
|
||
| fun belongsToEvent(eventId: Long): Boolean { | ||
| return this.team?.event?.id == eventId | ||
| return this.event?.id == eventId | ||
| } | ||
|
|
||
| fun removeSponsor() { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,28 +1,29 @@ | ||
| package backend.model.sponsoring | ||
|
|
||
| import backend.model.event.Event | ||
| import backend.model.event.Team | ||
| import backend.model.user.Sponsor | ||
| import org.javamoney.moneta.Money | ||
| import org.springframework.security.access.prepost.PreAuthorize | ||
|
|
||
| interface SponsoringService { | ||
|
|
||
| fun createSponsoring(sponsor: Sponsor, team: Team, amountPerKm: Money, limit: Money): Sponsoring | ||
| fun createSponsoring(event: Event, sponsor: Sponsor, teams: MutableSet<Team>, amountPerKm: Money, limit: Money): Sponsoring | ||
| fun findByTeamId(teamId: Long): Iterable<Sponsoring> | ||
| fun findBySponsorId(sponsorId: Long): Iterable<Sponsoring> | ||
| fun findOne(id: Long): Sponsoring? | ||
|
|
||
| @PreAuthorize("#sponsoring.team.isMember(authentication.name)") | ||
| @PreAuthorize("#sponsoring.isTeamMember(authentication.name)") | ||
| fun acceptSponsoring(sponsoring: Sponsoring): Sponsoring | ||
|
|
||
| @PreAuthorize("#sponsoring.team.isMember(authentication.name)") | ||
| @PreAuthorize("#sponsoring.isTeamMember(authentication.name)") | ||
| fun rejectSponsoring(sponsoring: Sponsoring): Sponsoring | ||
|
|
||
| @PreAuthorize("#sponsoring.checkWithdrawPermissions(authentication.name)") | ||
| fun withdrawSponsoring(sponsoring: Sponsoring): Sponsoring | ||
|
|
||
| @PreAuthorize("#team.isMember(authentication.name)") | ||
| fun createSponsoringWithOfflineSponsor(team: Team, amountPerKm: Money, limit: Money, unregisteredSponsor: UnregisteredSponsor): Sponsoring | ||
| @PreAuthorize("#unregisteredSponsor.team.isMember(authentication.name)") | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need to check how this behaves in the real world. Perhaps we might need to check |
||
| fun createSponsoringWithOfflineSponsor(event: Event, amountPerKm: Money, limit: Money, unregisteredSponsor: UnregisteredSponsor): Sponsoring | ||
|
|
||
| @PreAuthorize("hasAuthority('ADMIN')") | ||
| fun sendEmailsToSponsorsWhenEventHasStarted() | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For backwords compatibility, I'd personally like to keep this endpoint. It will accept a sponsoring be created for a single team.
We do have unit tests for it, so just adding a new endpoint in the first place should be the solution.