Skip to content

Commit fb04ad7

Browse files
Jxiorenovate[bot]dependabot[bot]
authored
32450 - Add Pay Service of fee code check (#3631)
Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1 parent 9449d4f commit fb04ad7

File tree

8 files changed

+135
-10
lines changed

8 files changed

+135
-10
lines changed

auth-api/poetry.lock

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

auth-api/src/auth_api/services/products.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
get_product_notification_data,
5757
get_product_notification_type,
5858
)
59+
from auth_api.utils.pay import get_account_fees
5960
from auth_api.utils.roles import CLIENT_ADMIN_ROLES, CLIENT_AUTH_ROLES, GOV_ORG_TYPES, STAFF
6061
from auth_api.utils.user_context import UserContext, user_context
6162

@@ -159,13 +160,14 @@ def resubmit_product_subscription(org_id, subscription_data: dict[str, Any], ski
159160
@staticmethod
160161
def _check_gov_org_add_product_previously_approved(
161162
org_id: int,
162-
product_code: str
163+
product_code: str,
164+
account_fees: list[str]
163165
) -> tuple[bool, Any]:
164166
"""Check if GOV org's account fee product was previously approved (NEW_PRODUCT_FEE_REVIEW task)."""
165167
inactive_sub = ProductSubscriptionModel.find_by_org_id_product_code(
166168
org_id=org_id, product_code=product_code, valid_statuses=(ProductSubscriptionStatus.INACTIVE.value,)
167169
)
168-
if not inactive_sub:
170+
if not inactive_sub or product_code not in account_fees:
169171
return False, None
170172
task_add_product = TaskModel.find_by_task_relationship_id(
171173
inactive_sub.id, TaskRelationshipType.PRODUCT.value, TaskStatus.COMPLETED.value
@@ -233,6 +235,7 @@ def create_product_subscription(
233235
check_auth(one_of_roles=(*CLIENT_ADMIN_ROLES, STAFF), org_id=org_id)
234236

235237
subscriptions_list = subscription_data.get("subscriptions")
238+
account_fees = get_account_fees(org)
236239
for subscription in subscriptions_list:
237240
auto_approve_current = auto_approve
238241
product_code = subscription.get("productCode")
@@ -246,7 +249,7 @@ def create_product_subscription(
246249

247250
if org.access_type in GOV_ORG_TYPES and not staff_review_for_create_org:
248251
previously_approved, inactive_sub = Product._check_gov_org_add_product_previously_approved(
249-
org.id, product_code
252+
org.id, product_code, account_fees
250253
)
251254
else:
252255
previously_approved, inactive_sub = Product._is_previously_approved(org_id, product_code)

auth-api/src/auth_api/utils/pay.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Copyright © 2026 Province of British Columbia
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
"""Pay API utility functions."""
15+
16+
from flask import current_app
17+
18+
from auth_api.exceptions.errors import Error
19+
from auth_api.models import Org as OrgModel
20+
from auth_api.services.rest_service import RestService
21+
from auth_api.utils.roles import GOV_ORG_TYPES
22+
23+
24+
def get_account_fees(org: OrgModel) -> list[str]:
25+
"""Fetch all account fees from pay-api and return a dict mapping product codes to fee existence."""
26+
if org.access_type not in GOV_ORG_TYPES:
27+
return []
28+
pay_url = current_app.config.get("PAY_API_URL")
29+
account_fees = []
30+
31+
try:
32+
token = RestService.get_service_account_token()
33+
response = RestService.get(endpoint=f"{pay_url}/accounts/{org.id}/fees", token=token, retry_on_failure=True)
34+
35+
if response and response.status_code == 200:
36+
response_data = response.json()
37+
account_fees_obj = response_data.get("accountFees", [])
38+
39+
for fee in account_fees_obj:
40+
product_code = fee.get("product")
41+
if product_code:
42+
account_fees.append(product_code)
43+
return account_fees
44+
except Exception as e: # NOQA # pylint: disable=broad-except
45+
# Log the error but don't fail the subscription creation
46+
# Return empty dict so subscription can proceed without fee-based review logic
47+
current_app.logger.info(f"{Error.ACCOUNT_FEES_FETCH_FAILED} for org {org.id}: {e}")
48+
return []
49+
50+
return account_fees

auth-api/tests/conftest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ def auth_mock(monkeypatch):
177177
monkeypatch.setattr("auth_api.services.affiliation_invitation.check_auth", lambda *args, **kwargs: None) # noqa: ARG005
178178

179179

180+
180181
@pytest.fixture()
181182
def notify_mock(monkeypatch):
182183
"""Mock send_email."""

auth-api/tests/unit/api/test_org.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
factory_user_model,
8080
patch_pay_account_delete,
8181
patch_pay_account_delete_error,
82+
patch_pay_account_fees,
8283
patch_pay_account_post,
8384
)
8485

@@ -569,6 +570,7 @@ def test_create_govn_org_with_products_single_staff_review_task(client, jwt, ses
569570
after approval.
570571
"""
571572
patch_pay_account_post(monkeypatch)
573+
patch_pay_account_fees(monkeypatch, [ProductCode.BUSINESS_SEARCH.value, ProductCode.PPR.value])
572574

573575
headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.public_user_role)
574576
client.post("/api/v1/users", headers=headers, content_type="application/json")
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
"""Tests for the pay utility functions."""
2+
3+
# Copyright © 2026 Province of British Columbia
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
17+
from unittest.mock import Mock
18+
19+
from flask import current_app
20+
21+
from auth_api.utils.enums import AccessType, ProductCode
22+
from auth_api.utils.pay import get_account_fees
23+
from tests.conftest import mock_token
24+
from tests.utilities.factory_utils import factory_org_model
25+
26+
27+
def test_get_account_fees_govm_org_success(monkeypatch, session): # pylint:disable=unused-argument
28+
"""Test that GOVM org with successful response returns list of product codes."""
29+
org = factory_org_model(org_info={"name": "Org 1", "accessType": AccessType.GOVM.value})
30+
31+
mock_response = Mock()
32+
mock_response.status_code = 200
33+
mock_response.json.return_value = {
34+
"accountFees": [
35+
{"product": ProductCode.BUSINESS.value},
36+
{"product": ProductCode.VS.value},
37+
{"product": ProductCode.BCA.value},
38+
]
39+
}
40+
41+
current_app.config["PAY_API_URL"] = "http://pay-api.test"
42+
43+
monkeypatch.setattr("auth_api.utils.pay.RestService.get_service_account_token", mock_token)
44+
monkeypatch.setattr("auth_api.utils.pay.RestService.get", lambda *args, **kwargs: mock_response) # noqa: ARG005
45+
46+
result = get_account_fees(org)
47+
48+
assert result == [
49+
ProductCode.BUSINESS.value,
50+
ProductCode.VS.value,
51+
ProductCode.BCA.value,
52+
]

auth-api/tests/utilities/factory_utils.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -534,3 +534,20 @@ def keycloak_delete_user_by_username(username):
534534
delete_user_url = f"{base_url}/auth/admin/realms/{realm}/users/{user.id}"
535535
response = requests.delete(delete_user_url, headers=headers, timeout=timeout)
536536
response.raise_for_status()
537+
538+
539+
def patch_pay_account_fees(monkeypatch, product_codes: list[str]):
540+
"""Patch GET /accounts/{id}/fees to return given product codes."""
541+
class MockFeeResponse:
542+
status_code = 200
543+
def json(self):
544+
return {"accountFees": [{"product": c} for c in product_codes]}
545+
546+
monkeypatch.setattr(
547+
"auth_api.utils.pay.RestService.get",
548+
lambda *_args, **_kwargs: MockFeeResponse(),
549+
)
550+
monkeypatch.setattr(
551+
"auth_api.utils.pay.RestService.get_service_account_token",
552+
lambda *_args, **_kwargs: "mock-token",
553+
)

queue_services/account-mailer/poetry.lock

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)