@greatapps/common 1.1.795 → 1.1.797

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 (28) hide show
  1. package/dist/components/modals/billing/BillingDataForm.mjs +406 -0
  2. package/dist/components/modals/billing/BillingDataForm.mjs.map +1 -0
  3. package/dist/components/modals/billing/RequiredBillingDataModal.mjs +5 -405
  4. package/dist/components/modals/billing/RequiredBillingDataModal.mjs.map +1 -1
  5. package/dist/i18n/resolve-locale.mjs +1 -1
  6. package/dist/i18n/resolve-locale.mjs.map +1 -1
  7. package/dist/index.mjs +4 -0
  8. package/dist/index.mjs.map +1 -1
  9. package/dist/modules/ia-credits/types/ai-credits-offer.type.mjs +1 -0
  10. package/dist/modules/ia-credits/types/ai-credits-offer.type.mjs.map +1 -1
  11. package/dist/modules/whitelabel/actions/find-whitelabel.action.mjs +1 -5
  12. package/dist/modules/whitelabel/actions/find-whitelabel.action.mjs.map +1 -1
  13. package/dist/modules/whitelabel/constants/whitelabel.constants.mjs +9 -0
  14. package/dist/modules/whitelabel/constants/whitelabel.constants.mjs.map +1 -0
  15. package/dist/modules/whitelabel/services/whitelabel.service.mjs +85 -11
  16. package/dist/modules/whitelabel/services/whitelabel.service.mjs.map +1 -1
  17. package/dist/testing/msw/session.handlers.mjs +1 -0
  18. package/dist/testing/msw/session.handlers.mjs.map +1 -1
  19. package/package.json +10 -9
  20. package/src/components/modals/billing/BillingDataForm.tsx +489 -0
  21. package/src/components/modals/billing/RequiredBillingDataModal.tsx +3 -475
  22. package/src/i18n/resolve-locale.ts +1 -1
  23. package/src/index.ts +3 -0
  24. package/src/modules/ia-credits/types/ai-credits-offer.type.ts +1 -0
  25. package/src/modules/whitelabel/actions/find-whitelabel.action.ts +1 -7
  26. package/src/modules/whitelabel/constants/whitelabel.constants.ts +5 -0
  27. package/src/modules/whitelabel/services/whitelabel.service.ts +300 -209
  28. package/src/testing/msw/session.handlers.ts +1 -0
