@greatapps/common 1.1.743 → 1.1.745

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/dist/components/modals/AccountFrozenModal.mjs +48 -0
  2. package/dist/components/modals/AccountFrozenModal.mjs.map +1 -0
  3. package/dist/components/navigation/FreezeWarningBanner.mjs +47 -0
  4. package/dist/components/navigation/FreezeWarningBanner.mjs.map +1 -0
  5. package/dist/components/navigation/FrozenAccountBanner.mjs +38 -0
  6. package/dist/components/navigation/FrozenAccountBanner.mjs.map +1 -0
  7. package/dist/components/navigation/SubscriptionBanner.mjs +17 -0
  8. package/dist/components/navigation/SubscriptionBanner.mjs.map +1 -1
  9. package/dist/handlers.mjs +2 -0
  10. package/dist/handlers.mjs.map +1 -1
  11. package/dist/i18n/messages/en-us.mjs +23 -0
  12. package/dist/i18n/messages/en-us.mjs.map +1 -1
  13. package/dist/i18n/messages/es-es.mjs +23 -0
  14. package/dist/i18n/messages/es-es.mjs.map +1 -1
  15. package/dist/i18n/messages/pt-br.mjs +25 -0
  16. package/dist/i18n/messages/pt-br.mjs.map +1 -1
  17. package/dist/index.mjs +59 -37
  18. package/dist/index.mjs.map +1 -1
  19. package/dist/modules/auth/utils/assert-account-not-frozen.mjs +26 -0
  20. package/dist/modules/auth/utils/assert-account-not-frozen.mjs.map +1 -0
  21. package/dist/modules/plans/utils/map-api-plan-to-ui.mjs +14 -2
  22. package/dist/modules/plans/utils/map-api-plan-to-ui.mjs.map +1 -1
  23. package/dist/modules/projects/services/projects.service.mjs +3 -0
  24. package/dist/modules/projects/services/projects.service.mjs.map +1 -1
  25. package/dist/modules/subscriptions/actions/ack-freeze-notice.action.mjs +10 -0
  26. package/dist/modules/subscriptions/actions/ack-freeze-notice.action.mjs.map +1 -0
  27. package/dist/modules/subscriptions/constants/query-keys.constants.mjs +2 -0
  28. package/dist/modules/subscriptions/constants/query-keys.constants.mjs.map +1 -1
  29. package/dist/modules/subscriptions/constants/subscription.constants.mjs +6 -0
  30. package/dist/modules/subscriptions/constants/subscription.constants.mjs.map +1 -1
  31. package/dist/modules/subscriptions/handlers/freeze-notice.handler.mjs +12 -0
  32. package/dist/modules/subscriptions/handlers/freeze-notice.handler.mjs.map +1 -0
  33. package/dist/modules/subscriptions/hooks/use-account-freeze.hook.mjs +42 -0
  34. package/dist/modules/subscriptions/hooks/use-account-freeze.hook.mjs.map +1 -0
  35. package/dist/modules/subscriptions/hooks/use-freeze-notice.hook.mjs +33 -0
  36. package/dist/modules/subscriptions/hooks/use-freeze-notice.hook.mjs.map +1 -0
  37. package/dist/modules/subscriptions/services/subscriptions.service.mjs +29 -0
  38. package/dist/modules/subscriptions/services/subscriptions.service.mjs.map +1 -1
  39. package/dist/modules/subscriptions/types/freeze-notice.type.mjs +1 -0
  40. package/dist/modules/subscriptions/types/freeze-notice.type.mjs.map +1 -0
  41. package/dist/modules/subscriptions/utils/get-account-freeze-date.mjs +19 -0
  42. package/dist/modules/subscriptions/utils/get-account-freeze-date.mjs.map +1 -0
  43. package/dist/modules/subscriptions/utils/get-account-freeze-state.mjs +21 -0
  44. package/dist/modules/subscriptions/utils/get-account-freeze-state.mjs.map +1 -0
  45. package/dist/server.mjs +4 -0
  46. package/dist/server.mjs.map +1 -1
  47. package/package.json +1 -1
  48. package/src/components/modals/AccountFrozenModal.tsx +58 -0
  49. package/src/components/navigation/FreezeWarningBanner.tsx +57 -0
  50. package/src/components/navigation/FrozenAccountBanner.tsx +49 -0
  51. package/src/components/navigation/SubscriptionBanner.tsx +19 -0
  52. package/src/handlers.ts +1 -0
  53. package/src/i18n/messages/en-us.ts +23 -0
  54. package/src/i18n/messages/es-es.ts +23 -0
  55. package/src/i18n/messages/pt-br.ts +26 -0
  56. package/src/index.ts +14 -0
  57. package/src/modules/auth/utils/assert-account-not-frozen.ts +38 -0
  58. package/src/modules/plans/utils/map-api-plan-to-ui.ts +16 -3
  59. package/src/modules/projects/services/projects.service.ts +3 -0
  60. package/src/modules/subscriptions/actions/ack-freeze-notice.action.ts +8 -0
  61. package/src/modules/subscriptions/constants/query-keys.constants.ts +2 -0
  62. package/src/modules/subscriptions/constants/subscription.constants.ts +8 -0
  63. package/src/modules/subscriptions/handlers/freeze-notice.handler.ts +10 -0
  64. package/src/modules/subscriptions/hooks/use-account-freeze.hook.ts +44 -0
  65. package/src/modules/subscriptions/hooks/use-freeze-notice.hook.ts +42 -0
  66. package/src/modules/subscriptions/services/subscriptions.service.ts +39 -0
  67. package/src/modules/subscriptions/types/freeze-notice.type.ts +5 -0
  68. package/src/modules/subscriptions/utils/get-account-freeze-date.ts +40 -0
  69. package/src/modules/subscriptions/utils/get-account-freeze-state.ts +43 -0
  70. package/src/server.ts +2 -0
