From 2710baa615b91171e06573a718cb3410382eebe5 Mon Sep 17 00:00:00 2001 From: schakal00783 Date: Mon, 21 Sep 2026 16:28:14 +0200 Subject: [PATCH] fix(import): Sammelbuchungen und nicht abrechenbare Projekte anlegen Beim Nachziehen der Harvest-Historie sind zwei Regeln im Weg gestanden, die fuer eine Uebernahme nicht passen: - Beim Anlegen verlangte die API auch fuer nicht abrechenbare Projekte einen Satz, obwohl validateProject ihn direkt danach verwirft. Nur Abruf rechnet ueber den Satz am Projekt ab, also gilt die Pflicht auch nur dort. - Der Importweg liess hoechstens 24 Stunden je Tag zu. In Harvest stehen Sammelbuchungen ("Nachbuchung DORA September und Oktober", 80 Stunden auf einem Tag). Sie abzulehnen verliert die Stunden und verfaelscht das Restbudget, sie zu verteilen erfindet Leistungsdaten - deshalb kommen sie so herein, wie sie gebucht wurden. Eine Buchung darf jetzt bis zu 31 Tage umfassen, damit Zahlendreher weiter auffallen. Im Buchungsdialog bleibt die Tagesgrenze bestehen. Co-Authored-By: Claude Opus 5 (1M context) --- server/src/routes/imports.js | 20 +++++++++++++------- server/src/routes/projects.js | 4 +++- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/server/src/routes/imports.js b/server/src/routes/imports.js index 2262e9c..f0f0414 100644 --- a/server/src/routes/imports.js +++ b/server/src/routes/imports.js @@ -16,11 +16,21 @@ import { isBillable, resolvePot } from '../lib/projects.js'; * * Wiedererkennung ueber harvest_id: ein zweiter Lauf legt nichts doppelt an, * sondern meldet die Buchung als "schon vorhanden". + * + * Eine Ausnahme von den Fachregeln: die Grenze von 24 Stunden je Tag gilt hier + * nicht. In Harvest stehen Sammelbuchungen ("Nachbuchung DORA September und + * Oktober", 80 Stunden auf einem Tag). Wer sie ablehnt, verliert die Stunden + * und damit das Restbudget des Projekts; wer sie auf Tage verteilt, erfindet + * Leistungsdaten. Sie kommen deshalb so herein, wie sie gebucht wurden. Im + * Buchungsdialog bleibt die Tagesgrenze bestehen. */ const router = express.Router(); const MAX_BATCH = 1000; +// Sammelbuchungen duerfen mehr als einen Tag umfassen - hoechstens aber 31 Tage, +// damit Zahlendreher nicht unbemerkt durchgehen. +const MAX_MINUTES = 31 * 24 * 60; router.post('/entries', async (req, res, next) => { const body = req.body || {}; @@ -47,7 +57,9 @@ router.post('/entries', async (req, res, next) => { const note = row.note == null ? null : String(row.note).trim().slice(0, 1000) || null; if (!parseDate(date)) throw new Error('Datum fehlt oder ist unguelltig.'); - if (!Number.isInteger(minutes) || minutes < 1 || minutes > 24 * 60) throw new Error('Minuten muessen zwischen 1 und 1440 liegen.'); + if (!Number.isInteger(minutes) || minutes < 1 || minutes > MAX_MINUTES) { + throw new Error(`Minuten muessen zwischen 1 und ${MAX_MINUTES} liegen.`); + } // Schon importiert? Dann nicht erneut anlegen. if (harvestId) { @@ -77,12 +89,6 @@ router.post('/entries', async (req, res, next) => { task = found; } - const [[day]] = await conn.query( - 'SELECT COALESCE(SUM(minutes), 0) AS total FROM pulse_time_entries WHERE user_id = ? AND entry_date = ?', - [userId, date] - ); - if (Number(day.total) + minutes > 24 * 60) throw new Error('An diesem Tag waeren es mehr als 24 Stunden.'); - const billable = isBillable(project, task); // Historische Uebernahme: abgelaufene Toepfe nehmen ihre eigenen Zeiten noch an. const pot = await resolvePot(conn, project, date, minutes, null, billable, { historic: true }); diff --git a/server/src/routes/projects.js b/server/src/routes/projects.js index c35da9d..14fe60d 100644 --- a/server/src/routes/projects.js +++ b/server/src/routes/projects.js @@ -195,7 +195,9 @@ router.post('/', requireRole('lead'), async (req, res, next) => { errors.push(...result.errors); pots.push(result.pot); } - } else if (!out.rate) { + } else if (out.type === 'abruf' && !out.rate) { + // Nur Abruf rechnet ueber den Satz am Projekt ab; nicht abrechenbare + // Projekte brauchen keinen - er wird gleich darunter ohnehin verworfen. errors.push('Abruf: Stundensatz oder Tagessatz fehlt.'); } if (out.type !== 'abruf') out.rate = null;