This commit is contained in:
2025-07-01 17:45:17 -03:00
parent d50d828029
commit a9f6a89d54
10 changed files with 337 additions and 28 deletions

View File

@@ -1,23 +0,0 @@
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,
)
from boto3clients import dynamodb_client
from config import ORDER_TABLE, USER_TABLE
logger = Logger(__name__)
user_layer = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client)
order_layer = DynamoDBPersistenceLayer(ORDER_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']
return True

View File

@@ -0,0 +1,57 @@
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 (
ComposeKey,
DynamoDBPersistenceLayer,
KeyPair,
SortKey,
)
from boto3clients import dynamodb_client
from config import ENROLLMENT_TABLE, ORDER_TABLE
logger = Logger(__name__)
enrollment_layer = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client)
order_layer = DynamoDBPersistenceLayer(ORDER_TABLE, dynamodb_client)
class TenantDoesNotExistError(Exception):
def __init__(self, *args):
super().__init__('Tenant does not exist')
@event_source(data_class=EventBridgeEvent)
@logger.inject_lambda_context
def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
new_image = event.detail['new_image']
order_id = new_image['id']
tenant_id = order_layer.collection.get_item(
KeyPair(
order_id,
SortKey('metadata#tenant', path_spec='tenant_id'),
),
exc_cls=TenantDoesNotExistError,
)
result = enrollment_layer.collection.query(
KeyPair(
# Post-migration: rename `vacancies` to `slots`
ComposeKey(tenant_id, prefix='vacancies'),
order_id,
)
)
with enrollment_layer.batch_writer() as batch:
for pair in result['items']:
batch.delete_item(
Key={
# Post-migration: rename `vacancies` to `slots`
'id': {'S': ComposeKey(pair['id'], prefix='vacancies')},
'sk': {'S': pair['sk']},
}
)
return True

View File

@@ -0,0 +1,4 @@
"""
Stopgap events. Everything here is a quick fix and should be replaced with
proper solutions.
"""

View File

@@ -0,0 +1,71 @@
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 (
ComposeKey,
DynamoDBPersistenceLayer,
KeyPair,
SortKey,
TransactKey,
)
from boto3clients import dynamodb_client
from config import ENROLLMENT_TABLE, ORDER_TABLE, USER_TABLE
logger = Logger(__name__)
user_layer = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client)
order_layer = DynamoDBPersistenceLayer(ORDER_TABLE, dynamodb_client)
enrollment_layer = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client)
@event_source(data_class=EventBridgeEvent)
@logger.inject_lambda_context
def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
"""Remove slots if the Tenant has a `metadata#billing_policy` and
the order is positive."""
new_image = event.detail['new_image']
order_id = new_image['id']
data = order_layer.collection.get_items(
TransactKey(order_id)
+ SortKey('0')
+ KeyPair(
pk=order_id,
sk=SortKey(
sk='metadata#tenant',
path_spec='tenant_id',
remove_prefix='metadata#',
),
rename_key='tenant_id',
)
)
tenant_id = data['tenant_id'].removeprefix('ORG#')
policy = user_layer.collection.get_item(
KeyPair(pk=tenant_id, sk='metadata#billing_policy'),
raise_on_error=False,
default=False,
)
# Skip if missing billing policy or order is zero/negative
if not policy or data['total'] <= 0:
return False
result = enrollment_layer.collection.query(
KeyPair(
ComposeKey(tenant_id, prefix='vacancies'),
order_id,
)
)
with enrollment_layer.batch_writer() as batch:
for pair in result['items']:
batch.delete_item(
Key={
'id': {'S': ComposeKey(pair['id'], prefix='vacancies')},
'sk': {'S': pair['sk']},
}
)
return True

View File

