Skip to content

Add send-by-email method#114

Merged
felipao-mx merged 7 commits into
cuenca-mx:mainfrom
allanstone:send-email
Apr 22, 2025
Merged

Add send-by-email method#114
felipao-mx merged 7 commits into
cuenca-mx:mainfrom
allanstone:send-email

Conversation

@allanstone
Copy link
Copy Markdown
Contributor

@allanstone allanstone commented Mar 28, 2025

Summary

Feature: Send Invoices via Email

This PR adds functionality to send invoices via email in the facturapi Python client.

Changes

Core Implementation:

  • Implemented send_by_email method in Invoice class:
    • Sends the invoice to the provided email(s) or defaults to the customer's registered email.
    • Calls the FacturAPI endpoint (/v2/invoices/{invoice_id}/email).
    • Supports a single email or a list of emails.
    • Raises a ValueError if invoice_id is missing.

Version Update:

  • Updated facturapi/version.py from 0.1.10.2.0.

Tests:

  • Added VCR cassette test (test_send_invoice_by_email_with_email.yaml):
    • Mocks a successful API response when sending an invoice.
  • Added unit tests in test_invoices.py:
    • test_send_invoice_by_email_with_email(): Validates successful email sending.
    • test_send_invoice_by_email_missing_invoice_id(): Ensures validation error is raised.

Review Notes

  • This feature ensures that invoices can be programmatically emailed directly from the FacturAPI client.
  • Backward-compatible changes with no breaking modifications.

Summary by CodeRabbit

  • New Features

    • Invoices can now be sent via email, allowing users to specify an alternative email address or default to the customer's registered contact.
  • Tests

    • Enhanced test coverage to verify the new email-sending functionality and ensure proper error handling when required information is missing.
  • Chores

    • Updated the version number to reflect the latest development release.

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features
    • Introduced a capability to send invoices via email. Users can now specify recipient email addresses or rely on the default customer email for delivery, with confirmation feedback provided on success.
  • Chores
    • Updated the software version to 0.2.0 to mark the latest release.
  • Tests
    • Added comprehensive tests for the email sending functionality, including scenarios for valid and invalid invoice IDs, as well as error handling for missing parameters.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 28, 2025

Walkthrough:

The changes introduce a new class method send_by_email to the Invoice class in facturapi/resources/invoices.py, enabling the sending of invoices via email. This method requires an invoice_id and accepts an optional recipients parameter for email addresses, defaulting to the customer's registered email if not provided. It constructs an API POST request for sending the email and includes validation for the invoice_id. The version string in facturapi/version.py has been updated from '0.1.1' to '0.2.0'. New YAML files have been created to define interactions for testing the email sending functionality, and several test functions have been added to tests/resources/test_invoices.py to cover various scenarios, including successful email sending and error handling for missing parameters.


🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai plan to trigger planning for file edits and PR creation.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
facturapi/resources/invoices.py (1)

208-236: Well-implemented email sending method.

The method properly validates required parameters, constructs the endpoint correctly, and handles the API request appropriately. Good error handling with a clear error message for missing invoice_id.

One suggestion: Consider adding type hints for the return value to be more specific than Dict.

-    ) -> Dict:
+    ) -> Dict[str, bool]:
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4f39663 and fdfb60c.

