Register all active platform services and base features in core DB so admin can manage them, add delivery/hospitality/sports-center backend phases, update apps catalog and production deploy tooling. Co-authored-by: Cursor <cursoragent@cursor.com>
376 lines
13 KiB
Python
376 lines
13 KiB
Python
"""Architecture tests — Hospitality module boundary enforcement."""
|
|
from __future__ import annotations
|
|
|
|
import ast
|
|
from pathlib import Path
|
|
|
|
from app.models import analytics as analytics_models
|
|
from app.models import catalog as catalog_models
|
|
from app.models import connectors as connectors_models
|
|
from app.models import foundation as models
|
|
from app.models import kitchen as kitchen_models
|
|
from app.models import pos_lite as pos_lite_models
|
|
from app.models import pos_pro as pos_pro_models
|
|
from app.models import qr as qr_models
|
|
from app.models import table_service as table_service_models
|
|
|
|
|
|
FORBIDDEN_IMPORT_PREFIXES = (
|
|
"backend.services.accounting",
|
|
"backend.services.crm",
|
|
"backend.services.loyalty",
|
|
"backend.services.communication",
|
|
"backend.services.sports_center",
|
|
"backend.services.automation",
|
|
"backend.services.notification",
|
|
"backend.services.file_storage",
|
|
"backend.services.identity_access",
|
|
"backend.core_service",
|
|
)
|
|
|
|
FOUNDATION_MODELS = [
|
|
models.Venue,
|
|
models.Branch,
|
|
models.DiningArea,
|
|
models.DiningTable,
|
|
models.Menu,
|
|
models.MenuCategory,
|
|
models.MenuItem,
|
|
models.BundleDefinition,
|
|
models.TenantBundle,
|
|
models.FeatureToggle,
|
|
models.HospitalityRole,
|
|
models.HospitalityPermission,
|
|
models.HospitalityConfiguration,
|
|
models.HospitalityEvent,
|
|
models.HospitalitySetting,
|
|
models.HospitalityAuditLog,
|
|
]
|
|
|
|
CATALOG_MODELS = [
|
|
catalog_models.Allergen,
|
|
catalog_models.ModifierGroup,
|
|
catalog_models.ModifierOption,
|
|
catalog_models.MenuItemModifier,
|
|
catalog_models.MenuItemAllergen,
|
|
catalog_models.AvailabilityWindow,
|
|
catalog_models.MenuMediaRef,
|
|
catalog_models.MenuLocalization,
|
|
]
|
|
|
|
QR_MODELS = [
|
|
qr_models.QrCode,
|
|
qr_models.QrMenuSession,
|
|
qr_models.QrOrderingSession,
|
|
qr_models.Cart,
|
|
qr_models.CartLine,
|
|
]
|
|
|
|
TABLE_SERVICE_MODELS = [
|
|
table_service_models.Reservation,
|
|
table_service_models.TableAssignment,
|
|
table_service_models.ServiceRequest,
|
|
table_service_models.WaitlistEntry,
|
|
]
|
|
|
|
POS_LITE_MODELS = [
|
|
pos_lite_models.PosRegister,
|
|
pos_lite_models.PosShift,
|
|
pos_lite_models.PosTicket,
|
|
pos_lite_models.PosTicketLine,
|
|
]
|
|
|
|
POS_PRO_MODELS = [
|
|
pos_pro_models.PosPayment,
|
|
pos_pro_models.PosDiscount,
|
|
pos_pro_models.PosTaxLine,
|
|
pos_pro_models.PosFloorPlan,
|
|
pos_pro_models.PosStation,
|
|
]
|
|
|
|
KITCHEN_MODELS = [
|
|
kitchen_models.KitchenStation,
|
|
kitchen_models.KitchenTicket,
|
|
kitchen_models.KitchenTicketItem,
|
|
]
|
|
|
|
CONNECTORS_MODELS = [
|
|
connectors_models.ConnectorRegistration,
|
|
connectors_models.ConnectorDispatch,
|
|
]
|
|
|
|
ANALYTICS_MODELS = [
|
|
analytics_models.AnalyticsReportDefinition,
|
|
analytics_models.AnalyticsSnapshot,
|
|
]
|
|
|
|
|
|
def test_all_models_have_tenant_id():
|
|
from app.core.database import Base
|
|
import app.models # noqa: F401
|
|
|
|
skip = {"alembic_version"}
|
|
for table in Base.metadata.tables.values():
|
|
if table.name in skip:
|
|
continue
|
|
assert "tenant_id" in table.columns, f"{table.name} missing tenant_id"
|
|
|
|
|
|
def test_foundation_aggregates_are_independent():
|
|
assert len(FOUNDATION_MODELS) == 16
|
|
for model in FOUNDATION_MODELS:
|
|
assert hasattr(model, "tenant_id")
|
|
assert hasattr(model, "id")
|
|
assert not model.__mapper__.relationships
|
|
|
|
|
|
def test_catalog_aggregates_are_independent():
|
|
assert len(CATALOG_MODELS) == 8
|
|
for model in CATALOG_MODELS:
|
|
assert hasattr(model, "tenant_id")
|
|
assert hasattr(model, "id")
|
|
assert not model.__mapper__.relationships
|
|
|
|
|
|
def test_qr_aggregates_are_independent():
|
|
assert len(QR_MODELS) == 5
|
|
for model in QR_MODELS:
|
|
assert hasattr(model, "tenant_id")
|
|
assert hasattr(model, "id")
|
|
assert not model.__mapper__.relationships
|
|
|
|
|
|
def test_table_service_aggregates_are_independent():
|
|
assert len(TABLE_SERVICE_MODELS) == 4
|
|
for model in TABLE_SERVICE_MODELS:
|
|
assert hasattr(model, "tenant_id")
|
|
assert hasattr(model, "id")
|
|
assert not model.__mapper__.relationships
|
|
|
|
|
|
def test_pos_lite_aggregates_are_independent():
|
|
assert len(POS_LITE_MODELS) == 4
|
|
for model in POS_LITE_MODELS:
|
|
assert hasattr(model, "tenant_id")
|
|
assert hasattr(model, "id")
|
|
assert not model.__mapper__.relationships
|
|
|
|
|
|
def test_pos_pro_aggregates_are_independent():
|
|
assert len(POS_PRO_MODELS) == 5
|
|
for model in POS_PRO_MODELS:
|
|
assert hasattr(model, "tenant_id")
|
|
assert hasattr(model, "id")
|
|
assert not model.__mapper__.relationships
|
|
|
|
|
|
def test_kitchen_aggregates_are_independent():
|
|
assert len(KITCHEN_MODELS) == 3
|
|
for model in KITCHEN_MODELS:
|
|
assert hasattr(model, "tenant_id")
|
|
assert hasattr(model, "id")
|
|
assert not model.__mapper__.relationships
|
|
|
|
|
|
def test_connectors_aggregates_are_independent():
|
|
assert len(CONNECTORS_MODELS) == 2
|
|
for model in CONNECTORS_MODELS:
|
|
assert hasattr(model, "tenant_id")
|
|
assert hasattr(model, "id")
|
|
assert not model.__mapper__.relationships
|
|
|
|
|
|
def test_analytics_aggregates_are_independent():
|
|
assert len(ANALYTICS_MODELS) == 2
|
|
for model in ANALYTICS_MODELS:
|
|
assert hasattr(model, "tenant_id")
|
|
assert hasattr(model, "id")
|
|
assert not model.__mapper__.relationships
|
|
|
|
|
|
def test_no_format_specific_hardcoded_engines():
|
|
forbidden = {"pos_engine", "kitchen_engine", "ordering_engine", "vendor_sdk"}
|
|
for model in (
|
|
FOUNDATION_MODELS
|
|
+ CATALOG_MODELS
|
|
+ QR_MODELS
|
|
+ TABLE_SERVICE_MODELS
|
|
+ POS_LITE_MODELS
|
|
+ POS_PRO_MODELS
|
|
+ KITCHEN_MODELS
|
|
+ CONNECTORS_MODELS
|
|
+ ANALYTICS_MODELS
|
|
):
|
|
columns = set(model.__table__.columns.keys())
|
|
assert forbidden.isdisjoint(columns), model.__tablename__
|
|
|
|
|
|
def test_permissions_defined():
|
|
from app.permissions.definitions import ALL_PERMISSIONS, PERMISSION_PREFIXES
|
|
|
|
assert "hospitality.view" in ALL_PERMISSIONS
|
|
assert "hospitality.venues.create" in ALL_PERMISSIONS
|
|
assert "hospitality.bundles.manage" in ALL_PERMISSIONS
|
|
assert "hospitality.tenant_bundles.manage" in ALL_PERMISSIONS
|
|
assert "hospitality.feature_toggles.manage" in ALL_PERMISSIONS
|
|
assert "hospitality.menus.manage" in ALL_PERMISSIONS
|
|
assert "hospitality.allergens.manage" in ALL_PERMISSIONS
|
|
assert "hospitality.modifier_groups.manage" in ALL_PERMISSIONS
|
|
assert "hospitality.availability_windows.manage" in ALL_PERMISSIONS
|
|
assert "hospitality.menu_media.manage" in ALL_PERMISSIONS
|
|
assert "hospitality.menu_localizations.manage" in ALL_PERMISSIONS
|
|
assert "hospitality.qr_codes.manage" in ALL_PERMISSIONS
|
|
assert "hospitality.qr_menu_sessions.manage" in ALL_PERMISSIONS
|
|
assert "hospitality.qr_ordering_sessions.manage" in ALL_PERMISSIONS
|
|
assert "hospitality.carts.manage" in ALL_PERMISSIONS
|
|
assert "hospitality.reservations.manage" in ALL_PERMISSIONS
|
|
assert "hospitality.table_assignments.manage" in ALL_PERMISSIONS
|
|
assert "hospitality.service_requests.manage" in ALL_PERMISSIONS
|
|
assert "hospitality.waitlist.manage" in ALL_PERMISSIONS
|
|
assert "hospitality.pos_registers.manage" in ALL_PERMISSIONS
|
|
assert "hospitality.pos_shifts.manage" in ALL_PERMISSIONS
|
|
assert "hospitality.pos_tickets.manage" in ALL_PERMISSIONS
|
|
assert "hospitality.pos_payments.manage" in ALL_PERMISSIONS
|
|
assert "hospitality.pos_discounts.manage" in ALL_PERMISSIONS
|
|
assert "hospitality.pos_floor_plans.manage" in ALL_PERMISSIONS
|
|
assert "hospitality.kitchen_stations.manage" in ALL_PERMISSIONS
|
|
assert "hospitality.kitchen_tickets.manage" in ALL_PERMISSIONS
|
|
assert "hospitality.connector_registrations.manage" in ALL_PERMISSIONS
|
|
assert "hospitality.connector_dispatches.manage" in ALL_PERMISSIONS
|
|
assert "hospitality.analytics_reports.manage" in ALL_PERMISSIONS
|
|
assert "hospitality.analytics_snapshots.manage" in ALL_PERMISSIONS
|
|
assert "hospitality.digital_menu.view" in ALL_PERMISSIONS
|
|
assert "hospitality.pos_lite.manage" in ALL_PERMISSIONS
|
|
assert "hospitality.audit.view" in ALL_PERMISSIONS
|
|
for prefix in PERMISSION_PREFIXES:
|
|
assert any(p.startswith(prefix.rstrip(".")) or p.startswith(prefix) for p in ALL_PERMISSIONS)
|
|
|
|
|
|
def test_events_defined():
|
|
from app.events.types import HospitalityEventType
|
|
|
|
assert HospitalityEventType.VENUE_CREATED.value == "hospitality.venue.created"
|
|
assert HospitalityEventType.MENU_CREATED.value == "hospitality.menu.created"
|
|
assert (
|
|
HospitalityEventType.TENANT_BUNDLE_ACTIVATED.value
|
|
== "hospitality.tenant_bundle.activated"
|
|
)
|
|
assert (
|
|
HospitalityEventType.FEATURE_TOGGLE_UPSERTED.value
|
|
== "hospitality.feature_toggle.upserted"
|
|
)
|
|
assert HospitalityEventType.ALLERGEN_CREATED.value == "hospitality.allergen.created"
|
|
assert (
|
|
HospitalityEventType.MODIFIER_GROUP_CREATED.value
|
|
== "hospitality.modifier_group.created"
|
|
)
|
|
assert (
|
|
HospitalityEventType.MENU_LOCALIZATION_UPSERTED.value
|
|
== "hospitality.menu_localization.upserted"
|
|
)
|
|
assert HospitalityEventType.QR_CODE_CREATED.value == "hospitality.qr_code.created"
|
|
assert (
|
|
HospitalityEventType.QR_ORDERING_SESSION_SUBMITTED.value
|
|
== "hospitality.qr_ordering_session.submitted"
|
|
)
|
|
assert HospitalityEventType.CART_CREATED.value == "hospitality.cart.created"
|
|
assert HospitalityEventType.CART_LINE_ADDED.value == "hospitality.cart_line.added"
|
|
assert (
|
|
HospitalityEventType.RESERVATION_CREATED.value == "hospitality.reservation.created"
|
|
)
|
|
assert (
|
|
HospitalityEventType.RESERVATION_STATUS_CHANGED.value
|
|
== "hospitality.reservation.status_changed"
|
|
)
|
|
assert (
|
|
HospitalityEventType.TABLE_ASSIGNMENT_CREATED.value
|
|
== "hospitality.table_assignment.created"
|
|
)
|
|
assert (
|
|
HospitalityEventType.SERVICE_REQUEST_CREATED.value
|
|
== "hospitality.service_request.created"
|
|
)
|
|
assert (
|
|
HospitalityEventType.WAITLIST_ENTRY_CREATED.value
|
|
== "hospitality.waitlist_entry.created"
|
|
)
|
|
assert HospitalityEventType.POS_REGISTER_CREATED.value == "hospitality.pos_register.created"
|
|
assert HospitalityEventType.POS_SHIFT_OPENED.value == "hospitality.pos_shift.opened"
|
|
assert HospitalityEventType.POS_TICKET_VOIDED.value == "hospitality.pos_ticket.voided"
|
|
assert HospitalityEventType.POS_PAYMENT_RECORDED.value == "hospitality.pos_payment.recorded"
|
|
assert HospitalityEventType.POS_STATION_CREATED.value == "hospitality.pos_station.created"
|
|
assert HospitalityEventType.KITCHEN_TICKET_CREATED.value == "hospitality.kitchen_ticket.created"
|
|
assert (
|
|
HospitalityEventType.KITCHEN_TICKET_ITEM_ADDED.value
|
|
== "hospitality.kitchen_ticket_item.added"
|
|
)
|
|
assert (
|
|
HospitalityEventType.CONNECTOR_REGISTRATION_CREATED.value
|
|
== "hospitality.connector_registration.created"
|
|
)
|
|
assert (
|
|
HospitalityEventType.CONNECTOR_DISPATCH_CREATED.value
|
|
== "hospitality.connector_dispatch.created"
|
|
)
|
|
assert (
|
|
HospitalityEventType.ANALYTICS_SNAPSHOT_REFRESHED.value
|
|
== "hospitality.analytics_snapshot.refreshed"
|
|
)
|
|
|
|
|
|
def test_platform_provider_contracts_exist():
|
|
from app.providers import (
|
|
AIProvider,
|
|
AccountingProvider,
|
|
CRMProvider,
|
|
CommunicationProvider,
|
|
Customer360Provider,
|
|
DeliveryProvider,
|
|
IdentityProvider,
|
|
LoyaltyProvider,
|
|
NotificationProvider,
|
|
StorageProvider,
|
|
WebsiteBuilderProvider,
|
|
)
|
|
|
|
assert AccountingProvider is not None
|
|
assert CRMProvider is not None
|
|
assert LoyaltyProvider is not None
|
|
assert CommunicationProvider is not None
|
|
assert DeliveryProvider is not None
|
|
assert WebsiteBuilderProvider is not None
|
|
assert NotificationProvider is not None
|
|
assert StorageProvider is not None
|
|
assert AIProvider is not None
|
|
assert IdentityProvider is not None
|
|
assert Customer360Provider is not None
|
|
|
|
|
|
def test_architecture_folders_exist():
|
|
root = Path(__file__).resolve().parents[1]
|
|
for name in ("policies", "specifications", "commands", "queries", "providers"):
|
|
assert (root / name).is_dir(), name
|
|
|
|
|
|
def test_no_forbidden_cross_service_imports():
|
|
root = Path(__file__).resolve().parents[1]
|
|
for path in root.rglob("*.py"):
|
|
if "tests" in path.parts:
|
|
continue
|
|
tree = ast.parse(path.read_text(encoding="utf-8"))
|
|
for node in ast.walk(tree):
|
|
if isinstance(node, ast.Import):
|
|
for alias in node.names:
|
|
for prefix in FORBIDDEN_IMPORT_PREFIXES:
|
|
assert not alias.name.startswith(prefix), path
|
|
if isinstance(node, ast.ImportFrom) and node.module:
|
|
for prefix in FORBIDDEN_IMPORT_PREFIXES:
|
|
assert not node.module.startswith(prefix), path
|
|
|
|
|
|
def test_api_ownership_no_foreign_domains():
|
|
root = Path(__file__).resolve().parents[1] / "api" / "v1"
|
|
text = "\n".join(p.read_text(encoding="utf-8") for p in root.glob("*.py"))
|
|
for forbidden in ("/accounting", "/crm/", "/loyalty", "/sports-center"):
|
|
assert forbidden not in text
|