/* * 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 RTCNumber *form.Input TouristTax *form.Input 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), }, RTCNumber: &form.Input{ Name: "rtc_number", }, TouristTax: &form.Input{ Name: "tourist_tax", }, 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 , rtc_number , to_price(tourist_tax) , 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.RTCNumber.Val, &f.TouristTax.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.RTCNumber.FillValue(r) f.TouristTax.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.CheckRequired(f.RTCNumber, l.GettextNoop("RTC number can not be empty.")) if v.CheckRequired(f.TouristTax, l.GettextNoop("Tourist tax can not be empty.")) { if v.CheckValidDecimal(f.TouristTax, l.GettextNoop("Tourist tax must be a decimal number.")) { v.CheckMinDecimal(f.TouristTax, 0.0, l.GettextNoop("Tourist tax must be zero or greater.")) } } 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 , rtc_number = $16 , tourist_tax = parse_price($17) where company_id = $18 `, 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, f.RTCNumber, f.TouristTax, company.ID) httplib.Redirect(w, r, "/admin/company", http.StatusSeeOther) }