jordi fita mas 50fbfce9ee Add the form to update company’s tax details
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.
2023-08-15 22:35:21 +02:00

284 lines
8.2 KiB
Go

/*
* SPDX-FileCopyrightText: 2023 jordi fita mas <jfita@peritasoft.com>
* 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)
}