feat: vollständige Firmendaten (Adresse, Abrechnung) bei Firmenanlage

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 <noreply@anthropic.com>
This commit is contained in:
noroot
2026-09-01 11:36:05 +02:00
parent fa5e68c892
commit 7db4707bc5
24 changed files with 620 additions and 68 deletions

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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)
}
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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))

View File

@@ -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)
}

View File

@@ -11,19 +11,65 @@
Ergebnis wie die öffentliche Registrierung, nur vom Betreiber aus.
</p>
{{if .Error}}<p class="fehler">{{.Error}}</p>{{end}}
<div class="form-card">
<form method="post" action="/betreiber/accounts">
<label for="account_name">Firmenname</label>
<input type="text" id="account_name" name="account_name" required>
<label for="email">E-Mail (erster Nutzer, wird Admin)</label>
<input type="email" id="email" name="email" required>
<label for="password">Initialpasswort (mind. 8 Zeichen)</label>
<input type="password" id="password" name="password" minlength="8" required>
<button type="submit">Firma anlegen</button>
<fieldset class="form-section">
<legend>Firma</legend>
<div class="form-grid">
<div class="form-full">
<label for="account_name">Firmenname</label>
<input type="text" id="account_name" name="account_name" required>
</div>
<div class="form-full">
<label for="strasse">Straße und Hausnummer</label>
<input type="text" id="strasse" name="strasse" required>
</div>
<div>
<label for="plz">PLZ</label>
<input type="text" id="plz" name="plz" required>
</div>
<div>
<label for="ort">Ort</label>
<input type="text" id="ort" name="ort" required>
</div>
<div>
<label for="land">Land</label>
<input type="text" id="land" name="land" value="Deutschland" required>
</div>
</div>
</fieldset>
<fieldset class="form-section">
<legend>Abrechnung</legend>
<div class="form-grid">
<div>
<label for="rechnungsemail">Rechnungsemail</label>
<input type="email" id="rechnungsemail" name="rechnungsemail" required>
</div>
<div>
<label for="ust_id">USt-IdNr. (optional)</label>
<input type="text" id="ust_id" name="ust_id" placeholder="z. B. DE123456789">
</div>
</div>
</fieldset>
<fieldset class="form-section">
<legend>Erster Nutzer (wird Admin)</legend>
<div class="form-grid">
<div>
<label for="email">E-Mail</label>
<input type="email" id="email" name="email" required>
</div>
<div>
<label for="password">Initialpasswort (mind. 8 Zeichen)</label>
<input type="password" id="password" name="password" minlength="8" required>
</div>
</div>
</fieldset>
<div class="form-actions">
<button type="submit">Firma anlegen</button>
</div>
</form>
</div>
</div>
</body>
</html>
{{end}}

View File

@@ -0,0 +1,63 @@
{{define "firmendaten"}}<!doctype html>
<html lang="de">
<head>{{template "head" .}}</head>
<body>
{{template "nav" .Nav}}
<div class="page">
<div class="page-header">
<div>
<h1>Firmendaten</h1>
<p class="hinweis">Adress- und Abrechnungsdaten deiner Firma.</p>
</div>
</div>
{{if .Error}}<p class="fehler">{{.Error}}</p>{{end}}
{{if .Gespeichert}}<p class="hinweis">Gespeichert.</p>{{end}}
<div class="form-card">
<form method="post" action="/verwaltung/firma">
<fieldset class="form-section">
<legend>Firma</legend>
<div class="form-grid">
<div class="form-full">
<label for="account_name">Firmenname</label>
<input type="text" id="account_name" name="account_name" value="{{.Name}}" required>
</div>
<div class="form-full">
<label for="strasse">Straße und Hausnummer</label>
<input type="text" id="strasse" name="strasse" value="{{.Strasse}}" required>
</div>
<div>
<label for="plz">PLZ</label>
<input type="text" id="plz" name="plz" value="{{.PLZ}}" required>
</div>
<div>
<label for="ort">Ort</label>
<input type="text" id="ort" name="ort" value="{{.Ort}}" required>
</div>
<div>
<label for="land">Land</label>
<input type="text" id="land" name="land" value="{{.Land}}" required>
</div>
</div>
</fieldset>
<fieldset class="form-section">
<legend>Abrechnung</legend>
<div class="form-grid">
<div>
<label for="rechnungsemail">Rechnungsemail</label>
<input type="email" id="rechnungsemail" name="rechnungsemail" value="{{.Rechnungsemail}}" required>
</div>
<div>
<label for="ust_id">USt-IdNr. (optional)</label>
<input type="text" id="ust_id" name="ust_id" value="{{.UStID}}" placeholder="z. B. DE123456789">
</div>
</div>
</fieldset>
<div class="form-actions">
<button type="submit">Speichern</button>
</div>
</form>
</div>
</div>
</body>
</html>
{{end}}

