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-03 11:30:56 +00:00
"github.com/julienschmidt/httprouter"
2023-02-01 13:15:02 +00:00
"html/template"
2023-05-31 18:01:00 +00:00
"math"
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
)
const (
ContextCompanyKey = "numerus-company"
)
type Company struct {
Convert from cents to “price” and back
I do not want to use floats in the Go lang application, because it is
not supposed to do anything with these values other than to print and
retrieve them from the user; all computations will be performed by
PostgreSQL in cents.
That means i have to “convert” from the price format that users expect
to see (e.g., 1.234,56) to cents (e.g., 123456) and back when passing
data between Go and PostgreSQL, and that conversion depends on the
currency’s decimal places.
At first i did everything in Go, but saw that i would need to do it in
a loop when retrieving the list of products, and immediately knew it was
a mistake—i needed a PL/pgSQL function for that.
I still need to convert from string to float, however, when printing the
value to the user. Because the string representation is in C, but i
need to format it according to the locale with golang/x/text. That
package has the information of how to correctly format numbers, but it
is in an internal package that i can not use, and numbers.Digit only
accepts numeric types, not a string.
2023-02-05 12:55:12 +00:00
Id int
CurrencySymbol string
DecimalDigits int
Slug string
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-02-03 11:30:56 +00:00
func CompanyHandler ( next http . Handler ) httprouter . Handle {
return func ( w http . ResponseWriter , r * http . Request , params httprouter . Params ) {
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
company := & Company {
2023-02-03 11:30:56 +00:00
Slug : params [ 0 ] . Value ,
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-02-03 11:30:56 +00:00
conn := getConn ( r )
Convert from cents to “price” and back
I do not want to use floats in the Go lang application, because it is
not supposed to do anything with these values other than to print and
retrieve them from the user; all computations will be performed by
PostgreSQL in cents.
That means i have to “convert” from the price format that users expect
to see (e.g., 1.234,56) to cents (e.g., 123456) and back when passing
data between Go and PostgreSQL, and that conversion depends on the
currency’s decimal places.
At first i did everything in Go, but saw that i would need to do it in
a loop when retrieving the list of products, and immediately knew it was
a mistake—i needed a PL/pgSQL function for that.
I still need to convert from string to float, however, when printing the
value to the user. Because the string representation is in C, but i
need to format it according to the locale with golang/x/text. That
package has the information of how to correctly format numbers, but it
is in an internal package that i can not use, and numbers.Digit only
accepts numeric types, not a string.
2023-02-05 12:55:12 +00:00
err := conn . QueryRow ( r . Context ( ) , "select company_id, currency_symbol, decimal_digits from company join currency using (currency_code) where slug = $1" , company . Slug ) . Scan ( & company . Id , & company . CurrencySymbol , & company . DecimalDigits )
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
if err != nil {
http . NotFound ( w , r )
return
}
ctx := context . WithValue ( r . Context ( ) , ContextCompanyKey , company )
r = r . WithContext ( ctx )
2023-02-03 11:30:56 +00:00
r2 := new ( http . Request )
* r2 = * r
r2 . URL = new ( url . URL )
* r2 . URL = * r . URL
r2 . URL . Path = params [ 1 ] . Value
next . ServeHTTP ( w , r2 )
}
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
}
Convert from cents to “price” and back
I do not want to use floats in the Go lang application, because it is
not supposed to do anything with these values other than to print and
retrieve them from the user; all computations will be performed by
PostgreSQL in cents.
That means i have to “convert” from the price format that users expect
to see (e.g., 1.234,56) to cents (e.g., 123456) and back when passing
data between Go and PostgreSQL, and that conversion depends on the
currency’s decimal places.
At first i did everything in Go, but saw that i would need to do it in
a loop when retrieving the list of products, and immediately knew it was
a mistake—i needed a PL/pgSQL function for that.
I still need to convert from string to float, however, when printing the
value to the user. Because the string representation is in C, but i
need to format it according to the locale with golang/x/text. That
package has the information of how to correctly format numbers, but it
is in an internal package that i can not use, and numbers.Digit only
accepts numeric types, not a string.
2023-02-05 12:55:12 +00:00
func ( c Company ) MinCents ( ) float64 {
var r float64
r = 1
for i := 0 ; i < c . DecimalDigits ; i ++ {
r /= 10.0
}
return r
}
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 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 {
2023-02-28 11:02:27 +00:00
Id int
Name string
Class string
Rate int
2023-01-28 13:18:58 +00:00
}
2023-03-03 15:49:06 +00:00
type PaymentMethod struct {
Id int
Name string
Instructions string
}
2023-02-01 13:15:02 +00:00
type taxDetailsForm struct {
Split the tax details “mega dialog” into separate pages
I needed to place the payment accounts section somewhere, and the most
logical place seemed to be that dialog, where users can set up company
parameters.
However, that dialog was already saturated with related, but ultimately
independent forms, and adding the account section would make things
even worse, specially given that we need to be able to edit those
accounts in a separate page.
We agreed to separate that dialog into tabs, which means separate pages.
When i had everything in a separated page, then i did not know how to
actually share the code for the tabs, and decided that, for now, these
“tabs” would be items from the profile menu. Same function, different
presentation.
2024-08-14 02:08:13 +00:00
locale * Locale
TradeName * InputField
BusinessName * InputField
VATIN * InputField
Phone * InputField
Email * InputField
Web * InputField
Address * InputField
City * InputField
Province * InputField
PostalCode * InputField
Country * SelectField
Currency * SelectField
2023-02-01 13:15:02 +00:00
}
func newTaxDetailsForm ( ctx context . Context , conn * Conn , locale * Locale ) * taxDetailsForm {
return & taxDetailsForm {
Split contact relation into tax_details, phone, web, and email
We need to have contacts with just a name: we need to assign
freelancer’s quote as expense linked the government, but of course we
do not have a phone or email for that “contact”, much less a VATIN or
other tax details.
It is also interesting for other expenses-only contacts to not have to
input all tax details, as we may not need to invoice then, thus are
useless for us, but sometimes it might be interesting to have them,
“just in case”.
Of course, i did not want to make nullable any of the tax details
required to generate an invoice, otherwise we could allow illegal
invoices. Therefore, that data had to go in a different relation,
and invoice’s foreign key update to point to that relation, not just
customer, or we would again be able to create invalid invoices.
We replaced the contact’s trade name with just name, because we do not
need _three_ names for a contact, but we _do_ need two: the one we use
to refer to them and the business name for tax purposes.
The new contact_phone, contact_web, and contact_email relations could be
simply a nullable field, but i did not see the point, since there are
not that many instances where i need any of this data.
Now company.taxDetailsForm is no longer “the same as contactForm with
some extra fields”, because i have to add a check whether the user needs
to invoice the contact, to check that the required values are there.
I have an additional problem with the contact form when not using
JavaScript: i must set the required field to all tax details fields to
avoid the “(optional)” suffix, and because they _are_ required when
that checkbox is enabled, but i can not set them optional when the check
is unchecked. My solution for now is to ignore the form validation,
and later i will add some JavaScript that adds the validation again,
so it will work in all cases.
2023-06-30 19:32:48 +00:00
locale : locale ,
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" ` ,
} ,
} ,
BusinessName : & InputField {
Name : "business_name" ,
Label : pgettext ( "input" , "Business name" , locale ) ,
Type : "text" ,
Required : true ,
Attributes : [ ] template . HTMLAttr {
` autocomplete="organization" ` ,
` minlength="2" ` ,
} ,
} ,
VATIN : & InputField {
Name : "vatin" ,
Label : pgettext ( "input" , "VAT number" , locale ) ,
Type : "text" ,
Required : true ,
} ,
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 ) ,
Required : true ,
Selected : [ ] string { "ES" } ,
Attributes : [ ] template . HTMLAttr {
` autocomplete="country" ` ,
} ,
} ,
2023-02-01 13:15:02 +00:00
Currency : & SelectField {
Name : "currency" ,
Label : pgettext ( "input" , "Currency" , locale ) ,
Options : MustGetOptions ( ctx , conn , "select currency_code, currency_symbol from currency order by currency_code" ) ,
2023-02-05 13:06:33 +00:00
Required : true ,
2023-02-08 12:47:36 +00:00
Selected : [ ] string { "EUR" } ,
2023-02-01 13:15:02 +00:00
} ,
}
}
func ( form * taxDetailsForm ) Parse ( r * http . Request ) error {
Split contact relation into tax_details, phone, web, and email
We need to have contacts with just a name: we need to assign
freelancer’s quote as expense linked the government, but of course we
do not have a phone or email for that “contact”, much less a VATIN or
other tax details.
It is also interesting for other expenses-only contacts to not have to
input all tax details, as we may not need to invoice then, thus are
useless for us, but sometimes it might be interesting to have them,
“just in case”.
Of course, i did not want to make nullable any of the tax details
required to generate an invoice, otherwise we could allow illegal
invoices. Therefore, that data had to go in a different relation,
and invoice’s foreign key update to point to that relation, not just
customer, or we would again be able to create invalid invoices.
We replaced the contact’s trade name with just name, because we do not
need _three_ names for a contact, but we _do_ need two: the one we use
to refer to them and the business name for tax purposes.
The new contact_phone, contact_web, and contact_email relations could be
simply a nullable field, but i did not see the point, since there are
not that many instances where i need any of this data.
Now company.taxDetailsForm is no longer “the same as contactForm with
some extra fields”, because i have to add a check whether the user needs
to invoice the contact, to check that the required values are there.
I have an additional problem with the contact form when not using
JavaScript: i must set the required field to all tax details fields to
avoid the “(optional)” suffix, and because they _are_ required when
that checkbox is enabled, but i can not set them optional when the check
is unchecked. My solution for now is to ignore the form validation,
and later i will add some JavaScript that adds the validation again,
so it will work in all cases.
2023-06-30 19:32:48 +00:00
if err := r . ParseForm ( ) ; err != nil {
2023-02-01 13:15:02 +00:00
return err
}
Split contact relation into tax_details, phone, web, and email
We need to have contacts with just a name: we need to assign
freelancer’s quote as expense linked the government, but of course we
do not have a phone or email for that “contact”, much less a VATIN or
other tax details.
It is also interesting for other expenses-only contacts to not have to
input all tax details, as we may not need to invoice then, thus are
useless for us, but sometimes it might be interesting to have them,
“just in case”.
Of course, i did not want to make nullable any of the tax details
required to generate an invoice, otherwise we could allow illegal
invoices. Therefore, that data had to go in a different relation,
and invoice’s foreign key update to point to that relation, not just
customer, or we would again be able to create invalid invoices.
We replaced the contact’s trade name with just name, because we do not
need _three_ names for a contact, but we _do_ need two: the one we use
to refer to them and the business name for tax purposes.
The new contact_phone, contact_web, and contact_email relations could be
simply a nullable field, but i did not see the point, since there are
not that many instances where i need any of this data.
Now company.taxDetailsForm is no longer “the same as contactForm with
some extra fields”, because i have to add a check whether the user needs
to invoice the contact, to check that the required values are there.
I have an additional problem with the contact form when not using
JavaScript: i must set the required field to all tax details fields to
avoid the “(optional)” suffix, and because they _are_ required when
that checkbox is enabled, but i can not set them optional when the check
is unchecked. My solution for now is to ignore the form validation,
and later i will add some JavaScript that adds the validation again,
so it will work in all cases.
2023-06-30 19:32:48 +00:00
form . TradeName . FillValue ( r )
form . BusinessName . FillValue ( r )
form . VATIN . 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 )
2023-02-01 13:15:02 +00:00
form . Currency . FillValue ( r )
return nil
}
func ( form * taxDetailsForm ) Validate ( ctx context . Context , conn * Conn ) bool {
validator := newFormValidator ( )
Split contact relation into tax_details, phone, web, and email
We need to have contacts with just a name: we need to assign
freelancer’s quote as expense linked the government, but of course we
do not have a phone or email for that “contact”, much less a VATIN or
other tax details.
It is also interesting for other expenses-only contacts to not have to
input all tax details, as we may not need to invoice then, thus are
useless for us, but sometimes it might be interesting to have them,
“just in case”.
Of course, i did not want to make nullable any of the tax details
required to generate an invoice, otherwise we could allow illegal
invoices. Therefore, that data had to go in a different relation,
and invoice’s foreign key update to point to that relation, not just
customer, or we would again be able to create invalid invoices.
We replaced the contact’s trade name with just name, because we do not
need _three_ names for a contact, but we _do_ need two: the one we use
to refer to them and the business name for tax purposes.
The new contact_phone, contact_web, and contact_email relations could be
simply a nullable field, but i did not see the point, since there are
not that many instances where i need any of this data.
Now company.taxDetailsForm is no longer “the same as contactForm with
some extra fields”, because i have to add a check whether the user needs
to invoice the contact, to check that the required values are there.
I have an additional problem with the contact form when not using
JavaScript: i must set the required field to all tax details fields to
avoid the “(optional)” suffix, and because they _are_ required when
that checkbox is enabled, but i can not set them optional when the check
is unchecked. My solution for now is to ignore the form validation,
and later i will add some JavaScript that adds the validation again,
so it will work in all cases.
2023-06-30 19:32:48 +00:00
country := ""
if validator . CheckValidSelectOption ( form . Country , gettext ( "Selected country is not valid." , form . locale ) ) {
country = form . Country . Selected [ 0 ]
}
validator . CheckRequiredInput ( form . BusinessName , gettext ( "Business name can not be empty." , form . locale ) )
validator . CheckInputMinLength ( form . BusinessName , 2 , gettext ( "Business name must have at least two letters." , form . locale ) )
if validator . CheckRequiredInput ( form . VATIN , gettext ( "VAT number can not be empty." , form . locale ) ) {
Create validation function for SQL domains and for phones
When i wrote the functions to import contact, i already created a couple
of “temporary” functions to validate whether the input given from the
Excel files was correct according to the various domains used in the
relations, so i can know whether i can import that data.
I realized that i could do exactly the same when validating forms: check
that the value conforms to the domain, in the exact same way, so i can
make sure that the value will be accepted without duplicating the logic,
at the expense of a call to the database.
In an ideal world, i would use pg_input_is_valid, but this function is
only available in PostgreSQL 16 and Debian 12 uses PostgreSQL 15.
These functions are in the public schema because initially i wanted to
use them to also validate email, which is needed in the login form, but
then i recanted and kept the same email validation in Go, because
something felt off about using the database for that particular form,
but i do not know why.
2023-07-03 09:31:59 +00:00
validator . CheckValidVATINInput ( ctx , conn , form . VATIN , country , gettext ( "This value is not a valid VAT number." , form . locale ) )
Split contact relation into tax_details, phone, web, and email
We need to have contacts with just a name: we need to assign
freelancer’s quote as expense linked the government, but of course we
do not have a phone or email for that “contact”, much less a VATIN or
other tax details.
It is also interesting for other expenses-only contacts to not have to
input all tax details, as we may not need to invoice then, thus are
useless for us, but sometimes it might be interesting to have them,
“just in case”.
Of course, i did not want to make nullable any of the tax details
required to generate an invoice, otherwise we could allow illegal
invoices. Therefore, that data had to go in a different relation,
and invoice’s foreign key update to point to that relation, not just
customer, or we would again be able to create invalid invoices.
We replaced the contact’s trade name with just name, because we do not
need _three_ names for a contact, but we _do_ need two: the one we use
to refer to them and the business name for tax purposes.
The new contact_phone, contact_web, and contact_email relations could be
simply a nullable field, but i did not see the point, since there are
not that many instances where i need any of this data.
Now company.taxDetailsForm is no longer “the same as contactForm with
some extra fields”, because i have to add a check whether the user needs
to invoice the contact, to check that the required values are there.
I have an additional problem with the contact form when not using
JavaScript: i must set the required field to all tax details fields to
avoid the “(optional)” suffix, and because they _are_ required when
that checkbox is enabled, but i can not set them optional when the check
is unchecked. My solution for now is to ignore the form validation,
and later i will add some JavaScript that adds the validation again,
so it will work in all cases.
2023-06-30 19:32:48 +00:00
}
if validator . CheckRequiredInput ( form . Phone , gettext ( "Phone can not be empty." , form . locale ) ) {
Create validation function for SQL domains and for phones
When i wrote the functions to import contact, i already created a couple
of “temporary” functions to validate whether the input given from the
Excel files was correct according to the various domains used in the
relations, so i can know whether i can import that data.
I realized that i could do exactly the same when validating forms: check
that the value conforms to the domain, in the exact same way, so i can
make sure that the value will be accepted without duplicating the logic,
at the expense of a call to the database.
In an ideal world, i would use pg_input_is_valid, but this function is
only available in PostgreSQL 16 and Debian 12 uses PostgreSQL 15.
These functions are in the public schema because initially i wanted to
use them to also validate email, which is needed in the login form, but
then i recanted and kept the same email validation in Go, because
something felt off about using the database for that particular form,
but i do not know why.
2023-07-03 09:31:59 +00:00
validator . CheckValidPhoneInput ( ctx , conn , form . Phone , country , gettext ( "This value is not a valid phone number." , form . locale ) )
Split contact relation into tax_details, phone, web, and email
We need to have contacts with just a name: we need to assign
freelancer’s quote as expense linked the government, but of course we
do not have a phone or email for that “contact”, much less a VATIN or
other tax details.
It is also interesting for other expenses-only contacts to not have to
input all tax details, as we may not need to invoice then, thus are
useless for us, but sometimes it might be interesting to have them,
“just in case”.
Of course, i did not want to make nullable any of the tax details
required to generate an invoice, otherwise we could allow illegal
invoices. Therefore, that data had to go in a different relation,
and invoice’s foreign key update to point to that relation, not just
customer, or we would again be able to create invalid invoices.
We replaced the contact’s trade name with just name, because we do not
need _three_ names for a contact, but we _do_ need two: the one we use
to refer to them and the business name for tax purposes.
The new contact_phone, contact_web, and contact_email relations could be
simply a nullable field, but i did not see the point, since there are
not that many instances where i need any of this data.
Now company.taxDetailsForm is no longer “the same as contactForm with
some extra fields”, because i have to add a check whether the user needs
to invoice the contact, to check that the required values are there.
I have an additional problem with the contact form when not using
JavaScript: i must set the required field to all tax details fields to
avoid the “(optional)” suffix, and because they _are_ required when
that checkbox is enabled, but i can not set them optional when the check
is unchecked. My solution for now is to ignore the form validation,
and later i will add some JavaScript that adds the validation again,
so it will work in all cases.
2023-06-30 19:32:48 +00:00
}
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 , country , gettext ( "This value is not a valid postal code." , form . locale ) )
}
2023-02-01 13:15:02 +00:00
validator . CheckValidSelectOption ( form . Currency , gettext ( "Selected currency is not valid." , form . locale ) )
Split contact relation into tax_details, phone, web, and email
We need to have contacts with just a name: we need to assign
freelancer’s quote as expense linked the government, but of course we
do not have a phone or email for that “contact”, much less a VATIN or
other tax details.
It is also interesting for other expenses-only contacts to not have to
input all tax details, as we may not need to invoice then, thus are
useless for us, but sometimes it might be interesting to have them,
“just in case”.
Of course, i did not want to make nullable any of the tax details
required to generate an invoice, otherwise we could allow illegal
invoices. Therefore, that data had to go in a different relation,
and invoice’s foreign key update to point to that relation, not just
customer, or we would again be able to create invalid invoices.
We replaced the contact’s trade name with just name, because we do not
need _three_ names for a contact, but we _do_ need two: the one we use
to refer to them and the business name for tax purposes.
The new contact_phone, contact_web, and contact_email relations could be
simply a nullable field, but i did not see the point, since there are
not that many instances where i need any of this data.
Now company.taxDetailsForm is no longer “the same as contactForm with
some extra fields”, because i have to add a check whether the user needs
to invoice the contact, to check that the required values are there.
I have an additional problem with the contact form when not using
JavaScript: i must set the required field to all tax details fields to
avoid the “(optional)” suffix, and because they _are_ required when
that checkbox is enabled, but i can not set them optional when the check
is unchecked. My solution for now is to ignore the form validation,
and later i will add some JavaScript that adds the validation again,
so it will work in all cases.
2023-06-30 19:32:48 +00:00
return validator . AllOK ( )
2023-02-01 13:15:02 +00:00
}
func ( form * taxDetailsForm ) mustFillFromDatabase ( ctx context . Context , conn * Conn , company * Company ) * taxDetailsForm {
2023-05-31 18:01:00 +00:00
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 . 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 )
}
2023-02-01 13:15:02 +00:00
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 {
Split the tax details “mega dialog” into separate pages
I needed to place the payment accounts section somewhere, and the most
logical place seemed to be that dialog, where users can set up company
parameters.
However, that dialog was already saturated with related, but ultimately
independent forms, and adding the account section would make things
even worse, specially given that we need to be able to edit those
accounts in a separate page.
We agreed to separate that dialog into tabs, which means separate pages.
When i had everything in a separated page, then i did not know how to
actually share the code for the tabs, and decided that, for now, these
“tabs” would be items from the profile menu. Same function, different
presentation.
2024-08-14 02:08:13 +00:00
DetailsForm * taxDetailsForm
}
func ( page * TaxDetailsPage ) MustRender ( w http . ResponseWriter , r * http . Request ) {
mustRenderMainTemplate ( w , r , "company/tax-details.gohtml" , page )
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-02-03 11:30:56 +00:00
func GetCompanyTaxDetailsForm ( w http . ResponseWriter , r * http . Request , _ httprouter . Params ) {
mustRenderTaxDetailsForm ( w , r , newTaxDetailsFormFromDatabase ( r ) )
}
func newTaxDetailsFormFromDatabase ( r * http . Request ) * taxDetailsForm {
locale := getLocale ( r )
conn := getConn ( r )
form := newTaxDetailsForm ( r . Context ( ) , conn , locale )
company := mustGetCompany ( r )
form . mustFillFromDatabase ( r . Context ( ) , conn , company )
return form
}
func HandleCompanyTaxDetailsForm ( w http . ResponseWriter , r * http . Request , _ httprouter . Params ) {
locale := getLocale ( r )
conn := getConn ( r )
form := newTaxDetailsForm ( r . Context ( ) , conn , 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 ok := form . Validate ( r . Context ( ) , conn ) ; ! ok {
2023-03-21 10:58:54 +00:00
if ! IsHTMxRequest ( r ) {
w . WriteHeader ( http . StatusUnprocessableEntity )
}
2023-02-03 11:30:56 +00:00
mustRenderTaxDetailsForm ( w , r , form )
return
}
company := mustGetCompany ( r )
Split the tax details “mega dialog” into separate pages
I needed to place the payment accounts section somewhere, and the most
logical place seemed to be that dialog, where users can set up company
parameters.
However, that dialog was already saturated with related, but ultimately
independent forms, and adding the account section would make things
even worse, specially given that we need to be able to edit those
accounts in a separate page.
We agreed to separate that dialog into tabs, which means separate pages.
When i had everything in a separated page, then i did not know how to
actually share the code for the tabs, and decided that, for now, these
“tabs” would be items from the profile menu. Same function, different
presentation.
2024-08-14 02:08:13 +00:00
conn . MustExec ( r . Context ( ) , `
2023-06-09 10:43:50 +00:00
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
Split the tax details “mega dialog” into separate pages
I needed to place the payment accounts section somewhere, and the most
logical place seemed to be that dialog, where users can set up company
parameters.
However, that dialog was already saturated with related, but ultimately
independent forms, and adding the account section would make things
even worse, specially given that we need to be able to edit those
accounts in a separate page.
We agreed to separate that dialog into tabs, which means separate pages.
When i had everything in a separated page, then i did not know how to
actually share the code for the tabs, and decided that, for now, these
“tabs” would be items from the profile menu. Same function, different
presentation.
2024-08-14 02:08:13 +00:00
where company_id = $ 13
2023-06-09 10:43:50 +00:00
` ,
form . BusinessName ,
form . VATIN ,
form . TradeName ,
form . Phone ,
form . Email ,
form . Web ,
form . Address ,
form . City ,
form . Province ,
form . PostalCode ,
form . Country ,
form . Currency ,
Split the tax details “mega dialog” into separate pages
I needed to place the payment accounts section somewhere, and the most
logical place seemed to be that dialog, where users can set up company
parameters.
However, that dialog was already saturated with related, but ultimately
independent forms, and adding the account section would make things
even worse, specially given that we need to be able to edit those
accounts in a separate page.
We agreed to separate that dialog into tabs, which means separate pages.
When i had everything in a separated page, then i did not know how to
actually share the code for the tabs, and decided that, for now, these
“tabs” would be items from the profile menu. Same function, different
presentation.
2024-08-14 02:08:13 +00:00
company . Id )
htmxRedirect ( w , r , companyURI ( company , "/tax-details" ) )
}
func mustRenderTaxDetailsForm ( w http . ResponseWriter , r * http . Request , form * taxDetailsForm ) {
page := & TaxDetailsPage {
DetailsForm : form ,
}
page . MustRender ( w , r )
}
func mustGetCompany ( r * http . Request ) * Company {
company := getCompany ( r )
if company == nil {
panic ( errors . New ( "company: required but not found" ) )
}
return company
}
func serveCompanyInvoicingForm ( w http . ResponseWriter , r * http . Request , _ httprouter . Params ) {
conn := getConn ( r )
company := mustGetCompany ( r )
locale := getLocale ( r )
form := newInvoicingFormFromDatabase ( r . Context ( ) , conn , company , locale )
form . MustRender ( w , r )
}
type InvoicingForm struct {
locale * Locale
InvoiceNumberFormat * InputField
NextInvoiceNumber * InputField
QuoteNumberFormat * InputField
NextQuoteNumber * InputField
LegalDisclaimer * InputField
}
func newInvoicingForm ( locale * Locale ) * InvoicingForm {
return & InvoicingForm {
locale : locale ,
InvoiceNumberFormat : & InputField {
Name : "invoice_number_format" ,
Label : pgettext ( "input" , "Invoice number format" , locale ) ,
Type : "text" ,
Required : true ,
} ,
NextInvoiceNumber : & InputField {
Name : "next_invoice_number" ,
Label : pgettext ( "input" , "Next invoice number" , locale ) ,
Type : "number" ,
Required : true ,
Attributes : [ ] template . HTMLAttr {
"min=1" ,
} ,
} ,
QuoteNumberFormat : & InputField {
Name : "quote_number_format" ,
Label : pgettext ( "input" , "Quotation number format" , locale ) ,
Type : "text" ,
Required : true ,
} ,
NextQuoteNumber : & InputField {
Name : "next_quotation_number" ,
Label : pgettext ( "input" , "Next quotation number" , locale ) ,
Type : "number" ,
Required : true ,
Attributes : [ ] template . HTMLAttr {
"min=1" ,
} ,
} ,
LegalDisclaimer : & InputField {
Name : "legal_disclaimer" ,
Label : pgettext ( "input" , "Legal disclaimer" , locale ) ,
Type : "textarea" ,
} ,
}
}
func newInvoicingFormFromDatabase ( ctx context . Context , conn * Conn , company * Company , locale * Locale ) * InvoicingForm {
form := newInvoicingForm ( locale )
form . mustFillFromDatabase ( ctx , conn , company )
return form
}
func ( form * InvoicingForm ) mustFillFromDatabase ( ctx context . Context , conn * Conn , company * Company ) {
err := conn . QueryRow ( ctx , `
select invoice_number_format
, quote_number_format
, legal_disclaimer
, coalesce ( invoice_number_counter . currval , 0 ) + 1
, coalesce ( quote_number_counter . currval , 0 ) + 1
from company
left join invoice_number_counter
on invoice_number_counter . company_id = company . company_id
and invoice_number_counter . year = date_part ( ' year ' , current_date )
left join quote_number_counter
on quote_number_counter . company_id = company . company_id
and quote_number_counter . year = date_part ( ' year ' , current_date )
where company . company_id = $ 1 ` , company . Id ) . Scan (
form . InvoiceNumberFormat ,
form . QuoteNumberFormat ,
form . LegalDisclaimer ,
form . NextInvoiceNumber ,
form . NextQuoteNumber ,
)
if err != nil {
panic ( err )
}
}
func ( form * InvoicingForm ) MustRender ( w http . ResponseWriter , r * http . Request ) {
mustRenderMainTemplate ( w , r , "company/invoicing.gohtml" , form )
}
func ( form * InvoicingForm ) Parse ( r * http . Request ) error {
if err := r . ParseForm ( ) ; err != nil {
return err
}
form . InvoiceNumberFormat . FillValue ( r )
form . NextInvoiceNumber . FillValue ( r )
form . QuoteNumberFormat . FillValue ( r )
form . NextQuoteNumber . FillValue ( r )
form . LegalDisclaimer . FillValue ( r )
return nil
}
func ( form * InvoicingForm ) Validate ( ) bool {
validator := newFormValidator ( )
validator . CheckRequiredInput ( form . InvoiceNumberFormat , gettext ( "Invoice number format can not be empty." , form . locale ) )
validator . CheckValidInteger ( form . NextInvoiceNumber , 1 , math . MaxInt32 , gettext ( "Next invoice number must be a number greater than zero." , form . locale ) )
validator . CheckRequiredInput ( form . QuoteNumberFormat , gettext ( "Quotation number format can not be empty." , form . locale ) )
validator . CheckValidInteger ( form . NextQuoteNumber , 1 , math . MaxInt32 , gettext ( "Next quotation number must be a number greater than zero." , form . locale ) )
return validator . AllOK ( )
}
func handleCompanyInvoicingForm ( w http . ResponseWriter , r * http . Request , _ httprouter . Params ) {
locale := getLocale ( r )
conn := getConn ( r )
form := newInvoicingForm ( 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 ok := form . Validate ( ) ; ! ok {
if ! IsHTMxRequest ( r ) {
w . WriteHeader ( http . StatusUnprocessableEntity )
}
form . MustRender ( w , r )
return
}
company := mustGetCompany ( r )
tx := conn . MustBegin ( r . Context ( ) )
defer tx . MustRollback ( r . Context ( ) )
tx . MustExec ( r . Context ( ) , `
update company
set invoice_number_format = $ 1
, quote_number_format = $ 2
, legal_disclaimer = $ 3
where company_id = $ 4
` ,
2023-06-09 10:43:50 +00:00
form . InvoiceNumberFormat ,
form . QuoteNumberFormat ,
form . LegalDisclaimer ,
company . Id )
tx . MustExec ( r . Context ( ) , `
insert into invoice_number_counter ( company_id , year , currval )
values ( $ 1 , date_part ( ' year ' , current_date ) , $ 2 )
on conflict ( company_id , year ) do update
set currval = excluded . currval
` ,
company . Id ,
form . NextInvoiceNumber . Integer ( ) - 1 )
tx . MustExec ( r . Context ( ) , `
insert into quote_number_counter ( company_id , year , currval )
values ( $ 1 , date_part ( ' year ' , current_date ) , $ 2 )
on conflict ( company_id , year ) do update
set currval = excluded . currval
` ,
company . Id ,
form . NextQuoteNumber . Integer ( ) - 1 )
tx . MustCommit ( r . Context ( ) )
Split the tax details “mega dialog” into separate pages
I needed to place the payment accounts section somewhere, and the most
logical place seemed to be that dialog, where users can set up company
parameters.
However, that dialog was already saturated with related, but ultimately
independent forms, and adding the account section would make things
even worse, specially given that we need to be able to edit those
accounts in a separate page.
We agreed to separate that dialog into tabs, which means separate pages.
When i had everything in a separated page, then i did not know how to
actually share the code for the tabs, and decided that, for now, these
“tabs” would be items from the profile menu. Same function, different
presentation.
2024-08-14 02:08:13 +00:00
htmxRedirect ( w , r , companyURI ( company , "/invoicing" ) )
2023-02-03 11:30:56 +00:00
}
Split the tax details “mega dialog” into separate pages
I needed to place the payment accounts section somewhere, and the most
logical place seemed to be that dialog, where users can set up company
parameters.
However, that dialog was already saturated with related, but ultimately
independent forms, and adding the account section would make things
even worse, specially given that we need to be able to edit those
accounts in a separate page.
We agreed to separate that dialog into tabs, which means separate pages.
When i had everything in a separated page, then i did not know how to
actually share the code for the tabs, and decided that, for now, these
“tabs” would be items from the profile menu. Same function, different
presentation.
2024-08-14 02:08:13 +00:00
func serveCompanyTaxes ( w http . ResponseWriter , r * http . Request , _ httprouter . Params ) {
2023-03-03 15:49:06 +00:00
conn := getConn ( r )
Split the tax details “mega dialog” into separate pages
I needed to place the payment accounts section somewhere, and the most
logical place seemed to be that dialog, where users can set up company
parameters.
However, that dialog was already saturated with related, but ultimately
independent forms, and adding the account section would make things
even worse, specially given that we need to be able to edit those
accounts in a separate page.
We agreed to separate that dialog into tabs, which means separate pages.
When i had everything in a separated page, then i did not know how to
actually share the code for the tabs, and decided that, for now, these
“tabs” would be items from the profile menu. Same function, different
presentation.
2024-08-14 02:08:13 +00:00
company := mustGetCompany ( r )
2023-03-03 15:49:06 +00:00
locale := getLocale ( r )
Split the tax details “mega dialog” into separate pages
I needed to place the payment accounts section somewhere, and the most
logical place seemed to be that dialog, where users can set up company
parameters.
However, that dialog was already saturated with related, but ultimately
independent forms, and adding the account section would make things
even worse, specially given that we need to be able to edit those
accounts in a separate page.
We agreed to separate that dialog into tabs, which means separate pages.
When i had everything in a separated page, then i did not know how to
actually share the code for the tabs, and decided that, for now, these
“tabs” would be items from the profile menu. Same function, different
presentation.
2024-08-14 02:08:13 +00:00
page := newTaxesPage ( r . Context ( ) , conn , company , locale )
page . MustRender ( w , r )
2023-02-03 11:30:56 +00:00
}
Split the tax details “mega dialog” into separate pages
I needed to place the payment accounts section somewhere, and the most
logical place seemed to be that dialog, where users can set up company
parameters.
However, that dialog was already saturated with related, but ultimately
independent forms, and adding the account section would make things
even worse, specially given that we need to be able to edit those
accounts in a separate page.
We agreed to separate that dialog into tabs, which means separate pages.
When i had everything in a separated page, then i did not know how to
actually share the code for the tabs, and decided that, for now, these
“tabs” would be items from the profile menu. Same function, different
presentation.
2024-08-14 02:08:13 +00:00
type TaxesPage struct {
Taxes [ ] * Tax
Form * taxForm
2023-03-03 15:49:06 +00:00
}
Split the tax details “mega dialog” into separate pages
I needed to place the payment accounts section somewhere, and the most
logical place seemed to be that dialog, where users can set up company
parameters.
However, that dialog was already saturated with related, but ultimately
independent forms, and adding the account section would make things
even worse, specially given that we need to be able to edit those
accounts in a separate page.
We agreed to separate that dialog into tabs, which means separate pages.
When i had everything in a separated page, then i did not know how to
actually share the code for the tabs, and decided that, for now, these
“tabs” would be items from the profile menu. Same function, different
presentation.
2024-08-14 02:08:13 +00:00
func newTaxesPage ( ctx context . Context , conn * Conn , company * Company , locale * Locale ) * TaxesPage {
form := newTaxForm ( ctx , conn , company , locale )
return newTaxesPageWithForm ( ctx , conn , company , form )
2023-02-03 11:30:56 +00:00
}
Split the tax details “mega dialog” into separate pages
I needed to place the payment accounts section somewhere, and the most
logical place seemed to be that dialog, where users can set up company
parameters.
However, that dialog was already saturated with related, but ultimately
independent forms, and adding the account section would make things
even worse, specially given that we need to be able to edit those
accounts in a separate page.
We agreed to separate that dialog into tabs, which means separate pages.
When i had everything in a separated page, then i did not know how to
actually share the code for the tabs, and decided that, for now, these
“tabs” would be items from the profile menu. Same function, different
presentation.
2024-08-14 02:08:13 +00:00
func newTaxesPageWithForm ( ctx context . Context , conn * Conn , company * Company , form * taxForm ) * TaxesPage {
return & TaxesPage {
Taxes : mustCollectTaxes ( ctx , conn , company ) ,
Form : 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
}
Split the tax details “mega dialog” into separate pages
I needed to place the payment accounts section somewhere, and the most
logical place seemed to be that dialog, where users can set up company
parameters.
However, that dialog was already saturated with related, but ultimately
independent forms, and adding the account section would make things
even worse, specially given that we need to be able to edit those
accounts in a separate page.
We agreed to separate that dialog into tabs, which means separate pages.
When i had everything in a separated page, then i did not know how to
actually share the code for the tabs, and decided that, for now, these
“tabs” would be items from the profile menu. Same function, different
presentation.
2024-08-14 02:08:13 +00:00
func ( page * TaxesPage ) MustRender ( w http . ResponseWriter , r * http . Request ) {
mustRenderMainTemplate ( w , r , "company/taxes.gohtml" , page )
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
Split the tax details “mega dialog” into separate pages
I needed to place the payment accounts section somewhere, and the most
logical place seemed to be that dialog, where users can set up company
parameters.
However, that dialog was already saturated with related, but ultimately
independent forms, and adding the account section would make things
even worse, specially given that we need to be able to edit those
accounts in a separate page.
We agreed to separate that dialog into tabs, which means separate pages.
When i had everything in a separated page, then i did not know how to
actually share the code for the tabs, and decided that, for now, these
“tabs” would be items from the profile menu. Same function, different
presentation.
2024-08-14 02:08:13 +00:00
func mustCollectTaxes ( ctx context . Context , conn * Conn , company * Company ) [ ] * Tax {
2023-02-28 11:02:27 +00:00
rows , err := conn . Query ( ctx , "select tax_id, tax.name, tax_class.name, (rate * 100)::integer from tax join tax_class using (tax_class_id) where tax.company_id = $1 order by rate, tax.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 { }
2023-02-28 11:02:27 +00:00
err = rows . Scan ( & tax . Id , & tax . Name , & tax . Class , & 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-03 11:30:56 +00:00
type taxForm struct {
2023-02-01 13:15:02 +00:00
locale * Locale
Name * InputField
2023-02-28 11:02:27 +00:00
Class * SelectField
2023-02-01 13:15:02 +00:00
Rate * InputField
}
2023-01-28 11:24:52 +00:00
2023-02-28 11:02:27 +00:00
func newTaxForm ( ctx context . Context , conn * Conn , company * Company , locale * Locale ) * taxForm {
2023-02-03 11:30:56 +00:00
return & taxForm {
2023-02-01 13:15:02 +00:00
locale : locale ,
Name : & InputField {
Name : "tax_name" ,
Label : pgettext ( "input" , "Tax name" , locale ) ,
Type : "text" ,
Required : true ,
} ,
2023-02-28 11:02:27 +00:00
Class : & SelectField {
Name : "tax_class" ,
Label : pgettext ( "input" , "Tax Class" , locale ) ,
Options : MustGetOptions ( ctx , conn , "select tax_class_id::text, name from tax_class where company_id = $1 order by name" , company . Id ) ,
Required : true ,
EmptyLabel : gettext ( "Select a tax class" , locale ) ,
} ,
2023-02-01 13:15:02 +00:00
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-03 11:30:56 +00:00
func ( form * taxForm ) Parse ( r * http . Request ) error {
2023-02-01 13:15:02 +00:00
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 )
2023-02-28 11:02:27 +00:00
form . Class . FillValue ( r )
2023-02-01 13:15:02 +00:00
form . Rate . FillValue ( r )
return nil
}
2023-01-28 13:18:58 +00:00
2023-02-03 11:30:56 +00:00
func ( form * taxForm ) Validate ( ) bool {
2023-02-01 13:15:02 +00:00
validator := newFormValidator ( )
validator . CheckRequiredInput ( form . Name , gettext ( "Tax name can not be empty." , form . locale ) )
2023-02-28 11:02:27 +00:00
validator . CheckValidSelectOption ( form . Class , gettext ( "Selected tax class is not valid." , form . locale ) )
2023-02-01 13:15:02 +00:00
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
}
2023-02-03 11:30:56 +00:00
func HandleAddCompanyTax ( w http . ResponseWriter , r * http . Request , _ httprouter . Params ) {
locale := getLocale ( r )
2023-02-28 11:02:27 +00:00
conn := getConn ( r )
company := mustGetCompany ( r )
form := newTaxForm ( r . Context ( ) , conn , company , locale )
2023-02-03 11:30:56 +00:00
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 ( ) {
2023-03-21 10:58:54 +00:00
if ! IsHTMxRequest ( r ) {
w . WriteHeader ( http . StatusUnprocessableEntity )
}
Split the tax details “mega dialog” into separate pages
I needed to place the payment accounts section somewhere, and the most
logical place seemed to be that dialog, where users can set up company
parameters.
However, that dialog was already saturated with related, but ultimately
independent forms, and adding the account section would make things
even worse, specially given that we need to be able to edit those
accounts in a separate page.
We agreed to separate that dialog into tabs, which means separate pages.
When i had everything in a separated page, then i did not know how to
actually share the code for the tabs, and decided that, for now, these
“tabs” would be items from the profile menu. Same function, different
presentation.
2024-08-14 02:08:13 +00:00
page := newTaxesPageWithForm ( r . Context ( ) , conn , company , form )
page . MustRender ( w , r )
2023-02-03 11:30:56 +00:00
return
}
2023-02-28 11:02:27 +00:00
conn . MustExec ( r . Context ( ) , "insert into tax (company_id, tax_class_id, name, rate) values ($1, $2, $3, $4 / 100::decimal)" , company . Id , form . Class , form . Name , form . Rate . Integer ( ) )
Split the tax details “mega dialog” into separate pages
I needed to place the payment accounts section somewhere, and the most
logical place seemed to be that dialog, where users can set up company
parameters.
However, that dialog was already saturated with related, but ultimately
independent forms, and adding the account section would make things
even worse, specially given that we need to be able to edit those
accounts in a separate page.
We agreed to separate that dialog into tabs, which means separate pages.
When i had everything in a separated page, then i did not know how to
actually share the code for the tabs, and decided that, for now, these
“tabs” would be items from the profile menu. Same function, different
presentation.
2024-08-14 02:08:13 +00:00
htmxRedirect ( w , r , companyURI ( company , "/taxes" ) )
}
func HandleDeleteCompanyTax ( w http . ResponseWriter , r * http . Request , params httprouter . Params ) {
taxId , err := strconv . Atoi ( params [ 0 ] . Value )
if err != nil {
http . NotFound ( w , r )
return
}
if err := verifyCsrfTokenValid ( r ) ; err != nil {
http . Error ( w , err . Error ( ) , http . StatusForbidden )
return
}
conn := getConn ( r )
conn . MustExec ( r . Context ( ) , "delete from tax where tax_id = $1" , taxId )
company := mustGetCompany ( r )
htmxRedirect ( w , r , companyURI ( company , "/taxes" ) )
}
func servePaymentMethods ( w http . ResponseWriter , r * http . Request , _ httprouter . Params ) {
conn := getConn ( r )
company := mustGetCompany ( r )
locale := getLocale ( r )
page := newPaymentMethodsPage ( r . Context ( ) , conn , company , locale )
page . MustRender ( w , r )
}
type PaymentMethodsPage struct {
PaymentMethods [ ] * PaymentMethod
Form * paymentMethodForm
}
func newPaymentMethodsPage ( ctx context . Context , conn * Conn , company * Company , locale * Locale ) * PaymentMethodsPage {
form := newPaymentMethodForm ( locale )
return newPaymentMethodsPageWithForm ( ctx , conn , company , form )
}
func newPaymentMethodsPageWithForm ( ctx context . Context , conn * Conn , company * Company , form * paymentMethodForm ) * PaymentMethodsPage {
return & PaymentMethodsPage {
PaymentMethods : mustCollectPaymentMethods ( ctx , conn , company ) ,
Form : form ,
2023-03-21 10:58:54 +00:00
}
2023-01-28 13:18:58 +00:00
}
2023-03-03 15:49:06 +00:00
Split the tax details “mega dialog” into separate pages
I needed to place the payment accounts section somewhere, and the most
logical place seemed to be that dialog, where users can set up company
parameters.
However, that dialog was already saturated with related, but ultimately
independent forms, and adding the account section would make things
even worse, specially given that we need to be able to edit those
accounts in a separate page.
We agreed to separate that dialog into tabs, which means separate pages.
When i had everything in a separated page, then i did not know how to
actually share the code for the tabs, and decided that, for now, these
“tabs” would be items from the profile menu. Same function, different
presentation.
2024-08-14 02:08:13 +00:00
func ( page * PaymentMethodsPage ) MustRender ( w http . ResponseWriter , r * http . Request ) {
mustRenderMainTemplate ( w , r , "company/payment_methods.gohtml" , page )
}
func mustCollectPaymentMethods ( ctx context . Context , conn * Conn , company * Company ) [ ] * PaymentMethod {
rows , err := conn . Query ( ctx , "select payment_method_id, name, instructions from payment_method where company_id = $1 order by name" , company . Id )
if err != nil {
panic ( err )
}
defer rows . Close ( )
var methods [ ] * PaymentMethod
for rows . Next ( ) {
method := & PaymentMethod { }
err = rows . Scan ( & method . Id , & method . Name , & method . Instructions )
if err != nil {
panic ( err )
}
methods = append ( methods , method )
}
if rows . Err ( ) != nil {
panic ( rows . Err ( ) )
}
return methods
}
2023-03-03 15:49:06 +00:00
type paymentMethodForm struct {
locale * Locale
Name * InputField
Instructions * InputField
}
func newPaymentMethodForm ( locale * Locale ) * paymentMethodForm {
return & paymentMethodForm {
locale : locale ,
Name : & InputField {
Name : "method_name" ,
Label : pgettext ( "input" , "Payment method name" , locale ) ,
Type : "text" ,
Required : true ,
} ,
Instructions : & InputField {
Name : "method_instructions" ,
Label : pgettext ( "input" , "Instructions" , locale ) ,
Type : "textarea" ,
Required : true ,
} ,
}
}
func ( form * paymentMethodForm ) Parse ( r * http . Request ) error {
if err := r . ParseForm ( ) ; err != nil {
return err
}
form . Name . FillValue ( r )
form . Instructions . FillValue ( r )
return nil
}
func ( form * paymentMethodForm ) Validate ( ) bool {
validator := newFormValidator ( )
validator . CheckRequiredInput ( form . Name , gettext ( "Payment method name can not be empty." , form . locale ) )
validator . CheckRequiredInput ( form . Instructions , gettext ( "Payment instructions can not be empty." , form . locale ) )
return validator . AllOK ( )
}
func HandleAddPaymentMethod ( w http . ResponseWriter , r * http . Request , _ httprouter . Params ) {
locale := getLocale ( r )
conn := getConn ( r )
company := mustGetCompany ( r )
form := newPaymentMethodForm ( 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 ( ) {
2023-03-21 10:58:54 +00:00
if ! IsHTMxRequest ( r ) {
w . WriteHeader ( http . StatusUnprocessableEntity )
}
Split the tax details “mega dialog” into separate pages
I needed to place the payment accounts section somewhere, and the most
logical place seemed to be that dialog, where users can set up company
parameters.
However, that dialog was already saturated with related, but ultimately
independent forms, and adding the account section would make things
even worse, specially given that we need to be able to edit those
accounts in a separate page.
We agreed to separate that dialog into tabs, which means separate pages.
When i had everything in a separated page, then i did not know how to
actually share the code for the tabs, and decided that, for now, these
“tabs” would be items from the profile menu. Same function, different
presentation.
2024-08-14 02:08:13 +00:00
page := newPaymentMethodsPageWithForm ( r . Context ( ) , conn , company , form )
page . MustRender ( w , r )
2023-03-03 15:49:06 +00:00
return
}
conn . MustExec ( r . Context ( ) , "insert into payment_method (company_id, name, instructions) values ($1, $2, $3)" , company . Id , form . Name , form . Instructions )
Split the tax details “mega dialog” into separate pages
I needed to place the payment accounts section somewhere, and the most
logical place seemed to be that dialog, where users can set up company
parameters.
However, that dialog was already saturated with related, but ultimately
independent forms, and adding the account section would make things
even worse, specially given that we need to be able to edit those
accounts in a separate page.
We agreed to separate that dialog into tabs, which means separate pages.
When i had everything in a separated page, then i did not know how to
actually share the code for the tabs, and decided that, for now, these
“tabs” would be items from the profile menu. Same function, different
presentation.
2024-08-14 02:08:13 +00:00
htmxRedirect ( w , r , companyURI ( company , "/payment-methods" ) )
}
func HandleDeletePaymentMethod ( w http . ResponseWriter , r * http . Request , params httprouter . Params ) {
paymentMethodId , err := strconv . Atoi ( params [ 0 ] . Value )
if err != nil {
http . NotFound ( w , r )
return
}
if err := verifyCsrfTokenValid ( r ) ; err != nil {
http . Error ( w , err . Error ( ) , http . StatusForbidden )
return
2023-03-21 10:58:54 +00:00
}
Split the tax details “mega dialog” into separate pages
I needed to place the payment accounts section somewhere, and the most
logical place seemed to be that dialog, where users can set up company
parameters.
However, that dialog was already saturated with related, but ultimately
independent forms, and adding the account section would make things
even worse, specially given that we need to be able to edit those
accounts in a separate page.
We agreed to separate that dialog into tabs, which means separate pages.
When i had everything in a separated page, then i did not know how to
actually share the code for the tabs, and decided that, for now, these
“tabs” would be items from the profile menu. Same function, different
presentation.
2024-08-14 02:08:13 +00:00
conn := getConn ( r )
conn . MustExec ( r . Context ( ) , "delete from payment_method where payment_method_id = $1" , paymentMethodId )
company := mustGetCompany ( r )
htmxRedirect ( w , r , companyURI ( company , "/payment-methods" ) )
2023-03-03 15:49:06 +00:00
}
2023-11-06 12:52:34 +00:00
func GetCompanySwitcher ( w http . ResponseWriter , r * http . Request , _ httprouter . Params ) {
page := & CompanySwitchPage {
Companies : mustCollectUserCompanies ( r . Context ( ) , getConn ( r ) ) ,
}
page . MustRender ( w , r )
}
type CompanySwitchPage struct {
Companies [ ] * UserCompany
}
type UserCompany struct {
Name string
Slug string
}
func ( page * CompanySwitchPage ) MustRender ( w http . ResponseWriter , r * http . Request ) {
Split the tax details “mega dialog” into separate pages
I needed to place the payment accounts section somewhere, and the most
logical place seemed to be that dialog, where users can set up company
parameters.
However, that dialog was already saturated with related, but ultimately
independent forms, and adding the account section would make things
even worse, specially given that we need to be able to edit those
accounts in a separate page.
We agreed to separate that dialog into tabs, which means separate pages.
When i had everything in a separated page, then i did not know how to
actually share the code for the tabs, and decided that, for now, these
“tabs” would be items from the profile menu. Same function, different
presentation.
2024-08-14 02:08:13 +00:00
mustRenderModalTemplate ( w , r , "company/switch.gohtml" , page )
2023-11-06 12:52:34 +00:00
}
func mustCollectUserCompanies ( ctx context . Context , conn * Conn ) [ ] * UserCompany {
rows , err := conn . Query ( ctx , "select business_name::text, slug::text from company order by business_name" )
if err != nil {
panic ( err )
}
defer rows . Close ( )
var companies [ ] * UserCompany
for rows . Next ( ) {
company := & UserCompany { }
err = rows . Scan ( & company . Name , & company . Slug )
if err != nil {
panic ( err )
}
companies = append ( companies , company )
}
if rows . Err ( ) != nil {
panic ( rows . Err ( ) )
}
return companies
}