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:
parent
eef17b3067
commit
c1fadac944
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ dependencies {
|
|||
implementation(projects.platform.platformDependencies)
|
||||
implementation(projects.backend.services.entries.entriesApi)
|
||||
implementation(projects.backend.services.entries.entriesDomain)
|
||||
implementation(projects.backend.services.billing.billingDomain)
|
||||
implementation(projects.backend.services.billing.billingService)
|
||||
implementation(projects.core.coreUtils)
|
||||
implementation(projects.core.coreDomain)
|
||||
implementation(projects.backend.infrastructure.monitoring.monitoringClient)
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ fun main(args: Array<String>) {
|
|||
runApplication<EntriesServiceApplication>(*args)
|
||||
}
|
||||
|
||||
@SpringBootApplication
|
||||
@SpringBootApplication(scanBasePackages = ["at.mocode.entries", "at.mocode.billing"])
|
||||
@EnableAspectJAutoProxy
|
||||
class EntriesServiceApplication {
|
||||
|
||||
|
|
|
|||
|
|
@ -35,6 +35,8 @@ data class Bewerb(
|
|||
val stechenGeplant: Boolean = false,
|
||||
// Finanzen
|
||||
val startgeldCent: Long? = null,
|
||||
val nenngeldCent: Long? = null,
|
||||
val nachnenngebuehrCent: Long? = null,
|
||||
val geldpreisAusbezahlt: Boolean = false,
|
||||
// ZNS-Integration
|
||||
val znsNummer: Int? = null,
|
||||
|
|
|
|||
|
|
@ -72,6 +72,8 @@ class BewerbRepositoryImpl : BewerbRepository {
|
|||
stechenGeplant = row[BewerbTable.stechenGeplant],
|
||||
// Finanzen
|
||||
startgeldCent = row[BewerbTable.startgeldCent],
|
||||
nenngeldCent = row[BewerbTable.nenngeldCent],
|
||||
nachnenngebuehrCent = row[BewerbTable.nachnenngebuehrCent],
|
||||
geldpreisAusbezahlt = row[BewerbTable.geldpreisAusbezahlt],
|
||||
// ZNS-Integration
|
||||
znsNummer = row[BewerbTable.znsNummer],
|
||||
|
|
@ -106,6 +108,8 @@ class BewerbRepositoryImpl : BewerbRepository {
|
|||
s[BewerbTable.stechenGeplant] = b.stechenGeplant
|
||||
// Finanzen
|
||||
s[BewerbTable.startgeldCent] = b.startgeldCent
|
||||
s[BewerbTable.nenngeldCent] = b.nenngeldCent
|
||||
s[BewerbTable.nachnenngebuehrCent] = b.nachnenngebuehrCent
|
||||
s[BewerbTable.geldpreisAusbezahlt] = b.geldpreisAusbezahlt
|
||||
// ZNS-Integration
|
||||
s[BewerbTable.znsNummer] = b.znsNummer
|
||||
|
|
@ -153,6 +157,8 @@ class BewerbRepositoryImpl : BewerbRepository {
|
|||
s[BewerbTable.stechenGeplant] = b.stechenGeplant
|
||||
// Finanzen
|
||||
s[BewerbTable.startgeldCent] = b.startgeldCent
|
||||
s[BewerbTable.nenngeldCent] = b.nenngeldCent
|
||||
s[BewerbTable.nachnenngebuehrCent] = b.nachnenngebuehrCent
|
||||
s[BewerbTable.geldpreisAusbezahlt] = b.geldpreisAusbezahlt
|
||||
// ZNS-Integration
|
||||
s[BewerbTable.znsNummer] = b.znsNummer
|
||||
|
|
|
|||
|
|
@ -36,6 +36,8 @@ object BewerbTable : Table("bewerbe") {
|
|||
|
||||
// Finanzen
|
||||
val startgeldCent = long("startgeld_cent").nullable()
|
||||
val nenngeldCent = long("nenngeld_cent").nullable()
|
||||
val nachnenngebuehrCent = long("nachnenngebuehr_cent").nullable()
|
||||
val geldpreisAusbezahlt = bool("geldpreis_ausbezahlt").default(false)
|
||||
|
||||
// ZNS-Integration
|
||||
|
|
|
|||
|
|
@ -10,6 +10,11 @@ suspend inline fun <T> tenantTransaction(crossinline block: () -> T): T = transa
|
|||
val schema = TenantContextHolder.current()?.schemaName
|
||||
?: error("No tenant in context. Ensure TenantWebFilter is installed and request has X-Event-Id")
|
||||
// Set search_path for this transaction/connection
|
||||
TransactionManager.current().exec("SET search_path TO \"$schema\"")
|
||||
val dialect = TransactionManager.current().db.vendor
|
||||
if (dialect == "postgresql") {
|
||||
TransactionManager.current().exec("SET search_path TO \"$schema\"")
|
||||
} else if (dialect == "h2") {
|
||||
TransactionManager.current().exec("SET SCHEMA \"$schema\"")
|
||||
}
|
||||
block()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,12 +2,15 @@
|
|||
|
||||
package at.mocode.entries.service.usecase
|
||||
|
||||
import at.mocode.billing.domain.model.BuchungsTyp
|
||||
import at.mocode.billing.service.TeilnehmerKontoService
|
||||
import at.mocode.core.domain.model.NennStatusE
|
||||
import at.mocode.entries.api.*
|
||||
import at.mocode.entries.domain.model.Nennung
|
||||
import at.mocode.entries.domain.model.NennungsTransfer
|
||||
import at.mocode.entries.domain.repository.NennungRepository
|
||||
import at.mocode.entries.domain.repository.NennungsTransferRepository
|
||||
import at.mocode.entries.service.bewerbe.BewerbRepository
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.stereotype.Service
|
||||
import kotlin.uuid.Uuid
|
||||
|
|
@ -23,7 +26,9 @@ import kotlin.uuid.Uuid
|
|||
@Service
|
||||
class NennungUseCases(
|
||||
private val nennungRepository: NennungRepository,
|
||||
private val transferRepository: NennungsTransferRepository
|
||||
private val transferRepository: NennungsTransferRepository,
|
||||
private val bewerbRepository: BewerbRepository,
|
||||
private val kontoService: TeilnehmerKontoService
|
||||
) {
|
||||
private val log = LoggerFactory.getLogger(NennungUseCases::class.java)
|
||||
|
||||
|
|
@ -74,6 +79,42 @@ class NennungUseCases(
|
|||
)
|
||||
val saved = nennungRepository.save(nennung)
|
||||
log.info("Nennung eingereicht: nennungId={} turnierId={}", saved.nennungId, saved.turnierId)
|
||||
|
||||
// Automatische Buchung der Gebühren (Nenngeld / Nachnenngebühr)
|
||||
// Wir nutzen den zahlerId, oder falls nicht gesetzt den reiterId
|
||||
val zahlerId = saved.zahlerId ?: saved.reiterId
|
||||
val bewerb = bewerbRepository.findById(saved.bewerbId)
|
||||
if (bewerb != null && (bewerb.nenngeldCent != null || bewerb.nachnenngebuehrCent != null)) {
|
||||
try {
|
||||
val konto = kontoService.getOrCreateKonto(saved.turnierId, zahlerId, "Zahler für Nennung ${saved.nennungId}")
|
||||
|
||||
// Nenngeld buchen
|
||||
if (bewerb.nenngeldCent != null && bewerb.nenngeldCent > 0) {
|
||||
kontoService.buche(
|
||||
kontoId = konto.kontoId,
|
||||
betragCent = -bewerb.nenngeldCent, // Gebühr ist negativ
|
||||
typ = BuchungsTyp.NENNGELD,
|
||||
zweck = "Nenngeld Bewerb ${bewerb.bezeichnung} (${bewerb.klasse})"
|
||||
)
|
||||
}
|
||||
|
||||
// Nachnenngebühr buchen (falls fällig und nicht erlassen)
|
||||
if (saved.isNachnenngebuehrFaellig() && bewerb.nachnenngebuehrCent != null && bewerb.nachnenngebuehrCent > 0) {
|
||||
kontoService.buche(
|
||||
kontoId = konto.kontoId,
|
||||
betragCent = -bewerb.nachnenngebuehrCent, // Gebühr ist negativ
|
||||
typ = BuchungsTyp.NACHNENNGEBUEHR,
|
||||
zweck = "Nachnenngebühr Bewerb ${bewerb.bezeichnung}"
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
log.error("Fehler bei der automatischen Buchung für Nennung {}: {}", saved.nennungId, e.message, e)
|
||||
// Wir lassen die Nennung bestehen, loggen aber den Fehler.
|
||||
// In einem echten System würde man hier evtl. ein Domain Event publizieren
|
||||
// oder die Transaktion rollbacken (wenn gewünscht).
|
||||
}
|
||||
}
|
||||
|
||||
return saved.toDetailDto()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ CREATE TABLE IF NOT EXISTS control.tenants (
|
|||
db_url TEXT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'ACTIVE', -- ACTIVE | READ_ONLY | LOCKED
|
||||
version INT NOT NULL DEFAULT 1,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- Index to speed up lookups by status when we add list operations later
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
-- Migration: Hinzufügen von Finanz-Feldern zu Bewerben für automatische Buchung
|
||||
ALTER TABLE bewerbe ADD COLUMN IF NOT EXISTS nenngeld_cent BIGINT;
|
||||
ALTER TABLE bewerbe ADD COLUMN IF NOT EXISTS nachnenngebuehr_cent BIGINT;
|
||||
|
|
@ -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 })
|
||||
}
|
||||
}
|
||||
|
|
@ -16,13 +16,19 @@ In dieser Session wurde das Fundament für den Kassa-Service (`billing-context`)
|
|||
* `billing-service` Modul erstellt, konfiguriert und mit `core-domain` (Serialisierung) verknüpft.
|
||||
* Exposed-Tabellendefinitionen (v1) für `TeilnehmerKonto` und `Buchung` implementiert.
|
||||
* `BillingController` mit REST-Endpunkten für Konten, Buchungen und Historie erstellt.
|
||||
* `TeilnehmerKontoService` um API-Methoden (`getKontoById`, `getBuchungsHistorie`, `buche`) erweitert.
|
||||
* `TeilnehmerKontoService` um API-Methoden (`getKontoById`, `getKonto`, `getBuchungsHistorie`, `buche`) erweitert.
|
||||
* Integrationstests (`TeilnehmerKontoServiceTest`) erfolgreich mit H2-In-Memory-DB durchgeführt.
|
||||
* **OpenAPI-Dokumentation:** `documentation.yaml` für `billing-service` erstellt und CRUD-Endpunkte für Konten und Buchungen dokumentiert.
|
||||
2. **ZNS-Importer Hardening:**
|
||||
2. **Entries-Integration (Neu):**
|
||||
* Automatische Buchung von Nenngeld und Nachnenngebühren bei Einreichung einer Nennung implementiert.
|
||||
* Erweiterung der `Bewerb`-Entität um Finanzfelder (`nenngeld_cent`, `nachnenngebuehr_cent`).
|
||||
* Neue Flyway-Migration `V8__add_bewerb_financial_fields.sql` im `entries-service` hinzugefügt.
|
||||
* `NennungUseCases` nutzt nun den `TeilnehmerKontoService` zur automatischen Belastung der Teilnehmerkonten (negativer Saldo).
|
||||
* `EntriesServiceApplication` scannt nun auch `at.mocode.billing` Pakete für die Cross-Context Integration.
|
||||
3. **ZNS-Importer Hardening:**
|
||||
* Erweiterung von `ZnsImportServiceTest` um Tests für mehrfache Qualifikationen und die Update-Strategie (Delete+Insert) bei Funktionären (`RICHT01.dat`).
|
||||
* Alle 11 Integrationstests sind erfolgreich durchgelaufen.
|
||||
3. **Kompilations-Fixes (Billing):**
|
||||
4. **Kompilations-Fixes (Billing):**
|
||||
* `billing-service` auf korrekte Exposed DSL Syntax (`selectAll().where { ... }`) umgestellt.
|
||||
* Explizite `transaction { ... }` Blöcke in `TeilnehmerKontoService` eingeführt.
|
||||
* Typ-Konsistenz für `Instant` (kotlin.time) in `billing-domain` zur Übereinstimmung mit `core-domain` hergestellt.
|
||||
|
|
@ -32,9 +38,15 @@ In dieser Session wurde das Fundament für den Kassa-Service (`billing-context`)
|
|||
- `backend/infrastructure/zns-importer/src/test/kotlin/at/mocode/zns/importer/ZnsImportServiceTest.kt`
|
||||
|
||||
### Nächste Schritte
|
||||
- Integration des Billing-Services in den `entries-context` (z.B. automatische Buchung von Nenngebühren).
|
||||
- [x] Integration des Billing-Services in den `entries-context` (automatische Buchung bei Nennung).
|
||||
- [x] Fix von Kompilationsfehlern und Test-Regressionen (H2/Exposed Kompatibilität).
|
||||
- UI-Anbindung im Frontend für Kontenübersicht und manuelle Buchungen.
|
||||
- Erweiterung der Abrechnungs-Logik (z.B. Rechnungserstellung als PDF).
|
||||
|
||||
### Technische Details & Fixes
|
||||
- **Exposed / H2:** `TIMESTAMPTZ` in Flyway-Migrationen auf `TIMESTAMP WITH TIME ZONE` umgestellt, um H2-Kompatibilität in Integrationstests zu gewährleisten.
|
||||
- **Multi-Tenancy:** `ExposedTenantTransactions` unterstützt nun sowohl PostgreSQL (`SET search_path`) als auch H2 (`SET SCHEMA`).
|
||||
- **Billing Config:** `BillingDatabaseConfiguration` ist nun robust gegen fehlende JDBC-URLs (wichtig für modularisierte Tests).
|
||||
|
||||
---
|
||||
*Co-authored-by: Junie <junie@jetbrains.com>*
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user