This commit is contained in:
2026-01-21 21:31:32 -03:00
parent 26c3df876f
commit 37a9b20188
38 changed files with 1009 additions and 532 deletions

View File

@@ -1,5 +1,5 @@
from datetime import date, datetime, time, timedelta
from typing import Annotated, TypedDict
from typing import Annotated, NotRequired, TypedDict
from uuid import uuid4
import pytz
@@ -16,7 +16,12 @@ from pydantic import UUID4, BaseModel, EmailStr, Field, FutureDate
from boto3clients import dynamodb_client
from config import DEDUP_WINDOW_OFFSET_DAYS, ENROLLMENT_TABLE, TZ, USER_TABLE
from exceptions import ConflictError, SubscriptionFrozenError, SubscriptionRequiredError
from exceptions import (
ConflictError,
SubscriptionConflictError,
SubscriptionFrozenError,
SubscriptionRequiredError,
)
from middlewares.authentication_middleware import User as Authenticated
logger = Logger(__name__)
@@ -49,12 +54,30 @@ class Subscription(BaseModel):
billing_day: int
class Seat(BaseModel):
id: str = Field(..., pattern=r'^SEAT#ORG#.+$')
sk: str = Field(..., pattern=r'^ORDER#.+#ENROLLMENT#.+$')
def org_id(self) -> str:
*_, org_id = self.id.split('#')
return org_id
def order_id(self) -> str:
_, order_id, *_ = self.sk.split('#')
return order_id
def enrollment_id(self) -> str:
*_, enrollment_id = self.sk.split('#')
return enrollment_id
class Enrollment(BaseModel):
id: UUID4 = Field(default_factory=uuid4)
user: User
course: Course
scheduled_for: FutureDate | None = None
deduplication_window: DeduplicationWindow | None = None
seat: Seat | None = None
class Org(BaseModel):
@@ -66,30 +89,22 @@ class Org(BaseModel):
def enroll(
org_id: Annotated[str | UUID4, Body(embed=True)],
enrollments: Annotated[tuple[Enrollment, ...], Body(embed=True)],
subscription: Annotated[Subscription | None, Body(embed=True)] = None,
):
now_ = now()
created_by: Authenticated = router.context['user']
org = dyn.collection.get_items(
org = dyn.collection.get_item(
KeyPair(
pk=str(org_id),
sk='0',
table_name=USER_TABLE,
)
+ KeyPair(
pk=str(org_id),
sk='METADATA#SUBSCRIPTION',
rename_key='subscription',
table_name=USER_TABLE,
)
)
if 'subscription' not in org:
raise SubscriptionRequiredError('Organization not subscribed')
ctx = {
'org': Org.model_validate(org),
'created_by': created_by,
'subscription': Subscription.model_validate(org['subscription']),
'subscription': subscription,
}
immediate = [e for e in enrollments if not e.scheduled_for]
@@ -133,8 +148,8 @@ Context = TypedDict(
'Context',
{
'org': Org,
'subscription': Subscription,
'created_by': Authenticated,
'subscription': NotRequired[Subscription],
},
)
@@ -144,7 +159,7 @@ def enroll_now(enrollment: Enrollment, context: Context):
user = enrollment.user
course = enrollment.course
org: Org = context['org']
subscription: Subscription = context['subscription']
subscription: Subscription | None = context.get('subscription')
created_by: Authenticated = context['created_by']
lock_hash = md5_hash(f'{user.id}{course.id}')
access_expires_at = now_ + timedelta(days=course.access_period)
@@ -160,24 +175,6 @@ def enroll_now(enrollment: Enrollment, context: Context):
)
with dyn.transact_writer() as transact:
transact.condition(
key=KeyPair(
pk='SUBSCRIPTION',
sk=f'ORG#{org.id}',
),
cond_expr='attribute_exists(sk)',
exc_cls=SubscriptionRequiredError,
table_name=USER_TABLE,
)
transact.condition(
key=KeyPair(
pk='SUBSCRIPTION#FROZEN',
sk=f'ORG#{org.id}',
),
cond_expr='attribute_not_exists(sk)',
exc_cls=SubscriptionFrozenError,
table_name=USER_TABLE,
)
transact.put(
item={
'id': enrollment.id,
@@ -209,15 +206,6 @@ def enroll_now(enrollment: Enrollment, context: Context):
'created_at': now_,
}
)
transact.put(
item={
'id': enrollment.id,
'sk': 'METADATA#SUBSCRIPTION_COVERED',
'org_id': org.id,
'billing_day': subscription.billing_day,
'created_at': now_,
}
)
transact.put(
item={
'id': enrollment.id,
@@ -248,6 +236,38 @@ def enroll_now(enrollment: Enrollment, context: Context):
exc_cls=DeduplicationConflictError,
)
if subscription:
transact.put(
item={
'id': enrollment.id,
'sk': 'METADATA#SUBSCRIPTION_COVERED',
'org_id': org.id,
'billing_day': subscription.billing_day,
'created_at': now_,
}
)
transact.condition(
key=KeyPair('SUBSCRIPTION', f'ORG#{org.id}'),
cond_expr='attribute_exists(sk)',
exc_cls=SubscriptionRequiredError,
table_name=USER_TABLE,
)
transact.condition(
key=KeyPair(str(org.id), 'METADATA#SUBSCRIPTION'),
cond_expr='billing_day = :billing_day',
expr_attr_values={
':billing_day': subscription.billing_day,
},
exc_cls=SubscriptionConflictError,
table_name=USER_TABLE,
)
transact.condition(
key=KeyPair('SUBSCRIPTION#FROZEN', f'ORG#{org.id}'),
cond_expr='attribute_not_exists(sk)',
exc_cls=SubscriptionFrozenError,
table_name=USER_TABLE,
)
# The deduplication window can be recalculated based on user settings.
if deduplication_window:
transact.put(
@@ -267,9 +287,9 @@ def enroll_later(enrollment: Enrollment, context: Context):
user = enrollment.user
course = enrollment.course
scheduled_for = date_to_midnight(enrollment.scheduled_for) # type: ignore
deduplication_window = enrollment.deduplication_window
dedup_window = enrollment.deduplication_window
org: Org = context['org']
subscription: Subscription = context['subscription']
subscription: Subscription | None = context.get('subscription')
created_by: Authenticated = context['created_by']
lock_hash = md5_hash(f'{user.id}{course.id}')
@@ -277,24 +297,6 @@ def enroll_later(enrollment: Enrollment, context: Context):
pk = f'SCHEDULED#ORG#{org.id}'
sk = f'{scheduled_for.isoformat()}#{lock_hash}'
transact.condition(
key=KeyPair(
pk='SUBSCRIPTION',
sk=f'ORG#{org.id}',
),
cond_expr='attribute_exists(sk)',
exc_cls=SubscriptionRequiredError,
table_name=USER_TABLE,
)
transact.condition(
key=KeyPair(
pk='SUBSCRIPTION#FROZEN',
sk=f'ORG#{org.id}',
),
cond_expr='attribute_not_exists(sk)',
exc_cls=SubscriptionFrozenError,
table_name=USER_TABLE,
)
transact.put(
item={
'id': pk,
@@ -306,13 +308,19 @@ def enroll_later(enrollment: Enrollment, context: Context):
'id': created_by.id,
'name': created_by.name,
},
'subscription_billing_day': subscription.billing_day,
'ttl': ttl(start_dt=scheduled_for),
'scheduled_at': now_,
}
| (
{'dedup_window_offset_days': deduplication_window.offset_days}
if deduplication_window
{'dedup_window_offset_days': dedup_window.offset_days}
if dedup_window
else {}
)
| (
{
'subscription_billing_day': subscription.billing_day,
}
if subscription
else {}
),
)
@@ -331,6 +339,28 @@ def enroll_later(enrollment: Enrollment, context: Context):
exc_cls=DeduplicationConflictError,
)
if subscription:
transact.condition(
key=KeyPair('SUBSCRIPTION', f'ORG#{org.id}'),
cond_expr='attribute_exists(sk)',
exc_cls=SubscriptionRequiredError,
table_name=USER_TABLE,
)
transact.condition(
key=KeyPair(str(org.id), 'METADATA#SUBSCRIPTION'),
cond_expr='billing_day = :billing_day',
expr_attr_values={
':billing_day': subscription.billing_day,
},
exc_cls=SubscriptionConflictError,
)
transact.condition(
key=KeyPair('SUBSCRIPTION#FROZEN', f'ORG#{org.id}'),
cond_expr='attribute_not_exists(sk)',
exc_cls=SubscriptionFrozenError,
table_name=USER_TABLE,
)
return enrollment