@greatapps/common 1.1.796 → 1.1.798
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/modals/billing/BillingDataForm.mjs +21 -63
- package/dist/components/modals/billing/BillingDataForm.mjs.map +1 -1
- package/dist/i18n/messages/en-us.mjs +4 -7
- package/dist/i18n/messages/en-us.mjs.map +1 -1
- package/dist/i18n/messages/es-es.mjs +4 -7
- package/dist/i18n/messages/es-es.mjs.map +1 -1
- package/dist/i18n/messages/pt-br.mjs +4 -7
- package/dist/i18n/messages/pt-br.mjs.map +1 -1
- package/dist/modules/ia-credits/types/ai-credits-offer.type.mjs +0 -2
- package/dist/modules/ia-credits/types/ai-credits-offer.type.mjs.map +1 -1
- package/dist/modules/whitelabel/constants/whitelabel.constants.mjs +0 -2
- package/dist/modules/whitelabel/constants/whitelabel.constants.mjs.map +1 -1
- package/dist/modules/whitelabel/services/whitelabel.service.mjs +33 -17
- package/dist/modules/whitelabel/services/whitelabel.service.mjs.map +1 -1
- package/dist/testing/msw/session.handlers.mjs +1 -0
- package/dist/testing/msw/session.handlers.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/modals/billing/BillingDataForm.tsx +23 -86
- package/src/i18n/messages/en-us.ts +4 -7
- package/src/i18n/messages/es-es.ts +4 -7
- package/src/i18n/messages/pt-br.ts +4 -7
- package/src/modules/ia-credits/types/ai-credits-offer.type.ts +0 -2
- package/src/modules/whitelabel/constants/whitelabel.constants.ts +0 -9
- package/src/modules/whitelabel/services/whitelabel.service.ts +41 -23
- package/src/testing/msw/session.handlers.ts +1 -0
|
@@ -2,7 +2,7 @@ import greatCache from "@greatapps/cache";
|
|
|
2
2
|
import { ApiError } from "../../../infra/api/types";
|
|
3
3
|
import { normalizeHostname } from "../utils/normalize-hostname";
|
|
4
4
|
import {
|
|
5
|
-
|
|
5
|
+
DEFAULT_WHITELABEL_ID,
|
|
6
6
|
MISSING_WHITELABEL_CACHE_TTL_SECONDS,
|
|
7
7
|
WHITELABEL_CACHE_TTL_SECONDS
|
|
8
8
|
} from "../constants/whitelabel.constants";
|
|
@@ -43,6 +43,9 @@ class WhitelabelService {
|
|
|
43
43
|
getMissingCacheKey(hostname) {
|
|
44
44
|
return `whitelabel-missing-${hostname}`;
|
|
45
45
|
}
|
|
46
|
+
getDefaultCacheKey() {
|
|
47
|
+
return `whitelabel-token-id-${DEFAULT_WHITELABEL_ID}`;
|
|
48
|
+
}
|
|
46
49
|
async fetchFromApi(hostname) {
|
|
47
50
|
const apiUrl = this.getApiUrl();
|
|
48
51
|
const whitelabelMasterToken = this.getToken();
|
|
@@ -78,11 +81,6 @@ class WhitelabelService {
|
|
|
78
81
|
}
|
|
79
82
|
return result.data[0];
|
|
80
83
|
}
|
|
81
|
-
/**
|
|
82
|
-
* Cache (positivo) + fetch + validação de hostname pra um domínio já normalizado. Usado tanto
|
|
83
|
-
* pelo host da requisição quanto pelo domínio padrão — sem fallback embutido, pra `getDefault`
|
|
84
|
-
* não poder recursar nela mesma.
|
|
85
|
-
*/
|
|
86
84
|
async lookup(hostname) {
|
|
87
85
|
const cache = this.createCache();
|
|
88
86
|
const cacheKey = this.getCacheKey(hostname);
|
|
@@ -118,23 +116,40 @@ class WhitelabelService {
|
|
|
118
116
|
await cache.insert(cacheKey, JSON.stringify(data), WHITELABEL_CACHE_TTL_SECONDS);
|
|
119
117
|
return data;
|
|
120
118
|
}
|
|
121
|
-
/**
|
|
122
|
-
* Whitelabel padrão (Great), destino de todo host sem whitelabel própria. Resolve pelo mesmo
|
|
123
|
-
* `lookup`, então herda o cache positivo — mas nunca cai em `getTokenByDomain`, que teria
|
|
124
|
-
* fallback: aqui a ausência é falha de plataforma, não caso esperado.
|
|
125
|
-
*/
|
|
126
119
|
async getDefault() {
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
);
|
|
130
|
-
|
|
131
|
-
|
|
120
|
+
const cache = this.createCache();
|
|
121
|
+
const cacheKey = this.getDefaultCacheKey();
|
|
122
|
+
const cachedData = await cache.select(cacheKey);
|
|
123
|
+
if (cachedData.status == 1 && "data" in cachedData && cachedData.data) {
|
|
124
|
+
const cachedWhitelabel = JSON.parse(cachedData.data);
|
|
125
|
+
return cachedWhitelabel;
|
|
126
|
+
}
|
|
127
|
+
const apiUrl = this.getApiUrl();
|
|
128
|
+
const masterToken = this.getToken();
|
|
129
|
+
const url = `${apiUrl}/v1/pt-br/${DEFAULT_WHITELABEL_ID}/whitelabels/${DEFAULT_WHITELABEL_ID}`;
|
|
130
|
+
const response = await fetch(url, {
|
|
131
|
+
method: "GET",
|
|
132
|
+
headers: {
|
|
133
|
+
authorization: masterToken
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
if (!response.ok) {
|
|
132
137
|
throw new ApiError(
|
|
133
|
-
`Default whitelabel not found: ${
|
|
138
|
+
`Default whitelabel not found: ${DEFAULT_WHITELABEL_ID}`,
|
|
134
139
|
"WL_DEFAULT_UNAVAILABLE",
|
|
135
140
|
500
|
|
136
141
|
);
|
|
137
142
|
}
|
|
143
|
+
const result = await response.json();
|
|
144
|
+
if (result.status !== 1 || !result.data?.length) {
|
|
145
|
+
throw new ApiError(
|
|
146
|
+
`Default whitelabel not found: ${DEFAULT_WHITELABEL_ID}`,
|
|
147
|
+
"WL_DEFAULT_UNAVAILABLE",
|
|
148
|
+
500
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
const data = { ...result.data[0], token: masterToken };
|
|
152
|
+
await cache.insert(cacheKey, JSON.stringify(data), WHITELABEL_CACHE_TTL_SECONDS);
|
|
138
153
|
return data;
|
|
139
154
|
}
|
|
140
155
|
async getTokenByDomain(hostname) {
|
|
@@ -216,6 +231,7 @@ class WhitelabelService {
|
|
|
216
231
|
});
|
|
217
232
|
await cache.delete(cacheKey);
|
|
218
233
|
await cache.delete(missingCacheKey);
|
|
234
|
+
await cache.delete(this.getDefaultCacheKey());
|
|
219
235
|
const data = await this.getTokenByDomain(hostname);
|
|
220
236
|
console.log("[WhitelabelService] Cache revalidated for domain", {
|
|
221
237
|
hostname
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/modules/whitelabel/services/whitelabel.service.ts"],"sourcesContent":["import greatCache from \"@greatapps/cache\";\nimport { ApiError } from \"../../../infra/api/types\";\nimport { WhitelabelTokenApiResponse, WhitelabelTokenData } from \"../schema\";\nimport { normalizeHostname } from \"../utils/normalize-hostname\";\nimport {\n DEFAULT_WHITELABEL_DOMAIN,\n MISSING_WHITELABEL_CACHE_TTL_SECONDS,\n WHITELABEL_CACHE_TTL_SECONDS,\n} from \"../constants/whitelabel.constants\";\n\nclass WhitelabelService {\n private getApiUrl(): string {\n const apiUrl = process.env.GAPPS_R3_API_URL;\n if (!apiUrl) {\n throw new ApiError(\n \"GAPPS_R3_API_URL not configured\",\n \"CONFIG_ERROR\",\n 500,\n );\n }\n return apiUrl;\n }\n\n private getToken(): string {\n const token = process.env.WHITELABEL_TOKEN_MASTER;\n if (!token) {\n throw new ApiError(\n \"WHITELABEL_TOKEN_MASTER not configured\",\n \"CONFIG_ERROR\",\n 500,\n );\n }\n return token;\n }\n\n private createCache() {\n return new greatCache({\n service: \"whitelabel-service\",\n version: \"1.0.3\",\n domain: \"whitelabel-cache.greatapps.com.br\",\n ambient: process.env.NODE_ENV || \"development\",\n });\n }\n\n private getCacheKey(hostname: string): string {\n return `whitelabel-token-${hostname}`;\n }\n\n private getMissingCacheKey(hostname: string): string {\n return `whitelabel-missing-${hostname}`;\n }\n\n private async fetchFromApi(hostname: string): Promise<WhitelabelTokenData | null> {\n const apiUrl = this.getApiUrl();\n const whitelabelMasterToken = this.getToken();\n const url = `${apiUrl}/v1/pt-br/1/whitelabel/${hostname}/token`;\n\n console.log(\"[WhitelabelService] Fetching token for domain\", { url });\n\n const response = await fetch(url, {\n method: \"GET\",\n headers: {\n authorization: whitelabelMasterToken,\n },\n });\n\n // Host sem whitelabel ativa é caso esperado (domínio novo, subdomínio livre) — não é falha de\n // rede, então nem lê o corpo: cai no fallback do whitelabel padrão. 403 NÃO entra aqui: a rota\n // emissora nunca responde 403, quem responde é borda (WAF, Access, rate limit) — tratar como\n // ausência de whitelabel faria um incidente de borda derrubar toda whitelabel de cliente no\n // whitelabel padrão, servindo branding e token de API errados em silêncio.\n if (response.status === 404) {\n console.log(\"[WhitelabelService] No whitelabel for domain\", {\n hostname,\n status: response.status,\n });\n return null;\n }\n\n if (!response.ok) {\n console.error(\"[WhitelabelService] Failed to fetch whitelabel token\", {\n hostname,\n status: response.status,\n });\n throw new ApiError(\n `Failed to fetch whitelabel token: ${response.status}`,\n \"FETCH_ERROR\",\n response.status,\n );\n }\n\n const result: WhitelabelTokenApiResponse = await response.json();\n\n if (result.status !== 1 || !result.data?.length) {\n return null;\n }\n\n return result.data[0];\n }\n\n /**\n * Cache (positivo) + fetch + validação de hostname pra um domínio já normalizado. Usado tanto\n * pelo host da requisição quanto pelo domínio padrão — sem fallback embutido, pra `getDefault`\n * não poder recursar nela mesma.\n */\n private async lookup(hostname: string): Promise<WhitelabelTokenData | null> {\n const cache = this.createCache();\n const cacheKey = this.getCacheKey(hostname);\n\n const cachedData = await cache.select(cacheKey);\n console.debug(\"[WhitelabelService] Cache lookup for domain\", {\n cacheKey,\n cachedData,\n });\n\n if (cachedData.status == 1 && \"data\" in cachedData && cachedData.data) {\n const cachedWhitelabel: WhitelabelTokenData = JSON.parse(cachedData.data);\n\n if (this.#matchesHostname(cachedWhitelabel, hostname)) {\n console.log(\"[WhitelabelService] Cache hit for domain\", { hostname });\n return cachedWhitelabel;\n }\n\n console.error(\"[WhitelabelService] Cached whitelabel does not match hostname\", {\n hostname,\n cacheKey,\n whitelabelId: cachedWhitelabel.id,\n whitelabelDomain: cachedWhitelabel.domain,\n });\n\n await cache.delete(cacheKey);\n }\n\n const data = await this.fetchFromApi(hostname);\n if (!data) return null;\n\n if (!this.#matchesHostname(data, hostname)) {\n console.error(\"[WhitelabelService] API returned whitelabel of another hostname\", {\n hostname,\n whitelabelId: data.id,\n whitelabelDomain: data.domain,\n });\n return data;\n }\n\n await cache.insert(cacheKey, JSON.stringify(data), WHITELABEL_CACHE_TTL_SECONDS);\n\n return data;\n }\n\n /**\n * Whitelabel padrão (Great), destino de todo host sem whitelabel própria. Resolve pelo mesmo\n * `lookup`, então herda o cache positivo — mas nunca cai em `getTokenByDomain`, que teria\n * fallback: aqui a ausência é falha de plataforma, não caso esperado.\n */\n private async getDefault(): Promise<WhitelabelTokenData> {\n const domain = normalizeHostname(\n process.env.WHITELABEL_DEFAULT_DOMAIN || DEFAULT_WHITELABEL_DOMAIN,\n );\n\n const data = await this.lookup(domain);\n\n if (!data) {\n throw new ApiError(\n `Default whitelabel not found: ${domain}`,\n \"WL_DEFAULT_UNAVAILABLE\",\n 500,\n );\n }\n\n return data;\n }\n\n async getTokenByDomain(hostname: string): Promise<WhitelabelTokenData> {\n hostname = `${normalizeHostname(process.env.WHITELABEL_DOMAIN || hostname)}`;\n console.debug(\"[WhitelabelService] Getting token for domain\", {\n hostname,\n byEnv: process.env.WHITELABEL_DOMAIN,\n });\n\n const cache = this.createCache();\n const missingCacheKey = this.getMissingCacheKey(hostname);\n\n const cachedMissing = await cache.select(missingCacheKey);\n if (cachedMissing.status == 1) {\n console.log(\"[WhitelabelService] Cached miss for domain, using default\", { hostname });\n return this.getDefault();\n }\n\n const data = await this.lookup(hostname);\n if (data) return data;\n\n console.log(\"[WhitelabelService] No whitelabel for domain, falling back to default\", {\n hostname,\n });\n await cache.insert(missingCacheKey, \"1\", MISSING_WHITELABEL_CACHE_TTL_SECONDS);\n\n return this.getDefault();\n }\n\n #matchesHostname(data: WhitelabelTokenData, hostname: string): boolean {\n if (!data.domain) return true;\n\n try {\n return normalizeHostname(new URL(data.domain).hostname) === hostname;\n } catch {\n return true;\n }\n }\n\n async getTokenByWhitelabelId(idWl: number): Promise<string> {\n const apiUrl = this.getApiUrl();\n const masterToken = this.getToken();\n const url = `${apiUrl}/v1/pt-br/${idWl}/tokens?limit=1&page=1&sort=id:desc`;\n console.debug(\"[WhitelabelService] Fetching token by whitelabel ID\", {\n url,\n config: {\n method: \"GET\",\n headers: { authorization: masterToken },\n },\n });\n\n const response = await fetch(url, {\n method: \"GET\",\n headers: { authorization: masterToken },\n });\n\n console.debug(\"[WhitelabelService] Response received for whitelabel ID\", {\n response,\n });\n\n if (!response.ok) {\n throw new ApiError(\n `Failed to fetch whitelabel token: ${response.status}`,\n \"WL_TOKEN_NOT_FOUND\",\n response.status,\n );\n }\n\n const result: WhitelabelTokenApiResponse = await response.json();\n\n if (result.status !== 1 || !result.data?.length) {\n throw new ApiError(\n \"Token do whitelabel não encontrado\",\n \"WL_TOKEN_NOT_FOUND\",\n 404,\n );\n }\n\n return result.data[0].token;\n }\n\n async revalidateByDomain(hostname: string): Promise<WhitelabelTokenData> {\n hostname = `${normalizeHostname(process.env.WHITELABEL_DOMAIN || hostname)}`;\n console.debug(\"[WhitelabelService] Getting token for domain\", {\n hostname,\n byEnv: process.env.WHITELABEL_DOMAIN,\n });\n\n const cache = this.createCache();\n const cacheKey = this.getCacheKey(hostname);\n const missingCacheKey = this.getMissingCacheKey(hostname);\n\n console.log(\"[WhitelabelService] Revalidating cache for domain\", {\n hostname,\n cacheKey,\n });\n\n await cache.delete(cacheKey);\n await cache.delete(missingCacheKey);\n\n const data = await this.getTokenByDomain(hostname);\n\n console.log(\"[WhitelabelService] Cache revalidated for domain\", {\n hostname,\n });\n\n return data;\n }\n}\n\nexport const whitelabelService = new WhitelabelService();\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../../src/modules/whitelabel/services/whitelabel.service.ts"],"sourcesContent":["import greatCache from \"@greatapps/cache\";\nimport { ApiError } from \"../../../infra/api/types\";\nimport { WhitelabelApiResponse, WhitelabelTokenApiResponse, WhitelabelTokenData } from \"../schema\";\nimport { normalizeHostname } from \"../utils/normalize-hostname\";\nimport {\n DEFAULT_WHITELABEL_ID,\n MISSING_WHITELABEL_CACHE_TTL_SECONDS,\n WHITELABEL_CACHE_TTL_SECONDS,\n} from \"../constants/whitelabel.constants\";\n\nclass WhitelabelService {\n private getApiUrl(): string {\n const apiUrl = process.env.GAPPS_R3_API_URL;\n if (!apiUrl) {\n throw new ApiError(\n \"GAPPS_R3_API_URL not configured\",\n \"CONFIG_ERROR\",\n 500,\n );\n }\n return apiUrl;\n }\n\n private getToken(): string {\n const token = process.env.WHITELABEL_TOKEN_MASTER;\n if (!token) {\n throw new ApiError(\n \"WHITELABEL_TOKEN_MASTER not configured\",\n \"CONFIG_ERROR\",\n 500,\n );\n }\n return token;\n }\n\n private createCache() {\n return new greatCache({\n service: \"whitelabel-service\",\n version: \"1.0.3\",\n domain: \"whitelabel-cache.greatapps.com.br\",\n ambient: process.env.NODE_ENV || \"development\",\n });\n }\n\n private getCacheKey(hostname: string): string {\n return `whitelabel-token-${hostname}`;\n }\n\n private getMissingCacheKey(hostname: string): string {\n return `whitelabel-missing-${hostname}`;\n }\n\n private getDefaultCacheKey(): string {\n return `whitelabel-token-id-${DEFAULT_WHITELABEL_ID}`;\n }\n\n private async fetchFromApi(hostname: string): Promise<WhitelabelTokenData | null> {\n const apiUrl = this.getApiUrl();\n const whitelabelMasterToken = this.getToken();\n const url = `${apiUrl}/v1/pt-br/1/whitelabel/${hostname}/token`;\n\n console.log(\"[WhitelabelService] Fetching token for domain\", { url });\n\n const response = await fetch(url, {\n method: \"GET\",\n headers: {\n authorization: whitelabelMasterToken,\n },\n });\n\n if (response.status === 404) {\n console.log(\"[WhitelabelService] No whitelabel for domain\", {\n hostname,\n status: response.status,\n });\n return null;\n }\n\n if (!response.ok) {\n console.error(\"[WhitelabelService] Failed to fetch whitelabel token\", {\n hostname,\n status: response.status,\n });\n throw new ApiError(\n `Failed to fetch whitelabel token: ${response.status}`,\n \"FETCH_ERROR\",\n response.status,\n );\n }\n\n const result: WhitelabelTokenApiResponse = await response.json();\n\n if (result.status !== 1 || !result.data?.length) {\n return null;\n }\n\n return result.data[0];\n }\n\n private async lookup(hostname: string): Promise<WhitelabelTokenData | null> {\n const cache = this.createCache();\n const cacheKey = this.getCacheKey(hostname);\n\n const cachedData = await cache.select(cacheKey);\n console.debug(\"[WhitelabelService] Cache lookup for domain\", {\n cacheKey,\n cachedData,\n });\n\n if (cachedData.status == 1 && \"data\" in cachedData && cachedData.data) {\n const cachedWhitelabel: WhitelabelTokenData = JSON.parse(cachedData.data);\n\n if (this.#matchesHostname(cachedWhitelabel, hostname)) {\n console.log(\"[WhitelabelService] Cache hit for domain\", { hostname });\n return cachedWhitelabel;\n }\n\n console.error(\"[WhitelabelService] Cached whitelabel does not match hostname\", {\n hostname,\n cacheKey,\n whitelabelId: cachedWhitelabel.id,\n whitelabelDomain: cachedWhitelabel.domain,\n });\n\n await cache.delete(cacheKey);\n }\n\n const data = await this.fetchFromApi(hostname);\n if (!data) return null;\n\n if (!this.#matchesHostname(data, hostname)) {\n console.error(\"[WhitelabelService] API returned whitelabel of another hostname\", {\n hostname,\n whitelabelId: data.id,\n whitelabelDomain: data.domain,\n });\n return data;\n }\n\n await cache.insert(cacheKey, JSON.stringify(data), WHITELABEL_CACHE_TTL_SECONDS);\n\n return data;\n }\n\n private async getDefault(): Promise<WhitelabelTokenData> {\n const cache = this.createCache();\n const cacheKey = this.getDefaultCacheKey();\n\n const cachedData = await cache.select(cacheKey);\n if (cachedData.status == 1 && \"data\" in cachedData && cachedData.data) {\n const cachedWhitelabel: WhitelabelTokenData = JSON.parse(cachedData.data);\n return cachedWhitelabel;\n }\n\n const apiUrl = this.getApiUrl();\n const masterToken = this.getToken();\n const url = `${apiUrl}/v1/pt-br/${DEFAULT_WHITELABEL_ID}/whitelabels/${DEFAULT_WHITELABEL_ID}`;\n\n const response = await fetch(url, {\n method: \"GET\",\n headers: {\n authorization: masterToken,\n },\n });\n\n if (!response.ok) {\n throw new ApiError(\n `Default whitelabel not found: ${DEFAULT_WHITELABEL_ID}`,\n \"WL_DEFAULT_UNAVAILABLE\",\n 500,\n );\n }\n\n const result: WhitelabelApiResponse = await response.json();\n\n if (result.status !== 1 || !result.data?.length) {\n throw new ApiError(\n `Default whitelabel not found: ${DEFAULT_WHITELABEL_ID}`,\n \"WL_DEFAULT_UNAVAILABLE\",\n 500,\n );\n }\n\n const data: WhitelabelTokenData = { ...result.data[0], token: masterToken };\n\n await cache.insert(cacheKey, JSON.stringify(data), WHITELABEL_CACHE_TTL_SECONDS);\n\n return data;\n }\n\n async getTokenByDomain(hostname: string): Promise<WhitelabelTokenData> {\n hostname = `${normalizeHostname(process.env.WHITELABEL_DOMAIN || hostname)}`;\n console.debug(\"[WhitelabelService] Getting token for domain\", {\n hostname,\n byEnv: process.env.WHITELABEL_DOMAIN,\n });\n\n const cache = this.createCache();\n const missingCacheKey = this.getMissingCacheKey(hostname);\n\n const cachedMissing = await cache.select(missingCacheKey);\n if (cachedMissing.status == 1) {\n console.log(\"[WhitelabelService] Cached miss for domain, using default\", { hostname });\n return this.getDefault();\n }\n\n const data = await this.lookup(hostname);\n if (data) return data;\n\n console.log(\"[WhitelabelService] No whitelabel for domain, falling back to default\", {\n hostname,\n });\n await cache.insert(missingCacheKey, \"1\", MISSING_WHITELABEL_CACHE_TTL_SECONDS);\n\n return this.getDefault();\n }\n\n #matchesHostname(data: WhitelabelTokenData, hostname: string): boolean {\n if (!data.domain) return true;\n\n try {\n return normalizeHostname(new URL(data.domain).hostname) === hostname;\n } catch {\n return true;\n }\n }\n\n async getTokenByWhitelabelId(idWl: number): Promise<string> {\n const apiUrl = this.getApiUrl();\n const masterToken = this.getToken();\n const url = `${apiUrl}/v1/pt-br/${idWl}/tokens?limit=1&page=1&sort=id:desc`;\n console.debug(\"[WhitelabelService] Fetching token by whitelabel ID\", {\n url,\n config: {\n method: \"GET\",\n headers: { authorization: masterToken },\n },\n });\n\n const response = await fetch(url, {\n method: \"GET\",\n headers: { authorization: masterToken },\n });\n\n console.debug(\"[WhitelabelService] Response received for whitelabel ID\", {\n response,\n });\n\n if (!response.ok) {\n throw new ApiError(\n `Failed to fetch whitelabel token: ${response.status}`,\n \"WL_TOKEN_NOT_FOUND\",\n response.status,\n );\n }\n\n const result: WhitelabelTokenApiResponse = await response.json();\n\n if (result.status !== 1 || !result.data?.length) {\n throw new ApiError(\n \"Token do whitelabel não encontrado\",\n \"WL_TOKEN_NOT_FOUND\",\n 404,\n );\n }\n\n return result.data[0].token;\n }\n\n async revalidateByDomain(hostname: string): Promise<WhitelabelTokenData> {\n hostname = `${normalizeHostname(process.env.WHITELABEL_DOMAIN || hostname)}`;\n console.debug(\"[WhitelabelService] Getting token for domain\", {\n hostname,\n byEnv: process.env.WHITELABEL_DOMAIN,\n });\n\n const cache = this.createCache();\n const cacheKey = this.getCacheKey(hostname);\n const missingCacheKey = this.getMissingCacheKey(hostname);\n\n console.log(\"[WhitelabelService] Revalidating cache for domain\", {\n hostname,\n cacheKey,\n });\n\n await cache.delete(cacheKey);\n await cache.delete(missingCacheKey);\n await cache.delete(this.getDefaultCacheKey());\n\n const data = await this.getTokenByDomain(hostname);\n\n console.log(\"[WhitelabelService] Cache revalidated for domain\", {\n hostname,\n });\n\n return data;\n }\n}\n\nexport const whitelabelService = new WhitelabelService();\n"],"mappings":"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","names":[]}
|
|
@@ -11,6 +11,7 @@ function makeSessionHandlers(opts = {}) {
|
|
|
11
11
|
const whitelabel = opts.whitelabel ?? createWhitelabelTokenMock();
|
|
12
12
|
return [
|
|
13
13
|
http.get(`${base}/v1/:locale/:wl/whitelabel/:host/token`, () => apiFind(whitelabel)),
|
|
14
|
+
http.get(`${base}/v1/:locale/:wl/whitelabels/:id`, () => apiFind(whitelabel)),
|
|
14
15
|
http.get(`${base}/v1/:locale/:wl/tokens`, () => apiFind(whitelabel)),
|
|
15
16
|
// The users path has more segments, so it must come before /accounts/:acc.
|
|
16
17
|
http.get(`${base}/v1/:locale/:wl/accounts/:acc/users/:user`, () => apiFind(user)),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/testing/msw/session.handlers.ts"],"sourcesContent":["import { http } from 'msw';\nimport type { Account } from '../../modules/accounts/types';\nimport type { User } from '../../modules/users/schema';\nimport type { WhitelabelTokenData } from '../../modules/whitelabel/schema';\nimport { TEST_API } from '../constants';\nimport { createAccountMock } from '../factories/account.factory';\nimport { createUserMock } from '../factories/user.factory';\nimport { createWhitelabelTokenMock } from '../factories/whitelabel.factory';\nimport { apiFind } from './envelope';\n\nexport interface SessionHandlersOptions {\n gappsBase?: string;\n user?: User;\n account?: Account;\n whitelabel?: WhitelabelTokenData;\n}\n\n// Server-side session resolution any app triggers when booting an authenticated route with\n// DUMMY_AUTH_TOKEN (whitelabel by domain, token by wl id, user, account). Compose with app\n// handlers: setupServer(...makeSessionHandlers(), ...myPageHandlers).\nexport function makeSessionHandlers(opts: SessionHandlersOptions = {}) {\n const base = opts.gappsBase ?? TEST_API.gapps;\n const user = opts.user ?? createUserMock();\n const account = opts.account ?? createAccountMock();\n const whitelabel = opts.whitelabel ?? createWhitelabelTokenMock();\n\n return [\n http.get(`${base}/v1/:locale/:wl/whitelabel/:host/token`, () => apiFind(whitelabel)),\n http.get(`${base}/v1/:locale/:wl/tokens`, () => apiFind(whitelabel)),\n // The users path has more segments, so it must come before /accounts/:acc.\n http.get(`${base}/v1/:locale/:wl/accounts/:acc/users/:user`, () => apiFind(user)),\n http.get(`${base}/v1/:locale/:wl/accounts/:acc`, () => apiFind(account)),\n ];\n}\n"],"mappings":"AAAA,SAAS,YAAY;AAIrB,SAAS,gBAAgB;AACzB,SAAS,yBAAyB;AAClC,SAAS,sBAAsB;AAC/B,SAAS,iCAAiC;AAC1C,SAAS,eAAe;AAYjB,SAAS,oBAAoB,OAA+B,CAAC,GAAG;AACrE,QAAM,OAAO,KAAK,aAAa,SAAS;AACxC,QAAM,OAAO,KAAK,QAAQ,eAAe;AACzC,QAAM,UAAU,KAAK,WAAW,kBAAkB;AAClD,QAAM,aAAa,KAAK,cAAc,0BAA0B;AAEhE,SAAO;AAAA,IACL,KAAK,IAAI,GAAG,IAAI,0CAA0C,MAAM,QAAQ,UAAU,CAAC;AAAA,IACnF,KAAK,IAAI,GAAG,IAAI,0BAA0B,MAAM,QAAQ,UAAU,CAAC;AAAA;AAAA,IAEnE,KAAK,IAAI,GAAG,IAAI,6CAA6C,MAAM,QAAQ,IAAI,CAAC;AAAA,IAChF,KAAK,IAAI,GAAG,IAAI,iCAAiC,MAAM,QAAQ,OAAO,CAAC;AAAA,EACzE;AACF;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/testing/msw/session.handlers.ts"],"sourcesContent":["import { http } from 'msw';\nimport type { Account } from '../../modules/accounts/types';\nimport type { User } from '../../modules/users/schema';\nimport type { WhitelabelTokenData } from '../../modules/whitelabel/schema';\nimport { TEST_API } from '../constants';\nimport { createAccountMock } from '../factories/account.factory';\nimport { createUserMock } from '../factories/user.factory';\nimport { createWhitelabelTokenMock } from '../factories/whitelabel.factory';\nimport { apiFind } from './envelope';\n\nexport interface SessionHandlersOptions {\n gappsBase?: string;\n user?: User;\n account?: Account;\n whitelabel?: WhitelabelTokenData;\n}\n\n// Server-side session resolution any app triggers when booting an authenticated route with\n// DUMMY_AUTH_TOKEN (whitelabel by domain, token by wl id, user, account). Compose with app\n// handlers: setupServer(...makeSessionHandlers(), ...myPageHandlers).\nexport function makeSessionHandlers(opts: SessionHandlersOptions = {}) {\n const base = opts.gappsBase ?? TEST_API.gapps;\n const user = opts.user ?? createUserMock();\n const account = opts.account ?? createAccountMock();\n const whitelabel = opts.whitelabel ?? createWhitelabelTokenMock();\n\n return [\n http.get(`${base}/v1/:locale/:wl/whitelabel/:host/token`, () => apiFind(whitelabel)),\n http.get(`${base}/v1/:locale/:wl/whitelabels/:id`, () => apiFind(whitelabel)),\n http.get(`${base}/v1/:locale/:wl/tokens`, () => apiFind(whitelabel)),\n // The users path has more segments, so it must come before /accounts/:acc.\n http.get(`${base}/v1/:locale/:wl/accounts/:acc/users/:user`, () => apiFind(user)),\n http.get(`${base}/v1/:locale/:wl/accounts/:acc`, () => apiFind(account)),\n ];\n}\n"],"mappings":"AAAA,SAAS,YAAY;AAIrB,SAAS,gBAAgB;AACzB,SAAS,yBAAyB;AAClC,SAAS,sBAAsB;AAC/B,SAAS,iCAAiC;AAC1C,SAAS,eAAe;AAYjB,SAAS,oBAAoB,OAA+B,CAAC,GAAG;AACrE,QAAM,OAAO,KAAK,aAAa,SAAS;AACxC,QAAM,OAAO,KAAK,QAAQ,eAAe;AACzC,QAAM,UAAU,KAAK,WAAW,kBAAkB;AAClD,QAAM,aAAa,KAAK,cAAc,0BAA0B;AAEhE,SAAO;AAAA,IACL,KAAK,IAAI,GAAG,IAAI,0CAA0C,MAAM,QAAQ,UAAU,CAAC;AAAA,IACnF,KAAK,IAAI,GAAG,IAAI,mCAAmC,MAAM,QAAQ,UAAU,CAAC;AAAA,IAC5E,KAAK,IAAI,GAAG,IAAI,0BAA0B,MAAM,QAAQ,UAAU,CAAC;AAAA;AAAA,IAEnE,KAAK,IAAI,GAAG,IAAI,6CAA6C,MAAM,QAAQ,IAAI,CAAC;AAAA,IAChF,KAAK,IAAI,GAAG,IAAI,iCAAiC,MAAM,QAAQ,OAAO,CAAC;AAAA,EACzE;AACF;","names":[]}
|
package/package.json
CHANGED
|
@@ -20,7 +20,7 @@ import { useUpdateBillingData } from '../../../modules/accounts/hooks/useAccount
|
|
|
20
20
|
import { COUNTRIES } from '../../../utils/countries';
|
|
21
21
|
import { readGeoCountry } from '../../../utils/geo-country';
|
|
22
22
|
import { BR_STATE_OPTIONS } from '../../../utils/constants/br-states';
|
|
23
|
-
import {
|
|
23
|
+
import { formatCPFCNPJ, formatPostalCode } from '../../../utils/format/masks';
|
|
24
24
|
import { isValidCNPJ, isValidCPF } from '../../../utils/validators/common';
|
|
25
25
|
import type { Account } from '../../../modules/accounts/types';
|
|
26
26
|
import type { UpdateBillingDataRequest } from '../../../modules/accounts/types';
|
|
@@ -28,7 +28,6 @@ import type { UpdateBillingDataRequest } from '../../../modules/accounts/types';
|
|
|
28
28
|
type TranslateFn = (key: any, values?: Record<string, string | number>) => string;
|
|
29
29
|
|
|
30
30
|
type BillingDataFormValues = {
|
|
31
|
-
personType: 'pf' | 'pj';
|
|
32
31
|
document: string;
|
|
33
32
|
financialName: string;
|
|
34
33
|
financialEmail: string;
|
|
@@ -55,9 +54,12 @@ const BR_REQUIRED_ADDRESS_FIELDS: readonly (keyof BillingDataFormValues)[] = [
|
|
|
55
54
|
'state',
|
|
56
55
|
];
|
|
57
56
|
|
|
57
|
+
function isCompanyDocument(document: string): boolean {
|
|
58
|
+
return document.replace(/\D/g, '').length > 11;
|
|
59
|
+
}
|
|
60
|
+
|
|
58
61
|
function buildBillingDataSchema(translate: TranslateFn) {
|
|
59
62
|
const base = z.object({
|
|
60
|
-
personType: z.enum(['pf', 'pj']),
|
|
61
63
|
document: z.string().trim(),
|
|
62
64
|
financialName: z
|
|
63
65
|
.string()
|
|
@@ -99,7 +101,7 @@ function buildBillingDataSchema(translate: TranslateFn) {
|
|
|
99
101
|
});
|
|
100
102
|
}
|
|
101
103
|
|
|
102
|
-
const isCompany = data.
|
|
104
|
+
const isCompany = isCompanyDocument(data.document);
|
|
103
105
|
const digits = data.document.replace(/\D/g, '');
|
|
104
106
|
|
|
105
107
|
if (digits.length !== (isCompany ? 14 : 11)) {
|
|
@@ -113,8 +115,6 @@ function buildBillingDataSchema(translate: TranslateFn) {
|
|
|
113
115
|
return;
|
|
114
116
|
}
|
|
115
117
|
|
|
116
|
-
/* Dígito verificador, mesmo algoritmo do backend: sem isso 111.111.111-11 passa
|
|
117
|
-
* aqui e o PUT da conta é recusado depois, sem o usuário saber o motivo. */
|
|
118
118
|
const isValid = isCompany ? isValidCNPJ(digits) : isValidCPF(digits);
|
|
119
119
|
if (!isValid) {
|
|
120
120
|
ctx.addIssue({
|
|
@@ -128,25 +128,13 @@ function buildBillingDataSchema(translate: TranslateFn) {
|
|
|
128
128
|
});
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
-
function formatDocument(document: string | null | undefined, isCompany: boolean) {
|
|
132
|
-
if (!document) return '';
|
|
133
|
-
return isCompany ? formatCNPJ(document) : formatCPF(document);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
131
|
export interface BillingDataFormProps {
|
|
137
132
|
account: Account | undefined;
|
|
138
133
|
onSaved: () => void;
|
|
139
|
-
/** Fixa o formulário em modo Brasil (documento, CEP e endereço obrigatórios) e esconde o select
|
|
140
|
-
* de país — usado por fluxos PIX, que só existem no Brasil. Sem isso, o país vem da conta ou da
|
|
141
|
-
* geolocalização do visitante. */
|
|
142
134
|
country?: 'BR';
|
|
143
135
|
submitLabel?: string;
|
|
144
136
|
}
|
|
145
137
|
|
|
146
|
-
/**
|
|
147
|
-
* Formulário de cadastro fiscal/cobrança, reaproveitado por qualquer fluxo que precise dele
|
|
148
|
-
* bloqueante (`RequiredBillingDataModal`) ou como etapa dentro de outro modal.
|
|
149
|
-
*/
|
|
150
138
|
export function BillingDataForm({ account, onSaved, country, submitLabel }: BillingDataFormProps) {
|
|
151
139
|
const translate = useTranslations();
|
|
152
140
|
const { user } = useAuth();
|
|
@@ -164,7 +152,6 @@ export function BillingDataForm({ account, onSaved, country, submitLabel }: Bill
|
|
|
164
152
|
resolver: zodResolver(schema),
|
|
165
153
|
mode: 'onChange',
|
|
166
154
|
defaultValues: {
|
|
167
|
-
personType: 'pf',
|
|
168
155
|
document: '',
|
|
169
156
|
financialName: '',
|
|
170
157
|
financialEmail: '',
|
|
@@ -189,18 +176,15 @@ export function BillingDataForm({ account, onSaved, country, submitLabel }: Bill
|
|
|
189
176
|
formState: { errors, isSubmitting, isValid },
|
|
190
177
|
} = form;
|
|
191
178
|
|
|
192
|
-
const
|
|
193
|
-
const isCompany = personType === 'pj';
|
|
179
|
+
const isCompany = isCompanyDocument(watch('document'));
|
|
194
180
|
const isBrazil = watch('country') === 'BR';
|
|
195
181
|
|
|
196
182
|
useEffect(() => {
|
|
197
183
|
if (!account || isPrefilled.current) return;
|
|
198
184
|
isPrefilled.current = true;
|
|
199
185
|
|
|
200
|
-
const accountIsCompany = account.financial_document_type === 2;
|
|
201
186
|
reset({
|
|
202
|
-
|
|
203
|
-
document: formatDocument(account.financial_document, accountIsCompany),
|
|
187
|
+
document: formatCPFCNPJ(account.financial_document ?? ''),
|
|
204
188
|
financialName: account.financial_name ?? '',
|
|
205
189
|
financialEmail: account.financial_email || user?.email || '',
|
|
206
190
|
cep: account.zipcode ?? '',
|
|
@@ -215,17 +199,7 @@ export function BillingDataForm({ account, onSaved, country, submitLabel }: Bill
|
|
|
215
199
|
}, [account, country, reset, user?.email]);
|
|
216
200
|
|
|
217
201
|
const handleDocumentChange = (rawValue: string) => {
|
|
218
|
-
setValue('document',
|
|
219
|
-
shouldValidate: true,
|
|
220
|
-
});
|
|
221
|
-
};
|
|
222
|
-
|
|
223
|
-
const handlePersonTypeChange = (value: 'pf' | 'pj') => {
|
|
224
|
-
if (value === personType) return;
|
|
225
|
-
setValue('document', '');
|
|
226
|
-
/* shouldValidate reroda o resolver: sem isso o `isValid` fica preso no valor
|
|
227
|
-
* do documento antigo e o botão continua habilitado com o campo já limpo. */
|
|
228
|
-
setValue('personType', value, { shouldValidate: true });
|
|
202
|
+
setValue('document', formatCPFCNPJ(rawValue), { shouldValidate: true });
|
|
229
203
|
};
|
|
230
204
|
|
|
231
205
|
const handleCepChange = (rawValue: string) => {
|
|
@@ -241,16 +215,14 @@ export function BillingDataForm({ account, onSaved, country, submitLabel }: Bill
|
|
|
241
215
|
setValue('city', data.localidade, { shouldValidate: true });
|
|
242
216
|
setValue('state', data.uf, { shouldValidate: true });
|
|
243
217
|
})
|
|
244
|
-
.catch(() => {
|
|
245
|
-
// CEP não encontrado: o usuário preenche o endereço na mão.
|
|
246
|
-
});
|
|
218
|
+
.catch(() => {});
|
|
247
219
|
};
|
|
248
220
|
|
|
249
221
|
async function onSubmit(data: BillingDataFormValues) {
|
|
250
222
|
const payload: UpdateBillingDataRequest =
|
|
251
223
|
data.country === 'BR'
|
|
252
224
|
? {
|
|
253
|
-
financial_document_type: data.
|
|
225
|
+
financial_document_type: isCompanyDocument(data.document) ? 2 : 1,
|
|
254
226
|
financial_document: data.document,
|
|
255
227
|
financial_name: data.financialName,
|
|
256
228
|
financial_email: data.financialEmail,
|
|
@@ -283,8 +255,6 @@ export function BillingDataForm({ account, onSaved, country, submitLabel }: Bill
|
|
|
283
255
|
);
|
|
284
256
|
onSaved();
|
|
285
257
|
} catch (error) {
|
|
286
|
-
/* O backend devolve a razão exata da recusa (ex.: documento fiscal inválido).
|
|
287
|
-
* Trocar por um texto genérico deixaria o usuário travado sem saber o que corrigir. */
|
|
288
258
|
const message =
|
|
289
259
|
error instanceof Error && error.message
|
|
290
260
|
? error.message
|
|
@@ -301,8 +271,6 @@ export function BillingDataForm({ account, onSaved, country, submitLabel }: Bill
|
|
|
301
271
|
<div className="flex flex-col gap-5">
|
|
302
272
|
{!country && (
|
|
303
273
|
<>
|
|
304
|
-
{/* Reage ao watch('country'): só quem está aqui dentro sabe quando o usuário
|
|
305
|
-
* troca de país no select logo abaixo. */}
|
|
306
274
|
<span className="paragraph-small-regular text-zinc-600">
|
|
307
275
|
{isBrazil
|
|
308
276
|
? translate('common.billing.requiredData.description')
|
|
@@ -329,49 +297,6 @@ export function BillingDataForm({ account, onSaved, country, submitLabel }: Bill
|
|
|
329
297
|
</>
|
|
330
298
|
)}
|
|
331
299
|
|
|
332
|
-
{isBrazil && (
|
|
333
|
-
<div className="flex gap-4">
|
|
334
|
-
<Controller
|
|
335
|
-
name="personType"
|
|
336
|
-
control={control}
|
|
337
|
-
render={({ field }) => (
|
|
338
|
-
<SelectField
|
|
339
|
-
label={translate('common.billing.requiredData.personType')}
|
|
340
|
-
placeholder={translate('common.billing.requiredData.select')}
|
|
341
|
-
value={field.value}
|
|
342
|
-
onChange={(value) => handlePersonTypeChange(value as 'pf' | 'pj')}
|
|
343
|
-
options={[
|
|
344
|
-
{
|
|
345
|
-
value: 'pf',
|
|
346
|
-
label: translate('common.billing.requiredData.personTypePf'),
|
|
347
|
-
},
|
|
348
|
-
{
|
|
349
|
-
value: 'pj',
|
|
350
|
-
label: translate('common.billing.requiredData.personTypePj'),
|
|
351
|
-
},
|
|
352
|
-
]}
|
|
353
|
-
containerClassName="flex-1"
|
|
354
|
-
/>
|
|
355
|
-
)}
|
|
356
|
-
/>
|
|
357
|
-
<div className="flex-1">
|
|
358
|
-
<FormField
|
|
359
|
-
label={
|
|
360
|
-
isCompany
|
|
361
|
-
? translate('common.billing.requiredData.cnpj')
|
|
362
|
-
: translate('common.billing.requiredData.cpf')
|
|
363
|
-
}
|
|
364
|
-
placeholder={isCompany ? '__.___.___/____-__' : '___.___.___-__'}
|
|
365
|
-
error={!!errors.document}
|
|
366
|
-
errorMessage={errors.document?.message}
|
|
367
|
-
{...register('document', {
|
|
368
|
-
onChange: (event) => handleDocumentChange(event.target.value),
|
|
369
|
-
})}
|
|
370
|
-
/>
|
|
371
|
-
</div>
|
|
372
|
-
</div>
|
|
373
|
-
)}
|
|
374
|
-
|
|
375
300
|
<FormField
|
|
376
301
|
label={
|
|
377
302
|
isBrazil && isCompany
|
|
@@ -384,6 +309,18 @@ export function BillingDataForm({ account, onSaved, country, submitLabel }: Bill
|
|
|
384
309
|
{...register('financialName')}
|
|
385
310
|
/>
|
|
386
311
|
|
|
312
|
+
{isBrazil && (
|
|
313
|
+
<FormField
|
|
314
|
+
label={translate('common.billing.requiredData.document')}
|
|
315
|
+
placeholder={translate('common.billing.requiredData.documentPlaceholder')}
|
|
316
|
+
error={!!errors.document}
|
|
317
|
+
errorMessage={errors.document?.message}
|
|
318
|
+
{...register('document', {
|
|
319
|
+
onChange: (event) => handleDocumentChange(event.target.value),
|
|
320
|
+
})}
|
|
321
|
+
/>
|
|
322
|
+
)}
|
|
323
|
+
|
|
387
324
|
<FormField
|
|
388
325
|
label={translate('common.billing.requiredData.financialEmail')}
|
|
389
326
|
placeholder={translate('common.billing.requiredData.financialEmailPlaceholder')}
|
|
@@ -1224,11 +1224,8 @@ const messages = {
|
|
|
1224
1224
|
addressHeading: 'Billing address',
|
|
1225
1225
|
select: 'Select',
|
|
1226
1226
|
typeHere: 'Type here',
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
personTypePj: 'Company',
|
|
1230
|
-
cpf: 'CPF',
|
|
1231
|
-
cnpj: 'CNPJ',
|
|
1227
|
+
document: 'CPF or CNPJ',
|
|
1228
|
+
documentPlaceholder: 'Enter your CPF or CNPJ',
|
|
1232
1229
|
fullName: 'Full name',
|
|
1233
1230
|
companyName: 'Legal name',
|
|
1234
1231
|
financialEmail: 'Billing email',
|
|
@@ -1252,8 +1249,8 @@ const messages = {
|
|
|
1252
1249
|
invalidCep: 'Invalid ZIP code',
|
|
1253
1250
|
invalidCpf: 'Invalid CPF',
|
|
1254
1251
|
invalidCnpj: 'Invalid CNPJ',
|
|
1255
|
-
invalidCpfChecksum: 'Invalid CPF
|
|
1256
|
-
invalidCnpjChecksum: 'Invalid CNPJ
|
|
1252
|
+
invalidCpfChecksum: 'Invalid CPF, check the verification digits',
|
|
1253
|
+
invalidCnpjChecksum: 'Invalid CNPJ, check the verification digits',
|
|
1257
1254
|
},
|
|
1258
1255
|
},
|
|
1259
1256
|
navigation: {
|
|
@@ -1230,11 +1230,8 @@ const messages = {
|
|
|
1230
1230
|
addressHeading: 'Dirección de facturación',
|
|
1231
1231
|
select: 'Selecciona',
|
|
1232
1232
|
typeHere: 'Escribe aquí',
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
personTypePj: 'Persona jurídica',
|
|
1236
|
-
cpf: 'CPF',
|
|
1237
|
-
cnpj: 'CNPJ',
|
|
1233
|
+
document: 'CPF o CNPJ',
|
|
1234
|
+
documentPlaceholder: 'Escribe tu CPF o CNPJ',
|
|
1238
1235
|
fullName: 'Nombre completo',
|
|
1239
1236
|
companyName: 'Razón social',
|
|
1240
1237
|
financialEmail: 'Correo de facturación',
|
|
@@ -1258,8 +1255,8 @@ const messages = {
|
|
|
1258
1255
|
invalidCep: 'Código postal inválido',
|
|
1259
1256
|
invalidCpf: 'CPF inválido',
|
|
1260
1257
|
invalidCnpj: 'CNPJ inválido',
|
|
1261
|
-
invalidCpfChecksum: 'CPF inválido
|
|
1262
|
-
invalidCnpjChecksum: 'CNPJ inválido
|
|
1258
|
+
invalidCpfChecksum: 'CPF inválido, revisa los dígitos verificadores',
|
|
1259
|
+
invalidCnpjChecksum: 'CNPJ inválido, revisa los dígitos verificadores',
|
|
1263
1260
|
},
|
|
1264
1261
|
},
|
|
1265
1262
|
navigation: {
|
|
@@ -1241,11 +1241,8 @@ const messages = {
|
|
|
1241
1241
|
addressHeading: 'Endereço de cobrança',
|
|
1242
1242
|
select: 'Selecione',
|
|
1243
1243
|
typeHere: 'Digite aqui',
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
personTypePj: 'Pessoa jurídica',
|
|
1247
|
-
cpf: 'CPF',
|
|
1248
|
-
cnpj: 'CNPJ',
|
|
1244
|
+
document: 'CPF ou CNPJ',
|
|
1245
|
+
documentPlaceholder: 'Digite seu CPF ou CNPJ',
|
|
1249
1246
|
fullName: 'Nome completo',
|
|
1250
1247
|
companyName: 'Razão social',
|
|
1251
1248
|
financialEmail: 'E-mail financeiro',
|
|
@@ -1269,8 +1266,8 @@ const messages = {
|
|
|
1269
1266
|
invalidCep: 'CEP inválido',
|
|
1270
1267
|
invalidCpf: 'CPF inválido',
|
|
1271
1268
|
invalidCnpj: 'CNPJ inválido',
|
|
1272
|
-
invalidCpfChecksum: 'CPF inválido
|
|
1273
|
-
invalidCnpjChecksum: 'CNPJ inválido
|
|
1269
|
+
invalidCpfChecksum: 'CPF inválido, confira os dígitos verificadores',
|
|
1270
|
+
invalidCnpjChecksum: 'CNPJ inválido, confira os dígitos verificadores',
|
|
1274
1271
|
},
|
|
1275
1272
|
},
|
|
1276
1273
|
|
|
@@ -71,8 +71,6 @@ const PurchaseIaCreditsPixDataSchema = z.object({
|
|
|
71
71
|
export const PurchaseIaCreditsResultSchema = z.object({
|
|
72
72
|
status: z.number(),
|
|
73
73
|
message: z.string().optional(),
|
|
74
|
-
/** Code estável da recusa (ex.: `FISCAL_DOCUMENT_REQUIRED`) — sem isso o `z.object` faz strip e o
|
|
75
|
-
* code do backend morre antes de chegar em quem decide reabrir um passo do fluxo por ele. */
|
|
76
74
|
code: z.string().optional(),
|
|
77
75
|
client_secret: z.string().optional(),
|
|
78
76
|
data: z
|
|
@@ -1,14 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Whitelabel 1 é a Great por definição — dono do domínio-mãe e o único destino
|
|
3
|
-
* seguro quando um host não tem whitelabel própria (ver `WhitelabelService.getDefault`).
|
|
4
|
-
*/
|
|
5
1
|
export const DEFAULT_WHITELABEL_ID = 1;
|
|
6
2
|
|
|
7
|
-
/** Domínio da whitelabel padrão, usado quando `WHITELABEL_DEFAULT_DOMAIN` não está configurada. */
|
|
8
|
-
export const DEFAULT_WHITELABEL_DOMAIN = 'greatpages.com.br';
|
|
9
|
-
|
|
10
|
-
/** TTL do cache positivo (token resolvido com sucesso) — uma semana. */
|
|
11
3
|
export const WHITELABEL_CACHE_TTL_SECONDS = 604800;
|
|
12
4
|
|
|
13
|
-
/** TTL do cache negativo (host sem whitelabel) — curto, pra não travar 5 minutos um domínio novo. */
|
|
14
5
|
export const MISSING_WHITELABEL_CACHE_TTL_SECONDS = 300;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import greatCache from "@greatapps/cache";
|
|
2
2
|
import { ApiError } from "../../../infra/api/types";
|
|
3
|
-
import { WhitelabelTokenApiResponse, WhitelabelTokenData } from "../schema";
|
|
3
|
+
import { WhitelabelApiResponse, WhitelabelTokenApiResponse, WhitelabelTokenData } from "../schema";
|
|
4
4
|
import { normalizeHostname } from "../utils/normalize-hostname";
|
|
5
5
|
import {
|
|
6
|
-
|
|
6
|
+
DEFAULT_WHITELABEL_ID,
|
|
7
7
|
MISSING_WHITELABEL_CACHE_TTL_SECONDS,
|
|
8
8
|
WHITELABEL_CACHE_TTL_SECONDS,
|
|
9
9
|
} from "../constants/whitelabel.constants";
|
|
@@ -50,6 +50,10 @@ class WhitelabelService {
|
|
|
50
50
|
return `whitelabel-missing-${hostname}`;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
private getDefaultCacheKey(): string {
|
|
54
|
+
return `whitelabel-token-id-${DEFAULT_WHITELABEL_ID}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
53
57
|
private async fetchFromApi(hostname: string): Promise<WhitelabelTokenData | null> {
|
|
54
58
|
const apiUrl = this.getApiUrl();
|
|
55
59
|
const whitelabelMasterToken = this.getToken();
|
|
@@ -64,11 +68,6 @@ class WhitelabelService {
|
|
|
64
68
|
},
|
|
65
69
|
});
|
|
66
70
|
|
|
67
|
-
// Host sem whitelabel ativa é caso esperado (domínio novo, subdomínio livre) — não é falha de
|
|
68
|
-
// rede, então nem lê o corpo: cai no fallback do whitelabel padrão. 403 NÃO entra aqui: a rota
|
|
69
|
-
// emissora nunca responde 403, quem responde é borda (WAF, Access, rate limit) — tratar como
|
|
70
|
-
// ausência de whitelabel faria um incidente de borda derrubar toda whitelabel de cliente no
|
|
71
|
-
// whitelabel padrão, servindo branding e token de API errados em silêncio.
|
|
72
71
|
if (response.status === 404) {
|
|
73
72
|
console.log("[WhitelabelService] No whitelabel for domain", {
|
|
74
73
|
hostname,
|
|
@@ -98,11 +97,6 @@ class WhitelabelService {
|
|
|
98
97
|
return result.data[0];
|
|
99
98
|
}
|
|
100
99
|
|
|
101
|
-
/**
|
|
102
|
-
* Cache (positivo) + fetch + validação de hostname pra um domínio já normalizado. Usado tanto
|
|
103
|
-
* pelo host da requisição quanto pelo domínio padrão — sem fallback embutido, pra `getDefault`
|
|
104
|
-
* não poder recursar nela mesma.
|
|
105
|
-
*/
|
|
106
100
|
private async lookup(hostname: string): Promise<WhitelabelTokenData | null> {
|
|
107
101
|
const cache = this.createCache();
|
|
108
102
|
const cacheKey = this.getCacheKey(hostname);
|
|
@@ -148,26 +142,49 @@ class WhitelabelService {
|
|
|
148
142
|
return data;
|
|
149
143
|
}
|
|
150
144
|
|
|
151
|
-
/**
|
|
152
|
-
* Whitelabel padrão (Great), destino de todo host sem whitelabel própria. Resolve pelo mesmo
|
|
153
|
-
* `lookup`, então herda o cache positivo — mas nunca cai em `getTokenByDomain`, que teria
|
|
154
|
-
* fallback: aqui a ausência é falha de plataforma, não caso esperado.
|
|
155
|
-
*/
|
|
156
145
|
private async getDefault(): Promise<WhitelabelTokenData> {
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
146
|
+
const cache = this.createCache();
|
|
147
|
+
const cacheKey = this.getDefaultCacheKey();
|
|
148
|
+
|
|
149
|
+
const cachedData = await cache.select(cacheKey);
|
|
150
|
+
if (cachedData.status == 1 && "data" in cachedData && cachedData.data) {
|
|
151
|
+
const cachedWhitelabel: WhitelabelTokenData = JSON.parse(cachedData.data);
|
|
152
|
+
return cachedWhitelabel;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const apiUrl = this.getApiUrl();
|
|
156
|
+
const masterToken = this.getToken();
|
|
157
|
+
const url = `${apiUrl}/v1/pt-br/${DEFAULT_WHITELABEL_ID}/whitelabels/${DEFAULT_WHITELABEL_ID}`;
|
|
158
|
+
|
|
159
|
+
const response = await fetch(url, {
|
|
160
|
+
method: "GET",
|
|
161
|
+
headers: {
|
|
162
|
+
authorization: masterToken,
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
if (!response.ok) {
|
|
167
|
+
throw new ApiError(
|
|
168
|
+
`Default whitelabel not found: ${DEFAULT_WHITELABEL_ID}`,
|
|
169
|
+
"WL_DEFAULT_UNAVAILABLE",
|
|
170
|
+
500,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
160
173
|
|
|
161
|
-
const
|
|
174
|
+
const result: WhitelabelApiResponse = await response.json();
|
|
162
175
|
|
|
163
|
-
if (!data) {
|
|
176
|
+
if (result.status !== 1 || !result.data?.length) {
|
|
164
177
|
throw new ApiError(
|
|
165
|
-
`Default whitelabel not found: ${
|
|
178
|
+
`Default whitelabel not found: ${DEFAULT_WHITELABEL_ID}`,
|
|
166
179
|
"WL_DEFAULT_UNAVAILABLE",
|
|
167
180
|
500,
|
|
168
181
|
);
|
|
169
182
|
}
|
|
170
183
|
|
|
184
|
+
const data: WhitelabelTokenData = { ...result.data[0], token: masterToken };
|
|
185
|
+
|
|
186
|
+
await cache.insert(cacheKey, JSON.stringify(data), WHITELABEL_CACHE_TTL_SECONDS);
|
|
187
|
+
|
|
171
188
|
return data;
|
|
172
189
|
}
|
|
173
190
|
|
|
@@ -268,6 +285,7 @@ class WhitelabelService {
|
|
|
268
285
|
|
|
269
286
|
await cache.delete(cacheKey);
|
|
270
287
|
await cache.delete(missingCacheKey);
|
|
288
|
+
await cache.delete(this.getDefaultCacheKey());
|
|
271
289
|
|
|
272
290
|
const data = await this.getTokenByDomain(hostname);
|
|
273
291
|
|
|
@@ -26,6 +26,7 @@ export function makeSessionHandlers(opts: SessionHandlersOptions = {}) {
|
|
|
26
26
|
|
|
27
27
|
return [
|
|
28
28
|
http.get(`${base}/v1/:locale/:wl/whitelabel/:host/token`, () => apiFind(whitelabel)),
|
|
29
|
+
http.get(`${base}/v1/:locale/:wl/whitelabels/:id`, () => apiFind(whitelabel)),
|
|
29
30
|
http.get(`${base}/v1/:locale/:wl/tokens`, () => apiFind(whitelabel)),
|
|
30
31
|
// The users path has more segments, so it must come before /accounts/:acc.
|
|
31
32
|
http.get(`${base}/v1/:locale/:wl/accounts/:acc/users/:user`, () => apiFind(user)),
|