@greatapps/common 1.1.742 → 1.1.744
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/billing/RequiredBillingDataModal.mjs +14 -30
- package/dist/components/modals/billing/RequiredBillingDataModal.mjs.map +1 -1
- package/dist/i18n/messages/en-us.mjs +6 -0
- package/dist/i18n/messages/en-us.mjs.map +1 -1
- package/dist/i18n/messages/es-es.mjs +6 -0
- package/dist/i18n/messages/es-es.mjs.map +1 -1
- package/dist/i18n/messages/pt-br.mjs +6 -0
- package/dist/i18n/messages/pt-br.mjs.map +1 -1
- package/dist/index.mjs +5 -1
- package/dist/index.mjs.map +1 -1
- package/dist/modules/accounts/hooks/use-required-billing-data.hook.mjs +3 -5
- package/dist/modules/accounts/hooks/use-required-billing-data.hook.mjs.map +1 -1
- package/dist/modules/accounts/utils/billing-data.mjs +12 -11
- package/dist/modules/accounts/utils/billing-data.mjs.map +1 -1
- 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/package.json +1 -1
- package/src/components/modals/billing/RequiredBillingDataModal.tsx +32 -66
- package/src/i18n/messages/en-us.ts +6 -0
- package/src/i18n/messages/es-es.ts +6 -0
- package/src/i18n/messages/pt-br.ts +6 -0
- package/src/index.ts +4 -1
- package/src/modules/accounts/hooks/use-required-billing-data.hook.ts +3 -4
- package/src/modules/accounts/utils/billing-data.ts +24 -17
- package/src/modules/plans/utils/map-api-plan-to-ui.ts +16 -3
|
@@ -1,30 +1,31 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
const REQUIRED_BILLING_FIELDS = [
|
|
2
|
+
"financial_document",
|
|
3
3
|
"financial_name",
|
|
4
4
|
"zipcode",
|
|
5
5
|
"address",
|
|
6
|
+
"address_number",
|
|
7
|
+
"neighborhood",
|
|
6
8
|
"city",
|
|
7
9
|
"state",
|
|
8
10
|
"country"
|
|
9
11
|
];
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
"
|
|
14
|
-
|
|
15
|
-
];
|
|
12
|
+
function isBrazilianBillingAccount(account) {
|
|
13
|
+
if (!account) return false;
|
|
14
|
+
const country = account.country?.trim().toUpperCase();
|
|
15
|
+
return !country || country === "BR";
|
|
16
|
+
}
|
|
16
17
|
function isFilled(value) {
|
|
17
18
|
return typeof value === "string" && value.trim().length > 0;
|
|
18
19
|
}
|
|
19
20
|
function hasCompleteBillingData(account) {
|
|
20
21
|
if (!account) return false;
|
|
21
|
-
const
|
|
22
|
-
for (const field of fields) {
|
|
22
|
+
for (const field of REQUIRED_BILLING_FIELDS) {
|
|
23
23
|
if (!isFilled(account[field])) return false;
|
|
24
24
|
}
|
|
25
25
|
return true;
|
|
26
26
|
}
|
|
27
27
|
export {
|
|
28
|
-
hasCompleteBillingData
|
|
28
|
+
hasCompleteBillingData,
|
|
29
|
+
isBrazilianBillingAccount
|
|
29
30
|
};
|
|
30
31
|
//# sourceMappingURL=billing-data.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/modules/accounts/utils/billing-data.ts"],"sourcesContent":["import type { Account } from '../types';\
|
|
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 true;\n}\n"],"mappings":"AA6BA,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;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;AACT;","names":[]}
|
|
@@ -3,6 +3,18 @@ import {
|
|
|
3
3
|
formatCurrencyNumber
|
|
4
4
|
} from "../../../utils/format/currency";
|
|
5
5
|
const STARTER_PLAN_ID = 415;
|
|
6
|
+
const AGENCY_PLAN_ID = 3;
|
|
7
|
+
const planIdOf = (plan) => plan.id_plan ?? plan.id;
|
|
8
|
+
function resolvePlanName(plan, translate) {
|
|
9
|
+
if (!translate) return plan.name;
|
|
10
|
+
const key = `common.plans.names.${planIdOf(plan)}`;
|
|
11
|
+
try {
|
|
12
|
+
const translated = translate(key);
|
|
13
|
+
return translated && translated !== key ? translated : plan.name;
|
|
14
|
+
} catch {
|
|
15
|
+
return plan.name;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
6
18
|
const BASE_FEATURES_KEYS = [
|
|
7
19
|
{ icon: "check", key: "plans.features.unlimitedVisits", fallback: "Visitas ilimitadas" },
|
|
8
20
|
{ icon: "check", key: "plans.features.unlimitedLeads", fallback: "Leads ilimitadas" },
|
|
@@ -70,7 +82,7 @@ function buildUiPlan(plan, currency, translate) {
|
|
|
70
82
|
const pricingByPeriod = computePricingByPeriod(plan);
|
|
71
83
|
const monthlyFinal = pricingByPeriod.monthly;
|
|
72
84
|
const isStarterPlan = plan.id === STARTER_PLAN_ID || plan.id_plan === STARTER_PLAN_ID;
|
|
73
|
-
const hasProjectAndOnboarding = plan
|
|
85
|
+
const hasProjectAndOnboarding = planIdOf(plan) === AGENCY_PLAN_ID;
|
|
74
86
|
const onboardingText = translate ? translate("common.plans.features.onboarding") : "Reuni\xE3o de Onboarding";
|
|
75
87
|
const projectMgmtText = translate ? translate("common.plans.features.projectManagement") : "Gest\xE3o de projetos";
|
|
76
88
|
const sharePagesText = translate ? translate("common.plans.features.sharePages") : "Compartilhar p\xE1ginas";
|
|
@@ -89,7 +101,7 @@ function buildUiPlan(plan, currency, translate) {
|
|
|
89
101
|
const unavailableLabel = translate ? translate("common.plans.usdNotConfigured") : "USD n\xE3o configurado";
|
|
90
102
|
return {
|
|
91
103
|
planId: plan.id,
|
|
92
|
-
name: plan
|
|
104
|
+
name: resolvePlanName(plan, translate),
|
|
93
105
|
isPopular: popular,
|
|
94
106
|
buttonVariant,
|
|
95
107
|
mainFeatures: buildMainFeaturesFromItems(plan.items, translate),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/modules/plans/utils/map-api-plan-to-ui.ts"],"sourcesContent":["import type {\n Plan,\n PlanFeature,\n PlanMainFeatures,\n PlanPricingByPeriod,\n UiPlan,\n} from \"../types/plan.type\";\nimport { ADDON_IDS } from \"../../subscriptions/constants/addons.constants\";\nimport {\n formatCurrencyNumber,\n type CurrencyCode,\n} from \"../../../utils/format/currency\";\n\nexport type PlanBillingPeriod = \"monthly\" | \"semiannual\" | \"annual\";\n\nconst STARTER_PLAN_ID = 415;\n\ntype TranslateFn = (key: any, values?: Record<string, string | number>) => string;\n\nconst BASE_FEATURES_KEYS = [\n { icon: \"check\", key: \"plans.features.unlimitedVisits\", fallback: \"Visitas ilimitadas\" },\n { icon: \"check\", key: \"plans.features.unlimitedLeads\", fallback: \"Leads ilimitadas\" },\n { icon: \"check\", key: \"plans.features.sharePages\", fallback: \"Compartilhar páginas\", tooltip: \"sharePages\" },\n { icon: \"check\", key: \"plans.features.hosting\", fallback: \"Hospedagem inclusa\" },\n // SSL (HTTPS) + CDN — brand/acronym: never translated\n { icon: \"check\", key: null, fallback: \"SSL (HTTPS) + CDN\" },\n { icon: \"check\", key: \"plans.features.freeTemplates\", fallback: \"Templates gratuitos\" },\n] as const;\n\nfunction buildBaseFeatures(translate?: TranslateFn): PlanFeature[] {\n return BASE_FEATURES_KEYS.map(({ icon, key, fallback, ...rest }) => {\n const text = key && translate ? translate(`common.${key}`) : fallback;\n const feature: PlanFeature = { icon, text };\n if (\"tooltip\" in rest && rest.tooltip) feature.tooltip = rest.tooltip;\n return feature;\n });\n}\n\nfunction buildMainFeaturesFromItems(\n items: Plan[\"items\"],\n translate?: TranslateFn\n): PlanMainFeatures {\n const pagesItem = items.find((i) => i.id_addon === ADDON_IDS.PAGES);\n const domainsItem = items.find((i) => i.id_addon === ADDON_IDS.DOMAINS);\n\n const pagesQty = pagesItem?.quantity ?? 0;\n const domainsQty = domainsItem?.quantity ?? 1;\n\n let pages: string;\n if (pagesQty === 0) {\n pages = translate\n ? translate(\"common.plans.mainFeatures.pagesUnlimited\")\n : \"Páginas ilimitadas\";\n } else if (pagesQty === 1) {\n pages = translate\n ? translate(\"common.plans.mainFeatures.pageSingular\")\n : \"1 página\";\n } else {\n pages = translate\n ? translate(\"common.plans.mainFeatures.pagesPlural\", { count: pagesQty })\n : `${pagesQty} páginas`;\n }\n\n let domains: string;\n if (domainsQty >= 999) {\n domains = translate\n ? translate(\"common.plans.mainFeatures.domainsUnlimited\")\n : \"Domínios ilimitados\";\n } else if (domainsQty === 1) {\n domains = translate\n ? translate(\"common.plans.mainFeatures.domainSingular\")\n : \"1 domínio externo\";\n } else {\n domains = translate\n ? translate(\"common.plans.mainFeatures.domainsPlural\", { count: domainsQty })\n : `${domainsQty} domínios externos`;\n }\n\n const users = translate\n ? translate(\"common.plans.mainFeatures.usersUnlimited\")\n : \"Usuários ilimitados\";\n\n return { pages, domains, users };\n}\n\nfunction computePricingByPeriod(apiPlan: Plan): PlanPricingByPeriod {\n const monthly = apiPlan.value;\n if (monthly == null) {\n return { monthly: 0, semiannual: 0, annual: 0 };\n }\n const round2 = (n: number) => Math.round(n * 100) / 100;\n\n const pricing = apiPlan.pricing;\n if (pricing) {\n return {\n monthly,\n semiannual: round2(pricing.semester.charged / 6),\n annual: round2(pricing.annual.charged / 12),\n };\n }\n\n return {\n monthly,\n semiannual: round2(monthly * (1 - apiPlan.discount_semester / 100)),\n annual: round2(monthly * (1 - apiPlan.discount_annual / 100)),\n };\n}\n\nfunction buildUiPlan(plan: Plan, currency: CurrencyCode, translate?: TranslateFn): UiPlan {\n const popular = plan.id === 2 || plan.id_plan === 2;\n const buttonVariant = popular ? \"brand\" : \"default\";\n\n const pricingByPeriod = computePricingByPeriod(plan);\n const monthlyFinal = pricingByPeriod.monthly;\n\n const isStarterPlan = plan.id === STARTER_PLAN_ID || plan.id_plan === STARTER_PLAN_ID;\n const hasProjectAndOnboarding =\n plan.name.toLowerCase().includes(\"agência\") || plan.name.toLowerCase().includes(\"agencia\");\n\n const onboardingText = translate\n ? translate(\"common.plans.features.onboarding\")\n : \"Reunião de Onboarding\";\n const projectMgmtText = translate\n ? translate(\"common.plans.features.projectManagement\")\n : \"Gestão de projetos\";\n const sharePagesText = translate\n ? translate(\"common.plans.features.sharePages\")\n : \"Compartilhar páginas\";\n\n const features: PlanFeature[] = [\n ...buildBaseFeatures(translate).map((feature) =>\n feature.text === sharePagesText && isStarterPlan\n ? { ...feature, icon: \"x\" as const, disabled: true }\n : feature\n ),\n hasProjectAndOnboarding\n ? { icon: \"check\", text: onboardingText }\n : { icon: \"x\", text: onboardingText, disabled: true },\n hasProjectAndOnboarding\n ? { icon: \"check\", text: projectMgmtText, tooltip: \"projectManagement\" }\n : { icon: \"x\", text: projectMgmtText, disabled: true, tooltip: \"projectManagement\" },\n ];\n\n const currencyAwareItems = plan.items.map((item) => ({\n ...item,\n value: item.value ?? 0,\n }));\n\n const priceUnavailable = plan.value == null;\n const unavailableLabel = translate ? translate(\"common.plans.usdNotConfigured\") : \"USD não configurado\";\n\n return {\n planId: plan.id,\n name: plan.name,\n isPopular: popular,\n buttonVariant,\n mainFeatures: buildMainFeaturesFromItems(plan.items, translate),\n features,\n originalPrice: priceUnavailable ? unavailableLabel : formatCurrencyNumber(monthlyFinal, 2, currency),\n price: priceUnavailable ? unavailableLabel : formatCurrencyNumber(monthlyFinal, 2, currency),\n pricingByPeriod,\n priceUnavailable,\n items: currencyAwareItems,\n };\n}\n\n/**\n * Mapeia um plano da API para UiPlan em BRL.\n * Assinatura de 1 argumento — seguro em `plans.map(mapApiPlanToUiPlan)`.\n * Passa `translate` opcional para i18n; sem ele cai nos literais pt-br (back-compat).\n */\nexport function mapApiPlanToUiPlan(plan: Plan, translate?: TranslateFn): UiPlan {\n return buildUiPlan(plan, \"brl\", translate);\n}\n\n/**\n * Variante currency-aware: a moeda vem do gateway da conta (getCurrencyForGateway).\n * Fábrica para uso ergonômico em `.map`: `plans.map(mapApiPlanToUiPlanForCurrency(currency, translate))`.\n * Mantida separada de mapApiPlanToUiPlan para não quebrar callers que passam a fn direto pro `.map`.\n */\nexport const mapApiPlanToUiPlanForCurrency =\n (currency: CurrencyCode, translate?: TranslateFn) =>\n (plan: Plan): UiPlan =>\n buildUiPlan(plan, currency, translate);\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../../src/modules/plans/utils/map-api-plan-to-ui.ts"],"sourcesContent":["import type {\n Plan,\n PlanFeature,\n PlanMainFeatures,\n PlanPricingByPeriod,\n UiPlan,\n} from \"../types/plan.type\";\nimport { ADDON_IDS } from \"../../subscriptions/constants/addons.constants\";\nimport {\n formatCurrencyNumber,\n type CurrencyCode,\n} from \"../../../utils/format/currency\";\n\nexport type PlanBillingPeriod = \"monthly\" | \"semiannual\" | \"annual\";\n\nconst STARTER_PLAN_ID = 415;\nconst AGENCY_PLAN_ID = 3;\n\ntype TranslateFn = (key: any, values?: Record<string, string | number>) => string;\n\nconst planIdOf = (plan: Plan): number => plan.id_plan ?? plan.id;\n\nfunction resolvePlanName(plan: Plan, translate?: TranslateFn): string {\n if (!translate) return plan.name;\n const key = `common.plans.names.${planIdOf(plan)}`;\n try {\n const translated = translate(key);\n return translated && translated !== key ? translated : plan.name;\n } catch {\n return plan.name;\n }\n}\n\nconst BASE_FEATURES_KEYS = [\n { icon: \"check\", key: \"plans.features.unlimitedVisits\", fallback: \"Visitas ilimitadas\" },\n { icon: \"check\", key: \"plans.features.unlimitedLeads\", fallback: \"Leads ilimitadas\" },\n { icon: \"check\", key: \"plans.features.sharePages\", fallback: \"Compartilhar páginas\", tooltip: \"sharePages\" },\n { icon: \"check\", key: \"plans.features.hosting\", fallback: \"Hospedagem inclusa\" },\n // SSL (HTTPS) + CDN — brand/acronym: never translated\n { icon: \"check\", key: null, fallback: \"SSL (HTTPS) + CDN\" },\n { icon: \"check\", key: \"plans.features.freeTemplates\", fallback: \"Templates gratuitos\" },\n] as const;\n\nfunction buildBaseFeatures(translate?: TranslateFn): PlanFeature[] {\n return BASE_FEATURES_KEYS.map(({ icon, key, fallback, ...rest }) => {\n const text = key && translate ? translate(`common.${key}`) : fallback;\n const feature: PlanFeature = { icon, text };\n if (\"tooltip\" in rest && rest.tooltip) feature.tooltip = rest.tooltip;\n return feature;\n });\n}\n\nfunction buildMainFeaturesFromItems(\n items: Plan[\"items\"],\n translate?: TranslateFn\n): PlanMainFeatures {\n const pagesItem = items.find((i) => i.id_addon === ADDON_IDS.PAGES);\n const domainsItem = items.find((i) => i.id_addon === ADDON_IDS.DOMAINS);\n\n const pagesQty = pagesItem?.quantity ?? 0;\n const domainsQty = domainsItem?.quantity ?? 1;\n\n let pages: string;\n if (pagesQty === 0) {\n pages = translate\n ? translate(\"common.plans.mainFeatures.pagesUnlimited\")\n : \"Páginas ilimitadas\";\n } else if (pagesQty === 1) {\n pages = translate\n ? translate(\"common.plans.mainFeatures.pageSingular\")\n : \"1 página\";\n } else {\n pages = translate\n ? translate(\"common.plans.mainFeatures.pagesPlural\", { count: pagesQty })\n : `${pagesQty} páginas`;\n }\n\n let domains: string;\n if (domainsQty >= 999) {\n domains = translate\n ? translate(\"common.plans.mainFeatures.domainsUnlimited\")\n : \"Domínios ilimitados\";\n } else if (domainsQty === 1) {\n domains = translate\n ? translate(\"common.plans.mainFeatures.domainSingular\")\n : \"1 domínio externo\";\n } else {\n domains = translate\n ? translate(\"common.plans.mainFeatures.domainsPlural\", { count: domainsQty })\n : `${domainsQty} domínios externos`;\n }\n\n const users = translate\n ? translate(\"common.plans.mainFeatures.usersUnlimited\")\n : \"Usuários ilimitados\";\n\n return { pages, domains, users };\n}\n\nfunction computePricingByPeriod(apiPlan: Plan): PlanPricingByPeriod {\n const monthly = apiPlan.value;\n if (monthly == null) {\n return { monthly: 0, semiannual: 0, annual: 0 };\n }\n const round2 = (n: number) => Math.round(n * 100) / 100;\n\n const pricing = apiPlan.pricing;\n if (pricing) {\n return {\n monthly,\n semiannual: round2(pricing.semester.charged / 6),\n annual: round2(pricing.annual.charged / 12),\n };\n }\n\n return {\n monthly,\n semiannual: round2(monthly * (1 - apiPlan.discount_semester / 100)),\n annual: round2(monthly * (1 - apiPlan.discount_annual / 100)),\n };\n}\n\nfunction buildUiPlan(plan: Plan, currency: CurrencyCode, translate?: TranslateFn): UiPlan {\n const popular = plan.id === 2 || plan.id_plan === 2;\n const buttonVariant = popular ? \"brand\" : \"default\";\n\n const pricingByPeriod = computePricingByPeriod(plan);\n const monthlyFinal = pricingByPeriod.monthly;\n\n const isStarterPlan = plan.id === STARTER_PLAN_ID || plan.id_plan === STARTER_PLAN_ID;\n const hasProjectAndOnboarding = planIdOf(plan) === AGENCY_PLAN_ID;\n\n const onboardingText = translate\n ? translate(\"common.plans.features.onboarding\")\n : \"Reunião de Onboarding\";\n const projectMgmtText = translate\n ? translate(\"common.plans.features.projectManagement\")\n : \"Gestão de projetos\";\n const sharePagesText = translate\n ? translate(\"common.plans.features.sharePages\")\n : \"Compartilhar páginas\";\n\n const features: PlanFeature[] = [\n ...buildBaseFeatures(translate).map((feature) =>\n feature.text === sharePagesText && isStarterPlan\n ? { ...feature, icon: \"x\" as const, disabled: true }\n : feature\n ),\n hasProjectAndOnboarding\n ? { icon: \"check\", text: onboardingText }\n : { icon: \"x\", text: onboardingText, disabled: true },\n hasProjectAndOnboarding\n ? { icon: \"check\", text: projectMgmtText, tooltip: \"projectManagement\" }\n : { icon: \"x\", text: projectMgmtText, disabled: true, tooltip: \"projectManagement\" },\n ];\n\n const currencyAwareItems = plan.items.map((item) => ({\n ...item,\n value: item.value ?? 0,\n }));\n\n const priceUnavailable = plan.value == null;\n const unavailableLabel = translate ? translate(\"common.plans.usdNotConfigured\") : \"USD não configurado\";\n\n return {\n planId: plan.id,\n name: resolvePlanName(plan, translate),\n isPopular: popular,\n buttonVariant,\n mainFeatures: buildMainFeaturesFromItems(plan.items, translate),\n features,\n originalPrice: priceUnavailable ? unavailableLabel : formatCurrencyNumber(monthlyFinal, 2, currency),\n price: priceUnavailable ? unavailableLabel : formatCurrencyNumber(monthlyFinal, 2, currency),\n pricingByPeriod,\n priceUnavailable,\n items: currencyAwareItems,\n };\n}\n\n/**\n * Mapeia um plano da API para UiPlan em BRL.\n * Assinatura de 1 argumento — seguro em `plans.map(mapApiPlanToUiPlan)`.\n * Passa `translate` opcional para i18n; sem ele cai nos literais pt-br (back-compat).\n */\nexport function mapApiPlanToUiPlan(plan: Plan, translate?: TranslateFn): UiPlan {\n return buildUiPlan(plan, \"brl\", translate);\n}\n\n/**\n * Variante currency-aware: a moeda vem do gateway da conta (getCurrencyForGateway).\n * Fábrica para uso ergonômico em `.map`: `plans.map(mapApiPlanToUiPlanForCurrency(currency, translate))`.\n * Mantida separada de mapApiPlanToUiPlan para não quebrar callers que passam a fn direto pro `.map`.\n */\nexport const mapApiPlanToUiPlanForCurrency =\n (currency: CurrencyCode, translate?: TranslateFn) =>\n (plan: Plan): UiPlan =>\n buildUiPlan(plan, currency, translate);\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -48,7 +48,7 @@ const COUNTRY_OPTIONS = COUNTRIES.map((country: { iso: string; name: string }) =
|
|
|
48
48
|
label: country.name,
|
|
49
49
|
}));
|
|
50
50
|
|
|
51
|
-
function buildBillingDataSchema(
|
|
51
|
+
function buildBillingDataSchema(translate: TranslateFn) {
|
|
52
52
|
const requiredText = z
|
|
53
53
|
.string()
|
|
54
54
|
.trim()
|
|
@@ -73,11 +73,6 @@ function buildBillingDataSchema(isInternational: boolean, translate: TranslateFn
|
|
|
73
73
|
country: requiredText,
|
|
74
74
|
});
|
|
75
75
|
|
|
76
|
-
/* Conta internacional não tem CPF/CNPJ, número nem bairro — mesmo recorte do
|
|
77
|
-
* AddCardModal. Exigir esses campos aqui trancaria o cliente fora do app, já que
|
|
78
|
-
* o modal é bloqueante. */
|
|
79
|
-
if (isInternational) return base;
|
|
80
|
-
|
|
81
76
|
return base.superRefine((data, ctx) => {
|
|
82
77
|
if (!data.streetNumber) {
|
|
83
78
|
ctx.addIssue({
|
|
@@ -140,7 +135,7 @@ function formatDocument(document: string | null | undefined, isCompany: boolean)
|
|
|
140
135
|
function RequiredBillingDataModalContent() {
|
|
141
136
|
const translate = useTranslations();
|
|
142
137
|
const { user } = useAuth();
|
|
143
|
-
const { isRequired, account
|
|
138
|
+
const { isRequired, account } = useRequiredBillingData();
|
|
144
139
|
const { mutateAsync: updateBillingData } = useUpdateBillingData();
|
|
145
140
|
const { mutateAsync: lookupCep } = useViaCep();
|
|
146
141
|
|
|
@@ -150,8 +145,8 @@ function RequiredBillingDataModalContent() {
|
|
|
150
145
|
const isPrefilled = useRef(false);
|
|
151
146
|
|
|
152
147
|
const schema = useMemo(
|
|
153
|
-
() => buildBillingDataSchema(
|
|
154
|
-
[
|
|
148
|
+
() => buildBillingDataSchema(translate),
|
|
149
|
+
[translate],
|
|
155
150
|
);
|
|
156
151
|
|
|
157
152
|
const form = useForm<BillingDataFormValues>({
|
|
@@ -203,9 +198,9 @@ function RequiredBillingDataModalContent() {
|
|
|
203
198
|
neighborhood: account.neighborhood ?? '',
|
|
204
199
|
city: account.city ?? '',
|
|
205
200
|
state: account.state ?? '',
|
|
206
|
-
country: account.country ||
|
|
201
|
+
country: account.country || 'BR',
|
|
207
202
|
});
|
|
208
|
-
}, [account,
|
|
203
|
+
}, [account, reset, user?.email]);
|
|
209
204
|
|
|
210
205
|
const handleDocumentChange = (rawValue: string) => {
|
|
211
206
|
setValue('document', isCompany ? formatCNPJ(rawValue) : formatCPF(rawValue), {
|
|
@@ -222,11 +217,6 @@ function RequiredBillingDataModalContent() {
|
|
|
222
217
|
};
|
|
223
218
|
|
|
224
219
|
const handleCepChange = (rawValue: string) => {
|
|
225
|
-
if (isInternational) {
|
|
226
|
-
setValue('cep', rawValue, { shouldValidate: true });
|
|
227
|
-
return;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
220
|
setValue('cep', formatPostalCode(rawValue), { shouldValidate: true });
|
|
231
221
|
|
|
232
222
|
const digits = rawValue.replace(/\D/g, '');
|
|
@@ -246,12 +236,8 @@ function RequiredBillingDataModalContent() {
|
|
|
246
236
|
|
|
247
237
|
async function onSubmit(data: BillingDataFormValues) {
|
|
248
238
|
const payload: UpdateBillingDataRequest = {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
: {
|
|
252
|
-
financial_document_type: data.personType === 'pf' ? 1 : 2,
|
|
253
|
-
financial_document: data.document,
|
|
254
|
-
}),
|
|
239
|
+
financial_document_type: data.personType === 'pf' ? 1 : 2,
|
|
240
|
+
financial_document: data.document,
|
|
255
241
|
financial_name: data.financialName,
|
|
256
242
|
financial_email: data.financialEmail,
|
|
257
243
|
zipcode: data.cep,
|
|
@@ -322,8 +308,7 @@ function RequiredBillingDataModalContent() {
|
|
|
322
308
|
>
|
|
323
309
|
<div className="flex flex-col gap-6 p-4 lg:p-5 flex-1 overflow-y-auto">
|
|
324
310
|
<div className="flex flex-col gap-5">
|
|
325
|
-
|
|
326
|
-
<div className="flex gap-4">
|
|
311
|
+
<div className="flex gap-4">
|
|
327
312
|
<Controller
|
|
328
313
|
name="personType"
|
|
329
314
|
control={control}
|
|
@@ -362,8 +347,7 @@ function RequiredBillingDataModalContent() {
|
|
|
362
347
|
})}
|
|
363
348
|
/>
|
|
364
349
|
</div>
|
|
365
|
-
|
|
366
|
-
)}
|
|
350
|
+
</div>
|
|
367
351
|
|
|
368
352
|
<FormField
|
|
369
353
|
label={
|
|
@@ -395,12 +379,8 @@ function RequiredBillingDataModalContent() {
|
|
|
395
379
|
</span>
|
|
396
380
|
|
|
397
381
|
<FormField
|
|
398
|
-
label={
|
|
399
|
-
|
|
400
|
-
? translate('common.billing.requiredData.postalCode')
|
|
401
|
-
: translate('common.billing.requiredData.cep')
|
|
402
|
-
}
|
|
403
|
-
placeholder={isInternational ? '' : '_____-___'}
|
|
382
|
+
label={translate('common.billing.requiredData.cep')}
|
|
383
|
+
placeholder="_____-___"
|
|
404
384
|
error={!!errors.cep}
|
|
405
385
|
errorMessage={errors.cep?.message}
|
|
406
386
|
{...register('cep', {
|
|
@@ -418,31 +398,27 @@ function RequiredBillingDataModalContent() {
|
|
|
418
398
|
{...register('street')}
|
|
419
399
|
/>
|
|
420
400
|
</div>
|
|
421
|
-
|
|
422
|
-
<
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
</div>
|
|
431
|
-
)}
|
|
401
|
+
<div className="flex-1">
|
|
402
|
+
<FormField
|
|
403
|
+
label={translate('common.billing.requiredData.streetNumber')}
|
|
404
|
+
placeholder="000"
|
|
405
|
+
error={!!errors.streetNumber}
|
|
406
|
+
errorMessage={errors.streetNumber?.message}
|
|
407
|
+
{...register('streetNumber')}
|
|
408
|
+
/>
|
|
409
|
+
</div>
|
|
432
410
|
</div>
|
|
433
411
|
|
|
434
412
|
<div className="flex gap-4">
|
|
435
|
-
|
|
436
|
-
<
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
</div>
|
|
445
|
-
)}
|
|
413
|
+
<div className="flex-1">
|
|
414
|
+
<FormField
|
|
415
|
+
label={translate('common.billing.requiredData.neighborhood')}
|
|
416
|
+
placeholder={translate('common.billing.requiredData.typeHere')}
|
|
417
|
+
error={!!errors.neighborhood}
|
|
418
|
+
errorMessage={errors.neighborhood?.message}
|
|
419
|
+
{...register('neighborhood')}
|
|
420
|
+
/>
|
|
421
|
+
</div>
|
|
446
422
|
<div className="flex-1">
|
|
447
423
|
<FormField
|
|
448
424
|
label={translate('common.billing.requiredData.complement')}
|
|
@@ -464,16 +440,7 @@ function RequiredBillingDataModalContent() {
|
|
|
464
440
|
/>
|
|
465
441
|
</div>
|
|
466
442
|
<div className="flex-1">
|
|
467
|
-
|
|
468
|
-
<FormField
|
|
469
|
-
label={translate('common.billing.requiredData.stateProvince')}
|
|
470
|
-
placeholder={translate('common.billing.requiredData.typeHere')}
|
|
471
|
-
error={!!errors.state}
|
|
472
|
-
errorMessage={errors.state?.message}
|
|
473
|
-
{...register('state')}
|
|
474
|
-
/>
|
|
475
|
-
) : (
|
|
476
|
-
<Controller
|
|
443
|
+
<Controller
|
|
477
444
|
name="state"
|
|
478
445
|
control={control}
|
|
479
446
|
render={({ field }) => (
|
|
@@ -489,8 +456,7 @@ function RequiredBillingDataModalContent() {
|
|
|
489
456
|
errorMessage={errors.state?.message}
|
|
490
457
|
/>
|
|
491
458
|
)}
|
|
492
|
-
|
|
493
|
-
)}
|
|
459
|
+
/>
|
|
494
460
|
</div>
|
|
495
461
|
</div>
|
|
496
462
|
|
|
@@ -535,6 +535,12 @@ const messages = {
|
|
|
535
535
|
onboarding: 'Onboarding meeting',
|
|
536
536
|
projectManagement: 'Project management',
|
|
537
537
|
},
|
|
538
|
+
names: {
|
|
539
|
+
'415': 'Starter',
|
|
540
|
+
'1': 'Essential',
|
|
541
|
+
'2': 'Growth',
|
|
542
|
+
'3': 'Agency',
|
|
543
|
+
},
|
|
538
544
|
mainFeatures: {
|
|
539
545
|
pagesUnlimited: 'Unlimited pages',
|
|
540
546
|
pageSingular: '1 page',
|
|
@@ -536,6 +536,12 @@ const messages = {
|
|
|
536
536
|
onboarding: 'Reunión de Onboarding',
|
|
537
537
|
projectManagement: 'Gestión de proyectos',
|
|
538
538
|
},
|
|
539
|
+
names: {
|
|
540
|
+
'415': 'Starter',
|
|
541
|
+
'1': 'Essential',
|
|
542
|
+
'2': 'Growth',
|
|
543
|
+
'3': 'Agency',
|
|
544
|
+
},
|
|
539
545
|
mainFeatures: {
|
|
540
546
|
pagesUnlimited: 'Páginas ilimitadas',
|
|
541
547
|
pageSingular: '1 página',
|
|
@@ -460,6 +460,12 @@ const messages = {
|
|
|
460
460
|
onboarding: 'Reunião de Onboarding',
|
|
461
461
|
projectManagement: 'Gestão de projetos',
|
|
462
462
|
},
|
|
463
|
+
names: {
|
|
464
|
+
'415': 'Iniciante',
|
|
465
|
+
'1': 'Essencial',
|
|
466
|
+
'2': 'Negócio',
|
|
467
|
+
'3': 'Agência',
|
|
468
|
+
},
|
|
463
469
|
mainFeatures: {
|
|
464
470
|
pagesUnlimited: 'Páginas ilimitadas',
|
|
465
471
|
pageSingular: '1 página',
|
package/src/index.ts
CHANGED
|
@@ -539,7 +539,10 @@ export {
|
|
|
539
539
|
export { useViaCep } from "./modules/accounts/hooks/useViaCep";
|
|
540
540
|
export { useAccountToken } from "./modules/accounts/hooks/use-account-token.hook";
|
|
541
541
|
export { useRequiredBillingData } from "./modules/accounts/hooks/use-required-billing-data.hook";
|
|
542
|
-
export {
|
|
542
|
+
export {
|
|
543
|
+
hasCompleteBillingData,
|
|
544
|
+
isBrazilianBillingAccount,
|
|
545
|
+
} from "./modules/accounts/utils/billing-data";
|
|
543
546
|
export type { BillingDataSnapshot } from "./modules/accounts/utils/billing-data";
|
|
544
547
|
|
|
545
548
|
// Account Types
|
|
@@ -4,15 +4,14 @@ import { useCurrentAccount } from './current-account.hook';
|
|
|
4
4
|
import { useActiveSubscription } from '../../subscriptions/hooks/find-active-subscription.hook';
|
|
5
5
|
import { hasPaidSubscription } from '../../subscriptions/utils/has-paid-subscription';
|
|
6
6
|
import { useManagementPermissions } from '../../auth/hooks/useManagementPermissions';
|
|
7
|
-
import { hasCompleteBillingData } from '../utils/billing-data';
|
|
8
|
-
import { isUsdAccount } from '../../../utils/format/gateway';
|
|
7
|
+
import { hasCompleteBillingData, isBrazilianBillingAccount } from '../utils/billing-data';
|
|
9
8
|
|
|
10
9
|
/**
|
|
11
10
|
* Contas que já pagaram mas nunca preencheram o cadastro de cobrança ficam sem
|
|
12
11
|
* nota fiscal (chamado "Nota fiscal não emitida em pagamentos confirmados").
|
|
13
12
|
* Este hook diz quando o preenchimento vira obrigatório — o modal que ele
|
|
14
13
|
* alimenta é bloqueante, então só liga com os dados já carregados e só para
|
|
15
|
-
* quem consegue resolver: owner/admin.
|
|
14
|
+
* quem consegue resolver: owner/admin de conta brasileira.
|
|
16
15
|
*/
|
|
17
16
|
export function useRequiredBillingData() {
|
|
18
17
|
const { isOwnerOrAdmin } = useManagementPermissions();
|
|
@@ -27,6 +26,7 @@ export function useRequiredBillingData() {
|
|
|
27
26
|
const isRequired =
|
|
28
27
|
!isLoading &&
|
|
29
28
|
isOwnerOrAdmin &&
|
|
29
|
+
isBrazilianBillingAccount(account) &&
|
|
30
30
|
hasPaidSubscription(subscription) &&
|
|
31
31
|
!hasCompleteBillingData(account);
|
|
32
32
|
|
|
@@ -34,6 +34,5 @@ export function useRequiredBillingData() {
|
|
|
34
34
|
isRequired,
|
|
35
35
|
isLoading,
|
|
36
36
|
account,
|
|
37
|
-
isInternational: isUsdAccount(account),
|
|
38
37
|
};
|
|
39
38
|
}
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import type { Account } from '../types';
|
|
2
|
-
import { isUsdAccount } from '../../../utils/format/gateway';
|
|
3
2
|
|
|
4
3
|
export type BillingDataSnapshot = Pick<
|
|
5
4
|
Account,
|
|
6
|
-
| 'gateway'
|
|
7
5
|
| 'financial_document'
|
|
8
6
|
| 'financial_name'
|
|
9
7
|
| 'financial_email'
|
|
@@ -16,33 +14,46 @@ export type BillingDataSnapshot = Pick<
|
|
|
16
14
|
| 'country'
|
|
17
15
|
>;
|
|
18
16
|
|
|
19
|
-
type BillingField = keyof
|
|
17
|
+
type BillingField = keyof BillingDataSnapshot;
|
|
20
18
|
|
|
21
19
|
/**
|
|
22
|
-
* Campos exigidos
|
|
20
|
+
* Campos exigidos pra emitir a NF-e — sem qualquer um deles a nota não sai.
|
|
23
21
|
*
|
|
24
22
|
* `financial_email` NÃO entra: o backend cai no e-mail do usuário quando ele está
|
|
25
23
|
* vazio (`financial_email || userDocument.email`, accounts/services.js), então a
|
|
26
24
|
* nota sai do mesmo jeito. Exigi-lo aqui bloquearia 4.7k contas pagantes da wl 1
|
|
27
25
|
* em vez das ~460 que de fato estão sem cadastro fiscal. O campo continua no
|
|
28
26
|
* formulário, pré-preenchido — só não é critério de bloqueio.
|
|
27
|
+
*
|
|
28
|
+
* `address_complement` é opcional de verdade.
|
|
29
29
|
*/
|
|
30
|
-
const
|
|
30
|
+
const REQUIRED_BILLING_FIELDS: readonly BillingField[] = [
|
|
31
|
+
'financial_document',
|
|
31
32
|
'financial_name',
|
|
32
33
|
'zipcode',
|
|
33
34
|
'address',
|
|
35
|
+
'address_number',
|
|
36
|
+
'neighborhood',
|
|
34
37
|
'city',
|
|
35
38
|
'state',
|
|
36
39
|
'country',
|
|
37
40
|
];
|
|
38
41
|
|
|
39
|
-
/**
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Conta sujeita à NF-e brasileira. Só ela entra no bloqueio: o formulário pede
|
|
44
|
+
* CPF/CNPJ e CEP de 8 dígitos, que um cliente de fora não tem como fornecer —
|
|
45
|
+
* e o modal é bloqueante, então seria tranca sem saída.
|
|
46
|
+
*
|
|
47
|
+
* País vazio conta como Brasil: é o default do cadastro e a origem da esmagadora
|
|
48
|
+
* maioria da base (155 contas pagas da wl 1 estão assim).
|
|
49
|
+
*/
|
|
50
|
+
export function isBrazilianBillingAccount(
|
|
51
|
+
account: Pick<BillingDataSnapshot, 'country'> | null | undefined,
|
|
52
|
+
): boolean {
|
|
53
|
+
if (!account) return false;
|
|
54
|
+
const country = account.country?.trim().toUpperCase();
|
|
55
|
+
return !country || country === 'BR';
|
|
56
|
+
}
|
|
46
57
|
|
|
47
58
|
function isFilled(value: string | null | undefined): boolean {
|
|
48
59
|
return typeof value === 'string' && value.trim().length > 0;
|
|
@@ -58,11 +69,7 @@ export function hasCompleteBillingData(
|
|
|
58
69
|
): boolean {
|
|
59
70
|
if (!account) return false;
|
|
60
71
|
|
|
61
|
-
const
|
|
62
|
-
? INTERNATIONAL_BILLING_FIELDS
|
|
63
|
-
: BR_BILLING_FIELDS;
|
|
64
|
-
|
|
65
|
-
for (const field of fields) {
|
|
72
|
+
for (const field of REQUIRED_BILLING_FIELDS) {
|
|
66
73
|
if (!isFilled(account[field])) return false;
|
|
67
74
|
}
|
|
68
75
|
|
|
@@ -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),
|