diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..b6fadf5 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,3156 @@ +# 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 +│ └── ExportService.php +├── config/ +│ └── libra.php # App-specific config +├── database/ +│ ├── factories/ +│ │ ├── UserFactory.php +│ │ ├── ConsultationFactory.php +│ │ ├── TimelineFactory.php +│ │ ├── TimelineUpdateFactory.php +│ │ └── PostFactory.php +│ ├── migrations/ +│ └── seeders/ +│ ├── DatabaseSeeder.php +│ ├── AdminSeeder.php +│ ├── WorkingHoursSeeder.php +│ └── DemoDataSeeder.php +├── docs/ +│ ├── architecture.md # This document +│ ├── prd.md # Product requirements +│ ├── epics/ # Epic definitions +│ └── stories/ # User stories +├── resources/ +│ ├── views/ +│ │ ├── components/ # Blade components +│ │ │ ├── layouts/ +│ │ │ │ ├── app.blade.php # Main app layout +│ │ │ │ ├── guest.blade.php # Guest/public layout +│ │ │ │ └── admin.blade.php # Admin layout +│ │ │ └── ui/ # Reusable UI components +│ │ │ ├── card.blade.php +│ │ │ ├── stat-card.blade.php +│ │ │ ├── status-badge.blade.php +│ │ │ └── language-switcher.blade.php +│ │ ├── emails/ # Email templates +│ │ │ ├── welcome.blade.php +│ │ │ ├── booking-submitted.blade.php +│ │ │ ├── booking-approved.blade.php +│ │ │ ├── booking-rejected.blade.php +│ │ │ ├── consultation-reminder.blade.php +│ │ │ ├── timeline-updated.blade.php +│ │ │ └── new-booking-request.blade.php +│ │ ├── livewire/ # Volt single-file components +│ │ │ ├── admin/ # Admin area +│ │ │ │ ├── dashboard.blade.php +│ │ │ │ ├── users/ +│ │ │ │ │ ├── index.blade.php +│ │ │ │ │ ├── create.blade.php +│ │ │ │ │ └── edit.blade.php +│ │ │ │ ├── consultations/ +│ │ │ │ │ ├── index.blade.php +│ │ │ │ │ ├── pending.blade.php +│ │ │ │ │ ├── calendar.blade.php +│ │ │ │ │ └── show.blade.php +│ │ │ │ ├── timelines/ +│ │ │ │ │ ├── index.blade.php +│ │ │ │ │ ├── create.blade.php +│ │ │ │ │ └── show.blade.php +│ │ │ │ ├── posts/ +│ │ │ │ │ ├── index.blade.php +│ │ │ │ │ ├── create.blade.php +│ │ │ │ │ └── edit.blade.php +│ │ │ │ ├── settings/ +│ │ │ │ │ ├── working-hours.blade.php +│ │ │ │ │ ├── blocked-times.blade.php +│ │ │ │ │ └── content-pages.blade.php +│ │ │ │ └── reports/ +│ │ │ │ └── index.blade.php +│ │ │ ├── client/ # Client area +│ │ │ │ ├── dashboard.blade.php +│ │ │ │ ├── consultations/ +│ │ │ │ │ ├── index.blade.php +│ │ │ │ │ └── book.blade.php +│ │ │ │ ├── timelines/ +│ │ │ │ │ ├── index.blade.php +│ │ │ │ │ └── show.blade.php +│ │ │ │ └── profile.blade.php +│ │ │ ├── pages/ # Public pages +│ │ │ │ ├── home.blade.php +│ │ │ │ ├── posts/ +│ │ │ │ │ ├── index.blade.php +│ │ │ │ │ └── show.blade.php +│ │ │ │ ├── terms.blade.php +│ │ │ │ └── privacy.blade.php +│ │ │ ├── auth/ # Auth pages (existing) +│ │ │ └── settings/ # User settings (existing) +│ │ ├── pdf/ # PDF templates +│ │ │ ├── users-export.blade.php +│ │ │ ├── consultations-export.blade.php +│ │ │ └── monthly-report.blade.php +│ │ └── errors/ # Error pages +│ │ ├── 404.blade.php +│ │ ├── 403.blade.php +│ │ └── 500.blade.php +│ ├── css/ +│ │ └── app.css # Tailwind entry point +│ ├── js/ +│ │ └── app.js # Minimal JS +│ └── lang/ # Translations +│ ├── ar/ +│ │ ├── auth.php +│ │ ├── validation.php +│ │ ├── pagination.php +│ │ ├── passwords.php +│ │ ├── messages.php +│ │ ├── models.php +│ │ ├── enums.php +│ │ └── emails.php +│ └── en/ +│ └── ... (same structure) +├── routes/ +│ ├── web.php # Web routes +│ └── console.php # Scheduled commands +├── tests/ +│ ├── Feature/ +│ │ ├── Admin/ +│ │ │ ├── DashboardTest.php +│ │ │ ├── UserManagementTest.php +│ │ │ ├── ConsultationManagementTest.php +│ │ │ ├── TimelineManagementTest.php +│ │ │ ├── PostManagementTest.php +│ │ │ └── SettingsTest.php +│ │ ├── Client/ +│ │ │ ├── DashboardTest.php +│ │ │ ├── BookingTest.php +│ │ │ ├── TimelineViewTest.php +│ │ │ └── ProfileTest.php +│ │ ├── Public/ +│ │ │ ├── HomePageTest.php +│ │ │ ├── PostsTest.php +│ │ │ └── LanguageSwitchTest.php +│ │ └── Auth/ +│ │ └── ... (existing tests) +│ └── Unit/ +│ ├── Actions/ +│ ├── Models/ +│ ├── Services/ +│ └── Enums/ +├── storage/ +│ └── app/ +│ └── exports/ # Generated export files +├── .github/ +│ └── workflows/ +│ └── ci.yml # GitHub Actions CI +└── public/ + └── build/ # Compiled assets +``` + +--- + +## 7. Authentication & Authorization + +### 7.1 Authentication (Fortify) + +**Current Configuration:** +- Email/password login +- Email verification +- Two-factor authentication (TOTP) +- Password reset + +**Modifications Required:** +1. **Disable public registration** - Admin creates all users +2. **Custom login redirect** - Admin → `/admin/dashboard`, Client → `/client/dashboard` +3. **Admin-triggered password reset** - No public reset + +```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 */ + --color-navy: #0A1F44; + --color-gold: #D4AF37; + --color-gold-light: #F4E4B8; + --color-cream: #F9F7F4; +} + +/* RTL utilities */ +[dir="rtl"] .rtl\:space-x-reverse > :not([hidden]) ~ :not([hidden]) { + --tw-space-x-reverse: 1; +} + +.font-arabic { + font-family: var(--font-arabic); +} + +.font-english { + font-family: var(--font-english); +} +``` + +--- + +## 13. Security + +### 13.1 Security Measures + +| Measure | Implementation | Configuration | +|---------|----------------|---------------| +| **CSRF Protection** | Laravel default | All POST/PUT/DELETE forms | +| **XSS Prevention** | Blade escaping `{{ }}` | Default for all output | +| **SQL Injection** | Eloquent ORM | Parameterized queries | +| **Password Hashing** | bcrypt | Laravel default | +| **Session Security** | Secure cookies | `SESSION_SECURE_COOKIE=true` | +| **Rate Limiting** | Throttle middleware | 5 login attempts/minute | +| **Input Validation** | Form Requests | All user input | +| **HTTPS** | Forced in production | `APP_URL=https://` | +| **CSP Headers** | Middleware | Restrictive policy | + +### 13.2 Rate Limiting + +```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 +│ │ ├── PostsTest.php +│ │ └── LanguageSwitchTest.php +│ ├── Auth/ # Existing auth tests +│ ├── Email/ +│ │ └── NotificationTest.php +│ └── Jobs/ +│ └── QueueJobsTest.php +└── Unit/ + ├── Actions/ + │ ├── CreateConsultationActionTest.php + │ └── ApproveConsultationActionTest.php + ├── Models/ + │ ├── UserTest.php + │ ├── ConsultationTest.php + │ └── TimelineTest.php + ├── Services/ + │ ├── AvailabilityServiceTest.php + │ └── CalendarServiceTest.php + └── Enums/ + └── ConsultationStatusTest.php +``` + +### 14.2 Test Examples + +```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 + +| Name | Hex | CSS Variable | Usage | +|------|-----|--------------|-------| +| Dark Navy Blue | `#0A1F44` | `--color-navy` | Primary background | +| Gold | `#D4AF37` | `--color-gold` | Accents, buttons | +| Light Gold | `#F4E4B8` | `--color-gold-light` | Hover states | +| Off-White | `#F9F7F4` | `--color-cream` | Text on dark, cards | +| Charcoal Gray | `#2C3E50` | `--color-charcoal` | Secondary text | +| Success Green | `#27AE60` | `--color-success` | Success states | +| Warning Red | `#E74C3C` | `--color-danger` | Error states | +| Pending Yellow | `#F39C12` | `--color-warning` | Pending states | + +### C. Consultation Duration Constants + +```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**