Files
service-finder/docs/p0_garage_anatomy_audit.md
2026-06-26 01:21:15 +00:00

453 lines
23 KiB
Markdown

# 🏗️ P0 DISCOVERY: Complete Organization (Garage) Data Anatomy & Mapping
**Audit Date:** 2026-06-25
**Scope:** Full-stack data anatomy of the `Organization` (Garage) entity
**Layers Covered:** Database Schema → API Endpoints → Frontend Wiring
**Report Version:** 1.0
---
## Table of Contents
1. [Executive Summary](#1-executive-summary)
2. [Database Schema & Relationships](#2-database-schema--relationships)
- 2.1 [The Address Question](#21-the-address-question-architects-hypothesis-verified)
- 2.2 [Contact Person: Dual System](#22-contact-person-dual-system)
- 2.3 [Relationships Map](#23-relationships-map)
- 2.4 [Subscription Architecture](#24-subscription-architecture)
- 2.5 [Digital Twin / Soft Delete](#25-digital-twin--soft-delete)
3. [API Coverage Mapping](#3-api-coverage-mapping)
- 3.1 [Complete Endpoint Table](#31-complete-endpoint-table)
- 3.2 [Request/Response Schema Analysis](#32-requestresponse-schema-analysis)
- 3.3 [Missing CRUD Operations](#33-missing-crud-operations)
4. [Frontend Wiring Analysis](#4-frontend-wiring-analysis)
- 4.1 [Garage List Page](#41-garage-list-page-frontend_adminpagesgaragesindexvue)
- 4.2 [Garage Detail Page](#42-garage-detail-page-frontend_adminpagesgaragesidindexvue)
- 4.3 [Edit Modal Wiring](#43-edit-modal-wiring)
- 4.4 [Dead Ends & Gaps](#44-dead-ends--gaps)
5. [Findings Summary](#5-findings-summary)
6. [Recommendations](#6-recommendations)
---
## 1. Executive Summary
This report presents a comprehensive three-layer audit of the **Organization (Garage)** entity across the Service Finder system. The audit covers the database schema ( [`backend/app/models/marketplace/organization.py:73`](backend/app/models/marketplace/organization.py:73) ), the admin API endpoints ( [`backend/app/api/v1/endpoints/admin_organizations.py:178`](backend/app/api/v1/endpoints/admin_organizations.py:178) ), and the frontend admin UI ( `/frontend_admin/pages/garages/` ).
**Key Findings:**
-**Address Architecture**: Three address types (Primary, Billing, Notification) exist as **flat denormalized columns** on `fleet.organizations`. The Architect's suspicion was **partially correct** — there is NO 1-to-Many `addresses` table with `address_type` enum. Instead, each address type has its own column prefix ( `address_*` , `billing_*` , `notification_*` ). A normalized `system.addresses` table exists but is only linked via a single `address_id` FK (not used for billing/notification distinction).
-**Contact Person**: A **dual system** exists — denormalized fields ( `contact_person_name` , `contact_email` , `contact_phone` ) on Organization for quick editing, AND a normalized `ContactPerson` model ( `fleet.contact_persons``identity.persons` ) with role/department/is_primary.
- ⚠️ **Missing CRUD**: `OrganizationFinancials` , `OrganizationRelationship` (B2B), `OrganizationSalesAssignment` , and `OrgRole` dynamic permissions exist in the database but have **zero admin API endpoints or frontend UI**.
- ⚠️ **Frontend Gap**: Billing and Notification `street_type` fields are missing from the frontend edit modal form, despite being present in the API schema.
---
## 2. Database Schema & Relationships
### 2.1 The Address Question (Architect's Hypothesis Verified)
**Source:** [`backend/app/models/marketplace/organization.py:73`](backend/app/models/marketplace/organization.py:73)
The database stores addresses as **three separate sets of flat denormalized columns** directly on the `fleet.organizations` table. This is NOT a normalized 1-to-Many `addresses` table with `address_type` enum.
#### Primary Address (lines 125-131)
```python
address_zip: Mapped[Optional[str]] # String(10)
address_city: Mapped[Optional[str]] # String(100)
address_street_name: Mapped[Optional[str]] # String(150)
address_street_type: Mapped[Optional[str]] # String(50)
address_house_number: Mapped[Optional[str]] # String(20)
address_hrsz: Mapped[Optional[str]] # String(50) - helyrajzi szám
plus_code: Mapped[Optional[str]] # String(20) - Plus code
```
#### Billing Address (lines 142-146)
```python
billing_zip: Mapped[Optional[str]] # String(10)
billing_city: Mapped[Optional[str]] # String(100)
billing_street_name: Mapped[Optional[str]] # String(150)
billing_street_type: Mapped[Optional[str]] # String(50)
billing_house_number: Mapped[Optional[str]] # String(20)
```
#### Notification Address (lines 149-153)
```python
notification_zip: Mapped[Optional[str]] # String(10)
notification_city: Mapped[Optional[str]] # String(100)
notification_street_name: Mapped[Optional[str]] # String(150)
notification_street_type: Mapped[Optional[str]] # String(50)
notification_house_number: Mapped[Optional[str]] # String(20)
```
#### Normalized Address System (Legacy/Secondary)
**Source:** [`backend/app/models/identity/address.py:39`](backend/app/models/identity/address.py:39)
There IS a normalized `system.addresses` table with GPS coordinates, geo-postal-code lookup, and full address text. The Organization has a single FK to it:
```python
address_id: Mapped[Optional[uuid.UUID]] = mapped_column(
PG_UUID(as_uuid=True), ForeignKey("system.addresses.id")
)
```
However, this `address_id` is **not used** for the billing/notification distinction. It appears to be a legacy holdover or a generalized address reference that the admin API does NOT populate or return.
**Conclusion:** The billing/notification address types are handled through column naming conventions, not through a normalized `address_type` enum. This works but is not extensible — adding a 4th address type (e.g., "postal", "legal") would require adding 5 more columns to the table.
---
### 2.2 Contact Person: Dual System
**Sources:**
- [`backend/app/models/marketplace/organization.py:136`](backend/app/models/marketplace/organization.py:136)
- [`backend/app/models/fleet/organization.py:95`](backend/app/models/fleet/organization.py:95)
#### System A: Denormalized (on Organization)
```python
contact_person_name: Mapped[Optional[str]] # String(255) - Kapcsolattartó neve
contact_email: Mapped[Optional[str]] # String(255) - Kapcsolattartó e-mail címe
contact_phone: Mapped[Optional[str]] # String(50) - Kapcsolattartó telefonszáma
```
#### System B: Normalized (ContactPerson model)
```python
class ContactPerson(Base):
__tablename__ = "contact_persons"
__table_args__ = {"schema": "fleet"}
id: int # PK
organization_id: int # FK → fleet.organizations.id
person_id: int # FK → identity.persons.id (BigInteger)
role: str # String(50) - "CEO", "Fleet Manager", etc.
is_primary: bool # default=False
department: Optional[str] # String(100)
notes: Optional[str] # Text
```
**Relationship:** `ContactPerson.person``identity.persons` (the actual person record with name, phone, email).
**How they interact:** The API loads both. The `GET /details` endpoint returns:
1. The denormalized `contact_person_name`, `contact_email`, `contact_phone` directly from the Organization record
2. A separate `primary_contact` object from the ContactPerson model (with role, department, person full_name, phone)
**Propagation logic** (in [`admin_organizations.py:1137`](backend/app/api/v1/endpoints/admin_organizations.py:1137)):
For `individual` orgs, the PUT endpoint propagates `contact_email``email` (User) and `contact_phone``phone` (Person).
---
### 2.3 Relationships Map
Below is the complete map of all relationships from the `Organization` entity, with their database schemas and connection types.
| Relationship | Schema.Table | FK Field | Type | Cascade | Admin API? |
|---|---|---|---|---|---|
| **assets** | `fleet.asset_assignments` | `organization_id` | `List[AssetAssignment]` | `all, delete-orphan` | ✅ GET vehicles |
| **members** | `fleet.organization_members` | `organization_id` | `List[OrganizationMember]` | `all, delete-orphan` | ✅ CRUD |
| **owner** | `identity.users` | `owner_id` | `Optional[User]` | — | ❌ Read-only via details |
| **financials** | `fleet.organization_financials` | `organization_id` | `List[OrganizationFinancials]` | `all, delete-orphan` | ❌ None |
| **service_profile** | `marketplace.service_profiles` | `organization_id` | `Optional[ServiceProfile]` | `SET NULL` | ❌ None |
| **branches** | `fleet.branches` | `organization_id` | `List[Branch]` | `all, delete-orphan` | ❌ None |
| **legal_owner** | `identity.persons` | `legal_owner_id` | `Optional[Person]` | — | ❌ Read-only via details |
| **subscription_tier** | `system.subscription_tiers` | `subscription_tier_id` | `Optional[SubscriptionTier]` | — | ✅ Read via details, PATCH subscription |
| **contact_persons** | `fleet.contact_persons` | `organization_id` | (via query) | — | ✅ Read-only via details |
| **org_relationships** | `fleet.org_relationships` | `source_org_id` / `target_org_id` | (via query) | — | ❌ None |
| **sales_assignments** | `fleet.org_sales_assignments` | `organization_id` | (via query) | — | ❌ None |
**Source:** [`backend/app/models/marketplace/organization.py:237`](backend/app/models/marketplace/organization.py:237)
---
### 2.4 Subscription Architecture
**Source:** [`backend/app/models/marketplace/organization.py:168`](backend/app/models/marketplace/organization.py:168)
The subscription system has **two parallel data sources**:
1. **Denormalized fields** on Organization:
- `subscription_plan` (String(30), default "FREE")
- `subscription_expires_at` (Optional datetime)
- `base_asset_limit` (Integer, default 1)
- `purchased_extra_slots` (Integer, default 0)
- `subscription_tier_id` (FK → `system.subscription_tiers.id`)
2. **Normalized** `OrganizationSubscription` table (queried separately in the details endpoint):
- The API queries `OrganizationSubscription` with `is_active=True` FIRST
- Falls back to `Organization.subscription_tier` if no active subscription found
- Returns `SubscriptionSummary` with `tier_name`, `tier_level`, `valid_from`, `expires_at`, `is_active`, `asset_count`, `asset_limit`
**Feature capabilities** come from two merged sources:
- `SubscriptionTier.feature_capabilities` (JSONB — tier defaults)
- `Organization.custom_features` (JSONB — org-specific overrides)
---
### 2.5 Digital Twin / Soft Delete
**Source:** [`backend/app/models/marketplace/organization.py:86`](backend/app/models/marketplace/organization.py:86)
The Organization implements a "Digital Twin" lifecycle pattern:
```python
legal_owner_id: int # FK → identity.persons (immutable owner reference)
first_registered_at: datetime # First registration (never changes)
current_lifecycle_started_at: datetime # Current lifecycle start (resets on re-register)
last_deactivated_at: datetime # Last deactivation
lifecycle_index: int # Reincarnation counter (default 1)
is_deleted: bool # Soft delete flag
is_active: bool # Active flag
```
This allows an organization to be "deleted" (re-registered under new name) while preserving vehicle/statistics history tied to the original Person.
---
## 3. API Coverage Mapping
### 3.1 Complete Endpoint Table
**Source:** [`backend/app/api/v1/endpoints/admin_organizations.py`](backend/app/api/v1/endpoints/admin_organizations.py)
| # | Method | Path | Purpose | Read/Write | Status |
|---|--------|------|---------|-----------|--------|
| 1 | `GET` | `/admin/organizations` | List orgs with pagination, search, filters | READ | ✅ Wired |
| 2 | `GET` | `/admin/organizations/{org_id}/details` | Full org details (all addresses, contacts, members, subscription) | READ | ✅ Wired |
| 3 | `PUT` | `/admin/organizations/{org_id}` | Update org fields (all 3 address types, contacts) | WRITE | ✅ Wired |
| 4 | `PUT` | `/admin/organizations/{org_id}/status` | Toggle status (active/inactive/suspended/pending_verification) | WRITE | ✅ Wired |
| 5 | `PATCH` | `/admin/organizations/{org_id}/subscription` | Update subscription plan/expiry | WRITE | ✅ Wired |
| 6 | `POST` | `/admin/organizations/{org_id}/members` | Add member by email | WRITE | ✅ Wired |
| 7 | `PUT` | `/admin/organizations/{org_id}/members/{member_id}` | Update member role/status | WRITE | ✅ Wired |
| 8 | `DELETE` | `/admin/organizations/{org_id}/members/{member_id}` | Remove member (soft delete) | WRITE | ✅ Wired |
| 9 | `GET` | `/admin/organizations/{org_id}/vehicles` | List fleet vehicles | READ | ✅ Wired |
### 3.2 Request/Response Schema Analysis
#### `OrganizationUpdate` schema (PUT) — Line 623
```python
class OrganizationUpdate(BaseModel):
# Basic fields
name, full_name, display_name # Names
tax_number, reg_number # Official registration
email, phone # Org-level contact (propagated for individual)
# Primary Address (5 fields)
address_zip, address_city, address_street_name, address_street_type, address_house_number
# Contact Person (3 fields)
contact_person_name, contact_email, contact_phone
# Billing Address (5 fields)
billing_zip, billing_city, billing_street_name, billing_street_type, billing_house_number
# Notification Address (5 fields)
notification_zip, notification_city, notification_street_name, notification_street_type, notification_house_number
```
All fields are `Optional` — only provided fields are updated.
#### `GarageDetailsResponse` schema (GET /details) — Line 127
Returns ALL the above fields **plus**:
- `subscription: SubscriptionSummary` (tier_name, tier_level, valid_from, expires_at, is_active, asset_count, asset_limit)
- `primary_contact: ContactPersonInfo` (id, full_name, role, department, phone, email, is_primary)
- `members: List[OrganizationMemberResponse]` (with nested Person data)
- `created_at`, `member_count`
---
### 3.3 Missing CRUD Operations
The following database entities have **NO admin API endpoints**:
| Entity | Table | Impact |
|--------|-------|--------|
| **OrganizationFinancials** | `fleet.organization_financials` | Cannot view/edit yearly financial records (revenue, expenses) |
| **OrganizationRelationship** | `fleet.org_relationships` | Cannot manage B2B relationships (customer/supplier/partner) |
| **OrganizationSalesAssignment** | `fleet.org_sales_assignments` | Cannot assign sales reps to organizations |
| **OrgRole** (dynamic) | `fleet.org_roles` | Cannot create/edit custom roles with JSONB permissions |
| **ContactPerson** (create/update) | `fleet.contact_persons` | Cannot add new ContactPerson records directly; only the denormalized fields are editable via PUT org |
| **ServiceProfile** | `marketplace.service_profiles` | Cannot manage service provider profile from admin |
| **Branch** | `fleet.branches` | Cannot create/edit physical locations/branches |
---
## 4. Frontend Wiring Analysis
### 4.1 Garage List Page ( `frontend_admin/pages/garages/index.vue` )
**Source:** [`/frontend_admin/pages/garages/index.vue`](frontend_admin/pages/garages/index.vue)
**Wired APIs:**
-`GET /admin/organizations` — Lists all orgs with search, filter, pagination
-`PUT /admin/organizations/{id}/status` — Status toggle per row
-`PATCH /admin/organizations/{id}/subscription` — Subscription update modal
**Displayed fields per garage card/row:**
| Field | Source | Status |
|-------|--------|--------|
| ID | `org.id` | ✅ |
| Company Name | `org.name` / `org.display_name` | ✅ |
| City | `org.address_city` | ✅ |
| Status | `org.status` | ✅ (with colored badge) |
| Member Count | `org.member_count` | ✅ |
| Subscription | `org.subscription_tier_name` | ✅ |
| Expires | `org.subscription_expires_at` | ✅ |
| Billing Address | — | ❌ Not shown |
| Notification Address | — | ❌ Not shown |
| Contact Person | — | ❌ Not shown |
| Financial info | — | ❌ Not shown |
**Stats cards at top:**
- Total garages count
- Active count
- Inactive count
- Pending verification count
---
### 4.2 Garage Detail Page ( `frontend_admin/pages/garages/[id]/index.vue` )
**Source:** [`/frontend_admin/pages/garages/[id]/index.vue`](frontend_admin/pages/garages/[id]/index.vue)
**Wired API:** `GET /admin/organizations/{id}/details`
#### General Tab (lines 131-256)
Displays:
-**Company Info**: `full_name`, `display_name`, `org_type`, `tax_number`, `reg_number`, `created_at`
-**Primary Address**: via `formattedAddress` computed (zip + city + street_name + street_type + house_number, filtered)
-**Subscription Info**: tier_name, tier_level, expires_at, asset_limit
-**Contact Person**: `primary_contact.full_name`, `primary_contact.role`, `primary_contact.department`, `primary_contact.phone`
-**Billing Address**: NOT displayed in General tab
-**Notification Address**: NOT displayed in General tab
-**Financials**: NOT displayed (Dashboard tab is "Coming Soon")
-**B2B Relationships**: NOT displayed
-**Branches**: NOT displayed
#### Employees Tab (lines 262-370)
- ✅ Full member table: name, email/phone, role badge, status, edit/remove actions
- ✅ Add employee modal
#### Fleet Tab (lines 375-457)
- ✅ Vehicle table: plate, brand, model, year, VIN, status
- ✅ RBAC-guarded with `fleet:view`
#### Analytics Tab
- ❌ "Coming soon" placeholder — not implemented
#### Missing Data Alert (lines 995-1022)
The frontend has a **missingFields** computed that flags:
- Missing tax_number (for business orgs)
- Missing contact_email / contact_phone
- Missing billing address (city + zip for business orgs)
This indicates the frontend already "knows" about the missing data problem but has no UI to collect it proactively.
---
### 4.3 Edit Modal Wiring
**Source:** [`/frontend_admin/pages/garages/[id]/index.vue:478`](frontend_admin/pages/garages/[id]/index.vue:478)
The edit modal has **3 tabs** with the following field coverage:
#### Tab 1: Basic (lines 517-598)
| Field | Form Key | API Field | Status |
|-------|----------|-----------|--------|
| Company Name | `full_name` | ✅ | ✅ |
| Display Name | `display_name` | ✅ | ✅ |
| Tax Number | `tax_number` | ✅ | ✅ |
| Reg Number | `reg_number` | ✅ | ✅ |
| ZIP | `address_zip` | ✅ | ✅ |
| City | `address_city` | ✅ | ✅ |
| Street Name | `address_street_name` | ✅ | ✅ |
| Street Type | `address_street_type` | ✅ | ✅ |
| House Number | `address_house_number` | ✅ | ✅ |
#### Tab 2: Contact (lines 601-628)
| Field | Form Key | API Field | Status |
|-------|----------|-----------|--------|
| Contact Name | `contact_person_name` | ✅ | ✅ |
| Contact Email | `contact_email` | ✅ | ✅ |
| Contact Phone | `contact_phone` | ✅ | ✅ |
#### Tab 3: Addresses (lines 631-713)
**Billing Address:**
| Field | Form Key | API Field | Status |
|-------|----------|-----------|--------|
| ZIP | `billing_zip` | ✅ | ✅ |
| City | `billing_city` | ✅ | ✅ |
| Street Name | `billing_street_name` | ✅ | ✅ |
| Street Type | `billing_street_type` | ❌ **MISSING** | ⚠️ |
| House Number | `billing_house_number` | ✅ | ✅ |
**Notification Address:**
| Field | Form Key | API Field | Status |
|-------|----------|-----------|--------|
| ZIP | `notification_zip` | ✅ | ✅ |
| City | `notification_city` | ✅ | ✅ |
| Street Name | `notification_street_name` | ✅ | ✅ |
| Street Type | `notification_street_type` | ❌ **MISSING** | ⚠️ |
| House Number | `notification_house_number` | ✅ | ✅ |
> ⚠️ **Bug**: `billing_street_type` and `notification_street_type` are present in the editForm (lines 943, 948) but have **no corresponding input fields** in the template. The API schema supports them, but the user cannot edit them through the UI.
---
### 4.4 Dead Ends & Gaps
| # | Gap | Layer | Severity |
|---|-----|-------|----------|
| 1 | **Billing/Notification street_type missing from UI** | Frontend | Low (data exists in DB & API) |
| 2 | **OrganizationFinancials - no CRUD anywhere** | API + Frontend | Medium (financial data invisible) |
| 3 | **OrganizationRelationship - no CRUD anywhere** | API + Frontend | Medium (B2B not manageable) |
| 4 | **OrganizationSalesAssignment - no CRUD anywhere** | API + Frontend | Low (sales pipeline not in admin) |
| 5 | **OrgRole dynamic permissions - no admin UI** | API + Frontend | Low (RBAC Phase 2 not deployed) |
| 6 | **ContactPerson create/update - no dedicated endpoint** | API | Low (denormalized fields cover basic needs) |
| 7 | **ServiceProfile - no admin management** | API + Frontend | Medium (service providers invisible) |
| 8 | **Branch - no admin management** | API + Frontend | Low (branches may not need admin UI) |
| 9 | **`address_id` (system.addresses FK) not populated** | API | Info (legacy field, not used) |
| 10 | **Analytics tab "Coming Soon"** | Frontend | Medium (TCO/KM analytics not implemented) |
---
## 5. Findings Summary
### ✅ What Works Well
1. **All 3 address types** (Primary, Billing, Notification) are properly modeled in DB, fully exposed via API, and wired in the frontend edit modal — with the minor exception of street_type fields.
2. **Contact Person dual system** is well-designed: fast denormalized fields for quick edits + normalized ContactPerson with full Person integration.
3. **Subscription system** has proper fallback logic (active subscription → org tier → defaults) and is fully wired.
4. **Member management** has full CRUD across all layers.
5. **Fleet/vehicle assignment** is properly wired with RBAC protection.
6. **Digital Twin lifecycle** fields are comprehensive and well-documented.
7. **Missing data alert** in the frontend proactively identifies incomplete profiles.
### ⚠️ What Needs Attention
1. **Missing street_type inputs** in the Addresses tab of the edit modal — minor template fix needed.
2. **No dedicated ContactPerson CRUD** — the normalized contact system can't be managed independently.
3. **Financials are invisible**`OrganizationFinancials` is orphaned at the API layer.
4. **B2B relationships**`OrganizationRelationship` model exists but has no management interface.
5. **Analytics tab** is a placeholder — no TCO/KM or financial analytics available.
6. **`address_id` (system.addresses)** — the normalized address FK is not populated by any admin endpoint, indicating either a legacy field or planned future use.
---
## 6. Recommendations
### Priority 1 (Quick Wins)
- **Fix missing street_type inputs** in the Addresses tab of the edit modal (add `billing_street_type` and `notification_street_type` template fields)
- **Add `address_id` population** to the PUT org endpoint if the normalized address system is intended for use
### Priority 2 (Medium Term)
- **Build OrganizationFinancials admin API** — at minimum a read-only endpoint and basic frontend display
- **Build OrganizationRelationship admin API** — enable B2B relationship management
- **Add ContactPerson create/update endpoint** — allow managing normalized contacts independently
- **Add ServiceProfile admin view** — see which orgs are service providers
### Priority 3 (Long Term)
- **Implement Analytics tab** — TCO/KM, financial trends, fleet utilization
- **Branch management UI** — create/edit physical locations
- **OrgRole dynamic permissions admin** — custom role builder UI
- **OrganizationSalesAssignment admin** — sales pipeline management
---
*End of Report — Full-stack Organization (Garage) Data Anatomy & Mapping*