From 61c813fc08efd62ea24460bb06bdac8847a7232a Mon Sep 17 00:00:00 2001 From: aldarien Date: Thu, 11 Sep 2025 15:16:12 -0300 Subject: [PATCH] feature/cierres (#30) Reservas agregar y aprobar Co-authored-by: Juan Pablo Vial Reviewed-on: http://git.provm.cl/Incoviba/oficial/pulls/30 --- app/common/Ideal/Model.php | 2 +- app/common/Implement/Repository/Factory.php | 12 +- ...=> 20250215133437_create_reservations.php} | 6 +- ...0215135457_create_reservation_details.php} | 8 +- ...250215135822_create_reservation_states.php | 2 +- ...05212635_add_comments_to_estado_cierre.php | 29 + app/resources/routes/api/personas.php | 7 + app/resources/routes/api/proyectos.php | 1 + .../routes/api/ventas/reservations.php | 9 + app/resources/routes/ventas/cierres.php | 4 +- .../views/layout/body/scripts/rut.blade.php | 6 + .../views/ventas/reservations.blade.php | 664 +++++++++++++++ .../ventas/reservations/modal/add.blade.php | 758 ++++++++++++++++++ app/src/Controller/API/Personas.php | 34 + app/src/Controller/API/Proyectos.php | 15 +- .../Controller/API/Ventas/Reservations.php | 118 ++- app/src/Controller/Ventas/Reservations.php | 28 + app/src/Exception/Model/InvalidState.php | 13 + app/src/Model/Persona.php | 13 +- app/src/Model/Persona/Datos.php | 3 +- app/src/Model/Proyecto/Broker.php | 33 + app/src/Model/Proyecto/Broker/Contract.php | 46 ++ .../Model/Proyecto/Broker/Contract/State.php | 10 + app/src/Model/Venta/Datos.php | 5 +- app/src/Model/Venta/Promotion.php | 83 ++ app/src/Model/Venta/Reservation.php | 84 +- .../Model/Venta/Reservation/Detail/Type.php | 17 + app/src/Model/Venta/Reservation/State.php | 9 +- .../Model/Venta/Reservation/State/Type.php | 22 +- app/src/Repository/Persona/Datos.php | 12 +- .../Repository/Proyecto/Broker/Contract.php | 6 +- app/src/Repository/Venta/EstadoPrecio.php | 2 - app/src/Repository/Venta/Promotion.php | 2 +- app/src/Repository/Venta/Reservation.php | 468 +++++++++-- .../Repository/Venta/Reservation/State.php | 13 +- app/src/Service/Direccion.php | 49 ++ app/src/Service/Persona.php | 172 +++- app/src/Service/Venta/Promotion.php | 14 + app/src/Service/Venta/Reservation.php | 238 +++++- app/tests/extension/ContainerTrait.php | 15 + app/tests/extension/Seeds/Bancos.php | 23 + app/tests/extension/Seeds/BrokerContracts.php | 45 ++ app/tests/extension/Seeds/Brokers.php | 50 ++ app/tests/extension/Seeds/Direcciones.php | 4 +- app/tests/extension/Seeds/Inmobiliarias.php | 12 +- app/tests/extension/Seeds/Promotions.php | 55 ++ .../extension/Seeds/ProyectoTipoUnidad.php | 42 + app/tests/extension/Seeds/Proyectos.php | 6 +- app/tests/extension/Seeds/Unidades.php | 47 ++ .../API/Ventas/ReservationTest.php | 92 +++ app/tests/integration/QueueTest.php | 9 +- .../unit/src/Model/Venta/ReservationTest.php | 2 +- .../unit/src/Repository/DireccionTest.php | 64 ++ app/tests/unit/src/Repository/PersonaTest.php | 62 ++ .../src/Repository/Venta/ReservationTest.php | 102 +++ app/tests/unit/src/Service/DireccionTest.php | 64 ++ app/tests/unit/src/Service/PersonaTest.php | 65 ++ app/tests/unit/src/Service/ValorTest.php | 12 +- .../unit/src/Service/Venta/BonoPieTest.php | 65 ++ app/tests/unit/src/Service/Venta/PagoTest.php | 4 +- .../src/Service/Venta/ReservationTest.php | 139 ++++ 61 files changed, 3806 insertions(+), 190 deletions(-) rename app/resources/database/migrations/{20250215133437_create_reservation.php => 20250215133437_create_reservations.php} (78%) rename app/resources/database/migrations/{20250215135457_create_reservation_datas.php => 20250215135457_create_reservation_details.php} (73%) create mode 100644 app/resources/database/migrations/20250805212635_add_comments_to_estado_cierre.php create mode 100644 app/resources/routes/api/personas.php create mode 100644 app/resources/views/ventas/reservations.blade.php create mode 100644 app/resources/views/ventas/reservations/modal/add.blade.php create mode 100644 app/src/Controller/API/Personas.php create mode 100644 app/src/Controller/Ventas/Reservations.php create mode 100644 app/src/Exception/Model/InvalidState.php create mode 100644 app/src/Model/Venta/Reservation/Detail/Type.php create mode 100644 app/src/Service/Direccion.php create mode 100644 app/tests/extension/ContainerTrait.php create mode 100644 app/tests/extension/Seeds/Bancos.php create mode 100644 app/tests/extension/Seeds/BrokerContracts.php create mode 100644 app/tests/extension/Seeds/Brokers.php create mode 100644 app/tests/extension/Seeds/Promotions.php create mode 100644 app/tests/extension/Seeds/ProyectoTipoUnidad.php create mode 100644 app/tests/extension/Seeds/Unidades.php create mode 100644 app/tests/integration/API/Ventas/ReservationTest.php create mode 100644 app/tests/unit/src/Repository/DireccionTest.php create mode 100644 app/tests/unit/src/Repository/PersonaTest.php create mode 100644 app/tests/unit/src/Repository/Venta/ReservationTest.php create mode 100644 app/tests/unit/src/Service/DireccionTest.php create mode 100644 app/tests/unit/src/Service/PersonaTest.php create mode 100644 app/tests/unit/src/Service/Venta/BonoPieTest.php create mode 100644 app/tests/unit/src/Service/Venta/ReservationTest.php diff --git a/app/common/Ideal/Model.php b/app/common/Ideal/Model.php index c68827a..ec2c59b 100644 --- a/app/common/Ideal/Model.php +++ b/app/common/Ideal/Model.php @@ -26,7 +26,7 @@ abstract class Model implements Define\Model public function jsonSerialize(): mixed { return [ - 'id' => $this->id, + 'id' => $this->id ?? '', ...$this->jsonComplement() ]; } diff --git a/app/common/Implement/Repository/Factory.php b/app/common/Implement/Repository/Factory.php index 00fd799..10ec520 100644 --- a/app/common/Implement/Repository/Factory.php +++ b/app/common/Implement/Repository/Factory.php @@ -2,7 +2,9 @@ namespace Incoviba\Common\Implement\Repository; use Closure; +use Exception; use Incoviba\Common\Define; +use Incoviba\Common\Implement\Exception\EmptyResult; class Factory implements Define\Repository\Factory { @@ -20,8 +22,16 @@ class Factory implements Define\Repository\Factory return $this; } + /** + * @return mixed + * @throws EmptyResult + */ public function run(): mixed { - return call_user_func_array($this->callable, $this->args); + try { + return call_user_func_array($this->callable, $this->args); + } catch (Exception $exception) { + throw new EmptyResult($exception->getMessage(), $exception); + } } } diff --git a/app/resources/database/migrations/20250215133437_create_reservation.php b/app/resources/database/migrations/20250215133437_create_reservations.php similarity index 78% rename from app/resources/database/migrations/20250215133437_create_reservation.php rename to app/resources/database/migrations/20250215133437_create_reservations.php index db9c32a..1a8dec7 100644 --- a/app/resources/database/migrations/20250215133437_create_reservation.php +++ b/app/resources/database/migrations/20250215133437_create_reservations.php @@ -4,7 +4,7 @@ declare(strict_types=1); use Phinx\Migration\AbstractMigration; -final class CreateReservation extends AbstractMigration +final class CreateReservations extends AbstractMigration { /** * Change Method. @@ -23,9 +23,11 @@ final class CreateReservation extends AbstractMigration $this->execute("ALTER DATABASE CHARACTER SET 'utf8mb4';"); $this->execute("ALTER DATABASE COLLATE='utf8mb4_general_ci';"); - $this->table('reservation') + $this->table('reservations') + ->addColumn('project_id', 'integer', ['signed' => false, 'null' => false]) ->addColumn('buyer_rut', 'integer', ['signed' => false, 'null' => false]) ->addColumn('date', 'date', ['null' => false]) + ->addForeignKey('project_id', 'proyecto', 'id', ['delete' => 'cascade', 'update' => 'cascade']) ->addForeignKey('buyer_rut', 'personas', 'rut', ['delete' => 'cascade', 'update' => 'cascade']) ->create(); diff --git a/app/resources/database/migrations/20250215135457_create_reservation_datas.php b/app/resources/database/migrations/20250215135457_create_reservation_details.php similarity index 73% rename from app/resources/database/migrations/20250215135457_create_reservation_datas.php rename to app/resources/database/migrations/20250215135457_create_reservation_details.php index ad85f98..3bdcac7 100644 --- a/app/resources/database/migrations/20250215135457_create_reservation_datas.php +++ b/app/resources/database/migrations/20250215135457_create_reservation_details.php @@ -4,7 +4,7 @@ declare(strict_types=1); use Phinx\Migration\AbstractMigration; -final class CreateReservationDatas extends AbstractMigration +final class CreateReservationDetails extends AbstractMigration { /** * Change Method. @@ -23,11 +23,13 @@ final class CreateReservationDatas extends AbstractMigration $this->execute("ALTER DATABASE CHARACTER SET 'utf8mb4';"); $this->execute("ALTER DATABASE COLLATE='utf8mb4_general_ci';"); - $this->table('reservation_data') + $this->table('reservation_details') ->addColumn('reservation_id', 'integer', ['signed' => false, 'null' => false]) ->addColumn('type', 'integer', ['length' => 1, 'signed' => false, 'null' => false]) ->addColumn('reference_id', 'integer', ['signed' => false, 'null' => false]) - ->addColumn('value', 'decimal', ['precision' => 10, 'scale' => 2, 'signed' => false, 'default' => 0.00, 'null' => true]) + ->addColumn('value', 'decimal', ['precision' => 10, 'scale' => 2, 'signed' => false, 'default' => null, 'null' => true]) + ->addForeignKey('reservation_id', 'reservations', 'id', ['delete' => 'cascade', 'update' => 'cascade']) + ->addIndex(['reservation_id', 'type', 'reference_id'], ['unique' => true, 'name' => 'idx_reservation_details']) ->create(); $this->execute('SET unique_checks=1; SET foreign_key_checks=1;'); diff --git a/app/resources/database/migrations/20250215135822_create_reservation_states.php b/app/resources/database/migrations/20250215135822_create_reservation_states.php index e617ef5..490d3dc 100644 --- a/app/resources/database/migrations/20250215135822_create_reservation_states.php +++ b/app/resources/database/migrations/20250215135822_create_reservation_states.php @@ -27,7 +27,7 @@ final class CreateReservationStates extends AbstractMigration ->addColumn('reservation_id', 'integer', ['signed' => false, 'null' => false]) ->addColumn('date', 'date', ['null' => false]) ->addColumn('type', 'integer', ['length' => 3, 'null' => false, 'default' => 0]) - ->addForeignKey('reservation_id', 'reservation', 'id', ['delete' => 'cascade', 'update' => 'cascade']) + ->addForeignKey('reservation_id', 'reservations', 'id', ['delete' => 'cascade', 'update' => 'cascade']) ->create(); $this->execute('SET unique_checks=1; SET foreign_key_checks=1;'); diff --git a/app/resources/database/migrations/20250805212635_add_comments_to_estado_cierre.php b/app/resources/database/migrations/20250805212635_add_comments_to_estado_cierre.php new file mode 100644 index 0000000..bbd9cae --- /dev/null +++ b/app/resources/database/migrations/20250805212635_add_comments_to_estado_cierre.php @@ -0,0 +1,29 @@ +table('estado_cierre_comentarios') + ->addColumn('estado_cierre_id', 'integer', ['signed' => false]) + ->addColumn('fecha', 'datetime', ['default' => 'CURRENT_TIMESTAMP']) + ->addColumn('comments', 'text') + ->addForeignKey('estado_cierre_id', 'estado_cierre', 'id', ['delete' => 'cascade', 'update' => 'cascade']) + ->create(); + } +} diff --git a/app/resources/routes/api/personas.php b/app/resources/routes/api/personas.php new file mode 100644 index 0000000..b01069c --- /dev/null +++ b/app/resources/routes/api/personas.php @@ -0,0 +1,7 @@ +group('/personas', function($app) {}); +$app->group('/persona/{rut}', function($app) { + $app->get('[/]', [Personas::class, 'get']); +}); diff --git a/app/resources/routes/api/proyectos.php b/app/resources/routes/api/proyectos.php index 15d0445..5ff5f90 100644 --- a/app/resources/routes/api/proyectos.php +++ b/app/resources/routes/api/proyectos.php @@ -30,4 +30,5 @@ $app->group('/proyecto/{proyecto_id}', function($app) { $app->post('/edit[/]', [Proyectos::class, 'terreno']); }); $app->get('/brokers', [Proyectos::class, 'brokers']); + $app->get('/promotions', [Proyectos::class, 'promotions']); }); diff --git a/app/resources/routes/api/ventas/reservations.php b/app/resources/routes/api/ventas/reservations.php index 7e2e194..31ce261 100644 --- a/app/resources/routes/api/ventas/reservations.php +++ b/app/resources/routes/api/ventas/reservations.php @@ -3,10 +3,19 @@ use Incoviba\Controller\API\Ventas\Reservations; $app->group('/reservations', function($app) { $app->post('/add[/]', [Reservations::class, 'add']); + $app->group('/project/{project_id}', function($app) { + $app->get('/active[/]', [Reservations::class, 'active']); + $app->get('/pending[/]', [Reservations::class, 'pending']); + $app->get('/rejected[/]', [Reservations::class, 'rejected']); + }); $app->get('[/]', Reservations::class); }); +$app->post('/reservation/add[/]', [Reservations::class, 'addOne']); $app->group('/reservation/{reservation_id}', function($app) { + $app->get('/approve[/]', [Reservations::class, 'approve']); + $app->get('/reject[/]', [Reservations::class, 'reject']); $app->post('/edit[/]', [Reservations::class, 'edit']); + $app->delete('/remove[/]', [Reservations::class, 'remove']); $app->delete('[/]', [Reservations::class, 'delete']); $app->get('[/]', [Reservations::class, 'get']); }); diff --git a/app/resources/routes/ventas/cierres.php b/app/resources/routes/ventas/cierres.php index 62e3084..2320f3d 100644 --- a/app/resources/routes/ventas/cierres.php +++ b/app/resources/routes/ventas/cierres.php @@ -1,7 +1,9 @@ group('/cierres', function($app) { + $app->get('/project/{project_id}', Cierres::class); $app->get('[/]', Cierres::class); }); $app->group('/cierre/{cierre_id}', function($app) { diff --git a/app/resources/views/layout/body/scripts/rut.blade.php b/app/resources/views/layout/body/scripts/rut.blade.php index a6b86c1..0045e4b 100644 --- a/app/resources/views/layout/body/scripts/rut.blade.php +++ b/app/resources/views/layout/body/scripts/rut.blade.php @@ -24,6 +24,12 @@ rut.replace(/\D/g, '') return rut.replace(/^(\d{1,2})(\d{3})(\d{3})$/, '$1.$2.$3') } + static clean(rut) { + if (!(typeof rut === 'string' || rut instanceof String)) { + rut = rut.toString() + } + return rut.replace(/\D/g, '') + } static validar(rut, digito) { if (!(typeof digito === 'string' || digito instanceof String)) { digito = digito.toString() diff --git a/app/resources/views/ventas/reservations.blade.php b/app/resources/views/ventas/reservations.blade.php new file mode 100644 index 0000000..a7f2cc4 --- /dev/null +++ b/app/resources/views/ventas/reservations.blade.php @@ -0,0 +1,664 @@ +@extends('layout.base') + +@section('page_title') + Cierres - Reservas +@endsection + +@section('page_content') +
+

Cierres - Reservas

+ +
+
Proyectos
+ @if (count($projects) == 0) +
+ No hay proyectos en venta. +
+ @else + + @endif +
+
+
+

+
+
+
+
+
+
+
+
+ + + +
+
+ +
+ + + + + + + + + + + + + +
UnidadesClienteFechaOferta¿Valida?Operador
+
+
+ + + + + + + + + + + + +
UnidadesClienteFechaOfertaOperador
+
+
+ + + + + + + + + + + + + +
UnidadesClienteFechaOfertaEstadoOperadorComentarios
+
+
+
+ + @include('ventas.reservations.modal.add') +@endsection + +@push('page_styles') + +@endpush + +@push('page_scripts') + +@endpush diff --git a/app/resources/views/ventas/reservations/modal/add.blade.php b/app/resources/views/ventas/reservations/modal/add.blade.php new file mode 100644 index 0000000..4c7fcf2 --- /dev/null +++ b/app/resources/views/ventas/reservations/modal/add.blade.php @@ -0,0 +1,758 @@ + + +@include('layout.body.scripts.rut') + +@push('page_scripts') + +@endpush diff --git a/app/src/Controller/API/Personas.php b/app/src/Controller/API/Personas.php new file mode 100644 index 0000000..ab424a9 --- /dev/null +++ b/app/src/Controller/API/Personas.php @@ -0,0 +1,34 @@ + $rut, + 'persona' => null, + 'success' => false + ]; + try { + $persona = $personaService->getById($rut); + $output['persona'] = $persona; + $output['success'] = true; + } catch (ServiceAction\Read $exception) { + $this->logger->error($exception->getMessage(), ['exception' => $exception]); + } + + return $this->withJson($response, $output); + } +} diff --git a/app/src/Controller/API/Proyectos.php b/app/src/Controller/API/Proyectos.php index e6994ec..d30adb8 100644 --- a/app/src/Controller/API/Proyectos.php +++ b/app/src/Controller/API/Proyectos.php @@ -187,5 +187,18 @@ class Proyectos } return $this->withJson($response, $output); } - + public function promotions(ServerRequestInterface $request, ResponseInterface $response, + Service\Venta\Promotion $promotionService, int $proyecto_id): ResponseInterface + { + $output = [ + 'project_id' => $proyecto_id, + 'promotions' => [] + ]; + try { + $output['promotions'] = $promotionService->getByProject($proyecto_id); + } catch (Read $exception) { + return $this->withError($response, $exception); + } + return $this->withJson($response, $output); + } } diff --git a/app/src/Controller/API/Ventas/Reservations.php b/app/src/Controller/API/Ventas/Reservations.php index 01c65db..607be0f 100644 --- a/app/src/Controller/API/Ventas/Reservations.php +++ b/app/src/Controller/API/Ventas/Reservations.php @@ -11,7 +11,8 @@ class Reservations { use withJson; - public function __invoke(ServerRequestInterface $request, ResponseInterface $response, Service\Venta\Reservation $reservationService): ResponseInterface + public function __invoke(ServerRequestInterface $request, ResponseInterface $response, + Service\Venta\Reservation $reservationService): ResponseInterface { $reservations = []; try { @@ -22,6 +23,18 @@ class Reservations return $this->withJson($response, compact('reservations')); } + public function getByProject(ServerRequestInterface $request, ResponseInterface $response, + Service\Venta\Reservation $reservationService, int $project_id): ResponseInterface + { + $reservations = []; + try { + $reservations = $reservationService->getByProject($project_id); + } catch (ServiceAction\Read $exception) { + return $this->withError($response, $exception); + } + + return $this->withJson($response, compact('reservations')); + } public function get(ServerRequestInterface $request, ResponseInterface $response, Service\Venta\Reservation $reservationService, int $reservation_id): ResponseInterface { $output = [ @@ -47,8 +60,8 @@ class Reservations 'partial' => false, 'errors' => [], ]; - - try { + var_dump($input); + /*try { $output['reservations'] []= [ 'reservation' => $reservationService->add($input), 'success' => true @@ -56,7 +69,7 @@ class Reservations $output['partial'] = true; } catch (ServiceAction\Create $exception) { $output['errors'] []= $this->parseError($exception); - } + }*/ if (count($input['reservations']) === count($output['reservations'])) { $output['success'] = true; @@ -64,6 +77,31 @@ class Reservations return $this->withJson($response, $output); } + public function addOne(ServerRequestInterface $request, ResponseInterface $response, Service\Venta\Reservation $reservationService): ResponseInterface + { + $input = $request->getParsedBody(); + $output = [ + 'input' => $input, + 'reservation' => null, + 'success' => false, + 'errors' => [], + ]; + $data = []; + foreach ($input as $key => $value) { + if (!str_starts_with($key, 'add_')) { + continue; + } + $data[substr($key, 4)] = $value; + } + try { + $output['reservation'] = $reservationService->add($data); + $output['success'] = true; + } catch (ServiceAction\Create $exception) { + $output['errors'] []= $this->parseError($exception); + } + + return $this->withJson($response, $output); + } public function edit(ServerRequestInterface $request, ResponseInterface $response, Service\Venta\Reservation $reservationService, int $reservation_id): ResponseInterface { $input = $request->getParsedBody(); @@ -100,4 +138,76 @@ class Reservations return $this->withJson($response, $output); } + + public function active(ServerRequestInterface $request, ResponseInterface $response, + Service\Venta\Reservation $reservationService, int $project_id): ResponseInterface + { + $output = [ + 'project_id' => $project_id, + 'reservations' => [], + 'success' => false, + ]; + try { + $output['reservations'] = $reservationService->getActive($project_id); + $output['success'] = true; + } catch (ServiceAction\Read) {} + return $this->withJson($response, $output); + } + public function pending(ServerRequestInterface $request, ResponseInterface $response, + Service\Venta\Reservation $reservationService, int $project_id): ResponseInterface + { + $output = [ + 'project_id' => $project_id, + 'reservations' => [], + 'success' => false, + ]; + try { + $output['reservations'] = $reservationService->getPending($project_id); + $output['success'] = true; + } catch (ServiceAction\Read) {} + return $this->withJson($response, $output); + } + public function rejected(ServerRequestInterface $request, ResponseInterface $response, + Service\Venta\Reservation $reservationService, int $project_id): ResponseInterface + { + $output = [ + 'project_id' => $project_id, + 'reservations' => [], + 'success' => false, + ]; + try { + $output['reservations'] = $reservationService->getRejected($project_id); + $output['success'] = true; + } catch (ServiceAction\Read) {} + return $this->withJson($response, $output); + } + + public function approve(ServerRequestInterface $request, ResponseInterface $response, + Service\Venta\Reservation $reservationService, int $reservation_id): ResponseInterface + { + $output = [ + 'reservation_id' => $reservation_id, + 'success' => false, + ]; + try { + $reservation = $reservationService->get($reservation_id); + $reservationService->approve($reservation); + $output['success'] = true; + } catch (ServiceAction\Read | ServiceAction\Update) {} + return $this->withJson($response, $output); + } + public function reject(ServerRequestInterface $request, ResponseInterface $response, + Service\Venta\Reservation $reservationService, int $reservation_id): ResponseInterface + { + $output = [ + 'reservation_id' => $reservation_id, + 'success' => false, + ]; + try { + $reservation = $reservationService->get($reservation_id); + $reservationService->reject($reservation); + $output['success'] = true; + } catch (ServiceAction\Read | ServiceAction\Update) {} + return $this->withJson($response, $output); + } } diff --git a/app/src/Controller/Ventas/Reservations.php b/app/src/Controller/Ventas/Reservations.php new file mode 100644 index 0000000..bcdac5c --- /dev/null +++ b/app/src/Controller/Ventas/Reservations.php @@ -0,0 +1,28 @@ +getVendibles('descripcion'); + } catch (Read) {} + $regions = []; + try { + $regions = $regionRepository->fetchAll(); + } catch (EmptyResult) {} + return $view->render($response, 'ventas.reservations', compact('projects', 'regions', 'project_id')); + } +} diff --git a/app/src/Exception/Model/InvalidState.php b/app/src/Exception/Model/InvalidState.php new file mode 100644 index 0000000..3c82782 --- /dev/null +++ b/app/src/Exception/Model/InvalidState.php @@ -0,0 +1,13 @@ +datos)) { - $this->datos = $this->runFactory('datos'); + try { + $this->datos = $this->runFactory('datos'); + } catch (EmptyResult) { + $this->datos = null; + } } return $this->datos; } public function __get(string $name): mixed { - if (property_exists($this, $name)) { - return $this->{$name}; - } if ($name === 'datos') { return $this->datos(); } if ($name === 'dv') { return $this->digito; } + if (property_exists($this, $name)) { + return $this->{$name}; + } throw new InvalidArgumentException("Property {$name} is not found in " . __CLASS__); } diff --git a/app/src/Model/Persona/Datos.php b/app/src/Model/Persona/Datos.php index 7158d39..3fa1df4 100644 --- a/app/src/Model/Persona/Datos.php +++ b/app/src/Model/Persona/Datos.php @@ -21,10 +21,11 @@ class Datos extends Ideal\Model public function jsonSerialize(): mixed { return [ + 'persona_rut' => $this->persona->rut, 'direccion' => $this->direccion ?? null, 'telefono' => $this->telefono ?? null, 'email' => $this->email ?? null, - 'fechaNacimiento' => $this->fechaNacimiento ?? null, + 'fechaNacimiento' => $this->fechaNacimiento?->format('Y-m-d') ?? null, 'sexo' => $this->sexo ?? null, 'estadoCivil' => $this->estadoCivil ?? null, 'nacionalidad' => $this->nacionalidad ?? null, diff --git a/app/src/Model/Proyecto/Broker.php b/app/src/Model/Proyecto/Broker.php index e55df74..342e668 100644 --- a/app/src/Model/Proyecto/Broker.php +++ b/app/src/Model/Proyecto/Broker.php @@ -1,7 +1,10 @@ contracts; } + public function getContract(Proyecto $proyecto, ?DateTimeInterface $date = null): ?Proyecto\Broker\Contract + { + if ($date === null) { + $date = new DateTimeImmutable(); + } + $contracts = $this->contracts(); + $valid = array_filter($contracts, fn(Proyecto\Broker\Contract $contract) => $contract->project->id === $proyecto->id); + $valid = array_filter($valid, fn(Proyecto\Broker\Contract $contract) => $contract->wasActive($date)); + if (count($valid) === 0) { + return null; + } + return last($valid); + } + + public function applyCommission(Proyecto $proyecto, float $price, ?DateTimeInterface $date = null): float + { + $contract = $this->getContract($proyecto, $date); + if ($contract === null) { + return $price; + } + return $contract->activate()->apply($proyecto, $price); + } + public function reverseCommission(Proyecto $proyecto, float $price, ?DateTimeInterface $date = null): float + { + $contract = $this->getContract($proyecto, $date); + if ($contract === null) { + return $price; + } + return $contract->activate()->reverse($proyecto, $price); + } public function rutFull(): string { diff --git a/app/src/Model/Proyecto/Broker/Contract.php b/app/src/Model/Proyecto/Broker/Contract.php index 89f6014..feb5625 100644 --- a/app/src/Model/Proyecto/Broker/Contract.php +++ b/app/src/Model/Proyecto/Broker/Contract.php @@ -1,6 +1,7 @@ promotions; } + public function isActive(): bool + { + $state = $this->current(); + return $state !== null and $state->isActive(); + } + public function activate(): self + { + $this->current()->activate(); + return $this; + } + public function wasActive(DateTimeInterface $date): bool + { + $states = $this->states(); + return array_any($states, fn($state) => $state->date <= $date); + } + + public function value(float $price): float + { + if (!$this->isActive()) { + return $price; + } + return $price * (1 + $this->commission); + } + public function inverse(float $price): float + { + if (!$this->isActive()) { + return $price; + } + return $price / (1 + $this->commission); + } + public function apply(Model\Proyecto $project, float $price): float + { + if (!$this->isActive() or $this->project->id !== $project->id) { + return $price; + } + return $this->value($price); + } + public function reverse(Model\Proyecto $project, float $price): float + { + if (!$this->isActive() or $this->project->id !== $project->id) { + return $price; + } + return $this->inverse($price); + } + protected function jsonComplement(): array { return [ diff --git a/app/src/Model/Proyecto/Broker/Contract/State.php b/app/src/Model/Proyecto/Broker/Contract/State.php index c773d6d..c3a982c 100644 --- a/app/src/Model/Proyecto/Broker/Contract/State.php +++ b/app/src/Model/Proyecto/Broker/Contract/State.php @@ -11,6 +11,16 @@ class State extends Common\Ideal\Model public DateTimeInterface $date; public State\Type $type; + public function isActive(): bool + { + return $this->type === State\Type::ACTIVE; + } + public function activate(): self + { + $this->type = State\Type::ACTIVE; + return $this; + } + protected function jsonComplement(): array { return [ diff --git a/app/src/Model/Venta/Datos.php b/app/src/Model/Venta/Datos.php index 58f4db5..f0931c4 100644 --- a/app/src/Model/Venta/Datos.php +++ b/app/src/Model/Venta/Datos.php @@ -1,6 +1,7 @@ $this->profesion ?? '', 'direccion' => $this->direccion ?? '', 'telefono' => $this->telefono ?? '', - 'email' => $this->email ?? '' + 'email' => $this->email ?? '', + 'fecha_nacimiento' => $this->fecha_nacimiento?->format('Y-m-d') ?? '', ]; } } diff --git a/app/src/Model/Venta/Promotion.php b/app/src/Model/Venta/Promotion.php index 8a1cb25..49b9676 100644 --- a/app/src/Model/Venta/Promotion.php +++ b/app/src/Model/Venta/Promotion.php @@ -3,6 +3,7 @@ namespace Incoviba\Model\Venta; use DateTimeInterface; use Incoviba\Common; +use Incoviba\Model\Proyecto; use Incoviba\Model\Proyecto\Broker; use Incoviba\Model\Venta\Promotion\State; use Incoviba\Model\Venta\Promotion\Type; @@ -61,13 +62,95 @@ class Promotion extends Common\Ideal\Model return $this->units; } + public function isActive(): bool + { + return $this->state === State::ACTIVE; + } + public function activate(): self + { + $this->state = State::ACTIVE; + return $this; + } + public function value(float $price): float { + if (!$this->isActive()) { + return $price; + } if ($this->type === Type::FIXED) { return $price + $this->amount; } return $price / (1 - $this->amount); } + public function inverse(float $price): float + { + if (!$this->isActive()) { + return $price; + } + if ($this->type === Type::FIXED) { + return $price - $this->amount; + } + return $price * (1 - $this->amount); + } + + public function apply(Unidad $unit, float $price, ?Broker $broker = null): float + { + if (!$this->isActive()) { + return $price; + } + $projectIds = array_map(fn(Proyecto $proyecto) => $proyecto->id, $this->projects()); + if (in_array($unit->proyectoTipoUnidad->proyecto->id, $projectIds)) { + return $this->value($price); + } + if ($broker !== null) { + $brokerIds = array_map(fn(Broker $broker) => $broker->id, $this->brokers()); + if (in_array($broker->id, $brokerIds)) { + return $this->value($price); + } + } + $typeIds = array_map(fn(Proyecto\TipoUnidad $type) => $type->id, $this->unitTypes()); + if (in_array($unit->proyectoTipoUnidad->tipoUnidad->id, $typeIds)) { + return $this->value($price); + } + $lineIds = array_map(fn(Proyecto\ProyectoTipoUnidad $line) => $line->id, $this->unitLines()); + if (in_array($unit->proyectoTipoUnidad->id, $lineIds)) { + return $this->value($price); + } + $unitIds = array_map(fn(Unidad $unit) => $unit->id, $this->units()); + if (in_array($unit->id, $unitIds)) { + return $this->value($price); + } + return $price; + } + public function reverse(Unidad $unit, float $price, ?Broker $broker = null): float + { + if (!$this->isActive()) { + return $price; + } + $projectIds = array_map(fn(Proyecto $proyecto) => $proyecto->id, $this->projects()); + if (in_array($unit->proyectoTipoUnidad->proyecto->id, $projectIds)) { + return $this->inverse($price); + } + if ($broker !== null) { + $brokerIds = array_map(fn(Broker $broker) => $broker->id, $this->brokers()); + if (in_array($broker->id, $brokerIds)) { + return $this->inverse($price); + } + } + $typeIds = array_map(fn(Proyecto\TipoUnidad $type) => $type->id, $this->unitTypes()); + if (in_array($unit->proyectoTipoUnidad->tipoUnidad->id, $typeIds)) { + return $this->inverse($price); + } + $lineIds = array_map(fn(Proyecto\ProyectoTipoUnidad $line) => $line->id, $this->unitLines()); + if (in_array($unit->proyectoTipoUnidad->id, $lineIds)) { + return $this->inverse($price); + } + $unitIds = array_map(fn(Unidad $unit) => $unit->id, $this->units()); + if (in_array($unit->id, $unitIds)) { + return $this->inverse($price); + } + return $price; + } protected function jsonComplement(): array { diff --git a/app/src/Model/Venta/Reservation.php b/app/src/Model/Venta/Reservation.php index 3a94ebf..cc777eb 100644 --- a/app/src/Model/Venta/Reservation.php +++ b/app/src/Model/Venta/Reservation.php @@ -4,19 +4,53 @@ namespace Incoviba\Model\Venta; use DateTimeInterface; use Incoviba\Common; use Incoviba\Model; +use InvalidArgumentException; class Reservation extends Common\Ideal\Model { + public Model\Proyecto $project; public Model\Persona $buyer; public DateTimeInterface $date; public array $units = []; public array $promotions = []; public ?Model\Proyecto\Broker $broker = null; + + public function offer(): float + { + return array_sum(array_column($this->units, 'value')); + } + public function withCommission(): float + { + $base = 0; + foreach ($this->units as $unit) { + foreach ($this->promotions as $promotion) { + $base += $promotion->activate()->reverse($unit['unit'], $unit['value'], $this->broker); + } + } + return $base; + } + public function base(): float + { + $base = $this->withCommission(); + if ($this->broker !== null) { + $base = $this->broker->reverseCommission($this->project, $base, $this->date); + } + return $base; + } + public function price(): float + { + $price = 0; + foreach ($this->units as $unit) { + $price += $unit->unit->precio($this->date)?->valor ?? 0; + } + return $price; + } + protected array $states = []; public function states(): array { - if (!isset($this->states)) { + if (count($this->states) === 0) { $this->states = $this->runFactory('states'); } return $this->states; @@ -27,7 +61,11 @@ class Reservation extends Common\Ideal\Model public function currentState(): Model\Venta\Reservation\State { if (!isset($this->currentState)) { - $this->currentState = last($this->states()); + $states = $this->states(); + if (count($states) === 0) { + throw new InvalidArgumentException('States must not be empty'); + } + $this->currentState = last($states); } return $this->currentState; } @@ -35,16 +73,15 @@ class Reservation extends Common\Ideal\Model public function addUnit(Model\Venta\Unidad $unit, float $value): self { if (($i = $this->findUnit($unit->id)) !== null) { - $this->units[$i]['value'] = $value; + $this->units[$i]->value = $value; return $this; } - $this->units[] = [ + $this->units[] = (object) [ 'unit' => $unit, 'value' => $value, ]; return $this; } - public function removeUnit(int $unit_id): self { if (($i = $this->findUnit($unit_id)) === null) { @@ -54,30 +91,47 @@ class Reservation extends Common\Ideal\Model $this->units = array_values($this->units); return $this; } - public function findUnit(int $unit_id): ?int { - foreach ($this->units as $idx => $unit) { - if ($unit['unit']->id == $unit_id) { - return $idx; - } - } - return null; + return array_find_key($this->units, fn($unit) => $unit->unit->id == $unit_id); } - public function hasUnit(int $unit_id): bool { return $this->findUnit($unit_id) !== null; } + public function summary(): string + { + $unitSummary = array_map(function($unit) { + $type = $unit->unit->proyectoTipoUnidad->tipoUnidad->descripcion; + $cap = strtoupper(strstr($type, 0, 1)); + return "{$cap}{$unit->unit->descripcion}"; + }, $this->units); + return implode('', $unitSummary); + } + + public function valid(): bool + { + $base = $this->base(); + $price = $this->price(); + return $base >= $price; + } + protected function jsonComplement(): array { return [ - 'buyer_rut' => $this->buyer->rut, + 'project_id' => $this->project->id, + 'buyer' => $this->buyer, 'date' => $this->date->format('Y-m-d'), 'units' => $this->units, 'promotions' => $this->promotions, - 'broker_rut' => $this->broker?->rut, + 'broker' => $this->broker, + 'offer' => $this->offer(), + 'with_commission' => $this->withCommission(), + 'base' => $this->base(), + 'price' => $this->price(), + 'valid' => $this->valid(), + 'summary' => $this->summary() ]; } } diff --git a/app/src/Model/Venta/Reservation/Detail/Type.php b/app/src/Model/Venta/Reservation/Detail/Type.php new file mode 100644 index 0000000..4129449 --- /dev/null +++ b/app/src/Model/Venta/Reservation/Detail/Type.php @@ -0,0 +1,17 @@ + $this->value, + 'description' => $this->name + ]; + } +} diff --git a/app/src/Model/Venta/Reservation/State.php b/app/src/Model/Venta/Reservation/State.php index a24771b..64eeed5 100644 --- a/app/src/Model/Venta/Reservation/State.php +++ b/app/src/Model/Venta/Reservation/State.php @@ -9,17 +9,14 @@ class State extends Common\Ideal\Model { public Model\Venta\Reservation $reservation; public DateTimeInterface $date; - public int $type; + public Model\Venta\Reservation\State\Type $type; protected function jsonComplement(): array { return [ 'reservation_id' => $this->reservation->id, 'date' => $this->date->format('Y-m-d'), - 'type' => [ - 'id' => $this->type, - 'description' => State\Type::name($this->type) - ] + 'type' => $this->type ]; } -} \ No newline at end of file +} diff --git a/app/src/Model/Venta/Reservation/State/Type.php b/app/src/Model/Venta/Reservation/State/Type.php index 464ec24..96d60e5 100644 --- a/app/src/Model/Venta/Reservation/State/Type.php +++ b/app/src/Model/Venta/Reservation/State/Type.php @@ -3,17 +3,21 @@ namespace Incoviba\Model\Venta\Reservation\State; enum Type: int { - case ACTIVE = 1; case INACTIVE = 0; + case ACTIVE = 1; + case PROMISED = 2; case REJECTED = -1; + case CANCELLED = -2; - public static function name(int $type): string + public function jsonSerialize(): array { - return match ($type) { - self::ACTIVE => 'active', - self::INACTIVE => 'inactive', - self::REJECTED => 'rejected', - default => throw new \InvalidArgumentException('Unexpected match value') - }; + return [ + 'id' => $this->value, + 'description' => $this->name + ]; } -} \ No newline at end of file + public static function getTypes(): array + { + return [self::ACTIVE->value, self::INACTIVE->value, self::REJECTED->value]; + } +} diff --git a/app/src/Repository/Persona/Datos.php b/app/src/Repository/Persona/Datos.php index bfec12b..06cd4b7 100644 --- a/app/src/Repository/Persona/Datos.php +++ b/app/src/Repository/Persona/Datos.php @@ -29,6 +29,9 @@ class Datos extends Ideal\Repository ->register('direccion_id', (new Implement\Repository\Mapper()) ->setProperty('direccion') ->setFunction(function($data) { + if ($data['direccion_id'] === null) { + return null; + } return $this->direccionRepository->fetchById($data['direccion_id']); })->setDefault(null)) ->register('telefono', (new Implement\Repository\Mapper())->setFunction(function($data) { @@ -56,7 +59,7 @@ class Datos extends Ideal\Repository } public function save(Define\Model $model): Model\Persona\Datos { - $this->saveNew([ + $model->id = $this->saveNew([ 'persona_rut', 'direccion_id', 'telefono', 'email', 'fecha_nacimiento', 'sexo', 'estado_civil', 'nacionalidad', 'ocupacion' ], [ @@ -113,4 +116,11 @@ class Datos extends Ideal\Repository } $this->connection->execute($query, $flattened); } + + public function filterData(array $data): array + { + $fields = ['persona_rut', 'direccion_id', 'telefono', 'email', 'fecha_nacimiento', 'sexo', 'estado_civil', + 'nacionalidad', 'ocupacion']; + return array_intersect_key($data, array_flip($fields)); + } } diff --git a/app/src/Repository/Proyecto/Broker/Contract.php b/app/src/Repository/Proyecto/Broker/Contract.php index 66d1bd5..221e74e 100644 --- a/app/src/Repository/Proyecto/Broker/Contract.php +++ b/app/src/Repository/Proyecto/Broker/Contract.php @@ -93,7 +93,7 @@ class Contract extends Common\Ideal\Repository ->select('a.*') ->from("{$this->getTable()} a") ->joined($this->statusJoin()) - ->where('a.broker_rut = :broker_rut AND bcs.state = :state'); + ->where('a.broker_rut = :broker_rut AND bcs.type = :state'); return $this->fetchMany($query, ['broker_rut' => $brokerRut, 'state' => Model\Proyecto\Broker\Contract\Type::ACTIVE]); } @@ -108,8 +108,8 @@ class Contract extends Common\Ideal\Repository ->select('a.*') ->from("{$this->getTable()} a") ->joined($this->statusJoin()) - ->where('a.proyecto_id = :proyecto_id AND bcs.state = :state'); - return $this->fetchMany($query, ['proyecto_id' => $projectId, 'state' => Model\Proyecto\Broker\Contract\State\Type::ACTIVE->value]); + ->where('a.project_id = :project_id AND bcs.type = :state'); + return $this->fetchMany($query, ['project_id' => $projectId, 'state' => Model\Proyecto\Broker\Contract\State\Type::ACTIVE->value]); } /** diff --git a/app/src/Repository/Venta/EstadoPrecio.php b/app/src/Repository/Venta/EstadoPrecio.php index 879ff05..231e1b4 100644 --- a/app/src/Repository/Venta/EstadoPrecio.php +++ b/app/src/Repository/Venta/EstadoPrecio.php @@ -54,8 +54,6 @@ class EstadoPrecio extends Ideal\Repository public function fetchByPrecio(int $precio_id): array { $query = "SELECT * FROM `{$this->getTable()}` WHERE `precio` = ?"; - error_log($query.PHP_EOL,3,'/logs/query.log'); - error_log($precio_id.PHP_EOL,3,'/logs/query.log'); return $this->fetchMany($query, [$precio_id]); } diff --git a/app/src/Repository/Venta/Promotion.php b/app/src/Repository/Venta/Promotion.php index 136b4aa..0b20bff 100644 --- a/app/src/Repository/Venta/Promotion.php +++ b/app/src/Repository/Venta/Promotion.php @@ -212,7 +212,7 @@ class Promotion extends Common\Ideal\Repository ->joined('LEFT OUTER JOIN proyecto_tipo_unidad ptu1 ON ptu.id = unidad.pt') ->where('(pp.project_id = :project_id OR put.project_id = :project_id OR ptu.proyecto = :project_id OR ptu1.proyecto = :project_id) AND a.state = :state') ->group('a.id'); - return $this->fetchMany($query, ['project_id' => $project_id, 'state' => Model\Venta\Promotion\State::ACTIVE]); + return $this->fetchMany($query, ['project_id' => $project_id, 'state' => Model\Venta\Promotion\State::ACTIVE->value]); } /** diff --git a/app/src/Repository/Venta/Reservation.php b/app/src/Repository/Venta/Reservation.php index 8ff13c7..45f005f 100644 --- a/app/src/Repository/Venta/Reservation.php +++ b/app/src/Repository/Venta/Reservation.php @@ -3,14 +3,19 @@ namespace Incoviba\Repository\Venta; use DateTimeInterface; use DateInterval; +use Incoviba\Common\Define; +use Incoviba\Exception\Model\InvalidState; use PDO; use Incoviba\Common; use Incoviba\Model; use Incoviba\Repository; +use PDOException; class Reservation extends Common\Ideal\Repository { - public function __construct(Common\Define\Connection $connection, protected Repository\Persona $personaRepository, + public function __construct(Common\Define\Connection $connection, + protected Repository\Proyecto $proyectoRepository, + protected Repository\Persona $personaRepository, protected Repository\Proyecto\Broker $brokerRepository, protected Unidad $unitRepository, protected Promotion $promotionRepository) { @@ -25,37 +30,35 @@ class Reservation extends Common\Ideal\Repository public function create(?array $data = null): Model\Venta\Reservation { $map = (new Common\Implement\Repository\MapperParser()) + ->register('project_id', (new Common\Implement\Repository\Mapper()) + ->setProperty('project') + ->setFunction(function($data) { + return $this->proyectoRepository->fetchById($data['project_id']); + })) ->register('buyer_rut', (new Common\Implement\Repository\Mapper()) ->setProperty('buyer') - ->setFunction(function($data) use ($data) { + ->setFunction(function($data) { return $this->personaRepository->fetchById($data['buyer_rut']); })) - ->register('date', new Common\Implement\Repository\Mapper\DateTime('date')) - ->register('broker_rut', (new Common\Implement\Repository\Mapper()) - ->setProperty('broker') - ->setDefault(null) - ->setFunction(function($data) use ($data) { - try { - return $this->brokerRepository->fetchById($data['broker_rut']); - } catch (Common\Implement\Exception\EmptyResult) { - return null; - } - })); + ->register('date', new Common\Implement\Repository\Mapper\DateTime('date')); return $this->parseData(new Model\Venta\Reservation(), $data, $map); } public function save(Common\Define\Model $model): Model\Venta\Reservation { - $model->id = $this->saveNew([ - 'buyer_rut', - 'date', - 'broker_rut' - ], [ - $model->buyer->rut, - $model->date->format('Y-m-d'), - $model->broker?->rut - ]); + if (!isset($model->id)) { + $model->id = $this->saveNew([ + 'project_id', + 'buyer_rut', + 'date' + ], [ + $model->project->id, + $model->buyer->rut, + $model->date->format('Y-m-d') + ]); + } $this->saveUnits($model); $this->savePromotions($model); + $this->saveBroker($model); return $model; } @@ -67,15 +70,22 @@ class Reservation extends Common\Ideal\Repository */ public function edit(Common\Define\Model $model, array $new_data): Model\Venta\Reservation { - return $this->update($model, ['buyer_rut', 'date', 'broker_rut'], $new_data); + $model = $this->update($model, ['project_id', 'buyer_rut', 'date'], $new_data); + $this->editUnits($model, $new_data); + $this->editPromotions($model, $new_data); + $this->editBroker($model, $new_data); + return $model; } public function load(array $data_row): Model\Venta\Reservation { $model = parent::load($data_row); - $this->fetchUnits($model); - $this->fetchPromotions($model); + $this->fetchUnits($model, $data_row); + try { + $this->fetchBroker($model, $data_row); + } catch (Common\Implement\Exception\EmptyResult) {} + $this->fetchPromotions($model, $data_row); return $model; } @@ -93,6 +103,88 @@ class Reservation extends Common\Ideal\Repository ->where('buyer_rut = :buyer_rut AND date >= :date'); return $this->fetchOne($query, ['buyer_rut' => $buyer_rut, 'date' => $date->sub(new DateInterval('P10D'))->format('Y-m-d')]); } + public function fetchByProject(int $project_id): array + { + $query = $this->connection->getQueryBuilder() + ->select() + ->from('reservations') + ->where('project_id = :project_id'); + return $this->fetchMany($query, ['project_id' => $project_id]); + } + + /** + * @param int $project_id + * @param int $state + * @return array + * @throws Common\Implement\Exception\EmptyResult + * @throws InvalidState + */ + public function fetchState(int $project_id, int $state): array + { + if (!in_array($state, Model\Venta\Reservation\State\Type::getTypes())) { + throw new InvalidState(); + } + $sub1 = $this->connection->getQueryBuilder() + ->select('MAX(id) AS id, reservation_id') + ->from('reservation_states') + ->group('reservation_id'); + $sub2 = $this->connection->getQueryBuilder() + ->select('er1.*') + ->from('reservation_states er1') + ->joined("INNER JOIN ({$sub1}) er0 ON er0.id = er1.id"); + + $query = $this->connection->getQueryBuilder() + ->select('a.*') + ->from("{$this->getTable()} a") + ->joined("INNER JOIN ({$sub2}) er ON er.reservation_id = a.id") + ->where('a.project_id = :project_id AND er.type = :state'); + return $this->fetchMany($query, ['project_id' => $project_id, + 'state' => $state]); + } + /** + * @param int $project_id + * @return array + * @throws Common\Implement\Exception\EmptyResult + */ + public function fetchActive(int $project_id): array + { + try { + return $this->fetchState($project_id, Model\Venta\Reservation\State\Type::ACTIVE->value); + } catch (InvalidState $exception) { + throw new Common\Implement\Exception\EmptyResult('Select active reservations', $exception); + } + } + + /** + * @param int $project_id + * @return array + * @throws Common\Implement\Exception\EmptyResult + */ + public function fetchPending(int $project_id): array + { + try { + return $this->fetchState($project_id, Model\Venta\Reservation\State\Type::INACTIVE->value); + } catch (InvalidState $exception) { + throw new Common\Implement\Exception\EmptyResult('Select pending reservations', $exception); + } + } + + /** + * @param int $project_id + * @return array + * @throws Common\Implement\Exception\EmptyResult + */ + public function fetchRejected(int $project_id): array + { + try { + return [ + ...$this->fetchState($project_id, Model\Venta\Reservation\State\Type::REJECTED->value), + ...$this->fetchState($project_id, Model\Venta\Reservation\State\Type::CANCELLED->value) + ]; + } catch (InvalidState $exception) { + throw new Common\Implement\Exception\EmptyResult('Select rejected reservations', $exception); + } + } protected function saveUnits(Model\Venta\Reservation $reservation): void { @@ -101,22 +193,74 @@ class Reservation extends Common\Ideal\Repository } $queryCheck = $this->connection->getQueryBuilder() ->select('COUNT(id) AS cnt') - ->from('reservation_data') - ->where('reservation_id = :id AND type = "Unit" AND reference_id = :unit_id'); + ->from('reservation_details') + ->where('reservation_id = :id AND type = :type AND reference_id = :unit_id'); $statementCheck = $this->connection->prepare($queryCheck); $queryInsert = $this->connection->getQueryBuilder() ->insert() - ->into('reservation_data') + ->into('reservation_details') ->columns(['reservation_id', 'type', 'reference_id', 'value']) ->values([':reservation_id', ':type', ':reference_id', ':value']); $statementInsert = $this->connection->prepare($queryInsert); foreach ($reservation->units as $unit) { - $statementCheck->execute(['id' => $reservation->id, 'unit_id' => $unit['unit']->id]); + $statementCheck->execute([ + 'id' => $reservation->id, + 'type' => Model\Venta\Reservation\Detail\Type::Unit->value, + 'unit_id' => $unit->unit->id + ]); $result = $statementCheck->fetch(PDO::FETCH_ASSOC); if ($result['cnt'] > 0) { continue; } - $statementInsert->execute(['reservation_id' => $reservation->id, 'type' => 'Unit', 'reference_id' => $unit['unit']->id, 'value' => $unit['value']]); + $statementInsert->execute([ + 'reservation_id' => $reservation->id, + 'type' => Model\Venta\Reservation\Detail\Type::Unit->value, + 'reference_id' => $unit->unit->id, + 'value' => $unit->value + ]); + } + + $this->cleanUpUnits($reservation); + } + protected function cleanUpUnits(Model\Venta\Reservation $reservation): void + { + $this->cleanUpDetails($reservation, + Model\Venta\Reservation\Detail\Type::Unit->value, + array_map(fn($unit) => $unit->unit->id, $reservation->units)); + } + protected function saveBroker(Model\Venta\Reservation &$reservation): void + { + if ($reservation->broker === null) { + $this->removeBroker($reservation); + return; + } + try { + $queryCheck = $this->connection->getQueryBuilder() + ->select('reference_id') + ->from('reservation_details') + ->where('reservation_id = :id AND type = :type'); + $statementCheck = $this->connection->execute($queryCheck, [ + 'id' => $reservation->id, + 'type' => Model\Venta\Reservation\Detail\Type::Broker->value + ]); + $result = $statementCheck->fetch(PDO::FETCH_ASSOC); + if ($result === false) { + throw new PDOException(); + } + $new_id = $reservation->broker->rut; + $reservation->broker = $this->brokerRepository->fetchById($result['reference_id']); + $this->editBroker($reservation, ['broker_rut' => $new_id]); + } catch (PDOException) { + $queryInsert = $this->connection->getQueryBuilder() + ->insert() + ->into('reservation_details') + ->columns(['reservation_id', 'type', 'reference_id']) + ->values([':reservation_id', ':type', ':reference_id']); + $this->connection->execute($queryInsert, [ + 'reservation_id' => $reservation->id, + 'type' => Model\Venta\Reservation\Detail\Type::Broker->value, + 'reference_id' => $reservation->broker->rut + ]); } } protected function savePromotions(Model\Venta\Reservation $reservation): void @@ -126,60 +270,152 @@ class Reservation extends Common\Ideal\Repository } $queryCheck = $this->connection->getQueryBuilder() ->select('COUNT(id) AS cnt') - ->from('reservation_data') - ->where('reservation_id = :id AND type = "Promotion" AND reference_id = :promotion_id'); + ->from('reservation_details') + ->where('reservation_id = :id AND type = :type AND reference_id = :promotion_id'); $statementCheck = $this->connection->prepare($queryCheck); $queryInsert = $this->connection->getQueryBuilder() ->insert() - ->into('reservation_data') + ->into('reservation_details') ->columns(['reservation_id', 'type', 'reference_id']) ->values([':reservation_id', ':type', ':reference_id']); $statementInsert = $this->connection->prepare($queryInsert); foreach ($reservation->promotions as $promotion) { - $statementCheck->execute(['id' => $reservation->id, 'promotion_id' => $promotion->id]); + $statementCheck->execute([ + 'id' => $reservation->id, + 'type' => Model\Venta\Reservation\Detail\Type::Promotion->value, + 'promotion_id' => $promotion->id + ]); $result = $statementCheck->fetch(PDO::FETCH_ASSOC); if ($result['cnt'] > 0) { continue; } - $statementInsert->execute(['reservation_id' => $reservation->id, 'type' => 'Promotion', 'reference_id' => $promotion->id]); + $statementInsert->execute([ + 'reservation_id' => $reservation->id, + 'type' => Model\Venta\Reservation\Detail\Type::Promotion->value, + 'reference_id' => $promotion->id + ]); } + + $this->cleanUpPromotions($reservation); } - protected function editUnits(Model\Venta\Reservation $reservation, array $new_data): void + protected function cleanUpPromotions(Model\Venta\Reservation $reservation): void + { + $this->cleanUpDetails($reservation, + Model\Venta\Reservation\Detail\Type::Promotion->value, + array_map(fn($promotion) => $promotion->id, $reservation->promotions)); + } + protected function cleanUpDetails(Model\Venta\Reservation $reservation, int $type_id, array $currentIds): void + { + $queryCheck = $this->connection->getQueryBuilder() + ->select('COUNT(id) AS cnt') + ->from('reservation_details') + ->where('reservation_id = :id AND type = :type'); + $statementCheck = $this->connection->prepare($queryCheck); + $deleteParam = implode(', ', array_map(fn($id) => ":id$id", $currentIds)); + $queryDelete = $this->connection->getQueryBuilder() + ->delete() + ->from('reservation_details') + ->where("reservation_id = :id AND type = :type AND reference_id NOT IN ({$deleteParam})"); + $statementDelete = $this->connection->prepare($queryDelete); + + try { + $statementCheck->execute([ + 'id' => $reservation->id, + 'type' => $type_id + ]); + $result = $statementCheck->fetch(PDO::FETCH_ASSOC); + if ($result['cnt'] <= count($currentIds)) { + return; + } + $deleteIdValues = array_combine(array_map(fn($id) => "id$id", $currentIds), $currentIds); + $statementDelete->execute(array_merge( + ['id' => $reservation->id, 'type' => $type_id], + $deleteIdValues)); + } catch (PDOException) {} + } + + protected function editUnits(Model\Venta\Reservation &$reservation, array $new_data): void { $querySelect = $this->connection->getQueryBuilder() ->select() - ->from('reservation_data') - ->where('reservation_id = :id AND type = "Unit" AND reference_id = :unit_id'); + ->from('reservation_details') + ->where('reservation_id = :id AND type = :type AND reference_id = :unit_id'); $statementSelect = $this->connection->prepare($querySelect); $queryUpdate = $this->connection->getQueryBuilder() - ->update('reservation_data') + ->update('reservation_details') ->set('value = :value') - ->where('reservation_id = :id AND type = "Unit" AND reference_id = :unit_id'); + ->where('reservation_id = :id AND type = :type AND reference_id = :unit_id'); $statementUpdate = $this->connection->prepare($queryUpdate); - foreach ($new_data as $unit_id => $value) { - $idx = $reservation->findUnit($unit_id); + $queryInsert = $this->connection->getQueryBuilder() + ->insert() + ->into('reservation_details') + ->columns(['reservation_id', 'type', 'reference_id', 'value']) + ->values([':reservation_id', ':type', ':reference_id', ':value']); + $statementInsert = $this->connection->prepare($queryInsert); + foreach ($new_data['units'] as $unit) { + $idx = $reservation->findUnit($unit['unit_id']); if ($idx === null) { - $reservation->addUnit($this->unitRepository->fetchById($unit_id), $value); + $reservation->addUnit($this->unitRepository->fetchById($unit['unit_id']), $unit['value']); + $statementInsert->execute([ + 'reservation_id' => $reservation->id, + 'type' => Model\Venta\Reservation\Detail\Type::Unit->value, + 'reference_id' => $unit['unit_id'], + 'value' => $unit['value'] + ]); continue; } - $statementSelect->execute(['id' => $reservation->id, 'unit_id' => $unit_id]); + $statementSelect->execute([ + 'id' => $reservation->id, + 'type' => Model\Venta\Reservation\Detail\Type::Unit->value, + 'unit_id' => $unit['unit_id'] + ]); $result = $statementSelect->fetch(PDO::FETCH_ASSOC); if (!$result) { + $reservation->addUnit($this->unitRepository->fetchById($unit['unit_id']), $unit['value']); + $statementInsert->execute([ + 'reservation_id' => $reservation->id, + 'type' => Model\Venta\Reservation\Detail\Type::Unit->value, + 'reference_id' => $unit['unit_id'], + 'value' => $unit['value'] + ]); continue; } - $statementUpdate->execute(['id' => $reservation->id, 'unit_id' => $unit_id, 'value' => $value]); - $reservation->units[$idx]['value'] = $value; + $statementUpdate->execute([ + 'id' => $reservation->id, + 'type' => Model\Venta\Reservation\Detail\Type::Unit->value, + 'unit_id' => $unit['unit_id'], + 'value' => $unit['value'] + ]); + $reservation->units[$idx]['value'] = $unit['value']; } } - protected function editPromotions(Model\Venta\Reservation $reservation, array $new_data): void + protected function editBroker(Model\Venta\Reservation &$reservation, array $new_data): void + { + if (!array_key_exists('broker_rut', $new_data) or $new_data['broker_rut'] === $reservation->broker->rut) { + return; + } + try { + $query = $this->connection->getQueryBuilder() + ->update('reservation_details') + ->set('reference_id = :broker_rut') + ->where('reservation_id = :id AND type = :type'); + $this->connection->execute($query, [ + 'id' => $reservation->id, + 'type' => Model\Venta\Reservation\Detail\Type::Broker->value, + 'broker_rut' => $new_data['broker_rut'] + ]); + $reservation->broker = $this->brokerRepository->fetchById($new_data['broker_rut']); + } catch (PDOException) {} + } + protected function editPromotions(Model\Venta\Reservation &$reservation, array $new_data): void { $querySelect = $this->connection->getQueryBuilder() ->select() - ->from('reservation_data') + ->from('reservation_details') ->where('reservation_id = :id AND type = "Promotion" AND reference_id = :promotion_id'); $statementSelect = $this->connection->prepare($querySelect); $queryUpdate = $this->connection->getQueryBuilder() - ->update('reservation_data') + ->update('reservation_details') ->set('value = :value') ->where('reservation_id = :id AND type = "Promotion" AND reference_id = :promotion_id'); $statementUpdate = $this->connection->prepare($queryUpdate); @@ -198,13 +434,26 @@ class Reservation extends Common\Ideal\Repository $reservation->promotions[$idx] = $this->promotionRepository->fetchById($promotion_id); } } - protected function fetchUnits(Model\Venta\Reservation &$reservation): Model\Venta\Reservation + + protected function fetchUnits(Model\Venta\Reservation &$reservation, array $new_data): Model\Venta\Reservation + { + $this->fetchSavedUnits($reservation); + + if (array_key_exists('units', $new_data) and count($new_data['units']) > 0) { + $this->fetchUnsavedUnits($reservation, $new_data); + } + return $reservation; + } + protected function fetchSavedUnits(Model\Venta\Reservation &$reservation): Model\Venta\Reservation { $query = $this->connection->getQueryBuilder() ->select() - ->from('reservation_data') - ->where('reservation_id = :id AND type = "Unit"'); - $statement = $this->connection->execute($query, ['id' => $reservation->id]); + ->from('reservation_details') + ->where('reservation_id = :id AND type = :type'); + $statement = $this->connection->execute($query, [ + 'id' => $reservation->id, + 'type' =>Model\Venta\Reservation\Detail\Type::Unit->value + ]); while ($result = $statement->fetch(PDO::FETCH_ASSOC)) { try { @@ -213,13 +462,79 @@ class Reservation extends Common\Ideal\Repository } return $reservation; } - protected function fetchPromotions(Model\Venta\Reservation $reservation): Model\Venta\Reservation + protected function fetchUnsavedUnits(Model\Venta\Reservation &$reservation, array $new_data): Model\Venta\Reservation + { + if (!array_key_exists('units', $new_data) or count($new_data['units']) > 0) { + return $reservation; + } + $queryCheck = $this->connection->getQueryBuilder() + ->select('COUNT(id) AS cnt') + ->from('reservation_details') + ->where('reservation_id = :id AND type = :type AND reference_id = :unit_id'); + $statementCheck = $this->connection->prepare($queryCheck); + foreach ($new_data['units'] as $unit) { + $statementCheck->execute([ + 'id' => $reservation->id, + 'type' => Model\Venta\Reservation\Detail\Type::Unit->value, + 'unit_id' => $unit['unit_id'] + ]); + $result = $statementCheck->fetch(PDO::FETCH_ASSOC); + if ($result['cnt'] > 0) { + continue; + } + try { + $reservation->addUnit($this->unitRepository->fetchById($unit['unit_id']), $unit['value']); + } catch (Common\Implement\Exception\EmptyResult) {} + } + return $reservation; + } + + /** + * @param Model\Venta\Reservation $reservation + * @param array $new_data + * @return Model\Venta\Reservation + * @throws Common\Implement\Exception\EmptyResult + */ + protected function fetchBroker(Model\Venta\Reservation &$reservation, array $new_data): Model\Venta\Reservation + { + if (!array_key_exists('broker_id', $new_data)) { + $query = $this->connection->getQueryBuilder() + ->select('reference_id') + ->from('reservation_details') + ->where('reservation_id = :id AND type = :type'); + try { + $statement =$this->connection->execute($query, [ + 'id' => $reservation->id, + 'type' => Model\Venta\Reservation\Detail\Type::Broker->value + ]); + $result = $statement->fetch(PDO::FETCH_ASSOC); + if ($result === false) { + throw new Common\Implement\Exception\EmptyResult($query); + } + $reservation->broker = $this->brokerRepository->fetchById($result['reference_id']); + } catch (PDOException) {} + + return $reservation; + } + try { + $reservation->broker = $this->brokerRepository->fetchById($new_data['broker_id']); + } catch (Common\Implement\Exception\EmptyResult) {} + return $reservation; + } + protected function fetchPromotions(Model\Venta\Reservation &$reservation, array $new_data): Model\Venta\Reservation + { + $this->fetchSavedPromotions($reservation); + $this->fetchUnsavedPromotions($reservation, $new_data); + return $reservation; + } + protected function fetchSavedPromotions(Model\Venta\Reservation &$reservation): Model\Venta\Reservation { $query = $this->connection->getQueryBuilder() ->select() - ->from('reservation_data') - ->where('type = "Promotion" AND reservation_id = :id'); - $statement = $this->connection->execute($query, ['id' => $reservation->id]); + ->from('reservation_details') + ->where('type = :type AND reservation_id = :id'); + $statement = $this->connection->execute($query, + ['id' => $reservation->id, 'type' => Model\Venta\Reservation\Detail\Type::Promotion->value]); while ($result = $statement->fetch(PDO::FETCH_ASSOC)) { try { $reservation->promotions []= $this->promotionRepository->fetchById($result['reference_id']); @@ -227,4 +542,43 @@ class Reservation extends Common\Ideal\Repository } return $reservation; } + protected function fetchUnsavedPromotions(Model\Venta\Reservation &$reservation, array $new_data): Model\Venta\Reservation + { + if (!array_key_exists('promotions', $new_data) or count($new_data['promotions']) > 0) { + return $reservation; + } + $queryCheck = $this->connection->getQueryBuilder() + ->select('COUNT(id) AS cnt') + ->from('reservation_details') + ->where('type = :type AND reservation_id = :id AND reference_id = :promotion_id'); + $statementCheck = $this->connection->prepare($queryCheck); + foreach ($new_data['promotions'] as $promotion) { + $statementCheck->execute([ + 'id' => $reservation->id, + 'promotion_id' => $promotion + ]); + $result = $statementCheck->fetch(PDO::FETCH_ASSOC); + if ($result['cnt'] > 0) { + continue; + } + try { + $reservation->promotions []= $this->promotionRepository->fetchById($promotion); + } catch (Common\Implement\Exception\EmptyResult) {} + } + return $reservation; + } + protected function removeBroker(Model\Venta\Reservation &$reservation): void + { + try { + $query = $this->connection->getQueryBuilder() + ->delete() + ->from('reservation_details') + ->where('reservation_id = :id AND type = :type'); + $this->connection->execute($query, [ + 'id' => $reservation->id, + 'type' => Model\Venta\Reservation\Detail\Type::Broker->value + ]); + $reservation->broker = null; + } catch (PDOException) {} + } } diff --git a/app/src/Repository/Venta/Reservation/State.php b/app/src/Repository/Venta/Reservation/State.php index 0a98a6d..841a849 100644 --- a/app/src/Repository/Venta/Reservation/State.php +++ b/app/src/Repository/Venta/Reservation/State.php @@ -7,7 +7,8 @@ use Incoviba\Repository; class State extends Common\Ideal\Repository { - public function __construct(Common\Define\Connection $connection, protected Repository\Venta\Reservation $reservationRepository) + public function __construct(Common\Define\Connection $connection, + protected Repository\Venta\Reservation $reservationRepository) { parent::__construct($connection); } @@ -19,12 +20,16 @@ class State extends Common\Ideal\Repository public function create(?array $data = null): Model\Venta\Reservation\State { - $map = (new Common\Implement\Repository\MapperParser(['type'])) + $map = (new Common\Implement\Repository\MapperParser()) ->register('reservation_id', (new Common\Implement\Repository\Mapper()) ->setProperty('reservation') - ->setFunction(function($data) use ($data) { + ->setFunction(function($data) { return $this->reservationRepository->fetchById($data['reservation_id']); })) + ->register('type', (new Common\Implement\Repository\Mapper()) + ->setFunction(function($data) { + return Model\Venta\Reservation\State\Type::from($data['type']); + })) ->register('date', new Common\Implement\Repository\Mapper\DateTime('date')); return $this->parseData(new Model\Venta\Reservation\State(), $data, $map); } @@ -32,7 +37,7 @@ class State extends Common\Ideal\Repository { $model->id = $this->saveNew( ['reservation_id', 'date', 'type'], - [$model->reservation->id, $model->date->format('Y-m-d'), $model->type] + [$model->reservation->id, $model->date->format('Y-m-d'), $model->type->value] ); return $model; } diff --git a/app/src/Service/Direccion.php b/app/src/Service/Direccion.php new file mode 100644 index 0000000..7113654 --- /dev/null +++ b/app/src/Service/Direccion.php @@ -0,0 +1,49 @@ + 'calle', + 'number' => 'numero', + 'comuna_id' => 'comuna', + ]; + foreach ($data as $key => $value) { + if (array_key_exists($key, $dataMap)) { + $data[$dataMap[$key]] = $value; + unset($data[$key]); + } + } + try { + return $this->direccionRepository->fetchByCalleAndNumeroAndExtraAndComuna($data['calle'], $data['numero'], $data['extra'], $data['comuna']); + } catch (Implement\Exception\EmptyResult) { + $filteredData = $this->direccionRepository->filterData($data); + $direccion = $this->direccionRepository->create($filteredData); + try { + return $this->direccionRepository->save($direccion); + } catch (PDOException $exception) { + throw new ServiceAction\Create(__CLASS__, $exception); + } + } + } +} diff --git a/app/src/Service/Persona.php b/app/src/Service/Persona.php index bfabeca..e41a8a6 100644 --- a/app/src/Service/Persona.php +++ b/app/src/Service/Persona.php @@ -15,7 +15,8 @@ class Persona extends Ideal\Service public function __construct(LoggerInterface $logger, protected Repository\Persona $personaRepository, protected Repository\Persona\Datos $datosPersonaRepository, - protected Repository\Venta\Propietario $propietarioRepository) + protected Repository\Venta\Propietario $propietarioRepository, + protected Direccion $direccionService) { parent::__construct($logger); } @@ -35,7 +36,7 @@ class Persona extends Ideal\Service /** * @param int $rut * @return Model\Persona - * @throws Read|Create + * @throws Read */ public function getById(int $rut): Model\Persona { @@ -44,7 +45,11 @@ class Persona extends Ideal\Service } catch (Implement\Exception\EmptyResult) { try { $this->propietarioRepository->fetchById($rut); - return $this->add(compact('rut')); + try { + return $this->add(compact('rut')); + } catch (Create $exception) { + throw new Read(__CLASS__, $exception); + } } catch (Implement\Exception\EmptyResult $exception) { throw new Read(__CLASS__, $exception); } @@ -63,44 +68,34 @@ class Persona extends Ideal\Service } catch (Implement\Exception\EmptyResult) { try { $propietario = $this->propietarioRepository->fetchById($data['rut']); - } catch (Implement\Exception\EmptyResult $exception) { - throw new Create(__CLASS__, $exception); - } - $data['rut'] = $propietario->rut; - $data['digito'] = $propietario->dv; - $data['nombres'] = $propietario->nombres; - $data['apellido_paterno'] = $propietario->apellidos['paterno']; - $data['apellido_materno'] = $propietario->apellidos['materno'] ?? ''; - $persona = $this->personaRepository->create($data); - try { - $persona = $this->personaRepository->save($persona); - } catch (PDOException $exception) { - throw new Create(__CLASS__, $exception); - } - } - if (isset($data['direccion_id']) or isset($data['email']) or isset($data['telefono'])) { - $datosData = ['persona_rut' => $persona->rut]; - if (isset($data['direccion_id'])) { - $datosData['direccion_id'] = $data['direccion_id']; - } - if (isset($data['email'])) { - $datosData['email'] = $data['email']; - } - if (isset($data['telefono'])) { - $datosData['telefono'] = $data['telefono']; - } - try { - $datos = $this->datosPersonaRepository->fetchByPersona($persona->rut); - $this->datosPersonaRepository->edit($datos, $data); + $persona = $this->addFromPropietario($propietario); } catch (Implement\Exception\EmptyResult) { - $datos = $this->datosPersonaRepository->create($datosData); + $dataMap = [ + 'digit' => 'digito', + 'name' => 'nombres', + 'names' => 'nombres', + 'last_name' => 'apellido_paterno', + 'last_name2' => 'apellido_materno', + ]; + foreach ($data as $key => $value) { + if (array_key_exists($key, $dataMap)) { + $data[$dataMap[$key]] = $value; + unset($data[$key]); + } + } + + $filteredData = $this->personaRepository->filterData($data); try { - $this->datosPersonaRepository->save($datos); + $persona = $this->personaRepository->create($filteredData); + $persona = $this->personaRepository->save($persona); } catch (PDOException $exception) { throw new Create(__CLASS__, $exception); } } + } + $this->addDatos($persona, $data); + return $this->process($persona); } @@ -176,4 +171,113 @@ class Persona extends Ideal\Service ->setArgs(['persona_rut' => $persona->rut])); return $persona; } + /** + * @param Model\Venta\Propietario $propietario + * @return Model\Persona + * @throws Create + */ + protected function addFromPropietario(Model\Venta\Propietario $propietario): Model\Persona + { + $data = [ + 'rut' => $propietario->rut, + 'digito' => $propietario->dv, + 'nombres' => $propietario->nombres, + 'apellido_paterno' => $propietario->apellidos['paterno'], + 'apellido_materno' => $propietario->apellidos['materno'] ?? '', + ]; + try { + $persona = $this->personaRepository->create($data); + $persona = $this->personaRepository->save($persona); + } catch (PDOException $exception) { + throw new Create(__CLASS__, $exception); + } + + $datosData = []; + if ($propietario->datos->direccion) { + $datosData['direccion_id'] = $propietario->datos?->direccion->id; + } + if ($propietario->datos->email) { + $datosData['email'] = $propietario->datos->email; + } + if ($propietario->datos->telefono) { + $datosData['telefono'] = $propietario->datos->telefono; + } + if ($propietario->datos->estado_civil) { + $datosData['estado_civil'] = $propietario->datos->estado_civil; + } + if ($propietario->datos->fecha_nacimiento) { + $datosData['fecha_nacimiento'] = $propietario->datos->fecha_nacimiento; + } + if ($propietario->datos->profesion) { + $datosData['ocupacion'] = $propietario->datos->profesion; + } + if ($propietario->datos->sexo) { + $datosData['sexo'] = $propietario->datos->sexo; + } + + $this->addDatos($persona, $datosData); + return $persona; + } + + /** + * @param Model\Persona $persona + * @param array $data + * @return Model\Persona + * @throws Create + */ + protected function addDatos(Model\Persona $persona, array $data): Model\Persona + { + $addressData = []; + foreach ($data as $key => $value) { + if (!str_starts_with($key, 'address_') and !str_starts_with($key, 'direccion_')) { + continue; + } + if (str_starts_with($key, 'direccion_')) { + $newKey = substr($key, strlen('direccion_')); + } else { + $newKey = substr($key, strlen('address_')); + } + $addressData[$newKey] = $value; + } + if (!empty($addressData)) { + $address = $this->direccionService->add($addressData); + foreach ($data as $key => $value) { + if (str_starts_with($key, 'address_') or str_starts_with($key, 'direccion_')) { + unset($data[$key]); + } + } + $data['direccion_id'] = $address->id; + } + + $dataMap = [ + 'phone' => 'telefono', + 'profession' => 'ocupacion', + 'profesion' => 'ocupacion', + 'sex' => 'sexo', + 'marital_status' => 'estado_civil', + 'birth_date' => 'fecha_nacimiento', + 'birthdate' => 'fecha_nacimiento', + ]; + foreach ($data as $key => $value) { + if (array_key_exists($key, $dataMap)) { + $data[$dataMap[$key]] = $value; + unset($data[$key]); + } + } + try { + $datos = $this->datosPersonaRepository->fetchByPersona($persona->rut); + $this->datosPersonaRepository->edit($datos, $data); + } catch (Implement\Exception\EmptyResult $exception) { + $datosData = ['persona_rut' => $persona->rut, ...$data]; + $datosData = $this->datosPersonaRepository->filterData($datosData); + $datos = $this->datosPersonaRepository->create($datosData); + try { + $this->datosPersonaRepository->save($datos); + } catch (PDOException $exception) { + throw new Create(__CLASS__, $exception); + } + } + + return $persona; + } } diff --git a/app/src/Service/Venta/Promotion.php b/app/src/Service/Venta/Promotion.php index 6dc0194..052be9b 100644 --- a/app/src/Service/Venta/Promotion.php +++ b/app/src/Service/Venta/Promotion.php @@ -48,6 +48,20 @@ class Promotion extends Ideal\Service } } + /** + * @param int $project_id + * @return array + * @throws Exception\ServiceAction\Read + */ + public function getByProject(int $project_id): array + { + try { + return array_map([$this, 'process'], $this->promotionRepository->fetchByProject($project_id)); + } catch (Implement\Exception\EmptyResult $exception) { + throw new Exception\ServiceAction\Read(__CLASS__, $exception); + } + } + /** * @param int $contract_id * @return array diff --git a/app/src/Service/Venta/Reservation.php b/app/src/Service/Venta/Reservation.php index 9ceca60..a12f442 100644 --- a/app/src/Service/Venta/Reservation.php +++ b/app/src/Service/Venta/Reservation.php @@ -1,8 +1,10 @@ reservationRepository->fetchAll($order); + return array_map([$this, 'process'], $this->reservationRepository->fetchAll($order)); } catch (Implement\Exception\EmptyResult) { return []; } } + /** + * @param int $project_id + * @return array + * @throws ServiceAction\Read + */ + public function getByProject(int $project_id): array + { + try { + return array_map([$this, 'process'], $this->reservationRepository->fetchByProject($project_id)); + } catch (Implement\Exception\EmptyResult $exception) { + throw new ServiceAction\Read(__CLASS__, $exception); + } + } + public function get(int $id): Model\Venta\Reservation { try { @@ -37,25 +59,131 @@ class Reservation extends Ideal\Service\API } } - public function add(array $data): Model\Venta\Reservation + /** + * @param int $buyer_rut + * @param DateTimeInterface $date + * @return Model\Venta\Reservation + * @throws Read + */ + public function getByBuyerAndDate(int $buyer_rut, DateTimeInterface $date): Model\Venta\Reservation { try { - $date = new DateTimeImmutable(); - try { - $date = new DateTimeImmutable($data['date']); - } catch (DateMalformedStringException) {} - return $this->process($this->reservationRepository->fetchByBuyerAndDate($data['buyer_rut'], $date)); - } catch (Implement\Exception\EmptyResult) {} - - try { - $reservationData = $this->reservationRepository->filterData($data); - $reservation = $this->reservationRepository->create($reservationData); - $this->reservationRepository->save($reservation); - return $this->process($reservation); - } catch (PDOException $exception) { - throw new ServiceAction\Create(__CLASS__, $exception); + return $this->process($this->reservationRepository->fetchByBuyerAndDate($buyer_rut, $date)); + } catch (Implement\Exception\EmptyResult $exception) { + throw new ServiceAction\Read(__CLASS__, $exception); } } + + /** + * @param int $project_id + * @return array + * @throws ServiceAction\Read + */ + public function getActive(int $project_id): array + { + try { + return array_map([$this, 'process'], $this->reservationRepository->fetchActive($project_id)); + } catch (Implement\Exception\EmptyResult $exception) { + throw new ServiceAction\Read(__CLASS__, $exception); + } + } + + /** + * @param int $project_id + * @return array + * @throws ServiceAction\Read + */ + public function getPending(int $project_id): array + { + try { + return array_map([$this, 'process'], $this->reservationRepository->fetchPending($project_id)); + } catch (Implement\Exception\EmptyResult $exception) { + throw new ServiceAction\Read(__CLASS__, $exception); + } + } + + /** + * @param int $project_id + * @return array + * @throws ServiceAction\Read + */ + public function getRejected(int $project_id): array + { + try { + return array_map([$this, 'process'], $this->reservationRepository->fetchRejected($project_id)); + } catch (Implement\Exception\EmptyResult $exception) { + throw new ServiceAction\Read(__CLASS__, $exception); + } + } + + public function add(array $data): Model\Venta\Reservation + { + $date = new DateTimeImmutable(); + try { + $date = new DateTimeImmutable($data['date']); + } catch (DateMalformedStringException) {} + try { + $reservation = $this->reservationRepository->fetchByBuyerAndDate($data['buyer_rut'], $date); + + if (array_key_exists('broker_rut', $data) and $data['broker_rut'] !== '') { + try { + $broker = $this->brokerService->get($data['broker_rut']); + $reservation = $this->reservationRepository->edit($reservation, ['broker_rut' => $broker->rut]); + } catch (ServiceAction\Read) {} + } + } catch (Implement\Exception\EmptyResult) { + if (!$this->reservationRepository->getConnection()->getPDO()->inTransaction()) { + $this->reservationRepository->getConnection()->getPDO()->beginTransaction(); + } + $buyerData = []; + foreach ($data as $key => $value) { + if (!str_starts_with($key, 'buyer_')) { + continue; + } + $buyerData[substr($key, strlen('buyer_'))] = $value; + } + $this->personaService->add($buyerData); + + $data['date'] = $date->format('Y-m-d'); + try { + $reservationData = $this->reservationRepository->filterData($data); + $reservation = $this->reservationRepository->create($reservationData); + $reservation = $this->reservationRepository->save($reservation); + + $stateType = Model\Venta\Reservation\State\Type::INACTIVE; + $stateData = [ + 'reservation_id' => $reservation->id, + 'date' => $data['date'], + 'type' => $stateType->value, + ]; + $state = $this->stateRepository->create($stateData); + $this->stateRepository->save($state); + + $units = array_combine($data['units'], $data['units_value']); + $this->addUnits($reservation, $units); + + if (array_key_exists('broker_rut', $data) and !empty($data['broker_rut'])) { + $this->addBroker($reservation, $data['broker_rut']); + } + + if (array_key_exists('promotions', $data)) { + $this->addPromotions($reservation, $data['promotions']); + } + + if ($this->reservationRepository->getConnection()->getPDO()->inTransaction()) { + $this->reservationRepository->getConnection()->getPDO()->commit(); + } + } catch (PDOException $exception) { + $this->logger->warning($exception->getMessage(), ['exception' => $exception->getTraceAsString()]); + if ($this->reservationRepository->getConnection()->getPDO()->inTransaction()) { + $this->reservationRepository->getConnection()->getPDO()->rollBack(); + } + throw new ServiceAction\Create(__CLASS__, $exception); + } + } + + return $this->process($reservation); + } public function edit(Define\Model $model, array $new_data): Model\Venta\Reservation { try { @@ -74,8 +202,82 @@ class Reservation extends Ideal\Service\API throw new ServiceAction\Delete(__CLASS__, $exception); } } + + /** + * @param Model\Venta\Reservation $reservation + * @return void + * @throws ServiceAction\Update + */ + public function approve(Model\Venta\Reservation $reservation): void + { + try { + $stateData = [ + 'reservation_id' => $reservation->id, + 'date' => new DateTimeImmutable(), + 'type' => Model\Venta\Reservation\State\Type::ACTIVE->value, + ]; + $state = $this->stateRepository->create($stateData); + $this->stateRepository->save($state); + } catch (PDOException $exception) { + throw new ServiceAction\Update(__CLASS__, $exception); + } + } + public function reject(Model\Venta\Reservation $reservation): void + { + try { + $stateData = [ + 'reservation_id' => $reservation->id, + 'date' => new DateTimeImmutable(), + 'type' => Model\Venta\Reservation\State\Type::REJECTED->value, + ]; + $state = $this->stateRepository->create($stateData); + $this->stateRepository->save($state); + } catch (PDOException $exception) { + throw new ServiceAction\Update(__CLASS__, $exception); + } + } + protected function process(Define\Model $model): Model\Venta\Reservation { + $model->addFactory('states', new Implement\Repository\Factory() + ->setArgs(['reservation_id' => $model->id]) + ->setCallable(function(int $reservation_id) { + return $this->stateRepository->fetchByReservation($reservation_id); + }) + ); + $model->buyer = $this->personaService->getById($model->buyer->rut); return $model; } + protected function addUnits(Model\Venta\Reservation $reservation, array $units): void + { + foreach ($units as $unit_id => $value) { + try { + $unit = $this->unitService->getById($unit_id); + } catch (ServiceAction\Read) { + continue; + } + $reservation->addUnit($unit, $value); + } + $this->reservationRepository->save($reservation); + } + protected function addBroker(Model\Venta\Reservation $reservation, int $broker_rut): void + { + try { + $broker = $this->brokerService->get($broker_rut); + $reservation->broker = $broker; + $this->reservationRepository->save($reservation); + } catch (ServiceAction\Read) {} + } + protected function addPromotions(Model\Venta\Reservation $reservation, array $promotions): void + { + foreach ($promotions as $promotion_id) { + try { + $promotion = $this->promotionService->getById($promotion_id); + } catch (ServiceAction\Read) { + continue; + } + $reservation->promotions []= $promotion; + } + $this->reservationRepository->save($reservation); + } } diff --git a/app/tests/extension/ContainerTrait.php b/app/tests/extension/ContainerTrait.php new file mode 100644 index 0000000..e8eb309 --- /dev/null +++ b/app/tests/extension/ContainerTrait.php @@ -0,0 +1,15 @@ +container = buildContainer(); + } +} diff --git a/app/tests/extension/Seeds/Bancos.php b/app/tests/extension/Seeds/Bancos.php new file mode 100644 index 0000000..631e595 --- /dev/null +++ b/app/tests/extension/Seeds/Bancos.php @@ -0,0 +1,23 @@ + $this->faker->company, + ]; + } + + $this->table('banco') + ->insertValues($data) + ->save(); + } +} diff --git a/app/tests/extension/Seeds/BrokerContracts.php b/app/tests/extension/Seeds/BrokerContracts.php new file mode 100644 index 0000000..9adac33 --- /dev/null +++ b/app/tests/extension/Seeds/BrokerContracts.php @@ -0,0 +1,45 @@ +loadValues('brokers', columns: 'rut'); + $projects = $this->loadValues('proyecto', columns: 'id'); + + $data = []; + foreach ($projects as $project) { + $count = $this->faker->numberBetween(1, count($brokers)); + for ($i = 0; $i < $count; $i++) { + $data []= [ + 'broker_rut' => $brokers[$i], + 'project_id' => $project, + 'commission' => $this->faker->randomFloat(4, 0, 1), + ]; + } + } + $this->table('broker_contracts')->insertValues($data)->save(); + + $contracts = $this->loadValues('broker_contracts', columns: 'id'); + $stateData = []; + foreach ($contracts as $contract) { + $stateData[]= [ + 'contract_id' => $contract, + 'date' => $this->faker->dateTimeBetween('-1 year'), + 'type' => 1 + ]; + } + $this->table('broker_contract_states')->insertValues($stateData)->save(); + } +} diff --git a/app/tests/extension/Seeds/Brokers.php b/app/tests/extension/Seeds/Brokers.php new file mode 100644 index 0000000..7ac1a73 --- /dev/null +++ b/app/tests/extension/Seeds/Brokers.php @@ -0,0 +1,50 @@ +loadValues('direccion', columns: 'id'); + + + + $count = 10; + $contactData = []; + for($i = 0; $i < $count; $i++) { + $rut = $this->faker->rut(false, false); + $contactData[]= [ + 'rut' => $rut, + 'digit' => $this->faker->digitoVerificador($rut), + 'name' => $this->faker->name, + 'email' => $this->faker->email, + 'phone' => $this->faker->phoneNumber, + 'address_id' => $this->faker->randomElement($addresses) + ]; + } + $this->table('broker_contacts')->insertValues($contactData)->save(); + + $contacts = $this->loadValues('broker_contacts', columns: 'id'); + + $data = []; + $brokerData = []; + for($i = 0; $i < $count; $i ++) { + $rut = $this->faker->rut(false, false); + $data[] = [ + 'rut' => $rut, + 'digit' => $this->faker->digitoVerificador($rut), + 'name' => $this->faker->word + ]; + $brokerData []= [ + 'broker_rut' => $rut, + 'representante_id' => $this->faker->randomElement($contacts), + 'legal_name' => $this->faker->company + ]; + } + + $this->table('brokers')->insertValues($data)->save(); + $this->table('broker_data')->insertValues($brokerData)->save(); + } +} diff --git a/app/tests/extension/Seeds/Direcciones.php b/app/tests/extension/Seeds/Direcciones.php index a681cf4..09c9eee 100644 --- a/app/tests/extension/Seeds/Direcciones.php +++ b/app/tests/extension/Seeds/Direcciones.php @@ -9,14 +9,14 @@ class Direcciones extends AbstractSeed { $comunas = $this->loadValues('comuna', columns: 'id'); - $n = 50; + $n = 100; $data = []; for ($i = 0; $i < $n; $i++) { $row = [ 'calle' => $this->faker->streetName, 'numero' => $this->faker->randomNumber(5), 'comuna' => $this->faker->randomElement($comunas), - 'extra' => '', + 'extra' => $this->faker->optional(0.9, '')->words(2, true), ]; $extraRand = ((int) round(rand() / getrandmax())) === 1; if ($extraRand) { diff --git a/app/tests/extension/Seeds/Inmobiliarias.php b/app/tests/extension/Seeds/Inmobiliarias.php index 04b1238..e8798e6 100644 --- a/app/tests/extension/Seeds/Inmobiliarias.php +++ b/app/tests/extension/Seeds/Inmobiliarias.php @@ -5,9 +5,17 @@ use Tests\Extension\AbstractSeed; class Inmobiliarias extends AbstractSeed { + public function getDependencies(): array + { + return [ + Bancos::class + ]; + } + public function run(): void { $tipos = $this->loadValues('tipo_sociedad', columns: 'id'); + $bancos = $this->loadValues('banco', columns: 'id'); $suffixes = [ 'Inmobiliaria ', 'Administradora ', @@ -28,8 +36,10 @@ class Inmobiliarias extends AbstractSeed 'dv' => $this->faker->digitoVerificador($rut), 'razon' => $razon, 'abreviacion' => $abreviacion, - 'sigla' => $sigla, + 'cuenta' => $this->faker->randomNumber(8), + 'banco' => $this->faker->randomElement($bancos), 'sociedad' => $this->faker->randomElement($tipos), + 'sigla' => $sigla, ]; } $this->table('inmobiliaria') diff --git a/app/tests/extension/Seeds/Promotions.php b/app/tests/extension/Seeds/Promotions.php new file mode 100644 index 0000000..00c5293 --- /dev/null +++ b/app/tests/extension/Seeds/Promotions.php @@ -0,0 +1,55 @@ +loadValues('proyecto', columns: 'id'); + + $data = []; + $count = $this->faker->numberBetween(1, 10); + for ($i = 0; $i < $count; $i++) { + $start = $this->faker->dateTimeBetween('-1 year'); + $end = $this->faker->dateTimeBetween($start, $start->add(new DateInterval('P1Y'))); + $data[] = [ + 'description' => $this->faker->sentence, + 'type' => 1, + 'amount' => $this->faker->randomFloat(2, 0, 1), + 'start_date' => $start, + 'end_date' => $end, + 'valid_until' => $this->faker->dateTimeBetween($end, $end->add(new DateInterval('P1M'))), + 'state' => 1, + ]; + } + $this->table('promotions')->insertValues($data)->save(); + + $promotions = $this->loadValues('promotions', columns: 'id'); + + $data = []; + foreach ($projects as $project) { + $hasPromo = $this->faker->boolean(10); + if (!$hasPromo) { + continue; + } + + $data []= [ + 'promotion_id' => $this->faker->randomElement($promotions), + 'project_id' => $project, + ]; + } + if (count($data) > 0) { + $this->table('promotion_projects')->insertValues($data)->save(); + } + } +} diff --git a/app/tests/extension/Seeds/ProyectoTipoUnidad.php b/app/tests/extension/Seeds/ProyectoTipoUnidad.php new file mode 100644 index 0000000..da2c384 --- /dev/null +++ b/app/tests/extension/Seeds/ProyectoTipoUnidad.php @@ -0,0 +1,42 @@ +loadValues('proyecto', columns: 'id'); + $types = $this->loadValues('tipo_unidad', columns: 'id'); + + $data = []; + foreach ($projects as $project) { + foreach ($types as $type) { + $count = $this->faker->numberBetween(1, 10); + for ($i = 0; $i < $count; $i++) { + $name = $this->faker->word; + $data []= [ + 'proyecto' => $project, + 'tipo' => $type, + 'nombre' => $name, + 'abreviacion' => substr($name, 0, 3), + 'm2' => $this->faker->randomFloat(2, 10, 100), + 'logia' => $this->faker->optional(.3, 0)->randomFloat(2, 1, 5), + 'terraza' => $this->faker->optional(.3, 0)->randomFloat(2, 2, 30), + 'descripcion' => $this->faker->sentence, + ]; + } + } + } + + $this->table('proyecto_tipo_unidad')->insertValues($data)->save(); + } +} diff --git a/app/tests/extension/Seeds/Proyectos.php b/app/tests/extension/Seeds/Proyectos.php index 557e757..3083a4d 100644 --- a/app/tests/extension/Seeds/Proyectos.php +++ b/app/tests/extension/Seeds/Proyectos.php @@ -27,9 +27,9 @@ class Proyectos extends AbstractSeed 'direccion' => $this->faker->randomElement($direcciones), 'superficie_sobre_nivel' => $this->faker->randomFloat(2, 1000, 10000), 'superficie_bajo_nivel' => $this->faker->randomFloat(2, 0, 5000), - 'pisos' => $this->faker->randomNumber(2), - 'subterraneos' => $this->faker->randomNumber(2), - 'corredor' => $this->faker->randomFloat(4, 0, 1) + 'pisos' => $this->faker->numberBetween(2, 30), + 'subterraneos' => $this->faker->numberBetween(0, 5), + 'corredor' => $this->faker->optional(.6, 0)->randomFloat(4, 0, 1) ]; } diff --git a/app/tests/extension/Seeds/Unidades.php b/app/tests/extension/Seeds/Unidades.php new file mode 100644 index 0000000..b879509 --- /dev/null +++ b/app/tests/extension/Seeds/Unidades.php @@ -0,0 +1,47 @@ +loadValues('proyecto_tipo_unidad', columns: ['id', 'proyecto', 'tipo', 'm2', 'logia', 'terraza']); + + $data = []; + foreach ($ptus as $s => $ptu) { + $count = $this->faker->numberBetween(1, 10); + $abr = $this->faker->word; + $orientation = $this->faker->randomElement(['N', 'NO', 'NP', 'S', 'SO', 'SP', 'P', 'O']); + + for ($i = 0; $i < $count; $i++) { + $data[] = [ + 'proyecto' => $ptu['proyecto'], + 'tipo' => $ptu['tipo'], + 'subtipo' => $s, + 'piso' => $i + 2, + 'descripcion' => ($i + 2) * 100 + $s, + 'abreviacion' => $abr, + 'm2' => $ptu['m2'], + 'logia' => $ptu['logia'], + 'cubierta' => 0, + 'terraza' => $ptu['terraza'], + 'orientacion' => $orientation, + 'costo_inmobiliaria' => $this->faker->randomFloat(2, 1000, 3000), + 'pt' => $ptu['id'], + 'valor' => $this->faker->randomFloat(2, 1000, 3000), + ]; + } + } + + $this->table('unidad')->insertValues($data)->save(); + } +} diff --git a/app/tests/integration/API/Ventas/ReservationTest.php b/app/tests/integration/API/Ventas/ReservationTest.php new file mode 100644 index 0000000..0ec2474 --- /dev/null +++ b/app/tests/integration/API/Ventas/ReservationTest.php @@ -0,0 +1,92 @@ +addProvider(new Rut($faker)); + + $comunas = $this->container->get(Repository\Comuna::class)->fetchAll(); + $projects = $this->container->get(Repository\Proyecto::class)->fetchAll(); + $project = $faker->randomElement($projects); + $brokers = $this->container->get(Repository\Proyecto\Broker::class)->fetchAll(); + $units = $this->container->get(Repository\Venta\Unidad::class)->fetchAll(); + + $selectedUnits = []; + $unitsValue = []; + $unitsCount = $faker->numberBetween(1, 3); + for ($i = 0; $i < $unitsCount; $i++) { + $selectedUnits[] = $faker->randomElement($units)->id; + $unitsValue[] = $faker->randomFloat(2, 1000, 10000); + } + + $broker = $faker->randomElement($brokers); + + $activePromotions = $this->container->get(Repository\Venta\Promotion::class)->fetchActiveByProject($project->id); + + $promotionsCount = $faker->numberBetween(0, min(3, count($activePromotions))); + $promotions = []; + for ($i = 0; $i < $promotionsCount; $i++) { + $promotions[] = $faker->randomElement($activePromotions)->id; + } + $rut = $faker->rut(false, false); + $data = [ + 'project_id' => $faker->randomElement($projects)->id, + 'date' => $faker->dateTimeBetween('-2 years', 'now')->format('Y-m-d'), + 'buyer_rut' => $rut, + 'buyer_digit' => $faker->digitoVerificador($rut), + 'buyer_names' => $faker->firstName, + 'buyer_last_name' => $faker->lastName, + 'buyer_last_name2' => $faker->lastName, + 'buyer_email' => $faker->email, + 'buyer_phone' => $faker->randomNumber(8), + 'buyer_address_street' => $faker->streetName, + 'buyer_address_number' => $faker->buildingNumber, + 'buyer_address_extra' => $faker->streetAddress, + 'buyer_address_comuna_id' => $faker->randomElement($comunas)->id, + 'units' => $selectedUnits, + 'units_value' => $unitsValue, + 'broker_rut' => $broker ? $broker->rut : '', + ]; + if (count($promotions) > 0) { + $data['promotions'] = $promotions; + } + + $reservation = $this->container->get(Service\Venta\Reservation::class)->add($data); + $this->assertInstanceOf(Model\Venta\Reservation::class, $reservation); + $this->assertEquals($data['date'], $reservation->date->format('Y-m-d')); + $this->assertEquals($data['project_id'], $reservation->project->id); + $this->assertEquals($data['buyer_rut'], $reservation->buyer->rut); + $this->assertEquals($data['buyer_digit'], $reservation->buyer->digito); + $this->assertEquals($data['buyer_names'], $reservation->buyer->nombres); + $this->assertEquals($data['buyer_last_name'], $reservation->buyer->apellidoPaterno); + $this->assertEquals($data['buyer_last_name2'], $reservation->buyer->apellidoMaterno); + $this->assertEquals($data['buyer_email'], $reservation->buyer->datos->email); + $this->assertEquals($data['buyer_phone'], $reservation->buyer->datos->telefono); + $this->assertEquals($data['buyer_address_street'], $reservation->buyer->datos->direccion->calle); + $this->assertEquals($data['buyer_address_number'], $reservation->buyer->datos->direccion->numero); + $this->assertEquals($data['buyer_address_extra'], $reservation->buyer->datos->direccion->extra); + $this->assertEquals($data['buyer_address_comuna_id'], $reservation->buyer->datos->direccion->comuna->id); + if ($broker !== null) { + $this->assertEquals($data['broker_rut'], $reservation->broker->rut); + } + $this->assertEquals(1, $reservation->currentState()->type->value); + } +} diff --git a/app/tests/integration/QueueTest.php b/app/tests/integration/QueueTest.php index aa3d601..195765a 100644 --- a/app/tests/integration/QueueTest.php +++ b/app/tests/integration/QueueTest.php @@ -11,17 +11,12 @@ use Incoviba\Common\Implement; use Incoviba\Common\Ideal; use Incoviba\Service; use Incoviba\Repository; +use Tests\Extension\ContainerTrait; use Tests\Extension\Faker\Provider\Rut; class QueueTest extends TestCase { - protected ContainerInterface $container; - - protected function setUp(): void - { - require_once implode(DIRECTORY_SEPARATOR, [dirname(__DIR__, 2), 'setup', 'container.php']); - $this->container = buildContainer(); - } + use ContainerTrait; public function testServiceWorker(): void { diff --git a/app/tests/unit/src/Model/Venta/ReservationTest.php b/app/tests/unit/src/Model/Venta/ReservationTest.php index 593dcb2..298c214 100644 --- a/app/tests/unit/src/Model/Venta/ReservationTest.php +++ b/app/tests/unit/src/Model/Venta/ReservationTest.php @@ -13,7 +13,7 @@ class ReservationTest extends AbstractModel protected function setUp(): void { $this->model = new Reservation(); - $this->properties = ['buyer', 'date', 'units', 'promotions', 'broker']; + $this->properties = ['project', 'buyer', 'date', 'units', 'promotions', 'broker']; $this->methods = ['states', 'currentState', 'addUnit', 'removeUnit', 'findUnit', 'hasUnit']; } } diff --git a/app/tests/unit/src/Repository/DireccionTest.php b/app/tests/unit/src/Repository/DireccionTest.php new file mode 100644 index 0000000..ed57212 --- /dev/null +++ b/app/tests/unit/src/Repository/DireccionTest.php @@ -0,0 +1,64 @@ +connection = $this->getMockBuilder(Define\Connection::class) + ->disableOriginalConstructor() + ->getMock(); + $this->comunaRepository = $this->getMockBuilder(Repository\Comuna::class) + ->disableOriginalConstructor() + ->getMock(); + $this->comunaRepository->method('fetchById')->willReturnCallback(function ($id) { + $comuna = new Model\Comuna(); + $comuna->id = $id; + return $comuna; + }); + } + + public function testCreate(): void + { + $direccionRepository = new Repository\Direccion($this->connection, $this->comunaRepository); + + $faker = Faker\Factory::create(); + $data = [ + 'calle' => $faker->streetName, + 'numero' => $faker->randomNumber(), + 'extra' => $faker->streetSuffix, + 'comuna' => $faker->randomNumber(), + ]; + $direccion = $direccionRepository->create($data); + + $this->assertTrue(!isset($direccion->id)); + $this->assertEquals($data['calle'], $direccion->calle); + $this->assertEquals($data['numero'], $direccion->numero); + $this->assertEquals($data['extra'], $direccion->extra); + $this->assertEquals($data['comuna'], $direccion->comuna->id); + } + public function testSave(): void + { + $direccionRepository = new Repository\Direccion($this->connection, $this->comunaRepository); + $faker = Faker\Factory::create(); + + $direccion = new Model\Direccion(); + $direccion->calle = $faker->streetName; + $direccion->numero = $faker->randomNumber(3); + $direccion->extra = $faker->streetSuffix; + $direccion->comuna = $this->getMockBuilder(Model\Comuna::class)->getMock(); + $direccion->comuna->id = $faker->numberBetween(10000, 18000); + $direccion = $direccionRepository->save($direccion); + + $this->assertTrue(isset($direccion->id)); + } +} diff --git a/app/tests/unit/src/Repository/PersonaTest.php b/app/tests/unit/src/Repository/PersonaTest.php new file mode 100644 index 0000000..6a96ae6 --- /dev/null +++ b/app/tests/unit/src/Repository/PersonaTest.php @@ -0,0 +1,62 @@ +connection = $this->getMockBuilder(Define\Connection::class) + ->disableOriginalConstructor() + ->getMock(); + } + + public function testCreate(): void + { + $personaRepository = new Repository\Persona($this->connection); + $faker = Faker\Factory::create(); + + $data = [ + 'rut' => $faker->randomNumber(8), + 'digito' => $faker->boolean(100 - round(1 / 11 * 100)) ? $faker->randomNumber(1) : 'K', + 'nombres' => $faker->name(), + 'apellido_paterno' => $faker->lastName(), + 'apellido_materno' => $faker->lastName() + ]; + $persona = $personaRepository->create($data); + + $this->assertEquals($data['rut'], $persona->rut); + $this->assertEquals($data['digito'], $persona->digito); + $this->assertEquals($data['nombres'], $persona->nombres); + $this->assertEquals($data['apellido_paterno'], $persona->apellidoPaterno); + $this->assertEquals($data['apellido_materno'], $persona->apellidoMaterno); + } + public function testSave(): void + { + $personaRepository = new Repository\Persona($this->connection); + $faker = Faker\Factory::create(); + + $data = [ + 'rut' => $faker->randomNumber(8), + 'digito' => $faker->boolean(100 - round(1 / 11 * 100)) ? $faker->randomNumber(1) : 'K', + 'nombres' => $faker->name(), + 'apellido_paterno' => $faker->lastName(), + 'apellido_materno' => $faker->lastName() + ]; + $persona = new Model\Persona(); + $persona->rut = $data['rut']; + $persona->digito = $data['digito']; + $persona->nombres = $data['nombres']; + $persona->apellidoPaterno = $data['apellido_paterno']; + $persona->apellidoMaterno = $data['apellido_materno']; + $persona = $personaRepository->save($persona); + $this->assertEquals($data['rut'], $persona->rut); + } +} diff --git a/app/tests/unit/src/Repository/Venta/ReservationTest.php b/app/tests/unit/src/Repository/Venta/ReservationTest.php new file mode 100644 index 0000000..618dc86 --- /dev/null +++ b/app/tests/unit/src/Repository/Venta/ReservationTest.php @@ -0,0 +1,102 @@ +connection = $this->getMockBuilder(Define\Connection::class) + ->disableOriginalConstructor() + ->getMock(); + $this->proyectoRepository = $this->getMockBuilder(Repository\Proyecto::class) + ->disableOriginalConstructor() + ->getMock(); + $this->proyectoRepository->method('fetchById')->willReturnCallback(function ($id) { + $proyecto = new Model\Proyecto(); + $proyecto->id = $id; + return $proyecto; + }); + $this->personaRepository = $this->getMockBuilder(Repository\Persona::class) + ->disableOriginalConstructor() + ->getMock(); + $this->personaRepository->method('fetchById')->willReturnCallback(function ($rut) { + $persona = new Model\Persona(); + $persona->rut = $rut; + return $persona; + }); + $this->brokerRepository = $this->getMockBuilder(Repository\Proyecto\Broker::class) + ->disableOriginalConstructor() + ->getMock(); + $this->brokerRepository->method('fetchById')->willReturnCallback(function ($rut) { + $broker = new Model\Proyecto\Broker(); + $broker->rut = $rut; + return $broker; + }); + $this->unitRepository = $this->getMockBuilder(Repository\Venta\Unidad::class) + ->disableOriginalConstructor() + ->getMock(); + $this->unitRepository->method('fetchById')->willReturnCallback(function ($id) { + $unidad = new Model\Venta\Unidad(); + $unidad->id = $id; + return $unidad; + }); + $this->promotionRepository = $this->getMockBuilder(Repository\Venta\Promotion::class) + ->disableOriginalConstructor() + ->getMock(); + $this->promotionRepository->method('fetchById')->willReturnCallback(function ($id) { + $promotion = new Model\Venta\Promotion(); + $promotion->id = $id; + return $promotion; + }); + } + + public function testCreate(): void + { + $reservationRepository = new Repository\Venta\Reservation($this->connection, $this->proyectoRepository, + $this->personaRepository, $this->brokerRepository, $this->unitRepository, $this->promotionRepository); + $faker = Faker\Factory::create(); + $data = [ + 'project_id' => $faker->numberBetween(1, 10), + 'buyer_rut' => $faker->randomNumber(8), + 'date' => $faker->dateTimeBetween('-2 years', 'now')->format('Y-m-d'), + ]; + $reservation = $reservationRepository->create($data); + + $this->assertTrue(!isset($reservation->id)); + $this->assertEquals($data['project_id'], $reservation->project->id); + $this->assertEquals($data['buyer_rut'], $reservation->buyer->rut); + $this->assertEquals($data['date'], $reservation->date->format('Y-m-d')); + } + public function testSave(): void + { + $reservationRepository = new Repository\Venta\Reservation($this->connection, $this->proyectoRepository, + $this->personaRepository, $this->brokerRepository, $this->unitRepository, $this->promotionRepository); + $faker = Faker\Factory::create(); + $data = [ + 'project_id' => $faker->numberBetween(1, 10), + 'buyer_rut' => $faker->randomNumber(8), + 'date' => $faker->dateTimeBetween('-2 years', 'now')->format('Y-m-d'), + ]; + $reservation = new Model\Venta\Reservation(); + $reservation->project = $this->proyectoRepository->fetchById($data['project_id']); + $reservation->buyer = $this->personaRepository->fetchById($data['buyer_rut']); + $reservation->date = new DateTimeImmutable($data['date']); + $reservation = $reservationRepository->save($reservation); + + $this->assertTrue(isset($reservation->id)); + } +} diff --git a/app/tests/unit/src/Service/DireccionTest.php b/app/tests/unit/src/Service/DireccionTest.php new file mode 100644 index 0000000..aa6d425 --- /dev/null +++ b/app/tests/unit/src/Service/DireccionTest.php @@ -0,0 +1,64 @@ +logger = $this->getMockBuilder(LoggerInterface::class)->getMock(); + $this->direccionRepository = $this->getMockBuilder(Repository\Direccion::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->direccionRepository->method('fetchByCalleAndNumeroAndExtraAndComuna') + ->willThrowException(new Implement\Exception\EmptyResult('')); + $this->direccionRepository->method('filterData')->willReturnArgument(0); + $this->direccionRepository->method('create')->willReturnCallback(function($data) { + $direccion = new Model\Direccion(); + $direccion->calle = $data['calle']; + $direccion->numero = $data['numero']; + $direccion->extra = $data['extra']; + $direccion->comuna = $this->getMockBuilder(Model\Comuna::class) + ->disableOriginalConstructor()->getMock(); + $direccion->comuna->id = $data['comuna']; + return $direccion; + }); + $this->direccionRepository->method('save')->willReturnCallback(function($direccion) { + $direccion->id = 1; + return $direccion; + }); + } + + public function testAdd(): void + { + $direccionService = new Service\Direccion($this->logger, $this->direccionRepository); + $faker = Faker\Factory::create('es_ES'); + + $data = [ + 'street' => $faker->streetName, + 'number' => $faker->randomNumber(3), + 'extra' => $faker->word, + 'comuna_id' => $faker->numberBetween(10000, 18000), + ]; + $direccion = $direccionService->add($data); + + $this->assertEquals($data['street'], $direccion->calle); + $this->assertEquals($data['number'], $direccion->numero); + $this->assertEquals($data['extra'], $direccion->extra); + $this->assertEquals($data['comuna_id'], $direccion->comuna->id); + } +} diff --git a/app/tests/unit/src/Service/PersonaTest.php b/app/tests/unit/src/Service/PersonaTest.php new file mode 100644 index 0000000..ff5c83b --- /dev/null +++ b/app/tests/unit/src/Service/PersonaTest.php @@ -0,0 +1,65 @@ +logger = $this->getMockBuilder(LoggerInterface::class)->getMock(); + $this->personaRepository = $this->getMockBuilder(Repository\Persona::class) + ->disableOriginalConstructor()->getMock(); + $this->personaRepository->method('fetchById')->willThrowException(new Implement\Exception\EmptyResult('')); + $this->personaRepository->method('filterData')->willReturnArgument(0); + $this->personaRepository->method('create')->willReturnCallback(function($data) { + $persona = new Model\Persona(); + $persona->rut = $data['rut']; + $persona->digito = $data['digito']; + $persona->nombres = $data['nombres']; + $persona->apellidoPaterno = $data['apellido_paterno']; + $persona->apellidoMaterno = $data['apellido_materno']; + return $persona; + }); + $this->personaRepository->method('save')->willReturnArgument(0); + $this->datosPersonaRepository = $this->getMockBuilder(Repository\Persona\Datos::class) + ->disableOriginalConstructor()->getMock(); + $this->propietarioRepository = $this->getMockBuilder(Repository\Venta\Propietario::class) + ->disableOriginalConstructor()->getMock(); + $this->propietarioRepository->method('fetchById')->willThrowException(new Implement\Exception\EmptyResult('')); + $this->direccionService = $this->getMockBuilder(Service\Direccion::class) + ->disableOriginalConstructor()->getMock(); + } + + public function testAdd(): void + { + $personaService = new Service\Persona($this->logger, $this->personaRepository, $this->datosPersonaRepository, + $this->propietarioRepository, $this->direccionService); + $faker = Faker\Factory::create('es_ES'); + $digit = $faker->boolean(100-round(1/11*100)) ? $faker->randomNumber(1) : 'K'; + $data = [ + 'rut' => $faker->randomNumber(8), + 'digito' => $digit, + 'nombres' => $faker->name(), + 'apellido_paterno' => $faker->lastName(), + 'apellido_materno' => $faker->lastName(), + ]; + $persona = $personaService->add($data); + $this->assertEquals($data['rut'], $persona->rut); + } +} diff --git a/app/tests/unit/src/Service/ValorTest.php b/app/tests/unit/src/Service/ValorTest.php index 667c92f..5cea05b 100644 --- a/app/tests/unit/src/Service/ValorTest.php +++ b/app/tests/unit/src/Service/ValorTest.php @@ -3,18 +3,22 @@ namespace Tests\Unit\Service; use PHPUnit\Framework\TestCase; use PHPUnit\Framework\Attributes\DataProvider; +use Faker\Factory; use Incoviba\Service\Valor; use Incoviba\Service; class ValorTest extends TestCase { protected Service\UF $ufService; + protected static float $staticUF = 35498.76; + protected function setUp(): void { $this->ufService = $this->getMockBuilder(Service\UF::class) ->disableOriginalConstructor() ->getMock(); - $this->ufService->method('get')->willReturn(35000.0); + $faker = Factory::create(); + $this->ufService->method('get')->willReturn(self::$staticUF); } public static function cleanDataProvider(): array @@ -55,14 +59,14 @@ class ValorTest extends TestCase $result = $valorService->toUF($input, $date); $this->assertIsFloat($result); - $this->assertEquals($input / 35000, $result); + $this->assertEquals($input / self::$staticUF, $result); } public static function pesosDataProvider(): array { return [ - [1000.01, 1000.01*35000, false], - [1000, 1000*35000, true], + [1000.01, round(1000.01*self::$staticUF), false], + [1000, round(1000*self::$staticUF), true], ['1000', 1000, false], ]; } diff --git a/app/tests/unit/src/Service/Venta/BonoPieTest.php b/app/tests/unit/src/Service/Venta/BonoPieTest.php new file mode 100644 index 0000000..6d739a6 --- /dev/null +++ b/app/tests/unit/src/Service/Venta/BonoPieTest.php @@ -0,0 +1,65 @@ +dateTimeBetween('-1 week'); + $data = [ + 'fecha' => $fecha->format('Y-m-d'), + 'valor' => $faker->randomFloat(2, 100, 1000), + ]; + + $uf = $faker->randomFloat(2, 20000, 40000); + $pago = new Model\Venta\Pago(); + $pago->id = $faker->randomNumber(); + $pago->fecha = $fecha; + $pago->valor = $data['valor'] * $uf; + $pago->uf = $uf; + $bonoPie = new Model\Venta\BonoPie(); + $bonoPie->valor = $data['valor']; + $bonoPie->pago = $pago; + + $logger = $this->getMockBuilder(LoggerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $bonoPieRepository = $this->getMockBuilder(Repository\Venta\BonoPie::class) + ->disableOriginalConstructor() + ->getMock(); + $bonoPieRepository->method('fetchByPago')->willThrowException(new Implement\Exception\EmptyResult('fetchByPago')); + $bonoPieRepository->method('create')->willReturn($bonoPie); + $bonoPieRepository->method('save')->willReturnCallback(function($bonoPie) use ($faker) { + $bonoPie->id = $faker->randomNumber(); + return $bonoPie; + }); + $bonoPieRepository->method('filterData')->willReturnCallback(function($data) { + return array_intersect_key($data, array_flip(['valor', 'pago'])); + }); + $valorService = $this->getMockBuilder(Service\Valor::class) + ->disableOriginalConstructor() + ->getMock(); + $valorService->method('toUF')->willReturn($data['valor']); + $ufService = $this->getMockBuilder(Service\UF::class) + ->disableOriginalConstructor() + ->getMock(); + $ufService->method('get')->with($fecha)->willReturn($uf); + $pagoService = $this->getMockBuilder(Service\Venta\Pago::class) + ->disableOriginalConstructor() + ->getMock(); + $pagoService->method('add')->willReturn($pago); + + $bonoPieService = new Service\Venta\BonoPie($logger, $bonoPieRepository, $valorService, $ufService, $pagoService); + + $this->assertEquals($bonoPie, $bonoPieService->add($data)); + } +} diff --git a/app/tests/unit/src/Service/Venta/PagoTest.php b/app/tests/unit/src/Service/Venta/PagoTest.php index 830a172..e983f7e 100644 --- a/app/tests/unit/src/Service/Venta/PagoTest.php +++ b/app/tests/unit/src/Service/Venta/PagoTest.php @@ -1,5 +1,5 @@ assertTrue($status); $this->assertEquals($pago->uf, $this->uf); } -} \ No newline at end of file +} diff --git a/app/tests/unit/src/Service/Venta/ReservationTest.php b/app/tests/unit/src/Service/Venta/ReservationTest.php new file mode 100644 index 0000000..ca31e38 --- /dev/null +++ b/app/tests/unit/src/Service/Venta/ReservationTest.php @@ -0,0 +1,139 @@ +logger = $this->getMockBuilder(LoggerInterface::class) + ->disableOriginalConstructor()->getMock(); + $this->reservationRepository = $this->getMockBuilder(Repository\Venta\Reservation::class) + ->disableOriginalConstructor()->getMock(); + $this->reservationRepository->method('fetchByBuyerAndDate')->willThrowException(new Implement\Exception\EmptyResult('')); + $this->reservationRepository->method('filterData')->willReturnArgument(0); + $this->reservationRepository->method('create')->willReturnCallback(function($data) { + $reservation = new Model\Venta\Reservation(); + $reservation->buyer = $this->getMockBuilder(Model\Persona::class)->disableOriginalConstructor()->getMock(); + $reservation->buyer->rut = $data['buyer_rut']; + $reservation->project = $this->getMockBuilder(Model\Proyecto::class)->disableOriginalConstructor()->getMock(); + $reservation->project->id = $data['project_id']; + $reservation->date = new DateTimeImmutable($data['date']); + if (array_key_exists('broker_rut', $data) and !empty($data['broker_rut'])) { + $reservation->broker = $this->getMockBuilder(Model\Proyecto\Broker::class)->disableOriginalConstructor()->getMock(); + $reservation->broker->rut = $data['broker_rut']; + } + return $reservation; + }); + $this->reservationRepository->method('save')->willReturnCallback(function($reservation) { + $reservation->id = 1; + return $reservation; + }); + $this->stateRepository = $this->getMockBuilder(Repository\Venta\Reservation\State::class) + ->disableOriginalConstructor()->getMock(); + $this->stateRepository->method('fetchByReservation')->willReturnCallback(function($reservation_id) { + $state = new Model\Venta\Reservation\State(); + $state->reservation = new Model\Venta\Reservation(); + $state->reservation->id = $reservation_id; + return $state; + }); + $this->personaService = $this->getMockBuilder(Service\Persona::class) + ->disableOriginalConstructor()->getMock(); + $this->personaService->method('add')->willReturnCallback(function($data) { + $persona = new Model\Persona(); + $persona->rut = $data['rut']; + return $persona; + }); + $this->personaService->method('getById')->willReturnCallback(function($id) { + $persona = new Model\Persona(); + $persona->rut = $id; + return $persona; + }); + $this->brokerService = $this->getMockBuilder(Service\Proyecto\Broker::class) + ->disableOriginalConstructor()->getMock(); + $this->brokerService->method('get')->willReturnCallback(function($id) { + $broker = new Model\Proyecto\Broker(); + $broker->rut = $id; + return $broker; + }); + $this->promotionService = $this->getMockBuilder(Service\Venta\Promotion::class) + ->disableOriginalConstructor()->getMock(); + $this->promotionService->method('getById')->willReturnCallback(function($id) { + $promotion = new Model\Venta\Promotion(); + $promotion->id = $id; + return $promotion; + }); + $this->unitService = $this->getMockBuilder(Service\Venta\Unidad::class) + ->disableOriginalConstructor()->getMock(); + $this->unitService->method('getById')->willReturnCallback(function($id) { + $unit = new Model\Venta\Unidad(); + $unit->id = $id; + return $unit; + }); + } + + public function testAdd(): void + { + $faker = Faker\Factory::create(); + $reservationService = new Service\Venta\Reservation($this->logger, $this->reservationRepository, + $this->stateRepository, $this->personaService, $this->brokerService, $this->promotionService, + $this->unitService); + + $units = []; + $unitsValue = []; + $unitsCount = $faker->numberBetween(1, 10); + for ($i = 0; $i < $unitsCount; $i++) { + $units[] = $faker->unique()->numberBetween(1, 100); + $unitsValue[] = $faker->randomFloat(2, 1000, 10000); + } + $digit = $faker->boolean(100-round(1/11*100)) ? $faker->randomNumber(1) : 'K'; + $broker = $faker->boolean(10) ? $faker->randomNumber(8, true) : ''; + $promotions = []; + $promotionsCount = $faker->numberBetween(0, 3); + for ($i = 0; $i < $promotionsCount; $i++) { + $promotions[] = $faker->numberBetween(1, 100); + } + + $data = [ + 'project_id' => $faker->numberBetween(1, 100), + 'date' => $faker->dateTimeBetween('-1 year', 'now')->format('Y-m-d'), + 'buyer_rut' => $faker->randomNumber(8, true), + 'buyer_digit' => $digit, + 'buyer_names' => $faker->firstName(), + 'buyer_last_name' => $faker->lastName(), + 'buyer_last_name2' => $faker->lastName(), + 'buyer_address_street' => $faker->streetName(), + 'buyer_address_number' => $faker->randomNumber(3), + 'buyer_address_comuna' => $faker->numberBetween(10000, 18000), + 'buyer_marital_status' => $faker->word(), + 'buyer_email' => $faker->email(), + 'buyer_phone' => $faker->randomNumber(9), + 'buyer_profession' => $faker->word(), + 'buyer_birthdate' => $faker->dateTimeBetween('-80 year', '-18 year')->format('Y-m-d'), + 'units' => $units, + 'units_value' => $unitsValue, + 'broker_rut' => $broker, + 'promotions' => $promotions + ]; + $reservation = $reservationService->add($data); + $this->assertEquals($data['project_id'], $reservation->project->id); + $this->assertEquals($data['buyer_rut'], $reservation->buyer->rut); + $this->assertCount($unitsCount, $reservation->units); + } +}