Skip to content

Commit 7bc0e2b

Browse files
Jxiorenovate[bot]dependabot[bot]
authored
14853 - Restore Create Task When New Product Add by GOVM/GOVN (#3630) (#3632)
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 343d925 commit 7bc0e2b

File tree

26 files changed

+688
-106
lines changed

26 files changed

+688
-106
lines changed

auth-api/src/auth_api/exceptions/errors.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ class Error(Enum):
125125
)
126126
BCEID_USERS_CANT_BE_OWNERS = "BCEID Users cant be owners", HTTPStatus.BAD_REQUEST
127127
PAYMENT_ACCOUNT_UPSERT_FAILED = "Account upsert failed in Pay", HTTPStatus.INTERNAL_SERVER_ERROR
128+
ACCOUNT_FEES_FETCH_FAILED = "Failed to fetch account fees from Pay API", HTTPStatus.INTERNAL_SERVER_ERROR
128129
GOVM_ACCOUNT_DATA_MISSING = (
129130
"GOVM account creation needs payment info , gl code and mailing address",
130131
HTTPStatus.BAD_REQUEST,

auth-api/src/auth_api/models/dataclass.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ class TaskSearch: # pylint: disable=too-many-instance-attributes
183183
submitted_sort_order: str = "asc"
184184
page: int = 1
185185
limit: int = 10
186+
action: str = ""
186187

187188

188189
@dataclass
@@ -201,6 +202,7 @@ class ProductReviewTask:
201202

202203
org_id: str
203204
org_name: str
205+
org_access_type: str
204206
product_code: str
205207
product_description: str
206208
product_subscription_id: int

auth-api/src/auth_api/models/task.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ def fetch_tasks(cls, task_search: TaskSearch):
6666
query = query.filter(Task.type == task_search.type)
6767
if task_search.status:
6868
query = query.filter(Task.status.in_(task_search.status))
69+
if task_search.action:
70+
query = query.filter(Task.action.in_(task_search.action))
6971
start_date = None
7072
end_date = None
7173
if task_search.start_date:

auth-api/src/auth_api/resources/v1/task.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ def get_tasks():
4646
relationship_status=request.args.get("relationshipStatus", None),
4747
type=request.args.get("type", None),
4848
status=request.args.getlist("status", None),
49+
action=request.args.getlist("action", None),
4950
modified_by=request.args.get("modifiedBy", None),
5051
submitted_sort_order=request.args.get("submittedSortOrder", None),
5152
page=int(request.args.get("page", 1)),

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

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -141,9 +141,16 @@ def create_org(org_info: dict, user_id):
141141
# create the membership record for this user if its not created by staff and access_type is anonymous
142142
Org.create_membership(org, user_id)
143143

144+
# Send an email to staff to remind review the pending account
145+
is_staff_review_needed = access_type == AccessType.GOVN.value or (
146+
access_type in (AccessType.EXTRA_PROVINCIAL.value, AccessType.REGULAR_BCEID.value)
147+
and not AffidavitModel.find_approved_by_user_id(user_id=user_id)
148+
and current_app.config.get("SKIP_STAFF_APPROVAL_BCEID") is False
149+
)
150+
144151
if product_subscriptions is not None:
145152
ProductService.create_product_subscription(
146-
org.id, subscription_data={"subscriptions": product_subscriptions}, skip_auth=True
153+
org.id, subscription_data={"subscriptions": product_subscriptions}, skip_auth=True, staff_review_for_create_org=is_staff_review_needed
147154
)
148155

149156
ProductService.create_subscription_from_bcol_profile(org.id, bcol_profile_flags)
@@ -153,13 +160,6 @@ def create_org(org_info: dict, user_id):
153160
if payment_account_status == PaymentAccountStatus.FAILED and error is not None:
154161
current_app.logger.warning(f"Account update payment Error: {error}")
155162

156-
# Send an email to staff to remind review the pending account
157-
is_staff_review_needed = access_type == AccessType.GOVN.value or (
158-
access_type in (AccessType.EXTRA_PROVINCIAL.value, AccessType.REGULAR_BCEID.value)
159-
and not AffidavitModel.find_approved_by_user_id(user_id=user_id)
160-
and current_app.config.get("SKIP_STAFF_APPROVAL_BCEID") is False
161-
)
162-
163163
if is_staff_review_needed:
164164
Org._create_staff_review_task(org, UserModel.find_by_jwt_token())
165165

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

Lines changed: 74 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@
5656
get_product_notification_data,
5757
get_product_notification_type,
5858
)
59-
from auth_api.utils.roles import CLIENT_ADMIN_ROLES, CLIENT_AUTH_ROLES, STAFF
59+
from auth_api.utils.pay import get_account_fees
60+
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

