✨ (backend||frontend) CPF #1273
Merged
jonathanreveille merged 15 commits intomainfrom Mar 20, 2026
Merged
Conversation
019ac98 to
fe05baa
Compare
d81994c to
75626da
Compare
fe05baa to
f1d384f
Compare
0a3fb8a to
4e43fe6
Compare
Base automatically changed from
feature/add_purchase_order_reference_confirm_quote
to
main
January 27, 2026 10:16
2173e96 to
6d690b4
Compare
There was a problem hiding this comment.
Pull request overview
This PR introduces a new OfferingDeepLink model to support CPF (Mon Compte Formation) deep linking functionality, allowing organizations to register external subscription links for course offerings. The feature enables learners to subscribe to courses through external platforms, with each organization able to provide their own unique deep link per offering. The model is restricted to credential-type products only and includes validation to ensure organizations are properly associated with offerings.
Changes:
- Added
OfferingDeepLinkmodel with validation rules ensuring organization-offering relationships, credential-only product type restriction, and unique deep links - Created
OfferingDeepLinkFactoryto support testing with automatic organization assignment logic - Implemented comprehensive test suite validating model constraints, uniqueness requirements, and relationship rules
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 15 comments.
| File | Description |
|---|---|
| src/backend/joanie/core/models/courses.py | Defines the OfferingDeepLink model with fields for deep_link, offering, organization, and is_active, plus custom validation logic in clean() method |
| src/backend/joanie/core/factories.py | Adds OfferingDeepLinkFactory with lazy organization attribute that finds available organizations from the offering |
| src/backend/joanie/tests/core/models/test_offering_deep_link.py | Comprehensive test suite covering credential-only constraint, uniqueness rules, organization-offering validation, and successful creation |
| src/backend/joanie/core/migrations/0091_offeringdeeplink_and_more.py | Django migration creating the joanie_offering_deep_link table with appropriate fields and constraints |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
kernicPanel
approved these changes
Feb 9, 2026
kernicPanel
reviewed
Feb 9, 2026
18821b2 to
a16f217
Compare
a16f217 to
79713ff
Compare
7f95166 to
fcb6317
Compare
9ce9049 to
ca0db43
Compare
We want to suggest deep links of an offering which will allow the learners to subscribe to the course outside from the platform. This link will redirect the learner to a platform where he can purchase the course session and use credits earned through work. Each organization that is related to the offering can only one unique link for redirection.
We want admin backoffice users to be able to manage deep links of an offering for organizations.
We want admin authenticated user to be able to activate on or off the deep links of an offering. This will allow them to activate or deactivate the possibility to redirect the learners to the external platform to make subscriptions.
The client API of the offering viewset returns one of the deep link in a random manner. It only returns a deep link when the offering deep links are activate and when some exists. Otherwise, it returns None.
We want to let the user add and edit deep links in joanie back office, since the CRUD management was implemented.
Admins can now POST to /api/v1.0/admin/orders/ to create an isolated order in the `to_own` state with a 100%-discount voucher attached, without requiring a batch order flow. A new AdminOrderCreateSerializer validates product_id (required), course_code and organization_id (optional). perform_create follows the same pattern as generate_orders_and_send_vouchers: the order is saved first without a voucher so that full_clean() passes with a null owner, then the voucher is attached before transitioning through assign() and forcing the to_own state.
Admins can now create standalone orders directly from the order list page via a dedicated creation form with Product, Course and Organization search fields. The OrderRepository exposes a create() method, useOrders exposes it through the apiInterface, and a new OrderCreateForm / create page handle the flow with redirect to the order view on success.
Add a voucher field to AdminOrderSerializer returning the code and whether the voucher has been claimed (is_used = owner is not None), so the admin frontend can display and copy the code until it is used.
Show the voucher code in monospace in the order details section. When the voucher is still available, a green "Available" chip acts as a copy-to-clipboard button. Once claimed, the code is struck through and a grey "Used" chip replaces the copy action.
Show the voucher code in the list with a copy button. The code truncates with ellipsis when the column is too narrow, while the button always stays visible via flexShrink. Also generalizes the copy confirmation message from "Link added to your clipboard" to "Copied to clipboard".
The OfferingSearch component sends a `query` parameter to search offerings by product title, course title or course code. Without a filterset on the viewset, that parameter was silently ignored, making it impossible to find a specific offering among many.
Offerings with a single organization don't exercise the organization selector in the order creation form. Adding a second organization to each dev offering makes it easier to test that flow locally.
A canceled order transitions to a state included in ORDER_STATES_VOUCHER_CLAIMABLE, which caused the 100% voucher to appear usable again and be applicable as a discount on a new order.
Now that we different payment methods in our sales tunnel such as credit card, external deep links, and batch orders. Some payment methods like orders of batch order and prepaid orders from external platform, do not require to have a contract signed or a payment schedule because they are both fully prepaid. We have changed the key in the payment plan endpoint, to return whether the API consumer should skip the contract input or not.
When get_prepaid_order() finds no matching order, the code fell through and created a new order with the 100% discount. This guard rejects vouchers tied to a to_own order when used on a different product/course.
46d014c to
e6a62de
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Purpose
This PR will define the model that we will use to store deep links for offering in this issue
A deep link is related to an offering. Each deep link is unique for the pair offering and organization. The deep links are exclusive to product type
credential.If you want your API to suggest DeepLinks on an offering, you must set them first into the backoffice.
The backoffice allows the admin user to create/edit/delete or to activate or deactivate them.
Once we have a confirmation about a new order that was created outside our platform, we implemented a new way to generate standalone orders for this case. Once the standalone order is created, the admin user will communicate the voucher code to the organization responsible of the learner, and thus, pass the voucher code to the learner.
On the learner's side, he will have a voucher code that is only eligible for 1 offering exclusively. The voucher code will not allow the learner to subscribe to another offering.
Backoffice to add a deep link on an offering (2 organizations related)

Activate/deactivate one or both :

Create standalone order

Our API consumer side :

Proposal
OfferingDeepLinkmodel to store links of offeringto purchase a training from an external platform
OfferingViewSet