Assist_Design/apps/bff/src/integrations/freebit/services/freebit-client.service.ts

247 lines
7.6 KiB
TypeScript
Raw Normal View History

import { Injectable, Inject } from "@nestjs/common";
import { Logger } from "nestjs-pino";
import { getErrorMessage } from "@bff/core/utils/error.util";
import { FreebitAuthService } from "./freebit-auth.service";
import { FreebitError } from "./freebit-error.service";
interface FreebitResponseBase {
resultCode?: string | number;
status?: {
message?: string;
statusCode?: string | number;
};
}
@Injectable()
export class FreebitClientService {
constructor(
private readonly authService: FreebitAuthService,
@Inject(Logger) private readonly logger: Logger
) {}
/**
* Make an authenticated request to Freebit API with retry logic
*/
async makeAuthenticatedRequest<TResponse extends FreebitResponseBase, TPayload extends object>(
endpoint: string,
payload: TPayload
): Promise<TResponse> {
const authKey = await this.authService.getAuthKey();
const config = this.authService.getConfig();
const requestPayload = { ...payload, authKey };
const url = `${config.baseUrl}${endpoint}`;
for (let attempt = 1; attempt <= config.retryAttempts; attempt++) {
try {
this.logger.debug(`Freebit API request (attempt ${attempt}/${config.retryAttempts})`, {
url,
payload: this.sanitizePayload(requestPayload),
});
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), config.timeout);
const response = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: `json=${JSON.stringify(requestPayload)}`,
signal: controller.signal,
});
clearTimeout(timeout);
if (!response.ok) {
throw new FreebitError(
`HTTP ${response.status}: ${response.statusText}`,
response.status.toString()
);
}
const responseData = (await response.json()) as TResponse;
if (responseData.resultCode && responseData.resultCode !== "100") {
throw new FreebitError(
`API Error: ${responseData.status?.message || "Unknown error"}`,
responseData.resultCode,
responseData.status?.statusCode,
responseData.status?.message
);
}
this.logger.debug("Freebit API request successful", {
url,
resultCode: responseData.resultCode,
});
return responseData;
} catch (error: unknown) {
if (error instanceof FreebitError) {
if (error.isAuthError() && attempt === 1) {
this.logger.warn("Auth error detected, clearing cache and retrying");
this.authService.clearAuthCache();
continue;
}
if (!error.isRetryable() || attempt === config.retryAttempts) {
throw error;
}
}
if (attempt === config.retryAttempts) {
const message = getErrorMessage(error);
this.logger.error(`Freebit API request failed after ${config.retryAttempts} attempts`, {
url,
error: message,
});
throw new FreebitError(`Request failed: ${message}`);
}
const delay = Math.min(1000 * Math.pow(2, attempt - 1), 10000);
this.logger.warn(`Freebit API request failed, retrying in ${delay}ms`, {
url,
attempt,
error: getErrorMessage(error),
});
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw new FreebitError("Request failed after all retry attempts");
}
/**
* Make an authenticated JSON request to Freebit API (for PA05-41)
*/
async makeAuthenticatedJsonRequest<
TResponse extends FreebitResponseBase,
TPayload extends object,
>(endpoint: string, payload: TPayload): Promise<TResponse> {
const config = this.authService.getConfig();
const url = `${config.baseUrl}${endpoint}`;
for (let attempt = 1; attempt <= config.retryAttempts; attempt++) {
try {
this.logger.debug(`Freebit JSON API request (attempt ${attempt}/${config.retryAttempts})`, {
url,
payload: this.sanitizePayload(payload as Record<string, unknown>),
});
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), config.timeout);
const response = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
signal: controller.signal,
});
clearTimeout(timeout);
if (!response.ok) {
throw new FreebitError(
`HTTP ${response.status}: ${response.statusText}`,
response.status.toString()
);
}
const responseData = (await response.json()) as TResponse;
if (responseData.resultCode && responseData.resultCode !== "100") {
throw new FreebitError(
`API Error: ${responseData.status?.message || "Unknown error"}`,
responseData.resultCode,
responseData.status?.statusCode,
responseData.status?.message
);
}
this.logger.debug("Freebit JSON API request successful", {
url,
resultCode: responseData.resultCode,
});
return responseData;
} catch (error: unknown) {
if (error instanceof FreebitError) {
if (error.isAuthError() && attempt === 1) {
this.logger.warn("Auth error detected, clearing cache and retrying");
this.authService.clearAuthCache();
continue;
}
if (!error.isRetryable() || attempt === config.retryAttempts) {
throw error;
}
}
if (attempt === config.retryAttempts) {
const message = getErrorMessage(error);
this.logger.error(
`Freebit JSON API request failed after ${config.retryAttempts} attempts`,
{
url,
error: message,
}
);
throw new FreebitError(`Request failed: ${message}`);
}
const delay = Math.min(1000 * Math.pow(2, attempt - 1), 10000);
this.logger.warn(`Freebit JSON API request failed, retrying in ${delay}ms`, {
url,
attempt,
error: getErrorMessage(error),
});
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw new FreebitError("Request failed after all retry attempts");
}
/**
* Make a simple request without authentication (for health checks)
*/
async makeSimpleRequest(endpoint: string): Promise<boolean> {
const config = this.authService.getConfig();
const url = `${config.baseUrl}${endpoint}`;
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), config.timeout);
const response = await fetch(url, {
method: "GET",
signal: controller.signal,
});
clearTimeout(timeout);
return response.ok;
} catch (error) {
this.logger.debug("Simple request failed", {
url,
error: getErrorMessage(error),
});
return false;
}
}
/**
* Sanitize payload for logging (remove sensitive data)
*/
private sanitizePayload(payload: Record<string, unknown>): Record<string, unknown> {
const sanitized = { ...payload };
// Remove sensitive fields
const sensitiveFields = ["authKey", "oemKey", "password", "secret"];
for (const field of sensitiveFields) {
if (sanitized[field]) {
sanitized[field] = "[REDACTED]";
}
}
return sanitized;
}
}