diff --git a/enrollments-events/tests/conftest.py b/enrollments-events/tests/conftest.py index aad09fe..1823837 100644 --- a/enrollments-events/tests/conftest.py +++ b/enrollments-events/tests/conftest.py @@ -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: diff --git a/enrollments-events/tests/events/emails/test_reminder_access_period_before_30_days.py b/enrollments-events/tests/events/emails/test_reminder_access_period_before_30_days.py index dcab221..d397c08 100644 --- a/enrollments-events/tests/events/emails/test_reminder_access_period_before_30_days.py +++ b/enrollments-events/tests/events/emails/test_reminder_access_period_before_30_days.py @@ -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, ): diff --git a/enrollments-events/tests/events/emails/test_reminder_cert_expiration_before_30_days.py b/enrollments-events/tests/events/emails/test_reminder_cert_expiration_before_30_days.py index 2b1c725..07d5489 100644 --- a/enrollments-events/tests/events/emails/test_reminder_cert_expiration_before_30_days.py +++ b/enrollments-events/tests/events/emails/test_reminder_cert_expiration_before_30_days.py @@ -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 = { diff --git a/enrollments-events/tests/events/emails/test_reminder_no_access_after_3_days.py b/enrollments-events/tests/events/emails/test_reminder_no_access_after_3_days.py index 422ae16..dbbb39f 100644 --- a/enrollments-events/tests/events/emails/test_reminder_no_access_after_3_days.py +++ b/enrollments-events/tests/events/emails/test_reminder_no_access_after_3_days.py @@ -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 = { diff --git a/enrollments-events/tests/events/emails/test_reminder_no_activity_after_7_days.py b/enrollments-events/tests/events/emails/test_reminder_no_activity_after_7_days.py index 45567c6..15237da 100644 --- a/enrollments-events/tests/events/emails/test_reminder_no_activity_after_7_days.py +++ b/enrollments-events/tests/events/emails/test_reminder_no_activity_after_7_days.py @@ -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 = { diff --git a/enrollments-events/tests/events/reporting/test_append_cert.py b/enrollments-events/tests/events/reporting/test_append_cert.py index da1dd42..a9a2801 100644 --- a/enrollments-events/tests/events/reporting/test_append_cert.py +++ b/enrollments-events/tests/events/reporting/test_append_cert.py @@ -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, ): diff --git a/enrollments-events/tests/events/reporting/test_send_report_email.py b/enrollments-events/tests/events/reporting/test_send_report_email.py index 27cc217..96d443c 100644 --- a/enrollments-events/tests/events/reporting/test_send_report_email.py +++ b/enrollments-events/tests/events/reporting/test_send_report_email.py @@ -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, ): diff --git a/enrollments-events/tests/events/stopgap/test_patch_course_metadata.py b/enrollments-events/tests/events/stopgap/test_patch_course_metadata.py index df208c7..a0724eb 100644 --- a/enrollments-events/tests/events/stopgap/test_patch_course_metadata.py +++ b/enrollments-events/tests/events/stopgap/test_patch_course_metadata.py @@ -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, ): diff --git a/enrollments-events/tests/events/stopgap/test_patch_konviva.py b/enrollments-events/tests/events/stopgap/test_patch_konviva.py index 1c07a0b..aa0ec2e 100644 --- a/enrollments-events/tests/events/stopgap/test_patch_konviva.py +++ b/enrollments-events/tests/events/stopgap/test_patch_konviva.py @@ -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, ): diff --git a/enrollments-events/tests/events/test_allocate_slots.py b/enrollments-events/tests/events/test_allocate_slots.py index 55edea7..d06d021 100644 --- a/enrollments-events/tests/events/test_allocate_slots.py +++ b/enrollments-events/tests/events/test_allocate_slots.py @@ -5,7 +5,7 @@ import events.allocate_slots as app def test_allocate_slots( - seeds, + dynamodb_seeds, dynamodb_persistence_layer: DynamoDBPersistenceLayer, lambda_context: LambdaContext, ): diff --git a/enrollments-events/tests/events/test_enroll.py b/enrollments-events/tests/events/test_enroll.py index 09313e9..a1ef4eb 100644 --- a/enrollments-events/tests/events/test_enroll.py +++ b/enrollments-events/tests/events/test_enroll.py @@ -5,7 +5,7 @@ import events.enroll as app def test_enroll( - seeds, + dynamodb_seeds, dynamodb_persistence_layer: DynamoDBPersistenceLayer, lambda_context: LambdaContext, ): diff --git a/enrollments-events/tests/events/test_issue_cert.py b/enrollments-events/tests/events/test_issue_cert.py index 734dac8..35d18f6 100644 --- a/enrollments-events/tests/events/test_issue_cert.py +++ b/enrollments-events/tests/events/test_issue_cert.py @@ -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, ): diff --git a/enrollments-events/tests/events/test_reenroll_if_failed.py b/enrollments-events/tests/events/test_reenroll_if_failed.py index ae061a2..719d6de 100644 --- a/enrollments-events/tests/events/test_reenroll_if_failed.py +++ b/enrollments-events/tests/events/test_reenroll_if_failed.py @@ -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, ): diff --git a/enrollments-events/tests/events/test_schedule_reminders.py b/enrollments-events/tests/events/test_schedule_reminders.py index 72de0f5..6e24e3d 100644 --- a/enrollments-events/tests/events/test_schedule_reminders.py +++ b/enrollments-events/tests/events/test_schedule_reminders.py @@ -8,7 +8,7 @@ import events.schedule_reminders as app def test_schedule_reminders( - seeds, + dynamodb_seeds, dynamodb_persistence_layer: DynamoDBPersistenceLayer, lambda_context: LambdaContext, ): diff --git a/enrollments-events/tests/events/test_set_access_expired.py b/enrollments-events/tests/events/test_set_access_expired.py index 1b56c25..65cb812 100644 --- a/enrollments-events/tests/events/test_set_access_expired.py +++ b/enrollments-events/tests/events/test_set_access_expired.py @@ -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, ): diff --git a/enrollments-events/tests/events/test_set_cert_expired.py b/enrollments-events/tests/events/test_set_cert_expired.py index aad27a4..24575f3 100644 --- a/enrollments-events/tests/events/test_set_cert_expired.py +++ b/enrollments-events/tests/events/test_set_cert_expired.py @@ -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, ): diff --git a/enrollments-events/tests/test_app.py b/enrollments-events/tests/test_app.py index 0afb12d..fb3abfa 100644 --- a/enrollments-events/tests/test_app.py +++ b/enrollments-events/tests/test_app.py @@ -16,7 +16,7 @@ body = { def test_postback( app, - seeds, + dynamodb_seeds, dynamodb_persistence_layer: DynamoDBPersistenceLayer, http_api_proxy: HttpApiProxy, lambda_context: LambdaContext, diff --git a/enrollments-events/uv.lock b/enrollments-events/uv.lock index 176a669..dce941a 100644 --- a/enrollments-events/uv.lock +++ b/enrollments-events/uv.lock @@ -576,7 +576,7 @@ wheels = [ [[package]] name = "layercake" -version = "0.11.4" +version = "0.12.0" source = { directory = "../layercake" } dependencies = [ { name = "arnparse" }, diff --git a/orders-events/app/app.py b/orders-events/app/app.py index 97478b1..d3ddc75 100644 --- a/orders-events/app/app.py +++ b/orders-events/app/app.py @@ -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/') +@app.post('//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) diff --git a/orders-events/app/config.py b/orders-events/app/config.py index e4ac2f8..7cbc12c 100644 --- a/orders-events/app/config.py +++ b/orders-events/app/config.py @@ -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)) diff --git a/orders-events/app/events/payments/create_invoice.py b/orders-events/app/events/payments/create_invoice.py index a7a047b..bf0ea5a 100644 --- a/orders-events/app/events/payments/create_invoice.py +++ b/orders-events/app/events/payments/create_invoice.py @@ -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: diff --git a/orders-events/template.yaml b/orders-events/template.yaml index 45d4a58..52f7ae4 100644 --- a/orders-events/template.yaml +++ b/orders-events/template.yaml @@ -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 diff --git a/orders-events/tests/conftest.py b/orders-events/tests/conftest.py index c3b02ec..0d3c87d 100644 --- a/orders-events/tests/conftest.py +++ b/orders-events/tests/conftest.py @@ -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 diff --git a/orders-events/tests/seeds.jsonl b/orders-events/tests/seeds.jsonl index 7912ba0..d88daa0 100644 --- a/orders-events/tests/seeds.jsonl +++ b/orders-events/tests/seeds.jsonl @@ -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"} diff --git a/orders-events/tests/test_app.py b/orders-events/tests/test_app.py new file mode 100644 index 0000000..3cbcf6a --- /dev/null +++ b/orders-events/tests/test_app.py @@ -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'