← Back to Documentation

BDM Universal API

Developer Guide - Integration & Architecture

Architecture Overview

The BDM Universal API is a standardized framework that provides consistent API patterns, authentication, and permission management across all BDM modules.

Core Components

1

BaseApiController

Standard CRUD operations, pagination, filtering, and response formatting

2

Authentication Layer

Sanctum tokens, API keys, and guest access tokens

3

Permission System

User-to-user sharing with role-based access control

4

Guest Access System

Token-based external access with security layers

5

Logging & Monitoring

Request logging, rate limiting, and analytics

Design Principles

  • ✓ Convention over configuration
  • ✓ Consistent URL patterns across modules
  • ✓ Standard response formats
  • ✓ Built-in security and rate limiting
  • ✓ Automatic request/response logging

Installation

1. Run Migrations

php artisan migrate

2. Publish Configuration (Optional)

php artisan vendor:publish --tag=bdm-api-config

3. Install Laravel Sanctum (if not already)

composer require laravel/sanctum
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrate

4. Configure CORS (if needed)

Edit config/cors.php:

'paths' => ['api/*'],
'allowed_methods' => ['*'],
'allowed_origins' => ['*'], // Or specify your domains
'allowed_headers' => ['*'],

5. Set Environment Variables

# .env
SANCTUM_STATEFUL_DOMAINS=localhost,127.0.0.1
SESSION_DOMAIN=localhost

# API Settings
BDM_API_RATE_LIMIT_PER_MINUTE=100
BDM_API_RATE_LIMIT_PER_HOUR=5000

Project Structure

app/
├── BDMCore/                    # Universal Framework
│   ├── API/
│   │   ├── Controllers/
│   │   │   └── BaseApiController.php
│   │   ├── Middleware/
│   │   │   ├── AuthenticateApi.php
│   │   │   ├── CheckModulePermission.php
│   │   │   └── LogApiRequest.php
│   │   └── Traits/
│   │       ├── HasStandardizedApi.php
│   │       └── HasModulePermissions.php
│   │
│   ├── Services/
│   │   ├── PermissionService.php
│   │   ├── GuestAccessService.php
│   │   └── ApiLoggerService.php
│   │
│   └── Models/
│       ├── ModulePermission.php
│       ├── GuestAccessToken.php
│       └── ApiRequestLog.php
│
├── BDMAccounting/              # Example Module
│   ├── API/
│   │   └── Controllers/
│   │       └── InvoiceApiController.php
│   ├── Models/
│   │   └── Invoice.php
│   └── Routes/
│       └── api.php
│
└── BDMStockControl/            # Another Module
    └── ...same structure

💡 Pattern: Each module has its own API/Controllers directory that extends BaseApiController.

Creating a New Module

⚠️ Important: Every BDM module must follow this structure to integrate with the Universal API.

1. Create Module Directory

app/
└── BDMYourModule/
    ├── API/
    │   └── Controllers/
    ├── Models/
    ├── Services/
    └── Routes/
        └── api.php

2. Create Permission Configuration

Create config/modules/bdm_yourmodule_permissions.php:

<?php

return [
    'roles' => [
        'owner' => [
            'label' => 'Owner',
            'permissions' => ['*'],
        ],
        'admin' => [
            'label' => 'Admin',
            'permissions' => [
                'products.view',
                'products.create',
                'products.edit',
                'products.delete',
                'reports.view',
            ],
        ],
        'manager' => [
            'label' => 'Manager',
            'permissions' => [
                'products.view',
                'products.create',
                'products.edit',
                'reports.view',
            ],
        ],
        'staff' => [
            'label' => 'Staff',
            'permissions' => [
                'products.view',
                'products.create',
            ],
        ],
        'viewer' => [
            'label' => 'Viewer',
            'permissions' => [
                'products.view',
            ],
        ],
    ],
];

3. Create API Configuration

Create config/modules/bdm_yourmodule_api.php:

<?php

return [
    'searchable_fields' => ['name', 'sku', 'description'],
    'sortable_fields' => ['name', 'price', 'created_at'],
    'filterable_fields' => ['category', 'status', 'brand'],
    'default_per_page' => 15,
    'max_per_page' => 100,
];

4. Update Model

Add trait to your model:

<?php

namespace App\BDMYourModule\Models;

use Illuminate\Database\Eloquent\Model;
use App\BDMCore\API\Traits\HasModulePermissions;

class Product extends Model
{
    use HasModulePermissions;

    protected $table = 'bdm_yourmodule_products';
    
    protected $fillable = [
        'name',
        'sku',
        'price',
        'user_id', // Important: Owner field
    ];

    // Define module slug
    public function getModuleSlug(): string
    {
        return 'yourmodule';
    }
}

5. Create API Controller

See API Controller section below for details.

6. Register Routes

Create app/BDMYourModule/Routes/api.php:

<?php

use Illuminate\Support\Facades\Route;
use App\BDMYourModule\API\Controllers\ProductApiController;

Route::prefix('v1/yourmodule')
    ->middleware(['auth:sanctum', 'log.api'])
    ->group(function () {
        Route::apiResource('products', ProductApiController::class);
    });

