Add the company relation and read-only form to edit
I do not have more time to update the update to the company today, but i
believe this is already a good amount of work for a commit.
The company is going to be used for row level security, as users will
only have access to the data from companies they are granted access, by
virtue of being in the company_user relation.
I did not know how add a row level security policy to the company_user
because i needed the to select on the same relation and this is not
allowed, because it would create an infinite loop.
Had to add the vat, pg_libphonenumber, and uri extensions in order to
validate VAT identification numbers, phone numbers, and URIs,
repectively. These libraries are not in Debian, but i created packages
for them all in https://dev.tandem.ws/tandem.
2023-01-24 20:46:07 +00:00
package pkg
import (
"context"
"errors"
2023-02-01 13:15:02 +00:00
"html/template"
Add the company relation and read-only form to edit
I do not have more time to update the update to the company today, but i
believe this is already a good amount of work for a commit.
The company is going to be used for row level security, as users will
only have access to the data from companies they are granted access, by
virtue of being in the company_user relation.
I did not know how add a row level security policy to the company_user
because i needed the to select on the same relation and this is not
allowed, because it would create an infinite loop.
Had to add the vat, pg_libphonenumber, and uri extensions in order to
validate VAT identification numbers, phone numbers, and URIs,
repectively. These libraries are not in Debian, but i created packages
for them all in https://dev.tandem.ws/tandem.
2023-01-24 20:46:07 +00:00
"net/http"
"net/url"
2023-01-28 13:18:58 +00:00
"strconv"
Add the company relation and read-only form to edit
I do not have more time to update the update to the company today, but i
believe this is already a good amount of work for a commit.
The company is going to be used for row level security, as users will
only have access to the data from companies they are granted access, by
virtue of being in the company_user relation.
I did not know how add a row level security policy to the company_user
because i needed the to select on the same relation and this is not
allowed, because it would create an infinite loop.
Had to add the vat, pg_libphonenumber, and uri extensions in order to
validate VAT identification numbers, phone numbers, and URIs,
repectively. These libraries are not in Debian, but i created packages
for them all in https://dev.tandem.ws/tandem.
2023-01-24 20:46:07 +00:00
"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 )
}
2023-01-28 11:24:52 +00:00
type CurrencyOption struct {
Code string
Symbol string
}
2023-01-27 20:30:14 +00:00
type CountryOption struct {
Code string
Name string
}
2023-01-28 13:18:58 +00:00
type Tax struct {
Id int
Name string
Rate int
}
2023-02-01 13:15:02 +00:00
type taxDetailsForm struct {
locale * Locale
BusinessName * InputField
VATIN * InputField
TradeName * InputField
Phone * InputField
Email * InputField
Web * InputField
Address * InputField
City * InputField
Province * InputField
PostalCode * InputField
Country * SelectField
Currency * SelectField
}
func newTaxDetailsForm ( ctx context . Context , conn * Conn , locale * Locale ) * taxDetailsForm {
return & taxDetailsForm {
locale : locale ,
BusinessName : & InputField {
Name : "business_name" ,
Label : pgettext ( "input" , "Business name" , locale ) ,
Type : "text" ,
Required : true ,
Attributes : [ ] template . HTMLAttr {
` autocomplete="organization" ` ,
} ,
} ,
VATIN : & InputField {
Name : "vatin" ,
Label : pgettext ( "input" , "VAT number" , locale ) ,
Type : "text" ,
Required : true ,
} ,
TradeName : & InputField {
Name : "trade_name" ,
Label : pgettext ( "input" , "Trade name" , locale ) ,
Type : "text" ,
} ,
Phone : & InputField {
Name : "phone" ,
Label : pgettext ( "input" , "Phone" , locale ) ,
Type : "tel" ,
Required : true ,
Attributes : [ ] template . HTMLAttr {
` autocomplete="tel" ` ,
} ,
} ,
Email : & InputField {
Name : "email" ,
Label : pgettext ( "input" , "Email" , locale ) ,
Type : "email" ,
Required : true ,
Attributes : [ ] template . HTMLAttr {
` autocomplete="email" ` ,
} ,
} ,
Web : & InputField {
Name : "web" ,
Label : pgettext ( "input" , "Web" , locale ) ,
Type : "url" ,
Attributes : [ ] template . HTMLAttr {
` autocomplete="url" ` ,
} ,
} ,
Address : & InputField {
Name : "address" ,
Label : pgettext ( "input" , "Address" , locale ) ,
Type : "text" ,
Required : true ,
Attributes : [ ] template . HTMLAttr {
` autocomplete="address-line1" ` ,
} ,
} ,
City : & InputField {
Name : "city" ,
Label : pgettext ( "input" , "City" , locale ) ,
Type : "text" ,
Required : true ,
} ,
Province : & InputField {
Name : "province" ,
Label : pgettext ( "input" , "Province" , locale ) ,
Type : "text" ,
Required : true ,
} ,
PostalCode : & InputField {
Name : "postal_code" ,
Label : pgettext ( "input" , "Postal code" , locale ) ,
Type : "text" ,
Required : true ,
Attributes : [ ] template . HTMLAttr {
` autocomplete="postal-code" ` ,
} ,
} ,
Country : & SelectField {
Name : "country" ,
Label : pgettext ( "input" , "Country" , locale ) ,
Options : mustGetCountryOptions ( ctx , conn , locale ) ,
Selected : "ES" ,
Attributes : [ ] template . HTMLAttr {
` autocomplete="country" ` ,
} ,
} ,
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 := r . ParseForm ( ) ; err != nil {
return err
}
form . BusinessName . FillValue ( r )
form . VATIN . FillValue ( r )
form . TradeName . FillValue ( r )
form . Phone . FillValue ( r )
form . Email . FillValue ( r )
form . Web . FillValue ( r )
form . Address . FillValue ( r )
form . City . FillValue ( r )
form . Province . FillValue ( r )
form . PostalCode . FillValue ( r )
form . Country . FillValue ( r )
form . Currency . FillValue ( r )
return nil
}
func ( form * taxDetailsForm ) Validate ( ctx context . Context , conn * Conn ) bool {
validator := newFormValidator ( )
validator . CheckRequiredInput ( form . BusinessName , gettext ( "Business name can not be empty." , form . locale ) )
if validator . CheckRequiredInput ( form . VATIN , gettext ( "VAT number can not be empty." , form . locale ) ) {
validator . CheckValidVATINInput ( form . VATIN , form . Country . Selected , gettext ( "This value is not a valid VAT number." , form . locale ) )
}
if validator . CheckRequiredInput ( form . Phone , gettext ( "Phone can not be empty." , form . locale ) ) {
validator . CheckValidPhoneInput ( form . Phone , form . Country . Selected , gettext ( "This value is not a valid phone number." , form . locale ) )
}
if validator . CheckRequiredInput ( form . Email , gettext ( "Email can not be empty." , form . locale ) ) {
validator . CheckValidEmailInput ( form . Email , gettext ( "This value is not a valid email. It should be like name@domain.com." , form . locale ) )
}
if form . Web . Val != "" {
validator . checkValidURL ( form . Web , gettext ( "This value is not a valid web address. It should be like https://domain.com/." , form . locale ) )
}
validator . CheckRequiredInput ( form . Address , gettext ( "Address can not be empty." , form . locale ) )
validator . CheckRequiredInput ( form . City , gettext ( "City can not be empty." , form . locale ) )
validator . CheckRequiredInput ( form . Province , gettext ( "Province can not be empty." , form . locale ) )
if validator . CheckRequiredInput ( form . PostalCode , gettext ( "Postal code can not be empty." , form . locale ) ) {
validator . CheckValidPostalCode ( ctx , conn , form . PostalCode , form . Country . Selected , gettext ( "This value is not a valid postal code." , form . locale ) )
}
validator . CheckValidSelectOption ( form . Country , gettext ( "Selected country is not valid." , form . locale ) )
validator . CheckValidSelectOption ( form . Currency , gettext ( "Selected currency is not valid." , form . locale ) )
return 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
}
Add the company relation and read-only form to edit
I do not have more time to update the update to the company today, but i
believe this is already a good amount of work for a commit.
The company is going to be used for row level security, as users will
only have access to the data from companies they are granted access, by
virtue of being in the company_user relation.
I did not know how add a row level security policy to the company_user
because i needed the to select on the same relation and this is not
allowed, because it would create an infinite loop.
Had to add the vat, pg_libphonenumber, and uri extensions in order to
validate VAT identification numbers, phone numbers, and URIs,
repectively. These libraries are not in Debian, but i created packages
for them all in https://dev.tandem.ws/tandem.
2023-01-24 20:46:07 +00:00
type TaxDetailsPage struct {
2023-02-01 13:15:02 +00:00
DetailsForm * taxDetailsForm
NewTaxForm * newTaxForm
Taxes [ ] * Tax
Add the company relation and read-only form to edit
I do not have more time to update the update to the company today, but i
believe this is already a good amount of work for a commit.
The company is going to be used for row level security, as users will
only have access to the data from companies they are granted access, by
virtue of being in the company_user relation.
I did not know how add a row level security policy to the company_user
because i needed the to select on the same relation and this is not
allowed, because it would create an infinite loop.
Had to add the vat, pg_libphonenumber, and uri extensions in order to
validate VAT identification numbers, phone numbers, and URIs,
repectively. These libraries are not in Debian, but i created packages
for them all in https://dev.tandem.ws/tandem.
2023-01-24 20:46:07 +00:00
}
func CompanyTaxDetailsHandler ( ) http . Handler {
return http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
locale := getLocale ( r )
conn := getConn ( r )
2023-02-01 13:15:02 +00:00
page := & TaxDetailsPage {
DetailsForm : newTaxDetailsForm ( r . Context ( ) , conn , locale ) ,
NewTaxForm : newNewTaxForm ( locale ) ,
}
company := mustGetCompany ( r )
2023-01-27 00:08:03 +00:00
if r . Method == "POST" {
2023-02-01 13:15:02 +00:00
if err := page . DetailsForm . Parse ( r ) ; err != nil {
http . Error ( w , err . Error ( ) , http . StatusBadRequest )
return
2023-01-27 00:08:03 +00:00
}
2023-02-01 13:15:02 +00:00
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 )
Add the company relation and read-only form to edit
I do not have more time to update the update to the company today, but i
believe this is already a good amount of work for a commit.
The company is going to be used for row level security, as users will
only have access to the data from companies they are granted access, by
virtue of being in the company_user relation.
I did not know how add a row level security policy to the company_user
because i needed the to select on the same relation and this is not
allowed, because it would create an infinite loop.
Had to add the vat, pg_libphonenumber, and uri extensions in order to
validate VAT identification numbers, phone numbers, and URIs,
repectively. These libraries are not in Debian, but i created packages
for them all in https://dev.tandem.ws/tandem.
2023-01-24 20:46:07 +00:00
}
2023-01-28 13:18:58 +00:00
page . Taxes = mustGetTaxes ( r . Context ( ) , conn , company )
2023-01-30 15:48:21 +00:00
mustRenderAppTemplate ( w , r , "tax-details.gohtml" , page )
2023-01-27 00:08:03 +00:00
} )
Add the company relation and read-only form to edit
I do not have more time to update the update to the company today, but i
believe this is already a good amount of work for a commit.
The company is going to be used for row level security, as users will
only have access to the data from companies they are granted access, by
virtue of being in the company_user relation.
I did not know how add a row level security policy to the company_user
because i needed the to select on the same relation and this is not
allowed, because it would create an infinite loop.
Had to add the vat, pg_libphonenumber, and uri extensions in order to
validate VAT identification numbers, phone numbers, and URIs,
repectively. These libraries are not in Debian, but i created packages
for them all in https://dev.tandem.ws/tandem.
2023-01-24 20:46:07 +00:00
}
func mustGetCompany ( r * http . Request ) * Company {
company := getCompany ( r )
if company == nil {
panic ( errors . New ( "company: required but not found" ) )
}
2023-01-27 00:08:03 +00:00
return company
Add the company relation and read-only form to edit
I do not have more time to update the update to the company today, but i
believe this is already a good amount of work for a commit.
The company is going to be used for row level security, as users will
only have access to the data from companies they are granted access, by
virtue of being in the company_user relation.
I did not know how add a row level security policy to the company_user
because i needed the to select on the same relation and this is not
allowed, because it would create an infinite loop.
Had to add the vat, pg_libphonenumber, and uri extensions in order to
validate VAT identification numbers, phone numbers, and URIs,
repectively. These libraries are not in Debian, but i created packages
for them all in https://dev.tandem.ws/tandem.
2023-01-24 20:46:07 +00:00
}
2023-01-27 20:30:14 +00:00
2023-02-01 13:15:02 +00:00
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 )
2023-01-27 20:30:14 +00:00
if err != nil {
panic ( err )
}
defer rows . Close ( )
2023-02-01 13:15:02 +00:00
var taxes [ ] * Tax
2023-01-27 20:30:14 +00:00
for rows . Next ( ) {
2023-02-01 13:15:02 +00:00
tax := & Tax { }
err = rows . Scan ( & tax . Id , & tax . Name , & tax . Rate )
2023-01-27 20:30:14 +00:00
if err != nil {
panic ( err )
}
2023-02-01 13:15:02 +00:00
taxes = append ( taxes , tax )
2023-01-27 20:30:14 +00:00
}
if rows . Err ( ) != nil {
panic ( rows . Err ( ) )
}
2023-02-01 13:15:02 +00:00
return taxes
2023-01-27 20:30:14 +00:00
}
2023-01-28 11:24:52 +00:00
2023-02-01 13:15:02 +00:00
type newTaxForm struct {
locale * Locale
Name * InputField
Rate * InputField
}
2023-01-28 11:24:52 +00:00
2023-02-01 13:15:02 +00:00
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" ,
} ,
} ,
2023-01-28 11:24:52 +00:00
}
}
2023-01-28 13:18:58 +00:00
2023-02-01 13:15:02 +00:00
func ( form * newTaxForm ) Parse ( r * http . Request ) error {
if err := r . ParseForm ( ) ; err != nil {
return err
2023-01-28 13:18:58 +00:00
}
2023-02-01 13:15:02 +00:00
form . Name . FillValue ( r )
form . Rate . FillValue ( r )
return nil
}
2023-01-28 13:18:58 +00:00
2023-02-01 13:15:02 +00:00
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 ) )
2023-01-28 13:18:58 +00:00
}
2023-02-01 13:15:02 +00:00
return validator . AllOK ( )
2023-01-28 13:18:58 +00:00
}
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 {
conn . MustExec ( r . Context ( ) , "delete from tax where tax_id = $1" , taxId )
2023-02-01 13:15:02 +00:00
http . Redirect ( w , r , "/company/" + company . Slug + "/tax-details" , http . StatusSeeOther )
2023-01-28 13:18:58 +00:00
} else {
2023-02-01 13:15:02 +00:00
locale := getLocale ( r )
form := newNewTaxForm ( locale )
if err := form . Parse ( r ) ; err != nil {
http . Error ( w , err . Error ( ) , http . StatusBadRequest )
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 )
2023-01-28 13:18:58 +00:00
}
} )
}