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 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': {