Skip to content

Conversation

@NathanVss
Copy link
Contributor

@NathanVss NathanVss commented Dec 1, 2025

Todo left

  • Make sure idp and domain are available on backend side
  • Make sure required services are activaed on backend side
  • Send webhook
  • Rebase to solve conflicts
  • Reuse check disable from @sylvinus for server dependencies
  • Nice UI
  • Disallow FI update if PC is activated

Test

  • Domains extraction
  • Get mail domain + status
  • Required service

Summary by CodeRabbit

  • New Features

    • ProConnect service UI with IDP selection, mail-domain checks, and richer per-service blocks
    • Service dependency support preventing activation until required services are active
    • Organizations now include mail_domain and mail_domain_status in API responses
  • Improvements

    • Clearer activation validation and user-facing error messages; cannot change IDP on active subscriptions
    • New service attribute component, translations, styles, and enhanced API exposure for selected config fields

✏️ Tip: You can customize this high-level summary in your review settings.

@NathanVss NathanVss changed the base branch from main to ref/subscription-active December 1, 2025 13:51
@coderabbitai
Copy link

coderabbitai bot commented Dec 1, 2025

Walkthrough

Adds service dependency and ProConnect activation logic, exposes operator/service config and organization mail-domain/status through APIs, updates admin/webhooks, adds a migration for service requirements, implements new frontend service UI (including ProConnect flows), and adds tests for the new behaviors.

Changes

Cohort / File(s) Summary
Backend Models & Migrations
src/backend/core/models.py, src/backend/core/migrations/0009_service_required_services.py
Add Organization mail-domain helpers and MailDomainStatus; add Service.required_services (M2M) and Service.can_activate; add ServiceSubscription validations (validate_required_services, validate_proconnect_subscription) and clean; migration to add required_services.
Backend Serializers & Viewsets
src/backend/core/api/serializers.py, src/backend/core/api/viewsets/service.py
Expose config on Operator/Service serializers; extend Organization/OrganizationService representations with mail_domain, mail_domain_status, and can_activate; adjust ServiceSubscriptionSerializer fields and add ProConnect validation and idp-change rules; viewsets add organization to serializer context and pass subscription metadata when creating.
Backend Admin & Webhooks
src/backend/core/admin.py, src/backend/core/webhooks.py
Move Operator config into a separate admin fieldset; add required_services to ServiceAdmin; webhook context now includes subscription_idp_name, subscription_is_active, organization_mail_domain, operator_id, and operator_name.
Backend Tests & Test Utils
src/backend/core/tests/models/test_organization.py, src/backend/core/tests/api/*, src/backend/core/tests/utils.py
Add tests for Organization domain parsing and mail-domain status, ProConnect subscription flows, service dependency activation semantics, operator/service config exposure; improve assert messages.
Frontend Types & Repository
src/frontend/src/features/api/Repository.ts
Add OperatorIdp type and config.idps; add MailDomainStatus and expose organization.mail_domain/mail_domain_status; add SERVICE_TYPE_PROCONNECT; extend Service with type, can_activate, and optional config.
Frontend Service UI Components
src/frontend/src/features/ui/components/service/ServiceBlock.tsx, .../ServiceBlockDispatcher.tsx, .../ServiceAttribute.tsx, .../implementations/ProConnectServiceBlock.tsx, .../implementations/RegularServiceBlock.tsx
Add ServiceAttribute component, useServiceBlock hook and ServiceBlock core component, dispatcher by service.type, Regular and ProConnect implementations. ProConnect block adds IDP selection modal, mail-domain display/warnings, form validation, and subscription lifecycle handling.
Frontend Pages, i18n & Styling
src/frontend/src/pages/operators/.../[id].tsx, src/frontend/src/features/i18n/translations.json, src/frontend/src/pages/operators/.../index.scss, src/frontend/package.json
Organization page uses ServiceBlockDispatcher and passes full organization object; add translations for services/idp/mail_domain/help_center (en/fr); add SCSS classes for service attribute UI; update package.json deps (add react-hook-form, reorder entries).
CI
.github/workflows/tests.yml
Add Docker network setup step to backend jobs and minor trigger quoting change.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Frontend as Browser UI
    participant API as Backend API
    participant DB as Database
    participant Webhook as Webhook Sender

    User->>Frontend: Open organization services page
    Frontend->>API: GET operator, organization, services
    API->>DB: fetch operator, org, services, subscriptions
    DB-->>API: return data (includes config, mail_domain, mail_domain_status, can_activate)
    API-->>Frontend: serialized payload

    User->>Frontend: Select IDP or toggle activation
    Frontend->>API: PATCH subscription (is_active / metadata.idp_id)
    API->>API: validate required_services & ProConnect prerequisites
    alt validation fails
        API-->>Frontend: 400 with errors
    else
        API->>DB: persist subscription change
        DB-->>API: saved
        API->>Webhook: send_webhooks (include idp_name, is_active, org mail_domain)
        API-->>Frontend: 200/201 success
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Pay extra attention to:
    • ProConnect validation logic and error messages in src/backend/core/models.py and serializers.
    • Service dependency traversal / possible cycles in Service.can_activate() and migration effects.
    • Serializer to_representation changes and context usage in src/backend/core/api/serializers.py and viewsets.
    • Frontend ProConnect flows, form handling, and modal save flows in ProConnectServiceBlock.tsx.

Possibly related PRs

Suggested reviewers

  • sylvinus

Poem

🐇 I hopped through configs in moonlit code,

Picked an IDP and cleared the mail-domain road,
Services now check their friends before flight,
Switches glow only when the rules say it's right,
A cheerful hop — deployment feels light.

Pre-merge checks and finishing touches

❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The PR title 'Add ProConnect card' is vague and does not adequately describe the substantial backend and frontend changes involved in this pull request. Consider a more descriptive title that highlights the primary changes, such as 'Add ProConnect service management with mail domain validation and required services' or 'Implement ProConnect card UI with backend subscription validation'.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 93.55% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/pc-service

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

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@NathanVss NathanVss changed the base branch from ref/subscription-active to main December 2, 2025 16:16
Copy link

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

🧹 Nitpick comments (4)
src/frontend/package.json (1)

24-24: Inconsistent version pinning for react-hook-form.

Most production dependencies use exact versions (e.g., "next": "15.5.4", "react": "19.1.0"), but react-hook-form uses a caret range (^7.67.0). Consider pinning to an exact version for consistency and reproducible builds.

-    "react-hook-form": "^7.67.0",
+    "react-hook-form": "7.67.0",
src/backend/core/models.py (1)

497-502: Consider defensive handling for email parsing.

While EmailField provides validation, if adresse_messagerie is somehow malformed (e.g., no @), this will raise an IndexError. The risk is low given Django's validation, but defensive coding would be safer.

     @property
     def adresse_messagerie_domain(self):
         """Get the mail domain for the organization."""
         if not self.adresse_messagerie:
             return None
-        return self.adresse_messagerie.split("@")[1]
+        parts = self.adresse_messagerie.split("@")
+        return parts[1] if len(parts) > 1 else None
src/frontend/src/pages/operators/[operator_id]/organizations/[id].tsx (2)

353-355: Clarify the purpose of the no-op onSubmit.

The onSubmit function is memoized but returns void 0 (effectively undefined). The actual submission logic appears to be in canActivateSubscription. Consider adding a comment or removing if unused.


240-242: Optimistic UI update may cause flickering on error.

setChecked(true) is called after onChangeSubscription, but the mutation is asynchronous. If the mutation fails, the UI will briefly show the checked state before reverting. Consider setting checked only on success.

               if (decision === "yes") {
+                setChecked(true);
                 onChangeSubscription(
                   { is_active: true },
                   {
+                    onSuccess: () => {
+                      // State already updated optimistically
+                    },
                     onError: () => {
                       setChecked(false);
                     },
                   }
                 );
-                setChecked(true);
               }

This is already partially implemented, but the current placement ensures the optimistic update happens first, which is correct. The suggestion is to make the intent clearer with a comment.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cdb580c and e574dcd.

⛔ Files ignored due to path filters (1)
  • src/frontend/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (9)
  • src/backend/core/admin.py (2 hunks)
  • src/backend/core/api/serializers.py (6 hunks)
  • src/backend/core/api/viewsets/service.py (2 hunks)
  • src/backend/core/migrations/0009_service_required_services.py (1 hunks)
  • src/backend/core/models.py (5 hunks)
  • src/frontend/package.json (1 hunks)
  • src/frontend/src/features/api/Repository.ts (3 hunks)
  • src/frontend/src/features/i18n/translations.json (2 hunks)
  • src/frontend/src/pages/operators/[operator_id]/organizations/[id].tsx (7 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
src/backend/**/*.py

📄 CodeRabbit inference engine (.cursor/rules/django-python.mdc)

src/backend/**/*.py: Use Django’s ORM for database interactions; avoid raw SQL unless strictly necessary for performance
Use Django’s built-in user model and authentication framework for user management
Log expected and unexpected application actions with appropriate log levels
Respect log level semantics: DEBUG (diagnostics), INFO (normal events), WARN (unexpected but continuing), ERROR (functional failure)
Do not log sensitive information (authorization tokens, passwords, financial/health data, PII)

Files:

  • src/backend/core/admin.py
  • src/backend/core/migrations/0009_service_required_services.py
  • src/backend/core/api/viewsets/service.py
  • src/backend/core/models.py
  • src/backend/core/api/serializers.py
src/backend/**/{models,forms,views}.py

📄 CodeRabbit inference engine (.cursor/rules/django-python.mdc)

Keep business logic in models and forms; keep views thin and focused on request handling

Files:

  • src/backend/core/models.py
src/backend/**/models.py

📄 CodeRabbit inference engine (.cursor/rules/django-python.mdc)

Define appropriate database indexes and optimize query patterns via model Meta and QuerySets

Files:

  • src/backend/core/models.py
src/backend/**/serializers.py

📄 CodeRabbit inference engine (.cursor/rules/django-python.mdc)

Use DRF serializers for JSON responses

Files:

  • src/backend/core/api/serializers.py
🧠 Learnings (1)
📚 Learning: 2025-10-03T23:40:57.889Z
Learnt from: CR
Repo: suitenumerique/st-deploycenter PR: 0
File: .cursor/rules/django-python.mdc:0-0
Timestamp: 2025-10-03T23:40:57.889Z
Learning: Applies to src/backend/**/serializers.py : Use DRF serializers for JSON responses

Applied to files:

  • src/backend/core/api/serializers.py
🧬 Code graph analysis (4)
src/frontend/src/features/api/Repository.ts (1)
src/backend/core/models.py (4)
  • Operator (187-281)
  • MailDomainStatus (334-337)
  • Organization (328-548)
  • Service (639-765)
src/backend/core/api/viewsets/service.py (2)
src/backend/core/models.py (2)
  • Organization (328-548)
  • ServiceSubscription (768-821)
src/frontend/src/features/api/Repository.ts (2)
  • Organization (33-45)
  • ServiceSubscription (47-54)
src/backend/core/models.py (1)
src/frontend/src/features/api/Repository.ts (1)
  • Organization (33-45)
src/backend/core/api/serializers.py (1)
src/backend/core/models.py (3)
  • mail_domain (513-516)
  • get_mail_domain_status (518-545)
  • can_activate (756-765)
🪛 Ruff (0.14.7)
src/backend/core/models.py

509-509: print found

Remove print

(T201)

🔇 Additional comments (15)
src/frontend/src/features/i18n/translations.json (1)

29-29: LGTM!

The new cannot_activate translations are well-crafted and consistent between English and French locales.

Also applies to: 77-77

src/backend/core/api/viewsets/service.py (1)

196-208: LGTM!

The metadata handling in the upsert flow is correctly implemented - extracting from validated data with an appropriate default and passing it to the subscription creation.

src/backend/core/migrations/0009_service_required_services.py (1)

1-18: LGTM!

The migration correctly adds the required_services ManyToManyField to the Service model with appropriate metadata. This aligns with the model definition for service dependency management.

src/backend/core/admin.py (2)

267-272: LGTM!

Good separation of the config field into its own fieldset for OperatorAdmin. This improves admin UI organization.


400-401: LGTM!

Adding required_services to the ServiceAdmin fieldset properly exposes the new ManyToMany relationship for managing service dependencies in the admin interface.

src/frontend/src/features/api/Repository.ts (2)

27-31: LGTM!

The MailDomainStatus enum correctly mirrors the backend Organization.MailDomainStatus enum values.


56-57: LGTM!

The SERVICE_TYPE_PROCONNECT constant and new Service type fields (type, can_activate) properly extend the frontend types to support the ProConnect activation flow.

Also applies to: 63-63, 70-70

src/backend/core/models.py (2)

334-337: LGTM!

The MailDomainStatus enum using StrEnum is clean and provides clear status values that serialize naturally to JSON.


728-734: LGTM!

The self-referential ManyToManyField with symmetrical=False is correctly set up for modeling service dependencies.

src/backend/core/api/serializers.py (2)

378-384: LGTM!

The to_representation override correctly enriches the organization response with mail domain data from the model's new utility method.


341-343: LGTM!

The list comprehension approach for read_only_fields is clear and maintainable.

src/frontend/src/pages/operators/[operator_id]/organizations/[id].tsx (4)

174-176: Placeholder implementation: canActivateSubscription always returns true.

This hook's canActivateSubscription always resolves to true, making it a no-op. The actual validation logic is only in ProConnectServiceBlock. If this is intentional for regular services, consider documenting it. Otherwise, this may need implementation.


48-62: LGTM!

The sorting logic correctly prioritizes ProConnect services first, then sorts by display priority in descending order. The null-coalescing to -Infinity ensures services without priority are placed at the end.


137-145: LGTM!

Clean dispatcher pattern that routes to the appropriate component based on service type.


383-390: LGTM!

Creative use of handleSubmit to validate the form before activation. The Promise wrapper cleanly integrates react-hook-form validation with the async activation flow.

Copy link

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

♻️ Duplicate comments (4)
src/backend/core/models.py (2)

505-510: Remove debug print statement and handle malformed URLs.

This code has two issues already flagged in previous reviews:

  1. Debug print statement on line 509 pollutes production logs
  2. Unsafe URL parsing with split("//")[1] can raise IndexError for malformed URLs

Based on coding guidelines, use proper logging and robust URL parsing:

+from urllib.parse import urlparse
+
 ...
 
 def site_internet_domain(self):
     """Get the website domain for the organization."""
     if not self.site_internet:
         return None
