Skip to content

Feat/purchase email #733

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 49 commits into from
Jun 2, 2025

Conversation

ChiefStief
Copy link
Collaborator

@ChiefStief ChiefStief commented Apr 14, 2025

Requirements List

  • A provider user

Description List

  • Modified post privilege purchase lambda to kick off privilege purchase, privilege renewed and privilege issued events
  • Modified Data client to return privilege data when creating privileges
  • Modified purchase client to return line items when making purchases
  • Modified Queued Lamda Processor construct to take in modular DLQ threshold
  • Created EventBusClient abstraction to help with event creation including abstractions for privilege purchase, privilege renewed and privilege issued events
  • Created NotificationStack for outgoing notifications including event subscriptions for notification lambdas
  • Modified email notification lambda to include privilege purchase notifications and associated helper methods in base-email-service
  • Added / modified tests
  • Lowered duplicate transaction window to 35 seconds because I triggered a duplicate transaction error by going through the workflow quickly and purchasing privileges of the same price twice quickly, which is a legitimate but unlikely use case. 35s should be enough to get rid of duplicates but not enough to be triggerable in this case

Testing List

  • For API configuration changes: CDK tests added/updated in backend/compact-connect/tests/unit/test_api.py
  • Code review
  • Try purchasing privileges with your provider user and confirm you get a good looking / expected email back

Closes #533

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features

    • Introduced event-driven privilege purchase notifications with email confirmations and detailed cost breakdowns.
    • Added a notification processing stack with error monitoring and alerting for privilege purchase events.
    • Enabled event publishing for privilege purchase, issuance, and renewal to an event bus.
    • Added new email template and service method for privilege purchase provider notifications.
  • Improvements

    • Enhanced privilege creation to return detailed privilege records.
    • Included itemized cost details in privilege purchase processing and event emissions.
    • Integrated event bus configuration and permissions across API and Lambda components.
    • Added customizable dead letter queue alarm threshold for queued Lambda processing.
    • Extended email report formatting with key-value tuples and two-column tables supporting text alignment.
  • Bug Fixes

    • Fixed serialization of transaction line items to ensure downstream compatibility.
  • Tests

    • Expanded tests for privilege purchase notifications, event emission, and cost breakdown validation.
    • Added event bus setup in test environments and new JSON event resource.
    • Improved test patching for email service client in privilege-related handlers.
  • Documentation

    • Refined code formatting and comments for improved readability and maintainability.

Dana Stiefel added 3 commits April 11, 2025 14:04
Copy link
Contributor

coderabbitai bot commented Apr 14, 2025

## Walkthrough

This change introduces a full event-driven notification system for privilege purchases. It adds event schemas, event publishing logic, notification infrastructure (NotificationStack), Lambda handlers for privilege purchase events, and email notification templates. The system ensures that providers receive email confirmations with detailed privilege and cost information upon privilege purchase.

## Changes

