wip checkout

This commit is contained in:
2026-01-09 11:20:56 -03:00
parent e29e81b253
commit 823134f450
18 changed files with 290 additions and 80 deletions

View File

@@ -55,7 +55,7 @@ app.include_router(orgs.admins, prefix='/orgs')
app.include_router(orgs.billing, prefix='/orgs') app.include_router(orgs.billing, prefix='/orgs')
app.include_router(orgs.custom_pricing, prefix='/orgs') app.include_router(orgs.custom_pricing, prefix='/orgs')
app.include_router(orgs.scheduled, 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.users, prefix='/orgs')
app.include_router(orgs.batch_jobs, prefix='/orgs') app.include_router(orgs.batch_jobs, prefix='/orgs')

View File

@@ -12,5 +12,4 @@ BUCKET_NAME: str = os.getenv('BUCKET_NAME') # type: ignore
DEDUP_WINDOW_OFFSET_DAYS = 90 DEDUP_WINDOW_OFFSET_DAYS = 90
PAPERFORGE_API = 'https://paperforge.saladeaula.digital' PAPERFORGE_API = 'https://paperforge.saladeaula.digital'
INTERNAL_EMAIL_DOMAIN = 'users.noreply.saladeaula.digital' INTERNAL_EMAIL_DOMAIN = 'users.noreply.saladeaula.digital'

View File

@@ -1,8 +1,9 @@
from aws_lambda_powertools.event_handler.api_gateway import Router from aws_lambda_powertools.event_handler.api_gateway import Router
from aws_lambda_powertools.event_handler.exceptions import ( from layercake.dynamodb import (
NotFoundError, DynamoDBPersistenceLayer,
SortKey,
TransactKey,
) )
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair
from boto3clients import dynamodb_client from boto3clients import dynamodb_client
from config import ORDER_TABLE from config import ORDER_TABLE
@@ -17,7 +18,12 @@ dyn = DynamoDBPersistenceLayer(ORDER_TABLE, dynamodb_client)
@router.get('/<order_id>') @router.get('/<order_id>')
def get_order(order_id: str): def get_order(order_id: str):
return dyn.collection.get_item( return dyn.collection.get_items(
KeyPair(order_id, '0'), TransactKey(order_id)
exc_cls=NotFoundError, + SortKey('0')
+ SortKey('ITEMS')
+ SortKey('ADDRESS')
+ SortKey('PIX')
+ SortKey('NFSE')
+ SortKey('FEE'),
) )

View File

@@ -1,5 +1,7 @@
import re import re
from datetime import date, datetime, timedelta
from decimal import Decimal from decimal import Decimal
from enum import Enum
from functools import reduce from functools import reduce
from http import HTTPStatus from http import HTTPStatus
from typing import Any, Literal from typing import Any, Literal
@@ -34,6 +36,13 @@ dyn = DynamoDBPersistenceLayer(ORDER_TABLE, dynamodb_client)
class CouponNotFoundError(NotFoundError): ... class CouponNotFoundError(NotFoundError): ...
class PaymentMethod(str, Enum):
PIX = 'PIX'
CREDIT_CARD = 'CREDIT_CARD'
BANK_SLIP = 'BANK_SLIP'
MANUAL = 'MANUAL'
class User(BaseModel): class User(BaseModel):
id: UUID4 | str id: UUID4 | str
name: NameStr name: NameStr
@@ -69,13 +78,16 @@ class Coupon(BaseModel):
class Checkout(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) id: UUID4 = Field(default_factory=uuid4)
name: str name: str
email: EmailStr email: EmailStr
address: Address address: Address
payment_method: Literal['PIX', 'CREDIT_CARD', 'BANK_SLIP', 'MANUAL'] payment_method: PaymentMethod
items: tuple[Item, ...] items: tuple[Item, ...]
enrollments: tuple[Enrollment, ...] = tuple() enrollments: tuple[Enrollment, ...] = tuple()
coupon: Coupon | None = None coupon: Coupon | None = None
@@ -129,6 +141,12 @@ def checkout(payload: Checkout):
enrollments = payload.enrollments enrollments = payload.enrollments
coupon = payload.coupon coupon = payload.coupon
subtotal = _sum_items(items) 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 = ( discount = (
_apply_discount(subtotal, coupon.amount, coupon.type) * -1 _apply_discount(subtotal, coupon.amount, coupon.type) * -1
if coupon if coupon
@@ -145,9 +163,9 @@ def checkout(payload: Checkout):
'subtotal': subtotal, 'subtotal': subtotal,
'total': total, 'total': total,
'discount': discount, 'discount': discount,
'due_date': due_date,
# Post-migration (orders): rename `create_date` to `created_at` # Post-migration (orders): rename `create_date` to `created_at`
'create_date': now_, 'create_date': now_,
'due_date': '',
} }
| ({'coupon': coupon.code} if coupon else {}) | ({'coupon': coupon.code} if coupon else {})
| ({'installments': payload.installments} if payload.installments else {}) | ({'installments': payload.installments} if payload.installments else {})
@@ -176,6 +194,15 @@ def checkout(payload: Checkout):
item={ item={
'id': order_id, 'id': order_id,
'sk': 'CREDIT_CARD', '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), 'ttl': ttl(start_dt=now_, minutes=5),
'created_at': now_, 'created_at': now_,
} }
@@ -208,13 +235,16 @@ def checkout(payload: Checkout):
item={ item={
'id': order_id, 'id': order_id,
'sk': f'ENROLLMENT#{enrollment.id}', 'sk': f'ENROLLMENT#{enrollment.id}',
'status': 'UNPROCESSED', 'status': 'PENDING',
'created_at': now_, 'created_at': now_,
} }
| enrollment.model_dump(exclude={'id'}) | 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, ...]): def _sum_items(items: tuple[Item, ...]):
@@ -246,3 +276,20 @@ def _apply_discount(
) )
return min(amount, subtotal) 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