@@ -0,0 +1,489 @@
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 } 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
+
13
+ import { Button } from '../../ui/buttons/Button';
14
+ import { FormField } from '../../ui/form/FormField';
15
+ import { SelectField } from '../../ui/form/SelectField';
16
+ import { Toast } from '../../ui/feedback/Toast';
17
+ import { useAuth } from '../../../providers/auth.provider';
18
+ import { useViaCep } from '../../../modules/accounts/hooks/useViaCep';
19
+ import { useUpdateBillingData } from '../../../modules/accounts/hooks/useAccountManagement';
20
+ import { COUNTRIES } from '../../../utils/countries';
21
+ import { readGeoCountry } from '../../../utils/geo-country';
22
+ import { BR_STATE_OPTIONS } from '../../../utils/constants/br-states';
23
+ import { formatCNPJ, formatCPF, formatPostalCode } from '../../../utils/format/masks';
24
+ import { isValidCNPJ, isValidCPF } from '../../../utils/validators/common';
25
+ import type { Account } from '../../../modules/accounts/types';
26
+ import type { UpdateBillingDataRequest } from '../../../modules/accounts/types';
27
+
28
+ type TranslateFn = (key: any, values?: Record<string, string | number>) => string;
29
+
30
+ type BillingDataFormValues = {
31
+ personType: 'pf' | 'pj';
32
+ document: string;
33
+ financialName: string;
34
+ financialEmail: string;
35
+ cep: string;
36
+ street: string;
37
+ streetNumber: string;
38
+ complement?: string;
39
+ neighborhood: string;
40
+ city: string;
41
+ state: string;
42
+ country: string;
43
+ };
44
+
45
+ const COUNTRY_OPTIONS = COUNTRIES.map((country: { iso: string; name: string }) => ({
46
+ value: country.iso,
47
+ label: country.name,
48
+ }));
49
+
50
+ const BR_REQUIRED_ADDRESS_FIELDS: readonly (keyof BillingDataFormValues)[] = [
51
+ 'street',
52
+ 'streetNumber',
53
+ 'neighborhood',
54
+ 'city',
55
+ 'state',
56
+ ];
57
+
58
+ function buildBillingDataSchema(translate: TranslateFn) {
59
+ const base = z.object({
60
+ personType: z.enum(['pf', 'pj']),
61
+ document: z.string().trim(),
62
+ financialName: z
63
+ .string()
64
+ .trim()
65
+ .min(1, translate('common.billing.requiredData.requiredField')),
66
+ financialEmail: z
67
+ .string()
68
+ .trim()
69
+ .min(1, translate('common.billing.requiredData.requiredField'))
70
+ .email(translate('common.billing.requiredData.invalidEmail')),
71
+ cep: z.string().trim(),
72
+ street: z.string().trim(),
73
+ streetNumber: z.string().trim(),
74
+ complement: z.string().trim().optional(),
75
+ neighborhood: z.string().trim(),
76
+ city: z.string().trim(),
77
+ state: z.string().trim(),
78
+ country: z.string().trim().min(1, translate('common.billing.requiredData.requiredField')),
79
+ });
80
+
81
+ return base.superRefine((data, ctx) => {
82
+ if (data.country !== 'BR') return;
83
+
84
+ for (const field of BR_REQUIRED_ADDRESS_FIELDS) {
85
+ if (!data[field]) {
86
+ ctx.addIssue({
87
+ code: 'custom',
88
+ path: [field],
89
+ message: translate('common.billing.requiredData.requiredField'),
90
+ });
91
+ }
92
+ }
93
+
94
+ if (data.cep.replace(/\D/g, '').length !== 8) {
95
+ ctx.addIssue({
96
+ code: 'custom',
97
+ path: ['cep'],
98
+ message: translate('common.billing.requiredData.invalidCep'),
99
+ });
100
+ }
101
+
102
+ const isCompany = data.personType === 'pj';
103
+ const digits = data.document.replace(/\D/g, '');
104
+
105
+ if (digits.length !== (isCompany ? 14 : 11)) {
106
+ ctx.addIssue({
107
+ code: 'custom',
108
+ path: ['document'],
109
+ message: isCompany
110
+ ? translate('common.billing.requiredData.invalidCnpj')
111
+ : translate('common.billing.requiredData.invalidCpf'),
112
+ });
113
+ return;
114
+ }
115
+
116
+ const isValid = isCompany ? isValidCNPJ(digits) : isValidCPF(digits);
117
+ if (!isValid) {
118
+ ctx.addIssue({
119
+ code: 'custom',
120
+ path: ['document'],
121
+ message: isCompany
122
+ ? translate('common.billing.requiredData.invalidCnpjChecksum')
123
+ : translate('common.billing.requiredData.invalidCpfChecksum'),
124
+ });
125
+ }
126
+ });
127
+ }
128
+
129
+ function formatDocument(document: string | null | undefined, isCompany: boolean) {
130
+ if (!document) return '';
131
+ return isCompany ? formatCNPJ(document) : formatCPF(document);
132
+ }
133
+
134
+ export interface BillingDataFormProps {
135
+ account: Account | undefined;
136
+ onSaved: () => void;
137
+ country?: 'BR';
138
+ submitLabel?: string;
139
+ }
140
+
141
+ export function BillingDataForm({ account, onSaved, country, submitLabel }: BillingDataFormProps) {
142
+ const translate = useTranslations();
143
+ const { user } = useAuth();
144
+ const { mutateAsync: updateBillingData } = useUpdateBillingData();
145
+ const { mutateAsync: lookupCep } = useViaCep();
146
+
147
+ const isPrefilled = useRef(false);
148
+
149
+ const schema = useMemo(
150
+ () => buildBillingDataSchema(translate),
151
+ [translate],
152
+ );
153
+
154
+ const form = useForm<BillingDataFormValues>({
155
+ resolver: zodResolver(schema),
156
+ mode: 'onChange',
157
+ defaultValues: {
158
+ personType: 'pf',
159
+ document: '',
160
+ financialName: '',
161
+ financialEmail: '',
162
+ cep: '',
163
+ street: '',
164
+ streetNumber: '',
165
+ complement: '',
166
+ neighborhood: '',
167
+ city: '',
168
+ state: '',
169
+ country: country ?? 'BR',
170
+ },
171
+ });
172
+
173
+ const {
174
+ register,
175
+ control,
176
+ handleSubmit,
177
+ reset,
178
+ setValue,
179
+ watch,
180
+ formState: { errors, isSubmitting, isValid },
181
+ } = form;
182
+
183
+ const personType = watch('personType');
184
+ const isCompany = personType === 'pj';
185
+ const isBrazil = watch('country') === 'BR';
186
+
187
+ useEffect(() => {
188
+ if (!account || isPrefilled.current) return;
189
+ isPrefilled.current = true;
190
+
191
+ const accountIsCompany = account.financial_document_type === 2;
192
+ reset({
193
+ personType: accountIsCompany ? 'pj' : 'pf',
194
+ document: formatDocument(account.financial_document, accountIsCompany),
195
+ financialName: account.financial_name ?? '',
196
+ financialEmail: account.financial_email || user?.email || '',
197
+ cep: account.zipcode ?? '',
198
+ street: account.address ?? '',
199
+ streetNumber: account.address_number ?? '',
200
+ complement: account.address_complement ?? '',
201
+ neighborhood: account.neighborhood ?? '',
202
+ city: account.city ?? '',
203
+ state: account.state ?? '',
204
+ country: country ?? (account.country || readGeoCountry() || 'BR'),
205
+ });
206
+ }, [account, country, reset, user?.email]);
207
+
208
+ const handleDocumentChange = (rawValue: string) => {
209
+ setValue('document', isCompany ? formatCNPJ(rawValue) : formatCPF(rawValue), {
210
+ shouldValidate: true,
211
+ });
212
+ };
213
+
214
+ const handlePersonTypeChange = (value: 'pf' | 'pj') => {
215
+ if (value === personType) return;
216
+ setValue('document', '');
217
+ setValue('personType', value, { shouldValidate: true });
218
+ };
219
+
220
+ const handleCepChange = (rawValue: string) => {
221
+ setValue('cep', formatPostalCode(rawValue), { shouldValidate: true });
222
+
223
+ const digits = rawValue.replace(/\D/g, '');
224
+ if (digits.length !== 8) return;
225
+
226
+ lookupCep(digits)
227
+ .then((data) => {
228
+ setValue('street', data.logradouro, { shouldValidate: true });
229
+ setValue('neighborhood', data.bairro, { shouldValidate: true });
230
+ setValue('city', data.localidade, { shouldValidate: true });
231
+ setValue('state', data.uf, { shouldValidate: true });
232
+ })
233
+ .catch(() => {});
234
+ };
235
+
236
+ async function onSubmit(data: BillingDataFormValues) {
237
+ const payload: UpdateBillingDataRequest =
238
+ data.country === 'BR'
239
+ ? {
240
+ financial_document_type: data.personType === 'pf' ? 1 : 2,
241
+ financial_document: data.document,
242
+ financial_name: data.financialName,
243
+ financial_email: data.financialEmail,
244
+ zipcode: data.cep,
245
+ address: data.street,
246
+ address_number: data.streetNumber,
247
+ address_complement: data.complement ?? '',
248
+ neighborhood: data.neighborhood,
249
+ city: data.city,
250
+ state: data.state,
251
+ country: data.country,
252
+ }
253
+ : {
254
+ financial_name: data.financialName,
255
+ financial_email: data.financialEmail,
256
+ country: data.country,
257
+ };
258
+
259
+ try {
260
+ await updateBillingData(payload);
261
+ toast.custom(
262
+ (t) => (
263
+ <Toast
264
+ variant="success"
265
+ message={translate('common.billing.requiredData.successToast')}
266
+ toastId={t}
267
+ />
268
+ ),
269
+ { duration: 5000 },
270
+ );
271
+ onSaved();
272
+ } catch (error) {
273
+ const message =
274
+ error instanceof Error && error.message
275
+ ? error.message
276
+ : translate('common.billing.requiredData.errorToast');
277
+ toast.custom((t) => <Toast variant="error" message={message} toastId={t} />, {
278
+ duration: 5000,
279
+ });
280
+ }
281
+ }
282
+
283
+ return (
284
+ <form onSubmit={handleSubmit(onSubmit)} className="flex flex-col flex-1 min-h-0" noValidate>
285
+ <div className="flex flex-col gap-6 p-4 lg:p-5 flex-1 overflow-y-auto">
286
+ <div className="flex flex-col gap-5">
287
+ {!country && (
288
+ <>
289
+ <span className="paragraph-small-regular text-zinc-600">
290
+ {isBrazil
291
+ ? translate('common.billing.requiredData.description')
292
+ : translate('common.billing.requiredData.descriptionInternational')}
293
+ </span>
294
+
295
+ <Controller
296
+ name="country"
297
+ control={control}
298
+ render={({ field }) => (
299
+ <SelectField
300
+ label={translate('common.billing.requiredData.country')}
301
+ placeholder={translate('common.billing.requiredData.select')}
302
+ value={field.value || undefined}
303
+ onChange={(value) =>
304
+ setValue('country', String(value), { shouldValidate: true })
305
+ }
306
+ options={COUNTRY_OPTIONS}
307
+ error={!!errors.country}
308
+ errorMessage={errors.country?.message}
309
+ />
310
+ )}
311
+ />
312
+ </>
313
+ )}
314
+
315
+ {isBrazil && (
316
+ <div className="flex gap-4">
317
+ <Controller
318
+ name="personType"
319
+ control={control}
320
+ render={({ field }) => (
321
+ <SelectField
322
+ label={translate('common.billing.requiredData.personType')}
323
+ placeholder={translate('common.billing.requiredData.select')}
324
+ value={field.value}
325
+ onChange={(value) => handlePersonTypeChange(value as 'pf' | 'pj')}
326
+ options={[
327
+ {
328
+ value: 'pf',
329
+ label: translate('common.billing.requiredData.personTypePf'),
330
+ },
331
+ {
332
+ value: 'pj',
333
+ label: translate('common.billing.requiredData.personTypePj'),
334
+ },
335
+ ]}
336
+ containerClassName="flex-1"
337
+ />
338
+ )}
339
+ />
340
+ <div className="flex-1">
341
+ <FormField
342
+ label={
343
+ isCompany
344
+ ? translate('common.billing.requiredData.cnpj')
345
+ : translate('common.billing.requiredData.cpf')
346
+ }
347
+ placeholder={isCompany ? '__.___.___/____-__' : '___.___.___-__'}
348
+ error={!!errors.document}
349
+ errorMessage={errors.document?.message}
350
+ {...register('document', {
351
+ onChange: (event) => handleDocumentChange(event.target.value),
352
+ })}
353
+ />
354
+ </div>
355
+ </div>
356
+ )}
357
+
358
+ <FormField
359
+ label={
360
+ isBrazil && isCompany
361
+ ? translate('common.billing.requiredData.companyName')
362
+ : translate('common.billing.requiredData.fullName')
363
+ }
364
+ placeholder={translate('common.billing.requiredData.typeHere')}
365
+ error={!!errors.financialName}
366
+ errorMessage={errors.financialName?.message}
367
+ {...register('financialName')}
368
+ />
369
+
370
+ <FormField
371
+ label={translate('common.billing.requiredData.financialEmail')}
372
+ placeholder={translate('common.billing.requiredData.financialEmailPlaceholder')}
373
+ type="email"
374
+ error={!!errors.financialEmail}
375
+ errorMessage={errors.financialEmail?.message}
376
+ {...register('financialEmail')}
377
+ />
378
+ </div>
379
+
380
+ {isBrazil && (
381
+ <>
382
+ <div className="h-px bg-zinc-200" />
383
+
384
+ <div className="flex flex-col gap-5">
385
+ <span className="paragraph-small-semibold text-zinc-950">
386
+ {translate('common.billing.requiredData.addressHeading')}
387
+ </span>
388
+
389
+ <FormField
390
+ label={translate('common.billing.requiredData.cep')}
391
+ placeholder="_____-___"
392
+ error={!!errors.cep}
393
+ errorMessage={errors.cep?.message}
394
+ {...register('cep', {
395
+ onChange: (event) => handleCepChange(event.target.value),
396
+ })}
397
+ />
398
+
399
+ <div className="flex gap-4">
400
+ <div className="flex-[3]">
401
+ <FormField
402
+ label={translate('common.billing.requiredData.street')}
403
+ placeholder={translate('common.billing.requiredData.typeHere')}
404
+ error={!!errors.street}
405
+ errorMessage={errors.street?.message}
406
+ {...register('street')}
407
+ />
408
+ </div>
409
+ <div className="flex-1">
410
+ <FormField
411
+ label={translate('common.billing.requiredData.streetNumber')}
412
+ placeholder="000"
413
+ error={!!errors.streetNumber}
414
+ errorMessage={errors.streetNumber?.message}
415
+ {...register('streetNumber')}
416
+ />
417
+ </div>
418
+ </div>
419
+
420
+ <div className="flex gap-4">
421
+ <div className="flex-1">
422
+ <FormField
423
+ label={translate('common.billing.requiredData.neighborhood')}
424
+ placeholder={translate('common.billing.requiredData.typeHere')}
425
+ error={!!errors.neighborhood}
426
+ errorMessage={errors.neighborhood?.message}
427
+ {...register('neighborhood')}
428
+ />
429
+ </div>
430
+ <div className="flex-1">
431
+ <FormField
432
+ label={translate('common.billing.requiredData.complement')}
433
+ optional
434
+ placeholder={translate('common.billing.requiredData.typeHere')}
435
+ {...register('complement')}
436
+ />
437
+ </div>
438
+ </div>
439
+
440
+ <div className="flex gap-4">
441
+ <div className="flex-1">
442
+ <FormField
443
+ label={translate('common.billing.requiredData.city')}
444
+ placeholder={translate('common.billing.requiredData.typeHere')}
445
+ error={!!errors.city}
446
+ errorMessage={errors.city?.message}
447
+ {...register('city')}
448
+ />
449
+ </div>
450
+ <div className="flex-1">
451
+ <Controller
452
+ name="state"
453
+ control={control}
454
+ render={({ field }) => (
455
+ <SelectField
456
+ label={translate('common.billing.requiredData.state')}
457
+ placeholder={translate('common.billing.requiredData.select')}
458
+ value={field.value || undefined}
459
+ onChange={(value) =>
460
+ setValue('state', String(value), { shouldValidate: true })
461
+ }
462
+ options={BR_STATE_OPTIONS}
463
+ error={!!errors.state}
464
+ errorMessage={errors.state?.message}
465
+ />
466
+ )}
467
+ />
468
+ </div>
469
+ </div>
470
+ </div>
471
+ </>
472
+ )}
473
+ </div>
474
+
475
+ <div className="flex items-center justify-end p-4 lg:p-5 border-t border-zinc-200 shrink-0">
476
+ <Button
477
+ type="submit"
478
+ className="h-10! w-full lg:w-fit"
479
+ disabled={!isValid || isSubmitting}
480
+ loading={isSubmitting}
481
+ >
482
+ {isSubmitting
483
+ ? translate('common.billing.requiredData.submitting')
484
+ : submitLabel ?? translate('common.billing.requiredData.submit')}
485
+ </Button>
486
+ </div>
487
+ </form>
488
+ );
489
+ }