Tests for einvoice validation at invoice level

This commit is contained in:
David Bomba 2025-02-16 18:05:30 +11:00
parent 540e40348c
commit 6215fca188
8 changed files with 295 additions and 15 deletions

View File

@ -12,12 +12,13 @@
namespace App\Http\Requests\Invoice; namespace App\Http\Requests\Invoice;
use App\Http\Requests\Request; use App\Http\Requests\Request;
use App\Http\ValidationRules\Invoice\LockedInvoiceRule;
use App\Http\ValidationRules\Project\ValidProjectForClient;
use App\Utils\Traits\ChecksEntityStatus;
use App\Utils\Traits\CleanLineItems;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use Illuminate\Validation\Rule; use Illuminate\Validation\Rule;
use App\Utils\Traits\CleanLineItems;
use App\Utils\Traits\ChecksEntityStatus;
use App\Http\ValidationRules\Invoice\LockedInvoiceRule;
use App\Http\ValidationRules\EInvoice\ValidInvoiceScheme;
use App\Http\ValidationRules\Project\ValidProjectForClient;
class UpdateInvoiceRequest extends Request class UpdateInvoiceRequest extends Request
{ {
@ -94,6 +95,7 @@ class UpdateInvoiceRequest extends Request
$rules['partial_due_date'] = ['bail', 'sometimes', 'nullable', 'exclude_if:partial,0', 'date', 'before:due_date', 'after_or_equal:date']; $rules['partial_due_date'] = ['bail', 'sometimes', 'nullable', 'exclude_if:partial,0', 'date', 'before:due_date', 'after_or_equal:date'];
$rules['due_date'] = ['bail', 'sometimes', 'nullable', 'after:partial_due_date', 'after_or_equal:date', Rule::requiredIf(fn () => strlen($this->partial_due_date) > 1), 'date']; $rules['due_date'] = ['bail', 'sometimes', 'nullable', 'after:partial_due_date', 'after_or_equal:date', Rule::requiredIf(fn () => strlen($this->partial_due_date) > 1), 'date'];
$rules['e_invoice'] = ['sometimes', 'nullable', new ValidInvoiceScheme()];
return $rules; return $rules;
} }

View File