@@ -5,12 +5,16 @@ import { useCharges } from "../../modules/charges/hooks/charges.hook";
5
5
  import { SUBSCRIPTION_GRACE_PERIOD_DAYS, MAX_PAYMENT_ATTEMPTS } from "../../modules/subscriptions/constants/subscription.constants";
6
6
  import { getSubscriptionExpiryDate } from "../../modules/subscriptions/utils/get-subscription-expiry-date";
7
7
  import { getSubscriptionCancellationState } from "../../modules/subscriptions/utils/get-subscription-cancellation-state";
8
+ import { getAccountFreezeState } from "../../modules/subscriptions/utils/get-account-freeze-state";
9
+ import { getAccountFreezeDate } from "../../modules/subscriptions/utils/get-account-freeze-date";
8
10
  import { toCalendarDate, daysBetween } from "../../infra/utils/date";
9
11
  import { OverdueInvoiceBanner } from "./OverdueInvoiceBanner";
10
12
  import { UpcomingInvoiceBanner } from "./UpcomingInvoiceBanner";
11
13
  import { CancelledSubscriptionBanner } from "./CancelledSubscriptionBanner";
12
14
  import { PendingCancellationBanner } from "./PendingCancellationBanner";
13
15
  import { TrialBanner } from "./TrialBanner";
16
+ import { FrozenAccountBanner } from "./FrozenAccountBanner";
17
+ import { FreezeWarningBanner } from "./FreezeWarningBanner";
14
18
 
15
19
  const UPCOMING_WINDOW_DAYS = 7;
16
20
  const PAYMENT_METHOD_BOLETO = 2;
