feat(billing): introduce billing domain and service infrastructure
- **Billing Domain:** - Added Kotlin Multiplatform project with domain models (`TeilnehmerKonto`, `Buchung`, `BuchungsTyp`) to represent billing entities. - Defined serialization strategies using `InstantSerializer`. - **Service Implementation:** - Introduced `BillingServiceApplication` as the main entry point for the billing service. - Developed `TeilnehmerKontoService` for account management and transactions. - **Persistence Layer:** - Implemented Exposed repositories (`ExposedTeilnehmerKontoRepository`, `ExposedBillingRepositories`) for database interaction. - Added table definitions (`TeilnehmerKontoTable`, `BuchungTable`) with indexes for efficient querying. - **Build Configuration:** - Setup Gradle build files for billing domain and service modules with dependencies for Kotlin, Serialization, Spring Boot, and Exposed. - **Test Additions:** - Extended ZNS importer tests with new scenarios for qualification parsing
This commit is contained in:
parent
bab95d14f4
commit
21f3a57e6e
|
|
@ -306,4 +306,52 @@ class ZnsImportServiceTest {
|
|||
assertThat(result.gesamtAktualisiert).isEqualTo(0)
|
||||
assertThat(result.fehler).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `importiereZip - Funktionaer mit mehrfachen Qualifikationen`() = runTest {
|
||||
// Zeile mit vielen Qualifikationen (Satznummer X014346)
|
||||
val qualifikationen = "DM,DPF,GAR-SP,SPF,SS*,RD,RS"
|
||||
val zeile = "X014346Schubert Renate $qualifikationen"
|
||||
val zip = buildZip("RICHT01.DAT" to zeile)
|
||||
|
||||
coEvery { funktionaerRepository.findBySatz("X", 14346) } returns null
|
||||
coEvery { funktionaerRepository.save(any()) } answers { firstArg<Funktionaer>() }
|
||||
coEvery { reiterRepository.findByName(any(), any()) } returns emptyList()
|
||||
|
||||
val result = service.importiereZip(zip)
|
||||
|
||||
assertThat(result.richterImportiert).isEqualTo(1)
|
||||
coVerify {
|
||||
funktionaerRepository.save(match { f ->
|
||||
f.qualifikationen.size == 7 &&
|
||||
f.qualifikationen.containsAll(listOf("DM", "DPF", "GAR-SP", "SPF", "SS*", "RD", "RS"))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `importiereZip - Funktionaer Update Strategie (Delete+Insert)`() = runTest {
|
||||
val zeile = funktionaerZeile(typ = "X", satznummer = "123456", name = "Geaendert Name")
|
||||
val zip = buildZip("RICHT01.DAT" to zeile)
|
||||
|
||||
val existing = Funktionaer(
|
||||
funktionaerId = kotlin.uuid.Uuid.random(),
|
||||
satzId = "X",
|
||||
satzNummer = 123456,
|
||||
name = "Alt Name"
|
||||
)
|
||||
|
||||
coEvery { funktionaerRepository.findBySatz("X", 123456) } returns existing
|
||||
coEvery { funktionaerRepository.save(any()) } answers { firstArg<Funktionaer>() }
|
||||
coEvery { reiterRepository.findByName(any(), any()) } returns emptyList()
|
||||
|
||||
val result = service.importiereZip(zip)
|
||||
|
||||
assertThat(result.richterAktualisiert).isEqualTo(1)
|
||||
coVerify {
|
||||
funktionaerRepository.save(match { f ->
|
||||
f.funktionaerId == existing.funktionaerId && f.name == "Geaendert Name"
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
26
backend/services/billing/billing-domain/build.gradle.kts
Normal file
26
backend/services/billing/billing-domain/build.gradle.kts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
plugins {
|
||||
alias(libs.plugins.kotlinMultiplatform)
|
||||
alias(libs.plugins.kotlinSerialization)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvm()
|
||||
js(IR) {
|
||||
browser()
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
val commonMain by getting {
|
||||
dependencies {
|
||||
implementation(projects.core.coreDomain)
|
||||
implementation(projects.core.coreUtils)
|
||||
implementation(libs.kotlinx.datetime)
|
||||
}
|
||||
}
|
||||
val commonTest by getting {
|
||||
dependencies {
|
||||
implementation(kotlin("test"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
@file:OptIn(ExperimentalUuidApi::class)
|
||||
|
||||
package at.mocode.billing.domain.model
|
||||
|
||||
import at.mocode.core.domain.serialization.InstantSerializer
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Instant
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
/**
|
||||
* Repräsentiert das Kassa-Konto eines Teilnehmers (Reiter oder Besitzer).
|
||||
* Ein Konto wird pro Veranstaltung/Turnier geführt, kann aber veranstaltungsübergreifend aggregiert werden.
|
||||
*/
|
||||
@Serializable
|
||||
data class TeilnehmerKonto constructor(
|
||||
val kontoId: Uuid = Uuid.random(),
|
||||
val veranstaltungId: Uuid,
|
||||
val personId: Uuid, // Referenz auf Reiter oder Besitzer
|
||||
val personName: String,
|
||||
val saldoCent: Long = 0L, // Aktueller Kontostand in Cent
|
||||
val bemerkungen: String? = null,
|
||||
@Serializable(with = InstantSerializer::class)
|
||||
val updatedAt: Instant = Clock.System.now()
|
||||
)
|
||||
|
||||
/**
|
||||
* Ein einzelner Buchungsvorgang (Zahlung, Gutschrift, Gebühr).
|
||||
*/
|
||||
@Serializable
|
||||
data class Buchung constructor(
|
||||
val buchungId: Uuid = Uuid.random(),
|
||||
val kontoId: Uuid,
|
||||
val betragCent: Long, // Positiv für Gutschrift/Zahlung, Negativ für Gebühr/Soll
|
||||
val typ: BuchungsTyp,
|
||||
val verwendungszweck: String,
|
||||
@Serializable(with = InstantSerializer::class)
|
||||
val gebuchtAm: Instant = Clock.System.now()
|
||||
)
|
||||
|
||||
@Serializable
|
||||
enum class BuchungsTyp {
|
||||
NENNGEBUEHR,
|
||||
STARTGEBUEHR,
|
||||
BOXENGEBUEHR,
|
||||
ZAHLUNG_BAR,
|
||||
ZAHLUNG_KARTE,
|
||||
GUTSCHRIFT,
|
||||
STORNIERUNG
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
@file:OptIn(ExperimentalUuidApi::class)
|
||||
|
||||
package at.mocode.billing.domain.repository
|
||||
|
||||
import at.mocode.billing.domain.model.Buchung
|
||||
import at.mocode.billing.domain.model.TeilnehmerKonto
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
/**
|
||||
* Repository für den Zugriff auf Teilnehmer-Konten.
|
||||
*/
|
||||
interface TeilnehmerKontoRepository {
|
||||
fun findByVeranstaltungAndPerson(veranstaltungId: Uuid, personId: Uuid): TeilnehmerKonto?
|
||||
fun findById(kontoId: Uuid): TeilnehmerKonto?
|
||||
fun save(konto: TeilnehmerKonto): TeilnehmerKonto
|
||||
fun updateSaldo(kontoId: Uuid, saldoCent: Long): Long
|
||||
}
|
||||
|
||||
/**
|
||||
* Repository für den Zugriff auf Buchungen.
|
||||
*/
|
||||
interface BuchungRepository {
|
||||
fun findByKonto(kontoId: Uuid): List<Buchung>
|
||||
fun save(buchung: Buchung): Buchung
|
||||
}
|
||||
40
backend/services/billing/billing-service/build.gradle.kts
Normal file
40
backend/services/billing/billing-service/build.gradle.kts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
plugins {
|
||||
alias(libs.plugins.kotlinJvm)
|
||||
alias(libs.plugins.spring.boot)
|
||||
alias(libs.plugins.spring.dependencyManagement)
|
||||
alias(libs.plugins.kotlinSpring)
|
||||
}
|
||||
|
||||
springBoot {
|
||||
mainClass.set("at.mocode.billing.service.BillingServiceApplicationKt")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Interne Module
|
||||
implementation(projects.platform.platformDependencies)
|
||||
implementation(projects.core.coreUtils)
|
||||
implementation(projects.backend.services.billing.billingDomain)
|
||||
|
||||
// Spring Boot Starters
|
||||
implementation(libs.spring.boot.starter.web)
|
||||
implementation(libs.spring.boot.starter.validation)
|
||||
implementation(libs.spring.boot.starter.actuator)
|
||||
implementation(libs.jackson.module.kotlin)
|
||||
|
||||
// Datenbank-Abhängigkeiten
|
||||
implementation(libs.exposed.core)
|
||||
implementation(libs.exposed.dao)
|
||||
implementation(libs.exposed.jdbc)
|
||||
implementation(libs.exposed.kotlin.datetime)
|
||||
implementation(libs.hikari.cp)
|
||||
runtimeOnly(libs.postgresql.driver)
|
||||
testRuntimeOnly(libs.h2.driver)
|
||||
|
||||
// Testing
|
||||
testImplementation(projects.platform.platformTesting)
|
||||
testImplementation(libs.spring.boot.starter.test)
|
||||
}
|
||||
|
||||
tasks.test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
@file:OptIn(ExperimentalUuidApi::class)
|
||||
|
||||
package at.mocode.billing.service
|
||||
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.runApplication
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
|
||||
@SpringBootApplication
|
||||
class BillingServiceApplication
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
runApplication<BillingServiceApplication>(*args)
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
@file:OptIn(ExperimentalUuidApi::class)
|
||||
|
||||
package at.mocode.billing.service
|
||||
|
||||
import at.mocode.billing.domain.model.Buchung
|
||||
import at.mocode.billing.domain.model.BuchungsTyp
|
||||
import at.mocode.billing.domain.model.TeilnehmerKonto
|
||||
import at.mocode.billing.domain.repository.BuchungRepository
|
||||
import at.mocode.billing.domain.repository.TeilnehmerKontoRepository
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import org.springframework.stereotype.Service
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
@Service
|
||||
class TeilnehmerKontoService(
|
||||
private val kontoRepository: TeilnehmerKontoRepository,
|
||||
private val buchungRepository: BuchungRepository
|
||||
) {
|
||||
|
||||
fun getOrCreateKonto(veranstaltungId: Uuid, personId: Uuid, personName: String): TeilnehmerKonto {
|
||||
return transaction {
|
||||
kontoRepository.findByVeranstaltungAndPerson(veranstaltungId, personId)
|
||||
?: kontoRepository.save(
|
||||
TeilnehmerKonto(
|
||||
veranstaltungId = veranstaltungId,
|
||||
personId = personId,
|
||||
personName = personName
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun bucheBetrag(kontoId: Uuid, betragCent: Long, typ: BuchungsTyp, verwendungszweck: String): Buchung {
|
||||
return transaction {
|
||||
val konto = kontoRepository.findById(kontoId) ?: throw IllegalArgumentException("Konto nicht gefunden: $kontoId")
|
||||
|
||||
val buchung = Buchung(
|
||||
kontoId = kontoId,
|
||||
betragCent = betragCent,
|
||||
typ = typ,
|
||||
verwendungszweck = verwendungszweck
|
||||
)
|
||||
|
||||
val neueBuchung = buchungRepository.save(buchung)
|
||||
kontoRepository.updateSaldo(kontoId, konto.saldoCent + betragCent)
|
||||
|
||||
neueBuchung
|
||||
}
|
||||
}
|
||||
|
||||
fun getBuchungen(kontoId: Uuid): List<Buchung> {
|
||||
return buchungRepository.findByKonto(kontoId)
|
||||
}
|
||||
|
||||
fun getKonto(kontoId: Uuid): TeilnehmerKonto? {
|
||||
return kontoRepository.findById(kontoId)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
@file:OptIn(ExperimentalUuidApi::class)
|
||||
|
||||
package at.mocode.billing.service.persistence
|
||||
|
||||
import org.jetbrains.exposed.v1.core.Table
|
||||
import org.jetbrains.exposed.v1.datetime.CurrentTimestamp
|
||||
import org.jetbrains.exposed.v1.datetime.timestamp
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
|
||||
/**
|
||||
* Exposed-Tabellendefinition für das Teilnehmer-Konto.
|
||||
*/
|
||||
object TeilnehmerKontoTable : Table("teilnehmer_konten") {
|
||||
val id = uuid("konto_id")
|
||||
val veranstaltungId = uuid("veranstaltung_id")
|
||||
val personId = uuid("person_id")
|
||||
val personName = varchar("person_name", 200)
|
||||
val saldoCent = long("saldo_cent").default(0L)
|
||||
val bemerkungen = text("bemerkungen").nullable()
|
||||
|
||||
val createdAt = timestamp("created_at").defaultExpression(CurrentTimestamp)
|
||||
val updatedAt = timestamp("updated_at").defaultExpression(CurrentTimestamp)
|
||||
|
||||
override val primaryKey = PrimaryKey(id)
|
||||
|
||||
init {
|
||||
index("idx_konto_veranstaltung_person", isUnique = true, veranstaltungId, personId)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exposed-Tabellendefinition für Buchungen.
|
||||
*/
|
||||
object BuchungTable : Table("buchungen") {
|
||||
val id = uuid("buchung_id")
|
||||
val kontoId = uuid("konto_id")
|
||||
val betragCent = long("betrag_cent")
|
||||
val typ = varchar("typ", 50)
|
||||
val verwendungszweck = varchar("verwendungszweck", 500)
|
||||
val gebuchtAm = timestamp("gebucht_am").defaultExpression(CurrentTimestamp)
|
||||
|
||||
override val primaryKey = PrimaryKey(id)
|
||||
|
||||
init {
|
||||
index("idx_buchung_konto", isUnique = false, kontoId)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
@file:OptIn(ExperimentalUuidApi::class)
|
||||
|
||||
package at.mocode.billing.service.persistence
|
||||
|
||||
import at.mocode.billing.domain.model.Buchung
|
||||
import at.mocode.billing.domain.model.BuchungsTyp
|
||||
import at.mocode.billing.domain.model.TeilnehmerKonto
|
||||
import at.mocode.billing.domain.repository.BuchungRepository
|
||||
import at.mocode.billing.domain.repository.TeilnehmerKontoRepository
|
||||
import org.jetbrains.exposed.v1.core.ResultRow
|
||||
import org.jetbrains.exposed.v1.core.and
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
import org.jetbrains.exposed.v1.datetime.CurrentTimestamp
|
||||
import org.jetbrains.exposed.v1.jdbc.insert
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.update
|
||||
import org.springframework.stereotype.Repository
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
@Repository
|
||||
class ExposedTeilnehmerKontoRepository : TeilnehmerKontoRepository {
|
||||
|
||||
override fun findByVeranstaltungAndPerson(veranstaltungId: Uuid, personId: Uuid): TeilnehmerKonto? {
|
||||
return TeilnehmerKontoTable
|
||||
.selectAll()
|
||||
.where { (TeilnehmerKontoTable.veranstaltungId eq veranstaltungId) and (TeilnehmerKontoTable.personId eq personId) }
|
||||
.singleOrNull()
|
||||
?.toModel()
|
||||
}
|
||||
|
||||
override fun findById(kontoId: Uuid): TeilnehmerKonto? {
|
||||
return TeilnehmerKontoTable
|
||||
.selectAll()
|
||||
.where { TeilnehmerKontoTable.id eq kontoId }
|
||||
.singleOrNull()
|
||||
?.toModel()
|
||||
}
|
||||
|
||||
override fun save(konto: TeilnehmerKonto): TeilnehmerKonto {
|
||||
val existing = findById(konto.kontoId)
|
||||
if (existing == null) {
|
||||
TeilnehmerKontoTable.insert {
|
||||
it[id] = konto.kontoId
|
||||
it[veranstaltungId] = konto.veranstaltungId
|
||||
it[personId] = konto.personId
|
||||
it[personName] = konto.personName
|
||||
it[saldoCent] = konto.saldoCent
|
||||
it[bemerkungen] = konto.bemerkungen
|
||||
}
|
||||
} else {
|
||||
TeilnehmerKontoTable.update({ TeilnehmerKontoTable.id eq konto.kontoId }) {
|
||||
it[personName] = konto.personName
|
||||
it[saldoCent] = konto.saldoCent
|
||||
it[bemerkungen] = konto.bemerkungen
|
||||
it[updatedAt] = CurrentTimestamp
|
||||
}
|
||||
}
|
||||
return findById(konto.kontoId)!!
|
||||
}
|
||||
|
||||
override fun updateSaldo(kontoId: Uuid, saldoCent: Long): Long {
|
||||
TeilnehmerKontoTable.update({ TeilnehmerKontoTable.id eq kontoId }) {
|
||||
it[this.saldoCent] = saldoCent
|
||||
it[updatedAt] = CurrentTimestamp
|
||||
}
|
||||
return saldoCent
|
||||
}
|
||||
|
||||
private fun ResultRow.toModel() = TeilnehmerKonto(
|
||||
kontoId = this[TeilnehmerKontoTable.id],
|
||||
veranstaltungId = this[TeilnehmerKontoTable.veranstaltungId],
|
||||
personId = this[TeilnehmerKontoTable.personId],
|
||||
personName = this[TeilnehmerKontoTable.personName],
|
||||
saldoCent = this[TeilnehmerKontoTable.saldoCent],
|
||||
bemerkungen = this[TeilnehmerKontoTable.bemerkungen],
|
||||
updatedAt = this[TeilnehmerKontoTable.updatedAt]
|
||||
)
|
||||
}
|
||||
|
||||
@Repository
|
||||
class ExposedBuchungRepository : BuchungRepository {
|
||||
|
||||
override fun findByKonto(kontoId: Uuid): List<Buchung> {
|
||||
return BuchungTable
|
||||
.selectAll()
|
||||
.where { BuchungTable.kontoId eq kontoId }
|
||||
.map { it.toModel() }
|
||||
}
|
||||
|
||||
override fun save(buchung: Buchung): Buchung {
|
||||
BuchungTable.insert {
|
||||
it[id] = buchung.buchungId
|
||||
it[kontoId] = buchung.kontoId
|
||||
it[betragCent] = buchung.betragCent
|
||||
it[typ] = buchung.typ.name
|
||||
it[verwendungszweck] = buchung.verwendungszweck
|
||||
it[gebuchtAm] = buchung.gebuchtAm
|
||||
}
|
||||
return buchung
|
||||
}
|
||||
|
||||
private fun ResultRow.toModel() = Buchung(
|
||||
buchungId = this[BuchungTable.id],
|
||||
kontoId = this[BuchungTable.kontoId],
|
||||
betragCent = this[BuchungTable.betragCent],
|
||||
typ = BuchungsTyp.valueOf(this[BuchungTable.typ]),
|
||||
verwendungszweck = this[BuchungTable.verwendungszweck],
|
||||
gebuchtAm = this[BuchungTable.gebuchtAm]
|
||||
)
|
||||
}
|
||||
|
|
@ -8,15 +8,20 @@ servers:
|
|||
- url: http://localhost:8091
|
||||
description: Lokaler Entwicklungs-Server
|
||||
paths:
|
||||
/reiter/search:
|
||||
/reiter:
|
||||
get:
|
||||
summary: Sucht Reiter
|
||||
summary: Alle Reiter abrufen (paginiert)
|
||||
parameters:
|
||||
- name: q
|
||||
- name: limit
|
||||
in: query
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
type: integer
|
||||
default: 100
|
||||
- name: offset
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 0
|
||||
responses:
|
||||
'200':
|
||||
description: Liste von Reitern
|
||||
|
|
@ -26,6 +31,450 @@ paths:
|
|||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Reiter'
|
||||
post:
|
||||
summary: Neuen Reiter erstellen
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ReiterCreateRequest'
|
||||
responses:
|
||||
'201':
|
||||
description: Reiter erstellt
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Reiter'
|
||||
/reiter/{id}:
|
||||
get:
|
||||
summary: Reiter nach ID abrufen
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
responses:
|
||||
'200':
|
||||
description: Reiter Details
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Reiter'
|
||||
'404':
|
||||
description: Nicht gefunden
|
||||
put:
|
||||
summary: Reiter aktualisieren
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ReiterUpdateRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: Reiter aktualisiert
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Reiter'
|
||||
'404':
|
||||
description: Nicht gefunden
|
||||
delete:
|
||||
summary: Reiter löschen
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
responses:
|
||||
'204':
|
||||
description: Erfolgreich gelöscht
|
||||
'404':
|
||||
description: Nicht gefunden
|
||||
/reiter/search:
|
||||
get:
|
||||
summary: Sucht Reiter nach Satznummer
|
||||
parameters:
|
||||
- name: q
|
||||
in: query
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: Liste von Reitern (Satznummer Match)
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Reiter'
|
||||
/reiter/satznummer/{nr}:
|
||||
get:
|
||||
summary: Reiter nach Satznummer suchen
|
||||
parameters:
|
||||
- name: nr
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: Reiter gefunden
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Reiter'
|
||||
'404':
|
||||
description: Nicht gefunden
|
||||
/horse:
|
||||
get:
|
||||
summary: Alle Pferde abrufen (paginiert)
|
||||
parameters:
|
||||
- name: jahrgang
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
- name: limit
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 100
|
||||
- name: offset
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 0
|
||||
responses:
|
||||
'200':
|
||||
description: Liste von Pferden
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Horse'
|
||||
post:
|
||||
summary: Neues Pferd erstellen
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/HorseCreateRequest'
|
||||
responses:
|
||||
'201':
|
||||
description: Pferd erstellt
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Horse'
|
||||
/horse/{id}:
|
||||
get:
|
||||
summary: Pferd nach ID abrufen
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
responses:
|
||||
'200':
|
||||
description: Pferd Details
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Horse'
|
||||
'404':
|
||||
description: Nicht gefunden
|
||||
put:
|
||||
summary: Pferd aktualisieren
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/HorseUpdateRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: Pferd aktualisiert
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Horse'
|
||||
delete:
|
||||
summary: Pferd löschen
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
responses:
|
||||
'204':
|
||||
description: Gelöscht
|
||||
/horse/search:
|
||||
get:
|
||||
summary: Sucht Pferde nach Lebensnummer
|
||||
parameters:
|
||||
- name: q
|
||||
in: query
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: Liste von Pferden
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Horse'
|
||||
/verein:
|
||||
get:
|
||||
summary: Alle Vereine abrufen (paginiert)
|
||||
parameters:
|
||||
- name: limit
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 100
|
||||
- name: offset
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 0
|
||||
responses:
|
||||
'200':
|
||||
description: Liste von Vereinen
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Verein'
|
||||
post:
|
||||
summary: Neuen Verein erstellen
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/VereinCreateRequest'
|
||||
responses:
|
||||
'201':
|
||||
description: Verein erstellt
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Verein'
|
||||
/verein/{id}:
|
||||
get:
|
||||
summary: Verein nach ID abrufen
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
responses:
|
||||
'200':
|
||||
description: Verein Details
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Verein'
|
||||
'404':
|
||||
description: Nicht gefunden
|
||||
put:
|
||||
summary: Verein aktualisieren
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/VereinUpdateRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: Verein aktualisiert
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Verein'
|
||||
delete:
|
||||
summary: Verein löschen
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
responses:
|
||||
'204':
|
||||
description: Gelöscht
|
||||
/funktionaer:
|
||||
get:
|
||||
summary: Alle Funktionäre abrufen (paginiert)
|
||||
parameters:
|
||||
- name: limit
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 100
|
||||
- name: offset
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 0
|
||||
responses:
|
||||
'200':
|
||||
description: Liste von Funktionären
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Funktionaer'
|
||||
post:
|
||||
summary: Neuen Funktionär erstellen
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/FunktionaerCreateRequest'
|
||||
responses:
|
||||
'201':
|
||||
description: Funktionär erstellt
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Funktionaer'
|
||||
/funktionaer/{id}:
|
||||
get:
|
||||
summary: Funktionär nach ID abrufen
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
responses:
|
||||
'200':
|
||||
description: Funktionär Details
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Funktionaer'
|
||||
'404':
|
||||
description: Nicht gefunden
|
||||
put:
|
||||
summary: Funktionär aktualisieren
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/FunktionaerUpdateRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: Funktionär aktualisiert
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Funktionaer'
|
||||
delete:
|
||||
summary: Funktionär löschen
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
responses:
|
||||
'204':
|
||||
description: Gelöscht
|
||||
/funktionaer/search:
|
||||
get:
|
||||
summary: Sucht Funktionäre nach SatzNummer
|
||||
parameters:
|
||||
- name: q
|
||||
in: query
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
responses:
|
||||
'200':
|
||||
description: Liste von Funktionären
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Funktionaer'
|
||||
/funktionaer/satz/{satzId}/{satzNummer}:
|
||||
get:
|
||||
summary: Funktionär nach Satz-ID und Nummer suchen
|
||||
parameters:
|
||||
- name: satzId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: satzNummer
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
responses:
|
||||
'200':
|
||||
description: Funktionär gefunden
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Funktionaer'
|
||||
'404':
|
||||
description: Nicht gefunden
|
||||
/rules/turnierklassen:
|
||||
get:
|
||||
summary: Alle Turnierklassen abrufen
|
||||
|
|
@ -58,9 +507,327 @@ components:
|
|||
reiterId:
|
||||
type: string
|
||||
format: uuid
|
||||
satznummer:
|
||||
type: string
|
||||
nachname:
|
||||
type: string
|
||||
vorname:
|
||||
type: string
|
||||
geburtsdatum:
|
||||
type: string
|
||||
format: date
|
||||
bundeslandNummer:
|
||||
type: integer
|
||||
vereinsName:
|
||||
type: string
|
||||
nation:
|
||||
type: string
|
||||
reiterLizenz:
|
||||
type: string
|
||||
startkarte:
|
||||
type: string
|
||||
fahrLizenz:
|
||||
type: string
|
||||
mitgliedsNummer:
|
||||
type: integer
|
||||
telefonNummer:
|
||||
type: string
|
||||
lastPayYear:
|
||||
type: integer
|
||||
feiId:
|
||||
type: string
|
||||
lizenzKlasse:
|
||||
type: string
|
||||
istAktiv:
|
||||
type: boolean
|
||||
updatedAt:
|
||||
type: string
|
||||
format: date-time
|
||||
ReiterCreateRequest:
|
||||
type: object
|
||||
required:
|
||||
- satznummer
|
||||
- nachname
|
||||
- vorname
|
||||
properties:
|
||||
satznummer:
|
||||
type: string
|
||||
nachname:
|
||||
type: string
|
||||
vorname:
|
||||
type: string
|
||||
geburtsdatum:
|
||||
type: string
|
||||
format: date
|
||||
bundeslandNummer:
|
||||
type: integer
|
||||
vereinsName:
|
||||
type: string
|
||||
nation:
|
||||
type: string
|
||||
reiterLizenz:
|
||||
type: string
|
||||
startkarte:
|
||||
type: string
|
||||
fahrLizenz:
|
||||
type: string
|
||||
mitgliedsNummer:
|
||||
type: integer
|
||||
telefonNummer:
|
||||
type: string
|
||||
lastPayYear:
|
||||
type: integer
|
||||
feiId:
|
||||
type: string
|
||||
lizenzKlasse:
|
||||
type: string
|
||||
istAktiv:
|
||||
type: boolean
|
||||
ReiterUpdateRequest:
|
||||
type: object
|
||||
properties:
|
||||
nachname:
|
||||
type: string
|
||||
vorname:
|
||||
type: string
|
||||
geburtsdatum:
|
||||
type: string
|
||||
format: date
|
||||
bundeslandNummer:
|
||||
type: integer
|
||||
vereinsName:
|
||||
type: string
|
||||
nation:
|
||||
type: string
|
||||
reiterLizenz:
|
||||
type: string
|
||||
startkarte:
|
||||
type: string
|
||||
fahrLizenz:
|
||||
type: string
|
||||
mitgliedsNummer:
|
||||
type: integer
|
||||
telefonNummer:
|
||||
type: string
|
||||
lastPayYear:
|
||||
type: integer
|
||||
feiId:
|
||||
type: string
|
||||
lizenzKlasse:
|
||||
type: string
|
||||
istAktiv:
|
||||
type: boolean
|
||||
Horse:
|
||||
type: object
|
||||
properties:
|
||||
pferdId:
|
||||
type: string
|
||||
format: uuid
|
||||
kopfnummer:
|
||||
type: string
|
||||
pferdeName:
|
||||
type: string
|
||||
lebensnummer:
|
||||
type: string
|
||||
geschlecht:
|
||||
type: string
|
||||
geburtsjahr:
|
||||
type: integer
|
||||
farbe:
|
||||
type: string
|
||||
satznummer:
|
||||
type: string
|
||||
istAktiv:
|
||||
type: boolean
|
||||
updatedAt:
|
||||
type: string
|
||||
format: date-time
|
||||
HorseCreateRequest:
|
||||
type: object
|
||||
required:
|
||||
- pferdeName
|
||||
- geschlecht
|
||||
properties:
|
||||
kopfnummer:
|
||||
type: string
|
||||
pferdeName:
|
||||
type: string
|
||||
lebensnummer:
|
||||
type: string
|
||||
geschlecht:
|
||||
type: string
|
||||
geburtsjahr:
|
||||
type: integer
|
||||
farbe:
|
||||
type: string
|
||||
satznummer:
|
||||
type: string
|
||||
istAktiv:
|
||||
type: boolean
|
||||
HorseUpdateRequest:
|
||||
type: object
|
||||
properties:
|
||||
kopfnummer:
|
||||
type: string
|
||||
pferdeName:
|
||||
type: string
|
||||
lebensnummer:
|
||||
type: string
|
||||
geschlecht:
|
||||
type: string
|
||||
geburtsjahr:
|
||||
type: integer
|
||||
farbe:
|
||||
type: string
|
||||
istAktiv:
|
||||
type: boolean
|
||||
Verein:
|
||||
type: object
|
||||
properties:
|
||||
vereinId:
|
||||
type: string
|
||||
format: uuid
|
||||
vereinsNummer:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
bundesland:
|
||||
type: string
|
||||
ort:
|
||||
type: string
|
||||
plz:
|
||||
type: string
|
||||
strasse:
|
||||
type: string
|
||||
email:
|
||||
type: string
|
||||
telefon:
|
||||
type: string
|
||||
website:
|
||||
type: string
|
||||
istVeranstalter:
|
||||
type: boolean
|
||||
istAktiv:
|
||||
type: boolean
|
||||
imageUrl:
|
||||
type: string
|
||||
bemerkungen:
|
||||
type: string
|
||||
updatedAt:
|
||||
type: string
|
||||
format: date-time
|
||||
VereinCreateRequest:
|
||||
type: object
|
||||
required:
|
||||
- vereinsNummer
|
||||
- name
|
||||
properties:
|
||||
vereinsNummer:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
bundesland:
|
||||
type: string
|
||||
ort:
|
||||
type: string
|
||||
plz:
|
||||
type: string
|
||||
strasse:
|
||||
type: string
|
||||
email:
|
||||
type: string
|
||||
telefon:
|
||||
type: string
|
||||
website:
|
||||
type: string
|
||||
istVeranstalter:
|
||||
type: boolean
|
||||
istAktiv:
|
||||
type: boolean
|
||||
imageUrl:
|
||||
type: string
|
||||
bemerkungen:
|
||||
type: string
|
||||
VereinUpdateRequest:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
bundesland:
|
||||
type: string
|
||||
ort:
|
||||
type: string
|
||||
plz:
|
||||
type: string
|
||||
strasse:
|
||||
type: string
|
||||
email:
|
||||
type: string
|
||||
telefon:
|
||||
type: string
|
||||
website:
|
||||
type: string
|
||||
istVeranstalter:
|
||||
type: boolean
|
||||
istAktiv:
|
||||
type: boolean
|
||||
imageUrl:
|
||||
type: string
|
||||
bemerkungen:
|
||||
type: string
|
||||
Funktionaer:
|
||||
type: object
|
||||
properties:
|
||||
funktionaerId:
|
||||
type: string
|
||||
format: uuid
|
||||
satzId:
|
||||
type: string
|
||||
satzNummer:
|
||||
type: integer
|
||||
name:
|
||||
type: string
|
||||
qualifikationen:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
istAktiv:
|
||||
type: boolean
|
||||
bemerkungen:
|
||||
type: string
|
||||
updatedAt:
|
||||
type: string
|
||||
format: date-time
|
||||
FunktionaerCreateRequest:
|
||||
type: object
|
||||
required:
|
||||
- satzId
|
||||
- satzNummer
|
||||
properties:
|
||||
satzId:
|
||||
type: string
|
||||
satzNummer:
|
||||
type: integer
|
||||
name:
|
||||
type: string
|
||||
qualifikationen:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
istAktiv:
|
||||
type: boolean
|
||||
bemerkungen:
|
||||
type: string
|
||||
FunktionaerUpdateRequest:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
qualifikationen:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
istAktiv:
|
||||
type: boolean
|
||||
bemerkungen:
|
||||
type: string
|
||||
|
|
|
|||
38
docs/99_Journal/2026-04-10_Billing-Setup_ZNS-Hardening.md
Normal file
38
docs/99_Journal/2026-04-10_Billing-Setup_ZNS-Hardening.md
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
---
|
||||
type: Journal
|
||||
status: ACTIVE
|
||||
owner: Curator
|
||||
last_update: 2026-04-10
|
||||
---
|
||||
# Journal Entry: 2026-04-10 - Billing Service Setup & ZNS Importer Hardening
|
||||
|
||||
## 👷 [Backend Developer] / 🏗️ [Lead Architect] / 🧹 [Curator]
|
||||
|
||||
### Zusammenfassung der Session
|
||||
In dieser Session wurde das Fundament für den Kassa-Service (`billing-context`) gelegt und die Robustheit des ZNS-Importers durch zusätzliche Integrationstests für Funktionäre gesteigert.
|
||||
|
||||
### Wichtigste Ergebnisse
|
||||
1. **Billing Service Initialisierung:**
|
||||
* `billing-service` Modul erstellt und konfiguriert.
|
||||
* Exposed-Tabellendefinitionen für `TeilnehmerKonto` und `Buchung` implementiert.
|
||||
* Repository-Schnittstellen (Domain) und Exposed-Implementierungen (Service) erstellt.
|
||||
* `TeilnehmerKontoService` mit Basis-Logik (Kontoerstellung & Buchungen) implementiert.
|
||||
2. **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):**
|
||||
* `billing-service` auf korrekte Exposed DSL Syntax (`selectAll().where { ... }`) umgestellt.
|
||||
* Explizite `transaction { ... }` Blöcke in `TeilnehmerKontoService` eingeführt (da `@Transactional` ohne JPA-Starter nicht verfügbar war).
|
||||
* Typ-Konsistenz für `Instant` (kotlin.time) in `billing-domain` zur Übereinstimmung mit `core-domain` hergestellt.
|
||||
|
||||
### Betroffene Dateien
|
||||
- `backend/services/billing/` (Neuer SCS-Kontext)
|
||||
- `backend/infrastructure/zns-importer/src/test/kotlin/at/mocode/zns/importer/ZnsImportServiceTest.kt`
|
||||
|
||||
### Nächste Schritte
|
||||
- Implementierung der REST-API für den Billing-Service (OpenAPI-First).
|
||||
- Integration des Billing-Services in den `entries-context` (z.B. automatische Buchung von Nenngebühren).
|
||||
- UI-Anbindung im Frontend für Kontenübersicht und manuelle Buchungen.
|
||||
|
||||
---
|
||||
*Co-authored-by: Junie <junie@jetbrains.com>*
|
||||
|
|
@ -94,6 +94,11 @@ include(":backend:services:masterdata:masterdata-domain")
|
|||
include(":backend:services:masterdata:masterdata-infrastructure")
|
||||
include(":backend:services:masterdata:masterdata-service")
|
||||
|
||||
// --- BILLING (Kassa, Zahlungen & Rechnungen) ---
|
||||
include(":backend:services:billing:billing-api")
|
||||
include(":backend:services:billing:billing-domain")
|
||||
include(":backend:services:billing:billing-service")
|
||||
|
||||
// --- PING (Ping Service) ---
|
||||
include(":backend:services:ping:ping-service")
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user