libra/docs/architecture.md

93 KiB

Libra Law Firm - Architecture Document

Version: 1.1 Date: December 21, 2025 Status: Approved Domain: libra.ps


Change Log

Date Version Description Author
Dec 21, 2025 1.0 Initial architecture document Winston (Architect)
Dec 21, 2025 1.1 Added CI/CD, rollback procedures, monitoring, timezone handling, alternatives analysis Winston (Architect)

Table of Contents

  1. Introduction
  2. High Level Architecture
  3. Technology Stack
  4. Data Models
  5. Database Schema
  6. Application Structure
  7. Authentication & Authorization
  8. Core Workflows
  9. Routing Structure
  10. Email System
  11. Background Jobs & Scheduling
  12. Localization & Timezone
  13. Security
  14. Testing Strategy
  15. Performance & Optimization
  16. Deployment & CI/CD
  17. Monitoring & Alerting
  18. Error Handling & Graceful Degradation
  19. Rollback & Recovery Procedures
  20. Coding Standards
  21. Technology Alternatives Considered
  22. Appendices

1. Introduction

1.1 Purpose

This document outlines the complete architecture for Libra Law Firm's web platform. It serves as the single source of truth for development, ensuring consistency and guiding AI-driven implementation.

1.2 Project Overview

Libra Law Firm is a bilingual (Arabic/English) Laravel application serving as both a public-facing website and internal management tool for:

  • Client consultation booking and management
  • Case timeline tracking
  • Legal content publishing
  • Administrative operations

1.3 Key Constraints

Constraint Details
Single Admin One lawyer manages all operations
No Online Payments All payments handled offline
Client-Managed Infrastructure Rocky Linux 9 server
No Self-Registration Admin creates all client accounts
Bilingual Requirement Arabic (RTL) primary, English (LTR) secondary

1.4 Project Context

Existing Foundation:

  • Laravel 12.43.1 with Fortify authentication
  • Livewire 3.7.3 + Volt 1.10.1 for reactive components
  • Flux UI Free 2.10.2 component library
  • Tailwind CSS 4.1.11
  • Pest 4.2.0 testing framework

2. High Level Architecture

2.1 Technical Summary

Libra is a server-rendered Laravel monolith with Livewire for reactive UI. The architecture follows Laravel's MVC pattern enhanced with:

  • Livewire 3 + Volt for interactive components without JavaScript complexity
  • Fortify for headless authentication with custom Volt views
  • Eloquent ORM for database operations
  • Blade + Flux UI for consistent, accessible component design
  • Queue workers for background email processing and scheduled reminders

This architecture prioritizes simplicity and maintainability over distributed complexity.

2.2 System Context Diagram

graph TB
    subgraph "Users"
        Admin[Admin/Lawyer]
        Client[Clients]
        Public[Public Visitors]
    end

    subgraph "Libra Platform"
        App[Laravel Application<br/>libra.ps]
    end

    subgraph "External Services"
        SMTP[SMTP Server<br/>no-reply@libra.ps]
    end

    subgraph "Infrastructure"
        Server[Rocky Linux 9<br/>Client-Managed]
    end

    Admin -->|Manage users, bookings,<br/>timelines, posts| App
    Client -->|Book consultations,<br/>view timelines| App
    Public -->|View posts,<br/>firm info| App

    App -->|Send emails| SMTP
    App -->|Deployed on| Server

2.3 Container Diagram

graph TB
    subgraph "Client Devices"
        Browser[Web Browser]
        Mobile[Mobile Browser]
    end

    subgraph "Web Server - Nginx"
        Static[Static Assets<br/>CSS/JS/Images]
        PHP[PHP-FPM 8.4]
    end

    subgraph "Laravel Application"
        Router[Route Handler]
        MW[Middleware Stack<br/>Auth, Locale, CSRF]

        subgraph "Presentation Layer"
            Volt[Volt Components]
            Flux[Flux UI Components]
            Blade[Blade Templates]
        end

        subgraph "Business Logic Layer"
            Actions[Action Classes]
            Fortify[Fortify Auth]
            Policies[Authorization Policies]
        end

        subgraph "Data Access Layer"
            Models[Eloquent Models]
            Observers[Model Observers]
        end
    end

    subgraph "Background Processing"
        Queue[Queue Worker<br/>Supervisor]
        Scheduler[Task Scheduler<br/>Cron]
    end

    subgraph "Data Storage"
        MariaDB[(MariaDB<br/>Production)]
        SQLite[(SQLite<br/>Development)]
        FileStorage[Local Storage<br/>Exports/Logs]
    end

    subgraph "External"
        SMTP[SMTP Server]
    end

    Browser --> Static
    Browser --> PHP
    Mobile --> Static
    Mobile --> PHP

    PHP --> Router
    Router --> MW
    MW --> Volt
    MW --> Blade
    Volt --> Flux
    Volt --> Actions
    Actions --> Fortify
    Actions --> Policies
    Actions --> Models
    Models --> Observers
    Models --> MariaDB
    Models --> SQLite

    Queue --> Actions
    Queue --> SMTP
    Scheduler --> Queue
    Actions --> FileStorage

2.4 Architectural Patterns

Pattern Description Rationale
MVC + Livewire Laravel MVC with Livewire reactive components Standard Laravel pattern; eliminates need for separate SPA
Action Classes Single-purpose classes for business logic Keeps controllers thin; reusable; testable
Form Objects Livewire form objects for validation Encapsulates validation; reusable forms
Observer Pattern Eloquent observers for model events Automatic audit logging, notification triggers
Queue-based Processing Background jobs for emails and heavy operations Non-blocking UX; reliable delivery
Policy-based Authorization Laravel Policies for access control Clean separation of authorization logic
Repository Pattern Optional for complex queries Only if query complexity warrants

3. Technology Stack

3.1 Core Technologies

Category Technology Version Purpose Rationale
Runtime PHP 8.4.x Server-side language Latest stable; required by Laravel 12
Framework Laravel 12.x Application framework Industry standard; excellent ecosystem
Reactive UI Livewire 3.7.x Interactive components No JS build complexity; server-state
Components Volt 1.10.x Single-file components Cleaner organization; class-based
UI Library Flux UI Free 2.10.x Pre-built components Consistent design; accessibility
CSS Tailwind CSS 4.x Utility-first styling Rapid development; RTL support
Auth Laravel Fortify 1.33.x Headless auth Flexible; 2FA support
Database (Prod) MariaDB 10.11+ Production database MySQL-compatible; performant
Database (Dev) SQLite Latest Development database Zero config; fast tests
Testing Pest 4.x Testing framework Elegant syntax; Laravel integration
Code Style Laravel Pint 1.x Code formatting Consistent style
Queue Database Driver - Job processing Simple; no Redis dependency

3.2 Frontend Dependencies

Package Version Purpose
Alpine.js (via Livewire) Minimal JS interactivity
Vite 6.x Asset bundling
Google Fonts - Cairo (Arabic), Montserrat (English)
Heroicons (via Flux) Icon library

3.3 Backend Dependencies (To Add)

Package Version Purpose
barryvdh/laravel-dompdf ^3.0 PDF export generation
league/csv ^9.0 CSV export generation
spatie/icalendar-generator ^2.0 .ics calendar file generation

3.4 Development Dependencies

Package Purpose
Laravel Sail Docker development environment (optional)
Laravel Telescope Debug assistant (dev only)
Laravel Pint Code formatting

4. Data Models

4.1 Entity Relationship Diagram

