3
0

2 Ревизии 7acd03a557 ... ff91a55acf

Автор SHA1 Съобщение Дата
  Fernando ff91a55acf feat: orderbook token sell route преди 2 седмици
  Fernando 9569b3a771 feat: cpr token generation after B3 register преди 2 седмици

+ 228 - 2
controllers/PaymentConfirmController.php

@@ -3,22 +3,34 @@
 namespace Controllers;
 
 use Libs\ResponseLib;
+use Models\CommodityModel;
 use Models\CprModel;
 use Models\PaymentModel;
 use Psr\Http\Message\ServerRequestInterface;
 use Services\B3CprService;
+use Services\TokenCreateService;
 
 class PaymentConfirmController
 {
     private PaymentModel $paymentModel;
     private CprModel $cprModel;
     private B3CprService $b3Service;
+    private CommodityModel $commodityModel;
+    private TokenCreateService $tokenCreateService;
+    private \PDO $pdo;
 
     public function __construct()
     {
+        if (!isset($GLOBALS['pdo']) || !$GLOBALS['pdo'] instanceof \PDO) {
+            throw new \RuntimeException('Global PDO connection not initialized');
+        }
+
+        $this->pdo = $GLOBALS['pdo'];
         $this->paymentModel = new PaymentModel();
         $this->cprModel = new CprModel();
         $this->b3Service = new B3CprService();
+        $this->commodityModel = new CommodityModel();
+        $this->tokenCreateService = new TokenCreateService();
     }
 
     public function __invoke(ServerRequestInterface $request)
@@ -61,10 +73,33 @@ class PaymentConfirmController
             return ResponseLib::sendFail('cURL error during B3 CPR request', ['error' => $result['error']], 'E_EXTERNAL')->withStatus(502);
         }
 
+        try {
+            $tokenResult = $this->createTokenFromCpr($cpr);
+        } catch (\Throwable $e) {
+            return ResponseLib::sendFail(
+                'Falha ao gerar token: ' . $e->getMessage(),
+                [],
+                'E_TOKEN_CREATE'
+            )->withStatus(500);
+        }
+
+        try {
+            $this->cprModel->updateTokenId((int)$cpr['cpr_id'], (int)$tokenResult['token_id']);
+        } catch (\Throwable $e) {
+            return ResponseLib::sendFail(
+                'Falha ao vincular token à CPR: ' . $e->getMessage(),
+                [],
+                'E_CPR_UPDATE'
+            )->withStatus(500);
+        }
+
         return ResponseLib::sendOk([
-            'message' => 'CPR gerada com sucesso',
+            'message' => 'CPR enviada e token criado com sucesso',
             'payment_id' => $paymentId,
             'b3_response' => $result['json'] ?? ($result['raw'] ?? null),
+            'token_id' => $tokenResult['token_id'],
+            'token_external_id' => $tokenResult['token_external_id'],
+            'tx_hash' => $tokenResult['tx_hash'],
         ], 'S_CPR_SENT');
     }
 
@@ -88,4 +123,195 @@ class PaymentConfirmController
 
         return $token;
     }
