jguapin / approval-mapping
Standalone approval mapping backend and UI package for Laravel.
Requires
- php: ^8.2
- illuminate/database: ^10.0|^11.0|^12.0|^13.0
- illuminate/http: ^10.0|^11.0|^12.0|^13.0
- illuminate/routing: ^10.0|^11.0|^12.0|^13.0
- illuminate/support: ^10.0|^11.0|^12.0|^13.0
- illuminate/validation: ^10.0|^11.0|^12.0|^13.0
Requires (Dev)
- orchestra/testbench: ^8.0|^9.0|^10.0
- phpunit/phpunit: ^10.5|^11.0|^12.0
README
Standalone Laravel package for versioned approval matrices, runtime approval requests, and an admin UI.
Configure who approves what (by company / business unit / branch / department / module), version those rules, then let host models create ApprovalRequest records via HasApprovalMapping. Microservices (PO / INV / ITMS / AP) use the same matrix over HTTP — see Remote services.
A working example here: https://github.com/jagwarthegreat/map-test
Features
- Versioned approval mapping (
AMVPM→AMPMA→AMLPM) - Runtime requests + audit logs (
APPRO→ARLPE) - Sanctum JSON API for versions, lookups, matrix saves, and remote approval requests
- Blade + Alpine admin UI at
/approval-mapping(zero frontend build) - Optional Vue 3 embeddable UI (publish assets, mount in your Vite app)
- Configurable org dimensions via feature flags + host model bindings
Architecture
flowchart LR Host["Host model + HasApprovalMapping"] --> Svc["ApprovalMappingService"] Ms["Microservice HTTP"] --> ReqApi["Request API"] ReqApi --> Svc UI["Blade or Vue admin"] --> API["Version / Lookup API"] API --> VSvc["ApprovalMappingVersionService"] Svc --> AMVPM["AMVPM active version"] AMVPM --> AMPMA["AMPMA mapping row"] AMPMA --> AMLPM["AMLPM level groups"] Svc --> APPRO["APPRO request"] APPRO --> ARLPE["ARLPE logs"]Loading
| Table | Model | Role |
|---|---|---|
AMVPM |
ApprovalMappingVersion |
Versioned matrix scope (BU, company, module, dates, active) |
AMPMA |
ApprovalMapping |
One mapping row (branch, department, type, …) |
AMLPM |
ApprovalMappingLevel |
JSON level → approver group IDs |
APPRO |
ApprovalRequest |
Runtime request created from a host model or remote HTTP submit |
ARLPE |
ApprovalRequestLog |
Approval actions audit trail |
Layers follow Controller → Service → Model. Host apps never hardcode package model class names for org entities — bind them in config and resolve via ModelResolver.
Installation
composer require jguapin/approval-mapping php artisan approval-mapping:install --migrate
Optional Vue UI assets:
php artisan approval-mapping:install --with-assets
What gets published
| Tag / flag | Destination |
|---|---|
| Config | config/approval-mapping.php |
| Migrations | database/migrations/*approval_mapping* |
| Views | resources/views/vendor/approval-mapping |
--with-assets |
resources/js/vendor/approval-mapping + public/vendor/approval-mapping (CSS) |
Configuration
After install, open config/approval-mapping.php.
Feature flags
Toggle each organizational dimension. Disabled dimensions disappear from the UI and lookups return []. Nullable DB columns stay as-is.
'features' => [ 'company' => true, 'business_unit' => true, 'branch' => true, 'department' => true, // false = free-text department in matrix 'module' => true, ],
Model bindings
Only user is required. Bind the rest to your Eloquent models (or leave null).
'models' => [ 'user' => \App\Models\User::class, 'user_assign_group' => \App\Models\UserGroup::class, 'sidebar_menu' => \App\Models\Menu::class, 'module' => null, 'company' => \App\Models\Company::class, 'business_unit' => \App\Models\BusinessUnit::class, 'branch' => \App\Models\Branch::class, 'department' => \App\Models\Department::class, 'company_branch_department' => \App\Models\CompanyBranchDepartment::class, ],
Field maps
Override column names when your schema differs from package defaults:
'field_maps' => [ 'company' => ['label' => 'company_name', 'code' => 'code'], 'business_unit' => ['label' => 'name', 'code' => 'unit_code'], 'branch' => ['label' => 'name', 'code' => 'branch_code'], 'department' => ['label' => 'dept_name', 'code' => 'dept_code'], 'module' => ['label' => 'name', 'code' => 'code', 'reference' => 'reference', 'status_col' => 'status', 'status_active' => 0], 'user_assign_group' => ['label' => 'group_name'], ],
Minimal setup (no org hierarchy)
'features' => [ 'company' => false, 'business_unit' => false, 'branch' => false, 'department' => false, 'module' => false, ], 'models' => [ 'user' => \App\Models\User::class, 'user_assign_group' => \App\Models\ApproverGroup::class, ],
Gotcha: runtime
resolveMapping()still requiresbusiness_unit_idinapprovalContext()even if the feature flag is off. Pass a real BU id (or a sentinel your versions use).
Blade UI (Alpine + package CSS)
This is the built-in zero-build admin. Stack: Blade + Alpine.js 3 (CDN) + custom am-* CSS — not Tailwind.
Setup
- Run
approval-mapping:install --migrateand configure models/features. - Ensure users can authenticate via the
web+authmiddleware (defaults). - Visit
/approval-mapping(override withroute.web_prefix).
No Vite/npm required. CSS is served at /approval-mapping/assets/approval-mapping.css. The page talks to session-auth JSON under /approval-mapping/api/....
User walkthrough
- Versions list — search, filter by company / BU / module (when enabled), paginate.
- + Add new — create a version (name, org scope, effective dates, active flag, notes).
- Edit / Delete — update metadata or remove a version.
- View details — open the mapping matrix:
- Rows grouped by department (when enabled)
- Sub-rows: branch, type (
direct/agency) - Level N columns: one or more approver groups per level (
+/-) - Add level columns with the header
+
- Update — saves mappings/levels for the current version.
- Save as new version — closes/copies into a new version name + effective_from.
- Sync to module — shown when
supports_syncis true (stub response today).
Feature flags control which filters, columns, and lookups appear.
Vue UI (embeddable)
Optional Vue 3 admin with the same flows as Blade. Host apps mount it; the package does not register a separate Vue route.
Setup
php artisan approval-mapping:install --with-assets
Published paths:
- JS/Vue:
resources/js/vendor/approval-mapping/approval-mapping/ - CSS:
public/vendor/approval-mapping/approval-mapping.css
Host requirements:
vue^3.4(peer dependency)@vitejs/plugin-vueso.vueSFCs compile- Sanctum auth against
/api/v1/approval-mapping(cookie session or token)
Mount
import { mountApprovalMapping } from './vendor/approval-mapping/approval-mapping'; mountApprovalMapping('#approval-mapping', { apiBase: '/api/v1/approval-mapping', csrfToken: document.querySelector('meta[name="csrf-token"]')?.content, features: { company: true, business_unit: true, branch: true, department: true, module: true, }, });
<div id="approval-mapping"></div> <link rel="stylesheet" href="/vendor/approval-mapping/approval-mapping.css">
Pass the same feature flags you use in config so the Vue UI matches Blade.
Auth note
- Blade UI → session (
authmiddleware on web routes) - Vue UI → Sanctum API (
auth:sanctum). Use cookie-based SPA auth or bearer tokens consistently with your host app.
Runtime integration (HasApprovalMapping)
Use the trait on any Eloquent model that should enter an approval flow:
use Jguapin\ApprovalMapping\Concerns\HasApprovalMapping; class PurchaseRequest extends Model { use HasApprovalMapping; protected $fillable = [ // ... 'approval_request_id', // optional: auto-filled after submit ]; protected string $approvalModuleCode = 'PR'; public function approvalContext(): array { return [ 'company_id' => $this->company_id, 'business_unit_id' => $this->business_unit_id, // required for resolve 'branch_id' => $this->branch_id, 'type' => 'direct', // or 'agency' ]; } // optional public function approvalReferenceType(): string { return 'purchase_request'; } }
Submit:
$request = $purchaseRequest->submitForApproval(['priority' => 'high']); // ApprovalRequest|null
Flow
sequenceDiagram participant Host as HostModel participant Trait as HasApprovalMapping participant Svc as ApprovalMappingService participant AMVPM as ActiveVersion participant AMPMA as MappingPlusLevels participant APPRO as ApprovalRequest Host->>Trait: submitForApproval(extraMetadata) Trait->>Svc: createRequestFor(model, moduleCode) Svc->>Host: approvalContext() Svc->>AMVPM: active version by BU (+ company) Svc->>AMPMA: mapping by module (+ branch/type) Svc->>APPRO: create pending + level_groups_snapshot Svc-->>Host: ApprovalRequest or nullLoading
Example ApprovalRequest output
{
"id": 42,
"requester_id": 7,
"business_unit": "3",
"module": "PR",
"amount": 0,
"status": "pending",
"current_level": 1,
"mapping_version_id": 5,
"approval_mapping_id": 18,
"metadata": {
"priority": "high",
"reference_type": "PurchaseRequest",
"reference_id": 1001,
"level_groups_snapshot": {
"1": [2, 9],
"2": [4]
}
}
}
When it returns null
| Condition | Result |
|---|---|
Host has no approvalContext() |
null |
| No active version for BU (+ company) | null |
| No mapping row for module / branch / type | null |
No requester (requested_by, created_by, or Auth::id()) |
null |
Module code comes from $approvalModuleCode if set, otherwise 'DEFAULT'.
Relationship helper:
$purchaseRequest->approval_request; // BelongsTo ApprovalRequest
Remote services (HTTP)
Use the trait only when the Eloquent model lives in the same app / DB as this package.
If PO / INV / ITMS / AP records live in another service, call the host request API instead. Full walkthrough: Microservice user guide.
Short version:
- Host keeps model bindings + sidebar/menus + the matrix.
- Microservice authenticates with Sanctum (
Authorization: Bearer …). - Submit with
module,reference_type,reference_id,requester_id, and org context. - Store
approval_request_idlocally on the remote record if you need it. There is no cross-service Eloquent relation.
API
Prefix defaults to api/v1/approval-mapping with middleware api + auth:sanctum.
| Method | Path | Purpose |
|---|---|---|
GET |
/lookup/{type} |
companies, business-units, branches, departments, modules, user-assign-groups |
GET |
/versions |
Paginated versions (search, filters, per_page) |
POST |
/versions |
Create version |
GET |
/versions/{version} |
Show version |
PUT |
/versions/{version} |
Update version |
DELETE |
/versions/{version} |
Delete version |
GET |
/versions/{version}/details |
Matrix rows + level columns |
PUT |
/versions/{version}/activate |
Activate (deactivates siblings in scope) |
PUT |
/versions/{version}/mappings-levels |
Save matrix { rows: [...] } |
POST |
/versions/save-as-new |
Copy mappings into a new version |
POST |
/versions/{version}/sync-to-module |
Stub sync (returns synced_count: 0 when allowed) |
POST |
/requests/resolve |
Check active mapping for module + org context |
POST |
/requests |
Create request (idempotent while pending for the same reference) |
GET |
/requests/by-reference |
Latest request by module + reference_type + reference_id |
GET |
/requests/{id} |
Show request by id |
Web UI also exposes the same version/lookup actions under {web_prefix}/api/* with session auth. Request endpoints are Sanctum API only.
Dual UI summary
| Blade | Vue | |
|---|---|---|
| Enable | Always (package routes) | --with-assets + host mount |
| Stack | Alpine CDN + am-* CSS |
Vue 3 SFCs + same CSS |
| Auth | Session auth |
Sanctum API |
| URL | /approval-mapping |
Wherever you mount the component |
There is no config flag that switches Blade vs Vue — use Blade out of the box, or mount Vue in your SPA when you need it.