Legacy-Abbau Schritt 3+4: kl_*-Tabellen und legacy_*-Spalten entfernt
Abschluss des Legacy-Abbaus. Die Migration in participants/ledger_entries war
laut Dev-DB vollstaendig (keine legacy_mitarbeiter_id, kein legacy_table), die
kl_*-Tabellen enthielten nur noch Golden-Master-Testfixtures. Da kein echter
Altbestand mehr importiert werden muss (Prod entsteht aus der jetzigen Dev-DB),
kommt die Altwelt komplett raus.
Laufzeit-Code (Schritt 3a):
- ledger.php: Option legacy_mitarbeiter_ids, die Spalten aus allen SELECTs und
Rueckgaben, ledger_fetch_participant_summary_by_legacy_id sowie das Loeschen
gespiegelter Legacy-Zeilen in ledger_void_entry/ledger_void_own_self_entry
entfernt
- imports.php, paypal-inbox.php: legacy_mitarbeiter_id aus Ergebnis und
Typannotationen entfernt
- ledger-preview.php: Spalte "Legacy" entfernt
Skripte (Schritt 3b/3c):
- die acht reinen Legacy-/Golden-Master-Skripte entfernt (backfill-*,
seed-golden-master, golden-master-data, check-golden-master, check-m4-*,
check-m3-saas-basis)
- init-mysql-dev.php legt jetzt einen SaaS-Mandanten mit Owner-Login,
Teilnehmer, Journalbuchungen und Hinweis an statt kl_*-Zeilen
Schema (Schritt 4, Migration 0024):
- participants.legacy_mitarbeiter_id + uq_participants_tenant_legacy
- ledger_entries.legacy_table/legacy_id + uq_ledger_entries_tenant_legacy
- DROP der Tabellen kl_Einzahlungen, kl_Kaffeeverbrauch, kl_hinweise,
kl_config, kl_Mitarbeiter
Verifikation unveraendert gruen: http-smoke 33/0, role-matrix 55/0,
tenant-isolation 11/1 (Altfehler), m3-auth/tenant-resolution/billing gruen.
check-m3-settings-flow 7/6 ist vorbestehend (schon bei 3e24910 rot, mit
spaeteren Settings-Feldern nicht synchron) und unabhaengig von diesem Abbau.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+3
-4
@@ -23,9 +23,9 @@ function imports_normalize_amount(string $raw): ?int
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Matches a CSV name against a participant's paypal_name or display_name
|
* Matches a CSV name against a participant's paypal_name or display_name
|
||||||
* (case-insensitive), the same rule the legacy CSV import used.
|
* (case-insensitive).
|
||||||
*
|
*
|
||||||
* @return array{participant_id: int, legacy_mitarbeiter_id: ?int, display_name: string}|null
|
* @return array{participant_id: int, display_name: string}|null
|
||||||
*/
|
*/
|
||||||
function imports_find_participant(PDO $pdo, int $tenantId, string $name): ?array
|
function imports_find_participant(PDO $pdo, int $tenantId, string $name): ?array
|
||||||
{
|
{
|
||||||
@@ -35,7 +35,7 @@ function imports_find_participant(PDO $pdo, int $tenantId, string $name): ?array
|
|||||||
}
|
}
|
||||||
|
|
||||||
$stmt = $pdo->prepare(
|
$stmt = $pdo->prepare(
|
||||||
'SELECT id, legacy_mitarbeiter_id, display_name
|
'SELECT id, display_name
|
||||||
FROM participants
|
FROM participants
|
||||||
WHERE tenant_id = ?
|
WHERE tenant_id = ?
|
||||||
AND (LOWER(paypal_name) = LOWER(?) OR LOWER(display_name) = LOWER(?))
|
AND (LOWER(paypal_name) = LOWER(?) OR LOWER(display_name) = LOWER(?))
|
||||||
@@ -50,7 +50,6 @@ function imports_find_participant(PDO $pdo, int $tenantId, string $name): ?array
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
'participant_id' => (int)$row['id'],
|
'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'],
|
'display_name' => (string)$row['display_name'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-100
@@ -166,10 +166,9 @@ function ledger_fetch_tenant_totals(PDO $pdo, int $tenantId, ?int $year = null):
|
|||||||
* - year: int
|
* - year: int
|
||||||
* - active_only: bool|null
|
* - active_only: bool|null
|
||||||
* - participant_ids: list<int>
|
* - participant_ids: list<int>
|
||||||
* - legacy_mitarbeiter_ids: list<int>
|
|
||||||
* - email_norms: list<string>
|
* - 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}>
|
* @return list<array{participant_id: int, tenant_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
|
function ledger_fetch_participant_summaries(PDO $pdo, int $tenantId, array $options = []): array
|
||||||
{
|
{
|
||||||
@@ -193,17 +192,6 @@ function ledger_fetch_participant_summaries(PDO $pdo, int $tenantId, array $opti
|
|||||||
array_push($params, ...$participantIds);
|
array_push($params, ...$participantIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (array_key_exists('legacy_mitarbeiter_ids', $options)) {
|
|
||||||
$legacyIds = is_array($options['legacy_mitarbeiter_ids'])
|
|
||||||
? ledger_normalize_ids($options['legacy_mitarbeiter_ids'])
|
|
||||||
: [];
|
|
||||||
if ($legacyIds === []) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
$where[] = 'p.legacy_mitarbeiter_id IN (' . implode(',', array_fill(0, count($legacyIds), '?')) . ')';
|
|
||||||
array_push($params, ...$legacyIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (array_key_exists('email_norms', $options)) {
|
if (array_key_exists('email_norms', $options)) {
|
||||||
$emailNorms = is_array($options['email_norms'])
|
$emailNorms = is_array($options['email_norms'])
|
||||||
? ledger_normalize_email_norms($options['email_norms'])
|
? ledger_normalize_email_norms($options['email_norms'])
|
||||||
@@ -230,7 +218,6 @@ function ledger_fetch_participant_summaries(PDO $pdo, int $tenantId, array $opti
|
|||||||
SELECT
|
SELECT
|
||||||
p.id AS participant_id,
|
p.id AS participant_id,
|
||||||
p.tenant_id,
|
p.tenant_id,
|
||||||
p.legacy_mitarbeiter_id,
|
|
||||||
p.display_name,
|
p.display_name,
|
||||||
p.email,
|
p.email,
|
||||||
p.email_norm,
|
p.email_norm,
|
||||||
@@ -249,7 +236,7 @@ function ledger_fetch_participant_summaries(PDO $pdo, int $tenantId, array $opti
|
|||||||
AND le.participant_id = p.id
|
AND le.participant_id = p.id
|
||||||
AND le.voided_at IS NULL
|
AND le.voided_at IS NULL
|
||||||
WHERE " . implode(' AND ', $where) . "
|
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
|
GROUP BY p.id, p.tenant_id, p.display_name, p.email, p.email_norm, p.paypal_name, p.active
|
||||||
ORDER BY p.display_name, p.id";
|
ORDER BY p.display_name, p.id";
|
||||||
|
|
||||||
$stmt = $pdo->prepare($sql);
|
$stmt = $pdo->prepare($sql);
|
||||||
@@ -260,7 +247,6 @@ function ledger_fetch_participant_summaries(PDO $pdo, int $tenantId, array $opti
|
|||||||
$summaries[] = [
|
$summaries[] = [
|
||||||
'participant_id' => (int)$row['participant_id'],
|
'participant_id' => (int)$row['participant_id'],
|
||||||
'tenant_id' => (int)$row['tenant_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'],
|
'display_name' => (string)$row['display_name'],
|
||||||
'email' => $row['email'] !== null ? (string)$row['email'] : null,
|
'email' => $row['email'] !== null ? (string)$row['email'] : null,
|
||||||
'email_norm' => $row['email_norm'] !== null ? (string)$row['email_norm'] : null,
|
'email_norm' => $row['email_norm'] !== null ? (string)$row['email_norm'] : null,
|
||||||
@@ -280,7 +266,7 @@ function ledger_fetch_participant_summaries(PDO $pdo, int $tenantId, array $opti
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @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
|
* @return array{participant_id: int, tenant_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
|
function ledger_fetch_participant_summary(PDO $pdo, int $tenantId, int $participantId, ?int $year = null): ?array
|
||||||
{
|
{
|
||||||
@@ -292,19 +278,6 @@ function ledger_fetch_participant_summary(PDO $pdo, int $tenantId, int $particip
|
|||||||
return $summaries[0] ?? null;
|
return $summaries[0] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @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_by_legacy_id(PDO $pdo, int $tenantId, int $legacyMitarbeiterId, ?int $year = null): ?array
|
|
||||||
{
|
|
||||||
$summaries = ledger_fetch_participant_summaries($pdo, $tenantId, [
|
|
||||||
'legacy_mitarbeiter_ids' => [$legacyMitarbeiterId],
|
|
||||||
'year' => $year ?? ledger_current_year(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return $summaries[0] ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ledger_is_default_tenant(PDO $pdo, int $tenantId): bool
|
function ledger_is_default_tenant(PDO $pdo, int $tenantId): bool
|
||||||
{
|
{
|
||||||
$tenant = ledger_fetch_default_tenant($pdo);
|
$tenant = ledger_fetch_default_tenant($pdo);
|
||||||
@@ -313,7 +286,7 @@ function ledger_is_default_tenant(PDO $pdo, int $tenantId): bool
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return list<array{participant_id: int, display_name: string, email: ?string, email_norm: ?string, paypal_name: ?string, active: bool, legacy_mitarbeiter_id: ?int, user_id: ?int, role: ?string, membership_status: ?string}>
|
* @return list<array{participant_id: int, display_name: string, email: ?string, email_norm: ?string, paypal_name: ?string, active: bool, user_id: ?int, role: ?string, membership_status: ?string}>
|
||||||
*/
|
*/
|
||||||
function ledger_fetch_participants_for_admin(PDO $pdo, int $tenantId): array
|
function ledger_fetch_participants_for_admin(PDO $pdo, int $tenantId): array
|
||||||
{
|
{
|
||||||
@@ -325,7 +298,6 @@ function ledger_fetch_participants_for_admin(PDO $pdo, int $tenantId): array
|
|||||||
p.email_norm,
|
p.email_norm,
|
||||||
p.paypal_name,
|
p.paypal_name,
|
||||||
p.active,
|
p.active,
|
||||||
p.legacy_mitarbeiter_id,
|
|
||||||
p.user_id,
|
p.user_id,
|
||||||
tm.role,
|
tm.role,
|
||||||
tm.status AS membership_status
|
tm.status AS membership_status
|
||||||
@@ -347,7 +319,6 @@ function ledger_fetch_participants_for_admin(PDO $pdo, int $tenantId): array
|
|||||||
'email_norm' => $row['email_norm'] !== null ? (string)$row['email_norm'] : null,
|
'email_norm' => $row['email_norm'] !== null ? (string)$row['email_norm'] : null,
|
||||||
'paypal_name' => $row['paypal_name'] !== null ? (string)$row['paypal_name'] : null,
|
'paypal_name' => $row['paypal_name'] !== null ? (string)$row['paypal_name'] : null,
|
||||||
'active' => (int)$row['active'] === 1,
|
'active' => (int)$row['active'] === 1,
|
||||||
'legacy_mitarbeiter_id' => $row['legacy_mitarbeiter_id'] !== null ? (int)$row['legacy_mitarbeiter_id'] : null,
|
|
||||||
'user_id' => $row['user_id'] !== null ? (int)$row['user_id'] : null,
|
'user_id' => $row['user_id'] !== null ? (int)$row['user_id'] : null,
|
||||||
'role' => $row['role'] !== null ? (string)$row['role'] : null,
|
'role' => $row['role'] !== null ? (string)$row['role'] : null,
|
||||||
'membership_status' => $row['membership_status'] !== null ? (string)$row['membership_status'] : null,
|
'membership_status' => $row['membership_status'] !== null ? (string)$row['membership_status'] : null,
|
||||||
@@ -426,17 +397,12 @@ function ledger_anonymize_participant(PDO $pdo, int $tenantId, int $participantI
|
|||||||
return $stmt->rowCount() > 0;
|
return $stmt->rowCount() > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Marks the ledger entry mirrored from a legacy row as voided instead of
|
|
||||||
* deleting it, so corrections stay auditable. Idempotent: voiding an
|
|
||||||
* already-voided or missing entry is a no-op and returns false.
|
|
||||||
*/
|
|
||||||
/**
|
/**
|
||||||
* Storniert einen Journaleintrag anhand seiner Ledger-ID - immer auf den
|
* Storniert einen Journaleintrag anhand seiner Ledger-ID - immer auf den
|
||||||
* Mandanten eingegrenzt, damit ueber eine untergeschobene ID nicht in fremden
|
* Mandanten eingegrenzt, damit ueber eine untergeschobene ID nicht in fremden
|
||||||
* Mandanten gebucht werden kann. Haengt am Eintrag noch eine Legacy-Zeile,
|
* Mandanten storniert werden kann. Markiert den Eintrag als storniert statt
|
||||||
* wird sie mitgeloescht, damit der Mirror den Eintrag nicht wieder auferstehen
|
* ihn zu loeschen, damit Korrekturen nachvollziehbar bleiben. Idempotent: ein
|
||||||
* laesst.
|
* bereits stornierter oder fehlender Eintrag ergibt false.
|
||||||
*
|
*
|
||||||
* @param string|null $expectedType Absichern, dass z. B. ueber das
|
* @param string|null $expectedType Absichern, dass z. B. ueber das
|
||||||
* Einzahlungs-Formular kein Strich storniert
|
* Einzahlungs-Formular kein Strich storniert
|
||||||
@@ -444,15 +410,8 @@ function ledger_anonymize_participant(PDO $pdo, int $tenantId, int $participantI
|
|||||||
*/
|
*/
|
||||||
function ledger_void_entry(PDO $pdo, int $tenantId, int $entryId, ?string $expectedType = null): bool
|
function ledger_void_entry(PDO $pdo, int $tenantId, int $entryId, ?string $expectedType = null): bool
|
||||||
{
|
{
|
||||||
// Nur bekannte Legacy-Tabellen, damit der Tabellenname nie ungeprueft in
|
|
||||||
// ein DELETE wandert.
|
|
||||||
$legacyKeys = [
|
|
||||||
'kl_Einzahlungen' => 'EinzahlungsID',
|
|
||||||
'kl_Kaffeeverbrauch' => 'VerbrauchID',
|
|
||||||
];
|
|
||||||
|
|
||||||
$stmt = $pdo->prepare(
|
$stmt = $pdo->prepare(
|
||||||
"SELECT id, type, legacy_table, legacy_id
|
"SELECT id, type
|
||||||
FROM ledger_entries
|
FROM ledger_entries
|
||||||
WHERE id = ? AND tenant_id = ? AND voided_at IS NULL
|
WHERE id = ? AND tenant_id = ? AND voided_at IS NULL
|
||||||
LIMIT 1"
|
LIMIT 1"
|
||||||
@@ -467,51 +426,30 @@ function ledger_void_entry(PDO $pdo, int $tenantId, int $entryId, ?string $expec
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
$stmt = $pdo->prepare('UPDATE ledger_entries SET voided_at = NOW() WHERE id = ? AND tenant_id = ? AND voided_at IS NULL');
|
||||||
$pdo->beginTransaction();
|
$stmt->execute([$entryId, $tenantId]);
|
||||||
|
|
||||||
$pdo->prepare('UPDATE ledger_entries SET voided_at = NOW() WHERE id = ? AND tenant_id = ? AND voided_at IS NULL')
|
return $stmt->rowCount() > 0;
|
||||||
->execute([$entryId, $tenantId]);
|
|
||||||
|
|
||||||
$legacyTable = $entry['legacy_table'] !== null ? (string)$entry['legacy_table'] : null;
|
|
||||||
if ($legacyTable !== null && $entry['legacy_id'] !== null && isset($legacyKeys[$legacyTable])) {
|
|
||||||
$pdo->prepare("DELETE FROM {$legacyTable} WHERE {$legacyKeys[$legacyTable]} = ?")
|
|
||||||
->execute([(int)$entry['legacy_id']]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$pdo->commit();
|
|
||||||
} catch (Throwable $e) {
|
|
||||||
if ($pdo->inTransaction()) {
|
|
||||||
$pdo->rollBack();
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Storniert den juengsten, noch nicht stornierten Web-Selbsteintrag eines
|
* Storniert den juengsten, noch nicht stornierten Web-Selbsteintrag eines
|
||||||
* Teilnehmers (Quelle 'self_entry' bei SaaS-nativen Mandanten bzw. 'legacy_web'
|
* Teilnehmers (Quelle 'self_entry'). Bewusst nur eigene Web-Eintraege - vom
|
||||||
* beim migrierten Default-Mandanten). Bewusst nur eigene Web-Eintraege - vom
|
* Kassenwart per Sammelerfassung gesetzte Striche ('manual_bulk') bleiben
|
||||||
* Kassenwart per Sammelerfassung gesetzte Striche ('manual_bulk'/'legacy_manual')
|
* unangetastet.
|
||||||
* bleiben unangetastet. Beim Default-Mandanten wird zusaetzlich die
|
|
||||||
* kl_Kaffeeverbrauch-Zeile geloescht, damit sie nicht erneut gespiegelt wird
|
|
||||||
* und auf den Legacy-Sammelseiten verschwindet.
|
|
||||||
*
|
*
|
||||||
* @return array{ok: bool, marks?: int, error?: string}
|
* @return array{ok: bool, marks?: int, error?: string}
|
||||||
*/
|
*/
|
||||||
function ledger_void_own_self_entry(PDO $pdo, int $tenantId, int $participantId): array
|
function ledger_void_own_self_entry(PDO $pdo, int $tenantId, int $participantId): array
|
||||||
{
|
{
|
||||||
$stmt = $pdo->prepare(
|
$stmt = $pdo->prepare(
|
||||||
"SELECT id, marks_count, legacy_table, legacy_id
|
"SELECT id, marks_count
|
||||||
FROM ledger_entries
|
FROM ledger_entries
|
||||||
WHERE tenant_id = ?
|
WHERE tenant_id = ?
|
||||||
AND participant_id = ?
|
AND participant_id = ?
|
||||||
AND type = 'consumption'
|
AND type = 'consumption'
|
||||||
AND voided_at IS NULL
|
AND voided_at IS NULL
|
||||||
AND source IN ('self_entry', 'legacy_web')
|
AND source = 'self_entry'
|
||||||
ORDER BY booked_at DESC, id DESC
|
ORDER BY booked_at DESC, id DESC
|
||||||
LIMIT 1"
|
LIMIT 1"
|
||||||
);
|
);
|
||||||
@@ -525,11 +463,6 @@ function ledger_void_own_self_entry(PDO $pdo, int $tenantId, int $participantId)
|
|||||||
try {
|
try {
|
||||||
$pdo->beginTransaction();
|
$pdo->beginTransaction();
|
||||||
|
|
||||||
if ($entry['legacy_table'] === 'kl_Kaffeeverbrauch' && $entry['legacy_id'] !== null) {
|
|
||||||
$pdo->prepare('DELETE FROM kl_Kaffeeverbrauch WHERE VerbrauchID = ?')
|
|
||||||
->execute([(int)$entry['legacy_id']]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$pdo->prepare("UPDATE ledger_entries SET voided_at = NOW() WHERE id = ? AND voided_at IS NULL")
|
$pdo->prepare("UPDATE ledger_entries SET voided_at = NOW() WHERE id = ? AND voided_at IS NULL")
|
||||||
->execute([(int)$entry['id']]);
|
->execute([(int)$entry['id']]);
|
||||||
|
|
||||||
@@ -546,9 +479,7 @@ function ledger_void_own_self_entry(PDO $pdo, int $tenantId, int $participantId)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Records a consumption entry straight into the ledger, without any legacy
|
* Records a consumption entry straight into the ledger.
|
||||||
* kl_Kaffeeverbrauch row. Used for tenants that have no legacy shadow table
|
|
||||||
* (every tenant except the migrated default tenant).
|
|
||||||
*/
|
*/
|
||||||
function ledger_record_consumption(PDO $pdo, int $tenantId, int $participantId, int $marks, int $unitPriceCents, string $source, ?int $createdByUserId = null): int
|
function ledger_record_consumption(PDO $pdo, int $tenantId, int $participantId, int $marks, int $unitPriceCents, string $source, ?int $createdByUserId = null): int
|
||||||
{
|
{
|
||||||
@@ -568,10 +499,6 @@ function ledger_record_consumption(PDO $pdo, int $tenantId, int $participantId,
|
|||||||
return (int)$pdo->lastInsertId();
|
return (int)$pdo->lastInsertId();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Records a payment entry straight into the ledger, without any legacy
|
|
||||||
* kl_Einzahlungen row. Used for tenants that have no legacy shadow table.
|
|
||||||
*/
|
|
||||||
/**
|
/**
|
||||||
* Bucht eine Einzahlung. $amountCents darf negativ sein (Abzug, Auszahlung,
|
* Bucht eine Einzahlung. $amountCents darf negativ sein (Abzug, Auszahlung,
|
||||||
* Korrektur) - fuer Tippfehler ist aber ledger_void_entry der richtige Weg,
|
* Korrektur) - fuer Tippfehler ist aber ledger_void_entry der richtige Weg,
|
||||||
@@ -597,12 +524,11 @@ function ledger_record_payment(PDO $pdo, int $tenantId, int $participantId, int
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ledger-native equivalent of the legacy "100-Tage-Liste" front/back split:
|
* "100-Tage-Liste" Vorder-/Rueckseiten-Aufteilung: aktive Teilnehmer, deren
|
||||||
* active participants whose summed marks within the trailing window are at
|
* summierte Striche im nachlaufenden Fenster die Zehner-Schwelle erreichen
|
||||||
* or above the ten-mark threshold ($atLeastTen), or below it. Used for
|
* ($atLeastTen) oder darunter liegen.
|
||||||
* tenants without a legacy kl_Kaffeeverbrauch history to filter against.
|
|
||||||
*
|
*
|
||||||
* @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}>
|
* @return list<array{participant_id: int, tenant_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_participants_by_window_marks(PDO $pdo, int $tenantId, int $windowDays, bool $atLeastTen): array
|
function ledger_fetch_participants_by_window_marks(PDO $pdo, int $tenantId, int $windowDays, bool $atLeastTen): array
|
||||||
{
|
{
|
||||||
@@ -637,7 +563,7 @@ function ledger_fetch_participants_by_window_marks(PDO $pdo, int $tenantId, int
|
|||||||
* - include_voided: bool
|
* - include_voided: bool
|
||||||
* - limit: int
|
* - 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}>
|
* @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, voided_at: ?string}>
|
||||||
*/
|
*/
|
||||||
/**
|
/**
|
||||||
* Verbrauch eines Teilnehmers pro Monat eines Jahres (nur nicht stornierte
|
* Verbrauch eines Teilnehmers pro Monat eines Jahres (nur nicht stornierte
|
||||||
@@ -707,8 +633,6 @@ function ledger_fetch_recent_entries(PDO $pdo, int $tenantId, array $options = [
|
|||||||
le.booked_at,
|
le.booked_at,
|
||||||
le.source,
|
le.source,
|
||||||
le.note,
|
le.note,
|
||||||
le.legacy_table,
|
|
||||||
le.legacy_id,
|
|
||||||
le.voided_at
|
le.voided_at
|
||||||
FROM ledger_entries le
|
FROM ledger_entries le
|
||||||
JOIN participants p
|
JOIN participants p
|
||||||
@@ -735,8 +659,6 @@ function ledger_fetch_recent_entries(PDO $pdo, int $tenantId, array $options = [
|
|||||||
'booked_at' => (string)$row['booked_at'],
|
'booked_at' => (string)$row['booked_at'],
|
||||||
'source' => (string)$row['source'],
|
'source' => (string)$row['source'],
|
||||||
'note' => $row['note'] !== null ? (string)$row['note'] : null,
|
'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,
|
'voided_at' => $row['voided_at'] !== null ? (string)$row['voided_at'] : null,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ function paypal_booking_note(array $payment): string
|
|||||||
* Zahlernamen (paypal_name/display_name), sonst ueber die Mitteilung (Mitglied
|
* Zahlernamen (paypal_name/display_name), sonst ueber die Mitteilung (Mitglied
|
||||||
* schreibt dort teils seinen Namen). Liefert null, wenn nicht eindeutig.
|
* schreibt dort teils seinen Namen). Liefert null, wenn nicht eindeutig.
|
||||||
*
|
*
|
||||||
* @return array{participant_id:int, legacy_mitarbeiter_id:?int, display_name:string}|null
|
* @return array{participant_id:int, display_name:string}|null
|
||||||
*/
|
*/
|
||||||
function paypal_match_participant(PDO $pdo, int $tenantId, string $payerName, ?string $note): ?array
|
function paypal_match_participant(PDO $pdo, int $tenantId, string $payerName, ?string $note): ?array
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
-- Legacy-Abbau Schritt 4: Altwelt vollstaendig entfernen.
|
||||||
|
--
|
||||||
|
-- Voraussetzung: kein Laufzeit-Code liest oder schreibt mehr die kl_*-Tabellen
|
||||||
|
-- oder die legacy_*-Spalten (Schritte 1-3). Die Migration ist forward-only.
|
||||||
|
|
||||||
|
-- participants: Verknuepfung zur alten Mitarbeitertabelle entfernen.
|
||||||
|
ALTER TABLE participants
|
||||||
|
DROP INDEX uq_participants_tenant_legacy,
|
||||||
|
DROP COLUMN legacy_mitarbeiter_id;
|
||||||
|
|
||||||
|
-- ledger_entries: Herkunftszeiger auf die Legacy-Buchungstabellen entfernen.
|
||||||
|
ALTER TABLE ledger_entries
|
||||||
|
DROP INDEX uq_ledger_entries_tenant_legacy,
|
||||||
|
DROP COLUMN legacy_table,
|
||||||
|
DROP COLUMN legacy_id;
|
||||||
|
|
||||||
|
-- Die alten Windows/IIS-Tabellen selbst.
|
||||||
|
DROP TABLE IF EXISTS kl_Einzahlungen;
|
||||||
|
DROP TABLE IF EXISTS kl_Kaffeeverbrauch;
|
||||||
|
DROP TABLE IF EXISTS kl_hinweise;
|
||||||
|
DROP TABLE IF EXISTS kl_config;
|
||||||
|
DROP TABLE IF EXISTS kl_Mitarbeiter;
|
||||||
@@ -59,7 +59,6 @@ include 'nav.php';
|
|||||||
Read-only Vorschau aus <code>ledger_entries</code> für
|
Read-only Vorschau aus <code>ledger_entries</code> für
|
||||||
<?php echo saas_html($tenantName); ?>
|
<?php echo saas_html($tenantName); ?>
|
||||||
(<code><?php echo saas_html($tenantSlug); ?></code>).
|
(<code><?php echo saas_html($tenantSlug); ?></code>).
|
||||||
Die bestehenden App-Seiten schreiben weiterhin in die Legacy-Tabellen.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h3>Tenant-Summen</h3>
|
<h3>Tenant-Summen</h3>
|
||||||
@@ -117,7 +116,6 @@ include 'nav.php';
|
|||||||
<th>Betrag</th>
|
<th>Betrag</th>
|
||||||
<th>Striche</th>
|
<th>Striche</th>
|
||||||
<th>Quelle</th>
|
<th>Quelle</th>
|
||||||
<th>Legacy</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
<?php foreach ($recentEntries as $entry): ?>
|
<?php foreach ($recentEntries as $entry): ?>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -127,14 +125,6 @@ include 'nav.php';
|
|||||||
<td><?php echo ledger_preview_money($entry['amount_cents']); ?></td>
|
<td><?php echo ledger_preview_money($entry['amount_cents']); ?></td>
|
||||||
<td><?php echo $entry['marks_count'] !== null ? number_format($entry['marks_count'], 0, ',', '.') : ''; ?></td>
|
<td><?php echo $entry['marks_count'] !== null ? number_format($entry['marks_count'], 0, ',', '.') : ''; ?></td>
|
||||||
<td><?php echo saas_html($entry['source']); ?></td>
|
<td><?php echo saas_html($entry['source']); ?></td>
|
||||||
<td>
|
|
||||||
<?php
|
|
||||||
echo saas_html((string)($entry['legacy_table'] ?? ''));
|
|
||||||
if ($entry['legacy_id'] !== null) {
|
|
||||||
echo ' #' . (int)$entry['legacy_id'];
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -1,236 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
require __DIR__ . '/dev-db.php';
|
|
||||||
|
|
||||||
function m3_env(string $name, string $default): string
|
|
||||||
{
|
|
||||||
$value = getenv($name);
|
|
||||||
if ($value === false || trim($value) === '') {
|
|
||||||
return $default;
|
|
||||||
}
|
|
||||||
|
|
||||||
return trim($value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function m3_email_norm(?string $email): ?string
|
|
||||||
{
|
|
||||||
$normalized = strtolower(trim((string)$email));
|
|
||||||
|
|
||||||
return $normalized !== '' ? $normalized : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function m3_decimal_to_cents(mixed $value): int
|
|
||||||
{
|
|
||||||
$normalized = str_replace(',', '.', trim((string)$value));
|
|
||||||
if ($normalized === '') {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
$negative = str_starts_with($normalized, '-');
|
|
||||||
if ($negative) {
|
|
||||||
$normalized = substr($normalized, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
[$euros, $cents] = array_pad(explode('.', $normalized, 2), 2, '0');
|
|
||||||
$euros = preg_replace('/[^0-9]/', '', $euros) ?? '0';
|
|
||||||
$cents = preg_replace('/[^0-9]/', '', $cents) ?? '0';
|
|
||||||
$cents = str_pad(substr($cents, 0, 2), 2, '0');
|
|
||||||
|
|
||||||
$result = ((int)$euros * 100) + (int)$cents;
|
|
||||||
|
|
||||||
return $negative ? -$result : $result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function m3_fetch_tenant_id(PDO $pdo, string $slug): int
|
|
||||||
{
|
|
||||||
$stmt = $pdo->prepare('SELECT id FROM tenants WHERE slug = ?');
|
|
||||||
$stmt->execute([$slug]);
|
|
||||||
$tenantId = $stmt->fetchColumn();
|
|
||||||
|
|
||||||
if ($tenantId === false) {
|
|
||||||
throw new RuntimeException("Tenant '{$slug}' was not created.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return (int)$tenantId;
|
|
||||||
}
|
|
||||||
|
|
||||||
function m3_upsert_user(PDO $pdo, string $email, string $emailNorm, string $displayName): int
|
|
||||||
{
|
|
||||||
$stmt = $pdo->prepare(
|
|
||||||
'INSERT INTO users (email, email_norm, display_name, status)
|
|
||||||
VALUES (?, ?, ?, ?)
|
|
||||||
ON DUPLICATE KEY UPDATE
|
|
||||||
email = VALUES(email),
|
|
||||||
display_name = VALUES(display_name),
|
|
||||||
status = VALUES(status)'
|
|
||||||
);
|
|
||||||
$stmt->execute([$email, $emailNorm, $displayName, 'active']);
|
|
||||||
|
|
||||||
$stmt = $pdo->prepare('SELECT id FROM users WHERE email_norm = ?');
|
|
||||||
$stmt->execute([$emailNorm]);
|
|
||||||
$userId = $stmt->fetchColumn();
|
|
||||||
|
|
||||||
if ($userId === false) {
|
|
||||||
throw new RuntimeException("User '{$emailNorm}' was not created.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return (int)$userId;
|
|
||||||
}
|
|
||||||
|
|
||||||
$pdo = dev_pdo();
|
|
||||||
$applied = dev_apply_migrations($pdo);
|
|
||||||
|
|
||||||
$tenantSlug = m3_env('M3_DEFAULT_TENANT_SLUG', 'default');
|
|
||||||
$tenantName = m3_env('M3_DEFAULT_TENANT_NAME', 'Kaffeeliste Bestand');
|
|
||||||
$ownerEmailNorm = m3_email_norm(getenv('M3_DEFAULT_TENANT_OWNER_EMAIL') ?: null);
|
|
||||||
|
|
||||||
$stats = [
|
|
||||||
'applied_migrations' => count($applied),
|
|
||||||
'participants' => 0,
|
|
||||||
'stale_participants_removed' => 0,
|
|
||||||
'users' => 0,
|
|
||||||
'memberships' => 0,
|
|
||||||
];
|
|
||||||
|
|
||||||
$pdo->beginTransaction();
|
|
||||||
|
|
||||||
try {
|
|
||||||
$stmt = $pdo->prepare(
|
|
||||||
'INSERT INTO tenants (slug, name, status, timezone, locale, currency_code)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?)
|
|
||||||
ON DUPLICATE KEY UPDATE
|
|
||||||
name = VALUES(name),
|
|
||||||
status = VALUES(status),
|
|
||||||
timezone = VALUES(timezone),
|
|
||||||
locale = VALUES(locale),
|
|
||||||
currency_code = VALUES(currency_code)'
|
|
||||||
);
|
|
||||||
$stmt->execute([$tenantSlug, $tenantName, 'active', 'Europe/Berlin', 'de-DE', 'EUR']);
|
|
||||||
$tenantId = m3_fetch_tenant_id($pdo, $tenantSlug);
|
|
||||||
|
|
||||||
$config = $pdo->query(
|
|
||||||
'SELECT KostenproStrich, paypaluse, paypallink, strichperweb
|
|
||||||
FROM kl_config
|
|
||||||
ORDER BY id
|
|
||||||
LIMIT 1'
|
|
||||||
)->fetch() ?: [
|
|
||||||
'KostenproStrich' => '0.20',
|
|
||||||
'paypaluse' => 0,
|
|
||||||
'paypallink' => '',
|
|
||||||
'strichperweb' => 1,
|
|
||||||
];
|
|
||||||
|
|
||||||
$stmt = $pdo->prepare(
|
|
||||||
'INSERT INTO tenant_settings
|
|
||||||
(tenant_id, mark_price_cents, self_entry_enabled, paypal_enabled, paypal_url_template)
|
|
||||||
VALUES (?, ?, ?, ?, ?)
|
|
||||||
ON DUPLICATE KEY UPDATE
|
|
||||||
mark_price_cents = VALUES(mark_price_cents),
|
|
||||||
self_entry_enabled = VALUES(self_entry_enabled),
|
|
||||||
paypal_enabled = VALUES(paypal_enabled),
|
|
||||||
paypal_url_template = VALUES(paypal_url_template)'
|
|
||||||
);
|
|
||||||
$stmt->execute([
|
|
||||||
$tenantId,
|
|
||||||
m3_decimal_to_cents($config['KostenproStrich']),
|
|
||||||
(int)$config['strichperweb'] === 1 ? 1 : 0,
|
|
||||||
(int)$config['paypaluse'] === 1 ? 1 : 0,
|
|
||||||
(string)$config['paypallink'],
|
|
||||||
]);
|
|
||||||
|
|
||||||
$members = $pdo->query(
|
|
||||||
'SELECT MitarbeiterID, Name, Email, paypalname, aktiv, admin
|
|
||||||
FROM kl_Mitarbeiter
|
|
||||||
ORDER BY MitarbeiterID'
|
|
||||||
)->fetchAll();
|
|
||||||
|
|
||||||
$upsertParticipant = $pdo->prepare(
|
|
||||||
'INSERT INTO participants
|
|
||||||
(tenant_id, display_name, email, email_norm, paypal_name, active, legacy_mitarbeiter_id)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
||||||
ON DUPLICATE KEY UPDATE
|
|
||||||
display_name = VALUES(display_name),
|
|
||||||
email = VALUES(email),
|
|
||||||
email_norm = VALUES(email_norm),
|
|
||||||
paypal_name = VALUES(paypal_name),
|
|
||||||
active = VALUES(active),
|
|
||||||
legacy_mitarbeiter_id = VALUES(legacy_mitarbeiter_id)'
|
|
||||||
);
|
|
||||||
$upsertMembership = $pdo->prepare(
|
|
||||||
'INSERT INTO tenant_memberships (tenant_id, user_id, role, status, joined_at)
|
|
||||||
VALUES (?, ?, ?, ?, NOW())
|
|
||||||
ON DUPLICATE KEY UPDATE
|
|
||||||
role = VALUES(role),
|
|
||||||
status = VALUES(status),
|
|
||||||
joined_at = COALESCE(tenant_memberships.joined_at, VALUES(joined_at))'
|
|
||||||
);
|
|
||||||
$linkParticipantUser = $pdo->prepare(
|
|
||||||
'UPDATE participants
|
|
||||||
SET user_id = ?
|
|
||||||
WHERE tenant_id = ?
|
|
||||||
AND legacy_mitarbeiter_id = ?'
|
|
||||||
);
|
|
||||||
|
|
||||||
foreach ($members as $member) {
|
|
||||||
$email = trim((string)$member['Email']);
|
|
||||||
$emailNorm = m3_email_norm($email);
|
|
||||||
$displayName = trim((string)$member['Name']);
|
|
||||||
if ($displayName === '') {
|
|
||||||
$displayName = $emailNorm ?? ('Mitarbeiter ' . (int)$member['MitarbeiterID']);
|
|
||||||
}
|
|
||||||
|
|
||||||
$upsertParticipant->execute([
|
|
||||||
$tenantId,
|
|
||||||
$displayName,
|
|
||||||
$email !== '' ? $email : null,
|
|
||||||
$emailNorm,
|
|
||||||
$member['paypalname'] !== null && trim((string)$member['paypalname']) !== ''
|
|
||||||
? trim((string)$member['paypalname'])
|
|
||||||
: null,
|
|
||||||
(int)$member['aktiv'] === 1 ? 1 : 0,
|
|
||||||
(int)$member['MitarbeiterID'],
|
|
||||||
]);
|
|
||||||
$stats['participants']++;
|
|
||||||
|
|
||||||
$isAdmin = (int)$member['admin'] === 1 && (int)$member['aktiv'] === 1;
|
|
||||||
$isOwner = $ownerEmailNorm !== null && $emailNorm === $ownerEmailNorm && (int)$member['aktiv'] === 1;
|
|
||||||
if ($emailNorm === null || (!$isAdmin && !$isOwner)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$userId = m3_upsert_user($pdo, $email, $emailNorm, $displayName);
|
|
||||||
$role = $isOwner ? 'owner' : 'admin';
|
|
||||||
$upsertMembership->execute([$tenantId, $userId, $role, 'active']);
|
|
||||||
$linkParticipantUser->execute([$userId, $tenantId, (int)$member['MitarbeiterID']]);
|
|
||||||
$stats['users']++;
|
|
||||||
$stats['memberships']++;
|
|
||||||
}
|
|
||||||
|
|
||||||
$removeStaleParticipants = $pdo->prepare(
|
|
||||||
'DELETE p
|
|
||||||
FROM participants p
|
|
||||||
LEFT JOIN kl_Mitarbeiter m
|
|
||||||
ON m.MitarbeiterID = p.legacy_mitarbeiter_id
|
|
||||||
WHERE p.tenant_id = ?
|
|
||||||
AND p.legacy_mitarbeiter_id IS NOT NULL
|
|
||||||
AND m.MitarbeiterID IS NULL'
|
|
||||||
);
|
|
||||||
$removeStaleParticipants->execute([$tenantId]);
|
|
||||||
$stats['stale_participants_removed'] = $removeStaleParticipants->rowCount();
|
|
||||||
|
|
||||||
$pdo->commit();
|
|
||||||
} catch (Throwable $e) {
|
|
||||||
$pdo->rollBack();
|
|
||||||
fwrite(STDERR, "M3 default tenant backfill failed: {$e->getMessage()}\n");
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "M3 default tenant backfill complete.\n";
|
|
||||||
echo "Tenant: {$tenantSlug}\n";
|
|
||||||
echo "Applied migrations: {$stats['applied_migrations']}\n";
|
|
||||||
echo "Participants mirrored: {$stats['participants']}\n";
|
|
||||||
echo "Stale participants removed: {$stats['stale_participants_removed']}\n";
|
|
||||||
echo "Users upserted: {$stats['users']}\n";
|
|
||||||
echo "Memberships upserted: {$stats['memberships']}\n";
|
|
||||||
@@ -1,212 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
require __DIR__ . '/dev-db.php';
|
|
||||||
|
|
||||||
function m4_env(string $name, string $default): string
|
|
||||||
{
|
|
||||||
$value = getenv($name);
|
|
||||||
if ($value === false || trim($value) === '') {
|
|
||||||
return $default;
|
|
||||||
}
|
|
||||||
|
|
||||||
return trim($value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function m4_fetch_tenant_id(PDO $pdo, string $slug): int
|
|
||||||
{
|
|
||||||
$stmt = $pdo->prepare('SELECT id FROM tenants WHERE slug = ?');
|
|
||||||
$stmt->execute([$slug]);
|
|
||||||
$tenantId = $stmt->fetchColumn();
|
|
||||||
|
|
||||||
if ($tenantId === false) {
|
|
||||||
throw new RuntimeException("Tenant '{$slug}' was not found. Run scripts/backfill-default-tenant.php first.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return (int)$tenantId;
|
|
||||||
}
|
|
||||||
|
|
||||||
function m4_count_missing_payment_participants(PDO $pdo, int $tenantId): int
|
|
||||||
{
|
|
||||||
$stmt = $pdo->prepare(
|
|
||||||
'SELECT COUNT(*)
|
|
||||||
FROM kl_Einzahlungen e
|
|
||||||
LEFT JOIN participants p
|
|
||||||
ON p.tenant_id = ?
|
|
||||||
AND p.legacy_mitarbeiter_id = e.MitarbeiterID
|
|
||||||
WHERE p.id IS NULL'
|
|
||||||
);
|
|
||||||
$stmt->execute([$tenantId]);
|
|
||||||
|
|
||||||
return (int)$stmt->fetchColumn();
|
|
||||||
}
|
|
||||||
|
|
||||||
function m4_count_missing_consumption_participants(PDO $pdo, int $tenantId): int
|
|
||||||
{
|
|
||||||
$stmt = $pdo->prepare(
|
|
||||||
'SELECT COUNT(*)
|
|
||||||
FROM kl_Kaffeeverbrauch v
|
|
||||||
LEFT JOIN participants p
|
|
||||||
ON p.tenant_id = ?
|
|
||||||
AND p.legacy_mitarbeiter_id = v.MitarbeiterID
|
|
||||||
WHERE p.id IS NULL'
|
|
||||||
);
|
|
||||||
$stmt->execute([$tenantId]);
|
|
||||||
|
|
||||||
return (int)$stmt->fetchColumn();
|
|
||||||
}
|
|
||||||
|
|
||||||
$pdo = dev_pdo();
|
|
||||||
$applied = dev_apply_migrations($pdo);
|
|
||||||
$tenantSlug = m4_env('M4_DEFAULT_TENANT_SLUG', m4_env('M3_DEFAULT_TENANT_SLUG', 'default'));
|
|
||||||
|
|
||||||
$stats = [
|
|
||||||
'applied_migrations' => count($applied),
|
|
||||||
'stale_payments_removed' => 0,
|
|
||||||
'stale_consumptions_removed' => 0,
|
|
||||||
'payments_mirrored' => 0,
|
|
||||||
'consumptions_mirrored' => 0,
|
|
||||||
];
|
|
||||||
|
|
||||||
try {
|
|
||||||
$tenantId = m4_fetch_tenant_id($pdo, $tenantSlug);
|
|
||||||
$missingPayments = m4_count_missing_payment_participants($pdo, $tenantId);
|
|
||||||
$missingConsumptions = m4_count_missing_consumption_participants($pdo, $tenantId);
|
|
||||||
if ($missingPayments > 0 || $missingConsumptions > 0) {
|
|
||||||
throw new RuntimeException(
|
|
||||||
"Missing participant mappings for {$missingPayments} payment(s) and {$missingConsumptions} consumption(s)."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
$pdo->beginTransaction();
|
|
||||||
|
|
||||||
$removeStalePayments = $pdo->prepare(
|
|
||||||
"DELETE le
|
|
||||||
FROM ledger_entries le
|
|
||||||
LEFT JOIN kl_Einzahlungen e
|
|
||||||
ON e.EinzahlungsID = le.legacy_id
|
|
||||||
WHERE le.tenant_id = ?
|
|
||||||
AND le.legacy_table = 'kl_Einzahlungen'
|
|
||||||
AND e.EinzahlungsID IS NULL"
|
|
||||||
);
|
|
||||||
$removeStalePayments->execute([$tenantId]);
|
|
||||||
$stats['stale_payments_removed'] = $removeStalePayments->rowCount();
|
|
||||||
|
|
||||||
$removeStaleConsumptions = $pdo->prepare(
|
|
||||||
"DELETE le
|
|
||||||
FROM ledger_entries le
|
|
||||||
LEFT JOIN kl_Kaffeeverbrauch v
|
|
||||||
ON v.VerbrauchID = le.legacy_id
|
|
||||||
WHERE le.tenant_id = ?
|
|
||||||
AND le.legacy_table = 'kl_Kaffeeverbrauch'
|
|
||||||
AND v.VerbrauchID IS NULL"
|
|
||||||
);
|
|
||||||
$removeStaleConsumptions->execute([$tenantId]);
|
|
||||||
$stats['stale_consumptions_removed'] = $removeStaleConsumptions->rowCount();
|
|
||||||
|
|
||||||
$insertPayments = $pdo->prepare(
|
|
||||||
"INSERT INTO ledger_entries
|
|
||||||
(tenant_id, participant_id, type, amount_cents, marks_count,
|
|
||||||
unit_price_cents, booked_at, source, note, legacy_table, legacy_id)
|
|
||||||
SELECT
|
|
||||||
p.tenant_id,
|
|
||||||
p.id,
|
|
||||||
'payment',
|
|
||||||
CAST(ROUND(e.Betrag * 100) AS SIGNED),
|
|
||||||
NULL,
|
|
||||||
NULL,
|
|
||||||
e.Datum,
|
|
||||||
'legacy_payment',
|
|
||||||
NULL,
|
|
||||||
'kl_Einzahlungen',
|
|
||||||
e.EinzahlungsID
|
|
||||||
FROM kl_Einzahlungen e
|
|
||||||
JOIN participants p
|
|
||||||
ON p.tenant_id = ?
|
|
||||||
AND p.legacy_mitarbeiter_id = e.MitarbeiterID
|
|
||||||
ON DUPLICATE KEY UPDATE
|
|
||||||
participant_id = VALUES(participant_id),
|
|
||||||
type = VALUES(type),
|
|
||||||
amount_cents = VALUES(amount_cents),
|
|
||||||
marks_count = VALUES(marks_count),
|
|
||||||
unit_price_cents = VALUES(unit_price_cents),
|
|
||||||
booked_at = VALUES(booked_at),
|
|
||||||
source = VALUES(source),
|
|
||||||
note = VALUES(note)"
|
|
||||||
);
|
|
||||||
$insertPayments->execute([$tenantId]);
|
|
||||||
|
|
||||||
$insertConsumptions = $pdo->prepare(
|
|
||||||
"INSERT INTO ledger_entries
|
|
||||||
(tenant_id, participant_id, type, amount_cents, marks_count,
|
|
||||||
unit_price_cents, booked_at, source, note, legacy_table, legacy_id)
|
|
||||||
SELECT
|
|
||||||
p.tenant_id,
|
|
||||||
p.id,
|
|
||||||
'consumption',
|
|
||||||
-CAST(ROUND(v.Kosten * 100) AS SIGNED),
|
|
||||||
v.AnzahlStriche,
|
|
||||||
CAST(ROUND(v.KostenproStrich * 100) AS SIGNED),
|
|
||||||
v.Datum,
|
|
||||||
CASE
|
|
||||||
WHEN v.Eintragsart = 2 THEN 'legacy_web'
|
|
||||||
ELSE 'legacy_manual'
|
|
||||||
END,
|
|
||||||
CASE
|
|
||||||
WHEN v.Eintragsart IS NULL THEN NULL
|
|
||||||
ELSE CONCAT('Legacy Eintragsart: ', v.Eintragsart)
|
|
||||||
END,
|
|
||||||
'kl_Kaffeeverbrauch',
|
|
||||||
v.VerbrauchID
|
|
||||||
FROM kl_Kaffeeverbrauch v
|
|
||||||
JOIN participants p
|
|
||||||
ON p.tenant_id = ?
|
|
||||||
AND p.legacy_mitarbeiter_id = v.MitarbeiterID
|
|
||||||
ON DUPLICATE KEY UPDATE
|
|
||||||
participant_id = VALUES(participant_id),
|
|
||||||
type = VALUES(type),
|
|
||||||
amount_cents = VALUES(amount_cents),
|
|
||||||
marks_count = VALUES(marks_count),
|
|
||||||
unit_price_cents = VALUES(unit_price_cents),
|
|
||||||
booked_at = VALUES(booked_at),
|
|
||||||
source = VALUES(source),
|
|
||||||
note = VALUES(note)"
|
|
||||||
);
|
|
||||||
$insertConsumptions->execute([$tenantId]);
|
|
||||||
|
|
||||||
$stmt = $pdo->prepare(
|
|
||||||
"SELECT COUNT(*)
|
|
||||||
FROM ledger_entries
|
|
||||||
WHERE tenant_id = ?
|
|
||||||
AND legacy_table = 'kl_Einzahlungen'"
|
|
||||||
);
|
|
||||||
$stmt->execute([$tenantId]);
|
|
||||||
$stats['payments_mirrored'] = (int)$stmt->fetchColumn();
|
|
||||||
|
|
||||||
$stmt = $pdo->prepare(
|
|
||||||
"SELECT COUNT(*)
|
|
||||||
FROM ledger_entries
|
|
||||||
WHERE tenant_id = ?
|
|
||||||
AND legacy_table = 'kl_Kaffeeverbrauch'"
|
|
||||||
);
|
|
||||||
$stmt->execute([$tenantId]);
|
|
||||||
$stats['consumptions_mirrored'] = (int)$stmt->fetchColumn();
|
|
||||||
|
|
||||||
$pdo->commit();
|
|
||||||
} catch (Throwable $e) {
|
|
||||||
if ($pdo->inTransaction()) {
|
|
||||||
$pdo->rollBack();
|
|
||||||
}
|
|
||||||
|
|
||||||
fwrite(STDERR, "M4 ledger backfill failed: {$e->getMessage()}\n");
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "M4 ledger backfill complete.\n";
|
|
||||||
echo "Tenant: {$tenantSlug}\n";
|
|
||||||
echo "Applied migrations: {$stats['applied_migrations']}\n";
|
|
||||||
echo "Stale payments removed: {$stats['stale_payments_removed']}\n";
|
|
||||||
echo "Stale consumptions removed: {$stats['stale_consumptions_removed']}\n";
|
|
||||||
echo "Payments mirrored: {$stats['payments_mirrored']}\n";
|
|
||||||
echo "Consumptions mirrored: {$stats['consumptions_mirrored']}\n";
|
|
||||||
@@ -1,191 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
require __DIR__ . '/dev-db.php';
|
|
||||||
require __DIR__ . '/golden-master-data.php';
|
|
||||||
|
|
||||||
$pdo = dev_pdo();
|
|
||||||
$expected = gm_expected_summaries();
|
|
||||||
$members = gm_members();
|
|
||||||
$emails = array_keys($expected);
|
|
||||||
$failures = [];
|
|
||||||
$passes = 0;
|
|
||||||
|
|
||||||
function gm_assert_equal(string $label, mixed $expected, mixed $actual, array &$failures, int &$passes): void
|
|
||||||
{
|
|
||||||
if ($expected === $actual) {
|
|
||||||
$passes++;
|
|
||||||
echo "PASS {$label}\n";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$failures[] = "{$label}: expected " . var_export($expected, true) . ', got ' . var_export($actual, true);
|
|
||||||
echo "FAIL {$label}\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
function gm_fetch_member_summaries(PDO $pdo, array $emails): array
|
|
||||||
{
|
|
||||||
$placeholders = implode(',', array_fill(0, count($emails), '?'));
|
|
||||||
$year = gm_current_year();
|
|
||||||
$sql = "
|
|
||||||
SELECT
|
|
||||||
m.Email AS email,
|
|
||||||
COALESCE(p.total_payments, 0) AS total_payments,
|
|
||||||
COALESCE(c.total_costs, 0) AS total_costs,
|
|
||||||
COALESCE(c.total_marks, 0) AS total_marks,
|
|
||||||
COALESCE(p.total_payments, 0) - COALESCE(c.total_costs, 0) AS balance,
|
|
||||||
COALESCE(yp.year_payments, 0) AS year_payments,
|
|
||||||
COALESCE(yc.year_costs, 0) AS year_costs,
|
|
||||||
COALESCE(yc.year_marks, 0) AS year_marks
|
|
||||||
FROM kl_Mitarbeiter m
|
|
||||||
LEFT JOIN (
|
|
||||||
SELECT MitarbeiterID, SUM(Betrag) AS total_payments
|
|
||||||
FROM kl_Einzahlungen
|
|
||||||
GROUP BY MitarbeiterID
|
|
||||||
) p ON p.MitarbeiterID = m.MitarbeiterID
|
|
||||||
LEFT JOIN (
|
|
||||||
SELECT MitarbeiterID, SUM(Kosten) AS total_costs, SUM(AnzahlStriche) AS total_marks
|
|
||||||
FROM kl_Kaffeeverbrauch
|
|
||||||
GROUP BY MitarbeiterID
|
|
||||||
) c ON c.MitarbeiterID = m.MitarbeiterID
|
|
||||||
LEFT JOIN (
|
|
||||||
SELECT MitarbeiterID, SUM(Betrag) AS year_payments
|
|
||||||
FROM kl_Einzahlungen
|
|
||||||
WHERE YEAR(Datum) = ?
|
|
||||||
GROUP BY MitarbeiterID
|
|
||||||
) yp ON yp.MitarbeiterID = m.MitarbeiterID
|
|
||||||
LEFT JOIN (
|
|
||||||
SELECT MitarbeiterID, SUM(Kosten) AS year_costs, SUM(AnzahlStriche) AS year_marks
|
|
||||||
FROM kl_Kaffeeverbrauch
|
|
||||||
WHERE YEAR(Datum) = ?
|
|
||||||
GROUP BY MitarbeiterID
|
|
||||||
) yc ON yc.MitarbeiterID = m.MitarbeiterID
|
|
||||||
WHERE m.Email IN ({$placeholders})
|
|
||||||
";
|
|
||||||
|
|
||||||
$stmt = $pdo->prepare($sql);
|
|
||||||
$stmt->execute(array_merge([$year, $year], $emails));
|
|
||||||
|
|
||||||
$actual = [];
|
|
||||||
while ($row = $stmt->fetch()) {
|
|
||||||
$actual[$row['email']] = [
|
|
||||||
'total_payments' => gm_decimal_to_cents($row['total_payments']),
|
|
||||||
'total_costs' => gm_decimal_to_cents($row['total_costs']),
|
|
||||||
'total_marks' => (int)$row['total_marks'],
|
|
||||||
'balance' => gm_decimal_to_cents($row['balance']),
|
|
||||||
'year_payments' => gm_decimal_to_cents($row['year_payments']),
|
|
||||||
'year_costs' => gm_decimal_to_cents($row['year_costs']),
|
|
||||||
'year_marks' => (int)$row['year_marks'],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return $actual;
|
|
||||||
}
|
|
||||||
|
|
||||||
function gm_fetch_sheet_emails(PDO $pdo, array $emails, string $mode): array
|
|
||||||
{
|
|
||||||
$placeholders = implode(',', array_fill(0, count($emails), '?'));
|
|
||||||
$stmt = $pdo->prepare(
|
|
||||||
"SELECT MAX(DATE(v.Datum))
|
|
||||||
FROM kl_Kaffeeverbrauch v
|
|
||||||
JOIN kl_Mitarbeiter m ON m.MitarbeiterID = v.MitarbeiterID
|
|
||||||
WHERE m.Email IN ({$placeholders})
|
|
||||||
AND DATE(v.Datum) < CURDATE()"
|
|
||||||
);
|
|
||||||
$stmt->execute($emails);
|
|
||||||
$referenceDate = $stmt->fetchColumn() ?: date('Y-m-d');
|
|
||||||
|
|
||||||
$operator = $mode === 'front' ? '>=' : '<';
|
|
||||||
$sql = "
|
|
||||||
SELECT m.Email AS email
|
|
||||||
FROM kl_Mitarbeiter m
|
|
||||||
LEFT JOIN kl_Kaffeeverbrauch v
|
|
||||||
ON m.MitarbeiterID = v.MitarbeiterID
|
|
||||||
AND v.Datum >= DATE_ADD(?, INTERVAL -100 DAY)
|
|
||||||
WHERE m.aktiv = 1
|
|
||||||
AND m.Email IN ({$placeholders})
|
|
||||||
GROUP BY m.MitarbeiterID, m.Email
|
|
||||||
HAVING COALESCE(SUM(v.AnzahlStriche), 0) {$operator} 10
|
|
||||||
ORDER BY m.Email
|
|
||||||
";
|
|
||||||
$stmt = $pdo->prepare($sql);
|
|
||||||
$stmt->execute(array_merge([$referenceDate], $emails));
|
|
||||||
|
|
||||||
return $stmt->fetchAll(PDO::FETCH_COLUMN);
|
|
||||||
}
|
|
||||||
|
|
||||||
$actualSummaries = gm_fetch_member_summaries($pdo, $emails);
|
|
||||||
gm_assert_equal('member summary count', count($expected), count($actualSummaries), $failures, $passes);
|
|
||||||
|
|
||||||
foreach ($expected as $email => $summary) {
|
|
||||||
gm_assert_equal("summary exists {$email}", true, isset($actualSummaries[$email]), $failures, $passes);
|
|
||||||
if (!isset($actualSummaries[$email])) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($summary as $key => $expectedValue) {
|
|
||||||
gm_assert_equal("{$email} {$key}", $expectedValue, $actualSummaries[$email][$key], $failures, $passes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$stmt = $pdo->prepare('SELECT Email, aktiv, admin, paypalname FROM kl_Mitarbeiter WHERE Email IN (' . implode(',', array_fill(0, count($emails), '?')) . ')');
|
|
||||||
$stmt->execute($emails);
|
|
||||||
$memberRows = [];
|
|
||||||
while ($row = $stmt->fetch()) {
|
|
||||||
$memberRows[$row['Email']] = $row;
|
|
||||||
}
|
|
||||||
foreach ($members as $member) {
|
|
||||||
$row = $memberRows[$member['email']] ?? null;
|
|
||||||
gm_assert_equal("member flags exist {$member['email']}", true, $row !== null, $failures, $passes);
|
|
||||||
if ($row === null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
gm_assert_equal("{$member['email']} active", $member['active'], (int)$row['aktiv'], $failures, $passes);
|
|
||||||
gm_assert_equal("{$member['email']} admin", $member['admin'], (int)$row['admin'], $failures, $passes);
|
|
||||||
gm_assert_equal("{$member['email']} paypalname", $member['paypalname'], $row['paypalname'], $failures, $passes);
|
|
||||||
}
|
|
||||||
|
|
||||||
$front = gm_fetch_sheet_emails($pdo, $emails, 'front');
|
|
||||||
$back = gm_fetch_sheet_emails($pdo, $emails, 'back');
|
|
||||||
gm_assert_equal('front sheet emails', gm_expected_front_sheet_emails(), $front, $failures, $passes);
|
|
||||||
gm_assert_equal('back sheet emails', gm_expected_back_sheet_emails(), $back, $failures, $passes);
|
|
||||||
|
|
||||||
$stmt = $pdo->prepare('SELECT MitarbeiterID FROM kl_Mitarbeiter WHERE Name = ? OR paypalname = ?');
|
|
||||||
$stmt->execute(['GM PayPal Alias', 'GM PayPal Alias']);
|
|
||||||
$paypalId = $stmt->fetchColumn();
|
|
||||||
gm_assert_equal('csv paypalname lookup finds participant', true, $paypalId !== false, $failures, $passes);
|
|
||||||
|
|
||||||
$stmt = $pdo->prepare(
|
|
||||||
'SELECT COUNT(*)
|
|
||||||
FROM kl_Einzahlungen
|
|
||||||
WHERE MitarbeiterID = ?
|
|
||||||
AND Betrag = ?
|
|
||||||
AND DATE(Datum) = DATE(?)'
|
|
||||||
);
|
|
||||||
$stmt->execute([$paypalId ?: 0, gm_cents_to_decimal(750), gm_dates()['recent']]);
|
|
||||||
gm_assert_equal('csv duplicate basis exists', 1, (int)$stmt->fetchColumn(), $failures, $passes);
|
|
||||||
|
|
||||||
$stmt = $pdo->prepare('SELECT COUNT(*) FROM kl_Mitarbeiter WHERE Name = ? OR paypalname = ?');
|
|
||||||
$stmt->execute(['GM Unbekannt', 'GM Unbekannt']);
|
|
||||||
gm_assert_equal('csv unknown lookup misses participant', 0, (int)$stmt->fetchColumn(), $failures, $passes);
|
|
||||||
|
|
||||||
$activeNoticeCount = (int)$pdo
|
|
||||||
->query("SELECT COUNT(*) FROM kl_hinweise WHERE nachricht LIKE '[GM]%' AND gueltig_bis >= NOW()")
|
|
||||||
->fetchColumn();
|
|
||||||
$expiredNoticeCount = (int)$pdo
|
|
||||||
->query("SELECT COUNT(*) FROM kl_hinweise WHERE nachricht LIKE '[GM]%' AND gueltig_bis < NOW()")
|
|
||||||
->fetchColumn();
|
|
||||||
gm_assert_equal('active GM notice count', 1, $activeNoticeCount, $failures, $passes);
|
|
||||||
gm_assert_equal('expired GM notice count', 1, $expiredNoticeCount, $failures, $passes);
|
|
||||||
|
|
||||||
if ($failures !== []) {
|
|
||||||
echo "\nGolden master check failed with " . count($failures) . " failure(s):\n";
|
|
||||||
foreach ($failures as $failure) {
|
|
||||||
echo "- {$failure}\n";
|
|
||||||
}
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "\nGolden master check passed with {$passes} assertions.\n";
|
|
||||||
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
require __DIR__ . '/dev-db.php';
|
|
||||||
|
|
||||||
function m3_check_assert(string $label, bool $condition, array &$failures, int &$passes): void
|
|
||||||
{
|
|
||||||
if ($condition) {
|
|
||||||
$passes++;
|
|
||||||
echo "PASS {$label}\n";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$failures[] = $label;
|
|
||||||
echo "FAIL {$label}\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
$pdo = dev_pdo();
|
|
||||||
dev_apply_schema($pdo);
|
|
||||||
|
|
||||||
$tenantSlug = getenv('M3_DEFAULT_TENANT_SLUG');
|
|
||||||
$tenantSlug = $tenantSlug !== false && trim($tenantSlug) !== '' ? trim($tenantSlug) : 'default';
|
|
||||||
|
|
||||||
$failures = [];
|
|
||||||
$passes = 0;
|
|
||||||
|
|
||||||
$stmt = $pdo->prepare('SELECT * FROM tenants WHERE slug = ?');
|
|
||||||
$stmt->execute([$tenantSlug]);
|
|
||||||
$tenants = $stmt->fetchAll();
|
|
||||||
|
|
||||||
m3_check_assert('default tenant exists exactly once', count($tenants) === 1, $failures, $passes);
|
|
||||||
if (count($tenants) !== 1) {
|
|
||||||
echo "\nM3 SaaS basis check failed: tenant '{$tenantSlug}' is missing or duplicated.\n";
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
$tenantId = (int)$tenants[0]['id'];
|
|
||||||
|
|
||||||
$legacyMemberCount = (int)$pdo->query('SELECT COUNT(*) FROM kl_Mitarbeiter')->fetchColumn();
|
|
||||||
|
|
||||||
$stmt = $pdo->prepare('SELECT COUNT(*) FROM tenant_settings WHERE tenant_id = ?');
|
|
||||||
$stmt->execute([$tenantId]);
|
|
||||||
m3_check_assert('tenant settings exist exactly once', (int)$stmt->fetchColumn() === 1, $failures, $passes);
|
|
||||||
|
|
||||||
$stmt = $pdo->prepare('SELECT COUNT(*) FROM participants WHERE tenant_id = ?');
|
|
||||||
$stmt->execute([$tenantId]);
|
|
||||||
m3_check_assert('participant count matches legacy members', (int)$stmt->fetchColumn() === $legacyMemberCount, $failures, $passes);
|
|
||||||
|
|
||||||
$stmt = $pdo->prepare(
|
|
||||||
'SELECT COUNT(DISTINCT legacy_mitarbeiter_id)
|
|
||||||
FROM participants
|
|
||||||
WHERE tenant_id = ?
|
|
||||||
AND legacy_mitarbeiter_id IS NOT NULL'
|
|
||||||
);
|
|
||||||
$stmt->execute([$tenantId]);
|
|
||||||
m3_check_assert('legacy member ids are mapped one-to-one', (int)$stmt->fetchColumn() === $legacyMemberCount, $failures, $passes);
|
|
||||||
|
|
||||||
$stmt = $pdo->prepare(
|
|
||||||
'SELECT COUNT(*)
|
|
||||||
FROM kl_Mitarbeiter m
|
|
||||||
LEFT JOIN participants p
|
|
||||||
ON p.tenant_id = ?
|
|
||||||
AND p.legacy_mitarbeiter_id = m.MitarbeiterID
|
|
||||||
WHERE p.id IS NULL'
|
|
||||||
);
|
|
||||||
$stmt->execute([$tenantId]);
|
|
||||||
m3_check_assert('no legacy member is missing a participant', (int)$stmt->fetchColumn() === 0, $failures, $passes);
|
|
||||||
|
|
||||||
$stmt = $pdo->prepare(
|
|
||||||
'SELECT COUNT(*)
|
|
||||||
FROM kl_Mitarbeiter m
|
|
||||||
LEFT JOIN users u
|
|
||||||
ON u.email_norm = LOWER(TRIM(m.Email))
|
|
||||||
LEFT JOIN tenant_memberships tm
|
|
||||||
ON tm.tenant_id = ?
|
|
||||||
AND tm.user_id = u.id
|
|
||||||
WHERE m.aktiv = 1
|
|
||||||
AND m.admin = 1
|
|
||||||
AND (tm.id IS NULL OR tm.role NOT IN (\'admin\', \'owner\'))'
|
|
||||||
);
|
|
||||||
$stmt->execute([$tenantId]);
|
|
||||||
m3_check_assert('active legacy admins have tenant memberships', (int)$stmt->fetchColumn() === 0, $failures, $passes);
|
|
||||||
|
|
||||||
$stmt = $pdo->prepare(
|
|
||||||
'SELECT COUNT(*)
|
|
||||||
FROM kl_Mitarbeiter m
|
|
||||||
JOIN users u
|
|
||||||
ON u.email_norm = LOWER(TRIM(m.Email))
|
|
||||||
JOIN tenant_memberships tm
|
|
||||||
ON tm.tenant_id = ?
|
|
||||||
AND tm.user_id = u.id
|
|
||||||
JOIN participants p
|
|
||||||
ON p.tenant_id = tm.tenant_id
|
|
||||||
AND p.legacy_mitarbeiter_id = m.MitarbeiterID
|
|
||||||
WHERE m.aktiv = 1
|
|
||||||
AND m.admin = 1
|
|
||||||
AND p.user_id <> u.id'
|
|
||||||
);
|
|
||||||
$stmt->execute([$tenantId]);
|
|
||||||
m3_check_assert('admin participants link to their login users', (int)$stmt->fetchColumn() === 0, $failures, $passes);
|
|
||||||
|
|
||||||
if ($failures !== []) {
|
|
||||||
echo "\nM3 SaaS basis check failed with " . count($failures) . " failure(s):\n";
|
|
||||||
foreach ($failures as $failure) {
|
|
||||||
echo "- {$failure}\n";
|
|
||||||
}
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "\nM3 SaaS basis check passed with {$passes} assertions.\n";
|
|
||||||
@@ -1,215 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
require __DIR__ . '/dev-db.php';
|
|
||||||
require __DIR__ . '/golden-master-data.php';
|
|
||||||
|
|
||||||
function m4_check_env(string $name, string $default): string
|
|
||||||
{
|
|
||||||
$value = getenv($name);
|
|
||||||
if ($value === false || trim($value) === '') {
|
|
||||||
return $default;
|
|
||||||
}
|
|
||||||
|
|
||||||
return trim($value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function m4_check_assert_equal(string $label, mixed $expected, mixed $actual, array &$failures, int &$passes): void
|
|
||||||
{
|
|
||||||
if ($expected === $actual) {
|
|
||||||
$passes++;
|
|
||||||
echo "PASS {$label}\n";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$failures[] = "{$label}: expected " . var_export($expected, true) . ', got ' . var_export($actual, true);
|
|
||||||
echo "FAIL {$label}\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
function m4_check_fetch_tenant_id(PDO $pdo, string $slug): int
|
|
||||||
{
|
|
||||||
$stmt = $pdo->prepare('SELECT id FROM tenants WHERE slug = ?');
|
|
||||||
$stmt->execute([$slug]);
|
|
||||||
$tenantId = $stmt->fetchColumn();
|
|
||||||
|
|
||||||
if ($tenantId === false) {
|
|
||||||
throw new RuntimeException("Tenant '{$slug}' not found.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return (int)$tenantId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, array{total_payments: int, total_costs: int, total_marks: int, balance: int, year_payments: int, year_costs: int, year_marks: int}>
|
|
||||||
*/
|
|
||||||
function m4_check_fetch_ledger_summaries(PDO $pdo, int $tenantId, array $emails): array
|
|
||||||
{
|
|
||||||
$placeholders = implode(',', array_fill(0, count($emails), '?'));
|
|
||||||
$year = gm_current_year();
|
|
||||||
$sql = "
|
|
||||||
SELECT
|
|
||||||
p.email_norm AS email,
|
|
||||||
COALESCE(SUM(CASE WHEN le.type = 'payment' THEN le.amount_cents ELSE 0 END), 0) AS total_payments,
|
|
||||||
COALESCE(SUM(CASE WHEN le.type = 'consumption' THEN -le.amount_cents ELSE 0 END), 0) AS total_costs,
|
|
||||||
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,
|
|
||||||
COALESCE(SUM(CASE WHEN le.type = 'payment' AND YEAR(le.booked_at) = ? THEN le.amount_cents ELSE 0 END), 0) AS year_payments,
|
|
||||||
COALESCE(SUM(CASE WHEN le.type = 'consumption' AND YEAR(le.booked_at) = ? THEN -le.amount_cents ELSE 0 END), 0) AS year_costs,
|
|
||||||
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 p.tenant_id = ?
|
|
||||||
AND p.email_norm IN ({$placeholders})
|
|
||||||
GROUP BY p.id, p.email_norm
|
|
||||||
";
|
|
||||||
|
|
||||||
$stmt = $pdo->prepare($sql);
|
|
||||||
$stmt->execute(array_merge([$year, $year, $year, $tenantId], $emails));
|
|
||||||
|
|
||||||
$summaries = [];
|
|
||||||
while ($row = $stmt->fetch()) {
|
|
||||||
$summaries[(string)$row['email']] = [
|
|
||||||
'total_payments' => (int)$row['total_payments'],
|
|
||||||
'total_costs' => (int)$row['total_costs'],
|
|
||||||
'total_marks' => (int)$row['total_marks'],
|
|
||||||
'balance' => (int)$row['balance'],
|
|
||||||
'year_payments' => (int)$row['year_payments'],
|
|
||||||
'year_costs' => (int)$row['year_costs'],
|
|
||||||
'year_marks' => (int)$row['year_marks'],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return $summaries;
|
|
||||||
}
|
|
||||||
|
|
||||||
$pdo = dev_pdo();
|
|
||||||
$tenantSlug = m4_check_env('M4_DEFAULT_TENANT_SLUG', m4_check_env('M3_DEFAULT_TENANT_SLUG', 'default'));
|
|
||||||
$tenantId = m4_check_fetch_tenant_id($pdo, $tenantSlug);
|
|
||||||
$expected = gm_expected_summaries();
|
|
||||||
$emails = array_keys($expected);
|
|
||||||
$failures = [];
|
|
||||||
$passes = 0;
|
|
||||||
|
|
||||||
$legacyPaymentCount = (int)$pdo->query('SELECT COUNT(*) FROM kl_Einzahlungen')->fetchColumn();
|
|
||||||
$legacyConsumptionCount = (int)$pdo->query('SELECT COUNT(*) FROM kl_Kaffeeverbrauch')->fetchColumn();
|
|
||||||
|
|
||||||
$stmt = $pdo->prepare(
|
|
||||||
"SELECT COUNT(*)
|
|
||||||
FROM ledger_entries
|
|
||||||
WHERE tenant_id = ?
|
|
||||||
AND legacy_table IN ('kl_Einzahlungen', 'kl_Kaffeeverbrauch')
|
|
||||||
AND voided_at IS NULL"
|
|
||||||
);
|
|
||||||
$stmt->execute([$tenantId]);
|
|
||||||
$ledgerLegacyCount = (int)$stmt->fetchColumn();
|
|
||||||
m4_check_assert_equal(
|
|
||||||
'active legacy ledger row count matches source rows',
|
|
||||||
$legacyPaymentCount + $legacyConsumptionCount,
|
|
||||||
$ledgerLegacyCount,
|
|
||||||
$failures,
|
|
||||||
$passes
|
|
||||||
);
|
|
||||||
|
|
||||||
$stmt = $pdo->prepare(
|
|
||||||
"SELECT
|
|
||||||
COUNT(*) AS total_rows,
|
|
||||||
COUNT(DISTINCT CONCAT(legacy_table, '#', legacy_id)) AS distinct_legacy_rows
|
|
||||||
FROM ledger_entries
|
|
||||||
WHERE tenant_id = ?
|
|
||||||
AND legacy_table IN ('kl_Einzahlungen', 'kl_Kaffeeverbrauch')"
|
|
||||||
);
|
|
||||||
$stmt->execute([$tenantId]);
|
|
||||||
$legacyDistinct = $stmt->fetch();
|
|
||||||
m4_check_assert_equal(
|
|
||||||
'legacy ledger ids are unique',
|
|
||||||
(int)$legacyDistinct['total_rows'],
|
|
||||||
(int)$legacyDistinct['distinct_legacy_rows'],
|
|
||||||
$failures,
|
|
||||||
$passes
|
|
||||||
);
|
|
||||||
|
|
||||||
// Ledger rows without a matching legacy row are only expected when they were
|
|
||||||
// voided through the storno flow in letzteneintraege.php. A missing legacy row
|
|
||||||
// on a non-voided ledger entry indicates real drift between the tables.
|
|
||||||
$stmt = $pdo->prepare(
|
|
||||||
"SELECT COUNT(*)
|
|
||||||
FROM ledger_entries le
|
|
||||||
LEFT JOIN kl_Einzahlungen e ON e.EinzahlungsID = le.legacy_id
|
|
||||||
WHERE le.tenant_id = ?
|
|
||||||
AND le.legacy_table = 'kl_Einzahlungen'
|
|
||||||
AND le.voided_at IS NULL
|
|
||||||
AND e.EinzahlungsID IS NULL"
|
|
||||||
);
|
|
||||||
$stmt->execute([$tenantId]);
|
|
||||||
m4_check_assert_equal('no stale non-voided payment ledger rows', 0, (int)$stmt->fetchColumn(), $failures, $passes);
|
|
||||||
|
|
||||||
$stmt = $pdo->prepare(
|
|
||||||
"SELECT COUNT(*)
|
|
||||||
FROM ledger_entries le
|
|
||||||
LEFT JOIN kl_Kaffeeverbrauch v ON v.VerbrauchID = le.legacy_id
|
|
||||||
WHERE le.tenant_id = ?
|
|
||||||
AND le.legacy_table = 'kl_Kaffeeverbrauch'
|
|
||||||
AND le.voided_at IS NULL
|
|
||||||
AND v.VerbrauchID IS NULL"
|
|
||||||
);
|
|
||||||
$stmt->execute([$tenantId]);
|
|
||||||
m4_check_assert_equal('no stale non-voided consumption ledger rows', 0, (int)$stmt->fetchColumn(), $failures, $passes);
|
|
||||||
|
|
||||||
$stmt = $pdo->prepare("SELECT COUNT(*) FROM ledger_entries WHERE tenant_id = ? AND type = 'payment' AND amount_cents <= 0");
|
|
||||||
$stmt->execute([$tenantId]);
|
|
||||||
m4_check_assert_equal('payment amounts are positive', 0, (int)$stmt->fetchColumn(), $failures, $passes);
|
|
||||||
|
|
||||||
$stmt = $pdo->prepare("SELECT COUNT(*) FROM ledger_entries WHERE tenant_id = ? AND type = 'consumption' AND amount_cents >= 0");
|
|
||||||
$stmt->execute([$tenantId]);
|
|
||||||
m4_check_assert_equal('consumption amounts are negative', 0, (int)$stmt->fetchColumn(), $failures, $passes);
|
|
||||||
|
|
||||||
$stmt = $pdo->prepare(
|
|
||||||
"SELECT COUNT(*)
|
|
||||||
FROM ledger_entries
|
|
||||||
WHERE tenant_id = ?
|
|
||||||
AND type = 'consumption'
|
|
||||||
AND (marks_count IS NULL OR unit_price_cents IS NULL)"
|
|
||||||
);
|
|
||||||
$stmt->execute([$tenantId]);
|
|
||||||
m4_check_assert_equal('consumption rows keep marks and unit price', 0, (int)$stmt->fetchColumn(), $failures, $passes);
|
|
||||||
|
|
||||||
$webLegacyCount = (int)$pdo
|
|
||||||
->query('SELECT COUNT(*) FROM kl_Kaffeeverbrauch WHERE Eintragsart = 2')
|
|
||||||
->fetchColumn();
|
|
||||||
$stmt = $pdo->prepare(
|
|
||||||
"SELECT COUNT(*)
|
|
||||||
FROM ledger_entries
|
|
||||||
WHERE tenant_id = ?
|
|
||||||
AND legacy_table = 'kl_Kaffeeverbrauch'
|
|
||||||
AND source = 'legacy_web'"
|
|
||||||
);
|
|
||||||
$stmt->execute([$tenantId]);
|
|
||||||
m4_check_assert_equal('web consumption source is preserved', $webLegacyCount, (int)$stmt->fetchColumn(), $failures, $passes);
|
|
||||||
|
|
||||||
$actualSummaries = m4_check_fetch_ledger_summaries($pdo, $tenantId, $emails);
|
|
||||||
m4_check_assert_equal('ledger member summary count', count($expected), count($actualSummaries), $failures, $passes);
|
|
||||||
|
|
||||||
foreach ($expected as $email => $summary) {
|
|
||||||
m4_check_assert_equal("ledger summary exists {$email}", true, isset($actualSummaries[$email]), $failures, $passes);
|
|
||||||
if (!isset($actualSummaries[$email])) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($summary as $key => $expectedValue) {
|
|
||||||
m4_check_assert_equal("ledger {$email} {$key}", $expectedValue, $actualSummaries[$email][$key], $failures, $passes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($failures !== []) {
|
|
||||||
echo "\nM4 ledger migration check failed with " . count($failures) . " failure(s):\n";
|
|
||||||
foreach ($failures as $failure) {
|
|
||||||
echo "- {$failure}\n";
|
|
||||||
}
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "\nM4 ledger migration check passed with {$passes} assertions.\n";
|
|
||||||
@@ -1,212 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
require __DIR__ . '/dev-db.php';
|
|
||||||
require __DIR__ . '/golden-master-data.php';
|
|
||||||
require __DIR__ . '/../app/ledger.php';
|
|
||||||
|
|
||||||
function m4_service_env(string $name, string $default): string
|
|
||||||
{
|
|
||||||
$value = getenv($name);
|
|
||||||
if ($value === false || trim($value) === '') {
|
|
||||||
return $default;
|
|
||||||
}
|
|
||||||
|
|
||||||
return trim($value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function m4_service_assert_equal(string $label, mixed $expected, mixed $actual, array &$failures, int &$passes): void
|
|
||||||
{
|
|
||||||
if ($expected === $actual) {
|
|
||||||
$passes++;
|
|
||||||
echo "PASS {$label}\n";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$failures[] = "{$label}: expected " . var_export($expected, true) . ', got ' . var_export($actual, true);
|
|
||||||
echo "FAIL {$label}\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
function m4_service_assert(string $label, bool $condition, array &$failures, int &$passes): void
|
|
||||||
{
|
|
||||||
m4_service_assert_equal($label, true, $condition, $failures, $passes);
|
|
||||||
}
|
|
||||||
|
|
||||||
function m4_service_fetch_tenant_id(PDO $pdo, string $slug): int
|
|
||||||
{
|
|
||||||
$stmt = $pdo->prepare('SELECT id FROM tenants WHERE slug = ?');
|
|
||||||
$stmt->execute([$slug]);
|
|
||||||
$tenantId = $stmt->fetchColumn();
|
|
||||||
|
|
||||||
if ($tenantId === false) {
|
|
||||||
throw new RuntimeException("Tenant '{$slug}' not found.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return (int)$tenantId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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 m4_service_totals_from_summaries(array $summaries): array
|
|
||||||
{
|
|
||||||
$totals = ledger_empty_totals();
|
|
||||||
|
|
||||||
foreach ($summaries as $summary) {
|
|
||||||
$totals['total_payments_cents'] += (int)$summary['total_payments_cents'];
|
|
||||||
$totals['total_costs_cents'] += (int)$summary['total_costs_cents'];
|
|
||||||
$totals['total_marks'] += (int)$summary['total_marks'];
|
|
||||||
$totals['balance_cents'] += (int)$summary['balance_cents'];
|
|
||||||
$totals['year_payments_cents'] += (int)$summary['year_payments_cents'];
|
|
||||||
$totals['year_costs_cents'] += (int)$summary['year_costs_cents'];
|
|
||||||
$totals['year_marks'] += (int)$summary['year_marks'];
|
|
||||||
}
|
|
||||||
|
|
||||||
return $totals;
|
|
||||||
}
|
|
||||||
|
|
||||||
$pdo = dev_pdo();
|
|
||||||
dev_apply_schema($pdo);
|
|
||||||
|
|
||||||
$tenantSlug = m4_service_env('M4_DEFAULT_TENANT_SLUG', m4_service_env('M3_DEFAULT_TENANT_SLUG', 'default'));
|
|
||||||
$tenantId = m4_service_fetch_tenant_id($pdo, $tenantSlug);
|
|
||||||
$expected = gm_expected_summaries();
|
|
||||||
$emails = array_keys($expected);
|
|
||||||
$failures = [];
|
|
||||||
$passes = 0;
|
|
||||||
|
|
||||||
$summaries = ledger_fetch_participant_summaries($pdo, $tenantId, [
|
|
||||||
'email_norms' => $emails,
|
|
||||||
'year' => gm_current_year(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
$summariesByEmail = [];
|
|
||||||
foreach ($summaries as $summary) {
|
|
||||||
if ($summary['email_norm'] !== null) {
|
|
||||||
$summariesByEmail[$summary['email_norm']] = $summary;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
m4_service_assert_equal('ledger service summary count', count($expected), count($summariesByEmail), $failures, $passes);
|
|
||||||
|
|
||||||
$keyMap = [
|
|
||||||
'total_payments' => 'total_payments_cents',
|
|
||||||
'total_costs' => 'total_costs_cents',
|
|
||||||
'total_marks' => 'total_marks',
|
|
||||||
'balance' => 'balance_cents',
|
|
||||||
'year_payments' => 'year_payments_cents',
|
|
||||||
'year_costs' => 'year_costs_cents',
|
|
||||||
'year_marks' => 'year_marks',
|
|
||||||
];
|
|
||||||
|
|
||||||
foreach ($expected as $email => $expectedSummary) {
|
|
||||||
m4_service_assert("ledger service summary exists {$email}", isset($summariesByEmail[$email]), $failures, $passes);
|
|
||||||
if (!isset($summariesByEmail[$email])) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
m4_service_assert(
|
|
||||||
"ledger service {$email} keeps legacy member id",
|
|
||||||
$summariesByEmail[$email]['legacy_mitarbeiter_id'] !== null,
|
|
||||||
$failures,
|
|
||||||
$passes
|
|
||||||
);
|
|
||||||
|
|
||||||
foreach ($keyMap as $expectedKey => $actualKey) {
|
|
||||||
m4_service_assert_equal(
|
|
||||||
"ledger service {$email} {$expectedKey}",
|
|
||||||
$expectedSummary[$expectedKey],
|
|
||||||
$summariesByEmail[$email][$actualKey],
|
|
||||||
$failures,
|
|
||||||
$passes
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$allSummaries = ledger_fetch_participant_summaries($pdo, $tenantId, [
|
|
||||||
'year' => gm_current_year(),
|
|
||||||
]);
|
|
||||||
$tenantTotals = ledger_fetch_tenant_totals($pdo, $tenantId, gm_current_year());
|
|
||||||
foreach (m4_service_totals_from_summaries($allSummaries) as $key => $value) {
|
|
||||||
m4_service_assert_equal("ledger service tenant total {$key}", $value, $tenantTotals[$key], $failures, $passes);
|
|
||||||
}
|
|
||||||
|
|
||||||
$stmt = $pdo->prepare('SELECT COUNT(*) FROM participants WHERE tenant_id = ? AND active = 1');
|
|
||||||
$stmt->execute([$tenantId]);
|
|
||||||
$activeExpectedCount = (int)$stmt->fetchColumn();
|
|
||||||
$activeSummaries = ledger_fetch_participant_summaries($pdo, $tenantId, ['active_only' => true]);
|
|
||||||
m4_service_assert_equal('ledger service active participant count', $activeExpectedCount, count($activeSummaries), $failures, $passes);
|
|
||||||
|
|
||||||
$paypalSummary = $summariesByEmail['gm-paypal@test.local'] ?? null;
|
|
||||||
m4_service_assert('ledger service paypal summary is present', $paypalSummary !== null, $failures, $passes);
|
|
||||||
if ($paypalSummary !== null) {
|
|
||||||
$singleSummary = ledger_fetch_participant_summary($pdo, $tenantId, $paypalSummary['participant_id'], gm_current_year());
|
|
||||||
m4_service_assert('ledger service single participant summary is present', $singleSummary !== null, $failures, $passes);
|
|
||||||
if ($singleSummary !== null) {
|
|
||||||
m4_service_assert_equal('ledger service single participant email', 'gm-paypal@test.local', $singleSummary['email_norm'], $failures, $passes);
|
|
||||||
m4_service_assert_equal('ledger service single participant balance', 650, $singleSummary['balance_cents'], $failures, $passes);
|
|
||||||
}
|
|
||||||
|
|
||||||
$legacySummary = ledger_fetch_participant_summary_by_legacy_id(
|
|
||||||
$pdo,
|
|
||||||
$tenantId,
|
|
||||||
(int)$paypalSummary['legacy_mitarbeiter_id'],
|
|
||||||
gm_current_year()
|
|
||||||
);
|
|
||||||
m4_service_assert('ledger service legacy participant summary is present', $legacySummary !== null, $failures, $passes);
|
|
||||||
if ($legacySummary !== null) {
|
|
||||||
m4_service_assert_equal('ledger service legacy participant id matches', $paypalSummary['participant_id'], $legacySummary['participant_id'], $failures, $passes);
|
|
||||||
m4_service_assert_equal('ledger service legacy participant balance', 650, $legacySummary['balance_cents'], $failures, $passes);
|
|
||||||
}
|
|
||||||
|
|
||||||
$paypalEntries = ledger_fetch_recent_entries($pdo, $tenantId, [
|
|
||||||
'participant_id' => $paypalSummary['participant_id'],
|
|
||||||
'limit' => 100,
|
|
||||||
]);
|
|
||||||
m4_service_assert_equal('ledger service paypal entry count', 2, count($paypalEntries), $failures, $passes);
|
|
||||||
foreach ($paypalEntries as $entry) {
|
|
||||||
m4_service_assert_equal('ledger service paypal entry tenant', $tenantId, $entry['tenant_id'], $failures, $passes);
|
|
||||||
m4_service_assert_equal('ledger service paypal entry participant', $paypalSummary['participant_id'], $entry['participant_id'], $failures, $passes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$recentEntries = ledger_fetch_recent_entries($pdo, $tenantId, ['limit' => 5]);
|
|
||||||
m4_service_assert_equal('ledger service recent entry limit', 5, count($recentEntries), $failures, $passes);
|
|
||||||
$previousBookedAt = null;
|
|
||||||
foreach ($recentEntries as $entry) {
|
|
||||||
m4_service_assert_equal('ledger service recent entry tenant', $tenantId, $entry['tenant_id'], $failures, $passes);
|
|
||||||
m4_service_assert('ledger service recent entry has amount', $entry['amount_cents'] !== 0, $failures, $passes);
|
|
||||||
if ($previousBookedAt !== null) {
|
|
||||||
m4_service_assert('ledger service recent entries are sorted', $previousBookedAt >= $entry['booked_at'], $failures, $passes);
|
|
||||||
}
|
|
||||||
$previousBookedAt = $entry['booked_at'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$legacyConsumptionCount = (int)$pdo->query('SELECT COUNT(*) FROM kl_Kaffeeverbrauch')->fetchColumn();
|
|
||||||
$consumptionEntries = ledger_fetch_recent_entries($pdo, $tenantId, [
|
|
||||||
'type' => 'consumption',
|
|
||||||
'limit' => 100,
|
|
||||||
]);
|
|
||||||
m4_service_assert_equal('ledger service consumption entry count', $legacyConsumptionCount, count($consumptionEntries), $failures, $passes);
|
|
||||||
|
|
||||||
m4_service_assert_equal('ledger service empty participant id filter', [], ledger_fetch_participant_summaries($pdo, $tenantId, [
|
|
||||||
'participant_ids' => [],
|
|
||||||
]), $failures, $passes);
|
|
||||||
m4_service_assert_equal('ledger service empty legacy id filter', [], ledger_fetch_participant_summaries($pdo, $tenantId, [
|
|
||||||
'legacy_mitarbeiter_ids' => [],
|
|
||||||
]), $failures, $passes);
|
|
||||||
m4_service_assert_equal('ledger service unknown legacy summary', null, ledger_fetch_participant_summary_by_legacy_id($pdo, $tenantId, 0), $failures, $passes);
|
|
||||||
m4_service_assert_equal('ledger service unknown tenant summaries', [], ledger_fetch_participant_summaries($pdo, 0), $failures, $passes);
|
|
||||||
m4_service_assert_equal('ledger service unknown tenant entries', [], ledger_fetch_recent_entries($pdo, 0), $failures, $passes);
|
|
||||||
m4_service_assert_equal('ledger service unknown tenant totals', ledger_empty_totals(), ledger_fetch_tenant_totals($pdo, 0), $failures, $passes);
|
|
||||||
|
|
||||||
if ($failures !== []) {
|
|
||||||
echo "\nM4 ledger service check failed with " . count($failures) . " failure(s):\n";
|
|
||||||
foreach ($failures as $failure) {
|
|
||||||
echo "- {$failure}\n";
|
|
||||||
}
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "\nM4 ledger service check passed with {$passes} assertions.\n";
|
|
||||||
@@ -1,269 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
function gm_current_year(): int
|
|
||||||
{
|
|
||||||
return (int)date('Y');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, string>
|
|
||||||
*/
|
|
||||||
function gm_dates(): array
|
|
||||||
{
|
|
||||||
$year = gm_current_year();
|
|
||||||
|
|
||||||
return [
|
|
||||||
'recent' => date('Y-m-d 10:00:00', strtotime('-1 day')),
|
|
||||||
'current' => sprintf('%04d-01-15 09:00:00', $year),
|
|
||||||
'previous' => sprintf('%04d-12-20 09:00:00', $year - 1),
|
|
||||||
'notice_active' => date('Y-m-d 12:00:00', strtotime('+30 days')),
|
|
||||||
'notice_expired' => date('Y-m-d 12:00:00', strtotime('-30 days')),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, array{name: string, email: string, active: int, admin: int, paypalname: ?string}>
|
|
||||||
*/
|
|
||||||
function gm_members(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'admin' => [
|
|
||||||
'name' => 'GM Admin',
|
|
||||||
'email' => 'gm-admin@test.local',
|
|
||||||
'active' => 1,
|
|
||||||
'admin' => 1,
|
|
||||||
'paypalname' => null,
|
|
||||||
],
|
|
||||||
'positive' => [
|
|
||||||
'name' => 'GM Positiv',
|
|
||||||
'email' => 'gm-positive@test.local',
|
|
||||||
'active' => 1,
|
|
||||||
'admin' => 0,
|
|
||||||
'paypalname' => null,
|
|
||||||
],
|
|
||||||
'negative' => [
|
|
||||||
'name' => 'GM Negativ',
|
|
||||||
'email' => 'gm-negative@test.local',
|
|
||||||
'active' => 1,
|
|
||||||
'admin' => 0,
|
|
||||||
'paypalname' => null,
|
|
||||||
],
|
|
||||||
'zero' => [
|
|
||||||
'name' => 'GM Nullsaldo',
|
|
||||||
'email' => 'gm-zero@test.local',
|
|
||||||
'active' => 1,
|
|
||||||
'admin' => 0,
|
|
||||||
'paypalname' => null,
|
|
||||||
],
|
|
||||||
'inactive' => [
|
|
||||||
'name' => 'GM Inaktiv',
|
|
||||||
'email' => 'gm-inactive@test.local',
|
|
||||||
'active' => 0,
|
|
||||||
'admin' => 0,
|
|
||||||
'paypalname' => null,
|
|
||||||
],
|
|
||||||
'paypal' => [
|
|
||||||
'name' => 'GM Paypal',
|
|
||||||
'email' => 'gm-paypal@test.local',
|
|
||||||
'active' => 1,
|
|
||||||
'admin' => 0,
|
|
||||||
'paypalname' => 'GM PayPal Alias',
|
|
||||||
],
|
|
||||||
'empty' => [
|
|
||||||
'name' => 'GM Ohne Buchungen',
|
|
||||||
'email' => 'gm-empty@test.local',
|
|
||||||
'active' => 1,
|
|
||||||
'admin' => 0,
|
|
||||||
'paypalname' => null,
|
|
||||||
],
|
|
||||||
'heavy' => [
|
|
||||||
'name' => 'GM Vieltrinker',
|
|
||||||
'email' => 'gm-heavy@test.local',
|
|
||||||
'active' => 1,
|
|
||||||
'admin' => 0,
|
|
||||||
'paypalname' => null,
|
|
||||||
],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, list<array{amount_cents: int, date_key: string}>>
|
|
||||||
*/
|
|
||||||
function gm_payments(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'admin' => [
|
|
||||||
['amount_cents' => 300, 'date_key' => 'current'],
|
|
||||||
],
|
|
||||||
'positive' => [
|
|
||||||
['amount_cents' => 2000, 'date_key' => 'current'],
|
|
||||||
['amount_cents' => 500, 'date_key' => 'previous'],
|
|
||||||
],
|
|
||||||
'negative' => [
|
|
||||||
['amount_cents' => 100, 'date_key' => 'current'],
|
|
||||||
],
|
|
||||||
'zero' => [
|
|
||||||
['amount_cents' => 200, 'date_key' => 'current'],
|
|
||||||
],
|
|
||||||
'inactive' => [
|
|
||||||
['amount_cents' => 1000, 'date_key' => 'current'],
|
|
||||||
],
|
|
||||||
'paypal' => [
|
|
||||||
['amount_cents' => 750, 'date_key' => 'recent'],
|
|
||||||
],
|
|
||||||
'heavy' => [
|
|
||||||
['amount_cents' => 500, 'date_key' => 'current'],
|
|
||||||
],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, list<array{marks: int, unit_price_cents: int, date_key: string, entry_type: ?int}>>
|
|
||||||
*/
|
|
||||||
function gm_consumptions(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'admin' => [
|
|
||||||
['marks' => 1, 'unit_price_cents' => 20, 'date_key' => 'recent', 'entry_type' => 2],
|
|
||||||
],
|
|
||||||
'positive' => [
|
|
||||||
['marks' => 5, 'unit_price_cents' => 20, 'date_key' => 'recent', 'entry_type' => 2],
|
|
||||||
['marks' => 2, 'unit_price_cents' => 20, 'date_key' => 'previous', 'entry_type' => null],
|
|
||||||
],
|
|
||||||
'negative' => [
|
|
||||||
['marks' => 10, 'unit_price_cents' => 20, 'date_key' => 'recent', 'entry_type' => null],
|
|
||||||
['marks' => 5, 'unit_price_cents' => 20, 'date_key' => 'previous', 'entry_type' => null],
|
|
||||||
],
|
|
||||||
'zero' => [
|
|
||||||
['marks' => 10, 'unit_price_cents' => 20, 'date_key' => 'recent', 'entry_type' => null],
|
|
||||||
],
|
|
||||||
'inactive' => [
|
|
||||||
['marks' => 20, 'unit_price_cents' => 20, 'date_key' => 'recent', 'entry_type' => null],
|
|
||||||
],
|
|
||||||
'paypal' => [
|
|
||||||
['marks' => 5, 'unit_price_cents' => 20, 'date_key' => 'recent', 'entry_type' => 2],
|
|
||||||
],
|
|
||||||
'heavy' => [
|
|
||||||
['marks' => 12, 'unit_price_cents' => 20, 'date_key' => 'recent', 'entry_type' => null],
|
|
||||||
],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, array{total_payments: int, total_costs: int, total_marks: int, balance: int, year_payments: int, year_costs: int, year_marks: int}>
|
|
||||||
*/
|
|
||||||
function gm_expected_summaries(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'gm-admin@test.local' => [
|
|
||||||
'total_payments' => 300,
|
|
||||||
'total_costs' => 20,
|
|
||||||
'total_marks' => 1,
|
|
||||||
'balance' => 280,
|
|
||||||
'year_payments' => 300,
|
|
||||||
'year_costs' => 20,
|
|
||||||
'year_marks' => 1,
|
|
||||||
],
|
|
||||||
'gm-positive@test.local' => [
|
|
||||||
'total_payments' => 2500,
|
|
||||||
'total_costs' => 140,
|
|
||||||
'total_marks' => 7,
|
|
||||||
'balance' => 2360,
|
|
||||||
'year_payments' => 2000,
|
|
||||||
'year_costs' => 100,
|
|
||||||
'year_marks' => 5,
|
|
||||||
],
|
|
||||||
'gm-negative@test.local' => [
|
|
||||||
'total_payments' => 100,
|
|
||||||
'total_costs' => 300,
|
|
||||||
'total_marks' => 15,
|
|
||||||
'balance' => -200,
|
|
||||||
'year_payments' => 100,
|
|
||||||
'year_costs' => 200,
|
|
||||||
'year_marks' => 10,
|
|
||||||
],
|
|
||||||
'gm-zero@test.local' => [
|
|
||||||
'total_payments' => 200,
|
|
||||||
'total_costs' => 200,
|
|
||||||
'total_marks' => 10,
|
|
||||||
'balance' => 0,
|
|
||||||
'year_payments' => 200,
|
|
||||||
'year_costs' => 200,
|
|
||||||
'year_marks' => 10,
|
|
||||||
],
|
|
||||||
'gm-inactive@test.local' => [
|
|
||||||
'total_payments' => 1000,
|
|
||||||
'total_costs' => 400,
|
|
||||||
'total_marks' => 20,
|
|
||||||
'balance' => 600,
|
|
||||||
'year_payments' => 1000,
|
|
||||||
'year_costs' => 400,
|
|
||||||
'year_marks' => 20,
|
|
||||||
],
|
|
||||||
'gm-paypal@test.local' => [
|
|
||||||
'total_payments' => 750,
|
|
||||||
'total_costs' => 100,
|
|
||||||
'total_marks' => 5,
|
|
||||||
'balance' => 650,
|
|
||||||
'year_payments' => 750,
|
|
||||||
'year_costs' => 100,
|
|
||||||
'year_marks' => 5,
|
|
||||||
],
|
|
||||||
'gm-empty@test.local' => [
|
|
||||||
'total_payments' => 0,
|
|
||||||
'total_costs' => 0,
|
|
||||||
'total_marks' => 0,
|
|
||||||
'balance' => 0,
|
|
||||||
'year_payments' => 0,
|
|
||||||
'year_costs' => 0,
|
|
||||||
'year_marks' => 0,
|
|
||||||
],
|
|
||||||
'gm-heavy@test.local' => [
|
|
||||||
'total_payments' => 500,
|
|
||||||
'total_costs' => 240,
|
|
||||||
'total_marks' => 12,
|
|
||||||
'balance' => 260,
|
|
||||||
'year_payments' => 500,
|
|
||||||
'year_costs' => 240,
|
|
||||||
'year_marks' => 12,
|
|
||||||
],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return list<string>
|
|
||||||
*/
|
|
||||||
function gm_expected_front_sheet_emails(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'gm-heavy@test.local',
|
|
||||||
'gm-negative@test.local',
|
|
||||||
'gm-zero@test.local',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return list<string>
|
|
||||||
*/
|
|
||||||
function gm_expected_back_sheet_emails(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'gm-admin@test.local',
|
|
||||||
'gm-empty@test.local',
|
|
||||||
'gm-paypal@test.local',
|
|
||||||
'gm-positive@test.local',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
function gm_cents_to_decimal(int $cents): string
|
|
||||||
{
|
|
||||||
return number_format($cents / 100, 2, '.', '');
|
|
||||||
}
|
|
||||||
|
|
||||||
function gm_decimal_to_cents(mixed $value): int
|
|
||||||
{
|
|
||||||
return (int)round(((float)$value) * 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
+57
-25
@@ -2,6 +2,15 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Richtet eine frische MySQL-Entwicklungsdatenbank ein: Schema/Migrationen
|
||||||
|
* anwenden und einen ersten SaaS-Mandanten samt Owner-Login, Teilnehmer,
|
||||||
|
* ein paar Journalbuchungen und einem Hinweis anlegen - alles idempotent.
|
||||||
|
*
|
||||||
|
* Der anzulegende Owner kommt aus DEV_AUTH_EMAIL (Anzeigename optional aus
|
||||||
|
* DEV_AUTH_NAME). Nach dem Lauf kann man sich damit ueber login.php anmelden.
|
||||||
|
*/
|
||||||
|
|
||||||
require __DIR__ . '/dev-db.php';
|
require __DIR__ . '/dev-db.php';
|
||||||
|
|
||||||
$email = getenv('DEV_AUTH_EMAIL');
|
$email = getenv('DEV_AUTH_EMAIL');
|
||||||
@@ -15,35 +24,58 @@ dev_apply_schema($pdo);
|
|||||||
|
|
||||||
$email = strtolower(trim((string)$email));
|
$email = strtolower(trim((string)$email));
|
||||||
$name = trim((string)(getenv('DEV_AUTH_NAME') ?: 'Test Admin'));
|
$name = trim((string)(getenv('DEV_AUTH_NAME') ?: 'Test Admin'));
|
||||||
|
$password = trim((string)(getenv('DEV_AUTH_PASSWORD') ?: 'DevPasswort123!'));
|
||||||
|
$slug = trim((string)(getenv('DEV_TENANT_SLUG') ?: 'dev'));
|
||||||
|
|
||||||
$stmt = $pdo->prepare(
|
// Mandant.
|
||||||
'INSERT INTO kl_Mitarbeiter (Name, Email, aktiv, admin)
|
$tenantId = (int)($pdo->query('SELECT id FROM tenants WHERE slug = ' . $pdo->quote($slug))->fetchColumn() ?: 0);
|
||||||
VALUES (?, ?, 1, 1)
|
if ($tenantId === 0) {
|
||||||
ON DUPLICATE KEY UPDATE Name = VALUES(Name), aktiv = 1, admin = 1'
|
$pdo->prepare('INSERT INTO tenants (slug, name, status) VALUES (?, ?, ?)')
|
||||||
);
|
->execute([$slug, 'Dev-Mandant', 'active']);
|
||||||
$stmt->execute([$name, $email]);
|
$tenantId = (int)$pdo->lastInsertId();
|
||||||
|
}
|
||||||
$memberId = (int)$pdo->query('SELECT MitarbeiterID FROM kl_Mitarbeiter WHERE Email = ' . $pdo->quote($email))->fetchColumn();
|
if ((int)$pdo->query("SELECT COUNT(*) FROM tenant_settings WHERE tenant_id = {$tenantId}")->fetchColumn() === 0) {
|
||||||
|
$pdo->prepare('INSERT INTO tenant_settings (tenant_id) VALUES (?)')->execute([$tenantId]);
|
||||||
$paymentCount = (int)$pdo->query('SELECT COUNT(*) FROM kl_Einzahlungen WHERE MitarbeiterID = ' . $memberId)->fetchColumn();
|
|
||||||
if ($paymentCount === 0) {
|
|
||||||
$stmt = $pdo->prepare('INSERT INTO kl_Einzahlungen (MitarbeiterID, Betrag, Datum) VALUES (?, ?, NOW())');
|
|
||||||
$stmt->execute([$memberId, 10.00]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$consumptionCount = (int)$pdo->query('SELECT COUNT(*) FROM kl_Kaffeeverbrauch WHERE MitarbeiterID = ' . $memberId)->fetchColumn();
|
// Owner-Login.
|
||||||
if ($consumptionCount === 0) {
|
$userId = (int)($pdo->query('SELECT id FROM users WHERE email_norm = ' . $pdo->quote($email))->fetchColumn() ?: 0);
|
||||||
$stmt = $pdo->prepare(
|
if ($userId === 0) {
|
||||||
'INSERT INTO kl_Kaffeeverbrauch (MitarbeiterID, AnzahlStriche, Kosten, KostenproStrich, Datum, Eintragsart)
|
$pdo->prepare(
|
||||||
VALUES (?, ?, ?, ?, NOW(), ?)'
|
'INSERT INTO users (email, email_norm, display_name, password_hash, status, email_verified_at)
|
||||||
);
|
VALUES (?, ?, ?, ?, ?, NOW())'
|
||||||
$stmt->execute([$memberId, 3, 0.60, 0.20, 2]);
|
)->execute([$email, $email, $name, password_hash($password, PASSWORD_DEFAULT), 'active']);
|
||||||
|
$userId = (int)$pdo->lastInsertId();
|
||||||
|
}
|
||||||
|
if ((int)$pdo->query("SELECT COUNT(*) FROM tenant_memberships WHERE tenant_id = {$tenantId} AND user_id = {$userId}")->fetchColumn() === 0) {
|
||||||
|
$pdo->prepare('INSERT INTO tenant_memberships (tenant_id, user_id, role, status, joined_at) VALUES (?, ?, ?, ?, NOW())')
|
||||||
|
->execute([$tenantId, $userId, 'owner', 'active']);
|
||||||
}
|
}
|
||||||
|
|
||||||
$noticeCount = (int)$pdo->query('SELECT COUNT(*) FROM kl_hinweise')->fetchColumn();
|
// Teilnehmer des Owners.
|
||||||
if ($noticeCount === 0) {
|
$participantId = (int)($pdo->query("SELECT id FROM participants WHERE tenant_id = {$tenantId} AND user_id = {$userId}")->fetchColumn() ?: 0);
|
||||||
$stmt = $pdo->prepare('INSERT INTO kl_hinweise (nachricht, gueltig_bis) VALUES (?, DATE_ADD(NOW(), INTERVAL 30 DAY))');
|
if ($participantId === 0) {
|
||||||
$stmt->execute(['Dev-Hinweis: MySQL-Testumgebung aktiv.']);
|
$pdo->prepare('INSERT INTO participants (tenant_id, user_id, display_name, email, email_norm, active) VALUES (?, ?, ?, ?, ?, 1)')
|
||||||
|
->execute([$tenantId, $userId, $name, $email, $email]);
|
||||||
|
$participantId = (int)$pdo->lastInsertId();
|
||||||
}
|
}
|
||||||
|
|
||||||
echo "MySQL dev schema is ready for {$email}.\n";
|
// Ein paar Buchungen, damit die Seiten nicht komplett leer sind.
|
||||||
|
if ((int)$pdo->query("SELECT COUNT(*) FROM ledger_entries WHERE tenant_id = {$tenantId} AND participant_id = {$participantId}")->fetchColumn() === 0) {
|
||||||
|
$pdo->prepare(
|
||||||
|
"INSERT INTO ledger_entries (tenant_id, participant_id, type, amount_cents, booked_at, source) VALUES (?, ?, 'payment', ?, NOW(), 'manual_bulk')"
|
||||||
|
)->execute([$tenantId, $participantId, 1000]);
|
||||||
|
$pdo->prepare(
|
||||||
|
"INSERT INTO ledger_entries (tenant_id, participant_id, type, amount_cents, marks_count, unit_price_cents, booked_at, source) VALUES (?, ?, 'consumption', ?, ?, 20, NOW(), 'manual_bulk')"
|
||||||
|
)->execute([$tenantId, $participantId, -60, 3]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hinweis.
|
||||||
|
if ((int)$pdo->query("SELECT COUNT(*) FROM notices WHERE tenant_id = {$tenantId} AND deleted_at IS NULL")->fetchColumn() === 0) {
|
||||||
|
$pdo->prepare(
|
||||||
|
'INSERT INTO notices (tenant_id, message, valid_from, valid_until, created_by_user_id)
|
||||||
|
VALUES (?, ?, NOW(), DATE_ADD(NOW(), INTERVAL 30 DAY), ?)'
|
||||||
|
)->execute([$tenantId, 'Dev-Hinweis: MySQL-Testumgebung aktiv.', $userId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "MySQL dev schema is ready. Login: {$email} / {$password} (Mandant '{$slug}').\n";
|
||||||
|
|||||||
@@ -1,129 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
require __DIR__ . '/dev-db.php';
|
|
||||||
require __DIR__ . '/golden-master-data.php';
|
|
||||||
|
|
||||||
$pdo = dev_pdo();
|
|
||||||
dev_apply_schema($pdo);
|
|
||||||
|
|
||||||
$dates = gm_dates();
|
|
||||||
$members = gm_members();
|
|
||||||
$payments = gm_payments();
|
|
||||||
$consumptions = gm_consumptions();
|
|
||||||
|
|
||||||
$emails = array_values(array_map(static fn (array $member): string => $member['email'], $members));
|
|
||||||
$placeholders = implode(',', array_fill(0, count($emails), '?'));
|
|
||||||
|
|
||||||
$pdo->beginTransaction();
|
|
||||||
|
|
||||||
try {
|
|
||||||
$step = 'select existing fixture ids';
|
|
||||||
$stmt = $pdo->prepare("SELECT MitarbeiterID FROM kl_Mitarbeiter WHERE Email IN ({$placeholders})");
|
|
||||||
$stmt->execute($emails);
|
|
||||||
$ids = array_map('intval', $stmt->fetchAll(PDO::FETCH_COLUMN));
|
|
||||||
|
|
||||||
if ($ids !== []) {
|
|
||||||
$idPlaceholders = implode(',', array_fill(0, count($ids), '?'));
|
|
||||||
$step = 'delete fixture payments';
|
|
||||||
$stmt = $pdo->prepare("DELETE FROM kl_Einzahlungen WHERE MitarbeiterID IN ({$idPlaceholders})");
|
|
||||||
$stmt->execute($ids);
|
|
||||||
$step = 'delete fixture consumption';
|
|
||||||
$stmt = $pdo->prepare("DELETE FROM kl_Kaffeeverbrauch WHERE MitarbeiterID IN ({$idPlaceholders})");
|
|
||||||
$stmt->execute($ids);
|
|
||||||
}
|
|
||||||
|
|
||||||
$step = 'delete fixture members';
|
|
||||||
$stmt = $pdo->prepare("DELETE FROM kl_Mitarbeiter WHERE Email IN ({$placeholders})");
|
|
||||||
$stmt->execute($emails);
|
|
||||||
|
|
||||||
$step = 'delete fixture notices';
|
|
||||||
$pdo->exec("DELETE FROM kl_hinweise WHERE nachricht LIKE '[GM]%'");
|
|
||||||
$step = 'delete fixture survey locks';
|
|
||||||
$pdo->exec("DELETE FROM CoffeeSurveyVotedEmails WHERE EmailNorm LIKE 'gm-%@test.local'");
|
|
||||||
|
|
||||||
$insertMember = $pdo->prepare(
|
|
||||||
'INSERT INTO kl_Mitarbeiter (Name, Email, paypalname, aktiv, admin)
|
|
||||||
VALUES (?, ?, ?, ?, ?)'
|
|
||||||
);
|
|
||||||
$selectMemberId = $pdo->prepare('SELECT MitarbeiterID FROM kl_Mitarbeiter WHERE Email = ?');
|
|
||||||
$memberIds = [];
|
|
||||||
|
|
||||||
foreach ($members as $key => $member) {
|
|
||||||
$step = 'insert member ' . $key;
|
|
||||||
$insertMember->execute([
|
|
||||||
$member['name'],
|
|
||||||
$member['email'],
|
|
||||||
$member['paypalname'],
|
|
||||||
$member['active'],
|
|
||||||
$member['admin'],
|
|
||||||
]);
|
|
||||||
$selectMemberId->execute([$member['email']]);
|
|
||||||
$memberIds[$key] = (int)$selectMemberId->fetchColumn();
|
|
||||||
}
|
|
||||||
|
|
||||||
$insertPayment = $pdo->prepare(
|
|
||||||
'INSERT INTO kl_Einzahlungen (MitarbeiterID, Betrag, Datum)
|
|
||||||
VALUES (?, ?, ?)'
|
|
||||||
);
|
|
||||||
foreach ($payments as $memberKey => $entries) {
|
|
||||||
foreach ($entries as $entry) {
|
|
||||||
$step = 'insert payment ' . $memberKey;
|
|
||||||
$insertPayment->execute([
|
|
||||||
$memberIds[$memberKey],
|
|
||||||
gm_cents_to_decimal($entry['amount_cents']),
|
|
||||||
$dates[$entry['date_key']],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$insertConsumption = $pdo->prepare(
|
|
||||||
'INSERT INTO kl_Kaffeeverbrauch
|
|
||||||
(MitarbeiterID, AnzahlStriche, Kosten, KostenproStrich, Datum, Eintragsart)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?)'
|
|
||||||
);
|
|
||||||
foreach ($consumptions as $memberKey => $entries) {
|
|
||||||
foreach ($entries as $entry) {
|
|
||||||
$step = 'insert consumption ' . $memberKey;
|
|
||||||
$costCents = $entry['marks'] * $entry['unit_price_cents'];
|
|
||||||
$insertConsumption->execute([
|
|
||||||
$memberIds[$memberKey],
|
|
||||||
$entry['marks'],
|
|
||||||
gm_cents_to_decimal($costCents),
|
|
||||||
gm_cents_to_decimal($entry['unit_price_cents']),
|
|
||||||
$dates[$entry['date_key']],
|
|
||||||
$entry['entry_type'],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$step = 'insert notices';
|
|
||||||
$insertNotice = $pdo->prepare('INSERT INTO kl_hinweise (nachricht, gueltig_bis) VALUES (?, ?)');
|
|
||||||
$insertNotice->execute(['[GM] Aktiver Hinweis für Golden-Master-Test', $dates['notice_active']]);
|
|
||||||
$insertNotice->execute(['[GM] Abgelaufener Hinweis für Golden-Master-Test', $dates['notice_expired']]);
|
|
||||||
|
|
||||||
$metadata = [
|
|
||||||
'golden_master_seed' => 'seeded',
|
|
||||||
'golden_master_seed_year' => (string)gm_current_year(),
|
|
||||||
'golden_master_seed_members' => (string)count($members),
|
|
||||||
'golden_master_seed_source' => 'scripts/seed-golden-master.php',
|
|
||||||
];
|
|
||||||
$insertMeta = $pdo->prepare(
|
|
||||||
'INSERT INTO dev_baseline_metadata (meta_key, meta_value)
|
|
||||||
VALUES (?, ?)
|
|
||||||
ON DUPLICATE KEY UPDATE meta_value = VALUES(meta_value)'
|
|
||||||
);
|
|
||||||
foreach ($metadata as $key => $value) {
|
|
||||||
$step = 'insert metadata ' . $key;
|
|
||||||
$insertMeta->execute([$key, $value]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$pdo->commit();
|
|
||||||
} catch (Throwable $e) {
|
|
||||||
$pdo->rollBack();
|
|
||||||
fwrite(STDERR, "Golden master seed failed at {$step}: {$e->getMessage()}\n");
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "Golden master fixtures seeded: " . count($members) . " members.\n";
|
|
||||||
Reference in New Issue
Block a user