Module Structure

Understanding how modules are organized helps you build maintainable and consistent integrations for Auto Commerce.

Directory Layout

A complete module contains both backend (Laravel) and frontend (React/Next.js) code:

modules/YourModule/
├── backend/                    # Laravel Backend
│   ├── App/
│   │   ├── Http/
│   │   │   ├── Controllers/    # API controllers
│   │   │   └── Requests/       # Form requests
│   │   ├── Services/           # Business logic
│   │   ├── Jobs/              # Queue jobs
│   │   └── Resources/         # API resources
│   ├── config/                # Module configuration
│   ├── routes/
│   │   ├── api.php            # API routes
│   │   └── webhooks.php       # Webhook routes
│   ├── Database/
│   │   └── Migrations/        # Database migrations
│   ├── tests/                 # Module tests
│   │   ├── Feature/           # API/integration tests
│   │   ├── Unit/              # Unit tests
│   │   ├── Helpers.php        # Test helpers
│   │   └── Pest.php           # Test configuration
│   └── YourModuleServiceProvider.php
│
├── frontend/                   # React/Next.js Frontend
│   ├── index.ts               # Public exports
│   ├── components/            # React components
│   │   ├── YourComponent.tsx
│   │   └── YourForm.tsx
│   ├── pages/                 # Module pages (dynamic routes)
│   │   ├── index.tsx          # /your-module
│   │   ├── settings.tsx       # /your-module/settings
│   │   └── detail.tsx         # /your-module/[id]
│   ├── hooks/                 # React hooks
│   │   └── use-your-module.ts
│   ├── types/                 # TypeScript types
│   │   └── index.ts
│   └── lib/                   # Utilities
│       └── api.ts
│
├── module.json                # Module manifest (REQUIRED)
└── README.md                  # Documentation

Core Files

module.json

The module manifest is the single source of truth for your module. It defines metadata, backend configuration, frontend routes, navigation, and settings.

JSON Schema Support

AutoCom provides a JSON Schema for IDE validation and autocomplete. Add the $schema field to enable it:

{
  "$schema": "./module.schema.json",
  "name": "StoreShopify",
  "alias": "store-shopify",
  "displayName": "Shopify Integration",
  "description": "Connect and sync orders from your Shopify store",
  "version": "1.0.0",
  "type": "store",
  "priority": 50,
  "enabled": false,
  "isCore": false,

  "backend": {
    "namespace": "Modules\\StoreShopify",
    "providers": ["Modules\\StoreShopify\\StoreShopifyServiceProvider"],
    "aliases": {},
    "migrations": true,
    "contracts": ["StoreProviderContract"],
    "workflowNodes": [
      "Modules\\StoreShopify\\Nodes\\Triggers\\ShopifyOrderCreatedTrigger",
      "Modules\\StoreShopify\\Nodes\\Actions\\SyncShopifyOrderAction"
    ],
    "widgetDataProviders": {
      "shopify:store-stats": "Modules\\StoreShopify\\App\\WidgetProviders\\StoreStatsProvider"
    }
  },

  "frontend": {
    "routes": {
      "basePath": "/integrations/shopify",
      "catchAll": true,
      "pages": {
        "/": "index.tsx",
        "/settings": "settings.tsx",
        "/sync": "sync.tsx"
      }
    },
    "navigation": {
      "title": "Shopify",
      "href": "/integrations/shopify",
      "icon": "Store",
      "position": 10
    },
    "exports": {
      "components": ["ShopifyConnectButton", "ShopifyStoreInfo"],
      "hooks": ["useShopify", "useShopifySync"],
      "types": ["ShopifyStore", "ShopifyOrder"]
    }
  },

  "dependencies": {
    "modules": ["Core", "Orders"],
    "backend": { "php": "^8.2" },
    "frontend": {}
  },

  "settings_schema": {
    "shop_domain": {
      "type": "string",
      "required": true,
      "label": "Shop Domain",
      "placeholder": "mystore.myshopify.com"
    },
    "api_key": {
      "type": "string",
      "required": true,
      "encrypted": true,
      "label": "API Key"
    },
    "api_secret": {
      "type": "string",
      "required": true,
      "encrypted": true,
      "label": "API Secret"
    },
    "auto_sync": {
      "type": "boolean",
      "default": true,
      "label": "Auto-sync orders"
    }
  }
}

Module Types

Type Description Example
core Essential system modules Orders, Products, Customers
store E-commerce platform integrations Shopify, WooCommerce
shipping Logistics provider integrations Delhivery, Shiprocket
channel Communication channel integrations WhatsApp, SMS, Email
payment Payment gateway integrations Razorpay, Stripe
ai AI service integrations OpenAI, Claude

Frontend Configuration

The frontend section configures how your module integrates with the UI:

