Skip to content
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

Mailchimp #1332

Merged
merged 19 commits into from
Dec 11, 2024
Merged

Mailchimp #1332

merged 19 commits into from
Dec 11, 2024

Conversation

Maijjay
Copy link
Contributor

@Maijjay Maijjay commented Oct 24, 2024

Summary by CodeRabbit

Release Notes

  • New Features

    • Introduced a new component for managing user marketing consent within course materials.
    • Added marketing consent functionality to course settings and enrollment forms.
    • Implemented backend services for updating and fetching user marketing consent.
    • Enhanced user interface with new localization entries for marketing consent messages.
    • Added a new Mailchimp synchronization service to manage user marketing consents.
  • Bug Fixes

    • Corrected SQL queries to ensure accurate retrieval and updating of marketing consent data.
  • Documentation

    • Updated documentation to reflect new marketing consent features and usage.
  • Tests

    • Added tests for new marketing consent functionalities and updated existing tests to include new fields.
  • Chores

    • Updated Kubernetes configurations to deploy the new Mailchimp syncer service.

Copy link
Contributor

coderabbitai bot commented Nov 6, 2024

Walkthrough

This pull request introduces a comprehensive set of changes related to user marketing consent management across various components and services. A new React component, SelectMarketingConsentForm, is added to handle user consent for marketing communications. Backend services are updated with new functions for fetching and updating marketing consent data. Database migrations add new tables and columns to track user consent, while corresponding SQL queries are defined to manage this data. Additionally, localization files are updated to include new keys for marketing consent prompts, enhancing user interaction.

Changes

File Path Change Summary
services/course-material/src/components/forms/SelectMarketingConsentForm.tsx New component for managing marketing consent with props for course ID and consent callbacks.
services/course-material/src/components/modals/CourseSettingsModal.tsx Added queries to fetch course details and user marketing consent status.
services/course-material/src/services/backend.ts Introduced updateMarketingConsent and fetchUserMarketingConsent functions.
services/headless-lms/entrypoint/src/main.rs Added new program entry for mailchimp-syncer.
services/headless-lms/migrations/20241023104801_add-marketing-consent.down.sql Rollback migration script for marketing consent features.
services/headless-lms/migrations/20241023104801_add-marketing-consent.up.sql Migration script to add marketing consent tables and columns.
services/headless-lms/models/.sqlx/query-*.json Multiple queries updated to include ask_marketing_consent column in various SQL operations.
services/headless-lms/models/src/courses.rs Added ask_marketing_consent field to course-related structs.
services/headless-lms/models/src/marketing_consents.rs New structs and functions for managing user marketing consent data.
services/headless-lms/server/src/controllers/course_material/courses.rs New functions for updating and fetching user marketing consent.
services/headless-lms/server/src/programs/mailchimp_syncer.rs Implemented Mailchimp synchronization logic for user marketing consents.
shared-module/packages/common/src/bindings.ts New UserMarketingConsent interface added; updated existing course interfaces to include ask_marketing_consent.
shared-module/packages/common/src/locales/en/main-frontend.json Added localization entries for marketing consent prompts.
shared-module/packages/common/src/locales/fi/main-frontend.json Added Finnish localization for marketing consent prompts.
kubernetes/base/headless-lms/mailchimp-syncer.yml New Kubernetes deployment configuration for mailchimp-syncer.
kubernetes/base/kustomization.yaml Added resource entry for the new Mailchimp syncer deployment.

Poem

In fields of code, where rabbits play,
New consent forms hop into the fray.
With checkboxes bright, they ask and they seek,
For marketing whispers, both gentle and meek.
So let’s celebrate this change with delight,
As we share our news, all fluffy and bright! 🐰✨


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between e93b0ca and 1bd3858.

📒 Files selected for processing (1)
  • system-tests/src/tests/change-course-language.spec.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • system-tests/src/tests/change-course-language.spec.ts

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 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 generate docstrings to generate docstrings for this PR. (Experiment)
  • @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 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.

@nygrenh nygrenh marked this pull request as ready for review November 26, 2024 08:05
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: 33

🧹 Outside diff range and nitpick comments (29)
services/headless-lms/models/src/marketing_consents.rs (4)

201-206: Use consistent timestamps from application code instead of now()

Setting synced_to_mailchimp_at = now() directly in the SQL query can lead to slight timing inconsistencies, especially in concurrent environments. It can also make testing and debugging more challenging.

Consider passing a consistent timestamp from the application code:

UPDATE user_marketing_consents
-   SET synced_to_mailchimp_at = now()
+   SET synced_to_mailchimp_at = $2
WHERE user_id = ANY($1::uuid[])

And in your Rust code:

let current_timestamp = Utc::now();
sqlx::query!(
    "
    UPDATE user_marketing_consents
    SET synced_to_mailchimp_at = $2
    WHERE user_id = ANY($1::uuid[])
    ",
    &ids,
    current_timestamp
)
.execute(conn)
.await?;

216-216: Rename parameter pool to conn for clarity

The parameter pool is of type &mut PgConnection, which suggests an individual database connection rather than a connection pool. For consistency and clarity, consider renaming pool to conn, as in other functions.


252-252: Align error handling by using consistent result types

The function update_bulk_user_consent returns anyhow::Result<()>, while other functions in the module return sqlx::Result<()> or ModelResult<()>. For consistency across the codebase, consider using a uniform result type.

For example, you could change the return type to ModelResult<()>:

pub async fn update_bulk_user_consent(
    conn: &mut PgConnection,
    mailchimp_data: Vec<(String, bool, String, String)>,
) -> ModelResult<()> {
    // Function body
}

135-135: Simplify the CASE WHEN statement in SQL query

The CASE WHEN statement on line 135 can be simplified. Since cmc.passed is nullable, using it directly will suffice.

Simplify the SQL as follows:

CASE WHEN cmc.passed IS NOT NULL THEN cmc.passed ELSE NULL END AS completed_course

Can be written as:

cmc.passed AS completed_course
services/headless-lms/server/src/programs/mailchimp_syncer.rs (5)

97-99: Consider handling the absence of DATABASE_URL more explicitly

Currently, if the DATABASE_URL environment variable is not set, a default development database URL is used. This could lead to unintended connections in production environments. It would be safer to fail fast or log a warning if the environment variable is missing to prevent accidental misuse.


534-538: Reuse reqwest::Client instead of creating a new one in each iteration

Creating a new HTTP client inside the loop is inefficient and can lead to resource exhaustion. Consider reusing the existing REQWEST_CLIENT or instantiate the client outside of the loop.


602-605: Use structured logging instead of eprintln!

For consistency and better log management, replace eprintln! with the appropriate logging macros like error!, warn!, or info!.

Apply this diff:

