@greatapps/common 1.1.744 → 1.1.746

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 (67) hide show
  1. package/dist/components/modals/AccountFrozenModal.mjs +69 -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 +20 -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 +31 -0
  12. package/dist/i18n/messages/en-us.mjs.map +1 -1
  13. package/dist/i18n/messages/es-es.mjs +31 -0
  14. package/dist/i18n/messages/es-es.mjs.map +1 -1
  15. package/dist/i18n/messages/pt-br.mjs +33 -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/projects/services/projects.service.mjs +3 -0
  22. package/dist/modules/projects/services/projects.service.mjs.map +1 -1
  23. package/dist/modules/subscriptions/actions/ack-freeze-notice.action.mjs +10 -0
  24. package/dist/modules/subscriptions/actions/ack-freeze-notice.action.mjs.map +1 -0
  25. package/dist/modules/subscriptions/constants/query-keys.constants.mjs +2 -0
  26. package/dist/modules/subscriptions/constants/query-keys.constants.mjs.map +1 -1
  27. package/dist/modules/subscriptions/constants/subscription.constants.mjs +6 -0
  28. package/dist/modules/subscriptions/constants/subscription.constants.mjs.map +1 -1
  29. package/dist/modules/subscriptions/handlers/freeze-notice.handler.mjs +12 -0
  30. package/dist/modules/subscriptions/handlers/freeze-notice.handler.mjs.map +1 -0
  31. package/dist/modules/subscriptions/hooks/use-account-freeze.hook.mjs +42 -0
  32. package/dist/modules/subscriptions/hooks/use-account-freeze.hook.mjs.map +1 -0
  33. package/dist/modules/subscriptions/hooks/use-freeze-notice.hook.mjs +33 -0
  34. package/dist/modules/subscriptions/hooks/use-freeze-notice.hook.mjs.map +1 -0
  35. package/dist/modules/subscriptions/services/subscriptions.service.mjs +29 -0
  36. package/dist/modules/subscriptions/services/subscriptions.service.mjs.map +1 -1
  37. package/dist/modules/subscriptions/types/freeze-notice.type.mjs +1 -0
  38. package/dist/modules/subscriptions/types/freeze-notice.type.mjs.map +1 -0
  39. package/dist/modules/subscriptions/utils/get-account-freeze-date.mjs +19 -0
  40. package/dist/modules/subscriptions/utils/get-account-freeze-date.mjs.map +1 -0
  41. package/dist/modules/subscriptions/utils/get-account-freeze-state.mjs +21 -0
  42. package/dist/modules/subscriptions/utils/get-account-freeze-state.mjs.map +1 -0
  43. package/dist/server.mjs +4 -0
  44. package/dist/server.mjs.map +1 -1
  45. package/package.json +1 -1
  46. package/src/components/modals/AccountFrozenModal.tsx +106 -0
  47. package/src/components/navigation/FreezeWarningBanner.tsx +57 -0
  48. package/src/components/navigation/FrozenAccountBanner.tsx +49 -0
  49. package/src/components/navigation/SubscriptionBanner.tsx +30 -0
  50. package/src/handlers.ts +1 -0
  51. package/src/i18n/messages/en-us.ts +31 -0
  52. package/src/i18n/messages/es-es.ts +31 -0
  53. package/src/i18n/messages/pt-br.ts +34 -0
  54. package/src/index.ts +14 -0
  55. package/src/modules/auth/utils/assert-account-not-frozen.ts +38 -0
  56. package/src/modules/projects/services/projects.service.ts +3 -0
  57. package/src/modules/subscriptions/actions/ack-freeze-notice.action.ts +8 -0
  58. package/src/modules/subscriptions/constants/query-keys.constants.ts +2 -0
  59. package/src/modules/subscriptions/constants/subscription.constants.ts +8 -0
  60. package/src/modules/subscriptions/handlers/freeze-notice.handler.ts +10 -0
  61. package/src/modules/subscriptions/hooks/use-account-freeze.hook.ts +44 -0
  62. package/src/modules/subscriptions/hooks/use-freeze-notice.hook.ts +42 -0
  63. package/src/modules/subscriptions/services/subscriptions.service.ts +39 -0
  64. package/src/modules/subscriptions/types/freeze-notice.type.ts +5 -0
  65. package/src/modules/subscriptions/utils/get-account-freeze-date.ts +40 -0
  66. package/src/modules/subscriptions/utils/get-account-freeze-state.ts +43 -0
  67. package/src/server.ts +2 -0
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/modules/subscriptions/constants/subscription.constants.ts"],"sourcesContent":["export const SUBSCRIPTION_GRACE_PERIOD_DAYS = 15;\n\n/* Stripe Smart Retries (dunning): até 8 tentativas de cobrança no cartão antes\n * do cancelamento automático. `tentativas restantes = MAX - attempt_count`. */\nexport const MAX_PAYMENT_ATTEMPTS = 8;\n"],"mappings":"AAAO,MAAM,iCAAiC;AAIvC,MAAM,uBAAuB;","names":[]}
1
+ {"version":3,"sources":["../../../../src/modules/subscriptions/constants/subscription.constants.ts"],"sourcesContent":["export const SUBSCRIPTION_GRACE_PERIOD_DAYS = 15;\n\nexport const ACCOUNT_FREEZE_AFTER_DAYS = 7;\nexport const FREEZE_WARNING_DAYS_BEFORE = 1;\n\n/* Congelamento só vale para a wl 1: o aviso de véspera e o e-mail de bloqueio saem do\n * cron `dailyNotifications` do gapps-r3-api, que só roda para ela. Congelar as demais\n * seria bloquear sem avisar. Espelhado nos gates dos dois backends. */\nexport const FREEZE_ELIGIBLE_WHITELABEL_ID = 1;\n\n/* Stripe Smart Retries (dunning): até 8 tentativas de cobrança no cartão antes\n * do cancelamento automático. `tentativas restantes = MAX - attempt_count`. */\nexport const MAX_PAYMENT_ATTEMPTS = 8;\n"],"mappings":"AAAO,MAAM,iCAAiC;AAEvC,MAAM,4BAA4B;AAClC,MAAM,6BAA6B;AAKnC,MAAM,gCAAgC;AAItC,MAAM,uBAAuB;","names":[]}
@@ -0,0 +1,12 @@
1
+ import "server-only";
2
+ import { safeRouteHandler } from "../../../infra/route/safe-route-handler";
3
+ import { subscriptionsService } from "../services/subscriptions.service";
4
+ function createFreezeNoticeHandler() {
5
+ return async function GET() {
6
+ return safeRouteHandler(() => subscriptionsService.getFreezeNotice());
7
+ };
8
+ }
9
+ export {
10
+ createFreezeNoticeHandler
11
+ };
12
+ //# sourceMappingURL=freeze-notice.handler.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../src/modules/subscriptions/handlers/freeze-notice.handler.ts"],"sourcesContent":["import 'server-only';\n\nimport { safeRouteHandler } from '../../../infra/route/safe-route-handler';\nimport { subscriptionsService } from '../services/subscriptions.service';\n\nexport function createFreezeNoticeHandler() {\n return async function GET() {\n return safeRouteHandler(() => subscriptionsService.getFreezeNotice());\n };\n}\n"],"mappings":"AAAA,OAAO;AAEP,SAAS,wBAAwB;AACjC,SAAS,4BAA4B;AAE9B,SAAS,4BAA4B;AAC1C,SAAO,eAAe,MAAM;AAC1B,WAAO,iBAAiB,MAAM,qBAAqB,gBAAgB,CAAC;AAAA,EACtE;AACF;","names":[]}
@@ -0,0 +1,42 @@
1
+ "use client";
2
+ import { useCallback } from "react";
3
+ import { UserProfile } from "../../users/schema";
4
+ import { useAuth } from "../../../providers/auth.provider";
5
+ import { useModalManager } from "../../../store/useModalManager";
6
+ import { useActiveSubscription } from "./find-active-subscription.hook";
7
+ import { getAccountFreezeState } from "../utils/get-account-freeze-state";
8
+ import { getAccountFreezeDate } from "../utils/get-account-freeze-date";
9
+ function useAccountFreeze() {
10
+ const { user } = useAuth();
11
+ const { openModal } = useModalManager();
12
+ const {
13
+ data: { data: [subscription] = [] } = {}
14
+ } = useActiveSubscription();
15
+ const isViewer = user?.profile === UserProfile.viewer;
16
+ const state = getAccountFreezeState(subscription);
17
+ const isFrozen = state === "frozen";
18
+ const isWarning = state === "warning";
19
+ const freezeDate = getAccountFreezeDate(subscription);
20
+ const guardAction = useCallback(
21
+ (action) => {
22
+ if (!isViewer && isFrozen) {
23
+ openModal("accountFrozenModal");
24
+ return false;
25
+ }
26
+ action?.();
27
+ return true;
28
+ },
29
+ [openModal, isViewer, isFrozen]
30
+ );
31
+ return {
32
+ state,
33
+ isFrozen,
34
+ isWarning,
35
+ freezeDate,
36
+ guardAction
37
+ };
38
+ }
39
+ export {
40
+ useAccountFreeze
41
+ };
42
+ //# sourceMappingURL=use-account-freeze.hook.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../src/modules/subscriptions/hooks/use-account-freeze.hook.ts"],"sourcesContent":["'use client';\n\nimport { useCallback } from 'react';\nimport { UserProfile } from '../../users/schema';\nimport { useAuth } from '../../../providers/auth.provider';\nimport { useModalManager } from '../../../store/useModalManager';\nimport { useActiveSubscription } from './find-active-subscription.hook';\nimport { getAccountFreezeState } from '../utils/get-account-freeze-state';\nimport { getAccountFreezeDate } from '../utils/get-account-freeze-date';\n\nexport function useAccountFreeze() {\n const { user } = useAuth();\n const { openModal } = useModalManager();\n const {\n data: { data: [subscription] = [] } = {},\n } = useActiveSubscription();\n\n const isViewer = user?.profile === UserProfile.viewer;\n const state = getAccountFreezeState(subscription);\n const isFrozen = state === 'frozen';\n const isWarning = state === 'warning';\n const freezeDate = getAccountFreezeDate(subscription);\n\n const guardAction = useCallback(\n (action?: () => void) => {\n if (!isViewer && isFrozen) {\n openModal('accountFrozenModal');\n return false;\n }\n\n action?.();\n return true;\n },\n [openModal, isViewer, isFrozen]\n );\n\n return {\n state,\n isFrozen,\n isWarning,\n freezeDate,\n guardAction,\n };\n}\n"],"mappings":";AAEA,SAAS,mBAAmB;AAC5B,SAAS,mBAAmB;AAC5B,SAAS,eAAe;AACxB,SAAS,uBAAuB;AAChC,SAAS,6BAA6B;AACtC,SAAS,6BAA6B;AACtC,SAAS,4BAA4B;AAE9B,SAAS,mBAAmB;AACjC,QAAM,EAAE,KAAK,IAAI,QAAQ;AACzB,QAAM,EAAE,UAAU,IAAI,gBAAgB;AACtC,QAAM;AAAA,IACJ,MAAM,EAAE,MAAM,CAAC,YAAY,IAAI,CAAC,EAAE,IAAI,CAAC;AAAA,EACzC,IAAI,sBAAsB;AAE1B,QAAM,WAAW,MAAM,YAAY,YAAY;AAC/C,QAAM,QAAQ,sBAAsB,YAAY;AAChD,QAAM,WAAW,UAAU;AAC3B,QAAM,YAAY,UAAU;AAC5B,QAAM,aAAa,qBAAqB,YAAY;AAEpD,QAAM,cAAc;AAAA,IAClB,CAAC,WAAwB;AACvB,UAAI,CAAC,YAAY,UAAU;AACzB,kBAAU,oBAAoB;AAC9B,eAAO;AAAA,MACT;AAEA,eAAS;AACT,aAAO;AAAA,IACT;AAAA,IACA,CAAC,WAAW,UAAU,QAAQ;AAAA,EAChC;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;","names":[]}
@@ -0,0 +1,33 @@
1
+ "use client";
2
+ import { useCallback } from "react";
3
+ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
4
+ import { apiFetch } from "../../../infra/http/api-fetch";
5
+ import { withAction } from "../../../utils/withAction";
6
+ import { ackFreezeNoticeAction } from "../actions/ack-freeze-notice.action";
7
+ import { FREEZE_NOTICE_QUERY_KEY } from "../constants/query-keys.constants";
8
+ import { useAuthQueryKey } from "../../../hooks/useAuthQueryKey";
9
+ function useFreezeNotice() {
10
+ const queryClient = useQueryClient();
11
+ const queryKey = useAuthQueryKey([...FREEZE_NOTICE_QUERY_KEY]);
12
+ const query = useQuery({
13
+ queryKey,
14
+ queryFn: ({ signal }) => apiFetch("/api/notices/freeze", { signal }),
15
+ staleTime: 1e4
16
+ });
17
+ const mutation = useMutation({
18
+ mutationFn: (cycle2) => withAction(ackFreezeNoticeAction)(cycle2),
19
+ onSuccess: () => {
20
+ queryClient.invalidateQueries({ queryKey });
21
+ }
22
+ });
23
+ const pending = !!query.data?.frozen && !query.data?.acknowledged;
24
+ const cycle = query.data?.cycle ?? null;
25
+ const ack = useCallback(() => {
26
+ if (cycle) mutation.mutate(cycle);
27
+ }, [cycle, mutation]);
28
+ return { pending, cycle, ack };
29
+ }
30
+ export {
31
+ useFreezeNotice
32
+ };
33
+ //# sourceMappingURL=use-freeze-notice.hook.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../src/modules/subscriptions/hooks/use-freeze-notice.hook.ts"],"sourcesContent":["'use client';\n\nimport { useCallback } from 'react';\nimport { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';\nimport { apiFetch } from '../../../infra/http/api-fetch';\nimport { withAction } from '../../../utils/withAction';\nimport { ackFreezeNoticeAction } from '../actions/ack-freeze-notice.action';\nimport { FREEZE_NOTICE_QUERY_KEY } from '../constants/query-keys.constants';\nimport { useAuthQueryKey } from '../../../hooks/useAuthQueryKey';\nimport type { FreezeNotice } from '../types/freeze-notice.type';\n\n/**\n * Modal única do congelamento (D4): pending quando `frozen && !acknowledged`.\n * ack() marca o episódio (id_subscription + date_due) como visto no gapps-r3-api\n * — reaparece só se um novo ciclo congelar de novo.\n */\nexport function useFreezeNotice() {\n const queryClient = useQueryClient();\n const queryKey = useAuthQueryKey([...FREEZE_NOTICE_QUERY_KEY]);\n\n const query = useQuery({\n queryKey,\n queryFn: ({ signal }) => apiFetch<FreezeNotice>('/api/notices/freeze', { signal }),\n staleTime: 10_000,\n });\n\n const mutation = useMutation({\n mutationFn: (cycle: string) => withAction(ackFreezeNoticeAction)(cycle),\n onSuccess: () => {\n queryClient.invalidateQueries({ queryKey });\n },\n });\n\n const pending = !!query.data?.frozen && !query.data?.acknowledged;\n const cycle = query.data?.cycle ?? null;\n\n const ack = useCallback(() => {\n if (cycle) mutation.mutate(cycle);\n }, [cycle, mutation]);\n\n return { pending, cycle, ack };\n}\n"],"mappings":";AAEA,SAAS,mBAAmB;AAC5B,SAAS,UAAU,aAAa,sBAAsB;AACtD,SAAS,gBAAgB;AACzB,SAAS,kBAAkB;AAC3B,SAAS,6BAA6B;AACtC,SAAS,+BAA+B;AACxC,SAAS,uBAAuB;AAQzB,SAAS,kBAAkB;AAChC,QAAM,cAAc,eAAe;AACnC,QAAM,WAAW,gBAAgB,CAAC,GAAG,uBAAuB,CAAC;AAE7D,QAAM,QAAQ,SAAS;AAAA,IACrB;AAAA,IACA,SAAS,CAAC,EAAE,OAAO,MAAM,SAAuB,uBAAuB,EAAE,OAAO,CAAC;AAAA,IACjF,WAAW;AAAA,EACb,CAAC;AAED,QAAM,WAAW,YAAY;AAAA,IAC3B,YAAY,CAACA,WAAkB,WAAW,qBAAqB,EAAEA,MAAK;AAAA,IACtE,WAAW,MAAM;AACf,kBAAY,kBAAkB,EAAE,SAAS,CAAC;AAAA,IAC5C;AAAA,EACF,CAAC;AAED,QAAM,UAAU,CAAC,CAAC,MAAM,MAAM,UAAU,CAAC,MAAM,MAAM;AACrD,QAAM,QAAQ,MAAM,MAAM,SAAS;AAEnC,QAAM,MAAM,YAAY,MAAM;AAC5B,QAAI,MAAO,UAAS,OAAO,KAAK;AAAA,EAClC,GAAG,CAAC,OAAO,QAAQ,CAAC;AAEpB,SAAO,EAAE,SAAS,OAAO,IAAI;AAC/B;","names":["cycle"]}
@@ -101,6 +101,35 @@ class SubscriptionsService {
101
101
  }
