@benup/bensdk 1.11.17 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/src/cli/init.js +224 -7
- package/bin/src/cli/init.js.map +1 -1
- package/bin/src/cli/templates/bensdk-base/.benSDKIgnore.template +3 -2
- package/bin/src/cli/templates/bensdk-base/.gitignore.template +4 -1
- package/bin/src/cli/templates/bensdk-base/README.md.template +52 -74
- package/bin/src/cli/templates/bensdk-base/context.config.ts.template +97 -21
- package/bin/src/cli/templates/bensdk-base/dev-test.example.json +22 -0
- package/bin/src/cli/templates/bensdk-base/docs/DEV_SERVER.md +201 -0
- package/bin/src/cli/templates/bensdk-docs/update-readme.ts +96 -0
- package/bin/src/cli/templates/bensdk-local-server/app.ts +234 -33
- package/bin/src/cli/templates/package.template.json +4 -1
- package/package.json +1 -1
package/bin/src/cli/init.js
CHANGED
|
@@ -26,6 +26,195 @@ const getAllFiles = (dirPath, arrayOfFiles = []) => {
|
|
|
26
26
|
});
|
|
27
27
|
return arrayOfFiles;
|
|
28
28
|
};
|
|
29
|
+
const BENEFIT_CATEGORIES = [
|
|
30
|
+
'Saúde',
|
|
31
|
+
'Alimentação',
|
|
32
|
+
'Transporte',
|
|
33
|
+
'Engajamento',
|
|
34
|
+
'Flexíveis',
|
|
35
|
+
'Auxílio Farmácia',
|
|
36
|
+
'Outros'
|
|
37
|
+
];
|
|
38
|
+
/** Convert BENEFIT-ID or BENEFIT_ID to Friendly Name */
|
|
39
|
+
function toFriendlyName(benefitID) {
|
|
40
|
+
return benefitID
|
|
41
|
+
.split(/[-_]/)
|
|
42
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
43
|
+
.join(' ');
|
|
44
|
+
}
|
|
45
|
+
function generateActionDiagram(action) {
|
|
46
|
+
switch (action) {
|
|
47
|
+
case 'GRANT':
|
|
48
|
+
return `### Concessão de Benefício (GRANT)
|
|
49
|
+
|
|
50
|
+
\`\`\`mermaid
|
|
51
|
+
flowchart TD
|
|
52
|
+
START([Início]) --> REQUESTED_GRANT
|
|
53
|
+
REQUESTED_GRANT -->|next| SYNC_EXISTING_GRANT
|
|
54
|
+
SYNC_EXISTING_GRANT -->|next| REQUEST_CREATE_EMPLOYEE
|
|
55
|
+
SYNC_EXISTING_GRANT -->|skip| GRANTED
|
|
56
|
+
REQUEST_CREATE_EMPLOYEE -->|next| GRANTED
|
|
57
|
+
REQUEST_CREATE_EMPLOYEE -->|fail| FAILED_TO_GRANT
|
|
58
|
+
|
|
59
|
+
GRANTED([GRANTED ✅])
|
|
60
|
+
FAILED_TO_GRANT([FAILED_TO_GRANT ❌])
|
|
61
|
+
\`\`\``;
|
|
62
|
+
case 'REVOKE':
|
|
63
|
+
return `### Revogação de Benefício (REVOKE)
|
|
64
|
+
|
|
65
|
+
\`\`\`mermaid
|
|
66
|
+
flowchart TD
|
|
67
|
+
START([Início]) --> REQUESTED_REVOKE
|
|
68
|
+
REQUESTED_REVOKE -->|next| SYNC_EXISTING_REVOKE
|
|
69
|
+
SYNC_EXISTING_REVOKE -->|next| REQUEST_REMOVE_EMPLOYEE
|
|
70
|
+
SYNC_EXISTING_REVOKE -->|skip| REVOKED
|
|
71
|
+
REQUEST_REMOVE_EMPLOYEE -->|next| REVOKED
|
|
72
|
+
REQUEST_REMOVE_EMPLOYEE -->|fail| FAILED_TO_REVOKE
|
|
73
|
+
|
|
74
|
+
REVOKED([REVOKED ✅])
|
|
75
|
+
FAILED_TO_REVOKE([FAILED_TO_REVOKE ❌])
|
|
76
|
+
\`\`\``;
|
|
77
|
+
case 'RECHARGE':
|
|
78
|
+
return `### Recarga de Benefício (RECHARGE)
|
|
79
|
+
|
|
80
|
+
\`\`\`mermaid
|
|
81
|
+
flowchart TD
|
|
82
|
+
START([Início]) --> REQUESTED_RECHARGE
|
|
83
|
+
REQUESTED_RECHARGE -->|next| CHECK_FOR_DUPLICATE_RECHARGES
|
|
84
|
+
CHECK_FOR_DUPLICATE_RECHARGES -->|next| PROCESS_RECHARGE
|
|
85
|
+
CHECK_FOR_DUPLICATE_RECHARGES -->|fail| RECHARGE_FAILED
|
|
86
|
+
PROCESS_RECHARGE -->|next| RECHARGE_SUCCEEDED
|
|
87
|
+
PROCESS_RECHARGE -->|fail| RECHARGE_FAILED
|
|
88
|
+
|
|
89
|
+
RECHARGE_SUCCEEDED([RECHARGE_SUCCEEDED ✅])
|
|
90
|
+
RECHARGE_FAILED([RECHARGE_FAILED ❌])
|
|
91
|
+
\`\`\``;
|
|
92
|
+
case 'GRANT_DEPENDENT':
|
|
93
|
+
return `### Concessão para Dependente (GRANT_DEPENDENT)
|
|
94
|
+
|
|
95
|
+
\`\`\`mermaid
|
|
96
|
+
flowchart TD
|
|
97
|
+
START([Início]) --> REQUESTED_GRANT_DEPENDENT
|
|
98
|
+
REQUESTED_GRANT_DEPENDENT -->|next| REQUEST_CREATE_DEPENDENT
|
|
99
|
+
REQUEST_CREATE_DEPENDENT -->|next| GRANTED_DEPENDENT
|
|
100
|
+
REQUEST_CREATE_DEPENDENT -->|fail| FAILED_TO_GRANT_DEPENDENT
|
|
101
|
+
|
|
102
|
+
GRANTED_DEPENDENT([GRANTED_DEPENDENT ✅])
|
|
103
|
+
FAILED_TO_GRANT_DEPENDENT([FAILED_TO_GRANT_DEPENDENT ❌])
|
|
104
|
+
\`\`\``;
|
|
105
|
+
case 'REVOKE_DEPENDENT':
|
|
106
|
+
return `### Revogação de Dependente (REVOKE_DEPENDENT)
|
|
107
|
+
|
|
108
|
+
\`\`\`mermaid
|
|
109
|
+
flowchart TD
|
|
110
|
+
START([Início]) --> REQUESTED_REVOKE_DEPENDENT
|
|
111
|
+
REQUESTED_REVOKE_DEPENDENT -->|next| REQUEST_REMOVE_DEPENDENT
|
|
112
|
+
REQUEST_REMOVE_DEPENDENT -->|next| REVOKED_DEPENDENT
|
|
113
|
+
REQUEST_REMOVE_DEPENDENT -->|fail| FAILED_TO_REVOKE_DEPENDENT
|
|
114
|
+
|
|
115
|
+
REVOKED_DEPENDENT([REVOKED_DEPENDENT ✅])
|
|
116
|
+
FAILED_TO_REVOKE_DEPENDENT([FAILED_TO_REVOKE_DEPENDENT ❌])
|
|
117
|
+
\`\`\``;
|
|
118
|
+
default:
|
|
119
|
+
return '';
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function generateReadme({ benefitID, benefitCategory, benefitDescription, actions }) {
|
|
123
|
+
const friendlyName = toFriendlyName(benefitID);
|
|
124
|
+
const metaLines = [];
|
|
125
|
+
if (benefitCategory)
|
|
126
|
+
metaLines.push(`**Categoria:** ${benefitCategory}`);
|
|
127
|
+
if (benefitDescription)
|
|
128
|
+
metaLines.push(benefitDescription);
|
|
129
|
+
const metaBlock = metaLines.length > 0 ? `\n> ${metaLines.join(' · ')}\n` : '';
|
|
130
|
+
const diagrams = actions
|
|
131
|
+
.map((a) => generateActionDiagram(a))
|
|
132
|
+
.filter(Boolean)
|
|
133
|
+
.join('\n\n');
|
|
134
|
+
return `<img src="ICON.png" alt="${friendlyName}" width="120">
|
|
135
|
+
|
|
136
|
+
# bensdk - ${friendlyName}
|
|
137
|
+
${metaBlock}
|
|
138
|
+
## Estrutura do Projeto
|
|
139
|
+
|
|
140
|
+
\`\`\`
|
|
141
|
+
├── src/
|
|
142
|
+
│ ├── benefit-definition.ts # Definição do benefício + máquina de estados
|
|
143
|
+
│ ├── handlers/ # Handlers executados pelo integrador
|
|
144
|
+
│ └── lib/ # Utilitários e mocks
|
|
145
|
+
├── bin/
|
|
146
|
+
│ ├── cli/ # CLI para geração de handlers e testes
|
|
147
|
+
│ └── server/ # Servidor local para testes (npm run dev:start)
|
|
148
|
+
├── ICON.png # Ícone do benefício
|
|
149
|
+
├── .benSDKIgnore # Ignora arquivos no empacotamento
|
|
150
|
+
└── dev-test.example.json # Modelo de credenciais para testes locais
|
|
151
|
+
\`\`\`
|
|
152
|
+
|
|
153
|
+
## Conceitos Básicos
|
|
154
|
+
|
|
155
|
+
### Actions
|
|
156
|
+
|
|
157
|
+
As **Actions** são os eventos enviados pelo integrador ao seu BenSDK. Cada action representa uma operação sobre um benefício (ex: concessão, revogação, recarga) e é processada de acordo com a **máquina de estados** definida em \`src/benefit-definition.ts\`.
|
|
158
|
+
|
|
159
|
+
### Handlers
|
|
160
|
+
|
|
161
|
+
Cada **estado** da máquina de estados corresponde a um arquivo de handler em \`src/handlers/\`. O handler recebe:
|
|
162
|
+
|
|
163
|
+
- \`message\` — contexto da mensagem recebida da fila
|
|
164
|
+
- \`action\` — dados da operação (funcionário, empresa, eligibilityOptions, ctx compartilhado)
|
|
165
|
+
- \`ctx\` — contexto de execução (APIs, logger, benefitDefinition)
|
|
166
|
+
|
|
167
|
+
**Exemplo:**
|
|
168
|
+
|
|
169
|
+
\`\`\`ts
|
|
170
|
+
const handler: StateHandler<ActionGrant, ActionLogGrant['REQUEST_CREATE_EMPLOYEE'], ActionCtx> =
|
|
171
|
+
async (message, action, ctx) => {
|
|
172
|
+
const res = await ctx.benefitPartnerAPI.post('/employees', { ... });
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
handlerResponse: { response: 'ACK' },
|
|
176
|
+
action: {
|
|
177
|
+
state: stateMachine.GRANT.REQUEST_CREATE_EMPLOYEE.next,
|
|
178
|
+
ctx: {},
|
|
179
|
+
logs: {}
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
};
|
|
183
|
+
\`\`\`
|
|
184
|
+
|
|
185
|
+
### ctx — Contexto compartilhado entre handlers
|
|
186
|
+
|
|
187
|
+
O campo \`ctx\` dentro de \`action\` é um objeto de estado acumulado ao longo de um fluxo. Use-o para passar dados entre etapas — por exemplo, um ID retornado pela API do parceiro que será necessário em handlers subsequentes. O schema do \`ctx\` é definido em \`benefit-definition.ts\`.
|
|
188
|
+
|
|
189
|
+
### eligibilityOptions
|
|
190
|
+
|
|
191
|
+
As \`eligibilityOptions\` são configurações por empresa que chegam junto com cada action (ex: código externo da empresa no sistema do parceiro, tipo de cartão). O schema é tipado em \`benefit-definition.ts\` e validado pelo integrador antes da execução.
|
|
192
|
+
|
|
193
|
+
## Fluxos
|
|
194
|
+
|
|
195
|
+
${diagrams}
|
|
196
|
+
|
|
197
|
+
## Comandos
|
|
198
|
+
|
|
199
|
+
| Comando | Descrição |
|
|
200
|
+
|---------|-----------|
|
|
201
|
+
| \`npm run dev:start\` | Inicia o servidor local com autenticação real contra a API do parceiro. [→ Ver documentação](docs/DEV_SERVER.md) |
|
|
202
|
+
| \`npm run test\` | Executa os testes unitários com Vitest |
|
|
203
|
+
| \`npm run generate\` | Gera handlers e testes a partir do \`benefit-definition.ts\` |
|
|
204
|
+
| \`npm run start\` | Abre o painel visual da máquina de estados |
|
|
205
|
+
| \`npm run lint\` | Valida tipos TypeScript, formatação e linting |
|
|
206
|
+
| \`npm run build\` | Compila o projeto para \`dist/\` |
|
|
207
|
+
|
|
208
|
+
## Publicação
|
|
209
|
+
|
|
210
|
+
1. Instale o [maria-cli](https://github.com/benup-dev/maria) seguindo as instruções do repositório.
|
|
211
|
+
2. Execute o comando abaixo na raiz do projeto:
|
|
212
|
+
|
|
213
|
+
\`\`\`bash
|
|
214
|
+
maria benefit deploy -r <link-ssh-do-repositorio> -n ${benefitID.toUpperCase()}
|
|
215
|
+
\`\`\`
|
|
216
|
+
`;
|
|
217
|
+
}
|
|
29
218
|
const staticFiles = getAllFiles(path.join(templatesDir, 'bensdk-base'));
|
|
30
219
|
export default async function (benefitID) {
|
|
31
220
|
const availableActions = ['GRANT', 'REVOKE'];
|
|
@@ -39,21 +228,35 @@ export default async function (benefitID) {
|
|
|
39
228
|
message: 'Select the available actions',
|
|
40
229
|
choices: ['RECHARGE', 'DEDUCTION', 'GRANT_DEPENDENT', 'REVOKE_DEPENDENT']
|
|
41
230
|
});
|
|
231
|
+
const { benefitCategory } = await inquirer.prompt({
|
|
232
|
+
type: 'list',
|
|
233
|
+
name: 'benefitCategory',
|
|
234
|
+
message: 'Categoria do benefício (opcional):',
|
|
235
|
+
choices: [
|
|
236
|
+
{ name: '(pular)', value: '' },
|
|
237
|
+
...BENEFIT_CATEGORIES.map((c) => ({ name: c, value: c }))
|
|
238
|
+
],
|
|
239
|
+
default: ''
|
|
240
|
+
});
|
|
241
|
+
const { benefitDescription } = await inquirer.prompt({
|
|
242
|
+
type: 'input',
|
|
243
|
+
name: 'benefitDescription',
|
|
244
|
+
message: 'Descrição curta do benefício (opcional, Enter para pular):'
|
|
245
|
+
});
|
|
42
246
|
const { projectGitSetup } = await inquirer.prompt({
|
|
43
247
|
type: 'confirm',
|
|
44
248
|
name: "projectGitSetup",
|
|
45
|
-
default:
|
|
46
|
-
message: '
|
|
249
|
+
default: true,
|
|
250
|
+
message: 'Configurar um projeto git com husky no pre-commit (recomendado)?'
|
|
47
251
|
});
|
|
48
252
|
const spinner = ora('Creating project').start();
|
|
49
|
-
|
|
253
|
+
const allActions = [...availableActions, ...customAvailableActions];
|
|
50
254
|
packageTemplate.name = benefitID;
|
|
51
255
|
let benefitDefinition = benefitDefinitionTemplate.replace(/benefitID: '.*'/, `benefitID: "${benefitID.toUpperCase()}"`);
|
|
52
256
|
if (!customAvailableActions.includes('RECHARGE')) {
|
|
53
257
|
benefitDefinition = benefitDefinition.replace(/\/\*\* <recharge> \*\/[\s\S]*?\/\*\* <\/recharge> \*\//g, '');
|
|
54
258
|
}
|
|
55
259
|
else {
|
|
56
|
-
// If RECHARGE is selected, just remove the recharge comment markers
|
|
57
260
|
benefitDefinition = benefitDefinition.replace(/\/\*\* <recharge> \*\/\n?/g, '');
|
|
58
261
|
benefitDefinition = benefitDefinition.replace(/\/\*\* <\/recharge> \*\/\n?/g, '');
|
|
59
262
|
}
|
|
@@ -62,7 +265,6 @@ export default async function (benefitID) {
|
|
|
62
265
|
benefitDefinition = benefitDefinition.replace(/\/\*\* <dependent> \*\/[\s\S]*?\/\*\* <\/dependent> \*\//g, '');
|
|
63
266
|
}
|
|
64
267
|
else {
|
|
65
|
-
// If RECHARGE is selected, just remove the recharge comment markers
|
|
66
268
|
benefitDefinition = benefitDefinition.replace(/\/\*\* <dependent> \*\/\n?/g, '');
|
|
67
269
|
benefitDefinition = benefitDefinition.replace(/\/\*\* <\/dependent> \*\/\n?/g, '');
|
|
68
270
|
}
|
|
@@ -84,25 +286,40 @@ export default async function (benefitID) {
|
|
|
84
286
|
});
|
|
85
287
|
fs.mkdirSync(`${appDirectory}/bin/cli`, { recursive: true });
|
|
86
288
|
fs.cpSync(path.join(templatesDir, 'bensdk-cli'), `${appDirectory}/bin/cli`, { recursive: true });
|
|
289
|
+
fs.mkdirSync(`${appDirectory}/bin/docs`, { recursive: true });
|
|
290
|
+
fs.cpSync(path.join(templatesDir, 'bensdk-docs'), `${appDirectory}/bin/docs`, { recursive: true });
|
|
87
291
|
staticFiles.forEach((relativeFilePath) => {
|
|
292
|
+
// README.md is generated dynamically below — skip the template copy
|
|
293
|
+
if (relativeFilePath === 'README.md.template')
|
|
294
|
+
return;
|
|
88
295
|
const sourcePath = path.join(templatesDir, 'bensdk-base', relativeFilePath);
|
|
89
296
|
const destinationPath = path.join(appDirectory, relativeFilePath.replace(/\.template$/, ''));
|
|
90
297
|
// Ensure the destination directory exists
|
|
91
298
|
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
|
|
92
299
|
fs.writeFileSync(destinationPath, fs.readFileSync(sourcePath, 'utf-8'));
|
|
93
300
|
});
|
|
301
|
+
// Generate README.md with benefit metadata and mermaid diagrams
|
|
302
|
+
const readme = generateReadme({
|
|
303
|
+
benefitID,
|
|
304
|
+
benefitCategory,
|
|
305
|
+
benefitDescription,
|
|
306
|
+
actions: allActions
|
|
307
|
+
});
|
|
308
|
+
fs.writeFileSync(`${appDirectory}/README.md`, readme);
|
|
94
309
|
spinner.stop();
|
|
95
310
|
fs.writeFileSync(`${appDirectory}/package.json`, JSON.stringify(packageTemplate, null, 4));
|
|
96
311
|
console.log(chalk.green('✔ Project created'));
|
|
97
312
|
let setupCmd = `cd ${appDirectory} && npm install > /dev/null && npm install @benup/bensdk > /dev/null`;
|
|
98
313
|
if (projectGitSetup) {
|
|
99
|
-
setupCmd += ` && git init && npm install --save-dev husky > /dev/null && npx husky init
|
|
314
|
+
setupCmd += ` && git init && npm install --save-dev husky > /dev/null && npx husky init`;
|
|
315
|
+
setupCmd += ` && echo "npm run docs:update && git add README.md" >> .husky/pre-commit`;
|
|
316
|
+
setupCmd += ` && echo "npm run lint" >> .husky/pre-commit`;
|
|
100
317
|
}
|
|
101
318
|
setupCmd += ` && npm run format > /dev/null && cd bin/viewer && npx vite build > /dev/null`;
|
|
102
319
|
spinner.text = `Building benSDK app '${benefitID}'`;
|
|
103
320
|
spinner.start();
|
|
104
321
|
exec(setupCmd, (error, stdout, stderr) => {
|
|
105
|
-
if (error
|
|
322
|
+
if (error) {
|
|
106
323
|
console.log(error, stderr);
|
|
107
324
|
console.log(chalk.red('Something was wrong during Building benSDK app'));
|
|
108
325
|
spinner.stop();
|
package/bin/src/cli/init.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init.js","sourceRoot":"","sources":["../../../src/cli/init.ts"],"names":[],"mappings":"AAAA,8BAA8B;AAC9B,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AACrC,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,QAAQ,MAAM,UAAU,CAAC;AAChC,OAAO,GAAG,MAAM,KAAK,CAAC;AACtB,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,aAAa,EAAE,MAAM,KAAK,CAAC;AACpC,OAAO,eAAe,MAAM,mCAAmC,CAAC,OAAO,IAAI,EAAE,MAAM,EAAE,CAAC;AAEtF,MAAM,UAAU,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAClD,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;AAC3C,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC;AAEvD,MAAM,yBAAyB,GAAG,EAAE,CAAC,YAAY,CAC/C,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,gCAAgC,CAAC,EACzD,OAAO,CACR,CAAC;AAEF,MAAM,eAAe,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,gCAAgC,CAAC,EAAE,OAAO,CAAC,CAAC;AAC5G,MAAM,eAAe,GAAG,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,sBAAsB,CAAC,CAAC;AAExE,MAAM,WAAW,GAAG,CAAC,OAAe,EAAE,eAAyB,EAAE,EAAE,EAAE;IACnE,MAAM,KAAK,GAAG,EAAE,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;IAEtC,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE;QACrB,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;QAC1C,IAAI,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC;YACxC,WAAW,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC;QACtC,CAAC;aAAM,CAAC;YACN,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,aAAa,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC;QACrF,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,OAAO,YAAY,CAAC;AACtB,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"init.js","sourceRoot":"","sources":["../../../src/cli/init.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,110 +1,88 @@
|
|
|
1
|
-
|
|
1
|
+
<img src="ICON.png" alt="BenefitID" width="120">
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
# bensdk - BenefitID
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
## 🔧 Estrutura do Projeto
|
|
5
|
+
## Estrutura do Projeto
|
|
9
6
|
|
|
10
7
|
```
|
|
11
8
|
├── src/
|
|
12
|
-
│ ├──
|
|
13
|
-
│
|
|
14
|
-
|
|
15
|
-
├──
|
|
16
|
-
├──
|
|
17
|
-
|
|
18
|
-
|
|
9
|
+
│ ├── benefit-definition.ts # Definição do benefício + máquina de estados
|
|
10
|
+
│ ├── handlers/ # Handlers executados pelo integrador
|
|
11
|
+
│ └── lib/ # Utilitários e mocks
|
|
12
|
+
├── bin/
|
|
13
|
+
│ ├── cli/ # CLI para geração de handlers e testes
|
|
14
|
+
│ └── server/ # Servidor local para testes (npm run dev:start)
|
|
15
|
+
├── ICON.png # Ícone do benefício
|
|
16
|
+
├── .benSDKIgnore # Ignora arquivos no empacotamento
|
|
17
|
+
└── dev-test.example.json # Modelo de credenciais para testes locais
|
|
19
18
|
```
|
|
20
19
|
|
|
20
|
+
## Conceitos Básicos
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
### 📤 Actions
|
|
22
|
+
### Actions
|
|
25
23
|
|
|
26
|
-
As **Actions** são
|
|
24
|
+
As **Actions** são os eventos enviados pelo integrador ao seu BenSDK. Cada action representa uma operação sobre um benefício (ex: concessão, revogação, recarga) e é processada de acordo com a **máquina de estados** definida em `src/benefit-definition.ts`.
|
|
27
25
|
|
|
28
|
-
###
|
|
26
|
+
### Handlers
|
|
29
27
|
|
|
30
|
-
|
|
28
|
+
Cada **estado** da máquina de estados corresponde a um arquivo de handler em `src/handlers/`. O handler recebe:
|
|
31
29
|
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
|
|
30
|
+
- `message`: contexto da mensagem recebida da fila
|
|
31
|
+
- `action`: dados da operação (funcionário, empresa, eligibilityOptions, ctx compartilhado)
|
|
32
|
+
- `ctx`: contexto de execução (APIs, logger, benefitDefinition)
|
|
35
33
|
|
|
36
|
-
|
|
37
|
-
- `request_remove_employee.handler.ts` → Quando um funcionário for removido
|
|
38
|
-
- `sync_existing_grant.handler.ts` → Verifica se o funcionário **já possui** o benefício
|
|
39
|
-
- `sync_existing_revoke.handler.ts` → Verifica se o funcionário **já foi removido**
|
|
34
|
+
### ctx: Contexto compartilhado entre handlers
|
|
40
35
|
|
|
41
|
-
|
|
36
|
+
O campo `ctx` dentro de `action` é um objeto de estado acumulado ao longo de um fluxo. Use-o para passar dados entre etapas, por exemplo, um ID retornado pela API do parceiro que será necessário em handlers subsequentes.
|
|
42
37
|
|
|
43
|
-
|
|
44
|
-
const handler: StateHandler<
|
|
45
|
-
ActionGrant,
|
|
46
|
-
ActionLogGrant['REQUEST_CREATE_EMPLOYEE'],
|
|
47
|
-
ActionCtx
|
|
48
|
-
> = async (message, action, ctx) => {
|
|
49
|
-
// Aqui você chama a API do seu benefício, por exemplo.
|
|
50
|
-
|
|
51
|
-
return {
|
|
52
|
-
handlerResponse: { response: 'ACK' },
|
|
53
|
-
action: {
|
|
54
|
-
state: stateMachine.next,
|
|
55
|
-
ctx: {},
|
|
56
|
-
logs: { date: new Date().toISOString() },
|
|
57
|
-
},
|
|
58
|
-
};
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
export default handler;
|
|
62
|
-
|
|
63
|
-
```
|
|
38
|
+
## Fluxos
|
|
64
39
|
|
|
65
|
-
###
|
|
40
|
+
### Concessão de Benefício (GRANT)
|
|
66
41
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
#### ✅ Concessão de Benefício (GRANT)
|
|
70
|
-
|
|
71
|
-
Nesse fluxo, o integrador:
|
|
72
|
-
1. Verifica se o funcionário já está no benefício (`SYNC_EXISTING_GRANT` - Executando o handler `sync_existing_grant.handler.ts`).
|
|
73
|
-
2. Caso não esteja, solicita sua inclusão (`REQUEST_CREATE_EMPLOYEE` - Executando o handler `request_create_employee.handler.ts` ).
|
|
74
|
-
|
|
75
|
-
```mermaid
|
|
42
|
+
```mermaid
|
|
76
43
|
flowchart TD
|
|
77
|
-
START([
|
|
78
|
-
REQUESTED_GRANT -->|next| SYNC_EXISTING_GRANT
|
|
44
|
+
START([Início]) --> REQUESTED_GRANT
|
|
45
|
+
REQUESTED_GRANT -->|next| SYNC_EXISTING_GRANT
|
|
79
46
|
SYNC_EXISTING_GRANT -->|next| REQUEST_CREATE_EMPLOYEE
|
|
80
47
|
SYNC_EXISTING_GRANT -->|skip| GRANTED
|
|
81
48
|
REQUEST_CREATE_EMPLOYEE -->|next| GRANTED
|
|
82
49
|
REQUEST_CREATE_EMPLOYEE -->|fail| FAILED_TO_GRANT
|
|
83
50
|
|
|
51
|
+
GRANTED([GRANTED ✅])
|
|
52
|
+
FAILED_TO_GRANT([FAILED_TO_GRANT ❌])
|
|
84
53
|
```
|
|
85
54
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
Nesse fluxo, o integrador:
|
|
89
|
-
1. Verifica se o funcionário já foi removido do benefício (`SYNC_EXISTING_REVOKE`).
|
|
90
|
-
2. Caso não tenha sido, solicita sua revogação (`REQUEST_REMOVE_EMPLOYEE`).
|
|
55
|
+
### Revogação de Benefício (REVOKE)
|
|
91
56
|
|
|
92
|
-
```mermaid
|
|
57
|
+
```mermaid
|
|
93
58
|
flowchart TD
|
|
94
|
-
START([
|
|
95
|
-
REQUESTED_REVOKE -->|next| SYNC_EXISTING_REVOKE
|
|
59
|
+
START([Início]) --> REQUESTED_REVOKE
|
|
60
|
+
REQUESTED_REVOKE -->|next| SYNC_EXISTING_REVOKE
|
|
96
61
|
SYNC_EXISTING_REVOKE -->|next| REQUEST_REMOVE_EMPLOYEE
|
|
97
62
|
SYNC_EXISTING_REVOKE -->|skip| REVOKED
|
|
98
63
|
REQUEST_REMOVE_EMPLOYEE -->|next| REVOKED
|
|
99
64
|
REQUEST_REMOVE_EMPLOYEE -->|fail| FAILED_TO_REVOKE
|
|
100
65
|
|
|
66
|
+
REVOKED([REVOKED ✅])
|
|
67
|
+
FAILED_TO_REVOKE([FAILED_TO_REVOKE ❌])
|
|
101
68
|
```
|
|
102
69
|
|
|
103
|
-
##
|
|
70
|
+
## Comandos
|
|
104
71
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
72
|
+
| Comando | Descricao |
|
|
73
|
+
|---------|-----------|
|
|
74
|
+
| `npm run dev:start` | Inicia o servidor local com autenticacao real contra a API do parceiro. [Ver documentacao](docs/DEV_SERVER.md) |
|
|
75
|
+
| `npm run test` | Executa os testes unitarios com Vitest |
|
|
76
|
+
| `npm run generate` | Gera handlers e testes a partir do `benefit-definition.ts` |
|
|
77
|
+
| `npm run start` | Abre o painel visual da maquina de estados |
|
|
78
|
+
| `npm run lint` | Valida tipos TypeScript, formatacao e linting |
|
|
79
|
+
| `npm run build` | Compila o projeto para `dist/` |
|
|
109
80
|
|
|
110
|
-
|
|
81
|
+
## Publicação
|
|
82
|
+
|
|
83
|
+
1. Instale o [maria-cli](https://github.com/benup-dev/maria) seguindo as instrucões do repositório.
|
|
84
|
+
2. Execute o comando abaixo na raiz do projeto:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
maria benefit deploy -r <link-ssh-do-repositorio> -n <benefitID>
|
|
88
|
+
```
|
|
@@ -1,8 +1,14 @@
|
|
|
1
|
-
import
|
|
1
|
+
import type { StateHandlerCtx } from '@benup/bensdk/bin/lib/types/state-handler.types.d.ts';
|
|
2
|
+
import type { BenefitDefinition } from '@benup/bensdk/bin/lib/types/benefit-definition.types';
|
|
3
|
+
import axios, { AxiosInstance } from 'axios';
|
|
2
4
|
import AxiosMockAdapter from 'axios-mock-adapter';
|
|
3
|
-
import {
|
|
5
|
+
import pino, { Logger } from 'pino';
|
|
4
6
|
|
|
5
|
-
export function createStateHandlerCtxMock(
|
|
7
|
+
export function createStateHandlerCtxMock(
|
|
8
|
+
benefitDefinition: BenefitDefinition,
|
|
9
|
+
logger: Logger = pino(),
|
|
10
|
+
disableMocks = false
|
|
11
|
+
) {
|
|
6
12
|
const benupAPI = axios.create({
|
|
7
13
|
validateStatus: () => true,
|
|
8
14
|
baseURL: 'http://localhost/benupAPI'
|
|
@@ -18,35 +24,105 @@ export function createStateHandlerCtxMock(benefitDefinition, logger) {
|
|
|
18
24
|
baseURL: 'http://localhost/benefitsAPI'
|
|
19
25
|
});
|
|
20
26
|
|
|
27
|
+
const benefitPartnerAPI = axios.create({
|
|
28
|
+
validateStatus: () => true,
|
|
29
|
+
baseURL: 'http://localhost/benefitPartnerAPI'
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// issueHelper mock: prints issues to the logger instead of sending them to a real service
|
|
33
|
+
// This way you can see issues raised by handlers during local development
|
|
34
|
+
const issuesHelper = {
|
|
35
|
+
sendIssue: async ({
|
|
36
|
+
title,
|
|
37
|
+
description,
|
|
38
|
+
fieldPath
|
|
39
|
+
}: {
|
|
40
|
+
title: string;
|
|
41
|
+
description: string;
|
|
42
|
+
fieldPath?: string;
|
|
43
|
+
}) => {
|
|
44
|
+
logger.warn(`[Issue] [${title}] ${description}${fieldPath ? ` → ${fieldPath}` : ''}`);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// When disableMocks=true the dev server will inject a real authenticated axios instance
|
|
49
|
+
// into ctxMock.benefitPartnerAPI. The mock here is a no-op passthrough function that can
|
|
50
|
+
// be applied to the authenticated instance if you need to stub specific endpoints
|
|
51
|
+
// (e.g. presigned URLs, file uploads) while keeping the rest as real API calls.
|
|
52
|
+
if (disableMocks) {
|
|
53
|
+
const apisMocks = {
|
|
54
|
+
benupAPI: new AxiosMockAdapter(benupAPI, { onNoMatch: 'throwException' }),
|
|
55
|
+
lgProxyAPI: new AxiosMockAdapter(lgProxyAPI, { onNoMatch: 'throwException' }),
|
|
56
|
+
benefitsAPI: new AxiosMockAdapter(benefitsAPI, { onNoMatch: 'throwException' }),
|
|
57
|
+
|
|
58
|
+
// Return a function so the dev server can apply mocks to the real authenticated instance.
|
|
59
|
+
// Use onNoMatch: 'passthrough' so real API calls are not blocked.
|
|
60
|
+
// Add any endpoint stubs here that you want to intercept even in real-call mode.
|
|
61
|
+
benefitPartnerAPI: (axiosInstance: AxiosInstance) => {
|
|
62
|
+
const mock = new AxiosMockAdapter(axiosInstance, { onNoMatch: 'passthrough' });
|
|
63
|
+
// Example: mock.onGet('/some-endpoint').reply(200, { ... });
|
|
64
|
+
return mock;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const ctxMock: StateHandlerCtx = {
|
|
69
|
+
rawMessage: {},
|
|
70
|
+
logger,
|
|
71
|
+
correlationIDs: {},
|
|
72
|
+
benefitsAPI,
|
|
73
|
+
lgProxyAPI,
|
|
74
|
+
benupAPI,
|
|
75
|
+
benefitDefinition,
|
|
76
|
+
benefitPartnerAPI,
|
|
77
|
+
// @ts-expect-error: universAPI intentionally omitted in mock context
|
|
78
|
+
universAPI: undefined,
|
|
79
|
+
issuesHelper
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
return { ctxMock, apisMocks };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Full mock setup for unit testing (vitest). All API calls must be explicitly mocked.
|
|
21
86
|
const apisMocks = {
|
|
22
|
-
benupAPI: new AxiosMockAdapter(benupAPI, {
|
|
23
|
-
|
|
24
|
-
}),
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
87
|
+
benupAPI: new AxiosMockAdapter(benupAPI, { onNoMatch: 'throwException' }),
|
|
88
|
+
lgProxyAPI: new AxiosMockAdapter(lgProxyAPI, { onNoMatch: 'throwException' }),
|
|
89
|
+
benefitsAPI: new AxiosMockAdapter(benefitsAPI, { onNoMatch: 'throwException' }),
|
|
90
|
+
benefitPartnerAPI: new AxiosMockAdapter(benefitPartnerAPI, { onNoMatch: 'throwException' }),
|
|
91
|
+
|
|
92
|
+
// Reset all mock adapters between tests
|
|
93
|
+
resetMocks: function () {
|
|
94
|
+
Object.entries(this)
|
|
95
|
+
.filter(([_, value]) => value instanceof AxiosMockAdapter)
|
|
96
|
+
.forEach(([_, mockAdapter]) => {
|
|
97
|
+
(mockAdapter as AxiosMockAdapter).reset();
|
|
98
|
+
});
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
// Restore all mock adapters (call in afterAll to clean up)
|
|
102
|
+
restoreAll: function () {
|
|
103
|
+
Object.entries(this)
|
|
104
|
+
.filter(([_, value]) => value instanceof AxiosMockAdapter)
|
|
105
|
+
.forEach(([_, mockAdapter]) => {
|
|
106
|
+
(mockAdapter as AxiosMockAdapter).restore();
|
|
107
|
+
});
|
|
108
|
+
}
|
|
31
109
|
};
|
|
32
110
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
values: ['123']
|
|
36
|
-
});
|
|
37
|
-
|
|
111
|
+
// Set up your mock responses here for unit tests.
|
|
112
|
+
// Example: apisMocks.benefitPartnerAPI.onGet('/some-path').replyOnce(200, { ... });
|
|
38
113
|
|
|
39
114
|
const ctxMock: StateHandlerCtx = {
|
|
40
115
|
rawMessage: {},
|
|
41
|
-
|
|
42
116
|
logger,
|
|
43
|
-
|
|
44
117
|
correlationIDs: {},
|
|
45
|
-
|
|
46
118
|
benefitsAPI,
|
|
47
119
|
lgProxyAPI,
|
|
48
120
|
benupAPI,
|
|
49
|
-
benefitDefinition
|
|
121
|
+
benefitDefinition,
|
|
122
|
+
benefitPartnerAPI,
|
|
123
|
+
// @ts-expect-error: universAPI intentionally omitted in mock context
|
|
124
|
+
universAPI: undefined,
|
|
125
|
+
issuesHelper
|
|
50
126
|
};
|
|
51
127
|
|
|
52
128
|
return { ctxMock, apisMocks };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "Copy this file to dev-test.json and fill in your real values. dev-test.json is gitignored.",
|
|
3
|
+
|
|
4
|
+
"credentials": {
|
|
5
|
+
"accountID": "FILL_ME",
|
|
6
|
+
"companyID": "FILL_ME",
|
|
7
|
+
"benefitID": "FILL_ME",
|
|
8
|
+
"connection": {
|
|
9
|
+
"url": "https://api.your-partner.com"
|
|
10
|
+
},
|
|
11
|
+
"secret": {
|
|
12
|
+
"username": "FILL_ME",
|
|
13
|
+
"password": "FILL_ME"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
"eligibilityOptions": {
|
|
18
|
+
"GRANT": {},
|
|
19
|
+
"RECHARGE": {},
|
|
20
|
+
"GRANT_DEPENDENT": {}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# Servidor de Desenvolvimento Local: `npm run dev:start`
|
|
2
|
+
|
|
3
|
+
O `dev:start` sobe um servidor HTTP local (porta 3000) que permite testar qualquer handler diretamente contra a API real do parceiro, com autenticacao verdadeira e sem precisar de mocks.
|
|
4
|
+
|
|
5
|
+
Isso e especialmente util porque:
|
|
6
|
+
- Os dados que chegam da folha LG variam por cliente e nem sempre sao previsiveis nos testes unitarios.
|
|
7
|
+
- As APIs dos parceiros tem comportamentos nao documentados que so aparecem em chamadas reais.
|
|
8
|
+
- E possivel executar cada estado da maquina em sequencia, acumulando o `ctx` entre chamadas, simulando exatamente o que o integrador faria.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Pre-requisitos
|
|
13
|
+
|
|
14
|
+
Crie o arquivo `dev-test.json` na raiz do projeto a partir do modelo:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
cp dev-test.example.json dev-test.json
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Preencha com suas credenciais reais. A estrutura exata de `credentials` deve seguir o schema definido no `auth.handler` do seu `benefit-definition.ts`:
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{
|
|
24
|
+
"credentials": {
|
|
25
|
+
"accountID": "...",
|
|
26
|
+
"companyID": "...",
|
|
27
|
+
"benefitID": "...",
|
|
28
|
+
"connection": {
|
|
29
|
+
"url": "https://api.seu-parceiro.com"
|
|
30
|
+
},
|
|
31
|
+
"secret": {
|
|
32
|
+
"username": "...",
|
|
33
|
+
"password": "..."
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"eligibilityOptions": {
|
|
37
|
+
"GRANT": {},
|
|
38
|
+
"RECHARGE": {}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
> **Atencao:** `dev-test.json` esta no `.gitignore` e no `.benSDKIgnore`. **Nunca commite credenciais reais.**
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Iniciando o servidor
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npm run dev:start
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Ao iniciar, o servidor exibe:
|
|
54
|
+
- Se as credenciais foram carregadas com sucesso.
|
|
55
|
+
- Quais acoes tem presets de `eligibilityOptions` configurados.
|
|
56
|
+
- Os endpoints disponiveis.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Endpoints
|
|
61
|
+
|
|
62
|
+
### `POST /`: Executar um handler
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
{
|
|
66
|
+
"run": "SYNC_EXISTING_GRANT",
|
|
67
|
+
"payload": {
|
|
68
|
+
"target": {
|
|
69
|
+
"employee": { "name": "...", "cpf": "...", "birthdate": "..." },
|
|
70
|
+
"company": { "cnpj": "..." }
|
|
71
|
+
},
|
|
72
|
+
"employmentContract": {
|
|
73
|
+
"benefits": {
|
|
74
|
+
"BENEFIT_ID": { "options": {} }
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
"message": {},
|
|
79
|
+
"ctx": {}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
| Campo | Tipo | Descricao |
|
|
84
|
+
|-------|------|-----------|
|
|
85
|
+
| `run` | `string` | Nome do estado a executar (ex: `SYNC_EXISTING_GRANT` ou `sync_existing_grant`) |
|
|
86
|
+
| `payload` | `object` | Dados da action: target, employmentContract, eligibilityOptions opcionais |
|
|
87
|
+
| `message` | `object` | Contexto da mensagem, pode ser `{}` em testes locais |
|
|
88
|
+
| `ctx` | `object` | Contexto acumulado entre handlers (use o `ctx` retornado pelo handler anterior) |
|
|
89
|
+
|
|
90
|
+
### `POST /run-action`: Formato legado
|
|
91
|
+
|
|
92
|
+
Mesmo comportamento, mas utilizando os campos `requestPayload` e `currentContext` no lugar de `payload` e `ctx`.
|
|
93
|
+
|
|
94
|
+
### `GET /state-machine`
|
|
95
|
+
|
|
96
|
+
Retorna o objeto `stateMachine` definido em `benefit-definition.ts`.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Como funciona por dentro
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
Requisicao POST /
|
|
104
|
+
|
|
|
105
|
+
+- Carrega credenciais do dev-test.json
|
|
106
|
+
+- Chama benefitDefinition.auth.handler(credentials, { tokenHelper, logger })
|
|
107
|
+
| +- Token cacheado em memoria, so autentica uma vez por sessao
|
|
108
|
+
+- Injeta axios autenticado em ctx.benefitPartnerAPI
|
|
109
|
+
+- Mescla eligibilityOptions: dev-test.json < sobrescrito pelo corpo da requisicao
|
|
110
|
+
+- Valida eligibilityOptions e ctx contra os schemas do benefit-definition.ts
|
|
111
|
+
+- Executa o handler e retorna a resposta como JSON
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## eligibilityOptions: presets no `dev-test.json`
|
|
117
|
+
|
|
118
|
+
Para nao precisar repetir as `eligibilityOptions` em cada requisicao, defina-as no `dev-test.json`. O servidor identifica automaticamente a qual action type pertence o handler informado em `run` consultando a `stateMachine`:
|
|
119
|
+
|
|
120
|
+
```json
|
|
121
|
+
{
|
|
122
|
+
"eligibilityOptions": {
|
|
123
|
+
"GRANT": {
|
|
124
|
+
"externalCompanyID": "12345",
|
|
125
|
+
"externalCardType": 1
|
|
126
|
+
},
|
|
127
|
+
"RECHARGE": {
|
|
128
|
+
"productCode": "VA"
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
- As options do `dev-test.json` sao as **default**.
|
|
135
|
+
- Se voce enviar `eligibilityOptions` no corpo da requisicao, eles **sobrescrevem** os do `dev-test.json`.
|
|
136
|
+
- Erros de validacao do schema aparecem como `WARN` no terminal e nao bloqueiam a execucao.
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## Testando um fluxo completo
|
|
141
|
+
|
|
142
|
+
Execute os handlers na ordem da maquina de estados, acumulando o `ctx`:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
# 1. Verificar se o beneficiario ja existe
|
|
146
|
+
curl -s -X POST http://localhost:3000 \
|
|
147
|
+
-H "Content-Type: application/json" \
|
|
148
|
+
-d '{
|
|
149
|
+
"run": "SYNC_EXISTING_GRANT",
|
|
150
|
+
"payload": { "target": { "employee": { ... } } },
|
|
151
|
+
"ctx": {}
|
|
152
|
+
}' | jq .
|
|
153
|
+
|
|
154
|
+
# Exemplo de resposta:
|
|
155
|
+
# {
|
|
156
|
+
# "handlerResponse": { "response": "ACK" },
|
|
157
|
+
# "action": {
|
|
158
|
+
# "state": "REQUEST_CREATE_EMPLOYEE",
|
|
159
|
+
# "ctx": { "someInternalID": "abc123" },
|
|
160
|
+
# "logs": { ... }
|
|
161
|
+
# }
|
|
162
|
+
# }
|
|
163
|
+
|
|
164
|
+
# 2. Criar o beneficiario, passando o ctx retornado acima
|
|
165
|
+
curl -s -X POST http://localhost:3000 \
|
|
166
|
+
-H "Content-Type: application/json" \
|
|
167
|
+
-d '{
|
|
168
|
+
"run": "REQUEST_CREATE_EMPLOYEE",
|
|
169
|
+
"payload": { "target": { "employee": { ... } } },
|
|
170
|
+
"ctx": { "someInternalID": "abc123" }
|
|
171
|
+
}' | jq .
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
> **Dica:** Use `| jq .action.ctx` para extrair apenas o ctx da resposta e passar para o proximo handler.
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## Variaveis de ambiente
|
|
179
|
+
|
|
180
|
+
| Variavel | Valor | Efeito |
|
|
181
|
+
|----------|-------|--------|
|
|
182
|
+
| `NODE_ENV` | `production` | Mantem a validacao TLS ativa (por padrao, desabilitada em dev para suportar certificados self-signed) |
|
|
183
|
+
| `VERBOSE` | `true` | Loga o payload completo de cada requisicao no terminal |
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## Troubleshooting
|
|
188
|
+
|
|
189
|
+
**`dev-test.json not found`**
|
|
190
|
+
Crie o arquivo: `cp dev-test.example.json dev-test.json`
|
|
191
|
+
|
|
192
|
+
**`Authentication failed`**
|
|
193
|
+
Verifique se as credenciais em `dev-test.json` estao corretas e se a URL da API esta acessivel a partir da sua maquina.
|
|
194
|
+
|
|
195
|
+
**`Handler not found: sync_existing_grant.handler.ts`**
|
|
196
|
+
Verifique se:
|
|
197
|
+
1. O nome em `run` corresponde a um estado da `stateMachine` em `benefit-definition.ts`.
|
|
198
|
+
2. O arquivo `src/handlers/{estado}.handler.ts` existe (rode `npm run generate` para gera-lo).
|
|
199
|
+
|
|
200
|
+
**TLS / certificados self-signed**
|
|
201
|
+
Em ambientes com zero-trust ou proxies com certificados self-signed, o servidor ja desabilita a verificacao TLS automaticamente (exceto quando `NODE_ENV=production`).
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* npm run docs:update
|
|
4
|
+
*
|
|
5
|
+
* Reads the current benefit-definition.ts, parses the stateMachine and
|
|
6
|
+
* availableActions, and regenerates the ## Fluxos section in README.md
|
|
7
|
+
* with up-to-date Mermaid diagrams.
|
|
8
|
+
*
|
|
9
|
+
* Run manually or let the pre-commit hook handle it automatically.
|
|
10
|
+
*/
|
|
11
|
+
import { readFileSync, writeFileSync } from 'fs';
|
|
12
|
+
import { join } from 'path';
|
|
13
|
+
import benefitDefinition from '../../src/benefit-definition';
|
|
14
|
+
|
|
15
|
+
const ACTION_LABELS: Record<string, string> = {
|
|
16
|
+
GRANT: 'Concessão de Benefício (GRANT)',
|
|
17
|
+
REVOKE: 'Revogação de Benefício (REVOKE)',
|
|
18
|
+
RECHARGE: 'Recarga de Benefício (RECHARGE)',
|
|
19
|
+
DEDUCTION: 'Dedução (DEDUCTION)',
|
|
20
|
+
GRANT_DEPENDENT: 'Concessão para Dependente (GRANT_DEPENDENT)',
|
|
21
|
+
REVOKE_DEPENDENT: 'Revogação de Dependente (REVOKE_DEPENDENT)',
|
|
22
|
+
BEFORE_ALL_RECHARGE: 'Pré-processamento de Recargas (BEFORE_ALL_RECHARGE)',
|
|
23
|
+
AFTER_ALL_RECHARGE: 'Pós-processamento de Recargas (AFTER_ALL_RECHARGE)',
|
|
24
|
+
DEDUCTION_FILE_INGESTION: 'Ingestão de Arquivo de Deduções (DEDUCTION_FILE_INGESTION)'
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function generateMermaidDiagram(
|
|
28
|
+
states: Record<string, Record<string, string>>
|
|
29
|
+
): string {
|
|
30
|
+
const lines: string[] = ['flowchart TD'];
|
|
31
|
+
|
|
32
|
+
// The starting state is usually REQUESTED_*, otherwise the first defined state
|
|
33
|
+
const firstState =
|
|
34
|
+
Object.keys(states).find((s) => s.startsWith('REQUESTED_')) ?? Object.keys(states)[0];
|
|
35
|
+
|
|
36
|
+
lines.push(` START([Início]) --> ${firstState}`);
|
|
37
|
+
|
|
38
|
+
// Track all states that appear as transition targets
|
|
39
|
+
const allTargets = new Set<string>();
|
|
40
|
+
|
|
41
|
+
for (const [state, transitions] of Object.entries(states)) {
|
|
42
|
+
for (const [transitionType, targetState] of Object.entries(
|
|
43
|
+
transitions as Record<string, string>
|
|
44
|
+
)) {
|
|
45
|
+
lines.push(` ${state} -->|${transitionType}| ${targetState}`);
|
|
46
|
+
allTargets.add(targetState);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Terminal states = appear as targets but are not defined as source states
|
|
51
|
+
const terminalStates = [...allTargets].filter((s) => !(s in states));
|
|
52
|
+
for (const terminal of terminalStates) {
|
|
53
|
+
const isFailure = terminal.includes('FAILED') || terminal.includes('FAIL');
|
|
54
|
+
lines.push(` ${terminal}([${terminal} ${isFailure ? '❌' : '✅'}])`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return '```mermaid\n' + lines.join('\n') + '\n```';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function generateActionSection(
|
|
61
|
+
action: string,
|
|
62
|
+
states: Record<string, Record<string, string>>
|
|
63
|
+
): string {
|
|
64
|
+
const label = ACTION_LABELS[action] ?? action;
|
|
65
|
+
const diagram = generateMermaidDiagram(states);
|
|
66
|
+
return `### ${label}\n\n${diagram}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const stateMachine = benefitDefinition.stateMachine as Record<
|
|
70
|
+
string,
|
|
71
|
+
Record<string, Record<string, string>>
|
|
72
|
+
>;
|
|
73
|
+
const availableActions = benefitDefinition.availableActions as readonly string[];
|
|
74
|
+
|
|
75
|
+
// Only generate diagrams for actions that are:
|
|
76
|
+
// 1. Listed in availableActions
|
|
77
|
+
// 2. Defined in stateMachine
|
|
78
|
+
const sections = Object.entries(stateMachine)
|
|
79
|
+
.filter(([action]) => availableActions.includes(action))
|
|
80
|
+
.map(([action, states]) => generateActionSection(action, states))
|
|
81
|
+
.join('\n\n');
|
|
82
|
+
|
|
83
|
+
const newFluxosSection = `## Fluxos\n\n${sections}`;
|
|
84
|
+
|
|
85
|
+
const readmePath = join(process.cwd(), 'README.md');
|
|
86
|
+
const readme = readFileSync(readmePath, 'utf-8');
|
|
87
|
+
|
|
88
|
+
// Replace everything between ## Fluxos and the next ## heading
|
|
89
|
+
const updated = readme.replace(/## Fluxos[\s\S]*?(?=\n## )/, newFluxosSection + '\n');
|
|
90
|
+
|
|
91
|
+
if (updated === readme) {
|
|
92
|
+
console.log('[benSDK] README.md: Fluxos section is already up to date');
|
|
93
|
+
} else {
|
|
94
|
+
writeFileSync(readmePath, updated);
|
|
95
|
+
console.log('[benSDK] README.md updated with current state machine diagrams');
|
|
96
|
+
}
|
|
@@ -1,80 +1,278 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
1
2
|
import { serve } from '@hono/node-server';
|
|
2
3
|
import { Hono } from 'hono';
|
|
3
4
|
import { cors } from 'hono/cors';
|
|
5
|
+
import { existsSync, readFileSync } from 'fs';
|
|
6
|
+
import { join } from 'path';
|
|
4
7
|
import pino from 'pino';
|
|
5
8
|
import { PassThrough } from 'stream';
|
|
6
|
-
import { WebSocketServer } from 'ws';
|
|
9
|
+
import { WebSocket, WebSocketServer } from 'ws';
|
|
7
10
|
import benefitsDefinition from '../../src/benefit-definition';
|
|
8
11
|
|
|
12
|
+
// Disable TLS certificate validation in non-production environments
|
|
13
|
+
// (e.g. self-signed certs used in zero-trust networks)
|
|
14
|
+
if (process.env['NODE_ENV'] !== 'production') {
|
|
15
|
+
process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// logStream is used to forward raw pino JSON logs to WebSocket connections (viewer)
|
|
9
19
|
const logStream = new PassThrough();
|
|
10
|
-
|
|
20
|
+
|
|
21
|
+
const logger = pino(
|
|
22
|
+
{
|
|
23
|
+
level: 'info',
|
|
24
|
+
transport: {
|
|
25
|
+
target: 'pino-pretty',
|
|
26
|
+
options: { colorize: true }
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
);
|
|
30
|
+
|
|
11
31
|
const app = new Hono();
|
|
12
32
|
const connections = new Set<WebSocket>();
|
|
13
|
-
|
|
33
|
+
|
|
34
|
+
// Forward raw log data to WebSocket connections (used by the viewer)
|
|
35
|
+
logStream.on('data', (chunk: Buffer) => {
|
|
14
36
|
connections.forEach((ws) => {
|
|
15
37
|
if (ws.readyState === WebSocket.OPEN) {
|
|
16
38
|
ws.send(chunk.toString());
|
|
17
39
|
}
|
|
18
40
|
});
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
logStream.on('data', onLog);
|
|
41
|
+
});
|
|
22
42
|
|
|
23
43
|
app.use(cors());
|
|
24
44
|
|
|
25
|
-
|
|
26
|
-
|
|
45
|
+
/**
|
|
46
|
+
* dev-test.json structure:
|
|
47
|
+
* {
|
|
48
|
+
* "credentials": { ...your real credentials here },
|
|
49
|
+
* "eligibilityOptions": {
|
|
50
|
+
* "GRANT": { ...options },
|
|
51
|
+
* "RECHARGE": { ...options },
|
|
52
|
+
* "GRANT_DEPENDENT": { ...options }
|
|
53
|
+
* }
|
|
54
|
+
* }
|
|
55
|
+
*
|
|
56
|
+
* Copy dev-test.example.json to dev-test.json and fill in your real values.
|
|
57
|
+
* dev-test.json is gitignored — never commit real credentials.
|
|
58
|
+
*/
|
|
59
|
+
const devTestPath = join(process.cwd(), 'dev-test.json');
|
|
60
|
+
let devTestConfig: { credentials?: Record<string, unknown>; eligibilityOptions?: Record<string, unknown> } = {};
|
|
27
61
|
|
|
28
|
-
|
|
62
|
+
if (existsSync(devTestPath)) {
|
|
63
|
+
devTestConfig = JSON.parse(readFileSync(devTestPath, 'utf-8'));
|
|
64
|
+
logger.info('[benSDK] Loaded dev-test.json');
|
|
65
|
+
} else {
|
|
66
|
+
logger.warn('[benSDK] dev-test.json not found — create it from dev-test.example.json to enable real credentials and eligibilityOptions presets');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Persistent in-memory token cache (survives between requests within the same server session)
|
|
70
|
+
const tokenCache = new Map<string, string>();
|
|
71
|
+
const tokenHelper = {
|
|
72
|
+
get: async ({ accountID, companyID, benefitID }: { accountID: string; companyID: string; benefitID: string }) => {
|
|
73
|
+
const key = `${accountID}:${companyID}:${benefitID}`;
|
|
74
|
+
return tokenCache.get(key) ?? null;
|
|
75
|
+
},
|
|
76
|
+
set: async ({
|
|
77
|
+
accountID,
|
|
78
|
+
companyID,
|
|
79
|
+
benefitID,
|
|
80
|
+
valueToBeStored
|
|
81
|
+
}: {
|
|
82
|
+
accountID: string;
|
|
83
|
+
companyID: string;
|
|
84
|
+
benefitID: string;
|
|
85
|
+
valueToBeStored: string;
|
|
86
|
+
storeForSeconds?: number;
|
|
87
|
+
}) => {
|
|
88
|
+
const key = `${accountID}:${companyID}:${benefitID}`;
|
|
89
|
+
tokenCache.set(key, valueToBeStored);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
app.get('/state-machine', async (c) => {
|
|
94
|
+
return c.json(benefitsDefinition.stateMachine, 200);
|
|
29
95
|
});
|
|
30
96
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
97
|
+
/**
|
|
98
|
+
* Find which action type (GRANT, REVOKE, RECHARGE, etc.) a handler state belongs to
|
|
99
|
+
* by looking up the state key in the stateMachine definition.
|
|
100
|
+
*/
|
|
101
|
+
function getActionTypeForHandler(handlerName: string): string | null {
|
|
102
|
+
const stateKey = handlerName.replace(/-/g, '_').toUpperCase();
|
|
103
|
+
for (const [action, states] of Object.entries(benefitsDefinition.stateMachine)) {
|
|
104
|
+
if (stateKey in (states as Record<string, unknown>)) {
|
|
105
|
+
return action;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const runAction = async (c) => {
|
|
112
|
+
let body: Record<string, unknown>;
|
|
113
|
+
try {
|
|
114
|
+
body = await c.req.json();
|
|
115
|
+
} catch {
|
|
116
|
+
return c.json({ error: 'Invalid JSON body' }, 400);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Support both simplified format (payload, ctx) and legacy format (requestPayload, currentContext)
|
|
120
|
+
const run = body.run as string | undefined;
|
|
121
|
+
const requestPayload = (body.requestPayload ?? body.payload) as Record<string, unknown> | undefined;
|
|
122
|
+
const message = body.message ?? {};
|
|
123
|
+
const currentContext = (body.currentContext ?? body.ctx ?? {}) as Record<string, unknown>;
|
|
124
|
+
|
|
125
|
+
// Validate ctx against the schema defined in benefit-definition.ts
|
|
126
|
+
if (benefitsDefinition.actions?.ctx) {
|
|
127
|
+
const ctxValidation = benefitsDefinition.actions.ctx.safeParse(currentContext);
|
|
128
|
+
if (!ctxValidation.success) {
|
|
129
|
+
logger.warn(
|
|
130
|
+
`[benSDK] ctx failed schema validation: ${JSON.stringify(ctxValidation.error.format())}`
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!run) {
|
|
136
|
+
return c.json(
|
|
137
|
+
{ error: 'Missing required field: "run" (handler state name, e.g. "SYNC_EXISTING_GRANT")' },
|
|
138
|
+
400
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Dynamically import the handler file
|
|
34
143
|
const fileName = `${run.toLowerCase()}.handler.ts`;
|
|
35
144
|
const fileDir = `../../src/handlers/${fileName}`;
|
|
36
|
-
|
|
145
|
+
let handler: { default: (...args: unknown[]) => Promise<unknown> };
|
|
146
|
+
try {
|
|
147
|
+
handler = await import(fileDir);
|
|
148
|
+
} catch {
|
|
149
|
+
return c.json(
|
|
150
|
+
{ error: `Handler not found: ${fileName}`, hint: 'Make sure the file exists in src/handlers/' },
|
|
151
|
+
404
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Authenticate with real credentials if auth is defined and credentials are available in dev-test.json
|
|
156
|
+
let authInstance;
|
|
157
|
+
if (benefitsDefinition.auth && devTestConfig.credentials) {
|
|
158
|
+
logger.info('[benSDK] Authenticating with credentials from dev-test.json...');
|
|
159
|
+
const authResult = await benefitsDefinition.auth.handler(devTestConfig.credentials, {
|
|
160
|
+
tokenHelper,
|
|
161
|
+
logger
|
|
162
|
+
});
|
|
37
163
|
|
|
38
|
-
|
|
164
|
+
if (authResult.outcome === 'FAILED') {
|
|
165
|
+
logger.error(`[benSDK] Authentication failed: ${authResult.reason}`);
|
|
166
|
+
return c.json({ error: 'Authentication failed', reason: authResult.reason }, 401);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
authInstance = authResult.instance;
|
|
170
|
+
logger.info('[benSDK] Authentication succeeded');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Create context mock (disableMocks=true when we have a real authenticated instance)
|
|
174
|
+
const { ctxMock, apisMocks } = (await import('../../context.config')).createStateHandlerCtxMock(
|
|
39
175
|
benefitsDefinition,
|
|
40
|
-
logger
|
|
176
|
+
logger,
|
|
177
|
+
!!authInstance
|
|
41
178
|
);
|
|
42
179
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
180
|
+
// Inject the real authenticated axios instance into benefitPartnerAPI
|
|
181
|
+
if (authInstance) {
|
|
182
|
+
ctxMock.benefitPartnerAPI = authInstance;
|
|
183
|
+
// Apply any additional mocks from context.config to the authenticated instance
|
|
184
|
+
// (e.g. presigned URL mocks, specific endpoint stubs that don't need real calls)
|
|
185
|
+
if (apisMocks.benefitPartnerAPI && typeof apisMocks.benefitPartnerAPI === 'function') {
|
|
186
|
+
apisMocks.benefitPartnerAPI(authInstance);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Determine which action type this handler belongs to (for eligibilityOptions lookup)
|
|
191
|
+
const actionType = getActionTypeForHandler(run);
|
|
192
|
+
|
|
193
|
+
// dev-test.json eligibilityOptions serve as defaults; request body overrides them
|
|
194
|
+
const devEligibilityOptions =
|
|
195
|
+
actionType && devTestConfig.eligibilityOptions?.[actionType]
|
|
196
|
+
? devTestConfig.eligibilityOptions[actionType]
|
|
197
|
+
: {};
|
|
46
198
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
199
|
+
// Validate dev-test.json eligibilityOptions against the schema defined in benefit-definition.ts
|
|
200
|
+
if (actionType && devTestConfig.eligibilityOptions?.[actionType]) {
|
|
201
|
+
const schema = benefitsDefinition.actions?.eligibilityOptions?.[actionType];
|
|
202
|
+
if (schema) {
|
|
203
|
+
const validation = schema.safeParse(devTestConfig.eligibilityOptions[actionType]);
|
|
204
|
+
if (!validation.success) {
|
|
205
|
+
logger.warn(
|
|
206
|
+
`[benSDK] dev-test.json eligibilityOptions.${actionType} failed schema validation: ${JSON.stringify(validation.error.format())}`
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const mergedPayload = {
|
|
213
|
+
...requestPayload,
|
|
214
|
+
eligibilityOptions: {
|
|
215
|
+
...devEligibilityOptions,
|
|
216
|
+
...(requestPayload?.eligibilityOptions ?? {})
|
|
52
217
|
},
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
});
|
|
218
|
+
ctx: currentContext
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
logger.info(`[benSDK] Running handler: ${run}`);
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
const response = await handler.default(message, mergedPayload, ctxMock);
|
|
225
|
+
logger.info(`[benSDK] Handler completed: ${run}`);
|
|
226
|
+
return c.json(response, 200);
|
|
227
|
+
} catch (error) {
|
|
228
|
+
logger.error(`[benSDK] Handler threw an exception: ${error?.message}`);
|
|
229
|
+
return c.json(
|
|
230
|
+
{ error: 'Handler threw an exception', message: error?.message, stack: error?.stack },
|
|
231
|
+
500
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
// Simplified endpoint: POST /
|
|
237
|
+
app.post('/', runAction);
|
|
238
|
+
// Legacy endpoint for backward compatibility: POST /run-action
|
|
239
|
+
app.post('/run-action', runAction);
|
|
57
240
|
|
|
58
241
|
const wss = new WebSocketServer({ noServer: true });
|
|
59
242
|
wss.on('connection', (ws) => {
|
|
60
243
|
connections.add(ws);
|
|
61
244
|
|
|
62
245
|
ws.on('message', (message) => {
|
|
63
|
-
|
|
246
|
+
logger.debug(`WebSocket message: ${message.toString()}`);
|
|
64
247
|
});
|
|
65
248
|
|
|
66
249
|
ws.on('close', () => {
|
|
67
|
-
console.log('Close connection');
|
|
68
250
|
connections.delete(ws);
|
|
69
251
|
});
|
|
70
252
|
});
|
|
71
253
|
|
|
72
254
|
async function startServer() {
|
|
73
|
-
return new Promise((res:
|
|
74
|
-
const server = serve({ fetch: app.fetch, port: 3000 })
|
|
255
|
+
return new Promise((res: () => void) => {
|
|
256
|
+
const server = serve({ fetch: app.fetch, port: 3000 }, () => {
|
|
257
|
+
logger.info('benSDK dev server running at http://localhost:3000');
|
|
258
|
+
logger.info(' POST / — Run a handler (e.g. { "run": "SYNC_EXISTING_GRANT", "payload": {}, "ctx": {} })');
|
|
259
|
+
logger.info(' POST /run-action — Run a handler (legacy format)');
|
|
260
|
+
logger.info(' GET /state-machine — Get the state machine definition');
|
|
261
|
+
|
|
262
|
+
if (devTestConfig.credentials) {
|
|
263
|
+
logger.info(' ✓ Real credentials loaded — benefitPartnerAPI will use real API calls');
|
|
264
|
+
} else {
|
|
265
|
+
logger.warn(' ⚠ No credentials found — benefitPartnerAPI will use mocked responses from context.config.ts');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (devTestConfig.eligibilityOptions) {
|
|
269
|
+
const actions = Object.keys(devTestConfig.eligibilityOptions).join(', ');
|
|
270
|
+
logger.info(` ✓ EligibilityOptions presets loaded for: ${actions}`);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
|
|
75
274
|
server.on('upgrade', (request, socket, head) => {
|
|
76
|
-
|
|
77
|
-
if (url === '/ws') {
|
|
275
|
+
if (request.url === '/ws') {
|
|
78
276
|
wss.handleUpgrade(request, socket, head, (ws) => {
|
|
79
277
|
wss.emit('connection', ws, request);
|
|
80
278
|
});
|
|
@@ -82,8 +280,11 @@ async function startServer() {
|
|
|
82
280
|
socket.destroy();
|
|
83
281
|
}
|
|
84
282
|
});
|
|
283
|
+
|
|
85
284
|
res();
|
|
86
285
|
});
|
|
87
286
|
}
|
|
88
287
|
|
|
288
|
+
startServer();
|
|
289
|
+
|
|
89
290
|
export { app, startServer };
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
"format": "npx prettier --write .",
|
|
9
9
|
"lint": "npx tsc --noemit && npx prettier --check . && npx eslint .",
|
|
10
10
|
"start": "cd bin/viewer && tsx startup.ts",
|
|
11
|
+
"dev:start": "tsx bin/server/app.ts",
|
|
12
|
+
"docs:update": "tsx bin/docs/update-readme.ts",
|
|
11
13
|
"generate": "tsx bin/cli/generate.ts",
|
|
12
14
|
"build": "tsc"
|
|
13
15
|
},
|
|
@@ -48,6 +50,7 @@
|
|
|
48
50
|
"svelte-check": "^4.0.0",
|
|
49
51
|
"vite": "^6.2.5",
|
|
50
52
|
"marked": "^15.0.10",
|
|
51
|
-
"open": "^10.1.1"
|
|
53
|
+
"open": "^10.1.1",
|
|
54
|
+
"pino-pretty": "^13.0.0"
|
|
52
55
|
}
|
|
53
56
|
}
|