Finanzen (movements-Ledger + Auto-Posting)
1. Zweck & Scope
Dieses Modul ist das finanzielle Herz von Caja und die Single Source of Truth für jeden Geldfluss der Dominicano Express GmbH. Es ersetzt das Excel-Blatt Erfassung_Digitar (Version 2, DIGITAR-Prinzip) durch eine movements-Tabelle, in die jede operative Erfassung genau einmal landet — manuell oder automatisch erzeugt durch operative Events (Offerte angenommen, Auftrag, Rechnung, Zahlung, Depot, Provision). Alle abgeleiteten Grössen (Kasse-Eingang/-Ausgang, Bank, offene Schuld, Status-Ampel, Monatskey, laufender Saldo) sind nicht gespeichert, sondern als GENERATED-Spalten bzw. Views berechnet — exakt nach der bewährten Excel-Routing-Logik, aber relational sauber (Zahlungen sind explizit an ihre Rechnung/ihr Depot verknüpft statt per Ref+Partei-String-Match). Buchhaltung = Cash-Basis + Offene Posten, keine doppelte Buchführung; das Modul speist die formale Buchhaltung (Treuhänder-Export, MWST-Kennzahlen, Monats-/Jahresreport), ersetzt aber nicht den rechtsgültigen Jahresabschluss. MWST ist seit dem Finanz-Entscheid vom 2026-06-28 scharf (app_settings.vat_enabled=true): Das System ist MwSt-fähig (Methode inclusive für allfällige steuerbare Inland-Leistungen, eingefrorener Satz, §3.8). Die Kern-Versandleistung (grenzüberschreitende Beförderung CH → DR / Ausfuhr) ist jedoch MwSt-BEFREIT (echte Befreiung Art. 23 MWSTG → 0 %, vat_code='EXEMPT', Entscheid 2026-06-28 Teil 3): kein MwSt-Aufschlag, nichts herausgerechnet, vat_amount_chf=0. Der Normalsatz (8.1 %) greift nur für allfällige steuerbare Inland-Zusatzleistungen. Einfuhrsteuer/eVV bleibt schema-ready (🔲). Das Ledger ist mehrwährungsfähig (CHF + DOP) mit Berichtswährung CHF (§3.9), DOP-Zahlungen laufen über Revolut Business (§10). Revisionssicherheit nach OR 957 / GeBüV: Monatsabschluss sperrt die Periode, jede Nachbuchung ist nur auditiert möglich, Bewegungen aus Quellen werden nie gelöscht — nur der Ursprung wird korrigiert (Upsert).
2. Domänenmodell
Kern ist movements (das Ledger). Jede Zeile trägt ihre fachlichen Eingabewerte (Datum, Referenz, Partei, Vorgangsart, Zahlweise, Gesamtbetrag, Bezahlt/Angezahlt, Notiz, Fälligkeit), die Währung (currency, Default CHF) + Kurs (fx_rate zu CHF), den MWST-Satz (eingefroren als vat_rate_percent + vat_amount_chf) plus Herkunft (source_type + source_id) für Idempotenz. Alles Rechnerische ist abgeleitet.
Entitäten & Beziehungen:
movements— n:1 zucontacts(Partei),movement_types(Vorgangsart-Code),payment_methods(Zahlweise-Code),vat_rates(MWST-Satz, optional),monthly_closings(überperiod_key). Optionaler Self-Linktransfer_group_idpaart die zwei Beine eines internen Transfers (z. B.CASH_TO_BANK).movement_types— Lookup mit stabilemcode(EN) + DE/ES/EN-Labels + Verhaltens-Flags (is_internal_transfer,is_receivable,is_payable,is_deposit,cash_sign).payment_methods— Lookup mit stabilemcode+ DE/ES/EN-Labels +channel ∈ {cash, bank, open}(entscheidet Kasse/Bank — harte Spec-Regel statt Textmatch).payment_links— explizite m:n-Verknüpfung einer Zahlung (movement vom TypINVOICE_PAYMENT/SUPPLIER_PAYMENT) auf ihre Ziel-Rechnung/-Schuld (movement vom TypINVOICE/RECEIVABLE/SUPPLIER_DEBT/DEPOSIT_CASH). Ersetzt den Excel-Ref+Partei-Match durch eine prüfbare Kante. (Migrations-Fallback:parent_ref+parent_contact_id.)vat_rates— effektiv-datierte MWST-Sätze (CH-Sätze), seit 2026-06-28 aktiv (active=true); enthält neben dem Normalsatz (8.1 %,STD) den CodeEXEMPT(0 %, echte Ausfuhr-Befreiung Art. 23 MWSTG) — Default für die Versandleistung. Der zum Buchungszeitpunkt gültige Satz wird je Bewegung alsvat_rate_percenteingefroren (beiEXEMPT: 0).monthly_closings— eine Zeile jeperiod_key(MM/YYYY), hältstatus(open|review|closed), Sperr-Zeitpunkt, Anfangs-/Endsaldo-Snapshot.audit_log(geteilt mit Modul Plattform) — unveränderbares Journal jeder Mutation anmovementsundmonthly_closings.
ER-Skizze (mermaid):
Diagramm wird geladen …
Auto-Posting-Quellen (operative Module → genau ein movements-Eintrag je Quelle):
| Event (Herkunftsmodul) | source_type | erzeugt movement_type |
|---|---|---|
| Rechnung gestellt (Modul 4/5/6) | invoice_issued | INVOICE (einmalige Forderungsbuchung, F-06) |
| Kundenzahlung (Modul 6) | debtor_payment | INVOICE_PAYMENT |
| Depot-Order angelegt (Modul 3/5) | deposit_order | DEPOSIT_CASH (Soll) |
| Depot-Zahlung (Modul 5/6) | deposit_payment | DEPOSIT_CASH (Abono) |
| Depot-Erstattung/-Verfall (Modul 5) | deposit_refund | EXPENSE (Rückzahlung) / INCOME (Verfall) |
| Affiliate-Payout bezahlt (Modul 7) | commission_payout | SUPPLIER_PAYMENT |
| Manuelle Erfassung (inkl. Lieferantenrechnung/-zahlung, F-02) | manual | beliebig (EXPENSE/SUPPLIER_PAYMENT/SUPPLIER_DEBT) |
| Excel-Migration | import | beliebig |
F-06 — keine Offerten-Vormerkung: Die frühere Zeile
quote_accepted → RECEIVABLEentfällt. Die Forderung wird genau einmal bei Rechnungsstellung (invoice_issued → INVOICE) gebucht; eine zusätzlicheRECEIVABLEbei Offerten-Annahme würde dieselbe Forderung inv_debitoren/v_dashboarddoppelt zählen. F-02 — Kreditoren manuell: Lieferantenrechnungen/-zahlungen haben keine Quell-Tabelle und werden als manuellemovements(source_type='manual',EXPENSE/SUPPLIER_PAYMENT/SUPPLIER_DEBT) erfasst. 🔲 strukturierte Kreditoren-Entität als spätere Option.
3. Supabase-Schema
Konvention: alle CHF-Beträge
numeric(12,2); Berichtswährung = CHF (jede Bewegung führt zusätzlichcurrency+fx_ratefür Originalwährung, §3.9). Stabile Codes sind TEXT-PKs der Lookups (nie Fachbegriff in Logik verdrahten — immer FK aufcode). Zeitstempeltimestamptz. Soft-Delete viavoided_at(kein hartes DELETE für quellen-erzeugte Zeilen).
3.1 Lookups (stabile Codes + DE/ES/EN-Labels)
movement_types — Quelle: Glossar + Erfassung_Digitar-Dropdown. Kanonische DDL: Dok 30 §3.1 (movement_types, K-06 — Plattform-Heimat, Finanzen-Superset) — hier nur der fachliche Auszug. Stabiler code (EN, nie übersetzen) + DE/ES/EN-Labels + Verhaltens-Flags is_internal_transfer, is_receivable (treibt Debitoren), is_payable (treibt Kreditoren), is_deposit (Anzahlung), cash_sign (+1 Eingang / −1 Ausgang / 0 neutral, nur bei CASH relevant). Fachliche Seed-Werte (Codes + Flags):
| code (stabil) | label_de | label_es | label_en | transfer | recv | pay | deposit | cash_sign |
|---|---|---|---|---|---|---|---|---|
DEPOSIT_CASH | Anzahlung Kasse (bar) | Depósito caja | Cash deposit | – | ✓ | – | ✓ | +1 |
INVOICE | Rechnung | Factura | Invoice | – | ✓ | – | – | +1 |
INVOICE_PAYMENT | Rechnungszahlung | Abono factura | Invoice payment | – | – | – | – | +1 |
RECEIVABLE | Forderung (Kunde) | Deuda | Receivable | – | ✓ | – | – | 0 |
INCOME | Einnahme | Ingreso | Income | – | – | – | – | +1 |
EXPENSE | Ausgabe | Gasto | Expense | – | – | ✓ | – | −1 |
SUPPLIER_PAYMENT | Lieferantenzahlung | Pago proveedor | Supplier payment | – | – | ✓ | – | −1 |
SUPPLIER_DEBT | Lieferantenschuld | Deuda proveedor | Supplier debt | – | – | ✓ | – | −1 |
CASH_TO_BANK | Kasse → Bank | Caja → Banco | Cash → Bank | ✓ | – | – | – | −1 |
BANK_TO_CASH | Bank → Kasse | Banco → Caja | Bank → Cash | ✓ | – | – | – | +1 |
BANK_TO_TWINT | Bank → Twint | Banco → Twint | Bank → Twint | ✓ | – | – | – | 0 |
TWINT_TO_BANK | Twint → Bank | Twint → Banco | Twint → Bank | ✓ | – | – | – | 0 |
payment_methods — channel ist die harte Cash/Bank-Entscheidung. Kanonische DDL: Dok 30 §3.2 (payment_methods, K-06) — hier nur der fachliche Auszug. Stabiler code + DE/ES/EN-Labels + channel ∈ {cash, bank, open} (entscheidet Kasse/Bank — Spec-Regel statt Textmatch). Fachliche Seeds:
| code | label_de | label_es | label_en | channel |
|---|---|---|---|---|
CASH | Bar | Efectivo | Cash | cash |
BANK | Bank | Banco | Bank | bank |
TWINT | Twint | Twint | Twint | bank |
CARD | Karte | Tarjeta | Card | bank |
OPEN | Offen | Abierto | Open | open |
Status-Codes (abgeleitet, nicht gespeichert — als Enum-Domäne dokumentiert):
| code | label_de | label_es | label_en | Ampel |
|---|---|---|---|---|
PAID | Bezahlt | Pagado | Paid | 🟢 |
PARTIAL | Teilweise bezahlt | Parcial | Partially paid | 🟠 |
OPEN | Offen | Abierto | Open | 🔴 |
DEPOSIT_NO_PAYMENT | Anzahlung ohne Zahlung | Depósito sin abono | Deposit without payment | 🟡 |
INTERNAL_TRANSFER | Interne Umbuchung | Transferencia interna | Internal transfer | ⚪ |
vat_rates (CH, aktiv seit 2026-06-28) — Kanonische DDL: Dok 30 §3.2 (vat_rates); hier nur fachlich. Effektiv-datierte CH-Sätze (code, Labels, rate_pct, valid_from/valid_to, active); der je Bewegung gültige Satz wird als vat_rate_percent eingefroren (§3.8).
- Seeds (CH ab 2024):
STD8.1 %,RED2.6 %,SPECIAL3.8 % (Beherbergung),EXEMPT0 % (echte Befreiung Art. 23 MWSTG — Default für die Versandleistung CH→DR/Ausfuhr),ZERO0 % (eVV-Vorsteuer-relevant/Import). - Entscheid 2026-06-28 Teil 3: Fracht/Versand =
EXEMPT(0 %);STD8.1 % nur für steuerbare Inland-Zusatzleistungen.
3.2 Kerntabelle movements
Kanonische DDL: Dok 30 §4.1 (
movements, Owner; K-05, K-18/19) — Single Source of Truth für Spalten, Constraints, Indizes. Hier nur der fachliche Auszug (Feldbedeutung + Caja-spezifische Regeln); keine Schema-Definition reproduzieren.
Eingabewerte (entsprechen Erfassung_Digitar A..G, O, P): entry_date (A), reference (B), contact_id → contacts (C, Partei), movement_type_code → movement_types (D), payment_method_code → payment_methods (E), total_chf (F, Gesamtbetrag CHF), paid_chf (G, Bezahlt/Angezahlt CHF), note (O), due_date (P).
Mehrwährung (§3.9, Berichtswährung CHF): currency → currencies (Default CHF), fx_rate (Kurs Original→CHF, CHF=1), total_orig/paid_orig (Originalbeträge). Es gilt total_chf = round(total_orig * fx_rate, 2); CHF bleibt die gebuchte/berichtete Grösse.
MWST (scharf seit 2026-06-28, §3.8):
total_chf— beiEXEMPT(Versand/Ausfuhr) der volle befreite Betrag; bei steuerbarer Leistung BRUTTO inkl. MWST (Inclusive).vat_code→vat_rates— DefaultEXEMPTfür die Versandleistung (Art. 23 MWSTG);STDnur für steuerbare Inland-Zusatzleistungen;NULLfür MWST-neutrale Transfers/Altzeilen.vat_rate_percent— eingefrorener Satz zum Buchungszeitpunkt; entkoppelt von späterenvat_rates-Änderungen (beiEXEMPT: 0).vat_amount_chf— beiEXEMPT/0 immer 0; bei steuerbarer Leistung aus dem BRUTTO herausgerechnet (total_chf - round(total_chf/(1+vat_rate_percent/100),2)). F-18: im Schreibpfad (Server-Action /post_movement) persistiert, nicht in einer View, keine generated column (Satz ist eingefroren).
Herkunft / Idempotenz: source_type (manual/import/invoice_issued/debtor_payment/deposit_order/deposit_payment/deposit_refund/commission_payout, plus revolut §10 — kanonische CHECK-Liste in Dok 30 §4.1) + source_id (Quell-Datensatz). F-02: kein creditor_invoice/creditor_payment (Kreditoren manuell). F-05: commission_payout (kanonisch, Dok 30 K-05), nicht affiliate_payout. transfer_group_id paart die zwei Beine eines internen Transfers. Unique (source_type, source_id) = genau eine Buchung pro operativer Quelle (Upsert-Target).
Revisionssicherheit: period_key (generated MM/YYYY aus entry_date), voided_at/voided_by/void_reason (Storno statt DELETE) + created/updated_*. Constraint: Beträge nie negativ.
Hinweis:
source_idist beimanualNULL — das Unique greift dann nicht (Postgres behandelt NULL als verschieden), was korrekt ist (mehrere manuelle Buchungen erlaubt; quellen-erzeugte sind eindeutig).
3.3 Explizite Zahlungs-Verknüpfung payment_links
Kanonische DDL: Dok 30 §4.2 (
payment_links) — hier nur fachlich.
Explizite Kante einer Zahlung auf ihre Ziel-Forderung: payment_id → movements (die Zahlung, INVOICE_PAYMENT / SUPPLIER_PAYMENT; on delete cascade), target_id → movements (Rechnung/Schuld, INVOICE / RECEIVABLE / SUPPLIER_DEBT / DEPOSIT_CASH; on delete restrict), amount_chf (> 0). Unique (payment_id, target_id). Ersetzt den Excel-Ref+Partei-Match durch eine prüfbare Verknüpfung (Migrations-Fallback s. §3.5).
3.4 Periodenabschluss monthly_closings
Kanonische DDL: Dok 30 §4.3 (
monthly_closings) — hier nur fachlich.
Eine Zeile je period_key (MM/YYYY, PK): status (open/review/closed), Snapshot von opening_cash/closing_cash/opening_bank/closing_bank bei Abschluss, closed_at/closed_by, reopened_at/reopened_by (Reopen audit-pflichtig), notes. status='closed' sperrt die Periode (Insert/Update via RLS verriegelt, §3.7/§8).
3.5 Abgeleitete Werte — GENERATED-Spalten vs. View
Die paritätsstabile, zeilenlokale Klassifikation (Kanal, Status, cash_sign) ist als Trigger-gepflegte Spalten oder rein in einer View lösbar. Weil cash_in/cash_out/open_debt von anderen Zeilen abhängen (SUMIFS über gleiche Ref+Partei) bzw. von der Zahlweise-Tabelle, sind sie nicht als GENERATED ALWAYS (immutable) abbildbar → sie leben in der View v_movements (Single Point der Routing-Logik). Nur das rein zeilenlokale period_key ist eine echte generated column (s. o.).
v_movements — bildet Erfassung_Digitar!H..M 1:1 ab, aber mit channel aus payment_methods statt Textmatch:
create view v_movements as
with base as (
select
m.*,
pm.channel as method_channel, -- cash | bank | open
mt.is_internal_transfer,
mt.is_receivable, mt.is_payable, mt.is_deposit
from movements m
join payment_methods pm on pm.code = m.payment_method_code
join movement_types mt on mt.code = m.movement_type_code
where m.voided_at is null
)
select
b.*,
-- H Kasse Eingang: BANK_TO_CASH ODER (CASH & type in {DEPOSIT_CASH,INVOICE,INVOICE_PAYMENT,INCOME})
case when b.movement_type_code = 'BANK_TO_CASH'
or (b.method_channel = 'cash'
and b.movement_type_code in ('DEPOSIT_CASH','INVOICE','INVOICE_PAYMENT','INCOME'))
then b.paid_chf else 0 end as cash_in_chf,
-- I Kasse Ausgang: CASH_TO_BANK ODER (CASH & type in {EXPENSE,SUPPLIER_PAYMENT,SUPPLIER_DEBT})
case when b.movement_type_code = 'CASH_TO_BANK'
or (b.method_channel = 'cash'
and b.movement_type_code in ('EXPENSE','SUPPLIER_PAYMENT','SUPPLIER_DEBT'))
then b.paid_chf else 0 end as cash_out_chf,
-- J Bank/Twint/Karte: método in {BANK,TWINT,CARD} ODER interner Transfer
case when b.method_channel = 'bank' or b.is_internal_transfer
then b.paid_chf else 0 end as bank_chf,
-- K Offene Schuld: 0 bei internem Transfer; bei {DEPOSIT_CASH,INVOICE,RECEIVABLE}
-- = max(0, total - Σ paid[gleiche Ref+Partei]); sonst 0
case
when b.is_internal_transfer then 0
when b.movement_type_code in ('DEPOSIT_CASH','INVOICE','RECEIVABLE') then
greatest(0, b.total_chf - coalesce((
select sum(s.paid_chf) from movements s
where s.voided_at is null
and s.reference is not distinct from b.reference
and s.contact_id is not distinct from b.contact_id
),0))
else 0
end as open_debt_chf,
-- M Status-Ampel
case
when b.is_internal_transfer then 'INTERNAL_TRANSFER'
when b.movement_type_code = 'DEPOSIT_CASH' and coalesce(b.paid_chf,0) = 0
then 'DEPOSIT_NO_PAYMENT'
when b.movement_type_code in ('DEPOSIT_CASH','INVOICE','RECEIVABLE','SUPPLIER_DEBT') then
case
when greatest(0, b.total_chf - coalesce((
select sum(s.paid_chf) from movements s
where s.voided_at is null
and s.reference is not distinct from b.reference
and s.contact_id is not distinct from b.contact_id),0)) = 0
then 'PAID'
when b.paid_chf > 0 then 'PARTIAL'
else 'OPEN'
end
else 'PAID'
end as status_code
from base b;
Normalisierter Pfad (Ziel): Sobald
payment_linksgepflegt sind, ersetzt die Restschuld-Berechnung denRef+Partei-Subselect durchtarget.total_chf - Σ payment_links.amount_chf. Die View bietet beide Pfade;Ref+Parteibleibt Migrations-Fallback für Altdaten ohne Links.
3.6 Geschäftliche Views
-- laufender Kassensaldo (Erfassung_Digitar!L) — Saldo NICHT gespeichert
create view v_kasse as
select v.*,
sum(v.cash_in_chf - v.cash_out_chf)
over (order by v.entry_date, v.created_at, v.id
rows between unbounded preceding and current row) as running_cash_chf
from v_movements v
where v.cash_in_chf <> 0 or v.cash_out_chf <> 0;
-- Debitoren = offene Kundenforderungen (INVOICE/RECEIVABLE/DEPOSIT_CASH mit Restschuld)
create view v_debitoren as
select v.id, v.entry_date, v.reference, v.contact_id, v.movement_type_code,
v.total_chf, v.paid_chf, v.open_debt_chf, v.status_code, v.due_date, v.note
from v_movements v
where v.movement_type_code in ('INVOICE','RECEIVABLE','DEPOSIT_CASH')
and v.open_debt_chf > 0.05;
-- Kreditoren = offene Lieferantenschulden (+ Prioritäts-Sortierung)
create view v_kreditoren as
select v.id, v.entry_date, v.reference, v.contact_id, v.movement_type_code,
v.total_chf, v.paid_chf, v.open_debt_chf, v.status_code, v.due_date, v.note,
case
when v.status_code = 'PAID' then '4 Bezahlt'
when v.due_date is not null and v.due_date < current_date then '2 Überfällig'
when v.due_date is not null and v.due_date <= current_date + 7 then '3 Bald fällig'
else '5 Offen'
end as priority -- '1 Dringend' wird via manuellem Flag/Notiz gesetzt
from v_movements v
where v.movement_type_code in ('EXPENSE','SUPPLIER_PAYMENT','SUPPLIER_DEBT')
and v.open_debt_chf > 0.05;
-- Anzahlungen (Depot) — Historie + Status
create view v_anzahlungen as
select v.id, v.entry_date, v.reference, v.contact_id,
v.total_chf as deposit_chf, v.paid_chf, v.open_debt_chf as pending_chf, v.status_code, v.note
from v_movements v
where v.movement_type_code = 'DEPOSIT_CASH';
-- Dashboard-KPIs (Übersicht_Resumen A..H), optional per Monat gefiltert in der App
create view v_dashboard as
select
sum(cash_in_chf) as cash_in,
sum(cash_out_chf) as cash_out,
sum(cash_in_chf) - sum(cash_out_chf) as cash_net,
sum(case when bank_chf<>0 and movement_type_code in
('DEPOSIT_CASH','INVOICE','INVOICE_PAYMENT','INCOME','BANK_TO_CASH','BANK_TO_TWINT')
then bank_chf else 0 end) as bank_in,
sum(case when bank_chf<>0 and movement_type_code in
('EXPENSE','SUPPLIER_PAYMENT','SUPPLIER_DEBT','CASH_TO_BANK','TWINT_TO_BANK')
then bank_chf else 0 end) as bank_out,
(select coalesce(sum(open_debt_chf),0) from v_debitoren) as debtors_open,
(select coalesce(sum(open_debt_chf),0) from v_kreditoren) as creditors_open,
(select coalesce(sum(paid_chf),0) from v_anzahlungen) as deposits_paid,
(select coalesce(sum(pending_chf),0) from v_anzahlungen) as deposits_open
from v_movements;
-- Monatskontrolle (CONTROL_MENSUAL): pro period_key Summen + Abschluss-Status
create view v_monatskontrolle as
select
v.period_key,
sum(v.cash_in_chf) as cash_in,
sum(v.cash_out_chf) as cash_out,
sum(v.cash_in_chf - v.cash_out_chf) as cash_net,
sum(v.bank_chf) as bank_total,
coalesce(sum(v.open_debt_chf) filter
(where v.movement_type_code in ('INVOICE','RECEIVABLE','DEPOSIT_CASH')),0) as debtors_open,
coalesce(sum(v.open_debt_chf) filter
(where v.movement_type_code in ('EXPENSE','SUPPLIER_PAYMENT','SUPPLIER_DEBT')),0) as creditors_open,
coalesce(c.status,'open') as closing_status
from v_movements v
left join monthly_closings c on c.period_key = v.period_key
group by v.period_key, c.status;
3.7 RLS-Skizze (wer darf was)
Rollen-Codes (kanonisch, Dok 30 K-17 / Modul 17, F-09): ADMIN (Marcel), BUCHHALTUNG (Mariela), OPERATIONS (Markus), FAHRER (Arkys), AFFILIATE (extern), READONLY (Treuhänder/Gast). Die Personennamen sind nur umgangssprachliche Referenz. RLS prüft über has_role('CODE') (nicht über einen auth_role()-Personennamen-Helper).
| Tabelle | SELECT | INSERT | UPDATE | DELETE |
|---|---|---|---|---|
movements | ADMIN, BUCHHALTUNG, OPERATIONS, READONLY (alle); FAHRER: nur eigene Quellen (Depot/Logistik); AFFILIATE: keine | ADMIN, BUCHHALTUNG (manuell); Auto-Posting via service_role aus Edge-Functions | ADMIN, BUCHHALTUNG — nur wenn period_key nicht closed und source_type='manual'; quellen-erzeugte Zeilen read-only (nur Ursprung editieren) | niemand (hartes DELETE blockiert); nur void via RPC durch ADMIN/BUCHHALTUNG |
payment_links | wie movements SELECT | ADMIN, BUCHHALTUNG, service_role | ADMIN, BUCHHALTUNG | cascade über payment-void |
movement_types, payment_methods, vat_rates | alle authentifizierten (Labels fürs UI) | ADMIN | ADMIN | ADMIN |
monthly_closings | ADMIN, BUCHHALTUNG, OPERATIONS, READONLY | ADMIN, BUCHHALTUNG | ADMIN, BUCHHALTUNG (Abschluss/Reopen) | niemand |
Beispiel-Policies:
alter table movements enable row level security;
-- Lesen: Finanz-/Leitungsrollen alles; FAHRER nur eigene Logistik-Quellen
create policy mov_select on movements for select using (
has_role('ADMIN') or has_role('BUCHHALTUNG') or has_role('OPERATIONS') or has_role('READONLY')
or (has_role('FAHRER') and source_type in ('deposit_order','deposit_payment'))
);
-- Manuelles Update nur in offener Periode und nur manuelle Quelle
create policy mov_update on movements for update using (
(has_role('ADMIN') or has_role('BUCHHALTUNG'))
and source_type = 'manual'
and not exists (select 1 from monthly_closings c
where c.period_key = movements.period_key and c.status = 'closed')
);
-- Kein hartes Löschen
create policy mov_no_delete on movements for delete using (false);
3.8 MWST scharf — Versand/Ausfuhr befreit (EXEMPT/0 %); inclusive nur für steuerbare Leistungen
Entscheid 2026-06-28 (Teil 3): MWST ist aktiv (app_settings.vat_enabled=true), das System bleibt MwSt-fähig (Methode vat_method='inclusive' für steuerbare Leistungen). Die Fracht-/Versandleistung ist aber MwSt-BEFREIT. Damit gilt:
-
Versandleistung = echte Befreiung (Art. 23 MWSTG → 0 %). Die grenzüberschreitende Beförderung CH → DR (Ausfuhr) ist von der Steuer befreit:
vat_code='EXEMPT',vat_rate_percent=0,vat_amount_chf=0. Es wird kein MwSt aufgeschlagen und nichts aus dem Betrag herausgerechnet —total_chfist der volle (befreite) Entgeltbetrag. Ausfuhrnachweis (Frachtdokument) wird als Beleg archiviert (Compliance §3.6). -
Inclusive nur für steuerbare Inland-Leistungen. Nur bei einer allf. steuerbaren Leistung (z. B. rein innerschweizerisch, ohne Ausfuhrbezug) gilt der Normalsatz 8.1 % (
STD) nach der Inclusive-Methode:total_chfist dann der Bruttobetrag, aus dem die MWST herausgerechnet wird:-- nur wenn vat_code steuerbar ist (z.B. STD/RED/SPECIAL); bei EXEMPT/ZERO entfällt das, vat_amount_chf=0 netto_chf = round(total_chf / (1 + vat_rate_percent/100), 2) vat_amount_chf = total_chf - netto_chfBeispiel Normalsatz 8.1 %: Brutto 100.00 CHF → Netto 92.51 CHF → MWST 7.49 CHF. Für die Versandleistung (EXEMPT) gilt das nicht: 100.00 CHF → MWST 0.00 CHF.
-
Eingefrorener Satz je Bewegung. Beim Schreiben wird der zum
entry_dategültigevat_rates.rate_pctalsvat_rate_percentauf der Zeile eingefroren (Spalte in Dok 30 §4 ergänzt) undvat_amount_chfdaraus berechnet (beiEXEMPT: 0). Spätere Satzänderungen berühren bestehende Buchungen nicht — der historische Satz/Status bleibt rechtssicher erhalten. Das ist bewusst keine generated column und keine View-Berechnung (der Satz darf nicht „mitwandern"). -
Schweizer Sätze / Codes (
vat_rates): Normalsatz 8.1 % (STD), reduziert 2.6 % (RED), Sondersatz Beherbergung 3.8 % (SPECIAL), echte Befreiung 0 % (EXEMPT) — Default für Versand/Ausfuhr, Vorsteuer-/Import-relevant 0 % (ZERO). Die Satz-Zuordnung je Produkt/Leistung macht das Produktmodul (Modul 3); für die Fracht ist sie mit diesem Entscheid aufEXEMPTfixiert. -
Idempotenz: Bei Upsert-Buchungen (Auto-Posting) wird derselbe eingefrorene Satz/Status erneut geschrieben → gleicher
vat_amount_chf(für Versand: 0), kein Drift. -
Rundung (Hinweis): Preise werden immer auf ganze CHF aufgerundet (
ceil, Produktmodul/Modul 3). Bei der befreiten Versandleistung ist derceil-Betrag zugleich der gebuchte Betrag (keine MwSt-Herausrechnung); bei steuerbaren Leistungen wird die MWST aus dem aufgerundeten Brutto herausgerechnet. -
MWST-neutral: Interne Transfers (
CASH_TO_BANKetc.) und reine Umschichtungen tragenvat_code=NULL/vat_amount_chf=0. Einfuhrsteuer/eVV (Import) bleibt als eigenervat_code(ZERO/Vorsteuer) schema-ready 🔲.
3.9 Mehrwährung CHF + DOP (Berichtswährung CHF)
Das Ledger ist mehrwährungsfähig. Berichts- und Abschlusswährung ist immer CHF; Fremdwährungen werden zum Buchungskurs umgerechnet und in CHF gebucht.
-
currencies-Lookup (stabile ISO-Codes) — Kanonische DDL: Dok 30 §3.4 (currencies); hier nur fachlich. Stabilercode(CHF/DOP/EUR) + DE/ES/EN-Labels +symbol+is_report(genau eine = CHF) +active. Seeds: CHF (is_report=true), DOP (Dominikanische Pesos), EUR (optional). -
Pro Bewegung:
currency(DefaultCHF) +fx_rate(Kurs Originalwährung→CHF zum Buchungszeitpunkt) +total_orig/paid_orig(Originalbetrag). Die CHF-Spalten bleiben die gebuchte und berichtete Grösse:total_chf = round(total_orig * fx_rate, 2). Beicurrency='CHF'istfx_rate=1undtotal_origoptional (=total_chf). (Die Spaltencurrency/fx_ratewerden in Dok 30 §4movementsund §3.4price_list_itemsergänzt — siehe dort.) -
DOP-Bewegungen (v. a. Auslagen/Zahlungen an Lieferanten/Fahrer in der DR) tragen Originalbetrag (DOP) + Kurs + CHF-Gegenwert. Der Kurs kommt bei Revolut-Importen direkt aus Revolut (§10), bei manueller Erfassung als Eingabe/Tageskurs.
-
Reporting/Abschluss (
v_kasse,v_dashboard,v_monatskontrolle, Treuhänder-Export) rechnen ausschliesslich in CHF — die Views aggregieren*_chf. Originalwährung + Kurs sind zur Nachvollziehbarkeit (Beleg, Kursdifferenz) mitgeführt, aber nie aggregiert. -
Kursdifferenzen: Differenzen zwischen Buchungs- und Zahlungskurs (Realisationsdifferenz) werden vorerst als separate
movements(INCOME/EXPENSE, Notiz „Kursdifferenz") erfasst 🔲 — formale Behandlung mit Treuhänder zu bestätigen.
4. Kern-Workflows
A · Manuelle Bewegung erfassen (Erfassung_Digitar-Ersatz)
- Nutzer (Mariela/Marcel) wählt Vorgang + Zahlweise aus Bottom-Sheet-Selects, gibt Datum, Ref, Partei, Gesamtbetrag, Bezahlt, Notiz.
- Insert in
movements(source_type='manual').period_key,channel,cash_in/out,open_debt,statusergeben sich ausv_movements. - Toast bestätigt; Kasse-/Dashboard-Kacheln revalidieren.
- Edge-Case: Periode bereits
closed→ Insert in dieser Periode blockiert (Policy), UI zeigt „Periode gesperrt — als Nachbuchung in offener Periode erfassen?". - Edge-Case:
total < paidbeiINVOICE→ Warnung „Überzahlung"; nur mitallow_overpaymentspeicherbar.
B · Auto-Posting Depot-Zahlung (idempotent)
- Logistik/Finanz erfasst Depot-Zahlung (Modul 5/6) → Event mit
payment.id. - Edge-Function
post_movementmacht Upsert aufmovementsmitsource_type='deposit_payment', source_id=payment.id:entry_date=zahldatum,contact_id,payment_method_code,paid_chf=betrag,note="Pago depósito automático - envío DD.MM.YY"(DEPOSIT_CASH, Abono). - Kanal folgt
payment_methods.channel→ Kasse oder Bank. - F-14 — Soll/Abono-Verknüpfung: Die Abono-Bewegung wird via
payment_links(payment_id=Abono, target_id=Depot-Soll-movement, amount=amount_chf)an dieDEPOSIT_CASH-Soll-Bewegung des Depot-Auftrags (AP-3) gehängt. Der Soll wird überdeposit_payments.deposit_order_id → deposit_orders.movement_ideindeutig gefunden — nicht perreference-Match. So finden sich Soll und Abono inv_anzahlungensicher (analogINVOICE/INVOICE_PAYMENT, Workflow C).deposit_orders.status→PARTIAL/PAID. - Korrektur: Betrag im Ursprung geändert → erneutes Event → Upsert überschreibt dieselbe Zeile (kein Duplikat, dank Unique
(source_type,source_id)). - Storno: Depot-Zahlung gelöscht/storniert →
void-RPC setztvoided_atauf der gekoppelten movement (bleibt im Audit).
C · Kundenzahlung gegen Rechnung (explizite Verknüpfung)
- Mariela öffnet offene
INVOICE, klickt „Zahlung erfassen". - Insert
movements(INVOICE_PAYMENT,debtor_payment) +payment_links(payment_id, target_id=factura, amount). v_debitorenrechnet Restschuld neu; StatusPARTIAL/PAID.invoices.status-Nachführung (F-04): Ein Trigger aufpayment_links/movementssummiert die derINVOICE-Bewegung zugeordneten Zahlungen und schreibtinvoices.statuszurück:Σ ≥ invoices.total_chf→PAID,0 < Σ < total_chf→PARTIAL, sonstOPEN. Dies ist eine bewusste Ausnahme von „Status nur aus Views" (der Restschuld-Betrag bleibt View-abgeleitet). Der Rechnungsstatus dient dem Mahnwesen/Debitoren; er ist kein Auslöser der Affiliate-Provision mehr — die Provision entsteht bei Offerte-Konversion (pending) und wird fällig/freigegeben beim Tracking-DELIVEREDder Sendung (Geschäftsentscheid 2026-06-28 #19; Modul 16 §4.5).- Edge-Case: Splitzahlung auf mehrere Rechnungen → mehrere
payment_links-Zeilen, Summe ≤paid_chf. - Edge-Case: Altdaten ohne Link → Fallback
Ref+Partei-Match (View deckt beides ab).
D · Interner Transfer (Kasse↔Bank↔Twint)
- Nutzer wählt z. B.
CASH_TO_BANK, Betrag, Datum. - System erzeugt ein movement mit
transfer_group_id(optional zweites Bein für die Gegenseite). Status =INTERNAL_TRANSFER,open_debt=0. - Aus Debitoren/Kreditoren/Erfolgsrechnung ausgeschlossen (nur Kasse↔Bank-Umschichtung).
- Edge-Case: Transfer darf Kassenbestand nicht negativ machen → Soft-Warnung (kein harter Block; Bargeldbestand kann real abweichen).
E · Monatsabschluss (revisionssicher)
- Marcel/Mariela wählt Monat →
v_monatskontrollezeigt Summen + offene Posten. - Status
review, solange offene Debitoren/Kreditoren/Anzahlungen > 0 (analog ExcelRevisar pendientes). - Bei
closed: Snapshot vonopening/closing cash+bankinmonthly_closings,closed_at/bygesetzt → Periode gesperrt (Insert/Update via Policy verriegelt). - Nachbuchung: nur via „Reopen" (audit-pflichtig,
reopened_at/by) oder Buchung in offener Folgeperiode mit Korrektur-Referenz. - Export: Treuhänder-CSV/Excel + PDF-Report der Periode.
F · MWST-Buchung (scharf seit 2026-06-28; Versand befreit, inclusive nur für steuerbare Leistungen)
app_settings.vat_enabled=true,vat_method='inclusive',vat_rates.active=true. Das Erfassungs-Formular verlangtvat_code(Default je Leistung/Produkt aus Modul 3) — für die Versandleistung ist der DefaultEXEMPT(Art. 23 MWSTG, 0 %).- Versand (EXEMPT):
vat_rate_percent=0,vat_amount_chf=0— keine Herausrechnung;total_chfist der volle befreite Betrag. Steuerbare Inland-Leistung (inclusive):total_chfist der Bruttobetrag; der zumentry_dategültige Satz (z. B.STD8.1 %) wird alsvat_rate_percenteingefroren; F-18:vat_amount_chfwird im Schreibpfad (Server-Action bzw.post_movement()für quellen-erzeugte Buchungen) pertotal_chf - round(total_chf/(1+vat_rate_percent/100),2)berechnet und auf der Zeile gespeichert — nicht in einer View, keine generated column (Satz ist eingefroren). Für Upsert-Buchungen idempotent (gleicher Wert bei Re-Upsert). - Interne Transfers / MWST-neutrale Vorgänge tragen
vat_code=NULL,vat_amount_chf=0. - Edge-Case Satzwechsel: Eine künftige CH-Satzänderung wird über
vat_rates.valid_from/valid_toneu effektiv-datiert; Altbuchungen behalten ihren eingefrorenenvat_rate_percent(kein Rückwirken). Die Export-Befreiung (EXEMPT) ist davon unberührt. - Edge-Case Fremdwährung: Bei DOP-Buchungen wird zuerst auf CHF umgerechnet (
total_chf = total_orig * fx_rate); bei steuerbarer Leistung dann die MWST aus dem CHF-Brutto herausgerechnet (§3.9 + §3.8), beiEXEMPTbleibtvat_amount_chf=0.
5. UI-Screens (Mobile/Tablet-First)
Grundsatz: Tabelle ab Desktop, Karten am Handy (kein Horizontal-Scroll), Bottom-Sheet-Selects für Vorgang/Zahlweise,
inputmode="decimal"für CHF, ≥44px-Targets, Swipe für Schnellaktionen, Empty-States/Skeletons. Sidebar ablg, Bottom-Tab-Bar + Drawer am Handy. Geld-Beträge rechtsbündig, monospaced.
5.1 Finanz-Dashboard — KPI-Kacheln aus v_dashboard: Kasse-Saldo, Bank/Twint-Saldo, Debitoren offen, Kreditoren offen, Anzahlungen offen, Cashflow netto. Monatsfilter (Bottom-Sheet, Default ALL).
- Handy: 2-spaltiges Kachel-Grid, vertikal gestapelt, Sparkline pro Kachel; Monatsfilter als Chip → Bottom-Sheet.
- Tablet/Desktop: 3×2-Kachelraster + Monatsübersicht-Tabelle (
v_monatskontrolle) darunter; Recharts-Balken Eingang/Ausgang je Monat.
5.2 Kasse / Cashflow (v_kasse) — chronologisches Journal mit laufendem Saldo.
- Handy: Karten-Liste (Datum + Partei oben; Eingang grün / Ausgang rot rechts; Saldo als Sub-Zeile; Methode/Status als Badges). Swipe-left = Storno-Dialog, Swipe-right = Duplizieren. Filter-Chips (Monat, Kanal cash/bank) sticky oben.
- Tablet/Desktop: TanStack-Table (Datum, Partei, Vorgang, Zahlweise, Kanal, Eingang, Ausgang, laufender Saldo, Status, Notiz), sticky Header, Spaltenfilter, CSV-Export.
5.3 Schnellerfassung (Quick-Entry) — Floating-Action-Button „+" öffnet Bottom-Sheet-Formular.
- Reihenfolge thumb-optimiert: Vorgang (Bottom-Sheet-Select mit Codes+Labels) → Zahlweise → Betrag (
inputmode=decimal, grosses Numpad) → Datum (Default heute) → Partei (Suche, mit Anlegen-Inline) → Notiz. - Kanal-Vorschau-Badge (Kasse/Bank) erscheint live, sobald Zahlweise gewählt. „Speichern & Neu" für Serienerfassung.
5.4 Debitoren / Kreditoren (v_debitoren / v_kreditoren) — offene Posten mit Ampel.
- Handy: Karten nach Priorität gruppiert (Kreditoren: Dringend → Überfällig → Bald fällig → Offen → Bezahlt). Restschuld prominent. Swipe-right = „Zahlung erfassen" → Bottom-Sheet.
- Tablet/Desktop: Tabelle mit Status-Badge, Fälligkeit, Restschuld; Zeilen-Action „Zahlung", die
payment_linkssetzt.
5.5 Anzahlungen / Depot (v_anzahlungen) — Box-Depot-Status.
- Handy: Karten je Depot (Kunde, Box-Typ×Menge, Depot total / bezahlt / offen, Ampel). Camera-First: „Box-QR scannen" springt vom Depot-Eintrag zur Box (Modul 5).
- Desktop: Tabelle + Inline-Zahlung.
5.6 Monatsabschluss (v_monatskontrolle + monthly_closings)
- Handy: Monats-Akkordeon; offene-Posten-Checkliste; grosser Button „Periode abschliessen" (disabled bei offenen Posten, mit Begründungs-Hinweis).
- Desktop: 12-Monats-Tabelle (Eingang/Ausgang/Netto/Saldo/Debitoren/Kreditoren/Status), Abschluss- + Export-Buttons je Zeile.
5.7 Treuhänder-Export — Dialog: Zeitraum, Format (CSV/Excel/PDF), MWST-Kennzahlen (wenn aktiv), „nur abgeschlossene Perioden". Generiert Datei via Edge-Function → Supabase Storage → Download-Link.
6. Integrationen & Verbindungen zu anderen Modulen
- Geteilte Entitäten:
movements(dieses Modul = Owner),movement_types,payment_methods,vat_rates,monthly_closings,audit_log; FK aufcontacts(Modul CRM). - Auto-Posting-Eingänge (alle idempotent via
source_type/source_id, Upsert):- Offerten (4): Bei Rechnungsstellung →
INVOICE(invoice_issued); die Offerten-Annahme bucht keineRECEIVABLE(F-06, keine Doppelzählung). - Logistik/ERP (5): Rechnung →
INVOICE; Depot-Order →DEPOSIT_CASH(Soll); Box-Lifecycle berührt Finanzen nur über Depot. - Produkte & Preise (3): liefern Beträge/Depot-Sätze für die Soll-Buchungen (effektiv-datierte Preislisten);
vat_codeggf. je Produkt. - Affiliate (7): Payout bezahlt →
SUPPLIER_PAYMENT(commission_payout, F-05), Partei = Affiliate-Kontakt. - Kreditoren (F-02): Lieferantenrechnung/-zahlung sind manuelle
movements(source_type='manual'), kein Auto-Posting. - Revolut Business (§10): importierte Revolut-Transaktionen (v. a. DOP-Auslagen/-Zahlungen) →
movements(source_type='revolut', idempotent über Revolut-transaction_id), Kurs/fx_rateaus Revolut. - Zahlungen (überall): Kunden-/Depot-Zahlung →
INVOICE_PAYMENT/Depot-Abono +payment_links.
- Offerten (4): Bei Rechnungsstellung →
- Ausgänge:
v_dashboard/v_monatskontrollespeisen das Plattform-Dashboard; Audit jeder Mutation →audit_log(Modul Plattform). - Auto-Posting-Kontrakt (Gewinn ggü. Excel): operative Events erzeugen die Buchung mit Quelle;
upsert+ Unique(source_type,source_id)= eine Buchung pro Event, Korrektur überschreibt statt zu duplizieren; Quellen-Buchungen sind im Ledger read-only (nur der Ursprung wird editiert).
7. Validierungen & Edge-Cases
- Beträge ≥ 0 (DB-Check); CHF auf 2 Nachkommastellen gerundet. Bruttopreise kommen vom Produktmodul auf ganze CHF aufgerundet (
ceil, §3.8). - Mehrwährung: bei
currency<>'CHF'mussfx_rate>0undtotal_origgesetzt sein;total_chf = round(total_orig*fx_rate,2)(Konsistenz-Check). Beicurrency='CHF'giltfx_rate=1. Aggregiert wird nur in CHF (§3.9). - MWST: Versandleistung befreit (
vat_code='EXEMPT',vat_rate_percent=0,vat_amount_chf=0, keine Herausrechnung; Art. 23 MWSTG). Nur bei steuerbarer Leistung (inclusive):vat_amount_chf = total_chf - round(total_chf/(1+vat_rate_percent/100),2);vat_rate_percentwird beim Schreiben aus dem zumentry_dategültigenvat_rates-Satz eingefroren und danach nicht nachgezogen (§3.8). MWST-neutrale Zeilen (Transfers):vat_code=NULL,vat_amount_chf=0. - Revolut-Idempotenz: importierte Transaktion ist eindeutig über
(source_type='revolut', source_id=transaction_id); Re-Import überschreibt statt zu duplizieren (§10.2). - Cash-Seite eindeutig: eine Zeile erzeugt nicht gleichzeitig
cash_inundcash_out(durch disjunkte Typ-Mengen garantiert). channel='open'(OPEN): Zeile ist nicht zahlungswirksam (cash_in=cash_out=bank=0), erzeugt aberopen_debtbeiINVOICE/RECEIVABLE/DEPOSIT_CASHbzw. Kreditor beiEXPENSE/SUPPLIER_PAYMENT/SUPPLIER_DEBTmitpaid=0→ erscheint korrekt alsOPEN.- Restschuld-Match: primär über
payment_links; Fallbackreference + contact_id(is not distinct frombehandelt NULL sauber). Risiko bei leerer Ref/Partei → UI erzwingt Ref beiINVOICE/RECEIVABLE. - Überzahlung:
Σ Zahlungen > total→open_debt=0(kein Negativ), Warn-Badge „Überzahlung"; nur mitallow_overpaymentspeicherbar. - Interner Transfer: immer
open_debt=0, StatusINTERNAL_TRANSFER, aus Deb./Kred./GuV ausgeschlossen; beitransfer_groupmüssen beide Beine denselben Betrag tragen. - Idempotenz: Doppel-Event derselben Quelle → Upsert, kein Duplikat. Manuelle Zeile (
source_id=NULL) ist absichtlich nicht unique-geschützt. - Periode gesperrt: Insert/Update in
closed-Periode blockiert; Datumswechsel einer Zeile in eine gesperrte Periode ebenfalls blockiert. - Storno statt Delete: quellen-erzeugte Buchung nie hart löschen;
voidsetztvoided_at, View filtert sie aus, Audit behält sie. - Monatskey-Robustheit:
period_keyausentry_dategeneriert (MM/YYYY); kein Free-Text-Parsing nötig (Excel parste Strings — hier echtedate).
8. Compliance-/Sicherheits-Hinweise
- OR 957 / GeBüV (Revisionssicherheit):
movementsist ein unveränderbares Journal — kein hartes DELETE, Korrektur nur via Storno (voided_at) + Neubuchung;audit_logprotokolliert wer/wann/alt→neu für jede Mutation anmovementsundmonthly_closings. Abgeschlossene Perioden sind gesperrt; Reopen ist audit-pflichtig. Aufbewahrung 10 Jahre (Storage-Belege + Ledger). - Cash-Basis-Abgrenzung: Caja ist operatives Vorsystem (Einnahmen/Ausgaben + Offene Posten), kein rechtsgültiger Abschluss — der Treuhänder-Export ist die offizielle Schnittstelle; das verhindert falsche Gewissheit über „fertige Buchhaltung".
- MWST/eVV: MWST ist scharf (seit 2026-06-28, §3.8). Die Kern-Versandleistung ist echt befreit (
vat_code='EXEMPT', 0 %, Art. 23 MWSTG — Ausfuhr CH→DR; kein Aufschlag, keine Herausrechnung); nur steuerbare Inland-Leistungen laufen inclusive/brutto mit eingefrorenem Satz (vat_rate_percent) und herausgerechnetemvat_amount_chf. So erscheint der Hauptumsatz korrekt als 0 %/befreit → rechtssichere, historisch stabile Steuerkennzahlen für den Treuhänder-Export. Einfuhrsteuer/eVV als eigenermovement_type/vat_code(ZERO/Vorsteuer) bleibt vorgesehen 🔲. - Mehrwährung/Revolut: Fremdwährung (DOP via Revolut, §3.9/§10) wird zum Buchungskurs in CHF gebucht; Originalbetrag + Kurs sind je Bewegung als Beleg mitgeführt (Nachvollziehbarkeit, GeBüV). Revolut-API-Secrets nur server-seitig (Vault), Import idempotent + revisionssicher (read-only, Storno statt Delete).
- revDSG/DSG:
movementsreferenziertcontacts(Personendaten); Zugriff strikt per RLS (AFFILIATE/READONLYsehen keine fremden Finanzdaten;FAHRERnur eigene Logistik-Quellen). Exporte enthalten nur das fachlich Nötige. - Least Privilege: Auto-Posting läuft über
service_rolein Edge-Functions, nicht über Client-Keys; Clients können quellen-erzeugte Buchungen nicht fälschen oder löschen.
9. Offene Punkte
- ✅ MWST Versand befreit (entschieden 2026-06-28, Teil 3): Die Versandleistung ist MwSt-BEFREIT (echte Befreiung Art. 23 MWSTG → 0 %,
vat_code='EXEMPT'; kein Aufschlag, keine Herausrechnung,vat_amount_chf=0). Das System bleibt MwSt-fähig: steuerbare Inland-Zusatzleistungen laufen inclusive/brutto mit eingefrorenem Satz (STD8.1 %) — §3.8. Verbleibend 🔲: ob/welche konkreten Inland-Zusatzleistungen steuerbar anfallen — Satz-Zuordnung je Leistung macht Modul 3 (Fracht =EXEMPTfixiert). - 🔲 Einfuhrsteuer/eVV-Modellierung: eigener
movement_type(z. B.IMPORT_TAX) und/odervat_code=ZEROmit Vorsteuer-Abzug — finale Abbildung mit Treuhänder klären. - 🔲 „1 Dringend"-Flag (Kreditoren): im DIGITAR-Modell nicht als Spalte vorhanden; Caja braucht ein explizites
urgent-Feld/Notiz-Konvention — Quelle/Trigger bestätigen. - ✅ Treuhänder-Exportformat (Default gesetzt 2026-06-28, §11): bexio-kompatibles Buchungs-CSV (UTF-8, Semikolon, doppelte Buchung: Datum, Soll-Konto, Haben-Konto, Betrag CHF, MWST-Code, Text, Referenz) auf Basis Kontenrahmen KMU (Käfer). Verbreitetster CH-KMU-Standard, vom Treuhänder per einmaligem Spalten-Mapping einlesbar; Banana/Sage/Abacus lesen dasselbe generische CSV. Final bestätigbar (welche genauen MWST-Kennzahlen der Treuhänder verlangt) — Detail-Spec §11.
- 🔲 Anfangssalden: Excel hält
Saldo inicial caja/bancoals Konstante (B2/D2). In Caja alsmonthly_closings-Opening der ersten Periode oder als Settings-Wert? Migrationswert aus Live-Excel übernehmen. - ✅ Mehrwährung (entschieden 2026-06-28): CHF + DOP, Berichtswährung CHF;
movementsführtcurrency+fx_rate+total_orig(§3.9, Spalten via Dok 30 §4). DOP-Zahlungen über Revolut Business (§10). Verbleibend 🔲: USD-Bedarf, Kursquelle für manuelle Buchungen, formale Behandlung von Kursdifferenzen mit Treuhänder. - 🔲
INCOMEim Dropdown: Glossar/Brief führenINCOME, das Live-Erfassung_Digitar-Dropdown listet es nicht explizit — als gültigen Vorgang aufnehmen (empfohlen) bestätigen. - 🔲 Revolut-Integration (§10): welche Konten/Währungen angebunden werden, API-Tier/Verfügbarkeit, Webhook vs. Polling, MVP per CSV vs. direkt API — siehe §10.
10. Revolut-Business-Integration (DOP-Zahlungen + Multi-Währungs-Konto)
Entscheid 2026-06-28: DOP-Zahlungen (v. a. an Lieferanten, Fahrer und Auslagen in der DR) sowie das Halten von Fremdwährung laufen über ein Revolut Business-Konto. Revolut wird als Zahlungs-/Konto-Quelle in Caja integriert: Revolut-Transaktionen werden importiert und als movements ins Ledger verbucht (Auto-Posting/Abgleich), sodass die Fremdwährungs-Realität der DR sauber in der CHF-Berichtswährung (§3.9) ankommt.
10.1 Zweck & Scope
- Konten-/Zahlungsquelle für DOP (und ggf. CHF/EUR) — Revolut Business hält Multi-Währungs-Salden und führt FX selbst durch.
- Single Source bleibt das Caja-Ledger: Revolut ist die Bank,
movementsist die Buchhaltung. Jede relevante Revolut-Transaktion wird genau einmal alsmovementgebucht (idempotent), nicht doppelt. - Kein Kunden-Inkasso über Revolut im MVP — Fokus ist die Auszahlungs-/Auslagen-Seite in der DR und die Multi-Währungs-Kasse. (CH-Kundenzahlungen laufen weiter über Bank/TWINT/Bar/Karte.)
10.2 Anbindung — Revolut Business API
Revolut bietet eine Business-API für Konten, Transaktionen, FX und Zahlungen. Genutzt werden:
| Bereich | Revolut-API | Verwendung in Caja |
|---|---|---|
| Accounts | Konten + Salden je Währung | Konto-Übersicht, Soll-Abgleich der Salden |
| Transactions | Transaktionsliste (inkl. transaction_id, Betrag, Währung, Counterparty, FX-Rate, Status, Datum) | Import → movements (Auto-Posting) |
| Foreign Exchange | ausgeführte FX-Trades + Kurse | fx_rate je Bewegung übernehmen; Kursdifferenzen erkennen |
| Payments (optional, später) | Zahlung an Counterparty auslösen | Auslagen/Lieferantenzahlung aus Caja heraus anstossen 🔲 |
- Datenfluss (Import → Ledger): Revolut-Transaktion → Import (Webhook oder Polling, §10.3) → Mapping auf
movements→ Upsert mitsource_type='revolut',source_id=<Revolut transaction_id>. Damit greift derselbe Idempotenz-Vertrag wie beim übrigen Auto-Posting (unique (source_type, source_id), §3.2): erneuter Import derselben Transaktion überschreibt dieselbe Zeile, kein Duplikat. - Währung & Kurs:
currency= Revolut-Transaktionswährung (z. B.DOP),total_orig/paid_orig= Originalbetrag,fx_rate= aus Revolut übernommener Kurs zu CHF,total_chf = round(total_orig * fx_rate, 2)(§3.9). Bei reinen CHF-Transaktionenfx_rate=1. - Typ-Mapping: Abfluss (Zahlung/Auslage) →
EXPENSE/SUPPLIER_PAYMENT; Zufluss →INCOME; konto-interner FX-/Umbuchungsvorgang → interner Transfer (MWST-neutral, §3.8). Counterparty → Match aufcontacts(sonst manuell zuzuordnen). - MWST: Auf importierten DR-Auslagen wird die MWST nach derselben Inclusive-Regel behandelt (§3.8); DR-Auslagen sind in der Regel ohne CH-MWST (
vat_codeentsprechend, ggf.ZERO/Vorsteuer-Import) — konkrete Zuordnung mit Treuhänder klären 🔲.
Hinweis zum eingefrorenen Posting-Schreibpfad: Der Revolut-Import läuft über die Edge Function
revolut-syncmitservice_role(kein Client-Key), genau wie das übrige Auto-Posting (Modul 20 §8/§9).source_type='revolut'wird in Dok 30 §4 in diemovements.source_type-CHECK-Liste aufgenommen.
10.3 Import-Mechanismus (MVP-Fallback → API)
- MVP-Fallback (sofort, robust): CSV-/Statement-Import. Revolut-Kontoauszug (CSV) wird hochgeladen; der Parser legt
movementsper Upsert an (Idempotenz über die Revolut-transaction_id-Spalte des Exports, ersatzweise Hash aus Datum+Betrag+Counterparty). So ist der Mehrwährungs-Abgleich ohne API-Freischaltung lauffähig. - Ausbau: Revolut Business API.
revolut-sync(Edge Function, Cron) zieht periodisch neue Transaktionen (Polling) oder empfängt sie per Webhook; dieselbe Upsert-Logik. Webhook bevorzugt (näher an Echtzeit, weniger Quota), Polling als Fallback. - Beide Wege schreiben dieselben
movementsüber denselben Idempotenz-Schlüssel → ein späterer Wechsel CSV→API ist transparent (kein Datenbruch).
10.4 Reconciliation (Revolut ↔ Caja)
- Banksaldo-Abgleich: Revolut-Kontosaldo je Währung (aus Accounts-API/CSV) wird gegen den aus
movementsabgeleiteten Saldo derselben Quelle gestellt → Differenz-Report. Eine eigene Reconciliation-View (v_revolut_abgleich🔲) markiert offene/abweichende Posten. - Status: importierte, aber noch nicht zugeordnete Transaktionen (z. B. unbekannte Counterparty) landen in einer Abgleich-Queue zur manuellen Zuordnung (analog Kreditoren-Workflow).
- Storno/Korrektur: stornierte Revolut-Transaktionen →
voidder gekoppeltenmovement(bleibt im Audit, §8); kein hartes Löschen.
10.5 Sicherheit & Betrieb
- Secrets: Revolut-API-Credentials (Client-ID/Key bzw. OAuth-Token) liegen ausschliesslich in Env/Vault (Vercel-/Supabase-Secrets), nie im Repo und nie im Client-Bundle. Zugriff nur aus
service_role-Edge-Functions (Modul 20 §6.2/§10.2). - Least Privilege: API-Scope auf read (Accounts/Transactions/FX) im MVP; payment-initiation (write) ist ein separater, später zu aktivierender Scope (🔲, Vier-Augen-Freigabe empfohlen).
- Revisionssicherheit: Revolut-erzeugte
movementssind im Ledger read-only (nur Storno via RPC), Periodensperre gilt (§3.7/§8); jede Mutation →audit_log.
10.6 Offene Punkte (Revolut)
- 🔲 Konten/Währungen: welche Revolut-Konten (DOP, CHF, EUR?) angebunden werden und welche als „Kasse DR" gelten.
- 🔲 API-Tier/Verfügbarkeit: Revolut-Business-Plan + API-Zugang/Quota; Webhook-Verfügbarkeit im gewählten Tier.
- 🔲 Payment-Initiation: ob Auslagen/Zahlungen aus Caja heraus ausgelöst werden (write-Scope) oder nur Import (read) — MVP = nur Import.
- 🔲 Counterparty-Mapping: automatische Zuordnung Revolut-Counterparty →
contacts(Heuristik/Alias-Tabelle). - 🔲 Kursdifferenzen: formale Verbuchung der Realisationsdifferenz (§3.9) mit Treuhänder bestätigen.
11. Treuhänder-Export — Format-Default (Best Practice CH-KMU)
Entscheid 2026-06-28 (Default gesetzt, final bestätigbar): Der Treuhänder-Export erfolgt als bexio-kompatibles Buchungs-CSV auf Basis des Kontenrahmens KMU (Käfer). Caja ist Cash-Basis-Vorsystem (§1) und ersetzt nicht die formale Buchhaltung — der Export ist die Schnittstelle, über die der Treuhänder die Bewegungen in seine Software übernimmt.
11.1 Empfehlung & Begründung
| Kriterium | Entscheid |
|---|---|
| Ziel-Tool / Format | bexio-kompatibles Buchungs-CSV (generisches Soll/Haben-Journal-CSV), Kontenrahmen KMU/Käfer |
| Warum bexio als Referenz | Mit Abstand verbreitetste Cloud-Buchhaltung für CH-KMU/GmbH; Treuhänder erhalten i. d. R. kostenlosen Mandanten-Zugang; CSV-Import ordnet beliebige Spalten einmalig zu und merkt sich das Mapping → kein starres Schema nötig. |
| Warum CSV (nicht proprietär) | Ein generisches Soll/Haben-CSV ist tool-neutral: Banana (günstigste Volllösung, ~CHF 69–149/J., KMU-Kontenrahmen inkl.), Sage, Abacus und Run my Accounts lesen denselben Buchungs-CSV-Aufbau (DATEV-/CSV-Import-Standard). Wir binden uns damit nicht an ein einzelnes Tool — der Treuhänder wählt seins. |
| Pragmatik (kleine GmbH) | Kein API-Lock-in, kein kostenpflichtiger Connector im MVP: eine Edge-Function erzeugt die CSV (§5.7) → Storage → Download. Direkter bexio-API-Push (manuelle Buchungen) ist noch eingeschränkt und bleibt spätere Option 🔲. |
| Kontenrahmen | KMU/Käfer (Schweizer Standard, 8 Kontenklassen) — von allen genannten Tools unterstützt; CH-MWST-Sätze 8.1 / 2.6 / 3.8 % sind dort Standard. |
Kurz: bexio-kompatibles KMU-CSV = verbreitet, günstig, vom Treuhänder ohne Spezial-Setup einlesbar und gleichzeitig zu Banana/Sage/Abacus/Run-my-Accounts kompatibel. Der Treuhänder muss final nur das Spalten-Mapping einmal bestätigen und die genau gewünschten MWST-Kennzahlen nennen.
11.2 Exportformat (CSV-Spalten — doppelte Buchung)
UTF-8 (BOM), Trennzeichen Semikolon
;, DatumTT.MM.JJJJ, Beträge mit Punkt als Dezimaltrenner, 2 Nachkommastellen, CHF (Berichtswährung, §3.9). Eine Zeile = eine Soll/Haben-Buchung. Spalten-Reihenfolge fix; Zusatzspalten am Ende.
| Spalte | Inhalt | Quelle in Caja |
|---|---|---|
Datum | Buchungsdatum TT.MM.JJJJ | movements.entry_date |
Soll | Soll-Konto (KMU-Nr.) | aus movement_type + payment_method.channel abgeleitetes Konto-Mapping (§11.3) |
Haben | Haben-Konto (KMU-Nr.) | analog (Gegenkonto) |
Betrag | Betrag in CHF (brutto bzw. befreit) | movements.total_chf / paid_chf |
MWST-Code | Steuer-Code (z. B. UN81 Normalsatz / EXEMPT/leer bei befreit) | movements.vat_code → Treuhänder-Steuercode-Mapping |
MWST-Betrag | herausgerechnete MWST (0 bei Versand/EXEMPT) | movements.vat_amount_chf |
Text | Buchungstext | movements.reference + contact + note |
Referenz | Beleg-/Quell-Referenz | movements.reference / source_type:source_id |
Währung | Originalwährung (Info) | movements.currency |
Kurs | fx_rate (Info, CHF=1) | movements.fx_rate |
- Nur abgeschlossene Perioden exportierbar (Dialog §5.7) → revisionssicher (§8), keine offenen/driftenden Zeilen im Treuhänder-File.
- MWST-Logik bleibt erhalten: Versandleistung befreit (
EXEMPT, 0 %, Art. 23 MWSTG, §3.8) →MWST-Betrag=0; steuerbare Inland-Leistung mit eingefrorenemvat_rate_percent→ herausgerechneterMWST-Betrag. Der historisch eingefrorene Satz garantiert stabile Kennzahlen. - PDF-Begleitexport (Perioden-Report + Provisions-Abrechnungen, Modul 7 §8) ergänzt das CSV für die Belegablage.
11.3 Konto-Mapping (🔲 mit Treuhänder final)
Das fachliche Mapping movement_type/channel → KMU-Konto (z. B. Kasse 1000, Bank 1020, Debitoren 1100, Kreditoren 2000, Dienstleistungsertrag 3400/3xxx, MWST-Konten 22xx) wird als Lookup-Tabelle (account_mapping 🔲) gepflegt und vom Treuhänder einmalig bestätigt. Bis dahin liefert der Export Konto-Vorschläge nach Käfer-Standard; das Mapping ist datengetrieben und ohne Code-Änderung anpassbar.
Quellen (Best-Practice-Recherche 2026-06-28):
- bexio — Kontenrahmen KMU (Übersicht + PDF)
- bexio Support — Buchungen importieren (CSV, Spalten-Mapping)
- tools4b — DATEV- & CSV-Import für bexio
- rombro — Buchungen in bexio importieren (CSV/Excel-Vorlage)
- Banana Accounting — günstige CH-KMU-Lösung mit KMU-Kontenrahmen (Vergleich)
- treuhand-suche.ch — Vergleich Schweizer Buchhaltungssoftware (Treuhänder-Export CSV/PDF)