diff --git a/VERSION.txt b/VERSION.txt index cc6fcd0425..574a01bf27 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -5.10.46 \ No newline at end of file +5.10.47 \ No newline at end of file diff --git a/app/DataMapper/Tax/BaseRule.php b/app/DataMapper/Tax/BaseRule.php index b0e6011440..862c7c03a5 100644 --- a/app/DataMapper/Tax/BaseRule.php +++ b/app/DataMapper/Tax/BaseRule.php @@ -316,7 +316,8 @@ class BaseRule implements RuleInterface return $this; - } elseif($this->client_region == 'AU') { //these are defaults and are only stubbed out for now, for AU we can actually remove these + } + elseif($this->client_region == 'AU') { //these are defaults and are only stubbed out for now, for AU we can actually remove these $this->tax_rate1 = $this->client->company->tax_data->regions->AU->subregions->AU->tax_rate; $this->tax_name1 = $this->client->company->tax_data->regions->AU->subregions->AU->tax_name; @@ -346,6 +347,10 @@ class BaseRule implements RuleInterface $this->tax_rate1 = $this->client->company->tax_data->regions->{$this->client_region}->subregions->{$company_country_code}->tax_rate; $this->tax_name1 = $this->client->company->tax_data->regions->{$this->client_region}->subregions->{$company_country_code}->tax_name; } + elseif($is_over_threshold){ + $this->tax_rate1 = $this->client->company->tax_data->regions->{$this->client_region}->subregions->{$this->client_subregion}->tax_rate; + $this->tax_name1 = $this->client->company->tax_data->regions->{$this->client_region}->subregions->{$this->client_subregion}->tax_name; + } } else { $this->tax_rate1 = $this->client->company->tax_data->regions->{$this->client_region}->subregions->{$this->client_subregion}->tax_rate; diff --git a/app/Http/Controllers/Bank/NordigenController.php b/app/Http/Controllers/Bank/NordigenController.php index 5e6764ecb0..d02a3cea87 100644 --- a/app/Http/Controllers/Bank/NordigenController.php +++ b/app/Http/Controllers/Bank/NordigenController.php @@ -218,6 +218,9 @@ class NordigenController extends BaseController $nordigen_account = $nordigen->getAccount($nordigenAccountId); + if(!$nordigen_account) + continue; + $existing_bank_integration = BankIntegration::withTrashed()->where('nordigen_account_id', $nordigen_account['id'])->where('company_id', $company->id)->where('is_deleted', 0)->first(); if (!$existing_bank_integration) { diff --git a/app/Http/Controllers/EInvoiceController.php b/app/Http/Controllers/EInvoiceController.php index 634a8475dc..4fa623eae4 100644 --- a/app/Http/Controllers/EInvoiceController.php +++ b/app/Http/Controllers/EInvoiceController.php @@ -127,15 +127,15 @@ class EInvoiceController extends BaseController * @var \App\Models\Company */ $company = auth()->user()->company(); - + $response = \Illuminate\Support\Facades\Http::baseUrl(config('ninja.hosted_ninja_url')) ->withHeaders([ 'Content-Type' => 'application/json', 'Accept' => 'application/json', + 'X-EInvoice-Token' => $company->account->e_invoicing_token, ]) ->post('/api/einvoice/quota', data: [ 'license_key' => config('ninja.license_key'), - 'e_invoicing_token' => $company->account->e_invoicing_token, 'account_key' => $company->account->key, ]); diff --git a/app/Http/Controllers/EInvoicePeppolController.php b/app/Http/Controllers/EInvoicePeppolController.php index 8e704b4182..f74a42e9a3 100644 --- a/app/Http/Controllers/EInvoicePeppolController.php +++ b/app/Http/Controllers/EInvoicePeppolController.php @@ -11,6 +11,7 @@ namespace App\Http\Controllers; +use App\Utils\Ninja; use Http; use Illuminate\Http\JsonResponse; use Illuminate\Http\Response; @@ -79,14 +80,23 @@ class EInvoicePeppolController extends BaseController { $company = auth()->user()->company(); + $headers = [ + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + ]; + + if (Ninja::isSelfHost()) { + $headers['X-EInvoice-Token'] = $company->account->e_invoicing_token; + } + + if (Ninja::isHosted()) { + $headers['X-EInvoice-Secret'] = config('ninja.hosted_einvoice_secret'); + } + $response = Http::baseUrl(config('ninja.hosted_ninja_url')) - ->withHeaders([ - 'Content-Type' => 'application/json', - 'Accept' => 'application/json', - ]) + ->withHeaders($headers) ->post('/api/einvoice/peppol/legal_entity', data: [ 'legal_entity_id' => $company->legal_entity_id, - 'e_invoicing_token' => $company->account->e_invoicing_token, ]); return response()->json($response->json(), 200); @@ -107,17 +117,26 @@ class EInvoicePeppolController extends BaseController */ $company = auth()->user()->company(); + $headers = [ + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + ]; + + if (Ninja::isSelfHost()) { + $headers['X-EInvoice-Token'] = $company->account->e_invoicing_token; + } + + if (Ninja::isHosted()) { + $headers['X-EInvoice-Secret'] = config('ninja.hosted_einvoice_secret'); + } + $response = Http::baseUrl(config('ninja.hosted_ninja_url')) - ->withHeaders([ - 'Content-Type' => 'application/json', - 'Accept' => 'application/json', - ]) + ->withHeaders($headers) ->post('/api/einvoice/peppol/setup', data: [ ...$request->validated(), 'classification' => $request->classification ?? $company->settings->classification, 'vat_number' => $request->vat_number ?? $company->settings->vat_number, 'id_number' => $request->id_number ?? $company->settings->id_number, - 'e_invoicing_token' => $company->account->e_invoicing_token, ]); if ($response->successful()) { @@ -169,15 +188,24 @@ class EInvoicePeppolController extends BaseController { $company = auth()->user()->company(); + $headers = [ + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + ]; + + if (Ninja::isSelfHost()) { + $headers['X-EInvoice-Token'] = $company->account->e_invoicing_token; + } + + if (Ninja::isHosted()) { + $headers['X-EInvoice-Secret'] = config('ninja.hosted_einvoice_secret'); + } + $response = Http::baseUrl(config('ninja.hosted_ninja_url')) - ->withHeaders([ - 'Content-Type' => 'application/json', - 'Accept' => 'application/json', - ]) + ->withHeaders($headers) ->put('/api/einvoice/peppol/update', data: [ ...$request->validated(), 'legal_entity_id' => $company->legal_entity_id, - 'e_invoicing_token' => $company->account->e_invoicing_token, ]); if ($response->successful()) { @@ -209,15 +237,26 @@ class EInvoicePeppolController extends BaseController */ $company = auth()->user()->company(); + $headers = [ + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + ]; + + if (Ninja::isSelfHost()) { + $headers['X-EInvoice-Token'] = $company->account->e_invoicing_token; + } + + if (Ninja::isHosted()) { + $headers['X-EInvoice-Secret'] = config('ninja.hosted_einvoice_secret'); + } + + nlog($headers); + $response = Http::baseUrl(config('ninja.hosted_ninja_url')) - ->withHeaders([ - 'Content-Type' => 'application/json', - 'Accept' => 'application/json', - ]) + ->withHeaders($headers) ->post('/api/einvoice/peppol/disconnect', data: [ 'company_key' => $company->company_key, 'legal_entity_id' => $company->legal_entity_id, - 'e_invoicing_token' => $company->account->e_invoicing_token, ]); if ($response->successful()) { @@ -234,6 +273,8 @@ class EInvoicePeppolController extends BaseController return response()->noContent(); } + nlog($response->status()); + return response()->noContent(status: 500); } diff --git a/app/Livewire/TasksTable.php b/app/Livewire/TasksTable.php index d3d68faecd..34954ef060 100644 --- a/app/Livewire/TasksTable.php +++ b/app/Livewire/TasksTable.php @@ -54,7 +54,7 @@ class TasksTable extends Component return render('components.livewire.tasks-table', [ 'tasks' => $query, - 'show_item_description' => auth()->guard('contact')->user()->company->invoice_task_item_description ?? false, + 'show_item_description' => auth()->guard('contact')->user()->client->getSetting("show_task_item_description"), ]); } } diff --git a/app/Services/EDocument/Gateway/Storecove/StorecoveAdapter.php b/app/Services/EDocument/Gateway/Storecove/StorecoveAdapter.php index dd061b0bd8..f5e06c1153 100644 --- a/app/Services/EDocument/Gateway/Storecove/StorecoveAdapter.php +++ b/app/Services/EDocument/Gateway/Storecove/StorecoveAdapter.php @@ -118,16 +118,13 @@ class StorecoveAdapter public function transform($invoice): self { $this->ninja_invoice = $invoice; - $serializer = $this->getSerializer(); - /** Currently - due to class structures, the serialization process goes like this: * * e-invoice => Peppol -> XML -> Peppol Decoded -> encode to Peppol -> deserialize to Storecove */ $p = (new Peppol($invoice))->run()->toXml(); - $context = [ DateTimeNormalizer::FORMAT_KEY => 'Y-m-d', AbstractObjectNormalizer::SKIP_NULL_VALUES => true, @@ -135,13 +132,12 @@ class StorecoveAdapter $e = new \InvoiceNinja\EInvoice\EInvoice(); $peppolInvoice = $e->decode('Peppol', $p, 'xml'); - $parent = \App\Services\EDocument\Gateway\Storecove\Models\Invoice::class; $peppolInvoice = $e->encode($peppolInvoice, 'json'); $this->storecove_invoice = $serializer->deserialize($peppolInvoice, $parent, 'json', $context); $this->buildNexus(); - + return $this; } @@ -395,6 +391,8 @@ class StorecoveAdapter // B2C under threshold - origin country VAT $this->nexus = $company_country_code; } + } elseif ($is_over_threshold && !in_array($company_country_code, $eu_countries)){ + $this->nexus = $client_country_code; } else { nlog("B2B with valid vat"); // B2B with valid VAT - origin country @@ -422,7 +420,7 @@ class StorecoveAdapter private function setupDestinationVAT($client_country_code):self { - nlog("configuring destination tax"); + $this->storecove_invoice->setConsumerTaxMode(true); $id = $this->ninja_invoice->company->tax_data->regions->EU->subregions->{$client_country_code}->vat_number; $scheme = $this->storecove->router->setInvoice($this->ninja_invoice)->resolveTaxScheme($client_country_code, $this->ninja_invoice->client->classification ?? 'individual'); diff --git a/app/Services/EDocument/Standards/Peppol.php b/app/Services/EDocument/Standards/Peppol.php index 1274518ea0..e643617451 100644 --- a/app/Services/EDocument/Standards/Peppol.php +++ b/app/Services/EDocument/Standards/Peppol.php @@ -944,7 +944,7 @@ class Peppol extends AbstractService $this->globalTaxCategories[] = $taxCategory; }); - + return $this; } @@ -1379,7 +1379,7 @@ class Peppol extends AbstractService public function getJurisdiction() { - + //calculate nexus $country_code = $this->company->country()->iso_3166_2; $br = new \App\DataMapper\Tax\BaseRule(); @@ -1390,10 +1390,11 @@ class Peppol extends AbstractService $country_code = $this->company->country()->iso_3166_2; } elseif(in_array($country_code, $eu_countries) && !in_array($this->invoice->client->country->iso_3166_2, $eu_countries)){ - //NON-EU sale + //EU => FOREIGN sale } - elseif(in_array($country_code, $eu_countries) && in_array($this->invoice->client->country->iso_3166_2, $eu_countries)){ - //EU Sale + elseif(in_array($this->invoice->client->country->iso_3166_2, $eu_countries)){ + // elseif(in_array($country_code, $eu_countries) && in_array($this->invoice->client->country->iso_3166_2, $eu_countries)){ + // EU Sale if((isset($this->company->tax_data->regions->EU->has_sales_above_threshold) && $this->company->tax_data->regions->EU->has_sales_above_threshold) || !$this->invoice->client->has_valid_vat_number){ //over threshold - tax in buyer country $country_code = $this->invoice->client->country->iso_3166_2; @@ -1424,12 +1425,12 @@ class Peppol extends AbstractService $eu_countries = $br->eu_country_codes; // If company is in EU, standardize to VAT - if (in_array($this->company->country()->iso_3166_2, $eu_countries)) { + // if (in_array($this->company->country()->iso_3166_2, $eu_countries)) { return "VAT"; - } + // } // For non-EU countries, return original or handle specifically - return $this->standardizeTaxSchemeId($tax_name); + // return $this->standardizeTaxSchemeId($tax_name); } /** diff --git a/config/ninja.php b/config/ninja.php index 6c42bf9496..e4b8de7b40 100644 --- a/config/ninja.php +++ b/config/ninja.php @@ -17,8 +17,8 @@ return [ 'require_https' => env('REQUIRE_HTTPS', true), 'app_url' => rtrim(env('APP_URL', ''), '/'), 'app_domain' => env('APP_DOMAIN', 'invoicing.co'), - 'app_version' => env('APP_VERSION', '5.10.46'), - 'app_tag' => env('APP_TAG', '5.10.46'), + 'app_version' => env('APP_VERSION', '5.10.47'), + 'app_tag' => env('APP_TAG', '5.10.47'), 'minimum_client_version' => '5.0.16', 'terms_version' => '1.0.1', 'api_secret' => env('API_SECRET', false), @@ -258,5 +258,5 @@ return [ 'qvalia_partner_number' => env('QVALIA_PARTNER_NUMBER', false), 'pdf_page_numbering_x_alignment' => env('PDF_PAGE_NUMBER_X', 0), 'pdf_page_numbering_y_alignment' => env('PDF_PAGE_NUMBER_Y', -6), - + 'hosted_einvoice_secret' => env('HOSTED_EINVOICE_SECRET', null), ]; diff --git a/tests/Unit/Tax/TaxRuleConsistencyTest.php b/tests/Unit/Tax/TaxRuleConsistencyTest.php index 84b9a37d3e..28a575766b 100644 --- a/tests/Unit/Tax/TaxRuleConsistencyTest.php +++ b/tests/Unit/Tax/TaxRuleConsistencyTest.php @@ -51,10 +51,11 @@ class TaxRuleConsistencyTest extends TestCase private function setupTestData(array $params = []): array { - + $company_iso = isset($params['company_country']) ? $params['company_country'] : 'DE'; + $settings = CompanySettings::defaults(); $settings->vat_number = $params['company_vat'] ?? 'DE123456789'; - $settings->country_id = Country::where('iso_3166_2', 'DE')->first()->id; + $settings->country_id = (string)Country::where('iso_3166_2', $company_iso)->first()->id; $settings->email = $this->faker->safeEmail(); $tax_data = new TaxModel(); @@ -137,7 +138,7 @@ class TaxRuleConsistencyTest extends TestCase 'expected_rate' => 19, // Should use German VAT 'expected_nexus' => 'DE', ], - 'B2B Transaction' => [ + 'B2B Transaction DE FR' => [ 'params' => [ 'company_country' => 'DE', 'client_country' => 'FR', @@ -150,6 +151,19 @@ class TaxRuleConsistencyTest extends TestCase 'expected_rate' => 19, // Should use German VAT 'expected_nexus' => 'DE', ], + 'B2B Transaction US DK' => [ + 'params' => [ + 'company_country' => 'US', + 'client_country' => 'DK', + 'company_vat' => 'US123456789', + 'client_vat' => 'DK123456789', + 'classification' => 'business', + 'has_valid_vat' => true, + 'over_threshold' => true, + ], + 'expected_rate' => 25, // Should use DK VAT + 'expected_nexus' => 'DK', + ], ]; foreach ($scenarios as $name => $scenario) { @@ -163,10 +177,10 @@ class TaxRuleConsistencyTest extends TestCase // Test StorecoveAdapter $storecove = new Storecove(); $storecove->build($data['invoice']); - + $this->assertEquals( $scenario['expected_rate'], - $baseRule->tax_rate1 + $baseRule->tax_rate1, "{$name} {$scenario['expected_nexus']}" ); $this->assertEquals(