-                eprintln!(
+                error!(

482-508: Handle unused variable failed_user_ids

The variable failed_user_ids collects user IDs for which email updates failed but is not used afterward. Consider logging these failures or implementing retry logic to handle them.


225-225: Specify appropriate field types when adding merge fields

When adding merge fields to Mailchimp, ensure the field type matches the expected data. For example, fields like MARKETING could be better represented as dropdown or radio types instead of text. This enhances data integrity and user experience in Mailchimp.

services/headless-lms/server/src/controllers/course_material/courses.rs (1)

940-941: Fix endpoint formatting in the documentation comment

The endpoint in the documentation comment includes an extra hyphen before fetch-user-marketing-consent. Apply this diff to correct it:

-GET `/api/v0/course-material/courses/:course_id/-fetch-user-marketing-consent`
+GET `/api/v0/course-material/courses/:course_id/fetch-user-marketing-consent`
services/headless-lms/models/.sqlx/query-a6659b3a5772ec77fc86b1a7e6abf2f884ee622559d13dfa0d47261cc9f8d4a2.json (1)

3-3: Consider handling duplicate UUIDs in the input array.

The current query might process the same UUID multiple times if it appears in the input array. Consider using DISTINCT to handle duplicates.

-UPDATE user_marketing_consents
-SET synced_to_mailchimp_at = now()
-WHERE user_id IN (
-    SELECT UNNEST($1::uuid [])
-  )
+UPDATE user_marketing_consents
+SET synced_to_mailchimp_at = now()
+WHERE user_id IN (
+    SELECT DISTINCT UNNEST($1::uuid[])
+  )
services/headless-lms/server/src/programs/mod.rs (1)

9-9: Consider background service configuration and monitoring.

Since this is a background service that will sync data with an external system (Mailchimp):

  1. Ensure proper retry mechanisms and error handling for network issues
  2. Consider implementing health checks and monitoring
  3. Plan for rate limiting and batch processing to handle large datasets
  4. Document the sync frequency and failure recovery procedures
services/headless-lms/models/.sqlx/query-11a20e21e4996d77f0cef1949b9efebe3aa4c2616f15ae2ef95e6940849a6fed.json (1)

1-18: Consider adding audit trail columns

For GDPR compliance and tracking purposes, it's important to maintain a history of consent changes.

Consider:

  1. Adding timestamp columns (created_at, updated_at)
  2. Adding version or revision tracking
  3. Implementing soft delete with deleted_at
  4. Creating a separate audit table to track consent history

Would you like me to provide a detailed implementation for these suggestions?

services/headless-lms/models/.sqlx/query-7cb0af035cdafd0dadcd587670bde30eb4e094a84a06a884c432bfc841c7f8bc.json (1)

2-3: Consider adding LIMIT clause

The query could potentially return multiple rows for the same user_id and course_id combination. Consider adding a LIMIT clause if only one record is expected.

  SELECT id,
    course_id,
    course_language_groups_id,
    user_id,
    user_mailchimp_id,
    consent,
    created_at,
    updated_at,
    deleted_at,
    synced_to_mailchimp_at
  FROM user_marketing_consents
- WHERE user_id = $1 AND course_id = $2
+ WHERE user_id = $1 AND course_id = $2
+ LIMIT 1
services/headless-lms/models/.sqlx/query-668b7cb6022b915f1838997679cc60864b00491bb34f1559377a36cee1a6485c.json (1)

3-3: Consider adding an index for performance optimization.

Since this query filters on exam_id and includes a JOIN between courses and course_exams, consider adding an index on course_exams(exam_id) if not already present. This would improve query performance, especially as the number of courses and exams grows.

services/headless-lms/models/.sqlx/query-f53c5545a9f632da9076ce08319f10b891d1b22fa2716e958a3de831f22217c8.json (1)

Line range hint 1-133: Consider consent management architecture implications.

Implementing marketing consent at the course level raises some architectural considerations:

  1. GDPR compliance: Ensure this structure allows for easy consent withdrawal and audit trails
  2. Consider if course-level granularity is appropriate vs. organization-level consent
  3. Think about consent expiration and renewal mechanisms

These aspects might need additional database fields or related tables in the future.

services/headless-lms/models/.sqlx/query-b7647f94557ccce49973abf5a9829a594929d26cdbdebe3ae65a75e63b25d537.json (2)

3-3: Improve SQL query formatting for better readability

The SQL query could benefit from consistent indentation and alignment. Also, the new ask_marketing_consent column is not aligned with other columns.

Consider reformatting like this:

-  c.ask_marketing_consent\nFROM courses as c\n
+    c.ask_marketing_consent
+FROM courses as c

Consider adding an index to improve query performance

Based on the analysis of the table structure and existing indexes, I recommend:

  1. The DISTINCT is necessary as the LEFT JOIN with course_instances can produce duplicate course records when a course has multiple instances.
  2. There is no existing index that would efficiently support the query's WHERE clause conditions on starts_at, ends_at, and course_id.
  3. Adding the following index would improve query performance:
CREATE INDEX idx_course_instances_temporal 
ON course_instances(course_id, starts_at, ends_at) 
WHERE deleted_at IS NULL;

This index will help the database efficiently filter course instances by their time range and course_id while respecting the soft delete pattern.

🔗 Analysis chain

Line range hint 3-133: Consider query performance with additional column

The query uses DISTINCT with a LEFT JOIN, which could impact performance on large datasets. While adding one boolean column won't significantly affect performance, consider:

  1. Whether DISTINCT is necessary (are there duplicate course IDs from the JOIN?)
  2. If an index on course_instances(course_id, starts_at, ends_at, deleted_at) would help
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Look for existing indexes that might help this query

# Search for index definitions
fd -e sql | xargs rg -l 'CREATE.*INDEX.*course_instances'

Length of output: 254


Script:

#!/bin/bash
# Let's check the content of these migration files to understand the existing indexes
cat services/headless-lms/migrations/20230622140948_add_unique_nulls_not_distict_to_unique_indexes.down.sql services/headless-lms/migrations/20210706120250_restrict_course_instance_null_names.up.sql

# Also, let's look for the table definition to understand the structure
fd -e sql | xargs rg 'CREATE TABLE.*course_instances'

Length of output: 5976


Script:

#!/bin/bash
# Let's check the course_instances table definition
cat services/headless-lms/migrations/20210609111555_add_user_exercise_states.up.sql

# Also let's check if there are any other indexes on course_instances
fd -e sql | xargs rg 'CREATE.*INDEX.*course_id.*starts_at.*ends_at'

Length of output: 2666

services/headless-lms/models/.sqlx/query-b57d40a7daf9d2ae609f953f48f756c796ee19b751ffe4740d06600cdae0fd69.json (1)

3-3: Consider query performance optimization.

The LEFT JOIN with course_module_completions might impact performance for users with many completions. Consider:

  1. Adding a covering index for the join conditions
  2. Evaluating if the completion status is always needed

Alternative query structure if completion status isn't always needed:

SELECT
    umc.id,
    -- other fields...
    EXISTS (
        SELECT 1 
        FROM course_module_completions 
        WHERE user_id = umc.user_id 
        AND course_id = umc.course_id 
        AND passed = true
    ) AS completed_course
FROM user_marketing_consents AS umc
-- other joins...
services/headless-lms/models/.sqlx/query-074ad35b8fa2588b0c36be3f18ec160f8bd276628e02678031d28e6545dfbb22.json (1)

2-3: Consider adding indexes for performance optimization

The query filters on course_language_groups_id and compares synced_to_mailchimp_at with updated_at. Consider adding a composite index to optimize these operations.

CREATE INDEX idx_user_marketing_consents_sync 
ON user_marketing_consents(course_language_groups_id, synced_to_mailchimp_at, updated_at);
services/headless-lms/models/.sqlx/query-ee5080bbfb823d38cfbd65043c874be0cd06b4310d4a5156fdb306054916c9e5.json (1)

Line range hint 1-149: Consider adding database constraints for marketing consent.

While the query structure is correct, consider whether additional database constraints should be added for the ask_marketing_consent field, such as:

  • A default value for backward compatibility
  • A trigger to handle consent revocation
  • An index if you'll be querying by this field frequently

Would you like me to provide example SQL for these constraints?

services/headless-lms/migrations/20241023104801_add-marketing-consent.up.sql (2)

29-29: Fix typo in comment documentation.

There's a typo in the comment: "Wheter" should be "Whether"

-COMMENT ON COLUMN user_marketing_consents.consent IS 'Wheter the user has given a marketing consent for a specific course.';
+COMMENT ON COLUMN user_marketing_consents.consent IS 'Whether the user has given a marketing consent for a specific course.';

8-11: Consider adding indexes for foreign key columns.

The table has multiple foreign key columns that will likely be used in JOIN operations and WHERE clauses. Consider adding indexes to improve query performance.

CREATE INDEX idx_user_marketing_consents_course_id ON user_marketing_consents(course_id);
CREATE INDEX idx_user_marketing_consents_course_language_groups_id ON user_marketing_consents(course_language_groups_id);
CREATE INDEX idx_user_marketing_consents_user_id ON user_marketing_consents(user_id);
services/course-material/src/components/modals/CourseSettingsModal.tsx (1)

Line range hint 1-244: Consider splitting the component for better maintainability

The CourseSettingsModal component is handling multiple responsibilities:

  • Course language selection
  • Course instance selection
  • Marketing consent management

Consider extracting these into separate components to improve maintainability and testing. This would also make it easier to add more features in the future.

Example structure:

// CourseSettingsModal.tsx
const CourseSettingsModal = () => (
  <Dialog>
    <CourseLanguageSection />
    <CourseInstanceSection />
    <MarketingConsentSection />
    <LanguageChangeWarning />
  </Dialog>
)
services/headless-lms/models/src/exams.rs (2)

Line range hint 392-394: Fix incorrect JOIN conditions in get_exams_for_course

The current JOIN conditions are incomplete and could lead to incorrect results:

  1. The course_exams JOIN is missing the connection to the exams table
  2. This could result in a cross product or missing results

Apply this fix to correct the JOIN conditions:

-  JOIN course_exams ON course_id = $1
-  JOIN courses ON courses.id = $1
+  JOIN course_exams ON course_exams.exam_id = exams.id AND course_exams.course_id = $1
+  JOIN courses ON courses.id = course_exams.course_id

101-101: Add explicit type casting for ask_marketing_consent

To ensure type safety and proper NULL handling for the boolean field:

-  ask_marketing_consent
+  COALESCE(ask_marketing_consent, false) as ask_marketing_consent
services/headless-lms/models/src/courses.rs (1)

Line range hint 1-979: Consider adding documentation for the marketing consent field

The new field would benefit from documentation explaining its purpose and usage, especially in the struct definitions.

Add documentation comments to the relevant structs:

 #[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
 #[cfg_attr(feature = "ts_rs", derive(TS))]
 pub struct Course {
     // ... other fields ...
+    /// Indicates whether the course requires marketing consent from users.
+    /// When true, users will be prompted to provide marketing consent specific to this course.
     pub ask_marketing_consent: bool,
 }
shared-module/packages/common/src/bindings.ts (1)

216-216: Unrelated change: Added hide_citations to ChatbotConversationInfo

This change appears to be unrelated to the marketing consent feature. While the change itself is valid, consider splitting unrelated changes into separate PRs for better traceability.

services/headless-lms/server/src/programs/seed/seed_courses.rs (1)

99-99: Consider extracting the default marketing consent value to a constant.

The changes correctly add the ask_marketing_consent field to all NewCourse struct instances. However, the default value false is duplicated across multiple locations.

Consider extracting the default value to a constant at the module level for better maintainability:

+const DEFAULT_MARKETING_CONSENT: bool = false;

// Then use it in all NewCourse instances:
-        ask_marketing_consent: false,
+        ask_marketing_consent: DEFAULT_MARKETING_CONSENT,

Also applies to: 2034-2034, 2161-2161, 3041-3041, 4449-4449

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between c4a2136 and 4a7a770.

📒 Files selected for processing (45)
  • services/course-material/src/components/forms/SelectMarketingConsentForm.tsx (1 hunks)
  • services/course-material/src/components/modals/CourseSettingsModal.tsx (3 hunks)
  • services/course-material/src/services/backend.ts (3 hunks)
  • services/headless-lms/entrypoint/src/main.rs (1 hunks)
  • services/headless-lms/migrations/20241023104801_add-marketing-consent.down.sql (1 hunks)
  • services/headless-lms/migrations/20241023104801_add-marketing-consent.up.sql (1 hunks)
  • services/headless-lms/models/.sqlx/query-074ad35b8fa2588b0c36be3f18ec160f8bd276628e02678031d28e6545dfbb22.json (1 hunks)
  • services/headless-lms/models/.sqlx/query-0a19c9653e258dd34bce3053b2c7cecf9c5427530798ed0225f4a64d6f3880ba.json (1 hunks)
  • services/headless-lms/models/.sqlx/query-11a20e21e4996d77f0cef1949b9efebe3aa4c2616f15ae2ef95e6940849a6fed.json (1 hunks)
  • services/headless-lms/models/.sqlx/query-32396d664f3f617f45c734993b62373ceff25cf391850e08a2697e0dd03f8d94.json (3 hunks)
  • services/headless-lms/models/.sqlx/query-393824929f6e6e064757af094951bc418d2da0f66429c32c48708cd3dccd0f3a.json (3 hunks)
  • services/headless-lms/models/.sqlx/query-401662d18cc190e1bab25142d7c3f021a4e66262cd6784eef8bc68a0c2727471.json (3 hunks)
  • services/headless-lms/models/.sqlx/query-668b7cb6022b915f1838997679cc60864b00491bb34f1559377a36cee1a6485c.json (3 hunks)
  • services/headless-lms/models/.sqlx/query-748c01cf60043988fba2a02a5d1ea5c3fd35a5adbd69b62e5dd5daa0855596b5.json (3 hunks)
  • services/headless-lms/models/.sqlx/query-7cb0af035cdafd0dadcd587670bde30eb4e094a84a06a884c432bfc841c7f8bc.json (1 hunks)
  • services/headless-lms/models/.sqlx/query-9fbc4e80f16e7cab3c8928ed195aa6b786df2e76edf819f9decfa7c441591925.json (3 hunks)
  • services/headless-lms/models/.sqlx/query-a6659b3a5772ec77fc86b1a7e6abf2f884ee622559d13dfa0d47261cc9f8d4a2.json (1 hunks)
  • services/headless-lms/models/.sqlx/query-a9f3390d8ad3debab9850325c54beba6bf46eaaec156732e307dcaa0ce08f600.json (3 hunks)
  • services/headless-lms/models/.sqlx/query-b57d40a7daf9d2ae609f953f48f756c796ee19b751ffe4740d06600cdae0fd69.json (1 hunks)
  • services/headless-lms/models/.sqlx/query-b5da68bc470f028a050fcca492eaf73c18e7f97137ea7f5f4ce1f1322e8c137c.json (3 hunks)
  • services/headless-lms/models/.sqlx/query-b7647f94557ccce49973abf5a9829a594929d26cdbdebe3ae65a75e63b25d537.json (3 hunks)
  • services/headless-lms/models/.sqlx/query-cdf31684a716948b9ed005acb59f8b556d48af088e3f489e627565a330b26ffb.json (3 hunks)
  • services/headless-lms/models/.sqlx/query-e1a28636054909bb7e9622667a32bdcb2a17e6df2bf5b046fb6ecc92640943f5.json (3 hunks)
  • services/headless-lms/models/.sqlx/query-ee5080bbfb823d38cfbd65043c874be0cd06b4310d4a5156fdb306054916c9e5.json (4 hunks)
  • services/headless-lms/models/.sqlx/query-f53c5545a9f632da9076ce08319f10b891d1b22fa2716e958a3de831f22217c8.json (3 hunks)
  • services/headless-lms/models/.sqlx/query-fa2a4f76355cf8f829e239ae278ab72efc11d1cf746c7081e5ca1e25eee9f7cd.json (3 hunks)
  • services/headless-lms/models/src/chapters.rs (1 hunks)
  • services/headless-lms/models/src/courses.rs (18 hunks)
  • services/headless-lms/models/src/exams.rs (1 hunks)
  • services/headless-lms/models/src/lib.rs (1 hunks)
  • services/headless-lms/models/src/library/copying.rs (4 hunks)
  • services/headless-lms/models/src/marketing_consents.rs (1 hunks)
  • services/headless-lms/models/src/test_helper.rs (1 hunks)
  • services/headless-lms/server/src/controllers/course_material/courses.rs (3 hunks)
  • services/headless-lms/server/src/programs/mailchimp_syncer.rs (1 hunks)
  • services/headless-lms/server/src/programs/mod.rs (1 hunks)
  • services/headless-lms/server/src/programs/seed/seed_courses.rs (5 hunks)
  • services/headless-lms/server/src/programs/seed/seed_organizations/uh_cs.rs (1 hunks)
  • services/headless-lms/server/src/programs/seed/seed_organizations/uh_mathstat.rs (4 hunks)
  • services/headless-lms/server/src/test_helper.rs (1 hunks)
  • services/headless-lms/server/src/ts_binding_generator.rs (1 hunks)
  • services/main-frontend/src/components/forms/NewCourseForm.tsx (2 hunks)
  • services/main-frontend/src/components/page-specific/manage/courses/id/index/UpdateCourseForm.tsx (3 hunks)
  • shared-module/packages/common/src/bindings.guard.ts (6 hunks)
  • shared-module/packages/common/src/bindings.ts (5 hunks)
✅ Files skipped from review due to trivial changes (1)
  • services/headless-lms/migrations/20241023104801_add-marketing-consent.down.sql
🔇 Additional comments (96)
services/headless-lms/server/src/programs/mailchimp_syncer.rs (1)

297-311: Add error handling for fetching Mailchimp data

Currently, if fetching Mailchimp data fails, the program may panic or proceed with empty data. Ensure that errors are properly caught and handled to prevent crashes and ensure data integrity.

Run the following script to check for unhandled errors in fetch_mailchimp_data_in_chunks:

services/headless-lms/server/src/controllers/course_material/courses.rs (5)

9-9: Verify the dependency on UserMarketingConsent

Ensure that the imported UserMarketingConsent struct from headless_lms_models::marketing_consents is correctly defined and compatible with its usage in the new functions.


905-910: Definition of UserMarketingConsentPayload is appropriate

The struct UserMarketingConsentPayload correctly encapsulates the required fields course_language_groups_id and consent. This complies with best practices for data transfer objects.


915-937: Ensure proper error handling and response codes

Verify that the update_marketing_consent function correctly handles potential errors from the database operation and returns appropriate HTTP status codes. This ensures that the client can handle responses effectively.


1045-1050: Confirm new routes are correctly registered

Ensure that the new routes for update_marketing_consent and fetch_user_marketing_consent are properly added and do not conflict with existing routes. This is crucial for the API endpoints to function as expected.


955-957: ⚠️ Potential issue

Correct error handling in fetch_user_marketing_consent function

The current implementation may not handle errors correctly. Using .await?.ok() is incorrect because the ? operator will propagate errors, and calling .ok() afterward on () doesn't achieve the intended result. Remove the .ok() to properly handle the Result.

Apply this diff to fix the error handling:

-let result =
-    models::marketing_consents::fetch_user_marketing_consent(&mut conn, *course_id, &user.id)
-        .await
-        .ok();
+let result = models::marketing_consents::fetch_user_marketing_consent(&mut conn, *course_id, &user.id)
+    .await?;

Likely invalid or redundant comment.

services/headless-lms/models/.sqlx/query-a6659b3a5772ec77fc86b1a7e6abf2f884ee622559d13dfa0d47261cc9f8d4a2.json (1)

1-12: Verify database performance optimization.

To ensure optimal performance for this query, we should verify that appropriate indexes exist on the user_marketing_consents table.

services/headless-lms/server/src/programs/mod.rs (1)

9-9: LGTM! Verify integration points.

The addition of the mailchimp_syncer module aligns with the PR's objective to implement marketing consent functionality.

Let's verify the integration points:

✅ Verification successful

Integration points verified successfully

The mailchimp_syncer module is well-integrated with proper:

  • Registration in the main entrypoint for program execution
  • Integration with marketing consent database models and structures
  • Implementation of background service with interval-based execution
  • Comprehensive error handling and logging
  • Database interactions for marketing consent data
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify the mailchimp_syncer integration
# Check for the main entry point registration
rg -A 5 "mailchimp_syncer" services/headless-lms/entrypoint/src/main.rs

# Check for related marketing consent structures
rg -A 10 "UserMarketingConsent" services/headless-lms/models/src/marketing_consents.rs

# Check for database migration files related to marketing consent
fd -e sql | rg -l "marketing_consent"

Length of output: 2209


Script:

#!/bin/bash
# Let's verify the mailchimp syncer implementation and its integration
# Check the implementation of mailchimp_syncer
ast-grep --pattern 'fn main() {
  $$$
}'  services/headless-lms/server/src/programs/mailchimp_syncer.rs

# Check for any background service or task scheduling patterns
rg -A 5 "tokio::time|interval|sleep|spawn" services/headless-lms/server/src/programs/mailchimp_syncer.rs

# Check for error handling and logging
rg "(error|warn|info|debug|trace)!" services/headless-lms/server/src/programs/mailchimp_syncer.rs

# Check for database interactions
rg "sqlx::(query|query_as)" services/headless-lms/server/src/programs/mailchimp_syncer.rs

Length of output: 1426

services/headless-lms/models/.sqlx/query-11a20e21e4996d77f0cef1949b9efebe3aa4c2616f15ae2ef95e6940849a6fed.json (2)

12-14: LGTM! Parameters are properly typed

The query uses strongly-typed parameters which prevents SQL injection vulnerabilities.


1-18: Verify related database queries and schema

Let's ensure consistency across related queries and validate the table structure.

✅ Verification successful

Schema and queries are properly structured

The verification confirms that:

  1. The table schema is properly defined with:

    • Correct foreign key constraints to courses, course_language_groups, and users tables
    • Appropriate uniqueness constraint using NULLS NOT DISTINCT for (user_id, course_language_groups_id)
    • Proper timestamps and soft deletion support
  2. The related queries are consistent and well-structured:

    • Insert/update query with proper conflict handling
    • Select queries with appropriate joins to related tables
    • Mailchimp synchronization tracking with synced_to_mailchimp_at

The implementation follows the established patterns in the codebase for similar consent-related tables and uniqueness constraints.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Find related SQL queries and schema definitions

# Look for other queries related to user_marketing_consents
echo "=== Related Queries ==="
rg -l "user_marketing_consents" --type sql

# Look for schema definition
echo -e "\n=== Schema Definition ==="
fd -e sql | xargs rg -l "CREATE TABLE.*user_marketing_consents"

# Look for other consent-related queries
echo -e "\n=== Other Consent Queries ==="
rg -l "consent" --type sql

Length of output: 1523


Script:

#!/bin/bash
# Let's examine the schema definition and related migrations

# Check the marketing consent table schema
echo "=== Marketing Consent Table Schema ==="
cat services/headless-lms/migrations/20241023104801_add-marketing-consent.up.sql

# Check indices for the table
echo -e "\n=== Indices on user_marketing_consents ==="
rg "CREATE.*INDEX.*user_marketing_consents" services/headless-lms/migrations/*

# Check if there are any foreign key constraints
echo -e "\n=== Foreign Key Constraints ==="
rg "FOREIGN KEY.*user_marketing_consents" services/headless-lms/migrations/*
rg "REFERENCES.*user_marketing_consents" services/headless-lms/migrations/*

# Look for any other queries in .sqlx files related to this table
echo -e "\n=== Related SQLX Queries ==="
rg -l "user_marketing_consents" services/headless-lms/models/.sqlx/

Length of output: 91992


Script:

#!/bin/bash
# Let's check the content of related SQLX queries to understand the complete usage pattern

echo "=== Related SQLX Query Contents ==="
for file in services/headless-lms/models/.sqlx/query-b57d40a7daf9d2ae609f953f48f756c796ee19b751ffe4740d06600cdae0fd69.json \
            services/headless-lms/models/.sqlx/query-7cb0af035cdafd0dadcd587670bde30eb4e094a84a06a884c432bfc841c7f8bc.json \
            services/headless-lms/models/.sqlx/query-074ad35b8fa2588b0c36be3f18ec160f8bd276628e02678031d28e6545dfbb22.json \
            services/headless-lms/models/.sqlx/query-a6659b3a5772ec77fc86b1a7e6abf2f884ee622559d13dfa0d47261cc9f8d4a2.json; do
    echo -e "\nFile: $file"
    cat "$file"
done

# Check if there are any other tables with similar consent-related uniqueness constraints
echo -e "\n=== Similar Uniqueness Constraints ==="
rg "CONSTRAINT.*uniqueness.*UNIQUE.*NULLS NOT DISTINCT" services/headless-lms/migrations/*

Length of output: 42643

services/headless-lms/models/.sqlx/query-0a19c9653e258dd34bce3053b2c7cecf9c5427530798ed0225f4a64d6f3880ba.json (1)

28-30: ⚠️ Potential issue

Review security implications of exposing access_token

The query exposes sensitive access_token data. Consider if this token needs to be retrieved in all cases or if it can be fetched separately when needed.

Let's verify the token usage:

services/course-material/src/components/forms/SelectMarketingConsentForm.tsx (1)

1-54: Verify security measures and enhance accessibility

Please ensure:

  1. Backend calls are protected against CSRF attacks
  2. The consent form meets accessibility guidelines
services/headless-lms/models/.sqlx/query-7cb0af035cdafd0dadcd587670bde30eb4e094a84a06a884c432bfc841c7f8bc.json (2)

57-60: LGTM: Parameter and nullability configuration

The parameter types and nullability constraints are well-defined:

  • Critical fields (id, course_id, user_id, consent) are correctly set as non-nullable
  • Integration fields (user_mailchimp_id, synced_to_mailchimp_at) are appropriately nullable
  • Parameters are properly typed as UUIDs

2-3: Verify indexes for query performance

The query filters on user_id and course_id. Ensure there's an appropriate index to support this query pattern.

services/headless-lms/models/.sqlx/query-a9f3390d8ad3debab9850325c54beba6bf46eaaec156732e307dcaa0ce08f600.json (3)

133-133: LGTM: Hash update is correct

The hash value has been properly updated to reflect the query changes.


3-3: Verify the existence of ask_marketing_consent column

The query has been updated to include the new ask_marketing_consent column. Let's verify its existence in the database schema.

✅ Verification successful

Column ask_marketing_consent is properly defined with appropriate migration files

The migration files show that:

  • Column is added with a non-null constraint and a default value of FALSE
  • Documentation comment explains its purpose
  • Proper up/down migrations exist for adding/removing the column
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify the column exists in the courses table
# Expected: Find migration file that adds this column

# Look for migration files that mention the new column
rg -l "ask_marketing_consent" --type sql

# Look for the column definition in the schema
fd -e sql -x grep -l "courses" {} \; | xargs rg "ask_marketing_consent"

Length of output: 739


129-130: Verify migration strategy for non-nullable constraint

The ask_marketing_consent column is marked as non-nullable (false in nullable array). This requires:

  1. A default value for existing rows during migration
  2. Handling in the application layer for new course creation

Please ensure there's a migration strategy in place.

✅ Verification successful

Migration strategy for non-nullable constraint is properly handled

The migration strategy is correctly implemented:

  • The up.sql migration adds the column with NOT NULL DEFAULT FALSE, ensuring existing rows get a default value
  • The schema change is reversible with a corresponding down.sql migration
  • The column's purpose is well-documented with a COMMENT statement
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Look for migration handling
# Expected: Find migration file with default value handling

# Search for relevant migration files
fd -e sql -x grep -l "courses" {} \; | xargs rg -A 5 -B 5 "ask_marketing_consent"

# Look for course creation handling in Rust code
ast-grep --pattern 'struct NewCourse {
  $$$
  ask_marketing_consent: $_
  $$$
}'

Length of output: 1681

services/headless-lms/models/.sqlx/query-cdf31684a716948b9ed005acb59f8b556d48af088e3f489e627565a330b26ffb.json (4)

100-104: LGTM! Column definition is well-structured

The column definition correctly specifies:

  • Ordinal position: 19
  • Name: ask_marketing_consent
  • Type: Bool (PostgreSQL boolean)

133-133: LGTM! Hash update is correct

The hash value has been updated to reflect the changes in the query.


3-3: Verify database schema for the new column

The query now includes ask_marketing_consent column, but we need to ensure the database schema has been properly migrated.

✅ Verification successful

Database schema is properly configured for the new column

The migration files confirm that:

  • The ask_marketing_consent column is added with NOT NULL DEFAULT FALSE constraint
  • The column is properly documented with a descriptive comment
  • Both up and down migrations exist for safe rollback
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check if the database migration for ask_marketing_consent exists
# and verify its default value since it's non-nullable

# Look for migration files containing ask_marketing_consent
fd -e sql . | xargs rg -l "ask_marketing_consent"

# Look for the column definition to verify default value
rg -A 3 "ask_marketing_consent.*BOOLEAN" 

Length of output: 734


129-130: Verify handling of non-nullable constraint

The column is marked as non-nullable (false), which means:

  1. Existing rows must have a default value
  2. New course creation must always include this field
✅ Verification successful

Based on the gathered information, I can now provide the final response:

Default value is properly handled for ask_marketing_consent column

The migration adds the column with a default value of FALSE:

ALTER TABLE courses
ADD COLUMN ask_marketing_consent BOOLEAN NOT NULL DEFAULT FALSE;

This ensures:

  1. Existing rows automatically get FALSE value
  2. New courses will get FALSE if not explicitly set
  3. The non-null constraint is satisfied for both existing and new data
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check course creation code to ensure ask_marketing_consent is always set

# Look for course creation/insertion code
ast-grep --pattern 'INSERT INTO courses'

# Check if the field is required in course creation forms/APIs
rg -A 5 "type NewCourse"

Length of output: 69


Script:

#!/bin/bash
# Let's try a different approach to find SQL queries and schema definitions

# Check the SQL query file content
cat services/headless-lms/models/.sqlx/query-cdf31684a716948b9ed005acb59f8b556d48af088e3f489e627565a330b26ffb.json

# Look for SQL migration files that might contain the table definition
fd -e sql -e SQL

# Search for course-related structs and types
rg -g '*.rs' -A 5 'struct.*Course'

Length of output: 73907


Script:

#!/bin/bash
# Let's check the migration that added the ask_marketing_consent column and related code

# Find the marketing consent migration
cat services/headless-lms/migrations/20241023104801_add-marketing-consent.up.sql

# Look for code that handles course creation
rg -g '*.rs' -A 5 'pub async fn (create|insert).*course'

# Look for NewCourse struct usage
ast-grep --pattern 'NewCourse { $$$'

Length of output: 6229


Script:

#!/bin/bash
# Let's check the default value handling in the code

# Look for code that handles course creation with NewCourse
rg -g '*.rs' -B 10 -A 5 'impl.*NewCourse'

# Check if there are any tests related to course creation
rg -g '*test*.rs' -A 5 'new_course|create_course'

# Look for any code that sets ask_marketing_consent
rg -g '*.rs' 'ask_marketing_consent.*='

Length of output: 4638


Script:

#!/bin/bash
# Let's check the NewCourse struct definition and its usage

# Look for NewCourse struct definition
rg -g '*.rs' -B 5 -A 15 'struct NewCourse'

# Check for any GraphQL mutations or API endpoints that create courses
rg -g '*.rs' -A 10 'mutation.*create.*course|POST.*course'

# Look for any test data that creates courses
rg -g '*.rs' -A 10 'test.*create.*course|mock.*course'

Length of output: 69566

services/headless-lms/models/.sqlx/query-401662d18cc190e1bab25142d7c3f021a4e66262cd6784eef8bc68a0c2727471.json (4)

100-104: LGTM! Column definition is well-structured

The ask_marketing_consent column is correctly defined as a boolean type, which is appropriate for storing consent flags.


133-133: Hash update is correct

The hash value has been automatically updated to reflect the query changes.


129-130: Verify migration strategy for non-nullable ask_marketing_consent

The ask_marketing_consent column is configured as non-nullable. Please ensure that the migration handles existing rows appropriately by setting a default value.

✅ Verification successful

Migration strategy for non-nullable ask_marketing_consent is properly handled

The migration appropriately sets a default value of FALSE when adding the non-nullable ask_marketing_consent column, ensuring existing rows will have a valid state:

ADD COLUMN ask_marketing_consent BOOLEAN NOT NULL DEFAULT FALSE;
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check migration files for ask_marketing_consent column addition

# Search for relevant migration files
fd -e sql | xargs rg -A 5 'ALTER TABLE.*courses.*ask_marketing_consent'

Length of output: 450


Script:

#!/bin/bash
# Check the up migration file for ask_marketing_consent column addition
cat services/headless-lms/migrations/20241023104801_add-marketing-consent.up.sql

Length of output: 4514


3-3: Verify index existence for the slug column

The query now filters by slug instead of join_code. To ensure optimal query performance, we should verify that an appropriate index exists on the slug column.

✅ Verification successful

Index exists but may not be optimal for the query

The search reveals that there are unique indexes on the slug column, but they are specifically for ensuring uniqueness when deleted_at IS NOT NULL. This doesn't help our query which filters for deleted_at IS NULL. Let's verify if there's a general index that would benefit this query.


Let's check for the actual table constraints to get a complete picture of the indexing situation.


Based on all the gathered evidence, I can now provide a final response:

Query performance is ensured with existing indexes

The codebase analysis reveals that the courses table has proper indexing for the query:

  • A unique index courses_slug_key_when_not_deleted exists for slug column
  • The query's WHERE clause (slug = $1 AND deleted_at IS NULL) is properly supported by the table's constraints and indexes
  • The change from join_code to slug is safe from a performance perspective
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for indexes on the slug column in the courses table

# Search for CREATE INDEX statements in migration files
fd -e sql | xargs rg -A 2 'CREATE.*INDEX.*courses.*slug'

# Alternative: Search for index definitions in schema files
fd schema -e sql | xargs rg -A 2 'CREATE.*INDEX.*courses.*slug'

Length of output: 1766


Script:

#!/bin/bash
# Search for any other index on courses table that might cover our query
fd -e sql | xargs rg -B 2 -A 2 'CREATE.*INDEX.*courses.*'

# Also check schema.sql which might have the complete index definitions
fd schema.sql | xargs cat

Length of output: 5389


Script:

#!/bin/bash
# Search for table creation and constraints for courses table
fd -e sql | xargs rg -B 2 -A 5 'CREATE TABLE courses|ALTER TABLE courses ADD CONSTRAINT'

# Also specifically look for unique constraints on slug
fd -e sql | xargs rg -B 2 -A 2 'ALTER TABLE courses ADD CONSTRAINT.*slug'

Length of output: 946

services/headless-lms/models/.sqlx/query-9fbc4e80f16e7cab3c8928ed195aa6b786df2e76edf819f9decfa7c441591925.json (4)

129-130: LGTM! Nullable configuration is consistent

The nullable configuration correctly matches the column definition, marking ask_marketing_consent as non-nullable (false).


133-133: LGTM! Hash update is present

The query hash has been properly updated to reflect the changes.


100-104: Verify database migration for non-nullable column

The ask_marketing_consent column is configured as non-nullable (Bool). Ensure there's a corresponding migration that:

  1. Adds the column with a default value for existing rows
  2. Handles the transition from nullable to non-nullable if applicable
✅ Verification successful

Let me check the content of these migration files to verify how the column is handled.


Migration for non-nullable column is properly handled

The migration properly handles the ask_marketing_consent column by:

  1. Adding it as a non-nullable BOOLEAN with a DEFAULT FALSE value, which ensures existing rows get a valid value
  2. Including proper cleanup in the down migration
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Look for related migrations
fd -e sql . | xargs grep -l "ask_marketing_consent"

Length of output: 207


Script:

#!/bin/bash
# Check the content of the migration files
echo "=== UP migration ==="
cat services/headless-lms/migrations/20241023104801_add-marketing-consent.up.sql
echo -e "\n=== DOWN migration ==="
cat services/headless-lms/migrations/20241023104801_add-marketing-consent.down.sql

Length of output: 4851


3-3: Verify the necessity of returning marketing consent data during deletion

The query returns ask_marketing_consent when soft-deleting a course. Consider whether this information is actually needed in the deletion response, as it might be unnecessary overhead.

✅ Verification successful

Based on the search results, I can now generate a final response. The code shows that the ask_marketing_consent field is part of the course data structure and is returned during course deletion in models/src/courses.rs. The deletion flow is used in the main frontend controller (controllers/main_frontend/courses.rs) where the complete course data is returned to the client, likely for displaying confirmation or maintaining consistency in the frontend state.

Returning marketing consent data during deletion is appropriate

The return of ask_marketing_consent during course deletion is justified as:

  • The deletion endpoint returns the complete course object to the frontend
  • This maintains consistency with other course-related API responses
  • The overhead is minimal as it's just one boolean field in an already-returned object
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check if the returned marketing consent is used in deletion flows
rg -A 5 "DELETE.*courses.*ask_marketing_consent" 

Length of output: 50


Script:

#!/bin/bash
# Let's search for the usage of this specific query file and the deletion flow
rg -l "9fbc4e80f16e7cab3c8928ed195aa6b786df2e76edf819f9decfa7c441591925"

# Also search for course deletion related code
ast-grep --pattern 'async fn delete_course' 

Length of output: 118


Script:

#!/bin/bash
# Let's try a broader search for course deletion related code and the returned fields
rg -A 10 "delete.*course" services/headless-lms/

# Also search for the struct that might be using these returned fields
ast-grep --pattern 'struct Course {
  $$$
}'

Length of output: 29209

services/headless-lms/models/.sqlx/query-fa2a4f76355cf8f829e239ae278ab72efc11d1cf746c7081e5ca1e25eee9f7cd.json (2)

129-130: Review the nullability of join_code

The join_code column is marked as nullable (true) but it's used as a filtering condition in the WHERE clause. This could potentially lead to unexpected behavior if NULL values are allowed. Consider:

  1. Making join_code non-nullable if it's a required field
  2. Adding a NOT NULL check in the WHERE clause if it must remain nullable

Let's check the table definition:

✅ Verification successful

Let me check for any migrations that might have added the join_code column since it's not in the initial table creation.


Let me check the migration that added the join_code column to understand its constraints:


Let me check the actual SQL query that uses this join_code to understand the context better:


The nullability of join_code is intentional and safe

The current design is correct for the following reasons:

  • The join_code column is intentionally nullable as shown in the migration that added it
  • The query is used to find courses by their join code, which is only relevant for courses that have is_joinable_by_code_only set to true
  • NULL values in join_code won't cause issues because:
    1. The WHERE clause join_code = $1 will only match non-NULL values that equal the input parameter
    2. This aligns with the business logic where not all courses need a join code
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check the courses table definition to understand join_code constraints

rg -A 10 "CREATE TABLE.*courses" --type sql

Length of output: 5378


Script:

#!/bin/bash
# Search for migrations that alter the courses table and add join_code
rg -B 2 -A 2 "ALTER TABLE.*courses.*join_code|ADD.*join_code.*courses" --type sql

Length of output: 439


Script:

#!/bin/bash
# Get the up migration that added join_code
cat services/headless-lms/migrations/20240930042528_add-join-code-uses.up.sql

Length of output: 1817


Script:

#!/bin/bash
# Find the SQL query that uses join_code in the WHERE clause
cat "services/headless-lms/models/.sqlx/query-fa2a4f76355cf8f829e239ae278ab72efc11d1cf746c7081e5ca1e25eee9f7cd.json"

Length of output: 3157


3-3: LGTM! Verify the query usage in the codebase.

The SQL query changes look good. The addition of ask_marketing_consent column and the switch to join_code filtering aligns with the PR objective.

Let's verify the query usage:

✅ Verification successful

The SQL query changes look good - no issues found

The switch from slug to join_code filtering in the query is consistent with the codebase. The code shows:

  1. The join_code field is properly used in the courses table and related queries
  2. There are dedicated endpoints and controllers for join code functionality
  3. The change aligns with the existing join code feature implementation
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check if there are any remaining references to the old slug-based filtering
# that might need to be updated to use join_code instead.

rg -l "WHERE.*slug.*=.*courses" --type sql

Length of output: 44


Script:

#!/bin/bash
# Let's try a broader search for SQL queries filtering by slug in courses table
rg -A 5 "FROM courses.*WHERE.*slug" --type sql

# Also search for join_code usage to understand the context better
rg -A 5 "FROM courses.*WHERE.*join_code" --type sql

# Let's also check the actual SQL files in the repository
fd -e sql -x cat {}

Length of output: 144230


Script:

#!/bin/bash
# Let's search for SQL queries that use join_code in courses table
rg -A 3 "FROM courses.*join_code" --type json

# Also search for any references to slug-based filtering in sqlx files
rg -A 3 "FROM courses.*slug" --type json

# Let's also check if there are any other files using join_code
rg -l "join_code" --type rust

Length of output: 894


Script:

#!/bin/bash
# Let's check the actual implementation in courses.rs to understand the context
rg -A 10 "join_code" services/headless-lms/models/src/courses.rs

# Also check the SQL query usage in controllers
rg -A 10 "join_code" services/headless-lms/server/src/controllers/main_frontend/courses.rs

# And check if there are any tests related to this functionality
rg -A 10 "join_code" services/headless-lms/server/tests/study_registry_test.rs

Length of output: 7331

services/headless-lms/models/.sqlx/query-e1a28636054909bb7e9622667a32bdcb2a17e6df2bf5b046fb6ecc92640943f5.json (4)

100-104: LGTM! Column definition is correctly structured

The new column definition follows the correct format and is properly typed as Bool with the correct ordinal position.


3-3: Verify database migration for the new non-nullable column

The query now includes ask_marketing_consent as a non-nullable column. Ensure that a migration exists to:

  1. Add the column with a default value
  2. Populate existing rows
  3. Set the column as NOT NULL
✅ Verification successful

Migration exists and properly handles the non-nullable column

The migration file 20241023104801_add-marketing-consent.up.sql correctly:

  1. Adds the column with a default value: ADD COLUMN ask_marketing_consent BOOLEAN NOT NULL DEFAULT FALSE
  2. Existing rows are handled by the DEFAULT FALSE constraint
  3. Column is set as NOT NULL from the start
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for related migration files
fd -e sql -e SQL . | xargs rg -i "ask_marketing_consent|alter.*courses"

Length of output: 8425


133-133: Verify hash value consistency

The hash value has been updated. Ensure it matches the actual content of the query and its configuration.

✅ Verification successful

Let me try a different approach to verify the hash consistency.


Hash value matches the query content

The hash value in the filename and the "hash" field in the JSON file are identical (e1a28636054909bb7e9622667a32bdcb2a17e6df2bf5b046fb6ecc92640943f5), and they correspond to the SQL query and its configuration defined in the file. This is a valid SQLx query file with consistent hash values.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# List all query hashes in the directory for cross-reference
fd -e json . | xargs rg "hash.*:"

Length of output: 9637


Script:

#!/bin/bash
# Get the content of the query file and its hash
cd services/headless-lms/models/.sqlx/
cat query-e1a28636054909bb7e9622667a32bdcb2a17e6df2bf5b046fb6ecc92640943f5.json

Length of output: 3165


129-130: Verify related insert/update queries

Since this column is non-nullable, ensure that all related insert and update queries for the courses table include this field.

✅ Verification successful

Let me gather more information about the insert/update queries by searching for SQL files and queries.


Let me search specifically for SQL queries related to course inserts and updates.


Let me check the content of these specific sqlx query files that contain INSERT/UPDATE operations for the courses table.


Based on the SQL queries found, I can now provide a final response:

All insert/update queries for the courses table include the ask_marketing_consent field

Looking at the SQL queries:

  • The INSERT queries (ee5080b and cc2fceb) include ask_marketing_consent as a required field
  • The main UPDATE query (f53c554) includes ask_marketing_consent in its SET clause
  • The field is non-nullable (false in the nullable array) and has a default value of FALSE from the migration

The field is properly handled in all relevant queries, so there's no need for additional changes.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for related query files
fd -e json . | xargs rg -l "courses.*insert|courses.*update"

# Search for TypeScript interfaces/types
fd -e ts -e tsx . | xargs rg -A 5 "interface.*Course|type.*Course"

Length of output: 85586


Script:

#!/bin/bash
# Search for SQL files and queries related to courses table
fd -e sql . | xargs rg -l "courses"

# Search for .sqlx files and their content
fd -e sqlx . | xargs rg -A 5 "courses"

# Search for JSON query files related to courses
fd -e json . services/headless-lms/models/.sqlx/ | xargs cat

Length of output: 137666


Script:

#!/bin/bash
# Search for SQL files with insert/update queries for courses table
fd -e sql . services/headless-lms/migrations/ | xargs rg -l "INSERT INTO courses|UPDATE courses"

# Search for SQL files that create/modify the courses table
fd -e sql . services/headless-lms/migrations/ | xargs rg -A 5 "CREATE TABLE.*courses|ALTER TABLE.*courses"

# Search for query files specifically in the sqlx directory
fd -e json . services/headless-lms/models/.sqlx/ | xargs rg -l "INSERT INTO courses|UPDATE courses"

Length of output: 35907


Script:

#!/bin/bash
# Get content of the identified sqlx query files
cat services/headless-lms/models/.sqlx/query-f53c5545a9f632da9076ce08319f10b891d1b22fa2716e958a3de831f22217c8.json
cat services/headless-lms/models/.sqlx/query-f5433a51465291a5dfe643100be0591d5de7b1537e60288fc92ac534d3242f16.json
cat services/headless-lms/models/.sqlx/query-ee5080bbfb823d38cfbd65043c874be0cd06b4310d4a5156fdb306054916c9e5.json
cat services/headless-lms/models/.sqlx/query-cc2fceb2f75d704b551705f15ffcdae210fce2baf7b9d3612f77b6548a6df68c.json
cat services/headless-lms/models/.sqlx/query-9fbc4e80f16e7cab3c8928ed195aa6b786df2e76edf819f9decfa7c441591925.json
cat services/headless-lms/models/.sqlx/query-8202eb97a2b1f388a51758e7fcf8d69b2e68544fae551e3da0a41bc6e9a2de9c.json

Length of output: 12209

services/headless-lms/models/.sqlx/query-748c01cf60043988fba2a02a5d1ea5c3fd35a5adbd69b62e5dd5daa0855596b5.json (3)

129-130: LGTM: Nullability configuration is correct

The nullability configuration properly marks ask_marketing_consent as non-nullable (false), which aligns with the business requirement of always having a defined marketing consent state.


3-3: Verify database migration for non-nullable column

The query includes the new ask_marketing_consent column which is marked as non-nullable. Ensure that a proper migration exists that:

  1. Adds the column with a default value for existing rows
  2. Sets the NOT NULL constraint after populating existing rows
✅ Verification successful

Migration is properly implemented for the non-nullable column

The migration files show that:

  1. The ask_marketing_consent column is added with NOT NULL DEFAULT FALSE, which safely handles existing rows
  2. The migration includes proper up/down scripts with related tables for the marketing consent feature
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for migration files that add ask_marketing_consent column
fd -e sql -e SQL | xargs rg -l "ask_marketing_consent"

# Search for TypeScript/Rust files that might contain migration code
rg -t typescript -t rust "ask_marketing_consent.*migration"

Length of output: 307


Script:

#!/bin/bash
# Let's check the content of the migration files
cat services/headless-lms/migrations/20241023104801_add-marketing-consent.up.sql
cat services/headless-lms/migrations/20241023104801_add-marketing-consent.down.sql

Length of output: 4739


100-104: Consider adding an index for performance optimization

Since this query is filtering on course_language_group_id and the new ask_marketing_consent column might be used in WHERE clauses in other queries, consider adding an index if there are performance requirements.

services/headless-lms/models/.sqlx/query-32396d664f3f617f45c734993b62373ceff25cf391850e08a2697e0dd03f8d94.json (4)

133-133: Hash update looks correct.

The hash has been properly updated to reflect the changes in the query structure.


129-130: Verify default value for non-nullable column.

The ask_marketing_consent column is configured as non-nullable. Ensure there's a default value defined in the migration to handle existing rows.

Let's check the migration:

✅ Verification successful

Default value is properly defined for non-nullable column

The migration file shows that the ask_marketing_consent column is added with NOT NULL DEFAULT FALSE, which properly handles existing rows by setting them to false by default.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Look for the migration that adds this column
fd -e sql | xargs rg -l "ask_marketing_consent.*BOOLEAN"

Length of output: 135


Script:

#!/bin/bash
# Let's check the content of the migration file
cat services/headless-lms/migrations/20241023104801_add-marketing-consent.up.sql

Length of output: 4514


3-3: Consider adding marketing consent validation in the WHERE clause.

The query fetches courses for a user but doesn't validate marketing consent status. Consider adding a condition to filter courses based on consent status if required by business logic.

Let's verify if marketing consent validation is needed:


100-104: Consider adding an index for performance optimization.

Since ask_marketing_consent is a boolean column that might be frequently used in WHERE clauses or joins, consider adding an index to optimize query performance.

Let's check if an index already exists:

services/headless-lms/models/.sqlx/query-668b7cb6022b915f1838997679cc60864b00491bb34f1559377a36cee1a6485c.json (2)

3-3: LGTM! Query structure and new column addition look good.

The SQL query is well-structured with proper JOIN conditions and WHERE clause filters. The ask_marketing_consent column is correctly added to the SELECT statement.


129-130: Verify the non-nullable constraint implementation.

The ask_marketing_consent column is configured as non-nullable (false in nullable array). This design decision requires careful consideration:

  1. Existing rows in the courses table need a default value
  2. New course creation must handle this required field
✅ Verification successful

Based on the shell script results, I can now generate the final response since I have found the relevant migration that adds the ask_marketing_consent column:

The non-nullable constraint implementation is properly handled

The migration file 20241023104801_add-marketing-consent.up.sql shows that the column is added with a NOT NULL constraint and a DEFAULT FALSE value, which ensures:

  1. Existing rows will automatically get the default value of false
  2. New course creation will use false if no value is specified
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for related migration files and default values

# Look for migration files that might handle the new column
fd -e sql . | xargs rg -l "ask_marketing_consent"

# Check for default value in table creation or modification
rg -A 5 "CREATE TABLE.*courses|ALTER TABLE.*courses" 

Length of output: 35027

services/headless-lms/models/.sqlx/query-f53c5545a9f632da9076ce08319f10b891d1b22fa2716e958a3de831f22217c8.json (4)

100-104: LGTM! Column metadata is well-structured.

The metadata for ask_marketing_consent is correctly defined with appropriate type and ordinal position.


108-108: LGTM! Parameters configuration is accurate.

The parameters array correctly includes all types including the new Bool type for ask_marketing_consent.


129-130: Verify migration strategy for non-nullable marketing consent.

Since ask_marketing_consent is set as non-nullable, ensure that:

  1. A database migration exists to add this column with a default value
  2. Existing records are handled appropriately

3-3: Verify parameter ordering in the query.

The query structure looks good with proper parameterization. However, let's verify that the parameter ordering matches the application code to prevent runtime errors.

✅ Verification successful

Parameter ordering in the query is correct and matches the struct definition

Looking at the CourseUpdate struct definition and the SQL query, the parameter ordering in the query matches exactly with the struct fields:

  1. name ($1)
  2. description ($2)
  3. is_draft ($3)
  4. is_test_mode ($4)
  5. can_add_chatbot ($5)
  6. is_unlisted ($6)
  7. is_joinable_by_code_only ($7)
  8. ask_marketing_consent ($8)
  9. id ($9)

The parameters are properly ordered and type-safe through SQLx's compile-time verification system.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify parameter ordering in related Rust code
# Look for the query usage to ensure parameters are passed in correct order

# Search for query usage in Rust files
rg -t rust "UPDATE courses.*SET name.*description.*is_draft.*is_test_mode.*can_add_chatbot.*is_unlisted.*is_joinable_by_code_only.*ask_marketing_consent" -A 5

Length of output: 160


Script:

#!/bin/bash
# Let's try a different approach to find the query usage by searching for the query hash
# The hash is from the filename: f53c5545a9f632da9076ce08319f10b891d1b22fa2716e958a3de831f22217c8

# Search for query hash in Rust files
rg -t rust "f53c5545a9f632da9076ce08319f10b891d1b22fa2716e958a3de831f22217c8" -B 5 -A 10

# Also search for the update_course function which likely uses this query
rg -t rust "update_course.*" -B 5 -A 10

Length of output: 13201


Script:

#!/bin/bash
# Let's look at the CourseUpdate struct to verify parameter ordering
ast-grep --pattern 'struct CourseUpdate {
  $$$
}'

# Also let's look at the actual update query implementation
ast-grep --pattern 'sqlx::query_as!(
  $$$
  UPDATE courses
  $$$
)'

Length of output: 123


Script:

#!/bin/bash
# Let's try a different approach using ripgrep to find the struct and query
# Search for CourseUpdate struct definition
rg -U "struct CourseUpdate \{[\s\S]*?\}" -A 5

# Search for the actual query implementation in update_course function
rg -U "pub async fn update_course.*?sqlx::query_as!\(.*?UPDATE courses.*?\)" -A 20 --multiline

Length of output: 1190

services/headless-lms/models/.sqlx/query-b7647f94557ccce49973abf5a9829a594929d26cdbdebe3ae65a75e63b25d537.json (3)

129-130: LGTM! Nullability configuration is correct

The nullability array has been properly updated to mark ask_marketing_consent as non-nullable (false), which matches the column description.


133-133: LGTM! Hash has been updated

The query hash has been correctly updated to reflect the changes in the query and metadata.


100-104: Verify the non-nullable constraint for ask_marketing_consent

The column description indicates that ask_marketing_consent is a boolean field. Since it's marked as non-nullable in the nullability array, ensure that:

  1. The database table has a DEFAULT value for this column
  2. All existing rows have been migrated with appropriate values
✅ Verification successful

Column ask_marketing_consent is properly constrained with a default value

The migration file shows that the column is added with NOT NULL DEFAULT FALSE constraint, which ensures both existing and new rows will always have a valid boolean value.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify the column constraints and default value

# Check if the column has a DEFAULT constraint
ast-grep --pattern 'ALTER TABLE courses ADD COLUMN ask_marketing_consent $$$'

# Check for any migration files that handle existing rows
fd -e sql -e go | xargs rg -l 'ask_marketing_consent'

Length of output: 532

services/headless-lms/models/.sqlx/query-b57d40a7daf9d2ae609f953f48f756c796ee19b751ffe4740d06600cdae0fd69.json (2)

3-3: Review data privacy implications.

The query retrieves sensitive PII (email, first_name, last_name). Ensure that:

  1. This data is only used for the intended marketing consent synchronization
  2. Appropriate access controls are in place
  3. Data is properly encrypted in transit
✅ Verification successful

Let me gather more information about the data handling and security configurations.


Based on the code analysis, I can now provide a final response about the data privacy implications.

Data handling appears to be properly secured with appropriate controls

The query is part of a legitimate marketing consent synchronization system with the following security measures in place:

  1. Data access is controlled:

    • Data is only accessed through authenticated endpoints
    • Marketing consent is explicitly tracked per user and course
    • Syncing is handled by a dedicated mailchimp_syncer service
  2. Data transmission security:

    • Mailchimp API communication uses HTTPS (api.mailchimp.com)
    • API calls use proper authentication with access tokens
  3. Data minimization:

    • PII is only synced after explicit user consent (tracked in user_marketing_consents table)
    • Syncing is selective and only includes necessary fields
    • Data is only updated when changes occur (tracked via synced_to_mailchimp_at)
  4. Proper audit trail:

    • All consent changes are timestamped (created_at, updated_at)
    • Sync status is tracked (synced_to_mailchimp_at)
    • Soft deletion is supported (deleted_at)
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check for SSL configuration in database connection
rg -A 5 'ssl_mode|sslmode' 
# Check for data encryption patterns
rg -A 5 'encrypt|cipher'

Length of output: 7772


Script:

#!/bin/bash
# Check for database configuration and connection settings
rg -A 10 'DATABASE_URL|db_url|postgres://' --type rust

# Check for mailchimp related code to understand data handling
rg -A 10 'mailchimp|marketing_consent' --type rust

# Look for environment configuration files
fd -e toml -e env -e yaml -x cat {} 

Length of output: 115972


90-107: Verify nullability configuration.

Some nullability configurations need verification:

  1. completed_course is marked as null (line 106) but has a type_info of "Bool"
  2. user_mailchimp_id is nullable but might be required for Mailchimp sync
✅ Verification successful

Nullability configuration is correct

After reviewing the SQL query and table definition:

  1. The completed_course nullability is correctly set to null as it's derived from a LEFT JOIN with course_module_completions and wrapped in a CASE statement that can return NULL.
  2. user_mailchimp_id is correctly marked as nullable in both the table definition and query metadata, which is appropriate since it's populated after syncing with Mailchimp (as indicated by the synced_to_mailchimp_at column).
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for related table definitions
ast-grep --pattern 'CREATE TABLE user_marketing_consents'

Length of output: 59


Script:

#!/bin/bash
# Search for SQL table definitions using ripgrep
rg -i "create table.*user_marketing" -A 10

# Also search for any migrations related to this table
fd -e sql | xargs rg "user_marketing"

# Let's also check the actual query this JSON file corresponds to
cat services/headless-lms/models/.sqlx/query-b57d40a7daf9d2ae609f953f48f756c796ee19b751ffe4740d06600cdae0fd69.sql

Length of output: 4335


Script:

#!/bin/bash
# Let's check the content of the JSON file to see the full query metadata
cat services/headless-lms/models/.sqlx/query-b57d40a7daf9d2ae609f953f48f756c796ee19b751ffe4740d06600cdae0fd69.json

# Also search for any Rust code that might be using this query
rg "b57d40a7daf9d2ae609f953f48f756c796ee19b751ffe4740d06600cdae0fd69" -A 5 -B 5

Length of output: 3204

services/headless-lms/models/.sqlx/query-074ad35b8fa2588b0c36be3f18ec160f8bd276628e02678031d28e6545dfbb22.json (1)

2-3: Verify security measures for PII data handling

This query retrieves sensitive user data (email, first/last name) for Mailchimp integration. Ensure proper security measures are in place.

✅ Verification successful

Let me gather more specific information about the data handling in this context.


Let me check one more aspect regarding the security configuration and data transmission.


Based on the gathered information, I can now provide the final response:

PII data handling appears to be properly secured

The implementation shows several security measures in place:

  • HTTPS/TLS is used for Mailchimp API communication
  • Sensitive configuration is handled through environment variables
  • Database connections use secure configurations
  • Data synchronization with Mailchimp is tracked and versioned
  • Access tokens and secrets are properly managed through environment variables

The query is part of a well-structured system that handles PII data securely through:

  • Controlled database access
  • Proper authentication mechanisms
  • Secure API communication
  • Version tracking of synced data
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check for encryption/security measures in related files

# Look for security-related configurations
echo "Checking for security configurations..."
rg -l "encryption|secure|sensitive" --type yaml --type json

# Check for proper error handling in related Rust files
echo "Checking for error handling in related files..."
ast-grep --pattern 'fn $_(user_marketing_consents|mailchimp)' -l rust

# Look for data protection patterns
echo "Checking for data protection patterns..."
rg -l "gdpr|data.protection|privacy" --type rust --type ts

Length of output: 1144


Script:

#!/bin/bash
# Check for Mailchimp-related implementations and data handling
echo "Checking Mailchimp integration implementations..."
rg -l "mailchimp" --type rust --type ts

# Look for database connection configurations
echo "Checking database connection configurations..."
rg -l "DATABASE_URL|ssl_mode|connection" --type yaml --type toml

# Check for specific user data handling patterns
echo "Looking for user data handling patterns..."
rg "UserMarketingConsents|user_marketing_consents" -A 5 --type rust

# Check for data transmission patterns
echo "Checking data transmission patterns..."
rg "send_to_mailchimp|sync.*mailchimp" -A 5 --type rust

Length of output: 19865


Script:

#!/bin/bash
# Check for TLS/HTTPS configuration and environment variables
echo "Checking for secure connection configurations..."
rg -l "https|tls|ssl" --type rust services/headless-lms/server/src/programs/mailchimp_syncer.rs

# Look for environment variable handling
echo "Checking environment variable handling..."
rg "env::" --type rust services/headless-lms/server/src/

# Check for any encryption or hashing implementations
echo "Checking for encryption patterns..."
rg "encrypt|hash|cipher" --type rust services/headless-lms/

Length of output: 16866

services/headless-lms/models/.sqlx/query-393824929f6e6e064757af094951bc418d2da0f66429c32c48708cd3dccd0f3a.json (4)

100-104: LGTM! Column configuration is correct.

The ask_marketing_consent column is properly defined as a boolean type with the correct ordinal position.


133-133: LGTM! Hash update is consistent.

The hash value has been correctly updated to reflect the changes in the query configuration.


129-130: Verify default value for non-nullable column.

The ask_marketing_consent column is marked as non-nullable (false). This requires either:

  • A default value in the database schema
  • Explicit value provision in all INSERT statements
✅ Verification successful

Default value is properly set in the schema

The migration file shows that ask_marketing_consent column has a default value of FALSE set in the schema:

ADD COLUMN ask_marketing_consent BOOLEAN NOT NULL DEFAULT FALSE;

This ensures the non-nullable constraint is satisfied without requiring explicit values in INSERT statements.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for default value in schema and INSERT statements

# Search for table creation or modification
fd -e sql | xargs rg -A 5 'ask_marketing_consent.*DEFAULT'

# Search for INSERT statements to verify value provision
rg "INSERT\s+INTO\s+courses.*VALUES"

Length of output: 828


3-3: Verify database indexes for optimal query performance.

The query involves multiple joins and subqueries. While the structure is correct, ensure proper indexing for optimal performance.

Consider adding the following indexes if not present:

  • roles(user_id, course_id) for the first subquery
  • roles(user_id, course_instance_id) for the second subquery
✅ Verification successful

Based on the search results, I can now provide a definitive response about the query performance and indexing:

Proper indexes exist for optimal query performance

The query is well-supported by existing indexes:

  • roles(user_id) index exists for the first subquery (created in migration 20240905095603)
  • course_instances has proper indexing through foreign key constraints
  • Soft delete filtering (deleted_at IS NULL) is properly handled in index definitions

The query structure with subqueries and joins is appropriate for the access pattern, and the necessary indexes are in place to support efficient execution.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for indexes on joined tables and relevant columns

# Search for index definitions in migration files
fd -e sql | xargs rg -A 5 'CREATE\s+INDEX.*(?:courses|roles|course_instances)'

# Search for table definitions to verify column constraints
fd -e sql | xargs rg -A 10 'CREATE\s+TABLE.*courses.*ask_marketing_consent'

Length of output: 862


Script:

#!/bin/bash
# Let's search for more specific index definitions and table constraints

# Search for migrations related to ask_marketing_consent
fd -e sql | xargs rg -A 5 'ask_marketing_consent'

# Search for additional indexes on roles and course_instances
fd -e sql | xargs rg -A 3 'CREATE.*INDEX.*(roles|course_instances)'

# Search for foreign key constraints that might have indexes
fd -e sql | xargs rg -A 3 'FOREIGN KEY.*course_instance_id|course_id'

Length of output: 117639

services/headless-lms/models/.sqlx/query-b5da68bc470f028a050fcca492eaf73c18e7f97137ea7f5f4ce1f1322e8c137c.json (3)

100-104: LGTM! Column metadata is correctly defined

The column metadata for ask_marketing_consent is properly configured:

  • Correct ordinal position (19)
  • Appropriate type (Bool)
  • Matches the SELECT query structure

129-130: LGTM! Nullable constraints are properly set

The nullable array has been correctly updated to mark ask_marketing_consent as non-nullable (false), which aligns with the business requirement of always having a defined marketing consent state.


3-3: Verify database schema compatibility

The SQL query has been correctly updated to include the ask_marketing_consent column. However, we should verify that this column exists in the database schema.

✅ Verification successful

Column schema matches the query requirements

The migration file 20241023104801_add-marketing-consent.up.sql confirms that:

  • The ask_marketing_consent column exists in the courses table
  • It's defined as BOOLEAN NOT NULL DEFAULT FALSE, which aligns with the query's metadata
  • The column has proper documentation via a COMMENT statement
  • The migration includes rollback support in the .down.sql file
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify the existence of ask_marketing_consent column in courses table
# and its constraints

# Check if the column exists and its properties
rg -A 5 "ask_marketing_consent" --glob "*.sql" --glob "migrations/*"

Length of output: 1462

services/headless-lms/models/.sqlx/query-ee5080bbfb823d38cfbd65043c874be0cd06b4310d4a5156fdb306054916c9e5.json (4)

3-3: LGTM! Query structure follows best practices.

The query is well-structured with proper parameterization ($1-$14) to prevent SQL injection, and the new ask_marketing_consent column is correctly integrated into both INSERT and RETURNING clauses.


148-148: LGTM! Hash update is present.

The query hash has been correctly updated to reflect the changes.


100-104: Verify the column definition matches the database schema.

The column definition looks correct with proper type (Bool) and ordinal positioning (19).

✅ Verification successful

Column definition matches the database schema

The migration file 20241023104801_add-marketing-consent.up.sql confirms that ask_marketing_consent is added as a BOOLEAN column to the courses table, which matches the type Bool in the query definition. The column is added with a NOT NULL constraint and a default value of FALSE.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify the database schema matches the query definition
# Look for the table definition to confirm the column type and position

# Search for table definition files
fd -e sql | xargs rg -l 'CREATE TABLE.*courses'

# Search for migration files that might have added this column
fd -e sql | xargs rg -l 'ALTER TABLE.*courses.*ask_marketing_consent'

Length of output: 441


Script:

#!/bin/bash
# Let's check the content of the migration file that adds the column
cat services/headless-lms/migrations/20241023104801_add-marketing-consent.down.sql

# And check the up migration as well (by changing .down. to .up.)
cat services/headless-lms/migrations/20241023104801_add-marketing-consent.up.sql

# Let's also check the initial table creation
cat services/headless-lms/migrations/20210216125447_setup_db.up.sql | grep -A 20 'CREATE TABLE.*courses'

Length of output: 5504


121-122: Verify the parameter configuration is consistent.

The parameter and nullable configurations look correct:

  • Parameter added as "Bool" type
  • Nullable set to false, making it a required field

Also applies to: 144-145

✅ Verification successful

Parameter configuration is consistent across the codebase

The verification confirms that the ask_marketing_consent parameter is consistently implemented:

  • Database schema defines it as BOOLEAN NOT NULL DEFAULT FALSE
  • Rust structs and TypeScript bindings correctly use bool/boolean type
  • Frontend forms and components handle it appropriately
  • All test data and seed data consistently use boolean values
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify the usage of ask_marketing_consent in the codebase
# to ensure the non-null constraint is handled properly

# Look for TypeScript/Rust struct definitions
ast-grep --pattern 'struct NewCourse {
  $$$
  ask_marketing_consent: $_
  $$$
}'

# Look for form handling code
rg -A 5 "ask_marketing_consent.*required"

Length of output: 127


Script:

#!/bin/bash
# Let's try a broader search to understand the context and usage

# Search for any mention of ask_marketing_consent
rg "ask_marketing_consent"

# Look for the SQL query definition
fd -e json . --exec cat {} \; | jq -r 'select(.query | contains("ask_marketing_consent"))'

# Check the schema or migration files
fd -e sql . --exec cat {} \; | grep -A 5 -B 5 "ask_marketing_consent"

Length of output: 14236

services/headless-lms/entrypoint/src/main.rs (1)

71-74: LGTM! The new program entry follows the established pattern.

The addition of the "mailchimp-syncer" program is well-structured and consistent with other program entries in the list.

Let's verify the existence of the referenced module and its main function:

✅ Verification successful

The shell script results show that the mailchimp_syncer module exists at services/headless-lms/server/src/programs/mailchimp_syncer.rs and contains a properly implemented main() function with the expected signature pub async fn main() -> anyhow::Result<()>. The module follows the same initialization pattern as other programs in the codebase (environment setup, database connection, etc.) and implements a continuous sync loop with proper error handling and logging.

The program entry is correctly implemented and the referenced module exists with proper implementation.

  • The module exists at the expected location
  • The main function has the correct signature and follows the codebase patterns
  • The implementation includes proper initialization, error handling, and logging
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify the existence of the mailchimp_syncer module and its main function
# Expected: Find the module file and confirm it has a main function

# Check if the module file exists
fd -t f "mailchimp_syncer.rs" services/headless-lms/

# Verify the main function exists in the module
ast-grep --pattern 'pub async fn main() -> anyhow::Result<()> { $$$ }' 

Length of output: 51697

services/headless-lms/migrations/20241023104801_add-marketing-consent.up.sql (2)

1-4: LGTM! Well-structured column addition with appropriate constraints.

The ask_marketing_consent column is properly defined with a NOT NULL constraint and a sensible default value, accompanied by clear documentation.


20-21: Verify trigger function dependency.

The migration uses trigger_set_timestamp() function. Please verify that this function exists in the database before this migration runs.

Also applies to: 48-49

✅ Verification successful

Let me verify the content of the found migration file to ensure it actually creates the trigger function.


The trigger function dependency is properly satisfied

The trigger_set_timestamp() function is created in an earlier migration file 20210216125447_setup_db.up.sql. The function is created with CREATE OR REPLACE FUNCTION and is already being used by multiple other tables in the database. The implementation matches the expected functionality for updating timestamps.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check if the trigger function exists in previous migrations
fd -e sql -e SQL . | xargs grep -l "CREATE.*FUNCTION.*trigger_set_timestamp"

Length of output: 142


Script:

#!/bin/bash
# Check the content of the setup migration
cat services/headless-lms/migrations/20210216125447_setup_db.up.sql

Length of output: 3340

services/main-frontend/src/components/page-specific/manage/courses/id/index/UpdateCourseForm.tsx (3)

39-39: LGTM: State management implementation is correct

The state initialization follows React best practices and properly derives its initial value from the course prop.


56-56: LGTM: Update payload correctly includes marketing consent

The marketing consent field is properly included in the course update payload.


Line range hint 39-151: Verify integration with marketing consent flow

The implementation adds the ability to toggle marketing consent requests for courses, but let's verify the complete user flow.

✅ Verification successful

Marketing consent implementation is properly integrated

The complete marketing consent flow is well-implemented across the codebase:

  • Backend has proper database schema (user_marketing_consents table) and API endpoints for managing consent
  • Frontend has both admin controls (UpdateCourseForm.tsx) and user-facing components (SelectMarketingConsentForm.tsx)
  • Integration with Mailchimp for consent synchronization is implemented
  • Full flow includes:
    1. Course admin can enable/disable asking for marketing consent
    2. Users see consent form when enabled (CourseSettingsModal.tsx)
    3. Consent data is stored and synced with external services

The implementation aligns perfectly with the PR objective of "User can give marketing consent to a specific course".

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check for corresponding user-facing components that handle the actual consent
rg -t typescript -t tsx "marketing.*consent" --glob '!**/UpdateCourseForm.tsx'

# Check for related backend API endpoints
rg -t typescript "updateMarketingConsent|getMarketingConsent"

Length of output: 214


Script:

#!/bin/bash
# Search for marketing consent related code without file type restrictions
rg "marketing.*consent" --glob '!**/UpdateCourseForm.tsx'

# Search for API endpoints and types related to marketing consent
rg "marketing.*consent" -i --type-add 'api:*.{ts,tsx,js,jsx}'

# Look for GraphQL or API schema definitions
fd -e graphql -e gql -X cat

Length of output: 29847

services/course-material/src/components/modals/CourseSettingsModal.tsx (1)

8-12: LGTM: Import statements are well-organized

The new imports for fetchCourseById and SelectMarketingConstentForm are properly organized and necessary for the marketing consent feature.

Also applies to: 15-15

services/headless-lms/models/src/lib.rs (1)

49-49: LGTM! Verify the marketing_consents module implementation.

The addition of the marketing_consents module follows the established pattern where each module corresponds to a database table. The module is correctly marked as public and placed in alphabetical order.

Let's verify the implementation of the marketing consents module:

✅ Verification successful

Marketing consents module implementation is complete and well-structured

The module implementation follows best practices with:

  • Comprehensive data models (UserMarketingConsent, UserMarketingConsentWithDetails, MarketingMailingListAccessToken)
  • Complete set of database operations:
    • upsert_marketing_consent for creating/updating consents
    • fetch_user_marketing_consent for retrieving individual consents
    • fetch_all_unsynced_user_marketing_consents_by_course_language_groups_id for batch processing
    • Mailchimp integration support with sync status tracking
    • Bulk update capabilities via update_bulk_user_consent
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify the marketing_consents module implementation
# Expected: The module should define the necessary types and implement database operations

# Check if the module file exists and contains the expected types
fd -t f "marketing_consents.rs" services/headless-lms/models/src/ -x cat {}

# Look for database operations (insert, update, select) in the module
rg -A 5 "impl.*MarketingConsent" services/headless-lms/models/src/

Length of output: 9925

services/main-frontend/src/components/forms/NewCourseForm.tsx (1)

89-89: Verify the property name and its purpose

The property name ask_marketing_consent suggests it controls whether to ask for marketing consent, but it might be intended to store the actual consent. Consider renaming to better reflect its purpose:

  • If it controls whether to show the consent prompt: should_ask_marketing_consent
  • If it stores the consent: has_marketing_consent

Also applies to: 126-126

✅ Verification successful

Based on the search results, I can now generate the final response as I have sufficient context about the property's purpose from the SQL migration and its usage across the codebase:

Property name correctly reflects its purpose

The property name ask_marketing_consent is correctly named as it indicates whether a course should ask users for marketing consent:

  • SQL migration comment explicitly states: "Whether this course asks the user for marketing consent"
  • The property controls the visibility of SelectMarketingConstentForm in CourseSettingsModal.tsx
  • It's used in conjunction with a separate user_marketing_consents table that stores the actual consent data
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for property usage to understand its purpose
rg -A 5 "ask_marketing_consent" 

Length of output: 24350

services/headless-lms/models/src/test_helper.rs (2)

179-179: LGTM! Default value for marketing consent is appropriate.

The addition of ask_marketing_consent: false as the default value for test data is a safe choice and aligns with the PR's objective of implementing marketing consent features.


179-179: Verify consistency with NewCourse struct definition

Let's ensure this test helper stays in sync with the actual NewCourse struct definition.

✅ Verification successful

Field ask_marketing_consent is correctly defined in both the test helper and NewCourse struct

The verification confirms that the ask_marketing_consent field in the test helper matches the definition in the NewCourse struct. Both are defined as boolean fields.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify that the NewCourse struct definition matches the test helper
# Expected: The NewCourse struct should contain the ask_marketing_consent field

# Search for the NewCourse struct definition
ast-grep --pattern 'struct NewCourse {
  $$$
  ask_marketing_consent: $_,
  $$$
}' services/headless-lms/models/src/courses.rs

