Переглянути джерело

implent the bank system on the backend

gdias 3 тижнів тому
батько
коміт
524d93ec84

+ 14 - 42
controllers/B3CprRegisterController.php

@@ -4,25 +4,23 @@ namespace Controllers;
 
 use Libs\ResponseLib;
 use Psr\Http\Message\ServerRequestInterface;
-use React\Http\Message\Response;
-use Services\B3CprService;
 use Models\CprModel;
 use Models\StatusModel;
-use Models\PaymentModel;
+use Services\PaymentService;
 
 class B3CprRegisterController
 {
-    private B3CprService $service;
     private CprModel $cprModel;
     private StatusModel $statusModel;
-    private PaymentModel $paymentModel;
+    private PaymentService $paymentService;
+
+    private const PAYMENT_VALUE = 10000;
 
     public function __construct()
     {
-        $this->service = new B3CprService();
         $this->cprModel = new CprModel();
         $this->statusModel = new StatusModel();
-        $this->paymentModel = new PaymentModel();
+        $this->paymentService = new PaymentService();
     }
 
     private function applyFixedCprDefaults(array $cpr): array
@@ -61,18 +59,8 @@ class B3CprRegisterController
             return ResponseLib::sendFail('Invalid JSON body', [], 'E_VALIDATE')->withStatus(400);
         }
 
-        $token = $body['b3_access_token'] ?? ($body['access_token'] ?? null);
-        if (!$token) {
-            $b3Auth = $request->getHeaderLine('X-B3-Authorization') ?: '';
-            if (stripos($b3Auth, 'Bearer ') === 0) {
-                $token = trim(substr($b3Auth, 7));
-            }
-        }
-        if (!$token) {
-            $token = $request->getHeaderLine('X-B3-Access-Token') ?: null;
-        }
-
         $cpr = $body['cpr'] ?? null;
+
         if (!is_array($cpr)) {
             $hasCprKeys = false;
             foreach ($body as $k => $_) {
@@ -112,39 +100,23 @@ class B3CprRegisterController
         }
 
         try {
-            $paymentExternalId = 'B3_DIRECT_' . time();
-            $paymentId = $this->paymentModel->create($paymentExternalId, $statusId, $userId);
+            $paymentData = $this->paymentService->initiatePayment(self::PAYMENT_VALUE);
         } catch (\Throwable $e) {
-            return ResponseLib::sendFail('Failed to create payment record: ' . $e->getMessage(), [], 'E_DATABASE')->withStatus(500);
+            return ResponseLib::sendFail('Failed to initiate payment: ' . $e->getMessage(), [], 'E_INTERNAL')->withStatus(500);
         }
 
         try {
-            $record = $this->cprModel->create($cpr, $statusId, $paymentId, $userId, $companyId);
+            $record = $this->cprModel->create($cpr, $statusId, (int)$paymentData['payment_id'], $userId, $companyId);
         } catch (\InvalidArgumentException $e) {
             return ResponseLib::sendFail($e->getMessage(), [], 'E_VALIDATE')->withStatus(400);
         } catch (\Throwable $e) {
             return ResponseLib::sendFail('Failed to create CPR: ' . $e->getMessage(), [], 'E_DATABASE')->withStatus(500);
         }
 
-        try {
-            $payload = $this->service->mapToB3($cpr);
-            if (!$token) {
-                $token = $this->service->getAccessToken();
-            }
-            $result = $this->service->postCpr($token, $payload);
-        } catch (\Throwable $e) {
-            return ResponseLib::sendFail('Failed to send CPR to B3: ' . $e->getMessage(), [], 'E_EXTERNAL')->withStatus(502);
-        }
-
-        if (isset($result['error'])) {
-            return ResponseLib::sendFail('cURL error during B3 CPR request', ['error' => $result['error']], 'E_EXTERNAL')->withStatus(502);
-        }
-
-        $status = (int)($result['status'] ?? 200);
-        if (isset($result['json'])) {
-            return Response::json($result['json'])->withStatus($status ?: 200);
-        }
-
-        return Response::json(['raw' => $result['raw'] ?? null, 'status' => $status])->withStatus($status ?: 502);
+        return ResponseLib::sendOk([
+            'cpr_id' => $record['cpr_id'] ?? null,
+            'payment_id' => $paymentData['payment_id'],
+            'payment_code' => $paymentData['payment_code'],
+        ], 'S_CREATED');
     }
 }

+ 91 - 0
controllers/PaymentConfirmController.php

