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:
+14
@@ -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)
|
||||
}
|
||||
+59
@@ -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)
|
||||
}
|
||||
}
|
||||
+47
@@ -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)
|
||||
}
|
||||
}
|
||||
+111
@@ -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]
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user