Phase1 Bearbeitung
This commit is contained in:
@@ -29,6 +29,9 @@ final class LedgerService
|
||||
throw new RuntimeException('Die Menge muss groesser als 0 sein.');
|
||||
}
|
||||
|
||||
$this->assertTenantMembership($tenantId, $recordedByUserId);
|
||||
$this->assertTenantMember($tenantId, $memberId);
|
||||
$this->assertTenantProduct($tenantId, $productId);
|
||||
$source = $this->findSource($tenantId, $sourceCode);
|
||||
$priceCents = $this->resolvePriceCents($tenantId, $productId, $effectiveAt);
|
||||
$totalCents = (int) round($priceCents * $quantity);
|
||||
@@ -179,6 +182,8 @@ final class LedgerService
|
||||
throw new RuntimeException('Der Einzahlungsbetrag muss groesser als 0 sein.');
|
||||
}
|
||||
|
||||
$this->assertTenantMembership($tenantId, $recordedByUserId);
|
||||
$this->assertTenantMember($tenantId, $memberId);
|
||||
$source = $this->findSource($tenantId, 'admin_backoffice');
|
||||
|
||||
$entryInsert = $this->pdo->prepare(
|
||||
@@ -270,6 +275,12 @@ final class LedgerService
|
||||
?string $notes,
|
||||
int $createdByUserId
|
||||
): int {
|
||||
$this->assertTenantMembership($tenantId, $createdByUserId);
|
||||
|
||||
if (trim($title) === '') {
|
||||
throw new RuntimeException('Bitte einen Titel fuer die Papierliste angeben.');
|
||||
}
|
||||
|
||||
$statement = $this->pdo->prepare(
|
||||
'INSERT INTO paper_sheets (
|
||||
tenant_id,
|
||||
@@ -320,6 +331,10 @@ final class LedgerService
|
||||
throw new RuntimeException('Die Menge muss groesser als 0 sein.');
|
||||
}
|
||||
|
||||
$this->assertTenantMembership($tenantId, $actorUserId);
|
||||
$this->assertTenantMember($tenantId, $memberId);
|
||||
$this->assertTenantProduct($tenantId, $productId);
|
||||
|
||||
$sheet = $this->pdo->prepare(
|
||||
'SELECT id, status FROM paper_sheets WHERE id = :id AND tenant_id = :tenant_id LIMIT 1'
|
||||
);
|
||||
@@ -375,6 +390,8 @@ final class LedgerService
|
||||
|
||||
public function postPaperSheet(int $tenantId, int $sheetId, int $actorUserId): void
|
||||
{
|
||||
$this->assertTenantMembership($tenantId, $actorUserId);
|
||||
|
||||
$sheetStatement = $this->pdo->prepare(
|
||||
'SELECT id, status FROM paper_sheets WHERE id = :id AND tenant_id = :tenant_id LIMIT 1'
|
||||
);
|
||||
@@ -432,6 +449,170 @@ final class LedgerService
|
||||
$this->audit->log($tenantId, $actorUserId, 'paper_sheet.posted', 'paper_sheet', $sheetId, 'success');
|
||||
}
|
||||
|
||||
public function reverseLedgerEntry(int $tenantId, int $ledgerEntryId, int $actorUserId, ?string $reason = null): int
|
||||
{
|
||||
$this->assertTenantMembership($tenantId, $actorUserId);
|
||||
|
||||
$entry = $this->ledgerEntry($tenantId, $ledgerEntryId);
|
||||
|
||||
if ($this->hasReversal($tenantId, $ledgerEntryId)) {
|
||||
throw new RuntimeException('Dieser Vorgang wurde bereits storniert.');
|
||||
}
|
||||
|
||||
$linesStatement = $this->pdo->prepare(
|
||||
'SELECT member_id, account_code, amount_cents
|
||||
FROM ledger_lines
|
||||
WHERE tenant_id = :tenant_id AND ledger_entry_id = :ledger_entry_id
|
||||
ORDER BY id ASC'
|
||||
);
|
||||
$linesStatement->execute([
|
||||
'tenant_id' => $tenantId,
|
||||
'ledger_entry_id' => $ledgerEntryId,
|
||||
]);
|
||||
$lines = $linesStatement->fetchAll();
|
||||
|
||||
if ($lines === []) {
|
||||
throw new RuntimeException('Es wurden keine Ledger-Zeilen fuer den Vorgang gefunden.');
|
||||
}
|
||||
|
||||
$description = 'Storno: ' . $entry['description'];
|
||||
|
||||
if ($reason !== null && trim($reason) !== '') {
|
||||
$description .= ' (' . trim($reason) . ')';
|
||||
}
|
||||
|
||||
$this->pdo->beginTransaction();
|
||||
|
||||
try {
|
||||
$entryInsert = $this->pdo->prepare(
|
||||
'INSERT INTO ledger_entries (
|
||||
tenant_id,
|
||||
member_id,
|
||||
source_id,
|
||||
type,
|
||||
reference_type,
|
||||
reference_id,
|
||||
description,
|
||||
occurred_at,
|
||||
created_by_user_id,
|
||||
reversal_of_entry_id
|
||||
) VALUES (
|
||||
:tenant_id,
|
||||
:member_id,
|
||||
:source_id,
|
||||
"correction",
|
||||
"ledger_reversal",
|
||||
:reference_id,
|
||||
:description,
|
||||
:occurred_at,
|
||||
:created_by_user_id,
|
||||
:reversal_of_entry_id
|
||||
)'
|
||||
);
|
||||
$entryInsert->execute([
|
||||
'tenant_id' => $tenantId,
|
||||
'member_id' => $entry['member_id'],
|
||||
'source_id' => $entry['source_id'],
|
||||
'reference_id' => $ledgerEntryId,
|
||||
'description' => $description,
|
||||
'occurred_at' => gmdate('Y-m-d H:i:s'),
|
||||
'created_by_user_id' => $actorUserId,
|
||||
'reversal_of_entry_id' => $ledgerEntryId,
|
||||
]);
|
||||
$reversalId = (int) $this->pdo->lastInsertId();
|
||||
|
||||
$lineInsert = $this->pdo->prepare(
|
||||
'INSERT INTO ledger_lines (
|
||||
ledger_entry_id,
|
||||
tenant_id,
|
||||
member_id,
|
||||
account_code,
|
||||
amount_cents
|
||||
) VALUES (
|
||||
:ledger_entry_id,
|
||||
:tenant_id,
|
||||
:member_id,
|
||||
:account_code,
|
||||
:amount_cents
|
||||
)'
|
||||
);
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$lineInsert->execute([
|
||||
'ledger_entry_id' => $reversalId,
|
||||
'tenant_id' => $tenantId,
|
||||
'member_id' => $line['member_id'] !== null ? (int) $line['member_id'] : null,
|
||||
'account_code' => $line['account_code'],
|
||||
'amount_cents' => 0 - (int) $line['amount_cents'],
|
||||
]);
|
||||
}
|
||||
|
||||
$this->pdo->commit();
|
||||
} catch (\Throwable $throwable) {
|
||||
$this->pdo->rollBack();
|
||||
throw $throwable;
|
||||
}
|
||||
|
||||
$this->audit->log($tenantId, $actorUserId, 'ledger_entry.reversed', 'ledger_entry', $reversalId, 'success', [
|
||||
'reversal_of_entry_id' => $ledgerEntryId,
|
||||
]);
|
||||
|
||||
return $reversalId;
|
||||
}
|
||||
|
||||
public function reversePaperSheet(int $tenantId, int $sheetId, int $actorUserId, ?string $reason = null): int
|
||||
{
|
||||
$this->assertTenantMembership($tenantId, $actorUserId);
|
||||
|
||||
$statement = $this->pdo->prepare(
|
||||
'SELECT DISTINCT le.id
|
||||
FROM ledger_entries le
|
||||
INNER JOIN consumption_events ce
|
||||
ON ce.id = le.reference_id
|
||||
AND le.reference_type = "consumption_event"
|
||||
INNER JOIN paper_sheet_lines psl
|
||||
ON psl.id = ce.paper_sheet_line_id
|
||||
WHERE le.tenant_id = :tenant_id
|
||||
AND psl.paper_sheet_id = :paper_sheet_id
|
||||
ORDER BY le.id ASC'
|
||||
);
|
||||
$statement->execute([
|
||||
'tenant_id' => $tenantId,
|
||||
'paper_sheet_id' => $sheetId,
|
||||
]);
|
||||
$entryIds = $statement->fetchAll(PDO::FETCH_COLUMN);
|
||||
|
||||
if ($entryIds === []) {
|
||||
throw new RuntimeException('Zu dieser Papierliste wurden keine verbuchten Eintraege gefunden.');
|
||||
}
|
||||
|
||||
$reversed = 0;
|
||||
|
||||
foreach ($entryIds as $entryId) {
|
||||
if ($this->hasReversal($tenantId, (int) $entryId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->reverseLedgerEntry(
|
||||
$tenantId,
|
||||
(int) $entryId,
|
||||
$actorUserId,
|
||||
$reason !== null && trim($reason) !== '' ? $reason : 'Papierliste #' . $sheetId
|
||||
);
|
||||
$reversed++;
|
||||
}
|
||||
|
||||
if ($reversed === 0) {
|
||||
throw new RuntimeException('Diese Papierliste wurde bereits vollstaendig storniert.');
|
||||
}
|
||||
|
||||
$this->audit->log($tenantId, $actorUserId, 'paper_sheet.reversed', 'paper_sheet', $sheetId, 'success', [
|
||||
'reversed_entries' => $reversed,
|
||||
]);
|
||||
|
||||
return $reversed;
|
||||
}
|
||||
|
||||
private function resolvePriceCents(int $tenantId, int $productId, string $effectiveAt): int
|
||||
{
|
||||
$statement = $this->pdo->prepare(
|
||||
@@ -475,4 +656,93 @@ final class LedgerService
|
||||
|
||||
return $source;
|
||||
}
|
||||
|
||||
private function assertTenantMember(int $tenantId, int $memberId): void
|
||||
{
|
||||
$statement = $this->pdo->prepare(
|
||||
'SELECT id FROM members WHERE id = :id AND tenant_id = :tenant_id AND status = "active" LIMIT 1'
|
||||
);
|
||||
$statement->execute([
|
||||
'id' => $memberId,
|
||||
'tenant_id' => $tenantId,
|
||||
]);
|
||||
|
||||
if (!$statement->fetchColumn()) {
|
||||
throw new RuntimeException('Das gewaehlte Mitglied gehoert nicht zu diesem Mandanten.');
|
||||
}
|
||||
}
|
||||
|
||||
private function assertTenantProduct(int $tenantId, int $productId): void
|
||||
{
|
||||
$statement = $this->pdo->prepare(
|
||||
'SELECT id FROM products WHERE id = :id AND tenant_id = :tenant_id AND is_active = 1 LIMIT 1'
|
||||
);
|
||||
$statement->execute([
|
||||
'id' => $productId,
|
||||
'tenant_id' => $tenantId,
|
||||
]);
|
||||
|
||||
if (!$statement->fetchColumn()) {
|
||||
throw new RuntimeException('Das gewaehlte Produkt gehoert nicht zu diesem Mandanten.');
|
||||
}
|
||||
}
|
||||
|
||||
private function assertTenantMembership(int $tenantId, int $userId): void
|
||||
{
|
||||
$statement = $this->pdo->prepare(
|
||||
'SELECT id
|
||||
FROM tenant_memberships
|
||||
WHERE tenant_id = :tenant_id
|
||||
AND user_id = :user_id
|
||||
AND status = "active"
|
||||
LIMIT 1'
|
||||
);
|
||||
$statement->execute([
|
||||
'tenant_id' => $tenantId,
|
||||
'user_id' => $userId,
|
||||
]);
|
||||
|
||||
if (!$statement->fetchColumn()) {
|
||||
throw new RuntimeException('Der Benutzer ist fuer diesen Mandanten nicht aktiv.');
|
||||
}
|
||||
}
|
||||
|
||||
private function ledgerEntry(int $tenantId, int $ledgerEntryId): array
|
||||
{
|
||||
$statement = $this->pdo->prepare(
|
||||
'SELECT *
|
||||
FROM ledger_entries
|
||||
WHERE id = :id
|
||||
AND tenant_id = :tenant_id
|
||||
LIMIT 1'
|
||||
);
|
||||
$statement->execute([
|
||||
'id' => $ledgerEntryId,
|
||||
'tenant_id' => $tenantId,
|
||||
]);
|
||||
$entry = $statement->fetch();
|
||||
|
||||
if (!$entry) {
|
||||
throw new RuntimeException('Der angeforderte Vorgang wurde nicht gefunden.');
|
||||
}
|
||||
|
||||
return $entry;
|
||||
}
|
||||
|
||||
private function hasReversal(int $tenantId, int $ledgerEntryId): bool
|
||||
{
|
||||
$statement = $this->pdo->prepare(
|
||||
'SELECT id
|
||||
FROM ledger_entries
|
||||
WHERE tenant_id = :tenant_id
|
||||
AND reversal_of_entry_id = :reversal_of_entry_id
|
||||
LIMIT 1'
|
||||
);
|
||||
$statement->execute([
|
||||
'tenant_id' => $tenantId,
|
||||
'reversal_of_entry_id' => $ledgerEntryId,
|
||||
]);
|
||||
|
||||
return (bool) $statement->fetchColumn();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user