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;
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 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
{
@ -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['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;
}

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',
'uses_inclusive_taxes',
'vendor_id',
'e_invoice',
];
protected $casts = [

View File

@ -199,12 +199,12 @@ class Peppol extends AbstractService
$this->p_invoice->DocumentCurrencyCode = $this->invoice->client->currency()->code;
if ($this->invoice->date && $this->invoice->due_date) {
$ip = new InvoicePeriod();
$ip->StartDate = new \DateTime($this->invoice->date);
$ip->EndDate = new \DateTime($this->invoice->due_date);
$this->p_invoice->InvoicePeriod = [$ip];
}
// if ($this->invoice->date && $this->invoice->due_date) {
// $ip = new InvoicePeriod();
// $ip->StartDate = new \DateTime($this->invoice->date);
// $ip->EndDate = new \DateTime($this->invoice->due_date);
// $this->p_invoice->InvoicePeriod = [$ip];
// }
if ($this->invoice->project_id) {
$pr = new \InvoiceNinja\EInvoice\Models\Peppol\ProjectReferenceType\ProjectReference();
@ -254,6 +254,7 @@ class Peppol extends AbstractService
*/
public function decode(mixed $invoice): self
{
$this->p_invoice = $this->e->decode('Peppol', json_encode($invoice), 'json');
return $this;
@ -267,7 +268,7 @@ class Peppol extends AbstractService
private function setInvoice(): self
{
/** 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);
@ -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
$settings = [
'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 Illuminate\Foundation\Testing\DatabaseTransactions;
use InvoiceNinja\EInvoice\Models\FatturaPA\FatturaElettronica;
use App\Services\EDocument\Standards\Validation\Peppol\InvoiceLevel;
use App\Services\EDocument\Standards\Validation\XsltDocumentValidator;
use InvoiceNinja\EInvoice\Models\Peppol\BranchType\FinancialInstitutionBranch;
use InvoiceNinja\EInvoice\Models\Peppol\FinancialAccountType\PayeeFinancialAccount;
@ -162,6 +163,51 @@ class PeppolTest extends TestCase
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()
{
$scenario = [
@ -452,6 +498,19 @@ class PeppolTest extends TestCase
$entity_data = $this->setupTestData($scenario);
$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'];
$settings = $company->settings;
@ -569,6 +628,19 @@ class PeppolTest extends TestCase
$invoice = $data['invoice'];
$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();
$p = new Peppol($invoice);
$p->run();
@ -599,6 +671,19 @@ class PeppolTest extends TestCase
$invoice = $data['invoice'];
$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();
$p = new Peppol($invoice);
$p->run();
@ -830,10 +915,18 @@ class PeppolTest extends TestCase
$stub = new \stdClass();
$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([
'account_id' => $this->account->id,
'settings' => $settings,
'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);
@ -844,7 +937,8 @@ class PeppolTest extends TestCase
$client_settings = ClientSettings::defaults();
$client_settings->currency_id = '3';
$client_settings->enable_e_invoice = true;
$client = Client::factory()->create([
'company_id' => $company->id,
'user_id' => $this->user->id,
@ -857,6 +951,7 @@ class PeppolTest extends TestCase
'country_id' => 276,
'routing_id' => 'ABC1234',
'settings' => $client_settings,
'is_tax_exempt' => false,
]);
@ -869,6 +964,7 @@ class PeppolTest extends TestCase
$item->is_amount_discount = false;
$item->tax_rate1 = 19;
$item->tax_name1 = 'mwst';
$item->tax_id = '1';
$invoice = Invoice::factory()->create([
'company_id' => $company->id,
@ -880,19 +976,23 @@ class PeppolTest extends TestCase
'tax_rate1' => 0,
'tax_name1' => '',
'tax_rate2' => 0,
'tax_rate3' => 0,
'tax_name2' => '',
'tax_rate3' => 0,
'tax_name3' => '',
'line_items' => [$item],
'number' => 'DE-'.rand(1000, 100000),
'date' => now()->format('Y-m-d'),
'is_amount_discount' => false,
'custom_surcharge1' => 10,
]);
$invoice->custom_surcharge1 = 10;
$invoice = $invoice->calc()->getInvoice();
$invoice->service()->markSent()->save();
nlog($invoice->toArray());
$this->assertEquals(130.90, $invoice->amount);
$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->user_id = $this->invoice->user_id;
$obj->company_id = $this->company->id;
$obj->activity_type_id = \App\Models\Activity::EMAIL_INVOICE;
$activity_repo->save($obj, $this->invoice, Ninja::eventVars());
}