diff --git a/deploy/input_is_valid.sql b/deploy/input_is_valid.sql new file mode 100644 index 0000000..77fdc26 --- /dev/null +++ b/deploy/input_is_valid.sql @@ -0,0 +1,23 @@ +-- Deploy camper:input_is_valid to pg +-- requires: roles +-- requires: schema_public + +begin; + +set search_path to public; + +create or replace function input_is_valid(input text, domname text) returns boolean as +$$ +begin + begin + execute format('select %L::%s', input, domname); + return true; + exception when others then + return false; + end; +end; +$$ +language plpgsql +stable; + +commit; diff --git a/deploy/input_is_valid_phone.sql b/deploy/input_is_valid_phone.sql new file mode 100644 index 0000000..cca15e4 --- /dev/null +++ b/deploy/input_is_valid_phone.sql @@ -0,0 +1,24 @@ +-- Deploy camper:input_is_valid_phone to pg +-- requires: roles +-- requires: schema_public +-- requires: extension_pg_libphonenumber + +begin; + +set search_path to public; + +create or replace function input_is_valid_phone(phone text, country text) returns boolean as +$$ +begin + begin + perform parse_packed_phone_number(phone, country); + return true; + exception when others then + return false; + end; +end; +$$ +language plpgsql +stable; + +commit; diff --git a/pkg/app/admin.go b/pkg/app/admin.go index a2ec2ce..ee2edbe 100644 --- a/pkg/app/admin.go +++ b/pkg/app/admin.go @@ -10,6 +10,7 @@ import ( "dev.tandem.ws/tandem/camper/pkg/auth" "dev.tandem.ws/tandem/camper/pkg/campsite" + "dev.tandem.ws/tandem/camper/pkg/company" "dev.tandem.ws/tandem/camper/pkg/database" httplib "dev.tandem.ws/tandem/camper/pkg/http" "dev.tandem.ws/tandem/camper/pkg/template" @@ -17,11 +18,13 @@ import ( type adminHandler struct { campsite *campsite.AdminHandler + company *company.AdminHandler } func newAdminHandler() *adminHandler { return &adminHandler{ campsite: campsite.NewAdminHandler(), + company: company.NewAdminHandler(), } } @@ -43,6 +46,8 @@ func (h *adminHandler) Handle(user *auth.User, company *auth.Company, conn *data switch head { case "campsites": h.campsite.Handler(user, company, conn).ServeHTTP(w, r) + case "company": + h.company.Handler(user, company, conn).ServeHTTP(w, r) case "": switch r.Method { case http.MethodGet: diff --git a/pkg/auth/user.go b/pkg/auth/user.go index 67dbe30..817ae76 100644 --- a/pkg/auth/user.go +++ b/pkg/auth/user.go @@ -44,3 +44,7 @@ func (user *User) IsEmployee() bool { role := user.Role[0] return role == 'e' || role == 'a' } + +func (user *User) IsAdmin() bool { + return user.Role[0] == 'a' +} diff --git a/pkg/company/admin.go b/pkg/company/admin.go new file mode 100644 index 0000000..d279aa7 --- /dev/null +++ b/pkg/company/admin.go @@ -0,0 +1,283 @@ +/* + * SPDX-FileCopyrightText: 2023 jordi fita mas + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package company + +import ( + "context" + "net/http" + + "dev.tandem.ws/tandem/camper/pkg/auth" + "dev.tandem.ws/tandem/camper/pkg/database" + "dev.tandem.ws/tandem/camper/pkg/form" + httplib "dev.tandem.ws/tandem/camper/pkg/http" + "dev.tandem.ws/tandem/camper/pkg/locale" + "dev.tandem.ws/tandem/camper/pkg/template" +) + +type AdminHandler struct { +} + +func NewAdminHandler() *AdminHandler { + return &AdminHandler{} +} + +func (h *AdminHandler) Handler(user *auth.User, company *auth.Company, conn *database.Conn) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var head string + head, r.URL.Path = httplib.ShiftPath(r.URL.Path) + + switch head { + case "": + switch r.Method { + case http.MethodGet: + f := newTaxDetailsForm(r.Context(), conn, user.Locale) + if err := f.FillFromDatabase(r.Context(), company, conn); err != nil { + panic(err) + } + f.MustRender(w, r, user, company) + case http.MethodPut: + editTaxDetails(w, r, user, company, conn) + default: + httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPut) + } + default: + http.NotFound(w, r) + } + }) +} + +type taxDetailsForm struct { + BusinessName *form.Input + VATIN *form.Input + TradeName *form.Input + Phone *form.Input + Email *form.Input + Web *form.Input + Address *form.Input + City *form.Input + Province *form.Input + PostalCode *form.Input + Country *form.Select + Currency *form.Select + DefaultLanguage *form.Select + InvoiceNumberFormat *form.Input + LegalDisclaimer *form.Input +} + +func newTaxDetailsForm(ctx context.Context, conn *database.Conn, l *locale.Locale) *taxDetailsForm { + return &taxDetailsForm{ + BusinessName: &form.Input{ + Name: "business_name", + }, + VATIN: &form.Input{ + Name: "vatin", + }, + TradeName: &form.Input{ + Name: "trade_name", + }, + Phone: &form.Input{ + Name: "phone", + }, + Email: &form.Input{ + Name: "email", + }, + Web: &form.Input{ + Name: "web", + }, + Address: &form.Input{ + Name: "address", + }, + City: &form.Input{ + Name: "city", + }, + Province: &form.Input{ + Name: "province", + }, + PostalCode: &form.Input{ + Name: "postal_code", + }, + Country: &form.Select{ + Name: "country", + Options: form.MustGetOptions(ctx, conn, "select country.country_code, coalesce(i18n.name, country.name) as l10n_name from country left join country_i18n as i18n on country.country_code = i18n.country_code and i18n.lang_tag = $1 order by l10n_name", l.Language), + }, + Currency: &form.Select{ + Name: "currency", + Options: form.MustGetOptions(ctx, conn, "select currency_code, currency_symbol from currency order by currency_code"), + }, + DefaultLanguage: &form.Select{ + Name: "default_language", + Options: form.MustGetOptions(ctx, conn, "select lang_tag, endonym from language where selectable"), + }, + InvoiceNumberFormat: &form.Input{ + Name: "invoice_number_format", + }, + LegalDisclaimer: &form.Input{ + Name: "legal_disclaimer", + }, + } +} + +func (f *taxDetailsForm) FillFromDatabase(ctx context.Context, company *auth.Company, conn *database.Conn) error { + return conn.QueryRow(ctx, ` + select business_name + , substr(vatin::text, 3) + , trade_name + , phone + , email + , web + , address + , city + , province + , postal_code + , array[country_code::text] + , array[currency_code::text] + , array[default_lang_tag] + , invoice_number_format + , legal_disclaimer + from company + where company.company_id = $1`, company.ID).Scan( + &f.BusinessName.Val, + &f.VATIN.Val, + &f.TradeName.Val, + &f.Phone.Val, + &f.Email.Val, + &f.Web.Val, + &f.Address.Val, + &f.City.Val, + &f.Province.Val, + &f.PostalCode.Val, + &f.Country.Selected, + &f.Currency.Selected, + &f.DefaultLanguage.Selected, + &f.InvoiceNumberFormat.Val, + &f.LegalDisclaimer.Val, + ) +} + +func (f *taxDetailsForm) Parse(r *http.Request) error { + if err := r.ParseForm(); err != nil { + return err + } + f.BusinessName.FillValue(r) + f.VATIN.FillValue(r) + f.TradeName.FillValue(r) + f.Phone.FillValue(r) + f.Email.FillValue(r) + f.Web.FillValue(r) + f.Address.FillValue(r) + f.City.FillValue(r) + f.Province.FillValue(r) + f.PostalCode.FillValue(r) + f.Country.FillValue(r) + f.Currency.FillValue(r) + f.DefaultLanguage.FillValue(r) + f.InvoiceNumberFormat.FillValue(r) + f.LegalDisclaimer.FillValue(r) + return nil +} + +func (f *taxDetailsForm) Valid(ctx context.Context, conn *database.Conn, l *locale.Locale) (bool, error) { + v := form.NewValidator(l) + + var country string + if v.CheckSelectedOptions(f.Country, l.GettextNoop("Selected country is not valid.")) { + country = f.Country.Selected[0] + } + + if v.CheckRequired(f.BusinessName, l.GettextNoop("Business name can not be empty.")) { + v.CheckMinLength(f.BusinessName, 2, l.GettextNoop("Business name must have at least two letters.")) + } + if v.CheckRequired(f.VATIN, l.GettextNoop("VAT number can not be empty.")) { + if _, err := v.CheckValidVATIN(ctx, conn, f.VATIN, country, l.GettextNoop("This VAT number is not valid.")); err != nil { + return false, err + } + } + if v.CheckRequired(f.Phone, l.GettextNoop("Phone can not be empty.")) { + if _, err := v.CheckValidPhone(ctx, conn, f.Phone, country, l.GettextNoop("This phone number is not valid.")); err != nil { + return false, err + } + } + if v.CheckRequired(f.Email, l.GettextNoop("Email can not be empty.")) { + v.CheckValidEmail(f.Email, l.GettextNoop("This email is not valid. It should be like name@domain.com.")) + } + if f.Web.Val != "" { + v.CheckValidURL(f.Web, l.GettextNoop("This web address is not valid. It should be like https://domain.com/.")) + } + v.CheckRequired(f.Address, l.GettextNoop("Address can not be empty.")) + v.CheckRequired(f.City, l.GettextNoop("City can not be empty.")) + v.CheckRequired(f.Province, l.GettextNoop("Province can not be empty.")) + if v.CheckRequired(f.PostalCode, l.GettextNoop("Postal code can not be empty.")) { + if _, err := v.CheckValidPostalCode(ctx, conn, f.PostalCode, country, l.GettextNoop("This postal code is not valid.")); err != nil { + return false, err + } + } + v.CheckSelectedOptions(f.Currency, l.GettextNoop("Selected currency is not valid.")) + v.CheckSelectedOptions(f.DefaultLanguage, l.GettextNoop("Selected language is not valid.")) + v.CheckRequired(f.InvoiceNumberFormat, l.GettextNoop("Invoice number format can not be empty.")) + + return v.AllOK, nil +} + +func (f *taxDetailsForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) { + template.MustRenderAdmin(w, r, user, company, "taxDetails.gohtml", f) +} + +func editTaxDetails(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) { + f := newTaxDetailsForm(r.Context(), conn, user.Locale) + if err := f.Parse(r); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if err := user.VerifyCSRFToken(r); err != nil { + http.Error(w, err.Error(), http.StatusForbidden) + return + } + if ok, err := f.Valid(r.Context(), conn, user.Locale); err != nil { + panic(err) + } else if !ok { + if !httplib.IsHTMxRequest(r) { + w.WriteHeader(http.StatusUnprocessableEntity) + } + f.MustRender(w, r, user, company) + return + } + conn.MustExec(r.Context(), ` + update company + set business_name = $1 + , vatin = ($11 || $2)::vatin + , trade_name = $3 + , phone = parse_packed_phone_number($4, $11) + , email = $5 + , web = $6 + , address = $7 + , city = $8 + , province = $9 + , postal_code = $10 + , country_code = $11 + , currency_code = $12 + , default_lang_tag = $13 + , invoice_number_format = $14 + , legal_disclaimer = $15 + where company_id = $16 + `, + f.BusinessName, + f.VATIN, + f.TradeName, + f.Phone, + f.Email, + f.Web, + f.Address, + f.City, + f.Province, + f.PostalCode, + f.Country, + f.Currency, + f.DefaultLanguage, + f.InvoiceNumberFormat, + f.LegalDisclaimer, + company.ID) + httplib.Redirect(w, r, "/admin/company", http.StatusSeeOther) +} diff --git a/pkg/database/db.go b/pkg/database/db.go index fbeba6b..abbbc96 100644 --- a/pkg/database/db.go +++ b/pkg/database/db.go @@ -86,3 +86,11 @@ func (c *Conn) MustGetText(ctx context.Context, sql string, args ...interface{}) panic(err) } } + +func (c *Conn) GetBool(ctx context.Context, sql string, args ...interface{}) (bool, error) { + var result bool + if err := c.QueryRow(ctx, sql, args...).Scan(&result); err != nil { + return false, err + } + return result, nil +} diff --git a/pkg/form/validator.go b/pkg/form/validator.go index f003c69..db9ceaa 100644 --- a/pkg/form/validator.go +++ b/pkg/form/validator.go @@ -6,9 +6,14 @@ package form import ( - "dev.tandem.ws/tandem/camper/pkg/locale" + "context" "errors" "net/mail" + "net/url" + "regexp" + + "dev.tandem.ws/tandem/camper/pkg/database" + "dev.tandem.ws/tandem/camper/pkg/locale" ) type Validator struct { @@ -27,11 +32,48 @@ func (v *Validator) CheckRequired(input *Input, message string) bool { return v.check(input, input.Val != "", message) } +func (v *Validator) CheckMinLength(input *Input, min int, message string) bool { + return v.check(input, len(input.Val) >= min, message) +} + func (v *Validator) CheckValidEmail(input *Input, message string) bool { _, err := mail.ParseAddress(input.Val) return v.check(input, err == nil, message) } +func (v *Validator) CheckValidURL(input *Input, message string) bool { + _, err := url.Parse(input.Val) + return v.check(input, err == nil, message) +} + +func (v *Validator) CheckValidVATIN(ctx context.Context, conn *database.Conn, input *Input, country string, message string) (bool, error) { + b, err := conn.GetBool(ctx, "select input_is_valid($1 || $2, 'vatin')", country, input.Val) + if err != nil { + return false, err + } + return v.check(input, b, message), nil +} + +func (v *Validator) CheckValidPhone(ctx context.Context, conn *database.Conn, input *Input, country string, message string) (bool, error) { + b, err := conn.GetBool(ctx, "select input_is_valid_phone($1, $2)", input.Val, country) + if err != nil { + return false, err + } + return v.check(input, b, message), nil +} + +func (v *Validator) CheckValidPostalCode(ctx context.Context, conn *database.Conn, input *Input, country string, message string) (bool, error) { + pattern, err := conn.GetText(ctx, "select '^' || postal_code_regex || '$' from country where country_code = $1", country) + if err != nil { + return false, err + } + match, err := regexp.MatchString(pattern, input.Val) + if err != nil { + return false, err + } + return v.check(input, match, message), nil +} + func (v *Validator) CheckPasswordConfirmation(password *Input, confirm *Input, message string) bool { return v.check(confirm, password.Val == confirm.Val, message) } diff --git a/pkg/template/render.go b/pkg/template/render.go index 859c632..846459a 100644 --- a/pkg/template/render.go +++ b/pkg/template/render.go @@ -47,6 +47,7 @@ func mustRenderLayout(w io.Writer, user *auth.User, company *auth.Company, templ "isLoggedIn": func() bool { return user.LoggedIn }, + "isAdmin": user.IsAdmin, "CSRFHeader": func() string { return fmt.Sprintf(`"%s": "%s"`, auth.CSRFTokenHeader, user.CSRFToken) }, diff --git a/po/ca.po b/po/ca.po index 540936a..53ae64f 100644 --- a/po/ca.po +++ b/po/ca.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: camper\n" "Report-Msgid-Bugs-To: jordi@tandem.blog\n" -"POT-Creation-Date: 2023-08-14 20:09+0200\n" +"POT-Creation-Date: 2023-08-15 22:22+0200\n" "PO-Revision-Date: 2023-07-22 23:45+0200\n" "Last-Translator: jordi fita mas \n" "Language-Team: Catalan \n" @@ -164,7 +164,7 @@ msgid "No campsite types added yet." msgstr "No s’ha afegit cap tipus d’allotjament encara." #: web/templates/admin/dashboard.gohtml:6 -#: web/templates/admin/dashboard.gohtml:10 web/templates/admin/layout.gohtml:44 +#: web/templates/admin/dashboard.gohtml:10 web/templates/admin/layout.gohtml:49 msgctxt "title" msgid "Dashboard" msgstr "Tauler" @@ -175,6 +175,7 @@ msgid "Login" msgstr "Entrada" #: web/templates/admin/login.gohtml:22 web/templates/admin/profile.gohtml:35 +#: web/templates/admin/taxDetails.gohtml:50 msgctxt "input" msgid "Email" msgstr "Correu-e" @@ -216,25 +217,103 @@ msgid "Language" msgstr "Idioma" #: web/templates/admin/profile.gohtml:75 +#: web/templates/admin/taxDetails.gohtml:144 msgctxt "action" msgid "Save changes" msgstr "Desa els canvis" +#: web/templates/admin/taxDetails.gohtml:6 +#: web/templates/admin/taxDetails.gohtml:12 +msgctxt "title" +msgid "Tax Details" +msgstr "Configuració fiscal" + +#: web/templates/admin/taxDetails.gohtml:17 +#: web/templates/admin/taxDetails.gohtml:58 +msgctxt "input" +msgid "Business Name" +msgstr "Nom de l’empresa" + +#: web/templates/admin/taxDetails.gohtml:26 +msgctxt "input" +msgid "VAT Number" +msgstr "NIF" + +#: web/templates/admin/taxDetails.gohtml:34 +msgctxt "input" +msgid "Trade Name" +msgstr "Nom comercial" + +#: web/templates/admin/taxDetails.gohtml:42 +msgctxt "input" +msgid "Phone" +msgstr "Telèfon" + +#: web/templates/admin/taxDetails.gohtml:66 +msgctxt "input" +msgid "Address" +msgstr "Adreça" + +#: web/templates/admin/taxDetails.gohtml:74 +msgctxt "input" +msgid "City" +msgstr "Població" + +#: web/templates/admin/taxDetails.gohtml:82 +msgctxt "input" +msgid "Province" +msgstr "Província" + +#: web/templates/admin/taxDetails.gohtml:90 +msgctxt "input" +msgid "Postal Code" +msgstr "Codi postal" + +#: web/templates/admin/taxDetails.gohtml:98 +msgctxt "input" +msgid "Country" +msgstr "País" + +#: web/templates/admin/taxDetails.gohtml:108 +msgctxt "input" +msgid "Currency" +msgstr "Moneda" + +#: web/templates/admin/taxDetails.gohtml:118 +msgctxt "input" +msgid "Default Language" +msgstr "Idioma per defecte" + +#: web/templates/admin/taxDetails.gohtml:128 +msgctxt "input" +msgid "Invoice Number Format" +msgstr "Format del número de factura" + +#: web/templates/admin/taxDetails.gohtml:136 +msgctxt "input" +msgid "Legal Disclaimer" +msgstr "Nota legal" + #: web/templates/admin/layout.gohtml:25 msgctxt "title" msgid "User Menu" msgstr "Menú d’usuari" #: web/templates/admin/layout.gohtml:33 +msgctxt "title" +msgid "Company Settings" +msgstr "Paràmetres de l’empresa" + +#: web/templates/admin/layout.gohtml:38 msgctxt "action" msgid "Logout" msgstr "Surt" -#: pkg/app/login.go:56 pkg/app/user.go:246 +#: pkg/app/login.go:56 pkg/app/user.go:246 pkg/company/admin.go:203 msgid "Email can not be empty." msgstr "No podeu deixar el correu-e en blanc." -#: pkg/app/login.go:57 pkg/app/user.go:247 +#: pkg/app/login.go:57 pkg/app/user.go:247 pkg/company/admin.go:204 msgid "This email is not valid. It should be like name@domain.com." msgstr "Aquest correu-e no és vàlid. Hauria de ser similar a nom@domini.com." @@ -259,7 +338,7 @@ msgstr "No podeu deixar el nom en blanc." msgid "Confirmation does not match password." msgstr "La confirmació no es correspon amb la contrasenya." -#: pkg/app/user.go:251 +#: pkg/app/user.go:251 pkg/company/admin.go:218 msgid "Selected language is not valid." msgstr "L’idioma escollit no és vàlid." @@ -267,7 +346,7 @@ msgstr "L’idioma escollit no és vàlid." msgid "File must be a valid PNG or JPEG image." msgstr "El fitxer has de ser una imatge PNG o JPEG vàlida." -#: pkg/app/admin.go:37 +#: pkg/app/admin.go:40 msgid "Access forbidden" msgstr "Accés prohibit" @@ -279,6 +358,66 @@ msgstr "El tipus d’allotjament escollit no és vàlid." msgid "Label can not be empty." msgstr "No podeu deixar l’etiqueta en blanc." +#: pkg/company/admin.go:186 +msgid "Selected country is not valid." +msgstr "El país escollit no és vàlid." + +#: pkg/company/admin.go:190 +msgid "Business name can not be empty." +msgstr "No podeu deixar el nom d’empresa en blanc." + +#: pkg/company/admin.go:191 +msgid "Business name must have at least two letters." +msgstr "El nom d’empresa ha de tenir com a mínim dues lletres." + +#: pkg/company/admin.go:193 +msgid "VAT number can not be empty." +msgstr "No podeu deixar el NIF en blanc." + +#: pkg/company/admin.go:194 +msgid "This VAT number is not valid." +msgstr "Aquest NIF no és vàlid." + +#: pkg/company/admin.go:198 +msgid "Phone can not be empty." +msgstr "No podeu deixar el telèfon en blanc." + +#: pkg/company/admin.go:199 +msgid "This phone number is not valid." +msgstr "Aquest número de telèfon no és vàlid." + +#: pkg/company/admin.go:207 +msgid "This web address is not valid. It should be like https://domain.com/." +msgstr "Aquesta adreça web no és vàlida. Hauria de ser similar a https://domini.com/." + +#: pkg/company/admin.go:209 +msgid "Address can not be empty." +msgstr "No podeu deixar l’adreça en blanc." + +#: pkg/company/admin.go:210 +msgid "City can not be empty." +msgstr "No podeu deixar la població en blanc." + +#: pkg/company/admin.go:211 +msgid "Province can not be empty." +msgstr "No podeu deixar la província en blanc." + +#: pkg/company/admin.go:212 +msgid "Postal code can not be empty." +msgstr "No podeu deixar el codi postal en blanc." + +#: pkg/company/admin.go:213 +msgid "This postal code is not valid." +msgstr "Aquest codi postal no és vàlid." + +#: pkg/company/admin.go:217 +msgid "Selected currency is not valid." +msgstr "La moneda escollida no és vàlida." + +#: pkg/company/admin.go:219 +msgid "Invoice number format can not be empty." +msgstr "No podeu deixar el format del número de factura en blanc." + #: pkg/auth/user.go:40 msgid "Cross-site request forgery detected." msgstr "S’ha detectat un intent de falsificació de petició a llocs creuats." @@ -313,6 +452,3 @@ msgstr "S’ha detectat un intent de falsificació de petició a llocs creuats." #~ msgid "No pages added yet." #~ msgstr "No s’ha afegit cap pàgina encara." - -#~ msgid "Title can not be empty." -#~ msgstr "No podeu deixar el títol en blanc." diff --git a/po/es.po b/po/es.po index f319b30..5f9e274 100644 --- a/po/es.po +++ b/po/es.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: camper\n" "Report-Msgid-Bugs-To: jordi@tandem.blog\n" -"POT-Creation-Date: 2023-08-14 20:09+0200\n" +"POT-Creation-Date: 2023-08-15 22:23+0200\n" "PO-Revision-Date: 2023-07-22 23:46+0200\n" "Last-Translator: jordi fita mas \n" "Language-Team: Spanish \n" @@ -164,7 +164,7 @@ msgid "No campsite types added yet." msgstr "No se ha añadido ningún tipo de alojamiento todavía." #: web/templates/admin/dashboard.gohtml:6 -#: web/templates/admin/dashboard.gohtml:10 web/templates/admin/layout.gohtml:44 +#: web/templates/admin/dashboard.gohtml:10 web/templates/admin/layout.gohtml:49 msgctxt "title" msgid "Dashboard" msgstr "Panel" @@ -175,6 +175,7 @@ msgid "Login" msgstr "Entrada" #: web/templates/admin/login.gohtml:22 web/templates/admin/profile.gohtml:35 +#: web/templates/admin/taxDetails.gohtml:50 msgctxt "input" msgid "Email" msgstr "Correo-e" @@ -216,25 +217,103 @@ msgid "Language" msgstr "Idioma" #: web/templates/admin/profile.gohtml:75 +#: web/templates/admin/taxDetails.gohtml:144 msgctxt "action" msgid "Save changes" msgstr "Guardar los cambios" +#: web/templates/admin/taxDetails.gohtml:6 +#: web/templates/admin/taxDetails.gohtml:12 +msgctxt "title" +msgid "Tax Details" +msgstr "Configuración fiscal" + +#: web/templates/admin/taxDetails.gohtml:17 +#: web/templates/admin/taxDetails.gohtml:58 +msgctxt "input" +msgid "Business Name" +msgstr "Nombre de empresa" + +#: web/templates/admin/taxDetails.gohtml:26 +msgctxt "input" +msgid "VAT Number" +msgstr "NIF" + +#: web/templates/admin/taxDetails.gohtml:34 +msgctxt "input" +msgid "Trade Name" +msgstr "Nombre comercial" + +#: web/templates/admin/taxDetails.gohtml:42 +msgctxt "input" +msgid "Phone" +msgstr "Teléfono" + +#: web/templates/admin/taxDetails.gohtml:66 +msgctxt "input" +msgid "Address" +msgstr "Dirección" + +#: web/templates/admin/taxDetails.gohtml:74 +msgctxt "input" +msgid "City" +msgstr "Población" + +#: web/templates/admin/taxDetails.gohtml:82 +msgctxt "input" +msgid "Province" +msgstr "Provincia" + +#: web/templates/admin/taxDetails.gohtml:90 +msgctxt "input" +msgid "Postal Code" +msgstr "Código postal" + +#: web/templates/admin/taxDetails.gohtml:98 +msgctxt "input" +msgid "Country" +msgstr "País" + +#: web/templates/admin/taxDetails.gohtml:108 +msgctxt "input" +msgid "Currency" +msgstr "Moneda" + +#: web/templates/admin/taxDetails.gohtml:118 +msgctxt "input" +msgid "Default Language" +msgstr "Idioma por defecto" + +#: web/templates/admin/taxDetails.gohtml:128 +msgctxt "input" +msgid "Invoice Number Format" +msgstr "Formato de número de factura" + +#: web/templates/admin/taxDetails.gohtml:136 +msgctxt "input" +msgid "Legal Disclaimer" +msgstr "Nota legal" + #: web/templates/admin/layout.gohtml:25 msgctxt "title" msgid "User Menu" msgstr "Menú de usuario" #: web/templates/admin/layout.gohtml:33 +msgctxt "title" +msgid "Company Settings" +msgstr "Parámetros de la empresa" + +#: web/templates/admin/layout.gohtml:38 msgctxt "action" msgid "Logout" msgstr "Salir" -#: pkg/app/login.go:56 pkg/app/user.go:246 +#: pkg/app/login.go:56 pkg/app/user.go:246 pkg/company/admin.go:203 msgid "Email can not be empty." msgstr "No podéis dejar el correo-e en blanco." -#: pkg/app/login.go:57 pkg/app/user.go:247 +#: pkg/app/login.go:57 pkg/app/user.go:247 pkg/company/admin.go:204 msgid "This email is not valid. It should be like name@domain.com." msgstr "Este correo-e no es válido. Tiene que ser parecido a nombre@dominio.com." @@ -259,7 +338,7 @@ msgstr "No podéis dejar el nombre en blanco." msgid "Confirmation does not match password." msgstr "La confirmación no se corresponde con la contraseña." -#: pkg/app/user.go:251 +#: pkg/app/user.go:251 pkg/company/admin.go:218 msgid "Selected language is not valid." msgstr "El idioma escogido no es válido." @@ -267,7 +346,7 @@ msgstr "El idioma escogido no es válido." msgid "File must be a valid PNG or JPEG image." msgstr "El archivo tiene que ser una imagen PNG o JPEG válida." -#: pkg/app/admin.go:37 +#: pkg/app/admin.go:40 msgid "Access forbidden" msgstr "Acceso prohibido" @@ -279,6 +358,66 @@ msgstr "El tipo de alojamiento escogido no es válido." msgid "Label can not be empty." msgstr "No podéis dejar la etiqueta en blanco." +#: pkg/company/admin.go:186 +msgid "Selected country is not valid." +msgstr "El país escogido no es válido." + +#: pkg/company/admin.go:190 +msgid "Business name can not be empty." +msgstr "No podéis dejar el nombre de empresa en blanco." + +#: pkg/company/admin.go:191 +msgid "Business name must have at least two letters." +msgstr "El nombre de la empresa tiene que tener como mínimo dos letras." + +#: pkg/company/admin.go:193 +msgid "VAT number can not be empty." +msgstr "No podéis dejar el NIF en blanco." + +#: pkg/company/admin.go:194 +msgid "This VAT number is not valid." +msgstr "Este NIF no es válido." + +#: pkg/company/admin.go:198 +msgid "Phone can not be empty." +msgstr "No podéis dejar el teléfono en blanco." + +#: pkg/company/admin.go:199 +msgid "This phone number is not valid." +msgstr "Este teléfono no es válido." + +#: pkg/company/admin.go:207 +msgid "This web address is not valid. It should be like https://domain.com/." +msgstr "Esta dirección web no es válida. Tiene que ser parecido a https://dominio.com/." + +#: pkg/company/admin.go:209 +msgid "Address can not be empty." +msgstr "No podéis dejar la dirección en blanco." + +#: pkg/company/admin.go:210 +msgid "City can not be empty." +msgstr "No podéis dejar la población en blanco." + +#: pkg/company/admin.go:211 +msgid "Province can not be empty." +msgstr "No podéis dejar la provincia en blanco." + +#: pkg/company/admin.go:212 +msgid "Postal code can not be empty." +msgstr "No podéis dejar el código postal en blanco." + +#: pkg/company/admin.go:213 +msgid "This postal code is not valid." +msgstr "Este código postal no es válido." + +#: pkg/company/admin.go:217 +msgid "Selected currency is not valid." +msgstr "La moneda escogida no es válida." + +#: pkg/company/admin.go:219 +msgid "Invoice number format can not be empty." +msgstr "No podéis dejar el formato de número de factura en blanco." + #: pkg/auth/user.go:40 msgid "Cross-site request forgery detected." msgstr "Se ha detectado un intento de falsificación de petición en sitios cruzados." @@ -313,6 +452,3 @@ msgstr "Se ha detectado un intento de falsificación de petición en sitios cruz #~ msgid "No pages added yet." #~ msgstr "No se ha añadido ninguna página todavía." - -#~ msgid "Title can not be empty." -#~ msgstr "No podéis dejar el título en blanco." diff --git a/revert/input_is_valid.sql b/revert/input_is_valid.sql new file mode 100644 index 0000000..affe24c --- /dev/null +++ b/revert/input_is_valid.sql @@ -0,0 +1,7 @@ +-- Revert camper:input_is_valid from pg + +begin; + +drop function if exists public.input_is_valid(text, text); + +commit; diff --git a/revert/input_is_valid_phone.sql b/revert/input_is_valid_phone.sql new file mode 100644 index 0000000..485c5a1 --- /dev/null +++ b/revert/input_is_valid_phone.sql @@ -0,0 +1,7 @@ +-- Revert camper:input_is_valid_phone from pg + +begin; + +drop function if exists public.input_is_valid_phone(text, text); + +commit; diff --git a/sqitch.plan b/sqitch.plan index c75ae07..46d5597 100644 --- a/sqitch.plan +++ b/sqitch.plan @@ -45,3 +45,5 @@ edit_campsite_type [roles schema_camper campsite_type company] 2023-08-07T22:21: campsite [roles schema_camper company campsite_type user_profile] 2023-08-14T10:11:51Z jordi fita mas # Add campsite relation add_campsite [roles schema_camper campsite campsite_type] 2023-08-14T17:03:23Z jordi fita mas # Add function to create campsites edit_campsite [roles schema_camper campsite] 2023-08-14T17:28:16Z jordi fita mas # Add function to update campsites +input_is_valid [roles schema_public] 2023-08-15T20:10:59Z jordi fita mas # Add function to check if an input string is valid for a domain +input_is_valid_phone [roles schema_public extension_pg_libphonenumber] 2023-08-15T20:15:01Z jordi fita mas # Add function to check if an input string is valid for the phone number domain diff --git a/test/input_is_valid.sql b/test/input_is_valid.sql new file mode 100644 index 0000000..a7e97bd --- /dev/null +++ b/test/input_is_valid.sql @@ -0,0 +1,45 @@ +-- Test input_is_valid +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(28); + +set search_path to camper, public; + +select has_function('public', 'input_is_valid', array ['text', 'text']); +select function_lang_is('public', 'input_is_valid', array ['text', 'text'], 'plpgsql'); +select function_returns('public', 'input_is_valid', array ['text', 'text'], 'boolean'); +select isnt_definer('public', 'input_is_valid', array ['text', 'text']); +select volatility_is('public', 'input_is_valid', array ['text', 'text'], 'stable'); +select function_privs_are('public', 'input_is_valid', array ['text', 'text'], 'guest', array ['EXECUTE']); +select function_privs_are('public', 'input_is_valid', array ['text', 'text'], 'employee', array ['EXECUTE']); +select function_privs_are('public', 'input_is_valid', array ['text', 'text'], 'admin', array ['EXECUTE']); +select function_privs_are('public', 'input_is_valid', array ['text', 'text'], 'authenticator', array ['EXECUTE']); + +select is( input_is_valid('123', 'integer'), true ); +select is( input_is_valid('abc', 'integer'), false ); +select is( input_is_valid('abc', 'email'), false ); +select is( input_is_valid('ESabc', 'vatin'), false ); +select is( input_is_valid('abc', 'text'), true ); +select is( input_is_valid('ES44444444A', 'vatin'), true ); +select is( input_is_valid('ES44444444A', 'text'), true ); +select is( input_is_valid('ES44444444A', 'email'), false ); +select is( input_is_valid('NL04RABO9373475770', 'text'), true ); +select is( input_is_valid('ESNL04RABO9373475770', 'vatin'), false ); +select is( input_is_valid('NL04RABO9373475770', 'email'), false ); +select is( input_is_valid('ARBNNL22', 'text'), true ); +select is( input_is_valid('ESARBNNL22', 'vatin'), false ); +select is( input_is_valid('ARBNNL22', 'email'), false ); +select is( input_is_valid('2023-05-12', 'text'), true ); +select is( input_is_valid('2023-05-12', 'date'), true ); +select is( input_is_valid('2023-05-12', 'integer'), false ); +select is( input_is_valid('', 'text'), true ); +select is( input_is_valid('', 'inexistent'), false ); + +select * +from finish(); + +rollback; diff --git a/test/input_is_valid_phone.sql b/test/input_is_valid_phone.sql new file mode 100644 index 0000000..4ec77fd --- /dev/null +++ b/test/input_is_valid_phone.sql @@ -0,0 +1,32 @@ +-- Test input_is_valid_phone +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + + +select plan(12); + +set search_path to camper, public; + +select has_function('public', 'input_is_valid_phone', array ['text', 'text']); +select function_lang_is('public', 'input_is_valid_phone', array ['text', 'text'], 'plpgsql'); +select function_returns('public', 'input_is_valid_phone', array ['text', 'text'], 'boolean'); +select isnt_definer('public', 'input_is_valid_phone', array ['text', 'text']); +select volatility_is('public', 'input_is_valid_phone', array ['text', 'text'], 'stable'); +select function_privs_are('public', 'input_is_valid_phone', array ['text', 'text'], 'guest', array ['EXECUTE']); +select function_privs_are('public', 'input_is_valid_phone', array ['text', 'text'], 'employee', array ['EXECUTE']); +select function_privs_are('public', 'input_is_valid_phone', array ['text', 'text'], 'admin', array ['EXECUTE']); +select function_privs_are('public', 'input_is_valid_phone', array ['text', 'text'], 'authenticator', array ['EXECUTE']); + + +select is( input_is_valid_phone('555-555-5555', 'US'), true ); +select is( input_is_valid_phone('555-555-5555555555', 'US'), false ); +select is( input_is_valid_phone('555-555-55555555555', 'US'), false ); + + +select * +from finish(); + +rollback; diff --git a/verify/input_is_valid.sql b/verify/input_is_valid.sql new file mode 100644 index 0000000..b4e2492 --- /dev/null +++ b/verify/input_is_valid.sql @@ -0,0 +1,7 @@ +-- Verify camper:input_is_valid on pg + +begin; + +select has_function_privilege('public.input_is_valid(text, text)', 'execute'); + +rollback; diff --git a/verify/input_is_valid_phone.sql b/verify/input_is_valid_phone.sql new file mode 100644 index 0000000..3afed06 --- /dev/null +++ b/verify/input_is_valid_phone.sql @@ -0,0 +1,7 @@ +-- Verify camper:input_is_valid_phone on pg + +begin; + +select has_function_privilege('public.input_is_valid_phone(text, text)', 'execute'); + +rollback; diff --git a/web/templates/admin/layout.gohtml b/web/templates/admin/layout.gohtml index ae46f6e..5920538 100644 --- a/web/templates/admin/layout.gohtml +++ b/web/templates/admin/layout.gohtml @@ -28,6 +28,11 @@
  • {{( pgettext "Profile" "title" )}}
  • + {{ if isAdmin -}} +
  • + {{( pgettext "Company Settings" "title" )}} +
  • + {{- end }}
  • diff --git a/web/templates/admin/taxDetails.gohtml b/web/templates/admin/taxDetails.gohtml new file mode 100644 index 0000000..2405687 --- /dev/null +++ b/web/templates/admin/taxDetails.gohtml @@ -0,0 +1,147 @@ + +{{ define "title" -}} + {{( pgettext "Tax Details" "title" )}} +{{- end }} + +{{ define "content" -}} + {{- /*gotype: dev.tandem.ws/tandem/camper/pkg/company.taxDetailsForm*/ -}} +
    +

    {{( pgettext "Tax Details" "title" )}}

    + {{ CSRFInput }} +
    + {{ with .BusinessName -}} + + {{ template "error-message" . }} + {{- end }} + {{ with .VATIN -}} + + {{ template "error-message" . }} + {{- end }} + {{ with .TradeName -}} + + {{ template "error-message" . }} + {{- end }} + {{ with .Phone -}} + + {{ template "error-message" . }} + {{- end }} + {{ with .Email -}} + + {{ template "error-message" . }} + {{- end }} + {{ with .Web -}} + + {{ template "error-message" . }} + {{- end }} + {{ with .Address -}} + + {{ template "error-message" . }} + {{- end }} + {{ with .City -}} + + {{ template "error-message" . }} + {{- end }} + {{ with .Province -}} + + {{ template "error-message" . }} + {{- end }} + {{ with .PostalCode -}} + + {{ template "error-message" . }} + {{- end }} + {{ with .Country -}} + + {{ template "error-message" . }} + {{- end }} + {{ with .Currency -}} + + {{ template "error-message" . }} + {{- end }} + {{ with .DefaultLanguage -}} + + {{ template "error-message" . }} + {{- end }} + {{ with .InvoiceNumberFormat -}} + + {{ template "error-message" . }} + {{- end }} + {{ with .LegalDisclaimer -}} + + {{ template "error-message" . }} + {{- end }} +
    + +
    +{{- end }}