Author SHA1 Message Date
cbazza 94d02736f7 ci: add automated Gitea Actions deployment workflow for Synology NAS
Build and Deploy to Synology NAS / test (push) Failing after 1h20m56s
Build and Deploy to Synology NAS / build (push) Failing after 11m7s
Build and Deploy to Synology NAS / deploy (push) Has been cancelled
Build and Deploy to Synology NAS / notify (push) Has been cancelled
- Add Gitea Actions workflow for automated build and deployment
- Add deployment script for Synology NAS
- Add Docker Compose configurations for production deployment
- Add Gitea runner setup
- Add comprehensive documentation for CI/CD setup
- Add health check endpoint for deployment verification
- Update .gitignore for CI/CD artifacts
2025-12-17 13:02:38 +01:00
cbazza 2f7bd923df local-docker-db branch openend
Build and Push Docker Image only on Conventional Commits / build (push) Has been cancelled
2025-12-03 12:59:30 +01:00
Marian aa8de00969 fix: seeder
Build and Push Docker Image only on Conventional Commits / build (push) Has been cancelled
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
2025-11-27 09:26:43 +00:00
Marian 741655c548 feat: add init seeder 2025-11-27 09:20:47 +00:00
Marian e598cc475c fix: startup 2025-11-26 14:09:30 +00:00
Marian 09dc2f1125 fix: entrypoint 2025-11-26 13:59:02 +00:00
Marian c5d3e067a0 fix: init problems 2025-11-26 13:53:39 +00:00
Marian 109434b02b fix: read search path from env 2025-11-26 10:34:13 +00:00
Marian 355bbaf0c1 fix: ci 2025-11-26 10:18:52 +00:00
cbazza b33ca76667 chore: project cleanup - remove unused dependencies and routes
- Remove unused axios package and 22 dependencies
- Fix 2 moderate npm security vulnerabilities
- Remove test route /test-zweite-db (no longer needed)
- Remove unused DB facade import from routes/web.php

This cleanup reduces bundle size and improves maintainability.
2025-11-21 12:05:52 +01:00
cbazza 3cfb69f0c7 chore: improve .gitignore with better organization
- Organized entries into logical sections (IDEs, macOS, MCP, etc.)
- Fixed .DS_Store pattern to work in all subdirectories (**/.DS_Store)
- Added macOS-specific ignores (.AppleDouble, .LSOverride)
- Added database SQLite ignores (*.sqlite, *.sqlite-journal)
- Added backup/temporary file patterns (*.bak, *.tmp, *.swp)
- Added log file ignore (*.log)
- Added docs temporary exports (*.pptx, *.docx in diagrams/)
- Removed obsolete AGENTS*.* pattern (file already deleted)
- Removed standalone 'env' pattern (covered by .env.backup)
2025-11-21 10:48:19 +01:00
cbazza b34612aeaa chore: remove DaxCompanySeeder (legacy demo data)
The DaxCompanySeeder created fake DAX-40 companies and transactions
for development. We now use real data from the backend via the
TransformDataPoolToProduction job, so this seeder is no longer needed.
2025-11-21 10:32:15 +01:00
cbazza f23d56a0b7 fix: move database schemas from database/ to docs/schemas
CSV schema files are database structure documentation, not production
database files. They belong in docs/ alongside other documentation.
2025-11-21 10:29:25 +01:00
cbazza ac1f72572d fix: move SQL queries from database/ to docs/queries
SQL query files (playground, test queries) are development documentation,
not production database files. They belong in docs/ alongside other documentation.
2025-11-21 10:23:33 +01:00
cbazza 03d6a48bfd fix: move migration diagrams from database/schemas to docs/diagrams
Migration diagrams (Keynote, PDF, Excel, Numbers) are documentation,
not technical database files. They belong in docs/ for better organization.
2025-11-21 10:21:46 +01:00
cbazza 05b9b13e2e fix: Trigger pipeline 2025-11-21 10:05:05 +01:00
cbazza 1cb38d7080 chore: major project cleanup and restructuring
## Dokumentation
- Entfernt AGENTS.md (redundant zu CLAUDE.md)
- Verschoben: 3 Dokumentationen von root → docs/
- Gelöscht: 8 historische Migrations-Pläne aus docs/archive/
- Entfernt: misc/ Ordner komplett (43 Dateien)

## Struktur
- Erstellt: database/queries/ für SQL Test-Queries (4 Dateien)
- Erstellt: database/schemas/ für Schema-Dokumentation (17 Dateien)
  - output_keys_mapping/ (8 CSV-Mappings)
  - migration-diagrams/ (4 Diagramme)

## Code-Qualität
- Formatiert: 7 Style-Issues in 74 Dateien mit Laravel Pint
- Gefixed: Migration für PostgreSQL/SQLite Kompatibilität
- Gefixed: 2 fehlgeschlagene Tests (CSRF + Text-Assertion)

## Tests
- Alle 73 Tests bestehen jetzt (100% Success Rate)
- AuthenticationTest: CSRF-Token Fix für Logout-Test
- CompanySearchTest: Text-Assertion aktualisiert

