# Libra Law Firm - Architecture Document **Version:** 1.2 **Date:** October 31, 2026 **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) | | Oct 31, 2026 | 1.2 | Added §6.2 Public-site presentation layer (PRD 1.4 redesign, ux-design.md v2.0 Part A) | Jarvis | --- ## Table of Contents 1. [Introduction](#1-introduction) 2. [High Level Architecture](#2-high-level-architecture) 3. [Technology Stack](#3-technology-stack) 4. [Data Models](#4-data-models) 5. [Database Schema](#5-database-schema) 6. [Application Structure](#6-application-structure) 7. [Authentication & Authorization](#7-authentication--authorization) 8. [Core Workflows](#8-core-workflows) 9. [Routing Structure](#9-routing-structure) 10. [Email System](#10-email-system) 11. [Background Jobs & Scheduling](#11-background-jobs--scheduling) 12. [Localization & Timezone](#12-localization--timezone) 13. [Security](#13-security) 14. [Testing Strategy](#14-testing-strategy) 15. [Performance & Optimization](#15-performance--optimization) 16. [Deployment & CI/CD](#16-deployment--cicd) 17. [Monitoring & Alerting](#17-monitoring--alerting) 18. [Error Handling & Graceful Degradation](#18-error-handling--graceful-degradation) 19. [Rollback & Recovery Procedures](#19-rollback--recovery-procedures) 20. [Coding Standards](#20-coding-standards) 21. [Technology Alternatives Considered](#21-technology-alternatives-considered) 22. [Appendices](#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 ```mermaid graph TB subgraph "Users" Admin[Admin/Lawyer] Client[Clients] Public[Public Visitors] end subgraph "Libra Platform" App[Laravel Application
libra.ps] end subgraph "External Services" SMTP[SMTP Server
no-reply@libra.ps] end subgraph "Infrastructure" Server[Rocky Linux 9
Client-Managed] end Admin -->|Manage users, bookings,
timelines, posts| App Client -->|Book consultations,
view timelines| App Public -->|View posts,
firm info| App App -->|Send emails| SMTP App -->|Deployed on| Server ``` ### 2.3 Container Diagram ```mermaid graph TB subgraph "Client Devices" Browser[Web Browser] Mobile[Mobile Browser] end subgraph "Web Server - Nginx" Static[Static Assets
CSS/JS/Images] PHP[PHP-FPM 8.4] end subgraph "Laravel Application" Router[Route Handler] MW[Middleware Stack
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
Supervisor] Scheduler[Task Scheduler
Cron] end subgraph "Data Storage" MariaDB[(MariaDB
Production)] SQLite[(SQLite
Development)] FileStorage[Local Storage
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 ```mermaid 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 ```php // 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 ```php // 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 ```php // 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 ```php // 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 ```php // 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 ```php // 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 ```php // 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 ```php // 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 │ ├── CaptchaService.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 │ │ │ ├── guest-booking-submitted.blade.php │ │ │ ├── guest-booking-approved.blade.php │ │ │ └── guest-booking-rejected.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 │ │ │ │ ├── booking.blade.php # Guest booking form │ │ │ │ ├── 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 │ │ │ ├── GuestBookingTest.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 ``` > §6.1 is the original (Dec 2025) plan. As built, the public layout is > `resources/views/components/layouts/public.blade.php` (not `guest.blade.php`) and public pages > are Volt components under `resources/views/livewire/pages/`. §6.2 is current. ### 6.2 Public-Site Presentation Layer (redesign, PRD 1.4) **Overview.** The October 2026 redesign (PRD Update 1.4; `docs/ux-design.md` Part A) is a presentation-only change to the public pages. No route, model, migration, service, mail or job changes. The redesign is isolated from the admin and client dashboards by loading its stylesheet and script only from the public and auth layouts, and by scoping every rule under a body class. **Components** | Component | Path | Owns | |---|---|---| | Public stylesheet | `resources/css/public.css` (new Vite entry) | Part A tokens (`--forest`, `--gold`, …), linen ground, type, buttons, sections, cards, docket rows, booking card, footer, reveal and reduced-motion rules. Every selector is scoped under `body.site`; auth-page rules under `body.site-auth`. | | Public script | `resources/js/public.js` (new Vite entry) imports `resources/js/site/balance.js`, `reveal.js`, `progress.js` | The balance SVG builder and damped spring, hero pointer response, finale scroll mapping, IntersectionObserver reveal, reading-progress rule, nav scrolled state. Initialises on `livewire:navigated` (fires on first load and after every `wire:navigate`) and tears down listeners on `livewire:navigating`. | | Font partial | `resources/views/partials/site-fonts.blade.php` (new) | Google Fonts link for Markazi Text (400–700) and IBM Plex Sans Arabic (300–600), `display=swap`. | | Public layout | `resources/views/components/layouts/public.blade.php` (changed) | Body class `site`; includes the font partial and `@vite(['resources/css/public.css','resources/js/public.js'])` after `partials.head`; drops the 1200px content wrapper and the 64px fixed-nav spacer (nav becomes sticky); adds the progress rule element. | | Auth layouts | `resources/views/components/layouts/auth/split.blade.php`, `card.blade.php`, `simple.blade.php` (changed) | Body class `site-auth`; font partial; `public.css` only (no public script). Composition unchanged. | | Navigation / footer | `resources/views/components/navigation.blade.php`, `footer.blade.php` (restyled in place; used only by the public layout) | Part A nav and footer. Keeps the existing Alpine mobile menu, auth-aware links and language toggle. | | Site Blade components | `resources/views/components/site/` (new): `balance.blade.php` (SVG mount point; props `id`, `size`, `mode` = `hero` / `finale` / `static`), `section-head.blade.php` (article label + h2 + lede), `post-card.blade.php` (text-only post card) | Shared markup for the public pages. | | Redesign copy | `lang/ar/site.php`, `lang/en/site.php` (new) | Strings introduced by the redesign only (hero line, trust strip, pillars, article labels, card titles, booking lede, nav and footer labels). Existing keys in `home.php`, `about.php`, `booking.php`, `footer.php`, `navigation.php` are reused, never duplicated or edited. `navigation.php` keys used by the dashboards are untouched. | | Photography | `public/images/site/olive-tree.jpg`, `embroidery-hoop.jpg`, `olives.jpg`, `hills.jpg` (new; extracted from the data URIs in `docs/ux-mockup.html`) | Production images (Unsplash licence). Portrait stays `public/images/huda-armouche.jpg`; seal uses `public/images/logo.png`. | | Guest booking | `resources/views/livewire/pages/booking.blade.php` (changed) | Gains `public bool $compact = false` (mount parameter). `compact` renders the one-screen home layout; default renders the `/booking` page layout. Validation, captcha, one-per-day rule, confirm step and redirect to `booking.success` are shared and unchanged. | | Availability calendar | `resources/views/livewire/availability-calendar.blade.php` (changed) | Gains `public string $variant = 'month'`. `strip` lists the next 14 dates whose `AvailabilityService::getDateStatus()` is available (scanning at most 60 days ahead) and their `getAvailableSlots()`; `month` is the existing grid. Default stays `month`, so the client dashboard's booking page (`resources/views/livewire/client/consultations/book.blade.php`) is unaffected; its public restyle applies only under `body.site`. | **Data flows** 1. Request to a public route → Volt page with `#[Layout('components.layouts.public')]` → layout loads `app.css` + `app.js` (unchanged, via `partials.head`), then `public.css` + `public.js`. 2. Home `/` → `Post::published()->latest('published_at')->take(3)` (existing) → `x-site.post-card`; embeds ``, which embeds ``. Slot choice → `$parent.selectSlot(date, time)` (existing contract) → submit → existing confirm step → existing `submit()` → redirect `/booking/success`. 3. `public.js` on `livewire:navigated` → finds `[data-balance]` mounts → builds the SVG → binds pointer (hero, fine pointers only) and scroll (finale) → spring per `requestAnimationFrame`. `prefers-reduced-motion: reduce` → every balance drawn level, no listeners bound. 4. Language: unchanged — `/language/{locale}` sets the session locale; layouts set `lang` and `dir`. **Module boundaries** - The redesign owns `public.css`, `public.js`, `resources/js/site/*`, `components/site/*`, `lang/*/site.php`, `public/images/site/*`, and the restyle of the public views listed above. - It does not own and must not change: `resources/css/app.css` `@theme` tokens and `.btn-*` classes, Flux configuration, anything under `resources/views/livewire/admin/` or `livewire/client/`, `components/layouts/app*`, emails, PDFs, `resources/views/livewire/pages/forms/nablus-workshop.blade.php`, `layouts/standalone.blade.php`, routes, models, services (read-only use of `AvailabilityService`), migrations. - Pages restyled: `livewire/pages/home.blade.php`, `about.blade.php`, `booking.blade.php`, `booking-success.blade.php`, `posts/index.blade.php`, `posts/show.blade.php`, `resources/views/pages/legal.blade.php` (and the `terms`/`privacy` wrappers if they render markup). - `partials/head.blade.php`: `theme-color` corrected to `#2D3624` (was the superseded `#4A4A42`); nothing else changes there, because the dashboards share it. **Open gaps** — none blocking. The four rulings (photography, 14-day strip, unmocked pages, auth scope) are recorded in `docs/ux-design.md` Part A *Open Questions* and the project ledger. --- ## 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 ```php // 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) ```php // 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 ```php // 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 ```php // 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 ```mermaid 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 ```mermaid 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 ```mermaid 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 ```mermaid 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 ```php // 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 ```php // 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 ```php // 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) ```blade {{-- resources/views/emails/booking-approved.blade.php --}} @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 {{ $locale === 'ar' ? 'عرض موعدي' : 'View My Consultation' }} {{ $locale === 'ar' ? 'مع تحيات،' : 'Best regards,' }}
{{ config('app.name') }}
``` --- ## 11. Background Jobs & Scheduling ### 11.1 Queue Configuration ```php // 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 ```php // 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 ```php // 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 ```ini ; /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) ```php // 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 ```php // 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 ```blade {{-- resources/views/components/layouts/app.blade.php --}} {{ $title ?? config('app.name') }} {{-- Fonts --}} @vite(['resources/css/app.css', 'resources/js/app.js']) ``` ```css /* resources/css/app.css */ @import "tailwindcss"; @theme { --font-arabic: 'Cairo', sans-serif; --font-english: 'Montserrat', sans-serif; /* Brand colors - Updated in Epic 12 */ --color-dark-forest: #2D3624; --color-warm-gold: #A68966; --color-warm-cream: #F4F1EA; --color-forest-green: #2D322A; --color-pure-white: #FFFFFF; --color-olive-green: #4A5D23; --color-primary-light: #3D4634; --color-gold-hover: #8A7555; --color-gold-light: #C4A882; --color-deep-black: #1A1A1A; } /* 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 ```php // 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 ```php // 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 ```php // 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 ```php // 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 │ │ ├── GuestBookingTest.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 │ └── CaptchaServiceTest.php └── Enums/ └── ConsultationStatusTest.php ``` ### 14.2 Test Examples ```php // tests/Feature/Client/BookingTest.php 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']); }); ``` ```php // tests/Feature/Admin/UserManagementTest.php 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 ```bash # 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 ```php // 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 | ```php // 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 ```js // 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 ```yaml # .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 ```bash #!/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 ```bash # 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 ```php // 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 ```php // 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 ```php // 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 ```php // 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 ```php // 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 ```php // 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 ```markdown ## 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 ```bash #!/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 ```bash # 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 | ```bash # /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 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(), ]; } }; ?>
{{ __('models.consultations') }}
@foreach($statuses as $s) @endforeach
@forelse($consultations as $consultation) {{-- Card content --}} @empty @endforelse
{{ $consultations->links() }}
``` ### 20.4 Action Class Template ```php 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 > **Note:** Color palette updated in Epic 12 (Dark Charcoal & Warm Gold refresh). > See `docs/brand.md` for complete brand guidelines. | Name | Hex | CSS Variable | Usage | |------|-----|--------------|-------| | Dark Forest Green | `#2D3624` | `--color-dark-forest` | Header/Footer backgrounds | | Warm Gold | `#A68966` | `--color-warm-gold` | CTA buttons, links, accents | | Warm Cream | `#F4F1EA` | `--color-warm-cream` | Main page background | | Forest Green | `#2D322A` | `--color-forest-green` | Headings, body text | | Pure White | `#FFFFFF` | `--color-pure-white` | Card backgrounds | | Olive Green | `#4A5D23` | `--color-olive-green` | Active states | | Primary Light | `#3D4634` | `--color-primary-light` | Primary hover states | | Gold Hover | `#8A7555` | `--color-gold-hover` | Button hover states | | Gold Light | `#C4A882` | `--color-gold-light` | Light accents | | Deep Black | `#1A1A1A` | `--color-deep-black` | Text, footer | | 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 ```php // config/libra.php return [ 'consultation' => [ 'duration_minutes' => 45, 'buffer_minutes' => 15, 'slot_minutes' => 60, // duration + buffer ], ]; ``` ### D. External Resources - [Laravel 12 Documentation](https://laravel.com/docs/12.x) - [Livewire 3 Documentation](https://livewire.laravel.com/docs) - [Volt Documentation](https://livewire.laravel.com/docs/volt) - [Flux UI Documentation](https://fluxui.dev/docs) - [Tailwind CSS 4 Documentation](https://tailwindcss.com/docs) - [Pest Testing Documentation](https://pestphp.com/docs) - [Laravel Fortify Documentation](https://laravel.com/docs/12.x/fortify) ### E. Contact Information - **Admin Email:** (configured in settings) - **System Email:** no-reply@libra.ps - **Domain:** libra.ps --- **END OF DOCUMENT**