From ae348377a56704348953399b3bf7ef985ed61790 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Rafael=20Siqueira?= Date: Sun, 18 Jan 2026 12:34:19 -0300 Subject: [PATCH] add freeze subscription --- .../app/routes/enrollments/enroll.py | 31 ++++++++++---- .../app/routes/orgs/__init__.py | 7 +++- .../app/routes/orgs/subscription.py | 18 ++++----- .../tests/routes/test_orgs.py | 19 +++++++++ api.saladeaula.digital/tests/seeds.jsonl | 1 + .../app/components/workspace-switcher.tsx | 1 - .../app/middleware/workspace.ts | 7 +++- .../app/routes/_.$orgid/route.tsx | 40 ++++++++++++------- .../app/routes/_app.orgs._index/columns.tsx | 17 +++++--- packages/ui/src/routes/orgs/data.tsx | 1 + 10 files changed, 102 insertions(+), 40 deletions(-) diff --git a/api.saladeaula.digital/app/routes/enrollments/enroll.py b/api.saladeaula.digital/app/routes/enrollments/enroll.py index 9452e6a..6f80576 100644 --- a/api.saladeaula.digital/app/routes/enrollments/enroll.py +++ b/api.saladeaula.digital/app/routes/enrollments/enroll.py @@ -34,6 +34,11 @@ class SubscriptionRequiredError(ServiceError): super().__init__(HTTPStatus.NOT_ACCEPTABLE, msg) +class SubscriptionFrozenError(ServiceError): + def __init__(self, msg: str | dict): + super().__init__(HTTPStatus.NOT_ACCEPTABLE, msg) + + class DeduplicationConflictError(ConflictError): ... @@ -90,15 +95,9 @@ def enroll( rename_key='subscription', 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') ctx = { @@ -184,6 +183,15 @@ def enroll_now(enrollment: Enrollment, context: Context): exc_cls=SubscriptionRequiredError, 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( item={ 'id': enrollment.id, @@ -292,6 +300,15 @@ def enroll_later(enrollment: Enrollment, context: Context): exc_cls=SubscriptionRequiredError, 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( item={ 'id': pk, diff --git a/api.saladeaula.digital/app/routes/orgs/__init__.py b/api.saladeaula.digital/app/routes/orgs/__init__.py index c7e1589..b01ed28 100644 --- a/api.saladeaula.digital/app/routes/orgs/__init__.py +++ b/api.saladeaula.digital/app/routes/orgs/__init__.py @@ -1,6 +1,6 @@ from aws_lambda_powertools import Logger 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 config import USER_TABLE @@ -44,4 +44,9 @@ def get_org(org_id: str): + SortKey('0') + SortKey('METADATA#ADDRESS', rename_key='address') + SortKey('METADATA#SUBSCRIPTION', rename_key='subscription') + + KeyPair( + pk='SUBSCRIPTION#FREEZE', + sk=SortKey(f'ORG#{org_id}'), + rename_key='subscription_freeze', + ) ) diff --git a/api.saladeaula.digital/app/routes/orgs/subscription.py b/api.saladeaula.digital/app/routes/orgs/subscription.py index fc7123d..f86a3a5 100644 --- a/api.saladeaula.digital/app/routes/orgs/subscription.py +++ b/api.saladeaula.digital/app/routes/orgs/subscription.py @@ -42,15 +42,6 @@ def add( cond_expr='attribute_exists(sk)', 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( item={ 'id': org_id, @@ -60,5 +51,14 @@ def add( '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) diff --git a/api.saladeaula.digital/tests/routes/test_orgs.py b/api.saladeaula.digital/tests/routes/test_orgs.py index 9872eb3..40ceb9b 100644 --- a/api.saladeaula.digital/tests/routes/test_orgs.py +++ b/api.saladeaula.digital/tests/routes/test_orgs.py @@ -6,6 +6,25 @@ from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair, PartitionKey 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( app, seeds, diff --git a/api.saladeaula.digital/tests/seeds.jsonl b/api.saladeaula.digital/tests/seeds.jsonl index de7745c..4027815 100644 --- a/api.saladeaula.digital/tests/seeds.jsonl +++ b/api.saladeaula.digital/tests/seeds.jsonl @@ -38,6 +38,7 @@ {"id": "cnpj", "sk": "00000000000191", "org_id": "6000f79-6e5c-49a0-952f-3bda330ef278"} {"id": "SUBSCRIPTION", "sk": "ORG#2a8963fc-4694-4fe2-953a-316d1b10f1f5"} {"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 {"id": "cpf", "sk": "07879819908", "user_id": "15bacf02-1535-4bee-9022-19d106fd7518"} diff --git a/apps/admin.saladeaula.digital/app/components/workspace-switcher.tsx b/apps/admin.saladeaula.digital/app/components/workspace-switcher.tsx index 9ea7ca1..aa80352 100644 --- a/apps/admin.saladeaula.digital/app/components/workspace-switcher.tsx +++ b/apps/admin.saladeaula.digital/app/components/workspace-switcher.tsx @@ -1,7 +1,6 @@ 'use client' import { formatCNPJ } from '@brazilian-utils/brazilian-utils' -import { IconRosetteDiscountCheckFilled } from '@tabler/icons-react' import { BadgeCheckIcon, CheckIcon, diff --git a/apps/admin.saladeaula.digital/app/middleware/workspace.ts b/apps/admin.saladeaula.digital/app/middleware/workspace.ts index 9c69f20..ecc1c8f 100644 --- a/apps/admin.saladeaula.digital/app/middleware/workspace.ts +++ b/apps/admin.saladeaula.digital/app/middleware/workspace.ts @@ -24,7 +24,9 @@ export type WorkspaceContextProps = { address: Address | null } -export const workspaceContext = createContext() +export const workspaceContext = createContext< + WorkspaceContextProps & { blocked: boolean } +>() export const workspaceMiddleware = async ( { params, request, context }: LoaderFunctionArgs, @@ -63,7 +65,8 @@ export const workspaceMiddleware = async ( activeWorkspace, workspaces, subscription: org?.['subscription'] || null, - address: org?.['address'] || null + address: org?.['address'] || null, + blocked: 'subscription_freeze' in org }) return await next() diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid/route.tsx b/apps/admin.saladeaula.digital/app/routes/_.$orgid/route.tsx index 4a6baa4..eaa68b1 100644 --- a/apps/admin.saladeaula.digital/app/routes/_.$orgid/route.tsx +++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid/route.tsx @@ -8,12 +8,19 @@ import { userContext } from '@repo/auth/context' import { authMiddleware } from '@repo/auth/middleware/auth' import { ModeToggle, ThemedImage } from '@repo/ui/components/dark-mode' import { NavUser } from '@repo/ui/components/nav-user' +import { + AlertDialog, + AlertDialogContent, + AlertDialogDescription, + AlertDialogTitle +} from '@repo/ui/components/ui/alert-dialog' import { SidebarInset, SidebarProvider, SidebarTrigger } from '@repo/ui/components/ui/sidebar' import { Toaster } from '@repo/ui/components/ui/sonner' +import { cn } from '@repo/ui/lib/utils' import { AppSidebar } from '@/components/app-sidebar' import { WorkspaceProvider } from '@/components/workspace-switcher' @@ -48,14 +55,7 @@ export function shouldRevalidate({ } export default function Route({ loaderData }: Route.ComponentProps) { - const { - user, - activeWorkspace, - workspaces, - subscription, - address, - sidebar_state - } = loaderData + const { user, sidebar_state, blocked, ...props } = loaderData useEffect(() => { if (typeof window !== 'undefined' && window.rybbit) { @@ -68,13 +68,23 @@ export default function Route({ loaderData }: Route.ComponentProps) { }, []) return ( - - + + {blocked ? ( + + + Serviço com acesso suspenso + + Seu acesso está temporariamente bloqueado devido a um pagamento em + atraso. Regularize para continuar usando a plataforma. + + + + ) : null} + + diff --git a/apps/insights.saladeaula.digital/app/routes/_app.orgs._index/columns.tsx b/apps/insights.saladeaula.digital/app/routes/_app.orgs._index/columns.tsx index 6900617..45c1797 100644 --- a/apps/insights.saladeaula.digital/app/routes/_app.orgs._index/columns.tsx +++ b/apps/insights.saladeaula.digital/app/routes/_app.orgs._index/columns.tsx @@ -1,7 +1,7 @@ 'use client' 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 { Abbr } from '@repo/ui/components/abbr' @@ -38,13 +38,20 @@ export const columns: ColumnDef[] = [ { header: 'Empresa', cell: ({ row }) => { - const { name, email } = row.original + const { name, email, subscription_covered } = row.original return (
- - {initials(name)} - +
+ {subscription_covered ? ( + + ) : null} + + + {initials(name)} + + +
  • diff --git a/packages/ui/src/routes/orgs/data.tsx b/packages/ui/src/routes/orgs/data.tsx index c3e7bd2..693f1e8 100644 --- a/packages/ui/src/routes/orgs/data.tsx +++ b/packages/ui/src/routes/orgs/data.tsx @@ -5,4 +5,5 @@ export type Org = { name: string email: string cnpj?: string + subscription_covered?: boolean }