Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
432 changes: 216 additions & 216 deletions backend/compact-connect/docs/api-specification/latest-oas30.json

Large diffs are not rendered by default.

6,227 changes: 3,113 additions & 3,114 deletions backend/compact-connect/docs/internal/api-specification/latest-oas30.json

Large diffs are not rendered by default.

282 changes: 141 additions & 141 deletions backend/compact-connect/docs/internal/postman/postman-collection.json

Large diffs are not rendered by default.

38 changes: 19 additions & 19 deletions backend/compact-connect/docs/postman/postman-collection.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -55,24 +55,6 @@ class ProviderRecordUtility:
A class for housing official logic for how to handle provider records without making database queries.
"""

@staticmethod
def sanitize_provider_account_profile_fields_from_response(provider_response: dict) -> dict:
"""
Remove sensitive profile related fields from a provider response.

These fields are only present if the provider has requested an email change or account recovery.
They should not be present in the provider response for any endpoint.

:param provider_response: The provider response to sanitize
:return: The provider response with sensitive profile related fields removed
"""
provider_response.pop('emailVerificationExpiry', None)
provider_response.pop('emailVerificationCode', None)
provider_response.pop('pendingEmailAddress', None)
provider_response.pop('recoveryToken', None)
provider_response.pop('recoveryExpiry', None)
return provider_response

@staticmethod
def get_records_of_type(
provider_records: Iterable[dict],
Expand Down Expand Up @@ -245,8 +227,8 @@ def populate_provider_record(

@staticmethod
def get_enriched_history_with_synthetic_updates_from_privilege(
privilege: dict,
history: list[dict],
privilege: dict,
history: list[dict],
) -> list[dict]:
"""
Enrich the privilege history with 'synthetic updates'.
Expand Down Expand Up @@ -367,8 +349,7 @@ def get_enriched_history_with_synthetic_updates_from_privilege(

@staticmethod
def construct_simplified_privilege_history_object(
privilege_data: list[dict],
should_include_encumbrance_details: bool = True
privilege_data: list[dict], should_include_encumbrance_details: bool = True
) -> dict:
"""
Construct a simplified list of history events to be easily consumed by the front end
Expand All @@ -393,10 +374,7 @@ def construct_simplified_privilege_history_object(
and should_include_encumbrance_details
):
event['note'] = event['encumbranceDetails']['clinicalPrivilegeActionCategory']
elif (
event['updateType'] == UpdateCategory.DEACTIVATION
and event.get('deactivationDetails')
):
elif event['updateType'] == UpdateCategory.DEACTIVATION and event.get('deactivationDetails'):
event['note'] = event['deactivationDetails']['note']

unsanitized_history = {
Expand Down Expand Up @@ -787,6 +765,4 @@ def generate_api_response_object(self) -> dict:
provider['privileges'] = privileges
provider['militaryAffiliations'] = military_affiliations

ProviderRecordUtility.sanitize_provider_account_profile_fields_from_response(provider)

return provider
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,4 @@ class AdverseActionGeneralResponseSchema(AdverseActionPublicResponseSchema):
encumbranceType = EncumbranceTypeField(required=True, allow_none=False)
clinicalPrivilegeActionCategory = ClinicalPrivilegeActionCategoryField(required=True, allow_none=False)
liftingUser = Raw(required=False, allow_none=False)
submittingUser = Raw(required=True, allow_none=False)
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# ruff: noqa: F401
from .api import AttestationResponseSchema
from .record import AttestationRecordSchema
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# ruff: noqa: N801, N815 invalid-name
from marshmallow.fields import Boolean, Raw, String
from marshmallow.validate import OneOf

from cc_common.data_model.schema.base_record import ForgivingSchema
from cc_common.data_model.schema.fields import Compact


class AttestationResponseSchema(ForgivingSchema):
"""
Schema for attestation API responses.

This schema validates the response from the attestation endpoint,
matching the actual API behavior as tested.

Serialization direction:
Python -> load() -> API
"""

type = String(required=True, allow_none=False, validate=OneOf(['attestation']))
compact = Compact(required=True, allow_none=False)
attestationId = String(required=True, allow_none=False)
version = String(required=True, allow_none=False)
dateCreated = Raw(required=True, allow_none=False)
dateOfUpdate = Raw(required=True, allow_none=False)
text = String(required=True, allow_none=False)
required = Boolean(required=True, allow_none=False)
displayName = String(required=True, allow_none=False)
description = String(required=True, allow_none=False)
locale = String(required=True, allow_none=False, validate=OneOf(['en']))
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
Jurisdiction,
NationalProviderIdentifier,
Set,
SocialSecurityNumber,
)
from cc_common.data_model.schema.license.api import (
LicenseGeneralResponseSchema,
Expand All @@ -31,6 +32,20 @@
)


class ProviderSSNResponseSchema(ForgivingSchema):
"""
Schema for provider SSN API responses.

This schema validates the response from the provider SSN endpoint,
ensuring the SSN is properly formatted.

Serialization direction:
Python -> load() -> API
"""

ssn = SocialSecurityNumber(required=True, allow_none=False)


class ProviderReadPrivateResponseSchema(ForgivingSchema):
"""
Provider object fields that are sanitized for users with the 'readPrivate' permission.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# ruff: noqa: N801, N815 invalid-name
from cc_common.data_model.schema.base_record import ForgivingSchema
from cc_common.data_model.schema.compact.api import CompactOptionsResponseSchema
from cc_common.data_model.schema.compact.common import COMPACT_TYPE
from cc_common.data_model.schema.jurisdiction.api import JurisdictionOptionsResponseSchema
from cc_common.data_model.schema.jurisdiction.common import JURISDICTION_TYPE
from marshmallow import ValidationError, validates_schema
from marshmallow.fields import Dict, List, String


class PurchasePrivilegeOptionsResponseSchema(ForgivingSchema):
"""
Schema for purchase privilege options response.

This schema validates the overall response structure containing available privilege
purchase options for a provider. It validates each item individually based on its
type field, ensuring only supported types are allowed through.

Serialization direction:
Python -> load() -> API
"""

items = List(
Dict(required=True, allow_none=False), # Allow any dict through since items are validated individually
required=True,
allow_none=False,
)

@validates_schema
def validate_items(self, data, **kwargs): # noqa: ARG002 unused-argument
"""Validate each item in the items list based on its type field."""
if 'items' not in data:
return # Let the field validation handle missing items

items = data['items']
if not isinstance(items, list):
raise ValidationError({'items': ['Expected a list of items']})

sanitized_items = []
for i, item in enumerate(items):
# Ensure item is a dictionary
if not isinstance(item, dict):
raise ValidationError({'items': {i: [f'Invalid item type: expected dict, got {type(item).__name__}']}})

# Ensure item has a type field
if 'type' not in item:
raise ValidationError({'items': {i: ['Item missing required "type" field']}})

# Validate based on type
item_type = item['type']
if item_type == COMPACT_TYPE:
# Validate as compact option
compact_schema = CompactOptionsResponseSchema()
try:
sanitized_items.append(compact_schema.load(item))
except ValidationError as e:
raise ValidationError({'items': {i: {'compact': e.messages}}}) from e
elif item_type == JURISDICTION_TYPE:
# Validate as jurisdiction option
jurisdiction_schema = JurisdictionOptionsResponseSchema()
try:
sanitized_items.append(jurisdiction_schema.load(item))
except ValidationError as e:
raise ValidationError({'items': {i: {'jurisdiction': e.messages}}}) from e
else:
# Reject unsupported types
raise ValidationError({'items': {i: [f'Unsupported item type: {item_type}']}})
data['items'] = sanitized_items # Replace original items with sanitized ones


class TransactionResponseSchema(ForgivingSchema):
"""
Schema for transaction processing response.

This schema validates the response from payment processor transaction operations.

Serialization direction:
Python -> load() -> API
"""

transactionId = String(required=True, allow_none=False)
message = String(required=False, allow_none=False)
lineItems = List(Dict(required=True, allow_none=False), required=False, allow_none=False)
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
from marshmallow.validate import Length, OneOf

from cc_common.config import config
from cc_common.data_model.schema.base_record import ForgivingSchema
from cc_common.data_model.schema.common import StaffUserStatus
from cc_common.data_model.schema.fields import Compact


class UserAttributesAPISchema(Schema):
Expand Down Expand Up @@ -94,3 +96,25 @@ def transform_to_dynamo_permissions(self, data, **kwargs): # noqa: ARG002 unuse
}

return data


class UserMergedResponseSchema(ForgivingSchema):
"""
Schema for merged user response data from the /me endpoint.

This schema validates the merged user data that combines multiple user records
across different compacts into a single response object.

Serialization direction:
Python -> load() -> API
"""

type = String(required=True, allow_none=False, validate=OneOf(['user']))
userId = Raw(required=True, allow_none=False)
status = String(required=True, allow_none=False, validate=OneOf([status.value for status in StaffUserStatus]))
dateOfUpdate = Raw(required=True, allow_none=False)
attributes = Nested(UserAttributesAPISchema(), required=True, allow_none=False)
permissions = Dict(
keys=Compact(), # Key is one compact
values=Nested(CompactPermissionsAPISchema(), required=True, allow_none=False),
)
Loading
Loading