@greatapps/common 1.1.439 → 1.1.441
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/embeds/CrispEmbed.mjs +51 -21
- package/dist/components/embeds/CrispEmbed.mjs.map +1 -1
- package/dist/components/embeds/FrillEmbed.mjs +7 -1
- package/dist/components/embeds/FrillEmbed.mjs.map +1 -1
- package/dist/components/modals/cards/AddCardModal.mjs +2 -5
- package/dist/components/modals/cards/AddCardModal.mjs.map +1 -1
- package/dist/index.mjs +9 -1
- package/dist/index.mjs.map +1 -1
- package/dist/modules/plans/actions/list-plans.action.mjs +26 -0
- package/dist/modules/plans/actions/list-plans.action.mjs.map +1 -0
- package/dist/modules/plans/hooks/list-plans.hook.mjs +22 -0
- package/dist/modules/plans/hooks/list-plans.hook.mjs.map +1 -0
- package/dist/modules/plans/hooks/use-plan-by-id.hook.mjs +18 -2
- package/dist/modules/plans/hooks/use-plan-by-id.hook.mjs.map +1 -1
- package/dist/modules/plans/services/plans.service.mjs +60 -4
- package/dist/modules/plans/services/plans.service.mjs.map +1 -1
- package/dist/modules/plans/types/plan.type.mjs +1 -0
- package/dist/modules/plans/types/plan.type.mjs.map +1 -1
- package/dist/modules/plans/utils/map-api-plan-to-ui.mjs +58 -0
- package/dist/modules/plans/utils/map-api-plan-to-ui.mjs.map +1 -0
- package/dist/server.mjs +2 -0
- package/dist/server.mjs.map +1 -1
- package/dist/utils/format/currency.mjs +13 -0
- package/dist/utils/format/currency.mjs.map +1 -0
- package/package.json +1 -1
- package/src/components/embeds/CrispEmbed.tsx +73 -29
- package/src/components/embeds/FrillEmbed.tsx +8 -1
- package/src/components/modals/cards/AddCardModal.tsx +1 -2
- package/src/index.ts +6 -2
- package/src/modules/plans/actions/list-plans.action.ts +28 -0
- package/src/modules/plans/hooks/list-plans.hook.ts +21 -0
- package/src/modules/plans/hooks/use-plan-by-id.hook.ts +26 -5
- package/src/modules/plans/services/plans.service.ts +87 -4
- package/src/modules/plans/types/plan.type.ts +46 -0
- package/src/modules/plans/utils/map-api-plan-to-ui.ts +89 -0
- package/src/server.ts +1 -0
- package/src/utils/format/currency.ts +15 -0
|
@@ -1,11 +1,90 @@
|
|
|
1
1
|
import 'server-only';
|
|
2
2
|
|
|
3
|
-
import { api } from '
|
|
4
|
-
import { ApiError,
|
|
5
|
-
import { Plan,
|
|
3
|
+
import { api, apiClient } from '@greatapps/common/server';
|
|
4
|
+
import { ApiError, buildQueryParams, PlanSchema } from '@greatapps/common';
|
|
5
|
+
import type { ApiPaginatedActionResult, PaginatedSuccessResult, Plan, SuccessResult } from '@greatapps/common';
|
|
6
|
+
import greatCache from '@greatapps/cache';
|
|
7
|
+
|
|
8
|
+
export type ListPlansParams = {
|
|
9
|
+
sort?: string;
|
|
10
|
+
active?: boolean;
|
|
11
|
+
search?: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const PLANS_CACHE_TTL = 604800;
|
|
6
15
|
|
|
7
16
|
class PlansService {
|
|
17
|
+
private cache = new greatCache({
|
|
18
|
+
service: 'plans-service',
|
|
19
|
+
version: '1.0.1',
|
|
20
|
+
domain: 'whitelabel-cache.greatapps.com.br',
|
|
21
|
+
ambient: process.env.NODE_ENV || 'development',
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
private buildCacheKey(key: string, params?: ListPlansParams): string {
|
|
25
|
+
const sort = params?.sort ?? 'id:ASC';
|
|
26
|
+
const active = params?.active ?? '';
|
|
27
|
+
const search = params?.search ?? '';
|
|
28
|
+
return `${key}-${sort}-${active}-${search}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Lista planos do whitelabel.
|
|
33
|
+
* Exemplo:
|
|
34
|
+
* GET /{id_wl}/plans?active=true&search=client&sort=id:ASC
|
|
35
|
+
*/
|
|
36
|
+
async listPlans(params?: ListPlansParams): Promise<PaginatedSuccessResult<Plan>> {
|
|
37
|
+
const cacheKey = this.buildCacheKey('plans', params);
|
|
38
|
+
|
|
39
|
+
const cachedData = await this.cache.select(cacheKey);
|
|
40
|
+
|
|
41
|
+
if (cachedData.status == 1 && 'data' in cachedData && cachedData.data) {
|
|
42
|
+
const data = JSON.parse(cachedData.data) as Plan[];
|
|
43
|
+
return { data, total: data.length, success: true };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const query = buildQueryParams({
|
|
47
|
+
sort: params?.sort ?? 'id:ASC',
|
|
48
|
+
active: params?.active,
|
|
49
|
+
search: params?.search,
|
|
50
|
+
});
|
|
51
|
+
const url = `/plans${query ? `?${query}` : ''}`;
|
|
52
|
+
|
|
53
|
+
const response = await apiClient.get<ApiPaginatedActionResult<Plan[]>>(url);
|
|
54
|
+
|
|
55
|
+
if (response.status === 0) {
|
|
56
|
+
throw new ApiError(
|
|
57
|
+
(response as { message?: string }).message || 'Erro ao listar planos',
|
|
58
|
+
'LIST_PLANS_FAILED',
|
|
59
|
+
400
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const rawData = (response as { data?: unknown }).data;
|
|
64
|
+
const data = Array.isArray(rawData)
|
|
65
|
+
? rawData.map((item) => PlanSchema.parse(item))
|
|
66
|
+
: [];
|
|
67
|
+
|
|
68
|
+
await this.cache.insert(cacheKey, JSON.stringify(data), PLANS_CACHE_TTL);
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
data,
|
|
72
|
+
total: response.total,
|
|
73
|
+
success: true,
|
|
74
|
+
} satisfies PaginatedSuccessResult<Plan>;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
8
78
|
async findById(idPlan: number | string): Promise<SuccessResult<Plan>> {
|
|
79
|
+
const cacheKey = this.buildCacheKey(`plan-${idPlan}`);
|
|
80
|
+
|
|
81
|
+
const cachedData = await this.cache.select(cacheKey);
|
|
82
|
+
|
|
83
|
+
if (cachedData.status == 1 && 'data' in cachedData && cachedData.data) {
|
|
84
|
+
const data = JSON.parse(cachedData.data) as Plan;
|
|
85
|
+
return { data, success: true };
|
|
86
|
+
}
|
|
87
|
+
|
|
9
88
|
const response = await api.apps.get<ApiPaginatedActionResult<Plan>>(
|
|
10
89
|
`/plans/${idPlan}?type=client`
|
|
11
90
|
);
|
|
@@ -26,9 +105,13 @@ class PlansService {
|
|
|
26
105
|
);
|
|
27
106
|
}
|
|
28
107
|
|
|
108
|
+
const plan = PlanSchema.parse(response.data[0]);
|
|
109
|
+
|
|
110
|
+
await this.cache.insert(cacheKey, JSON.stringify(plan), PLANS_CACHE_TTL);
|
|
111
|
+
|
|
29
112
|
return {
|
|
30
113
|
success: true,
|
|
31
|
-
data:
|
|
114
|
+
data: plan,
|
|
32
115
|
};
|
|
33
116
|
}
|
|
34
117
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import z from 'zod';
|
|
2
|
+
import type { BillingPeriod } from '../../subscriptions/types/billing-period.type';
|
|
2
3
|
|
|
3
4
|
export const PlanItemSchema = z.object({
|
|
4
5
|
id_addon: z.number(),
|
|
@@ -10,6 +11,7 @@ export type PlanItem = z.infer<typeof PlanItemSchema>;
|
|
|
10
11
|
|
|
11
12
|
export const PlanSchema = z.object({
|
|
12
13
|
id: z.number(),
|
|
14
|
+
id_plan: z.number().optional(),
|
|
13
15
|
name: z.string(),
|
|
14
16
|
value: z.number().default(0),
|
|
15
17
|
type: z.string().optional(),
|
|
@@ -19,3 +21,47 @@ export const PlanSchema = z.object({
|
|
|
19
21
|
});
|
|
20
22
|
|
|
21
23
|
export type Plan = z.infer<typeof PlanSchema>;
|
|
24
|
+
|
|
25
|
+
// ── UI Plan types (used by list-plans action and consumers) ──
|
|
26
|
+
|
|
27
|
+
export type PlanFeature = {
|
|
28
|
+
icon: 'check' | 'x';
|
|
29
|
+
text: string;
|
|
30
|
+
disabled?: boolean;
|
|
31
|
+
tooltip?: keyof PlanTooltips;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type PlanMainFeatures = {
|
|
35
|
+
pages: string;
|
|
36
|
+
domains: string;
|
|
37
|
+
users: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** Preços mensais equivalentes por período de cobrança (já com desconto aplicado). */
|
|
41
|
+
export type PlanPricingByPeriod = {
|
|
42
|
+
monthly: number;
|
|
43
|
+
semiannual: number;
|
|
44
|
+
annual: number;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type UiPlan = {
|
|
48
|
+
idPlan: number;
|
|
49
|
+
name: string;
|
|
50
|
+
isPopular?: boolean;
|
|
51
|
+
isCurrentPlan?: boolean;
|
|
52
|
+
currentPeriodicity?: BillingPeriod | null;
|
|
53
|
+
mainFeatures: PlanMainFeatures;
|
|
54
|
+
features: PlanFeature[];
|
|
55
|
+
originalPrice: string;
|
|
56
|
+
price: string;
|
|
57
|
+
pricingByPeriod?: PlanPricingByPeriod;
|
|
58
|
+
buttonVariant?: 'brand' | 'default';
|
|
59
|
+
items: PlanItem[];
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export type PlanTooltips = {
|
|
63
|
+
domains: string;
|
|
64
|
+
users: string;
|
|
65
|
+
sharePages: string;
|
|
66
|
+
projectManagement: string;
|
|
67
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { Plan, PlanFeature, PlanMainFeatures, PlanPricingByPeriod, UiPlan } from '../types/plan.type';
|
|
2
|
+
import { ADDON_IDS } from '../../subscriptions/constants/addons.constants';
|
|
3
|
+
import { formatCurrencyNumber } from '../../../utils/format/currency';
|
|
4
|
+
|
|
5
|
+
export type PlanBillingPeriod = 'monthly' | 'semiannual' | 'annual';
|
|
6
|
+
|
|
7
|
+
const BASE_FEATURES: PlanFeature[] = [
|
|
8
|
+
{ icon: 'check', text: 'Visitas ilimitadas' },
|
|
9
|
+
{ icon: 'check', text: 'Leads ilimitadas' },
|
|
10
|
+
{ icon: 'check', text: 'Compartilhar páginas', tooltip: 'sharePages' },
|
|
11
|
+
{ icon: 'check', text: 'Hospedagem inclusa' },
|
|
12
|
+
{ icon: 'check', text: 'SSL (HTTPS) + CDN' },
|
|
13
|
+
{ icon: 'check', text: 'Templates Free' },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
function buildMainFeaturesFromItems(items: Plan['items']): PlanMainFeatures {
|
|
17
|
+
const pagesItem = items.find((i) => i.id_addon === ADDON_IDS.PAGES);
|
|
18
|
+
const domainsItem = items.find((i) => i.id_addon === ADDON_IDS.DOMAINS);
|
|
19
|
+
|
|
20
|
+
const pagesQty = pagesItem?.quantity ?? 0;
|
|
21
|
+
const domainsQty = domainsItem?.quantity ?? 1;
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
pages:
|
|
25
|
+
pagesQty === 0
|
|
26
|
+
? 'Páginas ilimitadas'
|
|
27
|
+
: pagesQty === 1
|
|
28
|
+
? '1 página'
|
|
29
|
+
: `${pagesQty} páginas`,
|
|
30
|
+
domains:
|
|
31
|
+
domainsQty >= 999
|
|
32
|
+
? 'Domínios ilimitados'
|
|
33
|
+
: domainsQty === 1
|
|
34
|
+
? '1 domínio externo'
|
|
35
|
+
: `${domainsQty} domínios externos`,
|
|
36
|
+
users: 'Usuários ilimitados',
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function computePricingByPeriod(apiPlan: Plan): PlanPricingByPeriod {
|
|
41
|
+
const monthly = apiPlan.value;
|
|
42
|
+
const round2 = (n: number) => Math.round(n * 100) / 100;
|
|
43
|
+
return {
|
|
44
|
+
monthly,
|
|
45
|
+
semiannual: round2(monthly * (1 - apiPlan.discount_semester / 100)),
|
|
46
|
+
annual: round2(monthly * (1 - apiPlan.discount_annual / 100)),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function mapApiPlanToUiPlan(
|
|
51
|
+
apiPlan: Plan,
|
|
52
|
+
options: {
|
|
53
|
+
planId: number;
|
|
54
|
+
isPopular?: boolean;
|
|
55
|
+
buttonVariant?: 'brand' | 'default';
|
|
56
|
+
}
|
|
57
|
+
): UiPlan {
|
|
58
|
+
const { planId, isPopular, buttonVariant } = options;
|
|
59
|
+
|
|
60
|
+
const pricingByPeriod = computePricingByPeriod(apiPlan);
|
|
61
|
+
const monthlyFinal = pricingByPeriod.monthly;
|
|
62
|
+
|
|
63
|
+
const lowerName = apiPlan.name.toLowerCase();
|
|
64
|
+
const hasProjectAndOnboarding =
|
|
65
|
+
lowerName.includes('agência') || lowerName.includes('agencia');
|
|
66
|
+
|
|
67
|
+
const features: PlanFeature[] = [
|
|
68
|
+
...BASE_FEATURES,
|
|
69
|
+
hasProjectAndOnboarding
|
|
70
|
+
? { icon: 'check', text: 'Reunião de Onboarding' }
|
|
71
|
+
: { icon: 'x', text: 'Reunião de Onboarding', disabled: true },
|
|
72
|
+
hasProjectAndOnboarding
|
|
73
|
+
? { icon: 'check', text: 'Gestão de projetos', tooltip: 'projectManagement' }
|
|
74
|
+
: { icon: 'x', text: 'Gestão de projetos', disabled: true, tooltip: 'projectManagement' },
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
idPlan: planId,
|
|
79
|
+
name: apiPlan.name,
|
|
80
|
+
isPopular,
|
|
81
|
+
buttonVariant,
|
|
82
|
+
mainFeatures: buildMainFeaturesFromItems(apiPlan.items),
|
|
83
|
+
features,
|
|
84
|
+
originalPrice: formatCurrencyNumber(monthlyFinal),
|
|
85
|
+
price: formatCurrencyNumber(monthlyFinal),
|
|
86
|
+
pricingByPeriod,
|
|
87
|
+
items: apiPlan.items,
|
|
88
|
+
};
|
|
89
|
+
}
|
package/src/server.ts
CHANGED
|
@@ -23,6 +23,7 @@ export { findUserById } from './modules/users/action/find-user-by-id.action';
|
|
|
23
23
|
export { findCurrentAccount } from './modules/accounts/actions/find-current-account.action';
|
|
24
24
|
export { listSubscriptionsAction } from './modules/subscriptions/actions/list-subscriptions.action';
|
|
25
25
|
export { findPlanByIdAction } from './modules/plans/actions/find-plan-by-id.action';
|
|
26
|
+
export { listPlansAction } from './modules/plans/actions/list-plans.action';
|
|
26
27
|
export { calculateSubscriptionAction } from './modules/subscriptions/actions/calculate-subscription.action';
|
|
27
28
|
export { updateSubscriptionPlanAction } from './modules/subscriptions/actions/update-subscription-plan.action';
|
|
28
29
|
export { listCardsAction } from './modules/cards/actions/list-cards.action';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formata um número (opcional) para moeda BRL
|
|
3
|
+
* @param value - Número a ser formatado (pode ser undefined ou null)
|
|
4
|
+
* @param decimals - Casas decimais (padrão: 2)
|
|
5
|
+
* @returns String formatada como "R$ X.XXX,XX"
|
|
6
|
+
*/
|
|
7
|
+
export const formatCurrencyNumber = (value?: number | null, decimals = 2): string => {
|
|
8
|
+
const safeValue = value ?? 0;
|
|
9
|
+
return safeValue.toLocaleString('pt-BR', {
|
|
10
|
+
style: 'currency',
|
|
11
|
+
currency: 'BRL',
|
|
12
|
+
minimumFractionDigits: decimals,
|
|
13
|
+
maximumFractionDigits: decimals,
|
|
14
|
+
});
|
|
15
|
+
};
|