Skip to content

Latest commit

 

History

History
455 lines (360 loc) · 13.6 KB

File metadata and controls

455 lines (360 loc) · 13.6 KB

Architecture Overview

VisitRomagna — Agritourism booking platform for Romagna, Italy

High-Level Architecture

VisitRomagna is a Next.js 16 App Router monolith using a layered architecture. The frontend (React 19 SSR/CSR pages) and backend (REST API route handlers) coexist in a single deployment. The data layer currently uses an in-memory store with seeded fixtures, backed by a Prisma 7 schema ready for SQLite/PostgreSQL migration.

graph TB
    subgraph Client["Browser / PWA"]
        Pages["React Pages (SSR + CSR)"]
        Hooks["React Hooks (useApiQuery, useAuth)"]
        SSEClient["SSE Client"]
    end

    subgraph NextJS["Next.js 16 App Router"]
        subgraph APILayer["API Layer (60+ routes)"]
            Auth["Auth Routes<br/>/api/auth/*"]
            Core["Core Routes<br/>/api/experiences, /api/bookings"]
            Host["Host Routes<br/>/api/pricing, /api/channels"]
            Admin["Admin Routes<br/>/api/admin/*, /api/feature-flags"]
            B2B["B2B Routes<br/>/api/agents, /api/b2b-proposals"]
            RT["SSE Route<br/>/api/sse"]
        end

        subgraph Middleware["Middleware Layer"]
            AuthMW["Auth Middleware<br/>(RBAC)"]
            Validation["Zod Validation"]
            RateLimit["Rate Limiter"]
        end

        subgraph Services["Domain Services"]
            BookingSvc["BookingService"]
            ExpSvc["ExperienceService"]
            UserSvc["UserService"]
            ReviewSvc["ReviewService"]
        end

        subgraph Infrastructure["Infrastructure"]
            EventBus["Domain Event Bus"]
            SSEMgr["SSE Manager"]
            PayEngine["Payment Engine<br/>(Stripe + IVA)"]
            I18N["i18n Engine<br/>(5 locales)"]
        end

        subgraph Data["Data Layer"]
            InMemory["InMemoryStore<br/>(data.ts)"]
            PrismaSchema["Prisma Schema<br/>(25+ models)"]
        end
    end

    subgraph External["External Services"]
        Stripe["Stripe<br/>(Payments)"]
        Strava["Strava API<br/>(Cycling)"]
        Redis["Redis<br/>(Pub/Sub)"]
    end

    Pages --> |fetch| APILayer
    Hooks --> |HTTP| APILayer
    SSEClient --> |EventSource| RT

    APILayer --> AuthMW
    APILayer --> Validation
    APILayer --> RateLimit
    APILayer --> Services

    Services --> Data
    Services --> EventBus
    EventBus --> SSEMgr
    PayEngine --> Stripe
    BookingSvc --> PayEngine

    InMemory -.->|future migration| PrismaSchema
    SSEMgr -.->|future| Redis

    style Client fill:#fef3c7,stroke:#d97706
    style NextJS fill:#f0fdf4,stroke:#16a34a
    style External fill:#eff6ff,stroke:#2563eb
Loading

Layered Architecture

The codebase follows a 4-layer architecture with clear dependency rules:

graph LR
    A["API Routes<br/>(src/app/api)"] --> B["Domain Services<br/>(src/lib/domains)"]
    B --> C["Data Layer<br/>(src/lib/data.ts)"]
    C --> D["Storage<br/>(src/lib/db.ts)"]

    A --> E["Middleware<br/>(auth, validation,<br/>rate-limiting)"]
    B --> F["Event Bus<br/>(src/lib/events)"]
    F --> G["Subscribers<br/>(cross-feature reactions)"]

    style A fill:#dbeafe
    style B fill:#fef9c3
    style C fill:#dcfce7
    style D fill:#f3e8ff
Loading
Layer Location Responsibility
API Routes src/app/api/ HTTP handlers, request/response mapping, middleware application
Domain Services src/lib/domains/ Business logic, domain event emission, orchestration
Data Layer src/lib/data.ts CRUD operations, seed data, in-memory queries
Storage src/lib/db.ts Generic InMemoryStore<T> (future: Prisma Client)
Events src/lib/events/ Domain event bus, cross-feature subscriber wiring
Middleware src/lib/auth-middleware.ts, api-utils.ts RBAC, Zod validation, rate limiting, error formatting

Data Model

The Prisma schema defines 25+ models organized into 10 domains:

