From 005df48fed54e239a0035c8aaf8bb49b33dd21fb Mon Sep 17 00:00:00 2001 From: fds2610-bot Date: Thu, 13 Aug 2026 10:41:05 +0200 Subject: [PATCH] =?UTF-8?q?Stichtag=20f=C3=BCr=20den=20Umsatzabruf?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Der Abrufzeitraum kannte bisher nur Spannen. Der Stichtag steht jetzt an zwei Stellen: als Vorgabe der Bankverbindung (neuer Abrufzeitraum "Ab Stichtag" mit Datum in bank_zugaenge.abruf_stichtag) und als einmalige Angabe neben "Umsätze abrufen", die der Vorgabe vorgeht und nichts speichert. Der einmalige Stichtag liegt im Abrufzustand, nicht in der Datenbank: Nach einer TAN-Runde läuft der Abruf im nächsten Request weiter und muss dort denselben Zeitraum holen wie beim Start. "Ab Stichtag" ohne Datum und unlesbare Datumsangaben werden abgewiesen; ein Stichtag in der Zukunft wird auf heute gesetzt, statt den Abruf leer ausgehen zu lassen. Co-Authored-By: Claude Opus 5 --- Changes.md | 29 +++++++ README.md | 5 +- docs/banking.md | 39 +++++++++- migrations/011_abruf_stichtag.sql | 10 +++ public/static/app.css | 26 +++++++ src/Controllers/BankingController.php | 14 +++- src/Models/Bankzugang.php | 106 ++++++++++++++++++++++++-- src/Services/FinTsAbruf.php | 25 ++++-- src/Views/banking/formular.php | 18 ++++- src/Views/banking/index.php | 24 +++++- tests/16_BankingTest.php | 85 +++++++++++++++++++++ tests/e2e/specs/module.spec.js | 15 ++++ 12 files changed, 376 insertions(+), 20 deletions(-) create mode 100644 migrations/011_abruf_stichtag.sql diff --git a/Changes.md b/Changes.md index fab17ec..d881c0b 100644 --- a/Changes.md +++ b/Changes.md @@ -1,5 +1,34 @@ # Änderungen +## Stichtag für den Umsatzabruf (13.08.2026) + +Der Abrufzeitraum kannte bisher nur Spannen: seit dem letzten Abruf, 7, 30 oder 365 Tage. +Wer einen Bestand ab einem festen Datum aufbauen wollte – etwa ab dem Wechsel vom Lexware +FinanzManager –, hatte dafür keine Angabe. Der Stichtag steht jetzt an zwei Stellen. + +**Vorgabe der Bankverbindung** (`Models\Bankzugang`, `Views/banking/formular.php`, +`migrations/011_abruf_stichtag.sql`) +- Neuer Abrufzeitraum *Ab Stichtag* und das dazugehörige Datumsfeld im Zugangsformular + (neue Spalte `bank_zugaenge.abruf_stichtag`). Gewählt, wird jeder Abruf ab diesem Tag + geholt. +- *Ab Stichtag* ohne Datum wird abgewiesen – eine Vorgabe, die nichts vorgibt, führte beim + Abruf nur zu einem anderen Zeitraum als gedacht. Ein unlesbares Datum ebenso. +- Die Übersicht der Bankverbindungen zeigt den Stichtag neben dem Zeitraum. + +**Stichtag für einen einzelnen Abruf** (`Services\FinTsAbruf`, `BankingController`, +`Views/banking/index.php`) +- Neben *Umsätze abrufen* steht ein Datumsfeld. Ausgefüllt geht es dem eingestellten + Abrufzeitraum vor und gilt **nur für diesen Lauf**: Wer einmalig weiter zurückgreifen + will, muss die Einstellung des Zugangs nicht ändern und hinterher zurückstellen. +- Der Stichtag liegt im Abrufzustand, nicht in der Datenbank – nach einer TAN-Runde läuft + der Abruf im nächsten Request weiter und holt dort denselben Zeitraum wie beim Start. +- Ein Stichtag in der Zukunft wird auf heute gesetzt, statt den Abruf leer ausgehen zu + lassen; das Protokoll vermerkt es. + +Angenommen werden das Format des Datumsfelds (`2026-01-01`) und die deutsche Schreibweise +(`01.01.2026`); alles andere gilt als Fehleingabe. Wie weit die Bank zurückreicht, +entscheidet sie selbst – 90 Tage sind üblich, mehr liefert nicht jede. + ## Termine und Verträge wandern unter die Verwaltung (13.08.2026) Die Seitenleiste war lang geworden. *Termine* und *Verträge* sind kein täglicher Einstieg – diff --git a/README.md b/README.md index 6482112..9f006eb 100644 --- a/README.md +++ b/README.md @@ -24,8 +24,9 @@ Haushalt wird beides parallel genutzt. Monat, Ausgaben nach Kategorie, Kontostände, offene Vorgänge. * **Online-Banking** – FinTS-Abruf mit TAN-Dialog über mehrere Requests, Einrichtung in drei Schritten (Zugangsdaten, TAN-Verfahren, Kontenzuordnung mit Anlegen fehlender Konten), - abgerufene Umsätze buchen sich auf Wunsch selbst, Abgleich gegen vorhandene Buchungen, - Regeln für die automatische Kategorisierung. + Abrufzeitraum als Spanne oder Stichtag – auch für einen einzelnen Abruf –, abgerufene + Umsätze buchen sich auf Wunsch selbst, Abgleich gegen vorhandene Buchungen, Regeln für + die automatische Kategorisierung. * **Wertpapiere** – Depotbestände mit Einstand und Bewertung, Depotbuchungen, Kursabruf, Veräußerungsgeschäfte und Erträge; Bestandslisten der Bank lassen sich als CSV übernehmen. diff --git a/docs/banking.md b/docs/banking.md index adc75b8..f25ba31 100644 --- a/docs/banking.md +++ b/docs/banking.md @@ -50,6 +50,41 @@ Platz, und ein Request, der fünf Minuten auf eine TAN wartet, blockiert einen W Was in der Sitzung liegt, enthält keine PIN – die Bibliothek schließt sie beim Persistieren ausdrücklich aus. Sie wird bei jedem Request neu aus der Datenbank geholt und entschlüsselt. +### Wie weit zurück: Zeitraum und Stichtag + +Ab wann abgerufen wird, bestimmt `Bankzugang::abrufBeginn()`. Zwei Angaben spielen hinein: + +* Der **Abrufzeitraum** der Bankverbindung (`bank_zugaenge.abrufzeitraum`): seit dem letzten + Abruf, letzte 7/30/365 Tage – oder *Ab Stichtag* mit dem Datum aus + `bank_zugaenge.abruf_stichtag`. +* Ein **Stichtag für diesen einen Abruf**, angegeben im Datumsfeld neben *Umsätze abrufen*. + Er geht der Vorgabe vor und wird **nirgends gespeichert**. + +Beides nebeneinander, weil beides gebraucht wird: Die Vorgabe beantwortet den Alltag („immer +seit dem letzten Abruf“), der einmalige Stichtag den Sonderfall – einen Bestand ab einem +festen Datum aufbauen, eine Lücke nachholen. Ohne ihn müsste man die Einstellung des Zugangs +ändern und hinterher zurückstellen, mit dem üblichen Ausgang, dass man das Zurückstellen +vergisst. + +Der einmalige Stichtag liegt im **Abrufzustand**, nicht in der Datenbank: Nach einer +TAN-Runde läuft der Abruf im nächsten Request weiter und muss dort denselben Zeitraum holen +wie beim Start. + +„Seit dem letzten Abruf“ nimmt den **Tag** des letzten Laufs, nicht die Uhrzeit: Banken +buchen nachträglich, und ein Umsatz, der schon da war, fällt ohnehin über seinen +Fingerabdruck heraus. Beim allerersten Abruf gilt ein Jahr zurück – sonst käme entweder +nichts oder alles. + +Ein Stichtag **in der Zukunft** wird auf heute gesetzt statt abgewiesen: Aus der Zukunft +liefert keine Bank etwas, und ein verrutschtes Datum soll den Abruf nicht ergebnislos machen +– das Protokoll vermerkt die Korrektur. *Ab Stichtag* **ohne** Datum lässt sich dagegen gar +nicht erst speichern; eine Vorgabe, die nichts vorgibt, führte beim Abruf nur zu einem +anderen Zeitraum als gedacht. + +Wie weit die Bank tatsächlich zurückreicht, entscheidet sie selbst – 90 Tage sind üblich, +mehr liefert nicht jede. Ein früherer Stichtag holt deshalb nicht zwingend mehr; die Lücke +davor schließt der Anfangssaldo des Kontos oder der QIF-Import. + ### TAN-Verfahren * **Klassisch** (chipTAN, photoTAN): Die Maske zeigt den Hinweistext der Bank und, wenn @@ -244,7 +279,9 @@ der Bibliothek an das eigene Log hängt. PIN und TAN maskieren beide Seiten. `tests/16_BankingTest.php` prüft alles um den Dialog herum: Verschlüsselung (Kennung, kein Klartext, wechselnder Geheimtext, kein Verschachteln), PIN bleibt beim Speichern erhalten, -Abrufzeiträume, Dublettenerkennung, Regeln nach Priorität und Feld, Splitschema mit +Abrufzeiträume samt Stichtag (gespeicherte Vorgabe, einmalige Angabe geht ihr vor und lässt +die Vorgabe stehen, Zukunft wird auf heute gesetzt, unlesbare Datumsangaben werden +abgewiesen), Dublettenerkennung, Regeln nach Priorität und Feld, Splitschema mit Restausgleich, Abweisung ungültiger Ausdrücke, Abgleich gegen vorhandene Buchungen samt Toleranz sowie Übernehmen, Verwerfen und Zurücksetzen. diff --git a/migrations/011_abruf_stichtag.sql b/migrations/011_abruf_stichtag.sql new file mode 100644 index 0000000..056bc61 --- /dev/null +++ b/migrations/011_abruf_stichtag.sql @@ -0,0 +1,10 @@ +-- Stichtag für den Umsatzabruf. +-- +-- Bisher konnte der Abrufzeitraum nur relativ gewählt werden (letzter Abruf, +-- 7/30/365 Tage). Wer einen Bestand ab einem festen Datum aufbauen will – etwa +-- ab dem Wechsel vom Lexware FinanzManager –, braucht ein Datum statt einer +-- Spanne. Der Wert ist die Vorgabe des Zugangs; beim einzelnen Abruf lässt sich +-- ein abweichender Stichtag angeben, der nicht gespeichert wird. + +ALTER TABLE bank_zugaenge + ADD COLUMN IF NOT EXISTS abruf_stichtag DATE NULL AFTER abrufzeitraum; diff --git a/public/static/app.css b/public/static/app.css index c43a674..6782924 100644 --- a/public/static/app.css +++ b/public/static/app.css @@ -219,6 +219,32 @@ code { form.inline { display: inline; margin: 0; } +/* Der Abrufknopf mit dem Stichtagsfeld davor: Beide gehören zusammen und sollen + auf einer Zeile bleiben – auch in der rechtsbündigen Aktionsspalte. */ +.abrufform { + display: inline-flex; + align-items: center; + gap: .4rem; +} + +.stichtagwahl { + display: inline-flex; + align-items: center; + gap: .35rem; + font-size: .88rem; + color: var(--text-gedaempft); +} + +.stichtagfeld { + padding: .25rem .4rem; + border: 1px solid var(--rand); + border-radius: 4px; + background: var(--flaeche); + color: var(--text); + font: inherit; + font-size: .85rem; +} + .inhalt { padding: 1.25rem 1.5rem 3rem; flex: 1; diff --git a/src/Controllers/BankingController.php b/src/Controllers/BankingController.php index 1a82349..293adfb 100644 --- a/src/Controllers/BankingController.php +++ b/src/Controllers/BankingController.php @@ -363,7 +363,16 @@ class BankingController extends BaseController // ── Abruf ──────────────────────────────────────────────────────────────── - /** @param array $params */ + /** + * Umsätze abrufen. + * + * Das Stichtagsfeld neben dem Knopf gilt **nur für diesen Lauf**: Wer + * einmalig weiter zurückgreifen will, soll dafür nicht die Einstellung des + * Zugangs ändern und hinterher zurückstellen müssen. Bleibt es leer, gilt + * der Abrufzeitraum der Bankverbindung. + * + * @param array $params + */ public function abrufen(array $params): void { $this->verlangeAnmeldung(); @@ -373,7 +382,7 @@ class BankingController extends BaseController // als schon vorhanden, und sie blieben für immer unsichtbar. Bankumsatz::verwaisteOeffnen(); - $ergebnis = FinTsAbruf::starten((int)$params['id']); + $ergebnis = FinTsAbruf::starten((int)$params['id'], 'voll', false, $this->post('stichtag')); $this->zeigeAbruf($ergebnis, (int)$params['id']); } @@ -658,6 +667,7 @@ class BankingController extends BaseController 'benutzerkennung' => $this->post('benutzerkennung'), 'pin' => $this->post('pin'), 'abrufzeitraum' => $this->post('abrufzeitraum'), + 'abruf_stichtag' => $this->post('abruf_stichtag'), 'aktiv' => isset($_POST['aktiv']) ? 1 : 0, ]; } diff --git a/src/Models/Bankzugang.php b/src/Models/Bankzugang.php index 4229c1a..f12ee0c 100644 --- a/src/Models/Bankzugang.php +++ b/src/Models/Bankzugang.php @@ -24,6 +24,7 @@ class Bankzugang 'sieben_tage' => 'Letzte 7 Tage', 'dreissig_tage' => 'Letzte 30 Tage', 'jahr' => 'Letzte 365 Tage', + 'stichtag' => 'Ab Stichtag', ]; /** @return list> */ @@ -74,13 +75,14 @@ class Bankzugang Database::execute( 'INSERT INTO bank_zugaenge (benutzer_id, name, bankname, blz, fints_url, benutzerkennung, pin, - tan_verfahren_id, tan_verfahren_name, tan_medium, abrufzeitraum, aktiv) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', + tan_verfahren_id, tan_verfahren_name, tan_medium, abrufzeitraum, + abruf_stichtag, aktiv) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', [ Mandant::id(), $felder['name'], $felder['bankname'], $felder['blz'], $felder['fints_url'], $felder['benutzerkennung'], $felder['pin'], $felder['tan_verfahren_id'], $felder['tan_verfahren_name'], $felder['tan_medium'], - $felder['abrufzeitraum'], $felder['aktiv'], + $felder['abrufzeitraum'], $felder['abruf_stichtag'], $felder['aktiv'], ] ); @@ -117,13 +119,13 @@ class Bankzugang Database::execute( 'UPDATE bank_zugaenge SET name = ?, bankname = ?, blz = ?, fints_url = ?, benutzerkennung = ?, pin = ?, tan_verfahren_id = ?, tan_verfahren_name = ?, - tan_medium = ?, abrufzeitraum = ?, aktiv = ? + tan_medium = ?, abrufzeitraum = ?, abruf_stichtag = ?, aktiv = ? WHERE id = ? AND benutzer_id = ?', [ $felder['name'], $felder['bankname'], $felder['blz'], $felder['fints_url'], $felder['benutzerkennung'], $felder['pin'], $felder['tan_verfahren_id'], $felder['tan_verfahren_name'], $felder['tan_medium'], - $felder['abrufzeitraum'], $felder['aktiv'], + $felder['abrufzeitraum'], $felder['abruf_stichtag'], $felder['aktiv'], $id, Mandant::id(), ] ); @@ -162,6 +164,34 @@ class Bankzugang ); } + /** + * Ab wann für diesen Zugang abgerufen wird. + * + * Ein **beim Abruf angegebener Stichtag** geht der gespeicherten Vorgabe vor + * und wird nirgends festgehalten: Er gilt für diesen einen Lauf – etwa um + * einmalig weiter zurückzugreifen, ohne die Einstellung des Zugangs zu + * ändern. + * + * @param array $zugang + * @return array{0: \DateTimeImmutable, 1: string} Startdatum und Beschreibung + */ + public static function abrufBeginn( + array $zugang, + ?string $stichtag = null, + ?\DateTimeImmutable $heute = null + ): array { + if (trim((string)$stichtag) !== '') { + return self::startdatum('stichtag', null, $heute, $stichtag); + } + + return self::startdatum( + (string)($zugang['abrufzeitraum'] ?? ''), + ($zugang['zuletzt_am'] ?? null) === null ? null : (string)$zugang['zuletzt_am'], + $heute, + ($zugang['abruf_stichtag'] ?? null) === null ? null : (string)$zugang['abruf_stichtag'] + ); + } + /** * Ab wann abgerufen wird. „Seit dem letzten Abruf“ nimmt den Tag des letzten * Laufs – bewusst den ganzen Tag und nicht die Uhrzeit: Banken buchen @@ -173,7 +203,8 @@ class Bankzugang public static function startdatum( ?string $zeitraum, ?string $zuletztAm, - ?\DateTimeImmutable $heute = null + ?\DateTimeImmutable $heute = null, + ?string $stichtag = null ): array { $heute = $heute ?? new \DateTimeImmutable('today'); $zeitraum = array_key_exists((string)$zeitraum, self::ZEITRAEUME) ? (string)$zeitraum : 'letzter_abruf'; @@ -182,10 +213,59 @@ class Bankzugang 'sieben_tage' => [$heute->modify('-7 days'), 'letzte 7 Tage'], 'dreissig_tage' => [$heute->modify('-30 days'), 'letzte 30 Tage'], 'jahr' => [$heute->modify('-365 days'), 'letzte 365 Tage'], + // Ohne brauchbares Datum wäre „Ab Stichtag“ eine Angabe ohne Inhalt – + // dann gilt dieselbe Regel wie ohne Vorgabe. + 'stichtag' => self::abStichtag($heute, $stichtag) ?? self::seitLetztemAbruf($heute, $zuletztAm), default => self::seitLetztemAbruf($heute, $zuletztAm), }; } + /** + * Ein Stichtag als Datum – oder null, wenn sich keines daraus lesen lässt. + * + * Angenommen werden das Format des Datumsfelds (`2026-08-13`) und die + * deutsche Schreibweise (`13.08.2026`); alles andere gilt als Fehleingabe + * statt als „irgendein Tag“. + */ + public static function stichtagDatum(?string $roh): ?\DateTimeImmutable + { + $roh = trim((string)$roh); + if ($roh === '') { + return null; + } + + foreach (['Y-m-d', 'd.m.Y'] as $format) { + $datum = \DateTimeImmutable::createFromFormat('!' . $format, $roh); + $fehler = \DateTimeImmutable::getLastErrors(); + + // `createFromFormat` biegt Unsinn wie den 31.02. still zurecht – die + // Warnungen sind das Einzige, woran sich das erkennen lässt. + if ($datum !== false + && ($fehler === false || ($fehler['warning_count'] === 0 && $fehler['error_count'] === 0))) { + return $datum; + } + } + + return null; + } + + /** @return array{0: \DateTimeImmutable, 1: string}|null */ + private static function abStichtag(\DateTimeImmutable $heute, ?string $stichtag): ?array + { + $datum = self::stichtagDatum($stichtag); + if ($datum === null) { + return null; + } + + // Aus der Zukunft liefert keine Bank etwas; ein verrutschter Stichtag + // soll den Abruf nicht ergebnislos machen. + if ($datum > $heute) { + return [$heute, 'ab Stichtag ' . $heute->format('d.m.Y') . ' (in der Zukunft, auf heute gesetzt)']; + } + + return [$datum, 'ab Stichtag ' . $datum->format('d.m.Y')]; + } + /** @return array{0: \DateTimeImmutable, 1: string} */ private static function seitLetztemAbruf(\DateTimeImmutable $heute, ?string $zuletztAm): array { @@ -223,6 +303,17 @@ class Bankzugang $verfahren = ($verfahren === null || $verfahren === '') ? null : (int)$verfahren; $zeitraum = (string)($daten['abrufzeitraum'] ?? 'letzter_abruf'); + $zeitraum = array_key_exists($zeitraum, self::ZEITRAEUME) ? $zeitraum : 'letzter_abruf'; + + // Ein leeres Datumsfeld ist kein Fehler; „Ab Stichtag“ ohne Datum schon – + // sonst stünde eine Vorgabe da, die nichts vorgibt. + $stichtag = self::stichtagDatum((string)($daten['abruf_stichtag'] ?? '')); + if ($stichtag === null && trim((string)($daten['abruf_stichtag'] ?? '')) !== '') { + throw new \InvalidArgumentException('Der Stichtag ist kein gültiges Datum.'); + } + if ($stichtag === null && $zeitraum === 'stichtag') { + throw new \InvalidArgumentException('Für „Ab Stichtag“ fehlt das Datum.'); + } return [ 'name' => mb_substr($name, 0, 128), @@ -234,7 +325,8 @@ class Bankzugang 'tan_verfahren_id' => $verfahren, 'tan_verfahren_name' => mb_substr(trim((string)($daten['tan_verfahren_name'] ?? '')), 0, 128), 'tan_medium' => Crypto::verschluessle(trim((string)($daten['tan_medium'] ?? ''))), - 'abrufzeitraum' => array_key_exists($zeitraum, self::ZEITRAEUME) ? $zeitraum : 'letzter_abruf', + 'abrufzeitraum' => $zeitraum, + 'abruf_stichtag' => $stichtag?->format('Y-m-d'), 'aktiv' => !empty($daten['aktiv']) ? 1 : 0, ]; } diff --git a/src/Services/FinTsAbruf.php b/src/Services/FinTsAbruf.php index 1b140d5..1f5ad67 100644 --- a/src/Services/FinTsAbruf.php +++ b/src/Services/FinTsAbruf.php @@ -83,11 +83,20 @@ class FinTsAbruf * Einrichtungsschritt – dort sollen erst die Konten zugeordnet werden, bevor * Umsätze hereinkommen, die sonst ohne Konto dastünden. * + * `$stichtag` gilt nur für diesen Lauf und geht dem Abrufzeitraum des Zugangs + * vor. Er liegt im Zustand, nicht in der Datenbank: Nach einer TAN-Runde + * läuft der Abruf im nächsten Request weiter und muss dort denselben Zeitraum + * holen wie beim Start. + * * @return array{status: string, meldung: string, protokoll: list, * neu: int, uebersprungen: int, tan?: array} */ - public static function starten(int $zugangId, string $modus = 'voll', bool $einrichtung = false): array - { + public static function starten( + int $zugangId, + string $modus = 'voll', + bool $einrichtung = false, + ?string $stichtag = null + ): array { $zugang = Bankzugang::zugangsdaten($zugangId); if ($zugang === null) { return self::ergebnis('fehler', 'Den Bankzugang gibt es nicht.', []); @@ -100,12 +109,18 @@ class FinTsAbruf } } + $stichtag = trim((string)$stichtag); + if ($stichtag !== '' && Bankzugang::stichtagDatum($stichtag) === null) { + return self::ergebnis('fehler', 'Der Stichtag ist kein gültiges Datum.', []); + } + self::vergessen(); $abruf = new self($zugang, Bankabruf::beginnen($zugangId), [ 'schritt' => 'anmelden', 'modus' => $modus === 'konten' ? 'konten' : 'voll', 'einrichtung' => $einrichtung, + 'stichtag' => $stichtag, 'konto_nummer' => 0, 'neu' => 0, 'uebersprungen' => 0, @@ -253,9 +268,9 @@ class FinTsAbruf private function umsaetzeAbrufen(): array { $konten = $this->abzurufendeKonten(); - [$von, $beschreibung] = Bankzugang::startdatum( - (string)$this->zugang['abrufzeitraum'], - $this->zugang['zuletzt_am'] === null ? null : (string)$this->zugang['zuletzt_am'] + [$von, $beschreibung] = Bankzugang::abrufBeginn( + $this->zugang, + (string)($this->zustand['stichtag'] ?? '') ); if ($this->zustand['konto_nummer'] === 0) { diff --git a/src/Views/banking/formular.php b/src/Views/banking/formular.php index f610fc2..ac5aaaf 100644 --- a/src/Views/banking/formular.php +++ b/src/Views/banking/formular.php @@ -22,8 +22,15 @@ include __DIR__ . '/../layout/kopf.php';
Zurück zu den Bankverbindungen -
+ + +
@@ -72,6 +79,15 @@ include __DIR__ . '/../layout/kopf.php'; + + +

+ Gilt, wenn oben Ab Stichtag gewählt ist: Abgerufen wird dann jedes Mal ab + diesem Tag. Für einen einzelnen Abruf lässt sich oben neben Umsätze abrufen + ein abweichender Stichtag angeben, ohne diese Einstellung zu ändern. +

+