@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.
- package/dist/components/modals/AccountFrozenModal.mjs +48 -0
- package/dist/components/modals/AccountFrozenModal.mjs.map +1 -0
- package/dist/components/navigation/FreezeWarningBanner.mjs +47 -0
- package/dist/components/navigation/FreezeWarningBanner.mjs.map +1 -0
- package/dist/components/navigation/FrozenAccountBanner.mjs +38 -0
- package/dist/components/navigation/FrozenAccountBanner.mjs.map +1 -0
- package/dist/components/navigation/SubscriptionBanner.mjs +17 -0
- package/dist/components/navigation/SubscriptionBanner.mjs.map +1 -1
- package/dist/handlers.mjs +2 -0
- package/dist/handlers.mjs.map +1 -1
- package/dist/i18n/messages/en-us.mjs +23 -0
- package/dist/i18n/messages/en-us.mjs.map +1 -1
- package/dist/i18n/messages/es-es.mjs +23 -0
- package/dist/i18n/messages/es-es.mjs.map +1 -1
- package/dist/i18n/messages/pt-br.mjs +25 -0
- package/dist/i18n/messages/pt-br.mjs.map +1 -1
- package/dist/index.mjs +59 -37
- package/dist/index.mjs.map +1 -1
- package/dist/modules/auth/utils/assert-account-not-frozen.mjs +26 -0
- package/dist/modules/auth/utils/assert-account-not-frozen.mjs.map +1 -0
- package/dist/modules/plans/utils/map-api-plan-to-ui.mjs +14 -2
- package/dist/modules/plans/utils/map-api-plan-to-ui.mjs.map +1 -1
- package/dist/modules/projects/services/projects.service.mjs +3 -0
- package/dist/modules/projects/services/projects.service.mjs.map +1 -1
- package/dist/modules/subscriptions/actions/ack-freeze-notice.action.mjs +10 -0
- package/dist/modules/subscriptions/actions/ack-freeze-notice.action.mjs.map +1 -0
- package/dist/modules/subscriptions/constants/query-keys.constants.mjs +2 -0
- package/dist/modules/subscriptions/constants/query-keys.constants.mjs.map +1 -1
- package/dist/modules/subscriptions/constants/subscription.constants.mjs +6 -0
- package/dist/modules/subscriptions/constants/subscription.constants.mjs.map +1 -1
- package/dist/modules/subscriptions/handlers/freeze-notice.handler.mjs +12 -0
- package/dist/modules/subscriptions/handlers/freeze-notice.handler.mjs.map +1 -0
- package/dist/modules/subscriptions/hooks/use-account-freeze.hook.mjs +42 -0
- package/dist/modules/subscriptions/hooks/use-account-freeze.hook.mjs.map +1 -0
- package/dist/modules/subscriptions/hooks/use-freeze-notice.hook.mjs +33 -0
- package/dist/modules/subscriptions/hooks/use-freeze-notice.hook.mjs.map +1 -0
- package/dist/modules/subscriptions/services/subscriptions.service.mjs +29 -0
- package/dist/modules/subscriptions/services/subscriptions.service.mjs.map +1 -1
- package/dist/modules/subscriptions/types/freeze-notice.type.mjs +1 -0
- package/dist/modules/subscriptions/types/freeze-notice.type.mjs.map +1 -0
- package/dist/modules/subscriptions/utils/get-account-freeze-date.mjs +19 -0
- package/dist/modules/subscriptions/utils/get-account-freeze-date.mjs.map +1 -0
- package/dist/modules/subscriptions/utils/get-account-freeze-state.mjs +21 -0
- package/dist/modules/subscriptions/utils/get-account-freeze-state.mjs.map +1 -0
- package/dist/server.mjs +4 -0
- package/dist/server.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/modals/AccountFrozenModal.tsx +58 -0
- package/src/components/navigation/FreezeWarningBanner.tsx +57 -0
- package/src/components/navigation/FrozenAccountBanner.tsx +49 -0
- package/src/components/navigation/SubscriptionBanner.tsx +19 -0
- package/src/handlers.ts +1 -0
- package/src/i18n/messages/en-us.ts +23 -0
- package/src/i18n/messages/es-es.ts +23 -0
- package/src/i18n/messages/pt-br.ts +26 -0
- package/src/index.ts +14 -0
- package/src/modules/auth/utils/assert-account-not-frozen.ts +38 -0
- package/src/modules/plans/utils/map-api-plan-to-ui.ts +16 -3
- package/src/modules/projects/services/projects.service.ts +3 -0
- package/src/modules/subscriptions/actions/ack-freeze-notice.action.ts +8 -0
- package/src/modules/subscriptions/constants/query-keys.constants.ts +2 -0
- package/src/modules/subscriptions/constants/subscription.constants.ts +8 -0
- package/src/modules/subscriptions/handlers/freeze-notice.handler.ts +10 -0
- package/src/modules/subscriptions/hooks/use-account-freeze.hook.ts +44 -0
- package/src/modules/subscriptions/hooks/use-freeze-notice.hook.ts +42 -0
- package/src/modules/subscriptions/services/subscriptions.service.ts +39 -0
- package/src/modules/subscriptions/types/freeze-notice.type.ts +5 -0
- package/src/modules/subscriptions/utils/get-account-freeze-date.ts +40 -0
- package/src/modules/subscriptions/utils/get-account-freeze-state.ts +43 -0
- 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
|
|
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,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,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';
|