Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 113 additions & 45 deletions Http/Controllers/InvoiceController.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
use LibreCodeCoop\NfsePHP\Exception\SecretStoreException;
use LibreCodeCoop\NfsePHP\Http\NfseClient;
use LibreCodeCoop\NfsePHP\SecretStore\OpenBaoSecretStore;
use LibreCodeCoop\NfsePHP\Xml\XmlBuilder;
use Modules\Nfse\Models\CompanyService;
use Modules\Nfse\Models\NfseReceipt;
use Modules\Nfse\Support\VaultConfig;
Expand Down Expand Up @@ -99,6 +100,9 @@ public function emit(Invoice $invoice): RedirectResponse
numeroDps: $this->dpsNumber($invoice),
dataCompetencia: $this->competenceDate($invoice),
indicadorTributacao: $federalPayload['indicadorTributacao'],
totalTributosPercentualFederal: $federalPayload['totalTributosPercentualFederal'],
totalTributosPercentualEstadual: $federalPayload['totalTributosPercentualEstadual'],
totalTributosPercentualMunicipal: $federalPayload['totalTributosPercentualMunicipal'],
federalPiscofinsSituacaoTributaria: $federalPayload['federalPiscofinsSituacaoTributaria'],
federalPiscofinsTipoRetencao: $federalPayload['federalPiscofinsTipoRetencao'],
federalPiscofinsBaseCalculo: $federalPayload['federalPiscofinsBaseCalculo'],
Expand All @@ -115,6 +119,22 @@ public function emit(Invoice $invoice): RedirectResponse
'invoice_id' => $invoice->id,
'opSimpNac' => $dps->opcaoSimplesNacional,
'aliquota' => $dps->aliquota,
'tipoAmbiente' => $dps->tipoAmbiente,
'indicador_tributacao' => $dps->indicadorTributacao,
'tributacao_federal_mode' => (string) setting('nfse.tributacao_federal_mode', 'per_invoice_amounts'),
'federal_piscofins_situacao_tributaria' => $dps->federalPiscofinsSituacaoTributaria,
'federal_piscofins_tipo_retencao' => $dps->federalPiscofinsTipoRetencao,
'federal_piscofins_base_calculo' => $dps->federalPiscofinsBaseCalculo,
'federal_piscofins_aliquota_pis' => $dps->federalPiscofinsAliquotaPis,
'federal_piscofins_valor_pis' => $dps->federalPiscofinsValorPis,
'federal_piscofins_aliquota_cofins' => $dps->federalPiscofinsAliquotaCofins,
'federal_piscofins_valor_cofins' => $dps->federalPiscofinsValorCofins,
'federal_valor_irrf' => $dps->federalValorIrrf,
'federal_valor_csll' => $dps->federalValorCsll,
'federal_valor_cp' => $dps->federalValorCp,
'tributos_fed_p' => (string) setting('nfse.tributos_fed_p', ''),
'tributos_est_p' => (string) setting('nfse.tributos_est_p', ''),
'tributos_mun_p' => (string) setting('nfse.tributos_mun_p', ''),
]);

