@greatapps/common 1.1.741 → 1.1.742
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/BuyCreditsModal.mjs +42 -30
- package/dist/components/modals/BuyCreditsModal.mjs.map +1 -1
- package/dist/components/modals/cards/AddCardModal.mjs +2 -1
- package/dist/components/modals/cards/AddCardModal.mjs.map +1 -1
- package/dist/index.mjs +25 -9
- package/dist/index.mjs.map +1 -1
- package/dist/modules/accounts/hooks/use-currency-formatter.hook.mjs +3 -1
- package/dist/modules/accounts/hooks/use-currency-formatter.hook.mjs.map +1 -1
- package/dist/modules/accounts/hooks/use-required-billing-data.hook.mjs +3 -2
- package/dist/modules/accounts/hooks/use-required-billing-data.hook.mjs.map +1 -1
- package/dist/modules/accounts/utils/billing-data.mjs +3 -8
- package/dist/modules/accounts/utils/billing-data.mjs.map +1 -1
- package/dist/modules/plans/types/plan.type.mjs +15 -0
- package/dist/modules/plans/types/plan.type.mjs.map +1 -1
- package/dist/modules/plans/utils/map-api-plan-to-ui.mjs +10 -2
- package/dist/modules/plans/utils/map-api-plan-to-ui.mjs.map +1 -1
- package/dist/modules/subscriptions/constants/addons.constants.mjs +15 -2
- package/dist/modules/subscriptions/constants/addons.constants.mjs.map +1 -1
- package/dist/utils/format/currency.mjs +4 -1
- package/dist/utils/format/currency.mjs.map +1 -1
- package/dist/utils/format/gateway.mjs +18 -0
- package/dist/utils/format/gateway.mjs.map +1 -0
- package/package.json +1 -1
- package/src/components/modals/BuyCreditsModal.tsx +48 -32
- package/src/components/modals/cards/AddCardModal.tsx +2 -1
- package/src/index.ts +14 -5
- package/src/modules/accounts/hooks/use-currency-formatter.hook.tsx +3 -0
- package/src/modules/accounts/hooks/use-required-billing-data.hook.ts +3 -2
- package/src/modules/accounts/utils/billing-data.ts +2 -13
- package/src/modules/plans/types/plan.type.ts +17 -0
- package/src/modules/plans/utils/map-api-plan-to-ui.ts +12 -4
- package/src/modules/subscriptions/constants/addons.constants.ts +41 -28
- package/src/utils/format/currency.ts +5 -29
- package/src/utils/format/gateway.ts +22 -0
|
@@ -12,14 +12,16 @@ import { Toast } from '../ui/feedback/Toast';
|
|
|
12
12
|
import { SelectField } from '../ui/form/SelectField';
|
|
13
13
|
import { Skeleton } from '../ui/feedback/Skeleton';
|
|
14
14
|
import { PaymentInfoCard } from '../ui/data-display/PaymentInfoCard';
|
|
15
|
-
import { ADDON_IDS } from '../../modules/subscriptions/constants/addons.constants';
|
|
16
15
|
import {
|
|
17
|
-
|
|
16
|
+
ADDON_IDS,
|
|
18
17
|
AI_CREDIT_VALUES,
|
|
18
|
+
buildAiCreditOptions,
|
|
19
19
|
} from '../../modules/subscriptions/constants/addons.constants';
|
|
20
20
|
import { useBuyCreditsModal } from '../../store/useBuyCreditsModal';
|
|
21
21
|
import { useModalManager } from '../../store/useModalManager';
|
|
22
22
|
import { useCards } from '../../modules/cards/hooks/cards.hook';
|
|
23
|
+
import { useCurrencyFormatter } from '../../modules/accounts/hooks/use-currency-formatter.hook';
|
|
24
|
+
import { usePlanById } from '../../modules/plans/hooks/use-plan-by-id.hook';
|
|
23
25
|
import { useActiveSubscription } from '../../modules/subscriptions/hooks/find-active-subscription.hook';
|
|
24
26
|
import { useCalculateSubscription } from '../../modules/subscriptions/hooks/calculate-subscription.hook';
|
|
25
27
|
import { useUpdateSubscriptionPlan } from '../../modules/subscriptions/hooks/update-subscription-plan.hook';
|
|
@@ -49,6 +51,7 @@ export default function BuyCreditsModal() {
|
|
|
49
51
|
const isDefaultWl = useIsDefaultWhitelabel();
|
|
50
52
|
const { isExternalContracting, redirectToExternal } = useExternalContracting();
|
|
51
53
|
const { data: cardsData } = useCards();
|
|
54
|
+
const { locale, formatCurrencyNumber } = useCurrencyFormatter();
|
|
52
55
|
const { data: subscriptionData } = useActiveSubscription();
|
|
53
56
|
const subscription = subscriptionData?.data?.[0] ?? null;
|
|
54
57
|
const defaultCardId =
|
|
@@ -75,9 +78,20 @@ export default function BuyCreditsModal() {
|
|
|
75
78
|
const subscriptionId = subscription?.id;
|
|
76
79
|
const planId = subscription?.id_plan;
|
|
77
80
|
|
|
81
|
+
const { data: planData } = usePlanById(planId);
|
|
82
|
+
const perCreditPrice = useMemo(() => {
|
|
83
|
+
const planAiItem = planData?.data?.items?.find(
|
|
84
|
+
(item) => item.id_addon === ADDON_IDS.AI && (item.value ?? 0) > 0
|
|
85
|
+
);
|
|
86
|
+
return planAiItem?.value ?? aiItem?.value ?? 0;
|
|
87
|
+
}, [planData, aiItem]);
|
|
88
|
+
|
|
89
|
+
const hasPerCreditPrice = perCreditPrice > 0;
|
|
90
|
+
|
|
91
|
+
const aiCreditOptions = useMemo(() => buildAiCreditOptions(perCreditPrice), [perCreditPrice]);
|
|
92
|
+
|
|
78
93
|
const buildPlanExtras = useCallback(
|
|
79
94
|
(aiQuantity: number) => {
|
|
80
|
-
const aiOption = AI_CREDIT_OPTIONS.find(opt => opt.credits === aiQuantity);
|
|
81
95
|
return [
|
|
82
96
|
...(subscription?.items ?? [])
|
|
83
97
|
.filter((item) => item.id_addon != null && item.id_addon !== ADDON_IDS.AI)
|
|
@@ -87,10 +101,9 @@ export default function BuyCreditsModal() {
|
|
|
87
101
|
value: Number(item.value ?? 0),
|
|
88
102
|
price: item.price,
|
|
89
103
|
})),
|
|
90
|
-
{
|
|
91
|
-
id_addon: ADDON_IDS.AI,
|
|
104
|
+
{
|
|
105
|
+
id_addon: ADDON_IDS.AI,
|
|
92
106
|
quantity: aiQuantity,
|
|
93
|
-
value: aiOption?.price ?? 0
|
|
94
107
|
},
|
|
95
108
|
];
|
|
96
109
|
},
|
|
@@ -161,14 +174,13 @@ export default function BuyCreditsModal() {
|
|
|
161
174
|
shouldDirty: true,
|
|
162
175
|
});
|
|
163
176
|
|
|
164
|
-
|
|
165
|
-
if (!option) return;
|
|
177
|
+
if (!AI_CREDIT_VALUES.has(credits)) return;
|
|
166
178
|
|
|
167
179
|
calculate({
|
|
168
180
|
id_product: 1,
|
|
169
181
|
...(planId != null && { id_plan: planId }),
|
|
170
182
|
...(subscriptionId != null && { id_subscription: subscriptionId }),
|
|
171
|
-
plan_extras: buildPlanExtras(
|
|
183
|
+
plan_extras: buildPlanExtras(credits),
|
|
172
184
|
});
|
|
173
185
|
},
|
|
174
186
|
[form, subscriptionId, planId, calculate, buildPlanExtras]
|
|
@@ -186,14 +198,8 @@ export default function BuyCreditsModal() {
|
|
|
186
198
|
return getPriceFromCalculatedData(calculatedData, subscription?.periodicity);
|
|
187
199
|
}, [calculatedData, subscription]);
|
|
188
200
|
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
style: 'currency',
|
|
192
|
-
currency: 'BRL',
|
|
193
|
-
});
|
|
194
|
-
}, []);
|
|
195
|
-
|
|
196
|
-
const isConfirmDisabled = !isDirty || !isValid || isCalculatePending || isSubmitting;
|
|
201
|
+
const isConfirmDisabled =
|
|
202
|
+
!isDirty || !isValid || isCalculatePending || isSubmitting || !hasPerCreditPrice;
|
|
197
203
|
|
|
198
204
|
const handleClose = useCallback(() => {
|
|
199
205
|
form.reset();
|
|
@@ -205,45 +211,55 @@ export default function BuyCreditsModal() {
|
|
|
205
211
|
}, [openModal]);
|
|
206
212
|
|
|
207
213
|
const selectOptions = useMemo(() => {
|
|
208
|
-
return
|
|
214
|
+
return aiCreditOptions.map((opt) => ({
|
|
209
215
|
value: opt.credits,
|
|
210
216
|
label: (
|
|
211
217
|
<div className="flex items-center justify-between gap-2 w-full">
|
|
212
218
|
<span className="paragraph-small-medium text-zinc-950 flex-1">
|
|
213
|
-
{translate('common.credits.buy.creditsOptionLabel', { credits: opt.credits.toLocaleString(
|
|
219
|
+
{translate('common.credits.buy.creditsOptionLabel', { credits: opt.credits.toLocaleString(locale) })}
|
|
214
220
|
</span>
|
|
215
|
-
|
|
221
|
+
{hasPerCreditPrice && (
|
|
222
|
+
<span className="paragraph-small-medium text-zinc-500 opacity-70">{formatCurrencyNumber(opt.price)}</span>
|
|
223
|
+
)}
|
|
216
224
|
</div>
|
|
217
225
|
),
|
|
218
|
-
displayValue: translate('common.credits.buy.creditsOptionDisplayValue', { credits: opt.credits.toLocaleString(
|
|
226
|
+
displayValue: translate('common.credits.buy.creditsOptionDisplayValue', { credits: opt.credits.toLocaleString(locale) }),
|
|
219
227
|
}));
|
|
220
|
-
}, [translate]);
|
|
228
|
+
}, [aiCreditOptions, translate, locale, formatCurrencyNumber, hasPerCreditPrice]);
|
|
221
229
|
|
|
222
230
|
const onSubmit = useCallback(
|
|
223
231
|
async (data: BuyCreditFormData) => {
|
|
224
232
|
if (!subscriptionId) return;
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
if (!option) return;
|
|
233
|
+
if (!hasPerCreditPrice) return;
|
|
234
|
+
if (!AI_CREDIT_VALUES.has(data.creditOption)) return;
|
|
228
235
|
|
|
229
236
|
await updatePlanMutation.mutateAsync({
|
|
230
237
|
subscriptionId,
|
|
231
238
|
data: {
|
|
232
|
-
plan_extras: buildPlanExtras(
|
|
239
|
+
plan_extras: buildPlanExtras(data.creditOption),
|
|
233
240
|
},
|
|
234
241
|
});
|
|
235
242
|
|
|
236
243
|
toast.custom((t) => (
|
|
237
244
|
<Toast
|
|
238
245
|
variant="success"
|
|
239
|
-
message={translate('common.credits.buy.creditsChangedToast', { credits: data.creditOption.toLocaleString(
|
|
246
|
+
message={translate('common.credits.buy.creditsChangedToast', { credits: data.creditOption.toLocaleString(locale) })}
|
|
240
247
|
toastId={t}
|
|
241
248
|
/>
|
|
242
249
|
));
|
|
243
250
|
form.reset();
|
|
244
251
|
closeModal();
|
|
245
252
|
},
|
|
246
|
-
[
|
|
253
|
+
[
|
|
254
|
+
subscriptionId,
|
|
255
|
+
hasPerCreditPrice,
|
|
256
|
+
updatePlanMutation,
|
|
257
|
+
form,
|
|
258
|
+
closeModal,
|
|
259
|
+
buildPlanExtras,
|
|
260
|
+
translate,
|
|
261
|
+
locale,
|
|
262
|
+
]
|
|
247
263
|
);
|
|
248
264
|
|
|
249
265
|
const renderPriceDisplay = useCallback(() => {
|
|
@@ -253,12 +269,12 @@ export default function BuyCreditsModal() {
|
|
|
253
269
|
if (totalPrice !== null) {
|
|
254
270
|
return (
|
|
255
271
|
<span className="paragraph-xlarge-semibold text-zinc-950">
|
|
256
|
-
{totalPrice === 0 ? translate('common.credits.buy.defaultPrice') :
|
|
272
|
+
{totalPrice === 0 ? translate('common.credits.buy.defaultPrice') : formatCurrencyNumber(totalPrice)}
|
|
257
273
|
</span>
|
|
258
274
|
);
|
|
259
275
|
}
|
|
260
276
|
return <span className="paragraph-xlarge-semibold text-zinc-400">--</span>;
|
|
261
|
-
}, [isCalculatePending, totalPrice,
|
|
277
|
+
}, [isCalculatePending, totalPrice, formatCurrencyNumber, translate]);
|
|
262
278
|
|
|
263
279
|
if (isExternalContracting) {
|
|
264
280
|
return (
|
|
@@ -355,7 +371,7 @@ export default function BuyCreditsModal() {
|
|
|
355
371
|
<Skeleton className="h-5 w-20" />
|
|
356
372
|
) : (
|
|
357
373
|
<span className="paragraph-small-semibold text-zinc-950">
|
|
358
|
-
{
|
|
374
|
+
{formatCurrencyNumber(immediateChargeTotal)}
|
|
359
375
|
</span>
|
|
360
376
|
)}
|
|
361
377
|
</div>
|
|
@@ -385,7 +401,7 @@ export default function BuyCreditsModal() {
|
|
|
385
401
|
<Skeleton className="h-5 w-20" />
|
|
386
402
|
) : (
|
|
387
403
|
<span className="paragraph-small-semibold text-zinc-950">
|
|
388
|
-
{
|
|
404
|
+
{formatCurrencyNumber(immediateChargeTotal)}
|
|
389
405
|
</span>
|
|
390
406
|
)}
|
|
391
407
|
</div>
|
|
@@ -9,6 +9,7 @@ import { useCreateCard } from '../../../modules/cards/hooks/create-card.hook';
|
|
|
9
9
|
import { useCreateSetupIntent } from '../../../modules/cards/hooks/create-setup-intent.hook';
|
|
10
10
|
import { useCurrentAccount } from '../../../modules/accounts/hooks/current-account.hook';
|
|
11
11
|
import { isValidTaxId } from '../../../utils/validators/common';
|
|
12
|
+
import { isUsdAccount } from '../../../utils/format/gateway';
|
|
12
13
|
import { useExternalContracting } from '../../../providers/whitelabel.provider';
|
|
13
14
|
import { CardFormFields, BillingFormFields, buildCardFormSchema } from './CardFormFields';
|
|
14
15
|
import type { CardFormData, StripeElementsStatus } from './CardFormFields';
|
|
@@ -79,7 +80,7 @@ export default function AddCardModal() {
|
|
|
79
80
|
const isOpen = activeModal === 'addCardModal';
|
|
80
81
|
const { isExternalContracting, redirectToExternal } = useExternalContracting();
|
|
81
82
|
const { data: account } = useCurrentAccount();
|
|
82
|
-
const isInternational = account
|
|
83
|
+
const isInternational = isUsdAccount(account);
|
|
83
84
|
const [currentStep, setCurrentStep] = useState(1);
|
|
84
85
|
const [stripeStatus, setStripeStatus] = useState<StripeElementsStatus>(INITIAL_STRIPE_STATUS);
|
|
85
86
|
const [stripeErrors, setStripeErrors] = useState<Record<string, string | undefined>>({});
|
package/src/index.ts
CHANGED
|
@@ -123,7 +123,10 @@ export {
|
|
|
123
123
|
export {
|
|
124
124
|
ADDON_IDS,
|
|
125
125
|
AI_CREDIT_OPTIONS,
|
|
126
|
+
AI_CREDIT_SIZES,
|
|
126
127
|
AI_CREDIT_VALUES,
|
|
128
|
+
buildAiCreditOptions,
|
|
129
|
+
resolveAiCreditPrice,
|
|
127
130
|
} from "./modules/subscriptions/constants/addons.constants";
|
|
128
131
|
export type {
|
|
129
132
|
AddonKindEnum,
|
|
@@ -487,8 +490,18 @@ export {
|
|
|
487
490
|
formatCurrencyNumber,
|
|
488
491
|
parseCurrencyToNumber,
|
|
489
492
|
getCurrencyForGateway,
|
|
493
|
+
getLocaleForCurrency,
|
|
490
494
|
} from "./utils/format/currency";
|
|
491
495
|
export type { CurrencyCode } from "./utils/format/currency";
|
|
496
|
+
export {
|
|
497
|
+
USD_GATEWAY,
|
|
498
|
+
isUsdGateway,
|
|
499
|
+
isUsdAccount,
|
|
500
|
+
allowedPaymentMethods,
|
|
501
|
+
isPaymentMethodAllowed,
|
|
502
|
+
canBuyStandaloneAiCredits,
|
|
503
|
+
} from "./utils/format/gateway";
|
|
504
|
+
export type { AccountGatewayLike, PaymentMethodKind } from "./utils/format/gateway";
|
|
492
505
|
export { isValidCPF, isValidCNPJ, isValidTaxId } from "./utils/validators/common";
|
|
493
506
|
export { BR_STATE_OPTIONS } from "./utils/constants/br-states";
|
|
494
507
|
export type { TimezoneOption } from "./modules/accounts/services/timezone.service";
|
|
@@ -526,11 +539,7 @@ export {
|
|
|
526
539
|
export { useViaCep } from "./modules/accounts/hooks/useViaCep";
|
|
527
540
|
export { useAccountToken } from "./modules/accounts/hooks/use-account-token.hook";
|
|
528
541
|
export { useRequiredBillingData } from "./modules/accounts/hooks/use-required-billing-data.hook";
|
|
529
|
-
export {
|
|
530
|
-
hasCompleteBillingData,
|
|
531
|
-
isInternationalAccount,
|
|
532
|
-
INTERNATIONAL_GATEWAY,
|
|
533
|
-
} from "./modules/accounts/utils/billing-data";
|
|
542
|
+
export { hasCompleteBillingData } from "./modules/accounts/utils/billing-data";
|
|
534
543
|
export type { BillingDataSnapshot } from "./modules/accounts/utils/billing-data";
|
|
535
544
|
|
|
536
545
|
// Account Types
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
formatCurrency as formatCurrencyBase,
|
|
6
6
|
formatCurrencyNumber as formatCurrencyNumberBase,
|
|
7
7
|
getCurrencyForGateway,
|
|
8
|
+
getLocaleForCurrency,
|
|
8
9
|
type CurrencyCode,
|
|
9
10
|
} from '../../../utils/format/currency';
|
|
10
11
|
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
*/
|
|
19
20
|
export function useCurrencyFormatter(): {
|
|
20
21
|
currency: CurrencyCode;
|
|
22
|
+
locale: string;
|
|
21
23
|
formatCurrency: (value: string | number) => string;
|
|
22
24
|
formatCurrencyNumber: (value?: number | null, decimals?: number) => string;
|
|
23
25
|
} {
|
|
@@ -26,6 +28,7 @@ export function useCurrencyFormatter(): {
|
|
|
26
28
|
|
|
27
29
|
return {
|
|
28
30
|
currency,
|
|
31
|
+
locale: getLocaleForCurrency(currency),
|
|
29
32
|
formatCurrency: (value) => formatCurrencyBase(value, currency),
|
|
30
33
|
formatCurrencyNumber: (value, decimals = 2) => formatCurrencyNumberBase(value, decimals, currency),
|
|
31
34
|
};
|
|
@@ -4,7 +4,8 @@ import { useCurrentAccount } from './current-account.hook';
|
|
|
4
4
|
import { useActiveSubscription } from '../../subscriptions/hooks/find-active-subscription.hook';
|
|
5
5
|
import { hasPaidSubscription } from '../../subscriptions/utils/has-paid-subscription';
|
|
6
6
|
import { useManagementPermissions } from '../../auth/hooks/useManagementPermissions';
|
|
7
|
-
import { hasCompleteBillingData
|
|
7
|
+
import { hasCompleteBillingData } from '../utils/billing-data';
|
|
8
|
+
import { isUsdAccount } from '../../../utils/format/gateway';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Contas que já pagaram mas nunca preencheram o cadastro de cobrança ficam sem
|
|
@@ -33,6 +34,6 @@ export function useRequiredBillingData() {
|
|
|
33
34
|
isRequired,
|
|
34
35
|
isLoading,
|
|
35
36
|
account,
|
|
36
|
-
isInternational:
|
|
37
|
+
isInternational: isUsdAccount(account),
|
|
37
38
|
};
|
|
38
39
|
}
|
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
import type { Account } from '../types';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Gateway das contas cobradas em USD. Elas não têm CPF/CNPJ, bairro nem número —
|
|
5
|
-
* mesmo critério usado no AddCardModal (`account.gateway === 'stripe_usd'`).
|
|
6
|
-
*/
|
|
7
|
-
export const INTERNATIONAL_GATEWAY = 'stripe_usd';
|
|
2
|
+
import { isUsdAccount } from '../../../utils/format/gateway';
|
|
8
3
|
|
|
9
4
|
export type BillingDataSnapshot = Pick<
|
|
10
5
|
Account,
|
|
@@ -49,12 +44,6 @@ const BR_BILLING_FIELDS: readonly BillingField[] = [
|
|
|
49
44
|
'neighborhood',
|
|
50
45
|
];
|
|
51
46
|
|
|
52
|
-
export function isInternationalAccount(
|
|
53
|
-
account: Pick<BillingDataSnapshot, 'gateway'> | null | undefined,
|
|
54
|
-
): boolean {
|
|
55
|
-
return account?.gateway === INTERNATIONAL_GATEWAY;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
47
|
function isFilled(value: string | null | undefined): boolean {
|
|
59
48
|
return typeof value === 'string' && value.trim().length > 0;
|
|
60
49
|
}
|
|
@@ -69,7 +58,7 @@ export function hasCompleteBillingData(
|
|
|
69
58
|
): boolean {
|
|
70
59
|
if (!account) return false;
|
|
71
60
|
|
|
72
|
-
const fields =
|
|
61
|
+
const fields = isUsdAccount(account)
|
|
73
62
|
? INTERNATIONAL_BILLING_FIELDS
|
|
74
63
|
: BR_BILLING_FIELDS;
|
|
75
64
|
|
|
@@ -14,6 +14,22 @@ export const PlanItemSchema = z.object({
|
|
|
14
14
|
|
|
15
15
|
export type PlanItem = z.infer<typeof PlanItemSchema>;
|
|
16
16
|
|
|
17
|
+
export const PlanPeriodPricingSchema = z.object({
|
|
18
|
+
charged: z.number(),
|
|
19
|
+
list: z.number(),
|
|
20
|
+
discount: z.number(),
|
|
21
|
+
discount_percent: z.number(),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export const PlanApiPricingSchema = z.object({
|
|
25
|
+
currency: z.enum(['brl', 'usd']),
|
|
26
|
+
monthly: PlanPeriodPricingSchema,
|
|
27
|
+
semester: PlanPeriodPricingSchema,
|
|
28
|
+
annual: PlanPeriodPricingSchema,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export type PlanApiPricing = z.infer<typeof PlanApiPricingSchema>;
|
|
32
|
+
|
|
17
33
|
export const PlanSchema = z.object({
|
|
18
34
|
id: z.number(),
|
|
19
35
|
id_plan: z.number().optional(),
|
|
@@ -28,6 +44,7 @@ export const PlanSchema = z.object({
|
|
|
28
44
|
type: z.string().optional(),
|
|
29
45
|
discount_semester: z.number().default(0),
|
|
30
46
|
discount_annual: z.number().default(0),
|
|
47
|
+
pricing: PlanApiPricingSchema.optional(),
|
|
31
48
|
items: z.array(PlanItemSchema).optional().default([]),
|
|
32
49
|
});
|
|
33
50
|
|
|
@@ -83,14 +83,22 @@ function buildMainFeaturesFromItems(
|
|
|
83
83
|
return { pages, domains, users };
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
function computePricingByPeriod(apiPlan: Plan
|
|
87
|
-
// Backend agora resolve value por moeda da conta; mapper apenas lê.
|
|
88
|
-
// value pode ser null (ex.: conta USD, preço USD não cadastrado).
|
|
86
|
+
function computePricingByPeriod(apiPlan: Plan): PlanPricingByPeriod {
|
|
89
87
|
const monthly = apiPlan.value;
|
|
90
88
|
if (monthly == null) {
|
|
91
89
|
return { monthly: 0, semiannual: 0, annual: 0 };
|
|
92
90
|
}
|
|
93
91
|
const round2 = (n: number) => Math.round(n * 100) / 100;
|
|
92
|
+
|
|
93
|
+
const pricing = apiPlan.pricing;
|
|
94
|
+
if (pricing) {
|
|
95
|
+
return {
|
|
96
|
+
monthly,
|
|
97
|
+
semiannual: round2(pricing.semester.charged / 6),
|
|
98
|
+
annual: round2(pricing.annual.charged / 12),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
94
102
|
return {
|
|
95
103
|
monthly,
|
|
96
104
|
semiannual: round2(monthly * (1 - apiPlan.discount_semester / 100)),
|
|
@@ -102,7 +110,7 @@ function buildUiPlan(plan: Plan, currency: CurrencyCode, translate?: TranslateFn
|
|
|
102
110
|
const popular = plan.id === 2 || plan.id_plan === 2;
|
|
103
111
|
const buttonVariant = popular ? "brand" : "default";
|
|
104
112
|
|
|
105
|
-
const pricingByPeriod = computePricingByPeriod(plan
|
|
113
|
+
const pricingByPeriod = computePricingByPeriod(plan);
|
|
106
114
|
const monthlyFinal = pricingByPeriod.monthly;
|
|
107
115
|
|
|
108
116
|
const isStarterPlan = plan.id === STARTER_PLAN_ID || plan.id_plan === STARTER_PLAN_ID;
|
|
@@ -1,28 +1,41 @@
|
|
|
1
|
-
export const ADDON_IDS = {
|
|
2
|
-
DOMAINS: 1,
|
|
3
|
-
PAGES: 2,
|
|
4
|
-
PROJECTS: 3,
|
|
5
|
-
AB_TEST: 4,
|
|
6
|
-
HEATMAP: 5,
|
|
7
|
-
AI: 6,
|
|
8
|
-
} as const;
|
|
9
|
-
|
|
10
|
-
export type AddonKindEnum = (typeof ADDON_IDS)[keyof typeof ADDON_IDS];
|
|
11
|
-
|
|
12
|
-
export interface AiCreditOption {
|
|
13
|
-
credits: number;
|
|
14
|
-
price: number;
|
|
15
|
-
priceLabel
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
1
|
+
export const ADDON_IDS = {
|
|
2
|
+
DOMAINS: 1,
|
|
3
|
+
PAGES: 2,
|
|
4
|
+
PROJECTS: 3,
|
|
5
|
+
AB_TEST: 4,
|
|
6
|
+
HEATMAP: 5,
|
|
7
|
+
AI: 6,
|
|
8
|
+
} as const;
|
|
9
|
+
|
|
10
|
+
export type AddonKindEnum = (typeof ADDON_IDS)[keyof typeof ADDON_IDS];
|
|
11
|
+
|
|
12
|
+
export interface AiCreditOption {
|
|
13
|
+
credits: number;
|
|
14
|
+
price: number;
|
|
15
|
+
priceLabel?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const AI_CREDIT_SIZES = [0, 250, 500, 1000, 2500, 5000, 10000] as const;
|
|
19
|
+
|
|
20
|
+
export function resolveAiCreditPrice(quantity: number, perCreditPrice: number): number {
|
|
21
|
+
return Math.round(quantity * perCreditPrice * 100) / 100;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function buildAiCreditOptions(perCreditPrice: number): AiCreditOption[] {
|
|
25
|
+
return AI_CREDIT_SIZES.map((credits) => ({
|
|
26
|
+
credits,
|
|
27
|
+
price: resolveAiCreditPrice(credits, perCreditPrice),
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const AI_CREDIT_OPTIONS: AiCreditOption[] = [
|
|
32
|
+
{ credits: 0, price: 0, priceLabel: 'R$ 0,00' },
|
|
33
|
+
{ credits: 250, price: 9.99, priceLabel: 'R$ 9,99' },
|
|
34
|
+
{ credits: 500, price: 19.98, priceLabel: 'R$ 19,98' },
|
|
35
|
+
{ credits: 1000, price: 39.96, priceLabel: 'R$ 39,96' },
|
|
36
|
+
{ credits: 2500, price: 99.90, priceLabel: 'R$ 99,90' },
|
|
37
|
+
{ credits: 5000, price: 199.80, priceLabel: 'R$ 199,80' },
|
|
38
|
+
{ credits: 10000, price: 399.60, priceLabel: 'R$ 399,60' },
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
export const AI_CREDIT_VALUES: ReadonlySet<number> = new Set(AI_CREDIT_SIZES);
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* 'stripe_usd' → USD; demais gateways → BRL.
|
|
4
|
-
*/
|
|
1
|
+
import { isUsdGateway } from './gateway';
|
|
2
|
+
|
|
5
3
|
export type CurrencyCode = 'brl' | 'usd';
|
|
6
4
|
|
|
7
5
|
const CURRENCY_FORMAT: Record<CurrencyCode, { locale: string; currency: string }> = {
|
|
@@ -9,21 +7,11 @@ const CURRENCY_FORMAT: Record<CurrencyCode, { locale: string; currency: string }
|
|
|
9
7
|
usd: { locale: 'en-US', currency: 'USD' },
|
|
10
8
|
};
|
|
11
9
|
|
|
12
|
-
/**
|
|
13
|
-
* Deriva a moeda de exibição a partir do gateway da conta.
|
|
14
|
-
* A moeda é definida pelo backend no roteamento de gateway — read-only para o usuário.
|
|
15
|
-
* @param gateway - account.gateway ('stripe_usd' | 'stripe_brl' | 'vindi' | ...)
|
|
16
|
-
*/
|
|
17
10
|
export const getCurrencyForGateway = (gateway?: string | null): CurrencyCode =>
|
|
18
|
-
gateway
|
|
11
|
+
isUsdGateway(gateway) ? 'usd' : 'brl';
|
|
12
|
+
|
|
13
|
+
export const getLocaleForCurrency = (currency: CurrencyCode = 'brl'): string => CURRENCY_FORMAT[currency].locale;
|
|
19
14
|
|
|
20
|
-
/**
|
|
21
|
-
* Formata um valor (string ou número) como moeda.
|
|
22
|
-
* String: assume centavos (remove não-dígitos e divide por 100) — comportamento legado.
|
|
23
|
-
* @param value - valor a formatar
|
|
24
|
-
* @param currency - moeda de exibição (padrão 'brl' por retrocompatibilidade)
|
|
25
|
-
* @returns String formatada (ex: "R$ 1.234,56" ou "$1,234.56")
|
|
26
|
-
*/
|
|
27
15
|
export const formatCurrency = (value: string | number, currency: CurrencyCode = 'brl'): string => {
|
|
28
16
|
const fmt = CURRENCY_FORMAT[currency];
|
|
29
17
|
if (typeof value === 'number') {
|
|
@@ -41,23 +29,11 @@ export const formatCurrency = (value: string | number, currency: CurrencyCode =
|
|
|
41
29
|
});
|
|
42
30
|
};
|
|
43
31
|
|
|
44
|
-
/**
|
|
45
|
-
* Converte uma string de moeda para número (centavos / 100). Independente de moeda.
|
|
46
|
-
* @param value - String com valor de moeda
|
|
47
|
-
* @returns Número decimal
|
|
48
|
-
*/
|
|
49
32
|
export const parseCurrencyToNumber = (value: string): number => {
|
|
50
33
|
const numericValue = value.replace(/\D/g, '');
|
|
51
34
|
return parseFloat(numericValue) / 100;
|
|
52
35
|
};
|
|
53
36
|
|
|
54
|
-
/**
|
|
55
|
-
* Formata um número (opcional) como moeda.
|
|
56
|
-
* @param value - Número a formatar (pode ser undefined/null → 0)
|
|
57
|
-
* @param decimals - Casas decimais (padrão 2)
|
|
58
|
-
* @param currency - Moeda de exibição (padrão 'brl' por retrocompatibilidade)
|
|
59
|
-
* @returns String formatada (ex: "R$ 1.234,56" ou "$1,234.56")
|
|
60
|
-
*/
|
|
61
37
|
export const formatCurrencyNumber = (value?: number | null, decimals = 2, currency: CurrencyCode = 'brl'): string => {
|
|
62
38
|
const safeValue = value ?? 0;
|
|
63
39
|
const fmt = CURRENCY_FORMAT[currency];
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export const USD_GATEWAY = 'stripe_usd';
|
|
2
|
+
|
|
3
|
+
export type PaymentMethodKind = 'card' | 'pix' | 'boleto';
|
|
4
|
+
|
|
5
|
+
export type AccountGatewayLike = { gateway?: string | null } | null | undefined;
|
|
6
|
+
|
|
7
|
+
const normalizeGateway = (gateway?: string | null): string => (gateway ?? '').trim().toLowerCase();
|
|
8
|
+
|
|
9
|
+
export const isUsdGateway = (gateway?: string | null): boolean => normalizeGateway(gateway) === USD_GATEWAY;
|
|
10
|
+
|
|
11
|
+
export const isUsdAccount = (account: AccountGatewayLike): boolean => isUsdGateway(account?.gateway);
|
|
12
|
+
|
|
13
|
+
const USD_PAYMENT_METHODS: readonly PaymentMethodKind[] = ['card'];
|
|
14
|
+
const BRL_PAYMENT_METHODS: readonly PaymentMethodKind[] = ['card', 'pix', 'boleto'];
|
|
15
|
+
|
|
16
|
+
export const allowedPaymentMethods = (account: AccountGatewayLike): readonly PaymentMethodKind[] =>
|
|
17
|
+
isUsdAccount(account) ? USD_PAYMENT_METHODS : BRL_PAYMENT_METHODS;
|
|
18
|
+
|
|
19
|
+
export const isPaymentMethodAllowed = (account: AccountGatewayLike, method: PaymentMethodKind): boolean =>
|
|
20
|
+
allowedPaymentMethods(account).includes(method);
|
|
21
|
+
|
|
22
|
+
export const canBuyStandaloneAiCredits = (account: AccountGatewayLike): boolean => !isUsdAccount(account);
|