@@ -20,7 +20,7 @@ Globals:
Architectures:
- x86_64
Layers:
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:72
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:78
Environment:
Variables:
TZ: America/Sao_Paulo
@@ -30,6 +30,7 @@ Globals:
POWERTOOLS_LOGGER_LOG_EVENT: true
USER_TABLE: !Ref UserTable
ORDER_TABLE: !Ref OrderTable
ENROLLMENT_TABLE: !Ref EnrollmentTable
Resources:
EventLog:
@@ -89,6 +90,31 @@ Resources:
metadata__tenant_id:
- exists: false
EventRemoveSlotsOnCanceledFunction:
Type: AWS::Serverless::Function
Properties:
Handler: events.delete_slots_on_canceled.lambda_handler
LoggingConfig:
LogGroup: !Ref EventLog
Policies:
- DynamoDBWritePolicy:
TableName: !Ref OrderTable
- DynamoDBWritePolicy:
TableName: !Ref EnrollmentTable
Events:
Event:
Type: EventBridgeRule
Properties:
Pattern:
resources: [!Ref OrderTable]
detail-type: [MODIFY]
detail:
new_image:
sk: ["0"]
cnpj:
- exists: true
status: [CANCELED, EXPIRED]
EventSetAsPaidFunction:
Type: AWS::Serverless::Function
Properties:
@@ -113,3 +139,27 @@ Resources:
total: [0]
status: [CREATING, PENDING]
payment_method: [MANUAL]
EventRemoveSlotsFunction:
Type: AWS::Serverless::Function
Properties:
Handler: events.stopgap.remove_slots.lambda_handler
LoggingConfig:
LogGroup: !Ref EventLog
Policies:
- DynamoDBReadPolicy:
TableName: !Ref UserTable
- DynamoDBReadPolicy:
TableName: !Ref OrderTable
- DynamoDBCrudPolicy:
TableName: !Ref EnrollmentTable
Events:
DynamoDBEvent:
Type: EventBridgeRule
Properties:
Pattern:
resources: [!Ref OrderTable]
detail:
new_image:
sk: [generated_items]
status: [SUCCESS]

View File

@@ -0,0 +1,30 @@
from layercake.dynamodb import PartitionKey
import events.stopgap.remove_slots as app
from ...conftest import LambdaContext
def test_remove_slots(
dynamodb_seeds,
dynamodb_persistence_layer,
lambda_context: LambdaContext,
):
event = {
'detail': {
'new_image': {
'id': '9omWNKymwU5U4aeun6mWzZ',
'sk': 'generated_items',
'create_date': '2024-07-23T20:43:37.303418-03:00',
'status': 'SUCCESS',
'scope': 'MILTI_USER',
}
},
}
assert app.lambda_handler(event, lambda_context) # type: ignore
result = dynamodb_persistence_layer.collection.query(
PartitionKey('vacancies#cJtK9SsnJhKPyxESe7g3DG')
)
assert len(result['items']) == 0

View File

@@ -0,0 +1,29 @@
from aws_lambda_powertools.utilities.typing import LambdaContext
from layercake.dynamodb import DynamoDBPersistenceLayer, PartitionKey
import events.assign_tenant_cnpj as app
def test_assign_tenant_cnpj(
dynamodb_seeds,
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
lambda_context: LambdaContext,
):
event = {
'detail': {
'new_image': {
'id': '9omWNKymwU5U4aeun6mWzZ',
'cnpj': '15608435000190',
'email': 'sergio@somosbeta.com.br',
}
}
}
assert app.lambda_handler(event, lambda_context) # type: ignore
result = dynamodb_persistence_layer.collection.query(
PartitionKey('9omWNKymwU5U4aeun6mWzZ')
)
assert 4 == len(result['items'])
print(result['items'])

View File

@@ -0,0 +1,27 @@
from aws_lambda_powertools.utilities.typing import LambdaContext
from layercake.dynamodb import DynamoDBPersistenceLayer, PartitionKey
import events.remove_slots_on_canceled as app
def test_delete_slots_on_canceled(
dynamodb_seeds,
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
lambda_context: LambdaContext,
):
event = {
'detail': {
'new_image': {
'id': '9omWNKymwU5U4aeun6mWzZ',
'status': 'CANCELED',
}
}
}
assert app.lambda_handler(event, lambda_context) # type: ignore
result = dynamodb_persistence_layer.collection.query(
PartitionKey('vacancies#cJtK9SsnJhKPyxESe7g3DG')
)
assert len(result['items']) == 0

View File

