finish seat

This commit is contained in:
2026-01-25 04:52:44 -03:00
parent 5fac7888a8
commit 3719842ae9
31 changed files with 731 additions and 134 deletions

View File

@@ -34,7 +34,6 @@ app = APIGatewayHttpResolver(
serializer=serializer,
)
app.use(middlewares=[AuthenticationMiddleware()])
app.enable_swagger(path='/swagger')
app.include_router(coupons.router, prefix='/coupons')
app.include_router(courses.router, prefix='/courses')
app.include_router(enrollments.router, prefix='/enrollments')

View File

@@ -59,3 +59,6 @@ class CPFConflictError(ConflictError): ...
class CancelPolicyConflictError(ConflictError): ...
class EnrollmentConflictError(ConflictError): ...

View File

@@ -5,7 +5,7 @@ from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair
from boto3clients import dynamodb_client
from config import ENROLLMENT_TABLE
from exceptions import CancelPolicyConflictError
from exceptions import CancelPolicyConflictError, EnrollmentConflictError
from middlewares.authentication_middleware import User as Authenticated
logger = Logger(__name__)
@@ -21,7 +21,7 @@ def cancel(enrollment_id: str):
with dyn.transact_writer() as transact:
transact.update(
key=KeyPair(enrollment_id, '0'),
cond_expr='#status = :pending',
cond_expr='attribute_exists(sk) AND #status = :pending',
update_expr='SET #status = :canceled, \
canceled_at = :now, \
updated_at = :now',
@@ -33,6 +33,7 @@ def cancel(enrollment_id: str):
':canceled': 'CANCELED',
':now': now_,
},
exc_cls=EnrollmentConflictError,
)
transact.put(
item={

View File

@@ -19,9 +19,16 @@ from layercake.strutils import md5_hash
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 config import (
DEDUP_WINDOW_OFFSET_DAYS,
ENROLLMENT_TABLE,
ORDER_TABLE,
TZ,
USER_TABLE,
)
from exceptions import (
ConflictError,
OrderNotFoundError,
SubscriptionConflictError,
SubscriptionFrozenError,
SubscriptionRequiredError,
@@ -62,20 +69,7 @@ class Subscription(BaseModel):
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
order_id: UUID4
class Enrollment(BaseModel):
@@ -166,9 +160,9 @@ def enroll_now(enrollment: Enrollment, context: Context):
user = enrollment.user
course = enrollment.course
seat = enrollment.seat
org: Org = context['org']
subscription: Subscription | None = context.get('subscription')
created_by: Authenticated = context['created_by']
org = context['org']
subscription = context.get('subscription')
created_by = context['created_by']
lock_hash = md5_hash(f'{user.id}{course.id}')
access_expires_at = now_ + timedelta(days=course.access_period)
deduplication_window = enrollment.deduplication_window
@@ -182,7 +176,7 @@ def enroll_now(enrollment: Enrollment, context: Context):
days=course.access_period - offset_days,
)
if not subscription and not seat:
if not (bool(subscription) ^ bool(seat)):
raise BadRequestError('Malformed body')
with dyn.transact_writer() as transact:
@@ -220,8 +214,29 @@ def enroll_now(enrollment: Enrollment, context: Context):
)
if seat:
transact.condition(
key=KeyPair(str(seat.order_id), '0'),
cond_expr='attribute_exists(sk)',
exc_cls=OrderNotFoundError,
table_name=ORDER_TABLE,
)
transact.put(
item={
'id': seat.order_id,
'sk': f'ENROLLMENT#{enrollment.id}',
'course': course.model_dump(),
'user': user.model_dump(),
'status': 'EXECUTED',
'executed_at': now_,
'created_at': now_,
},
table_name=ORDER_TABLE,
)
transact.delete(
key=KeyPair(seat.id, seat.sk),
key=KeyPair(
f'SEAT#ORG#{org.id}',
f'ORDER#{seat.order_id}#ENROLLMENT#{enrollment.id}',
),
cond_expr='attribute_exists(sk)',
exc_cls=SeatNotFoundError,
)
@@ -307,14 +322,14 @@ def enroll_later(enrollment: Enrollment, context: Context):
user = enrollment.user
course = enrollment.course
seat = enrollment.seat
scheduled_for = date_to_midnight(enrollment.scheduled_for) # type: ignore
scheduled_for = _date_to_midnight(enrollment.scheduled_for) # type: ignore
dedup_window = enrollment.deduplication_window
org: Org = context['org']
subscription: Subscription | None = context.get('subscription')
created_by: Authenticated = context['created_by']
org = context['org']
subscription = context.get('subscription')
created_by = context['created_by']
lock_hash = md5_hash(f'{user.id}{course.id}')
if not subscription and not seat:
if not (bool(subscription) ^ bool(seat)):
raise BadRequestError('Malformed body')
with dyn.transact_writer() as transact:
@@ -349,6 +364,35 @@ def enroll_later(enrollment: Enrollment, context: Context):
else {}
),
)
if seat:
transact.condition(
key=KeyPair(str(seat.order_id), '0'),
cond_expr='attribute_exists(sk)',
exc_cls=OrderNotFoundError,
table_name=ORDER_TABLE,
)
transact.put(
item={
'id': seat.order_id,
'sk': f'ENROLLMENT#{enrollment.id}',
'user': user.model_dump(),
'course': course.model_dump(),
'status': 'SCHEDULED',
'scheduled_at': now_,
'created_at': now_,
},
table_name=ORDER_TABLE,
)
transact.delete(
key=KeyPair(
f'SEAT#ORG#{org.id}',
f'ORDER#{seat.order_id}#ENROLLMENT#{enrollment.id}',
),
cond_expr='attribute_exists(sk)',
exc_cls=SeatNotFoundError,
)
transact.put(
item={
'id': 'LOCK#SCHEDULED',
@@ -387,15 +431,8 @@ def enroll_later(enrollment: Enrollment, context: Context):
table_name=USER_TABLE,
)
if seat:
transact.delete(
key=KeyPair(seat.id, seat.sk),
cond_expr='attribute_exists(sk)',
exc_cls=SeatNotFoundError,
)
return enrollment
def date_to_midnight(dt: date) -> datetime:
def _date_to_midnight(dt: date) -> datetime:
return datetime.combine(dt, time(0, 0)).replace(tzinfo=pytz.timezone(TZ))

View File

@@ -36,7 +36,7 @@ class User(BaseModel):
def add(org_id: str, user: Annotated[User, Body(embed=True)]):
now_ = now()
org = dyn.collection.get_item(
KeyPair(pk=org_id, sk='0'),
KeyPair(org_id, '0'),
exc_cls=OrgNotFoundError,
)

View File

@@ -1,5 +1,5 @@
from http import HTTPStatus
from typing import Annotated
from typing import Annotated, cast
from aws_lambda_powertools import Logger
from aws_lambda_powertools.event_handler.api_gateway import Router
@@ -11,8 +11,9 @@ from pydantic import FutureDatetime
from api_gateway import JSONResponse
from boto3clients import dynamodb_client
from config import ENROLLMENT_TABLE
from middlewares.authentication_middleware import User as Authenticated
from ...enrollments.enroll import Enrollment, Org, Subscription, enroll_now
from ...enrollments.enroll import Context, Enrollment, Org, Subscription, enroll_now
logger = Logger(__name__)
router = Router()
@@ -72,12 +73,18 @@ def proceed(
KeyPair(pk, sk),
exc_cls=ScheduledNotFoundError,
)
org = Org(
id=org_id,
name=scheduled['org_name'],
)
subscription = Subscription(
billing_day=scheduled['subscription_billing_day'],
billing_day = scheduled.get('subscription_billing_day')
ctx = cast(
Context,
{
'created_by': router.context['user'],
'org': Org(id=org_id, name=scheduled['org_name']),
**(
{'subscription': Subscription(billing_day=billing_day)}
if billing_day
else {}
),
},
)
try:
@@ -86,11 +93,7 @@ def proceed(
user=scheduled['user'],
course=scheduled['course'],
),
{
'org': org,
'subscription': subscription,
'created_by': router.context['user'],
},
ctx,
)
with dyn.transact_writer() as transact:

View File

@@ -2,10 +2,10 @@ from aws_lambda_powertools.event_handler.api_gateway import Router
from layercake.dynamodb import DynamoDBPersistenceLayer, PartitionKey
from boto3clients import dynamodb_client
from config import COURSE_TABLE
from config import ENROLLMENT_TABLE
router = Router()
dyn = DynamoDBPersistenceLayer(COURSE_TABLE, dynamodb_client)
dyn = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client)
@router.get('/<org_id>/seats')

View File

@@ -38,9 +38,6 @@ class User(BaseModel):
email: EmailStr
# class OrgNotFoundError(NotFoundError): ...
@router.post('/<org_id>/users')
def add(
org_id: str,