add iugu postback

This commit is contained in:
2026-01-12 23:30:52 -03:00
parent 29b8305713
commit ad23e9aa51
25 changed files with 191 additions and 29 deletions

View File

@@ -145,7 +145,7 @@ def dynamodb_persistence_layer(dynamodb_client):
@pytest.fixture()
def seeds(dynamodb_client):
def dynamodb_seeds(dynamodb_client):
from layercake.dynamodb import serialize
with open('tests/seeds.jsonl', 'rb') as fp:

View File

@@ -5,7 +5,7 @@ import events.send_reminder_emails as app
def test_reminder_access_period_before_30_days(
seeds,
dynamodb_seeds,
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
lambda_context: LambdaContext,
):

View File

@@ -4,7 +4,7 @@ import events.send_reminder_emails as app
def test_reminder_cert_expiration_before_30_days(
seeds,
dynamodb_seeds,
lambda_context: LambdaContext,
):
event = {

View File

@@ -4,7 +4,7 @@ import events.send_reminder_emails as app
def test_reminder_no_access_after_3_days(
seeds,
dynamodb_seeds,
lambda_context: LambdaContext,
):
event = {

View File

@@ -4,7 +4,7 @@ import events.send_reminder_emails as app
def test_reminder_no_activity_after_7_days(
seeds,
dynamodb_seeds,
lambda_context: LambdaContext,
):
event = {

View File

@@ -12,7 +12,7 @@ import events.reporting.append_cert as app
def test_append_cert(
seeds,
dynamodb_seeds,
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
lambda_context: LambdaContext,
):
@@ -65,7 +65,7 @@ def test_append_cert(
def test_report_exists(
seeds,
dynamodb_seeds,
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
lambda_context: LambdaContext,
):

View File

@@ -9,7 +9,7 @@ import events.reporting.send_report_email as app
def test_send_report_email(
monkeypatch,
seeds,
dynamodb_seeds,
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
lambda_context: LambdaContext,
):

View File

@@ -8,7 +8,7 @@ import events.stopgap.patch_course_metadata as app
def test_enroll(
seeds,
dynamodb_seeds,
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
lambda_context: LambdaContext,
):

View File

@@ -5,7 +5,7 @@ import events.stopgap.patch_konviva as app
def test_patch_konviva(
seeds,
dynamodb_seeds,
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
lambda_context: LambdaContext,
):

View File

@@ -5,7 +5,7 @@ import events.allocate_slots as app
def test_allocate_slots(
seeds,
dynamodb_seeds,
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
lambda_context: LambdaContext,
):

View File

@@ -5,7 +5,7 @@ import events.enroll as app
def test_enroll(
seeds,
dynamodb_seeds,
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
lambda_context: LambdaContext,
):

View File

@@ -5,7 +5,7 @@ import events.issue_cert as app
def test_issue_cert(
seeds,
dynamodb_seeds,
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
lambda_context: LambdaContext,
):
@@ -44,7 +44,7 @@ def test_issue_cert(
def test_non_exp_interval(
seeds,
dynamodb_seeds,
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
lambda_context: LambdaContext,
):

View File

@@ -5,7 +5,7 @@ import events.reenroll_if_failed as app
def test_reenroll_custom_dedup_window(
seeds,
dynamodb_seeds,
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
lambda_context: LambdaContext,
):

View File

@@ -8,7 +8,7 @@ import events.schedule_reminders as app
def test_schedule_reminders(
seeds,
dynamodb_seeds,
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
lambda_context: LambdaContext,
):

View File

@@ -9,7 +9,7 @@ import events.set_access_expired as app
def test_set_access_expired(
seeds,
dynamodb_seeds,
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
lambda_context: LambdaContext,
):

View File

@@ -9,7 +9,7 @@ import events.set_cert_expired as app
def test_set_cert_expired(
seeds,
dynamodb_seeds,
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
lambda_context: LambdaContext,
):
@@ -34,7 +34,7 @@ def test_set_cert_expired(
def test_existing_issued_cert(
seeds,
dynamodb_seeds,
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
lambda_context: LambdaContext,
):

View File

@@ -16,7 +16,7 @@ body = {
def test_postback(
app,
seeds,
dynamodb_seeds,
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
http_api_proxy: HttpApiProxy,
lambda_context: LambdaContext,

View File

@@ -576,7 +576,7 @@ wheels = [
[[package]]
name = "layercake"
version = "0.11.4"
version = "0.12.0"
source = { directory = "../layercake" }
dependencies = [
{ name = "arnparse" },

View File

@@ -1,5 +1,6 @@
from http import HTTPStatus
from typing import Any
from urllib.parse import parse_qsl
from aws_lambda_powertools import Logger, Tracer
from aws_lambda_powertools.event_handler.api_gateway import (
@@ -8,7 +9,8 @@ from aws_lambda_powertools.event_handler.api_gateway import (
)
from aws_lambda_powertools.logging import correlation_paths
from aws_lambda_powertools.utilities.typing import LambdaContext
from layercake.dynamodb import DynamoDBPersistenceLayer
from layercake.dateutils import now
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair
from boto3clients import dynamodb_client
from config import ORDER_TABLE
@@ -19,10 +21,35 @@ app = APIGatewayHttpResolver(enable_validation=True)
dyn = DynamoDBPersistenceLayer(ORDER_TABLE, dynamodb_client)
@app.post('/postback/<order_id>')
@app.post('/<order_id>/postback')
@tracer.capture_method
def postback(order_id: str):
return Response(status_code=HTTPStatus.NO_CONTENT)
decoded_body = dict(parse_qsl(app.current_event.decoded_body))
logger.info('IUGU Postback', decoded_body=decoded_body)
event = decoded_body['event']
status = decoded_body['data[status]'].upper()
if event != 'invoice.status_changed':
return Response(status_code=HTTPStatus.NO_CONTENT)
try:
dyn.update_item(
key=KeyPair(order_id, '0'),
update_expr='SET #status = :status, \
updated_at = :now',
cond_expr='attribute_exists(sk)',
expr_attr_names={
'#status': 'status',
},
expr_attr_values={
':status': status,
':now': now(),
},
)
except Exception:
return Response(status_code=HTTPStatus.NOT_FOUND)
else:
return Response(status_code=HTTPStatus.NO_CONTENT)
@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_HTTP)

View File

@@ -7,7 +7,8 @@ ENROLLMENT_TABLE: str = os.getenv('ENROLLMENT_TABLE') # type: ignore
IUGU_ACCOUNT_ID: str = 'AF01CF1B3451459F92666F10589278EE'
IUGU_API_TOKEN: str = os.getenv('IUGU_API_TOKEN') # type: ignore
IUGU_TEST_MODE: bool = os.getenv('AWS_LAMBDA_FUNCTION_NAME') is None
# IUGU_TEST_MODE: bool = os.getenv('AWS_LAMBDA_FUNCTION_NAME') is None
IUGU_TEST_MODE: bool = True
IUGU_POSTBACK_URL = 'https://zjg09ppxq8.execute-api.sa-east-1.amazonaws.com'
HTTP_CONNECT_TIMEOUT = int(os.environ.get('HTTP_CONNECT_TIMEOUT', 1))

View File

@@ -55,7 +55,7 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
items=r.get('items', []),
**new_image,
),
postback_url=f'{IUGU_POSTBACK_URL}/postback/{order_id}',
postback_url=f'{IUGU_POSTBACK_URL}/{order_id}/postback',
)
try:

View File

@@ -39,7 +39,8 @@ Globals:
ENROLLMENT_TABLE: !Ref EnrollmentTable
COURSE_TABLE: !Ref CourseTable
BUCKET_NAME: !Ref BucketName
IUGU_API_TOKEN: '{{resolve:ssm:/saladeaula/iugu_api_token}}'
# IUGU_API_TOKEN: '{{resolve:ssm:/saladeaula/iugu_api_token}}'
IUGU_API_TOKEN: 419BEF0AD0B4EC180AEF80281BBF3A1CBBCC0EC45C8AE200D8A53ACC994DE639
Resources:
EventLog:
@@ -64,7 +65,6 @@ Resources:
Type: AWS::Serverless::Function
Properties:
Handler: app.lambda_handler
Timeout: 12
LoggingConfig:
LogGroup: !Ref HttpLog
Policies:
@@ -74,7 +74,7 @@ Resources:
Post:
Type: HttpApi
Properties:
Path: /
Path: /{id}/postback
Method: POST
ApiId: !Ref HttpApi

View File

@@ -1,5 +1,9 @@
import base64
import json
import os
from dataclasses import dataclass
from http import HTTPMethod
from urllib.parse import urlencode
import jsonlines
import pytest
@@ -35,6 +39,87 @@ def lambda_context() -> LambdaContext:
return LambdaContext()
class HttpApiProxy:
def __call__(
self,
raw_path: str,
method: str = HTTPMethod.GET,
body: dict | str | None = None,
*,
headers: dict = {},
cookies: list[str] = [],
query_string_parameters: dict = {},
is_base64_encoded: bool = True,
**kwargs,
) -> dict:
if isinstance(body, dict):
body = json.dumps(body)
if is_base64_encoded and body:
body = _base64_encode(body)
return {
'version': '2.0',
'routeKey': '$default',
'rawPath': raw_path,
'rawQueryString': urlencode(query_string_parameters),
'cookies': cookies,
'headers': headers,
'queryStringParameters': query_string_parameters,
'requestContext': {
'accountId': '123456789012',
'apiId': 'api-id',
'authorizer': {
'jwt': {
'claims': {
'aud': '1db63660-063d-4280-b2ea-388aca4a9459',
'client_id': '1db63660-063d-4280-b2ea-388aca4a9459',
'email': 'sergio@somosbeta.com.br',
'email_verified': 'true',
'exp': '1765205975',
'iat': '1765202375',
'iss': 'https://id.saladeaula.digital',
'jti': 'Fbbyvwwze3npdEgs',
'name': 'Sérgio R Siqueira',
'scope': 'openid profile email offline_access apps:admin',
'sub': '5OxmMjL-ujoR5IMGegQz',
},
'scopes': None,
}
},
'domainName': 'id.execute-api.us-east-1.amazonaws.com',
'domainPrefix': 'id',
'http': {
'method': str(method),
'path': raw_path,
'protocol': 'HTTP/1.1',
'sourceIp': '192.168.0.1/32',
'userAgent': 'agent',
},
'requestId': 'id',
'routeKey': '$default',
'stage': '$default',
'time': '12/Mar/2020:19:03:58 +0000',
'timeEpoch': 1583348638390,
},
'body': body,
'pathParameters': {'parameter1': 'value1'},
'isBase64Encoded': is_base64_encoded,
'stageVariables': {'stageVariable1': 'value1', 'stageVariable2': 'value2'},
}
def _base64_encode(s: str) -> str | None:
if not s:
return None
return base64.b64encode(s.encode()).decode()
@pytest.fixture
def http_api_proxy():
return HttpApiProxy()
@pytest.fixture
def dynamodb_client():
from boto3clients import dynamodb_client as client
@@ -74,3 +159,10 @@ def dynamodb_seeds(dynamodb_persistence_layer):
for line in reader.iter(type=dict, skip_invalid=True):
dynamodb_persistence_layer.put_item(item=line)
@pytest.fixture
def app():
import app
return app

View File

@@ -15,6 +15,7 @@
{"id": "2849f1d5-f4f1-411e-8497-ec3a40afc0ab", "sk": "ADDRESS", "city": "São José", "postcode": "88101001", "state": "SC", "created_at": "2026-01-07T19:09:54.193859-03:00", "address1": "Avenida Presidente Kennedy" "address2": "", "neighborhood": "Campinas"}
// Seeds for Iugu
// file: tests/test_app.py
// file: tests/events/payments/test_create_invoice.py
{"id": "121c1140-779d-4664-8d99-4a006a22f547", "sk": "0", "total": "267.3", "name": "Beta Educação", "payment_method": "BANK_SLIP", "create_date": "2026-01-07T19:07:49.272967-03:00", "due_date": "2026-01-12T00:35:44.897447-03:00", "coupon": "10OFF", "discount": "-29.7", "updated_at": "2026-01-07T19:07:51.512605-03:00", "tenant_id": "cJtK9SsnJhKPyxESe7g3DG", "email": "org+15608435000190@users.noreply.saladeaula.digital", "org_id": "cJtK9SsnJhKPyxESe7g3DG", "cnpj": "15608435000190", "status": "PENDING", "subtotal": 297}
{"id": "121c1140-779d-4664-8d99-4a006a22f547", "sk": "ADDRESS", "city": "São José", "neighborhood": "Campinas", "address2": "", "postcode": "88101001", "state": "SC", "address1": "Avenida Presidente Kennedy", "created_at": "2026-01-07T19:07:49.272967-03:00"}

View File

@@ -0,0 +1,41 @@
from http import HTTPMethod, HTTPStatus
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair
from .conftest import HttpApiProxy, LambdaContext
order_id = '121c1140-779d-4664-8d99-4a006a22f547'
def test_postback(
app,
dynamodb_seeds,
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
http_api_proxy: HttpApiProxy,
lambda_context: LambdaContext,
):
# This data was added from seeds
r = app.lambda_handler(
http_api_proxy(
raw_path=f'/{order_id}/postback',
method=HTTPMethod.POST,
body=(
'event=invoice.status_changed&'
'data[id]=RDBBACE5DE174554BA2C836E96D751AA&'
'data[status]=paid&'
'data[account_id]=AF01CF1B3451459F92666F10589278EE&'
'data[payment_method]=iugu_credit_card&'
'data[paid_at]=2022-10-17T18:21:55-03:00&'
'data[paid_cents]=10255&'
'data[order_id]=cPqkdJUeqqCB6WATsSWnsZ'
),
is_base64_encoded=True,
headers={'content-type': 'application/x-www-form-urlencoded'},
),
lambda_context,
)
assert r['statusCode'] == HTTPStatus.NO_CONTENT
order = dynamodb_persistence_layer.get_item(KeyPair(order_id, '0'))
assert order['status'] == 'PAID'