@@ -672,6 +672,111 @@ def test_create_govn_org_with_products_single_staff_review_task(client, jwt, ses
672672 )
673673
674674
675+ def test_govn_org_add_product_pending_staff_review (
676+ client , jwt , session , keycloak_mock , monkeypatch
677+ ): # pylint:disable=unused-argument
678+ """Assert adding a product to a GOVN org via POST /orgs/{id}/products results in PENDING_STAFF_REVIEW."""
679+ patch_pay_account_post (monkeypatch )
680+ headers = factory_auth_header (jwt = jwt , claims = TestJwtClaims .public_user_role )
681+ client .post ("/api/v1/users" , headers = headers , content_type = "application/json" )
682+
683+ govn_org_payload = {
684+ "name" : "test govn add product" ,
685+ "accessType" : AccessType .GOVN .value ,
686+ "typeCode" : OrgType .PREMIUM .value ,
687+ "mailingAddress" : TestOrgInfo .get_mailing_address (),
688+ "paymentInfo" : {"paymentMethod" : PaymentMethod .DIRECT_PAY .value },
689+ }
690+ rv = client .post (
691+ "/api/v1/orgs" ,
692+ data = json .dumps (govn_org_payload ),
693+ headers = headers ,
694+ content_type = "application/json" ,
695+ )
696+ assert rv .status_code == HTTPStatus .CREATED
697+ org_id = rv .json ["id" ]
698+
699+ product_code = ProductCode .BUSINESS_SEARCH .value
700+ rv_products = client .post (
701+ f"/api/v1/orgs/{ org_id } /products" ,
702+ data = json .dumps ({"subscriptions" : [{"productCode" : product_code }]}),
703+ headers = headers ,
704+ content_type = "application/json" ,
705+ )
706+ assert rv_products .status_code == HTTPStatus .CREATED
707+ subscriptions = rv_products .json .get ("subscriptions" , [])
708+ product = next ((p for p in subscriptions if p .get ("code" ) == product_code ), None )
709+ assert product is not None , f"Product { product_code } should appear in response"
710+ assert product ["subscriptionStatus" ] == ProductSubscriptionStatus .PENDING_STAFF_REVIEW .value , (
711+ "GOVN org adding product should require staff review"
712+ )
713+
714+
715+ @pytest .mark .parametrize (
716+ "product_code,expected_status,use_factory_org,claims_attr,assert_no_product_task" ,
717+ [
718+ (ProductCode .NDS .value , ProductSubscriptionStatus .ACTIVE .value , True , "system_role" , True ),
719+ (ProductCode .VS .value , ProductSubscriptionStatus .PENDING_STAFF_REVIEW .value , False , "public_user_role" , False ),
720+ ],
721+ )
722+ def test_post_org_products_skip_auth_system_vs_org_admin (
723+ client , jwt , session , keycloak_mock , monkeypatch ,
724+ product_code , expected_status , use_factory_org , claims_attr , assert_no_product_task ,
725+ ): # pylint:disable=unused-argument
726+ """Assert POST /orgs/{id}/products: SYSTEM vs org admin (non-GOVN/GOVM); focus on PENDING_STAFF_REVIEW for org admin."""
727+ headers = factory_auth_header (jwt = jwt , claims = getattr (TestJwtClaims , claims_attr ))
728+
729+ if use_factory_org :
730+ regular_org = factory_org_model (org_info = {"name" : "regular org for system" , "accessType" : AccessType .REGULAR .value })
731+ org_id = regular_org .id
732+ else :
733+ patch_pay_account_post (monkeypatch )
734+ client .post ("/api/v1/users" , headers = headers , content_type = "application/json" )
735+ rv = client .post (
736+ "/api/v1/orgs" ,
737+ data = json .dumps ({
738+ "name" : "regular org for org admin" ,
739+ "accessType" : AccessType .REGULAR .value ,
740+ "typeCode" : OrgType .PREMIUM .value ,
741+ "mailingAddress" : TestOrgInfo .get_mailing_address (),
742+ "paymentInfo" : {"paymentMethod" : PaymentMethod .DIRECT_PAY .value },
743+ }),
744+ headers = headers ,
745+ content_type = "application/json" ,
746+ )
747+ assert rv .status_code == HTTPStatus .CREATED
748+ org_id = rv .json ["id" ]
749+
750+ rv_products = client .post (
751+ f"/api/v1/orgs/{ org_id } /products" ,
752+ data = json .dumps ({"subscriptions" : [{"productCode" : product_code }]}),
753+ headers = headers ,
754+ content_type = "application/json" ,
755+ )
756+ assert rv_products .status_code == HTTPStatus .CREATED
757+ subs = rv_products .json .get ("subscriptions" , [])
758+ product = next ((p for p in subs if p .get ("code" ) == product_code ), None )
759+ if assert_no_product_task :
760+ # NDS is hidden: POST response may not include it; use GET ?include_hidden=true to verify (as in prod workflow)
761+ get_headers = factory_auth_header (jwt = jwt , claims = TestJwtClaims .staff_view_accounts_role )
762+ rv_get = client .get (
763+ f"/api/v1/orgs/{ org_id } /products?include_hidden=true" ,
764+ headers = get_headers ,
765+ )
766+ assert rv_get .status_code == HTTPStatus .OK
767+ subs = json .loads (rv_get .data )
768+ product = next ((p for p in subs if p .get ("code" ) == product_code ), None )
769+ assert product is not None
770+ assert product ["subscriptionStatus" ] == expected_status
771+
772+ if assert_no_product_task :
773+ product_tasks = [
774+ t for t in TaskService .fetch_tasks (TaskSearch (status = [TaskStatus .OPEN .value ], page = 1 , limit = 100 ))["tasks" ]
775+ if t .get ("relationship_type" ) == TaskRelationshipType .PRODUCT .value and t .get ("account_id" ) == org_id
776+ ]
777+ assert len (product_tasks ) == 0 , "SYSTEM adding product should not create a product review task"
778+
779+
675780def test_add_org_invalid_returns_400 (client , jwt , session ): # pylint:disable=unused-argument
676781 """Assert that POSTing an invalid org returns a 400."""
677782 headers = factory_auth_header (jwt , claims = TestJwtClaims .public_user_role )
0 commit comments