Backend-Arbeitsstand: ERP-Sync, Lieferlebenszyklus, Reports + config.toml
Bringt das Backend vom initialen Skeleton auf den aktuellen Arbeitsstand (Clean Architecture: domain → application → infrastructure → api). Wesentliche Bereiche: - ERP-Anbindung (MSSQL-Pull der Touren, Import-Scheduler, Rückschreiben) - Lieferlebenszyklus: Scan/Hold/Cancel/Complete, Gutschriften, Notizen, Bild-Anhänge, Unterschriften, PDF-Lieferreport → DOCUframe - Stammdaten: Kunden, Artikel, Lager, Zahlungsarten, Services - Keycloak-JWT-Gate + Fahrer-Provisionierung via Admin-API - Admin-API-Key-Gate (X-Admin-Api-Key) für Maschinen-Endpunkte Jüngste Änderungen dieser Session: - Belegspezifische Kontaktdaten: alle ERP-Adressen (Beleg-/Liefer-/ Rechnungsadresse, Ansprechpartner, Kundenstamm) mit Telefon/Mobil/ E-Mail werden gesynct (Migration 0029, MSSQL-Query, TourDetails) - Konfiguration von .env (envy/dotenvy) auf config.toml (toml/serde) umgestellt; Vorlage config.example.toml, Pfad via HOLZLEITNER_CONFIG Nicht im Repo (per .gitignore): config.toml (Secrets), data/ (Laufzeit-/ Kundendaten), demo.mp4, .claude/, variocontrol-ai/. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
17
migrations/0007_scan_audit_unremove.sql
Normal file
17
migrations/0007_scan_audit_unremove.sql
Normal file
@ -0,0 +1,17 @@
|
||||
-- 0007_scan_audit_unremove.sql
|
||||
--
|
||||
-- Erweitert den CHECK-Constraint auf scan_audit.action um den Wert
|
||||
-- 'unremove'. Hintergrund: Phase C+D-4 erlaubt das Wiederherstellen
|
||||
-- entfernter Items — jedes Apply landet als eigene Audit-Zeile, und
|
||||
-- ohne diesen Constraint-Update bricht die Insert-Query mit 500.
|
||||
--
|
||||
-- Reversibel: ältere Server-Versionen, die 'unremove' nicht kennen,
|
||||
-- können nach diesem Constraint-Update weiterhin die anderen Actions
|
||||
-- normal schreiben — die Erweiterung ist additiv.
|
||||
|
||||
ALTER TABLE scan_audit
|
||||
DROP CONSTRAINT IF EXISTS scan_audit_action_check;
|
||||
|
||||
ALTER TABLE scan_audit
|
||||
ADD CONSTRAINT scan_audit_action_check
|
||||
CHECK (action IN ('scan','unscan','hold','unhold','remove','unremove'));
|
||||
57
migrations/0008_payment_methods.sql
Normal file
57
migrations/0008_payment_methods.sql
Normal file
@ -0,0 +1,57 @@
|
||||
-- 0008_payment_methods.sql
|
||||
--
|
||||
-- Zahlungs-Stammdaten + Zahlungs-Felder an Lieferungen.
|
||||
--
|
||||
-- Designentscheidung: eigene Tabelle statt Enum, damit Methoden über
|
||||
-- einen API-Endpoint zur Laufzeit angelegt/deaktiviert werden können
|
||||
-- (z. B. neue Anbieter wie PayPal oder Klarna). FK auf `deliveries`
|
||||
-- mit `ON DELETE RESTRICT` — historische Lieferungen sollen ihren
|
||||
-- Methoden-Bezug niemals verlieren. Soft-Delete ist als
|
||||
-- `active`-Flag möglich; Hard-Delete bleibt für nie-genutzte Methoden.
|
||||
|
||||
CREATE TABLE payment_methods (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
-- `code` ist der stabile Programm-Identifier (z. B. „cash"). Wird vom
|
||||
-- Aufrufer vergeben, muss eindeutig sein. App und Berichts-Code
|
||||
-- können darüber spezielle Methoden referenzieren, ohne die UUID
|
||||
-- kennen zu müssen.
|
||||
code TEXT NOT NULL UNIQUE,
|
||||
-- Display-Name in der UI — frei änderbar via PATCH, z. B.
|
||||
-- „EC-Karte" → „Girocard".
|
||||
name TEXT NOT NULL,
|
||||
-- Soft-Delete: deaktivierte Methoden bleiben im DB-Stamm und
|
||||
-- bleiben damit für historische Lieferungen referenzierbar.
|
||||
-- Bei neuen Lieferungen filtert die App standardmäßig auf
|
||||
-- `active = true`.
|
||||
active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Initialer Stamm: die vier vom CEO definierten Methoden mit
|
||||
-- deterministischen UUIDs, damit Demo-Seeds und Tests stabile Keys
|
||||
-- haben.
|
||||
INSERT INTO payment_methods (id, code, name) VALUES
|
||||
('99999999-9999-9999-9999-999999999901', 'cash', 'Bar'),
|
||||
('99999999-9999-9999-9999-999999999902', 'ec_card', 'EC-Karte'),
|
||||
('99999999-9999-9999-9999-999999999903', 'credit_card', 'Kreditkarte'),
|
||||
('99999999-9999-9999-9999-999999999904', 'invoice', 'Auf Rechnung (14 Tage netto)');
|
||||
|
||||
-- Lieferungen: Vorauszahlung + gewählte Zahlungsmethode für den Rest.
|
||||
-- `prepaid_amount` ist `DOUBLE PRECISION` (≈ f64), kein NUMERIC —
|
||||
-- für 2 Nachkommastellen reicht das locker und die sqlx-Default-
|
||||
-- Features unterstützen es nativ ohne extra Crates.
|
||||
ALTER TABLE deliveries
|
||||
ADD COLUMN prepaid_amount DOUBLE PRECISION NOT NULL DEFAULT 0
|
||||
CHECK (prepaid_amount >= 0),
|
||||
-- Default beim Migrieren auf „cash" — die häufigste Methode. Die
|
||||
-- Default-Klausel räumen wir gleich wieder ab, damit zukünftige
|
||||
-- INSERTs den FK explizit setzen müssen.
|
||||
ADD COLUMN payment_method_id UUID NOT NULL
|
||||
DEFAULT '99999999-9999-9999-9999-999999999901'
|
||||
REFERENCES payment_methods(id) ON DELETE RESTRICT;
|
||||
|
||||
ALTER TABLE deliveries ALTER COLUMN payment_method_id DROP DEFAULT;
|
||||
|
||||
-- Lookup-Index: Reports oder Filter „alle Lieferungen mit Rechnung"
|
||||
-- profitieren davon. Ohne Index wäre das ein voller Tabelle-Scan.
|
||||
CREATE INDEX deliveries_payment_method ON deliveries(payment_method_id);
|
||||
16
migrations/0009_warehouse_filiale_rename.sql
Normal file
16
migrations/0009_warehouse_filiale_rename.sql
Normal file
@ -0,0 +1,16 @@
|
||||
-- 0009_warehouse_filiale_rename.sql
|
||||
--
|
||||
-- Wording-Angleichung: Die App spricht durchgängig von „Filiale" statt
|
||||
-- „Außenlager" (Nicht-Standard-Lager sind faktisch andere Filialen, aus
|
||||
-- denen sperrige Geräte separat geholt werden). Der Stammdaten-Name wird
|
||||
-- entsprechend angepasst.
|
||||
--
|
||||
-- Bewusst als eigene Migration statt Edit an 0002: 0002 ist bereits
|
||||
-- angewendet, eine nachträgliche Änderung würde die sqlx-Checksum brechen.
|
||||
-- Dieses idempotente UPDATE wirkt sowohl auf bestehende DBs als auch auf
|
||||
-- frische Setups (dort seedet 0002 zuerst „Außenlager A", danach benennt
|
||||
-- diese Migration um).
|
||||
UPDATE warehouses
|
||||
SET name = 'Filiale Freilassing'
|
||||
WHERE code = 'A'
|
||||
AND name = 'Außenlager A';
|
||||
18
migrations/0010_app_state.sql
Normal file
18
migrations/0010_app_state.sql
Normal file
@ -0,0 +1,18 @@
|
||||
-- 0010_app_state.sql
|
||||
--
|
||||
-- Generischer Key-Value-Store für kleine, langlebige Backend-Zustände, die
|
||||
-- Prozess-Neustarts überleben müssen, aber kein eigenes Schema rechtfertigen.
|
||||
--
|
||||
-- Erster Nutzer: die GSD/DOCUframe-Session-Id. Der GSD-Server „blockt" einen
|
||||
-- Lizenz-Seat pro Session, bis die Session abläuft oder via
|
||||
-- `/v1/license/release` freigegeben wird. Ginge die Session-Id bei einem
|
||||
-- Backend-Neustart verloren (reiner In-Memory-Cache), bliebe der Seat
|
||||
-- verwaist geblockt — bei wiederholten Neustarts droht ein Lizenz-Lockout.
|
||||
-- Deshalb persistieren wir die Id hier durabel (überlebt Restart UND
|
||||
-- Redeploy, da im DB-Volume) und können sie wiederverwenden bzw. gezielt
|
||||
-- freigeben.
|
||||
CREATE TABLE app_state (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
34
migrations/0011_attachments.sql
Normal file
34
migrations/0011_attachments.sql
Normal file
@ -0,0 +1,34 @@
|
||||
-- 0011_attachments.sql
|
||||
--
|
||||
-- Metadaten-Registry für hochgeladene Dateien (aktuell Bild-Notizen).
|
||||
--
|
||||
-- Die eigentlichen Bytes liegen in DOCUframe; hier halten wir die
|
||||
-- Metadaten und die DOCUframe-Referenz (`docuframe_object_id` = ~ObjectID).
|
||||
-- Die App referenziert ausschließlich `attachments.id` (unsere UUID) —
|
||||
-- DOCUframe bleibt damit ein austauschbares Implementierungsdetail
|
||||
-- (z. B. später Object-Storage statt DOCUframe).
|
||||
--
|
||||
-- Verknüpfung zur Notiz: `delivery_notes.image_attachment` enthält die
|
||||
-- `attachments.id` (als Text). Über `delivery_id` hängt der Anhang
|
||||
-- zusätzlich direkt an der Lieferung (CASCADE-Cleanup).
|
||||
CREATE TABLE attachments (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
-- DOCUframe ~ObjectID — für den späteren Download der Bytes.
|
||||
docuframe_object_id TEXT NOT NULL,
|
||||
mime_type TEXT NOT NULL,
|
||||
size_bytes BIGINT NOT NULL CHECK (size_bytes >= 0),
|
||||
-- Originaldateiname beim Upload (optional).
|
||||
filename TEXT,
|
||||
-- SHA-256 der Bytes (Hex) — Integrität / Dedup.
|
||||
checksum_sha256 TEXT NOT NULL,
|
||||
-- Bildabmessungen in Pixeln (nullable: nicht jede Datei ist ein Bild
|
||||
-- bzw. das Format ist evtl. nicht erkennbar).
|
||||
width INT,
|
||||
height INT,
|
||||
-- Personalnummer des Hochladenden (aus dem JWT).
|
||||
uploaded_by BIGINT NOT NULL,
|
||||
uploaded_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
delivery_id UUID NOT NULL REFERENCES deliveries(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX attachments_delivery ON attachments(delivery_id);
|
||||
35
migrations/0012_credit_quantity.sql
Normal file
35
migrations/0012_credit_quantity.sql
Normal file
@ -0,0 +1,35 @@
|
||||
-- 0012_credit_quantity.sql
|
||||
--
|
||||
-- Mengen-Gutschrift ("Belegzeile ganz oder teilweise entfernen").
|
||||
--
|
||||
-- Bis hierher war "entfernen" binär: scan_status kippte auf 'removed' und
|
||||
-- die ganze Position galt als zurückgegeben. Für Teilmengen
|
||||
-- ("Kunde nimmt 1 von 3 Stück nicht an") braucht es eine eigene
|
||||
-- Mengen-Dimension, getrennt von scanned_quantity (= verladen):
|
||||
--
|
||||
-- required_quantity = ERP-Soll (unverändert)
|
||||
-- scanned_quantity = verladen / Beladen (unverändert)
|
||||
-- credited_quantity = Gutschrift / zurück (NEU)
|
||||
-- ausgeliefert = required - credited (abgeleitet)
|
||||
--
|
||||
-- scan_status = 'removed' bedeutet künftig "voll gutgeschrieben"
|
||||
-- (credited == required). Teil-Gutschrift = 0 < credited < required;
|
||||
-- der Status bleibt dann 'done' (bzw. was er vorher war).
|
||||
|
||||
ALTER TABLE delivery_items
|
||||
ADD COLUMN credited_quantity INT NOT NULL DEFAULT 0
|
||||
CHECK (credited_quantity >= 0 AND credited_quantity <= required_quantity);
|
||||
|
||||
-- Audit der Gutschrift läuft über dieselbe append-only scan_audit-Tabelle
|
||||
-- wie Scan/Hold. Die bestehenden delta/resulting_quantity beschreiben die
|
||||
-- SCAN-Dimension; für remove/unremove kommen zwei eigene, nur dort gefüllte
|
||||
-- Spalten dazu, damit die beiden Dimensionen sauber getrennt bleiben.
|
||||
--
|
||||
-- credit_delta = +n bei remove, -n bei unremove
|
||||
-- resulting_credited_quantity = Snapshot von credited_quantity danach
|
||||
--
|
||||
-- Beide nullable: bei scan/unscan/hold/unhold/etc. bleiben sie NULL.
|
||||
ALTER TABLE scan_audit
|
||||
ADD COLUMN credit_delta INT,
|
||||
ADD COLUMN resulting_credited_quantity INT
|
||||
CHECK (resulting_credited_quantity IS NULL OR resulting_credited_quantity >= 0);
|
||||
15
migrations/0013_note_credit_link.sql
Normal file
15
migrations/0013_note_credit_link.sql
Normal file
@ -0,0 +1,15 @@
|
||||
-- 0013_note_credit_link.sql
|
||||
--
|
||||
-- Verknüpft eine Notiz mit der Belegzeile, für die sie als Gutschrift-Grund
|
||||
-- angelegt wurde. Damit lässt sich die Notiz beim Zurücknehmen der Gutschrift
|
||||
-- (Unremove) gezielt wieder löschen.
|
||||
--
|
||||
-- Nullable: normale Notizen (Text/Foto ohne Gutschrift-Bezug) haben hier NULL.
|
||||
-- ON DELETE SET NULL: verschwindet die Belegzeile mal (z. B. ERP-Resync), bleibt
|
||||
-- die Notiz als reine Doku erhalten und wird nur „entkoppelt".
|
||||
ALTER TABLE delivery_notes
|
||||
ADD COLUMN credit_delivery_item_id UUID
|
||||
REFERENCES delivery_items(id) ON DELETE SET NULL;
|
||||
|
||||
CREATE INDEX delivery_notes_credit_item
|
||||
ON delivery_notes (credit_delivery_item_id);
|
||||
43
migrations/0014_delivery_credit_audit.sql
Normal file
43
migrations/0014_delivery_credit_audit.sql
Normal file
@ -0,0 +1,43 @@
|
||||
-- 0014_delivery_credit_audit.sql
|
||||
--
|
||||
-- Betrags-Gutschrift pro Lieferung (Geld-Nachlass, unabhängig von Stückzahl;
|
||||
-- ≤150 €, in 10-€-Schritten, Pflichtgrund).
|
||||
--
|
||||
-- Append-only Audit-Log — analog scan_audit: jede Änderung (set/remove) ist
|
||||
-- eine eigene Zeile, nichts wird geupdated/gelöscht. Der "aktuelle" Stand
|
||||
-- einer Lieferung ist das jüngste Ereignis (DISTINCT ON delivery_id …
|
||||
-- ORDER BY recorded_at DESC): action='set' → Betrag+Grund, action='remove'
|
||||
-- → keine Gutschrift.
|
||||
--
|
||||
-- Idempotenz: client_event_id ist UNIQUE. Ein Netz-Retry desselben Events
|
||||
-- kollidiert auf dem Index (INSERT … ON CONFLICT DO NOTHING) — der Server
|
||||
-- liefert den aktuellen Stand zurück, ohne eine Dublette anzuhängen.
|
||||
|
||||
CREATE TABLE delivery_credit_audit (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
|
||||
-- Idempotenz-Schlüssel vom Client.
|
||||
client_event_id UUID NOT NULL UNIQUE,
|
||||
|
||||
delivery_id UUID NOT NULL REFERENCES deliveries(id) ON DELETE CASCADE,
|
||||
|
||||
action TEXT NOT NULL CHECK (action IN ('set', 'remove')),
|
||||
|
||||
-- Resultierender Betrag NACH diesem Ereignis (in Cent). 0 bei 'remove'.
|
||||
-- Harte Obergrenze 150 € als CHECK; die 10-€-Schritt-Regel prüft der
|
||||
-- Use Case (fachlich, leichter änderbar als ein DB-CHECK).
|
||||
amount_cents BIGINT NOT NULL DEFAULT 0
|
||||
CHECK (amount_cents >= 0 AND amount_cents <= 15000),
|
||||
|
||||
-- Pflicht bei 'set' (im Use Case erzwungen), bei 'remove' optional.
|
||||
reason TEXT,
|
||||
|
||||
-- Akteur: Personalnummer aus dem JWT (Pflicht), Fahrzeug optional.
|
||||
author_personalnummer BIGINT NOT NULL,
|
||||
author_car_id UUID,
|
||||
|
||||
recorded_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX delivery_credit_audit_delivery
|
||||
ON delivery_credit_audit (delivery_id, recorded_at DESC);
|
||||
11
migrations/0015_note_amount_credit_flag.sql
Normal file
11
migrations/0015_note_amount_credit_flag.sql
Normal file
@ -0,0 +1,11 @@
|
||||
-- 0015_note_amount_credit_flag.sql
|
||||
--
|
||||
-- Markiert eine Notiz als Grund-Notiz einer Betrags-Gutschrift (Geld-Nachlass
|
||||
-- auf Lieferungs-Ebene). Anders als die Mengen-Gutschrift hängt der Nachlass
|
||||
-- nicht an einer Belegzeile, daher kein credit_delivery_item_id, sondern ein
|
||||
-- einfaches Flag.
|
||||
--
|
||||
-- Damit kann der Client beim Entfernen der Gutschrift die zugehörige(n)
|
||||
-- Grund-Notiz(en) der Lieferung gezielt wieder löschen.
|
||||
ALTER TABLE delivery_notes
|
||||
ADD COLUMN is_amount_credit_note BOOLEAN NOT NULL DEFAULT false;
|
||||
65
migrations/0016_services.sql
Normal file
65
migrations/0016_services.sql
Normal file
@ -0,0 +1,65 @@
|
||||
-- 0016_services.sql
|
||||
--
|
||||
-- Services (früher „Lieferoptionen") — admin-konfigurierbare Stammdaten plus
|
||||
-- Pro-Lieferung-Werte.
|
||||
--
|
||||
-- Im alten ERPframe-Stand kamen die Optionen datengetrieben pro Lieferung
|
||||
-- (key/display/numerical/value). Neu: der Administrator pflegt die Definitionen
|
||||
-- frei (anlegen/ändern/löschen, Muster wie payment_methods), der Fahrer wählt
|
||||
-- sie in Phase 4 pro Lieferung aus (Checkbox bzw. Zahl mit min/max).
|
||||
|
||||
CREATE TABLE services (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
-- Stabiler Programm-Identifier (z. B. "podium_setup"). Eindeutig.
|
||||
key TEXT NOT NULL UNIQUE,
|
||||
-- Anzeige-Name in der UI (z. B. "Podest aufgestellt"). Frei änderbar.
|
||||
name TEXT NOT NULL,
|
||||
-- 'boolean' → Checkbox, 'numeric' → Zahlenfeld mit min/max.
|
||||
kind TEXT NOT NULL CHECK (kind IN ('boolean', 'numeric')),
|
||||
-- Nur für numeric relevant; bei boolean NULL.
|
||||
min_value INT,
|
||||
max_value INT,
|
||||
-- Soft-Delete: deaktivierte Services bleiben für historische Lieferungen
|
||||
-- referenzierbar, tauchen aber im Default-Listing nicht auf.
|
||||
active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
-- Anzeige-Reihenfolge in Phase 4.
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
|
||||
-- boolean trägt keine Grenzen; numeric darf welche haben.
|
||||
CONSTRAINT services_bounds_kind CHECK (
|
||||
(kind = 'boolean' AND min_value IS NULL AND max_value IS NULL)
|
||||
OR kind = 'numeric'
|
||||
),
|
||||
-- Falls beide gesetzt: min ≤ max.
|
||||
CONSTRAINT services_min_le_max CHECK (
|
||||
min_value IS NULL OR max_value IS NULL OR min_value <= max_value
|
||||
)
|
||||
);
|
||||
|
||||
-- Initialer Stamm (deterministische UUIDs für stabile Seeds/Tests). Der
|
||||
-- Administrator kann sie jederzeit ändern/deaktivieren/ergänzen.
|
||||
INSERT INTO services (id, key, name, kind, min_value, max_value, sort_order) VALUES
|
||||
('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaa01', 'podium_setup', 'Podest aufgestellt', 'boolean', NULL, NULL, 1),
|
||||
('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaa02', 'old_appliance_taken', 'Altgerät mitgenommen', 'boolean', NULL, NULL, 2),
|
||||
('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaa03', 'commissioning', 'Inbetriebnahme', 'boolean', NULL, NULL, 3),
|
||||
('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaa04', 'floor', 'Stockwerk', 'numeric', 0, 20, 4);
|
||||
|
||||
-- Pro-Lieferung gewählter Wert eines Service. Genau eine Zeile je
|
||||
-- (Lieferung, Service) — Upsert. `bool_value` für boolean, `numeric_value`
|
||||
-- für numeric (Use Case stellt sicher, dass nur das passende Feld gesetzt ist).
|
||||
CREATE TABLE delivery_services (
|
||||
delivery_id UUID NOT NULL REFERENCES deliveries(id) ON DELETE CASCADE,
|
||||
-- RESTRICT: ein referenzierter Service darf nicht hart gelöscht werden
|
||||
-- (sonst verlöre die Lieferung ihren Wert) — stattdessen deaktivieren.
|
||||
service_id UUID NOT NULL REFERENCES services(id) ON DELETE RESTRICT,
|
||||
bool_value BOOLEAN,
|
||||
numeric_value INT,
|
||||
author_personalnummer BIGINT,
|
||||
author_car_id UUID,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
|
||||
PRIMARY KEY (delivery_id, service_id)
|
||||
);
|
||||
|
||||
CREATE INDEX delivery_services_delivery ON delivery_services (delivery_id);
|
||||
32
migrations/0017_seed_real_services.sql
Normal file
32
migrations/0017_seed_real_services.sql
Normal file
@ -0,0 +1,32 @@
|
||||
-- 0017_seed_real_services.sql
|
||||
--
|
||||
-- Ersetzt den Platzhalter-Seed aus 0016 durch die **echten** Lieferoptionen
|
||||
-- aus dem Altsystem. Quelle: DOCUframe-Makro
|
||||
-- `_SV/_DF_APP/_initDelivery.dfm` (gegengeprüft in `_getOptionColumnByKey.dfm`)
|
||||
-- — 8 Optionen, 2 numerisch (Stückzahlen), 6 boolean. Im Makro gibt es keine
|
||||
-- min/max-Werte; für die Stückzahlen setzen wir fachlich `min = 0`, kein max.
|
||||
--
|
||||
-- (Migration 0016 wurde bereits angewandt und bleibt unangetastet — daher die
|
||||
-- Korrektur in einer eigenen Migration statt eines Edits.)
|
||||
|
||||
-- Platzhalter-Seeds entfernen. Erst etwaige Pro-Lieferung-Werte lösen
|
||||
-- (FK ON DELETE RESTRICT), dann die Definitionen.
|
||||
DELETE FROM delivery_services
|
||||
WHERE service_id IN (
|
||||
SELECT id FROM services
|
||||
WHERE key IN ('podium_setup', 'old_appliance_taken', 'commissioning', 'floor')
|
||||
);
|
||||
DELETE FROM services
|
||||
WHERE key IN ('podium_setup', 'old_appliance_taken', 'commissioning', 'floor');
|
||||
|
||||
-- Echte Optionen (deterministische UUIDs für stabile Seeds/Tests).
|
||||
-- sort_order = Reihenfolge wie in der alten App (numerische zuerst).
|
||||
INSERT INTO services (id, key, name, kind, min_value, max_value, sort_order) VALUES
|
||||
('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaa01', 'AMOUNT_OLD_DEVICES', 'Anzahl alter Geräte', 'numeric', 0, NULL, 1),
|
||||
('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaa02', 'AMOUNT_MOUNTED_DEVICES', 'Anzahl montierter Geräte', 'numeric', 0, NULL, 2),
|
||||
('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaa03', 'DEVICE_ALIGNED', 'Gerät ausgerichtet', 'boolean', NULL, NULL, 3),
|
||||
('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaa04', 'ON_PLATFORM', 'Auf Podest gestellt', 'boolean', NULL, NULL, 4),
|
||||
('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaa05', 'REVISIT_REQUIRED', 'Erneute Anfahrt notwendig', 'boolean', NULL, NULL, 5),
|
||||
('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaa06', 'TEST_RUN_COMPLETE', 'Testdurchlauf durchgeführt', 'boolean', NULL, NULL, 6),
|
||||
('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaa07', 'TIGHTNESS_TEST_DONE', 'Dichtigkeitstest durchgeführt','boolean', NULL, NULL, 7),
|
||||
('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaa08', 'TUBE_EXTENSION_REQUIRED', 'Schlaucherweiterung notwendig','boolean', NULL, NULL, 8);
|
||||
11
migrations/0018_delivery_total_amount.sql
Normal file
11
migrations/0018_delivery_total_amount.sql
Normal file
@ -0,0 +1,11 @@
|
||||
-- 0018_delivery_total_amount.sql
|
||||
--
|
||||
-- Gesamt-/Rechnungsbetrag (brutto, EUR) einer Lieferung. Zusammen mit
|
||||
-- `prepaid_amount` (bei Bestellung bezahlt) und der Betrags-Gutschrift rechnet
|
||||
-- die App daraus den offenen Betrag:
|
||||
-- offen = max(0, total_amount - prepaid_amount - gutschrift)
|
||||
--
|
||||
-- Wird vom ERP-Sync geliefert. Default 0, bis das Sync-Makro den Wert mitsendet.
|
||||
ALTER TABLE deliveries
|
||||
ADD COLUMN total_amount DOUBLE PRECISION NOT NULL DEFAULT 0
|
||||
CHECK (total_amount >= 0);
|
||||
16
migrations/0019_item_unit_price.sql
Normal file
16
migrations/0019_item_unit_price.sql
Normal file
@ -0,0 +1,16 @@
|
||||
-- 0019_item_unit_price.sql
|
||||
--
|
||||
-- Preis pro Belegzeile/Artikel + Entfernen des statischen Liefer-Totals.
|
||||
--
|
||||
-- Hintergrund: Der Gesamt-/Warenwert einer Lieferung wird jetzt **aus den
|
||||
-- Artikeln berechnet** (Σ Stückpreis × ausgelieferte Menge), nicht mehr als
|
||||
-- statischer Betrag geführt. Ausgelieferte Menge = required - credited, d. h.
|
||||
-- entfernte/teil-entfernte Positionen reduzieren den Wert automatisch. Damit
|
||||
-- ist `deliveries.total_amount` (Migration 0018) überflüssig und wird wieder
|
||||
-- entfernt — eine Quelle der Wahrheit (der Stückpreis).
|
||||
|
||||
ALTER TABLE deliveries DROP COLUMN total_amount;
|
||||
|
||||
ALTER TABLE delivery_items
|
||||
ADD COLUMN unit_price DOUBLE PRECISION NOT NULL DEFAULT 0
|
||||
CHECK (unit_price >= 0);
|
||||
40
migrations/0020_delivery_completions.sql
Normal file
40
migrations/0020_delivery_completions.sql
Normal file
@ -0,0 +1,40 @@
|
||||
-- 0020_delivery_completions.sql
|
||||
--
|
||||
-- Abschluss-Beleg einer Lieferung: dokumentiert den Moment, in dem Kunde
|
||||
-- und Fahrer unterschreiben und die Lieferung auf `completed` geht.
|
||||
--
|
||||
-- Genau EINE Zeile pro Lieferung (PK = delivery_id). Der Abschluss ist
|
||||
-- terminal — ein zweiter Versuch wird vom Use Case über das
|
||||
-- `state == active`-Gate abgelehnt; existiert die Zeile schon und steht
|
||||
-- die Lieferung auf `completed`, liefert der Server idempotent denselben
|
||||
-- Stand zurück.
|
||||
--
|
||||
-- Die beiden Unterschriften liegen NICHT in der DB, sondern als PNG-Dateien
|
||||
-- lokal im Backend-Server (Pfad konfigurierbar). Hier stehen nur die
|
||||
-- relativen Datei-Referenzen.
|
||||
--
|
||||
-- Die Checkbox-Bestätigungen des Kunden werden hier dauerhaft dokumentiert:
|
||||
-- * receipt_confirmed — „Ware ordnungsgemäß erhalten / Aufbau korrekt"
|
||||
-- * notes_acknowledged — „Anmerkungen zur Lieferung zur Kenntnis genommen"
|
||||
-- `acknowledged_note_ids` hält die konkreten Notiz-IDs fest, die zum
|
||||
-- Zeitpunkt des Abschlusses sichtbar waren und mit-bestätigt wurden
|
||||
-- (Audit-Robustheit, falls später Notizen dazukommen/wegfallen).
|
||||
|
||||
CREATE TABLE delivery_completions (
|
||||
delivery_id UUID PRIMARY KEY REFERENCES deliveries(id) ON DELETE CASCADE,
|
||||
|
||||
-- Relative Pfade/Referenzen der lokal gespeicherten Signatur-PNGs.
|
||||
customer_signature_path TEXT NOT NULL,
|
||||
driver_signature_path TEXT NOT NULL,
|
||||
|
||||
-- Dokumentierte Checkbox-Bestätigungen des Kunden.
|
||||
receipt_confirmed BOOLEAN NOT NULL,
|
||||
notes_acknowledged BOOLEAN NOT NULL,
|
||||
acknowledged_note_ids UUID[] NOT NULL DEFAULT '{}',
|
||||
|
||||
-- Akteur: Personalnummer aus dem JWT (Pflicht), Fahrzeug optional.
|
||||
completed_by_personalnummer BIGINT NOT NULL,
|
||||
completed_by_car_id UUID,
|
||||
|
||||
completed_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
18
migrations/0021_remove_credit_card.sql
Normal file
18
migrations/0021_remove_credit_card.sql
Normal file
@ -0,0 +1,18 @@
|
||||
-- 0021_remove_credit_card.sql
|
||||
--
|
||||
-- Entfernt die Zahlungsmethode `credit_card` (Kreditkarte) aus den
|
||||
-- Stammdaten. Sie wurde in Migration 0008 geseedet, kommt aber im
|
||||
-- ERP-Liefer-Flow nicht vor (Allowlist der ERP-Zahlungsbedingungen:
|
||||
-- D16/d53/D10 = cash/ec_card/invoice).
|
||||
--
|
||||
-- 0008 selbst bleibt unangetastet (angewandte Migration → Checksum). Daher
|
||||
-- diese Folge-Migration: erst etwaige Referenzen umhängen (FK
|
||||
-- ON DELETE RESTRICT auf deliveries.payment_method_id), dann löschen.
|
||||
|
||||
-- Bestehende Lieferungen, die noch auf Kreditkarte zeigen, auf `cash`
|
||||
-- (ERP-Default) umstellen — verhindert die FK-Verletzung beim DELETE.
|
||||
UPDATE deliveries
|
||||
SET payment_method_id = (SELECT id FROM payment_methods WHERE code = 'cash')
|
||||
WHERE payment_method_id = (SELECT id FROM payment_methods WHERE code = 'credit_card');
|
||||
|
||||
DELETE FROM payment_methods WHERE code = 'credit_card';
|
||||
12
migrations/0022_scan_audit_manual.sql
Normal file
12
migrations/0022_scan_audit_manual.sql
Normal file
@ -0,0 +1,12 @@
|
||||
-- 0022_scan_audit_manual.sql
|
||||
--
|
||||
-- Fallback-Scan: markiert einen Scan-Eintrag als **manuell bestätigt**
|
||||
-- ("Ich habe den Artikel geladen"), wenn der Barcode-/QR-Scan nicht klappt.
|
||||
--
|
||||
-- Reine Audit-Information: WIE der Scan zustande kam. Die Mengen-/Status-
|
||||
-- Wahrheit (scanned_quantity, scan_status) bleibt unverändert — ein manueller
|
||||
-- Scan ist fachlich ein vollwertiger Scan, nur ohne Barcode-Lesung.
|
||||
-- Default false: alle bestehenden Einträge gelten als regulär gescannt.
|
||||
|
||||
ALTER TABLE scan_audit
|
||||
ADD COLUMN manual BOOLEAN NOT NULL DEFAULT false;
|
||||
15
migrations/0023_item_parent_artikel_nr.sql
Normal file
15
migrations/0023_item_parent_artikel_nr.sql
Normal file
@ -0,0 +1,15 @@
|
||||
-- 0023_item_parent_artikel_nr.sql
|
||||
--
|
||||
-- Stücklisten-Hierarchie sichtbar machen: Artikelnummer des Oberartikels,
|
||||
-- zu dem eine Komponente gehört.
|
||||
--
|
||||
-- Bisher teilten sich Oberartikel und seine Komponenten nur die
|
||||
-- `belegzeilen_nr`; die `komponenten_artikel_nr` trägt die EIGENE Nummer der
|
||||
-- Komponente (muss je belegzeilen_nr eindeutig sein) und taugt daher nicht
|
||||
-- als Parent-Referenz. `parent_artikel_nr` schließt diese Lücke:
|
||||
-- * Oberartikel / reguläre Belegzeile → NULL
|
||||
-- * Komponente → Artikelnummer des Oberartikels
|
||||
--
|
||||
-- Damit kann die App Komponenten unter ihrem Oberartikel einrücken.
|
||||
ALTER TABLE delivery_items
|
||||
ADD COLUMN parent_artikel_nr TEXT;
|
||||
20
migrations/0024_completion_payment_collected.sql
Normal file
20
migrations/0024_completion_payment_collected.sql
Normal file
@ -0,0 +1,20 @@
|
||||
-- 0024_completion_payment_collected.sql
|
||||
--
|
||||
-- Inkasso-Bestätigung beim Abschluss: Wenn beim Abschluss noch ein offener
|
||||
-- Betrag besteht UND die Zahlungsmethode ein Vor-Ort-Inkasso ist (Bar oder
|
||||
-- EC-Karte), muss der Fahrer vor den Unterschriften bestätigen, dass das Geld
|
||||
-- erhalten/abgerechnet wurde. „Auf Rechnung" (invoice) bleibt bewusst offen →
|
||||
-- kein Inkasso, keine Bestätigung.
|
||||
--
|
||||
-- * payment_collected — Fahrer hat das Inkasso bestätigt. Bei
|
||||
-- Lieferungen ohne Inkasso (offen == 0 oder
|
||||
-- Methode = invoice) bleibt es `false`.
|
||||
-- * collected_amount_cents — Snapshot des tatsächlich kassierten offenen
|
||||
-- Betrags in Cent (server-seitig autoritativ
|
||||
-- berechnet). NULL = kein Inkasso erforderlich.
|
||||
-- Eingefroren, damit der Report-/Audit-Wert
|
||||
-- unabhängig von späteren Daten-Resyncs bleibt.
|
||||
|
||||
ALTER TABLE delivery_completions
|
||||
ADD COLUMN payment_collected BOOLEAN NOT NULL DEFAULT false,
|
||||
ADD COLUMN collected_amount_cents BIGINT;
|
||||
18
migrations/0025_delivery_belegart_code.sql
Normal file
18
migrations/0025_delivery_belegart_code.sql
Normal file
@ -0,0 +1,18 @@
|
||||
-- 0025_delivery_belegart_code.sql
|
||||
--
|
||||
-- Belegart lesbar machen: bisher steht nur die `erp_belegart_id` (die ERP-
|
||||
-- `row_id` der Belegart, z. B. 24) in `deliveries` — eine nackte Nummer ohne
|
||||
-- Aussagekraft. Der Sync zieht jetzt zusätzlich den Belegart-Code und die
|
||||
-- Bezeichnung aus ERPframe (`Belegarten.Belegart` / `.Bezeichnung`,
|
||||
-- z. B. „VL5" / „Lieferschein EH") mit, damit der Report sie statt der
|
||||
-- nackten ID anzeigen kann.
|
||||
--
|
||||
-- * erp_belegart_code — Kurzcode der Belegart (z. B. „VL5").
|
||||
-- * erp_belegart_name — Klartext-Bezeichnung (z. B. „Lieferschein EH").
|
||||
--
|
||||
-- Beide nullable: Altbestand vor diesem Sync hat sie nicht; der nächste
|
||||
-- (Re-)Sync füllt sie idempotent nach.
|
||||
|
||||
ALTER TABLE deliveries
|
||||
ADD COLUMN erp_belegart_code TEXT,
|
||||
ADD COLUMN erp_belegart_name TEXT;
|
||||
40
migrations/0026_delivery_report_jobs.sql
Normal file
40
migrations/0026_delivery_report_jobs.sql
Normal file
@ -0,0 +1,40 @@
|
||||
-- 0026_delivery_report_jobs.sql
|
||||
--
|
||||
-- Zustandsanker für die Übertragung des PDF-Lieferreports an DOCUframe.
|
||||
-- Beim Abschluss wird der Report erzeugt und in DOCUframe abgelegt; das ist
|
||||
-- ein mehrstufiger, netzabhängiger Vorgang (Upload → ~ObjectID → Makro
|
||||
-- `_SV_assignDeliveryReport`). Damit nichts verloren geht und fehlgeschlagene
|
||||
-- Übertragungen per Cron wiederholt werden können, hält diese Tabelle den
|
||||
-- Fortschritt **hart** in Postgres.
|
||||
--
|
||||
-- Genau eine Zeile pro Lieferung (PK = delivery_id). `status` ist die
|
||||
-- Resume-Marke; ein Retry führt nur die noch offenen Schritte aus:
|
||||
-- * 'pending' — angelegt, noch nichts übertragen
|
||||
-- * 'uploaded' — PDF liegt in DOCUframe, `docuframe_object_id` gesetzt,
|
||||
-- Makro-Zuordnung steht noch aus
|
||||
-- * 'done' — Makro erfolgreich; lokale Dateien wurden aufgeräumt
|
||||
--
|
||||
-- Fehlersemantik: schlägt ein Schritt fehl, bleibt `status` auf der erreichten
|
||||
-- Stufe, `last_error`/`attempts`/`last_attempt_at` werden gesetzt. Der Cron
|
||||
-- greift alle Zeilen mit status <> 'done' erneut auf.
|
||||
|
||||
CREATE TABLE delivery_report_jobs (
|
||||
delivery_id UUID PRIMARY KEY REFERENCES deliveries(id) ON DELETE CASCADE,
|
||||
belegnummer TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'pending'
|
||||
CHECK (status IN ('pending', 'uploaded', 'done')),
|
||||
-- DOCUframe ~ObjectID des hochgeladenen Reports — hart gespeichert nach
|
||||
-- dem Upload, damit ein Retry nicht doppelt hochlädt.
|
||||
docuframe_object_id TEXT,
|
||||
-- Zeitpunkt der erfolgreichen Makro-Zuordnung (= „Report hochgeladen").
|
||||
report_uploaded_at TIMESTAMPTZ,
|
||||
attempts INT NOT NULL DEFAULT 0,
|
||||
last_error TEXT,
|
||||
last_attempt_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- Schneller Zugriff des Retry-Cron auf offene Jobs.
|
||||
CREATE INDEX delivery_report_jobs_open ON delivery_report_jobs (status)
|
||||
WHERE status <> 'done';
|
||||
14
migrations/0027_attachment_deleted.sql
Normal file
14
migrations/0027_attachment_deleted.sql
Normal file
@ -0,0 +1,14 @@
|
||||
-- 0027_attachment_deleted.sql
|
||||
--
|
||||
-- „Soft-Delete" für Bild-Notizen: Sobald der PDF-Lieferreport (in dem die
|
||||
-- Bilder eingebettet sind) erfolgreich in DOCUframe liegt, wird die lokale
|
||||
-- Bilddatei gelöscht, um keinen Datenmüll zu halten. Die Metadaten-Zeile
|
||||
-- bleibt jedoch erhalten — so ist weiterhin ersichtlich, dass es ein Bild
|
||||
-- gab. Die App zeigt für solche Anhänge keinen Vorschau-Download mehr,
|
||||
-- sondern den Hinweis, dass das Bild im Lieferbericht enthalten ist.
|
||||
--
|
||||
-- `deleted_at` doppelt als Flag (NULL = lokale Datei vorhanden) UND als
|
||||
-- Audit-Zeitstempel (wann aufgeräumt).
|
||||
|
||||
ALTER TABLE attachments
|
||||
ADD COLUMN deleted_at TIMESTAMPTZ;
|
||||
20
migrations/0028_completion_mail_sent.sql
Normal file
20
migrations/0028_completion_mail_sent.sql
Normal file
@ -0,0 +1,20 @@
|
||||
-- 0028_completion_mail_sent.sql
|
||||
--
|
||||
-- Markierung, ob für eine ausgelieferte (abgeschlossene) Lieferung die
|
||||
-- Liefer-/Belegmail bereits versendet wurde. Der externe Mailclient pollt die
|
||||
-- noch NICHT versendeten Belege (GET /admin/delivered-belegnummern liefert nur
|
||||
-- `mail_sent_at IS NULL`), stößt ERPframe an (das die Mails verschickt) und
|
||||
-- markiert sie anschließend über POST /admin/mark-mail-sent als versendet.
|
||||
--
|
||||
-- NULL = noch nicht versendet (offen).
|
||||
-- Wert = Zeitstempel, wann markiert wurde (Audit + server-seitiges Dedup,
|
||||
-- damit derselbe Beleg nicht alle 5 Minuten erneut eine Mail auslöst).
|
||||
|
||||
ALTER TABLE delivery_completions
|
||||
ADD COLUMN mail_sent_at TIMESTAMPTZ;
|
||||
|
||||
-- Partieller Index: der Mailclient fragt praktisch immer nur die offenen
|
||||
-- (mail_sent_at IS NULL) Belege ab.
|
||||
CREATE INDEX delivery_completions_mail_unsent
|
||||
ON delivery_completions (completed_at)
|
||||
WHERE mail_sent_at IS NULL;
|
||||
62
migrations/0029_delivery_contact_sources.sql
Normal file
62
migrations/0029_delivery_contact_sources.sql
Normal file
@ -0,0 +1,62 @@
|
||||
-- 0029_delivery_contact_sources.sql
|
||||
--
|
||||
-- Belegspezifische Kontaktdaten-Snapshots. ERPframe modelliert pro Beleg bis
|
||||
-- zu fünf Adressen (Belegadresse `Belegkopf.AdressId`, Lieferadresse
|
||||
-- `LieferAdressId`, Rechnungsadresse `RechnungsAdressId`, Ansprechpartner
|
||||
-- `AnsprechpartnerId`, plus die Kundenstamm-Adresse über `Kunden.AdressId`).
|
||||
-- Jeder dieser Adress-Datensätze trägt seinerseits mehrere Telefon-,
|
||||
-- Mobil- und E-Mail-Spalten (`Telefon..Telefon4`, `Mobiltel..Mobiltel2`,
|
||||
-- `EMail..EMail3`, `InternetAdresse`) sowie einen Namensblock
|
||||
-- (`Anrede`/`Titel`/`Name1..3`/`Abteilung`/`Funktion`).
|
||||
--
|
||||
-- Wir spiegeln das als zwei Tabellen: pro Lieferung 0..n
|
||||
-- `delivery_contact_sources` (eine Zeile je Rolle), und pro Source 0..n
|
||||
-- `delivery_contact_channels` (eine Zeile je Telefon-/Mobil-/E-Mail-/Web-
|
||||
-- Eintrag). Snapshot-Semantik wie bei `deliveries.snap_*`: beim Sync werden
|
||||
-- alle Sources einer Lieferung gelöscht und neu geschrieben — die Lieferung
|
||||
-- behält damit den Stand vom letzten Sync, unabhängig von späteren ERP-
|
||||
-- Adressänderungen.
|
||||
--
|
||||
-- `Fax` wird bewusst nicht gespiegelt (in der App nicht benutzt).
|
||||
|
||||
CREATE TABLE delivery_contact_sources (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
delivery_id UUID NOT NULL REFERENCES deliveries(id) ON DELETE CASCADE,
|
||||
-- Mapping auf die ERP-FKs am Belegkopf bzw. den Kunden-Umweg:
|
||||
-- header → Belegkopf.AdressId
|
||||
-- delivery → Belegkopf.LieferAdressId
|
||||
-- billing → Belegkopf.RechnungsAdressId
|
||||
-- contact_person → Belegkopf.AnsprechpartnerId
|
||||
-- customer_master → Kunden.AdressId (über Belegkopf.KundenId)
|
||||
role TEXT NOT NULL
|
||||
CHECK (role IN ('header','delivery','billing',
|
||||
'contact_person','customer_master')),
|
||||
anrede TEXT,
|
||||
titel TEXT,
|
||||
name1 TEXT,
|
||||
name2 TEXT,
|
||||
name3 TEXT,
|
||||
abteilung TEXT,
|
||||
funktion TEXT,
|
||||
UNIQUE (delivery_id, role)
|
||||
);
|
||||
CREATE INDEX delivery_contact_sources_delivery
|
||||
ON delivery_contact_sources(delivery_id);
|
||||
|
||||
CREATE TABLE delivery_contact_channels (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
source_id UUID NOT NULL REFERENCES delivery_contact_sources(id) ON DELETE CASCADE,
|
||||
-- phone → Adressen.Telefon{,2,3,4}
|
||||
-- mobile → Adressen.Mobiltel{,2}
|
||||
-- email → Adressen.EMail{,2,3}
|
||||
-- web → Adressen.InternetAdresse
|
||||
kind TEXT NOT NULL
|
||||
CHECK (kind IN ('phone','mobile','email','web')),
|
||||
-- 1-basierte Position aus dem ERP-Spaltennamen (Telefon → 1, Telefon2 → 2,
|
||||
-- usw.). Erlaubt der App, „primären" Kanal je Kind stabil zu erkennen.
|
||||
position SMALLINT NOT NULL CHECK (position >= 1),
|
||||
value TEXT NOT NULL CHECK (value <> ''),
|
||||
UNIQUE (source_id, kind, position)
|
||||
);
|
||||
CREATE INDEX delivery_contact_channels_source
|
||||
ON delivery_contact_channels(source_id);
|
||||
Reference in New Issue
Block a user