add events
This commit is contained in:
@@ -62,10 +62,10 @@ Quando uma matrícula é criada, também é agendados emails/eventos.
|
|||||||
|
|
||||||
- `REMINDER_NO_ACCESS_AFTER_3_DAYS` se o usuário não acessar o curso 3 dias após a criação.
|
- `REMINDER_NO_ACCESS_AFTER_3_DAYS` se o usuário não acessar o curso 3 dias após a criação.
|
||||||
- `REMINDER_NO_ACTIVITY_AFTER_7_DAYS` 7 dias após a última atividade do usuário no curso.
|
- `REMINDER_NO_ACTIVITY_AFTER_7_DAYS` 7 dias após a última atividade do usuário no curso.
|
||||||
- `REMINDER_ACCESS_PERIOD_BEFORE_15_DAYS` 30 dias antes do perído de acesso ao curso terminar.
|
- `REMINDER_ACCESS_PERIOD_BEFORE_15_DAYS` 30 dias antes do período de acesso ao curso terminar.
|
||||||
- `REMINDER_CERT_EXPIRATION_BEFORE_30_DAYS` se houver certificado, avisa 30 dias antes do certificado expirar.
|
- `REMINDER_CERT_EXPIRATION_BEFORE_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)**.
|
- `SET_AS_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)**.
|
- `SET_AS_EXPIRED` se não houver certificado e o período de acesso for atingido, a matrícula será marcada com **expirada (EXPIRED)**.
|
||||||
|
|
||||||
```json
|
```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#REMINDER_NO_ACCESS_3_DAYS", "name": "Sérgio R Siqueira", "email": "osergiosiqueira@gmail.com", "ttl": 1874507093}
|
||||||
|
|||||||
@@ -1,14 +1,22 @@
|
|||||||
import os
|
import os
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import boto3
|
import boto3
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from mypy_boto3_dynamodb.client import DynamoDBClient
|
||||||
|
from mypy_boto3_sesv2 import SESV2Client
|
||||||
|
else:
|
||||||
|
DynamoDBClient = object
|
||||||
|
SESV2Client = object
|
||||||
|
|
||||||
def get_dynamodb_client():
|
|
||||||
|
def get_dynamodb_client() -> DynamoDBClient:
|
||||||
if os.getenv('AWS_LAMBDA_FUNCTION_NAME'):
|
if os.getenv('AWS_LAMBDA_FUNCTION_NAME'):
|
||||||
return boto3.client('dynamodb')
|
return boto3.client('dynamodb')
|
||||||
|
|
||||||
return boto3.client('dynamodb', endpoint_url='http://127.0.0.1:8000')
|
return boto3.client('dynamodb', endpoint_url='http://127.0.0.1:8000')
|
||||||
|
|
||||||
|
|
||||||
dynamodb_client = get_dynamodb_client()
|
dynamodb_client: DynamoDBClient = get_dynamodb_client()
|
||||||
sesv2_client = boto3.client('sesv2')
|
sesv2_client: SESV2Client = boto3.client('sesv2')
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
|
|||||||
order = order_layer.collection.get_items(
|
order = order_layer.collection.get_items(
|
||||||
TransactKey(order_id) + SortKey('0') + SortKey('items', path_spec='items'),
|
TransactKey(order_id) + SortKey('0') + SortKey('items', path_spec='items'),
|
||||||
)
|
)
|
||||||
tenant_id = order['tenant_id']
|
org_id = order['tenant_id']
|
||||||
items = {
|
items = {
|
||||||
item['id']: int(item['quantity'])
|
item['id']: int(item['quantity'])
|
||||||
for item in order['items']
|
for item in order['items']
|
||||||
@@ -51,10 +51,10 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
|
|||||||
for slot in slots:
|
for slot in slots:
|
||||||
transact.put(
|
transact.put(
|
||||||
item={
|
item={
|
||||||
'id': f'vacancies#{tenant_id}',
|
'id': f'vacancies#{org_id}',
|
||||||
'sk': f'{order_id}#{uuid4()}',
|
'sk': f'{order_id}#{uuid4()}',
|
||||||
# Post-migration: uncomment the follow lines
|
# Post-migration: uncomment the follow lines
|
||||||
# 'id': f'SLOT#ORG#{tenant_id}',
|
# 'id': f'SLOT#ORG#{org_id}',
|
||||||
# 'sk': f'ORDER#{order_id}#ENROLLMENT#{uuid4()}',
|
# 'sk': f'ORDER#{order_id}#ENROLLMENT#{uuid4()}',
|
||||||
'course': asdict(slot),
|
'course': asdict(slot),
|
||||||
'created_at': now_,
|
'created_at': now_,
|
||||||
|
|||||||
@@ -1,12 +1,61 @@
|
|||||||
from aws_lambda_powertools import Logger
|
from aws_lambda_powertools import Logger
|
||||||
from aws_lambda_powertools.utilities.data_classes import EventBridgeEvent, event_source
|
from aws_lambda_powertools.utilities.data_classes import (
|
||||||
|
EventBridgeEvent,
|
||||||
|
event_source,
|
||||||
|
)
|
||||||
from aws_lambda_powertools.utilities.typing import LambdaContext
|
from aws_lambda_powertools.utilities.typing import LambdaContext
|
||||||
|
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair
|
||||||
|
|
||||||
|
from boto3clients import dynamodb_client, sesv2_client
|
||||||
|
from config import (
|
||||||
|
EMAIL_SENDER,
|
||||||
|
ENROLLMENT_TABLE,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .email_ import send_email
|
||||||
|
|
||||||
logger = Logger(__name__)
|
logger = Logger(__name__)
|
||||||
|
enrollment_layer = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client)
|
||||||
|
|
||||||
|
|
||||||
@event_source(data_classe=EventBridgeEvent)
|
SUBJECT = 'Seu certificado de {course} está prestes a expirar'
|
||||||
|
MESSAGE = """
|
||||||
|
Oi {first_name}, tudo bem?<br/><br/>
|
||||||
|
|
||||||
|
O certificado do curso <b>{course}</b> vai expirar em breve.<br/>
|
||||||
|
Para manter sua certificação válida, é recomendável refazer o curso 30 dias antes da expiração.<br/><br/>
|
||||||
|
|
||||||
|
<a href="https://saladeaula.digital">👉 Acesse o curso e renove sua certificação</a>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@event_source(data_class=EventBridgeEvent)
|
||||||
@logger.inject_lambda_context
|
@logger.inject_lambda_context
|
||||||
def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
|
def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
|
||||||
new_image = event.detail['new_image']
|
"""If a certificate exists, remind the user 30 days before
|
||||||
return True
|
the certificate expires."""
|
||||||
|
old_image = event.detail['old_image']
|
||||||
|
|
||||||
|
# Post-migration: Remove the following lines
|
||||||
|
if 'email' not in old_image:
|
||||||
|
# If email is missing, use enrollment email
|
||||||
|
cur_image = enrollment_layer.get_item(KeyPair(old_image['id'], '0'))
|
||||||
|
old_image['name'] = cur_image['user']['name']
|
||||||
|
old_image['email'] = cur_image['user']['email']
|
||||||
|
old_image['course'] = cur_image['course']['name']
|
||||||
|
|
||||||
|
return send_email(
|
||||||
|
to=(old_image['name'], old_image['email']),
|
||||||
|
subject=SUBJECT,
|
||||||
|
message=MESSAGE,
|
||||||
|
context={
|
||||||
|
'course': old_image['course'],
|
||||||
|
},
|
||||||
|
sender=EMAIL_SENDER,
|
||||||
|
sesv2_client=sesv2_client,
|
||||||
|
event={
|
||||||
|
'id': old_image['id'],
|
||||||
|
'sk': 'SCHEDULE#REMINDER_NO_ACCESS_AFTER_3_DAYS',
|
||||||
|
},
|
||||||
|
dynamodb_persistence_layer=enrollment_layer,
|
||||||
|
)
|
||||||
|
|||||||
@@ -4,10 +4,7 @@ from aws_lambda_powertools.utilities.data_classes import (
|
|||||||
event_source,
|
event_source,
|
||||||
)
|
)
|
||||||
from aws_lambda_powertools.utilities.typing import LambdaContext
|
from aws_lambda_powertools.utilities.typing import LambdaContext
|
||||||
from layercake.dateutils import now
|
|
||||||
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair
|
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair
|
||||||
from layercake.email_ import Message
|
|
||||||
from layercake.strutils import first_word, truncate_str
|
|
||||||
|
|
||||||
from boto3clients import dynamodb_client, sesv2_client
|
from boto3clients import dynamodb_client, sesv2_client
|
||||||
from config import (
|
from config import (
|
||||||
@@ -15,26 +12,28 @@ from config import (
|
|||||||
ENROLLMENT_TABLE,
|
ENROLLMENT_TABLE,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from .email_ import send_email
|
||||||
|
|
||||||
|
logger = Logger(__name__)
|
||||||
|
enrollment_layer = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client)
|
||||||
|
|
||||||
SUBJECT = 'Seu curso de {course} está esperando por você na EDUSEG®'
|
SUBJECT = 'Seu curso de {course} está esperando por você na EDUSEG®'
|
||||||
MESSAGE = """
|
MESSAGE = """
|
||||||
Oi {first_name}, tudo bem?<br/><br/>
|
Oi {first_name}, tudo bem?<br/><br/>
|
||||||
|
|
||||||
Você foi matriculado no curso de <b>{course}</b> há 3 dias, mas ainda não iniciou.<br/>
|
Há 3 dias você foi matriculado no curso <b>{course}</b>.<br/>
|
||||||
Não perca a oportunidade de aprender e aproveitar ao máximo seu curso!<br/><br/>
|
Ainda não começou? Não perca a oportunidade de aprender e aproveitar ao máximo seu curso!<br/><br/>
|
||||||
|
|
||||||
Clique no link para acessar seu curso:
|
<a href="https://saladeaula.digital">👉 Acesse seu curso agora</a>
|
||||||
<a href="https://saladeaula.digital">https://saladeaula.digital</a>
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
logger = Logger(__name__)
|
|
||||||
enrollment_layer = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client)
|
|
||||||
|
|
||||||
|
|
||||||
@event_source(data_class=EventBridgeEvent)
|
@event_source(data_class=EventBridgeEvent)
|
||||||
@logger.inject_lambda_context
|
@logger.inject_lambda_context
|
||||||
def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
|
def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
|
||||||
|
"""If the user does not access the course within 3 days after
|
||||||
|
enrollment creation."""
|
||||||
old_image = event.detail['old_image']
|
old_image = event.detail['old_image']
|
||||||
now_ = now()
|
|
||||||
|
|
||||||
# Post-migration: Remove the following lines
|
# Post-migration: Remove the following lines
|
||||||
if 'email' not in old_image:
|
if 'email' not in old_image:
|
||||||
@@ -44,53 +43,18 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
|
|||||||
old_image['email'] = cur_image['user']['email']
|
old_image['email'] = cur_image['user']['email']
|
||||||
old_image['course'] = cur_image['course']['name']
|
old_image['course'] = cur_image['course']['name']
|
||||||
|
|
||||||
emailmsg = Message(
|
return send_email(
|
||||||
from_=EMAIL_SENDER,
|
to=(old_image['name'], old_image['email']),
|
||||||
to=(
|
subject=SUBJECT,
|
||||||
old_image['name'],
|
message=MESSAGE,
|
||||||
old_image['email'],
|
context={
|
||||||
),
|
'course': old_image['course'],
|
||||||
subject=SUBJECT.format(course=truncate_str(old_image['course'])),
|
|
||||||
)
|
|
||||||
emailmsg.add_alternative(
|
|
||||||
MESSAGE.format(
|
|
||||||
first_name=first_word(old_image['name']),
|
|
||||||
course=old_image['course'],
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
sesv2_client.send_email(
|
|
||||||
Content={
|
|
||||||
'Raw': {
|
|
||||||
'Data': emailmsg.as_bytes(),
|
|
||||||
},
|
},
|
||||||
}
|
sender=EMAIL_SENDER,
|
||||||
)
|
sesv2_client=sesv2_client,
|
||||||
logger.info('Email sent')
|
event={
|
||||||
except Exception as exc:
|
|
||||||
logger.exception(exc)
|
|
||||||
|
|
||||||
enrollment_layer.put_item(
|
|
||||||
item={
|
|
||||||
'id': old_image['id'],
|
'id': old_image['id'],
|
||||||
'sk': 'SCHEDULE#REMINDER_NO_ACCESS_AFTER_3_DAYS#FAILED',
|
'sk': 'SCHEDULE#REMINDER_NO_ACCESS_AFTER_3_DAYS',
|
||||||
# Post-migration: Uncomment the following line
|
},
|
||||||
# 'sk': f'{old_image["sk"]}#FAILED',
|
dynamodb_persistence_layer=enrollment_layer,
|
||||||
'created_at': now_,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
enrollment_layer.put_item(
|
|
||||||
item={
|
|
||||||
'id': old_image['id'],
|
|
||||||
'sk': 'SCHEDULE#REMINDER_NO_ACCESS_AFTER_3_DAYS#EXECUTED',
|
|
||||||
# Post-migration: Uncomment the following line
|
|
||||||
# 'sk': f'{old_image["sk"]}#EXECUTED',
|
|
||||||
'created_at': now_,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|||||||
@@ -4,10 +4,7 @@ from aws_lambda_powertools.utilities.data_classes import (
|
|||||||
event_source,
|
event_source,
|
||||||
)
|
)
|
||||||
from aws_lambda_powertools.utilities.typing import LambdaContext
|
from aws_lambda_powertools.utilities.typing import LambdaContext
|
||||||
from layercake.dateutils import now
|
|
||||||
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair
|
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair
|
||||||
from layercake.email_ import Message
|
|
||||||
from layercake.strutils import first_word, truncate_str
|
|
||||||
|
|
||||||
from boto3clients import dynamodb_client, sesv2_client
|
from boto3clients import dynamodb_client, sesv2_client
|
||||||
from config import (
|
from config import (
|
||||||
@@ -15,19 +12,28 @@ from config import (
|
|||||||
ENROLLMENT_TABLE,
|
ENROLLMENT_TABLE,
|
||||||
)
|
)
|
||||||
|
|
||||||
SUBJECT = ''
|
from .email_ import send_email
|
||||||
MESSAGE = """
|
|
||||||
"""
|
|
||||||
|
|
||||||
logger = Logger(__name__)
|
logger = Logger(__name__)
|
||||||
enrollment_layer = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client)
|
enrollment_layer = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client)
|
||||||
|
|
||||||
|
|
||||||
|
SUBJECT = 'Seu curso de {course} está parado há 7 dias...'
|
||||||
|
MESSAGE = """
|
||||||
|
Oi {first_name}, tudo bem?<br><br>
|
||||||
|
|
||||||
|
Percebemos que você não acessou seu curso <b>{course}</b> nos últimos 7 dias.<br/>
|
||||||
|
Não deixe seu período de acesso expirar! Retome seu aprendizado agora mesmo.<br/><br/>
|
||||||
|
|
||||||
|
<a href="https://saladeaula.digital">👉 Clique aqui para acessar seu curso</a>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
@event_source(data_class=EventBridgeEvent)
|
@event_source(data_class=EventBridgeEvent)
|
||||||
@logger.inject_lambda_context
|
@logger.inject_lambda_context
|
||||||
def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
|
def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
|
||||||
|
"""7 days after the user's last activity in the course."""
|
||||||
old_image = event.detail['old_image']
|
old_image = event.detail['old_image']
|
||||||
now_ = now()
|
|
||||||
|
|
||||||
# Post-migration: Remove the following lines
|
# Post-migration: Remove the following lines
|
||||||
if 'email' not in old_image:
|
if 'email' not in old_image:
|
||||||
@@ -37,53 +43,18 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
|
|||||||
old_image['email'] = cur_image['user']['email']
|
old_image['email'] = cur_image['user']['email']
|
||||||
old_image['course'] = cur_image['course']['name']
|
old_image['course'] = cur_image['course']['name']
|
||||||
|
|
||||||
emailmsg = Message(
|
return send_email(
|
||||||
from_=EMAIL_SENDER,
|
to=(old_image['name'], old_image['email']),
|
||||||
to=(
|
subject=SUBJECT,
|
||||||
old_image['name'],
|
message=MESSAGE,
|
||||||
old_image['email'],
|
context={
|
||||||
),
|
'course': old_image['course'],
|
||||||
subject=SUBJECT.format(course=truncate_str(old_image['course'])),
|
|
||||||
)
|
|
||||||
emailmsg.add_alternative(
|
|
||||||
MESSAGE.format(
|
|
||||||
first_name=first_word(old_image['name']),
|
|
||||||
course=old_image['course'],
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
sesv2_client.send_email(
|
|
||||||
Content={
|
|
||||||
'Raw': {
|
|
||||||
'Data': emailmsg.as_bytes(),
|
|
||||||
},
|
},
|
||||||
}
|
sender=EMAIL_SENDER,
|
||||||
)
|
sesv2_client=sesv2_client,
|
||||||
logger.info('Email sent')
|
event={
|
||||||
except Exception as exc:
|
|
||||||
logger.exception(exc)
|
|
||||||
|
|
||||||
enrollment_layer.put_item(
|
|
||||||
item={
|
|
||||||
'id': old_image['id'],
|
'id': old_image['id'],
|
||||||
'sk': 'SCHEDULE#REMINDER_NO_ACTIVITY_AFTER_7_DAYS#FAILED',
|
'sk': 'SCHEDULE#REMINDER_NO_ACTIVITY_AFTER_7_DAYS',
|
||||||
# Post-migration: Uncomment the following line
|
},
|
||||||
# 'sk': f'{old_image["sk"]}#FAILED',
|
dynamodb_persistence_layer=enrollment_layer,
|
||||||
'created_at': now_,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
enrollment_layer.put_item(
|
|
||||||
item={
|
|
||||||
'id': old_image['id'],
|
|
||||||
'sk': 'SCHEDULE#REMINDER_NO_ACTIVITY_AFTER_7_DAYS#EXECUTED',
|
|
||||||
# Post-migration: Uncomment the following line
|
|
||||||
# 'sk': f'{old_image["sk"]}#EXECUTED',
|
|
||||||
'created_at': now_,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|||||||
@@ -89,7 +89,11 @@ def _handler(record: Course, context: dict) -> Enrollment:
|
|||||||
enrollment,
|
enrollment,
|
||||||
persistence_layer=enrollment_layer,
|
persistence_layer=enrollment_layer,
|
||||||
deduplication_window=DeduplicationWindow(offset_days=90),
|
deduplication_window=DeduplicationWindow(offset_days=90),
|
||||||
linked_entities=frozenset({LinkedEntity(context['order_id'], 'ORDER')}),
|
linked_entities=frozenset(
|
||||||
|
{
|
||||||
|
LinkedEntity(context['order_id'], 'ORDER'),
|
||||||
|
}
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
return enrollment
|
return enrollment
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ dependencies = ["layercake"]
|
|||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
dev = [
|
dev = [
|
||||||
|
"boto3-stubs[dynamodb,sesv2]>=1.40.15",
|
||||||
"jsonlines>=4.0.0",
|
"jsonlines>=4.0.0",
|
||||||
"pytest>=8.3.4",
|
"pytest>=8.3.4",
|
||||||
"pytest-cov>=6.0.0",
|
"pytest-cov>=6.0.0",
|
||||||
|
|||||||
@@ -140,6 +140,32 @@ Resources:
|
|||||||
scope: [SINGLE_USER]
|
scope: [SINGLE_USER]
|
||||||
status: [PENDING]
|
status: [PENDING]
|
||||||
|
|
||||||
|
EventReenrollIfFailedFunction:
|
||||||
|
Type: AWS::Serverless::Function
|
||||||
|
Properties:
|
||||||
|
Handler: events.reenroll_if_failed.lambda_handler
|
||||||
|
LoggingConfig:
|
||||||
|
LogGroup: !Ref EventLog
|
||||||
|
Policies:
|
||||||
|
- DynamoDBCrudPolicy:
|
||||||
|
TableName: !Ref EnrollmentTable
|
||||||
|
Events:
|
||||||
|
DynamoDBEvent:
|
||||||
|
Type: EventBridgeRule
|
||||||
|
Properties:
|
||||||
|
Pattern:
|
||||||
|
resources: [!Ref EnrollmentTable]
|
||||||
|
detail-type: [MODIFY]
|
||||||
|
detail:
|
||||||
|
changes: [status]
|
||||||
|
new_image:
|
||||||
|
sk: ["0"]
|
||||||
|
status: [FAILED]
|
||||||
|
score:
|
||||||
|
- numeric: ["<", 70]
|
||||||
|
old_image:
|
||||||
|
status: [IN_PROGRESS]
|
||||||
|
|
||||||
EventAllocateSlotsFunction:
|
EventAllocateSlotsFunction:
|
||||||
Type: AWS::Serverless::Function
|
Type: AWS::Serverless::Function
|
||||||
Properties:
|
Properties:
|
||||||
@@ -182,6 +208,7 @@ Resources:
|
|||||||
- !Sub arn:aws:ses:${AWS::Region}:${AWS::AccountId}:identity/eduseg.com.br
|
- !Sub arn:aws:ses:${AWS::Region}:${AWS::AccountId}:identity/eduseg.com.br
|
||||||
- !Sub arn:aws:ses:${AWS::Region}:${AWS::AccountId}:configuration-set/tracking
|
- !Sub arn:aws:ses:${AWS::Region}:${AWS::AccountId}:configuration-set/tracking
|
||||||
|
|
||||||
|
# If the user does not access the course within 3 days after enrollment creation
|
||||||
EventReminderNoAccessAfter3DaysFunction:
|
EventReminderNoAccessAfter3DaysFunction:
|
||||||
Type: AWS::Serverless::Function
|
Type: AWS::Serverless::Function
|
||||||
Properties:
|
Properties:
|
||||||
@@ -204,10 +231,11 @@ Resources:
|
|||||||
sk:
|
sk:
|
||||||
- SCHEDULE#REMINDER_NO_ACCESS_AFTER_3_DAYS
|
- SCHEDULE#REMINDER_NO_ACCESS_AFTER_3_DAYS
|
||||||
# Post-migration: remove the following lines
|
# Post-migration: remove the following lines
|
||||||
- SCHEDULES#REMINDER_NO_ACCESS_AFTER_3_DAYS
|
|
||||||
- schedules#does_not_access
|
- schedules#does_not_access
|
||||||
- schedules#reminder_no_access_3_days
|
- schedules#reminder_no_access_3_days
|
||||||
|
- SCHEDULES#REMINDER_NO_ACCESS_AFTER_3_DAYS
|
||||||
|
|
||||||
|
# 7 days after the user's last activity in the course
|
||||||
EventReminderNoActivityAfter7DaysFunction:
|
EventReminderNoActivityAfter7DaysFunction:
|
||||||
Type: AWS::Serverless::Function
|
Type: AWS::Serverless::Function
|
||||||
Properties:
|
Properties:
|
||||||
@@ -229,6 +257,58 @@ Resources:
|
|||||||
keys:
|
keys:
|
||||||
sk:
|
sk:
|
||||||
- SCHEDULE#REMINDER_NO_ACTIVITY_AFTER_7_DAYS
|
- SCHEDULE#REMINDER_NO_ACTIVITY_AFTER_7_DAYS
|
||||||
|
# Post-migration: remove the following line
|
||||||
|
- schedules#no_activity
|
||||||
|
|
||||||
|
# 30 days before the course access period ends.
|
||||||
|
EventReminderAccessPeriodBefore30DaysFunction:
|
||||||
|
Type: AWS::Serverless::Function
|
||||||
|
Properties:
|
||||||
|
Handler: events.emails.reminder_access_period_before_30_days.lambda_handler
|
||||||
|
LoggingConfig:
|
||||||
|
LogGroup: !Ref EventLog
|
||||||
|
Policies:
|
||||||
|
- !Ref SesPolicy
|
||||||
|
- DynamoDBCrudPolicy:
|
||||||
|
TableName: !Ref EnrollmentTable
|
||||||
|
Events:
|
||||||
|
DynamoDBEvent:
|
||||||
|
Type: EventBridgeRule
|
||||||
|
Properties:
|
||||||
|
Pattern:
|
||||||
|
resources: [!Ref EnrollmentTable]
|
||||||
|
detail-type: [EXPIRE]
|
||||||
|
detail:
|
||||||
|
keys:
|
||||||
|
sk:
|
||||||
|
- SCHEDULE#REMINDER_CERT_EXPIRATION_BEFORE_30_DAYS
|
||||||
|
# Post-migration: remove the following line
|
||||||
|
- schedules#access_period_ends
|
||||||
|
|
||||||
|
# If a certificate exists, remind the user 30 days before the certificate expires
|
||||||
|
EventReminderCertExpirationBefore30DaysFunction:
|
||||||
|
Type: AWS::Serverless::Function
|
||||||
|
Properties:
|
||||||
|
Handler: events.emails.reminder_cert_expiration_before_30_days.lambda_handler
|
||||||
|
LoggingConfig:
|
||||||
|
LogGroup: !Ref EventLog
|
||||||
|
Policies:
|
||||||
|
- !Ref SesPolicy
|
||||||
|
- DynamoDBCrudPolicy:
|
||||||
|
TableName: !Ref EnrollmentTable
|
||||||
|
Events:
|
||||||
|
DynamoDBEvent:
|
||||||
|
Type: EventBridgeRule
|
||||||
|
Properties:
|
||||||
|
Pattern:
|
||||||
|
resources: [!Ref EnrollmentTable]
|
||||||
|
detail-type: [EXPIRE]
|
||||||
|
detail:
|
||||||
|
keys:
|
||||||
|
sk:
|
||||||
|
- SCHEDULE#REMINDER_CERT_EXPIRATION_BEFORE_30_DAYS
|
||||||
|
# Post-migration: remove the following line
|
||||||
|
- schedules#expiration
|
||||||
|
|
||||||
EventScheduleRemindersFunction:
|
EventScheduleRemindersFunction:
|
||||||
Type: AWS::Serverless::Function
|
Type: AWS::Serverless::Function
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ from aws_lambda_powertools.utilities.typing import LambdaContext
|
|||||||
|
|
||||||
|
|
||||||
def test_reminder_no_access_after_3_days(
|
def test_reminder_no_access_after_3_days(
|
||||||
dynamodb_client,
|
|
||||||
dynamodb_seeds,
|
dynamodb_seeds,
|
||||||
lambda_context: LambdaContext,
|
lambda_context: LambdaContext,
|
||||||
):
|
):
|
||||||
@@ -11,9 +10,9 @@ def test_reminder_no_access_after_3_days(
|
|||||||
'detail': {
|
'detail': {
|
||||||
'old_image': {
|
'old_image': {
|
||||||
'id': '47ZxxcVBjvhDS5TE98tpfQ',
|
'id': '47ZxxcVBjvhDS5TE98tpfQ',
|
||||||
'sk': 'schedules#reminder_no_access_3_days',
|
'sk': 'SCHEDULE#REMINDER_NO_ACCESS_AFTER_3_DAYS',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
assert app.lambda_handler(event, lambda_context)
|
assert app.lambda_handler(event, lambda_context) # type: ignore
|
||||||
|
|||||||
71
enrollments-events/uv.lock
generated
71
enrollments-events/uv.lock
generated
@@ -115,6 +115,27 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/e8/d9/d676f22160055bf29b28ace2e0e6853c10c338c1fbaaf3d6234f85c2857c/boto3-1.38.20-py3-none-any.whl", hash = "sha256:0494bafa771561c02ae5926143ce69b6ee4017f11ced22d0293a8372acb7472a", size = 139936, upload-time = "2025-05-20T23:12:56.529Z" },
|
{ url = "https://files.pythonhosted.org/packages/e8/d9/d676f22160055bf29b28ace2e0e6853c10c338c1fbaaf3d6234f85c2857c/boto3-1.38.20-py3-none-any.whl", hash = "sha256:0494bafa771561c02ae5926143ce69b6ee4017f11ced22d0293a8372acb7472a", size = 139936, upload-time = "2025-05-20T23:12:56.529Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "boto3-stubs"
|
||||||
|
version = "1.40.15"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "botocore-stubs" },
|
||||||
|
{ name = "types-s3transfer" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/b1/54/c8a0d43c5d17e20433b23ee78cc8348b0cba5a5255d6c2f66aafa86c64ad/boto3_stubs-1.40.15.tar.gz", hash = "sha256:47370ffdfd9f1899900bba554f4ae1846423c459beaccf11e2eae46896af5119", size = 101393, upload-time = "2025-08-21T19:48:27.26Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ba/40/fe3cb27e3eee35815902a37d26b7c0af308fe2b08cd0671fb0a8475dfa12/boto3_stubs-1.40.15-py3-none-any.whl", hash = "sha256:95b6a828b758ed56d90ea2530a6794506ca403cfbef3bd2584a2e7c43e3f6607", size = 70011, upload-time = "2025-08-21T19:48:20.458Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.optional-dependencies]
|
||||||
|
dynamodb = [
|
||||||
|
{ name = "mypy-boto3-dynamodb" },
|
||||||
|
]
|
||||||
|
sesv2 = [
|
||||||
|
{ name = "mypy-boto3-sesv2" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "botocore"
|
name = "botocore"
|
||||||
version = "1.38.20"
|
version = "1.38.20"
|
||||||
@@ -129,6 +150,18 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/e2/be/f0eb1d687ca841f9a8cf6077340123ade5069984121b67e0709b3a368851/botocore-1.38.20-py3-none-any.whl", hash = "sha256:70feba9b3f73946a9739d0c16703190d79379f065cf6e29883b5d7f791b247b8", size = 13558776, upload-time = "2025-05-20T23:12:39.685Z" },
|
{ url = "https://files.pythonhosted.org/packages/e2/be/f0eb1d687ca841f9a8cf6077340123ade5069984121b67e0709b3a368851/botocore-1.38.20-py3-none-any.whl", hash = "sha256:70feba9b3f73946a9739d0c16703190d79379f065cf6e29883b5d7f791b247b8", size = 13558776, upload-time = "2025-05-20T23:12:39.685Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "botocore-stubs"
|
||||||
|
version = "1.38.46"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "types-awscrt" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/05/45/27cabc7c3022dcb12de5098cc646b374065f5e72fae13600ff1756f365ee/botocore_stubs-1.38.46.tar.gz", hash = "sha256:a04e69766ab8bae338911c1897492f88d05cd489cd75f06e6eb4f135f9da8c7b", size = 42299, upload-time = "2025-06-29T22:58:24.765Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cc/84/06490071e26bab22ac79a684e98445df118adcf80c58c33ba5af184030f2/botocore_stubs-1.38.46-py3-none-any.whl", hash = "sha256:cc21d9a7dd994bdd90872db4664d817c4719b51cda8004fd507a4bf65b085a75", size = 66083, upload-time = "2025-06-29T22:58:22.234Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "camel-converter"
|
name = "camel-converter"
|
||||||
version = "4.0.1"
|
version = "4.0.1"
|
||||||
@@ -334,6 +367,7 @@ dependencies = [
|
|||||||
|
|
||||||
[package.dev-dependencies]
|
[package.dev-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
|
{ name = "boto3-stubs", extra = ["dynamodb", "sesv2"] },
|
||||||
{ name = "jsonlines" },
|
{ name = "jsonlines" },
|
||||||
{ name = "pytest" },
|
{ name = "pytest" },
|
||||||
{ name = "pytest-cov" },
|
{ name = "pytest-cov" },
|
||||||
@@ -345,6 +379,7 @@ requires-dist = [{ name = "layercake", directory = "../layercake" }]
|
|||||||
|
|
||||||
[package.metadata.requires-dev]
|
[package.metadata.requires-dev]
|
||||||
dev = [
|
dev = [
|
||||||
|
{ name = "boto3-stubs", extras = ["dynamodb", "sesv2"], specifier = ">=1.40.15" },
|
||||||
{ name = "jsonlines", specifier = ">=4.0.0" },
|
{ name = "jsonlines", specifier = ">=4.0.0" },
|
||||||
{ name = "pytest", specifier = ">=8.3.4" },
|
{ name = "pytest", specifier = ">=8.3.4" },
|
||||||
{ name = "pytest-cov", specifier = ">=6.0.0" },
|
{ name = "pytest-cov", specifier = ">=6.0.0" },
|
||||||
@@ -520,6 +555,24 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/bf/3e/a3ec8d44b35e495444cac8ce3573b33adf19a9b6d70f2a51e4a971f17c81/meilisearch-0.34.1-py3-none-any.whl", hash = "sha256:43efa4521ce7dc3b065d404267ad5b3acb825602e6219b8b5356650306686cd4", size = 24918, upload-time = "2025-04-04T13:45:06.869Z" },
|
{ url = "https://files.pythonhosted.org/packages/bf/3e/a3ec8d44b35e495444cac8ce3573b33adf19a9b6d70f2a51e4a971f17c81/meilisearch-0.34.1-py3-none-any.whl", hash = "sha256:43efa4521ce7dc3b065d404267ad5b3acb825602e6219b8b5356650306686cd4", size = 24918, upload-time = "2025-04-04T13:45:06.869Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mypy-boto3-dynamodb"
|
||||||
|
version = "1.40.14"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/de/85/f5d4261c084cac14e4f19bb074f9292f68e18493174289fb21e07339f25c/mypy_boto3_dynamodb-1.40.14.tar.gz", hash = "sha256:7ec8eb714ac080e7d5572ec8c556953930aba5d2fbcc058aa3cbb87ccce4ac79", size = 47978, upload-time = "2025-08-20T19:27:30.166Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d9/b3/6f2e15a44e66a8cc98fd1032f3aba770f946ba361782a0a979a115fdf6e2/mypy_boto3_dynamodb-1.40.14-py3-none-any.whl", hash = "sha256:302cc169dde3b87a41924855dcfbae173247e18833dee80919f7cc690189f376", size = 57017, upload-time = "2025-08-20T19:27:20.701Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mypy-boto3-sesv2"
|
||||||
|
version = "1.40.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a7/df/8f1ccaacb68e65b363945627595eced58c8adeeefe782e4a5ab2b9b507af/mypy_boto3_sesv2-1.40.0.tar.gz", hash = "sha256:6862b8e3e7d32b04fee68e1b72acf51af3a6ffd8293f7a17f3612d6bfb9773cc", size = 46597, upload-time = "2025-07-31T19:51:20.069Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/15/c9/bc8df41c06ec3cc6cc22b653859ebae070d2f54f30a2e9c9632e2d8273b4/mypy_boto3_sesv2-1.40.0-py3-none-any.whl", hash = "sha256:493569840df0a55ba8c616635a1154f60f9802fe793862fef00b0231bb27768e", size = 51741, upload-time = "2025-07-31T19:51:18.213Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "orjson"
|
name = "orjson"
|
||||||
version = "3.10.18"
|
version = "3.10.18"
|
||||||
@@ -852,6 +905,24 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252, upload-time = "2022-10-06T17:21:44.262Z" },
|
{ url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252, upload-time = "2022-10-06T17:21:44.262Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "types-awscrt"
|
||||||
|
version = "0.27.6"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/56/ce/5d84526a39f44c420ce61b16654193f8437d74b54f21597ea2ac65d89954/types_awscrt-0.27.6.tar.gz", hash = "sha256:9d3f1865a93b8b2c32f137514ac88cb048b5bc438739945ba19d972698995bfb", size = 16937, upload-time = "2025-08-13T01:54:54.659Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ac/af/e3d20e3e81d235b3964846adf46a334645a8a9b25a0d3d472743eb079552/types_awscrt-0.27.6-py3-none-any.whl", hash = "sha256:18aced46da00a57f02eb97637a32e5894dc5aa3dc6a905ba3e5ed85b9f3c526b", size = 39626, upload-time = "2025-08-13T01:54:53.454Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "types-s3transfer"
|
||||||
|
version = "0.13.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/42/c1/45038f259d6741c252801044e184fec4dbaeff939a58f6160d7c32bf4975/types_s3transfer-0.13.0.tar.gz", hash = "sha256:203dadcb9865c2f68fb44bc0440e1dc05b79197ba4a641c0976c26c9af75ef52", size = 14175, upload-time = "2025-05-28T02:16:07.614Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c8/5d/6bbe4bf6a79fb727945291aef88b5ecbdba857a603f1bbcf1a6be0d3f442/types_s3transfer-0.13.0-py3-none-any.whl", hash = "sha256:79c8375cbf48a64bff7654c02df1ec4b20d74f8c5672fc13e382f593ca5565b3", size = 19588, upload-time = "2025-05-28T02:16:06.709Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typing-extensions"
|
name = "typing-extensions"
|
||||||
version = "4.13.2"
|
version = "4.13.2"
|
||||||
|
|||||||
@@ -65,6 +65,16 @@ class LifecycleEvents(str, Enum):
|
|||||||
EXPIRATION = 'schedules#expiration'
|
EXPIRATION = 'schedules#expiration'
|
||||||
|
|
||||||
|
|
||||||
|
class SlotDoesNotExistError(Exception):
|
||||||
|
def __init__(self, *args):
|
||||||
|
super().__init__('Slot does not exist')
|
||||||
|
|
||||||
|
|
||||||
|
class DeduplicationConflictError(Exception):
|
||||||
|
def __init__(self, *args):
|
||||||
|
super().__init__('Enrollment already exists')
|
||||||
|
|
||||||
|
|
||||||
def enroll(
|
def enroll(
|
||||||
enrollment: Enrollment,
|
enrollment: Enrollment,
|
||||||
*,
|
*,
|
||||||
@@ -151,7 +161,7 @@ def enroll(
|
|||||||
transact.put(
|
transact.put(
|
||||||
item={
|
item={
|
||||||
'id': enrollment.id,
|
'id': enrollment.id,
|
||||||
'sk': f'linked_entities#{type}',
|
'sk': f'LINKED_ENTITIES#{type}',
|
||||||
'created_at': now_,
|
'created_at': now_,
|
||||||
f'{type}_id': entity.id,
|
f'{type}_id': entity.id,
|
||||||
}
|
}
|
||||||
@@ -169,10 +179,6 @@ def enroll(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
class SlotDoesNotExistError(Exception):
|
|
||||||
def __init__(self, *args):
|
|
||||||
super().__init__('Slot does not exist')
|
|
||||||
|
|
||||||
transact.delete(
|
transact.delete(
|
||||||
key=KeyPair(slot.id, slot.sk),
|
key=KeyPair(slot.id, slot.sk),
|
||||||
cond_expr='attribute_exists(sk)',
|
cond_expr='attribute_exists(sk)',
|
||||||
@@ -197,10 +203,6 @@ def enroll(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
class DeduplicationConflictError(Exception):
|
|
||||||
def __init__(self, *args):
|
|
||||||
super().__init__('Enrollment already exists')
|
|
||||||
|
|
||||||
# Prevents the user from enrolling in the same course again until
|
# Prevents the user from enrolling in the same course again until
|
||||||
# the deduplication window expires or is removed.
|
# the deduplication window expires or is removed.
|
||||||
if deduplication_window:
|
if deduplication_window:
|
||||||
|
|||||||
@@ -1 +1,3 @@
|
|||||||
# id.saladeaula.digital
|
# [id.saladeaula.digital](https://id.saladeaula.digital)
|
||||||
|
|
||||||
|
O código-fonte para [id.saladeaula.digital](https://id.saladeaula.digital), construído com [React Router](https://github.com/remix-run/react-router).
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { Route } from './+types'
|
|||||||
import { isValidCPF } from '@brazilian-utils/brazilian-utils'
|
import { isValidCPF } from '@brazilian-utils/brazilian-utils'
|
||||||
import { zodResolver } from '@hookform/resolvers/zod'
|
import { zodResolver } from '@hookform/resolvers/zod'
|
||||||
import { Loader2Icon } from 'lucide-react'
|
import { Loader2Icon } from 'lucide-react'
|
||||||
|
import { useState } from 'react'
|
||||||
import { useForm } from 'react-hook-form'
|
import { useForm } from 'react-hook-form'
|
||||||
import { useFetcher } from 'react-router'
|
import { useFetcher } from 'react-router'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
@@ -13,8 +14,7 @@ import { Input } from '@/components/ui/input'
|
|||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import * as httpStatus from '@/lib/http-status'
|
import * as httpStatus from '@/lib/http-status'
|
||||||
|
|
||||||
import { useState } from 'react'
|
import logo from '@/components/logo.svg'
|
||||||
import logo from './logo.svg'
|
|
||||||
|
|
||||||
const cpf = z.string().refine(isValidCPF, { message: 'CPF inválido' })
|
const cpf = z.string().refine(isValidCPF, { message: 'CPF inválido' })
|
||||||
const email = z.string().email({ message: 'Email inválido' })
|
const email = z.string().email({ message: 'Email inválido' })
|
||||||
@@ -142,7 +142,11 @@ export default function Index({}: Route.ComponentProps) {
|
|||||||
|
|
||||||
<p className="text-white/50 text-xs text-center">
|
<p className="text-white/50 text-xs text-center">
|
||||||
Ao fazer login, você concorda com nossa{' '}
|
Ao fazer login, você concorda com nossa{' '}
|
||||||
<a href="#" className="underline hover:no-underline">
|
<a
|
||||||
|
href="//eduseg.com.br/politica"
|
||||||
|
target="_blank"
|
||||||
|
className="underline hover:no-underline"
|
||||||
|
>
|
||||||
política de privacidade
|
política de privacidade
|
||||||
</a>
|
</a>
|
||||||
.
|
.
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
<svg width="18" height="24" viewBox="0 0 18 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M16.2756 23.4353L8.93847 20.1298C8.7383 20.0015 8.48167 20.0015 8.27893 20.1298L0.941837 23.4353C0.533793 23.6945 0 23.4019 0 22.9194V1.12629C0.00256631 0.787535 0.277162 0.512939 0.615915 0.512939H16.6066C16.9454 0.512939 17.22 0.787535 17.22 1.12629V22.9194C17.22 23.4019 16.6862 23.6945 16.2781 23.4353H16.2756Z" fill="#8CD366"></path>
|
|
||||||
<path d="M10.7274 3.71313H3.34668V6.41803H10.7274V3.71313Z" fill="#2E3524"></path>
|
|
||||||
<path d="M9.42115 8.4939H3.34668V10.6496H9.42115V8.4939Z" fill="#2E3524"></path>
|
|
||||||
<path d="M10.7274 12.7263H3.34668V15.4312H10.7274V12.7263Z" fill="#2E3524"></path>
|
|
||||||
<path d="M12.9984 13.6731H12.9958C12.5111 13.6731 12.1182 14.066 12.1182 14.5508V14.5533C12.1182 15.0381 12.5111 15.431 12.9958 15.431H12.9984C13.4831 15.431 13.8761 15.0381 13.8761 14.5533V14.5508C13.8761 14.066 13.4831 13.6731 12.9984 13.6731Z" fill="#2E3524"></path>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 987 B |
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "client",
|
"name": "id-saladeaula-digital",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
|
|||||||
org_id = new_image['org_id']
|
org_id = new_image['org_id']
|
||||||
enrollment = enrollment_layer.collection.get_items(
|
enrollment = enrollment_layer.collection.get_items(
|
||||||
TransactKey(new_image['id']) + SortKey('0') + SortKey('author')
|
TransactKey(new_image['id']) + SortKey('0') + SortKey('author')
|
||||||
|
# Post-migration: uncomment the following line
|
||||||
|
# + SortKey('CREATED_BY')
|
||||||
)
|
)
|
||||||
|
|
||||||
if not enrollment:
|
if not enrollment:
|
||||||
@@ -97,7 +99,7 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
|
|||||||
|
|
||||||
# Add enrollment entry to billing
|
# Add enrollment entry to billing
|
||||||
try:
|
try:
|
||||||
author = enrollment.get('author')
|
canceled_by = enrollment.get('author')
|
||||||
course_id = enrollment['course']['id']
|
course_id = enrollment['course']['id']
|
||||||
course = course_layer.collection.get_items(
|
course = course_layer.collection.get_items(
|
||||||
KeyPair(
|
KeyPair(
|
||||||
@@ -129,11 +131,11 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
|
|||||||
| (
|
| (
|
||||||
{
|
{
|
||||||
'author': {
|
'author': {
|
||||||
'id': author['user_id'],
|
'id': canceled_by['user_id'],
|
||||||
'name': author['name'],
|
'name': canceled_by['name'],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if author
|
if canceled_by
|
||||||
else {}
|
else {}
|
||||||
),
|
),
|
||||||
cond_expr='attribute_not_exists(sk)',
|
cond_expr='attribute_not_exists(sk)',
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
|
|||||||
TransactKey(enrollment_id)
|
TransactKey(enrollment_id)
|
||||||
+ SortKey('METADATA#SUBSCRIPTION_COVERED')
|
+ SortKey('METADATA#SUBSCRIPTION_COVERED')
|
||||||
# Post-migration: uncomment the following line
|
# Post-migration: uncomment the following line
|
||||||
# + SortKey('CANCELED', path_spec='author', rename_key='author')
|
# + SortKey('CANCELED', path_spec='canceled_by', rename_key='canceled_by')
|
||||||
+ SortKey('canceled', path_spec='author', rename_key='author')
|
+ SortKey('canceled', path_spec='author', rename_key='canceled_by')
|
||||||
)
|
)
|
||||||
|
|
||||||
created_at: datetime = fromisoformat(new_image['create_date']) # type: ignore
|
created_at: datetime = fromisoformat(new_image['create_date']) # type: ignore
|
||||||
@@ -49,7 +49,7 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
author = subscription.get('author')
|
canceled_by = subscription.get('canceled_by')
|
||||||
# Retrieve canceled enrollment data
|
# Retrieve canceled enrollment data
|
||||||
old_enrollment = order_layer.collection.get_item(
|
old_enrollment = order_layer.collection.get_item(
|
||||||
KeyPair(
|
KeyPair(
|
||||||
@@ -68,7 +68,9 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
|
|||||||
}
|
}
|
||||||
| pick(('user', 'course', 'enrolled_at'), old_enrollment)
|
| pick(('user', 'course', 'enrolled_at'), old_enrollment)
|
||||||
# Add author if present
|
# Add author if present
|
||||||
| ({'author': author} if author else {}),
|
| ({'author': canceled_by} if canceled_by else {}),
|
||||||
|
# Post-migration: uncomment the following line
|
||||||
|
# | ({'created_by': canceled_by} if canceled_by else {}),
|
||||||
cond_expr='attribute_not_exists(sk)',
|
cond_expr='attribute_not_exists(sk)',
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
|||||||
@@ -30,7 +30,6 @@
|
|||||||
{"id": "77055ad7-03e1-4b07-98dc-a2f1a90913ba", "sk": "METADATA#SUBSCRIPTION_COVERED", "billing_day": 6, "org_id": "cJtK9SsnJhKPyxESe7g3DG"}
|
{"id": "77055ad7-03e1-4b07-98dc-a2f1a90913ba", "sk": "METADATA#SUBSCRIPTION_COVERED", "billing_day": 6, "org_id": "cJtK9SsnJhKPyxESe7g3DG"}
|
||||||
{"id": "77055ad7-03e1-4b07-98dc-a2f1a90913ba", "sk": "canceled", "canceled_at": "2025-08-18T15:41:49.927856-03:00", "author": {"id": "123", "name": "Dexter Holland"}}
|
{"id": "77055ad7-03e1-4b07-98dc-a2f1a90913ba", "sk": "canceled", "canceled_at": "2025-08-18T15:41:49.927856-03:00", "author": {"id": "123", "name": "Dexter Holland"}}
|
||||||
|
|
||||||
|
|
||||||
// Course
|
// Course
|
||||||
{"id": "123", "sk": "0", "access_period": "360", "cert": {"exp_interval": 360}, "created_at": "2024-12-30T00:33:33.088916-03:00", "metadata__konviva_class_id": "194", "metadata__unit_price": 99, "name": "Direção Defensiva (08 horas)", "tenant_id": "*", "updated_at": "2025-07-24T00:00:24.639003-03:00"}
|
{"id": "123", "sk": "0", "access_period": "360", "cert": {"exp_interval": 360}, "created_at": "2024-12-30T00:33:33.088916-03:00", "metadata__konviva_class_id": "194", "metadata__unit_price": 99, "name": "Direção Defensiva (08 horas)", "tenant_id": "*", "updated_at": "2025-07-24T00:00:24.639003-03:00"}
|
||||||
{"id": "CUSTOM_PRICING#ORG#cJtK9SsnJhKPyxESe7g3DG", "sk": "COURSE#123", "created_at": "2025-07-24T16:10:09.304073-03:00", "unit_price": "79.2"}
|
{"id": "CUSTOM_PRICING#ORG#cJtK9SsnJhKPyxESe7g3DG", "sk": "COURSE#123", "created_at": "2025-07-24T16:10:09.304073-03:00", "unit_price": "79.2"}
|
||||||
|
|||||||
Reference in New Issue
Block a user