mirror of
https://github.com/movixcorp/MovixOpenSource.git
synced 2026-08-09 12:57:38 +00:00
Gros lot de travail synchronisé depuis le repo privé. Cette mise à jour regroupe la refonte du lecteur et du Cast, de nouvelles sources VOD/Live TV, le durcissement de l’API et des proxies, ainsi qu’une nouvelle version de l’application mobile. Lecteur, sources et Live TV - Refonte du lecteur HLS avec sélection et mémorisation de la qualité, détection des résolutions, scan des liens, choix des pistes audio et fallback contrôlé de 1080p vers 720p. - Ajoute une garde contre les boucles de changement de source, notamment lors des erreurs vidéo et des réponses 429. - Revoit SeekStreaming de bout en bout : validation des embeds, prise en charge de plusieurs candidats, regroupement par serveur, règles de headers persistantes et cache d’extraction raccourci. - Ajoute SwiftFlow pour les films et séries, avec lecteurs VF/VOSTFR, cache stale-while-revalidate, déduplication des requêtes et limite partagée côté API. - Adapte Coflix à son nouveau domaine et à son nouveau thème, ajoute un kill switch, développe les wrappers 1jour1film et actualise le domaine Wiflix. - Ajoute NorthLive et Vavoo à la Live TV, regroupe les variantes d’une même chaîne et permet de choisir le serveur ou la qualité. - Enrichit les Top 10 avec des filtres par période et par algorithme, ainsi que de nouvelles métriques de visionnage. Cast et application mobile - Unifie le contrôle Cast entre le Web Cast SDK et le pont Android : état distant, lecture, pause, seek, arrêt et reprise après reconnexion. - Ajoute sur Android un relais média LAN sécurisé, limité au Chromecast et au réseau Wi-Fi sélectionnés, avec sessions opaques et service foreground. - Prépare les sources avant diffusion et prend en charge les playlists HLS TS/fMP4, les flux progressifs, les sous-titres VTT, SeekStreaming et les segments MPEG-TS encapsulés dans des PNG. - Renforce le proxy média local : support Range, validation des redirections, liaison DNS/socket au réseau Cast et meilleure gestion des headers Fsvid. - Sécurise le bridge WebView avec contrôle de l’origine top-frame, capacités propres à chaque document et invalidation lors des navigations. - Ajoute le Picture-in-Picture Android avec lecture/pause et seek de 10 secondes depuis la fenêtre flottante. - Maintient désormais l’écran allumé uniquement pendant une lecture active. - Publie l’application Android 2.5.7, build 16, avec le nouvel APK et ses métadonnées d’intégrité. API, base de données et paiements - Remplace 13 migrations SQL isolées par un manifeste de schéma MySQL centralisé, additif et capable de détecter les divergences de structure. - Ajoute le workflow db:init avec prévisualisation, dry-run, verrou MySQL, confirmations pour les tables Wrapped et couverture de tests dédiée. - Rend configurables les limites du pool MySQL et fiabilise les advisory locks. - Revoit entièrement PayGate : références de callback séparées, nonces, validation des wallets et transactions, états monotones, déduplication des paiements et protection contre les courses ou réutilisations de txid. - Renforce l’annulation, la réconciliation et la livraison idempotente des factures VIP, avec réponses no-store sur les routes sensibles. - Distingue les JWT invalides des indisponibilités MySQL afin de renvoyer correctement 401 ou 503 sans provoquer de déconnexions inutiles. - Utilise ZZAPI/Claude Haiku pour la modération, avec OpenRouter/DeepSeek en solution de secours. - Améliore la gestion HLS et Range dans proxiesembed. Frontend, profils et infrastructure - Protège la sélection et l’hydratation des profils contre les réponses concurrentes, les intentions obsolètes et l’effacement accidentel du stockage local. - Rend VITE_SITE_URL obligatoire et l’injecte dans le runtime, les données structurées, le service worker, robots.txt et sitemap.xml. - Optimise les probes réseau du service worker avec cache court et single-flight. - Charge dynamiquement les traductions anglaises, plusieurs routes secondaires et les données Search/TMDB uniquement lorsqu’elles deviennent nécessaires. - Ajoute des caches mémoire/session pour les images et contenus TMDB et réduit les animations, calculs et rendus hors écran. - Ajoute la page publique des frais, les coins carrés persistants, une nouvelle intro Movix, les avertissements de publicités adultes et de nombreux textes Cast, qualité, Live TV et sources. - Met à jour les extensions Chrome 1.3.12 et Firefox 1.5.9, ainsi que le userscript, avec la nouvelle extraction SeekStreaming et la préparation Cast. - Ajoute une large couverture de tests Kotlin et JavaScript pour le Cast, le proxy média, le PiP, le bridge, la base de données, PayGate et le lecteur. - Ajoute bootstrap-icons et patch-package, régénère les lockfiles npm et actualise la documentation d’installation et d’initialisation MySQL. Co-authored-by: GPT-5.6 Sol <noreply@openai.com>
525 lines
17 KiB
JavaScript
525 lines
17 KiB
JavaScript
'use strict';
|
|
|
|
const crypto = require('crypto');
|
|
const axios = require('axios');
|
|
|
|
const PAYGATE_API_BASE = 'https://api.paygate.to';
|
|
const PAYGATE_DEFAULT_CHECKOUT_HOST = 'checkout.paygate.to';
|
|
const EVM_ADDRESS_RE = /^0x[a-fA-F0-9]{40}$/;
|
|
const EVM_TX_RE = /^0x[a-fA-F0-9]{64}$/;
|
|
|
|
const STABLE_PAYOUT_COINS = new Set([
|
|
'polygon_usdc',
|
|
'polygon_usdt',
|
|
'bep20_usdc',
|
|
'bep20_usdt',
|
|
'erc20_usdc',
|
|
'erc20_usdt'
|
|
]);
|
|
|
|
const NATIVE_PAYOUT_PATHS = Object.freeze({
|
|
polygon_pol: 'polygon/pol',
|
|
eth: 'eth',
|
|
bep20_bnb: 'bep20/bnb'
|
|
});
|
|
|
|
class PaygateProtocolError extends Error {
|
|
constructor(message, code, statusCode = 400, upstreamStatus = null) {
|
|
super(message);
|
|
this.name = 'PaygateProtocolError';
|
|
this.code = code;
|
|
this.statusCode = statusCode;
|
|
this.upstreamStatus = upstreamStatus;
|
|
}
|
|
}
|
|
|
|
function fail(message, code, statusCode = 400, upstreamStatus = null) {
|
|
throw new PaygateProtocolError(message, code, statusCode, upstreamStatus);
|
|
}
|
|
|
|
function readScalarString(value, fieldName) {
|
|
if (typeof value !== 'string') {
|
|
fail(`${fieldName} PayGate invalide`, 'PAYGATE_FIELD_INVALID');
|
|
}
|
|
const normalized = value.trim();
|
|
if (!normalized) {
|
|
fail(`${fieldName} PayGate manquant`, 'PAYGATE_FIELD_INVALID');
|
|
}
|
|
return normalized;
|
|
}
|
|
|
|
function normalizePaygateOpaqueValue(value, fieldName = 'valeur opaque') {
|
|
if (typeof value !== 'string' || !value.trim()) {
|
|
fail(`${fieldName} PayGate invalide`, 'PAYGATE_OPAQUE_VALUE_INVALID');
|
|
}
|
|
const raw = value.trim();
|
|
if (!raw.includes('%')) {
|
|
return raw;
|
|
}
|
|
|
|
let decoded;
|
|
try {
|
|
decoded = decodeURIComponent(raw);
|
|
} catch {
|
|
fail(`${fieldName} PayGate mal encodé`, 'PAYGATE_OPAQUE_VALUE_INVALID');
|
|
}
|
|
|
|
if (/%[0-9a-fA-F]{2}/.test(decoded)) {
|
|
fail(`${fieldName} PayGate doublement encodé`, 'PAYGATE_OPAQUE_VALUE_INVALID');
|
|
}
|
|
|
|
return decoded;
|
|
}
|
|
|
|
function setOpaqueQueryParam(searchParams, key, value) {
|
|
searchParams.set(key, normalizePaygateOpaqueValue(value, key));
|
|
}
|
|
|
|
function safeComparePaygateNonce(expected, actual) {
|
|
if (typeof expected !== 'string' || typeof actual !== 'string') {
|
|
return false;
|
|
}
|
|
const expectedDigest = crypto.createHash('sha256').update(expected, 'utf8').digest();
|
|
const actualDigest = crypto.createHash('sha256').update(actual, 'utf8').digest();
|
|
const equalDigest = crypto.timingSafeEqual(expectedDigest, actualDigest);
|
|
return equalDigest && Buffer.byteLength(expected) === Buffer.byteLength(actual);
|
|
}
|
|
|
|
function readOptionalScalarString(value, fieldName) {
|
|
if (value === undefined || value === null) return null;
|
|
return readScalarString(value, fieldName);
|
|
}
|
|
|
|
function normalizeCallbackBaseUrl(value, { requireHttps = false } = {}) {
|
|
const raw = readScalarString(value, 'callback base URL');
|
|
let url;
|
|
try {
|
|
url = new URL(raw);
|
|
} catch {
|
|
fail('Callback base URL PayGate invalide', 'PAYGATE_CALLBACK_BASE_INVALID');
|
|
}
|
|
|
|
if (!['http:', 'https:'].includes(url.protocol)) {
|
|
fail('Protocole callback PayGate invalide', 'PAYGATE_CALLBACK_BASE_INVALID');
|
|
}
|
|
if (requireHttps && url.protocol !== 'https:') {
|
|
fail('HTTPS requis pour le callback PayGate', 'PAYGATE_CALLBACK_HTTPS_REQUIRED');
|
|
}
|
|
if (
|
|
url.username
|
|
|| url.password
|
|
|| url.pathname !== '/'
|
|
|| url.search
|
|
|| url.hash
|
|
) {
|
|
fail('Callback base URL PayGate non canonique', 'PAYGATE_CALLBACK_BASE_INVALID');
|
|
}
|
|
|
|
return url.origin;
|
|
}
|
|
|
|
function parseCallbackQueryEntries(rawQuery) {
|
|
if (!rawQuery) return [];
|
|
|
|
const entries = [];
|
|
const keys = new Set();
|
|
for (const component of rawQuery.split('&')) {
|
|
const separatorIndex = component.indexOf('=');
|
|
const rawKey = separatorIndex === -1 ? component : component.slice(0, separatorIndex);
|
|
const rawValue = separatorIndex === -1 ? '' : component.slice(separatorIndex + 1);
|
|
let key;
|
|
try {
|
|
key = readScalarString(decodeURIComponent(rawKey), 'clé callback_url');
|
|
} catch (error) {
|
|
if (error instanceof PaygateProtocolError) throw error;
|
|
fail('callback_url PayGate invalide', 'PAYGATE_CALLBACK_URL_INVALID');
|
|
}
|
|
if (keys.has(key)) {
|
|
fail('callback_url PayGate ambiguë', 'PAYGATE_CALLBACK_URL_INVALID');
|
|
}
|
|
keys.add(key);
|
|
entries.push([key, normalizePaygateOpaqueValue(rawValue, `valeur ${key}`)]);
|
|
}
|
|
return entries;
|
|
}
|
|
|
|
function canonicalizeCallbackUrl(value) {
|
|
const raw = readScalarString(value, 'callback_url');
|
|
let url;
|
|
try {
|
|
url = new URL(raw);
|
|
} catch {
|
|
fail('callback_url PayGate invalide', 'PAYGATE_CALLBACK_URL_INVALID');
|
|
}
|
|
|
|
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.hash) {
|
|
fail('callback_url PayGate invalide', 'PAYGATE_CALLBACK_URL_INVALID');
|
|
}
|
|
|
|
const sortedEntries = parseCallbackQueryEntries(url.search.slice(1))
|
|
.sort(([leftKey, leftValue], [rightKey, rightValue]) => (
|
|
leftKey.localeCompare(rightKey) || leftValue.localeCompare(rightValue)
|
|
));
|
|
url.search = sortedEntries
|
|
.map(([key, itemValue]) => `${encodeURIComponent(key)}=${encodeURIComponent(itemValue)}`)
|
|
.join('&');
|
|
return url.toString();
|
|
}
|
|
|
|
function buildPaygateCallbackUrl({ baseUrl, reference, nonce, requireHttps = false }) {
|
|
const normalizedBaseUrl = normalizeCallbackBaseUrl(baseUrl, { requireHttps });
|
|
const normalizedReference = readScalarString(reference, 'reference');
|
|
const normalizedNonce = readScalarString(nonce, 'nonce');
|
|
if (!/^[a-f0-9]{48}$/i.test(normalizedReference)) {
|
|
fail('Référence callback PayGate invalide', 'PAYGATE_CALLBACK_REFERENCE_INVALID');
|
|
}
|
|
if (!/^[a-f0-9]{48}$/i.test(normalizedNonce)) {
|
|
fail('Nonce callback PayGate invalide', 'PAYGATE_CALLBACK_NONCE_INVALID');
|
|
}
|
|
|
|
const url = new URL('/api/vip/paygate/callback', normalizedBaseUrl);
|
|
url.searchParams.set('reference', normalizedReference.toLowerCase());
|
|
url.searchParams.set('nonce', normalizedNonce.toLowerCase());
|
|
return url.toString();
|
|
}
|
|
|
|
function normalizeCheckoutHost(value = PAYGATE_DEFAULT_CHECKOUT_HOST) {
|
|
const raw = readScalarString(value, 'domaine checkout')
|
|
.replace(/^https:\/\//i, '');
|
|
if (/[/@?#]/.test(raw)) {
|
|
fail('Domaine checkout PayGate invalide', 'PAYGATE_CHECKOUT_HOST_INVALID');
|
|
}
|
|
let url;
|
|
try {
|
|
url = new URL(`https://${raw}`);
|
|
} catch {
|
|
fail('Domaine checkout PayGate invalide', 'PAYGATE_CHECKOUT_HOST_INVALID');
|
|
}
|
|
return url.host;
|
|
}
|
|
|
|
function buildPaygateCheckoutUrl({
|
|
checkoutAddress,
|
|
amountEur,
|
|
payerEmail,
|
|
branding = {},
|
|
checkoutHost = PAYGATE_DEFAULT_CHECKOUT_HOST
|
|
}) {
|
|
const numericAmount = Number(amountEur);
|
|
if (!Number.isFinite(numericAmount) || numericAmount <= 0) {
|
|
fail('Montant checkout PayGate invalide', 'PAYGATE_AMOUNT_INVALID');
|
|
}
|
|
|
|
const email = readScalarString(payerEmail, 'email');
|
|
const url = new URL(`https://${normalizeCheckoutHost(checkoutHost)}/pay.php`);
|
|
setOpaqueQueryParam(url.searchParams, 'address', checkoutAddress);
|
|
url.searchParams.set('amount', numericAmount.toFixed(2));
|
|
url.searchParams.set('email', email);
|
|
url.searchParams.set('currency', 'EUR');
|
|
for (const key of ['logo', 'background', 'theme', 'button']) {
|
|
if (typeof branding[key] === 'string' && branding[key].trim()) {
|
|
url.searchParams.set(key, branding[key].trim());
|
|
}
|
|
}
|
|
return url.toString();
|
|
}
|
|
|
|
function parsePaygateWalletResponse(payload, expectedCallbackUrl) {
|
|
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
fail('Réponse wallet PayGate invalide', 'PAYGATE_WALLET_RESPONSE_INVALID', 502);
|
|
}
|
|
|
|
const checkoutAddress = normalizePaygateOpaqueValue(payload.address_in, 'address_in');
|
|
const temporaryWalletAddress = readScalarString(
|
|
payload.polygon_address_in,
|
|
'polygon_address_in'
|
|
);
|
|
if (!EVM_ADDRESS_RE.test(temporaryWalletAddress)) {
|
|
fail('Adresse temporaire PayGate invalide', 'PAYGATE_WALLET_ADDRESS_INVALID', 502);
|
|
}
|
|
|
|
const callbackUrl = canonicalizeCallbackUrl(payload.callback_url);
|
|
const expected = canonicalizeCallbackUrl(expectedCallbackUrl);
|
|
if (callbackUrl !== expected) {
|
|
fail('Callback wallet PayGate incohérent', 'PAYGATE_WALLET_CALLBACK_MISMATCH', 502);
|
|
}
|
|
|
|
return {
|
|
checkoutAddress,
|
|
temporaryWalletAddress,
|
|
callbackUrl,
|
|
ipnToken: normalizePaygateOpaqueValue(payload.ipn_token, 'ipn_token')
|
|
};
|
|
}
|
|
|
|
function parsePositiveNumber(value, fieldName) {
|
|
if ((typeof value !== 'string' && typeof value !== 'number') || value === '') {
|
|
fail(`${fieldName} PayGate invalide`, 'PAYGATE_AMOUNT_INVALID');
|
|
}
|
|
const parsed = Number(value);
|
|
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
fail(`${fieldName} PayGate invalide`, 'PAYGATE_AMOUNT_INVALID');
|
|
}
|
|
return parsed;
|
|
}
|
|
|
|
function normalizePayoutCoin(value) {
|
|
const coin = readScalarString(value, 'coin').toLowerCase();
|
|
if (!STABLE_PAYOUT_COINS.has(coin) && !Object.hasOwn(NATIVE_PAYOUT_PATHS, coin)) {
|
|
fail('Coin PayGate non supporté', 'PAYGATE_COIN_UNSUPPORTED');
|
|
}
|
|
return coin;
|
|
}
|
|
|
|
function parsePaygateCallbackPayload(payload) {
|
|
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
fail('Callback PayGate invalide', 'PAYGATE_CALLBACK_INVALID');
|
|
}
|
|
|
|
const reference = readOptionalScalarString(payload.reference, 'reference');
|
|
const publicId = readOptionalScalarString(
|
|
payload.publicId ?? payload.public_id,
|
|
'publicId'
|
|
);
|
|
if ((reference && publicId) || (!reference && !publicId)) {
|
|
fail('Identifiant callback PayGate invalide', 'PAYGATE_CALLBACK_IDENTIFIER_INVALID');
|
|
}
|
|
if (reference && !/^[a-f0-9]{48}$/i.test(reference)) {
|
|
fail('Référence callback PayGate invalide', 'PAYGATE_CALLBACK_REFERENCE_INVALID');
|
|
}
|
|
if (publicId && !/^inv_[a-f0-9]{24}$/i.test(publicId)) {
|
|
fail('publicId callback PayGate invalide', 'PAYGATE_CALLBACK_PUBLIC_ID_INVALID');
|
|
}
|
|
|
|
const nonce = readScalarString(payload.nonce, 'nonce');
|
|
if (!/^[a-f0-9]{48}$/i.test(nonce)) {
|
|
fail('Nonce callback PayGate invalide', 'PAYGATE_CALLBACK_NONCE_INVALID');
|
|
}
|
|
|
|
const addressIn = readScalarString(payload.address_in, 'address_in');
|
|
const txidIn = readScalarString(payload.txid_in, 'txid_in');
|
|
const txidOut = readScalarString(payload.txid_out, 'txid_out');
|
|
if (!EVM_ADDRESS_RE.test(addressIn)) {
|
|
fail('Adresse callback PayGate invalide', 'PAYGATE_CALLBACK_ADDRESS_INVALID');
|
|
}
|
|
if (!EVM_TX_RE.test(txidIn) || !EVM_TX_RE.test(txidOut)) {
|
|
fail('Transaction callback PayGate invalide', 'PAYGATE_CALLBACK_TX_INVALID');
|
|
}
|
|
const normalizedTxidIn = txidIn.toLowerCase();
|
|
const normalizedTxidOut = txidOut.toLowerCase();
|
|
if (normalizedTxidIn === normalizedTxidOut) {
|
|
fail('Transactions callback PayGate identiques', 'PAYGATE_CALLBACK_TX_DUPLICATE');
|
|
}
|
|
|
|
return {
|
|
reference: reference ? reference.toLowerCase() : null,
|
|
publicId: publicId || null,
|
|
nonce: nonce.toLowerCase(),
|
|
coin: normalizePayoutCoin(payload.coin),
|
|
valueCoin: parsePositiveNumber(payload.value_coin, 'value_coin'),
|
|
valueForwardedCoin: parsePositiveNumber(
|
|
payload.value_forwarded_coin,
|
|
'value_forwarded_coin'
|
|
),
|
|
txidIn: normalizedTxidIn,
|
|
txidOut: normalizedTxidOut,
|
|
addressIn: addressIn.toLowerCase()
|
|
};
|
|
}
|
|
|
|
function parsePaygatePaymentStatus(payload) {
|
|
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
fail('Statut PayGate invalide', 'PAYGATE_STATUS_INVALID', 502);
|
|
}
|
|
const status = readScalarString(payload.status, 'status').toLowerCase();
|
|
if (status === 'unpaid') {
|
|
return { status: 'unpaid' };
|
|
}
|
|
if (status !== 'paid') {
|
|
fail('Statut PayGate inconnu', 'PAYGATE_STATUS_INVALID', 502);
|
|
}
|
|
|
|
const txidOut = readScalarString(payload.txid_out, 'txid_out');
|
|
if (!EVM_TX_RE.test(txidOut)) {
|
|
fail('Transaction statut PayGate invalide', 'PAYGATE_STATUS_TX_INVALID', 502);
|
|
}
|
|
return {
|
|
status: 'paid',
|
|
coin: normalizePayoutCoin(payload.coin),
|
|
valueCoin: parsePositiveNumber(payload.value_coin, 'value_coin'),
|
|
txidOut: txidOut.toLowerCase()
|
|
};
|
|
}
|
|
|
|
function roundUsd(value, decimalPlaces = 8) {
|
|
return Number(Number(value).toFixed(decimalPlaces));
|
|
}
|
|
|
|
function normalizeRoundedUsd(value, {
|
|
code,
|
|
message,
|
|
storageDecimalPlaces = null
|
|
}) {
|
|
let numericValue;
|
|
try {
|
|
numericValue = Number(value);
|
|
} catch {
|
|
fail(message, code, 502);
|
|
}
|
|
const roundedValue = roundUsd(numericValue);
|
|
if (!Number.isFinite(roundedValue) || roundedValue <= 0) {
|
|
fail(message, code, 502);
|
|
}
|
|
if (
|
|
storageDecimalPlaces !== null
|
|
&& roundUsd(roundedValue, storageDecimalPlaces) <= 0
|
|
) {
|
|
fail(message, code, 502);
|
|
}
|
|
return roundedValue;
|
|
}
|
|
|
|
function normalizePaygateInvoiceUsd(value) {
|
|
return normalizeRoundedUsd(value, {
|
|
code: 'PAYGATE_CONVERT_INVALID',
|
|
message: 'Conversion PayGate invalide',
|
|
storageDecimalPlaces: 2
|
|
});
|
|
}
|
|
|
|
async function convertPaygatePaymentToUsd(payment, { httpClient = axios, timeoutMs = 15000 } = {}) {
|
|
const coin = normalizePayoutCoin(payment?.coin);
|
|
const valueCoin = parsePositiveNumber(payment?.valueCoin, 'value_coin');
|
|
if (STABLE_PAYOUT_COINS.has(coin)) {
|
|
return normalizeRoundedUsd(valueCoin, {
|
|
code: 'PAYGATE_CONVERSION_INVALID',
|
|
message: 'Conversion PayGate invalide'
|
|
});
|
|
}
|
|
|
|
let response;
|
|
try {
|
|
response = await httpClient.get(
|
|
`${PAYGATE_API_BASE}/crypto/${NATIVE_PAYOUT_PATHS[coin]}/info.php`,
|
|
{ timeout: timeoutMs }
|
|
);
|
|
} catch (error) {
|
|
fail(
|
|
'Prix natif PayGate indisponible',
|
|
'PAYGATE_NATIVE_PRICE_UNAVAILABLE',
|
|
503,
|
|
error?.response?.status || null
|
|
);
|
|
}
|
|
|
|
let usdPrice;
|
|
try {
|
|
usdPrice = parsePositiveNumber(response?.data?.prices?.USD, 'prix USD');
|
|
} catch {
|
|
fail('Prix natif PayGate invalide', 'PAYGATE_NATIVE_PRICE_INVALID', 502);
|
|
}
|
|
const usdValue = valueCoin * usdPrice;
|
|
if (!Number.isFinite(usdValue) || usdValue <= 0) {
|
|
fail('Conversion native PayGate invalide', 'PAYGATE_NATIVE_CONVERSION_INVALID', 502);
|
|
}
|
|
return normalizeRoundedUsd(usdValue, {
|
|
code: 'PAYGATE_NATIVE_CONVERSION_INVALID',
|
|
message: 'Conversion native PayGate invalide'
|
|
});
|
|
}
|
|
|
|
function createPaygateClient({ httpClient = axios, timeoutMs = 15000 } = {}) {
|
|
return {
|
|
async convertEurToUsd(amountEur) {
|
|
const amount = parsePositiveNumber(amountEur, 'montant EUR');
|
|
let response;
|
|
try {
|
|
response = await httpClient.get(`${PAYGATE_API_BASE}/control/convert.php`, {
|
|
params: { value: amount.toFixed(2), from: 'eur' },
|
|
timeout: timeoutMs
|
|
});
|
|
} catch (error) {
|
|
fail(
|
|
'Conversion PayGate indisponible',
|
|
'PAYGATE_CONVERT_UNAVAILABLE',
|
|
503,
|
|
error?.response?.status || null
|
|
);
|
|
}
|
|
if (String(response?.data?.status || '').toLowerCase() !== 'success') {
|
|
fail('Réponse conversion PayGate invalide', 'PAYGATE_CONVERT_INVALID', 502);
|
|
}
|
|
return normalizePaygateInvoiceUsd(
|
|
parsePositiveNumber(response.data.value_coin, 'value_coin')
|
|
);
|
|
},
|
|
|
|
async createWallet({ settlementWallet, callbackUrl }) {
|
|
const wallet = readScalarString(settlementWallet, 'settlement wallet');
|
|
if (!EVM_ADDRESS_RE.test(wallet)) {
|
|
fail('Settlement wallet PayGate invalide', 'PAYGATE_SETTLEMENT_WALLET_INVALID', 500);
|
|
}
|
|
let response;
|
|
try {
|
|
response = await httpClient.get(`${PAYGATE_API_BASE}/control/wallet.php`, {
|
|
params: { address: wallet, callback: callbackUrl },
|
|
timeout: timeoutMs
|
|
});
|
|
} catch (error) {
|
|
fail(
|
|
'Création wallet PayGate indisponible',
|
|
'PAYGATE_WALLET_UNAVAILABLE',
|
|
503,
|
|
error?.response?.status || null
|
|
);
|
|
}
|
|
return parsePaygateWalletResponse(response?.data, callbackUrl);
|
|
},
|
|
|
|
async fetchPaymentStatus(ipnToken) {
|
|
const normalizedToken = normalizePaygateOpaqueValue(ipnToken, 'ipn_token');
|
|
let response;
|
|
try {
|
|
response = await httpClient.get(`${PAYGATE_API_BASE}/control/payment-status.php`, {
|
|
params: { ipn_token: normalizedToken },
|
|
timeout: timeoutMs
|
|
});
|
|
} catch (error) {
|
|
fail(
|
|
'Statut PayGate indisponible',
|
|
'PAYGATE_STATUS_UNAVAILABLE',
|
|
503,
|
|
error?.response?.status || null
|
|
);
|
|
}
|
|
return parsePaygatePaymentStatus(response?.data);
|
|
},
|
|
|
|
convertPaymentToUsd(payment) {
|
|
return convertPaygatePaymentToUsd(payment, { httpClient, timeoutMs });
|
|
}
|
|
};
|
|
}
|
|
|
|
module.exports = {
|
|
PAYGATE_API_BASE,
|
|
PAYGATE_DEFAULT_CHECKOUT_HOST,
|
|
STABLE_PAYOUT_COINS,
|
|
NATIVE_PAYOUT_PATHS,
|
|
EVM_ADDRESS_RE,
|
|
EVM_TX_RE,
|
|
PaygateProtocolError,
|
|
normalizePaygateOpaqueValue,
|
|
setOpaqueQueryParam,
|
|
safeComparePaygateNonce,
|
|
normalizeCallbackBaseUrl,
|
|
canonicalizeCallbackUrl,
|
|
buildPaygateCallbackUrl,
|
|
buildPaygateCheckoutUrl,
|
|
parsePaygateWalletResponse,
|
|
parsePaygateCallbackPayload,
|
|
parsePaygatePaymentStatus,
|
|
normalizePaygateInvoiceUsd,
|
|
convertPaygatePaymentToUsd,
|
|
createPaygateClient
|
|
};
|