@greatapps/common 1.1.735 → 1.1.737
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/modules/plans/services/plans.service.mjs +39 -2
- package/dist/modules/plans/services/plans.service.mjs.map +1 -1
- package/dist/modules/subscriptions/services/subscriptions.service.mjs +5 -2
- package/dist/modules/subscriptions/services/subscriptions.service.mjs.map +1 -1
- package/package.json +1 -1
- package/src/modules/plans/services/plans.service.ts +49 -2
- package/src/modules/subscriptions/services/subscriptions.service.ts +24 -0
|
@@ -1,14 +1,40 @@
|
|
|
1
1
|
import "server-only";
|
|
2
2
|
import { api, getUserContext } from "@greatapps/common/server";
|
|
3
3
|
import { ApiError, buildQueryParams, PlanSchema } from "@greatapps/common";
|
|
4
|
+
import greatCache from "@greatapps/cache";
|
|
5
|
+
const PLANS_CACHE_TTL = 21600;
|
|
4
6
|
class PlansService {
|
|
7
|
+
cache = new greatCache({
|
|
8
|
+
service: "plans",
|
|
9
|
+
version: "1.0",
|
|
10
|
+
domain: "whitelabel-cache.greatapps.com.br",
|
|
11
|
+
ambient: process.env.NODE_ENV || "development"
|
|
12
|
+
});
|
|
13
|
+
buildCacheKey(key, params) {
|
|
14
|
+
const idWl = params?.id_wl ?? "";
|
|
15
|
+
const idAccount = params?.id_account ?? "";
|
|
16
|
+
const sort = params?.sort ?? "id:ASC";
|
|
17
|
+
const active = params?.active ?? "";
|
|
18
|
+
const search = params?.search ?? "";
|
|
19
|
+
return `${key}-${idWl}-${idAccount}-${sort}-${active}-${search}-v6`;
|
|
20
|
+
}
|
|
5
21
|
/**
|
|
6
22
|
* Lista planos do whitelabel.
|
|
7
23
|
* Exemplo:
|
|
8
24
|
* GET /{id_wl}/plans?active=true&search=client&sort=id:ASC
|
|
9
25
|
*/
|
|
10
26
|
async listPlans(params) {
|
|
11
|
-
const { id_account } = await getUserContext();
|
|
27
|
+
const { id_wl, id_account } = await getUserContext();
|
|
28
|
+
const cacheKey = this.buildCacheKey("plans", {
|
|
29
|
+
...params,
|
|
30
|
+
id_wl,
|
|
31
|
+
id_account
|
|
32
|
+
});
|
|
33
|
+
const cachedData = await this.cache.select(cacheKey);
|
|
34
|
+
if (cachedData.status == 1 && "data" in cachedData && cachedData.data) {
|
|
35
|
+
const data2 = JSON.parse(cachedData.data);
|
|
36
|
+
return { data: data2, total: data2.length, success: true };
|
|
37
|
+
}
|
|
12
38
|
const query = buildQueryParams({
|
|
13
39
|
sort: params?.sort ?? "id:ASC",
|
|
14
40
|
active: params?.active,
|
|
@@ -26,6 +52,7 @@ class PlansService {
|
|
|
26
52
|
}
|
|
27
53
|
const rawData = response.data;
|
|
28
54
|
const data = Array.isArray(rawData) ? rawData.map((item) => PlanSchema.parse(item)) : [];
|
|
55
|
+
await this.cache.insert(cacheKey, JSON.stringify(data), PLANS_CACHE_TTL);
|
|
29
56
|
return {
|
|
30
57
|
data,
|
|
31
58
|
total: response.total,
|
|
@@ -33,7 +60,16 @@ class PlansService {
|
|
|
33
60
|
};
|
|
34
61
|
}
|
|
35
62
|
async findById(idPlan) {
|
|
36
|
-
const { id_account } = await getUserContext();
|
|
63
|
+
const { id_wl, id_account } = await getUserContext();
|
|
64
|
+
const cacheKey = this.buildCacheKey(`plan-${idPlan}-v2`, {
|
|
65
|
+
id_wl,
|
|
66
|
+
id_account: String(id_account)
|
|
67
|
+
});
|
|
68
|
+
const cachedData = await this.cache.select(cacheKey);
|
|
69
|
+
if (cachedData.status == 1 && "data" in cachedData && cachedData.data) {
|
|
70
|
+
const data = JSON.parse(cachedData.data);
|
|
71
|
+
return { data, success: true };
|
|
72
|
+
}
|
|
37
73
|
const query = buildQueryParams({ type: "client", id_account });
|
|
38
74
|
const response = await api.apps.get(
|
|
39
75
|
`/plans/${idPlan}?${query}`
|
|
@@ -49,6 +85,7 @@ class PlansService {
|
|
|
49
85
|
throw new ApiError("Plano n\xE3o encontrado", "PLAN_NOT_FOUND", 404);
|
|
50
86
|
}
|
|
51
87
|
const plan = PlanSchema.parse(response.data[0]);
|
|
88
|
+
await this.cache.insert(cacheKey, JSON.stringify(plan), PLANS_CACHE_TTL);
|
|
52
89
|
return {
|
|
53
90
|
success: true,
|
|
54
91
|
data: plan
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/modules/plans/services/plans.service.ts"],"sourcesContent":["import \"server-only\";\n\nimport { api, getUserContext } from \"@greatapps/common/server\";\nimport { ApiError, buildQueryParams, PlanSchema } from \"@greatapps/common\";\nimport type {\n ApiPaginatedActionResult,\n PaginatedSuccessResult,\n Plan,\n SuccessResult,\n} from \"@greatapps/common\";\n\nexport type ListPlansParams = {\n sort?: string;\n active?: boolean;\n search?: string;\n};\n\nclass PlansService {\n /**\n * Lista planos do whitelabel.\n * Exemplo:\n * GET /{id_wl}/plans?active=true&search=client&sort=id:ASC\n */\n async listPlans(\n params?: ListPlansParams,\n ): Promise<PaginatedSuccessResult<Plan>> {\n const { id_account } = await getUserContext();\n\n const query = buildQueryParams({\n sort: params?.sort ?? \"id:ASC\",\n active: params?.active,\n search: params?.search,\n id_account: id_account,\n });\n const url = `/plans${query ? `?${query}` : \"\"}`;\n\n const response = await api.apps.get<ApiPaginatedActionResult<Plan[]>>(url);\n\n if (response.status === 0) {\n throw new ApiError(\n (response as { message?: string }).message || \"Erro ao listar planos\",\n \"LIST_PLANS_FAILED\",\n 400,\n );\n }\n\n const rawData = (response as { data?: unknown }).data;\n const data = Array.isArray(rawData)\n ? rawData.map((item) => PlanSchema.parse(item))\n : [];\n\n return {\n data,\n total: response.total,\n success: true,\n } satisfies PaginatedSuccessResult<Plan>;\n }\n\n async findById(idPlan: number | string): Promise<SuccessResult<Plan>> {\n const { id_account } = await getUserContext();\n\n const query = buildQueryParams({ type: \"client\", id_account });\n\n const response = await api.apps.get<ApiPaginatedActionResult<Plan>>(\n `/plans/${idPlan}?${query}`,\n );\n\n if (response.status === 0) {\n throw new ApiError(\n response.message || \"Erro ao buscar plano\",\n \"FIND_PLAN_FAILED\",\n 400,\n );\n }\n\n if (!response.data?.length) {\n throw new ApiError(\"Plano não encontrado\", \"PLAN_NOT_FOUND\", 404);\n }\n\n const plan = PlanSchema.parse(response.data[0]);\n\n return {\n success: true,\n data: plan,\n };\n }\n}\n\nexport const plansService = new PlansService();\n"],"mappings":"AAAA,OAAO;AAEP,SAAS,KAAK,sBAAsB;AACpC,SAAS,UAAU,kBAAkB,kBAAkB;
|
|
1
|
+
{"version":3,"sources":["../../../../src/modules/plans/services/plans.service.ts"],"sourcesContent":["import \"server-only\";\n\nimport { api, getUserContext } from \"@greatapps/common/server\";\nimport { ApiError, buildQueryParams, PlanSchema } from \"@greatapps/common\";\nimport type {\n ApiPaginatedActionResult,\n PaginatedSuccessResult,\n Plan,\n SuccessResult,\n} from \"@greatapps/common\";\nimport greatCache from \"@greatapps/cache\";\n\nexport type ListPlansParams = {\n sort?: string;\n active?: boolean;\n search?: string;\n};\n\nconst PLANS_CACHE_TTL = 21600;\n\nclass PlansService {\n private cache = new greatCache({\n service: \"plans\",\n version: \"1.0\",\n domain: \"whitelabel-cache.greatapps.com.br\",\n ambient: process.env.NODE_ENV || \"development\",\n });\n\n private buildCacheKey(key: string, params?: Record<string, unknown>): string {\n const idWl = params?.id_wl ?? \"\";\n const idAccount = params?.id_account ?? \"\";\n const sort = params?.sort ?? \"id:ASC\";\n const active = params?.active ?? \"\";\n const search = params?.search ?? \"\";\n // v6: descarta as entradas v5 que ficaram vivas durante a janela sem cache\n return `${key}-${idWl}-${idAccount}-${sort}-${active}-${search}-v6`;\n }\n\n /**\n * Lista planos do whitelabel.\n * Exemplo:\n * GET /{id_wl}/plans?active=true&search=client&sort=id:ASC\n */\n async listPlans(\n params?: ListPlansParams,\n ): Promise<PaginatedSuccessResult<Plan>> {\n const { id_wl, id_account } = await getUserContext();\n const cacheKey = this.buildCacheKey(\"plans\", {\n ...params,\n id_wl,\n id_account,\n });\n\n const cachedData = await this.cache.select(cacheKey);\n\n if (cachedData.status == 1 && \"data\" in cachedData && cachedData.data) {\n const data = JSON.parse(cachedData.data) as Plan[];\n return { data, total: data.length, success: true };\n }\n\n const query = buildQueryParams({\n sort: params?.sort ?? \"id:ASC\",\n active: params?.active,\n search: params?.search,\n id_account: id_account,\n });\n const url = `/plans${query ? `?${query}` : \"\"}`;\n\n const response = await api.apps.get<ApiPaginatedActionResult<Plan[]>>(url);\n\n if (response.status === 0) {\n throw new ApiError(\n (response as { message?: string }).message || \"Erro ao listar planos\",\n \"LIST_PLANS_FAILED\",\n 400,\n );\n }\n\n const rawData = (response as { data?: unknown }).data;\n const data = Array.isArray(rawData)\n ? rawData.map((item) => PlanSchema.parse(item))\n : [];\n\n await this.cache.insert(cacheKey, JSON.stringify(data), PLANS_CACHE_TTL);\n\n return {\n data,\n total: response.total,\n success: true,\n } satisfies PaginatedSuccessResult<Plan>;\n }\n\n async findById(idPlan: number | string): Promise<SuccessResult<Plan>> {\n const { id_wl, id_account } = await getUserContext();\n const cacheKey = this.buildCacheKey(`plan-${idPlan}-v2`, {\n id_wl,\n id_account: String(id_account),\n });\n\n const cachedData = await this.cache.select(cacheKey);\n\n if (cachedData.status == 1 && \"data\" in cachedData && cachedData.data) {\n const data = JSON.parse(cachedData.data) as Plan;\n return { data, success: true };\n }\n\n const query = buildQueryParams({ type: \"client\", id_account });\n\n const response = await api.apps.get<ApiPaginatedActionResult<Plan>>(\n `/plans/${idPlan}?${query}`,\n );\n\n if (response.status === 0) {\n throw new ApiError(\n response.message || \"Erro ao buscar plano\",\n \"FIND_PLAN_FAILED\",\n 400,\n );\n }\n\n if (!response.data?.length) {\n throw new ApiError(\"Plano não encontrado\", \"PLAN_NOT_FOUND\", 404);\n }\n\n const plan = PlanSchema.parse(response.data[0]);\n\n await this.cache.insert(cacheKey, JSON.stringify(plan), PLANS_CACHE_TTL);\n\n return {\n success: true,\n data: plan,\n };\n }\n}\n\nexport const plansService = new PlansService();\n"],"mappings":"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","names":["data"]}
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
import {
|
|
16
16
|
SubscriptionPendingPixResponseSchema
|
|
17
17
|
} from "../types/pix-pending.type";
|
|
18
|
+
const SUBSCRIPTION_WRITE_TIMEOUT_MS = 6e4;
|
|
18
19
|
class SubscriptionsService {
|
|
19
20
|
async listSubscriptions(params) {
|
|
20
21
|
const { id_account } = await getUserContext();
|
|
@@ -61,7 +62,8 @@ class SubscriptionsService {
|
|
|
61
62
|
const { id_account } = await getUserContext();
|
|
62
63
|
const response = await api.apps.put(
|
|
63
64
|
`/accounts/${id_account}/subscriptions/${subscriptionId}/action/plan`,
|
|
64
|
-
data
|
|
65
|
+
data,
|
|
66
|
+
{ timeout: SUBSCRIPTION_WRITE_TIMEOUT_MS }
|
|
65
67
|
);
|
|
66
68
|
if (response.status === 0) {
|
|
67
69
|
throw new ApiError(
|
|
@@ -87,7 +89,8 @@ class SubscriptionsService {
|
|
|
87
89
|
const { id_account } = await getUserContext();
|
|
88
90
|
const response = await api.apps.put(
|
|
89
91
|
`/accounts/${id_account}/subscriptions/${subscriptionId}/action/payment`,
|
|
90
|
-
data
|
|
92
|
+
data,
|
|
93
|
+
{ timeout: SUBSCRIPTION_WRITE_TIMEOUT_MS }
|
|
91
94
|
);
|
|
92
95
|
if (response.status === 0) {
|
|
93
96
|
throw new ApiError(
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/modules/subscriptions/services/subscriptions.service.ts"],"sourcesContent":["import \"server-only\";\r\n\r\nimport { api } from \"../../../infra/api/client\";\r\nimport {\r\n ApiError,\r\n ApiActionResult,\r\n ApiPaginatedActionResult,\r\n PaginatedSuccessResult,\r\n SuccessResult,\r\n} from \"../../../infra/api/types\";\r\nimport { buildQueryParams } from \"../../../infra/utils/params\";\r\nimport { assertManagementPermission } from \"../../auth/utils/assert-management-permission\";\r\nimport { getUserContext } from \"../../auth/utils/get-user-context\";\r\nimport {\r\n Subscription,\r\n SubscriptionSchema,\r\n FindSubscriptionsParams,\r\n} from \"../types/subscription.type\";\r\nimport {\r\n type CalculateSubscriptionRequest,\r\n type CalculateSubscriptionResponse,\r\n type UpdateSubscriptionPlanRequest,\r\n CalculateSubscriptionResponseSchema,\r\n} from \"../types/calculate-subscription.type\";\r\nimport {\r\n SubscriptionPendingPixResponseSchema,\r\n type SubscriptionPendingPixResponse,\r\n} from \"../types/pix-pending.type\";\r\n\r\nexport type UpdateSubscriptionPlanResponse =\r\n | Subscription\r\n | SubscriptionPendingPixResponse;\r\n\r\nclass SubscriptionsService {\r\n async listSubscriptions(\r\n params?: Partial<FindSubscriptionsParams>,\r\n ): Promise<PaginatedSuccessResult<Subscription>> {\r\n const { id_account } = await getUserContext();\r\n\r\n const query = buildQueryParams(\r\n params as Record<string, string | boolean | undefined>,\r\n );\r\n const url = `/accounts/${id_account}/subscriptions${query ? `?${query}` : \"\"}`;\r\n\r\n const response =\r\n await api.apps.get<ApiPaginatedActionResult<Subscription>>(url);\r\n\r\n if (response.status === 0) {\r\n throw new ApiError(\r\n response.message || \"Erro ao listar assinaturas\",\r\n \"LIST_SUBSCRIPTIONS_FAILED\",\r\n 400,\r\n );\r\n }\r\n\r\n const data = response.data.map((item) => SubscriptionSchema.parse(item));\r\n\r\n return {\r\n data,\r\n total: response.total,\r\n success: true,\r\n } satisfies PaginatedSuccessResult<Subscription>;\r\n }\r\n\r\n async calculateSubscription(\r\n data: CalculateSubscriptionRequest,\r\n ): Promise<SuccessResult<CalculateSubscriptionResponse>> {\r\n const { id_account } = await getUserContext();\r\n\r\n const response = await api.apps.post<\r\n ApiActionResult<CalculateSubscriptionResponse>\r\n >(`/accounts/${id_account}/subscriptions/action/calculate`, data);\r\n\r\n console.log(\"Calculate subscription response:\", {\r\n response,\r\n data: JSON.stringify(data, null, 2),\r\n });\r\n\r\n if (response.status === 0) {\r\n throw new ApiError(\r\n response.message || \"Erro ao calcular assinatura\",\r\n \"CALCULATE_SUBSCRIPTION_FAILED\",\r\n 400,\r\n );\r\n }\r\n\r\n return {\r\n success: true,\r\n data: CalculateSubscriptionResponseSchema.parse(response.data),\r\n };\r\n }\r\n\r\n async updateSubscriptionPlan(\r\n subscriptionId: number | string,\r\n data: UpdateSubscriptionPlanRequest,\r\n ): Promise<\r\n SuccessResult<UpdateSubscriptionPlanResponse | null> & { client_secret?: string }\r\n > {\r\n await assertManagementPermission('manage_subscriptions');\r\n const { id_account } = await getUserContext();\r\n\r\n const response = await api.apps.put<\r\n ApiActionResult<UpdateSubscriptionPlanResponse | UpdateSubscriptionPlanResponse[]> & {\r\n client_secret?: string;\r\n }\r\n >(\r\n `/accounts/${id_account}/subscriptions/${subscriptionId}/action/plan`,\r\n data,\r\n );\r\n\r\n if (response.status === 0) {\r\n throw new ApiError(\r\n response.message || \"Erro ao atualizar plano\",\r\n \"UPDATE_SUBSCRIPTION_PLAN_FAILED\",\r\n 400,\r\n );\r\n }\r\n\r\n const raw = Array.isArray(response.data) ? response.data[0] : response.data;\r\n\r\n // Preserva resposta pix-pending (id: null, pending: true, pix: {...})\r\n // pra clientes poderem rotear o user pra fluxo dedicado de pix em vez\r\n // de tratar como subscription comum. Caso contrário, parseia como\r\n // Subscription (compatível com o comportamento antigo de devolver { id }).\r\n let parsed: UpdateSubscriptionPlanResponse | null = null;\r\n if (raw) {\r\n const pixResult = SubscriptionPendingPixResponseSchema.safeParse(raw);\r\n parsed = pixResult.success ? pixResult.data : SubscriptionSchema.parse(raw);\r\n }\r\n\r\n return {\r\n success: true,\r\n data: parsed,\r\n ...(response.client_secret ? { client_secret: response.client_secret } : {}),\r\n };\r\n }\r\n\r\n async updateSubscriptionPayment(\r\n subscriptionId: number | string,\r\n data: { id_card: string; payment_method: number },\r\n ): Promise<SuccessResult<void>> {\r\n await assertManagementPermission(\"manage_subscriptions\");\r\n const { id_account } = await getUserContext();\r\n\r\n const response = await api.apps.put<ApiActionResult<void>>(\r\n `/accounts/${id_account}/subscriptions/${subscriptionId}/action/payment`,\r\n data,\r\n );\r\n\r\n if (response.status === 0) {\r\n throw new ApiError(\r\n response.message || \"Erro ao atualizar método de pagamento\",\r\n \"UPDATE_SUBSCRIPTION_PAYMENT_FAILED\",\r\n 400,\r\n );\r\n }\r\n\r\n return { success: true };\r\n }\r\n}\r\n\r\nexport const subscriptionsService = new SubscriptionsService();\r\n"],"mappings":"AAAA,OAAO;AAEP,SAAS,WAAW;AACpB;AAAA,EACE;AAAA,OAKK;AACP,SAAS,wBAAwB;AACjC,SAAS,kCAAkC;AAC3C,SAAS,sBAAsB;AAC/B;AAAA,EAEE;AAAA,OAEK;AACP;AAAA,EAIE;AAAA,OACK;AACP;AAAA,EACE;AAAA,OAEK;
|
|
1
|
+
{"version":3,"sources":["../../../../src/modules/subscriptions/services/subscriptions.service.ts"],"sourcesContent":["import \"server-only\";\r\n\r\nimport { api } from \"../../../infra/api/client\";\r\nimport {\r\n ApiError,\r\n ApiActionResult,\r\n ApiPaginatedActionResult,\r\n PaginatedSuccessResult,\r\n SuccessResult,\r\n} from \"../../../infra/api/types\";\r\nimport { buildQueryParams } from \"../../../infra/utils/params\";\r\nimport { assertManagementPermission } from \"../../auth/utils/assert-management-permission\";\r\nimport { getUserContext } from \"../../auth/utils/get-user-context\";\r\nimport {\r\n Subscription,\r\n SubscriptionSchema,\r\n FindSubscriptionsParams,\r\n} from \"../types/subscription.type\";\r\nimport {\r\n type CalculateSubscriptionRequest,\r\n type CalculateSubscriptionResponse,\r\n type UpdateSubscriptionPlanRequest,\r\n CalculateSubscriptionResponseSchema,\r\n} from \"../types/calculate-subscription.type\";\r\nimport {\r\n SubscriptionPendingPixResponseSchema,\r\n type SubscriptionPendingPixResponse,\r\n} from \"../types/pix-pending.type\";\r\n\r\n/**\r\n * Timeout das mutations de assinatura que ESCREVEM (action/plan e action/payment).\r\n *\r\n * O default do ApiClient é 30s. Essas duas rotas caem no mesmo motor pesado do\r\n * backend (planEdit delega pra paymentEdit) — troca de método de pagamento, mandato\r\n * PIX recorrente Woovi, void-and-replace de fatura — e passam de 30s com alguma\r\n * frequência: em produção elas aparecem como `outcome=canceled` com wallTime ~29.9s.\r\n *\r\n * O problema não é o usuário ver \"Tempo excedido\": é que o abort MATA o worker no\r\n * meio da saga. Se o corte cai depois de criar o mandato Woovi e suprimir a fatura\r\n * de ciclo, sobra mandato órfão + fatura sem dunning, o usuário não recebe QR nenhum\r\n * e a próxima tentativa cria um segundo mandato. Deixar a request terminar converte\r\n * uma escrita parcial numa escrita completa.\r\n *\r\n * Não é a solução definitiva — a rota deveria ser assíncrona (202 + polling no\r\n * correlation_id). É o teto que dá folga enquanto isso.\r\n *\r\n * Só nas mutations: calculateSubscription é idempotente (não escreve nada), então\r\n * um timeout nela não deixa rastro e não precisa esperar mais.\r\n */\r\nconst SUBSCRIPTION_WRITE_TIMEOUT_MS = 60000;\r\n\r\nexport type UpdateSubscriptionPlanResponse =\r\n | Subscription\r\n | SubscriptionPendingPixResponse;\r\n\r\nclass SubscriptionsService {\r\n async listSubscriptions(\r\n params?: Partial<FindSubscriptionsParams>,\r\n ): Promise<PaginatedSuccessResult<Subscription>> {\r\n const { id_account } = await getUserContext();\r\n\r\n const query = buildQueryParams(\r\n params as Record<string, string | boolean | undefined>,\r\n );\r\n const url = `/accounts/${id_account}/subscriptions${query ? `?${query}` : \"\"}`;\r\n\r\n const response =\r\n await api.apps.get<ApiPaginatedActionResult<Subscription>>(url);\r\n\r\n if (response.status === 0) {\r\n throw new ApiError(\r\n response.message || \"Erro ao listar assinaturas\",\r\n \"LIST_SUBSCRIPTIONS_FAILED\",\r\n 400,\r\n );\r\n }\r\n\r\n const data = response.data.map((item) => SubscriptionSchema.parse(item));\r\n\r\n return {\r\n data,\r\n total: response.total,\r\n success: true,\r\n } satisfies PaginatedSuccessResult<Subscription>;\r\n }\r\n\r\n async calculateSubscription(\r\n data: CalculateSubscriptionRequest,\r\n ): Promise<SuccessResult<CalculateSubscriptionResponse>> {\r\n const { id_account } = await getUserContext();\r\n\r\n const response = await api.apps.post<\r\n ApiActionResult<CalculateSubscriptionResponse>\r\n >(`/accounts/${id_account}/subscriptions/action/calculate`, data);\r\n\r\n console.log(\"Calculate subscription response:\", {\r\n response,\r\n data: JSON.stringify(data, null, 2),\r\n });\r\n\r\n if (response.status === 0) {\r\n throw new ApiError(\r\n response.message || \"Erro ao calcular assinatura\",\r\n \"CALCULATE_SUBSCRIPTION_FAILED\",\r\n 400,\r\n );\r\n }\r\n\r\n return {\r\n success: true,\r\n data: CalculateSubscriptionResponseSchema.parse(response.data),\r\n };\r\n }\r\n\r\n async updateSubscriptionPlan(\r\n subscriptionId: number | string,\r\n data: UpdateSubscriptionPlanRequest,\r\n ): Promise<\r\n SuccessResult<UpdateSubscriptionPlanResponse | null> & { client_secret?: string }\r\n > {\r\n await assertManagementPermission('manage_subscriptions');\r\n const { id_account } = await getUserContext();\r\n\r\n const response = await api.apps.put<\r\n ApiActionResult<UpdateSubscriptionPlanResponse | UpdateSubscriptionPlanResponse[]> & {\r\n client_secret?: string;\r\n }\r\n >(\r\n `/accounts/${id_account}/subscriptions/${subscriptionId}/action/plan`,\r\n data,\r\n { timeout: SUBSCRIPTION_WRITE_TIMEOUT_MS },\r\n );\r\n\r\n if (response.status === 0) {\r\n throw new ApiError(\r\n response.message || \"Erro ao atualizar plano\",\r\n \"UPDATE_SUBSCRIPTION_PLAN_FAILED\",\r\n 400,\r\n );\r\n }\r\n\r\n const raw = Array.isArray(response.data) ? response.data[0] : response.data;\r\n\r\n // Preserva resposta pix-pending (id: null, pending: true, pix: {...})\r\n // pra clientes poderem rotear o user pra fluxo dedicado de pix em vez\r\n // de tratar como subscription comum. Caso contrário, parseia como\r\n // Subscription (compatível com o comportamento antigo de devolver { id }).\r\n let parsed: UpdateSubscriptionPlanResponse | null = null;\r\n if (raw) {\r\n const pixResult = SubscriptionPendingPixResponseSchema.safeParse(raw);\r\n parsed = pixResult.success ? pixResult.data : SubscriptionSchema.parse(raw);\r\n }\r\n\r\n return {\r\n success: true,\r\n data: parsed,\r\n ...(response.client_secret ? { client_secret: response.client_secret } : {}),\r\n };\r\n }\r\n\r\n async updateSubscriptionPayment(\r\n subscriptionId: number | string,\r\n data: { id_card: string; payment_method: number },\r\n ): Promise<SuccessResult<void>> {\r\n await assertManagementPermission(\"manage_subscriptions\");\r\n const { id_account } = await getUserContext();\r\n\r\n const response = await api.apps.put<ApiActionResult<void>>(\r\n `/accounts/${id_account}/subscriptions/${subscriptionId}/action/payment`,\r\n data,\r\n { timeout: SUBSCRIPTION_WRITE_TIMEOUT_MS },\r\n );\r\n\r\n if (response.status === 0) {\r\n throw new ApiError(\r\n response.message || \"Erro ao atualizar método de pagamento\",\r\n \"UPDATE_SUBSCRIPTION_PAYMENT_FAILED\",\r\n 400,\r\n );\r\n }\r\n\r\n return { success: true };\r\n }\r\n}\r\n\r\nexport const subscriptionsService = new SubscriptionsService();\r\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
Plan,
|
|
9
9
|
SuccessResult,
|
|
10
10
|
} from "@greatapps/common";
|
|
11
|
+
import greatCache from "@greatapps/cache";
|
|
11
12
|
|
|
12
13
|
export type ListPlansParams = {
|
|
13
14
|
sort?: string;
|
|
@@ -15,7 +16,26 @@ export type ListPlansParams = {
|
|
|
15
16
|
search?: string;
|
|
16
17
|
};
|
|
17
18
|
|
|
19
|
+
const PLANS_CACHE_TTL = 21600;
|
|
20
|
+
|
|
18
21
|
class PlansService {
|
|
22
|
+
private cache = new greatCache({
|
|
23
|
+
service: "plans",
|
|
24
|
+
version: "1.0",
|
|
25
|
+
domain: "whitelabel-cache.greatapps.com.br",
|
|
26
|
+
ambient: process.env.NODE_ENV || "development",
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
private buildCacheKey(key: string, params?: Record<string, unknown>): string {
|
|
30
|
+
const idWl = params?.id_wl ?? "";
|
|
31
|
+
const idAccount = params?.id_account ?? "";
|
|
32
|
+
const sort = params?.sort ?? "id:ASC";
|
|
33
|
+
const active = params?.active ?? "";
|
|
34
|
+
const search = params?.search ?? "";
|
|
35
|
+
// v6: descarta as entradas v5 que ficaram vivas durante a janela sem cache
|
|
36
|
+
return `${key}-${idWl}-${idAccount}-${sort}-${active}-${search}-v6`;
|
|
37
|
+
}
|
|
38
|
+
|
|
19
39
|
/**
|
|
20
40
|
* Lista planos do whitelabel.
|
|
21
41
|
* Exemplo:
|
|
@@ -24,7 +44,19 @@ class PlansService {
|
|
|
24
44
|
async listPlans(
|
|
25
45
|
params?: ListPlansParams,
|
|
26
46
|
): Promise<PaginatedSuccessResult<Plan>> {
|
|
27
|
-
const { id_account } = await getUserContext();
|
|
47
|
+
const { id_wl, id_account } = await getUserContext();
|
|
48
|
+
const cacheKey = this.buildCacheKey("plans", {
|
|
49
|
+
...params,
|
|
50
|
+
id_wl,
|
|
51
|
+
id_account,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const cachedData = await this.cache.select(cacheKey);
|
|
55
|
+
|
|
56
|
+
if (cachedData.status == 1 && "data" in cachedData && cachedData.data) {
|
|
57
|
+
const data = JSON.parse(cachedData.data) as Plan[];
|
|
58
|
+
return { data, total: data.length, success: true };
|
|
59
|
+
}
|
|
28
60
|
|
|
29
61
|
const query = buildQueryParams({
|
|
30
62
|
sort: params?.sort ?? "id:ASC",
|
|
@@ -49,6 +81,8 @@ class PlansService {
|
|
|
49
81
|
? rawData.map((item) => PlanSchema.parse(item))
|
|
50
82
|
: [];
|
|
51
83
|
|
|
84
|
+
await this.cache.insert(cacheKey, JSON.stringify(data), PLANS_CACHE_TTL);
|
|
85
|
+
|
|
52
86
|
return {
|
|
53
87
|
data,
|
|
54
88
|
total: response.total,
|
|
@@ -57,7 +91,18 @@ class PlansService {
|
|
|
57
91
|
}
|
|
58
92
|
|
|
59
93
|
async findById(idPlan: number | string): Promise<SuccessResult<Plan>> {
|
|
60
|
-
const { id_account } = await getUserContext();
|
|
94
|
+
const { id_wl, id_account } = await getUserContext();
|
|
95
|
+
const cacheKey = this.buildCacheKey(`plan-${idPlan}-v2`, {
|
|
96
|
+
id_wl,
|
|
97
|
+
id_account: String(id_account),
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const cachedData = await this.cache.select(cacheKey);
|
|
101
|
+
|
|
102
|
+
if (cachedData.status == 1 && "data" in cachedData && cachedData.data) {
|
|
103
|
+
const data = JSON.parse(cachedData.data) as Plan;
|
|
104
|
+
return { data, success: true };
|
|
105
|
+
}
|
|
61
106
|
|
|
62
107
|
const query = buildQueryParams({ type: "client", id_account });
|
|
63
108
|
|
|
@@ -79,6 +124,8 @@ class PlansService {
|
|
|
79
124
|
|
|
80
125
|
const plan = PlanSchema.parse(response.data[0]);
|
|
81
126
|
|
|
127
|
+
await this.cache.insert(cacheKey, JSON.stringify(plan), PLANS_CACHE_TTL);
|
|
128
|
+
|
|
82
129
|
return {
|
|
83
130
|
success: true,
|
|
84
131
|
data: plan,
|
|
@@ -27,6 +27,28 @@ import {
|
|
|
27
27
|
type SubscriptionPendingPixResponse,
|
|
28
28
|
} from "../types/pix-pending.type";
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Timeout das mutations de assinatura que ESCREVEM (action/plan e action/payment).
|
|
32
|
+
*
|
|
33
|
+
* O default do ApiClient é 30s. Essas duas rotas caem no mesmo motor pesado do
|
|
34
|
+
* backend (planEdit delega pra paymentEdit) — troca de método de pagamento, mandato
|
|
35
|
+
* PIX recorrente Woovi, void-and-replace de fatura — e passam de 30s com alguma
|
|
36
|
+
* frequência: em produção elas aparecem como `outcome=canceled` com wallTime ~29.9s.
|
|
37
|
+
*
|
|
38
|
+
* O problema não é o usuário ver "Tempo excedido": é que o abort MATA o worker no
|
|
39
|
+
* meio da saga. Se o corte cai depois de criar o mandato Woovi e suprimir a fatura
|
|
40
|
+
* de ciclo, sobra mandato órfão + fatura sem dunning, o usuário não recebe QR nenhum
|
|
41
|
+
* e a próxima tentativa cria um segundo mandato. Deixar a request terminar converte
|
|
42
|
+
* uma escrita parcial numa escrita completa.
|
|
43
|
+
*
|
|
44
|
+
* Não é a solução definitiva — a rota deveria ser assíncrona (202 + polling no
|
|
45
|
+
* correlation_id). É o teto que dá folga enquanto isso.
|
|
46
|
+
*
|
|
47
|
+
* Só nas mutations: calculateSubscription é idempotente (não escreve nada), então
|
|
48
|
+
* um timeout nela não deixa rastro e não precisa esperar mais.
|
|
49
|
+
*/
|
|
50
|
+
const SUBSCRIPTION_WRITE_TIMEOUT_MS = 60000;
|
|
51
|
+
|
|
30
52
|
export type UpdateSubscriptionPlanResponse =
|
|
31
53
|
| Subscription
|
|
32
54
|
| SubscriptionPendingPixResponse;
|
|
@@ -106,6 +128,7 @@ class SubscriptionsService {
|
|
|
106
128
|
>(
|
|
107
129
|
`/accounts/${id_account}/subscriptions/${subscriptionId}/action/plan`,
|
|
108
130
|
data,
|
|
131
|
+
{ timeout: SUBSCRIPTION_WRITE_TIMEOUT_MS },
|
|
109
132
|
);
|
|
110
133
|
|
|
111
134
|
if (response.status === 0) {
|
|
@@ -145,6 +168,7 @@ class SubscriptionsService {
|
|
|
145
168
|
const response = await api.apps.put<ApiActionResult<void>>(
|
|
146
169
|
`/accounts/${id_account}/subscriptions/${subscriptionId}/action/payment`,
|
|
147
170
|
data,
|
|
171
|
+
{ timeout: SUBSCRIPTION_WRITE_TIMEOUT_MS },
|
|
148
172
|
);
|
|
149
173
|
|
|
150
174
|
if (response.status === 0) {
|