@@ -0,0 +1,91 @@
+<?php
+
+namespace Controllers;
+
+use Libs\ResponseLib;
+use Models\CprModel;
+use Models\PaymentModel;
+use Psr\Http\Message\ServerRequestInterface;
+use Services\B3CprService;
+
+class PaymentConfirmController
+{
+    private PaymentModel $paymentModel;
+    private CprModel $cprModel;
+    private B3CprService $b3Service;
+
+    public function __construct()
+    {
+        $this->paymentModel = new PaymentModel();
+        $this->cprModel = new CprModel();
+        $this->b3Service = new B3CprService();
+    }
+
+    public function __invoke(ServerRequestInterface $request)
+    {
+        $body = json_decode((string)$request->getBody(), true) ?? [];
+        $paymentId = isset($body['payment_id']) ? (int)$body['payment_id'] : 0;
+
+        if ($paymentId <= 0) {
+            return ResponseLib::sendFail('payment_id inválido', [], 'E_VALIDATE')->withStatus(400);
+        }
+
+        $payment = $this->paymentModel->findById($paymentId);
+        if (!$payment) {
+            return ResponseLib::sendFail('Pagamento não encontrado', [], 'E_NOT_FOUND')->withStatus(404);
+        }
+
+        $statusId = (int)($payment['status_id'] ?? 0);
+        if ($statusId === 0) {
+            return ResponseLib::sendFail('Pagamento ainda não confirmado', ['payment_id' => $paymentId], 'E_PAYMENT_PENDING')->withStatus(409);
+        }
+
+        if ($statusId !== 1) {
+            return ResponseLib::sendFail('Pagamento em status inválido', ['status_id' => $statusId], 'E_PAYMENT_STATUS')->withStatus(409);
+        }
+
+        $cpr = $this->cprModel->findByPaymentId($paymentId);
+        if (!$cpr) {
+            return ResponseLib::sendFail('Nenhuma CPR vinculada ao pagamento', [], 'E_CPR_NOT_FOUND')->withStatus(404);
+        }
+
+        try {
+            $payload = $this->b3Service->mapToB3($cpr);
+            $token = $this->resolveB3Token($request, $body);
+            $result = $this->b3Service->postCpr($token, $payload);
+        } catch (\Throwable $e) {
+            return ResponseLib::sendFail('Falha ao enviar CPR à B3: ' . $e->getMessage(), [], 'E_EXTERNAL')->withStatus(502);
+        }
+
+        if (isset($result['error'])) {
+            return ResponseLib::sendFail('cURL error during B3 CPR request', ['error' => $result['error']], 'E_EXTERNAL')->withStatus(502);
+        }
+
+        return ResponseLib::sendOk([
+            'message' => 'CPR gerada com sucesso',
+            'payment_id' => $paymentId,
+            'b3_response' => $result['json'] ?? ($result['raw'] ?? null),
+        ], 'S_CPR_SENT');
+    }
+
+    private function resolveB3Token(ServerRequestInterface $request, array $body): string
+    {
+        $token = $body['b3_access_token'] ?? ($body['access_token'] ?? null);
+        if (!$token) {
+            $b3Auth = $request->getHeaderLine('X-B3-Authorization') ?: '';
+            if (stripos($b3Auth, 'Bearer ') === 0) {
+                $token = trim(substr($b3Auth, 7));
+            }
+        }
+
+        if (!$token) {
+            $token = $request->getHeaderLine('X-B3-Access-Token') ?: null;
+        }
+
+        if (!$token) {
+            $token = $this->b3Service->getAccessToken();
+        }
+
+        return $token;
+    }
+}

+ 3 - 0
middlewares/JwtAuthMiddleware.php

@@ -52,6 +52,9 @@ class JwtAuthMiddleware
                 ->withAttribute('api_user_id', $userId)
                 ->withAttribute('api_company_id', $user['company_id']);
 
+            $GLOBALS['api_user_id'] = (int)$userId;
+            $GLOBALS['api_company_id'] = (int)$user['company_id'];
+
             return $next($request);
 
         } catch (\Exception $e) {

+ 17 - 0
models/CprModel.php

@@ -323,4 +323,21 @@ class CprModel
 
         return $record ?: null;
     }
+
+    public function findByPaymentId(int $paymentId): ?array
+    {
+        $stmt = $this->pdo->prepare('SELECT * FROM "cpr" WHERE payment_id = :payment_id ORDER BY cpr_id DESC LIMIT 1');
+        $stmt->execute(['payment_id' => $paymentId]);
+        $record = $stmt->fetch(\PDO::FETCH_ASSOC);
+
+        if (!$record) {
+            return null;
+        }
+
+        if (isset($record['cpr_children_codes'])) {
+            $record['cpr_children_codes'] = $this->decodeChildrenCodes((string)$record['cpr_children_codes']);
+        }
+
+        return $record;
+    }
 }

