add freeze subscription
This commit is contained in:
@@ -34,6 +34,11 @@ class SubscriptionRequiredError(ServiceError):
|
|||||||
super().__init__(HTTPStatus.NOT_ACCEPTABLE, msg)
|
super().__init__(HTTPStatus.NOT_ACCEPTABLE, msg)
|
||||||
|
|
||||||
|
|
||||||
|
class SubscriptionFrozenError(ServiceError):
|
||||||
|
def __init__(self, msg: str | dict):
|
||||||
|
super().__init__(HTTPStatus.NOT_ACCEPTABLE, msg)
|
||||||
|
|
||||||
|
|
||||||
class DeduplicationConflictError(ConflictError): ...
|
class DeduplicationConflictError(ConflictError): ...
|
||||||
|
|
||||||
|
|
||||||
@@ -90,15 +95,9 @@ def enroll(
|
|||||||
rename_key='subscription',
|
rename_key='subscription',
|
||||||
table_name=USER_TABLE,
|
table_name=USER_TABLE,
|
||||||
)
|
)
|
||||||
+ KeyPair(
|
|
||||||
pk='SUBSCRIPTION',
|
|
||||||
sk=f'ORG#{org_id}',
|
|
||||||
rename_key='subscribed',
|
|
||||||
table_name=USER_TABLE,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if 'subscribed' not in org:
|
if 'subscription' not in org:
|
||||||
raise SubscriptionRequiredError('Organization not subscribed')
|
raise SubscriptionRequiredError('Organization not subscribed')
|
||||||
|
|
||||||
ctx = {
|
ctx = {
|
||||||
@@ -184,6 +183,15 @@ def enroll_now(enrollment: Enrollment, context: Context):
|
|||||||
exc_cls=SubscriptionRequiredError,
|
exc_cls=SubscriptionRequiredError,
|
||||||
table_name=USER_TABLE,
|
table_name=USER_TABLE,
|
||||||
)
|
)
|
||||||
|
transact.condition(
|
||||||
|
key=KeyPair(
|
||||||
|
pk='SUBSCRIPTION#FREEZE',
|
||||||
|
sk=f'ORG#{org.id}',
|
||||||
|
),
|
||||||
|
cond_expr='attribute_not_exists(sk)',
|
||||||
|
exc_cls=SubscriptionFrozenError,
|
||||||
|
table_name=USER_TABLE,
|
||||||
|
)
|
||||||
transact.put(
|
transact.put(
|
||||||
item={
|
item={
|
||||||
'id': enrollment.id,
|
'id': enrollment.id,
|
||||||
@@ -292,6 +300,15 @@ def enroll_later(enrollment: Enrollment, context: Context):
|
|||||||
exc_cls=SubscriptionRequiredError,
|
exc_cls=SubscriptionRequiredError,
|
||||||
table_name=USER_TABLE,
|
table_name=USER_TABLE,
|
||||||
)
|
)
|
||||||
|
transact.condition(
|
||||||
|
key=KeyPair(
|
||||||
|
pk='SUBSCRIPTION#FREEZE',
|
||||||
|
sk=f'ORG#{org.id}',
|
||||||
|
),
|
||||||
|
cond_expr='attribute_not_exists(sk)',
|
||||||
|
exc_cls=SubscriptionFrozenError,
|
||||||
|
table_name=USER_TABLE,
|
||||||
|
)
|
||||||
transact.put(
|
transact.put(
|
||||||
item={
|
item={
|
||||||
'id': pk,
|
'id': pk,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from aws_lambda_powertools import Logger
|
from aws_lambda_powertools import Logger
|
||||||
from aws_lambda_powertools.event_handler.api_gateway import Router
|
from aws_lambda_powertools.event_handler.api_gateway import Router
|
||||||
from layercake.dynamodb import DynamoDBPersistenceLayer, SortKey, TransactKey
|
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair, SortKey, TransactKey
|
||||||
|
|
||||||
from boto3clients import dynamodb_client
|
from boto3clients import dynamodb_client
|
||||||
from config import USER_TABLE
|
from config import USER_TABLE
|
||||||
@@ -44,4 +44,9 @@ def get_org(org_id: str):
|
|||||||
+ SortKey('0')
|
+ SortKey('0')
|
||||||
+ SortKey('METADATA#ADDRESS', rename_key='address')
|
+ SortKey('METADATA#ADDRESS', rename_key='address')
|
||||||
+ SortKey('METADATA#SUBSCRIPTION', rename_key='subscription')
|
+ SortKey('METADATA#SUBSCRIPTION', rename_key='subscription')
|
||||||
|
+ KeyPair(
|
||||||
|
pk='SUBSCRIPTION#FREEZE',
|
||||||
|
sk=SortKey(f'ORG#{org_id}'),
|
||||||
|
rename_key='subscription_freeze',
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -42,15 +42,6 @@ def add(
|
|||||||
cond_expr='attribute_exists(sk)',
|
cond_expr='attribute_exists(sk)',
|
||||||
exc_cls=OrgNotFoundError,
|
exc_cls=OrgNotFoundError,
|
||||||
)
|
)
|
||||||
transact.put(
|
|
||||||
item={
|
|
||||||
'id': 'SUBSCRIPTION',
|
|
||||||
'sk': f'ORG#{org_id}',
|
|
||||||
'name': name,
|
|
||||||
'created_at': now_,
|
|
||||||
},
|
|
||||||
cond_expr='attribute_not_exists(sk)',
|
|
||||||
)
|
|
||||||
transact.put(
|
transact.put(
|
||||||
item={
|
item={
|
||||||
'id': org_id,
|
'id': org_id,
|
||||||
@@ -60,5 +51,14 @@ def add(
|
|||||||
'created_at': now_,
|
'created_at': now_,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
transact.put(
|
||||||
|
item={
|
||||||
|
'id': 'SUBSCRIPTION',
|
||||||
|
'sk': f'ORG#{org_id}',
|
||||||
|
'name': name,
|
||||||
|
'created_at': now_,
|
||||||
|
},
|
||||||
|
cond_expr='attribute_not_exists(sk)',
|
||||||
|
)
|
||||||
|
|
||||||
return JSONResponse(status_code=HTTPStatus.CREATED)
|
return JSONResponse(status_code=HTTPStatus.CREATED)
|
||||||
|
|||||||
@@ -6,6 +6,25 @@ from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair, PartitionKey
|
|||||||
from ..conftest import HttpApiProxy, LambdaContext
|
from ..conftest import HttpApiProxy, LambdaContext
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_org(
|
||||||
|
app,
|
||||||
|
seeds,
|
||||||
|
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
|
||||||
|
http_api_proxy: HttpApiProxy,
|
||||||
|
lambda_context: LambdaContext,
|
||||||
|
):
|
||||||
|
r = app.lambda_handler(
|
||||||
|
http_api_proxy(
|
||||||
|
raw_path='/orgs/2a8963fc-4694-4fe2-953a-316d1b10f1f5',
|
||||||
|
method=HTTPMethod.GET,
|
||||||
|
),
|
||||||
|
lambda_context,
|
||||||
|
)
|
||||||
|
body = json.loads(r['body'])
|
||||||
|
|
||||||
|
assert 'subscription_freeze' in body
|
||||||
|
|
||||||
|
|
||||||
def test_add_org(
|
def test_add_org(
|
||||||
app,
|
app,
|
||||||
seeds,
|
seeds,
|
||||||
|
|||||||
@@ -38,6 +38,7 @@
|
|||||||
{"id": "cnpj", "sk": "00000000000191", "org_id": "6000f79-6e5c-49a0-952f-3bda330ef278"}
|
{"id": "cnpj", "sk": "00000000000191", "org_id": "6000f79-6e5c-49a0-952f-3bda330ef278"}
|
||||||
{"id": "SUBSCRIPTION", "sk": "ORG#2a8963fc-4694-4fe2-953a-316d1b10f1f5"}
|
{"id": "SUBSCRIPTION", "sk": "ORG#2a8963fc-4694-4fe2-953a-316d1b10f1f5"}
|
||||||
{"id": "SUBSCRIPTION", "sk": "ORG#cJtK9SsnJhKPyxESe7g3DG"}
|
{"id": "SUBSCRIPTION", "sk": "ORG#cJtK9SsnJhKPyxESe7g3DG"}
|
||||||
|
{"id": "SUBSCRIPTION#FROZEN", "sk": "ORG#2a8963fc-4694-4fe2-953a-316d1b10f1f5", "frozen": true, "created_at": "2025-12-24T00:05:27-03:00"}
|
||||||
|
|
||||||
// CPFs
|
// CPFs
|
||||||
{"id": "cpf", "sk": "07879819908", "user_id": "15bacf02-1535-4bee-9022-19d106fd7518"}
|
{"id": "cpf", "sk": "07879819908", "user_id": "15bacf02-1535-4bee-9022-19d106fd7518"}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { formatCNPJ } from '@brazilian-utils/brazilian-utils'
|
import { formatCNPJ } from '@brazilian-utils/brazilian-utils'
|
||||||
import { IconRosetteDiscountCheckFilled } from '@tabler/icons-react'
|
|
||||||
import {
|
import {
|
||||||
BadgeCheckIcon,
|
BadgeCheckIcon,
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
|
|||||||
@@ -24,7 +24,9 @@ export type WorkspaceContextProps = {
|
|||||||
address: Address | null
|
address: Address | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export const workspaceContext = createContext<WorkspaceContextProps>()
|
export const workspaceContext = createContext<
|
||||||
|
WorkspaceContextProps & { blocked: boolean }
|
||||||
|
>()
|
||||||
|
|
||||||
export const workspaceMiddleware = async (
|
export const workspaceMiddleware = async (
|
||||||
{ params, request, context }: LoaderFunctionArgs,
|
{ params, request, context }: LoaderFunctionArgs,
|
||||||
@@ -63,7 +65,8 @@ export const workspaceMiddleware = async (
|
|||||||
activeWorkspace,
|
activeWorkspace,
|
||||||
workspaces,
|
workspaces,
|
||||||
subscription: org?.['subscription'] || null,
|
subscription: org?.['subscription'] || null,
|
||||||
address: org?.['address'] || null
|
address: org?.['address'] || null,
|
||||||
|
blocked: 'subscription_freeze' in org
|
||||||
})
|
})
|
||||||
|
|
||||||
return await next()
|
return await next()
|
||||||
|
|||||||
@@ -8,12 +8,19 @@ import { userContext } from '@repo/auth/context'
|
|||||||
import { authMiddleware } from '@repo/auth/middleware/auth'
|
import { authMiddleware } from '@repo/auth/middleware/auth'
|
||||||
import { ModeToggle, ThemedImage } from '@repo/ui/components/dark-mode'
|
import { ModeToggle, ThemedImage } from '@repo/ui/components/dark-mode'
|
||||||
import { NavUser } from '@repo/ui/components/nav-user'
|
import { NavUser } from '@repo/ui/components/nav-user'
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogTitle
|
||||||
|
} from '@repo/ui/components/ui/alert-dialog'
|
||||||
import {
|
import {
|
||||||
SidebarInset,
|
SidebarInset,
|
||||||
SidebarProvider,
|
SidebarProvider,
|
||||||
SidebarTrigger
|
SidebarTrigger
|
||||||
} from '@repo/ui/components/ui/sidebar'
|
} from '@repo/ui/components/ui/sidebar'
|
||||||
import { Toaster } from '@repo/ui/components/ui/sonner'
|
import { Toaster } from '@repo/ui/components/ui/sonner'
|
||||||
|
import { cn } from '@repo/ui/lib/utils'
|
||||||
|
|
||||||
import { AppSidebar } from '@/components/app-sidebar'
|
import { AppSidebar } from '@/components/app-sidebar'
|
||||||
import { WorkspaceProvider } from '@/components/workspace-switcher'
|
import { WorkspaceProvider } from '@/components/workspace-switcher'
|
||||||
@@ -48,14 +55,7 @@ export function shouldRevalidate({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Route({ loaderData }: Route.ComponentProps) {
|
export default function Route({ loaderData }: Route.ComponentProps) {
|
||||||
const {
|
const { user, sidebar_state, blocked, ...props } = loaderData
|
||||||
user,
|
|
||||||
activeWorkspace,
|
|
||||||
workspaces,
|
|
||||||
subscription,
|
|
||||||
address,
|
|
||||||
sidebar_state
|
|
||||||
} = loaderData
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window !== 'undefined' && window.rybbit) {
|
if (typeof window !== 'undefined' && window.rybbit) {
|
||||||
@@ -68,13 +68,23 @@ export default function Route({ loaderData }: Route.ComponentProps) {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WorkspaceProvider
|
<WorkspaceProvider {...props}>
|
||||||
activeWorkspace={activeWorkspace}
|
{blocked ? (
|
||||||
workspaces={workspaces}
|
<AlertDialog open={true}>
|
||||||
subscription={subscription}
|
<AlertDialogContent>
|
||||||
address={address}
|
<AlertDialogTitle>Serviço com acesso suspenso</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
Seu acesso está temporariamente bloqueado devido a um pagamento em
|
||||||
|
atraso. Regularize para continuar usando a plataforma.
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<SidebarProvider
|
||||||
|
defaultOpen={sidebar_state === 'true'}
|
||||||
|
className={cn('flex', blocked && 'pointer-events-none')}
|
||||||
>
|
>
|
||||||
<SidebarProvider defaultOpen={sidebar_state === 'true'} className="flex">
|
|
||||||
<AppSidebar />
|
<AppSidebar />
|
||||||
|
|
||||||
<SidebarInset className="relative flex flex-col flex-1 min-w-0">
|
<SidebarInset className="relative flex flex-col flex-1 min-w-0">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { type ColumnDef } from '@tanstack/react-table'
|
import { type ColumnDef } from '@tanstack/react-table'
|
||||||
import { EllipsisIcon, PencilIcon } from 'lucide-react'
|
import { BadgeCheckIcon, EllipsisIcon, PencilIcon } from 'lucide-react'
|
||||||
import { NavLink } from 'react-router'
|
import { NavLink } from 'react-router'
|
||||||
|
|
||||||
import { Abbr } from '@repo/ui/components/abbr'
|
import { Abbr } from '@repo/ui/components/abbr'
|
||||||
@@ -38,13 +38,20 @@ export const columns: ColumnDef<Org>[] = [
|
|||||||
{
|
{
|
||||||
header: 'Empresa',
|
header: 'Empresa',
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const { name, email } = row.original
|
const { name, email, subscription_covered } = row.original
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-2.5 items-center">
|
<div className="flex gap-2.5 items-center">
|
||||||
|
<div className="relative">
|
||||||
|
{subscription_covered ? (
|
||||||
|
<BadgeCheckIcon className="fill-blue-500 stroke-white absolute size-4 dark:size-3.5 -top-0 -right-0 z-2" />
|
||||||
|
) : null}
|
||||||
<Avatar className="size-10 hidden lg:block">
|
<Avatar className="size-10 hidden lg:block">
|
||||||
<AvatarFallback className="border">{initials(name)}</AvatarFallback>
|
<AvatarFallback className="border">
|
||||||
|
{initials(name)}
|
||||||
|
</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
<li className="font-bold">
|
<li className="font-bold">
|
||||||
|
|||||||
@@ -5,4 +5,5 @@ export type Org = {
|
|||||||
name: string
|
name: string
|
||||||
email: string
|
email: string
|
||||||
cnpj?: string
|
cnpj?: string
|
||||||
|
subscription_covered?: boolean
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user