From a41f306d3f8648693d8bffde3042c954a4a61dc7 Mon Sep 17 00:00:00 2001 From: Juan Pablo Vial Date: Thu, 13 Nov 2025 15:19:33 -0300 Subject: [PATCH] Reservation Payment detail --- app/src/Model/Venta/Reservation.php | 2 + .../Model/Venta/Reservation/Detail/Type.php | 3 + app/src/Model/Venta/Reservation/Payment.php | 23 ++ app/src/Repository/Venta/Reservation.php | 111 +++++- app/src/Service/Venta/Reservation.php | 348 ++++++++++++++---- 5 files changed, 419 insertions(+), 68 deletions(-) create mode 100644 app/src/Model/Venta/Reservation/Payment.php diff --git a/app/src/Model/Venta/Reservation.php b/app/src/Model/Venta/Reservation.php index 79639d6..0c7cb1f 100644 --- a/app/src/Model/Venta/Reservation.php +++ b/app/src/Model/Venta/Reservation.php @@ -14,6 +14,7 @@ class Reservation extends Common\Ideal\Model public array $units = []; public array $promotions = []; public ?Model\Proyecto\Broker $broker = null; + public ?Model\Venta\Reservation\Payment $payment = null; public function offer(): float { @@ -131,6 +132,7 @@ class Reservation extends Common\Ideal\Model 'units' => $this->units, 'promotions' => $this->promotions, 'broker' => $this->broker, + 'payment' => $this->payment, 'offer' => $this->offer(), 'with_commission' => $this->withCommission(), 'base' => $this->base(), diff --git a/app/src/Model/Venta/Reservation/Detail/Type.php b/app/src/Model/Venta/Reservation/Detail/Type.php index 4129449..ce1eb0e 100644 --- a/app/src/Model/Venta/Reservation/Detail/Type.php +++ b/app/src/Model/Venta/Reservation/Detail/Type.php @@ -6,6 +6,9 @@ enum Type: int case Unit = 1; case Promotion = 2; case Broker = 3; + case Advance = 4; // Pie + case Credit = 5; // Credito + case Subsidy = 6; // Subsidio public function jsonSerialize(): array { diff --git a/app/src/Model/Venta/Reservation/Payment.php b/app/src/Model/Venta/Reservation/Payment.php new file mode 100644 index 0000000..4c815e9 --- /dev/null +++ b/app/src/Model/Venta/Reservation/Payment.php @@ -0,0 +1,23 @@ + $this->pie, + 'credito' => $this->credito, + 'subsidio' => $this->subsidio + ]; + } +} diff --git a/app/src/Repository/Venta/Reservation.php b/app/src/Repository/Venta/Reservation.php index d4fcf56..e3135b4 100644 --- a/app/src/Repository/Venta/Reservation.php +++ b/app/src/Repository/Venta/Reservation.php @@ -3,14 +3,14 @@ namespace Incoviba\Repository\Venta; use DateTimeInterface; use DateInterval; +use PDO; +use PDOException; use Incoviba\Common\Define; use Incoviba\Common\Implement\Exception\EmptyResult; use Incoviba\Exception\Model\InvalidState; -use PDO; use Incoviba\Common; use Incoviba\Model; use Incoviba\Repository; -use PDOException; class Reservation extends Common\Ideal\Repository { @@ -18,7 +18,9 @@ class Reservation extends Common\Ideal\Repository protected Repository\Proyecto $proyectoRepository, protected Repository\Persona $personaRepository, protected Repository\Proyecto\Broker $brokerRepository, - protected Unidad $unitRepository, protected Promotion $promotionRepository) + protected Unidad $unitRepository, + protected Promotion $promotionRepository, protected Pie $advanceRepository, + protected Credito $creditRepository, protected Subsidio $subsidyRepository) { parent::__construct($connection); } @@ -44,6 +46,12 @@ class Reservation extends Common\Ideal\Repository ->register('date', new Common\Implement\Repository\Mapper\DateTime('date')); return $this->parseData(new Model\Venta\Reservation(), $data, $map); } + + /** + * @param Define\Model $model + * @return Model\Venta\Reservation + * @throws PDOException + */ public function save(Common\Define\Model $model): Model\Venta\Reservation { if (!isset($model->id)) { @@ -85,8 +93,11 @@ class Reservation extends Common\Ideal\Repository $this->fetchUnits($model, $data_row); try { $this->fetchBroker($model, $data_row); - } catch (Common\Implement\Exception\EmptyResult) {} + } catch (EmptyResult) {} $this->fetchPromotions($model, $data_row); + try { + $this->fetchPayment($model, $data_row); + } catch (EmptyResult) {} return $model; } @@ -248,6 +259,11 @@ class Reservation extends Common\Ideal\Repository return $reservations; } + /** + * @param Model\Venta\Reservation $reservation + * @return void + * @throws PDOException + */ protected function saveUnits(Model\Venta\Reservation $reservation): void { if (empty($reservation->units)) { @@ -290,6 +306,12 @@ class Reservation extends Common\Ideal\Repository Model\Venta\Reservation\Detail\Type::Unit->value, array_map(fn($unit) => $unit->unit->id, $reservation->units)); } + + /** + * @param Model\Venta\Reservation $reservation + * @return void + * @throws PDOException + */ protected function saveBroker(Model\Venta\Reservation &$reservation): void { if ($reservation->broker === null) { @@ -312,7 +334,7 @@ class Reservation extends Common\Ideal\Repository $new_id = $reservation->broker->rut; $reservation->broker = $this->brokerRepository->fetchById($result['reference_id']); $this->editBroker($reservation, ['broker_rut' => $new_id]); - } catch (PDOException) { + } catch (EmptyResult | PDOException) { $queryInsert = $this->connection->getQueryBuilder() ->insert() ->into('reservation_details') @@ -325,6 +347,12 @@ class Reservation extends Common\Ideal\Repository ]); } } + + /** + * @param Model\Venta\Reservation $reservation + * @return void + * @throws PDOException + */ protected function savePromotions(Model\Venta\Reservation $reservation): void { if (empty($reservation->promotions)) { @@ -366,6 +394,34 @@ class Reservation extends Common\Ideal\Repository Model\Venta\Reservation\Detail\Type::Promotion->value, array_map(fn($promotion) => $promotion->id, $reservation->promotions)); } + public function saveDetail(Model\Venta\Reservation $reservation, int $type, int $referenceId, ?float $value = null): void + { + $queryCheck = $this->connection->getQueryBuilder() + ->select('1') + ->from('reservation_details') + ->where('reservation_id = :reservation_id AND type = :type AND reference_id = :reference_id AND value = :value'); + $statementCheck = $this->connection->prepare($queryCheck); + $statementCheck->execute([ + 'reservation_id' => $reservation->id, + 'type' => $type, + 'reference_id' => $referenceId, + 'value' => $value + ]); + $result = $statementCheck->fetch(PDO::FETCH_ASSOC); + if ($result === false) { + $queryInsert = $this->connection->getQueryBuilder() + ->insert() + ->into('reservation_details') + ->columns(['reservation_id', 'type', 'reference_id', 'value']) + ->values([':reservation_id', ':type', ':reference_id', ':value']); + $this->connection->execute($queryInsert, [ + 'reservation_id' => $reservation->id, + 'type' => $type, + 'reference_id' => $referenceId, + 'value' => $value + ]); + } + } protected function cleanUpDetails(Model\Venta\Reservation $reservation, int $type_id, array $currentIds): void { $queryCheck = $this->connection->getQueryBuilder() @@ -643,4 +699,49 @@ class Reservation extends Common\Ideal\Repository $reservation->broker = null; } catch (PDOException) {} } + + /** + * @param Model\Venta\Reservation $reservation + * @param array $new_data + * @return Model\Venta\Reservation + * @throws EmptyResult + */ + protected function fetchPayment(Model\Venta\Reservation &$reservation, array $new_data): Model\Venta\Reservation + { + $types = [ + Model\Venta\Reservation\Detail\Type::Advance->value, + Model\Venta\Reservation\Detail\Type::Credit->value, + Model\Venta\Reservation\Detail\Type::Subsidy->value + ]; + $typesString = implode(', ', $types); + $query = $this->connection->getQueryBuilder() + ->select() + ->from('reservation_details') + ->where("reservation_id = :id AND type IN ({$typesString})"); + try { + $statement = $this->connection->execute($query, ['id' => $reservation->id]); + $results = $statement->fetchAll(PDO::FETCH_ASSOC); + } catch (PDOException $exception) { + throw new EmptyResult($query, $exception, ['id' => $reservation->id, 'data' => $new_data]); + } + if (count($results) === 0) { + throw new EmptyResult($query); + } + $payment = new Model\Venta\Reservation\Payment(); + foreach ($results as $result) { + switch ($result['type']) { + case Model\Venta\Reservation\Detail\Type::Advance->value: + $payment->pie = $this->advanceRepository->fetchById($result['reference_id']); + break; + case Model\Venta\Reservation\Detail\Type::Credit->value: + $payment->credito = $this->creditRepository->fetchById($result['reference_id']); + break; + case Model\Venta\Reservation\Detail\Type::Subsidy->value: + $payment->subsidio = $this->subsidyRepository->fetchById($result['reference_id']); + break; + } + } + $reservation->payment = $payment; + return $reservation; + } } diff --git a/app/src/Service/Venta/Reservation.php b/app/src/Service/Venta/Reservation.php index a0f0eee..22285f5 100644 --- a/app/src/Service/Venta/Reservation.php +++ b/app/src/Service/Venta/Reservation.php @@ -116,73 +116,20 @@ class Reservation extends Ideal\Service\API } } + /** + * @param array $data + * @return Model\Venta\Reservation + * @throws ServiceAction\Create + */ public function add(array $data): Model\Venta\Reservation { - $date = new DateTimeImmutable(); + $date = $this->parseDate($data['date'] ?? 'now'); + try { - $date = new DateTimeImmutable($data['date']); - } catch (DateMalformedStringException) {} - try { - $reservation = $this->reservationRepository->fetchByBuyerAndUnitAndDate($data['buyer_rut'], (int) $data['units'][0], $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]); - if ($this->reservationRepository->getConnection()->getPDO()->inTransaction()) { - $this->reservationRepository->getConnection()->getPDO()->rollBack(); - } - throw new ServiceAction\Create(__CLASS__, $exception); - } + return $this->updateExistingReservation($data, $date); + } catch (Read | ServiceAction\Update) { + return $this->createNewReservation($data, $date); } - - return $this->process($reservation); } public function edit(Define\Model $model, array $new_data): Model\Venta\Reservation { @@ -280,6 +227,13 @@ class Reservation extends Ideal\Service\API $this->reservationRepository->save($reservation); } catch (ServiceAction\Read) {} } + + /** + * @param Model\Venta\Reservation $reservation + * @param array $promotions + * @return void + * @throws PDOException + */ protected function addPromotions(Model\Venta\Reservation $reservation, array $promotions): void { foreach ($promotions as $promotion_id) { @@ -292,4 +246,272 @@ class Reservation extends Ideal\Service\API } $this->reservationRepository->save($reservation); } + + /** + * @param Model\Venta\Reservation $reservation + * @param array $payment + * @return void + * @throws ServiceAction\Create + */ + protected function addPayment(Model\Venta\Reservation $reservation, array $payment): void + { + $fields = [ + 'pie', + 'advance', + 'cuotas', + 'investments', + 'credito', + 'credit', + 'subsidio', + 'subsidy', + 'ahorro', + 'savings', + ]; + $filteredData = array_intersect_key($payment, array_flip($fields)); + $map = [ + 'pie' => 'advance', + 'cuotas' => 'investments', + 'credito' => 'credit', + 'subsidio' => 'subsidy', + 'ahorro' => 'savings' + ]; + $mappedData = []; + foreach ($filteredData as $key => $value) { + $mappedData[$map[$key]] = $value; + } + if (array_key_exists('advance', $mappedData)) { + $this->addAdvance($reservation, $mappedData); + } + if (array_key_exists('credit', $mappedData)) { + $this->addCredit($reservation, $mappedData); + } + if (array_key_exists('subsidy', $mappedData)) { + $this->addSubsidy($reservation, $mappedData); + } + } + + /** + * @param Model\Venta\Reservation $reservation + * @param array $data + * @return void + * @throws ServiceAction\Create + */ + protected function addAdvance(Model\Venta\Reservation $reservation, array $data): void + { + $fields = [ + 'advance', + 'investments' + ]; + $filteredData = array_intersect_key($data, array_flip($fields)); + $inputData = [ + 'fecha' => $reservation->date->format('Y-m-d'), + 'valor' => $filteredData['advance'], + 'cuotas' => $filteredData['investments'] + ]; + $pie = $this->pieService->add($inputData); + try { + $this->reservationRepository->saveDetail($reservation, Model\Venta\Reservation\Detail\Type::Advance->value, $pie->id); + } catch (PDOException $exception) { + throw new ServiceAction\Create(__CLASS__, $exception); + } + } + + /** + * @param Model\Venta\Reservation $reservation + * @param array $data + * @return void + * @throws ServiceAction\Create + */ + protected function addCredit(Model\Venta\Reservation $reservation, array $data): void + { + $creditValue = $data['credit']; + $inputData = [ + 'fecha' => $reservation->date->format('Y-m-d'), + 'valor' => $creditValue + ]; + $credit = $this->creditService->add($inputData); + try { + $this->reservationRepository->saveDetail($reservation, Model\Venta\Reservation\Detail\Type::Credit->value, $credit->id); + } catch (PDOException $exception) { + throw new ServiceAction\Create(__CLASS__, $exception); + } + } + + /** + * @param Model\Venta\Reservation $reservation + * @param array $data + * @return void + * @throws ServiceAction\Create + */ + protected function addSubsidy(Model\Venta\Reservation $reservation, array $data): void + { + $fields = [ + 'subsidy', + 'savings' + ]; + $filteredData = array_intersect_key($data, array_flip($fields)); + $inputData = [ + 'fecha' => $reservation->date->format('Y-m-d'), + 'subsidio' => $filteredData['subsidy'], + 'ahorro' => $filteredData['savings'] + ]; + $subsidy = $this->subsidyService->add($inputData); + try { + $this->reservationRepository->saveDetail($reservation, Model\Venta\Reservation\Detail\Type::Subsidy->value, $subsidy->id); + } catch (PDOException $exception) { + throw new ServiceAction\Create(__CLASS__, $exception); + } + } + + /** + * @param array $data + * @param DateTimeImmutable $date + * @return Model\Venta\Reservation + * @throws Read + * @throws ServiceAction\Update + */ + protected function updateExistingReservation(array $data, DateTimeImmutable $date): Model\Venta\Reservation + { + try { + $reservation = $this->reservationRepository->fetchByBuyerAndUnitAndDate( + $data['buyer_rut'], + (int) ($data['units'][0] ?? 0), + $date + ); + } catch (Implement\Exception\EmptyResult $exception) { + throw new Read(__CLASS__, $exception); + } + + if (!empty($data['broker_rut'] ?? null)) { + $this->updateBrokerForReservation($reservation, $data['broker_rut']); + } + + return $this->process($reservation); + } + + /** + * @param array $data + * @param DateTimeImmutable $date + * @return Model\Venta\Reservation + * @throws ServiceAction\Create + */ + protected function createNewReservation(array $data, DateTimeImmutable $date): Model\Venta\Reservation + { + $pdo = $this->reservationRepository->getConnection()->getPDO(); + $inTransaction = $pdo->inTransaction(); + + try { + if (!$inTransaction) { + $pdo->beginTransaction(); + } + + $this->createOrUpdateBuyer($data); + $reservation = $this->persistReservation($data, $date->format('Y-m-d')); + $this->createInitialState($reservation, $date); + + $this->processReservationDetails($reservation, $data); + + if (!$inTransaction) { + $pdo->commit(); + } + + return $this->process($reservation); + } catch (PDOException $exception) { + if (!$inTransaction && $pdo->inTransaction()) { + $pdo->rollBack(); + } + $this->logger->error('Failed to create reservation', [ + 'error' => $exception->getMessage(), + 'data' => $this->sanitizeData($data) + ]); + throw new ServiceAction\Create(__CLASS__, $exception); + } + } + protected function parseDate(?string $dateString): DateTimeImmutable + { + try { + return new DateTimeImmutable($dateString); + } catch (\Exception) { + return new DateTimeImmutable(); + } + } + + /** + * @param array $data + * @return void + * @throws ServiceAction\Create + */ + protected function createOrUpdateBuyer(array $data): void + { + $buyerData = array_filter($data, fn($key) => str_starts_with($key, 'buyer_'), ARRAY_FILTER_USE_KEY); + $buyerData = array_combine( + array_map(fn($key) => substr($key, strlen('buyer_')), array_keys($buyerData)), + $buyerData + ); + + $this->personaService->add($buyerData); + } + protected function persistReservation(array $data, string $date): Model\Venta\Reservation + { + $data['date'] = $date; + $reservationData = $this->reservationRepository->filterData($data); + $reservation = $this->reservationRepository->create($reservationData); + return $this->reservationRepository->save($reservation); + } + protected function createInitialState(Model\Venta\Reservation $reservation, DateTimeInterface $date): void + { + $stateData = [ + 'reservation_id' => $reservation->id, + 'date' => $date->format('Y-m-d'), + 'type' => Model\Venta\Reservation\State\Type::INACTIVE->value, + ]; + $state = $this->stateRepository->create($stateData); + $this->stateRepository->save($state); + } + protected function processReservationDetails(Model\Venta\Reservation $reservation, array $data): void + { + if (!empty($data['units']) && !empty($data['units_value'])) { + $units = array_combine($data['units'], $data['units_value']); + $this->addUnits($reservation, $units); + } + + if (!empty($data['broker_rut'])) { + $this->addBroker($reservation, $data['broker_rut']); + } + + if (!empty($data['promotions'])) { + $this->addPromotions($reservation, $data['promotions']); + } + + $paymentData = array_filter($data, fn($key) => str_starts_with($key, 'payment_'), ARRAY_FILTER_USE_KEY); + if (!empty($paymentData)) { + $this->addPayment($reservation, $paymentData); + } + } + + /** + * @param Model\Venta\Reservation $reservation + * @param string $brokerRut + * @return void + * @throws ServiceAction\Update + */ + protected function updateBrokerForReservation(Model\Venta\Reservation $reservation, string $brokerRut): void + { + try { + $broker = $this->brokerService->get($brokerRut); + $this->reservationRepository->edit($reservation, ['broker_rut' => $broker->rut]); + } catch (Implement\Exception\EmptyResult | ServiceAction\Read $exception) { + $this->logger->warning('Broker not found', ['broker_rut' => $brokerRut]); + throw new ServiceAction\Update(__CLASS__, $exception); + } + } + protected function sanitizeData(array $data): array + { + $sensitiveFields = ['password', 'token', 'api_key', 'rut']; + return array_map( + fn($value, $key) => in_array($key, $sensitiveFields, true) ? '***' : $value, + $data, + array_keys($data) + ); + } }