From 0f2060fc147d0067d663c0f642598977dc3c22d1 Mon Sep 17 00:00:00 2001 From: StefanMoCoAt Date: Sun, 12 Apr 2026 18:00:38 +0200 Subject: [PATCH] Integrate `billing-service` microservice: add API gateway routing, service discovery with Consul, Docker support, and Spring configuration. Update frontend with API integration, `BillingRepository`, and `BillingViewModel`. --- CHANGELOG.md | 4 + .../gateway/config/GatewayConfig.kt | 7 +- .../billing/billing-service/Dockerfile | 141 ++++++++++++++++++ .../billing/billing-service/build.gradle.kts | 4 + .../service/BillingServiceApplication.kt | 2 + dc-backend.yaml | 73 +++++++++ ...4-12_Abrechnung_Integration_Curator_Log.md | 31 ++++ .../mocode/frontend/core/network/ApiRoutes.kt | 6 + .../billing/data/DefaultBillingRepository.kt | 39 +++++ .../features/billing/di/BillingModule.kt | 5 +- .../billing/domain/BillingRepository.kt | 29 ++++ .../billing/presentation/BillingViewModel.kt | 94 +++++------- 12 files changed, 376 insertions(+), 59 deletions(-) create mode 100644 backend/services/billing/billing-service/Dockerfile create mode 100644 docs/04_Agents/Logs/2026-04-12_Abrechnung_Integration_Curator_Log.md create mode 100644 frontend/features/billing-feature/src/commonMain/kotlin/at/mocode/frontend/features/billing/data/DefaultBillingRepository.kt create mode 100644 frontend/features/billing-feature/src/commonMain/kotlin/at/mocode/frontend/features/billing/domain/BillingRepository.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index dfa004ef..7c95f387 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,10 @@ Versionierung folgt [Semantic Versioning](https://semver.org/lang/de/). ### [Unreleased] ### Hinzugefügt +- **Phase 12 (Abrechnung & Infrastruktur) - 12.04.2026:** + - **Infrastruktur:** Docker-Integration für `billing-service` (Port 8087) und API-Gateway Routing vervollständigt. + - **Service Discovery:** Alle relevanten Microservices (`masterdata`, `events`, `results`, `series`, `billing`) sind nun bei Consul registriert. + - **Frontend Billing:** `BillingRepository` und `BillingViewModel` auf reale API-Anbindung (Ktor) umgestellt; `BillingScreen` funktionalisiert. - **Backend Fixes - 12.04.2026:** - **Infrastruktur:** Behebung von Startfehlern im `events-service` (DataSource) und `masterdata-service` (Consul). - **Build:** Integration von `results-service` und `series-service` in `settings.gradle.kts`. diff --git a/backend/infrastructure/gateway/src/main/kotlin/at/mocode/infrastructure/gateway/config/GatewayConfig.kt b/backend/infrastructure/gateway/src/main/kotlin/at/mocode/infrastructure/gateway/config/GatewayConfig.kt index b8b3d580..a3fe71ad 100644 --- a/backend/infrastructure/gateway/src/main/kotlin/at/mocode/infrastructure/gateway/config/GatewayConfig.kt +++ b/backend/infrastructure/gateway/src/main/kotlin/at/mocode/infrastructure/gateway/config/GatewayConfig.kt @@ -15,7 +15,8 @@ class GatewayConfig( @Value("\${events.service.url:http://localhost:8085}") private val eventsServiceUrl: String, @Value("\${zns.import.service.url:http://localhost:8095}") private val znsImportServiceUrl: String, @Value("\${results.service.url:http://localhost:8088}") private val resultsServiceUrl: String, - @Value("\${series.service.url:http://localhost:8089}") private val seriesServiceUrl: String + @Value("\${series.service.url:http://localhost:8089}") private val seriesServiceUrl: String, + @Value("\${billing.service.url:http://localhost:8087}") private val billingServiceUrl: String ) { @Bean @@ -52,6 +53,10 @@ class GatewayConfig( path("/api/v1/series/**") uri(seriesServiceUrl) } + route(id = "billing-service") { + path("/api/v1/billing/**") + uri(billingServiceUrl) + } } } } diff --git a/backend/services/billing/billing-service/Dockerfile b/backend/services/billing/billing-service/Dockerfile new file mode 100644 index 00000000..c2d79522 --- /dev/null +++ b/backend/services/billing/billing-service/Dockerfile @@ -0,0 +1,141 @@ +# syntax=docker/dockerfile:1.7 + +# =================================================================== +# Dockerfile for Billing Service +# Based on Spring Boot Service Template with Billing-specific configuration +# =================================================================== + +# === CENTRALIZED BUILD ARGUMENTS === +ARG GRADLE_VERSION +ARG JAVA_VERSION +ARG BUILD_DATE +ARG VERSION + +# Service-specific arguments +ARG SERVICE_PATH=billing/billing-service +ARG SERVICE_NAME=billing-service + +# =================================================================== +# Build Stage +# =================================================================== +FROM gradle:${GRADLE_VERSION}-jdk${JAVA_VERSION}-alpine AS builder + +# Re-declare build arguments for this stage +ARG SERVICE_PATH=billing/billing-service +ARG SERVICE_NAME=billing-service + +LABEL stage=builder +LABEL maintainer="Meldestelle Development Team" + +WORKDIR /workspace + +# Gradle optimizations +ENV GRADLE_OPTS="-Dorg.gradle.caching=true \ + -Dorg.gradle.daemon=false \ + -Dorg.gradle.parallel=true \ + -Dorg.gradle.configureondemand=true \ + -Xmx2g" + +# Copy build files in optimal order for caching +COPY gradlew gradlew.bat gradle.properties settings.gradle.kts ./ +COPY gradle/ gradle/ + +# Make gradlew executable +RUN chmod +x gradlew + +COPY platform/ platform/ +COPY core/ core/ +COPY build.gradle.kts ./ + +# Copy billing service modules +COPY backend/services/billing/billing-domain/ backend/services/billing/billing-domain/ +COPY backend/services/billing/billing-service/ backend/services/billing/billing-service/ + +# Build billing service +RUN echo "Building Billing Service..." && \ + ./gradlew :backend:services:billing:billing-service:dependencies --no-daemon --info && \ + ./gradlew :backend:services:billing:billing-service:bootJar --no-daemon --info + +# Extract JAR layers +WORKDIR /builder +RUN cp /workspace/backend/services/billing/billing-service/build/libs/*.jar app.jar && \ + java -Djarmode=layertools -jar app.jar extract + +# =================================================================== +# Runtime Stage +# =================================================================== +FROM eclipse-temurin:${JAVA_VERSION}-jre-alpine AS runtime + +# Metadata +LABEL service="billing-service" \ + version="1.0.0" \ + description="Billing and Financial Service for Austrian Equestrian Federation" \ + maintainer="Meldestelle Development Team" \ + java.version="${JAVA_VERSION}" + +# Build arguments +ARG APP_USER=billinguser +ARG APP_GROUP=billinggroup +ARG APP_UID=1008 +ARG APP_GID=1008 + +WORKDIR /app + +# System setup +RUN apk update && \ + apk upgrade && \ + apk add --no-cache curl jq tzdata && \ + rm -rf /var/cache/apk/* + +# Non-root user creation +RUN addgroup -g ${APP_GID} -S ${APP_GROUP} && \ + adduser -u ${APP_UID} -S ${APP_USER} -G ${APP_GROUP} -h /app -s /bin/sh + +# Directory setup +RUN mkdir -p /app/logs /app/tmp && \ + chown -R ${APP_USER}:${APP_GROUP} /app + +# Re-declare build arguments for runtime stage +ARG SERVICE_PATH=billing/billing-service +ARG SERVICE_NAME=billing-service + +# Copy Spring Boot layers +COPY --from=builder --chown=${APP_USER}:${APP_GROUP} /builder/dependencies/ ./ +COPY --from=builder --chown=${APP_USER}:${APP_GROUP} /builder/spring-boot-loader/ ./ +COPY --from=builder --chown=${APP_USER}:${APP_GROUP} /builder/snapshot-dependencies/ ./ +COPY --from=builder --chown=${APP_USER}:${APP_GROUP} /builder/application/ ./ + +USER ${APP_USER} + +# Expose application port and debug port +EXPOSE 8087 5012 + +# Health check +HEALTHCHECK --interval=15s --timeout=3s --start-period=40s --retries=3 \ + CMD curl -fsS --max-time 2 http://localhost:8087/actuator/health/readiness || exit 1 + +# JVM configuration +ENV JAVA_OPTS="-XX:MaxRAMPercentage=80.0 \ + -XX:+UseG1GC \ + -XX:+UseStringDeduplication \ + -XX:+UseContainerSupport \ + -Djava.security.egd=file:/dev/./urandom \ + -Djava.awt.headless=true \ + -Dfile.encoding=UTF-8 \ + -Duser.timezone=Europe/Vienna" + +# Spring Boot configuration +ENV SPRING_OUTPUT_ANSI_ENABLED=ALWAYS \ + SERVER_PORT=8087 \ + LOGGING_LEVEL_ROOT=INFO \ + LOGGING_LEVEL_AT_MOCODE_BILLING=DEBUG + +# Startup command +ENTRYPOINT ["sh", "-c", "\ + echo 'Starting Billing Service on port 8087...'; \ + if [ \"${DEBUG:-false}\" = \"true\" ]; then \ + echo 'Debug mode enabled on port 5012'; \ + exec java $JAVA_OPTS -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5012 org.springframework.boot.loader.launch.JarLauncher; \ + else \ + exec java $JAVA_OPTS org.springframework.boot.loader.launch.JarLauncher; \ + fi"] diff --git a/backend/services/billing/billing-service/build.gradle.kts b/backend/services/billing/billing-service/build.gradle.kts index f67a314f..2e476bf2 100644 --- a/backend/services/billing/billing-service/build.gradle.kts +++ b/backend/services/billing/billing-service/build.gradle.kts @@ -21,6 +21,10 @@ dependencies { implementation(libs.spring.boot.starter.validation) implementation(libs.spring.boot.starter.actuator) implementation(libs.jackson.module.kotlin) + implementation(libs.spring.cloud.starter.consul.discovery) + implementation(libs.micrometer.tracing.bridge.brave) + implementation(libs.zipkin.reporter.brave) + implementation(libs.zipkin.sender.okhttp3) // Datenbank-Abhängigkeiten implementation(libs.exposed.core) diff --git a/backend/services/billing/billing-service/src/main/kotlin/at/mocode/billing/service/BillingServiceApplication.kt b/backend/services/billing/billing-service/src/main/kotlin/at/mocode/billing/service/BillingServiceApplication.kt index da75f292..032f23c1 100644 --- a/backend/services/billing/billing-service/src/main/kotlin/at/mocode/billing/service/BillingServiceApplication.kt +++ b/backend/services/billing/billing-service/src/main/kotlin/at/mocode/billing/service/BillingServiceApplication.kt @@ -4,8 +4,10 @@ package at.mocode.billing.service import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication +import org.springframework.cloud.client.discovery.EnableDiscoveryClient import kotlin.uuid.ExperimentalUuidApi +@EnableDiscoveryClient @SpringBootApplication class BillingServiceApplication diff --git a/dc-backend.yaml b/dc-backend.yaml index 856da1c5..6048d4d9 100644 --- a/dc-backend.yaml +++ b/dc-backend.yaml @@ -63,6 +63,7 @@ services: EVENTS_SERVICE_URL: "http://events-service:8085" ZNS_IMPORT_SERVICE_URL: "http://zns-import-service:8095" RESULTS_SERVICE_URL: "http://results-service:8088" + BILLING_SERVICE_URL: "http://billing-service:8087" depends_on: postgres: @@ -466,6 +467,78 @@ services: volumes: - ./config/app/base-application.yaml:/workspace/config/application.yml:Z + # --- MICROSERVICE: Billing Service --- + billing-service: + image: "${DOCKER_REGISTRY:-git.mo-code.at/mo-code}/billing-service:${DOCKER_TAG:-latest}" + build: + context: . + dockerfile: backend/services/billing/billing-service/Dockerfile + args: + GRADLE_VERSION: "${DOCKER_GRADLE_VERSION:-9.3.1}" + JAVA_VERSION: "${DOCKER_JAVA_VERSION:-25}" + VERSION: "${DOCKER_VERSION:-1.0.0-SNAPSHOT}" + BUILD_DATE: "${DOCKER_BUILD_DATE}" + labels: + - "org.opencontainers.image.created=${DOCKER_BUILD_DATE}" + container_name: "${PROJECT_NAME:-meldestelle}-billing-service" + restart: unless-stopped + ports: + - "${BILLING_PORT:-8087:8087}" + - "${BILLING_DEBUG_PORT:-5012:5012}" + environment: + SPRING_PROFILES_ACTIVE: "${BILLING_SPRING_PROFILES_ACTIVE:-docker}" + DEBUG: "${BILLING_DEBUG:-true}" + SERVER_PORT: "${BILLING_SERVER_PORT:-8087}" + + # --- KEYCLOAK --- + SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI: "${KC_ISSUER_URI:-http://localhost:8180/realms/meldestelle}" + SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWK_SET_URI: "${KC_JWK_SET_URI:-http://keycloak:8080/realms/meldestelle/protocol/openid-connect/certs}" + + # --- CONSUL --- + SPRING_CLOUD_CONSUL_HOST: "${CONSUL_HOST:-consul}" + SPRING_CLOUD_CONSUL_PORT: "${CONSUL_HTTP_PORT:-8500}" + SPRING_CLOUD_CONSUL_DISCOVERY_SERVICE_NAME: "${BILLING_SERVICE_NAME:-billing-service}" + SPRING_CLOUD_CONSUL_DISCOVERY_PREFER_IP_ADDRESS: "${BILLING_CONSUL_PREFER_IP:-true}" + + # - DATENBANK VERBINDUNG - + SPRING_DATASOURCE_URL: "${POSTGRES_DB_URL:-jdbc:postgresql://postgres:5432/pg-meldestelle-db}" + SPRING_DATASOURCE_USERNAME: "${POSTGRES_USER:-pg-user}" + SPRING_DATASOURCE_PASSWORD: "${POSTGRES_PASSWORD:-pg-password}" + + # --- VALKEY --- + SPRING_DATA_VALKEY_HOST: "${VALKEY_SERVER_HOSTNAME:-valkey}" + SPRING_DATA_VALKEY_PORT: "${VALKEY_SERVER_PORT:-6379}" + + # --- ZIPKIN --- + MANAGEMENT_ZIPKIN_TRACING_ENDPOINT: "${ZIPKIN_ENDPOINT:-http://zipkin:9411/api/v2/spans}" + + depends_on: + postgres: + condition: "service_healthy" + keycloak: + condition: "service_healthy" + consul: + condition: "service_healthy" + valkey: + condition: "service_healthy" + zipkin: + condition: "service_started" + + healthcheck: + test: [ "CMD", "wget", "--spider", "-q", "http://localhost:8087/actuator/health/readiness" ] + interval: 15s + timeout: 5s + retries: 5 + start_period: 40s + + networks: + meldestelle-network: + aliases: + - "billing-service" + profiles: [ "backend", "all" ] + volumes: + - ./config/app/base-application.yaml:/workspace/config/application.yml:Z + # --- MICROSERVICE: Series Service --- series-service: image: "${DOCKER_REGISTRY:-git.mo-code.at/mo-code}/series-service:${DOCKER_TAG:-latest}" diff --git a/docs/04_Agents/Logs/2026-04-12_Abrechnung_Integration_Curator_Log.md b/docs/04_Agents/Logs/2026-04-12_Abrechnung_Integration_Curator_Log.md new file mode 100644 index 00000000..bc74b4ea --- /dev/null +++ b/docs/04_Agents/Logs/2026-04-12_Abrechnung_Integration_Curator_Log.md @@ -0,0 +1,31 @@ +# Curator Log: Phase 12 - Abrechnung (Billing) & Infrastruktur-Fixes +**Datum:** 2026-04-12 +**Status:** In Arbeit / Integration abgeschlossen + +## 🏗️ Infrastruktur-Updates +- **Billing Service:** + - Dockerfile für `billing-service` erstellt (Multi-Stage Build mit JRE 25). + - Service in `dc-backend.yaml` integriert (Port 8087, Debug 5012). + - Gateway-Routing in `GatewayConfig.kt` für `/api/v1/billing/**` konfiguriert. + - Spring Cloud Consul Discovery im `billing-service` aktiviert und Abhängigkeiten in `build.gradle.kts` ergänzt. + +## 🎨 Frontend-Integration (Billing Context) +- **Domain & Data:** + - `BillingRepository` Interface definiert für Kontenverwaltung und Buchungshistorie. + - `DefaultBillingRepository` implementiert mit Ktor-Client. + - `ApiRoutes` um Billing-Konstanten erweitert. +- **UI & State:** + - `BillingViewModel` auf das reale Repository umgestellt (Mocks entfernt). + - `BillingModule` (Koin) um Repository-Injektion erweitert. + - `TurnierAbrechnungTab` im Turnier-Feature nutzt nun den funktionalen `BillingScreen`. + +## 🧹 Fixes & Aufräumarbeiten +- Behebung von `Unresolved reference` Fehlern in der DI-Konfiguration des `billing-service`. +- Konsolidierung der Koin-Module im `billing-feature`. + +## 🛤️ Roadmap-Status +- Phase 12 (Billing) von "Geplant" auf "In Arbeit" gesetzt. +- Backend-Kommunikation für Konten und Buchungen ist verifiziert. + +--- +*Dokumentiert durch den Curator am 12.04.2026* diff --git a/frontend/core/network/src/commonMain/kotlin/at/mocode/frontend/core/network/ApiRoutes.kt b/frontend/core/network/src/commonMain/kotlin/at/mocode/frontend/core/network/ApiRoutes.kt index 4f70864d..854d6da3 100644 --- a/frontend/core/network/src/commonMain/kotlin/at/mocode/frontend/core/network/ApiRoutes.kt +++ b/frontend/core/network/src/commonMain/kotlin/at/mocode/frontend/core/network/ApiRoutes.kt @@ -41,4 +41,10 @@ object ApiRoutes { const val ROOT = "/api/v1/series" fun stand(serieId: String) = "$ROOT/$serieId/stand" } + + object Billing { + const val ROOT = "/api/v1/billing" + const val KONTEN = "$ROOT/konten" + fun buchungen(kontoId: String) = "$KONTEN/$kontoId/buchungen" + } } diff --git a/frontend/features/billing-feature/src/commonMain/kotlin/at/mocode/frontend/features/billing/data/DefaultBillingRepository.kt b/frontend/features/billing-feature/src/commonMain/kotlin/at/mocode/frontend/features/billing/data/DefaultBillingRepository.kt new file mode 100644 index 00000000..93dc3943 --- /dev/null +++ b/frontend/features/billing-feature/src/commonMain/kotlin/at/mocode/frontend/features/billing/data/DefaultBillingRepository.kt @@ -0,0 +1,39 @@ +package at.mocode.frontend.features.billing.data + +import at.mocode.frontend.core.network.ApiRoutes +import at.mocode.frontend.features.billing.domain.* +import io.ktor.client.* +import io.ktor.client.call.* +import io.ktor.client.request.* +import io.ktor.http.* + +class DefaultBillingRepository( + private val client: HttpClient +) : BillingRepository { + + override suspend fun getOrCreateKonto( + veranstaltungId: String, + personId: String, + personName: String + ): Result = runCatching { + client.get(ApiRoutes.Billing.KONTEN) { + parameter("veranstaltungId", veranstaltungId) + parameter("personId", personId) + parameter("personName", personName) + }.body() + } + + override suspend fun getBuchungen(kontoId: String): Result> = runCatching { + client.get(ApiRoutes.Billing.buchungen(kontoId)).body() + } + + override suspend fun addBuchung( + kontoId: String, + request: BuchungRequest + ): Result = runCatching { + client.post(ApiRoutes.Billing.buchungen(kontoId)) { + contentType(ContentType.Application.Json) + setBody(request) + }.body() + } +} diff --git a/frontend/features/billing-feature/src/commonMain/kotlin/at/mocode/frontend/features/billing/di/BillingModule.kt b/frontend/features/billing-feature/src/commonMain/kotlin/at/mocode/frontend/features/billing/di/BillingModule.kt index dce23a9b..99e196d7 100644 --- a/frontend/features/billing-feature/src/commonMain/kotlin/at/mocode/frontend/features/billing/di/BillingModule.kt +++ b/frontend/features/billing-feature/src/commonMain/kotlin/at/mocode/frontend/features/billing/di/BillingModule.kt @@ -1,10 +1,13 @@ package at.mocode.frontend.features.billing.di +import at.mocode.frontend.features.billing.data.DefaultBillingRepository import at.mocode.frontend.features.billing.domain.BillingCalculator +import at.mocode.frontend.features.billing.domain.BillingRepository import at.mocode.frontend.features.billing.presentation.BillingViewModel import org.koin.dsl.module val billingModule = module { single { BillingCalculator() } - factory { BillingViewModel() } + single { DefaultBillingRepository(get()) } + factory { BillingViewModel(get()) } } diff --git a/frontend/features/billing-feature/src/commonMain/kotlin/at/mocode/frontend/features/billing/domain/BillingRepository.kt b/frontend/features/billing-feature/src/commonMain/kotlin/at/mocode/frontend/features/billing/domain/BillingRepository.kt new file mode 100644 index 00000000..aa587feb --- /dev/null +++ b/frontend/features/billing-feature/src/commonMain/kotlin/at/mocode/frontend/features/billing/domain/BillingRepository.kt @@ -0,0 +1,29 @@ +package at.mocode.frontend.features.billing.domain + +interface BillingRepository { + + /** + * Holt das Konto eines Teilnehmers für eine Veranstaltung. + * Erstellt das Konto automatisch im Backend, falls es noch nicht existiert. + */ + suspend fun getOrCreateKonto( + veranstaltungId: String, + personId: String, + personName: String = "Unbekannt" + ): Result + + /** + * Holt die Buchungshistorie für ein bestimmtes Konto. + */ + suspend fun getBuchungen( + kontoId: String + ): Result> + + /** + * Fügt eine neue Buchung zu einem Konto hinzu. + */ + suspend fun addBuchung( + kontoId: String, + request: BuchungRequest + ): Result +} diff --git a/frontend/features/billing-feature/src/commonMain/kotlin/at/mocode/frontend/features/billing/presentation/BillingViewModel.kt b/frontend/features/billing-feature/src/commonMain/kotlin/at/mocode/frontend/features/billing/presentation/BillingViewModel.kt index a69c653f..2c3bff6c 100644 --- a/frontend/features/billing-feature/src/commonMain/kotlin/at/mocode/frontend/features/billing/presentation/BillingViewModel.kt +++ b/frontend/features/billing-feature/src/commonMain/kotlin/at/mocode/frontend/features/billing/presentation/BillingViewModel.kt @@ -18,78 +18,58 @@ data class BillingUiState( ) @OptIn(ExperimentalUuidApi::class) -class BillingViewModel : ViewModel() { +class BillingViewModel( + private val repository: BillingRepository +) : ViewModel() { private val _uiState = MutableStateFlow(BillingUiState()) val uiState = _uiState.asStateFlow() - fun loadKonten(veranstaltungId: Long) { + fun loadKonto(veranstaltungId: String, personId: String, personName: String) { viewModelScope.launch { _uiState.value = _uiState.value.copy(isLoading = true) - // TODO: Echter API-Call zum backend:billing-service - // Simuliere Daten für MVP (Mock) - val mockKonten = listOf( - TeilnehmerKontoDto( - id = Uuid.random().toString(), - veranstaltungId = veranstaltungId.toString(), - personId = Uuid.random().toString(), - personName = "Max Mustermann", - saldoCent = -4500L, - bemerkungen = "Stallbox reserviert" - ), - TeilnehmerKontoDto( - id = Uuid.random().toString(), - veranstaltungId = veranstaltungId.toString(), - personId = Uuid.random().toString(), - personName = "Erika Musterreiterin", - saldoCent = 1250L - ) - ) - _uiState.value = _uiState.value.copy(konten = mockKonten, isLoading = false) + repository.getOrCreateKonto(veranstaltungId, personId, personName) + .onSuccess { konto -> + _uiState.value = _uiState.value.copy(selectedKonto = konto, error = null) + loadBuchungen(konto.id) + } + .onFailure { + _uiState.value = _uiState.value.copy(isLoading = false, error = it.message) + } } } - fun selectKonto(konto: TeilnehmerKontoDto) { + private fun loadBuchungen(kontoId: String) { viewModelScope.launch { - _uiState.value = _uiState.value.copy(selectedKonto = konto, isLoading = true) - // TODO: API-Call für Buchungen - val mockBuchungen = listOf( - BuchungDto( - id = Uuid.random().toString(), - kontoId = konto.id, - betragCent = -4000L, - typ = "NENNUNG", - verwendungszweck = "Nenngeld Bewerb 1", - gebuchtAm = "2026-04-10T10:00:00Z" - ), - BuchungDto( - id = Uuid.random().toString(), - kontoId = konto.id, - betragCent = -500L, - typ = "GEBUEHR", - verwendungszweck = "Systemgebühr", - gebuchtAm = "2026-04-10T10:05:00Z" - ) - ) - _uiState.value = _uiState.value.copy(buchungen = mockBuchungen, isLoading = false) + _uiState.value = _uiState.value.copy(isLoading = true) + repository.getBuchungen(kontoId) + .onSuccess { buchungen -> + _uiState.value = _uiState.value.copy(buchungen = buchungen, isLoading = false, error = null) + } + .onFailure { + _uiState.value = _uiState.value.copy(isLoading = false, error = it.message) + } } } fun buche(betragCent: Long, zweck: String) { val konto = _uiState.value.selectedKonto ?: return viewModelScope.launch { - // TODO: API-Call POST /billing/konten/{id}/buchungen - val neueBuchung = BuchungDto( - id = Uuid.random().toString(), - kontoId = konto.id, - betragCent = betragCent, - typ = "MANUELL", - verwendungszweck = zweck, - gebuchtAm = "2026-04-10T13:00:00Z" - ) - _uiState.value = _uiState.value.copy( - buchungen = listOf(neueBuchung) + _uiState.value.buchungen, - selectedKonto = konto.copy(saldoCent = konto.saldoCent + betragCent) - ) + _uiState.value = _uiState.value.copy(isLoading = true) + val request = BuchungRequest(betragCent = betragCent, verwendungszweck = zweck) + repository.addBuchung(konto.id, request) + .onSuccess { aktualisiertesKonto -> + _uiState.value = _uiState.value.copy(selectedKonto = aktualisiertesKonto) + loadBuchungen(konto.id) + } + .onFailure { + _uiState.value = _uiState.value.copy(isLoading = false, error = it.message) + } } } + + // Für Abwärtskompatibilität oder Listenansicht (optional) + fun selectKonto(konto: TeilnehmerKontoDto) { + _uiState.value = _uiState.value.copy(selectedKonto = konto) + loadBuchungen(konto.id) + } }