diff --git a/konviva-events/app/app.py b/konviva-events/app/app.py index fce2804..20562ae 100644 --- a/konviva-events/app/app.py +++ b/konviva-events/app/app.py @@ -26,7 +26,7 @@ dyn = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client) def postback(): json_body = app.current_event.json_body - status = json_body['status'] + event_name = json_body['event_name'] score = round(Decimal(json_body['APROVEITAMENTO']), 2) progress = round(Decimal(json_body['ANDAMENTO']), 2) enrollment_id = dyn.collection.get_item( @@ -34,16 +34,29 @@ def postback(): # Post-migration: uncomment the following line # pk='KONVIVA', pk='konviva', - sk=SortKey(json_body['ID_MATRICULA'], path_spec='enrollment_id'), + sk=SortKey( + json_body['ID_MATRICULA'], + path_spec='enrollment_id', + ), ), exc_cls=EnrollmentNotFoundError, ) - if status == 'IN_PROGRESS': - update_progress(enrollment_id, progress, dynamodb_persistence_layer=dyn) - - if status == 'COMPLETED': - set_score(enrollment_id, score, progress, dynamodb_persistence_layer=dyn) + # Make sure webhooks send the correct event names + match event_name: + case 'UPDATING': + update_progress( + enrollment_id, + progress, + dynamodb_persistence_layer=dyn, + ) + case 'COMPLETED': + set_score( + enrollment_id, + score, + progress, + dynamodb_persistence_layer=dyn, + ) return Response(status_code=HTTPStatus.NO_CONTENT) diff --git a/konviva-events/app/enrollment.py b/konviva-events/app/enrollment.py index aff303d..30f67d3 100644 --- a/konviva-events/app/enrollment.py +++ b/konviva-events/app/enrollment.py @@ -1,4 +1,3 @@ -from datetime import datetime, timedelta from decimal import Decimal from aws_lambda_powertools.event_handler.exceptions import ( @@ -7,7 +6,7 @@ from aws_lambda_powertools.event_handler.exceptions import ( ) from botocore.args import logger from glom import glom -from layercake.dateutils import fromisoformat, now, ttl +from layercake.dateutils import now, ttl from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair, SortKey, TransactKey from layercake.strutils import md5_hash @@ -124,56 +123,34 @@ def set_score( rename_key='dedup_window_offset_days', ), ) - now_ = now() user_id = enrollment['user']['id'] course_id = glom(enrollment, 'course.id') - created_at: datetime = fromisoformat(enrollment['created_at']) # type: ignore - access_period = created_at + timedelta( - days=int(glom(enrollment, 'metadata__course.access_period')) - ) try: - match score >= 70, now_ > access_period: - case True, True: - # Got a score of 70 or higher, but the access period has expired - return _set_status_as_archived( - id, - dynamodb_persistence_layer=dynamodb_persistence_layer, - ) - case True, False: - # Got a score of 70 or higher, and still within the access period - return _set_status_as_completed( - id, - score, - progress=progress, - user_id=user_id, - course_id=course_id, - cert_exp_interval=int( - glom( - enrollment, 'metadata__course.cert.exp_interval', default=0 - ) - ), - dedup_window_offset_days=int( - enrollment['dedup_window_offset_days'] - ), - dynamodb_persistence_layer=dynamodb_persistence_layer, - ) - case False, True: - # Got a score below 70, and the access period has expired - return _set_status_as_expired( - id, - dynamodb_persistence_layer=dynamodb_persistence_layer, - ) - case _: - # Got a score below 70, and still within the access period - return _set_status_as_failed( - id, - score, - progress=progress, - user_id=user_id, - course_id=course_id, - dynamodb_persistence_layer=dynamodb_persistence_layer, - ) + if score >= 70: + # Got a score of 70 or higher + return _set_status_as_completed( + id, + score, + progress=progress, + user_id=user_id, + course_id=course_id, + cert_exp_interval=int( + glom(enrollment, 'metadata__course.cert.exp_interval', default=0) + ), + dedup_window_offset_days=int(enrollment['dedup_window_offset_days']), + dynamodb_persistence_layer=dynamodb_persistence_layer, + ) + + # Got a score below 70 + return _set_status_as_failed( + id, + score, + progress=progress, + user_id=user_id, + course_id=course_id, + dynamodb_persistence_layer=dynamodb_persistence_layer, + ) except EnrollmentConflictError as err: logger.exception(err) raise @@ -193,15 +170,15 @@ def _set_status_as_completed( ) -> bool: now_ = now() lock_hash = md5_hash(f'{user_id}{course_id}') - archive_ttl = ttl( + cert_exp_ttl = ttl( start_dt=now_, days=cert_exp_interval, ) - cert_expiration_reminder_ttl = ttl( + cert_exp_reminder_ttl = ttl( start_dt=now_, days=cert_exp_interval - 30, ) - deduplication_lock_ttl = ttl( + dedup_lock_ttl = ttl( start_dt=now_, days=cert_exp_interval - dedup_window_offset_days, ) @@ -209,8 +186,10 @@ def _set_status_as_completed( with dynamodb_persistence_layer.transact_writer() as transact: transact.update( key=KeyPair(pk=id, sk='0'), - update_expr='SET #status = :completed, progress = :progress, \ - score = :score, updated_at = :updated_at', + update_expr='SET #status = :completed, \ + progress = :progress, \ + score = :score, \ + updated_at = :updated_at', cond_expr='#status = :in_progress', expr_attr_names={'#status': 'status'}, expr_attr_values={ @@ -236,15 +215,15 @@ def _set_status_as_completed( item={ 'id': id, 'sk': 'SCHEDULE#REMINDER_CERT_EXPIRATION_BEFORE_30_DAYS', - 'ttl': cert_expiration_reminder_ttl, + 'ttl': cert_exp_ttl, 'created_at': now_, } ) transact.put( item={ 'id': id, - 'sk': 'SCHEDULE#SET_AS_ARCHIVED', - 'ttl': archive_ttl, + 'sk': 'SCHEDULE#SET_CERT_EXPIRED', + 'ttl': cert_exp_reminder_ttl, 'created_at': now_, } ) @@ -252,7 +231,7 @@ def _set_status_as_completed( item={ 'id': id, 'sk': 'LOCK', - 'ttl': deduplication_lock_ttl, + 'ttl': dedup_lock_ttl, 'hash': lock_hash, 'created_at': now_, } @@ -262,7 +241,7 @@ def _set_status_as_completed( 'id': 'LOCK', 'sk': lock_hash, 'enrollment_id': id, - 'ttl': deduplication_lock_ttl, + 'ttl': dedup_lock_ttl, 'created_at': now_, } ) @@ -274,12 +253,6 @@ def _set_status_as_completed( sk='CANCEL_POLICY', ) ) - transact.delete( - key=KeyPair( - pk=id, - sk='SCHEDULE#SET_AS_EXPIRED', - ) - ) transact.delete( key=KeyPair( pk=id, @@ -312,8 +285,11 @@ def _set_status_as_failed( with dynamodb_persistence_layer.transact_writer() as transact: transact.update( key=KeyPair(pk=id, sk='0'), - update_expr='SET #status = :failed, progress = :progress, \ - score = :score, updated_at = :updated_at', + update_expr='SET #status = :failed, \ + progress = :progress, \ + score = :score, \ + access_expired = :access_expired, \ + updated_at = :updated_at', cond_expr='#status = :in_progress', expr_attr_names={'#status': 'status'}, expr_attr_values={ @@ -321,6 +297,7 @@ def _set_status_as_failed( ':in_progress': 'IN_PROGRESS', ':score': score, ':progress': progress, + ':access_expired': True, ':updated_at': now_, }, exc_cls=EnrollmentConflictError, @@ -343,7 +320,7 @@ def _set_status_as_failed( transact.delete( key=KeyPair( pk=id, - sk='SCHEDULE#SET_AS_EXPIRED', + sk='SCHEDULE#SET_ACCESS_EXPIRED', ) ) transact.delete( @@ -369,90 +346,6 @@ def _set_status_as_failed( return True -def _set_status_as_archived( - id: str, - *, - dynamodb_persistence_layer: DynamoDBPersistenceLayer, -): - now_ = now() - with dynamodb_persistence_layer.transact_writer() as transact: - transact.update( - key=KeyPair(pk=id, sk='0'), - update_expr='SET #status = :archived, updated_at = :updated_at', - cond_expr='#status = :completed', - expr_attr_names={'#status': 'status'}, - expr_attr_values={ - ':archived': 'ARCHIVED', - ':completed': 'COMPLETED', - ':updated_at': now_, - }, - exc_cls=EnrollmentConflictError, - ) - transact.put( - item={ - 'id': id, - 'sk': 'ARCHIVED', - 'archived_at': now_, - }, - cond_expr='attribute_not_exists(sk)', - ) - # Remove events that no longer apply - transact.delete( - key=KeyPair( - pk=id, - sk='SCHEDULE#SET_AS_ARCHIVED', - ) - ) - - return True - - -def _set_status_as_expired( - id: str, - *, - dynamodb_persistence_layer: DynamoDBPersistenceLayer, -): - now_ = now() - - with dynamodb_persistence_layer.transact_writer() as transact: - transact.update( - key=KeyPair(pk=id, sk='0'), - update_expr='SET #status = :expired, updated_at = :updated_at', - cond_expr='#status = :in_progress OR #status = :pending', - expr_attr_names={ - '#status': 'status', - }, - expr_attr_values={ - ':pending': 'PENDING', - ':in_progress': 'IN_PROGRESS', - ':expired': 'EXPIRED', - ':updated_at': now_, - }, - exc_cls=EnrollmentConflictError, - ) - transact.put( - item={ - 'id': id, - 'sk': 'EXPIRED', - 'expired_at': now_, - }, - cond_expr='attribute_not_exists(sk)', - ) - # Remove events and policies that no longer apply - transact.delete( - key=KeyPair( - pk=id, - sk='CANCEL_POLICY', - ), - ) - transact.delete( - key=KeyPair( - pk=id, - sk='SCHEDULE#SET_AS_EXPIRED', - ) - ) - - class EnrollmentNotFoundError(NotFoundError): def __init__(self, *_): super().__init__('Enrollment not found') diff --git a/konviva-events/app/events/cancel.py b/konviva-events/app/events/cancel.py index dd97a5c..a934aaf 100644 --- a/konviva-events/app/events/cancel.py +++ b/konviva-events/app/events/cancel.py @@ -11,14 +11,14 @@ from boto3clients import dynamodb_client from config import ENROLLMENT_TABLE logger = Logger(__name__) -enrollment_layer = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client) +dyn = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client) @event_source(data_class=EventBridgeEvent) @logger.inject_lambda_context def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool: new_image = event.detail['new_image'] - data = enrollment_layer.get_item(KeyPair(new_image['id'], 'konviva')) + data = dyn.get_item(KeyPair(new_image['id'], 'konviva')) try: result = konviva.cancel_enrollment(data['enrollment_id']) diff --git a/konviva-events/app/events/create_user.py b/konviva-events/app/events/create_user.py index 432e93a..70a8d3c 100644 --- a/konviva-events/app/events/create_user.py +++ b/konviva-events/app/events/create_user.py @@ -13,7 +13,7 @@ from boto3clients import dynamodb_client from config import USER_TABLE logger = Logger(__name__) -user_layer = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client) +dyn = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client) class UserNotFoundError(Exception): ... @@ -43,7 +43,7 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool: except Exception: raise - with user_layer.transact_writer() as transact: + with dyn.transact_writer() as transact: transact.update( key=KeyPair(new_image['id'], '0'), update_expr='SET metadata__konviva_user_id = :user_id, \ diff --git a/konviva-events/app/events/enroll.py b/konviva-events/app/events/enroll.py index c875c24..0175aa4 100644 --- a/konviva-events/app/events/enroll.py +++ b/konviva-events/app/events/enroll.py @@ -12,7 +12,7 @@ from boto3clients import dynamodb_client from config import ENROLLMENT_TABLE logger = Logger(__name__) -enrollment_layer = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client) +dyn = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client) @event_source(data_class=EventBridgeEvent) @@ -32,7 +32,7 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool: logger.exception(err) return False - with enrollment_layer.transact_writer() as transact: + with dyn.transact_writer() as transact: transact.update( key=KeyPair(new_image['id'], 'konviva'), update_expr='SET enrollment_id = :enrollment_id', diff --git a/konviva-events/app/events/update_user.py b/konviva-events/app/events/update_user.py index 4a1d13e..3d6b298 100644 --- a/konviva-events/app/events/update_user.py +++ b/konviva-events/app/events/update_user.py @@ -4,14 +4,10 @@ from aws_lambda_powertools.utilities.data_classes import ( event_source, ) from aws_lambda_powertools.utilities.typing import LambdaContext -from layercake.dynamodb import DynamoDBPersistenceLayer import konviva -from boto3clients import dynamodb_client -from config import ENROLLMENT_TABLE logger = Logger(__name__) -enrollment_layer = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client) @event_source(data_class=EventBridgeEvent) diff --git a/konviva-events/tests/seeds.jsonl b/konviva-events/tests/seeds.jsonl index e1b68b9..091d31c 100644 --- a/konviva-events/tests/seeds.jsonl +++ b/konviva-events/tests/seeds.jsonl @@ -14,7 +14,7 @@ {"id": "6c7e3d9b-f5d1-4da4-9e55-0825bb6ff2b8", "sk": "METADATA#COURSE", "access_period": 360, "cert": {"exp_interval": 365}, "created_at": "2025-04-06T11:07:32.762178-03:00"} {"id": "6c7e3d9b-f5d1-4da4-9e55-0825bb6ff2b8", "sk": "METADATA#DEDUPLICATION_WINDOW", "offset_days": 90, "created_at": "2025-04-06T11:07:32.762178-03:00"} {"id": "6c7e3d9b-f5d1-4da4-9e55-0825bb6ff2b8", "sk": "SCHEDULE#REMINDER_ACCESS_PERIOD_BEFORE_30_DAYS"} -{"id": "6c7e3d9b-f5d1-4da4-9e55-0825bb6ff2b8", "sk": "SCHEDULE#SET_AS_EXPIRED"} +{"id": "6c7e3d9b-f5d1-4da4-9e55-0825bb6ff2b8", "sk": "SCHEDULE#SET_ACCESS_EXPIRED"} {"id": "cc2c3bce-c34a-4e82-aa6c-1a19e70ec5ae", "sk": "0", "progress": 109, "status": "COMPLETED", "user": {"id": "321", "name": "Chester Bennington"}, "course": {"id": "432", "name": "pytest"}, "created_at": "2022-04-06T11:07:32.762178-03:00"} {"id": "cc2c3bce-c34a-4e82-aa6c-1a19e70ec5ae", "sk": "METADATA#COURSE", "access_period": 360, "cert": {"exp_interval": 365}, "created_at": "2025-04-06T11:07:32.762178-03:00"} diff --git a/konviva-events/tests/test_app.py b/konviva-events/tests/test_app.py index 24e88bf..94da9b8 100644 --- a/konviva-events/tests/test_app.py +++ b/konviva-events/tests/test_app.py @@ -1,3 +1,4 @@ +import pprint from http import HTTPMethod, HTTPStatus from layercake.dateutils import now @@ -22,7 +23,7 @@ def test_start_progress( 'ID_MATRICULA': '123', 'APROVEITAMENTO': '23.152173913043477', 'ANDAMENTO': '38.888888888888886', - 'status': 'IN_PROGRESS', + 'event_name': 'UPDATING', }, ), lambda_context, @@ -57,8 +58,7 @@ def test_update_progress( 'ID_MATRICULA': '456', 'APROVEITAMENTO': '23.152173913043477', 'ANDAMENTO': '12.888888888888886', - 'status': 'IN_PROGRESS', - 'event': 'Matrícula - atualização de conteúdo', + 'event_name': 'UPDATING', }, ), lambda_context, @@ -99,7 +99,7 @@ def test_set_as_completed( 'ID_MATRICULA': '567', 'APROVEITAMENTO': '89.152173913043477', 'ANDAMENTO': '100', - 'status': 'COMPLETED', + 'event_name': 'COMPLETED', }, ), lambda_context, @@ -111,14 +111,14 @@ def test_set_as_completed( PartitionKey('6c7e3d9b-f5d1-4da4-9e55-0825bb6ff2b8') ) - assert len(r['items']) == 7 + assert len(r['items']) == 8 assert any(item.get('sk') == 'COMPLETED' for item in r['items']) assert any(item.get('sk') == 'LOCK' for item in r['items']) assert any( item.get('sk') == 'SCHEDULE#REMINDER_CERT_EXPIRATION_BEFORE_30_DAYS' for item in r['items'] ) - assert any(item.get('sk') == 'SCHEDULE#SET_AS_ARCHIVED' for item in r['items']) + assert any(item.get('sk') == 'SCHEDULE#SET_CERT_EXPIRED' for item in r['items']) r = dynamodb_persistence_layer.collection.query(PartitionKey('LOCK')) assert len(r['items']) == 1 @@ -147,7 +147,7 @@ def test_set_as_failed( 'ID_MATRICULA': '567', 'APROVEITAMENTO': '12.152173913043477', 'ANDAMENTO': '100', - 'status': 'COMPLETED', + 'event_name': 'COMPLETED', }, ), lambda_context, @@ -161,63 +161,63 @@ def test_set_as_failed( assert any(item.get('sk') == 'FAILED' for item in r['items']) -def test_set_as_archived( - app, - seeds, - dynamodb_persistence_layer: DynamoDBPersistenceLayer, - http_api_proxy: HttpApiProxy, - lambda_context: LambdaContext, -): - # This data was added from seeds - r = app.lambda_handler( - http_api_proxy( - raw_path='/', - method=HTTPMethod.POST, - body={ - 'ID_MATRICULA': '899', - 'APROVEITAMENTO': '70', - 'ANDAMENTO': '100', - 'status': 'COMPLETED', - }, - ), - lambda_context, - ) - assert r['statusCode'] == HTTPStatus.NO_CONTENT +# def test_set_as_archived( +# app, +# seeds, +# dynamodb_persistence_layer: DynamoDBPersistenceLayer, +# http_api_proxy: HttpApiProxy, +# lambda_context: LambdaContext, +# ): +# # This data was added from seeds +# r = app.lambda_handler( +# http_api_proxy( +# raw_path='/', +# method=HTTPMethod.POST, +# body={ +# 'ID_MATRICULA': '899', +# 'APROVEITAMENTO': '70', +# 'ANDAMENTO': '100', +# 'status': 'COMPLETED', +# }, +# ), +# lambda_context, +# ) +# assert r['statusCode'] == HTTPStatus.NO_CONTENT - # Check `seeds.jsonl` for sample data related to this query - r = dynamodb_persistence_layer.collection.query( - PartitionKey('cc2c3bce-c34a-4e82-aa6c-1a19e70ec5ae') - ) - assert any(item.get('sk') == 'ARCHIVED' for item in r['items']) - assert len(r['items']) == 4 +# # Check `seeds.jsonl` for sample data related to this query +# r = dynamodb_persistence_layer.collection.query( +# PartitionKey('cc2c3bce-c34a-4e82-aa6c-1a19e70ec5ae') +# ) +# assert any(item.get('sk') == 'ARCHIVED' for item in r['items']) +# assert len(r['items']) == 4 -def test_set_as_expired( - app, - seeds, - dynamodb_persistence_layer: DynamoDBPersistenceLayer, - http_api_proxy: HttpApiProxy, - lambda_context: LambdaContext, -): - # This data was added from seeds - r = app.lambda_handler( - http_api_proxy( - raw_path='/', - method=HTTPMethod.POST, - body={ - 'ID_MATRICULA': '221', - 'APROVEITAMENTO': '69', - 'ANDAMENTO': '100', - 'status': 'COMPLETED', - }, - ), - lambda_context, - ) - assert r['statusCode'] == HTTPStatus.NO_CONTENT +# def test_set_as_expired( +# app, +# seeds, +# dynamodb_persistence_layer: DynamoDBPersistenceLayer, +# http_api_proxy: HttpApiProxy, +# lambda_context: LambdaContext, +# ): +# # This data was added from seeds +# r = app.lambda_handler( +# http_api_proxy( +# raw_path='/', +# method=HTTPMethod.POST, +# body={ +# 'ID_MATRICULA': '221', +# 'APROVEITAMENTO': '69', +# 'ANDAMENTO': '100', +# 'status': 'COMPLETED', +# }, +# ), +# lambda_context, +# ) +# assert r['statusCode'] == HTTPStatus.NO_CONTENT - # Check `seeds.jsonl` for sample data related to this query - r = dynamodb_persistence_layer.collection.query( - PartitionKey('5db53b35-0bae-4907-afda-a213cb5bf651') - ) - assert any(item.get('sk') == 'EXPIRED' for item in r['items']) - assert len(r['items']) == 3 +# # Check `seeds.jsonl` for sample data related to this query +# r = dynamodb_persistence_layer.collection.query( +# PartitionKey('5db53b35-0bae-4907-afda-a213cb5bf651') +# ) +# assert any(item.get('sk') == 'EXPIRED' for item in r['items']) +# assert len(r['items']) == 3