@greatapps/common 1.1.806 → 1.1.808

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 (34) hide show
  1. package/dist/components/account/sections/affiliates/ReceivingAccountModal.mjs +56 -18
  2. package/dist/components/account/sections/affiliates/ReceivingAccountModal.mjs.map +1 -1
  3. package/dist/components/account/sections/affiliates/RequestWithdrawalSheet.mjs +30 -3
  4. package/dist/components/account/sections/affiliates/RequestWithdrawalSheet.mjs.map +1 -1
  5. package/dist/i18n/messages/en-us.mjs +9 -1
  6. package/dist/i18n/messages/en-us.mjs.map +1 -1
  7. package/dist/i18n/messages/es-es.mjs +9 -1
  8. package/dist/i18n/messages/es-es.mjs.map +1 -1
  9. package/dist/i18n/messages/pt-br.mjs +9 -1
  10. package/dist/i18n/messages/pt-br.mjs.map +1 -1
  11. package/dist/index.mjs +7 -1
  12. package/dist/index.mjs.map +1 -1
  13. package/dist/testing/factories/affiliate.factory.mjs +24 -0
  14. package/dist/testing/factories/affiliate.factory.mjs.map +1 -0
  15. package/dist/testing/factories/index.mjs +4 -0
  16. package/dist/testing/factories/index.mjs.map +1 -1
  17. package/dist/testing/factories/withdrawal-limits.factory.mjs +18 -0
  18. package/dist/testing/factories/withdrawal-limits.factory.mjs.map +1 -0
  19. package/dist/utils/format/masks.mjs +6 -1
  20. package/dist/utils/format/masks.mjs.map +1 -1
  21. package/dist/utils/validators/pix-key.mjs +31 -0
  22. package/dist/utils/validators/pix-key.mjs.map +1 -0
  23. package/package.json +10 -9
  24. package/src/components/account/sections/affiliates/ReceivingAccountModal.tsx +75 -20
  25. package/src/components/account/sections/affiliates/RequestWithdrawalSheet.tsx +36 -3
  26. package/src/i18n/messages/en-us.ts +11 -0
  27. package/src/i18n/messages/es-es.ts +11 -0
  28. package/src/i18n/messages/pt-br.ts +11 -0
  29. package/src/index.ts +2 -0
  30. package/src/testing/factories/affiliate.factory.ts +22 -0
  31. package/src/testing/factories/index.ts +2 -0
  32. package/src/testing/factories/withdrawal-limits.factory.ts +16 -0
  33. package/src/utils/format/masks.ts +6 -2
  34. package/src/utils/validators/pix-key.ts +29 -0
@@ -23,6 +23,11 @@ import {
23
23
  parseCurrencyToNumber,
24
24
  } from '../../../../utils/format/currency';
25
25
  import { formatDocument, formatPixKey } from '../../../../utils/format/masks';
26
+ import {
27
+ AMBIGUOUS_PIX_KEY,
28
+ isPixKeyFromAnotherDocument,
29
+ normalizePixKeyForStorage,
30
+ } from '../../../../utils/validators/pix-key';
26
31
  import { formatFileSize, isPDFFile } from '../../../../utils/file/index';
27
32
  import { useUploadProgress } from '../../../../hooks/useUploadProgress';
28
33
  import useIsMobile from '../../../../hooks/useIsMobile';