102
102
  return { success: true };
103
103
  }
104
+ async getFreezeNotice() {
105
+ const { id_account } = await getUserContext();
106
+ const response = await api.apps.get(
107
+ `/accounts/${id_account}/notices/freeze`
108
+ );
109
+ if (response.status === 0 || !response.data) {
110
+ throw new ApiError(
111
+ (response.status === 0 ? response.message : "") || "Erro ao consultar aviso de congelamento",
112
+ "GET_FREEZE_NOTICE_FAILED",
113
+ 400
114
+ );
115
+ }
116
+ return response.data;
117
+ }
118
+ async ackFreezeNotice(cycle) {
119
+ const { id_account } = await getUserContext();
120
+ const response = await api.apps.post(
121
+ `/accounts/${id_account}/notices/freeze/ack`,
122
+ { cycle }
123
+ );
124
+ if (response.status === 0) {
125
+ throw new ApiError(
126
+ response.message || "Erro ao confirmar aviso de congelamento",
127
+ "ACK_FREEZE_NOTICE_FAILED",
128
+ 400
129
+ );
130
+ }
131
+ return { success: true };
132
+ }
104
133
  }
105
134
  const subscriptionsService = new SubscriptionsService();
106
135
  export {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/modules/subscriptions/services/subscriptions.service.ts"],"sourcesContent":["import \"server-only\";\r\n\r\nimport { api } from \"../../../infra/api/client\";\r\nimport {\r\n ApiError,\r\n ApiActionResult,\r\n ApiPaginatedActionResult,\r\n PaginatedSuccessResult,\r\n SuccessResult,\r\n} from \"../../../infra/api/types\";\r\nimport { buildQueryParams } from \"../../../infra/utils/params\";\r\nimport { assertManagementPermission } from \"../../auth/utils/assert-management-permission\";\r\nimport { getUserContext } from \"../../auth/utils/get-user-context\";\r\nimport {\r\n Subscription,\r\n SubscriptionSchema,\r\n FindSubscriptionsParams,\r\n} from \"../types/subscription.type\";\r\nimport {\r\n type CalculateSubscriptionRequest,\r\n type CalculateSubscriptionResponse,\r\n type UpdateSubscriptionPlanRequest,\r\n CalculateSubscriptionResponseSchema,\r\n} from \"../types/calculate-subscription.type\";\r\nimport {\r\n SubscriptionPendingPixResponseSchema,\r\n type SubscriptionPendingPixResponse,\r\n} from \"../types/pix-pending.type\";\r\n\r\n/**\r\n * Timeout das mutations de assinatura que ESCREVEM (action/plan e action/payment).\r\n *\r\n * O default do ApiClient é 30s. Essas duas rotas caem no mesmo motor pesado do\r\n * backend (planEdit delega pra paymentEdit) — troca de método de pagamento, mandato\r\n * PIX recorrente Woovi, void-and-replace de fatura — e passam de 30s com alguma\r\n * frequência: em produção elas aparecem como `outcome=canceled` com wallTime ~29.9s.\r\n *\r\n * O problema não é o usuário ver \"Tempo excedido\": é que o abort MATA o worker no\r\n * meio da saga. Se o corte cai depois de criar o mandato Woovi e suprimir a fatura\r\n * de ciclo, sobra mandato órfão + fatura sem dunning, o usuário não recebe QR nenhum\r\n * e a próxima tentativa cria um segundo mandato. Deixar a request terminar converte\r\n * uma escrita parcial numa escrita completa.\r\n *\r\n * Não é a solução definitiva — a rota deveria ser assíncrona (202 + polling no\r\n * correlation_id). É o teto que dá folga enquanto isso.\r\n *\r\n * Só nas mutations: calculateSubscription é idempotente (não escreve nada), então\r\n * um timeout nela não deixa rastro e não precisa esperar mais.\r\n */\r\nconst SUBSCRIPTION_WRITE_TIMEOUT_MS = 60000;\r\n\r\nexport type UpdateSubscriptionPlanResponse =\r\n | Subscription\r\n | SubscriptionPendingPixResponse;\r\n\r\nclass SubscriptionsService {\r\n async listSubscriptions(\r\n params?: Partial<FindSubscriptionsParams>,\r\n ): Promise<PaginatedSuccessResult<Subscription>> {\r\n const { id_account } = await getUserContext();\r\n\r\n const query = buildQueryParams(\r\n params as Record<string, string | boolean | undefined>,\r\n );\r\n const url = `/accounts/${id_account}/subscriptions${query ? `?${query}` : \"\"}`;\r\n\r\n const response =\r\n await api.apps.get<ApiPaginatedActionResult<Subscription>>(url);\r\n\r\n if (response.status === 0) {\r\n throw new ApiError(\r\n response.message || \"Erro ao listar assinaturas\",\r\n \"LIST_SUBSCRIPTIONS_FAILED\",\r\n 400,\r\n );\r\n }\r\n\r\n const data = response.data.map((item) => SubscriptionSchema.parse(item));\r\n\r\n return {\r\n data,\r\n total: response.total,\r\n success: true,\r\n } satisfies PaginatedSuccessResult<Subscription>;\r\n }\r\n\r\n async calculateSubscription(\r\n data: CalculateSubscriptionRequest,\r\n ): Promise<SuccessResult<CalculateSubscriptionResponse>> {\r\n const { id_account } = await getUserContext();\r\n\r\n const response = await api.apps.post<\r\n ApiActionResult<CalculateSubscriptionResponse>\r\n >(`/accounts/${id_account}/subscriptions/action/calculate`, data);\r\n\r\n console.log(\"Calculate subscription response:\", {\r\n response,\r\n data: JSON.stringify(data, null, 2),\r\n });\r\n\r\n if (response.status === 0) {\r\n throw new ApiError(\r\n response.message || \"Erro ao calcular assinatura\",\r\n \"CALCULATE_SUBSCRIPTION_FAILED\",\r\n 400,\r\n );\r\n }\r\n\r\n return {\r\n success: true,\r\n data: CalculateSubscriptionResponseSchema.parse(response.data),\r\n };\r\n }\r\n\r\n async updateSubscriptionPlan(\r\n subscriptionId: number | string,\r\n data: UpdateSubscriptionPlanRequest,\r\n ): Promise<\r\n SuccessResult<UpdateSubscriptionPlanResponse | null> & { client_secret?: string }\r\n > {\r\n await assertManagementPermission('manage_subscriptions');\r\n const { id_account } = await getUserContext();\r\n\r\n const response = await api.apps.put<\r\n ApiActionResult<UpdateSubscriptionPlanResponse | UpdateSubscriptionPlanResponse[]> & {\r\n client_secret?: string;\r\n }\r\n >(\r\n `/accounts/${id_account}/subscriptions/${subscriptionId}/action/plan`,\r\n data,\r\n { timeout: SUBSCRIPTION_WRITE_TIMEOUT_MS },\r\n );\r\n\r\n if (response.status === 0) {\r\n throw new ApiError(\r\n response.message || \"Erro ao atualizar plano\",\r\n \"UPDATE_SUBSCRIPTION_PLAN_FAILED\",\r\n 400,\r\n );\r\n }\r\n\r\n const raw = Array.isArray(response.data) ? response.data[0] : response.data;\r\n\r\n // Preserva resposta pix-pending (id: null, pending: true, pix: {...})\r\n // pra clientes poderem rotear o user pra fluxo dedicado de pix em vez\r\n // de tratar como subscription comum. Caso contrário, parseia como\r\n // Subscription (compatível com o comportamento antigo de devolver { id }).\r\n let parsed: UpdateSubscriptionPlanResponse | null = null;\r\n if (raw) {\r\n const pixResult = SubscriptionPendingPixResponseSchema.safeParse(raw);\r\n parsed = pixResult.success ? pixResult.data : SubscriptionSchema.parse(raw);\r\n }\r\n\r\n return {\r\n success: true,\r\n data: parsed,\r\n ...(response.client_secret ? { client_secret: response.client_secret } : {}),\r\n };\r\n }\r\n\r\n async updateSubscriptionPayment(\r\n subscriptionId: number | string,\r\n data: { id_card: string; payment_method: number },\r\n ): Promise<SuccessResult<void>> {\r\n await assertManagementPermission(\"manage_subscriptions\");\r\n const { id_account } = await getUserContext();\r\n\r\n const response = await api.apps.put<ApiActionResult<void>>(\r\n `/accounts/${id_account}/subscriptions/${subscriptionId}/action/payment`,\r\n data,\r\n { timeout: SUBSCRIPTION_WRITE_TIMEOUT_MS },\r\n );\r\n\r\n if (response.status === 0) {\r\n throw new ApiError(\r\n response.message || \"Erro ao atualizar método de pagamento\",\r\n \"UPDATE_SUBSCRIPTION_PAYMENT_FAILED\",\r\n 400,\r\n );\r\n }\r\n\r\n return { success: true };\r\n }\r\n}\r\n\r\nexport const subscriptionsService = new SubscriptionsService();\r\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../../src/modules/subscriptions/services/subscriptions.service.ts"],"sourcesContent":["import \"server-only\";\r\n\r\nimport { api } from \"../../../infra/api/client\";\r\nimport {\r\n ApiError,\r\n ApiActionResult,\r\n ApiPaginatedActionResult,\r\n PaginatedSuccessResult,\r\n SuccessResult,\r\n} from \"../../../infra/api/types\";\r\nimport { buildQueryParams } from \"../../../infra/utils/params\";\r\nimport { assertManagementPermission } from \"../../auth/utils/assert-management-permission\";\r\nimport { getUserContext } from \"../../auth/utils/get-user-context\";\r\nimport {\r\n Subscription,\r\n SubscriptionSchema,\r\n FindSubscriptionsParams,\r\n} from \"../types/subscription.type\";\r\nimport {\r\n type CalculateSubscriptionRequest,\r\n type CalculateSubscriptionResponse,\r\n type UpdateSubscriptionPlanRequest,\r\n CalculateSubscriptionResponseSchema,\r\n} from \"../types/calculate-subscription.type\";\r\nimport {\r\n SubscriptionPendingPixResponseSchema,\r\n type SubscriptionPendingPixResponse,\r\n} from \"../types/pix-pending.type\";\r\nimport type { FreezeNotice } from \"../types/freeze-notice.type\";\r\n\r\n/**\r\n * Timeout das mutations de assinatura que ESCREVEM (action/plan e action/payment).\r\n *\r\n * O default do ApiClient é 30s. Essas duas rotas caem no mesmo motor pesado do\r\n * backend (planEdit delega pra paymentEdit) — troca de método de pagamento, mandato\r\n * PIX recorrente Woovi, void-and-replace de fatura — e passam de 30s com alguma\r\n * frequência: em produção elas aparecem como `outcome=canceled` com wallTime ~29.9s.\r\n *\r\n * O problema não é o usuário ver \"Tempo excedido\": é que o abort MATA o worker no\r\n * meio da saga. Se o corte cai depois de criar o mandato Woovi e suprimir a fatura\r\n * de ciclo, sobra mandato órfão + fatura sem dunning, o usuário não recebe QR nenhum\r\n * e a próxima tentativa cria um segundo mandato. Deixar a request terminar converte\r\n * uma escrita parcial numa escrita completa.\r\n *\r\n * Não é a solução definitiva — a rota deveria ser assíncrona (202 + polling no\r\n * correlation_id). É o teto que dá folga enquanto isso.\r\n *\r\n * Só nas mutations: calculateSubscription é idempotente (não escreve nada), então\r\n * um timeout nela não deixa rastro e não precisa esperar mais.\r\n */\r\nconst SUBSCRIPTION_WRITE_TIMEOUT_MS = 60000;\r\n\r\nexport type UpdateSubscriptionPlanResponse =\r\n | Subscription\r\n | SubscriptionPendingPixResponse;\r\n\r\nclass SubscriptionsService {\r\n async listSubscriptions(\r\n params?: Partial<FindSubscriptionsParams>,\r\n ): Promise<PaginatedSuccessResult<Subscription>> {\r\n const { id_account } = await getUserContext();\r\n\r\n const query = buildQueryParams(\r\n params as Record<string, string | boolean | undefined>,\r\n );\r\n const url = `/accounts/${id_account}/subscriptions${query ? `?${query}` : \"\"}`;\r\n\r\n const response =\r\n await api.apps.get<ApiPaginatedActionResult<Subscription>>(url);\r\n\r\n if (response.status === 0) {\r\n throw new ApiError(\r\n response.message || \"Erro ao listar assinaturas\",\r\n \"LIST_SUBSCRIPTIONS_FAILED\",\r\n 400,\r\n );\r\n }\r\n\r\n const data = response.data.map((item) => SubscriptionSchema.parse(item));\r\n\r\n return {\r\n data,\r\n total: response.total,\r\n success: true,\r\n } satisfies PaginatedSuccessResult<Subscription>;\r\n }\r\n\r\n async calculateSubscription(\r\n data: CalculateSubscriptionRequest,\r\n ): Promise<SuccessResult<CalculateSubscriptionResponse>> {\r\n const { id_account } = await getUserContext();\r\n\r\n const response = await api.apps.post<\r\n ApiActionResult<CalculateSubscriptionResponse>\r\n >(`/accounts/${id_account}/subscriptions/action/calculate`, data);\r\n\r\n console.log(\"Calculate subscription response:\", {\r\n response,\r\n data: JSON.stringify(data, null, 2),\r\n });\r\n\r\n if (response.status === 0) {\r\n throw new ApiError(\r\n response.message || \"Erro ao calcular assinatura\",\r\n \"CALCULATE_SUBSCRIPTION_FAILED\",\r\n 400,\r\n );\r\n }\r\n\r\n return {\r\n success: true,\r\n data: CalculateSubscriptionResponseSchema.parse(response.data),\r\n };\r\n }\r\n\r\n async updateSubscriptionPlan(\r\n subscriptionId: number | string,\r\n data: UpdateSubscriptionPlanRequest,\r\n ): Promise<\r\n SuccessResult<UpdateSubscriptionPlanResponse | null> & { client_secret?: string }\r\n > {\r\n await assertManagementPermission('manage_subscriptions');\r\n const { id_account } = await getUserContext();\r\n\r\n const response = await api.apps.put<\r\n ApiActionResult<UpdateSubscriptionPlanResponse | UpdateSubscriptionPlanResponse[]> & {\r\n client_secret?: string;\r\n }\r\n >(\r\n `/accounts/${id_account}/subscriptions/${subscriptionId}/action/plan`,\r\n data,\r\n { timeout: SUBSCRIPTION_WRITE_TIMEOUT_MS },\r\n );\r\n\r\n if (response.status === 0) {\r\n throw new ApiError(\r\n response.message || \"Erro ao atualizar plano\",\r\n \"UPDATE_SUBSCRIPTION_PLAN_FAILED\",\r\n 400,\r\n );\r\n }\r\n\r\n const raw = Array.isArray(response.data) ? response.data[0] : response.data;\r\n\r\n // Preserva resposta pix-pending (id: null, pending: true, pix: {...})\r\n // pra clientes poderem rotear o user pra fluxo dedicado de pix em vez\r\n // de tratar como subscription comum. Caso contrário, parseia como\r\n // Subscription (compatível com o comportamento antigo de devolver { id }).\r\n let parsed: UpdateSubscriptionPlanResponse | null = null;\r\n if (raw) {\r\n const pixResult = SubscriptionPendingPixResponseSchema.safeParse(raw);\r\n parsed = pixResult.success ? pixResult.data : SubscriptionSchema.parse(raw);\r\n }\r\n\r\n return {\r\n success: true,\r\n data: parsed,\r\n ...(response.client_secret ? { client_secret: response.client_secret } : {}),\r\n };\r\n }\r\n\r\n async updateSubscriptionPayment(\r\n subscriptionId: number | string,\r\n data: { id_card: string; payment_method: number },\r\n ): Promise<SuccessResult<void>> {\r\n await assertManagementPermission(\"manage_subscriptions\");\r\n const { id_account } = await getUserContext();\r\n\r\n const response = await api.apps.put<ApiActionResult<void>>(\r\n `/accounts/${id_account}/subscriptions/${subscriptionId}/action/payment`,\r\n data,\r\n { timeout: SUBSCRIPTION_WRITE_TIMEOUT_MS },\r\n );\r\n\r\n if (response.status === 0) {\r\n throw new ApiError(\r\n response.message || \"Erro ao atualizar método de pagamento\",\r\n \"UPDATE_SUBSCRIPTION_PAYMENT_FAILED\",\r\n 400,\r\n );\r\n }\r\n\r\n return { success: true };\r\n }\r\n\r\n async getFreezeNotice(): Promise<FreezeNotice> {\r\n const { id_account } = await getUserContext();\r\n\r\n const response = await api.apps.get<ApiActionResult<FreezeNotice>>(\r\n `/accounts/${id_account}/notices/freeze`,\r\n );\r\n\r\n if (response.status === 0 || !response.data) {\r\n throw new ApiError(\r\n (response.status === 0 ? response.message : \"\") ||\r\n \"Erro ao consultar aviso de congelamento\",\r\n \"GET_FREEZE_NOTICE_FAILED\",\r\n 400,\r\n );\r\n }\r\n\r\n return response.data;\r\n }\r\n\r\n async ackFreezeNotice(cycle: string): Promise<SuccessResult<void>> {\r\n const { id_account } = await getUserContext();\r\n\r\n const response = await api.apps.post<ApiActionResult<void>>(\r\n `/accounts/${id_account}/notices/freeze/ack`,\r\n { cycle },\r\n );\r\n\r\n if (response.status === 0) {\r\n throw new ApiError(\r\n response.message || \"Erro ao confirmar aviso de congelamento\",\r\n \"ACK_FREEZE_NOTICE_FAILED\",\r\n 400,\r\n );\r\n }\r\n\r\n return { success: true };\r\n }\r\n}\r\n\r\nexport const subscriptionsService = new SubscriptionsService();\r\n"],"mappings":"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","names":[]}
