Compare commits

..

No commits in common. "627841d4ddca191eb777ff6be2851665d9a814a9" and "d9c93b8797adb31e454652d5a2de87ff6bf06ec5" have entirely different histories.

41 changed files with 46 additions and 955 deletions

View File

@ -2,17 +2,9 @@ begin;
set search_path to auth, numerus, public;
insert into auth."user" (user_id, email, name, password, role)
values (1, 'demo@numerus', 'Demo User', 'demo', 'invoicer')
, (2, 'admin@numerus', 'Demo Admin', 'admin', 'admin')
;
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country, currency_code)
values (1, 'Juli Verd', 'ES40404040D', 'Pesebre', parse_packed_phone_number('972 50 60 70', 'ES'), 'info@numerus.cat', 'https://numerus.cat/', 'C/ de lHort', 'Castelló dEmpúries', 'Alt Empordà', '17486', 'Espanya', 'EUR');
insert into company_user (company_id, user_id)
values (1, 1)
, (1, 2)
insert into auth."user" (email, name, password, role)
values ('demo@numerus', 'Demo User', 'demo', 'invoicer')
, ('admin@numerus', 'Demo Admin', 'admin', 'admin')
;
commit;

View File

@ -1,12 +0,0 @@
-- Deploy numerus:available_currencies to pg
-- requires: schema_numerus
-- requires: currency
begin;
insert into numerus.currency(currency_code, currency_symbol)
values ('EUR', '')
, ('USD', '$')
;
commit;

View File

@ -1,36 +0,0 @@
-- Deploy numerus:company to pg
-- requires: schema_numerus
-- requires: extension_vat
-- requires: email
-- requires: extension_pg_libphonenumber
-- requires: extension_uri
-- requires: currency_code
-- requires: currency
begin;
set search_path to numerus,public;
create table company (
company_id serial primary key,
slug uuid not null unique default gen_random_uuid(),
business_name text not null,
vatin vatin not null,
trade_name text not null,
phone packed_phone_number not null,
email email not null,
web uri not null,
address text not null,
city text not null,
province text not null,
postal_code text not null,
country text not null,
currency_code currency_code not null references currency,
created_at timestamptz not null default current_timestamp
);
grant select on table company to invoicer;
grant select on table company to admin;
commit;

View File

@ -1,38 +0,0 @@
-- Deploy numerus:company_user to pg
-- requires: schema_numerus
-- requires: user
-- requires: company
begin;
set search_path to numerus, auth, public;
create table company_user (
company_id integer not null references company,
user_id integer not null references "user",
primary key (company_id, user_id)
);
grant select on table company_user to invoicer;
grant select on table company_user to admin;
alter table company enable row level security;
create policy company_policy
on company
using (
exists(
select 1
from company_user
join user_profile using (user_id)
where company_user.company_id = company.company_id
)
);
-- TODO:
-- I think we can not do the same for company_user because it would be
-- an infinite loop, but in this case i think it is fine because we can
-- only see ids, nothing more.
commit;

View File

@ -1,18 +0,0 @@
-- Deploy numerus:currency to pg
-- requires: schema_numerus
-- requires: currency_code
begin;
set search_path to numerus, public;
create table currency (
currency_code currency_code not null primary key,
currency_symbol text not null,
decimal_digits integer not null default 2
);
grant select on table currency to invoicer;
grant select on table currency to admin;
commit;

View File

@ -1,14 +0,0 @@
-- Deploy numerus:currency_code to pg
-- requires: schema_numerus
begin;
set search_path to numerus, public;
create domain currency_code as text
check (value ~ '^[A-Z]{3}$');
comment on domain currency_code is
'A correctly formated, but not necessarily valid, ISO 4217 currency code';
commit;

View File

@ -1,8 +0,0 @@
-- Deploy numerus:extension_pg_libphonenumber to pg
-- requires: schema_public
begin;
create extension if not exists pg_libphonenumber;
commit;

View File

@ -1,8 +0,0 @@
-- Deploy numerus:extension_uri to pg
-- requires: schema_public
begin;
create extension if not exists uri;
commit;

View File

@ -1,8 +0,0 @@
-- Deploy numerus:extension_vat to pg
-- requires: schema_public
begin;
create extension if not exists vat;
commit;

View File

