> ## Documentation Index
> Fetch the complete documentation index at: https://docs.erp.arukz.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Build a custom plugin for ARUKZ ERP

> A worked recipe for a new ARUKZ ERP module: autoloading, a company-scoped migration, the model, service provider, Filament screens, permissions and tests.

A new ARUKZ ERP module is a folder under `plugins/arukz/` with a `composer.json`, its migrations, an Eloquent model that uses `BelongsToCompany`, a service provider that registers the install command, and a Filament plugin class that adds its screens to the panel. Permissions need no code: any resource under the `Arukz\` namespace is checked against Shield permissions automatically. The worked example below adds dyeing batches for a textile mill.

## First, should it be a plugin?

| The customer needs | Do this |
| - | - |
| An extra field on an existing record (say, GSM on products) | A custom field under **Settings → Fields**, no code |
| A different filter or column set on a list | A saved table view, no code |
| A new kind of record with its own lifecycle, screens and reports | A new plugin, as below |
| A change to an existing flow for every company | Change the existing plugin, with tests |

Every company shares one codebase, so a plugin built for one customer is installed for the whole platform. Make it generic ("Dyeing batches"), not about one customer.

## The files

```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
plugins/arukz/textile-dyeing/
├── composer.json
├── database/migrations/2026_11_01_000001_create_dyeing_batches_table.php
├── src/
│   ├── TextileDyeingServiceProvider.php
│   ├── TextileDyeingPlugin.php
│   ├── Models/DyeingBatch.php
│   └── Filament/Clusters/
│       ├── TextileDyeing.php
│       └── TextileDyeing/Resources/DyeingBatchResource.php (+ Pages/)
└── tests/Feature/DyeingBatchTest.php
```

**1. Autoload the namespace**

Give the plugin a `composer.json` with PSR-4 autoloading (`"Arukz\\TextileDyeing\\": "src/"`). The root `composer.json` merges every `plugins/*/*/composer.json`; by house convention also add the namespace to the root `autoload.psr-4` next to the other `Arukz\\` entries, then run `composer dump-autoload`.

**2. Write a company-scoped migration**

```php theme={"theme":{"light":"github-light","dark":"github-dark"}}
Schema::create('dyeing_batches', function (Blueprint $table): void {
    $table->id();
    $table->string('batch_number');
    $table->foreignId('company_id')->nullable()->constrained('companies')->nullOnDelete();
    $table->foreignId('partner_id')->nullable()->constrained('partners_partners')->nullOnDelete();
    $table->foreignId('product_id')->constrained('products_products')->restrictOnDelete();
    $table->foreignId('sale_order_id')->nullable()->constrained('sales_orders')->nullOnDelete();
    $table->decimal('quantity', 15, 4);
    $table->string('status')->default('planned')->index();
    $table->date('dyed_on')->nullable()->index();
    $table->foreignId('creator_id')->nullable()->constrained('users')->nullOnDelete();
    $table->softDeletes();
    $table->timestamps();
    $table->unique(['company_id', 'batch_number']);
});
```

IDs instead of names, `company_id` on the table, numbers unique per company, `restrictOnDelete` where history must survive, and indexes on what you filter by. See [Data model](https://docs.erp.arukz.com/developers/data-model.md).

**3. Write the model**

Use `BelongsToCompany` (company separation and an automatic `company_id`) and take numbers from `SequenceService`, never `count() + 1`:

```php theme={"theme":{"light":"github-light","dark":"github-dark"}}
class DyeingBatch extends Model
{
    use BelongsToCompany, SoftDeletes;

    protected static function booted(): void
    {
        static::creating(function (DyeingBatch $batch): void {
            $batch->batch_number ??= SequenceService::next('textile_dyeing.batch', $batch->company_id);
            $batch->creator_id ??= auth()->id();
        });
    }
}
```

Relate to `Webkul\Security\Models\User`, not `App\Models\User`: the Webkul model has the partner, allowed companies and `isPlatformOperator()`.

**4. Register the service provider**

Extend `Webkul\PluginManager\PackageServiceProvider`. Its `$name` becomes the plugin ID, the `plugins` table row and the `textile-dyeing:install` command. Declare migrations and **dependencies**, and add the Filament plugin to the panel:

```php theme={"theme":{"light":"github-light","dark":"github-dark"}}
$package->name(static::$name)
    ->hasMigrations(['2026_11_01_000001_create_dyeing_batches_table'])
    ->runsMigrations()
    ->hasDependencies('products', 'inventories', 'sales')
    ->hasInstallCommand(fn (InstallCommand $command) => $command->installDependencies()->runsMigrations());
```

**5. Add the Filament plugin and screens**

The plugin class is named like the provider with `Plugin` in place of `ServiceProvider`. In `register()`, return early when the plugin is not installed, then call `discoverClusters()` for the cluster folder only. Build the resource with Filament 5 class names:

| Use (Filament 5) | Not (Filament 3, will crash) |
| - | - |
| `Filament\Actions\ViewAction`, `EditAction`, `DeleteAction`, `BulkActionGroup` | `Filament\Tables\Actions\...` |
| `Filament\Schemas\Schema` in `form(Schema $schema)` | `Filament\Forms\Form` |
| `Filament\Schemas\Components\Section`, `Tabs`, `Grid`, `Fieldset` | `Filament\Forms\Components\Section` |
| `$table->recordActions([...])`, `->toolbarActions([...])` | `->actions([...])`, `->bulkActions([...])` |

Relationship selects (`Select::make('partner_id')->relationship('partner', 'name')`) are limited to the current company by the scope automatically.

**6. Generate and grant permissions**

Every action on a model whose resource lives under `Arukz\` needs the matching Shield permission, named `<action>_<module>_<resource>`, for example `view_any_textile_dyeing_dyeing::batch`. Generate them with `php artisan shield:generate --all --option=permissions --panel=admin` (the install command does it too), then grant them to the roles that should use the module. Until then nobody without the permission can open the screens. Write a policy class only for a rule beyond "does the role have it".

**7. Register, install and reload**

Add the provider to `bootstrap/providers.php`, run `composer dump-autoload`, run `php artisan textile-dyeing:install` as the web server user, then rebuild the caches and reload PHP-FPM (see [Operations](https://docs.erp.arukz.com/developers/operations.md)).

**8. Test it**

At least: a batch made in company A is not returned to a user of company B; two companies both get number `0001`; a user without `view_any_...` cannot open the list. `tests/Feature/Arukz/CompanyScopingTest.php` also checks every ARUKZ model uses `BelongsToCompany`, and `ModuleIntegrityTest` fails if any ARUKZ file imports a class that does not exist.

## Checklist before a pull request

* Every business table has `company_id`, and the model uses `BelongsToCompany`.
* Foreign keys, not copied names; uniqueness per company.
* Numbers from `SequenceService`.
* `hasDependencies(...)` lists every plugin whose tables you reference.
* `register()` returns early when the plugin is not installed, and only `discoverClusters()` scans the cluster folder.
* Permissions generated and granted to the company `Admin` role.
* Filament 5 class names only; tests for isolation, numbering and permissions pass.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.