@@ -144,6 +149,12 @@ export function RequestWithdrawalSheet() {
144
149
  const requiresInvoice = limits?.requiresInvoice ?? false;
145
150
  const hasReceivingAccount = !!(affiliate?.pix && affiliate?.holder_name && affiliate?.holder_document);
146
151
 
152
+ const holderDocument = affiliate?.holder_document ?? '';
153
+ const storedPixKey = affiliate?.pix ? normalizePixKeyForStorage(affiliate.pix, holderDocument) : undefined;
154
+ const isPixKeyInvalid = storedPixKey === null || storedPixKey === AMBIGUOUS_PIX_KEY;
155
+ const isPixKeyMismatch = typeof storedPixKey === 'string' && isPixKeyFromAnotherDocument(storedPixKey, holderDocument);
156
+ const hasPixKeyIssue = isPixKeyInvalid || isPixKeyMismatch;
157
+
147
158
  const minValueLabel = minWithdrawal.toFixed(2).replace('.', ',');
148
159
 
149
160
  const withdrawalSchema = useMemo(
@@ -309,8 +320,13 @@ export function RequestWithdrawalSheet() {
309
320
  <span className="w-40 text-zinc-950 paragraph-small-medium">
310
321
  {translateSheet('pixKeyLabel')}
311
322
  </span>
312
- <span className="w-full text-zinc-600 paragraph-small-regular truncate">
313
- {affiliate?.pix ? formatPixKey(affiliate.pix) : '—'}
323
+ <span
324
+ className={cn(
325
+ 'w-full paragraph-small-regular truncate',
326
+ hasPixKeyIssue ? 'text-red-600' : 'text-zinc-600'
327
+ )}
328
+ >
329
+ {affiliate?.pix ? (hasPixKeyIssue ? affiliate.pix : formatPixKey(affiliate.pix)) : '—'}
314
330
  </span>
315
331
  </div>
316
332
  <div className="flex items-center gap-3">
@@ -322,9 +338,24 @@ export function RequestWithdrawalSheet() {
322
338
  </span>
323
339
  </div>
324
340
  </div>
341
+ {hasPixKeyIssue && (
342
+ <div role="alert" className="flex flex-col gap-3 rounded-lg bg-red-50 p-4">
343
+ <div className="flex items-start gap-3">
344
+ <IconAlertTriangle size={20} className="text-red-600 shrink-0 mt-0.5" />
345
+ <p className="paragraph-small-medium text-zinc-950">
346
+ {isPixKeyInvalid
347
+ ? translateSheet('pixKeyInvalidWarning')
348
+ : translateSheet('pixKeyMismatchWarning')}
349
+ </p>
350
+ </div>
351
+ <Button className="h-10! w-fit" type="button" onClick={openReceivingAccount}>
352
+ {translateSheet('updateReceivingAccountButton')}
353
+ </Button>
354
+ </div>
355
+ )}
325
356
  </div>
326
357
 
327
- {limits && !limits.canWithdraw ? (
358
+ {limits && !limits.canWithdraw && !hasPixKeyIssue ? (
328
359
  <div className="flex flex-col gap-3 bg-zinc-50 rounded-lg p-4">
329
360
  <div className="size-8 bg-white rounded-lg shadow-[0px_2px_10px_0px_rgba(26,26,26,0.05)] flex items-center justify-center">
330
361
  <IconAlertTriangle size={18} className="text-amber-500" />
@@ -350,6 +381,7 @@ export function RequestWithdrawalSheet() {
350
381
  render={({ field, fieldState }) => (
351
382
  <FormField
352
383
  required
384
+ disabled={hasPixKeyIssue}
353
385
  label={translateSheet('withdrawalValueLabel')}
354
386
  placeholder={translateSheet('withdrawalValuePlaceholder')}
355
387
  inputMode="numeric"
@@ -481,6 +513,7 @@ export function RequestWithdrawalSheet() {
481
513
  !affiliate ||
482
514
  !limits ||
483
515
  !hasReceivingAccount ||
516
+ hasPixKeyIssue ||
484
517
  !withdrawalAmount ||
485
518
  withdrawalAmount < minWithdrawal ||
486
519
  toCents(withdrawalAmount) > toCents(availableToWithdraw) ||
@@ -537,6 +537,11 @@ const messages = {
537
537
  blockedDescription:
538
538
  'The registered Pix key is a tax ID that differs from the account holder document. Update the key or the document to request withdrawals again.',
539
539
  blockedEditAccountButton: 'Edit receiving account',
540
+ pixKeyInvalidWarning:
541
+ 'Your Pix key is invalid. Update your receiving details to request a withdrawal.',
542
+ pixKeyMismatchWarning:
543
+ 'The Pix key does not match the account holder CPF/CNPJ. Update your receiving details to request a withdrawal.',
544
+ updateReceivingAccountButton: 'Update receiving details',
540
545
  },
541
546
  bankAccount: {
542
547
  dialogTitle: 'Receiving account',
@@ -558,6 +563,12 @@ const messages = {
558
563
  validationCnpjChecksum: 'Invalid CNPJ — check the verification digits',
559
564
  validationPixDocumentMismatch:
560
565
  'The Pix key is a tax ID that differs from the account holder document. Use a key for the same document or fix the holder tax ID.',
566
+ validationPixInvalid:
567
+ 'Invalid Pix key. Use a CPF, CNPJ, mobile number with area code, email or random key.',
568
+ pixKeyPreview: 'Will be saved as: {value}',
569
+ pixAmbiguousQuestion: 'Is this number a mobile phone or a CPF?',
570
+ pixAmbiguousMobile: 'Mobile',
571
+ pixAmbiguousCpf: 'CPF',
561
572
  pixCnpjInvoiceNotice:
562
573
  'Company account: withdrawals require a PDF invoice and do not use the individual monthly limit.',
563
574
  },
@@ -540,6 +540,11 @@ const messages = {
540
540
  blockedDescription:
541
541
  'La clave Pix registrada es un CPF/CNPJ distinto del documento del titular. Ajusta la clave o el documento para volver a solicitar retiros.',
542
542
  blockedEditAccountButton: 'Editar cuenta de cobro',
543
+ pixKeyInvalidWarning:
544
+ 'Tu clave Pix no es válida. Actualiza tus datos de cobro para solicitar el retiro.',
545
+ pixKeyMismatchWarning:
546
+ 'La clave Pix no coincide con el CPF/CNPJ del titular. Actualiza tus datos de cobro para solicitar el retiro.',
547
+ updateReceivingAccountButton: 'Actualizar datos de cobro',
543
548
  },
544
549
  bankAccount: {
545
550
  dialogTitle: 'Cuenta de recepción',
@@ -561,6 +566,12 @@ const messages = {
561
566
  validationCnpjChecksum: 'CNPJ inválido — verifica los dígitos verificadores',
562
567
  validationPixDocumentMismatch:
563
568
  'La clave Pix es un CPF/CNPJ distinto del documento del titular. Usa una clave del mismo documento o corrige el CPF/CNPJ del titular.',
569
+ validationPixInvalid:
570
+ 'Clave Pix inválida. Usa CPF, CNPJ, celular con código de área, correo electrónico o clave aleatoria.',
571
+ pixKeyPreview: 'Se guardará como: {value}',
572
+ pixAmbiguousQuestion: '¿Este número es un celular o un CPF?',
573
+ pixAmbiguousMobile: 'Celular',
574
+ pixAmbiguousCpf: 'CPF',
564
575
  pixCnpjInvoiceNotice:
565
576
  'Cuenta CNPJ: el retiro exige factura en PDF y no usa el límite mensual de CPF.',
566
577
  },
@@ -548,6 +548,11 @@ const messages = {
548
548
  blockedDescription:
549
549
  'A chave Pix cadastrada é um CPF/CNPJ diferente do documento do titular. Ajuste a chave ou o documento para voltar a solicitar saques.',
550
550
  blockedEditAccountButton: 'Editar conta de recebimento',
551
+ pixKeyInvalidWarning:
552
+ 'Sua chave Pix está inválida. Atualize seus dados de recebimento para solicitar o saque.',
553
+ pixKeyMismatchWarning:
554
+ 'A chave Pix não confere com o CPF/CNPJ do titular. Atualize seus dados de recebimento para solicitar o saque.',
555
+ updateReceivingAccountButton: 'Atualizar dados de recebimento',
551
556
  },
552
557
  bankAccount: {
553
558
  dialogTitle: 'Conta de recebimento',
@@ -569,6 +574,12 @@ const messages = {
569
574
  validationCnpjChecksum: 'CNPJ inválido — confira os dígitos verificadores',
570
575
  validationPixDocumentMismatch:
571
576
  'A chave Pix informada é um CPF/CNPJ diferente do documento do titular. Use uma chave do mesmo documento ou corrija o CPF/CNPJ do titular.',
577
+ validationPixInvalid:
578
+ 'Chave Pix inválida. Use CPF, CNPJ, celular com DDD, e-mail ou chave aleatória.',
579
+ pixKeyPreview: 'Será salva como: {value}',
580
+ pixAmbiguousQuestion: 'Esse número é um celular ou um CPF?',
581
+ pixAmbiguousMobile: 'Celular',
582
+ pixAmbiguousCpf: 'CPF',
572
583
  pixCnpjInvoiceNotice:
573
584
  'Conta CNPJ: o saque passa a exigir nota fiscal em PDF e não usa o limite mensal de CPF.',
574
585
  },
package/src/index.ts CHANGED
@@ -559,7 +559,9 @@ export {
559
559
  formatCPF,
560
560
  formatCNPJ,
561
561
  formatCardNumber,
562
+ formatPixKey,
562
563
  } from "./utils/format/masks";
564
+ export { AMBIGUOUS_PIX_KEY, isPixKeyFromAnotherDocument, normalizePixKeyForStorage } from "./utils/validators/pix-key";
563
565
  export {
564
566
  formatCurrency,
565
567
  formatCurrencyNumber,
@@ -0,0 +1,22 @@
1
+ import { faker } from '@faker-js/faker';
2
+ import type { Affiliate } from '../../modules/affiliates/types/affiliate.type';
3
+ import { TEST_IDS } from '../constants';
4
+ import { createMock } from './create-mock';
5
+
6
+ export const createAffiliateMock = createMock<Affiliate, Partial<Affiliate>>((overrides) => ({
7
+ id: overrides?.id ?? 1,
8
+ id_wl: overrides?.id_wl ?? TEST_IDS.wl,
9
+ id_account: overrides?.id_account ?? TEST_IDS.account,
10
+ active: overrides?.active ?? true,
11
+ deleted: overrides?.deleted ?? false,
12
+ balance: overrides?.balance ?? 0,
13
+ datetime_add: overrides?.datetime_add ?? null,
14
+ datetime_alt: overrides?.datetime_alt ?? null,
15
+ datetime_del: overrides?.datetime_del ?? null,
16
+ holder_name: overrides?.holder_name || faker.person.fullName(),
17
+ pix: overrides?.pix ?? null,
18
+ holder_document: overrides?.holder_document ?? null,
19
+ bank: overrides?.bank ?? null,
20
+ agency: overrides?.agency ?? null,
21
+ account: overrides?.account ?? null,
22
+ }));
@@ -8,3 +8,5 @@ export { createPlanMock, createPlanItemMock } from './plan.factory';
8
8
  export { createProjectMock } from './project.factory';
9
9
  export { createProjectUserMock } from './project-user.factory';
10
10
  export { createCouponMock } from './coupon.factory';
11
+ export { createAffiliateMock } from './affiliate.factory';
12
+ export { createWithdrawalLimitsMock } from './withdrawal-limits.factory';
@@ -0,0 +1,16 @@
1
+ import type { WithdrawalLimits } from '../../modules/affiliates/types/affiliate.type';
2
+ import { createMock } from './create-mock';
3
+
4
+ export const createWithdrawalLimitsMock = createMock<WithdrawalLimits, Partial<WithdrawalLimits>>((overrides) => ({
5
+ personType: overrides?.personType ?? 'cpf',
6
+ financialDocumentType: overrides?.financialDocumentType ?? 1,
7
+ requiresInvoice: overrides?.requiresInvoice ?? false,
8
+ minValue: overrides?.minValue ?? 25,
9
+ maxValuePerMonth: overrides?.maxValuePerMonth ?? 350,
10
+ monthlyUsed: overrides?.monthlyUsed ?? 0,
11
+ monthlyRemaining: overrides?.monthlyRemaining ?? 350,
12
+ balance: overrides?.balance ?? 500,
13
+ availableToWithdraw: overrides?.availableToWithdraw ?? 350,
14
+ canWithdraw: overrides?.canWithdraw ?? true,
15
+ blockReason: overrides?.blockReason ?? null,
16
+ }));
@@ -92,8 +92,12 @@ function formatIfCpfCnpjShaped(value: string): string {
92
92
  /** Documento do titular: CPF/CNPJ mascarado; qualquer outro conteúdo volta intacto. */
93
93
  export const formatDocument = formatIfCpfCnpjShaped;
94
94
 
95
- /** Chave PIX: só mascara quando a chave é o próprio CPF/CNPJ; e-mail, telefone e aleatória saem como estão. */
96
- export const formatPixKey = formatIfCpfCnpjShaped;
95
+ export function formatPixKey(value: string): string {
96
+ if (/^\+55\d{11}$/.test(value)) return `+55 ${formatPhone(value.slice(3))}`;
97
+ if (/^\d{11}$/.test(value)) return formatCPF(value);
98
+ if (/^\d{14}$/.test(value)) return formatCNPJ(value);
99
+ return value;
100
+ }
97
101
 
98
102
  /** Tira a máscara antes de gravar; valor que não é CPF/CNPJ volta como veio. */
99
103
  export function normalizeCpfCnpjForStorage(value: string): string {
@@ -0,0 +1,29 @@
1
+ import { isValidCNPJ, isValidCPF, isValidEmail } from './common';
2
+
3
+ const RANDOM_KEY = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
4
+ const PIX_KEY_CHARS = /^[\d ().\/+-]+$/;
5
+ const MOBILE_WITH_COUNTRY_CODE = /^55[1-9]{2}9\d{8}$/;
6
+ const MOBILE = /^[1-9]{2}9\d{8}$/;
7
+
8
+ export const AMBIGUOUS_PIX_KEY = Symbol('ambiguous-pix-key');
9
+
10
+ export function normalizePixKeyForStorage(value: string, holderDocument: string): string | typeof AMBIGUOUS_PIX_KEY | null {
11
+ const key = value.trim();
12
+ if (isValidEmail(key) || RANDOM_KEY.test(key)) return key;
13
+ if (!PIX_KEY_CHARS.test(key)) return null;
14
+ const digits = key.replace(/\D/g, '');
15
+ if (key.startsWith('+')) return MOBILE_WITH_COUNTRY_CODE.test(digits) ? `+${digits}` : null;
16
+ if (digits.length === 14) return isValidCNPJ(digits) ? digits : null;
17
+ if (digits.length === 13) return MOBILE_WITH_COUNTRY_CODE.test(digits) ? `+${digits}` : null;
18
+ if (digits.length !== 11) return null;
19
+ if (digits === holderDocument.replace(/\D/g, '')) return digits;
20
+ const isMobile = MOBILE.test(digits);
21
+ const isCpf = isValidCPF(digits);
22
+ if (isMobile && isCpf) return AMBIGUOUS_PIX_KEY;
23
+ if (isMobile) return `+55${digits}`;
24
+ return isCpf ? digits : null;
25
+ }
26
+
27
+ export function isPixKeyFromAnotherDocument(canonicalPixKey: string, holderDocument: string): boolean {
28
+ return /^\d+$/.test(canonicalPixKey) && canonicalPixKey !== holderDocument.replace(/\D/g, '');
29
+ }