add enroll to subscribed

This commit is contained in:
2025-12-08 16:48:31 -03:00
parent 1ff2634bc0
commit 93d96486ff
11 changed files with 148 additions and 108 deletions

View File

@@ -1,37 +1,74 @@
from abc import ABC
from dataclasses import dataclass
from datetime import timedelta
from datetime import datetime, timedelta
from enum import Enum
from typing import TypedDict
from typing import Any, Literal, TypedDict
from uuid import uuid4
from layercake.dateutils import now, ttl
from layercake.dynamodb import DynamoDBPersistenceLayer
from layercake.extra_types import CpfStr, NameStr
from layercake.strutils import md5_hash
from pydantic import (
UUID4,
BaseModel,
ConfigDict,
EmailStr,
Field,
)
from typing_extensions import NotRequired
from config import DEDUP_WINDOW_OFFSET_DAYS
from schemas import Enrollment
Org = TypedDict(
'Org',
{
'org_id': str,
'name': str,
},
)
DeduplicationWindow = TypedDict(
'DeduplicationWindow',
{
'offset_days': int,
},
)
class User(BaseModel):
model_config = ConfigDict(arbitrary_types_allowed=True)
id: UUID4 | str
name: NameStr
email: EmailStr
email_verified: bool = False
cpf: CpfStr | None = None
class Course(BaseModel):
id: UUID4 | str
name: str
access_period: int = 90 # 3 months
class Enrollment(BaseModel):
id: UUID4 | str = Field(default_factory=uuid4)
user: User
course: Course
progress: int = Field(default=0, ge=0, le=100)
status: Literal['PENDING'] = 'PENDING'
def model_dump(
self,
exclude=None,
*args,
**kwargs,
) -> dict[str, Any]:
return super().model_dump(
exclude={'user': {'email_verified'}},
*args,
**kwargs,
)
Org = TypedDict('Org', {'org_id': str, 'name': str})
CreatedBy = TypedDict('CreatedBy', {'id': str, 'name': str})
DeduplicationWindow = TypedDict('DeduplicationWindow', {'offset_days': int})
Subscription = TypedDict(
'Subscription',
{
'org_id': str,
'billing_day': int,
'billing_period': str,
'billing_period': NotRequired[str],
},
)
@@ -58,6 +95,8 @@ def enroll(
*,
org: Org | None = None,
subscription: Subscription | None = None,
created_by: CreatedBy | None = None,
scheduled_at: datetime | None = None,
linked_entities: frozenset[LinkedEntity] = frozenset(),
deduplication_window: DeduplicationWindow | None = None,
persistence_layer: DynamoDBPersistenceLayer,
@@ -65,7 +104,7 @@ def enroll(
now_ = now()
user = enrollment.user
course = enrollment.course
lock_hash = md5_hash('%s%s' % (user.id, course.id))
lock_hash = md5_hash(f'{user.id}{course.id}')
access_expires_at = now_ + timedelta(days=course.access_period)
with persistence_layer.transact_writer() as transact:
@@ -76,8 +115,9 @@ def enroll(
'access_expires_at': access_expires_at,
**enrollment.model_dump(),
}
| ({'org_id': org['org_id']} if org else {})
| ({'subscription_covered': True} if subscription else {})
| ({'org_id': org['org_id']} if org else {}),
| ({'scheduled_at': scheduled_at} if scheduled_at else {})
)
# Relationships between this enrollment and its related entities
@@ -123,6 +163,16 @@ def enroll(
| subscription,
)
if created_by:
transact.put(
item={
'id': enrollment.id,
'sk': 'CREATED_BY',
'created_by': created_by,
'created_at': now_,
}
)
# Prevents the user from enrolling in the same course again until
# the deduplication window expires or is removed.
offset_days = (

View File

@@ -1,5 +1,3 @@
from uuid import uuid4
from aws_lambda_powertools import Logger
from aws_lambda_powertools.utilities.data_classes import (
EventBridgeEvent,
@@ -19,11 +17,13 @@ from layercake.dynamodb import (
from boto3clients import dynamodb_client
from config import COURSE_TABLE, ENROLLMENT_TABLE, ORDER_TABLE
from enrollment import (
Course,
Enrollment,
Kind,
LinkedEntity,
User,
enroll,
)
from schemas import Course, Enrollment, User
logger = Logger(__name__)
order_layer = DynamoDBPersistenceLayer(ORDER_TABLE, dynamodb_client)
@@ -82,11 +82,10 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
)
def _handler(record: Course, context: dict) -> Enrollment:
def _handler(course: Course, context: dict) -> Enrollment:
enrollment = Enrollment(
id=uuid4(),
user=context['user'],
course=record,
course=course,
)
enroll(

View File

@@ -1,3 +1,5 @@
from datetime import datetime
from aws_lambda_powertools import Logger
from aws_lambda_powertools.utilities.data_classes import (
EventBridgeEvent,
@@ -7,9 +9,8 @@ from aws_lambda_powertools.utilities.typing import LambdaContext
from layercake.dynamodb import DynamoDBPersistenceLayer
from boto3clients import dynamodb_client
from config import (
ENROLLMENT_TABLE,
)
from config import ENROLLMENT_TABLE
from enrollment import Enrollment, enroll
logger = Logger(__name__)
dyn = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client)
@@ -21,5 +22,29 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
old_image = event.detail['old_image']
# Key pattern `SCHEDULED#ORG#{org_id}`
*_, org_id = old_image['id'].split('#')
offset_days = old_image.get('dedup_window_offset_days')
billing_day = old_image.get('subscription_billing_day')
enrollment = Enrollment(
course=old_image['course'],
user=old_image['user'],
)
return True
return enroll(
enrollment,
org={
'org_id': org_id,
'name': old_image['org_name'],
},
subscription=(
{
'org_id': org_id,
'billing_day': int(billing_day),
}
if billing_day
else None
),
scheduled_at=datetime.fromisoformat(old_image['created_at']),
# Transfer the deduplication window if it exists
deduplication_window={'offset_days': offset_days} if offset_days else None,
persistence_layer=dyn,
)

View File

@@ -10,8 +10,7 @@ from layercake.dynamodb import DynamoDBPersistenceLayer, SortKey, TransactKey
from boto3clients import dynamodb_client
from config import ENROLLMENT_TABLE
from enrollment import Kind, LinkedEntity, enroll
from schemas import Course, Enrollment, User
from enrollment import Course, Enrollment, Kind, LinkedEntity, User, enroll
logger = Logger(__name__)
dyn = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client)
@@ -24,16 +23,16 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
metadata = dyn.collection.get_items(
TransactKey(new_image['id'])
+ SortKey(
'METADATA#SUBSCRIPTION_COVERED',
sk='METADATA#SUBSCRIPTION_COVERED',
rename_key='subscription',
)
+ SortKey(
'METADATA#DEDUPLICATION_WINDOW',
sk='METADATA#DEDUPLICATION_WINDOW',
path_spec='offset_days',
rename_key='dedup_window_offset_days',
)
+ SortKey(
'ORG',
sk='ORG',
rename_key='org',
),
flatten_top=False,

View File

@@ -1,47 +0,0 @@
from typing import Any, Literal
from uuid import uuid4
from layercake.extra_types import CpfStr, NameStr
from pydantic import (
UUID4,
BaseModel,
ConfigDict,
EmailStr,
Field,
)
class User(BaseModel):
model_config = ConfigDict(arbitrary_types_allowed=True)
id: UUID4 | str = Field(default_factory=uuid4)
name: NameStr
email: EmailStr
email_verified: bool = False
cpf: CpfStr | None = None
class Course(BaseModel):
id: UUID4 | str = Field(default_factory=uuid4)
name: str
access_period: int = 90 # 3 months
class Enrollment(BaseModel):
id: UUID4 | str = Field(default_factory=uuid4)
user: User
course: Course
progress: int = Field(default=0, ge=0, le=100)
status: Literal['PENDING'] = 'PENDING'
def model_dump(
self,
exclude=None,
*args,
**kwargs,
) -> dict[str, Any]:
return super().model_dump(
exclude={'user': {'email_verified'}},
*args,
**kwargs,
)

View File

@@ -29,7 +29,7 @@ def test_append_cert(
'cert_expires_at': cert_expires_at.isoformat(),
'user': {
'id': '1234',
'name': 'Tobias Summit',
'name': 'Tobias Sammit',
},
'org_id': '1e2eaf0e-e319-49eb-ab33-1ddec156dc94',
'created_at': '2025-01-01T00:00:00-03:06',
@@ -81,7 +81,7 @@ def test_report_exists(
'cert_expires_at': '2025-07-02T00:00:00-03:06',
'user': {
'id': '1234',
'name': 'Tobias Summit',
'name': 'Tobias Sammit',
},
'org_id': '00237409-9384-4692-9be5-b4443a41e1c4',
'created_at': '2025-01-01T00:00:00-03:06',