View File

@@ -4,7 +4,7 @@ from .admins import router as admins
from .billing import router as billing from .billing import router as billing
from .custom_pricing import router as custom_pricing from .custom_pricing import router as custom_pricing
from .enrollments.scheduled import router as scheduled 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.add import router as users
from .users.batch_jobs import router as batch_jobs from .users.batch_jobs import router as batch_jobs
@@ -15,7 +15,7 @@ __all__ = [
'billing', 'billing',
'custom_pricing', 'custom_pricing',
'scheduled', 'scheduled',
'submission', 'submissions',
'users', 'users',
'batch_jobs', 'batch_jobs',
] ]

View File

@@ -24,7 +24,7 @@ class MemberNotFoundError(NotFoundError): ...
@router.get('/<org_id>/admins') @router.get('/<org_id>/admins')
def get_admins(org_id: str): def admins(org_id: str):
return dyn.collection.query( return dyn.collection.query(
# Post-migration: rename `admins` to `ADMIN` # Post-migration: rename `admins` to `ADMIN`
KeyPair(org_id, 'admins#'), KeyPair(org_id, 'admins#'),

View File

@@ -9,7 +9,7 @@ dyn = DynamoDBPersistenceLayer(COURSE_TABLE, dynamodb_client)
@router.get('/<org_id>/custom-pricing') @router.get('/<org_id>/custom-pricing')
def get_custom_pricing(org_id: str): def custom_pricing(org_id: str):
return dyn.collection.query( return dyn.collection.query(
PartitionKey(f'CUSTOM_PRICING#ORG#{org_id}'), PartitionKey(f'CUSTOM_PRICING#ORG#{org_id}'),
limit=150, limit=150,

View File

@@ -19,7 +19,7 @@ def submissions(org_id: str):
) )
@router.get('/<org_id>/enrollments/<submission_id>/submitted') @router.get('/<org_id>/enrollments/submissions/<submission_id>')
def submitted(org_id: str, submission_id: str): def submitted(org_id: str, submission_id: str):
return dyn.collection.get_item( return dyn.collection.get_item(
KeyPair( KeyPair(

View File

@@ -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('/<org_id>/seats')
def seats(org_id: str):
return dyn.collection.query(
PartitionKey(f'SEAT#ORG#{org_id}'),
limit=150,
)

View File

@@ -1,9 +1,12 @@
import json import json
from datetime import date, datetime
from http import HTTPMethod, HTTPStatus from http import HTTPMethod, HTTPStatus
from pprint import pprint from pprint import pprint
from layercake.dynamodb import DynamoDBPersistenceLayer, PartitionKey from layercake.dynamodb import DynamoDBPersistenceLayer, PartitionKey
from routes.orders.checkout import _calc_due_date
from ...conftest import HttpApiProxy, LambdaContext from ...conftest import HttpApiProxy, LambdaContext
@@ -147,3 +150,33 @@ def test_checkout_from_user(
) )
print(r) print(r)
assert r['statusCode'] == HTTPStatus.CREATED 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)

View File

@@ -20,6 +20,7 @@ dyn = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client)
@logger.inject_lambda_context @logger.inject_lambda_context
def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool: def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
new_image = event.detail['new_image'] new_image = event.detail['new_image']
# Copy metadata from the failed enrollment for reuse in the new enrollment
metadata = dyn.collection.get_items( metadata = dyn.collection.get_items(
TransactKey(new_image['id']) TransactKey(new_image['id'])
+ SortKey( + SortKey(
@@ -51,7 +52,7 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
enrollment, enrollment,
org=metadata.get('org', None), org=metadata.get('org', None),
subscription=subscription, 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, deduplication_window={'offset_days': offset_days} if offset_days else None,
linked_entities=frozenset( linked_entities=frozenset(
{ {

View File

@@ -261,6 +261,31 @@ Resources:
scope: [MULTI_USER] scope: [MULTI_USER]
status: [PENDING] 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: SesPolicy:
Type: AWS::IAM::ManagedPolicy Type: AWS::IAM::ManagedPolicy
Properties: Properties:

View File

@@ -5,6 +5,9 @@ ORDER_TABLE: str = os.getenv('ORDER_TABLE') # type: ignore
COURSE_TABLE: str = os.getenv('COURSE_TABLE') # type: ignore COURSE_TABLE: str = os.getenv('COURSE_TABLE') # type: ignore
ENROLLMENT_TABLE: str = os.getenv('ENROLLMENT_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 BUCKET_NAME: str = os.getenv('BUCKET_NAME') # type: ignore
EMAIL_SENDER = ('EDUSEG®', 'noreply@eduseg.com.br') EMAIL_SENDER = ('EDUSEG®', 'noreply@eduseg.com.br')

View File

@@ -57,14 +57,15 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
}, },
) )
transact.update( if 'org_id' not in new_image:
key=KeyPair(new_image['id'], 'author'), transact.update(
update_expr='SET user_id = :user_id, updated_at = :updated_at', key=KeyPair(new_image['id'], 'author'),
expr_attr_values={ update_expr='SET user_id = :user_id, updated_at = :updated_at',
':user_id': r['user_id'], expr_attr_values={
':updated_at': now_, ':user_id': r['user_id'],
}, ':updated_at': now_,
) },
)
logger.info('IDs updated') logger.info('IDs updated')

View File

@@ -16,36 +16,66 @@ Documentation:
""" """
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime
from decimal import Decimal
from enum import Enum from enum import Enum
from urllib.parse import ParseResult, urlparse from urllib.parse import ParseResult, urlparse
import requests import requests
from aws_lambda_powertools import Logger from aws_lambda_powertools import Logger
from pydantic import BaseModel, HttpUrl from layercake.extra_types import CreditCard
from pydantic import BaseModel, ConfigDict, HttpUrl
# from data_classes.invoice import Invoice, Pix
# from data_classes.order import CreditCard, Order, PaymentMethod, Status
logger = Logger(__name__) logger = Logger(__name__)
class Status(Enum): class Status(str, Enum):
PAID = 'PAID' PAID = 'PAID'
DECLINED = 'DECLINED' DECLINED = 'DECLINED'
class PaymentMethod(Enum): class PaymentMethod(str, Enum):
PIX = 'PIX' PIX = 'PIX'
BANK_SLIP = 'BANK_SLIP' BANK_SLIP = 'BANK_SLIP'
CREDIT_CARD = 'CREDIT_CARD' 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): class BankSlip(BaseModel):
digitable_line: str digitable_line: str
bank_slip_url: HttpUrl bank_slip_url: HttpUrl
@@ -115,7 +145,7 @@ class Iugu:
payload = { payload = {
'order_id': order.id, 'order_id': order.id,
'external_reference': 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, 'items': items,
'email': order.email, 'email': order.email,
'payable_with': order.payment_method.lower(), 'payable_with': order.payment_method.lower(),
@@ -126,68 +156,71 @@ class Iugu:
'cpf_cnpj': order.cnpj if order.cnpj else order.cpf, 'cpf_cnpj': order.cnpj if order.cnpj else order.cpf,
'address': { 'address': {
'zip_code': order.address.postcode, 'zip_code': order.address.postcode,
'street': order.address.street, 'street': order.address.address1,
'number': order.address.street_number, 'number': '',
'district': order.address.neighborhood, 'district': order.address.neighborhood,
'city': order.address.city, 'city': order.address.city,
'state': order.address.state, 'state': order.address.state,
'complement': order.address.complement, 'complement': order.address.address2,
'country=': 'Brasil', 'country=': 'Brasil',
}, },
}, },
} }
try: try:
response = requests.post(url, json=payload, timeout=15) r = requests.post(url, json=payload, timeout=15)
response.raise_for_status() r.raise_for_status()
except requests.HTTPError as err: except requests.HTTPError as err:
logger.exception(err) logger.exception(err)
raise raise
else: else:
response = response.json() return r.json()
pix = ( # pix = (
Pix(**response['pix']) # Pix(**response['pix'])
if order.payment_method == PaymentMethod.PIX # if order.payment_method == PaymentMethod.PIX
else None # else None
) # )
bank_slip = ( # bank_slip = (
BankSlip(**response['bank_slip']) # BankSlip(**response['bank_slip'])
if order.payment_method == PaymentMethod.BANK_SLIP # if order.payment_method == PaymentMethod.BANK_SLIP
else None # else None
) # )
return Invoice( # return Invoice(
id=response['secure_id'], # id=response['secure_id'],
pdf=bank_slip.bank_slip_url # pdf=bank_slip.bank_slip_url
if bank_slip # if bank_slip
else '%s.pdf' % response['secure_url'], # else '%s.pdf' % response['secure_url'],
pix=pix, # pix=pix,
) # )
def payment_token(self, credit_card: CreditCard) -> Token: def payment_token(self, credit_card: CreditCard) -> Token:
url = self.url(path='/v1/payment_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: try:
response = requests.post(url, json=payload, timeout=15) r = requests.post(
response.raise_for_status() 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: except requests.HTTPError as err:
logger.exception(err) logger.exception(err)
raise raise
else: else:
return Token(response.json()['id']) return Token(r.json()['id'])
def charge( def charge(
self, self,
@@ -234,13 +267,13 @@ class Iugu:
url = self.url(path=f'/v1/invoices/{format_id(invoice_id)}') url = self.url(path=f'/v1/invoices/{format_id(invoice_id)}')
try: try:
response = requests.get(url, timeout=15) r = requests.get(url, timeout=15)
response.raise_for_status() r.raise_for_status()
except requests.HTTPError as err: except requests.HTTPError as err:
logger.exception(err) logger.exception(err)
raise raise
else: else:
return response.json() return r.json()
def format_id(invoice_id: str) -> str: def format_id(invoice_id: str) -> str:

View File

@@ -39,6 +39,7 @@ Globals:
ENROLLMENT_TABLE: !Ref EnrollmentTable ENROLLMENT_TABLE: !Ref EnrollmentTable
COURSE_TABLE: !Ref CourseTable COURSE_TABLE: !Ref CourseTable
BUCKET_NAME: !Ref BucketName BUCKET_NAME: !Ref BucketName
IUGU_API_TOKEN: '{{resolve:ssm:/saladeaula/iugu_api_token}}'
Resources: Resources:
EventLog: EventLog:
@@ -46,6 +47,37 @@ Resources:
Properties: Properties:
RetentionInDays: 90 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: EventBillingAppendEnrollmentFunction:
Type: AWS::Serverless::Function Type: AWS::Serverless::Function
Properties: Properties:
@@ -286,3 +318,13 @@ Resources:
new_image: new_image:
sk: [generated_items] sk: [generated_items]
status: [SUCCESS] 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

View File

@@ -7,9 +7,13 @@
// Orders // Orders
{"id": "9omWNKymwU5U4aeun6mWzZ", "sk": "0", "total": 398, "status": "PENDING", "payment_method": "MANUAL", "tenant_id": "cJtK9SsnJhKPyxESe7g3DG"} {"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": "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": "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 // User data
{"id": "5OxmMjL-ujoR5IMGegQz", "sk": "0", "name": "Sérgio R Siqueira"} {"id": "5OxmMjL-ujoR5IMGegQz", "sk": "0", "name": "Sérgio R Siqueira"}
{"id": "cnpj", "sk": "15608435000190", "user_id": "cJtK9SsnJhKPyxESe7g3DG"} {"id": "cnpj", "sk": "15608435000190", "user_id": "cJtK9SsnJhKPyxESe7g3DG"}

2
orders-events/uv.lock generated
View File

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