Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
b5af10f
[PRMP-1464] introduce model, mock data, start implementing hcw api se…
steph-torres-nhs Feb 19, 2026
2ca5d16
[PRMP-1464] implement get practitioner using hcw api
steph-torres-nhs Feb 20, 2026
386ad9f
[PRMP-1464] remove comment
steph-torres-nhs Feb 23, 2026
dfe3a71
[PRMP-1464] add headers
steph-torres-nhs Feb 23, 2026
646d908
[PRMP-1464] remove comment
steph-torres-nhs Feb 23, 2026
eb9416c
[PRMP-1444] cread user restrictions model
steph-torres-nhs Feb 16, 2026
ce7292e
[PRMP-1444] adjust ods code attribute
steph-torres-nhs Feb 16, 2026
ea03142
[PRMP-1444] update model
steph-torres-nhs Feb 18, 2026
ae7b929
[PRMP-1444] address pr comments
steph-torres-nhs Feb 19, 2026
3f63d8a
[PRMP-1444] change status to is active
steph-torres-nhs Feb 19, 2026
5f9e3d1
[PRMP-1444] ensure model aligns with table
steph-torres-nhs Feb 19, 2026
d0d7c40
[PRMP-1465] changes
SWhyteAnswer Feb 24, 2026
3b82540
[PRMP-1465] changes
SWhyteAnswer Feb 24, 2026
aac606f
[PRMP-1464] tests
SWhyteAnswer Feb 24, 2026
5b97c65
[PRMP-1465] changes
SWhyteAnswer Feb 24, 2026
57cab4e
[PRMP-1465] edit comment
SWhyteAnswer Feb 25, 2026
5af470e
[PRMP-1465] corrected db env
SWhyteAnswer Feb 27, 2026
736b579
[PRMP-1465] new func for parse bod
SWhyteAnswer Feb 27, 2026
0fb099b
[PRMP-1465] removing json check
SWhyteAnswer Feb 27, 2026
295439d
[PRMP-1465] new util for ods/creator
SWhyteAnswer Feb 27, 2026
da3905c
[PRMP-1465] ods to cust
SWhyteAnswer Feb 27, 2026
e2edc22
[PRMP-1465] custom exception
SWhyteAnswer Feb 27, 2026
bc8fbc4
[PRMP-1465] response model
SWhyteAnswer Feb 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions lambdas/handlers/post_user_restriction_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import json

from pydantic import ValidationError
from services.post_user_restriction_service import PostUserRestrictionService
from utils.audit_logging_setup import LoggingService
from utils.decorators.ensure_env_var import ensure_environment_variables
from utils.decorators.handle_lambda_exceptions import handle_lambda_exceptions
from utils.decorators.override_error_check import override_error_check
from utils.decorators.set_audit_arg import set_request_context_for_logging
from utils.exceptions import HealthcareWorkerAPIException, HealthcareWorkerPractitionerModelException, OdsErrorException, UserRestrictionException
from utils.lambda_response import ApiGatewayResponse
from utils.ods_utils import extract_creator_and_ods_code_from_request_context

logger = LoggingService(__name__)


def parse_body(body: str | None) -> tuple[tuple[str, str] | None, object]:
if not body:
logger.error("Missing request body")
return None, ApiGatewayResponse(
400, "Missing request body", "POST"
).create_api_gateway_response()

payload = json.loads(body)

smart_card_id = payload.get("smart_card_id")
nhs_number = payload.get("nhs_number")
if not smart_card_id or not nhs_number:
logger.error("Missing required fields")
return None, ApiGatewayResponse(
400, "Missing required fields", "POST"
).create_api_gateway_response()

return (smart_card_id, nhs_number), None


@set_request_context_for_logging
@override_error_check
@ensure_environment_variables(
names=[
"RESTRICTIONS_TABLE_NAME",
"HEALTHCARE_WORKER_API_URL",
]
)
@handle_lambda_exceptions
def lambda_handler(event, context):
logger.info("Starting create user restriction process")

parsed, error = parse_body(event.get("body"))
if error:
return error
smart_card_id, nhs_number = parsed

try:
creator, ods_code = extract_creator_and_ods_code_from_request_context()
except OdsErrorException:
logger.error("Missing user context")
return ApiGatewayResponse(
400, "Missing user context", "POST"
).create_api_gateway_response()

service = PostUserRestrictionService()
try:
response = service.create_restriction(
smart_card_id=smart_card_id,
nhs_number=nhs_number,
custodian=ods_code,
creator=creator,
)
# Translate service validation errors into a 400 response.
except UserRestrictionException as exc:
logger.error(exc)
return ApiGatewayResponse(
400, str(exc), "POST"
).create_api_gateway_response()
except HealthcareWorkerAPIException as exc:
logger.error(exc)
return ApiGatewayResponse(
exc.status_code, exc.message, "POST"
).create_api_gateway_response()
except HealthcareWorkerPractitionerModelException as exc:
logger.error(exc)
return ApiGatewayResponse(
400, "Unable to process restricted user information", "POST"
).create_api_gateway_response()
except ValidationError as exc:
logger.error(exc)
return ApiGatewayResponse(
400, "Invalid request body", "POST"
).create_api_gateway_response()