-    print(self.site_internet)
-    return self.site_internet.split("//")[1].replace("/", "")
+    try:
+        parsed = urlparse(self.site_internet)
+        return parsed.netloc or None
+    except Exception as e:
+        logger.warning(
+            "Invalid URL format for organization %s: %s",
+            self.id,
+            self.site_internet
+        )
+        return None

757-766: Logic bug: can_activate returns True if ANY required service is active instead of ALL.

This critical issue was already flagged in previous reviews. Using .exists() returns True if at least one required service has an active subscription. The method must verify that all required services are active.

Apply this diff:

 def can_activate(self, organization: Organization):
     """
     Check if the service can be activated for the given organization.
     """
-    if not self.required_services.count():
+    required_count = self.required_services.count()
+    if not required_count:
         return True
-    return self.required_services.all().filter(
+    active_required_count = self.required_services.filter(
         subscriptions__organization=organization,
         subscriptions__is_active=True
-    ).exists()
+    ).distinct().count()
+    return active_required_count == required_count
src/backend/core/api/viewsets/service.py (1)

101-104: Unhandled DoesNotExist exception for invalid organization ID.

This issue was already flagged in previous reviews. The direct call to Organization.objects.get() will raise Organization.DoesNotExist for invalid IDs, causing a 500 error instead of a proper 404 response.

Apply this diff:

+from django.shortcuts import get_object_or_404
+
 def get_serializer_context(self):
-    """Add operator_id and organization to serializer context."""
+    """Add operator_id and organization to serializer context."""
     context = super().get_serializer_context()
     context["operator_id"] = self.kwargs["operator_id"]
