wip checkout
This commit is contained in:
@@ -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')
|
||||||
|
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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'),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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',
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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#'),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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(
|
||||||
16
api.saladeaula.digital/app/routes/orgs/seats.py
Normal file
16
api.saladeaula.digital/app/routes/orgs/seats.py
Normal 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,
|
||||||
|
)
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if 'org_id' not in new_image:
|
||||||
transact.update(
|
transact.update(
|
||||||
key=KeyPair(new_image['id'], 'author'),
|
key=KeyPair(new_image['id'], 'author'),
|
||||||
update_expr='SET user_id = :user_id, updated_at = :updated_at',
|
update_expr='SET user_id = :user_id, updated_at = :updated_at',
|
||||||
|
|||||||
@@ -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,47 +156,51 @@ 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 = {
|
|
||||||
|
try:
|
||||||
|
r = requests.post(
|
||||||
|
url,
|
||||||
|
json={
|
||||||
'test': self.credentials.test_mode,
|
'test': self.credentials.test_mode,
|
||||||
'account_id': self.credentials.account_id,
|
'account_id': self.credentials.account_id,
|
||||||
'method': 'credit_card',
|
'method': 'credit_card',
|
||||||
@@ -175,19 +209,18 @@ class Iugu:
|
|||||||
'verification_value': credit_card.cvv,
|
'verification_value': credit_card.cvv,
|
||||||
'first_name': credit_card.first_name,
|
'first_name': credit_card.first_name,
|
||||||
'last_name': credit_card.last_name,
|
'last_name': credit_card.last_name,
|
||||||
'month': credit_card.exp.strftime('%m'),
|
'month': credit_card.exp_month,
|
||||||
'year': credit_card.exp.strftime('%Y'),
|
'year': credit_card.exp_year,
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
|
timeout=15,
|
||||||
try:
|
)
|
||||||
response = requests.post(url, json=payload, timeout=15)
|
r.raise_for_status()
|
||||||
response.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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
2
orders-events/uv.lock
generated
@@ -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" },
|
||||||
|
|||||||
Reference in New Issue
Block a user