@@ -0,0 +1 @@
1
+ //# sourceMappingURL=freeze-notice.type.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,19 @@
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
+ function getAccountFreezeDate(subscription) {
7
+ if (!subscription) return null;
8
+ if (subscription.type !== "client") return null;
9
+ if (Number(subscription.id_wl) !== FREEZE_ELIGIBLE_WHITELABEL_ID) return null;
10
+ const dueDate = toCalendarDate(subscription.date_due);
11
+ if (!dueDate) return null;
12
+ const result = new Date(dueDate);
13
+ result.setDate(result.getDate() + ACCOUNT_FREEZE_AFTER_DAYS);
14
+ return result;
15
+ }
16
+ export {
17
+ getAccountFreezeDate
18
+ };
19
+ //# sourceMappingURL=get-account-freeze-date.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../src/modules/subscriptions/utils/get-account-freeze-date.ts"],"sourcesContent":["import {\n ACCOUNT_FREEZE_AFTER_DAYS,\n FREEZE_ELIGIBLE_WHITELABEL_ID,\n} from \"../constants/subscription.constants\";\nimport { toCalendarDate } from \"../../../infra/utils/date\";\nimport type { Subscription } from \"../types/subscription.type\";\n\n/**\n * Data em que a conta é congelada por falta de pagamento: date_due + ACCOUNT_FREEZE_AFTER_DAYS.\n *\n * Só congela assinatura `client` da whitelabel 1 — MESMA régua do\n * `isSubscriptionFrozen` (gapps-r3-api) e do `isAccountFrozen` (gpages-r3-api).\n * Se as três divergirem, o usuário é bloqueado por uma camada e liberado por outra.\n *\n * - `type !== \"client\"` cobre free, trial, partner e whitelabel de uma vez (a coluna\n * aceita mais valores do que a lista que o SubscriptionBanner enumera).\n * - `id_wl !== 1`: o pipeline de aviso (cron `dailyNotifications` do gapps-r3-api) só\n * existe para a wl 1. Congelar conta de whitelabel seria bloquear sem nunca avisar —\n * o defeito do ID-5029 que este card existe para não repetir.\n */\nexport function getAccountFreezeDate(\n subscription:\n | Pick<Subscription, \"type\" | \"date_due\" | \"id_wl\">\n | undefined\n | null,\n): Date | null {\n if (!subscription) return null;\n\n if (subscription.type !== \"client\") return null;\n\n if (Number(subscription.id_wl) !== FREEZE_ELIGIBLE_WHITELABEL_ID) return null;\n\n const dueDate = toCalendarDate(subscription.date_due);\n\n if (!dueDate) return null;\n\n const result = new Date(dueDate);\n result.setDate(result.getDate() + ACCOUNT_FREEZE_AFTER_DAYS);\n return result;\n}\n"],"mappings":"AAAA;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,sBAAsB;AAgBxB,SAAS,qBACd,cAIa;AACb,MAAI,CAAC,aAAc,QAAO;AAE1B,MAAI,aAAa,SAAS,SAAU,QAAO;AAE3C,MAAI,OAAO,aAAa,KAAK,MAAM,8BAA+B,QAAO;AAEzE,QAAM,UAAU,eAAe,aAAa,QAAQ;AAEpD,MAAI,CAAC,QAAS,QAAO;AAErB,QAAM,SAAS,IAAI,KAAK,OAAO;AAC/B,SAAO,QAAQ,OAAO,QAAQ,IAAI,yBAAyB;AAC3D,SAAO;AACT;","names":[]}
@@ -0,0 +1,21 @@
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
+ function getAccountFreezeState(subscription) {
6
+ if (!subscription) return "none";
7
+ if (getSubscriptionCancellationState(subscription) !== "none") return "none";
8
+ const freezeDate = getAccountFreezeDate(subscription);
9
+ if (!freezeDate) return "none";
10
+ const today = toCalendarDate(/* @__PURE__ */ new Date());
11
+ if (!today) return "none";
12
+ if (today.getTime() >= freezeDate.getTime()) return "frozen";
13
+ const warningDate = new Date(freezeDate);
14
+ warningDate.setDate(warningDate.getDate() - FREEZE_WARNING_DAYS_BEFORE);
15
+ if (today.getTime() >= warningDate.getTime()) return "warning";
16
+ return "none";
17
+ }
18
+ export {
19
+ getAccountFreezeState
20
+ };
21
+ //# sourceMappingURL=get-account-freeze-state.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../src/modules/subscriptions/utils/get-account-freeze-state.ts"],"sourcesContent":["import { FREEZE_WARNING_DAYS_BEFORE } from \"../constants/subscription.constants\";\nimport { toCalendarDate } from \"../../../infra/utils/date\";\nimport { getAccountFreezeDate } from \"./get-account-freeze-date\";\nimport { getSubscriptionCancellationState } from \"./get-subscription-cancellation-state\";\nimport type { Subscription } from \"../types/subscription.type\";\n\nexport type AccountFreezeState = \"none\" | \"warning\" | \"frozen\";\n\n/**\n * Estado de congelamento por falta de pagamento (date_due + ACCOUNT_FREEZE_AFTER_DAYS).\n *\n * Conta cancelada/pendente de cancelamento é tratada pelos ramos de cancelamento\n * (mesma ordem do SubscriptionBanner) — aqui sempre \"none\" nesse caso, pra não\n * disputar mensagem com o CancelledSubscriptionBanner/PendingCancellationBanner.\n */\nexport function getAccountFreezeState(\n subscription:\n | Pick<\n Subscription,\n \"type\" | \"active\" | \"date_due\" | \"date_cancellation\" | \"id_wl\"\n >\n | undefined\n | null,\n): AccountFreezeState {\n if (!subscription) return \"none\";\n\n if (getSubscriptionCancellationState(subscription) !== \"none\") return \"none\";\n\n const freezeDate = getAccountFreezeDate(subscription);\n if (!freezeDate) return \"none\";\n\n const today = toCalendarDate(new Date());\n if (!today) return \"none\";\n\n if (today.getTime() >= freezeDate.getTime()) return \"frozen\";\n\n const warningDate = new Date(freezeDate);\n warningDate.setDate(warningDate.getDate() - FREEZE_WARNING_DAYS_BEFORE);\n\n if (today.getTime() >= warningDate.getTime()) return \"warning\";\n\n return \"none\";\n}\n"],"mappings":"AAAA,SAAS,kCAAkC;AAC3C,SAAS,sBAAsB;AAC/B,SAAS,4BAA4B;AACrC,SAAS,wCAAwC;AAY1C,SAAS,sBACd,cAOoB;AACpB,MAAI,CAAC,aAAc,QAAO;AAE1B,MAAI,iCAAiC,YAAY,MAAM,OAAQ,QAAO;AAEtE,QAAM,aAAa,qBAAqB,YAAY;AACpD,MAAI,CAAC,WAAY,QAAO;AAExB,QAAM,QAAQ,eAAe,oBAAI,KAAK,CAAC;AACvC,MAAI,CAAC,MAAO,QAAO;AAEnB,MAAI,MAAM,QAAQ,KAAK,WAAW,QAAQ,EAAG,QAAO;AAEpD,QAAM,cAAc,IAAI,KAAK,UAAU;AACvC,cAAY,QAAQ,YAAY,QAAQ,IAAI,0BAA0B;AAEtE,MAAI,MAAM,QAAQ,KAAK,YAAY,QAAQ,EAAG,QAAO;AAErD,SAAO;AACT;","names":[]}
package/dist/server.mjs CHANGED
@@ -7,6 +7,7 @@ import { assertManagementPermission } from "./modules/auth/utils/assert-manageme
7
7
  import { assertManagementSubscription } from "./modules/auth/utils/assert-management-subscription";