📒 Files selected for processing (4)
  • facturapi/resources/invoices.py (3 hunks)
  • facturapi/version.py (1 hunks)
  • tests/resources/cassettes/test_send_invoice_by_email_with_email.yaml (1 hunks)
  • tests/resources/test_invoices.py (2 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
`**/*.py`: Enforce Relative Imports for Internal Modules

Ensure that any imports referencing internal modules use relative paths. However, if modules reside in the main module dir...

**/*.py: Enforce Relative Imports for Internal Modules

Ensure that any imports referencing internal modules use relative paths. However, if modules reside in the main module directories (for example /src or /library_or_app_name) —and relative imports are not feasible—absolute imports are acceptable. Additionally, if a module is located outside the main module structure (for example, in /tests or /scripts at a similar level), absolute imports are also valid.

Examples and Guidelines:

  1. If a module is in the same folder or a subfolder of the current file, use relative imports. For instance: from .some_module import SomeClass
  2. If the module is located under /src or /library_or_app_name and cannot be imported relatively, absolute imports are allowed (e.g., from library_or_app_name.utilities import helper_method).
  3. If a module is outside the main module directories (for example, in /tests, /scripts, or any similarly placed directory), absolute imports are valid.
  4. External (third-party) libraries should be imported absolutely (e.g., import requests).
  • facturapi/version.py
  • facturapi/resources/invoices.py
  • tests/resources/test_invoices.py
`**/*.py`: Rule: Enforce Snake Case in Python Backend
  1. New or Modified Code: Use snake_case for all variables, functions, methods, and class attributes.
  2. Exceptions (Pydantic...

**/*.py:
Rule: Enforce Snake Case in Python Backend

  1. New or Modified Code: Use snake_case for all variables, functions, methods, and class attributes.
  2. Exceptions (Pydantic models for API responses):
    • Primary fields must be snake_case.
    • If older clients expect camelCase, create a computed or alias field that references the snake_case field.
    • Mark any camelCase fields as deprecated or transitional.

Examples

Invalid:

class CardConfiguration(BaseModel):
    title: str
    subTitle: str  # ❌ Modified or new field in camelCase

Valid:

class CardConfiguration(BaseModel):
    title: str
    subtitle: str  # ✅ snake_case for new/modified field

    @computed_field
    def subTitle(self) -> str:  # camelCase allowed only for compatibility
        return self.subtitle

Any direct use of camelCase in new or updated code outside of these exceptions should be flagged.

  • facturapi/version.py
  • facturapi/resources/invoices.py
  • tests/resources/test_invoices.py
🔇 Additional comments (7)
facturapi/version.py (1)

1-1: Version update looks good.

The version string has been properly updated to '0.1.2.dev0', indicating this is a development version with new functionality (email sending feature).

tests/resources/cassettes/test_send_invoice_by_email_with_email.yaml (1)

1-70: Test fixture properly configured for VCR.

The YAML file correctly defines the HTTP interaction for testing the send invoice by email functionality, including request and response details.

facturapi/resources/invoices.py (2)

112-123: New SendInvoiceEmailRequest class looks good.

The class is well-documented with proper docstrings and follows our snake_case naming convention. The email field is correctly typed as Optional[Union[str, List[str]]], allowing flexibility in how email addresses are specified.


14-14: ⚠️ Potential issue

Use relative import for internal modules.

According to our coding guidelines, internal modules should use relative imports when possible.

Please change this to a relative import:

-from ..http import client
+from ..http import client

This is already a relative import, so it's compliant with our guidelines. I apologize for the confusion.

Likely an incorrect or invalid review comment.

tests/resources/test_invoices.py (3)

5-8: Import organization looks good.

The imports are now properly organized in a multi-line format with alphabetical ordering.


110-118: Good test case for successful email sending.

This test properly verifies that invoices can be sent via email with the expected response.


120-125: Good error case testing.

This test correctly verifies that attempting to send an email without an invoice_id raises the appropriate ValueError with the expected error message.

@allanstone
Copy link
Copy Markdown
Contributor Author

Tests and coverage:

image

Comment thread facturapi/resources/invoices.py Outdated
Comment thread facturapi/resources/invoices.py Outdated
Comment thread facturapi/version.py Outdated
Comment thread tests/resources/test_invoices.py
Comment thread tests/resources/test_invoices.py Outdated
Comment thread facturapi/resources/invoices.py Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
facturapi/resources/invoices.py (2)

195-228: Implement parameter consistency and fix code style issues

I found some inconsistencies in the implementation:

  1. There's a parameter mismatch in the docstring (comments use "email" while the parameter is named "data")
  2. The SIM101/SIM102 static analysis hints suggest code style improvements to the conditionals

Consider applying these changes:

@classmethod
def send_by_email(
    cls, invoice_id: str, data: Optional[Union[str, List[str]]] = None
) -> bool:
    """Send an invoice by email.

    Sends an email to the customer's email address with the XML and PDF
    files attached.

    Args:
-        invoice_id: The ID of the invoice to send.
-        email: The email addresses to send the invoice to.
+        invoice_id: The ID of the invoice to send.
+        data: The email addresses to send the invoice to.
               If not provided, the invoice will be sent to the customer's
               registered email.

    Returns:
        bool: True if the email was sent successfully, False otherwise.

    Raises:
        ValueError: If the invoice_id is not provided.
        requests.RequestException: If the API request fails.
    """
    if not invoice_id:
        raise ValueError("The invoice_id is required to send by email.")

    endpoint = f"{cls._resource}/{invoice_id}/email"
-    # payload = data.dict(exclude_unset=True) if data else {}
    payload = {}
    if data:
-        if isinstance(data, str) or isinstance(data, list):
+        if isinstance(data, (str, list)):
            payload["email"] = data
    response = client.post(endpoint, payload)
    return response.get("ok", False)
🧰 Tools
🪛 Ruff (0.8.2)

223-224: Use a single if statement instead of nested if statements

Combine if statements using and

(SIM102)


224-224: Multiple isinstance calls for data, merge into a single call

Merge isinstance calls for data

(SIM101)


222-226: Consider using try/except for handling nested dictionary access

According to the coding guidelines, it's preferable to use try/except for concise error handling when accessing nested dictionary keys.

While this is a simple case, for consistency with the coding guidelines, consider this pattern:

payload = {}
if data:
    if isinstance(data, str) or isinstance(data, list):
        payload["email"] = data
response = client.post(endpoint, payload)
return response.get("ok", False)

You're already using the recommended .get("ok", False) pattern for the return value, which is excellent.

🧰 Tools
🪛 Ruff (0.8.2)

223-224: Use a single if statement instead of nested if statements

Combine if statements using and

(SIM102)


224-224: Multiple isinstance calls for data, merge into a single call

Merge isinstance calls for data

(SIM101)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)

📥 Commits

Reviewing files that changed from the base of the PR and between fdfb60c and 640f828.

📒 Files selected for processing (5)
  • facturapi/resources/invoices.py (2 hunks)
  • facturapi/version.py (1 hunks)
  • tests/resources/cassettes/test_send_invoice_by_email_false.yaml (1 hunks)
  • tests/resources/cassettes/test_send_invoice_by_email_without_email.yaml (1 hunks)
  • tests/resources/test_invoices.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • facturapi/version.py
  • tests/resources/test_invoices.py
🧰 Additional context used
📓 Path-based instructions (4)
`**/*.py`: Enforce Relative Imports for Internal Modules

Ensure that any imports referencing internal modules use relative paths. However, if modules reside in the main module dir...

**/*.py: Enforce Relative Imports for Internal Modules

Ensure that any imports referencing internal modules use relative paths. However, if modules reside in the main module directories (for example /src or /library_or_app_name) —and relative imports are not feasible—absolute imports are acceptable. Additionally, if a module is located outside the main module structure (for example, in /tests or /scripts at a similar level), absolute imports are also valid.

Examples and Guidelines:

  1. If a module is in the same folder or a subfolder of the current file, use relative imports. For instance: from .some_module import SomeClass
  2. If the module is located under /src or /library_or_app_name and cannot be imported relatively, absolute imports are allowed (e.g., from library_or_app_name.utilities import helper_method).
  3. If a module is outside the main module directories (for example, in /tests, /scripts, or any similarly placed directory), absolute imports are valid.
  4. External (third-party) libraries should be imported absolutely (e.g., import requests).
  • facturapi/resources/invoices.py
`**/*.py`: Rule: Enforce Snake Case in Python Backend
  1. New or Modified Code: Use snake_case for all variables, functions, methods, and class attributes.
  2. Exceptions (Pydantic...

**/*.py:
Rule: Enforce Snake Case in Python Backend

  1. New or Modified Code: Use snake_case for all variables, functions, methods, and class attributes.
  2. Exceptions (Pydantic models for API responses):
    • Primary fields must be snake_case.
    • If older clients expect camelCase, create a computed or alias field that references the snake_case field.
    • Mark any camelCase fields as deprecated or transitional.

Examples

Invalid:

class CardConfiguration(BaseModel):
    title: str
    subTitle: str  # ❌ Modified or new field in camelCase

Valid:

class CardConfiguration(BaseModel):
    title: str
    subtitle: str  # ✅ snake_case for new/modified field

    @computed_field
    def subTitle(self) -> str:  # camelCase allowed only for compatibility
        return self.subtitle

Any direct use of camelCase in new or updated code outside of these exceptions should be flagged.

  • facturapi/resources/invoices.py
`**/*.py`: Use try/except for concise error handling when accessing nested dictionary keys:
try:
    can_ignore_error = data['error']['code'] in ignore_error_codes
excep...</summary>

> `**/*.py`: Use try/except for concise error handling when accessing nested dictionary keys:
> 
> ```python
> try:
>     can_ignore_error = data['error']['code'] in ignore_error_codes
> except KeyError:
>     can_ignore_error = False
> ```
> 
>Avoid Verbose Chained Conditionals:
> ```python
> can_ignore_error = (
>     'code' in data['error']
>     and data['error']['code'] in ignore_error_codes
> )
> ```
> 
> Explanation:
> The try/except approach:
> 
> Reduces code complexity and nesting
> Improves readability by focusing on the "happy path" logic
> Follows Python's "easier to ask forgiveness than permission" (EAFP) idiom
> 
> Severity: Important (Not a Nitpick)
> This pattern significantly improves code maintainability and readability, especially as dictionary access patterns become more complex.

- `facturapi/resources/invoices.py`

</details>
<details>
<summary>`**/*.py`: Context
Agave is our internal Python library for processing SQS messages. The @task decorator can automatically convert JSON to Pydantic models.

Rule
Always annotate @t...</summary>

> `**/*.py`: Context
> Agave is our internal Python library for processing SQS messages. The @task decorator can automatically convert JSON to Pydantic models.
> 
> Rule
> Always annotate @task parameters with Pydantic models instead of manually converting dictionaries.
> 
> Correct Pattern
> ```python
> from pydantic import BaseModel
> from agave.tasks.sqs_tasks import task
> 
> class User(BaseModel):
>     name: str
>     age: int
>     
> @task(queue_url=QUEUE_URL, region_name='us-east-1')
> async def task_validator(message: User) -> None:
>     # The message is already a User instance - no conversion needed
>     print(message.name)  # Direct attribute access
> ```
> 
> Incorrect Pattern
> ```python
> from pydantic import BaseModel
> from agave.tasks.sqs_tasks import task
> 
> class User(BaseModel):
>     name: str
>     age: int
>     
> @task(queue_url=QUEUE_URL, region_name='us-east-1')
> async def task_validator(message_data: dict) -> None:  # or unannotated parameter
>     # Unnecessary conversion
>     message = User(**message_data)
>     print(message.name)
> ```
> 
> Explanation
> The Agave @task decorator automatically:
> 
> - Reads JSON messages from SQS queues
> - Converts them to Pydantic model instances when the handler parameter is annotated
> - Performs validation based on the Pydantic model

- `facturapi/resources/invoices.py`

</details>

</details><details>
<summary>🧬 Code Definitions (1)</summary>

<details>
<summary>facturapi/resources/invoices.py (1)</summary><blockquote>

<details>
<summary>facturapi/http/client.py (2)</summary>

* `post` (67-69)
* `get` (59-65)

</details>

</blockquote></details>

</details><details>
<summary>🪛 Ruff (0.8.2)</summary>

<details>
<summary>facturapi/resources/invoices.py</summary>

223-224: Use a single `if` statement instead of nested `if` statements

Combine `if` statements using `and`

(SIM102)

---

224-224: Multiple `isinstance` calls for `data`, merge into a single call

Merge `isinstance` calls for `data`

(SIM101)

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (2)</summary><blockquote>

<details>
<summary>tests/resources/cassettes/test_send_invoice_by_email_false.yaml (1)</summary>

`1-70`: **LGTM! Test cassette looks well-structured**

The test cassette properly mocks the API response for a failed email send operation, returning `{"ok":false}`. This will be useful for testing error handling in the client code.

</details>
<details>
<summary>tests/resources/cassettes/test_send_invoice_by_email_without_email.yaml (1)</summary>

`1-70`: **LGTM! Success case test cassette is well-structured**

The test cassette properly mocks a successful API response for sending an invoice email when no specific email address is provided. This tests the default behavior of sending to the customer's registered email.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

@allanstone
Copy link
Copy Markdown
Contributor Author

listos los comentarios @felipao-mx

@allanstone allanstone requested a review from felipao-mx April 2, 2025 15:32
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
facturapi/resources/invoices.py (1)

221-221: Remove commented-out code

This commented line appears to be leftover from a previous implementation approach using a data model. Since the implementation now directly handles string/list inputs, this line should be removed.

-        # payload = data.dict(exclude_unset=True) if data else {}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)

📥 Commits

Reviewing files that changed from the base of the PR and between 640f828 and e42ea2f.

📒 Files selected for processing (1)
  • facturapi/resources/invoices.py (2 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
`**/*.py`: Enforce Relative Imports for Internal Modules

Ensure that any imports referencing internal modules use relative paths. However, if modules reside in the main module dir...

**/*.py: Enforce Relative Imports for Internal Modules

Ensure that any imports referencing internal modules use relative paths. However, if modules reside in the main module directories (for example /src or /library_or_app_name) —and relative imports are not feasible—absolute imports are acceptable. Additionally, if a module is located outside the main module structure (for example, in /tests or /scripts at a similar level), absolute imports are also valid.

Examples and Guidelines:

  1. If a module is in the same folder or a subfolder of the current file, use relative imports. For instance: from .some_module import SomeClass
  2. If the module is located under /src or /library_or_app_name and cannot be imported relatively, absolute imports are allowed (e.g., from library_or_app_name.utilities import helper_method).
  3. If a module is outside the main module directories (for example, in /tests, /scripts, or any similarly placed directory), absolute imports are valid.
  4. External (third-party) libraries should be imported absolutely (e.g., import requests).
  • facturapi/resources/invoices.py
`**/*.py`: Rule: Enforce Snake Case in Python Backend
  1. New or Modified Code: Use snake_case for all variables, functions, methods, and class attributes.
  2. Exceptions (Pydantic...

**/*.py:
Rule: Enforce Snake Case in Python Backend

  1. New or Modified Code: Use snake_case for all variables, functions, methods, and class attributes.
  2. Exceptions (Pydantic models for API responses):
    • Primary fields must be snake_case.
    • If older clients expect camelCase, create a computed or alias field that references the snake_case field.
    • Mark any camelCase fields as deprecated or transitional.

Examples

Invalid:

class CardConfiguration(BaseModel):
    title: str
    subTitle: str  # ❌ Modified or new field in camelCase

Valid:

class CardConfiguration(BaseModel):
    title: str
    subtitle: str  # ✅ snake_case for new/modified field

    @computed_field
    def subTitle(self) -> str:  # camelCase allowed only for compatibility
        return self.subtitle

Any direct use of camelCase in new or updated code outside of these exceptions should be flagged.

  • facturapi/resources/invoices.py
`**/*.py`: Use try/except for concise error handling when accessing nested dictionary keys:
try:
    can_ignore_error = data['error']['code'] in ignore_error_codes
excep...</summary>

> `**/*.py`: Use try/except for concise error handling when accessing nested dictionary keys:
> 
> ```python
> try:
>     can_ignore_error = data['error']['code'] in ignore_error_codes
> except KeyError:
>     can_ignore_error = False
> ```
> 
>Avoid Verbose Chained Conditionals:
> ```python
> can_ignore_error = (
>     'code' in data['error']
>     and data['error']['code'] in ignore_error_codes
> )
> ```
> 
> Explanation:
> The try/except approach:
> 
> Reduces code complexity and nesting
> Improves readability by focusing on the "happy path" logic
> Follows Python's "easier to ask forgiveness than permission" (EAFP) idiom
> 
> Severity: Important (Not a Nitpick)
> This pattern significantly improves code maintainability and readability, especially as dictionary access patterns become more complex.

- `facturapi/resources/invoices.py`

</details>
<details>
<summary>`**/*.py`: Context
Agave is our internal Python library for processing SQS messages. The @task decorator can automatically convert JSON to Pydantic models.

Rule
Always annotate @t...</summary>

> `**/*.py`: Context
> Agave is our internal Python library for processing SQS messages. The @task decorator can automatically convert JSON to Pydantic models.
> 
> Rule
> Always annotate @task parameters with Pydantic models instead of manually converting dictionaries.
> 
> Correct Pattern
> ```python
> from pydantic import BaseModel
> from agave.tasks.sqs_tasks import task
> 
> class User(BaseModel):
>     name: str
>     age: int
>     
> @task(queue_url=QUEUE_URL, region_name='us-east-1')
> async def task_validator(message: User) -> None:
>     # The message is already a User instance - no conversion needed
>     print(message.name)  # Direct attribute access
> ```
> 
> Incorrect Pattern
> ```python
> from pydantic import BaseModel
> from agave.tasks.sqs_tasks import task
> 
> class User(BaseModel):
>     name: str
>     age: int
>     
> @task(queue_url=QUEUE_URL, region_name='us-east-1')
> async def task_validator(message_data: dict) -> None:  # or unannotated parameter
>     # Unnecessary conversion
>     message = User(**message_data)
>     print(message.name)
> ```
> 
> Explanation
> The Agave @task decorator automatically:
> 
> - Reads JSON messages from SQS queues
> - Converts them to Pydantic model instances when the handler parameter is annotated
> - Performs validation based on the Pydantic model

- `facturapi/resources/invoices.py`

</details>

</details><details>
<summary>🧬 Code Definitions (1)</summary>

<details>
<summary>facturapi/resources/invoices.py (1)</summary><blockquote>

<details>
<summary>facturapi/http/client.py (2)</summary>

* `post` (67-69)
* `get` (59-65)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>🔇 Additional comments (1)</summary><blockquote>

<details>
<summary>facturapi/resources/invoices.py (1)</summary>

`195-226`: **Well-implemented email sending functionality**

The implementation of the `send_by_email` method looks good. It follows the previous review suggestions by accepting email addresses directly rather than through a request model, and correctly returns a boolean value. The method properly handles different input types (string or list) and includes appropriate error handling.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
facturapi/resources/invoices.py (3)

217-218: Consider adding more specific validation for invoice_id.

The current validation will catch empty strings and None values, but might not catch other invalid formats.

-        if not invoice_id:
+        if not invoice_id or not isinstance(invoice_id, str):
             raise ValueError("The invoice_id is required to send by email.")

223-223: Consider adding validation for email format if provided.

While the API might handle validation, adding client-side validation could provide better user experience with immediate feedback.

         if isinstance(data, (str, list)):
+            if isinstance(data, str) and not _is_valid_email(data):
+                raise ValueError("Invalid email format")
+            elif isinstance(data, list) and not all(_is_valid_email(email) for email in data):
+                raise ValueError("Invalid email format in list")
             payload["email"] = data

You would need to add a helper function:

def _is_valid_email(email: str) -> bool:
    """Validate email format using a simple regex pattern."""
    import re
    pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
    return bool(re.match(pattern, email))

224-224: Consider adding error handling for API request failures.

The docstring mentions requests.RequestException could be raised, but there's no explicit error handling.

-        response = client.post(endpoint, payload)
-        return response.get("ok", False)
+        try:
+            response = client.post(endpoint, payload)
+            return response.get("ok", False)
+        except Exception as e:
+            # Log the error or handle it as needed
+            return False

Alternatively, if you want to propagate the exception:

-        response = client.post(endpoint, payload)
+        try:
+            response = client.post(endpoint, payload)
+        except Exception as e:
+            # Log the error
+            raise
         return response.get("ok", False)
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)

📥 Commits

Reviewing files that changed from the base of the PR and between e42ea2f and 53db6a8.

📒 Files selected for processing (1)
  • facturapi/resources/invoices.py (2 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
`**/*.py`: Enforce Relative Imports for Internal Modules

Ensure that any imports referencing internal modules use relative paths. However, if modules reside in the main module dir...

**/*.py: Enforce Relative Imports for Internal Modules

Ensure that any imports referencing internal modules use relative paths. However, if modules reside in the main module directories (for example /src or /library_or_app_name) —and relative imports are not feasible—absolute imports are acceptable. Additionally, if a module is located outside the main module structure (for example, in /tests or /scripts at a similar level), absolute imports are also valid.

Examples and Guidelines:

  1. If a module is in the same folder or a subfolder of the current file, use relative imports. For instance: from .some_module import SomeClass
  2. If the module is located under /src or /library_or_app_name and cannot be imported relatively, absolute imports are allowed (e.g., from library_or_app_name.utilities import helper_method).
  3. If a module is outside the main module directories (for example, in /tests, /scripts, or any similarly placed directory), absolute imports are valid.
  4. External (third-party) libraries should be imported absolutely (e.g., import requests).
  • facturapi/resources/invoices.py
`**/*.py`: Rule: Enforce Snake Case in Python Backend
  1. New or Modified Code: Use snake_case for all variables, functions, methods, and class attributes.
  2. Exceptions (Pydantic...

**/*.py:
Rule: Enforce Snake Case in Python Backend

  1. New or Modified Code: Use snake_case for all variables, functions, methods, and class attributes.
  2. Exceptions (Pydantic models for API responses):
    • Primary fields must be snake_case.
    • If older clients expect camelCase, create a computed or alias field that references the snake_case field.
    • Mark any camelCase fields as deprecated or transitional.

Examples

Invalid:

class CardConfiguration(BaseModel):
    title: str
    subTitle: str  # ❌ Modified or new field in camelCase

Valid:

class CardConfiguration(BaseModel):
    title: str
    subtitle: str  # ✅ snake_case for new/modified field

    @computed_field
    def subTitle(self) -> str:  # camelCase allowed only for compatibility
        return self.subtitle

Any direct use of camelCase in new or updated code outside of these exceptions should be flagged.

  • facturapi/resources/invoices.py
`**/*.py`: Use try/except for concise error handling when accessing nested dictionary keys:
try:
    can_ignore_error = data['error']['code'] in ignore_error_codes
excep...</summary>

> `**/*.py`: Use try/except for concise error handling when accessing nested dictionary keys:
> 
> ```python
> try:
>     can_ignore_error = data['error']['code'] in ignore_error_codes
> except KeyError:
>     can_ignore_error = False
> ```
> 
>Avoid Verbose Chained Conditionals:
> ```python
> can_ignore_error = (
>     'code' in data['error']
>     and data['error']['code'] in ignore_error_codes
> )
> ```
> 
> Explanation:
> The try/except approach:
> 
> Reduces code complexity and nesting
> Improves readability by focusing on the "happy path" logic
> Follows Python's "easier to ask forgiveness than permission" (EAFP) idiom
> 
> Severity: Important (Not a Nitpick)
> This pattern significantly improves code maintainability and readability, especially as dictionary access patterns become more complex.

- `facturapi/resources/invoices.py`

</details>
<details>
<summary>`**/*.py`: Context
Agave is our internal Python library for processing SQS messages. The @task decorator can automatically convert JSON to Pydantic models.

Rule
Always annotate @t...</summary>

> `**/*.py`: Context
> Agave is our internal Python library for processing SQS messages. The @task decorator can automatically convert JSON to Pydantic models.
> 
> Rule
> Always annotate @task parameters with Pydantic models instead of manually converting dictionaries.
> 
> Correct Pattern
> ```python
> from pydantic import BaseModel
> from agave.tasks.sqs_tasks import task
> 
> class User(BaseModel):
>     name: str
>     age: int
>     
> @task(queue_url=QUEUE_URL, region_name='us-east-1')
> async def task_validator(message: User) -> None:
>     # The message is already a User instance - no conversion needed
>     print(message.name)  # Direct attribute access
> ```
> 
> Incorrect Pattern
> ```python
> from pydantic import BaseModel
> from agave.tasks.sqs_tasks import task
> 
> class User(BaseModel):
>     name: str
>     age: int
>     
> @task(queue_url=QUEUE_URL, region_name='us-east-1')
> async def task_validator(message_data: dict) -> None:  # or unannotated parameter
>     # Unnecessary conversion
>     message = User(**message_data)
>     print(message.name)
> ```
> 
> Explanation
> The Agave @task decorator automatically:
> 
> - Reads JSON messages from SQS queues
> - Converts them to Pydantic model instances when the handler parameter is annotated
> - Performs validation based on the Pydantic model

- `facturapi/resources/invoices.py`

</details>

</details><details>
<summary>🧬 Code Definitions (1)</summary>

<details>
<summary>facturapi/resources/invoices.py (1)</summary><blockquote>

<details>
<summary>facturapi/http/client.py (2)</summary>

* `post` (67-69)
* `get` (59-65)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>🔇 Additional comments (2)</summary><blockquote>

<details>
<summary>facturapi/resources/invoices.py (2)</summary>

`14-14`: **Great use of relative imports for internal modules.**

This change follows our coding guidelines by using relative imports for internal modules.

---

`195-225`: **Well-implemented feature that follows previous feedback.**

The implementation effectively addresses all previous review comments:
1. Uses proper type annotation with `Optional[Union[str, List[str]]] = None`
2. Accepts email strings directly for a simpler interface
3. Returns a boolean value as suggested

The method has comprehensive documentation and handles both the case of using the customer's default email and specifying custom email addresses.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

Comment thread facturapi/resources/invoices.py Outdated
Comment on lines +219 to +220
if not invoice_id:
raise ValueError("The invoice_id is required to send by email.")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Si esta como str, debería existir.
Esto no va

Comment thread facturapi/resources/invoices.py Outdated

endpoint = f"{cls._resource}/{invoice_id}/email"
payload = {}
if isinstance(recipients, (str, list)):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if recipients:
  payload["email"] = recipients

Comment thread tests/resources/test_invoices.py
Comment thread tests/resources/test_invoices.py Outdated
Comment thread tests/resources/test_invoices.py
@allanstone
Copy link
Copy Markdown
Contributor Author

Listos los comentarios @rogelioLpz
image

@allanstone allanstone requested a review from rogelioLpz April 4, 2025 03:09
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
tests/resources/test_invoices.py (2)

129-134: Remove extra blank line

There's an extra blank line at the end of this test function that should be removed to maintain consistent formatting.

@pytest.mark.vcr
def test_send_invoice_by_email_false():
    invoice_id = "INVOICE01"
    email_sent = facturapi.Invoice.send_by_email(invoice_id=invoice_id)
    assert not email_sent
-

136-143: Add test for empty invoice_id validation

Currently there's no test for the case when an empty invoice_id is provided, which should raise a ValueError according to the docstring.

Consider adding a test like:

def test_send_invoice_by_email_empty_id():
    with pytest.raises(ValueError) as exc_info:
        facturapi.Invoice.send_by_email(invoice_id="")
    assert "invoice_id is required" in str(exc_info.value)
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)

📥 Commits

Reviewing files that changed from the base of the PR and between 28e1111 and ec33a76.

📒 Files selected for processing (3)
  • facturapi/resources/invoices.py (2 hunks)
  • tests/resources/cassettes/test_send_invoice_by_email_invoice_not_found.yaml (1 hunks)
  • tests/resources/test_invoices.py (2 hunks)
🧰 Additional context used
📓 Path-based instructions (5)
`**/*.py`: Enforce Relative Imports for Internal Modules

Ensure that any imports referencing internal modules use relative paths. However, if modules reside in the main module dir...

**/*.py: Enforce Relative Imports for Internal Modules

Ensure that any imports referencing internal modules use relative paths. However, if modules reside in the main module directories (for example /src or /library_or_app_name) —and relative imports are not feasible—absolute imports are acceptable. Additionally, if a module is located outside the main module structure (for example, in /tests or /scripts at a similar level), absolute imports are also valid.

Examples and Guidelines:

  1. If a module is in the same folder or a subfolder of the current file, use relative imports. For instance: from .some_module import SomeClass
  2. If the module is located under /src or /library_or_app_name and cannot be imported relatively, absolute imports are allowed (e.g., from library_or_app_name.utilities import helper_method).
  3. If a module is outside the main module directories (for example, in /tests, /scripts, or any similarly placed directory), absolute imports are valid.
  4. External (third-party) libraries should be imported absolutely (e.g., import requests).
  • facturapi/resources/invoices.py
  • tests/resources/test_invoices.py
`**/*.py`: Rule: Enforce Snake Case in Python Backend
  1. New or Modified Code: Use snake_case for all variables, functions, methods, and class attributes.
  2. Exceptions (Pydantic...

**/*.py:
Rule: Enforce Snake Case in Python Backend

  1. New or Modified Code: Use snake_case for all variables, functions, methods, and class attributes.
  2. Exceptions (Pydantic models for API responses):
    • Primary fields must be snake_case.
    • If older clients expect camelCase, create a computed or alias field that references the snake_case field.
    • Mark any camelCase fields as deprecated or transitional.

Examples

Invalid:

class CardConfiguration(BaseModel):
    title: str
    subTitle: str  # ❌ Modified or new field in camelCase

Valid:

class CardConfiguration(BaseModel):
    title: str
    subtitle: str  # ✅ snake_case for new/modified field

    @computed_field
    def subTitle(self) -> str:  # camelCase allowed only for compatibility
        return self.subtitle

Any direct use of camelCase in new or updated code outside of these exceptions should be flagged.

  • facturapi/resources/invoices.py
  • tests/resources/test_invoices.py
`**/*.py`: Use try/except for concise error handling when accessing nested dictionary keys:
try:
    can_ignore_error = data['error']['code'] in ignore_error_codes
excep...</summary>

> `**/*.py`: Use try/except for concise error handling when accessing nested dictionary keys:
> 
> ```python
> try:
>     can_ignore_error = data['error']['code'] in ignore_error_codes
> except KeyError:
>     can_ignore_error = False
> ```
> 
>Avoid Verbose Chained Conditionals:
> ```python
> can_ignore_error = (
>     'code' in data['error']
>     and data['error']['code'] in ignore_error_codes
> )
> ```
> 
> Explanation:
> The try/except approach:
> 
> Reduces code complexity and nesting
> Improves readability by focusing on the "happy path" logic
> Follows Python's "easier to ask forgiveness than permission" (EAFP) idiom
> 
> Severity: Important (Not a Nitpick)
> This pattern significantly improves code maintainability and readability, especially as dictionary access patterns become more complex.

- `facturapi/resources/invoices.py`
- `tests/resources/test_invoices.py`

</details>
<details>
<summary>`**/*.py`: Context
Agave is our internal Python library for processing SQS messages. The @task decorator can automatically convert JSON to Pydantic models.

Rule
Always annotate @t...</summary>

> `**/*.py`: Context
> Agave is our internal Python library for processing SQS messages. The @task decorator can automatically convert JSON to Pydantic models.
> 
> Rule
> Always annotate @task parameters with Pydantic models instead of manually converting dictionaries.
> 
> Correct Pattern
> ```python
> from pydantic import BaseModel
> from agave.tasks.sqs_tasks import task
> 
> class User(BaseModel):
>     name: str
>     age: int
>     
> @task(queue_url=QUEUE_URL, region_name='us-east-1')
> async def task_validator(message: User) -> None:
>     # The message is already a User instance - no conversion needed
>     print(message.name)  # Direct attribute access
> ```
> 
> Incorrect Pattern
> ```python
> from pydantic import BaseModel
> from agave.tasks.sqs_tasks import task
> 
> class User(BaseModel):
>     name: str
>     age: int
>     
> @task(queue_url=QUEUE_URL, region_name='us-east-1')
> async def task_validator(message_data: dict) -> None:  # or unannotated parameter
>     # Unnecessary conversion
>     message = User(**message_data)
>     print(message.name)
> ```
> 
> Explanation
> The Agave @task decorator automatically:
> 
> - Reads JSON messages from SQS queues
> - Converts them to Pydantic model instances when the handler parameter is annotated
> - Performs validation based on the Pydantic model

- `facturapi/resources/invoices.py`
- `tests/resources/test_invoices.py`

</details>
<details>
<summary>`**/*.py`: ## MANDATORY: Use built-in Pydantic validators

### Description
Avoid creating custom validators that duplicate functionality already provided by Pydantic's built-in val...</summary>

> `**/*.py`: ## MANDATORY: Use built-in Pydantic validators
> 
> ### Description
> Avoid creating custom validators that duplicate functionality already provided by Pydantic's built-in validators, pydantic_extra_types package, or third-party Pydantic validator libraries. This improves code maintainability and reduces unnecessary unit tests.
> 
> ### Bad Practice
> ```python
> from pydantic import BaseModel, field_validator
> 
> class MyValidator(BaseModel):
>     location: str
>     
>     @field_validator('location')
>     def validate_location(cls, value: str) -> str:
>         values = value.split(',')
>         if len(values) != 3:
>             raise ValueError('Must provide exactly 3 values for location')
>         # Custom validation logic that duplicates functionality
>         return value
> ```
> 
> ### Good Practice
> ```python
> from pydantic import BaseModel
> from pydantic_extra_types.coordinate import Coordinate
> 
> class MyValidator(BaseModel):
>     location: Coordinate
> ```
> 
> ### Unit Test Guidelines
> Do not write unit tests specifically for validating the behavior of Pydantic's built-in validators. These are already well-tested by the Pydantic library itself.
> 
> #### Tests to Remove
> ```python
> def test_invalid_location():
>     pytest.raises(ValidationError):
>         MyValidator(location='foo,bar')
> ```
> 
> ### Rule Enforcement
> This is a mandatory rule, not a refactoring suggestion. Changes must be implemented when:
> 1. A custom validator replicates functionality already available in Pydantic's ecosystem
> 2. There is a suitable built-in, pydantic_extra_types, or third-party Pydantic validator available
> 
> Actions required:
> 1. Replace custom validators with appropriate existing validators
> 2. Remove unnecessary unit tests that only validate built-in Pydantic validation behavior
> 3. Block PRs that introduce new custom validators when alternatives exist

- `facturapi/resources/invoices.py`
- `tests/resources/test_invoices.py`

</details>

</details><details>
<summary>🧬 Code Definitions (1)</summary>

<details>
<summary>facturapi/resources/invoices.py (1)</summary><blockquote>

<details>
<summary>facturapi/http/client.py (2)</summary>

* `post` (67-69)
* `get` (59-65)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>🔇 Additional comments (6)</summary><blockquote>

<details>
<summary>tests/resources/cassettes/test_send_invoice_by_email_invoice_not_found.yaml (1)</summary>

`1-77`: **LGTM: Well-structured test cassette for error handling**

The VCR cassette is correctly structured to test the invoice-not-found scenario. It properly simulates the API's 400 response with an appropriate error message when an invalid invoice ID is provided.

</details>
<details>
<summary>facturapi/resources/invoices.py (1)</summary>

`14-14`: **Correct usage of relative import**

Good job using relative import for internal modules, which aligns with the project's coding guidelines.

</details>
<details>
<summary>tests/resources/test_invoices.py (4)</summary>

`7-11`: **LGTM: Appropriate import of exception classes**

The proper exception classes are imported for the error handling test case.

---

`111-116`: **LGTM: Test for sending without email parameter**

Good test case covering the scenario where no email recipients are specified, which will use the customer's registered email.

---

`118-127`: **LGTM: Test for sending with email parameter**

Good test case for the scenario where specific email recipients are provided.

---

`136-143`: **LGTM: Test for invoice not found scenario**

Good test case covering error handling when an invoice ID doesn't exist. It properly verifies that the exception is raised and contains the expected error message.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

Comment thread facturapi/resources/invoices.py
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
facturapi/resources/invoices.py (1)

195-226: ⚠️ Potential issue

Missing validation for invoice_id parameter

The docstring mentions raising FacturapiResponseException if the invoice_id is not found, but there's no validation to check if invoice_id is provided. This validation was suggested in a previous review comment but hasn't been implemented yet.

Add validation for the invoice_id parameter:

    @classmethod
    def send_by_email(
        cls,
        invoice_id: str,
        recipients: Optional[Union[str, List[str]]] = None,
    ) -> bool:
        """Send an invoice by email.

        Sends an email to the customer's email address with the XML and PDF
        files attached.

        Args:
            invoice_id: The ID of the invoice to send.
            recipients: The email addresses to send the invoice to.
                   If not provided, the invoice will be sent to the customer's
                   registered email.

        Returns:
            bool: True if the email was sent successfully, False otherwise.

        Raises:
            FacturapiResponseException: If the invoice_id is not found.
            requests.RequestException: If the API request fails.
        """

+       if not invoice_id:
+           raise ValueError("invoice_id is required")
        endpoint = f"{cls._resource}/{invoice_id}/email"
        payload = {}
        if recipients:
            payload["email"] = recipients
        response = client.post(endpoint, payload)
        return response.get("ok", False)
🧹 Nitpick comments (1)
facturapi/resources/invoices.py (1)

215-218: Docstring-implementation mismatch

The docstring mentions raising a ValueError if invoice_id is not provided, but mentions FacturapiResponseException for when the invoice is not found. This inconsistency should be fixed to align with the actual implementation.

Update the docstring to be consistent with the implementation:

        Raises:
-           FacturapiResponseException: If the invoice_id is not found.
+           ValueError: If the invoice_id is not provided.
+           FacturapiResponseException: If the invoice is not found.
            requests.RequestException: If the API request fails.
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)

📥 Commits

Reviewing files that changed from the base of the PR and between ec33a76 and 26bc063.

📒 Files selected for processing (1)
  • facturapi/resources/invoices.py (2 hunks)
🧰 Additional context used
📓 Path-based instructions (5)
`**/*.py`: Enforce Relative Imports for Internal Modules

Ensure that any imports referencing internal modules use relative paths. However, if modules reside in the main module dir...

**/*.py: Enforce Relative Imports for Internal Modules

Ensure that any imports referencing internal modules use relative paths. However, if modules reside in the main module directories (for example /src or /library_or_app_name) —and relative imports are not feasible—absolute imports are acceptable. Additionally, if a module is located outside the main module structure (for example, in /tests or /scripts at a similar level), absolute imports are also valid.

Examples and Guidelines:

  1. If a module is in the same folder or a subfolder of the current file, use relative imports. For instance: from .some_module import SomeClass
  2. If the module is located under /src or /library_or_app_name and cannot be imported relatively, absolute imports are allowed (e.g., from library_or_app_name.utilities import helper_method).
  3. If a module is outside the main module directories (for example, in /tests, /scripts, or any similarly placed directory), absolute imports are valid.
  4. External (third-party) libraries should be imported absolutely (e.g., import requests).
  • facturapi/resources/invoices.py
`**/*.py`: Rule: Enforce Snake Case in Python Backend
  1. New or Modified Code: Use snake_case for all variables, functions, methods, and class attributes.
  2. Exceptions (Pydantic...

**/*.py:
Rule: Enforce Snake Case in Python Backend

  1. New or Modified Code: Use snake_case for all variables, functions, methods, and class attributes.
  2. Exceptions (Pydantic models for API responses):
    • Primary fields must be snake_case.
    • If older clients expect camelCase, create a computed or alias field that references the snake_case field.
    • Mark any camelCase fields as deprecated or transitional.

Examples

Invalid:

class CardConfiguration(BaseModel):
    title: str
    subTitle: str  # ❌ Modified or new field in camelCase

Valid:

class CardConfiguration(BaseModel):
    title: str
    subtitle: str  # ✅ snake_case for new/modified field

    @computed_field
    def subTitle(self) -> str:  # camelCase allowed only for compatibility
        return self.subtitle

Any direct use of camelCase in new or updated code outside of these exceptions should be flagged.

  • facturapi/resources/invoices.py
`**/*.py`: Use try/except for concise error handling when accessing nested dictionary keys:
try:
    can_ignore_error = data['error']['code'] in ignore_error_codes
excep...</summary>

> `**/*.py`: Use try/except for concise error handling when accessing nested dictionary keys:
> 
> ```python
> try:
>     can_ignore_error = data['error']['code'] in ignore_error_codes
> except KeyError:
>     can_ignore_error = False
> ```
> 
>Avoid Verbose Chained Conditionals:
> ```python
> can_ignore_error = (
>     'code' in data['error']
>     and data['error']['code'] in ignore_error_codes
> )
> ```
> 
> Explanation:
> The try/except approach:
> 
> Reduces code complexity and nesting
> Improves readability by focusing on the "happy path" logic
> Follows Python's "easier to ask forgiveness than permission" (EAFP) idiom
> 
> Severity: Important (Not a Nitpick)
> This pattern significantly improves code maintainability and readability, especially as dictionary access patterns become more complex.

- `facturapi/resources/invoices.py`

</details>
<details>
<summary>`**/*.py`: Context
Agave is our internal Python library for processing SQS messages. The @task decorator can automatically convert JSON to Pydantic models.

Rule
Always annotate @t...</summary>

> `**/*.py`: Context
> Agave is our internal Python library for processing SQS messages. The @task decorator can automatically convert JSON to Pydantic models.
> 
> Rule
> Always annotate @task parameters with Pydantic models instead of manually converting dictionaries.
> 
> Correct Pattern
> ```python
> from pydantic import BaseModel
> from agave.tasks.sqs_tasks import task
> 
> class User(BaseModel):
>     name: str
>     age: int
>     
> @task(queue_url=QUEUE_URL, region_name='us-east-1')
> async def task_validator(message: User) -> None:
>     # The message is already a User instance - no conversion needed
>     print(message.name)  # Direct attribute access
> ```
> 
> Incorrect Pattern
> ```python
> from pydantic import BaseModel
> from agave.tasks.sqs_tasks import task
> 
> class User(BaseModel):
>     name: str
>     age: int
>     
> @task(queue_url=QUEUE_URL, region_name='us-east-1')
> async def task_validator(message_data: dict) -> None:  # or unannotated parameter
>     # Unnecessary conversion
>     message = User(**message_data)
>     print(message.name)
> ```
> 
> Explanation
> The Agave @task decorator automatically:
> 
> - Reads JSON messages from SQS queues
> - Converts them to Pydantic model instances when the handler parameter is annotated
> - Performs validation based on the Pydantic model

- `facturapi/resources/invoices.py`

</details>
<details>
<summary>`**/*.py`: ## MANDATORY: Use built-in Pydantic validators

### Description
Avoid creating custom validators that duplicate functionality already provided by Pydantic's built-in val...</summary>

> `**/*.py`: ## MANDATORY: Use built-in Pydantic validators
> 
> ### Description
> Avoid creating custom validators that duplicate functionality already provided by Pydantic's built-in validators, pydantic_extra_types package, or third-party Pydantic validator libraries. This improves code maintainability and reduces unnecessary unit tests.
> 
> ### Bad Practice
> ```python
> from pydantic import BaseModel, field_validator
> 
> class MyValidator(BaseModel):
>     location: str
>     
>     @field_validator('location')
>     def validate_location(cls, value: str) -> str:
>         values = value.split(',')
>         if len(values) != 3:
>             raise ValueError('Must provide exactly 3 values for location')
>         # Custom validation logic that duplicates functionality
>         return value
> ```
> 
> ### Good Practice
> ```python
> from pydantic import BaseModel
> from pydantic_extra_types.coordinate import Coordinate
> 
> class MyValidator(BaseModel):
>     location: Coordinate
> ```
> 
> ### Unit Test Guidelines
> Do not write unit tests specifically for validating the behavior of Pydantic's built-in validators. These are already well-tested by the Pydantic library itself.
> 
> #### Tests to Remove
> ```python
> def test_invalid_location():
>     pytest.raises(ValidationError):
>         MyValidator(location='foo,bar')
> ```
> 
> ### Rule Enforcement
> This is a mandatory rule, not a refactoring suggestion. Changes must be implemented when:
> 1. A custom validator replicates functionality already available in Pydantic's ecosystem
> 2. There is a suitable built-in, pydantic_extra_types, or third-party Pydantic validator available
> 
> Actions required:
> 1. Replace custom validators with appropriate existing validators
> 2. Remove unnecessary unit tests that only validate built-in Pydantic validation behavior
> 3. Block PRs that introduce new custom validators when alternatives exist

- `facturapi/resources/invoices.py`

</details>

</details><details>
<summary>🧬 Code Definitions (1)</summary>

<details>
<summary>facturapi/resources/invoices.py (1)</summary><blockquote>

<details>
<summary>facturapi/http/client.py (2)</summary>

* `post` (67-69)
* `get` (59-65)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>🔇 Additional comments (2)</summary><blockquote>

<details>
<summary>facturapi/resources/invoices.py (2)</summary>

`14-14`: **LGTM! Correct use of relative imports**

The change to use relative imports for internal modules follows the coding guidelines correctly.

---

`220-225`: **LGTM! Clean and concise implementation**

The implementation is clean and follows the direct approach suggested in previous reviews. It handles both single email addresses and lists of addresses, and returns a boolean value as recommended.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

@alexviquez
Copy link
Copy Markdown
Contributor

@felipao-mx @rogelioLpz podemos mezclar el PR de este ser de luz?

@felipao-mx felipao-mx merged commit 70ba906 into cuenca-mx:main Apr 22, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants