From 823134f4503918ac0b76d9cbf77bda8dcfeaacaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Rafael=20Siqueira?= Date: Fri, 9 Jan 2026 11:20:56 -0300 Subject: [PATCH] wip checkout --- api.saladeaula.digital/app/app.py | 2 +- api.saladeaula.digital/app/config.py | 1 - .../app/routes/orders/__init__.py | 18 ++- .../app/routes/orders/checkout.py | 57 +++++++- .../app/routes/orgs/__init__.py | 4 +- .../app/routes/orgs/admins.py | 2 +- .../app/routes/orgs/custom_pricing.py | 2 +- .../{submission.py => submissions.py} | 2 +- .../app/routes/orgs/seats.py | 16 +++ .../tests/routes/orders/test_checkout.py | 33 +++++ .../app/events/reenroll_if_failed.py | 3 +- enrollments-events/template.yaml | 25 ++++ orders-events/app/config.py | 3 + orders-events/app/events/append_org_id.py | 17 +-- orders-events/app/iugu.py | 135 +++++++++++------- orders-events/template.yaml | 42 ++++++ orders-events/tests/seeds.jsonl | 6 +- orders-events/uv.lock | 2 +- 18 files changed, 290 insertions(+), 80 deletions(-) rename api.saladeaula.digital/app/routes/orgs/enrollments/{submission.py => submissions.py} (93%) create mode 100644 api.saladeaula.digital/app/routes/orgs/seats.py diff --git a/api.saladeaula.digital/app/app.py b/api.saladeaula.digital/app/app.py index 5a3948a..63922f2 100644 --- a/api.saladeaula.digital/app/app.py +++ b/api.saladeaula.digital/app/app.py @@ -55,7 +55,7 @@ app.include_router(orgs.admins, prefix='/orgs') app.include_router(orgs.billing, prefix='/orgs') app.include_router(orgs.custom_pricing, prefix='/orgs') app.include_router(orgs.scheduled, prefix='/orgs') -app.include_router(orgs.submission, prefix='/orgs') +app.include_router(orgs.submissions, prefix='/orgs') app.include_router(orgs.users, prefix='/orgs') app.include_router(orgs.batch_jobs, prefix='/orgs') diff --git a/api.saladeaula.digital/app/config.py b/api.saladeaula.digital/app/config.py index c99d4ca..bc68c30 100644 --- a/api.saladeaula.digital/app/config.py +++ b/api.saladeaula.digital/app/config.py @@ -12,5 +12,4 @@ BUCKET_NAME: str = os.getenv('BUCKET_NAME') # type: ignore DEDUP_WINDOW_OFFSET_DAYS = 90 PAPERFORGE_API = 'https://paperforge.saladeaula.digital' - INTERNAL_EMAIL_DOMAIN = 'users.noreply.saladeaula.digital' diff --git a/api.saladeaula.digital/app/routes/orders/__init__.py b/api.saladeaula.digital/app/routes/orders/__init__.py index 5d0c698..77434d5 100644 --- a/api.saladeaula.digital/app/routes/orders/__init__.py +++ b/api.saladeaula.digital/app/routes/orders/__init__.py @@ -1,8 +1,9 @@ from aws_lambda_powertools.event_handler.api_gateway import Router -from aws_lambda_powertools.event_handler.exceptions import ( - NotFoundError, +from layercake.dynamodb import ( + DynamoDBPersistenceLayer, + SortKey, + TransactKey, ) -from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair from boto3clients import dynamodb_client from config import ORDER_TABLE @@ -17,7 +18,12 @@ dyn = DynamoDBPersistenceLayer(ORDER_TABLE, dynamodb_client) @router.get('/') def get_order(order_id: str): - return dyn.collection.get_item( - KeyPair(order_id, '0'), - exc_cls=NotFoundError, + return dyn.collection.get_items( + TransactKey(order_id) + + SortKey('0') + + SortKey('ITEMS') + + SortKey('ADDRESS') + + SortKey('PIX') + + SortKey('NFSE') + + SortKey('FEE'), ) diff --git a/api.saladeaula.digital/app/routes/orders/checkout.py b/api.saladeaula.digital/app/routes/orders/checkout.py index 6e5f3a5..a3c66eb 100644 --- a/api.saladeaula.digital/app/routes/orders/checkout.py +++ b/api.saladeaula.digital/app/routes/orders/checkout.py @@ -1,5 +1,7 @@ import re +from datetime import date, datetime, timedelta from decimal import Decimal +from enum import Enum from functools import reduce from http import HTTPStatus from typing import Any, Literal @@ -34,6 +36,13 @@ dyn = DynamoDBPersistenceLayer(ORDER_TABLE, dynamodb_client) class CouponNotFoundError(NotFoundError): ... +class PaymentMethod(str, Enum): + PIX = 'PIX' + CREDIT_CARD = 'CREDIT_CARD' + BANK_SLIP = 'BANK_SLIP' + MANUAL = 'MANUAL' + + class User(BaseModel): id: UUID4 | str name: NameStr @@ -69,13 +78,16 @@ class Coupon(BaseModel): class Checkout(BaseModel): - model_config = ConfigDict(str_strip_whitespace=True) + model_config = ConfigDict( + str_strip_whitespace=True, + use_enum_values=True, + ) id: UUID4 = Field(default_factory=uuid4) name: str email: EmailStr address: Address - payment_method: Literal['PIX', 'CREDIT_CARD', 'BANK_SLIP', 'MANUAL'] + payment_method: PaymentMethod items: tuple[Item, ...] enrollments: tuple[Enrollment, ...] = tuple() coupon: Coupon | None = None @@ -129,6 +141,12 @@ def checkout(payload: Checkout): enrollments = payload.enrollments coupon = payload.coupon subtotal = _sum_items(items) + payment_method = payload.payment_method + due_date = ( + _calc_due_date(now_, 3) + if payment_method == 'BANK_SLIP' + else now_ + timedelta(hours=1) + ) discount = ( _apply_discount(subtotal, coupon.amount, coupon.type) * -1 if coupon @@ -145,9 +163,9 @@ def checkout(payload: Checkout): 'subtotal': subtotal, 'total': total, 'discount': discount, + 'due_date': due_date, # Post-migration (orders): rename `create_date` to `created_at` 'create_date': now_, - 'due_date': '', } | ({'coupon': coupon.code} if coupon else {}) | ({'installments': payload.installments} if payload.installments else {}) @@ -176,6 +194,15 @@ def checkout(payload: Checkout): item={ 'id': order_id, 'sk': 'CREDIT_CARD', + 'brand': credit_card.brand, + 'last4': credit_card.last4, + 'created_at': now_, + } + ) + transact.put( + item={ + 'id': 'TRANSACTION', + 'sk': order_id, 'ttl': ttl(start_dt=now_, minutes=5), 'created_at': now_, } @@ -208,13 +235,16 @@ def checkout(payload: Checkout): item={ 'id': order_id, 'sk': f'ENROLLMENT#{enrollment.id}', - 'status': 'UNPROCESSED', + 'status': 'PENDING', 'created_at': now_, } | enrollment.model_dump(exclude={'id'}) ) - return JSONResponse(body={'id': order_id}, status_code=HTTPStatus.CREATED) + return JSONResponse( + body={'id': order_id}, + status_code=HTTPStatus.CREATED, + ) def _sum_items(items: tuple[Item, ...]): @@ -246,3 +276,20 @@ def _apply_discount( ) return min(amount, subtotal) + + +def _calc_due_date( + start_date: datetime, + business_days: int, + holidays: set[date] | None = None, +) -> datetime: + holidays = holidays or set() + current_dt = start_date + + while business_days > 0: + current_dt += timedelta(days=1) + + if current_dt.weekday() < 5 and current_dt.date() not in holidays: + business_days -= 1 + + return current_dt diff --git a/api.saladeaula.digital/app/routes/orgs/__init__.py b/api.saladeaula.digital/app/routes/orgs/__init__.py index 7da4daf..a17508c 100644 --- a/api.saladeaula.digital/app/routes/orgs/__init__.py +++ b/api.saladeaula.digital/app/routes/orgs/__init__.py @@ -4,7 +4,7 @@ from .admins import router as admins from .billing import router as billing from .custom_pricing import router as custom_pricing from .enrollments.scheduled import router as scheduled -from .enrollments.submission import router as submission +from .enrollments.submissions import router as submissions from .users.add import router as users from .users.batch_jobs import router as batch_jobs @@ -15,7 +15,7 @@ __all__ = [ 'billing', 'custom_pricing', 'scheduled', - 'submission', + 'submissions', 'users', 'batch_jobs', ] diff --git a/api.saladeaula.digital/app/routes/orgs/admins.py b/api.saladeaula.digital/app/routes/orgs/admins.py index d735e96..8b49634 100644 --- a/api.saladeaula.digital/app/routes/orgs/admins.py +++ b/api.saladeaula.digital/app/routes/orgs/admins.py @@ -24,7 +24,7 @@ class MemberNotFoundError(NotFoundError): ... @router.get('//admins') -def get_admins(org_id: str): +def admins(org_id: str): return dyn.collection.query( # Post-migration: rename `admins` to `ADMIN` KeyPair(org_id, 'admins#'), diff --git a/api.saladeaula.digital/app/routes/orgs/custom_pricing.py b/api.saladeaula.digital/app/routes/orgs/custom_pricing.py index c2511e6..ed74397 100644 --- a/api.saladeaula.digital/app/routes/orgs/custom_pricing.py +++ b/api.saladeaula.digital/app/routes/orgs/custom_pricing.py @@ -9,7 +9,7 @@ dyn = DynamoDBPersistenceLayer(COURSE_TABLE, dynamodb_client) @router.get('//custom-pricing') -def get_custom_pricing(org_id: str): +def custom_pricing(org_id: str): return dyn.collection.query( PartitionKey(f'CUSTOM_PRICING#ORG#{org_id}'), limit=150, diff --git a/api.saladeaula.digital/app/routes/orgs/enrollments/submission.py b/api.saladeaula.digital/app/routes/orgs/enrollments/submissions.py similarity index 93% rename from api.saladeaula.digital/app/routes/orgs/enrollments/submission.py rename to api.saladeaula.digital/app/routes/orgs/enrollments/submissions.py index c436a64..364ad97 100644 --- a/api.saladeaula.digital/app/routes/orgs/enrollments/submission.py +++ b/api.saladeaula.digital/app/routes/orgs/enrollments/submissions.py @@ -19,7 +19,7 @@ def submissions(org_id: str): ) -@router.get('//enrollments//submitted') +@router.get('//enrollments/submissions/') def submitted(org_id: str, submission_id: str): return dyn.collection.get_item( KeyPair( diff --git a/api.saladeaula.digital/app/routes/orgs/seats.py b/api.saladeaula.digital/app/routes/orgs/seats.py new file mode 100644 index 0000000..b6328b0 --- /dev/null +++ b/api.saladeaula.digital/app/routes/orgs/seats.py @@ -0,0 +1,16 @@ +from aws_lambda_powertools.event_handler.api_gateway import Router +from layercake.dynamodb import DynamoDBPersistenceLayer, PartitionKey + +from boto3clients import dynamodb_client +from config import COURSE_TABLE + +router = Router() +dyn = DynamoDBPersistenceLayer(COURSE_TABLE, dynamodb_client) + + +@router.get('//seats') +def seats(org_id: str): + return dyn.collection.query( + PartitionKey(f'SEAT#ORG#{org_id}'), + limit=150, + ) diff --git a/api.saladeaula.digital/tests/routes/orders/test_checkout.py b/api.saladeaula.digital/tests/routes/orders/test_checkout.py index 819f713..888d494 100644 --- a/api.saladeaula.digital/tests/routes/orders/test_checkout.py +++ b/api.saladeaula.digital/tests/routes/orders/test_checkout.py @@ -1,9 +1,12 @@ import json +from datetime import date, datetime from http import HTTPMethod, HTTPStatus from pprint import pprint from layercake.dynamodb import DynamoDBPersistenceLayer, PartitionKey +from routes.orders.checkout import _calc_due_date + from ...conftest import HttpApiProxy, LambdaContext @@ -147,3 +150,33 @@ def test_checkout_from_user( ) print(r) assert r['statusCode'] == HTTPStatus.CREATED + + +def test_calc_due_date_skips_weekends_and_holidays(): + start_date = datetime(2026, 1, 9, 10, 30) # Friday + business_days = 3 + + holidays = { + date(2026, 1, 12), # Monday (holiday) + } + + result = _calc_due_date( + start_date=start_date, + business_days=business_days, + holidays=holidays, + ) + + # Mon (12) -> holiday (ignored) + # Tue (13) -> 1 + # Wed (14) -> 2 + # Thu (15) -> 3 ✅ + expected = datetime(2026, 1, 15, 10, 30) + + assert result == expected + + +def test_calc_due_date_only_weekends(): + start_date = datetime(2026, 1, 8, 9, 0) # Thursday + result = _calc_due_date(start_date, 1) + + assert result == datetime(2026, 1, 9, 9, 0) diff --git a/enrollments-events/app/events/reenroll_if_failed.py b/enrollments-events/app/events/reenroll_if_failed.py index 1281760..56b4135 100644 --- a/enrollments-events/app/events/reenroll_if_failed.py +++ b/enrollments-events/app/events/reenroll_if_failed.py @@ -20,6 +20,7 @@ dyn = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client) @logger.inject_lambda_context def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool: new_image = event.detail['new_image'] + # Copy metadata from the failed enrollment for reuse in the new enrollment metadata = dyn.collection.get_items( TransactKey(new_image['id']) + SortKey( @@ -51,7 +52,7 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool: enrollment, org=metadata.get('org', None), subscription=subscription, - # Transfer the deduplication window if it exists + # Reuse the deduplication window if it exists deduplication_window={'offset_days': offset_days} if offset_days else None, linked_entities=frozenset( { diff --git a/enrollments-events/template.yaml b/enrollments-events/template.yaml index 513fcd1..231e76c 100644 --- a/enrollments-events/template.yaml +++ b/enrollments-events/template.yaml @@ -261,6 +261,31 @@ Resources: scope: [MULTI_USER] status: [PENDING] + EventAllocateSeatsFunction: + Type: AWS::Serverless::Function + Properties: + Handler: events.allocate_seats.lambda_handler + LoggingConfig: + LogGroup: !Ref EventLog + Policies: + - DynamoDBCrudPolicy: + TableName: !Ref OrderTable + - DynamoDBCrudPolicy: + TableName: !Ref EnrollmentTable + - DynamoDBReadPolicy: + TableName: !Ref CourseTable + Events: + DynamoDBEvent: + Type: EventBridgeRule + Properties: + Pattern: + resources: [!Ref OrderTable] + detail-type: [INSERT] + detail: + new_image: + sk: [SEATS_ALLOCATION] + status: [PENDING] + SesPolicy: Type: AWS::IAM::ManagedPolicy Properties: diff --git a/orders-events/app/config.py b/orders-events/app/config.py index 4401c2f..0619787 100644 --- a/orders-events/app/config.py +++ b/orders-events/app/config.py @@ -5,6 +5,9 @@ ORDER_TABLE: str = os.getenv('ORDER_TABLE') # type: ignore COURSE_TABLE: str = os.getenv('COURSE_TABLE') # type: ignore 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 + BUCKET_NAME: str = os.getenv('BUCKET_NAME') # type: ignore EMAIL_SENDER = ('EDUSEG®', 'noreply@eduseg.com.br') diff --git a/orders-events/app/events/append_org_id.py b/orders-events/app/events/append_org_id.py index 80336a9..10d1d84 100644 --- a/orders-events/app/events/append_org_id.py +++ b/orders-events/app/events/append_org_id.py @@ -57,14 +57,15 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool: }, ) - transact.update( - key=KeyPair(new_image['id'], 'author'), - update_expr='SET user_id = :user_id, updated_at = :updated_at', - expr_attr_values={ - ':user_id': r['user_id'], - ':updated_at': now_, - }, - ) + if 'org_id' not in new_image: + transact.update( + key=KeyPair(new_image['id'], 'author'), + update_expr='SET user_id = :user_id, updated_at = :updated_at', + expr_attr_values={ + ':user_id': r['user_id'], + ':updated_at': now_, + }, + ) logger.info('IDs updated') diff --git a/orders-events/app/iugu.py b/orders-events/app/iugu.py index f19f4a1..3c20c60 100644 --- a/orders-events/app/iugu.py +++ b/orders-events/app/iugu.py @@ -16,36 +16,66 @@ Documentation: """ from dataclasses import dataclass +from datetime import datetime +from decimal import Decimal from enum import Enum from urllib.parse import ParseResult, urlparse import requests from aws_lambda_powertools import Logger -from pydantic import BaseModel, HttpUrl - -# from data_classes.invoice import Invoice, Pix -# from data_classes.order import CreditCard, Order, PaymentMethod, Status +from layercake.extra_types import CreditCard +from pydantic import BaseModel, ConfigDict, HttpUrl logger = Logger(__name__) -class Status(Enum): +class Status(str, Enum): PAID = 'PAID' DECLINED = 'DECLINED' -class PaymentMethod(Enum): +class PaymentMethod(str, Enum): PIX = 'PIX' BANK_SLIP = 'BANK_SLIP' CREDIT_CARD = 'CREDIT_CARD' -class Order: ... +@dataclass +class Address(BaseModel): + postcode: str + neighborhood: str + city: str + state: str + address1: str + address2: str | None = None -class CreditCard: ... +class Item(BaseModel): + id: str + name: str + quantity: int = 1 + unit_price: Decimal +class Order(BaseModel): + model_config = ConfigDict(use_enum_values=True) + + id: str + email: str + name: str + due_date: datetime + address: Address + items: tuple[Item, ...] + payment_method: PaymentMethod + cpf: str | None = None + cnpj: str | None = None + + +@dataclass +class Invoice: ... + + +@dataclass class BankSlip(BaseModel): digitable_line: str bank_slip_url: HttpUrl @@ -115,7 +145,7 @@ class Iugu: payload = { 'order_id': order.id, 'external_reference': order.id, - 'due_date': order.due_date.strftime('%Y-%m-%d'), # type: ignore + 'due_date': order.due_date.strftime('%Y-%m-%d'), 'items': items, 'email': order.email, 'payable_with': order.payment_method.lower(), @@ -126,68 +156,71 @@ class Iugu: 'cpf_cnpj': order.cnpj if order.cnpj else order.cpf, 'address': { 'zip_code': order.address.postcode, - 'street': order.address.street, - 'number': order.address.street_number, + 'street': order.address.address1, + 'number': '', 'district': order.address.neighborhood, 'city': order.address.city, 'state': order.address.state, - 'complement': order.address.complement, + 'complement': order.address.address2, 'country=': 'Brasil', }, }, } try: - response = requests.post(url, json=payload, timeout=15) - response.raise_for_status() + r = requests.post(url, json=payload, timeout=15) + r.raise_for_status() except requests.HTTPError as err: logger.exception(err) raise else: - response = response.json() - pix = ( - Pix(**response['pix']) - if order.payment_method == PaymentMethod.PIX - else None - ) - bank_slip = ( - BankSlip(**response['bank_slip']) - if order.payment_method == PaymentMethod.BANK_SLIP - else None - ) + return r.json() + # pix = ( + # Pix(**response['pix']) + # if order.payment_method == PaymentMethod.PIX + # else None + # ) + # bank_slip = ( + # BankSlip(**response['bank_slip']) + # if order.payment_method == PaymentMethod.BANK_SLIP + # else None + # ) - return Invoice( - id=response['secure_id'], - pdf=bank_slip.bank_slip_url - if bank_slip - else '%s.pdf' % response['secure_url'], - pix=pix, - ) + # return Invoice( + # id=response['secure_id'], + # pdf=bank_slip.bank_slip_url + # if bank_slip + # else '%s.pdf' % response['secure_url'], + # pix=pix, + # ) def payment_token(self, credit_card: CreditCard) -> Token: url = self.url(path='/v1/payment_token') - payload = { - 'test': self.credentials.test_mode, - 'account_id': self.credentials.account_id, - 'method': 'credit_card', - 'data': { - 'number': credit_card.number, - 'verification_value': credit_card.cvv, - 'first_name': credit_card.first_name, - 'last_name': credit_card.last_name, - 'month': credit_card.exp.strftime('%m'), - 'year': credit_card.exp.strftime('%Y'), - }, - } try: - response = requests.post(url, json=payload, timeout=15) - response.raise_for_status() + r = requests.post( + url, + json={ + 'test': self.credentials.test_mode, + 'account_id': self.credentials.account_id, + 'method': 'credit_card', + 'data': { + 'number': credit_card.number, + 'verification_value': credit_card.cvv, + 'first_name': credit_card.first_name, + 'last_name': credit_card.last_name, + 'month': credit_card.exp_month, + 'year': credit_card.exp_year, + }, + }, + timeout=15, + ) + r.raise_for_status() except requests.HTTPError as err: logger.exception(err) raise else: - return Token(response.json()['id']) + return Token(r.json()['id']) def charge( self, @@ -234,13 +267,13 @@ class Iugu: url = self.url(path=f'/v1/invoices/{format_id(invoice_id)}') try: - response = requests.get(url, timeout=15) - response.raise_for_status() + r = requests.get(url, timeout=15) + r.raise_for_status() except requests.HTTPError as err: logger.exception(err) raise else: - return response.json() + return r.json() def format_id(invoice_id: str) -> str: diff --git a/orders-events/template.yaml b/orders-events/template.yaml index 2d66168..11acae1 100644 --- a/orders-events/template.yaml +++ b/orders-events/template.yaml @@ -39,6 +39,7 @@ Globals: ENROLLMENT_TABLE: !Ref EnrollmentTable COURSE_TABLE: !Ref CourseTable BUCKET_NAME: !Ref BucketName + IUGU_API_TOKEN: '{{resolve:ssm:/saladeaula/iugu_api_token}}' Resources: EventLog: @@ -46,6 +47,37 @@ Resources: Properties: RetentionInDays: 90 + HttpLog: + Type: AWS::Logs::LogGroup + Properties: + RetentionInDays: 90 + + HttpApi: + Type: AWS::Serverless::HttpApi + Properties: + CorsConfiguration: + AllowOrigins: ['*'] + AllowMethods: [POST, OPTIONS] + AllowHeaders: [Content-Type, X-Requested-With] + + HttpApiFunction: + Type: AWS::Serverless::Function + Properties: + Handler: app.lambda_handler + Timeout: 12 + LoggingConfig: + LogGroup: !Ref HttpLog + Policies: + - DynamoDBWritePolicy: + TableName: !Ref OrderTable + Events: + Post: + Type: HttpApi + Properties: + Path: / + Method: POST + ApiId: !Ref HttpApi + EventBillingAppendEnrollmentFunction: Type: AWS::Serverless::Function Properties: @@ -286,3 +318,13 @@ Resources: new_image: sk: [generated_items] status: [SUCCESS] + +Outputs: + HttpApiUrl: + Description: URL of your API endpoint + Value: + Fn::Sub: 'https://${HttpApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}' + HttpApiId: + Description: Api ID of HttpApi + Value: + Ref: HttpApi diff --git a/orders-events/tests/seeds.jsonl b/orders-events/tests/seeds.jsonl index 5eb8204..19aab55 100644 --- a/orders-events/tests/seeds.jsonl +++ b/orders-events/tests/seeds.jsonl @@ -7,9 +7,13 @@ // Orders {"id": "9omWNKymwU5U4aeun6mWzZ", "sk": "0", "total": 398, "status": "PENDING", "payment_method": "MANUAL", "tenant_id": "cJtK9SsnJhKPyxESe7g3DG"} {"id": "18f934d8-035a-4ebc-9f8b-6c84782b8c73", "sk": "0", "payment_method": "PAID"} -{"id": "6a60d026-d383-4707-b093-b6eddea1a24e", "sk": "items", "items": [{"id": "a810dd22-56c0-4d9b-8cd2-7e2ee9c45839", "name": "pytest", "quantity": 1, "unit_price": 109}]} +{"id": "6a60d026-d383-4707-b093-b6eddea1a24e", "sk": "ITEMS", "items": [{"id": "a810dd22-56c0-4d9b-8cd2-7e2ee9c45839", "name": "pytest", "quantity": 1, "unit_price": 109}]} {"id": "a810dd22-56c0-4d9b-8cd2-7e2ee9c45839", "sk": "metadata#betaeducacao", "course_id": "dc1a0428-47bf-4db1-a5da-24be49c9fda6", "create_date": "2025-06-05T12:13:54.371416+00:00"} +{"id": "2849f1d5-f4f1-411e-8497-ec3a40afc0ab", "sk": "0" "payment_method": "BANK_SLIP", "status": "PENDING", "total": 178.2, "due_date": "", "email": "org+15608435000190@users.noreply.saladeaula.digital", "name": "Beta Educação", "coupon": "10OFF", "discount": -19.8, "create_date": "2026-01-07T19:09:54.193859-03:00", "updated_at": "2026-01-07T19:09:54.871374-03:00", "org_id": "cJtK9SsnJhKPyxESe7g3DG", "subtotal": 198, "tenant_id": "cJtK9SsnJhKPyxESe7g3DG", "cnpj": "15608435000190"} +{"id": "2849f1d5-f4f1-411e-8497-ec3a40afc0ab", "sk": "ITEMS", "items": [ { "name": "CIPA Grau de Risco 1", "id": "3c27ea9c-9464-46a1-9717-8c1441793186", "quantity": 1, "unit_price": 99 }, { "name": "CIPA Grau de Risco 2", "id": "99bb3b60-4ded-4a8e-937c-ba2d78ec6454", "quantity": 1, "unit_price": 99 } ], "created_at": "2026-01-07T19:09:54.193859-03:00"} +{"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"} + // User data {"id": "5OxmMjL-ujoR5IMGegQz", "sk": "0", "name": "Sérgio R Siqueira"} {"id": "cnpj", "sk": "15608435000190", "user_id": "cJtK9SsnJhKPyxESe7g3DG"} diff --git a/orders-events/uv.lock b/orders-events/uv.lock index 193b632..612d00c 100644 --- a/orders-events/uv.lock +++ b/orders-events/uv.lock @@ -651,7 +651,7 @@ wheels = [ [[package]] name = "layercake" -version = "0.11.3" +version = "0.12.0" source = { directory = "../layercake" } dependencies = [ { name = "arnparse" },