Length of output: 128


Script:

#!/bin/bash
# Let's first check if the file exists and see its content
cat services/headless-lms/models/src/courses.rs

# As a backup, let's also search for NewCourse struct definition in the entire codebase
rg "struct NewCourse" -A 20

Length of output: 39369

services/headless-lms/server/src/programs/seed/seed_organizations/uh_mathstat.rs (1)

Line range hint 1-365: Document marketing consent behavior in README

Since this introduces a new feature for course-specific marketing consent, it would be valuable to document:

  • The purpose and behavior of the ask_marketing_consent field
  • How it affects course enrollment
  • Any default values or migration strategies

Consider adding this documentation to the relevant README file or creating a new documentation file specifically for marketing consent management.

Let's check if there's any existing documentation:

services/headless-lms/server/src/ts_binding_generator.rs (1)

173-174: LGTM! Verify the model type exists.

The addition of UserMarketingConsent type export is correctly placed and aligns with the PR's marketing consent feature.

Let's verify that the type exists in the models crate:

✅ Verification successful

Type exists and is correctly referenced

The UserMarketingConsent type is properly defined in services/headless-lms/models/src/marketing_consents.rs and is correctly imported in the server codebase. The TypeScript binding generator's reference to this type is valid.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify that UserMarketingConsent type exists in the models crate
# Expected: Find the type definition in the marketing_consent module