erDiagram
    users ||--o{ consultations : "has many"
    users ||--o{ timelines : "has many"
    users ||--o{ notifications : "has many"
    timelines ||--o{ timeline_updates : "has many"
    users ||--o{ admin_logs : "performed by"

    users {
        bigint id PK
        enum user_type "individual|company|admin"
        string full_name
        string national_id "nullable, encrypted"
        string company_name "nullable"
        string company_cert_number "nullable"
        string contact_person_name "nullable"
        string contact_person_id "nullable"
        string email UK
        string phone
        string password "hashed"
        enum status "active|deactivated"
        string preferred_language "ar|en"
        timestamp email_verified_at
        text two_factor_secret "nullable"
        text two_factor_recovery_codes "nullable"
        timestamp two_factor_confirmed_at
        timestamps created_updated
    }

    consultations {
        bigint id PK
        bigint user_id FK
        date booking_date
        time booking_time
        text problem_summary
        enum consultation_type "free|paid"
        decimal payment_amount "nullable"
        enum payment_status "pending|received|na"
        enum status "pending|approved|rejected|completed|no_show|cancelled"
        text admin_notes "nullable"
        timestamps created_updated
    }

    timelines {
        bigint id PK
        bigint user_id FK
        string case_name
        string case_reference "nullable"
        enum status "active|archived"
        timestamps created_updated
    }

    timeline_updates {
        bigint id PK
        bigint timeline_id FK
        bigint admin_id FK
        text update_text
        timestamps created_updated
    }

    posts {
        bigint id PK
        json title "ar and en"
        json body "ar and en"
        enum status "draft|published"
        timestamp published_at "nullable"
        timestamps created_updated
    }

    working_hours {
        bigint id PK
        tinyint day_of_week "0-6 Sunday-Saturday"
        time start_time
        time end_time
        boolean is_active
    }

    blocked_times {
        bigint id PK
        date block_date
        time start_time "nullable for full day"
        time end_time "nullable for full day"
        string reason "nullable"
        timestamps created_updated
    }

    notifications {
        bigint id PK
        bigint user_id FK
        string type
        json data
        timestamp read_at "nullable"
        timestamp sent_at "nullable"
        timestamps created_updated
    }

    admin_logs {
        bigint id PK
        bigint admin_id FK
        string action
        string target_type
        bigint target_id "nullable"
        json old_values "nullable"
        json new_values "nullable"
        string ip_address
        timestamp created_at
    }

    settings {
        bigint id PK
        string key UK
        json value
        timestamps created_updated
    }

4.2 Model Definitions

User Model

// app/Models/User.php
class User extends Authenticatable
{
    use HasFactory, Notifiable, TwoFactorAuthenticatable;

    protected $fillable = [
        'user_type',
        'full_name',
        'national_id',
        'company_name',
        'company_cert_number',
        'contact_person_name',
        'contact_person_id',
        'email',
        'phone',
        'password',
        'status',
        'preferred_language',
    ];

    protected $hidden = [
        'password',
        'remember_token',
        'two_factor_secret',
        'two_factor_recovery_codes',
    ];

    protected function casts(): array
    {
        return [
            'user_type' => UserType::class,
            'status' => UserStatus::class,
            'email_verified_at' => 'datetime',
            'two_factor_confirmed_at' => 'datetime',
            'password' => 'hashed',
        ];
    }

    // Relationships
    public function consultations(): HasMany;
    public function timelines(): HasMany;
    public function notifications(): HasMany;
    public function adminLogs(): HasMany;

    // Scopes
    public function scopeActive(Builder $query): Builder;
    public function scopeClients(Builder $query): Builder;
    public function scopeIndividuals(Builder $query): Builder;
    public function scopeCompanies(Builder $query): Builder;

    // Helpers
    public function isAdmin(): bool;
    public function isClient(): bool;
    public function isIndividual(): bool;
    public function isCompany(): bool;
    public function canBookOnDate(Carbon $date): bool;
    public function hasBookingOnDate(Carbon $date): bool;
}

Consultation Model

// app/Models/Consultation.php
class Consultation extends Model
{
    use HasFactory;

    protected $fillable = [
        'user_id',
        'booking_date',
        'booking_time',
        'problem_summary',
        'consultation_type',
        'payment_amount',
        'payment_status',
        'status',
        'admin_notes',
    ];

    protected function casts(): array
    {
        return [
            'booking_date' => 'date',
            'booking_time' => 'datetime:H:i',
            'consultation_type' => ConsultationType::class,
            'payment_status' => PaymentStatus::class,
            'status' => ConsultationStatus::class,
            'payment_amount' => 'decimal:2',
        ];
    }

    // Relationships
    public function user(): BelongsTo;

    // Scopes
    public function scopePending(Builder $query): Builder;
    public function scopeApproved(Builder $query): Builder;
    public function scopeUpcoming(Builder $query): Builder;
    public function scopeForDate(Builder $query, Carbon $date): Builder;
    public function scopeForDateRange(Builder $query, Carbon $start, Carbon $end): Builder;

    // Accessors
    public function getEndTimeAttribute(): Carbon;
    public function getStartDateTimeAttribute(): Carbon;
    public function getEndDateTimeAttribute(): Carbon;

    // Helpers
    public function isPaid(): bool;
    public function isPending(): bool;
    public function canBeApproved(): bool;
    public function canBeCancelled(): bool;
}

Timeline Model

// app/Models/Timeline.php
class Timeline extends Model
{
    use HasFactory;

    protected $fillable = [
        'user_id',
        'case_name',
        'case_reference',
        'status',
    ];

    protected function casts(): array
    {
        return [
            'status' => TimelineStatus::class,
        ];
    }

    // Relationships
    public function user(): BelongsTo;
    public function updates(): HasMany;
    public function latestUpdate(): HasOne;

    // Scopes
    public function scopeActive(Builder $query): Builder;
    public function scopeArchived(Builder $query): Builder;
    public function scopeForUser(Builder $query, User $user): Builder;
}

Post Model

// app/Models/Post.php
class Post extends Model
{
    use HasFactory;

    protected $fillable = [
        'title',
        'body',
        'status',
        'published_at',
    ];

    protected function casts(): array
    {
        return [
            'title' => 'array',
            'body' => 'array',
            'status' => PostStatus::class,
            'published_at' => 'datetime',
        ];
    }

    // Accessors for localized content
    public function getTitleLocalizedAttribute(): string;
    public function getBodyLocalizedAttribute(): string;

    // Scopes
    public function scopePublished(Builder $query): Builder;
    public function scopeDraft(Builder $query): Builder;

    // Helpers
    public function isPublished(): bool;
}

4.3 Enums

// app/Enums/UserType.php
enum UserType: string
{
    case Individual = 'individual';
    case Company = 'company';
    case Admin = 'admin';

    public function label(): string
    {
        return match($this) {
            self::Individual => __('enums.user_type.individual'),
            self::Company => __('enums.user_type.company'),
            self::Admin => __('enums.user_type.admin'),
        };
    }
}

// app/Enums/UserStatus.php
enum UserStatus: string
{
    case Active = 'active';
    case Deactivated = 'deactivated';
}

// app/Enums/ConsultationType.php
enum ConsultationType: string
{
    case Free = 'free';
    case Paid = 'paid';
}

// app/Enums/ConsultationStatus.php
enum ConsultationStatus: string
{
    case Pending = 'pending';
    case Approved = 'approved';
    case Rejected = 'rejected';
    case Completed = 'completed';
    case NoShow = 'no_show';
    case Cancelled = 'cancelled';

    public function color(): string
    {
        return match($this) {
            self::Pending => 'yellow',
            self::Approved => 'blue',
            self::Rejected => 'red',
            self::Completed => 'green',
            self::NoShow => 'gray',
            self::Cancelled => 'red',
        };
    }
}

// app/Enums/PaymentStatus.php
enum PaymentStatus: string
{
    case Pending = 'pending';
    case Received = 'received';
    case NotApplicable = 'na';
}

// app/Enums/TimelineStatus.php
enum TimelineStatus: string
{
    case Active = 'active';
    case Archived = 'archived';
}

// app/Enums/PostStatus.php
enum PostStatus: string
{
    case Draft = 'draft';
    case Published = 'published';
}

5. Database Schema

5.1 Migration Order

  1. 0001_01_01_000000_create_users_table.php (modify existing)
  2. 0001_01_01_000001_create_cache_table.php (existing)
  3. 0001_01_01_000002_create_jobs_table.php (existing)
  4. 2025_01_01_000001_add_profile_fields_to_users_table.php
  5. 2025_01_01_000002_create_consultations_table.php
  6. 2025_01_01_000003_create_timelines_table.php
  7. 2025_01_01_000004_create_timeline_updates_table.php
  8. 2025_01_01_000005_create_posts_table.php
  9. 2025_01_01_000006_create_working_hours_table.php
  10. 2025_01_01_000007_create_blocked_times_table.php
  11. 2025_01_01_000008_create_notifications_table.php
  12. 2025_01_01_000009_create_admin_logs_table.php
  13. 2025_01_01_000010_create_settings_table.php

5.2 Key Migration Examples

Users Table Extension

// database/migrations/2025_01_01_000001_add_profile_fields_to_users_table.php
public function up(): void
{
    Schema::table('users', function (Blueprint $table) {
        $table->string('user_type')->default('individual')->after('id');
        $table->string('full_name')->nullable()->after('name');
        $table->string('national_id')->nullable()->after('full_name');
        $table->string('company_name')->nullable()->after('national_id');
        $table->string('company_cert_number')->nullable()->after('company_name');
        $table->string('contact_person_name')->nullable()->after('company_cert_number');
        $table->string('contact_person_id')->nullable()->after('contact_person_name');
        $table->string('phone', 20)->nullable()->after('email');
        $table->string('status')->default('active')->after('password');
        $table->string('preferred_language', 2)->default('ar')->after('status');

        $table->index('user_type');
        $table->index('status');
        $table->index(['user_type', 'status']);
    });
}

Consultations Table

// database/migrations/2025_01_01_000002_create_consultations_table.php
public function up(): void
{
    Schema::create('consultations', function (Blueprint $table) {
        $table->id();
        $table->foreignId('user_id')->constrained()->cascadeOnDelete();
        $table->date('booking_date');
        $table->time('booking_time');
        $table->text('problem_summary');
        $table->string('consultation_type')->default('free');
        $table->decimal('payment_amount', 10, 2)->nullable();
        $table->string('payment_status')->default('na');
        $table->string('status')->default('pending');
        $table->text('admin_notes')->nullable();
        $table->timestamps();

        $table->index('booking_date');
        $table->index('status');
        $table->index(['booking_date', 'booking_time']);
        $table->unique(['user_id', 'booking_date']); // One booking per user per day
    });
}

Working Hours Table

// database/migrations/2025_01_01_000006_create_working_hours_table.php
public function up(): void
{
    Schema::create('working_hours', function (Blueprint $table) {
        $table->id();
        $table->tinyInteger('day_of_week')->unsigned(); // 0=Sunday, 6=Saturday
        $table->time('start_time');
        $table->time('end_time');
        $table->boolean('is_active')->default(true);

        $table->unique('day_of_week');
    });
}

5.3 Indexes Strategy

Table Index Columns Purpose
users user_type_status user_type, status Filter clients
consultations booking_date booking_date Calendar queries
consultations status status Filter by status
consultations user_date_unique user_id, booking_date Enforce 1/day limit
timelines user_status user_id, status Client timeline list
posts status_published status, published_at Published posts list
admin_logs target target_type, target_id Audit trail lookup

6. Application Structure

6.1 Directory Structure

libra/
├── app/
│   ├── Actions/                      # Business logic (single-purpose)
│   │   ├── Fortify/                 # Auth actions (existing)
│   │   │   ├── CreateNewUser.php
│   │   │   ├── ResetUserPassword.php
│   │   │   └── UpdateUserPassword.php
│   │   ├── Consultation/
│   │   │   ├── CreateConsultationAction.php
│   │   │   ├── ApproveConsultationAction.php
│   │   │   ├── RejectConsultationAction.php
│   │   │   ├── CompleteConsultationAction.php
│   │   │   └── CancelConsultationAction.php
│   │   ├── Timeline/
│   │   │   ├── CreateTimelineAction.php
│   │   │   ├── AddTimelineUpdateAction.php
│   │   │   └── ArchiveTimelineAction.php
│   │   ├── User/
│   │   │   ├── CreateClientAction.php
│   │   │   ├── UpdateClientAction.php
│   │   │   ├── DeactivateClientAction.php
│   │   │   ├── ReactivateClientAction.php
│   │   │   ├── DeleteClientAction.php
│   │   │   └── ConvertClientTypeAction.php
│   │   ├── Post/
│   │   │   ├── CreatePostAction.php
│   │   │   ├── UpdatePostAction.php
│   │   │   ├── PublishPostAction.php
│   │   │   └── DeletePostAction.php
│   │   └── Export/
│   │       ├── ExportUsersAction.php
│   │       ├── ExportConsultationsAction.php
│   │       └── GenerateMonthlyReportAction.php
│   ├── Enums/                        # PHP 8.1+ enums
│   │   ├── UserType.php
│   │   ├── UserStatus.php
│   │   ├── ConsultationType.php
│   │   ├── ConsultationStatus.php
│   │   ├── PaymentStatus.php
│   │   ├── TimelineStatus.php
│   │   └── PostStatus.php
│   ├── Http/
│   │   ├── Controllers/              # Minimal controllers
│   │   │   ├── LanguageController.php
│   │   │   └── CalendarDownloadController.php
│   │   └── Middleware/
│   │       ├── SetLocale.php
│   │       ├── EnsureUserIsAdmin.php
│   │       └── EnsureUserIsActive.php
│   ├── Jobs/                         # Queue jobs
│   │   ├── SendWelcomeEmail.php
│   │   ├── SendBookingNotification.php
│   │   ├── SendConsultationReminder.php
│   │   ├── SendTimelineUpdateNotification.php
│   │   └── GenerateExportFile.php
│   ├── Livewire/                     # Full-page Livewire components (if any)
│   │   └── Forms/                    # Livewire form objects
│   │       ├── ConsultationForm.php
│   │       ├── ClientForm.php
│   │       ├── TimelineForm.php
│   │       └── PostForm.php
│   ├── Mail/                         # Mailable classes
│   │   ├── WelcomeMail.php
│   │   ├── BookingSubmittedMail.php
│   │   ├── BookingApprovedMail.php
│   │   ├── BookingRejectedMail.php
│   │   ├── ConsultationReminderMail.php
│   │   ├── TimelineUpdatedMail.php
│   │   └── NewBookingRequestMail.php
│   ├── Models/                       # Eloquent models
│   │   ├── User.php
│   │   ├── Consultation.php
│   │   ├── Timeline.php
│   │   ├── TimelineUpdate.php
│   │   ├── Post.php
│   │   ├── WorkingHour.php
│   │   ├── BlockedTime.php
│   │   ├── Notification.php
│   │   ├── AdminLog.php
│   │   └── Setting.php
│   ├── Observers/                    # Model observers
│   │   ├── ConsultationObserver.php
│   │   ├── TimelineUpdateObserver.php
│   │   └── UserObserver.php
│   ├── Policies/                     # Authorization policies
│   │   ├── ConsultationPolicy.php
│   │   ├── TimelinePolicy.php
│   │   ├── PostPolicy.php
│   │   └── UserPolicy.php
│   ├── Providers/
│   │   ├── AppServiceProvider.php
│   │   └── FortifyServiceProvider.php
│   └── Services/                     # Cross-cutting services
│       ├── AvailabilityService.php
│       ├── CalendarService.php
│       └── ExportService.php
├── config/
│   └── libra.php                     # App-specific config
├── database/
│   ├── factories/
│   │   ├── UserFactory.php
│   │   ├── ConsultationFactory.php
│   │   ├── TimelineFactory.php
│   │   ├── TimelineUpdateFactory.php
│   │   └── PostFactory.php
│   ├── migrations/
│   └── seeders/
│       ├── DatabaseSeeder.php
│       ├── AdminSeeder.php
│       ├── WorkingHoursSeeder.php
│       └── DemoDataSeeder.php
├── docs/
│   ├── architecture.md               # This document
│   ├── prd.md                        # Product requirements
│   ├── epics/                        # Epic definitions
│   └── stories/                      # User stories
├── resources/
│   ├── views/
│   │   ├── components/               # Blade components
│   │   │   ├── layouts/
│   │   │   │   ├── app.blade.php    # Main app layout
│   │   │   │   ├── guest.blade.php  # Guest/public layout
│   │   │   │   └── admin.blade.php  # Admin layout
│   │   │   └── ui/                   # Reusable UI components
│   │   │       ├── card.blade.php
│   │   │       ├── stat-card.blade.php
│   │   │       ├── status-badge.blade.php
│   │   │       └── language-switcher.blade.php
│   │   ├── emails/                   # Email templates
│   │   │   ├── welcome.blade.php
│   │   │   ├── booking-submitted.blade.php
│   │   │   ├── booking-approved.blade.php
│   │   │   ├── booking-rejected.blade.php
│   │   │   ├── consultation-reminder.blade.php
│   │   │   ├── timeline-updated.blade.php
│   │   │   └── new-booking-request.blade.php
│   │   ├── livewire/                 # Volt single-file components
│   │   │   ├── admin/               # Admin area
│   │   │   │   ├── dashboard.blade.php
│   │   │   │   ├── users/
│   │   │   │   │   ├── index.blade.php
│   │   │   │   │   ├── create.blade.php
│   │   │   │   │   └── edit.blade.php
│   │   │   │   ├── consultations/
│   │   │   │   │   ├── index.blade.php
│   │   │   │   │   ├── pending.blade.php
│   │   │   │   │   ├── calendar.blade.php
│   │   │   │   │   └── show.blade.php
│   │   │   │   ├── timelines/
│   │   │   │   │   ├── index.blade.php
│   │   │   │   │   ├── create.blade.php
│   │   │   │   │   └── show.blade.php
│   │   │   │   ├── posts/
│   │   │   │   │   ├── index.blade.php
│   │   │   │   │   ├── create.blade.php
│   │   │   │   │   └── edit.blade.php
│   │   │   │   ├── settings/
│   │   │   │   │   ├── working-hours.blade.php
│   │   │   │   │   ├── blocked-times.blade.php
│   │   │   │   │   └── content-pages.blade.php
│   │   │   │   └── reports/
│   │   │   │       └── index.blade.php
│   │   │   ├── client/              # Client area
│   │   │   │   ├── dashboard.blade.php
│   │   │   │   ├── consultations/
│   │   │   │   │   ├── index.blade.php
│   │   │   │   │   └── book.blade.php
│   │   │   │   ├── timelines/
│   │   │   │   │   ├── index.blade.php
│   │   │   │   │   └── show.blade.php
│   │   │   │   └── profile.blade.php
│   │   │   ├── pages/               # Public pages
│   │   │   │   ├── home.blade.php
│   │   │   │   ├── posts/
│   │   │   │   │   ├── index.blade.php
│   │   │   │   │   └── show.blade.php
│   │   │   │   ├── terms.blade.php
│   │   │   │   └── privacy.blade.php
│   │   │   ├── auth/                # Auth pages (existing)
│   │   │   └── settings/            # User settings (existing)
│   │   ├── pdf/                     # PDF templates
│   │   │   ├── users-export.blade.php
│   │   │   ├── consultations-export.blade.php
│   │   │   └── monthly-report.blade.php
│   │   └── errors/                  # Error pages
│   │       ├── 404.blade.php
│   │       ├── 403.blade.php
│   │       └── 500.blade.php
│   ├── css/
│   │   └── app.css                  # Tailwind entry point
│   ├── js/
│   │   └── app.js                   # Minimal JS
│   └── lang/                        # Translations
│       ├── ar/
│       │   ├── auth.php
│       │   ├── validation.php
│       │   ├── pagination.php
│       │   ├── passwords.php
│       │   ├── messages.php
│       │   ├── models.php
│       │   ├── enums.php
│       │   └── emails.php
│       └── en/
│           └── ... (same structure)
├── routes/
│   ├── web.php                      # Web routes
│   └── console.php                  # Scheduled commands
├── tests/
│   ├── Feature/
│   │   ├── Admin/
│   │   │   ├── DashboardTest.php
│   │   │   ├── UserManagementTest.php
│   │   │   ├── ConsultationManagementTest.php
│   │   │   ├── TimelineManagementTest.php
│   │   │   ├── PostManagementTest.php
│   │   │   └── SettingsTest.php
│   │   ├── Client/
│   │   │   ├── DashboardTest.php
│   │   │   ├── BookingTest.php
│   │   │   ├── TimelineViewTest.php
│   │   │   └── ProfileTest.php
│   │   ├── Public/
│   │   │   ├── HomePageTest.php
│   │   │   ├── PostsTest.php
│   │   │   └── LanguageSwitchTest.php
│   │   └── Auth/
│   │       └── ... (existing tests)
│   └── Unit/
│       ├── Actions/
│       ├── Models/
│       ├── Services/
│       └── Enums/
├── storage/
│   └── app/
│       └── exports/                 # Generated export files
├── .github/
│   └── workflows/
│       └── ci.yml                   # GitHub Actions CI
└── public/
    └── build/                       # Compiled assets

7. Authentication & Authorization

7.1 Authentication (Fortify)

Current Configuration:

  • Email/password login
  • Email verification
  • Two-factor authentication (TOTP)
  • Password reset

Modifications Required:

  1. Disable public registration - Admin creates all users
  2. Custom login redirect - Admin → /admin/dashboard, Client → /client/dashboard
  3. Admin-triggered password reset - No public reset
// config/fortify.php
'features' => [
    // Features::registration(), // DISABLED
    Features::resetPasswords(),
    Features::emailVerification(),
    Features::updateProfileInformation(),
    Features::updatePasswords(),
    Features::twoFactorAuthentication([
        'confirm' => true,
        'confirmPassword' => true,
    ]),
],

7.2 Authorization (Policies)

// app/Policies/ConsultationPolicy.php
class ConsultationPolicy
{
    public function viewAny(User $user): bool
    {
        return true; // All authenticated users can see their own
    }

    public function view(User $user, Consultation $consultation): bool
    {
        return $user->isAdmin() || $consultation->user_id === $user->id;
    }

    public function create(User $user): bool
    {
        return $user->isClient();
    }

    public function update(User $user, Consultation $consultation): bool
    {
        return $user->isAdmin();
    }

    public function delete(User $user, Consultation $consultation): bool
    {
        return $user->isAdmin();
    }
}

// app/Policies/TimelinePolicy.php
class TimelinePolicy
{
    public function viewAny(User $user): bool
    {
        return true;
    }

    public function view(User $user, Timeline $timeline): bool
    {
        return $user->isAdmin() || $timeline->user_id === $user->id;
    }

    public function create(User $user): bool
    {
        return $user->isAdmin();
    }

    public function update(User $user, Timeline $timeline): bool
    {
        return $user->isAdmin();
    }

    public function addUpdate(User $user, Timeline $timeline): bool
    {
        return $user->isAdmin();
    }
}

// app/Policies/UserPolicy.php
class UserPolicy
{
    public function viewAny(User $user): bool
    {
        return $user->isAdmin();
    }

    public function view(User $user, User $model): bool
    {
        return $user->isAdmin() || $user->id === $model->id;
    }

    public function create(User $user): bool
    {
        return $user->isAdmin();
    }

    public function update(User $user, User $model): bool
    {
        return $user->isAdmin();
    }

    public function delete(User $user, User $model): bool
    {
        return $user->isAdmin() && !$model->isAdmin();
    }
}

7.3 Middleware

// app/Http/Middleware/SetLocale.php
class SetLocale
{
    public function handle(Request $request, Closure $next): Response
    {
        $locale = session('locale')
            ?? $request->user()?->preferred_language
            ?? config('app.locale');

        if (!in_array($locale, ['ar', 'en'])) {
            $locale = 'ar';
        }

        app()->setLocale($locale);

        return $next($request);
    }
}

// app/Http/Middleware/EnsureUserIsAdmin.php
class EnsureUserIsAdmin
{
    public function handle(Request $request, Closure $next): Response
    {
        if (!$request->user()?->isAdmin()) {
            abort(403, __('messages.unauthorized'));
        }

        return $next($request);
    }
}

// app/Http/Middleware/EnsureUserIsActive.php
class EnsureUserIsActive
{
    public function handle(Request $request, Closure $next): Response
    {
        if ($request->user()?->status === UserStatus::Deactivated) {
            Auth::logout();
            $request->session()->invalidate();
            $request->session()->regenerateToken();

            return redirect()->route('login')
                ->with('error', __('messages.account_deactivated'));
        }

        return $next($request);
    }
}

7.4 Middleware Registration

// bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
    $middleware->web(append: [
        \App\Http\Middleware\SetLocale::class,
    ]);

    $middleware->alias([
        'admin' => \App\Http\Middleware\EnsureUserIsAdmin::class,
        'active' => \App\Http\Middleware\EnsureUserIsActive::class,
    ]);

    $middleware->validateCsrfTokens(except: [
        // Add any exceptions here
    ]);
})

8. Core Workflows

8.1 Consultation Booking Flow

sequenceDiagram
    participant C as Client
    participant V as Volt Component
    participant A as Action
    participant S as AvailabilityService
    participant M as Model
    participant Q as Queue
    participant E as Email (Admin)

    C->>V: Open booking page
    V->>S: getAvailableSlots(date)
    S->>M: Query working_hours, blocked_times, consultations
    S-->>V: Available time slots
    V-->>C: Display calendar

    C->>V: Select date/time, enter summary
    V->>V: Validate (date future, slot available, no existing booking)

    alt Validation fails
        V-->>C: Show error message
    else Validation passes
        V->>A: CreateConsultationAction
        A->>M: Check user has no booking on date
        A->>M: Create consultation (status: pending)
        A->>M: Log admin action
        M-->>A: Consultation created
        A->>Q: Dispatch SendBookingNotification (to admin)
        Q->>E: Email admin at no-reply@libra.ps
        A-->>V: Success
        V-->>C: Show confirmation message
    end

8.2 Admin Consultation Approval Flow

sequenceDiagram
    participant Ad as Admin
    participant V as Volt Component
    participant A as Action
    participant M as Model
    participant Q as Queue
    participant E as Email (Client)

    Ad->>V: View pending consultation
    Ad->>V: Set type (free/paid), amount if paid
    Ad->>V: Click Approve

    V->>A: ApproveConsultationAction
    A->>M: Update consultation (status: approved, type, amount)
    A->>M: Log admin action
    M-->>A: Updated

    A->>Q: Dispatch SendBookingApprovedMail (with .ics attachment)
    A->>Q: Schedule SendConsultationReminder (24h before)
    A->>Q: Schedule SendConsultationReminder (2h before)

    Q->>E: Send approval email to client
    A-->>V: Success
    V-->>Ad: Refresh list, show success toast

8.3 Timeline Update Flow

sequenceDiagram
    participant Ad as Admin
    participant V as Volt Component
    participant A as Action
    participant M as Model
    participant O as Observer
    participant Q as Queue
    participant E as Email

    Ad->>V: Navigate to timeline
    Ad->>V: Enter update text
    Ad->>V: Submit

    V->>A: AddTimelineUpdateAction
    A->>M: Create TimelineUpdate
    M->>O: TimelineUpdateObserver::created
    O->>Q: Dispatch SendTimelineUpdateNotification
    A->>M: Log admin action
    A-->>V: Success

    Q->>E: Send notification to client
    V-->>Ad: Refresh timeline, show new update

8.4 User Creation Flow

sequenceDiagram
    participant Ad as Admin
    participant V as Volt Component
    participant A as Action
    participant M as Model
    participant Q as Queue
    participant E as Email

    Ad->>V: Fill user form (type, name, email, etc.)
    Ad->>V: Set password
    Ad->>V: Submit

    V->>V: Validate form
    V->>A: CreateClientAction
    A->>M: Create User
    A->>M: Log admin action
    M-->>A: User created
    A->>Q: Dispatch SendWelcomeEmail (with credentials)
    Q->>E: Send welcome email to new user
    A-->>V: Success
    V-->>Ad: Redirect to user list

9. Routing Structure

9.1 Route Definitions

// routes/web.php

use App\Http\Controllers\LanguageController;
use App\Http\Controllers\CalendarDownloadController;
use Illuminate\Support\Facades\Route;
use Livewire\Volt\Volt;

/*
|--------------------------------------------------------------------------
| Public Routes
|--------------------------------------------------------------------------
*/

// Home
Volt::route('/', 'pages.home')->name('home');

// Posts
Volt::route('/posts', 'pages.posts.index')->name('posts.index');
Volt::route('/posts/{post}', 'pages.posts.show')->name('posts.show');

// Legal pages
Volt::route('/terms', 'pages.terms')->name('terms');
Volt::route('/privacy', 'pages.privacy')->name('privacy');

// Language switch
Route::get('/language/{locale}', LanguageController::class)
    ->name('language.switch')
    ->where('locale', 'ar|en');

/*
|--------------------------------------------------------------------------
| Client Routes (Authenticated Non-Admin)
|--------------------------------------------------------------------------
*/

Route::middleware(['auth', 'verified', 'active'])->prefix('client')->name('client.')->group(function () {
    Volt::route('/dashboard', 'client.dashboard')->name('dashboard');

    // Consultations
    Volt::route('/consultations', 'client.consultations.index')->name('consultations.index');
    Volt::route('/consultations/book', 'client.consultations.book')->name('consultations.book');
    Route::get('/consultations/{consultation}/calendar', CalendarDownloadController::class)
        ->name('consultations.calendar');

    // Timelines
    Volt::route('/timelines', 'client.timelines.index')->name('timelines.index');
    Volt::route('/timelines/{timeline}', 'client.timelines.show')->name('timelines.show');

    // Profile (view only)
    Volt::route('/profile', 'client.profile')->name('profile');
});

/*
|--------------------------------------------------------------------------
| Admin Routes
|--------------------------------------------------------------------------
*/

Route::middleware(['auth', 'verified', 'admin'])->prefix('admin')->name('admin.')->group(function () {
    Volt::route('/dashboard', 'admin.dashboard')->name('dashboard');

    // Users
    Volt::route('/users', 'admin.users.index')->name('users.index');
    Volt::route('/users/create', 'admin.users.create')->name('users.create');
    Volt::route('/users/{user}/edit', 'admin.users.edit')->name('users.edit');

    // Consultations
    Volt::route('/consultations', 'admin.consultations.index')->name('consultations.index');
    Volt::route('/consultations/pending', 'admin.consultations.pending')->name('consultations.pending');
    Volt::route('/consultations/calendar', 'admin.consultations.calendar')->name('consultations.calendar');
    Volt::route('/consultations/{consultation}', 'admin.consultations.show')->name('consultations.show');

    // Timelines
    Volt::route('/timelines', 'admin.timelines.index')->name('timelines.index');
    Volt::route('/timelines/create', 'admin.timelines.create')->name('timelines.create');
    Volt::route('/timelines/{timeline}', 'admin.timelines.show')->name('timelines.show');

    // Posts
    Volt::route('/posts', 'admin.posts.index')->name('posts.index');
    Volt::route('/posts/create', 'admin.posts.create')->name('posts.create');
    Volt::route('/posts/{post}/edit', 'admin.posts.edit')->name('posts.edit');

    // Settings
    Volt::route('/settings/working-hours', 'admin.settings.working-hours')->name('settings.working-hours');
    Volt::route('/settings/blocked-times', 'admin.settings.blocked-times')->name('settings.blocked-times');
    Volt::route('/settings/content-pages', 'admin.settings.content-pages')->name('settings.content-pages');

    // Reports
    Volt::route('/reports', 'admin.reports.index')->name('reports.index');
});

9.2 Route Summary

Area Route Prefix Middleware Description
Public / none Home, posts, legal pages
Client /client auth, verified, active Client dashboard, bookings, timelines
Admin /admin auth, verified, admin Full management interface
Auth / (Fortify) varies Login, logout, password, 2FA
Settings /settings auth, verified User profile settings

10. Email System

10.1 Configuration

// config/mail.php (via .env)
MAIL_MAILER=smtp
MAIL_HOST=smtp.libra.ps
MAIL_PORT=587
MAIL_USERNAME=no-reply@libra.ps
MAIL_PASSWORD=****
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS=no-reply@libra.ps
MAIL_FROM_NAME="${APP_NAME}"

10.2 Email Templates

Email Class Trigger Recipient Queue Attachments
WelcomeMail User created Client Yes None
BookingSubmittedMail Consultation created Client Yes None
BookingApprovedMail Consultation approved Client Yes .ics file
BookingRejectedMail Consultation rejected Client Yes None
ConsultationReminderMail 24h/2h before Client Scheduled .ics file
TimelineUpdatedMail Timeline update added Client Yes None
NewBookingRequestMail Consultation created Admin Yes None

10.3 Email Template Example

// app/Mail/BookingApprovedMail.php
class BookingApprovedMail extends Mailable implements ShouldQueue
{
    use Queueable, SerializesModels;

    public function __construct(
        public Consultation $consultation
    ) {}

    public function envelope(): Envelope
    {
        $locale = $this->consultation->user->preferred_language;

        return new Envelope(
            subject: $locale === 'ar'
                ? 'تمت الموافقة على موعدك - مكتب ليبرا للمحاماة'
                : 'Your Consultation Approved - Libra Law Firm',
        );
    }

    public function content(): Content
    {
        return new Content(
            markdown: 'emails.booking-approved',
            with: [
                'consultation' => $this->consultation,
                'user' => $this->consultation->user,
                'locale' => $this->consultation->user->preferred_language,
            ],
        );
    }

    public function attachments(): array
    {
        $calendar = app(CalendarService::class);

        return [
            Attachment::fromData(
                fn () => $calendar->generateIcs($this->consultation),
                'consultation.ics'
            )->withMime('text/calendar'),
        ];
    }
}

10.4 Email Template Structure (Blade)

{{-- resources/views/emails/booking-approved.blade.php --}}
<x-mail::message>
@if($locale === 'ar')
# مرحباً {{ $user->full_name }}،

تمت الموافقة على موعد استشارتك.

**تفاصيل الموعد:**
- **التاريخ:** {{ $consultation->booking_date->format('d/m/Y') }}
- **الوقت:** {{ $consultation->booking_time->format('h:i A') }}
- **المدة:** 45 دقيقة
@if($consultation->consultation_type === 'paid')
- **نوع الاستشارة:** مدفوعة
- **المبلغ:** {{ $consultation->payment_amount }} شيكل
@else
- **نوع الاستشارة:** مجانية
@endif

يرجى إضافة الموعد إلى تقويمك باستخدام الملف المرفق.

@else
# Hello {{ $user->full_name }},

Your consultation has been approved.

**Appointment Details:**
- **Date:** {{ $consultation->booking_date->format('m/d/Y') }}
- **Time:** {{ $consultation->booking_time->format('h:i A') }}
- **Duration:** 45 minutes
@if($consultation->consultation_type === 'paid')
- **Type:** Paid Consultation
- **Amount:** {{ $consultation->payment_amount }} ILS
@else
- **Type:** Free Consultation
@endif

Please add this appointment to your calendar using the attached file.
@endif

<x-mail::button :url="route('client.consultations.index')">
{{ $locale === 'ar' ? 'عرض موعدي' : 'View My Consultation' }}
</x-mail::button>

{{ $locale === 'ar' ? 'مع تحيات،' : 'Best regards,' }}<br>
{{ config('app.name') }}
</x-mail::message>

11. Background Jobs & Scheduling

11.1 Queue Configuration

// config/queue.php
'default' => env('QUEUE_CONNECTION', 'database'),

'connections' => [
    'database' => [
        'driver' => 'database',
        'connection' => null,
        'table' => 'jobs',
        'queue' => 'default',
        'retry_after' => 90,
        'after_commit' => true,
    ],
],

11.2 Queue Jobs

Job Purpose Queue Retries Timeout
SendWelcomeEmail Welcome email to new user emails 3 60s
SendBookingNotification Notify admin of new booking emails 3 60s
SendBookingApprovedMail Approval email with .ics emails 3 60s
SendConsultationReminder Reminder 24h/2h before reminders 3 60s
SendTimelineUpdateNotification Timeline update notification emails 3 60s
GenerateExportFile Generate CSV/PDF export exports 1 300s

11.3 Job Example

// app/Jobs/SendConsultationReminder.php
class SendConsultationReminder implements ShouldQueue
{
    use Queueable;

    public int $tries = 3;
    public int $timeout = 60;
    public string $queue = 'reminders';

    public function __construct(
        public Consultation $consultation,
        public string $reminderType // '24h' or '2h'
    ) {}

    public function handle(): void
    {
        // Don't send if consultation is no longer approved
        if ($this->consultation->status !== ConsultationStatus::Approved) {
            return;
        }

        Mail::to($this->consultation->user)
            ->send(new ConsultationReminderMail(
                $this->consultation,
                $this->reminderType
            ));
    }

    public function failed(\Throwable $exception): void
    {
        Log::error('Failed to send consultation reminder', [
            'consultation_id' => $this->consultation->id,
            'reminder_type' => $this->reminderType,
            'error' => $exception->getMessage(),
        ]);
    }
}

11.4 Scheduled Tasks

// routes/console.php
use Illuminate\Support\Facades\Schedule;
use App\Models\Consultation;
use App\Jobs\SendConsultationReminder;
use App\Enums\ConsultationStatus;

// Send 24-hour reminders at 9 AM Gaza time
Schedule::call(function () {
    $tomorrow = now()->addDay()->toDateString();

    Consultation::where('status', ConsultationStatus::Approved)
        ->where('booking_date', $tomorrow)
        ->each(function ($consultation) {
            SendConsultationReminder::dispatch($consultation, '24h');
        });
})->dailyAt('09:00')->timezone('Asia/Gaza');

// Send 2-hour reminders (check every 15 minutes)
Schedule::call(function () {
    $targetTime = now()->addHours(2);

    Consultation::where('status', ConsultationStatus::Approved)
        ->where('booking_date', $targetTime->toDateString())
        ->whereBetween('booking_time', [
            $targetTime->subMinutes(7)->format('H:i:s'),
            $targetTime->addMinutes(8)->format('H:i:s'),
        ])
        ->each(function ($consultation) {
            SendConsultationReminder::dispatch($consultation, '2h');
        });
})->everyFifteenMinutes()->timezone('Asia/Gaza');

// Prune old failed jobs
Schedule::command('queue:prune-failed --hours=168')->weekly();

// Clear old cache
Schedule::command('cache:prune-stale-tags')->hourly();

11.5 Supervisor Configuration

; /etc/supervisor/conf.d/libra-worker.conf
[program:libra-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/libra/artisan queue:work database --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=www-data
numprocs=2
redirect_stderr=true
stdout_logfile=/var/www/libra/storage/logs/worker.log
stopwaitsecs=3600

12. Localization & Timezone

12.1 Timezone Configuration

Application Timezone: Asia/Gaza (Palestine)

// config/app.php
'timezone' => 'Asia/Gaza',

// config/libra.php
return [
    'timezone' => 'Asia/Gaza',
    'date_formats' => [
        'ar' => 'd/m/Y',      // 21/12/2025
        'en' => 'm/d/Y',      // 12/21/2025
    ],
    'time_format' => 'h:i A', // 02:30 PM (both languages)
    'datetime_formats' => [
        'ar' => 'd/m/Y h:i A',
        'en' => 'm/d/Y h:i A',
    ],
];

12.2 Date/Time Helper

// app/Helpers/DateTimeHelper.php
class DateTimeHelper
{
    public static function formatDate(Carbon $date, ?string $locale = null): string
    {
        $locale = $locale ?? app()->getLocale();
        $format = config("libra.date_formats.{$locale}", 'd/m/Y');

        return $date->format($format);
    }

    public static function formatTime(Carbon $time): string
    {
        return $time->format(config('libra.time_format'));
    }

    public static function formatDateTime(Carbon $datetime, ?string $locale = null): string
    {
        $locale = $locale ?? app()->getLocale();
        $format = config("libra.datetime_formats.{$locale}");

        return $datetime->format($format);
    }

    public static function parseUserDate(string $date, ?string $locale = null): Carbon
    {
        $locale = $locale ?? app()->getLocale();
        $format = config("libra.date_formats.{$locale}");

        return Carbon::createFromFormat($format, $date, config('libra.timezone'));
    }
}

12.3 Localization Structure

resources/lang/
├── ar/
│   ├── auth.php          # Authentication messages
│   ├── validation.php    # Validation messages
│   ├── pagination.php    # Pagination
│   ├── passwords.php     # Password reset messages
│   ├── messages.php      # General app messages
│   ├── models.php        # Model field labels
│   ├── enums.php         # Enum labels
│   └── emails.php        # Email content
└── en/
    └── ... (same structure)

12.4 RTL/LTR Support

{{-- resources/views/components/layouts/app.blade.php --}}
<!DOCTYPE html>
<html
    lang="{{ app()->getLocale() }}"
    dir="{{ app()->getLocale() === 'ar' ? 'rtl' : 'ltr' }}"
    class="{{ app()->getLocale() === 'ar' ? 'font-arabic' : 'font-english' }}"
>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>{{ $title ?? config('app.name') }}</title>

    {{-- Fonts --}}
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Cairo:wght@300;400;600;700&family=Montserrat:wght@300;400;600;700&display=swap" rel="stylesheet">

    @vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
/* resources/css/app.css */
@import "tailwindcss";

@theme {
    --font-arabic: 'Cairo', sans-serif;
    --font-english: 'Montserrat', sans-serif;

    /* Brand colors */
    --color-navy: #0A1F44;
    --color-gold: #D4AF37;
    --color-gold-light: #F4E4B8;
    --color-cream: #F9F7F4;
}

/* RTL utilities */
[dir="rtl"] .rtl\:space-x-reverse > :not([hidden]) ~ :not([hidden]) {
    --tw-space-x-reverse: 1;
}

.font-arabic {
    font-family: var(--font-arabic);
}

.font-english {
    font-family: var(--font-english);
}

13. Security

13.1 Security Measures

Measure Implementation Configuration
CSRF Protection Laravel default All POST/PUT/DELETE forms
XSS Prevention Blade escaping {{ }} Default for all output
SQL Injection Eloquent ORM Parameterized queries
Password Hashing bcrypt Laravel default
Session Security Secure cookies SESSION_SECURE_COOKIE=true
Rate Limiting Throttle middleware 5 login attempts/minute
Input Validation Form Requests All user input
HTTPS Forced in production APP_URL=https://
CSP Headers Middleware Restrictive policy

13.2 Rate Limiting

// bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
    $middleware->throttleApi();
})

// app/Providers/AppServiceProvider.php
public function boot(): void
{
    RateLimiter::for('login', function (Request $request) {
        return Limit::perMinute(5)->by($request->ip());
    });

    RateLimiter::for('api', function (Request $request) {
        return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
    });
}

13.3 Content Security Policy

// app/Http/Middleware/ContentSecurityPolicy.php
class ContentSecurityPolicy
{
    public function handle(Request $request, Closure $next): Response
    {
        $response = $next($request);

        $response->headers->set('Content-Security-Policy', implode('; ', [
            "default-src 'self'",
            "script-src 'self' 'unsafe-inline' 'unsafe-eval'", // Required for Livewire
            "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
            "font-src 'self' https://fonts.gstatic.com",
            "img-src 'self' data: https:",
            "connect-src 'self'",
            "frame-ancestors 'none'",
        ]));

        $response->headers->set('X-Content-Type-Options', 'nosniff');
        $response->headers->set('X-Frame-Options', 'DENY');
        $response->headers->set('X-XSS-Protection', '1; mode=block');
        $response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');

        return $response;
    }
}

13.4 Sensitive Data Handling

// app/Models/User.php
// Encrypt sensitive fields
protected function nationalId(): Attribute
{
    return Attribute::make(
        get: fn (?string $value) => $value ? decrypt($value) : null,
        set: fn (?string $value) => $value ? encrypt($value) : null,
    );
}

// Never expose in API/exports
protected $hidden = [
    'password',
    'remember_token',
    'two_factor_secret',
    'two_factor_recovery_codes',
    'national_id', // Encrypted, don't expose
];

13.5 Audit Logging

// app/Observers/UserObserver.php
class UserObserver
{
    public function created(User $user): void
    {
        $this->log('created', $user);
    }

    public function updated(User $user): void
    {
        if ($user->wasChanged()) {
            $this->log('updated', $user, $user->getOriginal(), $user->getAttributes());
        }
    }

    public function deleted(User $user): void
    {
        $this->log('deleted', $user);
    }

    private function log(string $action, User $user, ?array $old = null, ?array $new = null): void
    {
        AdminLog::create([
            'admin_id' => auth()->id(),
            'action' => $action,
            'target_type' => User::class,
            'target_id' => $user->id,
            'old_values' => $old ? $this->filterSensitive($old) : null,
            'new_values' => $new ? $this->filterSensitive($new) : null,
            'ip_address' => request()->ip(),
        ]);
    }

    private function filterSensitive(array $values): array
    {
        unset(
            $values['password'],
            $values['remember_token'],
            $values['two_factor_secret'],
            $values['two_factor_recovery_codes']
        );

        return $values;
    }
}

14. Testing Strategy

14.1 Test Organization

tests/
├── Feature/                    # Integration tests
│   ├── Admin/
│   │   ├── DashboardTest.php
│   │   ├── UserManagementTest.php
│   │   ├── ConsultationManagementTest.php
│   │   ├── TimelineManagementTest.php
│   │   ├── PostManagementTest.php
│   │   └── SettingsTest.php
│   ├── Client/
│   │   ├── DashboardTest.php
│   │   ├── BookingTest.php
│   │   ├── TimelineViewTest.php
│   │   └── ProfileTest.php
│   ├── Public/
│   │   ├── HomePageTest.php
│   │   ├── PostsTest.php
│   │   └── LanguageSwitchTest.php
│   ├── Auth/                   # Existing auth tests
│   ├── Email/
│   │   └── NotificationTest.php
│   └── Jobs/
│       └── QueueJobsTest.php
└── Unit/
    ├── Actions/
    │   ├── CreateConsultationActionTest.php
    │   └── ApproveConsultationActionTest.php
    ├── Models/
    │   ├── UserTest.php
    │   ├── ConsultationTest.php
    │   └── TimelineTest.php
    ├── Services/
    │   ├── AvailabilityServiceTest.php
    │   └── CalendarServiceTest.php
    └── Enums/
        └── ConsultationStatusTest.php

14.2 Test Examples

// tests/Feature/Client/BookingTest.php
<?php

use App\Models\User;
use App\Models\Consultation;
use App\Models\WorkingHour;
use App\Enums\ConsultationStatus;
use App\Jobs\SendBookingNotification;
use Livewire\Volt\Volt;
use Illuminate\Support\Facades\Queue;

beforeEach(function () {
    // Seed working hours
    WorkingHour::factory()->weekdays()->create();
});

test('client can book consultation on available date', function () {
    Queue::fake();

    $client = User::factory()->client()->create();
    $bookingDate = now()->addDays(3)->startOfDay();

    // Ensure it's a weekday
    while ($bookingDate->isWeekend()) {
        $bookingDate->addDay();
    }

    Volt::test('client.consultations.book')
        ->actingAs($client)
        ->set('form.booking_date', $bookingDate->format('Y-m-d'))
        ->set('form.booking_time', '10:00')
        ->set('form.problem_summary', 'I need legal advice regarding a contract dispute.')
        ->call('submit')
        ->assertHasNoErrors()
        ->assertDispatched('notify');

    expect(Consultation::where('user_id', $client->id)->exists())->toBeTrue();

    $consultation = Consultation::where('user_id', $client->id)->first();
    expect($consultation->status)->toBe(ConsultationStatus::Pending);
    expect($consultation->booking_date->format('Y-m-d'))->toBe($bookingDate->format('Y-m-d'));

    Queue::assertPushed(SendBookingNotification::class);
});

test('client cannot book more than once per day', function () {
    $client = User::factory()->client()->create();
    $bookingDate = now()->addDays(3);

    // Create existing booking
    Consultation::factory()->for($client)->create([
        'booking_date' => $bookingDate,
    ]);

    Volt::test('client.consultations.book')
        ->actingAs($client)
        ->set('form.booking_date', $bookingDate->format('Y-m-d'))
        ->set('form.booking_time', '14:00')
        ->set('form.problem_summary', 'Another consultation request.')
        ->call('submit')
        ->assertHasErrors(['form.booking_date']);

    expect(Consultation::where('user_id', $client->id)->count())->toBe(1);
});

test('client cannot book on blocked date', function () {
    $client = User::factory()->client()->create();
    $blockedDate = now()->addDays(5);

    BlockedTime::factory()->create([
        'block_date' => $blockedDate,
        'start_time' => null, // Full day block
        'end_time' => null,
    ]);

    Volt::test('client.consultations.book')
        ->actingAs($client)
        ->set('form.booking_date', $blockedDate->format('Y-m-d'))
        ->set('form.booking_time', '10:00')
        ->set('form.problem_summary', 'Test booking.')
        ->call('submit')
        ->assertHasErrors(['form.booking_date']);
});
// tests/Feature/Admin/UserManagementTest.php
<?php

use App\Models\User;
use App\Enums\UserType;
use App\Enums\UserStatus;
use App\Jobs\SendWelcomeEmail;
use Livewire\Volt\Volt;
use Illuminate\Support\Facades\Queue;

test('admin can create individual client', function () {
    Queue::fake();

    $admin = User::factory()->admin()->create();

    Volt::test('admin.users.create')
        ->actingAs($admin)
        ->set('form.user_type', 'individual')
        ->set('form.full_name', 'Ahmad Hassan')
        ->set('form.national_id', '123456789')
        ->set('form.email', 'ahmad@example.com')
        ->set('form.phone', '+970591234567')
        ->set('form.password', 'SecurePass123!')
        ->set('form.password_confirmation', 'SecurePass123!')
        ->set('form.preferred_language', 'ar')
        ->call('submit')
        ->assertHasNoErrors()
        ->assertRedirect(route('admin.users.index'));

    $user = User::where('email', 'ahmad@example.com')->first();

    expect($user)->not->toBeNull()
        ->and($user->user_type)->toBe(UserType::Individual)
        ->and($user->status)->toBe(UserStatus::Active);

    Queue::assertPushed(SendWelcomeEmail::class);
});

test('admin can deactivate client', function () {
    $admin = User::factory()->admin()->create();
    $client = User::factory()->client()->create();

    Volt::test('admin.users.edit', ['user' => $client])
        ->actingAs($admin)
        ->call('deactivate')
        ->assertHasNoErrors();

    $client->refresh();
    expect($client->status)->toBe(UserStatus::Deactivated);
});

test('admin cannot delete another admin', function () {
    $admin1 = User::factory()->admin()->create();
    $admin2 = User::factory()->admin()->create();

    Volt::test('admin.users.edit', ['user' => $admin2])
        ->actingAs($admin1)
        ->call('delete')
        ->assertForbidden();
});

14.3 Coverage Targets

Area Target Priority
Actions 95% High
Models (methods) 90% High
Policies 100% High
Services 90% High
Volt Components 80% Medium
Critical user flows 100% Critical

14.4 Running Tests

# Run all tests
php artisan test

# Run specific test file
php artisan test tests/Feature/Client/BookingTest.php

# Run tests matching filter
php artisan test --filter=booking

# Run with coverage
php artisan test --coverage --min=80

# Run parallel
php artisan test --parallel

15. Performance & Optimization

15.1 Performance Targets

Metric Target Measurement
Page Load (TTFB) < 200ms Server response time
Full Page Load < 3s Including assets
Database Queries < 20 per page Query monitoring
Memory Usage < 128MB Per request
Queue Job Processing < 5s For email jobs

15.2 Database Optimization

// Eager loading in components
public function mount(): void
{
    $this->consultations = Consultation::query()
        ->with('user:id,full_name,email,phone')
        ->where('status', ConsultationStatus::Pending)
        ->orderBy('created_at', 'desc')
        ->get();
}

// Chunking for large operations
User::query()
    ->where('status', UserStatus::Active)
    ->chunk(100, function ($users) {
        foreach ($users as $user) {
            // Process
        }
    });

15.3 Caching Strategy

Cache TTL Key Pattern Invalidation
Working Hours 1 hour working_hours:all On update
Blocked Times 1 hour blocked_times:{month} On update
Dashboard Stats 5 minutes admin:stats:{date} Time-based
Published Posts 10 minutes posts:published On publish
User Permissions 1 hour user:{id}:permissions On role change
// app/Services/AvailabilityService.php
public function getWorkingHours(): Collection
{
    return Cache::remember('working_hours:all', now()->addHour(), function () {
        return WorkingHour::where('is_active', true)->get();
    });
}

public function invalidateWorkingHoursCache(): void
{
    Cache::forget('working_hours:all');
}

15.4 Asset Optimization

// vite.config.js
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';

export default defineConfig({
    plugins: [
        laravel({
            input: ['resources/css/app.css', 'resources/js/app.js'],
            refresh: true,
        }),
    ],
    build: {
        rollupOptions: {
            output: {
                manualChunks: {
                    vendor: ['alpinejs'],
                },
            },
        },
    },
});

16. Deployment & CI/CD

16.1 GitHub Actions CI Pipeline

# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main, develop]

jobs:
  tests:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.4'
          extensions: mbstring, xml, ctype, iconv, intl, pdo_sqlite
          coverage: xdebug

      - name: Get composer cache directory
        id: composer-cache
        run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT

      - name: Cache composer dependencies
        uses: actions/cache@v4
        with:
          path: ${{ steps.composer-cache.outputs.dir }}
          key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
          restore-keys: ${{ runner.os }}-composer-

      - name: Install PHP dependencies
        run: composer install --no-interaction --prefer-dist --optimize-autoloader

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install NPM dependencies
        run: npm ci

      - name: Build assets
        run: npm run build

      - name: Prepare Laravel
        run: |
          cp .env.example .env
          php artisan key:generate
          touch database/database.sqlite          

      - name: Run Pint (code style)
        run: vendor/bin/pint --test

      - name: Run tests
        run: php artisan test --parallel --coverage --min=80
        env:
          DB_CONNECTION: sqlite
          DB_DATABASE: database/database.sqlite

  deploy-staging:
    needs: tests
    if: github.ref == 'refs/heads/develop'
    runs-on: ubuntu-latest
    steps:
      - name: Deploy to staging
        run: echo "Deploy to staging server"
        # Add actual deployment steps

  deploy-production:
    needs: tests
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment: production
    steps:
      - name: Deploy to production
        run: echo "Deploy to production server"
        # Add actual deployment steps

16.2 Deployment Script

#!/bin/bash
# deploy.sh - Production deployment script

set -e

echo "🚀 Starting deployment..."

# Variables
APP_DIR="/var/www/libra"
BACKUP_DIR="/var/www/backups"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)

# Create backup
echo "📦 Creating backup..."
mysqldump -u libra_user -p libra > "${BACKUP_DIR}/libra_${TIMESTAMP}.sql"

# Pull latest code
echo "📥 Pulling latest code..."
cd $APP_DIR
git fetch origin main
git reset --hard origin/main

# Install dependencies
echo "📚 Installing dependencies..."
composer install --no-dev --optimize-autoloader --no-interaction
npm ci
npm run build

# Run migrations
echo "🗃️ Running migrations..."
php artisan migrate --force

# Clear and rebuild caches
echo "🔄 Rebuilding caches..."
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache

# Restart queue workers
echo "👷 Restarting queue workers..."
php artisan queue:restart

# Set permissions
echo "🔒 Setting permissions..."
chown -R www-data:www-data storage bootstrap/cache
chmod -R 775 storage bootstrap/cache

echo "✅ Deployment complete!"

16.3 Environment Configuration

# Production .env
APP_NAME="Libra Law Firm"
APP_ENV=production
APP_KEY=base64:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
APP_DEBUG=false
APP_TIMEZONE=Asia/Gaza
APP_URL=https://libra.ps

LOG_CHANNEL=daily
LOG_LEVEL=warning

DB_CONNECTION=mariadb
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=libra
DB_USERNAME=libra_user
DB_PASSWORD=secure_password_here

BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local
QUEUE_CONNECTION=database
SESSION_DRIVER=database
SESSION_LIFETIME=120
SESSION_SECURE_COOKIE=true

MAIL_MAILER=smtp
MAIL_HOST=smtp.libra.ps
MAIL_PORT=587
MAIL_USERNAME=no-reply@libra.ps
MAIL_PASSWORD=secure_password_here
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS=no-reply@libra.ps
MAIL_FROM_NAME="Libra Law Firm"

16.4 Server Requirements

Component Requirement
OS Rocky Linux 9
PHP 8.4+ with extensions: BCMath, Ctype, Fileinfo, JSON, Mbstring, OpenSSL, PDO, PDO_MySQL, Tokenizer, XML, cURL
Database MariaDB 10.11+
Web Server Nginx 1.24+
Process Manager Supervisor (for queue workers)
Cron For Laravel scheduler
SSL Let's Encrypt or client-provided

17. Monitoring & Alerting

17.1 Logging Configuration

// config/logging.php
'channels' => [
    'stack' => [
        'driver' => 'stack',
        'channels' => ['daily', 'slack'],
        'ignore_exceptions' => false,
    ],

    'daily' => [
        'driver' => 'daily',
        'path' => storage_path('logs/laravel.log'),
        'level' => env('LOG_LEVEL', 'debug'),
        'days' => 14,
        'replace_placeholders' => true,
    ],

    'slack' => [
        'driver' => 'slack',
        'url' => env('LOG_SLACK_WEBHOOK_URL'),
        'username' => 'Libra Alert',
        'emoji' => ':boom:',
        'level' => 'error',
    ],

    'queue' => [
        'driver' => 'daily',
        'path' => storage_path('logs/queue.log'),
        'level' => 'info',
        'days' => 7,
    ],

    'audit' => [
        'driver' => 'daily',
        'path' => storage_path('logs/audit.log'),
        'level' => 'info',
        'days' => 90,
    ],
],

17.2 Alerting Thresholds

Alert Condition Channel Severity
Application Error Any exception Slack High
Failed Job 3 consecutive failures Slack, Email High
High Error Rate > 10 errors/minute Slack Critical
Queue Backup > 100 pending jobs Slack Medium
Disk Space < 10% free Email High
Database Connection Connection failure Slack, Email Critical

17.3 Health Check Endpoint

// routes/web.php
Route::get('/health', function () {
    $checks = [
        'app' => true,
        'database' => false,
        'cache' => false,
        'queue' => false,
    ];

    try {
        DB::connection()->getPdo();
        $checks['database'] = true;
    } catch (\Exception $e) {
        Log::error('Health check: Database connection failed', ['error' => $e->getMessage()]);
    }

    try {
        Cache::put('health-check', true, 10);
        $checks['cache'] = Cache::get('health-check') === true;
    } catch (\Exception $e) {
        Log::error('Health check: Cache failed', ['error' => $e->getMessage()]);
    }

    try {
        $pendingJobs = DB::table('jobs')->count();
        $checks['queue'] = $pendingJobs < 1000;
        $checks['queue_pending'] = $pendingJobs;
    } catch (\Exception $e) {
        Log::error('Health check: Queue check failed', ['error' => $e->getMessage()]);
    }

    $allHealthy = !in_array(false, $checks, true);

    return response()->json([
        'status' => $allHealthy ? 'healthy' : 'unhealthy',
        'timestamp' => now()->toIso8601String(),
        'checks' => $checks,
    ], $allHealthy ? 200 : 503);
})->name('health');

17.4 Application Metrics

// app/Console/Commands/CollectMetrics.php
class CollectMetrics extends Command
{
    protected $signature = 'metrics:collect';
    protected $description = 'Collect application metrics for monitoring';

    public function handle(): void
    {
        $metrics = [
            'users' => [
                'total' => User::count(),
                'active' => User::where('status', UserStatus::Active)->count(),
                'new_this_month' => User::where('created_at', '>=', now()->startOfMonth())->count(),
            ],
            'consultations' => [
                'pending' => Consultation::where('status', ConsultationStatus::Pending)->count(),
                'today' => Consultation::where('booking_date', today())->count(),
                'this_month' => Consultation::where('booking_date', '>=', now()->startOfMonth())->count(),
            ],
            'queue' => [
                'pending_jobs' => DB::table('jobs')->count(),
                'failed_jobs' => DB::table('failed_jobs')->count(),
            ],
            'system' => [
                'disk_free_percent' => round((disk_free_space('/') / disk_total_space('/')) * 100, 2),
            ],
        ];

        Log::channel('metrics')->info('Application metrics', $metrics);

        // Alert if thresholds exceeded
        if ($metrics['queue']['pending_jobs'] > 100) {
            Log::channel('slack')->warning('Queue backup detected', $metrics['queue']);
        }

        if ($metrics['system']['disk_free_percent'] < 10) {
            Log::channel('slack')->error('Low disk space', $metrics['system']);
        }
    }
}

// Schedule in routes/console.php
Schedule::command('metrics:collect')->everyFiveMinutes();

18. Error Handling & Graceful Degradation

18.1 Exception Handling

// bootstrap/app.php
->withExceptions(function (Exceptions $exceptions) {
    // Don't report certain exceptions
    $exceptions->dontReport([
        \Illuminate\Auth\AuthenticationException::class,
        \Illuminate\Validation\ValidationException::class,
    ]);

    // Custom rendering for specific exceptions
    $exceptions->render(function (NotFoundHttpException $e, Request $request) {
        if ($request->is('admin/*')) {
            return response()->view('errors.admin-404', [], 404);
        }
        if ($request->is('client/*')) {
            return response()->view('errors.client-404', [], 404);
        }
        return response()->view('errors.404', [], 404);
    });

    $exceptions->render(function (AuthorizationException $e, Request $request) {
        return response()->view('errors.403', [
            'message' => $e->getMessage(),
        ], 403);
    });

    // Report to external service
    $exceptions->reportable(function (\Throwable $e) {
        if (app()->bound('sentry')) {
            app('sentry')->captureException($e);
        }
    });
})

18.2 Graceful Degradation Strategy

Failure Impact Degradation Strategy
Email Service Down Notifications not sent Queue emails, retry with backoff, log failures, continue operation
Cache Unavailable Slower responses Fallback to database queries, log warning
Queue Worker Down Delayed processing Jobs remain in database, process when restored
Database Slow Slow page loads Show cached data where possible, timeout long queries

18.3 Email Failure Handling

// app/Jobs/SendBookingNotification.php
class SendBookingNotification implements ShouldQueue
{
    use Queueable;

    public int $tries = 3;
    public array $backoff = [60, 300, 900]; // 1min, 5min, 15min

    public function handle(): void
    {
        Mail::to(config('libra.admin_email'))
            ->send(new NewBookingRequestMail($this->consultation));
    }

    public function failed(\Throwable $exception): void
    {
        // Log the failure
        Log::error('Failed to send booking notification', [
            'consultation_id' => $this->consultation->id,
            'attempts' => $this->attempts(),
            'error' => $exception->getMessage(),
        ]);

        // Create in-app notification as fallback
        Notification::create([
            'user_id' => User::where('user_type', UserType::Admin)->first()?->id,
            'type' => 'booking_notification_failed',
            'data' => [
                'consultation_id' => $this->consultation->id,
                'message' => 'Email notification failed - please check manually',
            ],
        ]);
    }
}

18.4 Livewire Error Handling

// In Volt components
public function submit(): void
{
    try {
        $this->validate();

        $result = app(CreateConsultationAction::class)->execute(
            $this->user,
            $this->form->toArray()
        );

        $this->dispatch('notify', [
            'type' => 'success',
            'message' => __('messages.booking_submitted'),
        ]);

        $this->redirect(route('client.consultations.index'));

    } catch (ValidationException $e) {
        throw $e; // Let Livewire handle validation

    } catch (BookingLimitExceededException $e) {
        $this->addError('form.booking_date', $e->getMessage());

    } catch (\Exception $e) {
        report($e); // Log the error

        $this->dispatch('notify', [
            'type' => 'error',
            'message' => __('messages.general_error'),
        ]);
    }
}

19. Rollback & Recovery Procedures

19.1 Pre-Deployment Checklist

## Pre-Deployment Checklist

- [ ] Database backup completed
- [ ] Code backup/tag created
- [ ] All tests passing on CI
- [ ] Staging environment verified
- [ ] Rollback procedure reviewed
- [ ] Team notified of deployment window

19.2 Rollback Procedure

#!/bin/bash
# rollback.sh - Emergency rollback script

set -e

echo "🔄 Starting rollback..."

APP_DIR="/var/www/libra"
BACKUP_DIR="/var/www/backups"

# Get the previous commit or tag
PREVIOUS_VERSION=${1:-$(git describe --tags --abbrev=0 HEAD^)}

echo "Rolling back to: ${PREVIOUS_VERSION}"

# Step 1: Put application in maintenance mode
echo "🚧 Enabling maintenance mode..."
cd $APP_DIR
php artisan down --render="errors::503" --retry=60

# Step 2: Restore database (if needed)
read -p "Restore database backup? (y/n) " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
    echo "📦 Restoring database..."
    LATEST_BACKUP=$(ls -t ${BACKUP_DIR}/libra_*.sql | head -1)
    mysql -u libra_user -p libra < $LATEST_BACKUP
    echo "Database restored from: ${LATEST_BACKUP}"
fi

# Step 3: Rollback code
echo "⏪ Rolling back code..."
git fetch --all --tags
git checkout $PREVIOUS_VERSION

# Step 4: Install dependencies for that version
echo "📚 Installing dependencies..."
composer install --no-dev --optimize-autoloader --no-interaction
npm ci
npm run build

# Step 5: Run any rollback migrations (if applicable)
# Note: Be careful with this - may need manual intervention
# php artisan migrate:rollback --step=1

# Step 6: Clear caches
echo "🔄 Clearing caches..."
php artisan config:clear
php artisan route:clear
php artisan view:clear
php artisan cache:clear

# Step 7: Rebuild caches
echo "🔄 Rebuilding caches..."
php artisan config:cache
php artisan route:cache
php artisan view:cache

# Step 8: Restart queue workers
echo "👷 Restarting queue workers..."
php artisan queue:restart

# Step 9: Bring application back up
echo "✅ Disabling maintenance mode..."
php artisan up

echo "🎉 Rollback complete to version: ${PREVIOUS_VERSION}"
echo "⚠️  Please verify the application is working correctly!"

19.3 Database Migration Rollback

# Rollback last migration
php artisan migrate:rollback --step=1

# Rollback to specific batch
php artisan migrate:rollback --batch=5

# Check migration status
php artisan migrate:status

19.4 Recovery Scenarios

Scenario Recovery Steps
Failed Deployment 1. Enable maintenance mode, 2. Run rollback.sh, 3. Verify functionality
Database Corruption 1. Enable maintenance mode, 2. Restore from backup, 3. Clear caches, 4. Verify data
Queue Stuck 1. Clear failed jobs, 2. Restart workers, 3. Monitor processing
High Memory Usage 1. Restart PHP-FPM, 2. Check for memory leaks, 3. Review recent changes
SSL Certificate Expired 1. Renew certificate, 2. Restart Nginx, 3. Verify HTTPS

19.5 Backup Strategy

Backup Type Frequency Retention Storage
Database (full) Daily at 2 AM 30 days /var/www/backups/
Database (incremental) Every 6 hours 7 days /var/www/backups/
Application files Weekly 4 weeks Off-site
User uploads Daily 90 days Off-site
# /etc/cron.d/libra-backups
0 2 * * * root /var/www/libra/scripts/backup-database.sh >> /var/log/libra-backup.log 2>&1
0 */6 * * * root /var/www/libra/scripts/backup-incremental.sh >> /var/log/libra-backup.log 2>&1

20. Coding Standards

20.1 Critical Rules

Rule Description
Volt Pattern Use class-based Volt components (per CLAUDE.md)
Flux UI Use Flux components when available
Form Validation Always use Form Request or Livewire form objects
Eloquent Prefer Model::query() over DB:: facade
Actions Use single-purpose Action classes for business logic
Testing Every feature must have corresponding Pest tests
Pint Run vendor/bin/pint --dirty before committing

20.2 Naming Conventions

Element Convention Example
Models Singular PascalCase Consultation
Tables Plural snake_case consultations
Columns snake_case booking_date
Controllers PascalCase + Controller ConsultationController
Actions Verb + Noun + Action CreateConsultationAction
Jobs Verb + Noun SendBookingNotification
Events Past tense ConsultationApproved
Listeners Verb phrase SendApprovalNotification
Volt Components kebab-case path admin/consultations/index
Enums PascalCase ConsultationStatus
Enum Cases PascalCase NoShow
Traits Adjective or -able HasConsultations, Bookable
Interfaces Adjective or -able Exportable

20.3 Volt Component Template

<?php
// resources/views/livewire/admin/consultations/index.blade.php

use App\Models\Consultation;
use App\Enums\ConsultationStatus;
use Livewire\Volt\Component;
use Livewire\WithPagination;

new class extends Component {
    use WithPagination;

    public string $search = '';
    public string $status = '';

    public function updatedSearch(): void
    {
        $this->resetPage();
    }

    public function updatedStatus(): void
    {
        $this->resetPage();
    }

    public function with(): array
    {
        return [
            'consultations' => Consultation::query()
                ->with('user:id,full_name,email')
                ->when($this->search, fn ($q) => $q->whereHas('user', fn ($q) =>
                    $q->where('full_name', 'like', "%{$this->search}%")
                ))
                ->when($this->status, fn ($q) => $q->where('status', $this->status))
                ->orderBy('booking_date', 'desc')
                ->paginate(15),
            'statuses' => ConsultationStatus::cases(),
        ];
    }
}; ?>

<div>
    <x-slot name="header">
        <flux:heading size="xl">{{ __('models.consultations') }}</flux:heading>
    </x-slot>

    <div class="flex gap-4 mb-6">
        <flux:input
            wire:model.live.debounce.300ms="search"
            placeholder="{{ __('messages.search') }}"
            icon="magnifying-glass"
        />

        <flux:select wire:model.live="status">
            <option value="">{{ __('messages.all_statuses') }}</option>
            @foreach($statuses as $s)
                <option value="{{ $s->value }}">{{ $s->label() }}</option>
            @endforeach
        </flux:select>
    </div>

    <div class="space-y-4">
        @forelse($consultations as $consultation)
            <x-ui.card wire:key="consultation-{{ $consultation->id }}">
                {{-- Card content --}}
            </x-ui.card>
        @empty
            <x-ui.empty-state :message="__('messages.no_consultations')" />
        @endforelse
    </div>

    <div class="mt-6">
        {{ $consultations->links() }}
    </div>
</div>

20.4 Action Class Template

<?php
// app/Actions/Consultation/CreateConsultationAction.php

namespace App\Actions\Consultation;

use App\Models\Consultation;
use App\Models\User;
use App\Enums\ConsultationStatus;
use App\Enums\PaymentStatus;
use App\Jobs\SendBookingNotification;
use App\Exceptions\BookingLimitExceededException;
use Illuminate\Support\Facades\DB;

class CreateConsultationAction
{
    public function execute(User $user, array $data): Consultation
    {
        // Validate booking limit
        if ($user->hasBookingOnDate($data['booking_date'])) {
            throw new BookingLimitExceededException(
                __('messages.booking_limit_exceeded')
            );
        }

        return DB::transaction(function () use ($user, $data) {
            $consultation = Consultation::create([
                'user_id' => $user->id,
                'booking_date' => $data['booking_date'],
                'booking_time' => $data['booking_time'],
                'problem_summary' => $data['problem_summary'],
                'status' => ConsultationStatus::Pending,
                'payment_status' => PaymentStatus::NotApplicable,
            ]);

            SendBookingNotification::dispatch($consultation);

            return $consultation;
        });
    }
}

21. Technology Alternatives Considered

21.1 Frontend Framework

Option Pros Cons Decision
Livewire 3 + Volt No JS build, Laravel native, simple state management Learning curve, larger payload SELECTED
React/Vue SPA Rich ecosystem, familiar to many Separate build, API needed, complexity Rejected
Blade + Alpine only Simplest, no Livewire overhead Limited interactivity Considered for simpler features
Inertia.js Best of both worlds Additional complexity, less documentation Rejected

Rationale: Livewire 3 provides excellent developer experience with Laravel, eliminates API development overhead, and Volt single-file components align well with the project's maintainability goals.

21.2 Database

Option Pros Cons Decision
MariaDB MySQL compatible, open source, client preference - SELECTED (Production)
SQLite Zero config, fast tests, portable Not suitable for production SELECTED (Development)
PostgreSQL Advanced features, JSON support Overkill for this project Rejected
MySQL Industry standard MariaDB preferred by client Rejected

21.3 Authentication

Option Pros Cons Decision
Laravel Fortify Headless, flexible, 2FA support Requires custom views SELECTED
Laravel Breeze Quick setup, includes views Less flexible, opinionated Rejected
Laravel Jetstream Full-featured, team support Too complex, Livewire/Inertia choice Rejected
Custom auth Full control Reinventing wheel, security risks Rejected

21.4 Queue System

Option Pros Cons Decision
Database Simple, no extra services Slower than Redis SELECTED
Redis Fast, feature-rich Additional service to maintain Future consideration
Amazon SQS Managed, scalable Overkill, cost, lock-in Rejected
Beanstalkd Lightweight, fast Less Laravel integration Rejected

Rationale: Database queue driver is sufficient for expected volume (~100 jobs/day). Redis can be added later if performance requires.

21.5 CSS Framework

Option Pros Cons Decision
Tailwind CSS 4 Utility-first, RTL support, Flux UI compatible Learning curve SELECTED
Bootstrap 5 Familiar, RTL plugin Larger bundle, less flexible PRD mentioned but Tailwind preferred
Custom CSS Full control Time-consuming, maintenance burden Rejected

22. Appendices

A. Flux UI Free Components

Available components in Flux UI Free edition:

avatar, badge, brand, breadcrumbs, button, callout, checkbox, dropdown,
field, heading, icon, input, modal, navbar, otp-input, profile, radio,
select, separator, skeleton, switch, text, textarea, tooltip

B. Color Palette

Name Hex CSS Variable Usage
Dark Navy Blue #0A1F44 --color-navy Primary background
Gold #D4AF37 --color-gold Accents, buttons
Light Gold #F4E4B8 --color-gold-light Hover states
Off-White #F9F7F4 --color-cream Text on dark, cards
Charcoal Gray #2C3E50 --color-charcoal Secondary text
Success Green #27AE60 --color-success Success states
Warning Red #E74C3C --color-danger Error states
Pending Yellow #F39C12 --color-warning Pending states

C. Consultation Duration Constants

// config/libra.php
return [
    'consultation' => [
        'duration_minutes' => 45,
        'buffer_minutes' => 15,
        'slot_minutes' => 60, // duration + buffer
    ],
];

D. External Resources

E. Contact Information

  • Admin Email: (configured in settings)
  • System Email: no-reply@libra.ps
  • Domain: libra.ps

END OF DOCUMENT