Then load in routes/api.php:

require app_path('BDMYourModule/Routes/api.php');

Creating an API Controller

Basic Controller

<?php

namespace App\BDMYourModule\API\Controllers;

use App\BDMCore\API\Controllers\BaseApiController;
use App\BDMYourModule\Models\Product;
use Illuminate\Http\Request;

class ProductApiController extends BaseApiController
{
    protected $model = Product::class;
    protected $moduleSlug = 'yourmodule';
    
    /**
     * Validation rules for create
     */
    protected function createRules(): array
    {
        return [
            'name' => 'required|string|max:255',
            'sku' => 'required|string|unique:bdm_yourmodule_products',
            'price' => 'required|numeric|min:0',
        ];
    }
    
    /**
     * Validation rules for update
     */
    protected function updateRules(): array
    {
        return [
            'name' => 'sometimes|string|max:255',
            'sku' => 'sometimes|string',
            'price' => 'sometimes|numeric|min:0',
        ];
    }
}

✓ That's it! By extending BaseApiController, you automatically get:

  • index() - List with pagination/filtering
  • store() - Create new record
  • show($id) - Get single record
  • update($id) - Update record
  • destroy($id) - Delete record
  • • Permission checking
  • • Request logging
  • • Standard responses

Adding Custom Endpoints

/**
 * Custom action: Mark product as featured
 */
public function markFeatured(Request $request, $id)
{
    // Check permission
    if (!$this->canPerformAction('products.edit')) {
        return $this->errorResponse('Unauthorized', 403);
    }
    
    $product = $this->getModelQuery()->findOrFail($id);
    $product->update(['is_featured' => true]);
    
    return $this->successResponse(
        $product,
        'Product marked as featured'
    );
}

Customizing Query Scope

/**
 * Customize base query
 */
protected function getModelQuery()
{
    return parent::getModelQuery()
        ->with(['category', 'supplier']) // Eager load
        ->where('status', 'active');     // Filter
}

Working with Permissions

Checking Permissions in Controller

public function someAction(Request $request)
{
    // Check if user can perform action
    if (!$this->canPerformAction('products.delete')) {
        return $this->errorResponse('Unauthorized', 403);
    }
    
    // Your logic here
}

Using Permission Service

use App\BDMCore\Services\PermissionService;

public function index(PermissionService $permissionService)
{
    $user = auth()->user();
    
    // Check if user has access to module
    if (!$permissionService->hasAccess($user, 'yourmodule')) {
        return $this->errorResponse('No access', 403);
    }
    
    // Get effective user ID (owner if accessing via shared permission)
    $effectiveUserId = $permissionService->getEffectiveUserId(
        $user, 
        'yourmodule'
    );
    
    // Query using effective user ID
    $products = Product::where('user_id', $effectiveUserId)->get();
    
    return $this->successResponse($products);
}

Sharing Module Access Programmatically

use App\BDMCore\Services\PermissionService;
use App\BDMCore\Enums\PermissionRole;

$permissionService = app(PermissionService::class);

$permission = $permissionService->shareModule(
    owner: auth()->user(),
    sharedWithEmail: 'colleague@example.com',
    moduleSlug: 'yourmodule',
    role: PermissionRole::MANAGER,
    expiresAt: now()->addMonths(6)
);

Middleware Usage

// In routes/api.php
Route::middleware(['auth:sanctum', 'module.permission:yourmodule'])
    ->group(function () {
        Route::apiResource('products', ProductApiController::class);
    });

Implementing Guest Access

Creating Guest Access

use App\BDMCore\Services\GuestAccessService;

public function createGuestAccess(Request $request, $id)
{
    $product = Product::findOrFail($id);
    
    $guestAccessService = app(GuestAccessService::class);
    
    $token = $guestAccessService->createToken(
        user: auth()->user(),
        resourceType: 'yourmodule_product',
        resourceId: $product->id,
        guestEmail: $request->guest_email,
        guestName: $request->guest_name,
        permissions: ['view', 'download'],
        expiresAt: now()->addDays(30),
        requiresPassword: true,
        password: $request->password
    );
    
    // Send email to guest
    Mail::to($request->guest_email)->send(
        new GuestAccessEmail($token)
    );
    
    return $this->successResponse([
        'token' => $token->token,
        'link' => route('guest.access', $token->token)
    ]);
}

Creating Guest Access Handler

Create app/BDMCore/GuestAccess/Handlers/YourModuleProductHandler.php:

<?php

namespace App\BDMCore\GuestAccess\Handlers;

use App\BDMCore\GuestAccess\Contracts\GuestAccessHandler;
use App\BDMCore\Models\GuestAccessToken;
use App\BDMYourModule\Models\Product;

class YourModuleProductHandler implements GuestAccessHandler
{
    public function handle(GuestAccessToken $token): array
    {
        $product = Product::findOrFail($token->resource_id);
        
        // Mask sensitive data
        return [
            'id' => $product->id,
            'name' => $product->name,
            'price' => $product->price,
            // Don't include cost, supplier_id, etc.
        ];
    }
    
