Skip to content
Open
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
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,20 @@ Para guias detalhados de instalação, configuração e uso, acesse nossa docume
* 📖 **[Guia de Instalação](https://nfe.io/docs/plugins/whmcs/instalacao/)**
* ⚙️ **[Manual de Configuração](https://nfe.io/docs/plugins/whmcs/configuracao/)**

## 📚 Atualização

Para atualizar o módulo com segurança, siga os passos abaixo:

1. **Faça um backup:** Antes de começar, faça um backup completo do seu banco de dados e dos arquivos do WHMCS.
2. **Baixe a nova versão:** Faça o download do *release* mais recente e confira os arquivos.
3. **Envie os arquivos:** Faça o upload dos arquivos baixados para a pasta do seu WHMCS, substituindo os antigos.

⚠️ **Atenção:** Em hipótese alguma desative o módulo no painel do WHMCS, pois isso apagará suas configurações. Apenas substitua os arquivos pela nova versão.

## 🛠️ Requisitos do Sistema

* **WHMCS:** Versão 8.0 ou superior
* **PHP:** Versão 7.2 ou superior
* **WHMCS:** Versão 8+ ou superior
* **PHP:** Versão 8+ ou superior
* **Conta NFE.io:** Uma conta ativa na [NFE.io](https://nfe.io) com chave de API.

## 📦 Changelog
Expand Down
19 changes: 12 additions & 7 deletions modules/addons/NFEioServiceInvoices/callback.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,18 @@

new NFEioServiceInvoices\Loader();

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo "Method Not Allowed";
exit();
}

if (isset($_GET['echo'])) {
http_response_code(200);
echo "ok";
exit();
}

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo "Method Not Allowed";
exit();
}

$headers = array_change_key_case(getallheaders(), CASE_LOWER);
$signature = $headers['x-hub-signature'] ?? $headers['x-nfeio-signature'] ?? null;

Expand All @@ -44,6 +44,11 @@

$payload = json_decode($body, true);

// Fix v1 to v2: Se a NFE.io mandar os dados encapsulados dentro da chave 'payload', nós extraímos para a raiz
if (isset($payload['payload']) && is_array($payload['payload'])) {
$payload = $payload['payload'];
}

if (!is_array($payload) || !isset($payload['id'], $payload['status'], $payload['flowStatus'], $payload['environment'])) {
logModuleCall('nfeio_serviceinvoices', 'callback_error', 'Payload inválido', ['body' => $payload]);
http_response_code(400);
Expand Down Expand Up @@ -102,4 +107,4 @@
logModuleCall('nfeio_serviceinvoices', 'callback', 'Nenhuma informação foi alterada', ['nfe' => $nfe, 'payload' => $payload]);
http_response_code(200);
exit();
}
}
69 changes: 69 additions & 0 deletions modules/addons/NFEioServiceInvoices/lib/Admin/Controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,58 @@ public function configuration($vars)
$vars['moduleCallBackUrl'] = $moduleCallBackUrl;
$vars['companies'] = $registeredCompanies;

