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

# Conventions Reference

> Exact class names, method signatures and what calls what. Keep this open while you work.

<Warning>
  Core finds most module code by building a class name as a string, so a typo means the feature
  silently doesn't exist. Copy names from here rather than from memory, and run
  `php artisan module:doctor` to check.
</Warning>

Throughout this page, `{M}` is the `name` field from `module.json`, for example `PterodactylService`.

## Service module classes

| Class                                                 | Called by                                                                              | Signature                                                                                                                                                                                                                                                       | Static?              |
| ----------------------------------------------------- | -------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------- |
| `Modules\{M}\Services\ModuleServiceActions`           | `CreateServiceJob`, `HandleConfigurableOptionsChangeService`                           | `createServerForService(Service $s, array $cfg = []): void`<br />`hasExistingConfiguration(Service $s): bool`<br />`createServer(Product $p, User $u, array $cfg = []): Service`<br />`createServerForServiceWithoutProduct(Service $s, array $cfg = []): void` | instance             |
| `Modules\{M}\Services\Actions\SuspendServerService`   | `ServiceManagementService`                                                             | `suspendServer(int $serviceId): array`                                                                                                                                                                                                                          | **static**           |
| `Modules\{M}\Services\Actions\UnsuspendServerService` | `ServiceManagementService`                                                             | `unsuspendServer(int $serviceId): array`                                                                                                                                                                                                                        | **static**           |
| `Modules\{M}\Services\Actions\DeleteServerService`    | `ServiceManagementService`                                                             | `deleteServer(int $serviceId): array`                                                                                                                                                                                                                           | **static**           |
| `Modules\{M}\Services\RenewService`                   | `RenewServiceController`, `SubscriptionJob`, `OrderService`                            | extends `BaseRenewService`, override `onAfterRenewal(Service $s, string $cycle): void`                                                                                                                                                                          | instance             |
| `Modules\{M}\Services\CancelService`                  | `CancelServiceController`, `CancellationVerificationService`                           | extends `BaseCancelService`, implement `remoteResourceId()` and `deleteRemoteResource()`                                                                                                                                                                        | instance             |
| `Modules\{M}\Services\UpgradeService`                 | `HandleUpgradeService`                                                                 | `processUpgrade(Service $s, Invoice $i): bool`                                                                                                                                                                                                                  | instance (container) |
| `Modules\{M}\Services\DowngradeService`               | `HandleDowngradeService`                                                               | `processDowngrade(Service $s, Invoice $i): bool`                                                                                                                                                                                                                | instance (container) |
| `Modules\{M}\Services\AddonService`                   | `HandleAddonService`, `HandleAddon`                                                    | `applyAddon(Service $s, array $addon, int $qty = 1): array`<br />`removeAddon(Service $s, array $addon, int $qty = 1): array`                                                                                                                                   | instance (container) |
| `Modules\{M}\Services\MarketCheckoutConfig`           | `MarketController`, `ServiceCreationService`                                           | `getCheckoutFields(): array`<br />`getServiceConfigFields(): array`<br />`validateCheckoutData(array $d): array`<br />`getServerConfiguration(int $productId): array` (optional)                                                                                | **static**           |
| `Modules\{M}\Services\ServiceControlsService`         | `ServiceControlLoader`                                                                 | `getServiceControls($serviceConfig = null, $serviceId = null): array`                                                                                                                                                                                           | instance             |
| `Modules\{M}\Services\ServiceDetails`                 | `ServiceDetailsController` (user and admin)                                            | `getSections(Service $s): array`<br />`getTabConfig(): array`<br />`getServiceData(Service $s): array`<br />`getAvailableActions(Service $s): array`<br />`updateAccount(Service $s, array $d): array`<br />`changeOwner(Service $s, int $ownerId): array`      | instance             |
| `Modules\{M}\Services\{M}`                            | `ProductController`, `ConfigurableOptionGroupController`, `AddonPresetGroupController` | Your own methods, for admin dropdowns                                                                                                                                                                                                                           | instance             |

<Note>
  Only `ModuleServiceActions` and the three `Actions\*` classes are needed for a working module.
  Everything else gates an optional feature.
</Note>

<Warning>
  `ServiceControlLoader` always passes two arguments to `getServiceControls()`, so declare both even if
  you ignore them.
</Warning>

<Info>
  There used to be a `Modules\{M}\Services\{M}Service` class for termination. It never resolved,
  because the lookup produced names like `PterodactylServiceService`. Termination goes through
  `Actions\DeleteServerService` via `ServiceManagementService::executeModuleAction()`. Ignore that
  pattern if you see it in older code.
</Info>

## Gateway module classes

| Class                                             | Called by                                         | Notes                                                                           |
| ------------------------------------------------- | ------------------------------------------------- | ------------------------------------------------------------------------------- |
| `Modules\{M}\Services\{M}PaymentService`          | `PaymentServiceProvider`, `PaymentGatewayManager` | **Required.** Extend `BasePaymentService`.                                      |
| `Modules\{M}\Http\Controllers\PaymentController`  | routes                                            | Extend `ModulePaymentController`, set `$paymentServiceClass` and `$gatewayName` |
| `Modules\{M}\Http\Controllers\SettingsController` | routes                                            | Extend `ModuleSettingsController`                                               |
| `Modules\{M}\Services\CheckoutHandler`            | `CheckoutService`                                 | Optional. `shouldHandleCheckout()` plus `handleCheckout()` (23 args)            |
| `Modules\{M}\Services\RenewalHandler`             | `RenewServiceController`                          | Optional. `shouldHandleRenewal()` plus `handleRenewal()` (7 args)               |
| `Modules\{M}\Services\InvoicePaymentHandler`      | `PaymentController`                               | Optional. `shouldHandleInvoicePayment()` plus `handleInvoicePayment()` (6 args) |

Gateway key: `Str::lower(Str::before($moduleName, 'Gateway'))`, so `AcmeGateway` gives `acme`.
`getGatewayName()` must return it.

<AccordionGroup>
  <Accordion title="PaymentGatewayInterface" icon="code">
    ```php theme={null}
    createPayment(float $amount, string $currency, array $metadata = []): array
    executePayment(string $paymentId, ?string $payerId = null): bool
    getPaymentDetails(string $paymentId): array
    getApprovalUrl(string $paymentId, array $paymentDetails): ?string
    getConfig(): array
    validateNotification(array $data): bool
    getVerificationMethod(): string                  // 'webhook' | 'redirect' | 'both'
    getPaymentToken(string $paymentId, int $userId): ?PaymentToken     // from the base
    createPaymentToken(array $tokenData): PaymentToken                 // from the base
    handlePaymentSuccess(string $paymentId, array $paymentData): array // from the base
    ```

    Plus `protected function getGatewayName(): string`.
  </Accordion>

  <Accordion title="Optional gateway interfaces" icon="plug">
    | Interface                                           | Adds                                                                                         |
    | --------------------------------------------------- | -------------------------------------------------------------------------------------------- |
    | `App\Services\Payment\HandlesGatewayWebhook`        | `handleWebhook(Request $r): Response`                                                        |
    | `App\Services\Payment\GatewayVariantInterface`      | `getGatewayVariants(): ?array`, `resolveVariant(string $k): bool`, `getModuleName(): string` |
    | `App\Services\Payment\GatewayFlowProviderInterface` | `getGatewayFlowHandler(): object`                                                            |
  </Accordion>

  <Accordion title="getConfig() shape" icon="gear">
    ```php theme={null}
    [
        'name' => 'Acme',              // internal
        'display_name' => 'Acme Pay',  // shown at checkout
        'description' => '...',
        'logo' => '/modules-logos/acme-gateway.png',
        'enabled' => bool,             // gates visibility entirely
        'mode' => 'sandbox',           // optional
    ]
    ```
  </Accordion>

  <Accordion title="handlePaymentSuccess() types" icon="list">
    `$paymentData['type']` is one of `market`, `renewal`, `invoice`, `upgrade`, `downgrade`,
    `configurable_options_change`, `addon`, `balance`, `standalone`.
  </Accordion>