"frontend": {
  "routes": {
    "basePath": "/your-module",      // URL prefix for all pages
    "catchAll": true,                // Handle all sub-routes
    "pages": {                       // Map routes to page files
      "/": "index.tsx",
      "/settings": "settings.tsx",
      "/[id]": "detail.tsx"          // Dynamic route
    }
  },
  "navigation": {                    // Sidebar menu entry
    "title": "Your Module",
    "href": "/your-module",
    "icon": "Box",                   // Lucide icon name
    "position": 10,                  // Sort order
    "children": [                    // Sub-menu items
      { "title": "Overview", "href": "/your-module", "icon": "LayoutDashboard" }
    ]
  },
  "exports": {                       // Public API exports
    "components": ["YourComponent"],
    "hooks": ["useYourModule"],
    "types": ["YourType"]
  }
}

Service Provider

The main entry point for your module:

namespace Modules\YourModule\Providers;

use Illuminate\Support\ServiceProvider;
use Illuminate\Database\Eloquent\Factory;

class YourModuleServiceProvider extends ServiceProvider
{
    protected $moduleName = 'YourModule';
    protected $moduleNameLower = 'yourmodule';

    /**
     * Boot the application events.
     */
    public function boot()
    {
        $this->registerTranslations();
        $this->registerConfig();
        $this->registerViews();
        $this->loadMigrationsFrom(module_path($this->moduleName, 'Database/Migrations'));
        $this->registerEvents();
    }

    /**
     * Register the service provider.
     */
    public function register()
    {
        $this->app->register(RouteServiceProvider::class);

        // Bind services
        $this->app->singleton(YourService::class, function ($app) {
            return new YourService();
        });
    }

    protected function registerConfig()
    {
        $this->publishes([
            module_path($this->moduleName, 'Config/config.php') => config_path($this->moduleNameLower . '.php'),
        ], 'config');

        $this->mergeConfigFrom(
            module_path($this->moduleName, 'Config/config.php'), $this->moduleNameLower
        );
    }

    protected function registerViews()
    {
        $viewPath = resource_path('views/modules/' . $this->moduleNameLower);
        $sourcePath = module_path($this->moduleName, 'Resources/views');

        $this->publishes([
            $sourcePath => $viewPath
        ], ['views', $this->moduleNameLower . '-module-views']);

        $this->loadViewsFrom(array_merge($this->getPublishableViewPaths(), [$sourcePath]), $this->moduleNameLower);
    }

    protected function registerTranslations()
    {
        $langPath = resource_path('lang/modules/' . $this->moduleNameLower);

        if (is_dir($langPath)) {
            $this->loadTranslationsFrom($langPath, $this->moduleNameLower);
        } else {
            $this->loadTranslationsFrom(module_path($this->moduleName, 'Resources/lang'), $this->moduleNameLower);
        }
    }

    protected function registerEvents()
    {
        Event::listen(OrderCreated::class, HandleOrderCreated::class);
        Event::listen(OrderUpdated::class, HandleOrderUpdated::class);
    }

    /**
     * Get the services provided by the provider.
     */
    public function provides()
    {
        return [];
    }

    private function getPublishableViewPaths(): array
    {
        $paths = [];
        foreach (\Config::get('view.paths') as $path) {
            if (is_dir($path . '/modules/' . $this->moduleNameLower)) {
                $paths[] = $path . '/modules/' . $this->moduleNameLower;
            }
        }
        return $paths;
    }
}

Routes

Define your module's HTTP endpoints:

// Http/routes.php
use Illuminate\Support\Facades\Route;
use Modules\YourModule\Http\Controllers\Api\YourController;

// API Routes
Route::group([
    'prefix' => 'integrations/yourmodule',
    'middleware' => ['auth:api', 'tenant']
], function () {
    Route::post('/connect', [YourController::class, 'connect']);
    Route::post('/disconnect', [YourController::class, 'disconnect']);
    Route::get('/status', [YourController::class, 'status']);
    Route::post('/sync', [YourController::class, 'sync']);
});

// Webhook Routes (no auth)
Route::group([
    'prefix' => 'webhooks/yourmodule',
    'middleware' => ['tenant']
], function () {
    Route::post('/{topic}', [YourController::class, 'webhook']);
});

Key Directories

Entities/

Eloquent models for module-specific data:

namespace Modules\StoreShopify\Entities;

use Illuminate\Database\Eloquent\Model;

class ShopifyConnection extends Model
{
    protected $fillable = [
        'tenant_id',
        'shop_domain',
        'access_token',
        'is_active',
        'last_sync_at'
    ];

    protected $casts = [
        'is_active' => 'boolean',
        'last_sync_at' => 'datetime'
    ];

    public function tenant()
    {
        return $this->belongsTo(Tenant::class);
    }
}

Services/

Business logic and external API integration:

