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
- Introduction
- High Level Architecture
- Technology Stack
- Data Models
- Database Schema
- Application Structure
- Authentication & Authorization
- Core Workflows
- Routing Structure
- Email System
- Background Jobs & Scheduling
- Localization & Timezone
- Security
- Testing Strategy
- Performance & Optimization
- Deployment & CI/CD
- Monitoring & Alerting
- Error Handling & Graceful Degradation
- Rollback & Recovery Procedures
- Coding Standards
- Technology Alternatives Considered
- Appendices
1. Introduction
1.1 Purpose
This document outlines the complete architecture for Libra Law Firm's web platform. It serves as the single source of truth for development, ensuring consistency and guiding AI-driven implementation.
1.2 Project Overview
Libra Law Firm is a bilingual (Arabic/English) Laravel application serving as both a public-facing website and internal management tool for:
- Client consultation booking and management
- Case timeline tracking
- Legal content publishing
- Administrative operations
1.3 Key Constraints
| Constraint |
Details |
| Single Admin |
One lawyer manages all operations |
| No Online Payments |
All payments handled offline |
| Client-Managed Infrastructure |
Rocky Linux 9 server |
| No Self-Registration |
Admin creates all client accounts |
| Bilingual Requirement |
Arabic (RTL) primary, English (LTR) secondary |
1.4 Project Context
Existing Foundation:
- Laravel 12.43.1 with Fortify authentication
- Livewire 3.7.3 + Volt 1.10.1 for reactive components
- Flux UI Free 2.10.2 component library
- Tailwind CSS 4.1.11
- Pest 4.2.0 testing framework
2. High Level Architecture
2.1 Technical Summary
Libra is a server-rendered Laravel monolith with Livewire for reactive UI. The architecture follows Laravel's MVC pattern enhanced with:
- Livewire 3 + Volt for interactive components without JavaScript complexity
- Fortify for headless authentication with custom Volt views
- Eloquent ORM for database operations
- Blade + Flux UI for consistent, accessible component design
- Queue workers for background email processing and scheduled reminders
This architecture prioritizes simplicity and maintainability over distributed complexity.
2.2 System Context Diagram
graph TB
subgraph "Users"
Admin[Admin/Lawyer]
Client[Clients]
Public[Public Visitors]
end
subgraph "Libra Platform"
App[Laravel Application<br/>libra.ps]
end
subgraph "External Services"
SMTP[SMTP Server<br/>no-reply@libra.ps]
end
subgraph "Infrastructure"
Server[Rocky Linux 9<br/>Client-Managed]
end
Admin -->|Manage users, bookings,<br/>timelines, posts| App
Client -->|Book consultations,<br/>view timelines| App
Public -->|View posts,<br/>firm info| App
App -->|Send emails| SMTP
App -->|Deployed on| Server
2.3 Container Diagram
graph TB
subgraph "Client Devices"
Browser[Web Browser]
Mobile[Mobile Browser]
end
subgraph "Web Server - Nginx"
Static[Static Assets<br/>CSS/JS/Images]
PHP[PHP-FPM 8.4]
end
subgraph "Laravel Application"
Router[Route Handler]
MW[Middleware Stack<br/>Auth, Locale, CSRF]
subgraph "Presentation Layer"
Volt[Volt Components]
Flux[Flux UI Components]
Blade[Blade Templates]
end
subgraph "Business Logic Layer"
Actions[Action Classes]
Fortify[Fortify Auth]
Policies[Authorization Policies]
end
subgraph "Data Access Layer"
Models[Eloquent Models]
Observers[Model Observers]
end
end
subgraph "Background Processing"
Queue[Queue Worker<br/>Supervisor]
Scheduler[Task Scheduler<br/>Cron]
end
subgraph "Data Storage"
MariaDB[(MariaDB<br/>Production)]
SQLite[(SQLite<br/>Development)]
FileStorage[Local Storage<br/>Exports/Logs]
end
subgraph "External"
SMTP[SMTP Server]
end
Browser --> Static
Browser --> PHP
Mobile --> Static
Mobile --> PHP
PHP --> Router
Router --> MW
MW --> Volt
MW --> Blade
Volt --> Flux
Volt --> Actions
Actions --> Fortify
Actions --> Policies
Actions --> Models
Models --> Observers
Models --> MariaDB
Models --> SQLite
Queue --> Actions
Queue --> SMTP
Scheduler --> Queue
Actions --> FileStorage
2.4 Architectural Patterns
| Pattern |
Description |
Rationale |
| MVC + Livewire |
Laravel MVC with Livewire reactive components |
Standard Laravel pattern; eliminates need for separate SPA |
| Action Classes |
Single-purpose classes for business logic |
Keeps controllers thin; reusable; testable |
| Form Objects |
Livewire form objects for validation |
Encapsulates validation; reusable forms |
| Observer Pattern |
Eloquent observers for model events |
Automatic audit logging, notification triggers |
| Queue-based Processing |
Background jobs for emails and heavy operations |
Non-blocking UX; reliable delivery |
| Policy-based Authorization |
Laravel Policies for access control |
Clean separation of authorization logic |
| Repository Pattern |
Optional for complex queries |
Only if query complexity warrants |
3. Technology Stack
3.1 Core Technologies
| Category |
Technology |
Version |
Purpose |
Rationale |
| Runtime |
PHP |
8.4.x |
Server-side language |
Latest stable; required by Laravel 12 |
| Framework |
Laravel |
12.x |
Application framework |
Industry standard; excellent ecosystem |
| Reactive UI |
Livewire |
3.7.x |
Interactive components |
No JS build complexity; server-state |
| Components |
Volt |
1.10.x |
Single-file components |
Cleaner organization; class-based |
| UI Library |
Flux UI Free |
2.10.x |
Pre-built components |
Consistent design; accessibility |
| CSS |
Tailwind CSS |
4.x |
Utility-first styling |
Rapid development; RTL support |
| Auth |
Laravel Fortify |
1.33.x |
Headless auth |
Flexible; 2FA support |
| Database (Prod) |
MariaDB |
10.11+ |
Production database |
MySQL-compatible; performant |
| Database (Dev) |
SQLite |
Latest |
Development database |
Zero config; fast tests |
| Testing |
Pest |
4.x |
Testing framework |
Elegant syntax; Laravel integration |
| Code Style |
Laravel Pint |
1.x |
Code formatting |
Consistent style |
| Queue |
Database Driver |
- |
Job processing |
Simple; no Redis dependency |
3.2 Frontend Dependencies
| Package |
Version |
Purpose |
| Alpine.js |
(via Livewire) |
Minimal JS interactivity |
| Vite |
6.x |
Asset bundling |
| Google Fonts |
- |
Cairo (Arabic), Montserrat (English) |
| Heroicons |
(via Flux) |
Icon library |
3.3 Backend Dependencies (To Add)
| Package |
Version |
Purpose |
barryvdh/laravel-dompdf |
^3.0 |
PDF export generation |
league/csv |
^9.0 |
CSV export generation |
spatie/icalendar-generator |
^2.0 |
.ics calendar file generation |
3.4 Development Dependencies
| Package |
Purpose |
| Laravel Sail |
Docker development environment (optional) |
| Laravel Telescope |
Debug assistant (dev only) |
| Laravel Pint |
Code formatting |
4. Data Models
4.1 Entity Relationship Diagram
erDiagram
users ||--o{ consultations : "has many"
users ||--o{ timelines : "has many"
users ||--o{ notifications : "has many"
timelines ||--o{ timeline_updates : "has many"
users ||--o{ admin_logs : "performed by"
users {
bigint id PK
enum user_type "individual|company|admin"
string full_name
string national_id "nullable, encrypted"
string company_name "nullable"
string company_cert_number "nullable"
string contact_person_name "nullable"
string contact_person_id "nullable"
string email UK
string phone
string password "hashed"
enum status "active|deactivated"
string preferred_language "ar|en"
timestamp email_verified_at
text two_factor_secret "nullable"
text two_factor_recovery_codes "nullable"
timestamp two_factor_confirmed_at
timestamps created_updated
}
consultations {
bigint id PK
bigint user_id FK
date booking_date
time booking_time
text problem_summary
enum consultation_type "free|paid"
decimal payment_amount "nullable"
enum payment_status "pending|received|na"
enum status "pending|approved|rejected|completed|no_show|cancelled"
text admin_notes "nullable"
timestamps created_updated
}
timelines {
bigint id PK
bigint user_id FK
string case_name
string case_reference "nullable"
enum status "active|archived"
timestamps created_updated
}
timeline_updates {
bigint id PK
bigint timeline_id FK
bigint admin_id FK
text update_text
timestamps created_updated
}
posts {
bigint id PK
json title "ar and en"
json body "ar and en"
enum status "draft|published"
timestamp published_at "nullable"
timestamps created_updated
}
working_hours {
bigint id PK
tinyint day_of_week "0-6 Sunday-Saturday"
time start_time
time end_time
boolean is_active
}
blocked_times {
bigint id PK
date block_date
time start_time "nullable for full day"
time end_time "nullable for full day"
string reason "nullable"
timestamps created_updated
}
notifications {
bigint id PK
bigint user_id FK
string type
json data
timestamp read_at "nullable"
timestamp sent_at "nullable"
timestamps created_updated
}
admin_logs {
bigint id PK
bigint admin_id FK
string action
string target_type
bigint target_id "nullable"
json old_values "nullable"
json new_values "nullable"
string ip_address
timestamp created_at
}
settings {
bigint id PK
string key UK
json value
timestamps created_updated
}
4.2 Model Definitions
User Model
// app/Models/User.php
class User extends Authenticatable
{
use HasFactory, Notifiable, TwoFactorAuthenticatable;
protected $fillable = [
'user_type',
'full_name',
'national_id',
'company_name',
'company_cert_number',
'contact_person_name',
'contact_person_id',
'email',
'phone',
'password',
'status',
'preferred_language',
];
protected $hidden = [
'password',
'remember_token',
'two_factor_secret',
'two_factor_recovery_codes',
];
protected function casts(): array
{
return [
'user_type' => UserType::class,
'status' => UserStatus::class,
'email_verified_at' => 'datetime',
'two_factor_confirmed_at' => 'datetime',
'password' => 'hashed',
];
}
// Relationships
public function consultations(): HasMany;
public function timelines(): HasMany;
public function notifications(): HasMany;
public function adminLogs(): HasMany;
// Scopes
public function scopeActive(Builder $query): Builder;
public function scopeClients(Builder $query): Builder;
public function scopeIndividuals(Builder $query): Builder;
public function scopeCompanies(Builder $query): Builder;
// Helpers
public function isAdmin(): bool;
public function isClient(): bool;
public function isIndividual(): bool;
public function isCompany(): bool;
public function canBookOnDate(Carbon $date): bool;
public function hasBookingOnDate(Carbon $date): bool;
}
Consultation Model
// app/Models/Consultation.php
class Consultation extends Model
{
use HasFactory;
protected $fillable = [
'user_id',
'booking_date',
'booking_time',
'problem_summary',
'consultation_type',
'payment_amount',
'payment_status',
'status',
'admin_notes',
];
protected function casts(): array
{
return [
'booking_date' => 'date',
'booking_time' => 'datetime:H:i',
'consultation_type' => ConsultationType::class,
'payment_status' => PaymentStatus::class,
'status' => ConsultationStatus::class,
'payment_amount' => 'decimal:2',
];
}
// Relationships
public function user(): BelongsTo;
// Scopes
public function scopePending(Builder $query): Builder;
public function scopeApproved(Builder $query): Builder;
public function scopeUpcoming(Builder $query): Builder;
public function scopeForDate(Builder $query, Carbon $date): Builder;
public function scopeForDateRange(Builder $query, Carbon $start, Carbon $end): Builder;
// Accessors
public function getEndTimeAttribute(): Carbon;
public function getStartDateTimeAttribute(): Carbon;
public function getEndDateTimeAttribute(): Carbon;
// Helpers
public function isPaid(): bool;
public function isPending(): bool;
public function canBeApproved(): bool;
public function canBeCancelled(): bool;
}
Timeline Model
// app/Models/Timeline.php
class Timeline extends Model
{
use HasFactory;
protected $fillable = [
'user_id',
'case_name',
'case_reference',
'status',
];
protected function casts(): array
{
return [
'status' => TimelineStatus::class,
];
}
// Relationships
public function user(): BelongsTo;
public function updates(): HasMany;
public function latestUpdate(): HasOne;
// Scopes
public function scopeActive(Builder $query): Builder;
public function scopeArchived(Builder $query): Builder;
public function scopeForUser(Builder $query, User $user): Builder;
}
Post Model
// app/Models/Post.php
class Post extends Model
{
use HasFactory;
protected $fillable = [
'title',
'body',
'status',
'published_at',
];
protected function casts(): array
{
return [
'title' => 'array',
'body' => 'array',
'status' => PostStatus::class,
'published_at' => 'datetime',
];
}
// Accessors for localized content
public function getTitleLocalizedAttribute(): string;
public function getBodyLocalizedAttribute(): string;
// Scopes
public function scopePublished(Builder $query): Builder;
public function scopeDraft(Builder $query): Builder;
// Helpers
public function isPublished(): bool;
}
4.3 Enums
// app/Enums/UserType.php
enum UserType: string
{
case Individual = 'individual';
case Company = 'company';
case Admin = 'admin';
public function label(): string
{
return match($this) {
self::Individual => __('enums.user_type.individual'),
self::Company => __('enums.user_type.company'),
self::Admin => __('enums.user_type.admin'),
};
}
}
// app/Enums/UserStatus.php
enum UserStatus: string
{
case Active = 'active';
case Deactivated = 'deactivated';
}
// app/Enums/ConsultationType.php
enum ConsultationType: string
{
case Free = 'free';
case Paid = 'paid';
}
// app/Enums/ConsultationStatus.php
enum ConsultationStatus: string
{
case Pending = 'pending';
case Approved = 'approved';
case Rejected = 'rejected';
case Completed = 'completed';
case NoShow = 'no_show';
case Cancelled = 'cancelled';
public function color(): string
{
return match($this) {
self::Pending => 'yellow',
self::Approved => 'blue',
self::Rejected => 'red',
self::Completed => 'green',
self::NoShow => 'gray',
self::Cancelled => 'red',
};
}
}
// app/Enums/PaymentStatus.php
enum PaymentStatus: string
{
case Pending = 'pending';
case Received = 'received';
case NotApplicable = 'na';
}
// app/Enums/TimelineStatus.php
enum TimelineStatus: string
{
case Active = 'active';
case Archived = 'archived';
}
// app/Enums/PostStatus.php
enum PostStatus: string
{
case Draft = 'draft';
case Published = 'published';
}
5. Database Schema
5.1 Migration Order
0001_01_01_000000_create_users_table.php (modify existing)
0001_01_01_000001_create_cache_table.php (existing)
0001_01_01_000002_create_jobs_table.php (existing)
2025_01_01_000001_add_profile_fields_to_users_table.php
2025_01_01_000002_create_consultations_table.php
2025_01_01_000003_create_timelines_table.php
2025_01_01_000004_create_timeline_updates_table.php
2025_01_01_000005_create_posts_table.php
2025_01_01_000006_create_working_hours_table.php
2025_01_01_000007_create_blocked_times_table.php
2025_01_01_000008_create_notifications_table.php
2025_01_01_000009_create_admin_logs_table.php
2025_01_01_000010_create_settings_table.php
5.2 Key Migration Examples
Users Table Extension
// database/migrations/2025_01_01_000001_add_profile_fields_to_users_table.php
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->string('user_type')->default('individual')->after('id');
$table->string('full_name')->nullable()->after('name');
$table->string('national_id')->nullable()->after('full_name');
$table->string('company_name')->nullable()->after('national_id');
$table->string('company_cert_number')->nullable()->after('company_name');
$table->string('contact_person_name')->nullable()->after('company_cert_number');
$table->string('contact_person_id')->nullable()->after('contact_person_name');
$table->string('phone', 20)->nullable()->after('email');
$table->string('status')->default('active')->after('password');
$table->string('preferred_language', 2)->default('ar')->after('status');
$table->index('user_type');
$table->index('status');
$table->index(['user_type', 'status']);
});
}
Consultations Table
// database/migrations/2025_01_01_000002_create_consultations_table.php
public function up(): void
{
Schema::create('consultations', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->date('booking_date');
$table->time('booking_time');
$table->text('problem_summary');
$table->string('consultation_type')->default('free');
$table->decimal('payment_amount', 10, 2)->nullable();
$table->string('payment_status')->default('na');
$table->string('status')->default('pending');
$table->text('admin_notes')->nullable();
$table->timestamps();
$table->index('booking_date');
$table->index('status');
$table->index(['booking_date', 'booking_time']);
$table->unique(['user_id', 'booking_date']); // One booking per user per day
});
}
Working Hours Table
// database/migrations/2025_01_01_000006_create_working_hours_table.php
public function up(): void
{
Schema::create('working_hours', function (Blueprint $table) {
$table->id();
$table->tinyInteger('day_of_week')->unsigned(); // 0=Sunday, 6=Saturday
$table->time('start_time');
$table->time('end_time');
$table->boolean('is_active')->default(true);
$table->unique('day_of_week');
});
}
5.3 Indexes Strategy
| Table |
Index |
Columns |
Purpose |
| users |
user_type_status |
user_type, status |
Filter clients |
| consultations |
booking_date |
booking_date |
Calendar queries |
| consultations |
status |
status |
Filter by status |
| consultations |
user_date_unique |
user_id, booking_date |
Enforce 1/day limit |
| timelines |
user_status |
user_id, status |
Client timeline list |
| posts |
status_published |
status, published_at |
Published posts list |
| admin_logs |
target |
target_type, target_id |
Audit trail lookup |
6. Application Structure
6.1 Directory Structure
libra/
├── app/
│ ├── Actions/ # Business logic (single-purpose)
│ │ ├── Fortify/ # Auth actions (existing)
│ │ │ ├── CreateNewUser.php
│ │ │ ├── ResetUserPassword.php
│ │ │ └── UpdateUserPassword.php
│ │ ├── Consultation/
│ │ │ ├── CreateConsultationAction.php
│ │ │ ├── ApproveConsultationAction.php
│ │ │ ├── RejectConsultationAction.php
│ │ │ ├── CompleteConsultationAction.php
│ │ │ └── CancelConsultationAction.php
│ │ ├── Timeline/
│ │ │ ├── CreateTimelineAction.php
│ │ │ ├── AddTimelineUpdateAction.php
│ │ │ └── ArchiveTimelineAction.php
│ │ ├── User/
│ │ │ ├── CreateClientAction.php
│ │ │ ├── UpdateClientAction.php
│ │ │ ├── DeactivateClientAction.php
│ │ │ ├── ReactivateClientAction.php
│ │ │ ├── DeleteClientAction.php
│ │ │ └── ConvertClientTypeAction.php
│ │ ├── Post/
│ │ │ ├── CreatePostAction.php
│ │ │ ├── UpdatePostAction.php
│ │ │ ├── PublishPostAction.php
│ │ │ └── DeletePostAction.php
│ │ └── Export/
│ │ ├── ExportUsersAction.php
│ │ ├── ExportConsultationsAction.php
│ │ └── GenerateMonthlyReportAction.php
│ ├── Enums/ # PHP 8.1+ enums
│ │ ├── UserType.php
│ │ ├── UserStatus.php
│ │ ├── ConsultationType.php
│ │ ├── ConsultationStatus.php
│ │ ├── PaymentStatus.php
│ │ ├── TimelineStatus.php
│ │ └── PostStatus.php
│ ├── Http/
│ │ ├── Controllers/ # Minimal controllers
│ │ │ ├── LanguageController.php
│ │ │ └── CalendarDownloadController.php
│ │ └── Middleware/
│ │ ├── SetLocale.php
│ │ ├── EnsureUserIsAdmin.php
│ │ └── EnsureUserIsActive.php
│ ├── Jobs/ # Queue jobs
│ │ ├── SendWelcomeEmail.php
│ │ ├── SendBookingNotification.php
│ │ ├── SendConsultationReminder.php
│ │ ├── SendTimelineUpdateNotification.php
│ │ └── GenerateExportFile.php
│ ├── Livewire/ # Full-page Livewire components (if any)
│ │ └── Forms/ # Livewire form objects
│ │ ├── ConsultationForm.php
│ │ ├── ClientForm.php
│ │ ├── TimelineForm.php
│ │ └── PostForm.php
│ ├── Mail/ # Mailable classes
│ │ ├── WelcomeMail.php
│ │ ├── BookingSubmittedMail.php
│ │ ├── BookingApprovedMail.php
│ │ ├── BookingRejectedMail.php
│ │ ├── ConsultationReminderMail.php
│ │ ├── TimelineUpdatedMail.php
│ │ └── NewBookingRequestMail.php
│ ├── Models/ # Eloquent models
│ │ ├── User.php
│ │ ├── Consultation.php
│ │ ├── Timeline.php
│ │ ├── TimelineUpdate.php
│ │ ├── Post.php
│ │ ├── WorkingHour.php
│ │ ├── BlockedTime.php
│ │ ├── Notification.php
│ │ ├── AdminLog.php
│ │ └── Setting.php
│ ├── Observers/ # Model observers
│ │ ├── ConsultationObserver.php
│ │ ├── TimelineUpdateObserver.php
│ │ └── UserObserver.php
│ ├── Policies/ # Authorization policies
│ │ ├── ConsultationPolicy.php
│ │ ├── TimelinePolicy.php
│ │ ├── PostPolicy.php
│ │ └── UserPolicy.php
│ ├── Providers/
│ │ ├── AppServiceProvider.php
│ │ └── FortifyServiceProvider.php
│ └── Services/ # Cross-cutting services
│ ├── AvailabilityService.php
│ ├── CalendarService.php
│ ├── 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:
- Disable public registration - Admin creates all users
- Custom login redirect - Admin →
/admin/dashboard, Client → /client/dashboard
- Admin-triggered password reset - No public reset
// config/fortify.php
'features' => [
// Features::registration(), // DISABLED
Features::resetPasswords(),
Features::emailVerification(),
Features::updateProfileInformation(),
Features::updatePasswords(),
Features::twoFactorAuthentication([
'confirm' => true,
'confirmPassword' => true,
]),
],
7.2 Authorization (Policies)
// app/Policies/ConsultationPolicy.php
class ConsultationPolicy
{
public function viewAny(User $user): bool
{
return true; // All authenticated users can see their own
}
public function view(User $user, Consultation $consultation): bool
{
return $user->isAdmin() || $consultation->user_id === $user->id;
}
public function create(User $user): bool
{
return $user->isClient();
}
public function update(User $user, Consultation $consultation): bool
{
return $user->isAdmin();
}
public function delete(User $user, Consultation $consultation): bool
{
return $user->isAdmin();
}
}
// app/Policies/TimelinePolicy.php
class TimelinePolicy
{
public function viewAny(User $user): bool
{
return true;
}
public function view(User $user, Timeline $timeline): bool
{
return $user->isAdmin() || $timeline->user_id === $user->id;
}
public function create(User $user): bool
{
return $user->isAdmin();
}
public function update(User $user, Timeline $timeline): bool
{
return $user->isAdmin();
}
public function addUpdate(User $user, Timeline $timeline): bool
{
return $user->isAdmin();
}
}
// app/Policies/UserPolicy.php
class UserPolicy
{
public function viewAny(User $user): bool
{
return $user->isAdmin();
}
public function view(User $user, User $model): bool
{
return $user->isAdmin() || $user->id === $model->id;
}
public function create(User $user): bool
{
return $user->isAdmin();
}
public function update(User $user, User $model): bool
{
return $user->isAdmin();
}
public function delete(User $user, User $model): bool
{
return $user->isAdmin() && !$model->isAdmin();
}
}
7.3 Middleware
// app/Http/Middleware/SetLocale.php
class SetLocale
{
public function handle(Request $request, Closure $next): Response
{
$locale = session('locale')
?? $request->user()?->preferred_language
?? config('app.locale');
if (!in_array($locale, ['ar', 'en'])) {
$locale = 'ar';
}
app()->setLocale($locale);
return $next($request);
}
}
// app/Http/Middleware/EnsureUserIsAdmin.php
class EnsureUserIsAdmin
{
public function handle(Request $request, Closure $next): Response
{
if (!$request->user()?->isAdmin()) {
abort(403, __('messages.unauthorized'));
}
return $next($request);
}
}
// app/Http/Middleware/EnsureUserIsActive.php
class EnsureUserIsActive
{
public function handle(Request $request, Closure $next): Response
{
if ($request->user()?->status === UserStatus::Deactivated) {
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect()->route('login')
->with('error', __('messages.account_deactivated'));
}
return $next($request);
}
}
7.4 Middleware Registration
// bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
$middleware->web(append: [
\App\Http\Middleware\SetLocale::class,
]);
$middleware->alias([
'admin' => \App\Http\Middleware\EnsureUserIsAdmin::class,
'active' => \App\Http\Middleware\EnsureUserIsActive::class,
]);
$middleware->validateCsrfTokens(except: [
// Add any exceptions here
]);
})
8. Core Workflows
8.1 Consultation Booking Flow
sequenceDiagram
participant C as Client
participant V as Volt Component
participant A as Action
participant S as AvailabilityService
participant M as Model
participant Q as Queue
participant E as Email (Admin)
C->>V: Open booking page
V->>S: getAvailableSlots(date)
S->>M: Query working_hours, blocked_times, consultations
S-->>V: Available time slots
V-->>C: Display calendar
C->>V: Select date/time, enter summary
V->>V: Validate (date future, slot available, no existing booking)
alt Validation fails
V-->>C: Show error message
else Validation passes
V->>A: CreateConsultationAction
A->>M: Check user has no booking on date
A->>M: Create consultation (status: pending)
A->>M: Log admin action
M-->>A: Consultation created
A->>Q: Dispatch SendBookingNotification (to admin)
Q->>E: Email admin at no-reply@libra.ps
A-->>V: Success
V-->>C: Show confirmation message
end
8.2 Admin Consultation Approval Flow
sequenceDiagram
participant Ad as Admin
participant V as Volt Component
participant A as Action
participant M as Model
participant Q as Queue
participant E as Email (Client)
Ad->>V: View pending consultation
Ad->>V: Set type (free/paid), amount if paid
Ad->>V: Click Approve
V->>A: ApproveConsultationAction
A->>M: Update consultation (status: approved, type, amount)
A->>M: Log admin action
M-->>A: Updated
A->>Q: Dispatch SendBookingApprovedMail (with .ics attachment)
A->>Q: Schedule SendConsultationReminder (24h before)
A->>Q: Schedule SendConsultationReminder (2h before)
Q->>E: Send approval email to client
A-->>V: Success
V-->>Ad: Refresh list, show success toast
8.3 Timeline Update Flow
sequenceDiagram
participant Ad as Admin
participant V as Volt Component
participant A as Action
participant M as Model
participant O as Observer
participant Q as Queue
participant E as Email
Ad->>V: Navigate to timeline
Ad->>V: Enter update text
Ad->>V: Submit
V->>A: AddTimelineUpdateAction
A->>M: Create TimelineUpdate
M->>O: TimelineUpdateObserver::created
O->>Q: Dispatch SendTimelineUpdateNotification
A->>M: Log admin action
A-->>V: Success
Q->>E: Send notification to client
V-->>Ad: Refresh timeline, show new update
8.4 User Creation Flow
sequenceDiagram
participant Ad as Admin
participant V as Volt Component
participant A as Action
participant M as Model
participant Q as Queue
participant E as Email
Ad->>V: Fill user form (type, name, email, etc.)
Ad->>V: Set password
Ad->>V: Submit
V->>V: Validate form
V->>A: CreateClientAction
A->>M: Create User
A->>M: Log admin action
M-->>A: User created
A->>Q: Dispatch SendWelcomeEmail (with credentials)
Q->>E: Send welcome email to new user
A-->>V: Success
V-->>Ad: Redirect to user list
9. Routing Structure
9.1 Route Definitions
// routes/web.php
use App\Http\Controllers\LanguageController;
use App\Http\Controllers\CalendarDownloadController;
use Illuminate\Support\Facades\Route;
use Livewire\Volt\Volt;
/*
|--------------------------------------------------------------------------
| Public Routes
|--------------------------------------------------------------------------
*/
// Home
Volt::route('/', 'pages.home')->name('home');
// Posts
Volt::route('/posts', 'pages.posts.index')->name('posts.index');
Volt::route('/posts/{post}', 'pages.posts.show')->name('posts.show');
// Legal pages
Volt::route('/terms', 'pages.terms')->name('terms');
Volt::route('/privacy', 'pages.privacy')->name('privacy');
// Language switch
Route::get('/language/{locale}', LanguageController::class)
->name('language.switch')
->where('locale', 'ar|en');
/*
|--------------------------------------------------------------------------
| Client Routes (Authenticated Non-Admin)
|--------------------------------------------------------------------------
*/
Route::middleware(['auth', 'verified', 'active'])->prefix('client')->name('client.')->group(function () {
Volt::route('/dashboard', 'client.dashboard')->name('dashboard');
// Consultations
Volt::route('/consultations', 'client.consultations.index')->name('consultations.index');
Volt::route('/consultations/book', 'client.consultations.book')->name('consultations.book');
Route::get('/consultations/{consultation}/calendar', CalendarDownloadController::class)
->name('consultations.calendar');
// Timelines
Volt::route('/timelines', 'client.timelines.index')->name('timelines.index');
Volt::route('/timelines/{timeline}', 'client.timelines.show')->name('timelines.show');
// Profile (view only)
Volt::route('/profile', 'client.profile')->name('profile');
});
/*
|--------------------------------------------------------------------------
| Admin Routes
|--------------------------------------------------------------------------
*/
Route::middleware(['auth', 'verified', 'admin'])->prefix('admin')->name('admin.')->group(function () {
Volt::route('/dashboard', 'admin.dashboard')->name('dashboard');
// Users
Volt::route('/users', 'admin.users.index')->name('users.index');
Volt::route('/users/create', 'admin.users.create')->name('users.create');
Volt::route('/users/{user}/edit', 'admin.users.edit')->name('users.edit');
// Consultations
Volt::route('/consultations', 'admin.consultations.index')->name('consultations.index');
Volt::route('/consultations/pending', 'admin.consultations.pending')->name('consultations.pending');
Volt::route('/consultations/calendar', 'admin.consultations.calendar')->name('consultations.calendar');
Volt::route('/consultations/{consultation}', 'admin.consultations.show')->name('consultations.show');
// Timelines
Volt::route('/timelines', 'admin.timelines.index')->name('timelines.index');
Volt::route('/timelines/create', 'admin.timelines.create')->name('timelines.create');
Volt::route('/timelines/{timeline}', 'admin.timelines.show')->name('timelines.show');
// Posts
Volt::route('/posts', 'admin.posts.index')->name('posts.index');
Volt::route('/posts/create', 'admin.posts.create')->name('posts.create');
Volt::route('/posts/{post}/edit', 'admin.posts.edit')->name('posts.edit');
// Settings
Volt::route('/settings/working-hours', 'admin.settings.working-hours')->name('settings.working-hours');
Volt::route('/settings/blocked-times', 'admin.settings.blocked-times')->name('settings.blocked-times');
Volt::route('/settings/content-pages', 'admin.settings.content-pages')->name('settings.content-pages');
// Reports
Volt::route('/reports', 'admin.reports.index')->name('reports.index');
});
9.2 Route Summary
| Area |
Route Prefix |
Middleware |
Description |
| Public |
/ |
none |
Home, posts, legal pages |
| Client |
/client |
auth, verified, active |
Client dashboard, bookings, timelines |
| Admin |
/admin |
auth, verified, admin |
Full management interface |
| Auth |
/ (Fortify) |
varies |
Login, logout, password, 2FA |
| Settings |
/settings |
auth, verified |
User profile settings |
10. Email System
10.1 Configuration
// config/mail.php (via .env)
MAIL_MAILER=smtp
MAIL_HOST=smtp.libra.ps
MAIL_PORT=587
MAIL_USERNAME=no-reply@libra.ps
MAIL_PASSWORD=****
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS=no-reply@libra.ps
MAIL_FROM_NAME="${APP_NAME}"
10.2 Email Templates
| Email Class |
Trigger |
Recipient |
Queue |
Attachments |
WelcomeMail |
User created |
Client |
Yes |
None |
BookingSubmittedMail |
Consultation created |
Client |
Yes |
None |
BookingApprovedMail |
Consultation approved |
Client |
Yes |
.ics file |
BookingRejectedMail |
Consultation rejected |
Client |
Yes |
None |
ConsultationReminderMail |
24h/2h before |
Client |
Scheduled |
.ics file |
TimelineUpdatedMail |
Timeline update added |
Client |
Yes |
None |
NewBookingRequestMail |
Consultation created |
Admin |
Yes |
None |
10.3 Email Template Example
// app/Mail/BookingApprovedMail.php
class BookingApprovedMail extends Mailable implements ShouldQueue
{
use Queueable, SerializesModels;
public function __construct(
public Consultation $consultation
) {}
public function envelope(): Envelope
{
$locale = $this->consultation->user->preferred_language;
return new Envelope(
subject: $locale === 'ar'
? 'تمت الموافقة على موعدك - مكتب ليبرا للمحاماة'
: 'Your Consultation Approved - Libra Law Firm',
);
}
public function content(): Content
{
return new Content(
markdown: 'emails.booking-approved',
with: [
'consultation' => $this->consultation,
'user' => $this->consultation->user,
'locale' => $this->consultation->user->preferred_language,
],
);
}
public function attachments(): array
{
$calendar = app(CalendarService::class);
return [
Attachment::fromData(
fn () => $calendar->generateIcs($this->consultation),
'consultation.ics'
)->withMime('text/calendar'),
];
}
}
10.4 Email Template Structure (Blade)
{{-- resources/views/emails/booking-approved.blade.php --}}
<x-mail::message>
@if($locale === 'ar')
# مرحباً {{ $user->full_name }}،
تمت الموافقة على موعد استشارتك.
**تفاصيل الموعد:**
- **التاريخ:** {{ $consultation->booking_date->format('d/m/Y') }}
- **الوقت:** {{ $consultation->booking_time->format('h:i A') }}
- **المدة:** 45 دقيقة
@if($consultation->consultation_type === 'paid')
- **نوع الاستشارة:** مدفوعة
- **المبلغ:** {{ $consultation->payment_amount }} شيكل
@else
- **نوع الاستشارة:** مجانية
@endif
يرجى إضافة الموعد إلى تقويمك باستخدام الملف المرفق.
@else
# Hello {{ $user->full_name }},
Your consultation has been approved.
**Appointment Details:**
- **Date:** {{ $consultation->booking_date->format('m/d/Y') }}
- **Time:** {{ $consultation->booking_time->format('h:i A') }}
- **Duration:** 45 minutes
@if($consultation->consultation_type === 'paid')
- **Type:** Paid Consultation
- **Amount:** {{ $consultation->payment_amount }} ILS
@else
- **Type:** Free Consultation
@endif
Please add this appointment to your calendar using the attached file.
@endif
<x-mail::button :url="route('client.consultations.index')">
{{ $locale === 'ar' ? 'عرض موعدي' : 'View My Consultation' }}
</x-mail::button>
{{ $locale === 'ar' ? 'مع تحيات،' : 'Best regards,' }}<br>
{{ config('app.name') }}
</x-mail::message>
11. Background Jobs & Scheduling
11.1 Queue Configuration
// config/queue.php
'default' => env('QUEUE_CONNECTION', 'database'),
'connections' => [
'database' => [
'driver' => 'database',
'connection' => null,
'table' => 'jobs',
'queue' => 'default',
'retry_after' => 90,
'after_commit' => true,
],
],
11.2 Queue Jobs
| Job |
Purpose |
Queue |
Retries |
Timeout |
SendWelcomeEmail |
Welcome email to new user |
emails |
3 |
60s |
SendBookingNotification |
Notify admin of new booking |
emails |
3 |
60s |
SendBookingApprovedMail |
Approval email with .ics |
emails |
3 |
60s |
SendConsultationReminder |
Reminder 24h/2h before |
reminders |
3 |
60s |
SendTimelineUpdateNotification |
Timeline update notification |
emails |
3 |
60s |
GenerateExportFile |
Generate CSV/PDF export |
exports |
1 |
300s |
11.3 Job Example
// app/Jobs/SendConsultationReminder.php
class SendConsultationReminder implements ShouldQueue
{
use Queueable;
public int $tries = 3;
public int $timeout = 60;
public string $queue = 'reminders';
public function __construct(
public Consultation $consultation,
public string $reminderType // '24h' or '2h'
) {}
public function handle(): void
{
// Don't send if consultation is no longer approved
if ($this->consultation->status !== ConsultationStatus::Approved) {
return;
}
Mail::to($this->consultation->user)
->send(new ConsultationReminderMail(
$this->consultation,
$this->reminderType
));
}
public function failed(\Throwable $exception): void
{
Log::error('Failed to send consultation reminder', [
'consultation_id' => $this->consultation->id,
'reminder_type' => $this->reminderType,
'error' => $exception->getMessage(),
]);
}
}
11.4 Scheduled Tasks
// routes/console.php
use Illuminate\Support\Facades\Schedule;
use App\Models\Consultation;
use App\Jobs\SendConsultationReminder;
use App\Enums\ConsultationStatus;
// Send 24-hour reminders at 9 AM Gaza time
Schedule::call(function () {
$tomorrow = now()->addDay()->toDateString();
Consultation::where('status', ConsultationStatus::Approved)
->where('booking_date', $tomorrow)
->each(function ($consultation) {
SendConsultationReminder::dispatch($consultation, '24h');
});
})->dailyAt('09:00')->timezone('Asia/Gaza');
// Send 2-hour reminders (check every 15 minutes)
Schedule::call(function () {
$targetTime = now()->addHours(2);
Consultation::where('status', ConsultationStatus::Approved)
->where('booking_date', $targetTime->toDateString())
->whereBetween('booking_time', [
$targetTime->subMinutes(7)->format('H:i:s'),
$targetTime->addMinutes(8)->format('H:i:s'),
])
->each(function ($consultation) {
SendConsultationReminder::dispatch($consultation, '2h');
});
})->everyFifteenMinutes()->timezone('Asia/Gaza');
// Prune old failed jobs
Schedule::command('queue:prune-failed --hours=168')->weekly();
// Clear old cache
Schedule::command('cache:prune-stale-tags')->hourly();
11.5 Supervisor Configuration
; /etc/supervisor/conf.d/libra-worker.conf
[program:libra-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/libra/artisan queue:work database --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=www-data
numprocs=2
redirect_stderr=true
stdout_logfile=/var/www/libra/storage/logs/worker.log
stopwaitsecs=3600
12. Localization & Timezone
12.1 Timezone Configuration
Application Timezone: Asia/Gaza (Palestine)
// config/app.php
'timezone' => 'Asia/Gaza',
// config/libra.php
return [
'timezone' => 'Asia/Gaza',
'date_formats' => [
'ar' => 'd/m/Y', // 21/12/2025
'en' => 'm/d/Y', // 12/21/2025
],
'time_format' => 'h:i A', // 02:30 PM (both languages)
'datetime_formats' => [
'ar' => 'd/m/Y h:i A',
'en' => 'm/d/Y h:i A',
],
];
12.2 Date/Time Helper
// app/Helpers/DateTimeHelper.php
class DateTimeHelper
{
public static function formatDate(Carbon $date, ?string $locale = null): string
{
$locale = $locale ?? app()->getLocale();
$format = config("libra.date_formats.{$locale}", 'd/m/Y');
return $date->format($format);
}
public static function formatTime(Carbon $time): string
{
return $time->format(config('libra.time_format'));
}
public static function formatDateTime(Carbon $datetime, ?string $locale = null): string
{
$locale = $locale ?? app()->getLocale();
$format = config("libra.datetime_formats.{$locale}");
return $datetime->format($format);
}
public static function parseUserDate(string $date, ?string $locale = null): Carbon
{
$locale = $locale ?? app()->getLocale();
$format = config("libra.date_formats.{$locale}");
return Carbon::createFromFormat($format, $date, config('libra.timezone'));
}
}
12.3 Localization Structure
resources/lang/
├── ar/
│ ├── auth.php # Authentication messages
│ ├── validation.php # Validation messages
│ ├── pagination.php # Pagination
│ ├── passwords.php # Password reset messages
│ ├── messages.php # General app messages
│ ├── models.php # Model field labels
│ ├── enums.php # Enum labels
│ └── emails.php # Email content
└── en/
└── ... (same structure)
12.4 RTL/LTR Support
{{-- resources/views/components/layouts/app.blade.php --}}
<!DOCTYPE html>
<html
lang="{{ app()->getLocale() }}"
dir="{{ app()->getLocale() === 'ar' ? 'rtl' : 'ltr' }}"
class="{{ app()->getLocale() === 'ar' ? 'font-arabic' : 'font-english' }}"
>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ $title ?? config('app.name') }}</title>
{{-- Fonts --}}
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Cairo:wght@300;400;600;700&family=Montserrat:wght@300;400;600;700&display=swap" rel="stylesheet">
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
/* resources/css/app.css */
@import "tailwindcss";
@theme {
--font-arabic: 'Cairo', sans-serif;
--font-english: 'Montserrat', sans-serif;
/* Brand colors - 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
// bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
$middleware->throttleApi();
})
// app/Providers/AppServiceProvider.php
public function boot(): void
{
RateLimiter::for('login', function (Request $request) {
return Limit::perMinute(5)->by($request->ip());
});
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
}
13.3 Content Security Policy
// app/Http/Middleware/ContentSecurityPolicy.php
class ContentSecurityPolicy
{
public function handle(Request $request, Closure $next): Response
{
$response = $next($request);
$response->headers->set('Content-Security-Policy', implode('; ', [
"default-src 'self'",
"script-src 'self' 'unsafe-inline' 'unsafe-eval'", // Required for Livewire
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
"font-src 'self' https://fonts.gstatic.com",
"img-src 'self' data: https:",
"connect-src 'self'",
"frame-ancestors 'none'",
]));
$response->headers->set('X-Content-Type-Options', 'nosniff');
$response->headers->set('X-Frame-Options', 'DENY');
$response->headers->set('X-XSS-Protection', '1; mode=block');
$response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
return $response;
}
}
13.4 Sensitive Data Handling
// app/Models/User.php
// Encrypt sensitive fields
protected function nationalId(): Attribute
{
return Attribute::make(
get: fn (?string $value) => $value ? decrypt($value) : null,
set: fn (?string $value) => $value ? encrypt($value) : null,
);
}
// Never expose in API/exports
protected $hidden = [
'password',
'remember_token',
'two_factor_secret',
'two_factor_recovery_codes',
'national_id', // Encrypted, don't expose
];
13.5 Audit Logging
// app/Observers/UserObserver.php
class UserObserver
{
public function created(User $user): void
{
$this->log('created', $user);
}
public function updated(User $user): void
{
if ($user->wasChanged()) {
$this->log('updated', $user, $user->getOriginal(), $user->getAttributes());
}
}
public function deleted(User $user): void
{
$this->log('deleted', $user);
}
private function log(string $action, User $user, ?array $old = null, ?array $new = null): void
{
AdminLog::create([
'admin_id' => auth()->id(),
'action' => $action,
'target_type' => User::class,
'target_id' => $user->id,
'old_values' => $old ? $this->filterSensitive($old) : null,
'new_values' => $new ? $this->filterSensitive($new) : null,
'ip_address' => request()->ip(),
]);
}
private function filterSensitive(array $values): array
{
unset(
$values['password'],
$values['remember_token'],
$values['two_factor_secret'],
$values['two_factor_recovery_codes']
);
return $values;
}
}
14. Testing Strategy
14.1 Test Organization
tests/
├── Feature/ # Integration tests
│ ├── Admin/
│ │ ├── DashboardTest.php
│ │ ├── UserManagementTest.php
│ │ ├── ConsultationManagementTest.php
│ │ ├── TimelineManagementTest.php
│ │ ├── PostManagementTest.php
│ │ └── SettingsTest.php
│ ├── Client/
│ │ ├── DashboardTest.php
│ │ ├── BookingTest.php
│ │ ├── TimelineViewTest.php
│ │ └── ProfileTest.php
│ ├── Public/
│ │ ├── HomePageTest.php
│ │ ├── 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
// tests/Feature/Client/BookingTest.php
<?php
use App\Models\User;
use App\Models\Consultation;
use App\Models\WorkingHour;
use App\Enums\ConsultationStatus;
use App\Jobs\SendBookingNotification;
use Livewire\Volt\Volt;
use Illuminate\Support\Facades\Queue;
beforeEach(function () {
// Seed working hours
WorkingHour::factory()->weekdays()->create();
});
test('client can book consultation on available date', function () {
Queue::fake();
$client = User::factory()->client()->create();
$bookingDate = now()->addDays(3)->startOfDay();
// Ensure it's a weekday
while ($bookingDate->isWeekend()) {
$bookingDate->addDay();
}
Volt::test('client.consultations.book')
->actingAs($client)
->set('form.booking_date', $bookingDate->format('Y-m-d'))
->set('form.booking_time', '10:00')
->set('form.problem_summary', 'I need legal advice regarding a contract dispute.')
->call('submit')
->assertHasNoErrors()
->assertDispatched('notify');
expect(Consultation::where('user_id', $client->id)->exists())->toBeTrue();
$consultation = Consultation::where('user_id', $client->id)->first();
expect($consultation->status)->toBe(ConsultationStatus::Pending);
expect($consultation->booking_date->format('Y-m-d'))->toBe($bookingDate->format('Y-m-d'));
Queue::assertPushed(SendBookingNotification::class);
});
test('client cannot book more than once per day', function () {
$client = User::factory()->client()->create();
$bookingDate = now()->addDays(3);
// Create existing booking
Consultation::factory()->for($client)->create([
'booking_date' => $bookingDate,
]);
Volt::test('client.consultations.book')
->actingAs($client)
->set('form.booking_date', $bookingDate->format('Y-m-d'))
->set('form.booking_time', '14:00')
->set('form.problem_summary', 'Another consultation request.')
->call('submit')
->assertHasErrors(['form.booking_date']);
expect(Consultation::where('user_id', $client->id)->count())->toBe(1);
});
test('client cannot book on blocked date', function () {
$client = User::factory()->client()->create();
$blockedDate = now()->addDays(5);
BlockedTime::factory()->create([
'block_date' => $blockedDate,
'start_time' => null, // Full day block
'end_time' => null,
]);
Volt::test('client.consultations.book')
->actingAs($client)
->set('form.booking_date', $blockedDate->format('Y-m-d'))
->set('form.booking_time', '10:00')
->set('form.problem_summary', 'Test booking.')
->call('submit')
->assertHasErrors(['form.booking_date']);
});
// tests/Feature/Admin/UserManagementTest.php
<?php
use App\Models\User;
use App\Enums\UserType;
use App\Enums\UserStatus;
use App\Jobs\SendWelcomeEmail;
use Livewire\Volt\Volt;
use Illuminate\Support\Facades\Queue;
test('admin can create individual client', function () {
Queue::fake();
$admin = User::factory()->admin()->create();
Volt::test('admin.users.create')
->actingAs($admin)
->set('form.user_type', 'individual')
->set('form.full_name', 'Ahmad Hassan')
->set('form.national_id', '123456789')
->set('form.email', 'ahmad@example.com')
->set('form.phone', '+970591234567')
->set('form.password', 'SecurePass123!')
->set('form.password_confirmation', 'SecurePass123!')
->set('form.preferred_language', 'ar')
->call('submit')
->assertHasNoErrors()
->assertRedirect(route('admin.users.index'));
$user = User::where('email', 'ahmad@example.com')->first();
expect($user)->not->toBeNull()
->and($user->user_type)->toBe(UserType::Individual)
->and($user->status)->toBe(UserStatus::Active);
Queue::assertPushed(SendWelcomeEmail::class);
});
test('admin can deactivate client', function () {
$admin = User::factory()->admin()->create();
$client = User::factory()->client()->create();
Volt::test('admin.users.edit', ['user' => $client])
->actingAs($admin)
->call('deactivate')
->assertHasNoErrors();
$client->refresh();
expect($client->status)->toBe(UserStatus::Deactivated);
});
test('admin cannot delete another admin', function () {
$admin1 = User::factory()->admin()->create();
$admin2 = User::factory()->admin()->create();
Volt::test('admin.users.edit', ['user' => $admin2])
->actingAs($admin1)
->call('delete')
->assertForbidden();
});
14.3 Coverage Targets
| Area |
Target |
Priority |
| Actions |
95% |
High |
| Models (methods) |
90% |
High |
| Policies |
100% |
High |
| Services |
90% |
High |
| Volt Components |
80% |
Medium |
| Critical user flows |
100% |
Critical |
14.4 Running Tests
# Run all tests
php artisan test
# Run specific test file
php artisan test tests/Feature/Client/BookingTest.php
# Run tests matching filter
php artisan test --filter=booking
# Run with coverage
php artisan test --coverage --min=80
# Run parallel
php artisan test --parallel
15. Performance & Optimization
15.1 Performance Targets
| Metric |
Target |
Measurement |
| Page Load (TTFB) |
< 200ms |
Server response time |
| Full Page Load |
< 3s |
Including assets |
| Database Queries |
< 20 per page |
Query monitoring |
| Memory Usage |
< 128MB |
Per request |
| Queue Job Processing |
< 5s |
For email jobs |
15.2 Database Optimization
// Eager loading in components
public function mount(): void
{
$this->consultations = Consultation::query()
->with('user:id,full_name,email,phone')
->where('status', ConsultationStatus::Pending)
->orderBy('created_at', 'desc')
->get();
}
// Chunking for large operations
User::query()
->where('status', UserStatus::Active)
->chunk(100, function ($users) {
foreach ($users as $user) {
// Process
}
});
15.3 Caching Strategy
| Cache |
TTL |
Key Pattern |
Invalidation |
| Working Hours |
1 hour |
working_hours:all |
On update |
| Blocked Times |
1 hour |
blocked_times:{month} |
On update |
| Dashboard Stats |
5 minutes |
admin:stats:{date} |
Time-based |
| Published Posts |
10 minutes |
posts:published |
On publish |
| User Permissions |
1 hour |
user:{id}:permissions |
On role change |
// app/Services/AvailabilityService.php
public function getWorkingHours(): Collection
{
return Cache::remember('working_hours:all', now()->addHour(), function () {
return WorkingHour::where('is_active', true)->get();
});
}
public function invalidateWorkingHoursCache(): void
{
Cache::forget('working_hours:all');
}
15.4 Asset Optimization
// vite.config.js
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
export default defineConfig({
plugins: [
laravel({
input: ['resources/css/app.css', 'resources/js/app.js'],
refresh: true,
}),
],
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['alpinejs'],
},
},
},
},
});
16. Deployment & CI/CD
16.1 GitHub Actions CI Pipeline
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
extensions: mbstring, xml, ctype, iconv, intl, pdo_sqlite
coverage: xdebug
- name: Get composer cache directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache composer dependencies
uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-composer-
- name: Install PHP dependencies
run: composer install --no-interaction --prefer-dist --optimize-autoloader
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install NPM dependencies
run: npm ci
- name: Build assets
run: npm run build
- name: Prepare Laravel
run: |
cp .env.example .env
php artisan key:generate
touch database/database.sqlite
- name: Run Pint (code style)
run: vendor/bin/pint --test
- name: Run tests
run: php artisan test --parallel --coverage --min=80
env:
DB_CONNECTION: sqlite
DB_DATABASE: database/database.sqlite
deploy-staging:
needs: tests
if: github.ref == 'refs/heads/develop'
runs-on: ubuntu-latest
steps:
- name: Deploy to staging
run: echo "Deploy to staging server"
# Add actual deployment steps
deploy-production:
needs: tests
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: production
steps:
- name: Deploy to production
run: echo "Deploy to production server"
# Add actual deployment steps
16.2 Deployment Script
#!/bin/bash
# deploy.sh - Production deployment script
set -e
echo "🚀 Starting deployment..."
# Variables
APP_DIR="/var/www/libra"
BACKUP_DIR="/var/www/backups"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
# Create backup
echo "📦 Creating backup..."
mysqldump -u libra_user -p libra > "${BACKUP_DIR}/libra_${TIMESTAMP}.sql"
# Pull latest code
echo "📥 Pulling latest code..."
cd $APP_DIR
git fetch origin main
git reset --hard origin/main
# Install dependencies
echo "📚 Installing dependencies..."
composer install --no-dev --optimize-autoloader --no-interaction
npm ci
npm run build
# Run migrations
echo "🗃️ Running migrations..."
php artisan migrate --force
# Clear and rebuild caches
echo "🔄 Rebuilding caches..."
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache
# Restart queue workers
echo "👷 Restarting queue workers..."
php artisan queue:restart
# Set permissions
echo "🔒 Setting permissions..."
chown -R www-data:www-data storage bootstrap/cache
chmod -R 775 storage bootstrap/cache
echo "✅ Deployment complete!"
16.3 Environment Configuration
# Production .env
APP_NAME="Libra Law Firm"
APP_ENV=production
APP_KEY=base64:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
APP_DEBUG=false
APP_TIMEZONE=Asia/Gaza
APP_URL=https://libra.ps
LOG_CHANNEL=daily
LOG_LEVEL=warning
DB_CONNECTION=mariadb
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=libra
DB_USERNAME=libra_user
DB_PASSWORD=secure_password_here
BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local
QUEUE_CONNECTION=database
SESSION_DRIVER=database
SESSION_LIFETIME=120
SESSION_SECURE_COOKIE=true
MAIL_MAILER=smtp
MAIL_HOST=smtp.libra.ps
MAIL_PORT=587
MAIL_USERNAME=no-reply@libra.ps
MAIL_PASSWORD=secure_password_here
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS=no-reply@libra.ps
MAIL_FROM_NAME="Libra Law Firm"
16.4 Server Requirements
| Component |
Requirement |
| OS |
Rocky Linux 9 |
| PHP |
8.4+ with extensions: BCMath, Ctype, Fileinfo, JSON, Mbstring, OpenSSL, PDO, PDO_MySQL, Tokenizer, XML, cURL |
| Database |
MariaDB 10.11+ |
| Web Server |
Nginx 1.24+ |
| Process Manager |
Supervisor (for queue workers) |
| Cron |
For Laravel scheduler |
| SSL |
Let's Encrypt or client-provided |
17. Monitoring & Alerting
17.1 Logging Configuration
// config/logging.php
'channels' => [
'stack' => [
'driver' => 'stack',
'channels' => ['daily', 'slack'],
'ignore_exceptions' => false,
],
'daily' => [
'driver' => 'daily',
'path' => storage_path('logs/laravel.log'),
'level' => env('LOG_LEVEL', 'debug'),
'days' => 14,
'replace_placeholders' => true,
],
'slack' => [
'driver' => 'slack',
'url' => env('LOG_SLACK_WEBHOOK_URL'),
'username' => 'Libra Alert',
'emoji' => ':boom:',
'level' => 'error',
],
'queue' => [
'driver' => 'daily',
'path' => storage_path('logs/queue.log'),
'level' => 'info',
'days' => 7,
],
'audit' => [
'driver' => 'daily',
'path' => storage_path('logs/audit.log'),
'level' => 'info',
'days' => 90,
],
],
17.2 Alerting Thresholds
| Alert |
Condition |
Channel |
Severity |
| Application Error |
Any exception |
Slack |
High |
| Failed Job |
3 consecutive failures |
Slack, Email |
High |
| High Error Rate |
> 10 errors/minute |
Slack |
Critical |
| Queue Backup |
> 100 pending jobs |
Slack |
Medium |
| Disk Space |
< 10% free |
Email |
High |
| Database Connection |
Connection failure |
Slack, Email |
Critical |
17.3 Health Check Endpoint
// routes/web.php
Route::get('/health', function () {
$checks = [
'app' => true,
'database' => false,
'cache' => false,
'queue' => false,
];
try {
DB::connection()->getPdo();
$checks['database'] = true;
} catch (\Exception $e) {
Log::error('Health check: Database connection failed', ['error' => $e->getMessage()]);
}
try {
Cache::put('health-check', true, 10);
$checks['cache'] = Cache::get('health-check') === true;
} catch (\Exception $e) {
Log::error('Health check: Cache failed', ['error' => $e->getMessage()]);
}
try {
$pendingJobs = DB::table('jobs')->count();
$checks['queue'] = $pendingJobs < 1000;
$checks['queue_pending'] = $pendingJobs;
} catch (\Exception $e) {
Log::error('Health check: Queue check failed', ['error' => $e->getMessage()]);
}
$allHealthy = !in_array(false, $checks, true);
return response()->json([
'status' => $allHealthy ? 'healthy' : 'unhealthy',
'timestamp' => now()->toIso8601String(),
'checks' => $checks,
], $allHealthy ? 200 : 503);
})->name('health');
17.4 Application Metrics
// app/Console/Commands/CollectMetrics.php
class CollectMetrics extends Command
{
protected $signature = 'metrics:collect';
protected $description = 'Collect application metrics for monitoring';
public function handle(): void
{
$metrics = [
'users' => [
'total' => User::count(),
'active' => User::where('status', UserStatus::Active)->count(),
'new_this_month' => User::where('created_at', '>=', now()->startOfMonth())->count(),
],
'consultations' => [
'pending' => Consultation::where('status', ConsultationStatus::Pending)->count(),
'today' => Consultation::where('booking_date', today())->count(),
'this_month' => Consultation::where('booking_date', '>=', now()->startOfMonth())->count(),
],
'queue' => [
'pending_jobs' => DB::table('jobs')->count(),
'failed_jobs' => DB::table('failed_jobs')->count(),
],
'system' => [
'disk_free_percent' => round((disk_free_space('/') / disk_total_space('/')) * 100, 2),
],
];
Log::channel('metrics')->info('Application metrics', $metrics);
// Alert if thresholds exceeded
if ($metrics['queue']['pending_jobs'] > 100) {
Log::channel('slack')->warning('Queue backup detected', $metrics['queue']);
}
if ($metrics['system']['disk_free_percent'] < 10) {
Log::channel('slack')->error('Low disk space', $metrics['system']);
}
}
}
// Schedule in routes/console.php
Schedule::command('metrics:collect')->everyFiveMinutes();
18. Error Handling & Graceful Degradation
18.1 Exception Handling
// bootstrap/app.php
->withExceptions(function (Exceptions $exceptions) {
// Don't report certain exceptions
$exceptions->dontReport([
\Illuminate\Auth\AuthenticationException::class,
\Illuminate\Validation\ValidationException::class,
]);
// Custom rendering for specific exceptions
$exceptions->render(function (NotFoundHttpException $e, Request $request) {
if ($request->is('admin/*')) {
return response()->view('errors.admin-404', [], 404);
}
if ($request->is('client/*')) {
return response()->view('errors.client-404', [], 404);
}
return response()->view('errors.404', [], 404);
});
$exceptions->render(function (AuthorizationException $e, Request $request) {
return response()->view('errors.403', [
'message' => $e->getMessage(),
], 403);
});
// Report to external service
$exceptions->reportable(function (\Throwable $e) {
if (app()->bound('sentry')) {
app('sentry')->captureException($e);
}
});
})
18.2 Graceful Degradation Strategy
| Failure |
Impact |
Degradation Strategy |
| Email Service Down |
Notifications not sent |
Queue emails, retry with backoff, log failures, continue operation |
| Cache Unavailable |
Slower responses |
Fallback to database queries, log warning |
| Queue Worker Down |
Delayed processing |
Jobs remain in database, process when restored |
| Database Slow |
Slow page loads |
Show cached data where possible, timeout long queries |
18.3 Email Failure Handling
// app/Jobs/SendBookingNotification.php
class SendBookingNotification implements ShouldQueue
{
use Queueable;
public int $tries = 3;
public array $backoff = [60, 300, 900]; // 1min, 5min, 15min
public function handle(): void
{
Mail::to(config('libra.admin_email'))
->send(new NewBookingRequestMail($this->consultation));
}
public function failed(\Throwable $exception): void
{
// Log the failure
Log::error('Failed to send booking notification', [
'consultation_id' => $this->consultation->id,
'attempts' => $this->attempts(),
'error' => $exception->getMessage(),
]);
// Create in-app notification as fallback
Notification::create([
'user_id' => User::where('user_type', UserType::Admin)->first()?->id,
'type' => 'booking_notification_failed',
'data' => [
'consultation_id' => $this->consultation->id,
'message' => 'Email notification failed - please check manually',
],
]);
}
}
18.4 Livewire Error Handling
// In Volt components
public function submit(): void
{
try {
$this->validate();
$result = app(CreateConsultationAction::class)->execute(
$this->user,
$this->form->toArray()
);
$this->dispatch('notify', [
'type' => 'success',
'message' => __('messages.booking_submitted'),
]);
$this->redirect(route('client.consultations.index'));
} catch (ValidationException $e) {
throw $e; // Let Livewire handle validation
} catch (BookingLimitExceededException $e) {
$this->addError('form.booking_date', $e->getMessage());
} catch (\Exception $e) {
report($e); // Log the error
$this->dispatch('notify', [
'type' => 'error',
'message' => __('messages.general_error'),
]);
}
}
19. Rollback & Recovery Procedures
19.1 Pre-Deployment Checklist
## Pre-Deployment Checklist
- [ ] Database backup completed
- [ ] Code backup/tag created
- [ ] All tests passing on CI
- [ ] Staging environment verified
- [ ] Rollback procedure reviewed
- [ ] Team notified of deployment window
19.2 Rollback Procedure
#!/bin/bash
# rollback.sh - Emergency rollback script
set -e
echo "🔄 Starting rollback..."
APP_DIR="/var/www/libra"
BACKUP_DIR="/var/www/backups"
# Get the previous commit or tag
PREVIOUS_VERSION=${1:-$(git describe --tags --abbrev=0 HEAD^)}
echo "Rolling back to: ${PREVIOUS_VERSION}"
# Step 1: Put application in maintenance mode
echo "🚧 Enabling maintenance mode..."
cd $APP_DIR
php artisan down --render="errors::503" --retry=60
# Step 2: Restore database (if needed)
read -p "Restore database backup? (y/n) " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
echo "📦 Restoring database..."
LATEST_BACKUP=$(ls -t ${BACKUP_DIR}/libra_*.sql | head -1)
mysql -u libra_user -p libra < $LATEST_BACKUP
echo "Database restored from: ${LATEST_BACKUP}"
fi
# Step 3: Rollback code
echo "⏪ Rolling back code..."
git fetch --all --tags
git checkout $PREVIOUS_VERSION
# Step 4: Install dependencies for that version
echo "📚 Installing dependencies..."
composer install --no-dev --optimize-autoloader --no-interaction
npm ci
npm run build
# Step 5: Run any rollback migrations (if applicable)
# Note: Be careful with this - may need manual intervention
# php artisan migrate:rollback --step=1
# Step 6: Clear caches
echo "🔄 Clearing caches..."
php artisan config:clear
php artisan route:clear
php artisan view:clear
php artisan cache:clear
# Step 7: Rebuild caches
echo "🔄 Rebuilding caches..."
php artisan config:cache
php artisan route:cache
php artisan view:cache
# Step 8: Restart queue workers
echo "👷 Restarting queue workers..."
php artisan queue:restart
# Step 9: Bring application back up
echo "✅ Disabling maintenance mode..."
php artisan up
echo "🎉 Rollback complete to version: ${PREVIOUS_VERSION}"
echo "⚠️ Please verify the application is working correctly!"
19.3 Database Migration Rollback
# Rollback last migration
php artisan migrate:rollback --step=1
# Rollback to specific batch
php artisan migrate:rollback --batch=5
# Check migration status
php artisan migrate:status
19.4 Recovery Scenarios
| Scenario |
Recovery Steps |
| Failed Deployment |
1. Enable maintenance mode, 2. Run rollback.sh, 3. Verify functionality |
| Database Corruption |
1. Enable maintenance mode, 2. Restore from backup, 3. Clear caches, 4. Verify data |
| Queue Stuck |
1. Clear failed jobs, 2. Restart workers, 3. Monitor processing |
| High Memory Usage |
1. Restart PHP-FPM, 2. Check for memory leaks, 3. Review recent changes |
| SSL Certificate Expired |
1. Renew certificate, 2. Restart Nginx, 3. Verify HTTPS |
19.5 Backup Strategy
| Backup Type |
Frequency |
Retention |
Storage |
| Database (full) |
Daily at 2 AM |
30 days |
/var/www/backups/ |
| Database (incremental) |
Every 6 hours |
7 days |
/var/www/backups/ |
| Application files |
Weekly |
4 weeks |
Off-site |
| User uploads |
Daily |
90 days |
Off-site |
# /etc/cron.d/libra-backups
0 2 * * * root /var/www/libra/scripts/backup-database.sh >> /var/log/libra-backup.log 2>&1
0 */6 * * * root /var/www/libra/scripts/backup-incremental.sh >> /var/log/libra-backup.log 2>&1
20. Coding Standards
20.1 Critical Rules
| Rule |
Description |
| Volt Pattern |
Use class-based Volt components (per CLAUDE.md) |
| Flux UI |
Use Flux components when available |
| Form Validation |
Always use Form Request or Livewire form objects |
| Eloquent |
Prefer Model::query() over DB:: facade |
| Actions |
Use single-purpose Action classes for business logic |
| Testing |
Every feature must have corresponding Pest tests |
| Pint |
Run vendor/bin/pint --dirty before committing |
20.2 Naming Conventions
| Element |
Convention |
Example |
| Models |
Singular PascalCase |
Consultation |
| Tables |
Plural snake_case |
consultations |
| Columns |
snake_case |
booking_date |
| Controllers |
PascalCase + Controller |
ConsultationController |
| Actions |
Verb + Noun + Action |
CreateConsultationAction |
| Jobs |
Verb + Noun |
SendBookingNotification |
| Events |
Past tense |
ConsultationApproved |
| Listeners |
Verb phrase |
SendApprovalNotification |
| Volt Components |
kebab-case path |
admin/consultations/index |
| Enums |
PascalCase |
ConsultationStatus |
| Enum Cases |
PascalCase |
NoShow |
| Traits |
Adjective or -able |
HasConsultations, Bookable |
| Interfaces |
Adjective or -able |
Exportable |
20.3 Volt Component Template
<?php
// resources/views/livewire/admin/consultations/index.blade.php
use App\Models\Consultation;
use App\Enums\ConsultationStatus;
use Livewire\Volt\Component;
use Livewire\WithPagination;
new class extends Component {
use WithPagination;
public string $search = '';
public string $status = '';
public function updatedSearch(): void
{
$this->resetPage();
}
public function updatedStatus(): void
{
$this->resetPage();
}
public function with(): array
{
return [
'consultations' => Consultation::query()
->with('user:id,full_name,email')
->when($this->search, fn ($q) => $q->whereHas('user', fn ($q) =>
$q->where('full_name', 'like', "%{$this->search}%")
))
->when($this->status, fn ($q) => $q->where('status', $this->status))
->orderBy('booking_date', 'desc')
->paginate(15),
'statuses' => ConsultationStatus::cases(),
];
}
}; ?>
<div>
<x-slot name="header">
<flux:heading size="xl">{{ __('models.consultations') }}</flux:heading>
</x-slot>
<div class="flex gap-4 mb-6">
<flux:input
wire:model.live.debounce.300ms="search"
placeholder="{{ __('messages.search') }}"
icon="magnifying-glass"
/>
<flux:select wire:model.live="status">
<option value="">{{ __('messages.all_statuses') }}</option>
@foreach($statuses as $s)
<option value="{{ $s->value }}">{{ $s->label() }}</option>
@endforeach
</flux:select>
</div>
<div class="space-y-4">
@forelse($consultations as $consultation)
<x-ui.card wire:key="consultation-{{ $consultation->id }}">
{{-- Card content --}}
</x-ui.card>
@empty
<x-ui.empty-state :message="__('messages.no_consultations')" />
@endforelse
</div>
<div class="mt-6">
{{ $consultations->links() }}
</div>
</div>
20.4 Action Class Template
<?php
// app/Actions/Consultation/CreateConsultationAction.php
namespace App\Actions\Consultation;
use App\Models\Consultation;
use App\Models\User;
use App\Enums\ConsultationStatus;
use App\Enums\PaymentStatus;
use App\Jobs\SendBookingNotification;
use App\Exceptions\BookingLimitExceededException;
use Illuminate\Support\Facades\DB;
class CreateConsultationAction
{
public function execute(User $user, array $data): Consultation
{
// Validate booking limit
if ($user->hasBookingOnDate($data['booking_date'])) {
throw new BookingLimitExceededException(
__('messages.booking_limit_exceeded')
);
}
return DB::transaction(function () use ($user, $data) {
$consultation = Consultation::create([
'user_id' => $user->id,
'booking_date' => $data['booking_date'],
'booking_time' => $data['booking_time'],
'problem_summary' => $data['problem_summary'],
'status' => ConsultationStatus::Pending,
'payment_status' => PaymentStatus::NotApplicable,
]);
SendBookingNotification::dispatch($consultation);
return $consultation;
});
}
}
21. Technology Alternatives Considered
21.1 Frontend Framework
| Option |
Pros |
Cons |
Decision |
| Livewire 3 + Volt |
No JS build, Laravel native, simple state management |
Learning curve, larger payload |
SELECTED |
| React/Vue SPA |
Rich ecosystem, familiar to many |
Separate build, API needed, complexity |
Rejected |
| Blade + Alpine only |
Simplest, no Livewire overhead |
Limited interactivity |
Considered for simpler features |
| Inertia.js |
Best of both worlds |
Additional complexity, less documentation |
Rejected |
Rationale: Livewire 3 provides excellent developer experience with Laravel, eliminates API development overhead, and Volt single-file components align well with the project's maintainability goals.
21.2 Database
| Option |
Pros |
Cons |
Decision |
| MariaDB |
MySQL compatible, open source, client preference |
- |
SELECTED (Production) |
| SQLite |
Zero config, fast tests, portable |
Not suitable for production |
SELECTED (Development) |
| PostgreSQL |
Advanced features, JSON support |
Overkill for this project |
Rejected |
| MySQL |
Industry standard |
MariaDB preferred by client |
Rejected |
21.3 Authentication
| Option |
Pros |
Cons |
Decision |
| Laravel Fortify |
Headless, flexible, 2FA support |
Requires custom views |
SELECTED |
| Laravel Breeze |
Quick setup, includes views |
Less flexible, opinionated |
Rejected |
| Laravel Jetstream |
Full-featured, team support |
Too complex, Livewire/Inertia choice |
Rejected |
| Custom auth |
Full control |
Reinventing wheel, security risks |
Rejected |
21.4 Queue System
| Option |
Pros |
Cons |
Decision |
| Database |
Simple, no extra services |
Slower than Redis |
SELECTED |
| Redis |
Fast, feature-rich |
Additional service to maintain |
Future consideration |
| Amazon SQS |
Managed, scalable |
Overkill, cost, lock-in |
Rejected |
| Beanstalkd |
Lightweight, fast |
Less Laravel integration |
Rejected |
Rationale: Database queue driver is sufficient for expected volume (~100 jobs/day). Redis can be added later if performance requires.
21.5 CSS Framework
| Option |
Pros |
Cons |
Decision |
| Tailwind CSS 4 |
Utility-first, RTL support, Flux UI compatible |
Learning curve |
SELECTED |
| Bootstrap 5 |
Familiar, RTL plugin |
Larger bundle, less flexible |
PRD mentioned but Tailwind preferred |
| Custom CSS |
Full control |
Time-consuming, maintenance burden |
Rejected |
22. Appendices
A. Flux UI Free Components
Available components in Flux UI Free edition:
avatar, badge, brand, breadcrumbs, button, callout, checkbox, dropdown,
field, heading, icon, input, modal, navbar, otp-input, profile, radio,
select, separator, skeleton, switch, text, textarea, tooltip
B. Color Palette
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
// config/libra.php
return [
'consultation' => [
'duration_minutes' => 45,
'buffer_minutes' => 15,
'slot_minutes' => 60, // duration + buffer
],
];
D. External Resources
E. Contact Information
- Admin Email: (configured in settings)
- System Email: no-reply@libra.ps
- Domain: libra.ps
END OF DOCUMENT