@greatapps/common 1.1.739 → 1.1.741

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 (35) hide show
  1. package/dist/components/modals/billing/RequiredBillingDataModal.mjs +448 -0
  2. package/dist/components/modals/billing/RequiredBillingDataModal.mjs.map +1 -0
  3. package/dist/i18n/messages/en-us.mjs +39 -0
  4. package/dist/i18n/messages/en-us.mjs.map +1 -1
  5. package/dist/i18n/messages/es-es.mjs +39 -0
  6. package/dist/i18n/messages/es-es.mjs.map +1 -1
  7. package/dist/i18n/messages/pt-br.mjs +40 -0
  8. package/dist/i18n/messages/pt-br.mjs.map +1 -1
  9. package/dist/index.mjs +48 -32
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/modules/accounts/actions/account-management.action.mjs +15 -0
  12. package/dist/modules/accounts/actions/account-management.action.mjs.map +1 -1
  13. package/dist/modules/accounts/hooks/use-required-billing-data.hook.mjs +26 -0
  14. package/dist/modules/accounts/hooks/use-required-billing-data.hook.mjs.map +1 -0
  15. package/dist/modules/accounts/hooks/useAccountManagement.mjs +13 -1
  16. package/dist/modules/accounts/hooks/useAccountManagement.mjs.map +1 -1
  17. package/dist/modules/accounts/utils/billing-data.mjs +35 -0
  18. package/dist/modules/accounts/utils/billing-data.mjs.map +1 -0
  19. package/dist/modules/subscriptions/utils/has-paid-subscription.mjs +13 -0
  20. package/dist/modules/subscriptions/utils/has-paid-subscription.mjs.map +1 -0
  21. package/dist/server.mjs +2 -0
  22. package/dist/server.mjs.map +1 -1
  23. package/package.json +1 -1
  24. package/src/components/modals/billing/RequiredBillingDataModal.tsx +548 -0
  25. package/src/i18n/messages/en-us.ts +40 -0
  26. package/src/i18n/messages/es-es.ts +40 -0
  27. package/src/i18n/messages/pt-br.ts +42 -0
  28. package/src/index.ts +11 -0
  29. package/src/modules/accounts/actions/account-management.action.ts +24 -0
  30. package/src/modules/accounts/hooks/use-required-billing-data.hook.ts +38 -0
  31. package/src/modules/accounts/hooks/useAccountManagement.ts +13 -0
  32. package/src/modules/accounts/types.ts +11 -0
  33. package/src/modules/accounts/utils/billing-data.ts +81 -0
  34. package/src/modules/subscriptions/utils/has-paid-subscription.ts +27 -0
  35. package/src/server.ts +1 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../src/modules/accounts/utils/billing-data.ts"],"sourcesContent":["import type { Account } from '../types';\n\n/**\n * Gateway das contas cobradas em USD. Elas não têm CPF/CNPJ, bairro nem número —\n * mesmo critério usado no AddCardModal (`account.gateway === 'stripe_usd'`).\n */\nexport const INTERNATIONAL_GATEWAY = 'stripe_usd';\n\nexport type BillingDataSnapshot = Pick<\n Account,\n | 'gateway'\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 Omit<BillingDataSnapshot, 'gateway'>;\n\n/**\n * Campos exigidos em qualquer país — sem eles a nota fiscal 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 */\nconst INTERNATIONAL_BILLING_FIELDS: readonly BillingField[] = [\n 'financial_name',\n 'zipcode',\n 'address',\n 'city',\n 'state',\n 'country',\n];\n\n/** No Brasil a NF-e também exige documento fiscal, número e bairro. */\nconst BR_BILLING_FIELDS: readonly BillingField[] = [\n ...INTERNATIONAL_BILLING_FIELDS,\n 'financial_document',\n 'address_number',\n 'neighborhood',\n];\n\nexport function isInternationalAccount(\n account: Pick<BillingDataSnapshot, 'gateway'> | null | undefined,\n): boolean {\n return account?.gateway === INTERNATIONAL_GATEWAY;\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 const fields = isInternationalAccount(account)\n ? INTERNATIONAL_BILLING_FIELDS\n : BR_BILLING_FIELDS;\n\n for (const field of fields) {\n if (!isFilled(account[field])) return false;\n }\n\n return true;\n}\n"],"mappings":"AAMO,MAAM,wBAAwB;AA4BrC,MAAM,+BAAwD;AAAA,EAC5D;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAGA,MAAM,oBAA6C;AAAA,EACjD,GAAG;AAAA,EACH;AAAA,EACA;AAAA,EACA;AACF;AAEO,SAAS,uBACd,SACS;AACT,SAAO,SAAS,YAAY;AAC9B;AAEA,SAAS,SAAS,OAA2C;AAC3D,SAAO,OAAO,UAAU,YAAY,MAAM,KAAK,EAAE,SAAS;AAC5D;AAOO,SAAS,uBACd,SACS;AACT,MAAI,CAAC,QAAS,QAAO;AAErB,QAAM,SAAS,uBAAuB,OAAO,IACzC,+BACA;AAEJ,aAAW,SAAS,QAAQ;AAC1B,QAAI,CAAC,SAAS,QAAQ,KAAK,CAAC,EAAG,QAAO;AAAA,EACxC;AAEA,SAAO;AACT;","names":[]}
