@greatapps/common 1.1.763 → 1.1.765
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 +139 -129
- package/dist/components/modals/billing/RequiredBillingDataModal.mjs.map +1 -1
- package/dist/i18n/messages/en-us.mjs +1 -0
- package/dist/i18n/messages/en-us.mjs.map +1 -1
- package/dist/i18n/messages/es-es.mjs +1 -0
- package/dist/i18n/messages/es-es.mjs.map +1 -1
- package/dist/i18n/messages/pt-br.mjs +1 -0
- package/dist/i18n/messages/pt-br.mjs.map +1 -1
- package/dist/index.mjs +4 -0
- package/dist/index.mjs.map +1 -1
- package/dist/middlewares/chain.mjs +10 -1
- package/dist/middlewares/chain.mjs.map +1 -1
- package/dist/modules/auth/utils/assert-subscription-addon.mjs +11 -0
- package/dist/modules/auth/utils/assert-subscription-addon.mjs.map +1 -1
- package/dist/modules/plans/hooks/use-has-plan-addon.hook.mjs +4 -2
- package/dist/modules/plans/hooks/use-has-plan-addon.hook.mjs.map +1 -1
- package/dist/modules/subscriptions/types/subscription.type.mjs +1 -0
- package/dist/modules/subscriptions/types/subscription.type.mjs.map +1 -1
- package/dist/modules/subscriptions/utils/get-scheduled-plan-items.mjs +12 -0
- package/dist/modules/subscriptions/utils/get-scheduled-plan-items.mjs.map +1 -0
- package/dist/modules/subscriptions/utils/is-limited-by-scheduled-downgrade.mjs +16 -0
- package/dist/modules/subscriptions/utils/is-limited-by-scheduled-downgrade.mjs.map +1 -0
- package/dist/server.mjs +2 -0
- package/dist/server.mjs.map +1 -1
- package/dist/utils/geo-country.mjs +16 -0
- package/dist/utils/geo-country.mjs.map +1 -0
- package/package.json +1 -1
- package/src/components/modals/billing/RequiredBillingDataModal.tsx +163 -144
- package/src/i18n/messages/en-us.ts +2 -0
- package/src/i18n/messages/es-es.ts +2 -0
- package/src/i18n/messages/pt-br.ts +2 -0
- package/src/index.ts +3 -0
- package/src/middlewares/chain.ts +14 -1
- package/src/modules/auth/utils/assert-subscription-addon.ts +12 -0
- package/src/modules/plans/hooks/use-has-plan-addon.hook.ts +5 -2
- package/src/modules/subscriptions/types/subscription.type.ts +1 -0
- package/src/modules/subscriptions/utils/get-scheduled-plan-items.ts +15 -0
- package/src/modules/subscriptions/utils/is-limited-by-scheduled-downgrade.ts +24 -0
- package/src/server.ts +1 -0
- package/src/utils/geo-country.ts +15 -0
|
@@ -21,6 +21,7 @@ import { useViaCep } from '../../../modules/accounts/hooks/useViaCep';
|
|
|
21
21
|
import { useUpdateBillingData } from '../../../modules/accounts/hooks/useAccountManagement';
|
|
22
22
|
import { useRequiredBillingData } from '../../../modules/accounts/hooks/use-required-billing-data.hook';
|
|
23
23
|
import { COUNTRIES } from '../../../utils/countries';
|
|
24
|
+
import { readGeoCountry } from '../../../utils/geo-country';
|
|
24
25
|
import { BR_STATE_OPTIONS } from '../../../utils/constants/br-states';
|
|
25
26
|
import { formatCNPJ, formatCPF, formatPostalCode } from '../../../utils/format/masks';
|
|
26
27
|
import { isValidCNPJ, isValidCPF } from '../../../utils/validators/common';
|
|
@@ -48,46 +49,48 @@ const COUNTRY_OPTIONS = COUNTRIES.map((country: { iso: string; name: string }) =
|
|
|
48
49
|
label: country.name,
|
|
49
50
|
}));
|
|
50
51
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
const BR_REQUIRED_ADDRESS_FIELDS: readonly (keyof BillingDataFormValues)[] = [
|
|
53
|
+
'street',
|
|
54
|
+
'streetNumber',
|
|
55
|
+
'neighborhood',
|
|
56
|
+
'city',
|
|
57
|
+
'state',
|
|
58
|
+
];
|
|
56
59
|
|
|
60
|
+
function buildBillingDataSchema(translate: TranslateFn) {
|
|
57
61
|
const base = z.object({
|
|
58
62
|
personType: z.enum(['pf', 'pj']),
|
|
59
63
|
document: z.string().trim(),
|
|
60
|
-
financialName:
|
|
64
|
+
financialName: z
|
|
65
|
+
.string()
|
|
66
|
+
.trim()
|
|
67
|
+
.min(1, translate('common.billing.requiredData.requiredField')),
|
|
61
68
|
financialEmail: z
|
|
62
69
|
.string()
|
|
63
70
|
.trim()
|
|
64
71
|
.min(1, translate('common.billing.requiredData.requiredField'))
|
|
65
72
|
.email(translate('common.billing.requiredData.invalidEmail')),
|
|
66
|
-
cep:
|
|
67
|
-
street:
|
|
73
|
+
cep: z.string().trim(),
|
|
74
|
+
street: z.string().trim(),
|
|
68
75
|
streetNumber: z.string().trim(),
|
|
69
76
|
complement: z.string().trim().optional(),
|
|
70
77
|
neighborhood: z.string().trim(),
|
|
71
|
-
city:
|
|
72
|
-
state:
|
|
73
|
-
country:
|
|
78
|
+
city: z.string().trim(),
|
|
79
|
+
state: z.string().trim(),
|
|
80
|
+
country: z.string().trim().min(1, translate('common.billing.requiredData.requiredField')),
|
|
74
81
|
});
|
|
75
82
|
|
|
76
83
|
return base.superRefine((data, ctx) => {
|
|
77
|
-
if (
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
code: 'custom',
|
|
88
|
-
path: ['neighborhood'],
|
|
89
|
-
message: translate('common.billing.requiredData.requiredField'),
|
|
90
|
-
});
|
|
84
|
+
if (data.country !== 'BR') return;
|
|
85
|
+
|
|
86
|
+
for (const field of BR_REQUIRED_ADDRESS_FIELDS) {
|
|
87
|
+
if (!data[field]) {
|
|
88
|
+
ctx.addIssue({
|
|
89
|
+
code: 'custom',
|
|
90
|
+
path: [field],
|
|
91
|
+
message: translate('common.billing.requiredData.requiredField'),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
91
94
|
}
|
|
92
95
|
|
|
93
96
|
if (data.cep.replace(/\D/g, '').length !== 8) {
|
|
@@ -180,6 +183,7 @@ function RequiredBillingDataModalContent() {
|
|
|
180
183
|
|
|
181
184
|
const personType = watch('personType');
|
|
182
185
|
const isCompany = personType === 'pj';
|
|
186
|
+
const isBrazil = watch('country') === 'BR';
|
|
183
187
|
|
|
184
188
|
useEffect(() => {
|
|
185
189
|
if (!account || isPrefilled.current) return;
|
|
@@ -198,7 +202,7 @@ function RequiredBillingDataModalContent() {
|
|
|
198
202
|
neighborhood: account.neighborhood ?? '',
|
|
199
203
|
city: account.city ?? '',
|
|
200
204
|
state: account.state ?? '',
|
|
201
|
-
country: account.country || 'BR',
|
|
205
|
+
country: account.country || readGeoCountry() || 'BR',
|
|
202
206
|
});
|
|
203
207
|
}, [account, reset, user?.email]);
|
|
204
208
|
|
|
@@ -235,20 +239,27 @@ function RequiredBillingDataModalContent() {
|
|
|
235
239
|
};
|
|
236
240
|
|
|
237
241
|
async function onSubmit(data: BillingDataFormValues) {
|
|
238
|
-
const payload: UpdateBillingDataRequest =
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
242
|
+
const payload: UpdateBillingDataRequest =
|
|
243
|
+
data.country === 'BR'
|
|
244
|
+
? {
|
|
245
|
+
financial_document_type: data.personType === 'pf' ? 1 : 2,
|
|
246
|
+
financial_document: data.document,
|
|
247
|
+
financial_name: data.financialName,
|
|
248
|
+
financial_email: data.financialEmail,
|
|
249
|
+
zipcode: data.cep,
|
|
250
|
+
address: data.street,
|
|
251
|
+
address_number: data.streetNumber,
|
|
252
|
+
address_complement: data.complement ?? '',
|
|
253
|
+
neighborhood: data.neighborhood,
|
|
254
|
+
city: data.city,
|
|
255
|
+
state: data.state,
|
|
256
|
+
country: data.country,
|
|
257
|
+
}
|
|
258
|
+
: {
|
|
259
|
+
financial_name: data.financialName,
|
|
260
|
+
financial_email: data.financialEmail,
|
|
261
|
+
country: data.country,
|
|
262
|
+
};
|
|
252
263
|
|
|
253
264
|
try {
|
|
254
265
|
await updateBillingData(payload);
|
|
@@ -296,7 +307,9 @@ function RequiredBillingDataModalContent() {
|
|
|
296
307
|
{translate('common.billing.requiredData.title')}
|
|
297
308
|
</DialogTitle>
|
|
298
309
|
<span className="paragraph-small-regular text-zinc-600">
|
|
299
|
-
{
|
|
310
|
+
{isBrazil
|
|
311
|
+
? translate('common.billing.requiredData.description')
|
|
312
|
+
: translate('common.billing.requiredData.descriptionInternational')}
|
|
300
313
|
</span>
|
|
301
314
|
</div>
|
|
302
315
|
</DialogHeader>
|
|
@@ -308,7 +321,26 @@ function RequiredBillingDataModalContent() {
|
|
|
308
321
|
>
|
|
309
322
|
<div className="flex flex-col gap-6 p-4 lg:p-5 flex-1 overflow-y-auto">
|
|
310
323
|
<div className="flex flex-col gap-5">
|
|
311
|
-
<
|
|
324
|
+
<Controller
|
|
325
|
+
name="country"
|
|
326
|
+
control={control}
|
|
327
|
+
render={({ field }) => (
|
|
328
|
+
<SelectField
|
|
329
|
+
label={translate('common.billing.requiredData.country')}
|
|
330
|
+
placeholder={translate('common.billing.requiredData.select')}
|
|
331
|
+
value={field.value || undefined}
|
|
332
|
+
onChange={(value) =>
|
|
333
|
+
setValue('country', String(value), { shouldValidate: true })
|
|
334
|
+
}
|
|
335
|
+
options={COUNTRY_OPTIONS}
|
|
336
|
+
error={!!errors.country}
|
|
337
|
+
errorMessage={errors.country?.message}
|
|
338
|
+
/>
|
|
339
|
+
)}
|
|
340
|
+
/>
|
|
341
|
+
|
|
342
|
+
{isBrazil && (
|
|
343
|
+
<div className="flex gap-4">
|
|
312
344
|
<Controller
|
|
313
345
|
name="personType"
|
|
314
346
|
control={control}
|
|
@@ -347,11 +379,12 @@ function RequiredBillingDataModalContent() {
|
|
|
347
379
|
})}
|
|
348
380
|
/>
|
|
349
381
|
</div>
|
|
350
|
-
|
|
382
|
+
</div>
|
|
383
|
+
)}
|
|
351
384
|
|
|
352
385
|
<FormField
|
|
353
386
|
label={
|
|
354
|
-
isCompany
|
|
387
|
+
isBrazil && isCompany
|
|
355
388
|
? translate('common.billing.requiredData.companyName')
|
|
356
389
|
: translate('common.billing.requiredData.fullName')
|
|
357
390
|
}
|
|
@@ -371,113 +404,99 @@ function RequiredBillingDataModalContent() {
|
|
|
371
404
|
/>
|
|
372
405
|
</div>
|
|
373
406
|
|
|
374
|
-
|
|
407
|
+
{isBrazil && (
|
|
408
|
+
<>
|
|
409
|
+
<div className="h-px bg-zinc-200" />
|
|
375
410
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
411
|
+
<div className="flex flex-col gap-5">
|
|
412
|
+
<span className="paragraph-small-semibold text-zinc-950">
|
|
413
|
+
{translate('common.billing.requiredData.addressHeading')}
|
|
414
|
+
</span>
|
|
380
415
|
|
|
381
|
-
<FormField
|
|
382
|
-
label={translate('common.billing.requiredData.cep')}
|
|
383
|
-
placeholder="_____-___"
|
|
384
|
-
error={!!errors.cep}
|
|
385
|
-
errorMessage={errors.cep?.message}
|
|
386
|
-
{...register('cep', {
|
|
387
|
-
onChange: (event) => handleCepChange(event.target.value),
|
|
388
|
-
})}
|
|
389
|
-
/>
|
|
390
|
-
|
|
391
|
-
<div className="flex gap-4">
|
|
392
|
-
<div className="flex-[3]">
|
|
393
416
|
<FormField
|
|
394
|
-
label={translate('common.billing.requiredData.
|
|
395
|
-
placeholder=
|
|
396
|
-
error={!!errors.
|
|
397
|
-
errorMessage={errors.
|
|
398
|
-
{...register('
|
|
417
|
+
label={translate('common.billing.requiredData.cep')}
|
|
418
|
+
placeholder="_____-___"
|
|
419
|
+
error={!!errors.cep}
|
|
420
|
+
errorMessage={errors.cep?.message}
|
|
421
|
+
{...register('cep', {
|
|
422
|
+
onChange: (event) => handleCepChange(event.target.value),
|
|
423
|
+
})}
|
|
399
424
|
/>
|
|
400
|
-
</div>
|
|
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>
|
|
410
|
-
</div>
|
|
411
425
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
426
|
+
<div className="flex gap-4">
|
|
427
|
+
<div className="flex-[3]">
|
|
428
|
+
<FormField
|
|
429
|
+
label={translate('common.billing.requiredData.street')}
|
|
430
|
+
placeholder={translate('common.billing.requiredData.typeHere')}
|
|
431
|
+
error={!!errors.street}
|
|
432
|
+
errorMessage={errors.street?.message}
|
|
433
|
+
{...register('street')}
|
|
434
|
+
/>
|
|
435
|
+
</div>
|
|
436
|
+
<div className="flex-1">
|
|
437
|
+
<FormField
|
|
438
|
+
label={translate('common.billing.requiredData.streetNumber')}
|
|
439
|
+
placeholder="000"
|
|
440
|
+
error={!!errors.streetNumber}
|
|
441
|
+
errorMessage={errors.streetNumber?.message}
|
|
442
|
+
{...register('streetNumber')}
|
|
443
|
+
/>
|
|
444
|
+
</div>
|
|
445
|
+
</div>
|
|
431
446
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
onChange={(value) =>
|
|
452
|
-
setValue('state', value as string, { shouldValidate: true })
|
|
453
|
-
}
|
|
454
|
-
options={BR_STATE_OPTIONS}
|
|
455
|
-
error={!!errors.state}
|
|
456
|
-
errorMessage={errors.state?.message}
|
|
457
|
-
/>
|
|
458
|
-
)}
|
|
459
|
-
/>
|
|
460
|
-
</div>
|
|
461
|
-
</div>
|
|
447
|
+
<div className="flex gap-4">
|
|
448
|
+
<div className="flex-1">
|
|
449
|
+
<FormField
|
|
450
|
+
label={translate('common.billing.requiredData.neighborhood')}
|
|
451
|
+
placeholder={translate('common.billing.requiredData.typeHere')}
|
|
452
|
+
error={!!errors.neighborhood}
|
|
453
|
+
errorMessage={errors.neighborhood?.message}
|
|
454
|
+
{...register('neighborhood')}
|
|
455
|
+
/>
|
|
456
|
+
</div>
|
|
457
|
+
<div className="flex-1">
|
|
458
|
+
<FormField
|
|
459
|
+
label={translate('common.billing.requiredData.complement')}
|
|
460
|
+
optional
|
|
461
|
+
placeholder={translate('common.billing.requiredData.typeHere')}
|
|
462
|
+
{...register('complement')}
|
|
463
|
+
/>
|
|
464
|
+
</div>
|
|
465
|
+
</div>
|
|
462
466
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
467
|
+
<div className="flex gap-4">
|
|
468
|
+
<div className="flex-1">
|
|
469
|
+
<FormField
|
|
470
|
+
label={translate('common.billing.requiredData.city')}
|
|
471
|
+
placeholder={translate('common.billing.requiredData.typeHere')}
|
|
472
|
+
error={!!errors.city}
|
|
473
|
+
errorMessage={errors.city?.message}
|
|
474
|
+
{...register('city')}
|
|
475
|
+
/>
|
|
476
|
+
</div>
|
|
477
|
+
<div className="flex-1">
|
|
478
|
+
<Controller
|
|
479
|
+
name="state"
|
|
480
|
+
control={control}
|
|
481
|
+
render={({ field }) => (
|
|
482
|
+
<SelectField
|
|
483
|
+
label={translate('common.billing.requiredData.state')}
|
|
484
|
+
placeholder={translate('common.billing.requiredData.select')}
|
|
485
|
+
value={field.value || undefined}
|
|
486
|
+
onChange={(value) =>
|
|
487
|
+
setValue('state', String(value), { shouldValidate: true })
|
|
488
|
+
}
|
|
489
|
+
options={BR_STATE_OPTIONS}
|
|
490
|
+
error={!!errors.state}
|
|
491
|
+
errorMessage={errors.state?.message}
|
|
492
|
+
/>
|
|
493
|
+
)}
|
|
494
|
+
/>
|
|
495
|
+
</div>
|
|
496
|
+
</div>
|
|
497
|
+
</div>
|
|
498
|
+
</>
|
|
499
|
+
)}
|
|
481
500
|
</div>
|
|
482
501
|
|
|
483
502
|
<div className="flex items-center justify-end p-4 lg:p-5 border-t border-zinc-200 shrink-0">
|
|
@@ -354,6 +354,8 @@ const messages = {
|
|
|
354
354
|
title: 'Complete your billing information',
|
|
355
355
|
description:
|
|
356
356
|
'We need this information to issue the invoice for your payments. Filling it in is required to keep using the platform.',
|
|
357
|
+
descriptionInternational:
|
|
358
|
+
'Since your billing address is outside Brazil, we only need your name and email for your payment receipts.',
|
|
357
359
|
addressHeading: 'Billing address',
|
|
358
360
|
select: 'Select',
|
|
359
361
|
typeHere: 'Type here',
|
|
@@ -355,6 +355,8 @@ const messages = {
|
|
|
355
355
|
title: 'Completa tus datos de facturación',
|
|
356
356
|
description:
|
|
357
357
|
'Necesitamos esta información para emitir la factura de tus pagos. Completarla es obligatorio para seguir usando la plataforma.',
|
|
358
|
+
descriptionInternational:
|
|
359
|
+
'Como tu facturación es fuera de Brasil, solo necesitamos tu nombre y correo para los recibos de tus pagos.',
|
|
358
360
|
addressHeading: 'Dirección de facturación',
|
|
359
361
|
select: 'Selecciona',
|
|
360
362
|
typeHere: 'Escribe aquí',
|
|
@@ -367,6 +367,8 @@ const messages = {
|
|
|
367
367
|
title: 'Complete seus dados de cobrança',
|
|
368
368
|
description:
|
|
369
369
|
'Precisamos dessas informações para emitir a nota fiscal dos seus pagamentos. O preenchimento é obrigatório para continuar usando a plataforma.',
|
|
370
|
+
descriptionInternational:
|
|
371
|
+
'Como sua cobrança é fora do Brasil, precisamos apenas do nome e do e-mail para os recibos dos seus pagamentos.',
|
|
370
372
|
addressHeading: 'Endereço de cobrança',
|
|
371
373
|
select: 'Selecione',
|
|
372
374
|
typeHere: 'Digite aqui',
|
package/src/index.ts
CHANGED
|
@@ -224,6 +224,9 @@ export {
|
|
|
224
224
|
export { SUBSCRIPTION_GRACE_PERIOD_DAYS } from "./modules/subscriptions/constants/subscription.constants";
|
|
225
225
|
export { getSubscriptionCancellationState } from "./modules/subscriptions/utils/get-subscription-cancellation-state";
|
|
226
226
|
export type { SubscriptionCancellationState } from "./modules/subscriptions/utils/get-subscription-cancellation-state";
|
|
227
|
+
export { getScheduledPlanItems } from "./modules/subscriptions/utils/get-scheduled-plan-items";
|
|
228
|
+
export { isLimitedByScheduledDowngrade } from "./modules/subscriptions/utils/is-limited-by-scheduled-downgrade";
|
|
229
|
+
export type { ScheduledPlanItem } from "./modules/subscriptions/utils/get-scheduled-plan-items";
|
|
227
230
|
export {
|
|
228
231
|
ACCOUNT_FREEZE_AFTER_DAYS,
|
|
229
232
|
FREEZE_WARNING_DAYS_BEFORE,
|
package/src/middlewares/chain.ts
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server';
|
|
2
2
|
|
|
3
3
|
import { MiddlewareConfig } from './types';
|
|
4
|
+
import { GEO_COUNTRY_COOKIE, GEO_UNKNOWN_COUNTRIES } from '../utils/geo-country';
|
|
5
|
+
|
|
6
|
+
function publishGeoCountry(request: NextRequest, response: NextResponse): void {
|
|
7
|
+
const country = request.headers.get('cf-ipcountry')?.toUpperCase();
|
|
8
|
+
|
|
9
|
+
if (!country || GEO_UNKNOWN_COUNTRIES.includes(country)) return;
|
|
10
|
+
if (request.cookies.get(GEO_COUNTRY_COOKIE)?.value === country) return;
|
|
11
|
+
|
|
12
|
+
response.cookies.set(GEO_COUNTRY_COOKIE, country, { path: '/', sameSite: 'lax' });
|
|
13
|
+
}
|
|
4
14
|
|
|
5
15
|
function shouldProcessPath(pathname: string, config: MiddlewareConfig): boolean {
|
|
6
16
|
const { excludePaths, includePaths } = config;
|
|
@@ -40,6 +50,9 @@ export function createMiddlewareChain(middlewares: MiddlewareConfig[]) {
|
|
|
40
50
|
}
|
|
41
51
|
}
|
|
42
52
|
|
|
43
|
-
|
|
53
|
+
const response = NextResponse.next();
|
|
54
|
+
publishGeoCountry(request, response);
|
|
55
|
+
|
|
56
|
+
return response;
|
|
44
57
|
};
|
|
45
58
|
}
|
|
@@ -3,6 +3,9 @@ import "server-only";
|
|
|
3
3
|
import { ApiError } from "../../../infra/api/types";
|
|
4
4
|
import { listSubscriptionsAction } from "../../subscriptions/actions/list-subscriptions.action";
|
|
5
5
|
import type { AddonKindEnum } from "../../subscriptions/constants/addons.constants";
|
|
6
|
+
import { getScheduledPlanItems } from "../../subscriptions/utils/get-scheduled-plan-items";
|
|
7
|
+
|
|
8
|
+
export const ADDON_SCHEDULED_FOR_REMOVAL = "ADDON_SCHEDULED_FOR_REMOVAL";
|
|
6
9
|
|
|
7
10
|
export async function assertSubscriptionAddon(
|
|
8
11
|
addonId: AddonKindEnum,
|
|
@@ -39,4 +42,13 @@ export async function assertSubscriptionAddon(
|
|
|
39
42
|
403,
|
|
40
43
|
);
|
|
41
44
|
}
|
|
45
|
+
|
|
46
|
+
const scheduledItems = getScheduledPlanItems(subscription);
|
|
47
|
+
if (scheduledItems && !scheduledItems.some((item) => item.id_addon === addonId)) {
|
|
48
|
+
throw new ApiError(
|
|
49
|
+
`O plano agendado para o próximo ciclo não inclui ${addonLabel}. Cancele o downgrade agendado para voltar a usar este recurso.`,
|
|
50
|
+
ADDON_SCHEDULED_FOR_REMOVAL,
|
|
51
|
+
403,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
42
54
|
}
|
|
@@ -2,13 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
import type { AddonKindEnum } from '../../subscriptions/constants/addons.constants';
|
|
4
4
|
import { useActiveSubscription } from '../../subscriptions/hooks/find-active-subscription.hook';
|
|
5
|
+
import { getScheduledPlanItems } from '../../subscriptions/utils/get-scheduled-plan-items';
|
|
5
6
|
|
|
6
7
|
export function useHasPlanAddon(addonId: AddonKindEnum) {
|
|
7
8
|
const { data: subscriptionData, isLoading: isLoadingSubscription } = useActiveSubscription();
|
|
8
9
|
const activeSubscription = subscriptionData?.data?.[0] ?? null;
|
|
9
10
|
|
|
10
11
|
const hasAddon = !!activeSubscription?.items?.find((i) => i.id_addon === addonId);
|
|
11
|
-
const isLoading = isLoadingSubscription;
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
const scheduledItems = getScheduledPlanItems(activeSubscription);
|
|
14
|
+
const keepsAddon = !scheduledItems || scheduledItems.some((i) => i.id_addon === addonId);
|
|
15
|
+
|
|
16
|
+
return { hasAddon: hasAddon && keepsAddon, isLoading: isLoadingSubscription };
|
|
14
17
|
}
|
|
@@ -85,6 +85,7 @@ export const SubscriptionSchema = z.object({
|
|
|
85
85
|
is_plan_base: z.boolean(),
|
|
86
86
|
is_ai_credit: z.boolean(),
|
|
87
87
|
})),
|
|
88
|
+
new_limits: z.record(z.string(), z.number()).nullable().optional(),
|
|
88
89
|
new_discounts: z.array(z.unknown()),
|
|
89
90
|
changes: z.array(z.object({
|
|
90
91
|
type: z.string(),
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Subscription, SubscriptionItem } from "../types/subscription.type";
|
|
2
|
+
|
|
3
|
+
export type ScheduledPlanItem = Pick<SubscriptionItem, "id_addon" | "quantity">;
|
|
4
|
+
|
|
5
|
+
export function getScheduledPlanItems(
|
|
6
|
+
subscription: Pick<Subscription, "pending_change"> | undefined | null,
|
|
7
|
+
): ScheduledPlanItem[] | null {
|
|
8
|
+
const newLimits = subscription?.pending_change?.new_limits;
|
|
9
|
+
if (!newLimits || Object.keys(newLimits).length === 0) return null;
|
|
10
|
+
|
|
11
|
+
return Object.entries(newLimits).map(([idAddon, quantity]) => ({
|
|
12
|
+
id_addon: Number(idAddon),
|
|
13
|
+
quantity,
|
|
14
|
+
}));
|
|
15
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Subscription } from "../types/subscription.type";
|
|
2
|
+
import { ADDON_IDS, type AddonKindEnum } from "../constants/addons.constants";
|
|
3
|
+
|
|
4
|
+
export function isLimitedByScheduledDowngrade(
|
|
5
|
+
subscription: Subscription | undefined | null,
|
|
6
|
+
addonId: AddonKindEnum,
|
|
7
|
+
): boolean {
|
|
8
|
+
const newLimits = subscription?.pending_change?.new_limits;
|
|
9
|
+
if (!newLimits || Object.keys(newLimits).length === 0) return false;
|
|
10
|
+
|
|
11
|
+
const scheduled = newLimits[String(addonId)];
|
|
12
|
+
if (scheduled == null) return true;
|
|
13
|
+
|
|
14
|
+
const current = (subscription?.items ?? [])
|
|
15
|
+
.filter((item) => item.id_addon === addonId)
|
|
16
|
+
.reduce((sum, item) => sum + (item.quantity ?? 0), 0);
|
|
17
|
+
|
|
18
|
+
if (addonId === ADDON_IDS.DOMAINS) return scheduled < current;
|
|
19
|
+
|
|
20
|
+
if (scheduled === 0) return false;
|
|
21
|
+
if (current === 0) return true;
|
|
22
|
+
|
|
23
|
+
return scheduled < current;
|
|
24
|
+
}
|
package/src/server.ts
CHANGED
|
@@ -10,6 +10,7 @@ export { revalidateWhitelabelAction } from './modules/whitelabel/actions/revalid
|
|
|
10
10
|
export { assertManagementPermission } from './modules/auth/utils/assert-management-permission';
|
|
11
11
|
export { assertManagementSubscription } from './modules/auth/utils/assert-management-subscription';
|
|
12
12
|
export { assertSubscriptionAddon } from './modules/auth/utils/assert-subscription-addon';
|
|
13
|
+
export { ADDON_SCHEDULED_FOR_REMOVAL } from './modules/auth/utils/assert-subscription-addon';
|
|
13
14
|
export { assertPaidSubscription } from './modules/auth/utils/assert-paid-subscription';
|
|
14
15
|
export { assertAccountNotFrozen } from './modules/auth/utils/assert-account-not-frozen';
|
|
15
16
|
export { subscriptionsService } from './modules/subscriptions/services/subscriptions.service';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { COUNTRIES } from './countries';
|
|
2
|
+
|
|
3
|
+
export const GEO_COUNTRY_COOKIE = 'gs-geo-country';
|
|
4
|
+
|
|
5
|
+
export const GEO_UNKNOWN_COUNTRIES = ['XX', 'T1'];
|
|
6
|
+
|
|
7
|
+
export function readGeoCountry(): string | null {
|
|
8
|
+
if (typeof document === 'undefined') return null;
|
|
9
|
+
|
|
10
|
+
const match = new RegExp(`(?:^|;\\s*)${GEO_COUNTRY_COOKIE}=([^;]+)`).exec(document.cookie);
|
|
11
|
+
if (!match) return null;
|
|
12
|
+
|
|
13
|
+
const iso = match[1].toUpperCase();
|
|
14
|
+
return COUNTRIES.some((country: { iso: string }) => country.iso === iso) ? iso : null;
|
|
15
|
+
}
|