fix redirect to checkout when the org has not a subscription
This commit is contained in:
@@ -1,10 +1,11 @@
|
|||||||
import re
|
import re
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from http import HTTPStatus
|
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.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.dynamodb import DynamoDBPersistenceLayer
|
||||||
from layercake.extra_types import CnpjStr, CpfStr, NameStr
|
from layercake.extra_types import CnpjStr, CpfStr, NameStr
|
||||||
from pydantic import (
|
from pydantic import (
|
||||||
@@ -12,6 +13,7 @@ from pydantic import (
|
|||||||
BaseModel,
|
BaseModel,
|
||||||
ConfigDict,
|
ConfigDict,
|
||||||
EmailStr,
|
EmailStr,
|
||||||
|
Field,
|
||||||
field_validator,
|
field_validator,
|
||||||
model_validator,
|
model_validator,
|
||||||
)
|
)
|
||||||
@@ -20,6 +22,7 @@ from api_gateway import JSONResponse
|
|||||||
from boto3clients import dynamodb_client
|
from boto3clients import dynamodb_client
|
||||||
from config import ORDER_TABLE
|
from config import ORDER_TABLE
|
||||||
from routes.enrollments.enroll import Enrollment
|
from routes.enrollments.enroll import Enrollment
|
||||||
|
from routes.orgs.address import address
|
||||||
|
|
||||||
router = Router()
|
router = Router()
|
||||||
dyn = DynamoDBPersistenceLayer(ORDER_TABLE, dynamodb_client)
|
dyn = DynamoDBPersistenceLayer(ORDER_TABLE, dynamodb_client)
|
||||||
@@ -62,6 +65,7 @@ class Coupon(BaseModel):
|
|||||||
class Checkout(BaseModel):
|
class Checkout(BaseModel):
|
||||||
model_config = ConfigDict(str_strip_whitespace=True)
|
model_config = ConfigDict(str_strip_whitespace=True)
|
||||||
|
|
||||||
|
id: UUID4 = Field(default_factory=uuid4)
|
||||||
name: str
|
name: str
|
||||||
email: EmailStr
|
email: EmailStr
|
||||||
address: Address
|
address: Address
|
||||||
@@ -69,11 +73,11 @@ class Checkout(BaseModel):
|
|||||||
items: tuple[Item, ...]
|
items: tuple[Item, ...]
|
||||||
enrollments: tuple[Enrollment, ...] | None = None
|
enrollments: tuple[Enrollment, ...] | None = None
|
||||||
coupon: Coupon | None = None
|
coupon: Coupon | None = None
|
||||||
user: User | None = None
|
|
||||||
org_id: UUID4 | str | None = None
|
org_id: UUID4 | str | None = None
|
||||||
user_id: UUID4 | str | None = None
|
user_id: UUID4 | str | None = None
|
||||||
cnpj: CnpjStr | None = None
|
cnpj: CnpjStr | None = None
|
||||||
cpf: CpfStr | None = None
|
cpf: CpfStr | None = None
|
||||||
|
created_by: User | None = None
|
||||||
|
|
||||||
@model_validator(mode='after')
|
@model_validator(mode='after')
|
||||||
def verify_fields(self):
|
def verify_fields(self):
|
||||||
@@ -84,15 +88,67 @@ class Checkout(BaseModel):
|
|||||||
if self.org_id is None:
|
if self.org_id is None:
|
||||||
raise ValueError('org_id is missing')
|
raise ValueError('org_id is missing')
|
||||||
|
|
||||||
if self.user is None:
|
if self.created_by is None:
|
||||||
raise ValueError('user is missing')
|
raise ValueError('created_by is missing')
|
||||||
|
|
||||||
if self.cpf is not None and self.user_id is None:
|
if self.cpf is not None and self.user_id is None:
|
||||||
raise ValueError('user_id is missing')
|
raise ValueError('user_id is missing')
|
||||||
|
|
||||||
return self
|
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('/')
|
@router.post('/')
|
||||||
def checkout(body: Annotated[Checkout, Body()]):
|
def checkout(payload: Checkout):
|
||||||
return JSONResponse(status_code=HTTPStatus.CREATED)
|
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)
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
import json
|
||||||
from http import HTTPMethod, HTTPStatus
|
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
|
from ...conftest import HttpApiProxy, LambdaContext
|
||||||
|
|
||||||
@@ -20,9 +22,9 @@ def test_checkout(
|
|||||||
'org_id': 'f6000f79-6e5c-49a0-952f-3bda330ef278',
|
'org_id': 'f6000f79-6e5c-49a0-952f-3bda330ef278',
|
||||||
'cnpj': '00000000000191',
|
'cnpj': '00000000000191',
|
||||||
'name': 'Branco do Brasil',
|
'name': 'Branco do Brasil',
|
||||||
'email': 'sergio@somosbeta.com.br',
|
'email': 'bb@users.noreply.saladeaula.digital',
|
||||||
'payment_method': 'MANUAL',
|
'payment_method': 'BANK_SLIP',
|
||||||
'user': {
|
'created_by': {
|
||||||
'id': '15bacf02-1535-4bee-9022-19d106fd7518',
|
'id': '15bacf02-1535-4bee-9022-19d106fd7518',
|
||||||
'name': 'Sérgio R Siqueira',
|
'name': 'Sérgio R Siqueira',
|
||||||
},
|
},
|
||||||
@@ -46,46 +48,50 @@ def test_checkout(
|
|||||||
),
|
),
|
||||||
lambda_context,
|
lambda_context,
|
||||||
)
|
)
|
||||||
print(r)
|
body = json.loads(r['body'])
|
||||||
|
print(body)
|
||||||
assert r['statusCode'] == HTTPStatus.CREATED
|
assert r['statusCode'] == HTTPStatus.CREATED
|
||||||
|
|
||||||
|
r = dynamodb_persistence_layer.collection.query(PartitionKey(body['id']))
|
||||||
|
pprint(r['items'])
|
||||||
|
|
||||||
def test_checkout_from_user(
|
|
||||||
app,
|
# def test_checkout_from_user(
|
||||||
seeds,
|
# app,
|
||||||
http_api_proxy: HttpApiProxy,
|
# seeds,
|
||||||
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
|
# http_api_proxy: HttpApiProxy,
|
||||||
lambda_context: LambdaContext,
|
# dynamodb_persistence_layer: DynamoDBPersistenceLayer,
|
||||||
):
|
# lambda_context: LambdaContext,
|
||||||
r = app.lambda_handler(
|
# ):
|
||||||
http_api_proxy(
|
# r = app.lambda_handler(
|
||||||
raw_path='/orders',
|
# http_api_proxy(
|
||||||
method=HTTPMethod.POST,
|
# raw_path='/orders',
|
||||||
body={
|
# method=HTTPMethod.POST,
|
||||||
'user_id': '15bacf02-1535-4bee-9022-19d106fd7518',
|
# body={
|
||||||
'cpf': '07879819908',
|
# 'user_id': '15bacf02-1535-4bee-9022-19d106fd7518',
|
||||||
'name': 'Sérgio R Siqueira',
|
# 'cpf': '07879819908',
|
||||||
'email': 'sergio@somosbeta.com.br',
|
# 'name': 'Sérgio R Siqueira',
|
||||||
'payment_method': 'MANUAL',
|
# 'email': 'sergio@somosbeta.com.br',
|
||||||
'address': {
|
# 'payment_method': 'MANUAL',
|
||||||
'city': 'Curitiba',
|
# 'address': {
|
||||||
'postcode': '81280350',
|
# 'city': 'Curitiba',
|
||||||
'neighborhood': 'Cidade Industrial',
|
# 'postcode': '81280350',
|
||||||
'address1': 'Rua Monsenhor Ivo Zanlorenzi',
|
# 'neighborhood': 'Cidade Industrial',
|
||||||
'address2': 'nº 5190, ap 1802',
|
# 'address1': 'Rua Monsenhor Ivo Zanlorenzi',
|
||||||
'state': 'PR',
|
# 'address2': 'nº 5190, ap 1802',
|
||||||
},
|
# 'state': 'PR',
|
||||||
'items': [
|
# },
|
||||||
{
|
# 'items': [
|
||||||
'id': 'e1c44881-2fe3-484e-ada2-12b6bf5b9398',
|
# {
|
||||||
'name': 'NR-35 Segurança nos Trabalhos em Altura',
|
# 'id': 'e1c44881-2fe3-484e-ada2-12b6bf5b9398',
|
||||||
'quantity': 2,
|
# 'name': 'NR-35 Segurança nos Trabalhos em Altura',
|
||||||
'unit_price': 119,
|
# 'quantity': 2,
|
||||||
}
|
# 'unit_price': 119,
|
||||||
],
|
# }
|
||||||
},
|
# ],
|
||||||
),
|
# },
|
||||||
lambda_context,
|
# ),
|
||||||
)
|
# lambda_context,
|
||||||
print(r)
|
# )
|
||||||
assert r['statusCode'] == HTTPStatus.CREATED
|
# print(r)
|
||||||
|
# assert r['statusCode'] == HTTPStatus.CREATED
|
||||||
|
|||||||
@@ -30,20 +30,8 @@ import { initials } from '@repo/ui/lib/utils'
|
|||||||
import { Link } from 'react-router'
|
import { Link } from 'react-router'
|
||||||
|
|
||||||
import type { Workspace, WorkspaceContextProps } from '@/middleware/workspace'
|
import type { Workspace, WorkspaceContextProps } from '@/middleware/workspace'
|
||||||
import type { Address } from '@/routes/_.$orgid.enrollments.buy/review'
|
|
||||||
|
|
||||||
type Subscription = {
|
const WorkspaceContext = createContext<WorkspaceContextProps | null>(null)
|
||||||
billing_day: number
|
|
||||||
payment_method: 'PIX' | 'BANK_SLIP' | 'MANUAL'
|
|
||||||
}
|
|
||||||
|
|
||||||
const WorkspaceContext = createContext<
|
|
||||||
| (WorkspaceContextProps & {
|
|
||||||
subscription: Subscription | null
|
|
||||||
address: Address | null
|
|
||||||
})
|
|
||||||
| null
|
|
||||||
>(null)
|
|
||||||
|
|
||||||
export function useWorksapce() {
|
export function useWorksapce() {
|
||||||
const ctx = use(WorkspaceContext)
|
const ctx = use(WorkspaceContext)
|
||||||
@@ -56,33 +44,12 @@ export function useWorksapce() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function WorkspaceProvider({
|
export function WorkspaceProvider({
|
||||||
activeWorkspace,
|
children,
|
||||||
workspaces,
|
...props
|
||||||
subscription,
|
}: WorkspaceContextProps & {
|
||||||
address,
|
|
||||||
children
|
|
||||||
}: {
|
|
||||||
activeWorkspace: Workspace
|
|
||||||
workspaces: Workspace[]
|
|
||||||
subscription?: Subscription
|
|
||||||
address?: Address
|
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}) {
|
}) {
|
||||||
return (
|
return <WorkspaceContext value={props}>{children}</WorkspaceContext>
|
||||||
<WorkspaceContext
|
|
||||||
value={{
|
|
||||||
activeWorkspace,
|
|
||||||
workspaces,
|
|
||||||
address: address && Object.keys(address).length > 0 ? address : null,
|
|
||||||
subscription:
|
|
||||||
subscription && Object.keys(subscription).length > 0
|
|
||||||
? subscription
|
|
||||||
: null
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</WorkspaceContext>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function WorkspaceSwitcher() {
|
export function WorkspaceSwitcher() {
|
||||||
|
|||||||
@@ -3,6 +3,13 @@ import { type LoaderFunctionArgs, createContext } from 'react-router'
|
|||||||
import { userContext } from '@repo/auth/context'
|
import { userContext } from '@repo/auth/context'
|
||||||
import { request as req } from '@repo/util/request'
|
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 = {
|
export type Workspace = {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
@@ -12,6 +19,8 @@ export type Workspace = {
|
|||||||
export type WorkspaceContextProps = {
|
export type WorkspaceContextProps = {
|
||||||
activeWorkspace: Workspace
|
activeWorkspace: Workspace
|
||||||
workspaces: Workspace[]
|
workspaces: Workspace[]
|
||||||
|
subscription: Subscription | null
|
||||||
|
address: Address | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export const workspaceContext = createContext<WorkspaceContextProps>()
|
export const workspaceContext = createContext<WorkspaceContextProps>()
|
||||||
@@ -43,7 +52,33 @@ export const workspaceMiddleware = async (
|
|||||||
({ id }) => id === org_id
|
({ id }) => id === org_id
|
||||||
) as Workspace
|
) 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()
|
return await next()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const emptyObjectToNull = (data: any) =>
|
||||||
|
data && Object.keys(data).length === 0 ? null : data
|
||||||
|
|||||||
@@ -67,7 +67,6 @@ export default function Route({
|
|||||||
loaderData: { enrollments }
|
loaderData: { enrollments }
|
||||||
}: Route.ComponentProps) {
|
}: Route.ComponentProps) {
|
||||||
const { orgid } = useParams()
|
const { orgid } = useParams()
|
||||||
const { subscription } = useWorksapce()
|
|
||||||
const [searchParams, setSearchParams] = useSearchParams()
|
const [searchParams, setSearchParams] = useSearchParams()
|
||||||
const [selectedRows, setSelectedRows] = useState<Enrollment[]>([])
|
const [selectedRows, setSelectedRows] = useState<Enrollment[]>([])
|
||||||
const status = searchParams.get('status')
|
const status = searchParams.get('status')
|
||||||
@@ -205,7 +204,7 @@ export default function Route({
|
|||||||
<DataTableViewOptions className="flex-1" />
|
<DataTableViewOptions className="flex-1" />
|
||||||
|
|
||||||
<Button className="flex-1" asChild>
|
<Button className="flex-1" asChild>
|
||||||
<Link to={subscription ? 'add' : 'buy'}>
|
<Link to="add">
|
||||||
<PlusIcon /> Adicionar
|
<PlusIcon /> Adicionar
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ import { ScheduledForInput } from './scheduled-for'
|
|||||||
import { CoursePicker } from './course-picker'
|
import { CoursePicker } from './course-picker'
|
||||||
import { UserPicker } from './user-picker'
|
import { UserPicker } from './user-picker'
|
||||||
import { cn } from '@repo/ui/lib/utils'
|
import { cn } from '@repo/ui/lib/utils'
|
||||||
|
import { workspaceContext } from '@/middleware/workspace'
|
||||||
|
|
||||||
const emptyRow = {
|
const emptyRow = {
|
||||||
user: undefined,
|
user: undefined,
|
||||||
@@ -84,6 +85,12 @@ export function meta({}: Route.MetaArgs) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function loader({ params, context, request }: Route.LoaderArgs) {
|
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 url = new URL(request.url)
|
||||||
const submissionId = url.searchParams.get('submission')
|
const submissionId = url.searchParams.get('submission')
|
||||||
const cloudflare = context.get(cloudflareContext)
|
const cloudflare = context.get(cloudflareContext)
|
||||||
|
|||||||
@@ -27,32 +27,17 @@ export const middleware: Route.MiddlewareFunction[] = [
|
|||||||
workspaceMiddleware
|
workspaceMiddleware
|
||||||
]
|
]
|
||||||
|
|
||||||
export async function loader({ params, context, request }: Route.ActionArgs) {
|
export async function loader({ context, request }: Route.ActionArgs) {
|
||||||
const user = context.get(userContext)!
|
const user = context.get(userContext)!
|
||||||
const { activeWorkspace, workspaces } = context.get(workspaceContext)
|
const workspace = context.get(workspaceContext)
|
||||||
const rawCookie = request.headers.get('cookie') || ''
|
const rawCookie = request.headers.get('cookie') || ''
|
||||||
const parsedCookies = cookie.parse(rawCookie)
|
const parsedCookies = cookie.parse(rawCookie)
|
||||||
const { sidebar_state = 'true' } = parsedCookies
|
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 {
|
return {
|
||||||
user,
|
user,
|
||||||
activeWorkspace,
|
|
||||||
workspaces,
|
|
||||||
sidebar_state,
|
sidebar_state,
|
||||||
subscription,
|
...workspace
|
||||||
address
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,9 +49,14 @@ export function shouldRevalidate({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Route({ loaderData }: Route.ComponentProps) {
|
export default function Route({ loaderData }: Route.ComponentProps) {
|
||||||
const { user, activeWorkspace, workspaces, sidebar_state } = loaderData
|
const {
|
||||||
const subscription = use(loaderData.subscription)
|
user,
|
||||||
const address = use(loaderData.address)
|
activeWorkspace,
|
||||||
|
workspaces,
|
||||||
|
subscription,
|
||||||
|
address,
|
||||||
|
sidebar_state
|
||||||
|
} = loaderData
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window !== 'undefined' && window.rybbit) {
|
if (typeof window !== 'undefined' && window.rybbit) {
|
||||||
|
|||||||
Reference in New Issue
Block a user