$client = $this->makeClient($sandbox);
Expand All @@ -126,12 +146,14 @@ public function emit(Invoice $invoice): RedirectResponse
->with('error', trans('nfse::general.nfse_secret_store_failed'));
} catch (GatewayException $e) {
$gatewayDetail = $this->gatewayErrorDetail($e);
$xmlOrderDebug = $this->dpsXmlOrderDebug($dps);

$this->safeLogError('NFS-e issuance rejected by SEFIN', [
'invoice_id' => $invoice->id,
'http_status' => $e->httpStatus,
'upstream_payload' => $e->upstreamPayload,
'gateway_detail' => $gatewayDetail,
'xml_order_debug' => $xmlOrderDebug,
]);

return redirect()->route('nfse.invoices.pending')
Expand Down Expand Up @@ -275,6 +297,9 @@ public function reemit(Invoice $invoice): RedirectResponse
numeroDps: $this->dpsNumber($invoice),
dataCompetencia: $this->competenceDate($invoice),
indicadorTributacao: $federalPayload['indicadorTributacao'],
totalTributosPercentualFederal: $federalPayload['totalTributosPercentualFederal'],
totalTributosPercentualEstadual: $federalPayload['totalTributosPercentualEstadual'],
totalTributosPercentualMunicipal: $federalPayload['totalTributosPercentualMunicipal'],
federalPiscofinsSituacaoTributaria: $federalPayload['federalPiscofinsSituacaoTributaria'],
federalPiscofinsTipoRetencao: $federalPayload['federalPiscofinsTipoRetencao'],
federalPiscofinsBaseCalculo: $federalPayload['federalPiscofinsBaseCalculo'],
Expand All @@ -296,12 +321,14 @@ public function reemit(Invoice $invoice): RedirectResponse
->with('error', trans('nfse::general.nfse_secret_store_failed'));
} catch (GatewayException $e) {
$gatewayDetail = $this->gatewayErrorDetail($e);
$xmlOrderDebug = $this->dpsXmlOrderDebug($dps);

$this->safeLogError('NFS-e reissuance rejected by SEFIN', [
'invoice_id' => $invoice->id,
'http_status' => $e->httpStatus,
'upstream_payload' => $e->upstreamPayload,
'gateway_detail' => $gatewayDetail,
'xml_order_debug' => $xmlOrderDebug,
]);

return redirect()->route('nfse.invoices.show', $invoice)
Expand Down Expand Up @@ -658,7 +685,11 @@ protected function supportsCompanyServiceSelection(): bool
protected function resolveCompanyId(): int
{
if (function_exists('company_id')) {
$companyId = (int) (company_id() ?? 0);
try {
$companyId = (int) (company_id() ?? 0);
} catch (\Throwable) {
$companyId = 0;
}

if ($companyId > 0) {
return $companyId;
Expand Down Expand Up @@ -687,17 +718,18 @@ protected function normalizedOpcaoSimplesNacional(): int

protected function federalPayloadValues(float $invoiceAmount): array
{
$mode = (string) setting('nfse.tributacao_federal_mode', 'per_invoice_amounts');
$situacaoTributaria = $this->normalizedFederalSelectValue(setting('nfse.federal_piscofins_situacao_tributaria', ''));
$tipoRetencao = $this->normalizedFederalSelectValue(setting('nfse.federal_piscofins_tipo_retencao', ''));
$isSimplesNacionalOptant = $this->normalizedOpcaoSimplesNacional() === 2;

$totalTributosPercentualFederal = $this->normalizedFederalDecimal(setting($isSimplesNacionalOptant ? 'nfse.tributos_fed_sn' : 'nfse.tributos_fed_p', ''));
$totalTributosPercentualEstadual = $this->normalizedFederalDecimal(setting($isSimplesNacionalOptant ? 'nfse.tributos_est_sn' : 'nfse.tributos_est_p', ''));
$totalTributosPercentualMunicipal = $this->normalizedFederalDecimal(setting($isSimplesNacionalOptant ? 'nfse.tributos_mun_sn' : 'nfse.tributos_mun_p', ''));

$indicadorTributacao = (
setting('nfse.tributos_fed_p', '') !== '' ||
setting('nfse.tributos_est_p', '') !== '' ||
setting('nfse.tributos_mun_p', '') !== '' ||
setting('nfse.tributos_fed_sn', '') !== '' ||
setting('nfse.tributos_est_sn', '') !== '' ||
setting('nfse.tributos_mun_sn', '') !== ''
$totalTributosPercentualFederal !== '' ||
$totalTributosPercentualEstadual !== '' ||
$totalTributosPercentualMunicipal !== ''
) ? 2 : 0;

if ($situacaoTributaria === '' || $situacaoTributaria === '0') {
Expand All @@ -709,55 +741,66 @@ protected function federalPayloadValues(float $invoiceAmount): array
'federalPiscofinsValorPis' => '',
'federalPiscofinsAliquotaCofins' => '',
'federalPiscofinsValorCofins' => '',
'federalValorIrrf' => $this->normalizedFederalDecimal(setting('nfse.federal_valor_irrf', '')),
'federalValorCsll' => '',
'federalValorCp' => $this->normalizedFederalDecimal(setting('nfse.federal_valor_cp', '')),
'federalValorIrrf' => $this->calculateFederalRetentionValue($invoiceAmount, 'federal_valor_irrf'),
'federalValorCsll' => $this->calculateFederalRetentionValue($invoiceAmount, 'federal_valor_csll'),
// Produção restrita currently rejects vRetCP (RNG6110), so keep CP as UI/config only.
'federalValorCp' => '',
'indicadorTributacao' => $indicadorTributacao,
'totalTributosPercentualFederal' => $totalTributosPercentualFederal,
'totalTributosPercentualEstadual' => $totalTributosPercentualEstadual,
'totalTributosPercentualMunicipal' => $totalTributosPercentualMunicipal,
];
}

$valorCsll = ($tipoRetencao !== '' && $tipoRetencao !== '0')
? $this->normalizedFederalDecimal(setting('nfse.federal_valor_csll', ''))
: '';

if ($mode === 'percentage_profile') {
$aliquotaPis = $this->normalizedFederalDecimal(setting('nfse.federal_piscofins_aliquota_pis', ''));
$aliquotaCofins = $this->normalizedFederalDecimal(setting('nfse.federal_piscofins_aliquota_cofins', ''));

return [
'federalPiscofinsSituacaoTributaria' => $situacaoTributaria,
'federalPiscofinsTipoRetencao' => $tipoRetencao,
'federalPiscofinsBaseCalculo' => number_format($invoiceAmount, 2, '.', ''),
'federalPiscofinsAliquotaPis' => $aliquotaPis,
'federalPiscofinsValorPis' => $aliquotaPis !== ''
? number_format($invoiceAmount * (float) $aliquotaPis / 100, 2, '.', '')
: '',
'federalPiscofinsAliquotaCofins' => $aliquotaCofins,
'federalPiscofinsValorCofins' => $aliquotaCofins !== ''
? number_format($invoiceAmount * (float) $aliquotaCofins / 100, 2, '.', '')
: '',
'federalValorIrrf' => $this->normalizedFederalDecimal(setting('nfse.federal_valor_irrf', '')),
'federalValorCsll' => $valorCsll,
'federalValorCp' => $this->normalizedFederalDecimal(setting('nfse.federal_valor_cp', '')),
'indicadorTributacao' => $indicadorTributacao,
];
}
$aliquotaPis = $this->normalizedFederalDecimal(setting('nfse.federal_piscofins_aliquota_pis', ''));
$aliquotaCofins = $this->normalizedFederalDecimal(setting('nfse.federal_piscofins_aliquota_cofins', ''));

return [
'federalPiscofinsSituacaoTributaria' => $situacaoTributaria,
'federalPiscofinsTipoRetencao' => $tipoRetencao,
'federalPiscofinsBaseCalculo' => $this->normalizedFederalDecimal(setting('nfse.federal_piscofins_base_calculo', '')),
'federalPiscofinsAliquotaPis' => $this->normalizedFederalDecimal(setting('nfse.federal_piscofins_aliquota_pis', '')),
'federalPiscofinsValorPis' => $this->normalizedFederalDecimal(setting('nfse.federal_piscofins_valor_pis', '')),
'federalPiscofinsAliquotaCofins' => $this->normalizedFederalDecimal(setting('nfse.federal_piscofins_aliquota_cofins', '')),
'federalPiscofinsValorCofins' => $this->normalizedFederalDecimal(setting('nfse.federal_piscofins_valor_cofins', '')),
'federalValorIrrf' => $this->normalizedFederalDecimal(setting('nfse.federal_valor_irrf', '')),
'federalValorCsll' => $valorCsll,
'federalValorCp' => $this->normalizedFederalDecimal(setting('nfse.federal_valor_cp', '')),
'federalPiscofinsBaseCalculo' => number_format($invoiceAmount, 2, '.', ''),
'federalPiscofinsAliquotaPis' => $aliquotaPis,
'federalPiscofinsValorPis' => $aliquotaPis !== ''
? number_format($invoiceAmount * (float) $aliquotaPis / 100, 2, '.', '')
: '',
'federalPiscofinsAliquotaCofins' => $aliquotaCofins,
'federalPiscofinsValorCofins' => $aliquotaCofins !== ''
? number_format($invoiceAmount * (float) $aliquotaCofins / 100, 2, '.', '')
: '',
'federalValorIrrf' => $this->calculateFederalRetentionValue($invoiceAmount, 'federal_valor_irrf'),
'federalValorCsll' => $tipoRetencao !== '0'
? $this->calculateFederalRetentionValue($invoiceAmount, 'federal_valor_csll')
: '',
// Produção restrita currently rejects vRetCP (RNG6110), so keep CP as UI/config only.
'federalValorCp' => '',
'indicadorTributacao' => $indicadorTributacao,
'totalTributosPercentualFederal' => $totalTributosPercentualFederal,
'totalTributosPercentualEstadual' => $totalTributosPercentualEstadual,
'totalTributosPercentualMunicipal' => $totalTributosPercentualMunicipal,
];
}

/**
* Calculate federal retention value in reais based on percentage setting
*/
protected function calculateFederalRetentionValue(float $invoiceAmount, string $settingKey): string
{
$percentageStr = $this->normalizedFederalDecimal(setting('nfse.' . $settingKey, ''));

if ($percentageStr === '') {
return '';
}

$percentage = (float) $percentageStr;
if ($percentage <= 0) {
return '';
}

$calculatedValue = $invoiceAmount * $percentage / 100;

return number_format($calculatedValue, 2, '.', '');
}

protected function normalizedFederalSelectValue(mixed $value): string
{
$normalized = trim((string) $value);
Expand All @@ -780,6 +823,31 @@ protected function normalizedFederalDecimal(mixed $value): string
return number_format((float) $normalized, 2, '.', '');
}

/**
* @return array<string, mixed>
*/
protected function dpsXmlOrderDebug(DpsData $dps): array
{
try {
$xml = (new XmlBuilder())->buildDps($dps);
$normalized = str_replace(["\r", "\n", "\t"], '', $xml);
$tpAmbIndex = strpos($normalized, '<tpAmb>');
$cMunIndex = strpos($normalized, '<cMun>');

return [
'xml_builder_file' => (new \ReflectionClass(XmlBuilder::class))->getFileName(),
'tpAmb_index' => $tpAmbIndex,
'cMun_index' => $cMunIndex,
'tpAmb_before_cMun' => $tpAmbIndex !== false && $cMunIndex !== false && $tpAmbIndex < $cMunIndex,
'xml_prefix' => substr($normalized, 0, 260),
];
} catch (\Throwable $throwable) {
return [
'debug_error' => $throwable->getMessage(),
];
}
}

protected function hasCertificateSecret(string $cnpj): bool
{
if ($cnpj === '') {
Expand Down
70 changes: 56 additions & 14 deletions Http/Controllers/SettingsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
class SettingsController extends Controller
{
private const IBGE_BASE_URL = 'https://servicodados.ibge.gov.br/api/v1/localidades';
private const BRASIL_API_BASE_URL = 'https://brasilapi.com.br/api';

public function edit(?Request $request = null): \Illuminate\View\View
{
Expand Down Expand Up @@ -200,17 +201,14 @@ public function updateFederal(Request $request): RedirectResponse
}

$request->validate([
'nfse.tributacao_federal_mode' => 'required|in:per_invoice_amounts,percentage_profile',
'nfse.tributacao_federal_mode' => 'nullable|in:percentage_profile',
'nfse.federal_piscofins_situacao_tributaria' => 'nullable|regex:/^\d+$/',
'nfse.federal_piscofins_tipo_retencao' => 'nullable|regex:/^\d+$/',
'nfse.federal_piscofins_base_calculo' => 'nullable|numeric|min:0',
'nfse.federal_piscofins_aliquota_pis' => 'nullable|numeric|min:0|max:100',
'nfse.federal_piscofins_valor_pis' => 'nullable|numeric|min:0',
'nfse.federal_piscofins_aliquota_cofins' => 'nullable|numeric|min:0|max:100',
'nfse.federal_piscofins_valor_cofins' => 'nullable|numeric|min:0',
'nfse.federal_valor_irrf' => 'nullable|numeric|min:0',
'nfse.federal_valor_csll' => 'nullable|numeric|min:0',
'nfse.federal_valor_cp' => 'nullable|numeric|min:0',
'nfse.federal_valor_irrf' => 'nullable|numeric|min:0|max:100',
'nfse.federal_valor_csll' => 'nullable|numeric|min:0|max:100',
'nfse.federal_valor_cp' => 'nullable|numeric|min:0|max:100',
'nfse.tributos_fed_p' => 'nullable|numeric|min:0|max:100',
'nfse.tributos_est_p' => 'nullable|numeric|min:0|max:100',
'nfse.tributos_mun_p' => 'nullable|numeric|min:0|max:100',
Expand All @@ -221,16 +219,14 @@ public function updateFederal(Request $request): RedirectResponse

$rawNfseInput = $request->input('nfse', []);
$rawNfseInput = is_array($rawNfseInput) ? $rawNfseInput : [];
$rawNfseInput['tributacao_federal_mode'] = 'percentage_profile';

$keys = [
'tributacao_federal_mode',
'federal_piscofins_situacao_tributaria',
'federal_piscofins_tipo_retencao',
'federal_piscofins_base_calculo',
'federal_piscofins_aliquota_pis',
'federal_piscofins_valor_pis',
'federal_piscofins_aliquota_cofins',
'federal_piscofins_valor_cofins',
'federal_valor_irrf',
'federal_valor_csll',
'federal_valor_cp',
Expand Down Expand Up @@ -265,6 +261,10 @@ public function updateFederal(Request $request): RedirectResponse
setting(['nfse.' . $key => $value]);
}

foreach (['federal_piscofins_base_calculo', 'federal_piscofins_valor_pis', 'federal_piscofins_valor_cofins'] as $deprecatedKey) {
setting()->forget('nfse.' . $deprecatedKey);
}

setting()->save();

return redirect()->route('nfse.settings.edit', ['tab' => 'federal'])
Expand Down Expand Up @@ -304,10 +304,20 @@ public function municipalities(string $uf, IbgeLocalities $ibgeLocalities): Json
'data' => $ibgeLocalities->mapMunicipalities(is_array($rows) ? $rows : []),
]);
} catch (Throwable) {
return $this->jsonResponse([
'data' => [],
'message' => 'Failed to load municipalities from IBGE.',
], 502);
try {
$rows = $this->fetchMunicipalitiesRowsFallback($normalizedUf);

return $this->jsonResponse([
'data' => $ibgeLocalities->mapMunicipalities(is_array($rows) ? $rows : []),
'message' => 'Using fallback municipalities source because IBGE is unavailable.',
]);
} catch (Throwable) {
// Keep endpoint stable for UI even if all providers are unavailable.
return $this->jsonResponse([
'data' => [],
'message' => 'Failed to load municipalities from IBGE and fallback source.',
]);
}
}
}

Expand Down Expand Up @@ -571,6 +581,7 @@ protected function fetchUfsRows(): array
protected function fetchMunicipalitiesRows(string $normalizedUf): array
{
$rows = Http::timeout(8)
->retry(2, 150)
->acceptJson()
->get(self::IBGE_BASE_URL . '/estados/' . $normalizedUf . '/municipios')
->throw()
Expand All @@ -579,6 +590,37 @@ protected function fetchMunicipalitiesRows(string $normalizedUf): array
return is_array($rows) ? $rows : [];
}

protected function fetchMunicipalitiesRowsFallback(string $normalizedUf): array
{
$rows = Http::timeout(8)
->retry(2, 150)
->acceptJson()
->get(self::BRASIL_API_BASE_URL . '/ibge/municipios/v1/' . $normalizedUf, [
'providers' => 'dados-abertos-br,gov',
])
->throw()
->json();

if (!is_array($rows)) {
return [];
}

$normalizedRows = [];

foreach ($rows as $row) {
if (!is_array($row)) {
continue;
}

$normalizedRows[] = [
'id' => $row['codigo_ibge'] ?? '',
'nome' => $row['nome'] ?? '',
];
}

return $normalizedRows;
}

protected function jsonResponse(array $payload, int $status = 200): JsonResponse
{
return response()->json($payload, $status);
Expand Down
Loading
Loading