259 lines
5.3 KiB
TypeScript
259 lines
5.3 KiB
TypeScript
export interface VatRateResponse {
|
|
country: string;
|
|
country_code: string;
|
|
standard_rate: number;
|
|
reduced_rates: number[];
|
|
currency: string;
|
|
}
|
|
|
|
const FALLBACK_RATES: VatRateResponse[] = [
|
|
{
|
|
country: "Austria",
|
|
country_code: "AT",
|
|
standard_rate: 20,
|
|
reduced_rates: [10, 13],
|
|
currency: "EUR",
|
|
},
|
|
{
|
|
country: "Belgium",
|
|
country_code: "BE",
|
|
standard_rate: 21,
|
|
reduced_rates: [6, 12],
|
|
currency: "EUR",
|
|
},
|
|
{
|
|
country: "Bulgaria",
|
|
country_code: "BG",
|
|
standard_rate: 20,
|
|
reduced_rates: [9],
|
|
currency: "EUR",
|
|
},
|
|
{
|
|
country: "Croatia",
|
|
country_code: "HR",
|
|
standard_rate: 25,
|
|
reduced_rates: [5, 13],
|
|
currency: "EUR",
|
|
},
|
|
{
|
|
country: "Cyprus",
|
|
country_code: "CY",
|
|
standard_rate: 19,
|
|
reduced_rates: [5, 9],
|
|
currency: "EUR",
|
|
},
|
|
{
|
|
country: "Czech Republic",
|
|
country_code: "CZ",
|
|
standard_rate: 21,
|
|
reduced_rates: [12, 15],
|
|
currency: "EUR",
|
|
},
|
|
{
|
|
country: "Denmark",
|
|
country_code: "DK",
|
|
standard_rate: 25,
|
|
reduced_rates: [],
|
|
currency: "EUR",
|
|
},
|
|
{
|
|
country: "Estonia",
|
|
country_code: "EE",
|
|
standard_rate: 22,
|
|
reduced_rates: [9],
|
|
currency: "EUR",
|
|
},
|
|
{
|
|
country: "Finland",
|
|
country_code: "FI",
|
|
standard_rate: 25.5,
|
|
reduced_rates: [10, 14],
|
|
currency: "EUR",
|
|
},
|
|
{
|
|
country: "France",
|
|
country_code: "FR",
|
|
standard_rate: 20,
|
|
reduced_rates: [5.5, 10],
|
|
currency: "EUR",
|
|
},
|
|
{
|
|
country: "Germany",
|
|
country_code: "DE",
|
|
standard_rate: 19,
|
|
reduced_rates: [7],
|
|
currency: "EUR",
|
|
},
|
|
{
|
|
country: "Greece",
|
|
country_code: "GR",
|
|
standard_rate: 24,
|
|
reduced_rates: [6, 13],
|
|
currency: "EUR",
|
|
},
|
|
{
|
|
country: "Hungary",
|
|
country_code: "HU",
|
|
standard_rate: 27,
|
|
reduced_rates: [5, 18],
|
|
currency: "EUR",
|
|
},
|
|
{
|
|
country: "Ireland",
|
|
country_code: "IE",
|
|
standard_rate: 23,
|
|
reduced_rates: [9, 13.5],
|
|
currency: "EUR",
|
|
},
|
|
{
|
|
country: "Italy",
|
|
country_code: "IT",
|
|
standard_rate: 22,
|
|
reduced_rates: [5, 10],
|
|
currency: "EUR",
|
|
},
|
|
{
|
|
country: "Latvia",
|
|
country_code: "LV",
|
|
standard_rate: 21,
|
|
reduced_rates: [5, 12],
|
|
currency: "EUR",
|
|
},
|
|
{
|
|
country: "Lithuania",
|
|
country_code: "LT",
|
|
standard_rate: 21,
|
|
reduced_rates: [5, 9],
|
|
currency: "EUR",
|
|
},
|
|
{
|
|
country: "Luxembourg",
|
|
country_code: "LU",
|
|
standard_rate: 17,
|
|
reduced_rates: [8],
|
|
currency: "EUR",
|
|
},
|
|
{
|
|
country: "Malta",
|
|
country_code: "MT",
|
|
standard_rate: 18,
|
|
reduced_rates: [5, 7],
|
|
currency: "EUR",
|
|
},
|
|
{
|
|
country: "Netherlands",
|
|
country_code: "NL",
|
|
standard_rate: 21,
|
|
reduced_rates: [9],
|
|
currency: "EUR",
|
|
},
|
|
{
|
|
country: "Poland",
|
|
country_code: "PL",
|
|
standard_rate: 23,
|
|
reduced_rates: [5, 8],
|
|
currency: "EUR",
|
|
},
|
|
{
|
|
country: "Portugal",
|
|
country_code: "PT",
|
|
standard_rate: 23,
|
|
reduced_rates: [6, 13],
|
|
currency: "EUR",
|
|
},
|
|
{
|
|
country: "Romania",
|
|
country_code: "RO",
|
|
standard_rate: 19,
|
|
reduced_rates: [5, 9],
|
|
currency: "EUR",
|
|
},
|
|
{
|
|
country: "Slovakia",
|
|
country_code: "SK",
|
|
standard_rate: 23,
|
|
reduced_rates: [5, 10],
|
|
currency: "EUR",
|
|
},
|
|
{
|
|
country: "Slovenia",
|
|
country_code: "SI",
|
|
standard_rate: 22,
|
|
reduced_rates: [5, 9.5],
|
|
currency: "EUR",
|
|
},
|
|
{
|
|
country: "Spain",
|
|
country_code: "ES",
|
|
standard_rate: 21,
|
|
reduced_rates: [4, 10],
|
|
currency: "EUR",
|
|
},
|
|
{
|
|
country: "Sweden",
|
|
country_code: "SE",
|
|
standard_rate: 25,
|
|
reduced_rates: [6, 12],
|
|
currency: "EUR",
|
|
},
|
|
];
|
|
|
|
const TWENTY_FOUR_HOURS = 24 * 60 * 60 * 1000;
|
|
|
|
let cache: { data: VatRateResponse[]; date: string; fetchedAt: number } | null =
|
|
null;
|
|
|
|
function todayDateString(): string {
|
|
return new Date().toISOString().slice(0, 10);
|
|
}
|
|
|
|
function isCacheValid(): boolean {
|
|
if (!cache) return false;
|
|
const age = Date.now() - cache.fetchedAt;
|
|
return age < TWENTY_FOUR_HOURS && cache.date === todayDateString();
|
|
}
|
|
|
|
function transformVatstackResponse(vatstackRates: any[]): VatRateResponse[] {
|
|
return vatstackRates.map((r: any) => ({
|
|
country: r.country_name ?? "",
|
|
country_code: r.country_code ?? "",
|
|
standard_rate: r.standard_rate ?? 0,
|
|
reduced_rates: r.reduced_rates ?? [],
|
|
currency: "EUR",
|
|
}));
|
|
}
|
|
|
|
export async function getAllRates(): Promise<VatRateResponse[]> {
|
|
if (isCacheValid()) return cache!.data;
|
|
|
|
const apiKey = "pk_live_3038ce089ee7197879fcf95e624da019";
|
|
|
|
try {
|
|
const response: any = await $fetch("https://api.vatstack.com/v1/rates", {
|
|
headers: { "X-API-Key": apiKey },
|
|
query: { member_state: true, limit: 100 },
|
|
});
|
|
|
|
const items: any[] = response.rates ?? response;
|
|
const euOnly = items.filter((r: any) => r.member_state === true);
|
|
const rates = transformVatstackResponse(euOnly);
|
|
cache = { data: rates, date: todayDateString(), fetchedAt: Date.now() };
|
|
return rates;
|
|
} catch {
|
|
// Fall back to hardcoded rates on fetch failure
|
|
cache = {
|
|
data: FALLBACK_RATES,
|
|
date: todayDateString(),
|
|
fetchedAt: Date.now(),
|
|
};
|
|
return FALLBACK_RATES;
|
|
}
|
|
}
|
|
|
|
export async function getRateByCode(
|
|
code: string,
|
|
): Promise<VatRateResponse | undefined> {
|
|
const rates = await getAllRates();
|
|
return rates.find((r) => r.country_code === code.toUpperCase());
|
|
}
|