- name
- create-nexopos-module
- description
- Create, extend, or repair modules for the NexoPOS Laravel application. Use when a request involves scaffolding a module under modules/, module routes, controllers, models, migrations, permissions, settings, CRUD, menus, widgets, events, view injections, Blade/Vue, module Vite assets, dashboard Vue mounting (nsExtraComponents vs createApp), Tailwind module prefixes, POS cart (product-row meta, line-extra, unit price, order types, pay queue, cart buttons), or tests while following NexoPOS module conventions. Also use whenever the task is about mastering or extending the live POS cart.
# Create NexoPOS Modules
Build modules that match the current repository rather than relying on generic Laravel package patterns.
## Establish the scope
1. Inspect `AGENTS.md`, the requested feature, and nearby modules before changing files.
2. Identify the module namespace, display name, author, description, version, and required capabilities. Infer low-risk values from the request or existing module; ask only when a choice materially changes the result.
3. Determine whether to create a module or extend an existing one. Never overwrite an existing module unless the user explicitly requests it.
4. Search version-specific Laravel documentation before changing Laravel code, as required by the repository instructions.
5. Read [references/nexopos-module-conventions.md](references/nexopos-module-conventions.md). Load only the linked `.github/instructions` files relevant to the feature.
6. **Widget work (mandatory):** read `.github/instructions/nexopos-widgets.instructions.md` completely before reading or editing widget PHP, Vue, registration, layout policy, default order, or tests. Inspect `WidgetService`, `ns-dragzone.vue`, and one current widget.
7. **POS work (mandatory):** if the feature touches cart, products on POS, order types, payments, product-row UI, unit price, or once-per-line fees, read the full POS mastery guide: [references/pos-lifecycle.md](references/pos-lifecycle.md).
8. For `nsHttpClient`, frontend globals, notifications, localization, or module TypeScript declarations, read [references/frontend-apis.md](references/frontend-apis.md).
9. For module Vue + Tailwind (shared runtime, Tailwind prefix, UI conventions), read [references/module-frontend.md](references/module-frontend.md).
10. **Any dashboard Vue page or UI under `#dashboard-content`:** read [references/dashboard-vue-mounting.md](references/dashboard-vue-mounting.md) first. Nested `createApp()` there breaks reactivity.
11. For a settings-managed external font or reusable asset registry, read [references/module-font-registries.md](references/module-font-registries.md).
12. For module tests or module PHPUnit configuration, read [references/module-testing.md](references/module-testing.md).
13. **Custom fields under a cart product line:** `ns-pos-product-row-components` — not raw HTML.
14. **Once-per-line money (room, setup fee):** `ns-pos-product-line-extra` — not unit price × qty.
## Master POS extensions
There is no separate skill for POS: **this skill + [pos-lifecycle.md](references/pos-lifecycle.md)** are authoritative for module POS work.
### Load order for POS tasks
1. [pos-lifecycle.md](references/pos-lifecycle.md) — decision matrix, hooks, pricing, product-row, queues
2. Source of truth: `resources/ts/pos-init.ts`, `ns-pos-cart.vue`
3. Reference modules: `modules/NsAppointments` (row meta + line-extra + order types), `modules/NsGastro` (cart buttons)
### Pricing (do not get this wrong)
```text
line_total = (unit_price × quantity − discount) + line_extra
```
| Kind of amount | Filter / path | Multiplied by qty? |
| --- | --- | --- |
| Service / product unit | `ns-pos-product-*-price`, `ns-pos-product-unit-price` | **Yes** |
| Room / setup / cover fee on the same line | `ns-pos-product-line-extra` | **No** (once per line) |
Never bake a once-fee into unit price: quantity 2 would charge the fee twice.
### Cart line UI
- Register with `nsHooks.addFilter('ns-pos-product-row-components', …)` + `markRaw()`.
- Prefer **Options API + string `template`** for components injected into POS cart (dual-Vue: POS app ≠ `NexoPOSVue` SFCs).
- Update lines with `POS.updateProduct(product, patch, index)`.
- Gate on flags set in `addToCartQueue` (merge **`productData`**, not only `this.product` — unit `$quantities` live there).
### Boot checklist (every POS module)
| Step | Mechanism |
| --- | --- |
| Load assets only on POS | `RenderFooterEvent` → `@moduleViteAssets('Resources/ts/pos.ts', …)` |
| Blocking boot requirement | `POS.bootGuards` (no timeout; operator/security input) |
| Init context | `POS.initialQueue` |
| Enrich products | `POS.addToCartQueue` |
| Product-row UI | `ns-pos-product-row-components` |
| Once-per-line fees | `ns-pos-product-line-extra` |
| Cart buttons | `ns-after-cart-reset` (priority ≥ 20) → `POS.cartButtons` / `cartHeaderButtons` |
| POS header buttons | `ns-pos-header` → `header.buttons.MyButton` (Options API + string `template` preferred) |
| Order types | PHP `ns-orders-types` + enabled in `ns_pos_order_types` + optional `orderTypeQueue` |
| Before Pay | `ns-pay-queue` classes |
| On submit | `ns-order-before-submit` (sync only) |
| Module i18n | Always `__m('Text', 'ModuleNamespace')` in PHP and Vue/TS (no `t()` wrappers) |
Compare `order.type.identifier`, never `order.type === 'booking'`.
Full detail, complete examples, and debugging: [references/pos-lifecycle.md](references/pos-lifecycle.md).
## Prefer repository evidence
Use this priority when examples conflict:
1. Working code in a maintained, comparable module
2. Current framework and NexoPOS APIs in `app/` / `resources/ts/pos-init.ts`
3. Relevant `.github/instructions/*.instructions.md` guidance
4. Generic Laravel conventions
Inspect at least one comparable module and the core class or API being extended. Preserve its naming, registration, localization, authorization, and testing patterns.
## Scaffold safely
For a new module, inspect the command first with `php artisan make:module --help`, then run it non-interactively:
```bash
php artisan make:module --no-interaction \
--namespace=ExampleModule \
--name="Example Module" \
--author="Example Author" \
--description="What the module does" \
--vers=1.0
```
Do not pass `--force` without explicit overwrite authorization. Add only directories and files required by the feature; the generator supplies the baseline structure.
For PHP classes not supplied by a NexoPOS generator, use the appropriate `php artisan make:* --no-interaction` command when it supports the target location. Otherwise, follow a sibling module exactly.
### Scheduled jobs and the module console kernel
Modules register scheduled work through `Console/Kernel.php`, not `Routes/console.php`. Laravel's application bootstrap calls `withSchedule()` in `bootstrap/app.php`; NexoPOS then includes `bootstrap/modules-schedule.php`, which scans enabled/autoloaded modules and invokes `schedule(Schedule $schedule)` on `Modules\{Namespace}\Console\Kernel`.
Use the generator when adding the kernel:
```bash
php artisan modules:kernel ExampleModule --no-interaction
```
Define the schedule in the module kernel and keep the method public because NexoPOS calls it from the bootstrap bridge:
```php
namespace Modules\ExampleModule\Console;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
use Modules\ExampleModule\Jobs\RefreshExampleDataJob;
class Kernel extends ConsoleKernel
{
public function schedule( Schedule $schedule ): void
{
$schedule->job( new RefreshExampleDataJob )
->everyFifteenMinutes()
->withoutOverlapping()
->name( 'example-module:refresh-data' );
}
}
```
This keeps module jobs in the same Laravel scheduler as core jobs, so they run under the normal `schedule:run` / `schedule:work` process and can use scheduler features such as overlap locks, single-server execution, names, environments, and background execution. Do not register schedules from the module service provider, and do not assume a module `Routes/console.php` file will be loaded; the current module loader only uses that path as metadata. A module kernel is considered only when the module is enabled or explicitly autoloaded. After adding or changing one, inspect the result with `php artisan schedule:list --no-interaction`.
The bootstrap bridge currently constructs the kernel with the application and event dispatcher arguments. Extend Laravel's `ConsoleKernel` scaffold or otherwise provide a compatible constructor; do not replace it with a zero-argument custom kernel without checking that contract.
## Implement in vertical slices
Build the smallest complete path through the module:
1. Define storage and domain behavior with module-prefixed tables, models, services, and migrations where needed.
2. Add authorization before exposing operations. Define module permissions, protect server routes or controllers, and keep UI permission checks as a convenience rather than the security boundary. When creating permissions, grant them to `admin` by default (or another explicitly required operational role). Never suggest assigning new permissions to the `user` role by default.
3. Add request validation, controllers, and routes. Always add `Illuminate\Routing\Middleware\SubstituteBindings` explicitly to module route groups because NexoPOS does not add it automatically. Module `Routes/api.php` is already mounted under `/api`; do not add an `api` prefix inside it.
4. Integrate through current NexoPOS events, listeners, menus, settings, CRUD APIs, or widgets. Prefer event-based view injection; do not restore removed hook patterns.
5. Add Blade or Vue UI only when required. Reuse existing NexoPOS components, semantic theme classes, localization helpers, and frontend globals.
6. Add focused PHPUnit coverage for happy paths, authorization or validation failures, and relevant edge cases.
Every visible field created through a NexoPOS form descriptor, `FormInput`, settings page, or CRUD form must include a concise localized `description`. Explain the field's operational effect, units, scope, or consequences instead of merely repeating its label. Use the module localization helper for module-owned descriptions.
Keep business logic out of controllers and listeners when it warrants a service. Use explicit PHP types, Laravel 12 conventions, factories in tests, and existing module namespaces: `Modules\{Namespace}\...`.
## Observe module invariants
- Keep `config.xml`, the module directory, the main module class, PHP namespaces, view namespace, translation namespace, and asset namespace consistent.
- Use PascalCase for the module namespace and a module-specific lowercase prefix for tables, routes, option keys, and permissions.
- Always wrap module routes with `SubstituteBindings::class`; NexoPOS route discovery does not apply this middleware automatically. Import `Illuminate\Routing\Middleware\SubstituteBindings` and include it in the route group middleware array for both `Routes/api.php` and `Routes/web.php`:
```php
use Illuminate\Routing\Middleware\SubstituteBindings;
use Illuminate\Support\Facades\Route;
Route::middleware([
SubstituteBindings::class,
])->group(function (): void {
// Module routes...
});
```
- Before creating a permission or role, check both unique fields: `namespace` and `name`. Use namespace lookup first to preserve existing records, then verify the intended name is not already used by another record before inserting. If a name collision exists, choose a module-scoped unique name or abort with a clear migration error; never blindly create permissions or roles.
- For `ns-dashboard-menus` integration, remember `AsideMenu::menu()` and `AsideMenu::subMenu()` already return keyed arrays (`[identifier => config]`). Insert or merge them directly with `array_insert_after`, `array_insert_before`, `array_merge`, or spread syntax. Never wrap them again under the same identifier key, or you create double nesting.
- For `SettingsPage::validateForm()` rules, tab fields are posted under their tab identifier. If a field named `ns_example_enabled` lives in a tab with identifier `general`, validate it as `general.ns_example_enabled`; array children use `general.ns_example_ids.*`. Keep field `name` as the option key so `getPlainData()` can flatten it for storage.
- Use named routes for generated links.
- **Blade JSON serialization:** never pass an inline multiline array directly to `@json`, such as `@json([ ... ])`; Blade may parse it incorrectly. Assign the array to a PHP variable in an `@php` block, then render `@json($config)`.
- Use **`__m('Text', 'ModuleNamespace')` for all module-owned strings** (PHP and Vue/TS). Do not wrap copy in `t()` / `translate()` — NexoPOS scans `__m(...)` for translations. Frontend: global `__m` / `window.__m` on dashboard and POS.
- Treat the live POS `order.type` value as an order-type object. Compare `order.type.identifier`, not `order.type` itself, with an identifier string.
- **POS product-row meta:** `ns-pos-product-row-components` + `markRaw()` + gate on product flags + `POS.updateProduct`. Prefer Options API string templates for POS-injected components. Reference: `modules/NsAppointments` (`AppointmentsCartMeta.ts` + `pos.ts`).
- **POS once-per-line fees:** `ns-pos-product-line-extra` (e.g. room). Unit price stays service-only. See [pos-lifecycle.md](references/pos-lifecycle.md#unit-price-vs-once-per-line-extra).
- **POS cart fields that must reload:** name cart keys like **order product DB columns**; migrate columns; copy from `getData()` in BeforeCreated/BeforeUpdated. Flash `getData()` alone is not durable. See [pos-lifecycle.md § Persist cart fields](references/pos-lifecycle.md#persist-cart-fields-on-order-products-round-trip).
- **POS add-to-cart:** queue results merge via `productData`; always merge `$quantities` from `productData` when reading sale price.
- Let NexoPOS discover module routes, migrations, listeners, commands, and providers where current code does so. Do not duplicate registration.
- Do not register console commands or schedules from a module service provider.
- Make migrations repeat-safe and rollback-safe. Inspect the live schema and comparable migrations before choosing columns or constraints.
- **Before every migration schema operation, check existence:** use `Schema::hasTable()` before altering or dropping a table, `Schema::hasColumn()` before adding or dropping each column, and `Schema::hasIndex()` before adding or dropping each index. Use `Schema::createIfMissing()` for module tables. Guard columns individually so a migration can recover safely after a previous partial DDL failure; never assume that because one new column exists, the remaining columns or indexes also exist.
- Avoid cascade deletion where NexoPOS conventions require application-managed cleanup.
- Use model events only for model-local state. Put broader side effects in listeners, services, or jobs.
- Do not introduce dependencies or new top-level directories without approval.
- **Dashboard Vue:** never `createApp()` / `nsCreateApp().mount()` inside `#dashboard-content`. Register on `nsExtraComponents` and use a component tag so the UI is a child of `nsDashboardContent`. See [references/dashboard-vue-mounting.md](references/dashboard-vue-mounting.md).
- **Dashboard widgets:** declare the suggested `1x1`–`3x5` footprint and an intentional strict/restricted/unrestricted policy; pass the `widget` prop; provide `widget-handle` and `onRemove`; and place `ns-widget-layout-selector` inside the widget template only when sizing should be exposed. Preserve `WidgetService::DEFAULT_WIDGET_ORDER` packing when core footprints change.
## Handle frontend assets correctly
Load module assets from Blade with paths relative to the module root and no leading slash. On dashboard pages, load the standalone CSS entry in the header and load the Vue registration script in the footer inject before `app-init`:
```blade
@section('layout.dashboard.header')
@parent
@moduleViteAssets('Resources/css/style.css', 'ExampleModule')
@endsection
@section('layout.dashboard.footer.inject')
@parent
@moduleViteAssets('Resources/ts/page.ts', 'ExampleModule')
@endsection
```
Treat this placement as required when `style.css` supplies prefixed Tailwind utilities: declaring or importing the TypeScript entry does not replace explicitly loading the CSS entry in the layout header. Include both files as Vite inputs, and do not rely on a TypeScript-side CSS import for dashboard page styles. Do not use `@vite` for module assets. Keep Vite inputs and output aligned with `Resources/...` and `Public/build`, and use Tailwind CSS v4 semantic/theme-aware classes rather than hard-coded colors. Build module assets when frontend files change.
For POS-only module UI, scope two listeners to the POS route: add the standalone prefixed CSS view through `RenderHeaderEvent`, and add the TypeScript view through `RenderFooterEvent`. A footer TypeScript directive or a TypeScript-side CSS import is not a substitute for loading the stylesheet in the document header.
### Vue + Tailwind modules (required pattern)
Full detail: [references/module-frontend.md](references/module-frontend.md).
**Dashboard mount (read this):** [references/dashboard-vue-mounting.md](references/dashboard-vue-mounting.md).
**POS mount / hooks:** [references/pos-lifecycle.md](references/pos-lifecycle.md).
1. **Dashboard vs standalone mount**
- **Inside `#dashboard-content`:** only `nsExtraComponents['my-page'] = MyPage` + `<my-page>` in Blade. Script in footer inject before `app-init`. **No** nested `createApp`.
- **Standalone** (no dashboard content root): `nsCreateApp(Page).mount('#root')` is OK.
- Symptom of the wrong approach: UI visible, clicks/`ref` dead after page load.
2. **Shared Vue** — `defineNexoPOSModuleConfig` (or `nexoposVueRuntime()`). Prefer `.vue` SFCs for dashboard; for **POS cart injection**, prefer Options API + string `template` (see pos-lifecycle dual-Vue note). Never bundle a second Vue.
3. **Tailwind prefix** — every module CSS entry that imports Tailwind **must** use a short module prefix:
GitHubで見る