feat(billing): implement REST API, database config, and tests for billing service
- **REST API:** Added `BillingController` with endpoints for managing participant accounts and transactions, including history retrieval. - **Database Configuration:** Introduced `BillingDatabaseConfiguration` to initialize database schema using Exposed. - **Testing:** Added integration tests for `TeilnehmerKontoService` using H2 in-memory database.
This commit is contained in:
+127
@@ -0,0 +1,127 @@
|
||||
@file:OptIn(ExperimentalUuidApi::class)
|
||||
|
||||
package at.mocode.billing.api.rest
|
||||
|
||||
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.service.TeilnehmerKontoService
|
||||
import at.mocode.core.domain.serialization.InstantSerializer
|
||||
import jakarta.validation.Valid
|
||||
import jakarta.validation.constraints.NotBlank
|
||||
import jakarta.validation.constraints.NotNull
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.*
|
||||
import kotlin.time.Instant
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
import kotlin.uuid.Uuid
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/billing")
|
||||
class BillingController(
|
||||
private val kontoService: TeilnehmerKontoService
|
||||
) {
|
||||
|
||||
data class KontoDto(
|
||||
val kontoId: String,
|
||||
val veranstaltungId: String,
|
||||
val personId: String,
|
||||
val personName: String,
|
||||
val saldoCent: Long,
|
||||
val bemerkungen: String?,
|
||||
@Serializable(with = InstantSerializer::class)
|
||||
val updatedAt: Instant
|
||||
)
|
||||
|
||||
data class BuchungDto(
|
||||
val buchungId: String,
|
||||
val kontoId: String,
|
||||
val betragCent: Long,
|
||||
val typ: BuchungsTyp,
|
||||
val verwendungszweck: String,
|
||||
@Serializable(with = InstantSerializer::class)
|
||||
val gebuchtAm: Instant
|
||||
)
|
||||
|
||||
data class CreateKontoRequest(
|
||||
@field:NotNull val veranstaltungId: String,
|
||||
@field:NotNull val personId: String,
|
||||
@field:NotBlank val personName: String
|
||||
)
|
||||
|
||||
data class BuchungRequest(
|
||||
@field:NotNull val betragCent: Long,
|
||||
@field:NotNull val typ: BuchungsTyp,
|
||||
@field:NotBlank val verwendungszweck: String
|
||||
)
|
||||
|
||||
@GetMapping("/konten/{kontoId}")
|
||||
fun getKonto(@PathVariable kontoId: String): ResponseEntity<KontoDto> {
|
||||
val uuid = try { Uuid.parse(kontoId) } catch (e: Exception) { return ResponseEntity.badRequest().build() }
|
||||
val konto = kontoService.getKontoById(uuid) ?: return ResponseEntity.notFound().build()
|
||||
return ResponseEntity.ok(konto.toDto())
|
||||
}
|
||||
|
||||
@GetMapping("/konten")
|
||||
fun getKontoByVeranstaltungUndPerson(
|
||||
@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 konto = kontoService.getOrCreateKonto(vUuid, pUuid, "Unbekannt") // Name wird bei getOrCreate ggf. ignoriert wenn existiert
|
||||
return ResponseEntity.ok(konto.toDto())
|
||||
}
|
||||
|
||||
@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 konto = kontoService.getOrCreateKonto(vUuid, pUuid, request.personName)
|
||||
return ResponseEntity.ok(konto.toDto())
|
||||
}
|
||||
|
||||
@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 buchungen = kontoService.getBuchungsHistorie(uuid)
|
||||
return ResponseEntity.ok(buchungen.map { it.toDto() })
|
||||
}
|
||||
|
||||
@PostMapping("/konten/{kontoId}/buchungen")
|
||||
fun addBuchung(
|
||||
@PathVariable kontoId: String,
|
||||
@Valid @RequestBody request: BuchungRequest
|
||||
): ResponseEntity<KontoDto> {
|
||||
val uuid = try { Uuid.parse(kontoId) } catch (e: Exception) { return ResponseEntity.badRequest().build() }
|
||||
val konto = kontoService.buche(
|
||||
kontoId = uuid,
|
||||
betragCent = request.betragCent,
|
||||
typ = request.typ,
|
||||
zweck = request.verwendungszweck
|
||||
)
|
||||
return ResponseEntity.ok(konto.toDto())
|
||||
}
|
||||
|
||||
private fun TeilnehmerKonto.toDto() = KontoDto(
|
||||
kontoId = kontoId.toString(),
|
||||
veranstaltungId = veranstaltungId.toString(),
|
||||
personId = personId.toString(),
|
||||
personName = personName,
|
||||
saldoCent = saldoCent,
|
||||
bemerkungen = bemerkungen,
|
||||
updatedAt = updatedAt
|
||||
)
|
||||
|
||||
private fun Buchung.toDto() = BuchungDto(
|
||||
buchungId = buchungId.toString(),
|
||||
kontoId = kontoId.toString(),
|
||||
betragCent = betragCent,
|
||||
typ = typ,
|
||||
verwendungszweck = verwendungszweck,
|
||||
gebuchtAm = gebuchtAm
|
||||
)
|
||||
}
|
||||
+18
-13
@@ -31,7 +31,19 @@ class TeilnehmerKontoService(
|
||||
}
|
||||
}
|
||||
|
||||
fun bucheBetrag(kontoId: Uuid, betragCent: Long, typ: BuchungsTyp, verwendungszweck: String): Buchung {
|
||||
fun getKontoById(kontoId: Uuid): TeilnehmerKonto? {
|
||||
return transaction {
|
||||
kontoRepository.findById(kontoId)
|
||||
}
|
||||
}
|
||||
|
||||
fun getBuchungsHistorie(kontoId: Uuid): List<Buchung> {
|
||||
return transaction {
|
||||
buchungRepository.findByKonto(kontoId)
|
||||
}
|
||||
}
|
||||
|
||||
fun buche(kontoId: Uuid, betragCent: Long, typ: BuchungsTyp, zweck: String): TeilnehmerKonto {
|
||||
return transaction {
|
||||
val konto = kontoRepository.findById(kontoId) ?: throw IllegalArgumentException("Konto nicht gefunden: $kontoId")
|
||||
|
||||
@@ -39,21 +51,14 @@ class TeilnehmerKontoService(
|
||||
kontoId = kontoId,
|
||||
betragCent = betragCent,
|
||||
typ = typ,
|
||||
verwendungszweck = verwendungszweck
|
||||
verwendungszweck = zweck
|
||||
)
|
||||
|
||||
val neueBuchung = buchungRepository.save(buchung)
|
||||
kontoRepository.updateSaldo(kontoId, konto.saldoCent + betragCent)
|
||||
buchungRepository.save(buchung)
|
||||
val neuerSaldo = konto.saldoCent + betragCent
|
||||
kontoRepository.updateSaldo(kontoId, neuerSaldo)
|
||||
|
||||
neueBuchung
|
||||
kontoRepository.findById(kontoId)!!
|
||||
}
|
||||
}
|
||||
|
||||
fun getBuchungen(kontoId: Uuid): List<Buchung> {
|
||||
return buchungRepository.findByKonto(kontoId)
|
||||
}
|
||||
|
||||
fun getKonto(kontoId: Uuid): TeilnehmerKonto? {
|
||||
return kontoRepository.findById(kontoId)
|
||||
}
|
||||
}
|
||||
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package at.mocode.billing.service.config
|
||||
|
||||
import at.mocode.billing.service.persistence.BuchungTable
|
||||
import at.mocode.billing.service.persistence.TeilnehmerKontoTable
|
||||
import jakarta.annotation.PostConstruct
|
||||
import org.jetbrains.exposed.v1.jdbc.Database
|
||||
import org.jetbrains.exposed.v1.jdbc.SchemaUtils
|
||||
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
|
||||
|
||||
@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
|
||||
) {
|
||||
|
||||
private val log = LoggerFactory.getLogger(BillingDatabaseConfiguration::class.java)
|
||||
|
||||
@PostConstruct
|
||||
fun initializeDatabase() {
|
||||
log.info("Initializing database schema for Billing Service...")
|
||||
try {
|
||||
Database.connect(jdbcUrl, user = username, password = password)
|
||||
transaction {
|
||||
SchemaUtils.create(
|
||||
TeilnehmerKontoTable,
|
||||
BuchungTable
|
||||
)
|
||||
}
|
||||
log.info("Billing database schema initialized successfully")
|
||||
} catch (e: Exception) {
|
||||
log.error("Failed to initialize billing database schema", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
+210
@@ -0,0 +1,210 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: Billing SCS API
|
||||
description: >
|
||||
API für den Billing-Bounded-Context (Kassa, Abrechnung, Teilnehmerkonten)
|
||||
version: 1.0.0
|
||||
servers:
|
||||
- url: http://localhost:8089
|
||||
description: Lokaler Entwicklungs-Server
|
||||
paths:
|
||||
/api/billing/konten:
|
||||
get:
|
||||
summary: Teilnehmerkonto suchen
|
||||
description: Sucht ein Konto basierend auf Veranstaltungs-ID und Personen-ID. Erstellt das Konto, falls es nicht existiert.
|
||||
parameters:
|
||||
- name: veranstaltungId
|
||||
in: query
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
- name: personId
|
||||
in: query
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
responses:
|
||||
'200':
|
||||
description: Teilnehmerkonto
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/KontoDto'
|
||||
'400':
|
||||
description: Ungültige UUID-Formate
|
||||
post:
|
||||
summary: Teilnehmerkonto erstellen oder abrufen
|
||||
description: Erstellt ein neues Teilnehmerkonto für eine Veranstaltung und eine Person.
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/CreateKontoRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: Teilnehmerkonto (neu erstellt oder bestehend)
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/KontoDto'
|
||||
'400':
|
||||
description: Validierungsfehler
|
||||
/api/billing/konten/{kontoId}:
|
||||
get:
|
||||
summary: Teilnehmerkonto nach ID abrufen
|
||||
parameters:
|
||||
- name: kontoId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
responses:
|
||||
'200':
|
||||
description: Details zum Teilnehmerkonto
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/KontoDto'
|
||||
'404':
|
||||
description: Konto nicht gefunden
|
||||
'400':
|
||||
description: Ungültige Konto-ID
|
||||
/api/billing/konten/{kontoId}/buchungen:
|
||||
get:
|
||||
summary: Buchungshistorie abrufen
|
||||
description: Liefert alle Buchungen für ein bestimmtes Teilnehmerkonto.
|
||||
parameters:
|
||||
- name: kontoId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
responses:
|
||||
'200':
|
||||
description: Liste von Buchungen
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/BuchungDto'
|
||||
'400':
|
||||
description: Ungültige Konto-ID
|
||||
post:
|
||||
summary: Buchung hinzufügen
|
||||
description: Führt eine neue Buchung auf dem Teilnehmerkonto durch und aktualisiert den Saldo.
|
||||
parameters:
|
||||
- name: kontoId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/BuchungRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: Aktualisiertes Teilnehmerkonto nach der Buchung
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/KontoDto'
|
||||
'400':
|
||||
description: Validierungsfehler oder ungültige Konto-ID
|
||||
components:
|
||||
schemas:
|
||||
KontoDto:
|
||||
type: object
|
||||
properties:
|
||||
kontoId:
|
||||
type: string
|
||||
format: uuid
|
||||
veranstaltungId:
|
||||
type: string
|
||||
format: uuid
|
||||
personId:
|
||||
type: string
|
||||
format: uuid
|
||||
personName:
|
||||
type: string
|
||||
saldoCent:
|
||||
type: integer
|
||||
format: int64
|
||||
description: Aktueller Saldo in Cent
|
||||
bemerkungen:
|
||||
type: string
|
||||
nullable: true
|
||||
updatedAt:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Zeitpunkt der letzten Aktualisierung
|
||||
BuchungDto:
|
||||
type: object
|
||||
properties:
|
||||
buchungId:
|
||||
type: string
|
||||
format: uuid
|
||||
kontoId:
|
||||
type: string
|
||||
format: uuid
|
||||
betragCent:
|
||||
type: integer
|
||||
format: int64
|
||||
description: Betrag in Cent (positiv für Gutschriften, negativ für Belastungen)
|
||||
typ:
|
||||
$ref: '#/components/schemas/BuchungsTyp'
|
||||
verwendungszweck:
|
||||
type: string
|
||||
gebuchtAm:
|
||||
type: string
|
||||
format: date-time
|
||||
CreateKontoRequest:
|
||||
type: object
|
||||
required:
|
||||
- veranstaltungId
|
||||
- personId
|
||||
- personName
|
||||
properties:
|
||||
veranstaltungId:
|
||||
type: string
|
||||
format: uuid
|
||||
personId:
|
||||
type: string
|
||||
format: uuid
|
||||
personName:
|
||||
type: string
|
||||
minLength: 1
|
||||
BuchungRequest:
|
||||
type: object
|
||||
required:
|
||||
- betragCent
|
||||
- typ
|
||||
- verwendungszweck
|
||||
properties:
|
||||
betragCent:
|
||||
type: integer
|
||||
format: int64
|
||||
typ:
|
||||
$ref: '#/components/schemas/BuchungsTyp'
|
||||
verwendungszweck:
|
||||
type: string
|
||||
minLength: 1
|
||||
BuchungsTyp:
|
||||
type: string
|
||||
enum:
|
||||
- NENNGEBUEHR
|
||||
- KOPPELGEBUEHR
|
||||
- NACHNENNGEBUEHR
|
||||
- STARTGEBUEHR
|
||||
- EINZAHLUNG
|
||||
- AUSZAHLUNG
|
||||
- SONSTIGES
|
||||
Reference in New Issue
Block a user