← Back to Documentation

BDM Universal API

Migration Guide - Upgrading Existing Modules

Migration Overview

This guide helps you migrate existing BDM modules to use the Universal API framework. The migration process maintains backward compatibility while adding new features.

✅ What You'll Gain

  • ✓ Standardized API endpoints
  • ✓ Built-in authentication
  • ✓ Permission system
  • ✓ Guest access capability
  • ✓ Rate limiting
  • ✓ Request logging
  • ✓ Webhook support
  • ✓ Consistent responses

📦 What Stays the Same

  • ✓ Existing database tables
  • ✓ Business logic
  • ✓ Web controllers
  • ✓ Frontend views
  • ✓ Model relationships
  • ✓ Validation rules

⏱️ Estimated Time

Small Module (1-3 models): 2-4 hours
Medium Module (4-8 models): 1-2 days
Large Module (9+ models): 3-5 days

Preparation

1. Backup Everything

# Backup database
mysqldump -u username -p database_name > backup_$(date +%Y%m%d).sql

# Backup code
git checkout -b migration-universal-api
git commit -am "Pre-migration checkpoint"

# Tag current state
git tag pre-universal-api-migration

2. Review Current API (if exists)

  • • Document existing endpoints
  • • List all request/response formats
  • • Note any custom authentication
  • • Identify API consumers (mobile apps, integrations)
  • • Check for hardcoded URLs in frontend

3. Analyze Module Structure

Identify:

  • ✓ Main models and relationships
  • ✓ Existing controllers (web and API if any)
  • ✓ Routes (web.php, api.php)
  • ✓ Validation rules
  • ✓ Custom business logic
  • ✓ Event listeners

4. Set Up Testing Environment

# Create test database
CREATE DATABASE bdm_test;

# Copy .env to .env.testing
cp .env .env.testing

# Update test database credentials
DB_DATABASE=bdm_test

Migration Checklist

Create configuration files

config/modules/bdm_[module]_permissions.php

config/modules/bdm_[module]_api.php

Update models

Add HasModulePermissions trait

Add user_id if missing

Create API controllers

Extend BaseApiController

Implement validation rules

Update routes

Create app/BDM[Module]/Routes/api.php

Register in routes/api.php

Create guest access handlers

For resources that need external access

Write tests

Feature tests for all endpoints

Permission tests

Update frontend (if needed)

Change API endpoints

Update response handling

Documentation

Update API documentation

Document breaking changes

Deploy

Run migrations (if any new tables)

Clear cache

Test in production

Step-by-Step Migration Guide

1

Create Permission Configuration

Create config/modules/bdm_accounting_permissions.php

<?php

return [
    'roles' => [
        'owner' => [
            'label' => 'Owner',
            'permissions' => ['*'],
        ],
        'admin' => [
            'label' => 'Administrator',
            'permissions' => [
                'invoices.view',
                'invoices.create',
                'invoices.edit',
                'invoices.delete',
                // ... add all permissions
            ],
        ],
        // ... other roles
    ],
    
    'guest_access' => [
        'invoice' => [
            'default_permissions' => ['view', 'download', 'pay'],
            'default_expiry_days' => 30,
        ],
    ],
];
2

Create API Configuration

Create config/modules/bdm_accounting_api.php

<?php

return [
    'searchable_fields' => [
        'invoices' => ['number', 'customer_name', 'customer_email'],
    ],
    
    'sortable_fields' => [
        'invoices' => ['number', 'total', 'due_date', 'created_at'],
    ],
    
    'filterable_fields' => [
        'invoices' => ['status', 'customer_id'],
    ],
    
    'webhook_events' => [
        'invoice.created' => 'Invoice Created',
        'invoice.paid' => 'Invoice Paid',
    ],
];
3

Update Model

Add trait to existing model:

<?php

namespace App\BDMAccounting\Models;

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

class Invoice extends Model
{
    use HasModulePermissions;

    protected $table = 'bdm_accounting_invoices';
    
    protected $fillable = [
        'number',
        'customer_name',
        'total',
        'user_id', // Add if missing
    ];

    public function getModuleSlug(): string
    {
        return 'accounting';
    }
}

⚠️ Migration Note: If your table doesn't have user_id, create a migration to add it.

4

Create API Controller

Create app/BDMAccounting/API/Controllers/InvoiceApiController.php

<?php

namespace App\BDMAccounting\API\Controllers;

use App\BDMCore\API\Controllers\BaseApiController;
use App\BDMAccounting\Models\Invoice;
use Illuminate\Http\Request;

class InvoiceApiController extends BaseApiController
{
    protected $model = Invoice::class;
    protected $moduleSlug = 'accounting';
    
