Fix LemFi EUR to NGN rate integration
This commit is contained in:
+2
-1
@@ -5,7 +5,8 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start"
|
"start": "next start",
|
||||||
|
"test": "node --test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"next": "^16.4.3",
|
"next": "^16.4.3",
|
||||||
|
|||||||
+11
-32
@@ -1,4 +1,4 @@
|
|||||||
import { log } from "console";
|
import { fetchLemfiRate } from "../../../utils/lemfi.js";
|
||||||
|
|
||||||
const COMMON_HEADERS = {
|
const COMMON_HEADERS = {
|
||||||
'accept': '*/*',
|
'accept': '*/*',
|
||||||
@@ -194,40 +194,19 @@ export default async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const lemfiCall = new Promise((resolve, reject) => {
|
const lemfiHref = "https://lemfi.com/en-gb/international-money-transfer";
|
||||||
fetch("https://lemfi.com/api/lemonade/v2/exchange", {
|
const lemfiProvider = "LemFi (referral code - OBIN6518)";
|
||||||
headers: {
|
const lemfiCall = fetchLemfiRate()
|
||||||
accept: "application/json",
|
.then((rate) => ({
|
||||||
"accept-language": "en-GB,en;q=0.9,nl-NL;q=0.8,nl;q=0.7,en-US;q=0.6",
|
rate: rate.toFixed(2),
|
||||||
"content-type": "application/json",
|
provider: lemfiProvider,
|
||||||
priority: "u=1, i",
|
|
||||||
"sec-ch-ua": '"Not(A:Brand";v="99", "Google Chrome";v="133", "Chromium";v="133"',
|
|
||||||
"sec-ch-ua-mobile": "?0",
|
|
||||||
"sec-ch-ua-platform": '"macOS"',
|
|
||||||
"sec-fetch-dest": "empty",
|
|
||||||
"sec-fetch-mode": "cors",
|
|
||||||
"sec-fetch-site": "same-origin"
|
|
||||||
},
|
|
||||||
referrer: "https://lemfi.com/",
|
|
||||||
referrerPolicy: "origin",
|
|
||||||
body: '{"from":"EUR","to":"NGN"}',
|
|
||||||
method: "POST",
|
|
||||||
mode: "cors",
|
|
||||||
credentials: "include"
|
|
||||||
}).then((response) => resolve(response.json()))
|
|
||||||
.catch((error) => reject(error));
|
|
||||||
}).then((result) => {
|
|
||||||
return {
|
|
||||||
rate: 0,
|
|
||||||
provider: "Lemfi(referal code - OBIN6518)",
|
|
||||||
bestRate: false,
|
bestRate: false,
|
||||||
href: "https://lemfi.com",
|
href: lemfiHref,
|
||||||
logo: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSMYTBiPB_joupdSSoBL1fA0wySDPcMpY92XQ&s"
|
logo: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSMYTBiPB_joupdSSoBL1fA0wySDPcMpY92XQ&s"
|
||||||
};
|
}))
|
||||||
})
|
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error("Error calling Lemfi:", error);
|
console.error("Error calling LemFi:", error);
|
||||||
return createDefaultErrorResponse("Lemfi(referal code - OBIN6518)", "https://lemfi.com");
|
return createDefaultErrorResponse(lemfiProvider, lemfiHref);
|
||||||
});
|
});
|
||||||
|
|
||||||
const afriexCall = new Promise((resolve, reject) => {
|
const afriexCall = new Promise((resolve, reject) => {
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { test } from "node:test";
|
||||||
|
import { fetchLemfiRate, parseLemfiQuote } from "../utils/lemfi.js";
|
||||||
|
|
||||||
|
// Captured from LemFi's Netherlands EUR → NGN calculator endpoint.
|
||||||
|
const quote = {
|
||||||
|
exchange_from: "EUR",
|
||||||
|
exchange_to: "NGN",
|
||||||
|
rate: "9.8057976951626334827662874e+25",
|
||||||
|
ID: "64eeb42d-7f05-4b50-a172-5e581358517a",
|
||||||
|
};
|
||||||
|
|
||||||
|
test("decodes the live quote to the rate displayed by LemFi", () => {
|
||||||
|
assert.equal(parseLemfiQuote({ data: quote }), 1522);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("keeps promotional rewards out of the base rate comparison", () => {
|
||||||
|
assert.equal(parseLemfiQuote({ data: { ...quote, promotion_rate: { rate: "1e30" } } }), 1522);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects malformed rates and currency pairs", () => {
|
||||||
|
for (const change of [
|
||||||
|
{ exchange_from: "GBP" }, { exchange_to: "USD" },
|
||||||
|
{ rate: undefined }, { rate: "invalid" }, { rate: true },
|
||||||
|
{ rate: "Infinity" }, { rate: "0" }, { rate: "-1" },
|
||||||
|
{ ID: undefined }, { ID: "abc" }, { ID: "000" },
|
||||||
|
]) {
|
||||||
|
assert.throws(() => parseLemfiQuote({ data: { ...quote, ...change } }));
|
||||||
|
}
|
||||||
|
assert.throws(() => parseLemfiQuote({}));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("requests the EUR to NGN Netherlands quote with a timeout", async (t) => {
|
||||||
|
t.mock.method(globalThis, "fetch", async (url, options) => {
|
||||||
|
assert.equal(url.origin + url.pathname, "https://lemfi.com/api/exchange-quote");
|
||||||
|
assert.deepEqual(Object.fromEntries(url.searchParams), {
|
||||||
|
from: "EUR", to: "NGN", country: "Netherlands", lang: "en",
|
||||||
|
});
|
||||||
|
assert.ok(options.signal instanceof AbortSignal);
|
||||||
|
return { ok: true, json: async () => ({ data: quote }) };
|
||||||
|
});
|
||||||
|
assert.equal(await fetchLemfiRate(), 1522);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects upstream HTTP and network errors", async (t) => {
|
||||||
|
const fetchMock = t.mock.method(globalThis, "fetch", async () => ({ ok: false, status: 503 }));
|
||||||
|
await assert.rejects(fetchLemfiRate(), /503/);
|
||||||
|
fetchMock.mock.mockImplementation(async () => { throw new Error("Network unavailable"); });
|
||||||
|
await assert.rejects(fetchLemfiRate(), /Network unavailable/);
|
||||||
|
});
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
export function parseLemfiQuote(result) {
|
||||||
|
const quote = result?.data;
|
||||||
|
if (quote?.exchange_from !== "EUR" || quote?.exchange_to !== "NGN") {
|
||||||
|
throw new Error("LemFi did not return a EUR to NGN quote");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match LemFi's calculator: divide the encoded rate by the digits in its ID.
|
||||||
|
// Use the base rate so referral rewards and LemFi Cash do not inflate comparisons.
|
||||||
|
const divisor = typeof quote.ID === "string" ? Number(quote.ID.replace(/\D/g, "")) : 0;
|
||||||
|
const encodedRate = typeof quote.rate === "string" || typeof quote.rate === "number"
|
||||||
|
? Number(quote.rate)
|
||||||
|
: NaN;
|
||||||
|
const rate = encodedRate / divisor;
|
||||||
|
if (!Number.isFinite(divisor) || divisor <= 0 || !Number.isFinite(rate) || rate <= 0) {
|
||||||
|
throw new Error("LemFi returned an invalid exchange rate");
|
||||||
|
}
|
||||||
|
|
||||||
|
return rate;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchLemfiRate() {
|
||||||
|
const url = new URL("https://lemfi.com/api/exchange-quote");
|
||||||
|
url.search = new URLSearchParams({ from: "EUR", to: "NGN", country: "Netherlands", lang: "en" });
|
||||||
|
const response = await fetch(url, {
|
||||||
|
headers: { accept: "application/json" },
|
||||||
|
signal: AbortSignal.timeout(8000),
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`LemFi quote request failed (${response.status})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseLemfiQuote(await response.json());
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user