erDiagram
    User ||--o| HostProfile : "has"
    User ||--o{ Booking : "makes"
    User ||--o{ Review : "writes"
    User ||--o{ SavedItinerary : "creates"
    User ||--o{ Wishlist : "saves"
    User ||--o{ Session : "authenticates"
    User ||--o| UserLoyalty : "earns"
    User ||--o| ReferralCode : "owns"
    User ||--o| StravaConnection : "connects"

    HostProfile ||--o{ Experience : "offers"

    Experience ||--o{ Booking : "receives"
    Experience ||--o{ Review : "rated by"
    Experience ||--o{ Availability : "has slots"
    Experience ||--o{ PricingRule : "priced by"
    Experience ||--o{ CyclingRouteExperience : "linked to"

    Booking ||--o| Payment : "paid via"
    Booking ||--o| Review : "reviewed by"

    CyclingRoute ||--o{ CyclingRouteExperience : "includes"
    CyclingRoute ||--o{ ActivityLog : "tracked by"

    GiftCard ||--o{ GiftCardRedemption : "redeemed"

    GroupBooking ||--o{ GroupQuote : "quoted"

    ReferralCode ||--o{ ReferralRedemption : "redeemed"

    Conversation ||--o{ Message : "contains"
Loading

Key Model Groups

Domain Models
Auth User, Session
Hosting HostProfile, Experience, Availability
Booking Booking, Payment
Reviews Review
Wishlists Wishlist, SavedItinerary
Cycling CyclingRoute, CyclingRouteExperience, ActivityLog, CyclingChallenge
Messaging Conversation, Message, Notification
Financial PricingRule, GiftCard, GiftCardRedemption
Marketing Campaign, GroupBooking, GroupQuote
Loyalty ReferralCode, ReferralRedemption, UserLoyalty
Media MediaUpload
Strava StravaConnection
Cart ItineraryCart

Request Flow

A typical API request flows through the following layers:

sequenceDiagram
    participant Client
    participant Route as API Route Handler
    participant RateLimit as Rate Limiter
    participant Auth as Auth Middleware
    participant Validate as Zod Validator
    participant Service as Domain Service
    participant Data as Data Layer
    participant EventBus as Event Bus
    participant SSE as SSE Manager

    Client->>Route: POST /api/bookings
    Route->>RateLimit: checkRateLimit()
    RateLimit-->>Route: ✓ (or 429)
    Route->>Auth: withAuth(handler)
    Auth-->>Route: ✓ user attached (or 401/403)
    Route->>Validate: validateBody(schema)
    Validate-->>Route: ✓ typed data (or 400)
    Route->>Service: bookingService.create(input)
    Service->>Data: createBooking(input)
    Data-->>Service: booking record
    Service->>EventBus: emit("booking.created", payload)
    EventBus->>SSE: notifyBookingCreated(hostId)
    SSE-->>Client: SSE event push
    Service-->>Route: booking
    Route-->>Client: 201 { booking }
Loading

Authentication & Authorization

Cookie-based session auth with RBAC:

graph TD
    A[Request] --> B{Has vr_session cookie?}
    B -->|No| C{Route requires auth?}
    C -->|Yes| D[401 Unauthorized]
    C -->|No| E[Allow — optional auth]

    B -->|Yes| F[Lookup session by token]
    F --> G{Session valid & not expired?}
    G -->|No| D
    G -->|Yes| H[Load User from session]
    H --> I{Route has role requirement?}
    I -->|No| J[✓ Allow]
    I -->|Yes| K{User role in allowed roles?}
    K -->|Yes| J
    K -->|No| L[403 Forbidden]
Loading

Roles

Role Access Level
tourist Public + own bookings, reviews, wishlists
host Tourist + own listings, pricing, analytics, host forum
agent B2B portal, proposals, API keys
admin Full platform access, moderation, feature flags

Route Protection Map

/api/admin/*              → admin only
/api/financial/*          → host, admin
/api/revenue-intelligence → host, admin
/api/observability/*      → admin only
/api/demand-intelligence  → admin only
/api/guest-crm/*          → admin only
/api/experiments          → admin only
/api/feature-flags        → admin only
/api/agents               → agent, admin
/api/b2b-proposals        → agent, admin

Dev Bypass: In development mode, append ?dev=true to any authenticated route to bypass auth with a mock admin user.


Domain Event System

The event bus implements a publish-subscribe pattern that connects all features:

graph LR
    subgraph Emitters
        BS["BookingService"]
        RS["ReviewService"]
        US["UserService"]
        QR["QR Scan Handler"]
        FS["Festival Handler"]
    end

    subgraph EventBus["Event Bus (eventBus)"]
        Dispatch["dispatch + persist"]
    end

    subgraph Subscribers
        CRM["CRM Segment Update"]
        Loyalty["Loyalty Points Credit"]
        Trust["Trust Score Recalc"]
        Demand["Demand Intelligence"]
        Campaign["Campaign Auto-Create"]
        Obs["Observability Logger"]
    end

    BS -->|booking.created<br/>booking.confirmed<br/>booking.cancelled<br/>booking.completed| Dispatch
    RS -->|review.submitted<br/>review.responded| Dispatch
    US -->|user.registered| Dispatch
    QR -->|qr.scanned| Dispatch
    FS -->|festival.started| Dispatch

    Dispatch -->|booking.completed| CRM
    Dispatch -->|booking.completed| Loyalty
    Dispatch -->|review.submitted| Trust
    Dispatch -->|qr.scanned| Demand
    Dispatch -->|festival.started| Campaign
    Dispatch -->|*| Obs

    style EventBus fill:#fef3c7,stroke:#d97706
Loading

Event Types

Category Events
Booking booking.created, booking.confirmed, booking.cancelled, booking.completed
Review review.submitted, review.approved, review.responded
Payment payment.succeeded, payment.refunded
Messaging message.sent, message.read
Listing listing.published, listing.updated
Campaign campaign.started, campaign.ended, campaign.activated
User user.registered, user.verified
CRM crm.segment_changed
Trust trust.updated
Loyalty loyalty.tier_changed
Demand demand.signal_detected
QR qr.scanned
Festival festival.started, festival.ended

Payment Architecture

graph TD
    A[Guest selects experience] --> B[Calculate price]
    B --> C{PricingRules apply?}
    C -->|seasonal/early-bird/group| D[Apply multiplier]
    C -->|none| E[Base price]
    D --> F[Calculate IVA]
    E --> F

    F --> G[Calculate payment split]
    G --> H["Gross Amount"]
    G --> I["Stripe Fee (2.9% + €0.25)"]
    G --> J["Platform Commission (15%)"]
    G --> K["Host Payout"]
    G --> L["IVA on Commission (22%)"]

    H --> M{Stripe configured?}
    M -->|Yes| N[Create Checkout Session]
    M -->|No| O[Simulated Payment]
    N --> P[Stripe Webhook → booking.confirmed]
    O --> P

    P --> Q[Generate FatturaPA XML]

    style F fill:#fef3c7
    style G fill:#dcfce7
Loading

IVA Rates (Italian Value-Added Tax)

Category Rate
Agriturismi (accommodation) 10% reduced
Cultura (cultural services) 10% reduced
Corsi di Cucina, Vino, Cicloturismo, Natura 22% standard

Real-Time Communication

SSE-based server→client push, chosen over WebSocket for Vercel serverless compatibility:

sequenceDiagram
    participant Client
    participant SSERoute as GET /api/sse
    participant SSEManager
    participant EventBus
    participant Service as Domain Service

    Client->>SSERoute: EventSource(?channel=user:abc)
    SSERoute->>SSEManager: addClient(userId, channel)
    SSEManager-->>Client: event: connected

    loop Heartbeat (30s)
        SSEManager-->>Client: : heartbeat
    end

    Service->>EventBus: emit(booking.created)
    EventBus->>SSEManager: notifyBookingCreated(hostId)
    SSEManager-->>Client: event: notification
Loading

Channel Types

Channel Purpose
user:<id> Per-user notifications (bookings, messages)
host:<id> Host-specific alerts (new bookings, reviews)
admin Platform-wide admin events
conversation:<id> Live chat messages
experience:<id> Availability updates

i18n Strategy

5 supported locales with path-based routing:

/ (Italian — default)
/en/* (English)
/de/* (German)
/fr/* (French)
/es/* (Spanish)
  • Server-side: Intl.DateTimeFormat and Intl.NumberFormat for locale-aware formatting
  • Client-side: useLocale() hook reads URL path → localStorage → default fallback
  • SEO: <link rel="alternate" hreflang="..."> tags generated per page
  • Translation: Dictionary-based lookup via t(key, locale) helper

Deployment Architecture

graph TB
    subgraph Production
        LB["Load Balancer / CDN"]
        subgraph Container["Docker Container"]
            App["Next.js App<br/>(SSR + API)"]
        end
        DB["PostgreSQL 16"]
        Cache["Redis 7"]
        Pay["Stripe API"]
    end

    LB --> App
    App --> DB
    App --> Cache
    App --> Pay

    style Production fill:#f0fdf4
Loading
Environment Database Payments Real-time
Development SQLite (in-memory) Simulated In-process SSE
Docker PostgreSQL 16 Configurable Redis pub/sub ready
Production PostgreSQL 16 Stripe live Redis pub/sub

Future Architecture Roadmap

The codebase includes preparatory abstractions for these upgrades:

  1. Prisma MigrationBaseRepository<T> interface + InMemoryStore can be swapped to Prisma Client with no consumer changes
  2. Redis Pub/Sub — SSE Manager designed for multi-instance broadcast via Redis adapter
  3. Moonshot Features — Six experimental systems (destination graph, host guilds, mobility cloud, agentic concierge, provenance passport, impact exchange)
  4. Multi-Region — Region model and domain-scoped routing for expanding beyond Romagna