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:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
74
internal/web/firmendaten_handlers.go
Normal file
74
internal/web/firmendaten_handlers.go
Normal 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)
|
||||
}
|
||||
87
internal/web/firmendaten_handlers_test.go
Normal file
87
internal/web/firmendaten_handlers_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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}}
|
||||
|
||||
63
internal/web/templates/firmendaten.html
Normal file
63
internal/web/templates/firmendaten.html
Normal 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}}
|
||||
@@ -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}}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user