</AccordionGroup>

## OAuth provider modules

Discovered by `App\Modules\OAuthProviderRegistry`, which scans enabled modules whose name contains
`Provider`.

| Concern                | Where                                                                                                |
| ---------------------- | ---------------------------------------------------------------------------------------------------- |
| Discovery              | `OAuthProviderRegistry::all()`, `forLogin()`, `forUser(?User)`                                       |
| Required manifest keys | `link_url`, `link_column`, `logo`                                                                    |
| Button colours         | `colors` in `module.json` (`bg`, `text`, `hover`), else a built-in table by name                     |
| Driver registration    | `$socialiteDriver` and `$socialiteProvider` on `OAuthProviderServiceProvider`                        |
| Credentials            | Module model, passed per request with `Socialite::driver(...)->setConfig(new Config(...))`           |
| Registration           | `UserRegistrationService::registerViaOAuth($user, $key, $label, $linkColumn, $avatar, $extra)`       |
| Consumers              | `Auth\LoginController` and `MarketController` use `forLogin()`, `ProfileController` uses `forUser()` |

<Warning>
  `registerViaOAuth()` resolves by link column, then by email **only if that account already carries
  the same provider ID**, then creates. An email match on an unlinked account throws
  `"...already exists but is not linked to {$label}"`, which each controller surfaces verbatim.

  Don't relax that: matching on email alone is an account-takeover path.
</Warning>

## Core base classes and interfaces

| Class                              | Namespace                      | For                                                  |
| ---------------------------------- | ------------------------------ | ---------------------------------------------------- |
| `BaseRenewService`                 | `App\Services`                 | Service renewal                                      |
| `BaseCancelService`                | `App\Services`                 | Service cancellation                                 |
| `CancelServiceInterface`           | `App\Contracts`                | Cancellation contract                                |
| `RenewServiceInterface`            | `App\Services`                 | Renewal contract                                     |
| `BasePaymentService`               | `App\Services\Payment`         | Gateway payment service                              |
| `PaymentGatewayInterface`          | `App\Services\Payment`         | Gateway contract                                     |
| `PaymentGatewayManager`            | `App\Services\Payment`         | Gateway registry and resolution                      |
| `ModulePaymentController`          | `App\Http\Controllers\Payment` | Gateway payment controller                           |
| `ModuleSettingsController`         | `App\Http\Controllers\Payment` | Declarative admin settings, any module type          |
| `BaseModuleServiceProvider`        | `App\Modules`                  | Shared module bootstrapping                          |
| `BaseModuleRouteServiceProvider`   | `App\Modules`                  | Shared route wiring                                  |
| `ServiceModuleServiceProvider`     | `App\Modules`                  | Service module provider base                         |
| `GatewayModuleServiceProvider`     | `App\Services\Payment\Modules` | Gateway provider base                                |
| `OAuthProviderServiceProvider`     | `App\Modules`                  | OAuth provider base                                  |
| `AddonModuleServiceProvider`       | `App\Modules`                  | Addon provider base                                  |
| `EmptyGatewayEventServiceProvider` | `App\Services\Payment\Modules` | Event provider for gateways with no listeners        |
| `ModuleContracts`                  | `App\Modules`                  | The extension-point spec that drives `module:doctor` |
| `ModulePermissionRegistry`         | `App\Modules`                  | Merges module permissions into core                  |
| `OAuthProviderRegistry`            | `App\Modules`                  | Enabled OAuth providers                              |
| `ServiceControlLoader`             | `App\Services`                 | Loads `ServiceControlsService`                       |
| `ModulePath`                       | `App\Support`                  | Module paths and groups                              |

