From 3bea74c42039a571eca4b2125f4377e0b844f41a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Rafael=20Siqueira?= Date: Wed, 2 Jul 2025 21:20:21 -0300 Subject: [PATCH] update --- README.md | 60 +++++++++++++++++++---- http-api/app/rules/course.py | 5 +- http-api/app/rules/enrollment.py | 83 +++++++++++++++++--------------- 3 files changed, 97 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index 0e2e29b..7d89cee 100644 --- a/README.md +++ b/README.md @@ -7,26 +7,46 @@ Toda compra é relacionada a empresa responsável, que é definida como o `tenan O gestor responsável pela ação também é relacionado à compra, com base no email presente na compra. ```json -{"id": "10", "sk": "0", "metadata__tenant_id": "100", "metadata__related_ids": ["100", "123"]} -{"id": "10", "sk": "metadata#tenant", "tenant_id": "ORG#100"} -{"id": "10", "sk": "related_ids#org", "org_id": "100"} -{"id": "10", "sk": "related_ids#user", "user_id": "123"} -{"id": "10", "sk": "slots", "status": "PENDING", "kind": "BATCH"} -{"id": "10", "sk": "slots#11", "status": "SUCCESS"} -{"id": "10", "sk": "slots#12", "status": "ROLLBACK"} +{"id": "10", "sk": "0", "user": {"id": "123", "name": "Sérgio"}, "tenant": "100"} +{"id": "10", "sk": "linked_entities#org", "org_id": "111"} +{"id": "10", "sk": "linked_entities#user", "user_id": "123"} +{"id": "10", "sk": "slots", "status": "PENDING", "mode": "BATCH"} +{"id": "10", "sk": "slots#enrollment#11", "status": "SUCCESS"} +{"id": "10", "sk": "slots#enrollment#12", "status": "ROLLBACK"} +``` + +Quando o responsável é uma pessoa física (CPF). + +```json +{"id": "20", "sk": "0", "user": {"id": "123", "name": "Sérgio"}, "tenant": "self"} +{"id": "20", "sk": "slots", "status": "PENDING", "mode": "STANDALONE"} +{"id": "20", "sk": "slots#enrollment#1123", "status": "SUCCESS"} ``` # Usuários # Matrículas +```json +{"id": "9omWNKymwU5U4aeun6mWzZ", "sk": "0", "course": {"id": "10", "name": "pytest", "access_period": 360}, "tenant": "100"} +{"id": "9omWNKymwU5U4aeun6mWzZ", "sk": "tenant", "org_id": "100", "name": "EDUSEG"} +{"id": "9omWNKymwU5U4aeun6mWzZ", "sk": "author", "user_id": "202", "name": "Tiago Maciel"} +{"id": "9omWNKymwU5U4aeun6mWzZ", "sk": "metadata#cert", "exp_interval": 365} +{"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": "archived", "archived_at": "2025-04-06T11:07:32.762178-03:00"} +{"id": "9omWNKymwU5U4aeun6mWzZ", "sk": "expired", "expired": "2025-04-06T11:07:32.762178-03:00"} +``` + ### Emails/eventos agendados Quando uma matrícula é criada, também é agendados emails/eventos. - `reminder_no_access_3_days` se o usuário não acessar o curso 3 dias após a criação. - `no_activity_7_days` 7 dias após a última atividade do usuário no curso. -- `access_period_reminder_30_days` 30 dias antes do perído de acesso ao curso finalizar. +- `access_period_reminder_30_days` 30 dias antes do perído de acesso ao curso terminar. - `cert_expiration_reminder_30_days` se houver certificado, avisa 30 dias antes do certificado expirar. - `course_archived` após o certificado expirar, a matrícula será marcada como **arquivada (ARCHIVED)**. - `course_expired` se não houver certificado e o período de acesso for atingido, a matrícula será marcada com **expirada (EXPIRED)**. @@ -36,15 +56,37 @@ Quando uma matrícula é criada, também é agendados emails/eventos. {"id": "9omWNKymwU5U4aeun6mWzZ", "sk": "schedules#course_expired", "name": "Sérgio R Siqueira", "email": "osergiosiqueira@gmail.com", "ttl": 1874507093} ``` +Quando o status da matrícula for alterado para `COMPLETED`, os eventos `course_expired` e `access_period_reminder_30_days` serão removidos a adicionado o evento `course_archived`. + +```json +{"id": "9omWNKymwU5U4aeun6mWzZ", "sk": "schedules#reminder_no_access_3_days", "name": "Sérgio R Siqueira", "email": "osergiosiqueira@gmail.com", "ttl": 1874507093} +{"id": "9omWNKymwU5U4aeun6mWzZ", "sk": "schedules#course_expired", "name": "Sérgio R Siqueira", "email": "osergiosiqueira@gmail.com", "ttl": 1874507093} +``` + ### Proteção contra duplicação +A proteção contra duplicação é um mecanismo que impede que o gestor matricule o mesmo colaborador no curso por engano. + +O gestor pode definir o número de dias para que a proteção seja removida automaticamente antes do fim do acesso ao curso. + +Se um certificado for emitido para a matrícula, o período de proteção será recalculado conforme a validade do certificado. + +```json +{"id": "9omWNKymwU5U4aeun6mWzZ", "sk": "metadata#deduplication_window", "offset_days": 90} +{"id": "9omWNKymwU5U4aeun6mWzZ", "sk": "lock", "hash": "1e67e29464877783e49e07fb7d9dd372", "ttl": 1767625113} +{"id": "lock", "sk": "1e67e29464877783e49e07fb7d9dd372", "ttl": 1767625113} +``` + ### Política de cancelamento Apenas matrículas com `metadata#cancel_policy` podem ser canceladas. +Se houver `metadata#parent_slot`, + ```json {"id": "9omWNKymwU5U4aeun6mWzZ", "sk": "0"} -{"id": "9omWNKymwU5U4aeun6mWzZ", "sk": "metadata#cancel_policy"} +{"id": "9omWNKymwU5U4aeun6mWzZ", "sk": "cancel_policy"} +{"id": "9omWNKymwU5U4aeun6mWzZ", "sk": "metadata#parent_slot", "slot": {"id": "slots#123", "sk": "1221#f7120daf-96d2-4639-b8f4-d736fd99e4ee"}} ``` # Cursos diff --git a/http-api/app/rules/course.py b/http-api/app/rules/course.py index 0e3dcb6..9309b53 100644 --- a/http-api/app/rules/course.py +++ b/http-api/app/rules/course.py @@ -11,11 +11,12 @@ def create_course( persistence_layer: DynamoDBPersistenceLayer, ): now_ = now() + with persistence_layer.transact_writer() as transact: transact.put( item={ 'sk': '0', - 'metadata__tenant_id': org.id, + 'tenant': org.id, 'create_date': now_, **course.model_dump(), } @@ -24,7 +25,7 @@ def create_course( item={ 'id': course.id, 'sk': 'metadata#tenant', - 'tenant_id': f'ORG#{org.id}', + 'org_id': org.id, 'name': org.name, 'create_date': now_, } diff --git a/http-api/app/rules/enrollment.py b/http-api/app/rules/enrollment.py index cde9c8c..30515d6 100644 --- a/http-api/app/rules/enrollment.py +++ b/http-api/app/rules/enrollment.py @@ -16,15 +16,15 @@ Author = TypedDict('Author', {'id': str, 'name': str}) DeduplicationWindow = TypedDict('DeduplicationWindow', {'offset_days': int}) -class RelatedId(str): - def __new__(cls, id: str, kind: str) -> Self: - return super().__new__(cls, '#'.join([kind.upper(), id])) +class LinkedEntity(str): + def __new__(cls, id: str, type: str) -> Self: + return super().__new__(cls, '#'.join([type.upper(), id])) - def __init__(self, id: str, kind: str) -> None: + def __init__(self, id: str, type: str) -> None: # __init__ is used to store the parameters for later reference. # For immutable types like str, __init__ cannot change the instance's value. self.id = id - self.kind = kind + self.type = type @dataclass(frozen=True) @@ -33,9 +33,9 @@ class Vacancy: sk: str @property - def order_id(self) -> RelatedId: + def order_id(self) -> LinkedEntity: idx, _ = self.sk.split('#') - return RelatedId(idx, 'order') + return LinkedEntity(idx, 'order') class LifecycleEvents(str, Enum): @@ -71,7 +71,7 @@ def enroll( tenant: Tenant, vacancy: Vacancy | None = None, author: Author | None = None, - related_ids: frozenset[RelatedId] = frozenset(), + linked_entities: frozenset[LinkedEntity] = frozenset(), deduplication_window: DeduplicationWindow | None = None, persistence_layer: DynamoDBPersistenceLayer, ) -> bool: @@ -84,39 +84,35 @@ def enroll( with persistence_layer.transact_writer() as transact: if vacancy: - related_ids = frozenset({vacancy.order_id}) | related_ids + linked_entities = frozenset({vacancy.order_id}) | linked_entities transact.put( item={ 'sk': '0', 'create_date': now_, - 'metadata__tenant_id': tenant_id, - 'metadata__related_ids': { - RelatedId(tenant_id, 'org'), - RelatedId(user.id, 'user'), # type: ignore - } - | related_ids, + 'tenant': tenant_id, **enrollment.model_dump(), }, ) transact.put( item={ 'id': enrollment.id, - 'sk': 'metadata#tenant', - 'tenant_id': f'ORG#{tenant_id}', + 'sk': 'tenant', + 'org_id': tenant_id, 'name': tenant['name'], - 'create_date': now_, + 'created_at': now_, }, ) transact.put( item={ 'id': enrollment.id, + # Post-migration: uncomment the following line # 'sk': LifecycleEvents.REMINDER_NO_ACCESS_3_DAYS, 'sk': LifecycleEvents.DOES_NOT_ACCESS, 'name': user.name, 'email': user.email, 'course': course.name, - 'create_date': now_, + 'created_at': now_, 'ttl': ttl(days=3, start_dt=now_), }, ) @@ -127,35 +123,37 @@ def enroll( item={ 'id': enrollment.id, 'sk': LifecycleEvents.EXPIRATION, + # Post-migration: uncomment the following line # 'sk': LifecycleEvents.COURSE_EXPIRED, 'name': user.name, 'email': user.email, 'course': course.name, - 'create_date': now_, + 'created_at': now_, 'ttl': ttl(start_dt=now_ + timedelta(days=course.access_period)), }, ) transact.put( item={ 'id': enrollment.id, + # Post-migration: uncomment the following line # 'sk': LifecycleEvents.ACCESS_PERIOD_REMINDER_30_DAYS, 'sk': LifecycleEvents.ACCESS_PERIOD_ENDS, 'name': user.name, 'email': user.email, 'course': course.name, - 'create_date': now_, + 'created_at': now_, 'ttl': ttl(start_dt=now_ + timedelta(days=course.access_period - 30)), }, ) - for related_id in related_ids: - kind = related_id.kind.lower() + for entity in linked_entities: + type = entity.type.lower() transact.put( item={ 'id': enrollment.id, - 'sk': f'related_ids#{kind}', - 'create_date': now_, - f'{kind}_id': related_id.id, + 'sk': f'linked_entities#{type}', + 'created_at': now_, + f'{type}_id': entity.id, } ) @@ -163,9 +161,11 @@ def enroll( transact.put( item={ 'id': enrollment.id, + # Post-migration: uncomment the following line + # 'sk': 'metadata#parent_slot', 'sk': 'parent_vacancy', 'vacancy': asdict(vacancy), - 'create_date': now_, + 'created_at': now_, } ) @@ -181,8 +181,8 @@ def enroll( transact.put( item={ 'id': enrollment.id, - 'sk': 'metadata#cancel_policy', - 'create_date': now_, + 'sk': 'cancel_policy', + 'created_at': now_, } ) @@ -190,10 +190,10 @@ def enroll( transact.put( item={ 'id': enrollment.id, - 'sk': 'metadata#author', + 'sk': 'author', 'user_id': author['id'], 'name': author['name'], - 'create_date': now_, + 'created_at': now_, }, ) @@ -213,7 +213,7 @@ def enroll( 'id': 'lock', 'sk': lock_hash, 'enrollment_id': enrollment.id, - 'create_date': now_, + 'created_at': now_, 'ttl': ttl_expiration, }, cond_expr='attribute_not_exists(sk)', @@ -222,9 +222,9 @@ def enroll( transact.put( item={ 'id': enrollment.id, - 'sk': 'metadata#lock', + 'sk': 'lock', 'hash': lock_hash, - 'create_date': now_, + 'created_at': now_, 'ttl': ttl_expiration, }, ) @@ -234,7 +234,7 @@ def enroll( 'id': enrollment.id, 'sk': 'metadata#deduplication_window', 'offset_days': offset_days, - 'create_date': now_, + 'created_at': now_, }, ) else: @@ -263,7 +263,7 @@ def set_status_as_canceled( with persistence_layer.transact_writer() as transact: transact.update( key=KeyPair(id, '0'), - update_expr='SET #status = :canceled, update_date = :update', + update_expr='SET #status = :canceled, updated_at = :update', expr_attr_names={ '#status': 'status', }, @@ -275,9 +275,9 @@ def set_status_as_canceled( transact.put( item={ 'id': id, - 'sk': 'canceled_date', + 'sk': 'canceled', 'author': author, - 'create_date': now_, + 'canceled_at': now_, }, ) transact.delete( @@ -306,17 +306,20 @@ def set_status_as_canceled( # Put the vacancy back and assign a new ID transact.put( item={ + # Post-migration: uncomment the following line + # 'id': f'slots#org#{org_id}', 'id': f'vacancies#{org_id}', 'sk': f'{order_id}#{uuid4()}', 'course': course, - 'create_date': now_, + 'created_at': now_, }, cond_expr='attribute_not_exists(sk)', ) - # Post-migration: rename `generated_items` to `slots`. # Set the status of `generated_items` to `ROLLBACK` to know # which slot is available for reuse transact.update( + # Post-migration: uncomment the following line + # key=KeyPair(order_id, f'slots#enrollment#{enrollment_id}'), key=KeyPair(order_id, f'generated_items#{enrollment_id}'), update_expr='SET #status = :status, update_date = :update', expr_attr_names={