libra/docs/architecture.md

3179 lines
94 KiB
Markdown

# 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<br/>libra.ps]
end
subgraph "External Services"
SMTP[SMTP Server<br/>no-reply@libra.ps]
end
subgraph "Infrastructure"
Server[Rocky Linux 9<br/>Client-Managed]
end
Admin -->|Manage users, bookings,<br/>timelines, posts| App
Client -->|Book consultations,<br/>view timelines| App
Public -->|View posts,<br/>firm info| App
App -->|Send emails| SMTP
App -->|Deployed on| Server
```
### 2.3 Container Diagram
```mermaid
graph TB
subgraph "Client Devices"
Browser[Web Browser]
Mobile[Mobile Browser]
end
subgraph "Web Server - Nginx"
Static[Static Assets<br/>CSS/JS/Images]
PHP[PHP-FPM 8.4]
end
subgraph "Laravel Application"
Router[Route Handler]
MW[Middleware Stack<br/>Auth, Locale, CSRF]
subgraph "Presentation Layer"
Volt[Volt Components]
Flux[Flux UI Components]
Blade[Blade Templates]
end
subgraph "Business Logic Layer"
Actions[Action Classes]
Fortify[Fortify Auth]
Policies[Authorization Policies]
end
subgraph "Data Access Layer"
Models[Eloquent Models]
Observers[Model Observers]
end
end
subgraph "Background Processing"
Queue[Queue Worker<br/>Supervisor]
Scheduler[Task Scheduler<br/>Cron]
end
subgraph "Data Storage"
MariaDB[(MariaDB<br/>Production)]
SQLite[(SQLite<br/>Development)]
FileStorage[Local Storage<br/>Exports/Logs]
end
subgraph "External"
SMTP[SMTP Server]
end
Browser --> Static
Browser --> PHP
Mobile --> Static
Mobile --> PHP
PHP --> Router
Router --> MW
MW --> Volt
MW --> Blade
Volt --> Flux
Volt --> Actions
Actions --> Fortify
Actions --> Policies
Actions --> Models
Models --> Observers
Models --> MariaDB
Models --> SQLite
Queue --> Actions
Queue --> SMTP
Scheduler --> Queue
Actions --> FileStorage
```
### 2.4 Architectural Patterns
| Pattern | Description | Rationale |
|---------|-------------|-----------|
| **MVC + Livewire** | Laravel MVC with Livewire reactive components | Standard Laravel pattern; eliminates need for separate SPA |
| **Action Classes** | Single-purpose classes for business logic | Keeps controllers thin; reusable; testable |
| **Form Objects** | Livewire form objects for validation | Encapsulates validation; reusable forms |
| **Observer Pattern** | Eloquent observers for model events | Automatic audit logging, notification triggers |
| **Queue-based Processing** | Background jobs for emails and heavy operations | Non-blocking UX; reliable delivery |
| **Policy-based Authorization** | Laravel Policies for access control | Clean separation of authorization logic |
| **Repository Pattern** | Optional for complex queries | Only if query complexity warrants |
---
## 3. Technology Stack
### 3.1 Core Technologies
| Category | Technology | Version | Purpose | Rationale |
|----------|------------|---------|---------|-----------|
| **Runtime** | PHP | 8.4.x | Server-side language | Latest stable; required by Laravel 12 |
| **Framework** | Laravel | 12.x | Application framework | Industry standard; excellent ecosystem |
| **Reactive UI** | Livewire | 3.7.x | Interactive components | No JS build complexity; server-state |
| **Components** | Volt | 1.10.x | Single-file components | Cleaner organization; class-based |
| **UI Library** | Flux UI Free | 2.10.x | Pre-built components | Consistent design; accessibility |
| **CSS** | Tailwind CSS | 4.x | Utility-first styling | Rapid development; RTL support |
| **Auth** | Laravel Fortify | 1.33.x | Headless auth | Flexible; 2FA support |
| **Database (Prod)** | MariaDB | 10.11+ | Production database | MySQL-compatible; performant |
| **Database (Dev)** | SQLite | Latest | Development database | Zero config; fast tests |
| **Testing** | Pest | 4.x | Testing framework | Elegant syntax; Laravel integration |
| **Code Style** | Laravel Pint | 1.x | Code formatting | Consistent style |
| **Queue** | Database Driver | - | Job processing | Simple; no Redis dependency |
### 3.2 Frontend Dependencies
| Package | Version | Purpose |
|---------|---------|---------|
| Alpine.js | (via Livewire) | Minimal JS interactivity |
| Vite | 6.x | Asset bundling |
| Google Fonts | - | Cairo (Arabic), Montserrat (English) |
| Heroicons | (via Flux) | Icon library |
### 3.3 Backend Dependencies (To Add)
| Package | Version | Purpose |
|---------|---------|---------|
| `barryvdh/laravel-dompdf` | ^3.0 | PDF export generation |
| `league/csv` | ^9.0 | CSV export generation |
| `spatie/icalendar-generator` | ^2.0 | .ics calendar file generation |
### 3.4 Development Dependencies
| Package | Purpose |
|---------|---------|
| Laravel Sail | Docker development environment (optional) |
| Laravel Telescope | Debug assistant (dev only) |
| Laravel Pint | Code formatting |
---
## 4. Data Models
### 4.1 Entity Relationship Diagram
```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 --}}
<x-mail::message>
@if($locale === 'ar')
# مرحباً {{ $user->full_name }}،
تمت الموافقة على موعد استشارتك.
**تفاصيل الموعد:**
- **التاريخ:** {{ $consultation->booking_date->format('d/m/Y') }}
- **الوقت:** {{ $consultation->booking_time->format('h:i A') }}
- **المدة:** 45 دقيقة
@if($consultation->consultation_type === 'paid')
- **نوع الاستشارة:** مدفوعة
- **المبلغ:** {{ $consultation->payment_amount }} شيكل
@else
- **نوع الاستشارة:** مجانية
@endif
يرجى إضافة الموعد إلى تقويمك باستخدام الملف المرفق.
@else
# Hello {{ $user->full_name }},
Your consultation has been approved.
**Appointment Details:**
- **Date:** {{ $consultation->booking_date->format('m/d/Y') }}
- **Time:** {{ $consultation->booking_time->format('h:i A') }}
- **Duration:** 45 minutes
@if($consultation->consultation_type === 'paid')
- **Type:** Paid Consultation
- **Amount:** {{ $consultation->payment_amount }} ILS
@else
- **Type:** Free Consultation
@endif
Please add this appointment to your calendar using the attached file.
@endif
<x-mail::button :url="route('client.consultations.index')">
{{ $locale === 'ar' ? 'عرض موعدي' : 'View My Consultation' }}
</x-mail::button>
{{ $locale === 'ar' ? 'مع تحيات،' : 'Best regards,' }}<br>
{{ config('app.name') }}
</x-mail::message>
```
---
## 11. Background Jobs & Scheduling
### 11.1 Queue Configuration
```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 --}}
<!DOCTYPE html>
<html
lang="{{ app()->getLocale() }}"
dir="{{ app()->getLocale() === 'ar' ? 'rtl' : 'ltr' }}"
class="{{ app()->getLocale() === 'ar' ? 'font-arabic' : 'font-english' }}"
>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ $title ?? config('app.name') }}</title>
{{-- Fonts --}}
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Cairo:wght@300;400;600;700&family=Montserrat:wght@300;400;600;700&display=swap" rel="stylesheet">
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
```
```css
/* resources/css/app.css */
@import "tailwindcss";
@theme {
--font-arabic: 'Cairo', sans-serif;
--font-english: 'Montserrat', sans-serif;
/* Brand colors - Updated in Epic 12 */
--color-dark-forest: #2D3624;
--color-warm-gold: #A68966;
--color-warm-cream: #F4F1EA;
--color-forest-green: #2D322A;
--color-pure-white: #FFFFFF;
--color-olive-green: #4A5D23;
--color-primary-light: #3D4634;
--color-gold-hover: #8A7555;
--color-gold-light: #C4A882;
--color-deep-black: #1A1A1A;
}
/* RTL utilities */
[dir="rtl"] .rtl\:space-x-reverse > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 1;
}
.font-arabic {
font-family: var(--font-arabic);
}
.font-english {
font-family: var(--font-english);
}
```
---
## 13. Security
### 13.1 Security Measures
| Measure | Implementation | Configuration |
|---------|----------------|---------------|
| **CSRF Protection** | Laravel default | All POST/PUT/DELETE forms |
| **XSS Prevention** | Blade escaping `{{ }}` | Default for all output |
| **SQL Injection** | Eloquent ORM | Parameterized queries |
| **Password Hashing** | bcrypt | Laravel default |
| **Session Security** | Secure cookies | `SESSION_SECURE_COOKIE=true` |
| **Rate Limiting** | Throttle middleware | 5 login attempts/minute |
| **Input Validation** | Form Requests | All user input |
| **HTTPS** | Forced in production | `APP_URL=https://` |
| **CSP Headers** | Middleware | Restrictive policy |
### 13.2 Rate Limiting
```php
// bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
$middleware->throttleApi();
})
// app/Providers/AppServiceProvider.php
public function boot(): void
{
RateLimiter::for('login', function (Request $request) {
return Limit::perMinute(5)->by($request->ip());
});
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
}
```
### 13.3 Content Security Policy
```php
// app/Http/Middleware/ContentSecurityPolicy.php
class ContentSecurityPolicy
{
public function handle(Request $request, Closure $next): Response
{
$response = $next($request);
$response->headers->set('Content-Security-Policy', implode('; ', [
"default-src 'self'",
"script-src 'self' 'unsafe-inline' 'unsafe-eval'", // Required for Livewire
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
"font-src 'self' https://fonts.gstatic.com",
"img-src 'self' data: https:",
"connect-src 'self'",
"frame-ancestors 'none'",
]));
$response->headers->set('X-Content-Type-Options', 'nosniff');
$response->headers->set('X-Frame-Options', 'DENY');
$response->headers->set('X-XSS-Protection', '1; mode=block');
$response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
return $response;
}
}
```
### 13.4 Sensitive Data Handling
```php
// app/Models/User.php
// Encrypt sensitive fields
protected function nationalId(): Attribute
{
return Attribute::make(
get: fn (?string $value) => $value ? decrypt($value) : null,
set: fn (?string $value) => $value ? encrypt($value) : null,
);
}
// Never expose in API/exports
protected $hidden = [
'password',
'remember_token',
'two_factor_secret',
'two_factor_recovery_codes',
'national_id', // Encrypted, don't expose
];
```
### 13.5 Audit Logging
```php
// app/Observers/UserObserver.php
class UserObserver
{
public function created(User $user): void
{
$this->log('created', $user);
}
public function updated(User $user): void
{
if ($user->wasChanged()) {
$this->log('updated', $user, $user->getOriginal(), $user->getAttributes());
}
}
public function deleted(User $user): void
{
$this->log('deleted', $user);
}
private function log(string $action, User $user, ?array $old = null, ?array $new = null): void
{
AdminLog::create([
'admin_id' => auth()->id(),
'action' => $action,
'target_type' => User::class,
'target_id' => $user->id,
'old_values' => $old ? $this->filterSensitive($old) : null,
'new_values' => $new ? $this->filterSensitive($new) : null,
'ip_address' => request()->ip(),
]);
}
private function filterSensitive(array $values): array
{
unset(
$values['password'],
$values['remember_token'],
$values['two_factor_secret'],
$values['two_factor_recovery_codes']
);
return $values;
}
}
```
---
## 14. Testing Strategy
### 14.1 Test Organization
```
tests/
├── Feature/ # Integration tests
│ ├── Admin/
│ │ ├── DashboardTest.php
│ │ ├── UserManagementTest.php
│ │ ├── ConsultationManagementTest.php
│ │ ├── TimelineManagementTest.php
│ │ ├── PostManagementTest.php
│ │ └── SettingsTest.php
│ ├── Client/
│ │ ├── DashboardTest.php
│ │ ├── BookingTest.php
│ │ ├── TimelineViewTest.php
│ │ └── ProfileTest.php
│ ├── Public/
│ │ ├── HomePageTest.php
│ │ ├── GuestBookingTest.php
│ │ ├── PostsTest.php
│ │ └── LanguageSwitchTest.php
│ ├── Auth/ # Existing auth tests
│ ├── Email/
│ │ └── NotificationTest.php
│ └── Jobs/
│ └── QueueJobsTest.php
└── Unit/
├── Actions/
│ ├── CreateConsultationActionTest.php
│ └── ApproveConsultationActionTest.php
├── Models/
│ ├── UserTest.php
│ ├── ConsultationTest.php
│ └── TimelineTest.php
├── Services/
│ ├── AvailabilityServiceTest.php
│ ├── CalendarServiceTest.php
│ └── CaptchaServiceTest.php
└── Enums/
└── ConsultationStatusTest.php
```
### 14.2 Test Examples
```php
// tests/Feature/Client/BookingTest.php
<?php
use App\Models\User;
use App\Models\Consultation;
use App\Models\WorkingHour;
use App\Enums\ConsultationStatus;
use App\Jobs\SendBookingNotification;
use Livewire\Volt\Volt;
use Illuminate\Support\Facades\Queue;
beforeEach(function () {
// Seed working hours
WorkingHour::factory()->weekdays()->create();
});
test('client can book consultation on available date', function () {
Queue::fake();
$client = User::factory()->client()->create();
$bookingDate = now()->addDays(3)->startOfDay();
// Ensure it's a weekday
while ($bookingDate->isWeekend()) {
$bookingDate->addDay();
}
Volt::test('client.consultations.book')
->actingAs($client)
->set('form.booking_date', $bookingDate->format('Y-m-d'))
->set('form.booking_time', '10:00')
->set('form.problem_summary', 'I need legal advice regarding a contract dispute.')
->call('submit')
->assertHasNoErrors()
->assertDispatched('notify');
expect(Consultation::where('user_id', $client->id)->exists())->toBeTrue();
$consultation = Consultation::where('user_id', $client->id)->first();
expect($consultation->status)->toBe(ConsultationStatus::Pending);
expect($consultation->booking_date->format('Y-m-d'))->toBe($bookingDate->format('Y-m-d'));
Queue::assertPushed(SendBookingNotification::class);
});
test('client cannot book more than once per day', function () {
$client = User::factory()->client()->create();
$bookingDate = now()->addDays(3);
// Create existing booking
Consultation::factory()->for($client)->create([
'booking_date' => $bookingDate,
]);
Volt::test('client.consultations.book')
->actingAs($client)
->set('form.booking_date', $bookingDate->format('Y-m-d'))
->set('form.booking_time', '14:00')
->set('form.problem_summary', 'Another consultation request.')
->call('submit')
->assertHasErrors(['form.booking_date']);
expect(Consultation::where('user_id', $client->id)->count())->toBe(1);
});
test('client cannot book on blocked date', function () {
$client = User::factory()->client()->create();
$blockedDate = now()->addDays(5);
BlockedTime::factory()->create([
'block_date' => $blockedDate,
'start_time' => null, // Full day block
'end_time' => null,
]);
Volt::test('client.consultations.book')
->actingAs($client)
->set('form.booking_date', $blockedDate->format('Y-m-d'))
->set('form.booking_time', '10:00')
->set('form.problem_summary', 'Test booking.')
->call('submit')
->assertHasErrors(['form.booking_date']);
});
```
```php
// tests/Feature/Admin/UserManagementTest.php
<?php
use App\Models\User;
use App\Enums\UserType;
use App\Enums\UserStatus;
use App\Jobs\SendWelcomeEmail;
use Livewire\Volt\Volt;
use Illuminate\Support\Facades\Queue;
test('admin can create individual client', function () {
Queue::fake();
$admin = User::factory()->admin()->create();
Volt::test('admin.users.create')
->actingAs($admin)
->set('form.user_type', 'individual')
->set('form.full_name', 'Ahmad Hassan')
->set('form.national_id', '123456789')
->set('form.email', 'ahmad@example.com')
->set('form.phone', '+970591234567')
->set('form.password', 'SecurePass123!')
->set('form.password_confirmation', 'SecurePass123!')
->set('form.preferred_language', 'ar')
->call('submit')
->assertHasNoErrors()
->assertRedirect(route('admin.users.index'));
$user = User::where('email', 'ahmad@example.com')->first();
expect($user)->not->toBeNull()
->and($user->user_type)->toBe(UserType::Individual)
->and($user->status)->toBe(UserStatus::Active);
Queue::assertPushed(SendWelcomeEmail::class);
});
test('admin can deactivate client', function () {
$admin = User::factory()->admin()->create();
$client = User::factory()->client()->create();
Volt::test('admin.users.edit', ['user' => $client])
->actingAs($admin)
->call('deactivate')
->assertHasNoErrors();
$client->refresh();
expect($client->status)->toBe(UserStatus::Deactivated);
});
test('admin cannot delete another admin', function () {
$admin1 = User::factory()->admin()->create();
$admin2 = User::factory()->admin()->create();
Volt::test('admin.users.edit', ['user' => $admin2])
->actingAs($admin1)
->call('delete')
->assertForbidden();
});
```
### 14.3 Coverage Targets
| Area | Target | Priority |
|------|--------|----------|
| Actions | 95% | High |
| Models (methods) | 90% | High |
| Policies | 100% | High |
| Services | 90% | High |
| Volt Components | 80% | Medium |
| Critical user flows | 100% | Critical |
### 14.4 Running Tests
```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
<?php
// resources/views/livewire/admin/consultations/index.blade.php
use App\Models\Consultation;
use App\Enums\ConsultationStatus;
use Livewire\Volt\Component;
use Livewire\WithPagination;
new class extends Component {
use WithPagination;
public string $search = '';
public string $status = '';
public function updatedSearch(): void
{
$this->resetPage();
}
public function updatedStatus(): void
{
$this->resetPage();
}
public function with(): array
{
return [
'consultations' => Consultation::query()
->with('user:id,full_name,email')
->when($this->search, fn ($q) => $q->whereHas('user', fn ($q) =>
$q->where('full_name', 'like', "%{$this->search}%")
))
->when($this->status, fn ($q) => $q->where('status', $this->status))
->orderBy('booking_date', 'desc')
->paginate(15),
'statuses' => ConsultationStatus::cases(),
];
}
}; ?>
<div>
<x-slot name="header">
<flux:heading size="xl">{{ __('models.consultations') }}</flux:heading>
</x-slot>
<div class="flex gap-4 mb-6">
<flux:input
wire:model.live.debounce.300ms="search"
placeholder="{{ __('messages.search') }}"
icon="magnifying-glass"
/>
<flux:select wire:model.live="status">
<option value="">{{ __('messages.all_statuses') }}</option>
@foreach($statuses as $s)
<option value="{{ $s->value }}">{{ $s->label() }}</option>
@endforeach
</flux:select>
</div>
<div class="space-y-4">
@forelse($consultations as $consultation)
<x-ui.card wire:key="consultation-{{ $consultation->id }}">
{{-- Card content --}}
</x-ui.card>
@empty
<x-ui.empty-state :message="__('messages.no_consultations')" />
@endforelse
</div>
<div class="mt-6">
{{ $consultations->links() }}
</div>
</div>
```
### 20.4 Action Class Template
```php
<?php
// app/Actions/Consultation/CreateConsultationAction.php
namespace App\Actions\Consultation;
use App\Models\Consultation;
use App\Models\User;
use App\Enums\ConsultationStatus;
use App\Enums\PaymentStatus;
use App\Jobs\SendBookingNotification;
use App\Exceptions\BookingLimitExceededException;
use Illuminate\Support\Facades\DB;
class CreateConsultationAction
{
public function execute(User $user, array $data): Consultation
{
// Validate booking limit
if ($user->hasBookingOnDate($data['booking_date'])) {
throw new BookingLimitExceededException(
__('messages.booking_limit_exceeded')
);
}
return DB::transaction(function () use ($user, $data) {
$consultation = Consultation::create([
'user_id' => $user->id,
'booking_date' => $data['booking_date'],
'booking_time' => $data['booking_time'],
'problem_summary' => $data['problem_summary'],
'status' => ConsultationStatus::Pending,
'payment_status' => PaymentStatus::NotApplicable,
]);
SendBookingNotification::dispatch($consultation);
return $consultation;
});
}
}
```
---
## 21. Technology Alternatives Considered
### 21.1 Frontend Framework
| Option | Pros | Cons | Decision |
|--------|------|------|----------|
| **Livewire 3 + Volt** | No JS build, Laravel native, simple state management | Learning curve, larger payload | **SELECTED** |
| React/Vue SPA | Rich ecosystem, familiar to many | Separate build, API needed, complexity | Rejected |
| Blade + Alpine only | Simplest, no Livewire overhead | Limited interactivity | Considered for simpler features |
| Inertia.js | Best of both worlds | Additional complexity, less documentation | Rejected |
**Rationale:** Livewire 3 provides excellent developer experience with Laravel, eliminates API development overhead, and Volt single-file components align well with the project's maintainability goals.
### 21.2 Database
| Option | Pros | Cons | Decision |
|--------|------|------|----------|
| **MariaDB** | MySQL compatible, open source, client preference | - | **SELECTED (Production)** |
| **SQLite** | Zero config, fast tests, portable | Not suitable for production | **SELECTED (Development)** |
| PostgreSQL | Advanced features, JSON support | Overkill for this project | Rejected |
| MySQL | Industry standard | MariaDB preferred by client | Rejected |
### 21.3 Authentication
| Option | Pros | Cons | Decision |
|--------|------|------|----------|
| **Laravel Fortify** | Headless, flexible, 2FA support | Requires custom views | **SELECTED** |
| Laravel Breeze | Quick setup, includes views | Less flexible, opinionated | Rejected |
| Laravel Jetstream | Full-featured, team support | Too complex, Livewire/Inertia choice | Rejected |
| Custom auth | Full control | Reinventing wheel, security risks | Rejected |
### 21.4 Queue System
| Option | Pros | Cons | Decision |
|--------|------|------|----------|
| **Database** | Simple, no extra services | Slower than Redis | **SELECTED** |
| Redis | Fast, feature-rich | Additional service to maintain | Future consideration |
| Amazon SQS | Managed, scalable | Overkill, cost, lock-in | Rejected |
| Beanstalkd | Lightweight, fast | Less Laravel integration | Rejected |
**Rationale:** Database queue driver is sufficient for expected volume (~100 jobs/day). Redis can be added later if performance requires.
### 21.5 CSS Framework
| Option | Pros | Cons | Decision |
|--------|------|------|----------|
| **Tailwind CSS 4** | Utility-first, RTL support, Flux UI compatible | Learning curve | **SELECTED** |
| Bootstrap 5 | Familiar, RTL plugin | Larger bundle, less flexible | PRD mentioned but Tailwind preferred |
| Custom CSS | Full control | Time-consuming, maintenance burden | Rejected |
---
## 22. Appendices
### A. Flux UI Free Components
Available components in Flux UI Free edition:
```
avatar, badge, brand, breadcrumbs, button, callout, checkbox, dropdown,
field, heading, icon, input, modal, navbar, otp-input, profile, radio,
select, separator, skeleton, switch, text, textarea, tooltip
```
### B. Color Palette
> **Note:** Color palette updated in Epic 12 (Dark Charcoal & Warm Gold refresh).
> See `docs/brand.md` for complete brand guidelines.
| Name | Hex | CSS Variable | Usage |
|------|-----|--------------|-------|
| Dark Forest Green | `#2D3624` | `--color-dark-forest` | Header/Footer backgrounds |
| Warm Gold | `#A68966` | `--color-warm-gold` | CTA buttons, links, accents |
| Warm Cream | `#F4F1EA` | `--color-warm-cream` | Main page background |
| Forest Green | `#2D322A` | `--color-forest-green` | Headings, body text |
| Pure White | `#FFFFFF` | `--color-pure-white` | Card backgrounds |
| Olive Green | `#4A5D23` | `--color-olive-green` | Active states |
| Primary Light | `#3D4634` | `--color-primary-light` | Primary hover states |
| Gold Hover | `#8A7555` | `--color-gold-hover` | Button hover states |
| Gold Light | `#C4A882` | `--color-gold-light` | Light accents |
| Deep Black | `#1A1A1A` | `--color-deep-black` | Text, footer |
| Success Green | `#27AE60` | `--color-success` | Success states |
| Warning Red | `#E74C3C` | `--color-danger` | Error states |
| Pending Yellow | `#F39C12` | `--color-warning` | Pending states |
### C. Consultation Duration Constants
```php
// config/libra.php
return [
'consultation' => [
'duration_minutes' => 45,
'buffer_minutes' => 15,
'slot_minutes' => 60, // duration + buffer
],
];
```
### D. External Resources
- [Laravel 12 Documentation](https://laravel.com/docs/12.x)
- [Livewire 3 Documentation](https://livewire.laravel.com/docs)
- [Volt Documentation](https://livewire.laravel.com/docs/volt)
- [Flux UI Documentation](https://fluxui.dev/docs)
- [Tailwind CSS 4 Documentation](https://tailwindcss.com/docs)
- [Pest Testing Documentation](https://pestphp.com/docs)
- [Laravel Fortify Documentation](https://laravel.com/docs/12.x/fortify)
### E. Contact Information
- **Admin Email:** (configured in settings)
- **System Email:** no-reply@libra.ps
- **Domain:** libra.ps
---
**END OF DOCUMENT**