# Successful create returns the full restriction payload.
response_body = json.dumps(response.model_dump(by_alias=True))

return ApiGatewayResponse(
201, response_body, "POST"
).create_api_gateway_response()
Empty file.
34 changes: 34 additions & 0 deletions lambdas/models/user_restrictions/user_restriction_response.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from datetime import datetime, timezone

from pydantic import BaseModel, ConfigDict, Field
from pydantic.alias_generators import to_pascal


class UserRestrictionResponse(BaseModel):
model_config = ConfigDict(
validate_by_alias=True,
validate_by_name=True,
alias_generator=to_pascal,
populate_by_name=True,
)

id: str = Field(alias="ID")
nhs_number: str = Field(alias="NHSNumber")
patient_name: str | None = None
restricted_smartcard_id: str
restricted_user_name: str
created: str

@classmethod
def from_restriction(cls, restriction, restricted_user_name: str) -> "UserRestrictionResponse":
created_iso = datetime.fromtimestamp(
restriction.created, tz=timezone.utc
).strftime("%Y-%m-%dT%H:%M:%SZ")

return cls(
id=restriction.id,
nhs_number=restriction.nhs_number,
restricted_smartcard_id=restriction.restricted_user,
restricted_user_name=restricted_user_name,
created=created_iso,
)
24 changes: 16 additions & 8 deletions lambdas/models/user_restrictions/user_restrictions.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,26 @@ class UserRestriction(BaseModel):
creator: str = Field(
alias=UserRestrictionsFields.CREATOR,
)
removed_by: str | None = Field(
alias=UserRestrictionsFields.REMOVED_BY,
default=None,
)
removed_by: str | None = Field(alias=UserRestrictionsFields.REMOVED_BY, default=None)
is_active: bool = Field(default=True)
last_updated: int = Field(
default_factory=lambda: int(datetime.now(timezone.utc).timestamp()),
)

def model_dump_camel_case(self, *args, **kwargs):
model_dump_results = self.model_dump(*args, **kwargs)
camel_case_model_dump_results = {}
for key in model_dump_results:
camel_case_model_dump_results[to_camel(key)] = model_dump_results[key]
return camel_case_model_dump_results
return self.camelize(model_dump_results)

def camelize(self, model: dict) -> dict:
camel_case_dict = {}
for key, value in model.items():
if isinstance(value, dict):
return self.camelize(value)
if isinstance(value, list):
result = []
for item in value:
result.append(self.camelize(item))
value = result
camel_case_dict[to_camel(key)] = value

return camel_case_dict
49 changes: 49 additions & 0 deletions lambdas/services/post_user_restriction_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import os

from models.user_restrictions.user_restriction_response import UserRestrictionResponse
from models.user_restrictions.user_restrictions import UserRestriction, UserRestrictionsFields
from services.base.dynamo_service import DynamoDBService
from services.user_restrictions.healthcare_worker_service import HealthCareWorkerApiService
from utils.audit_logging_setup import LoggingService
from utils.exceptions import UserRestrictionException

logger = LoggingService(__name__)


class PostUserRestrictionService:
def __init__(self):
self.table_name = os.getenv("RESTRICTIONS_TABLE_NAME")
self.db_service = DynamoDBService()
self.healthcare_service = HealthCareWorkerApiService()

def create_restriction(
self,
smart_card_id: str,
nhs_number: str,
custodian: str,
creator: str,
) -> UserRestrictionResponse:
if smart_card_id == creator:
raise UserRestrictionException("You cannot create a restriction for yourself")

practitioner = self.healthcare_service.get_practitioner(smart_card_id)
restricted_user_name = f"{practitioner.first_name} {practitioner.last_name}"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

believe we are returning this the the frontend, may need to change what is returned from this function


# Build the domain model that will be persisted to DB.
restriction = UserRestriction(
restricted_user=smart_card_id,
nhs_number=nhs_number,
custodian=custodian,
creator=creator,
)

# Persist the new restriction in the configured table.
self.db_service.create_item(
table_name=self.table_name,
item=restriction.model_dump(by_alias=True, exclude_none=True),
key_name=UserRestrictionsFields.ID.value,
)

logger.info("Created user restriction")
return UserRestrictionResponse.from_restriction(restriction, restricted_user_name)

Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
import requests
from pydantic import ValidationError

from models.user_restrictions.practitioner import Practitioner
from pydantic import ValidationError

from models.user_restrictions.practitioner import Practitioner
from services.base.nhs_oauth_service import NhsOauthService
from services.base.ssm_service import SSMService
Expand Down
Loading
Loading