8
8
  import { assertSubscriptionAddon } from "./modules/auth/utils/assert-subscription-addon";
9
9
  import { assertPaidSubscription } from "./modules/auth/utils/assert-paid-subscription";
10
+ import { assertAccountNotFrozen } from "./modules/auth/utils/assert-account-not-frozen";
10
11
  import { subscriptionsService } from "./modules/subscriptions/services/subscriptions.service";
11
12
  import { plansService } from "./modules/plans/services/plans.service";
12
13
  import { cardsService } from "./modules/cards/services/cards.service";
@@ -21,6 +22,7 @@ import { findUserById } from "./modules/users/action/find-user-by-id.action";
21
22
  import { findCurrentAccount } from "./modules/accounts/actions/find-current-account.action";
22
23
  import { getAccountTokenAction } from "./modules/accounts/actions/get-account-token.action";
23
24
  import { listSubscriptionsAction } from "./modules/subscriptions/actions/list-subscriptions.action";
25
+ import { ackFreezeNoticeAction } from "./modules/subscriptions/actions/ack-freeze-notice.action";
24
26
  import { calculateSubscriptionAction } from "./modules/subscriptions/actions/calculate-subscription.action";
25
27
  import { updateSubscriptionPlanAction } from "./modules/subscriptions/actions/update-subscription-plan.action";