    protected function createRules(): array
    {
        return [
            'customer_name' => 'required|string|max:255',
            'customer_email' => 'required|email',
            'total' => 'required|numeric|min:0',
        ];
    }
    
    protected function updateRules(): array
    {
        return [
            'customer_name' => 'sometimes|string|max:255',
            'customer_email' => 'sometimes|email',
            'total' => 'sometimes|numeric|min:0',
        ];
    }
    
    // Add custom endpoints if needed
    public function sendInvoice(Request $request, $id)
    {
        if (!$this->canPerformAction('invoices.send')) {
            return $this->errorResponse('Unauthorized', 403);
        }
        
        $invoice = $this->getModelQuery()->findOrFail($id);
        
        // Your send logic here
        
        return $this->successResponse($invoice, 'Invoice sent');
    }
}
5

Create Routes File

Create app/BDMAccounting/Routes/api.php

<?php

use Illuminate\Support\Facades\Route;
use App\BDMAccounting\API\Controllers\InvoiceApiController;

Route::prefix('v1/accounting')
    ->middleware(['auth:sanctum', 'log.api'])
    ->group(function () {
        
        // Standard REST endpoints
        Route::apiResource('invoices', InvoiceApiController::class);
        
        // Custom actions
        Route::post('invoices/{id}/send', [InvoiceApiController::class, 'sendInvoice']);
        Route::post('invoices/{id}/guest-access', [InvoiceApiController::class, 'createGuestAccess']);
    });

Then register in routes/api.php:

// Load module API routes
require app_path('BDMAccounting/Routes/api.php');
6

Test Your Endpoints

# Get auth token
POST /api/login
{
  "email": "user@example.com",
  "password": "password"
}

# Test new endpoint
GET /api/v1/accounting/invoices
Authorization: Bearer YOUR_TOKEN

# Test create
POST /api/v1/accounting/invoices
Authorization: Bearer YOUR_TOKEN
{
  "customer_name": "Test Customer",
  "customer_email": "test@example.com",
  "total": 100.00
}

Code Transformation Examples

Before: Custom Controller

// Old custom controller
class InvoiceController extends Controller
{
    public function index(Request $request)
    {
        $invoices = Invoice::where('user_id', auth()->id())
            ->paginate(15);
            
        return response()->json([
            'data' => $invoices
        ]);
    }
    
    public function store(Request $request)
    {
        $validated = $request->validate([
            'customer_name' => 'required',
            'total' => 'required|numeric',
        ]);
        
        $invoice = Invoice::create([
            'user_id' => auth()->id(),
            ...$validated
        ]);
        
        return response()->json([
            'data' => $invoice
        ], 201);
    }
}

After: BaseApiController

// New API controller
class InvoiceApiController extends BaseApiController
{
    protected $model = Invoice::class;
    protected $moduleSlug = 'accounting';
    
    protected function createRules(): array
    {
        return [
            'customer_name' => 'required|string|max:255',
            'total' => 'required|numeric|min:0',
        ];
    }
    
    // index() and store() inherited from BaseApiController
    // Handles: pagination, filtering, permissions, logging automatically
}

Before: Manual Permission Checking

public function update(Request $request, $id)
{
    $invoice = Invoice::findOrFail($id);
    
    // Manual permission check
    if ($invoice->user_id !== auth()->id()) {
        return response()->json(['error' => 'Unauthorized'], 403);
    }
    
    $invoice->update($request->all());
    
    return response()->json(['data' => $invoice]);
}

After: Automatic Permission Handling

// update() method inherited from BaseApiController
// Automatically handles:
// - Permission checking (via HasModulePermissions trait)
// - Shared access (user can update if shared with them)
// - Validation
// - Standard response format

// You only need to define validation rules:
protected function updateRules(): array
{
    return [
        'customer_name' => 'sometimes|string|max:255',
        'total' => 'sometimes|numeric|min:0',
    ];
}

Before: Custom Filtering

public function index(Request $request)
{
    $query = Invoice::where('user_id', auth()->id());
    
    if ($request->status) {
        $query->where('status', $request->status);
    }
    
    if ($request->search) {
        $query->where('customer_name', 'like', "%{$request->search}%");
    }
    
    if ($request->sort_by) {
        $query->orderBy($request->sort_by, $request->sort_order ?? 'asc');
    }
    
    return response()->json(['data' => $query->paginate(15)]);
}

After: Configuration-Based Filtering

// In config/modules/bdm_accounting_api.php
'searchable_fields' => [
    'invoices' => ['customer_name', 'number'],
],
'sortable_fields' => [
    'invoices' => ['created_at', 'total', 'status'],
],
'filterable_fields' => [
    'invoices' => ['status'],
],