View File

@@ -28,6 +28,7 @@
{{if .IsFachebene}}<a href="/faelle">Posteingang</a>{{end}}
{{if .IsFachebene}}<a href="/registereintraege">Register</a>{{end}}
{{if .IsFachebene}}<a href="/wiedervorlage">Wiedervorlage</a>{{end}}
{{if .IsAdmin}}<a href="/verwaltung/firma">Firmendaten</a>{{end}}
{{if .IsAdmin}}<a href="/verwaltung/nutzer">Nutzerverwaltung</a>{{end}}
{{if .IsAdmin}}<a href="/verwaltung/einladung">Einladungslink</a>{{end}}
{{if .IsAdmin}}<a href="/verwaltung/abteilungen">Abteilungen</a>{{end}}

View File

@@ -9,18 +9,64 @@
den Betreiber freigeschaltet.
</p>
{{if .Error}}<p class="fehler">{{.Error}}</p>{{end}}
<div class="form-card">
<form method="post" action="/register">
<label for="account_name">Firmenname</label>
<input type="text" id="account_name" name="account_name" required>
<label for="email">E-Mail (erster Nutzer, wird Admin)</label>
<input type="email" id="email" name="email" required>
<label for="password">Passwort (mind. 8 Zeichen)</label>
<input type="password" id="password" name="password" minlength="8" required>
<button type="submit">Konto anlegen</button>
<fieldset class="form-section">
<legend>Firma</legend>
<div class="form-grid">
<div class="form-full">
<label for="account_name">Firmenname</label>
<input type="text" id="account_name" name="account_name" required>
</div>
<div class="form-full">
<label for="strasse">Straße und Hausnummer</label>
<input type="text" id="strasse" name="strasse" required>
</div>
<div>
<label for="plz">PLZ</label>
<input type="text" id="plz" name="plz" required>
</div>
<div>
<label for="ort">Ort</label>
<input type="text" id="ort" name="ort" required>
</div>
<div>
<label for="land">Land</label>
<input type="text" id="land" name="land" value="Deutschland" required>
</div>
</div>
</fieldset>
<fieldset class="form-section">
<legend>Abrechnung</legend>
<div class="form-grid">
<div>
<label for="rechnungsemail">Rechnungsemail</label>
<input type="email" id="rechnungsemail" name="rechnungsemail" required>
</div>
<div>
<label for="ust_id">USt-IdNr. (optional)</label>
<input type="text" id="ust_id" name="ust_id" placeholder="z. B. DE123456789">
</div>
</div>
</fieldset>
<fieldset class="form-section">
<legend>Erster Nutzer (wird Admin)</legend>
<div class="form-grid">
<div>
<label for="email">E-Mail</label>
<input type="email" id="email" name="email" required>
</div>
<div>
<label for="password">Passwort (mind. 8 Zeichen)</label>
<input type="password" id="password" name="password" minlength="8" required>
</div>
</div>
</fieldset>
<div class="form-actions">
<button type="submit">Konto anlegen</button>
</div>
</form>
</div>
<p><a href="/login">Schon ein Konto? Anmelden</a></p>
</div>
</body>