namespace Modules\StoreShopify\Services;

use GuzzleHttp\Client;

class ShopifyService
{
    protected $client;
    protected $shopDomain;
    protected $accessToken;

    public function __construct()
    {
        $this->client = new Client();
        $this->loadSettings();
    }

    protected function loadSettings()
    {
        $this->shopDomain = ModuleManager::getSetting('store-shopify', 'shop_domain');
        $this->accessToken = ModuleManager::getSetting('store-shopify', 'access_token');
    }

    public function fetchOrders($since = null)
    {
        $url = "https://{$this->shopDomain}/admin/api/2024-01/orders.json";

        $response = $this->client->get($url, [
            'headers' => [
                'X-Shopify-Access-Token' => $this->accessToken
            ],
            'query' => [
                'status' => 'any',
                'updated_at_min' => $since ? $since->toIso8601String() : null
            ]
        ]);

        return json_decode($response->getBody(), true);
    }

    public function createProduct($productData)
    {
        // Implementation
    }
}

Jobs/

Background queue jobs for async processing:

namespace Modules\StoreShopify\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;

class SyncShopifyOrders implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    protected $tenantId;
    protected $since;

    public function __construct($tenantId, $since = null)
    {
        $this->tenantId = $tenantId;
        $this->since = $since;
    }

    public function handle(ShopifyService $shopifyService)
    {
        // Switch to tenant context
        tenancy()->initialize($this->tenantId);

        // Sync orders
        $orders = $shopifyService->fetchOrders($this->since);

        foreach ($orders['orders'] as $orderData) {
            // Process and save order
            OrderService::createFromShopify($orderData);
        }
    }
}

Events & Listeners/

Custom events for your module:

// Events/ShopifyOrderSynced.php
namespace Modules\StoreShopify\Events;

class ShopifyOrderSynced
{
    public $order;
    public $shopifyData;

    public function __construct($order, $shopifyData)
    {
        $this->order = $order;
        $this->shopifyData = $shopifyData;
    }
}

// Listeners/NotifyOrderSynced.php
namespace Modules\StoreShopify\Listeners;

class NotifyOrderSynced
{
    public function handle(ShopifyOrderSynced $event)
    {
        // Send notification, log, etc.
        Log::info('Shopify order synced', [
            'order_id' => $event->order->id,
            'shopify_id' => $event->shopifyData['id']
        ]);
    }
}

Database/Migrations/

Version-controlled database changes:

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up()
    {
        Schema::create('shopify_connections', function (Blueprint $table) {
            $table->id();
            $table->foreignId('tenant_id')->constrained()->onDelete('cascade');
            $table->string('shop_domain')->unique();
            $table->text('access_token')->nullable();
            $table->boolean('is_active')->default(true);
            $table->timestamp('last_sync_at')->nullable();
            $table->timestamps();

            $table->index(['tenant_id', 'shop_domain']);
        });
    }

    public function down()
    {
        Schema::dropIfExists('shopify_connections');
    }
};

File Naming Conventions

  • Models: StudlyCase (e.g., ShopifyConnection.php)
  • Controllers: StudlyCaseController (e.g., ShopifyAuthController.php)
  • Services: StudlyCaseService (e.g., ShopifyService.php)
  • Jobs: VerbNoun (e.g., SyncShopifyOrders.php)
  • Events: NounVerbed (e.g., OrderSynced.php)
  • Listeners: VerbNoun (e.g., HandleOrderSynced.php)

Module Testing

Each module can include its own test suite to ensure functionality works correctly. Tests live in the backend/tests/ directory.

Test Directory Structure

modules/YourModule/backend/tests/
├── Feature/                # Integration/API tests
│   └── YourApiTest.php
├── Unit/                   # Unit tests
│   └── YourModelTest.php
├── Helpers.php             # Test helper functions
└── Pest.php                # Pest configuration

Setting Up Module Tests

  1. Create the test directories and configuration:
mkdir -p modules/YourModule/backend/tests/Feature
mkdir -p modules/YourModule/backend/tests/Unit
  1. Create Pest.php configuration:
<?php

use Illuminate\Foundation\Testing\DatabaseTransactions;

require_once __DIR__ . '/Helpers.php';

uses(Tests\TestCase::class, DatabaseTransactions::class)->in('Feature', 'Unit');
  1. Create Helpers.php with test factories:
<?php

use Modules\YourModule\App\Models\YourModel;

if (!function_exists('createTestYourModel')) {
    function createTestYourModel(array $attributes = []): YourModel
    {
        return YourModel::create(array_merge([
            'name' => 'Test Item',
            'status' => 'active',
        ], $attributes));
    }
}

Running Module Tests

# Run all tests for a specific module
php artisan test:module YourModule

# Run only unit tests
php artisan test:module YourModule --unit

