131 lines
3.9 KiB
TypeScript
131 lines
3.9 KiB
TypeScript
// routes/api/import/incomes.ts
|
|
// Import incomes from JSON; skip items that already exist (by id or by content).
|
|
|
|
import { define } from "../../../utils.ts";
|
|
import {
|
|
createIncome,
|
|
getIncomesByUser,
|
|
} from "../../../services/financhalServices.ts";
|
|
import { SessionCookie } from "../../auth/callback.ts";
|
|
|
|
type ImportItem = {
|
|
id?: string;
|
|
source?: string | null;
|
|
amount?: number | null;
|
|
frequency?: string | null;
|
|
received_date?: string | number | null;
|
|
};
|
|
|
|
function signature(inc: {
|
|
source?: string | null;
|
|
amount?: number | null;
|
|
frequency?: string | null;
|
|
received_date?: number | null;
|
|
}): string {
|
|
const r = inc.received_date ?? 0;
|
|
return `${inc.source ?? ""}|${inc.amount ?? 0}|${inc.frequency ?? ""}|${r}`;
|
|
}
|
|
|
|
export const handler = define.handlers({
|
|
async POST(ctx) {
|
|
try {
|
|
const stateData: SessionCookie = ctx.state.data;
|
|
const user_id = stateData?.user_id;
|
|
if (!user_id) {
|
|
return new Response(
|
|
JSON.stringify({ error: "Unauthorized" }),
|
|
{ status: 401, headers: { "Content-Type": "application/json" } },
|
|
);
|
|
}
|
|
|
|
const contentType = ctx.req.headers.get("content-type") ?? "";
|
|
if (!contentType.includes("application/json")) {
|
|
return new Response(
|
|
JSON.stringify({ error: "Content-Type must be application/json" }),
|
|
{ status: 400, headers: { "Content-Type": "application/json" } },
|
|
);
|
|
}
|
|
|
|
const body = await ctx.req.json();
|
|
const raw = Array.isArray(body) ? body : body?.incomes;
|
|
if (!Array.isArray(raw)) {
|
|
return new Response(
|
|
JSON.stringify({
|
|
error: "Body must be an array or { incomes: [] }",
|
|
}),
|
|
{ status: 400, headers: { "Content-Type": "application/json" } },
|
|
);
|
|
}
|
|
|
|
const existing = await getIncomesByUser(user_id);
|
|
const existingIds = new Set(existing.map((i) => i.id));
|
|
const existingSigs = new Set(
|
|
existing.map((i) =>
|
|
signature({
|
|
source: i.source,
|
|
amount: i.amount,
|
|
frequency: i.frequency,
|
|
received_date: i.received_date,
|
|
})
|
|
),
|
|
);
|
|
|
|
let added = 0;
|
|
for (const item of raw as ImportItem[]) {
|
|
const id = typeof item.id === "string" ? item.id : undefined;
|
|
if (id && existingIds.has(id)) continue;
|
|
|
|
const amount = item.amount != null ? Number(item.amount) : null;
|
|
const receivedRaw = item.received_date;
|
|
let received_date: number;
|
|
if (receivedRaw == null || receivedRaw === "") {
|
|
received_date = Date.now();
|
|
} else if (typeof receivedRaw === "number") {
|
|
received_date = receivedRaw;
|
|
} else {
|
|
received_date =
|
|
Date.parse(String(receivedRaw).replace(/Z?$/, "T00:00:00")) ||
|
|
Date.now();
|
|
}
|
|
const src = item.source != null ? String(item.source) : null;
|
|
const freq = (item.frequency != null ? String(item.frequency) : "") ||
|
|
"monthly";
|
|
|
|
const sig = signature({
|
|
source: src,
|
|
amount,
|
|
frequency: freq,
|
|
received_date,
|
|
});
|
|
if (existingSigs.has(sig)) continue;
|
|
|
|
await createIncome(
|
|
user_id,
|
|
src,
|
|
amount != null && amount > 0 ? amount : null,
|
|
freq,
|
|
received_date,
|
|
"",
|
|
);
|
|
added++;
|
|
existingSigs.add(sig);
|
|
}
|
|
|
|
return new Response(
|
|
JSON.stringify({
|
|
added,
|
|
total: raw.length,
|
|
skipped: raw.length - added,
|
|
}),
|
|
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
);
|
|
} catch (e) {
|
|
console.error("Import incomes error:", e);
|
|
return new Response(
|
|
JSON.stringify({ error: "Failed to import incomes" }),
|
|
{ status: 500, headers: { "Content-Type": "application/json" } },
|
|
);
|
|
}
|
|
},
|
|
});
|