## Core events

| Event                                                                            | Fires when                     |
| -------------------------------------------------------------------------------- | ------------------------------ |
| `UserRegistered`                                                                 | A user registers               |
| `ServiceCheckoutInitiated`                                                       | Checkout starts                |
| `ServicePaymentCompleted`                                                        | Payment for a service succeeds |
| `InvoicePaid`, `StandaloneInvoicePaid`                                           | An invoice is marked paid      |
| `ServiceRenewalInitiated`, `ServiceRenewed`                                      | Renewal starts, completes      |
| `ServiceUpgradeCompleted`, `ServiceDowngradeCompleted`                           | A plan change completes        |
| `ServiceConfigurationChanged`                                                    | Configurable options changed   |
| `AddonPurchased`                                                                 | An addon is bought             |
| `ServiceSuspended`, `ServiceUnsuspended`                                         | Suspension state changes       |
| `ServiceTerminated`, `ServiceCancelled`                                          | A service ends                 |
| `ServiceWarned`                                                                  | An overdue warning is issued   |
| `ServiceAutoRenewalToggled`                                                      | Auto-renew switched            |
| `OrderRequiresManualApproval`                                                    | An order is held for review    |
| `BankWireProofSubmitted`                                                         | Bank transfer proof uploaded   |
| `TicketCreated`, `TicketMessageSent`, `TicketStatusChanged`, `TicketTransferred` | Support tickets                |
| `DiscordLinked`, `DiscordUnlinked`                                               | Discord link state             |

<Info>All post-events. None can veto or change the action.</Info>

## Core jobs that call modules

| Job                                      | Resolves                             |
| ---------------------------------------- | ------------------------------------ |
| `CreateServiceJob`                       | `ModuleServiceActions`               |
| `HandleUpgradeService`                   | `UpgradeService`                     |
| `HandleDowngradeService`                 | `DowngradeService`                   |
| `HandleAddonService`                     | `AddonService`                       |
| `HandleConfigurableOptionsChangeService` | `ModuleServiceActions`               |
| `SubscriptionJob`                        | `RenewService`, `Services\Actions\*` |

## Middleware

| Middleware                        | Use                                                     |
| --------------------------------- | ------------------------------------------------------- |
| `AdminMiddleware`                 | Admin only                                              |
| `PermissionMiddleware`            | Permission check, from route defaults or the merged map |
| `AdminReauthMiddleware`           | Password reconfirmation for sensitive admin actions     |
| `AdminAuditLogMiddleware`         | Logs admin mutations                                    |
| `ModuleServiceAuditLogMiddleware` | Logs module service panel actions                       |
| `CheckServicePermissions`         | Sub-user permissions on `/service/{serviceId}/...`      |
| `ServiceRoutesMiddleware`         | Routes the request to the right service module          |
| `CheckUserSuspension`             | Blocks suspended users                                  |
| `CheckIpBan`                      | IP bans                                                 |
| `Require2FA`                      | Enforces 2FA                                            |

## Route files

| File           | Loaded by                     | Prefix and middleware                         |
| -------------- | ----------------------------- | --------------------------------------------- |
| `web.php`      | `RouteServiceProvider::map()` | `web`                                         |
| `api.php`      | `RouteServiceProvider::map()` | prefix `api`, name `api.`, middleware `api`   |
| `settings.php` | `require` from `web.php`      | admin stack, `/admin/{module}/...`            |
| `panel.php`    | `require` from `web.php`      | `service/{serviceId}` plus service middleware |
| `payment.php`  | `require` from `web.php`      | `auth`, `verified`                            |
| `console.php`  | The provider, after boot      | not applicable, `Schedule::` calls            |

## module.json fields

