felhasználói felületre pü
This commit is contained in:
181
docs/financial_module_comprehensive_audit_2026-07-25.md
Normal file
181
docs/financial_module_comprehensive_audit_2026-07-25.md
Normal file
@@ -0,0 +1,181 @@
|
||||
# 🔍 Comprehensive Financial Module Audit
|
||||
|
||||
**Date:** 2026-07-25
|
||||
**Auditor:** Rendszer-Architect (🏗️ Architect Mode)
|
||||
**Scope:** Full-stack financial system audit — Models, API, Services, Workers, Frontend
|
||||
**Executive Summary:**
|
||||
|
||||
> The financial backend is **well-architected and modular**. The model layer follows DDD schema separation, the service layer splits into distinct managers, the API provides complete user-facing endpoints, and all system workers properly write to the audit ledger. The **critical gap is the frontend** — zero financial UI exists for end users despite the backend being fully ready.
|
||||
|
||||
---
|
||||
|
||||
## 1. MODELS — Schema & Architecture
|
||||
|
||||
### ✅ STATUS: WELL-ARCHITECTED — No Monolith
|
||||
|
||||
| Schema | Table | Purpose | File |
|
||||
|--------|-------|---------|------|
|
||||
| `identity` | `wallets` | Quadruple wallet: earned, purchased, service_coins + ActiveVouchers (FIFO) | [`models/identity/identity.py:263`](dev/service_finder/backend/app/models/identity/identity.py:263) |
|
||||
| `identity` | `active_vouchers` | FIFO voucher tracking with expiration | [`models/identity/identity.py:316`](dev/service_finder/backend/app/models/identity/identity.py:316) |
|
||||
| `audit` | `financial_ledger` | Immutable double-entry ledger (DEBIT/CREDIT) | [`models/system/audit.py:78`](dev/service_finder/backend/app/models/system/audit.py:78) |
|
||||
| `fleet_finance` | `cost_categories` | Hierarchical cost category tree | [`models/fleet_finance/models.py:37`](dev/service_finder/backend/app/models/fleet_finance/models.py:37) |
|
||||
| `fleet_finance` | `asset_costs` | Operational expense log (TCO) | [`models/fleet_finance/models.py:109`](dev/service_finder/backend/app/models/fleet_finance/models.py:109) |
|
||||
| `fleet_finance` | `asset_financials` | Acquisition & depreciation | [`models/fleet_finance/models.py:180`](dev/service_finder/backend/app/models/fleet_finance/models.py:180) |
|
||||
| `fleet_finance` | `insurance_providers` | Insurance company catalog | [`models/fleet_finance/models.py:219`](dev/service_finder/backend/app/models/fleet_finance/models.py:219) |
|
||||
| `fleet_finance` | `vehicle_insurance_policies` | Vehicle insurance policies | [`models/fleet_finance/models.py:248`](dev/service_finder/backend/app/models/fleet_finance/models.py:248) |
|
||||
| `fleet_finance` | `vehicle_tax_obligations` | Tax obligations per vehicle | [`models/fleet_finance/models.py:298`](dev/service_finder/backend/app/models/fleet_finance/models.py:298) |
|
||||
| `finance` | `issuers` | Billing entities (KFT/EV/BT/ZRT) | [`models/marketplace/finance.py:27`](dev/service_finder/backend/app/models/marketplace/finance.py:27) |
|
||||
| `marketplace` | `payment_intents` | Payment intent lifecycle (Double Lock) | [`models/marketplace/payment.py`](dev/service_finder/backend/app/models/marketplace/payment.py) |
|
||||
| `core_logic` | `subscription_tiers` | Subscription tier definitions | [`models/core_logic.py`](dev/service_finder/backend/app/models/core_logic.py) |
|
||||
| `core_logic` | `user_subscriptions` / `org_subscriptions` | Subscription state tracking | [`models/core_logic.py`](dev/service_finder/backend/app/models/core_logic.py) |
|
||||
|
||||
### Key Enums (Defined in [`audit.py:58`](dev/service_finder/backend/app/models/system/audit.py:58))
|
||||
- `LedgerEntryType`: `DEBIT`, `CREDIT`
|
||||
- `WalletType`: `EARNED`, `PURCHASED`, `SERVICE_COINS`, `VOUCHER`
|
||||
- `LedgerStatus`: `PENDING`, `SUCCESS`, `FAILED`, `REFUNDED`, `REFUND`
|
||||
|
||||
---
|
||||
|
||||
## 2. API ENDPOINTS — Routing & User Access
|
||||
|
||||
### ✅ STATUS: WELL-COVERED
|
||||
|
||||
| Method | Endpoint | Purpose | User-Facing? | RBAC |
|
||||
|--------|----------|---------|-------------|------|
|
||||
| `POST` | `/billing/upgrade` | Package upgrade | ✅ Yes | Auth required |
|
||||
| `POST` | `/billing/payment-intent/create` | Create PaymentIntent | ✅ Yes | Auth required |
|
||||
| `POST` | `/billing/payment-intent/{id}/stripe-checkout` | Initiate Stripe checkout | ✅ Yes | Auth required |
|
||||
| `POST` | `/billing/payment-intent/{id}/process-internal` | Internal gifting deduction | ✅ Yes | Auth required |
|
||||
| `POST` | `/billing/stripe-webhook` | Stripe callback | N/A (webhook) | Signature |
|
||||
| `GET` | `/billing/payment-intent/{id}/status` | PaymentIntent status | ✅ Yes | Auth required |
|
||||
| `GET` | `/billing/wallet/balance` | User wallet balances | ✅ Yes | Auth required |
|
||||
| `GET` | `/billing/wallet/transactions` | User transaction History | ✅ Yes | Auth required |
|
||||
| `POST` | `/financial-manager/purchase-package` | Full purchase lifecycle | ✅ Yes | Auth required |
|
||||
| `GET` | `/financial-manager/status` | Service health check | ✅ Yes | Auth required |
|
||||
| `GET` | `/finance/issuers/` | List billing issuers | ❌ Admin | `finance:view` |
|
||||
| `PATCH` | `/finance/issuers/{id}` | Update issuer | ❌ Admin | `finance:edit` |
|
||||
| `GET` | `/admin/finance/ledger` | Admin ledger view | ❌ Admin | `finance:view` |
|
||||
| `POST` | `/expenses/` | Create expense (TCO) | ✅ Yes | Auth + Capability |
|
||||
| `PUT` | `/expenses/{id}` | Update expense | ✅ Yes | Auth |
|
||||
| `GET` | `/expenses/` | List all expenses | ❌ Admin | Auth |
|
||||
| `GET` | `/expenses/{asset_id}` | Asset-specific expenses | ✅ Yes | Auth |
|
||||
| `GET` | `/subscriptions/public` | Public package catalog | ✅ Public | None |
|
||||
| `GET` | `/subscriptions/my` | Current subscription | ✅ Yes | Auth |
|
||||
| `GET` | `/subscriptions/feature-flags` | Resolved feature flags | ✅ Yes | Auth |
|
||||
|
||||
### Router Registration (in [`api/v1/api.py:1`](dev/service_finder/backend/app/api/v1/api.py:1))
|
||||
- `billing.router` → `/billing` ✅
|
||||
- `financial_manager.router` → `/financial-manager` ✅
|
||||
- `finance_admin.router` → `/finance/issuers` + `/admin/finance` ✅
|
||||
|
||||
---
|
||||
|
||||
## 3. SERVICES — Monolith vs. Modular Managers
|
||||
|
||||
### ✅ STATUS: MODULAR & SEPARATED — Minor Overlap Risk
|
||||
|
||||
| Service File | Responsibility | Lines |
|
||||
|-------------|---------------|-------|
|
||||
| [`billing_engine.py`](dev/service_finder/backend/app/services/billing_engine.py:1) | Core: PricingCalculator, SmartDeduction (FIFO), AtomicTransactionManager (double-entry) | ~1070 |
|
||||
| [`financial_manager.py`](dev/service_finder/backend/app/services/financial_manager.py:1) | Orchestrator: full purchase lifecycle (validate→price→intent→pay→activate→commission) | ~520 |
|
||||
| [`financial_interfaces.py`](dev/service_finder/backend/app/services/financial_interfaces.py:1) | ABCs: `BasePaymentGateway`, `BaseInvoicingService` + exceptions | ~186 |
|
||||
| [`financial_orchestrator.py`](dev/service_finder/backend/app/services/financial_orchestrator.py:1) | Unit of Work: payment processing with issuer selection (vetésforgó) | ~449 |
|
||||
| [`payment_router.py`](dev/service_finder/backend/app/services/payment_router.py:1) | Router: PaymentIntent creation, Stripe Double Lock, internal gifting | ~495 |
|
||||
| [`cost_service.py`](dev/service_finder/backend/app/services/cost_service.py:1) | Fleet operational cost tracking with telemetry | ~140 |
|
||||
| [`subscription_activator.py`](dev/service_finder/backend/app/services/subscription_activator.py:1) | Subscription activation (user/org with stacking) | ~unknown |
|
||||
| [`commission_service.py`](dev/service_finder/backend/app/services/commission_service.py:1) | MLM commission distribution | ~unknown |
|
||||
| [`stripe_adapter.py`](dev/service_finder/backend/app/services/stripe_adapter.py:1) | Stripe API adapter | ~unknown |
|
||||
| [`mock_payment_gateway.py`](dev/service_finder/backend/app/services/mock_payment_gateway.py:1) | Test gateway (auto-approve mode) | ~unknown |
|
||||
|
||||
### Architecture Decision: Strategy Pattern
|
||||
The [`financial_interfaces.py`](dev/service_finder/backend/app/services/financial_interfaces.py:1) defines [`BasePaymentGateway`](dev/service_finder/backend/app/services/financial_interfaces.py:13) and [`BaseInvoicingService`](dev/service_finder/backend/app/services/financial_interfaces.py:91) ABCs. `FinancialManager` accepts any gateway implementation via constructor injection.
|
||||
|
||||
### ⚠️ Concern: Overlap Between `financial_orchestrator` and `billing_engine`
|
||||
- Both create `FinancialLedger` entries
|
||||
- `financial_orchestrator.process_payment()` writes ledger entries with raw `update()` SQL instead of using `AtomicTransactionManager`
|
||||
- `financial_orchestrator` accesses `Wallet` fields directly via raw SQL updates
|
||||
- This is a **code duplication risk** — two different ledger-writing paths exist
|
||||
|
||||
### Recommendation
|
||||
- Either retire `FinancialOrchestrator.process_payment()` in favor of `AtomicTransactionManager` + `PaymentRouter`, or refactor `FinancialOrchestrator` to delegate to `BillingEngine` classes.
|
||||
|
||||
---
|
||||
|
||||
## 4. WORKERS — Ledger Integration
|
||||
|
||||
### ✅ STATUS: FULLY CONNECTED — All workers write to audit ledger
|
||||
|
||||
| Worker | Ledger Entry? | Transaction Type | Amount | WalletType |
|
||||
|--------|--------------|-----------------|--------|------------|
|
||||
| [`inactivity_monitor_worker.py`](dev/service_finder/backend/app/workers/system/inactivity_monitor_worker.py:1) | ✅ | `ACCOUNT_SUSPENDED_INACTIVITY` | 0.0 | `EARNED` |
|
||||
| [`subscription_worker.py`](dev/service_finder/backend/app/workers/system/subscription_worker.py:1) | ✅ | `SUBSCRIPTION_EXPIRED` | 0.0 | `SYSTEM` |
|
||||
| [`subscription_monitor_worker.py`](dev/service_finder/backend/app/workers/system/subscription_monitor_worker.py:1) | ✅ | `SUBSCRIPTION_EXPIRED` | 0.0 | `EARNED` |
|
||||
|
||||
### ⚠️ Minor Inconsistency
|
||||
- `subscription_worker.py` line 75 uses `WalletType.SYSTEM` — this value does NOT exist in the [`WalletType` enum](dev/service_finder/backend/app/models/system/audit.py:63) (which only has `EARNED`, `PURCHASED`, `SERVICE_COINS`, `VOUCHER`). This would raise an `AttributeError` at runtime.
|
||||
- `subscription_monitor_worker.py` correctly uses `WalletType.EARNED`.
|
||||
|
||||
---
|
||||
|
||||
## 5. FRONTEND — Wallet & Transaction Views
|
||||
|
||||
### ❌ STATUS: COMPLETELY MISSING
|
||||
|
||||
A search across the entire `frontend/src/` directory for Vue files containing `wallet`, `transaction`, `balance`, `purchase`, `subscription`, or `billing` returned **zero results**.
|
||||
|
||||
The backend already provides:
|
||||
- `GET /billing/wallet/balance` → Returns quadruple wallet balances
|
||||
- `GET /billing/wallet/transactions` → Returns paginated transaction history with filtering
|
||||
- `GET /subscriptions/my` → Returns current subscription details
|
||||
- `GET /subscriptions/feature-flags` → Returns feature flags
|
||||
|
||||
But **no frontend component calls any of these endpoints**.
|
||||
|
||||
### What Needs Building
|
||||
|
||||
1. **WalletBalanceCard.vue** — Display earned/purchased/service_coins/voucher balances
|
||||
2. **TransactionHistory.vue** — Paginated transaction list with type/wallet filters
|
||||
3. **SubscriptionStatusBanner.vue** — Current tier, expiry, upgrade CTA
|
||||
4. **CheckoutView.vue** — Package selection → PaymentIntent → Stripe/internal redirect
|
||||
5. **PaymentStatusView.vue** — Success/cancel pages after Stripe redirect
|
||||
6. **Admin Ledger View** (already spec'd in [`docs/sf/epic_10_admin_frontend_spec.md`](dev/service_finder/docs/sf/epic_10_admin_frontend_spec.md)) — Paginated financial ledger with user join data
|
||||
|
||||
---
|
||||
|
||||
## 6. GAP ANALYSIS SUMMARY
|
||||
|
||||
| Area | Status | Criticality | Action |
|
||||
|------|--------|-------------|--------|
|
||||
| **Backend Models** | ✅ Complete | — | No action needed |
|
||||
| **Backend API — User-facing** | ✅ Complete | — | No action needed |
|
||||
| **Backend API — Admin** | ✅ Complete | — | No action needed |
|
||||
| **Backend Services** | ✅ Mostly modular | ⚠️ Medium | Refactor `FinancialOrchestrator` to delegate ledger writes to `AtomicTransactionManager` |
|
||||
| **Workers → Ledger** | ✅ Connected | 🔴 Bug | Fix `subscription_worker.py:75` — `WalletType.SYSTEM` does not exist |
|
||||
| **Frontend — Wallet** | ❌ Missing | 🔴 Critical | Build `WalletBalanceCard.vue` + `TransactionHistory.vue` |
|
||||
| **Frontend — Checkout** | ❌ Missing | 🔴 Critical | Build subscription purchase flow (catalog → intent → payment → confirmation) |
|
||||
| **Frontend — Admin Ledger** | ❌ Missing | 🟡 Medium | Build admin ledger view (already spec'd) |
|
||||
|
||||
---
|
||||
|
||||
## 7. RECOMMENDED ACTIONS (Priority Order)
|
||||
|
||||
### 🔴 P0 — Critical Bugs
|
||||
1. **Fix `subscription_worker.py:75`**: Change `WalletType.SYSTEM` to `WalletType.EARNED` (or add `SYSTEM` to the enum)
|
||||
2. **Fix `FinancialOrchestrator.refund_payment()` line 399/403**: Code references `Wallet.wallet_type` field which doesn't exist on the Wallet model; also references `wallet.balance` which doesn't exist. This code would **crash at runtime**.
|
||||
|
||||
### 🔴 P0 — Missing Frontend
|
||||
3. Build `WalletBalanceCard.vue` — user wallet summary display
|
||||
4. Build `TransactionHistory.vue` — paginated transaction list
|
||||
5. Build subscription checkout flow (catalog → Intent → payment → confirmation)
|
||||
|
||||
### 🟡 P1 — Code Quality
|
||||
6. Refactor `FinancialOrchestrator.process_payment()` to delegate to `AtomicTransactionManager` for ledger creation (eliminate dual ledger-writing paths)
|
||||
7. Audit `FinancialOrchestrator.refund_payment()` — it references non-existent fields (`Wallet.wallet_type`, `Wallet.balance`)
|
||||
|
||||
### 🟢 P2 — Documentation
|
||||
8. Update [`epic_3_financial_motor_architecture.md`](dev/service_finder/docs/sf/epic_3_financial_motor_architecture.md:1) with current service file structure
|
||||
9. Create `logic_spec_financial_manager.md` documenting the full purchase lifecycle flow with Mermaid sequence diagram
|
||||
|
||||
---
|
||||
|
||||
*Audit completed by Rendszer-Architect. Ready for Gitea ticket creation.*
|
||||
203
docs/p0_expense_500_import_bugfix_2026-07-26.md
Normal file
203
docs/p0_expense_500_import_bugfix_2026-07-26.md
Normal file
@@ -0,0 +1,203 @@
|
||||
# P0 Root Cause Analysis: POST /api/v1/expenses/ 500 Internal Server Error
|
||||
|
||||
**Date:** 2026-07-26
|
||||
**Gitea Issue:** [#421](https://gitea.kincses.dev/kincses/service-finder/issues/421)
|
||||
**Severity:** P0 (Critical — breaks entire expense creation flow)
|
||||
**Status:** Architect Analysis Complete — Awaiting Code Fix
|
||||
|
||||
---
|
||||
|
||||
## 📋 Executive Summary
|
||||
|
||||
The `POST /api/v1/expenses/` endpoint crashes with a `500 Internal Server Error` because the helper function `_check_org_capability()` contains a **broken import path** and a **wrong join column name**. Both defects are located in `backend/app/api/v1/endpoints/expenses.py`.
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Root Cause Trace
|
||||
|
||||
### Traceback (from `docker compose logs sf_api`)
|
||||
|
||||
```
|
||||
File "/app/app/api/v1/endpoints/expenses.py", line 377, in create_expense
|
||||
can_approve = await _check_org_capability(db, current_user.id, organization_id, "can_approve_expense")
|
||||
File "/app/app/api/v1/endpoints/expenses.py", line 104, in _check_org_capability
|
||||
from app.models.fleet.org_role import OrgRole
|
||||
ModuleNotFoundError: No module named 'app.models.fleet.org_role'
|
||||
```
|
||||
|
||||
### Call Flow
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["POST /api/v1/expenses/"] --> B["expenses.py: create_expense() line 377"]
|
||||
B --> C["_check_org_capability() line 93"]
|
||||
C --> D["line 104: from app.models.fleet.org_role import OrgRole"]
|
||||
D --> E["ModuleNotFoundError 💥"]
|
||||
E --> F["500 Internal Server Error"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Bug #1: Wrong Import Path (line 104)
|
||||
|
||||
### Broken Code
|
||||
|
||||
```python
|
||||
# File: backend/app/api/v1/endpoints/expenses.py, line 104
|
||||
from app.models.fleet.org_role import OrgRole
|
||||
```
|
||||
|
||||
### Why It Fails
|
||||
|
||||
There is **no directory** `backend/app/models/fleet/` and no file `org_role.py` at that path.
|
||||
|
||||
### Correct Import
|
||||
|
||||
The `OrgRole` model lives in `backend/app/models/marketplace/organization.py`:
|
||||
|
||||
```python
|
||||
from app.models.marketplace.organization import OrgRole
|
||||
```
|
||||
|
||||
### Evidence: Other Files Import Correctly
|
||||
|
||||
| File | Import | Status |
|
||||
|------|--------|--------|
|
||||
| `backend/app/api/deps.py:13` | `from app.models.marketplace.organization import OrgRole, OrganizationMember` | ✅ Correct |
|
||||
| `backend/app/api/v1/endpoints/assets.py:14` | `from app.models import ... OrgRole` (via `__init__.py`) | ✅ Correct |
|
||||
| `backend/app/api/v1/endpoints/organizations.py:23` | `from app.models.marketplace.organization import ... OrgRole` | ✅ Correct |
|
||||
| `backend/app/api/v1/endpoints/expenses.py:104` | `from app.models.fleet.org_role import OrgRole` | ❌ **BROKEN** |
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Bug #2: Wrong Join Column (line 108)
|
||||
|
||||
### Broken Code
|
||||
|
||||
```python
|
||||
# File: backend/app/api/v1/endpoints/expenses.py, lines 106-114
|
||||
stmt = (
|
||||
select(OrgRole.permissions)
|
||||
.join(OrganizationMember, OrganizationMember.role == OrgRole.name) # ← BUG
|
||||
.where(
|
||||
OrganizationMember.user_id == user_id,
|
||||
OrganizationMember.organization_id == organization_id,
|
||||
OrganizationMember.status == "active",
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
### Why It Fails
|
||||
|
||||
The `OrgRole` model's column is `name_key` (line 55 of the model), **not** `name`:
|
||||
|
||||
```python
|
||||
class OrgRole(Base):
|
||||
__tablename__ = "org_roles"
|
||||
__table_args__ = {"schema": "fleet"}
|
||||
|
||||
name_key: Mapped[str] = mapped_column(String(50), unique=True, nullable=False, index=True)
|
||||
# ... no 'name' column exists
|
||||
```
|
||||
|
||||
Attempting `OrgRole.name` would raise an `AttributeError` after the import is fixed, causing a second crash.
|
||||
|
||||
### Correct Join
|
||||
|
||||
```python
|
||||
.join(OrganizationMember, OrganizationMember.role == OrgRole.name_key)
|
||||
```
|
||||
|
||||
### Evidence: `assets.py` Has the Correct Pattern
|
||||
|
||||
The `assets.py` endpoint uses the identical capability-check pattern correctly:
|
||||
|
||||
```python
|
||||
# assets.py line 80-83 — CORRECT pattern
|
||||
role_stmt = select(OrgRole.permissions).where(
|
||||
OrgRole.name_key == org_role_name, # ← Uses name_key, not .name
|
||||
OrgRole.is_active == True
|
||||
).limit(1)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧩 Impact Analysis
|
||||
|
||||
### What's Broken
|
||||
|
||||
- **Entire `POST /api/v1/expenses/` endpoint** — all expense creation requests fail with 500
|
||||
- The crash occurs at the **RBAC Phase 2 capability check** (JSONB-based `can_approve_expense`)
|
||||
- This blocks the P0 Smart Expense Workflow (odometer normalization, VAT calculation, gamification hooks, AssetEvent linking)
|
||||
|
||||
### What Still Works
|
||||
|
||||
- `GET /api/v1/expenses/` — list all expenses (does not call `_check_org_capability`)
|
||||
- `GET /api/v1/expenses/{asset_id}` — list asset expenses (does not call `_check_org_capability`)
|
||||
- `PUT /api/v1/expenses/{expense_id}` — update expense (does not call `_check_org_capability`)
|
||||
|
||||
### Why This Was Not Caught
|
||||
|
||||
1. The function uses a lazy import inside the function body — the import error only surfaces at runtime, not at module load time.
|
||||
2. No e2e test exercises the `POST /expenses/` path through this function.
|
||||
3. The `backend/app/tests/e2e/test_expense_flow.py` test targets `POST /api/v1/expenses/add`, which is a **different, legacy endpoint** — not the current `POST /api/v1/expenses/`.
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Fix Plan (2 Lines, 1 File)
|
||||
|
||||
### File: `backend/app/api/v1/endpoints/expenses.py`
|
||||
|
||||
**Change 1 — Line 104:** Replace the broken import:
|
||||
|
||||
```diff
|
||||
- from app.models.fleet.org_role import OrgRole
|
||||
+ from app.models.marketplace.organization import OrgRole
|
||||
```
|
||||
|
||||
**Change 2 — Line 108:** Fix the join column:
|
||||
|
||||
```diff
|
||||
- .join(OrganizationMember, OrganizationMember.role == OrgRole.name)
|
||||
+ .join(OrganizationMember, OrganizationMember.role == OrgRole.name_key)
|
||||
```
|
||||
|
||||
### Optional: Hoist to top-level imports
|
||||
|
||||
Since `OrganizationMember` is already imported at the top of the file (line 46), move `OrgRole` there too:
|
||||
|
||||
```python
|
||||
# line 46 area — add OrgRole
|
||||
from app.models.marketplace.organization import OrganizationMember, OrgRole
|
||||
```
|
||||
|
||||
Then remove the lazy import body from `_check_org_capability()`.
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Verification
|
||||
|
||||
1. **Unit test:** `POST /api/v1/expenses/` with valid payload → 201 Created
|
||||
2. **Capability check:** User with `can_approve_expense: true` → expense `APPROVED`
|
||||
3. **Capability check:** User without `can_approve_expense` → `PENDING_APPROVAL`
|
||||
4. **No ModuleNotFoundError** at any point
|
||||
|
||||
---
|
||||
|
||||
## 📊 Summary
|
||||
|
||||
| Item | Detail |
|
||||
|------|--------|
|
||||
| **Endpoint** | `POST /api/v1/expenses/` |
|
||||
| **Error** | `ModuleNotFoundError: No module named 'app.models.fleet.org_role'` |
|
||||
| **Crash File** | `backend/app/api/v1/endpoints/expenses.py` |
|
||||
| **Crash Line** | 104 |
|
||||
| **Root Cause 1** | Wrong import: `app.models.fleet.org_role` → `app.models.marketplace.organization` |
|
||||
| **Root Cause 2** | Wrong column: `OrgRole.name` → `OrgRole.name_key` |
|
||||
| **Gitea Issue** | #421 |
|
||||
| **Fix Complexity** | Trivial (2 lines) |
|
||||
| **Risk** | Minimal |
|
||||
|
||||
---
|
||||
|
||||
*Analysis by: Service Finder Rendszer-Architect — 2026-07-26*
|
||||
248
docs/p0_expense_edit_modal_root_cause_analysis.md
Normal file
248
docs/p0_expense_edit_modal_root_cause_analysis.md
Normal file
@@ -0,0 +1,248 @@
|
||||
# P0 Root Cause Analysis: Expense Edit Modal Data Binding Failure
|
||||
|
||||
**Date:** 2026-07-26
|
||||
**Scope:** Frontend `CostEntryWizard.vue` + Backend `expenses.py`
|
||||
**Symptom:** Edit modal defaults category to "Finanszírozás", fails to bind vehicle/provider, missing specific details (fuel amount).
|
||||
|
||||
---
|
||||
|
||||
## 1. Data Storage: How Expenses & Specific Details Are Persisted
|
||||
|
||||
### DB Table: `fleet_finance.asset_costs`
|
||||
|
||||
| Column | Type | Stores |
|
||||
|--------|------|--------|
|
||||
| `category_id` | `int4` | FK → `fleet_finance.cost_categories.id` — **leaf/subcategory ID**, not parent |
|
||||
| `service_provider_id` | `int4` | FK → `marketplace.service_providers.id` — the selected provider |
|
||||
| `vendor_organization_id` | `int4` | FK → `fleet.organizations.id` — B2B vendor org |
|
||||
| `external_vendor_name` | `varchar(200)` | Free-text vendor name |
|
||||
| `data` | `JSONB` | **All specific details:** `liters`, `net_amount`, `vat_rate`, `invoice_number`, `invoice_date`, `payment_method`, `payment_deadline`, `mileage_at_cost`, `vendor_id`, `vendor_name`, `vendor_source` |
|
||||
| `amount_gross` | `numeric(18,2)` | Gross (Bruttó) amount |
|
||||
| `amount_net` | `numeric(18,2)` | Net amount |
|
||||
| `invoice_number` | `varchar(100)` | Dedicated column (duplicated in `data`) |
|
||||
|
||||
**Key Finding:** Specific details like fuel liters, payment method, invoice date, etc. are stored in the `data` JSONB column. There is **no separate related table** for fuel-specific details.
|
||||
|
||||
---
|
||||
|
||||
## 2. Backend API Analysis
|
||||
|
||||
### 2A. No Single-Expense Read Endpoint Exists
|
||||
|
||||
The backend has:
|
||||
- `GET /expenses/` — lists all expenses (paginated)
|
||||
- `GET /expenses/{asset_id}` — lists expenses for a specific asset
|
||||
- `PUT /expenses/{expense_id}` — updates an expense
|
||||
|
||||
**There is NO `GET /expenses/by-id/{expense_id}` endpoint** that returns a single enriched expense with all resolved relationships.
|
||||
|
||||
### 2B. List Endpoints Return Raw ORM Objects
|
||||
|
||||
Both list endpoints return `result.scalars().all()` — raw SQLAlchemy `AssetCost` objects serialized by FastAPI's default JSON encoder. The `AssetCostResponse` Pydantic schema (which includes `category_name`, `category_code`, `vendor_name`, `description`, `mileage_at_cost`) is **defined but never used** by the list endpoints.
|
||||
|
||||
This means:
|
||||
- `data` (JSONB) is serialized as a raw dict — this part actually works
|
||||
- But `category_name`, `category_code`, `vendor_name` are **not resolved** (they require JOINs or relationship loading)
|
||||
- The response lacks the parent category ID (only the leaf `category_id` is present)
|
||||
|
||||
### 2C. PUT Endpoint Has Partial Data Loss Risk
|
||||
|
||||
The `update_expense` function handles `mileage_at_cost` and `description` inside `data` JSONB correctly, but the frontend's `updateExpense()` in `cost.ts` sends `service_provider_id` only if it's in the payload. The `PUT` endpoint's `AssetCostUpdate` schema does include `service_provider_id: Optional[int]`, so if the frontend sends it, it would work.
|
||||
|
||||
---
|
||||
|
||||
## 3. Frontend Data Flow: The Edit Path
|
||||
|
||||
### Step 1: User clicks Edit → `CostsView.vue` line 527
|
||||
|
||||
```typescript
|
||||
function openEditWizard(cost: any) {
|
||||
const vehicleId = cost.asset_id || cost.vehicle_id
|
||||
editVehicle.value = vehicleStore.vehicles.find(v => v.id === vehicleId) || null
|
||||
editCostData.value = cost // ← passes raw list-response object directly
|
||||
showEditWizard.value = true
|
||||
}
|
||||
```
|
||||
|
||||
The `cost` object comes from the `GET /expenses` list response — a raw SQLAlchemy object with fields: `id`, `asset_id`, `organization_id`, `category_id`, `amount_net`, `amount_gross`, `vat_rate`, `currency`, `date`, `invoice_number`, `status`, `data`, `service_provider_id`, `vendor_organization_id`, `external_vendor_name`, `invoice_date`, `fulfillment_date`.
|
||||
|
||||
### Step 2: Hydration watcher → `CostEntryWizard.vue` lines 972-988
|
||||
|
||||
```typescript
|
||||
watch(() => props.editCost, (cost) => {
|
||||
if (!cost) return
|
||||
form.date = cost.date ? getLocalDateString(new Date(cost.date)) : getLocalDateString()
|
||||
form.mainCategory = String(cost.category_id || '') // ← BUG #1
|
||||
form.subCategory = String(cost.sub_category_id || '') // ← BUG #1 (never exists)
|
||||
form.netAmount = cost.data?.net_amount || cost.amount_net || 0
|
||||
form.vatRate = cost.data?.vat_rate || 27
|
||||
form.grossAmount = cost.amount_gross || 0
|
||||
form.currency = cost.currency || 'HUF'
|
||||
form.invoiceNumber = cost.data?.invoice_number || ''
|
||||
form.invoiceDate = cost.data?.invoice_date || ''
|
||||
form.paymentDeadline = cost.data?.payment_deadline || ''
|
||||
form.paymentMethod = cost.data?.payment_method || 'TRANSFER'
|
||||
form.mileageAtCost = cost.mileage_at_cost || cost.data?.mileage_at_cost || null
|
||||
form.liters = cost.data?.liters || null // ← BUG #2 area
|
||||
// NOTE: selectedProvider is NEVER set from cost.service_provider_id! ← BUG #3
|
||||
// NOTE: selectedVehicleId is NEVER set from cost.asset_id! ← BUG #4
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Root Cause Map: 5 Specific Bugs
|
||||
|
||||
### 🔴 BUG #1 — Category Hierarchy Collapse (CRITICAL)
|
||||
|
||||
**Location:** `CostEntryWizard.vue:976`
|
||||
|
||||
**What happens:**
|
||||
- `category_id` in the DB is the **leaf/subcategory** ID (e.g., `id=4` for "Üzemanyag / Benzin")
|
||||
- The wizard has a **two-level** selector: `mainCategory` (parent) → `subCategory` (leaf)
|
||||
- The hydration sets `form.mainCategory = String(cost.category_id || '')` — puts the leaf ID into the parent dropdown
|
||||
- `form.subCategory = String(cost.sub_category_id || '')` — `sub_category_id` doesn't exist in the response, so it's always `''`
|
||||
- When the component renders, `mainCategory` is set to an ID that doesn't match any main category (since it's a leaf), so the `<select>` defaults to the **first option**: "Finanszírozás"
|
||||
- Subcategories are never populated because `onMainCategoryChange()` was never triggered with the correct parent ID
|
||||
|
||||
**User experience:** Category dropdown shows "Finanszírozás" (first item) instead of the actual category.
|
||||
|
||||
**Fix approach:**
|
||||
1. Backend must return `parent_category_id` alongside `category_id` in the response, OR
|
||||
2. Frontend must look up the parent from the flattened category tree (`window.__allCostCategories`) and split `category_id` into main+sub correctly
|
||||
|
||||
### 🔴 BUG #2 — Vehicle Not Bound When Not in Store (HIGH)
|
||||
|
||||
**Location:** `CostsView.vue:530` + `CostEntryWizard.vue:93`
|
||||
|
||||
**What happens:**
|
||||
- `openEditWizard` finds the vehicle by `cost.asset_id` in `vehicleStore.vehicles`
|
||||
- If the vehicle is not in the store (e.g., filtered by another org, or not loaded), `editVehicle` is `null`
|
||||
- In `CostEntryWizard.vue:77`, when `editVehicle` is null AND `userVehicles.length > 0`, the global vehicle dropdown is shown
|
||||
- But `selectedVehicleId` is **never set** from `cost.asset_id` in the hydration watcher
|
||||
- Result: vehicle dropdown shows "Select Vehicle..." with nothing selected
|
||||
|
||||
**Fix approach:** In the hydration watcher, set `selectedVehicleId.value = cost.asset_id` when `props.vehicle` is null.
|
||||
|
||||
### 🔴 BUG #3 — Provider/ServiceProvider Not Hydrated (HIGH)
|
||||
|
||||
**Location:** `CostEntryWizard.vue:972-988` (absence of provider hydration)
|
||||
|
||||
**What happens:**
|
||||
- The DB stores `service_provider_id` (int) and `external_vendor_name` (string) directly on the AssetCost row
|
||||
- The list response includes these fields
|
||||
- But the hydration watcher **never reads** `cost.service_provider_id` or `cost.external_vendor_name`
|
||||
- `selectedProvider` ref is never populated, so `ProviderAutocomplete` shows empty
|
||||
- The user sees no provider selected even though one was saved
|
||||
|
||||
**Fix approach:** In the hydration watcher, if `cost.service_provider_id` exists, construct a `ProviderSearchResult` object and set `selectedProvider.value`. If only `external_vendor_name` exists, set it as the provider name.
|
||||
|
||||
### 🟡 BUG #4 — `data.liters` May Be Lost in List Serialization (MEDIUM)
|
||||
|
||||
**Location:** `CostEntryWizard.vue:987`
|
||||
|
||||
**What happens:**
|
||||
- The hydration reads `cost.data?.liters`
|
||||
- The list endpoint returns raw SQLAlchemy objects; the `data` JSONB column **should** serialize correctly as a dict via FastAPI's `jsonable_encoder`
|
||||
- However, if the `data` column is `None` or `{}` in the DB, `cost.data?.liters` returns `undefined`, and `|| null` gives `null`
|
||||
- This should work for most cases, but there's a subtle issue: the `data` field on the SQLAlchemy model has `server_default="'{}'::jsonb"`, but if a row was inserted without going through SQLAlchemy (e.g., raw SQL), `data` could be `None`
|
||||
|
||||
**Fix approach:** Add defensive null-check: `(cost.data && cost.data.liters) ? cost.data.liters : null`
|
||||
|
||||
### 🟡 BUG #5 — `mainCategory` Watcher Clears `subCategory` on Hydration (MEDIUM)
|
||||
|
||||
**Location:** `CostEntryWizard.vue:818-819`
|
||||
|
||||
**What happens:**
|
||||
- The hydration watcher runs and sets `form.mainCategory = String(cost.category_id || '')`
|
||||
- Vue's reactivity triggers the `v-model` change on the `<select>` element
|
||||
- This calls `@change="onMainCategoryChange"` which does:
|
||||
```typescript
|
||||
form.subCategory = '' // ← CLEARS whatever the hydration may try to set
|
||||
subCategories.value = []
|
||||
```
|
||||
- Even if we fix BUG #1 to correctly populate subCategory, the `@change` handler would clear it
|
||||
|
||||
**Fix approach:** In the hydration watcher, after setting `mainCategory`, manually call `onMainCategoryChange()` (which populates `subCategories`), and THEN set `form.subCategory` to the correct leaf ID. The order must be: set mainCategory → populate subCategories → set subCategory.
|
||||
|
||||
---
|
||||
|
||||
## 5. Data Flow Diagram
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ BACKEND │
|
||||
│ │
|
||||
│ GET /expenses?organization_id=X │
|
||||
│ └→ returns: [AssetCost ORM objects] │
|
||||
│ fields: id, asset_id, category_id(leaf!), service_provider_id, │
|
||||
│ amount_gross, date, data{liters, net_amount, ...}, │
|
||||
│ external_vendor_name, invoice_date, ... │
|
||||
│ │
|
||||
│ MISSING: parent_category_id, category_name, vendor_name, │
|
||||
│ vehicle_name, license_plate │
|
||||
│ │
|
||||
│ There is NO GET /expenses/by-id/{id} endpoint! │
|
||||
└──────────────────────┬──────────────────────────────────────────────┘
|
||||
│ raw ORM objects (FastAPI jsonable_encoder)
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ FRONTEND │
|
||||
│ │
|
||||
│ CostsView.vue │
|
||||
│ └→ openEditWizard(cost) │
|
||||
│ editCostData.value = cost ← raw list item │
|
||||
│ editVehicle.value = store.find(v => v.id === cost.asset_id) │
|
||||
│ │
|
||||
│ CostEntryWizard.vue │
|
||||
│ └→ watch(editCost): │
|
||||
│ form.mainCategory = String(cost.category_id) ← BUG: leaf ID │
|
||||
│ form.subCategory = String(cost.sub_category_id) ← BUG: undef │
|
||||
│ form.liters = cost.data?.liters │
|
||||
│ selectedProvider = ??? ← BUG: never │
|
||||
│ selectedVehicleId = ??? ← BUG: never │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Recommended Fix Plan
|
||||
|
||||
### Phase 1: Backend (Minimal — No Schema Changes)
|
||||
|
||||
1. **Create `GET /expenses/by-id/{expense_id}` endpoint** in `expenses.py` that:
|
||||
- Fetches a single `AssetCost` by ID
|
||||
- JOINs `CostCategory` to resolve `category_name`, `category_code`, `parent_id`
|
||||
- JOINs `ServiceProvider` to resolve provider name
|
||||
- Returns the data using `AssetCostResponse` schema (already defined!)
|
||||
- Includes `parent_category_id` in the response for the two-level dropdown
|
||||
|
||||
2. **Optionally, enrich the list endpoint** to return `parent_category_id` by joining `CostCategory.parent_id`, but this is less critical if the single-expense endpoint is used for edit hydration.
|
||||
|
||||
### Phase 2: Frontend (Edit Path)
|
||||
|
||||
1. **In `CostsView.vue` `openEditWizard()`:** fetch the full expense detail from the new `GET /expenses/by-id/{id}` endpoint instead of passing the raw list object.
|
||||
|
||||
2. **In `CostEntryWizard.vue` hydration watcher:**
|
||||
- Split `category_id` into `mainCategory` (parent) and `subCategory` (leaf) using `window.__allCostCategories`
|
||||
- Call `onMainCategoryChange()` after setting `mainCategory` to populate subcategories, then set `subCategory`
|
||||
- Set `selectedProvider.value` from `cost.service_provider_id` + resolved provider name
|
||||
- Set `selectedVehicleId.value` from `cost.asset_id` when vehicle prop is null
|
||||
- Defensive null-check on `cost.data?.liters`
|
||||
|
||||
### Phase 3: Edge Cases
|
||||
|
||||
1. When `service_provider_id` is null but `external_vendor_name` is set, populate the provider autocomplete with the free-text name
|
||||
2. Handle the case where `vendor_organization_id` points to a B2B org (different from `service_provider_id`)
|
||||
|
||||
---
|
||||
|
||||
## 7. Files That Need Changes
|
||||
|
||||
| File | Change Type | Priority |
|
||||
|------|-------------|----------|
|
||||
| `backend/app/api/v1/endpoints/expenses.py` | Add `GET /expenses/by-id/{id}` endpoint | P0 |
|
||||
| `backend/app/schemas/asset_cost.py` | Add `parent_category_id` to `AssetCostResponse` | P0 |
|
||||
| `frontend_app/src/views/costs/CostsView.vue` | Fetch full expense detail on edit | P0 |
|
||||
| `frontend_app/src/components/cost/CostEntryWizard.vue` | Fix category split, provider hydration, vehicle selection | P0 |
|
||||
| `frontend_app/src/stores/cost.ts` | Add `fetchExpenseById()` action (optional, if store-based) | P1 |
|
||||
149
docs/p0_wallet_balance_500_bugfix_2026-07-26.md
Normal file
149
docs/p0_wallet_balance_500_bugfix_2026-07-26.md
Normal file
@@ -0,0 +1,149 @@
|
||||
# P0 Bug #420: `GET /api/v1/billing/wallet/balance` - 500 Internal Server Error
|
||||
|
||||
**Dátum:** 2026-07-26
|
||||
**Súlyosság:** P0 - Blokkoló (minden wallet/balance hívást érint)
|
||||
**Státusz:** Megoldva (pycache purge + restart)
|
||||
|
||||
---
|
||||
|
||||
## 1. Hiba Leírása
|
||||
|
||||
A `GET /api/v1/billing/wallet/balance` végpont `500 Internal Server Error` hibával tért vissza bizonyos meglévő felhasználók (pl. user_id=86, 28) esetén.
|
||||
|
||||
### Hibaüzenet a logból:
|
||||
```
|
||||
2026-07-26 12:37:49,388 [ERROR] app.api.v1.endpoints.billing: Pénztárca egyenleg lekérdezési hiba: 'FinancialLedger' object has no attribute 'description'
|
||||
```
|
||||
|
||||
### Érintett fájlok:
|
||||
- [`backend/app/models/system/audit.py:78`](backend/app/models/system/audit.py:78) — `FinancialLedger` SQLAlchemy modell
|
||||
- [`backend/app/services/billing_engine.py:1078`](backend/app/services/billing_engine.py:1078) — `get_user_balance()`
|
||||
- [`backend/app/services/billing_engine.py:598`](backend/app/services/billing_engine.py:598) — `get_wallet_summary()`
|
||||
- [`backend/app/api/v1/endpoints/billing.py:298`](backend/app/api/v1/endpoints/billing.py:298) — `get_wallet_balance()` endpoint
|
||||
|
||||
---
|
||||
|
||||
## 2. Root Cause Analysis
|
||||
|
||||
### 2.1 A FinancialLedger modell
|
||||
|
||||
A [`FinancialLedger`](backend/app/models/system/audit.py:78) modell **NEM rendelkezik `description` oszloppal**. Az egyetlen "leíró" mező a `details` (JSONB). A modell oszlopai:
|
||||
|
||||
```
|
||||
id, user_id, person_id, amount, currency, transaction_type,
|
||||
related_agent_id, details, created_at, entry_type, balance_after,
|
||||
wallet_type, issuer_id, invoice_status, tax_amount, gross_amount,
|
||||
net_amount, transaction_id, status
|
||||
```
|
||||
|
||||
### 2.2 A jelenlegi forráskód helyes
|
||||
|
||||
A [`billing_engine.py`](backend/app/services/billing_engine.py:542) `get_transaction_history()` metódusa helyesen használja a `details` JSONB mezőt a leírás kinyerésére:
|
||||
|
||||
```python
|
||||
# Line 577-579
|
||||
desc = None
|
||||
if entry.details and isinstance(entry.details, dict):
|
||||
desc = entry.details.get("description") or entry.details.get("desc") or None
|
||||
```
|
||||
|
||||
A [`billing.py`](backend/app/api/v1/endpoints/billing.py:373) router szintén helyesen:
|
||||
```python
|
||||
# Line 376-378
|
||||
description = ""
|
||||
if entry.details and isinstance(entry.details, dict):
|
||||
description = entry.details.get("description", "")
|
||||
```
|
||||
|
||||
### 2.3 A tényleges ok: Stale Python Bytecode Cache
|
||||
|
||||
Az MD5 ellenőrzés igazolta, hogy a konténerben futó forráskód és a host-on lévő forráskód **azonos**:
|
||||
|
||||
| Fájl | MD5 |
|
||||
|------|-----|
|
||||
| `billing.py` (host & container) | `6b00968c129376e3db630e266835261b` |
|
||||
| `billing_engine.py` (host & container) | `54d86868aa077bebfeddaf32c69642fc` |
|
||||
|
||||
A konténerben azonban régi `.pyc` bytecode fájlok voltak cache-elve:
|
||||
```
|
||||
/app/app/api/v1/endpoints/__pycache__/billing.cpython-312.pyc
|
||||
/app/app/services/__pycache__/billing_engine.cpython-312.pyc
|
||||
```
|
||||
|
||||
Ezek a `.pyc` fájlok egy **korábbi forráskód-verzióhoz** tartoztak, ahol még létezett `entry.description` hivatkozás a `FinancialLedger` objektumokon (vagy egy másik séma érvényben volt).
|
||||
|
||||
### 2.4 Miért nem frissültek a .pyc fájlok?
|
||||
|
||||
A `docker-compose.yml`-ben a `sf_api` konténer forráskódja volume mount-tal van csatolva (nem COPY). Amikor a forráskód változik a host-on, a Python értelmező a konténerben azonnal látja az új `.py` fájlokat, de a már legenerált `.pyc` cache fájlok megmaradnak. Ha a `.py` fájl timestamp-je régebbi, mint a `.pyc`-é (vagy a Python nem érzékeli a változást), a régi bytecode fut.
|
||||
|
||||
---
|
||||
|
||||
## 3. Megoldás
|
||||
|
||||
### Végrehajtott lépések:
|
||||
|
||||
1. **Pycache törlése a konténerben:**
|
||||
```bash
|
||||
docker compose exec sf_api find /app -type d -name "__pycache__" -exec rm -rf {} +
|
||||
```
|
||||
|
||||
2. **Konténer újraindítása:**
|
||||
```bash
|
||||
docker compose restart sf_api
|
||||
```
|
||||
|
||||
3. **Verifikáció:** A restart után az endpoint már NEM dob 500-as hibát. A logokban nem jelenik meg több `description` hiba.
|
||||
|
||||
---
|
||||
|
||||
## 4. Code Mode Blueprint (Preventív Intézkedés)
|
||||
|
||||
Bár a jelenlegi forráskód helyes, az alábbi preventív intézkedést javaslom a `Code Mode`-ban:
|
||||
|
||||
### 4.1 Docker Compose: pycache purge a startup előtt
|
||||
|
||||
A `docker-compose.yml` `sf_api` szolgáltatás `command` sorában futtassunk egy automatikus pycache purge-öt a Uvicorn indítása előtt:
|
||||
|
||||
```yaml
|
||||
command: >
|
||||
/bin/sh -c "
|
||||
find /app -type d -name '__pycache__' -exec rm -rf {} + 2>/dev/null;
|
||||
uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
|
||||
"
|
||||
```
|
||||
|
||||
### 4.2 Opcionális: `.pyc` fájlok teljes letiltása fejlesztői környezetben
|
||||
|
||||
A `docker-compose.yml` environment változói közé:
|
||||
```yaml
|
||||
environment:
|
||||
- PYTHONDONTWRITEBYTECODE=1
|
||||
```
|
||||
|
||||
Ez megakadályozza, hogy a Python egyáltalán `.pyc` fájlokat generáljon, így mindig a forráskódból fog futni.
|
||||
|
||||
---
|
||||
|
||||
## 5. Függelék: Mermaid Folyamatábra
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client
|
||||
participant FastAPI as billing.py router
|
||||
participant BillingEngine as billing_engine.py
|
||||
participant Model as FinancialLedger
|
||||
participant DB as PostgreSQL
|
||||
|
||||
Client->>FastAPI: GET /api/v1/billing/wallet/balance
|
||||
FastAPI->>BillingEngine: get_user_balance(db, user_id)
|
||||
BillingEngine->>BillingEngine: get_wallet_summary(db, user_id)
|
||||
BillingEngine->>DB: SELECT * FROM identity.wallets WHERE user_id=?
|
||||
BillingEngine->>DB: SELECT * FROM audit.financial_ledger LIMIT 10
|
||||
BillingEngine->>Model: entry.details.get("description")
|
||||
Note over Model: Helyes: details JSONB mezőből olvas
|
||||
Model-->>BillingEngine: description string
|
||||
BillingEngine-->>FastAPI: {"earned": X, "purchased": Y, ...}
|
||||
FastAPI-->>Client: 200 OK + JSON balances
|
||||
```
|
||||
|
||||
**A hiba idején** a `.pyc` bytecode `entry.description`-t próbált elérni, ami nem létező attribútum a `FinancialLedger` modellen. Ez `AttributeError`-t okozott, amit a `billing.py` router `except Exception` ága elkapott és 500-ra fordított.
|
||||
Reference in New Issue
Block a user