Skip to content

Commit 81692d9

Browse files
Merge pull request #32 from indexa-git/refactor-pyazul
2 parents 8f83cfc + e9d6f64 commit 81692d9

File tree

108 files changed

+11480
-3219
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

108 files changed

+11480
-3219
lines changed

.bandit

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[bandit]
2+
exclude = tests,.venv,.mypy_cache,.ruff_cache,.pytest_cache
3+
tests = B201,B301
4+
skips = B101,B601

.cursor/rules/3ds_secure_flow.mdc

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
---
2+
description:
3+
globs:
4+
alwaysApply: true
5+
---
6+
\# PyAzul 3D Secure (3DS) Implementation Guide
7+
8+
This guide details the 3D Secure flow implementation within the `pyazul` library.
9+
10+
## Core Components for 3DS
11+
12+
1. **`SecureService` ([pyazul/pyazul/services/secure.py](mdc:pyazul/pyazul/pyazul/pyazul/services/secure.py))**:
13+
* Handles all logic specific to 3DS transactions, including:
14+
* Initiating 3DS sales, token sales, and holds (`process_sale`, `process_token_sale`, `process_hold`).
15+
* Processing 3DS method notifications (`process_3ds_method`).
16+
* Processing 3DS challenge responses (`process_challenge`).
17+
* Generating HTML forms for ACS redirection (`_create_challenge_form`).
18+
* Uses the shared `AzulAPI` client (passed during its initialization), ensuring `is_secure=True` is used for 3DS-specific authentication via `AzulAPI`.
19+
* Manages 3DS session state internally using dictionaries keyed by `secure_id` (for initial session data like `term_url` and `azul_order_id`) and `AzulOrderId` (for transaction processing states).
20+
21+
2. **`PyAzul` Facade ([pyazul/pyazul/index.py](mdc:pyazul/pyazul/pyazul/pyazul/index.py))**:
22+
* Exposes user-friendly methods for 3DS operations:
23+
* `secure_sale`, `secure_token_sale`, `secure_hold`
24+
* `process_3ds_method` (Note: the facade method is `process_3ds_method`, not `secure_3ds_method` as previously might have been implied by some docs/tests)
25+
* `process_challenge`
26+
* `create_challenge_form` (convenience wrapper around `SecureService._create_challenge_form`).
27+
* `get_secure_session_info(secure_id)`: retrieves session data stored by `SecureService`.
28+
* These methods delegate to the `SecureService` instance, which is initialized by `PyAzul`.
29+
30+
## Flow Overview
31+
32+
1. **Initiation**:
33+
* User calls `azul.secure_sale()` (or `secure_token_sale`, `secure_hold`) with payment details, `cardHolderInfo`, and `threeDSAuth` (which includes `TermUrl` and `MethodNotificationUrl`).
34+
* `SecureService` generates a unique `secure_id` (UUID).
35+
* `TermUrl` and `MethodNotificationUrl` provided by the user are internally appended with `?secure_id=<generated_id>` by `SecureService` before being sent to Azul.
36+
* `SecureService` makes an initial request to Azul. It then stores initial session data (including `azul_order_id` from the Azul response and the modified `term_url`) internally, associated with the generated `secure_id`.
37+
* The response from `azul.secure_sale()` may include HTML for immediate redirection (to the ACS for a challenge or to the 3DS Method URL). This response will also contain the `id` (which is the `secure_id`).
38+
39+
2. **Method Notification Callback (Your `MethodNotificationUrl`)**:
40+
* Your application endpoint (the `MethodNotificationUrl` you provided) is called by the ACS/PSP, with `secure_id` available as a query parameter.
41+
* Your application should first use `secure_id` to retrieve the stored session data: `session_data = await azul.get_secure_session_info(secure_id)`.
42+
* From `session_data`, retrieve the `azul_order_id`: `azul_order_id = session_data.get("azul_order_id")`.
43+
* Then, call `await azul.process_3ds_method(azul_order_id=azul_order_id, method_notification_status="RECEIVED")`.
44+
* `SecureService` uses its internal state to check if this method was already processed (to prevent duplicates) and updates the transaction state.
45+
* The response from `azul.process_3ds_method` might trigger a challenge, requiring redirection. In this case, use `azul.create_challenge_form(...)` with data from the response and the `term_url` (retrieved from `session_data.get("term_url")`) to generate the necessary HTML form.
46+
47+
3. **Challenge Callback (Your `TermUrl`)**:
48+
* Your application endpoint (the `TermUrl` you provided) is called by the ACS, typically via POST, after the cardholder completes (or skips) the challenge.
49+
* The `secure_id` (from the `TermUrl` query parameters) and `CRes` (Challenge Response, from the POST body) are received.
50+
* Your application calls `await azul.process_challenge(session_id=secure_id, challenge_response=CRes)`.
51+
* `SecureService` uses `secure_id` to retrieve session data (like `azul_order_id`) from its internal store and makes the final API call to process the challenge result.
52+
* The final transaction status (Approved/Declined) is returned.
53+
54+
Refer to [pyazul/pyazul/README.md](mdc:pyazul/pyazul/pyazul/pyazul/README.md) for detailed FastAPI examples of this flow.
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
---
2+
description:
3+
globs:
4+
alwaysApply: true
5+
---
6+
\
7+
# PyAzul Architecture Overview
8+
9+
This document outlines the high-level architecture of the `pyazul` library.
10+
11+
## Core Components
12+
13+
1. **`PyAzul` Facade ([pyazul/index.py](mdc:pyazul/index.py))**:
14+
* This is the main entry point for users of the library.
15+
* It initializes and provides access to all underlying services.
16+
* It manages a shared `AzulAPI` client instance and configuration (`AzulSettings`).
17+
18+
2. **`AzulAPI` Client ([pyazul/api/client.py](mdc:pyazul/api/client.py))**:
19+
* A single instance of `AzulAPI` is created by `PyAzul` and passed to services that require API communication.
20+
* It's responsible for all HTTP requests to the Azul gateway.
21+
* Handles SSL context with certificates, request signing (including `Auth1`/`Auth2` headers), and distinguishes between standard and 3DS (`is_secure=True`) authentication headers using credentials from `AzulSettings`.
22+
* Implements retry logic for production environments.
23+
24+
3. **Service Layer ([pyazul/services/](mdc:pyazul/services))**:
25+
* Functionality is modularized into services:
26+
* `TransactionService` ([pyazul/services/transaction.py](mdc:pyazul/services/transaction.py)): Handles standard payment operations (sale, hold, refund, etc.).
27+
* `DataVaultService` ([pyazul/services/datavault.py](mdc:pyazul/services/datavault.py)): Manages card tokenization.
28+
* `PaymentPageService` ([pyazul/services/payment_page.py](mdc:pyazul/services/payment_page.py)): Generates HTML for Azul's hosted payment page.
29+
* `SecureService` ([pyazul/services/secure.py](mdc:pyazul/services/secure.py)): Manages the 3D Secure authentication flow.
30+
* Services receive the shared `AzulAPI` client and `AzulSettings` (or just `AzulAPI` if settings are only needed by the client itself, as `AzulAPI` now takes `settings` in its constructor).
31+
32+
4. **Configuration ([pyazul/core/config.py](mdc:pyazul/core/config.py))**:
33+
* Managed by the `AzulSettings` Pydantic model.
34+
* Settings are loaded from a `.env` file and environment variables.
35+
* `PyAzul` can be initialized with a custom `AzulSettings` instance.
36+
37+
5. **Pydantic Models ([pyazul/models/](mdc:pyazul/models))**:
38+
* Used for request and response data validation and serialization.
39+
* Centralized and re-exported via `[pyazul/models/__init__.py](mdc:pyazul/models/__init__.py)`.
40+
41+
## Key Principles
42+
* **Facade Pattern**: `PyAzul` simplifies interaction with the various services.
43+
* **Dependency Injection**: `AzulAPI` client and `AzulSettings` are managed by `PyAzul` and provided to services. The 3DS session store is injected into `PyAzul` and then into `SecureService`.
44+
* **Asynchronous**: Core operations are `async/await` based.
45+
46+
Refer to [README.md](mdc:README.md) for usage examples.
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
---
2+
description:
3+
globs:
4+
alwaysApply: false
5+
---
6+
# PyAzul Coding Standards and Linting
7+
8+
This document outlines key coding standards, linting practices, and items to add to the `pyazul` library to ensure code quality, consistency, and maintainability.
9+
10+
## Exception Handling
11+
* **Chaining (`raise ... from ...`)**:
12+
* As detailed in `[error_handling_conventions.mdc](mdc:pyazul/.cursor/rules/error_handling_conventions.mdc)`, always use `raise NewException(...) from original_exception` when re-raising exceptions.
13+
* **Linter Issue**: The linter (Trunk) correctly flags violations of this. Ensure all `raise` statements within `except` blocks adhere to this.
14+
* **Files to Check/Verify**:
15+
* `[pyazul/api/client.py](mdc:pyazul/pyazul/api/client.py)`: (e.g., in `_load_certificates`, `_async_request`). Ensure all are covered.
16+
17+
## Linter Compliance & Specific Issues
18+
* **Trunk Linters**: The project uses Trunk for linting. All linter warnings and errors should be addressed promptly.
19+
* **Unused Variables**:
20+
* Pay attention to warnings about unused variables. If a variable is truly not needed, remove it. If it's part of a tuple unpacking and intentionally unused, prefix its name with an underscore (e.g., `_`).
21+
* **Specific Linter Issue**: In `[pyazul/api/client.py](mdc:pyazul/pyazul/api/client.py)`, method `_check_for_errors`, the loop `for field, value in error_indicators:` does not use `field`.
22+
* **Action**: Change to `for _, value in error_indicators:`.
23+
24+
## Type Hinting
25+
* **Comprehensive Typing**: All functions and methods must have comprehensive type hints for parameters and return values. This improves readability and allows for static analysis.
26+
* **`typing.NoReturn`**: For functions that are guaranteed to always raise an exception and never return normally (e.g., they end in `raise`), use `typing.NoReturn` as the return type. This was applied to `_log_and_raise_api_error` in `[pyazul/api/client.py](mdc:pyazul/pyazul/api/client.py)`.
27+
* **Pydantic Models**: Utilize Pydantic models for data structures passed between components. Service method signatures should generally expect these models, even if the `PyAzul` facade offers dictionary-based input for user convenience.
28+
29+
## Logging
30+
* Employ the standard `logging` module for clear, contextual logs.
31+
* **`AzulAPI` ([pyazul/api/client.py](mdc:pyazul/pyazul/api/client.py))**:
32+
* Log request/response data at `DEBUG` level for troubleshooting.
33+
* Log significant errors at `ERROR` level.
34+
* **Services (e.g., `[pyazul/services/secure.py](mdc:pyazul/pyazul/services/secure.py)`)**:
35+
* Log key events in flows at `INFO` or `DEBUG` level.
36+
* Log warnings for unexpected but recoverable situations with `WARNING`.
37+
* Log errors that lead to exceptions with `ERROR`.
38+
39+
## Documentation
40+
* **Docstrings**: All public classes, methods, and functions require clear docstrings. Describe purpose, arguments (`Args:`), return values (`Returns:`), and any exceptions raised (`Raises:`).
41+
* **[README.md](mdc:pyazul/README.md)**: Must be kept current, reflecting the library's public API and common usage patterns, especially for complex flows like 3DS.
42+
* **Code Comments**: Use to explain non-obvious logic. Avoid redundant comments that reiterate what the code clearly states.
43+
44+
## Modularity and Cohesion
45+
* Maintain the established separation of concerns: `PyAzul` (facade), Services (business logic), `AzulAPI` (HTTP client), `AzulSettings` (config), Models (data contracts).
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
---
2+
description: Details how configuration is managed in PyAzul, focusing on AzulSettings and .env files.
3+
globs:
4+
alwaysApply: true
5+
---
6+
# PyAzul Configuration Management
7+
8+
Configuration for the `pyazul` library is managed through the `AzulSettings` Pydantic model, defined in `[pyazul/core/config.py](mdc:pyazul/pyazul/core/config.py)`.
9+
10+
## Loading Configuration
11+
* By default, `PyAzul()` (see `[pyazul/index.py](mdc:pyazul/pyazul/index.py)`) initializes `AzulSettings` by loading values from a `.env` file in the project root and then from environment variables (environment variables override `.env` values).
12+
* A custom, pre-configured `AzulSettings` instance can also be passed directly to the `PyAzul` constructor: `PyAzul(settings=my_custom_settings)`.
13+
14+
## Key Configuration Variables (in `.env` or as environment variables)
15+
16+
* **API Credentials**:
17+
* `AUTH1`: Your primary Auth1 key. Used for all API requests.
18+
* `AUTH2`: Your primary Auth2 key. Used for all API requests.
19+
* `MERCHANT_ID`: Your merchant identifier. Used for all API calls and for the Payment Page.
20+
* **Certificates**:
21+
* `AZUL_CERT`: Path to your SSL certificate file OR the full PEM content as a string.
22+
* `AZUL_KEY`: Path to your SSL private key file OR the full PEM content as a string OR Base64 encoded PEM content.
23+
* The library handles writing PEM content to temporary files if direct content is provided (see `_load_certificates` in `[pyazul/core/config.py](mdc:pyazul/pyazul/core/config.py)`). The `AzulAPI` client in `[pyazul/api/client.py](mdc:pyazul/pyazul/api/client.py)` uses these settings to establish its SSL context.
24+
* **Environment**:
25+
* `ENVIRONMENT`: Set to `dev` for development/testing or `prod` for production. Controls API endpoints.
26+
* **Payment Page Settings (Optional, if using Payment Page)**:
27+
* `AZUL_AUTH_KEY`: Authentication key for generating the payment page hash.
28+
* `MERCHANT_NAME`: Your business name displayed on the page.
29+
* `MERCHANT_TYPE`: Your business type.
30+
* **Other**:
31+
* `CHANNEL`: Default payment channel (e.g., "EC" for E-Commerce).
32+
* `CUSTOM_URL`: Optionally override base API URLs.
33+
34+
35+
Consult `[pyazul/core/config.py](mdc:pyazul/pyazul/core/config.py)` for all available settings fields.
36+
The [README.md](mdc:pyazul/README.md) provides a `.env` example and further details.
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
---
2+
description:
3+
globs:
4+
alwaysApply: true
5+
---
6+
# PyAzul Error Handling Conventions
7+
8+
`pyazul` uses a hierarchy of custom exceptions to report errors, all defined in `[pyazul/core/exceptions.py](mdc:pyazul/core/exceptions.py)`.
9+
10+
## Custom Exception Hierarchy
11+
All custom exceptions inherit from `pyazul.core.exceptions.AzulError`.
12+
13+
* **`AzulError`**: Base class for all library-specific errors.
14+
* **`SSLError`**: Raised for issues related to SSL certificate loading or configuration (e.g., file not found, invalid format). This is primarily raised from `_load_certificates` in `[pyazul/api/client.py](mdc:pyazul/api/client.py)`.
15+
* **`APIError`**:
16+
* Raised for general issues during HTTP communication with the Azul API.
17+
* Examples: Network errors, unexpected HTTP status codes not covered by `AzulResponseError`, issues decoding JSON responses.
18+
* **`AzulResponseError`**:
19+
* Raised when the Azul API explicitly returns an error in its response payload (e.g., transaction declined, invalid field value).
20+
* Contains a `response_data` attribute holding the raw dictionary response from Azul for inspection.
21+
* The error message typically includes `ErrorMessage` or `ErrorDescription` from the Azul response.
22+
* This is checked and raised in the `_check_for_errors` method of `[pyazul/api/client.py](mdc:pyazul/api/client.py)`.
23+
24+
## Best Practices for Using and Raising Exceptions
25+
* **Catch Specific Exceptions**: When handling errors from `pyazul`, catch more specific exceptions first, then broader ones.
26+
```python
27+
from pyazul import AzulError, AzulResponseError, SSLError
28+
# from pyazul.core.exceptions import APIError (if needing to catch it separately)
29+
30+
try:
31+
response = await azul.sale(...)
32+
except AzulResponseError as e:
33+
print(f"Azul API Error: {e.message}")
34+
print(f"Response Data: {e.response_data}")
35+
except SSLError as e:
36+
print(f"SSL Configuration Error: {e}")
37+
except APIError as e: # Catches other API communication issues
38+
print(f"API Communication Error: {e}")
39+
except AzulError as e: # Catch-all for other pyazul errors
40+
print(f"PyAzul Library Error: {e}")
41+
except Exception as e:
42+
print(f"An unexpected error occurred: {e}")
43+
```
44+
* **Chaining Exceptions (`raise ... from ...`)**:
45+
* **Critically Important**: When catching an exception and raising a new custom `pyazul` exception (or any exception), use the `raise NewException(...) from original_exception` syntax. This preserves the context and stack trace of the original error, which is invaluable for debugging.
46+
* This practice is implemented in `[pyazul/services/secure.py](mdc:pyazul/services/secure.py)` and `[pyazul/api/client.py](mdc:pyazul/api/client.py)`.
47+
* Failure to do this can hide the root cause of problems.
48+
* Example: `raise AzulError("Something went wrong") from caught_exception`
49+
50+
See the [README.md](mdc:README.md) for a basic error handling example.
51+
The `[coding_standards_and_linting.mdc](mdc:coding_standards_and_linting.mdc)` rule also emphasizes this.

0 commit comments

Comments
 (0)