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
- Executive Summary
- Database Schema & Relationships
- API Coverage Mapping
- Frontend Wiring Analysis
- 4.1 Garage List Page
- 4.2 Garage Detail Page
- 4.3 Edit Modal Wiring
- 4.4 Dead Ends & Gaps
- Findings Summary
- 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-Manyaddressestable withaddress_typeenum. Instead, each address type has its own column prefix (address_*,billing_*,notification_*). A normalizedsystem.addressestable exists but is only linked via a singleaddress_idFK (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 normalizedContactPersonmodel (fleet.contact_persons→identity.persons) with role/department/is_primary. - ⚠️ Missing CRUD:
OrganizationFinancials,OrganizationRelationship(B2B),OrganizationSalesAssignment, andOrgRoledynamic permissions exist in the database but have zero admin API endpoints or frontend UI. - ⚠️ Frontend Gap: Billing and Notification
street_typefields 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.person → identity.persons (the actual person record with name, phone, email).
How they interact: The API loads both. The GET /details endpoint returns:
- The denormalized
contact_person_name,contact_email,contact_phonedirectly from the Organization record - A separate
primary_contactobject 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_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
2.4 Subscription Architecture
Source: backend/app/models/marketplace/organization.py:168
The subscription system has two parallel data sources:
-
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)
-
Normalized
OrganizationSubscriptiontable (queried separately in the details endpoint):- The API queries
OrganizationSubscriptionwithis_active=TrueFIRST - Falls back to
Organization.subscription_tierif no active subscription found - Returns
SubscriptionSummarywithtier_name,tier_level,valid_from,expires_at,is_active,asset_count,asset_limit
- The API queries
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
formattedAddresscomputed (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_typeandnotification_street_typeare 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
- 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.
- Contact Person dual system is well-designed: fast denormalized fields for quick edits + normalized ContactPerson with full Person integration.
- Subscription system has proper fallback logic (active subscription → org tier → defaults) and is fully wired.
- Member management has full CRUD across all layers.
- Fleet/vehicle assignment is properly wired with RBAC protection.
- Digital Twin lifecycle fields are comprehensive and well-documented.
- Missing data alert in the frontend proactively identifies incomplete profiles.
⚠️ What Needs Attention
- Missing street_type inputs in the Addresses tab of the edit modal — minor template fix needed.
- No dedicated ContactPerson CRUD — the normalized contact system can't be managed independently.
- Financials are invisible —
OrganizationFinancialsis orphaned at the API layer. - B2B relationships —
OrganizationRelationshipmodel exists but has no management interface. - Analytics tab is a placeholder — no TCO/KM or financial analytics available.
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_typeandnotification_street_typetemplate fields) - Add
address_idpopulation 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