@ -1,105 +0,0 @@
package pkg
import (
"context"
"errors"
"net/http"
"net/url"
"strings"
)
const (
ContextCompanyKey = "numerus-company"
)
type Company struct {
Id int
Slug string
}
func CompanyHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
slug := r.URL.Path
if idx := strings.IndexByte(slug, '/'); idx >= 0 {
slug = slug[:idx]
}
conn := getConn(r)
company := &Company{
Slug: slug,
}
err := conn.QueryRow(r.Context(), "select company_id from company where slug = $1", slug).Scan(&company.Id)
if err != nil {
http.NotFound(w, r)
return
}
ctx := context.WithValue(r.Context(), ContextCompanyKey, company)
r = r.WithContext(ctx)
// Same as StripPrefix
p := strings.TrimPrefix(r.URL.Path, slug)
rp := strings.TrimPrefix(r.URL.RawPath, slug)
if len(p) < len(r.URL.Path) && (r.URL.RawPath == "" || len(rp) < len(r.URL.RawPath)) {
r2 := new(http.Request)
*r2 = *r
r2.URL = new(url.URL)
*r2.URL = *r.URL
if p == "" {
r2.URL.Path = "/"
} else {
r2.URL.Path = p
}
r2.URL.RawPath = rp
next.ServeHTTP(w, r2)
} else {
http.NotFound(w, r)
}
})
}
func getCompany(r *http.Request) *Company {
company := r.Context().Value(ContextCompanyKey)
if company == nil {
return nil
}
return company.(*Company)
}
type TaxDetailsPage struct {
Title string
BusinessName string
VATIN string
TradeName string
Phone string
Email string
Web string
Address string
City string
Province string
PostalCode string
Country string
}
func CompanyTaxDetailsHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
locale := getLocale(r)
page := &TaxDetailsPage{
Title: pgettext("title", "Tax Details", locale),
}
company := mustGetCompany(r)
conn := getConn(r)
err := conn.QueryRow(r.Context(), "select business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country from company where company_id = $1", company.Id).Scan(&page.BusinessName, &page.VATIN, &page.TradeName, &page.Phone, &page.Email, &page.Web, &page.Address, &page.City, &page.Province, &page.PostalCode, &page.Country);
if err != nil {
panic(err)
}
mustRenderAppTemplate(w, r, "tax-details.html", page)
});
}
func mustGetCompany(r *http.Request) *Company {
company := getCompany(r)
if company == nil {
panic(errors.New("company: required but not found"))
}
return company;
}

View File

@ -61,9 +61,13 @@ func LoginHandler() http.Handler {
func LogoutHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
conn := getConn(r)
conn.MustExec(r.Context(), "select logout()")
http.SetCookie(w, createSessionCookie("", -24*time.Hour))
user := getUser(r)
if user.LoggedIn {
conn := getConn(r)
conn.MustExec(r.Context(), "select logout()")
http.SetCookie(w, createSessionCookie("", -24*time.Hour))
}
http.Redirect(w, r, "/login", http.StatusSeeOther)
})
}
@ -126,14 +130,3 @@ func getUser(r *http.Request) *AppUser {
func getConn(r *http.Request) *Conn {
return r.Context().Value(ContextConnKey).(*Conn)
}
func Authenticated(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user := getUser(r)
if user.LoggedIn {
next.ServeHTTP(w, r);
} else {
http.Redirect(w, r, "/login", http.StatusSeeOther)
}
});
}

View File

@ -23,6 +23,10 @@ type ProfilePage struct {
func ProfileHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user := getUser(r)
if !user.LoggedIn {
http.Redirect(w, r, "/login", http.StatusUnauthorized)
return
}
conn := getConn(r)
locale := getLocale(r)
page := ProfilePage{

View File

@ -5,28 +5,15 @@ import (
)
func NewRouter(db *Db) http.Handler {
companyRouter := http.NewServeMux()
companyRouter.Handle("/tax-details", CompanyTaxDetailsHandler())
companyRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
mustRenderAppTemplate(w, r, "dashboard.html", nil)
})
router := http.NewServeMux()
router.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("web/static"))))
router.Handle("/login", LoginHandler())
router.Handle("/logout", Authenticated(LogoutHandler()))
router.Handle("/profile", Authenticated(ProfileHandler()))
router.Handle("/company/", Authenticated(http.StripPrefix("/company/", CompanyHandler(companyRouter))))
router.Handle("/logout", LogoutHandler())
router.Handle("/profile", ProfileHandler())
router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
user := getUser(r)
if user.LoggedIn {
conn := getConn(r)
var slug string
err := conn.QueryRow(r.Context(), "select slug::text from company order by company_id limit 1").Scan(&slug)
if err != nil {
panic(err)
}
http.Redirect(w, r, "/company/"+slug, http.StatusFound)
mustRenderAppTemplate(w, r, "dashboard.html", nil)
} else {
http.Redirect(w, r, "/login", http.StatusSeeOther)
}

View File

