numerus/pkg/company.go

282 lines
7.8 KiB
Go
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package pkg
import (
"context"
"errors"
"html/template"
"net/http"
"net/url"
"strconv"
"strings"
)
const (
ContextCompanyKey = "numerus-company"
)
type Company struct {
Id int
Slug string
}
func CompanyHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
slug := r.URL.Path
if idx := strings.IndexByte(slug, '/'); idx >= 0 {
slug = slug[:idx]
}
conn := getConn(r)
company := &Company{
Slug: slug,
}
err := conn.QueryRow(r.Context(), "select company_id from company where slug = $1", slug).Scan(&company.Id)
if err != nil {
http.NotFound(w, r)
return
}
ctx := context.WithValue(r.Context(), ContextCompanyKey, company)
r = r.WithContext(ctx)
// Same as StripPrefix
p := strings.TrimPrefix(r.URL.Path, slug)
rp := strings.TrimPrefix(r.URL.RawPath, slug)
if len(p) < len(r.URL.Path) && (r.URL.RawPath == "" || len(rp) < len(r.URL.RawPath)) {
r2 := new(http.Request)
*r2 = *r
r2.URL = new(url.URL)
*r2.URL = *r.URL
if p == "" {
r2.URL.Path = "/"
} else {
r2.URL.Path = p
}
r2.URL.RawPath = rp
next.ServeHTTP(w, r2)
} else {
http.NotFound(w, r)
}
})
}
func getCompany(r *http.Request) *Company {
company := r.Context().Value(ContextCompanyKey)
if company == nil {
return nil
}
return company.(*Company)
}
type CurrencyOption struct {
Code string
Symbol string
}
type CountryOption struct {
Code string
Name string
}
type Tax struct {
Id int
Name string
Rate int
}
type taxDetailsForm struct {
*contactForm
Currency *SelectField
}
func newTaxDetailsForm(ctx context.Context, conn *Conn, locale *Locale) *taxDetailsForm {
return &taxDetailsForm{
contactForm: newContactForm(ctx, conn, locale),
Currency: &SelectField{
Name: "currency",
Label: pgettext("input", "Currency", locale),
Options: MustGetOptions(ctx, conn, "select currency_code, currency_symbol from currency order by currency_code"),
Selected: "EUR",
},
}
}
func (form *taxDetailsForm) Parse(r *http.Request) error {
if err := form.contactForm.Parse(r); err != nil {
return err
}
form.Currency.FillValue(r)
return nil
}
func (form *taxDetailsForm) Validate(ctx context.Context, conn *Conn) bool {
validator := newFormValidator()
validator.CheckValidSelectOption(form.Currency, gettext("Selected currency is not valid.", form.locale))
return form.contactForm.Validate(ctx, conn) && validator.AllOK()
}
func (form *taxDetailsForm) mustFillFromDatabase(ctx context.Context, conn *Conn, company *Company) *taxDetailsForm {
err := conn.QueryRow(ctx, "select business_name, substr(vatin::text, 3), trade_name, phone, email, web, address, city, province, postal_code, country_code, currency_code from company where company_id = $1", company.Id).Scan(form.BusinessName, form.VATIN, form.TradeName, form.Phone, form.Email, form.Web, form.Address, form.City, form.Province, form.PostalCode, form.Country, form.Currency)
if err != nil {
panic(err)
}
return form
}
type TaxDetailsPage struct {
DetailsForm *taxDetailsForm
NewTaxForm *newTaxForm
Taxes []*Tax
}
func CompanyTaxDetailsHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
locale := getLocale(r)
conn := getConn(r)
page := &TaxDetailsPage{
DetailsForm: newTaxDetailsForm(r.Context(), conn, locale),
NewTaxForm: newNewTaxForm(locale),
}
company := mustGetCompany(r)
if r.Method == "POST" {
if err := page.DetailsForm.Parse(r); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := verifyCsrfTokenValid(r); err != nil {
http.Error(w, err.Error(), http.StatusForbidden)
return
}
if ok := page.DetailsForm.Validate(r.Context(), conn); ok {
form := page.DetailsForm
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 where company_id = $13", form.BusinessName, form.VATIN, form.TradeName, form.Phone, form.Email, form.Web, form.Address, form.City, form.Province, form.PostalCode, form.Country, form.Currency, company.Id)
http.Redirect(w, r, "/company/"+company.Slug+"/tax-details", http.StatusSeeOther)
return
}
w.WriteHeader(http.StatusUnprocessableEntity)
} else {
page.DetailsForm.mustFillFromDatabase(r.Context(), conn, company)
}
page.Taxes = mustGetTaxes(r.Context(), conn, company)
mustRenderAppTemplate(w, r, "tax-details.gohtml", page)
})
}
func mustGetCompany(r *http.Request) *Company {
company := getCompany(r)
if company == nil {
panic(errors.New("company: required but not found"))
}
return company
}
func mustGetTaxes(ctx context.Context, conn *Conn, company *Company) []*Tax {
rows, err := conn.Query(ctx, "select tax_id, name, (rate * 100)::integer from tax where company_id = $1 order by rate, name", company.Id)
if err != nil {
panic(err)
}
defer rows.Close()
var taxes []*Tax
for rows.Next() {
tax := &Tax{}
err = rows.Scan(&tax.Id, &tax.Name, &tax.Rate)
if err != nil {
panic(err)
}
taxes = append(taxes, tax)
}
if rows.Err() != nil {
panic(rows.Err())
}
return taxes
}
type newTaxForm struct {
locale *Locale
Name *InputField
Rate *InputField
}
func newNewTaxForm(locale *Locale) *newTaxForm {
return &newTaxForm{
locale: locale,
Name: &InputField{
Name: "tax_name",
Label: pgettext("input", "Tax name", locale),
Type: "text",
Required: true,
},
Rate: &InputField{
Name: "tax_rate",
Label: pgettext("input", "Rate (%)", locale),
Type: "number",
Required: true,
Attributes: []template.HTMLAttr{
"min=-99",
"max=99",
},
},
}
}
func (form *newTaxForm) Parse(r *http.Request) error {
if err := r.ParseForm(); err != nil {
return err
}
form.Name.FillValue(r)
form.Rate.FillValue(r)
return nil
}
func (form *newTaxForm) Validate() bool {
validator := newFormValidator()
validator.CheckRequiredInput(form.Name, gettext("Tax name can not be empty.", form.locale))
if validator.CheckRequiredInput(form.Rate, gettext("Tax rate can not be empty.", form.locale)) {
validator.CheckValidInteger(form.Rate, -99, 99, gettext("Tax rate must be an integer between -99 and 99.", form.locale))
}
return validator.AllOK()
}
func CompanyTaxHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
param := r.URL.Path
if idx := strings.LastIndexByte(param, '/'); idx >= 0 {
param = param[idx+1:]
}
conn := getConn(r)
company := mustGetCompany(r)
if taxId, err := strconv.Atoi(param); err == nil {
if err := verifyCsrfTokenValid(r); err != nil {
http.Error(w, err.Error(), http.StatusForbidden)
return
}
conn.MustExec(r.Context(), "delete from tax where tax_id = $1", taxId)
http.Redirect(w, r, "/company/"+company.Slug+"/tax-details", http.StatusSeeOther)
} else {
locale := getLocale(r)
form := newNewTaxForm(locale)
if err := form.Parse(r); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := verifyCsrfTokenValid(r); err != nil {
http.Error(w, err.Error(), http.StatusForbidden)
return
}
if form.Validate() {
conn.MustExec(r.Context(), "insert into tax (company_id, name, rate) values ($1, $2, $3 / 100::decimal)", company.Id, form.Name, form.Rate.Integer())
http.Redirect(w, r, "/company/"+company.Slug+"/tax-details", http.StatusSeeOther)
} else {
w.WriteHeader(http.StatusUnprocessableEntity)
}
page := &TaxDetailsPage{
DetailsForm: newTaxDetailsForm(r.Context(), conn, locale).mustFillFromDatabase(r.Context(), conn, company),
NewTaxForm: form,
Taxes: mustGetTaxes(r.Context(), conn, company),
}
mustRenderAppTemplate(w, r, "tax-details.gohtml", page)
}
})
}