# Modular Architecture Developer Onboarding

This project is a Laravel 12 application organized around a small core plus business modules. The goal is to keep shared platform concerns in `src/Core` while each domain area lives under `src/Modules/<ModuleName>`.

## Mental Model

`src/Core` is the platform layer. It owns authentication screens, organization switching, shared layouts, global middleware-facing routes, tenancy models, users, branches, shared database seeders, and global UI assets.

`src/Modules` contains feature modules such as:

- `Inventory`
- `Pos`
- `Hr`
- `Procurement`
- `Finance`

Each module is a Laravel-style vertical slice: routes, controllers, models, services, views, migrations, seeders, and sometimes console commands are colocated inside the module folder.

## Autoloading

Composer maps the project namespaces like this:

```json
"App\\": "src/Core/Application/",
"Core\\": "src/Core/",
"Modules\\": "src/Modules/",
"Database\\Factories\\": "src/Core/Database/Factories/",
"Database\\Seeders\\": "src/Core/Database/Seeders/"
```

That means a class at:

```text
src/Modules/Pos/Http/Controllers/PosController.php
```

uses:

```php
namespace Modules\Pos\Http\Controllers;
```

After adding new classes or namespaces, run:

```bash
composer dump-autoload
```

## Bootstrapping

Laravel loads service providers from `bootstrap/providers.php`.

Current module providers are registered there:

```php
Modules\Inventory\Providers\InventoryServiceProvider::class,
Modules\Pos\Providers\PosServiceProvider::class,
Modules\Hr\Providers\HrServiceProvider::class,
Modules\Procurement\Providers\ProcurementServiceProvider::class,
Modules\Finance\Providers\FinanceServiceProvider::class,
```

A module is not fully active until its provider is added to this file.

## Core Layer

Important core locations:

```text
src/Core/Application/        App namespace controllers, requests, providers, view components
src/Core/Routes/             Core web, auth, api, console routes
src/Core/Resources/views/    Shared layouts, auth views, components
src/Core/Database/           Core migrations, factories, seeders
src/Core/Tenancy/            Institution and organization selection concerns
src/Core/Branches/           Branch model and branch-level relationships
src/Core/Users/              User model and user middleware
src/Core/Providers/          Core service provider
```

`CoreServiceProvider` currently:

- loads core migrations from `src/Core/Database/Migrations`
- adds `src/Core/Resources/views` as a global Blade view location
- registers global view composers for inventory categories, guarded by `Schema::hasTable('categories')`

## Module Shape

A typical module looks like this:

```text
src/Modules/Example/
  Database/
    Migrations/
    Seeders/
  Http/
    Controllers/
    Requests/
    Middleware/
  Models/
  Providers/
    ExampleServiceProvider.php
  Resources/
    views/
  Routes/
    web.php
    api.php
  Services/
  Actions/
  Console/
```

Not every folder is required. Keep the module as small as the feature needs.

## Service Providers

Module providers are the module entry point. They usually load:

- migrations
- routes
- views
- Blade directives
- container bindings
- console commands or schedules

Minimal provider:

```php
<?php

namespace Modules\Example\Providers;

use Illuminate\Support\ServiceProvider;

class ExampleServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        // Bind interfaces or singleton services here.
    }

    public function boot(): void
    {
        $this->loadMigrationsFrom(__DIR__.'/../Database/Migrations');
        $this->loadRoutesFrom(__DIR__.'/../Routes/web.php');
        $this->loadViewsFrom(__DIR__.'/../Resources/views', 'example');
    }
}
```

If the module exposes API routes, follow the Inventory pattern:

```php
$this->app['router']->middleware('api')
    ->prefix('api')
    ->group(__DIR__.'/../Routes/api.php');
```

## Routes

Core routes live in `src/Core/Routes/web.php`.

Module web routes live in `src/Modules/<Module>/Routes/web.php` and are loaded by the module provider.

Common module route pattern:

```php
Route::middleware(['web', 'auth', 'organization'])
    ->prefix('example')
    ->name('example.')
    ->group(function () {
        Route::get('/', DashboardController::class)->name('dashboard');
    });
```

Conventions:

- Use a module URL prefix: `/pos`, `/hr`, `/finance`
- Use a route name prefix: `pos.`, `hr.`, `finance.`
- Use `organization` middleware for institution-scoped modules
- Add module-specific middleware only when the module has extra access rules

POS example:

```php
Route::middleware(['web', 'auth', 'organization', CheckPosBranchAccess::class])
    ->prefix('pos')
    ->name('pos.')
    ->group(function () {
        Route::get('/', [PosController::class, 'index'])->name('dashboard');
    });
```

## Views

Most modules register a namespaced view path:

```php
$this->loadViewsFrom(__DIR__.'/../Resources/views', 'pos');
```

Use namespaced view references:

```php
return view('pos::dashboard');
return view('finance::invoices.show', compact('invoice'));
```

