diff --git a/modules/fatture/src/Gestori/Scadenze.php b/modules/fatture/src/Gestori/Scadenze.php index 6ab4a4b90..6a8e5abde 100644 --- a/modules/fatture/src/Gestori/Scadenze.php +++ b/modules/fatture/src/Gestori/Scadenze.php @@ -22,6 +22,7 @@ use Modules\Fatture\Fattura; use Modules\PrimaNota\Movimento; +use Modules\Pagamenti\Pagamento; use Modules\Scadenzario\Scadenza; use Plugins\AssicurazioneCrediti\AssicurazioneCrediti; use Plugins\ImportFE\FatturaElettronica as FatturaElettronicaImport; @@ -34,8 +35,9 @@ */ class Scadenze { - public function __construct(private readonly Fattura $fattura) + public function __construct(private readonly Fattura $fattura, $database = null) { + $this->database = $database ?: database(); // Allow mocking } /** @@ -49,12 +51,14 @@ public function registra($is_pagato = false, $ignora_fe = false) // Rimozione degli elementi pre-esistenti e ottimizzazione caricamento assicurazioni $assicurazioni_map = $this->rimuovi(); + $scadenze = []; + if (!$ignora_fe && $this->fattura->module == 'Fatture di acquisto' && $this->fattura->isFE()) { - $scadenze_fe = $this->registraScadenzeFE($is_pagato, $assicurazioni_map); + $scadenze = $this->registraScadenzeFE($is_pagato, $assicurazioni_map); } - if (empty($scadenze_fe)) { - $this->registraScadenzeTradizionali($is_pagato, $assicurazioni_map); + if (empty($scadenze)) { + $scadenze = $this->registraScadenzeTradizionali($is_pagato, $assicurazioni_map); } // Registrazione scadenza per Ritenuta d'Acconto @@ -79,8 +83,10 @@ public function registra($is_pagato = false, $ignora_fe = false) $id_banca_controparte = $this->fattura->id_banca_controparte; $importo = -$ritenuta_acconto; - self::registraScadenza($this->fattura, $importo, $scadenza, $is_pagato, $id_pagamento, $id_banca_azienda, $id_banca_controparte, 'ritenuta_acconto', $assicurazioni_map); + $scadenze[] = $this->registraScadenza($this->fattura, $importo, $scadenza, $is_pagato, $id_pagamento, $id_banca_azienda, $id_banca_controparte, 'ritenuta_acconto', $assicurazioni_map); } + + return $scadenze; } /** @@ -98,14 +104,20 @@ public function rimuovi() $assicurazioni_map = []; if ($id_anagrafiche->isNotEmpty()) { - $assicurazioni_list = AssicurazioneCrediti::whereIn('id_anagrafica', $id_anagrafiche)->get(); + foreach ($id_anagrafiche as $id_anagrafica) { + $assicurazioni_list = $this->trovaAssicurazioneCrediti($id_anagrafica); + + if (empty($assicurazioni_list)) { + continue; + } - // Crea una mappa per accesso rapido: chiave = id_anagrafica, valore = lista assicurazioni - foreach ($assicurazioni_list as $assicurazione) { - if (!isset($assicurazioni_map[$assicurazione->id_anagrafica])) { - $assicurazioni_map[$assicurazione->id_anagrafica] = []; + // Crea una mappa per accesso rapido: chiave = id_anagrafica, valore = lista assicurazioni + foreach ($assicurazioni_list as $assicurazione) { + if (!isset($assicurazioni_map[$assicurazione->id_anagrafica])) { + $assicurazioni_map[$assicurazione->id_anagrafica] = []; + } + $assicurazioni_map[$assicurazione->id_anagrafica][] = $assicurazione; } - $assicurazioni_map[$assicurazione->id_anagrafica][] = $assicurazione; } // Salva le assicurazioni da aggiornare dopo la cancellazione @@ -120,7 +132,7 @@ public function rimuovi() } } - database()->delete('co_scadenzario', ['id_documento' => $this->fattura->id]); + $this->database->delete('co_scadenzario', ['id_documento' => $this->fattura->id]); foreach ($assicurazioni as $assicurazione) { $assicurazione->fixTotale(); @@ -149,14 +161,14 @@ protected function registraScadenza(Fattura $fattura, $importo, $data_scadenza, $descrizione = $fattura->tipo->getTranslation('title').' numero '.$numero; $id_anagrafica = $fattura->id_anagrafica; - $scadenza = Scadenza::build($id_anagrafica, $descrizione, $importo, $data_scadenza, $id_pagamento, $id_banca_azienda, $id_banca_controparte, $type, $is_pagato, $fattura->id); + $scadenza = $this->generaScadenza($id_anagrafica, $descrizione, $importo, $data_scadenza, $id_pagamento, $id_banca_azienda, $id_banca_controparte, $type, $is_pagato, $fattura->id); $scadenza->data_emissione = $fattura->data; $scadenza->save(); $assicurazione_crediti = null; - if ($assicurazioni_map !== null && isset($assicurazioni_map[$id_anagrafica])) { + if ($assicurazioni_map !== null && !empty($assicurazioni_map) && isset($assicurazioni_map[$id_anagrafica])) { // Ottimizzazione: usa la mappa delle assicurazioni già caricata foreach ($assicurazioni_map[$id_anagrafica] as $assicurazione) { if ($scadenza->scadenza >= $assicurazione->data_inizio && $scadenza->scadenza <= $assicurazione->data_fine) { @@ -164,6 +176,9 @@ protected function registraScadenza(Fattura $fattura, $importo, $data_scadenza, break; } } + } elseif ($assicurazioni_map !== null) { + // Mappa vuota o senza entry per questa anagrafica, nessuna assicurazione da cercare + $assicurazione_crediti = null; } else { // Query al database se la mappa non è disponibile $assicurazione_crediti = AssicurazioneCrediti::where('id_anagrafica', $scadenza->id_anagrafica)->where('data_inizio', '<=', $scadenza->scadenza)->where('data_fine', '>=', $scadenza->scadenza)->first(); @@ -176,7 +191,7 @@ protected function registraScadenza(Fattura $fattura, $importo, $data_scadenza, // Pagamento automatico se scadenza = data fattura e flag attivo if (!$is_pagato && $scadenza->scadenza->format('Y-m-d') <= date('Y-m-d') && $importo) { - $pagamento = \Modules\Pagamenti\Pagamento::find($id_pagamento); + $pagamento = $this->trovaPagamento($id_pagamento); if (!empty($pagamento) && $pagamento->registra_pagamento_automatico) { $importo_da_registrare = abs($importo); $dir = $fattura->tipo->dir; @@ -193,6 +208,28 @@ protected function registraScadenza(Fattura $fattura, $importo, $data_scadenza, } } } + return $scadenza; + } + + protected function trovaPagamento($id_pagamento): ?Pagamento + { + return Pagamento::where('id', $id_pagamento)->first(); + } + + protected function trovaAssicurazioneCrediti($id_anagrafica) + { + return AssicurazioneCrediti::where('id_anagrafica', $id_anagrafica)->get(); + } + + + protected function trovaAssicurazioneCreditiConScadenze($id_anagrafica, $data_scadenza): ?AssicurazioneCrediti + { + return AssicurazioneCrediti::where('id_anagrafica', $id_anagrafica)->where('data_inizio', '<=', $data_scadenza)->where('data_fine', '>=', $data_scadenza)->first(); + } + + protected function generaScadenza($id_anagrafica, $descrizione, $importo, $data_scadenza, $id_pagamento, $id_banca_azienda, $id_banca_controparte, $type, $is_pagato): Scadenza + { + return Scadenza::build($id_anagrafica, $descrizione, $importo, $data_scadenza, $id_pagamento, $id_banca_azienda, $id_banca_controparte, $type, $is_pagato); } /** @@ -216,6 +253,7 @@ protected function registraScadenzeFE($is_pagato = false, $assicurazioni_map = n $pagamenti = isset($pagamenti[0]) ? $pagamenti : [$pagamenti]; } + $results = []; foreach ($pagamenti as $pagamento) { $rate = $pagamento['DettaglioPagamento']; $rate = isset($rate[0]) ? $rate : [$rate]; @@ -227,11 +265,11 @@ protected function registraScadenzeFE($is_pagato = false, $assicurazioni_map = n $scadenza = !empty($rata['DataScadenzaPagamento']) ? FatturaElettronicaImport::parseDate($rata['DataScadenzaPagamento']) : $this->fattura->data; $importo = $this->fattura->isNota() ? $rata['ImportoPagamento'] : -$rata['ImportoPagamento']; - self::registraScadenza($this->fattura, $importo, $scadenza, $is_pagato, $id_pagamento, $id_banca_azienda, $id_banca_controparte, 'fattura', $assicurazioni_map); + $results[] = $this->registraScadenza($this->fattura, $importo, $scadenza, $is_pagato, $id_pagamento, $id_banca_azienda, $id_banca_controparte, 'fattura', $assicurazioni_map); } } - return !empty($pagamenti); + return $results; } /** @@ -247,17 +285,20 @@ protected function registraScadenzeTradizionali($is_pagato = false, $assicurazio $netto = $this->fattura->isNota() ? -$netto : $netto; // Calcolo delle rate - $rate = ($this->fattura->pagamento ?: \Modules\Pagamenti\Pagamento::where('id', $this->fattura->id_pagamento)->first())->calcola($netto, $this->fattura->data, $this->fattura->id_anagrafica); + $rate = ($this->fattura->pagamento ?: $this->trovaPagamento($this->fattura->id_pagamento))->calcola($netto, $this->fattura->data, $this->fattura->id_anagrafica, $this->database); $direzione = $this->fattura->tipo->dir; - foreach ($rate as $rata) { - $scadenza = $rata['scadenza']; - $importo = $direzione == 'uscita' ? -$rata['importo'] : $rata['importo']; - $id_pagamento = $this->fattura->id_pagamento; - $id_banca_azienda = $this->fattura->id_banca_azienda; - $id_banca_controparte = $this->fattura->id_banca_controparte; + $results = []; +foreach ($rate as $rata) { + $scadenza = $rata['scadenza']; + $importo = $direzione == 'uscita' ? -$rata['importo'] : $rata['importo']; + $id_pagamento = $this->fattura->id_pagamento; + $id_banca_azienda = $this->fattura->id_banca_azienda; + $id_banca_controparte = $this->fattura->id_banca_controparte; - self::registraScadenza($this->fattura, $importo, $scadenza, $is_pagato, $id_pagamento, $id_banca_azienda, $id_banca_controparte, 'fattura', $assicurazioni_map); - } + $results[] = $this->registraScadenza($this->fattura, $importo, $scadenza, $is_pagato, $id_pagamento, $id_banca_azienda, $id_banca_controparte, 'fattura', $assicurazioni_map); + } + + return $results; } } diff --git a/modules/fatture/tests/GenerazioneScadenzeTest.php b/modules/fatture/tests/GenerazioneScadenzeTest.php new file mode 100644 index 000000000..b62b53529 --- /dev/null +++ b/modules/fatture/tests/GenerazioneScadenzeTest.php @@ -0,0 +1,189 @@ +getFatturaConRate('2025-03-30', 500, [ + $this->mockModel(Pagamento::class, [ + 'giorno' => 0, + 'num_giorni' => 90, + 'prc' => 100, + ]), + ]); + + $gestore = $this->getGestore($fattura); + $gestore->shouldReceive('trovaPagamento')->andReturn(null); + $gestore->shouldReceive('trovaAssicurazioneCrediti')->andReturn(null); + $gestore->shouldReceive('trovaAssicurazioneCreditiConScadenze')->andReturn(null); + + $scadenze = $gestore->registra(false, true); + + $this->assertEquals(1, count($scadenze)); + + $this->assertEquals('2025-03-30', $scadenze[0]->data_emissione); + $this->assertEquals('2025-06-28', $scadenze[0]->scadenza->format('Y-m-d')); + $this->assertEquals(500, $scadenze[0]->da_pagare); + } + + public function testRimessaDiretta90GiorniAl15() + { + $fattura = $this->getFatturaConRate('2025-03-30', 500, [ + $this->mockModel(Pagamento::class, [ + 'giorno' => 15, + 'num_giorni' => 90, + 'prc' => 100, + ]), + ]); + + $gestore = $this->getGestore($fattura); + $gestore->shouldReceive('trovaPagamento')->andReturn(null); + $gestore->shouldReceive('trovaAssicurazioneCrediti')->andReturn(null); + $gestore->shouldReceive('trovaAssicurazioneCreditiConScadenze')->andReturn(null); + + $scadenze = $gestore->registra(false, true); + + $this->assertEquals(1, count($scadenze)); + + $this->assertEquals('2025-03-30', $scadenze[0]->data_emissione); + $this->assertEquals('2025-06-15', $scadenze[0]->scadenza->format('Y-m-d')); + $this->assertEquals(500, $scadenze[0]->da_pagare); + } + + public function test3RateStatiche() + { + $fattura = $this->getFatturaConRate('2025-03-30', 600, [ + $this->mockModel(Pagamento::class, [ + 'giorno' => 0, + 'num_giorni' => 90, + 'prc' => 33, + ]), + $this->mockModel(Pagamento::class, [ + 'giorno' => 0, + 'num_giorni' => 180, + 'prc' => 33, + ]), + $this->mockModel(Pagamento::class, [ + 'giorno' => 0, + 'num_giorni' => 270, + 'prc' => 34, + ]), + ]); + + $gestore = $this->getGestore($fattura); + $gestore->shouldReceive('trovaPagamento')->andReturn(null); + $gestore->shouldReceive('trovaAssicurazioneCrediti')->andReturn(null); + $gestore->shouldReceive('trovaAssicurazioneCreditiConScadenze')->andReturn(null); + + $scadenze = $gestore->registra(false, true); + + $this->assertEquals(3, count($scadenze)); + + $this->assertEquals('2025-03-30', $scadenze[0]->data_emissione); + $this->assertEquals('2025-06-28', $scadenze[0]->scadenza->format('Y-m-d')); + $this->assertEquals(198, $scadenze[0]->da_pagare); + + $this->assertEquals('2025-03-30', $scadenze[1]->data_emissione); + $this->assertEquals('2025-09-26', $scadenze[1]->scadenza->format('Y-m-d')); + $this->assertEquals(198, $scadenze[1]->da_pagare); + + $this->assertEquals('2025-03-30', $scadenze[2]->data_emissione); + $this->assertEquals('2025-12-25', $scadenze[2]->scadenza->format('Y-m-d')); + $this->assertEquals(204, $scadenze[2]->da_pagare); + } + + protected function mockModel($class, $attributes = null) + { + $ref = Mockery::mock($class)->shouldAllowMockingProtectedMethods()->makePartial(); + + $ref->shouldReceive('save')->andReturn(null); + $ref->shouldReceive('delete')->andReturn(null); + $ref->shouldReceive('getDateFormat')->andReturn('Y-m-d'); + + // Fix per gestione attributi su modello mocked di Eloquent + $ref->shouldReceive('getMutatedAttributes')->andReturnUsing(function () use ($class) { + preg_match_all('/(?<=^|;)get([^;]+?)Attribute(;|$)/', implode(';', get_class_methods($class)), $matches); + + $ref = $matches[1]; + + return collect($ref) + ->map(function ($match) { + return lcfirst(static::$snakeAttributes ? Str::snake($match) : $match); + })->all(); + }); + + if (!empty($attributes)) { + foreach ($attributes as $key => $value) { + $ref->shouldReceive('getAttribute')->with($key)->andReturn($value); + } + } + + return $ref; + } + + protected function getFatturaConRate($data, $netto, $rate): Fattura + { + $tipo = $this->mockModel(Tipo::class); + $tipo->shouldReceive('getAttribute')->with('dir')->andReturn('entrata'); + $tipo->shouldReceive('getTranslation')->andReturn('Fattura di vendita'); + + $pagamento = $this->mockModel(Pagamento::class); + $pagamento->shouldReceive('trovaRate')->andReturn($rate); + + $fattura = $this->mockModel(Fattura::class, [ + 'numero_esterno' => '2025-01', + 'idpagamento' => null, + 'id_banca_controparte' => null, + 'id_banca_azienda' => null, + 'idanagrafica' => 'test-an-1', + 'data' => $data, + 'id' => 'test-1', + 'netto' => $netto, + 'scadenze' => collect([]), + 'pagamento' => $pagamento, + 'tipo' => $tipo, + 'ritenuta_acconto' => null, + ]); + $fattura->shouldReceive('associate')->andReturn(null); + $fattura->shouldReceive('isNota')->andReturn(false); + + return $fattura; + } + + protected function getGestore($fattura): Scadenze + { + $database = $this->mockModel(Database::class); + $database->shouldReceive('delete')->andReturn(null); + $database->shouldReceive('selectOne')->andReturn(null); + + $gestore = Mockery::mock(Scadenze::class, [$fattura, $database])->shouldAllowMockingProtectedMethods()->makePartial(); + $gestore->shouldReceive('generaScadenza')->andReturnUsing(function ($idanagrafica, $descrizione, $importo, $data_scadenza, $id_pagamento, $id_banca_azienda, $id_banca_controparte, $type, $is_pagato) use ($fattura) { + $scadenza = $this->mockModel(Scadenza::class, [ + 'idanagrafica' => $idanagrafica, + 'descrizione' => $descrizione, + 'scadenza' => \Carbon\Carbon::create($data_scadenza), + 'da_pagare' => $importo, + 'tipo' => $type, + 'id_pagamento' => $id_pagamento, + 'id_banca_azienda' => $id_banca_azienda, + 'id_banca_controparte' => $id_banca_controparte, + + 'pagato' => $is_pagato ? $importo : 0, + 'data_pagamento' => $is_pagato ? $data_scadenza : null, + ]); + + $scadenza->shouldReceive('documento')->andReturn($fattura); + + return $scadenza; + }); + + return $gestore; + } +} diff --git a/modules/pagamenti/src/Pagamento.php b/modules/pagamenti/src/Pagamento.php index 02a2e9783..9679f627b 100755 --- a/modules/pagamenti/src/Pagamento.php +++ b/modules/pagamenti/src/Pagamento.php @@ -55,10 +55,17 @@ public function rate() { return $this->hasMany(Pagamento::class, 'id'); } + + protected function trovaRate() + { + return Pagamento::where('name', '=', $this->name)->get()->sortBy('num_giorni')->toArray(); + } - public function calcola($importo, $data, $id_anagrafica) + public function calcola($importo, $data, $id_anagrafica, $database = null) { - $rate = Pagamento::where('name', '=', $this->name)->get()->sortBy('num_giorni')->pluck('id')->toArray(); + $database = $database ?: database(); // Allow mocking + + $rate = $this->trovaRate(); $number = count($rate); $totale = 0.0; @@ -67,7 +74,6 @@ public function calcola($importo, $data, $id_anagrafica) $count = 0; foreach ($rate as $key => $rata) { $date = new Carbon($data); - $rata = Pagamento::find($rata); // X giorni esatti if ($rata->giorno == 0) { // Offset della rata @@ -113,7 +119,7 @@ public function calcola($importo, $data, $id_anagrafica) } // Posticipo la scadenza in base alle regole pagamenti dell'anagrafica - $regola_pagamento = database()->selectOne('an_pagamenti_anagrafiche', '*', ['id_anagrafica' => $id_anagrafica, 'mese' => $date->format('m')]); + $regola_pagamento = $database->selectOne('an_pagamenti_anagrafiche', '*', ['id_anagrafica' => $id_anagrafica, 'mese' => $date->format('m')]); if (!empty($regola_pagamento)) { $date->modify('last day of this month'); $date->addDays($regola_pagamento['giorno_fisso']); diff --git a/package.json b/package.json index aeaefaff8..a5d381549 100644 --- a/package.json +++ b/package.json @@ -99,7 +99,7 @@ "build-OSM": "gulp && npm run-script update-openapi-client", "dump-OSM": "php composer.phar dump-autoload", "windows-fix": "yarn global add windows-build-tools", - "php-cs-fix": "vendor/bin/php-cs-fixer fix", + "php-cs-fix": "php composer.phar run format", "rector": "vendor/bin/rector process", "setup-corepack": "npm install -g corepack && corepack enable", "export-openapi": "php artisan api:openapi:export --output=openapi.json", diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 000000000..dd0c8ca0c --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,17 @@ + + + + + tests/ + + + + ./modules/*/tests/ + + + \ No newline at end of file diff --git a/tests/GeneratorTest.php b/tests/GeneratorTest.php new file mode 100644 index 000000000..f0a002a2b --- /dev/null +++ b/tests/GeneratorTest.php @@ -0,0 +1,70 @@ +test(null, '|TEST'); + } + + public function testNumbersWithSuffix() + { + $this->test('|TEST'); + } + + public function testCommonNumbers() + { + $this->test(); + } + + public function testDates() + { + $this->test('/YYYY'); + $this->test('/yy'); + + $this->test(null, 'YYYY-'); + $this->test(null, 'yy-'); + } + + protected function test($prefix = null, $suffix = null) + { + $date = date('Y-m-d H:i:s'); + $info = Generator::dateToPattern($date); + + // Individuazione valori relativi a suffisso e prefisso + $prefix_value = Generator::complete($prefix, $info); + $suffix_value = Generator::complete($suffix, $info); + + $step = 3; + + // Pattern di base con numero di caratteri incrementale + $pattern = $prefix.'#'.$suffix; + + $previous = null; + for ($i = 0; $i < 10000; $i = $i + $step) { + $value = $prefix_value.($i + 1).$suffix_value; + $this->assertEquals($value, Generator::generate($pattern, $previous, $step, $info)); + + $previous = $value; + } + + // Pattern con padding + $length = 5; + $pattern = $prefix.str_repeat('#', $length).$suffix; + + $previous = null; + for ($i = 0; $i < 10000; $i = $i + $step) { + $value = $prefix_value.$this->pad($i + 1, $length).$suffix_value; + $this->assertEquals($value, Generator::generate($pattern, $previous, $step, $info)); + + $previous = $value; + } + } + + protected function pad($number, $length) + { + return str_pad($number, $length, '0', STR_PAD_LEFT); + } +}