Enhance billing logic: add REST support for manual and automated transactions, refine billing routes, adapt frontend API integration, and implement transaction type validation.

This commit is contained in:
2026-04-12 18:35:49 +02:00
parent 03950f8b0c
commit 9754f3e36b
10 changed files with 162 additions and 27 deletions
@@ -16,28 +16,24 @@ class DefaultBillingRepository(
personId: String,
personName: String
): Result<TeilnehmerKontoDto> = runCatching {
client.get(ApiRoutes.Billing.KONTEN) {
parameter("veranstaltungId", veranstaltungId)
parameter("personId", personId)
client.get(ApiRoutes.Billing.personKonto(veranstaltungId, personId)) {
parameter("personName", personName)
}.body()
}
override suspend fun getKonten(veranstaltungId: String): Result<List<TeilnehmerKontoDto>> = runCatching {
client.get(ApiRoutes.Billing.KONTEN) {
parameter("veranstaltungId", veranstaltungId)
}.body()
client.get(ApiRoutes.Billing.veranstaltungKonten(veranstaltungId)).body()
}
override suspend fun getBuchungen(kontoId: String): Result<List<BuchungDto>> = runCatching {
client.get(ApiRoutes.Billing.buchungen(kontoId)).body()
client.get(ApiRoutes.Billing.historie(kontoId)).body()
}
override suspend fun addBuchung(
kontoId: String,
request: BuchungRequest
): Result<TeilnehmerKontoDto> = runCatching {
client.post(ApiRoutes.Billing.buchungen(kontoId)) {
client.post(ApiRoutes.Billing.buche(kontoId)) {
contentType(ContentType.Application.Json)
setBody(request)
}.body()
@@ -59,7 +59,7 @@ data class BuchungDto(
data class BuchungRequest(
val betragCent: Long,
val verwendungszweck: String,
val typ: String = "MANUELL"
val typ: String // Pflichtfeld, muss mit Backend-Enum übereinstimmen
)
enum class GebuehrTyp {
@@ -120,8 +120,8 @@ fun BillingScreen(
if (showBuchungsDialog) {
ManuelleBuchungDialog(
onDismiss = { showBuchungsDialog = false },
onConfirm = { betrag, zweck ->
viewModel.buche(betrag, zweck)
onConfirm = { betrag, zweck, typ ->
viewModel.buche(betrag, zweck, typ)
showBuchungsDialog = false
}
)
@@ -179,28 +179,43 @@ private fun BuchungItem(buchung: BuchungDto) {
@Composable
private fun ManuelleBuchungDialog(
onDismiss: () -> Unit,
onConfirm: (Long, String) -> Unit
onConfirm: (Long, String, String) -> Unit
) {
var betragStr by remember { mutableStateOf("") }
var zweck by remember { mutableStateOf("") }
var isGutschrift by remember { mutableStateOf(false) }
var selectedTyp by remember { mutableStateOf("ZAHLUNG_BAR") }
val typen = listOf(
"ZAHLUNG_BAR" to "Zahlung Bar",
"ZAHLUNG_KARTE" to "Zahlung Karte",
"GUTSCHRIFT" to "Gutschrift",
"NENNGEBUEHR" to "Nenngebühr",
"STARTGEBUEHR" to "Startgebühr",
"BOXENGEBUEHR" to "Boxengebühr",
"STORNIERUNG" to "Stornierung"
)
AlertDialog(
onDismissRequest = onDismiss,
title = { Text("Manuelle Buchung") },
text = {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) {
RadioButton(selected = !isGutschrift, onClick = { isGutschrift = false })
Text("Belastung (-)", modifier = Modifier.clickable { isGutschrift = false })
Spacer(Modifier.width(16.dp))
RadioButton(selected = isGutschrift, onClick = { isGutschrift = true })
Text("Gutschrift (+)", modifier = Modifier.clickable { isGutschrift = true })
Text("Buchungstyp:", style = MaterialTheme.typography.labelMedium)
// Einfache Auswahl via FlowRow oder Column (hier Column für Platz)
Column {
typen.forEach { (id, label) ->
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.clickable { selectedTyp = id }) {
RadioButton(selected = selectedTyp == id, onClick = { selectedTyp = id })
Text(label, fontSize = 12.sp)
}
}
}
OutlinedTextField(
value = betragStr,
onValueChange = { if (it.isEmpty() || it.toDoubleOrNull() != null) betragStr = it },
label = { Text("Betrag in €") },
label = { Text("Betrag in € (immer positiv eingeben)") },
modifier = Modifier.fillMaxWidth(),
singleLine = true
)
@@ -218,7 +233,7 @@ private fun ManuelleBuchungDialog(
onClick = {
val euro = betragStr.toDoubleOrNull() ?: 0.0
val cent = (euro * 100).toLong()
onConfirm(if (isGutschrift) cent else -cent, zweck)
onConfirm(cent, zweck, selectedTyp)
},
enabled = betragStr.isNotEmpty() && zweck.isNotEmpty()
) {
@@ -66,11 +66,11 @@ class BillingViewModel(
}
}
fun buche(betragCent: Long, zweck: String) {
fun buche(betragCent: Long, zweck: String, typ: String) {
val konto = _uiState.value.selectedKonto ?: return
viewModelScope.launch {
_uiState.value = _uiState.value.copy(isLoading = true)
val request = BuchungRequest(betragCent = betragCent, verwendungszweck = zweck)
val request = BuchungRequest(betragCent = betragCent, verwendungszweck = zweck, typ = typ)
repository.addBuchung(konto.id, request)
.onSuccess { aktualisiertesKonto ->
_uiState.value = _uiState.value.copy(selectedKonto = aktualisiertesKonto)