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
@@ -42,6 +42,8 @@ data class Buchung constructor(
@Serializable
enum class BuchungsTyp {
NENNGEBUEHR,
NENNGELD,
NACHNENNGEBUEHR,
STARTGEBUEHR,
BOXENGEBUEHR,
ZAHLUNG_BAR,
@@ -58,7 +58,7 @@ class BillingController(
@GetMapping("/konten/{kontoId}")
fun getKonto(@PathVariable kontoId: String): ResponseEntity<KontoDto> {
val uuid = try { Uuid.parse(kontoId) } catch (e: Exception) { return ResponseEntity.badRequest().build() }
val uuid = try { Uuid.parse(kontoId) } catch (_: Exception) { return ResponseEntity.badRequest().build() }
val konto = kontoService.getKontoById(uuid) ?: return ResponseEntity.notFound().build()
return ResponseEntity.ok(konto.toDto())
}
@@ -68,8 +68,8 @@ class BillingController(
@RequestParam veranstaltungId: String,
@RequestParam personId: String
): ResponseEntity<KontoDto> {
val vUuid = try { Uuid.parse(veranstaltungId) } catch (e: Exception) { return ResponseEntity.badRequest().build() }
val pUuid = try { Uuid.parse(personId) } catch (e: Exception) { return ResponseEntity.badRequest().build() }
val vUuid = try { Uuid.parse(veranstaltungId) } catch (_: Exception) { return ResponseEntity.badRequest().build() }
val pUuid = try { Uuid.parse(personId) } catch (_: Exception) { return ResponseEntity.badRequest().build() }
val konto = kontoService.getOrCreateKonto(vUuid, pUuid, "Unbekannt") // Name wird bei getOrCreate ggf. ignoriert wenn existiert
return ResponseEntity.ok(konto.toDto())
@@ -77,8 +77,8 @@ class BillingController(
@PostMapping("/konten")
fun createKonto(@Valid @RequestBody request: CreateKontoRequest): ResponseEntity<KontoDto> {
val vUuid = try { Uuid.parse(request.veranstaltungId) } catch (e: Exception) { return ResponseEntity.badRequest().build() }
val pUuid = try { Uuid.parse(request.personId) } catch (e: Exception) { return ResponseEntity.badRequest().build() }
val vUuid = try { Uuid.parse(request.veranstaltungId) } catch (_: Exception) { return ResponseEntity.badRequest().build() }
val pUuid = try { Uuid.parse(request.personId) } catch (_: Exception) { return ResponseEntity.badRequest().build() }
val konto = kontoService.getOrCreateKonto(vUuid, pUuid, request.personName)
return ResponseEntity.ok(konto.toDto())
@@ -86,7 +86,7 @@ class BillingController(
@GetMapping("/konten/{kontoId}/buchungen")
fun getBuchungen(@PathVariable kontoId: String): ResponseEntity<List<BuchungDto>> {
val uuid = try { Uuid.parse(kontoId) } catch (e: Exception) { return ResponseEntity.badRequest().build() }
val uuid = try { Uuid.parse(kontoId) } catch (_: Exception) { return ResponseEntity.badRequest().build() }
val buchungen = kontoService.getBuchungsHistorie(uuid)
return ResponseEntity.ok(buchungen.map { it.toDto() })
}
@@ -96,7 +96,7 @@ class BillingController(
@PathVariable kontoId: String,
@Valid @RequestBody request: BuchungRequest
): ResponseEntity<KontoDto> {
val uuid = try { Uuid.parse(kontoId) } catch (e: Exception) { return ResponseEntity.badRequest().build() }
val uuid = try { Uuid.parse(kontoId) } catch (_: Exception) { return ResponseEntity.badRequest().build() }
val konto = kontoService.buche(
kontoId = uuid,
betragCent = request.betragCent,
@@ -37,6 +37,12 @@ class TeilnehmerKontoService(
}
}
fun getKonto(veranstaltungId: Uuid, personId: Uuid): TeilnehmerKonto? {
return transaction {
kontoRepository.findByVeranstaltungAndPerson(veranstaltungId, personId)
}
}
fun getBuchungsHistorie(kontoId: Uuid): List<Buchung> {
return transaction {
buchungRepository.findByKonto(kontoId)
@@ -9,18 +9,23 @@ import org.jetbrains.exposed.v1.jdbc.transactions.transaction
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Profile
@Configuration
class BillingDatabaseConfiguration(
@Value("\${spring.datasource.url}") private val jdbcUrl: String,
@Value("\${spring.datasource.username}") private val username: String,
@Value("\${spring.datasource.password}") private val password: String
@Value("\${spring.datasource.url:}") private val jdbcUrl: String,
@Value("\${spring.datasource.username:}") private val username: String,
@Value("\${spring.datasource.password:}") private val password: String
) {
private val log = LoggerFactory.getLogger(BillingDatabaseConfiguration::class.java)
@PostConstruct
fun initializeDatabase() {
if (jdbcUrl.isBlank()) {
log.warn("No spring.datasource.url provided. Skipping Billing database initialization.")
return
}
log.info("Initializing database schema for Billing Service...")
try {
Database.connect(jdbcUrl, user = username, password = password)