diff --git a/.cursor/rules/rules.mdc b/.cursor/rules/rules.mdc new file mode 100644 index 00000000..8843aa52 --- /dev/null +++ b/.cursor/rules/rules.mdc @@ -0,0 +1,469 @@ +--- +description: +globs: +alwaysApply: true +--- +--- +description: +globs: +alwaysApply: false +--- +# Mercado Pago PHP SDK - Cursor Rules + +## Visão Geral do Projeto +Este é o SDK oficial do Mercado Pago para PHP, uma biblioteca de processamento de pagamentos que segue as melhores práticas PHP e convenções PSR. O SDK é distribuído via Composer e segue uma arquitetura modular com clara separação de responsabilidades. + +## Estrutura e Organização do Projeto + +### Estrutura de Diretórios Principal +``` +src/ +├── MercadoPago/ # Namespace principal +│ ├── Client/ # Clientes de API +│ │ ├── Base/ # Classes base +│ │ └── Resource/ # Clientes específicos +│ ├── Config/ # Configurações +│ │ └── Config.php # Classe de configuração +│ ├── Core/ # Funcionalidades principais +│ │ ├── Annotation/ # Anotações personalizadas +│ │ └── Http/ # Cliente HTTP +│ ├── Entity/ # Entidades/Modelos +│ │ ├── Payment.php # Modelo de pagamento +│ │ └── Preference.php # Modelo de preferência +│ ├── Exception/ # Exceções personalizadas +│ └── Utils/ # Utilitários +tests/ +├── Unit/ # Testes unitários +└── Integration/ # Testes de integração +``` + +## Convenções de Nomenclatura + +### Classes e Interfaces +- **Namespaces**: `MercadoPago\{Module}` (ex: `MercadoPago\Client`) +- **Clientes**: `{Resource}Client` (ex: `PaymentClient`, `PreferenceClient`) +- **Modelos**: Nomes em PascalCase (ex: `Payment`, `Preference`) +- **Interfaces**: Sufixo `Interface` (ex: `ClientInterface`, `PaymentInterface`) +- **Exceções**: Sufixo `Exception` (ex: `MPException`, `ValidationException`) + +### Métodos e Variáveis +- Métodos em camelCase +- Propriedades em camelCase +- Constantes em SCREAMING_SNAKE_CASE +- Parâmetros em camelCase + +### Arquivos e Diretórios +- Arquivos .php em PascalCase +- Diretórios em PascalCase +- Testes com sufixo Test +- PSR-4 autoloading + +## Padrões de Código + +### Estilo PHP +```php +post(self::PATH, $paymentData); + return Payment::fromArray($response); + } catch (Exception $e) { + throw new MPException('Error creating payment', 0, $e); + } + } +} +``` + +### Modelos +```php +populate($data); + } + + public function jsonSerialize(): array + { + return [ + 'id' => $this->id, + 'amount' => $this->amount, + 'status' => $this->status, + 'date_created' => $this->dateCreated->format(DATE_ISO8601), + 'external_reference' => $this->externalReference, + 'metadata' => $this->metadata, + ]; + } + + public static function fromArray(array $data): self + { + return new self($data); + } +} +``` + +### Configuração +```php +accessToken; + } + + public function getTimeout(): int + { + return $this->timeout ?? self::DEFAULT_TIMEOUT; + } + + public function getBaseUrl(): string + { + return $this->baseUrl ?? self::DEFAULT_BASE_URL; + } +} +``` + +## Práticas de Desenvolvimento + +### Geral +- Seguir PSR-12 para estilo de código +- Usar type hints e return types +- Implementar interfaces para contratos +- Seguir princípios SOLID +- Documentar com PHPDoc + +### Tratamento de Erros +```php +errorCode = $errorCode; + $this->details = $details; + } + + public function getErrorCode(): string + { + return $this->errorCode; + } + + public function getDetails(): array + { + return $this->details; + } +} +``` + +### Logging +```php +logger->info('Processing payment', ['data' => $paymentData]); + + try { + $payment = $this->client->create($paymentData); + $this->logger->info('Payment processed', ['id' => $payment->getId()]); + return $payment; + } catch (Exception $e) { + $this->logger->error('Payment processing failed', [ + 'error' => $e->getMessage(), + 'data' => $paymentData + ]); + throw $e; + } + } +} +``` + +## Testes + +### PHPUnit +```php +httpClient = $this->createMock(HttpClientInterface::class); + $this->client = new PaymentClient($this->httpClient, new Config('test-token')); + } + + public function testCreatePayment(): void + { + // Arrange + $paymentData = [ + 'amount' => 100.0, + 'description' => 'Test payment' + ]; + + $expected = [ + 'id' => 'test-id', + 'status' => 'pending' + ]; + + $this->httpClient + ->expects($this->once()) + ->method('post') + ->with('/v1/payments', $paymentData) + ->willReturn($expected); + + // Act + $result = $this->client->create($paymentData); + + // Assert + $this->assertInstanceOf(Payment::class, $result); + $this->assertEquals('test-id', $result->getId()); + $this->assertEquals('pending', $result->getStatus()); + } +} +``` + +## Documentação + +### PHPDoc +```php +/** + * Processa um pagamento usando a API do Mercado Pago. + * + * @param array $paymentData Os dados do pagamento + * @return Payment O pagamento processado + * @throws PaymentProcessingException Se houver erro no processamento + * @throws ValidationException Se os dados forem inválidos + */ +public function processPayment(array $paymentData): Payment +{ + // Implementação +} +``` + +## Gerenciamento de Dependências + +### composer.json +```json +{ + "name": "mercadopago/sdk-php", + "description": "Mercado Pago PHP SDK", + "type": "library", + "require": { + "php": ">=8.1", + "guzzlehttp/guzzle": "^7.0", + "psr/log": "^3.0", + "symfony/validator": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.5", + "phpstan/phpstan": "^1.0", + "squizlabs/php_codesniffer": "^3.6" + }, + "autoload": { + "psr-4": { + "MercadoPago\\": "src/MercadoPago" + } + }, + "autoload-dev": { + "psr-4": { + "Tests\\": "tests/" + } + } +} +``` + +## Segurança + +### Práticas +- Validar todas as entradas +- Usar HTTPS sempre +- Implementar rate limiting +- Seguir OWASP guidelines + +### Validação +```php +validate($this); + } +} +``` + +## Performance + +### Otimizações +- Usar connection pooling +- Implementar caching +- Configurar timeouts +- Usar async quando apropriado + +### Cache +```php +cache->getItem("payment.$id"); + return $item->isHit() ? $item->get() : null; + } + + public function save(Payment $payment): void + { + $item = $this->cache->getItem("payment.{$payment->getId()}"); + $item->set($payment); + $item->expiresAfter(3600); + $this->cache->save($item); + } +} +``` + +## Qualidade de Código + +### Ferramentas +- PHP_CodeSniffer para estilo +- PHPStan para análise estática +- PHPUnit para testes +- SonarQube para qualidade + +### CI/CD +- GitHub Actions +- Composer scripts +- Testes automatizados +- Deploy automático + +## Configuração do Editor + +### VSCode/PHPStorm +```json +{ + "php.suggest.basic": false, + "php.validate.enable": true, + "php.format.rules.alignLineComments": true, + "php.format.rules.spacesBeforeBrace": true +} +``` + +## Scripts Composer + +### composer.json +```json +{ + "scripts": { + "test": "phpunit", + "test:coverage": "phpunit --coverage-html coverage", + "cs": "phpcs --standard=PSR12 src tests", + "cs:fix": "phpcbf --standard=PSR12 src tests", + "stan": "phpstan analyse src tests", + "check": [ + "@cs", + "@stan", + "@test" + ] + } +} +``` diff --git a/examples/Chargeback/Get.php b/examples/Chargeback/Get.php new file mode 100644 index 00000000..47739193 --- /dev/null +++ b/examples/Chargeback/Get.php @@ -0,0 +1,39 @@ +"); + +// Step 3: Initialize the API client +$client = new ChargebackClient(); + +try { + // Step 4: Get chargeback by ID + $chargeback_id = "CHARGEBACK_ID"; + $chargeback = $client->get($chargeback_id); + + echo "Chargeback ID: " . $chargeback->id . "\n"; + echo "Payment ID: " . $chargeback->payment_id . "\n"; + echo "Amount: " . $chargeback->amount . "\n"; + echo "Status: " . $chargeback->status . "\n"; + echo "Reason: " . $chargeback->reason . "\n"; + echo "Stage: " . $chargeback->stage . "\n"; + + // Step 5: Handle exceptions +} catch (MPApiException $e) { + echo "Status code: " . $e->getApiResponse()->getStatusCode() . "\n"; + echo "Content: "; + var_dump($e->getApiResponse()->getContent()); + echo "\n"; +} catch (\Exception $e) { + echo $e->getMessage(); +} \ No newline at end of file diff --git a/examples/Chargeback/Search.php b/examples/Chargeback/Search.php new file mode 100644 index 00000000..962338e7 --- /dev/null +++ b/examples/Chargeback/Search.php @@ -0,0 +1,57 @@ +"); + +// Step 3: Initialize the API client +$client = new ChargebackClient(); + +try { + // Step 4: Create search filters + $search_filters = array( + "payment_id" => "PAYMENT_ID", // Optional: filter by payment ID + "status" => "open", // Optional: filter by status (open, closed, etc.) + "stage" => "chargeback", // Optional: filter by stage + "offset" => 0, + "limit" => 50 + ); + + $search_request = new MPSearchRequest($search_filters); + + // Step 5: Perform the search + $search_result = $client->search($search_request); + + echo "Total results: " . $search_result->paging->total . "\n"; + echo "Results found: " . count($search_result->results) . "\n\n"; + + // Step 6: Display results + foreach ($search_result->results as $chargeback) { + echo "Chargeback ID: " . $chargeback->id . "\n"; + echo "Payment ID: " . $chargeback->payment_id . "\n"; + echo "Amount: " . $chargeback->amount . " " . $chargeback->currency . "\n"; + echo "Status: " . $chargeback->status . "\n"; + echo "Reason: " . $chargeback->reason . "\n"; + echo "Date Created: " . $chargeback->date_created . "\n"; + echo "---\n"; + } + + // Step 7: Handle exceptions +} catch (MPApiException $e) { + echo "Status code: " . $e->getApiResponse()->getStatusCode() . "\n"; + echo "Content: "; + var_dump($e->getApiResponse()->getContent()); + echo "\n"; +} catch (\Exception $e) { + echo $e->getMessage(); +} \ No newline at end of file diff --git a/src/MercadoPago/Client/Chargeback/ChargebackClient.php b/src/MercadoPago/Client/Chargeback/ChargebackClient.php new file mode 100644 index 00000000..51a9bce2 --- /dev/null +++ b/src/MercadoPago/Client/Chargeback/ChargebackClient.php @@ -0,0 +1,62 @@ +getContent()); + $result->setResponse($response); + return $result; + } + + /** + * Method responsible for search chargebacks. + * @param \MercadoPago\Net\MPSearchRequest $request search request. + * @param \MercadoPago\Client\Common\RequestOptions request options to be sent. + * @return \MercadoPago\Resources\ChargebackSearch search results. + * @throws \MercadoPago\Exceptions\MPApiException if the request fails. + * @throws \Exception if the request fails. + */ + public function search(MPSearchRequest $request, ?RequestOptions $request_options = null): ChargebackSearch + { + $query_params = isset($request) ? $request->getParameters() : null; + $response = parent::send(self::URL_SEARCH, HttpMethod::GET, null, $query_params, $request_options); + $result = Serializer::deserializeFromJson(ChargebackSearch::class, $response->getContent()); + $result->setResponse($response); + return $result; + } +} \ No newline at end of file diff --git a/src/MercadoPago/Resources/Chargeback.php b/src/MercadoPago/Resources/Chargeback.php new file mode 100644 index 00000000..1db8efc8 --- /dev/null +++ b/src/MercadoPago/Resources/Chargeback.php @@ -0,0 +1,90 @@ + "MercadoPago\Resources\Common\Source", + "payment_method" => "MercadoPago\Resources\Payment\PaymentMethod", + "transaction_details" => "MercadoPago\Resources\Payment\TransactionDetails", + ]; + + /** + * Method responsible for getting map of entities. + */ + public function getMap(): array + { + return $this->map; + } +} \ No newline at end of file diff --git a/src/MercadoPago/Resources/ChargebackSearch.php b/src/MercadoPago/Resources/ChargebackSearch.php new file mode 100644 index 00000000..c3968dd6 --- /dev/null +++ b/src/MercadoPago/Resources/ChargebackSearch.php @@ -0,0 +1,32 @@ + "MercadoPago\Resources\Chargeback", + "paging" => "MercadoPago\Resources\Common\Paging", + ]; + + /** + * Method responsible for getting map of entities. + */ + public function getMap(): array + { + return $this->map; + } +} \ No newline at end of file diff --git a/tests/MercadoPago/Client/Unit/Chargeback/ChargebackClientUnitTest.php b/tests/MercadoPago/Client/Unit/Chargeback/ChargebackClientUnitTest.php new file mode 100644 index 00000000..49885e2a --- /dev/null +++ b/tests/MercadoPago/Client/Unit/Chargeback/ChargebackClientUnitTest.php @@ -0,0 +1,56 @@ +mockHttpRequest($filepath, 200); + + $http_client = new MPDefaultHttpClient($mock_http_request); + MercadoPagoConfig::setHttpClient($http_client); + + $client = new ChargebackClient(); + $chargeback = $client->get("123456"); + + $this->assertSame(200, $chargeback->getResponse()->getStatusCode()); + $this->assertSame("123456", $chargeback->id); + $this->assertSame(987654321, $chargeback->payment_id); + $this->assertSame(100.0, $chargeback->amount); + $this->assertSame("BRL", $chargeback->currency); + $this->assertSame("fraud", $chargeback->reason); + $this->assertSame("chargeback", $chargeback->stage); + $this->assertSame("open", $chargeback->status); + } + + public function testSearchSuccess(): void + { + $filepath = '../../../../Resources/Mocks/Response/Chargeback/chargeback_search.json'; + $mock_http_request = $this->mockHttpRequest($filepath, 200); + + $http_client = new MPDefaultHttpClient($mock_http_request); + MercadoPagoConfig::setHttpClient($http_client); + + $client = new ChargebackClient(); + $filters = array("payment_id" => "987654321"); + $search_request = new MPSearchRequest(50, 0, $filters); + $search_result = $client->search($search_request); + + $this->assertSame(200, $search_result->getResponse()->getStatusCode()); + $this->assertSame(1, $search_result->paging->total); + $this->assertSame(1, count($search_result->results)); + $this->assertSame("123456", $search_result->results[0]->id); + $this->assertSame(987654321, $search_result->results[0]->payment_id); + } +} \ No newline at end of file diff --git a/tests/MercadoPago/Resources/Mocks/Response/Chargeback/chargeback_base.json b/tests/MercadoPago/Resources/Mocks/Response/Chargeback/chargeback_base.json new file mode 100644 index 00000000..f6f8983d --- /dev/null +++ b/tests/MercadoPago/Resources/Mocks/Response/Chargeback/chargeback_base.json @@ -0,0 +1,42 @@ +{ + "id": "123456", + "payment_id": 987654321, + "amount": 100.0, + "currency": "BRL", + "reason": "fraud", + "stage": "chargeback", + "status": "open", + "date_created": "2023-01-15T10:30:00.000-04:00", + "date_last_updated": "2023-01-15T10:30:00.000-04:00", + "documentation_deadline": "2023-01-22T23:59:59.000-04:00", + "coverage_applied": false, + "coverage_eligible": true, + "external_reference": "ext_ref_123", + "metadata": { + "key1": "value1", + "key2": "value2" + }, + "documentation_status": "pending", + "chargeback_sequence_number": 1, + "source": { + "id": "475845652", + "name": "Test Source", + "type": "collector" + }, + "case": { + "id": "case_123", + "number": "CB-123456789", + "type": "chargeback" + }, + "risk_score": 75.5, + "payment_method": { + "id": "visa", + "type": "credit_card" + }, + "transaction_details": { + "net_received_amount": 95.0, + "total_paid_amount": 100.0, + "overpaid_amount": 0, + "installment_amount": 100.0 + } +} \ No newline at end of file diff --git a/tests/MercadoPago/Resources/Mocks/Response/Chargeback/chargeback_search.json b/tests/MercadoPago/Resources/Mocks/Response/Chargeback/chargeback_search.json new file mode 100644 index 00000000..a162707c --- /dev/null +++ b/tests/MercadoPago/Resources/Mocks/Response/Chargeback/chargeback_search.json @@ -0,0 +1,51 @@ +{ + "paging": { + "total": 1, + "limit": 50, + "offset": 0 + }, + "results": [ + { + "id": "123456", + "payment_id": 987654321, + "amount": 100.0, + "currency": "BRL", + "reason": "fraud", + "stage": "chargeback", + "status": "open", + "date_created": "2023-01-15T10:30:00.000-04:00", + "date_last_updated": "2023-01-15T10:30:00.000-04:00", + "documentation_deadline": "2023-01-22T23:59:59.000-04:00", + "coverage_applied": false, + "coverage_eligible": true, + "external_reference": "ext_ref_123", + "metadata": { + "key1": "value1", + "key2": "value2" + }, + "documentation_status": "pending", + "chargeback_sequence_number": 1, + "source": { + "id": "475845652", + "name": "Test Source", + "type": "collector" + }, + "case": { + "id": "case_123", + "number": "CB-123456789", + "type": "chargeback" + }, + "risk_score": 75.5, + "payment_method": { + "id": "visa", + "type": "credit_card" + }, + "transaction_details": { + "net_received_amount": 95.0, + "total_paid_amount": 100.0, + "overpaid_amount": 0, + "installment_amount": 100.0 + } + } + ] +} \ No newline at end of file