| File(s)                                                                                                   | Change Summary |
|-----------------------------------------------------------------------------------------------------------|---------------|
| .../common_constructs/queued_lambda_processor.py                                                          | Added configurable DLQ alarm threshold to processor constructor and alarms. |
| .../lambdas/nodejs/email-notification-service/lambda.ts                                                   | Added handler case for privilege purchase provider notification email. |
| .../lambdas/nodejs/lib/email/base-email-service.ts                                                        | Added methods for tuple/table insertion and text alignment in email templates. |
| .../lambdas/nodejs/lib/email/email-notification-service.ts                                                | Added method to send privilege purchase provider notification emails. |
| .../lambdas/nodejs/tests/email-notification-service.test.ts                                               | Added tests for privilege purchase provider notification email sending and error handling. |
| .../lambdas/python/common/cc_common/config.py                                                             | Added event bus client as a cached property. |
| .../lambdas/python/common/cc_common/data_model/data_client.py                                             | `create_provider_privileges` now returns privilege summaries. |
| .../lambdas/python/common/cc_common/email_service_client.py                                               | Added method to send privilege purchase emails. |
| .../lambdas/python/common/cc_common/event_bus_client.py                                                   | New module: EventBusClient for publishing privilege-related events. |
| .../lambdas/python/common/cc_common/data_model/schema/data_event/api.py                                   | New module: Marshmallow schemas for event payloads (privilege purchase, issuance, renewal). |
| .../lambdas/python/provider-data-v1/handlers/privileges.py                                                | Added SQS handler for privilege purchase events to send notification emails. |
| .../lambdas/python/purchases/handlers/privileges.py                                                       | Publishes privilege purchase, issued, and renewed events after privilege creation. |
| .../lambdas/python/purchases/purchase_client.py                                                           | Ensures line items are returned as strings in privilege purchase response. |
| .../stacks/api_stack/v1_api/api.py                                                                        | Loads event bus from SSM and passes to API components. |
| .../stacks/api_stack/v1_api/purchases.py, .../stacks/api_stack/v1_api/provider_management.py              | Accepts event bus as constructor parameter and grants Lambda permissions to put events. |
| .../stacks/notification_stack.py                                                                          | New NotificationStack: sets up event-driven notification processing for privilege purchases. |
| .../pipeline/backend_stage.py                                                                             | Instantiates NotificationStack alongside ReportingStack. |
| .../lambdas/python/common/tests/function/test_data_client.py                                              | Adds assertions for returned privilege data. |
| .../lambdas/python/common/tests/resources/events/purchase_event_body.json                                 | Adds sample privilege purchase event for testing. |
| .../lambdas/python/provider-data-v1/tests/function/test_handlers/test_privileges.py                       | Adds test for privilege purchase message handler sending email. |
| .../lambdas/python/purchases/tests/function/test_handlers/test_purchase_privileges.py, .../unit/test_purchase_client.py | Adds/updates tests for line items in privilege purchase responses and event emission. |
| .../lambdas/python/purchases/tests/__init__.py, .../function/__init__.py                                  | Adds event bus to test environment and setup. |
| .../common_constructs/stack.py, .../data_model/schema/*, .../event_batch_writer.py, .../handlers/bulk_upload.py, .../handlers/ingest.py | Minor formatting, import, or whitespace changes. |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant PurchasesLambda as Purchases Lambda
    participant EventBus as Event Bus
    participant NotificationStack as Notification Lambda (SQS)
    participant EmailService as Email Service Lambda
    participant Provider as Provider

    PurchasesLambda->>EventBus: Publish privilege.purchase event (with privileges, cost, line items)
    EventBus->>NotificationStack: Route event to SQS queue (via EventBridge rule)
    NotificationStack->>EmailService: Invoke send_privilege_purchase_email (with event details)
    EmailService->>Provider: Send privilege purchase confirmation email

Assessment against linked issues

Objective Addressed Explanation
Email confirmation includes privilege ID, state, license type, and price (overall and line items) (#533)

Suggested reviewers

  • jlkravitz
  • landonshumway-ia

Poem

A rabbit hopped with glee today,
For privileges now email their way!
With IDs, states, and prices shown,
Each purchase proof is clearly known.
Events and stacks in tidy rows—
Notifications hop as the system grows!
🐇💌✨


<!-- walkthrough_end -->


---

<details>
<summary>📜 Recent review details</summary>

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


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 97495954d318affb8a9926a1c3654d00381a591b and 5e89ea9bccbd462f6009f98bcb243beff948a291.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_privileges.py` (4 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_privileges.py (4)</summary><blockquote>

<details>
<summary>backend/compact-connect/lambdas/python/provider-data-v1/tests/function/__init__.py (1)</summary>

* `_load_provider_data` (224-244)

</details>
<details>
<summary>backend/compact-connect/lambdas/python/common/cc_common/email_service_client.py (1)</summary>

* `send_privilege_purchase_email` (187-218)

</details>
<details>
<summary>backend/compact-connect/lambdas/python/common/cc_common/exceptions.py (1)</summary>

* `CCInternalException` (31-32)

</details>
<details>
<summary>backend/compact-connect/lambdas/python/provider-data-v1/handlers/privileges.py (1)</summary>

* `privilege_purchase_message_handler` (127-165)

</details>

</blockquote></details>

</details><details>
<summary>🪛 Pylint (3.3.7)</summary>

<details>
<summary>backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_privileges.py</summary>

[convention] 168-168: Line too long (108/100)

(C0301)

---

[convention] 214-214: Line too long (114/100)

(C0301)

---

[convention] 220-220: Line too long (117/100)

(C0301)

---

[convention] 249-249: Line too long (120/100)

(C0301)

---

[convention] 289-289: Line too long (117/100)

(C0301)

---

[convention] 292-292: Import outside toplevel (handlers.privileges.privilege_purchase_message_handler)

(C0415)

---

[warning] 294-294: Using open without explicitly specifying an encoding

(W1514)

---

[error] 300-300: Too many positional arguments for function call

(E1121)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: TestPython

</details>

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

<details>
<summary>backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_privileges.py (3)</summary>

`166-175`: **LGTM! Refactoring improves test isolation.**

The refactoring from decorator-based patching to context manager patching is a good improvement that provides better test isolation and more explicit control over when the mock is active.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Pylint (3.3.7)</summary>

[convention] 168-168: Line too long (108/100)

(C0301)

</details>

</details>

---

`210-225`: **LGTM! Consistent refactoring pattern applied.**

The refactoring maintains the same testing logic while improving mock lifecycle management through context manager usage.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Pylint (3.3.7)</summary>

[convention] 210-210: Line too long (120/100)

(C0301)

---

[convention] 214-214: Line too long (114/100)

(C0301)

---

[convention] 220-220: Line too long (117/100)

(C0301)

</details>

</details>

---

`249-261`: **LGTM! Refactoring pattern consistently applied.**

The change to context manager patching aligns with the other test method refactorings and improves test clarity.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Pylint (3.3.7)</summary>

[convention] 249-249: Line too long (120/100)

(C0301)

</details>

</details>

</details>

</blockquote></details>

</details>
<!-- internal state start -->


<!-- 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= -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=csg-org/CompactConnect&utm_content=733):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Explain this complex logic.`
  - `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 explain this code block.`
  -	`@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 explain its main purpose.`
  - `@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.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

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 generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@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](https://docs.coderabbit.ai/guides/configure-coderabbit) 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](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->

Copy link
Contributor

@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 (3)
backend/compact-connect/lambdas/python/purchases/handlers/privileges.py (3)

2-2: Remove unused imports.

The static analysis correctly identifies that UTC and datetime are imported but never used in this file.

-from datetime import UTC, date, datetime
+from datetime import date
🧰 Tools
🪛 Ruff (0.11.9)

2-2: datetime.UTC imported but unused

Remove unused import

(F401)


2-2: datetime.datetime imported but unused

Remove unused import

(F401)


315-317: Fix the cost calculation to handle potential edge cases.

The current implementation could fail if unitPrice or quantity contain non-numeric values or if line items are missing required fields.

Apply this diff to make the calculation more robust:

-        # calculate total cost of transaction
-        total_cost = 0
-        for line_item in cost_line_items:
-            total_cost = total_cost + float(line_item['unitPrice']) * int(line_item['quantity'])
+        # calculate total cost of transaction
+        total_cost = 0
+        for line_item in cost_line_items:
+            try:
+                unit_price = float(line_item.get('unitPrice', 0))
+                quantity = int(line_item.get('quantity', 0))
+                total_cost += unit_price * quantity
+            except (ValueError, TypeError) as e:
+                logger.error(f'Invalid line item data: {line_item}, error: {e}')
+                raise CCInternalException('Invalid transaction line item data') from e

332-336: Fix the logical error in determining renewed vs issued privileges.

The current logic compares jurisdiction strings against a list of privilege records, which will always return False. This means all privileges will be incorrectly classified as "issued" instead of "renewed".

The comparison should check if the jurisdiction exists in the existing privileges:

         for jurisdiction in selected_jurisdictions_postal_abbreviations:
-            if jurisdiction in existing_privileges_for_license:
+            if any(privilege['jurisdiction'].lower() == jurisdiction for privilege in existing_privileges_for_license):
                 privileges_renewed.append(jurisdiction)
             else:
                 privileges_issued.append(jurisdiction)
🧹 Nitpick comments (1)
backend/compact-connect/lambdas/python/purchases/handlers/privileges.py (1)

296-353: Consider extracting event publishing logic into a separate method.

The event publishing section significantly increases the length of the main handler function. Consider extracting this logic into a dedicated method to improve readability and separation of concerns.

Example refactor:

+    def _publish_privilege_events(self, provider_email: str, compact_abbr: str, license_jurisdiction: str, 
+                                 privileges: list, total_cost: str, cost_line_items: list,
+                                 selected_jurisdictions_postal_abbreviations: list, 
+                                 existing_privileges_for_license: list):
+        """Publish privilege purchase, issued, and renewed events."""
+        config.event_bus_client.publish_privilege_purchase_event(
+            source='post_purchase_privileges',
+            jurisdiction=license_jurisdiction,
+            compact=compact_abbr,
+            provider_email=provider_email,
+            privileges=privileges,
+            total_cost=total_cost,
+            cost_line_items=cost_line_items,
+        )
+        
+        existing_jurisdictions = {privilege['jurisdiction'].lower() for privilege in existing_privileges_for_license}
+        
+        for jurisdiction in selected_jurisdictions_postal_abbreviations:
+            if jurisdiction in existing_jurisdictions:
+                config.event_bus_client.publish_privilege_renewed_event(
+                    source='post_purchase_privileges',
+                    jurisdiction=jurisdiction,
+                    compact=compact_abbr,
+                    provider_email=provider_email,
+                )
+            else:
+                config.event_bus_client.publish_privilege_issued_event(
+                    source='post_purchase_privileges',
+                    jurisdiction=jurisdiction,
+                    compact=compact_abbr,
+                    provider_email=provider_email,
+                )

         # calculate total cost of transaction
-        total_cost = 0
-        for line_item in cost_line_items:
-            total_cost = total_cost + float(line_item['unitPrice']) * int(line_item['quantity'])
-
-        config.event_bus_client.publish_privilege_purchase_event(
-            source='post_purchase_privileges',
-            jurisdiction=license_jurisdiction,
-            compact=compact_abbr,
-            provider_email=provider_email,
-            privileges=privileges,
-            total_cost=str(total_cost),
-            cost_line_items=cost_line_items,
-        )
-
-        privileges_renewed = []
-        privileges_issued = []
-
-        for jurisdiction in selected_jurisdictions_postal_abbreviations:
-            if jurisdiction in existing_privileges_for_license:
-                privileges_renewed.append(jurisdiction)
-            else:
-                privileges_issued.append(jurisdiction)
-
-        for privilege_jurisdiction_issued in privileges_issued:
-            config.event_bus_client.publish_privilege_issued_event(
-                source='post_purchase_privileges',
-                jurisdiction=privilege_jurisdiction_issued,
-                compact=compact_abbr,
-                provider_email=provider_email,
-            )
-
-        for privilege_jurisdiction_renewed in privileges_renewed:
-            config.event_bus_client.publish_privilege_renewed_event(
-                source='post_purchase_privileges',
-                jurisdiction=privilege_jurisdiction_renewed,
-                compact=compact_abbr,
-                provider_email=provider_email,
-            )
+        try:
+            total_cost = sum(float(item.get('unitPrice', 0)) * int(item.get('quantity', 0)) 
+                           for item in cost_line_items)
+        except (ValueError, TypeError) as e:
+            logger.error(f'Invalid line item data: {cost_line_items}, error: {e}')
+            raise CCInternalException('Invalid transaction line item data') from e
+            
+        self._publish_privilege_events(
+            provider_email, compact_abbr, license_jurisdiction, privileges, 
+            str(total_cost), cost_line_items, selected_jurisdictions_postal_abbreviations, 
+            existing_privileges_for_license
+        )
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 60d51c6 and c9c35c5.

📒 Files selected for processing (2)
  • backend/compact-connect/lambdas/python/purchases/handlers/privileges.py (3 hunks)
  • backend/compact-connect/lambdas/python/purchases/tests/function/test_handlers/test_purchase_privileges.py (7 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • backend/compact-connect/lambdas/python/purchases/tests/function/test_handlers/test_purchase_privileges.py
🧰 Additional context used
🪛 Ruff (0.11.9)
backend/compact-connect/lambdas/python/purchases/handlers/privileges.py

2-2: datetime.UTC imported but unused

Remove unused import

(F401)


2-2: datetime.datetime imported but unused

Remove unused import

(F401)

⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: TestPython

Copy link
Collaborator

@landonshumway-ia landonshumway-ia left a comment

Choose a reason for hiding this comment

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

Awesome work!

@ChiefStief ChiefStief requested a review from jusdino May 28, 2025 20:29
Copy link
Collaborator

@jusdino jusdino left a comment

Choose a reason for hiding this comment

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

Beautiful 🥹

@ChiefStief ChiefStief requested a review from jlkravitz May 29, 2025 16:04
@ChiefStief
Copy link
Collaborator Author

ChiefStief commented May 29, 2025

@jlkravitz This is ready for review. Also, heads up that one of @landonshumway-ia current tickets is blocked until this gets merged so I think this is the highest priority thing from our perspective. Thanks!

@jlkravitz
Copy link
Collaborator

@ChiefStief on this right now! Sorry I didn't see this message- I should check my email notification settings...

Copy link
Collaborator

@jlkravitz jlkravitz left a comment

Choose a reason for hiding this comment

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

Left several comments! I think a couple of resolved comments still require some changes, but feel free to respond on those threads if I'm wrong and/or if you decided to leave them out for whatever reason.

Dana Stiefel added 2 commits May 30, 2025 15:39
Copy link
Contributor

@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: 2

♻️ Duplicate comments (4)
backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_privileges.py (1)

292-292: Remove commented out code.

This commented line should be removed as it serves no purpose and was flagged in previous reviews.

-        # self._run_privilege_purchase_message_handler()
backend/compact-connect/lambdas/python/purchases/handlers/privileges.py (1)

327-329: 🛠️ Refactor suggestion

Enhance cost calculation robustness.

The cost calculation should handle potential edge cases where unitPrice or quantity might be missing or contain invalid values, as mentioned in previous reviews.

Apply this diff to make the calculation more robust:

-        # calculate total cost of transaction
-        total_cost = 0
-        for line_item in cost_line_items:
-            total_cost = total_cost + float(line_item['unitPrice']) * int(line_item['quantity'])
+        # calculate total cost of transaction
+        total_cost = 0
+        for line_item in cost_line_items:
+            try:
+                unit_price = float(line_item.get('unitPrice', 0))
+                quantity = int(line_item.get('quantity', 0))
+                total_cost += unit_price * quantity
+            except (ValueError, TypeError) as e:
+                logger.error(f'Invalid line item data: {line_item}, error: {e}')
+                raise CCInternalException('Invalid transaction line item data') from e
backend/compact-connect/lambdas/python/common/cc_common/event_bus_client.py (2)

18-22: [Duplicate] Consider dependency injection in the constructor.

This empty constructor still creates tight coupling to the global config module, making testing difficult.


23-44: [Duplicate] Add error handling and logging to the _publish_event method.

The method still lacks error handling for API calls that could fail due to network issues or permission problems.

🧹 Nitpick comments (7)
backend/compact-connect/lambdas/python/purchases/handlers/privileges.py (1)

331-367: Consider extracting event publishing logic into a separate method.

The event publishing logic is well-implemented but makes the main handler quite long. Consider extracting this into a separate method like _publish_privilege_events() to improve readability and separate concerns, as suggested in previous reviews.

This would help organize the code by separating privilege creation logic from event publishing logic.

🧰 Tools
🪛 Pylint (3.3.7)

[convention] 344-344: Line too long (104/100)

(C0301)

backend/compact-connect/lambdas/python/common/cc_common/data_model/schema/data_event/api.py (2)

1-53: Add module and class docstrings for better maintainability.

The schema structure is well-designed with proper inheritance and field validation. However, adding docstrings would improve code documentation and address static analysis warnings.

Consider adding a module docstring and class docstrings:

+"""
+Marshmallow schemas for privilege purchase event validation and serialization.
+
+These schemas define the structure for privilege-related events published to the event bus,
+including purchase, issuance, and renewal events.
+"""
+
 # ruff: noqa: N801, N815  invalid-name

Also consider adding brief class docstrings for each schema class to document their purpose.

🧰 Tools
🪛 Pylint (3.3.7)

[convention] 40-40: Line too long (120/100)

(C0301)


[convention] 43-43: Line too long (104/100)

(C0301)


[convention] 1-1: Missing module docstring

(C0114)


[error] 2-2: Unable to import 'cc_common.data_model.schema.base_record'

(E0401)


[error] 3-6: Unable to import 'cc_common.data_model.schema.fields'

(E0401)


[error] 7-13: Unable to import 'marshmallow.fields'

(E0401)


[error] 14-14: Unable to import 'marshmallow.validate'

(E0401)


[convention] 17-17: Missing class docstring

(C0115)


[refactor] 17-17: Too few public methods (0/2)

(R0903)


[convention] 25-25: Missing class docstring

(C0115)


[refactor] 25-25: Too few public methods (0/2)

(R0903)


[convention] 32-32: Missing class docstring

(C0115)


[refactor] 32-32: Too few public methods (0/2)

(R0903)


[convention] 38-38: Missing class docstring

(C0115)


[refactor] 38-38: Too few public methods (0/2)

(R0903)


[convention] 47-47: Missing class docstring

(C0115)


[refactor] 47-47: Too few public methods (0/2)

(R0903)


[convention] 51-51: Missing class docstring

(C0115)


[refactor] 51-51: Too few public methods (0/2)

(R0903)


40-44: Fix line length issues for better readability.

The long lines exceed the project's 100-character limit. Consider breaking them for better readability:

-    privileges = List(Nested(PrivilegeEventPrivilegeSchema(), required=True, allow_none=False), validate=Length(1, 100))
+    privileges = List(
+        Nested(PrivilegeEventPrivilegeSchema(), required=True, allow_none=False), 
+        validate=Length(1, 100)
+    )
     totalCost = String(required=True, allow_none=False)
-    costLineItems = List(
-        Nested(PrivilegeEventLineItemSchema(), required=True, allow_none=False), validate=Length(1, 300)
-    )
+    costLineItems = List(
+        Nested(PrivilegeEventLineItemSchema(), required=True, allow_none=False), 
+        validate=Length(1, 300)
+    )
🧰 Tools
🪛 Pylint (3.3.7)

[convention] 40-40: Line too long (120/100)

(C0301)


[convention] 43-43: Line too long (104/100)

(C0301)

backend/compact-connect/stacks/notification_stack.py (1)

49-49: Consider breaking long lines for better readability.

-                'EMAIL_NOTIFICATION_SERVICE_LAMBDA_NAME': persistent_stack.email_notification_service_lambda.function_name,  # noqa: E501 line-too-long
+                'EMAIL_NOTIFICATION_SERVICE_LAMBDA_NAME': (
+                    persistent_stack.email_notification_service_lambda.function_name
+                ),
🧰 Tools
🪛 Pylint (3.3.7)

[convention] 49-49: Line too long (151/100)

(C0301)

backend/compact-connect/lambdas/python/common/cc_common/event_bus_client.py (2)

46-55: Add comprehensive docstrings for public methods.

The public event publishing methods lack docstrings. Given their complex parameter lists and role as public API, they should include detailed documentation.

Example for the first method:

 def publish_privilege_purchase_event(
     self,
     source: str,
     jurisdiction: str,
     compact: str,
     provider_email: str,
     privileges: list[dict],
     total_cost: str,
     cost_line_items: list[dict],
 ):
+    """
+    Publish a privilege purchase event to the event bus.
+    
+    Args:
+        source: The source system publishing the event
+        jurisdiction: The jurisdiction where the privilege was purchased
+        compact: The compact identifier
+        provider_email: Email address of the provider purchasing the privilege
+        privileges: List of privilege dictionaries containing privilege details
+        total_cost: Total cost of the purchase as a string
+        cost_line_items: List of cost breakdown items
+    """

Also applies to: 73-79, 94-100

🧰 Tools
🪛 Pylint (3.3.7)

[convention] 46-46: Missing function or method docstring

(C0116)


[refactor] 46-46: Too many arguments (8/5)

(R0913)


[refactor] 46-46: Too many positional arguments (8/5)

(R0917)


71-71: Fix line length violations for better readability.

The _publish_event calls exceed the 100-character limit. Consider multi-line formatting:

-        self._publish_event(source=source, detail_type='privilege.purchase', detail=deserialized_detail)
+        self._publish_event(
+            source=source, 
+            detail_type='privilege.purchase', 
+            detail=deserialized_detail
+        )

Apply similar formatting to lines 92 and 113.

Also applies to: 92-92, 113-113

🧰 Tools
🪛 Pylint (3.3.7)

[convention] 71-71: Line too long (104/100)

(C0301)

backend/compact-connect/lambdas/python/purchases/tests/function/test_handlers/test_purchase_privileges.py (1)

339-339: Minor style improvements for better maintainability.

Consider addressing these minor issues:

  1. Line length violations on lines 339 and 343 could be resolved with multi-line formatting
  2. Add docstrings to the new test methods for better documentation
  3. Specify encoding when opening files (line 344)

Example for line 339:

-        event = self._when_testing_provider_user_event_with_custom_claims(license_expiration_date=test_expiration_date)
+        event = self._when_testing_provider_user_event_with_custom_claims(
+            license_expiration_date=test_expiration_date
+        )

Also applies to: 343-343

🧰 Tools
🪛 Pylint (3.3.7)

[convention] 339-339: Line too long (119/100)

(C0301)

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 428e9c0 and 9749595.

📒 Files selected for processing (8)
  • backend/compact-connect/lambdas/python/common/cc_common/data_model/schema/data_event/api.py (1 hunks)
  • backend/compact-connect/lambdas/python/common/cc_common/event_bus_client.py (1 hunks)
  • backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_privileges.py (4 hunks)
  • backend/compact-connect/lambdas/python/purchases/handlers/privileges.py (2 hunks)
  • backend/compact-connect/lambdas/python/purchases/purchase_client.py (2 hunks)
  • backend/compact-connect/lambdas/python/purchases/tests/function/test_handlers/test_purchase_privileges.py (7 hunks)
  • backend/compact-connect/lambdas/python/purchases/tests/unit/test_purchase_client.py (2 hunks)
  • backend/compact-connect/stacks/notification_stack.py (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_privileges.py (4)
backend/compact-connect/lambdas/python/common/tests/function/__init__.py (1)
  • _load_provider_data (212-227)
backend/compact-connect/lambdas/python/common/cc_common/email_service_client.py (1)
  • send_privilege_purchase_email (187-218)
backend/compact-connect/lambdas/python/common/cc_common/exceptions.py (1)
  • CCInternalException (31-32)
backend/compact-connect/lambdas/python/provider-data-v1/handlers/privileges.py (1)
  • privilege_purchase_message_handler (127-165)
🪛 Pylint (3.3.7)
backend/compact-connect/lambdas/python/purchases/tests/function/test_handlers/test_purchase_privileges.py

[convention] 33-33: Line too long (117/100)

(C0301)


[convention] 249-249: Line too long (101/100)

(C0301)


[convention] 249-249: Missing function or method docstring

(C0116)


[convention] 339-339: Line too long (119/100)

(C0301)


[convention] 343-343: Line too long (107/100)

(C0301)


[convention] 271-271: Missing function or method docstring

(C0116)


[convention] 276-276: Import outside toplevel (handlers.privileges.post_purchase_privileges)

(C0415)


[convention] 304-304: Missing function or method docstring

(C0116)


[convention] 309-309: Import outside toplevel (handlers.privileges.post_purchase_privileges)

(C0415)


[convention] 326-326: Missing function or method docstring

(C0116)


[convention] 331-331: Import outside toplevel (handlers.privileges.post_purchase_privileges)

(C0415)


[warning] 344-344: Using open without explicitly specifying an encoding

(W1514)


[convention] 533-533: Line too long (109/100)

(C0301)


[convention] 580-580: Line too long (109/100)

(C0301)

backend/compact-connect/lambdas/python/common/cc_common/data_model/schema/data_event/api.py

[convention] 40-40: Line too long (120/100)

(C0301)


[convention] 43-43: Line too long (104/100)

(C0301)


[convention] 1-1: Missing module docstring

(C0114)


[error] 2-2: Unable to import 'cc_common.data_model.schema.base_record'

(E0401)


[error] 3-6: Unable to import 'cc_common.data_model.schema.fields'

(E0401)


[error] 7-13: Unable to import 'marshmallow.fields'

(E0401)


[error] 14-14: Unable to import 'marshmallow.validate'

(E0401)


[convention] 17-17: Missing class docstring

(C0115)


[refactor] 17-17: Too few public methods (0/2)

(R0903)


[convention] 25-25: Missing class docstring

(C0115)


[refactor] 25-25: Too few public methods (0/2)

(R0903)


[convention] 32-32: Missing class docstring

(C0115)


[refactor] 32-32: Too few public methods (0/2)

(R0903)


[convention] 38-38: Missing class docstring

(C0115)


[refactor] 38-38: Too few public methods (0/2)

(R0903)


[convention] 47-47: Missing class docstring

(C0115)


[refactor] 47-47: Too few public methods (0/2)

(R0903)


[convention] 51-51: Missing class docstring

(C0115)


[refactor] 51-51: Too few public methods (0/2)

(R0903)

backend/compact-connect/lambdas/python/common/cc_common/event_bus_client.py

[convention] 71-71: Line too long (104/100)

(C0301)


[convention] 92-92: Line too long (102/100)

(C0301)


[convention] 113-113: Line too long (103/100)

(C0301)


[convention] 1-1: Missing module docstring

(C0114)


[convention] 46-46: Missing function or method docstring

(C0116)


[refactor] 46-46: Too many arguments (8/5)

(R0913)


[refactor] 46-46: Too many positional arguments (8/5)

(R0917)


[convention] 73-73: Missing function or method docstring

(C0116)


[convention] 94-94: Missing function or method docstring

(C0116)

backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_privileges.py

[convention] 168-168: Line too long (108/100)

(C0301)


[convention] 214-214: Line too long (114/100)

(C0301)


[convention] 220-220: Line too long (117/100)

(C0301)


[convention] 249-249: Line too long (120/100)

(C0301)


[convention] 289-289: Line too long (117/100)

(C0301)


[convention] 293-293: Import outside toplevel (handlers.privileges.privilege_purchase_message_handler)

(C0415)


[warning] 295-295: Using open without explicitly specifying an encoding

(W1514)


[error] 301-301: Too many positional arguments for function call

(E1121)

backend/compact-connect/lambdas/python/purchases/handlers/privileges.py

[convention] 344-344: Line too long (104/100)

(C0301)

backend/compact-connect/lambdas/python/purchases/purchase_client.py

[convention] 467-467: Line too long (104/100)

(C0301)

backend/compact-connect/lambdas/python/purchases/tests/unit/test_purchase_client.py

[convention] 235-235: Line too long (111/100)

(C0301)


[convention] 235-235: Missing function or method docstring

(C0116)


[convention] 236-236: Import outside toplevel (purchase_client.PurchaseClient)

(C0415)


[convention] 322-322: Line too long (114/100)

(C0301)

backend/compact-connect/stacks/notification_stack.py

[convention] 49-49: Line too long (151/100)

(C0301)


[convention] 58-58: Line too long (112/100)

(C0301)


[convention] 62-62: Line too long (102/100)

(C0301)


[convention] 82-82: Line too long (119/100)

(C0301)


[convention] 99-99: Line too long (104/100)

(C0301)


[convention] 1-1: Missing module docstring

(C0114)


[error] 5-5: Unable to import 'aws_cdk'

(E0401)


[error] 6-6: Unable to import 'aws_cdk.aws_cloudwatch'

(E0401)


[error] 7-7: Unable to import 'aws_cdk.aws_cloudwatch_actions'

(E0401)


[error] 8-8: Unable to import 'aws_cdk.aws_events'

(E0401)


[error] 9-9: Unable to import 'aws_cdk.aws_events_targets'

(E0401)


[error] 10-10: Unable to import 'cdk_nag'

(E0401)


[error] 15-15: Unable to import 'constructs'

(E0401)


[convention] 20-20: Missing class docstring

(C0115)

⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: TestPython
🔇 Additional comments (13)
backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_privileges.py (2)

166-175: LGTM: Improved test isolation with targeted patching.

The refactoring from decorator-based to context manager patching with a more specific target (handlers.privileges.config.email_service_client) improves test isolation and clarity about what's being mocked.

🧰 Tools
🪛 Pylint (3.3.7)

[convention] 168-168: Line too long (108/100)

(C0301)


219-225: LGTM: Consistent refactoring pattern applied.

These changes follow the same improved patching pattern established in the earlier test method, maintaining consistency across the test suite.

Also applies to: 255-261

🧰 Tools
🪛 Pylint (3.3.7)

[convention] 220-220: Line too long (117/100)

(C0301)

backend/compact-connect/lambdas/python/purchases/purchase_client.py (2)

411-411: LGTM: Well-justified duplicate window adjustment.

The 35-second duplicate window strikes a good balance between preventing duplicate transactions and allowing legitimate retries, especially given the 30-second API Gateway timeout mentioned in previous discussions.


462-479: LGTM: Proper serialization handling for downstream processing.

The line items serialization correctly handles AuthorizeNet SDK object conversion to JSON-serializable dictionaries. The string casting prevents serialization errors and follows the same pattern used elsewhere in the codebase for transaction details.

🧰 Tools
🪛 Pylint (3.3.7)

[convention] 467-467: Line too long (104/100)

(C0301)

backend/compact-connect/lambdas/python/purchases/tests/unit/test_purchase_client.py (1)

234-278: LGTM! Excellent use of comprehensive assertion approach.

This test follows the best practice suggested in past reviews by using a single comprehensive assertion to validate the entire lineItems structure rather than multiple individual field assertions. The test thoroughly validates both line items with all expected fields.

🧰 Tools
🪛 Pylint (3.3.7)

[convention] 235-235: Line too long (111/100)

(C0301)


[convention] 235-235: Missing function or method docstring

(C0116)


[convention] 236-236: Import outside toplevel (purchase_client.PurchaseClient)

(C0415)

backend/compact-connect/lambdas/python/purchases/handlers/privileges.py (1)

308-320: Well-structured privilege filtering approach.

The privilege filtering creates a clean, minimal subset of data that's appropriate for event publishing. Good implementation that separates concerns between internal data structures and event payloads.

backend/compact-connect/stacks/notification_stack.py (2)

20-32: Excellent infrastructure setup with comprehensive monitoring.

The notification stack is well-designed with proper error handling, alarms, and permissions. The integration of EventBridge, SQS, and Lambda follows AWS best practices.

🧰 Tools
🪛 Pylint (3.3.7)

[convention] 20-20: Missing class docstring

(C0115)


100-100: Great addition of configurable DLQ alarm threshold.

Setting the DLQ alarm threshold to 1 ensures immediate notification of any failed message processing, which is appropriate for critical email notifications.

backend/compact-connect/lambdas/python/purchases/tests/function/test_handlers/test_purchase_privileges.py (5)

33-33: Good addition of test constant.

The MOCK_LINE_ITEMS constant promotes consistency and maintainability across tests.

🧰 Tools
🪛 Pylint (3.3.7)

[convention] 33-33: Line too long (117/100)

(C0301)


140-142: LGTM! Mock properly updated to match interface changes.

The mock now correctly returns both transactionId and lineItems, matching the updated purchase client interface.


261-267: Excellent test coverage for updated response format.

The assertions properly verify both transactionId and lineItems in responses, ensuring complete test coverage of the interface changes.

Also applies to: 533-533, 580-580


269-301: Comprehensive event bus integration testing.

These tests provide excellent coverage of the event publishing functionality:

  • Separate tests for each event type (purchase, issued, renewed)
  • Proper verification of event parameters and data
  • Good test isolation with focused assertions

The renewed event test properly sets up an existing privilege scenario, ensuring realistic test conditions.

Also applies to: 302-323, 324-366

🧰 Tools
🪛 Pylint (3.3.7)

[convention] 271-271: Missing function or method docstring

(C0116)


[convention] 276-276: Import outside toplevel (handlers.privileges.post_purchase_privileges)

(C0415)


713-715: Proper update to transaction voiding.

The void transaction call now includes lineItems in the order information, ensuring complete transaction data is provided for proper cleanup.

@ChiefStief ChiefStief requested a review from jlkravitz May 30, 2025 22:01
@ChiefStief
Copy link
Collaborator Author

@jlkravitz Ok, ready for another look.

Copy link
Collaborator

@jlkravitz jlkravitz left a comment

Choose a reason for hiding this comment

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

one commented out code and one coderabbit comment but otherwise looks good!

@ChiefStief
Copy link
Collaborator Author

@jlkravitz Ok, I resolved the coderabbit comment and deleted the last commented out code

@ChiefStief ChiefStief requested a review from jlkravitz May 30, 2025 22:59
Copy link
Collaborator

@jlkravitz jlkravitz left a comment

Choose a reason for hiding this comment

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

@isabeleliassen good to merge!

@isabeleliassen isabeleliassen self-requested a review June 2, 2025 13:33
@isabeleliassen isabeleliassen merged commit 7c2af7a into csg-org:development Jun 2, 2025
4 checks passed
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.

practitioner receives email confirmation with proof of privilege purchase
5 participants