| name | criar-modulo |
| description | Use ao criar novo módulo Laravel modular (nWidart) no oimpresso — qualquer pasta nova em `Modules/<Nome>/`, ou pedido explícito "criar módulo", "novo módulo", "scaffold módulo". Carrega checklist das 8 peças obrigatórias + 3 rotas admin Install (sem elas botão Install fica sem ação) + padrão `Route::has()` pra link público condicional + pegadinhas. Substitui leitura repetida de ADR 0011 + 0024 + receita ADS/Repair. |
| trust_level | L2 |
| owner | wagner |
| parent_mission | meta-skill-roi-erp-autonomo |
| charter_adr | 0080 |
| tier | B |
| parent_adr | 0095 |
Criar módulo Laravel no Oimpresso ERP
Quando ativa
- Pedido explícito: "criar módulo", "novo módulo", "scaffold um módulo X"
- Edit/Write em qualquer arquivo dentro de
Modules/<Nome novo>/
- Adição de entrada nova em
modules_statuses.json
Fonte canônica completa
memory/requisitos/Infra/RUNBOOK-criar-modulo.md — receita reproduzível com troubleshooting.
Checklist mínimo (não pular nenhum)
Módulo aparecer em /manage-modules com botão Install funcional + opcionalmente sidebar exige 8 peças:
| # | Arquivo | Por quê |
|---|
| 1 | module.json | nWidart enxerga + provider list |
| 2 | composer.json | psr-4 Modules\\<Nome>\\: "" |
| 3 | Config/config.php | mergeConfigFrom + publishes |
| 4 | Providers/<Nome>ServiceProvider.php | register Config + lang + RouteServiceProvider |
| 5 | Providers/RouteServiceProvider.php | mapWebRoutes (e api se houver) |
| 6 | Http/Controllers/DataController.php | 3 hooks: superadmin_package + user_permissions + modifyAdminMenu |
| 7 | Http/Controllers/InstallController.php | extends BaseModuleInstallController |
| 8 | Routes/web.php | rotas do módulo + 3 rotas admin Install (§críticas) |
E mais 3 fora da pasta do módulo:
modules_statuses.json (raiz) — entrada "<Nome>": true
- (Se rotas públicas) link condicional via
Route::has() em home_header.blade.php + auth2.blade.php + HandleInertiaRequests.php publicRoutes + SiteHeader.tsx
- ⚠️
phpunit.xml — quando criar a primeira Tests/Feature/*Test.php do módulo, adicionar <directory>./Modules/<Nome>/Tests/Feature</directory> (e Unit se houver) dentro da <testsuite name="Feature">. Esquecer = testes no repo mas CI nunca roda → falsa sensação de cobertura. Erro recorrente; ver memory/requisitos/Infra/RUNBOOK-pest-suite.md.
§Críticas — 3 rotas admin Install OBRIGATÓRIAS
Sem isso o botão Install na tela /manage-modules fica visível mas SEM AÇÃO (vai pra #). Install/ModulesController.php:57 usa action() que precisa de rota registrada apontando pro InstallController.
use Modules\<Nome>\Http\Controllers\InstallController;
Route::middleware(['web', 'authh', 'auth', 'SetSessionData', 'language', 'timezone', 'AdminSidebarMenu'])
->prefix('<modulo-prefix>')
->group(function () {
Route::get('install', [InstallController::class, 'index']);
Route::get('install/uninstall', [InstallController::class, 'uninstall']);
Route::get('install/update', [InstallController::class, 'update']);
});
Vale mesmo se o módulo só expõe rotas públicas (caso ConsultaOs).
Link público condicional (Route::has())
Se o módulo expõe rota pública (ex: /consulta-os, /repair-status) que deve aparecer no header do CMS APENAS quando o módulo está ativo:
Blade legado: adicionar em home_header.blade.php + auth2.blade.php:
@if(Route::has('<rota-nomeada>'))
<li><a href="{{ route('<rota-nomeada>') }}">Acompanhar pedido</a></li>
@endif
Inertia: adicionar flag em HandleInertiaRequests::share() chave publicRoutes e ler em SiteHeader.tsx via usePage().props.publicRoutes. Quando módulo é desativado em /manage-modules, a rota some, Route::has() vira false, link some.
Referências canônicas pra imitar
| Caso | Imitar |
|---|
| Só rotas públicas + Install routes | Modules/ConsultaOs/ (validado 2026-05-04) |
| Sidebar admin completa + service singletons | Modules/ADS/ (validado 2026-05-03) |
| CRUD multi-tenant | Modules/Repair/, Modules/Project/, Modules/Jana/ |
| Spec-driven | Modules/NFSe/ |
Pegadinhas críticas
- ❌ NÃO usar
__('alias::file.key') em DataController/topnav — LegacyMenuAdapter lê literal, não resolve traduções → labels saem crus em prod. Hardcodar PT-BR (NFSe sempre fez assim).
- ❌ NÃO usar
route('xxx.yyy') em Pages React — Ziggy não está disponível. Usar template literal: href={`/<prefix>/admin/${id}`}.
- ❌ NÃO esquecer das rotas admin Install se o módulo tem só rotas públicas — botão Install fica sem ação.
- ❌ NÃO rodar
npm run build (config errado) — sempre npm run build:inertia pra gerar Pages no manifest.
- ❌ NÃO esquecer de rodar
composer install no Hostinger pós-deploy se mexeu em composer.json/lock — sintoma: tela branca Inertia (null.component).
⚠️ Erros frequentes em DataController (pattern UltimatePOS exige formato exato)
superadmin_package — DEVE retornar array de arrays com name field, NÃO array com keys string:
public function superadmin_package() {
return [
'meu_modulo' => [
'label' => '...',
'default' => false,
],
];
}
public function superadmin_package() {
return [
[
'name' => 'meu_modulo',
'label' => '...',
'default' => false,
],
];
}
Por quê: Modules/Accounting/Helpers/general_helper.php:303 faz $permission[0]['name'] — se você passou key string, $permission não tem índice 0.
Middleware stack das rotas admin — pattern canônico tem 'authh' (com duplo h) + 'SetSessionData':
Route::middleware(['web', 'authh', 'auth', 'SetSessionData', 'language', 'timezone', 'AdminSidebarMenu', 'CheckUserLogin'])
->prefix('meu-modulo')
->group(function () { ... });
Rotas Install usam index (não install) e URL install (não install/install):
Route::get('install', [InstallController::class, 'index']);
Route::get('install/uninstall', [InstallController::class, 'uninstall']);
Route::get('install/update', [InstallController::class, 'update']);
⚠️ Schemas DB que controllers acessam — VERIFICAR antes de escrever query
Erros comuns (não chute schema):
mcp_memory_documents — tem coluna status direta (varchar20), não frontmatter_json LIKE '%"status"%'
mcp_audit_log — usa ts como timestamp canonical (não só created_at); endpoint é ENUM(7 valores: tools/list, tools/call, resources/list, resources/read, prompts/list, prompts/get, initialize)
mcp_skill_approvals — registra decision (approve/reject/request_changes), não status. "Pending" semanticamente = mcp_skill_versions.status='review'
mcp_alertas — tem kind (enum 5 valores: cota_excedida/tool_destrutiva/ip_suspeito/taxa_errors/cliente_externo), NÃO category/severity/module/detail
mcp_governance_rules.category — enum (promotion/archival/escalation/retry/budget/review)
Sempre rodar DESCRIBE <tabela> antes de escrever query nova:
ssh -4 -i ~/.ssh/id_ed25519_oimpresso -p 65002 u906587222@148.135.133.115 \
'cd ~/domains/oimpresso.com/public_html && PASS=$(grep "^DB_PASSWORD=" .env | cut -d= -f2- | tr -d "\"") && \
mysql -u u906587222_oimpresso -p"$PASS" u906587222_oimpresso -e "DESCRIBE <tabela>;"'
⚠️ Translations: pasta pt/ (não pt-BR/) é o pattern UltimatePOS
Modules/<Nome>/Resources/lang/
├── pt/
│ └── <alias>.php
└── en/
└── <alias>.php
KB tem ambos pt/ e pt-BR/ por histórico, mas TeamMcp/ADS/NFSe canonical é só pt/ + en/.
ServiceProvider.registerTranslations() carrega __DIR__ . '/../Resources/lang' — Laravel resolve por locale automático.
⚠️ Lição de aprendizado registrada
Erro real cometido em 2026-05-06 ao criar Modules/Governance:
- ❌ Não invoquei skill
criar-modulo antes de criar — perdi 4 round-trips de bugfix
- ❌
superadmin_package formato errado (key string em vez de name field) — Wagner viu 'Undefined array key 0'
- ❌ Middleware sem
authh + SetSessionData
- ❌ Rotas Install com
install/install em vez de só install, action install em vez de index
- ❌ Queries DB com colunas inventadas (
frontmatter_json, mcp_alertas.category, mcp_skill_approvals.status)
- ❌ Translations só em
pt-BR/ — pattern canonical é pt/ + en/
Antídoto: PRIMEIRO comando ao iniciar criação de módulo: invocar skill criar-modulo via tool Skill. Antes de escrever 1 linha de código novo em Modules/<Nome>/.
Validação local antes de comitar
php -l Modules/<Nome>/Http/Controllers/InstallController.php
php -l Modules/<Nome>/Routes/web.php
php artisan route:list --path=<prefix>/install
npm run build:inertia
grep -i "Pages/<Nome>" public/build-inertia/manifest.json
Deploy Hostinger pós-merge
ssh -4 -i ~/.ssh/id_ed25519_oimpresso -p 65002 u906587222@148.135.133.115 \
'cd ~/domains/oimpresso.com/public_html && git pull && composer dump-autoload --no-scripts && php artisan cache:clear && php artisan view:clear'
Depois login superadmin → /manage-modules → clicar Install no card do módulo.
Refs