close bolling

This commit is contained in:
2025-07-27 20:57:41 -03:00
parent 68a187220c
commit cc28e16c29
11 changed files with 365 additions and 21 deletions

View File

@@ -22,6 +22,7 @@ enrollment_layer = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client)
@logger.inject_lambda_context
def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
new_image = event.detail['new_image']
now_ = now()
data = user_layer.get_item(
# Post-migration: uncomment the following line
# KeyPair(new_image['org_id'], 'METADATA#BILLING_TERMS'),
@@ -32,10 +33,21 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
return False
try:
enrollment_layer.put_item(
with enrollment_layer.transact_writer() as transact:
transact.update(
key=KeyPair(new_image['id'], '0'),
update_expr='SET subscription_covered = :subscription_covered, \
updated_at = :updated_at',
expr_attr_values={
':subscription_covered': True,
':updated_at': now_,
},
cond_expr='attribute_exists(sk)',
)
transact.put(
item={
'id': new_image['id'],
'sk': 'METADATA#BILLING_TERMS',
'sk': 'METADATA#SUBSCRIPTION_COVERED',
'org_id': new_image['tenant_id'],
'billing_day': data['billing_day'],
'created_at': now(),

View File

@@ -42,10 +42,10 @@ Resources:
Properties:
RetentionInDays: 90
EventSetTermsIfSubscribedFunction:
EventSetSubscriptionCoveredFunction:
Type: AWS::Serverless::Function
Properties:
Handler: events.stopgap.set_terms_if_subscribed.lambda_handler
Handler: events.stopgap.set_subscription_covered.lambda_handler
LoggingConfig:
LogGroup: !Ref EventLog
Policies:

View File

@@ -6,7 +6,7 @@ from aws_lambda_powertools.utilities.data_classes import (
event_source,
)
from aws_lambda_powertools.utilities.typing import LambdaContext
from layercake.dateutils import now, ttl
from layercake.dateutils import fromisoformat, now, ttl
from layercake.dynamodb import (
DynamoDBPersistenceLayer,
KeyPair,
@@ -35,12 +35,17 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
data = enrollment_layer.collection.get_items(
TransactKey(enrollment_id) + SortKey('0') + SortKey('author')
)
created_at: datetime = fromisoformat(data['create_date']) # type: ignore
if not data:
logger.debug('Enrollment not found')
return False
start_date, end_date = get_billing_period(new_image['billing_day'])
start_date, end_date = get_billing_period(
new_image['billing_day'],
year=created_at.year,
month=created_at.month,
)
pk = 'BILLING#ORG#{org_id}'.format(org_id=org_id)
sk = 'START#{start}#END#{end}'.format(
start=start_date.isoformat(),

View File

@@ -0,0 +1,68 @@
import locale
import os
from datetime import date
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 jinja2 import Environment, FileSystemLoader
from layercake.dateutils import fromisoformat
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair
from weasyprint import HTML
from boto3clients import dynamodb_client
from config import ORDER_TABLE
logger = Logger(__name__)
order_layer = DynamoDBPersistenceLayer(ORDER_TABLE, dynamodb_client)
curdir = os.path.dirname(__file__)
env = Environment(loader=FileSystemLoader(curdir))
locale.setlocale(locale.LC_ALL, 'pt_BR.UTF-8')
def currency(value: float | int) -> str:
return locale.currency(value, grouping=True)
def datetime_format(dt: date, fmt='%H:%M %d-%m-%y'):
if isinstance(dt, str):
dt = fromisoformat(dt) # type: ignore
return dt.strftime(fmt)
env.filters['datetime_format'] = datetime_format
env.filters['currency'] = currency
@event_source(data_class=EventBridgeEvent)
@logger.inject_lambda_context
def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
new_image = event.detail['new_image']
_, start_date, _, end_date, *_ = new_image['sk'].split('#')
result = order_layer.collection.query(
KeyPair(
pk=new_image['id'],
sk=f'START#{start_date}#END#{end_date}#ENROLLMENT',
),
)
template = env.get_template('tmpl.html')
html_rendered = template.render(
start_date=start_date,
end_date=end_date,
items=result['items'],
)
HTML(string=html_rendered, base_url='').write_pdf('cert.pdf')
return order_layer.update_item(
key=KeyPair(new_image['id'], new_image['sk']),
update_expr='SET #status = :status',
expr_attr_names={'#status': 'status'},
expr_attr_values={':status': 'CLOSED'},
)

Binary file not shown.

View File

@@ -0,0 +1,200 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
<meta name="author" content="EDUSEG® <https://eduseg.com.br>" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
@page {
size: A4 portrait;
margin: 0.458cm;
}
@font-face {
font-family: "SF-Pro";
src: url("fonts/SF-Pro.ttf") format("truetype");
}
html,
body,
div,
h1,
h2,
ul,
p,
a {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
html {
font-family: SF-Pro;
font-size: 11pt;
line-height: 1.4;
margin: 0;
padding: 0;
box-sizing: border-box;
}
section {
width: 100%;
break-after: page;
box-sizing: border-box;
}
strong {
font-weight: bold;
}
table {
table-layout: auto;
width: 100%;
border-spacing: 0;
border-radius: 0.5rem;
border: 1px solid #efefef;
overflow: visible;
}
td {
padding: 0.625rem;
}
tbody > tr > td {
border-top: 1px solid #efefef;
}
thead {
background-color: #f3f4f680;
font-weight: 600;
display: table-header-group;
}
tr {
page-break-inside: avoid;
}
.space-y-0\.5 > :not(:last-child) {
margin-bottom: 0.125rem;
}
.space-y-2\.5 > :not(:last-child) {
margin-bottom: 0.625rem;
}
</style>
</head>
<body>
<section class="space-y-2.5">
<?xml version="1.0" encoding="UTF-8"?>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 1143.4 257.88"
style="width: 10rem"
>
<defs>
<style>
.cls-1 {
fill: #8cd366;
}
.cls-2 {
fill: #2e3524;
}
</style>
</defs>
<g>
<g>
<path
class="cls-1"
d="M119.06,170.25l-53.68-24.18c-1.47-.94-3.35-.94-4.82,0l-53.68,24.18c-2.98,1.9-6.89-.24-6.89-3.77V7.01C0,4.54,2.01,2.54,4.48,2.54h117c2.47,0,4.48,2.01,4.48,4.48v159.46c0,3.54-3.91,5.68-6.89,3.77Z"
/>
<g>
<path
class="cls-2"
d="M73.52,57.89H20.82v15.77h52.7v-15.77Z"
/>
<path
class="cls-2"
d="M84.06,86.96H20.82v20.97h63.24v-20.97Z"
/>
<path
class="cls-2"
d="M84.06,23.67H20.82v20.97h63.24v-20.97Z"
/>
<path
class="cls-2"
d="M105.14,102.66c0-2.91-2.36-5.27-5.27-5.27s-5.27,2.36-5.27,5.27,2.36,5.27,5.27,5.27,5.27-2.36,5.27-5.27Z"
/>
</g>
</g>
<g>
<path
class="cls-2"
d="M191.46,2.54h72.24v34.76h-34.54v69.52h30.81v31h-30.81v82.76h34.54v34.76h-72.24V2.54Z"
/>
<path
class="cls-2"
d="M283.8,2.54h45.22c8.38,0,16.01,1.84,22.92,5.52,6.91,3.68,12.35,8.75,16.33,15.17,3.98,6.42,5.97,13.65,5.97,21.65v168.12c0,8-1.94,15.23-5.8,21.65-3.88,6.42-9.26,11.49-16.18,15.17-6.91,3.68-14.65,5.54-23.23,5.54h-45.22V2.54ZM334.52,222.11c1.36-1.46,2.03-3.27,2.03-5.37V41.12c-.2-2.1-1.04-3.89-2.52-5.37-1.47-1.46-3.25-2.22-5.34-2.22h-10.96v190.79h11.27c2.29,0,4.12-.73,5.49-2.22h.02Z"
/>
<path
class="cls-2"
d="M415.84,252.32c-6.81-3.68-12.2-8.73-16.18-15.17-3.97-6.42-5.97-13.63-5.97-21.65V2.54h37.68v218.61c0,2.1.73,3.91,2.2,5.37,1.46,1.48,3.25,2.22,5.34,2.22,2.29,0,4.12-.73,5.49-2.22,1.36-1.46,2.03-3.27,2.03-5.37V2.54h37.68v212.98c0,8.02-1.94,15.23-5.8,21.65-3.88,6.42-9.26,11.49-16.18,15.17-6.92,3.68-14.65,5.54-23.23,5.54s-16.27-1.84-23.08-5.54l.04-.02Z"
/>
<path
class="cls-2"
d="M526.39,252.32c-6.81-3.68-12.2-8.73-16.18-15.17-3.97-6.42-5.97-13.63-5.97-21.65v-61.61h37.68v67.24c0,2.1.73,3.91,2.2,5.37,1.45,1.48,3.25,2.21,5.34,2.21,2.29,0,4.12-.73,5.49-2.21,1.36-1.47,2.03-3.27,2.03-5.37v-60.94c0-4.43-1.73-8.06-5.19-10.91-3.45-2.84-9.05-6.27-16.8-10.27-6.49-3.36-11.78-6.37-15.86-9.01-4.09-2.63-7.6-6.16-10.52-10.59-2.93-4.43-4.4-9.58-4.4-15.49v-61.57c0-8,1.98-15.21,5.97-21.65,3.98-6.42,9.37-11.47,16.18-15.17,6.79-3.68,14.5-5.52,23.08-5.52s16.33,1.84,23.23,5.52c6.91,3.68,12.3,8.75,16.18,15.17,3.88,6.42,5.8,13.65,5.8,21.65v55.62h-37.68v-61.25c0-2.1-.69-3.89-2.03-5.37-1.36-1.46-3.19-2.22-5.49-2.22-2.09,0-3.88.73-5.34,2.22-1.48,1.48-2.2,3.27-2.2,5.37v54.61c0,4.84,1.83,8.81,5.51,11.85,3.66,3.06,9.57,6.91,17.75,11.53,6.29,3.38,11.4,6.33,15.4,8.84,3.97,2.53,7.33,5.84,10.04,9.95,2.72,4.11,4.09,8.81,4.09,14.06v67.94c0,8.02-1.94,15.23-5.8,21.65-3.88,6.42-9.26,11.49-16.18,15.17-6.92,3.68-14.65,5.54-23.23,5.54s-16.27-1.84-23.08-5.54Z"
/>
<path
class="cls-2"
d="M613.84,2.54h72.24v34.76h-34.54v69.52h30.81v31h-30.81v82.76h34.54v34.76h-72.24V2.54Z"
/>
<path
class="cls-2"
d="M720.31,252.32c-4.4-3.27-7.91-7.9-10.52-13.9-2.61-6.01-3.92-12.9-3.92-20.69V42.03c0-7.79,1.98-14.91,5.97-21.33,3.98-6.42,9.43-11.47,16.33-15.17,6.91-3.68,14.56-5.52,22.94-5.52s16.07,1.84,23.08,5.52c7.02,3.68,12.45,8.75,16.33,15.17,3.88,6.42,5.8,13.54,5.8,21.33v73.95h-37.68V36.71c0-2.1-.69-3.89-2.03-5.37-1.36-1.46-3.19-2.22-5.49-2.22-2.09,0-3.88.73-5.34,2.22-1.48,1.48-2.2,3.27-2.2,5.37v181.02c0,2.1.73,3.91,2.2,5.37,1.45,1.48,3.25,2.22,5.34,2.22,2.3,0,4.13-.73,5.49-2.22,1.36-1.47,2.03-3.27,2.03-5.37v-36.02h-9.11v-40.13h46.78v113.76h-37.68v-11.06c-2.31,4.23-5.34,7.42-9.11,9.63-3.77,2.22-8.47,3.32-14.13,3.32s-10.67-1.63-15.08-4.9l-.02-.02Z"
/>
<path
class="cls-2"
d="M824.08,19.6h-4.8l-.04-2.89h4.29c.65-.01,1.26-.12,1.82-.31.56-.21,1.01-.5,1.36-.89.34-.4.51-.89.51-1.47,0-.73-.13-1.3-.38-1.73-.24-.43-.65-.73-1.24-.91-.58-.19-1.36-.29-2.35-.29h-2.96v15.98h-3.22V8.24h6.18c1.47,0,2.73.21,3.8.64,1.08.41,1.91,1.05,2.49,1.91.59.84.89,1.9.89,3.18,0,.8-.19,1.51-.56,2.13-.37.62-.92,1.16-1.64,1.62-.71.44-1.59.81-2.62,1.09-.04,0-.1.05-.16.16-.04.1-.09.16-.13.16-.25.15-.42.26-.49.33-.06.06-.13.1-.2.11-.06.01-.24.02-.53.02ZM823.86,19.6l.47-2.2c2.31,0,3.89.5,4.73,1.51.85.99,1.27,2.26,1.27,3.8v1.2c0,.55.02,1.07.07,1.58.06.49.16.9.31,1.24v.36h-3.31c-.15-.39-.24-.93-.27-1.64-.02-.71-.02-1.23-.02-1.56v-1.16c0-1.08-.24-1.87-.73-2.38-.49-.5-1.33-.76-2.51-.76ZM810.38,17.93c0,1.97.33,3.81,1,5.51.68,1.69,1.63,3.17,2.84,4.44,1.22,1.26,2.61,2.24,4.2,2.96,1.6.7,3.3,1.04,5.11,1.04s3.53-.35,5.11-1.04c1.59-.71,2.98-1.7,4.18-2.96,1.2-1.27,2.14-2.76,2.82-4.44.68-1.7,1.02-3.54,1.02-5.51s-.34-3.8-1.02-5.49c-.68-1.69-1.62-3.16-2.82-4.42-1.2-1.26-2.59-2.24-4.18-2.93-1.59-.71-3.29-1.07-5.11-1.07s-3.51.36-5.11,1.07c-1.59.7-2.98,1.67-4.2,2.93-1.22,1.26-2.16,2.73-2.84,4.42-.67,1.69-1,3.52-1,5.49ZM807.75,17.93c0-2.36.41-4.54,1.22-6.55.81-2.01,1.95-3.77,3.4-5.27,1.45-1.51,3.13-2.68,5.02-3.51,1.91-.84,3.96-1.27,6.13-1.27s4.21.42,6.11,1.27c1.91.83,3.59,2,5.02,3.51,1.45,1.5,2.59,3.25,3.4,5.27.83,2.01,1.25,4.2,1.25,6.55s-.42,4.54-1.25,6.55c-.81,2.01-1.95,3.78-3.4,5.31-1.44,1.51-3.11,2.69-5.02,3.53-1.9.84-3.93,1.27-6.11,1.27s-4.22-.42-6.13-1.27c-1.9-.84-3.57-2.02-5.02-3.53-1.45-1.53-2.58-3.3-3.4-5.31-.82-2.01-1.22-4.2-1.22-6.55Z"
/>
</g>
</g>
</svg>
<p>
Matrículas realizadas entre
<strong>{{ start_date|datetime_format('%d/%m/%Y') }}</strong> e
<strong>{{ end_date|datetime_format('%d/%m/%Y') }}</strong>
</p>
<table class="table-layout border border-gray-300">
<thead>
<tr>
<td>Curso</td>
<td>Colaborador</td>
<td>Matrículado em</td>
<td>Valor unit.</td>
<td>Autor</td>
</tr>
</thead>
<tbody>
{% for x in items %}
<tr>
<td>{{ x.course.name }}</td>
<td>{{ x.user.name }}</td>
<td>
{{ x.enrolled_at|datetime_format('%d/%m/%Y, %H:%M')
}}
</td>
<td>{{ x.unit_price|currency }}</td>
<td>{{ x.author.name }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</section>
</body>
</html>

View File

@@ -64,7 +64,28 @@ Resources:
detail-type: [INSERT]
detail:
new_image:
sk: ["METADATA#BILLING_TERMS"]
sk: ["METADATA#SUBSCRIPTION_COVERED"]
EventBillingCloseWindowFunction:
Type: AWS::Serverless::Function
Properties:
Handler: events.billing.close_window.lambda_handler
LoggingConfig:
LogGroup: !Ref EventLog
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref OrderTable
Events:
Event:
Type: EventBridgeRule
Properties:
Pattern:
resources: [!Ref OrderTable]
detail-type: [EXPIRE]
detail:
new_image:
sk:
- suffix: SCHEDULE#AUTO_CLOSE
EventAppendOrgIdFunction:
Type: AWS::Serverless::Function
@@ -128,7 +149,7 @@ Resources:
Policies:
- DynamoDBWritePolicy:
TableName: !Ref OrderTable
- DynamoDBWritePolicy:
- DynamoDBCrudPolicy:
TableName: !Ref EnrollmentTable
Events:
Event:

View File

@@ -1,10 +1,10 @@
from aws_lambda_powertools.utilities.typing import LambdaContext
from layercake.dynamodb import DynamoDBPersistenceLayer, PartitionKey
import events.reporting.add_item as app
import events.billing.append_enrollment as app
def test_add_item(
def test_append_enrollment(
dynamodb_seeds,
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
lambda_context: LambdaContext,
@@ -13,7 +13,7 @@ def test_add_item(
'detail': {
'new_image': {
'id': '945e8672-1d72-45c6-b76c-ac06aa8b52ab',
'sk': 'METADATA#BILLING_TERMS',
'sk': 'METADATA#SUBSCRIPTION_COVERED',
'billing_day': 5,
'created_at': '2025-07-23T18:09:22.785678-03:00',
'org_id': 'edp8njvgQuzNkLx2ySNfAD',
@@ -26,5 +26,11 @@ def test_add_item(
r = dynamodb_persistence_layer.collection.query(
PartitionKey('BILLING#ORG#edp8njvgQuzNkLx2ySNfAD')
)
items = r['items']
print(r)
assert items[0]['sk'] == 'START#2025-06-05#END#2025-07-04#SCHEDULE#AUTO_CLOSE'
assert (
items[1]['sk']
== 'START#2025-06-05#END#2025-07-04#ENROLLMENT#945e8672-1d72-45c6-b76c-ac06aa8b52ab'
)
assert items[2]['sk'] == 'START#2025-06-05#END#2025-07-04'

View File

@@ -0,0 +1,29 @@
from aws_lambda_powertools.utilities.typing import LambdaContext
from layercake.dynamodb import DynamoDBPersistenceLayer, PartitionKey
import events.billing.close_window as app
def test_append_enrollment(
dynamodb_seeds,
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
lambda_context: LambdaContext,
):
event = {
'detail': {
'new_image': {
'id': 'BILLING#ORG#BES6dmWgTMXRYmfDyYYXUF',
'sk': 'START#2025-07-01#END#2025-07-31#SCHEDULE#AUTO_CLOSE',
'created_at': '2025-07-24T15:20:52.464244-03:00',
'ttl': 1754017200,
}
}
}
assert app.lambda_handler(event, lambda_context) # type: ignore
# r = dynamodb_persistence_layer.collection.query(
# PartitionKey('BILLING#ORG#edp8njvgQuzNkLx2ySNfAD')
# )
# print(r)

View File

@@ -15,3 +15,6 @@
{"id": {"S": "945e8672-1d72-45c6-b76c-ac06aa8b52ab"}, "sk": {"S": "author"}, "name": {"S": "Carolina Brand"}, "user_id": {"S": "SMEXYk5MQkKCzknJpxqr8n"}}
{"id": {"S": "CUSTOM_PRICING#ORG#edp8njvgQuzNkLx2ySNfAD"},"sk": {"S": "COURSE#123"},"created_at": {"S": "2025-07-24T16:10:09.304073-03:00"},"unit_price": {"N": "79.2"}}
{"id": {"S": "123"},"sk": {"S": "0"},"access_period": {"N": "360"},"cert": {"M": {"exp_interval": {"N": "360"}}},"created_at": {"S": "2024-12-30T00:33:33.088916-03:00"},"metadata__konviva_class_id": {"N": "194"},"metadata__unit_price": {"N": "99"},"name": {"S": "Direção Defensiva (08 horas)"},"tenant_id": {"S": "*"},"updated_at": {"S": "2025-07-24T00:00:24.639003-03:00"}}
{"id": {"S": "BILLING#ORG#BES6dmWgTMXRYmfDyYYXUF"},"sk": {"S": "START#2025-07-01#END#2025-07-31"},"created_at": {"S": "2025-07-24T15:20:52.464244-03:00"},"status": {"S": "PENDING"}}
{"id": {"S": "BILLING#ORG#BES6dmWgTMXRYmfDyYYXUF"},"sk": {"S": "START#2025-07-01#END#2025-07-31#ENROLLMENT#a08c94a2-7ee4-45fd-bfe7-73568c738b8b"},"author": {"M": {"id": {"S": "SMEXYk5MQkKCzknJpxqr8n"},"name": {"S": "Carolina Brand"}}},"course": {"M": {"id": {"S": "7f7905aa-ec6d-4189-b884-50fa9b1bd0b8"},"name": {"S": "NR-10 Reciclagem: 08 horas"}}},"created_at": {"S": "2025-07-24T16:38:33.095216-03:00"},"enrolled_at": {"S": "2025-07-24T11:26:56.975207-03:00"},"unit_price": {"N": "169"},"user": {"M": {"id": {"S": "iPWidwn4HsYtikiZD33smV"},"name": {"S": "William da Silva Nascimento"}}}}
{"id": {"S": "BILLING#ORG#BES6dmWgTMXRYmfDyYYXUF"},"sk": {"S": "START#2025-07-01#END#2025-07-31#ENROLLMENT#ac09e8da-6cb2-4e31-84e7-238df2647a7a"},"author": {"M": {"id": {"S": "SMEXYk5MQkKCzknJpxqr8n"},"name": {"S": "Carolina Brand"}}},"course": {"M": {"id": {"S": "7f7905aa-ec6d-4189-b884-50fa9b1bd0b8"},"name": {"S": "NR-10 Reciclagem: 08 horas"}}},"created_at": {"S": "2025-07-24T16:38:58.694031-03:00"},"enrolled_at": {"S": "2025-07-24T11:26:56.913746-03:00"},"unit_price": {"N": "169"},"user": {"M": {"id": {"S": "ca8c9fca-b508-4842-8a48-fd5cc5632ac0"},"name": {"S": "Geovane Soares De Lima"}}}}