diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml new file mode 100644 index 0000000..87ebe6d --- /dev/null +++ b/.github/workflows/docker-build.yml @@ -0,0 +1,58 @@ +name: Build and Push Docker Image only on Conventional Commits + +on: + push: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + + env: + DOCKER_REGISTRY: git.trai-infra.comstack.de + DOCKER_IMAGE: marian.lippitz/tap-frontend + + steps: + - name: Checkout Code + uses: actions/checkout@v3 + + - name: Validate Conventional Commit + id: validate + run: | + if git log -1 --pretty=%B | grep -Eq "^(feat|fix|docs|style|refactor|perf|test|chore|build|ci|revert)(\(.+\))?: .+"; then + echo "valid=true" >> $GITHUB_OUTPUT + else + echo "valid=false" >> $GITHUB_OUTPUT + fi + + - name: Stop if not a Conventional Commit + if: ${{ steps.validate.outputs.valid == 'false' }} + run: | + echo "Latest commit is not a valid Conventional Commit. Skipping build." + exit 0 + + - name: Docker Login + env: + DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} + DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} + run: | + echo "$DOCKER_PASSWORD" | docker login $DOCKER_REGISTRY -u "$DOCKER_USERNAME" --password-stdin + + - name: Build Docker Image + run: | + docker build -t "$DOCKER_REGISTRY/$DOCKER_IMAGE:latest" . + + - name: Push Docker Image + run: | + docker push "$DOCKER_REGISTRY/$DOCKER_IMAGE:latest" + + - name: Trigger Kubernetes Redeployment + uses: alpine/kubectl:1.34.1 + env: + KUBECONFIG: ${{ secrets.KUBECONFIG }} + with: + args: > + patch deployment tap-frontend -n frontend + -p '{"spec":{"template":{"metadata":{"annotations":{"kubectl.kubernetes.io/restartedAt":"'"$(date -u +'%Y-%m-%dT%H:%M:%SZ')"'"}}}}}' + && kubectl rollout status deployment tap-frontend -n frontend --timeout=120s \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 6365e0e..5f97d80 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,9 +24,11 @@ # Provide php alias RUN ln -sf /usr/bin/php82 /usr/bin/php + # Add custom PHP logging configuration + COPY deploy/php-fpm/zz-logging.ini /etc/php82/conf.d/ + # Create user, dirs, and sockets - RUN addgroup -S web && adduser -S web -G web \ - && mkdir -p /run/nginx /run/php /var/www/html /var/log/supervisor + RUN mkdir -p /run/nginx /run/php /var/www/html /var/log/supervisor # Configure PHP-FPM socket RUN sed -i 's|^listen = 127\\.0\\.0\\.1:9000|listen = /run/php/php-fpm.sock|' /etc/php82/php-fpm.d/www.conf \ @@ -51,8 +53,14 @@ # Permissions for Laravel writable directories RUN mkdir -p storage bootstrap/cache \ && chown -R nginx:nginx storage bootstrap/cache \ - && chmod -R ug+rwX storage bootstrap/cache - + && chmod -R ug+rwX storage bootstrap/cache + +# Create Nginx error log file with correct permissions +RUN mkdir -p /var/log/nginx \ + && touch /var/log/nginx/error.log \ + && chown -R nginx:nginx /var/log/nginx \ + && chown -R nginx:nginx /var/log/php82 \ + && chmod -R 664 /var/log/nginx/error.log # Build frontend assets RUN npm ci && npm run build diff --git a/deploy/docker-entrypoint.sh b/deploy/docker-entrypoint.sh index 99e46cc..57ee9dd 100644 --- a/deploy/docker-entrypoint.sh +++ b/deploy/docker-entrypoint.sh @@ -1,27 +1,88 @@ #!/bin/sh +# Docker entrypoint for Nginx + PHP/Laravel set -eu -SERVER_NAME="${SERVER_NAME:-$(hostname -f 2>/dev/null || hostname)}" +# -------- defaults (override via env) -------- +: "${APP_DIR:=/var/www/html}" +: "${NGINX_TEMPLATE:=/etc/nginx/templates/default.conf.template}" +: "${NGINX_HTTPD_DIR:=/etc/nginx/http.d}" +: "${NGINX_CONF_OUT:=${NGINX_HTTPD_DIR}/default.conf}" +: "${HTPASSWD_PATH:=/etc/nginx/htpasswd}" +: "${BASIC_AUTH_REALM:=Restricted Content}" +: "${SUPERVISOR_CONF:=/etc/supervisor/supervisord.conf}" + +log() { printf '[entrypoint] %s\n' "$*"; } +err() { printf '[entrypoint][ERROR] %s\n' "$*" >&2; } +die() { err "$*"; exit 1; } +need() { command -v "$1" >/dev/null 2>&1 || die "Missing required command: $1"; } + +# required tools +need envsubst; need nginx; need php; need supervisord + +# server name +SERVER_NAME="${SERVER_NAME:-$(hostname -f 2>/dev/null || hostname 2>/dev/null || echo localhost)}" export SERVER_NAME -# Ensure dirs -mkdir -p /etc/nginx/http.d /etc/nginx/conf.d /run/nginx /run/php +# truthy helper +is_truthy() { + case "${1:-}" in 1|true|TRUE|yes|YES|on|ON) return 0;; *) return 1;; esac +} -# Render vhost ONLY into http.d so it's inside http{} context -if [ -f /etc/nginx/templates/default.conf.template ]; then - envsubst '$SERVER_NAME' < /etc/nginx/templates/default.conf.template > /etc/nginx/http.d/default.conf +# -------- basic auth directives -------- +BASIC_AUTH_DIRECTIVES="" +if is_truthy "${ENABLE_BASIC_AUTH:-}"; then + log "Enabling basic authentication" + [ -f "$HTPASSWD_PATH" ] || die "htpasswd not found: $HTPASSWD_PATH" + BASIC_AUTH_DIRECTIVES=$(cat <<'EOF' + auth_basic "__REALM__"; + auth_basic_user_file __HTPASSWD__; +EOF +) + # safe placeholder substitution + esc() { printf '%s' "$1" | sed 's/[&/\]/\\&/g'; } + BASIC_AUTH_DIRECTIVES=$(printf '%s' "$BASIC_AUTH_DIRECTIVES" \ + | sed "s|__REALM__|$(esc "$BASIC_AUTH_REALM")|g" \ + | sed "s|__HTPASSWD__|$(esc "$HTPASSWD_PATH")|g") else - echo "[entrypoint] ERROR: missing /etc/nginx/templates/default.conf.template" >&2 - exit 1 + log "Basic authentication is disabled" +fi +export BASIC_AUTH_DIRECTIVES + +# -------- render nginx config (atomic) -------- +[ -f "$NGINX_TEMPLATE" ] || die "Missing template: $NGINX_TEMPLATE" +[ -d "$NGINX_HTTPD_DIR" ] || die "Missing nginx conf dir: $NGINX_HTTPD_DIR" + +tmpconf="$(mktemp "${NGINX_CONF_OUT}.XXXXXX")" +envsubst '$SERVER_NAME,$BASIC_AUTH_DIRECTIVES' <"$NGINX_TEMPLATE" >"$tmpconf" \ + || die "envsubst failed" + +mv -f "$tmpconf" "$NGINX_CONF_OUT" +log "Wrote nginx config -> $NGINX_CONF_OUT" + +# final sanity test against active config tree +nginx -t || die "nginx config test failed after install" + +# -------- Laravel cache clears (best-effort) -------- +if [ -f "$APP_DIR/artisan" ]; then + log "Clearing Laravel caches" + ( cd "$APP_DIR" && php artisan config:clear || log "config:clear failed (continuing)" ) + ( cd "$APP_DIR" && php artisan cache:clear || log "cache:clear failed (continuing)" ) +else + log "No Laravel app at $APP_DIR; skipping artisan" fi -echo "[entrypoint] Using SERVER_NAME=$SERVER_NAME" -nginx -t || { echo "[entrypoint] nginx config test failed" >&2; cat /var/log/nginx/error.log || true; exit 1; } +# -------- entrypoint behavior -------- +# If the first arg is an option (starts with -), assume supervisord. +if [ "${1:-}" ] && [ "${1#-}" != "$1" ]; then + set -- /usr/bin/supervisord -n -c "$SUPERVISOR_CONF" "$@" +fi -php artisan config:clear -php artisan cache:clear +# If they provided a command (e.g., sh, bash, php -v), run it. +if [ "${1:-}" ] && [ "$1" != "/usr/bin/supervisord" ] && [ "$1" != "supervisord" ]; then + log "Executing custom command: $*" + exec "$@" +fi -envsubst '$SERVER_NAME' < /etc/nginx/templates/default.conf.template > /etc/nginx/http.d/default.conf - -exec /usr/bin/supervisord -n -c /etc/supervisor/supervisord.conf - \ No newline at end of file +# Default: start supervisord in the foreground (PID 1) +log "Starting supervisord" +exec /usr/bin/supervisord -n -c "$SUPERVISOR_CONF" diff --git a/deploy/nginx/nginx.conf b/deploy/nginx/nginx.conf index b07e7d0..c0e766c 100644 --- a/deploy/nginx/nginx.conf +++ b/deploy/nginx/nginx.conf @@ -1,7 +1,9 @@ -user nginx; +user nginx; worker_processes auto; -error_log /var/log/nginx/error.log warn; -pid /run/nginx.pid; + +# Send logs to container fds (works regardless of privileges) +error_log /proc/self/fd/2 warn; +pid /run/nginx/nginx.pid; events { worker_connections 1024; @@ -15,14 +17,13 @@ http { '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; - access_log /dev/stdout main; - error_log /dev/stderr warn; + access_log /proc/self/fd/1 main; + # (No extra error_log here; the top-level one above is enough) sendfile on; keepalive_timeout 65; - # Server Blöcke sind NICHT hier! - # Stattdessen in: + # Include vhosts (your Laravel server block lives in one of these) include /etc/nginx/conf.d/*.conf; include /etc/nginx/http.d/*.conf; } diff --git a/deploy/nginx/nginx.default.conf.template b/deploy/nginx/nginx.default.conf.template index f115970..2eaa5fd 100644 --- a/deploy/nginx/nginx.default.conf.template +++ b/deploy/nginx/nginx.default.conf.template @@ -5,42 +5,89 @@ server { root /var/www/html/public; index index.php; - # Cache-Control für statische Dateien - location ~* \.(?:ico|css|js|gif|jpe?g|png|svg|woff2?|eot|ttf|otf|webp|avif)$ { + # ------------------------------------------------- + # Redirect all HTTP requests to HTTPS (optional, remove if TLS is handled upstream) + # ------------------------------------------------- + # return 301 https://$host$request_uri; + + # ------------------------------------------------- + # Livewire + Laravel routes (must come before static block) + # ------------------------------------------------- + location ^~ /livewire/ { + try_files $uri /index.php?$query_string; + expires off; + add_header Cache-Control "no-store"; + } + + # ------------------------------------------------- + # Static assets with long caching + # ------------------------------------------------- + location ~* \.(?:ico|css|js|gif|jpe?g|png|svg|woff2?|eot|ttf|otf|webp|avif)(\?.*)?$ { expires 7d; add_header Cache-Control "public, max-age=604800, immutable"; try_files $uri =404; access_log off; } - # Haupt-Entry-Point (Laravel) + # ------------------------------------------------- + # Main Laravel entry point + # ------------------------------------------------- location / { try_files $uri $uri/ /index.php?$query_string; + ${BASIC_AUTH_DIRECTIVES} } - # PHP-FPM Verarbeitung + # ------------------------------------------------- + # PHP-FPM configuration (matches your Alpine socket setup) + # ------------------------------------------------- location ~ \.php$ { include fastcgi_params; fastcgi_index index.php; - fastcgi_pass 127.0.0.1:9000; + fastcgi_pass 127.0.0.1:9000; # <-- switch from socket to TCP fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param DOCUMENT_ROOT $document_root; + fastcgi_read_timeout 60s; fastcgi_buffering on; fastcgi_buffers 16 16k; fastcgi_buffer_size 32k; - # Forward the X-Forwarded-Proto header - fastcgi_param HTTPS $http_x_forwarded_proto; + # --- Proxy headers for HTTPS and load balancers --- + fastcgi_param HTTP_X_FORWARDED_PROTO $http_x_forwarded_proto; + fastcgi_param HTTP_X_FORWARDED_HOST $http_host; + fastcgi_param HTTP_X_FORWARDED_FOR $proxy_add_x_forwarded_for; + + # --- Tell PHP it's HTTPS when forwarded --- + set $https_flag ""; + if ($http_x_forwarded_proto = "https") { set $https_flag "on"; } + fastcgi_param HTTPS $https_flag; } - # Maximale Client Upload-Größe + # ------------------------------------------------- + # Upload limits + # ------------------------------------------------- client_max_body_size 64m; - # Sicherheits-Header + # ------------------------------------------------- + # Security headers + # ------------------------------------------------- add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; - add_header Referrer-Policy "strict-origin-when-cross-origin" always; - # Gzip-Kompression -} \ No newline at end of file + # ------------------------------------------------- + # Gzip compression + # ------------------------------------------------- + gzip on; + gzip_types + text/plain + text/css + application/json + application/javascript + text/xml + application/xml + application/xml+rss + image/svg+xml; + gzip_min_length 1000; + gzip_proxied any; + gzip_vary on; +} diff --git a/deploy/php-fpm/zz-logging.conf b/deploy/php-fpm/zz-logging.ini similarity index 100% rename from deploy/php-fpm/zz-logging.conf rename to deploy/php-fpm/zz-logging.ini diff --git a/deploy/php-fpm/zz-socket.conf b/deploy/php-fpm/zz-socket.conf deleted file mode 100644 index 7631ab7..0000000 --- a/deploy/php-fpm/zz-socket.conf +++ /dev/null @@ -1,3 +0,0 @@ -listen = 127.0.0.1:9000 -user = nginx -group = nginx diff --git a/docker/healthcheck.sh b/docker/healthcheck.sh deleted file mode 100644 index 38ca447..0000000 --- a/docker/healthcheck.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/sh -set -eu - -PORT="${APP_PORT:-8080}" - -# 1) App Health -curl -fsS "http://127.0.0.1:${PORT}/healthz" >/dev/null - -# 2) PHP-FPM Ping via Nginx -curl -fsS "http://127.0.0.1:${PORT}/fpm-ping" >/dev/null - -exit 0 diff --git a/docker/nginx.conf b/docker/nginx.conf deleted file mode 100644 index 2c9ef12..0000000 --- a/docker/nginx.conf +++ /dev/null @@ -1,29 +0,0 @@ -# keine 'user' Direktive im non-root Betrieb -worker_processes auto; -error_log /dev/stderr warn; -pid /tmp/nginx.pid; - -events { worker_connections 1024; } - -http { - include /etc/nginx/mime.types; - default_type application/octet-stream; - - log_format main '$remote_addr - $remote_user [$time_local] "$request" ' - '$status $body_bytes_sent "$http_referer" ' - '"$http_user_agent" "$http_x_forwarded_for"'; - access_log /dev/stdout main; - - sendfile on; - keepalive_timeout 65; - server_tokens off; - - # Temp/Cache-Pfade, die 'nginx' gehören - client_body_temp_path /var/lib/nginx/tmp/client_body 1 2; - proxy_temp_path /var/lib/nginx/tmp/proxy 1 2; - fastcgi_temp_path /var/lib/nginx/tmp/fastcgi 1 2; - scgi_temp_path /var/lib/nginx/tmp/scgi 1 2; - uwsgi_temp_path /var/lib/nginx/tmp/uwsgi 1 2; - - include /etc/nginx/conf.d/*.conf; -} diff --git a/docker/site.conf b/docker/site.conf deleted file mode 100644 index 55d893c..0000000 --- a/docker/site.conf +++ /dev/null @@ -1,60 +0,0 @@ -server { - listen 8080; - server_name localhost; - - root /var/www/html/public; - index index.php index.html; - - # Health checks - location = /healthz { - access_log off; - return 200 "ok\n"; - } - - # FPM ping/status (optional, secure in prod) - location = /fpm-ping { - access_log off; - include fastcgi_params; - fastcgi_pass 127.0.0.1:9000; - fastcgi_param SCRIPT_FILENAME $document_root/index.php; - fastcgi_param SCRIPT_NAME /ping; - fastcgi_param PATH_INFO /ping; - } - - location = /fpm-status { - access_log off; - include fastcgi_params; - fastcgi_pass 127.0.0.1:9000; - fastcgi_param SCRIPT_FILENAME $document_root/index.php; - fastcgi_param SCRIPT_NAME /status; - fastcgi_param PATH_INFO /status; - } - - # Main app - location / { - try_files $uri $uri/ /index.php?$query_string; - } - - # PHP handling - location ~ \.php$ { - include fastcgi_params; - fastcgi_pass 127.0.0.1:9000; - fastcgi_index index.php; - fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; - fastcgi_read_timeout 60s; - fastcgi_buffers 16 16k; - } - - # Deny hidden files - location ~ /\.ht { - deny all; - } - - # Cache static assets - location ~* \.(?:css|js|jpg|jpeg|gif|png|svg|ico|webp|woff2?)$ { - access_log off; - expires 7d; - add_header Cache-Control "public"; - try_files $uri =404; - } -} diff --git a/swagger.yaml b/swagger.yaml new file mode 100644 index 0000000..9488ef6 --- /dev/null +++ b/swagger.yaml @@ -0,0 +1,940 @@ +openapi: 3.1.0 +info: + title: Trusted AI Analyst Data API + version: '1.0.0' + description: > + Data endpoints powering the analyst dashboard, portfolio-wide transaction review, + company search, and per-company transaction drill-down experiences. +servers: + - url: http://trusted_ai.test/api + description: Local development (composer run dev) + - url: https://api.trusted-ai.com + description: Production +tags: + - name: Dashboard + description: KPIs and alert overviews for the landing dashboard. + - name: Transactions + description: Portfolio-wide transaction review tooling. + - name: Companies + description: Company discovery and drill-down data. +components: + securitySchemes: + sessionCookie: + type: apiKey + in: cookie + name: laravel_session + description: Browser session issued after login; required for all endpoints documented here. + schemas: + PaginationMeta: + type: object + required: + - page + - perPage + - total + - lastPage + properties: + page: + type: integer + minimum: 1 + example: 1 + perPage: + type: integer + minimum: 1 + example: 12 + total: + type: integer + minimum: 0 + example: 128 + lastPage: + type: integer + minimum: 1 + example: 11 + CountAmountSummary: + type: object + required: + - count + - amount + properties: + count: + type: integer + minimum: 0 + example: 42 + amount: + type: number + format: double + example: 2750000.5 + description: Monetary volume in EUR unless noted otherwise. + StatusOption: + type: object + required: + - value + - label + properties: + value: + type: string + example: true_positive + label: + type: string + example: Bestätigte Treffer + ChannelOption: + type: object + required: + - value + - label + properties: + value: + type: string + example: SEPA + label: + type: string + example: SEPA + RiskSegment: + type: object + required: + - label + - count + - volume + properties: + label: + type: string + example: Kritisch (≥80) + count: + type: integer + example: 17 + volume: + type: number + format: double + example: 8900000.0 + TopCompanyAlert: + type: object + required: + - companyId + - companyName + - ticker + - sector + - alerts + - alertVolume + - avgRiskScore + properties: + companyId: + type: integer + example: 24 + companyName: + type: string + example: Allianz SE + ticker: + type: string + example: ALV + sector: + type: string + example: Insurance + alerts: + type: integer + example: 6 + alertVolume: + type: number + format: double + example: 1450000.0 + avgRiskScore: + type: integer + minimum: 0 + maximum: 100 + example: 82 + DashboardOverview: + type: object + required: + - generatedAt + - activeAnalysts + - totalTransactions + - totalVolume + - alertsToday + - averageAlertsPerAnalyst + - precisionRate + - activeAlerts + - riskSegments + - topCompanies + - runbooksExecutedToday + properties: + generatedAt: + type: string + format: date-time + example: '2025-01-19T07:30:00Z' + activeAnalysts: + type: integer + example: 24 + totalTransactions: + type: integer + example: 312 + totalVolume: + type: number + format: double + example: 98765432.1 + alertsToday: + type: integer + example: 56 + averageAlertsPerAnalyst: + type: integer + example: 3 + precisionRate: + type: integer + minimum: 0 + maximum: 100 + example: 84 + activeAlerts: + type: object + required: + - count + - highRiskCount + properties: + count: + type: integer + example: 128 + highRiskCount: + type: integer + example: 19 + riskSegments: + type: array + items: + $ref: '#/components/schemas/RiskSegment' + topCompanies: + type: array + maxItems: 3 + items: + $ref: '#/components/schemas/TopCompanyAlert' + runbooksExecutedToday: + type: integer + example: 41 + TransactionStatus: + type: string + enum: + - true_positive + - false_positive + - cleared + CompanySummary: + type: object + required: + - id + - name + - legalName + - ticker + - sector + - country + - headquarters + - kycRiskLevel + - summary + properties: + id: + type: integer + example: 12 + name: + type: string + example: Allianz + legalName: + type: string + example: Allianz SE + ticker: + type: string + example: ALV + sector: + type: string + example: Insurance + country: + type: string + example: Germany + headquarters: + type: string + example: Munich + kycRiskLevel: + type: string + example: high + summary: + type: string + example: Multinational insurance provider with EMEA focus. + CompanyDetail: + allOf: + - $ref: '#/components/schemas/CompanySummary' + - type: object + properties: + createdAt: + type: string + format: date-time + nullable: true + updatedAt: + type: string + format: date-time + nullable: true + TransactionPreview: + type: object + required: + - id + - company + - counterparty + - counterpartyCountry + - status + - statusLabel + - requiresReview + - riskScore + - amount + - currency + - reference + - channel + - executedAt + properties: + id: + type: integer + example: 512 + company: + $ref: '#/components/schemas/CompanySummary' + counterparty: + type: string + example: Alpine Holdings Ltd. + counterpartyCountry: + type: string + example: Switzerland + status: + $ref: '#/components/schemas/TransactionStatus' + statusLabel: + type: string + example: Bestätigter Treffer + requiresReview: + type: boolean + example: true + riskScore: + type: integer + minimum: 0 + maximum: 100 + example: 87 + amount: + type: number + format: double + example: 245000.75 + currency: + type: string + example: EUR + reference: + type: string + example: PAY-2024-10-1942 + channel: + type: string + example: SWIFT + executedAt: + type: string + format: date-time + example: '2025-01-18T09:14:00Z' + flaggedReason: + type: string + nullable: true + example: Counterparty on sanctions watchlist + TransactionSignal: + type: object + required: + - type + - value + properties: + type: + type: string + example: Adverse Media + value: + type: string + example: Enforcement action reported in 2024-11 + weight: + type: number + format: double + nullable: true + example: 0.8 + AlertSummary: + type: object + required: + - id + - counterparty + - executedAt + - channel + - status + - statusLabel + properties: + id: + type: integer + example: 911 + counterparty: + type: string + example: Northbridge Trading Ltd. + executedAt: + type: string + format: date-time + example: '2025-01-18T14:52:00Z' + channel: + type: string + example: SWIFT + status: + $ref: '#/components/schemas/TransactionStatus' + statusLabel: + type: string + example: Bestätigter Treffer + riskScore: + type: integer + minimum: 0 + maximum: 100 + example: 92 + TransactionHistoryItem: + type: object + required: + - id + - executedAt + - channel + - reference + - amount + - currency + - riskScore + properties: + id: + type: integer + example: 877 + executedAt: + type: string + format: date-time + example: '2024-12-22T15:37:00Z' + channel: + type: string + example: SWIFT + reference: + type: string + example: PAY-2024-12-1187 + amount: + type: number + format: double + example: 99000.0 + currency: + type: string + example: EUR + riskScore: + type: integer + minimum: 0 + maximum: 100 + example: 74 + RecommendedAction: + type: object + required: + - title + - description + properties: + title: + type: string + example: Verdachtsmeldung vorbereiten + description: + type: string + example: Erstellen Sie den Meldeentwurf für die FIU und sichern Sie Belege. + TransactionDetail: + allOf: + - $ref: '#/components/schemas/TransactionPreview' + - type: object + required: + - flaggedBy + - flaggedReason + - signals + properties: + flaggedBy: + type: string + example: Screening Engine + flaggedReason: + type: string + example: Counterparty matched to EU sanctions list + signals: + type: array + items: + $ref: '#/components/schemas/TransactionSignal' + TransactionCaseFile: + allOf: + - $ref: '#/components/schemas/TransactionDetail' + - type: object + properties: + recommendedActions: + type: array + items: + $ref: '#/components/schemas/RecommendedAction' + counterpartyHistory: + type: array + items: + $ref: '#/components/schemas/TransactionHistoryItem' + TransactionMetrics: + type: object + required: + - total + - byStatus + properties: + total: + $ref: '#/components/schemas/CountAmountSummary' + byStatus: + type: object + additionalProperties: + $ref: '#/components/schemas/CountAmountSummary' + description: Breakdown keyed by transaction status. + CompanyTransactionMetrics: + type: object + required: + - totalCount + - totalVolume + - openAlerts + - highRiskShare + - last30Days + - byStatus + properties: + totalCount: + type: integer + example: 96 + totalVolume: + type: number + format: double + example: 7200000.0 + openAlerts: + $ref: '#/components/schemas/CountAmountSummary' + highRiskShare: + type: integer + minimum: 0 + maximum: 100 + example: 28 + last30Days: + $ref: '#/components/schemas/CountAmountSummary' + byStatus: + type: object + additionalProperties: + $ref: '#/components/schemas/CountAmountSummary' + TransactionCollectionResponse: + type: object + required: + - filters + - metrics + - data + - pagination + properties: + filters: + type: object + properties: + statusOptions: + type: array + items: + $ref: '#/components/schemas/StatusOption' + perPage: + type: integer + example: 12 + metrics: + $ref: '#/components/schemas/TransactionMetrics' + data: + type: array + items: + $ref: '#/components/schemas/TransactionPreview' + pagination: + $ref: '#/components/schemas/PaginationMeta' + selectedTransactionId: + type: integer + nullable: true + example: 512 + selectedTransaction: + allOf: + - $ref: '#/components/schemas/TransactionDetail' + nullable: true + CompanySearchOverview: + type: object + required: + - totalCompanies + - openAlerts + - openAlertVolume + - averageRiskScore + - watchlistHits + - automationShare + properties: + totalCompanies: + type: integer + example: 40 + openAlerts: + type: integer + example: 112 + openAlertVolume: + type: number + format: double + example: 5640000.0 + averageRiskScore: + type: integer + minimum: 0 + maximum: 100 + example: 71 + watchlistHits: + type: integer + example: 9 + automationShare: + type: integer + minimum: 0 + maximum: 100 + example: 82 + CompanyAlertPreview: + type: object + required: + - transactionId + - counterparty + - executedAt + - channel + - flaggedReason + properties: + transactionId: + type: integer + example: 731 + counterparty: + type: string + example: Baltic Commodities LLC + executedAt: + type: string + format: date-time + example: '2025-01-17T11:48:00Z' + channel: + type: string + example: SWIFT + flaggedReason: + type: string + example: Pattern matches sanctions typology + riskScore: + type: integer + minimum: 0 + maximum: 100 + example: 88 + CompanySearchResult: + allOf: + - $ref: '#/components/schemas/CompanySummary' + - type: object + required: + - alertCount + - alertVolume + - alertRiskScore + - latestAlert + properties: + alertCount: + type: integer + example: 7 + alertVolume: + type: number + format: double + example: 980000.0 + alertRiskScore: + type: integer + minimum: 0 + maximum: 100 + example: 79 + latestAlert: + allOf: + - $ref: '#/components/schemas/CompanyAlertPreview' + CompanySearchResponse: + type: object + required: + - overview + - results + - meta + properties: + overview: + $ref: '#/components/schemas/CompanySearchOverview' + results: + type: array + items: + $ref: '#/components/schemas/CompanySearchResult' + meta: + type: object + required: + - query + - resultCount + - limit + properties: + query: + type: string + nullable: true + example: Allianz + resultCount: + type: integer + example: 6 + limit: + type: integer + example: 6 + CompanyTransactionsResponse: + type: object + required: + - company + - filters + - metrics + - data + - pagination + properties: + company: + $ref: '#/components/schemas/CompanyDetail' + filters: + type: object + properties: + statusOptions: + type: array + items: + $ref: '#/components/schemas/StatusOption' + channelOptions: + type: array + items: + $ref: '#/components/schemas/ChannelOption' + metrics: + $ref: '#/components/schemas/CompanyTransactionMetrics' + data: + type: array + items: + $ref: '#/components/schemas/TransactionPreview' + pagination: + $ref: '#/components/schemas/PaginationMeta' + selectedTransactionId: + type: integer + nullable: true + example: 877 + selectedTransaction: + allOf: + - $ref: '#/components/schemas/TransactionCaseFile' + nullable: true + recentAlerts: + type: array + items: + $ref: '#/components/schemas/AlertSummary' +paths: + /dashboard/overview: + get: + tags: + - Dashboard + operationId: getDashboardOverview + summary: Retrieve dashboard overview metrics + description: > + Aggregated KPIs and alert slices needed for the dashboard hero cards and side panels. + security: + - sessionCookie: [] + responses: + '200': + description: Overview data ready for dashboard rendering. + content: + application/json: + schema: + $ref: '#/components/schemas/DashboardOverview' + '401': + description: Session invalid or expired. + /transactions: + get: + tags: + - Transactions + operationId: listTransactions + summary: List transactions with global filters + description: > + Returns the paginated transaction stream together with per-status metrics and default selection, + mirroring the Livewire transaction review experience. + security: + - sessionCookie: [] + parameters: + - name: search + in: query + schema: + type: string + description: Match against reference, counterparty, or company identifiers. + - name: status + in: query + schema: + type: string + enum: + - all + - true_positive + - false_positive + - cleared + default: all + description: Filter by review status; `all` keeps every status. + - name: page + in: query + schema: + type: integer + minimum: 1 + default: 1 + - name: perPage + in: query + schema: + type: integer + minimum: 1 + maximum: 100 + default: 12 + responses: + '200': + description: Paginated transaction set with metrics. + content: + application/json: + schema: + $ref: '#/components/schemas/TransactionCollectionResponse' + '401': + description: Session invalid or expired. + /transactions/{transactionId}: + get: + tags: + - Transactions + operationId: getTransactionCaseFile + summary: Fetch a single transaction case file + description: > + Detailed transaction record used when an analyst expands a case in the transaction review workspace. + security: + - sessionCookie: [] + parameters: + - name: transactionId + in: path + required: true + schema: + type: integer + responses: + '200': + description: Transaction detail with detection signals. + content: + application/json: + schema: + $ref: '#/components/schemas/TransactionDetail' + '401': + description: Session invalid or expired. + '404': + description: Transaction not found. + /companies/search: + get: + tags: + - Companies + operationId: searchCompanies + summary: Search companies and retrieve screening overview + description: > + Provides the metrics and top results required to populate the company search tiles. + security: + - sessionCookie: [] + parameters: + - name: q + in: query + schema: + type: string + description: Free-text query across name, legal name, ticker, or sector. + - name: limit + in: query + schema: + type: integer + minimum: 1 + maximum: 25 + default: 6 + responses: + '200': + description: Matching companies with alert context. + content: + application/json: + schema: + $ref: '#/components/schemas/CompanySearchResponse' + '401': + description: Session invalid or expired. + /companies/{companyId}: + get: + tags: + - Companies + operationId: getCompany + summary: Retrieve a company profile + description: > + Returns core firmographics and KYC risk tier needed for per-company transaction pages. + security: + - sessionCookie: [] + parameters: + - name: companyId + in: path + required: true + schema: + type: integer + responses: + '200': + description: Company detail. + content: + application/json: + schema: + $ref: '#/components/schemas/CompanyDetail' + '401': + description: Session invalid or expired. + '404': + description: Company not found. + /companies/{companyId}/transactions: + get: + tags: + - Companies + operationId: listCompanyTransactions + summary: List transactions for a specific company + description: > + Supplies per-company metrics, channel/status filters, paginated cases, and the current selection + for the company drill-down view. + security: + - sessionCookie: [] + parameters: + - name: companyId + in: path + required: true + schema: + type: integer + - name: status + in: query + schema: + type: string + enum: + - all + - true_positive + - false_positive + - cleared + default: all + - name: channel + in: query + schema: + type: string + default: all + description: Channel label returned in channelOptions; use `all` for no filtering. + - name: page + in: query + schema: + type: integer + minimum: 1 + default: 1 + - name: perPage + in: query + schema: + type: integer + minimum: 1 + maximum: 100 + default: 10 + responses: + '200': + description: Company-specific transaction data with metrics. + content: + application/json: + schema: + $ref: '#/components/schemas/CompanyTransactionsResponse' + '401': + description: Session invalid or expired. + '404': + description: Company not found or no transactions. + /companies/{companyId}/transactions/{transactionId}: + get: + tags: + - Companies + operationId: getCompanyTransactionCaseFile + summary: Fetch a company transaction case file with history + description: > + Returns the selected transaction enriched with recommended actions and counterparty history + used in the right-hand panel of the company drill-down. + security: + - sessionCookie: [] + parameters: + - name: companyId + in: path + required: true + schema: + type: integer + - name: transactionId + in: path + required: true + schema: + type: integer + responses: + '200': + description: Transaction case file for the company context. + content: + application/json: + schema: + $ref: '#/components/schemas/TransactionCaseFile' + '401': + description: Session invalid or expired. + '404': + description: Transaction not found for the given company.