// Lista de países (nome e código)
$isoCountries = [
'BR' => 'Brasil', 'US' => 'Estados Unidos', 'AR' => 'Argentina', 'DE' => 'Alemanha', 'FR' => 'França',
'IT' => 'Itália', 'JP' => 'Japão', 'CN' => 'China', 'GB' => 'Reino Unido', 'CA' => 'Canadá',
'MX' => 'México', 'ES' => 'Espanha', 'PT' => 'Portugal', 'RU' => 'Rússia', 'IN' => 'Índia',
'AU' => 'Austrália', 'ZA' => 'África do Sul', 'NL' => 'Holanda', 'CH' => 'Suíça', 'SE' => 'Suécia',
'NO' => 'Noruega', 'FI' => 'Finlândia', 'DK' => 'Dinamarca', 'KR' => 'Coreia do Sul', 'CL' => 'Chile',
'CO' => 'Colômbia', 'PE' => 'Peru', 'PL' => 'Polônia', 'TR' => 'Turquia', 'NZ' => 'Nova Zelândia',
'IE' => 'Irlanda', 'BE' => 'Bélgica', 'AT' => 'Áustria', 'CZ' => 'República Tcheca', 'HU' => 'Hungria',
'GR' => 'Grécia', 'IL' => 'Israel', 'SG' => 'Singapura', 'TH' => 'Tailândia', 'UA' => 'Ucrânia',
'RO' => 'Romênia', 'BG' => 'Bulgária', 'HR' => 'Croácia', 'SK' => 'Eslováquia', 'SI' => 'Eslovênia',
'EE' => 'Estônia', 'LT' => 'Lituânia', 'LV' => 'Letônia', 'LU' => 'Luxemburgo', 'IS' => 'Islândia',
'MT' => 'Malta', 'CY' => 'Chipre', 'EG' => 'Egito', 'SA' => 'Arábia Saudita', 'AE' => 'Emirados Árabes',
'QA' => 'Catar', 'KW' => 'Kuwait', 'JO' => 'Jordânia', 'LB' => 'Líbano', 'IR' => 'Irã', 'PK' => 'Paquistão',
'ID' => 'Indonésia', 'MY' => 'Malásia', 'PH' => 'Filipinas', 'VN' => 'Vietnã', 'BD' => 'Bangladesh',
'NG' => 'Nigéria', 'KE' => 'Quênia', 'GH' => 'Gana', 'TZ' => 'Tanzânia', 'UG' => 'Uganda', 'MA' => 'Marrocos',
'DZ' => 'Argélia', 'TN' => 'Tunísia', 'SN' => 'Senegal', 'CI' => 'Costa do Marfim', 'CM' => 'Camarões',
'ET' => 'Etiópia', 'SD' => 'Sudão', 'AO' => 'Angola', 'MZ' => 'Moçambique', 'ZW' => 'Zimbábue',
'GH' => 'Gana', 'RW' => 'Ruanda', 'UG' => 'Uganda', 'ZM' => 'Zâmbia', 'MW' => 'Malawi', 'LS' => 'Lesoto',
'SZ' => 'Suazilândia', 'BW' => 'Botsuana', 'NA' => 'Namíbia', 'CD' => 'Congo', 'CG' => 'Congo',
'GA' => 'Gabão', 'GQ' => 'Guiné Equatorial', 'ST' => 'São Tomé e Príncipe', 'CV' => 'Cabo Verde',
'GM' => 'Gâmbia', 'SL' => 'Serra Leoa', 'LR' => 'Libéria', 'BF' => 'Burkina Faso', 'NE' => 'Níger',
'ML' => 'Mali', 'MR' => 'Mauritânia', 'BJ' => 'Benin', 'TG' => 'Togo', 'CI' => 'Costa do Marfim',
'GN' => 'Guiné', 'GW' => 'Guiné-Bissau', 'SR' => 'Suriname', 'GY' => 'Guiana', 'TT' => 'Trinidad e Tobago',
'JM' => 'Jamaica', 'HT' => 'Haiti', 'DO' => 'República Dominicana', 'CU' => 'Cuba', 'BS' => 'Bahamas',
'BB' => 'Barbados', 'AG' => 'Antígua e Barbuda', 'VC' => 'São Vicente e Granadinas', 'LC' => 'Santa Lúcia',
'GD' => 'Granada', 'KN' => 'São Cristóvão e Nevis', 'DM' => 'Dominica', 'FM' => 'Micronésia', 'PW' => 'Palau',
'MH' => 'Ilhas Marshall', 'SB' => 'Ilhas Salomão', 'VU' => 'Vanuatu', 'FJ' => 'Fiji', 'WS' => 'Samoa',
'TO' => 'Tonga', 'TV' => 'Tuvalu', 'KI' => 'Quiribati', 'NR' => 'Nauru', 'PG' => 'Papua Nova Guiné',
'NC' => 'Nova Caledônia', 'PF' => 'Polinésia Francesa', 'FR' => 'França', 'ES' => 'Espanha', 'PT' => 'Portugal',
];
$vars['countries'] = [];
foreach ($isoCountries as $code => $name) {
$vars['countries'][] = ['code' => $code, 'name' => $name];
}

// Carregar países selecionados
$storage = new \WHMCSExpert\Addon\Storage($config->getStorageKey());
$selectedCountries = $storage->get('issue_note_countries');

// Decodifica a string JSON vinda do banco de dados
if (is_string($selectedCountries)) {
$selectedCountries = json_decode($selectedCountries, true);
}

if ($selectedCountries && is_array($selectedCountries)) {
$vars['issue_note_countries'] = $selectedCountries;
} else {
// Seleciona todos por padrão apenas se não existe no banco
$vars['issue_note_countries'] = array_keys($isoCountries);
}