| Field                               | Required | Used by                                               |
| ----------------------------------- | -------- | ----------------------------------------------------- |
| `name`                              | yes      | Namespace segment, `Service.module`, everything       |
| `alias`                             | yes      | Config, translation and view keys                     |
| `description`                       | yes      | Admin module list, gateway checkout                   |
| `category`                          | yes      | Decides the group directory                           |
| `providers`                         | yes      | Laravel provider registration                         |
| `display`                           | no       | UI label. Set it: the fallback can surface your alias |
| `logo`                              | no       | Admin list, checkout, login buttons                   |
| `hasSettings`, `settingsUrl`        | no       | Settings button in the admin module list              |
| `permissions`                       | no       | Admin permission nodes and route mapping              |
| `link_url`, `link_column`, `colors` | OAuth    | Login and profile buttons                             |
| `keywords`, `priority`, `files`     | no       | nwidart, unused by DezerX logic                       |

### permissions shape

```json theme={null}
"permissions": {
    "category": "gateway_settings",
    "category_name": "Gateway & Service Settings",
    "nodes": {
        "/admin/acme/settings": {
            "label": "Manage Acme",
            "routes": ["/acme/settings", "/acme/test-connection"]
        },
        "DELETE:/admin/acme/backups/{id}": { "label": "Delete Acme Backups" }
    }
}
```

<Warning>
  Node keys are stored in `AdminRole.permissions` JSON, so never rename one after release. Routes omit
  the `/admin` prefix. A node with no category is grantable to superadmins only.
</Warning>

Existing categories you can add to: `gateway_settings` ("Gateway & Service Settings") and
`addon_module_settings` ("Addon Module Settings").

## Settings page definition

Returned from `settingsDefinition()`. Rendered by the shared `admin/ModuleSettings` page.

<ParamField body="title" type="string" required>
  Heading shown on the page.
</ParamField>

<ParamField body="description" type="string" required>
  One line under the heading.
</ParamField>

<ParamField body="update_url" type="string" required>
  Where the form POSTs. Use `url()`, not `route()`.
</ParamField>

<ParamField body="fields" type="array" required>
  See [field types](/extensions/tutorials/fundamentals#field-types).
</ParamField>

<ParamField body="groups" type="array">
  Splits the form into tabs. Each entry takes `key`, `label` and optional `description`.
</ParamField>

<ParamField body="test_url" type="string">
  Adds a Test connection button to the header.
</ParamField>

<ParamField body="test_label" type="string">
  Overrides the button text.
</ParamField>

<ParamField body="notices" type="string[]">
  Sidebar hints. Ones containing a URL get a copy button.
</ParamField>

<ParamField body="status_cards" type="array">
  Sidebar panels. Take `title`, `description`, `status`, `status_tone` and `rows`.
</ParamField>

<Note>
  `module_name`, `display_name`, `logo`, `category`, `kind_label`, `back_url` and `back_label` are
  filled in from `module.json` by `ModuleSettingsController`. You don't set them.
</Note>

## Commands

```bash theme={null}
php artisan make:module {Name} --type={service|gateway|provider|addon} [--webhook] [--enable]
php artisan module:doctor [{Name}...] [--json] [--strict]
php artisan module:list
php artisan module:enable {Name}
php artisan module:disable {Name}
php artisan module:migrate {Name}

composer dump-autoload            # after adding root-layout classes
php artisan route:clear && php artisan config:clear
vendor/bin/pest                   # test suite
```

<Info>
  `make:payment-gateway` still exists and works, but `make:module --type=gateway` supersedes it.
</Info>

## Things that still need a core change

| What                                                  | Where                                            |
| ----------------------------------------------------- | ------------------------------------------------ |
| A sidebar link without seeding a row                  | `SidebarLink` table, via the admin UI            |
| Replacing a string-resolved contract with a typed one | `App\Modules\ModuleContracts` plus the call site |

Everything else, including admin routes, permissions, OAuth registration and provider branding, is
declarable from the module itself.
