diff --git a/app/Http/Requests/Invoice/UpdateInvoiceRequest.php b/app/Http/Requests/Invoice/UpdateInvoiceRequest.php index 4055891cd0..315d921610 100644 --- a/app/Http/Requests/Invoice/UpdateInvoiceRequest.php +++ b/app/Http/Requests/Invoice/UpdateInvoiceRequest.php @@ -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; } diff --git a/app/Http/ValidationRules/EInvoice/ValidInvoiceScheme.php b/app/Http/ValidationRules/EInvoice/ValidInvoiceScheme.php new file mode 100644 index 0000000000..12fa6f71b3 --- /dev/null +++ b/app/Http/ValidationRules/EInvoice/ValidInvoiceScheme.php @@ -0,0 +1,65 @@ +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; + } + + +} diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index 41fd525aac..2b4d17cd33 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -197,6 +197,7 @@ class Invoice extends BaseModel 'auto_bill_enabled', 'uses_inclusive_taxes', 'vendor_id', + 'e_invoice', ]; protected $casts = [ diff --git a/app/Services/EDocument/Standards/Peppol.php b/app/Services/EDocument/Standards/Peppol.php index f2d3bb3fbe..3db1c0fef8 100644 --- a/app/Services/EDocument/Standards/Peppol.php +++ b/app/Services/EDocument/Standards/Peppol.php @@ -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, diff --git a/app/Services/EDocument/Standards/Validation/Peppol/InvoiceLevel.php b/app/Services/EDocument/Standards/Validation/Peppol/InvoiceLevel.php new file mode 100644 index 0000000000..43a971c35e --- /dev/null +++ b/app/Services/EDocument/Standards/Validation/Peppol/InvoiceLevel.php @@ -0,0 +1,25 @@ + '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); diff --git a/tests/Feature/EInvoice/RequestValidation/InvoicePeriodTest.php b/tests/Feature/EInvoice/RequestValidation/InvoicePeriodTest.php new file mode 100644 index 0000000000..dd4833d05d --- /dev/null +++ b/tests/Feature/EInvoice/RequestValidation/InvoicePeriodTest.php @@ -0,0 +1,80 @@ +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); + + + } +} diff --git a/tests/Integration/DownloadHistoricalInvoiceTest.php b/tests/Integration/DownloadHistoricalInvoiceTest.php index 55697c6a01..2dd28404d6 100644 --- a/tests/Integration/DownloadHistoricalInvoiceTest.php +++ b/tests/Integration/DownloadHistoricalInvoiceTest.php @@ -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()); }