@@ -0,0 +1,13 @@
1
+ const FREE_PLAN_ID = 5;
2
+ function hasPaidSubscription(subscription) {
3
+ if (!subscription) return false;
4
+ if (subscription.type === "free" || subscription.id_plan === FREE_PLAN_ID) {
5
+ return false;
6
+ }
7
+ if (subscription.type === "trial") return false;
8
+ return subscription.charged === true;
9
+ }
10
+ export {
11
+ hasPaidSubscription
12
+ };
13
+ //# sourceMappingURL=has-paid-subscription.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../src/modules/subscriptions/utils/has-paid-subscription.ts"],"sourcesContent":["import type { Subscription } from '../types/subscription.type';\n\nconst FREE_PLAN_ID = 5;\n\ntype PaidSubscriptionSnapshot = Pick<Subscription, 'type' | 'id_plan' | 'charged'>;\n\n/**\n * A conta já teve pagamento confirmado numa assinatura paga.\n *\n * Diferente de `hasActivePaidSubscription`, aqui NÃO importa se a assinatura\n * segue vigente: quem pagou e depois cancelou/atrasou continua precisando de\n * nota fiscal do que já foi cobrado. Por isso o sinal é `charged` (marcado pelo\n * backend no `bill_paid`), e não `active`/`date_due`.\n *\n * Free e trial ficam de fora — nunca houve 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 subscription.charged === true;\n}\n"],"mappings":"AAEA,MAAM,eAAe;AAcd,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,aAAa,YAAY;AAClC;","names":[]}
package/dist/server.mjs CHANGED
@@ -47,6 +47,7 @@ import {
47
47
  updateUserAction,
48
48
  updateAccountAction,
49
49
  updateAccountUserByIdAction,
50
+ updateBillingDataAction,
50
51
  deleteAccountUserAction,
51
52
  deleteAccountAction,
52
53
  createAccountUserAction,
@@ -135,6 +136,7 @@ export {
135
136
  unlinkGoogleAction,
136
137
  updateAccountAction,
137
138
  updateAccountUserByIdAction,
139
+ updateBillingDataAction,
138
140
  updateProjectAction,
139
141
  updateSubscriptionPaymentAction,
140
142
  updateSubscriptionPlanAction,
@@ -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 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,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 { 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":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@greatapps/common",
3
- "version": "1.1.739",
3
+ "version": "1.1.741",
4
4
  "description": "Shared library for GreatApps frontend applications",
5
5
  "main": "./dist/index.mjs",
6
6
  "types": "./src/index.ts",
@@ -0,0 +1,548 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any -- o generic Translator do next-intl não é
2
+ expressável estruturalmente; o `any` do TranslateFn é intencional e está contido neste arquivo
3
+ (mesmo padrão de modules/plans/utils/map-api-plan-to-ui.ts). */
4
+ 'use client';
5
+
6
+ import { useEffect, useMemo, useRef, useState } from 'react';
7
+ import { Controller, useForm } from 'react-hook-form';
8
+ import { zodResolver } from '@hookform/resolvers/zod';
9
+ import { useTranslations } from 'next-intl';
10
+ import { toast } from 'sonner';
11
+ import z from 'zod';
12
+ import { IconFileInvoice } from '@tabler/icons-react';
13
+
14
+ import { Dialog, DialogContent, DialogHeader, DialogTitle } from '../../ui/overlay/Dialog';
15
+ import { Button } from '../../ui/buttons/Button';
16
+ import { FormField } from '../../ui/form/FormField';
17
+ import { SelectField } from '../../ui/form/SelectField';
18
+ import { Toast } from '../../ui/feedback/Toast';
19
+ import { useAuth } from '../../../providers/auth.provider';
20
+ import { useViaCep } from '../../../modules/accounts/hooks/useViaCep';
21
+ import { useUpdateBillingData } from '../../../modules/accounts/hooks/useAccountManagement';
22
+ import { useRequiredBillingData } from '../../../modules/accounts/hooks/use-required-billing-data.hook';
23
+ import { COUNTRIES } from '../../../utils/countries';
24
+ import { BR_STATE_OPTIONS } from '../../../utils/constants/br-states';
25
+ import { formatCNPJ, formatCPF, formatPostalCode } from '../../../utils/format/masks';
26
+ import { isValidCNPJ, isValidCPF } from '../../../utils/validators/common';
27
+ import type { UpdateBillingDataRequest } from '../../../modules/accounts/types';
28
+
29
+ type TranslateFn = (key: any, values?: Record<string, string | number>) => string;
30
+
31
+ type BillingDataFormValues = {
32
+ personType: 'pf' | 'pj';
33
+ document: string;
34
+ financialName: string;
35
+ financialEmail: string;
36
+ cep: string;
37
+ street: string;
38
+ streetNumber: string;
39
+ complement?: string;
40
+ neighborhood: string;
41
+ city: string;
42
+ state: string;
43
+ country: string;
44
+ };
45
+
46
+ const COUNTRY_OPTIONS = COUNTRIES.map((country: { iso: string; name: string }) => ({
47
+ value: country.iso,
48
+ label: country.name,
49
+ }));
50
+
51
+ function buildBillingDataSchema(isInternational: boolean, translate: TranslateFn) {
52
+ const requiredText = z
53
+ .string()
54
+ .trim()
55
+ .min(1, translate('common.billing.requiredData.requiredField'));
56
+
57
+ const base = z.object({
58
+ personType: z.enum(['pf', 'pj']),
59
+ document: z.string().trim(),
60
+ financialName: requiredText,
61
+ financialEmail: z
62
+ .string()
63
+ .trim()
64
+ .min(1, translate('common.billing.requiredData.requiredField'))
65
+ .email(translate('common.billing.requiredData.invalidEmail')),
66
+ cep: requiredText,
67
+ street: requiredText,
68
+ streetNumber: z.string().trim(),
69
+ complement: z.string().trim().optional(),
70
+ neighborhood: z.string().trim(),
71
+ city: requiredText,
72
+ state: requiredText,
73
+ country: requiredText,
74
+ });
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
+ return base.superRefine((data, ctx) => {
82
+ if (!data.streetNumber) {
83
+ ctx.addIssue({
84
+ code: 'custom',
85
+ path: ['streetNumber'],
86
+ message: translate('common.billing.requiredData.requiredField'),
87
+ });
88
+ }
89
+
90
+ if (!data.neighborhood) {
91
+ ctx.addIssue({
92
+ code: 'custom',
93
+ path: ['neighborhood'],
94
+ message: translate('common.billing.requiredData.requiredField'),
95
+ });
96
+ }
97
+
98
+ if (data.cep.replace(/\D/g, '').length !== 8) {
99
+ ctx.addIssue({
100
+ code: 'custom',
101
+ path: ['cep'],
102
+ message: translate('common.billing.requiredData.invalidCep'),
103
+ });
104
+ }
105
+
106
+ const isCompany = data.personType === 'pj';
107
+ const digits = data.document.replace(/\D/g, '');
108
+
109
+ if (digits.length !== (isCompany ? 14 : 11)) {
110
+ ctx.addIssue({
111
+ code: 'custom',
112
+ path: ['document'],
113
+ message: isCompany
114
+ ? translate('common.billing.requiredData.invalidCnpj')
115
+ : translate('common.billing.requiredData.invalidCpf'),
116
+ });
117
+ return;
118
+ }
119
+
120
+ /* Dígito verificador, mesmo algoritmo do backend: sem isso 111.111.111-11 passa
121
+ * aqui e o PUT da conta é recusado depois, sem o usuário saber o motivo. */
122
+ const isValid = isCompany ? isValidCNPJ(digits) : isValidCPF(digits);
123
+ if (!isValid) {
124
+ ctx.addIssue({
125
+ code: 'custom',
126
+ path: ['document'],
127
+ message: isCompany
128
+ ? translate('common.billing.requiredData.invalidCnpjChecksum')
129
+ : translate('common.billing.requiredData.invalidCpfChecksum'),
130
+ });
131
+ }
132
+ });
133
+ }
134
+
135
+ function formatDocument(document: string | null | undefined, isCompany: boolean) {
136
+ if (!document) return '';
137
+ return isCompany ? formatCNPJ(document) : formatCPF(document);
138
+ }
139
+
140
+ function RequiredBillingDataModalContent() {
141
+ const translate = useTranslations();
142
+ const { user } = useAuth();
143
+ const { isRequired, account, isInternational } = useRequiredBillingData();
144
+ const { mutateAsync: updateBillingData } = useUpdateBillingData();
145
+ const { mutateAsync: lookupCep } = useViaCep();
146
+
147
+ /* O modal fecha na hora do submit e não reabre enquanto o invalidate do account
148
+ * não volta — sem isso o usuário vê o modal piscar de novo com o cache antigo. */
149
+ const [isSubmitted, setIsSubmitted] = useState(false);
150
+ const isPrefilled = useRef(false);
151
+
152
+ const schema = useMemo(
153
+ () => buildBillingDataSchema(isInternational, translate),
154
+ [isInternational, translate],
155
+ );
156
+
157
+ const form = useForm<BillingDataFormValues>({
158
+ resolver: zodResolver(schema),
159
+ mode: 'onChange',
160
+ defaultValues: {
161
+ personType: 'pf',
162
+ document: '',
163
+ financialName: '',
164
+ financialEmail: '',
165
+ cep: '',
166
+ street: '',
167
+ streetNumber: '',
168
+ complement: '',
169
+ neighborhood: '',
170
+ city: '',
171
+ state: '',
172
+ country: 'BR',
173
+ },
174
+ });
175
+
176
+ const {
177
+ register,
178
+ control,
179
+ handleSubmit,
180
+ reset,
181
+ setValue,
182
+ watch,
183
+ formState: { errors, isSubmitting, isValid },
184
+ } = form;
185
+
186
+ const personType = watch('personType');
187
+ const isCompany = personType === 'pj';
188
+
189
+ useEffect(() => {
190
+ if (!account || isPrefilled.current) return;
191
+ isPrefilled.current = true;
192
+
193
+ const accountIsCompany = account.financial_document_type === 2;
194
+ reset({
195
+ personType: accountIsCompany ? 'pj' : 'pf',
196
+ document: formatDocument(account.financial_document, accountIsCompany),
197
+ financialName: account.financial_name ?? '',
198
+ financialEmail: account.financial_email || user?.email || '',
199
+ cep: account.zipcode ?? '',
200
+ street: account.address ?? '',
201
+ streetNumber: account.address_number ?? '',
202
+ complement: account.address_complement ?? '',
203
+ neighborhood: account.neighborhood ?? '',
204
+ city: account.city ?? '',
205
+ state: account.state ?? '',
206
+ country: account.country || (isInternational ? 'US' : 'BR'),
207
+ });
208
+ }, [account, isInternational, reset, user?.email]);
209
+
210
+ const handleDocumentChange = (rawValue: string) => {
211
+ setValue('document', isCompany ? formatCNPJ(rawValue) : formatCPF(rawValue), {
212
+ shouldValidate: true,
213
+ });
214
+ };
215
+
216
+ const handlePersonTypeChange = (value: 'pf' | 'pj') => {
217
+ if (value === personType) return;
218
+ setValue('document', '');
219
+ /* shouldValidate reroda o resolver: sem isso o `isValid` fica preso no valor
220
+ * do documento antigo e o botão continua habilitado com o campo já limpo. */
221
+ setValue('personType', value, { shouldValidate: true });
222
+ };
223
+
224
+ const handleCepChange = (rawValue: string) => {
225
+ if (isInternational) {
226
+ setValue('cep', rawValue, { shouldValidate: true });
227
+ return;
228
+ }
229
+
230
+ setValue('cep', formatPostalCode(rawValue), { shouldValidate: true });
231
+
232
+ const digits = rawValue.replace(/\D/g, '');
233
+ if (digits.length !== 8) return;
234
+
235
+ lookupCep(digits)
236
+ .then((data) => {
237
+ setValue('street', data.logradouro, { shouldValidate: true });
238
+ setValue('neighborhood', data.bairro, { shouldValidate: true });
239
+ setValue('city', data.localidade, { shouldValidate: true });
240
+ setValue('state', data.uf, { shouldValidate: true });
241
+ })
242
+ .catch(() => {
243
+ // CEP não encontrado: o usuário preenche o endereço na mão.
244
+ });
245
+ };
246
+
247
+ async function onSubmit(data: BillingDataFormValues) {
248
+ const payload: UpdateBillingDataRequest = {
249
+ ...(isInternational
250
+ ? {}
251
+ : {
252
+ financial_document_type: data.personType === 'pf' ? 1 : 2,
253
+ financial_document: data.document,
254
+ }),
255
+ financial_name: data.financialName,
256
+ financial_email: data.financialEmail,
257
+ zipcode: data.cep,
258
+ address: data.street,
259
+ address_number: data.streetNumber,
260
+ address_complement: data.complement ?? '',
261
+ neighborhood: data.neighborhood,
262
+ city: data.city,
263
+ state: data.state,
264
+ country: data.country,
265
+ };
266
+
267
+ try {
268
+ await updateBillingData(payload);
269
+ setIsSubmitted(true);
270
+ toast.custom(
271
+ (t) => (
272
+ <Toast
273
+ variant="success"
274
+ message={translate('common.billing.requiredData.successToast')}
275
+ toastId={t}
276
+ />
277
+ ),
278
+ { duration: 5000 },
279
+ );
280
+ } catch (error) {
281
+ /* O backend devolve a razão exata da recusa (ex.: documento fiscal inválido).
282
+ * Trocar por um texto genérico deixaria o usuário travado sem saber o que corrigir. */
283
+ const message =
284
+ error instanceof Error && error.message
285
+ ? error.message
286
+ : translate('common.billing.requiredData.errorToast');
287
+ toast.custom((t) => <Toast variant="error" message={message} toastId={t} />, {
288
+ duration: 5000,
289
+ });
290
+ }
291
+ }
292
+
293
+ if (!isRequired || isSubmitted) return null;
294
+
295
+ return (
296
+ <Dialog open onOpenChange={() => {}}>
297
+ <DialogContent
298
+ showCloseButton={false}
299
+ onEscapeKeyDown={(event: Event) => event.preventDefault()}
300
+ onPointerDownOutside={(event: Event) => event.preventDefault()}
301
+ onInteractOutside={(event: Event) => event.preventDefault()}
302
+ className="flex flex-col p-0 gap-0 max-w-full sm:max-w-full border-0 rounded-t-2xl rounded-b-none h-dvh top-0 bottom-0 left-0 right-0 translate-x-0 translate-y-0 lg:max-w-[502px] lg:h-auto lg:max-h-[90vh] lg:rounded-lg lg:border lg:top-[50%] lg:left-[50%] lg:right-auto lg:bottom-auto lg:translate-x-[-50%] lg:translate-y-[-50%] overflow-hidden"
303
+ >
304
+ <DialogHeader className="gap-3 text-left p-4 lg:p-5 border-b border-zinc-200 shrink-0">
305
+ <div className="flex items-center justify-center w-10 h-10 bg-zinc-50 rounded-lg">
306
+ <IconFileInvoice size={24} className="text-zinc-950" />
307
+ </div>
308
+ <div className="flex flex-col gap-2">
309
+ <DialogTitle className="paragraph-medium-semibold text-zinc-950">
310
+ {translate('common.billing.requiredData.title')}
311
+ </DialogTitle>
312
+ <span className="paragraph-small-regular text-zinc-600">
313
+ {translate('common.billing.requiredData.description')}
314
+ </span>
315
+ </div>
316
+ </DialogHeader>
317
+
318
+ <form
319
+ onSubmit={handleSubmit(onSubmit)}
320
+ className="flex flex-col flex-1 min-h-0"
321
+ noValidate
322
+ >
323
+ <div className="flex flex-col gap-6 p-4 lg:p-5 flex-1 overflow-y-auto">
324
+ <div className="flex flex-col gap-5">
325
+ {!isInternational && (
326
+ <div className="flex gap-4">
327
+ <Controller
328
+ name="personType"
329
+ control={control}
330
+ render={({ field }) => (
331
+ <SelectField
332
+ label={translate('common.billing.requiredData.personType')}
333
+ placeholder={translate('common.billing.requiredData.select')}
334
+ value={field.value}
335
+ onChange={(value) => handlePersonTypeChange(value as 'pf' | 'pj')}
336
+ options={[
337
+ {
338
+ value: 'pf',
339
+ label: translate('common.billing.requiredData.personTypePf'),
340
+ },
341
+ {
342
+ value: 'pj',
343
+ label: translate('common.billing.requiredData.personTypePj'),
344
+ },
345
+ ]}
346
+ containerClassName="flex-1"
347
+ />
348
+ )}
349
+ />
350
+ <div className="flex-1">
351
+ <FormField
352
+ label={
353
+ isCompany
354
+ ? translate('common.billing.requiredData.cnpj')
355
+ : translate('common.billing.requiredData.cpf')
356
+ }
357
+ placeholder={isCompany ? '__.___.___/____-__' : '___.___.___-__'}
358
+ error={!!errors.document}
359
+ errorMessage={errors.document?.message}
360
+ {...register('document', {
361
+ onChange: (event) => handleDocumentChange(event.target.value),
362
+ })}
363
+ />
364
+ </div>
365
+ </div>
366
+ )}
367
+
368
+ <FormField
369
+ label={
370
+ isCompany
371
+ ? translate('common.billing.requiredData.companyName')
372
+ : translate('common.billing.requiredData.fullName')
373
+ }
374
+ placeholder={translate('common.billing.requiredData.typeHere')}
375
+ error={!!errors.financialName}
376
+ errorMessage={errors.financialName?.message}
377
+ {...register('financialName')}
378
+ />
379
+
380
+ <FormField
381
+ label={translate('common.billing.requiredData.financialEmail')}
382
+ placeholder={translate('common.billing.requiredData.financialEmailPlaceholder')}
383
+ type="email"
384
+ error={!!errors.financialEmail}
385
+ errorMessage={errors.financialEmail?.message}
386
+ {...register('financialEmail')}
387
+ />
388
+ </div>
389
+
390
+ <div className="h-px bg-zinc-200" />
391
+
392
+ <div className="flex flex-col gap-5">
393
+ <span className="paragraph-small-semibold text-zinc-950">
394
+ {translate('common.billing.requiredData.addressHeading')}
395
+ </span>
396
+
397
+ <FormField
398
+ label={
399
+ isInternational
400
+ ? translate('common.billing.requiredData.postalCode')
401
+ : translate('common.billing.requiredData.cep')
402
+ }
403
+ placeholder={isInternational ? '' : '_____-___'}
404
+ error={!!errors.cep}
405
+ errorMessage={errors.cep?.message}
406
+ {...register('cep', {
407
+ onChange: (event) => handleCepChange(event.target.value),
408
+ })}
409
+ />
410
+
411
+ <div className="flex gap-4">
412
+ <div className="flex-[3]">
413
+ <FormField
414
+ label={translate('common.billing.requiredData.street')}
415
+ placeholder={translate('common.billing.requiredData.typeHere')}
416
+ error={!!errors.street}
417
+ errorMessage={errors.street?.message}
418
+ {...register('street')}
419
+ />
420
+ </div>
421
+ {!isInternational && (
422
+ <div className="flex-1">
423
+ <FormField
424
+ label={translate('common.billing.requiredData.streetNumber')}
425
+ placeholder="000"
426
+ error={!!errors.streetNumber}
427
+ errorMessage={errors.streetNumber?.message}
428
+ {...register('streetNumber')}
429
+ />
430
+ </div>
431
+ )}
432
+ </div>
433
+
434
+ <div className="flex gap-4">
435
+ {!isInternational && (
436
+ <div className="flex-1">
437
+ <FormField
438
+ label={translate('common.billing.requiredData.neighborhood')}
439
+ placeholder={translate('common.billing.requiredData.typeHere')}
440
+ error={!!errors.neighborhood}
441
+ errorMessage={errors.neighborhood?.message}
442
+ {...register('neighborhood')}
443
+ />
444
+ </div>
445
+ )}
446
+ <div className="flex-1">
447
+ <FormField
448
+ label={translate('common.billing.requiredData.complement')}
449
+ optional
450
+ placeholder={translate('common.billing.requiredData.typeHere')}
451
+ {...register('complement')}
452
+ />
453
+ </div>
454
+ </div>
455
+
456
+ <div className="flex gap-4">
457
+ <div className="flex-1">
458
+ <FormField
459
+ label={translate('common.billing.requiredData.city')}
460
+ placeholder={translate('common.billing.requiredData.typeHere')}
461
+ error={!!errors.city}
462
+ errorMessage={errors.city?.message}
463
+ {...register('city')}
464
+ />
465
+ </div>
466
+ <div className="flex-1">
467
+ {isInternational ? (
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
477
+ name="state"
478
+ control={control}
479
+ render={({ field }) => (
480
+ <SelectField
481
+ label={translate('common.billing.requiredData.state')}
482
+ placeholder={translate('common.billing.requiredData.select')}
483
+ value={field.value || undefined}
484
+ onChange={(value) =>
485
+ setValue('state', value as string, { shouldValidate: true })
486
+ }
487
+ options={BR_STATE_OPTIONS}
488
+ error={!!errors.state}
489
+ errorMessage={errors.state?.message}
490
+ />
491
+ )}
492
+ />
493
+ )}
494
+ </div>
495
+ </div>
496
+
497
+ <Controller
498
+ name="country"
499
+ control={control}
500
+ render={({ field }) => (
501
+ <SelectField
502
+ label={translate('common.billing.requiredData.country')}
503
+ placeholder={translate('common.billing.requiredData.select')}
504
+ value={field.value || undefined}
505
+ onChange={(value) =>
506
+ setValue('country', value as string, { shouldValidate: true })
507
+ }
508
+ options={COUNTRY_OPTIONS}
509
+ error={!!errors.country}
510
+ errorMessage={errors.country?.message}
511
+ />
512
+ )}
513
+ />
514
+ </div>
515
+ </div>
516
+
517
+ <div className="flex items-center justify-end p-4 lg:p-5 border-t border-zinc-200 shrink-0">
518
+ <Button
519
+ type="submit"
520
+ className="h-10! w-full lg:w-fit"
521
+ disabled={!isValid || isSubmitting}
522
+ loading={isSubmitting}
523
+ >
524
+ {isSubmitting
525
+ ? translate('common.billing.requiredData.submitting')
526
+ : translate('common.billing.requiredData.submit')}
527
+ </Button>
528
+ </div>
529
+ </form>
530
+ </DialogContent>
531
+ </Dialog>
532
+ );
533
+ }
534
+
535
+ /**
536
+ * Modal bloqueante que exige o cadastro de cobrança/fiscal de contas que já
537
+ * pagaram sem ele — sem esses dados a nota fiscal não é emitida. Monte uma vez
538
+ * no layout raiz do app; ele mesmo decide se aparece (ver `useRequiredBillingData`).
539
+ */
540
+ export default function RequiredBillingDataModal() {
541
+ const { user, isLoading } = useAuth();
542
+
543
+ /* Sem sessão não há conta nem assinatura pra consultar — evita disparar as
544
+ * queries (e tomar 401) em /login, /register e demais rotas públicas. */
545
+ if (isLoading || !user) return null;
546
+
547
+ return <RequiredBillingDataModalContent />;
548
+ }