Skip to content

Feat/deactivate privilege #563

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from 4 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
Binary file modified backend/compact-connect/docs/design/license-events-diagram.pdf
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,7 @@ def _generate_privilege_record(
'compactTransactionId': compact_transaction_id,
'attestations': attestations,
'privilegeId': privilege_id,
'persistedStatus': 'active',
}

@logger_inject_kwargs(logger, 'compact', 'provider_id', 'compact_transaction_id')
Expand Down Expand Up @@ -889,3 +890,93 @@ def find_home_state_license(self, *, compact: str, provider_id: str, licenses: l
# The user has not registered with the system and set a home jurisdiction
logger.info('No home jurisdiction selection found. Cannot determine home state license')
return None

@logger_inject_kwargs(logger, 'compact', 'provider_id', 'jurisdiction')
def deactivate_privilege(self, *, compact: str, provider_id: str, jurisdiction: str) -> None:
"""
Deactivate a privilege by setting its persistedStatus to inactive.
This will create a history record and update the provider record.

:param str compact: The compact name
:param str provider_id: The provider ID
:param str jurisdiction: The jurisdiction postal abbreviation
:raises CCNotFoundException: If the privilege record is not found
"""
# Get the privilege record
privilege_records = self.config.provider_table.query(
KeyConditionExpression=Key('pk').eq(f'{compact}#PROVIDER#{provider_id}')
& Key('sk').eq(f'{compact}#PROVIDER#privilege/{jurisdiction}#'),
)['Items']

# Find the main privilege record (not history records)
privilege_record_schema = PrivilegeRecordSchema()
privilege_record = next(
(privilege_record_schema.load(record) for record in privilege_records if record['type'] == 'privilege'),
None,
)

if privilege_record is None:
raise CCNotFoundException(f'Privilege not found for jurisdiction {jurisdiction}')

# If already inactive, do nothing
if privilege_record.get('persistedStatus', 'active') == 'inactive':
return

# Create the update record
# Use the schema to generate the update record with proper pk/sk
privilege_update_record = PrivilegeUpdateRecordSchema().dump(
{
'type': 'privilegeUpdate',
'updateType': 'deactivation',
'providerId': provider_id,
'compact': compact,
'jurisdiction': jurisdiction,
'dateOfUpdate': self.config.current_standard_datetime.isoformat(),
'previous': {
# We're relying on the schema to trim out unneeded fields
**privilege_record,
},
'updatedValues': {
'persistedStatus': 'inactive',
},
}
)

# Update the privilege record and create history record
self.config.dynamodb_client.transact_write_items(
TransactItems=[
{
'Update': {
'TableName': self.config.provider_table.name,
'Key': {
'pk': {'S': f'{compact}#PROVIDER#{provider_id}'},
'sk': {'S': f'{compact}#PROVIDER#privilege/{jurisdiction}#'},
},
'UpdateExpression': 'SET persistedStatus = :status, dateOfUpdate = :dateOfUpdate',
'ExpressionAttributeValues': {
':status': {'S': 'inactive'},
':dateOfUpdate': {'S': self.config.current_standard_datetime.isoformat()},
},
},
},
{
'Put': {
'TableName': self.config.provider_table.name,
'Item': TypeSerializer().serialize(privilege_update_record)['M'],
},
},
{
'Update': {
'TableName': self.config.provider_table.name,
'Key': {
'pk': {'S': f'{compact}#PROVIDER#{provider_id}'},
'sk': {'S': f'{compact}#PROVIDER'},
},
'UpdateExpression': 'DELETE privilegeJurisdictions :jurisdiction',
'ExpressionAttributeValues': {
':jurisdiction': {'SS': [jurisdiction]},
},
},
},
],
)
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,21 @@
from cc_common.data_model.schema.fields import ActiveInactive, Compact, Jurisdiction, UpdateType


class AttestationVersionResponseSchema(Schema):
"""
This schema is intended to be used by any api response in the system which needs to track which attestations have
been accepted by a user (i.e. when purchasing privileges).

This schema is intended to be used as a nested field in other schemas.

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

attestationId = String(required=True, allow_none=False)
version = String(required=True, allow_none=False)


class PrivilegeUpdatePreviousGeneralResponseSchema(ForgivingSchema):
"""
A snapshot of a previous state of a privilege object
Expand All @@ -14,12 +29,15 @@ class PrivilegeUpdatePreviousGeneralResponseSchema(ForgivingSchema):
Python -> load() -> API
"""

dateOfUpdate = Raw(required=True, allow_none=False)
# list of attestations that were accepted when purchasing this privilege
attestations = List(Nested(AttestationVersionResponseSchema()), required=False, allow_none=False)
compactTransactionId = String(required=False, allow_none=False)
dateOfExpiration = Raw(required=True, allow_none=False)
dateOfIssuance = Raw(required=True, allow_none=False)
dateOfRenewal = Raw(required=True, allow_none=False)
dateOfExpiration = Raw(required=True, allow_none=False)
dateOfUpdate = Raw(required=True, allow_none=False)
persistedStatus = ActiveInactive(required=True, allow_none=False)
privilegeId = String(required=True, allow_none=False)
compactTransactionId = String(required=False, allow_none=False)


class PrivilegeUpdateGeneralResponseSchema(ForgivingSchema):
Expand All @@ -41,21 +59,6 @@ class PrivilegeUpdateGeneralResponseSchema(ForgivingSchema):
updatedValues = Nested(PrivilegeUpdatePreviousGeneralResponseSchema(partial=True), required=True, allow_none=False)


class AttestationVersionResponseSchema(Schema):
"""
This schema is intended to be used by any api response in the system which needs to track which attestations have
been accepted by a user (i.e. when purchasing privileges).

This schema is intended to be used as a nested field in other schemas.

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

attestationId = String(required=True, allow_none=False)
version = String(required=True, allow_none=False)


class PrivilegeGeneralResponseSchema(ForgivingSchema):
"""
A snapshot of a previous state of a privilege object
Expand All @@ -68,15 +71,16 @@ class PrivilegeGeneralResponseSchema(ForgivingSchema):
providerId = Raw(required=True, allow_none=False)
compact = Compact(required=True, allow_none=False)
jurisdiction = Jurisdiction(required=True, allow_none=False)
# list of attestations that were accepted when purchasing this privilege
attestations = List(Nested(AttestationVersionResponseSchema()), required=False, allow_none=False)
# the id of the transaction that was made when the user purchased the privilege
compactTransactionId = String(required=False, allow_none=False)
dateOfExpiration = Raw(required=True, allow_none=False)
dateOfIssuance = Raw(required=True, allow_none=False)
dateOfRenewal = Raw(required=True, allow_none=False)
dateOfExpiration = Raw(required=True, allow_none=False)
dateOfUpdate = Raw(required=True, allow_none=False)
# the id of the transaction that was made when the user purchased the privilege
compactTransactionId = String(required=False, allow_none=False)
history = List(Nested(PrivilegeUpdateGeneralResponseSchema, required=False, allow_none=False))
persistedStatus = ActiveInactive(required=True, allow_none=False)
# the human-friendly identifier for this privilege
privilegeId = String(required=True, allow_none=False)
status = ActiveInactive(required=True, allow_none=False)
history = List(Nested(PrivilegeUpdateGeneralResponseSchema, required=False, allow_none=False))
# list of attestations that were accepted when purchasing this privilege
attestations = List(Nested(AttestationVersionResponseSchema()), required=False, allow_none=False)
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
# ruff: noqa: N801, N815, ARG002 invalid-name unused-argument
from datetime import date, datetime

from marshmallow import Schema, post_dump, pre_dump, pre_load
from marshmallow.fields import UUID, Date, DateTime, List, Nested, String

from cc_common.config import config
from cc_common.data_model.schema.base_record import BaseRecordSchema, CalculatedStatusRecordSchema, ForgivingSchema
from cc_common.data_model.schema.base_record import BaseRecordSchema, ForgivingSchema
from cc_common.data_model.schema.common import ChangeHashMixin, ensure_value_is_datetime
from cc_common.data_model.schema.fields import Compact, Jurisdiction, UpdateType
from cc_common.data_model.schema.fields import ActiveInactive, Compact, Jurisdiction, UpdateType


class AttestationVersionRecordSchema(Schema):
Expand All @@ -24,7 +26,7 @@ class AttestationVersionRecordSchema(Schema):


@BaseRecordSchema.register_schema('privilege')
class PrivilegeRecordSchema(CalculatedStatusRecordSchema):
class PrivilegeRecordSchema(BaseRecordSchema):
"""
Schema for privilege records in the license data table

Expand All @@ -48,6 +50,13 @@ class PrivilegeRecordSchema(CalculatedStatusRecordSchema):
attestations = List(Nested(AttestationVersionRecordSchema()), required=True, allow_none=False)
# the human-friendly identifier for this privilege
privilegeId = String(required=True, allow_none=False)
# the persisted status of the privilege, which can be manually set to inactive
persistedStatus = ActiveInactive(required=True, allow_none=False)

# This field is the actual status referenced by the system, which is determined by the expiration date
# in addition to the persistedStatus. This should never be written to the DB. It is calculated
# whenever the record is loaded.
status = ActiveInactive(required=True, allow_none=False)

@pre_dump
def generate_pk_sk(self, in_data, **kwargs): # noqa: ARG001 unused-argument
Expand All @@ -68,6 +77,27 @@ def _enforce_datetimes(self, in_data, **kwargs):

return in_data

@pre_dump
def remove_status_field_if_present(self, in_data, **kwargs):
"""Remove the status field before dumping to the database"""
in_data.pop('status', None)
return in_data

@pre_load
def _calculate_status(self, in_data, **kwargs):
"""Determine the status of the record based on the expiration date and persistedStatus"""
in_data['status'] = (
'active'
if (
in_data.get('persistedStatus', 'active') == 'active'
and date.fromisoformat(in_data['dateOfExpiration'])
> datetime.now(tz=config.expiration_date_resolution_timezone).date()
)
else 'inactive'
)

return in_data


class PrivilegeUpdatePreviousRecordSchema(ForgivingSchema):
"""
Expand All @@ -84,6 +114,7 @@ class PrivilegeUpdatePreviousRecordSchema(ForgivingSchema):
privilegeId = String(required=True, allow_none=False)
compactTransactionId = String(required=False, allow_none=False)
attestations = List(Nested(AttestationVersionRecordSchema()), required=False, allow_none=False)
persistedStatus = ActiveInactive(required=False, allow_none=False)


@BaseRecordSchema.register_schema('privilegeUpdate')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ def _load_compact_configuration_data(self):
# compact and jurisdiction records go in the compact configuration table
self._compact_configuration_table.put_item(Item=record)

def _load_provider_data(self):
def _load_provider_data(self) -> str:
"""Use the canned test resources to load a basic provider to the DB"""
test_resources = glob('../common/tests/resources/dynamo/provider.json')

Expand All @@ -218,6 +218,7 @@ def privilege_jurisdictions_to_set(obj: dict):

logger.debug('Loading resource, %s: %s', resource, str(record))
self._provider_table.put_item(Item=record)
return record['providerId']

def _load_license_data(self, status: str = 'active', expiration_date: str = None):
"""Use the canned test resources to load a basic provider to the DB"""
Expand Down
Loading