From e51964bc8b7c2fd9aa8e681f537a06c1e4ff223a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Rafael=20Siqueira?= Date: Thu, 11 Sep 2025 21:53:08 -0300 Subject: [PATCH] wip reenroll --- enrollments-events/app/enrollment.py | 81 +-- enrollments-events/app/events/enroll.py | 10 +- enrollments-events/app/events/issue_cert.py | 1 + .../app/events/reenroll_if_failed.py | 32 +- ...717-8c1441793186-cipa-grau-de-risco-1.html | 500 ------------------ ...t_reminder_access_period_before_30_days.py | 2 + ...reminder_cert_expiration_before_30_days.py | 1 + .../test_reminder_no_access_after_3_days.py | 2 + .../test_reminder_no_activity_after_7_days.py | 2 + .../tests/events/test_reenroll_if_failed.py | 30 ++ .../tests/events/test_schedule_reminders.py | 2 +- enrollments-events/tests/seeds.jsonl | 8 +- .../app/events/billing/append_enrollment.py | 2 +- 13 files changed, 106 insertions(+), 567 deletions(-) delete mode 100644 enrollments-events/app/samples/3c27ea9c-9464-46a1-9717-8c1441793186-cipa-grau-de-risco-1.html create mode 100644 enrollments-events/tests/events/test_reenroll_if_failed.py diff --git a/enrollments-events/app/enrollment.py b/enrollments-events/app/enrollment.py index cb8027f..9cc68fc 100644 --- a/enrollments-events/app/enrollment.py +++ b/enrollments-events/app/enrollment.py @@ -1,5 +1,4 @@ -from dataclasses import asdict, dataclass -from typing import Self, TypedDict +from typing import NotRequired, Self, TypedDict from layercake.dateutils import now, ttl from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair @@ -8,8 +7,16 @@ from layercake.strutils import md5_hash from schemas import Enrollment Tenant = TypedDict('Tenant', {'id': str, 'name': str}) -Author = TypedDict('Author', {'id': str, 'name': str}) +CreatedBy = TypedDict('CreatedBy', {'id': str, 'name': str}) DeduplicationWindow = TypedDict('DeduplicationWindow', {'offset_days': int}) +Subscription = TypedDict( + 'Subscription', + { + 'org_id': str, + 'billing_day': int, + 'billing_period': NotRequired[str], + }, +) class LinkedEntity(str): @@ -23,32 +30,16 @@ class LinkedEntity(str): self.type = type -@dataclass(frozen=True) -class Slot: - id: str - sk: str - - @property - def order_id(self) -> LinkedEntity: - idx, _ = self.sk.split('#') - return LinkedEntity(idx, 'ORDER') - - class DeduplicationConflictError(Exception): def __init__(self, *args): super().__init__('Enrollment already exists') -class SlotDoesNotExistError(Exception): - def __init__(self, *args): - super().__init__('Slot does not exist') - - def enroll( enrollment: Enrollment, *, - slot: Slot | None = None, - author: Author | None = None, + created_by: CreatedBy | None = None, + subscription: Subscription | None = None, linked_entities: frozenset[LinkedEntity] = frozenset(), deduplication_window: DeduplicationWindow | None = None, persistence_layer: DynamoDBPersistenceLayer, @@ -60,9 +51,6 @@ def enroll( lock_hash = md5_hash('%s%s' % (user.id, course.id)) with persistence_layer.transact_writer() as transact: - if slot: - linked_entities = frozenset({slot.order_id}) | linked_entities - transact.put( item={ 'sk': '0', @@ -90,46 +78,33 @@ def enroll( } ) - if slot: - transact.put( - item={ - 'id': enrollment.id, - # Post-migration: uncomment the following line - # 'sk': 'METADATA#SOURCE_SLOT', - 'sk': 'parent_vacancy', - 'vacancy': asdict(slot), - 'created_at': now_, - } - ) - - transact.delete( - key=KeyPair(slot.id, slot.sk), - cond_expr='attribute_exists(sk)', - exc_cls=SlotDoesNotExistError, - ) - transact.put( - item={ - 'id': enrollment.id, - 'sk': 'CANCEL_POLICY', - 'created_at': now_, - } - ) - - if author: + if created_by: transact.put( item={ 'id': enrollment.id, 'sk': 'author', - 'user_id': author['id'], - 'name': author['name'], + # Post-migration: uncomment the following line + # 'sk': 'created_by', + 'user_id': created_by['id'], + 'name': created_by['name'], 'created_at': now_, }, ) + if subscription: + transact.put( + item={ + 'id': enrollment.id, + 'sk': 'METADATA#SUBSCRIPTION_COVERED', + 'created_at': now_, + } + | subscription, + ) + # Prevents the user from enrolling in the same course again until # the deduplication window expires or is removed. if deduplication_window: - offset_days = deduplication_window['offset_days'] + offset_days = int(deduplication_window['offset_days']) ttl_ = ttl( start_dt=now_, days=course.access_period - offset_days, diff --git a/enrollments-events/app/events/enroll.py b/enrollments-events/app/events/enroll.py index 6122f8e..9fffede 100644 --- a/enrollments-events/app/events/enroll.py +++ b/enrollments-events/app/events/enroll.py @@ -18,7 +18,7 @@ from layercake.dynamodb import ( from boto3clients import dynamodb_client from config import COURSE_TABLE, ENROLLMENT_TABLE, ORDER_TABLE -from enrollment import DeduplicationWindow, LinkedEntity, enroll +from enrollment import LinkedEntity, enroll from schemas import Course, Enrollment, User logger = Logger(__name__) @@ -88,12 +88,8 @@ def _handler(record: Course, context: dict) -> Enrollment: enroll( enrollment, persistence_layer=enrollment_layer, - deduplication_window=DeduplicationWindow(offset_days=90), - linked_entities=frozenset( - { - LinkedEntity(context['order_id'], 'ORDER'), - } - ), + deduplication_window={'offset_days': 90}, + linked_entities=frozenset({LinkedEntity(context['order_id'], 'ORDER')}), ) return enrollment diff --git a/enrollments-events/app/events/issue_cert.py b/enrollments-events/app/events/issue_cert.py index fd76d88..cf80ed7 100644 --- a/enrollments-events/app/events/issue_cert.py +++ b/enrollments-events/app/events/issue_cert.py @@ -49,6 +49,7 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool: 'started_at': started_at.strftime('%d/%m/%Y'), 'completed_at': completed_at.strftime('%d/%m/%Y'), 'today': _datefmt(now_), + 'year': now_.strftime('%Y'), }, }, ) diff --git a/enrollments-events/app/events/reenroll_if_failed.py b/enrollments-events/app/events/reenroll_if_failed.py index ef12a9d..93a9fd8 100644 --- a/enrollments-events/app/events/reenroll_if_failed.py +++ b/enrollments-events/app/events/reenroll_if_failed.py @@ -1,3 +1,5 @@ +from uuid import uuid4 + from aws_lambda_powertools import Logger from aws_lambda_powertools.utilities.data_classes import ( EventBridgeEvent, @@ -10,6 +12,8 @@ from boto3clients import dynamodb_client from config import ( ENROLLMENT_TABLE, ) +from enrollment import LinkedEntity, enroll +from schemas import Course, Enrollment, User logger = Logger(__name__) enrollment_layer = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client) @@ -19,11 +23,31 @@ enrollment_layer = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client) @logger.inject_lambda_context def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool: new_image = event.detail['new_image'] - data = enrollment_layer.collection.get_items( + metadata = enrollment_layer.collection.get_items( TransactKey(new_image['id']) + SortKey('METADATA#SUBSCRIPTION_COVERED', rename_key='subscription') - + SortKey('author', rename_key='created_by') - + SortKey('tenant', rename_key='org') + + SortKey('METADATA#COURSE', rename_key='course') + + SortKey( + 'METADATA#DEDUPLICATION_WINDOW', + path_spec='offset_days', + rename_key='dedup_window_offset_days', + ) + + SortKey('konviva') + + SortKey('tenant', rename_key='org'), + # Post-migration: uncomment the following lines + # + SortKey('KONVIVA', rename_key='konviva') + # + SortKey('ORG', rename_key='org'), + flatten_top=False, ) + user = User.model_validate(new_image['user']) + course = Course.model_validate(new_image['course'] | metadata['course']) + enrollment = Enrollment(id=uuid4(), course=course, user=user) + subscription = metadata['subscription'] if 'subscription' in metadata else None - return True + return enroll( + enrollment, + subscription=subscription, + deduplication_window={'offset_days': metadata['dedup_window_offset_days']}, + linked_entities=frozenset({LinkedEntity(new_image['id'], 'ENROLLMENT')}), + persistence_layer=enrollment_layer, + ) diff --git a/enrollments-events/app/samples/3c27ea9c-9464-46a1-9717-8c1441793186-cipa-grau-de-risco-1.html b/enrollments-events/app/samples/3c27ea9c-9464-46a1-9717-8c1441793186-cipa-grau-de-risco-1.html deleted file mode 100644 index 15ece8c..0000000 --- a/enrollments-events/app/samples/3c27ea9c-9464-46a1-9717-8c1441793186-cipa-grau-de-risco-1.html +++ /dev/null @@ -1,500 +0,0 @@ - - - - - CIPA Grau de Risco 1 - - - - - -
-
-
-
- - - - - - - - - - - - - - - - - - - - -
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
- -

Certificamos que

-

{{ name }}

-

- Portador(a) do CPF {{ cpf }}, concluiu o - curso de CIPA Grau de Risco 1 com - aproveitamento de - {{ score }}% -

-

- Realizado entre - {{ started_at }} e - {{ completed_at }} -

-

- Data de emissão do certificado: {{ today }} -

- -
-
{{ name }}
-
- -
-
- Tiago Maciel dos Santos - -

Tiago Maciel do Santos

-

Responsável legal

-

EDUSEG LTDA

-

CNPJ 15.608.435/0001-90

-
-
- -
- © 2025 EDUSEG® Todos os direitos reservados. CNPJ - 15.608.435/0001-90 -
-
-
- -
-
-

Conteúdo programático ministrado

-
    -
  • - Estudo do ambiente, das condições de trabalho, bem como - dos riscos originados do processo produtivo -
  • -
  • - Noções sobre acidentes e doenças relacionadas ao - trabalho decorrentes das condições de trabalho e da - exposição aos riscos existentes no estabelecimento e - suas medidas de prevenção -
  • -
  • - Metodologia de investigação e análise de acidentes e - doenças relacionadas ao trabalho -
  • -
  • - Princípios gerais de higiene do trabalho e de medidas de - prevenção dos riscos -
  • -
  • - Noções sobre as legislações trabalhista e previdenciária - relativas à segurança e saúde no trabalho -
  • -
  • - Noções sobre a inclusão de pessoas com deficiência e - reabilitados nos processos de trabalho -
  • -
  • - Violência, assédio, igualdade e diversidade no âmbito do - trabalho -
  • -
  • - Organização da CIPA e outros assuntos necessários ao - exercício das atribuições da Comissão -
  • -
-
- -
-
-

Treinamento ministrado

-

CIPA Grau de Risco 1

-
- -
-

Carga horária

-

8 horas

-
- -
-

Validade

-

24 meses após conclusão

-
- -
-

Instrutor e responsável técnico

-
- -

Francis Ricardo Baretta

-

CPF 039.539.409-02

-

Eng. de Segurança no Trabalho Eng. Eletricista

-

CREA/SC 126693-0

-
- - -
-
-
- - diff --git a/enrollments-events/tests/events/emails/test_reminder_access_period_before_30_days.py b/enrollments-events/tests/events/emails/test_reminder_access_period_before_30_days.py index cb97d45..bbd1d51 100644 --- a/enrollments-events/tests/events/emails/test_reminder_access_period_before_30_days.py +++ b/enrollments-events/tests/events/emails/test_reminder_access_period_before_30_days.py @@ -15,4 +15,6 @@ def test_reminder_access_period_before_30_days( } } + app.send_email = lambda *args, **kwargs: ... + assert app.lambda_handler(event, lambda_context) # type: ignore diff --git a/enrollments-events/tests/events/emails/test_reminder_cert_expiration_before_30_days.py b/enrollments-events/tests/events/emails/test_reminder_cert_expiration_before_30_days.py index f116c53..2a6fecf 100644 --- a/enrollments-events/tests/events/emails/test_reminder_cert_expiration_before_30_days.py +++ b/enrollments-events/tests/events/emails/test_reminder_cert_expiration_before_30_days.py @@ -14,5 +14,6 @@ def test_reminder_cert_expiration_before_30_days( } } } + app.send_email = lambda *args, **kwargs: ... assert app.lambda_handler(event, lambda_context) # type: ignore diff --git a/enrollments-events/tests/events/emails/test_reminder_no_access_after_3_days.py b/enrollments-events/tests/events/emails/test_reminder_no_access_after_3_days.py index 9b04f8f..fb25c7d 100644 --- a/enrollments-events/tests/events/emails/test_reminder_no_access_after_3_days.py +++ b/enrollments-events/tests/events/emails/test_reminder_no_access_after_3_days.py @@ -15,4 +15,6 @@ def test_reminder_no_access_after_3_days( } } + app.send_email = lambda *args, **kwargs: ... + assert app.lambda_handler(event, lambda_context) # type: ignore diff --git a/enrollments-events/tests/events/emails/test_reminder_no_activity_after_7_days.py b/enrollments-events/tests/events/emails/test_reminder_no_activity_after_7_days.py index b4e7ac9..5635f5e 100644 --- a/enrollments-events/tests/events/emails/test_reminder_no_activity_after_7_days.py +++ b/enrollments-events/tests/events/emails/test_reminder_no_activity_after_7_days.py @@ -15,4 +15,6 @@ def test_reminder_no_activity_after_7_days( } } + app.send_email = lambda *args, **kwargs: ... + assert app.lambda_handler(event, lambda_context) # type: ignore diff --git a/enrollments-events/tests/events/test_reenroll_if_failed.py b/enrollments-events/tests/events/test_reenroll_if_failed.py new file mode 100644 index 0000000..1f3834f --- /dev/null +++ b/enrollments-events/tests/events/test_reenroll_if_failed.py @@ -0,0 +1,30 @@ +import app.events.reenroll_if_failed as app +from aws_lambda_powertools.utilities.typing import LambdaContext +from layercake.dynamodb import DynamoDBPersistenceLayer + + +def test_enroll( + seeds, + dynamodb_persistence_layer: DynamoDBPersistenceLayer, + lambda_context: LambdaContext, +): + event = { + 'detail': { + 'new_image': { + 'id': '294e9864-8284-4287-b153-927b15d90900', + 'sk': '0', + 'user': { + 'id': '2beb8642-aab4-4088-86d4-2966fac7c570', + 'name': 'Gary Pihl', + 'email': 'gary@boston.com', + 'cpf': '83152103000', + }, + 'course': { + 'id': '62bea9b7-971d-4ee8-ab56-942dc7ca6fcd', + 'name': 'pytest', + }, + } + } + } + enrollment = app.lambda_handler(event, lambda_context) # type: ignore + print(enrollment) diff --git a/enrollments-events/tests/events/test_schedule_reminders.py b/enrollments-events/tests/events/test_schedule_reminders.py index be012e6..29317c4 100644 --- a/enrollments-events/tests/events/test_schedule_reminders.py +++ b/enrollments-events/tests/events/test_schedule_reminders.py @@ -18,7 +18,7 @@ def test_schedule_reminders( 'sk': '0', 'user': { 'id': '1234', - 'name': 'Ozzy Orbourne', + 'name': 'Ozzy Osbourne', 'email': 'ozzy@osbourne.com', }, 'course': { diff --git a/enrollments-events/tests/seeds.jsonl b/enrollments-events/tests/seeds.jsonl index 9332d43..48d434e 100644 --- a/enrollments-events/tests/seeds.jsonl +++ b/enrollments-events/tests/seeds.jsonl @@ -25,6 +25,12 @@ {"id": "14682b79-3df2-4351-9229-8b558af046a0", "sk": "METADATA#COURSE", "access_period": 360} {"id": "1ee108ae-67d4-4545-bf6d-4e641cdaa4e0", "sk": "0", "score": 100, "course": {"name": "CIPA Grau de Risco 1"}, "user": {"name": "Kurt Cobain"}} -{"id": "1ee108ae-67d4-4545-bf6d-4e641cdaa4e0", "sk": "METADATA#COURSE", "cert": {"s3_uri": "s3://saladeaula.digital/certs/3c27ea9c-9464-46a1-9717-8c1441793186-cipa-grau-de-risco-1.html"}} +{"id": "1ee108ae-67d4-4545-bf6d-4e641cdaa4e0", "sk": "METADATA#COURSE", "cert": {"s3_uri": "s3://saladeaula.digital/certs/samples/cipa-grau-de-risco-1.html"}} {"id": "1ee108ae-67d4-4545-bf6d-4e641cdaa4e0", "sk": "STARTED", "started_at": "2025-08-24T01:44:42.703012-03:06"} {"id": "1ee108ae-67d4-4545-bf6d-4e641cdaa4e0", "sk": "COMPLETED", "completed_at": "2025-08-31T21:59:10.842467-03:00"} + +{"id": "294e9864-8284-4287-b153-927b15d90900", "sk": "0"} +{"id": "294e9864-8284-4287-b153-927b15d90900", "sk": "METADATA#SUBSCRIPTION_COVERED", "updated_at": "2025-09-09T15:20:17.187461-03:00", "billing_day": 1, "org_id": "123", "created_at": "2025-09-09T15:20:14.021500-03:00", "billing_period": "START#2025-09-01#END#2025-09-30"} +{"id": "294e9864-8284-4287-b153-927b15d90900", "sk": "METADATA#COURSE", "access_period": 360, "created_at": "2025-09-09T09:11:29.292760-03:00", "cert": {"exp_interval": 360}} +{"id": "294e9864-8284-4287-b153-927b15d90900", "sk": "konviva", "class_id": 34, "user_id": 26943, "created_at": "2025-09-09T09:11:29.315247-03:00", "enrollment_id": 244488} +{"id": "294e9864-8284-4287-b153-927b15d90900", "sk": "METADATA#DEDUPLICATION_WINDOW", "offset_days": 90, "created_at": "2025-09-11T09:00:45.923035-03:00"} \ No newline at end of file diff --git a/order-events/app/events/billing/append_enrollment.py b/order-events/app/events/billing/append_enrollment.py index b57fcea..549bbd4 100644 --- a/order-events/app/events/billing/append_enrollment.py +++ b/order-events/app/events/billing/append_enrollment.py @@ -125,7 +125,7 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool: 'enrolled_at': enrollment['created_at'], 'created_at': now_, } - # Add author if present + # Add canceled_by if present | ( { 'author': {