@ -12,17 +12,10 @@ func templateFile (name string) string {
func mustRenderTemplate(wr io.Writer, r *http.Request, layout string, filename string, data interface{}) {
locale := getLocale(r)
company := getCompany(r)
t := template.New(filename)
t.Funcs(template.FuncMap{
"gettext": locale.Get,
"pgettext": locale.GetC,
"companyURI": func(uri string) string {
if company == nil {
return uri;
}
return "/company/" + company.Slug + uri;
},
})
if _, err := t.ParseFiles(templateFile(filename), templateFile(layout)); err != nil {
panic(err)

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: numerus\n"
"Report-Msgid-Bugs-To: jordi@tandem.blog\n"
"POT-Creation-Date: 2023-01-24 21:37+0100\n"
"POT-Creation-Date: 2023-01-23 18:50+0100\n"
"PO-Revision-Date: 2023-01-18 17:08+0100\n"
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
"Language-Team: Catalan <ca@dodds.net>\n"
@ -26,13 +26,12 @@ msgstr "Entrada"
msgid "Invalid user or password"
msgstr "Nom dusuari o contrasenya incorrectes"
#: web/template/login.html:13 web/template/profile.html:15
#: web/template/tax-details.html:23
#: web/template/login.html:13 web/template/profile.html:14
msgctxt "input"
msgid "Email"
msgstr "Correu-e"
#: web/template/login.html:18 web/template/profile.html:23
#: web/template/login.html:18 web/template/profile.html:22
msgctxt "input"
msgid "Password"
msgstr "Contrasenya"
@ -42,112 +41,51 @@ msgctxt "action"
msgid "Login"
msgstr "Entra"
#: web/template/profile.html:3 pkg/profile.go:29
#: web/template/profile.html:2 pkg/profile.go:33
msgctxt "title"
msgid "User Settings"
msgstr "Configuració usuari"
#: web/template/profile.html:6
#: web/template/profile.html:5
msgctxt "title"
msgid "User Access Data"
msgstr "Dades accés usuari"
#: web/template/profile.html:10
#: web/template/profile.html:9
msgctxt "input"
msgid "User name"
msgstr "Nom dusuari"
#: web/template/profile.html:19
#: web/template/profile.html:18
msgctxt "title"
msgid "Password Change"
msgstr "Canvi contrasenya"
#: web/template/profile.html:28
#: web/template/profile.html:27
msgctxt "input"
msgid "Password Confirmation"
msgstr "Confirmació contrasenya"
#: web/template/profile.html:33
#: web/template/profile.html:31
msgctxt "input"
msgid "Language"
msgstr "Idioma"
#: web/template/profile.html:36
#: web/template/profile.html:33
msgctxt "language option"
msgid "Automatic"
msgstr "Automàtic"
#: web/template/profile.html:42
#: web/template/profile.html:38
msgctxt "action"
msgid "Save changes"
msgstr "Desa canvis"
#: web/template/tax-details.html:3 pkg/company.go:87
msgctxt "title"
msgid "Tax Details"
msgstr "Configuració fiscal"
#: web/template/tax-details.html:7
msgctxt "input"
msgid "Business name"
msgstr "Nom i cognom"
#: web/template/tax-details.html:11
msgctxt "input"
msgid "VAT number"
msgstr "DNI / NIF"
#: web/template/tax-details.html:15
msgctxt "input"
msgid "Trade name"
msgstr "Nom comercial"
#: web/template/tax-details.html:19
msgctxt "input"
msgid "Phone"
msgstr "Telèfon"
#: web/template/tax-details.html:27
msgctxt "input"
msgid "Web"
msgstr "Web"
#: web/template/tax-details.html:31
msgctxt "input"
msgid "Address"
msgstr "Adreça"
#: web/template/tax-details.html:35
msgctxt "input"
msgid "City"
msgstr "Població"
#: web/template/tax-details.html:39
msgctxt "input"
msgid "Province"
msgstr "Província"
#: web/template/tax-details.html:43
msgctxt "input"
msgid "Postal code"
msgstr "Codi postal"
#: web/template/tax-details.html:47
msgctxt "input"
msgid "Country"
msgstr "País"
#: web/template/app.html:20
msgctxt "menu"
msgid "Account"
msgstr "Compte"
#: web/template/app.html:26
msgctxt "menu"
msgid "Tax Details"
msgstr "Configuració fiscal"
#: web/template/app.html:33
#: web/template/app.html:27
msgctxt "action"
msgid "Logout"
msgstr "Surt"

View File

@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: numerus\n"
"Report-Msgid-Bugs-To: jordi@tandem.blog\n"
"POT-Creation-Date: 2023-01-24 21:37+0100\n"
"POT-Creation-Date: 2023-01-23 18:50+0100\n"
"PO-Revision-Date: 2023-01-18 17:45+0100\n"
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
"Language-Team: Spanish <es@tp.org.es>\n"
@ -26,13 +26,12 @@ msgstr "Entrada"
msgid "Invalid user or password"
msgstr "Nombre de usuario o contraseña inválido"
#: web/template/login.html:13 web/template/profile.html:15
#: web/template/tax-details.html:23
#: web/template/login.html:13 web/template/profile.html:14
msgctxt "input"
msgid "Email"
msgstr "Correo-e"
#: web/template/login.html:18 web/template/profile.html:23
#: web/template/login.html:18 web/template/profile.html:22
msgctxt "input"
msgid "Password"
msgstr "Contraseña"
@ -42,112 +41,51 @@ msgctxt "action"
msgid "Login"
msgstr "Entrar"
#: web/template/profile.html:3 pkg/profile.go:29
#: web/template/profile.html:2 pkg/profile.go:33
msgctxt "title"
msgid "User Settings"
msgstr "Configuración usuario"
#: web/template/profile.html:6
#: web/template/profile.html:5
msgctxt "title"
msgid "User Access Data"
msgstr "Datos acceso usuario"
#: web/template/profile.html:10
#: web/template/profile.html:9
msgctxt "input"
msgid "User name"
msgstr "Nombre de usuario"
#: web/template/profile.html:19
#: web/template/profile.html:18
msgctxt "title"
msgid "Password Change"
msgstr "Cambio de contraseña"
#: web/template/profile.html:28
#: web/template/profile.html:27
msgctxt "input"
msgid "Password Confirmation"
msgstr "Confirmación contrasenya"
#: web/template/profile.html:33
#: web/template/profile.html:31
msgctxt "input"
msgid "Language"
msgstr "Idioma"
#: web/template/profile.html:36
#: web/template/profile.html:33
msgctxt "language option"
msgid "Automatic"
msgstr "Automático"
#: web/template/profile.html:42
#: web/template/profile.html:38
msgctxt "action"
msgid "Save changes"
msgstr "Guardar cambios"
#: web/template/tax-details.html:3 pkg/company.go:87
msgctxt "title"
msgid "Tax Details"
msgstr "Configuración fiscal"
#: web/template/tax-details.html:7
msgctxt "input"
msgid "Business name"
msgstr "Nombre y apellidos"
#: web/template/tax-details.html:11
msgctxt "input"
msgid "VAT number"
msgstr "DNI / NIF"
#: web/template/tax-details.html:15
msgctxt "input"
msgid "Trade name"
msgstr "Nombre comercial"
#: web/template/tax-details.html:19
msgctxt "input"
msgid "Phone"
msgstr "Teléfono"
#: web/template/tax-details.html:27
msgctxt "input"
msgid "Web"
msgstr "Web"
#: web/template/tax-details.html:31
msgctxt "input"
msgid "Address"
msgstr "Dirección"
#: web/template/tax-details.html:35
msgctxt "input"
msgid "City"
msgstr "Población"
#: web/template/tax-details.html:39
msgctxt "input"
msgid "Province"
msgstr "Provincia"
#: web/template/tax-details.html:43
msgctxt "input"
msgid "Postal code"
msgstr "Código postal"
#: web/template/tax-details.html:47
msgctxt "input"
msgid "Country"
msgstr "País"
#: web/template/app.html:20
msgctxt "menu"
msgid "Account"
msgstr "Cuenta"
#: web/template/app.html:26
msgctxt "menu"
msgid "Tax Details"
msgstr "Configuración fiscal"
#: web/template/app.html:33
#: web/template/app.html:27
msgctxt "action"
msgid "Logout"
msgstr "Salir"

View File

@ -1,7 +0,0 @@
-- Revert numerus:available_currencies from pg
begin;
delete from numerus.currency;
commit;

View File

@ -1,7 +0,0 @@
-- Revert numerus:company from pg
begin;
drop table numerus.company;
commit;

View File

@ -1,9 +0,0 @@
-- Revert numerus:company_user from pg
begin;
drop policy if exists company_policy on numerus.company;
drop policy if exists company_policy on numerus.company_user;
drop table if exists numerus.company_user;
commit;

View File

@ -1,7 +0,0 @@
-- Revert numerus:currency from pg
begin;
drop table numerus.currency;
commit;

View File

@ -1,7 +0,0 @@
-- Revert numerus:currency_code from pg
begin;
drop domain if exists numerus.currency_code;
commit;

View File

@ -1,7 +0,0 @@
-- Revert numerus:extension_pg_libphonenumber from pg
begin;
drop extension if exists pg_libphonenumber;
commit;

View File

@ -1,7 +0,0 @@
-- Revert numerus:extension_uri from pg
begin;
drop extension if exists uri;
commit;

View File

@ -1,7 +0,0 @@
-- Revert numerus:extension_vat from pg
begin;
drop extension if exists vat;
commit;

View File

@ -9,11 +9,11 @@ schema_numerus [roles] 2023-01-12T22:57:22Z jordi fita mas <jordi@tandem.blog> #
extension_citext [schema_public] 2023-01-12T23:03:33Z jordi fita mas <jordi@tandem.blog> # Add citext extension
email [schema_numerus extension_citext] 2023-01-12T23:09:59Z jordi fita mas <jordi@tandem.blog> # Add email domain
language [schema_numerus] 2023-01-21T20:55:49Z jordi fita mas <jordi@tandem.blog> # Add relation of available languages
user [roles schema_auth email language] 2023-01-12T23:44:03Z jordi fita mas <jordi@tandem.blog> # Create user relation
user [roles schema_auth email language] 2023-01-12T23:44:03Z jordi fita mas <jordi@tandem.blog> # Create user table
ensure_role_exists [schema_auth user] 2023-01-12T23:57:59Z jordi fita mas <jordi@tandem.blog> # Add trigger to ensure the users role exists
extension_pgcrypto [schema_auth] 2023-01-13T00:11:50Z jordi fita mas <jordi@tandem.blog> # Add pgcrypto extension
encrypt_password [schema_auth user extension_pgcrypto] 2023-01-13T00:14:30Z jordi fita mas <jordi@tandem.blog> # Add trigger to encrypt users password
login_attempt [schema_auth] 2023-01-17T14:05:49Z jordi fita mas <jordi@tandem.blog> # Add relation to log login attempts
login_attempt [schema_auth] 2023-01-17T14:05:49Z jordi fita mas <jordi@tandem.blog> # Add table to log login attempts
login [roles schema_numerus schema_auth extension_pgcrypto email user login_attempt] 2023-01-13T00:32:32Z jordi fita mas <jordi@tandem.blog> # Add function to login
current_user_cookie [schema_numerus] 2023-01-21T20:16:28Z jordi fita mas <jordi@tandem.blog> # Add function to get the cookie of the current Numerus user
current_user_email [schema_numerus] 2023-01-23T19:11:53Z jordi fita mas <jordi@tandem.blog> # Add function to get the email of the current Numerus user
@ -24,11 +24,3 @@ set_cookie [schema_public check_cookie] 2023-01-19T11:00:22Z jordi fita mas <jor
available_languages [schema_numerus language] 2023-01-21T21:11:08Z jordi fita mas <jordi@tandem.blog> # Add the initial available languages
user_profile [schema_numerus user current_user_email current_user_cookie] 2023-01-21T23:18:20Z jordi fita mas <jordi@tandem.blog> # Add view for user profile
change_password [schema_numerus user] 2023-01-23T20:22:45Z jordi fita mas <jordi@tandem.blog> # Add function to change the current users password
extension_vat [schema_public] 2023-01-24T10:28:17Z jordi fita mas <jordi@tandem.blog> # Add vat extension
extension_pg_libphonenumber [schema_public] 2023-01-24T13:50:14Z jordi fita mas <jordi@tandem.blog> # Add extension for phone numbers
extension_uri [schema_public] 2023-01-24T14:29:29Z jordi fita mas <jordi@tandem.blog> # Add extension for URIs
currency_code [schema_numerus] 2023-01-24T14:36:04Z jordi fita mas <jordi@tandem.blog> # Add the domain for currency code in ISO 4217
currency [schema_numerus currency_code] 2023-01-24T14:45:26Z jordi fita mas <jordi@tandem.blog> # Add the relation for currencies
available_currencies [schema_numerus currency] 2023-01-24T14:54:18Z jordi fita mas <jordi@tandem.blog> # Add the initial list of available currencies
company [schema_numerus extension_vat email extension_pg_libphonenumber extension_uri currency_code currency] 2023-01-24T15:03:15Z jordi fita mas <jordi@tandem.blog> # Add the relation for companies
company_user [schema_numerus user company] 2023-01-24T17:50:06Z jordi fita mas <jordi@tandem.blog> # Add the relation of companies and their users

View File

@ -1,164 +0,0 @@
-- Test company
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(77);
set search_path to numerus, auth, public;
select has_table('company');
select has_pk('company');
select table_privs_are('company', 'guest', array []::text[]);
select table_privs_are('company', 'invoicer', array ['SELECT']);
select table_privs_are('company', 'admin', array ['SELECT']);
select table_privs_are('company', 'authenticator', array []::text[]);
select has_column('company', 'company_id');
select col_is_pk('company', 'company_id');
select col_type_is('company', 'company_id', 'integer');
select col_not_null('company', 'company_id');
select col_has_default('company', 'company_id');
select col_default_is('company', 'company_id', 'nextval(''company_company_id_seq''::regclass)');
select has_column('company', 'slug');
select col_is_unique('company', 'slug');
select col_type_is('company', 'slug', 'uuid');
select col_not_null('company', 'slug');
select col_has_default('company', 'slug');
select col_default_is('company', 'slug', 'gen_random_uuid()');
select has_column('company', 'business_name');
select col_type_is('company', 'business_name', 'text');
select col_not_null('company', 'business_name');
select col_hasnt_default('company', 'business_name');
select has_column('company', 'vatin');
select col_type_is('company', 'vatin', 'vatin');
select col_not_null('company', 'vatin');
select col_hasnt_default('company', 'vatin');
select has_column('company', 'trade_name');
select col_type_is('company', 'trade_name', 'text');
select col_not_null('company', 'trade_name');
select col_hasnt_default('company', 'trade_name');
select has_column('company', 'phone');
select col_type_is('company', 'phone', 'packed_phone_number');
select col_not_null('company', 'phone');
select col_hasnt_default('company', 'phone');
select has_column('company', 'email');
select col_type_is('company', 'email', 'email');
select col_not_null('company', 'email');
select col_hasnt_default('company', 'email');
select has_column('company', 'web');
select col_type_is('company', 'web', 'uri');
select col_not_null('company', 'web');
select col_hasnt_default('company', 'web');
select has_column('company', 'address');
select col_type_is('company', 'address', 'text');
select col_not_null('company', 'address');
select col_hasnt_default('company', 'address');
select has_column('company', 'city');
select col_type_is('company', 'city', 'text');
select col_not_null('company', 'city');
select col_hasnt_default('company', 'city');
select has_column('company', 'province');
select col_type_is('company', 'province', 'text');
select col_not_null('company', 'province');
select col_hasnt_default('company', 'province');
select has_column('company', 'postal_code');
select col_type_is('company', 'postal_code', 'text');
select col_not_null('company', 'postal_code');
select col_hasnt_default('company', 'postal_code');
select has_column('company', 'country');
select col_type_is('company', 'country', 'text');
select col_not_null('company', 'country');
select col_hasnt_default('company', 'country');
select has_column('company', 'currency_code');
select col_is_fk('company', 'currency_code');
select fk_ok('company', 'currency_code', 'currency', 'currency_code');
select col_type_is('company', 'currency_code', 'currency_code');
select col_not_null('company', 'currency_code');
select col_hasnt_default('company', 'currency_code');
select has_column('company', 'created_at');
select col_type_is('company', 'created_at', 'timestamp with time zone');
select col_not_null('company', 'created_at');
select col_has_default('company', 'created_at');
select col_default_is('company', 'created_at', current_timestamp);
set client_min_messages to warning;
truncate company_user cascade;
truncate company cascade;
truncate auth."user" cascade;
reset client_min_messages;
insert into auth."user" (user_id, email, name, password, role, cookie, cookie_expires_at)
values (1, 'demo@tandem.blog', 'Demo', 'test', 'invoicer', '44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e', current_timestamp + interval '1 month')
, (5, 'admin@tandem.blog', 'Demo', 'test', 'admin', '12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524', current_timestamp + interval '1 month')
;
insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, country, currency_code)
values (2, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', '', 'EUR')
, (4, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', '', 'USD')
, (6, 'Company 6', 'XX345', '', '777-777-777', 'c@c', '', '', '', '', '', '', 'USD')
;
insert into company_user (company_id, user_id)
values (2, 1)
, (2, 5)
, (4, 1)
, (6, 5)
;
prepare company_data as
select company_id, business_name
from company
order by company_id;
set role invoicer;
select is_empty('company_data', 'Should show no data when cookie is not set yet');
reset role;
select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog');
select results_eq(
'company_data',
$$ values ( 2, 'Company 2' )
, ( 4, 'Company 4' )
$$,
'Should only list companies where demo@tandem.blog is user of'
);
reset role;
select set_cookie('12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524/admin@tandem.blog');
select results_eq(
'company_data',
$$ values ( 2, 'Company 2' )
, ( 6, 'Company 6' )
$$,
'Should only list companies where admin@tandem.blog is user of'
);
reset role;
select set_cookie('not-a-cookie');
select throws_ok(
'company_data',
'42501', 'permission denied for table company',
'Should not allow select to guest users'
);
reset role;
select finish();
rollback;

View File

@ -1,39 +0,0 @@
-- Test company_user
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(19);
set search_path to numerus, auth, public;
select has_table('company_user');
select has_pk('company_user' );
select col_is_pk('company_user', array['company_id', 'user_id']);
select table_privs_are('company_user', 'guest', array []::text[]);
select table_privs_are('company_user', 'invoicer', array ['SELECT']);
select table_privs_are('company_user', 'admin', array ['SELECT']);
select table_privs_are('company_user', 'authenticator', array []::text[]);
select has_column('company_user', 'company_id');
select col_is_fk('company_user', 'company_id');
select fk_ok('company_user', 'company_id', 'company', 'company_id');
select col_type_is('company_user', 'company_id', 'integer');
select col_not_null('company_user', 'company_id');
select col_hasnt_default('company_user', 'company_id');
select has_column('company_user', 'user_id');
select col_is_fk('company_user', 'user_id');
select fk_ok('company_user', 'user_id', 'user', 'user_id');
select col_type_is('company_user', 'user_id', 'integer');
select col_not_null('company_user', 'user_id');
select col_hasnt_default('company_user', 'user_id');
select *
from finish();
rollback;

View File

@ -1,42 +0,0 @@
-- Test currency
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(20);
set search_path to numerus, public;
select has_table('currency');
select has_pk('currency');
select table_privs_are('currency', 'guest', array []::text[]);
select table_privs_are('currency', 'invoicer', array ['SELECT']);
select table_privs_are('currency', 'admin', array ['SELECT']);
select table_privs_are('currency', 'authenticator', array []::text[]);
select has_column('currency', 'currency_code');
select col_is_pk('currency', 'currency_code');
select col_type_is('currency', 'currency_code', 'currency_code');
select col_not_null('currency', 'currency_code');
select col_hasnt_default('currency', 'currency_code');
select has_column('currency', 'currency_symbol');
select col_type_is('currency', 'currency_symbol', 'text');
select col_not_null('currency', 'currency_symbol');
select col_hasnt_default('currency', 'currency_symbol');
select has_column('currency', 'decimal_digits');
select col_type_is('currency', 'decimal_digits', 'integer');
select col_not_null('currency', 'decimal_digits');
select col_has_default('currency', 'decimal_digits');
select col_default_is('currency', 'decimal_digits', 2);
set client_min_messages to warning;
truncate currency cascade;
reset client_min_messages;
select finish();
rollback;

View File

@ -1,38 +0,0 @@
-- Test currency_code
set client_min_messages to warning;
create extension if not exists pgtap;
reset client_min_messages;
begin;
select plan(6);
set search_path to numerus, public;
select has_domain('currency_code');
select domain_type_is('currency_code', 'text');
select lives_ok($$ select 'EUR'::currency_code $$, 'Should be able to cast valid text to currency code');
select throws_ok(
$$ SELECT '123'::currency_code $$,
23514, null,
'Should reject numeric text'
);
select throws_ok(
$$ SELECT 'eur'::currency_code $$,
23514, null,
'Should reject lowecase text'
);
select throws_ok(
$$ SELECT 'EURO'::currency_code $$,
23514, null,
'Should reject text longer than three letters'
);
select *
from finish();
rollback;

View File

@ -9,12 +9,9 @@ select plan(1);
select extensions_are(array[
'citext'
, 'pg_libphonenumber'
, 'pgtap'
, 'pgcrypto'
, 'plpgsql'
, 'uri'
, 'vat'
]);
select *

View File

@ -1,21 +0,0 @@
-- Verify numerus:available_currencies on pg
begin;
set search_path to numerus;
select 1 / count(*)
from currency
where currency_code = 'EUR'
and currency_symbol = ''
and decimal_digits = 2
;
select 1 / count(*)
from currency
where currency_code = 'USD'
and currency_symbol = '$'
and decimal_digits = 2
;
rollback;

View File

@ -1,23 +0,0 @@
-- Verify numerus:company on pg
begin;
select company_id
, slug
, business_name
, vatin
, trade_name
, phone
, email
, web
, address
, city
, province
, postal_code
, country
, currency_code
, created_at
from numerus.company
where false;
rollback;

View File

@ -1,10 +0,0 @@
-- Verify numerus:company_user on pg
begin;
select company_id
, user_id
from numerus.company_user
where false;
rollback;

View File

@ -1,11 +0,0 @@
-- Verify numerus:currency on pg
begin;
select currency_code
, currency_symbol
, decimal_digits
from numerus.currency
where false;
rollback;

View File

@ -1,7 +0,0 @@
-- Verify numerus:currency_code on pg
begin;
select pg_catalog.has_type_privilege('numerus.currency_code', 'usage');
rollback;

View File

@ -1,7 +0,0 @@
-- Verify numerus:extension_pg_libphonenumber on pg
begin;
select 1/count(*) from pg_extension where extname = 'pg_libphonenumber';
rollback;

View File

@ -1,7 +0,0 @@
-- Verify numerus:extension_uri on pg
begin;
select 1/count(*) from pg_extension where extname = 'uri';
rollback;

View File

@ -1,7 +0,0 @@
-- Verify numerus:extension_vat on pg
begin;
select 1/count(*) from pg_extension where extname = 'vat';
rollback;

View File

@ -245,10 +245,9 @@ main {
.input {
position: relative;
display: inline-block;
margin-top: 2rem;
}
input[type="text"], input[type="password"], input[type="email"], input[type="tel"], input[type="url"], select {
input[type="text"], input[type="password"], input[type="email"], select {
background-color: var(--numerus--background-color);
border: 1px solid var(--numerus--color--black);
border-radius: 0;
@ -256,10 +255,6 @@ input[type="text"], input[type="password"], input[type="email"], input[type="tel
min-width: 30rem;
}
input.width-2x {
min-width: 60.95rem;
}
.input input::placeholder {
color: transparent;
}
@ -314,7 +309,7 @@ fieldset {
}
.full-width legend {
margin-bottom: -1rem;
margin-bottom: initial;
}

View File

@ -17,13 +17,7 @@
<li role="presentation">
<a role="menuitem" href="/profile">
<i class="ri-account-circle-line"></i>
{{( pgettext "Account" "menu" )}}
</a>
</li>
<li role="presentation">
<a role="menuitem" href="{{ companyURI "/tax-details" }}">
<i class="ri-vip-diamond-line"></i>
{{( pgettext "Tax Details" "menu" )}}
{{( gettext "Account" )}}
</a>
</li>
<li role="presentation">

View File

@ -1,51 +0,0 @@
{{ define "content" }}
<section class="dialog-content">
<h2>{{(pgettext "Tax Details" "title")}}</h2>
<form method="POST">
<div class="input">
<input type="text" name="business_name" id="business_name" required="required" value="{{ .BusinessName }}" placeholder="{{( pgettext "Business name" "input" )}}">
<label for="business_name">{{( pgettext "Business name" "input" )}}</label>
</div>
<div class="input">
<input type="text" name="vatin" id="vatin" required="required" value="{{ .VATIN }}" placeholder="{{( pgettext "VAT number" "input" )}}">
<label for="vatin">{{( pgettext "VAT number" "input" )}}</label>
</div>
<div class="input">
<input type="text" name="trade_name" id="trade_name" value="{{ .TradeName }}" placeholder="{{( pgettext "Trade name" "input" )}}">
<label for="trade_name">{{( pgettext "Trade name" "input" )}}</label>
</div>
<div class="input">
<input type="tel" name="phone" id="phone" required="required" value="{{ .Phone }}" placeholder="{{( pgettext "Phone" "input" )}}">
<label for="phone">{{( pgettext "Phone" "input" )}}</label>
</div>
<div class="input">
<input type="email" name="email" id="email" required="required" value="{{ .Email }}" placeholder="{{( pgettext "Email" "input" )}}">
<label for="email">{{( pgettext "Email" "input" )}}</label>
</div>
<div class="input">
<input type="url" name="web" id="web" value="{{ .Web }}" placeholder="{{( pgettext "Web" "input" )}}">
<label for="web">{{( pgettext "Web" "input" )}}</label>
</div>
<div class="input">
<input type="text" name="address" id="address" class="width-2x" required="required" value="{{ .Address }}" placeholder="{{( pgettext "Address" "input" )}}">
<label for="address">{{( pgettext "Address" "input" )}}</label>
</div>
<div class="input">
<input type="text" name="city" id="city" required="required" value="{{ .City }}" placeholder="{{( pgettext "City" "input" )}}">
<label for="city">{{( pgettext "City" "input" )}}</label>
</div>
<div class="input">
<input type="text" name="province" id="province" required="required" value="{{ .City }}" placeholder="{{( pgettext "Province" "input" )}}">
<label for="province">{{( pgettext "Province" "input" )}}</label>
</div>
<div class="input">
<input type="text" name="postal_code" id="postal_code" required="required" value="{{ .PostalCode }}" placeholder="{{( pgettext "Postal code" "input" )}}">
<label for="postal_code">{{( pgettext "Postal code" "input" )}}</label>
</div>
<div class="input">
<input type="text" name="country" id="country" required="required" value="{{ .Country }}" placeholder="{{( pgettext "Country" "input" )}}">
<label for="country">{{( pgettext "Country" "input" )}}</label>
</div>
</form>
</section>
{{- end }}