@@ -76,6 +80,21 @@ export function SubscriptionBanner({
76
80
 
77
81
  if (subscription.type === "whitelabel") return null;
78
82
 
83
+ // Congelamento (date_due + ACCOUNT_FREEZE_AFTER_DAYS): entra antes do dunning
84
+ // e do aviso genérico de atraso, que hoje engolem essa janela.
85
+ const freezeState = getAccountFreezeState(subscription);
86
+ if (freezeState === "frozen") {
87
+ return <FrozenAccountBanner onDetails={onDetails} />;
88
+ }
89
+ if (freezeState === "warning") {
90
+ return (
91
+ <FreezeWarningBanner
92
+ freezeDate={getAccountFreezeDate(subscription)}
93
+ onDetails={onDetails}
94
+ />
95
+ );
96
+ }
97
+
79
98
  // Cartão em dunning (Stripe Smart Retries): comunica as tentativas de cobrança
80
99
  // restantes antes do cancelamento automático — não mais por tolerância fixa de
81
100
  // dias. attempt_count vive no invoice (payment_retry); só cartão tem retries.
package/src/handlers.ts CHANGED
@@ -18,5 +18,6 @@ export { createListAvailableUsersHandler } from './modules/projects/handlers/lis
18
18
  export { createListAllAccountUsersHandler } from './modules/projects/handlers/list-all-account-users.handler';
19
19
 
20
20
  export { createListSubscriptionsHandler } from './modules/subscriptions/handlers/list-subscriptions.handler';
21
+ export { createFreezeNoticeHandler } from './modules/subscriptions/handlers/freeze-notice.handler';
21
22
 
22
23
  export { createListMessagesHandler } from './modules/users/handlers/list-messages.handler';
@@ -432,6 +432,23 @@ const messages = {
432
432
  knowPlans: 'View plans',
433
433
  },
434
434
  },
435
+ subscription: {
436
+ frozen: {
437
+ banner: {
438
+ message: 'Your account has its resources blocked due to a missed payment.',
439
+ regularize: 'Fix subscription',
440
+ },
441
+ warningBanner: {
442
+ message: 'Payment overdue: your account will be blocked on {freezeDate}. Fix it to keep access.',
443
+ details: 'Details',
444
+ },
445
+ modal: {
446
+ title: 'Account blocked due to a missed payment',
447
+ description: 'Your account has its resources blocked due to a missed payment. Fix your subscription to unlock it.',
448
+ cta: 'Fix subscription',
449
+ },
450
+ },
451
+ },
435
452
  forms: {
436
453
  combobox: {
437
454
  placeholder: 'Select an option',
@@ -535,6 +552,12 @@ const messages = {
535
552
  onboarding: 'Onboarding meeting',
536
553
  projectManagement: 'Project management',
537
554
  },
555
+ names: {
556
+ '415': 'Starter',
557
+ '1': 'Essential',
558
+ '2': 'Growth',
559
+ '3': 'Agency',
560
+ },
538
561
  mainFeatures: {
539
562
  pagesUnlimited: 'Unlimited pages',
540
563
  pageSingular: '1 page',
@@ -433,6 +433,23 @@ const messages = {
433
433
  knowPlans: 'Conoce nuestros planes',
434
434
  },
435
435
  },
436
+ subscription: {
437
+ frozen: {
438
+ banner: {
439
+ message: 'Tu cuenta tiene los recursos bloqueados por falta de pago.',
440
+ regularize: 'Regularizar suscripción',
441
+ },
442
+ warningBanner: {
443
+ message: 'Pago atrasado: tu cuenta será bloqueada el {freezeDate}. Regulariza para no perder el acceso.',
444
+ details: 'Detalles',
445
+ },
446
+ modal: {
447
+ title: 'Cuenta bloqueada por falta de pago',
448
+ description: 'Tu cuenta tiene los recursos bloqueados por falta de pago. Regulariza tu suscripción para liberarla.',
449
+ cta: 'Regularizar suscripción',
450
+ },
451
+ },
452
+ },
436
453
  forms: {
437
454
  combobox: {
438
455
  placeholder: 'Selecciona una opción',
@@ -536,6 +553,12 @@ const messages = {
536
553
  onboarding: 'Reunión de Onboarding',
537
554
  projectManagement: 'Gestión de proyectos',
538
555
  },
556
+ names: {
557
+ '415': 'Starter',
558
+ '1': 'Essential',
559
+ '2': 'Growth',
560
+ '3': 'Agency',
561
+ },
539
562
  mainFeatures: {
540
563
  pagesUnlimited: 'Páginas ilimitadas',
541
564
  pageSingular: '1 página',
@@ -448,6 +448,26 @@ const messages = {
448
448
  },
449
449
  },
450
450
 
451
+ // Congelamento por falta de pagamento (date_due + 7d) — banner, aviso de
452
+ // véspera e modal única entre as 3 aplicações.
453
+ subscription: {
454
+ frozen: {
455
+ banner: {
456
+ message: 'Sua conta está com os recursos bloqueados por falta de pagamento.',
457
+ regularize: 'Regularizar assinatura',
458
+ },
459
+ warningBanner: {
460
+ message: 'Pagamento em atraso: sua conta será bloqueada em {freezeDate}. Regularize para não perder o acesso.',
461
+ details: 'Detalhes',
462
+ },
463
+ modal: {
464
+ title: 'Conta bloqueada por falta de pagamento',
465
+ description: 'Sua conta está com os recursos bloqueados por falta de pagamento. Regularize a assinatura para liberar.',
466
+ cta: 'Regularizar assinatura',
467
+ },
468
+ },
469
+ },
470
+
451
471
  // Nomes dos idiomas exibidos no seletor.
452
472
  // Planos: features e main features (mapApiPlanToUiPlan).
453
473
  plans: {
@@ -460,6 +480,12 @@ const messages = {
460
480
  onboarding: 'Reunião de Onboarding',
461
481
  projectManagement: 'Gestão de projetos',
462
482
  },
483
+ names: {
484
+ '415': 'Iniciante',
485
+ '1': 'Essencial',
486
+ '2': 'Negócio',
487
+ '3': 'Agência',
488
+ },
463
489
  mainFeatures: {
464
490
  pagesUnlimited: 'Páginas ilimitadas',
465
491
  pageSingular: '1 página',
package/src/index.ts CHANGED
@@ -84,9 +84,12 @@ export { useSubscriptions } from "./modules/subscriptions/hooks/list-subscriptio
84
84
  export {
85
85
  SUBSCRIPTIONS_QUERY_KEY,
86
86
  CHARGES_QUERY_KEY,
87
+ FREEZE_NOTICE_QUERY_KEY,
87
88
  } from "./modules/subscriptions/constants/query-keys.constants";
88
89
  export { useActiveSubscription } from "./modules/subscriptions/hooks/find-active-subscription.hook";
89
90
  export { useCancelledSubscriptionGuard } from "./modules/subscriptions/hooks/use-cancelled-subscription-guard";
91
+ export { useAccountFreeze } from "./modules/subscriptions/hooks/use-account-freeze.hook";
92
+ export { useFreezeNotice } from "./modules/subscriptions/hooks/use-freeze-notice.hook";
90
93
  export { usePlanById } from "./modules/plans/hooks/use-plan-by-id.hook";
91
94
  export { useHasPlanAddon } from "./modules/plans/hooks/use-has-plan-addon.hook";
92
95
  export { useCalculateSubscription } from "./modules/subscriptions/hooks/calculate-subscription.hook";
@@ -199,6 +202,14 @@ export {
199
202
  export { SUBSCRIPTION_GRACE_PERIOD_DAYS } from "./modules/subscriptions/constants/subscription.constants";
200
203
  export { getSubscriptionCancellationState } from "./modules/subscriptions/utils/get-subscription-cancellation-state";
201
204
  export type { SubscriptionCancellationState } from "./modules/subscriptions/utils/get-subscription-cancellation-state";
205
+ export {
206
+ ACCOUNT_FREEZE_AFTER_DAYS,
207
+ FREEZE_WARNING_DAYS_BEFORE,
208
+ } from "./modules/subscriptions/constants/subscription.constants";
209
+ export { getAccountFreezeState } from "./modules/subscriptions/utils/get-account-freeze-state";
210
+ export type { AccountFreezeState } from "./modules/subscriptions/utils/get-account-freeze-state";
211
+ export { getAccountFreezeDate } from "./modules/subscriptions/utils/get-account-freeze-date";
212
+ export type { FreezeNotice } from "./modules/subscriptions/types/freeze-notice.type";
202
213
  export {
203
214
  useCountPages,
204
215
  COUNT_PAGES_QUERY_KEY,
@@ -216,6 +227,7 @@ export { useAccountDeletionWarningModal } from "./store/useAccountDeletionWarnin
216
227
  export { default as BuyCreditsModal } from "./components/modals/BuyCreditsModal";
217
228
  export { default as CreditsDisabledModal } from "./components/modals/CreditsDisabledModal";
218
229
  export { default as PaidPlanRequiredModal } from "./components/modals/PaidPlanRequiredModal";
230
+ export { default as AccountFrozenModal } from "./components/modals/AccountFrozenModal";
219
231
  export { default as AccountDeletionWarningModal } from "./components/modals/AccountDeletionWarningModal";
220
232
  export { default as RequiredBillingDataModal } from "./components/modals/billing/RequiredBillingDataModal";
221
233
  export { default as AddCardModal } from "./components/modals/cards/AddCardModal";
@@ -294,6 +306,8 @@ export { SubscriptionBanner } from "./components/navigation/SubscriptionBanner";
294
306
  export { OverdueInvoiceBanner } from "./components/navigation/OverdueInvoiceBanner";
295
307
  export { UpcomingInvoiceBanner } from "./components/navigation/UpcomingInvoiceBanner";
296
308
  export { TrialBanner } from "./components/navigation/TrialBanner";
309
+ export { FrozenAccountBanner } from "./components/navigation/FrozenAccountBanner";
310
+ export { FreezeWarningBanner } from "./components/navigation/FreezeWarningBanner";
297
311
  export type { NavItemConfig } from "./components/navigation/subcomponents/NavItems";
298
312
  export { useMobileNavbarSheet } from "./store/useMobileNavbarSheet";
299
313
 
@@ -0,0 +1,38 @@
1
+ import "server-only";
2
+
3
+ import { ApiError } from "../../../infra/api/types";
4
+ import { listSubscriptionsAction } from "../../subscriptions/actions/list-subscriptions.action";
5
+ import { getAccountFreezeState } from "../../subscriptions/utils/get-account-freeze-state";
6
+
7
+ /**
8
+ * Bloqueia mutações de conta congelada por falta de pagamento (date_due + 7d).
9
+ *
10
+ * Fail-OPEN na leitura: falha de rede/payload ao consultar billing não pode
11
+ * travar cliente adimplente. Diferente do assertManagementSubscription
12
+ * (default-deny) porque ali o risco é liberar entitlement não contratado; aqui
13
+ * o gate primário já é este server action, e o caso vencido (expired) continua
14
+ * coberto pelo assertManagementSubscription rodando ao lado.
15
+ */
16
+ export async function assertAccountNotFrozen(): Promise<void> {
17
+ const result = await listSubscriptionsAction({ active: true });
18
+
19
+ if (!result.success) {
20
+ console.error(
21
+ "[assertAccountNotFrozen] failed to read subscription context:",
22
+ result.error,
23
+ );
24
+ return;
25
+ }
26
+
27
+ const subscription = result.data?.data?.[0];
28
+
29
+ if (!subscription) return;
30
+
31
+ if (getAccountFreezeState(subscription) !== "frozen") return;
32
+
33
+ throw new ApiError(
34
+ "Sua conta está com os recursos bloqueados por falta de pagamento. Regularize a assinatura para liberar.",
35
+ "ACCOUNT_FROZEN",
36
+ 403,
37
+ );
38
+ }
@@ -14,9 +14,23 @@ import {
14
14
  export type PlanBillingPeriod = "monthly" | "semiannual" | "annual";
15
15
 
16
16
  const STARTER_PLAN_ID = 415;
17
+ const AGENCY_PLAN_ID = 3;
17
18
 
18
19
  type TranslateFn = (key: any, values?: Record<string, string | number>) => string;
19
20
 
21
+ const planIdOf = (plan: Plan): number => plan.id_plan ?? plan.id;
22
+
23
+ function resolvePlanName(plan: Plan, translate?: TranslateFn): string {
24
+ if (!translate) return plan.name;
25
+ const key = `common.plans.names.${planIdOf(plan)}`;
26
+ try {
27
+ const translated = translate(key);
28
+ return translated && translated !== key ? translated : plan.name;
29
+ } catch {
30
+ return plan.name;
31
+ }
32
+ }
33
+
20
34
  const BASE_FEATURES_KEYS = [
21
35
  { icon: "check", key: "plans.features.unlimitedVisits", fallback: "Visitas ilimitadas" },
22
36
  { icon: "check", key: "plans.features.unlimitedLeads", fallback: "Leads ilimitadas" },
@@ -114,8 +128,7 @@ function buildUiPlan(plan: Plan, currency: CurrencyCode, translate?: TranslateFn
114
128
  const monthlyFinal = pricingByPeriod.monthly;
115
129
 
116
130
  const isStarterPlan = plan.id === STARTER_PLAN_ID || plan.id_plan === STARTER_PLAN_ID;
117
- const hasProjectAndOnboarding =
118
- plan.name.toLowerCase().includes("agência") || plan.name.toLowerCase().includes("agencia");
131
+ const hasProjectAndOnboarding = planIdOf(plan) === AGENCY_PLAN_ID;
119
132
 
120
133
  const onboardingText = translate
121
134
  ? translate("common.plans.features.onboarding")
@@ -151,7 +164,7 @@ function buildUiPlan(plan: Plan, currency: CurrencyCode, translate?: TranslateFn
151
164
 
152
165
  return {
153
166
  planId: plan.id,
154
- name: plan.name,
167
+ name: resolvePlanName(plan, translate),
155
168
  isPopular: popular,
156
169
  buttonVariant,
157
170
  mainFeatures: buildMainFeaturesFromItems(plan.items, translate),
@@ -5,6 +5,7 @@ import { ApiError } from '../../../infra/api/types';
5
5
  import { buildQueryParams } from '../../../infra/utils/params';
6
6
  import { getUserContext } from '../../auth/utils/get-user-context';
7
7
  import { assertManagementSubscription } from '../../auth/utils/assert-management-subscription';
8
+ import { assertAccountNotFrozen } from '../../auth/utils/assert-account-not-frozen';
8
9
  import {
9
10
  Project,
10
11
  ProjectSchema,
@@ -80,6 +81,7 @@ class ProjectsService {
80
81
 
81
82
  async create(data: CreateProjectRequest): Promise<Project> {
82
83
  await assertManagementSubscription();
84
+ await assertAccountNotFrozen();
83
85
  const { id_account, id_user } = await getUserContext();
84
86
  const url = `/account/${id_account}/user/${id_user}/projects`;
85
87
 
@@ -102,6 +104,7 @@ class ProjectsService {
102
104
 
103
105
  async update(projectId: number, data: UpdateProjectRequest): Promise<Project> {
104
106
  await assertManagementSubscription();
107
+ await assertAccountNotFrozen();
105
108
  const { id_account, id_user } = await getUserContext();
106
109
  const url = `/account/${id_account}/user/${id_user}/projects/${projectId}`;
107
110
 
@@ -0,0 +1,8 @@
1
+ 'use server';
2
+
3
+ import { safeMutationAction } from '../../../utils/safeMutationAction';
4
+ import { subscriptionsService } from '../services/subscriptions.service';
5
+
6
+ export async function ackFreezeNoticeAction(cycle: string) {
7
+ return safeMutationAction(() => subscriptionsService.ackFreezeNotice(cycle));
8
+ }
@@ -1,3 +1,5 @@
1
1
  export const SUBSCRIPTIONS_QUERY_KEY = ['subscriptions'] as const;
2
2
 
3
3
  export const CHARGES_QUERY_KEY = ['charges'] as const;
4
+
5
+ export const FREEZE_NOTICE_QUERY_KEY = ['freeze-notice'] as const;
@@ -1,5 +1,13 @@
1
1
  export const SUBSCRIPTION_GRACE_PERIOD_DAYS = 15;
2
2
 
3
+ export const ACCOUNT_FREEZE_AFTER_DAYS = 7;
4
+ export const FREEZE_WARNING_DAYS_BEFORE = 1;
5
+
6
+ /* Congelamento só vale para a wl 1: o aviso de véspera e o e-mail de bloqueio saem do
7
+ * cron `dailyNotifications` do gapps-r3-api, que só roda para ela. Congelar as demais
8
+ * seria bloquear sem avisar. Espelhado nos gates dos dois backends. */
9
+ export const FREEZE_ELIGIBLE_WHITELABEL_ID = 1;
10
+
3
11
  /* Stripe Smart Retries (dunning): até 8 tentativas de cobrança no cartão antes
4
12
  * do cancelamento automático. `tentativas restantes = MAX - attempt_count`. */
5
13
  export const MAX_PAYMENT_ATTEMPTS = 8;
@@ -0,0 +1,10 @@
1
+ import 'server-only';
2
+
3
+ import { safeRouteHandler } from '../../../infra/route/safe-route-handler';
4
+ import { subscriptionsService } from '../services/subscriptions.service';
5
+
6
+ export function createFreezeNoticeHandler() {
7
+ return async function GET() {
8
+ return safeRouteHandler(() => subscriptionsService.getFreezeNotice());
9
+ };
10
+ }
@@ -0,0 +1,44 @@
1
+ 'use client';
2
+
3
+ import { useCallback } from 'react';
4
+ import { UserProfile } from '../../users/schema';
5
+ import { useAuth } from '../../../providers/auth.provider';
6
+ import { useModalManager } from '../../../store/useModalManager';
7
+ import { useActiveSubscription } from './find-active-subscription.hook';
8
+ import { getAccountFreezeState } from '../utils/get-account-freeze-state';
9
+ import { getAccountFreezeDate } from '../utils/get-account-freeze-date';
10
+
11
+ export function useAccountFreeze() {
12
+ const { user } = useAuth();
13
+ const { openModal } = useModalManager();
14
+ const {
15
+ data: { data: [subscription] = [] } = {},
16
+ } = useActiveSubscription();
17
+
18
+ const isViewer = user?.profile === UserProfile.viewer;
19
+ const state = getAccountFreezeState(subscription);
20
+ const isFrozen = state === 'frozen';
21
+ const isWarning = state === 'warning';
22
+ const freezeDate = getAccountFreezeDate(subscription);
23
+
24
+ const guardAction = useCallback(
25
+ (action?: () => void) => {
26
+ if (!isViewer && isFrozen) {
27
+ openModal('accountFrozenModal');
28
+ return false;
29
+ }
30
+
31
+ action?.();
32
+ return true;
33
+ },
34
+ [openModal, isViewer, isFrozen]
35
+ );
36
+
37
+ return {
38
+ state,
39
+ isFrozen,
40
+ isWarning,
41
+ freezeDate,
42
+ guardAction,
43
+ };
44
+ }
@@ -0,0 +1,42 @@
1
+ 'use client';
2
+
3
+ import { useCallback } from 'react';
4
+ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
5
+ import { apiFetch } from '../../../infra/http/api-fetch';
6
+ import { withAction } from '../../../utils/withAction';
7
+ import { ackFreezeNoticeAction } from '../actions/ack-freeze-notice.action';
8
+ import { FREEZE_NOTICE_QUERY_KEY } from '../constants/query-keys.constants';
9
+ import { useAuthQueryKey } from '../../../hooks/useAuthQueryKey';
10
+ import type { FreezeNotice } from '../types/freeze-notice.type';
11
+
12
+ /**
13
+ * Modal única do congelamento (D4): pending quando `frozen && !acknowledged`.
14
+ * ack() marca o episódio (id_subscription + date_due) como visto no gapps-r3-api
15
+ * — reaparece só se um novo ciclo congelar de novo.
16
+ */
17
+ export function useFreezeNotice() {
18
+ const queryClient = useQueryClient();
19
+ const queryKey = useAuthQueryKey([...FREEZE_NOTICE_QUERY_KEY]);
20
+
21
+ const query = useQuery({
22
+ queryKey,
23
+ queryFn: ({ signal }) => apiFetch<FreezeNotice>('/api/notices/freeze', { signal }),
24
+ staleTime: 10_000,
25
+ });
26
+
27
+ const mutation = useMutation({
28
+ mutationFn: (cycle: string) => withAction(ackFreezeNoticeAction)(cycle),
29
+ onSuccess: () => {
30
+ queryClient.invalidateQueries({ queryKey });
31
+ },
32
+ });
33
+
34
+ const pending = !!query.data?.frozen && !query.data?.acknowledged;
35
+ const cycle = query.data?.cycle ?? null;
36
+
37
+ const ack = useCallback(() => {
38
+ if (cycle) mutation.mutate(cycle);
39
+ }, [cycle, mutation]);
40
+
41
+ return { pending, cycle, ack };
42
+ }
@@ -26,6 +26,7 @@ import {
26
26
  SubscriptionPendingPixResponseSchema,
27
27
  type SubscriptionPendingPixResponse,
28
28
  } from "../types/pix-pending.type";
29
+ import type { FreezeNotice } from "../types/freeze-notice.type";
29
30
 
30
31
  /**
31
32
  * Timeout das mutations de assinatura que ESCREVEM (action/plan e action/payment).
@@ -181,6 +182,44 @@ class SubscriptionsService {
181
182
 
182
183
  return { success: true };
183
184
  }
185
+
186
+ async getFreezeNotice(): Promise<FreezeNotice> {
187
+ const { id_account } = await getUserContext();
188
+
189
+ const response = await api.apps.get<ApiActionResult<FreezeNotice>>(
190
+ `/accounts/${id_account}/notices/freeze`,
191
+ );
192
+
193
+ if (response.status === 0 || !response.data) {
194
+ throw new ApiError(
195
+ (response.status === 0 ? response.message : "") ||
196
+ "Erro ao consultar aviso de congelamento",
197
+ "GET_FREEZE_NOTICE_FAILED",
198
+ 400,
199
+ );
200
+ }
201
+
202
+ return response.data;
203
+ }
204
+
205
+ async ackFreezeNotice(cycle: string): Promise<SuccessResult<void>> {
206
+ const { id_account } = await getUserContext();
207
+
208
+ const response = await api.apps.post<ApiActionResult<void>>(
209
+ `/accounts/${id_account}/notices/freeze/ack`,
210
+ { cycle },
211
+ );
212
+
213
+ if (response.status === 0) {
214
+ throw new ApiError(
215
+ response.message || "Erro ao confirmar aviso de congelamento",
216
+ "ACK_FREEZE_NOTICE_FAILED",
217
+ 400,
218
+ );
219
+ }
220
+
221
+ return { success: true };
222
+ }
184
223
  }
185
224
 
186
225
  export const subscriptionsService = new SubscriptionsService();
@@ -0,0 +1,5 @@
1
+ export interface FreezeNotice {
2
+ frozen: boolean;
3
+ cycle: string | null;
4
+ acknowledged: boolean;
5
+ }
@@ -0,0 +1,40 @@
1
+ import {
2
+ ACCOUNT_FREEZE_AFTER_DAYS,
3
+ FREEZE_ELIGIBLE_WHITELABEL_ID,
4
+ } from "../constants/subscription.constants";
5
+ import { toCalendarDate } from "../../../infra/utils/date";
6
+ import type { Subscription } from "../types/subscription.type";
7
+
8
+ /**
9
+ * Data em que a conta é congelada por falta de pagamento: date_due + ACCOUNT_FREEZE_AFTER_DAYS.
10
+ *
11
+ * Só congela assinatura `client` da whitelabel 1 — MESMA régua do
12
+ * `isSubscriptionFrozen` (gapps-r3-api) e do `isAccountFrozen` (gpages-r3-api).
13
+ * Se as três divergirem, o usuário é bloqueado por uma camada e liberado por outra.
14
+ *
15
+ * - `type !== "client"` cobre free, trial, partner e whitelabel de uma vez (a coluna
16
+ * aceita mais valores do que a lista que o SubscriptionBanner enumera).
17
+ * - `id_wl !== 1`: o pipeline de aviso (cron `dailyNotifications` do gapps-r3-api) só
18
+ * existe para a wl 1. Congelar conta de whitelabel seria bloquear sem nunca avisar —
19
+ * o defeito do ID-5029 que este card existe para não repetir.
20
+ */
21
+ export function getAccountFreezeDate(
22
+ subscription:
23
+ | Pick<Subscription, "type" | "date_due" | "id_wl">
24
+ | undefined
25
+ | null,
26
+ ): Date | null {
27
+ if (!subscription) return null;
28
+
29
+ if (subscription.type !== "client") return null;
30
+
31
+ if (Number(subscription.id_wl) !== FREEZE_ELIGIBLE_WHITELABEL_ID) return null;
32
+
33
+ const dueDate = toCalendarDate(subscription.date_due);
34
+
35
+ if (!dueDate) return null;
36
+
37
+ const result = new Date(dueDate);
38
+ result.setDate(result.getDate() + ACCOUNT_FREEZE_AFTER_DAYS);
39
+ return result;
40
+ }
@@ -0,0 +1,43 @@
1
+ import { FREEZE_WARNING_DAYS_BEFORE } from "../constants/subscription.constants";
2
+ import { toCalendarDate } from "../../../infra/utils/date";
3
+ import { getAccountFreezeDate } from "./get-account-freeze-date";
4
+ import { getSubscriptionCancellationState } from "./get-subscription-cancellation-state";
5
+ import type { Subscription } from "../types/subscription.type";
6
+
7
+ export type AccountFreezeState = "none" | "warning" | "frozen";
8
+
9
+ /**
10
+ * Estado de congelamento por falta de pagamento (date_due + ACCOUNT_FREEZE_AFTER_DAYS).
11
+ *
12
+ * Conta cancelada/pendente de cancelamento é tratada pelos ramos de cancelamento
13
+ * (mesma ordem do SubscriptionBanner) — aqui sempre "none" nesse caso, pra não
14
+ * disputar mensagem com o CancelledSubscriptionBanner/PendingCancellationBanner.
15
+ */
16
+ export function getAccountFreezeState(
17
+ subscription:
18
+ | Pick<
19
+ Subscription,
20
+ "type" | "active" | "date_due" | "date_cancellation" | "id_wl"
21
+ >
22
+ | undefined
23
+ | null,
24
+ ): AccountFreezeState {
25
+ if (!subscription) return "none";
26
+
27
+ if (getSubscriptionCancellationState(subscription) !== "none") return "none";
28
+
29
+ const freezeDate = getAccountFreezeDate(subscription);
30
+ if (!freezeDate) return "none";
31
+
32
+ const today = toCalendarDate(new Date());
33
+ if (!today) return "none";
34
+
35
+ if (today.getTime() >= freezeDate.getTime()) return "frozen";
36
+
37
+ const warningDate = new Date(freezeDate);
38
+ warningDate.setDate(warningDate.getDate() - FREEZE_WARNING_DAYS_BEFORE);
39
+
40
+ if (today.getTime() >= warningDate.getTime()) return "warning";
41
+
42
+ return "none";
43
+ }
package/src/server.ts CHANGED
@@ -11,6 +11,7 @@ export { assertManagementPermission } from './modules/auth/utils/assert-manageme
11
11
  export { assertManagementSubscription } from './modules/auth/utils/assert-management-subscription';
12
12
  export { assertSubscriptionAddon } from './modules/auth/utils/assert-subscription-addon';
13
13
  export { assertPaidSubscription } from './modules/auth/utils/assert-paid-subscription';
14
+ export { assertAccountNotFrozen } from './modules/auth/utils/assert-account-not-frozen';
14
15
  export { subscriptionsService } from './modules/subscriptions/services/subscriptions.service';
15
16
  export { plansService } from './modules/plans/services/plans.service';
16
17
  export { cardsService } from './modules/cards/services/cards.service';
@@ -27,6 +28,7 @@ export { findUserById } from './modules/users/action/find-user-by-id.action';
27
28
  export { findCurrentAccount } from './modules/accounts/actions/find-current-account.action';
28
29
  export { getAccountTokenAction } from './modules/accounts/actions/get-account-token.action';
29
30
  export { listSubscriptionsAction } from './modules/subscriptions/actions/list-subscriptions.action';
31
+ export { ackFreezeNoticeAction } from './modules/subscriptions/actions/ack-freeze-notice.action';
30
32
  export { calculateSubscriptionAction } from './modules/subscriptions/actions/calculate-subscription.action';
31
33
  export { updateSubscriptionPlanAction } from './modules/subscriptions/actions/update-subscription-plan.action';
32
34
  export { updateSubscriptionPaymentAction } from './modules/subscriptions/actions/update-subscription-payment.action';