@ -0,0 +1,65 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*1`
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\ValidationRules\EInvoice;
use App\Services\EDocument\Standards\Validation\Peppol\InvoiceLevel;
use Closure;
use InvoiceNinja\EInvoice\EInvoice;
use Illuminate\Validation\Validator;
use InvoiceNinja\EInvoice\Models\Peppol\Invoice;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Contracts\Validation\ValidatorAwareRule;
/**
* Class ValidScheme.
*/
class ValidInvoiceScheme implements ValidationRule, ValidatorAwareRule
{
/**
* The validator instance.
*
* @var Validator
*/
protected $validator;
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if (isset($value['Invoice'])) {
$r = new EInvoice();
$errors = $r->validateRequest($value['Invoice'], InvoiceLevel::class);
foreach ($errors as $key => $msg) {
$this->validator->errors()->add(
"e_invoice.{$key}",
"{$key} - {$msg}"
);
}
}
}
/**
* Set the current validator.
*/
public function setValidator(Validator $validator): static
{
$this->validator = $validator;
return $this;
}
}

View File

@ -197,6 +197,7 @@ class Invoice extends BaseModel
'auto_bill_enabled', 'auto_bill_enabled',
'uses_inclusive_taxes', 'uses_inclusive_taxes',
'vendor_id', 'vendor_id',
'e_invoice',
]; ];
protected $casts = [ protected $casts = [

View File

@ -199,12 +199,12 @@ class Peppol extends AbstractService
$this->p_invoice->DocumentCurrencyCode = $this->invoice->client->currency()->code; $this->p_invoice->DocumentCurrencyCode = $this->invoice->client->currency()->code;
if ($this->invoice->date && $this->invoice->due_date) { // if ($this->invoice->date && $this->invoice->due_date) {
$ip = new InvoicePeriod(); // $ip = new InvoicePeriod();
$ip->StartDate = new \DateTime($this->invoice->date); // $ip->StartDate = new \DateTime($this->invoice->date);
$ip->EndDate = new \DateTime($this->invoice->due_date); // $ip->EndDate = new \DateTime($this->invoice->due_date);
$this->p_invoice->InvoicePeriod = [$ip]; // $this->p_invoice->InvoicePeriod = [$ip];
} // }
if ($this->invoice->project_id) { if ($this->invoice->project_id) {
$pr = new \InvoiceNinja\EInvoice\Models\Peppol\ProjectReferenceType\ProjectReference(); $pr = new \InvoiceNinja\EInvoice\Models\Peppol\ProjectReferenceType\ProjectReference();
@ -254,6 +254,7 @@ class Peppol extends AbstractService
*/ */
public function decode(mixed $invoice): self public function decode(mixed $invoice): self
{ {
$this->p_invoice = $this->e->decode('Peppol', json_encode($invoice), 'json'); $this->p_invoice = $this->e->decode('Peppol', json_encode($invoice), 'json');
return $this; return $this;
@ -267,7 +268,7 @@ class Peppol extends AbstractService
private function setInvoice(): self private function setInvoice(): self
{ {
/** Handle Existing Document */ /** Handle Existing Document */
if ($this->invoice->e_invoice && isset($this->invoice->e_invoice->Invoice)) { if ($this->invoice->e_invoice && isset($this->invoice->e_invoice->Invoice) && isset($this->invoice->e_invoice->Invoice->ID)) {
$this->decode($this->invoice->e_invoice->Invoice); $this->decode($this->invoice->e_invoice->Invoice);
@ -1256,6 +1257,12 @@ class Peppol extends AbstractService
} }
} }
if(isset($this->invoice->e_invoice->Invoice)) {
foreach(get_object_vars($this->invoice->e_invoice->Invoice) as $prop => $value) {
$this->p_invoice->{$prop} = $value;
}
}
// Plucks special overriding properties scanning the correct settings level // Plucks special overriding properties scanning the correct settings level
$settings = [ $settings = [
'AccountingCostCode' => 7, 'AccountingCostCode' => 7,

View File

@ -0,0 +1,25 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Services\EDocument\Standards\Validation\Peppol;
use Symfony\Component\Serializer\Attribute\SerializedName;
use InvoiceNinja\EInvoice\Models\Peppol\PeriodType\InvoicePeriod;
class InvoiceLevel
{
/** @var InvoicePeriod[] */
#[SerializedName('cac:InvoicePeriod')]
public array $InvoicePeriod;
}

View File

@ -32,6 +32,7 @@ use InvoiceNinja\EInvoice\Models\Peppol\PaymentMeans;
use App\Services\EDocument\Gateway\Storecove\Storecove; use App\Services\EDocument\Gateway\Storecove\Storecove;
use Illuminate\Foundation\Testing\DatabaseTransactions; use Illuminate\Foundation\Testing\DatabaseTransactions;
use InvoiceNinja\EInvoice\Models\FatturaPA\FatturaElettronica; use InvoiceNinja\EInvoice\Models\FatturaPA\FatturaElettronica;
use App\Services\EDocument\Standards\Validation\Peppol\InvoiceLevel;
use App\Services\EDocument\Standards\Validation\XsltDocumentValidator; use App\Services\EDocument\Standards\Validation\XsltDocumentValidator;
use InvoiceNinja\EInvoice\Models\Peppol\BranchType\FinancialInstitutionBranch; use InvoiceNinja\EInvoice\Models\Peppol\BranchType\FinancialInstitutionBranch;
use InvoiceNinja\EInvoice\Models\Peppol\FinancialAccountType\PayeeFinancialAccount; use InvoiceNinja\EInvoice\Models\Peppol\FinancialAccountType\PayeeFinancialAccount;
@ -162,6 +163,51 @@ class PeppolTest extends TestCase
return compact('company', 'client', 'invoice'); return compact('company', 'client', 'invoice');
} }
public function testInvoicePeriodValidation()
{
$scenario = [
'company_vat' => 'DE923356489',
'company_country' => 'DE',
'client_country' => 'DE',
'client_vat' => 'DE923256489',
'client_id_number' => '123456789',
'classification' => 'business',
'has_valid_vat' => true,
'over_threshold' => true,
'legal_entity_id' => 290868,
'is_tax_exempt' => false,
];
$entity_data = $this->setupTestData($scenario);
$invoice = $entity_data['invoice'];
$data = $invoice->toArray();
$data['e_invoice'] = [
'Invoice' => [
'InvoicePeriod' => [
[
'StartDate' => '-01',
'EndDate' => 'boop',
'Description' => 'Mustafa',
'HelterSkelter' => 'sif'
]
]
]
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->putJson('/api/v1/invoices/'.$invoice->hashed_id, $data);
$response->assertStatus(422);
}
public function testInvoiceValidationWithSmallDiscount() public function testInvoiceValidationWithSmallDiscount()
{ {
$scenario = [ $scenario = [
@ -452,6 +498,19 @@ class PeppolTest extends TestCase
$entity_data = $this->setupTestData($scenario); $entity_data = $this->setupTestData($scenario);
$invoice = $entity_data['invoice']; $invoice = $entity_data['invoice'];
$invoice->e_invoice = [
'Invoice' => [
'InvoicePeriod' => [
[
'cbc:StartDate' => $invoice->date,
'cbc:EndDate' => $invoice->due_date ?? $invoice->date,
]
]
]
];
$invoice->save();
$company = $entity_data['company']; $company = $entity_data['company'];
$settings = $company->settings; $settings = $company->settings;
@ -569,6 +628,19 @@ class PeppolTest extends TestCase
$invoice = $data['invoice']; $invoice = $data['invoice'];
$invoice = $invoice->calc()->getInvoice(); $invoice = $invoice->calc()->getInvoice();
$invoice->e_invoice = [
'Invoice' => [
'InvoicePeriod' => [
[
'cbc:StartDate' => $invoice->date,
'cbc:EndDate' => $invoice->due_date ?? $invoice->date,
]
]
]
];
$invoice->save();
$storecove = new Storecove(); $storecove = new Storecove();
$p = new Peppol($invoice); $p = new Peppol($invoice);
$p->run(); $p->run();
@ -599,6 +671,19 @@ class PeppolTest extends TestCase
$invoice = $data['invoice']; $invoice = $data['invoice'];
$invoice = $invoice->calc()->getInvoice(); $invoice = $invoice->calc()->getInvoice();
$invoice->e_invoice = [
'Invoice' => [
'InvoicePeriod' => [
[
'cbc:StartDate' => $invoice->date,
'cbc:EndDate' => $invoice->due_date ?? $invoice->date,
]
]
]
];
$invoice->save();
$storecove = new Storecove(); $storecove = new Storecove();
$p = new Peppol($invoice); $p = new Peppol($invoice);
$p->run(); $p->run();
@ -830,10 +915,18 @@ class PeppolTest extends TestCase
$stub = new \stdClass(); $stub = new \stdClass();
$stub->Invoice = $einvoice; $stub->Invoice = $einvoice;
$tax_data = new TaxModel();
$tax_data->regions->EU->has_sales_above_threshold = true;
$tax_data->regions->EU->tax_all_subregions = true;
$tax_data->seller_subregion = 'DE';
$company = Company::factory()->create([ $company = Company::factory()->create([
'account_id' => $this->account->id, 'account_id' => $this->account->id,
'settings' => $settings, 'settings' => $settings,
'e_invoice' => $stub, 'e_invoice' => $stub,
'calculate_taxes' => true,
'tax_data' => $tax_data,
'legal_entity_id' => 290868,
]); ]);
$cu = CompanyUserFactory::create($this->user->id, $company->id, $this->account->id); $cu = CompanyUserFactory::create($this->user->id, $company->id, $this->account->id);
@ -844,6 +937,7 @@ class PeppolTest extends TestCase
$client_settings = ClientSettings::defaults(); $client_settings = ClientSettings::defaults();
$client_settings->currency_id = '3'; $client_settings->currency_id = '3';
$client_settings->enable_e_invoice = true;
$client = Client::factory()->create([ $client = Client::factory()->create([
'company_id' => $company->id, 'company_id' => $company->id,
@ -857,6 +951,7 @@ class PeppolTest extends TestCase
'country_id' => 276, 'country_id' => 276,
'routing_id' => 'ABC1234', 'routing_id' => 'ABC1234',
'settings' => $client_settings, 'settings' => $client_settings,
'is_tax_exempt' => false,
]); ]);
@ -869,6 +964,7 @@ class PeppolTest extends TestCase
$item->is_amount_discount = false; $item->is_amount_discount = false;
$item->tax_rate1 = 19; $item->tax_rate1 = 19;
$item->tax_name1 = 'mwst'; $item->tax_name1 = 'mwst';
$item->tax_id = '1';
$invoice = Invoice::factory()->create([ $invoice = Invoice::factory()->create([
'company_id' => $company->id, 'company_id' => $company->id,
@ -880,19 +976,23 @@ class PeppolTest extends TestCase
'tax_rate1' => 0, 'tax_rate1' => 0,
'tax_name1' => '', 'tax_name1' => '',
'tax_rate2' => 0, 'tax_rate2' => 0,
'tax_rate3' => 0,
'tax_name2' => '', 'tax_name2' => '',
'tax_rate3' => 0,
'tax_name3' => '', 'tax_name3' => '',
'line_items' => [$item], 'line_items' => [$item],
'number' => 'DE-'.rand(1000, 100000), 'number' => 'DE-'.rand(1000, 100000),
'date' => now()->format('Y-m-d'), 'date' => now()->format('Y-m-d'),
'is_amount_discount' => false, 'is_amount_discount' => false,
'custom_surcharge1' => 10,
]); ]);
$invoice->custom_surcharge1 = 10;
$invoice = $invoice->calc()->getInvoice(); $invoice = $invoice->calc()->getInvoice();
$invoice->service()->markSent()->save(); $invoice->service()->markSent()->save();
nlog($invoice->toArray());
$this->assertEquals(130.90, $invoice->amount); $this->assertEquals(130.90, $invoice->amount);
$peppol = new Peppol($invoice); $peppol = new Peppol($invoice);

View File

@ -0,0 +1,80 @@
<?php
namespace Tests\Feature\EInvoice\RequestValidation;
use Tests\TestCase;
use Illuminate\Support\Facades\Validator;
use App\Http\Requests\Invoice\UpdateInvoiceRequest;
use Illuminate\Routing\Middleware\ThrottleRequests;
use Tests\MockAccountData;
class InvoicePeriodTest extends TestCase
{
use MockAccountData;
protected UpdateInvoiceRequest $request;
protected function setUp(): void
{
parent::setUp();
$this->withoutMiddleware(
ThrottleRequests::class
);
$this->makeTestData();
}
public function testEInvoicePeriodValidationPasses()
{
$data = $this->invoice->toArray();
$data['e_invoice'] = [
'Invoice' => [
'InvoicePeriod' => [
'StartDate' => '2025-01-01',
'EndDate' => '2025-01-01',
]
]
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->putJson('/api/v1/invoices/'.$this->invoice->hashed_id, $data);
$response->assertStatus(200);
$arr = $response->json();
}
public function testEInvoicePeriodValidationFails()
{
$data = $this->invoice->toArray();
$data['e_invoice'] = [
'Invoice' => [
'InvoicePeriod' => [
'notarealvar' => '2025-01-01',
'worseVar' => '2025-01-01',
'Description' => 'Mustafa'
]
]
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->putJson('/api/v1/invoices/'.$this->invoice->hashed_id, $data);
$arr = $response->json();
nlog($arr);
$response->assertStatus(422);
}
}

View File

@ -119,7 +119,7 @@ class DownloadHistoricalInvoiceTest extends TestCase
$obj->invoice_id = $this->invoice->id; $obj->invoice_id = $this->invoice->id;
$obj->user_id = $this->invoice->user_id; $obj->user_id = $this->invoice->user_id;
$obj->company_id = $this->company->id; $obj->company_id = $this->company->id;
$obj->activity_type_id = \App\Models\Activity::EMAIL_INVOICE;
$activity_repo->save($obj, $this->invoice, Ninja::eventVars()); $activity_repo->save($obj, $this->invoice, Ninja::eventVars());
} }