    public function generateDownload(GuestAccessToken $token): string
    {
        $product = Product::findOrFail($token->resource_id);
        
        // Generate PDF
        $pdf = PDF::loadView('products.pdf', compact('product'));
        
        return $pdf->download('product-' . $product->id . '.pdf');
    }
    
    public function executeAction(
        GuestAccessToken $token, 
        string $action, 
        array $data
    ): array {
        if ($action === 'request_quote') {
            // Handle quote request
            // ...
            
            return ['success' => true, 'message' => 'Quote requested'];
        }
        
        throw new \Exception('Action not allowed');
    }
}

Register Handler

In app/Providers/AppServiceProvider.php:

use App\BDMCore\Services\GuestAccessService;
use App\BDMCore\GuestAccess\Handlers\YourModuleProductHandler;

public function boot()
{
    GuestAccessService::registerHandler(
        'yourmodule_product',
        YourModuleProductHandler::class
    );
}

Triggering Webhooks

Dispatch Webhook Event

use App\BDMCore\Services\WebhookService;

public function store(Request $request)
{
    $product = Product::create($request->validated());
    
    // Trigger webhook
    app(WebhookService::class)->dispatch(
        event: 'product.created',
        payload: [
            'product_id' => $product->id,
            'name' => $product->name,
            'price' => $product->price,
        ],
        userId: auth()->id()
    );
    
    return $this->successResponse($product, 'Product created');
}

Available Events

Define webhook events in your module config:

// config/modules/bdm_yourmodule_webhooks.php
return [
    'events' => [
        'product.created' => 'Product Created',
        'product.updated' => 'Product Updated',
        'product.deleted' => 'Product Deleted',
        'order.placed' => 'Order Placed',
        'order.shipped' => 'Order Shipped',
    ],
];

Testing Your API

Feature Test Example

<?php

namespace Tests\Feature;

use Tests\TestCase;
use App\Models\User;
use App\BDMYourModule\Models\Product;
use Illuminate\Foundation\Testing\RefreshDatabase;

class ProductApiTest extends TestCase
{
    use RefreshDatabase;

    public function test_can_list_products()
    {
        $user = User::factory()->create();
        Product::factory()->count(5)->create(['user_id' => $user->id]);

        $response = $this->actingAs($user, 'sanctum')
            ->getJson('/api/v1/yourmodule/products');

        $response->assertOk()
            ->assertJsonStructure([
                'success',
                'data' => [
                    '*' => ['id', 'name', 'price']
                ]
            ]);
    }

    public function test_can_create_product()
    {
        $user = User::factory()->create();

        $response = $this->actingAs($user, 'sanctum')
            ->postJson('/api/v1/yourmodule/products', [
                'name' => 'Test Product',
                'sku' => 'TEST-001',
                'price' => 99.99
            ]);

        $response->assertCreated()
            ->assertJson([
                'success' => true,
                'data' => ['name' => 'Test Product']
            ]);

        $this->assertDatabaseHas('bdm_yourmodule_products', [
            'name' => 'Test Product'
        ]);
    }

    public function test_cannot_access_other_users_products()
    {
        $user1 = User::factory()->create();
        $user2 = User::factory()->create();
        
        $product = Product::factory()->create(['user_id' => $user1->id]);

        $response = $this->actingAs($user2, 'sanctum')
            ->getJson("/api/v1/yourmodule/products/{$product->id}");

        $response->assertNotFound();
    }
}

Running Tests

# Run all tests
php artisan test

# Run specific test file
php artisan test tests/Feature/ProductApiTest.php

# Run with coverage
php artisan test --coverage

Troubleshooting

401 Unauthorized

Cause: Missing or invalid authentication token

Solution:

  • • Ensure token is included: Authorization: Bearer TOKEN
  • • Check token hasn't expired
  • • Verify Sanctum is properly configured

403 Forbidden

Cause: User doesn't have permission

Solution:

  • • Check user has access to module
  • • Verify role has required permission
  • • Check permission config file

429 Too Many Requests

Cause: Rate limit exceeded

Solution:

  • • Wait for rate limit to reset (check X-RateLimit-Reset header)
  • • Reduce request frequency
  • • Request higher rate limit if needed

CORS Errors

Cause: Cross-origin request blocked

Solution:

  • • Add origin to config/cors.php
  • • Ensure API routes use api middleware group
  • • Check preflight OPTIONS requests are allowed

Debugging Tips

# Enable query logging
DB::enableQueryLog();
// ... your code
dd(DB::getQueryLog());

# Check API logs
tail -f storage/logs/laravel.log

# View request logs in database
SELECT * FROM bdm_api_request_logs 
WHERE user_id = ? 
ORDER BY created_at DESC 
LIMIT 50;

Quick Reference

Useful Commands

Run migrations: php artisan migrate
Run tests: php artisan test
Clear cache: php artisan cache:clear

Important Files

config/bdm_api.php

config/modules/*_permissions.php

app/BDMCore/API/Controllers/BaseApiController.php