This commit is contained in:
Juan Pablo Vial
2025-05-12 19:46:09 -04:00
parent 3006adb0f7
commit f14cdd2730
10 changed files with 268 additions and 21 deletions

View File

@ -1,11 +1,13 @@
<?php
namespace Incoviba\Controller\API\Admin;
use Exception;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Incoviba\Common\Implement\Exception\EmptyResult;
use Incoviba\Controller\API\withJson;
use Incoviba\Exception\ServiceAction\Read;
use Incoviba\Exception\ServiceAction\Update;
use Incoviba\Exception\ServiceAction\Delete;
use Incoviba\Service;
class Users
@ -27,4 +29,34 @@ class Users
} catch (EmptyResult) {}
return $this->withJson($response, $output);
}
public function edit(ServerRequestInterface $request, ResponseInterface $response,
Service\Login $loginService, int $user_id): ResponseInterface
{
$input = $request->getParsedBody();
$output = [
'input' => array_filter($input, fn($key) => $key !== 'password', ARRAY_FILTER_USE_KEY),
'success' => false,
'user' => null
];
try {
$user = $loginService->editUser($user_id, $input);
$output['success'] = true;
$output['user'] = $user;
} catch (Read|Update) {}
return $this->withJson($response, $output);
}
public function delete(ServerRequestInterface $request, ResponseInterface $response,
Service\Login $loginService, int $user_id): ResponseInterface
{
$output = [
'success' => false,
'user' => null
];
try {
$user = $loginService->deleteUser($user_id);
$output['success'] = true;
$output['user'] = $user;
} catch (Read|Delete) {}
return $this->withJson($response, $output);
}
}

View File

@ -1,14 +1,14 @@
<?php
namespace Incoviba\Controller\API;
use Incoviba\Common\Implement\Exception\EmptyResult;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Incoviba\Common\Implement\Exception\EmptyResult;
use Incoviba\Common\Ideal;
use Incoviba\Repository;
use Incoviba\Service;
use Psr\Log\LoggerInterface;
class Login
class Login extends Ideal\Controller
{
use withJson;
@ -27,7 +27,13 @@ class Login
$loginService->login($user);
$output['token'] = $loginService->getToken();
}
} catch (EmptyResult) {}
} catch (EmptyResult $exception) {
$output['error'] = [
'code' => $exception->getCode(),
'message' => $exception->getMessage(),
'stackTrace' => $exception->getTraceAsString()
];
}
return $this->withJson($response, $output);
}
}

View File

