# 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](#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 ``` --- ## 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 10 */ --color-charcoal: #4A4A42; --color-warm-gray: #C9C4BA; --color-off-white: #E8E4DC; --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 10 (Brand Color Refresh). > See `docs/brand.md` for complete brand guidelines. | Name | Hex | CSS Variable | Usage | |------|-----|--------------|-------| | Charcoal | `#4A4A42` | `--color-charcoal` | Primary brand, buttons | | Warm Gray | `#C9C4BA` | `--color-warm-gray` | Accents, borders | | Off-White | `#E8E4DC` | `--color-off-white` | Backgrounds, cards | | 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**