WIP
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -69,14 +69,14 @@ class Address(BaseModel):
|
||||
class Item(BaseModel):
|
||||
id: UUID4
|
||||
name: str
|
||||
unit_price: Decimal
|
||||
quantity: int = 1
|
||||
unit_price: Decimal = Field(..., ge=1)
|
||||
quantity: int = Field(1, ge=1)
|
||||
|
||||
|
||||
class Coupon(BaseModel):
|
||||
code: str
|
||||
type: Literal['PERCENT', 'FIXED']
|
||||
amount: Decimal
|
||||
amount: Decimal = Field(..., ge=1)
|
||||
|
||||
|
||||
class Checkout(BaseModel):
|
||||
|
||||
Reference in New Issue
Block a user