# Search for the type definition
rg -p "struct UserMarketingConsent" services/headless-lms/server/

# Verify the module exists
fd -p "marketing_consent.rs" services/headless-lms/server/

Length of output: 243


Script:

#!/bin/bash
# Let's search for the actual UserMarketingConsent type and its module
rg -p "UserMarketingConsent" services/headless-lms/server/src/models/ -A 3

# Also check if it's defined in a different location
fd marketing_consent services/headless-lms/server/src/models/

# Check if it's imported from another crate
rg "use.*marketing_consent.*UserMarketingConsent" services/headless-lms/server/

Length of output: 708


Script:

#!/bin/bash
# The type seems to be in headless_lms_models crate, let's verify its location
fd -g "*.rs" -p headless_lms_models

# Check the marketing_consents module in headless_lms_models
rg -p "pub struct UserMarketingConsent" --type rust

Length of output: 455

services/headless-lms/server/src/test_helper.rs (1)

192-192: LGTM! The new field is correctly added to the test data setup.

The addition of ask_marketing_consent: false aligns with the PR objective of implementing marketing consent features and provides a reasonable default for test data.

services/headless-lms/models/src/exams.rs (1)

100-101: Verify Course struct compatibility with new fields

The SQL query now includes join_code and ask_marketing_consent fields from the courses table. These fields must be properly defined in the Course struct to avoid runtime errors.

Let's verify the Course struct definition:

✅ Verification successful

Course struct is compatible with the new fields

The Course struct in services/headless-lms/models/src/courses.rs already includes both fields:

  • join_code: Option<String>
  • ask_marketing_consent: bool

The SQL query changes are properly aligned with the struct definition, ensuring type safety and preventing runtime errors.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check if Course struct includes the new fields
# Expected: Course struct should have join_code and ask_marketing_consent fields

rg -A 20 "struct Course \{" services/headless-lms/models/src

Length of output: 1696

services/headless-lms/models/src/chapters.rs (1)

623-623: LGTM! Verify consistent defaults across tests

The addition of ask_marketing_consent: false is a necessary adaptation to support the new required field in NewCourse. The default value is appropriate for this test case as it's not relevant to testing course module constraints.

Let's verify that this default value is consistent across other test cases:

✅ Verification successful

LGTM! Default value is consistently used

The verification shows that ask_marketing_consent: false is consistently used as the default value in test helper macros across both services/headless-lms/models/src/test_helper.rs and services/headless-lms/server/src/test_helper.rs. These test helpers are used to create test data throughout the codebase, ensuring consistent behavior in test cases.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for consistent usage of ask_marketing_consent in test cases
# Expected: All test cases should set ask_marketing_consent to false when creating new courses

# Search for NewCourse struct initialization in test files
rg -U "NewCourse\s*\{(?s:.)*\}" --type rust --glob "**/test*"

Length of output: 26124

services/headless-lms/server/src/programs/seed/seed_organizations/uh_cs.rs (1)

500-500: LGTM! Default opt-out for marketing consent aligns with privacy best practices.

The initialization of ask_marketing_consent to false is appropriate for seed data and follows good privacy practices by making marketing consent opt-in rather than opt-out.

Let's verify if this field is consistently set across all course creation calls:

✅ Verification successful

Based on the search results, I can now generate the final response since I have enough information to verify the concern. The search shows that ask_marketing_consent is consistently set to false across all NewCourse struct initializations in the codebase, including:

  • Test files (study_registry_test.rs)
  • Model implementations (courses.rs, chapters.rs, copying.rs)
  • Seed data files (uh_cs.rs, uh_mathstat.rs, seed_courses.rs)

LGTM! Marketing consent field is consistently set to false across all course initializations

The ask_marketing_consent field is consistently set to false in all course initializations throughout the codebase, following good privacy practices by making marketing consent opt-in by default.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check if ask_marketing_consent is consistently set across all course creations
# Expected: All NewCourse struct initializations should include ask_marketing_consent field

# Search for NewCourse struct initializations
ast-grep --pattern 'NewCourse {
  $$$
}'

# Search for create_new_course function calls
ast-grep --pattern 'create_new_course(
  $$$
)'

Length of output: 30452

services/headless-lms/models/src/courses.rs (5)

51-51: LGTM: Field addition to Course struct

The ask_marketing_consent field is correctly added to the Course struct with appropriate visibility.


84-84: LGTM: Field addition to NewCourse struct

The ask_marketing_consent field is correctly added to the NewCourse struct, maintaining consistency with the Course struct.


582-582: LGTM: Field addition to CourseUpdate struct

The ask_marketing_consent field is correctly added to the CourseUpdate struct, enabling modification of the consent setting.


Line range hint 1-979: Verify database migration for the new field

While the model changes look correct, ensure there's a corresponding database migration for adding the ask_marketing_consent column.

✅ Verification successful

Based on the output, I can see that there are corresponding migration files for the ask_marketing_consent column:

  • services/headless-lms/migrations/20241023104801_add-marketing-consent.up.sql adds the column
  • services/headless-lms/migrations/20241023104801_add-marketing-consent.down.sql removes the column

Let me verify the content of these migration files to ensure they properly handle the column addition:


Database migration for ask_marketing_consent is properly implemented

The migration files show a complete implementation:

  • Up migration adds the ask_marketing_consent boolean column with NOT NULL constraint and FALSE default
  • Creates supporting tables user_marketing_consents and marketing_mailing_list_access_tokens with proper foreign key constraints and triggers
  • Down migration properly removes the column and supporting tables
  • All columns are well documented with comments
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Look for related database migration files
# Expected: Find migration file adding ask_marketing_consent column

fd -e sql -e rs "migration" | xargs rg "ask_marketing_consent"

Length of output: 4869


Script:

#!/bin/bash
# Check the content of the migration files
cat services/headless-lms/migrations/20241023104801_add-marketing-consent.up.sql
cat services/headless-lms/migrations/20241023104801_add-marketing-consent.down.sql

Length of output: 4739


600-602: Verify the SQL update statement parameter ordering

The SQL update statement and its parameters need careful verification to ensure the correct parameter ordering, as misalignment could lead to data corruption.

Also applies to: 631-631

✅ Verification successful

Parameter ordering in SQL update statement is correct and consistent

After examining the code in courses.rs, the parameter ordering in the SQL update statement at lines 600-602 is correct and matches the order of fields being updated. The parameters $1 through $9 are properly aligned with their corresponding fields in both the update statement and the function parameters:

$1 -> name
$2 -> description
$3 -> is_draft
$4 -> is_test_mode
$5 -> can_add_chatbot
$6 -> is_unlisted
$7 -> is_joinable_by_code_only
$8 -> ask_marketing_consent
$9 -> id (WHERE clause)

The parameter ordering is consistent with the CourseUpdate struct fields and the function parameters in update_course. No issues were found with the parameter ordering.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify parameter ordering in SQL update statements
# Look for similar update statements in the codebase for consistency

rg -A 10 "UPDATE courses\nSET" 

Length of output: 224


Script:

#!/bin/bash
# Description: Verify parameter ordering in SQL update statements
# Look for similar update statements in the codebase for consistency

# Using multiline mode and a more flexible pattern
rg -U "UPDATE courses.*SET" -A 10

# Also search for the specific function containing this update
ast-grep --pattern 'impl $_ {
  $$$
  async fn update_course($$$) {
    $$$
  }
  $$$
}'

Length of output: 126


Script:

#!/bin/bash
# Let's try to find the file first and then examine its content
fd "courses.rs" --type f

# Then we'll look at the content of the specific file
cat services/headless-lms/models/src/courses.rs

Length of output: 24180

services/course-material/src/services/backend.ts (1)

52-52: LGTM: Type imports and guards are properly organized.

The additions follow the established pattern of importing types and their corresponding guards.

Also applies to: 90-90

services/headless-lms/models/src/library/copying.rs (2)

1178-1178: LGTM! Default value set appropriately.

The ask_marketing_consent field is correctly initialized to false by default in the test helper, which is a safe default value for new courses.


53-54: LGTM! SQL query updated correctly for marketing consent.

The ask_marketing_consent column has been properly added to both the INSERT and RETURNING clauses, maintaining consistency with the database schema changes.

Also applies to: 75-76, 90-91

✅ Verification successful

Let me search for the migration that adds the ask_marketing_consent column.


Let me check the content of the migration file to verify the column addition.


SQL query changes are correctly aligned with the database schema

The ask_marketing_consent column has been properly added to the courses table through a migration, and the SQL queries in the copying.rs file correctly include this new column. The migration file confirms:

  • Column is added as BOOLEAN NOT NULL DEFAULT FALSE
  • Proper documentation is included via a COMMENT
  • The column is used in the context of marketing consent features
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify the column exists in the courses table
# Test: Check if ask_marketing_consent column exists in courses table
# Expect: Column should be present in the table definition

# Search for table definition
rg -A 20 "CREATE TABLE.*courses"

Length of output: 9685


Script:

#!/bin/bash
# Search for migration files that mention ask_marketing_consent
rg -l "ask_marketing_consent" services/headless-lms/migrations/

Length of output: 219


Script:

#!/bin/bash
# Check the content of the migration file
cat services/headless-lms/migrations/20241023104801_add-marketing-consent.up.sql

Length of output: 4514

shared-module/packages/common/src/bindings.ts (2)

478-478: LGTM: Marketing consent flag consistently added to course interfaces

The ask_marketing_consent boolean flag has been consistently added to all relevant course interfaces (Course, CourseUpdate, NewCourse), maintaining type consistency across the codebase.

Also applies to: 508-508, 525-525


1156-1164: LGTM: Well-structured UserMarketingConsent interface

The new interface includes all necessary fields for tracking user marketing consent:

  • Proper ID and relationship fields (user_id, course_id)
  • Timestamps for auditing (created_at, updated_at)
  • Soft delete support (deleted_at)
  • Boolean consent field
shared-module/packages/common/src/bindings.guard.ts (4)

598-599: LGTM: Added hide_citations field check

The addition of the hide_citations boolean field check in isChatbotConversationInfo is implemented correctly.


1028-1029: LGTM: Added join_code and ask_marketing_consent field checks

The addition of the optional join_code string field and required ask_marketing_consent boolean field checks in isCourse are implemented correctly.


1077-1078: LGTM: Added ask_marketing_consent field check

The addition of the ask_marketing_consent boolean field check in isCourseUpdate is implemented correctly.


2199-2211: LGTM: New UserMarketingConsent type guard

The new isUserMarketingConsent type guard function is implemented correctly with all required field checks:

  • id (string)
  • course_id (string)
  • user_id (string)
  • consent (boolean)
  • created_at (string)
  • updated_at (string)
  • deleted_at (null | string)

Comment on lines 253 to 275
let user_ids: Vec<Uuid> = mailchimp_data
.iter()
.filter_map(|(user_id, _, _, _)| Uuid::parse_str(user_id).ok())
.collect();

let consents: Vec<bool> = mailchimp_data
.iter()
.map(|(_, consent, _, _)| *consent)
.collect();

let timestamps: Vec<DateTime<Utc>> = mailchimp_data
.iter()
.filter_map(|(_, _, ts, _)| {
DateTime::parse_from_rfc3339(ts)
.ok()
.map(|dt| dt.with_timezone(&Utc)) // Convert to Utc
})
.collect();

let course_language_groups_ids: Vec<Uuid> = mailchimp_data
.iter()
.filter_map(|(_, _, _, lang_id)| Uuid::parse_str(lang_id).ok())
.collect();
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix potential misalignment in vectors due to filter_map usage

In update_bulk_user_consent, the use of filter_map for parsing user_ids, timestamps, and course_language_groups_ids can result in vectors of differing lengths. This misalignment can cause incorrect associations between user IDs and their consents in the SQL query, leading to data integrity issues.

To address this, validate all fields together and only collect entries where all fields are successfully parsed:

let valid_data: Vec<(Uuid, bool, DateTime<Utc>, Uuid)> = mailchimp_data
    .iter()
    .filter_map(|(user_id_str, consent, ts_str, lang_id_str)| {
        let user_id = Uuid::parse_str(user_id_str).ok()?;
        let timestamp = DateTime::parse_from_rfc3339(ts_str).ok()?.with_timezone(&Utc);
        let lang_id = Uuid::parse_str(lang_id_str).ok()?;
        Some((user_id, *consent, timestamp, lang_id))
    })
    .collect();

let user_ids: Vec<Uuid> = valid_data.iter().map(|(user_id, _, _, _)| *user_id).collect();
let consents: Vec<bool> = valid_data.iter().map(|(_, consent, _, _)| *consent).collect();
let timestamps: Vec<DateTime<Utc>> = valid_data.iter().map(|(_, _, timestamp, _)| *timestamp).collect();
let course_language_groups_ids: Vec<Uuid> = valid_data.iter().map(|(_, _, _, lang_id)| *lang_id).collect();

Comment on lines 144 to 152
// If the required field is missing, add it
if let Err(e) =
add_field_to_mailchimp(list_id, required_field, server_prefix, access_token).await
{
warn!("Failed to add required field '{}': {}", required_field, e);
} else {
info!("Successfully added required field '{}'", required_field);
}
} else {
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Handle errors when adding required fields to Mailchimp more robustly

If adding a required field fails, the code logs a warning and continues. This could lead to incomplete schema setup and errors during data synchronization. Consider implementing retries or aborting the synchronization process if critical fields cannot be added.

Comment on lines 536 to 537
.bearer_auth(access_token)
.send()
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Inconsistent authentication method with Mailchimp API

The Mailchimp API expects the Authorization header to include apikey followed by the access token. Using .bearer_auth(access_token) may not work as intended. Ensure consistent authentication across all API calls.

Apply this diff to fix the authentication method:

-                .bearer_auth(access_token)
+                .header("Authorization", format!("apikey {}", access_token))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.bearer_auth(access_token)
.send()
.header("Authorization", format!("apikey {}", access_token))
.send()

Comment on lines 530 to 532
"https://{}.api.mailchimp.com/3.0/lists/{}/members?offset={}&count={}&fields=members.merge_fields,members.status,members.last_changed?merge_fields[MARKETING]=disallowed",
server_prefix, list_id, offset, chunk_size
);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix malformed URL in fetch_mailchimp_data_in_chunks

The URL constructed for fetching Mailchimp data has improper placement of the ? in the query parameters. This could lead to incorrect API calls or failures.

Apply this diff to correct the URL construction:

-                "https://{}.api.mailchimp.com/3.0/lists/{}/members?offset={}&count={}&fields=members.merge_fields,members.status,members.last_changed?merge_fields[MARKETING]=disallowed",
+                "https://{}.api.mailchimp.com/3.0/lists/{}/members?offset={}&count={}&fields=members.merge_fields,members.status,members.last_changed&merge_fields[MARKETING]=disallowed",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"https://{}.api.mailchimp.com/3.0/lists/{}/members?offset={}&count={}&fields=members.merge_fields,members.status,members.last_changed?merge_fields[MARKETING]=disallowed",
server_prefix, list_id, offset, chunk_size
);
"https://{}.api.mailchimp.com/3.0/lists/{}/members?offset={}&count={}&fields=members.merge_fields,members.status,members.last_changed&merge_fields[MARKETING]=disallowed",
server_prefix, list_id, offset, chunk_size
);

Comment on lines 606 to 610
eprintln!(
"Error while processing chunk {}/{}: ",
(total_records + BATCH_SIZE - 1) / BATCH_SIZE,
e
);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix formatting in error logging

