feat: init

This commit is contained in:
2026-02-13 22:02:30 +01:00
commit 8f9ff830fb
16711 changed files with 3307340 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
export default defineEventHandler(async (event) => {
const code = getRouterParam(event, 'code') ?? ''
const rate = await getRateByCode(code)
if (!rate) {
throw createError({ statusCode: 404, statusMessage: 'Country not found', data: { error: 'Country not found' } })
}
setHeader(event, 'Cache-Control', 'public, max-age=3600')
return rate
})

View File

@@ -0,0 +1,5 @@
export default defineEventHandler(async (event) => {
const rates = await getAllRates()
setHeader(event, 'Cache-Control', 'public, max-age=3600')
return rates
})

View File

@@ -0,0 +1,32 @@
const WINDOW_MS = 6000;
const MAX_REQUESTS = 60;
const hits = new Map<string, { count: number; resetAt: number }>();
export default defineEventHandler((event) => {
const path = getRequestURL(event).pathname;
if (!path.startsWith("/api/")) return;
const ip = getRequestIP(event, { xForwardedFor: true }) ?? "127.0.0.1";
const now = Date.now();
let entry = hits.get(ip);
if (!entry || now >= entry.resetAt) {
entry = { count: 0, resetAt: now + WINDOW_MS };
hits.set(ip, entry);
}
entry.count++;
setHeader(event, "X-RateLimit-Limit", MAX_REQUESTS);
setHeader(
event,
"X-RateLimit-Remaining",
Math.max(0, MAX_REQUESTS - entry.count),
);
if (entry.count > MAX_REQUESTS) {
setHeader(event, "Retry-After", "1");
throw createError({ statusCode: 429, statusMessage: "Too Many Requests" });
}
});

258
server/utils/vatRates.ts Normal file
View File

@@ -0,0 +1,258 @@
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());
}