remove deduplication as default

This commit is contained in:
2025-10-17 16:17:07 -03:00
parent 94d00ba203
commit f7babaca9f
15 changed files with 114 additions and 58 deletions

View File

@@ -2,12 +2,18 @@ AWSTemplateFormatVersion: "2010-09-09"
Transform: "AWS::Serverless-2016-10-31" Transform: "AWS::Serverless-2016-10-31"
Parameters: Parameters:
UsersTable:
Type: String
Default: betaeducacao-prod-users_d2o3r5gmm4it7j
CourseTable: CourseTable:
Type: String Type: String
Default: saladeaula_courses Default: saladeaula_courses
EnrollmentTable: EnrollmentTable:
Type: String Type: String
Default: betaeducacao-prod-enrollments Default: betaeducacao-prod-enrollments
OrderTable:
Type: String
Default: betaeducacao-prod-orders
BucketName: BucketName:
Type: String Type: String
Default: saladeaula.digital Default: saladeaula.digital
@@ -66,10 +72,14 @@ Resources:
LoggingConfig: LoggingConfig:
LogGroup: !Ref HttpLog LogGroup: !Ref HttpLog
Policies: Policies:
- DynamoDBCrudPolicy:
TableName: !Ref UserTable
- DynamoDBCrudPolicy: - DynamoDBCrudPolicy:
TableName: !Ref CourseTable TableName: !Ref CourseTable
- DynamoDBCrudPolicy: - DynamoDBCrudPolicy:
TableName: !Ref EnrollmentTable TableName: !Ref EnrollmentTable
- DynamoDBCrudPolicy:
TableName: !Ref OrderTable
- S3CrudPolicy: - S3CrudPolicy:
BucketName: !Ref BucketName BucketName: !Ref BucketName
Events: Events:

View File

@@ -1,5 +1,7 @@
import os import os
DEDUP_WINDOW_OFFSET_DAYS = 90
USER_TABLE: str = os.getenv('USER_TABLE') # type: ignore USER_TABLE: str = os.getenv('USER_TABLE') # type: ignore
ORDER_TABLE: str = os.getenv('ORDER_TABLE') # type: ignore ORDER_TABLE: str = os.getenv('ORDER_TABLE') # type: ignore
ENROLLMENT_TABLE: str = os.getenv('ENROLLMENT_TABLE') # type: ignore ENROLLMENT_TABLE: str = os.getenv('ENROLLMENT_TABLE') # type: ignore

View File

@@ -5,9 +5,10 @@ from enum import Enum
from typing import NotRequired, TypedDict from typing import NotRequired, TypedDict
from layercake.dateutils import now, ttl from layercake.dateutils import now, ttl
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair from layercake.dynamodb import DynamoDBPersistenceLayer
from layercake.strutils import md5_hash from layercake.strutils import md5_hash
from config import DEDUP_WINDOW_OFFSET_DAYS
from schemas import Enrollment from schemas import Enrollment
Org = TypedDict( Org = TypedDict(
@@ -124,8 +125,11 @@ def enroll(
# 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: offset_days = (
offset_days = int(deduplication_window['offset_days']) int(deduplication_window['offset_days'])
if deduplication_window
else DEDUP_WINDOW_OFFSET_DAYS
)
ttl_ = ttl( ttl_ = ttl(
start_dt=now_, start_dt=now_,
days=course.access_period - offset_days, days=course.access_period - offset_days,
@@ -150,7 +154,9 @@ def enroll(
'ttl': ttl_, 'ttl': ttl_,
}, },
) )
# Deduplication window can be recalculated if needed
# The deduplication window can be recalculated based on user settings.
if deduplication_window:
transact.put( transact.put(
item={ item={
'id': enrollment.id, 'id': enrollment.id,
@@ -159,11 +165,5 @@ def enroll(
'created_at': now_, 'created_at': now_,
}, },
) )
else:
transact.condition(
key=KeyPair('LOCK', lock_hash),
cond_expr='attribute_not_exists(sk)',
exc_cls=DeduplicationConflictError,
)
return True return True

View File

@@ -92,7 +92,6 @@ def _handler(record: Course, context: dict) -> Enrollment:
enroll( enroll(
enrollment, enrollment,
persistence_layer=enrollment_layer, persistence_layer=enrollment_layer,
deduplication_window={'offset_days': 90},
linked_entities=frozenset( linked_entities=frozenset(
{ {
LinkedEntity( LinkedEntity(

View File

@@ -3,7 +3,7 @@ from datetime import datetime, timedelta
from typing import NotRequired, TypedDict from typing import NotRequired, TypedDict
import requests import requests
from aws_lambda_powertools import Logger from aws_lambda_powertools import Logger, Tracer
from aws_lambda_powertools.utilities.data_classes import ( from aws_lambda_powertools.utilities.data_classes import (
EventBridgeEvent, EventBridgeEvent,
event_source, event_source,
@@ -21,12 +21,14 @@ from config import (
PAPERFORGE_API, PAPERFORGE_API,
) )
tracer = Tracer()
logger = Logger(__name__) logger = Logger(__name__)
dyn = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client) dyn = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client)
@event_source(data_class=EventBridgeEvent)
@logger.inject_lambda_context @logger.inject_lambda_context
@tracer.capture_lambda_handler
@event_source(data_class=EventBridgeEvent)
def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool: def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
new_image = event.detail['new_image'] new_image = event.detail['new_image']
now_ = now() now_ = now()

View File

@@ -23,18 +23,25 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
new_image = event.detail['new_image'] new_image = event.detail['new_image']
metadata = dyn.collection.get_items( metadata = dyn.collection.get_items(
TransactKey(new_image['id']) TransactKey(new_image['id'])
+ SortKey('METADATA#SUBSCRIPTION_COVERED', rename_key='subscription') + SortKey(
'METADATA#SUBSCRIPTION_COVERED',
rename_key='subscription',
)
+ SortKey( + SortKey(
'METADATA#DEDUPLICATION_WINDOW', 'METADATA#DEDUPLICATION_WINDOW',
path_spec='offset_days', path_spec='offset_days',
rename_key='dedup_window_offset_days', rename_key='dedup_window_offset_days',
) )
+ SortKey('ORG', rename_key='org'), + SortKey(
'ORG',
rename_key='org',
),
flatten_top=False, flatten_top=False,
) )
user = User.model_validate(new_image['user']) user = User.model_validate(new_image['user'])
course = Course.model_validate(new_image['course']) course = Course.model_validate(new_image['course'])
subscription = metadata['subscription'] if 'subscription' in metadata else None subscription = metadata['subscription'] if 'subscription' in metadata else None
offset_days = metadata.get('dedup_window_offset_days', None)
enrollment = Enrollment( enrollment = Enrollment(
id=uuid4(), id=uuid4(),
course=course, course=course,
@@ -45,9 +52,7 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
enrollment, enrollment,
org=metadata.get('org', None), org=metadata.get('org', None),
subscription=subscription, subscription=subscription,
deduplication_window={ deduplication_window={'offset_days': offset_days} if offset_days else None,
'offset_days': metadata['dedup_window_offset_days'],
},
linked_entities=frozenset( linked_entities=frozenset(
{ {
LinkedEntity( LinkedEntity(

View File

@@ -31,12 +31,12 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool | No
) )
target_month = expires_at.strftime('%Y-%m') target_month = expires_at.strftime('%Y-%m')
now_ = now() now_ = now(tz)
pk = f'CERT#REPORTING#ORG#{org_id}' pk = f'CERT_REPORTING#ORG#{org_id}'
try: try:
if now_ > expires_at: if now_ > expires_at:
raise InvalidDateError() raise InvalidDateError('Invalid date')
# The reporting month is the month before the certificate expires # The reporting month is the month before the certificate expires
report_month = (expires_at.replace(day=1) - timedelta(days=1)).replace(day=1) report_month = (expires_at.replace(day=1) - timedelta(days=1)).replace(day=1)
@@ -52,7 +52,7 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool | No
'created_at': now_, 'created_at': now_,
}, },
cond_expr='attribute_not_exists(sk)', cond_expr='attribute_not_exists(sk)',
exc_cls=ReportingConflictError, exc_cls=ReportExistsError,
) )
transact.put( transact.put(
item={ item={
@@ -64,7 +64,7 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool | No
}, },
) )
except Exception as exc: except Exception as exc:
logger.exception(exc) logger.info(exc)
try: try:
dyn.put_item( dyn.put_item(
@@ -89,4 +89,6 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool | No
class InvalidDateError(Exception): ... class InvalidDateError(Exception): ...
class ReportingConflictError(Exception): ... class ReportExistsError(Exception):
def __init__(self, *args: object) -> None:
super().__init__('Report already exists')

View File

@@ -49,7 +49,7 @@ user_layer = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client)
@logger.inject_lambda_context @logger.inject_lambda_context
def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool: def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
old_image = event.detail['old_image'] old_image = event.detail['old_image']
# Key pattern `CERT#REPORTING#ORG#{org_id}` # Key pattern `CERT_REPORTING#ORG#{org_id}`
*_, org_id = old_image['id'].split('#') *_, org_id = old_image['id'].split('#')
# Key pattern `MONTH#{month}#SCHEDULE#SEND_REPORT_EMAIL` # Key pattern `MONTH#{month}#SCHEDULE#SEND_REPORT_EMAIL`
_, month, *_ = old_image['sk'].split('#') _, month, *_ = old_image['sk'].split('#')

View File

@@ -22,7 +22,6 @@ Globals:
Function: Function:
CodeUri: app/ CodeUri: app/
Runtime: python3.13 Runtime: python3.13
Tracing: Active
Architectures: Architectures:
- x86_64 - x86_64
Layers: Layers:
@@ -312,7 +311,8 @@ Resources:
Type: AWS::Serverless::Function Type: AWS::Serverless::Function
Properties: Properties:
Handler: events.issue_cert.lambda_handler Handler: events.issue_cert.lambda_handler
# Timeout: 30 Tracing: Active
Timeout: 30
LoggingConfig: LoggingConfig:
LogGroup: !Ref EventLog LogGroup: !Ref EventLog
Policies: Policies:
@@ -391,6 +391,6 @@ Resources:
detail: detail:
keys: keys:
id: id:
- prefix: CERT#REPORTING#ORG - prefix: CERT_REPORTING#ORG
sk: sk:
- suffix: SCHEDULE#SEND_REPORT_EMAIL - suffix: SCHEDULE#SEND_REPORT_EMAIL

View File

@@ -45,7 +45,7 @@ def test_append_cert(
) )
r = dynamodb_persistence_layer.collection.get_items( r = dynamodb_persistence_layer.collection.get_items(
TransactKey('CERT#REPORTING#ORG#1e2eaf0e-e319-49eb-ab33-1ddec156dc94') TransactKey('CERT_REPORTING#ORG#1e2eaf0e-e319-49eb-ab33-1ddec156dc94')
+ SortKey( + SortKey(
sk=report_sk, sk=report_sk,
rename_key='report_email', rename_key='report_email',
@@ -61,3 +61,31 @@ def test_append_cert(
assert 'course' in r['enrollment'] assert 'course' in r['enrollment']
assert 'ttl' in r['report_email'] assert 'ttl' in r['report_email']
def test_report_exists(
seeds,
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
lambda_context: LambdaContext,
):
event = {
'detail': {
'new_image': {
'id': 'e45019d8-be7a-4a82-9b37-12a01f0127bb',
'sk': '0',
'course': {
'id': '431',
'name': 'How to Sing Better',
},
'cert_expires_at': '2025-07-02T00:00:00-03:06',
'user': {
'id': '1234',
'name': 'Tobias Summit',
},
'org_id': '00237409-9384-4692-9be5-b4443a41e1c4',
'created_at': '2025-01-01T00:00:00-03:06',
'completed_at': '2025-01-10T00:00:00-03:06',
}
}
}
assert app.lambda_handler(event, lambda_context) # type: ignore

View File

@@ -12,7 +12,7 @@ def test_send_report_email(
dynamodb_persistence_layer: DynamoDBPersistenceLayer, dynamodb_persistence_layer: DynamoDBPersistenceLayer,
lambda_context: LambdaContext, lambda_context: LambdaContext,
): ):
pk = 'CERT#REPORTING#ORG#00237409-9384-4692-9be5-b4443a41e1c4' pk = 'CERT_REPORTING#ORG#00237409-9384-4692-9be5-b4443a41e1c4'
event = { event = {
'detail': { 'detail': {
'old_image': { 'old_image': {

View File

@@ -1,6 +1,6 @@
import app.events.enroll as app import app.events.enroll as app
from aws_lambda_powertools.utilities.typing import LambdaContext from aws_lambda_powertools.utilities.typing import LambdaContext
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair, PartitionKey
def test_enroll( def test_enroll(
@@ -30,3 +30,6 @@ def test_enroll(
KeyPair(enrollment_id, f'LINKED_ENTITIES#PARENT#ORDER#{order_id}'), KeyPair(enrollment_id, f'LINKED_ENTITIES#PARENT#ORDER#{order_id}'),
) )
assert enrollment assert enrollment
r = dynamodb_persistence_layer.collection.query(PartitionKey(enrollment['id']))
assert not any(x['sk'] == 'METADATA#DEDUPLICATION_WINDOW' for x in r['items'])

View File

@@ -3,7 +3,7 @@ from aws_lambda_powertools.utilities.typing import LambdaContext
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair
def test_reenroll( def test_reenroll_custom_dedup_window(
seeds, seeds,
dynamodb_persistence_layer: DynamoDBPersistenceLayer, dynamodb_persistence_layer: DynamoDBPersistenceLayer,
lambda_context: LambdaContext, lambda_context: LambdaContext,
@@ -46,3 +46,8 @@ def test_reenroll(
) )
) )
assert child assert child
dedup_window = dynamodb_persistence_layer.collection.get_item(
KeyPair(child_id, 'METADATA#DEDUPLICATION_WINDOW')
)
assert dedup_window

View File

@@ -38,8 +38,8 @@
{"id": "294e9864-8284-4287-b153-927b15d90900", "sk": "tenant", "org_id": "123", "name": "EDUSEG", "create_date": "2025-09-12T17:11:00.556907-03:00"} {"id": "294e9864-8284-4287-b153-927b15d90900", "sk": "tenant", "org_id": "123", "name": "EDUSEG", "create_date": "2025-09-12T17:11:00.556907-03:00"}
// Certificate reporting // Certificate reporting
{"id": "CERT#REPORTING#ORG#00237409-9384-4692-9be5-b4443a41e1c4", "sk": "MONTH#2025-06", "status": "PENDING"} {"id": "CERT_REPORTING#ORG#00237409-9384-4692-9be5-b4443a41e1c4", "sk": "MONTH#2025-06", "status": "PENDING"}
{"id": "CERT#REPORTING#ORG#00237409-9384-4692-9be5-b4443a41e1c4", "sk": "MONTH#2025-07#ENROLLMENT#ba4d48e6-3671-4060-988a-d6cf97dd0ea4", "completed_at": "2025-01-10T00:00:00-03:06", "enrolled_at": "2025-01-01T00:00:00-03:06", "expires_at": "2026-02-10T20:14:42.880991", "course": {"name": "How to Sing Better", "id": "431"}, "created_at": "2025-10-11T23:39:12.194344-03:00", "user": {"name": "Tobias Summit", "id": "1234"}, "enrollment_id": "e45019d8-be7a-4a82-9b37-12a01f0127bb"} {"id": "CERT_REPORTING#ORG#00237409-9384-4692-9be5-b4443a41e1c4", "sk": "MONTH#2025-07#ENROLLMENT#ba4d48e6-3671-4060-988a-d6cf97dd0ea4", "completed_at": "2025-01-10T00:00:00-03:06", "enrolled_at": "2025-01-01T00:00:00-03:06", "expires_at": "2026-02-10T20:14:42.880991", "course": {"name": "How to Sing Better", "id": "431"}, "created_at": "2025-10-11T23:39:12.194344-03:00", "user": {"name": "Tobias Summit", "id": "1234"}, "enrollment_id": "e45019d8-be7a-4a82-9b37-12a01f0127bb"}
// Org // Org
{"id": "1e2eaf0e-e319-49eb-ab33-1ddec156dc94", "sk": "0", "name": "pytest"} {"id": "1e2eaf0e-e319-49eb-ab33-1ddec156dc94", "sk": "0", "name": "pytest"}

View File

@@ -8,7 +8,7 @@ Globals:
Architectures: Architectures:
- x86_64 - x86_64
Layers: Layers:
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:96 - !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:99
Environment: Environment:
Variables: Variables:
LOG_LEVEL: DEBUG LOG_LEVEL: DEBUG