+ 9 - 0
models/PaymentModel.php

@@ -53,4 +53,13 @@ class PaymentModel
 
         return (int)$stmt->fetchColumn();
     }
+
+    public function findById(int $paymentId): ?array
+    {
+        $stmt = $this->pdo->prepare('SELECT * FROM "payment" WHERE payment_id = :payment_id');
+        $stmt->execute(['payment_id' => $paymentId]);
+        $record = $stmt->fetch(\PDO::FETCH_ASSOC);
+
+        return $record ?: null;
+    }
 }

+ 1 - 0
public/index.php

@@ -72,5 +72,6 @@ $app->post('/token/get', $authJwt, \Controllers\TokenGetController::class);
 
 $app->post('/b3/token', \Controllers\B3TokenController::class);
 $app->post('/b3/cpr/register', $authJwt, \Controllers\B3CprRegisterController::class);
+$app->post('/b3/payment/confirm', $authJwt, \Controllers\PaymentConfirmController::class);
 
 $app->run();

+ 87 - 3
services/PaymentService.php

@@ -2,19 +2,103 @@
 
 namespace Services;
 
+use Libs\BashExecutor;
 use Models\PaymentModel;
 
 class PaymentService
 {
+    private const DEFAULT_EXPIRES = 1800;
+    private const DEFAULT_STATUS_ID = 0;
+
     private PaymentModel $paymentModel;
+    private string $wooviCliPath;
 
-    public function __construct()
+    public function __construct(?string $wooviCliPath = null)
     {
         $this->paymentModel = new PaymentModel();
+        $this->wooviCliPath = $wooviCliPath ?? $this->resolveWooviCliPath();
+    }
+
+    public function initiatePayment(int $value): array
+    {
+        if ($value <= 0) {
+            throw new \InvalidArgumentException('Payment value must be greater than zero');
+        }
+
+        $userId = $this->resolveAuthenticatedUserId();
+        $externalId = $this->generateExternalId();
+        $pixCode = $this->runWooviCommand($value, self::DEFAULT_EXPIRES, $externalId);
+
+        $paymentId = $this->paymentModel->create(
+            $externalId,
+            self::DEFAULT_STATUS_ID,
+            $userId,
+            null,
+            '',
+            ''
+        );
+
+        return [
+            'payment_id' => $paymentId,
+            'payment_code' => $pixCode,
+        ];
+    }
+
+    private function resolveAuthenticatedUserId(): int
+    {
+        $userId = $GLOBALS['api_user_id'] ?? null;
+        if (!is_numeric($userId) || (int)$userId <= 0) {
+            throw new \RuntimeException('Authenticated user context not available for payment creation');
+        }
+
+        return (int)$userId;
     }
 
-    public function createPendingPayment(string $externalId, int $statusId, int $userId): int
+    private function generateExternalId(): string
     {
-        return $this->paymentModel->create($externalId, $statusId, $userId, null, null, null);
+        return 'PAY_' . bin2hex(random_bytes(8));
+    }
+
+    private function resolveWooviCliPath(): string
+    {
+        if (!empty($_ENV['WOOVI_CLI_PATH'])) {
+            return $_ENV['WOOVI_CLI_PATH'];
+        }
+
+        return dirname(__DIR__, 2) . '/cli-woovi/woovi';
+    }
+
+    private function runWooviCommand(int $value, int $expires, string $externalId): string
+    {
+        $cliPath = $this->wooviCliPath;
+        if (!is_file($cliPath)) {
+            throw new \RuntimeException('Woovi CLI executable not found at ' . $cliPath);
+        }
+        if (!is_executable($cliPath)) {
+            throw new \RuntimeException('Woovi CLI executable is not executable at ' . $cliPath);
+        }
+
+        $command = sprintf(
+            '%s -value=%d -expires=%d -correlation=%s',
+            escapeshellarg($cliPath),
+            $value,
+            $expires,
+            escapeshellarg($externalId)
+        );
+
+        $result = BashExecutor::run($command, 60);
+        if (($result['exitCode'] ?? 1) !== 0) {
+            $message = $result['error'] ?: $result['output'] ?: 'Unknown Woovi CLI error';
+            throw new \RuntimeException('Woovi CLI failed: ' . $message);
+        }
+
+        $output = trim((string)($result['output'] ?? ''));
+        if ($output === '') {
+            $error = trim((string)($result['error'] ?? ''));
+            $combined = $error !== '' ? $error : 'empty response';
+            throw new \RuntimeException('Woovi CLI did not return a payment code: ' . $combined);
+        }
+
+        return $output;
     }
 }