458 lines
17 KiB
PHP
458 lines
17 KiB
PHP
<?php
|
|
namespace Incoviba\Service\Venta\MediosPago;
|
|
|
|
use InvalidArgumentException;
|
|
use Psr\Http\Message\ServerRequestInterface;
|
|
use Incoviba\Common\Define\Connection;
|
|
use Incoviba\Common\Ideal;
|
|
use Incoviba\Common\Implement\Exception\EmptyResponse;
|
|
use Incoviba\Exception\InvalidResult;
|
|
use Incoviba\Exception\ServiceAction\Read;
|
|
use Incoviba\Model;
|
|
use Incoviba\Model\Persona;
|
|
use Incoviba\Model\Venta\Propietario;
|
|
use Incoviba\Service\HMAC;
|
|
use Incoviba\Service\Venta\MediosPago\Toku\{Customer,Subscription,Invoice};
|
|
use Psr\Log\LoggerInterface;
|
|
use Throwable;
|
|
|
|
class Toku extends Ideal\Service
|
|
{
|
|
const string CUSTOMER = 'customer';
|
|
const string SUBSCRIPTION = 'subscription';
|
|
const string INVOICE = 'invoice';
|
|
|
|
protected Customer $customer;
|
|
protected Subscription $subscription;
|
|
protected Invoice $invoice;
|
|
|
|
public function __construct(LoggerInterface $logger, protected Connection $connection)
|
|
{
|
|
parent::__construct($logger);
|
|
}
|
|
|
|
public function register(string $type, EndPoint $endPoint): self
|
|
{
|
|
if (!in_array(strtolower($type), ['customer', 'subscription', 'invoice'])) {
|
|
throw new InvalidArgumentException("{$type} is not a valid type of EndPoint for " . __CLASS__);
|
|
}
|
|
$this->{strtolower($type)} = $endPoint;
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* @param Persona|Propietario $persona
|
|
* @return array
|
|
* @throws InvalidResult
|
|
*/
|
|
public function sendPersona(Model\Persona|Model\Venta\Propietario $persona): array
|
|
{
|
|
$rut = implode('', [$persona->rut, strtoupper($persona->dv)]);
|
|
try {
|
|
return $this->customer->getById($rut);
|
|
} catch (InvalidResult $exception) {
|
|
$datos = $persona->datos;
|
|
$customerData = [
|
|
'rut' => $rut,
|
|
'nombreCompleto' => $persona->nombreCompleto(),
|
|
'email' => $datos?->email ?? '',
|
|
'telefono' => $datos?->telefono ?? ''
|
|
];
|
|
try {
|
|
if (!$this->customer->add($customerData)) {
|
|
throw new InvalidResult("Could not save Customer for Persona {$rut}", 409, $exception);
|
|
}
|
|
} catch (EmptyResponse $exception) {
|
|
throw new InvalidResult("Could not save Customer for Persona {$rut}", 409, $exception);
|
|
}
|
|
return $this->customer->getById($rut);
|
|
}
|
|
}
|
|
/**
|
|
* @param Model\Venta $venta
|
|
* @return array
|
|
* @throws InvalidResult
|
|
*/
|
|
public function sendVenta(Model\Venta $venta): array
|
|
{
|
|
$customer = $this->sendPersona($venta->propietario());
|
|
try {
|
|
return $this->subscription->getById($venta->id);
|
|
} catch (InvalidResult $exception) {
|
|
$subscriptionData = [
|
|
'customer' => $customer['toku_id'],
|
|
'product_id' => $venta->id,
|
|
'venta' => $venta
|
|
];
|
|
try {
|
|
if (!$this->subscription->add($subscriptionData)) {
|
|
throw new InvalidResult("Could not save Subscription for Venta {$venta->id}", 409, $exception);
|
|
}
|
|
} catch (EmptyResponse $exception) {
|
|
throw new InvalidResult("Could not save Subscription for Venta {$venta->id}", 409, $exception);
|
|
}
|
|
return $this->subscription->getById($venta->id);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param Model\Venta $venta
|
|
* @param array $cuotas_ids
|
|
* @return array
|
|
* @throws InvalidResult
|
|
*/
|
|
public function sendCuotas(Model\Venta $venta, array $cuotas_ids = []): array
|
|
{
|
|
$customer = $this->sendPersona($venta->propietario());
|
|
$subscription = $this->sendVenta($venta);
|
|
|
|
$customerInvoices = [];
|
|
try {
|
|
$customerInvoices = $this->invoice->getByCustomer($customer['toku_id']);
|
|
$customerInvoices = array_filter($customerInvoices, function($invoiceRow) use ($subscription) {
|
|
return $invoiceRow['subscription'] === $subscription['toku_id'];
|
|
});
|
|
} catch (EmptyResponse) {}
|
|
|
|
$invoices = [];
|
|
$errors = [];
|
|
foreach ($venta->formaPago()->pie->cuotas() as $cuota) {
|
|
if (count($cuotas_ids) > 0 and !in_array($cuota->id, $cuotas_ids)) {
|
|
continue;
|
|
}
|
|
if (count($customerInvoices) and in_array($cuota->id, array_column($customerInvoices, 'invoice_external_id'))) {
|
|
$invoice = array_find($customerInvoices, function($invoiceRow) use ($cuota) {
|
|
return $invoiceRow['invoice_external_id'] === $cuota->id;
|
|
});
|
|
if ($invoice !== null) {
|
|
$invoices []= $invoice;
|
|
$this->invoice->save($invoice);
|
|
continue;
|
|
}
|
|
}
|
|
try {
|
|
$invoices []= $this->invoice->getById($cuota->id);
|
|
} catch (InvalidResult $exception) {
|
|
try {
|
|
$invoiceData = [
|
|
'customer' => $customer['toku_id'],
|
|
'product_id' => $venta->id,
|
|
'subscription' => $subscription['toku_id'],
|
|
'cuota' => $cuota,
|
|
'venta' => $venta
|
|
];
|
|
if (!$this->invoice->add($invoiceData)) {
|
|
throw new EmptyResponse("Could not add Invoice for Cuota {$cuota->id}", $exception);
|
|
}
|
|
$invoices []= $this->invoice->getById($cuota->id);
|
|
} catch (EmptyResponse $exception) {
|
|
$this->logger->warning($exception);
|
|
$errors []= $exception;
|
|
}
|
|
}
|
|
}
|
|
if (count($errors) > 0) {
|
|
$this->logger->warning("Revisar el envío de cuotas de la Venta {$venta->id}");
|
|
}
|
|
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)) {
|
|
$this->logger->warning("{$input['event_type']} is not a valid event");
|
|
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);
|
|
}
|
|
}
|
|
public function check(bool $update = false): array
|
|
{
|
|
try {
|
|
list('existingSubscriptions' => $existingSubscriptions, 'missingVentas' => $missingVentas) = $this->subscription->check();
|
|
} catch (Read) {
|
|
return [];
|
|
}
|
|
|
|
$queues = [];
|
|
if (count($missingVentas) > 0) {
|
|
foreach ($missingVentas as $venta) {
|
|
$cuotas = $venta->formaPago()->pie->cuotas();
|
|
if (count($cuotas) === 0) {
|
|
continue;
|
|
}
|
|
$queueData = [
|
|
'type' => 'request',
|
|
'url' => "/api/external/toku/cuotas/{$venta->id}",
|
|
'method' => 'post',
|
|
'body' => ['cuotas' => array_map(function(Model\Venta\Cuota $cuota) {return $cuota->id;}, $cuotas)]
|
|
];
|
|
$queues []= $queueData;
|
|
}
|
|
}
|
|
if ($update and count($existingSubscriptions) > 0) {
|
|
foreach ($existingSubscriptions as $subscription) {
|
|
$cuotas = $subscription->venta->formaPago()->pie->cuotas();
|
|
if (count($cuotas) === 0) {
|
|
continue;
|
|
}
|
|
|
|
$propietario = $subscription->venta->propietario();
|
|
try {
|
|
$customer = $this->customer->getById($propietario->rut());
|
|
} catch (InvalidResult) {
|
|
continue;
|
|
}
|
|
try {
|
|
$editData = [
|
|
'rut' => $customer['rut'],
|
|
'nombreCompleto' => $propietario->nombreCompleto(),
|
|
'email' => $propietario->datos?->email ?? '',
|
|
'telefono' => $propietario->datos?->telefono ?? ''
|
|
];
|
|
$this->customer->edit($customer['toku_id'], $editData);
|
|
} catch (EmptyResponse $exception) {
|
|
$this->logger->warning($exception);
|
|
}
|
|
foreach ($cuotas as $cuota) {
|
|
try {
|
|
$invoice = $this->invoice->getById($cuota->id);
|
|
$editData = [
|
|
'customer' => $customer['toku_id'],
|
|
'product_id' => $subscription->venta->id,
|
|
'subscription' => $subscription->toku_id,
|
|
'cuota' => $cuota,
|
|
'venta' => $subscription->venta
|
|
];
|
|
try {
|
|
$this->invoice->edit($invoice['toku_id'], $editData);
|
|
} catch (EmptyResponse) {}
|
|
} catch (InvalidResult) {
|
|
$invoiceData = [
|
|
'customer' => $customer['toku_id'],
|
|
'product_id' => $subscription->venta->id,
|
|
'subscription' => $subscription->toku_id,
|
|
'cuota' => $cuota,
|
|
'venta' => $subscription->venta
|
|
];
|
|
try {
|
|
$this->invoice->add($invoiceData);
|
|
} catch (EmptyResponse) {}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return $queues;
|
|
}
|
|
public function reset(array $skips = []): array
|
|
{
|
|
$output = [];
|
|
try {
|
|
$output['customer'] = $this->customer->reset($skips['customer'] ?? []);
|
|
$output['subscription'] = $this->subscription->reset($skips['subscription'] ?? []);
|
|
$output['invoice'] = $this->invoice->reset($skips['invoice'] ?? []);
|
|
} catch (InvalidResult $exception) {
|
|
$this->logger->warning($exception);
|
|
return [];
|
|
}
|
|
return $output;
|
|
}
|
|
public function queue(array $venta_ids): array
|
|
{
|
|
$queues = [];
|
|
foreach ($venta_ids as $venta_id) {
|
|
if ($this->subscription->queue($venta_id)) {
|
|
$queues []= [
|
|
'type' => 'request',
|
|
'url' => "/api/external/toku/cuotas/{$venta_id}",
|
|
'method' => 'post',
|
|
'body' => []
|
|
];
|
|
}
|
|
}
|
|
return $queues;
|
|
}
|
|
|
|
/**
|
|
* @param array $request
|
|
* @return bool
|
|
* @throws InvalidResult
|
|
*/
|
|
protected function updatePago(array $request): bool
|
|
{
|
|
# If $customer is not found, it will throw an exception and stop
|
|
$customer = $this->customer->getByExternalId($request['customer']);
|
|
$invoice = $this->invoice->getByExternalId($request['invoice']);
|
|
|
|
return $this->invoice->update($invoice['toku_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'];
|
|
if (!array_key_exists('amount', $data) and array_key_exists('payment_amount', $data)) {
|
|
$data['amount'] = $data['payment_amount'];
|
|
}
|
|
$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;
|
|
}
|
|
|
|
public function validateToken(ServerRequestInterface $request, array $tokenConfig): bool
|
|
{
|
|
if (!$request->hasHeader('User-Agent') or !str_starts_with($request->getHeaderLine('User-Agent'), 'Toku-Webhooks')) {
|
|
return false;
|
|
}
|
|
if (!$request->hasHeader('X-Datadog-Tags') or !$request->hasHeader('Tracestate')) {
|
|
return false;
|
|
}
|
|
if (!$request->hasHeader('Toku-Signature')) {
|
|
return false;
|
|
}
|
|
|
|
$tokuSignature = $request->getHeaderLine('Toku-Signature');
|
|
try {
|
|
list($timestamp, $signature) = array_map(function($elem) {
|
|
return explode('=', $elem)[1];
|
|
}, explode(',', $tokuSignature));
|
|
$body = $request->getBody()->getContents();
|
|
$json = json_decode($body, true);
|
|
if (!is_array($json)) {
|
|
return false;
|
|
}
|
|
if (!array_key_exists('id', $json)) {
|
|
return false;
|
|
}
|
|
$eventId = $json['id'];
|
|
$eventType = $json['event_type'];
|
|
|
|
$query = $this->connection->getQueryBuilder()
|
|
->select('secret')
|
|
->from('toku_webhooks')
|
|
->where('enabled = ? AND events LIKE ?');
|
|
$params = [true, "%\"{$eventType}\"%"];
|
|
$statement = $this->connection->prepare($query);
|
|
$statement->execute($params);
|
|
$results = $statement->fetchColumn();
|
|
if (count($results) === 0) {
|
|
return false;
|
|
}
|
|
|
|
if (array_any($results, fn($secret) => HMAC::validate($timestamp, $signature, $eventId, $secret))) {
|
|
return true;
|
|
}
|
|
|
|
} catch (Throwable $throwable) {
|
|
$this->logger->error($throwable);
|
|
}
|
|
return false;
|
|
}
|
|
}
|