add purge to canceled and failed enrollment
This commit is contained in:
@@ -1,8 +1,5 @@
|
||||
from typing import Annotated
|
||||
|
||||
from aws_lambda_powertools import Logger
|
||||
from aws_lambda_powertools.event_handler.api_gateway import Router
|
||||
from aws_lambda_powertools.event_handler.openapi.params import Body
|
||||
from layercake.dateutils import now
|
||||
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair
|
||||
|
||||
@@ -17,10 +14,7 @@ dyn = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client)
|
||||
|
||||
|
||||
@router.patch('/<enrollment_id>/cancel')
|
||||
def cancel(
|
||||
enrollment_id: str,
|
||||
lock_hash: Annotated[str | None, Body(embed=True)] = None,
|
||||
):
|
||||
def cancel(enrollment_id: str):
|
||||
now_ = now()
|
||||
canceled_by: Authenticated = router.context['user']
|
||||
|
||||
@@ -50,33 +44,7 @@ def cancel(
|
||||
}
|
||||
)
|
||||
transact.delete(
|
||||
key=KeyPair(
|
||||
pk=enrollment_id,
|
||||
sk='CANCEL_POLICY',
|
||||
),
|
||||
key=KeyPair(enrollment_id, 'CANCEL_POLICY'),
|
||||
cond_expr='attribute_exists(sk)',
|
||||
exc_cls=CancelPolicyConflictError,
|
||||
)
|
||||
# Remove reminders and policies that no longer apply
|
||||
transact.delete(
|
||||
key=KeyPair(
|
||||
pk=enrollment_id,
|
||||
sk='SCHEDULE#REMINDER_NO_ACCESS_AFTER_3_DAYS',
|
||||
)
|
||||
)
|
||||
transact.delete(
|
||||
key=KeyPair(
|
||||
pk=enrollment_id,
|
||||
sk='SCHEDULE#REMINDER_ACCESS_PERIOD_BEFORE_30_DAYS',
|
||||
)
|
||||
)
|
||||
transact.delete(
|
||||
key=KeyPair(
|
||||
pk=enrollment_id,
|
||||
sk='METADATA#PARENT_SLOT',
|
||||
)
|
||||
)
|
||||
|
||||
if lock_hash:
|
||||
transact.delete(key=KeyPair(enrollment_id, 'LOCK'))
|
||||
transact.delete(key=KeyPair('LOCK', lock_hash))
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import type { ColumnDef } from '@tanstack/react-table'
|
||||
import type { ComponentProps, MouseEvent } from 'react'
|
||||
import { useRequest, useToggle } from 'ahooks'
|
||||
import {
|
||||
CircleXIcon,
|
||||
@@ -9,6 +8,7 @@ import {
|
||||
FileBadgeIcon,
|
||||
LockOpenIcon
|
||||
} from 'lucide-react'
|
||||
import type { ComponentProps, MouseEvent } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import {
|
||||
@@ -36,8 +36,8 @@ import {
|
||||
} from '@repo/ui/components/ui/dropdown-menu'
|
||||
import { Spinner } from '@repo/ui/components/ui/spinner'
|
||||
import {
|
||||
type Enrollment,
|
||||
columns as columns_
|
||||
columns as columns_,
|
||||
type Enrollment
|
||||
} from '@repo/ui/routes/enrollments/columns'
|
||||
|
||||
export type { Enrollment }
|
||||
@@ -123,7 +123,6 @@ function ActionMenu({ row }: { row: any }) {
|
||||
<CancelItem
|
||||
id={row.id}
|
||||
cancelPolicy={data?.cancel_policy}
|
||||
lock={data?.lock}
|
||||
onSuccess={onSuccess}
|
||||
/>
|
||||
</>
|
||||
@@ -256,14 +255,10 @@ const getDaysRemaining = (ttl: number) => {
|
||||
function CancelItem({
|
||||
id,
|
||||
cancelPolicy,
|
||||
lock,
|
||||
onSuccess,
|
||||
...props
|
||||
}: ItemProps & {
|
||||
cancelPolicy?: object
|
||||
lock?: {
|
||||
hash: string
|
||||
}
|
||||
}) {
|
||||
const [loading, { set }] = useToggle(false)
|
||||
const [open, { set: setOpen }] = useToggle(false)
|
||||
@@ -274,8 +269,7 @@ function CancelItem({
|
||||
|
||||
const r = await fetch(`/~/api/enrollments/${id}/cancel`, {
|
||||
method: 'PATCH',
|
||||
headers: new Headers({ 'Content-Type': 'application/json' }),
|
||||
body: JSON.stringify({ lock_hash: lock?.hash })
|
||||
headers: new Headers({ 'Content-Type': 'application/json' })
|
||||
})
|
||||
|
||||
if (r.ok) {
|
||||
|
||||
@@ -61,6 +61,8 @@ Org = TypedDict('Org', {'org_id': str, 'name': str})
|
||||
|
||||
CreatedBy = TypedDict('CreatedBy', {'id': str, 'name': str})
|
||||
|
||||
Seat = TypedDict('Seat', {'id': str, 'sk': str})
|
||||
|
||||
DeduplicationWindow = TypedDict('DeduplicationWindow', {'offset_days': int})
|
||||
|
||||
Subscription = TypedDict(
|
||||
@@ -100,6 +102,11 @@ class SubscriptionFrozenError(Exception):
|
||||
super().__init__('Subscription is frozen')
|
||||
|
||||
|
||||
class SeatNotFoundError(Exception):
|
||||
def __init__(self, msg: str | dict):
|
||||
super().__init__('Seat required')
|
||||
|
||||
|
||||
def enroll(
|
||||
enrollment: Enrollment,
|
||||
*,
|
||||
@@ -108,6 +115,7 @@ def enroll(
|
||||
subscription: Subscription | None = None,
|
||||
created_by: CreatedBy | None = None,
|
||||
scheduled_at: datetime | None = None,
|
||||
seat: Seat | None = None,
|
||||
linked_entities: frozenset[LinkedEntity] = frozenset(),
|
||||
deduplication_window: DeduplicationWindow | None = None,
|
||||
persistence_layer: DynamoDBPersistenceLayer,
|
||||
@@ -138,6 +146,8 @@ def enroll(
|
||||
'sk': 'CANCEL_POLICY',
|
||||
'created_at': now_,
|
||||
}
|
||||
#
|
||||
| ({'seat': seat} if seat else {})
|
||||
)
|
||||
|
||||
# Relationships between this enrollment and its related entities
|
||||
|
||||
@@ -30,6 +30,7 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
|
||||
offset_days = old_image.get('dedup_window_offset_days')
|
||||
billing_day = old_image.get('subscription_billing_day')
|
||||
created_by = old_image.get('created_by')
|
||||
seat = old_image.get('seat')
|
||||
enrollment = Enrollment(
|
||||
course=old_image['course'],
|
||||
user=old_image['user'],
|
||||
@@ -53,6 +54,7 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
|
||||
subscription=subscription,
|
||||
cancel_policy=bool(subscription),
|
||||
created_by=created_by,
|
||||
seat=seat,
|
||||
scheduled_at=datetime.fromisoformat(old_image['scheduled_at']),
|
||||
# Transfer the deduplication window if it exists
|
||||
deduplication_window={'offset_days': offset_days} if offset_days else None,
|
||||
|
||||
41
enrollments-events/app/events/purge_reminders.py
Normal file
41
enrollments-events/app/events/purge_reminders.py
Normal file
@@ -0,0 +1,41 @@
|
||||
from aws_lambda_powertools import Logger
|
||||
from aws_lambda_powertools.utilities.data_classes import (
|
||||
EventBridgeEvent,
|
||||
event_source,
|
||||
)
|
||||
from aws_lambda_powertools.utilities.typing import LambdaContext
|
||||
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair
|
||||
from layercake.strutils import md5_hash
|
||||
|
||||
from boto3clients import dynamodb_client
|
||||
from config import ENROLLMENT_TABLE
|
||||
|
||||
logger = Logger(__name__)
|
||||
dyn = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client)
|
||||
|
||||
|
||||
@event_source(data_class=EventBridgeEvent)
|
||||
@logger.inject_lambda_context
|
||||
def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
|
||||
new_image = event.detail['new_image']
|
||||
enrollment_id = new_image['id']
|
||||
user_id = new_image['user']['id']
|
||||
course_id = new_image['course']['id']
|
||||
lock_hash = md5_hash(f'{user_id}{course_id}')
|
||||
|
||||
with dyn.transact_writer() as transact:
|
||||
transact.delete(
|
||||
key=KeyPair(enrollment_id, 'SCHEDULE#REMINDER_NO_ACCESS_AFTER_3_DAYS')
|
||||
)
|
||||
transact.delete(
|
||||
key=KeyPair(enrollment_id, 'SCHEDULE#REMINDER_NO_ACTIVITY_AFTER_7_DAYS')
|
||||
)
|
||||
transact.delete(
|
||||
key=KeyPair(enrollment_id, 'SCHEDULE#REMINDER_ACCESS_PERIOD_BEFORE_30_DAYS')
|
||||
)
|
||||
transact.delete(key=KeyPair(enrollment_id, 'CANCEL_POLICY'))
|
||||
# Remove locks related to this enrollment
|
||||
transact.delete(key=KeyPair(enrollment_id, 'LOCK'))
|
||||
transact.delete(key=KeyPair('LOCK', lock_hash))
|
||||
|
||||
return True
|
||||
@@ -326,6 +326,27 @@ Resources:
|
||||
sk: ['0']
|
||||
status: [PENDING]
|
||||
|
||||
EventPurgeRemindersFunction:
|
||||
Type: AWS::Serverless::Function
|
||||
Properties:
|
||||
Handler: events.purge_reminders.lambda_handler
|
||||
LoggingConfig:
|
||||
LogGroup: !Ref EventLog
|
||||
Policies:
|
||||
- DynamoDBCrudPolicy:
|
||||
TableName: !Ref EnrollmentTable
|
||||
Events:
|
||||
DynamoDBEvent:
|
||||
Type: EventBridgeRule
|
||||
Properties:
|
||||
Pattern:
|
||||
resources: [!Ref EnrollmentTable]
|
||||
detail-type: [MODIFY]
|
||||
detail:
|
||||
new_image:
|
||||
sk: ['0']
|
||||
status: [CANCELED, FAILED]
|
||||
|
||||
# Deprecated
|
||||
EventSetAccessExpiredFunction:
|
||||
Type: AWS::Serverless::Function
|
||||
|
||||
29
enrollments-events/tests/events/test_purge_reminders.py
Normal file
29
enrollments-events/tests/events/test_purge_reminders.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from aws_lambda_powertools.utilities.typing import LambdaContext
|
||||
from layercake.dynamodb import DynamoDBPersistenceLayer
|
||||
|
||||
import events.purge_reminders as app
|
||||
|
||||
|
||||
def test_purge_reminders(
|
||||
dynamodb_seeds,
|
||||
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
|
||||
lambda_context: LambdaContext,
|
||||
):
|
||||
event = {
|
||||
'detail': {
|
||||
'new_image': {
|
||||
'id': '14682b79-3df2-4351-9229-8b558af046a0',
|
||||
'sk': '0',
|
||||
'user': {
|
||||
'id': '1234',
|
||||
'name': 'Ozzy Osbourne',
|
||||
'email': 'ozzy@osbourne.com',
|
||||
},
|
||||
'course': {
|
||||
'id': '12334',
|
||||
'name': 'pytest',
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
assert app.lambda_handler(event, lambda_context) # type: ignore
|
||||
@@ -140,8 +140,6 @@ def set_score(
|
||||
id,
|
||||
score,
|
||||
progress=progress,
|
||||
user_id=user_id,
|
||||
course_id=course_id,
|
||||
dynamodb_persistence_layer=dynamodb_persistence_layer,
|
||||
)
|
||||
except EnrollmentConflictError as err:
|
||||
@@ -251,26 +249,6 @@ def _set_status_as_completed(
|
||||
}
|
||||
)
|
||||
|
||||
# Remove reminders and policies that no longer apply
|
||||
transact.delete(
|
||||
key=KeyPair(
|
||||
pk=id,
|
||||
sk='CANCEL_POLICY',
|
||||
)
|
||||
)
|
||||
transact.delete(
|
||||
key=KeyPair(
|
||||
pk=id,
|
||||
sk='SCHEDULE#REMINDER_NO_ACTIVITY_AFTER_7_DAYS',
|
||||
)
|
||||
)
|
||||
transact.delete(
|
||||
key=KeyPair(
|
||||
pk=id,
|
||||
sk='SCHEDULE#REMINDER_ACCESS_PERIOD_BEFORE_30_DAYS',
|
||||
)
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -279,13 +257,10 @@ def _set_status_as_failed(
|
||||
/,
|
||||
score: Decimal,
|
||||
progress: Decimal,
|
||||
user_id: str,
|
||||
course_id: str,
|
||||
*,
|
||||
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
|
||||
) -> bool:
|
||||
now_ = now()
|
||||
lock_hash = md5_hash(f'{user_id}{course_id}')
|
||||
|
||||
with dynamodb_persistence_layer.transact_writer() as transact:
|
||||
transact.update(
|
||||
@@ -308,32 +283,6 @@ def _set_status_as_failed(
|
||||
},
|
||||
exc_cls=EnrollmentConflictError,
|
||||
)
|
||||
# Remove reminders and events that no longer apply
|
||||
transact.delete(
|
||||
key=KeyPair(
|
||||
pk=id,
|
||||
sk='CANCEL_POLICY',
|
||||
)
|
||||
)
|
||||
transact.delete(
|
||||
key=KeyPair(
|
||||
pk=id,
|
||||
sk='SCHEDULE#REMINDER_NO_ACTIVITY_AFTER_7_DAYS',
|
||||
)
|
||||
)
|
||||
transact.delete(
|
||||
key=KeyPair(
|
||||
pk=id,
|
||||
sk='SCHEDULE#REMINDER_ACCESS_PERIOD_BEFORE_30_DAYS',
|
||||
)
|
||||
)
|
||||
# Remove locks related to this enrollment
|
||||
transact.delete(
|
||||
key=KeyPair(pk=id, sk='LOCK'),
|
||||
)
|
||||
transact.delete(
|
||||
key=KeyPair(pk='LOCK', sk=lock_hash),
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -244,6 +244,13 @@ else:
|
||||
self.rename_key = rename_key
|
||||
self.remove_prefix = remove_prefix
|
||||
|
||||
if remove_prefix:
|
||||
warnings.warn(
|
||||
'SortKey.remove_prefix() is deprecated and will be removed in the future.',
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
|
||||
class Key(ABC, dict):
|
||||
@abstractmethod
|
||||
@@ -671,9 +678,7 @@ class DynamoDBPersistenceLayer:
|
||||
try:
|
||||
r = self.client.query(**attrs)
|
||||
except ClientError as err:
|
||||
logger.info(attrs)
|
||||
logger.exception(err)
|
||||
raise
|
||||
raise err
|
||||
else:
|
||||
return dict(
|
||||
items=[deserialize(v) for v in r.get('Items', [])],
|
||||
@@ -696,8 +701,7 @@ class DynamoDBPersistenceLayer:
|
||||
r = self.client.get_item(**attrs)
|
||||
except ClientError as err:
|
||||
logger.info(attrs)
|
||||
logger.exception(err)
|
||||
raise
|
||||
raise err
|
||||
else:
|
||||
return deserialize(r.get('Item', {}))
|
||||
|
||||
@@ -720,8 +724,7 @@ class DynamoDBPersistenceLayer:
|
||||
self.client.put_item(**attrs)
|
||||
except ClientError as err:
|
||||
logger.info(attrs)
|
||||
logger.exception(err)
|
||||
raise
|
||||
raise err
|
||||
else:
|
||||
return True
|
||||
|
||||
@@ -754,8 +757,7 @@ class DynamoDBPersistenceLayer:
|
||||
self.client.update_item(**attrs)
|
||||
except ClientError as err:
|
||||
logger.info(attrs)
|
||||
logger.exception(err)
|
||||
raise
|
||||
raise err
|
||||
else:
|
||||
return True
|
||||
|
||||
@@ -790,8 +792,7 @@ class DynamoDBPersistenceLayer:
|
||||
self.client.delete_item(**attrs)
|
||||
except ClientError as err:
|
||||
logger.info(attrs)
|
||||
logger.exception(err)
|
||||
raise
|
||||
raise err
|
||||
else:
|
||||
return True
|
||||
|
||||
@@ -1134,6 +1135,12 @@ class DynamoDBCollection:
|
||||
return sk.rename_key
|
||||
|
||||
if isinstance(sk, SortKey):
|
||||
warnings.warn(
|
||||
'SortKey.remove_prefix() is deprecated and will be removed '
|
||||
'in the future.',
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return sk.removeprefix(sk.remove_prefix or '')
|
||||
|
||||
return pk
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import re
|
||||
from datetime import date
|
||||
from typing import TYPE_CHECKING, Annotated, Any
|
||||
|
||||
import ftfy
|
||||
@@ -74,6 +73,7 @@ class CreditCard(BaseModel):
|
||||
>>> cc
|
||||
CreditCard(holder_name='Mike Shinoda', number='4111111111111111', cvv='123', exp_month='01', exp_year='2026')
|
||||
"""
|
||||
|
||||
holder_name: NameStr
|
||||
number: PaymentCardNumber
|
||||
cvv: str = Field(..., min_length=3)
|
||||
|
||||
2
layercake/uv.lock
generated
2
layercake/uv.lock
generated
@@ -836,7 +836,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "layercake"
|
||||
version = "0.13.0"
|
||||
version = "0.13.1"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "arnparse" },
|
||||
|
||||
Reference in New Issue
Block a user