Inventory also calls `View::addLocation($views)`, which allows non-namespaced view lookup from that module. Prefer namespaced views for new modules because they make ownership obvious.

Shared layouts and components are in:

```text
src/Core/Resources/views
```

## Models

Module models live in:

```text
src/Modules/<Module>/Models
```

Example:

```php
namespace Modules\Finance\Models;

use Illuminate\Database\Eloquent\Model;

class FinanceInvoice extends Model
{
    protected $guarded = [];
}
```

Cross-module relationships are allowed, but keep them intentional. For example, POS sale items can belong to Inventory items, and Finance can reference shared institutions or users.

## Services And Actions

Use `Services/` for reusable domain operations and context helpers.

Examples:

- `Modules\Pos\Services\PosContext`
- `Modules\Hr\Services\HrContext`
- `Modules\Finance\Services\FinancePostingService`
- `Modules\Inventory\Services\StockOperationsService`

Use `Actions/` when the operation is a concrete workflow, especially if it is called from a controller and should stay testable.

Examples:

- `Modules\Pos\Actions\CreatePosSale`
- `Modules\Pos\Actions\ClosePosSession`

## Migrations

Module migrations live in:

```text
src/Modules/<Module>/Database/Migrations
```

The module provider loads them with:

```php
$this->loadMigrationsFrom(__DIR__.'/../Database/Migrations');
```

Guidelines:

- Prefix module tables, for example `hr_employees`, `pos_sales`, `finance_invoices`
- Use explicit short index names for long table or column combinations
- MySQL index and constraint identifiers are limited to 64 characters

Good:

```php
$table->index(['employee_id', 'starts_on', 'ends_on'], 'hr_emp_branch_dates_idx');
```

Risky:

```php
$table->index(['employee_id', 'starts_on', 'ends_on']);
```

Laravel will generate a long name from the table and columns, which can exceed MySQL limits.

## Seeders And Permissions

Core seeding starts from:

```text
src/Core/Database/Seeders/DatabaseSeeder.php
```

Module permission seeders are called there:

```php
PosPermissionSeeder::class,
HrPermissionSeeder::class,
ProcurementPermissionSeeder::class,
FinancePermissionSeeder::class,
```

Permission naming styles currently include both:

```text
pos.create-sale
hr.employee.view
finance.invoice.issue
inventory.stock.transfer
```

When adding a new module:

- create a module permission seeder if the module has its own permissions
- use `Permission::findOrCreate($name, 'web')`
- assign permissions to existing roles where appropriate
- add the seeder to core `DatabaseSeeder`

For cross-cutting core roles, update `InstitutionRolePermissionSeeder` only when the permission belongs to the core role catalog. For module-specific permissions, prefer a module seeder.

## Authorization Contexts

Some modules use a context service to centralize access checks:

- `PosContext`
- `HrContext`
- `FinanceContext`
- `ProcurementContext`

This pattern keeps controllers thin and makes access behavior consistent.

Typical responsibilities:

- resolve current institution and branch from session
- check whether the signed-in user can access the module
- check permission names
- provide helpers for Blade directives or middleware

Provider-level Blade directives can wrap these checks:

```php
Blade::if('posAccess', fn () => app(PosContext::class)->canAccess());
Blade::if('posPermission', fn (string $permission) => app(PosContext::class)->canPerform($permission));
```

## Navigation And Landing

The main dashboard route in `src/Core/Routes/web.php` redirects users to a landing page:

```php
Route::get('/dashboard', function (\Modules\Pos\Services\ResolveUserLandingPage $landing) {
    return redirect()->to($landing->url(request()->user()));
})->name('dashboard');
```

If a new module should become a default landing destination for certain roles, update the landing resolution service.

Shared tabs/sidebar/top navigation live under `src/Core/Resources/views/components` and related layout files. Add module navigation there when exposing a new user-facing module.

## Adding A New Module

Assume the module is named `Clinic`.

### 1. Create the directory

```text
src/Modules/Clinic/
  Database/Migrations/
  Database/Seeders/
  Http/Controllers/
  Models/
  Providers/
  Resources/views/
  Routes/
  Services/
```

### 2. Add a service provider

Create:

```text
src/Modules/Clinic/Providers/ClinicServiceProvider.php
```

```php
<?php

namespace Modules\Clinic\Providers;

use Illuminate\Support\ServiceProvider;

class ClinicServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        //
    }

    public function boot(): void
    {
        $this->loadMigrationsFrom(__DIR__.'/../Database/Migrations');
        $this->loadRoutesFrom(__DIR__.'/../Routes/web.php');
        $this->loadViewsFrom(__DIR__.'/../Resources/views', 'clinic');
    }
}
```

### 3. Register the provider

Add it to `bootstrap/providers.php`:

```php
Modules\Clinic\Providers\ClinicServiceProvider::class,
```

### 4. Add routes

Create:

```text
src/Modules/Clinic/Routes/web.php
```

