Created
July 23, 2026 12:08
-
-
Save MoisesTedeschi/caa199939c93117fe3b6f62a8650ee0b to your computer and use it in GitHub Desktop.
Validação de Oferta
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * ============================================================================ | |
| * Exemplo sênior: consumo de API com autenticação usando Clean Code | |
| * e padrões de projeto (Strategy, Result Object, Dependency Injection, | |
| * Facade e Custom Errors), mantendo Fetch API como mecanismo de requisição. | |
| * | |
| * Fluxo de negócio modelado: | |
| * 1) Validar oferta (API de validação) | |
| * 2) Se válida, postar order (API de criação de pedido) | |
| * ============================================================================ | |
| */ | |
| /* ----------------------------------------------------------------------- */ | |
| /* 1. Erros customizados — deixam o motivo da falha explícito e tipado */ | |
| /* ----------------------------------------------------------------------- */ | |
| class ApiError extends Error { | |
| constructor(message, { status = null, cause = null } = {}) { | |
| super(message); | |
| this.name = "ApiError"; | |
| this.status = status; | |
| this.cause = cause; | |
| } | |
| } | |
| class NetworkError extends ApiError { | |
| constructor(cause) { | |
| super("Falha de rede ao chamar a API", { cause }); | |
| this.name = "NetworkError"; | |
| } | |
| } | |
| class UnauthorizedError extends ApiError { | |
| constructor(status) { | |
| super("Falha de autenticação com a API", { status }); | |
| this.name = "UnauthorizedError"; | |
| } | |
| } | |
| /* ----------------------------------------------------------------------- */ | |
| /* 2. Result — evita usar exceptions para controlar fluxo de negócio. */ | |
| /* O chamador decide o que fazer com sucesso/erro sem try/catch */ | |
| /* espalhado pelo código. */ | |
| /* ----------------------------------------------------------------------- */ | |
| class Result { | |
| constructor(isSuccess, value, error) { | |
| this.isSuccess = isSuccess; | |
| this.value = value; | |
| this.error = error; | |
| } | |
| static ok(value) { | |
| return new Result(true, value, null); | |
| } | |
| static fail(error) { | |
| return new Result(false, null, error); | |
| } | |
| } | |
| /* ----------------------------------------------------------------------- */ | |
| /* 3. Strategy de autenticação — permite trocar Bearer/API Key/OAuth sem */ | |
| /* alterar o cliente HTTP (Open/Closed Principle). */ | |
| /* ----------------------------------------------------------------------- */ | |
| class AuthStrategy { | |
| /** @returns {Record<string, string>} headers a serem aplicados na requisição */ | |
| getHeaders() { | |
| throw new Error("getHeaders() deve ser implementado pela estratégia concreta"); | |
| } | |
| } | |
| class BearerTokenAuth extends AuthStrategy { | |
| constructor(token) { | |
| super(); | |
| this._token = token; | |
| } | |
| getHeaders() { | |
| return { Authorization: `Bearer ${this._token}` }; | |
| } | |
| } | |
| class ApiKeyAuth extends AuthStrategy { | |
| constructor(apiKey, headerName = "x-api-key") { | |
| super(); | |
| this._apiKey = apiKey; | |
| this._headerName = headerName; | |
| } | |
| getHeaders() { | |
| return { [this._headerName]: this._apiKey }; | |
| } | |
| } | |
| /* ----------------------------------------------------------------------- */ | |
| /* 4. HttpClient — única camada que conhece o `fetch`. Responsabilidade */ | |
| /* única: fazer a requisição HTTP e normalizar erros. Nada de regra */ | |
| /* de negócio aqui. */ | |
| /* ----------------------------------------------------------------------- */ | |
| class HttpClient { | |
| /** | |
| * @param {AuthStrategy} authStrategy | |
| * @param {number} timeoutMs | |
| */ | |
| constructor(authStrategy, timeoutMs = 8000) { | |
| this._authStrategy = authStrategy; | |
| this._timeoutMs = timeoutMs; | |
| } | |
| /** | |
| * @param {string} url | |
| * @param {"GET"|"POST"|"PUT"|"DELETE"} method | |
| * @param {object|null} body | |
| * @returns {Promise<Result>} | |
| */ | |
| async request(url, method, body = null) { | |
| const controller = new AbortController(); | |
| const timeoutId = setTimeout(() => controller.abort(), this._timeoutMs); | |
| try { | |
| const response = await fetch(url, { | |
| method, | |
| headers: { | |
| "Content-Type": "application/json", | |
| ...this._authStrategy.getHeaders() | |
| }, | |
| body: body ? JSON.stringify(body) : null, | |
| signal: controller.signal | |
| }); | |
| clearTimeout(timeoutId); | |
| if (response.status === 401 || response.status === 403) { | |
| return Result.fail(new UnauthorizedError(response.status)); | |
| } | |
| if (!response.ok) { | |
| return Result.fail( | |
| new ApiError(`API retornou status ${response.status}`, { status: response.status }) | |
| ); | |
| } | |
| const data = await response.json(); | |
| return Result.ok(data); | |
| } catch (error) { | |
| clearTimeout(timeoutId); | |
| return Result.fail(new NetworkError(error)); | |
| } | |
| } | |
| } | |
| /* ----------------------------------------------------------------------- */ | |
| /* 5. Camada de domínio — cada serviço conhece apenas o seu endpoint e o */ | |
| /* formato do payload. Não sabe nada sobre HTTP, fetch ou autenticação. */ | |
| /* ----------------------------------------------------------------------- */ | |
| class OfferValidationService { | |
| /** | |
| * @param {HttpClient} httpClient | |
| * @param {string} baseUrl | |
| */ | |
| constructor(httpClient, baseUrl) { | |
| this._httpClient = httpClient; | |
| this._baseUrl = baseUrl; | |
| } | |
| /** | |
| * @param {{ clienteId: string, planoOferta: string }} dados | |
| * @returns {Promise<Result>} value esperado: { valido: boolean, planoAtual?: string } | |
| */ | |
| async validar(dados) { | |
| return this._httpClient.request(`${this._baseUrl}/oferta/validar`, "POST", dados); | |
| } | |
| } | |
| class OrderService { | |
| /** | |
| * @param {HttpClient} httpClient | |
| * @param {string} baseUrl | |
| */ | |
| constructor(httpClient, baseUrl) { | |
| this._httpClient = httpClient; | |
| this._baseUrl = baseUrl; | |
| } | |
| /** | |
| * @param {{ clienteId: string, planoOferta: string, idempotencyKey: string }} dados | |
| * @returns {Promise<Result>} | |
| */ | |
| async criar(dados) { | |
| return this._httpClient.request(`${this._baseUrl}/order`, "POST", dados); | |
| } | |
| } | |
| /* ----------------------------------------------------------------------- */ | |
| /* 6. Facade — orquestra o caso de uso completo (validar → postar order) */ | |
| /* Essa é a única classe que conhece a ordem das etapas do negócio. */ | |
| /* ----------------------------------------------------------------------- */ | |
| class ContratacaoOfertaUseCase { | |
| /** | |
| * @param {OfferValidationService} offerValidationService | |
| * @param {OrderService} orderService | |
| */ | |
| constructor(offerValidationService, orderService) { | |
| this._offerValidationService = offerValidationService; | |
| this._orderService = orderService; | |
| } | |
| /** | |
| * @param {{ clienteId: string, planoOferta: string, idempotencyKey: string }} dados | |
| * @returns {Promise<Result>} | |
| */ | |
| async executar(dados) { | |
| const validacao = await this._offerValidationService.validar({ | |
| clienteId: dados.clienteId, | |
| planoOferta: dados.planoOferta | |
| }); | |
| if (!validacao.isSuccess) { | |
| return Result.fail(validacao.error); | |
| } | |
| if (!validacao.value.valido) { | |
| return Result.fail(new ApiError("Oferta inválida ou expirada")); | |
| } | |
| const order = await this._orderService.criar(dados); | |
| if (!order.isSuccess) { | |
| return Result.fail(order.error); | |
| } | |
| return Result.ok(order.value); | |
| } | |
| } | |
| /* ----------------------------------------------------------------------- */ | |
| /* 7. Composição e uso — único lugar onde as peças são "montadas" */ | |
| /* (Dependency Injection manual, sem framework). */ | |
| /* ----------------------------------------------------------------------- */ | |
| async function main() { | |
| const auth = new BearerTokenAuth("SEU_TOKEN_AQUI"); | |
| const httpClient = new HttpClient(auth); | |
| const offerValidationService = new OfferValidationService( | |
| httpClient, | |
| "https://api.suaempresa.com" | |
| ); | |
| const orderService = new OrderService(httpClient, "https://api.suaempresa.com"); | |
| const contratarOferta = new ContratacaoOfertaUseCase(offerValidationService, orderService); | |
| const resultado = await contratarOferta.executar({ | |
| clienteId: "12345", | |
| planoOferta: "premium-anual", | |
| idempotencyKey: crypto.randomUUID() | |
| }); | |
| if (resultado.isSuccess) { | |
| console.log("Pedido criado com sucesso:", resultado.value); | |
| } else { | |
| console.error(`Falha no fluxo (${resultado.error.name}):`, resultado.error.message); | |
| } | |
| } | |
| main(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment