@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.
Files changed (34) hide show
  1. package/dist/components/modals/BuyCreditsModal.mjs +42 -30
  2. package/dist/components/modals/BuyCreditsModal.mjs.map +1 -1
  3. package/dist/components/modals/cards/AddCardModal.mjs +2 -1
  4. package/dist/components/modals/cards/AddCardModal.mjs.map +1 -1
  5. package/dist/index.mjs +25 -9
  6. package/dist/index.mjs.map +1 -1
  7. package/dist/modules/accounts/hooks/use-currency-formatter.hook.mjs +3 -1
  8. package/dist/modules/accounts/hooks/use-currency-formatter.hook.mjs.map +1 -1
  9. package/dist/modules/accounts/hooks/use-required-billing-data.hook.mjs +3 -2
  10. package/dist/modules/accounts/hooks/use-required-billing-data.hook.mjs.map +1 -1
  11. package/dist/modules/accounts/utils/billing-data.mjs +3 -8
  12. package/dist/modules/accounts/utils/billing-data.mjs.map +1 -1
  13. package/dist/modules/plans/types/plan.type.mjs +15 -0
  14. package/dist/modules/plans/types/plan.type.mjs.map +1 -1
  15. package/dist/modules/plans/utils/map-api-plan-to-ui.mjs +10 -2
  16. package/dist/modules/plans/utils/map-api-plan-to-ui.mjs.map +1 -1
  17. package/dist/modules/subscriptions/constants/addons.constants.mjs +15 -2
  18. package/dist/modules/subscriptions/constants/addons.constants.mjs.map +1 -1
  19. package/dist/utils/format/currency.mjs +4 -1
  20. package/dist/utils/format/currency.mjs.map +1 -1
  21. package/dist/utils/format/gateway.mjs +18 -0
  22. package/dist/utils/format/gateway.mjs.map +1 -0
  23. package/package.json +1 -1
  24. package/src/components/modals/BuyCreditsModal.tsx +48 -32
  25. package/src/components/modals/cards/AddCardModal.tsx +2 -1
  26. package/src/index.ts +14 -5
  27. package/src/modules/accounts/hooks/use-currency-formatter.hook.tsx +3 -0
  28. package/src/modules/accounts/hooks/use-required-billing-data.hook.ts +3 -2
  29. package/src/modules/accounts/utils/billing-data.ts +2 -13
  30. package/src/modules/plans/types/plan.type.ts +17 -0
  31. package/src/modules/plans/utils/map-api-plan-to-ui.ts +12 -4
  32. package/src/modules/subscriptions/constants/addons.constants.ts +41 -28
  33. package/src/utils/format/currency.ts +5 -29
  34. 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
- AI_CREDIT_OPTIONS,
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
- const option = AI_CREDIT_OPTIONS.find((opt) => opt.credits === credits);
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(option.credits),
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 formatPrice = useCallback((price: number) => {
190
- return price.toLocaleString('pt-BR', {
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 AI_CREDIT_OPTIONS.map((opt) => ({
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('pt-BR') })}
219
+ {translate('common.credits.buy.creditsOptionLabel', { credits: opt.credits.toLocaleString(locale) })}
214
220
  </span>
215
- <span className="paragraph-small-medium text-zinc-500 opacity-70">{opt.priceLabel}</span>
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('pt-BR') }),
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
- const option = AI_CREDIT_OPTIONS.find((opt) => opt.credits === data.creditOption);
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(option.credits),
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('pt-BR') })}
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
- [subscriptionId, updatePlanMutation, form, closeModal, buildPlanExtras]
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') : formatPrice(totalPrice)}
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, formatPrice]);
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
- {formatPrice(immediateChargeTotal)}
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
- {formatPrice(immediateChargeTotal)}
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?.gateway === 'stripe_usd';
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, isInternationalAccount } from '../utils/billing-data';
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: isInternationalAccount(account),
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 = isInternationalAccount(account)
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, currency: CurrencyCode): PlanPricingByPeriod {
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, currency);
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: string;
16
- }
17
-
18
- export const AI_CREDIT_OPTIONS: AiCreditOption[] = [
19
- { credits: 0, price: 0, priceLabel: 'R$ 0,00' },
20
- { credits: 250, price: 9.99, priceLabel: 'R$ 9,99' },
21
- { credits: 500, price: 19.98, priceLabel: 'R$ 19,98' },
22
- { credits: 1000, price: 39.96, priceLabel: 'R$ 39,96' },
23
- { credits: 2500, price: 99.90, priceLabel: 'R$ 99,90' },
24
- { credits: 5000, price: 199.80, priceLabel: 'R$ 199,80' },
25
- { credits: 10000, price: 399.60, priceLabel: 'R$ 399,60' },
26
- ];
27
-
28
- export const AI_CREDIT_VALUES = new Set(AI_CREDIT_OPTIONS.map((opt) => opt.credits));
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
- * Moeda de exibição. Espelha getCurrencyForGateway do backend (gapps-r3-api):
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 === 'stripe_usd' ? 'usd' : 'brl';
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);