-    organization = models.Organization.objects.get(
-        id=self.kwargs["organization_id"]
+    organization = get_object_or_404(
+        models.Organization, id=self.kwargs["organization_id"]
     )
     context["organization"] = organization
     return context
src/frontend/src/pages/operators/[operator_id]/organizations/[id].tsx (1)

360-362: Add null check for operator.config before accessing idps.

This issue was already flagged in previous reviews. operator?.config.idps assumes config exists when operator exists. If config is null or undefined, this will throw a runtime error.

Apply this diff:

 const getIdp = (idp_id: string) => {
-  return operator?.config.idps?.find((idp) => idp.id == idp_id);
+  return operator?.config?.idps?.find((idp) => idp.id === idp_id);
 };

Note: Also changed == to === for strict equality comparison.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e574dcd and 21f8e1b.

📒 Files selected for processing (6)
  • src/backend/core/api/viewsets/service.py (2 hunks)
  • src/backend/core/models.py (6 hunks)
  • src/backend/core/webhooks.py (2 hunks)
  • src/frontend/src/features/i18n/translations.json (2 hunks)
  • src/frontend/src/pages/operators/[operator_id]/organizations/[id].tsx (7 hunks)
  • src/frontend/src/pages/operators/[operator_id]/organizations/index.scss (2 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
src/backend/**/*.py

📄 CodeRabbit inference engine (.cursor/rules/django-python.mdc)

src/backend/**/*.py: Use Django’s ORM for database interactions; avoid raw SQL unless strictly necessary for performance
Use Django’s built-in user model and authentication framework for user management
Log expected and unexpected application actions with appropriate log levels
Respect log level semantics: DEBUG (diagnostics), INFO (normal events), WARN (unexpected but continuing), ERROR (functional failure)
Do not log sensitive information (authorization tokens, passwords, financial/health data, PII)

Files:

  • src/backend/core/webhooks.py
  • src/backend/core/api/viewsets/service.py
  • src/backend/core/models.py
src/backend/**/{models,forms,views}.py

📄 CodeRabbit inference engine (.cursor/rules/django-python.mdc)

Keep business logic in models and forms; keep views thin and focused on request handling

Files:

  • src/backend/core/models.py
src/backend/**/models.py

📄 CodeRabbit inference engine (.cursor/rules/django-python.mdc)

Define appropriate database indexes and optimize query patterns via model Meta and QuerySets

Files:

  • src/backend/core/models.py
🧬 Code graph analysis (3)
src/backend/core/webhooks.py (1)
src/backend/core/models.py (2)
  • idp_name (884-895)
  • mail_domain (513-516)
src/backend/core/api/viewsets/service.py (1)
src/backend/core/models.py (2)
  • Organization (328-548)
  • ServiceSubscription (769-898)
src/frontend/src/pages/operators/[operator_id]/organizations/[id].tsx (3)
src/backend/core/models.py (4)
  • Service (639-766)
  • Organization (328-548)
  • ServiceSubscription (769-898)
  • MailDomainStatus (334-337)
src/frontend/src/hooks/useQueries.tsx (1)
  • useMutationUpdateOrganizationServiceSubscription (87-117)
src/frontend/src/features/layouts/components/GlobalLayout.tsx (1)
  • useOperatorContext (57-63)
🪛 Ruff (0.14.7)
src/backend/core/models.py

509-509: print found

Remove print

(T201)

🔇 Additional comments (16)
src/frontend/src/features/i18n/translations.json (1)

29-50: LGTM! Well-structured translation additions.

The new translation keys for ProConnect (cannot_activate, idp, mail_domain) are complete and consistent across both English and French locales. The nested structure follows existing patterns.

Also applies to: 95-116

src/backend/core/models.py (5)

334-337: LGTM! Proper enum definition.

The MailDomainStatus enum is well-defined using StrEnum with clear status values.


518-545: LGTM! Sound RPNT validation logic.

The mail domain status computation correctly validates RPNT criteria using set operations. Note that this method depends on adresse_messagerie_domain and site_internet_domain(), which have separate issues that need fixing.


728-735: LGTM! Proper service dependency relationship.

The required_services ManyToManyField is correctly configured with symmetrical=False for directed dependency relationships.


822-856: LGTM! Thorough validation of required services.

The validation correctly ensures all required services have active subscriptions before allowing activation, with clear error messages listing missing services.


858-873: LGTM! Appropriate ProConnect-specific validation.

The validation correctly enforces mail domain and IDP requirements for ProConnect subscriptions before activation.

src/backend/core/webhooks.py (1)

224-224: LGTM! Webhook context enrichment aligns with model changes.

The addition of subscription_idp_name and organization_mail_domain to the webhook payload appropriately exposes the new ProConnect-related data for external systems.

Also applies to: 234-234

src/backend/core/api/viewsets/service.py (1)

198-205: LGTM! Explicit metadata handling in subscription creation.

The explicit extraction of metadata from validated data with a default empty dict is appropriate for the upsert behavior.

src/frontend/src/pages/operators/[operator_id]/organizations/index.scss (1)

189-253: LGTM! Well-structured styling additions for ProConnect UI.

The new CSS classes for service attributes, warnings, and modals are well-organized with appropriate visual hierarchy and interactive states.

src/frontend/src/pages/operators/[operator_id]/organizations/[id].tsx (7)

57-71: LGTM! Efficient ProConnect prioritization logic.

The memoized sorting correctly places ProConnect services first, then orders by display priority.


130-140: LGTM! Appropriate loading states.

The rendering guard correctly waits for both organization and operator data before rendering services.


156-195: LGTM! Well-structured service block hook.

The hook appropriately encapsulates subscription state and mutation logic, with extensibility for custom activation validation.


374-488: LGTM! Robust form validation for ProConnect activation.

The form validation correctly enforces IDP and mail domain requirements before allowing activation, with clear error messaging.


490-501: LGTM! Appropriate mail domain status warnings.

The conditional warnings for mail domain status use properly internationalized translation keys that are defined in the translations file.


381-389: LGTM! Proper IDP pre-configuration flow.

The saveIdpChange function correctly allows IDP selection before subscription activation by creating an inactive subscription if needed.


508-537: LGTM! Clean and reusable attribute component.

The ServiceAttribute component is well-structured with appropriate support for both static display and interactive editing.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (4)
src/backend/core/api/serializers.py (2)

287-290: Re‑exposing Operator.config risks leaking sensitive keys; either remove or filter it.

Including config in OperatorSerializer again will expose all operator configuration, which previously contained values like external_management_api_key. This is a direct security/privacy risk; either remove config from fields or implement a filtered representation that strips secrets (keys containing e.g. key, secret, token, credential). As per coding guidelines, sensitive information should not be exposed via APIs.


419-423: Guard against missing organization in serializer context to avoid KeyError.

Accessing self.context["organization"] directly will raise KeyError if the serializer is instantiated without that key (e.g., reuse in another view), leading to a 500. Either validate presence explicitly or fall back gracefully.

For example:

def to_representation(self, instance):
    data = super().to_representation(instance)
    organization = self.context.get("organization")
    if organization is None:
        # Either be strict:
        # raise ValueError("OrganizationServiceSerializer requires 'organization' in context")
        data["can_activate"] = False
    else:
        data["can_activate"] = instance.can_activate(organization)
    return data

This keeps the API robust while still exposing the flag when context is correctly set.

src/frontend/src/features/api/Repository.ts (1)

11-24: Make Operator.config and idps nullable/optional to match backend JSONField.

Backend allows Operator.config to be null and not necessarily contain idps. Typing it as always-present { idps: OperatorIdp[] } makes downstream code like operator?.config.idps unsafe when config is null or missing idps, and also hides that idps may be absent.

Consider aligning the type with the backend shape:

 export type Operator = {
   id: string;
   name: string;
   url: string;
   scope: string;
   is_active: boolean;
-  config: {
-    idps: OperatorIdp[];
-  };
+  config?: {
+    idps?: OperatorIdp[];
+  } | null;
 };

Then, at call sites, prefer operator.config?.idps ?? [] or operator.config?.idps?.find(...) so accesses stay safe when config or idps is missing.

src/frontend/src/features/ui/components/service/implementations/ProConnectServiceBlock.tsx (1)

41-49: Guard against operator.config being null and use strict equality for IDP lookup.

Operator.config is a nullable JSON field on the backend, so using operator?.config.idps can still throw when config is null, and == risks silent coercions.

Recommend:

  const getIdp = (idp_id: string) => {
-    return operator?.config.idps?.find((idp) => idp.id == idp_id);
+    return operator?.config?.idps?.find((idp) => idp.id === idp_id);
  };
...
-                          options={
-                            operator?.config.idps?.map((idp) => ({
-                              label: idp.name,
-                              value: idp.id,
-                            })) ?? []
-                          }
+                          options={
+                            operator?.config?.idps?.map((idp) => ({
+                              label: idp.name,
+                              value: idp.id,
+                            })) ?? []
+                          }

This matches the nullable config shape and avoids unexpected matches from loose equality.

Also applies to: 120-133

🧹 Nitpick comments (6)
src/backend/core/api/serializers.py (2)

305-320: Confirm that Service.config is safe to expose as‑is (no secrets embedded).

Adding config to ServiceSerializer means all service configuration becomes part of the public API response for this endpoint. If Service.config can contain credentials, tokens, or other sensitive/internal‑only values (now or in the future), implement a filtered representation similar to what is needed for Operator.config, or split public vs private config.


339-344: Computed read_only_fields cleanly makes only metadata writable.

Deriving read_only_fields from fields and excluding metadata ensures all other fields (created_at, updated_at, is_active, service, operator) are read‑only in this composite serializer. This matches the intended “only metadata is mutable” behavior and stays resilient if fields changes.

You could make intent clearer by spelling out the list (even if slightly more verbose) to avoid surprises if fields is refactored later.

src/frontend/src/features/ui/components/service/implementations/ProConnectServiceBlock.tsx (2)

68-76: Preserve existing subscription metadata when changing IDP (if other keys exist).

saveIdpChange currently sends metadata: { idp_id: idp?.id }, overwriting any other metadata fields the backend might have stored for this subscription.

If metadata is expected to hold additional information, consider shallow-merging:

  const saveIdpChange = (idp?: OperatorIdp) => {
-    const data: Partial<ServiceSubscription> = {
-      metadata: { idp_id: idp?.id },
-    };
+    const previousMetadata = props.service.subscription?.metadata ?? {};
+    const data: Partial<ServiceSubscription> = {
+      metadata: { ...previousMetadata, idp_id: idp?.id },
+    };

The rest of the function can stay as is.


80-92: Minor UX tweaks: avoid incidental form submit in modal and harden external links.

Two small refinements:

  • The close button in the IDP modal is declared as type="submit", which will submit the surrounding form even though it only closes the modal:
-                          <Button type="submit" onClick={idpModal.close}>
+                          <Button type="button" onClick={idpModal.close}>
  • For links that open in a new tab, add rel="noreferrer noopener" for safety:
-              <a target="_blank" href={props.service.config?.help_center_url}>
+              <a
+                target="_blank"
+                rel="noreferrer noopener"
+                href={props.service.config?.help_center_url}
+              >

(You may want to mirror the same pattern in ServiceBlock’s main “goto” link.)

Also applies to: 94-115, 193-205

src/frontend/src/pages/operators/[operator_id]/organizations/[id].tsx (1)

108-116: Loading guard is safe, consider future error handling.

isServicesLoading || !organization || !operator ensures the dispatcher is only rendered when all required data and context are present, which protects useOperatorContext consumers.

If useOrganization / useOrganizationServices can end in an error state (404/500), you may eventually want to differentiate “still loading” from “failed to load” instead of showing a perpetual spinner.

src/frontend/src/features/ui/components/service/ServiceBlock.tsx (1)

171-183: Add rel="noreferrer noopener" to external “goto” links.

Since the “goto” link opens services in a new tab, it’s safer to include rel attributes:

-              <a target="_blank" href={service.url}>
+              <a target="_blank" rel="noreferrer noopener" href={service.url}>

Consider mirroring this pattern in other external links (e.g., ProConnect help center).

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 21f8e1b and e74f33e.

📒 Files selected for processing (10)
  • src/backend/core/api/serializers.py (7 hunks)
  • src/backend/core/webhooks.py (2 hunks)
  • src/frontend/src/features/api/Repository.ts (3 hunks)
  • src/frontend/src/features/i18n/translations.json (2 hunks)
  • src/frontend/src/features/ui/components/service/ServiceAttribute.tsx (1 hunks)
  • src/frontend/src/features/ui/components/service/ServiceBlock.tsx (1 hunks)
  • src/frontend/src/features/ui/components/service/ServiceBlockDispatcher.tsx (1 hunks)
  • src/frontend/src/features/ui/components/service/implementations/ProConnectServiceBlock.tsx (1 hunks)
  • src/frontend/src/features/ui/components/service/implementations/RegularServiceBlock.tsx (1 hunks)
  • src/frontend/src/pages/operators/[operator_id]/organizations/[id].tsx (3 hunks)
✅ Files skipped from review due to trivial changes (1)
  • src/frontend/src/features/ui/components/service/ServiceAttribute.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/backend/core/webhooks.py
🧰 Additional context used
📓 Path-based instructions (2)
src/backend/**/*.py

📄 CodeRabbit inference engine (.cursor/rules/django-python.mdc)

src/backend/**/*.py: Use Django’s ORM for database interactions; avoid raw SQL unless strictly necessary for performance
Use Django’s built-in user model and authentication framework for user management
Log expected and unexpected application actions with appropriate log levels
Respect log level semantics: DEBUG (diagnostics), INFO (normal events), WARN (unexpected but continuing), ERROR (functional failure)
Do not log sensitive information (authorization tokens, passwords, financial/health data, PII)

Files:

  • src/backend/core/api/serializers.py
src/backend/**/serializers.py

📄 CodeRabbit inference engine (.cursor/rules/django-python.mdc)

Use DRF serializers for JSON responses

Files:

  • src/backend/core/api/serializers.py
🧠 Learnings (2)
📚 Learning: 2025-10-03T23:40:57.889Z
Learnt from: CR
Repo: suitenumerique/st-deploycenter PR: 0
File: .cursor/rules/django-python.mdc:0-0
Timestamp: 2025-10-03T23:40:57.889Z
Learning: Applies to src/backend/**/serializers.py : Use DRF serializers for JSON responses

Applied to files:

  • src/backend/core/api/serializers.py
📚 Learning: 2025-10-03T23:40:57.889Z
Learnt from: CR
Repo: suitenumerique/st-deploycenter PR: 0
File: .cursor/rules/django-python.mdc:0-0
Timestamp: 2025-10-03T23:40:57.889Z
Learning: Applies to src/backend/**/*.py : Do not log sensitive information (authorization tokens, passwords, financial/health data, PII)

Applied to files:

  • src/backend/core/api/serializers.py
🧬 Code graph analysis (7)
src/frontend/src/features/ui/components/service/implementations/ProConnectServiceBlock.tsx (6)
src/frontend/src/features/api/Repository.ts (4)
  • Service (58-74)
  • Organization (33-45)
  • OperatorIdp (11-14)
  • ServiceSubscription (47-54)
src/frontend/src/pages/operators/[operator_id]/organizations/[id].tsx (1)
  • Organization (16-122)
src/frontend/src/features/layouts/components/GlobalLayout.tsx (1)
  • useOperatorContext (57-63)
src/frontend/src/features/ui/components/service/ServiceBlock.tsx (2)
  • useServiceBlock (11-53)
  • ServiceBlock (55-189)
src/frontend/src/features/ui/components/service/ServiceAttribute.tsx (1)
  • ServiceAttribute (1-30)
src/backend/core/models.py (1)
  • MailDomainStatus (334-337)
src/frontend/src/features/ui/components/service/implementations/RegularServiceBlock.tsx (2)
src/frontend/src/features/api/Repository.ts (2)
  • Service (58-74)
  • Organization (33-45)
src/frontend/src/features/ui/components/service/ServiceBlock.tsx (2)
  • useServiceBlock (11-53)
  • ServiceBlock (55-189)
src/frontend/src/features/ui/components/service/ServiceBlock.tsx (3)
src/frontend/src/features/api/Repository.ts (4)
  • Service (58-74)
  • Organization (33-45)
  • updateOrganizationServiceSubscription (159-174)
  • ServiceSubscription (47-54)
src/frontend/src/hooks/useQueries.tsx (1)
  • useMutationUpdateOrganizationServiceSubscription (87-117)
src/frontend/src/features/layouts/components/GlobalLayout.tsx (1)
  • useOperatorContext (57-63)
src/frontend/src/features/ui/components/service/ServiceBlockDispatcher.tsx (3)
src/frontend/src/features/api/Repository.ts (3)
  • Service (58-74)
  • Organization (33-45)
  • SERVICE_TYPE_PROCONNECT (56-56)
src/frontend/src/features/ui/components/service/implementations/ProConnectServiceBlock.tsx (1)
  • ProConnectServiceBlock (41-211)
src/frontend/src/features/ui/components/service/implementations/RegularServiceBlock.tsx (1)
  • RegularServiceBlock (7-13)
src/frontend/src/pages/operators/[operator_id]/organizations/[id].tsx (2)
src/frontend/src/features/api/Repository.ts (1)
  • SERVICE_TYPE_PROCONNECT (56-56)
src/frontend/src/features/ui/components/service/ServiceBlockDispatcher.tsx (1)
  • ServiceBlockDispatcher (9-17)
src/frontend/src/features/api/Repository.ts (1)
src/backend/core/models.py (5)
  • Operator (187-281)
  • MailDomainStatus (334-337)
  • Organization (328-548)
  • Service (639-766)
  • ServiceSubscription (769-898)
src/backend/core/api/serializers.py (1)
src/backend/core/models.py (3)
  • mail_domain (513-516)
  • get_mail_domain_status (518-545)
  • can_activate (757-766)
🔇 Additional comments (8)
src/backend/core/api/serializers.py (3)

326-330: Writable metadata / is_active here looks intentional and consistent.

ServiceSubscriptionSerializer now exposes metadata and is_active as writable while keeping timestamps read‑only, which matches common patterns for subscription state + freeform metadata. No correctness issues spotted.


379-385: Appending mail domain info in to_representation is straightforward and correct.

Using get_mail_domain_status() to inject mail_domain and mail_domain_status into the serialized output aligns with the model helpers and avoids extra DB hits. No functional or privacy issues evident.


393-397: Keeping can_activate out of Meta.fields but adding it in to_representation is a good choice.

Not declaring can_activate in fields while still adding it at render time ensures it’s output‑only and can’t be posted/updated by clients, which is appropriate for a computed capability flag.

src/frontend/src/features/api/Repository.ts (1)

27-45: Mail-domain and ProConnect typings look consistent with backend.

MailDomainStatus values, Organization.mail_domain / mail_domain_status, SERVICE_TYPE_PROCONNECT, and Service.type / can_activate / config.help_center_url all line up cleanly with the backend model descriptions and downstream usage in the UI (ProConnect flows, tooltips, help center link). No changes needed here.

Also applies to: 56-73

src/frontend/src/features/ui/components/service/implementations/RegularServiceBlock.tsx (1)

1-13: Straightforward wrapper around ServiceBlock looks good.

Nice separation: regular services reuse useServiceBlock + ServiceBlock without adding logic, leaving specialization to the ProConnect implementation.

src/frontend/src/features/ui/components/service/ServiceBlockDispatcher.tsx (1)

1-17: Type-based dispatch between ProConnect and regular services is clear and minimal.

The dispatcher cleanly centralizes the SERVICE_TYPE_PROCONNECT check and keeps both implementations’ props in sync. Looks good.

src/frontend/src/pages/operators/[operator_id]/organizations/[id].tsx (1)

35-49: Service ordering (ProConnect first, then by priority) is correctly implemented.

Using SERVICE_TYPE_PROCONNECT checks followed by display_priority ?? -Infinity yields the intended order (ProConnect at the top, then descending priority, with undefined priorities last). Copying services.results before sorting preserves the original data.

src/frontend/src/features/ui/components/service/ServiceBlock.tsx (1)

11-53: Hook abstraction for service subscription state looks solid.

Centralizing subscription state, mutation wiring, and canActivateSubscription in useServiceBlock keeps the UI component lean and reusable. Syncing checked on service.subscription changes ensures React Query updates propagate correctly.

Copy link

@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

♻️ Duplicate comments (3)
src/backend/core/models.py (3)

498-503: Handle malformed email addresses to prevent IndexError.

The previous review flagged this: if adresse_messagerie doesn't contain @ (e.g., from data import or corruption), split("@")[1] will raise IndexError. While Django's EmailField validates at save time, defensive coding requires handling edge cases.

Apply this diff to add robust error handling:

 @property
 def adresse_messagerie_domain(self):
     """Get the mail domain for the organization."""
     if not self.adresse_messagerie:
         return None
-    return self.adresse_messagerie.split("@")[1]
+    try:
+        return self.adresse_messagerie.split("@")[1]
+    except IndexError:
+        logger.warning(
+            "Invalid email format for organization %s: %s",
+            self.id,
+            self.adresse_messagerie
+        )
+        return None

Based on coding guidelines, log unexpected application actions with appropriate levels.


770-779: Logic bug: can_activate checks if ANY required service is subscribed instead of ALL.

This critical issue was flagged in the previous review but remains unresolved. Using .exists() returns True if at least one required service has an active subscription. The method must verify that all required services are active.

Apply this diff to fix the logic:

     def can_activate(self, organization: Organization):
         """
         Check if the service can be activated for the given organization.
         """
-        if not self.required_services.count():
+        required_count = self.required_services.count()
+        if not required_count:
             return True
-        return self.required_services.all().filter(
+        active_required_count = self.required_services.filter(
             subscriptions__organization=organization,
             subscriptions__is_active=True
-        ).exists()
+        ).distinct().count()
+        return active_required_count == required_count

896-908: Handle missing idps key to prevent KeyError.

This issue was flagged in the previous review but remains unresolved. Line 903 directly accesses self.operator.config["idps"] without using .get(). If the config dict exists but doesn't have an idps key, this will raise KeyError.

Apply this diff:

 @property
 def idp_name(self):
     """
     Get the name of the IDP for the ProConnect subscription.
     """
     if not self.metadata.get("idp_id"):
         return None
-    if not self.operator.config["idps"]:
+    idps = self.operator.config.get("idps")
+    if not idps:
         return None
-    for idp in self.operator.config["idps"]:
+    for idp in idps:
         if idp["id"] == self.metadata.get("idp_id"):
             return idp["name"]
     return None
🧹 Nitpick comments (2)
src/backend/core/models.py (1)

506-523: Handle empty domain edge case.

The urlparse approach is excellent (addressing the previous review), but if parsed.netloc is empty (e.g., for malformed URLs like "example.com" without a scheme), the method returns an empty string instead of None.

Apply this diff to ensure consistent None return for invalid cases:

 @property
 def site_internet_domain(self):
     """
     Get the website domain for the organization.

     Not sure that this method is completely exhaustive.
     """
     if not self.site_internet:
         return None
     parsed = urlparse(self.site_internet)
     domain = parsed.netloc
+    if not domain:
+        return None
     # Remove port number if present (e.g., "example.com:8080" -> "example.com")
     if ":" in domain:
         domain = domain.split(":")[0]
     # Remove www. prefix if present
     if domain.startswith("www."):
         return domain[4:]
     return domain
src/backend/core/tests/models/test_organization.py (1)

78-105: Consider adding tests for critical validation logic.

The current tests provide good coverage for domain extraction. However, consider adding tests for the following critical validation logic introduced in this PR:

  • Organization.get_mail_domain_status() with various RPNT criteria combinations
  • Service.can_activate() to verify the ALL-vs-ANY logic (especially important given the bug flagged in the models review)
  • ServiceSubscription.validate_required_services() and validate_proconnect_subscription()

These validations enforce business rules that could break silently without test coverage.

Do you want me to generate test cases for these validation methods?

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e74f33e and a8cbb4c.

📒 Files selected for processing (2)
  • src/backend/core/models.py (6 hunks)
  • src/backend/core/tests/models/test_organization.py (1 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
src/backend/**/*.py

📄 CodeRabbit inference engine (.cursor/rules/django-python.mdc)

src/backend/**/*.py: Use Django’s ORM for database interactions; avoid raw SQL unless strictly necessary for performance
Use Django’s built-in user model and authentication framework for user management
Log expected and unexpected application actions with appropriate log levels
Respect log level semantics: DEBUG (diagnostics), INFO (normal events), WARN (unexpected but continuing), ERROR (functional failure)
Do not log sensitive information (authorization tokens, passwords, financial/health data, PII)

Files:

  • src/backend/core/tests/models/test_organization.py
  • src/backend/core/models.py
src/backend/**/tests/**/*.py

📄 CodeRabbit inference engine (.cursor/rules/django-python.mdc)

src/backend/**/tests/**/*.py: Use Django’s testing tools with pytest-django for reliable test suites
Unit tests should focus on a single use-case, keep assertions minimal, and cover all plausible scenarios

Files:

  • src/backend/core/tests/models/test_organization.py
src/backend/**/{models,forms,views}.py

📄 CodeRabbit inference engine (.cursor/rules/django-python.mdc)

Keep business logic in models and forms; keep views thin and focused on request handling

Files:

  • src/backend/core/models.py
src/backend/**/models.py

📄 CodeRabbit inference engine (.cursor/rules/django-python.mdc)

Define appropriate database indexes and optimize query patterns via model Meta and QuerySets

Files:

  • src/backend/core/models.py
🧬 Code graph analysis (2)
src/backend/core/tests/models/test_organization.py (2)
src/backend/core/factories.py (1)
  • OrganizationFactory (69-102)
src/backend/core/models.py (2)
  • adresse_messagerie_domain (499-503)
  • site_internet_domain (507-523)
src/backend/core/models.py (1)
src/frontend/src/features/api/Repository.ts (2)
  • Organization (33-45)
  • ServiceSubscription (47-54)
🪛 Ruff (0.14.7)
src/backend/core/tests/models/test_organization.py

73-73: Redefinition of unused test_site_internet_domain_classic_trailing_slash from line 68

(F811)

🔇 Additional comments (8)
src/backend/core/models.py (7)

335-339: LGTM!

The MailDomainStatus enum is well-defined with clear values that align with the frontend types.


525-558: LGTM, but depends on fixing property issues.

The mail_domain property and get_mail_domain_status method implement sound RPNT validation logic. However, ensure the adresse_messagerie_domain and site_internet_domain properties are fixed (per other review comments) to prevent propagation of IndexError or empty string issues.


741-748: LGTM!

The required_services ManyToManyField is correctly configured with symmetrical=False for dependency relationships.


835-868: LGTM!

The validate_required_services method correctly verifies that all required services have active subscriptions using set comparison, and provides clear error messages listing missing services.


871-886: LGTM, but depends on fixing domain extraction.

The ProConnect validation logic is correct. However, line 882's check on self.organization.mail_domain depends on adresse_messagerie_domain not raising IndexError (see earlier review comment).


888-894: LGTM!

The clean method correctly invokes validation before saving, following Django best practices.


6-6: StrEnum requirement is already met. The project specifies Python 3.13+ as the minimum version (requires-python = ">=3.13,<4.0" in src/backend/pyproject.toml), which exceeds the Python 3.11 requirement for StrEnum. No action needed.

src/backend/core/tests/models/test_organization.py (1)

1-44: LGTM!

The test setup and adresse_messagerie_domain tests provide good coverage of empty, valid, and invalid email formats.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
src/backend/core/tests/models/test_organization.py (1)

74-77: Duplicate test method - only one will run.

This is flagged by static analysis (Ruff F811). The test method test_site_internet_domain_classic_trailing_slash is defined twice (lines 69 and 74). Python silently overrides the first definition, so only the second test runs.

🧹 Nitpick comments (2)
src/backend/core/tests/api/test_services.py (1)

698-732: Add blank line before function definition.

PEP 8 recommends two blank lines before top-level function definitions.

+
 def test_api_organization_proconnect_service_cannot_activate_if_idp_is_not_set():
src/backend/core/tests/models/test_organization.py (1)

20-28: Consider using is None instead of == None.

PEP 8 recommends using is None for None comparisons since None is a singleton.

     def test_messagerie_domain_empty(self):
         """The messagerie domain should be None if the email is not set."""
         organization = factories.OrganizationFactory(adresse_messagerie=None)
-        assert organization.adresse_messagerie_domain == None
+        assert organization.adresse_messagerie_domain is None

     def test_messagerie_domain_empty_str(self):
         """The messagerie domain should be None if the email is an empty string."""
         organization = factories.OrganizationFactory(adresse_messagerie="")
-        assert organization.adresse_messagerie_domain == None
+        assert organization.adresse_messagerie_domain is None

Similar changes apply to lines 52 and 57 for site_internet_domain tests.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a8cbb4c and 4b4bec7.

📒 Files selected for processing (6)
  • bin/pytest (1 hunks)
  • src/backend/core/api/serializers.py (6 hunks)
  • src/backend/core/tests/api/test_organizations.py (3 hunks)
  • src/backend/core/tests/api/test_services.py (1 hunks)
  • src/backend/core/tests/models/test_organization.py (1 hunks)
  • src/backend/core/tests/utils.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/backend/core/api/serializers.py
🧰 Additional context used
📓 Path-based instructions (2)
src/backend/**/*.py

📄 CodeRabbit inference engine (.cursor/rules/django-python.mdc)

src/backend/**/*.py: Use Django’s ORM for database interactions; avoid raw SQL unless strictly necessary for performance
Use Django’s built-in user model and authentication framework for user management
Log expected and unexpected application actions with appropriate log levels
Respect log level semantics: DEBUG (diagnostics), INFO (normal events), WARN (unexpected but continuing), ERROR (functional failure)
Do not log sensitive information (authorization tokens, passwords, financial/health data, PII)

Files:

  • src/backend/core/tests/api/test_services.py
  • src/backend/core/tests/utils.py
  • src/backend/core/tests/api/test_organizations.py
  • src/backend/core/tests/models/test_organization.py
src/backend/**/tests/**/*.py

📄 CodeRabbit inference engine (.cursor/rules/django-python.mdc)

src/backend/**/tests/**/*.py: Use Django’s testing tools with pytest-django for reliable test suites
Unit tests should focus on a single use-case, keep assertions minimal, and cover all plausible scenarios

Files:

  • src/backend/core/tests/api/test_services.py
  • src/backend/core/tests/utils.py
  • src/backend/core/tests/api/test_organizations.py
  • src/backend/core/tests/models/test_organization.py
🧠 Learnings (1)
📚 Learning: 2025-10-03T23:40:57.889Z
Learnt from: CR
Repo: suitenumerique/st-deploycenter PR: 0
File: .cursor/rules/django-python.mdc:0-0
Timestamp: 2025-10-03T23:40:57.889Z
Learning: Applies to src/backend/**/tests/**/*.py : Use Django’s testing tools with pytest-django for reliable test suites

Applied to files:

  • bin/pytest
🧬 Code graph analysis (3)
src/backend/core/tests/api/test_services.py (1)
src/backend/core/models.py (1)
  • idp_name (897-908)
src/backend/core/tests/api/test_organizations.py (1)
src/backend/core/models.py (2)
  • Organization (329-561)
  • MailDomainStatus (335-338)
src/backend/core/tests/models/test_organization.py (1)
src/backend/core/models.py (4)
  • adresse_messagerie_domain (499-503)
  • site_internet_domain (507-523)
  • get_mail_domain_status (531-558)
  • MailDomainStatus (335-338)
🪛 Ruff (0.14.7)
src/backend/core/tests/models/test_organization.py

74-74: Redefinition of unused test_site_internet_domain_classic_trailing_slash from line 69

(F811)

🔇 Additional comments (8)
bin/pytest (1)

9-9: LGTM!

The addition of -vvv for maximum verbosity will aid in diagnosing test failures, particularly useful given the expanded test coverage in this PR. The flag placement after "$@" preserves user-provided arguments.

src/backend/core/tests/utils.py (1)

22-23: LGTM!

The enhanced assertion message provides clearer context for debugging test failures by showing the expected value, actual value, and the key where the mismatch occurred.

src/backend/core/tests/api/test_services.py (4)

489-510: Good test coverage for can_activate without required services.

The test properly verifies that services without required dependencies can be activated by default.


513-552: Good coverage for required services constraint.

The test properly verifies both the can_activate flag and the 400 error response with a descriptive message when required services are not activated.


662-697: Good coverage for ProConnect mail domain requirement.

The test properly verifies that ProConnect services require a mail domain for activation.


795-814: Good test for IDP name resolution.

The test verifies that the idp_name property correctly resolves the IDP name from the operator's config based on the idp_id in subscription metadata. This aligns with the idp_name property implementation in src/backend/core/models.py (lines 896-907).

src/backend/core/tests/models/test_organization.py (1)

110-131: Good coverage for get_mail_domain_status logic.

The tests thoroughly verify the RPNT-based mail domain status resolution, including edge cases for empty RPNT, unmatched criteria, valid website with invalid email, and fully valid scenarios. The test at line 130 correctly uses different domains to verify the implementation uses adresse_messagerie_domain when both are valid.

src/backend/core/tests/api/test_organizations.py (1)

356-390: Good test extension for mail_domain and mail_domain_status.

The test correctly verifies that the API exposes mail_domain and mail_domain_status fields. The RPNT values ["1.1", "1.2", "2.1", "2.2", "2.3"] satisfy both website and email validation criteria, correctly resulting in MailDomainStatus.VALID and the domain extracted from adresse_messagerie.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (2)
src/backend/core/api/serializers.py (2)

287-319: Re‑evaluate exposing raw config on Operator and Service serializers – risk of leaking secrets.

Adding config to both OperatorSerializer and ServiceSerializer means any sensitive keys stored there (e.g. external_management_api_key from previous tests/fixtures) become part of the public API response. That’s effectively equivalent to logging secrets and violates the usual “no credentials in API payloads” expectation.

Unless you are certain that:

  • operator.config never contains secrets, and
  • service.config is strictly non‑sensitive display/configuration data,

you should either remove these fields from the API, or filter out sensitive keys before serializing.

Example: the safer option is to keep config internal on OperatorSerializer:

 class OperatorSerializer(serializers.ModelSerializer):
@@
     class Meta:
         model = models.Operator
-        fields = ["id", "name", "url", "scope", "is_active", "user_role", "config"]
+        fields = ["id", "name", "url", "scope", "is_active", "user_role"]
         read_only_fields = fields

If you truly need to expose some parts of config, consider a dedicated, sanitized representation (e.g. a SerializerMethodField that drops any keys containing “key”, “secret”, “token”, “credential”, etc.), and apply similar scrutiny to ServiceSerializer.config.


460-464: Guard against missing organization in serializer context when computing can_activate.

OrganizationServiceSerializer.to_representation directly accesses self.context["organization"]. If the serializer is ever instantiated without that context key (e.g. reused elsewhere or in tests), this will raise a KeyError and return a 500 instead of a controlled error.

Either:

  • Fail fast with a clear exception, if organization is mandatory, or
  • Gracefully handle the missing context (e.g. default can_activate to False).

Example (fail‑fast with a clear message):

     def to_representation(self, instance):
         """Convert the representation to the desired format."""
-        data = super().to_representation(instance)
-        data["can_activate"] = instance.can_activate(self.context["organization"])
+        if "organization" not in self.context:
+            raise ValueError(
+                "OrganizationServiceSerializer requires 'organization' in context "
+                "to compute can_activate"
+            )
+        data = super().to_representation(instance)
+        data["can_activate"] = instance.can_activate(self.context["organization"])
         return data
🧹 Nitpick comments (5)
src/backend/core/tests/api/test_services.py (1)

489-659: Good coverage of required‑services activation; consider minor tightening of assertions and reuse.

These four tests exercise the key paths for can_activate (no required services, missing required services, cross‑organization, and blocked‑but‑updatable subscriptions) and look correct.

Two small suggestions:

  • In test_api_organization_service_can_update_subscription_when_cannot_activate, after the second PATCH that only updates metadata, also assert that is_active remains False to fully pin down the behavior.
  • Given the repeated setup (user/operator/org, service + required service, configs), you could factor a small helper/fixture to keep these tests shorter and more focused on expectations.
src/backend/core/tests/api/test_services_proconnect.py (2)

12-143: ProConnect precondition tests are correct but inline comments are slightly misleading.

The three tests around “cannot activate if mail domain/IDP is not set” plus the “can activate when both are set” correctly assert:

  • can_activate is True (no required services restriction), and
  • activation is actually blocked/enabled via the subscription PATCH returning 400/201 based on mail‑domain/IDP presence.

However, the inline comments like “Check that the service cannot be activated by default” contradict the assert content["can_activate"] is True. To avoid confusion for future readers, consider rewording those comments to reflect that can_activate is purely about service dependencies, while ProConnect‑specific mail‑domain/IDP checks are enforced at subscription time.

Also, the setup for rpnt, adresse_messagerie, and site_internet is duplicated across several tests; a small factory helper or pytest fixture could DRY this up.


167-251: Subscription update / IDP change flow is well covered; confirm single‑request deactivate+IDP‑change behavior.

This test thoroughly checks that:

  • Changing idp_id on an active subscription is rejected.
  • Other metadata can still be updated while active.
  • After deactivation, idp_id can be changed.
  • You cannot both change idp_id and end up active in the same request.

One nuance: _validate_proconnect_subscription only blocks when the resulting is_active is True, so a single PATCH that sets {"is_active": False, "metadata": {"idp_id": "new"}} on an active subscription will be accepted. If product requirements truly want a strict “deactivate first, then change IDP in a separate call” policy, you may want an extra test for that case and to tighten the validator accordingly.

src/backend/core/api/serializers.py (2)

323-387: ProConnect idp_id update guardrails look correct; consider clarifying edge semantics.

The new _validate_proconnect_subscription and validate implementation correctly:

  • Scope the extra checks to ProConnect services.
  • Only run on updates (instance present).
  • Forbid changing metadata["idp_id"] when the resulting subscription is active.
  • Return a clear error structure on violation, matching the new tests.

Two details to double‑check:

  • A single PATCH that both deactivates and changes idp_id (e.g. {"is_active": False, "metadata": {"idp_id": "new"}}) will be allowed, because is_active is computed from attrs and becomes False. If you want “deactivate first, then change IDP” as two distinct steps, you’d need to treat “was active before this request and metadata.idp_id is changing” as invalid regardless of the new is_active value.
  • The error is raised as a top‑level serializers.ValidationError from validate, which is correct for DRF; no change needed there.

Behavior is consistent with the current tests; only adjust if product rules require stricter sequencing.


421-427: Organization mail_domain and mail_domain_status enrichment is fine; consider making them explicit fields.

to_representation cleanly injects mail_domain and mail_domain_status from get_mail_domain_status(), which matches how the tests and frontend are expected to consume this data.

Since these keys are not declared in Meta.fields, they won’t appear in your OpenAPI schema. If you rely on schema‑driven frontend or documentation, consider adding them as explicit read‑only fields (e.g. SerializerMethodFields) so they’re discoverable.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4b4bec7 and e82be60.

📒 Files selected for processing (4)
  • src/backend/core/api/serializers.py (6 hunks)
  • src/backend/core/tests/api/test_services.py (1 hunks)
  • src/backend/core/tests/api/test_services_proconnect.py (1 hunks)
  • src/frontend/src/features/ui/components/service/implementations/ProConnectServiceBlock.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/frontend/src/features/ui/components/service/implementations/ProConnectServiceBlock.tsx
🧰 Additional context used
📓 Path-based instructions (3)
src/backend/**/*.py

📄 CodeRabbit inference engine (.cursor/rules/django-python.mdc)

src/backend/**/*.py: Use Django’s ORM for database interactions; avoid raw SQL unless strictly necessary for performance
Use Django’s built-in user model and authentication framework for user management
Log expected and unexpected application actions with appropriate log levels
Respect log level semantics: DEBUG (diagnostics), INFO (normal events), WARN (unexpected but continuing), ERROR (functional failure)
Do not log sensitive information (authorization tokens, passwords, financial/health data, PII)

Files:

  • src/backend/core/tests/api/test_services.py
  • src/backend/core/tests/api/test_services_proconnect.py
  • src/backend/core/api/serializers.py
src/backend/**/tests/**/*.py

📄 CodeRabbit inference engine (.cursor/rules/django-python.mdc)

src/backend/**/tests/**/*.py: Use Django’s testing tools with pytest-django for reliable test suites
Unit tests should focus on a single use-case, keep assertions minimal, and cover all plausible scenarios

Files:

  • src/backend/core/tests/api/test_services.py
  • src/backend/core/tests/api/test_services_proconnect.py
src/backend/**/serializers.py

📄 CodeRabbit inference engine (.cursor/rules/django-python.mdc)

Use DRF serializers for JSON responses

Files:

  • src/backend/core/api/serializers.py
🧠 Learnings (2)
📚 Learning: 2025-10-03T23:40:57.889Z
Learnt from: CR
Repo: suitenumerique/st-deploycenter PR: 0
File: .cursor/rules/django-python.mdc:0-0
Timestamp: 2025-10-03T23:40:57.889Z
Learning: Applies to src/backend/**/serializers.py : Use DRF serializers for JSON responses

Applied to files:

  • src/backend/core/api/serializers.py
📚 Learning: 2025-10-03T23:40:57.889Z
Learnt from: CR
Repo: suitenumerique/st-deploycenter PR: 0
File: .cursor/rules/django-python.mdc:0-0
Timestamp: 2025-10-03T23:40:57.889Z
Learning: Applies to src/backend/**/*.py : Do not log sensitive information (authorization tokens, passwords, financial/health data, PII)

Applied to files:

  • src/backend/core/api/serializers.py
🧬 Code graph analysis (3)
src/backend/core/tests/api/test_services.py (1)
src/backend/core/factories.py (3)
  • OrganizationFactory (69-102)
  • ServiceFactory (116-132)
  • ServiceSubscriptionFactory (146-156)
src/backend/core/tests/api/test_services_proconnect.py (2)
src/backend/core/factories.py (3)
  • OrganizationFactory (69-102)
  • ServiceFactory (116-132)
  • ServiceSubscriptionFactory (146-156)
src/backend/core/models.py (1)
  • idp_name (897-908)
src/backend/core/api/serializers.py (1)
src/backend/core/models.py (3)
  • mail_domain (526-529)
  • get_mail_domain_status (531-558)
  • can_activate (770-779)
🔇 Additional comments (1)
src/backend/core/tests/api/test_services_proconnect.py (1)

145-165: idp_name test nicely validates model‑level behavior.

test_api_organization_proconnect_subscription_idp_name directly exercises the ServiceSubscription.idp_name logic against a realistic operator.config["idps"] payload and looks spot‑on. No changes needed.

@NathanVss NathanVss force-pushed the feat/pc-service branch 2 times, most recently from de82624 to 7912cbc Compare December 4, 2025 16:57
Copy link

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/frontend/src/features/i18n/translations.json (1)

23-50: Add missing English help_center block under organizations.services.

ProConnectServiceBlock uses t("organizations.services.help_center.label"), but the EN tree currently has only mail_domain under organizations.services (no help_center sibling), while FR defines help_center.label. This will cause EN users to see a raw key or fallback.

Add an EN help_center block mirroring FR:

       "values": {
         "users": "Users"
-          },
-          "mail_domain": {
+          },
+          "mail_domain": {
             "label": "NDD",
             "none": "None",
             "required": "Mail domain is required",
             "need_email_setup": "The mail domain is not valid. Please set it to the website domain.",
             "invalid": "The mail domain is not valid. Please set it to the website domain."
-          }
+          },
+          "help_center": {
+            "label": "Help center"
+          }

Optionally, you can also align EN need_email_setup vs invalid to mirror the clearer FR wording (one for “needs setup to mail provider”, one for “no valid domain found”).

♻️ Duplicate comments (7)
src/frontend/src/features/api/Repository.ts (1)

11-25: Align Operator.config typing with backend nullability.

Backend Operator.config is a nullable JSON field; the current type (config: { idps: OperatorIdp[]; }) still assumes it always exists and always has idps. Call sites are already using optional chaining (e.g. operator?.config?.idps), so the runtime is prepared for null, but the TS type doesn’t reflect that.

Consider loosening the type:

 export type Operator = {
   id: string;
   name: string;
   url: string;
   scope: string;
   is_active: boolean;
-  config: {
-    idps: OperatorIdp[];
-  };
+  config?: {
+    idps?: OperatorIdp[];
+  } | null;
 };

This will better match actual API responses and keeps call sites honest about null/undefined.

src/frontend/src/features/i18n/translations.json (1)

95-118: Fix typo in French IDP modal help text.

organizations.services.idp.modal.help still uses "Selectionnez un FI..."; in French it should be "Sélectionnez un FI..." with an accented é. This was flagged previously and is still present; please correct the string.

src/backend/core/models.py (3)

505-510: Defensive parsing for adresse_messagerie_domain

Even though adresse_messagerie is an EmailField and BaseModel.save enforces full_clean(), being defensive here avoids relying entirely on upstream validation and guards against malformed values introduced by future changes or data migrations.

You can safely handle unexpected formats without logging PII:

     @property
     def adresse_messagerie_domain(self):
         """Get the mail domain for the organization."""
         if not self.adresse_messagerie:
             return None
-        return self.adresse_messagerie.split("@")[1]
+        _local, sep, domain = self.adresse_messagerie.partition("@")
+        if not sep or not domain:
+            logger.warning(
+                "Invalid adresse_messagerie format for Organization %s",
+                self.id,
+            )
+            return None
+        return domain

This keeps logs free of email addresses while preventing potential IndexError. As per coding guidelines, avoid logging PII.


773-785: can_activate only requires one required service instead of all

The current implementation returns True as soon as any required service has an active subscription:

if not self.required_services.count():
    return True
return (
    self.required_services.all()
    .filter(subscriptions__organization=organization, subscriptions__is_active=True)
    .exists()
)

Given the semantics of “required services” and the stricter check in ServiceSubscription.validate_required_services, this should require all required services to be active for the organization, otherwise can_activate will disagree with model validation and the UI can show an activatable service that will later be rejected.

A safer implementation:

     def can_activate(self, organization: Organization):
         """
         Check if the service can be activated for the given organization.
         """
-        if not self.required_services.count():
-            return True
-        return (
-            self.required_services.all()
-            .filter(
-                subscriptions__organization=organization, subscriptions__is_active=True
-            )
-            .exists()
-        )
+        required_qs = self.required_services.all()
+        required_count = required_qs.count()
+        if not required_count:
+            return True
+
+        active_required_count = (
+            required_qs.filter(
+                subscriptions__organization=organization,
+                subscriptions__is_active=True,
+            )
+            .distinct()
+            .count()
+        )
+        return active_required_count == required_count

This keeps can_activate aligned with the subscription-level validation.


843-855: Guard against missing idps in operator.config when resolving idp_name

idp_name currently indexes self.operator.config["idps"] directly. If config exists but has no "idps" key, this will raise KeyError:

if not self.operator.config["idps"]:
    return None
for idp in self.operator.config["idps"]:
    ...

Use .get (or a local variable) to make this robust:

     @property
     def idp_name(self):
         """
         Get the name of the IDP for the ProConnect subscription.
         """
         if not self.metadata.get("idp_id"):
             return None
-        if not self.operator.config["idps"]:
-            return None
-        for idp in self.operator.config["idps"]:
+        idps = self.operator.config.get("idps") or []
+        if not idps:
+            return None
+        for idp in idps:
             if idp["id"] == self.metadata.get("idp_id"):
                 return idp["name"]
         return None

This matches how operator.config is used elsewhere and avoids runtime errors when configuration is incomplete. Based on learnings, avoid leaking config issues as 500s.

src/backend/core/api/serializers.py (2)

287-290: Re-evaluate exposing raw Operator.config in the public API

OperatorSerializer now includes the config field:

class Meta:
    model = models.Operator
    fields = ["id", "name", "url", "scope", "is_active", "user_role", "config"]

Earlier analysis showed that Operator.config may contain sensitive keys like API tokens (e.g. external_management_api_key). Exposing the entire JSON unfiltered to clients risks leaking credentials and violates the “no sensitive data” guideline.

Consider either:

  • Removing config from the public serializer and exposing only the specific, non-sensitive pieces needed by the frontend via dedicated fields, or
  • Implementing a filtered representation (e.g., whitelisted keys or filtering out anything whose key contains key, secret, token, credential).

Example whitelist approach:

class OperatorSerializer(serializers.ModelSerializer):
    user_role = serializers.SerializerMethodField(read_only=True)

    class Meta:
        model = models.Operator
        fields = ["id", "name", "url", "scope", "is_active", "user_role", "config"]
        read_only_fields = fields

    def to_representation(self, instance):
        data = super().to_representation(instance)
        config = data.get("config")
        if isinstance(config, dict):
            allowed_keys = {"idps"}  # adjust to what the UI truly needs
            data["config"] = {k: v for k, v in config.items() if k in allowed_keys}
        return data

As per coding guidelines and retrieved learnings, API responses should not include secrets or PII.


459-463: Avoid KeyError by validating organization in serializer context

OrganizationServiceSerializer.to_representation assumes self.context["organization"] is always present:

data["can_activate"] = instance.can_activate(self.context["organization"])

If the serializer is instantiated without that context key (e.g., reused in a different view), this will raise a KeyError and return 500 instead of a clear error.

Options:

  • Enforce the requirement explicitly:
    def to_representation(self, instance):
        """Convert the representation to the desired format."""
-        data = super().to_representation(instance)
-        data["can_activate"] = instance.can_activate(self.context["organization"])
+        data = super().to_representation(instance)
+        if "organization" not in self.context:
+            raise ValueError(
+                "OrganizationServiceSerializer requires 'organization' in context"
+            )
+        organization = self.context["organization"]
+        data["can_activate"] = instance.can_activate(organization)
         return data
  • Or, if organization is optional in some uses, fall back to a safe default (e.g., can_activate = False) when missing.
🧹 Nitpick comments (10)
bin/pytest (1)

9-9: Consider making verbosity configurable instead of hardcoded.

Hardcoding -vvv (maximum verbosity) can generate excessive output in CI/CD pipelines, making it harder to identify actual failures. Consider making verbosity configurable so developers can increase it when debugging while keeping CI logs concise.

You could default to -v or -vv and let users override via the "$@" passthrough:

-    pytest "$@" -s -vvv
+    pytest -s -v "$@"

Then developers can run ./bin/pytest -vvv when they need maximum verbosity.

src/backend/core/admin.py (1)

388-406: Consider using a ManyToMany-friendly widget for required_services.

Adding required_services directly into the main fieldset works, but the default multiple-select can become unwieldy as the number of services grows. You might want to enable filter_horizontal or filter_vertical on required_services in ServiceAdmin to keep the admin usable for large datasets.

src/frontend/src/features/ui/components/service/ServiceAttribute.tsx (1)

1-30: Default interactive based on presence of onClick to avoid silently disabled buttons.

Right now disabled={!interactive} means that if a caller passes onClick but forgets interactive, the button is disabled and the handler never fires.

You can make usage safer by normalizing the flag:

 export const ServiceAttribute = ({
     name,
     value,
     children,
     interactive,
     onClick,
   }: {
@@
-  }) => {
-    return (
+  }) => {
+    const isInteractive = interactive ?? !!onClick;
+    return (
       <div className="dc__service__attribute">
@@
           <button
             className="dc__service__attribute__top__value"
-            onClick={onClick}
-            disabled={!interactive}
+            onClick={onClick}
+            disabled={!isInteractive}
             type="button"
           >

Existing call sites that already set interactive keep their behavior; new ones get a sensible default.

src/frontend/src/features/ui/components/service/implementations/RegularServiceBlock.tsx (1)

1-13: RegularServiceBlock wiring is correct; consider explicitly threading base props.

The wrapper correctly delegates to useServiceBlock and ServiceBlock. To reduce coupling if useServiceBlock’s return shape changes later, you might explicitly pass the base props through:

-export const RegularServiceBlock = (props: {
-    service: Service;
-    organization: Organization;
-  }) => {
-    const blockProps = useServiceBlock(props.service, props.organization);
-    return <ServiceBlock {...blockProps} />;
-  };
+export const RegularServiceBlock = (props: {
+  service: Service;
+  organization: Organization;
+}) => {
+  const blockProps = useServiceBlock(props.service, props.organization);
+  return <ServiceBlock {...blockProps} service={props.service} organization={props.organization} />;
+};

Not mandatory, but it avoids a hidden dependency on useServiceBlock always returning service and organization.

src/backend/core/tests/models/test_organization.py (1)

20-181: Tests are correct; minor style cleanups for readability.

The test coverage for adresse_messagerie_domain, site_internet_domain, and get_mail_domain_status looks solid and matches the model implementation.

Two small, non-blocking polish suggestions:

  • Prefer is None over == None in assertions (adresse_messagerie_domain / site_internet_domain “empty” cases) to follow common Python style.
  • test_site_internet_domain_classic_trailing_query_params and test_site_internet_domain_classic_trailing_slash_query_params currently use the same URL (https://www.commune.fr/?param=value); if they’re meant to cover different shapes, adjust one URL, otherwise consider merging them.

Purely cosmetic, no need to block the PR.

src/frontend/src/features/ui/components/service/implementations/ProConnectServiceBlock.tsx (2)

102-115: Avoid submitting the form when closing the IDP modal

The modal close button is declared with type="submit" inside a <form>. Clicking it will submit the parent form and trigger react-hook-form validation even though the user only intends to close the modal.

Consider switching it to a plain button:

-  <Button type="submit" onClick={idpModal.close}>
+  <Button type="button" onClick={idpModal.close}>

This keeps validation tied to the explicit activation flow (canActivateSubscription) instead of a passive close action.


193-205: Harden external help-center links opened in a new tab

Both the <a> and the Button open help_center_url with target="_blank" but without rel="noopener noreferrer". For untrusted or operator-configurable URLs, this can allow the opened page to reach back into window.opener.

Suggestion:

-  <a target="_blank" href={props.service.config?.help_center_url}>
+  <a
+    target="_blank"
+    rel="noopener noreferrer"
+    href={props.service.config?.help_center_url}
+  >-  <Button
+  <Button
     color="tertiary"
     size="nano"
     href={props.service.config?.help_center_url}
-    target="_blank"
+    target="_blank"
+    rel="noopener noreferrer"
  />
src/frontend/src/pages/operators/[operator_id]/organizations/index.scss (1)

196-202: Remove duplicate margin-top declaration

.dc__service__warning sets margin-top twice (4px then 16px); only the last one is effective, so the first is dead code.

You can safely drop the earlier declaration to avoid confusion:

 .dc__service__warning {
   color: var(--c--theme--colors--warning-500);
   font-size: 12px;
   font-weight: 400;
-  margin-top: 4px;
   margin-top: 16px;
 }
src/backend/core/api/serializers.py (2)

305-320: Double-check that exposing Service.config is safe

ServiceSerializer now returns config to clients. Depending on how Service.config is populated, this may be fine (purely public display/configuration data) or could leak operational details (e.g., backend URLs, tokens).

If there is any chance config holds sensitive information, mirror the approach suggested for OperatorSerializer: either restrict what goes into config, or filter its representation to a safe subset before sending it to clients.


419-425: Be explicit about serializing mail_domain_status

OrganizationSerializer.to_representation injects:

mail_domain, mail_domain_status = instance.get_mail_domain_status()
data["mail_domain"] = mail_domain
data["mail_domain_status"] = mail_domain_status

Because MailDomainStatus is a StrEnum, JSON rendering will work (values are string-like). For clarity and to decouple the API from the enum implementation, consider serializing the raw value:

 mail_domain, mail_domain_status = instance.get_mail_domain_status()
 data["mail_domain"] = mail_domain
- data["mail_domain_status"] = mail_domain_status
+ data["mail_domain_status"] = mail_domain_status.value

This makes the public API field a plain string ("valid", "need_email_setup", "invalid") regardless of internal enum representation.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e82be60 and 7912cbc.

⛔ Files ignored due to path filters (1)
  • src/frontend/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (23)
  • bin/pytest (1 hunks)
  • src/backend/core/admin.py (2 hunks)
  • src/backend/core/api/serializers.py (6 hunks)
  • src/backend/core/api/viewsets/service.py (2 hunks)
  • src/backend/core/migrations/0009_service_required_services.py (1 hunks)
  • src/backend/core/models.py (6 hunks)
  • src/backend/core/tests/api/test_operators.py (2 hunks)
  • src/backend/core/tests/api/test_organizations.py (3 hunks)
  • src/backend/core/tests/api/test_services.py (1 hunks)
  • src/backend/core/tests/api/test_services_proconnect.py (1 hunks)
  • src/backend/core/tests/models/test_organization.py (1 hunks)
  • src/backend/core/tests/utils.py (1 hunks)
  • src/backend/core/webhooks.py (2 hunks)
  • src/frontend/package.json (1 hunks)
  • src/frontend/src/features/api/Repository.ts (3 hunks)
  • src/frontend/src/features/i18n/translations.json (2 hunks)
  • src/frontend/src/features/ui/components/service/ServiceAttribute.tsx (1 hunks)
  • src/frontend/src/features/ui/components/service/ServiceBlock.tsx (1 hunks)
  • src/frontend/src/features/ui/components/service/ServiceBlockDispatcher.tsx (1 hunks)
  • src/frontend/src/features/ui/components/service/implementations/ProConnectServiceBlock.tsx (1 hunks)
  • src/frontend/src/features/ui/components/service/implementations/RegularServiceBlock.tsx (1 hunks)
  • src/frontend/src/pages/operators/[operator_id]/organizations/[id].tsx (3 hunks)
  • src/frontend/src/pages/operators/[operator_id]/organizations/index.scss (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (10)
  • src/frontend/src/features/ui/components/service/ServiceBlockDispatcher.tsx
  • src/backend/core/api/viewsets/service.py
  • src/backend/core/webhooks.py
  • src/frontend/src/features/ui/components/service/ServiceBlock.tsx
  • src/backend/core/tests/utils.py
  • src/backend/core/tests/api/test_operators.py
  • src/backend/core/tests/api/test_services_proconnect.py
  • src/backend/core/tests/api/test_organizations.py
  • src/backend/core/migrations/0009_service_required_services.py
  • src/frontend/package.json
🧰 Additional context used
📓 Path-based instructions (5)
src/backend/**/*.py

📄 CodeRabbit inference engine (.cursor/rules/django-python.mdc)

src/backend/**/*.py: Use Django’s ORM for database interactions; avoid raw SQL unless strictly necessary for performance
Use Django’s built-in user model and authentication framework for user management
Log expected and unexpected application actions with appropriate log levels
Respect log level semantics: DEBUG (diagnostics), INFO (normal events), WARN (unexpected but continuing), ERROR (functional failure)
Do not log sensitive information (authorization tokens, passwords, financial/health data, PII)

Files:

  • src/backend/core/tests/api/test_services.py
  • src/backend/core/models.py
  • src/backend/core/admin.py
  • src/backend/core/tests/models/test_organization.py
  • src/backend/core/api/serializers.py
src/backend/**/tests/**/*.py

📄 CodeRabbit inference engine (.cursor/rules/django-python.mdc)

src/backend/**/tests/**/*.py: Use Django’s testing tools with pytest-django for reliable test suites
Unit tests should focus on a single use-case, keep assertions minimal, and cover all plausible scenarios

Files:

  • src/backend/core/tests/api/test_services.py
  • src/backend/core/tests/models/test_organization.py
src/backend/**/{models,forms,views}.py

📄 CodeRabbit inference engine (.cursor/rules/django-python.mdc)

Keep business logic in models and forms; keep views thin and focused on request handling

Files:

  • src/backend/core/models.py
src/backend/**/models.py

📄 CodeRabbit inference engine (.cursor/rules/django-python.mdc)

Define appropriate database indexes and optimize query patterns via model Meta and QuerySets

Files:

  • src/backend/core/models.py
src/backend/**/serializers.py

📄 CodeRabbit inference engine (.cursor/rules/django-python.mdc)

Use DRF serializers for JSON responses

Files:

  • src/backend/core/api/serializers.py
🧠 Learnings (3)
📚 Learning: 2025-10-03T23:40:57.889Z
Learnt from: CR
Repo: suitenumerique/st-deploycenter PR: 0
File: .cursor/rules/django-python.mdc:0-0
Timestamp: 2025-10-03T23:40:57.889Z
Learning: Applies to src/backend/**/tests/**/*.py : Use Django’s testing tools with pytest-django for reliable test suites

Applied to files:

  • bin/pytest
📚 Learning: 2025-10-03T23:40:57.889Z
Learnt from: CR
Repo: suitenumerique/st-deploycenter PR: 0
File: .cursor/rules/django-python.mdc:0-0
Timestamp: 2025-10-03T23:40:57.889Z
Learning: Applies to src/backend/**/serializers.py : Use DRF serializers for JSON responses

Applied to files:

  • src/backend/core/api/serializers.py
📚 Learning: 2025-10-03T23:40:57.889Z
Learnt from: CR
Repo: suitenumerique/st-deploycenter PR: 0
File: .cursor/rules/django-python.mdc:0-0
Timestamp: 2025-10-03T23:40:57.889Z
Learning: Applies to src/backend/**/*.py : Do not log sensitive information (authorization tokens, passwords, financial/health data, PII)

Applied to files:

  • src/backend/core/api/serializers.py
🧬 Code graph analysis (5)
src/frontend/src/pages/operators/[operator_id]/organizations/[id].tsx (2)
src/frontend/src/features/api/Repository.ts (1)
  • SERVICE_TYPE_PROCONNECT (56-56)
src/frontend/src/features/ui/components/service/ServiceBlockDispatcher.tsx (1)
  • ServiceBlockDispatcher (9-17)
src/backend/core/tests/api/test_services.py (1)
src/backend/core/factories.py (8)
  • UserFactory (16-27)
  • OperatorFactory (47-55)
  • UserOperatorRoleFactory (58-66)
  • OrganizationFactory (69-102)
  • OperatorOrganizationRoleFactory (105-113)
  • ServiceFactory (116-132)
  • OperatorServiceConfigFactory (135-143)
  • ServiceSubscriptionFactory (146-156)
src/frontend/src/features/ui/components/service/implementations/RegularServiceBlock.tsx (2)
src/frontend/src/features/api/Repository.ts (2)
  • Service (58-74)
  • Organization (33-45)
src/frontend/src/features/ui/components/service/ServiceBlock.tsx (2)
  • useServiceBlock (11-53)
  • ServiceBlock (55-189)
src/backend/core/models.py (1)
src/frontend/src/features/api/Repository.ts (2)
  • Organization (33-45)
  • ServiceSubscription (47-54)
src/backend/core/tests/models/test_organization.py (2)
src/backend/core/models.py (5)
  • Organization (329-564)
  • adresse_messagerie_domain (506-510)
  • site_internet_domain (513-529)
  • get_mail_domain_status (537-564)
  • MailDomainStatus (335-342)
src/backend/core/factories.py (1)
  • OrganizationFactory (69-102)
🔇 Additional comments (5)
src/backend/core/admin.py (1)

267-272: Operator admin fieldsets reorganization looks good.

Moving config into its own “Configuration” fieldset is consistent with the rest of the admin and keeps the financial info section clean.

src/backend/core/tests/api/test_services.py (1)

490-660: Strong coverage of required_services activation semantics.

The new tests around can_activate and required_services nicely cover:

  • base case (no required services → can_activate=True),
  • blocked activation with clear 400 error when a required service is inactive,
  • allowing metadata-only updates when activation is blocked,
  • ensuring activation depends on the required service being active for the same organization.

The setups are consistent with the existing test style and should guard regressions in the activation rules.

src/frontend/src/features/api/Repository.ts (1)

27-74: New mail-domain and service typing matches backend usage.

  • MailDomainStatus plus Organization.mail_domain / mail_domain_status align with the backend enum and serializer behaviour.
  • SERVICE_TYPE_PROCONNECT and the added Service fields (type, can_activate, config.help_center_url) are consistent with how the UI dispatches and renders ProConnect vs regular services.

These additions look coherent and should make downstream components’ props safer and clearer.

src/frontend/src/pages/operators/[operator_id]/organizations/[id].tsx (1)

7-15: Service ordering + dispatcher integration look correct.

  • servicesOrdered correctly promotes ProConnect (type === SERVICE_TYPE_PROCONNECT) to the top and then sorts the rest by display_priority descending, with missing priorities naturally falling to the end.
  • The map now delegates to ServiceBlockDispatcher with the full service and organization objects, which plugs into the new ProConnect/regular block split without changing the page’s overall layout.
  • The loading guard (isServicesLoading || !organization || !operator) keeps the UI from rendering blocks before essential data is present.

All of this is consistent with the new service model and UI components.

Also applies to: 35-49, 107-118

src/backend/core/api/serializers.py (1)

326-371: ProConnect IDP immutability validation looks good; ensure it’s wired to update flows only

The _validate_proconnect_subscription / validate combo correctly prevents changing metadata.idp_id on an already-active ProConnect subscription by comparing the old and new values and raising a field-scoped ValidationError.

A couple of minor points to keep in mind:

  • This logic only runs when self.instance is set (i.e., updates), which is appropriate; new subscriptions are governed by the model-level validate_proconnect_subscription.
  • The guard if not isinstance(new_metadata, dict) or not "idp_id" in new_metadata: intentionally skips cases where metadata changes don’t touch idp_id, which seems consistent with the requirement.

No changes strictly required here; just ensure your API endpoints use this serializer for the flows where IDP updates are allowed/forbidden so the constraint is actually enforced.

We it fails it was quite difficult to understand where
the error come from.
Let's add the config of the operator to the serializer,
we need to be able to grab the idp informations from the
frontend.
The frontend will start to put some data in this field,
so let's add it to the serializer so the frontend can
get its value.
We need to be able to allow activation on services only
if some others are activated.
This is a special service which behave is a quite
specific way. So let's implements those checks.
In order to provide the domain mail to the ProConnect card,
we need to implement email extraction based on the RPNT data.
We need to get more information from the outcoming webhooks.
Let's add the previous commits attribute into django admin.
We'll need RHF to perform form-like validation on cards.
Let's adjust the types to the recent backend changes.
As of now, the service cards are not always generic. Let's implement
a new approach that allow for deep customization and reusability of
default components.
Update the translation based on the previous commits.
Copy link

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

♻️ Duplicate comments (6)
src/backend/core/models.py (2)

773-785: Logic bug: can_activate incorrectly returns True if ANY required service is active instead of ALL.

The current implementation uses .exists(), which returns True as soon as at least one required service has an active subscription. This means can_activate will incorrectly report True even when some required services are missing.

Apply this fix to check that ALL required services are active:

 def can_activate(self, organization: Organization):
     """
     Check if the service can be activated for the given organization.
     """
-    if not self.required_services.count():
+    required_count = self.required_services.count()
+    if not required_count:
         return True
-    return (
-        self.required_services.all()
-        .filter(
-            subscriptions__organization=organization, subscriptions__is_active=True
-        )
-        .exists()
-    )
+    active_count = (
+        self.required_services.filter(
+            subscriptions__organization=organization,
+            subscriptions__is_active=True,
+        )
+        .distinct()
+        .count()
+    )
+    return active_count == required_count

843-855: Potential KeyError when accessing operator.config["idps"].

Line 850 directly accesses self.operator.config["idps"] without using .get(). If the config dict exists but lacks the idps key, this will raise KeyError.

Apply this diff:

 @property
 def idp_name(self):
     """
     Get the name of the IDP for the ProConnect subscription.
     """
     if not self.metadata.get("idp_id"):
         return None
-    if not self.operator.config["idps"]:
+    idps = self.operator.config.get("idps")
+    if not idps:
         return None
-    for idp in self.operator.config["idps"]:
+    for idp in idps:
         if idp["id"] == self.metadata.get("idp_id"):
             return idp["name"]
     return None
src/backend/core/api/serializers.py (1)

459-463: Potential KeyError if organization is missing from serializer context.

Line 462 directly accesses self.context["organization"] without checking if the key exists. If the serializer is instantiated without this context, a KeyError will be raised.

Add validation to ensure clear error messaging:

 def to_representation(self, instance):
     """Convert the representation to the desired format."""
     data = super().to_representation(instance)
+    if "organization" not in self.context:
+        raise ValueError("OrganizationServiceSerializer requires 'organization' in context")
+    organization = self.context["organization"]
-    data["can_activate"] = instance.can_activate(self.context["organization"])
+    data["can_activate"] = instance.can_activate(organization)
     return data
src/frontend/src/features/i18n/translations.json (1)

29-53: Translations for ProConnect are complete; one minor FR typo remains

The new EN/FR keys for services.cannot_activate, services.idp, services.mail_domain, and services.help_center look consistent with the UI usage.

Minor nit: in the French block, organizations.services.idp.modal.help still has Selectionnez un FI pour l'organisation. — you probably want Sélectionnez with the accent:

-            "help": "Selectionnez un FI pour l'organisation.",
+            "help": "Sélectionnez un FI pour l'organisation.",

Purely cosmetic, feel free to adjust when convenient.

Also applies to: 98-121

src/frontend/src/features/ui/components/service/implementations/ProConnectServiceBlock.tsx (1)

68-76: Add a defensive guard to prevent IDP changes on active subscriptions

UI-wise, interactive={!props.service.subscription?.is_active} disables the attribute button, but saveIdpChange will still happily send an IDP update if onChange is ever triggered (e.g. via a future refactor or programmatic call), which conflicts with the “cannot change IDP once active” rule.

A lightweight guard keeps behavior consistent with backend validation:

-  const saveIdpChange = (idp?: OperatorIdp) => {
+  const saveIdpChange = (idp?: OperatorIdp) => {
+    if (props.service.subscription?.is_active) {
+      return;
+    }
     const data: Partial<ServiceSubscription> = {
       metadata: { idp_id: idp?.id },
     };
     if (!props.service.subscription) {
       data.is_active = false;
     }
     blockProps.onChangeSubscription(data);
   };

This ensures active subscriptions can’t get their IDP mutated accidentally, even if the UI wiring changes later.

Also applies to: 138-144

src/frontend/src/features/api/Repository.ts (1)

11-24: Revisit Operator.config.idps typing to match nullable backend config

Backend Operator.config is nullable and may not always contain an idps array, but the type here assumes a non‑null { idps: OperatorIdp[] }. This, combined with usages like operator?.config.idps?.map(...), can lead to runtime errors when config is null or missing idps.

Consider aligning the type and updating call sites to use optional chaining/defaults:

 export type Operator = {
   id: string;
   name: string;
   url: string;
   scope: string;
   is_active: boolean;
-  config: {
-    idps: OperatorIdp[];
-  };
+  config: {
+    idps?: OperatorIdp[];
+  } | null;
 };

And then in consumers (e.g. ProConnectServiceBlock):

-const getIdp = (idp_id: string) => {
-  return operator?.config.idps?.find((idp) => idp.id == idp_id);
-};
+const getIdp = (idp_id: string) => {
+  return operator?.config?.idps?.find((idp) => idp.id === idp_id);
+};
...
-operator?.config.idps?.map((idp) => ...)
+operator?.config?.idps?.map((idp) => ...) ?? []

This keeps the frontend model honest with the backend and prevents crashes when config is absent.

🧹 Nitpick comments (3)
src/backend/core/api/serializers.py (1)

349-349: Improve readability of membership check.

Use not in operator for clearer intent.

-        if not isinstance(new_metadata, dict) or not "idp_id" in new_metadata:
+        if not isinstance(new_metadata, dict) or "idp_id" not in new_metadata:
             return
src/frontend/src/features/ui/components/service/implementations/ProConnectServiceBlock.tsx (1)

51-53: Minor cleanups: no-op submit handler and external link rel

Two small nits you may want to address:

  1. onSubmit is a memoized no-op solely to satisfy <form onSubmit={handleSubmit(onSubmit)}>. You can simplify without useMemo:
-  const onSubmit = useMemo(() => {
-    return () => void 0;
-  }, []);
+  const onSubmit = () => void 0;
  1. The help-center link opens in a new tab but lacks a rel attribute; adding rel="noreferrer noopener" is a safer default when target="_blank":
-              <a target="_blank" href={props.service.config?.help_center_url}>
+              <a
+                target="_blank"
+                rel="noreferrer noopener"
+                href={props.service.config?.help_center_url}
+              >
                 {t("organizations.services.help_center.label")}
               </a>

Both are non-functional tweaks but improve clarity and security posture.

Also applies to: 193-205

src/frontend/src/pages/operators/[operator_id]/organizations/index.scss (1)

189-257: SCSS additions look good; fix duplicate margin-top on warning

The new classes for service attributes/errors/warnings align with the new components and use consistent theming.

One small issue: .dc__service__warning defines margin-top twice, with the latter winning:

.dc__service__warning {
  ...
  margin-top: 4px;
  margin-top: 16px;
}

Drop one (probably the 4px) to avoid confusion:

 .dc__service__warning {
   color: var(--c--theme--colors--warning-500);
   font-size: 12px;
   font-weight: 400;
-  margin-top: 4px;
-  margin-top: 16px;
+  margin-top: 16px;
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7912cbc and 6a07e3d.

⛔ Files ignored due to path filters (1)
  • src/frontend/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (22)
  • src/backend/core/admin.py (2 hunks)
  • src/backend/core/api/serializers.py (6 hunks)
  • src/backend/core/api/viewsets/service.py (2 hunks)
  • src/backend/core/migrations/0009_service_required_services.py (1 hunks)
  • src/backend/core/models.py (6 hunks)
  • src/backend/core/tests/api/test_operators.py (2 hunks)
  • src/backend/core/tests/api/test_organizations.py (3 hunks)
  • src/backend/core/tests/api/test_services.py (1 hunks)
  • src/backend/core/tests/api/test_services_proconnect.py (1 hunks)
  • src/backend/core/tests/models/test_organization.py (1 hunks)
  • src/backend/core/tests/utils.py (1 hunks)
  • src/backend/core/webhooks.py (2 hunks)
  • src/frontend/package.json (1 hunks)
  • src/frontend/src/features/api/Repository.ts (3 hunks)
  • src/frontend/src/features/i18n/translations.json (2 hunks)
  • src/frontend/src/features/ui/components/service/ServiceAttribute.tsx (1 hunks)
  • src/frontend/src/features/ui/components/service/ServiceBlock.tsx (1 hunks)
  • src/frontend/src/features/ui/components/service/ServiceBlockDispatcher.tsx (1 hunks)
  • src/frontend/src/features/ui/components/service/implementations/ProConnectServiceBlock.tsx (1 hunks)
  • src/frontend/src/features/ui/components/service/implementations/RegularServiceBlock.tsx (1 hunks)
  • src/frontend/src/pages/operators/[operator_id]/organizations/[id].tsx (3 hunks)
  • src/frontend/src/pages/operators/[operator_id]/organizations/index.scss (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (11)
  • src/frontend/src/features/ui/components/service/ServiceAttribute.tsx
  • src/backend/core/tests/utils.py
  • src/frontend/src/features/ui/components/service/ServiceBlock.tsx
  • src/backend/core/webhooks.py
  • src/backend/core/migrations/0009_service_required_services.py
  • src/backend/core/api/viewsets/service.py
  • src/frontend/package.json
  • src/backend/core/tests/api/test_services_proconnect.py
  • src/frontend/src/features/ui/components/service/implementations/RegularServiceBlock.tsx
  • src/backend/core/tests/api/test_organizations.py
  • src/backend/core/admin.py
🧰 Additional context used
📓 Path-based instructions (5)
src/backend/**/*.py

📄 CodeRabbit inference engine (.cursor/rules/django-python.mdc)

src/backend/**/*.py: Use Django’s ORM for database interactions; avoid raw SQL unless strictly necessary for performance
Use Django’s built-in user model and authentication framework for user management
Log expected and unexpected application actions with appropriate log levels
Respect log level semantics: DEBUG (diagnostics), INFO (normal events), WARN (unexpected but continuing), ERROR (functional failure)
Do not log sensitive information (authorization tokens, passwords, financial/health data, PII)

Files:

  • src/backend/core/tests/api/test_services.py
  • src/backend/core/tests/api/test_operators.py
  • src/backend/core/tests/models/test_organization.py
  • src/backend/core/api/serializers.py
  • src/backend/core/models.py
src/backend/**/tests/**/*.py

📄 CodeRabbit inference engine (.cursor/rules/django-python.mdc)

src/backend/**/tests/**/*.py: Use Django’s testing tools with pytest-django for reliable test suites
Unit tests should focus on a single use-case, keep assertions minimal, and cover all plausible scenarios

Files:

  • src/backend/core/tests/api/test_services.py
  • src/backend/core/tests/api/test_operators.py
  • src/backend/core/tests/models/test_organization.py
src/backend/**/serializers.py

📄 CodeRabbit inference engine (.cursor/rules/django-python.mdc)

Use DRF serializers for JSON responses

Files:

  • src/backend/core/api/serializers.py
src/backend/**/{models,forms,views}.py

📄 CodeRabbit inference engine (.cursor/rules/django-python.mdc)

Keep business logic in models and forms; keep views thin and focused on request handling

Files:

  • src/backend/core/models.py
src/backend/**/models.py

📄 CodeRabbit inference engine (.cursor/rules/django-python.mdc)

Define appropriate database indexes and optimize query patterns via model Meta and QuerySets

Files:

  • src/backend/core/models.py
🧠 Learnings (2)
📚 Learning: 2025-10-03T23:40:57.889Z
Learnt from: CR
Repo: suitenumerique/st-deploycenter PR: 0
File: .cursor/rules/django-python.mdc:0-0
Timestamp: 2025-10-03T23:40:57.889Z
Learning: Applies to src/backend/**/serializers.py : Use DRF serializers for JSON responses

Applied to files:

  • src/backend/core/api/serializers.py
📚 Learning: 2025-10-03T23:40:57.889Z
Learnt from: CR
Repo: suitenumerique/st-deploycenter PR: 0
File: .cursor/rules/django-python.mdc:0-0
Timestamp: 2025-10-03T23:40:57.889Z
Learning: Applies to src/backend/**/*.py : Do not log sensitive information (authorization tokens, passwords, financial/health data, PII)

Applied to files:

  • src/backend/core/api/serializers.py
🧬 Code graph analysis (7)
src/frontend/src/features/ui/components/service/ServiceBlockDispatcher.tsx (4)
src/frontend/src/pages/operators/[operator_id]/organizations/[id].tsx (1)
  • Organization (16-122)
src/frontend/src/features/api/Repository.ts (1)
  • SERVICE_TYPE_PROCONNECT (56-56)
src/frontend/src/features/ui/components/service/implementations/ProConnectServiceBlock.tsx (1)
  • ProConnectServiceBlock (41-211)
src/frontend/src/features/ui/components/service/implementations/RegularServiceBlock.tsx (1)
  • RegularServiceBlock (7-13)
src/backend/core/tests/models/test_organization.py (2)
src/backend/core/models.py (5)
  • Organization (329-564)
  • adresse_messagerie_domain (506-510)
  • site_internet_domain (513-529)
  • get_mail_domain_status (537-564)
  • MailDomainStatus (335-342)
src/backend/core/factories.py (1)
  • OrganizationFactory (69-102)
src/frontend/src/features/api/Repository.ts (1)
src/backend/core/models.py (4)
  • MailDomainStatus (335-342)
  • Organization (329-564)
  • Service (655-785)
  • ServiceSubscription (788-917)
src/frontend/src/features/ui/components/service/implementations/ProConnectServiceBlock.tsx (5)
src/frontend/src/features/layouts/components/GlobalLayout.tsx (1)
  • useOperatorContext (57-63)
src/frontend/src/features/ui/components/service/ServiceBlock.tsx (2)
  • useServiceBlock (11-53)
  • ServiceBlock (55-189)
src/frontend/src/features/api/Repository.ts (1)
  • OperatorIdp (11-14)
src/frontend/src/features/ui/components/service/ServiceAttribute.tsx (1)
  • ServiceAttribute (1-30)
src/backend/core/models.py (1)
  • MailDomainStatus (335-342)
src/backend/core/api/serializers.py (1)
src/backend/core/models.py (3)
  • mail_domain (532-535)
  • get_mail_domain_status (537-564)
  • can_activate (773-785)
src/frontend/src/pages/operators/[operator_id]/organizations/[id].tsx (2)
src/frontend/src/features/api/Repository.ts (1)
  • SERVICE_TYPE_PROCONNECT (56-56)
src/frontend/src/features/ui/components/service/ServiceBlockDispatcher.tsx (1)
  • ServiceBlockDispatcher (9-17)
src/backend/core/models.py (1)
src/frontend/src/features/api/Repository.ts (2)
  • Organization (33-45)
  • ServiceSubscription (47-54)
🔇 Additional comments (3)
src/frontend/src/features/api/Repository.ts (1)

27-74: Mail-domain and service extensions look consistent with backend model

The MailDomainStatus enum, Organization.mail_domain/mail_domain_status, SERVICE_TYPE_PROCONNECT, and the extended Service fields (type, can_activate, config.help_center_url) line up with the described backend behavior and are typed sensibly for the consuming UI components.

src/frontend/src/pages/operators/[operator_id]/organizations/[id].tsx (1)

35-49: Service ordering and dispatcher integration look solid

servicesOrdered defensively handles services?.results, promotes ProConnect first, then orders by operator_config.display_priority (descending with undefineds last), and the render path correctly waits for services/organization/operator before mapping through ServiceBlockDispatcher. This matches the intended UX without obvious edge‑case issues.

Also applies to: 108-116

src/frontend/src/features/ui/components/service/ServiceBlockDispatcher.tsx (1)

1-17: Dispatcher abstraction is clear and type-safe

The ServiceBlockDispatcher cleanly routes between ProConnectServiceBlock and RegularServiceBlock based on service.type and reuses shared Service/Organization types, which keeps the per-service branching centralized.

@NathanVss NathanVss requested a review from sylvinus December 4, 2025 18:05
We do not want to expose all params, we just want to cherry-pick.
Copy link

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/backend/core/api/viewsets/service.py (1)

181-186: Handle DoesNotExist exceptions for invalid foreign key lookups.

Lines 181-185 perform direct .get() calls for Organization, Service, and Operator without exception handling. Invalid IDs will raise DoesNotExist exceptions, causing 500 errors instead of proper 404 responses.

+from django.shortcuts import get_object_or_404
+
     def partial_update(self, request, *args, **kwargs):
         """
         Partially update or create the subscription for the operator-organization-service triple.
         Creates the subscription if it doesn't exist (upsert behavior).
         """
         queryset = self.get_queryset()
         subscription = queryset.first()
 
-        organization = models.Organization.objects.get(
-            id=self.kwargs["organization_id"]
+        organization = get_object_or_404(
+            models.Organization, id=self.kwargs["organization_id"]
         )
-        service = models.Service.objects.get(id=self.kwargs["service_id"])
-        operator = models.Operator.objects.get(id=self.kwargs["operator_id"])
+        service = get_object_or_404(models.Service, id=self.kwargs["service_id"])
+        operator = get_object_or_404(models.Operator, id=self.kwargs["operator_id"])
♻️ Duplicate comments (2)
src/backend/core/api/serializers.py (1)

476-480: Handle missing organization in serializer context gracefully.

Direct access to self.context["organization"] raises KeyError if the serializer is instantiated without the required context. The viewset at lines 101-104 of service.py provides this context, but the serializer should be defensive.

     def to_representation(self, instance):
         """Convert the representation to the desired format."""
         data = super().to_representation(instance)
-        data["can_activate"] = instance.can_activate(self.context["organization"])
+        organization = self.context.get("organization")
+        if not organization:
+            raise ValueError("OrganizationServiceSerializer requires 'organization' in context")
+        data["can_activate"] = instance.can_activate(organization)
         return data
src/backend/core/api/viewsets/service.py (1)

98-105: Handle DoesNotExist exception for invalid organization ID.

The direct Organization.objects.get() call raises Organization.DoesNotExist for invalid IDs, resulting in a 500 error instead of a proper 404 response. The permission class only validates that organization_id exists in kwargs, not that it's valid in the database.

+from django.shortcuts import get_object_or_404
+
     def get_serializer_context(self):
-        """Add operator_id and organization to serializer context."""
+        """Add operator_id and organization to serializer context."""
         context = super().get_serializer_context()
         context["operator_id"] = self.kwargs["operator_id"]
-        organization = models.Organization.objects.get(
-            id=self.kwargs["organization_id"]
+        organization = get_object_or_404(
+            models.Organization, id=self.kwargs["organization_id"]
         )
         context["organization"] = organization
         return context
🧹 Nitpick comments (1)
src/backend/core/api/serializers.py (1)

366-366: Use not in operator for better Python style.

Replace not "idp_id" in new_metadata with the more Pythonic "idp_id" not in new_metadata.

-        if not isinstance(new_metadata, dict) or not "idp_id" in new_metadata:
+        if not isinstance(new_metadata, dict) or "idp_id" not in new_metadata:
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6a07e3d and 5b79ee8.

📒 Files selected for processing (4)
  • src/backend/core/api/serializers.py (6 hunks)
  • src/backend/core/api/viewsets/service.py (2 hunks)
  • src/backend/core/tests/api/test_operators.py (3 hunks)
  • src/backend/core/tests/api/test_services.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/backend/core/tests/api/test_operators.py
  • src/backend/core/tests/api/test_services.py
🧰 Additional context used
📓 Path-based instructions (2)
src/backend/**/*.py

📄 CodeRabbit inference engine (.cursor/rules/django-python.mdc)

src/backend/**/*.py: Use Django’s ORM for database interactions; avoid raw SQL unless strictly necessary for performance
Use Django’s built-in user model and authentication framework for user management
Log expected and unexpected application actions with appropriate log levels
Respect log level semantics: DEBUG (diagnostics), INFO (normal events), WARN (unexpected but continuing), ERROR (functional failure)
Do not log sensitive information (authorization tokens, passwords, financial/health data, PII)

Files:

  • src/backend/core/api/viewsets/service.py
  • src/backend/core/api/serializers.py
src/backend/**/serializers.py

📄 CodeRabbit inference engine (.cursor/rules/django-python.mdc)

Use DRF serializers for JSON responses

Files:

  • src/backend/core/api/serializers.py
🧠 Learnings (2)
📚 Learning: 2025-10-03T23:40:57.889Z
Learnt from: CR
Repo: suitenumerique/st-deploycenter PR: 0
File: .cursor/rules/django-python.mdc:0-0
Timestamp: 2025-10-03T23:40:57.889Z
Learning: Applies to src/backend/**/serializers.py : Use DRF serializers for JSON responses

Applied to files:

  • src/backend/core/api/serializers.py
📚 Learning: 2025-10-03T23:40:57.889Z
Learnt from: CR
Repo: suitenumerique/st-deploycenter PR: 0
File: .cursor/rules/django-python.mdc:0-0
Timestamp: 2025-10-03T23:40:57.889Z
Learning: Applies to src/backend/**/*.py : Do not log sensitive information (authorization tokens, passwords, financial/health data, PII)

Applied to files:

  • src/backend/core/api/serializers.py
🧬 Code graph analysis (1)
src/backend/core/api/serializers.py (1)
src/backend/core/models.py (15)
  • Meta (62-63)
  • Meta (174-177)
  • Meta (227-231)
  • Meta (318-323)
  • Meta (488-500)
  • Meta (596-604)
  • Meta (640-649)
  • Meta (753-760)
  • Meta (832-838)
  • Meta (979-997)
  • Meta (1062-1069)
  • Operator (188-282)
  • ServiceSubscription (788-917)
  • mail_domain (532-535)
  • get_mail_domain_status (537-564)
🔇 Additional comments (2)
src/backend/core/api/serializers.py (2)

286-307: Good implementation of config whitelisting.

The SerializerMethodField with explicit whitelist (["idps"]) effectively addresses the security concerns raised in previous reviews by preventing exposure of sensitive keys like external_management_api_key.


314-337: LGTM! Consistent security pattern.

The whitelist approach for Service config mirrors the OperatorSerializer implementation and appropriately exposes only non-sensitive configuration (["help_center_url"]).

The optimistic implementation was made for the activation
but not when disabling.
Added error handling for the case when an organization is not found in the database, raising a NotFound exception with a clear message.
Added a check in the OrganizationServiceSerializer to ensure that the 'organization' key is present in the context, raising a ValueError if it is missing. This improves error handling and ensures proper usage of the serializer.
Added a step to create a Docker network in both the linting and testing jobs of the CI workflow. This ensures the necessary network setup is in place before running subsequent steps.
Copy link

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/backend/core/api/serializers.py (1)

343-402: Active ProConnect subscriptions can still lose idp_id via metadata replacement.

_validate_proconnect_subscription returns early when "idp_id" is not in attrs["metadata"]. For an active ProConnect subscription with an existing metadata["idp_id"], a partial update that sends metadata without idp_id replaces the JSON and effectively clears the IDP while leaving is_active=True, contradicting the invariant described in the docstring.

You can close this hole by only short‑circuiting on non‑dict metadata, and always comparing the effective idp_id for active ProConnect subscriptions:

    if "metadata" not in attrs:
        return

    new_metadata = attrs["metadata"]
-    if not isinstance(new_metadata, dict) or not "idp_id" in new_metadata:
-        return
+    if not isinstance(new_metadata, dict):
+        return

    current_idp_id = instance.metadata.get("idp_id")
    new_idp_id = new_metadata.get("idp_id")
    if current_idp_id == new_idp_id:
        return

Also, because validate() guards on self.instance, this check currently only runs on updates. If you also need similar constraints on initial creation via OrganizationServiceSubscriptionViewSet.partial_update, you’ll need a complementary check there or rely on model‑level validation being invoked explicitly.

src/backend/core/api/viewsets/service.py (1)

186-191: Unhandled DoesNotExist exceptions in partial_update for organization/service/operator lookups.

Lines 186-190 perform direct .objects.get() calls that will raise unhandled DoesNotExist exceptions if any ID is invalid, resulting in 500 responses instead of 404s. The codebase already establishes the correct pattern in get_serializer_context (lines 101-108) and get_object (lines 153-158) where DoesNotExist is caught and converted to NotFound. Apply the same pattern here:

-        organization = models.Organization.objects.get(
-            id=self.kwargs["organization_id"]
-        )
-        service = models.Service.objects.get(id=self.kwargs["service_id"])
-        operator = models.Operator.objects.get(id=self.kwargs["operator_id"])
+        try:
+            organization = models.Organization.objects.get(
+                id=self.kwargs["organization_id"]
+            )
+        except models.Organization.DoesNotExist as err:
+            raise NotFound("Organization not found.") from err
+
+        try:
+            service = models.Service.objects.get(id=self.kwargs["service_id"])
+        except models.Service.DoesNotExist as err:
+            raise NotFound("Service not found.") from err
+
+        try:
+            operator = models.Operator.objects.get(id=self.kwargs["operator_id"])
+        except models.Operator.DoesNotExist as err:
+            raise NotFound("Operator not found.") from err
♻️ Duplicate comments (2)
src/frontend/src/features/ui/components/service/ServiceBlock.tsx (1)

134-149: Align aria-label with tooltip and adjust wrapper semantics

The tooltip already distinguishes between externally managed and non-activatable services, but the aria-label stays hardcoded to the “externally managed” message:

aria-label={t("organizations.services.externally_managed")}

It should reuse the same condition as the tooltip so screen-reader users get the right reason:

-              aria-label={t("organizations.services.externally_managed")}
+              aria-label={t(
+                isExternallyManaged
+                  ? "organizations.services.externally_managed"
+                  : "organizations.services.cannot_activate"
+              )}

Also, since this wrapper has role="button" and tabIndex={0} but no click/key handlers, it’s effectively non-interactive; you could either drop the role/tabIndex or add aria-disabled and key handlers if you want it to behave like a (disabled) button.

src/backend/core/api/viewsets/service.py (1)

97-110: Graceful 404 when organization ID is invalid.

Catching Organization.DoesNotExist in get_serializer_context and re‑raising NotFound("Organization not found.") cleanly converts bad organization_id path params into a 404 and guarantees the serializer always sees a valid organization in its context; this addresses the earlier unhandled exception risk.

🧹 Nitpick comments (4)
.github/workflows/tests.yml (1)

20-21: Consider removing network setup from the lint-back job.

Linting is a static analysis task that doesn't typically require Docker network setup. The network setup step appears to be unnecessary overhead in this job.

  lint-back:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
-     - name: Run network setup
-       run: make create-docker-network
      - name: Create env files
        run: make create-env-files

If linting does require network setup for some reason (e.g., to resolve DNS or access services), then this change can be safely ignored.

src/frontend/src/features/ui/components/service/ServiceBlock.tsx (2)

26-39: Tighten onChangeSubscription typing and drop the as cast

Hook logic looks good, but you can simplify the types and avoid the unsafe cast by reusing the mutation’s own parameter type instead of MutateOptions + as:

-import { MutateOptions } from "@tanstack/react-query";
@@
-  const onChangeSubscription = (
-    data: Partial<ServiceSubscription>,
-    options?: MutateOptions
-  ) => {
+  const onChangeSubscription = (
+    data: Partial<ServiceSubscription>,
+    options?: Parameters<typeof updateOrganizationServiceSubscription>[1]
+  ) => {
@@
-      options as Parameters<typeof updateOrganizationServiceSubscription>[1]
+      options
     );
   };

This keeps onChangeSubscription exactly in sync with the mutation signature and removes one import.


172-185: Add rel="noreferrer noopener" on external link opened in new tab

The plain <a> uses target="_blank" without a rel, which can allow reverse tabnabbing:

<a target="_blank" href={service.url}>

Consider:

-              <a target="_blank" href={service.url}>
+              <a target="_blank" rel="noreferrer noopener" href={service.url}>

This keeps behavior while hardening the link.

src/backend/core/api/serializers.py (1)

285-307: Config whitelisting appropriately limits exposed data.

Exposing config via computed dicts that only return idps (operator) and help_center_url (service) is a solid improvement over returning the raw JSON and significantly reduces the risk of leaking secrets. If more config keys need selective exposure later, consider extracting the whitelist logic into a small shared helper to keep behaviour consistent across serializers.

Based on learnings, this aligns with the guidance to avoid exposing sensitive configuration data.

Also applies to: 313-337

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5b79ee8 and 1a9b562.

📒 Files selected for processing (4)
  • .github/workflows/tests.yml (1 hunks)
  • src/backend/core/api/serializers.py (6 hunks)
  • src/backend/core/api/viewsets/service.py (2 hunks)
  • src/frontend/src/features/ui/components/service/ServiceBlock.tsx (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
src/backend/**/*.py

📄 CodeRabbit inference engine (.cursor/rules/django-python.mdc)

src/backend/**/*.py: Use Django’s ORM for database interactions; avoid raw SQL unless strictly necessary for performance
Use Django’s built-in user model and authentication framework for user management
Log expected and unexpected application actions with appropriate log levels
Respect log level semantics: DEBUG (diagnostics), INFO (normal events), WARN (unexpected but continuing), ERROR (functional failure)
Do not log sensitive information (authorization tokens, passwords, financial/health data, PII)

Files:

  • src/backend/core/api/viewsets/service.py
  • src/backend/core/api/serializers.py
src/backend/**/serializers.py

📄 CodeRabbit inference engine (.cursor/rules/django-python.mdc)

Use DRF serializers for JSON responses

Files:

  • src/backend/core/api/serializers.py
🧠 Learnings (2)
📚 Learning: 2025-10-03T23:40:57.889Z
Learnt from: CR
Repo: suitenumerique/st-deploycenter PR: 0
File: .cursor/rules/django-python.mdc:0-0
Timestamp: 2025-10-03T23:40:57.889Z
Learning: Applies to src/backend/**/serializers.py : Use DRF serializers for JSON responses

Applied to files:

  • src/backend/core/api/serializers.py
📚 Learning: 2025-10-03T23:40:57.889Z
Learnt from: CR
Repo: suitenumerique/st-deploycenter PR: 0
File: .cursor/rules/django-python.mdc:0-0
Timestamp: 2025-10-03T23:40:57.889Z
Learning: Applies to src/backend/**/*.py : Do not log sensitive information (authorization tokens, passwords, financial/health data, PII)

Applied to files:

  • src/backend/core/api/serializers.py
🧬 Code graph analysis (3)
src/backend/core/api/viewsets/service.py (3)
src/frontend/src/features/api/Repository.ts (2)
  • Organization (33-45)
  • ServiceSubscription (47-54)
src/backend/core/models.py (2)
  • Organization (329-564)
  • ServiceSubscription (788-917)
src/backend/core/authentication/views.py (1)
  • get (116-137)
src/frontend/src/features/ui/components/service/ServiceBlock.tsx (4)
src/frontend/src/pages/operators/[operator_id]/organizations/[id].tsx (1)
  • Organization (16-122)
src/frontend/src/features/api/Repository.ts (1)
  • updateOrganizationServiceSubscription (159-174)
src/frontend/src/hooks/useQueries.tsx (1)
  • useMutationUpdateOrganizationServiceSubscription (87-117)
src/frontend/src/features/layouts/components/GlobalLayout.tsx (1)
  • useOperatorContext (57-63)
src/backend/core/api/serializers.py (1)
src/backend/core/models.py (15)
  • Meta (62-63)
  • Meta (174-177)
  • Meta (227-231)
  • Meta (318-323)
  • Meta (488-500)
  • Meta (596-604)
  • Meta (640-649)
  • Meta (753-760)
  • Meta (832-838)
  • Meta (979-997)
  • Meta (1062-1069)
  • Operator (188-282)
  • ServiceSubscription (788-917)
  • mail_domain (532-535)
  • get_mail_domain_status (537-564)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: test-back
🔇 Additional comments (6)
.github/workflows/tests.yml (2)

9-9: Cosmetic YAML quoting change.

The quoted wildcard syntax "*" is functionally equivalent to the unquoted '*' and is a valid YAML style preference.


32-33: Network setup step appropriately added to test-back job.

The "Run network setup" step is correctly placed before environment file creation in the test-back job, which aligns with the backend changes that introduce service dependencies and ProConnect-related logic requiring Docker network configuration during tests.

src/frontend/src/features/ui/components/service/ServiceBlock.tsx (1)

92-131: Switch activation/deactivation flow looks consistent

The optimistic update pattern for both activation and deactivation (setting checked immediately and reverting in onError) is consistent and should feel snappy for users while still recovering correctly on failures.

src/backend/core/api/serializers.py (2)

436-442: Organization serialization enrichment with mail-domain fields is consistent.

Injecting mail_domain and mail_domain_status from instance.get_mail_domain_status() in to_representation cleanly augments the API without changing the DB schema and matches the frontend Organization shape; this looks good.


476-482: Explicit requirement for organization in serializer context is a good safety guard.

Enforcing "organization" in self.context and raising a clear ValueError if it’s missing makes misuse of OrganizationServiceSerializer fail fast and aligns with OrganizationServiceViewSet.get_serializer_context() now always providing that object.

src/backend/core/api/viewsets/service.py (1)

201-210: Persisting validated metadata on subscription creation is correct.

Using serializer.validated_data.get("metadata", {}) when creating the ServiceSubscription ensures any provided metadata is saved while preserving the {} default when omitted, and keeps the viewset aligned with the updated serializer contract.

@NathanVss NathanVss merged commit 1a9b562 into main Dec 8, 2025
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants