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
- Create the test directories and configuration:
mkdir -p modules/YourModule/backend/tests/Feature
mkdir -p modules/YourModule/backend/tests/Unit
- Create
Pest.phpconfiguration:
<?php
use Illuminate\Foundation\Testing\DatabaseTransactions;
require_once __DIR__ . '/Helpers.php';
uses(Tests\TestCase::class, DatabaseTransactions::class)->in('Feature', 'Unit');
- Create
Helpers.phpwith 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
- CLI Commands - Generate and manage modules from CLI
- Module Dependencies - Understand dependency management
- Module Lifecycle - Hook into module events
- Backend Development - Detailed backend implementation
- Frontend Development - Add UI components and pages
- Creating Modules - Quick start guide
- Module Testing - Testing your module