## Ergebnis
- Root ist sauber (nur CLAUDE.md)
- Dokumentation strukturiert in docs/
- Database-Dateien organisiert in database/
- Code entspricht Style-Guide
- Alle Tests bestehen
2025-11-21 10:00:32 +01:00
cbazza 3a54f6cba3 fix: changes and fixes for the detail transaction view 2025-11-20 14:56:28 +01:00
cbazza 67df5f36c1 fix: placeholder for Stimmrecht added on level 8 to the menue 2025-11-20 12:24:43 +01:00
cbazza ce80a5a022 fix: all 8 categories integrated into the detail view of transactions 2025-11-20 12:16:00 +01:00
cbazza 5e36c85fd9 Merge branch 'main' of ssh://git.trai-infra.comstack.de:9922/bob.molitor/frontend 2025-11-20 11:54:32 +01:00
cbazza a36d2b4cc4 fix: added new command - complete rebuild 2025-11-20 11:54:08 +01:00
Marian 6254583ce8 fix: ci and k8s resource 2025-11-20 10:11:35 +00:00
cbazza 1505975ebb fix: Menue Transaktionen und Stammdaten added to trasaction details 2025-11-20 11:08:23 +01:00
u00lipp 7d085f417c add cronjob 2025-11-20 10:39:02 +01:00
cbazza cb168f3dd8 fix: Trigger pipeline 2025-11-19 12:26:55 +01:00
79 changed files with 10627 additions and 5919 deletions
Vendored
BIN
View File
Binary file not shown.
+8 -8
View File
@@ -27,15 +27,15 @@ LOG_LEVEL=debug
# DB_USERNAME=root
# DB_PASSWORD=
# Secondary PostgreSQL Connection (for backend schema)
# Secondary PostgreSQL Connection (for backend schema) - Local Docker
DB_CONNECTION2=pgsql
DB_HOST2=127.0.0.1
DB_PORT2=5433
DB_DATABASE2=risk_ingest_db
DB_USERNAME2=risk_ingest_user
DB_PASSWORD2=S0prast3r1a
DB_PORT2=5432
DB_DATABASE2=ingest_db
DB_USERNAME2=ingest_user
DB_PASSWORD2=ingest_pwd
SESSION_DRIVER=database
SESSION_DRIVER=file
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
@@ -43,9 +43,9 @@ SESSION_DOMAIN=null
BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local
QUEUE_CONNECTION=database
QUEUE_CONNECTION=sync
CACHE_STORE=database
CACHE_STORE=file
# CACHE_PREFIX=
MEMCACHED_HOST=127.0.0.1
+29
View File
@@ -0,0 +1,29 @@
# Gitea Runner Configuration
# Kopiere diese Datei zu .env.gitea-runner und passe die Werte an
# ============================================
# Gitea Instance
# ============================================
# URL deiner Gitea-Installation
# Wenn Gitea auf der gleichen NAS läuft:
GITEA_URL=http://your-synology-ip:3000
# Wenn Gitea in Docker läuft und im gleichen Netzwerk:
# GITEA_URL=http://gitea:3000
# ============================================
# Runner Configuration
# ============================================
# Runner Registration Token
# Generiere diesen Token in Gitea:
# Repository → Settings → Actions → Runners → Create new Runner
RUNNER_TOKEN=your_gitea_runner_registration_token_here
# Runner Name (wird in Gitea angezeigt)
RUNNER_NAME=synology-runner
# Maximale Anzahl gleichzeitiger Jobs
RUNNER_CAPACITY=1
# Log Level: trace, debug, info, warn, error, fatal
LOG_LEVEL=info
+75
View File
@@ -0,0 +1,75 @@
# Laravel Application Configuration für Synology NAS Deployment
# Kopiere diese Datei zu .env.synology und passe die Werte an
# ============================================
# Application Settings
# ============================================
APP_NAME=Laravel
APP_ENV=production
APP_KEY=base64:L9RVZ3pNFyAvTbMqicT1rL5GbgE+7lJerkU9wyc95H8=
APP_DEBUG=false
APP_URL=http://your-synology-ip:8080
APP_LOCALE=en
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=en_US
APP_MAINTENANCE_DRIVER=file
BCRYPT_ROUNDS=12
# ============================================
# Logging
# ============================================
LOG_CHANNEL=stack
LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=info
# ============================================
# Database - PostgreSQL (Haupt-Datenbank)
# ============================================
DB_CONNECTION2=pgsql
DB_HOST2=postgres
DB_PORT2=5432
DB_DATABASE2=ingest_db
DB_USERNAME2=ingest_user
DB_PASSWORD2=ingest_pwd
# ============================================
# Session & Cache
# ============================================
SESSION_DRIVER=file
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null
# ============================================
# Broadcasting & Queues
# ============================================
BROADCAST_CONNECTION=log
QUEUE_CONNECTION=sync
# ============================================
# Cache
# ============================================
CACHE_STORE=file
FILESYSTEM_DISK=local
# ============================================
# Mail Configuration
# ============================================
MAIL_MAILER=log
MAIL_SCHEME=null
MAIL_HOST=127.0.0.1
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"
# ============================================
# Vite
# ============================================
VITE_APP_NAME="${APP_NAME}"
+233
View File
@@ -0,0 +1,233 @@
name: Build and Deploy to Synology NAS
on:
push:
branches:
- main
- develop
pull_request:
branches:
- main
env:
DOCKER_IMAGE: laravel-app
DOCKER_REGISTRY: ${{ secrets.SYNOLOGY_HOST }}:5000 # Synology Docker Registry Port
jobs:
# Job 1: Run Tests
test:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Validate Conventional Commit (subject line only)
id: validate
run: |
SUBJECT="$(git log -1 --pretty=%s)"
echo "Commit subject: $SUBJECT"
if echo "$SUBJECT" | 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: Setup PHP
if: ${{ steps.validate.outputs.valid == 'true' }}
uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
extensions: mbstring, xml, ctype, json, bcmath, pdo, pdo_sqlite
coverage: none
- name: Install Composer Dependencies
if: ${{ steps.validate.outputs.valid == 'true' }}
run: composer install --prefer-dist --no-progress --no-suggest
- name: Setup Node.js
if: ${{ steps.validate.outputs.valid == 'true' }}
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install NPM Dependencies
if: ${{ steps.validate.outputs.valid == 'true' }}
run: npm ci
- name: Build Frontend Assets
if: ${{ steps.validate.outputs.valid == 'true' }}
run: npm run build
- name: Run Tests
if: ${{ steps.validate.outputs.valid == 'true' }}
run: php artisan test
# Job 2: Build and Push Docker Image
build:
runs-on: ubuntu-latest
needs: test
if: github.event_name == 'push'
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Validate Conventional Commit
id: validate
run: |
SUBJECT="$(git log -1 --pretty=%s)"
echo "Commit subject: $SUBJECT"
if echo "$SUBJECT" | 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 != 'true' }}
run: |
echo "Latest commit is not a valid Conventional Commit. Skipping build."
exit 0
- name: Set up Docker Buildx
if: ${{ steps.validate.outputs.valid == 'true' }}
uses: docker/setup-buildx-action@v3
- name: Determine Docker Tag
id: docker_tag
if: ${{ steps.validate.outputs.valid == 'true' }}
run: |
if [ "${{ github.ref_name }}" == "main" ]; then
echo "tag=latest" >> "$GITHUB_OUTPUT"
echo "env=production" >> "$GITHUB_OUTPUT"
else
echo "tag=${{ github.ref_name }}" >> "$GITHUB_OUTPUT"
echo "env=staging" >> "$GITHUB_OUTPUT"
fi
- name: Build Docker Image
if: ${{ steps.validate.outputs.valid == 'true' }}
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
push: false
load: true
tags: ${{ env.DOCKER_IMAGE }}:${{ steps.docker_tag.outputs.tag }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Save Docker Image
if: ${{ steps.validate.outputs.valid == 'true' }}
run: |
docker save ${{ env.DOCKER_IMAGE }}:${{ steps.docker_tag.outputs.tag }} | gzip > laravel-app.tar.gz
- name: Upload Docker Image Artifact
if: ${{ steps.validate.outputs.valid == 'true' }}
uses: actions/upload-artifact@v4
with:
name: docker-image
path: laravel-app.tar.gz
retention-days: 1
# Job 3: Deploy to Synology NAS
deploy:
runs-on: ubuntu-latest
needs: build
if: github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'develop')
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Download Docker Image Artifact
uses: actions/download-artifact@v4
with:
name: docker-image
- name: Determine Environment
id: env
run: |
if [ "${{ github.ref_name }}" == "main" ]; then
echo "env=production" >> "$GITHUB_OUTPUT"
else
echo "env=staging" >> "$GITHUB_OUTPUT"
fi
- name: Setup SSH
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SYNOLOGY_SSH_KEY }}" > ~/.ssh/synology_key
chmod 600 ~/.ssh/synology_key
ssh-keyscan -H ${{ secrets.SYNOLOGY_HOST }} >> ~/.ssh/known_hosts
- name: Copy Files to Synology
run: |
# Transfer Docker Image
scp -i ~/.ssh/synology_key laravel-app.tar.gz \
${{ secrets.SYNOLOGY_USER }}@${{ secrets.SYNOLOGY_HOST }}:/volume1/docker/laravel-app/
# Transfer Docker Compose file
scp -i ~/.ssh/synology_key docker-compose.synology.yml \
${{ secrets.SYNOLOGY_USER }}@${{ secrets.SYNOLOGY_HOST }}:/volume1/docker/laravel-app/
# Transfer deployment script
scp -i ~/.ssh/synology_key scripts/deploy-synology.sh \
${{ secrets.SYNOLOGY_USER }}@${{ secrets.SYNOLOGY_HOST }}:/volume1/docker/laravel-app/
- name: Deploy on Synology
run: |
ssh -i ~/.ssh/synology_key \
${{ secrets.SYNOLOGY_USER }}@${{ secrets.SYNOLOGY_HOST }} \
"cd /volume1/docker/laravel-app && chmod +x deploy-synology.sh && ./deploy-synology.sh"
- name: Health Check
run: |
sleep 10 # Wait for container to start
# Check if the app is responding
SYNOLOGY_IP="${{ secrets.SYNOLOGY_HOST }}"
MAX_RETRIES=30
RETRY_COUNT=0
while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
if curl -f -s http://$SYNOLOGY_IP:8080/health > /dev/null 2>&1; then
echo "✅ Application is healthy!"
exit 0
fi
echo "⏳ Waiting for application to be ready... ($RETRY_COUNT/$MAX_RETRIES)"
sleep 5
RETRY_COUNT=$((RETRY_COUNT + 1))
done
echo "❌ Health check failed after $MAX_RETRIES attempts"
exit 1
- name: Cleanup
if: always()
run: |
rm -f ~/.ssh/synology_key
ssh -i ~/.ssh/synology_key \
${{ secrets.SYNOLOGY_USER }}@${{ secrets.SYNOLOGY_HOST }} \
"rm -f /volume1/docker/laravel-app/laravel-app.tar.gz" || true
# Job 4: Notify on Success/Failure (optional)
notify:
runs-on: ubuntu-latest
needs: [test, build, deploy]
if: always()
steps:
- name: Deployment Status
run: |
if [ "${{ needs.deploy.result }}" == "success" ]; then
echo "✅ Deployment to Synology NAS successful!"
echo "🌐 Application available at: http://${{ secrets.SYNOLOGY_HOST }}:8080"
else
echo "❌ Deployment failed. Check the logs for details."
exit 1
fi
+13 -3
View File
@@ -3,7 +3,7 @@ name: Build and Push Docker Image only on Conventional Commits
on:
push:
branches:
- main
- '**'
jobs:
build:
@@ -47,13 +47,23 @@ jobs:
if: ${{ steps.validate.outputs.valid == 'true' }}
uses: docker/setup-buildx-action@v3
- name: Determine Docker tag
id: docker_tag
if: ${{ steps.validate.outputs.valid == 'true' }}
run: |
if [ "${{ github.ref_name }}" == "main" ]; then
echo "tag=latest" >> "$GITHUB_OUTPUT"
else
echo "tag=dev" >> "$GITHUB_OUTPUT"
fi
- name: Build and push
if: ${{ steps.validate.outputs.valid == 'true' }}
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ env.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:latest
tags: ${{ env.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:${{ steps.docker_tag.outputs.tag }}
provenance: false # avoids warnings on some registries
- name: Install kubectl
@@ -69,7 +79,7 @@ jobs:
echo "${{ secrets.KUBECONFIG }}" > kubeconfig
- name: Restart deployment and wait
if: ${{ steps.validate.outputs.valid == 'true' }}
if: ${{ steps.validate.outputs.valid == 'true' && github.ref_name == 'main' }}
env:
KUBECONFIG: ${{ github.workspace }}/kubeconfig
run: |
+41 -6
View File
@@ -15,16 +15,51 @@ Homestead.yaml
npm-debug.log
yarn-error.log
/auth.json
# IDEs
/.fleet
/.idea
/.nova
/.zed
.DS_Store
env
/.junie/*
/.cursor/*
/.vscode/*
# macOS
**/.DS_Store
.AppleDouble
.LSOverride
# MCP
*.mcp*
*.mdc
/.mcp.json
AGENTS*.*
/.junie/*
/.cursor/*
/.vscode/*
# Database
/database/*.sqlite
/database/*.sqlite-journal
*.sql.gz
/backups
# CI/CD & Deployment
.env.synology
.env.gitea-runner
kubeconfig
*.tar.gz
laravel-app.tar.gz
# Backup & Temporary Files
*.bak
*.backup
*.tmp
*.swp
*~
.*.swp
.*.swo
# Logs
*.log
# Documentation temporary files (generated exports)
/docs/diagrams/*.pptx
/docs/diagrams/*.docx
-622
View File
@@ -1,622 +0,0 @@
# Repository Guidelines
## Project Structure & Module Organization
The application follows Laravel 12's streamlined tree. Server code lives in `app/`, with Livewire & Volt components under `app/Livewire` and HTTP actions under `app/Http`. Volt views and shared blades live in `resources/views`, UI assets in `resources/js` and `resources/css`. Routes are split across `routes/web.php`, `routes/api.php`, and CLI automation in `routes/console.php`. Database migrations, factories, and seeders live within `database/`. Automated tests are grouped by layer in `tests/Feature` and `tests/Unit`.
## Build, Test, and Development Commands
Use `composer run dev` for the full local stack (PHP server, queue listener, Pail, and Vite). Frontend-only work can rely on `npm run dev`, while `npm run build` compiles production assets. `composer test` clears config cache then runs the Pest suite. Run targeted tests via `php artisan test tests/Feature/FooTest.php`. When onboarding, execute `composer run setup` to install dependencies, provision `.env`, migrate, and build assets.
## Coding Style & Naming Conventions
PHP code targets 8.4+ and follows Laravel Pint defaults; run `vendor/bin/pint --dirty` before committing. Use descriptive camelCase for variables and methods, and PascalCase for classes, Livewire components, and enums. Volt components should keep their Blade/PHP in a single `.blade.php` file and align with existing Flux UI usage. Tailwind v4 powers styling—reference the utility-first classes already present in `resources/css/app.css`.
## Testing Guidelines
Write tests with Pest v4, mirroring the structure under `tests/Feature` for HTTP and Livewire flows and `tests/Unit` for pure logic. Prefer model factories and Volt testing helpers (`Volt::test`) to cover component behavior. Name tests after user-facing behavior and exercise validation errors alongside success paths. Always run the nearest relevant `php artisan test` command before pushing.
## Commit & Pull Request Guidelines
Commits are short, present-tense summaries (for example, `add billing address migration`). Bundle related changes together and include schema updates with their factories and tests. Pull requests should describe the change, list validation steps (commands run, screenshots for UI), and link to any tracked issue. Flag follow-up work clearly so reviewers can plan next steps.
===
<laravel-boost-guidelines>
=== foundation rules ===
# Laravel Boost Guidelines
The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to enhance the user's satisfaction building Laravel applications.
## Foundational Context
This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions.
- php - 8.4.14
- laravel/fortify (FORTIFY) - v1
- laravel/framework (LARAVEL) - v12
- laravel/prompts (PROMPTS) - v0
- livewire/flux (FLUXUI_FREE) - v2
- livewire/livewire (LIVEWIRE) - v3
- livewire/volt (VOLT) - v1
- laravel/mcp (MCP) - v0
- laravel/pint (PINT) - v1
- laravel/sail (SAIL) - v1
- pestphp/pest (PEST) - v4
- phpunit/phpunit (PHPUNIT) - v12
- tailwindcss (TAILWINDCSS) - v4
## Conventions
- You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, naming.
- Use descriptive names for variables and methods. For example, `isRegisteredForDiscounts`, not `discount()`.
- Check for existing components to reuse before writing a new one.
## Verification Scripts
- Do not create verification scripts or tinker when tests cover that functionality and prove it works. Unit and feature tests are more important.
## Application Structure & Architecture
- Stick to existing directory structure - don't create new base folders without approval.
- Do not change the application's dependencies without approval.
## Frontend Bundling
- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `npm run build`, `npm run dev`, or `composer run dev`. Ask them.
## Replies
- Be concise in your explanations - focus on what's important rather than explaining obvious details.
## Documentation Files
- You must only create documentation files if explicitly requested by the user.
=== boost rules ===
## Laravel Boost
- Laravel Boost is an MCP server that comes with powerful tools designed specifically for this application. Use them.
## Artisan
- Use the `list-artisan-commands` tool when you need to call an Artisan command to double check the available parameters.
## URLs
- Whenever you share a project URL with the user you should use the `get-absolute-url` tool to ensure you're using the correct scheme, domain / IP, and port.
## Tinker / Debugging
- You should use the `tinker` tool when you need to execute PHP to debug code or query Eloquent models directly.
- Use the `database-query` tool when you only need to read from the database.
## Reading Browser Logs With the `browser-logs` Tool
- You can read browser logs, errors, and exceptions using the `browser-logs` tool from Boost.
- Only recent browser logs will be useful - ignore old logs.
## Searching Documentation (Critically Important)
- Boost comes with a powerful `search-docs` tool you should use before any other approaches. This tool automatically passes a list of installed packages and their versions to the remote Boost API, so it returns only version-specific documentation specific for the user's circumstance. You should pass an array of packages to filter on if you know you need docs for particular packages.
- The 'search-docs' tool is perfect for all Laravel related packages, including Laravel, Inertia, Livewire, Filament, Tailwind, Pest, Nova, Nightwatch, etc.
- You must use this tool to search for Laravel-ecosystem documentation before falling back to other approaches.
- Search the documentation before making code changes to ensure we are taking the correct approach.
- Use multiple, broad, simple, topic based queries to start. For example: `['rate limiting', 'routing rate limiting', 'routing']`.
- Do not add package names to queries - package information is already shared. For example, use `test resource table`, not `filament 4 test resource table`.
### Available Search Syntax
- You can and should pass multiple queries at once. The most relevant results will be returned first.
1. Simple Word Searches with auto-stemming - query=authentication - finds 'authenticate' and 'auth'
2. Multiple Words (AND Logic) - query=rate limit - finds knowledge containing both "rate" AND "limit"
3. Quoted Phrases (Exact Position) - query="infinite scroll" - Words must be adjacent and in that order
4. Mixed Queries - query=middleware "rate limit" - "middleware" AND exact phrase "rate limit"
5. Multiple Queries - queries=["authentication", "middleware"] - ANY of these terms
=== php rules ===
## PHP
- Always use curly braces for control structures, even if it has one line.
### Constructors
- Use PHP 8 constructor property promotion in `__construct()`.
- <code-snippet>public function __construct(public GitHub $github) { }</code-snippet>
- Do not allow empty `__construct()` methods with zero parameters.
### Type Declarations
- Always use explicit return type declarations for methods and functions.
- Use appropriate PHP type hints for method parameters.
<code-snippet name="Explicit Return Types and Method Params" lang="php">
protected function isAccessible(User $user, ?string $path = null): bool
{
...
}
</code-snippet>
## Comments
- Prefer PHPDoc blocks over comments. Never use comments within the code itself unless there is something _very_ complex going on.
## PHPDoc Blocks
- Add useful array shape type definitions for arrays when appropriate.
## Enums
- Typically, keys in an Enum should be TitleCase. For example: `FavoritePerson`, `BestLake`, `Monthly`.
=== laravel/core rules ===
## Do Things the Laravel Way
- Use `php artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using the `list-artisan-commands` tool.
- If you're creating a generic PHP class, use `artisan make:class`.
- Pass `--no-interaction` to all Artisan commands to ensure they work without user input. You should also pass the correct `--options` to ensure correct behavior.
### Database
- Always use proper Eloquent relationship methods with return type hints. Prefer relationship methods over raw queries or manual joins.
- Use Eloquent models and relationships before suggesting raw database queries
- Avoid `DB::`; prefer `Model::query()`. Generate code that leverages Laravel's ORM capabilities rather than bypassing them.
- Generate code that prevents N+1 query problems by using eager loading.
- Use Laravel's query builder for very complex database operations.
### Model Creation
- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `list-artisan-commands` to check the available options to `php artisan make:model`.
### APIs & Eloquent Resources
- For APIs, default to using Eloquent API Resources and API versioning unless existing API routes do not, then you should follow existing application convention.
### Controllers & Validation
- Always create Form Request classes for validation rather than inline validation in controllers. Include both validation rules and custom error messages.
- Check sibling Form Requests to see if the application uses array or string based validation rules.
### Queues
- Use queued jobs for time-consuming operations with the `ShouldQueue` interface.
### Authentication & Authorization
- Use Laravel's built-in authentication and authorization features (gates, policies, Sanctum, etc.).
### URL Generation
- When generating links to other pages, prefer named routes and the `route()` function.
### Configuration
- Use environment variables only in configuration files - never use the `env()` function directly outside of config files. Always use `config('app.name')`, not `env('APP_NAME')`.
### Testing
- When creating models for tests, use the factories for the models. Check if the factory has custom states that can be used before manually setting up the model.
- Faker: Use methods such as `$this->faker->word()` or `fake()->randomDigit()`. Follow existing conventions whether to use `$this->faker` or `fake()`.
- When creating tests, make use of `php artisan make:test [options] <name>` to create a feature test, and pass `--unit` to create a unit test. Most tests should be feature tests.
### Vite Error
- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `npm run build` or ask the user to run `npm run dev` or `composer run dev`.
=== laravel/v12 rules ===
## Laravel 12
- Use the `search-docs` tool to get version specific documentation.
- Since Laravel 11, Laravel has a new streamlined file structure which this project uses.
### Laravel 12 Structure
- No middleware files in `app/Http/Middleware/`.
- `bootstrap/app.php` is the file to register middleware, exceptions, and routing files.
- `bootstrap/providers.php` contains application specific service providers.
- **No app\Console\Kernel.php** - use `bootstrap/app.php` or `routes/console.php` for console configuration.
- **Commands auto-register** - files in `app/Console/Commands/` are automatically available and do not require manual registration.
### Database
- When modifying a column, the migration must include all of the attributes that were previously defined on the column. Otherwise, they will be dropped and lost.
- Laravel 11 allows limiting eagerly loaded records natively, without external packages: `$query->latest()->limit(10);`.
### Models
- Casts can and likely should be set in a `casts()` method on a model rather than the `$casts` property. Follow existing conventions from other models.
=== fluxui-free/core rules ===
## Flux UI Free
- This project is using the free edition of Flux UI. It has full access to the free components and variants, but does not have access to the Pro components.
- Flux UI is a component library for Livewire. Flux is a robust, hand-crafted, UI component library for your Livewire applications. It's built using Tailwind CSS and provides a set of components that are easy to use and customize.
- You should use Flux UI components when available.
- Fallback to standard Blade components if Flux is unavailable.
- If available, use Laravel Boost's `search-docs` tool to get the exact documentation and code snippets available for this project.
- Flux UI components look like this:
<code-snippet name="Flux UI Component Usage Example" lang="blade">
<flux:button variant="primary"/>
</code-snippet>
### Available Components
This is correct as of Boost installation, but there may be additional components within the codebase.
<available-flux-components>
avatar, badge, brand, breadcrumbs, button, callout, checkbox, dropdown, field, heading, icon, input, modal, navbar, profile, radio, select, separator, switch, text, textarea, tooltip
</available-flux-components>
=== livewire/core rules ===
## Livewire Core
- Use the `search-docs` tool to find exact version specific documentation for how to write Livewire & Livewire tests.
- Use the `php artisan make:livewire [Posts\\CreatePost]` artisan command to create new components
- State should live on the server, with the UI reflecting it.
- All Livewire requests hit the Laravel backend, they're like regular HTTP requests. Always validate form data, and run authorization checks in Livewire actions.
## Livewire Best Practices
- Livewire components require a single root element.
- Use `wire:loading` and `wire:dirty` for delightful loading states.
- Add `wire:key` in loops:
```blade
@foreach ($items as $item)
<div wire:key="item-{{ $item->id }}">
{{ $item->name }}
</div>
@endforeach
```
- Prefer lifecycle hooks like `mount()`, `updatedFoo()` for initialization and reactive side effects:
<code-snippet name="Lifecycle hook examples" lang="php">
public function mount(User $user) { $this->user = $user; }
public function updatedSearch() { $this->resetPage(); }
</code-snippet>
## Testing Livewire
<code-snippet name="Example Livewire component test" lang="php">
Livewire::test(Counter::class)
->assertSet('count', 0)
->call('increment')
->assertSet('count', 1)
->assertSee(1)
->assertStatus(200);
</code-snippet>
<code-snippet name="Testing a Livewire component exists within a page" lang="php">
$this->get('/posts/create')
->assertSeeLivewire(CreatePost::class);
</code-snippet>
=== livewire/v3 rules ===
## Livewire 3
### Key Changes From Livewire 2
- These things changed in Livewire 2, but may not have been updated in this application. Verify this application's setup to ensure you conform with application conventions.
- Use `wire:model.live` for real-time updates, `wire:model` is now deferred by default.
- Components now use the `App\Livewire` namespace (not `App\Http\Livewire`).
- Use `$this->dispatch()` to dispatch events (not `emit` or `dispatchBrowserEvent`).
- Use the `components.layouts.app` view as the typical layout path (not `layouts.app`).
### New Directives
- `wire:show`, `wire:transition`, `wire:cloak`, `wire:offline`, `wire:target` are available for use. Use the documentation to find usage examples.
### Alpine
- Alpine is now included with Livewire, don't manually include Alpine.js.
- Plugins included with Alpine: persist, intersect, collapse, and focus.
### Lifecycle Hooks
- You can listen for `livewire:init` to hook into Livewire initialization, and `fail.status === 419` for the page expiring:
<code-snippet name="livewire:load example" lang="js">
document.addEventListener('livewire:init', function () {
Livewire.hook('request', ({ fail }) => {
if (fail && fail.status === 419) {
alert('Your session expired');
}
});
Livewire.hook('message.failed', (message, component) => {
console.error(message);
});
});
</code-snippet>
=== volt/core rules ===
## Livewire Volt
- This project uses Livewire Volt for interactivity within its pages. New pages requiring interactivity must also use Livewire Volt. There is documentation available for it.
- Make new Volt components using `php artisan make:volt [name] [--test] [--pest]`
- Volt is a **class-based** and **functional** API for Livewire that supports single-file components, allowing a component's PHP logic and Blade templates to co-exist in the same file
- Livewire Volt allows PHP logic and Blade templates in one file. Components use the `@livewire("volt-anonymous-fragment-eyJuYW1lIjoidm9sdC1hbm9ueW1vdXMtZnJhZ21lbnQtYmQ5YWJiNTE3YWMyMTgwOTA1ZmUxMzAxODk0MGJiZmIiLCJwYXRoIjoic3RvcmFnZVwvZnJhbWV3b3JrXC92aWV3c1wvMTUxYWRjZWRjMzBhMzllOWIxNzQ0ZDRiMWRjY2FjYWIuYmxhZGUucGhwIn0=", Livewire\Volt\Precompilers\ExtractFragments::componentArguments([...get_defined_vars(), ...array (
)]))
</code-snippet>
### Volt Class Based Component Example
To get started, define an anonymous class that extends Livewire\Volt\Component. Within the class, you may utilize all of the features of Livewire using traditional Livewire syntax:
<code-snippet name="Volt Class-based Volt Component Example" lang="php">
use Livewire\Volt\Component;
new class extends Component {
public $count = 0;
public function increment()
{
$this->count++;
}
} ?>
<div>
<h1>{{ $count }}</h1>
<button wire:click="increment">+</button>
</div>
</code-snippet>
### Testing Volt & Volt Components
- Use the existing directory for tests if it already exists. Otherwise, fallback to `tests/Feature/Volt`.
<code-snippet name="Livewire Test Example" lang="php">
use Livewire\Volt\Volt;
test('counter increments', function () {
Volt::test('counter')
->assertSee('Count: 0')
->call('increment')
->assertSee('Count: 1');
});
</code-snippet>
<code-snippet name="Volt Component Test Using Pest" lang="php">
declare(strict_types=1);
use App\Models\{User, Product};
use Livewire\Volt\Volt;
test('product form creates product', function () {
$user = User::factory()->create();
Volt::test('pages.products.create')
->actingAs($user)
->set('form.name', 'Test Product')
->set('form.description', 'Test Description')
->set('form.price', 99.99)
->call('create')
->assertHasNoErrors();
expect(Product::where('name', 'Test Product')->exists())->toBeTrue();
});
</code-snippet>
### Common Patterns
<code-snippet name="CRUD With Volt" lang="php">
<?php
use App\Models\Product;
use function Livewire\Volt\{state, computed};
state(['editing' => null, 'search' => '']);
$products = computed(fn() => Product::when($this->search,
fn($q) => $q->where('name', 'like', "%{$this->search}%")
)->get());
$edit = fn(Product $product) => $this->editing = $product->id;
$delete = fn(Product $product) => $product->delete();
?>
<!-- HTML / UI Here -->
</code-snippet>
<code-snippet name="Real-Time Search With Volt" lang="php">
<flux:input
wire:model.live.debounce.300ms="search"
placeholder="Search..."
/>
</code-snippet>
<code-snippet name="Loading States With Volt" lang="php">
<flux:button wire:click="save" wire:loading.attr="disabled">
<span wire:loading.remove>Save</span>
<span wire:loading>Saving...</span>
</flux:button>
</code-snippet>
=== pint/core rules ===
## Laravel Pint Code Formatter
- You must run `vendor/bin/pint --dirty` before finalizing changes to ensure your code matches the project's expected style.
- Do not run `vendor/bin/pint --test`, simply run `vendor/bin/pint` to fix any formatting issues.
=== pest/core rules ===
## Pest
### Testing
- If you need to verify a feature is working, write or update a Unit / Feature test.
### Pest Tests
- All tests must be written using Pest. Use `php artisan make:test --pest <name>`.
- You must not remove any tests or test files from the tests directory without approval. These are not temporary or helper files - these are core to the application.
- Tests should test all of the happy paths, failure paths, and weird paths.
- Tests live in the `tests/Feature` and `tests/Unit` directories.
- Pest tests look and behave like this:
<code-snippet name="Basic Pest Test Example" lang="php">
it('is true', function () {
expect(true)->toBeTrue();
});
</code-snippet>
### Running Tests
- Run the minimal number of tests using an appropriate filter before finalizing code edits.
- To run all tests: `php artisan test`.
- To run all tests in a file: `php artisan test tests/Feature/ExampleTest.php`.
- To filter on a particular test name: `php artisan test --filter=testName` (recommended after making a change to a related file).
- When the tests relating to your changes are passing, ask the user if they would like to run the entire test suite to ensure everything is still passing.
### Pest Assertions
- When asserting status codes on a response, use the specific method like `assertForbidden` and `assertNotFound` instead of using `assertStatus(403)` or similar, e.g.:
<code-snippet name="Pest Example Asserting postJson Response" lang="php">
it('returns all', function () {
$response = $this->postJson('/api/docs', []);
$response->assertSuccessful();
});
</code-snippet>
### Mocking
- Mocking can be very helpful when appropriate.
- When mocking, you can use the `Pest\Laravel\mock` Pest function, but always import it via `use function Pest\Laravel\mock;` before using it. Alternatively, you can use `$this->mock()` if existing tests do.
- You can also create partial mocks using the same import or self method.
### Datasets
- Use datasets in Pest to simplify tests which have a lot of duplicated data. This is often the case when testing validation rules, so consider going with this solution when writing tests for validation rules.
<code-snippet name="Pest Dataset Example" lang="php">
it('has emails', function (string $email) {
expect($email)->not->toBeEmpty();
})->with([
'james' => 'james@laravel.com',
'taylor' => 'taylor@laravel.com',
]);
</code-snippet>
=== pest/v4 rules ===
## Pest 4
- Pest v4 is a huge upgrade to Pest and offers: browser testing, smoke testing, visual regression testing, test sharding, and faster type coverage.
- Browser testing is incredibly powerful and useful for this project.
- Browser tests should live in `tests/Browser/`.
- Use the `search-docs` tool for detailed guidance on utilizing these features.
### Browser Testing
- You can use Laravel features like `Event::fake()`, `assertAuthenticated()`, and model factories within Pest v4 browser tests, as well as `RefreshDatabase` (when needed) to ensure a clean state for each test.
- Interact with the page (click, type, scroll, select, submit, drag-and-drop, touch gestures, etc.) when appropriate to complete the test.
- If requested, test on multiple browsers (Chrome, Firefox, Safari).
- If requested, test on different devices and viewports (like iPhone 14 Pro, tablets, or custom breakpoints).
- Switch color schemes (light/dark mode) when appropriate.
- Take screenshots or pause tests for debugging when appropriate.
### Example Tests
<code-snippet name="Pest Browser Test Example" lang="php">
it('may reset the password', function () {
Notification::fake();
$this->actingAs(User::factory()->create());
$page = visit('/sign-in'); // Visit on a real browser...
$page->assertSee('Sign In')
->assertNoJavascriptErrors() // or ->assertNoConsoleLogs()
->click('Forgot Password?')
->fill('email', 'nuno@laravel.com')
->click('Send Reset Link')
->assertSee('We have emailed your password reset link!')
Notification::assertSent(ResetPassword::class);
});
</code-snippet>
<code-snippet name="Pest Smoke Testing Example" lang="php">
$pages = visit(['/', '/about', '/contact']);
$pages->assertNoJavascriptErrors()->assertNoConsoleLogs();
</code-snippet>
=== tailwindcss/core rules ===
## Tailwind Core
- Use Tailwind CSS classes to style HTML, check and use existing tailwind conventions within the project before writing your own.
- Offer to extract repeated patterns into components that match the project's conventions (i.e. Blade, JSX, Vue, etc..)
- Think through class placement, order, priority, and defaults - remove redundant classes, add classes to parent or child carefully to limit repetition, group elements logically
- You can use the `search-docs` tool to get exact examples from the official documentation when needed.
### Spacing
- When listing items, use gap utilities for spacing, don't use margins.
<code-snippet name="Valid Flex Gap Spacing Example" lang="html">
<div class="flex gap-8">
<div>Superior</div>
<div>Michigan</div>
<div>Erie</div>
</div>
</code-snippet>
### Dark Mode
- If existing pages and components support dark mode, new pages and components must support dark mode in a similar way, typically using `dark:`.
=== tailwindcss/v4 rules ===
## Tailwind 4
- Always use Tailwind CSS v4 - do not use the deprecated utilities.
- `corePlugins` is not supported in Tailwind v4.
- In Tailwind v4, you import Tailwind using a regular CSS `@import` statement, not using the `@tailwind` directives used in v3:
<code-snippet name="Tailwind v4 Import Tailwind Diff" lang="diff">
- @tailwind base;
- @tailwind components;
- @tailwind utilities;
+ @import "tailwindcss";
</code-snippet>
### Replaced Utilities
- Tailwind v4 removed deprecated utilities. Do not use the deprecated option - use the replacement.
- Opacity values are still numeric.
| Deprecated | Replacement |
|------------+--------------|
| bg-opacity-* | bg-black/* |
| text-opacity-* | text-black/* |
| border-opacity-* | border-black/* |
| divide-opacity-* | divide-black/* |
| ring-opacity-* | ring-black/* |
| placeholder-opacity-* | placeholder-black/* |
| flex-shrink-* | shrink-* |
| flex-grow-* | grow-* |
| overflow-ellipsis | text-ellipsis |
| decoration-slice | box-decoration-slice |
| decoration-clone | box-decoration-clone |
=== tests rules ===
## Test Enforcement
- Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass.
- Run the minimum number of tests needed to ensure code quality and speed. Use `php artisan test` with a specific filename or filter.
=== laravel/fortify rules ===
## Laravel Fortify
Fortify is a headless authentication backend that provides authentication routes and controllers for Laravel applications.
**Before implementing any authentication features, use the `search-docs` tool to get the latest docs for that specific feature.**
### Configuration & Setup
- Check `config/fortify.php` to see what's enabled. Use `search-docs` for detailed information on specific features.
- Enable features by adding them to the `'features' => []` array: `Features::registration()`, `Features::resetPasswords()`, etc.
- To see the all Fortify registered routes, use the `list-routes` tool with the `only_vendor: true` and `action: "Fortify"` parameters.
- Fortify includes view routes by default (login, register). Set `'views' => false` in the configuration file to disable them if you're handling views yourself.
### Customization
- Views can be customized in `FortifyServiceProvider`'s `boot()` method using `Fortify::loginView()`, `Fortify::registerView()`, etc.
- Customize authentication logic with `Fortify::authenticateUsing()` for custom user retrieval / validation.
- Actions in `app/Actions/Fortify/` handle business logic (user creation, password reset, etc.). They're fully customizable, so you can modify them to change feature behavior.
## Available Features
- `Features::registration()` for user registration.
- `Features::emailVerification()` to verify new user emails.
- `Features::twoFactorAuthentication()` for 2FA with QR codes and recovery codes.
- Add options: `['confirmPassword' => true, 'confirm' => true]` to require password confirmation and OTP confirmation before enabling 2FA.
- `Features::updateProfileInformation()` to let users update their profile.
- `Features::updatePasswords()` to let users change their passwords.
- `Features::resetPasswords()` for password reset via email.
</laravel-boost-guidelines>
+281
View File
@@ -0,0 +1,281 @@
# CI/CD Automatisierung für Synology NAS
Dieses Projekt ist vollständig für automatisches Deployment auf deiner Synology NAS mit Gitea Actions vorbereitet.
## 🚀 Schnellstart
### 1. Gitea Runner einrichten (einmalig)
```bash
# Auf der Synology NAS
cd /volume1/docker
mkdir gitea-runner
cd gitea-runner
# Environment Datei erstellen
cat > .env << 'EOF'
GITEA_URL=http://your-synology-ip:3000
RUNNER_TOKEN=your_token_from_gitea
RUNNER_NAME=synology-runner
EOF
# Runner starten
docker-compose -f /path/to/docker-compose.gitea-runner.yml up -d
```
**Runner Token generieren:**
1. Gitea öffnen
2. Repository → Settings → Actions → Runners
3. "Create new Runner" klicken
4. Token kopieren und in `.env` einfügen
📖 Detaillierte Anleitung: [GITEA_RUNNER_SETUP.md](GITEA_RUNNER_SETUP.md)
### 2. Repository Secrets konfigurieren (einmalig)
In Gitea: **Repository → Settings → Secrets**
| Secret Name | Wert | Beschreibung |
|------------|------|--------------|
| `SYNOLOGY_HOST` | `192.168.1.100` | IP deiner Synology |
| `SYNOLOGY_USER` | `admin` | SSH-Benutzername |
| `SYNOLOGY_SSH_KEY` | `-----BEGIN...` | Private SSH-Key |
| `APP_KEY` | `base64:...` | Laravel APP_KEY |
**SSH-Key erstellen:**
```bash
# Auf der Synology NAS
ssh-keygen -t ed25519 -C "gitea-deploy" -f ~/.ssh/gitea_deploy
cat ~/.ssh/gitea_deploy.pub >> ~/.ssh/authorized_keys
cat ~/.ssh/gitea_deploy # Diesen Key als Secret verwenden
```
### 3. Code pushen und automatisch deployen! 🎉
```bash
git add .
git commit -m "feat: meine neue Funktion"
git push origin main
```
Das wars! Der Rest läuft automatisch:
- ✅ Tests werden ausgeführt
- ✅ Docker Image wird gebaut
- ✅ Deployment auf NAS
- ✅ Health Check
- ✅ Benachrichtigung bei Erfolg/Fehler
## 📁 Wichtige Dateien
| Datei | Beschreibung |
|-------|--------------|
| [`.gitea/workflows/deploy-synology.yml`](.gitea/workflows/deploy-synology.yml) | Gitea Actions Workflow |
| [`docker-compose.synology.yml`](docker-compose.synology.yml) | Docker Compose für Deployment |
| [`scripts/deploy-synology.sh`](scripts/deploy-synology.sh) | Deployment-Script |
| [`.env.synology.example`](.env.synology.example) | Environment-Beispiel |
| [`SYNOLOGY_DEPLOYMENT.md`](SYNOLOGY_DEPLOYMENT.md) | Vollständige Deployment-Doku |
| [`GITEA_RUNNER_SETUP.md`](GITEA_RUNNER_SETUP.md) | Gitea Runner Setup-Anleitung |
## 🔄 Workflow-Prozess
```
1. Developer pushed Code zu Gitea
2. Gitea Actions Workflow startet
3. Tests laufen (Pest)
4. Docker Image wird gebaut
5. Image wird zur Synology NAS übertragen
6. Deployment-Script läuft auf NAS
7. Container werden aktualisiert
8. Migrationen & Cache-Optimierung
9. Health Check
10. ✅ Deployment erfolgreich!
```
## 🎯 Features
### Automatisierte Tests
- PHP-Tests mit Pest
- Frontend-Build-Validierung
- Nur erfolgreiche Builds werden deployed
### Intelligentes Deployment
- Automatisches Backup vor Deployment
- Rollback bei Fehlern
- Health Checks nach Deployment
- Zero-Downtime durch Docker
### Branch-spezifisches Deployment
- `main` → Production (Port 8080)
- `develop` → Staging (Port 8081)
- Feature-Branches → Nur Tests
### Conventional Commits
Der Workflow akzeptiert nur Conventional Commits:
- `feat:` - Neue Features
- `fix:` - Bugfixes
- `docs:` - Dokumentation
- `chore:` - Wartung
- `refactor:` - Code-Refactoring
- `test:` - Tests
- `ci:` - CI/CD Änderungen
**Beispiel:**
```bash
git commit -m "feat: add user authentication"
git commit -m "fix: resolve database connection issue"
```
## 🔍 Monitoring
### Workflow Status prüfen
**In Gitea:**
1. Repository öffnen
2. Tab "Actions" anklicken
3. Alle Runs mit Status sehen
**Logs ansehen:**
- Klicke auf einen Run
- Klicke auf einen Job (test/build/deploy)
- Sieh detaillierte Logs
### Container Logs
```bash
# Auf der Synology NAS via SSH
docker logs -f laravel_app
docker logs -f laravel_postgres
```
### Health Endpoint
```bash
curl http://your-synology-ip:8080/health
```
Response:
```json
{
"status": "healthy",
"timestamp": "2025-12-17T10:30:00Z",
"app": "Laravel",
"env": "production"
}
```
## 🛠 Manuelle Befehle
### Manuelles Deployment
```bash
# Auf der Synology NAS
cd /volume1/docker/laravel-app
./scripts/deploy-synology.sh
```
### Container verwalten
```bash
# Status prüfen
docker-compose -f docker-compose.synology.yml ps
# Logs ansehen
docker-compose -f docker-compose.synology.yml logs -f
# Neustart
docker-compose -f docker-compose.synology.yml restart
# Stoppen
docker-compose -f docker-compose.synology.yml down
```
### Laravel Artisan
```bash
# Migrationen
docker exec laravel_app php artisan migrate
# Cache leeren
docker exec laravel_app php artisan cache:clear
# Shell öffnen
docker exec -it laravel_app sh
```
## 🐛 Troubleshooting
### Workflow startet nicht
**Prüfen:**
1. Runner Status in Gitea → Settings → Actions → Runners
2. Commit-Message ist Conventional Commit Format
3. Branch ist `main` oder `develop`
**Lösung:**
```bash
# Runner neu starten
docker restart gitea-runner
# Runner Logs prüfen
docker logs gitea-runner
```
### Deployment schlägt fehl
**Prüfen:**
1. SSH-Verbindung zur NAS funktioniert
2. Alle Secrets sind korrekt gesetzt
3. Deployment-Script ist executable
**Lösung:**
```bash
# SSH-Key testen
ssh -i ~/.ssh/gitea_deploy admin@your-synology-ip
# Permissions prüfen
chmod +x scripts/deploy-synology.sh
```
### Health Check fehlschlägt
**Prüfen:**
1. Container läuft: `docker ps | grep laravel_app`
2. Logs: `docker logs laravel_app`
3. Netzwerk: `curl http://localhost:8080/health`
**Lösung:**
```bash
# Container neu starten
docker-compose -f docker-compose.synology.yml restart
# .env prüfen
docker exec laravel_app cat .env.synology
```
## 📚 Weitere Dokumentation
- **Deployment Details:** [SYNOLOGY_DEPLOYMENT.md](SYNOLOGY_DEPLOYMENT.md)
- **Gitea Runner Setup:** [GITEA_RUNNER_SETUP.md](GITEA_RUNNER_SETUP.md)
- **Workflow Konfiguration:** [.gitea/workflows/deploy-synology.yml](.gitea/workflows/deploy-synology.yml)
- **Deployment Script:** [scripts/deploy-synology.sh](scripts/deploy-synology.sh)
## 🎉 Das wars!
Nach der einmaligen Einrichtung läuft alles automatisch:
1. Code schreiben
2. Commit & Push
3. Warten bis Deployment fertig ist
4. Anwendung ist live! 🚀
**Happy Deploying!** 🎊
+210
View File
@@ -0,0 +1,210 @@
# Deployment Files Übersicht
Diese Datei gibt einen Überblick über alle Dateien, die für das automatisierte Deployment auf deiner Synology NAS erstellt wurden.
## 📋 Erstelle Dateien
### 1. CI/CD Workflow
- **`.gitea/workflows/deploy-synology.yml`**
- Gitea Actions Workflow für automatisches Build & Deployment
- Führt Tests aus, baut Docker Image, deployt auf NAS
- Triggert bei Push auf `main` oder `develop`
### 2. Docker Konfiguration
- **`docker-compose.synology.yml`**
- Docker Compose für Synology NAS Deployment
- Definiert Laravel App + PostgreSQL Datenbank
- Mit persistenten Volumes und Health Checks
- **`docker-compose.gitea-runner.yml`**
- Docker Compose für Gitea Runner
- Ermöglicht automatische Workflows
- **`Dockerfile`**
- Bereits vorhanden, wird verwendet für Image-Build
- Multi-stage Build für optimale Image-Größe
### 3. Environment Konfiguration
- **`.env.synology.example`**
- Beispiel-Environment für Synology Deployment
- Muss kopiert werden zu `.env.synology`
- Enthält alle notwendigen Variablen
- **`.env.gitea-runner.example`**
- Beispiel-Environment für Gitea Runner
- Muss kopiert werden zu `.env.gitea-runner`
### 4. Deployment Scripts
- **`scripts/deploy-synology.sh`**
- Automatisches Deployment-Script
- Läuft auf der Synology NAS
- Handhabt Backup, Deployment, Rollback, Health Checks
### 5. Dokumentation
- **`SYNOLOGY_DEPLOYMENT.md`**
- Vollständige Anleitung für manuelles Deployment
- Inklusive CI/CD Setup Abschnitt
- Troubleshooting Tipps
- **`GITEA_RUNNER_SETUP.md`**
- Detaillierte Anleitung für Gitea Runner Setup
- Secrets-Konfiguration
- Monitoring & Troubleshooting
- **`CI_CD_README.md`**
- Schnellstart-Anleitung für CI/CD
- Übersicht über alle Features
- Wichtige Befehle
- **`DEPLOYMENT_FILES_OVERVIEW.md`** (diese Datei)
- Übersicht über alle erstellten Dateien
### 6. Application Code
- **`routes/web.php`**
- Health Check Endpoint hinzugefügt (`/health`)
- Wird für automatische Health Checks verwendet
### 7. Git Konfiguration
- **`.gitignore`**
- Erweitert um CI/CD spezifische Dateien
- `.env.synology` und `.env.gitea-runner` ignoriert
- Build-Artefakte ignoriert
## 🔧 Verwendung der Dateien
### Einmalige Einrichtung
1. **Gitea Runner deployen:**
```bash
# Auf Synology NAS
cp .env.gitea-runner.example .env.gitea-runner
# Bearbeite .env.gitea-runner mit deinen Werten
docker-compose -f docker-compose.gitea-runner.yml up -d
```
2. **Repository Secrets in Gitea setzen:**
- `SYNOLOGY_HOST`
- `SYNOLOGY_USER`
- `SYNOLOGY_SSH_KEY`
- `APP_KEY`
3. **Workflow-Dateien committen:**
```bash
git add .gitea/
git commit -m "ci: add automated deployment"
git push origin main
```
### Manuelles Deployment
Falls du manuell deployen möchtest (ohne CI/CD):
```bash
# Auf Synology NAS
cd /volume1/docker/laravel-app
cp .env.synology.example .env.synology
# Bearbeite .env.synology
docker-compose -f docker-compose.synology.yml up -d --build
```
## 📊 Datei-Abhängigkeiten
```
.gitea/workflows/deploy-synology.yml
├── docker-compose.synology.yml
├── scripts/deploy-synology.sh
├── .env.synology (zu erstellen)
└── Dockerfile
docker-compose.gitea-runner.yml
└── .env.gitea-runner (zu erstellen)
```
## ✅ Checkliste für Deployment
### Vor dem ersten Deployment:
- [ ] Gitea Runner deployed (`docker-compose.gitea-runner.yml`)
- [ ] Runner in Gitea registriert
- [ ] Repository Secrets gesetzt
- [ ] `.env.synology` auf NAS erstellt und konfiguriert
- [ ] SSH-Keys für Deployment eingerichtet
- [ ] Projekt-Verzeichnis auf NAS: `/volume1/docker/laravel-app`
### Vor jedem Push:
- [ ] Tests lokal ausgeführt (`php artisan test`)
- [ ] Conventional Commit Message verwendet
- [ ] Code reviewed
### Nach dem Deployment:
- [ ] Workflow Status in Gitea Actions überprüft
- [ ] Health Check erfolgreich: `curl http://nas-ip:8080/health`
- [ ] Anwendung erreichbar: `http://nas-ip:8080`
- [ ] Logs prüfen: `docker logs laravel_app`
## 🔐 Sicherheit
### Dateien die NICHT committet werden sollten:
- `.env.synology` - Enthält Produktions-Secrets
- `.env.gitea-runner` - Enthält Runner-Token
- `kubeconfig` - Falls verwendet
- `*.tar.gz` - Build-Artefakte
- SSH Private Keys
Diese Dateien sind bereits in `.gitignore` eingetragen!
### Dateien die committet werden sollten:
- `.env.synology.example` - Template für andere Entwickler
- `.env.gitea-runner.example` - Template für Runner
- Alle `docker-compose*.yml` Dateien
- Alle Dokumentations-Dateien
- Workflow-Dateien in `.gitea/workflows/`
- Deployment-Scripts in `scripts/`
## 🚀 Nächste Schritte
1. **Lies die Dokumentation:**
- Start mit [CI_CD_README.md](CI_CD_README.md) für Schnellstart
- Dann [GITEA_RUNNER_SETUP.md](GITEA_RUNNER_SETUP.md) für Runner-Setup
- Bei Problemen: [SYNOLOGY_DEPLOYMENT.md](SYNOLOGY_DEPLOYMENT.md)
2. **Setup durchführen:**
- Folge der Anleitung in [GITEA_RUNNER_SETUP.md](GITEA_RUNNER_SETUP.md)
- Konfiguriere Secrets in Gitea
- Teste mit einem Push
3. **Deployment testen:**
- Mache eine kleine Änderung
- Committe mit Conventional Commit
- Push und beobachte Workflow in Gitea Actions
4. **Optional: Erweitere Workflow:**
- Füge Notifications hinzu (Email, Telegram, Discord)
- Konfiguriere mehrere Environments (Staging, Production)
- Füge weitere Tests hinzu
## 📝 Hinweise
- Alle Scripts sind für **Synology DSM 7.x** optimiert
- Docker und Docker Compose müssen auf der NAS installiert sein
- Gitea Actions muss in Gitea aktiviert sein
- PHP 8.4 und Laravel 12 werden verwendet
## 🆘 Support
Bei Fragen oder Problemen:
1. Prüfe die Troubleshooting-Sektionen in den Dokumentations-Dateien
2. Prüfe Logs: `docker logs gitea-runner` und `docker logs laravel_app`
3. Verifiziere alle Secrets und Environment-Variablen
4. Stelle sicher, dass alle Ports verfügbar sind
---
**Erstellt am:** 2025-12-17
**Version:** 1.0
**Für:** Synology NAS Deployment mit Gitea Actions
+411
View File
@@ -0,0 +1,411 @@
# Gitea Runner Setup auf Synology NAS
Diese Anleitung erklärt, wie du einen Gitea Actions Runner auf deiner Synology NAS einrichtest, um automatische Deployments durchzuführen.
## Voraussetzungen
- Gitea läuft auf deiner Synology NAS
- Docker ist auf der Synology NAS installiert
- SSH-Zugriff auf die Synology NAS
- Portainer läuft (optional, aber empfohlen für Container-Management)
## Option 1: Gitea Runner mit Docker Compose (empfohlen)
### 1. Runner Docker Compose Datei erstellen
Erstelle eine Datei `/volume1/docker/gitea-runner/docker-compose.yml`:
```yaml
version: "3.9"
services:
runner:
image: gitea/act_runner:latest
container_name: gitea-runner
restart: unless-stopped
environment:
- GITEA_INSTANCE_URL=http://your-gitea-url:3000
- GITEA_RUNNER_REGISTRATION_TOKEN=${RUNNER_TOKEN}
- GITEA_RUNNER_NAME=synology-runner
- GITEA_RUNNER_LABELS=ubuntu-latest:docker://node:20-bullseye
volumes:
- ./data:/data
- /var/run/docker.sock:/var/run/docker.sock
networks:
- gitea-network
networks:
gitea-network:
external: true
```
### 2. Runner Token generieren
1. Öffne deine Gitea-Instanz im Browser
2. Navigiere zu deinem Repository
3. Gehe zu **Settings****Actions** → **Runners**
4. Klicke auf **Create new Runner**
5. Kopiere den generierten Token
### 3. Environment Variables setzen
Erstelle eine `.env` Datei im selben Verzeichnis:
```env
RUNNER_TOKEN=dein_gitea_runner_token_hier
```
### 4. Runner starten
```bash
cd /volume1/docker/gitea-runner
docker-compose up -d
```
### 5. Runner verifizieren
```bash
# Logs anschauen
docker logs -f gitea-runner
# Status prüfen
docker ps | grep gitea-runner
```
In Gitea solltest du nun unter **Settings****Actions****Runners** deinen registrierten Runner sehen.
## Option 2: Runner direkt in Portainer deployen
### 1. In Portainer einloggen
Öffne Portainer: `http://your-synology-ip:9000`
### 2. Stack erstellen
1. Gehe zu **Stacks****+ Add stack**
2. Name: `gitea-runner`
3. Wähle **Web editor**
4. Füge die obige docker-compose.yml ein
### 3. Environment Variables
Füge folgende Environment Variable hinzu:
- **RUNNER_TOKEN**: Dein Gitea Runner Token
### 4. Deploy
Klicke auf **Deploy the stack**
## Runner Konfiguration
### Custom Runner Labels
Um spezifische Workflows auszuführen, kannst du Custom Labels hinzufügen:
```yaml
environment:
- GITEA_RUNNER_LABELS=ubuntu-latest:docker://node:20-bullseye,ubuntu-22.04:docker://ubuntu:22.04,php-8.4:docker://php:8.4-cli
```
### Mehrere Runner
Für parallele Builds kannst du mehrere Runner starten:
```yaml
services:
runner-1:
image: gitea/act_runner:latest
container_name: gitea-runner-1
# ... config ...
runner-2:
image: gitea/act_runner:latest
container_name: gitea-runner-2
# ... config ...
```
## Repository Secrets konfigurieren
Um das automatische Deployment zu ermöglichen, musst du Secrets in deinem Gitea-Repository hinterlegen:
### 1. In Gitea Repository Settings
1. Gehe zu deinem Repository
2. Klicke auf **Settings** → **Secrets**
3. Füge folgende Secrets hinzu:
| Secret Name | Beschreibung | Beispiel |
|------------|--------------|----------|
| `SYNOLOGY_HOST` | IP oder Hostname deiner Synology | `192.168.1.100` |
| `SYNOLOGY_USER` | SSH-Benutzername | `admin` |
| `SYNOLOGY_SSH_KEY` | Privater SSH-Key für Zugriff | `-----BEGIN OPENSSH PRIVATE KEY-----...` |
| `APP_KEY` | Laravel Application Key | `base64:xyz...` |
### 2. SSH-Key generieren (falls nicht vorhanden)
Auf deiner Synology NAS:
```bash
# SSH-Key-Pair generieren
ssh-keygen -t ed25519 -C "gitea-runner@synology" -f ~/.ssh/gitea_deploy
# Public Key zum authorized_keys hinzufügen
cat ~/.ssh/gitea_deploy.pub >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
# Private Key anzeigen (für Secret)
cat ~/.ssh/gitea_deploy
```
Kopiere den **gesamten** Private Key (inklusive `-----BEGIN` und `-----END` Zeilen) und füge ihn als `SYNOLOGY_SSH_KEY` Secret hinzu.
### 3. Secrets verifizieren
Stelle sicher, dass alle benötigten Secrets gesetzt sind:
```bash
# In Gitea Repository Settings → Secrets
✅ SYNOLOGY_HOST
✅ SYNOLOGY_USER
✅ SYNOLOGY_SSH_KEY
✅ APP_KEY
```
## Workflow aktivieren
### 1. Workflow-Datei pushen
```bash
# Lokal im Projekt
git add .gitea/workflows/deploy-synology.yml
git commit -m "ci: add automated deployment workflow"
git push origin main
```
### 2. Workflow ausführen
Gitea Actions wird automatisch bei jedem Push auf `main` oder `develop` ausgelöst.
### 3. Workflow Status prüfen
1. Gehe zu deinem Repository in Gitea
2. Klicke auf **Actions**
3. Du siehst alle Workflow-Runs mit Status (Success/Failure)
4. Klicke auf einen Run, um Details und Logs zu sehen
## Workflow manuell auslösen
Falls du den Workflow manuell starten möchtest:
### 1. Workflow-Datei erweitern
Füge in `.gitea/workflows/deploy-synology.yml` hinzu:
```yaml
on:
push:
branches:
- main
- develop
workflow_dispatch: # Ermöglicht manuelles Auslösen
```
### 2. Manuell auslösen
1. Gehe zu **Actions** in deinem Repository
2. Wähle den Workflow aus
3. Klicke auf **Run workflow**
## Monitoring & Logs
### Runner Logs
```bash
# Live Logs
docker logs -f gitea-runner
# Letzte 100 Zeilen
docker logs --tail 100 gitea-runner
```
### Workflow Logs
In Gitea UI:
1. **Actions** Tab
2. Wähle einen Workflow Run
3. Klicke auf die einzelnen Jobs, um Logs zu sehen
### Deployment Logs auf Synology
```bash
# Laravel App Logs
docker logs -f laravel_app
# Deployment Script Logs
# Diese werden in der Workflow-Ausgabe angezeigt
```
## Troubleshooting
### Problem: Runner verbindet sich nicht
```bash
# Logs prüfen
docker logs gitea-runner
# Runner neu registrieren
docker-compose down
docker-compose up -d
```
### Problem: Workflow startet nicht
1. **Runner Status prüfen**: Gehe zu Repository → Settings → Actions → Runners
2. **Runner Labels prüfen**: Stelle sicher, dass `runs-on: ubuntu-latest` mit den Runner Labels übereinstimmt
3. **Secrets prüfen**: Alle benötigten Secrets müssen gesetzt sein
### Problem: SSH-Verbindung fehlschlägt
```bash
# SSH-Key Permissions prüfen
chmod 600 ~/.ssh/gitea_deploy
chmod 644 ~/.ssh/gitea_deploy.pub
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
# SSH-Verbindung testen (lokal auf der NAS)
ssh -i ~/.ssh/gitea_deploy admin@localhost
```
### Problem: Deployment schlägt fehl
```bash
# Prüfe deployment script permissions
cd /volume1/docker/laravel-app
ls -la deploy-synology.sh
# Mache es executable
chmod +x deploy-synology.sh
# Teste deployment script manuell
./deploy-synology.sh
```
### Problem: Health Check fehlschlägt
```bash
# Prüfe ob Container läuft
docker ps | grep laravel_app
# Prüfe Logs
docker logs laravel_app
# Teste Health Endpoint manuell
curl http://localhost:8080/health
```
## Best Practices
### 1. Separate Runner für Production und Staging
```yaml
services:
runner-production:
environment:
- GITEA_RUNNER_LABELS=production:docker://ubuntu:22.04
runner-staging:
environment:
- GITEA_RUNNER_LABELS=staging:docker://ubuntu:22.04
```
Im Workflow:
```yaml
jobs:
deploy-production:
runs-on: production
if: github.ref == 'refs/heads/main'
```
### 2. Workflow nur für Conventional Commits
Der aktuelle Workflow filtert bereits nach Conventional Commits:
- `feat:` - Neue Features
- `fix:` - Bugfixes
- `docs:` - Dokumentation
- `chore:` - Wartungsarbeiten
- etc.
### 3. Notifications
Erweitere den Workflow um Benachrichtigungen:
```yaml
- name: Send Notification
if: always()
run: |
# Telegram, Discord, Email, etc.
curl -X POST https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage \
-d chat_id=${{ secrets.TELEGRAM_CHAT_ID }} \
-d text="Deployment status: ${{ job.status }}"
```
### 4. Rollback-Strategie
Im deployment script ist bereits ein Backup-Mechanismus implementiert:
- Datenbank-Backups vor jedem Deployment
- Letzte 5 Backups werden aufbewahrt
- Automatischer Rollback bei Fehlern
## Erweiterte Konfiguration
### Multi-Stage Deployments
Für staging und production environments:
```yaml
jobs:
deploy-staging:
if: github.ref == 'refs/heads/develop'
# ... deploy to staging ...
deploy-production:
if: github.ref == 'refs/heads/main'
needs: [test, deploy-staging]
# ... deploy to production ...
```
### Caching
Um Build-Zeiten zu verkürzen:
```yaml
- name: Cache Composer
uses: actions/cache@v3
with:
path: vendor
key: ${{ runner.os }}-composer-${{ hashFiles('composer.lock') }}
- name: Cache NPM
uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
```
## Weitere Ressourcen
- [Gitea Actions Dokumentation](https://docs.gitea.com/usage/actions/overview)
- [Act Runner Dokumentation](https://gitea.com/gitea/act_runner)
- [Docker Compose Dokumentation](https://docs.docker.com/compose/)
## Support
Bei Problemen:
1. Prüfe Runner Logs: `docker logs gitea-runner`
2. Prüfe Workflow Logs in Gitea Actions Tab
3. Verifiziere alle Secrets sind korrekt gesetzt
4. Teste SSH-Verbindung zur Synology manuell
+435
View File
@@ -0,0 +1,435 @@
# Synology NAS Deployment Anleitung
Diese Anleitung beschreibt, wie du diese Laravel-Anwendung auf deiner Synology NAS mit Portainer deployen kannst.
## 📋 Inhaltsverzeichnis
1. [Voraussetzungen](#voraussetzungen)
2. [Manuelles Deployment](#-vorbereitung)
3. [Automatisches CI/CD Deployment](#-automatisches-deployment-mit-gitea-actions)
4. [Monitoring & Wartung](#-monitoring--wartung)
5. [Troubleshooting](#-troubleshooting)
## Voraussetzungen
- Synology NAS mit Docker-Unterstützung
- Portainer läuft bereits auf deiner NAS
- SSH-Zugriff auf die Synology NAS (optional, aber empfohlen)
- Mindestens 2GB freier RAM
- Mindestens 5GB freier Speicherplatz
- (Optional) Gitea für automatisches CI/CD Deployment
## 📦 Vorbereitung
### 1. Projekt-Dateien auf die NAS übertragen
Es gibt mehrere Möglichkeiten, die Dateien auf deine Synology zu übertragen:
#### Option A: Via Git (empfohlen)
```bash
# SSH auf die Synology NAS
ssh admin@your-synology-ip
# Navigiere zu einem geeigneten Verzeichnis (z.B. /volume1/docker/laravel)
cd /volume1/docker
git clone <dein-repository-url> laravel-app
cd laravel-app
```
#### Option B: Via File Station
1. Öffne die Synology File Station
2. Erstelle einen Ordner: `/docker/laravel-app`
3. Lade alle Projekt-Dateien in diesen Ordner hoch
#### Option C: Via rsync/scp
```bash
# Vom lokalen Rechner aus
rsync -avz --exclude 'node_modules' --exclude 'vendor' \
/Users/sebastianfrohlich/Herd/frontend/ \
admin@your-synology-ip:/volume1/docker/laravel-app/
```
### 2. Umgebungsvariablen konfigurieren
```bash
# SSH auf der NAS
cd /volume1/docker/laravel-app
# Kopiere die Synology-Beispiel-Datei
cp .env.synology.example .env.synology
# Bearbeite die Datei mit deinen spezifischen Einstellungen
nano .env.synology
```
**Wichtige Anpassungen in `.env.synology`:**
```env
# Ersetze mit deiner Synology IP-Adresse
APP_URL=http://192.168.1.100:8080
# Setze APP_DEBUG auf false für Produktion
APP_DEBUG=false
# Generiere einen neuen APP_KEY (wichtig für Sicherheit!)
# Dies kann später mit: docker exec laravel_app php artisan key:generate gemacht werden
# PostgreSQL Passwort - ändere dies!
DB_PASSWORD2=dein_sicheres_passwort_hier
```
## 🚀 Deployment mit Portainer
### Methode 1: Docker Compose Stack (empfohlen)
1. **Öffne Portainer** in deinem Browser: `http://your-synology-ip:9000`
2. **Navigiere zu "Stacks"**:
- Klicke auf "Stacks" im linken Menü
- Klicke auf "+ Add stack"
3. **Stack konfigurieren**:
- **Name**: `laravel-app`
- **Build method**: Wähle "Repository"
- **Repository URL**: Gib deine Git-Repository-URL ein (falls vorhanden)
- Oder wähle "Upload" und lade `docker-compose.synology.yml` hoch
- Oder wähle "Web editor" und kopiere den Inhalt von `docker-compose.synology.yml`
4. **Umgebungsvariablen setzen**:
Klicke auf "Add an environment variable" und füge folgende Variablen hinzu:
```
APP_NAME=Laravel
APP_ENV=production
APP_KEY=base64:L9RVZ3pNFyAvTbMqicT1rL5GbgE+7lJerkU9wyc95H8=
APP_DEBUG=false
APP_URL=http://your-synology-ip:8080
DB_CONNECTION2=pgsql
DB_HOST2=postgres
DB_PORT2=5432
DB_DATABASE2=ingest_db
DB_USERNAME2=ingest_user
DB_PASSWORD2=dein_sicheres_passwort
```
5. **Stack deployen**:
- Klicke auf "Deploy the stack"
- Warte, bis der Build-Prozess abgeschlossen ist (kann 5-10 Minuten dauern)
### Methode 2: Build und Deploy manuell via SSH
```bash
# SSH auf die Synology
ssh admin@your-synology-ip
# Navigiere zum Projekt-Verzeichnis
cd /volume1/docker/laravel-app
# Baue das Docker Image
docker build -t laravel-app:latest .
# Starte die Services mit docker-compose
docker-compose -f docker-compose.synology.yml up -d
```
## 🔍 Verifikation
### 1. Überprüfe den Container-Status
In Portainer:
- Gehe zu "Containers"
- Du solltest zwei laufende Container sehen:
- `laravel_app` (Status: running, Port: 0.0.0.0:8080->80/tcp)
- `laravel_postgres` (Status: running, Port: 5432/tcp)
Via SSH:
```bash
docker ps
```
### 2. Überprüfe die Logs
In Portainer:
- Klicke auf den Container `laravel_app`
- Wähle "Logs"
- Du solltest keine Fehler sehen
Via SSH:
```bash
# Laravel App Logs
docker logs laravel_app
# PostgreSQL Logs
docker logs laravel_postgres
```
### 3. Teste die Anwendung
Öffne deinen Browser und navigiere zu:
```
http://your-synology-ip:8080
```
Du solltest die Laravel-Anwendung sehen!
## 🔧 Nützliche Befehle
### Artisan-Befehle ausführen
```bash
# Laravel Cache leeren
docker exec laravel_app php artisan cache:clear
# Neuen APP_KEY generieren
docker exec laravel_app php artisan key:generate
# Migrationen ausführen
docker exec laravel_app php artisan migrate
# Seeder ausführen
docker exec laravel_app php artisan db:seed
```
### Container neustarten
Via Portainer:
- Gehe zu "Containers"
- Wähle den Container aus
- Klicke auf "Restart"
Via SSH:
```bash
docker-compose -f docker-compose.synology.yml restart
```
### Container stoppen und entfernen
Via Portainer:
- Gehe zu "Stacks"
- Wähle den Stack "laravel-app"
- Klicke auf "Stop" oder "Delete"
Via SSH:
```bash
docker-compose -f docker-compose.synology.yml down
# Mit Volumes löschen (Achtung: Löscht die Datenbank!)
docker-compose -f docker-compose.synology.yml down -v
```
### Logs live verfolgen
```bash
# Alle Container
docker-compose -f docker-compose.synology.yml logs -f
# Nur Laravel App
docker logs -f laravel_app
# Nur PostgreSQL
docker logs -f laravel_postgres
```
## 🔐 Sicherheitshinweise
1. **APP_KEY ändern**: Generiere einen neuen APP_KEY für die Produktion:
```bash
docker exec laravel_app php artisan key:generate
```
2. **Datenbank-Passwort**: Ändere das Standard-PostgreSQL-Passwort in `.env.synology`
3. **APP_DEBUG**: Stelle sicher, dass `APP_DEBUG=false` in der Produktion
4. **Firewall**: Konfiguriere die Synology-Firewall, um nur benötigte Ports zu öffnen
5. **SSL/HTTPS**: Für den Produktionsbetrieb solltest du einen Reverse Proxy (z.B. Synology DSM Reverse Proxy) mit SSL-Zertifikat einrichten
## 🌐 Reverse Proxy einrichten (optional, aber empfohlen)
Für den Zugriff über eine Domain mit HTTPS:
1. **In Synology DSM**:
- Gehe zu "Systemsteuerung" → "Anmeldungsportal" → "Erweitert"
- Klicke auf "Reverse Proxy" → "Erstellen"
2. **Konfiguration**:
- **Protokoll**: HTTPS
- **Hostname**: your-domain.com
- **Port**: 443
- **Zielprotokoll**: HTTP
- **Zielhost**: localhost
- **Zielport**: 8080
3. **SSL-Zertifikat**:
- Gehe zu "Systemsteuerung" → "Sicherheit" → "Zertifikat"
- Füge ein Let's Encrypt-Zertifikat hinzu
## 📊 Monitoring & Wartung
### Container-Ressourcen überwachen
In Portainer:
- Gehe zu "Containers"
- Wähle einen Container
- Klicke auf "Stats" für Echtzeit-Metriken (CPU, RAM, Netzwerk)
### Backup
```bash
# PostgreSQL Datenbank sichern
docker exec laravel_postgres pg_dump -U ingest_user ingest_db > backup_$(date +%Y%m%d).sql
# Gesamten Stack sichern (inkl. Volumes)
docker run --rm \
-v laravel-app_postgres-data:/data \
-v /volume1/docker/backups:/backup \
alpine tar czf /backup/postgres-backup-$(date +%Y%m%d).tar.gz /data
```
### Updates
```bash
# Projekt-Code aktualisieren (wenn via Git)
cd /volume1/docker/laravel-app
git pull
# Neu bauen und deployen
docker-compose -f docker-compose.synology.yml up -d --build
# Migrationen ausführen
docker exec laravel_app php artisan migrate --force
```
## ❓ Troubleshooting
### Problem: Container startet nicht
```bash
# Logs prüfen
docker logs laravel_app
# Container interaktiv starten für Debugging
docker exec -it laravel_app sh
```
### Problem: Datenbank-Verbindung fehlgeschlagen
```bash
# PostgreSQL-Container prüfen
docker exec laravel_postgres pg_isready -U ingest_user
# Umgebungsvariablen prüfen
docker exec laravel_app env | grep DB_
```
### Problem: Permissions-Fehler
```bash
# Storage-Permissions korrigieren
docker exec laravel_app chmod -R 775 storage bootstrap/cache
docker exec laravel_app chown -R nginx:nginx storage bootstrap/cache
```
### Problem: Port bereits belegt
Wenn Port 8080 bereits verwendet wird:
1. Öffne `docker-compose.synology.yml`
2. Ändere die Port-Mapping: `"8081:80"` statt `"8080:80"`
3. Aktualisiere `APP_URL` in `.env.synology` entsprechend
## 🤖 Automatisches Deployment mit Gitea Actions
Wenn du Gitea auf deiner Synology NAS verwendest, kannst du den gesamten Build- und Deployment-Prozess automatisieren!
### Vorteile von CI/CD
- ✅ Automatischer Build bei jedem Git Push
- ✅ Automatische Tests vor Deployment
- ✅ Automatisches Deployment auf die NAS
- ✅ Rollback bei Fehlern
- ✅ Keine manuellen Schritte mehr nötig
### Quick Start
1. **Gitea Runner einrichten**
- Siehe detaillierte Anleitung: [GITEA_RUNNER_SETUP.md](GITEA_RUNNER_SETUP.md)
- Kurz: Runner Docker Container auf NAS deployen
- Runner in Gitea registrieren
2. **Repository Secrets konfigurieren**
Gehe in Gitea zu: **Repository → Settings → Secrets**
Füge folgende Secrets hinzu:
```
SYNOLOGY_HOST=192.168.1.100
SYNOLOGY_USER=admin
SYNOLOGY_SSH_KEY=<dein-ssh-private-key>
APP_KEY=<dein-laravel-app-key>
```
3. **Workflow pushen**
Der Workflow in [`.gitea/workflows/deploy-synology.yml`](.gitea/workflows/deploy-synology.yml) ist bereits vorkonfiguriert.
```bash
git add .gitea/workflows/deploy-synology.yml
git commit -m "ci: add automated deployment workflow"
git push origin main
```
4. **Automatisches Deployment genießen! 🎉**
Bei jedem Push auf `main` oder `develop`:
- Tests werden ausgeführt
- Docker Image wird gebaut
- Deployment auf NAS erfolgt automatisch
- Health Check verifiziert Deployment
### Workflow-Ablauf
```mermaid
graph LR
A[Git Push] --> B[Run Tests]
B --> C{Tests OK?}
C -->|Ja| D[Build Docker Image]
C -->|Nein| E[Abbruch]
D --> F[Transfer zu NAS]
F --> G[Deploy auf NAS]
G --> H[Health Check]
H --> I{Healthy?}
I -->|Ja| J[✅ Success]
I -->|Nein| K[Rollback]
```
### Workflow überwachen
1. Gehe zu deinem Repository in Gitea
2. Klicke auf **Actions**
3. Siehe alle Workflow-Runs mit Status
4. Klicke auf einen Run für Details und Logs
### Erweiterte Konfiguration
Weitere Details zur CI/CD-Konfiguration findest du in:
- [GITEA_RUNNER_SETUP.md](GITEA_RUNNER_SETUP.md) - Detaillierte Runner-Setup Anleitung
- [`.gitea/workflows/deploy-synology.yml`](.gitea/workflows/deploy-synology.yml) - Workflow-Konfiguration
- [`scripts/deploy-synology.sh`](scripts/deploy-synology.sh) - Deployment-Script
## 📝 Weitere Ressourcen
- [Laravel Dokumentation](https://laravel.com/docs)
- [Docker Dokumentation](https://docs.docker.com/)
- [Portainer Dokumentation](https://docs.portainer.io/)
- [Synology Docker Anleitung](https://www.synology.com/en-us/dsm/packages/Docker)
- [Gitea Actions Dokumentation](https://docs.gitea.com/usage/actions/overview)
## 🆘 Support
Bei Problemen:
1. Prüfe die Container-Logs
2. Überprüfe die Umgebungsvariablen
3. Stelle sicher, dass alle Ports verfügbar sind
4. Prüfe die Synology-Firewall-Einstellungen
5. Bei CI/CD: Prüfe Gitea Actions Logs und Runner Status
@@ -1,30 +1,90 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands;
use App\Jobs\SyncBackendDataPool;
use App\Jobs\TransformDataPoolToProduction;
use Illuminate\Console\Command;
use function Laravel\Prompts\confirm;
use function Laravel\Prompts\info;
use function Laravel\Prompts\spin;
use function Laravel\Prompts\warning;
class RebuildFromDataPoolCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:rebuild-from-data-pool-command';
protected $signature = 'backend:rebuild
{--batch-size=1000 : Number of records to process per batch}
{--transform-batch-size=100 : Number of records to process per batch for transformation}
{--queue : Dispatch jobs to the queue instead of running synchronously}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Command description';
protected $description = 'Complete rebuild: Full sync from backend + transformation to production tables';
/**
* Execute the console command.
*/
public function handle()
public function handle(): int
{
//
info('Preparing for COMPLETE REBUILD');
warning('This will:');
warning('1. Truncate and rebuild backend_data_pool (full sync)');
warning('2. Transform all data into companies and transactions');
if (! $this->option('no-interaction') && ! confirm('Do you want to continue?', true)) {
info('Rebuild cancelled.');
return self::SUCCESS;
}
$batchSize = (int) $this->option('batch-size');
$transformBatchSize = (int) $this->option('transform-batch-size');
$useQueue = $this->option('queue');
// Step 1: Full Sync
info('Step 1/2: Full Sync from Backend');
$syncJob = new SyncBackendDataPool(
fullSync: true,
batchSize: $batchSize
);
if ($useQueue) {
info('Dispatching sync job to queue...');
dispatch($syncJob);
info('Sync job dispatched!');
warning('Please wait for the sync job to complete before transformation.');
return self::SUCCESS;
} else {
spin(
fn () => $syncJob->handle(),
'Syncing data from backend...'
);
info('✓ Sync completed!');
}
$this->newLine();
// Step 2: Transform Data
info('Step 2/2: Transform Data Pool to Production');
$transformJob = new TransformDataPoolToProduction(batchSize: $transformBatchSize);
spin(
fn () => $transformJob->handle(),
'Transforming data...'
);
info('✓ Transformation completed!');
$this->newLine();
info('Complete rebuild finished successfully!');
return self::SUCCESS;
}
}
@@ -94,7 +94,7 @@ class SyncBackendDataPoolCommand extends Command
*/
private function showStats(): int
{
$job = new SyncBackendDataPool();
$job = new SyncBackendDataPool;
$stats = $job->getStats();
info('Backend Data Pool Statistics');
@@ -117,7 +117,7 @@ class SyncBackendDataPoolCommand extends Command
*/
private function showStatsBefore(): void
{
$job = new SyncBackendDataPool();
$job = new SyncBackendDataPool;
$stats = $job->getStats();
$this->newLine();
@@ -138,7 +138,7 @@ class SyncBackendDataPoolCommand extends Command
*/
private function showStatsAfter(): void
{
$job = new SyncBackendDataPool();
$job = new SyncBackendDataPool;
$stats = $job->getStats();
$this->newLine();
@@ -82,7 +82,7 @@ class TransformDataPoolCommand extends Command
*/
private function showStats(): int
{
$job = new TransformDataPoolToProduction();
$job = new TransformDataPoolToProduction;
$stats = $job->getStats();
info('Data Pool Transformation Statistics');
@@ -105,7 +105,7 @@ class TransformDataPoolCommand extends Command
*/
private function showStatsBefore(): void
{
$job = new TransformDataPoolToProduction();
$job = new TransformDataPoolToProduction;
$stats = $job->getStats();
$this->newLine();
@@ -126,7 +126,7 @@ class TransformDataPoolCommand extends Command
*/
private function showStatsAfter(): void
{
$job = new TransformDataPoolToProduction();
$job = new TransformDataPoolToProduction;
$stats = $job->getStats();
$this->newLine();
+1 -2
View File
@@ -29,8 +29,7 @@ class SyncBackendDataPool implements ShouldQueue
public function __construct(
public bool $fullSync = true,
public ?int $batchSize = 1000,
) {
}
) {}
/**
* Execute the job.
+1 -1
View File
@@ -127,7 +127,7 @@ class Transaction extends Model
{
$risk = $this->getRiskAssessment();
if (!$risk) {
if (! $risk) {
return true; // Keine Risk-Assessment → Review
}
+1 -1
View File
@@ -69,7 +69,7 @@ class Transaction extends Model
}
} catch (\Exception $e) {
// If schema query fails (e.g., during migrations), just use standard casts
\Illuminate\Support\Facades\Log::warning('Failed to load JSONB column casts: ' . $e->getMessage());
\Illuminate\Support\Facades\Log::warning('Failed to load JSONB column casts: '.$e->getMessage());
}
// Cache for subsequent calls
Binary file not shown.
Binary file not shown.
Binary file not shown.
+14 -14
View File
@@ -86,14 +86,14 @@ return [
'driver' => 'pgsql',
'url' => env('DB_URL'),
'host' => env('DB_HOST2', '127.0.0.1'),
'port' => env('DB_PORT2', '5433'),
'database' => env('DB_DATABASE2', 'risk_ingest_db'),
'username' => env('DB_USERNAME2', 'risk_ingest_user'),
'password' => env('DB_PASSWORD2', ''),
'port' => env('DB_PORT2', '5432'),
'database' => env('DB_DATABASE2', 'ingest_db'),
'username' => env('DB_USERNAME2', 'ingest_user'),
'password' => env('DB_PASSWORD2', 'ingest_pwd'),
'charset' => env('DB_CHARSET', 'utf8'),
'prefix' => '',
'prefix_indexes' => true,
'search_path' => 'public',
'search_path' => env('DB_SEARCH_PATH2', 'public'),
'sslmode' => 'prefer',
],
@@ -101,13 +101,13 @@ return [
'driver' => env('DB_CONNECTION2'),
'host' => env('DB_HOST2', '127.0.0.1'),
'port' => env('DB_PORT2', '5432'),
'database' => env('DB_DATABASE2', 'laravel'),
'username' => env('DB_USERNAME2', 'root'),
'password' => env('DB_PASSWORD2', ''),
'database' => env('DB_DATABASE2', 'ingest_db'),
'username' => env('DB_USERNAME2', 'ingest_user'),
'password' => env('DB_PASSWORD2', 'ingest_pwd'),
'charset' => env('DB_CHARSET', 'utf8'),
'prefix' => '',
'prefix_indexes' => true,
'search_path' => 'backend', // Backend Schema (gleiche DB wie pgsql_second, anderes Schema)
'search_path' => env('DB_BACKEND_SEARCH_PATH2', 'backend'), // Backend Schema (gleiche DB wie pgsql_second, anderes Schema)
'sslmode' => 'prefer',
],
@@ -115,13 +115,13 @@ return [
'driver' => env('DB_CONNECTION2'),
'host' => env('DB_HOST2', '127.0.0.1'),
'port' => env('DB_PORT2', '5432'),
'database' => env('DB_DATABASE2', 'laravel'),
'username' => env('DB_USERNAME2', 'root'),
'password' => env('DB_PASSWORD2', ''),
'database' => env('DB_DATABASE2', 'ingest_db'),
'username' => env('DB_USERNAME2', 'ingest_user'),
'password' => env('DB_PASSWORD2', 'ingest_pwd'),
'charset' => env('DB_CHARSET', 'utf8'),
'prefix' => '',
'prefix_indexes' => true,
'search_path' => 'public',
'search_path' => env('DB_SEARCH_PATH2', 'public'),
'sslmode' => 'prefer',
],
@@ -175,7 +175,7 @@ return [
'options' => [
'cluster' => env('REDIS_CLUSTER', 'redis'),
'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_') . '_database_'),
'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'),
'persistent' => env('REDIS_PERSISTENT', false),
],
@@ -12,37 +12,64 @@ return new class extends Migration
*/
public function up(): void
{
// Use raw SQL with CASCADE to drop columns and dependent views
DB::statement('ALTER TABLE transactions
DROP COLUMN IF EXISTS registry_company_number CASCADE,
DROP COLUMN IF EXISTS registry_source CASCADE,
DROP COLUMN IF EXISTS registry_match_score CASCADE,
DROP COLUMN IF EXISTS registry_data CASCADE,
DROP COLUMN IF EXISTS registry_last_refreshed_at CASCADE,
DROP COLUMN IF EXISTS genesis_context CASCADE,
DROP COLUMN IF EXISTS genesis_last_refreshed_at CASCADE,
DROP COLUMN IF EXISTS govdata_data CASCADE,
DROP COLUMN IF EXISTS govdata_last_refreshed_at CASCADE,
DROP COLUMN IF EXISTS bundesanzeiger_data CASCADE,
DROP COLUMN IF EXISTS bundesanzeiger_last_refreshed_at CASCADE,
DROP COLUMN IF EXISTS insolvency_data CASCADE,
DROP COLUMN IF EXISTS insolvency_last_refreshed_at CASCADE,
DROP COLUMN IF EXISTS rss_alerts CASCADE,
DROP COLUMN IF EXISTS rss_last_refreshed_at CASCADE,
DROP COLUMN IF EXISTS sanctions_data CASCADE,
DROP COLUMN IF EXISTS sanctions_last_refreshed_at CASCADE,
DROP COLUMN IF EXISTS pep_data CASCADE,
DROP COLUMN IF EXISTS pep_last_refreshed_at CASCADE,
DROP COLUMN IF EXISTS gleif_lei CASCADE,
DROP COLUMN IF EXISTS gleif_data CASCADE,
DROP COLUMN IF EXISTS gleif_last_refreshed_at CASCADE,
DROP COLUMN IF EXISTS eu_sanctions_data CASCADE,
DROP COLUMN IF EXISTS eu_sanctions_last_refreshed_at CASCADE,
DROP COLUMN IF EXISTS handelsregister_data CASCADE,
DROP COLUMN IF EXISTS handelsregister_last_refreshed_at CASCADE,
DROP COLUMN IF EXISTS handelsregister_status CASCADE,
DROP COLUMN IF EXISTS handelsregister_entity_id CASCADE
');
// Check if we're using PostgreSQL (production) or SQLite (testing)
$driver = Schema::getConnection()->getDriverName();
if ($driver === 'pgsql') {
// PostgreSQL: Use raw SQL with CASCADE to drop columns and dependent views
DB::statement('ALTER TABLE transactions
DROP COLUMN IF EXISTS registry_company_number CASCADE,
DROP COLUMN IF EXISTS registry_source CASCADE,
DROP COLUMN IF EXISTS registry_match_score CASCADE,
DROP COLUMN IF EXISTS registry_data CASCADE,
DROP COLUMN IF EXISTS registry_last_refreshed_at CASCADE,
DROP COLUMN IF EXISTS genesis_context CASCADE,
DROP COLUMN IF EXISTS genesis_last_refreshed_at CASCADE,
DROP COLUMN IF EXISTS govdata_data CASCADE,
DROP COLUMN IF EXISTS govdata_last_refreshed_at CASCADE,
DROP COLUMN IF EXISTS bundesanzeiger_data CASCADE,
DROP COLUMN IF EXISTS bundesanzeiger_last_refreshed_at CASCADE,
DROP COLUMN IF EXISTS insolvency_data CASCADE,
DROP COLUMN IF EXISTS insolvency_last_refreshed_at CASCADE,
DROP COLUMN IF EXISTS rss_alerts CASCADE,
DROP COLUMN IF EXISTS rss_last_refreshed_at CASCADE,
DROP COLUMN IF EXISTS sanctions_data CASCADE,
DROP COLUMN IF EXISTS sanctions_last_refreshed_at CASCADE,
DROP COLUMN IF EXISTS pep_data CASCADE,
DROP COLUMN IF EXISTS pep_last_refreshed_at CASCADE,
DROP COLUMN IF EXISTS gleif_lei CASCADE,
DROP COLUMN IF EXISTS gleif_data CASCADE,
DROP COLUMN IF EXISTS gleif_last_refreshed_at CASCADE,
DROP COLUMN IF EXISTS eu_sanctions_data CASCADE,
DROP COLUMN IF EXISTS eu_sanctions_last_refreshed_at CASCADE,
DROP COLUMN IF EXISTS handelsregister_data CASCADE,
DROP COLUMN IF EXISTS handelsregister_last_refreshed_at CASCADE,
DROP COLUMN IF EXISTS handelsregister_status CASCADE,
DROP COLUMN IF EXISTS handelsregister_entity_id CASCADE
');
} else {
// SQLite: Use Schema Builder to drop columns (if they exist)
Schema::table('transactions', function (Blueprint $table) {
$columns = [
'registry_company_number', 'registry_source', 'registry_match_score',
'registry_data', 'registry_last_refreshed_at', 'genesis_context',
'genesis_last_refreshed_at', 'govdata_data', 'govdata_last_refreshed_at',
'bundesanzeiger_data', 'bundesanzeiger_last_refreshed_at', 'insolvency_data',
'insolvency_last_refreshed_at', 'rss_alerts', 'rss_last_refreshed_at',
'sanctions_data', 'sanctions_last_refreshed_at', 'pep_data',
'pep_last_refreshed_at', 'gleif_lei', 'gleif_data',
'gleif_last_refreshed_at', 'eu_sanctions_data', 'eu_sanctions_last_refreshed_at',
'handelsregister_data', 'handelsregister_last_refreshed_at',
'handelsregister_status', 'handelsregister_entity_id',
];
foreach ($columns as $column) {
if (Schema::hasColumn('transactions', $column)) {
$table->dropColumn($column);
}
}
});
}
}
/**
+23 -7
View File
@@ -3,8 +3,8 @@
namespace Database\Seeders;
use App\Models\User;
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash;
class DatabaseSeeder extends Seeder
{
@@ -13,13 +13,29 @@ class DatabaseSeeder extends Seeder
*/
public function run(): void
{
// User::factory(10)->create();
// Create initial admin user without using factory (production-safe)
User::firstOrCreate(
['email' => 'admin@example.com'],
[
'name' => 'Admin User',
'email' => 'admin@example.com',
'password' => Hash::make('password'),
'email_verified_at' => now(),
]
);
User::factory()->withoutTwoFactor()->create([
'name' => 'Test User',
'email' => 'test@example.com',
]);
// Create test user
User::firstOrCreate(
['email' => 'test@example.com'],
[
'name' => 'Test User',
'email' => 'test@example.com',
'password' => Hash::make('password'),
'email_verified_at' => now(),
]
);
$this->call(DaxCompanySeeder::class);
// Note: Company and Transaction data is now synced from the backend
// via TransformDataPoolToProduction job, not seeded manually
}
}
-153
View File
@@ -1,153 +0,0 @@
<?php
namespace Database\Seeders;
use App\Models\Company;
use App\Models\Transaction;
use Carbon\Carbon;
use Illuminate\Database\Seeder;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
class DaxCompanySeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
$companies = [
['name' => 'Adidas AG', 'legal_name' => 'adidas Aktiengesellschaft', 'ticker' => 'ADS', 'sector' => 'Konsumgüter (Zyklisch)', 'headquarters' => 'Herzogenaurach', 'summary' => 'Globaler Sportartikelhersteller mit Schwerpunkt auf Schuhen, Bekleidung und digitalen Services.', 'kyc_risk_level' => 'medium'],
['name' => 'Airbus SE', 'legal_name' => 'Airbus Societas Europaea', 'ticker' => 'AIR', 'sector' => 'Industrie', 'headquarters' => 'Leiden', 'country' => 'NL', 'summary' => 'Luft- und Raumfahrtkonzern mit zivilen, militärischen und Raumfahrtaktivitäten.', 'kyc_risk_level' => 'medium'],
['name' => 'Allianz SE', 'legal_name' => 'Allianz Societas Europaea', 'ticker' => 'ALV', 'sector' => 'Finanzen', 'headquarters' => 'München', 'summary' => 'Versicherung und Vermögensverwaltung mit globalem Risikoportfolio.', 'kyc_risk_level' => 'low'],
['name' => 'BASF SE', 'legal_name' => 'BASF Societas Europaea', 'ticker' => 'BAS', 'sector' => 'Grundstoffe', 'headquarters' => 'Ludwigshafen', 'summary' => 'Chemiekonzern mit Aktivitäten in Materialien, Industrial Solutions und Agricultural Solutions.', 'kyc_risk_level' => 'medium'],
['name' => 'Bayer AG', 'legal_name' => 'Bayer Aktiengesellschaft', 'ticker' => 'BAYN', 'sector' => 'Gesundheit', 'headquarters' => 'Leverkusen', 'summary' => 'Life-Science-Unternehmen mit den Segmenten Pharmaceuticals, Consumer Health und Crop Science.', 'kyc_risk_level' => 'medium'],
['name' => 'Beiersdorf AG', 'legal_name' => 'Beiersdorf Aktiengesellschaft', 'ticker' => 'BEI', 'sector' => 'Konsumgüter (Defensiv)', 'headquarters' => 'Hamburg', 'summary' => 'Hautpflegeexperte mit Marken wie Nivea, Eucerin und Tesa.', 'kyc_risk_level' => 'low'],
['name' => 'BMW AG', 'legal_name' => 'Bayerische Motoren Werke Aktiengesellschaft', 'ticker' => 'BMW', 'sector' => 'Konsumgüter (Zyklisch)', 'headquarters' => 'München', 'summary' => 'Hersteller von Premiumfahrzeugen mit den Marken BMW, MINI und Rolls-Royce.', 'kyc_risk_level' => 'medium'],
['name' => 'Brenntag SE', 'legal_name' => 'Brenntag Societas Europaea', 'ticker' => 'BNR', 'sector' => 'Industrie', 'headquarters' => 'Essen', 'summary' => 'Weltweit führender Chemie- und Inhaltsstoffdistributor mit Value-Added Services.', 'kyc_risk_level' => 'medium'],
['name' => 'Commerzbank AG', 'legal_name' => 'Commerzbank Aktiengesellschaft', 'ticker' => 'CBK', 'sector' => 'Finanzen', 'headquarters' => 'Frankfurt am Main', 'summary' => 'Universalbank mit Fokus auf Mittelstands- und Firmenkunden.', 'kyc_risk_level' => 'high'],
['name' => 'Daimler Truck AG', 'legal_name' => 'Daimler Truck Holding Aktiengesellschaft', 'ticker' => 'DTG', 'sector' => 'Industrie', 'headquarters' => 'Leinfelden-Echterdingen', 'summary' => 'Hersteller von schweren Lkw und Bussen mit globalem Produktionsnetzwerk.', 'kyc_risk_level' => 'medium'],
['name' => 'Deutsche Bank AG', 'legal_name' => 'Deutsche Bank Aktiengesellschaft', 'ticker' => 'DBK', 'sector' => 'Finanzen', 'headquarters' => 'Frankfurt am Main', 'summary' => 'Globales Investment- und Firmenkundengeschäft mit Vermögensverwaltung.', 'kyc_risk_level' => 'high'],
['name' => 'Deutsche Börse AG', 'legal_name' => 'Deutsche Börse Aktiengesellschaft', 'ticker' => 'DB1', 'sector' => 'Finanzen', 'headquarters' => 'Eschborn', 'summary' => 'Marktinfrastrukturbetreiber für Handel, Clearing und Settlement.', 'kyc_risk_level' => 'low'],
['name' => 'Deutsche Post AG', 'legal_name' => 'Deutsche Post Aktiengesellschaft', 'ticker' => 'DPW', 'sector' => 'Industrie', 'headquarters' => 'Bonn', 'summary' => 'Logistikkonzern der Deutsche Post DHL Group mit globaler Supply-Chain-Plattform.', 'kyc_risk_level' => 'medium'],
['name' => 'Deutsche Telekom AG', 'legal_name' => 'Deutsche Telekom Aktiengesellschaft', 'ticker' => 'DTE', 'sector' => 'Kommunikation', 'headquarters' => 'Bonn', 'summary' => 'Integrierter Telekommunikationsanbieter mit Fokus auf Europa und USA.', 'kyc_risk_level' => 'low'],
['name' => 'E.ON SE', 'legal_name' => 'E.ON Societas Europaea', 'ticker' => 'EOAN', 'sector' => 'Versorger', 'headquarters' => 'Essen', 'summary' => 'Energieversorger mit Schwerpunkt auf Energienetzen und Kundenlösungen.', 'kyc_risk_level' => 'medium'],
['name' => 'Fresenius SE & Co. KGaA', 'legal_name' => 'Fresenius SE & Co. KGaA', 'ticker' => 'FRE', 'sector' => 'Gesundheit', 'headquarters' => 'Bad Homburg vor der Höhe', 'summary' => 'Gesundheitskonzern mit Kliniken, Medizintechnik und Dienstleistungen.', 'kyc_risk_level' => 'medium'],
['name' => 'Hannover Rück SE', 'legal_name' => 'Hannover Rück Societas Europaea', 'ticker' => 'HNR1', 'sector' => 'Finanzen', 'headquarters' => 'Hannover', 'summary' => 'Rückversicherer mit Sparten in Schaden/Unfall und Leben/Kranken.', 'kyc_risk_level' => 'low'],
['name' => 'Heidelberg Materials AG', 'legal_name' => 'Heidelberg Materials Aktiengesellschaft', 'ticker' => 'HEI', 'sector' => 'Grundstoffe', 'headquarters' => 'Heidelberg', 'summary' => 'Baustoffhersteller mit Schwerpunkt auf Zement, Zuschlagstoffen und Transportbeton.', 'kyc_risk_level' => 'medium'],
['name' => 'Henkel AG & Co. KGaA', 'legal_name' => 'Henkel AG & Co. Kommanditgesellschaft auf Aktien', 'ticker' => 'HEN3', 'sector' => 'Konsumgüter (Defensiv)', 'headquarters' => 'Düsseldorf', 'summary' => 'Hersteller von Konsumgütern und Klebstofftechnologien mit globaler Präsenz.', 'kyc_risk_level' => 'low'],
['name' => 'Infineon Technologies AG', 'legal_name' => 'Infineon Technologies Aktiengesellschaft', 'ticker' => 'IFX', 'sector' => 'Informationstechnologie', 'headquarters' => 'Neubiberg', 'summary' => 'Halbleiterhersteller für Mobilität, Energieeffizienz und Sicherheit.', 'kyc_risk_level' => 'medium'],
['name' => 'Mercedes-Benz Group AG', 'legal_name' => 'Mercedes-Benz Group Aktiengesellschaft', 'ticker' => 'MBG', 'sector' => 'Konsumgüter (Zyklisch)', 'headquarters' => 'Stuttgart', 'summary' => 'Hersteller von Premium-Pkw und Vans mit starkem E-Mobility-Fokus.', 'kyc_risk_level' => 'medium'],
['name' => 'Porsche SE', 'legal_name' => 'Porsche Automobil Holding Societas Europaea', 'ticker' => 'PAH3', 'sector' => 'Konsumgüter (Zyklisch)', 'headquarters' => 'Stuttgart', 'summary' => 'Strategische Beteiligungsgesellschaft mit Schwerpunkt Automobil- und Mobilitätssektor.', 'kyc_risk_level' => 'medium'],
['name' => 'PUMA SE', 'legal_name' => 'PUMA Societas Europaea', 'ticker' => 'PUM', 'sector' => 'Konsumgüter (Zyklisch)', 'headquarters' => 'Herzogenaurach', 'summary' => 'Sportartikelunternehmen mit Performance- und Lifestyle-Sortiment.', 'kyc_risk_level' => 'medium'],
['name' => 'Qiagen N.V.', 'legal_name' => 'Qiagen Naamloze Vennootschap', 'ticker' => 'QIA', 'sector' => 'Gesundheit', 'headquarters' => 'Venlo', 'country' => 'NL', 'summary' => 'Spezialist für molekulare Diagnostik und Probenaufbereitung.', 'kyc_risk_level' => 'medium'],
['name' => 'RWE AG', 'legal_name' => 'RWE Aktiengesellschaft', 'ticker' => 'RWE', 'sector' => 'Versorger', 'headquarters' => 'Essen', 'summary' => 'Energieunternehmen in Transformation hin zu erneuerbaren Energien.', 'kyc_risk_level' => 'medium'],
['name' => 'SAP SE', 'legal_name' => 'SAP Societas Europaea', 'ticker' => 'SAP', 'sector' => 'Informationstechnologie', 'headquarters' => 'Walldorf', 'summary' => 'Anbieter von Unternehmenssoftware für ERP und Cloud-Lösungen.', 'kyc_risk_level' => 'low'],
['name' => 'Siemens AG', 'legal_name' => 'Siemens Aktiengesellschaft', 'ticker' => 'SIE', 'sector' => 'Industrie', 'headquarters' => 'München', 'summary' => 'Technologiekonzern mit Fokus auf Automatisierung, Digitalisierung und Infrastruktur.', 'kyc_risk_level' => 'low'],
['name' => 'Siemens Healthineers AG', 'legal_name' => 'Siemens Healthineers Aktiengesellschaft', 'ticker' => 'SHL', 'sector' => 'Gesundheit', 'headquarters' => 'Erlangen', 'summary' => 'Medizintechnikanbieter für Bildgebung, Diagnostik und Therapie.', 'kyc_risk_level' => 'medium'],
['name' => 'Symrise AG', 'legal_name' => 'Symrise Aktiengesellschaft', 'ticker' => 'SY1', 'sector' => 'Grundstoffe', 'headquarters' => 'Holzminden', 'summary' => 'Hersteller von Duft-, Geschmacks- und Nährstofflösungen.', 'kyc_risk_level' => 'medium'],
['name' => 'Volkswagen AG', 'legal_name' => 'Volkswagen Aktiengesellschaft', 'ticker' => 'VOW', 'sector' => 'Konsumgüter (Zyklisch)', 'headquarters' => 'Wolfsburg', 'summary' => 'Automobilkonzern mit Markenportfolio von Volumen bis Luxus.', 'kyc_risk_level' => 'high'],
];
$channels = [
'SEPA-Überweisung',
'Auslandsüberweisung',
'SWIFT MT103',
'Grenzüberschreitendes ACH',
'Hochwertiges Clearing',
'Handelsfinanzierungs-Abwicklung',
];
$flaggedReasons = [
'Gegenpartei erscheint auf konsolidierter Sanktionsliste.',
'Transaktionsbetrag liegt 300 % über dem historischen Durchschnitt.',
'Unüblicher Zahlungskorridor für das Branchensegment.',
'Mehrere gestapelte Überweisungen im Monitoring erkannt.',
'Begünstigte Bank sitzt in Hochrisiko-Jurisdiktion.',
'Schnelle Mittelbewegungen über verschachtelte Konten.',
'Negativmeldung zur Geschäftsleitung der Gegenpartei gefunden.',
];
$analysts = ['AML-Radar', 'Sanktionswächter', 'Verhaltensanalyse', 'Netzwerk-Intelligence', 'Adverse-Media-Scan'];
$signalCatalog = [
['type' => 'Sanktionsliste', 'value' => 'Treffer EU-Konsolidierte Liste'],
['type' => 'PEP-Screening', 'value' => 'Politisch exponierte Person, Stufe 2'],
['type' => 'Geo-Risiko', 'value' => 'Hochrisikoland: TR'],
['type' => 'Transaktionsdichte', 'value' => '6 verknüpfte Zahlungen binnen 24h'],
['type' => 'Strukturierung', 'value' => 'Hinweise auf Smurfing-Muster'],
['type' => 'Negativnachrichten', 'value' => 'Aktuelle Betrugsermittlungen gemeldet'],
['type' => 'Wirtschaftlich Berechtigter', 'value' => 'Intransparente Eigentümerstruktur'],
['type' => 'Branchenabweichung', 'value' => 'Handelswert 210 % über Peergroup'],
];
$statusPool = [
Transaction::STATUS_TRUE_POSITIVE,
Transaction::STATUS_FALSE_POSITIVE,
Transaction::STATUS_FALSE_POSITIVE,
Transaction::STATUS_CLEARED,
Transaction::STATUS_CLEARED,
Transaction::STATUS_CLEARED,
];
$faker = fake('en_GB');
$sequence = 1;
DB::transaction(function () use (
$companies,
$channels,
$flaggedReasons,
$analysts,
$signalCatalog,
$statusPool,
$faker,
&$sequence
) {
Transaction::query()->delete();
Company::query()->delete();
foreach ($companies as $companyData) {
$company = Company::create(array_merge(
[
'country' => $companyData['country'] ?? 'DE',
],
$companyData
));
$transactionTotal = random_int(4, 7);
for ($i = 0; $i < $transactionTotal; $i++) {
$status = Arr::random($statusPool);
$riskScore = match ($status) {
Transaction::STATUS_TRUE_POSITIVE => random_int(82, 95),
Transaction::STATUS_FALSE_POSITIVE => random_int(48, 65),
default => random_int(12, 28),
};
$executedAt = Carbon::now()
->subDays(random_int(1, 45))
->setTime(random_int(7, 19), random_int(0, 59), random_int(0, 59));
$company->transactions()->create([
'reference' => 'TRX-'.str_pad((string) $sequence++, 6, '0', STR_PAD_LEFT),
'amount' => $faker->randomFloat(2, 8_500, 4_500_000),
'currency' => Arr::random(['EUR', 'USD', 'GBP']),
'counterparty' => $faker->company(),
'counterparty_country' => Arr::random(['DE', 'DE', 'CH', 'AT', 'NL', 'GB', 'LU', 'SG']),
'channel' => Arr::random($channels),
'executed_at' => $executedAt,
'risk_score' => $riskScore,
'status' => $status,
'requires_review' => true,
'flagged_by' => Arr::random($analysts),
'flagged_reason' => Arr::random($flaggedReasons),
'signals' => array_values(Arr::random(
$signalCatalog,
random_int(1, min(3, count($signalCatalog)))
)),
]);
}
}
});
}
}
+21 -3
View File
@@ -62,11 +62,29 @@ 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) --------
# -------- Laravel setup --------
if [ -f "$APP_DIR/artisan" ]; then
log "Setting up Laravel application"
# Ensure cache directories exist with proper permissions
mkdir -p "$APP_DIR/storage/framework/cache/data" "$APP_DIR/bootstrap/cache"
chmod -R 775 "$APP_DIR/storage" "$APP_DIR/bootstrap/cache" 2>/dev/null || true
# Remove bootstrap config cache to force reload from .env
rm -f "$APP_DIR/bootstrap/cache/config.php" 2>/dev/null || true
# Run database migrations FIRST (creates cache table if using database driver)
log "Running database migrations"
( cd "$APP_DIR" && php artisan migrate --force 2>&1 || log "migrations failed (continuing)" ) | head -10
# Run database seeders to create initial users
log "Running database seeders"
( cd "$APP_DIR" && php artisan db:seed --force 2>&1 || log "seeders failed (continuing)" ) | head -10
# Clear caches AFTER migrations (now cache table exists)
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)" )
( cd "$APP_DIR" && php artisan config:clear 2>&1 || log "config:clear failed (continuing)" ) | head -5
( cd "$APP_DIR" && php artisan cache:clear 2>&1 || log "cache:clear failed (continuing)" ) | head -5
else
log "No Laravel app at $APP_DIR; skipping artisan"
fi
+74
View File
@@ -0,0 +1,74 @@
version: "3.9"
# Gitea Runner für automatische CI/CD Deployments
# Dieses Compose-File wird auf der Synology NAS deployed
services:
gitea-runner:
image: gitea/act_runner:latest
container_name: gitea-runner
restart: unless-stopped
environment:
# Gitea Instance URL - Passe dies an deine Gitea-Installation an
- GITEA_INSTANCE_URL=${GITEA_URL:-http://gitea:3000}
# Runner Registration Token - Wird aus Gitea UI generiert
- GITEA_RUNNER_REGISTRATION_TOKEN=${RUNNER_TOKEN}
# Runner Name - Wird in Gitea angezeigt
- GITEA_RUNNER_NAME=${RUNNER_NAME:-synology-runner}
# Runner Labels - Definiert welche Workflows dieser Runner ausführen kann
# Format: label:docker://image
- GITEA_RUNNER_LABELS=ubuntu-latest:docker://catthehacker/ubuntu:act-latest,ubuntu-22.04:docker://catthehacker/ubuntu:act-22.04,ubuntu-20.04:docker://catthehacker/ubuntu:act-20.04
# Runner Capacity - Maximale Anzahl gleichzeitiger Jobs
- GITEA_RUNNER_CAPACITY=${RUNNER_CAPACITY:-1}
# Log Level
- GITEA_RUNNER_LOG_LEVEL=${LOG_LEVEL:-info}
volumes:
# Runner Daten (Config, Cache, etc.)
- runner-data:/data
# Docker Socket - Ermöglicht dem Runner Docker-Container zu starten
- /var/run/docker.sock:/var/run/docker.sock
# Optional: Workspace für Job-Artefakte
- runner-workspace:/workspace
networks:
- gitea-network
# Health Check
healthcheck:
test: ["CMD", "pgrep", "-f", "act_runner"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
# Resource Limits (optional, aber empfohlen)
deploy:
resources:
limits:
cpus: '2'
memory: 2G
reservations:
cpus: '1'
memory: 512M
volumes:
runner-data:
driver: local
runner-workspace:
driver: local
networks:
gitea-network:
# Wenn Gitea in einem separaten Netzwerk läuft, verwende:
# external: true
# Ansonsten:
driver: bridge
+91
View File
@@ -0,0 +1,91 @@
version: "3.9"
services:
app:
build:
context: .
dockerfile: Dockerfile
image: laravel-app:latest
container_name: laravel_app
restart: unless-stopped
ports:
- "8080:80" # Angepasst für Synology (Port 81 oft belegt)
environment:
# Laravel Environment
- APP_NAME=${APP_NAME:-Laravel}
- APP_ENV=${APP_ENV:-production}
- APP_KEY=${APP_KEY}
- APP_DEBUG=${APP_DEBUG:-false}
- APP_URL=${APP_URL:-http://localhost:8080}
# Database - PostgreSQL
- DB_CONNECTION2=${DB_CONNECTION2:-pgsql}
- DB_HOST2=${DB_HOST2:-postgres}
- DB_PORT2=${DB_PORT2:-5432}
- DB_DATABASE2=${DB_DATABASE2:-ingest_db}
- DB_USERNAME2=${DB_USERNAME2:-ingest_user}
- DB_PASSWORD2=${DB_PASSWORD2:-ingest_pwd}
# Session & Cache
- SESSION_DRIVER=${SESSION_DRIVER:-file}
- CACHE_STORE=${CACHE_STORE:-file}
- QUEUE_CONNECTION=${QUEUE_CONNECTION:-sync}
# Logging
- LOG_CHANNEL=${LOG_CHANNEL:-stack}
- LOG_LEVEL=${LOG_LEVEL:-info}
# Mail
- MAIL_MAILER=${MAIL_MAILER:-log}
volumes:
- app-storage:/var/www/html/storage
- app-bootstrap-cache:/var/www/html/bootstrap/cache
- ./database:/var/www/html/database
networks:
- laravel-network
depends_on:
postgres:
condition: service_healthy
postgres:
image: postgres:16-alpine
container_name: laravel_postgres
restart: unless-stopped
environment:
- POSTGRES_DB=${DB_DATABASE2:-ingest_db}
- POSTGRES_USER=${DB_USERNAME2:-ingest_user}
- POSTGRES_PASSWORD=${DB_PASSWORD2:-ingest_pwd}
- PGDATA=/var/lib/postgresql/data/pgdata
volumes:
- postgres-data:/var/lib/postgresql/data
ports:
- "5432:5432" # Optional: Direktzugriff auf PostgreSQL
networks:
- laravel-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USERNAME2:-ingest_user}"]
interval: 10s
timeout: 5s
retries: 5
volumes:
postgres-data:
driver: local
app-storage:
driver: local
app-bootstrap-cache:
driver: local
networks:
laravel-network:
driver: bridge
+999
View File
@@ -0,0 +1,999 @@
# API-Dokumentation: Risk Intelligence Platform
## 📋 Inhaltsverzeichnis
1. [Übersicht](#übersicht)
2. [API-Spezifikation](#api-spezifikation)
3. [Authentication](#authentication)
4. [Endpoints](#endpoints)
5. [Datenmodelle](#datenmodelle)
6. [Beispiele](#beispiele)
7. [Error Handling](#error-handling)
8. [Rate Limiting](#rate-limiting)
9. [Changelog](#changelog)
---
## Übersicht
Die **Trusted AI Analyst Data API** stellt Daten für das Analyst Dashboard, Transaction Review, Company Search und Company Drill-Down bereit.
### API-Version
**Version:** 1.0.0
**Format:** JSON
**Authentifizierung:** Session-basiert (Laravel Session Cookie)
### Base URLs
| Environment | URL |
|-------------|-----|
| **Development** | `http://trusted_ai.test/api` |
| **Production** | `https://afc.trai.sft.comstack.de/api` |
### OpenAPI Spezifikation
Die vollständige API-Spezifikation ist in [swagger.yaml](../swagger.yaml) dokumentiert.
**Swagger UI:** Die Spezifikation kann mit [Swagger Editor](https://editor.swagger.io) oder Swagger UI visualisiert werden.
---
## API-Spezifikation
### API-Kategorien
| Tag | Beschreibung | Endpoints |
|-----|--------------|-----------|
| **Dashboard** | KPIs und Alert-Übersichten | 1 Endpoint |
| **Transactions** | Portfolio-weite Transaction Review | 2 Endpoints |
| **Companies** | Company Discovery & Drill-Down | 4 Endpoints |
**Gesamt:** 7 API-Endpoints
---
## Authentication
### Session-basierte Authentifizierung
Die API nutzt **Laravel Session Cookies** für die Authentifizierung.
```yaml
securitySchemes:
sessionCookie:
type: apiKey
in: cookie
name: laravel_session
```
### Login-Flow
1. **Login via Web-Interface:**
```http
POST /login HTTP/1.1
Content-Type: application/x-www-form-urlencoded
email=user@example.com&password=secret
```
2. **Session Cookie erhalten:**
```http
HTTP/1.1 302 Found
Set-Cookie: laravel_session=eyJ....; Path=/; HttpOnly; SameSite=lax
```
3. **API-Request mit Cookie:**
```http
GET /api/dashboard/overview HTTP/1.1
Cookie: laravel_session=eyJ....
```
### Authentifizierungs-Fehler
```json
HTTP/1.1 401 Unauthorized
{
"message": "Unauthenticated."
}
```
**Wichtig:** Alle API-Endpoints erfordern eine gültige Session!
---
## Endpoints
### 1. Dashboard
#### GET /dashboard/overview
Liefert aggregierte KPIs und Alert-Übersichten für das Dashboard.
**Request:**
```http
GET /api/dashboard/overview HTTP/1.1
Cookie: laravel_session=...
```
**Response:** `200 OK`
```json
{
"generatedAt": "2025-01-19T07:30:00Z",
"activeAnalysts": 24,
"totalTransactions": 312,
"totalVolume": 98765432.10,
"alertsToday": 56,
"averageAlertsPerAnalyst": 3,
"precisionRate": 84,
"activeAlerts": {
"count": 128,
"highRiskCount": 19
},
"riskSegments": [
{
"label": "Kritisch (≥80)",
"count": 17,
"volume": 8900000.0
},
{
"label": "Hoch (60-79)",
"count": 42,
"volume": 15200000.0
},
{
"label": "Mittel (40-59)",
"count": 89,
"volume": 32100000.0
}
],
"topCompanies": [
{
"companyId": 24,
"companyName": "Allianz SE",
"ticker": "ALV",
"sector": "Insurance",
"alerts": 6,
"alertVolume": 1450000.0,
"avgRiskScore": 82
}
],
"runbooksExecutedToday": 41
}
```
---
### 2. Transactions
#### GET /transactions
Liste alle Transaktionen mit globalen Filtern (Portfolio-weite Transaction Review).
**Query Parameters:**
| Parameter | Typ | Default | Beschreibung |
|-----------|-----|---------|--------------|
| `search` | string | - | Match gegen Reference, Counterparty, Company |
| `status` | enum | `all` | `all`, `true_positive`, `false_positive`, `cleared` |
| `page` | integer | `1` | Seiten-Nummer (min: 1) |
| `perPage` | integer | `12` | Items pro Seite (min: 1, max: 100) |
**Request:**
```http
GET /api/transactions?status=true_positive&page=1&perPage=12 HTTP/1.1
Cookie: laravel_session=...
```
**Response:** `200 OK`
```json
{
"filters": {
"statusOptions": [
{
"value": "true_positive",
"label": "Bestätigte Treffer"
},
{
"value": "false_positive",
"label": "Fehlalarme"
},
{
"value": "cleared",
"label": "Freigegeben"
}
],
"perPage": 12
},
"metrics": {
"total": {
"count": 312,
"amount": 98765432.10
},
"byStatus": {
"true_positive": {
"count": 42,
"amount": 15200000.0
},
"false_positive": {
"count": 89,
"amount": 32100000.0
},
"cleared": {
"count": 181,
"amount": 51465432.10
}
}
},
"data": [
{
"id": 512,
"company": {
"id": 12,
"name": "Allianz",
"legalName": "Allianz SE",
"ticker": "ALV",
"sector": "Insurance",
"country": "Germany",
"headquarters": "Munich",
"kycRiskLevel": "high",
"summary": "Multinational insurance provider with EMEA focus."
},
"counterparty": "Alpine Holdings Ltd.",
"counterpartyCountry": "Switzerland",
"status": "true_positive",
"statusLabel": "Bestätigter Treffer",
"requiresReview": true,
"riskScore": 87,
"amount": 245000.75,
"currency": "EUR",
"reference": "PAY-2024-10-1942",
"channel": "SWIFT",
"executedAt": "2025-01-18T09:14:00Z",
"flaggedReason": "Counterparty on sanctions watchlist"
}
],
"pagination": {
"page": 1,
"perPage": 12,
"total": 128,
"lastPage": 11
},
"selectedTransactionId": null,
"selectedTransaction": null
}
```
---
#### GET /transactions/{transactionId}
Hole detaillierte Transaction Case File.
**Path Parameters:**
| Parameter | Typ | Beschreibung |
|-----------|-----|--------------|
| `transactionId` | integer | Transaction ID |
**Request:**
```http
GET /api/transactions/512 HTTP/1.1
Cookie: laravel_session=...
```
**Response:** `200 OK`
```json
{
"id": 512,
"company": { ... },
"counterparty": "Alpine Holdings Ltd.",
"status": "true_positive",
"statusLabel": "Bestätigter Treffer",
"requiresReview": true,
"riskScore": 87,
"amount": 245000.75,
"currency": "EUR",
"reference": "PAY-2024-10-1942",
"channel": "SWIFT",
"executedAt": "2025-01-18T09:14:00Z",
"flaggedBy": "Screening Engine",
"flaggedReason": "Counterparty matched to EU sanctions list",
"signals": [
{
"type": "Adverse Media",
"value": "Enforcement action reported in 2024-11",
"weight": 0.8
},
{
"type": "Sanctions",
"value": "EU Sanctions List Match",
"weight": 1.0
}
]
}
```
**Errors:**
- `401 Unauthorized` - Session ungültig
- `404 Not Found` - Transaction existiert nicht
---
### 3. Companies
#### GET /companies/search
Suche Companies und erhalte Screening-Übersicht.
**Query Parameters:**
| Parameter | Typ | Default | Beschreibung |
|-----------|-----|---------|--------------|
| `q` | string | - | Freitext-Suche (Name, Legal Name, Ticker, Sector) |
| `limit` | integer | `6` | Max. Ergebnisse (min: 1, max: 25) |
**Request:**
```http
GET /api/companies/search?q=Allianz&limit=6 HTTP/1.1
Cookie: laravel_session=...
```
**Response:** `200 OK`
```json
{
"overview": {
"totalCompanies": 40,
"openAlerts": 112,
"openAlertVolume": 5640000.0,
"averageRiskScore": 71,
"watchlistHits": 9,
"automationShare": 82
},
"results": [
{
"id": 12,
"name": "Allianz",
"legalName": "Allianz SE",
"ticker": "ALV",
"sector": "Insurance",
"country": "Germany",
"headquarters": "Munich",
"kycRiskLevel": "high",
"summary": "Multinational insurance provider with EMEA focus.",
"alertCount": 7,
"alertVolume": 980000.0,
"alertRiskScore": 79,
"latestAlert": {
"transactionId": 731,
"counterparty": "Baltic Commodities LLC",
"executedAt": "2025-01-17T11:48:00Z",
"channel": "SWIFT",
"flaggedReason": "Pattern matches sanctions typology",
"riskScore": 88
}
}
],
"meta": {
"query": "Allianz",
"resultCount": 6,
"limit": 6
}
}
```
---
#### GET /companies/{companyId}
Hole Company-Profil.
**Path Parameters:**
| Parameter | Typ | Beschreibung |
|-----------|-----|--------------|
| `companyId` | integer | Company ID |
**Request:**
```http
GET /api/companies/12 HTTP/1.1
Cookie: laravel_session=...
```
**Response:** `200 OK`
```json
{
"id": 12,
"name": "Allianz",
"legalName": "Allianz SE",
"ticker": "ALV",
"sector": "Insurance",
"country": "Germany",
"headquarters": "Munich",
"kycRiskLevel": "high",
"summary": "Multinational insurance provider with EMEA focus.",
"createdAt": "2024-10-15T08:30:00Z",
"updatedAt": "2025-01-18T14:22:00Z"
}
```
**Errors:**
- `401 Unauthorized` - Session ungültig
- `404 Not Found` - Company existiert nicht
---
#### GET /companies/{companyId}/transactions
Liste alle Transactions für eine Company.
**Path Parameters:**
| Parameter | Typ | Beschreibung |
|-----------|-----|--------------|
| `companyId` | integer | Company ID |
**Query Parameters:**
| Parameter | Typ | Default | Beschreibung |
|-----------|-----|---------|--------------|
| `status` | enum | `all` | `all`, `true_positive`, `false_positive`, `cleared` |
| `channel` | string | `all` | Channel-Filter (z.B. `SWIFT`, `SEPA`) |
| `page` | integer | `1` | Seiten-Nummer |
| `perPage` | integer | `10` | Items pro Seite (min: 1, max: 100) |
**Request:**
```http
GET /api/companies/12/transactions?status=true_positive&page=1&perPage=10 HTTP/1.1
Cookie: laravel_session=...
```
**Response:** `200 OK`
```json
{
"company": {
"id": 12,
"name": "Allianz",
"legalName": "Allianz SE",
"ticker": "ALV",
"sector": "Insurance",
"country": "Germany",
"headquarters": "Munich",
"kycRiskLevel": "high",
"summary": "Multinational insurance provider..."
},
"filters": {
"statusOptions": [
{
"value": "true_positive",
"label": "Bestätigte Treffer"
}
],
"channelOptions": [
{
"value": "SWIFT",
"label": "SWIFT"
},
{
"value": "SEPA",
"label": "SEPA"
}
]
},
"metrics": {
"totalCount": 96,
"totalVolume": 7200000.0,
"openAlerts": {
"count": 12,
"amount": 1850000.0
},
"highRiskShare": 28,
"last30Days": {
"count": 15,
"amount": 980000.0
},
"byStatus": {
"true_positive": {
"count": 12,
"amount": 1850000.0
},
"false_positive": {
"count": 24,
"amount": 2100000.0
},
"cleared": {
"count": 60,
"amount": 3250000.0
}
}
},
"data": [
{
"id": 877,
"company": { ... },
"counterparty": "Alpine Holdings Ltd.",
"status": "true_positive",
"riskScore": 87,
"amount": 245000.75,
"currency": "EUR",
"reference": "PAY-2024-10-1942",
"channel": "SWIFT",
"executedAt": "2025-01-18T09:14:00Z"
}
],
"pagination": {
"page": 1,
"perPage": 10,
"total": 12,
"lastPage": 2
},
"selectedTransactionId": null,
"selectedTransaction": null,
"recentAlerts": [
{
"id": 911,
"counterparty": "Northbridge Trading Ltd.",
"executedAt": "2025-01-18T14:52:00Z",
"channel": "SWIFT",
"status": "true_positive",
"statusLabel": "Bestätigter Treffer",
"riskScore": 92
}
]
}
```
**Errors:**
- `401 Unauthorized` - Session ungültig
- `404 Not Found` - Company nicht gefunden oder keine Transactions
---
#### GET /companies/{companyId}/transactions/{transactionId}
Hole Transaction Case File mit Company-Context und History.
**Path Parameters:**
| Parameter | Typ | Beschreibung |
|-----------|-----|--------------|
| `companyId` | integer | Company ID |
| `transactionId` | integer | Transaction ID |
**Request:**
```http
GET /api/companies/12/transactions/877 HTTP/1.1
Cookie: laravel_session=...
```
**Response:** `200 OK`
```json
{
"id": 877,
"company": { ... },
"counterparty": "Alpine Holdings Ltd.",
"status": "true_positive",
"statusLabel": "Bestätigter Treffer",
"requiresReview": true,
"riskScore": 87,
"amount": 245000.75,
"currency": "EUR",
"reference": "PAY-2024-10-1942",
"channel": "SWIFT",
"executedAt": "2025-01-18T09:14:00Z",
"flaggedBy": "Screening Engine",
"flaggedReason": "Counterparty matched to EU sanctions list",
"signals": [
{
"type": "Adverse Media",
"value": "Enforcement action reported in 2024-11",
"weight": 0.8
}
],
"recommendedActions": [
{
"title": "Verdachtsmeldung vorbereiten",
"description": "Erstellen Sie den Meldeentwurf für die FIU und sichern Sie Belege."
},
{
"title": "Enhanced Due Diligence",
"description": "Prüfen Sie zusätzliche Informationsquellen zur Counterparty."
}
],
"counterpartyHistory": [
{
"id": 765,
"executedAt": "2024-12-22T15:37:00Z",
"channel": "SWIFT",
"reference": "PAY-2024-12-1187",
"amount": 99000.0,
"currency": "EUR",
"riskScore": 74
}
]
}
```
**Errors:**
- `401 Unauthorized` - Session ungültig
- `404 Not Found` - Transaction nicht gefunden für diese Company
---
## Datenmodelle
### Transaction Status
```typescript
enum TransactionStatus {
TRUE_POSITIVE = 'true_positive', // Kritisches Risiko
FALSE_POSITIVE = 'false_positive', // Hohes Risiko
CLEARED = 'cleared' // Geringes Risiko
}
```
### KYC Risk Level
```typescript
enum KycRiskLevel {
LOW = 'low',
HIGH = 'high',
CRITICAL = 'critical'
}
```
### Pagination Meta
```json
{
"page": 1,
"perPage": 12,
"total": 128,
"lastPage": 11
}
```
### Count Amount Summary
```json
{
"count": 42,
"amount": 2750000.50
}
```
### Transaction Signal
```json
{
"type": "Adverse Media",
"value": "Enforcement action reported in 2024-11",
"weight": 0.8
}
```
### Recommended Action
```json
{
"title": "Verdachtsmeldung vorbereiten",
"description": "Erstellen Sie den Meldeentwurf für die FIU..."
}
```
---
## Beispiele
### cURL-Beispiele
#### Login & Session Cookie erhalten
```bash
# Login
curl -c cookies.txt -X POST http://trusted_ai.test/login \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "email=user@example.com&password=secret"
# Session Cookie ist jetzt in cookies.txt gespeichert
```
#### Dashboard Overview abrufen
```bash
curl -b cookies.txt http://trusted_ai.test/api/dashboard/overview
```
#### Transactions filtern
```bash
curl -b cookies.txt "http://trusted_ai.test/api/transactions?status=true_positive&page=1&perPage=12"
```
#### Company suchen
```bash
curl -b cookies.txt "http://trusted_ai.test/api/companies/search?q=Allianz&limit=6"
```
#### Company Transactions abrufen
```bash
curl -b cookies.txt "http://trusted_ai.test/api/companies/12/transactions?status=all&page=1"
```
---
### JavaScript (Fetch API)
```javascript
// Session Cookie wird automatisch vom Browser gesendet
// Dashboard Overview
const overview = await fetch('/api/dashboard/overview', {
credentials: 'include' // Wichtig: Session Cookie mitsenden
}).then(r => r.json());
console.log(overview);
// Transactions mit Filter
const transactions = await fetch('/api/transactions?status=true_positive&page=1&perPage=12', {
credentials: 'include'
}).then(r => r.json());
console.log(transactions.data);
// Company Search
const companies = await fetch('/api/companies/search?q=Allianz', {
credentials: 'include'
}).then(r => r.json());
console.log(companies.results);
```
---
### TypeScript Types
```typescript
// Dashboard Overview Response
interface DashboardOverview {
generatedAt: string;
activeAnalysts: number;
totalTransactions: number;
totalVolume: number;
alertsToday: number;
averageAlertsPerAnalyst: number;
precisionRate: number;
activeAlerts: {
count: number;
highRiskCount: number;
};
riskSegments: RiskSegment[];
topCompanies: TopCompanyAlert[];
runbooksExecutedToday: number;
}
// Transaction Collection Response
interface TransactionCollectionResponse {
filters: {
statusOptions: StatusOption[];
perPage: number;
};
metrics: TransactionMetrics;
data: TransactionPreview[];
pagination: PaginationMeta;
selectedTransactionId: number | null;
selectedTransaction: TransactionDetail | null;
}
// Company Search Response
interface CompanySearchResponse {
overview: CompanySearchOverview;
results: CompanySearchResult[];
meta: {
query: string | null;
resultCount: number;
limit: number;
};
}
```
---
## Error Handling
### Standard Error Response
```json
{
"message": "Error message",
"errors": {
"field": ["Validation error"]
}
}
```
### HTTP Status Codes
| Code | Bedeutung | Beschreibung |
|------|-----------|--------------|
| `200` | OK | Request erfolgreich |
| `401` | Unauthorized | Session ungültig oder abgelaufen |
| `404` | Not Found | Resource nicht gefunden |
| `422` | Unprocessable Entity | Validierungsfehler |
| `500` | Internal Server Error | Server-Fehler |
### Beispiel: 401 Unauthorized
```json
HTTP/1.1 401 Unauthorized
Content-Type: application/json
{
"message": "Unauthenticated."
}
```
**Aktion:** Redirect zu `/login`
### Beispiel: 404 Not Found
```json
HTTP/1.1 404 Not Found
Content-Type: application/json
{
"message": "Company not found."
}
```
### Beispiel: 422 Validation Error
```json
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json
{
"message": "The given data was invalid.",
"errors": {
"status": ["The selected status is invalid."],
"page": ["The page must be at least 1."]
}
}
```
---
## Rate Limiting
### Aktueller Status
**Aktuell:** Kein Rate Limiting implementiert.
### Geplant (Future)
```
Rate Limit: 60 Requests pro Minute pro User
Header: X-RateLimit-Limit, X-RateLimit-Remaining
```
**Response bei Überschreitung:**
```json
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 60
{
"message": "Too many requests. Please try again later."
}
```
---
## Changelog
### Version 1.0.0 (2025-01-19)
**Initial Release**
**Endpoints:**
- ✅ `GET /dashboard/overview` - Dashboard KPIs
- ✅ `GET /transactions` - Portfolio-wide Transactions
- ✅ `GET /transactions/{id}` - Transaction Detail
- ✅ `GET /companies/search` - Company Search
- ✅ `GET /companies/{id}` - Company Profile
- ✅ `GET /companies/{id}/transactions` - Company Transactions
- ✅ `GET /companies/{id}/transactions/{id}` - Company Transaction Detail
**Features:**
- Session-basierte Authentifizierung
- Pagination Support
- Filter & Search
- Metrics & Aggregationen
- OpenAPI 3.1 Spezifikation
---
## Implementierungs-Status
### ⚠️ Wichtig: API noch nicht implementiert!
Die in diesem Dokument beschriebenen Endpoints sind **aktuell noch nicht implementiert**.
**Status:**
- ✅ OpenAPI Spezifikation vorhanden ([swagger.yaml](../swagger.yaml))
- ❌ API Routes noch nicht definiert (`routes/api.php` ist leer)
- ❌ API Controllers noch nicht erstellt
- ❌ API Responses noch nicht implementiert
### Nächste Schritte zur Implementierung
1. **API Routes definieren:**
```php
// routes/api.php
Route::middleware(['auth:sanctum'])->group(function () {
Route::get('/dashboard/overview', [DashboardController::class, 'overview']);
Route::get('/transactions', [TransactionController::class, 'index']);
Route::get('/transactions/{transaction}', [TransactionController::class, 'show']);
Route::get('/companies/search', [CompanyController::class, 'search']);
Route::get('/companies/{company}', [CompanyController::class, 'show']);
Route::get('/companies/{company}/transactions', [CompanyController::class, 'transactions']);
});
```
2. **API Controllers erstellen:**
```bash
php artisan make:controller Api/DashboardController
php artisan make:controller Api/TransactionController
php artisan make:controller Api/CompanyController
```
3. **API Resources erstellen:**
```bash
php artisan make:resource TransactionResource
php artisan make:resource CompanyResource
```
4. **Tests schreiben:**
```bash
php artisan make:test Api/DashboardTest
php artisan make:test Api/TransactionTest
php artisan make:test Api/CompanyTest
```
---
## Zusammenfassung
### API-Übersicht
| Kategorie | Endpoints | Status |
|-----------|-----------|--------|
| Dashboard | 1 | ⚠️ Spezifiziert, nicht implementiert |
| Transactions | 2 | ⚠️ Spezifiziert, nicht implementiert |
| Companies | 4 | ⚠️ Spezifiziert, nicht implementiert |
| **Gesamt** | **7** | - |
### Datenmodelle
- **DashboardOverview** - Dashboard-KPIs
- **TransactionCollectionResponse** - Paginierte Transactions
- **TransactionDetail** - Detaillierte Transaction
- **CompanySearchResponse** - Company-Suchergebnisse
- **CompanyDetail** - Company-Profil
- **CompanyTransactionsResponse** - Company-spezifische Transactions
### Authentifizierung
**Session-basiert** via Laravel Session Cookie
- Login erforderlich
- Cookie-basierte Authentifizierung
- CSRF-Protection
---
**Erstellt:** 2025-11-24
**Version:** 1.0.0
**Autor:** Risk Intelligence Platform Team
**OpenAPI Spec:** [swagger.yaml](../swagger.yaml)
File diff suppressed because it is too large Load Diff
+401
View File
@@ -0,0 +1,401 @@
# Backend Sync & Transform Commands
Dokumentation für die manuellen Artisan Commands zum Synchronisieren und Transformieren von Backend-Daten.
## Übersicht
Das System verfügt über 3 Haupt-Commands zur Datensynchronisation:
1. **`backend:sync-data-pool`** - Synchronisiert Daten vom Backend-Schema in den Data Pool
2. **`backend:transform-data-pool`** - Transformiert Data Pool in Companies + Transactions
3. **`backend:rebuild`** - Kompletter Rebuild (Sync + Transform in einem Schritt)
---
## 1. Backend Sync Command
Synchronisiert Daten von `backend.transactions` + `backend.transaction_outputs` nach `public.backend_data_pool`.
### Verwendung
#### Incremental Sync (Standard)
Nur neue oder geänderte Datensätze seit dem letzten Sync:
```bash
php artisan backend:sync-data-pool --incremental
```
#### Full Sync
Kompletter Rebuild - löscht alle Daten im Data Pool und baut sie neu auf:
```bash
php artisan backend:sync-data-pool --full
```
#### Statistiken anzeigen
Zeigt nur Statistiken an, ohne Sync durchzuführen:
```bash
php artisan backend:sync-data-pool --stats
```
#### Im Hintergrund über Queue
Führt den Sync asynchron über die Queue aus:
```bash
php artisan backend:sync-data-pool --full --queue
```
#### Batch-Größe anpassen
Definiert, wie viele Records pro Batch verarbeitet werden (Standard: 1000):
```bash
php artisan backend:sync-data-pool --full --batch-size=2000
```
### Optionen
| Option | Beschreibung | Default |
|--------|--------------|---------|
| `--full` | Kompletter Sync (truncate + rebuild) | false |
| `--incremental` | Nur neue/geänderte Records | false |
| `--batch-size` | Records pro Batch | 1000 |
| `--queue` | Im Hintergrund ausführen | false |
| `--stats` | Nur Statistiken anzeigen | false |
### Datenbankverbindungen
- **Source:** `DB::connection('pgsql')``backend.transactions`, `backend.transaction_outputs`
- **Target:** `DB::table('backend_data_pool')``public.backend_data_pool`
### Was passiert beim Sync?
1. **Full Sync:**
- Truncate `backend_data_pool`
- Alle `done` Transactions vom Backend laden
- Mit `transaction_outputs` joinen
- In Batches in Data Pool schreiben
2. **Incremental Sync:**
- Letzten Sync-Zeitstempel ermitteln
- Nur Records mit `last_modified_at > last_synced_at` laden
- In Batches in Data Pool schreiben
---
## 2. Transform Command
Transformiert Daten aus `backend_data_pool` in die Production-Tables `companies` und `transactions`.
### Verwendung
#### Standard Transformation
```bash
php artisan backend:transform-data-pool
```
#### Statistiken anzeigen
```bash
php artisan backend:transform-data-pool --stats
```
#### Im Hintergrund über Queue
```bash
php artisan backend:transform-data-pool --queue
```
#### Batch-Größe anpassen
```bash
php artisan backend:transform-data-pool --batch-size=200
```
### Optionen
| Option | Beschreibung | Default |
|--------|--------------|---------|
| `--batch-size` | Records pro Batch | 100 |
| `--queue` | Im Hintergrund ausführen | false |
| `--stats` | Nur Statistiken anzeigen | false |
### Datenbankverbindungen
- **Source:** `DB::table('backend_data_pool')``public.backend_data_pool`
- **Target:** Eloquent Models → `public.companies`, `public.transactions`
### Was passiert bei der Transformation?
1. Alle eindeutigen `transaction_id` aus Data Pool laden
2. Für jede Transaction:
- **Company erstellen/aktualisieren:**
- Name: `corporate_entity`
- Country, Sector, HQ, Summary, Legal Name extrahieren
- KYC Risk Level aus `risk_score` berechnen
- **Transaction erstellen/aktualisieren:**
- Reference, Amount, Currency, Counterparty
- Risk Score & Level mapping
- Status basierend auf Risk Level
- Alle `output_keys` als JSON-Felder speichern
### Risk Score Mapping
Backend verwendet eine Skala von **-100 bis 100**:
| Backend Score | Risk Level | Transaction Status |
|---------------|------------|-------------------|
| -100 bis 20 | `critical` | True Positive |
| 20 bis 80 | `high` | False Positive |
| 80 bis 100 | `low` | Cleared |
Für das Frontend wird der Score auf **0-255** normalisiert:
```
normalized = ((score + 100) / 200) * 255
```
---
## 3. Complete Rebuild Command
Führt Full Sync + Transformation in einem Schritt aus.
### Verwendung
#### Standard Rebuild
```bash
php artisan backend:rebuild
```
#### Mit angepassten Batch-Größen
```bash
php artisan backend:rebuild --batch-size=2000 --transform-batch-size=150
```
#### Im Hintergrund über Queue
```bash
php artisan backend:rebuild --queue
```
**Hinweis:** Bei `--queue` wird nur der Sync-Job in die Queue geschickt. Die Transformation muss manuell gestartet werden, nachdem der Sync abgeschlossen ist.
### Optionen
| Option | Beschreibung | Default |
|--------|--------------|---------|
| `--batch-size` | Records pro Batch (Sync) | 1000 |
| `--transform-batch-size` | Records pro Batch (Transform) | 100 |
| `--queue` | Im Hintergrund ausführen | false |
### Ablauf
1. **Step 1/2:** Full Sync from Backend
- Truncate `backend_data_pool`
- Sync alle `done` Transactions
2. **Step 2/2:** Transform Data Pool
- Companies erstellen/aktualisieren
- Transactions erstellen/aktualisieren
---
## Automatisierung mit Laravel Scheduler
Die Commands sind bereits im Laravel Scheduler konfiguriert ([routes/console.php](routes/console.php)):
### Aktuelle Schedule
```php
// Incremental Sync: Alle 6 Stunden (0:00, 6:00, 12:00, 18:00)
Schedule::job(new SyncBackendDataPool(fullSync: false, batchSize: 1000))
->everySixHours()
->name('sync-backend-data-pool-incremental')
->withoutOverlapping(1800);
// Full Sync: Jeden Sonntag um 3:00 Uhr
Schedule::job(new SyncBackendDataPool(fullSync: true, batchSize: 1000))
->weeklyOn(0, '03:00')
->name('sync-backend-data-pool-full')
->withoutOverlapping(3600);
// Transformation: 30 Minuten nach jedem Sync (0:30, 6:30, 12:30, 18:30)
Schedule::job(new TransformDataPoolToProduction(batchSize: 100))
->cron('30 */6 * * *')
->name('transform-data-pool-to-production')
->withoutOverlapping(1800);
```
### Scheduler aktivieren
#### Für lokale Entwicklung
```bash
php artisan schedule:work
```
#### Für Produktion (Crontab)
```bash
crontab -e
```
Folgende Zeile hinzufügen:
```
* * * * * cd /Users/sebastianfrohlich/Herd/frontend && php artisan schedule:run >> /dev/null 2>&1
```
### Scheduled Tasks anzeigen
```bash
php artisan schedule:list
```
### Scheduled Task testen
```bash
php artisan schedule:test sync-backend-data-pool-incremental
php artisan schedule:test sync-backend-data-pool-full
php artisan schedule:test transform-data-pool-to-production
```
---
## Features
### Alle Commands bieten
**Interaktive Bestätigung** - Sicherheitsfrage vor kritischen Operationen
**Statistiken vorher/nachher** - Direkter Vergleich der Datenänderungen
**Batch-Processing** - Performante Verarbeitung großer Datenmengen
**Queue-Support** - Ausführung im Hintergrund möglich
**Laravel Prompts** - Moderne, benutzerfreundliche CLI-Oberfläche
**Error Handling** - Robuste Fehlerbehandlung mit Logging
**Timeout Protection** - 1 Stunde Timeout pro Job
**Retry Logic** - 3 Versuche bei Fehlern
**Overlap Prevention** - Verhindert gleichzeitige Ausführung
---
## Datenfluss
```
┌──────────────────────┐
│ backend.transactions │
│ backend.transaction_│
│ outputs │
└──────────┬───────────┘
│ SyncBackendDataPool
│ (backend:sync-data-pool)
┌──────────────────────┐
│ public.backend_data_ │
│ pool │
└──────────┬───────────┘
│ TransformDataPoolToProduction
│ (backend:transform-data-pool)
┌──────────────────────┐
│ public.companies │
│ public.transactions │
└──────────────────────┘
```
---
## Troubleshooting
### Command funktioniert nicht
```bash
# Commands neu registrieren
php artisan optimize:clear
# Artisan Cache leeren
php artisan cache:clear
```
### Queue-Jobs laufen nicht
```bash
# Queue Worker starten
php artisan queue:work
# Queue Status prüfen
php artisan queue:monitor
```
### Logs prüfen
```bash
# Application Logs
tail -f storage/logs/laravel.log
# Nur Sync-Logs
tail -f storage/logs/laravel.log | grep SyncBackendDataPool
# Nur Transform-Logs
tail -f storage/logs/laravel.log | grep TransformDataPoolToProduction
```
---
## Beispiele
### Kompletter Rebuild mit Custom Settings
```bash
php artisan backend:rebuild \
--batch-size=5000 \
--transform-batch-size=250
```
### Nur neuen Daten synchronisieren und transformieren
```bash
php artisan backend:sync-data-pool --incremental
php artisan backend:transform-data-pool
```
### Statistiken aller Systeme anzeigen
```bash
php artisan backend:sync-data-pool --stats
php artisan backend:transform-data-pool --stats
```
---
## Performance-Tipps
1. **Batch-Größe anpassen:**
- Kleine Batches (100-500): Weniger RAM, langsamer
- Große Batches (2000-5000): Mehr RAM, schneller
2. **Queue nutzen für lange Jobs:**
```bash
php artisan backend:sync-data-pool --full --queue
```
3. **Incremental Sync bevorzugen:**
- Full Sync nur wenn wirklich nötig
- Incremental ist viel schneller
4. **Off-Peak Zeiten nutzen:**
- Full Sync: Sonntag 3:00 Uhr (bereits konfiguriert)
- Incremental: Alle 6 Stunden (bereits konfiguriert)
---
## Weiterführende Links
- [routes/console.php](routes/console.php) - Scheduler Configuration
- [app/Jobs/SyncBackendDataPool.php](app/Jobs/SyncBackendDataPool.php) - Sync Job
- [app/Jobs/TransformDataPoolToProduction.php](app/Jobs/TransformDataPoolToProduction.php) - Transform Job
- [app/Console/Commands/SyncBackendDataPoolCommand.php](app/Console/Commands/SyncBackendDataPoolCommand.php)
- [app/Console/Commands/TransformDataPoolCommand.php](app/Console/Commands/TransformDataPoolCommand.php)
- [app/Console/Commands/RebuildFromDataPoolCommand.php](app/Console/Commands/RebuildFromDataPoolCommand.php)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+991
View File
@@ -0,0 +1,991 @@
# Entwickler-Setup-Guide: Risk Intelligence Platform
## 📋 Inhaltsverzeichnis
1. [Voraussetzungen](#voraussetzungen)
2. [Schnellstart](#schnellstart)
3. [Detaillierte Installation](#detaillierte-installation)
4. [Datenbank-Setup](#datenbank-setup)
5. [Environment-Konfiguration](#environment-konfiguration)
6. [Frontend-Assets](#frontend-assets)
7. [Erste Schritte](#erste-schritte)
8. [Development-Workflow](#development-workflow)
9. [Troubleshooting](#troubleshooting)
10. [Nützliche Commands](#nützliche-commands)
---
## Voraussetzungen
### Erforderliche Software
| Software | Mindestversion | Empfohlen | Installation |
|----------|---------------|-----------|--------------|
| **PHP** | 8.2 | 8.4.14+ | [Laravel Herd](https://herd.laravel.com) |
| **Composer** | 2.0 | Latest | Via Herd oder [getcomposer.org](https://getcomposer.org) |
| **Node.js** | 18.x | 20.x+ | [nodejs.org](https://nodejs.org) |
| **npm** | 9.x | Latest | Mit Node.js installiert |
| **PostgreSQL** | 14.x | 16.x+ | [postgresql.org](https://www.postgresql.org) |
| **Git** | 2.x | Latest | [git-scm.com](https://git-scm.com) |
### Empfohlene Tools
- **Laravel Herd** - Lokale PHP-Entwicklungsumgebung (macOS)
- **TablePlus** / **pgAdmin** - PostgreSQL GUI
- **VS Code** - Editor mit PHP/Laravel Extensions
- **Postman** / **Insomnia** - API Testing (optional)
### PHP Extensions
Folgende PHP Extensions werden benötigt:
```bash
# Prüfe installierte Extensions
php -m
# Benötigte Extensions:
- pdo_pgsql # PostgreSQL Driver
- mbstring # Multibyte String
- xml # XML Support
- curl # HTTP Client
- bcmath # Precision Math
- fileinfo # File Information
- json # JSON Support
- openssl # Encryption
- tokenizer # PHP Tokenizer
- zip # ZIP Archive
```
---
## Schnellstart
Für erfahrene Entwickler - vollständige Installation in 5 Minuten:
```bash
# 1. Repository klonen
git clone <repository-url> risk-platform
cd risk-platform
# 2. Dependencies installieren & Setup ausführen
composer setup
# 3. PostgreSQL-Datenbank erstellen (siehe unten)
# Anpassen: .env mit DB-Credentials
# 4. Migrations ausführen
php artisan migrate --seed
# 5. Development-Server starten
composer run dev
```
**Fertig!** Die Applikation läuft auf [http://localhost:8000](http://localhost:8000)
---
## Detaillierte Installation
### Schritt 1: Repository klonen
```bash
# Via HTTPS
git clone https://github.com/your-org/risk-platform.git
cd risk-platform
# Oder via SSH
git clone git@github.com:your-org/risk-platform.git
cd risk-platform
```
### Schritt 2: PHP Dependencies installieren
```bash
composer install
```
**Was passiert dabei?**
- Installiert alle Laravel-Pakete
- Installiert Livewire, Flux UI, Fortify
- Installiert Dev-Dependencies (Pest, Pint, Boost)
- Generiert Autoload-Files
### Schritt 3: Environment-Datei erstellen
```bash
# .env.example nach .env kopieren
cp .env.example .env
# Application Key generieren
php artisan key:generate
```
### Schritt 4: Node Dependencies installieren
```bash
npm install
```
**Was passiert dabei?**
- Installiert Vite
- Installiert Tailwind CSS v4
- Installiert Build-Tools
---
## Datenbank-Setup
### PostgreSQL Installation
#### macOS (mit Homebrew)
```bash
# PostgreSQL installieren
brew install postgresql@16
# Als Service starten
brew services start postgresql@16
# Verbindung testen
psql postgres
```
#### macOS (mit Postgres.app)
1. Download von [postgresapp.com](https://postgresapp.com)
2. App starten
3. Default Server initialisieren
#### Ubuntu/Debian
```bash
sudo apt update
sudo apt install postgresql postgresql-contrib
sudo systemctl start postgresql
sudo systemctl enable postgresql
```
### Datenbank erstellen
```bash
# Als postgres User einloggen
psql postgres
# Oder direkt mit spezifischem User
sudo -u postgres psql
```
```sql
-- Datenbank erstellen
CREATE DATABASE risk_platform_db;
-- User erstellen (optional)
CREATE USER risk_user WITH ENCRYPTED PASSWORD 'your_secure_password';
-- Rechte vergeben
GRANT ALL PRIVILEGES ON DATABASE risk_platform_db TO risk_user;
-- In PostgreSQL 15+ auch Schema-Rechte vergeben
\c risk_platform_db
GRANT ALL ON SCHEMA public TO risk_user;
-- Exit
\q
```
### Schemas erstellen
```sql
-- Verbindung zur DB
psql -U risk_user -d risk_platform_db
-- PUBLIC Schema (wird automatisch erstellt)
-- BACKEND Schema erstellen
CREATE SCHEMA IF NOT EXISTS backend;
-- Rechte für BACKEND Schema
GRANT ALL ON SCHEMA backend TO risk_user;
-- Exit
\q
```
### Verbindung testen
```bash
# Direkter Connect
psql -h 127.0.0.1 -p 5432 -U risk_user -d risk_platform_db
# Mit Laravel Tinker
php artisan tinker
>>> DB::connection('pgsql_second')->select('SELECT version()');
>>> DB::connection('backend')->select('SELECT current_schema()');
```
---
## Environment-Konfiguration
### .env Datei anpassen
```bash
# .env öffnen und anpassen
nano .env
```
#### Grundkonfiguration
```ini
APP_NAME="Risk Intelligence Platform"
APP_ENV=local
APP_KEY=base64:xxx # Wird von key:generate erstellt
APP_DEBUG=true
APP_URL=http://localhost:8000
APP_LOCALE=de
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=de_DE
```
#### Datenbank-Konfiguration (Wichtig!)
```ini
# Default Connection (PUBLIC Schema)
DB_CONNECTION=pgsql
DB_CONNECTION2=pgsql
DB_HOST2=127.0.0.1
DB_PORT2=5432
DB_DATABASE2=risk_platform_db
DB_USERNAME2=risk_user
DB_PASSWORD2=your_secure_password
```
**Wichtig:** Die Applikation nutzt `DB_*2` Variablen für PostgreSQL!
#### Cache & Session
```ini
CACHE_STORE=database
SESSION_DRIVER=database
QUEUE_CONNECTION=database
```
**Warum database?**
- Einfaches Setup ohne Redis
- Funktioniert out-of-the-box
- Für Produktion: Wechsel zu Redis empfohlen
#### Mail-Konfiguration (Development)
```ini
MAIL_MAILER=log
MAIL_FROM_ADDRESS="noreply@risk-platform.test"
MAIL_FROM_NAME="${APP_NAME}"
```
Mails werden in `storage/logs/laravel.log` gespeichert.
#### Optional: Redis (für bessere Performance)
```ini
CACHE_STORE=redis
SESSION_DRIVER=redis
QUEUE_CONNECTION=redis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
```
### Vollständige .env Vorlage
<details>
<summary>Klicken für vollständige .env Beispiel-Datei</summary>
```ini
APP_NAME="Risk Intelligence Platform"
APP_ENV=local
APP_KEY=base64:xxx
APP_DEBUG=true
APP_URL=http://localhost:8000
APP_LOCALE=de
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=de_DE
APP_MAINTENANCE_DRIVER=file
PHP_CLI_SERVER_WORKERS=4
BCRYPT_ROUNDS=12
LOG_CHANNEL=stack
LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
# PostgreSQL Configuration
DB_CONNECTION=pgsql
DB_CONNECTION2=pgsql
DB_HOST2=127.0.0.1
DB_PORT2=5432
DB_DATABASE2=risk_platform_db
DB_USERNAME2=risk_user
DB_PASSWORD2=your_secure_password
# Session & Cache
SESSION_DRIVER=database
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null
CACHE_STORE=database
QUEUE_CONNECTION=database
# Broadcasting & Filesystem
BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local
# Mail
MAIL_MAILER=log
MAIL_FROM_ADDRESS="noreply@risk-platform.test"
MAIL_FROM_NAME="${APP_NAME}"
# Vite
VITE_APP_NAME="${APP_NAME}"
```
</details>
---
## Frontend-Assets
### Assets kompilieren
```bash
# Development Build (einmalig)
npm run build
# Development mit Hot Reload
npm run dev
```
### Was macht `npm run dev`?
```
┌─────────────────────────────────────────────────┐
│ VITE v7.0.4 ready in X ms │
│ │
│ ➜ Local: http://localhost:5173/ │
│ ➜ Network: use --host to expose │
│ │
│ ➜ Laravel: http://localhost:8000 │
└─────────────────────────────────────────────────┘
✓ compiled successfully
```
- Startet Vite Dev-Server auf Port 5173
- Hot Module Replacement (HMR) aktiviert
- Tailwind CSS v4 wird kompiliert
- Livewire-Assets werden gebundled
### Production Build
```bash
npm run build
# Output:
# dist/assets/app-xxxxx.js
# dist/assets/app-xxxxx.css
```
---
## Erste Schritte
### 1. Migrations ausführen
```bash
# Alle Migrations ausführen
php artisan migrate
# Mit Seeding (Test-Daten)
php artisan migrate --seed
# Migration-Status prüfen
php artisan migrate:status
```
**Erwartete Tabellen nach Migration:**
```
┌──────────────────────────────────┬────────┐
│ Migration │ Ran? │
├──────────────────────────────────┼────────┤
│ 0001_01_01_000000_create_users │ Yes │
│ 0001_01_01_000001_create_cache │ Yes │
│ 0001_01_01_000002_create_jobs │ Yes │
│ 2025_09_02_075243_add_two_factor │ Yes │
│ 2025_10_20_181750_create_companies │ Yes │
│ 2025_10_20_181753_create_transactions │ Yes │
│ 2025_11_16_140224_add_output_columns │ Yes │
│ 2025_11_16_150000_create_backend_data_pool │ Yes │
│ ... │ │
└──────────────────────────────────┴────────┘
```
### 2. Test-User erstellen
```bash
php artisan tinker
```
```php
// Test-User erstellen
$user = App\Models\User::create([
'name' => 'Admin User',
'email' => 'admin@example.com',
'password' => bcrypt('password'),
'email_verified_at' => now(),
]);
// Oder mit Factory
App\Models\User::factory()->create([
'email' => 'test@example.com',
]);
exit
```
### 3. Development-Server starten
#### Option A: Composer Dev Command (Empfohlen)
```bash
composer run dev
```
**Startet parallel:**
- `php artisan serve` → http://localhost:8000
- `php artisan queue:listen` → Queue Worker
- `php artisan pail` → Live Logs
- `npm run dev` → Vite Dev Server
#### Option B: Einzelne Services manuell
```bash
# Terminal 1: Laravel Server
php artisan serve
# Terminal 2: Vite Dev Server
npm run dev
# Terminal 3 (optional): Queue Worker
php artisan queue:work
# Terminal 4 (optional): Logs
php artisan pail
```
#### Option C: Laravel Herd (macOS)
Wenn du Laravel Herd nutzt:
1. Projekt in Herd-Verzeichnis verschieben
2. Automatische Domain: `risk-platform.test`
3. HTTPS automatisch aktiviert
4. Kein `php artisan serve` notwendig
```bash
# In Herd-Verzeichnis verschieben
mv ~/risk-platform ~/Herd/risk-platform
# Browser öffnen
open https://risk-platform.test
```
### 4. Applikation öffnen
Browser öffnen: [http://localhost:8000](http://localhost:8000)
**Login-Credentials:**
- Email: `test@example.com`
- Password: `password`
---
## Development-Workflow
### Typischer Entwicklungstag
```bash
# 1. Morgen: Repository aktualisieren
git pull origin main
# 2. Dependencies aktualisieren (falls composer.lock geändert)
composer install
npm install
# 3. Neue Migrations (falls vorhanden)
php artisan migrate
# 4. Dev-Server starten
composer run dev
# 5. Am Ende: Code formatieren
vendor/bin/pint
# 6. Tests ausführen
php artisan test
# 7. Commit & Push
git add .
git commit -m "feat: neue feature"
git push
```
### Code-Formatierung
```bash
# Alle Dateien formatieren
vendor/bin/pint
# Nur geänderte Dateien
vendor/bin/pint --dirty
# Test-Modus (ohne Änderungen)
vendor/bin/pint --test
```
### Tests ausführen
```bash
# Alle Tests
php artisan test
# Nur Feature Tests
php artisan test --testsuite=Feature
# Nur Unit Tests
php artisan test --testsuite=Unit
# Spezifische Test-Datei
php artisan test tests/Feature/CompanyTest.php
# Mit Filter
php artisan test --filter=testCompanyCreation
# Mit Coverage (benötigt Xdebug)
php artisan test --coverage
```
### Backend-Daten synchronisieren
```bash
# Stats anzeigen (kein Sync)
php artisan backend:sync-data-pool --stats
# Incremental Sync
php artisan backend:sync-data-pool --incremental
# Full Sync
php artisan backend:sync-data-pool --full
# Transformation
php artisan backend:transform-data-pool
# Kompletter Rebuild
php artisan backend:rebuild
```
### Scheduler lokal testen
```bash
# Schedule Worker (läuft dauerhaft)
php artisan schedule:work
# Oder: Nächste Due-Tasks anzeigen
php artisan schedule:list
# Einzelnen Task testen
php artisan schedule:test sync-backend-data-pool-incremental
```
---
## Troubleshooting
### Problem: "SQLSTATE[08006] Connection refused"
**Ursache:** PostgreSQL läuft nicht oder falsche Credentials
**Lösung:**
```bash
# PostgreSQL-Status prüfen
brew services list | grep postgresql
# PostgreSQL starten
brew services start postgresql@16
# Verbindung testen
psql -h 127.0.0.1 -p 5432 -U risk_user -d risk_platform_db
# .env prüfen
cat .env | grep DB_
```
---
### Problem: "Class 'PDO' not found"
**Ursache:** PHP PostgreSQL Extension fehlt
**Lösung:**
```bash
# Installierte Extensions prüfen
php -m | grep pdo
# Mit Herd (macOS)
# Extensions sind normalerweise vorinstalliert
# Oder PHP neu installieren via Homebrew
brew reinstall php@8.4
```
---
### Problem: "Vite manifest not found"
**Ursache:** Frontend-Assets nicht kompiliert
**Lösung:**
```bash
# Development Build
npm run build
# Oder Dev-Server starten
npm run dev
```
---
### Problem: "419 Page Expired" bei Forms
**Ursache:** CSRF-Token ungültig (Session abgelaufen)
**Lösung:**
```bash
# Cache leeren
php artisan config:clear
php artisan cache:clear
php artisan view:clear
# Browser: Hard Refresh (Cmd+Shift+R / Ctrl+Shift+R)
```
---
### Problem: Migrations laufen nicht
**Ursache:** Datenbank-Schema fehlt oder keine Rechte
**Lösung:**
```bash
# Schema-Rechte prüfen
psql -U risk_user -d risk_platform_db
# In psql:
SELECT schema_name FROM information_schema.schemata;
# Rechte vergeben
GRANT ALL ON SCHEMA public TO risk_user;
GRANT ALL ON ALL TABLES IN SCHEMA public TO risk_user;
# Migration erneut versuchen
php artisan migrate
```
---
### Problem: Queue-Jobs laufen nicht
**Ursache:** Queue-Worker läuft nicht
**Lösung:**
```bash
# Queue-Worker starten
php artisan queue:work
# Oder mit Auto-Restart bei Code-Änderungen
php artisan queue:listen
# Failed Jobs anzeigen
php artisan queue:failed
# Failed Jobs erneut versuchen
php artisan queue:retry all
```
---
### Problem: "Class not found" Fehler
**Ursache:** Autoload-Cache veraltet
**Lösung:**
```bash
# Autoload neu generieren
composer dump-autoload
# Alle Caches leeren
php artisan optimize:clear
# Oder einzeln:
php artisan config:clear
php artisan cache:clear
php artisan route:clear
php artisan view:clear
```
---
### Problem: Livewire-Components laden nicht
**Ursache:** Volt-Components nicht registriert
**Lösung:**
```bash
# Volt Provider prüfen
cat bootstrap/providers.php | grep Volt
# Cache leeren
php artisan optimize:clear
# Browser Cache leeren
# Cmd+Shift+R (macOS) oder Ctrl+Shift+R (Windows/Linux)
```
---
### Problem: Styling fehlt / Tailwind funktioniert nicht
**Ursache:** Vite-Server läuft nicht oder falsche Konfiguration
**Lösung:**
```bash
# Vite Dev-Server neu starten
npm run dev
# Oder Production-Build
npm run build
# Tailwind-Config prüfen
cat tailwind.config.js
# Node-Modules neu installieren
rm -rf node_modules package-lock.json
npm install
```
---
## Nützliche Commands
### Laravel Artisan
```bash
# Liste aller Commands
php artisan list
# Command-Hilfe
php artisan help migrate
# Interaktive Shell
php artisan tinker
# Cache Management
php artisan cache:clear
php artisan config:clear
php artisan route:clear
php artisan view:clear
php artisan optimize:clear # Alle Caches auf einmal
# Database
php artisan db:show # DB-Info anzeigen
php artisan migrate:status # Migration-Status
php artisan migrate:fresh --seed # DB neu aufsetzen + Seeds
# Make Commands
php artisan make:model Company
php artisan make:controller CompanyController
php artisan make:migration create_companies_table
php artisan make:livewire CompanySearch
php artisan make:test CompanyTest
php artisan make:class Services/MyService
# Routes
php artisan route:list # Alle Routes anzeigen
php artisan route:list --path=companies # Gefiltert
```
### Composer
```bash
# Dependencies installieren
composer install
# Development-Dependencies überspringen
composer install --no-dev
# Einzelnes Package installieren
composer require laravel/telescope
# Package entfernen
composer remove laravel/telescope
# Autoload regenerieren
composer dump-autoload
# Setup ausführen (custom script)
composer setup
# Dev-Server starten (custom script)
composer run dev
# Tests ausführen (custom script)
composer test
```
### NPM
```bash
# Dependencies installieren
npm install
# Dev-Server starten
npm run dev
# Production Build
npm run build
# Package installieren
npm install alpine.js
# Package entfernen
npm uninstall alpine.js
# Outdated Packages prüfen
npm outdated
# Updates installieren
npm update
```
### Git
```bash
# Aktueller Branch
git branch
# Neuen Branch erstellen
git checkout -b feature/neue-funktion
# Änderungen anzeigen
git status
git diff
# Commit
git add .
git commit -m "feat: neue funktion"
git push origin feature/neue-funktion
# Main aktualisieren
git checkout main
git pull origin main
```
### PostgreSQL
```bash
# Verbinden
psql -U risk_user -d risk_platform_db
# In psql:
\dt # Alle Tabellen
\d companies # Tabellen-Schema anzeigen
\dn # Alle Schemas
\du # Alle User
\l # Alle Datenbanken
\q # Exit
# Backup erstellen
pg_dump -U risk_user risk_platform_db > backup.sql
# Backup einspielen
psql -U risk_user risk_platform_db < backup.sql
```
---
## Nächste Schritte
Nach erfolgreichem Setup:
1. **Architektur-Dokumentation lesen** → [docs/architecture-overview.md](architecture-overview.md)
2. **Backend-Sync Commands kennenlernen** → [docs/backend-sync-commands.md](backend-sync-commands.md)
3. **Code-Konventionen studieren** → [CLAUDE.md](../CLAUDE.md)
4. **Tests schreiben & ausführen**`php artisan test`
5. **Erste Änderungen committen**`git commit`
---
## Hilfe & Support
### Dokumentation
- **Architektur-Übersicht:** [docs/architecture-overview.md](architecture-overview.md)
- **Backend-Sync:** [docs/backend-sync-commands.md](backend-sync-commands.md)
- **Scheduling:** [docs/scheduling-setup.md](scheduling-setup.md)
- **Migration:** [docs/sync-migration-instructions.md](sync-migration-instructions.md)
### Laravel Dokumentation
- **Laravel 12:** [https://laravel.com/docs/12.x](https://laravel.com/docs/12.x)
- **Livewire 3:** [https://livewire.laravel.com](https://livewire.laravel.com)
- **Flux UI:** [https://flux.laravel.com](https://flux.laravel.com)
- **Pest Testing:** [https://pestphp.com](https://pestphp.com)
### Logs prüfen
```bash
# Application Logs (Live)
tail -f storage/logs/laravel.log
# Nur Fehler
tail -f storage/logs/laravel.log | grep ERROR
# Mit Laravel Pail (farbig)
php artisan pail
```
### Debugging
```bash
# Laravel Debugbar (installiert)
# Aktiviert automatisch wenn APP_DEBUG=true
# Erscheint am unteren Bildschirmrand im Browser
# Tinker für schnelle Tests
php artisan tinker
>>> App\Models\Company::count()
>>> DB::table('transactions')->latest()->first()
```
---
**Setup-Zeit:** ~15-30 Minuten
**Erstellt:** 2025-11-24
**Version:** 1.0
**Autor:** Risk Intelligence Platform Team
Viel Erfolg beim Entwickeln! 🚀
@@ -0,0 +1,64 @@
;;;;;
Hauptebene Titel;Unterebene Titel;Daten;Prompt ID;Überschriften je Prompt ID;prompt_id: output_key = DB Feld transaction
Transaktion;;;;;
Transaktion;Analyse Transaktionsbetrag;;"51
59
67
68
69";"Plausibilität Betrag
Plausibilität Transaktionszeitpunkt
Plausibilität Transaktionsbetrag umsatzbezogen
Plausibilität Transaktionsbetrag ergebnisbezogen
Plausibilität Transaktionsbetrag Branchüblichkeit";"51: tranx_TX_AMOUNT
59: tranx_seasonality
67: tranx_revenueimpact
68: tranx_profitimpact
69: tranx_turnoverimpact"
Transaktion;Analyse Transaktionszweck & außerbetragliche Parameter;;"53
57
62
64
56
58
80
83
85
65
95
96";"Verwendungszweck
Plausibilität Zahlungszweck
Plausibilität Geschäftbeziehung
Plausibilität Transaktionsbetrag Branchenadäquanz
Bewertung
Plausibilität Transaktionsumfang
Auffälligkeiten Transaktion
Plausibilität Transaktion Marktüblichkeit
Transaktion Namensabgleich
Titel fehlt
Plausibilität Instruktion Transaktion
Prüfung letztendliche wirtschaftliche Eigentümer";"53: tranx_TX_PURPOSE
57: tranx_PURPOSEbase
62: tranx_businesslogic
64: tranx_plausibilty
56: tranx_report
58: tranx_performanceperiod
80: tranx_pattern2
83: tranx_contracttenor
85: tranx_mismatch
65: tranx_outliers
95: tranx_weekday
96: tranx_holdingobfuscation
"
Transaktion;Anzeige historischer Transaktionen mit Sender und/oder Empfänger aus der Transaktion inklusive der dokumentierten Auffälligkeiten zu AML pro Datensatz;;"52
79
66
93";"Transaktionshistorie
Smurfing
Historische Transaktionen
Bankverbindung";"52: tranx_historical
79: tranx_duplicate
66: tranx_patterns
93: tranx_newbankaccount"
Transaktion;Zusätzliche Feststellungen (KYC/EDD);;54;Gegenpartei;54: tranx_counterpartyassessment
Transaktion;Export controls/trade restrictions;;81;Sanktionen;"81 entity_corporate_exportcontrol
81 counterparty_corporate_exportcontrol"
1
2 Hauptebene Titel Unterebene Titel Daten Prompt ID Überschriften je Prompt ID prompt_id: output_key = DB Feld transaction
3 Transaktion
4 Transaktion Analyse Transaktionsbetrag 51 59 67 68 69 Plausibilität Betrag Plausibilität Transaktionszeitpunkt Plausibilität Transaktionsbetrag umsatzbezogen Plausibilität Transaktionsbetrag ergebnisbezogen Plausibilität Transaktionsbetrag Branchüblichkeit 51: tranx_TX_AMOUNT 59: tranx_seasonality 67: tranx_revenueimpact 68: tranx_profitimpact 69: tranx_turnoverimpact
5 Transaktion Analyse Transaktionszweck & außerbetragliche Parameter 53 57 62 64 56 58 80 83 85 65 95 96 Verwendungszweck Plausibilität Zahlungszweck Plausibilität Geschäftbeziehung Plausibilität Transaktionsbetrag Branchenadäquanz Bewertung Plausibilität Transaktionsumfang Auffälligkeiten Transaktion Plausibilität Transaktion Marktüblichkeit Transaktion Namensabgleich Titel fehlt Plausibilität Instruktion Transaktion Prüfung letztendliche wirtschaftliche Eigentümer 53: tranx_TX_PURPOSE 57: tranx_PURPOSEbase 62: tranx_businesslogic 64: tranx_plausibilty 56: tranx_report 58: tranx_performanceperiod 80: tranx_pattern2 83: tranx_contracttenor 85: tranx_mismatch 65: tranx_outliers 95: tranx_weekday 96: tranx_holdingobfuscation
6 Transaktion Anzeige historischer Transaktionen mit Sender und/oder Empfänger aus der Transaktion inklusive der dokumentierten Auffälligkeiten zu AML pro Datensatz 52 79 66 93 Transaktionshistorie Smurfing Historische Transaktionen Bankverbindung 52: tranx_historical 79: tranx_duplicate 66: tranx_patterns 93: tranx_newbankaccount
7 Transaktion Zusätzliche Feststellungen (KYC/EDD) 54 Gegenpartei 54: tranx_counterpartyassessment
8 Transaktion Export controls/trade restrictions 81 Sanktionen 81 entity_corporate_exportcontrol 81 counterparty_corporate_exportcontrol
@@ -0,0 +1,70 @@
;;;;;
Hauptebene Titel;Unterebene Titel;Daten;Prompt ID;Überschriften je Prompt ID;prompt_id: output_key = DB Feld transaction
Stammdaten;;;;;
Stammdaten;Rechtlicher Name (aktuell) / Firma;;"9
9
42
42";"Name
Name der Gegenpartei
Warnmitteilung
Warnmitteilung der Gegenpartei";"9: entity_corporate_name
9: counterparty_corporate_name
42: entity_corporate_warnings
42: counterparty_corporate_warnings"
Stammdaten;Rechtsform;;10;"Rechtsform
Rechtsform d. Gegenpartei";"10: entity_corporate_form
10: counterparty_corporate_form"
Stammdaten;Registergericht & Nummer;;11;"Registernummer
Registernummer d. Gegenpartei";"11: entity_corporate_forum
11: counterparty_corporate_forum"
Stammdaten;Sitz / eingetragene Adresse;;"12
48
50
74
86";"Sitzadresse
Sitzadresse d. Gegenpartei
Korruption
Korruption d. Gegenpartei
Länderrisiko
Länderrisiko d. Gegenpartei
Korruption Land
Korruption Land d. Gegenpartei
Risikoländer
Risikoländer d. Gegenpartei";"12: entity_corporate_HQ
12: counterparty_corporate_HQ
48: entity_corporate_corruptionexposure
48: counterparty_corporate_corruptionexposure
50: entity_corporate_CTYexposure
50: counterparty_corporate_CTYexposure
74: entity_corruption_country
74: counterparty_corruption_country
86: entity_country_risk
86: counterparty_country_risk"
Stammdaten;Handelsname/Marke;;"34
42";"Handelsnamen
Handelsnamen d. Gegenpartei
Warnmitteilungen
Warnmitteilungen d. Gegenpartei";"34: entity_corporate_brands
34: counterparty_corporate_brands
42: entity_corporate_warnings
42: counterparty_corporate_warnings"
Stammdaten;"Ebene 1
Summary Geschäftsmodell";;3;Geschäft;"3: entity_corporate_purpose
3: counterparty_corporate_purpose"
Stammdaten;Brief History of the company;;2;Historie;"2: entity_corporate_history
2: counterparty_corporate_history"
Stammdaten;IBAN/BIC der Firma (alle, Listenform);;"
37";IBAN / BIC;"37: entity_corporate_IBAN
37: counterparty_corporate_IBAN"
Stammdaten;Mitarbeiteranzahl;;22;Mitarbeiteranzahl;"22: entity_corporate_employeecount
22: counterparty_corporate_employeecount"
Stammdaten;LEI (Legal Endity Identifikation Number , zu deutsch: internationale Handelsbuchnummer) + Status;;20;"LEI
";"20: entity_corporate_LEI
20: counterparty_corporate_LEI"
Stammdaten;USt-IdNr. (VAT);;19;"Umsatzsteuer-Identifikationsnummer
";"19: entity_corporate_taxID
19: counterparty_corporate_taxID"
Stammdaten;Offizielle Website;;35;Website;"35: entity_corporate_website
35: counterparty_corporate_website"
Stammdaten;Domain(s) & WHOIS;;36;Domains;"36: entity_corporate_domain
36: counterparty_corporate_domain"
1
2 Hauptebene Titel Unterebene Titel Daten Prompt ID Überschriften je Prompt ID prompt_id: output_key = DB Feld transaction
3 Stammdaten
4 Stammdaten Rechtlicher Name (aktuell) / Firma 9 9 42 42 Name Name der Gegenpartei Warnmitteilung Warnmitteilung der Gegenpartei 9: entity_corporate_name 9: counterparty_corporate_name 42: entity_corporate_warnings 42: counterparty_corporate_warnings
5 Stammdaten Rechtsform 10 Rechtsform Rechtsform d. Gegenpartei 10: entity_corporate_form 10: counterparty_corporate_form
6 Stammdaten Registergericht & Nummer 11 Registernummer Registernummer d. Gegenpartei 11: entity_corporate_forum 11: counterparty_corporate_forum
7 Stammdaten Sitz / eingetragene Adresse 12 48 50 74 86 Sitzadresse Sitzadresse d. Gegenpartei Korruption Korruption d. Gegenpartei Länderrisiko Länderrisiko d. Gegenpartei Korruption Land Korruption Land d. Gegenpartei Risikoländer Risikoländer d. Gegenpartei 12: entity_corporate_HQ 12: counterparty_corporate_HQ 48: entity_corporate_corruptionexposure 48: counterparty_corporate_corruptionexposure 50: entity_corporate_CTYexposure 50: counterparty_corporate_CTYexposure 74: entity_corruption_country 74: counterparty_corruption_country 86: entity_country_risk 86: counterparty_country_risk
8 Stammdaten Handelsname/Marke 34 42 Handelsnamen Handelsnamen d. Gegenpartei Warnmitteilungen Warnmitteilungen d. Gegenpartei 34: entity_corporate_brands 34: counterparty_corporate_brands 42: entity_corporate_warnings 42: counterparty_corporate_warnings
9 Stammdaten Ebene 1 Summary Geschäftsmodell 3 Geschäft 3: entity_corporate_purpose 3: counterparty_corporate_purpose
10 Stammdaten Brief History of the company 2 Historie 2: entity_corporate_history 2: counterparty_corporate_history
11 Stammdaten IBAN/BIC der Firma (alle, Listenform) 37 IBAN / BIC 37: entity_corporate_IBAN 37: counterparty_corporate_IBAN
12 Stammdaten Mitarbeiteranzahl 22 Mitarbeiteranzahl 22: entity_corporate_employeecount 22: counterparty_corporate_employeecount
13 Stammdaten LEI (Legal Endity Identifikation Number , zu deutsch: internationale Handelsbuchnummer) + Status 20 LEI 20: entity_corporate_LEI 20: counterparty_corporate_LEI
14 Stammdaten USt-IdNr. (VAT) 19 Umsatzsteuer-Identifikationsnummer 19: entity_corporate_taxID 19: counterparty_corporate_taxID
15 Stammdaten Offizielle Website 35 Website 35: entity_corporate_website 35: counterparty_corporate_website
16 Stammdaten Domain(s) & WHOIS 36 Domains 36: entity_corporate_domain 36: counterparty_corporate_domain
@@ -0,0 +1,17 @@
;;;;;
Hauptebene Titel;Unterebene Titel;Daten;Prompt ID;Überschriften je Prompt ID;prompt_id: output_key = DB Feld transaction
Unternehmens-Status;;;;;
Unternehmens-Status;Liquidation/Stilllegung;;29;Liquidation;"29: entity_corporate_liquidation
29: counterparty_corporate_liquidation
"
Unternehmens-Status;Insolvenzeröffnungen;;28;Insolvenzverfahren;"28: entity_corporate_insolvency
28: counterparty_corporate_insolvency
"
Unternehmens-Status;Rechtliche /gerichtliche Verfahren;;89;Rechtsverletzungen;"89: entity_corporate_courtcases
89: counterparty_corporate_courtcases
"
Unternehmens-Status;M&A;;90;M&A;"90: entity_corporate_manda
90: counterparty_corporate_manda
"
Unternehmens-Status;Betrug;;91;Transaktion Betrugstypologie;"91: tranx_fakepurposecheck
"
1
2 Hauptebene Titel Unterebene Titel Daten Prompt ID Überschriften je Prompt ID prompt_id: output_key = DB Feld transaction
3 Unternehmens-Status
4 Unternehmens-Status Liquidation/Stilllegung 29 Liquidation 29: entity_corporate_liquidation 29: counterparty_corporate_liquidation
5 Unternehmens-Status Insolvenzeröffnungen 28 Insolvenzverfahren 28: entity_corporate_insolvency 28: counterparty_corporate_insolvency
6 Unternehmens-Status Rechtliche /gerichtliche Verfahren 89 Rechtsverletzungen 89: entity_corporate_courtcases 89: counterparty_corporate_courtcases
7 Unternehmens-Status M&A 90 M&A 90: entity_corporate_manda 90: counterparty_corporate_manda
8 Unternehmens-Status Betrug 91 Transaktion Betrugstypologie 91: tranx_fakepurposecheck
@@ -0,0 +1,70 @@
;;;;;
Hauptebene Titel;Unterebene Titel;Daten;Prompt ID;Überschriften je Prompt ID;prompt_id: output_key = DB Feld transaction
4. Branche(n);;;;;
4. Branche(n);Tätigkeitsbeschreibung;;"1
4
48
40
75
78";"Kurzporträt
Kurzporträt d. Gegenpartei
Sektor
Sektor d. Gegenpartei
Rating
Rating d. Gegenpartei
Korruption
Korruption d. Gegenpartei
Korruption Geschäftspartner
Korruption Geschäftspartner d. Gegenpartei
Reputationsrisiken
Reputationsrisiken d. Gegenpartei
";"1: entity_corporate_summary
1: counterparty_corporate_summary
4: entity_corporate_sector
4: counterparty_corporate_sector
48: entity_corporate_corruptionexposure
48: counterparty_corporate_corruptionexposure
40: entity_corporate_ESG
40: counterparty_corporate_ESG
75: entity_corruption_relationship
75: counterparty_corruption_relationship
78: entity_corporate_adverse2
78: counterparty_corporate_adverse2
"
4. Branche(n);Branche;;"5
13
42
48
40
73";"NACE / NAICS Code
Niederlassungen
Korruption
Warnmitteilungen
Rating
Korruption Sektor";"5: entity_corporate_nace
5: counterparty_corporate_nace
13: entity_corporate_locations
13: counterparty_corporate_locations
42: entity_corporate_warnings
42: counterparty_corporate_warnings
48: entity_corporate_corruptionexposure
48: counterparty_corporate_corruptionexposure
40: entity_corporate_ESG
40: counterparty_corporate_ESG
73: entity_corruption_sector
73: counterparty_corruption_sector
"
4. Branche(n);Warengruppen/Dienstleistungen;;"6
40";"Produkte / Leistungen
Rating";"6: entity_corporate_products
6: counterparty_corporate_products
40: entity_corporate_ESG
40: counterparty_corporate_ESG"
4. Branche(n);Lieferkettenhinweise;;"8
72";"Lieferbeziehungen
Sanktionen";"8: entity_corporate_supply
8: counterparty_corporate_supply
72: entity_sanctions_circumvention
72: counterparty_sanctions_circumvention"
1
2 Hauptebene Titel Unterebene Titel Daten Prompt ID Überschriften je Prompt ID prompt_id: output_key = DB Feld transaction
3 4. Branche(n)
4 4. Branche(n) Tätigkeitsbeschreibung 1 4 48 40 75 78 Kurzporträt Kurzporträt d. Gegenpartei Sektor Sektor d. Gegenpartei Rating Rating d. Gegenpartei Korruption Korruption d. Gegenpartei Korruption Geschäftspartner Korruption Geschäftspartner d. Gegenpartei Reputationsrisiken Reputationsrisiken d. Gegenpartei 1: entity_corporate_summary 1: counterparty_corporate_summary 4: entity_corporate_sector 4: counterparty_corporate_sector 48: entity_corporate_corruptionexposure 48: counterparty_corporate_corruptionexposure 40: entity_corporate_ESG 40: counterparty_corporate_ESG 75: entity_corruption_relationship 75: counterparty_corruption_relationship 78: entity_corporate_adverse2 78: counterparty_corporate_adverse2
5 4. Branche(n) Branche 5 13 42 48 40 73 NACE / NAICS Code Niederlassungen Korruption Warnmitteilungen Rating Korruption Sektor 5: entity_corporate_nace 5: counterparty_corporate_nace 13: entity_corporate_locations 13: counterparty_corporate_locations 42: entity_corporate_warnings 42: counterparty_corporate_warnings 48: entity_corporate_corruptionexposure 48: counterparty_corporate_corruptionexposure 40: entity_corporate_ESG 40: counterparty_corporate_ESG 73: entity_corruption_sector 73: counterparty_corruption_sector
6 4. Branche(n) Warengruppen/Dienstleistungen 6 40 Produkte / Leistungen Rating 6: entity_corporate_products 6: counterparty_corporate_products 40: entity_corporate_ESG 40: counterparty_corporate_ESG
7 4. Branche(n) Lieferkettenhinweise 8 72 Lieferbeziehungen Sanktionen 8: entity_corporate_supply 8: counterparty_corporate_supply 72: entity_sanctions_circumvention 72: counterparty_sanctions_circumvention
@@ -0,0 +1,51 @@
;;;;;
Hauptebene Titel;Unterebene Titel;Daten;Prompt ID;Überschriften je Prompt ID;prompt_id: output_key = DB Feld transaction
5. Länder;;;;;
5. Länder;Hauptsitz (Stadt/Land);;"74
50
86
84";"Korruption Land
Länderrisiko
Risikoländer
Geocoding";"74: entity_corruption_country
74: counterparty_corruption_country
50: entity_corporate_CTYexposure
50: counterparty_corporate_CTYexposure
86: entity_country_risk
86: counterparty_country_risk
84: entity_corporate_haven
84: counterparty_corporate_haven
"
5. Länder;Betriebsstätten/Filialen;;"84
13
50
86
84";"Geocoding
Niederlassungen
Länderrisiko
Risikoländer
Geocoding";"84: entity_corporate_haven
84: counterparty_corporate_haven
13: entity_corporate_locations
13: counterparty_corporate_locations
50: entity_corporate_CTYexposure
50: counterparty_corporate_CTYexposure
86: entity_country_risk
86: counterparty_country_risk"
5. Länder;Kundensegmente/Regionen;;"7
50
86
84";"Markt
Länderrisiko
Risikoländer
Geocoding";"7: entity_corporate_markets
7: counterparty_corporate_markets
50: entity_corporate_CTYexposure
50: counterparty_corporate_CTYexposure
86: entity_country_risk
86: counterparty_country_risk
84: entity_corporate_haven
84: counterparty_corporate_haven"
5. Länder;Corruption Perception Index (CPI);;50;Länderrisiko;"50: entity_corporate_CTYexposure
50: counterparty_corporate_CTYexposure"
1
2 Hauptebene Titel Unterebene Titel Daten Prompt ID Überschriften je Prompt ID prompt_id: output_key = DB Feld transaction
3 5. Länder
4 5. Länder Hauptsitz (Stadt/Land) 74 50 86 84 Korruption Land Länderrisiko Risikoländer Geocoding 74: entity_corruption_country 74: counterparty_corruption_country 50: entity_corporate_CTYexposure 50: counterparty_corporate_CTYexposure 86: entity_country_risk 86: counterparty_country_risk 84: entity_corporate_haven 84: counterparty_corporate_haven
5 5. Länder Betriebsstätten/Filialen 84 13 50 86 84 Geocoding Niederlassungen Länderrisiko Risikoländer Geocoding 84: entity_corporate_haven 84: counterparty_corporate_haven 13: entity_corporate_locations 13: counterparty_corporate_locations 50: entity_corporate_CTYexposure 50: counterparty_corporate_CTYexposure 86: entity_country_risk 86: counterparty_country_risk
6 5. Länder Kundensegmente/Regionen 7 50 86 84 Markt Länderrisiko Risikoländer Geocoding 7: entity_corporate_markets 7: counterparty_corporate_markets 50: entity_corporate_CTYexposure 50: counterparty_corporate_CTYexposure 86: entity_country_risk 86: counterparty_country_risk 84: entity_corporate_haven 84: counterparty_corporate_haven
7 5. Länder Corruption Perception Index (CPI) 50 Länderrisiko 50: entity_corporate_CTYexposure 50: counterparty_corporate_CTYexposure
@@ -0,0 +1,5 @@
;;;;;;;
Hauptebene Titel;Unterebene Titel;Daten;Prompt ID;Überschriften je Prompt ID;prompt_id: output_key = DB Feld transaction;Notizen, Hinweise;Antwort SK 20251119
6. Strukturen & Verflechtungen;;;;;;;
6. Strukturen & Verflechtungen;Obergesellschaft(en);;14;Muttergesellschaft;"14: entity_corporate_holding
14: counterparty_corporate_holding";;
1
2 Hauptebene Titel Unterebene Titel Daten Prompt ID Überschriften je Prompt ID prompt_id: output_key = DB Feld transaction Notizen, Hinweise Antwort SK 20251119
3 6. Strukturen & Verflechtungen
4 6. Strukturen & Verflechtungen Obergesellschaft(en) 14 Muttergesellschaft 14: entity_corporate_holding 14: counterparty_corporate_holding
@@ -0,0 +1,95 @@
;;;;;
Hauptebene Titel;Unterebene Titel;Daten;Prompt ID;Überschriften je Prompt ID;prompt_id: output_key = DB Feld transaction
7. Natürliche Personen;;;;;
7. Natürliche Personen;Geschäftsführer/Vorstand;;"16
43
44
45
77";"Unternehmensführung
Sanktionen
Sanktionen
Sanktionen
PEP";"16: entity_corporate_board
16: counterparty_corporate_board
43: entity_corporate_eusanctions
43: counterparty_corporate_eusanctions
44: entity_corporate_ofacsanctions
44: counterparty_corporate_ofacsanctions
45: entity_corporate_uksanctions
45: counterparty_corporate_uksanctions
77: entity_corporate_pep
77: counterparty_corporate_pep"
7. Natürliche Personen;Aufsichtsrat/Beirat;;"17
43
44
45
77";"Kontrollorgan
Sanktionen
Sanktionen
Sanktionen
PEP";"17: entity_corporate_supervisory
17: counterparty_corporate_supervisory
43: entity_corporate_eusanctions
43: counterparty_corporate_eusanctions
44: entity_corporate_ofacsanctions
44: counterparty_corporate_ofacsanctions
45: entity_corporate_uksanctions
45: counterparty_corporate_uksanctions
77: entity_corporate_pep
77: counterparty_corporate_pep"
7. Natürliche Personen;Anteilseignerliste;;"15
43
44
45
77";"Gesellschafter
Sanktionen
Sanktionen
Sanktionen
PEP";"15: entity_corporate_shareholders
15: counterparty_corporate_shareholders
43: entity_corporate_eusanctions
43: counterparty_corporate_eusanctions
44: entity_corporate_ofacsanctions
44: counterparty_corporate_ofacsanctions
45: entity_corporate_uksanctions
45: counterparty_corporate_uksanctions
77: entity_corporate_pep
77: counterparty_corporate_pep
"
7. Natürliche Personen;Wirtschaftlich Berechtigte (UBOs);;"21
43
44
45
46";"letztendliche wirtschafltiche Eigentümer
Sanktionen
Sanktionen
Sanktionen
PEP";"21: entity_corporate_UBO
21: counterparty_corporate_UBO
43: entity_corporate_eusanctions
43: counterparty_corporate_eusanctions
44: entity_corporate_ofacsanctions
44: counterparty_corporate_ofacsanctions
45: entity_corporate_uksanctions
45: counterparty_corporate_uksanctions
46: entity_corporate_pepexposure
46: counterparty_corporate_pepexposure"
7. Natürliche Personen;Zeichnungsberechtigte;;"18
43
44
45
77";"Bevollmächtigte
Sanktionen
Sanktionen
Sanktionen
PEP
";"18: entity_corporate_powerofattorney
18: counterparty_corporate_powerofattorney
43: entity_corporate_eusanctions
43: counterparty_corporate_eusanctions
44: entity_corporate_ofacsanctions
44: counterparty_corporate_ofacsanctions
45: entity_corporate_uksanctions
45: counterparty_corporate_uksanctions
77: entity_corporate_pep
77: counterparty_corporate_pep"
1
2 Hauptebene Titel Unterebene Titel Daten Prompt ID Überschriften je Prompt ID prompt_id: output_key = DB Feld transaction
3 7. Natürliche Personen
4 7. Natürliche Personen Geschäftsführer/Vorstand 16 43 44 45 77 Unternehmensführung Sanktionen Sanktionen Sanktionen PEP 16: entity_corporate_board 16: counterparty_corporate_board 43: entity_corporate_eusanctions 43: counterparty_corporate_eusanctions 44: entity_corporate_ofacsanctions 44: counterparty_corporate_ofacsanctions 45: entity_corporate_uksanctions 45: counterparty_corporate_uksanctions 77: entity_corporate_pep 77: counterparty_corporate_pep
5 7. Natürliche Personen Aufsichtsrat/Beirat 17 43 44 45 77 Kontrollorgan Sanktionen Sanktionen Sanktionen PEP 17: entity_corporate_supervisory 17: counterparty_corporate_supervisory 43: entity_corporate_eusanctions 43: counterparty_corporate_eusanctions 44: entity_corporate_ofacsanctions 44: counterparty_corporate_ofacsanctions 45: entity_corporate_uksanctions 45: counterparty_corporate_uksanctions 77: entity_corporate_pep 77: counterparty_corporate_pep
6 7. Natürliche Personen Anteilseignerliste 15 43 44 45 77 Gesellschafter Sanktionen Sanktionen Sanktionen PEP 15: entity_corporate_shareholders 15: counterparty_corporate_shareholders 43: entity_corporate_eusanctions 43: counterparty_corporate_eusanctions 44: entity_corporate_ofacsanctions 44: counterparty_corporate_ofacsanctions 45: entity_corporate_uksanctions 45: counterparty_corporate_uksanctions 77: entity_corporate_pep 77: counterparty_corporate_pep
7 7. Natürliche Personen Wirtschaftlich Berechtigte (UBOs) 21 43 44 45 46 letztendliche wirtschafltiche Eigentümer Sanktionen Sanktionen Sanktionen PEP 21: entity_corporate_UBO 21: counterparty_corporate_UBO 43: entity_corporate_eusanctions 43: counterparty_corporate_eusanctions 44: entity_corporate_ofacsanctions 44: counterparty_corporate_ofacsanctions 45: entity_corporate_uksanctions 45: counterparty_corporate_uksanctions 46: entity_corporate_pepexposure 46: counterparty_corporate_pepexposure
8 7. Natürliche Personen Zeichnungsberechtigte 18 43 44 45 77 Bevollmächtigte Sanktionen Sanktionen Sanktionen PEP 18: entity_corporate_powerofattorney 18: counterparty_corporate_powerofattorney 43: entity_corporate_eusanctions 43: counterparty_corporate_eusanctions 44: entity_corporate_ofacsanctions 44: counterparty_corporate_ofacsanctions 45: entity_corporate_uksanctions 45: counterparty_corporate_uksanctions 77: entity_corporate_pep 77: counterparty_corporate_pep
@@ -0,0 +1,55 @@
;;;;;
Hauptebene Titel;Unterebene Titel;Daten;Prompt ID;Überschriften je Prompt ID;prompt_id: output_key = DB Feld transaction
8. Zusätzliche Informationen;;;;;
8. Zusätzliche Informationen;Adverse Media (negative Medienberichte, aber evtl. noch nicht offiziell);;47;Adverse Media;"47: entity_corporate_adversemediascanning
47: counterparty_corporate_adversemediascanning
"
8. Zusätzliche Informationen;Jahresabschlüsse;;"23
24
25
26";"Umsatz
EBIT/EBITDA
Nettogewinn
Gesamtvermögen";"23_ entity_corporate_turnover
23_ counterparty_corporate_turnover
24_ entity_corporate_EBIT
24_ counterparty_corporate_EBIT
25_ entity_corporate_netprofits
25_ counterparty_corporate_netprofits
26_ entity_corporate_balancesheet
26_ counterparty_corporate_balancesheet
"
8. Zusätzliche Informationen;AdHoc Meldungen, Corporate News;;"30
76
82";"Adhoc Berichte
Führungspersonal
Mitteilungen";"30 ""counterparty_corporate_adhoc""
30 ""entity_corporate_adhoc""
76 ""counterparty_corporate_adverse""
76 ""entity_corporate_adverse""
82 ""counterparty_corporate_mediamatch""
82 ""entity_corporate_mediamatch"""
8. Zusätzliche Informationen;Pressemitteilungen;;31;Presseberichte;"31: entity_corporate_pressrelease
31: counterparty_corporate_pressrelease
"
8. Zusätzliche Informationen;Stimmrechtsmitteilungen;;32;Stimmrechtsmitteilungen;"32: entity_corporate_votes
32: counterparty_corporate_votes
"
8. Zusätzliche Informationen;Jahresbericht;;;Jahresbericht;bitte nur als Platzhalter anlegen
8. Zusätzliche Informationen;Audit-Hinweise (Bestätigungsvermerk);;27;Prüfberichte;"27: entity_corporate_auditfindings
27: counterparty_corporate_auditfindings"
8. Zusätzliche Informationen;Ratings;;"38
39";"Rating
Rating";"38: entity_corporate_solvency
38: counterparty_corporate_solvency
39: entity_corporate_rating
39: counterparty_corporate_rating
"
8. Zusätzliche Informationen;Schufa, Hermes , Creditreform auf Zahlungsverhalten/Bönität;;38;Rating;"38: entity_corporate_solvency
38: counterparty_corporate_solvency"
8. Zusätzliche Informationen;ESG Rating;;40;Rating;"40: entity_corporate_ESG
40: counterparty_corporate_ESG
"
1
2 Hauptebene Titel Unterebene Titel Daten Prompt ID Überschriften je Prompt ID prompt_id: output_key = DB Feld transaction
3 8. Zusätzliche Informationen
4 8. Zusätzliche Informationen Adverse Media (negative Medienberichte, aber evtl. noch nicht offiziell) 47 Adverse Media 47: entity_corporate_adversemediascanning 47: counterparty_corporate_adversemediascanning
5 8. Zusätzliche Informationen Jahresabschlüsse 23 24 25 26 Umsatz EBIT/EBITDA Nettogewinn Gesamtvermögen 23_ entity_corporate_turnover 23_ counterparty_corporate_turnover 24_ entity_corporate_EBIT 24_ counterparty_corporate_EBIT 25_ entity_corporate_netprofits 25_ counterparty_corporate_netprofits 26_ entity_corporate_balancesheet 26_ counterparty_corporate_balancesheet
6 8. Zusätzliche Informationen AdHoc Meldungen, Corporate News 30 76 82 Adhoc Berichte Führungspersonal Mitteilungen 30 "counterparty_corporate_adhoc" 30 "entity_corporate_adhoc" 76 "counterparty_corporate_adverse" 76 "entity_corporate_adverse" 82 "counterparty_corporate_mediamatch" 82 "entity_corporate_mediamatch"
7 8. Zusätzliche Informationen Pressemitteilungen 31 Presseberichte 31: entity_corporate_pressrelease 31: counterparty_corporate_pressrelease
8 8. Zusätzliche Informationen Stimmrechtsmitteilungen 32 Stimmrechtsmitteilungen 32: entity_corporate_votes 32: counterparty_corporate_votes
9 8. Zusätzliche Informationen Jahresbericht Jahresbericht bitte nur als Platzhalter anlegen
10 8. Zusätzliche Informationen Audit-Hinweise (Bestätigungsvermerk) 27 Prüfberichte 27: entity_corporate_auditfindings 27: counterparty_corporate_auditfindings
11 8. Zusätzliche Informationen Ratings 38 39 Rating Rating 38: entity_corporate_solvency 38: counterparty_corporate_solvency 39: entity_corporate_rating 39: counterparty_corporate_rating
12 8. Zusätzliche Informationen Schufa, Hermes , Creditreform auf Zahlungsverhalten/Bönität 38 Rating 38: entity_corporate_solvency 38: counterparty_corporate_solvency
13 8. Zusätzliche Informationen ESG Rating 40 Rating 40: entity_corporate_ESG 40: counterparty_corporate_ESG
@@ -363,30 +363,30 @@ Total Score: 22.5 → "low"
## 📋 Implementierungs-Schritte
### Phase 1: Vorbereitung
### Phase 1: Vorbereitung ✅ ABGESCHLOSSEN
1. ✅ Data Pool ist befüllt
2. KYC Risk Calculator Service erstellen
3. Data Extraction Helpers erstellen
4. Mapping-Logik definieren
2. KYC Risk Calculator Service erstellen
3. Data Extraction Helpers erstellen
4. Mapping-Logik definieren
### Phase 2: Job Implementation
1. TransformDataPoolToProduction Job erstellen
2. Company Creation Logic implementieren
3. Transaction Creation Logic implementieren
4. Error Handling & Logging
### Phase 2: Job Implementation ✅ ABGESCHLOSSEN
1. TransformDataPoolToProduction Job erstellen
2. Company Creation Logic implementieren
3. Transaction Creation Logic implementieren
4. Error Handling & Logging
### Phase 3: Testing
1. ⬜ Unit Tests für Risk Calculator
2. ⬜ Feature Tests für Transformation Job
3. ⬜ Datenintegritäts-Checks
### Phase 4: Scheduling
1. Schedule konfigurieren
2. Queue Setup (optional)
### Phase 4: Scheduling ✅ ABGESCHLOSSEN
1. Schedule konfigurieren (läuft alle 6h um :30)
2. Queue Setup (optional)
3. ⬜ Monitoring einrichten
### Phase 5: Deployment
1. Produktions-Test mit echten Daten
1. Produktions-Test mit echten Daten
2. ⬜ Performance-Optimierung
3. ⬜ Dokumentation finalisieren
@@ -424,5 +424,24 @@ Nach erfolgreicher Implementation von Stufe 2:
---
*Erstellt am: 2025-11-16*
*Status: PLANUNG - Wartet auf Entscheidungen zu offenen Fragen*
## 🎉 Status Update
**Erstellt am:** 2025-11-16
**Aktualisiert am:** 2025-11-21
**Status:** ✅ **PRODUKTIV** - Stufe 2 ist implementiert und läuft automatisch!
### Was funktioniert:
**TransformDataPoolToProduction Job** - Vollständig implementiert
**Automatisches Scheduling** - Läuft alle 6 Stunden um :30 (0:30, 6:30, 12:30, 18:30)
**Company & Transaction Creation** - Automatische Erstellung und Updates
**Risk Score Mapping** - Verwendet Backend risk_score direkt
**Artisan Command** - `php artisan backend:transform-data-pool` verfügbar
### Offene TODOs:
⬜ Unit Tests für Transformation Job (Phase 3)
⬜ Feature Tests für Transformation Job (Phase 3)
⬜ Monitoring einrichten (Phase 4)
⬜ Performance-Optimierung (Phase 5)
⬜ KycRiskCalculator Service integrieren (optional, aktuell nicht genutzt)
File diff suppressed because it is too large Load Diff
+61
View File
@@ -0,0 +1,61 @@
apiVersion: batch/v1
kind: CronJob
metadata:
name: sync-backend-data-pool
namespace: frontend
spec:
schedule: "*/5 * * * *" # Runs every 5 minutes - adjust as needed
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
concurrencyPolicy: Forbid # Prevents overlapping executions
jobTemplate:
spec:
template:
metadata:
labels:
app: sync-backend-data-pool
spec:
restartPolicy: OnFailure
containers:
- name: sync-backend-data-pool
image: git.trai-infra.comstack.de/marian.lippitz/tap-frontend:latest
args: ["php", "artisan", "schedule:run"]
envFrom:
- configMapRef:
name: tap-frontend-config
volumeMounts:
- name: tap-frontend-config-volume
mountPath: /var/www/html/.env
subPath: env
volumes:
- name: tap-frontend-config-volume
configMap:
name: tap-frontend-config
---
apiVersion: batch/v1
kind: Job
metadata:
name: sync-backend-data-pool-manual
namespace: frontend
spec:
template:
metadata:
labels:
app: sync-backend-data-pool-manual
spec:
restartPolicy: OnFailure
containers:
- name: sync-backend-data-pool
image: git.trai-infra.comstack.de/marian.lippitz/tap-frontend:latest
args: ["php", "artisan", "tinker", "--execute=dispatch(new App\\Jobs\\SyncBackendDataPool(fullSync: false, batchSize: 1000))"]
envFrom:
- configMapRef:
name: tap-frontend-config
volumeMounts:
- name: tap-frontend-config-volume
mountPath: /var/www/html/.env
subPath: env
volumes:
- name: tap-frontend-config-volume
configMap:
name: tap-frontend-config
File diff suppressed because it is too large Load Diff
-415
View File
@@ -1,415 +0,0 @@
# Implementation Summary: Backend Schema Integration
## ✅ Was wurde implementiert
### 1. Database Configuration
**Datei**: [config/database.php](config/database.php)
Zwei Verbindungen hinzugefügt:
- `pgsql` → public Schema (Default)
- `backend` → backend Schema (search_path: 'backend')
```php
'default' => env('DB_CONNECTION', 'pgsql'), // Geändert von sqlite zu pgsql
'pgsql' => [
'search_path' => 'public',
],
'backend' => [
'search_path' => 'backend', // Zugriff auf backend Schema
],
```
### 2. Backend Models
#### Backend\Transaction Model
**Datei**: [app/Models/Backend/Transaction.php](app/Models/Backend/Transaction.php)
**Eigenschaften**:
- Connection: `backend`
- Table: `transactions`
- 14 Felder (Rohdaten aus CSV Upload)
**Relationships**:
- `outputs()` - HasMany zu TransactionOutput
**Helper Methods**:
```php
// Outputs abrufen
getOutput(string $key): ?array
getOutputsArray(): array
getCompanyInfo(): ?array
getRiskAssessment(): ?array
getSanctionsCheck(): ?array
getPepCheck(): ?array
// Status & Review
hasOutput(string $key): bool
isProcessed(): bool
requiresReview(): bool
// Display Helpers
getCompanyName(): string
getRiskScore(): int
getRiskLevel(): string
```
#### Backend\TransactionOutput Model
**Datei**: [app/Models/Backend/TransactionOutput.php](app/Models/Backend/TransactionOutput.php)
**Eigenschaften**:
- Connection: `backend`
- Table: `transaction_outputs`
- 5 Felder (KI-generierte Outputs)
**Relationships**:
- `transaction()` - BelongsTo Transaction
**Konstanten** (Output-Keys):
```php
KEY_COMPANY_INFO = 'company_info'
KEY_RISK_ASSESSMENT = 'risk_assessment'
KEY_SANCTIONS = 'sanctions'
KEY_PEP = 'pep'
KEY_REGISTRY = 'registry'
KEY_GLEIF = 'gleif'
KEY_INSOLVENCY = 'insolvency'
KEY_BUNDESANZEIGER = 'bundesanzeiger'
KEY_RSS = 'rss'
KEY_EU_SANCTIONS = 'eu_sanctions'
KEY_HANDELSREGISTER = 'handelsregister'
KEY_GENESIS = 'genesis'
KEY_GOVDATA = 'govdata'
```
**Helper Methods**:
```php
availableKeys(): array
getKeyLabel(string $key): string
```
### 3. Repository Layer
**Datei**: [app/Repositories/TransactionRepository.php](app/Repositories/TransactionRepository.php)
**Methoden**:
```php
// Basic CRUD
all(): Collection
find(int $id): ?Transaction
paginated(int $perPage = 20)
// Filtering
requiresReview(): Collection
highRisk(int $threshold = 70): Collection
byCompany(string $companyName): Collection
byStatus(string $status): Collection
byDateRange(string $startDate, string $endDate): Collection
search(string $query): Collection
// Status-specific
pending(): Collection
processing(): Collection
completed(): Collection
failed(): Collection
// Statistics
stats(): array
dashboardData(): array
getAllCompanies(): Collection
```
### 4. Tests
**Datei**: [tests/Feature/BackendModelsTest.php](tests/Feature/BackendModelsTest.php)
11 Tests geschrieben (3 laufen ohne DB-Verbindung):
- ✅ Connection Tests
- ✅ Model Relationship Tests
- ✅ Helper Method Tests
- ✅ Constant Tests
---
## 📋 Nächste Schritte: Component-Migration
### Betroffene Components
#### 1. transaction-review.blade.php
**Aktuell**: Verwendet `App\Models\Transaction` und `App\Models\Company`
**Änderungen**:
```php
// Alt
use App\Models\Transaction;
use App\Models\Company;
// Neu
use App\Models\Backend\Transaction;
use App\Repositories\TransactionRepository;
// Repository verwenden statt direkte Model-Queries
public function __construct(
private TransactionRepository $transactions
) {}
$transactions = $this->transactions->all();
```
**Mapping**:
| Alt (public.transactions) | Neu (backend) |
|---------------------------|---------------|
| `$transaction->company->name` | `$transaction->getCompanyName()` |
| `$transaction->company->legal_name` | `$transaction->getCompanyInfo()['legal_name']` |
| `$transaction->company->sector` | `$transaction->getCompanyInfo()['sector']` |
| `$transaction->risk_score` | `$transaction->getRiskScore()` |
| `$transaction->requires_review` | `$transaction->requiresReview()` |
| `$transaction->amount` | `$transaction->tx_amount` |
| `$transaction->counterparty` | `$transaction->corporate_counterparty` |
| `$transaction->executed_at` | `$transaction->tx_date` (⚠️ ist text) |
#### 2. companies/transactions.blade.php
**Aktuell**: Verwendet `App\Models\Company` Parameter
**Änderungen**:
- Company-Daten kommen jetzt aus `transaction_outputs`
- Grouping nach `corporate_entity` statt `company_id`
**Neuer Ansatz**:
```php
// Alle Transaktionen für eine Firma
$companyName = 'Siemens AG'; // Aus Route oder Parameter
$transactions = $this->transactions->byCompany($companyName);
// Company-Info aus erster Transaction
$companyInfo = $transactions->first()?->getCompanyInfo();
```
#### 3. company-search.blade.php
**Änderungen**:
- Suche jetzt in `backend.transactions.corporate_entity`
- Keine separate Company-Tabelle mehr
```php
// Repository Method
public function getAllCompanies(): Collection
{
return DB::connection('backend')
->table('transactions')
->select('corporate_entity as name')
->distinct()
->orderBy('corporate_entity')
->get();
}
```
---
## 🔄 Migrationsplan (Step-by-Step)
### Phase 1: Testen ohne Breaking Changes (empfohlen)
**Option A: Feature Flag**
```php
// config/features.php
'use_backend_schema' => env('FEATURE_USE_BACKEND_SCHEMA', false),
// In Components
if (config('features.use_backend_schema')) {
$transactions = app(TransactionRepository::class)->all();
} else {
$transactions = Transaction::all(); // Alt
}
```
**Option B: Neue Routes** (empfohlen für parallele Tests)
```php
// routes/web.php
Route::get('/beta/transactions', ...); // Nutzt Backend-Schema
Route::get('/transactions', ...); // Alte Implementation
```
### Phase 2: Direkte Migration (schneller, aber riskanter)
1. **Alle `use App\Models\Transaction` ersetzen**:
```bash
find resources/views/livewire -type f -name "*.php" -exec sed -i '' 's/use App\\Models\\Transaction/use App\\Models\\Backend\\Transaction/g' {} +
```
2. **Alle `use App\Models\Company` entfernen**:
```bash
find resources/views/livewire -type f -name "*.php" -exec sed -i '' 's/use App\\Models\\Company;//g' {} +
```
3. **Component für Component anpassen**:
- transaction-review.blade.php
- companies/transactions.blade.php
- company-search.blade.php
---
## 🗺️ Daten-Mapping Referenz
### Transaction Fields
| public.transactions | backend.transactions | Typ-Unterschied |
|---------------------|----------------------|-----------------|
| id | id | bigint → integer |
| company_id | - (via corporate_entity) | Relationship entfällt |
| reference | - | Neu: auto-generated |
| amount | tx_amount | numeric → double |
| currency | tx_currency | ✓ |
| counterparty | corporate_counterparty | ✓ |
| counterparty_country | tx_country_incoming | ✓ |
| channel | - | Nicht in backend |
| executed_at | tx_date | **timestamp → text!** |
| risk_score | - (in outputs) | Via getRiskScore() |
| status | status | ✓ |
| requires_review | - (computed) | Via requiresReview() |
| flagged_by | - | Nicht in backend |
| flagged_reason | tx_purpose | Ähnlich |
| signals | - | Nicht in backend |
### Company Fields (jetzt in transaction_outputs)
| public.companies | transaction_outputs (key='company_info') |
|------------------|-------------------------------------------|
| name | content['name'] |
| legal_name | content['legal_name'] |
| ticker | content['ticker'] |
| sector | content['sector'] |
| country | content['country'] |
| headquarters | content['headquarters'] |
| kyc_risk_level | content['kyc_risk_level'] |
| summary | content['summary'] |
### Enrichment Fields (in transaction_outputs)
Alle 35 Enrichment-Felder aus `public.transactions` sind jetzt separate Outputs:
```php
// Alt
$transaction->sanctions_data // JSON
// Neu
$transaction->getOutput('sanctions') // Array
```
---
## ⚠️ Breaking Changes
### 1. Timestamps sind Text
```php
// Alt
$transaction->executed_at->format('d.m.Y')
// Neu (ACHTUNG: tx_date ist Text!)
$transaction->tx_date // Already string, no format()
```
**Empfehlung**: Migration hinzufügen um `tx_date` von `text` zu `timestamp` zu ändern.
### 2. Company Relationship entfällt
```php
// Alt
$transaction->company->name
$transaction->company()->where(...)
// Neu
$transaction->getCompanyName()
$transaction->getCompanyInfo()['name']
// Kein Relationship mehr verfügbar
```
### 3. Feld-Namen ändern sich
```php
// Alt → Neu
amount → tx_amount
counterparty → corporate_counterparty
executed_at → tx_date
```
**Empfehlung**: Accessor in Model für Backward-Compatibility:
```php
// In Backend\Transaction Model
protected $appends = ['amount', 'counterparty', 'executed_at'];
public function getAmountAttribute(): float
{
return $this->tx_amount;
}
public function getCounterpartyAttribute(): string
{
return $this->corporate_counterparty;
}
public function getExecutedAtAttribute(): string
{
return $this->tx_date;
}
```
---
## 🎯 Empfohlenes Vorgehen
### Option 1: Schrittweise mit Feature Flags (Sicher, 2-3 Wochen)
1. ✅ Backend Models & Repository erstellt
2. ⬜ Feature Flag System einrichten
3. ⬜ Parallele Routes `/beta/*` erstellen
4. ⬜ Einen Component nach dem anderen migrieren
5. ⬜ Testen mit echten Nutzern (10%)
6. ⬜ Gradual Rollout
7. ⬜ Alte Components entfernen
### Option 2: Direkte Migration (Schnell, 3-5 Tage)
1. ✅ Backend Models & Repository erstellt
2. ⬜ Alle Components in einem PR umstellen
3. ⬜ Accessor für Backward-Compatibility hinzufügen
4. ⬜ Intensives Testing
5. ⬜ Deploy mit Rollback-Plan
### Option 3: Hybrid (Empfohlen, 1 Woche)
1. ✅ Backend Models & Repository erstellt
2. ⬜ Accessor für Backward-Compatibility in Backend Models
3. ⬜ `transaction-review` Component migrieren (wichtigster)
4. ⬜ 1-2 Tage Testing
5. ⬜ Restliche Components migrieren
6. ⬜ Deploy
---
## 📝 Checkliste für Component-Migration
Für jeden Component:
- [ ] Import `App\Models\Transaction``App\Models\Backend\Transaction`
- [ ] Import `App\Models\Company` entfernen
- [ ] Repository injecten statt direkte Model-Queries
- [ ] `company->` zu `getCompanyInfo()` ändern
- [ ] Field-Namen anpassen (`amount``tx_amount`, etc.)
- [ ] `executed_at` zu `tx_date` ändern
- [ ] `->format()` Calls bei `tx_date` entfernen (ist schon Text)
- [ ] Tests schreiben/anpassen
- [ ] Manuell testen
- [ ] PR erstellen
---
## 🚀 Los geht's!
Möchten Sie:
**A)** Dass ich jetzt `transaction-review.blade.php` auf Backend-Schema umstelle?
**B)** Erst Accessors für Backward-Compatibility hinzufügen?
**C)** Ein Feature-Flag-System einrichten?
**D)** Etwas anderes?
Was ist Ihr bevorzugter Ansatz?
-965
View File
@@ -1,965 +0,0 @@
# Inkrementelle Migrations-Strategie: Parallele Schema-Integration
## Konzept: Strangler Fig Pattern
Diese Strategie nutzt das **Strangler Fig Pattern** - das Backend-Schema wird parallel integriert und schrittweise übernimmt die Logik, während das alte System weiterläuft. Kein Big Bang, keine Breaking Changes.
```
Phase 1: Beide Systeme parallel
┌─────────────────┐ ┌─────────────────┐
│ Public Schema │ │ Backend Schema │
│ (Aktiv) │ │ (Read-Only) │
└─────────────────┘ └─────────────────┘
Phase 2: Dual-Write Pattern
┌─────────────────┐ ┌─────────────────┐
│ Public Schema │────▶│ Backend Schema │
│ (Primary) │ │ (Secondary) │
└─────────────────┘ └─────────────────┘
Phase 3: Umstellung
┌─────────────────┐ ┌─────────────────┐
│ Public Schema │◀────│ Backend Schema │
│ (Read-Only) │ │ (Primary) │
└─────────────────┘ └─────────────────┘
Phase 4: Deprecation
┌─────────────────┐
│ Backend Schema │
│ (Einzige Quelle)│
└─────────────────┘
```
---
## Phase 1: Dual-Connection Setup (1-2 Tage)
### 1.1 Database-Konfiguration erweitern
**config/database.php**:
```php
<?php
return [
'default' => env('DB_CONNECTION', 'pgsql'),
'connections' => [
// Bestehende Public-Schema Verbindung
'pgsql' => [
'driver' => 'pgsql',
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '5432'),
'database' => env('DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
'charset' => 'utf8',
'prefix' => '',
'prefix_indexes' => true,
'search_path' => 'public',
'sslmode' => 'prefer',
],
// Neue Backend-Schema Verbindung
'backend' => [
'driver' => 'pgsql',
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '5432'),
'database' => env('DB_DATABASE', 'forge'), // Gleiche DB
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
'charset' => 'utf8',
'prefix' => '',
'prefix_indexes' => true,
'search_path' => 'backend', // Unterschiedliches Schema!
'sslmode' => 'prefer',
],
],
];
```
**.env**:
```env
# Keine Änderung nötig - beide Connections nutzen gleiche Credentials
DB_CONNECTION=pgsql
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=your_database
DB_USERNAME=your_user
DB_PASSWORD=your_password
```
### 1.2 Backend Models erstellen
**app/Models/Backend/Company.php**:
```php
<?php
namespace App\Models\Backend;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Company extends Model
{
use HasFactory;
protected $connection = 'backend';
protected $table = 'companies';
protected $fillable = [
'name',
'legal_name',
'ticker',
'sector',
'country',
'headquarters',
'kyc_risk_level',
'summary',
];
public function transactions(): HasMany
{
return $this->hasMany(Transaction::class);
}
/**
* Sync zu public.companies (während Migration)
*/
public function syncToPublic(): \App\Models\Company
{
return \App\Models\Company::updateOrCreate(
['id' => $this->id],
$this->only($this->fillable)
);
}
}
```
**app/Models/Backend/Transaction.php**:
```php
<?php
namespace App\Models\Backend;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Transaction extends Model
{
use HasFactory;
protected $connection = 'backend';
protected $table = 'transactions';
public const UPDATED_AT = 'last_modified_at';
protected $fillable = [
'corporate_entity',
'corporate_counterparty',
'tx_date',
'tx_amount',
'tx_currency',
'tx_purpose',
'tx_country_outgoing',
'tx_country_incoming',
'source_file',
'raw_payload',
'status',
];
protected $casts = [
'tx_amount' => 'decimal:2',
// tx_date ist als TEXT gespeichert - später migrieren zu timestamp
];
public function company(): BelongsTo
{
return $this->belongsTo(Company::class);
}
/**
* Konvertiere zu public.transactions Format
*/
public function toPublicFormat(): array
{
return [
'company_id' => $this->company_id,
'reference' => $this->id, // oder generiere unique reference
'amount' => $this->tx_amount,
'currency' => $this->tx_currency,
'counterparty' => $this->corporate_counterparty,
'counterparty_country' => $this->tx_country_incoming,
'executed_at' => $this->tx_date,
'status' => $this->status,
'requires_review' => true,
];
}
}
```
### 1.3 Test der Dual-Connection
```php
<?php
// tests/Feature/DualConnectionTest.php
use App\Models\Backend\Company as BackendCompany;
use App\Models\Company as PublicCompany;
test('can access both schemas', function () {
// Public Schema
$publicCount = PublicCompany::count();
expect($publicCount)->toBeGreaterThan(0);
// Backend Schema
$backendCount = BackendCompany::count();
expect($backendCount)->toBeGreaterThanOrEqual(0);
});
test('connections are isolated', function () {
$public = PublicCompany::first();
$backend = BackendCompany::first();
// Verschiedene Connections
expect($public->getConnectionName())->toBe('pgsql');
expect($backend->getConnectionName())->toBe('backend');
});
```
---
## Phase 2: Abstraction Layer (2-3 Tage)
### 2.1 Repository Pattern mit Feature Flags
**app/Repositories/CompanyRepository.php**:
```php
<?php
namespace App\Repositories;
use App\Models\Backend\Company as BackendCompany;
use App\Models\Company as PublicCompany;
use Illuminate\Database\Eloquent\Collection;
class CompanyRepository
{
public function __construct(
private bool $useBackendSchema = false
) {
// Feature Flag aus Config
$this->useBackendSchema = config('features.use_backend_schema', false);
}
public function all(): Collection
{
return $this->useBackendSchema
? BackendCompany::all()
: PublicCompany::all();
}
public function find(int $id): PublicCompany|BackendCompany|null
{
return $this->useBackendSchema
? BackendCompany::find($id)
: PublicCompany::find($id);
}
public function create(array $data): PublicCompany|BackendCompany
{
if ($this->useBackendSchema) {
$company = BackendCompany::create($data);
// Dual-Write: Sync zu Public während Übergangsphase
if (config('features.dual_write', true)) {
$company->syncToPublic();
}
return $company;
}
return PublicCompany::create($data);
}
public function update(int $id, array $data): bool
{
$company = $this->find($id);
if (!$company) {
return false;
}
$result = $company->update($data);
// Dual-Write
if ($this->useBackendSchema && config('features.dual_write', true)) {
$company->syncToPublic();
}
return $result;
}
public function delete(int $id): bool
{
$company = $this->find($id);
if (!$company) {
return false;
}
// Dual-Delete
if ($this->useBackendSchema && config('features.dual_write', true)) {
PublicCompany::destroy($id);
}
return $company->delete();
}
/**
* Helper: Get model class
*/
public function getModelClass(): string
{
return $this->useBackendSchema
? BackendCompany::class
: PublicCompany::class;
}
}
```
**app/Repositories/TransactionRepository.php**:
```php
<?php
namespace App\Repositories;
use App\Models\Backend\Transaction as BackendTransaction;
use App\Models\Transaction as PublicTransaction;
use Illuminate\Database\Eloquent\Collection;
class TransactionRepository
{
public function __construct(
private bool $useBackendSchema = false
) {
$this->useBackendSchema = config('features.use_backend_schema', false);
}
public function forCompany(int $companyId): Collection
{
return $this->useBackendSchema
? BackendTransaction::where('company_id', $companyId)->get()
: PublicTransaction::where('company_id', $companyId)->get();
}
public function requiresReview(): Collection
{
if ($this->useBackendSchema) {
// Backend hat kein requires_review Feld - nutze status
return BackendTransaction::where('status', 'pending')->get();
}
return PublicTransaction::where('requires_review', true)->get();
}
public function highRisk(int $threshold = 70): Collection
{
if ($this->useBackendSchema) {
// Backend hat keinen risk_score - Alternative Logik
return BackendTransaction::where('status', 'flagged')->get();
}
return PublicTransaction::where('risk_score', '>=', $threshold)->get();
}
}
```
### 2.2 Feature Flag Konfiguration
**config/features.php**:
```php
<?php
return [
/*
|--------------------------------------------------------------------------
| Backend Schema Migration Flags
|--------------------------------------------------------------------------
*/
// Hauptschalter: Nutze Backend-Schema statt Public
'use_backend_schema' => env('FEATURE_USE_BACKEND_SCHEMA', false),
// Dual-Write: Schreibe in beide Schemas während Migration
'dual_write' => env('FEATURE_DUAL_WRITE', true),
// Read-Verification: Vergleiche Reads aus beiden Schemas (Logging)
'verify_reads' => env('FEATURE_VERIFY_READS', false),
// Schrittweise Migration pro Bereich
'backend_schema_areas' => [
'companies' => env('FEATURE_BACKEND_COMPANIES', false),
'transactions' => env('FEATURE_BACKEND_TRANSACTIONS', false),
'reports' => env('FEATURE_BACKEND_REPORTS', false),
],
];
```
**.env** (für schrittweise Aktivierung):
```env
# Phase 1: Beide Schemas verfügbar, aber Public aktiv
FEATURE_USE_BACKEND_SCHEMA=false
FEATURE_DUAL_WRITE=false
# Phase 2: Dual-Write aktivieren
# FEATURE_USE_BACKEND_SCHEMA=false
# FEATURE_DUAL_WRITE=true
# Phase 3: Backend als Primary, Public als Fallback
# FEATURE_USE_BACKEND_SCHEMA=true
# FEATURE_DUAL_WRITE=true
# Phase 4: Nur Backend
# FEATURE_USE_BACKEND_SCHEMA=true
# FEATURE_DUAL_WRITE=false
```
### 2.3 Service Provider für Dependency Injection
**app/Providers/RepositoryServiceProvider.php**:
```php
<?php
namespace App\Providers;
use App\Repositories\CompanyRepository;
use App\Repositories\TransactionRepository;
use Illuminate\Support\ServiceProvider;
class RepositoryServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->singleton(CompanyRepository::class, function ($app) {
return new CompanyRepository(
useBackendSchema: config('features.use_backend_schema', false)
);
});
$this->app->singleton(TransactionRepository::class, function ($app) {
return new TransactionRepository(
useBackendSchema: config('features.use_backend_schema', false)
);
});
}
}
```
**bootstrap/providers.php**:
```php
<?php
return [
App\Providers\AppServiceProvider::class,
App\Providers\RepositoryServiceProvider::class, // Neu hinzufügen
];
```
---
## Phase 3: Controller Migration (3-5 Tage)
### 3.1 Controller auf Repository umstellen
**Vorher** (app/Http/Controllers/CompanyController.php):
```php
<?php
namespace App\Http\Controllers;
use App\Models\Company;
use Illuminate\Http\Request;
class CompanyController extends Controller
{
public function index()
{
$companies = Company::with('transactions')->get();
return view('companies.index', compact('companies'));
}
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string',
'country' => 'required|string|size:2',
// ...
]);
$company = Company::create($validated);
return redirect()->route('companies.show', $company);
}
}
```
**Nachher**:
```php
<?php
namespace App\Http\Controllers;
use App\Repositories\CompanyRepository;
use Illuminate\Http\Request;
class CompanyController extends Controller
{
public function __construct(
private CompanyRepository $companies
) {}
public function index()
{
$companies = $this->companies->all();
return view('companies.index', compact('companies'));
}
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string',
'country' => 'required|string|size:2',
// ...
]);
$company = $this->companies->create($validated);
return redirect()->route('companies.show', $company);
}
}
```
### 3.2 Volt/Livewire Components anpassen
**Vorher** (resources/views/pages/companies/index.blade.php):
```php
<?php
use App\Models\Company;
$companies = Company::query()
->with('transactions')
->orderBy('name')
->get();
?>
<div>
@foreach($companies as $company)
<flux:card>{{ $company->name }}</flux:card>
@endforeach
</div>
```
**Nachher**:
```php
<?php
use App\Repositories\CompanyRepository;
$companyRepo = app(CompanyRepository::class);
$companies = $companyRepo->all();
?>
<div>
@foreach($companies as $company)
<flux:card>{{ $company->name }}</flux:card>
@endforeach
</div>
```
---
## Phase 4: Monitoring & Verification (Parallel zu Phase 3)
### 4.1 Dual-Read Verification Middleware
**app/Http/Middleware/VerifyDualSchemaReads.php**:
```php
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
class VerifyDualSchemaReads
{
public function handle(Request $request, Closure $next)
{
if (!config('features.verify_reads')) {
return $next($request);
}
// Capture queries von beiden Schemas
$publicQueries = [];
$backendQueries = [];
\DB::connection('pgsql')->listen(function ($query) use (&$publicQueries) {
$publicQueries[] = $query->sql;
});
\DB::connection('backend')->listen(function ($query) use (&$backendQueries) {
$backendQueries[] = $query->sql;
});
$response = $next($request);
// Log für Analyse
if (!empty($publicQueries) || !empty($backendQueries)) {
Log::channel('migration')->info('Dual Schema Access', [
'route' => $request->path(),
'public_queries' => count($publicQueries),
'backend_queries' => count($backendQueries),
]);
}
return $response;
}
}
```
### 4.2 Health Check Command
**app/Console/Commands/VerifySchemaConsistency.php**:
```php
<?php
namespace App\Console\Commands;
use App\Models\Backend\Company as BackendCompany;
use App\Models\Company as PublicCompany;
use Illuminate\Console\Command;
class VerifySchemaConsistency extends Command
{
protected $signature = 'schema:verify-consistency';
protected $description = 'Verify data consistency between public and backend schemas';
public function handle(): int
{
$this->info('Checking schema consistency...');
// Company Count
$publicCount = PublicCompany::count();
$backendCount = BackendCompany::count();
$this->table(
['Schema', 'Companies', 'Status'],
[
['Public', $publicCount, '✓'],
['Backend', $backendCount, $backendCount === $publicCount ? '✓' : '⚠'],
]
);
if ($backendCount !== $publicCount) {
$this->warn("Company count mismatch: Public={$publicCount}, Backend={$backendCount}");
}
// Sample Data Verification
$sampleSize = min(10, $publicCount);
$publicSample = PublicCompany::take($sampleSize)->get();
$mismatches = 0;
foreach ($publicSample as $publicCompany) {
$backendCompany = BackendCompany::find($publicCompany->id);
if (!$backendCompany) {
$this->warn("Company {$publicCompany->id} missing in backend");
$mismatches++;
continue;
}
if ($publicCompany->name !== $backendCompany->name) {
$this->warn("Company {$publicCompany->id} name mismatch");
$mismatches++;
}
}
if ($mismatches === 0) {
$this->info('✓ All consistency checks passed!');
return self::SUCCESS;
}
$this->error("✗ Found {$mismatches} inconsistencies");
return self::FAILURE;
}
}
```
---
## Phase 5: Graduelle Umstellung (2-4 Wochen)
### Woche 1: Read-Only Access
```env
FEATURE_USE_BACKEND_SCHEMA=false
FEATURE_DUAL_WRITE=false
FEATURE_VERIFY_READS=true
```
- ✅ Backend-Schema ist verfügbar
- ✅ Monitoring läuft
- ✅ Keine Produktions-Daten betroffen
### Woche 2: Dual-Write aktivieren
```env
FEATURE_USE_BACKEND_SCHEMA=false # Lesen: Public
FEATURE_DUAL_WRITE=true # Schreiben: Beide
FEATURE_VERIFY_READS=true
```
- ✅ Neue Daten gehen in beide Schemas
- ✅ Public bleibt Primary
- ⚠️ Monitor auf Sync-Errors
### Woche 3: Backend als Primary (Canary)
```env
# Nur für 10% Traffic oder spezifische Routes
FEATURE_BACKEND_COMPANIES=true # Companies von Backend lesen
FEATURE_BACKEND_TRANSACTIONS=false # Transactions noch von Public
FEATURE_DUAL_WRITE=true
```
- ✅ Schrittweise Umstellung pro Feature
- ✅ A/B Testing möglich
- ✅ Rollback jederzeit möglich
### Woche 4: Full Switchover
```env
FEATURE_USE_BACKEND_SCHEMA=true # Lesen: Backend
FEATURE_DUAL_WRITE=true # Schreiben: Beide (Sicherheit)
```
- ✅ Backend ist Primary
- ✅ Public als Safety Net
### Nach 2 Wochen stabiler Betrieb:
```env
FEATURE_USE_BACKEND_SCHEMA=true
FEATURE_DUAL_WRITE=false # Public wird deprecated
```
---
## Phase 6: Testing-Strategie
### 6.1 Feature Tests mit Feature Flags
```php
<?php
// tests/Feature/CompanyControllerTest.php
use App\Models\Backend\Company as BackendCompany;
use App\Models\Company as PublicCompany;
test('can create company with public schema', function () {
config(['features.use_backend_schema' => false]);
$response = $this->post('/companies', [
'name' => 'Test Corp',
'country' => 'DE',
]);
$response->assertRedirect();
expect(PublicCompany::where('name', 'Test Corp')->exists())->toBeTrue();
});
test('can create company with backend schema', function () {
config(['features.use_backend_schema' => true]);
$response = $this->post('/companies', [
'name' => 'Test Corp Backend',
'country' => 'DE',
]);
$response->assertRedirect();
expect(BackendCompany::where('corporate_entity', 'Test Corp Backend')->exists())->toBeTrue();
});
test('dual write creates in both schemas', function () {
config([
'features.use_backend_schema' => true,
'features.dual_write' => true,
]);
$response = $this->post('/companies', [
'name' => 'Dual Write Test',
'country' => 'DE',
]);
$response->assertRedirect();
expect(BackendCompany::where('corporate_entity', 'Dual Write Test')->exists())->toBeTrue();
expect(PublicCompany::where('name', 'Dual Write Test')->exists())->toBeTrue();
});
```
### 6.2 Performance Tests
```php
<?php
// tests/Performance/SchemaPerformanceTest.php
test('backend schema is not slower than public', function () {
// Public Schema
$start = microtime(true);
config(['features.use_backend_schema' => false]);
app(CompanyRepository::class)->all();
$publicTime = microtime(true) - $start;
// Backend Schema
$start = microtime(true);
config(['features.use_backend_schema' => true]);
app(CompanyRepository::class)->all();
$backendTime = microtime(true) - $start;
// Backend sollte nicht mehr als 20% langsamer sein
expect($backendTime)->toBeLessThan($publicTime * 1.2);
});
```
---
## Phase 7: Cleanup & Optimization (Nach 4-6 Wochen)
### 7.1 Repository vereinfachen
Wenn Backend-Schema stabil läuft:
```php
<?php
namespace App\Repositories;
use App\Models\Backend\Company;
class CompanyRepository
{
// Feature Flags entfernen
public function all()
{
return Company::all();
}
// Dual-Write Code entfernen
public function create(array $data)
{
return Company::create($data);
}
}
```
### 7.2 Public Schema deprecaten
```sql
-- Tabellen in _deprecated Schema verschieben
CREATE SCHEMA IF NOT EXISTS _deprecated;
ALTER TABLE public.companies SET SCHEMA _deprecated;
ALTER TABLE public.transactions SET SCHEMA _deprecated;
-- Optional: Views für Legacy-Support
CREATE VIEW public.companies AS
SELECT
id,
corporate_entity as name,
NULL as legal_name,
country,
'medium' as kyc_risk_level,
NULL as summary,
created_at::timestamp,
last_modified_at::timestamp as updated_at
FROM backend.companies;
```
---
## Vorteile dieser Strategie
### ✅ Zero Downtime
- Keine Breaking Changes
- Rollback jederzeit möglich
- Schrittweise Umstellung
### ✅ Sicherheit
- Dual-Write als Safety Net
- Continuous Verification
- Feature Flags für granulare Kontrolle
### ✅ Flexibilität
- A/B Testing möglich
- Schrittweise Migration pro Feature
- Team kann parallel arbeiten
### ✅ Lernkurve
- Team lernt neues Schema schrittweise
- Bugs können isoliert gefunden werden
- Keine Hektik
---
## Zeitplan (Realistisch)
| Woche | Phase | Aufwand | Risiko |
|-------|-------|---------|--------|
| 1 | Setup & Backend Models | 2-3 Tage | Niedrig |
| 2 | Repository Pattern | 2-3 Tage | Niedrig |
| 3-4 | Controller Migration | 5-7 Tage | Mittel |
| 5 | Read-Only Testing | 2-3 Tage | Niedrig |
| 6 | Dual-Write Phase | 1 Woche | Mittel |
| 7-8 | Gradual Switchover | 2 Wochen | Mittel |
| 9-10 | Monitoring & Stabilisierung | 2 Wochen | Niedrig |
| 11-12 | Cleanup | 1 Woche | Niedrig |
**Total**: 10-12 Wochen (inkl. Buffer)
---
## Nächste konkrete Schritte
### Schritt 1: Database Config (heute, 30 Min)
```bash
# config/database.php erweitern
# .env bleibt unverändert
php artisan config:clear
php artisan tinker
>>> DB::connection('backend')->select('SELECT 1')
```
### Schritt 2: Backend Models (heute, 1-2 Std)
```bash
# Models erstellen
mkdir -p app/Models/Backend
# Company.php & Transaction.php erstellen
```
### Schritt 3: Erster Test (heute, 30 Min)
```bash
php artisan test --filter=DualConnectionTest
```
### Schritt 4: Feature Flags (morgen, 1-2 Std)
```bash
# config/features.php erstellen
# Repository Pattern implementieren
```
Möchten Sie, dass ich mit **Schritt 1-2 beginne** und die konkrete Implementierung starte?
-86
View File
@@ -1,86 +0,0 @@
# MCP-Server Aktivierung und Tests
## Aktivierung der globalen MCP-Server
### Methode 1: @-Erwähnung im Chat
Schreibe einfach den Server-Namen mit @ in deiner Nachricht:
- `@sqlite`
- `@postgresql`
- `@bear`
- `@MCP_DOCKER`
- `@Ref`
### Methode 2: CLI Flag beim Start
```bash
claude --mcp-config ~/.claude.json
```
### Methode 3: Beide Configs kombinieren
```bash
claude --mcp-config .mcp.json ~/.claude.json
```
## Test-Befehle für jeden Server
### 1. SQLite Server
**Datenbank:** `/Users/sebastianfrohlich/Downloads/company.db`
Nach Aktivierung mit `@sqlite`:
```
Bitte zeige mir alle Tabellen in der SQLite-Datenbank
```
### 2. PostgreSQL Server
**Verbindung:** localhost:5433, DB: risk_ingest_db
Nach Aktivierung mit `@postgresql`:
```
Bitte zeige mir das Schema der PostgreSQL-Datenbank risk_ingest_db
```
### 3. Bear Notes Server
**Pfad:** `/Users/sebastianfrohlich/Projekte/bear-notes-mcp`
Nach Aktivierung mit `@bear`:
```
Erstelle eine neue Bear-Notiz mit dem Titel "Test MCP Server"
```
### 4. MCP_DOCKER Server
**Command:** `docker mcp gateway run`
Nach Aktivierung mit `@MCP_DOCKER`:
```
Zeige mir die verfügbaren Docker-Container
```
### 5. Ref.tools Server
**URL:** https://api.ref.tools/mcp
Nach Aktivierung mit `@Ref`:
```
Nutze Ref.tools um [spezifische Aufgabe]
```
## Debugging
### Server-Status prüfen
```bash
claude mcp list
```
### MCP-Debug-Modus aktivieren
```bash
claude --mcp-debug
```
### Server-Logs anzeigen
Prüfe die Logs in:
- `~/.claude/logs/`
## Hinweise
- Globale Server aus `~/.claude.json` sind standardmäßig nicht in jeder Session geladen
- Projekt-Server aus `.mcp.json` werden automatisch geladen
- @-Erwähnung ist die einfachste Methode zur Ad-hoc-Aktivierung
- Einige Server benötigen laufende Dienste (z.B. PostgreSQL muss auf Port 5433 laufen)
-801
View File
@@ -1,801 +0,0 @@
# Migrationsplan: Schema-Restrukturierung
## Zielsetzung
### Aktueller Zustand
```
public.companies (11 Spalten)
↓ 1:N
public.transactions (49 Spalten) - Angereicherte Produktionsdaten
backend.transactions (14 Spalten) - Rohdaten
↓ N:M
backend.transaction_outputs (5 Spalten) - AI-generierte Outputs
```
### Ziel-Zustand
```
backend.transactions (14 Spalten) - Ersetzt public.companies
↓ 1:N
public.transactions (5 Spalten, ähnlich backend.transaction_outputs) - Vereinfacht
```
---
## ⚠️ Klärungsfragen (KRITISCH)
Bevor wir fortfahren, müssen folgende Fragen geklärt werden:
### 1. Companies → backend.transactions Mapping
**Problem**: Die Strukturen sind sehr unterschiedlich
| public.companies | backend.transactions | Kompatibilität |
|------------------|---------------------|----------------|
| id (bigint) | id (integer) | ⚠️ Typ-Unterschied |
| name | corporate_entity | ✅ Ähnlich |
| legal_name | - | ❌ Fehlt |
| ticker | - | ❌ Fehlt |
| sector | - | ❌ Fehlt |
| country | tx_country_outgoing? | ⚠️ Unklar |
| headquarters | - | ❌ Fehlt |
| kyc_risk_level | - | ❌ Fehlt |
| summary | - | ❌ Fehlt |
| - | corporate_counterparty | ❌ Neu |
| - | tx_date, tx_amount, tx_currency | ❌ Neu |
| - | tx_purpose | ❌ Neu |
| - | source_file, raw_payload | ❌ Neu |
| - | status | ❌ Neu |
**Frage**:
- Soll `backend.transactions` erweitert werden, um Company-Felder aufzunehmen?
- Oder sollen Companies als einzelne Zeilen ohne Transaktionsdaten gespeichert werden?
- Wie wird `corporate_entity` zu `companies.name` gemappt?
### 2. public.transactions → transaction_outputs Mapping
**Problem**: Drastischer Datenverlust bei Vereinfachung
| public.transactions (49 Felder) | backend.transaction_outputs (5 Felder) |
|---------------------------------|----------------------------------------|
| Alle Core-Felder (14) | ❌ Verloren |
| Alle Enrichment-Felder (34) | ❌ Verloren |
| - | transaction_id (Foreign Key) |
| - | prompt_id (Welcher?) |
| - | output_key (Welcher Typ?) |
| - | content (Wie strukturiert?) |
| - | run_id (Optional) |
**Frage**:
- Welche Daten aus den 49 Feldern sollen in `content` serialisiert werden?
- Welchen `output_key` verwenden wir? (z.B. "transaction_data", "risk_assessment"?)
- Welchen `prompt_id` verwenden wir? (Muss in `backend.prompt_templates` existieren)
- Was passiert mit den 11 Enrichment-Datenquellen?
### 3. Relationship & Foreign Keys
**Problem**: Beziehungen ändern sich fundamental
**Aktuell**:
```
companies.id → transactions.company_id (1:N)
```
**Ziel** (unklar):
```
backend.transactions.id → public.transactions.transaction_id (1:N)?
```
**Frage**:
- Bleibt die 1:N Beziehung erhalten?
- Wie wird `transaction_id` in der neuen `public.transactions` gemappt?
---
## Vorgeschlagene Alternative: Erweiterte Migration
Ich schlage eine modifizierte Zielstruktur vor, die Datenverlust minimiert:
### Option A: Erweitere backend.transactions (Empfohlen)
```sql
backend.companies (neue Tabelle)
- id (integer)
- name (text)
- legal_name (text, nullable)
- country (text)
- kyc_risk_level (text)
- ... weitere Company-Felder
backend.transactions (erweitert, bleibt)
- id (integer)
- company_id (integer FK → backend.companies)
- corporate_counterparty (text)
- tx_date (text → sollte timestamp werden)
- tx_amount (double precision)
- tx_currency (text)
- ... bestehende Felder
public.transaction_enrichments (neue Tabelle)
- id (bigint)
- transaction_id (integer FK → backend.transactions)
- enrichment_type (varchar) -- 'registry', 'sanctions', etc.
- data (jsonb)
- last_refreshed_at (timestamp)
- created_at, updated_at
```
**Vorteile**:
- ✅ Kein Datenverlust
- ✅ Klare Trennung: Companies, Transactions, Enrichments
- ✅ Backend-Schema behält Rohdaten
- ✅ Public-Schema behält angereicherte Daten
- ✅ Laravel-Logik kann schrittweise migriert werden
### Option B: Vollständiger Umzug zu backend Schema
```sql
backend.companies (neu)
- Alle Felder von public.companies
backend.transactions (bleibt)
- Bestehende Struktur
backend.transaction_enrichments (neu)
- Alle Enrichment-Daten aus public.transactions
public.* (deprecated, später löschen)
```
**Vorteile**:
- ✅ Alles im backend Schema
- ✅ Klare Schema-Trennung
- ❌ Laravel-App muss komplett umgeschrieben werden
- ❌ Größere Breaking Changes
---
## Migrationsplan (nach Klärung)
### Phase 1: Vorbereitung (1-2 Tage)
#### 1.1 Backup erstellen
```bash
# Vollständiges Backup
pg_dump -h localhost -U username -d database_name > backup_$(date +%Y%m%d_%H%M%S).sql
# Schema-spezifische Backups
pg_dump -h localhost -U username -d database_name -n public > backup_public_$(date +%Y%m%d).sql
pg_dump -h localhost -U username -d database_name -n backend > backup_backend_$(date +%Y%m%d).sql
```
#### 1.2 Datenanalyse
```sql
-- Anzahl Companies
SELECT COUNT(*) FROM public.companies;
-- Anzahl Transactions
SELECT COUNT(*) FROM public.transactions;
-- Datenintegrität prüfen
SELECT
COUNT(*) as total_transactions,
COUNT(DISTINCT company_id) as unique_companies,
COUNT(*) FILTER (WHERE company_id NOT IN (SELECT id FROM public.companies)) as orphaned_transactions
FROM public.transactions;
-- Enrichment-Daten Analyse
SELECT
COUNT(*) FILTER (WHERE registry_data IS NOT NULL) as has_registry,
COUNT(*) FILTER (WHERE sanctions_data IS NOT NULL) as has_sanctions,
COUNT(*) FILTER (WHERE pep_data IS NOT NULL) as has_pep
FROM public.transactions;
```
#### 1.3 Test-Umgebung aufsetzen
```bash
# Kopie der Datenbank für Tests
createdb -T production_db test_migration_db
```
### Phase 2: Schema-Erweiterung (2-3 Tage)
#### 2.1 backend.companies erstellen
**Laravel Migration**:
```php
<?php
// database/migrations/2025_11_12_create_backend_companies_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::connection('backend')->create('companies', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('legal_name')->nullable();
$table->string('ticker')->nullable();
$table->string('sector')->nullable();
$table->string('country', 2)->default('DE');
$table->string('headquarters')->nullable();
$table->string('kyc_risk_level')->default('medium');
$table->text('summary')->nullable();
$table->timestamps();
$table->index('name');
});
}
public function down(): void
{
Schema::connection('backend')->dropIfExists('companies');
}
};
```
#### 2.2 backend.transactions erweitern
**SQL Migration** (wenn Laravel Multi-Schema-Support limitiert ist):
```sql
-- Füge company_id zu backend.transactions hinzu
ALTER TABLE backend.transactions
ADD COLUMN company_id INTEGER REFERENCES backend.companies(id) ON DELETE CASCADE;
-- Index für Performance
CREATE INDEX idx_backend_transactions_company_id ON backend.transactions(company_id);
```
#### 2.3 public.transaction_enrichments erstellen
**Laravel Migration**:
```php
<?php
// database/migrations/2025_11_12_create_transaction_enrichments_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('transaction_enrichments', function (Blueprint $table) {
$table->id();
$table->integer('backend_transaction_id'); // FK zu backend.transactions
$table->string('enrichment_type', 50); // 'registry', 'sanctions', etc.
$table->jsonb('data');
$table->timestamp('last_refreshed_at')->nullable();
$table->timestamps();
$table->index(['backend_transaction_id', 'enrichment_type']);
$table->unique(['backend_transaction_id', 'enrichment_type']);
});
}
public function down(): void
{
Schema::dropIfExists('transaction_enrichments');
}
};
```
### Phase 3: Datenmigration (3-5 Tage)
#### 3.1 Companies migrieren
```php
<?php
// database/migrations/2025_11_12_migrate_companies_data.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
public function up(): void
{
// Kopiere Companies von public → backend
DB::statement("
INSERT INTO backend.companies (
id, name, legal_name, ticker, sector,
country, headquarters, kyc_risk_level, summary,
created_at, updated_at
)
SELECT
id, name, legal_name, ticker, sector,
country, headquarters, kyc_risk_level, summary,
created_at, updated_at
FROM public.companies
ON CONFLICT (id) DO NOTHING
");
// Aktualisiere Sequence
DB::statement("
SELECT setval('backend.companies_id_seq',
(SELECT MAX(id) FROM backend.companies)
)
");
}
public function down(): void
{
DB::statement("TRUNCATE backend.companies CASCADE");
}
};
```
#### 3.2 Transaction Core-Daten migrieren
```php
<?php
// database/migrations/2025_11_12_migrate_transaction_core_data.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
public function up(): void
{
// Füge company_id zu bestehenden backend.transactions hinzu
// (falls diese Daten schon existieren und verknüpft werden müssen)
// ODER: Migriere von public.transactions → backend.transactions
DB::statement("
INSERT INTO backend.transactions (
corporate_entity,
corporate_counterparty,
tx_date,
tx_amount,
tx_currency,
tx_purpose,
tx_country_incoming,
company_id,
status,
created_at,
last_modified_at
)
SELECT
c.name as corporate_entity,
t.counterparty as corporate_counterparty,
t.executed_at::text as tx_date,
t.amount as tx_amount,
t.currency as tx_currency,
t.flagged_reason as tx_purpose,
t.counterparty_country as tx_country_incoming,
t.company_id,
t.status,
t.created_at::text,
t.updated_at::text
FROM public.transactions t
JOIN public.companies c ON t.company_id = c.id
WHERE NOT EXISTS (
SELECT 1 FROM backend.transactions bt
WHERE bt.corporate_entity = c.name
AND bt.tx_date = t.executed_at::text
)
");
}
public function down(): void
{
// Rollback nur für migrierte Daten
DB::statement("
DELETE FROM backend.transactions
WHERE company_id IS NOT NULL
");
}
};
```
#### 3.3 Enrichment-Daten migrieren
```php
<?php
// database/migrations/2025_11_12_migrate_enrichment_data.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
private array $enrichmentSources = [
'registry' => ['registry_data', 'registry_last_refreshed_at'],
'genesis' => ['genesis_context', 'genesis_last_refreshed_at'],
'govdata' => ['govdata_data', 'govdata_last_refreshed_at'],
'bundesanzeiger' => ['bundesanzeiger_data', 'bundesanzeiger_last_refreshed_at'],
'insolvency' => ['insolvency_data', 'insolvency_last_refreshed_at'],
'rss' => ['rss_alerts', 'rss_last_refreshed_at'],
'sanctions' => ['sanctions_data', 'sanctions_last_refreshed_at'],
'pep' => ['pep_data', 'pep_last_refreshed_at'],
'gleif' => ['gleif_data', 'gleif_last_refreshed_at'],
'eu_sanctions' => ['eu_sanctions_data', 'eu_sanctions_last_refreshed_at'],
'handelsregister' => ['handelsregister_data', 'handelsregister_last_refreshed_at'],
];
public function up(): void
{
foreach ($this->enrichmentSources as $type => [$dataField, $refreshField]) {
DB::statement("
INSERT INTO public.transaction_enrichments (
backend_transaction_id,
enrichment_type,
data,
last_refreshed_at,
created_at,
updated_at
)
SELECT
bt.id as backend_transaction_id,
'{$type}' as enrichment_type,
t.{$dataField} as data,
t.{$refreshField} as last_refreshed_at,
NOW(),
NOW()
FROM public.transactions t
JOIN backend.transactions bt ON (
bt.tx_date = t.executed_at::text
AND bt.tx_amount = t.amount
)
WHERE t.{$dataField} IS NOT NULL
ON CONFLICT (backend_transaction_id, enrichment_type) DO NOTHING
");
}
}
public function down(): void
{
DB::statement("TRUNCATE public.transaction_enrichments");
}
};
```
### Phase 4: Model-Anpassung (2-3 Tage)
#### 4.1 Neue Models erstellen
**Backend\Company Model**:
```php
<?php
// app/Models/Backend/Company.php
namespace App\Models\Backend;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Company extends Model
{
protected $connection = 'backend';
protected $table = 'companies';
protected $fillable = [
'name', 'legal_name', 'ticker', 'sector',
'country', 'headquarters', 'kyc_risk_level', 'summary'
];
public function transactions(): HasMany
{
return $this->hasMany(Transaction::class);
}
}
```
**Backend\Transaction Model**:
```php
<?php
// app/Models/Backend/Transaction.php
namespace App\Models\Backend;
use App\Models\TransactionEnrichment;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Transaction extends Model
{
protected $connection = 'backend';
protected $table = 'transactions';
protected $fillable = [
'company_id', 'corporate_entity', 'corporate_counterparty',
'tx_date', 'tx_amount', 'tx_currency', 'tx_purpose',
'tx_country_outgoing', 'tx_country_incoming',
'source_file', 'raw_payload', 'status'
];
protected $casts = [
'tx_amount' => 'decimal:2',
];
public function company(): BelongsTo
{
return $this->belongsTo(Company::class);
}
public function enrichments(): HasMany
{
return $this->hasMany(TransactionEnrichment::class, 'backend_transaction_id');
}
public function getEnrichment(string $type): ?array
{
return $this->enrichments()
->where('enrichment_type', $type)
->first()
?->data;
}
}
```
**TransactionEnrichment Model**:
```php
<?php
// app/Models/TransactionEnrichment.php
namespace App\Models;
use App\Models\Backend\Transaction;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class TransactionEnrichment extends Model
{
protected $fillable = [
'backend_transaction_id',
'enrichment_type',
'data',
'last_refreshed_at'
];
protected $casts = [
'data' => 'array',
'last_refreshed_at' => 'datetime'
];
public function transaction(): BelongsTo
{
return $this->belongsTo(Transaction::class, 'backend_transaction_id');
}
}
```
### Phase 5: Code-Refactoring (5-7 Tage)
#### 5.1 Controller anpassen
**Vorher**:
```php
use App\Models\Transaction;
$transactions = Transaction::with('company')
->where('risk_score', '>', 50)
->get();
```
**Nachher**:
```php
use App\Models\Backend\Transaction;
$transactions = Transaction::with(['company', 'enrichments'])
->where('risk_score', '>', 50)
->get();
// Enrichment-Daten abrufen
foreach ($transactions as $transaction) {
$sanctionsData = $transaction->getEnrichment('sanctions');
$pepData = $transaction->getEnrichment('pep');
}
```
#### 5.2 Compatibility Layer (Optional)
```php
<?php
// app/Models/Transaction.php (Legacy-Support)
namespace App\Models;
use App\Models\Backend\Transaction as BackendTransaction;
/**
* @deprecated Use App\Models\Backend\Transaction instead
*/
class Transaction extends BackendTransaction
{
// Proxy zu neuem Model für Backward-Compatibility
public function __get($key)
{
// Prüfe ob Enrichment-Feld
if (str_ends_with($key, '_data')) {
$type = str_replace('_data', '', $key);
return $this->getEnrichment($type);
}
return parent::__get($key);
}
}
```
### Phase 6: Testing (3-5 Tage)
#### 6.1 Unit Tests
```php
<?php
// tests/Unit/Models/Backend/TransactionTest.php
use App\Models\Backend\Transaction;
use App\Models\TransactionEnrichment;
test('transaction has enrichments relationship', function () {
$transaction = Transaction::factory()->create();
TransactionEnrichment::factory()->create([
'backend_transaction_id' => $transaction->id,
'enrichment_type' => 'sanctions',
'data' => ['status' => 'clear']
]);
expect($transaction->enrichments)->toHaveCount(1);
expect($transaction->getEnrichment('sanctions'))->toBe(['status' => 'clear']);
});
```
#### 6.2 Integration Tests
```php
<?php
// tests/Feature/TransactionMigrationTest.php
test('data integrity after migration', function () {
// Vergleiche Anzahl
$oldCount = DB::table('public.transactions')->count();
$newCount = DB::table('backend.transactions')->count();
expect($newCount)->toBeGreaterThanOrEqual($oldCount);
// Vergleiche Enrichment-Daten
$oldEnrichments = DB::table('public.transactions')
->whereNotNull('sanctions_data')
->count();
$newEnrichments = DB::table('public.transaction_enrichments')
->where('enrichment_type', 'sanctions')
->count();
expect($newEnrichments)->toBe($oldEnrichments);
});
```
### Phase 7: Deployment (1-2 Tage)
#### 7.1 Deployment-Schritte
```bash
# 1. Backup
php artisan backup:database
# 2. Migrations ausführen (in Reihenfolge!)
php artisan migrate --path=database/migrations/2025_11_12_create_backend_companies_table.php
php artisan migrate --path=database/migrations/2025_11_12_create_transaction_enrichments_table.php
php artisan migrate --path=database/migrations/2025_11_12_migrate_companies_data.php
php artisan migrate --path=database/migrations/2025_11_12_migrate_transaction_core_data.php
php artisan migrate --path=database/migrations/2025_11_12_migrate_enrichment_data.php
# 3. Verification
php artisan tinker
>>> DB::table('backend.companies')->count()
>>> DB::table('backend.transactions')->count()
>>> DB::table('public.transaction_enrichments')->count()
# 4. Clear caches
php artisan cache:clear
php artisan config:clear
php artisan route:clear
php artisan view:clear
# 5. Run tests
php artisan test --filter=TransactionMigration
```
#### 7.2 Rollback-Plan
```bash
# Falls etwas schief geht
php artisan migrate:rollback --step=5
# Restore from backup
psql -U username -d database_name < backup_20251112_120000.sql
```
### Phase 8: Cleanup (nach 2-4 Wochen Monitoring)
```sql
-- Wenn alles stabil läuft, alte Tabellen entfernen
DROP TABLE public.transactions CASCADE;
DROP TABLE public.companies CASCADE;
-- Views für Backward-Compatibility (optional)
CREATE VIEW public.companies AS
SELECT * FROM backend.companies;
CREATE VIEW public.transactions AS
SELECT
bt.id,
bt.company_id,
bt.corporate_counterparty as counterparty,
bt.tx_amount as amount,
bt.tx_currency as currency,
bt.tx_date::timestamp as executed_at,
bt.status,
bt.created_at::timestamp,
bt.last_modified_at::timestamp as updated_at
FROM backend.transactions bt;
```
---
## Zeitplan
| Phase | Dauer | Abhängigkeiten |
|-------|-------|----------------|
| 1. Vorbereitung | 1-2 Tage | - |
| 2. Schema-Erweiterung | 2-3 Tage | Phase 1 |
| 3. Datenmigration | 3-5 Tage | Phase 2 |
| 4. Model-Anpassung | 2-3 Tage | Phase 3 |
| 5. Code-Refactoring | 5-7 Tage | Phase 4 |
| 6. Testing | 3-5 Tage | Phase 5 |
| 7. Deployment | 1-2 Tage | Phase 6 |
| 8. Cleanup | Nach 2-4 Wochen | Phase 7 |
**Gesamtdauer**: 17-27 Arbeitstage (3-5 Wochen)
---
## Risiken & Mitigation
### Risiko 1: Datenverlust
**Mitigation**:
- Vollständige Backups vor jedem Schritt
- Test-Migration in Staging-Umgebung
- Datenvalidierung nach jeder Phase
### Risiko 2: Downtime
**Mitigation**:
- Migrations während Wartungsfenster
- Blue-Green Deployment
- Read-Replica für Zero-Downtime
### Risiko 3: Performance-Probleme
**Mitigation**:
- Indizes auf Foreign Keys
- Batch-Processing für große Datasets
- Query-Optimierung mit EXPLAIN ANALYZE
### Risiko 4: Code-Inkompatibilität
**Mitigation**:
- Compatibility Layer
- Schrittweises Refactoring
- Feature-Flags für graduelle Umstellung
---
## Nächste Schritte
1. ✅ **Klärung der Fragen oben**
2. ⬜ Detaillierte Datenanalyse durchführen
3. ⬜ Test-Umgebung aufsetzen
4. ⬜ Erste Migration in Staging testen
5. ⬜ Review & Approval vom Team
6. ⬜ Production-Migration planen
---
*Erstellt am: 2025-11-12*
*Status: ENTWURF - Wartet auf Klärung der kritischen Fragen*
@@ -1,332 +0,0 @@
# Detaillierte Beschreibung der Datenbank-Tabellen
## **Backend Schema**
### **1. backend.transactions**
**Zweck**: Rohdaten-Tabelle für eingehende Transaktionen aus verschiedenen Quellen (vermutlich CSV/Excel-Uploads oder API-Imports)
**Struktur**: 14 Spalten
#### Identifikation
- **id** (integer, NOT NULL, AUTO_INCREMENT)
- Primärschlüssel
- Sequenz: `backend.transactions_id_seq`
#### Transaktions-Stammdaten
- **corporate_entity** (text, NOT NULL)
- Name der durchführenden Firma/Entität
- Kein Foreign Key - als Textfeld gespeichert
- **corporate_counterparty** (text, NOT NULL)
- Name der Gegenpartei/Empfänger
- Freitext, keine Normalisierung
- **tx_date** (text, NOT NULL)
- Transaktionsdatum
- ⚠️ Als Text gespeichert (nicht als DATE/TIMESTAMP)
- Wahrscheinlich verschiedene Formate möglich
- **tx_amount** (double precision, NOT NULL)
- Transaktionsbetrag
- Fließkommazahl für Währungsbeträge
- **tx_currency** (text, nullable)
- Währungscode (z.B. EUR, USD)
- Optional
- **tx_purpose** (text, nullable)
- Verwendungszweck/Beschreibung der Transaktion
- Freitextfeld
#### Geografische Informationen
- **tx_country_outgoing** (text, nullable)
- Herkunftsland der Zahlung
- **tx_country_incoming** (text, nullable)
- Zielland der Zahlung
#### Metadaten & Verarbeitung
- **source_file** (text, nullable)
- Name/Pfad der Quelldatei
- Für Nachverfolgbarkeit der Datenherkunft
- **raw_payload** (text, nullable)
- Rohdaten im Originalformat
- Ermöglicht Reprocessing bei Bedarf
- **status** (text, NOT NULL)
- Verarbeitungsstatus (z.B. "pending", "processed", "error")
- **created_at** (text, NOT NULL)
- Erstellungszeitpunkt
- ⚠️ Als Text gespeichert (nicht als TIMESTAMP)
- **last_modified_at** (text, NOT NULL)
- Letzte Änderung
- ⚠️ Als Text gespeichert (nicht als TIMESTAMP)
**Charakteristik**: ETL-/Staging-Tabelle mit lockerer Typisierung für maximale Flexibilität beim Import
---
### **2. backend.transaction_outputs**
**Zweck**: Speichert generierte Outputs/Ergebnisse aus Prompt-Verarbeitung für Transaktionen (vermutlich KI/LLM-generierte Analysen)
**Struktur**: 5 Spalten
#### Primärschlüssel (zusammengesetzt)
- **transaction_id** (integer, NOT NULL)
- Foreign Key zu `backend.transactions.id`
- Referenziert die analysierte Transaktion
- **prompt_id** (integer, NOT NULL)
- Foreign Key zu `backend.prompt_templates` (vermutlich)
- Identifiziert welcher Prompt verwendet wurde
- **output_key** (text, NOT NULL)
- Schlüssel für den Output-Typ
- Beispiele: "risk_assessment", "compliance_check", "summary", "recommendations"
#### Output-Daten
- **content** (text, NOT NULL)
- Der generierte Inhalt/Ergebnis
- Kann strukturierter Text, JSON oder Markdown sein
#### Verknüpfung
- **run_id** (integer, nullable)
- Foreign Key zu `backend.prompt_runs` (vermutlich)
- Gruppiert Outputs aus demselben Batch/Durchlauf
- Optional für ad-hoc Generierungen
**Charakteristik**: N:M-Mapping zwischen Transaktionen und Prompts mit flexiblen Output-Keys
---
## **Public Schema**
### **3. public.companies**
**Zweck**: Normalisierte Firmenstammdaten für KYC (Know Your Customer) und Compliance
**Struktur**: 11 Spalten
#### Identifikation
- **id** (bigint, NOT NULL, AUTO_INCREMENT)
- Primärschlüssel
- Sequenz: `companies_id_seq`
#### Firmenidentifikation
- **name** (varchar, NOT NULL)
- Primärer Firmenname (Kurzform/Handelsname)
- **legal_name** (varchar, nullable)
- Offizieller rechtlicher Name
- Kann vom Handelsnamen abweichen
- **ticker** (varchar, nullable)
- Börsenticker-Symbol (z.B. "AAPL", "MSFT")
- Nur für börsennotierte Unternehmen
#### Klassifikation & Lokalisierung
- **sector** (varchar, nullable)
- Wirtschaftssektor/Branche
- Z.B. "Technology", "Finance", "Manufacturing"
- **country** (varchar, NOT NULL, default: 'DE')
- Ländercode (ISO 2-Letter)
- Standard: Deutschland
- **headquarters** (varchar, nullable)
- Hauptsitz/Firmenzentrale
- Stadt oder Stadt + Land
#### Risk & Compliance
- **kyc_risk_level** (varchar, NOT NULL, default: 'medium')
- KYC-Risikoeinstufung
- Mögliche Werte: "low", "medium", "high"
- Default: mittleres Risiko
#### Zusatzinformationen
- **summary** (text, nullable)
- Firmenbeschreibung/Zusammenfassung
- Freitextfeld für Kontext
#### Zeitstempel
- **created_at** (timestamp, nullable)
- Erstellungszeitpunkt
- **updated_at** (timestamp, nullable)
- Letzte Aktualisierung
- Laravel-Standard für Timestamps
**Charakteristik**: Saubere, normalisierte Stammdatentabelle mit KYC-Fokus
---
### **4. public.transactions**
**Zweck**: Produktive Transaktionsdaten mit umfassender Anreicherung aus externen Datenquellen und Risikoanalyse
**Struktur**: 54 Spalten (!)
#### Identifikation
- **id** (bigint, NOT NULL, AUTO_INCREMENT) - 9x aufgelistet (⚠️ Schema-Anomalie!)
- Primärschlüssel
- Sequenz: `transactions_id_seq`
#### Transaktions-Basis
- **company_id** (bigint, NOT NULL, default: 1)
- Foreign Key zu `public.companies.id`
- Zuordnung zur durchführenden Firma
- **reference** (varchar, NOT NULL)
- Transaktionsreferenz/Buchungsnummer
- Eindeutiger Identifier
- **amount** (numeric, NOT NULL)
- Transaktionsbetrag
- Numeric für präzise Währungsbeträge
- **currency** (varchar, NOT NULL, default: 'EUR')
- Währungscode
- Standard: Euro
- **counterparty** (varchar, NOT NULL)
- Name der Gegenpartei
- **counterparty_country** (varchar, nullable)
- Land der Gegenpartei
- **channel** (varchar, nullable)
- Transaktionskanal (z.B. "wire", "sepa", "swift")
- **executed_at** (timestamp, NOT NULL)
- Ausführungszeitpunkt der Transaktion
#### Risk Management
- **risk_score** (smallint, NOT NULL, default: 0)
- Risikobewertung (0-100 oder ähnlich)
- **status** (varchar, NOT NULL)
- Transaktionsstatus (z.B. "pending", "approved", "flagged")
- **requires_review** (boolean, NOT NULL, default: true)
- Manuelles Review erforderlich?
- **flagged_by** (varchar, nullable)
- System/User der die Transaktion markiert hat
- **flagged_reason** (text, nullable)
- Grund für Markierung
- **signals** (json, nullable)
- Risikosignale/Trigger als JSON
- Strukturierte Risikoindikatoren
#### Externe Datenquellen (11 Integrationen)
**1. Registry (Handelsregister Basic)**
- **registry_company_number** (text)
- **registry_source** (text) - Quelle (z.B. "Handelsregister")
- **registry_match_score** (double precision) - Matching-Genauigkeit
- **registry_data** (jsonb) - Registrierungsdaten
- **registry_last_refreshed_at** (timestamp)
**2. Genesis (Statistisches Bundesamt)**
- **genesis_context** (jsonb)
- **genesis_last_refreshed_at** (timestamp)
**3. GovData (Offene Verwaltungsdaten)**
- **govdata_data** (jsonb)
- **govdata_last_refreshed_at** (timestamp)
**4. Bundesanzeiger**
- **bundesanzeiger_data** (jsonb)
- **bundesanzeiger_last_refreshed_at** (timestamp)
**5. Insolvency (Insolvenzregister)**
- **insolvency_data** (jsonb)
- **insolvency_last_refreshed_at** (timestamp)
**6. RSS Alerts (News/Medien)**
- **rss_alerts** (jsonb)
- **rss_last_refreshed_at** (timestamp)
**7. Sanctions (Sanktionslisten)**
- **sanctions_data** (jsonb)
- **sanctions_last_refreshed_at** (timestamp)
**8. PEP (Politically Exposed Persons)**
- **pep_data** (jsonb)
- **pep_last_refreshed_at** (timestamp)
**9. GLEIF (Legal Entity Identifier)**
- **gleif_lei** (text) - LEI-Nummer
- **gleif_data** (json)
- **gleif_last_refreshed_at** (timestamp)
**10. EU Sanctions**
- **eu_sanctions_data** (jsonb)
- **eu_sanctions_last_refreshed_at** (timestamp)
**11. Handelsregister (Extended)**
- **handelsregister_data** (jsonb)
- **handelsregister_last_refreshed_at** (timestamp)
- **handelsregister_status** (text)
- **handelsregister_entity_id** (bigint)
#### Zeitstempel
- **created_at** (timestamp, nullable)
- **updated_at** (timestamp, nullable)
**Charakteristik**: Hochgradig angereichertes Data Warehouse für Compliance und Risk Management mit Multi-Source-Integration
---
## Zusammenfassung der Architektur
```
┌─────────────────────────────────────┐
│ Backend Schema (Staging) │
├─────────────────────────────────────┤
│ • Rohdaten-Import │
│ • Lockere Typisierung (text) │
│ • Source-Tracking │
│ • Prompt/AI-Integration │
└────────────┬────────────────────────┘
│ ETL/Processing
┌─────────────────────────────────────┐
│ Public Schema (Production) │
├─────────────────────────────────────┤
│ • Normalisierte Daten │
│ • Strikte Typisierung │
│ • Multi-Source-Enrichment │
│ • Risk & Compliance Features │
└─────────────────────────────────────┘
```
## Datenfluss-Hypothese
1. **Import**: Rohdaten landen in `backend.transactions`
2. **AI-Verarbeitung**: Prompts generieren Outputs in `backend.transaction_outputs`
3. **Enrichment**: Externe Datenquellen werden abgefragt
4. **Normalisierung**: Daten werden nach `public.companies` und `public.transactions` übertragen
5. **Risk Assessment**: Risikoscores und Flags werden berechnet
6. **Review**: Transaktionen mit `requires_review=true` landen in der Queue
## Technische Hinweise
### Probleme
- ⚠️ `public.transactions` hat 9x duplizierte `id` Spalte im Schema
- ⚠️ `backend.transactions` speichert Timestamps als TEXT statt TIMESTAMP
- ⚠️ Keine expliziten Foreign Key Constraints sichtbar zwischen den Schemas
### Empfehlungen
1. Schema-Anomalie in `public.transactions` untersuchen
2. Datum-Felder in `backend.transactions` zu echten TIMESTAMP-Typen migrieren
3. Indizes auf häufig genutzte JOIN/WHERE Spalten prüfen
4. Foreign Key Constraints zwischen den Schemas dokumentieren
-56
View File
@@ -1,56 +0,0 @@
# Datenbank-Tabellen Übersicht
## Alle Tabellen in deiner PostgreSQL-Datenbank (Schema: public)
**Insgesamt: 41 Tabellen**
### ✅ Von Laravel-Migrationen erstellt (13 Tabellen):
1. `cache`
2. `cache_locks`
3. `companies`
4. `failed_jobs`
5. `job_batches`
6. `jobs`
7. `migrations` (Laravel-interne Tracking-Tabelle)
8. `password_reset_tokens`
9. `sessions`
10. `transactions`
11. `users`
*(Die `users`-Tabelle wurde zusätzlich durch Migration [2025_09_02_075243_add_two_factor_columns_to_users_table.php](database/migrations/2025_09_02_075243_add_two_factor_columns_to_users_table.php) um 2FA-Spalten erweitert)*
---
### ❌ NICHT von Laravel-Migrationen erstellt (28 Tabellen):
1. `alembic_version` (Python Alembic Migrations)
2. `bundesanzeiger_cache`
3. `companies_view` (PostgreSQL View)
4. `company_gleif_cache`
5. `company_master_data`
6. `company_master_data_links`
7. `company_opencorporates_cache`
8. `company_registry_cache`
9. `dpma_cache`
10. `entity_corporate_context`
11. `eu_sanctions_cache`
12. `evidence_registry`
13. `genesis_cache`
14. `govdata_cache`
15. `handelsregister_cache`
16. `handelsregister_document_links`
17. `handelsregister_documents`
18. `handelsregister_entities`
19. `handelsregister_entity_transactions`
20. `handelsregister_relations`
21. `insolvency_cache`
22. `pep_cache`
23. `prompt_runs`
24. `prompt_templates`
25. `rss_cache`
26. `sanctions_cache`
27. `test_transaction_llm`
28. `transaction` (Singular-Version, eventuell Legacy?)
29. `transaction_outputs`
30. `transactions_enriched`
Die meisten dieser externen Tabellen scheinen Cache-Tabellen für verschiedene Datenquellen (Handelsregister, Sanctions, GLEIF, etc.) und Enrichment-Daten zu sein. Die `alembic_version`-Tabelle deutet darauf hin, dass möglicherweise ein Python-Backend parallel läuft.
-610
View File
@@ -1,610 +0,0 @@
# Analyse: Laravel Models vs Migrations vs Datenbank
## Übersicht
Diese Analyse vergleicht die Laravel Eloquent Models mit den entsprechenden Migration-Dateien und der tatsächlichen Datenbankstruktur.
---
## **1. Company Model & Migration**
### ✅ **PERFEKT SYNCHRON**
#### Migration
**Datei**: [database/migrations/2025_10_20_181750_create_companies_table.php](database/migrations/2025_10_20_181750_create_companies_table.php)
```php
Schema::create('companies', function (Blueprint $table) {
$table->id();
$table->string('name')->unique();
$table->string('legal_name')->nullable();
$table->string('ticker')->nullable();
$table->string('sector')->nullable();
$table->string('country', 2)->default('DE');
$table->string('headquarters')->nullable();
$table->string('kyc_risk_level')->default('medium');
$table->text('summary')->nullable();
$table->timestamps();
});
```
**Felder**:
- `id` (auto-increment)
- `name` (string, unique)
- `legal_name` (string, nullable)
- `ticker` (string, nullable)
- `sector` (string, nullable)
- `country` (string(2), default: 'DE')
- `headquarters` (string, nullable)
- `kyc_risk_level` (string, default: 'medium')
- `summary` (text, nullable)
- `timestamps` (created_at, updated_at)
#### Model
**Datei**: [app/Models/Company.php](app/Models/Company.php)
```php
protected $fillable = [
'name',
'legal_name',
'ticker',
'sector',
'country',
'headquarters',
'kyc_risk_level',
'summary',
];
public function transactions(): HasMany
{
return $this->hasMany(Transaction::class);
}
```
#### Datenbank-Status
- ✅ Alle Felder vorhanden
- ✅ Datentypen stimmen überein
- ✅ Defaults korrekt gesetzt
- ✅ Unique Constraint auf `name`
- ✅ Relationship `hasMany(Transaction::class)` definiert
---
## **2. Transaction Model & Migration**
### ⚠️ **TEILWEISE DISKREPANZEN**
#### Migration
**Datei**: [database/migrations/2025_10_20_181753_create_transactions_table.php](database/migrations/2025_10_20_181753_create_transactions_table.php)
**Gesamt**: 49 Spalten (ohne timestamps)
##### Core Felder (15 Spalten)
```php
$table->id();
$table->foreignId('company_id')->constrained()->cascadeOnDelete();
$table->string('reference')->unique();
$table->decimal('amount', 16, 2);
$table->string('currency', 3)->default('EUR');
$table->string('counterparty');
$table->string('counterparty_country', 2)->nullable();
$table->string('channel')->nullable();
$table->dateTime('executed_at');
$table->unsignedTinyInteger('risk_score')->default(0);
$table->string('status', 32)->index();
$table->boolean('requires_review')->default(true);
$table->string('flagged_by')->nullable();
$table->text('flagged_reason')->nullable();
$table->json('signals')->nullable();
```
##### Enrichment-Felder (11 Datenquellen, 34 Spalten)
**1. Registry (5 Felder)**
```php
$table->text('registry_company_number')->nullable();
$table->text('registry_source')->nullable();
$table->double('registry_match_score')->nullable();
$table->jsonb('registry_data')->nullable();
$table->dateTime('registry_last_refreshed_at')->nullable();
```
**2. Genesis (2 Felder)**
```php
$table->jsonb('genesis_context')->nullable();
$table->dateTime('genesis_last_refreshed_at')->nullable();
```
**3. GovData (2 Felder)**
```php
$table->jsonb('govdata_data')->nullable();
$table->dateTime('govdata_last_refreshed_at')->nullable();
```
**4. Bundesanzeiger (2 Felder)**
```php
$table->jsonb('bundesanzeiger_data')->nullable();
$table->dateTime('bundesanzeiger_last_refreshed_at')->nullable();
```
**5. Insolvency (2 Felder)**
```php
$table->jsonb('insolvency_data')->nullable();
$table->dateTime('insolvency_last_refreshed_at')->nullable();
```
**6. RSS Alerts (2 Felder)**
```php
$table->jsonb('rss_alerts')->nullable();
$table->dateTime('rss_last_refreshed_at')->nullable();
```
**7. Sanctions (2 Felder)**
```php
$table->jsonb('sanctions_data')->nullable();
$table->dateTime('sanctions_last_refreshed_at')->nullable();
```
**8. PEP (2 Felder)**
```php
$table->jsonb('pep_data')->nullable();
$table->dateTime('pep_last_refreshed_at')->nullable();
```
**9. GLEIF (3 Felder)**
```php
$table->text('gleif_lei')->nullable();
$table->json('gleif_data')->nullable();
$table->dateTime('gleif_last_refreshed_at')->nullable();
```
**10. EU Sanctions (2 Felder)**
```php
$table->jsonb('eu_sanctions_data')->nullable();
$table->dateTime('eu_sanctions_last_refreshed_at')->nullable();
```
**11. Handelsregister (4 Felder)**
```php
$table->jsonb('handelsregister_data')->nullable();
$table->dateTime('handelsregister_last_refreshed_at')->nullable();
$table->text('handelsregister_status')->nullable();
$table->bigInteger('handelsregister_entity_id')->nullable();
```
#### Model
**Datei**: [app/Models/Transaction.php](app/Models/Transaction.php)
```php
// Status-Konstanten
public const STATUS_TRUE_POSITIVE = 'true_positive';
public const STATUS_FALSE_POSITIVE = 'false_positive';
public const STATUS_CLEARED = 'cleared';
// Fillable (nur Core-Felder!)
protected $fillable = [
'company_id',
'reference',
'amount',
'currency',
'counterparty',
'counterparty_country',
'channel',
'executed_at',
'risk_score',
'status',
'requires_review',
'flagged_by',
'flagged_reason',
'signals',
];
// Casts
protected $casts = [
'executed_at' => 'datetime',
'requires_review' => 'boolean',
'signals' => 'array',
];
// Relationship
public function company(): BelongsTo
{
return $this->belongsTo(Company::class);
}
// Helper-Methode
public function statusLabel(): string
{
return match ($this->status) {
self::STATUS_TRUE_POSITIVE => __('Bestätigter Treffer'),
self::STATUS_FALSE_POSITIVE => __('Fehlalarm'),
default => __('Freigegeben'),
};
}
```
### ⚠️ **FEHLENDE FELDER IM MODEL**
Das Transaction Model hat **NUR 14 Core-Felder** im `$fillable` Array, aber die Migration definiert **49 Felder** (exkl. timestamps).
#### Fehlende Enrichment-Felder (34 Spalten):
**Registry-Felder:**
- `registry_company_number`
- `registry_source`
- `registry_match_score`
- `registry_data`
- `registry_last_refreshed_at`
**Genesis-Felder:**
- `genesis_context`
- `genesis_last_refreshed_at`
**GovData-Felder:**
- `govdata_data`
- `govdata_last_refreshed_at`
**Bundesanzeiger-Felder:**
- `bundesanzeiger_data`
- `bundesanzeiger_last_refreshed_at`
**Insolvency-Felder:**
- `insolvency_data`
- `insolvency_last_refreshed_at`
**RSS-Felder:**
- `rss_alerts`
- `rss_last_refreshed_at`
**Sanctions-Felder:**
- `sanctions_data`
- `sanctions_last_refreshed_at`
**PEP-Felder:**
- `pep_data`
- `pep_last_refreshed_at`
**GLEIF-Felder:**
- `gleif_lei`
- `gleif_data`
- `gleif_last_refreshed_at`
**EU Sanctions-Felder:**
- `eu_sanctions_data`
- `eu_sanctions_last_refreshed_at`
**Handelsregister-Felder:**
- `handelsregister_data`
- `handelsregister_last_refreshed_at`
- `handelsregister_status`
- `handelsregister_entity_id`
### ⚠️ **FEHLENDE CASTS**
Das Model sollte Casts für alle zeitbasierten und JSON-Felder haben:
**Fehlende DateTime-Casts:**
- `registry_last_refreshed_at`
- `genesis_last_refreshed_at`
- `govdata_last_refreshed_at`
- `bundesanzeiger_last_refreshed_at`
- `insolvency_last_refreshed_at`
- `rss_last_refreshed_at`
- `sanctions_last_refreshed_at`
- `pep_last_refreshed_at`
- `gleif_last_refreshed_at`
- `eu_sanctions_last_refreshed_at`
- `handelsregister_last_refreshed_at`
**Fehlende JSON/Array-Casts:**
- `registry_data`
- `genesis_context`
- `govdata_data`
- `bundesanzeiger_data`
- `insolvency_data`
- `rss_alerts`
- `sanctions_data`
- `pep_data`
- `gleif_data`
- `eu_sanctions_data`
- `handelsregister_data`
---
## **3. Vergleich: Datenbank vs Migration**
### public.companies
**Status**: ✅ **100% Übereinstimmung**
| Feature | Migration | Datenbank | Status |
|---------|-----------|-----------|--------|
| Spalten | 11 | 11 | ✅ |
| Unique Constraint | `name` | `name` | ✅ |
| Defaults | `country='DE'`, `kyc_risk_level='medium'` | Identisch | ✅ |
### public.transactions
**Status**: ✅ **Migration deckt alle DB-Felder ab**
#### Constraints & Indizes
| Constraint | Migration | Datenbank | Status |
|------------|-----------|-----------|--------|
| Foreign Key | `company_id → companies.id` | Vorhanden | ✅ |
| Cascade Delete | `cascadeOnDelete()` | Implementiert | ✅ |
| Unique | `reference` | Vorhanden | ✅ |
| Index | `status` | Vorhanden | ✅ |
#### Datentypen-Vergleich
| Feld | Migration | Datenbank | Status |
|------|-----------|-----------|--------|
| id | `id()` | bigint | ✅ |
| company_id | `foreignId()` | bigint | ✅ |
| amount | `decimal(16,2)` | numeric | ✅ |
| currency | `string(3)` | varchar | ✅ |
| counterparty_country | `string(2)` | varchar | ✅ |
| risk_score | `unsignedTinyInteger` | smallint | ⚠️* |
| status | `string(32)` | varchar | ✅ |
| requires_review | `boolean` | boolean | ✅ |
| signals | `json` | json | ✅ |
| *_data | `jsonb` | jsonb | ✅ |
| gleif_data | `json` | json | ✅ |
| executed_at | `dateTime` | timestamp | ✅ |
| *_last_refreshed_at | `dateTime` | timestamp | ✅ |
*`unsignedTinyInteger` (0-255) vs `smallint` (-32768 bis 32767) sind funktional kompatibel
---
## Zusammenfassung
### ✅ **Stärken**
1. **Migration-Dateien sind vollständig**
- Alle Datenbank-Felder korrekt definiert
- Foreign Key Constraints implementiert
- Indizes sinnvoll gesetzt
2. **Core-Model-Felder stimmen überein**
- Basis-Transaktionsfelder vollständig
- Relationships sauber definiert
3. **Datenbank-Konsistenz**
- Migrations wurden korrekt ausgeführt
- Constraints sind aktiv
---
## ⚠️ **Probleme & Empfehlungen**
### Problem 1: Transaction Model ist unvollständig
**Problem**:
- Das Model definiert nur 14 von 49 Feldern im `$fillable` Array
- Alle 34 Enrichment-Felder fehlen
**Auswirkungen**:
- ❌ Enrichment-Felder können nicht via Mass Assignment gesetzt werden
- ❌ Keine automatischen Type Casts für externe Datenfelder
- ❌ Potenzielle Fehler beim Zugriff auf nicht-gecastete JSON-Daten
- ❌ DateTime-Felder werden als Strings zurückgegeben
**Lösungsvorschläge**:
**Option 1**: Alle Felder zu `$fillable` hinzufügen
```php
protected $fillable = [
// Core fields
'company_id', 'reference', 'amount', 'currency',
'counterparty', 'counterparty_country', 'channel',
'executed_at', 'risk_score', 'status',
'requires_review', 'flagged_by', 'flagged_reason', 'signals',
// Registry
'registry_company_number', 'registry_source', 'registry_match_score',
'registry_data', 'registry_last_refreshed_at',
// Genesis
'genesis_context', 'genesis_last_refreshed_at',
// ... alle weiteren Felder
];
```
**Option 2**: `$guarded` verwenden (empfohlen für interne Anwendungen)
```php
protected $guarded = ['id'];
```
**Option 3**: Separate Accessor/Mutator für Enrichment-Felder
```php
public function registryData(): Attribute
{
return Attribute::make(
get: fn ($value) => json_decode($value, true),
set: fn ($value) => json_encode($value),
);
}
```
### Problem 2: Fehlende Casts für Enrichment-Felder
**Problem**:
- Keine Casts für `*_last_refreshed_at` Felder
- Keine Casts für `*_data` JSON-Felder
**Auswirkungen**:
- DateTime-Felder werden als Strings zurückgegeben (kein Carbon-Objekt)
- JSON-Felder müssen manuell dekodiert werden
**Lösung**:
```php
protected $casts = [
// Existing
'executed_at' => 'datetime',
'requires_review' => 'boolean',
'signals' => 'array',
// DateTime casts for all refresh timestamps
'registry_last_refreshed_at' => 'datetime',
'genesis_last_refreshed_at' => 'datetime',
'govdata_last_refreshed_at' => 'datetime',
'bundesanzeiger_last_refreshed_at' => 'datetime',
'insolvency_last_refreshed_at' => 'datetime',
'rss_last_refreshed_at' => 'datetime',
'sanctions_last_refreshed_at' => 'datetime',
'pep_last_refreshed_at' => 'datetime',
'gleif_last_refreshed_at' => 'datetime',
'eu_sanctions_last_refreshed_at' => 'datetime',
'handelsregister_last_refreshed_at' => 'datetime',
// JSON/Array casts for all data fields
'registry_data' => 'array',
'genesis_context' => 'array',
'govdata_data' => 'array',
'bundesanzeiger_data' => 'array',
'insolvency_data' => 'array',
'rss_alerts' => 'array',
'sanctions_data' => 'array',
'pep_data' => 'array',
'gleif_data' => 'array',
'eu_sanctions_data' => 'array',
'handelsregister_data' => 'array',
];
```
### Problem 3: Datenbank-Schema-Anomalie
**Problem**:
- Die `public.transactions` Tabelle zeigt 9x duplizierte `id` Spalten im describe_table Output
**Mögliche Ursachen**:
- Korruptes Schema-Metadaten
- Mehrfache Migration-Ausführungen ohne Rollback
- PostgreSQL-Katalog-Problem
**Lösung**:
1. Schema inspizieren: `\d+ transactions` in psql
2. Bei Bedarf Migration neu ausführen
3. Oder manuelles ALTER TABLE zur Bereinigung
---
## Nächste Schritte
### Empfohlene Reihenfolge:
1. ✅ **Transaction Model aktualisieren**
- Alle fehlenden Felder zu `$fillable` hinzufügen
- Alle fehlenden Casts definieren
2. ✅ **Tests schreiben**
- Unit-Tests für Model-Casts
- Feature-Tests für Enrichment-Datenfluss
3. ⚠️ **Datenbank-Anomalie untersuchen**
- PostgreSQL-Schema inspizieren
- Ggf. Migration neu ausführen
4. 📝 **Dokumentation erweitern**
- Enrichment-Pipeline dokumentieren
- API für externe Datenquellen dokumentieren
---
## Checkliste
### Companies
- [x] Migration vollständig
- [x] Model synchron mit Migration
- [x] Datenbank korrekt strukturiert
- [x] Relationships definiert
- [x] Casts korrekt
### Transactions
- [x] Migration vollständig
- [ ] Model synchron mit Migration ⚠️
- [x] Datenbank korrekt strukturiert
- [x] Relationships definiert
- [ ] Casts vollständig ⚠️
- [ ] Schema-Anomalie behoben ⚠️
---
## Anhang: Vollständige Feldliste Transaction Model
### Core Felder (14)
✅ Im Model vorhanden
1. company_id
2. reference
3. amount
4. currency
5. counterparty
6. counterparty_country
7. channel
8. executed_at
9. risk_score
10. status
11. requires_review
12. flagged_by
13. flagged_reason
14. signals
### Enrichment Felder (34)
❌ Im Model fehlend
**Registry (5)**
15. registry_company_number
16. registry_source
17. registry_match_score
18. registry_data
19. registry_last_refreshed_at
**Genesis (2)**
20. genesis_context
21. genesis_last_refreshed_at
**GovData (2)**
22. govdata_data
23. govdata_last_refreshed_at
**Bundesanzeiger (2)**
24. bundesanzeiger_data
25. bundesanzeiger_last_refreshed_at
**Insolvency (2)**
26. insolvency_data
27. insolvency_last_refreshed_at
**RSS (2)**
28. rss_alerts
29. rss_last_refreshed_at
**Sanctions (2)**
30. sanctions_data
31. sanctions_last_refreshed_at
**PEP (2)**
32. pep_data
33. pep_last_refreshed_at
**GLEIF (3)**
34. gleif_lei
35. gleif_data
36. gleif_last_refreshed_at
**EU Sanctions (2)**
37. eu_sanctions_data
38. eu_sanctions_last_refreshed_at
**Handelsregister (4)**
39. handelsregister_data
40. handelsregister_last_refreshed_at
41. handelsregister_status
42. handelsregister_entity_id
---
*Analysiert am: 2025-11-12*
+13 -288
View File
@@ -1,13 +1,13 @@
{
"name": "trusted_ai",
"name": "frontend",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "frontend",
"dependencies": {
"@tailwindcss/vite": "^4.1.11",
"autoprefixer": "^10.4.20",
"axios": "^1.7.4",
"concurrently": "^9.0.1",
"laravel-vite-plugin": "^2.0",
"tailwindcss": "^4.0.7",
@@ -1070,12 +1070,6 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/autoprefixer": {
"version": "10.4.21",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz",
@@ -1113,17 +1107,6 @@
"postcss": "^8.1.0"
}
},
"node_modules/axios": {
"version": "1.12.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
"integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/baseline-browser-mapping": {
"version": "2.8.16",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.16.tgz",
@@ -1152,6 +1135,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.8.9",
"caniuse-lite": "^1.0.30001746",
@@ -1166,19 +1150,6 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001751",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001751.tgz",
@@ -1268,18 +1239,6 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT"
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/concurrently": {
"version": "9.2.1",
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz",
@@ -1304,15 +1263,6 @@
"url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
@@ -1322,20 +1272,6 @@
"node": ">=8"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/electron-to-chromium": {
"version": "1.5.237",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.237.tgz",
@@ -1361,51 +1297,6 @@
"node": ">=10.13.0"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/esbuild": {
"version": "0.25.11",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.11.tgz",
@@ -1473,42 +1364,6 @@
}
}
},
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/fraction.js": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
@@ -1536,15 +1391,6 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
@@ -1554,55 +1400,6 @@
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
@@ -1618,45 +1415,6 @@
"node": ">=8"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
@@ -1951,36 +1709,6 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/minipass": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
@@ -2046,6 +1774,7 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -2072,6 +1801,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -2087,12 +1817,6 @@
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"license": "MIT"
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@@ -2247,10 +1971,10 @@
}
},
"node_modules/tar": {
"version": "7.5.1",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.1.tgz",
"integrity": "sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==",
"license": "ISC",
"version": "7.5.2",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.2.tgz",
"integrity": "sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==",
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/fs-minipass": "^4.0.0",
"chownr": "^3.0.0",
@@ -2324,10 +2048,11 @@
}
},
"node_modules/vite": {
"version": "7.1.10",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.10.tgz",
"integrity": "sha512-CmuvUBzVJ/e3HGxhg6cYk88NGgTnBoOo7ogtfJJ0fefUWAxN/WDSUa50o+oVBxuIhO8FoEZW0j2eW7sfjs5EtA==",
"version": "7.2.4",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.4.tgz",
"integrity": "sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==",
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
-1
View File
@@ -8,7 +8,6 @@
"dependencies": {
"@tailwindcss/vite": "^4.1.11",
"autoprefixer": "^10.4.20",
"axios": "^1.7.4",
"concurrently": "^9.0.1",
"laravel-vite-plugin": "^2.0",
"tailwindcss": "^4.0.7",
+6 -6
View File
@@ -34,16 +34,16 @@
$formatNumber = static fn ($value): string => number_format($value, 0, ',', '.');
$riskSegments = collect([
'Kritisch (≥80)' => [
'transactions' => $transactions->filter(fn ($trx) => $trx->risk_score >= 80),
'Kritisches Risiko' => [
'transactions' => $transactions->filter(fn ($trx) => $trx->status === \App\Models\Transaction::STATUS_TRUE_POSITIVE),
'status' => \App\Models\Transaction::STATUS_TRUE_POSITIVE,
],
'Hoch (65-79)' => [
'transactions' => $transactions->filter(fn ($trx) => $trx->risk_score >= 65 && $trx->risk_score < 80),
'Hohes Risiko' => [
'transactions' => $transactions->filter(fn ($trx) => $trx->status === \App\Models\Transaction::STATUS_FALSE_POSITIVE),
'status' => \App\Models\Transaction::STATUS_FALSE_POSITIVE,
],
'Gering (40-64)' => [
'transactions' => $transactions->filter(fn ($trx) => $trx->risk_score >= 40 && $trx->risk_score < 65),
'Geringes Risiko' => [
'transactions' => $transactions->filter(fn ($trx) => $trx->status === \App\Models\Transaction::STATUS_CLEARED),
'status' => \App\Models\Transaction::STATUS_CLEARED,
],
])->map(fn ($data, $label) => [
File diff suppressed because it is too large Load Diff
+8 -18
View File
@@ -1,6 +1,5 @@
<?php
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Route;
use Laravel\Fortify\Features;
use Livewire\Volt\Volt;
@@ -9,23 +8,14 @@ Route::get('/', function () {
return view('welcome');
})->name('home');
// Test Route für PGSQL - kann beliebig abgeändert werden (zum Testen)
Route::get('/test-zweite-db', function () {
try {
// Eine einfache Abfrage auf der zweiten Verbindung
$result = DB::connection('pgsql_second')->select('SELECT NOW() as current_time');
return response()->json([
'status' => 'Erfolgreich verbunden mit zweiter Datenbank',
'zeitstempel' => $result[0]->current_time,
]);
} catch (\Exception $e) {
return response()->json([
'status' => 'Fehler bei der Verbindung',
'meldung' => $e->getMessage(),
], 500);
}
});
Route::get('/health', function () {
return response()->json([
'status' => 'healthy',
'timestamp' => now()->toIso8601String(),
'app' => config('app.name'),
'env' => config('app.env'),
]);
})->name('health');
Route::view('dashboard', 'dashboard')
->middleware(['auth', 'verified'])
+258
View File
@@ -0,0 +1,258 @@
#!/bin/bash
# Deployment Script für Synology NAS
# Dieses Script wird auf der Synology NAS ausgeführt
set -e # Exit on error
# ============================================
# Configuration
# ============================================
PROJECT_DIR="/volume1/docker/laravel-app"
DOCKER_IMAGE="laravel-app"
COMPOSE_FILE="docker-compose.synology.yml"
ENV_FILE=".env.synology"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# ============================================
# Helper Functions
# ============================================
log_info() {
echo -e "${GREEN}[INFO]${NC} $1"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# ============================================
# Pre-deployment Checks
# ============================================
log_info "Starting deployment process..."
# Check if running on Synology
if [ ! -d "/volume1" ]; then
log_error "This script should be run on Synology NAS"
exit 1
fi
# Navigate to project directory
cd "$PROJECT_DIR" || {
log_error "Project directory not found: $PROJECT_DIR"
exit 1
}
# Check if Docker is installed
if ! command -v docker &> /dev/null; then
log_error "Docker is not installed"
exit 1
fi
# Check if docker-compose is installed
if ! command -v docker-compose &> /dev/null; then
log_error "docker-compose is not installed"
exit 1
fi
# ============================================
# Load Docker Image
# ============================================
if [ -f "laravel-app.tar.gz" ]; then
log_info "Loading Docker image from artifact..."
gunzip -c laravel-app.tar.gz | docker load
# Tag the image appropriately
LOADED_IMAGE=$(docker images --format "{{.Repository}}:{{.Tag}}" | grep laravel-app | head -n1)
if [ -n "$LOADED_IMAGE" ]; then
docker tag "$LOADED_IMAGE" "${DOCKER_IMAGE}:latest"
log_info "Tagged image as ${DOCKER_IMAGE}:latest"
fi
else
log_warn "No Docker image artifact found. Using existing image or building..."
# Check if Dockerfile exists and build if necessary
if [ -f "Dockerfile" ]; then
log_info "Building Docker image..."
docker build -t "${DOCKER_IMAGE}:latest" .
else
log_error "Neither Docker artifact nor Dockerfile found"
exit 1
fi
fi
# ============================================
# Environment Configuration
# ============================================
if [ ! -f "$ENV_FILE" ]; then
log_warn "Environment file not found: $ENV_FILE"
if [ -f ".env.synology.example" ]; then
log_info "Copying from .env.synology.example..."
cp .env.synology.example "$ENV_FILE"
log_warn "⚠️ Please configure $ENV_FILE with your settings!"
else
log_error "No environment configuration found"
exit 1
fi
fi
# ============================================
# Backup Current Deployment
# ============================================
log_info "Creating backup of current deployment..."
BACKUP_DIR="/volume1/docker/backups/laravel-app"
mkdir -p "$BACKUP_DIR"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="$BACKUP_DIR/backup_${TIMESTAMP}.tar.gz"
# Backup database if container is running
if docker ps --format '{{.Names}}' | grep -q "laravel_postgres"; then
log_info "Backing up PostgreSQL database..."
docker exec laravel_postgres pg_dump -U ingest_user ingest_db > "$BACKUP_DIR/db_${TIMESTAMP}.sql"
fi
# Keep only last 5 backups
log_info "Cleaning old backups (keeping last 5)..."
ls -t "$BACKUP_DIR"/db_*.sql 2>/dev/null | tail -n +6 | xargs -r rm
# ============================================
# Stop Current Containers
# ============================================
log_info "Stopping current containers..."
if [ -f "$COMPOSE_FILE" ]; then
docker-compose -f "$COMPOSE_FILE" down || log_warn "No containers to stop"
else
log_warn "Compose file not found: $COMPOSE_FILE"
fi
# ============================================
# Clean up old images (optional)
# ============================================
log_info "Cleaning up old Docker images..."
docker image prune -f
# ============================================
# Start New Deployment
# ============================================
log_info "Starting new deployment..."
if [ ! -f "$COMPOSE_FILE" ]; then
log_error "Docker Compose file not found: $COMPOSE_FILE"
exit 1
fi
# Pull/use latest images and start services
docker-compose -f "$COMPOSE_FILE" up -d --build
# ============================================
# Wait for Services to Start
# ============================================
log_info "Waiting for services to start..."
sleep 10
# Check if containers are running
if ! docker ps --format '{{.Names}}' | grep -q "laravel_app"; then
log_error "Laravel app container failed to start"
# Show logs
log_info "Container logs:"
docker-compose -f "$COMPOSE_FILE" logs --tail=50
# Rollback
log_warn "Attempting rollback..."
docker-compose -f "$COMPOSE_FILE" down
exit 1
fi
# ============================================
# Post-deployment Tasks
# ============================================
log_info "Running post-deployment tasks..."
# Wait for database to be ready
log_info "Waiting for database to be ready..."
sleep 5
# Run migrations
log_info "Running database migrations..."
docker exec laravel_app php artisan migrate --force || log_warn "Migrations failed"
# Clear caches
log_info "Clearing caches..."
docker exec laravel_app php artisan cache:clear || log_warn "Cache clear failed"
docker exec laravel_app php artisan config:clear || log_warn "Config clear failed"
docker exec laravel_app php artisan view:clear || log_warn "View clear failed"
# Optimize
log_info "Optimizing application..."
docker exec laravel_app php artisan config:cache || log_warn "Config cache failed"
docker exec laravel_app php artisan route:cache || log_warn "Route cache failed"
docker exec laravel_app php artisan view:cache || log_warn "View cache failed"
# ============================================
# Health Check
# ============================================
log_info "Performing health check..."
MAX_RETRIES=30
RETRY_COUNT=0
HEALTH_CHECK_URL="http://localhost:8080"
while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
if curl -f -s "$HEALTH_CHECK_URL" > /dev/null 2>&1; then
log_info "✅ Application is healthy!"
break
fi
log_info "⏳ Waiting for application... ($RETRY_COUNT/$MAX_RETRIES)"
sleep 5
RETRY_COUNT=$((RETRY_COUNT + 1))
done
if [ $RETRY_COUNT -eq $MAX_RETRIES ]; then
log_error "Health check failed after $MAX_RETRIES attempts"
# Show recent logs
log_info "Recent application logs:"
docker-compose -f "$COMPOSE_FILE" logs --tail=100
exit 1
fi
# ============================================
# Display Status
# ============================================
log_info "Deployment completed successfully! 🚀"
log_info "Container status:"
docker-compose -f "$COMPOSE_FILE" ps
log_info ""
log_info "==================================="
log_info "Application URL: http://$(hostname -I | awk '{print $1}'):8080"
log_info "==================================="
log_info ""
log_info "Useful commands:"
log_info " - View logs: docker-compose -f $COMPOSE_FILE logs -f"
log_info " - Restart: docker-compose -f $COMPOSE_FILE restart"
log_info " - Stop: docker-compose -f $COMPOSE_FILE down"
log_info " - Shell access: docker exec -it laravel_app sh"
# ============================================
# Cleanup
# ============================================
log_info "Cleaning up deployment artifacts..."
rm -f laravel-app.tar.gz
log_info "Deployment script completed!"
exit 0
+3 -1
View File
@@ -69,7 +69,9 @@ test('users with two factor enabled are redirected to two factor challenge', fun
test('users can logout', function () {
$user = User::factory()->create();
$response = $this->actingAs($user)->post(route('logout'));
$response = $this->actingAs($user)
->withSession(['_token' => 'test-token'])
->post(route('logout'), ['_token' => 'test-token']);
$response->assertRedirect(route('home'));
+1 -1
View File
@@ -23,7 +23,7 @@ test('company search page displays correctly', function () {
$response = $this->get(route('company-search'));
$response->assertSee('Unternehmensauskunft');
$response->assertSee('Suchen Sie nach Unternehmensinformationen');
$response->assertSee('Einfache Suche für Unternehmensdaten und historische Cases');
});
test('search input is wired to component', function () {
@@ -1,7 +1,6 @@
<?php
use App\Models\User;
use Livewire\Volt\Volt;
it('requires authentication to access upload page', function () {
$response = $this->get('/upload');