fix redirect to checkout when the org has not a subscription

This commit is contained in:
2026-01-07 14:30:29 -03:00
parent d222872000
commit ded57b43f0
7 changed files with 173 additions and 113 deletions

View File

@@ -1,10 +1,11 @@
import re
from decimal import Decimal
from http import HTTPStatus
from typing import Annotated, Literal
from typing import Any, Literal
from uuid import uuid4
from aws_lambda_powertools.event_handler.api_gateway import Router
from aws_lambda_powertools.event_handler.openapi.params import Body
from layercake.dateutils import now
from layercake.dynamodb import DynamoDBPersistenceLayer
from layercake.extra_types import CnpjStr, CpfStr, NameStr
from pydantic import (
@@ -12,6 +13,7 @@ from pydantic import (
BaseModel,
ConfigDict,
EmailStr,
Field,
field_validator,
model_validator,
)
@@ -20,6 +22,7 @@ from api_gateway import JSONResponse
from boto3clients import dynamodb_client
from config import ORDER_TABLE
from routes.enrollments.enroll import Enrollment
from routes.orgs.address import address
router = Router()
dyn = DynamoDBPersistenceLayer(ORDER_TABLE, dynamodb_client)
@@ -62,6 +65,7 @@ class Coupon(BaseModel):
class Checkout(BaseModel):
model_config = ConfigDict(str_strip_whitespace=True)
id: UUID4 = Field(default_factory=uuid4)
name: str
email: EmailStr
address: Address
@@ -69,11 +73,11 @@ class Checkout(BaseModel):
items: tuple[Item, ...]
enrollments: tuple[Enrollment, ...] | None = None
coupon: Coupon | None = None
user: User | None = None
org_id: UUID4 | str | None = None
user_id: UUID4 | str | None = None
cnpj: CnpjStr | None = None
cpf: CpfStr | None = None
created_by: User | None = None
@model_validator(mode='after')
def verify_fields(self):
@@ -84,15 +88,67 @@ class Checkout(BaseModel):
if self.org_id is None:
raise ValueError('org_id is missing')
if self.user is None:
raise ValueError('user is missing')
if self.created_by is None:
raise ValueError('created_by is missing')
if self.cpf is not None and self.user_id is None:
raise ValueError('user_id is missing')
return self
def model_dump(self, **kwargs) -> dict[str, Any]:
return super().model_dump(
exclude_none=True,
exclude={'items', 'address', 'created_by'},
**kwargs,
)
@router.post('/')
def checkout(body: Annotated[Checkout, Body()]):
return JSONResponse(status_code=HTTPStatus.CREATED)
def checkout(payload: Checkout):
now_ = now()
order_id = str(payload.id)
address = payload.address
coupon = payload.coupon
with dyn.transact_writer() as transact:
transact.put(
item={
'id': order_id,
'sk': '0',
'total': '',
'discount': '',
'due_date': '',
'created_at': now_,
}
| ({'coupon': coupon.code} if coupon else {})
| payload.model_dump()
)
transact.put(
item={
'id': order_id,
'sk': 'ITEMS',
'items': [],
'created_at': now_,
}
)
transact.put(
item={
'id': order_id,
'sk': 'ADDRESS',
'created_at': now_,
}
| address.model_dump()
)
if coupon:
transact.put(
item={
'id': order_id,
'sk': 'COUPON',
'created_at': now_,
}
| coupon.model_dump()
)
return JSONResponse(body={'id': order_id}, status_code=HTTPStatus.CREATED)

View File

@@ -1,6 +1,8 @@
import json
from http import HTTPMethod, HTTPStatus
from pprint import pprint
from layercake.dynamodb import DynamoDBPersistenceLayer
from layercake.dynamodb import DynamoDBPersistenceLayer, PartitionKey
from ...conftest import HttpApiProxy, LambdaContext
@@ -20,9 +22,9 @@ def test_checkout(
'org_id': 'f6000f79-6e5c-49a0-952f-3bda330ef278',
'cnpj': '00000000000191',
'name': 'Branco do Brasil',
'email': 'sergio@somosbeta.com.br',
'payment_method': 'MANUAL',
'user': {
'email': 'bb@users.noreply.saladeaula.digital',
'payment_method': 'BANK_SLIP',
'created_by': {
'id': '15bacf02-1535-4bee-9022-19d106fd7518',
'name': 'Sérgio R Siqueira',
},
@@ -46,46 +48,50 @@ def test_checkout(
),
lambda_context,
)
print(r)
body = json.loads(r['body'])
print(body)
assert r['statusCode'] == HTTPStatus.CREATED
r = dynamodb_persistence_layer.collection.query(PartitionKey(body['id']))
pprint(r['items'])
def test_checkout_from_user(
app,
seeds,
http_api_proxy: HttpApiProxy,
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
lambda_context: LambdaContext,
):
r = app.lambda_handler(
http_api_proxy(
raw_path='/orders',
method=HTTPMethod.POST,
body={
'user_id': '15bacf02-1535-4bee-9022-19d106fd7518',
'cpf': '07879819908',
'name': 'Sérgio R Siqueira',
'email': 'sergio@somosbeta.com.br',
'payment_method': 'MANUAL',
'address': {
'city': 'Curitiba',
'postcode': '81280350',
'neighborhood': 'Cidade Industrial',
'address1': 'Rua Monsenhor Ivo Zanlorenzi',
'address2': 'nº 5190, ap 1802',
'state': 'PR',
},
'items': [
{
'id': 'e1c44881-2fe3-484e-ada2-12b6bf5b9398',
'name': 'NR-35 Segurança nos Trabalhos em Altura',
'quantity': 2,
'unit_price': 119,
}
],
},
),
lambda_context,
)
print(r)
assert r['statusCode'] == HTTPStatus.CREATED
# def test_checkout_from_user(
# app,
# seeds,
# http_api_proxy: HttpApiProxy,
# dynamodb_persistence_layer: DynamoDBPersistenceLayer,
# lambda_context: LambdaContext,
# ):
# r = app.lambda_handler(
# http_api_proxy(
# raw_path='/orders',
# method=HTTPMethod.POST,
# body={
# 'user_id': '15bacf02-1535-4bee-9022-19d106fd7518',
# 'cpf': '07879819908',
# 'name': 'Sérgio R Siqueira',
# 'email': 'sergio@somosbeta.com.br',
# 'payment_method': 'MANUAL',
# 'address': {
# 'city': 'Curitiba',
# 'postcode': '81280350',
# 'neighborhood': 'Cidade Industrial',
# 'address1': 'Rua Monsenhor Ivo Zanlorenzi',
# 'address2': 'nº 5190, ap 1802',
# 'state': 'PR',
# },
# 'items': [
# {
# 'id': 'e1c44881-2fe3-484e-ada2-12b6bf5b9398',
# 'name': 'NR-35 Segurança nos Trabalhos em Altura',
# 'quantity': 2,
# 'unit_price': 119,
# }
# ],
# },
# ),
# lambda_context,
# )
# print(r)
# assert r['statusCode'] == HTTPStatus.CREATED

View File

@@ -30,20 +30,8 @@ import { initials } from '@repo/ui/lib/utils'
import { Link } from 'react-router'
import type { Workspace, WorkspaceContextProps } from '@/middleware/workspace'
import type { Address } from '@/routes/_.$orgid.enrollments.buy/review'
type Subscription = {
billing_day: number
payment_method: 'PIX' | 'BANK_SLIP' | 'MANUAL'
}
const WorkspaceContext = createContext<
| (WorkspaceContextProps & {
subscription: Subscription | null
address: Address | null
})
| null
>(null)
const WorkspaceContext = createContext<WorkspaceContextProps | null>(null)
export function useWorksapce() {
const ctx = use(WorkspaceContext)
@@ -56,33 +44,12 @@ export function useWorksapce() {
}
export function WorkspaceProvider({
activeWorkspace,
workspaces,
subscription,
address,
children
}: {
activeWorkspace: Workspace
workspaces: Workspace[]
subscription?: Subscription
address?: Address
children,
...props
}: WorkspaceContextProps & {
children: React.ReactNode
}) {
return (
<WorkspaceContext
value={{
activeWorkspace,
workspaces,
address: address && Object.keys(address).length > 0 ? address : null,
subscription:
subscription && Object.keys(subscription).length > 0
? subscription
: null
}}
>
{children}
</WorkspaceContext>
)
return <WorkspaceContext value={props}>{children}</WorkspaceContext>
}
export function WorkspaceSwitcher() {

View File

@@ -3,6 +3,13 @@ import { type LoaderFunctionArgs, createContext } from 'react-router'
import { userContext } from '@repo/auth/context'
import { request as req } from '@repo/util/request'
import type { Address } from '@/routes/_.$orgid.enrollments.buy/review'
export type Subscription = {
billing_day: number
payment_method: 'PIX' | 'BANK_SLIP' | 'MANUAL'
}
export type Workspace = {
id: string
name: string
@@ -12,6 +19,8 @@ export type Workspace = {
export type WorkspaceContextProps = {
activeWorkspace: Workspace
workspaces: Workspace[]
subscription: Subscription | null
address: Address | null
}
export const workspaceContext = createContext<WorkspaceContextProps>()
@@ -43,7 +52,33 @@ export const workspaceMiddleware = async (
({ id }) => id === org_id
) as Workspace
context.set(workspaceContext, { activeWorkspace, workspaces })
const [subscription, address] = await Promise.all([
req({
url: `/orgs/${activeWorkspace.id}/subscription`,
request,
context
})
.then((r) => r.json())
.then(emptyObjectToNull),
req({
url: `/orgs/${activeWorkspace.id}/address`,
request,
context
})
.then((r) => r.json())
.then(emptyObjectToNull)
])
context.set(workspaceContext, {
activeWorkspace,
workspaces,
subscription,
address
})
return await next()
}
const emptyObjectToNull = (data: any) =>
data && Object.keys(data).length === 0 ? null : data

View File

@@ -67,7 +67,6 @@ export default function Route({
loaderData: { enrollments }
}: Route.ComponentProps) {
const { orgid } = useParams()
const { subscription } = useWorksapce()
const [searchParams, setSearchParams] = useSearchParams()
const [selectedRows, setSelectedRows] = useState<Enrollment[]>([])
const status = searchParams.get('status')
@@ -205,7 +204,7 @@ export default function Route({
<DataTableViewOptions className="flex-1" />
<Button className="flex-1" asChild>
<Link to={subscription ? 'add' : 'buy'}>
<Link to="add">
<PlusIcon /> Adicionar
</Link>
</Button>

View File

@@ -72,6 +72,7 @@ import { ScheduledForInput } from './scheduled-for'
import { CoursePicker } from './course-picker'
import { UserPicker } from './user-picker'
import { cn } from '@repo/ui/lib/utils'
import { workspaceContext } from '@/middleware/workspace'
const emptyRow = {
user: undefined,
@@ -84,6 +85,12 @@ export function meta({}: Route.MetaArgs) {
}
export async function loader({ params, context, request }: Route.LoaderArgs) {
const { subscription } = context.get(workspaceContext)
// If there's no subscription for the org, redirect to checkout
if (!subscription) {
throw redirect('../enrollments/buy')
}
const url = new URL(request.url)
const submissionId = url.searchParams.get('submission')
const cloudflare = context.get(cloudflareContext)

View File

@@ -27,32 +27,17 @@ export const middleware: Route.MiddlewareFunction[] = [
workspaceMiddleware
]
export async function loader({ params, context, request }: Route.ActionArgs) {
export async function loader({ context, request }: Route.ActionArgs) {
const user = context.get(userContext)!
const { activeWorkspace, workspaces } = context.get(workspaceContext)
const workspace = context.get(workspaceContext)
const rawCookie = request.headers.get('cookie') || ''
const parsedCookies = cookie.parse(rawCookie)
const { sidebar_state = 'true' } = parsedCookies
const subscription = req({
url: `/orgs/${activeWorkspace.id}/subscription`,
request,
context
}).then((r) => r.json())
const address = req({
url: `/orgs/${activeWorkspace.id}/address`,
request,
context
}).then((r) => r.json())
return {
user,
activeWorkspace,
workspaces,
sidebar_state,
subscription,
address
...workspace
}
}
@@ -64,9 +49,14 @@ export function shouldRevalidate({
}
export default function Route({ loaderData }: Route.ComponentProps) {
const { user, activeWorkspace, workspaces, sidebar_state } = loaderData
const subscription = use(loaderData.subscription)
const address = use(loaderData.address)
const {
user,
activeWorkspace,
workspaces,
subscription,
address,
sidebar_state
} = loaderData
useEffect(() => {
if (typeof window !== 'undefined' && window.rybbit) {