# Run only feature tests
php artisan test:module YourModule --feature

# Filter by test name
php artisan test:module YourModule --filter="can create"

# List all modules with tests
php artisan test:module --list

For detailed testing information, see the Module Testing Guide.

JSON Schema Reference

The modules/module.schema.json file provides full documentation of all available fields. Key sections:

Section Purpose
name, alias, version Module identification (required)
type, priority, isCore Classification and loading behavior
backend Laravel configuration (namespace, providers, contracts)
frontend Next.js configuration (routes, navigation, exports)
dependencies Module and package dependencies
settings_schema Dynamic settings form definition
permissions Module-defined permissions
features Feature flags

Supported Settings Types

In settings_schema, you can use these field types:

Type Description Options
string Text input placeholder, validation.pattern
number Numeric input validation.min, validation.max
boolean Toggle switch default
select Dropdown options: [{label, value}]
password Password input encrypted: true
textarea Multi-line text placeholder
json JSON editor -

Widgets & Workflow Nodes

Modules can provide dashboard widgets and workflow automation nodes that are automatically registered.

Widget Data Providers

To provide real data for dashboard widgets, create a data provider class:

namespace Modules\YourModule\App\WidgetProviders;

use App\Core\Services\BaseWidgetDataProvider;

class YourWidgetProvider extends BaseWidgetDataProvider
{
    protected int $cacheTtl = 300; // 5 minutes

    // Optional: Define config schema for validation
    protected array $configSchema = [
        'type' => 'object',
        'properties' => [
            'showDetails' => ['type' => 'boolean'],
            'limit' => ['type' => 'integer', 'minimum' => 1, 'maximum' => 100]
        ]
    ];

    public static function getWidgetId(): string
    {
        return 'yourmodule:widget-name';
    }

    public function getData(array $config = [], array $context = []): array
    {
        $dateRange = $this->getDateRange($context);

        // Query your data
        $stats = YourModel::query()
            ->whereBetween('created_at', [$dateRange['start'], $dateRange['end']])
            ->selectRaw('COUNT(*) as total, SUM(amount) as revenue')
            ->first();

        return [
            'totalItems' => [
                'value' => $stats->total,
                'formatted' => $this->formatNumber($stats->total),
            ],
            'revenue' => [
                'value' => $stats->revenue,
                'formatted' => '₹' . $this->formatNumber($stats->revenue),
            ],
        ];
    }
}

Register in module.json:

{
  "backend": {
    "widgetDataProviders": {
      "yourmodule:widget-name": "Modules\\YourModule\\App\\WidgetProviders\\YourWidgetProvider"
    }
  }
}

Workflow Nodes

Create workflow triggers and actions that integrate with the automation system:

namespace Modules\YourModule\Nodes\Triggers;

use Modules\Workflows\Contracts\WorkflowNodeContract;
use Modules\Workflows\Nodes\BaseTriggerNode;

class YourEventTrigger extends BaseTriggerNode implements WorkflowNodeContract
{
    public static function getIdentifier(): string
    {
        return 'yourmodule.your-event-trigger';
    }

    public static function getName(): string
    {
        return 'Your Event Trigger';
    }

    public static function getCategory(): string
    {
        return 'trigger';
    }

    public static function getDescription(): string
    {
        return 'Triggers when your event occurs';
    }

    public static function getIcon(): string
    {
        return 'Zap';
    }

    public static function getColor(): string
    {
        return '#22C55E';
    }

    public static function getModule(): string
    {
        return 'YourModule';
    }

    public static function getOutputs(): array
    {
        return [
            ['name' => 'data', 'type' => 'object', 'description' => 'Event data'],
        ];
    }

    public function execute(array $input, array $config): array
    {
        // Trigger logic
        return ['data' => $input];
    }
}

Register in module.json:

{
  "backend": {
    "workflowNodes": [
      "Modules\\YourModule\\Nodes\\Triggers\\YourEventTrigger",
      "Modules\\YourModule\\Nodes\\Actions\\YourAction"
    ]
  }
}

Custom Categories

Register custom widget or node categories:

{
  "backend": {
    "widgetCategories": {
      "yourmodule": {
        "label": "Your Module",
        "icon": "Box",
        "description": "Widgets for Your Module"
      }
    },
    "nodeCategories": {
      "yourmodule": {
        "label": "Your Module",
        "icon": "Box",
        "color": "#8B5CF6",
        "description": "Nodes for Your Module automation"
      }
    }
  }
}

CLI Commands

Use Artisan commands to manage modules:

# Generate a new module
php artisan module:make MyModule --type=store

# List all modules
php artisan module:list
php artisan module:list --graph   # Show dependency graph

# Enable/disable modules
php artisan module:enable MyModule
php artisan module:disable MyModule

See CLI Commands for full documentation.

Next Steps