@ -44,7 +44,7 @@ class Toku extends Controller
$body = $request->getBody();
$input = json_decode($body->getContents(), true);
try {
if ($tokuService->updatePago($input['payment_intent'])) {
if ($tokuService->successEvent($input)) {
return $responseFactory->createResponse(204);
}
return $responseFactory->createResponse(409, 'Payment could not be updated');

View File

@ -27,7 +27,7 @@ class API
}
try {
$key = $this->apiService->getKey($request);
} catch (MissingAuthorizationHeader $exception) {
} catch (MissingAuthorizationHeader) {
return $this->responseFactory->createResponse(401);
}
if ($this->validateSimpleKey($request, $key)) {

View File

@ -38,7 +38,7 @@ class Job extends Ideal\Service
public function getPending(): array
{
try {
return array_merge([$this, 'process'],$this->jobRepository->fetchPending());
return array_map([$this, 'process'],$this->jobRepository->fetchPending());
} catch (EmptyResult $exception) {
throw new Read(__CLASS__, $exception);
}

View File

@ -6,6 +6,10 @@ use DateTimeImmutable;
use DateInterval;
use Exception;
use PDOException;
use Incoviba\Exception\ServiceAction\Create;
use Incoviba\Exception\ServiceAction\Delete;
use Incoviba\Exception\ServiceAction\Read;
use Incoviba\Exception\ServiceAction\Update;
use Incoviba\Common\Implement\Exception\EmptyResult;
use Incoviba\Repository;
use Incoviba\Model;
@ -40,6 +44,11 @@ class Login
} catch (PDOException|EmptyResult) {}
return false;
}
/**
* @return Model\User
* @throws Exception
*/
public function getUser(): Model\User
{
$login = $this->repository->fetchActiveBySelector($this->selector);
@ -57,20 +66,90 @@ class Login
return $this->cookie_separator;
}
/**
* @param array $data
* @return Model\User
* @throws Create
* @throws Read
*/
public function addUser(array $data): Model\User
{
try {
return $this->userRepository->fetchByName($data['name']);
} catch (EmptyResult) {
list($passphrase, $encrypted) = $this->splitPassword($data['password']);
$password = $this->cryptoJs_aes_decrypt($encrypted, $passphrase);
try {
$password = $this->cryptoJs_aes_decrypt($encrypted, $passphrase);
} catch (Exception $exception) {
throw new Read(__CLASS__, $exception);
}
$password = password_hash($password, PASSWORD_DEFAULT);
$user = $this->userRepository->create([
'name' => $data['name'],
'password' => $password,
'enabled' => $data['enabled'] ?? 1
]);
return $this->userRepository->save($user);
try {
return $this->userRepository->save($user);
} catch (PDOException $exception) {
throw new Create(__CLASS__, $exception);
}
}
}
/**
* @param int $user_id
* @param array $data
* @return Model\User
* @throws Read
* @throws Update
*/
public function editUser(int $user_id, array $data): Model\User
{
try {
$user = $this->userRepository->fetchById($user_id);
} catch (EmptyResult $exception) {
throw new Read(__CLASS__, $exception);
}
if (!isset($data['force']) and !$user->validate($data['old_password'])) {
throw new Read(__CLASS__);
}
list($passphrase, $encrypted) = $this->splitPassword($data['password']);
try {
$password = $this->cryptoJs_aes_decrypt($encrypted, $passphrase);
} catch (Exception $exception) {
throw new Read(__CLASS__, $exception);
}
$password = password_hash($password, PASSWORD_DEFAULT);
$userData = [
'password' => $password
];
try {
$user = $this->userRepository->edit($user, $userData);
} catch (PDOException | EmptyResult $exception) {
throw new Update(__CLASS__, $exception);
}
return $user;
}
/**
* @param int $user_id
* @return Model\User
* @throws Delete
* @throws Read
*/
public function deleteUser(int $user_id): Model\User
{
try {
$user = $this->userRepository->fetchById($user_id);
} catch (EmptyResult $exception) {
throw new Read(__CLASS__, $exception);
}
try {
$this->userRepository->remove($user);
return $user;
} catch (PDOException | EmptyResult $exception) {
throw new Delete(__CLASS__, $exception);
}
}
public function validateUser(Model\User $user, string $encryptedPassword): bool
@ -88,7 +167,8 @@ class Login
try {
$login = $this->repository->fetchActiveByUser($user->id);
$this->logout($login->user);
} catch (PDOException|EmptyResult) {
} catch (PDOException | EmptyResult $exception) {
error_log($exception, 3, '/logs/exception.log');
}
try {
@ -104,7 +184,8 @@ class Login
$this->repository->save($login);
$this->saveCookie($selector, $token, $login->dateTime->add(new DateInterval("PT{$this->max_login_time}H")));
return true;
} catch (PDOException|Exception) {
} catch (PDOException | Exception $exception) {
error_log($exception, 3, '/logs/exception.log');
return false;
}
}

View File

@ -44,7 +44,7 @@ class Queue extends Ideal\Service
return false;
}
$status = true;
$errors = [];
foreach ($jobs as $job) {
$type = 'default';
if (isset($job->configuration['type'])) {
@ -57,13 +57,15 @@ class Queue extends Ideal\Service
$worker = $this->workers[$type];
try {
$status &= $worker->run($job);
if (!$worker->execute($job)) {
$errors []= $job->id;
}
} catch (Exception $exception) {
$final = new Exception("Could not run job", 0, $exception);
$this->logger->warning($final);
$status &= false;
$errors []= $job->id;
}
}
return $status;
return count($errors) === 0;
}
}

View File

