Files
kaffeekasse-saas/app/ledger.php
T
clemens f9544f24fd M5 Kaffeeliste read-only auf Ledger umstellen
- kaffeeliste.php aus ledger_fetch_participant_summaries lesen lassen
- Legacy-Teilnehmerlinks ueber legacy_mitarbeiter_id erhalten
- Sidebar-Zugriff fuer Legacy-Admin und SaaS-Rollen trennen
- Smoke-Test um konkrete Ledger-Werte und Inaktiv-Ausschluss erweitern
- M5-App-Kern-Doku ergaenzen
2026-07-14 21:32:59 +02:00

284 lines
11 KiB
PHP

<?php
declare(strict_types=1);
require_once __DIR__ . '/bootstrap.php';
/**
* @return list<string>
*/
function ledger_normalize_email_norms(array $emails): array
{
$normalized = [];
foreach ($emails as $email) {
$emailNorm = strtolower(trim((string)$email));
if ($emailNorm !== '') {
$normalized[$emailNorm] = true;
}
}
return array_keys($normalized);
}
/**
* @return list<int>
*/
function ledger_normalize_ids(array $ids): array
{
$normalized = [];
foreach ($ids as $id) {
$id = (int)$id;
if ($id > 0) {
$normalized[$id] = true;
}
}
return array_keys($normalized);
}
function ledger_current_year(): int
{
return (int)date('Y');
}
/**
* @return array{total_payments_cents: int, total_costs_cents: int, total_marks: int, balance_cents: int, year_payments_cents: int, year_costs_cents: int, year_marks: int}
*/
function ledger_empty_totals(): array
{
return [
'total_payments_cents' => 0,
'total_costs_cents' => 0,
'total_marks' => 0,
'balance_cents' => 0,
'year_payments_cents' => 0,
'year_costs_cents' => 0,
'year_marks' => 0,
];
}
/**
* @return array{total_payments_cents: int, total_costs_cents: int, total_marks: int, balance_cents: int, year_payments_cents: int, year_costs_cents: int, year_marks: int}
*/
function ledger_fetch_tenant_totals(PDO $pdo, int $tenantId, ?int $year = null): array
{
$year = $year ?? ledger_current_year();
$stmt = $pdo->prepare(
"SELECT
COALESCE(SUM(CASE WHEN type = 'payment' THEN amount_cents ELSE 0 END), 0) AS total_payments_cents,
COALESCE(SUM(CASE WHEN type = 'consumption' THEN -amount_cents ELSE 0 END), 0) AS total_costs_cents,
COALESCE(SUM(CASE WHEN type = 'consumption' THEN marks_count ELSE 0 END), 0) AS total_marks,
COALESCE(SUM(amount_cents), 0) AS balance_cents,
COALESCE(SUM(CASE WHEN type = 'payment' AND YEAR(booked_at) = ? THEN amount_cents ELSE 0 END), 0) AS year_payments_cents,
COALESCE(SUM(CASE WHEN type = 'consumption' AND YEAR(booked_at) = ? THEN -amount_cents ELSE 0 END), 0) AS year_costs_cents,
COALESCE(SUM(CASE WHEN type = 'consumption' AND YEAR(booked_at) = ? THEN marks_count ELSE 0 END), 0) AS year_marks
FROM ledger_entries
WHERE tenant_id = ?
AND voided_at IS NULL"
);
$stmt->execute([$year, $year, $year, $tenantId]);
$row = $stmt->fetch();
if ($row === false) {
return ledger_empty_totals();
}
return [
'total_payments_cents' => (int)$row['total_payments_cents'],
'total_costs_cents' => (int)$row['total_costs_cents'],
'total_marks' => (int)$row['total_marks'],
'balance_cents' => (int)$row['balance_cents'],
'year_payments_cents' => (int)$row['year_payments_cents'],
'year_costs_cents' => (int)$row['year_costs_cents'],
'year_marks' => (int)$row['year_marks'],
];
}
/**
* Options:
* - year: int
* - active_only: bool|null
* - participant_ids: list<int>
* - email_norms: list<string>
*
* @return list<array{participant_id: int, tenant_id: int, legacy_mitarbeiter_id: ?int, display_name: string, email: ?string, email_norm: ?string, paypal_name: ?string, active: bool, total_payments_cents: int, total_costs_cents: int, total_marks: int, balance_cents: int, year_payments_cents: int, year_costs_cents: int, year_marks: int}>
*/
function ledger_fetch_participant_summaries(PDO $pdo, int $tenantId, array $options = []): array
{
$year = isset($options['year']) ? (int)$options['year'] : ledger_current_year();
$where = ['p.tenant_id = ?'];
$params = [$year, $year, $year, $tenantId];
if (array_key_exists('active_only', $options) && $options['active_only'] !== null) {
$where[] = 'p.active = ?';
$params[] = $options['active_only'] ? 1 : 0;
}
if (array_key_exists('participant_ids', $options)) {
$participantIds = is_array($options['participant_ids'])
? ledger_normalize_ids($options['participant_ids'])
: [];
if ($participantIds === []) {
return [];
}
$where[] = 'p.id IN (' . implode(',', array_fill(0, count($participantIds), '?')) . ')';
array_push($params, ...$participantIds);
}
if (array_key_exists('email_norms', $options)) {
$emailNorms = is_array($options['email_norms'])
? ledger_normalize_email_norms($options['email_norms'])
: [];
if ($emailNorms === []) {
return [];
}
$where[] = 'p.email_norm IN (' . implode(',', array_fill(0, count($emailNorms), '?')) . ')';
array_push($params, ...$emailNorms);
}
$sql = "
SELECT
p.id AS participant_id,
p.tenant_id,
p.legacy_mitarbeiter_id,
p.display_name,
p.email,
p.email_norm,
p.paypal_name,
p.active,
COALESCE(SUM(CASE WHEN le.type = 'payment' THEN le.amount_cents ELSE 0 END), 0) AS total_payments_cents,
COALESCE(SUM(CASE WHEN le.type = 'consumption' THEN -le.amount_cents ELSE 0 END), 0) AS total_costs_cents,
COALESCE(SUM(CASE WHEN le.type = 'consumption' THEN le.marks_count ELSE 0 END), 0) AS total_marks,
COALESCE(SUM(le.amount_cents), 0) AS balance_cents,
COALESCE(SUM(CASE WHEN le.type = 'payment' AND YEAR(le.booked_at) = ? THEN le.amount_cents ELSE 0 END), 0) AS year_payments_cents,
COALESCE(SUM(CASE WHEN le.type = 'consumption' AND YEAR(le.booked_at) = ? THEN -le.amount_cents ELSE 0 END), 0) AS year_costs_cents,
COALESCE(SUM(CASE WHEN le.type = 'consumption' AND YEAR(le.booked_at) = ? THEN le.marks_count ELSE 0 END), 0) AS year_marks
FROM participants p
LEFT JOIN ledger_entries le
ON le.tenant_id = p.tenant_id
AND le.participant_id = p.id
AND le.voided_at IS NULL
WHERE " . implode(' AND ', $where) . "
GROUP BY p.id, p.tenant_id, p.legacy_mitarbeiter_id, p.display_name, p.email, p.email_norm, p.paypal_name, p.active
ORDER BY p.display_name, p.id";
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$summaries = [];
while ($row = $stmt->fetch()) {
$summaries[] = [
'participant_id' => (int)$row['participant_id'],
'tenant_id' => (int)$row['tenant_id'],
'legacy_mitarbeiter_id' => $row['legacy_mitarbeiter_id'] !== null ? (int)$row['legacy_mitarbeiter_id'] : null,
'display_name' => (string)$row['display_name'],
'email' => $row['email'] !== null ? (string)$row['email'] : null,
'email_norm' => $row['email_norm'] !== null ? (string)$row['email_norm'] : null,
'paypal_name' => $row['paypal_name'] !== null ? (string)$row['paypal_name'] : null,
'active' => (int)$row['active'] === 1,
'total_payments_cents' => (int)$row['total_payments_cents'],
'total_costs_cents' => (int)$row['total_costs_cents'],
'total_marks' => (int)$row['total_marks'],
'balance_cents' => (int)$row['balance_cents'],
'year_payments_cents' => (int)$row['year_payments_cents'],
'year_costs_cents' => (int)$row['year_costs_cents'],
'year_marks' => (int)$row['year_marks'],
];
}
return $summaries;
}
/**
* @return array{participant_id: int, tenant_id: int, legacy_mitarbeiter_id: ?int, display_name: string, email: ?string, email_norm: ?string, paypal_name: ?string, active: bool, total_payments_cents: int, total_costs_cents: int, total_marks: int, balance_cents: int, year_payments_cents: int, year_costs_cents: int, year_marks: int}|null
*/
function ledger_fetch_participant_summary(PDO $pdo, int $tenantId, int $participantId, ?int $year = null): ?array
{
$summaries = ledger_fetch_participant_summaries($pdo, $tenantId, [
'participant_ids' => [$participantId],
'year' => $year ?? ledger_current_year(),
]);
return $summaries[0] ?? null;
}
/**
* Options:
* - participant_id: int
* - type: string
* - include_voided: bool
* - limit: int
*
* @return list<array{id: int, tenant_id: int, participant_id: int, display_name: string, type: string, amount_cents: int, marks_count: ?int, unit_price_cents: ?int, booked_at: string, source: string, note: ?string, legacy_table: ?string, legacy_id: ?int, voided_at: ?string}>
*/
function ledger_fetch_recent_entries(PDO $pdo, int $tenantId, array $options = []): array
{
$limit = isset($options['limit']) ? (int)$options['limit'] : 100;
$limit = max(1, min($limit, 500));
$where = ['le.tenant_id = ?'];
$params = [$tenantId];
if (empty($options['include_voided'])) {
$where[] = 'le.voided_at IS NULL';
}
if (!empty($options['participant_id'])) {
$where[] = 'le.participant_id = ?';
$params[] = (int)$options['participant_id'];
}
if (!empty($options['type'])) {
$where[] = 'le.type = ?';
$params[] = trim((string)$options['type']);
}
$sql = "
SELECT
le.id,
le.tenant_id,
le.participant_id,
p.display_name,
le.type,
le.amount_cents,
le.marks_count,
le.unit_price_cents,
le.booked_at,
le.source,
le.note,
le.legacy_table,
le.legacy_id,
le.voided_at
FROM ledger_entries le
JOIN participants p
ON p.tenant_id = le.tenant_id
AND p.id = le.participant_id
WHERE " . implode(' AND ', $where) . "
ORDER BY le.booked_at DESC, le.id DESC
LIMIT {$limit}";
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$entries = [];
while ($row = $stmt->fetch()) {
$entries[] = [
'id' => (int)$row['id'],
'tenant_id' => (int)$row['tenant_id'],
'participant_id' => (int)$row['participant_id'],
'display_name' => (string)$row['display_name'],
'type' => (string)$row['type'],
'amount_cents' => (int)$row['amount_cents'],
'marks_count' => $row['marks_count'] !== null ? (int)$row['marks_count'] : null,
'unit_price_cents' => $row['unit_price_cents'] !== null ? (int)$row['unit_price_cents'] : null,
'booked_at' => (string)$row['booked_at'],
'source' => (string)$row['source'],
'note' => $row['note'] !== null ? (string)$row['note'] : null,
'legacy_table' => $row['legacy_table'] !== null ? (string)$row['legacy_table'] : null,
'legacy_id' => $row['legacy_id'] !== null ? (int)$row['legacy_id'] : null,
'voided_at' => $row['voided_at'] !== null ? (string)$row['voided_at'] : null,
];
}
return $entries;
}