26
28
  import { updateSubscriptionPaymentAction } from "./modules/subscriptions/actions/update-subscription-payment.action";
@@ -73,9 +75,11 @@ import { processImage } from "./modules/images/services/image-processing.service
73
75
  export {
74
76
  ApiClient,
75
77
  accountService,
78
+ ackFreezeNoticeAction,
76
79
  addProjectUserAction,
77
80
  api,
78
81
  apiClient,
82
+ assertAccountNotFrozen,
79
83
  assertManagementPermission,
80
84
  assertManagementSubscription,
81
85
  assertPaidSubscription,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/server.ts"],"sourcesContent":["import 'server-only';\r\n\r\n// API Client\r\nexport { ApiClient, api, apiClient } from './infra/api/client';\r\n\r\n// Services\r\nexport { authService } from './modules/auth/services/auth.service';\r\nexport { whitelabelService } from './modules/whitelabel/services/whitelabel.service';\r\nexport { revalidateWhitelabelAction } from './modules/whitelabel/actions/revalidate-whitelabel.action';\r\nexport { assertManagementPermission } from './modules/auth/utils/assert-management-permission';\r\nexport { assertManagementSubscription } from './modules/auth/utils/assert-management-subscription';\r\nexport { assertSubscriptionAddon } from './modules/auth/utils/assert-subscription-addon';\r\nexport { assertPaidSubscription } from './modules/auth/utils/assert-paid-subscription';\r\nexport { subscriptionsService } from './modules/subscriptions/services/subscriptions.service';\r\nexport { plansService } from './modules/plans/services/plans.service';\r\nexport { cardsService } from './modules/cards/services/cards.service';\r\nexport { chargesService } from './modules/charges/services/charges.service';\r\nexport { iaCreditsService } from './modules/ia-credits/services/ia-credits.service';\r\n\r\n// Actions\r\nexport { findWhitelabel } from './modules/whitelabel/actions/find-whitelabel.action';\r\nexport { validateSessionAction } from './modules/auth/actions/validate-session.action';\r\nexport { socialGoogleLoginAction } from './modules/auth/actions/social-google-login.action';\r\nexport { socialGoogleOnboardingAction } from './modules/auth/actions/social-google-onboarding.action';\r\nexport { unlinkGoogleAction } from './modules/auth/actions/unlink-google.action';\r\nexport { findUserById } from './modules/users/action/find-user-by-id.action';\r\nexport { findCurrentAccount } from './modules/accounts/actions/find-current-account.action';\r\nexport { getAccountTokenAction } from './modules/accounts/actions/get-account-token.action';\r\nexport { listSubscriptionsAction } from './modules/subscriptions/actions/list-subscriptions.action';\r\nexport { calculateSubscriptionAction } from './modules/subscriptions/actions/calculate-subscription.action';\r\nexport { updateSubscriptionPlanAction } from './modules/subscriptions/actions/update-subscription-plan.action';\r\nexport { updateSubscriptionPaymentAction } from './modules/subscriptions/actions/update-subscription-payment.action';\r\nexport { createCardAction } from './modules/cards/actions/create-card.action';\r\nexport { createSetupIntentAction } from './modules/cards/actions/create-setup-intent.action';\r\nexport { chargeActionAction } from './modules/charges/actions/charge-action.action';\r\nexport { resolvePlanExtrasPrices } from './modules/subscriptions/utils/resolve-plan-extras-prices';\r\nexport { hasSubscriptionExpired } from './modules/subscriptions/utils/has-subscription-expired';\r\nexport { hasActivePaidSubscription } from './modules/subscriptions/utils/has-active-paid-subscription';\r\n\r\n// Project Services & Actions (server-only)\r\nexport { projectsService } from './modules/projects/services/projects.service';\r\nexport { listProjectsAction } from './modules/projects/actions/list-projects.action';\r\nexport { findProjectAction } from './modules/projects/actions/find-project.action';\r\nexport { createProjectAction } from './modules/projects/actions/create-project.action';\r\nexport { updateProjectAction } from './modules/projects/actions/update-project.action';\r\nexport { deleteProjectAction } from './modules/projects/actions/delete-project.action';\r\n\r\n// Project Users Services & Actions (server-only)\r\nexport { projectUsersService } from './modules/projects/services/project-users.service';\r\nexport { listProjectUsersAction } from './modules/projects/actions/list-project-users.action';\r\nexport { addProjectUserAction } from './modules/projects/actions/add-project-user.action';\r\nexport { removeProjectUserAction } from './modules/projects/actions/remove-project-user.action';\r\n\r\n// Account Services & Actions (server-only)\r\nexport { accountService } from './modules/accounts/services/account.service';\r\nexport { twoFactorService } from './modules/accounts/services/two-factor.service';\r\nexport {\r\n listAccountUsersAction,\r\n updateUserAction,\r\n updateAccountAction,\r\n updateAccountUserByIdAction,\r\n updateBillingDataAction,\r\n deleteAccountUserAction,\r\n deleteAccountAction,\r\n createAccountUserAction,\r\n changePasswordAction,\r\n requestEmailChangeAction,\r\n confirmEmailChangeAction,\r\n requestPhoneChangeAction,\r\n confirmPhoneChangeAction,\r\n generateTwoFactorAction,\r\n confirmTwoFactorAction,\r\n disableTwoFactorAction,\r\n} from './modules/accounts/actions/account-management.action';\r\n\r\n// Server Utils\r\nexport { getClientInfoFromRequest } from './infra/utils/client-info';\r\nexport { getUserContext } from './modules/auth/utils/get-user-context';\r\nexport { resolveLocale } from './i18n/resolve-locale';\r\nexport type { ResolveLocaleOptions } from './i18n/resolve-locale';\r\nexport { requireValidSession } from './modules/auth/utils/require-valid-session';\r\nexport { requireTokenNotExpired } from './modules/auth/utils/require-token-not-expired';\r\nexport { buildWlOverride, buildWlOverrideFromJwt } from './modules/auth/utils/build-wl-override';\r\nexport { safeServerAction } from './utils/safeServerAction';\r\nexport { safeMutationAction } from './utils/safeMutationAction';\r\n\r\n// Image Processing (server-side)\r\nexport { processImageAction } from './modules/images/actions/process-image.action';\r\nexport { processImage } from './modules/images/services/image-processing.service';\r\nexport type {\r\n ProcessImageInput,\r\n ProcessImageOutput,\r\n ProcessImageActionInput,\r\n ProcessImageActionOutput,\r\n} from './modules/images/types/image.type';\r\n"],"mappings":"AAAA,OAAO;AAGP,SAAS,WAAW,KAAK,iBAAiB;AAG1C,SAAS,mBAAmB;AAC5B,SAAS,yBAAyB;AAClC,SAAS,kCAAkC;AAC3C,SAAS,kCAAkC;AAC3C,SAAS,oCAAoC;AAC7C,SAAS,+BAA+B;AACxC,SAAS,8BAA8B;AACvC,SAAS,4BAA4B;AACrC,SAAS,oBAAoB;AAC7B,SAAS,oBAAoB;AAC7B,SAAS,sBAAsB;AAC/B,SAAS,wBAAwB;AAGjC,SAAS,sBAAsB;AAC/B,SAAS,6BAA6B;AACtC,SAAS,+BAA+B;AACxC,SAAS,oCAAoC;AAC7C,SAAS,0BAA0B;AACnC,SAAS,oBAAoB;AAC7B,SAAS,0BAA0B;AACnC,SAAS,6BAA6B;AACtC,SAAS,+BAA+B;AACxC,SAAS,mCAAmC;AAC5C,SAAS,oCAAoC;AAC7C,SAAS,uCAAuC;AAChD,SAAS,wBAAwB;AACjC,SAAS,+BAA+B;AACxC,SAAS,0BAA0B;AACnC,SAAS,+BAA+B;AACxC,SAAS,8BAA8B;AACvC,SAAS,iCAAiC;AAG1C,SAAS,uBAAuB;AAChC,SAAS,0BAA0B;AACnC,SAAS,yBAAyB;AAClC,SAAS,2BAA2B;AACpC,SAAS,2BAA2B;AACpC,SAAS,2BAA2B;AAGpC,SAAS,2BAA2B;AACpC,SAAS,8BAA8B;AACvC,SAAS,4BAA4B;AACrC,SAAS,+BAA+B;AAGxC,SAAS,sBAAsB;AAC/B,SAAS,wBAAwB;AACjC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAGP,SAAS,gCAAgC;AACzC,SAAS,sBAAsB;AAC/B,SAAS,qBAAqB;AAE9B,SAAS,2BAA2B;AACpC,SAAS,8BAA8B;AACvC,SAAS,iBAAiB,8BAA8B;AACxD,SAAS,wBAAwB;AACjC,SAAS,0BAA0B;AAGnC,SAAS,0BAA0B;AACnC,SAAS,oBAAoB;","names":[]}
1
+ {"version":3,"sources":["../src/server.ts"],"sourcesContent":["import 'server-only';\r\n\r\n// API Client\r\nexport { ApiClient, api, apiClient } from './infra/api/client';\r\n\r\n// Services\r\nexport { authService } from './modules/auth/services/auth.service';\r\nexport { whitelabelService } from './modules/whitelabel/services/whitelabel.service';\r\nexport { revalidateWhitelabelAction } from './modules/whitelabel/actions/revalidate-whitelabel.action';\r\nexport { assertManagementPermission } from './modules/auth/utils/assert-management-permission';\r\nexport { assertManagementSubscription } from './modules/auth/utils/assert-management-subscription';\r\nexport { assertSubscriptionAddon } from './modules/auth/utils/assert-subscription-addon';\r\nexport { assertPaidSubscription } from './modules/auth/utils/assert-paid-subscription';\r\nexport { assertAccountNotFrozen } from './modules/auth/utils/assert-account-not-frozen';\r\nexport { subscriptionsService } from './modules/subscriptions/services/subscriptions.service';\r\nexport { plansService } from './modules/plans/services/plans.service';\r\nexport { cardsService } from './modules/cards/services/cards.service';\r\nexport { chargesService } from './modules/charges/services/charges.service';\r\nexport { iaCreditsService } from './modules/ia-credits/services/ia-credits.service';\r\n\r\n// Actions\r\nexport { findWhitelabel } from './modules/whitelabel/actions/find-whitelabel.action';\r\nexport { validateSessionAction } from './modules/auth/actions/validate-session.action';\r\nexport { socialGoogleLoginAction } from './modules/auth/actions/social-google-login.action';\r\nexport { socialGoogleOnboardingAction } from './modules/auth/actions/social-google-onboarding.action';\r\nexport { unlinkGoogleAction } from './modules/auth/actions/unlink-google.action';\r\nexport { findUserById } from './modules/users/action/find-user-by-id.action';\r\nexport { findCurrentAccount } from './modules/accounts/actions/find-current-account.action';\r\nexport { getAccountTokenAction } from './modules/accounts/actions/get-account-token.action';\r\nexport { listSubscriptionsAction } from './modules/subscriptions/actions/list-subscriptions.action';\r\nexport { ackFreezeNoticeAction } from './modules/subscriptions/actions/ack-freeze-notice.action';\r\nexport { calculateSubscriptionAction } from './modules/subscriptions/actions/calculate-subscription.action';\r\nexport { updateSubscriptionPlanAction } from './modules/subscriptions/actions/update-subscription-plan.action';\r\nexport { updateSubscriptionPaymentAction } from './modules/subscriptions/actions/update-subscription-payment.action';\r\nexport { createCardAction } from './modules/cards/actions/create-card.action';\r\nexport { createSetupIntentAction } from './modules/cards/actions/create-setup-intent.action';\r\nexport { chargeActionAction } from './modules/charges/actions/charge-action.action';\r\nexport { resolvePlanExtrasPrices } from './modules/subscriptions/utils/resolve-plan-extras-prices';\r\nexport { hasSubscriptionExpired } from './modules/subscriptions/utils/has-subscription-expired';\r\nexport { hasActivePaidSubscription } from './modules/subscriptions/utils/has-active-paid-subscription';\r\n\r\n// Project Services & Actions (server-only)\r\nexport { projectsService } from './modules/projects/services/projects.service';\r\nexport { listProjectsAction } from './modules/projects/actions/list-projects.action';\r\nexport { findProjectAction } from './modules/projects/actions/find-project.action';\r\nexport { createProjectAction } from './modules/projects/actions/create-project.action';\r\nexport { updateProjectAction } from './modules/projects/actions/update-project.action';\r\nexport { deleteProjectAction } from './modules/projects/actions/delete-project.action';\r\n\r\n// Project Users Services & Actions (server-only)\r\nexport { projectUsersService } from './modules/projects/services/project-users.service';\r\nexport { listProjectUsersAction } from './modules/projects/actions/list-project-users.action';\r\nexport { addProjectUserAction } from './modules/projects/actions/add-project-user.action';\r\nexport { removeProjectUserAction } from './modules/projects/actions/remove-project-user.action';\r\n\r\n// Account Services & Actions (server-only)\r\nexport { accountService } from './modules/accounts/services/account.service';\r\nexport { twoFactorService } from './modules/accounts/services/two-factor.service';\r\nexport {\r\n listAccountUsersAction,\r\n updateUserAction,\r\n updateAccountAction,\r\n updateAccountUserByIdAction,\r\n updateBillingDataAction,\r\n deleteAccountUserAction,\r\n deleteAccountAction,\r\n createAccountUserAction,\r\n changePasswordAction,\r\n requestEmailChangeAction,\r\n confirmEmailChangeAction,\r\n requestPhoneChangeAction,\r\n confirmPhoneChangeAction,\r\n generateTwoFactorAction,\r\n confirmTwoFactorAction,\r\n disableTwoFactorAction,\r\n} from './modules/accounts/actions/account-management.action';\r\n\r\n// Server Utils\r\nexport { getClientInfoFromRequest } from './infra/utils/client-info';\r\nexport { getUserContext } from './modules/auth/utils/get-user-context';\r\nexport { resolveLocale } from './i18n/resolve-locale';\r\nexport type { ResolveLocaleOptions } from './i18n/resolve-locale';\r\nexport { requireValidSession } from './modules/auth/utils/require-valid-session';\r\nexport { requireTokenNotExpired } from './modules/auth/utils/require-token-not-expired';\r\nexport { buildWlOverride, buildWlOverrideFromJwt } from './modules/auth/utils/build-wl-override';\r\nexport { safeServerAction } from './utils/safeServerAction';\r\nexport { safeMutationAction } from './utils/safeMutationAction';\r\n\r\n// Image Processing (server-side)\r\nexport { processImageAction } from './modules/images/actions/process-image.action';\r\nexport { processImage } from './modules/images/services/image-processing.service';\r\nexport type {\r\n ProcessImageInput,\r\n ProcessImageOutput,\r\n ProcessImageActionInput,\r\n ProcessImageActionOutput,\r\n} from './modules/images/types/image.type';\r\n"],"mappings":"AAAA,OAAO;AAGP,SAAS,WAAW,KAAK,iBAAiB;AAG1C,SAAS,mBAAmB;AAC5B,SAAS,yBAAyB;AAClC,SAAS,kCAAkC;AAC3C,SAAS,kCAAkC;AAC3C,SAAS,oCAAoC;AAC7C,SAAS,+BAA+B;AACxC,SAAS,8BAA8B;AACvC,SAAS,8BAA8B;AACvC,SAAS,4BAA4B;AACrC,SAAS,oBAAoB;AAC7B,SAAS,oBAAoB;AAC7B,SAAS,sBAAsB;AAC/B,SAAS,wBAAwB;AAGjC,SAAS,sBAAsB;AAC/B,SAAS,6BAA6B;AACtC,SAAS,+BAA+B;AACxC,SAAS,oCAAoC;AAC7C,SAAS,0BAA0B;AACnC,SAAS,oBAAoB;AAC7B,SAAS,0BAA0B;AACnC,SAAS,6BAA6B;AACtC,SAAS,+BAA+B;AACxC,SAAS,6BAA6B;AACtC,SAAS,mCAAmC;AAC5C,SAAS,oCAAoC;AAC7C,SAAS,uCAAuC;AAChD,SAAS,wBAAwB;AACjC,SAAS,+BAA+B;AACxC,SAAS,0BAA0B;AACnC,SAAS,+BAA+B;AACxC,SAAS,8BAA8B;AACvC,SAAS,iCAAiC;AAG1C,SAAS,uBAAuB;AAChC,SAAS,0BAA0B;AACnC,SAAS,yBAAyB;AAClC,SAAS,2BAA2B;AACpC,SAAS,2BAA2B;AACpC,SAAS,2BAA2B;AAGpC,SAAS,2BAA2B;AACpC,SAAS,8BAA8B;AACvC,SAAS,4BAA4B;AACrC,SAAS,+BAA+B;AAGxC,SAAS,sBAAsB;AAC/B,SAAS,wBAAwB;AACjC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAGP,SAAS,gCAAgC;AACzC,SAAS,sBAAsB;AAC/B,SAAS,qBAAqB;AAE9B,SAAS,2BAA2B;AACpC,SAAS,8BAA8B;AACvC,SAAS,iBAAiB,8BAA8B;AACxD,SAAS,wBAAwB;AACjC,SAAS,0BAA0B;AAGnC,SAAS,0BAA0B;AACnC,SAAS,oBAAoB;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@greatapps/common",
3
- "version": "1.1.744",
3
+ "version": "1.1.746",
4
4
  "description": "Shared library for GreatApps frontend applications",
5
5
  "main": "./dist/index.mjs",
6
6
  "types": "./src/index.ts",
@@ -0,0 +1,106 @@
1
+ "use client";
2
+
3
+ import { IconLock, IconAlertTriangle } from '@tabler/icons-react';
4
+ import { useTranslations } from 'next-intl';
5
+ import { Button } from '../ui/buttons/Button';
6
+ import { Separator } from '../ui/data-display/Separator';
7
+ import {
8
+ Dialog,
9
+ DialogContent,
10
+ DialogDescription,
11
+ DialogHeader,
12
+ DialogTitle,
13
+ } from '../ui/overlay/Dialog';
14
+ import { useWhitelabelUrls, useExternalContracting } from '../../providers/whitelabel.provider';
15
+ import { useModalManager } from '../../store/useModalManager';
16
+ import { formatShortDate } from '../../infra/utils/date';
17
+
18
+ /* Lista do que o congelamento tira, na ordem do card. O texto mora no catálogo. */
19
+ const BLOCKED_RESOURCE_KEYS = [
20
+ 'pages',
21
+ 'projects',
22
+ 'leadsExport',
23
+ 'pageSettings',
24
+ 'domains',
25
+ 'users',
26
+ 'abTests',
27
+ 'uploads',
28
+ 'integrations',
29
+ ] as const;
30
+
31
+ /* `freezeDate` no data = ainda VAI congelar (véspera); sem ele = já congelado. As duas
32
+ * variantes listam os mesmos recursos: o ponto do "Detalhes" no banner é o usuário saber o
33
+ * que perde ANTES de perder. */
34
+ interface FrozenModalData {
35
+ freezeDate?: Date | string | null;
36
+ }
37
+
38
+ export default function AccountFrozenModal() {
39
+ const translate = useTranslations();
40
+ const { activeModal, closeModal, modalData } = useModalManager();
41
+ const isOpen = activeModal === 'accountFrozenModal';
42
+ const { accountsUrl } = useWhitelabelUrls();
43
+ const { redirectToExternal } = useExternalContracting();
44
+
45
+ const freezeDate = (modalData as FrozenModalData | undefined)?.freezeDate ?? null;
46
+ const isWarning = Boolean(freezeDate);
47
+
48
+ const title = isWarning
49
+ ? translate('common.subscription.frozen.modal.warningTitle', {
50
+ freezeDate: formatShortDate(freezeDate ? new Date(freezeDate) : null),
51
+ })
52
+ : translate('common.subscription.frozen.modal.title');
53
+
54
+ const description = isWarning
55
+ ? translate('common.subscription.frozen.modal.warningDescription')
56
+ : translate('common.subscription.frozen.modal.description');
57
+
58
+ const handleRegularize = () => {
59
+ if (redirectToExternal('plans')) return;
60
+ window.location.href = `${accountsUrl}/subscriptions`;
61
+ };
62
+
63
+ return (
64
+ <Dialog open={isOpen} onOpenChange={() => closeModal()}>
65
+ <DialogContent className="flex flex-col p-0 gap-0 w-full md:w-[410px] lg:w-[410px]">
66
+ <DialogHeader className="gap-3 text-left p-5">
67
+ <div
68
+ className={`flex items-center justify-center w-10 h-10 rounded-lg ${isWarning ? 'bg-orange-50' : 'bg-zinc-50'}`}
69
+ >
70
+ {isWarning ? (
71
+ <IconAlertTriangle size={20} className="text-orange-500" />
72
+ ) : (
73
+ <IconLock size={24} className="text-zinc-950" />
74
+ )}
75
+ </div>
76
+ <div className="flex flex-col gap-2">
77
+ <DialogTitle className="paragraph-medium-semibold text-zinc-950">
78
+ {title}
79
+ </DialogTitle>
80
+ <DialogDescription className="paragraph-small-regular text-zinc-600" asChild>
81
+ <div className="flex flex-col gap-3">
82
+ <p>{description}</p>
83
+ <ul className="flex flex-col gap-1.5 list-disc pl-4">
84
+ {BLOCKED_RESOURCE_KEYS.map((key) => (
85
+ <li key={key}>
86
+ {translate(`common.subscription.frozen.modal.resources.${key}`)}
87
+ </li>
88
+ ))}
89
+ </ul>
90
+ <p>{translate('common.subscription.frozen.modal.keepsWorking')}</p>
91
+ </div>
92
+ </DialogDescription>
93
+ </div>
94
+ </DialogHeader>
95
+
96
+ <Separator />
97
+
98
+ <div className="flex items-center gap-2 p-5">
99
+ <Button className="w-fit h-10!" onClick={handleRegularize}>
100
+ {translate('common.subscription.frozen.modal.cta')}
101
+ </Button>
102
+ </div>
103
+ </DialogContent>
104
+ </Dialog>
105
+ );
106
+ }
@@ -0,0 +1,57 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useCallback } from 'react';
4
+ import { IconAlertTriangle, IconX } from '@tabler/icons-react';
5
+ import { useTranslations } from 'next-intl';
6
+ import { formatShortDate } from '../../infra/utils/date';
7
+
8
+ interface FreezeWarningBannerProps {
9
+ freezeDate: Date | null;
10
+ onDetails?: () => void;
11
+ }
12
+
13
+ export function FreezeWarningBanner({ freezeDate, onDetails }: FreezeWarningBannerProps) {
14
+ const translate = useTranslations();
15
+ const [visible, setVisible] = useState(true);
16
+ const [animated, setAnimated] = useState(false);
17
+
18
+ useEffect(() => {
19
+ const timer = setTimeout(() => setAnimated(true), 50);
20
+ return () => clearTimeout(timer);
21
+ }, []);
22
+
23
+ const dismiss = useCallback(() => setVisible(false), []);
24
+
25
+ if (!visible) return null;
26
+
27
+ return (
28
+ <div className="fixed top-[80px] md:top-0 left-0 right-0 z-[60] flex justify-center pointer-events-none px-4 lg:px-0">
29
+ <div
30
+ className={`flex flex-col sm:flex-row items-start sm:items-center justify-between bg-orange-500 rounded-lg px-3 py-2 sm:py-1 w-full lg:w-[805px] max-w-full pointer-events-auto transition-transform duration-500 ease-out gap-2 sm:gap-0 ${animated ? 'translate-y-0 md:translate-y-9' : '-translate-y-full'}`}
31
+ >
32
+ <div className="flex items-center gap-2 flex-1 w-full sm:w-auto">
33
+ <IconAlertTriangle size={16} className="text-white shrink-0" />
34
+ <span className="paragraph-xsmall-semibold text-white">
35
+ {translate('common.subscription.frozen.warningBanner.message', {
36
+ freezeDate: formatShortDate(freezeDate),
37
+ })}
38
+ </span>
39
+ </div>
40
+ <div className="flex items-center gap-1.5 w-full sm:w-auto justify-between sm:justify-start">
41
+ <button
42
+ type="button"
43
+ className="flex items-center justify-center p-1.5 cursor-pointer"
44
+ onClick={onDetails}
45
+ >
46
+ <span className="paragraph-xsmall-semibold text-white underline">
47
+ {translate('common.subscription.frozen.warningBanner.details')}
48
+ </span>
49
+ </button>
50
+ <button type="button" className="cursor-pointer shrink-0" onClick={dismiss}>
51
+ <IconX size={18} className="text-orange-200" />
52
+ </button>
53
+ </div>
54
+ </div>
55
+ </div>
56
+ );
57
+ }
@@ -0,0 +1,49 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect } from 'react';
4
+ import { IconAlertTriangle } from '@tabler/icons-react';
5
+ import { useTranslations } from 'next-intl';
6
+
7
+ interface FrozenAccountBannerProps {
8
+ onDetails?: () => void;
9
+ }
10
+
11
+ /**
12
+ * Banner permanente (D1): congelamento bloqueia publish/update/sync — sem
13
+ * botão de dismiss, ao contrário do OverdueInvoiceBanner.
14
+ */
15
+ export function FrozenAccountBanner({ onDetails }: FrozenAccountBannerProps) {
16
+ const translate = useTranslations();
17
+ const [animated, setAnimated] = useState(false);
18
+
19
+ useEffect(() => {
20
+ const timer = setTimeout(() => setAnimated(true), 50);
21
+ return () => clearTimeout(timer);
22
+ }, []);
23
+
24
+ return (
25
+ <div className="fixed top-[80px] md:top-0 left-0 right-0 z-[60] flex justify-center pointer-events-none px-4 lg:px-0">
26
+ <div
27
+ className={`flex flex-col sm:flex-row items-start sm:items-center justify-between bg-red-500 rounded-lg px-3 py-2 sm:py-1 w-full lg:w-[805px] max-w-full pointer-events-auto transition-transform duration-500 ease-out gap-2 sm:gap-0 ${animated ? 'translate-y-0 md:translate-y-9' : '-translate-y-full'}`}
28
+ >
29
+ <div className="flex items-center gap-2 flex-1 w-full sm:w-auto">
30
+ <IconAlertTriangle size={16} className="text-white shrink-0" />
31
+ <span className="paragraph-xsmall-semibold text-white">
32
+ {translate('common.subscription.frozen.banner.message')}
33
+ </span>
34
+ </div>
35
+ <div className="flex items-center gap-1.5 w-full sm:w-auto justify-between sm:justify-start">
36
+ <button
37
+ type="button"
38
+ className="flex items-center justify-center p-1.5 cursor-pointer"
39
+ onClick={onDetails}
40
+ >
41
+ <span className="paragraph-xsmall-semibold text-white underline">
42
+ {translate('common.subscription.frozen.banner.regularize')}
43
+ </span>
44
+ </button>
45
+ </div>
46
+ </div>
47
+ </div>
48
+ );
49
+ }
@@ -5,12 +5,17 @@ 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";
18
+ import { useModalManager } from "../../store/useModalManager";
14
19
 
