diff --git a/README.md b/README.md
index 65b776c..0f9d38b 100644
--- a/README.md
+++ b/README.md
@@ -37,6 +37,7 @@ Quando o responsável é uma pessoa física (CPF).
```json
{"id": "9omWNKymwU5U4aeun6mWzZ", "sk": "0", "course": {"id": "10", "name": "pytest"}, "org_id": "100"}
{"id": "9omWNKymwU5U4aeun6mWzZ", "sk": "METADATA#COURSE", "access_period": 360, "cert": {"exp_interval": 365}}
+{"id": "9omWNKymwU5U4aeun6mWzZ", "sk": "CERT", "s3_uri": "", "expires_at": "2026-03-01T11:07:32.762178-03:00", "created_at": "2025-04-06T11:07:32.762178-03:00"}
{"id": "9omWNKymwU5U4aeun6mWzZ", "sk": "METADATA#DEDUPLICATION_WINDOW", "offset_days": 90}
{"id": "9omWNKymwU5U4aeun6mWzZ", "sk": "ORG", "org_id": "100", "name": "EDUSEG"}
{"id": "9omWNKymwU5U4aeun6mWzZ", "sk": "CREATED_BY", "user_id": "202", "name": "Tiago Maciel"}
@@ -44,10 +45,10 @@ Quando o responsável é uma pessoa física (CPF).
{"id": "9omWNKymwU5U4aeun6mWzZ", "sk": "STARTED", "started_at": "2025-04-06T11:07:32.762178-03:00"}
{"id": "9omWNKymwU5U4aeun6mWzZ", "sk": "COMPLETED", "completed_at": "2025-04-06T11:07:32.762178-03:00"}
{"id": "9omWNKymwU5U4aeun6mWzZ", "sk": "FAILED", "failed_at": "2025-04-06T11:07:32.762178-03:00"}
-{"id": "9omWNKymwU5U4aeun6mWzZ", "sk": "CANCELED", "canceled_at": "2025-04-06T11:07:32.762178-03:00"}
+{"id": "9omWNKymwU5U4aeun6mWzZ", "sk": "CANCELED", "canceled_at": "2025-04-06T11:07:32.762178-03:00", "canceled_by": {"id": "", "name": ""}}
{"id": "9omWNKymwU5U4aeun6mWzZ", "sk": "ARCHIVED", "archived_at": "2025-04-06T11:07:32.762178-03:00"}
{"id": "9omWNKymwU5U4aeun6mWzZ", "sk": "EXPIRED", "expired_at": "2025-04-06T11:07:32.762178-03:00"}
-{"id": "9omWNKymwU5U4aeun6mWzZ", "sk": "LINKED_ENTITIES#ORDER", "order_id": "101"}
+{"id": "9omWNKymwU5U4aeun6mWzZ", "sk": "RELATION#ORDER", "order_id": "101"}
{"id": "9omWNKymwU5U4aeun6mWzZ", "sk": "LOCK", "hash": "1e67e29464877783e49e07fb7d9dd372", "ttl": 1874507093}
{"id": "9omWNKymwU5U4aeun6mWzZ", "sk": "SCHEDUE#REMINDER_NO_ACCESS_AFTER_3_DAYS", "ttl": 1874507093}
{"id": "9omWNKymwU5U4aeun6mWzZ", "sk": "SCHEDUE#REMINDER_NO_ACTIVITY_AFTER_7_DAYS", "ttl": 1874507093}
@@ -81,7 +82,7 @@ Quando uma matrícula é criada, também é agendados emails/eventos.
Quando o status da matrícula for alterado para `COMPLETED`, os eventos `SET_AS_EXPIRED` e `REMINDER_ACCESS_PERIOD_BEFORE_30_DAYS` serão _removidos_ a adicionado o evento `SET_AS_ARCHIVED`.
```json
-{"id": "9omWNKymwU5U4aeun6mWzZ", "sk": "0", ...}
+{"id": "9omWNKymwU5U4aeun6mWzZ", "sk": "0", "ttl": 1874507093}
{"id": "9omWNKymwU5U4aeun6mWzZ", "sk": "SCHEDULE#SET_AS_ARCHIVED", "ttl": 1874507093}
```
@@ -103,12 +104,12 @@ Se um certificado for emitido para a matrícula, o período de proteção será
Apenas matrículas com `CANCEL_POLICY` podem ser canceladas.
-Se houver `METADATA#SOURCE_SLOT`, deve ser devolvido.
+Se houver `METADATA#SOURCE`, deve ser devolvido.
```json
{"id": "9omWNKymwU5U4aeun6mWzZ", "sk": "0"}
{"id": "9omWNKymwU5U4aeun6mWzZ", "sk": "CANCEL_POLICY"}
-{"id": "9omWNKymwU5U4aeun6mWzZ", "sk": "METADATA#SOURCE_SLOT", "slot": {"id": "SLOT#ORG#123", "sk": "ORDER#1221#ENROLLMENT#9omWNKymwU5U4aeun6mWzZ"}}
+{"id": "9omWNKymwU5U4aeun6mWzZ", "sk": "METADATA#SOURCE", "slot": {"id": "SLOT#ORG#123", "sk": "ORDER#1221#ENROLLMENT#9omWNKymwU5U4aeun6mWzZ"}}
```
# Cursos
diff --git a/enrollments-events/app/certs/sample.html b/enrollments-events/app/certs/sample.html
index 930cc3c..2eac9f2 100644
--- a/enrollments-events/app/certs/sample.html
+++ b/enrollments-events/app/certs/sample.html
@@ -2,7 +2,7 @@
- NR-10 Complementar (SEP)
+ 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
+ {{ progress }}%
+
+
Realizado entre {{ started_at }} e {{ completed_at }}
+
São José, SC, {{ today }}
+
+
+
{name}
+
+

+
+
+

+
+
Tiago Maciel do Santos
+
Responsável legal
+
EDUSEG LTDA
+
CNPJ 15.608.435/0001-90
+
+
@@ -233,17 +428,26 @@
Carga horária
- 40 horas
+ 8 horas
Instrutor e responsável técnico
-
Francis Ricardo Baretta
+

+
Francis Ricardo Baretta
CPF 039.539.409-02
Eng. de Segurança no Trabalho Eng. Eletricista
CREA/SC 126693-0
+
+
diff --git a/http-api/app/rules/enrollment.py b/http-api/app/rules/enrollment.py
index 2965cf1..77fdb99 100644
--- a/http-api/app/rules/enrollment.py
+++ b/http-api/app/rules/enrollment.py
@@ -1,9 +1,11 @@
from dataclasses import asdict, dataclass
-from datetime import timedelta
-from enum import Enum
from typing import Self, TypedDict
from uuid import uuid4
+from aws_lambda_powertools.event_handler.exceptions import (
+ BadRequestError,
+ NotFoundError,
+)
from layercake.dateutils import now, ttl
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair
from layercake.strutils import md5_hash
@@ -38,43 +40,21 @@ class Slot:
return LinkedEntity(idx, 'order')
-class LifecycleEvents(str, Enum):
- """Lifecycle events related to scheduling actions."""
-
- # Reminder if the user does not access within 3 days
- # REMINDER_NO_ACCESS_3_DAYS = 'schedules#reminder_no_access_3_days'
- DOES_NOT_ACCESS = 'schedules#does_not_access'
-
- # When there is no activity 7 days after the first access
- # NO_ACTIVITY_7_DAYS = 'schedules#no_activity_7_days'
- NO_ACTIVITY = 'schedules#no_activity'
-
- # Reminder 30 days before the access period expires
- # ACCESS_PERIOD_REMINDER_30_DAYS = 'schedules#access_period_reminder_30_days'
- ACCESS_PERIOD_ENDS = 'schedules#access_period_ends'
-
- # Reminder for certificate expiration set to 30 days from now
- CERT_EXPIRATION_REMINDER_30_DAYS = 'schedules#cert_expiration_reminder_30_days'
-
- # Archive the course after the certificate expires
- # SET_AS_ARCHIVE = 'schedules#set_as_archive'
- ARCHIVE_IT = 'schedules#archive_it'
-
- # When the access period ends for a course without a certificate
- # SET_AS_EXPIRE = 'schedules#set_as_expire'
- EXPIRATION = 'schedules#expiration'
-
-
-class SlotDoesNotExistError(Exception):
+class SlotDoesNotExistError(NotFoundError):
def __init__(self, *args):
- super().__init__('Slot does not exist')
+ super().__init__('Slot not found')
-class DeduplicationConflictError(Exception):
+class DeduplicationConflictError(BadRequestError):
def __init__(self, *args):
super().__init__('Enrollment already exists')
+class EnrollmentConflictError(BadRequestError):
+ def __init__(self, *_):
+ super().__init__('Enrollment status conflict')
+
+
def enroll(
enrollment: Enrollment,
*,
@@ -145,7 +125,7 @@ def enroll(
transact.put(
item={
'id': enrollment.id,
- 'sk': 'cancel_policy',
+ 'sk': 'CANCEL_POLICY',
'created_at': now_,
}
)
@@ -165,9 +145,7 @@ def enroll(
# the deduplication window expires or is removed.
if deduplication_window:
offset_days = deduplication_window['offset_days']
- ttl_expiration = ttl(
- start_dt=now_ + timedelta(days=course.access_period - offset_days)
- )
+ ttl_expiration = ttl(start_dt=now_, days=course.access_period - offset_days)
transact.put(
item={
'id': 'lock',
@@ -224,11 +202,13 @@ def set_status_as_canceled(
transact.update(
key=KeyPair(id, '0'),
update_expr='SET #status = :canceled, updated_at = :updated_at',
+ cond_expr='#status = :pending',
expr_attr_names={
'#status': 'status',
},
expr_attr_values={
':canceled': 'CANCELED',
+ ':pending': 'PENDING',
':updated_at': now_,
},
)
@@ -241,20 +221,50 @@ def set_status_as_canceled(
},
)
transact.delete(
- key=KeyPair(id, 'CANCEL_POLICY'),
+ key=KeyPair(
+ pk=id,
+ sk='CANCEL_POLICY',
+ ),
cond_expr='attribute_exists(sk)',
+ exc_cls=EnrollmentConflictError,
)
- # Remove schedules lifecycle events, referencies and locks
- transact.delete(key=KeyPair(id, 'SCHEDULE#REMINDER_NO_ACCESS_AFTER_3_DAYS'))
- transact.delete(key=KeyPair(id, 'SCHEDULE#REMINDER_NO_ACTIVITY_AFTER_7_DAYS'))
+ # Remove reminders and policies that no longer apply
transact.delete(
- key=KeyPair(id, 'SCHEDULE#REMINDER_ACCESS_PERIOD_BEFORE_30_DAYS')
+ key=KeyPair(
+ pk=id,
+ sk='SCHEDULE#REMINDER_NO_ACCESS_AFTER_3_DAYS',
+ )
)
transact.delete(
- key=KeyPair(id, 'SCHEDULE#REMINDER_CERT_EXPIRATION_BEFORE_30_DAYS')
+ key=KeyPair(
+ pk=id,
+ sk='SCHEDULE#REMINDER_NO_ACTIVITY_AFTER_7_DAYS',
+ )
+ )
+ transact.delete(
+ key=KeyPair(
+ pk=id,
+ sk='SCHEDULE#REMINDER_ACCESS_PERIOD_BEFORE_30_DAYS',
+ )
+ )
+ transact.delete(
+ key=KeyPair(
+ pk=id,
+ sk='SCHEDULE#REMINDER_CERT_EXPIRATION_BEFORE_30_DAYS',
+ )
+ )
+ transact.delete(
+ key=KeyPair(
+ pk=id,
+ sk='SCHEDULE#SET_AS_EXPIRED',
+ )
+ )
+ transact.delete(
+ key=KeyPair(
+ pk=id,
+ sk='parent_vacancy',
+ )
)
- transact.delete(key=KeyPair(id, 'SCHEDULE#SET_AS_EXPIRED'))
- transact.delete(key=KeyPair(id, 'parent_vacancy'))
if lock_hash:
transact.delete(key=KeyPair(id, 'LOCK'))
diff --git a/order-events/app/events/billing/cancel_enrollment.py b/order-events/app/events/billing/cancel_enrollment.py
index b82761f..1d73f21 100644
--- a/order-events/app/events/billing/cancel_enrollment.py
+++ b/order-events/app/events/billing/cancel_enrollment.py
@@ -30,7 +30,7 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
+ SortKey('METADATA#SUBSCRIPTION_COVERED')
# Post-migration: uncomment the following line
# + SortKey('CANCELED', path_spec='canceled_by', rename_key='canceled_by')
- + SortKey('canceled', path_spec='author', rename_key='canceled_by')
+ + SortKey('CANCELED', path_spec='author', rename_key='canceled_by')
)
created_at: datetime = fromisoformat(new_image['created_at']) # type: ignore
@@ -67,7 +67,7 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
'created_at': now_,
}
| pick(('user', 'course', 'enrolled_at'), old_enrollment)
- # Add author if present
+ # Add created_by if present
| ({'author': canceled_by} if canceled_by else {}),
# Post-migration: uncomment the following line
# | ({'created_by': canceled_by} if canceled_by else {}),