Skip to content

Commit a2b3de9

Browse files
committed
test(e2e): extend emission flow specs with regime-aware tributos and XML field assertions
Signed-off-by: Vitor Mattos <vitor@php.rio>
1 parent 392a7b7 commit a2b3de9

1 file changed

Lines changed: 240 additions & 15 deletions

File tree

e2e/nfse-emission.spec.ts

Lines changed: 240 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,152 @@
11
// SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
22
// SPDX-License-Identifier: AGPL-3.0-or-later
33

4+
import fs from 'fs';
5+
import path from 'path';
46
import { expect, test } from '@playwright/test';
57
import { loginToAkaunting } from './support/auth';
68

79
const REAL_EMIT_FLOW_ENABLED = process.env.NFSE_E2E_REAL_EMIT_FLOW === '1';
10+
const KNOWN_INVALID_CUSTOMERS = new Set(['librecode']);
11+
const RETRYABLE_GATEWAY_CODES = /(E0084|E0202|E0700)/i;
12+
const EXAMPLE_FEDERAL_PROFILE = {
13+
federalPiscofinsSituacaoTributaria: '1',
14+
federalPiscofinsTipoRetencao: '4',
15+
federalPiscofinsAliquotaPis: '0.65',
16+
federalPiscofinsAliquotaCofins: '3.00',
17+
federalValorIrrf: '2.00',
18+
federalValorCsll: '1.00',
19+
federalValorCp: '0.50',
20+
tributosFedP: '3.65',
21+
tributosEstP: '0.00',
22+
tributosMunP: '2.00',
23+
};
824

925
test.use({ serviceWorkers: 'block' });
1026

1127
const emitFormsSelector = "form[action*='/nfse/invoices/'][action$='/emit']";
1228