The error message in eprintln! does not correctly format the chunk number and the error. The variables are not properly referenced in the format string.

Apply this diff to correct the error message:

-                eprintln!(
-                    "Error while processing chunk {}/{}: ",
-                    (total_records + BATCH_SIZE - 1) / BATCH_SIZE,
-                    e
-                );
+                eprintln!(
+                    "Error while processing chunk {}/{}: {}",
+                    (total_records + BATCH_SIZE - 1) / BATCH_SIZE,
+                    total_records / BATCH_SIZE,
+                    e
+                );

Alternatively, use the error! macro for consistent logging:

-                eprintln!(
+                error!(
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
eprintln!(
"Error while processing chunk {}/{}: ",
(total_records + BATCH_SIZE - 1) / BATCH_SIZE,
e
);
eprintln!(
"Error while processing chunk {}/{}: {}",
(total_records + BATCH_SIZE - 1) / BATCH_SIZE,
total_records / BATCH_SIZE,
e
);

@@ -189,6 +189,7 @@ macro_rules! insert_data {
copy_user_permissions: false,
is_joinable_by_code_only: false,
join_code: None,
ask_marketing_consent:false,
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Codebase verification

Based on the search results from our previous queries, we can see that while there is extensive implementation of marketing consent functionality in the codebase (models, controllers, etc.), there are no dedicated test cases covering the marketing consent scenarios. The absence of test coverage for both true and false cases of ask_marketing_consent confirms the original review comment's concern.

Add test coverage for marketing consent scenarios

  • No test cases found that verify the behavior when ask_marketing_consent is set to true or false
  • Implement tests for the following endpoints that handle marketing consent:
    • POST /api/v0/course-material/courses/:course_id/user-marketing-consent
    • GET /api/v0/course-material/courses/:course_id/fetch-user-marketing-consent
🔗 Analysis chain

Verify test coverage for marketing consent scenarios.

Consider adding test cases that specifically verify the behavior when ask_marketing_consent is set to true. This will ensure both consent states are properly tested.

Let's check for existing test coverage:

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Search for test cases related to marketing consent
# Look for test files and functions that might be testing marketing consent
rg -t rust "ask_marketing_consent.*true" services/headless-lms/
rg -t rust "#\[test\].*marketing.*consent" services/headless-lms/

Length of output: 133


Script:

#!/bin/bash
# Let's broaden our search to find all occurrences of marketing consent in test files
# and their surrounding context to better understand the test coverage

# Search for any mention of marketing consent in test files
rg -t rust "marketing.*consent" services/headless-lms/ -B 2 -A 2

# Search for all occurrences of ask_marketing_consent to see its usage
rg -t rust "ask_marketing_consent" services/headless-lms/ -B 2 -A 2

# Look for test helper functions that might be setting up test data
rg -t rust "fn.*test.*" services/headless-lms/server/src/test_helper.rs -B 2 -A 10

Length of output: 46251


Script:

#!/bin/bash
# Let's search for test files and test cases that might be testing marketing consent functionality
rg -t rust "test.*marketing" services/headless-lms/ -B 2 -A 5

# Look for actual test implementations involving marketing consent
ast-grep --pattern 'async fn test_$_($$$) {
  $$$
  ask_marketing_consent
  $$$
}'

# Search for test files that might be using the marketing consent APIs
rg -t rust "upsert_marketing_consent|fetch_user_marketing_consent" services/headless-lms/server/src/tests/ -B 2 -A 5

Length of output: 347

@@ -959,6 +976,7 @@ mod test {
copy_user_permissions: false,
is_joinable_by_code_only: false,
join_code: None,
ask_marketing_consent: false,
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider adding test cases for marketing consent

While the test module includes comprehensive language code validation tests, it lacks specific test cases for the new marketing consent functionality.

Consider adding the following test cases:

  1. Setting marketing consent during course creation
  2. Updating marketing consent
  3. Retrieving courses with different consent settings
#[tokio::test]
async fn test_marketing_consent_operations() {
    insert_data!(:tx, user: _user, :org);
    let course_language_group_id = course_language_groups::insert(
        tx.as_mut(),
        PKeyPolicy::Fixed(Uuid::parse_str("8e40c36c-835b-479c-8f07-863ad408f181").unwrap()),
    )
    .await
    .unwrap();
    
    // Test creation with marketing consent
    let new_course = NewCourse {
        // ... other fields ...
        ask_marketing_consent: true,
    };
    let course_id = courses::insert(
        tx.as_mut(),
        PKeyPolicy::Fixed(Uuid::parse_str("95d8ab4d-073c-4794-b8c5-f683f0856356").unwrap()),
        course_language_group_id,
        &new_course,
    )
    .await
    .unwrap();
    
    // Test retrieval
    let course = courses::get_course(tx.as_mut(), course_id).await.unwrap();
    assert_eq!(course.ask_marketing_consent, true);
    
    // Test update
    let course_update = CourseUpdate {
        // ... other fields ...
        ask_marketing_consent: false,
    };
    let updated_course = courses::update_course(tx.as_mut(), course_id, course_update).await.unwrap();
    assert_eq!(updated_course.ask_marketing_consent, false);
}

Comment on lines 749 to 765
export const updateMarketingConsent = async (
courseId: string,
courseLanguageGroupsId: string,
consent: boolean,
): Promise<string> => {
const res = await courseMaterialClient.post(
`courses/${courseId}/user-marketing-consent`,
{
course_language_groups_id: courseLanguageGroupsId,
consent,
},
{
responseType: "json",
},
)
return validateResponse(res, isString)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix the API endpoint path and consider improving the return type.

  1. The API endpoint path is missing a leading slash, which is inconsistent with other endpoints in this file.
  2. Consider using a more specific return type instead of string to better document the expected response.

Apply this diff to fix the path:

-    `courses/${courseId}/user-marketing-consent`,
+    `/courses/${courseId}/user-marketing-consent`,

Consider creating a specific type for the response:

type MarketingConsentUpdateResponse = {
  status: string;
  // Add other relevant fields based on the actual API response
}

Comment on lines 767 to 772
export const fetchUserMarketingConsent = async (
courseId: string,
): Promise<UserMarketingConsent> => {
const res = await courseMaterialClient.get(`courses/${courseId}/fetch-user-marketing-consent`)
return validateResponse(res, isUserMarketingConsent)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix the API endpoint path.

The API endpoint path is missing a leading slash, which is inconsistent with other endpoints in this file.

Apply this diff to fix the path:

-  const res = await courseMaterialClient.get(`courses/${courseId}/fetch-user-marketing-consent`)
+  const res = await courseMaterialClient.get(`/courses/${courseId}/fetch-user-marketing-consent`)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const fetchUserMarketingConsent = async (
courseId: string,
): Promise<UserMarketingConsent> => {
const res = await courseMaterialClient.get(`courses/${courseId}/fetch-user-marketing-consent`)
return validateResponse(res, isUserMarketingConsent)
}
export const fetchUserMarketingConsent = async (
courseId: string,
): Promise<UserMarketingConsent> => {
const res = await courseMaterialClient.get(`/courses/${courseId}/fetch-user-marketing-consent`)
return validateResponse(res, isUserMarketingConsent)
}

Comment on lines 749 to 772
export const updateMarketingConsent = async (
courseId: string,
courseLanguageGroupsId: string,
consent: boolean,
): Promise<string> => {
const res = await courseMaterialClient.post(
`courses/${courseId}/user-marketing-consent`,
{
course_language_groups_id: courseLanguageGroupsId,
consent,
},
{
responseType: "json",
},
)
return validateResponse(res, isString)
}

export const fetchUserMarketingConsent = async (
courseId: string,
): Promise<UserMarketingConsent> => {
const res = await courseMaterialClient.get(`courses/${courseId}/fetch-user-marketing-consent`)
return validateResponse(res, isUserMarketingConsent)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Codebase verification

API paths need leading slashes for consistency

The integration of marketing consent functions is properly implemented in the SelectMarketingConsentForm component, but the API paths in backend.ts should be updated with leading slashes for consistency:

  • courses/${courseId}/user-marketing-consent/courses/${courseId}/user-marketing-consent
  • courses/${courseId}/fetch-user-marketing-consent/courses/${courseId}/fetch-user-marketing-consent
🔗 Analysis chain

Verify the integration of marketing consent functions.

Let's verify how these functions are being used in the codebase to ensure proper integration.


Let me check the implementation in the form component to ensure proper integration.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for usages of the new marketing consent functions
echo "Searching for updateMarketingConsent usage..."
rg "updateMarketingConsent" --type ts

echo -e "\nSearching for fetchUserMarketingConsent usage..."
rg "fetchUserMarketingConsent" --type ts

# Search for related test files
echo -e "\nSearching for related test files..."
fd -e test.ts -e spec.ts marketing

Length of output: 1245


Script:

#!/bin/bash
# Get the implementation of SelectMarketingConsentForm
echo "Content of SelectMarketingConsentForm.tsx:"
cat services/course-material/src/components/forms/SelectMarketingConsentForm.tsx

# Check for any API path constants or configurations
echo -e "\nSearching for API path configurations:"
rg -g '!*.test.ts' -g '!*.spec.ts' "courses/.*/user-marketing-consent" --type ts

Length of output: 2045

}) => {
const [marketingConsent, setMarketingConsent] = useState(false)

useEffect(() => {
Copy link
Member

Choose a reason for hiding this comment

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

I think this you useQuery hook and after that useEffect if needed

@@ -71,6 +76,12 @@ const CourseSettingsModal: React.FC<React.PropsWithChildren<CourseSettingsModalP
})
sortInstances()

const getCourse = useQuery({
queryKey: ["courses/${course_id}", selectedLangCourseId],
Copy link
Member

Choose a reason for hiding this comment

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

Make the first string to not look like a template string

CREATE TABLE user_marketing_consents (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
course_id UUID NOT NULL REFERENCES courses(id),
course_language_groups_id UUID NOT NULL REFERENCES course_language_groups(id),
Copy link
Member

Choose a reason for hiding this comment

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

course_language_group_id

let result = sqlx::query_as!(
UserMarketingConsent,
"
SELECT id,
Copy link
Member

Choose a reason for hiding this comment

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

Try the select *

Ok(result)
}

// Used to update the synced_to_mailchimp_at to a list of users when they are successfully synced to mailchimp
Copy link
Member

Choose a reason for hiding this comment

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

Make it a documentation comment ///

@@ -138,6 +139,16 @@ const UpdateCourseForm: React.FC<React.PropsWithChildren<UpdateCourseFormProps>>
checked={joinableByCodeOnlyStatus}
/>
</FieldContainer>
<FieldContainer>
<CheckBox
// eslint-disable-next-line i18next/no-literal-string
Copy link
Member

Choose a reason for hiding this comment

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

Needs translating

if let Err(e) =
add_field_to_mailchimp(list_id, required_field, server_prefix, access_token).await
{
warn!("Failed to add required field '{}': {}", required_field, e);
Copy link
Member

Choose a reason for hiding this comment

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

Return an error


loop {
let url = format!(
"https://{}.api.mailchimp.com/3.0/lists/{}/members?offset={}&count={}&fields=members.merge_fields,members.status,members.last_changed?merge_fields[MARKETING]=disallowed",
Copy link
Member

Choose a reason for hiding this comment

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

Change it to filter based on mailchimp's unsubscribe status


const BATCH_SIZE: usize = 1000; // Adjust based on DB capabilities

async fn process_mailchimp_data(
Copy link
Member

Choose a reason for hiding this comment

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

The naming could reflect that we're processing the unsubscribes

//

/// Fetches data from Mailchimp in chunks.
async fn fetch_mailchimp_data_in_chunks(
Copy link
Member

Choose a reason for hiding this comment

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

The naming could reflect that we're processing the unsubscribes

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: 8

🧹 Outside diff range and nitpick comments (9)
services/headless-lms/models/.sqlx/query-d2b6cb427cd241cddbfb9f4fabd743103c99859dc65a648774fdd8c6bb7d89b4.json (1)

3-3: Add RETURNING clause for update verification

The current query doesn't return any information about the affected rows. Consider adding a RETURNING clause to verify the success of the update operation.

- "query": "\nUPDATE user_marketing_consents\nSET user_mailchimp_id = updated_data.user_mailchimp_id\nFROM (\n    SELECT UNNEST($1::uuid[]) AS user_id, UNNEST($2::text[]) AS user_mailchimp_id\n) AS updated_data\nWHERE user_marketing_consents.user_id = updated_data.user_id\n",
+ "query": "\nUPDATE user_marketing_consents\nSET user_mailchimp_id = updated_data.user_mailchimp_id\nFROM (\n    SELECT UNNEST($1::uuid[]) AS user_id, UNNEST($2::text[]) AS user_mailchimp_id\n) AS updated_data\nWHERE user_marketing_consents.user_id = updated_data.user_id\nRETURNING user_id, user_mailchimp_id\n",
services/headless-lms/models/.sqlx/query-5d95679ce5dfd7c1eafc3fb8d38892d0e9407051f27b20ea136276c634a4240b.json (1)

7-10: Add array length validation

The query uses multiple UNNEST operations assuming all input arrays have the same length. This could lead to unexpected results if arrays have different lengths.

Consider adding a check in the application code before executing this query to ensure all input arrays have the same length. Example validation in TypeScript:

if (new Set([
  userIds.length,
  lastUpdated.length,
  courseLanguageGroupIds.length,
  emailSubscriptions.length
]).size !== 1) {
  throw new Error('All input arrays must have the same length');
}
services/headless-lms/models/.sqlx/query-0a881c84d211755c24c03586b0d5be217790808cb8751ae50d121321f0be9856.json (1)

2-3: Consider optimizing the query performance

The query could benefit from the following optimizations:

  1. Add a composite index on (user_id, course_id) to improve lookup performance
  2. Replace SELECT * with specific columns needed by the application

Consider adding the following index:

CREATE INDEX idx_user_marketing_consents_user_course 
ON user_marketing_consents(user_id, course_id);
services/headless-lms/models/.sqlx/query-c876ab77b567d1e1e76589198411794883766ed0ae390953ece769d7ab658dd0.json (1)

36-40: Consider schema improvements

A few suggestions for the schema:

  1. The email_subscription_in_mailchimp field is currently VARCHAR. Consider using an ENUM type to restrict possible values
  2. The completed_course nullability is marked as null in the nullable array (line 112), but it should be true since it's derived from a LEFT JOIN

Here's a suggested schema improvement for the email subscription status:

CREATE TYPE mailchimp_subscription_status AS ENUM ('subscribed', 'unsubscribed', 'pending', 'cleaned');
ALTER TABLE user_marketing_consents 
  ALTER COLUMN email_subscription_in_mailchimp TYPE mailchimp_subscription_status 
  USING email_subscription_in_mailchimp::mailchimp_subscription_status;

Also applies to: 112-112

services/headless-lms/models/.sqlx/query-2835b0aa8afe26fc0aede0c931563d10f9cadc90a2b82a8cd590f722a4a296a2.json (1)

3-3: Consider adding performance optimizations to the query.

While the query structure is correct, consider the following performance improvements:

  1. Add a LIMIT clause to prevent large result sets
  2. Ensure indexes exist on join conditions and filter columns
  3. The OR condition with synced_to_mailchimp_at might prevent index usage

Consider adding these indexes if they don't exist:

CREATE INDEX IF NOT EXISTS idx_user_marketing_consents_course_lang_sync 
ON user_marketing_consents(course_language_group_id, synced_to_mailchimp_at, updated_at);

CREATE INDEX IF NOT EXISTS idx_user_marketing_consents_user_course 
ON user_marketing_consents(user_id, course_id);
services/headless-lms/server/tests/study_registry_test.rs (1)

Line range hint 21-21: Consider adding dedicated tests for marketing consent functionality

This test file focuses on study registry completions. Given that marketing consent is a significant new feature, consider creating a dedicated test file (e.g., marketing_consent_test.rs) to thoroughly test:

  • Marketing consent status retrieval
  • Consent updates
  • Integration with course registration

Would you like me to help create a template for the dedicated marketing consent test file?

services/headless-lms/server/src/programs/mailchimp_syncer.rs (2)

71-71: Consider making sync interval configurable

The sync interval is hardcoded to 10 seconds. Consider making this configurable through environment variables for different deployment environments.

-const SYNC_INTERVAL_SECS: u64 = 10;
+const SYNC_INTERVAL_SECS: u64 = std::env::var("SYNC_INTERVAL_SECS")
+    .unwrap_or_else(|_| "10".to_string())
+    .parse()
+    .unwrap_or(10);

432-528: Improve batch handling in send_users_to_mailchimp

Consider adding batch size limits and implementing more efficient success tracking:

  1. Validate batch size against Mailchimp's limits
  2. Use a HashSet for tracking successful syncs
+    const MAX_BATCH_SIZE: usize = 500; // Mailchimp's recommended batch size
+    if users_details.len() > MAX_BATCH_SIZE {
+        warn!("Batch size {} exceeds recommended maximum of {}", users_details.len(), MAX_BATCH_SIZE);
+    }
+    let mut successfully_synced_user_ids = HashSet::new();
services/headless-lms/server/src/controllers/course_material/courses.rs (1)

943-959: Consider explicit error handling

The current implementation silently converts errors to None using .ok(). Consider logging the error or handling specific error cases explicitly for better debugging and user feedback.

-    let result =
-        models::marketing_consents::fetch_user_marketing_consent(&mut conn, *course_id, &user.id)
-            .await
-            .ok();
+    let result = match models::marketing_consents::fetch_user_marketing_consent(&mut conn, *course_id, &user.id).await {
+        Ok(consent) => Some(consent),
+        Err(e) => {
+            error!("Failed to fetch marketing consent: {}", e);
+            None
+        }
+    };
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 4a7a770 and 0305ed4.

📒 Files selected for processing (14)
  • services/course-material/src/components/forms/SelectMarketingConsentForm.tsx (1 hunks)
  • services/course-material/src/components/modals/CourseSettingsModal.tsx (3 hunks)
  • services/headless-lms/migrations/20241023104801_add-marketing-consent.up.sql (1 hunks)
  • services/headless-lms/models/.sqlx/query-0a881c84d211755c24c03586b0d5be217790808cb8751ae50d121321f0be9856.json (1 hunks)
  • services/headless-lms/models/.sqlx/query-2835b0aa8afe26fc0aede0c931563d10f9cadc90a2b82a8cd590f722a4a296a2.json (1 hunks)
  • services/headless-lms/models/.sqlx/query-398c47b513e0dfaebd258b876a46c9e73cacecfb2b845f0d35895295e47e4d9a.json (1 hunks)
  • services/headless-lms/models/.sqlx/query-5d95679ce5dfd7c1eafc3fb8d38892d0e9407051f27b20ea136276c634a4240b.json (1 hunks)
  • services/headless-lms/models/.sqlx/query-8b8d471ed21eb709b5332c5bb4ab3460a7f3089988d959d05fba690f17d9d456.json (1 hunks)
  • services/headless-lms/models/.sqlx/query-c876ab77b567d1e1e76589198411794883766ed0ae390953ece769d7ab658dd0.json (1 hunks)
  • services/headless-lms/models/.sqlx/query-d2b6cb427cd241cddbfb9f4fabd743103c99859dc65a648774fdd8c6bb7d89b4.json (1 hunks)
  • services/headless-lms/models/src/marketing_consents.rs (1 hunks)
  • services/headless-lms/server/src/controllers/course_material/courses.rs (3 hunks)
  • services/headless-lms/server/src/programs/mailchimp_syncer.rs (1 hunks)
  • services/headless-lms/server/tests/study_registry_test.rs (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • services/headless-lms/models/.sqlx/query-8b8d471ed21eb709b5332c5bb4ab3460a7f3089988d959d05fba690f17d9d456.json
🚧 Files skipped from review as they are similar to previous changes (4)
  • services/course-material/src/components/forms/SelectMarketingConsentForm.tsx
  • services/course-material/src/components/modals/CourseSettingsModal.tsx
  • services/headless-lms/migrations/20241023104801_add-marketing-consent.up.sql
  • services/headless-lms/models/src/marketing_consents.rs
🔇 Additional comments (16)
services/headless-lms/models/.sqlx/query-d2b6cb427cd241cddbfb9f4fabd743103c99859dc65a648774fdd8c6bb7d89b4.json (1)

3-3: Consider validating array lengths and handling non-existent users

The current query assumes that:

  1. The input arrays have matching lengths
  2. All user_ids exist in the user_marketing_consents table

Consider adding validation or handling these edge cases.

Consider implementing one of these solutions:

  1. Add a check constraint or trigger in the database
  2. Validate array lengths in the application code
  3. Use a CTE to filter non-existent users:
WITH updated_data AS (
    SELECT UNNEST($1::uuid[]) AS user_id, 
           UNNEST($2::text[]) AS user_mailchimp_id
),
valid_users AS (
    SELECT ud.* 
    FROM updated_data ud
    JOIN user_marketing_consents umc ON umc.user_id = ud.user_id
)
UPDATE user_marketing_consents
SET user_mailchimp_id = valid_users.user_mailchimp_id
FROM valid_users
WHERE user_marketing_consents.user_id = valid_users.user_id
RETURNING user_id, user_mailchimp_id;
services/headless-lms/models/.sqlx/query-398c47b513e0dfaebd258b876a46c9e73cacecfb2b845f0d35895295e47e4d9a.json (4)

13-14: LGTM: Parameter types are properly defined

The parameter types are correctly specified as UUIDs for IDs and Bool for consent, which ensures type safety at the database level.


15-15: LGTM: Non-null constraint is enforced

The nullable: [false] specification ensures that the returned ID will never be null, which is appropriate for a primary key column.


2-3: Verify foreign key constraints in the database schema

The query assumes the existence of foreign key constraints for user_id, course_id, and course_language_group_id. Let's verify these constraints are properly defined.

✅ Verification successful

Foreign key constraints and uniqueness constraints are properly defined

The database schema has been verified and shows:

  • All necessary foreign key constraints are in place for user_id, course_id, and course_language_group_id
  • A unique constraint course_language_group_specific_marketing_user_uniqueness is defined on (user_id, course_language_group_id), which matches the ON CONFLICT clause in the query
  • The schema design is intentional in allowing multiple consents for the same user+course with different language groups, as indicated by the constraint and table comments
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for foreign key constraints in the database schema

# Search for table definition and constraints
rg -A 10 "CREATE TABLE.*user_marketing_consents" 

# Search for index definitions
rg "CREATE.*INDEX.*user_marketing_consents"

Length of output: 1481


Script:

#!/bin/bash
# Let's check for unique constraints and indexes in the migration file
rg -A 5 "CREATE.*UNIQUE.*user_marketing_consents|ALTER TABLE.*user_marketing_consents.*ADD CONSTRAINT" services/headless-lms/migrations/20241023104801_add-marketing-consent.up.sql

# Also check if there are any other migration files that modify this table
fd ".*\.sql" services/headless-lms/migrations/ --exec rg -l "user_marketing_consents"

Length of output: 423


Script:

#!/bin/bash
# Let's check the complete content of the migration file to see if there are any unique constraints or indexes defined after the table creation
cat services/headless-lms/migrations/20241023104801_add-marketing-consent.up.sql

Length of output: 4693


3-3: Consider adding course_id to the conflict resolution key

The current UPSERT conflict resolution is based on (user_id, course_language_group_id), but not course_id. This could potentially lead to inconsistencies if a course changes language groups. Consider whether course_id should be part of the unique constraint.

services/headless-lms/models/.sqlx/query-5d95679ce5dfd7c1eafc3fb8d38892d0e9407051f27b20ea136276c634a4240b.json (1)

Line range hint 13-15: Potential edge case in timestamp comparison

The condition synced_to_mailchimp_at < updated_data.last_updated might miss updates when timestamps are equal. Consider whether updates should also occur when timestamps are equal.

If updates should occur when timestamps are equal, modify the condition:

-      AND user_marketing_consents.synced_to_mailchimp_at < updated_data.last_updated
+      AND user_marketing_consents.synced_to_mailchimp_at <= updated_data.last_updated
✅ Verification successful

Timestamp comparison is appropriate for this use case

The < comparison in synced_to_mailchimp_at < updated_data.last_updated is correct here. This is a synchronization query where we want to update records only when there are newer changes (strictly newer timestamp) to avoid duplicate or unnecessary updates. Using <= would cause redundant updates when timestamps are equal, which is not desired in a sync scenario.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for other timestamp comparisons in similar contexts
rg -A 2 "synced_to_mailchimp_at\s*[<>=]" --type sql

Length of output: 53


Script:

#!/bin/bash
# Let's try to find the SQL query file and examine the context
fd -e sql -e sqlx . | xargs cat

Length of output: 131135


Script:

#!/bin/bash
# Let's try to find the actual SQL query file content
cat services/headless-lms/models/.sqlx/query-5d95679ce5dfd7c1eafc3fb8d38892d0e9407051f27b20ea136276c634a4240b.json

Length of output: 1145

services/headless-lms/models/.sqlx/query-0a881c84d211755c24c03586b0d5be217790808cb8751ae50d121321f0be9856.json (2)

56-60: Consider adding error handling for Mailchimp sync failures

The synced_to_mailchimp_at field suggests external synchronization. Ensure proper error handling and retry mechanisms are in place for sync failures.


26-40: Verify GDPR compliance for marketing consent data

The table stores sensitive marketing consent data with integration to Mailchimp. Please ensure:

  1. Proper data retention policies are in place
  2. Clear documentation of data processing purposes
  3. Ability to fully delete user data when requested
  4. Audit trail for consent changes
✅ Verification successful

Let me gather more information about data deletion and audit capabilities.


Let me check for soft delete implementation and any GDPR-related documentation.


Based on the analysis of the database schema and migrations, I can now provide a final response:

GDPR compliance measures are in place for marketing consent data

The codebase shows appropriate GDPR compliance measures:

  1. Proper data deletion through soft delete mechanism (deleted_at column)
  2. Clear data purpose documentation in table comments
  3. Consent tracking with timestamps and audit trail:
    • created_at, updated_at track consent changes
    • synced_to_mailchimp_at tracks external system synchronization
  4. User-specific consent management:
    • Unique constraint ensures one consent record per user per course
    • Explicit consent boolean field
    • Mailchimp integration status tracking
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check for data retention policies in documentation
fd -t f "README|CONTRIBUTING|ARCHITECTURE" -X grep -l "retention"

# Look for other queries that might bypass soft deletes
rg -t sql "user_marketing_consents" -A 5

Length of output: 4320


Script:

#!/bin/bash
# Check for deletion-related queries
rg -t sql "DELETE.*user_marketing_consents|deleted_at.*user_marketing_consents" -A 3

# Look for audit/history tracking tables and triggers
rg -t sql "CREATE TABLE.*history|CREATE TRIGGER.*history|audit" -A 5

# Check for related model files that might handle deletion
fd -t f "user.*marketing.*consent" -X cat

Length of output: 916


Script:

#!/bin/bash
# Check for soft delete implementation in the table
rg -t sql "trigger_set_timestamp|deleted_at" -A 5

# Look for GDPR or privacy-related documentation
fd -t f "README|CONTRIBUTING|PRIVACY|GDPR" -X grep -l -i "gdpr\|privacy\|data protection"

# Check for any user deletion handlers
rg "delete.*user" -t rust -t typescript

Length of output: 125758

services/headless-lms/models/.sqlx/query-c876ab77b567d1e1e76589198411794883766ed0ae390953ece769d7ab658dd0.json (1)

71-75: Ensure proper PII data handling

The query fetches sensitive personal information (first_name, last_name, email). Please verify:

  1. The data is encrypted in transit
  2. Access to this query is properly restricted
  3. The data is logged appropriately (avoid logging PII)
  4. The application implements proper data retention policies

Let's check for potential PII exposure in logs:

✅ Verification successful

PII data handling appears to be properly implemented

Based on the comprehensive analysis:

  1. Email logging is minimal and only used for system tests and login state management
  2. The system implements proper security measures:
    • Session management with TTL (100 days with extension policy)
    • Database-level data retention policies (expired records cleanup)
    • Encrypted media handling for content
  3. The logging configuration is properly set up with appropriate log levels (info for application, warn for SQL)
  4. The query in question is part of a secure database access layer with proper parameterization

The PII handling appears to follow security best practices, with no concerning patterns of PII exposure in logs or unsafe data handling.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for potential PII logging

# Search for log statements that might contain email or user details
rg -A 2 "console\.(log|debug|info|warn|error).*email" --type ts --type js
rg -A 2 "println.*email" --type rust

Length of output: 387


Script:

#!/bin/bash
# Let's perform a more comprehensive check for PII handling

# Check for email-related patterns in the codebase
rg -A 3 "email.*log|log.*email" --type-not json

# Look for logging configurations and potential PII exposure
rg -A 3 "logging|logger" --type rust --type ts --type js

# Check for data encryption/security related code
rg -A 3 "encrypt|cipher|crypto" --type rust --type ts --type js

# Look for data retention related code
rg -A 3 "retention|expire|ttl" --type rust --type ts --type js

Length of output: 21999

services/headless-lms/models/.sqlx/query-2835b0aa8afe26fc0aede0c931563d10f9cadc90a2b82a8cd590f722a4a296a2.json (1)

62-75: Review PII handling in the query.

The query retrieves sensitive user information (first_name, last_name, email). Ensure that:

  1. This data is necessary for the marketing consent flow
  2. Appropriate data protection measures are in place
  3. The data usage complies with privacy regulations (GDPR, etc.)
✅ Verification successful

Let me gather more context about the specific query and its usage.


Let me check one more thing to understand if this is specifically for marketing consent purposes.


PII usage is appropriate for marketing consent management

The query is specifically designed for marketing consent management and Mailchimp integration. The PII fields (first_name, last_name, email) are necessary for:

  1. Managing user marketing consents in Mailchimp
  2. Syncing user data with Mailchimp for email subscriptions
  3. Tracking consent status per course language group

The data usage is legitimate as:

  • It's part of an explicit marketing consent flow
  • Users must provide consent (tracked in the consent column)
  • The system maintains proper tracking of sync status with Mailchimp
  • Access is controlled through the marketing consent management system
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check for other instances of PII exposure in queries
rg -l "first_name|last_name|email" --type json .

Length of output: 957


Script:

#!/bin/bash
# Check the actual SQL query and its context
cat services/headless-lms/models/.sqlx/query-2835b0aa8afe26fc0aede0c931563d10f9cadc90a2b82a8cd590f722a4a296a2.json

# Find where this query is being used
rg -l "2835b0aa8afe26fc0aede0c931563d10f9cadc90a2b82a8cd590f722a4a296a2" services/headless-lms/

# Look for any PII handling or data protection measures
rg "privacy|gdpr|mask|encrypt" services/headless-lms/

Length of output: 4138


Script:

#!/bin/bash
# Look for the marketing consent related code
rg -A 5 "user_marketing_consents" services/headless-lms/

Length of output: 10599

services/headless-lms/server/tests/study_registry_test.rs (1)

167-167: Consider adding test cases for marketing consent scenarios

While the default value of false for ask_marketing_consent is reasonable, we should ensure comprehensive test coverage for this new feature.

Let's check if there are any existing test cases for marketing consent:

Consider adding test cases that:

  1. Create courses with ask_marketing_consent: true
  2. Verify the behavior of study registry operations with marketing consent enabled
services/headless-lms/server/src/programs/mailchimp_syncer.rs (3)

670-673: ⚠️ Potential issue

Fix error message formatting

The error message is malformed and missing the error details in the format string.

-            error!(
-                "Error while processing chunk {}/{}: ",
-                (total_records + BATCH_SIZE - 1) / BATCH_SIZE,
-                e
-            );
+            error!(
+                "Error while processing chunk {}/{}: {}",
+                (total_records + BATCH_SIZE - 1) / BATCH_SIZE,
+                total_records / BATCH_SIZE,
+                e
+            );

Likely invalid or redundant comment.


93-107: 🛠️ Refactor suggestion

Improve error handling for schema setup failures

The current implementation returns immediately on the first schema setup failure. Consider implementing a more resilient approach that logs errors but continues with other tokens.

-            return Err(e);
+            warn!("Skipping token due to schema setup failure for list '{}': {:?}",
+                token.mailchimp_mailing_list_id, e);
+            continue;

Likely invalid or redundant comment.


25-66: Verify data privacy compliance for Mailchimp field schema

The schema includes sensitive user information (first name, last name, user ID). Ensure this aligns with data privacy requirements and that users have explicitly consented to sharing this data with Mailchimp.

Consider implementing field-level encryption for sensitive data or using anonymous identifiers instead of direct user IDs.

✅ Verification successful

Based on the search results, I can now provide a definitive response about the data privacy considerations in the Mailchimp integration:

Mailchimp data sharing is properly managed through explicit user consent

The codebase shows a robust consent management system:

  • Marketing consent is explicitly tracked via ask_marketing_consent flag in courses
  • User consent is stored in user_marketing_consents table with a boolean consent field
  • The system includes a proper research consent framework with user opt-in/opt-out options
  • Privacy policy links are provided in the footer (PRIVACY_LINK_EN and PRIVACY_LINK_FI)
  • Users can manage their consent preferences through settings pages
  • The Mailchimp syncer only processes data for users who have explicitly consented
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check for privacy policy or consent documentation
fd -e md -e txt -e rst privacy policy consent

Length of output: 195


Script:

#!/bin/bash
# Let's search for privacy and consent related terms in the codebase
rg -i "privacy|consent|gdpr|data.protection" -A 2 -B 2

# Also check for any marketing consent related code
ast-grep --pattern 'marketing_consent'

# Check if there's any documentation about data handling
fd -e md -e txt -e rst . | xargs rg -i "privacy|consent|gdpr|mailchimp"

# Look for any user consent related functions or methods
ast-grep --pattern 'fn $_(consent|privacy|marketing)'

Length of output: 100471

services/headless-lms/server/src/controllers/course_material/courses.rs (2)

905-910: LGTM: Well-structured payload definition

The struct is properly defined with all necessary derive macros and appropriate field types.


1042-1049: LGTM: Routes properly configured

The new routes are well-structured and follow REST conventions with appropriate HTTP methods.

Comment on lines 2 to 3
"db_name": "PostgreSQL",
"query": "\n UPDATE user_marketing_consents\n SET consent = false,\n email_subscription_in_mailchimp = updated_data.email_subscription_in_mailchimp,\n synced_to_mailchimp_at = updated_data.last_updated\n FROM (\n SELECT UNNEST($1::Uuid[]) AS user_id,\n UNNEST($2::timestamptz[]) AS last_updated,\n UNNEST($3::Uuid[]) AS course_language_group_id,\n UNNEST($4::text[]) AS email_subscription_in_mailchimp\n\n ) AS updated_data\n WHERE user_marketing_consents.user_id = updated_data.user_id\n AND user_marketing_consents.synced_to_mailchimp_at < updated_data.last_updated\n AND user_marketing_consents.course_language_group_id = updated_data.course_language_group_id\n ",
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Hardcoded consent value needs review

The query unconditionally sets consent = false. This seems potentially incorrect as the consent status should typically be a parameter rather than hardcoded.

Consider modifying the query to accept consent as a parameter:

-  "query": "\n    UPDATE user_marketing_consents\n    SET consent = false,
+  "query": "\n    UPDATE user_marketing_consents\n    SET consent = UNNEST($5::boolean[]),

And update the parameters array to include the new boolean array parameter.

Committable suggestion skipped: line range outside the PR's diff.

@@ -0,0 +1,68 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT *\n FROM user_marketing_consents\n WHERE user_id = $1 AND course_id = $2\n ",
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add soft delete filter to prevent exposing deleted records

The query should include a WHERE clause to filter out soft-deleted records.

Update the query to:

- SELECT *
- FROM user_marketing_consents
- WHERE user_id = $1 AND course_id = $2
+ SELECT *
+ FROM user_marketing_consents
+ WHERE user_id = $1 
+ AND course_id = $2
+ AND deleted_at IS NULL
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"query": "\n SELECT *\n FROM user_marketing_consents\n WHERE user_id = $1 AND course_id = $2\n ",
"query": "\n SELECT *\n FROM user_marketing_consents\n WHERE user_id = $1 \n AND course_id = $2\n AND deleted_at IS NULL\n ",

@@ -0,0 +1,116 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n umc.id,\n umc.course_id,\n umc.course_language_group_id,\n umc.user_id,\n umc.user_mailchimp_id,\n umc.consent,\n umc.email_subscription_in_mailchimp,\n umc.created_at,\n umc.updated_at,\n umc.deleted_at,\n umc.synced_to_mailchimp_at,\n u.first_name AS first_name,\n u.last_name AS last_name,\n u.email AS email,\n c.name AS course_name,\n c.language_code AS locale,\n CASE WHEN cmc.passed IS NOT NULL THEN cmc.passed ELSE NULL END AS completed_course\n FROM user_marketing_consents AS umc\n JOIN user_details AS u ON u.user_id = umc.user_id\n JOIN courses AS c ON c.id = umc.course_id\n LEFT JOIN course_module_completions AS cmc\n ON cmc.user_id = umc.user_id AND cmc.course_id = umc.course_id\n WHERE umc.course_language_group_id = $1\n AND (umc.synced_to_mailchimp_at IS NULL OR umc.synced_to_mailchimp_at < u.updated_at)\n ",
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Codebase verification

Add missing indexes to optimize marketing consent query performance

The query performance can be improved by adding the following indexes:

  • user_marketing_consents: Add a composite index on (course_language_group_id, synced_to_mailchimp_at) to optimize the WHERE clause filtering
  • user_marketing_consents: Add an index on (user_id, course_id) to optimize the join with course_module_completions

The existing indexes on user_details (including one on email and search helper indexes) are sufficient for this query's performance.

🔗 Analysis chain

Consider query performance optimizations

The query might benefit from the following optimizations:

  1. Add an index on (course_language_group_id, synced_to_mailchimp_at) in the user_marketing_consents table
  2. Add an index on updated_at in the user_details table
  3. Consider pagination to handle large result sets

Let's verify the existing indexes:


Let me search for more information about the table structure and existing indexes:

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for existing indexes on the relevant columns

# Search for index definitions in migration files
fd -e sql | xargs rg -A 2 'CREATE\s+INDEX.*user_marketing_consents'
fd -e sql | xargs rg -A 2 'CREATE\s+INDEX.*user_details'

Length of output: 495


Script:

#!/bin/bash
# Search for table creation statements to understand the schema and indexes
fd -e sql | xargs rg -A 10 'CREATE TABLE.*user_marketing_consents'
fd -e sql | xargs rg -A 10 'CREATE TABLE.*user_details'

# Search for any additional index creation statements that might have different patterns
fd -e sql | xargs rg -A 2 'INDEX.*user_marketing_consents'
fd -e sql | xargs rg -A 2 'INDEX.*user_details'

Length of output: 3670

Comment on lines 95 to 113
"nullable": [
false,
false,
false,
false,
true,
false,
true,
false,
false,
true,
true,
true,
true,
false,
false,
false,
null
]
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix inconsistency in nullable definitions.

There are potential issues with the nullable definitions:

  1. completed_course is marked as null in the nullable array (line 112) but should be true since it's an optional field from a LEFT JOIN
  2. Some fields are marked as non-nullable (false) but might be null in practice (e.g., user_details fields)

Please verify and update the nullable definitions to match the actual database schema:

    "nullable": [
      false,  // id
      false,  // course_id
      false,  // course_language_group_id
      false,  // user_id
      true,   // user_mailchimp_id
      false,  // consent
      true,   // email_subscription_in_mailchimp
      false,  // created_at
      false,  // updated_at
      true,   // deleted_at
      true,   // synced_to_mailchimp_at
      true,   // first_name
      true,   // last_name
      false,  // email
      false,  // course_name
      false,  // locale
-     null    // completed_course
+     true    // completed_course
    ]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"nullable": [
false,
false,
false,
false,
true,
false,
true,
false,
false,
true,
true,
true,
true,
false,
false,
false,
null
]
"nullable": [
false,
false,
false,
false,
true,
false,
true,
false,
false,
true,
true,
true,
true,
false,
false,
false,
true
]

Comment on lines 530 to 579
/// Updates the email addresses of multiple users in a Mailchimp mailing list.
async fn update_emails_in_mailchimp(
users: Vec<UserMarketingConsentWithDetails>,
list_id: &str,
server_prefix: &str,
access_token: &str,
) -> anyhow::Result<Vec<Uuid>> {
let mut successfully_synced_user_ids = Vec::new();
let mut failed_user_ids = Vec::new();

for user in users {
if let Some(ref user_mailchimp_id) = user.user_mailchimp_id {
let url = format!(
"https://{}.api.mailchimp.com/3.0/lists/{}/members/{}",
server_prefix, list_id, user_mailchimp_id
);

// Prepare the body for the PUT request
let body = serde_json::json!({
"email_address": &user.email,
"status": "subscribed",
});

// Update the email
let update_response = REQWEST_CLIENT
.put(&url)
.header("Authorization", format!("apikey {}", access_token))
.json(&body)
.send()
.await?;

if update_response.status().is_success() {
successfully_synced_user_ids.push(user.user_id);
} else {
failed_user_ids.push(user.user_id);
}
} else {
continue;
}
}

if !failed_user_ids.is_empty() {
eprintln!("Failed to update the following users:");
for user_id in &failed_user_ids {
error!("User ID: {}", user_id);
}
}

Ok(successfully_synced_user_ids)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Optimize email updates and improve error handling

The current implementation processes email updates sequentially. Consider:

  1. Implementing concurrent updates using futures::stream::StreamExt
  2. Preserving detailed error information
+    use futures::stream::{StreamExt, FuturesUnordered};
+    let mut futures = FuturesUnordered::new();
+    for user in users {
+        futures.push(async move {
+            // ... update logic ...
+            Result::<_, anyhow::Error>::Ok((user.user_id, result))
+        });
+    }
+    while let Some(result) = futures.next().await {
+        match result {
+            Ok((user_id, _)) => successfully_synced_user_ids.push(user_id),
+            Err(e) => error!("Failed to update email for user {}: {}", user_id, e),
+        }
+    }

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines 338 to 429
async fn sync_contacts(conn: &mut PgConnection, _config: &SyncerConfig) -> anyhow::Result<()> {
let access_tokens =
headless_lms_models::marketing_consents::fetch_all_marketing_mailing_list_access_tokens(
conn,
)
.await?;

let mut successfully_synced_user_ids = Vec::new();

// Iterate through tokens and fetch and send user details to Mailchimp
for token in access_tokens {
// Fetch all users from Mailchimp and sync possible changes locally
let mailchimp_data = fetch_unsubscribed_users_from_mailchimp_in_chunks(
&token.mailchimp_mailing_list_id,
&token.server_prefix,
&token.access_token,
1000,
)
.await?;

println!(
"Processing Mailchimp data for list: {}",
token.mailchimp_mailing_list_id
);

process_unsubscribed_users_from_mailchimp(conn, mailchimp_data).await?;

// Fetch unsynced emails and update them in Mailchimp
let users_with_unsynced_emails =
headless_lms_models::marketing_consents::fetch_all_unsynced_updated_emails(
conn,
token.course_language_group_id,
)
.await?;

println!(
"Found {} unsynced user email(s) for course language group: {}",
users_with_unsynced_emails.len(),
token.course_language_group_id
);

if !users_with_unsynced_emails.is_empty() {
let email_synced_user_ids = update_emails_in_mailchimp(
users_with_unsynced_emails,
&token.mailchimp_mailing_list_id,
&token.server_prefix,
&token.access_token,
)
.await?;

// Store the successfully synced user IDs from updating emails
successfully_synced_user_ids.extend(email_synced_user_ids);
}

// Fetch unsynced user consents and update them in Mailchimp
let unsynced_users_details =
headless_lms_models::marketing_consents::fetch_all_unsynced_user_marketing_consents_by_course_language_group_id(
conn,
token.course_language_group_id,
)
.await?;

println!(
"Found {} unsynced user consent(s) for course language group: {}",
unsynced_users_details.len(),
token.course_language_group_id
);

if !unsynced_users_details.is_empty() {
let consent_synced_user_ids =
send_users_to_mailchimp(conn, token, unsynced_users_details).await?;

// Store the successfully synced user IDs from syncing user consents
successfully_synced_user_ids.extend(consent_synced_user_ids);
}
}

// If there are any successfully synced users, update the database to mark them as synced
if !successfully_synced_user_ids.is_empty() {
headless_lms_models::marketing_consents::update_synced_to_mailchimp_at_to_all_synced_users(
conn,
&successfully_synced_user_ids,
)
.await?;
println!(
"Successfully updated synced status for {} users.",
successfully_synced_user_ids.len()
);
}

Ok(())
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Refactor sync_contacts for better separation of concerns

The function handles multiple responsibilities: fetching tokens, processing unsubscribed users, updating emails, and syncing consents. Consider breaking it down into smaller, focused functions.

Also, replace println! statements with proper logging:

-        println!(
+        info!(
             "Processing Mailchimp data for list: {}",
             token.mailchimp_mailing_list_id
         );

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines 173 to 181
if let Err(e) =
remove_field_from_mailchimp(list_id, &field.field_id, server_prefix, access_token)
.await
{
warn!("Could not remove field '{}': {}", field.field_name, e);
} else {
info!("Removed field '{}'", field.field_name);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Implement retry mechanism for field operations

Field removal failures are only logged as warnings without retry attempts. Consider implementing a retry mechanism with exponential backoff for transient failures.

-            if let Err(e) =
-                remove_field_from_mailchimp(list_id, &field.field_id, server_prefix, access_token)
-                    .await
-            {
-                warn!("Could not remove field '{}': {}", field.field_name, e);
+            let mut retries = 3;
+            let mut delay = Duration::from_secs(1);
+            while retries > 0 {
+                match remove_field_from_mailchimp(list_id, &field.field_id, server_prefix, access_token).await {
+                    Ok(_) => break,
+                    Err(e) if retries > 1 => {
+                        warn!("Failed to remove field '{}', retrying in {:?}: {}", field.field_name, delay, e);
+                        tokio::time::sleep(delay).await;
+                        delay *= 2;
+                        retries -= 1;
+                    }
+                    Err(e) => {
+                        error!("Failed to remove field '{}' after all retries: {}", field.field_name, e);
+                    }
+                }

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines 916 to 937
async fn update_marketing_consent(
payload: web::Json<UserMarketingConsentPayload>,
pool: web::Data<PgPool>,
course_id: web::Path<Uuid>,
user: AuthUser,
) -> ControllerResult<web::Json<Uuid>> {
let mut conn = pool.acquire().await?;
let user_id = Some(user.id);

let token = authorize_access_to_course_material(&mut conn, user_id, *course_id).await?;

let result = models::marketing_consents::upsert_marketing_consent(
&mut conn,
*course_id,
payload.course_language_groups_id,
&user.id,
payload.consent,
)
.await?;

token.authorized_ok(web::Json(result))
}
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Codebase verification

Based on the database schema findings, I can now generate the final response:

Add foreign key validation for course_language_groups_id

The database schema shows that course_language_groups are linked to courses through a foreign key relationship, but there's no validation in the endpoint to ensure the provided course_language_groups_id belongs to the specified course_id. Add this validation to prevent potential data inconsistencies:

// Validate that course_language_groups_id belongs to the course
let valid = sqlx::query!(
    r#"
    SELECT 1 FROM courses 
    WHERE id = $1 AND course_language_group_id = $2
    "#,
    course_id,
    payload.course_language_groups_id
)
.fetch_optional(&mut conn)
.await?;

if valid.is_none() {
    return Err(Error::BadRequest("Invalid course_language_groups_id for this course".into()));
}
🔗 Analysis chain

Consider adding input validation

While the implementation is solid, consider adding validation to ensure the course_language_groups_id belongs to the specified course_id to prevent potential mismatches.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check if there's any existing validation for course_language_groups_id
# Look for relationships between course_id and course_language_groups_id

ast-grep --pattern 'course_language_groups_id.+course_id' -A 5

Length of output: 62


Script:

#!/bin/bash
# Let's try a broader search to understand the relationship between these entities
# First, let's look for any SQL queries or schema definitions involving these fields
rg -i "course_language_groups" -A 5

# Also search for the model function being called
rg "upsert_marketing_consent" -A 10

Length of output: 21147


Script:

#!/bin/bash
# Let's check if there's any validation in the courses table for course_language_group_id
rg -A 5 "courses.*course_language_group_id.*FOREIGN KEY|REFERENCES" 

# Also check if there's any validation in the code for this relationship
ast-grep --pattern 'fn.*course_id.*course_language_group' -A 10

Length of output: 102498

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: 1

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 0305ed4 and 6b6aa6b.

📒 Files selected for processing (6)
  • services/course-material/src/components/forms/SelectMarketingConsentForm.tsx (1 hunks)
  • services/course-material/src/components/modals/CourseSettingsModal.tsx (3 hunks)
  • services/course-material/src/services/backend.ts (3 hunks)
  • services/main-frontend/src/components/page-specific/manage/courses/id/index/UpdateCourseForm.tsx (3 hunks)
  • shared-module/packages/common/src/locales/en/main-frontend.json (1 hunks)
  • shared-module/packages/common/src/locales/fi/main-frontend.json (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • services/course-material/src/components/forms/SelectMarketingConsentForm.tsx
  • services/course-material/src/components/modals/CourseSettingsModal.tsx
  • services/main-frontend/src/components/page-specific/manage/courses/id/index/UpdateCourseForm.tsx
🔇 Additional comments (4)
services/course-material/src/services/backend.ts (2)

767-772: ⚠️ Potential issue

Fix the API endpoint path.

The API endpoint path is missing a leading slash, which is inconsistent with other endpoints in this file.

Apply this diff to fix the path:

-  const res = await courseMaterialClient.get(`/courses/${courseId}/fetch-user-marketing-consent`)
+  const res = await courseMaterialClient.get(`/courses/${courseId}/fetch-user-marketing-consent`)

Likely invalid or redundant comment.


749-772: Verify the integration of marketing consent functions.

Let's verify how these functions are being used in the codebase to ensure proper integration.

✅ Verification successful

Let me gather more context about the SelectMarketingConsentForm component to understand the complete integration.


Let me check the complete implementation of the SelectMarketingConsentForm component to understand how these functions are integrated.


Let me check for the UserMarketingConsent type definition in the shared module since it's being used in the validation.


Based on the comprehensive analysis of the codebase, I can now provide the final response:

Marketing consent functions are properly integrated ✓

The implementation shows a well-structured integration:

  • Functions are correctly imported and used in SelectMarketingConsentForm component
  • The interface UserMarketingConsent is properly defined in shared module
  • The component implements proper state management and error handling
  • The API endpoints and type validations are consistently implemented
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for usages of the new marketing consent functions
echo "Searching for updateMarketingConsent usage..."
rg "updateMarketingConsent" --type ts

echo -e "\nSearching for fetchUserMarketingConsent usage..."
rg "fetchUserMarketingConsent" --type ts

# Search for related test files
echo -e "\nSearching for related test files..."
fd -e test.ts -e spec.ts marketing

Length of output: 1262


Script:

#!/bin/bash
# Check the implementation of SelectMarketingConsentForm
echo "Checking SelectMarketingConsentForm implementation..."
ast-grep --pattern 'export const SelectMarketingConsentForm = $_'

# Check for any validation types/interfaces
echo -e "\nChecking for marketing consent types..."
rg "isUserMarketingConsent" --type ts -A 5

# Check for any related API endpoints
echo -e "\nChecking for related API endpoints..."
rg "/user-marketing-consent|/fetch-user-marketing-consent" --type ts

Length of output: 18072


Script:

#!/bin/bash
# Get the complete implementation of SelectMarketingConsentForm
echo "Checking SelectMarketingConsentForm complete implementation..."
cat services/course-material/src/components/forms/SelectMarketingConsentForm.tsx

# Check for the UserMarketingConsent type definition
echo -e "\nChecking UserMarketingConsent type..."
rg "type UserMarketingConsent" --type ts -A 5

Length of output: 2289


Script:

#!/bin/bash
# Check for UserMarketingConsent interface/type in shared module
echo "Checking for UserMarketingConsent definition..."
rg -g "*.ts" -g "*.tsx" "interface UserMarketingConsent" -A 5

# Check for any tests related to marketing consent
echo -e "\nChecking for related test files..."
rg -g "*test.ts" -g "*spec.ts" "marketing.*consent" -B 2 -A 2

Length of output: 724

shared-module/packages/common/src/locales/en/main-frontend.json (1)

301-301: LGTM! The new translation entry follows the established conventions.

The added translation key follows the correct format with the "label-" prefix, maintains consistent kebab-case naming, and is properly alphabetically sorted. The English text is clear and follows the same capitalization pattern as other labels.

shared-module/packages/common/src/locales/fi/main-frontend.json (1)

305-305: LGTM! Verify translation key in other language files.

The Finnish translation for the marketing consent label is properly formatted and grammatically correct.

Let's verify that this translation key exists in other language files:

Comment on lines 749 to 765
export const updateMarketingConsent = async (
courseId: string,
courseLanguageGroupsId: string,
consent: boolean,
): Promise<string> => {
const res = await courseMaterialClient.post(
`/courses/${courseId}/user-marketing-consent`,
{
course_language_groups_id: courseLanguageGroupsId,
consent,
},
{
responseType: "json",
},
)
return validateResponse(res, isString)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix the API endpoint path and consider improving the return type.

  1. The API endpoint path is missing a leading slash, which is inconsistent with other endpoints in this file.
  2. Consider using a more specific return type instead of string to better document the expected response.

Apply this diff to fix the path:

-    `/courses/${courseId}/user-marketing-consent`,
+    `/courses/${courseId}/user-marketing-consent`,

Consider creating a specific type for the response:

type MarketingConsentUpdateResponse = {
  status: string;
  // Add other relevant fields based on the actual API response
}

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

🧹 Outside diff range and nitpick comments (1)
services/course-material/src/components/modals/CourseSettingsModal.tsx (1)

199-210: Add null check for courseLanguageGroupsId prop

While the parent condition ensures getCourse.data exists, course_language_group_id might still be undefined. This could cause issues in the SelectMarketingConsentForm component.

Consider adding a null check:

       {getCourse.data?.ask_marketing_consent && (
+        getCourse.data.course_language_group_id && (
         <div
           className={css`
             padding: 1rem 3rem;
           `}
         >
           <SelectMarketingConsentForm
             courseId={selectedLangCourseId}
             courseLanguageGroupsId={getCourse.data.course_language_group_id}
           />
         </div>
+        )
       )}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 6b6aa6b and a4083db.

📒 Files selected for processing (3)
  • services/course-material/src/components/modals/CourseSettingsModal.tsx (3 hunks)
  • services/headless-lms/server/src/programs/mailchimp_syncer.rs (1 hunks)
  • shared-module/packages/common/src/locales/en/cms.json (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • services/headless-lms/server/src/programs/mailchimp_syncer.rs
🔇 Additional comments (5)
services/course-material/src/components/modals/CourseSettingsModal.tsx (3)

8-15: LGTM: Import statements are properly organized

The new imports for fetchCourseById and SelectMarketingConsentForm are correctly added and well-organized.


79-83: 🛠️ Refactor suggestion

Add error and loading states for getCourse query

The query implementation is missing error and loading state handling, which could lead to poor user experience when the course data fetch fails or is in progress.

Consider adding error and loading states before the marketing consent form:

+      {getCourse.isError && (
+        <div className={css`padding: 1rem 3rem;`}>
+          <ErrorBanner variant="readOnly" error={getCourse.error} />
+        </div>
+      )}
+      {getCourse.isPending && (
+        <div className={css`padding: 1rem 3rem;`}>
+          <Spinner variant="medium" />
+        </div>
+      )}
       {getCourse.data?.ask_marketing_consent && (

Likely invalid or redundant comment.


205-208: Verify marketing consent feature integration

Let's verify that all required backend endpoints and database tables are in place for the marketing consent feature.

✅ Verification successful

Let me gather more specific information about the marketing consent implementation.


Marketing consent feature integration is properly implemented

The verification shows a complete implementation of the marketing consent feature:

  • Database schema with user_marketing_consents and marketing_mailing_list_access_tokens tables
  • Backend endpoints for fetching and updating marketing consent
  • Frontend component with proper state management and API integration
  • Mailchimp integration support for email marketing
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify the existence of marketing consent related code

# Check for marketing consent related database migrations
echo "Checking for marketing consent migrations..."
fd -e sql . | xargs grep -l "marketing.*consent"

# Check for marketing consent related backend endpoints
echo "Checking for marketing consent endpoints..."
fd -e ts -e js . | xargs grep -l "marketing.*consent"

# Check for proper typing of marketing consent related props
echo "Checking for marketing consent types..."
ast-grep --pattern 'interface $_ {
  $$$
  ask_marketing_consent?: boolean
  $$$
}'

Length of output: 6722


Script:

#!/bin/bash
# Check the migration files content
echo "=== Marketing consent migration content ==="
cat services/headless-lms/migrations/20241023104801_add-marketing-consent.up.sql

# Check the backend service implementation
echo -e "\n=== Marketing consent backend implementation ==="
rg -A 5 "marketing.*consent" services/course-material/src/services/backend.ts

# Check the SelectMarketingConsentForm component
echo -e "\n=== Marketing consent form component ==="
fd -e tsx -e ts "SelectMarketingConsentForm" . | xargs cat

Length of output: 7372

shared-module/packages/common/src/locales/en/cms.json (2)

134-134: LGTM!

The new "terminology" entry follows the established naming convention and is properly ordered alphabetically.


113-113: Verify potential duplicate entry

Let's verify that this isn't a duplicate entry since the AI summary indicates that "primary-color" was both removed and added.

✅ Verification successful

No duplicate entries found for "primary-color"

The verification shows only one occurrence of "primary-color" at line 113, confirming there are no duplicate entries in the file.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for all occurrences of "primary-color" in the file
rg -n '"primary-color"' 'shared-module/packages/common/src/locales/en/cms.json'

Length of output: 119

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

🧹 Outside diff range and nitpick comments (8)
services/headless-lms/models/src/marketing_consents.rs (1)

106-109: Avoid using SELECT * in SQL queries

In fetch_user_marketing_consent, the SQL query uses SELECT *, which can lead to issues if the table schema changes. Specify the required columns explicitly to improve maintainability and reduce potential bugs.

services/course-material/src/components/forms/SelectMarketingConsentForm.tsx (2)

26-27: Remove unnecessary enabled condition in the fetchInitialMarketingConsent query

The enabled condition checks courseId !== undefined, but since courseId is of type string, it cannot be undefined. Consider removing the enabled condition as it's always true.


41-48: Ensure safe usage of fetchInitialMarketingConsent.data

In the useEffect, you're accessing fetchInitialMarketingConsent.data without checking if it's defined. Although you check fetchInitialMarketingConsent.isSuccess, it's good practice to ensure that data is not undefined to prevent potential runtime errors.

shared-module/packages/common/src/locales/en/course-material.json (1)

126-127: Ensure consistency and completeness in localization entries

The entries for "marketing-consent-checkbox-text" and "marketing-consent-privacy-policy-checkbox-text" are added. Ensure that these keys are also added to all other locale files to maintain consistency across different languages.

services/course-material/src/services/backend.ts (2)

749-767: LGTM! Consider adding request timeout.

The updateMarketingConsent function is well-implemented with proper validation. Consider adding a timeout to the request to handle slow responses.

 const res = await courseMaterialClient.post(
   `/courses/${courseId}/user-marketing-consent`,
   {
     course_language_groups_id: courseLanguageGroupsId,
     email_subscription: emailSubscription,
     marketing_consent: marketingConsent,
   },
   {
     responseType: "json",
+    timeout: 5000, // 5 seconds timeout
   },
 )

769-774: LGTM! Consider adding request timeout.

The fetchUserMarketingConsent function is well-implemented with proper validation. Consider adding a timeout to the request to handle slow responses.

-const res = await courseMaterialClient.get(`/courses/${courseId}/fetch-user-marketing-consent`)
+const res = await courseMaterialClient.get(
+  `/courses/${courseId}/fetch-user-marketing-consent`,
+  {
+    timeout: 5000, // 5 seconds timeout
+  }
+)
services/headless-lms/server/src/programs/mailchimp_syncer.rs (2)

617-620: Fix malformed URL in fetch_unsubscribed_users_from_mailchimp_in_chunks.

The URL constructed for fetching Mailchimp data could be improved for better readability and maintainability.

-        let url = format!(
-            "https://{}.api.mailchimp.com/3.0/lists/{}/members?offset={}&count={}&fields=members.merge_fields,members.status,members.last_changed&status=unsubscribed,non-subscribed",
-            server_prefix, list_id, offset, chunk_size
-        );
+        let base_url = format!("https://{}.api.mailchimp.com/3.0/lists/{}/members", server_prefix, list_id);
+        let params = format!(
+            "offset={}&count={}&fields=members.merge_fields,members.status,members.last_changed&status=unsubscribed,non-subscribed",
+            offset, chunk_size
+        );
+        let url = format!("{}?{}", base_url, params);

343-446: Consider implementing concurrent processing for better performance.

The sync_contacts function processes contacts sequentially. Consider implementing concurrent processing using tokio's async features for better performance.

use futures::stream::{StreamExt, FuturesUnordered};

// Example implementation for concurrent processing
let mut futures = FuturesUnordered::new();
for token in access_tokens {
    futures.push(async move {
        let mailchimp_data = fetch_unsubscribed_users_from_mailchimp_in_chunks(...).await?;
        process_unsubscribed_users_from_mailchimp(conn, mailchimp_data).await?;
        // ... rest of the processing ...
        Ok::<_, anyhow::Error>(())
    });
}

while let Some(result) = futures.next().await {
    if let Err(e) = result {
        error!("Error during synchronization: {:?}", e);
    }
}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between a4083db and 4491f0f.

📒 Files selected for processing (12)
  • services/course-material/src/components/forms/SelectCourseInstanceForm.tsx (4 hunks)
  • services/course-material/src/components/forms/SelectMarketingConsentForm.tsx (1 hunks)
  • services/course-material/src/components/modals/CourseSettingsModal.tsx (5 hunks)
  • services/course-material/src/services/backend.ts (3 hunks)
  • services/headless-lms/models/src/marketing_consents.rs (1 hunks)
  • services/headless-lms/server/src/controllers/course_material/courses.rs (3 hunks)
  • services/headless-lms/server/src/programs/mailchimp_syncer.rs (1 hunks)
  • services/headless-lms/server/src/ts_binding_generator.rs (1 hunks)
  • shared-module/packages/common/src/bindings.guard.ts (6 hunks)
  • shared-module/packages/common/src/bindings.ts (5 hunks)
  • shared-module/packages/common/src/locales/en/course-material.json (1 hunks)
  • shared-module/packages/common/src/locales/uk/course-material.json (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • services/headless-lms/server/src/ts_binding_generator.rs
🔇 Additional comments (13)
services/headless-lms/models/src/marketing_consents.rs (2)

226-229: Fix potential misalignment in vectors due to separate filter_map usage

In update_user_mailchimp_id_at_to_all_synced_users, parsing user_ids separately using filter_map can result in vectors of differing lengths between user_ids and user_mailchimp_ids. This may lead to misaligned data in the SQL update query. Consider validating all fields together and collecting only fully valid entries.


260-277: Fix potential misalignment in vectors due to separate filter_map usage

In update_unsubscribed_users_from_mailchimp_in_bulk, parsing user_ids, timestamps, and course_language_group_ids separately using filter_map can result in vectors of differing lengths. This could cause incorrect associations in the SQL update query. Consider parsing and validating all fields together, collecting only entries where all fields are successfully parsed.

shared-module/packages/common/src/locales/uk/course-material.json (1)

128-129: Verify Ukrainian translations with native speakers.

The marketing consent and privacy policy translations appear well-structured. However, it's recommended to have them reviewed by native Ukrainian speakers to ensure accuracy and cultural appropriateness.

✅ Verification successful

Ukrainian translations appear accurate and consistent with English source

The Ukrainian translations closely match the English source text in both meaning and structure:

  • Marketing consent text properly conveys the same information about updates, contact sharing, and third-party platforms
  • Privacy policy acceptance text accurately translates the terms acceptance statement
  • Text lengths are comparable (EN: 239 chars, UK: 248 chars) which is reasonable given language differences

The translations are only present in course-material.json files for both languages, maintaining consistency across the codebase.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check for consistency across language files
echo "Checking for marketing consent keys in other language files..."
fd -e json -e yaml . shared-module/packages/common/src/locales \
  -x grep -l "marketing-consent"

echo -e "\nComparing length of translations..."
for file in $(fd -e json . shared-module/packages/common/src/locales); do
  echo "=== $file ==="
  jq -r '."marketing-consent-checkbox-text" // empty | length' "$file"
done

Length of output: 10245


Script:

#!/bin/bash
# Let's check the actual content of marketing consent keys in English and Ukrainian files
echo "=== English marketing consent text (course-material.json) ==="
jq -r '."marketing-consent-checkbox-text", ."marketing-consent-privacy-policy-checkbox-text"' shared-module/packages/common/src/locales/en/course-material.json

echo -e "\n=== Ukrainian marketing consent text (course-material.json) ==="
jq -r '."marketing-consent-checkbox-text", ."marketing-consent-privacy-policy-checkbox-text"' shared-module/packages/common/src/locales/uk/course-material.json

# Check if these keys exist in main-frontend.json files as well
echo -e "\n=== English marketing consent text (main-frontend.json) ==="
jq -r '."marketing-consent-checkbox-text", ."marketing-consent-privacy-policy-checkbox-text"' shared-module/packages/common/src/locales/en/main-frontend.json

echo -e "\n=== Ukrainian marketing consent text (main-frontend.json) ==="
jq -r '."marketing-consent-checkbox-text", ."marketing-consent-privacy-policy-checkbox-text"' shared-module/packages/common/src/locales/uk/main-frontend.json

Length of output: 1813

services/headless-lms/server/src/programs/mailchimp_syncer.rs (3)

26-72: LGTM! Well-structured field definitions.

The required fields are clearly defined with appropriate default values. The schema is comprehensive and includes all necessary user data fields.


694-698: ⚠️ Potential issue

Fix formatting in error logging.

The error message in eprintln! does not correctly format the chunk number and the error.

-            error!(
-                "Error while processing chunk {}/{}: ",
-                (total_records + BATCH_SIZE - 1) / BATCH_SIZE,
-                e
-            );
+            error!(
+                "Error while processing chunk {}/{}: {}",
+                (total_records + BATCH_SIZE - 1) / BATCH_SIZE,
+                total_records / BATCH_SIZE,
+                e
+            );

Likely invalid or redundant comment.


179-187: 🛠️ Refactor suggestion

Implement retry mechanism for field operations.

The current implementation only logs warnings for field removal failures. Consider implementing a retry mechanism with exponential backoff for transient failures.

-            if let Err(e) =
-                remove_field_from_mailchimp(list_id, &field.field_id, server_prefix, access_token)
-                    .await
-            {
-                warn!("Could not remove field '{}': {}", field.field_name, e);
+            let mut retries = 3;
+            let mut delay = Duration::from_secs(1);
+            while retries > 0 {
+                match remove_field_from_mailchimp(list_id, &field.field_id, server_prefix, access_token).await {
+                    Ok(_) => break,
+                    Err(e) if retries > 1 => {
+                        warn!("Failed to remove field '{}', retrying in {:?}: {}", field.field_name, delay, e);
+                        tokio::time::sleep(delay).await;
+                        delay *= 2;
+                        retries -= 1;
+                    }
+                    Err(e) => {
+                        error!("Failed to remove field '{}' after all retries: {}", field.field_name, e);
+                    }
+                }

Likely invalid or redundant comment.

services/headless-lms/server/src/controllers/course_material/courses.rs (3)

905-911: LGTM! Well-structured payload for marketing consent

The struct has appropriate field types and clear naming that matches its purpose.


947-967: LGTM! Well-implemented consent fetching

The function properly handles authorization and optional results.


1050-1057: LGTM! Routes follow API conventions

The routes are properly configured with appropriate HTTP methods and consistent URL patterns.

shared-module/packages/common/src/bindings.ts (2)

1156-1168: LGTM! Well-structured interface for marketing consent

The interface has comprehensive fields with proper typing, including audit fields and Mailchimp integration fields.


478-478: LGTM! Consistent interface updates

The ask_marketing_consent field is consistently added to all relevant interfaces with proper typing.

Also applies to: 508-508, 525-525

shared-module/packages/common/src/bindings.guard.ts (2)

2199-2217: LGTM! Comprehensive type guard implementation

The type guard properly validates all fields with appropriate type checks and null handling.


1028-1029: LGTM! Consistent type guard updates

The type guards are consistently updated to validate the new ask_marketing_consent field.

Also applies to: 1077-1078, 1098-1099

Comment on lines +78 to +82
const getCourse = useQuery({
queryKey: ["courses", selectedLangCourseId],
queryFn: () => fetchCourseById(selectedLangCourseId as NonNullable<string>),
enabled: selectedLangCourseId !== null,
})
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Correct the 'enabled' condition in the getCourse query

The enabled condition checks selectedLangCourseId !== null, but since selectedLangCourseId is of type string, it cannot be null. Consider removing the enabled condition or updating it to check for undefined if selectedLangCourseId can be undefined.

Comment on lines +126 to +133
if (getCourse.isSuccess) {
await updateMarketingConsent(
getCourse.data.id,
getCourse.data.course_language_group_id,
isEmailSubscriptionConsentChecked,
isMarketingConsentChecked,
)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add error handling for getCourse query failure

Currently, updateMarketingConsent is called only if getCourse.isSuccess is true, but there's no error handling for when getCourse fails. Consider handling the failure case to inform the user or retry the fetch operation.

Comment on lines +239 to +243
disabled={
!selectedInstanceId ||
additionalQuestionsQuery.isPending ||
(getCourse.data?.ask_marketing_consent && !isEmailSubscriptionConsentChecked)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Ensure both consents are required before enabling the 'Continue' button

The disabled condition for the 'Continue' button only checks isEmailSubscriptionConsentChecked. If both marketing consent and email subscription consent are required, consider updating the condition to also check isMarketingConsentChecked to prevent users from proceeding without providing both consents.

Comment on lines +80 to +85
const askMarketingConsent = useQuery({
queryKey: ["courses", selectedLangCourseId],
queryFn: () => fetchCourseById(selectedLangCourseId as NonNullable<string>),
enabled: selectedLangCourseId !== null,
}).data?.ask_marketing_consent

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Handle loading and error states for askMarketingConsent

The askMarketingConsent value is directly accessed without handling loading or error states from the getCourse query. This might lead to unexpected behavior if the data is not yet available or if an error occurs. Consider checking getCourse.isLoading and getCourse.isError before using getCourse.data.

Comment on lines +109 to +114
setOpen(
(signedIn && shouldChooseInstance) ||
(signedIn && manualOpen) ||
(signedIn && askMarketingConsent === true && checkUserMarketingConsent === "unsubscribed"),
)
}, [loginState, pageState, manualOpen, askMarketingConsent, checkUserMarketingConsent])
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Improve the condition for opening the modal

The condition for setting open combines several states but may not account for loading or error states of the queries. Consider adding checks for getCourse.isSuccess and checkUserMarketingConsent to ensure the modal only opens when the data is available and reliable.

Comment on lines +913 to +945
/**
POST `/api/v0/course-material/courses/:course_id/user-marketing-consent` - Adds or updates user's marketing consent for a specific course.
*/
#[instrument(skip(pool, payload))]
async fn update_marketing_consent(
payload: web::Json<UserMarketingConsentPayload>,
pool: web::Data<PgPool>,
course_id: web::Path<Uuid>,
user: AuthUser,
) -> ControllerResult<web::Json<Uuid>> {
let mut conn = pool.acquire().await?;
let user_id = Some(user.id);

let token = authorize_access_to_course_material(&mut conn, user_id, *course_id).await?;

let email_subscription = if payload.email_subscription {
"subscribed"
} else {
"unsubscribed"
};

let result = models::marketing_consents::upsert_marketing_consent(
&mut conn,
*course_id,
payload.course_language_groups_id,
&user.id,
email_subscription,
payload.marketing_consent,
)
.await?;

token.authorized_ok(web::Json(result))
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add validation for course_language_groups_id

The function should validate that the provided course_language_groups_id belongs to the specified course to prevent potential data inconsistencies.

// Add this validation before line 934
let valid = sqlx::query!(
    r#"
    SELECT 1 FROM courses 
    WHERE id = $1 AND course_language_group_id = $2
    "#,
    course_id,
    payload.course_language_groups_id
)
.fetch_optional(&mut conn)
.await?;

if valid.is_none() {
    return Err(Error::BadRequest("Invalid course_language_groups_id for this course".into()));
}

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: 4

🧹 Outside diff range and nitpick comments (5)
services/course-material/src/components/ContentRenderer/core/formatting/TableBlock.tsx (1)

86-86: LGTM! Consider adding parentheses for better readability.

The content rendering logic is correct and maintains the same behavior. While the parentheses removal doesn't affect functionality, adding them back could improve readability as suggested by the linter:

-cell.content !== "" ? cell.content ?? "&#xFEFF;" : "&#xFEFF;"
+(cell.content !== "" ? cell.content ?? "&#xFEFF;" : "&#xFEFF;")

Also applies to: 108-108, 130-130

🧰 Tools
🪛 eslint

[error] 86-86: Replace cell.content·??·"&#xFEFF;" with (cell.content·??·"&#xFEFF;")

(prettier/prettier)

services/headless-lms/models/.sqlx/query-bbec51d086e46c8b3e4e547fc88ad8ed5ddd43db737ec99e48391668646d49f9.json (1)

Consider adding an index to optimize query performance

Based on the migration file analysis, while there is a unique constraint on (user_id, course_language_group_id), there's no index that fully supports the WHERE clause conditions in the batch update query. Adding the following index would improve query performance:

CREATE INDEX idx_marketing_consents_sync 
ON user_marketing_consents(user_id, consent, synced_to_mailchimp_at, course_language_group_id);
  • File: services/headless-lms/migrations/ - Create a new migration to add the suggested index

The other concerns about transaction isolation and batch size should be handled at the application level, as they are not directly related to the SQL query structure.

🔗 Analysis chain

Line range hint 3-16: Consider performance and consistency implications

The batch update query has several potential issues:

  1. Large arrays in UNNEST could impact performance
  2. No explicit transaction isolation level
  3. Possible race conditions in timestamp comparisons

Recommendations:

  1. Add batch size limits in the application code
  2. Consider using SERIALIZABLE isolation level
  3. Add appropriate indexes:
CREATE INDEX idx_marketing_consents_sync 
ON user_marketing_consents(user_id, consent, synced_to_mailchimp_at, course_language_group_id);
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check for existing indexes on user_marketing_consents table
rg -A 10 "CREATE.*INDEX.*ON.*user_marketing_consents"

Length of output: 55


Script:

#!/bin/bash
# Let's first check for migration files that might contain index definitions
fd -e sql | xargs rg -l "user_marketing_consents"

# Then check for table creation to understand the schema
fd -e sql | xargs rg -A 10 "CREATE TABLE.*user_marketing_consents"

Length of output: 1659


Script:

#!/bin/bash
# Let's check the complete migration file for any indexes
cat services/headless-lms/migrations/20241023104801_add-marketing-consent.up.sql

# Also check if there are any subsequent migrations that might have added indexes
fd -e sql -e up.sql -d services/headless-lms/migrations | sort | grep -A 999 20241023104801

Length of output: 4935

services/headless-lms/models/.sqlx/query-f52994d7e2e279a1a5f31d4486f6911035d1d91ddcb9d346df8aa848c0b29ec7.json (1)

1-122: Consider adding indexes for performance optimization

The query involves multiple LEFT JOINs and complex conditions. Consider adding indexes on frequently joined columns to improve query performance:

  • user_marketing_consents(course_language_group_id)
  • course_module_completions(user_id, course_id)
  • course_specific_consent_form_answers(course_id, user_id)
  • user_research_consents(user_id)
services/headless-lms/server/src/controllers/course_material/courses.rs (2)

906-912: Consider adding validation for UserMarketingConsentPayload

The struct should include validation to ensure the fields meet any business requirements (e.g., email format validation if needed).

#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Validate)]
#[cfg_attr(feature = "ts_rs", derive(TS))]
pub struct UserMarketingConsentPayload {
    #[validate(custom = "validate_course_language_group")]
    pub course_language_groups_id: Uuid,
    pub email_subscription: bool,
    pub marketing_consent: bool,
}

929-934: Consider using an enum for email subscription status

Instead of using string literals, consider using an enum for better type safety and maintainability.

#[derive(Debug, Serialize, Deserialize)]
pub enum EmailSubscriptionStatus {
    Subscribed,
    Unsubscribed,
}

let email_subscription = if payload.email_subscription {
    EmailSubscriptionStatus::Subscribed
} else {
    EmailSubscriptionStatus::Unsubscribed
};
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 4491f0f and 3a84785.

📒 Files selected for processing (14)
  • services/course-material/src/components/ContentRenderer/core/formatting/TableBlock.tsx (3 hunks)
  • services/course-material/src/services/backend.ts (3 hunks)
  • services/headless-lms/models/.sqlx/query-3036b60a162754931a2128c92caf88b9e98e50393c53dc95ac919e7065eb4a65.json (1 hunks)
  • services/headless-lms/models/.sqlx/query-6f9c9d6f61094b88673acaa9d820bc13fd0bbc0f7d52c0e3714ba77e0c81280e.json (1 hunks)
  • services/headless-lms/models/.sqlx/query-bbec51d086e46c8b3e4e547fc88ad8ed5ddd43db737ec99e48391668646d49f9.json (1 hunks)
  • services/headless-lms/models/.sqlx/query-f52994d7e2e279a1a5f31d4486f6911035d1d91ddcb9d346df8aa848c0b29ec7.json (1 hunks)
  • services/headless-lms/models/src/lib.rs (1 hunks)
  • services/headless-lms/server/src/controllers/course_material/courses.rs (3 hunks)
  • services/headless-lms/server/src/ts_binding_generator.rs (1 hunks)
  • shared-module/packages/common/src/bindings.guard.ts (5 hunks)
  • shared-module/packages/common/src/bindings.ts (4 hunks)
  • shared-module/packages/common/src/locales/en/main-frontend.json (1 hunks)
  • shared-module/packages/common/src/locales/fi/main-frontend.json (1 hunks)
  • shared-module/packages/common/src/locales/uk/course-material.json (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • shared-module/packages/common/src/locales/en/main-frontend.json
  • services/headless-lms/models/src/lib.rs
  • shared-module/packages/common/src/locales/fi/main-frontend.json
  • services/headless-lms/server/src/ts_binding_generator.rs
  • shared-module/packages/common/src/locales/uk/course-material.json
🧰 Additional context used
🪛 eslint
services/course-material/src/components/ContentRenderer/core/formatting/TableBlock.tsx

[error] 86-86: Replace cell.content·??·"&#xFEFF;" with (cell.content·??·"&#xFEFF;")

(prettier/prettier)


[error] 108-108: Replace cell.content·??·"&#xFEFF;" with (cell.content·??·"&#xFEFF;")

(prettier/prettier)


[error] 130-130: Replace cell.content·??·"&#xFEFF;" with (cell.content·??·"&#xFEFF;")

(prettier/prettier)

🔇 Additional comments (14)
services/course-material/src/components/ContentRenderer/core/formatting/TableBlock.tsx (2)

Line range hint 1-152: Clarification needed: Marketing consent integration

While the AI summary suggests this component's involvement in marketing consent functionality, this appears to be a generic table renderer with no direct handling of marketing consent data. Please clarify if this component needs to be modified to support marketing consent features.

🧰 Tools
🪛 Biome (1.9.4)

[error] 84-84: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

🪛 eslint

[error] 86-86: Replace cell.content·??·"&#xFEFF;" with (cell.content·??·"&#xFEFF;")

(prettier/prettier)


86-89: Verify XSS protection in parseText utility

The component uses dangerouslySetInnerHTML with parseText sanitization. While this appears safe, we should verify that parseText properly sanitizes HTML content.

Also applies to: 108-111, 130-133

✅ Verification successful

XSS protection is properly implemented

The code uses DOMPurify for HTML sanitization, which is a robust and well-maintained XSS sanitization library. The parseText utility properly sanitizes content by:

  1. Processing LaTeX, citations, and glossary terms
  2. Always applying DOMPurify sanitization as the final step before returning content
  3. Using trusted types configuration for additional security
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for parseText implementation to verify sanitization
ast-grep --pattern 'function parseText($_) {
  $$$
}'

Length of output: 55


Script:

#!/bin/bash
# Let's try different approaches to find the parseText implementation
# First, let's search for parseText using ripgrep to see its usage and definition
rg -l "parseText" 

# Then let's get the actual content of files containing parseText
rg "parseText" -A 5 -B 5

# Also search for any sanitization related imports or utilities
rg -l "sanitize|xss|DOMPurify|escape"

Length of output: 48106


Script:

#!/bin/bash
# Let's check the sanitization implementation in the course material
cat services/course-material/src/utils/sanitizeCourseMaterialHtml.ts

# Let's also check the parseText implementation
cat services/course-material/src/components/ContentRenderer/util/textParsing.tsx

Length of output: 3394

🧰 Tools
🪛 eslint

[error] 86-86: Replace cell.content·??·"&#xFEFF;" with (cell.content·??·"&#xFEFF;")

(prettier/prettier)

services/course-material/src/services/backend.ts (2)

751-769: Fix the API endpoint path and improve type safety

  1. The API endpoint path is missing a leading slash
  2. Consider using a more specific type for the response instead of string

771-776: Fix the API endpoint path

The API endpoint path is missing a leading slash

services/headless-lms/server/src/controllers/course_material/courses.rs (2)

935-944: Add validation for course_language_groups_id

The function should validate that the provided course_language_groups_id belongs to the specified course to prevent potential data inconsistencies.


1072-1079: LGTM: Route configuration is properly implemented

The new routes are correctly added to the configuration with proper HTTP methods and handlers.

shared-module/packages/common/src/bindings.ts (4)

478-478: LGTM! New marketing consent field added to Course interface

The boolean field ask_marketing_consent is properly typed and follows naming conventions.


508-508: LGTM! Marketing consent field added to CourseUpdate interface

The boolean field matches the Course interface definition.


525-525: LGTM! Marketing consent field added to NewCourse interface

The boolean field maintains consistency with Course and CourseUpdate interfaces.


1156-1168: LGTM! Well-structured UserMarketingConsent interface

The interface is well-designed with:

  • Proper tracking fields (created_at, updated_at, deleted_at)
  • Mailchimp integration fields
  • Appropriate use of nullable types
shared-module/packages/common/src/bindings.guard.ts (4)

1030-1031: LGTM! Course type guard updated for marketing consent

The type guard properly validates the new boolean field.


1079-1080: LGTM! CourseUpdate type guard updated for marketing consent

The type guard maintains consistency with Course type guard.


1100-1101: LGTM! NewCourse type guard updated for marketing consent

The type guard maintains consistency with related type guards.


2201-2219: LGTM! Comprehensive type guard for UserMarketingConsent

The type guard properly validates:

  • All required fields with correct types
  • Nullable fields with appropriate checks
  • Follows established type guard patterns in the codebase

Comment on lines +2 to +3
"db_name": "PostgreSQL",
"query": "\n INSERT INTO user_marketing_consents (user_id, course_id, course_language_group_id, consent, email_subscription_in_mailchimp)\n VALUES ($1, $2, $3, $4, $5)\n ON CONFLICT (user_id, course_language_group_id)\n DO UPDATE\n SET\n consent = $4,\n email_subscription_in_mailchimp = $5\n RETURNING id\n ",
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add timestamps for better tracking and auditing

The query should include created_at and updated_at timestamps to track when consents are created or modified. This is especially important for audit trails and synchronization with external services like Mailchimp.

  "query": "\n      INSERT INTO user_marketing_consents (user_id, course_id, course_language_group_id, consent, email_subscription_in_mailchimp)\n      VALUES ($1, $2, $3, $4, $5)\n      ON CONFLICT (user_id, course_language_group_id)\n      DO UPDATE\n      SET\n        consent = $4,
-       email_subscription_in_mailchimp = $5\n      RETURNING id\n      ",
+       email_subscription_in_mailchimp = $5,
+       updated_at = CURRENT_TIMESTAMP\n      RETURNING id\n      ",

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +13 to +14
"Left": ["Uuid", "Uuid", "Uuid", "Bool", "Varchar"]
},
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Codebase verification

Consider using ENUM type for email_subscription_in_mailchimp

Based on the code analysis, the email_subscription_in_mailchimp column has specific expected values used in the Mailchimp syncer: "subscribed", "unsubscribed", and "non-subscribed". To ensure data consistency, you should consider using an ENUM type instead of VARCHAR.

Example SQL modification:

CREATE TYPE email_subscription_status AS ENUM ('subscribed', 'unsubscribed', 'non-subscribed');
ALTER TABLE user_marketing_consents 
  ALTER COLUMN email_subscription_in_mailchimp TYPE email_subscription_status 
  USING email_subscription_in_mailchimp::email_subscription_status;
🔗 Analysis chain

Validate email_subscription_in_mailchimp values

The email_subscription_in_mailchimp parameter is defined as Varchar without any constraints. Consider adding a CHECK constraint or using an ENUM type to restrict possible values.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check if there's a constraint on email_subscription_in_mailchimp column
rg -A 5 "email_subscription_in_mailchimp.*CHECK|CREATE.*TYPE.*email_subscription"

Length of output: 83


Script:

#!/bin/bash
# Let's first check the schema definition for this column
rg -l "email_subscription_in_mailchimp" 

# Then let's look at the actual SQL migrations or schema files
fd -e sql | xargs rg "email_subscription_in_mailchimp"

Length of output: 871


Script:

#!/bin/bash
# Let's check the Rust model and any potential validations
cat services/headless-lms/models/src/marketing_consents.rs

# Let's also check if there are any enum definitions in TypeScript that might indicate expected values
cat shared-module/packages/common/src/bindings.ts

Length of output: 61884


Script:

#!/bin/bash
# Let's check the mailchimp syncer to understand the expected values
cat services/headless-lms/server/src/programs/mailchimp_syncer.rs

Length of output: 22807

Comment on lines +27 to +29
"parameters": {
"Left": ["Uuid"]
},
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Update parameters for pagination

Add LIMIT and OFFSET parameters to support pagination:

   "parameters": {
-    "Left": ["Uuid"]
+    "Left": ["Uuid", "Int8", "Int8"]
   },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"parameters": {
"Left": ["Uuid"]
},
"parameters": {
"Left": ["Uuid", "Int8", "Int8"]
},

Comment on lines +3 to +10
"query": "\n SELECT\n umc.user_id,\n u.email AS email,\n umc.email_subscription_in_mailchimp,\n umc.user_mailchimp_id\n FROM user_marketing_consents AS umc\n JOIN user_details AS u ON u.user_id = umc.user_id\n WHERE umc.course_language_group_id = $1\n AND umc.synced_to_mailchimp_at < u.updated_at\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "user_id",
"type_info": "Uuid"
},
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Codebase verification

⚠️ Potential issue

Based on the migration files examined, I can now generate the final response:

Consider adding index for synced_to_mailchimp_at and updated_at comparison

The query could benefit from an index to optimize the JOIN and WHERE conditions. The table definitions show:

  • user_details has a unique index on email
  • user_marketing_consents has a unique constraint on (user_id, course_language_group_id)
  • No index exists for the timestamp comparison synced_to_mailchimp_at < u.updated_at

Consider adding a composite index:

CREATE INDEX idx_marketing_consent_sync_status 
ON user_marketing_consents (course_language_group_id, synced_to_mailchimp_at)
WHERE deleted_at IS NULL;

• The email exposure is acceptable as this query appears to be part of a Mailchimp integration system where email addresses are required for synchronization.
• Pagination should be considered if large result sets are expected.

🔗 Analysis chain

Add pagination and review data exposure

The query retrieves sensitive user data (email addresses) and could return large result sets. Consider:

  1. Adding pagination using LIMIT/OFFSET
  2. Reviewing if email addresses need to be exposed in this query
  3. Adding appropriate indexes for the JOIN condition
  "query": "\n    SELECT\n        umc.user_id,\n        u.email AS email,\n        umc.email_subscription_in_mailchimp,\n        umc.user_mailchimp_id\n    FROM user_marketing_consents AS umc\n    JOIN user_details AS u ON u.user_id = umc.user_id\n    WHERE umc.course_language_group_id = $1\n    AND umc.synced_to_mailchimp_at < u.updated_at\n
+    LIMIT $2 OFFSET $3\n    ",
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check for existing indexes on the JOIN columns
rg -A 5 "CREATE.*INDEX.*ON.*(user_marketing_consents|user_details).*user_id"

# Look for other queries that might expose email addresses
rg "SELECT.*email.*FROM.*user_details"

Length of output: 119


Script:

#!/bin/bash
# Let's try a different approach to find schema definitions and indexes
# First, look for migration files
fd -e sql

# Then check their content for table definitions and indexes
fd -e sql -x cat {}

Length of output: 65984


Script:

#!/bin/bash
# Let's check for user_marketing_consents table definition and indexes
fd -e sql -x grep -l "user_marketing_consents" {}

# Then examine the specific migration file content
fd -e sql -x grep -A 20 "CREATE TABLE user_marketing_consents" {}

# Also check for user_details table definition
fd -e sql -x grep -A 20 "CREATE TABLE user_details" {}

Length of output: 2918

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

🧹 Outside diff range and nitpick comments (2)
kubernetes/base/headless-lms/mailchimp-syncer.yml (1)

25-31: Consider increasing resource limits for the syncing service.

The current resource limits might be too restrictive for a service that handles bulk operations:

  • Memory limit of 300Mi might be insufficient for large batches
  • CPU limit of 200m could throttle the service during peak operations
           resources:
             requests:
               memory: 100Mi
               cpu: 20m
             limits:
-              memory: 300Mi
-              cpu: 200m
+              memory: 512Mi
+              cpu: 500m
services/headless-lms/server/src/controllers/course_material/courses.rs (1)

929-933: Consider moving email subscription string conversion to model layer

The conversion of boolean to string should ideally be handled in the model layer (models::marketing_consents) to maintain better separation of concerns.

-    let email_subscription = if payload.email_subscription {
-        "subscribed"
-    } else {
-        "unsubscribed"
-    };

     let result = models::marketing_consents::upsert_marketing_consent(
         &mut conn,
         *course_id,
         payload.course_language_groups_id,
         &user.id,
-        email_subscription,
+        payload.email_subscription,
         payload.marketing_consent,
     )
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 3a84785 and 5dbf3e4.

📒 Files selected for processing (10)
  • kubernetes/base/headless-lms/mailchimp-syncer.yml (1 hunks)
  • kubernetes/base/kustomization.yaml (1 hunks)
  • services/course-material/src/services/backend.ts (3 hunks)
  • services/headless-lms/models/src/lib.rs (1 hunks)
  • services/headless-lms/server/src/controllers/course_material/courses.rs (3 hunks)
  • services/headless-lms/server/src/programs/mailchimp_syncer.rs (1 hunks)
  • services/headless-lms/server/src/ts_binding_generator.rs (1 hunks)
  • shared-module/packages/common/src/bindings.guard.ts (5 hunks)
  • shared-module/packages/common/src/bindings.ts (4 hunks)
  • shared-module/packages/common/src/locales/en/course-material.json (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • services/headless-lms/models/src/lib.rs
  • services/headless-lms/server/src/ts_binding_generator.rs
  • shared-module/packages/common/src/locales/en/course-material.json
  • shared-module/packages/common/src/bindings.ts
  • services/course-material/src/services/backend.ts
🔇 Additional comments (13)
shared-module/packages/common/src/bindings.guard.ts (4)

2202-2220: LGTM! The isUserMarketingConsent type guard is well implemented.

The type guard correctly validates all required properties of the UserMarketingConsent interface and properly handles optional properties with null checks. The implementation follows the established pattern in the codebase.


1031-1032: LGTM! The isCourse type guard updates are correct.

The changes properly validate the new optional join_code and required ask_marketing_consent properties with appropriate type checks.


1080-1081: LGTM! The isCourseUpdate type guard updates are correct.

The changes properly validate the new required is_joinable_by_code_only and ask_marketing_consent properties with boolean type checks.


1101-1102: LGTM! The isNewCourse type guard updates are correct.

The changes properly validate the new optional join_code and required ask_marketing_consent properties with appropriate type checks.

kubernetes/base/kustomization.yaml (1)

21-21: LGTM!

The addition of the mailchimp-syncer resource follows the existing pattern in the kustomization file.

services/headless-lms/server/src/programs/mailchimp_syncer.rs (5)

350-453: Refactor sync_contacts for better separation of concerns.

The function handles multiple responsibilities: fetching tokens, processing unsubscribed users, updating emails, and syncing consents.


701-705: Fix formatting in error logging.

The error message in eprintln! does not correctly format the chunk number and the error.


78-79: 🛠️ Refactor suggestion

Consider implementing rate limiting and increasing sync interval.

A 10-second sync interval might be too aggressive and could:

  1. Hit Mailchimp API rate limits
  2. Create unnecessary load on both systems
  3. Increase costs due to frequent API calls

Consider:

  • Increasing SYNC_INTERVAL_SECS to a more conservative value (e.g., 300 seconds)
  • Implementing rate limiting for API calls

189-190: 🛠️ Refactor suggestion

Implement retry mechanism for field operations.

Field addition/removal failures are only logged as warnings without retry attempts. This could lead to incomplete schema setup.

Also applies to: 206-209


240-240: ⚠️ Potential issue

Inconsistent authentication method with Mailchimp API.

The Mailchimp API expects the Authorization header to include apikey followed by the access token.

Also applies to: 295-295, 330-330, 512-512, 588-588, 631-631

services/headless-lms/server/src/controllers/course_material/courses.rs (3)

906-912: LGTM: Well-structured payload definition

The struct is well-defined with appropriate field types and necessary derive macros for serialization/deserialization.


952-968: LGTM: Well-implemented fetch endpoint

The function properly handles authorization, error cases, and returns an appropriate optional type for the consent data.


1091-1098: LGTM: Routes properly configured

The new routes follow RESTful conventions and are consistently integrated with existing course-related endpoints.

Comment on lines +42 to +44
echo Waiting for postgres to be available
timeout 120 ./wait-for-db.sh
./wait-for-db-migrations.sh
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Review database wait timeout and add health checks.

Two potential improvements:

  1. The 120-second timeout for database readiness might be insufficient for large databases
  2. Missing liveness and readiness probes which are crucial for Kubernetes health monitoring

Add health check probes:

          livenessProbe:
            httpGet:
              path: /health
              port: 8080
            initialDelaySeconds: 30
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: /health
              port: 8080
            initialDelaySeconds: 5
            periodSeconds: 5

Comment on lines +935 to +943
let result = models::marketing_consents::upsert_marketing_consent(
&mut conn,
*course_id,
payload.course_language_groups_id,
&user.id,
email_subscription,
payload.marketing_consent,
)
.await?;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add validation for course_language_groups_id

Add validation to ensure the provided course_language_groups_id belongs to the specified course to prevent potential data inconsistencies.

Add this validation before the upsert:

let valid = sqlx::query!(
    r#"
    SELECT 1 FROM courses 
    WHERE id = $1 AND course_language_group_id = $2
    "#,
    *course_id,
    payload.course_language_groups_id
)
.fetch_optional(&mut conn)
.await?;

if valid.is_none() {
    return Err(Error::BadRequest("Invalid course_language_groups_id for this course".into()));
}

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: 1

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 5dbf3e4 and e93b0ca.

📒 Files selected for processing (1)
  • system-tests/src/tests/change-course-language.spec.ts (1 hunks)

Comment on lines +60 to +61
// eslint-disable-next-line playwright/no-wait-for-timeout
await page.waitForTimeout(200)
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Replace arbitrary timeout with proper wait condition

The addition of waitForTimeout is a fragile solution that could lead to flaky tests. Playwright provides built-in synchronization mechanisms that are more reliable.

Consider replacing the timeout with a proper wait condition:

- // eslint-disable-next-line playwright/no-wait-for-timeout
- await page.waitForTimeout(200)
+ // Wait for any animations or state changes to complete
+ await page.waitForLoadState('networkidle')
+ // Or wait for a specific element state change that indicates readiness
+ await page.getByRole("button", { name: "Continue" }).waitFor({ state: "stable" })

This approach:

  1. Follows Playwright's best practices
  2. Makes the test more reliable by waiting for actual conditions rather than arbitrary timeouts
  3. Removes the need for eslint disable comment

Committable suggestion skipped: line range outside the PR's diff.

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: 1

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 5dbf3e4 and e93b0ca.

📒 Files selected for processing (1)
  • system-tests/src/tests/change-course-language.spec.ts (1 hunks)

Comment on lines +60 to +61
// eslint-disable-next-line playwright/no-wait-for-timeout
await page.waitForTimeout(200)
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Replace explicit timeout with a proper wait condition.

Using waitForTimeout is an anti-pattern in E2E testing as it makes tests brittle and can mask underlying issues. Consider using Playwright's built-in waiting mechanisms instead.

Here's a more robust approach:

- // eslint-disable-next-line playwright/no-wait-for-timeout
- await page.waitForTimeout(200)
+ // Wait for any animations or state changes to complete
+ await page.waitForLoadState('networkidle')
+ // Or wait for a specific element that indicates the action is complete
+ await page.getByRole("button", { name: "Continue" }).waitFor({ state: "stable" })

This change will:

  1. Make the test more reliable across different environments
  2. Better handle varying network conditions
  3. Follow Playwright's best practices for waiting

Committable suggestion skipped: line range outside the PR's diff.

@nygrenh nygrenh changed the title User can give marketing consent to a specific course Mailchimp Dec 11, 2024
@nygrenh nygrenh merged commit 58a64b0 into master Dec 11, 2024
18 checks passed
@nygrenh nygrenh deleted the marketing-consent branch December 11, 2024 11:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment