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"
Parameters:
UsersTable:
Type: String
Default: betaeducacao-prod-users_d2o3r5gmm4it7j
CourseTable:
Type: String
Default: saladeaula_courses
EnrollmentTable:
Type: String
Default: betaeducacao-prod-enrollments
OrderTable:
Type: String
Default: betaeducacao-prod-orders
BucketName:
Type: String
Default: saladeaula.digital
@@ -66,10 +72,14 @@ Resources:
LoggingConfig:
LogGroup: !Ref HttpLog
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref UserTable
- DynamoDBCrudPolicy:
TableName: !Ref CourseTable
- DynamoDBCrudPolicy:
TableName: !Ref EnrollmentTable
- DynamoDBCrudPolicy:
TableName: !Ref OrderTable
- S3CrudPolicy:
BucketName: !Ref BucketName
Events:

View File

@@ -1,5 +1,7 @@
import os
DEDUP_WINDOW_OFFSET_DAYS = 90
USER_TABLE: str = os.getenv('USER_TABLE') # type: ignore
ORDER_TABLE: str = os.getenv('ORDER_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 layercake.dateutils import now, ttl
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair
from layercake.dynamodb import DynamoDBPersistenceLayer
from layercake.strutils import md5_hash
from config import DEDUP_WINDOW_OFFSET_DAYS
from schemas import Enrollment
Org = TypedDict(
@@ -124,33 +125,38 @@ def enroll(
# Prevents the user from enrolling in the same course again until
# the deduplication window expires or is removed.
offset_days = (
int(deduplication_window['offset_days'])
if deduplication_window
else DEDUP_WINDOW_OFFSET_DAYS
)
ttl_ = ttl(
start_dt=now_,
days=course.access_period - offset_days,
)
transact.put(
item={
'id': 'LOCK',
'sk': lock_hash,
'enrollment_id': enrollment.id,
'created_at': now_,
'ttl': ttl_,
},
cond_expr='attribute_not_exists(sk)',
exc_cls=DeduplicationConflictError,
)
transact.put(
item={
'id': enrollment.id,
'sk': 'LOCK',
'hash': lock_hash,
'created_at': now_,
'ttl': ttl_,
},
)
# The deduplication window can be recalculated based on user settings.
if deduplication_window:
offset_days = int(deduplication_window['offset_days'])
ttl_ = ttl(
start_dt=now_,
days=course.access_period - offset_days,
)
transact.put(
item={
'id': 'LOCK',
'sk': lock_hash,
'enrollment_id': enrollment.id,
'created_at': now_,
'ttl': ttl_,
},
cond_expr='attribute_not_exists(sk)',
exc_cls=DeduplicationConflictError,
)
transact.put(
item={
'id': enrollment.id,
'sk': 'LOCK',
'hash': lock_hash,
'created_at': now_,
'ttl': ttl_,
},
)
# Deduplication window can be recalculated if needed
transact.put(
item={
'id': enrollment.id,
@@ -159,11 +165,5 @@ def enroll(
'created_at': now_,
},
)
else:
transact.condition(
key=KeyPair('LOCK', lock_hash),
cond_expr='attribute_not_exists(sk)',
exc_cls=DeduplicationConflictError,
)
return True

View File

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

View File

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

View File

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

View File

@@ -31,12 +31,12 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool | No
)
target_month = expires_at.strftime('%Y-%m')
now_ = now()
pk = f'CERT#REPORTING#ORG#{org_id}'
now_ = now(tz)
pk = f'CERT_REPORTING#ORG#{org_id}'
try:
if now_ > expires_at:
raise InvalidDateError()
raise InvalidDateError('Invalid date')
# The reporting month is the month before the certificate expires
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_,
},
cond_expr='attribute_not_exists(sk)',
exc_cls=ReportingConflictError,
exc_cls=ReportExistsError,
)
transact.put(
item={
@@ -64,7 +64,7 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool | No
},
)
except Exception as exc:
logger.exception(exc)
logger.info(exc)
try:
dyn.put_item(
@@ -89,4 +89,6 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool | No
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
def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
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('#')
# Key pattern `MONTH#{month}#SCHEDULE#SEND_REPORT_EMAIL`
_, month, *_ = old_image['sk'].split('#')

View File

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

View File

@@ -45,7 +45,7 @@ def test_append_cert(
)
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(
sk=report_sk,
rename_key='report_email',
@@ -61,3 +61,31 @@ def test_append_cert(
assert 'course' in r['enrollment']
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,
lambda_context: LambdaContext,
):
pk = 'CERT#REPORTING#ORG#00237409-9384-4692-9be5-b4443a41e1c4'
pk = 'CERT_REPORTING#ORG#00237409-9384-4692-9be5-b4443a41e1c4'
event = {
'detail': {
'old_image': {

View File

@@ -1,6 +1,6 @@
import app.events.enroll as app
from aws_lambda_powertools.utilities.typing import LambdaContext
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair, PartitionKey
def test_enroll(
@@ -30,3 +30,6 @@ def test_enroll(
KeyPair(enrollment_id, f'LINKED_ENTITIES#PARENT#ORDER#{order_id}'),
)
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
def test_reenroll(
def test_reenroll_custom_dedup_window(
seeds,
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
lambda_context: LambdaContext,
@@ -46,3 +46,8 @@ def test_reenroll(
)
)
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"}
// 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-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-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"}
// Org
{"id": "1e2eaf0e-e319-49eb-ab33-1ddec156dc94", "sk": "0", "name": "pytest"}

View File

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