```php
<?php

use Illuminate\Support\Facades\Route;
use Modules\Clinic\Http\Controllers\DashboardController;

Route::middleware(['web', 'auth', 'organization'])
    ->prefix('clinic')
    ->name('clinic.')
    ->group(function () {
        Route::get('/', DashboardController::class)->name('dashboard');
    });
```

### 5. Add a controller

```text
src/Modules/Clinic/Http/Controllers/DashboardController.php
```

```php
<?php

namespace Modules\Clinic\Http\Controllers;

use Illuminate\Routing\Controller;

class DashboardController extends Controller
{
    public function __invoke()
    {
        return view('clinic::dashboard');
    }
}
```

### 6. Add a view

```text
src/Modules/Clinic/Resources/views/dashboard.blade.php
```

```blade
@extends('layouts.app')

@section('title', 'Clinic')
@section('toptalk', 'Clinic')
@section('bottomtalk', 'Clinic workspace')

@section('content')
    <div class="bg-white rounded-lg shadow p-6">
        <h1 class="text-xl font-semibold">Clinic module</h1>
    </div>
@endsection
```

### 7. Add migrations

Create migration files in:

```text
src/Modules/Clinic/Database/Migrations
```

Example:

```php
Schema::create('clinic_visits', function (Blueprint $table) {
    $table->id();
    $table->foreignId('institution_id')->constrained()->cascadeOnDelete();
    $table->string('visit_number');
    $table->timestamps();

    $table->unique(['institution_id', 'visit_number'], 'clinic_visit_number_unique');
});
```

Run:

```bash
php artisan migrate
```

### 8. Add permissions

Create:

```text
src/Modules/Clinic/Database/Seeders/ClinicPermissionSeeder.php
```

```php
<?php

namespace Modules\Clinic\Database\Seeders;

use Illuminate\Database\Seeder;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;

class ClinicPermissionSeeder extends Seeder
{
    public const PERMISSIONS = [
        'clinic.dashboard.view',
        'clinic.visit.view',
        'clinic.visit.create',
        'clinic.visit.update',
    ];

    public function run(): void
    {
        foreach (self::PERMISSIONS as $permission) {
            Permission::findOrCreate($permission, 'web');
        }

        Role::whereIn('name', ['Institution Owner', 'Institution Admin', 'administrator'])
            ->get()
            ->each
            ->givePermissionTo(self::PERMISSIONS);
    }
}
```

Then add it to `src/Core/Database/Seeders/DatabaseSeeder.php`:

```php
use Modules\Clinic\Database\Seeders\ClinicPermissionSeeder;

// ...

$this->call([
    // ...
    ClinicPermissionSeeder::class,
]);
```

Run:

```bash
php artisan db:seed --class=Database\\Seeders\\DatabaseSeeder
```

### 9. Add navigation

Add a link in the shared navigation components under:

```text
src/Core/Resources/views/components
```

Use named routes:

```blade
route('clinic.dashboard')
```

Gate the link with permissions when needed:

```blade
@can('clinic.dashboard.view')
    ...
@endcan
```

### 10. Add tests

Feature tests live under:

```text
tests/Feature
```

At minimum, test:

- module dashboard loads for an authorized user
- unauthorized users are blocked
- migrations support `RefreshDatabase`
- key workflows are covered end to end

Run:

```bash
php artisan test
```

## Development Checklist

Use this checklist when adding or changing a module:

- Module namespace starts with `Modules\<ModuleName>`
- Provider exists and is registered in `bootstrap/providers.php`
- Provider loads migrations, routes, and views
- Routes use module prefix and route-name prefix
- Views use the module namespace, for example `clinic::dashboard`
- Tables are prefixed with a module identifier
- Long indexes and constraints have explicit short names
- Permissions are created through a seeder
- Module seeder is called from core `DatabaseSeeder`
- Navigation links are added to shared UI if user-facing
- Tests cover access and the main workflow
- `composer dump-autoload`, `php artisan migrate`, and `php artisan test` pass

## Common Pitfalls

### Provider not registered

Symptoms:

- routes are missing
- migrations do not run
- views cannot be found

Fix: add the provider to `bootstrap/providers.php`.

### View namespace mismatch

If provider registers:

```php
$this->loadViewsFrom(..., 'clinic');
```

Use:

```php
view('clinic::dashboard')
```

### Overlong MySQL names

Laravel auto-generates index names from table and column names. With long module table names, this can exceed MySQL's 64-character identifier limit.

Fix: pass explicit names to `index`, `unique`, and foreign constraints when needed.

### Seeding duplicate data

Seeders should usually be idempotent. Prefer:

```php
Model::updateOrCreate([...], [...]);
```

or `upsert` for bulk data.

### Cross-module coupling

It is fine for modules to depend on shared core models and stable domain services. Avoid reaching deep into another module's controller or view internals. Prefer services, actions, events, or Eloquent relationships.

