feat(entries+billing): integrate automatic fee booking for entries with billing service

- **Entries-Service Updates:**
  - Implemented automatic booking of fees (entry fees and late fees) during entry submission using `TeilnehmerKontoService`.
  - Enhanced `Bewerb` entity with financial fields (`nenngeldCent`, `nachnenngebuehrCent`).
  - Added Flyway migration to update `bewerbe` table with new financial fields.
  - Updated `EntriesServiceApplication` to include billing package scanning for integration.

- **Billing-Service Enhancements:**
  - Adjusted `TeilnehmerKontoService` to support fetching accounts by event and person.
  - Improved database configuration to handle missing JDBC URLs during tests.

- **Tests:**
  - Added integration tests to validate fee booking logic for entries, including late fee scenarios.
  - Introduced H2 database setup for test isolation.

- **Misc:**
  - Updated tenant-aware transactions to support H2 and PostgreSQL dialects.
  - Adjusted log and error handling for robust integration between services.
This commit is contained in:
2026-04-10 12:48:57 +02:00
parent eef17b3067
commit c1fadac944
16 changed files with 259 additions and 19 deletions
@@ -1,6 +1,10 @@
package at.mocode.entries.service.config
import at.mocode.billing.service.persistence.BuchungTable
import at.mocode.billing.service.persistence.TeilnehmerKontoTable
import org.jetbrains.exposed.v1.jdbc.Database
import org.jetbrains.exposed.v1.jdbc.SchemaUtils
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Profile
@@ -9,6 +13,7 @@ import javax.sql.DataSource
/**
* Verbindet Exposed mit der Spring-DataSource im Test-Profil.
* Ersetzt die EntriesDatabaseConfiguration (die mit @Profile("!test") ausgeschlossen ist).
* Initialisiert auch das Billing-Schema, da BillingDatabaseConfiguration im Test ebenfalls ausgeschlossen ist.
*/
@Configuration
@Profile("test")
@@ -16,6 +21,13 @@ class TestExposedConfiguration {
@Bean
fun exposedDatabase(dataSource: DataSource): Database {
return Database.connect(dataSource)
val db = Database.connect(dataSource)
transaction(db) {
SchemaUtils.create(
TeilnehmerKontoTable,
BuchungTable
)
}
return db
}
}
@@ -0,0 +1,142 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.entries.service.usecase
import at.mocode.billing.domain.model.BuchungsTyp
import at.mocode.billing.service.TeilnehmerKontoService
import at.mocode.entries.api.NennungEinreichenRequest
import at.mocode.entries.service.bewerbe.Bewerb
import at.mocode.entries.service.bewerbe.BewerbRepository
import at.mocode.entries.service.persistence.AbteilungTable
import at.mocode.entries.service.persistence.BewerbRichterEinsatzTable
import at.mocode.entries.service.persistence.BewerbTable
import at.mocode.entries.service.persistence.NennungTable
import at.mocode.entries.service.tenant.Tenant
import at.mocode.entries.service.tenant.TenantContextHolder
import at.mocode.entries.service.tenant.tenantTransaction
import org.jetbrains.exposed.v1.jdbc.deleteAll
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.context.ActiveProfiles
import kotlin.uuid.Uuid
@SpringBootTest
@ActiveProfiles("test")
class NennungBillingIntegrationTest {
@Autowired
private lateinit var nennungUseCases: NennungUseCases
@Autowired
private lateinit var bewerbRepository: BewerbRepository
@Autowired
private lateinit var kontoService: TeilnehmerKontoService
private val turnierId = Uuid.random()
private val reiterId = Uuid.random()
private val pferdId = Uuid.random()
private val abteilungId = Uuid.random()
@BeforeEach
fun setup() {
// We use PUBLIC schema in H2 for simplicity in this integration test
TenantContextHolder.set(Tenant(turnierId.toString(), "PUBLIC", "jdbc:h2:mem:entries-test"))
// Ensure tables exist in H2 (Flyway might have run on public already, but let's be sure)
kotlinx.coroutines.runBlocking {
tenantTransaction {
org.jetbrains.exposed.v1.jdbc.SchemaUtils.create(
NennungTable,
BewerbTable,
AbteilungTable,
BewerbRichterEinsatzTable
)
NennungTable.deleteAll()
BewerbRichterEinsatzTable.deleteAll()
BewerbTable.deleteAll()
AbteilungTable.deleteAll()
}
}
}
@AfterEach
fun teardown() {
TenantContextHolder.clear()
}
@Test
fun `nennung einreichen bucht automatisch Nenngeld`() = kotlinx.coroutines.runBlocking {
// GIVEN: Ein Bewerb mit Nenngeld
val bewerb = bewerbRepository.create(Bewerb(
id = Uuid.random(),
turnierId = turnierId,
klasse = "L",
bezeichnung = "Standardspringprüfung",
nenngeldCent = 2500, // 25,00 EUR
hoeheCm = 120
))
val request = NennungEinreichenRequest(
turnierId = turnierId,
bewerbId = bewerb.id,
abteilungId = abteilungId,
reiterId = reiterId,
pferdId = pferdId,
istNachnennung = false
)
// WHEN: Nennung einreichen
val result = nennungUseCases.nennungEinreichen(request)
// THEN: Konto muss existieren und Saldo muss -25,00 EUR sein (Gebühr)
val konto = kontoService.getKonto(turnierId, reiterId)
assertNotNull(konto, "Konto sollte automatisch erstellt worden sein")
assertEquals(-2500L, konto?.saldoCent)
val buchungen = kontoService.getBuchungsHistorie(konto!!.kontoId)
assertEquals(1, buchungen.size)
assertEquals(BuchungsTyp.NENNGELD, buchungen[0].typ)
assertEquals(-2500L, buchungen[0].betragCent)
}
@Test
fun `nachnennung bucht zusätzlich Nachnenngebühr`() = kotlinx.coroutines.runBlocking {
// GIVEN: Ein Bewerb mit Nenngeld und Nachnenngebühr
val bewerb = bewerbRepository.create(Bewerb(
id = Uuid.random(),
turnierId = turnierId,
klasse = "M",
bezeichnung = "Zeitspringprüfung",
nenngeldCent = 3000,
nachnenngebuehrCent = 1500,
hoeheCm = 130
))
val request = NennungEinreichenRequest(
turnierId = turnierId,
bewerbId = bewerb.id,
abteilungId = abteilungId,
reiterId = reiterId,
pferdId = pferdId,
istNachnennung = true
)
// WHEN: Nennung einreichen
nennungUseCases.nennungEinreichen(request)
// THEN: Saldo muss -45,00 EUR sein (-30 - 15)
val konto = kontoService.getKonto(turnierId, reiterId)
assertEquals(-4500L, konto?.saldoCent)
val buchungen = kontoService.getBuchungsHistorie(konto!!.kontoId)
assertEquals(2, buchungen.size)
// Einer muss NACHNENNGEBUEHR sein
assertNotNull(buchungen.find { it.typ == BuchungsTyp.NACHNENNGEBUEHR })
}
}