6263
from .activity_log_publisher import ActivityLogPublisher
@@ -156,6 +157,42 @@ def resubmit_product_subscription(org_id, subscription_data: dict[str, Any], ski
156157

157158
return Product.get_all_product_subscription(org_id=org_id, skip_auth=True)
158159

160+
@staticmethod
161+
def _check_gov_org_add_product_previously_approved(
162+
org_id: int,
163+
product_code: str,
164+
account_fees: list[str]
165+
) -> tuple[bool, Any]:
166+
"""Check if GOV org's account fee product was previously approved (NEW_PRODUCT_FEE_REVIEW task)."""
167+
inactive_sub = ProductSubscriptionModel.find_by_org_id_product_code(
168+
org_id=org_id, product_code=product_code, valid_statuses=(ProductSubscriptionStatus.INACTIVE.value,)
169+
)
170+
if not inactive_sub or product_code not in account_fees:
171+
return False, None
172+
task_add_product = TaskModel.find_by_task_relationship_id(
173+
inactive_sub.id, TaskRelationshipType.PRODUCT.value, TaskStatus.COMPLETED.value
174+
)
175+
task_create_org = TaskModel.find_by_task_relationship_id(
176+
org_id, TaskRelationshipType.ORG.value, TaskStatus.COMPLETED.value
177+
)
178+
product_invalid = (
179+
task_add_product is None or (
180+
task_add_product.action in (TaskAction.NEW_PRODUCT_FEE_REVIEW.value, TaskAction.PRODUCT_REVIEW.value)
181+
and task_add_product.relationship_status != TaskRelationshipStatus.ACTIVE.value
182+
)
183+
)
184+
if product_invalid:
185+
org_invalid = (
186+
task_create_org is None or (
187+
task_create_org.action in (TaskAction.AFFIDAVIT_REVIEW.value, TaskAction.ACCOUNT_REVIEW.value)
188+
and task_create_org.relationship_status != TaskRelationshipStatus.ACTIVE.value
189+
)
190+
)
191+
192+
if org_invalid:
193+
return False, None
194+
return True, inactive_sub
195+
159196
@staticmethod
160197
def _is_previously_approved(org_id: int, product_code: str):
161198
"""Check if this product has a task that was previously approved."""
@@ -170,7 +207,7 @@ def _is_previously_approved(org_id: int, product_code: str):
170207
)
171208
if task is None or (
172209
task.relationship_status != TaskRelationshipStatus.ACTIVE.value
173-
and task.action == TaskAction.PRODUCT_REVIEW.value
210+
and task.action in (TaskAction.PRODUCT_REVIEW.value, TaskAction.NEW_PRODUCT_FEE_REVIEW.value)
174211
):
175212
return False, None
176213

@@ -183,6 +220,7 @@ def create_product_subscription(
183220
is_new_transaction: bool = True,
184221
skip_auth=False,
185222
auto_approve=False,
223+
staff_review_for_create_org=False,
186224
):
187225
"""Create product subscription for the user.
188226
@@ -197,7 +235,9 @@ def create_product_subscription(
197235
check_auth(one_of_roles=(*CLIENT_ADMIN_ROLES, STAFF), org_id=org_id)
198236

199237
subscriptions_list = subscription_data.get("subscriptions")
238+
account_fees = get_account_fees(org) if org.access_type in GOV_ORG_TYPES and not staff_review_for_create_org else []
200239
for subscription in subscriptions_list:
240+
auto_approve_current = auto_approve
201241
product_code = subscription.get("productCode")
202242
if ProductSubscriptionModel.find_by_org_id_product_code(org_id, product_code):
203243
raise BusinessException(Error.PRODUCT_SUBSCRIPTION_EXISTS, None)
@@ -206,11 +246,20 @@ def create_product_subscription(
206246
# Check if product requires system admin, if yes abort
207247
if product_model.need_system_admin:
208248
check_auth(system_required=True, org_id=org_id)
209-
previously_approved, inactive_sub = Product._is_previously_approved(org_id, product_code)
249+
250+
if org.access_type in GOV_ORG_TYPES and not staff_review_for_create_org:
251+
previously_approved, inactive_sub = Product._check_gov_org_add_product_previously_approved(
252+
org.id, product_code, account_fees
253+
)
254+
else:
255+
previously_approved, inactive_sub = Product._is_previously_approved(org_id, product_code)
256+
210257
if previously_approved:
211-
auto_approve = True
258+
auto_approve_current = True
212259

213-
subscription_status = Product.find_subscription_status(org, product_model, auto_approve)
260+
subscription_status = Product.find_subscription_status(
261+
org, product_model, auto_approve_current, staff_review_for_create_org
262+
)
214263
product_subscription = Product._subscribe_and_publish_activity(
215264
SubscriptionRequest(
216265
org_id=org_id,
@@ -245,6 +294,7 @@ def create_product_subscription(
245294
ProductReviewTask(
246295
org_id=org.id,
247296
org_name=org.name,
297+
org_access_type=org.access_type,
248298
product_code=product_subscription.product_code,
249299
product_description=product_model.description,
250300
product_subscription_id=product_subscription.id,
@@ -374,11 +424,14 @@ def _reset_subscription_and_review_task(
374424
@staticmethod
375425
def _create_review_task(review_task: ProductReviewTask):
376426
task_type = review_task.product_description
377-
action_type = (
378-
TaskAction.QUALIFIED_SUPPLIER_REVIEW.value
379-
if review_task.product_code in QUALIFIED_SUPPLIER_PRODUCT_CODES
380-
else TaskAction.PRODUCT_REVIEW.value
381-
)
427+
428+
required_review_types = {AccessType.GOVM.value, AccessType.GOVN.value}
429+
if review_task.product_code in QUALIFIED_SUPPLIER_PRODUCT_CODES:
430+
action_type = TaskAction.QUALIFIED_SUPPLIER_REVIEW.value
431+
elif review_task.org_access_type in required_review_types:
432+
action_type = TaskAction.NEW_PRODUCT_FEE_REVIEW.value
433+
else:
434+
action_type = TaskAction.PRODUCT_REVIEW.value
382435

383436
task_info = {
384437
"name": review_task.org_name,
@@ -396,14 +449,13 @@ def _create_review_task(review_task: ProductReviewTask):
396449
TaskService.create_task(task_info, False)
397450

398451
@staticmethod
399-
def find_subscription_status(org, product_model, auto_approve=False):
452+
def find_subscription_status(org, product_model, auto_approve=False, staff_review_for_create_org=False):
400453
"""Return the subscriptions status based on org type."""
401-
# GOVM accounts has default active subscriptions
402-
skip_review_types = [AccessType.GOVM.value]
403-
if product_model.need_review and auto_approve is False:
454+
skip_review = org.access_type in GOV_ORG_TYPES and staff_review_for_create_org # prevent create second task when it's already added a staff review when creating org
455+
if (product_model.need_review or org.access_type in GOV_ORG_TYPES) and not auto_approve:
404456
return (
405457
ProductSubscriptionStatus.ACTIVE.value
406-
if (org.access_type in skip_review_types)
458+
if skip_review
407459
else ProductSubscriptionStatus.PENDING_STAFF_REVIEW.value
408460
)
409461
return ProductSubscriptionStatus.ACTIVE.value
@@ -455,7 +507,10 @@ def get_all_product_subscription(org_id, skip_auth=False, **kwargs):
455507
check_auth(one_of_roles=(*CLIENT_AUTH_ROLES, STAFF), org_id=org_id)
456508

457509
product_subscriptions: list[ProductSubscriptionModel] = ProductSubscriptionModel.find_by_org_ids([org_id])
458-
subscriptions_dict = {x.product_code: x.status_code for x in product_subscriptions}
510+
subscription_by_code = {
511+
sub.product_code: sub
512+
for sub in product_subscriptions
513+
}
459514

460515
# Include hidden products only for staff and SBC staff
461516
include_hidden = (
@@ -467,9 +522,9 @@ def get_all_product_subscription(org_id, skip_auth=False, **kwargs):
467522

468523
products = Product.get_products(include_hidden=include_hidden, staff_check=False)
469524
for product in products:
470-
product["subscriptionStatus"] = subscriptions_dict.get(
471-
product.get("code"), ProductSubscriptionStatus.NOT_SUBSCRIBED.value
472-
)
525+
sub = subscription_by_code.get(product.get("code"))
526+
product["subscriptionStatus"] = getattr(sub, "status_code", ProductSubscriptionStatus.NOT_SUBSCRIBED.value)
527+
product["id"] = getattr(sub, "id", None)
473528

474529
return products
475530

@@ -483,7 +538,6 @@ def update_product_subscription(product_sub_info: ProductSubscriptionInfo, is_ne
483538
is_hold = product_sub_info.is_hold
484539
org_id = product_sub_info.org_id
485540
org_name = product_sub_info.org_name
486-
487541
# Approve/Reject Product subscription
488542
product_subscription: ProductSubscriptionModel = ProductSubscriptionModel.find_by_id(product_subscription_id)
489543

@@ -504,7 +558,6 @@ def update_product_subscription(product_sub_info: ProductSubscriptionInfo, is_ne
504558
product_model: ProductCodeModel = ProductCodeModel.find_by_code(product_subscription.product_code)
505559
# Find admin email addresses
506560
admin_emails = UserService.get_admin_emails_for_org(org_id)
507-
508561
if admin_emails != "" and not is_hold:
509562
Product.send_product_subscription_notification(
510563
ProductNotificationInfo(

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,7 @@ class TaskAction(Enum):
323323
ACCOUNT_REVIEW = "ACCOUNT_REVIEW"
324324
PRODUCT_REVIEW = "PRODUCT_REVIEW"
325325
QUALIFIED_SUPPLIER_REVIEW = "QUALIFIED_SUPPLIER_REVIEW"
326+
NEW_PRODUCT_FEE_REVIEW = "NEW_PRODUCT_FEE_REVIEW"
326327

327328

328329
class ActivityAction(Enum):

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/src/auth_api/utils/roles.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
from enum import Enum
1717

18-
from .enums import OrgStatus, OrgType, ProductSubscriptionStatus, Status
18+
from .enums import AccessType, OrgStatus, OrgType, ProductSubscriptionStatus, Status
1919

2020

2121
class Role(Enum):
@@ -87,3 +87,5 @@ class Role(Enum):
8787
EXCLUDED_FIELDS = ("status_code", "type_code")
8888

8989
PREMIUM_ORG_TYPES = (OrgType.PREMIUM.value, OrgType.SBC_STAFF.value, OrgType.STAFF.value)
90+
91+
GOV_ORG_TYPES = (AccessType.GOVM.value, AccessType.GOVN.value)

0 commit comments

Comments
 (0)