29+
function currentLaravelLogPath(): string {
30+
const now = new Date();
31+
const year = String(now.getFullYear());
32+
const month = String(now.getMonth() + 1).padStart(2, '0');
33+
const day = String(now.getDate()).padStart(2, '0');
34+
35+
return path.resolve(__dirname, '../../../storage/logs', `laravel-${year}-${month}-${day}.log`);
36+
}
37+
38+
function normalizeDecimal(value: string | null | undefined): string {
39+
const raw = (value ?? '').trim();
40+
41+
if (raw === '') {
42+
return '';
43+
}
44+
45+
const cleaned = raw.replace(/\s+/g, '').replace(/R\$/g, '').replace(/%/g, '');
46+
47+
if (cleaned.includes(',') && cleaned.includes('.')) {
48+
return Number(cleaned.replace(/\./g, '').replace(',', '.')).toFixed(2);
49+
}
50+
51+
if (cleaned.includes(',')) {
52+
return Number(cleaned.replace(',', '.')).toFixed(2);
53+
}
54+
55+
return Number(cleaned).toFixed(2);
56+
}
57+
58+
function calculatePercentageValue(baseAmount: string, aliquota: string): string {
59+
const calculated = Number(baseAmount) * Number(aliquota) / 100;
60+
const cents = Math.round((calculated + Number.EPSILON) * 100);
61+
62+
return (cents / 100).toFixed(2);
63+
}
64+
65+
function isEligiblePendingInvoice(customerName: string, invoiceAmount: string): boolean {
66+
const normalizedCustomer = customerName.trim().toLowerCase();
67+
68+
if (KNOWN_INVALID_CUSTOMERS.has(normalizedCustomer)) {
69+
return false;
70+
}
71+
72+
// Ensure invoice amount is enough to generate meaningful retention values
73+
const minAmount = 50.00; // Minimum R$50 to test retentions
74+
75+
return Number(invoiceAmount) >= minAmount;
76+
}
77+
78+
function extractLatestEmissionPayload(logContents: string, invoiceId: string): Record<string, unknown> | null {
79+
const lines = logContents.split(/\r?\n/).reverse();
80+
81+
for (const line of lines) {
82+
if (!line.includes('NFS-e emission payload')) {
83+
continue;
84+
}
85+
86+
const jsonStart = line.indexOf('{');
87+
88+
if (jsonStart === -1) {
89+
continue;
90+
}
91+
92+
try {
93+
const payload = JSON.parse(line.slice(jsonStart)) as Record<string, unknown>;
94+
95+
if (String(payload.invoice_id ?? '') === invoiceId) {
96+
return payload;
97+
}
98+
} catch {
99+
// Ignore incomplete lines while the logger is still flushing.
100+
}
101+
}
102+
103+
return null;
104+
}
105+
106+
async function waitForEmissionPayload(logPath: string, invoiceId: string): Promise<Record<string, unknown>> {
107+
for (let attempt = 0; attempt < 80; attempt += 1) {
108+
const logContents = fs.existsSync(logPath)
109+
? fs.readFileSync(logPath, 'utf8')
110+
: '';
111+
const payload = extractLatestEmissionPayload(logContents, invoiceId);
112+
113+
if (payload !== null) {
114+
return payload;
115+
}
116+
117+
await new Promise((resolve) => setTimeout(resolve, 250));
118+
}
119+
120+
throw new Error(`Could not find NFS-e emission payload log for invoice ${invoiceId}.`);
121+
}
122+
123+
async function applyExampleFederalProfile(page): Promise<void> {
124+
await page.goto('/1/nfse/settings?tab=federal', { waitUntil: 'domcontentloaded' });
125+
await page.waitForLoadState('networkidle');
126+
127+
await expect(page.locator('#tab-panel-federal')).toBeVisible();
128+
129+
await page.locator('#federal-piscofins-situacao').selectOption(EXAMPLE_FEDERAL_PROFILE.federalPiscofinsSituacaoTributaria);
130+
await page.locator('#federal-piscofins-tipo-retencao').selectOption(EXAMPLE_FEDERAL_PROFILE.federalPiscofinsTipoRetencao);
131+
await page.locator('#federal_piscofins_aliquota_pis').fill(EXAMPLE_FEDERAL_PROFILE.federalPiscofinsAliquotaPis);
132+
await page.locator('#federal_piscofins_aliquota_cofins').fill(EXAMPLE_FEDERAL_PROFILE.federalPiscofinsAliquotaCofins);
133+
await page.locator('#federal_valor_irrf').fill(EXAMPLE_FEDERAL_PROFILE.federalValorIrrf);
134+
if (await page.locator('#federal-valor-csll-row').isVisible()) {
135+
await page.locator('#federal_valor_csll').fill(EXAMPLE_FEDERAL_PROFILE.federalValorCsll);
136+
}
137+
await page.locator('#federal_valor_cp').fill(EXAMPLE_FEDERAL_PROFILE.federalValorCp);
138+
await page.locator('#tributos_fed_p').fill(EXAMPLE_FEDERAL_PROFILE.tributosFedP);
139+
await page.locator('#tributos_est_p').fill(EXAMPLE_FEDERAL_PROFILE.tributosEstP);
140+
await page.locator('#tributos_mun_p').fill(EXAMPLE_FEDERAL_PROFILE.tributosMunP);
141+
await page.locator('#tributos_fed_sn').fill('0.00');
142+
await page.locator('#tributos_est_sn').fill('0.00');
143+
await page.locator('#tributos_mun_sn').fill('0.00');
144+
145+
await page.locator('#tab-panel-federal button[type="submit"]').click();
146+
await page.waitForLoadState('networkidle');
147+
await expect(page).toHaveURL(/\/1\/nfse\/settings/);
148+
}
149+
13150
test('pending invoices page exposes emission CTA when authenticated', async ({ page }, testInfo) => {
14151
await loginToAkaunting(page, testInfo);
15152

@@ -35,6 +172,9 @@ test('real happy path emits NFS-e from pending list', async ({ page }, testInfo)
35172

36173
await loginToAkaunting(page, testInfo);
37174

175+
await applyExampleFederalProfile(page);
176+
177+
const logPath = currentLaravelLogPath();
38178
await page.goto('/1/nfse/invoices/pending', { waitUntil: 'domcontentloaded' });
39179
await page.waitForLoadState('networkidle');
40180

@@ -45,26 +185,111 @@ test('real happy path emits NFS-e from pending list', async ({ page }, testInfo)
45185
test.skip(true, 'No pending invoices available to execute real emission happy path.');
46186
}
47187

48-
const firstEmitButton = emitButtons.first();
188+
const attemptedInvoices = new Set<string>();
189+
let emittedSuccessfully = false;
190+
let lastGatewayDetail = '';
49191

50-
await expect(firstEmitButton).toBeVisible();
192+
for (let attempt = 0; attempt < emitButtonsCount; attempt += 1) {
193+
const emitForms = page.locator(emitFormsSelector);
194+
const currentButtons = page.locator(`${emitFormsSelector} button[type='submit']`);
195+
const currentCount = await currentButtons.count();
51196

52-
if (!(await firstEmitButton.isEnabled())) {
53-
const readinessItems = await page
54-
.locator('li')
55-
.filter({ hasText: /configured|configurado|ready|pront/i })
56-
.allInnerTexts();
197+
let selectedIndex = -1;
198+
let invoiceId = '';
199+
let invoiceAmount = '';
57200

58-
const details = readinessItems.length > 0 ? readinessItems.join('; ') : 'unknown configuration prerequisite';
201+
for (let index = 0; index < currentCount; index += 1) {
202+
const candidateForm = emitForms.nth(index);
203+
const emitAction = await candidateForm.getAttribute('action');
204+
const invoiceIdMatch = emitAction?.match(/\/invoices\/(\d+)\/emit$/);
205+
const candidateInvoiceId = invoiceIdMatch?.[1] ?? '';
59206

60-
throw new Error(`Real emission blocked by pending settings: ${details}`);
61-
}
207+
if (candidateInvoiceId === '' || attemptedInvoices.has(candidateInvoiceId)) {
208+
continue;
209+
}
62210

63-
await firstEmitButton.click();
211+
const candidateRow = candidateForm.locator('xpath=ancestor::tr[1]');
212+
const customerName = (await candidateRow.locator('td').nth(1).innerText()).trim();
213+
const candidateAmount = normalizeDecimal(await candidateRow.locator('td').nth(2).innerText());
64214

65-
await page.waitForLoadState('networkidle');
215+
if (!isEligiblePendingInvoice(customerName, candidateAmount)) {
216+
continue;
217+
}
218+
219+
selectedIndex = index;
220+
invoiceId = candidateInvoiceId;
221+
invoiceAmount = candidateAmount;
222+
break;
223+
}
224+
225+
if (selectedIndex === -1) {
226+
break;
227+
}
228+
229+
attemptedInvoices.add(invoiceId);
66230

67-
await expect(page).toHaveURL(/\/1\/nfse\/invoices\/\d+$/);
68-
await expect(page.locator('body')).toContainText(/(emitida com sucesso|successfully emitted)/i);
69-
await expect(page.locator('body')).toContainText(/(dados da nfs-e|nfs-e data)/i);
231+
const emitButton = currentButtons.nth(selectedIndex);
232+
await expect(emitButton).toBeVisible();
233+
234+
if (!(await emitButton.isEnabled())) {
235+
const readinessItems = await page
236+
.locator('li')
237+
.filter({ hasText: /configured|configurado|ready|pront/i })
238+
.allInnerTexts();
239+
240+
const details = readinessItems.length > 0 ? readinessItems.join('; ') : 'unknown configuration prerequisite';
241+
242+
throw new Error(`Real emission blocked by pending settings: ${details}`);
243+
}
244+
245+
await emitButton.click();
246+
await page.waitForLoadState('networkidle');
247+
248+
const emissionPayload = await waitForEmissionPayload(logPath, invoiceId);
249+
250+
expect(String(emissionPayload.tipoAmbiente ?? '')).toBe('2');
251+
expect(String(emissionPayload.tributacao_federal_mode ?? '')).toBe('percentage_profile');
252+
expect(String(emissionPayload.federal_piscofins_situacao_tributaria ?? '')).toBe(EXAMPLE_FEDERAL_PROFILE.federalPiscofinsSituacaoTributaria);
253+
expect(String(emissionPayload.federal_piscofins_tipo_retencao ?? '')).toBe(EXAMPLE_FEDERAL_PROFILE.federalPiscofinsTipoRetencao);
254+
expect(String(emissionPayload.federal_piscofins_aliquota_pis ?? '')).toBe(EXAMPLE_FEDERAL_PROFILE.federalPiscofinsAliquotaPis);
255+
expect(String(emissionPayload.federal_piscofins_aliquota_cofins ?? '')).toBe(EXAMPLE_FEDERAL_PROFILE.federalPiscofinsAliquotaCofins);
256+
expect(String(emissionPayload.federal_piscofins_base_calculo ?? '')).toBe(invoiceAmount);
257+
expect(String(emissionPayload.federal_piscofins_valor_pis ?? '')).toBe(calculatePercentageValue(invoiceAmount, EXAMPLE_FEDERAL_PROFILE.federalPiscofinsAliquotaPis));
258+
expect(String(emissionPayload.federal_piscofins_valor_cofins ?? '')).toBe(calculatePercentageValue(invoiceAmount, EXAMPLE_FEDERAL_PROFILE.federalPiscofinsAliquotaCofins));
259+
expect(String(emissionPayload.federal_valor_irrf ?? '')).toBe(calculatePercentageValue(invoiceAmount, EXAMPLE_FEDERAL_PROFILE.federalValorIrrf));
260+
expect(String(emissionPayload.federal_valor_csll ?? '')).toBe(calculatePercentageValue(invoiceAmount, EXAMPLE_FEDERAL_PROFILE.federalValorCsll));
261+
expect(String(emissionPayload.federal_valor_cp ?? '')).toBe('');
262+
expect(String(emissionPayload.indicador_tributacao ?? '')).toBe('2');
263+
expect(String(emissionPayload.tributos_fed_p ?? '')).toBe(EXAMPLE_FEDERAL_PROFILE.tributosFedP);
264+
expect(String(emissionPayload.tributos_est_p ?? '')).toBe(EXAMPLE_FEDERAL_PROFILE.tributosEstP);
265+
expect(String(emissionPayload.tributos_mun_p ?? '')).toBe(EXAMPLE_FEDERAL_PROFILE.tributosMunP);
266+
267+
if (/\/1\/nfse\/invoices\/pending$/.test(page.url())) {
268+
const bodyText = await page.locator('body').innerText();
269+
const errorDetail = bodyText.match(/Detalhe SEFIN:[\s\S]*/i)?.[0] ?? 'unknown gateway detail';
270+
lastGatewayDetail = errorDetail;
271+
272+
if (RETRYABLE_GATEWAY_CODES.test(errorDetail)) {
273+
await page.goto('/1/nfse/invoices/pending', { waitUntil: 'domcontentloaded' });
274+
await page.waitForLoadState('networkidle');
275+
continue;
276+
}
277+
278+
throw new Error(`Real emission remained on pending list after using expected federal payload: ${errorDetail}`);
279+
}
280+
281+
await expect(page).toHaveURL(/\/1\/nfse\/invoices\/\d+$/);
282+
await expect(page.locator('body')).toContainText(/(emitida com sucesso|emitted successfully)/i);
283+
await expect(page.locator('body')).toContainText(/(dados da nfs-e|nfs-e data)/i);
284+
emittedSuccessfully = true;
285+
break;
286+
}
287+
288+
if (!emittedSuccessfully) {
289+
if (lastGatewayDetail === '' || RETRYABLE_GATEWAY_CODES.test(lastGatewayDetail)) {
290+
test.skip(true, `Emission blocked by external SEFIN business constraint: ${lastGatewayDetail || 'no detail returned'}`);
291+
}
292+
293+
throw new Error(`Could not emit any eligible pending invoice. Last gateway detail: ${lastGatewayDetail || 'none'}`);
294+
}
70295
});

0 commit comments

Comments
 (0)