@@ -1,7 +1,10 @@
{"id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "0"}, "name": {"S": "EDUSEG"}}
{"id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "metadata#payment_policy"}, "due_days": {"N": "90"}}
{"id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "metadata#billing_policy"}, "billing_day": {"N": "1"}, "payment_method": {"S": "PIX"}}
{"id": {"S": "9omWNKymwU5U4aeun6mWzZ"}, "sk": {"S": "0"}, "total": {"N": "398"}, "status": {"S": "PENDING"}}
{"id": {"S": "9omWNKymwU5U4aeun6mWzZ"}, "sk": {"S": "metadata#tenant"}, "org_id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}}
{"id": {"S": "9omWNKymwU5U4aeun6mWzZ"}, "sk": {"S": "metadata#tenant"}, "tenant_id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}}
{"id": {"S": "cnpj"}, "sk": {"S": "15608435000190"}, "user_id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}}
{"id": {"S": "email"}, "sk": {"S": "sergio@somosbeta.com.br"}, "user_id": {"S": "5OxmMjL-ujoR5IMGegQz"}}
{"id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "sk": {"S": "0"}, "name": {"S": "Sérgio R Siqueira"}}
{"id": {"S": "vacancies#cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "9omWNKymwU5U4aeun6mWzZ#1"}}
{"id": {"S": "vacancies#cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "9omWNKymwU5U4aeun6mWzZ#2"}}
{"id": {"S": "vacancies#cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "9omWNKymwU5U4aeun6mWzZ#3"}}

View File

@@ -221,6 +221,30 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" },
]
[[package]]
name = "click"
version = "8.2.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" },
]
[[package]]
name = "click-default-group"
version = "1.2.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1d/ce/edb087fb53de63dad3b36408ca30368f438738098e668b78c87f93cd41df/click_default_group-1.2.4.tar.gz", hash = "sha256:eb3f3c99ec0d456ca6cd2a7f08f7d4e91771bef51b01bdd9580cc6450fe1251e", size = 3505, upload-time = "2023-08-04T07:54:58.425Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/1a/aff8bb287a4b1400f69e09a53bd65de96aa5cee5691925b38731c67fc695/click_default_group-1.2.4-py2.py3-none-any.whl", hash = "sha256:9b60486923720e7fc61731bdb32b617039aba820e22e1c88766b1125592eaa5f", size = 4123, upload-time = "2023-08-04T07:54:56.875Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
@@ -495,7 +519,7 @@ wheels = [
[[package]]
name = "layercake"
version = "0.6.2"
version = "0.6.11"
source = { directory = "../layercake" }
dependencies = [
{ name = "arnparse" },
@@ -512,6 +536,7 @@ dependencies = [
{ name = "pytz" },
{ name = "requests" },
{ name = "smart-open", extra = ["s3"] },
{ name = "sqlite-utils" },
{ name = "weasyprint" },
]
@@ -531,6 +556,7 @@ requires-dist = [
{ name = "pytz", specifier = ">=2025.1" },
{ name = "requests", specifier = ">=2.32.3" },
{ name = "smart-open", extras = ["s3"], specifier = ">=7.1.0" },
{ name = "sqlite-utils", specifier = ">=3.38" },
{ name = "weasyprint", specifier = ">=65.0" },
]
@@ -911,6 +937,41 @@ s3 = [
{ name = "boto3" },
]
[[package]]
name = "sqlite-fts4"
version = "1.0.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c2/6d/9dad6c3b433ab8912ace969c66abd595f8e0a2ccccdb73602b1291dbda29/sqlite-fts4-1.0.3.tar.gz", hash = "sha256:78b05eeaf6680e9dbed8986bde011e9c086a06cb0c931b3cf7da94c214e8930c", size = 9718, upload-time = "2022-07-30T01:14:26.943Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/51/29/0096e8b1811aaa78cfb296996f621f41120c21c2f5cd448ae1d54979d9fc/sqlite_fts4-1.0.3-py3-none-any.whl", hash = "sha256:0359edd8dea6fd73c848989e1e2b1f31a50fe5f9d7272299ff0e8dbaa62d035f", size = 9972, upload-time = "2022-07-30T01:14:24.942Z" },
]
[[package]]
name = "sqlite-utils"
version = "3.38"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "click-default-group" },
{ name = "pluggy" },
{ name = "python-dateutil" },
{ name = "sqlite-fts4" },
{ name = "tabulate" },
]
sdist = { url = "https://files.pythonhosted.org/packages/51/43/ce9183a21911e0b73248c8fb83f8b8038515cb80053912c2a009e9765564/sqlite_utils-3.38.tar.gz", hash = "sha256:1ae77b931384052205a15478d429464f6c67a3ac3b4eafd3c674ac900f623aab", size = 214449, upload-time = "2024-11-23T22:49:40.308Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4d/eb/f8e8e827805f810838efff3311cccd2601238c5fa3fc35c1f878709e161b/sqlite_utils-3.38-py3-none-any.whl", hash = "sha256:8a27441015c3b2ef475f555861f7a2592f73bc60d247af9803a11b65fc605bf9", size = 68183, upload-time = "2024-11-23T22:49:38.289Z" },
]
[[package]]
name = "tabulate"
version = "0.9.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ec/fe/802052aecb21e3797b8f7902564ab6ea0d60ff8ca23952079064155d1ae1/tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c", size = 81090, upload-time = "2022-10-06T17:21:48.54Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252, upload-time = "2022-10-06T17:21:44.262Z" },
]
[[package]]
name = "tinycss2"
version = "1.4.0"