@@ -1896,7 +1896,7 @@ def test_api_offering_payment_plan_with_product_id(
18961896 "state" : enums .PAYMENT_STATE_PENDING ,
18971897 },
18981898 ],
1899- "from_batch_order " : False ,
1899+ "skip_contract_inputs " : False ,
19001900 },
19011901 response .json (),
19021902 )
@@ -1985,7 +1985,7 @@ def test_api_offering_payment_plan_with_product_id_discount(
19851985 "state" : enums .PAYMENT_STATE_PENDING ,
19861986 },
19871987 ],
1988- "from_batch_order " : False ,
1988+ "skip_contract_inputs " : False ,
19891989 },
19901990 response .json (),
19911991 )
@@ -2052,7 +2052,7 @@ def test_api_offering_payment_plan_with_certificate_product_id(
20522052 "state" : enums .PAYMENT_STATE_PENDING ,
20532053 },
20542054 ],
2055- "from_batch_order " : False ,
2055+ "skip_contract_inputs " : False ,
20562056 },
20572057 response .json (),
20582058 )
@@ -2123,7 +2123,7 @@ def test_api_offering_payment_plan_with_certificate_product_id_discount(
21232123 "state" : enums .PAYMENT_STATE_PENDING ,
21242124 },
21252125 ],
2126- "from_batch_order " : False ,
2126+ "skip_contract_inputs " : False ,
21272127 },
21282128 response .json (),
21292129 )
@@ -2267,7 +2267,7 @@ def test_api_offering_payment_plan_voucher_anonymous(
22672267 "state" : enums .PAYMENT_STATE_PENDING ,
22682268 },
22692269 ],
2270- "from_batch_order " : False ,
2270+ "skip_contract_inputs " : False ,
22712271 },
22722272 response .json (),
22732273 )
@@ -2381,7 +2381,7 @@ def test_api_offering_payment_plan_voucher_with_credential_product_id_with_vouch
23812381 "state" : enums .PAYMENT_STATE_PENDING ,
23822382 },
23832383 ],
2384- "from_batch_order " : False ,
2384+ "skip_contract_inputs " : False ,
23852385 },
23862386 response .json (),
23872387 )
@@ -2481,7 +2481,7 @@ def test_api_offering_payment_plan_voucher_with_certificate_product_id_with_vouc
24812481 "state" : enums .PAYMENT_STATE_PENDING ,
24822482 },
24832483 ],
2484- "from_batch_order " : False ,
2484+ "skip_contract_inputs " : False ,
24852485 },
24862486 response .json (),
24872487 )
@@ -2574,7 +2574,127 @@ def test_api_offering_payment_plan_voucher_code_from_batch_order(
25742574 "state" : enums .PAYMENT_STATE_PENDING ,
25752575 },
25762576 ],
2577- "from_batch_order" : True ,
2577+ "skip_contract_inputs" : True ,
2578+ },
2579+ response .json (),
2580+ )
2581+
2582+ self .assertStatusCodeEqual (response_relation_path , HTTPStatus .OK )
2583+ self .assertEqual (response_relation_path .json (), response .json ())
2584+ self .assertStatusCodeEqual (response_with_query_params , HTTPStatus .OK )
2585+ self .assertEqual (response_with_query_params .json (), response .json ())
2586+ self .assertStatusCodeEqual (
2587+ response_relation_path_with_query_param ,
2588+ HTTPStatus .OK ,
2589+ )
2590+ self .assertEqual (
2591+ response_relation_path_with_query_param .json (), response .json ()
2592+ )
2593+
2594+ @override_settings (
2595+ JOANIE_PAYMENT_SCHEDULE_LIMITS = {
2596+ 100 : (100 ,),
2597+ },
2598+ DEFAULT_CURRENCY = "EUR" ,
2599+ )
2600+ @patch (
2601+ "joanie.core.api.client.ValidateVoucherThrottle.get_rate" ,
2602+ return_value = "5/minute" ,
2603+ )
2604+ def test_api_offering_payment_plan_voucher_code_for_deep_link_prepaid_order (
2605+ self ,
2606+ _mock_get_rate ,
2607+ ):
2608+ """
2609+ The endpoint `payment_plan` should return True to skip contract inputs
2610+ when the order is in state `to_own` and is fully prepaid and is not related
2611+ to a batch order.
2612+ """
2613+ user = factories .UserFactory ()
2614+ token = self .generate_token_from_user (user )
2615+
2616+ course = factories .CourseFactory ()
2617+ course_run = factories .CourseRunFactory (
2618+ enrollment_start = datetime (2025 , 1 , 1 , 14 , tzinfo = ZoneInfo ("UTC" )),
2619+ start = datetime (2025 , 3 , 1 , 14 , tzinfo = ZoneInfo ("UTC" )),
2620+ end = datetime (2025 , 5 , 1 , 14 , tzinfo = ZoneInfo ("UTC" )),
2621+ course = course ,
2622+ )
2623+ product = factories .ProductFactory (
2624+ price = 10 ,
2625+ type = enums .PRODUCT_TYPE_CREDENTIAL ,
2626+ target_courses = [course_run .course ],
2627+ )
2628+ offering = factories .OfferingFactory (
2629+ course = course_run .course ,
2630+ product = product ,
2631+ )
2632+ # Create the order in draft state first (no owner, no batch_order)
2633+ order = factories .OrderGeneratorFactory (
2634+ product = offering .product ,
2635+ course = offering .course ,
2636+ organization = offering .organizations .first (),
2637+ owner = None ,
2638+ credit_card = None ,
2639+ state = enums .ORDER_STATE_DRAFT ,
2640+ )
2641+ voucher = factories .VoucherFactory (
2642+ discount = factories .DiscountFactory (rate = 1 ),
2643+ multiple_use = False ,
2644+ multiple_users = False ,
2645+ )
2646+ # Simulate what the admin endpoint does: attach voucher and force to_own
2647+ models .Order .objects .filter (pk = order .pk ).update (
2648+ state = enums .ORDER_STATE_TO_OWN ,
2649+ voucher = voucher ,
2650+ batch_order = None ,
2651+ )
2652+
2653+ mocked_now = datetime (2025 , 1 , 1 , 0 , tzinfo = ZoneInfo ("UTC" ))
2654+ payload = {"voucher_code" : voucher .code }
2655+
2656+ with (
2657+ mock .patch ("uuid.uuid4" , return_value = uuid .UUID (int = 1 )),
2658+ mock .patch ("django.utils.timezone.now" , return_value = mocked_now ),
2659+ ):
2660+ response = self .client .get (
2661+ f"/api/v1.0/courses/{ offering .course .code } /"
2662+ f"products/{ offering .product .id } /payment-plan/" ,
2663+ HTTP_AUTHORIZATION = f"Bearer { token } " ,
2664+ data = payload ,
2665+ )
2666+ response_relation_path = self .client .get (
2667+ f"/api/v1.0/offerings/{ offering .id } /payment-plan/" ,
2668+ HTTP_AUTHORIZATION = f"Bearer { token } " ,
2669+ data = payload ,
2670+ )
2671+ response_with_query_params = self .client .get (
2672+ f"/api/v1.0/courses/{ offering .course .code } /"
2673+ f"products/{ offering .product .id } /payment-plan/?voucher_code={ voucher .code } " ,
2674+ HTTP_AUTHORIZATION = f"Bearer { token } " ,
2675+ )
2676+ response_relation_path_with_query_param = self .client .get (
2677+ f"/api/v1.0/offerings/{ offering .id } /payment-plan/"
2678+ f"?voucher_code={ voucher .code } " ,
2679+ HTTP_AUTHORIZATION = f"Bearer { token } " ,
2680+ )
2681+
2682+ self .assertStatusCodeEqual (response , HTTPStatus .OK )
2683+ self .assertEqual (
2684+ {
2685+ "price" : 10.00 ,
2686+ "discount" : "-100%" ,
2687+ "discounted_price" : 0.00 ,
2688+ "payment_schedule" : [
2689+ {
2690+ "id" : "00000000-0000-0000-0000-000000000001" ,
2691+ "amount" : 0.00 ,
2692+ "currency" : settings .DEFAULT_CURRENCY ,
2693+ "due_date" : "2025-01-17" ,
2694+ "state" : enums .PAYMENT_STATE_PENDING ,
2695+ },
2696+ ],
2697+ "skip_contract_inputs" : True ,
25782698 },
25792699 response .json (),
25802700 )
0 commit comments