@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.
@@ -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;AAcvD,MAAM,aAAa;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMjB,MAAM,UACJ,QACuC;AACvC,UAAM,EAAE,WAAW,IAAI,MAAM,eAAe;AAE5C,UAAM,QAAQ,iBAAiB;AAAA,MAC7B,MAAM,QAAQ,QAAQ;AAAA,MACtB,QAAQ,QAAQ;AAAA,MAChB,QAAQ,QAAQ;AAAA,MAChB;AAAA,IACF,CAAC;AACD,UAAM,MAAM,SAAS,QAAQ,IAAI,KAAK,KAAK,EAAE;AAE7C,UAAM,WAAW,MAAM,IAAI,KAAK,IAAsC,GAAG;AAEzE,QAAI,SAAS,WAAW,GAAG;AACzB,YAAM,IAAI;AAAA,QACP,SAAkC,WAAW;AAAA,QAC9C;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAEA,UAAM,UAAW,SAAgC;AACjD,UAAM,OAAO,MAAM,QAAQ,OAAO,IAC9B,QAAQ,IAAI,CAAC,SAAS,WAAW,MAAM,IAAI,CAAC,IAC5C,CAAC;AAEL,WAAO;AAAA,MACL;AAAA,MACA,OAAO,SAAS;AAAA,MAChB,SAAS;AAAA,IACX;AAAA,EACF;AAAA,EAEA,MAAM,SAAS,QAAuD;AACpE,UAAM,EAAE,WAAW,IAAI,MAAM,eAAe;AAE5C,UAAM,QAAQ,iBAAiB,EAAE,MAAM,UAAU,WAAW,CAAC;AAE7D,UAAM,WAAW,MAAM,IAAI,KAAK;AAAA,MAC9B,UAAU,MAAM,IAAI,KAAK;AAAA,IAC3B;AAEA,QAAI,SAAS,WAAW,GAAG;AACzB,YAAM,IAAI;AAAA,QACR,SAAS,WAAW;AAAA,QACpB;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAEA,QAAI,CAAC,SAAS,MAAM,QAAQ;AAC1B,YAAM,IAAI,SAAS,2BAAwB,kBAAkB,GAAG;AAAA,IAClE;AAEA,UAAM,OAAO,WAAW,MAAM,SAAS,KAAK,CAAC,CAAC;AAE9C,WAAO;AAAA,MACL,SAAS;AAAA,MACT,MAAM;AAAA,IACR;AAAA,EACF;AACF;AAEO,MAAM,eAAe,IAAI,aAAa;","names":[]}
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":"AAAA,OAAO;AAEP,SAAS,KAAK,sBAAsB;AACpC,SAAS,UAAU,kBAAkB,kBAAkB;AAOvD,OAAO,gBAAgB;AAQvB,MAAM,kBAAkB;AAExB,MAAM,aAAa;AAAA,EACT,QAAQ,IAAI,WAAW;AAAA,IAC7B,SAAS;AAAA,IACT,SAAS;AAAA,IACT,QAAQ;AAAA,IACR,SAAS,QAAQ,IAAI,YAAY;AAAA,EACnC,CAAC;AAAA,EAEO,cAAc,KAAa,QAA0C;AAC3E,UAAM,OAAO,QAAQ,SAAS;AAC9B,UAAM,YAAY,QAAQ,cAAc;AACxC,UAAM,OAAO,QAAQ,QAAQ;AAC7B,UAAM,SAAS,QAAQ,UAAU;AACjC,UAAM,SAAS,QAAQ,UAAU;AAEjC,WAAO,GAAG,GAAG,IAAI,IAAI,IAAI,SAAS,IAAI,IAAI,IAAI,MAAM,IAAI,MAAM;AAAA,EAChE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,UACJ,QACuC;AACvC,UAAM,EAAE,OAAO,WAAW,IAAI,MAAM,eAAe;AACnD,UAAM,WAAW,KAAK,cAAc,SAAS;AAAA,MAC3C,GAAG;AAAA,MACH;AAAA,MACA;AAAA,IACF,CAAC;AAED,UAAM,aAAa,MAAM,KAAK,MAAM,OAAO,QAAQ;AAEnD,QAAI,WAAW,UAAU,KAAK,UAAU,cAAc,WAAW,MAAM;AACrE,YAAMA,QAAO,KAAK,MAAM,WAAW,IAAI;AACvC,aAAO,EAAE,MAAAA,OAAM,OAAOA,MAAK,QAAQ,SAAS,KAAK;AAAA,IACnD;AAEA,UAAM,QAAQ,iBAAiB;AAAA,MAC7B,MAAM,QAAQ,QAAQ;AAAA,MACtB,QAAQ,QAAQ;AAAA,MAChB,QAAQ,QAAQ;AAAA,MAChB;AAAA,IACF,CAAC;AACD,UAAM,MAAM,SAAS,QAAQ,IAAI,KAAK,KAAK,EAAE;AAE7C,UAAM,WAAW,MAAM,IAAI,KAAK,IAAsC,GAAG;AAEzE,QAAI,SAAS,WAAW,GAAG;AACzB,YAAM,IAAI;AAAA,QACP,SAAkC,WAAW;AAAA,QAC9C;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAEA,UAAM,UAAW,SAAgC;AACjD,UAAM,OAAO,MAAM,QAAQ,OAAO,IAC9B,QAAQ,IAAI,CAAC,SAAS,WAAW,MAAM,IAAI,CAAC,IAC5C,CAAC;AAEL,UAAM,KAAK,MAAM,OAAO,UAAU,KAAK,UAAU,IAAI,GAAG,eAAe;AAEvE,WAAO;AAAA,MACL;AAAA,MACA,OAAO,SAAS;AAAA,MAChB,SAAS;AAAA,IACX;AAAA,EACF;AAAA,EAEA,MAAM,SAAS,QAAuD;AACpE,UAAM,EAAE,OAAO,WAAW,IAAI,MAAM,eAAe;AACnD,UAAM,WAAW,KAAK,cAAc,QAAQ,MAAM,OAAO;AAAA,MACvD;AAAA,MACA,YAAY,OAAO,UAAU;AAAA,IAC/B,CAAC;AAED,UAAM,aAAa,MAAM,KAAK,MAAM,OAAO,QAAQ;AAEnD,QAAI,WAAW,UAAU,KAAK,UAAU,cAAc,WAAW,MAAM;AACrE,YAAM,OAAO,KAAK,MAAM,WAAW,IAAI;AACvC,aAAO,EAAE,MAAM,SAAS,KAAK;AAAA,IAC/B;AAEA,UAAM,QAAQ,iBAAiB,EAAE,MAAM,UAAU,WAAW,CAAC;AAE7D,UAAM,WAAW,MAAM,IAAI,KAAK;AAAA,MAC9B,UAAU,MAAM,IAAI,KAAK;AAAA,IAC3B;AAEA,QAAI,SAAS,WAAW,GAAG;AACzB,YAAM,IAAI;AAAA,QACR,SAAS,WAAW;AAAA,QACpB;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAEA,QAAI,CAAC,SAAS,MAAM,QAAQ;AAC1B,YAAM,IAAI,SAAS,2BAAwB,kBAAkB,GAAG;AAAA,IAClE;AAEA,UAAM,OAAO,WAAW,MAAM,SAAS,KAAK,CAAC,CAAC;AAE9C,UAAM,KAAK,MAAM,OAAO,UAAU,KAAK,UAAU,IAAI,GAAG,eAAe;AAEvE,WAAO;AAAA,MACL,SAAS;AAAA,MACT,MAAM;AAAA,IACR;AAAA,EACF;AACF;AAEO,MAAM,eAAe,IAAI,aAAa;","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;AAMP,MAAM,qBAAqB;AAAA,EACzB,MAAM,kBACJ,QAC+C;AAC/C,UAAM,EAAE,WAAW,IAAI,MAAM,eAAe;AAE5C,UAAM,QAAQ;AAAA,MACZ;AAAA,IACF;AACA,UAAM,MAAM,aAAa,UAAU,iBAAiB,QAAQ,IAAI,KAAK,KAAK,EAAE;AAE5E,UAAM,WACJ,MAAM,IAAI,KAAK,IAA4C,GAAG;AAEhE,QAAI,SAAS,WAAW,GAAG;AACzB,YAAM,IAAI;AAAA,QACR,SAAS,WAAW;AAAA,QACpB;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAEA,UAAM,OAAO,SAAS,KAAK,IAAI,CAAC,SAAS,mBAAmB,MAAM,IAAI,CAAC;AAEvE,WAAO;AAAA,MACL;AAAA,MACA,OAAO,SAAS;AAAA,MAChB,SAAS;AAAA,IACX;AAAA,EACF;AAAA,EAEA,MAAM,sBACJ,MACuD;AACvD,UAAM,EAAE,WAAW,IAAI,MAAM,eAAe;AAE5C,UAAM,WAAW,MAAM,IAAI,KAAK,KAE9B,aAAa,UAAU,mCAAmC,IAAI;AAEhE,YAAQ,IAAI,oCAAoC;AAAA,MAC9C;AAAA,MACA,MAAM,KAAK,UAAU,MAAM,MAAM,CAAC;AAAA,IACpC,CAAC;AAED,QAAI,SAAS,WAAW,GAAG;AACzB,YAAM,IAAI;AAAA,QACR,SAAS,WAAW;AAAA,QACpB;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAEA,WAAO;AAAA,MACL,SAAS;AAAA,MACT,MAAM,oCAAoC,MAAM,SAAS,IAAI;AAAA,IAC/D;AAAA,EACF;AAAA,EAEA,MAAM,uBACJ,gBACA,MAGA;AACA,UAAM,2BAA2B,sBAAsB;AACvD,UAAM,EAAE,WAAW,IAAI,MAAM,eAAe;AAE5C,UAAM,WAAW,MAAM,IAAI,KAAK;AAAA,MAK9B,aAAa,UAAU,kBAAkB,cAAc;AAAA,MACvD;AAAA,IACF;AAEA,QAAI,SAAS,WAAW,GAAG;AACzB,YAAM,IAAI;AAAA,QACR,SAAS,WAAW;AAAA,QACpB;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAEA,UAAM,MAAM,MAAM,QAAQ,SAAS,IAAI,IAAI,SAAS,KAAK,CAAC,IAAI,SAAS;AAMvE,QAAI,SAAgD;AACpD,QAAI,KAAK;AACP,YAAM,YAAY,qCAAqC,UAAU,GAAG;AACpE,eAAS,UAAU,UAAU,UAAU,OAAO,mBAAmB,MAAM,GAAG;AAAA,IAC5E;AAEA,WAAO;AAAA,MACL,SAAS;AAAA,MACT,MAAM;AAAA,MACN,GAAI,SAAS,gBAAgB,EAAE,eAAe,SAAS,cAAc,IAAI,CAAC;AAAA,IAC5E;AAAA,EACF;AAAA,EAEA,MAAM,0BACJ,gBACA,MAC8B;AAC9B,UAAM,2BAA2B,sBAAsB;AACvD,UAAM,EAAE,WAAW,IAAI,MAAM,eAAe;AAE5C,UAAM,WAAW,MAAM,IAAI,KAAK;AAAA,MAC9B,aAAa,UAAU,kBAAkB,cAAc;AAAA,MACvD;AAAA,IACF;AAEA,QAAI,SAAS,WAAW,GAAG;AACzB,YAAM,IAAI;AAAA,QACR,SAAS,WAAW;AAAA,QACpB;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAEA,WAAO,EAAE,SAAS,KAAK;AAAA,EACzB;AACF;AAEO,MAAM,uBAAuB,IAAI,qBAAqB;","names":[]}
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@greatapps/common",
3
- "version": "1.1.735",
3
+ "version": "1.1.737",
4
4
  "description": "Shared library for GreatApps frontend applications",
5
5
  "main": "./dist/index.mjs",
6
6
  "types": "./src/index.ts",
@@ -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) {