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) <noreply@anthropic.com>
This commit is contained in:
schakal00783
2026-09-21 16:28:14 +02:00
co-authored by Claude Opus 5
parent e962be41ea
commit 2710baa615
2 changed files with 16 additions and 8 deletions
+13 -7
View File
@@ -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 });
+3 -1
View File
@@ -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;