Skip to content
6 changes: 3 additions & 3 deletions auth-api/poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 6 additions & 3 deletions auth-api/src/auth_api/services/products.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
get_product_notification_data,
get_product_notification_type,
)
from auth_api.utils.pay import get_account_fees
from auth_api.utils.roles import CLIENT_ADMIN_ROLES, CLIENT_AUTH_ROLES, GOV_ORG_TYPES, STAFF
from auth_api.utils.user_context import UserContext, user_context

Expand Down Expand Up @@ -159,13 +160,14 @@ def resubmit_product_subscription(org_id, subscription_data: dict[str, Any], ski
@staticmethod
def _check_gov_org_add_product_previously_approved(
org_id: int,
product_code: str
product_code: str,
account_fees: list[str]
) -> tuple[bool, Any]:
"""Check if GOV org's account fee product was previously approved (NEW_PRODUCT_FEE_REVIEW task)."""
inactive_sub = ProductSubscriptionModel.find_by_org_id_product_code(
org_id=org_id, product_code=product_code, valid_statuses=(ProductSubscriptionStatus.INACTIVE.value,)
)
if not inactive_sub:
if not inactive_sub or product_code not in account_fees:
return False, None
task_add_product = TaskModel.find_by_task_relationship_id(
inactive_sub.id, TaskRelationshipType.PRODUCT.value, TaskStatus.COMPLETED.value
Expand Down Expand Up @@ -233,6 +235,7 @@ def create_product_subscription(
check_auth(one_of_roles=(*CLIENT_ADMIN_ROLES, STAFF), org_id=org_id)

subscriptions_list = subscription_data.get("subscriptions")
account_fees = get_account_fees(org)
for subscription in subscriptions_list:
auto_approve_current = auto_approve
product_code = subscription.get("productCode")
Expand All @@ -246,7 +249,7 @@ def create_product_subscription(

if org.access_type in GOV_ORG_TYPES and not staff_review_for_create_org:
previously_approved, inactive_sub = Product._check_gov_org_add_product_previously_approved(
org.id, product_code
org.id, product_code, account_fees
)
else:
previously_approved, inactive_sub = Product._is_previously_approved(org_id, product_code)
Expand Down
50 changes: 50 additions & 0 deletions auth-api/src/auth_api/utils/pay.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Copyright © 2026 Province of British Columbia
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Pay API utility functions."""

from flask import current_app

from auth_api.exceptions.errors import Error
from auth_api.models import Org as OrgModel
from auth_api.services.rest_service import RestService
from auth_api.utils.roles import GOV_ORG_TYPES


def get_account_fees(org: OrgModel) -> list[str]:
"""Fetch all account fees from pay-api and return a dict mapping product codes to fee existence."""
if org.access_type not in GOV_ORG_TYPES:
return []
pay_url = current_app.config.get("PAY_API_URL")
account_fees = []

try:
token = RestService.get_service_account_token()
response = RestService.get(endpoint=f"{pay_url}/accounts/{org.id}/fees", token=token, retry_on_failure=True)

if response and response.status_code == 200:
response_data = response.json()
account_fees_obj = response_data.get("accountFees", [])

for fee in account_fees_obj:
product_code = fee.get("product")
if product_code:
account_fees.append(product_code)
return account_fees
except Exception as e: # NOQA # pylint: disable=broad-except
# Log the error but don't fail the subscription creation
# Return empty dict so subscription can proceed without fee-based review logic
current_app.logger.info(f"{Error.ACCOUNT_FEES_FETCH_FAILED} for org {org.id}: {e}")
return []

return account_fees
1 change: 1 addition & 0 deletions auth-api/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ def auth_mock(monkeypatch):
monkeypatch.setattr("auth_api.services.affiliation_invitation.check_auth", lambda *args, **kwargs: None) # noqa: ARG005



@pytest.fixture()
def notify_mock(monkeypatch):
"""Mock send_email."""
Expand Down
2 changes: 2 additions & 0 deletions auth-api/tests/unit/api/test_org.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
factory_user_model,
patch_pay_account_delete,
patch_pay_account_delete_error,
patch_pay_account_fees,
patch_pay_account_post,
)

Expand Down Expand Up @@ -569,6 +570,7 @@ def test_create_govn_org_with_products_single_staff_review_task(client, jwt, ses
after approval.
"""
patch_pay_account_post(monkeypatch)
patch_pay_account_fees(monkeypatch, [ProductCode.BUSINESS_SEARCH.value, ProductCode.PPR.value])

headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.public_user_role)
client.post("/api/v1/users", headers=headers, content_type="application/json")
Expand Down
52 changes: 52 additions & 0 deletions auth-api/tests/unit/services/test_pay.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"""Tests for the pay utility functions."""

# Copyright © 2026 Province of British Columbia
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from unittest.mock import Mock

from flask import current_app

from auth_api.utils.enums import AccessType, ProductCode
from auth_api.utils.pay import get_account_fees
from tests.conftest import mock_token
from tests.utilities.factory_utils import factory_org_model


def test_get_account_fees_govm_org_success(monkeypatch, session): # pylint:disable=unused-argument
"""Test that GOVM org with successful response returns list of product codes."""
org = factory_org_model(org_info={"name": "Org 1", "accessType": AccessType.GOVM.value})

mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {
"accountFees": [
{"product": ProductCode.BUSINESS.value},
{"product": ProductCode.VS.value},
{"product": ProductCode.BCA.value},
]
}

current_app.config["PAY_API_URL"] = "http://pay-api.test"

monkeypatch.setattr("auth_api.utils.pay.RestService.get_service_account_token", mock_token)
monkeypatch.setattr("auth_api.utils.pay.RestService.get", lambda *args, **kwargs: mock_response) # noqa: ARG005

result = get_account_fees(org)

assert result == [
ProductCode.BUSINESS.value,
ProductCode.VS.value,
ProductCode.BCA.value,
]
17 changes: 17 additions & 0 deletions auth-api/tests/utilities/factory_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -534,3 +534,20 @@ def keycloak_delete_user_by_username(username):
delete_user_url = f"{base_url}/auth/admin/realms/{realm}/users/{user.id}"
response = requests.delete(delete_user_url, headers=headers, timeout=timeout)
response.raise_for_status()


def patch_pay_account_fees(monkeypatch, product_codes: list[str]):
"""Patch GET /accounts/{id}/fees to return given product codes."""
class MockFeeResponse:
status_code = 200
def json(self):
return {"accountFees": [{"product": c} for c in product_codes]}

monkeypatch.setattr(
"auth_api.utils.pay.RestService.get",
lambda *_args, **_kwargs: MockFeeResponse(),
)
monkeypatch.setattr(
"auth_api.utils.pay.RestService.get_service_account_token",
lambda *_args, **_kwargs: "mock-token",
)
8 changes: 4 additions & 4 deletions queue_services/account-mailer/poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading