749 lines
24 KiB
PHP
749 lines
24 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use PDO;
|
|
use RuntimeException;
|
|
|
|
final class LedgerService
|
|
{
|
|
public function __construct(
|
|
private readonly PDO $pdo,
|
|
private readonly AuditService $audit
|
|
) {
|
|
}
|
|
|
|
public function createConsumption(
|
|
int $tenantId,
|
|
int $memberId,
|
|
int $productId,
|
|
string $sourceCode,
|
|
string $effectiveAt,
|
|
int $recordedByUserId,
|
|
float $quantity,
|
|
?string $notes = null,
|
|
?int $paperSheetLineId = null,
|
|
?string $sourceReference = null
|
|
): void {
|
|
if ($quantity <= 0) {
|
|
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);
|
|
$description = sprintf('Verbrauch %s x #%d', rtrim(rtrim(number_format($quantity, 2, '.', ''), '0'), '.'), $productId);
|
|
$recordedAt = gmdate('Y-m-d H:i:s');
|
|
|
|
$this->pdo->beginTransaction();
|
|
|
|
try {
|
|
$eventInsert = $this->pdo->prepare(
|
|
'INSERT INTO consumption_events (
|
|
tenant_id,
|
|
member_id,
|
|
product_id,
|
|
source_id,
|
|
paper_sheet_line_id,
|
|
quantity,
|
|
unit_price_cents,
|
|
total_cents,
|
|
effective_at,
|
|
recorded_at,
|
|
recorded_by_user_id,
|
|
source_reference,
|
|
notes
|
|
) VALUES (
|
|
:tenant_id,
|
|
:member_id,
|
|
:product_id,
|
|
:source_id,
|
|
:paper_sheet_line_id,
|
|
:quantity,
|
|
:unit_price_cents,
|
|
:total_cents,
|
|
:effective_at,
|
|
:recorded_at,
|
|
:recorded_by_user_id,
|
|
:source_reference,
|
|
:notes
|
|
)'
|
|
);
|
|
$eventInsert->execute([
|
|
'tenant_id' => $tenantId,
|
|
'member_id' => $memberId,
|
|
'product_id' => $productId,
|
|
'source_id' => $source['id'],
|
|
'paper_sheet_line_id' => $paperSheetLineId,
|
|
'quantity' => $quantity,
|
|
'unit_price_cents' => $priceCents,
|
|
'total_cents' => $totalCents,
|
|
'effective_at' => $effectiveAt,
|
|
'recorded_at' => $recordedAt,
|
|
'recorded_by_user_id' => $recordedByUserId,
|
|
'source_reference' => $sourceReference,
|
|
'notes' => $notes,
|
|
]);
|
|
$eventId = (int) $this->pdo->lastInsertId();
|
|
|
|
$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
|
|
) VALUES (
|
|
:tenant_id,
|
|
:member_id,
|
|
:source_id,
|
|
"consumption",
|
|
"consumption_event",
|
|
:reference_id,
|
|
:description,
|
|
:occurred_at,
|
|
:created_by_user_id
|
|
)'
|
|
);
|
|
$entryInsert->execute([
|
|
'tenant_id' => $tenantId,
|
|
'member_id' => $memberId,
|
|
'source_id' => $source['id'],
|
|
'reference_id' => $eventId,
|
|
'description' => $description,
|
|
'occurred_at' => $effectiveAt,
|
|
'created_by_user_id' => $recordedByUserId,
|
|
]);
|
|
$entryId = (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
|
|
)'
|
|
);
|
|
|
|
$lineInsert->execute([
|
|
'ledger_entry_id' => $entryId,
|
|
'tenant_id' => $tenantId,
|
|
'member_id' => $memberId,
|
|
'account_code' => 'member_balance',
|
|
'amount_cents' => $totalCents,
|
|
]);
|
|
|
|
$lineInsert->execute([
|
|
'ledger_entry_id' => $entryId,
|
|
'tenant_id' => $tenantId,
|
|
'member_id' => null,
|
|
'account_code' => 'coffee_revenue',
|
|
'amount_cents' => -$totalCents,
|
|
]);
|
|
|
|
$this->pdo->commit();
|
|
} catch (\Throwable $throwable) {
|
|
$this->pdo->rollBack();
|
|
throw $throwable;
|
|
}
|
|
|
|
$this->audit->log($tenantId, $recordedByUserId, 'consumption.created', 'consumption_event', $eventId, 'success', [
|
|
'member_id' => $memberId,
|
|
'product_id' => $productId,
|
|
'quantity' => $quantity,
|
|
'source' => $sourceCode,
|
|
'total_cents' => $totalCents,
|
|
]);
|
|
}
|
|
|
|
public function createPayment(
|
|
int $tenantId,
|
|
int $memberId,
|
|
int $amountCents,
|
|
string $occurredAt,
|
|
int $recordedByUserId,
|
|
?string $notes = null
|
|
): void {
|
|
if ($amountCents <= 0) {
|
|
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(
|
|
'INSERT INTO ledger_entries (
|
|
tenant_id,
|
|
member_id,
|
|
source_id,
|
|
type,
|
|
description,
|
|
occurred_at,
|
|
created_by_user_id,
|
|
reference_type
|
|
) VALUES (
|
|
:tenant_id,
|
|
:member_id,
|
|
:source_id,
|
|
"payment",
|
|
:description,
|
|
:occurred_at,
|
|
:created_by_user_id,
|
|
"payment"
|
|
)'
|
|
);
|
|
|
|
$this->pdo->beginTransaction();
|
|
|
|
try {
|
|
$entryInsert->execute([
|
|
'tenant_id' => $tenantId,
|
|
'member_id' => $memberId,
|
|
'source_id' => $source['id'],
|
|
'description' => $notes !== null && trim($notes) !== '' ? $notes : 'Einzahlung',
|
|
'occurred_at' => $occurredAt,
|
|
'created_by_user_id' => $recordedByUserId,
|
|
]);
|
|
|
|
$entryId = (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
|
|
)'
|
|
);
|
|
|
|
$lineInsert->execute([
|
|
'ledger_entry_id' => $entryId,
|
|
'tenant_id' => $tenantId,
|
|
'member_id' => null,
|
|
'account_code' => 'cashbox',
|
|
'amount_cents' => $amountCents,
|
|
]);
|
|
|
|
$lineInsert->execute([
|
|
'ledger_entry_id' => $entryId,
|
|
'tenant_id' => $tenantId,
|
|
'member_id' => $memberId,
|
|
'account_code' => 'member_balance',
|
|
'amount_cents' => -$amountCents,
|
|
]);
|
|
|
|
$this->pdo->commit();
|
|
} catch (\Throwable $throwable) {
|
|
$this->pdo->rollBack();
|
|
throw $throwable;
|
|
}
|
|
|
|
$this->audit->log($tenantId, $recordedByUserId, 'payment.created', 'ledger_entry', $entryId, 'success', [
|
|
'member_id' => $memberId,
|
|
'amount_cents' => $amountCents,
|
|
]);
|
|
}
|
|
|
|
public function createPaperSheet(
|
|
int $tenantId,
|
|
string $title,
|
|
?string $periodLabel,
|
|
?string $periodStart,
|
|
?string $periodEnd,
|
|
?string $notes,
|
|
int $createdByUserId
|
|
): int {
|
|
$this->assertTenantMembership($tenantId, $createdByUserId);
|
|
|
|
if (trim($title) === '') {
|
|
throw new RuntimeException('Bitte einen Titel für die Liste angeben.');
|
|
}
|
|
|
|
$statement = $this->pdo->prepare(
|
|
'INSERT INTO paper_sheets (
|
|
tenant_id,
|
|
title,
|
|
period_label,
|
|
period_start,
|
|
period_end,
|
|
notes,
|
|
created_by_user_id
|
|
) VALUES (
|
|
:tenant_id,
|
|
:title,
|
|
:period_label,
|
|
:period_start,
|
|
:period_end,
|
|
:notes,
|
|
:created_by_user_id
|
|
)'
|
|
);
|
|
$statement->execute([
|
|
'tenant_id' => $tenantId,
|
|
'title' => $title,
|
|
'period_label' => $periodLabel,
|
|
'period_start' => $periodStart ?: null,
|
|
'period_end' => $periodEnd ?: null,
|
|
'notes' => $notes,
|
|
'created_by_user_id' => $createdByUserId,
|
|
]);
|
|
|
|
$sheetId = (int) $this->pdo->lastInsertId();
|
|
|
|
$this->audit->log($tenantId, $createdByUserId, 'paper_sheet.created', 'paper_sheet', $sheetId, 'success');
|
|
|
|
return $sheetId;
|
|
}
|
|
|
|
public function addPaperSheetLine(
|
|
int $tenantId,
|
|
int $sheetId,
|
|
int $memberId,
|
|
int $productId,
|
|
float $quantity,
|
|
?string $effectiveAt,
|
|
?string $note,
|
|
int $actorUserId
|
|
): void {
|
|
if ($quantity <= 0) {
|
|
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'
|
|
);
|
|
$sheet->execute([
|
|
'id' => $sheetId,
|
|
'tenant_id' => $tenantId,
|
|
]);
|
|
$paperSheet = $sheet->fetch();
|
|
|
|
if (!$paperSheet) {
|
|
throw new RuntimeException('Die Liste wurde nicht gefunden.');
|
|
}
|
|
|
|
if ($paperSheet['status'] !== 'draft') {
|
|
throw new RuntimeException('Nur Entwürfe können erweitert werden.');
|
|
}
|
|
|
|
$statement = $this->pdo->prepare(
|
|
'INSERT INTO paper_sheet_lines (
|
|
paper_sheet_id,
|
|
tenant_id,
|
|
member_id,
|
|
product_id,
|
|
quantity,
|
|
effective_at,
|
|
note
|
|
) VALUES (
|
|
:paper_sheet_id,
|
|
:tenant_id,
|
|
:member_id,
|
|
:product_id,
|
|
:quantity,
|
|
:effective_at,
|
|
:note
|
|
)'
|
|
);
|
|
$statement->execute([
|
|
'paper_sheet_id' => $sheetId,
|
|
'tenant_id' => $tenantId,
|
|
'member_id' => $memberId,
|
|
'product_id' => $productId,
|
|
'quantity' => $quantity,
|
|
'effective_at' => $effectiveAt ?: null,
|
|
'note' => $note,
|
|
]);
|
|
|
|
$lineId = (int) $this->pdo->lastInsertId();
|
|
|
|
$this->audit->log($tenantId, $actorUserId, 'paper_sheet.line_added', 'paper_sheet_line', $lineId, 'success', [
|
|
'paper_sheet_id' => $sheetId,
|
|
]);
|
|
}
|
|
|
|
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'
|
|
);
|
|
$sheetStatement->execute([
|
|
'id' => $sheetId,
|
|
'tenant_id' => $tenantId,
|
|
]);
|
|
$sheet = $sheetStatement->fetch();
|
|
|
|
if (!$sheet) {
|
|
throw new RuntimeException('Die Liste wurde nicht gefunden.');
|
|
}
|
|
|
|
if ($sheet['status'] !== 'draft') {
|
|
throw new RuntimeException('Die Liste wurde bereits verbucht.');
|
|
}
|
|
|
|
$linesStatement = $this->pdo->prepare(
|
|
'SELECT * FROM paper_sheet_lines WHERE paper_sheet_id = :paper_sheet_id AND tenant_id = :tenant_id ORDER BY id ASC'
|
|
);
|
|
$linesStatement->execute([
|
|
'paper_sheet_id' => $sheetId,
|
|
'tenant_id' => $tenantId,
|
|
]);
|
|
$lines = $linesStatement->fetchAll();
|
|
|
|
if ($lines === []) {
|
|
throw new RuntimeException('Die Liste enthält noch keine Positionen.');
|
|
}
|
|
|
|
foreach ($lines as $line) {
|
|
$this->createConsumption(
|
|
$tenantId,
|
|
(int) $line['member_id'],
|
|
(int) $line['product_id'],
|
|
'paper_sheet',
|
|
$line['effective_at'] ?: gmdate('Y-m-d H:i:s'),
|
|
$actorUserId,
|
|
(float) $line['quantity'],
|
|
$line['note'] ?: 'Liste',
|
|
(int) $line['id'],
|
|
'sheet:' . $sheetId
|
|
);
|
|
}
|
|
|
|
$update = $this->pdo->prepare(
|
|
'UPDATE paper_sheets SET status = "posted", posted_at = :posted_at WHERE id = :id AND tenant_id = :tenant_id'
|
|
);
|
|
$update->execute([
|
|
'posted_at' => gmdate('Y-m-d H:i:s'),
|
|
'id' => $sheetId,
|
|
'tenant_id' => $tenantId,
|
|
]);
|
|
|
|
$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 Buchungszeilen für 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 Liste wurden keine verbuchten Einträge 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 : 'Liste #' . $sheetId
|
|
);
|
|
$reversed++;
|
|
}
|
|
|
|
if ($reversed === 0) {
|
|
throw new RuntimeException('Diese Liste wurde bereits vollständig 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(
|
|
'SELECT price_cents
|
|
FROM product_prices
|
|
WHERE tenant_id = :tenant_id
|
|
AND product_id = :product_id
|
|
AND valid_from <= :effective_at
|
|
AND (valid_until IS NULL OR valid_until > :effective_at)
|
|
ORDER BY valid_from DESC
|
|
LIMIT 1'
|
|
);
|
|
$statement->execute([
|
|
'tenant_id' => $tenantId,
|
|
'product_id' => $productId,
|
|
'effective_at' => $effectiveAt,
|
|
]);
|
|
$price = $statement->fetchColumn();
|
|
|
|
if ($price === false) {
|
|
throw new RuntimeException('Für das Produkt ist kein aktiver Preis hinterlegt.');
|
|
}
|
|
|
|
return (int) $price;
|
|
}
|
|
|
|
private function findSource(int $tenantId, string $code): array
|
|
{
|
|
$statement = $this->pdo->prepare(
|
|
'SELECT id, code FROM capture_sources WHERE tenant_id = :tenant_id AND code = :code LIMIT 1'
|
|
);
|
|
$statement->execute([
|
|
'tenant_id' => $tenantId,
|
|
'code' => $code,
|
|
]);
|
|
$source = $statement->fetch();
|
|
|
|
if (!$source) {
|
|
throw new RuntimeException('Die Erfassungsquelle wurde nicht gefunden.');
|
|
}
|
|
|
|
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 gewählte Mitglied gehört nicht zu diesem Standort.');
|
|
}
|
|
}
|
|
|
|
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 gewählte Produkt gehört nicht zu dieser Kaffeeliste.');
|
|
}
|
|
}
|
|
|
|
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 für diesen Standort 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();
|
|
}
|
|
}
|