Files
OPEN_CODEX_API/src/services/fechamento/bancoPontos.ts
T
2026-04-27 16:50:27 -03:00

309 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import axios from "axios";
import { buildCommanderHeaders, resolveCommanderBaseUrl } from "./commanderHttp";
export type BancoPontosEstaAtivoFiltro = "all" | "true" | "false";
export type BancoPontosSaldoItem = {
parceiroId: string;
nome: string;
codinome: string | null;
estaAtivo: boolean;
logoUrl: string | null;
/** URL pública da foto (Commander pode enviar junto com `logoUrl`). */
fotoUrl?: string | null;
saldo: number;
};
export type BancoPontosExtratoItem = {
id: string;
parceiroId: string;
fechamentoId: string | null;
tipo: "credito" | "debito";
quantidade: number;
descricao: string;
criadoEm: string;
/** Mês da competência do fechamento (112), se a linha estiver vinculada a um fechamento. */
competenciaMes: number | null;
/** Ano da competência do fechamento, se houver. */
competenciaAno: number | null;
};
export type BancoPontosParceiroExtrato = {
parceiroId: string;
nome: string;
codinome: string | null;
logoUrl: string | null;
fotoUrl?: string | null;
saldo: number;
};
type MetaResponse = {
total: number;
paginaAtual: number;
totalPaginas: number;
};
export type ListarBancoPontosSaldosResponse = {
data: BancoPontosSaldoItem[];
meta: MetaResponse;
};
export type ListarBancoPontosExtratoResponse = {
parceiro: BancoPontosParceiroExtrato;
data: BancoPontosExtratoItem[];
meta: MetaResponse;
};
type ApiErrorShape = {
error?: {
message?: string;
};
};
function pickLogoUrl(row: { logoUrl?: unknown; logo_url?: unknown }): string | null {
const v = row.logoUrl ?? row.logo_url;
if (v == null) return null;
const s = String(v).trim();
return s.length > 0 ? s : null;
}
function pickSaldo(row: { saldo?: unknown; saldo_atual?: unknown }): number {
const v = row.saldo ?? row.saldo_atual;
if (v == null || v === "") return 0;
const n = Number(v);
return Number.isFinite(n) ? n : 0;
}
function pickFotoUrl(raw: { fotoUrl?: unknown; foto_url?: unknown }): string | null {
const v = raw.fotoUrl ?? raw.foto_url;
if (v == null) return null;
const s = String(v).trim();
return s.length > 0 ? s : null;
}
function normalizeSaldoItem(raw: Record<string, unknown>): BancoPontosSaldoItem {
const logo = pickLogoUrl(raw);
const foto = pickFotoUrl(raw) ?? logo;
return {
parceiroId: String(raw.parceiroId ?? raw.parceiro_id ?? ""),
nome: String(raw.nome ?? ""),
codinome: raw.codinome != null ? String(raw.codinome) : null,
estaAtivo: Boolean(raw.estaAtivo ?? raw.esta_ativo),
logoUrl: foto,
fotoUrl: foto,
saldo: pickSaldo(raw),
};
}
function normalizeParceiroExtrato(raw: Record<string, unknown>): BancoPontosParceiroExtrato {
const logo = pickLogoUrl(raw);
const foto = pickFotoUrl(raw) ?? logo;
return {
parceiroId: String(raw.parceiroId ?? raw.parceiro_id ?? ""),
nome: String(raw.nome ?? ""),
codinome: raw.codinome != null ? String(raw.codinome) : null,
logoUrl: foto,
fotoUrl: foto,
saldo: pickSaldo(raw),
};
}
function pickNullableInt(v: unknown): number | null {
if (v == null || v === "") return null;
const n = Number(v);
return Number.isFinite(n) ? Math.trunc(n) : null;
}
export function normalizeBancoPontosExtratoItem(raw: Record<string, unknown>): BancoPontosExtratoItem {
const fechamentoRaw = raw.fechamentoId ?? raw.fechamento_id;
return {
id: String(raw.id ?? ""),
parceiroId: String(raw.parceiroId ?? raw.parceiro_id ?? ""),
fechamentoId: fechamentoRaw != null && String(fechamentoRaw).trim() !== "" ? String(fechamentoRaw) : null,
tipo: raw.tipo === "debito" ? "debito" : "credito",
quantidade: Number(raw.quantidade ?? 0),
descricao: String(raw.descricao ?? ""),
criadoEm: String(raw.criadoEm ?? raw.criado_em ?? ""),
competenciaMes: pickNullableInt(raw.competenciaMes ?? raw.competencia_mes),
competenciaAno: pickNullableInt(raw.competenciaAno ?? raw.competencia_ano),
};
}
export type ListarBancoPontosSaldosParams = {
estaAtivo?: BancoPontosEstaAtivoFiltro;
busca?: string;
page: number;
perPage: number;
};
export type ListarBancoPontosExtratoParams = {
page: number;
perPage: number;
};
export type ExportarBancoPontosExtratoResponse = {
buffer: ArrayBuffer;
filename: string | null;
};
class FechamentoBancoPontosService {
private extractFilenameFromContentDisposition(value: string | undefined): string | null {
if (!value) return null;
const utf8Match = value.match(/filename\*=UTF-8''([^;]+)/i);
if (utf8Match?.[1]) {
try {
return decodeURIComponent(utf8Match[1]);
} catch {
return utf8Match[1];
}
}
const regularMatch = value.match(/filename="?([^";]+)"?/i);
return regularMatch?.[1] ?? null;
}
private messageFromResponseData(data: unknown): string | null {
if (data == null) return null;
if (typeof data === "object" && !(data instanceof ArrayBuffer) && !ArrayBuffer.isView(data)) {
const msg = (data as ApiErrorShape).error?.message;
return typeof msg === "string" && msg.trim() ? msg : null;
}
if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
const buf = data instanceof ArrayBuffer ? new Uint8Array(data) : new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
if (buf.byteLength === 0) return null;
try {
const text = new TextDecoder().decode(buf);
const trimmed = text.trim();
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
const json = JSON.parse(trimmed) as ApiErrorShape;
if (typeof json.error?.message === "string" && json.error.message.trim()) {
return json.error.message;
}
}
} catch {
return null;
}
}
return null;
}
private handleAxiosError(error: unknown, fallback: string): never {
if (axios.isAxiosError(error)) {
if (!error.response) {
throw new Error(
"Não foi possível conectar na API do Commander. Verifique VITE_API_BASE_URL_COMMANDER e se o backend está acessível.",
);
}
const status = error.response.status;
const fromBody =
this.messageFromResponseData(error.response.data) ??
(status === 404
? "Recurso não encontrado. Confirme se o backend está atualizado (rota de exportação)."
: null);
const message = (fromBody ?? error.message ?? fallback) as string;
throw new Error(message);
}
throw new Error(fallback);
}
async listarSaldos(params: ListarBancoPontosSaldosParams): Promise<ListarBancoPontosSaldosResponse> {
try {
const headers = await buildCommanderHeaders();
const baseUrl = resolveCommanderBaseUrl();
const response = await axios.get<ListarBancoPontosSaldosResponse>(`${baseUrl}/banco-pontos`, {
headers,
params: {
estaAtivo: params.estaAtivo ?? "all",
busca: params.busca?.trim() || undefined,
page: params.page,
perPage: params.perPage,
},
});
const body = response.data;
return {
...body,
data: (body.data ?? []).map((row) => normalizeSaldoItem(row as unknown as Record<string, unknown>)),
};
} catch (error) {
this.handleAxiosError(error, "Erro ao listar saldos do banco de pontos.");
}
}
async listarExtrato(
parceiroId: string,
params: ListarBancoPontosExtratoParams,
): Promise<ListarBancoPontosExtratoResponse> {
try {
const headers = await buildCommanderHeaders();
const baseUrl = resolveCommanderBaseUrl();
const response = await axios.get<ListarBancoPontosExtratoResponse>(
`${baseUrl}/banco-pontos/${parceiroId}/extrato`,
{
headers,
params: {
page: params.page,
perPage: params.perPage,
},
},
);
const body = response.data;
const parceiroRaw = body.parceiro as unknown;
const parceiroNorm =
parceiroRaw != null && typeof parceiroRaw === "object"
? normalizeParceiroExtrato(parceiroRaw as Record<string, unknown>)
: body.parceiro;
return {
...body,
parceiro: parceiroNorm,
data: (body.data ?? []).map((row) => normalizeBancoPontosExtratoItem(row as unknown as Record<string, unknown>)),
};
} catch (error) {
this.handleAxiosError(error, "Erro ao carregar extrato do banco de pontos.");
}
}
async exportarExtrato(parceiroId: string): Promise<ExportarBancoPontosExtratoResponse> {
try {
const headers = await buildCommanderHeaders();
const baseUrl = resolveCommanderBaseUrl();
const response = await axios.get<ArrayBuffer>(
`${baseUrl}/banco-pontos/${parceiroId}/extrato/exportar`,
{
headers,
responseType: "arraybuffer",
},
);
const buf = response.data;
if (buf.byteLength < 4) {
throw new Error("Resposta vazia ao exportar planilha.");
}
const bytes = new Uint8Array(buf);
const headText = new TextDecoder().decode(bytes.subarray(0, Math.min(256, bytes.length))).trim();
if (headText.startsWith("{")) {
let json: ApiErrorShape;
try {
json = JSON.parse(new TextDecoder().decode(bytes)) as ApiErrorShape;
} catch {
throw new Error("A API retornou uma resposta inválida ao exportar.");
}
const msg = json.error?.message?.trim();
throw new Error(msg || "Falha ao exportar extrato (resposta JSON). Atualize o Commander.");
}
if (bytes[0] !== 0x50 || bytes[1] !== 0x4b) {
throw new Error("A API não retornou um arquivo XLSX válido (assinatura inválida).");
}
return {
buffer: buf,
filename: this.extractFilenameFromContentDisposition(response.headers["content-disposition"]),
};
} catch (error) {
if (axios.isAxiosError(error)) {
this.handleAxiosError(error, "Erro ao exportar extrato do banco de pontos.");
}
throw error instanceof Error ? error : new Error("Erro ao exportar extrato do banco de pontos.");
}
}
}
export const fechamentoBancoPontosService = new FechamentoBancoPontosService();