Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/main/java/backend/configuration/TestDataInitializer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ class TestDataInitializer {
teamService.join(david, teamiOS)

setAuthenticatedUser(keno.email)
sponsoringService.createSponsoring(keno, teamiOS, euroOf(5), euroOf(10000000))
sponsoringService.createSponsoring(eventMunich, keno, mutableSetOf(teamiOS), euroOf(5), euroOf(10000000))
locationService.create(Coord(51.0505, 13.7372), leo, date.plusHours(1))
locationService.create(Coord(48.8534100, 2.3488000), leo, date.plusHours(2))

Expand Down Expand Up @@ -88,11 +88,11 @@ class TestDataInitializer {

// ---- Sponsor1 ----
val sponsor1 = userService.create("sponsor1@break-out.org", "password", { addRole(Sponsor::class) }).getRole(Sponsor::class)!!
sponsoringService.createSponsoring(sponsor1, team1, Money.parse("EUR 0.1"), Money.parse("EUR 100"))
sponsoringService.createSponsoring(eventMunich, sponsor1, mutableSetOf(team1), Money.parse("EUR 0.1"), Money.parse("EUR 100"))

// ---- Sponsor2 ----
val sponsor2 = userService.create("sponsor2@break-out.org", "password", { addRole(Sponsor::class) }).getRole(Sponsor::class)!!
sponsoringService.createSponsoring(sponsor2, team2, Money.parse("EUR 0.2"), Money.parse("EUR 200"))
sponsoringService.createSponsoring(eventMunich, sponsor2, mutableSetOf(team2), Money.parse("EUR 0.2"), Money.parse("EUR 200"))

// ---- Locations for team1 ----
locationService.create(Coord(51.0505, 13.7372), participant1, date.plusHours(1))
Expand Down
1 change: 0 additions & 1 deletion src/main/java/backend/controller/AdminController.kt
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ class AdminController(private val userService: UserService,
private val eventService: EventService,
private val challengeService: ChallengeService,
private val sponsoringService: SponsoringService,
private val locationService: LocationService,
private val postingService: PostingService) {


Expand Down
34 changes: 23 additions & 11 deletions src/main/java/backend/controller/SponsoringController.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import backend.configuration.CustomUserDetails
import backend.controller.exceptions.BadRequestException
import backend.controller.exceptions.NotFoundException
import backend.controller.exceptions.UnauthorizedException
import backend.model.event.Event
import backend.model.event.EventService
import backend.model.event.Team
import backend.model.event.TeamService
import backend.model.sponsoring.Sponsoring
Expand All @@ -28,6 +30,7 @@ import javax.validation.Valid
class SponsoringController(private var sponsoringService: SponsoringService,
private var userService: UserService,
private var teamService: TeamService,
private var eventService: EventService,
private var configurationService: ConfigurationService) {

private val jwtSecret: String = configurationService.getRequired("org.breakout.api.jwt_secret")
Expand Down Expand Up @@ -89,41 +92,49 @@ class SponsoringController(private var sponsoringService: SponsoringService,
@PreAuthorize("isAuthenticated()")
@PostMapping("/event/{eventId}/team/{teamId}/sponsoring/")
@ResponseStatus(CREATED)
fun createSponsoring(@PathVariable teamId: Long,
fun createSponsoring(@PathVariable eventId: Long,
Copy link
Contributor Author

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.

@PathVariable teamId: Long,
@Valid @RequestBody body: SponsoringView,
@AuthenticationPrincipal customUserDetails: CustomUserDetails): SponsoringView {

val user = userService.getUserFromCustomUserDetails(customUserDetails)
val event = eventService.findById(eventId) ?: throw NotFoundException("Event with id $eventId not found")
val team = teamService.findOne(teamId) ?: throw NotFoundException("Team with id $teamId not found")
if (team.event.id != event.id) {
throw BadRequestException("Team not part of event")
}
val amountPerKm = Money.of(body.amountPerKm, "EUR")
val limit = Money.of(body.limit, "EUR")

val sponsoring = if (body.unregisteredSponsor != null) {
user.getRole(Participant::class) ?: throw UnauthorizedException("Cannot add unregistered sponsor if user is no participant")
createSponsoringWithUnregisteredSponsor(team, amountPerKm, limit, body.unregisteredSponsor!!)
user.getRole(Participant::class)
?: throw UnauthorizedException("Cannot add unregistered sponsor if user is no participant")
createSponsoringWithUnregisteredSponsor(event, team, amountPerKm, limit, body.unregisteredSponsor!!)
} else {
val sponsor = user.getRole(Sponsor::class) ?: throw UnauthorizedException("Cannot add user as sponsor. Missing role sponsor")
createSponsoringWithAuthenticatedSponsor(team, amountPerKm, limit, sponsor)
val sponsor = user.getRole(Sponsor::class)
?: throw UnauthorizedException("Cannot add user as sponsor. Missing role sponsor")
createSponsoringWithAuthenticatedSponsor(event, mutableSetOf(team), amountPerKm, limit, sponsor)
}

return SponsoringView(sponsoring)
}

private fun createSponsoringWithAuthenticatedSponsor(team: Team, amount: Money, limit: Money, sponsor: Sponsor): Sponsoring {
return sponsoringService.createSponsoring(sponsor, team, amount, limit)
private fun createSponsoringWithAuthenticatedSponsor(event: Event, teams: MutableSet<Team>, amount: Money, limit: Money, sponsor: Sponsor): Sponsoring {
return sponsoringService.createSponsoring(event, sponsor, teams, amount, limit)
}

private fun createSponsoringWithUnregisteredSponsor(team: Team, amount: Money, limit: Money, sponsor: UnregisteredSponsorView): Sponsoring {
private fun createSponsoringWithUnregisteredSponsor(event: Event, team: Team, amount: Money, limit: Money, sponsor: UnregisteredSponsorView): Sponsoring {

val unregisteredSponsor = UnregisteredSponsor(
firstname = sponsor.firstname!!,
lastname = sponsor.lastname!!,
company = sponsor.company!!,
address = sponsor.address!!.toAddress()!!,
email = sponsor.email,
isHidden = sponsor.isHidden)
isHidden = sponsor.isHidden,
team = team)

return sponsoringService.createSponsoringWithOfflineSponsor(team, amount, limit, unregisteredSponsor)
return sponsoringService.createSponsoringWithOfflineSponsor(event, amount, limit, unregisteredSponsor)
}


Expand All @@ -150,7 +161,8 @@ class SponsoringController(private var sponsoringService: SponsoringService,
@PathVariable sponsoringId: Long,
@RequestBody body: Map<String, String>): SponsoringView {

val sponsoring = sponsoringService.findOne(sponsoringId) ?: throw NotFoundException("No sponsoring with id $sponsoringId found")
val sponsoring = sponsoringService.findOne(sponsoringId)
?: throw NotFoundException("No sponsoring with id $sponsoringId found")
val status = body["status"] ?: throw BadRequestException("Missing status in body")

return when (status.toLowerCase()) {
Expand Down
21 changes: 13 additions & 8 deletions src/main/java/backend/model/event/Team.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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()
Expand All @@ -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")
Expand All @@ -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("|")
}

Expand All @@ -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()}"
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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")
Expand Down
6 changes: 4 additions & 2 deletions src/main/java/backend/model/payment/SponsoringInvoice.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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")
Expand All @@ -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!!))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Contributor Author

@sibbl sibbl May 16, 2022

Choose a reason for hiding this comment

The 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 and s.id IN (SELECT sp.id FROM Sponsoring sp JOIN sp.teams t WHERE sp.id = s.id AND t.id = :teamId) but my gut told me that exist might be faster.

""")
fun findByTeamId(@Param("teamId") teamId: Long): Iterable<SponsoringInvoice>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ class SponsoringInvoiceServiceImpl(private val sponsoringInvoiceRepository: Spon

private fun invoiceToTeamSponsorPairs(invoice: SponsoringInvoice): List<Pair<Team, SponsoringInvoice>> {
val teamsFromChallenges = invoice.challenges.map { it.team }
val teamsFromSponsorings = invoice.sponsorings.map { it.team }
val teamsFromSponsorings = invoice.sponsorings.flatMap { it.teams }
val teamsFromBoth = teamsFromChallenges.union(teamsFromSponsorings).distinct()
return teamsFromBoth.filterNotNull().map { it to invoice }
}
Expand Down
35 changes: 24 additions & 11 deletions src/main/java/backend/model/sponsoring/Sponsoring.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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
}
Expand All @@ -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()
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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) {}
Expand All @@ -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
Expand Down Expand Up @@ -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
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 unregistedSponsor.team do this or any of the teams in the teams collection? Ideally it's the same team anyway.

We need to check this.

this.registeredSponsor != null -> EmailAddress(this.registeredSponsor!!.email) == EmailAddress(username)
else -> throw Exception("Error checking withdrawal permissions")
}
Expand Down Expand Up @@ -141,8 +153,9 @@ class Sponsoring : BasicEntity, Billable {
}

override fun billableAmount(): Money {

val distance: Double = team?.getCurrentDistance() ?: run {
val distance: Double = if (teams.isNotEmpty())
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
}
Expand All @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,16 @@ import org.springframework.data.repository.CrudRepository
import org.springframework.data.repository.query.Param

interface SponsoringRepository : CrudRepository<Sponsoring, Long> {
fun findByTeamId(teamId: Long): Iterable<Sponsoring>
@Query("select sp from Sponsoring sp join sp.teams as t where t.id = :teamId")
fun findByTeamId(@Param("teamId") teamId: Long): Iterable<Sponsoring>

@Query("Select s from Sponsoring s where s.registeredSponsor.account.id = :id")
fun findBySponsorAccountId(@Param("id") teamId: Long): Iterable<Sponsoring>

@Query("select s from Sponsoring sp join sp.registeredSponsor as s where sp.team.event.id = :eventId")
@Query("select s from Sponsoring sp join sp.registeredSponsor as s where sp.event.id = :eventId")
fun findAllRegisteredSponsorsWithSponsoringsAtEvent(@Param("eventId") eventId: Long): Iterable<Sponsor>

@Query("select s from Sponsoring sp join sp.unregisteredSponsor as s where sp.team.event.id = :eventId")
@Query("select s from Sponsoring sp join sp.unregisteredSponsor as s where sp.event.id = :eventId")
fun findAllUnregisteredSponsorsWithSponsoringsAtEvent(@Param("eventId") eventId: Long): Iterable<UnregisteredSponsor>
}

11 changes: 6 additions & 5 deletions src/main/java/backend/model/sponsoring/SponsoringService.kt
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)")
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 isTeamMember instead.

fun createSponsoringWithOfflineSponsor(event: Event, amountPerKm: Money, limit: Money, unregisteredSponsor: UnregisteredSponsor): Sponsoring

@PreAuthorize("hasAuthority('ADMIN')")
fun sendEmailsToSponsorsWhenEventHasStarted()
Expand Down
Loading