if ($msg->hasMessages()) {
$msg->display();
Expand Down Expand Up @@ -476,6 +528,23 @@ public function configuration($vars)
*/
public function configurationSave($vars)
{
$config = new \NFEioServiceInvoices\Configuration();
$storage = new \WHMCSExpert\Addon\Storage($config->getStorageKey());
$post = isset($_POST) ? $_POST : null;

// Salvar países selecionados
$issue_note_countries = isset($post['issue_note_countries']) ? $post['issue_note_countries'] : [];
if (empty($issue_note_countries)) {
// Se não vier nada, salva todos
$isoCountries = [
'BR','US','AR','DE','FR','IT','JP','CN','GB','CA','MX','ES','PT','RU','IN','AU','ZA','NL','CH','SE','NO','FI','DK','KR','CL','CO','PE','PL','TR','NZ','IE','BE','AT','CZ','HU','GR','IL','SG','TH','UA','RO','BG','HR','SK','SI','EE','LT','LV','LU','IS','MT','CY','EG','SA','AE','QA','KW','JO','LB','IR','PK','ID','MY','PH','VN','BD','NG','KE','GH','TZ','UG','MA','DZ','TN','SN','CI','CM','ET','SD','AO','MZ','ZW','GH','RW','UG','ZM','MW','LS','SZ','BW','NA','CD','CG','GA','GQ','ST','CV','GM','SL','LR','BF','NE','ML','MR','BJ','TG','CI','GN','GW','SR','GY','TT','JM','HT','DO','CU','BS','BB','AG','VC','LC','GD','KN','DM','FM','PW','MH','SB','VU','FJ','WS','TO','TV','KI','NR','PG','NC','PF','FR','ES','PT'
];
$issue_note_countries = $isoCountries;
}
// Garante que o array seja salvo como uma string JSON no banco de dados
$storage->set('issue_note_countries', json_encode($issue_note_countries));



$msg = new FlashMessages();
$assetsURL = Addon::I()->getAssetsURL();
Expand Down
31 changes: 20 additions & 11 deletions modules/addons/NFEioServiceInvoices/lib/Legacy/Functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -97,16 +97,17 @@ public function gnfe_customer($user_id, $client): array
$result['insc_municipal'] = $inscMunicipalCustomFieldValue;
}

if ($cpfIsValid) {
$result['success'] = true;
$result['doc_type'] = 1;
$result['document'] = $cpf;
$result['name'] = $client->firstname . ' ' . $client->lastname;
} elseif ($cnpjIsValid) {
// INVERTEMOS A ORDEM AQUI: CNPJ PRIMEIRO!
if ($cnpjIsValid) {
$result['success'] = true;
$result['doc_type'] = 2;
$result['document'] = $cnpj;
$result['name'] = $client->companyname ? $client->companyname : $client->firstname . ' ' . $client->lastname;
} elseif ($cpfIsValid) {
$result['success'] = true;
$result['doc_type'] = 1;
$result['document'] = $cpf;
$result['name'] = $client->firstname . ' ' . $client->lastname;
} else {
$result['error'] = true;
$result['message'] = 'Documento cadastrado não é um CPF ou CNPJ válido.';
Expand Down Expand Up @@ -159,13 +160,21 @@ function gnfe_issue_nfe($postfields, $companyId)

// Verifica se o webhook existe e é válido, senão cria
$webhook = $webhook_id ? $nfeio->getWebhook($webhook_id) : null;
if (!$webhook || $webhook->hooks->url !== $webhook_url) {

$webhookValido = false;
// Confirma se o retorno é um objeto e tem a URL antes de tentar ler
if (is_object($webhook) && isset($webhook->hooks) && $webhook->hooks->url === $webhook_url) {
$webhookValido = true;
}

if (!$webhookValido) {
$newHook = $nfeio->createWebhook($webhook_url);
if (!$newHook) {
return (object)['message' => 'Erro ao criar novo webhook'];

// Só salva no banco se NÃO for um erro E se o objeto contiver os dados
if ($newHook && !is_array($newHook) && isset($newHook->hooks)) {
$storage->set('webhook_id', (string) $newHook->hooks->id);
$storage->set('webhook_secret', (string) $newHook->hooks->secret);
}
$storage->set('webhook_id', (string) $newHook->hooks->id);
$storage->set('webhook_secret', (string) $newHook->hooks->secret);
}


Expand Down
22 changes: 22 additions & 0 deletions modules/addons/NFEioServiceInvoices/lib/NFEio/Nfe.php
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,28 @@ public function queue($invoiceId, $reissue = false)
$clientId = $clientData[0]['id'];
$clientCompanyId = $clientCompanyRepository->getCompanyByClientId($clientId);

// Validação do país do cliente
$clientCountry = isset($clientData[0]['country']) ? $clientData[0]['country'] : null;
$allowedCountries = $this->storage->get('issue_note_countries');

// Decodifica a string JSON
if (is_string($allowedCountries)) {
$allowedCountries = json_decode($allowedCountries, true);
}

if ($allowedCountries && is_array($allowedCountries)) {
if ($clientCountry && !in_array($clientCountry, $allowedCountries)) {
// País não permitido, não emite nota
logModuleCall('nfeio_serviceinvoices', 'nf_queue_country_block', [
'invoiceId' => $invoiceId,
'clientId' => $clientId,
'clientCountry' => $clientCountry,
'allowedCountries' => $allowedCountries
], 'Nota fiscal não emitida: país do cliente não está entre os permitidos.');
return ['success' => false, 'reason' => 'client_country_not_allowed'];
}
}

// se cliente possuir empresa associada, utiliza a empresa associada, senao usa a empresa padrão
// #163
if ($clientCompanyId) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,19 @@
</div>
</div>
{* /issue_note_after *}
{* issue_note_countries *}
<div class="form-group">
<label class="control-label col-sm-4" for="issue_note_countries">Países para emissão de nota:</label>
<div class="col-sm-8">
<select class="form-control" name="issue_note_countries[]" id="issue_note_countries" multiple>
{foreach from=$countries item=country}
<option value="{$country.code}" {if in_array($country.code, $issue_note_countries)}selected{/if}>{$country.name}</option>
{/foreach}
</select>
<span class="help-block">Selecione os países para os quais a nota fiscal deve ser emitida. Por padrão, todos estão selecionados.</span>
</div>
</div>
{* /issue_note_countries *}
{* cancel_invoice_cancel_nfe *}
<div class="form-group">
<label class="control-label col-sm-4"
Expand Down