@greatapps/common 1.1.798 → 1.1.800
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/modules/accounts/hooks/use-required-billing-data.hook.mjs +2 -2
- package/dist/modules/accounts/hooks/use-required-billing-data.hook.mjs.map +1 -1
- package/dist/modules/accounts/utils/billing-data.mjs +8 -1
- package/dist/modules/accounts/utils/billing-data.mjs.map +1 -1
- package/dist/modules/subscriptions/utils/has-paid-subscription.mjs +1 -1
- package/dist/modules/subscriptions/utils/has-paid-subscription.mjs.map +1 -1
- package/package.json +1 -1
- package/src/modules/accounts/hooks/use-required-billing-data.hook.ts +9 -2
- package/src/modules/accounts/utils/billing-data.ts +16 -1
- package/src/modules/subscriptions/utils/has-paid-subscription.ts +13 -8
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { useCurrentAccount } from "./current-account.hook";
|
|
3
|
-
import {
|
|
3
|
+
import { useCurrentSubscription } from "../../subscriptions/hooks/use-current-subscription.hook";
|
|
4
4
|
import { hasPaidSubscription } from "../../subscriptions/utils/has-paid-subscription";
|
|
5
5
|
import { useManagementPermissions } from "../../auth/hooks/useManagementPermissions";
|
|
6
6
|
import { hasCompleteBillingData, isBrazilianBillingAccount } from "../utils/billing-data";
|
|
@@ -10,7 +10,7 @@ function useRequiredBillingData() {
|
|
|
10
10
|
const {
|
|
11
11
|
data: { data: [subscription] = [] } = {},
|
|
12
12
|
isPending: isSubscriptionPending
|
|
13
|
-
} =
|
|
13
|
+
} = useCurrentSubscription();
|
|
14
14
|
const isLoading = isAccountPending || isSubscriptionPending;
|
|
15
15
|
const isRequired = !isLoading && isOwnerOrAdmin && isBrazilianBillingAccount(account) && hasPaidSubscription(subscription) && !hasCompleteBillingData(account);
|
|
16
16
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/modules/accounts/hooks/use-required-billing-data.hook.ts"],"sourcesContent":["'use client';\n\nimport { useCurrentAccount } from './current-account.hook';\nimport {
|
|
1
|
+
{"version":3,"sources":["../../../../src/modules/accounts/hooks/use-required-billing-data.hook.ts"],"sourcesContent":["'use client';\n\nimport { useCurrentAccount } from './current-account.hook';\nimport { useCurrentSubscription } from '../../subscriptions/hooks/use-current-subscription.hook';\nimport { hasPaidSubscription } from '../../subscriptions/utils/has-paid-subscription';\nimport { useManagementPermissions } from '../../auth/hooks/useManagementPermissions';\nimport { hasCompleteBillingData, isBrazilianBillingAccount } from '../utils/billing-data';\n\n/**\n * Contas que já pagaram mas nunca preencheram o cadastro de cobrança ficam sem\n * nota fiscal (chamado \"Nota fiscal não emitida em pagamentos confirmados\").\n * Este hook diz quando o preenchimento vira obrigatório — o modal que ele\n * alimenta é bloqueante, então só liga com os dados já carregados e só para\n * quem consegue resolver: owner/admin de conta brasileira.\n *\n * A assinatura vem de `useCurrentSubscription`, não de `useActiveSubscription`:\n * o segundo é `GET /subscriptions?limit=1` sem regra de escolha, então numa conta\n * com trial antiga a trial volta na frente da assinatura paga e o gate se cala.\n * Foi o que aconteceu na conta 428409, que pagou R$ 1.918 sem nota e nunca viu o\n * modal. O `current` aplica o ranqueamento do backend (cliente viva > trial viva >\n * free > encerrada) e devolve a mesma forma paginada.\n */\nexport function useRequiredBillingData() {\n const { isOwnerOrAdmin } = useManagementPermissions();\n const { data: account, isPending: isAccountPending } = useCurrentAccount();\n const {\n data: { data: [subscription] = [] } = {},\n isPending: isSubscriptionPending,\n } = useCurrentSubscription();\n\n const isLoading = isAccountPending || isSubscriptionPending;\n\n const isRequired =\n !isLoading &&\n isOwnerOrAdmin &&\n isBrazilianBillingAccount(account) &&\n hasPaidSubscription(subscription) &&\n !hasCompleteBillingData(account);\n\n return {\n isRequired,\n isLoading,\n account,\n };\n}\n"],"mappings":";AAEA,SAAS,yBAAyB;AAClC,SAAS,8BAA8B;AACvC,SAAS,2BAA2B;AACpC,SAAS,gCAAgC;AACzC,SAAS,wBAAwB,iCAAiC;AAgB3D,SAAS,yBAAyB;AACvC,QAAM,EAAE,eAAe,IAAI,yBAAyB;AACpD,QAAM,EAAE,MAAM,SAAS,WAAW,iBAAiB,IAAI,kBAAkB;AACzE,QAAM;AAAA,IACJ,MAAM,EAAE,MAAM,CAAC,YAAY,IAAI,CAAC,EAAE,IAAI,CAAC;AAAA,IACvC,WAAW;AAAA,EACb,IAAI,uBAAuB;AAE3B,QAAM,YAAY,oBAAoB;AAEtC,QAAM,aACJ,CAAC,aACD,kBACA,0BAA0B,OAAO,KACjC,oBAAoB,YAAY,KAChC,CAAC,uBAAuB,OAAO;AAEjC,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;","names":[]}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isValidCNPJ, isValidCPF } from "../../../utils/validators/common";
|
|
1
2
|
const REQUIRED_BILLING_FIELDS = [
|
|
2
3
|
"financial_document",
|
|
3
4
|
"financial_name",
|
|
@@ -17,12 +18,18 @@ function isBrazilianBillingAccount(account) {
|
|
|
17
18
|
function isFilled(value) {
|
|
18
19
|
return typeof value === "string" && value.trim().length > 0;
|
|
19
20
|
}
|
|
21
|
+
function hasValidDocument(document) {
|
|
22
|
+
const digits = (document ?? "").replace(/\D/g, "");
|
|
23
|
+
if (digits.length === 11) return isValidCPF(digits);
|
|
24
|
+
if (digits.length === 14) return isValidCNPJ(digits);
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
20
27
|
function hasCompleteBillingData(account) {
|
|
21
28
|
if (!account) return false;
|
|
22
29
|
for (const field of REQUIRED_BILLING_FIELDS) {
|
|
23
30
|
if (!isFilled(account[field])) return false;
|
|
24
31
|
}
|
|
25
|
-
return
|
|
32
|
+
return hasValidDocument(account.financial_document);
|
|
26
33
|
}
|
|
27
34
|
export {
|
|
28
35
|
hasCompleteBillingData,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/modules/accounts/utils/billing-data.ts"],"sourcesContent":["import type { Account } from '../types';\n\nexport type BillingDataSnapshot = Pick<\n Account,\n | 'financial_document'\n | 'financial_name'\n | 'financial_email'\n | 'zipcode'\n | 'address'\n | 'address_number'\n | 'neighborhood'\n | 'city'\n | 'state'\n | 'country'\n>;\n\ntype BillingField = keyof BillingDataSnapshot;\n\n/**\n * Campos exigidos pra emitir a NF-e — sem qualquer um deles a nota não sai.\n *\n * `financial_email` NÃO entra: o backend cai no e-mail do usuário quando ele está\n * vazio (`financial_email || userDocument.email`, accounts/services.js), então a\n * nota sai do mesmo jeito. Exigi-lo aqui bloquearia 4.7k contas pagantes da wl 1\n * em vez das ~460 que de fato estão sem cadastro fiscal. O campo continua no\n * formulário, pré-preenchido — só não é critério de bloqueio.\n *\n * `address_complement` é opcional de verdade.\n */\nconst REQUIRED_BILLING_FIELDS: readonly BillingField[] = [\n 'financial_document',\n 'financial_name',\n 'zipcode',\n 'address',\n 'address_number',\n 'neighborhood',\n 'city',\n 'state',\n 'country',\n];\n\n/**\n * Conta sujeita à NF-e brasileira. Só ela entra no bloqueio: o formulário pede\n * CPF/CNPJ e CEP de 8 dígitos, que um cliente de fora não tem como fornecer —\n * e o modal é bloqueante, então seria tranca sem saída.\n *\n * País vazio conta como Brasil: é o default do cadastro e a origem da esmagadora\n * maioria da base (155 contas pagas da wl 1 estão assim).\n */\nexport function isBrazilianBillingAccount(\n account: Pick<BillingDataSnapshot, 'country'> | null | undefined,\n): boolean {\n if (!account) return false;\n const country = account.country?.trim().toUpperCase();\n return !country || country === 'BR';\n}\n\nfunction isFilled(value: string | null | undefined): boolean {\n return typeof value === 'string' && value.trim().length > 0;\n}\n\n/**\n * A conta tem todos os dados de cobrança/fiscais necessários pra emissão de nota.\n * Retorna `false` quando a conta ainda não carregou — quem decide bloquear é o\n * chamador, que checa o loading antes (ver `useRequiredBillingData`).\n */\nexport function hasCompleteBillingData(\n account: BillingDataSnapshot | null | undefined,\n): boolean {\n if (!account) return false;\n\n for (const field of REQUIRED_BILLING_FIELDS) {\n if (!isFilled(account[field])) return false;\n }\n\n return
|
|
1
|
+
{"version":3,"sources":["../../../../src/modules/accounts/utils/billing-data.ts"],"sourcesContent":["import type { Account } from '../types';\nimport { isValidCNPJ, isValidCPF } from '../../../utils/validators/common';\n\nexport type BillingDataSnapshot = Pick<\n Account,\n | 'financial_document'\n | 'financial_name'\n | 'financial_email'\n | 'zipcode'\n | 'address'\n | 'address_number'\n | 'neighborhood'\n | 'city'\n | 'state'\n | 'country'\n>;\n\ntype BillingField = keyof BillingDataSnapshot;\n\n/**\n * Campos exigidos pra emitir a NF-e — sem qualquer um deles a nota não sai.\n *\n * `financial_email` NÃO entra: o backend cai no e-mail do usuário quando ele está\n * vazio (`financial_email || userDocument.email`, accounts/services.js), então a\n * nota sai do mesmo jeito. Exigi-lo aqui bloquearia 4.7k contas pagantes da wl 1\n * em vez das ~460 que de fato estão sem cadastro fiscal. O campo continua no\n * formulário, pré-preenchido — só não é critério de bloqueio.\n *\n * `address_complement` é opcional de verdade.\n */\nconst REQUIRED_BILLING_FIELDS: readonly BillingField[] = [\n 'financial_document',\n 'financial_name',\n 'zipcode',\n 'address',\n 'address_number',\n 'neighborhood',\n 'city',\n 'state',\n 'country',\n];\n\n/**\n * Conta sujeita à NF-e brasileira. Só ela entra no bloqueio: o formulário pede\n * CPF/CNPJ e CEP de 8 dígitos, que um cliente de fora não tem como fornecer —\n * e o modal é bloqueante, então seria tranca sem saída.\n *\n * País vazio conta como Brasil: é o default do cadastro e a origem da esmagadora\n * maioria da base (155 contas pagas da wl 1 estão assim).\n */\nexport function isBrazilianBillingAccount(\n account: Pick<BillingDataSnapshot, 'country'> | null | undefined,\n): boolean {\n if (!account) return false;\n const country = account.country?.trim().toUpperCase();\n return !country || country === 'BR';\n}\n\nfunction isFilled(value: string | null | undefined): boolean {\n return typeof value === 'string' && value.trim().length > 0;\n}\n\n/**\n * Documento preenchido não é documento bom. O caso que originou isto: uma conta pagava desde\n * junho/2025 com um CNPJ de um dígito trocado, a prefeitura recusava a NFS-e todo mês e o modal\n * nunca abria, porque o campo estava preenchido. Conferir o dígito verificador aqui é o que faz\n * o cliente ser levado a corrigir — o backend já recusa a escrita de um documento inválido desde\n * 28/05/2026, então o formulário não tem como salvar um valor ruim por cima.\n */\nfunction hasValidDocument(document: string | null | undefined): boolean {\n const digits = (document ?? '').replace(/\\D/g, '');\n if (digits.length === 11) return isValidCPF(digits);\n if (digits.length === 14) return isValidCNPJ(digits);\n return false;\n}\n\n/**\n * A conta tem todos os dados de cobrança/fiscais necessários pra emissão de nota.\n * Retorna `false` quando a conta ainda não carregou — quem decide bloquear é o\n * chamador, que checa o loading antes (ver `useRequiredBillingData`).\n */\nexport function hasCompleteBillingData(\n account: BillingDataSnapshot | null | undefined,\n): boolean {\n if (!account) return false;\n\n for (const field of REQUIRED_BILLING_FIELDS) {\n if (!isFilled(account[field])) return false;\n }\n\n return hasValidDocument(account.financial_document);\n}\n"],"mappings":"AACA,SAAS,aAAa,kBAAkB;AA6BxC,MAAM,0BAAmD;AAAA,EACvD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAUO,SAAS,0BACd,SACS;AACT,MAAI,CAAC,QAAS,QAAO;AACrB,QAAM,UAAU,QAAQ,SAAS,KAAK,EAAE,YAAY;AACpD,SAAO,CAAC,WAAW,YAAY;AACjC;AAEA,SAAS,SAAS,OAA2C;AAC3D,SAAO,OAAO,UAAU,YAAY,MAAM,KAAK,EAAE,SAAS;AAC5D;AASA,SAAS,iBAAiB,UAA8C;AACtE,QAAM,UAAU,YAAY,IAAI,QAAQ,OAAO,EAAE;AACjD,MAAI,OAAO,WAAW,GAAI,QAAO,WAAW,MAAM;AAClD,MAAI,OAAO,WAAW,GAAI,QAAO,YAAY,MAAM;AACnD,SAAO;AACT;AAOO,SAAS,uBACd,SACS;AACT,MAAI,CAAC,QAAS,QAAO;AAErB,aAAW,SAAS,yBAAyB;AAC3C,QAAI,CAAC,SAAS,QAAQ,KAAK,CAAC,EAAG,QAAO;AAAA,EACxC;AAEA,SAAO,iBAAiB,QAAQ,kBAAkB;AACpD;","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/modules/subscriptions/utils/has-paid-subscription.ts"],"sourcesContent":["import type { Subscription } from '../types/subscription.type';\nimport { FREE_PLAN_ID } from '../constants/subscription.constants';\n\ntype PaidSubscriptionSnapshot = Pick<Subscription, 'type' | 'id_plan'
|
|
1
|
+
{"version":3,"sources":["../../../../src/modules/subscriptions/utils/has-paid-subscription.ts"],"sourcesContent":["import type { Subscription } from '../types/subscription.type';\nimport { FREE_PLAN_ID } from '../constants/subscription.constants';\n\ntype PaidSubscriptionSnapshot = Pick<Subscription, 'type' | 'id_plan'>;\n\n/**\n * A assinatura é de plano pago — o que basta para a conta precisar de cadastro\n * fiscal, porque a cobrança vem e a nota tem que sair com ela.\n *\n * Não olha mais `charged`. O campo é escrito no `bill_paid`, evento do fluxo\n * Vindi, e na prática só a Stripe o preenche: das 7.528 assinaturas ativas de\n * plano pago com cobrança paga, 812 estão sem ele, e 806 dessas são Vindi (96%\n * da base Vindi). Exigi-lo deixava o gate cego justamente para a parte antiga da\n * base — a conta 428409 pagou R$ 1.918 sem nota com `charged = false`.\n *\n * Não exigir pagamento confirmado também alcança a conta ANTES da primeira\n * cobrança, que é quando o dado fiscal ainda dá tempo de evitar nota perdida.\n *\n * Free e trial ficam de fora: não geram cobrança a documentar.\n */\nexport function hasPaidSubscription(\n subscription: PaidSubscriptionSnapshot | undefined | null,\n): boolean {\n if (!subscription) return false;\n if (subscription.type === 'free' || subscription.id_plan === FREE_PLAN_ID) {\n return false;\n }\n if (subscription.type === 'trial') return false;\n\n return true;\n}\n"],"mappings":"AACA,SAAS,oBAAoB;AAmBtB,SAAS,oBACd,cACS;AACT,MAAI,CAAC,aAAc,QAAO;AAC1B,MAAI,aAAa,SAAS,UAAU,aAAa,YAAY,cAAc;AACzE,WAAO;AAAA,EACT;AACA,MAAI,aAAa,SAAS,QAAS,QAAO;AAE1C,SAAO;AACT;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useCurrentAccount } from './current-account.hook';
|
|
4
|
-
import {
|
|
4
|
+
import { useCurrentSubscription } from '../../subscriptions/hooks/use-current-subscription.hook';
|
|
5
5
|
import { hasPaidSubscription } from '../../subscriptions/utils/has-paid-subscription';
|
|
6
6
|
import { useManagementPermissions } from '../../auth/hooks/useManagementPermissions';
|
|
7
7
|
import { hasCompleteBillingData, isBrazilianBillingAccount } from '../utils/billing-data';
|
|
@@ -12,6 +12,13 @@ import { hasCompleteBillingData, isBrazilianBillingAccount } from '../utils/bill
|
|
|
12
12
|
* Este hook diz quando o preenchimento vira obrigatório — o modal que ele
|
|
13
13
|
* alimenta é bloqueante, então só liga com os dados já carregados e só para
|
|
14
14
|
* quem consegue resolver: owner/admin de conta brasileira.
|
|
15
|
+
*
|
|
16
|
+
* A assinatura vem de `useCurrentSubscription`, não de `useActiveSubscription`:
|
|
17
|
+
* o segundo é `GET /subscriptions?limit=1` sem regra de escolha, então numa conta
|
|
18
|
+
* com trial antiga a trial volta na frente da assinatura paga e o gate se cala.
|
|
19
|
+
* Foi o que aconteceu na conta 428409, que pagou R$ 1.918 sem nota e nunca viu o
|
|
20
|
+
* modal. O `current` aplica o ranqueamento do backend (cliente viva > trial viva >
|
|
21
|
+
* free > encerrada) e devolve a mesma forma paginada.
|
|
15
22
|
*/
|
|
16
23
|
export function useRequiredBillingData() {
|
|
17
24
|
const { isOwnerOrAdmin } = useManagementPermissions();
|
|
@@ -19,7 +26,7 @@ export function useRequiredBillingData() {
|
|
|
19
26
|
const {
|
|
20
27
|
data: { data: [subscription] = [] } = {},
|
|
21
28
|
isPending: isSubscriptionPending,
|
|
22
|
-
} =
|
|
29
|
+
} = useCurrentSubscription();
|
|
23
30
|
|
|
24
31
|
const isLoading = isAccountPending || isSubscriptionPending;
|
|
25
32
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Account } from '../types';
|
|
2
|
+
import { isValidCNPJ, isValidCPF } from '../../../utils/validators/common';
|
|
2
3
|
|
|
3
4
|
export type BillingDataSnapshot = Pick<
|
|
4
5
|
Account,
|
|
@@ -59,6 +60,20 @@ function isFilled(value: string | null | undefined): boolean {
|
|
|
59
60
|
return typeof value === 'string' && value.trim().length > 0;
|
|
60
61
|
}
|
|
61
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Documento preenchido não é documento bom. O caso que originou isto: uma conta pagava desde
|
|
65
|
+
* junho/2025 com um CNPJ de um dígito trocado, a prefeitura recusava a NFS-e todo mês e o modal
|
|
66
|
+
* nunca abria, porque o campo estava preenchido. Conferir o dígito verificador aqui é o que faz
|
|
67
|
+
* o cliente ser levado a corrigir — o backend já recusa a escrita de um documento inválido desde
|
|
68
|
+
* 28/05/2026, então o formulário não tem como salvar um valor ruim por cima.
|
|
69
|
+
*/
|
|
70
|
+
function hasValidDocument(document: string | null | undefined): boolean {
|
|
71
|
+
const digits = (document ?? '').replace(/\D/g, '');
|
|
72
|
+
if (digits.length === 11) return isValidCPF(digits);
|
|
73
|
+
if (digits.length === 14) return isValidCNPJ(digits);
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
62
77
|
/**
|
|
63
78
|
* A conta tem todos os dados de cobrança/fiscais necessários pra emissão de nota.
|
|
64
79
|
* Retorna `false` quando a conta ainda não carregou — quem decide bloquear é o
|
|
@@ -73,5 +88,5 @@ export function hasCompleteBillingData(
|
|
|
73
88
|
if (!isFilled(account[field])) return false;
|
|
74
89
|
}
|
|
75
90
|
|
|
76
|
-
return
|
|
91
|
+
return hasValidDocument(account.financial_document);
|
|
77
92
|
}
|
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
import type { Subscription } from '../types/subscription.type';
|
|
2
2
|
import { FREE_PLAN_ID } from '../constants/subscription.constants';
|
|
3
3
|
|
|
4
|
-
type PaidSubscriptionSnapshot = Pick<Subscription, 'type' | 'id_plan'
|
|
4
|
+
type PaidSubscriptionSnapshot = Pick<Subscription, 'type' | 'id_plan'>;
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* A
|
|
7
|
+
* A assinatura é de plano pago — o que basta para a conta precisar de cadastro
|
|
8
|
+
* fiscal, porque a cobrança vem e a nota tem que sair com ela.
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* Não olha mais `charged`. O campo é escrito no `bill_paid`, evento do fluxo
|
|
11
|
+
* Vindi, e na prática só a Stripe o preenche: das 7.528 assinaturas ativas de
|
|
12
|
+
* plano pago com cobrança paga, 812 estão sem ele, e 806 dessas são Vindi (96%
|
|
13
|
+
* da base Vindi). Exigi-lo deixava o gate cego justamente para a parte antiga da
|
|
14
|
+
* base — a conta 428409 pagou R$ 1.918 sem nota com `charged = false`.
|
|
13
15
|
*
|
|
14
|
-
*
|
|
16
|
+
* Não exigir pagamento confirmado também alcança a conta ANTES da primeira
|
|
17
|
+
* cobrança, que é quando o dado fiscal ainda dá tempo de evitar nota perdida.
|
|
18
|
+
*
|
|
19
|
+
* Free e trial ficam de fora: não geram cobrança a documentar.
|
|
15
20
|
*/
|
|
16
21
|
export function hasPaidSubscription(
|
|
17
22
|
subscription: PaidSubscriptionSnapshot | undefined | null,
|
|
@@ -22,5 +27,5 @@ export function hasPaidSubscription(
|
|
|
22
27
|
}
|
|
23
28
|
if (subscription.type === 'trial') return false;
|
|
24
29
|
|
|
25
|
-
return
|
|
30
|
+
return true;
|
|
26
31
|
}
|