From c7e79fe6733ee771884e3ed1580d2afa7f45a177 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 12 Aug 2025 13:41:11 +1000 Subject: [PATCH] Refactor to use generate parent/child ids --- .../Requests/Invoice/StoreInvoiceRequest.php | 7 +- app/Services/Invoice/HandleCancellation.php | 14 +- .../EInvoice/Verifactu/VerifactuApiTest.php | 301 +++++++++++++++++- 3 files changed, 316 insertions(+), 6 deletions(-) diff --git a/app/Http/Requests/Invoice/StoreInvoiceRequest.php b/app/Http/Requests/Invoice/StoreInvoiceRequest.php index f0b2be079f..b44c33e2d2 100644 --- a/app/Http/Requests/Invoice/StoreInvoiceRequest.php +++ b/app/Http/Requests/Invoice/StoreInvoiceRequest.php @@ -18,6 +18,7 @@ use Illuminate\Validation\Rule; use App\Utils\Traits\CleanLineItems; use App\Http\ValidationRules\Project\ValidProjectForClient; use App\Http\ValidationRules\Invoice\CanGenerateModificationInvoice; +use App\Http\ValidationRules\Invoice\VerifactuAmountCheck; class StoreInvoiceRequest extends Request { @@ -45,7 +46,7 @@ class StoreInvoiceRequest extends Request $rules = []; - $rules['client_id'] = ['required', 'bail', Rule::exists('clients', 'id')->where('company_id', $user->company()->id)->where('is_deleted', 0)]; + $rules['client_id'] = ['required', 'bail', new VerifactuAmountCheck($this->all()) , Rule::exists('clients', 'id')->where('company_id', $user->company()->id)->where('is_deleted', 0)]; if ($this->file('documents') && is_array($this->file('documents'))) { $rules['documents.*'] = $this->fileValidation(); @@ -72,7 +73,7 @@ class StoreInvoiceRequest extends Request $rules['date'] = 'bail|sometimes|date:Y-m-d'; $rules['due_date'] = ['bail', 'sometimes', 'nullable', 'after:partial_due_date', Rule::requiredIf(fn () => strlen($this->partial_due_date ?? '') > 1), 'date']; - $rules['line_items'] = 'array'; + $rules['line_items'] = ['bail', 'array']; $rules['discount'] = 'sometimes|numeric|max:99999999999999'; $rules['tax_rate1'] = 'bail|sometimes|numeric'; $rules['tax_rate2'] = 'bail|sometimes|numeric'; @@ -92,7 +93,7 @@ class StoreInvoiceRequest extends Request $rules['verifactu_modified'] = ['bail', 'boolean', 'required_with:modified_invoice_id']; $rules['modified_invoice_id'] = ['bail', 'required_with:verifactu_modified', new CanGenerateModificationInvoice()]; - + return $rules; } diff --git a/app/Services/Invoice/HandleCancellation.php b/app/Services/Invoice/HandleCancellation.php index a51adae2e4..7ad31ba3d0 100644 --- a/app/Services/Invoice/HandleCancellation.php +++ b/app/Services/Invoice/HandleCancellation.php @@ -60,7 +60,19 @@ class HandleCancellation extends AbstractService return $this->invoice; } - + + /** + * verifactuCancellation + * @todo we must ensure that if there have been previous credit notes attached to the invoice, + * that the credit notes are not exceeded by the cancellation amount. + * This is because the credit notes are not linked to the invoice, but are linked to the + * invoice's backup. + * So we need to check the backup for the credit notes and ensure that the cancellation amount + * does not exceed the credit notes. + * If it does, we need to create a new credit note with the remaining amount. + * This is because the credit notes are not linked to the invoice, but are linked to the + * @return Invoice + */ private function verifactuCancellation(): Invoice { diff --git a/tests/Feature/EInvoice/Verifactu/VerifactuApiTest.php b/tests/Feature/EInvoice/Verifactu/VerifactuApiTest.php index c48791d39f..f07fdb88ab 100644 --- a/tests/Feature/EInvoice/Verifactu/VerifactuApiTest.php +++ b/tests/Feature/EInvoice/Verifactu/VerifactuApiTest.php @@ -91,6 +91,305 @@ class VerifactuApiTest extends TestCase } + public function test_credits_never_exceed_original_invoice7() + { + + $settings = $this->company->settings; + $settings->e_invoice_type = 'verifactu'; + + $this->company->settings = $settings; + $this->company->save(); + + $invoice = $this->buildData(); + $invoice->service()->markSent()->save(); + + $this->assertEquals(121, $invoice->amount); + + + $data = $invoice->toArray(); + unset($data['client']); + unset($data['invitations']); + $data['client_id'] = $this->client->hashed_id; + $data['verifactu_modified'] = true; + $data['modified_invoice_id'] = $invoice->hashed_id; + $data['number'] = null; + $data['discount'] = 120; + $data['is_amount_discount'] = true; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->postJson('/api/v1/invoices', $data); + + $response->assertStatus(200); + + } + + + public function test_credits_never_exceed_original_invoice6() + { + + $settings = $this->company->settings; + $settings->e_invoice_type = 'verifactu'; + + $this->company->settings = $settings; + $this->company->save(); + + $invoice = $this->buildData(); + $invoice->service()->markSent()->save(); + + $this->assertEquals(121, $invoice->amount); + + $invoice->line_items = [[ + 'quantity' => -1, + 'cost' => 10, + 'discount' => 0, + 'tax_rate1' => 21, + 'tax_name1' => 'IVA', + ]]; + + $invoice->discount = 0; + $invoice->is_amount_discount = false; + + $data = $invoice->toArray(); + unset($data['client']); + unset($data['invitations']); + $data['client_id'] = $this->client->hashed_id; + $data['verifactu_modified'] = true; + $data['modified_invoice_id'] = $invoice->hashed_id; + $data['number'] = null; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->postJson('/api/v1/invoices', $data); + + $response->assertStatus(200); + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->postJson('/api/v1/invoices', $data); + + $response->assertStatus(200); + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->postJson('/api/v1/invoices', $data); + + $response->assertStatus(200); + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->postJson('/api/v1/invoices', $data); + + $response->assertStatus(200); + + + } + + public function test_credits_never_exceed_original_invoice5() + { + + $settings = $this->company->settings; + $settings->e_invoice_type = 'verifactu'; + + $this->company->settings = $settings; + $this->company->save(); + + $invoice = $this->buildData(); + $invoice->service()->markSent()->save(); + + $this->assertEquals(121, $invoice->amount); + + $invoice->line_items = [[ + 'quantity' => -5, + 'cost' => 100, + 'discount' => 0, + 'tax_rate1' => 21, + 'tax_name1' => 'IVA', + ]]; + + $invoice->discount = 0; + $invoice->is_amount_discount = false; + + $data = $invoice->toArray(); + unset($data['client']); + $data['client_id'] = $this->client->hashed_id; + $data['verifactu_modified'] = true; + $data['modified_invoice_id'] = $invoice->hashed_id; + $data['number'] = null; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->postJson('/api/v1/invoices', $data); + + + $response->assertStatus(422); + } + + public function test_credits_never_exceed_original_invoice4() + { + + $settings = $this->company->settings; + $settings->e_invoice_type = 'verifactu'; + + $this->company->settings = $settings; + $this->company->save(); + + $invoice = $this->buildData(); + $invoice->service()->markSent()->save(); + + $this->assertEquals(121, $invoice->amount); + + $data = $invoice->toArray(); + + unset($data['client']); + unset($data['company']); + unset($data['invitations']); + $data['client_id'] = $this->client->hashed_id; + $data['verifactu_modified'] = true; + $data['modified_invoice_id'] = $invoice->hashed_id; + $data['number'] = null; + $data['line_items'] = [[ + 'quantity' => -1, + 'cost' => 100, + 'discount' => 0, + 'tax_rate1' => 21, + 'tax_name1' => 'IVA', + ]]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->postJson('/api/v1/invoices', $data); + + $response->assertStatus(200); + } + + + public function test_credits_never_exceed_original_invoice3() + { + + $settings = $this->company->settings; + $settings->e_invoice_type = 'verifactu'; + + $this->company->settings = $settings; + $this->company->save(); + + $invoice = $this->buildData(); + $invoice->service()->markSent()->save(); + + $invoice->line_items = []; + $invoice->discount = 500; + $invoice->is_amount_discount = false; + + $data = $invoice->toArray(); + $data['client_id'] = $this->client->hashed_id; + $data['verifactu_modified'] = true; + $data['modified_invoice_id'] = $invoice->hashed_id; + $data['number'] = null; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->postJson('/api/v1/invoices', $data); + + $response->assertStatus(422); + } + + public function test_credits_never_exceed_original_invoice2() + { + + $settings = $this->company->settings; + $settings->e_invoice_type = 'verifactu'; + + $this->company->settings = $settings; + $this->company->save(); + + $invoice = $this->buildData(); + $invoice->service()->markSent()->save(); + + $invoice->line_items = []; + + $invoice->discount = 500; + $invoice->is_amount_discount = false; + + $data = $invoice->toArray(); + $data['client_id'] = $this->client->hashed_id; + $data['verifactu_modified'] = true; + $data['modified_invoice_id'] = $invoice->hashed_id; + $data['number'] = null; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->postJson('/api/v1/invoices', $data); + + $response->assertStatus(422); + + } + + + public function test_credits_never_exceed_original_invoice() + { + + $settings = $this->company->settings; + $settings->e_invoice_type = 'verifactu'; + + $this->company->settings = $settings; + $this->company->save(); + + $invoice = $this->buildData(); + $invoice->service()->markSent()->save(); + + // $invoice->line_items = []; + $invoice->discount = 5; + $invoice->is_amount_discount = false; + + $data = $invoice->toArray(); + $data['client_id'] = $this->client->hashed_id; + $data['verifactu_modified'] = true; + $data['modified_invoice_id'] = $invoice->hashed_id; + $data['number'] = null; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->postJson('/api/v1/invoices', $data); + + $response->assertStatus(200); + } + + public function test_verifactu_amount_check() + { + + $settings = $this->company->settings; + $settings->e_invoice_type = 'verifactu'; + + $this->company->settings = $settings; + $this->company->save(); + + $invoice = $this->buildData(); + $invoice->line_items = []; + $invoice->discount = 500; + $invoice->is_amount_discount = false; + + $data = $invoice->toArray(); + $data['client_id'] = $this->client->hashed_id; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->postJson('/api/v1/invoices', $data); + + $response->assertStatus(422); + + } + public function test_create_modification_invoice() { @@ -281,13 +580,11 @@ class VerifactuApiTest extends TestCase $response->assertStatus(200); $arr = $response->json(); -// nlog($arr); $this->assertEquals($arr['data'][0]['status_id'], Invoice::STATUS_CANCELLED); $this->assertEquals($arr['data'][0]['balance'], 121); $this->assertEquals($arr['data'][0]['amount'], 121); $this->assertNotNull($arr['data'][0]['backup']['child_invoice_ids'][0]); - $credit_invoice = Invoice::find($this->decodePrimaryKey($arr['data'][0]['backup']['child_invoice_ids'][0]));