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

23 KiB

🏗️ 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
  2. Database Schema & Relationships
  3. API Coverage Mapping
  4. Frontend Wiring Analysis
  5. Findings Summary
  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 ), the admin API endpoints ( 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_personsidentity.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

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)

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)

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)

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

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:

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:

System A: Denormalized (on Organization)

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)

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.personidentity.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): For individual orgs, the PUT endpoint propagates contact_emailemail (User) and contact_phonephone (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


2.4 Subscription Architecture

Source: 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

The Organization implements a "Digital Twin" lifecycle pattern:

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

# 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

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

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

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

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 invisibleOrganizationFinancials is orphaned at the API layer.
  4. B2B relationshipsOrganizationRelationship 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