0 ? $cents : null; } /** * Matches a CSV name against a participant's paypal_name or display_name * (case-insensitive), the same rule the legacy CSV import used. * * @return array{participant_id: int, legacy_mitarbeiter_id: ?int, display_name: string}|null */ function imports_find_participant(PDO $pdo, int $tenantId, string $name): ?array { $name = trim($name); if ($name === '') { return null; } $stmt = $pdo->prepare( 'SELECT id, legacy_mitarbeiter_id, display_name FROM participants WHERE tenant_id = ? AND (LOWER(paypal_name) = LOWER(?) OR LOWER(display_name) = LOWER(?)) LIMIT 1' ); $stmt->execute([$tenantId, $name, $name]); $row = $stmt->fetch(); if ($row === false) { return null; } return [ 'participant_id' => (int)$row['id'], 'legacy_mitarbeiter_id' => $row['legacy_mitarbeiter_id'] !== null ? (int)$row['legacy_mitarbeiter_id'] : null, 'display_name' => (string)$row['display_name'], ]; } /** * A payment counts as a duplicate if the same participant already has a * non-voided ledger payment of the same amount on the same day. */ function imports_is_duplicate(PDO $pdo, int $tenantId, int $participantId, int $amountCents, string $bookedAtDate): bool { $stmt = $pdo->prepare( "SELECT COUNT(*) FROM ledger_entries WHERE tenant_id = ? AND participant_id = ? AND type = 'payment' AND amount_cents = ? AND DATE(booked_at) = ? AND voided_at IS NULL" ); $stmt->execute([$tenantId, $participantId, $amountCents, $bookedAtDate]); return (int)$stmt->fetchColumn() > 0; } /** * Parses a PayPal-style export CSV: column 0 is the booking date, column 3 * the payer name, column 7 the gross amount. The first line is a header and * is skipped. * * @return list}> */ function imports_parse_csv(string $filePath): array { $handle = fopen($filePath, 'r'); if ($handle === false) { throw new RuntimeException('Die CSV-Datei konnte nicht gelesen werden.'); } $rows = []; $rowNumber = 0; fgetcsv($handle); // Header ueberspringen while (($data = fgetcsv($handle, 1000, ',')) !== false) { $rowNumber++; $rows[] = [ 'row_number' => $rowNumber, 'raw_name' => trim((string)($data[3] ?? '')), 'amount_raw' => trim((string)($data[7] ?? '')), 'date_raw' => trim((string)($data[0] ?? '')), 'raw' => $data, ]; } fclose($handle); return $rows; } function imports_create_batch(PDO $pdo, int $tenantId, string $originalFilename, string $checksum, ?int $createdByUserId): int { $stmt = $pdo->prepare( 'INSERT INTO payment_import_batches (tenant_id, original_filename, checksum, status, created_by_user_id) VALUES (?, ?, ?, ?, ?)' ); $stmt->execute([$tenantId, $originalFilename, $checksum, 'previewed', $createdByUserId]); return (int)$pdo->lastInsertId(); } function imports_store_row( PDO $pdo, int $batchId, int $rowNumber, ?int $participantId, string $rawName, int $amountCents, string $bookedAt, string $status, array $rawRow ): int { $stmt = $pdo->prepare( 'INSERT INTO payment_import_rows (batch_id, row_num, participant_id, raw_name, amount_cents, booked_at, status, raw_json) VALUES (?, ?, ?, ?, ?, ?, ?, ?)' ); $stmt->execute([ $batchId, $rowNumber, $participantId, $rawName, $amountCents, $bookedAt, $status, json_encode($rawRow, JSON_UNESCAPED_UNICODE), ]); return (int)$pdo->lastInsertId(); } /** * @return array{batch: array{id: int, original_filename: string, status: string, created_at: string}, rows: list}|null */ function imports_fetch_batch(PDO $pdo, int $tenantId, int $batchId): ?array { $stmt = $pdo->prepare( 'SELECT id, original_filename, status, created_at FROM payment_import_batches WHERE id = ? AND tenant_id = ?' ); $stmt->execute([$batchId, $tenantId]); $batch = $stmt->fetch(); if ($batch === false) { return null; } $stmt = $pdo->prepare( 'SELECT r.id, r.row_num, r.participant_id, r.raw_name, r.amount_cents, r.booked_at, r.status, p.display_name FROM payment_import_rows r LEFT JOIN participants p ON p.id = r.participant_id WHERE r.batch_id = ? ORDER BY r.row_num' ); $stmt->execute([$batchId]); return [ 'batch' => [ 'id' => (int)$batch['id'], 'original_filename' => (string)$batch['original_filename'], 'status' => (string)$batch['status'], 'created_at' => (string)$batch['created_at'], ], 'rows' => $stmt->fetchAll(), ]; } /** * Commits every 'matched' row of a previewed batch into the ledger: for * participants with a legacy_mitarbeiter_id (default tenant), a matching * kl_Einzahlungen row is dual-written and mirrored; every other tenant is * booked straight into the ledger. Already-committed batches are rejected. * * @return array{imported: int} */ function imports_commit_batch(PDO $pdo, int $tenantId, int $batchId): array { $stmt = $pdo->prepare('SELECT status FROM payment_import_batches WHERE id = ? AND tenant_id = ?'); $stmt->execute([$batchId, $tenantId]); $status = $stmt->fetchColumn(); if ($status === false) { throw new RuntimeException('Der Import wurde nicht gefunden.'); } if ($status !== 'previewed') { throw new RuntimeException('Dieser Import wurde bereits verarbeitet.'); } $stmt = $pdo->prepare( "SELECT r.id, r.participant_id, r.amount_cents, r.booked_at, p.legacy_mitarbeiter_id FROM payment_import_rows r JOIN participants p ON p.id = r.participant_id WHERE r.batch_id = ? AND r.status = 'matched'" ); $stmt->execute([$batchId]); $rows = $stmt->fetchAll(); $imported = 0; $pdo->beginTransaction(); try { $insertLegacy = $pdo->prepare( 'INSERT INTO kl_Einzahlungen (MitarbeiterID, Betrag, Datum) VALUES (?, ?, ?)' ); $updateRow = $pdo->prepare( "UPDATE payment_import_rows SET status = 'imported', ledger_entry_id = ? WHERE id = ?" ); foreach ($rows as $row) { $legacyMitarbeiterId = $row['legacy_mitarbeiter_id'] !== null ? (int)$row['legacy_mitarbeiter_id'] : null; $amountCents = (int)$row['amount_cents']; if ($legacyMitarbeiterId !== null) { $insertLegacy->execute([$legacyMitarbeiterId, $amountCents / 100, $row['booked_at']]); $legacyPaymentId = (int)$pdo->lastInsertId(); ledger_mirror_legacy_payment($pdo, $tenantId, $legacyPaymentId); $entryStmt = $pdo->prepare( "SELECT id FROM ledger_entries WHERE tenant_id = ? AND legacy_table = 'kl_Einzahlungen' AND legacy_id = ?" ); $entryStmt->execute([$tenantId, $legacyPaymentId]); $ledgerEntryId = (int)$entryStmt->fetchColumn(); } else { $ledgerEntryId = ledger_record_payment($pdo, $tenantId, (int)$row['participant_id'], $amountCents, 'csv_import'); } $updateRow->execute([$ledgerEntryId, $row['id']]); $imported++; } $pdo->prepare("UPDATE payment_import_batches SET status = 'committed', committed_at = NOW() WHERE id = ?") ->execute([$batchId]); $pdo->commit(); } catch (Throwable $e) { if ($pdo->inTransaction()) { $pdo->rollBack(); } throw $e; } return ['imported' => $imported]; }