From 7db4707bc5b9ecc105cb8c92697771a41fb0b344 Mon Sep 17 00:00:00 2001
From: noroot
Date: Tue, 1 Sep 2026 11:36:05 +0200
Subject: [PATCH] =?UTF-8?q?feat:=20vollst=C3=A4ndige=20Firmendaten=20(Adre?=
=?UTF-8?q?sse,=20Abrechnung)=20bei=20Firmenanlage?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bei Firmenanlage (Registrierung + Betreiber-Firmenanlage) müssen jetzt
Adresse (Straße, PLZ, Ort, Land) und Abrechnungsdaten (Rechnungsemail,
optional USt-IdNr.) erfasst werden, nicht nur der Firmenname (Migration
0023). USt-IdNr. bewusst optional - Kleinunternehmer nach §19 UStG
haben keine. Neue Seite /verwaltung/firma (admin-only) zum Einsehen/
Nachtragen für bestehende Firmen. store.CreateAccount nimmt jetzt ein
AccountInput statt nur einen Namen entgegen (Signaturänderung betrifft
~20 Testaufrufe, mechanisch umgestellt). register.html/
betreiber_account_neu.html auf form-card/form-grid umgestellt.
Co-Authored-By: Claude Sonnet 5
---
CLAUDE.md | 39 +++++++++
internal/store/account.go | 59 +++++++++++--
internal/store/admin_test.go | 4 +-
internal/store/auth_test.go | 61 ++++++++++++-
.../0023_account_firmendaten.down.sql | 6 ++
.../0023_account_firmendaten.up.sql | 20 +++++
internal/store/store_test.go | 2 +-
internal/web/auth_handlers.go | 19 +++-
internal/web/betreiber_account_crud_test.go | 8 +-
internal/web/betreiber_handlers.go | 20 +++--
.../web/betreiber_werkzeug_handlers_test.go | 2 +-
internal/web/einladung_handlers_test.go | 6 +-
internal/web/fachebene_handlers_test.go | 8 +-
internal/web/firmendaten_handlers.go | 74 ++++++++++++++++
internal/web/firmendaten_handlers_test.go | 87 +++++++++++++++++++
internal/web/freigabe_handlers_test.go | 3 +-
internal/web/passwort_reset_handlers_test.go | 4 +-
internal/web/register_handlers_test.go | 2 +-
internal/web/server.go | 5 +-
internal/web/server_test.go | 63 +++++++++++---
.../web/templates/betreiber_account_neu.html | 66 +++++++++++---
internal/web/templates/firmendaten.html | 63 ++++++++++++++
internal/web/templates/layout.html | 1 +
internal/web/templates/register.html | 66 +++++++++++---
24 files changed, 620 insertions(+), 68 deletions(-)
create mode 100644 internal/store/migrations/0023_account_firmendaten.down.sql
create mode 100644 internal/store/migrations/0023_account_firmendaten.up.sql
create mode 100644 internal/web/firmendaten_handlers.go
create mode 100644 internal/web/firmendaten_handlers_test.go
create mode 100644 internal/web/templates/firmendaten.html
diff --git a/CLAUDE.md b/CLAUDE.md
index f13ad15..ba41d8c 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -679,6 +679,45 @@ Zeile mit dem aktuellen Stand, eine neu registrierte Firma ohne eigene
---
+## Firmendaten: Adresse und Abrechnung (2026-09-01, Migration 0023)
+
+Auf Nutzerwunsch: bei jeder Firmenanlage (öffentliche Registrierung
+`POST /register` UND Betreiber-Firmenanlage `POST /betreiber/accounts`)
+müssen jetzt vollständige Firmendaten erfasst werden, nicht nur der
+Name. `account` bekommt sechs neue Spalten: `strasse`, `plz`, `ort`,
+`land` (Adresse) sowie `ust_id`, `rechnungsemail` (Abrechnung).
+Pflichtfelder bei Neuanlage: Name, Straße, PLZ, Ort, Land,
+Rechnungsemail. **Bewusst optional:** USt-IdNr. — Kleinunternehmer nach
+§19 UStG haben keine, eine Pflichtangabe wäre hier fachlich falsch.
+
+`store.AccountInput` bündelt diese Felder für `CreateAccount` (Signatur
+geändert von `(ctx, name string)` auf `(ctx, AccountInput)` — betrifft
+~20 Testaufrufe, mechanisch auf `AccountInput{Name: "..."}` umgestellt)
+und die neue Methode `UpdateAccountDetails` (Name + alle Firmendaten in
+einem Aufruf, getrennt von der bestehenden `UpdateAccount`, die nur den
+Namen ändert — z. B. für die schnelle Tippfehlerkorrektur durch den
+Betreiber). Migrationsspalten sind `NOT NULL DEFAULT ''` statt einer
+harten Pflicht ohne Default: bestehende, vor dieser Migration angelegte
+Accounts haben diese Daten schlicht noch nicht, die Migration darf sie
+nicht blockieren — die Pflicht gilt nur auf Anwendungsebene für NEUE
+Firmen.
+
+**Neue Seite für bestehende Firmen:** `GET/POST /verwaltung/firma`
+(Ebene 4, admin-only, eigener Nav-Punkt "Firmendaten") — zum Einsehen
+und Nachtragen/Korrigieren der eigenen Adress-/Abrechnungsdaten, auch
+für Accounts, die vor dieser Migration entstanden sind und die Felder
+sonst dauerhaft leer hätten. `register.html`/`betreiber_account_neu.html`
+wurden dabei auf das `.form-card`/`.form-section`/`.form-grid`-Muster
+umgestellt (vorher unstylte Rohformulare) — bei sieben-plus Feldern
+sonst unübersichtlich.
+
+Live end-to-end verifiziert: beide Anlage-Wege (Registrierung UND
+Betreiber-Firmenanlage) speichern alle Felder korrekt, die neue
+Firmendaten-Seite zeigt den aktuellen Stand vorausgefüllt und
+Änderungen werden korrekt persistiert.
+
+---
+
## Row-Level-Security (2026-09-01, Migration 0021)
**Ausgangslage:** RLS-Policies wirken nie bei Postgres-Superusern, und
diff --git a/internal/store/account.go b/internal/store/account.go
index a793513..b1f7ece 100644
--- a/internal/store/account.go
+++ b/internal/store/account.go
@@ -13,30 +13,56 @@ import (
// nutzt). Jeder Antrag gehört genau einem Account. EinladungToken ist
// der Sammellink für die Mitarbeiter-Selbstanmeldung (Ebene 1,
// "Einladung annehmen") — ein Token pro Account, per Admin erneuerbar.
+// Strasse/PLZ/Ort/Land/UStID/Rechnungsemail sind die Firmen- und
+// Abrechnungsdaten (Migration 0023) — bei bestehenden, vor dieser
+// Migration angelegten Accounts können sie leer sein, siehe dort.
type Account struct {
ID string
Name string
EinladungToken string
+ Strasse string
+ PLZ string
+ Ort string
+ Land string
+ UStID string
+ Rechnungsemail string
CreatedAt time.Time
}
-const accountColumns = `id, name, einladung_token, created_at`
+// AccountInput bündelt die Firmendaten für CreateAccount/
+// UpdateAccountDetails. Name ist die einzige Pflichtangabe auf
+// Store-Ebene — welche der übrigen Felder ein Formular tatsächlich
+// verlangt (z. B. Adresse bei Neuanlage), entscheidet die Web-Schicht,
+// nicht der Store (Tests legen Accounts oft ohne vollständige
+// Firmendaten an, das ist auf Store-Ebene kein Fehler).
+type AccountInput struct {
+ Name string
+ Strasse string
+ PLZ string
+ Ort string
+ Land string
+ UStID string
+ Rechnungsemail string
+}
+
+const accountColumns = `id, name, einladung_token, strasse, plz, ort, land, ust_id, rechnungsemail, created_at`
func scanAccount(row interface {
Scan(dest ...any) error
}) (Account, error) {
var a Account
- err := row.Scan(&a.ID, &a.Name, &a.EinladungToken, &a.CreatedAt)
+ err := row.Scan(&a.ID, &a.Name, &a.EinladungToken, &a.Strasse, &a.PLZ, &a.Ort, &a.Land, &a.UStID, &a.Rechnungsemail, &a.CreatedAt)
return a, err
}
// CreateAccount legt einen neuen Mandanten an. einladung_token wird von
// der Datenbank per DEFAULT erzeugt (siehe Migration 0013).
-func (s *Store) CreateAccount(ctx context.Context, name string) (Account, error) {
+func (s *Store) CreateAccount(ctx context.Context, in AccountInput) (Account, error) {
row := s.db(ctx).QueryRow(ctx, `
- INSERT INTO account (name) VALUES ($1)
+ INSERT INTO account (name, strasse, plz, ort, land, ust_id, rechnungsemail)
+ VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING `+accountColumns,
- name,
+ in.Name, in.Strasse, in.PLZ, in.Ort, in.Land, in.UStID, in.Rechnungsemail,
)
a, err := scanAccount(row)
if err != nil {
@@ -46,7 +72,8 @@ func (s *Store) CreateAccount(ctx context.Context, name string) (Account, error)
}
// UpdateAccount benennt einen Mandanten um (z. B. Tippfehler bei der
-// Betreiber-gestützten Anlage korrigieren).
+// Betreiber-gestützten Anlage korrigieren) — ändert bewusst nur den
+// Namen, nicht die übrigen Firmendaten, siehe UpdateAccountDetails.
func (s *Store) UpdateAccount(ctx context.Context, id, name string) (Account, error) {
row := s.db(ctx).QueryRow(ctx, `UPDATE account SET name = $2 WHERE id = $1 RETURNING `+accountColumns, id, name)
a, err := scanAccount(row)
@@ -59,6 +86,26 @@ func (s *Store) UpdateAccount(ctx context.Context, id, name string) (Account, er
return a, nil
}
+// UpdateAccountDetails aktualisiert Name und Firmen-/Abrechnungsdaten
+// gemeinsam — genutzt von der Firmendaten-Seite (Ebene 4, admin), auf
+// der ein Mandant seine eigenen Angaben pflegt/nachträgt.
+func (s *Store) UpdateAccountDetails(ctx context.Context, id string, in AccountInput) (Account, error) {
+ row := s.db(ctx).QueryRow(ctx, `
+ UPDATE account SET name = $2, strasse = $3, plz = $4, ort = $5, land = $6, ust_id = $7, rechnungsemail = $8
+ WHERE id = $1
+ RETURNING `+accountColumns,
+ id, in.Name, in.Strasse, in.PLZ, in.Ort, in.Land, in.UStID, in.Rechnungsemail,
+ )
+ a, err := scanAccount(row)
+ if errors.Is(err, pgx.ErrNoRows) {
+ return Account{}, ErrNotFound
+ }
+ if err != nil {
+ return Account{}, fmt.Errorf("store: update account details: %w", err)
+ }
+ return a, nil
+}
+
// GetAccount liest einen Mandanten anhand seiner ID.
func (s *Store) GetAccount(ctx context.Context, id string) (Account, error) {
row := s.db(ctx).QueryRow(ctx, `SELECT `+accountColumns+` FROM account WHERE id = $1`, id)
diff --git a/internal/store/admin_test.go b/internal/store/admin_test.go
index 1505b39..adbcdb6 100644
--- a/internal/store/admin_test.go
+++ b/internal/store/admin_test.go
@@ -3,6 +3,8 @@ package store_test
import (
"context"
"testing"
+
+ "github.com/netcell-it/deklarix/internal/store"
)
func TestAppUserRoleAllowsBetreiber(t *testing.T) {
@@ -37,7 +39,7 @@ func TestListAccounts(t *testing.T) {
if err != nil {
t.Fatalf("ListAccounts: %v", err)
}
- acc, err := s.CreateAccount(ctx, "Neuer Mandant fuer ListAccounts")
+ acc, err := s.CreateAccount(ctx, store.AccountInput{Name: "Neuer Mandant fuer ListAccounts"})
if err != nil {
t.Fatalf("CreateAccount: %v", err)
}
diff --git a/internal/store/auth_test.go b/internal/store/auth_test.go
index d447c6a..eb5bf86 100644
--- a/internal/store/auth_test.go
+++ b/internal/store/auth_test.go
@@ -13,7 +13,7 @@ func TestAccountCRUD(t *testing.T) {
s := openTestStore(t)
ctx := context.Background()
- acc, err := s.CreateAccount(ctx, "Beispiel Agentur GmbH")
+ acc, err := s.CreateAccount(ctx, store.AccountInput{Name: "Beispiel Agentur GmbH"})
if err != nil {
t.Fatalf("CreateAccount: %v", err)
}
@@ -30,10 +30,63 @@ func TestAccountCRUD(t *testing.T) {
}
}
+func TestCreateAccountMitFirmendaten(t *testing.T) {
+ s := openTestStore(t)
+ ctx := context.Background()
+ acc, err := s.CreateAccount(ctx, store.AccountInput{
+ Name: "Vollstaendig GmbH", Strasse: "Musterstraße 1", PLZ: "12345", Ort: "Musterstadt",
+ Land: "Deutschland", UStID: "DE123456789", Rechnungsemail: "rechnung@vollstaendig.example.com",
+ })
+ if err != nil {
+ t.Fatalf("CreateAccount: %v", err)
+ }
+ if acc.Strasse != "Musterstraße 1" || acc.PLZ != "12345" || acc.Ort != "Musterstadt" ||
+ acc.Land != "Deutschland" || acc.UStID != "DE123456789" || acc.Rechnungsemail != "rechnung@vollstaendig.example.com" {
+ t.Fatalf("Account = %+v, Firmendaten unvollständig gespeichert", acc)
+ }
+
+ got, err := s.GetAccount(ctx, acc.ID)
+ if err != nil {
+ t.Fatalf("GetAccount: %v", err)
+ }
+ if got.Rechnungsemail != acc.Rechnungsemail {
+ t.Fatalf("Rechnungsemail nach GetAccount = %q, want %q", got.Rechnungsemail, acc.Rechnungsemail)
+ }
+}
+
+func TestUpdateAccountDetails(t *testing.T) {
+ s := openTestStore(t)
+ ctx := context.Background()
+ acc, err := s.CreateAccount(ctx, store.AccountInput{Name: "Alte Firma", Ort: "Altstadt"})
+ if err != nil {
+ t.Fatalf("CreateAccount: %v", err)
+ }
+
+ updated, err := s.UpdateAccountDetails(ctx, acc.ID, store.AccountInput{
+ Name: "Neue Firma", Strasse: "Neue Straße 2", PLZ: "54321", Ort: "Neustadt",
+ Land: "Österreich", UStID: "", Rechnungsemail: "buchhaltung@neue-firma.example.com",
+ })
+ if err != nil {
+ t.Fatalf("UpdateAccountDetails: %v", err)
+ }
+ if updated.Name != "Neue Firma" || updated.Ort != "Neustadt" || updated.Land != "Österreich" {
+ t.Fatalf("Account nach Update = %+v, nicht wie erwartet aktualisiert", updated)
+ }
+}
+
+func TestUpdateAccountDetailsNotFound(t *testing.T) {
+ s := openTestStore(t)
+ ctx := context.Background()
+ _, err := s.UpdateAccountDetails(ctx, "00000000-0000-0000-0000-000000000000", store.AccountInput{Name: "X"})
+ if !errors.Is(err, store.ErrNotFound) {
+ t.Fatalf("err = %v, want ErrNotFound", err)
+ }
+}
+
func TestUpdateAccount(t *testing.T) {
s := openTestStore(t)
ctx := context.Background()
- acc, err := s.CreateAccount(ctx, "Alter Name GmbH")
+ acc, err := s.CreateAccount(ctx, store.AccountInput{Name: "Alter Name GmbH"})
if err != nil {
t.Fatalf("CreateAccount: %v", err)
}
@@ -66,7 +119,7 @@ func TestUpdateAccountNotFound(t *testing.T) {
func TestGetAccountByEinladungToken(t *testing.T) {
s := openTestStore(t)
ctx := context.Background()
- acc, err := s.CreateAccount(ctx, "Beispiel Agentur GmbH")
+ acc, err := s.CreateAccount(ctx, store.AccountInput{Name: "Beispiel Agentur GmbH"})
if err != nil {
t.Fatalf("CreateAccount: %v", err)
}
@@ -88,7 +141,7 @@ func TestGetAccountByEinladungToken(t *testing.T) {
func TestRegenerateEinladungToken(t *testing.T) {
s := openTestStore(t)
ctx := context.Background()
- acc, err := s.CreateAccount(ctx, "Beispiel Agentur GmbH")
+ acc, err := s.CreateAccount(ctx, store.AccountInput{Name: "Beispiel Agentur GmbH"})
if err != nil {
t.Fatalf("CreateAccount: %v", err)
}
diff --git a/internal/store/migrations/0023_account_firmendaten.down.sql b/internal/store/migrations/0023_account_firmendaten.down.sql
new file mode 100644
index 0000000..76952b7
--- /dev/null
+++ b/internal/store/migrations/0023_account_firmendaten.down.sql
@@ -0,0 +1,6 @@
+ALTER TABLE account DROP COLUMN strasse;
+ALTER TABLE account DROP COLUMN plz;
+ALTER TABLE account DROP COLUMN ort;
+ALTER TABLE account DROP COLUMN land;
+ALTER TABLE account DROP COLUMN ust_id;
+ALTER TABLE account DROP COLUMN rechnungsemail;
diff --git a/internal/store/migrations/0023_account_firmendaten.up.sql b/internal/store/migrations/0023_account_firmendaten.up.sql
new file mode 100644
index 0000000..f354ebc
--- /dev/null
+++ b/internal/store/migrations/0023_account_firmendaten.up.sql
@@ -0,0 +1,20 @@
+-- Adress- und Abrechnungsdaten der Firma — bisher trug account nur den
+-- Namen. NOT NULL DEFAULT '' statt einer harten NOT-NULL-Pflicht ohne
+-- Default: bestehende Accounts (vor dieser Migration angelegt) haben
+-- diese Daten schlicht noch nicht, das darf die Migration nicht
+-- blockieren. Die Anwendungsschicht erzwingt Pflichtfelder nur für NEU
+-- angelegte Firmen (Registrierung, Betreiber-Firmenanlage); bestehende
+-- Firmen werden nicht rückwirkend gezwungen, sie können es über die
+-- neue Firmendaten-Seite (Ebene 4) nachtragen.
+ALTER TABLE account ADD COLUMN strasse TEXT NOT NULL DEFAULT '';
+ALTER TABLE account ADD COLUMN plz TEXT NOT NULL DEFAULT '';
+ALTER TABLE account ADD COLUMN ort TEXT NOT NULL DEFAULT '';
+ALTER TABLE account ADD COLUMN land TEXT NOT NULL DEFAULT '';
+-- Umsatzsteuer-ID ist bewusst optional (NOT NULL DEFAULT '', keine
+-- Pflicht auch bei Neuanlage) — Kleinunternehmer nach §19 UStG haben
+-- keine.
+ALTER TABLE account ADD COLUMN ust_id TEXT NOT NULL DEFAULT '';
+-- Rechnungsemail kann von der E-Mail des ersten (admin-)Logins
+-- abweichen (z. B. eine buchhaltung@-Adresse) — eigenes Feld statt
+-- Wiederverwendung der Login-E-Mail.
+ALTER TABLE account ADD COLUMN rechnungsemail TEXT NOT NULL DEFAULT '';
diff --git a/internal/store/store_test.go b/internal/store/store_test.go
index 3dcf4f6..70c5d91 100644
--- a/internal/store/store_test.go
+++ b/internal/store/store_test.go
@@ -36,7 +36,7 @@ func openTestStore(t *testing.T) *store.Store {
// testAccountID legt einen Mandanten an und liefert dessen ID.
func testAccountID(t *testing.T, s *store.Store) string {
t.Helper()
- acc, err := s.CreateAccount(context.Background(), "Test-Mandant")
+ acc, err := s.CreateAccount(context.Background(), store.AccountInput{Name: "Test-Mandant"})
if err != nil {
t.Fatalf("CreateAccount: %v", err)
}
diff --git a/internal/web/auth_handlers.go b/internal/web/auth_handlers.go
index 5761ef5..26dca0e 100644
--- a/internal/web/auth_handlers.go
+++ b/internal/web/auth_handlers.go
@@ -6,6 +6,7 @@ import (
"time"
"github.com/netcell-it/deklarix/internal/auth"
+ "github.com/netcell-it/deklarix/internal/store"
)
type authPageData struct {
@@ -46,11 +47,21 @@ func (s *Server) handleRegister(w http.ResponseWriter, r *http.Request) {
return
}
- accountName := r.FormValue("account_name")
+ accountInput := store.AccountInput{
+ Name: r.FormValue("account_name"),
+ Strasse: r.FormValue("strasse"),
+ PLZ: r.FormValue("plz"),
+ Ort: r.FormValue("ort"),
+ Land: r.FormValue("land"),
+ UStID: r.FormValue("ust_id"), // optional, siehe Migration 0023 (Kleinunternehmer)
+ Rechnungsemail: r.FormValue("rechnungsemail"),
+ }
email := r.FormValue("email")
password := r.FormValue("password")
- if accountName == "" || email == "" || password == "" {
- s.renderAuthPage(w, "register", authPageData{Title: "Registrieren", Error: "Alle Felder sind Pflicht"})
+ if accountInput.Name == "" || email == "" || password == "" ||
+ accountInput.Strasse == "" || accountInput.PLZ == "" || accountInput.Ort == "" ||
+ accountInput.Land == "" || accountInput.Rechnungsemail == "" {
+ s.renderAuthPage(w, "register", authPageData{Title: "Registrieren", Error: "Alle Felder außer USt-IdNr. sind Pflicht"})
return
}
@@ -68,7 +79,7 @@ func (s *Server) handleRegister(w http.ResponseWriter, r *http.Request) {
// RLS-geschützt) von der Policy abgelehnt.
var errMsg string
err = s.store.WithTenantScope(r.Context(), "", false, func(ctx context.Context) error {
- acc, err := s.store.CreateAccount(ctx, accountName)
+ acc, err := s.store.CreateAccount(ctx, accountInput)
if err != nil {
errMsg = "Konto konnte nicht angelegt werden"
return err
diff --git a/internal/web/betreiber_account_crud_test.go b/internal/web/betreiber_account_crud_test.go
index 370664f..d256072 100644
--- a/internal/web/betreiber_account_crud_test.go
+++ b/internal/web/betreiber_account_crud_test.go
@@ -6,6 +6,8 @@ import (
"net/url"
"strings"
"testing"
+
+ "github.com/netcell-it/deklarix/internal/store"
)
func TestBetreiberKannFirmaAnlegen(t *testing.T) {
@@ -13,9 +15,9 @@ func TestBetreiberKannFirmaAnlegen(t *testing.T) {
s := newServer(t, fs)
betreiberCookie := seedAccountWithRole(t, fs, "Deklarix Betreiber", "betreiber@example.com", "betreiber")
- resp := postForm(t, s, betreiberCookie, "/betreiber/accounts", url.Values{
+ resp := postForm(t, s, betreiberCookie, "/betreiber/accounts", mergeValues(url.Values{
"account_name": {"Neue Firma GmbH"}, "email": {"admin@neue-firma.example.com"}, "password": {"ein-langes-passwort"},
- })
+ }, firmenPflichtfelder()))
if resp.Code != http.StatusSeeOther {
t.Fatalf("status = %d, body: %s", resp.Code, resp.Body.String())
}
@@ -62,7 +64,7 @@ func TestBetreiberKannFirmaUmbenennen(t *testing.T) {
fs := newFakeStore()
s := newServer(t, fs)
betreiberCookie := seedAccountWithRole(t, fs, "Deklarix Betreiber", "betreiber@example.com", "betreiber")
- acc, err := fs.CreateAccount(context.Background(), "Alter Name")
+ acc, err := fs.CreateAccount(context.Background(), store.AccountInput{Name: "Alter Name"})
if err != nil {
t.Fatalf("CreateAccount: %v", err)
}
diff --git a/internal/web/betreiber_handlers.go b/internal/web/betreiber_handlers.go
index b802b5c..6c9586d 100644
--- a/internal/web/betreiber_handlers.go
+++ b/internal/web/betreiber_handlers.go
@@ -111,11 +111,21 @@ func (s *Server) handleBetreiberAccountCreate(w http.ResponseWriter, r *http.Req
http.Error(w, "ungültiges Formular", http.StatusBadRequest)
return
}
- accountName := r.FormValue("account_name")
+ accountInput := store.AccountInput{
+ Name: r.FormValue("account_name"),
+ Strasse: r.FormValue("strasse"),
+ PLZ: r.FormValue("plz"),
+ Ort: r.FormValue("ort"),
+ Land: r.FormValue("land"),
+ UStID: r.FormValue("ust_id"), // optional, siehe Migration 0023 (Kleinunternehmer)
+ Rechnungsemail: r.FormValue("rechnungsemail"),
+ }
email := r.FormValue("email")
password := r.FormValue("password")
- if accountName == "" || email == "" || password == "" {
- data := betreiberAccountNeuData{Title: "Firma anlegen", Nav: navFor(r), Error: "Alle Felder sind Pflicht"}
+ if accountInput.Name == "" || email == "" || password == "" ||
+ accountInput.Strasse == "" || accountInput.PLZ == "" || accountInput.Ort == "" ||
+ accountInput.Land == "" || accountInput.Rechnungsemail == "" {
+ data := betreiberAccountNeuData{Title: "Firma anlegen", Nav: navFor(r), Error: "Alle Felder außer USt-IdNr. sind Pflicht"}
if err := s.templates.ExecuteTemplate(w, "betreiber-account-neu", data); err != nil {
http.Error(w, "Seite konnte nicht gerendert werden", http.StatusInternalServerError)
}
@@ -132,7 +142,7 @@ func (s *Server) handleBetreiberAccountCreate(w http.ResponseWriter, r *http.Req
}
ctx := r.Context()
- acc, err := s.store.CreateAccount(ctx, accountName)
+ acc, err := s.store.CreateAccount(ctx, accountInput)
if err != nil {
data := betreiberAccountNeuData{Title: "Firma anlegen", Nav: navFor(r), Error: "Konto konnte nicht angelegt werden: " + err.Error()}
if err := s.templates.ExecuteTemplate(w, "betreiber-account-neu", data); err != nil {
@@ -161,7 +171,7 @@ func (s *Server) handleBetreiberAccountCreate(w http.ResponseWriter, r *http.Req
}
return
}
- if _, err := s.store.CreateAuditEntry(ctx, currentUser(r).ID, "betreiber_firma_angelegt", "account", acc.ID, accountName+" / "+email); err != nil {
+ if _, err := s.store.CreateAuditEntry(ctx, currentUser(r).ID, "betreiber_firma_angelegt", "account", acc.ID, accountInput.Name+" / "+email); err != nil {
http.Error(w, "Audit-Log konnte nicht geschrieben werden: "+err.Error(), http.StatusInternalServerError)
return
}
diff --git a/internal/web/betreiber_werkzeug_handlers_test.go b/internal/web/betreiber_werkzeug_handlers_test.go
index 7c911ea..a20bbf0 100644
--- a/internal/web/betreiber_werkzeug_handlers_test.go
+++ b/internal/web/betreiber_werkzeug_handlers_test.go
@@ -106,7 +106,7 @@ func TestBetreiberCannotEditMandantenEigenesWerkzeug(t *testing.T) {
fs := newFakeStore()
s := newServer(t, fs)
betreiberCookie := seedAccountWithRole(t, fs, "Deklarix Betreiber", "betreiber@example.com", "betreiber")
- acc, err := fs.CreateAccount(context.Background(), "Mandant mit eigenem Werkzeug")
+ acc, err := fs.CreateAccount(context.Background(), store.AccountInput{Name: "Mandant mit eigenem Werkzeug"})
if err != nil {
t.Fatalf("CreateAccount: %v", err)
}
diff --git a/internal/web/einladung_handlers_test.go b/internal/web/einladung_handlers_test.go
index 839eeda..423b2c6 100644
--- a/internal/web/einladung_handlers_test.go
+++ b/internal/web/einladung_handlers_test.go
@@ -6,12 +6,14 @@ import (
"net/url"
"strings"
"testing"
+
+ "github.com/netcell-it/deklarix/internal/store"
)
func TestEinladungFormZeigtFirmenname(t *testing.T) {
fs := newFakeStore()
s := newServer(t, fs)
- acc, err := fs.CreateAccount(context.Background(), "Beispiel GmbH")
+ acc, err := fs.CreateAccount(context.Background(), store.AccountInput{Name: "Beispiel GmbH"})
if err != nil {
t.Fatalf("CreateAccount: %v", err)
}
@@ -41,7 +43,7 @@ func TestEinladungMitUnbekanntemTokenZeigtFehler(t *testing.T) {
func TestEinladungAnnehmenLegtMitarbeiterAn(t *testing.T) {
fs := newFakeStore()
s := newServer(t, fs)
- acc, err := fs.CreateAccount(context.Background(), "Beispiel GmbH")
+ acc, err := fs.CreateAccount(context.Background(), store.AccountInput{Name: "Beispiel GmbH"})
if err != nil {
t.Fatalf("CreateAccount: %v", err)
}
diff --git a/internal/web/fachebene_handlers_test.go b/internal/web/fachebene_handlers_test.go
index 4e6c6be..bb14df5 100644
--- a/internal/web/fachebene_handlers_test.go
+++ b/internal/web/fachebene_handlers_test.go
@@ -19,7 +19,7 @@ import (
func seedFallImAccount(t *testing.T, fs *fakeStore, s *web.Server, role string) (antragID string, fachebeneCookie *http.Cookie) {
t.Helper()
ctx := context.Background()
- acc, err := fs.CreateAccount(ctx, "Fachebene-Mandant")
+ acc, err := fs.CreateAccount(ctx, store.AccountInput{Name: "Fachebene-Mandant"})
if err != nil {
t.Fatalf("CreateAccount: %v", err)
}
@@ -62,7 +62,7 @@ func TestFaelleListeShowsOpenAntraegeForAccount(t *testing.T) {
}
// Ein Verantwortlicher eines anderen Mandanten darf ihn nicht sehen.
- otherAcc, err := fs.CreateAccount(context.Background(), "Anderer Mandant")
+ otherAcc, err := fs.CreateAccount(context.Background(), store.AccountInput{Name: "Anderer Mandant"})
if err != nil {
t.Fatalf("CreateAccount: %v", err)
}
@@ -174,7 +174,7 @@ func TestFallEntscheidenRueckfrageOhneAbweichungBrauchtKeineBegruendung(t *testi
func TestFallEntscheidenGenehmigtBrauchtZulaessigesWerkzeug(t *testing.T) {
fs := newFakeStore()
- acc, err := fs.CreateAccount(context.Background(), "Mit Katalog")
+ acc, err := fs.CreateAccount(context.Background(), store.AccountInput{Name: "Mit Katalog"})
if err != nil {
t.Fatalf("CreateAccount: %v", err)
}
@@ -225,7 +225,7 @@ func TestFallDetailRejectsForeignAccount(t *testing.T) {
s := newServer(t, fs)
antragID, _ := seedFallImAccount(t, fs, s, "verantwortlicher")
- otherAcc, err := fs.CreateAccount(context.Background(), "Fremder Mandant")
+ otherAcc, err := fs.CreateAccount(context.Background(), store.AccountInput{Name: "Fremder Mandant"})
if err != nil {
t.Fatalf("CreateAccount: %v", err)
}
diff --git a/internal/web/firmendaten_handlers.go b/internal/web/firmendaten_handlers.go
new file mode 100644
index 0000000..ede9ef7
--- /dev/null
+++ b/internal/web/firmendaten_handlers.go
@@ -0,0 +1,74 @@
+// Firmendaten (Migration 0023) — Adress- und Abrechnungsdaten des
+// eigenen Mandanten einsehen/pflegen. Bei der Neuanlage (Registrierung,
+// Betreiber-Firmenanlage) sind diese Felder Pflicht; bestehende, davor
+// angelegte Accounts können sie hier nachtragen.
+package web
+
+import (
+ "net/http"
+
+ "github.com/netcell-it/deklarix/internal/store"
+)
+
+type firmendatenData struct {
+ Title string
+ Nav navData
+ Name string
+ Strasse string
+ PLZ string
+ Ort string
+ Land string
+ UStID string
+ Rechnungsemail string
+ Error string
+ Gespeichert bool
+}
+
+func (s *Server) handleFirmendaten(w http.ResponseWriter, r *http.Request) {
+ acc, err := s.store.GetAccount(r.Context(), currentUser(r).AccountID)
+ if err != nil {
+ http.Error(w, "Firmendaten konnten nicht geladen werden: "+err.Error(), http.StatusInternalServerError)
+ return
+ }
+ data := firmendatenData{
+ Title: "Firmendaten", Nav: navFor(r),
+ Name: acc.Name, Strasse: acc.Strasse, PLZ: acc.PLZ, Ort: acc.Ort,
+ Land: acc.Land, UStID: acc.UStID, Rechnungsemail: acc.Rechnungsemail,
+ Gespeichert: r.URL.Query().Get("gespeichert") == "1",
+ }
+ if err := s.templates.ExecuteTemplate(w, "firmendaten", data); err != nil {
+ http.Error(w, "Seite konnte nicht gerendert werden", http.StatusInternalServerError)
+ }
+}
+
+func (s *Server) handleFirmendatenSpeichern(w http.ResponseWriter, r *http.Request) {
+ if err := r.ParseForm(); err != nil {
+ http.Error(w, "ungültiges Formular", http.StatusBadRequest)
+ return
+ }
+ in := store.AccountInput{
+ Name: r.FormValue("account_name"),
+ Strasse: r.FormValue("strasse"),
+ PLZ: r.FormValue("plz"),
+ Ort: r.FormValue("ort"),
+ Land: r.FormValue("land"),
+ UStID: r.FormValue("ust_id"),
+ Rechnungsemail: r.FormValue("rechnungsemail"),
+ }
+ if in.Name == "" || in.Strasse == "" || in.PLZ == "" || in.Ort == "" || in.Land == "" || in.Rechnungsemail == "" {
+ data := firmendatenData{
+ Title: "Firmendaten", Nav: navFor(r), Error: "Alle Felder außer USt-IdNr. sind Pflicht",
+ Name: in.Name, Strasse: in.Strasse, PLZ: in.PLZ, Ort: in.Ort, Land: in.Land,
+ UStID: in.UStID, Rechnungsemail: in.Rechnungsemail,
+ }
+ if err := s.templates.ExecuteTemplate(w, "firmendaten", data); err != nil {
+ http.Error(w, "Seite konnte nicht gerendert werden", http.StatusInternalServerError)
+ }
+ return
+ }
+ if _, err := s.store.UpdateAccountDetails(r.Context(), currentUser(r).AccountID, in); err != nil {
+ http.Error(w, "Speichern fehlgeschlagen: "+err.Error(), http.StatusInternalServerError)
+ return
+ }
+ http.Redirect(w, r, "/verwaltung/firma?gespeichert=1", http.StatusSeeOther)
+}
diff --git a/internal/web/firmendaten_handlers_test.go b/internal/web/firmendaten_handlers_test.go
new file mode 100644
index 0000000..3a20f16
--- /dev/null
+++ b/internal/web/firmendaten_handlers_test.go
@@ -0,0 +1,87 @@
+package web_test
+
+import (
+ "context"
+ "net/http"
+ "net/url"
+ "strings"
+ "testing"
+)
+
+func TestRegisterRequiresFirmenadresse(t *testing.T) {
+ fs := newFakeStore()
+ s := newServer(t, fs)
+
+ resp := postForm(t, s, nil, "/register", url.Values{
+ "account_name": {"Unvollständig GmbH"}, "email": {"unvollstaendig@example.com"}, "password": {"ein-sicheres-passwort"},
+ // Adresse/Rechnungsemail fehlen bewusst.
+ })
+ if resp.Code != http.StatusOK {
+ t.Fatalf("status = %d, want 200 (Formular mit Fehler)", resp.Code)
+ }
+ if !strings.Contains(resp.Body.String(), "Pflicht") {
+ t.Fatalf("erwartet Validierungsfehler, body: %s", resp.Body.String())
+ }
+}
+
+func TestRegisterSpeichertFirmendaten(t *testing.T) {
+ fs := newFakeStore()
+ s := newServer(t, fs)
+
+ resp := postForm(t, s, nil, "/register", mergeValues(url.Values{
+ "account_name": {"Vollständig GmbH"}, "email": {"admin@vollstaendig.example.com"}, "password": {"ein-sicheres-passwort"},
+ }, firmenPflichtfelder()))
+ if resp.Code != http.StatusSeeOther {
+ t.Fatalf("status = %d, want 303, body: %s", resp.Code, resp.Body.String())
+ }
+
+ user, err := fs.GetUserByEmail(context.Background(), "admin@vollstaendig.example.com")
+ if err != nil {
+ t.Fatalf("GetUserByEmail: %v", err)
+ }
+ acc, err := fs.GetAccount(context.Background(), user.AccountID)
+ if err != nil {
+ t.Fatalf("GetAccount: %v", err)
+ }
+ if acc.Strasse != "Teststraße 1" || acc.PLZ != "12345" || acc.Rechnungsemail != "rechnung@example.com" {
+ t.Fatalf("Account = %+v, Firmendaten nicht wie erwartet gespeichert", acc)
+ }
+}
+
+func TestAdminKannFirmendatenBearbeiten(t *testing.T) {
+ fs := newFakeStore()
+ s := newServer(t, fs)
+ cookie := seedAccountWithRole(t, fs, "Alte Firma", "admin@example.com", "admin")
+
+ getResp := getWithCookie(t, s, cookie, "/verwaltung/firma")
+ if getResp.Code != http.StatusOK {
+ t.Fatalf("GET status = %d, want 200", getResp.Code)
+ }
+ if !strings.Contains(getResp.Body.String(), "Alte Firma") {
+ t.Fatalf("erwartet aktuellen Firmennamen im Formular, body: %s", getResp.Body.String())
+ }
+
+ saveResp := postForm(t, s, cookie, "/verwaltung/firma", url.Values{
+ "account_name": {"Neue Firma"}, "strasse": {"Neue Straße 5"}, "plz": {"99999"}, "ort": {"Neustadt"},
+ "land": {"Deutschland"}, "rechnungsemail": {"buchhaltung@neue-firma.example.com"},
+ })
+ if saveResp.Code != http.StatusSeeOther {
+ t.Fatalf("save status = %d, want 303, body: %s", saveResp.Code, saveResp.Body.String())
+ }
+
+ afterResp := getWithCookie(t, s, cookie, "/verwaltung/firma")
+ if !strings.Contains(afterResp.Body.String(), "Neue Firma") || !strings.Contains(afterResp.Body.String(), "Neustadt") {
+ t.Fatalf("gespeicherte Firmendaten fehlen, body: %s", afterResp.Body.String())
+ }
+}
+
+func TestNichtAdminKannFirmendatenNichtBearbeiten(t *testing.T) {
+ fs := newFakeStore()
+ s := newServer(t, fs)
+ cookie := seedAccountWithRole(t, fs, "Test-Mandant", "mitarbeiter@example.com", "mitarbeiter")
+
+ resp := getWithCookie(t, s, cookie, "/verwaltung/firma")
+ if resp.Code != http.StatusNotFound {
+ t.Fatalf("status = %d, want 404", resp.Code)
+ }
+}
diff --git a/internal/web/freigabe_handlers_test.go b/internal/web/freigabe_handlers_test.go
index 4518e80..e9ad2da 100644
--- a/internal/web/freigabe_handlers_test.go
+++ b/internal/web/freigabe_handlers_test.go
@@ -7,6 +7,7 @@ import (
"strings"
"testing"
+ "github.com/netcell-it/deklarix/internal/store"
"github.com/netcell-it/deklarix/internal/web"
)
@@ -32,7 +33,7 @@ func besondereKategorieAntragForm() url.Values {
func seedFreigabeSzenario(t *testing.T, fs *fakeStore, s *web.Server) (accountID, antragID string, verantwortlicherCookie *http.Cookie) {
t.Helper()
ctx := context.Background()
- acc, err := fs.CreateAccount(ctx, "Freigabe-Mandant")
+ acc, err := fs.CreateAccount(ctx, store.AccountInput{Name: "Freigabe-Mandant"})
if err != nil {
t.Fatalf("CreateAccount: %v", err)
}
diff --git a/internal/web/passwort_reset_handlers_test.go b/internal/web/passwort_reset_handlers_test.go
index 81ac036..96e2537 100644
--- a/internal/web/passwort_reset_handlers_test.go
+++ b/internal/web/passwort_reset_handlers_test.go
@@ -13,10 +13,10 @@ func TestPasswortVergessenSendetLinkUndSetztNeuesPasswort(t *testing.T) {
fs := newFakeStore()
s, fm := newServerWithMailer(t, fs)
- regResp := postForm(t, s, nil, "/register", url.Values{
+ regResp := postForm(t, s, nil, "/register", mergeValues(url.Values{
"account_name": {"Reset-Firma"}, "email": {"reset@example.com"},
"password": {"altes-passwort"},
- })
+ }, firmenPflichtfelder()))
if regResp.Code != http.StatusSeeOther {
t.Fatalf("register status = %d, want 303", regResp.Code)
}
diff --git a/internal/web/register_handlers_test.go b/internal/web/register_handlers_test.go
index 6b5a2a7..e796090 100644
--- a/internal/web/register_handlers_test.go
+++ b/internal/web/register_handlers_test.go
@@ -16,7 +16,7 @@ import (
// einen tatsächlichen Registereintrag brauchen.
func genehmigeFall(t *testing.T, fs *fakeStore, s *web.Server) (accountID string) {
t.Helper()
- acc, err := fs.CreateAccount(context.Background(), "Register-Mandant")
+ acc, err := fs.CreateAccount(context.Background(), store.AccountInput{Name: "Register-Mandant"})
if err != nil {
t.Fatalf("CreateAccount: %v", err)
}
diff --git a/internal/web/server.go b/internal/web/server.go
index e069760..dfe6b20 100644
--- a/internal/web/server.go
+++ b/internal/web/server.go
@@ -38,8 +38,9 @@ type Store interface {
WithTenantScope(ctx context.Context, accountID string, isBetreiber bool, fn func(ctx context.Context) error) error
SetTenantScope(ctx context.Context, accountID string, isBetreiber bool) error
- CreateAccount(ctx context.Context, name string) (store.Account, error)
+ CreateAccount(ctx context.Context, in store.AccountInput) (store.Account, error)
UpdateAccount(ctx context.Context, id, name string) (store.Account, error)
+ UpdateAccountDetails(ctx context.Context, id string, in store.AccountInput) (store.Account, error)
GetAccount(ctx context.Context, id string) (store.Account, error)
GetAccountByEinladungToken(ctx context.Context, token string) (store.Account, error)
RegenerateEinladungToken(ctx context.Context, accountID, newToken string) error
@@ -166,6 +167,8 @@ func NewServer(st Store, regelwerk Regelwerk, mailer mail.Mailer) (*Server, erro
mux.HandleFunc("POST /login", s.handleLogin)
mux.HandleFunc("GET /verwaltung/loeschfristen", s.requireAdmin(s.handleLoeschfristenListe))
mux.HandleFunc("POST /verwaltung/loeschfristen", s.requireAdmin(s.handleLoeschfristenSpeichern))
+ mux.HandleFunc("GET /verwaltung/firma", s.requireAdmin(s.handleFirmendaten))
+ mux.HandleFunc("POST /verwaltung/firma", s.requireAdmin(s.handleFirmendatenSpeichern))
mux.HandleFunc("GET /verwaltung/email-vorlagen", s.requireAdmin(s.handleEmailVorlagenListe))
mux.HandleFunc("GET /verwaltung/email-vorlagen/{typ}", s.requireAdmin(s.handleEmailVorlageBearbeitenForm))
mux.HandleFunc("POST /verwaltung/email-vorlagen/{typ}", s.requireAdmin(s.handleEmailVorlageSpeichern))
diff --git a/internal/web/server_test.go b/internal/web/server_test.go
index f23f9d7..030d2c5 100644
--- a/internal/web/server_test.go
+++ b/internal/web/server_test.go
@@ -149,11 +149,15 @@ func (f *fakeStore) newID() string {
return fmt.Sprintf("id-%d", f.nextID)
}
-func (f *fakeStore) CreateAccount(ctx context.Context, name string) (store.Account, error) {
+func (f *fakeStore) CreateAccount(ctx context.Context, in store.AccountInput) (store.Account, error) {
f.mu.Lock()
defer f.mu.Unlock()
id := f.newID()
- acc := store.Account{ID: id, Name: name, EinladungToken: "einladung-token-" + id, CreatedAt: time.Now()}
+ acc := store.Account{
+ ID: id, Name: in.Name, EinladungToken: "einladung-token-" + id,
+ Strasse: in.Strasse, PLZ: in.PLZ, Ort: in.Ort, Land: in.Land,
+ UStID: in.UStID, Rechnungsemail: in.Rechnungsemail, CreatedAt: time.Now(),
+ }
f.accounts[acc.ID] = acc
return acc, nil
}
@@ -170,6 +174,19 @@ func (f *fakeStore) UpdateAccount(ctx context.Context, id, name string) (store.A
return acc, nil
}
+func (f *fakeStore) UpdateAccountDetails(ctx context.Context, id string, in store.AccountInput) (store.Account, error) {
+ f.mu.Lock()
+ defer f.mu.Unlock()
+ acc, ok := f.accounts[id]
+ if !ok {
+ return store.Account{}, store.ErrNotFound
+ }
+ acc.Name, acc.Strasse, acc.PLZ, acc.Ort, acc.Land, acc.UStID, acc.Rechnungsemail =
+ in.Name, in.Strasse, in.PLZ, in.Ort, in.Land, in.UStID, in.Rechnungsemail
+ f.accounts[id] = acc
+ return acc, nil
+}
+
func (f *fakeStore) GetAccountByEinladungToken(ctx context.Context, token string) (store.Account, error) {
f.mu.Lock()
defer f.mu.Unlock()
@@ -950,7 +967,7 @@ func newServerWithMailer(t *testing.T, fs *fakeStore) (*web.Server, *mail.FakeMa
func seedAccountWithRole(t *testing.T, fs *fakeStore, accountName, email, role string) *http.Cookie {
t.Helper()
ctx := context.Background()
- acc, err := fs.CreateAccount(ctx, accountName)
+ acc, err := fs.CreateAccount(ctx, store.AccountInput{Name: accountName})
if err != nil {
t.Fatalf("CreateAccount: %v", err)
}
@@ -1010,6 +1027,28 @@ func newAuthedTestServer(t *testing.T) (*web.Server, *fakeStore, *http.Cookie) {
return s, fs, cookie
}
+// firmenPflichtfelder liefert die seit Migration 0023 bei Firmenanlage
+// (Registrierung und Betreiber-Firmenanlage) verlangten Adress-/
+// Abrechnungsfelder — gemeinsame Basis für Tests, die nicht speziell
+// deren Validierung prüfen, sonst müsste jeder Testfall sie einzeln
+// nachpflegen.
+func firmenPflichtfelder() url.Values {
+ return url.Values{
+ "strasse": {"Teststraße 1"}, "plz": {"12345"}, "ort": {"Teststadt"},
+ "land": {"Deutschland"}, "rechnungsemail": {"rechnung@example.com"},
+ }
+}
+
+// mergeValues kopiert alle Werte aus extra in base (base wird mutiert
+// und zurückgegeben) — für Tests, die ein Basisformular um wenige
+// eigene Felder ergänzen wollen.
+func mergeValues(base url.Values, extra url.Values) url.Values {
+ for k, v := range extra {
+ base[k] = v
+ }
+ return base
+}
+
func postForm(t *testing.T, s *web.Server, cookie *http.Cookie, path string, form url.Values) *httptest.ResponseRecorder {
t.Helper()
req := httptest.NewRequest(http.MethodPost, path, strings.NewReader(form.Encode()))
@@ -1071,10 +1110,10 @@ func TestRegisterThenLoginThenAccessProtectedPage(t *testing.T) {
fs := newFakeStore()
s := newServer(t, fs)
- regResp := postForm(t, s, nil, "/register", url.Values{
+ regResp := postForm(t, s, nil, "/register", mergeValues(url.Values{
"account_name": {"Meine Firma"}, "email": {"neu@example.com"},
"password": {"ein-sicheres-passwort"},
- })
+ }, firmenPflichtfelder()))
if regResp.Code != http.StatusSeeOther {
t.Fatalf("register status = %d, want 303, body: %s", regResp.Code, regResp.Body.String())
}
@@ -1101,10 +1140,10 @@ func TestRegisterSeedsStandardGenehmigerRollen(t *testing.T) {
fs := newFakeStore()
s := newServer(t, fs)
- regResp := postForm(t, s, nil, "/register", url.Values{
+ regResp := postForm(t, s, nil, "/register", mergeValues(url.Values{
"account_name": {"Meine Firma"}, "email": {"neu2@example.com"},
"password": {"ein-sicheres-passwort"},
- })
+ }, firmenPflichtfelder()))
if regResp.Code != http.StatusSeeOther {
t.Fatalf("register status = %d, want 303, body: %s", regResp.Code, regResp.Body.String())
}
@@ -1147,10 +1186,10 @@ func TestRegisterSeedsStandardGenehmigerRollen(t *testing.T) {
func TestRegisterRejectsDuplicateEmail(t *testing.T) {
fs := newFakeStore()
s := newServer(t, fs)
- form := url.Values{
+ form := mergeValues(url.Values{
"account_name": {"A"}, "email": {"doppelt@example.com"},
"password": {"ein-sicheres-passwort"},
- }
+ }, firmenPflichtfelder())
if resp := postForm(t, s, nil, "/register", form); resp.Code != http.StatusSeeOther {
t.Fatalf("first register status = %d, want 303", resp.Code)
}
@@ -1170,7 +1209,7 @@ func TestLoginWithCorrectPassword(t *testing.T) {
if err != nil {
t.Fatalf("HashPassword: %v", err)
}
- acc, err := fs.CreateAccount(context.Background(), "Bestehender Mandant")
+ acc, err := fs.CreateAccount(context.Background(), store.AccountInput{Name: "Bestehender Mandant"})
if err != nil {
t.Fatalf("CreateAccount: %v", err)
}
@@ -1194,7 +1233,7 @@ func TestLoginRedirectsBetreiberToPlatformDashboard(t *testing.T) {
if err != nil {
t.Fatalf("HashPassword: %v", err)
}
- acc, err := fs.CreateAccount(context.Background(), "Deklarix Betreiber")
+ acc, err := fs.CreateAccount(context.Background(), store.AccountInput{Name: "Deklarix Betreiber"})
if err != nil {
t.Fatalf("CreateAccount: %v", err)
}
@@ -1218,7 +1257,7 @@ func TestLoginRejectsWrongPassword(t *testing.T) {
if err != nil {
t.Fatalf("HashPassword: %v", err)
}
- acc, _ := fs.CreateAccount(context.Background(), "X")
+ acc, _ := fs.CreateAccount(context.Background(), store.AccountInput{Name: "X"})
if _, err := fs.CreateUser(context.Background(), acc.ID, "x@example.com", hash, "mitarbeiter"); err != nil {
t.Fatalf("CreateUser: %v", err)
}
diff --git a/internal/web/templates/betreiber_account_neu.html b/internal/web/templates/betreiber_account_neu.html
index 50db08c..4752b67 100644
--- a/internal/web/templates/betreiber_account_neu.html
+++ b/internal/web/templates/betreiber_account_neu.html
@@ -11,19 +11,65 @@
Ergebnis wie die öffentliche Registrierung, nur vom Betreiber aus.
{{if .Error}}{{.Error}}
{{end}}
+
+