584 lines
21 KiB
TypeScript
584 lines
21 KiB
TypeScript
/**
|
|
* Service de récupération automatique des factures FreePro
|
|
*
|
|
* Le portail FreePro (https://pro.free.fr) est une SPA Angular.
|
|
* La connexion se fait via un formulaire HTML : il n'existe pas d'endpoint
|
|
* REST public accessible en fetch simple.
|
|
*
|
|
* Stratégie adoptée :
|
|
* 1. Tenter de télécharger directement le CSV de la facture du mois courant
|
|
* via l'URL https://pro.free.fr/account/invoice/{REF}/primary_csv après
|
|
* avoir obtenu un cookie de session via le formulaire de login.
|
|
* 2. Si l'accès direct échoue, retourner un message d'erreur explicite.
|
|
*
|
|
* Pour le test de connexion, on vérifie que la page /account/ est accessible
|
|
* avec les cookies obtenus — ce qui confirme que les credentials sont valides.
|
|
*/
|
|
|
|
import { getFreeproSettings, updateFreeproLastRun, createFreeproImport, getFreeproImportsByUser } from "./db";
|
|
import { processFreeproExcel } from "./freeproService";
|
|
|
|
// ── Types ──────────────────────────────────────────────────────────────────
|
|
|
|
interface FreeproInvoice {
|
|
invoiceNumber: string;
|
|
date: string;
|
|
amount: number;
|
|
month: string;
|
|
}
|
|
|
|
interface AutoImportResult {
|
|
success: boolean;
|
|
imported: number;
|
|
skipped: number;
|
|
errors: string[];
|
|
message: string;
|
|
}
|
|
|
|
// ── Constantes ─────────────────────────────────────────────────────────────
|
|
|
|
const FREEPRO_BASE_URL = "https://pro.free.fr";
|
|
const LOGIN_URL = `${FREEPRO_BASE_URL}/espace-client/connexion/#/`;
|
|
const LOGIN_FORM_URL = `${FREEPRO_BASE_URL}/espace-client/connexion/`;
|
|
// Endpoint réel capturé via analyse réseau du portail FreePro (XHR POST)
|
|
const DO_LOGIN_URL = `${FREEPRO_BASE_URL}/account/security/do_login`;
|
|
const ACCOUNT_URL = `${FREEPRO_BASE_URL}/account/`;
|
|
const BILLING_URL = `${FREEPRO_BASE_URL}/account/billing`;
|
|
|
|
/** Timeout en ms pour les requêtes HTTP vers FreePro.
|
|
* L'endpoint do_login peut mettre jusqu'à ~16-17s à répondre.
|
|
*/
|
|
const HTTP_TIMEOUT_MS = 25_000;
|
|
|
|
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* fetch avec timeout intégré
|
|
*/
|
|
async function fetchWithTimeout(url: string, options: RequestInit = {}, timeoutMs = HTTP_TIMEOUT_MS): Promise<Response> {
|
|
const controller = new AbortController();
|
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
try {
|
|
return await fetch(url, { ...options, signal: controller.signal });
|
|
} finally {
|
|
clearTimeout(timer);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extrait les cookies d'un header set-cookie
|
|
*/
|
|
function extractCookies(setCookieHeader: string | null): string {
|
|
if (!setCookieHeader) return "";
|
|
return setCookieHeader
|
|
.split(",")
|
|
.map((c) => c.split(";")[0].trim())
|
|
.filter(Boolean)
|
|
.join("; ");
|
|
}
|
|
|
|
/**
|
|
* Calcule le label du mois précédent (les factures FreePro arrivent en début de mois suivant)
|
|
*/
|
|
function previousMoisLabel(): string {
|
|
const now = new Date();
|
|
now.setMonth(now.getMonth() - 1);
|
|
const m = String(now.getMonth() + 1).padStart(2, "0");
|
|
const y = String(now.getFullYear());
|
|
return `${m}/${y}`;
|
|
}
|
|
|
|
// ── Connexion au portail FreePro ───────────────────────────────────────────
|
|
|
|
/**
|
|
* Tente de se connecter au portail FreePro via le formulaire HTML.
|
|
*
|
|
* Le portail est une SPA Angular. La connexion passe par :
|
|
* 1. GET /espace-client/connexion/ → récupérer les cookies initiaux
|
|
* 2. POST sur l'endpoint de soumission du formulaire avec les credentials
|
|
* 3. Vérification que la page /account/ est accessible (HTTP 200 non-redirigé)
|
|
*
|
|
* Retourne les cookies de session si la connexion réussit.
|
|
*/
|
|
async function loginToFreePro(
|
|
email: string,
|
|
password: string
|
|
): Promise<{ cookies: string; success: boolean; error?: string }> {
|
|
try {
|
|
// ── Étape 1 : récupérer la page de login pour les cookies initiaux ──
|
|
let initialCookies = "";
|
|
try {
|
|
const loginPageResp = await fetchWithTimeout(LOGIN_FORM_URL, {
|
|
method: "GET",
|
|
headers: {
|
|
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36",
|
|
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
"Accept-Language": "fr-FR,fr;q=0.9",
|
|
},
|
|
redirect: "follow",
|
|
}, 10_000);
|
|
initialCookies = extractCookies(loginPageResp.headers.get("set-cookie"));
|
|
} catch {
|
|
// Ignorer les erreurs de la page de login — on continue sans cookies initiaux
|
|
}
|
|
|
|
// ── Étape 2 : soumettre les credentials sur le vrai endpoint FreePro ──
|
|
// Endpoint capturé par analyse réseau : POST /account/security/do_login
|
|
// Payload : { login: string, password: string } (NB: "login" et non "email")
|
|
let sessionCookies = "";
|
|
let loginSuccess = false;
|
|
|
|
try {
|
|
const loginResp = await fetchWithTimeout(DO_LOGIN_URL, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36",
|
|
"Accept": "application/json, text/plain, */*",
|
|
"Accept-Language": "fr-FR,fr;q=0.9",
|
|
"Cookie": initialCookies,
|
|
"Referer": LOGIN_FORM_URL,
|
|
"Origin": FREEPRO_BASE_URL,
|
|
"X-Requested-With": "XMLHttpRequest",
|
|
},
|
|
// Le portail utilise "login" (pas "email") comme clé d'identifiant
|
|
body: JSON.stringify({ login: email, password }),
|
|
redirect: "follow",
|
|
}, 25_000);
|
|
|
|
if (loginResp.ok || loginResp.status === 302 || loginResp.status === 204) {
|
|
// Lire le corps pour vérifier l'absence d'erreur
|
|
// L'API FreePro retourne HTTP 200 même en cas d'échec, avec { error: "invalid_credentials" }
|
|
let bodyText = "";
|
|
try { bodyText = await loginResp.text(); } catch { /* ignorer */ }
|
|
|
|
if (bodyText.includes("invalid_credentials") || bodyText.includes("invalide")) {
|
|
return {
|
|
success: false,
|
|
cookies: "",
|
|
error: "Identifiants FreePro incorrects (email ou mot de passe invalide).",
|
|
};
|
|
}
|
|
|
|
const newCookies = extractCookies(loginResp.headers.get("set-cookie"));
|
|
sessionCookies = [initialCookies, newCookies].filter(Boolean).join("; ");
|
|
loginSuccess = true;
|
|
} else if (loginResp.status === 401 || loginResp.status === 403) {
|
|
return {
|
|
success: false,
|
|
cookies: "",
|
|
error: "Identifiants FreePro incorrects (email ou mot de passe invalide).",
|
|
};
|
|
} else {
|
|
// Lire le corps pour avoir plus d'info
|
|
let errorMsg = `Échec de connexion (HTTP ${loginResp.status})`;
|
|
try {
|
|
const body = await loginResp.text();
|
|
if (body.includes("invalid_credentials") || body.includes("invalide") || body.includes("invalid") || body.includes("incorrect")) {
|
|
errorMsg = "Identifiants FreePro incorrects (email ou mot de passe invalide).";
|
|
}
|
|
} catch { /* ignorer */ }
|
|
return { success: false, cookies: "", error: errorMsg };
|
|
}
|
|
} catch (err: any) {
|
|
if (err.name === "AbortError") {
|
|
return {
|
|
success: false,
|
|
cookies: "",
|
|
error: "Délai d'attente dépassé lors de la connexion au portail FreePro (12s).",
|
|
};
|
|
}
|
|
return {
|
|
success: false,
|
|
cookies: "",
|
|
error: `Erreur réseau lors de la connexion : ${err.message}`,
|
|
};
|
|
}
|
|
|
|
// ── Étape 3 : vérifier l'accès à /account/ ──
|
|
// Même si aucun endpoint de login n'a répondu 200, on tente d'accéder à /account/
|
|
// car certaines versions du portail redirigent directement après le formulaire HTML.
|
|
const cookiesToTry = sessionCookies || initialCookies;
|
|
|
|
try {
|
|
const accountResp = await fetchWithTimeout(ACCOUNT_URL, {
|
|
method: "GET",
|
|
headers: {
|
|
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36",
|
|
"Accept": "text/html,application/xhtml+xml",
|
|
"Cookie": cookiesToTry,
|
|
"Referer": FREEPRO_BASE_URL,
|
|
},
|
|
redirect: "manual", // Ne pas suivre les redirections pour détecter si on est connecté
|
|
}, 10_000);
|
|
|
|
// Si on obtient un 200 ou 304, on est connecté
|
|
if (accountResp.status === 200 || accountResp.status === 304) {
|
|
return { success: true, cookies: cookiesToTry };
|
|
}
|
|
|
|
// Si on est redirigé vers la page de login, les credentials sont invalides
|
|
const location = accountResp.headers.get("location") ?? "";
|
|
if (location.includes("connexion") || location.includes("login")) {
|
|
return {
|
|
success: false,
|
|
cookies: "",
|
|
error: "Identifiants FreePro incorrects. Vérifiez votre email et mot de passe.",
|
|
};
|
|
}
|
|
|
|
// Redirection vers /account/ elle-même = succès
|
|
if (location.includes("/account")) {
|
|
return { success: true, cookies: cookiesToTry };
|
|
}
|
|
} catch (err: any) {
|
|
if (err.name === "AbortError") {
|
|
return {
|
|
success: false,
|
|
cookies: "",
|
|
error: "Délai d'attente dépassé lors de la connexion au portail FreePro (15s). Vérifiez que le portail est accessible.",
|
|
};
|
|
}
|
|
}
|
|
|
|
// Si on arrive ici avec loginSuccess = true, on considère que la connexion a fonctionné
|
|
if (loginSuccess) {
|
|
return { success: true, cookies: sessionCookies };
|
|
}
|
|
|
|
return {
|
|
success: false,
|
|
cookies: "",
|
|
error: "Impossible de vérifier la connexion au portail FreePro. Le portail est peut-être inaccessible ou a changé de structure.",
|
|
};
|
|
} catch (err: any) {
|
|
if (err.name === "AbortError") {
|
|
return {
|
|
success: false,
|
|
cookies: "",
|
|
error: "Délai d'attente dépassé lors de la connexion au portail FreePro.",
|
|
};
|
|
}
|
|
return {
|
|
success: false,
|
|
cookies: "",
|
|
error: `Erreur réseau lors de la connexion : ${err.message}`,
|
|
};
|
|
}
|
|
}
|
|
|
|
// ── Récupération de la liste des factures ──────────────────────────────────
|
|
|
|
/** Endpoint API REST interne du portail FreePro (Angular) qui retourne la liste des factures en JSON. */
|
|
const INVOICES_API_URL = `${FREEPRO_BASE_URL}/api/api_red_october/v1/invoices`;
|
|
|
|
/**
|
|
* Récupère la liste des factures disponibles sur le portail FreePro.
|
|
*
|
|
* Le portail FreePro est une SPA Angular. L'API interne utilisée par Angular
|
|
* pour charger les données est : GET /api/api_red_october/v1/invoices
|
|
* Elle retourne un tableau JSON avec les champs : ref, billing_date, total_ht, total_ttc, invoice_status
|
|
*
|
|
* Cette API nécessite les cookies de session obtenus après connexion via /account/security/do_login.
|
|
*/
|
|
async function fetchInvoiceList(cookies: string): Promise<FreeproInvoice[]> {
|
|
try {
|
|
const resp = await fetchWithTimeout(INVOICES_API_URL, {
|
|
headers: {
|
|
"Cookie": cookies,
|
|
"Accept": "application/json",
|
|
"X-Requested-With": "XMLHttpRequest",
|
|
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36",
|
|
"Referer": BILLING_URL,
|
|
"Accept-Language": "fr-FR,fr;q=0.9",
|
|
},
|
|
redirect: "follow",
|
|
}, 15_000);
|
|
|
|
if (!resp.ok) return [];
|
|
|
|
const data = await resp.json();
|
|
return parseInvoiceListFromJson(data);
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parse la réponse JSON de l'API interne FreePro /api/api_red_october/v1/invoices.
|
|
*
|
|
* Structure d'un élément :
|
|
* { ref: "F202606003800", billing_date: "2026-06-01T00:00:00", total_ht: 1287.37,
|
|
* total_ttc: 1544.84, invoice_status: "waiting"|"paid", ... }
|
|
*/
|
|
function parseInvoiceListFromJson(data: any): FreeproInvoice[] {
|
|
if (!Array.isArray(data)) return [];
|
|
|
|
const invoices: FreeproInvoice[] = [];
|
|
|
|
for (const item of data) {
|
|
const invoiceNumber: string = item.ref ?? "";
|
|
const billingDate: string = item.billing_date ?? "";
|
|
const amount: number = typeof item.total_ttc === "number" ? item.total_ttc : 0;
|
|
|
|
if (!invoiceNumber || !billingDate) continue;
|
|
|
|
// billing_date est au format ISO : "2026-06-01T00:00:00"
|
|
// On extrait YYYY et MM pour construire le label du mois
|
|
let month = "";
|
|
const dateMatch = billingDate.match(/^(\d{4})-(\d{2})/);
|
|
if (dateMatch) {
|
|
month = `${dateMatch[2]}/${dateMatch[1]}`; // "06/2026"
|
|
}
|
|
|
|
if (month) {
|
|
invoices.push({ invoiceNumber, date: billingDate, amount, month });
|
|
}
|
|
}
|
|
|
|
// Trier par numéro de facture décroissant (plus récente en premier)
|
|
invoices.sort((a, b) => b.invoiceNumber.localeCompare(a.invoiceNumber));
|
|
|
|
return invoices;
|
|
}
|
|
|
|
/**
|
|
* Télécharge le CSV d'une facture FreePro
|
|
*/
|
|
async function downloadInvoiceCsv(cookies: string, invoiceNumber: string): Promise<Buffer | null> {
|
|
try {
|
|
const csvUrl = `${FREEPRO_BASE_URL}/account/invoice/${invoiceNumber}/primary_csv`;
|
|
const resp = await fetchWithTimeout(csvUrl, {
|
|
headers: {
|
|
"Cookie": cookies,
|
|
"Accept": "text/csv,application/csv,*/*",
|
|
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36",
|
|
"Referer": BILLING_URL,
|
|
},
|
|
redirect: "follow",
|
|
});
|
|
|
|
if (!resp.ok) return null;
|
|
const arrayBuffer = await resp.arrayBuffer();
|
|
return Buffer.from(arrayBuffer);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// ── Test de connexion ──────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Teste la connexion au portail FreePro avec les credentials fournis.
|
|
*
|
|
* Retourne toujours un résultat dans les 20 secondes maximum.
|
|
*/
|
|
export async function testFreeproConnection(
|
|
email: string,
|
|
password: string
|
|
): Promise<{ success: boolean; message: string }> {
|
|
// Validation basique des entrées
|
|
if (!email || !email.includes("@")) {
|
|
return { success: false, message: "Adresse email invalide." };
|
|
}
|
|
if (!password || password.length < 3) {
|
|
return { success: false, message: "Mot de passe trop court." };
|
|
}
|
|
|
|
try {
|
|
const loginResult = await loginToFreePro(email, password);
|
|
|
|
if (!loginResult.success) {
|
|
return {
|
|
success: false,
|
|
message: loginResult.error ?? "Échec de connexion au portail FreePro.",
|
|
};
|
|
}
|
|
|
|
// Tenter de récupérer la liste des factures pour enrichir le message
|
|
try {
|
|
const invoices = await fetchInvoiceList(loginResult.cookies);
|
|
if (invoices.length > 0) {
|
|
return {
|
|
success: true,
|
|
message: `Connexion réussie. ${invoices.length} facture(s) disponible(s) dans l'espace client.`,
|
|
};
|
|
}
|
|
} catch {
|
|
// Pas grave si on ne peut pas lister les factures
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
message: "Connexion au portail FreePro réussie. Les identifiants sont valides.",
|
|
};
|
|
} catch (err: any) {
|
|
return {
|
|
success: false,
|
|
message: `Erreur inattendue lors du test de connexion : ${err.message}`,
|
|
};
|
|
}
|
|
}
|
|
|
|
// ── Import automatique ─────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Exécute la récupération automatique des factures FreePro pour un utilisateur.
|
|
*/
|
|
export async function runFreeproAutoImport(userId: number): Promise<AutoImportResult> {
|
|
const result: AutoImportResult = {
|
|
success: false,
|
|
imported: 0,
|
|
skipped: 0,
|
|
errors: [],
|
|
message: "",
|
|
};
|
|
|
|
// 1. Charger les paramètres
|
|
const settings = await getFreeproSettings(userId);
|
|
if (!settings) {
|
|
result.message = "Paramètres FreePro non configurés";
|
|
await updateFreeproLastRun(userId, result.message, 0, false);
|
|
return result;
|
|
}
|
|
|
|
if (!settings.loginEmail || !settings.loginPassword) {
|
|
result.message = "Identifiants FreePro non configurés";
|
|
await updateFreeproLastRun(userId, result.message, 0, false);
|
|
return result;
|
|
}
|
|
|
|
// 2. Se connecter au portail
|
|
const loginResult = await loginToFreePro(settings.loginEmail, settings.loginPassword);
|
|
if (!loginResult.success) {
|
|
result.message = loginResult.error ?? "Échec de connexion au portail FreePro";
|
|
await updateFreeproLastRun(userId, result.message, 0, false);
|
|
return result;
|
|
}
|
|
|
|
const { cookies } = loginResult;
|
|
|
|
// 3. Récupérer la liste des factures
|
|
const availableInvoices = await fetchInvoiceList(cookies);
|
|
|
|
if (availableInvoices.length === 0) {
|
|
result.message = "Connexion réussie mais aucune facture trouvée dans la page /account/billing. Vérifiez que des factures sont disponibles dans votre espace client FreePro.";
|
|
await updateFreeproLastRun(userId, result.message, 0, false);
|
|
return result;
|
|
}
|
|
|
|
// 4. Récupérer les imports déjà existants pour éviter les doublons
|
|
const existingImports = await getFreeproImportsByUser(userId);
|
|
const existingMonths = new Set(existingImports.map((i) => i.moisLabel));
|
|
|
|
// 5. Filtrer selon la date d'antériorité
|
|
const maxAnteriorityDate = settings.maxAnteriority
|
|
? new Date(settings.maxAnteriority * 1000)
|
|
: null;
|
|
|
|
const toImport = availableInvoices.filter((inv) => {
|
|
if (existingMonths.has(inv.month)) {
|
|
result.skipped++;
|
|
return false;
|
|
}
|
|
if (maxAnteriorityDate) {
|
|
const invDate = new Date(inv.date);
|
|
if (invDate < maxAnteriorityDate) {
|
|
result.skipped++;
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
});
|
|
|
|
if (toImport.length === 0) {
|
|
result.success = true;
|
|
result.message = `Aucune nouvelle facture à importer (${result.skipped} déjà importée(s))`;
|
|
await updateFreeproLastRun(userId, result.message, 0, true);
|
|
return result;
|
|
}
|
|
|
|
// 6. Télécharger et importer chaque facture
|
|
for (const invoice of toImport) {
|
|
try {
|
|
const csvBuffer = await downloadInvoiceCsv(cookies, invoice.invoiceNumber);
|
|
|
|
if (!csvBuffer || csvBuffer.length === 0) {
|
|
result.errors.push(`Impossible de télécharger le CSV pour la facture ${invoice.invoiceNumber}`);
|
|
continue;
|
|
}
|
|
|
|
const fileName = `Facture_FreePro_${invoice.invoiceNumber}.csv`;
|
|
const processed = processFreeproExcel(csvBuffer, invoice.month, fileName);
|
|
|
|
await createFreeproImport(
|
|
{
|
|
userId,
|
|
moisLabel: processed.moisLabel,
|
|
annee: processed.annee,
|
|
mois: processed.mois,
|
|
refPiece: processed.refPiece ?? null,
|
|
fileName,
|
|
nbLignes: processed.nbLignes,
|
|
totalTtc: processed.totalTtc.toFixed(2),
|
|
},
|
|
processed.lines.map((l) => ({
|
|
structure: l.structure ?? null,
|
|
type: l.type,
|
|
montantCentimes: Math.round(l.montant * 100),
|
|
}))
|
|
);
|
|
|
|
result.imported++;
|
|
} catch (err: any) {
|
|
result.errors.push(`Erreur lors de l'import de ${invoice.invoiceNumber} : ${err.message}`);
|
|
}
|
|
}
|
|
|
|
// 7. Mettre à jour le statut
|
|
result.success = result.imported > 0 || (toImport.length === 0 && result.errors.length === 0);
|
|
if (result.errors.length > 0) {
|
|
result.message = `${result.imported} facture(s) importée(s), ${result.errors.length} erreur(s) : ${result.errors.join("; ")}`;
|
|
} else {
|
|
result.message = `${result.imported} facture(s) importée(s) avec succès`;
|
|
}
|
|
|
|
await updateFreeproLastRun(userId, result.message, result.imported, result.success);
|
|
return result;
|
|
}
|
|
|
|
// ── Scheduler en mémoire ───────────────────────────────────────────────────
|
|
|
|
const activeJobs = new Map<number, NodeJS.Timeout>();
|
|
|
|
export function startFreeproAutoJob(userId: number, frequencyMs: number): void {
|
|
stopFreeproAutoJob(userId);
|
|
const interval = setInterval(async () => {
|
|
try {
|
|
await runFreeproAutoImport(userId);
|
|
} catch (err: any) {
|
|
console.error(`[FreePro Auto] Erreur job userId=${userId}:`, err.message);
|
|
}
|
|
}, frequencyMs);
|
|
activeJobs.set(userId, interval);
|
|
console.log(`[FreePro Auto] Job démarré pour userId=${userId}, fréquence=${frequencyMs}ms`);
|
|
}
|
|
|
|
export function stopFreeproAutoJob(userId: number): void {
|
|
const interval = activeJobs.get(userId);
|
|
if (interval) {
|
|
clearInterval(interval);
|
|
activeJobs.delete(userId);
|
|
console.log(`[FreePro Auto] Job arrêté pour userId=${userId}`);
|
|
}
|
|
}
|
|
|
|
export function frequencyToMs(frequency: string): number {
|
|
switch (frequency) {
|
|
case "daily": return 24 * 60 * 60 * 1000;
|
|
case "weekly": return 7 * 24 * 60 * 60 * 1000;
|
|
case "monthly": return 30 * 24 * 60 * 60 * 1000;
|
|
default: return 0;
|
|
}
|
|
}
|