-}
+
+    private function createTokenFromCpr(array $cpr): array
+    {
+        $inputs = $this->prepareTokenInputs($cpr);
+
+        return $this->tokenCreateService->createToken(
+            $inputs['token_commodities_amount'],
+            $inputs['token_commodities_value'],
+            $inputs['token_uf'],
+            $inputs['token_city'],
+            $inputs['token_content'],
+            $inputs['token_flag'],
+            $inputs['wallet_id'],
+            $inputs['chain_id'],
+            $inputs['commodities_id'],
+            $inputs['cpr_id'],
+            $inputs['user_id']
+        );
+    }
+
+    /**
+     * @return array{
+     *     token_commodities_amount:int,
+     *     token_commodities_value:int,
+     *     token_uf:string,
+     *     token_city:string,
+     *     token_content:string,
+     *     token_flag:string,
+     *     wallet_id:int,
+     *     chain_id:int,
+     *     commodities_id:int,
+     *     cpr_id:int,
+     *     user_id:int
+     * }
+     */
+    private function prepareTokenInputs(array $cpr): array
+    {
+        $cprId = (int)($cpr['cpr_id'] ?? 0);
+        if ($cprId <= 0) {
+            throw new \InvalidArgumentException('CPR sem identificador válido.');
+        }
+
+        $userId = (int)($cpr['user_id'] ?? 0);
+        if ($userId <= 0) {
+            throw new \InvalidArgumentException('CPR sem usuário associado.');
+        }
+
+        $companyId = (int)($cpr['company_id'] ?? 0);
+        if ($companyId <= 0) {
+            throw new \InvalidArgumentException('CPR sem empresa associada.');
+        }
+
+        $wallet = $this->findWalletByCompanyId($companyId);
+        $commoditiesName = $this->requireStringField($cpr, ['cpr_product_name'], 'cpr_product_name');
+        $commoditiesId = $this->resolveCommodityId($commoditiesName);
+
+        $tokenCommoditiesAmount = $this->requireNumericField(
+            $cpr,
+            ['cpr_product_quantity', 'cpr_issue_quantity'],
+            'quantidade do produto'
+        );
+        $tokenCommoditiesValue = $this->requireNumericField(
+            $cpr,
+            ['cpr_issue_value', 'cpr_issue_financial_value'],
+            'valor do produto'
+        );
+        $tokenUf = $this->requireStringField(
+            $cpr,
+            ['cpr_deliveryPlace_state_acronym', 'cpr_issuers_state_acronym'],
+            'UF'
+        );
+        $tokenCity = $this->requireStringField(
+            $cpr,
+            ['cpr_deliveryPlace_city_name', 'cpr_issuers_city_name'],
+            'cidade'
+        );
+
+        return [
+            'token_commodities_amount' => $tokenCommoditiesAmount,
+            'token_commodities_value' => $tokenCommoditiesValue,
+            'token_uf' => $tokenUf,
+            'token_city' => $tokenCity,
+            'token_content' => (string)$cprId,
+            'token_flag' => '',
+            'wallet_id' => $wallet['wallet_id'],
+            'chain_id' => $wallet['chain_id'],
+            'commodities_id' => $commoditiesId,
+            'cpr_id' => $cprId,
+            'user_id' => $userId,
+        ];
+    }
+
+    private function findWalletByCompanyId(int $companyId): array
+    {
+        $stmt = $this->pdo->prepare(
+            'SELECT wallet_id, chain_id
+             FROM "wallet"
+             WHERE company_id = :company_id
+             ORDER BY wallet_id ASC
+             LIMIT 1'
+        );
+        $stmt->execute(['company_id' => $companyId]);
+        $wallet = $stmt->fetch(\PDO::FETCH_ASSOC);
+
+        if (!$wallet) {
+            throw new \RuntimeException('Nenhuma carteira encontrada para a empresa informada.');
+        }
+
+        return [
+            'wallet_id' => (int)$wallet['wallet_id'],
+            'chain_id' => (int)$wallet['chain_id'],
+        ];
+    }
+
+    private function resolveCommodityId(string $name): int
+    {
+        $commodityId = $this->commodityModel->getIdByName($name);
+        if ($commodityId === null) {
+            throw new \RuntimeException('Commodity não encontrada para o produto: ' . $name);
+        }
+
+        return $commodityId;
+    }
+
+    private function requireStringField(array $cpr, array $candidates, string $label): string
+    {
+        foreach ($candidates as $field) {
+            if (!array_key_exists($field, $cpr)) {
+                continue;
+            }
+            $value = $this->normalizeStringValue($cpr[$field]);
+            if ($value !== '') {
+                return $value;
+            }
+        }
+
+        throw new \InvalidArgumentException("Campo {$label} ausente ou inválido na CPR.");
+    }
+
+    private function requireNumericField(array $cpr, array $candidates, string $label): int
+    {
+        foreach ($candidates as $field) {
+            if (!array_key_exists($field, $cpr)) {
+                continue;
+            }
+            $value = $this->normalizeNumericValue($cpr[$field]);
+            if ($value !== null) {
+                return $value;
+            }
+        }
+
+        throw new \InvalidArgumentException("Campo {$label} ausente ou inválido na CPR.");
+    }
+
+    private function normalizeStringValue($value): string
+    {
+        if (is_array($value)) {
+            $value = reset($value);
+        }
+
+        if (!is_scalar($value)) {
+            return '';
+        }
+
+        $stringValue = trim((string)$value);
+        if ($stringValue === '') {
+            return '';
+        }
+
+        $parts = preg_split('/\s*;\s*/', $stringValue) ?: [];
+        $first = $parts[0] ?? $stringValue;
+
+        return trim((string)$first);
+    }
+
+    private function normalizeNumericValue($value): ?int
+    {
+        if (is_array($value)) {
+            $value = reset($value);
+        }
+
+        if (is_string($value)) {
+            $value = str_replace([' ', ','], ['', '.'], $value);
+        }
+
+        if (is_numeric($value)) {
+            return (int)round((float)$value);
+        }
+
+        return null;
+    }
+}

+ 95 - 0
controllers/TokenOrderbookController.php

@@ -0,0 +1,95 @@
+<?php
+
+namespace Controllers;
+
+use Libs\ResponseLib;
+use Models\OrderbookModel;
+use Models\TokenModel;
+use Psr\Http\Message\ServerRequestInterface;
+use Respect\Validation\Exceptions\ValidationException;
+use Respect\Validation\Validator as val;
+
+class TokenOrderbookController
+{
+    private TokenModel $tokenModel;
+    private OrderbookModel $orderbookModel;
+    private \PDO $pdo;
+
+    public function __construct()
+    {
+        if (!isset($GLOBALS['pdo']) || !$GLOBALS['pdo'] instanceof \PDO) {
+            throw new \RuntimeException('Global PDO connection not initialized');
+        }
+
+        $this->pdo = $GLOBALS['pdo'];
+        $this->tokenModel = new TokenModel();
+        $this->orderbookModel = new OrderbookModel();
+    }
+
+    public function __invoke(ServerRequestInterface $request)
+    {
+        $body = json_decode((string)$request->getBody(), true) ?? [];
+
+        try {
+            val::key('cpr_id', val::intType()->positive())
+                ->key('value', val::numericVal())
+                ->assert($body);
+        } catch (ValidationException $e) {
+            return ResponseLib::sendFail(
+                'Validation failed: ' . $e->getFullMessage(),
+                [],
+                'E_VALIDATE'
+            )->withStatus(400);
+        }
+
+        $cprId = (int)$body['cpr_id'];
+        $newValue = (int)round((float)$body['value']);
+
+        try {
+            $this->pdo->beginTransaction();
+
+            $token = $this->tokenModel->findByCprId($cprId, true);
+            if (!$token) {
+                $this->pdo->rollBack();
+                return ResponseLib::sendFail(
+                    'Token não encontrado para a CPR informada',
+                    ['cpr_id' => $cprId],
+                    'E_TOKEN_NOT_FOUND'
+                )->withStatus(404);
+            }
+
+            $this->tokenModel->updateCommoditiesValue((int)$token['token_id'], $newValue);
+
+            $orderbook = $this->orderbookModel->create([
+                'orderbook_flag' => '',
+                'orderbook_ts' => time(),
+                'orderbook_is_token' => true,
+                'orderbook_amount' => (string)($token['token_commodities_amount'] ?? '0'),
+                'status_id' => 1,
+                'user_id' => (int)$token['user_id'],
+                'wallet_id' => (int)$token['wallet_id'],
+                'token_id' => (int)$token['token_id'],
+                'currency_id' => null,
+                'chain_id' => (int)$token['chain_id'],
+            ]);
+
+            $this->pdo->commit();
+        } catch (\Throwable $e) {
+            if ($this->pdo->inTransaction()) {
+                $this->pdo->rollBack();
+            }
+
+            return ResponseLib::sendFail(
+                'Falha ao atualizar token e criar ordem: ' . $e->getMessage(),
+                [],
+                'E_ORDERBOOK'
+            )->withStatus(500);
+        }
+
+        return ResponseLib::sendOk([
+            'message' => 'Token atualizado e ordem registrada com sucesso',
+            'token_id' => (int)$token['token_id'],
+            'orderbook_id' => $orderbook['orderbook_id'],
+        ], 'S_ORDERBOOK_CREATED');
+    }
+}

+ 6 - 0
migrations/migration_user_id.sql

@@ -1,6 +1,7 @@
 BEGIN;
 ALTER TABLE "cpr"
     ADD COLUMN user_id INTEGER,
+    ADD COLUMN token_id INTEGER,
     ADD COLUMN company_id INTEGER;
 ALTER TABLE "cpr"
     ADD CONSTRAINT fk_cpr_user
@@ -12,4 +13,9 @@ ALTER TABLE "cpr"
         FOREIGN KEY (company_id)
         REFERENCES "company"(company_id)
         ON DELETE RESTRICT;
+ALTER TABLE "cpr"
+    ADD CONSTRAINT fk_cpr_token
+        FOREIGN KEY (token_id)
+        REFERENCES "token"(token_id)
+        ON DELETE RESTRICT;
 COMMIT;

+ 9 - 0
models/CommodityModel.php

@@ -62,4 +62,13 @@ class CommodityModel
         $stmt->execute(['flag' => $flag]);
         return $stmt->fetchAll(\PDO::FETCH_ASSOC);
     }
+
+    public function getIdByName(string $name): ?int
+    {
+        $stmt = $this->pdo->prepare('SELECT commodities_id FROM "commodities" WHERE LOWER(TRIM(commodities_name)) = LOWER(TRIM(:name)) LIMIT 1');
+        $stmt->execute(['name' => $name]);
+        $id = $stmt->fetchColumn();
+
+        return $id !== false ? (int)$id : null;
+    }
 }

+ 9 - 0
models/CprModel.php

@@ -165,6 +165,15 @@ class CprModel
         return $record;
     }
 
