From 68a187220cbb72208dcf38ec1b83989b06d8ac24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Rafael=20Siqueira?= Date: Fri, 25 Jul 2025 12:46:17 -0300 Subject: [PATCH] add billing --- order-events/app/events/billing/__init__.py | 0 .../app/events/billing/append_enrollment.py | 125 ++++++++++++++++++ 2 files changed, 125 insertions(+) create mode 100644 order-events/app/events/billing/__init__.py create mode 100644 order-events/app/events/billing/append_enrollment.py diff --git a/order-events/app/events/billing/__init__.py b/order-events/app/events/billing/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/order-events/app/events/billing/append_enrollment.py b/order-events/app/events/billing/append_enrollment.py new file mode 100644 index 0000000..bfd3d23 --- /dev/null +++ b/order-events/app/events/billing/append_enrollment.py @@ -0,0 +1,125 @@ +from datetime import datetime, time, timedelta + +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.dateutils import now, ttl +from layercake.dynamodb import ( + DynamoDBPersistenceLayer, + KeyPair, + SortKey, + TransactKey, +) +from layercake.funcs import pick + +from boto3clients import dynamodb_client +from config import COURSE_TABLE, ENROLLMENT_TABLE, ORDER_TABLE +from utils import get_billing_period + +logger = Logger(__name__) +order_layer = DynamoDBPersistenceLayer(ORDER_TABLE, dynamodb_client) +enrollment_layer = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client) +course_layer = DynamoDBPersistenceLayer(COURSE_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'] + now_ = now() + enrollment_id = new_image['id'] + org_id = new_image['org_id'] + data = enrollment_layer.collection.get_items( + TransactKey(enrollment_id) + SortKey('0') + SortKey('author') + ) + + if not data: + logger.debug('Enrollment not found') + return False + + start_date, end_date = get_billing_period(new_image['billing_day']) + pk = 'BILLING#ORG#{org_id}'.format(org_id=org_id) + sk = 'START#{start}#END#{end}'.format( + start=start_date.isoformat(), + end=end_date.isoformat(), + ) + + try: + with order_layer.transact_writer() as transact: + transact.put( + item={ + 'id': pk, + 'sk': sk, + 'status': 'PENDING', + 'created_at': now_, + }, + cond_expr='attribute_not_exists(sk)', + exc_cls=ExistingBillingConflictError, + ) + transact.put( + item={ + 'id': pk, + 'sk': f'{sk}#SCHEDULE#AUTO_CLOSE', + 'ttl': ttl( + start_dt=datetime.combine(end_date, time()) + timedelta(days=1) + ), + 'created_at': now_, + } + ) + except ExistingBillingConflictError: + pass + + try: + with order_layer.transact_writer() as transact: + author = data['author'] + course_id = data['course']['id'] + course = course_layer.collection.get_items( + KeyPair( + pk=course_id, + sk=SortKey('0', path_spec='metadata__unit_price'), + rename_key='unit_price', + ) + + KeyPair( + pk=f'CUSTOM_PRICING#ORG#{org_id}', + sk=SortKey(f'COURSE#{course_id}', path_spec='unit_price'), + rename_key='unit_price', + ), + flatten_top=False, + ) + + transact.condition( + key=KeyPair(pk, sk), + cond_expr='attribute_exists(sk)', + exc_cls=BillingNotFoundError, + ) + transact.put( + item={ + 'id': pk, + 'sk': f'{sk}#ENROLLMENT#{enrollment_id}', + 'user': pick(('id', 'name'), data['user']), + 'course': pick(('id', 'name'), data['course']), + 'unit_price': course['unit_price'], + 'author': { + 'id': author['user_id'], + 'name': author['name'], + }, + # Post-migration: uncomment the following line + # 'enrolled_at': data['created_at'], + 'enrolled_at': data['create_date'], + 'created_at': now_, + }, + cond_expr='attribute_not_exists(sk)', + ) + except Exception: + return False + else: + return True + + +class ExistingBillingConflictError(Exception): ... + + +class BillingNotFoundError(Exception): ...