3179 lines
94 KiB
Markdown
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**
|