From 50fbfce9eee0e128aace65037d77b9b35d195812 Mon Sep 17 00:00:00 2001 From: jordi fita mas Date: Tue, 15 Aug 2023 22:35:21 +0200 Subject: [PATCH] =?UTF-8?q?Add=20the=20form=20to=20update=20company?= =?UTF-8?q?=E2=80=99s=20tax=20details?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It is inside the “user menu” only because this is where Numerus has the same option, although it makes less sense in this case, because Numerus is geared toward individual freelancers while Camper is for companies. But, since it is easy to change afterward, this will do for now. However, it should be only shown to admin users, because regular employees have no UPDATE privilege on the company relation. Thus, the need for a new template function to check if the user is admin. Part of #17. --- deploy/input_is_valid.sql | 23 +++ deploy/input_is_valid_phone.sql | 24 +++ pkg/app/admin.go | 5 + pkg/auth/user.go | 4 + pkg/company/admin.go | 283 ++++++++++++++++++++++++++ pkg/database/db.go | 8 + pkg/form/validator.go | 44 +++- pkg/template/render.go | 1 + po/ca.po | 154 +++++++++++++- po/es.po | 154 +++++++++++++- revert/input_is_valid.sql | 7 + revert/input_is_valid_phone.sql | 7 + sqitch.plan | 2 + test/input_is_valid.sql | 45 ++++ test/input_is_valid_phone.sql | 32 +++ verify/input_is_valid.sql | 7 + verify/input_is_valid_phone.sql | 7 + web/templates/admin/layout.gohtml | 5 + web/templates/admin/taxDetails.gohtml | 147 +++++++++++++ 19 files changed, 940 insertions(+), 19 deletions(-) create mode 100644 deploy/input_is_valid.sql create mode 100644 deploy/input_is_valid_phone.sql create mode 100644 pkg/company/admin.go create mode 100644 revert/input_is_valid.sql create mode 100644 revert/input_is_valid_phone.sql create mode 100644 test/input_is_valid.sql create mode 100644 test/input_is_valid_phone.sql create mode 100644 verify/input_is_valid.sql create mode 100644 verify/input_is_valid_phone.sql create mode 100644 web/templates/admin/taxDetails.gohtml 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 }}