Core Module
Authentication
Core Module
The Core module provides foundational entities and authentication features required across the ERP. It includes authentication (Laravel Sanctum), authorization (Spatie Roles & Permissions), and Branch management.
ER Diagram
Module Relationship Diagram
erDiagram
USERS ||--o{ USER_BRANCHES : "can_access"
BRANCHES ||--o{ USER_BRANCHES : "assigned_to"
BRANCHES ||--o{ SHOWROOM : "has"
USERS {
bigint id PK
string name
string email
string password
string username
}
DESIGNATIONS {
bigint id PK
string designation_code "e.g., GM, SE"
string name "e.g., General Manager, Sales Executive"
text description
boolean is_active
}
COMPANY {
int id PK
string company_name
string company_logo_path
string address
string city
string country
string phone_number
string email
string website
boolean is_active
}
BRANCHES {
bigint id PK
string branch_name
string branch_code "e.g., NY01, LA02"
string address
string city
string phone_number
}
SHOWROOM {
bigint id PK
int branch_id FK
string showroom_name
string showroom_code "e.g., SR01, SR02"
string location
string contact_number
boolean is_active
}
USER_BRANCHES {
bigint user_id FK
bigint branch_id FK
bool is_default
}
COMPANIES {
int id PK
string company_name
string company_code
boolean is_active
}
COUNTRIES {
int id PK
string country_name
string iso_code
boolean is_active
}
REGIONS {
int id PK
string region_name
int country_id FK
boolean is_active
}
COLORS {
int id PK
string color_name
string hex_value
boolean is_active
}
TAXES {
int id PK
string tax_name
decimal tax_rate "e.g., 0.15 for 15%"
boolean is_active
}
MODULES {
int id PK
string name "Purchase, Sales, HR, etc."
}
APPROVAL_SETUPS {
int id PK
int module_id FK
string operation_name "e.g., Purchase Order, Invoice"
string model "Eloquent model class e.g., App\\Models\\PurchaseOrder"
bool is_active
}
APPROVAL_SETUP_STEPS {
int id PK
int approval_setup_id FK
int approver_id FK
int step_order
enum approval_type "approve, sign defines whether the step requires simple approval or digital signature"
}
APPROVAL_REQUESTS {
int id PK
int approval_setup_id FK
int record_id "ID of the business record (e.g., PO id)"
string status "pending, approved, rejected"
datetime created_at
}
APPROVAL_REQUEST_STEPS {
int id PK
int approval_request_id FK
int approver_id FK
int step_order
enum approval_type "approve, sign"
string status "pending, approved, rejected"
string signature_path "optional, for signed steps"
string reason "optional, for rejection reason"
datetime acted_at
}
MODULES ||--o{ APPROVAL_SETUPS : "has"
APPROVAL_SETUPS ||--o{ APPROVAL_SETUP_STEPS : "defines"
APPROVAL_SETUPS ||--o{ APPROVAL_REQUESTS : "triggers"
APPROVAL_REQUESTS ||--o{ APPROVAL_REQUEST_STEPS : "tracks"
USERS ||--o{ APPROVAL_SETUP_STEPS : "can be approver"
USERS ||--o{ APPROVAL_REQUEST_STEPS : "acts on"
Implementation
Approval Engine Service
namespace App\Services;
use App\Models\{
Module,
ApprovalSetup,
ApprovalSetupStep,
ApprovalRequest,
ApprovalRequestStep,
User
};
use Illuminate\Support\Facades\DB;
class ApprovalEngine
{
// ==========================
// 1. Setup Management
// ==========================
public function createSetup(int $moduleId, string $operationName, string $modelClass, array $approvers)
{
return DB::transaction(function () use ($moduleId, $operationName, $modelClass, $approvers) {
$setup = ApprovalSetup::create([
'module_id' => $moduleId,
'operation_name' => $operationName,
'model' => $modelClass,
'is_active' => true,
]);
foreach ($approvers as $index => $approverId) {
ApprovalSetupStep::create([
'approval_setup_id' => $setup->id,
'approver_id' => $approverId,
'step_order' => $index + 1,
'approval_type' => 'approve', // or 'sign' based on requirements
]);
}
return $setup->load('steps');
});
}
// ==========================
// 2. Create Approval Request
// ==========================
public function createRequest(int $approvalSetupId, int $recordId) {
return DB::transaction(function () use ($setupId, $recordId) {
// Create approval request
$request = ApprovalRequest::create([
'approval_setup_id' => $setupId,
'record_id' => $recordId,
'status' => 'pending',
'created_at' => now(),
]);
// Fetch setup steps
$setupSteps = \App\Models\ApprovalSetupStep::where('approval_setup_id', $setupId)
->orderBy('step_order')
->get();
// Create request steps
foreach ($setupSteps as $step) {
ApprovalRequestStep::create([
'approval_request_id' => $request->id,
'approver_id' => $step->approver_id,
'step_order' => $step->step_order,
'approval_type' => $step->approval_type,
'status' => 'pending',
]);
}
return $request;
});
}
/**
* Act on the next pending step of the approval request.
* Automatically moves sequentially to the next step.
*
* @param int $requestId ID of ApprovalRequest
* @param int $userId ID of the user acting
* @param string|null $signatureBase64 Signature (if required)
*/
public function actOnRequest(int $requestId, int $userId, ?string $signatureBase64 = null): ApprovalRequest
{
return DB::transaction(function () use ($requestId, $userId, $signatureBase64) {
$request = ApprovalRequest::with('steps')->findOrFail($requestId);
if ($request->status !== 'pending') {
throw new \Exception("Request already finalized.");
}
// Find the current pending step for this user
$currentStep = $request->steps()
->where('status', 'pending')
->orderBy('step_order')
->first();
if (!$currentStep) {
throw new \Exception("No pending step found.");
}
if ($currentStep->approver_id !== $userId) {
throw new \Exception("You are not authorized to approve this step.");
}
// If step requires signature
if ($currentStep->approval_type === 'sign') {
if (!$signatureBase64) {
throw new \Exception("Signature is required for this step.");
}
// Save signature as image
$signaturePath = "uploads/signatures/step_{$currentStep->id}_" . time() . ".png";
file_put_contents(public_path($signaturePath), base64_decode($signatureBase64));
$currentStep->signature_path = $signaturePath;
}
// Mark step as approved
$currentStep->status = 'approved';
$currentStep->acted_at = now();
$currentStep->save();
// Check if there is a next step
$nextStep = $request->steps()
->where('status', 'pending')
->orderBy('step_order')
->first();
// If no more steps, finalize request
if (!$nextStep) {
$request->status = 'approved';
$request->save();
// TODO: Optionally notify record owner or update main record status
}
return $request;
});
}
/**
* Reject the approval request at current step
*/
public function rejectRequest(int $requestId, int $userId, ?string $reason = null): ApprovalRequest
{
return DB::transaction(function () use ($requestId, $userId, $reason) {
$request = ApprovalRequest::with('steps')->findOrFail($requestId);
if ($request->status !== 'pending') {
throw new \Exception("Request already finalized.");
}
// Find the current pending step for this user
$currentStep = $request->steps()
->where('status', 'pending')
->orderBy('step_order')
->first();
if (!$currentStep || $currentStep->approver_id !== $userId) {
throw new \Exception("You are not authorized to reject this step.");
}
$currentStep->status = 'rejected';
$currentStep->reason = $reason;
$currentStep->acted_at = now();
$currentStep->save();
$request->status = 'rejected';
$request->save();
// TODO: Optionally notify record owner
return $request;
});
}
// ==========================
// 4. Notification (placeholder)
// ==========================
protected function notifyApprover(ApprovalRequest $request)
{
$nextStep = $request->steps()
->where('status', 'pending')
->sortBy('step_order')
->first();
if ($nextStep) {
$user = User::find($nextStep->approver_id);
// For real app, trigger notification (broadcast, email, in-app)
// Here just log
info("Approval request #{$request->id} pending for user {$user->name}");
}
}
}
Usage Example
Usage Example
$engine = new \App\Services\ApprovalEngine();
// 1. Create a request for a PO
$request = $engine->createRequest($setupId = 1, $recordId = 123);
$currentStep = $request->steps()
->where('status', 'pending')
->orderBy('step_order')
->first();
if ($currentStep->approval_type === 'sign') {
// 2. User acts on the request
$updatedRequest = $engine->actOnRequest(
$request->id,
$userId = 2,
$signatureBase64 = $signatureData // only if step type is 'sign'
);
} else {
// Render simple approve button
$updatedRequest = $engine->actOnRequest(
$request->id,
$userId = 2
);
}
// 2. User acts on the request
// 3. Reject example
$rejectedRequest = $engine->rejectRequest($request->id, $userId = 2, "Incorrect amount");
po->status = $updatedRequest->status; // 'approved' or 'rejected'
$po->save();
Related Documentation