Files
saladeaula.digital/enrollments-events/app/enrollment.py

170 lines
4.6 KiB
Python

from abc import ABC
from dataclasses import dataclass
from datetime import timedelta
from enum import Enum
from typing import NotRequired, TypedDict
from layercake.dateutils import now, ttl
from layercake.dynamodb import DynamoDBPersistenceLayer
from layercake.strutils import md5_hash
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,
},
)
Subscription = TypedDict(
'Subscription',
{
'org_id': str,
'billing_day': int,
'billing_period': NotRequired[str],
},
)
class Kind(str, Enum):
ORDER = 'ORDER'
ENROLLMENT = 'ENROLLMENT'
@dataclass(frozen=True)
class LinkedEntity(ABC):
id: str
kind: Kind
table_name: str | None = None
class DeduplicationConflictError(Exception):
def __init__(self, *args):
super().__init__('Enrollment already exists')
def enroll(
enrollment: Enrollment,
*,
org: Org | None = None,
subscription: Subscription | None = None,
linked_entities: frozenset[LinkedEntity] = frozenset(),
deduplication_window: DeduplicationWindow | None = None,
persistence_layer: DynamoDBPersistenceLayer,
) -> bool:
now_ = now()
user = enrollment.user
course = enrollment.course
lock_hash = md5_hash('%s%s' % (user.id, course.id))
access_expires_at = now_ + timedelta(days=course.access_period)
with persistence_layer.transact_writer() as transact:
transact.put(
item={
'sk': '0',
'created_at': now_,
'access_expires_at': access_expires_at,
**enrollment.model_dump(),
}
| ({'subscription_covered': True} if subscription else {})
| ({'org_id': org['org_id']} if org else {}),
)
# Relationships between this enrollment and its related entities
for entity in linked_entities:
# Parent knows the child
transact.put(
item={
'id': entity.id,
'sk': f'LINKED_ENTITIES#CHILD#ENROLLMENT#{enrollment.id}',
'created_at': now_,
},
cond_expr='attribute_not_exists(sk)',
table_name=entity.table_name,
)
# Child knows the parent
transact.put(
item={
'id': enrollment.id,
'sk': f'LINKED_ENTITIES#PARENT#{entity.kind.value}#{entity.id}',
'created_at': now_,
},
cond_expr='attribute_not_exists(sk)',
)
if org:
transact.put(
item={
'id': enrollment.id,
'sk': 'ORG',
'created_at': now_,
}
| org
)
if subscription:
transact.put(
item={
'id': enrollment.id,
'sk': 'METADATA#SUBSCRIPTION_COVERED',
'created_at': now_,
}
| subscription,
)
# 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
)
dedup_lock_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': dedup_lock_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': dedup_lock_ttl,
},
)
# The deduplication window can be recalculated based on user settings.
if deduplication_window:
transact.put(
item={
'id': enrollment.id,
'sk': 'METADATA#DEDUPLICATION_WINDOW',
'offset_days': offset_days,
'created_at': now_,
},
)
return True