15
20
  const UPCOMING_WINDOW_DAYS = 7;
16
21
  const PAYMENT_METHOD_BOLETO = 2;
@@ -25,6 +30,7 @@ export function SubscriptionBanner({
25
30
  onDetails,
26
31
  }: SubscriptionBannerProps) {
27
32
  const { data: { data: [subscription] = [] } = {} } = useActiveSubscription();
33
+ const { openModal } = useModalManager();
28
34
  const { data: pendingChargesData, isLoading: isLoadingCharges } = useCharges({
29
35
  page: 1,
30
36
  limit: 1,
@@ -76,6 +82,30 @@ export function SubscriptionBanner({
76
82
 
77
83
  if (subscription.type === "whitelabel") return null;
78
84
 
85
+ // Congelamento (date_due + ACCOUNT_FREEZE_AFTER_DAYS): entra antes do dunning
86
+ // e do aviso genérico de atraso, que hoje engolem essa janela.
87
+ const freezeState = getAccountFreezeState(subscription);
88
+ // "Detalhes" abre a MESMA modal do bloqueio, com a lista do que o congelamento tira: no
89
+ // aviso o usuário precisa ver o que vai perder antes de perder. O `freezeDate` no data é
90
+ // o que faz a modal renderizar a variante de véspera.
91
+ //
92
+ // IGNORA o `onDetails` do host de propósito: nos apps ele abre os detalhes da COBRANÇA,
93
+ // que responde outra pergunta. Aqui "Detalhes" significa "o que eu perco".
94
+ if (freezeState === "frozen") {
95
+ return (
96
+ <FrozenAccountBanner onDetails={() => openModal("accountFrozenModal")} />
97
+ );
98
+ }
99
+ if (freezeState === "warning") {
100
+ const freezeDate = getAccountFreezeDate(subscription);
101
+ return (
102
+ <FreezeWarningBanner
103
+ freezeDate={freezeDate}
104
+ onDetails={() => openModal("accountFrozenModal", { freezeDate })}
105
+ />
106
+ );
107
+ }
108
+
79
109
  // Cartão em dunning (Stripe Smart Retries): comunica as tentativas de cobrança
80
110
  // restantes antes do cancelamento automático — não mais por tolerância fixa de
81
111
  // 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';