@ -131,12 +131,36 @@ class Toku extends Ideal\Service
return $invoices;
}
/**
* @param array $input
* @return bool
* @throws InvalidResult
*/
public function successEvent(array $input): bool
{
$validEvents = ['payment_intent.succeeded', 'payment.succeeded', 'transaction.success',
'transaction.bulk_success', 'payment_intent.succeeded_batch'];
if (!in_array($input['event_type'], $validEvents)) {
throw new InvalidResult("{$input['event_type']} is not a valid event", 422);
}
switch ($input['event_type']) {
case 'payment_intent.succeeded_batch':
case 'transaction.bulk_success':
return $this->successBulk($input);
case 'transaction.success':
return $this->successTransaction($input);
default:
$paymentData = $this->mapEventData($input);
return $this->updatePago($paymentData);
}
}
/**
* @param array $request
* @return bool
* @throws InvalidResult
*/
public function updatePago(array $request): bool
protected function updatePago(array $request): bool
{
# If $customer is not found, it will throw an exception and stop
$customer = $this->customer->getByExternalId($request['customer']);
@ -144,4 +168,104 @@ class Toku extends Ideal\Service
return $this->invoice->update($invoice['id'], $request);
}
protected function successTransaction(array $input): bool
{
$intents = $this->mapMultiplePaymentIntentsData($input);
$errors = [];
foreach ($intents as $intent) {
if (!$this->updatePago($intent)) {
$errors []= $intent;
}
}
if (array_key_exists('wallet_movements', $input)) {
foreach ($input['wallet_movements'] as $walletMovement) {
if (array_key_exists('type', $walletMovement) and $walletMovement['type'] === 'SURPLUS') {
$this->logger->alert('Revisar el envío de cuotas de la Venta ' . $walletMovement['product_id']);
}
}
}
return count($errors) === 0;
}
/**
* @param array $input
* @return bool
* @throws InvalidResult
*/
protected function successBulk(array $input): bool
{
return match($input['event_type']) {
'payment_intent.succeeded_batch' => $this->successBulkPaymentIntent($input),
'transaction.bulk_success' => $this->successBulkTransaction($input),
default => false
};
}
/**
* @param array $input
* @return bool
* @throws InvalidResult
*/
protected function successBulkTransaction(array $input): bool
{
$errors = [];
foreach($input['events'] as $event) {
$event['event_type'] = 'transaction.success';
if (!$this->successEvent($event)) {
$errors []= $event;
}
}
return count($errors) === 0;
}
/**
* @param array $input
* @return bool
* @throws InvalidResult
*/
protected function successBulkPaymentIntent(array $input): bool
{
$errors = [];
foreach($input['payment_intent'] as $intent) {
$intent['event_type'] = 'payment_intent.succeeded';
$intent['payment_intent'] = $input['payment_intents'];
unset($intent['payment_intents']);
if (!$this->successEvent($intent)) {
$errors []= $intent;
}
}
return count($errors) === 0;
}
protected function mapEventData(array $input): array
{
return match ($input['event_type']) {
'payment_intent.succeeded' => $this->mapPaymentIntentData($input),
'payment.succeeded' => $this->mapPaymentEventData($input),
default => [],
};
}
protected function mapMultiplePaymentIntentsData(array $input): array
{
$output = [];
foreach ($input['payment_intents'] as $intent) {
$intent['transaction_date'] = $input['transaction']['transaction_date'];
$intent['customer'] = $input['customer']['id'];
$intent['invoice'] = $intent['id_invoice'];
$intent['subscription'] = $intent['id_subscription'];
$intent['cuota_id'] = $intent['invoice_external_id'];
$o = $this->mapPaymentIntentData($intent);
$output []= $o;
}
return $output;
}
protected function mapPaymentEventData(array $input): array
{
$data = $input['payment'];
$data['status'] = 'AUTHORIZED';
$data['date'] = $data['payment_date'];
return $data;
}
protected function mapPaymentIntentData(array $input): array
{
$data = $input['payment_intent'];
$data['date'] = $data['transaction_date'];
return $data;
}
}

View File

@ -66,14 +66,15 @@ class Invoice extends AbstractEndPoint
if ($data['status'] !== 'AUTHORIZED') {
throw new InvalidResult("Pago no autorizado", 422);
}
$dateString = $data['date'];
try {
$date = new DateTimeImmutable($data['transaction_date']);
$date = new DateTimeImmutable($dateString);
} catch (DateMalformedStringException $exception) {
throw new InvalidResult("Fecha no válida: {$data['transaction_date']}", 422, $exception);
throw new InvalidResult("Fecha no válida: {$dateString}", 422, $exception);
}
$uf = $this->ufService->get($date);
if ($uf === 0.0) {
throw new InvalidResult("No hay UF para la fecha: {$data['transaction_date']}", 422);
throw new InvalidResult("No hay UF para la fecha: {$dateString}", 422);
}
$valor = $data['amount'] / $uf;
if (abs($valor - $invoice->cuota->pago->valor()) >= 0.0001) {

View File

@ -2,13 +2,14 @@
namespace Incoviba\Service\Worker;
use Incoviba\Common\Implement\Exception\EmptyResponse;
use Incoviba\Service\Worker;
use Psr\Http\Client\ClientExceptionInterface;
use Psr\Http\Client\ClientInterface;
use Psr\Log\LoggerInterface;
use Incoviba\Common\Ideal;
use Incoviba\Model;
class Request extends Ideal\Service
class Request extends Ideal\Service implements Worker
{
public function __construct(LoggerInterface $logger, protected ClientInterface $client)
{