+    public function updateTokenId(int $cprId, int $tokenId): void
+    {
+        $stmt = $this->pdo->prepare('UPDATE "cpr" SET token_id = :token_id WHERE cpr_id = :cpr_id');
+        $stmt->execute([
+            'token_id' => $tokenId,
+            'cpr_id' => $cprId,
+        ]);
+    }
+
     private function normalizeChildrenCodes($value): string
     {
         if (is_array($value)) {

+ 81 - 0
models/OrderbookModel.php

@@ -0,0 +1,81 @@
+<?php
+
+namespace Models;
+
+class OrderbookModel
+{
+    private \PDO $pdo;
+
+    public function __construct()
+    {
+        if (isset($GLOBALS['pdo']) && $GLOBALS['pdo'] instanceof \PDO) {
+            $this->pdo = $GLOBALS['pdo'];
+            return;
+        }
+
+        throw new \RuntimeException('Global PDO connection not initialized');
+    }
+
+    /**
+     * @param array{
+     *   orderbook_flag:string,
+     *   orderbook_ts:int,
+     *   orderbook_is_token:bool,
+     *   orderbook_amount:string,
+     *   status_id:int,
+     *   user_id:int,
+     *   wallet_id:int,
+     *   token_id:int,
+     *   currency_id:?int,
+     *   chain_id:int
+     * } $data
+     */
+    public function create(array $data): array
+    {
+        $stmt = $this->pdo->prepare(
+            'INSERT INTO "orderbook" (
+                orderbook_flag,
+                orderbook_ts,
+                orderbook_is_token,
+                orderbook_amount,
+                status_id,
+                user_id,
+                wallet_id,
+                token_id,
+                currency_id,
+                chain_id
+            ) VALUES (
+                :orderbook_flag,
+                :orderbook_ts,
+                :orderbook_is_token,
+                :orderbook_amount,
+                :status_id,
+                :user_id,
+                :wallet_id,
+                :token_id,
+                :currency_id,
+                :chain_id
+            ) RETURNING orderbook_id'
+        );
+
+        $stmt->execute([
+            'orderbook_flag' => $data['orderbook_flag'],
+            'orderbook_ts' => $data['orderbook_ts'],
+            'orderbook_is_token' => $data['orderbook_is_token'],
+            'orderbook_amount' => $data['orderbook_amount'],
+            'status_id' => $data['status_id'],
+            'user_id' => $data['user_id'],
+            'wallet_id' => $data['wallet_id'],
+            'token_id' => $data['token_id'],
+            'currency_id' => $data['currency_id'],
+            'chain_id' => $data['chain_id'],
+        ]);
+
+        $orderbookId = (int)$stmt->fetchColumn();
+
+        return [
+            'orderbook_id' => $orderbookId,
+            'orderbook_ts' => $data['orderbook_ts'],
+        ];
+    }
+}

+ 44 - 0
models/TokenModel.php

@@ -58,5 +58,49 @@ class TokenModel
 
         return $stmt->fetchAll(\PDO::FETCH_ASSOC);
     }
+
+    public function findByCprId(int $cprId, bool $forUpdate = false): ?array
+    {
+        $sql = 'SELECT token_id,
+                    token_external_id,
+                    token_commodities_amount,
+                    token_commodities_value,
+                    token_uf,
+                    token_city,
+                    token_content,
+                    token_flag,
+                    wallet_id,
+                    chain_id,
+                    commodities_id,
+                    cpr_id,
+                    user_id
+             FROM "token"
+             WHERE cpr_id = :cpr_id
+            ORDER BY token_id DESC
+            LIMIT 1';
+
+        if ($forUpdate) {
+            $sql .= ' FOR UPDATE';
+        }
+
+        $stmt = $this->pdo->prepare($sql);
+        $stmt->execute(['cpr_id' => $cprId]);
+        $record = $stmt->fetch(\PDO::FETCH_ASSOC);
+
+        return $record ?: null;
+    }
+
+    public function updateCommoditiesValue(int $tokenId, float $value): void
+    {
+        $stmt = $this->pdo->prepare(
+            'UPDATE "token"
+             SET token_commodities_value = :value
+             WHERE token_id = :token_id'
+        );
+        $stmt->execute([
+            'value' => $value,
+            'token_id' => $tokenId,
+        ]);
+    }
 }
 

+ 1 - 0
public/index.php

@@ -70,6 +70,7 @@ $app->post('/cpr/history', $authJwt, \Controllers\CprQueryController::class);
 
 $app->post('/wallet/tokens', $authJwt, \Controllers\WalletTokensController::class);
 $app->post('/token/get', $authJwt, \Controllers\TokenGetController::class);
+$app->post('/token/orderbook', $authJwt, \Controllers\TokenOrderbookController::class);
 
 $app->post('/b3/token', \Controllers\B3TokenController::class);
 $app->post('/b3/cpr/register', $authJwt, \Controllers\B3CprRegisterController::class);