// BaseApiController handles:
// GET /api/v1/accounting/invoices?search=acme&filter[status]=paid&sort_by=total
// All filtering, sorting, searching automatic!

Testing Migrated Modules

Feature Tests

<?php

namespace Tests\Feature;

use Tests\TestCase;
use App\Models\User;
use App\BDMAccounting\Models\Invoice;
use Illuminate\Foundation\Testing\RefreshDatabase;

class InvoiceApiTest extends TestCase
{
    use RefreshDatabase;

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

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

        $response->assertOk()
            ->assertJsonStructure([
                'success',
                'data' => [
                    '*' => ['id', 'customer_name', 'total']
                ],
                'meta'
            ]);
    }

    public function test_cannot_access_other_users_invoices()
    {
        $user1 = User::factory()->create();
        $user2 = User::factory()->create();
        
        $invoice = Invoice::factory()->create(['user_id' => $user1->id]);

        $response = $this->actingAs($user2, 'sanctum')
            ->getJson("/api/v1/accounting/invoices/{$invoice->id}");

        $response->assertNotFound();
    }

    public function test_shared_user_can_access()
    {
        $owner = User::factory()->create();
        $sharedUser = User::factory()->create();
        
        // Share module with user
        app(PermissionService::class)->shareModule(
            $owner,
            $sharedUser->email,
            'accounting',
            PermissionRole::VIEWER
        );
        
        $invoice = Invoice::factory()->create(['user_id' => $owner->id]);

        $response = $this->actingAs($sharedUser, 'sanctum')
            ->getJson("/api/v1/accounting/invoices/{$invoice->id}");

        $response->assertOk();
    }
}

Run Tests

# Run all tests
php artisan test

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

# Run with coverage
php artisan test --coverage

# Stop on first failure
php artisan test --stop-on-failure

Common Migration Issues

❌ Issue: Table Missing user_id Column

Error: Column 'user_id' not found

Solution: Create migration to add column:

php artisan make:migration add_user_id_to_invoices_table

// In migration
public function up()
{
    Schema::table('bdm_accounting_invoices', function (Blueprint $table) {
        $table->foreignId('user_id')->after('id')->constrained();
        $table->index('user_id');
    });
    
    // If you have existing data, assign to a default user
    DB::table('bdm_accounting_invoices')
        ->whereNull('user_id')
        ->update(['user_id' => 1]); // Admin user
}

⚠️ Issue: Middleware Not Applied

Error: Routes accessible without authentication

Solution: Check middleware in routes:

// Ensure these middleware are applied
Route::middleware(['auth:sanctum', 'log.api'])
    ->group(function () {
        // Your routes
    });

ℹ️ Issue: CORS Errors

Error: Access-Control-Allow-Origin header missing

Solution: Configure CORS in config/cors.php:

'paths' => ['api/*'],
'allowed_origins' => ['http://localhost:3000'], // Your frontend URL
'supports_credentials' => true,

🔍 Issue: Rate Limit Exceeded in Development

Error: 429 Too Many Requests

Solution: Increase rate limits in .env:

BDM_API_RATE_LIMIT_PER_MINUTE=500
BDM_API_RATE_LIMIT_PER_HOUR=20000

✓ Issue: Routes Not Found

Error: 404 on new endpoints

Solution: Clear route cache:

php artisan route:clear
php artisan route:cache
php artisan config:clear

Rollback Plan

If Something Goes Wrong

Always have a rollback plan. Here's how to safely revert changes:

1. Git Rollback

# Return to pre-migration state
git checkout pre-universal-api-migration

# Or reset to specific commit
git reset --hard COMMIT_HASH

# Force push if already deployed (careful!)
git push origin main --force

2. Database Rollback

# Restore from backup
mysql -u username -p database_name < backup_YYYYMMDD.sql

# Or rollback specific migration
php artisan migrate:rollback --step=1

3. Clear All Caches

php artisan cache:clear
php artisan config:clear
php artisan route:clear
php artisan view:clear

💡 Prevention Tips

  • ✓ Always test in staging first
  • ✓ Keep backups before migration
  • ✓ Migrate one module at a time
  • ✓ Run comprehensive tests
  • ✓ Have a rollback window planned
  • ✓ Monitor logs after deployment

Post-Migration Checklist

All endpoints tested and working
Permission system tested
Frontend updated (if needed)
API consumers notified of changes
Documentation updated
Monitoring in place
Old API routes deprecated/removed
Team trained on new features

🎉 Congratulations! Your module is now using the BDM Universal API Framework