diff --git a/debian/control b/debian/control index ed8e44a..3968f89 100644 --- a/debian/control +++ b/debian/control @@ -17,7 +17,8 @@ Build-Depends: postgresql-15-pg-libphonenumber, postgresql-15-pgtap, postgresql-15-pguri, - postgresql-15-vat + postgresql-15-vat, + postgresql-15-iban Standards-Version: 4.6.0 XS-Go-Import-Path: dev.tandem.ws/tandem/numerus Vcs-Browser: https://dev.tandem.ws/tandem/numerus @@ -55,6 +56,7 @@ Depends: postgresql-15-pg-libphonenumber, postgresql-15-pguri, postgresql-15-vat, + postgresql-15-iban, sqitch Description: Simple invoicing and accounting web application A simple web application to keep invoice and accouting records, intended for diff --git a/deploy/add_contact.sql b/deploy/add_contact.sql index 67f5057..4d0fffa 100644 --- a/deploy/add_contact.sql +++ b/deploy/add_contact.sql @@ -8,12 +8,17 @@ -- requires: contact -- requires: tag_name -- requires: tax_details +-- requires: contact_web +-- requires: contact_phone +-- requires: contact_tax_details +-- requires: contact_iban +-- requires: contact_bic begin; set search_path to numerus, public; -create or replace function add_contact(company_id integer, name text, phone text, email email, web uri, tax_details tax_details, tags tag_name[]) returns uuid as +create or replace function add_contact(company_id integer, name text, phone text, email text, web text, tax_details tax_details, iban text, bic text, tags tag_name[]) returns uuid as $$ declare cid integer; @@ -43,21 +48,34 @@ begin ; end if; - if web is not null and web <> '' then + if web is not null and trim(web) <> '' then insert into contact_web (contact_id, uri) values (cid, add_contact.web) ; end if; + if iban is not null and trim(iban) <> '' then + insert into contact_iban (contact_id, iban) + values (cid, add_contact.iban) + ; + end if; + + if bic is not null and trim(bic) <> '' then + insert into contact_swift (contact_id, bic) + values (cid, add_contact.bic) + ; + end if; + + return cslug; end $$ language plpgsql ; -revoke execute on function add_contact(integer, text, text, email, uri, tax_details, tag_name[]) from public; -grant execute on function add_contact(integer, text, text, email, uri, tax_details, tag_name[]) to invoicer; -grant execute on function add_contact(integer, text, text, email, uri, tax_details, tag_name[]) to admin; +revoke execute on function add_contact(integer, text, text, text, text, tax_details, text, text, tag_name[]) from public; +grant execute on function add_contact(integer, text, text, text, text, tax_details, text, text, tag_name[]) to invoicer; +grant execute on function add_contact(integer, text, text, text, text, tax_details, text, text, tag_name[]) to admin; drop function if exists add_contact(integer, text, text, text, text, email, uri, text, text, text, text, country_code, tag_name[]); diff --git a/deploy/bic.sql b/deploy/bic.sql new file mode 100644 index 0000000..d5bdc61 --- /dev/null +++ b/deploy/bic.sql @@ -0,0 +1,14 @@ +-- Deploy numerus:bic to pg +-- requires: schema_numerus + +begin; + +set search_path to numerus, public; + +create domain bic as text +check ( value ~ '^[A-Z]{6}[A-Z0-9]{2}([A-Z0-9]{3})?$' ) +; + +comment on domain bic is 'A valid, but not necessarily existent, Business Identifier Code (ISO 9362)'; + +commit; diff --git a/deploy/contact_iban.sql b/deploy/contact_iban.sql new file mode 100644 index 0000000..474ba26 --- /dev/null +++ b/deploy/contact_iban.sql @@ -0,0 +1,31 @@ +-- Deploy numerus:contact_iban to pg +-- requires: schema_numerus +-- requires: roles +-- requires: contact +-- requires: extension_iban + +begin; + +set search_path to numerus, public; + +create table contact_iban ( + contact_id integer primary key references contact, + iban iban not null +); + +grant select, insert, update, delete on table contact_iban to invoicer; +grant select, insert, update, delete on table contact_iban to admin; + +alter table contact_iban enable row level security; + +create policy company_policy +on contact_iban +using ( + exists( + select 1 + from contact + where contact.contact_id = contact_iban.contact_id + ) +); + +commit; diff --git a/deploy/contact_swift.sql b/deploy/contact_swift.sql new file mode 100644 index 0000000..8df34e2 --- /dev/null +++ b/deploy/contact_swift.sql @@ -0,0 +1,31 @@ +-- Deploy numerus:contact_swift to pg +-- requires: schema_numerus +-- requires: roles +-- requires: contact +-- requires: bic + +begin; + +set search_path to numerus, public; + +create table contact_swift ( + contact_id integer primary key references contact, + bic bic not null +); + +grant select, insert, update, delete on table contact_swift to invoicer; +grant select, insert, update, delete on table contact_swift to admin; + +alter table contact_swift enable row level security; + +create policy company_policy +on contact_swift +using ( + exists( + select 1 + from contact + where contact.contact_id = contact_swift.contact_id + ) +); + +commit; diff --git a/deploy/edit_contact.sql b/deploy/edit_contact.sql index 32bb75d..fbf757e 100644 --- a/deploy/edit_contact.sql +++ b/deploy/edit_contact.sql @@ -8,12 +8,17 @@ -- requires: extension_vat -- requires: extension_pg_libphonenumber -- requires: tax_details +-- requires: contact_web +-- requires: contact_phone +-- requires: contact_tax_details +-- requires: contact_iban +-- requires: contact_bic begin; set search_path to numerus, public; -create or replace function edit_contact(contact_slug uuid, name text, phone text, email email, web uri, tax_details tax_details, tags tag_name[]) returns uuid as +create or replace function edit_contact(contact_slug uuid, name text, phone text, email text, web text, tax_details tax_details, iban text, bic text, tags tag_name[]) returns uuid as $$ declare cid integer; @@ -74,7 +79,7 @@ begin ; end if; - if web is null or web = '' then + if web is null or trim(web) = '' then delete from contact_web where contact_id = cid ; @@ -86,15 +91,39 @@ begin ; end if; + if iban is null or trim(iban) = '' then + delete from contact_iban + where contact_id = cid + ; + else + insert into contact_iban (contact_id, iban) + values (cid, edit_contact.iban) + on conflict (contact_id) do update + set iban = excluded.iban + ; + end if; + + if bic is null or trim(bic) = '' then + delete from contact_swift + where contact_id = cid + ; + else + insert into contact_swift (contact_id, bic) + values (cid, edit_contact.bic) + on conflict (contact_id) do update + set bic = excluded.bic + ; + end if; + return contact_slug; end $$ language plpgsql ; -revoke execute on function edit_contact(uuid, text, text, email, uri, tax_details, tag_name[]) from public; -grant execute on function edit_contact(uuid, text, text, email, uri, tax_details, tag_name[]) to invoicer; -grant execute on function edit_contact(uuid, text, text, email, uri, tax_details, tag_name[]) to admin; +revoke execute on function edit_contact(uuid, text, text, text, text, tax_details, text, text, tag_name[]) from public; +grant execute on function edit_contact(uuid, text, text, text, text, tax_details, text, text, tag_name[]) to invoicer; +grant execute on function edit_contact(uuid, text, text, text, text, tax_details, text, text, tag_name[]) to admin; drop function if exists edit_contact(uuid, text, text, text, text, email, uri, text, text, text, text, country_code, tag_name[]); diff --git a/deploy/extension_iban.sql b/deploy/extension_iban.sql new file mode 100644 index 0000000..ef9f6ee --- /dev/null +++ b/deploy/extension_iban.sql @@ -0,0 +1,8 @@ +-- Deploy numerus:extension_iban to pg +-- requires: schema_numerus + +begin; + +create extension if not exists iban; + +commit; diff --git a/pkg/contacts.go b/pkg/contacts.go index 77e06a5..79c8732 100644 --- a/pkg/contacts.go +++ b/pkg/contacts.go @@ -95,7 +95,7 @@ func HandleAddContact(w http.ResponseWriter, r *http.Request, _ httprouter.Param return } company := mustGetCompany(r) - conn.MustExec(r.Context(), "select add_contact($1, $2, $3, $4, $5, $6, $7)", company.Id, form.Name, form.Phone.ValueOrNil(), form.Email.ValueOrNil(), form.Web.ValueOrNil(), form.TaxDetails(), form.Tags) + conn.MustExec(r.Context(), "select add_contact($1, $2, $3, $4, $5, $6, $7, $8, $9)", company.Id, form.Name, form.Phone, form.Email, form.Web, form.TaxDetails(), form.IBAN, form.BIC, form.Tags) htmxRedirect(w, r, companyURI(company, "/contacts")) } @@ -115,7 +115,7 @@ func HandleUpdateContact(w http.ResponseWriter, r *http.Request, params httprout mustRenderEditContactForm(w, r, params[0].Value, form) return } - slug := conn.MustGetText(r.Context(), "", "select edit_contact($1, $2, $3, $4, $5, $6, $7)", params[0].Value, form.Name, form.Phone.ValueOrNil(), form.Email.ValueOrNil(), form.Web.ValueOrNil(), form.TaxDetails(), form.Tags) + slug := conn.MustGetText(r.Context(), "", "select edit_contact($1, $2, $3, $4, $5, $6, $7, $8, $9)", params[0].Value, form.Name, form.Phone, form.Email, form.Web, form.TaxDetails(), form.IBAN, form.BIC, form.Tags) if slug == "" { http.NotFound(w, r) } @@ -230,6 +230,8 @@ type contactForm struct { Province *InputField PostalCode *InputField Country *SelectField + IBAN *InputField + BIC *InputField Tags *TagsField } @@ -326,6 +328,16 @@ func newContactForm(ctx context.Context, conn *Conn, locale *Locale) *contactFor `autocomplete="country"`, }, }, + IBAN: &InputField{ + Name: "iban", + Label: pgettext("input", "IBAN", locale), + Type: "text", + }, + BIC: &InputField{ + Name: "bic", + Label: pgettext("bic", "BIC", locale), + Type: "text", + }, Tags: &TagsField{ Name: "tags", Label: pgettext("input", "Tags", locale), @@ -349,6 +361,8 @@ func (form *contactForm) Parse(r *http.Request) error { form.Province.FillValue(r) form.PostalCode.FillValue(r) form.Country.FillValue(r) + form.IBAN.FillValue(r) + form.BIC.FillValue(r) form.Tags.FillValue(r) return nil } @@ -387,6 +401,12 @@ func (form *contactForm) Validate(ctx context.Context, conn *Conn) bool { 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)) } + if form.IBAN.Val != "" { + validator.CheckValidIBANInput(form.IBAN, gettext("This values is not a valid IBAN.", form.locale)) + } + if form.BIC.Val != "" { + validator.CheckValidBICInput(form.IBAN, gettext("This values is not a valid BIC.", form.locale)) + } return validator.AllOK() } @@ -405,11 +425,15 @@ func (form *contactForm) MustFillFromDatabase(ctx context.Context, conn *Conn, s , province , postal_code , country_code + , iban + , bic , tags from contact left join contact_email using (contact_id) left join contact_phone using (contact_id) left join contact_web using (contact_id) + left join contact_iban using (contact_id) + left join contact_swift using (contact_id) left join contact_tax_details using (contact_id) where slug = $1 `, slug).Scan( @@ -425,6 +449,8 @@ func (form *contactForm) MustFillFromDatabase(ctx context.Context, conn *Conn, s form.Province, form.PostalCode, form.Country, + form.IBAN, + form.BIC, form.Tags)) } diff --git a/pkg/form.go b/pkg/form.go index 35c9719..0f7883d 100644 --- a/pkg/form.go +++ b/pkg/form.go @@ -59,13 +59,6 @@ func (field *InputField) Value() (driver.Value, error) { return field.Val, nil } -func (field *InputField) ValueOrNil() driver.Valuer { - if field.Val == "" { - return nil - } - return field -} - func (field *InputField) FillValue(r *http.Request) { field.Val = strings.TrimSpace(r.FormValue(field.Name)) } @@ -457,6 +450,16 @@ func (v *FormValidator) CheckValidPhoneInput(field *InputField, country string, return v.checkInput(field, true, message) } +func (v *FormValidator) CheckValidIBANInput(field *InputField, message string) bool { + // TODO: actual IBAN validation + return v.checkInput(field, true, message) +} + +func (v *FormValidator) CheckValidBICInput(field *InputField, message string) bool { + // TODO: actual BIC validation + return v.checkInput(field, true, message) +} + func (v *FormValidator) CheckPasswordConfirmation(password *InputField, confirm *InputField, message string) bool { return v.checkInput(confirm, password.Val == confirm.Val, message) } diff --git a/po/ca.po b/po/ca.po index 68340ea..722befb 100644 --- a/po/ca.po +++ b/po/ca.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: numerus\n" "Report-Msgid-Bugs-To: jordi@tandem.blog\n" -"POT-Creation-Date: 2023-06-30 21:08+0200\n" +"POT-Creation-Date: 2023-07-02 01:58+0200\n" "PO-Revision-Date: 2023-01-18 17:08+0100\n" "Last-Translator: jordi fita mas \n" "Language-Team: Catalan \n" @@ -116,7 +116,7 @@ msgstr "Actualitza" #: web/template/invoices/new.gohtml:90 web/template/invoices/edit.gohtml:91 #: web/template/quotes/new.gohtml:91 web/template/quotes/edit.gohtml:92 -#: web/template/contacts/new.gohtml:44 web/template/contacts/edit.gohtml:48 +#: web/template/contacts/new.gohtml:49 web/template/contacts/edit.gohtml:53 #: web/template/expenses/new.gohtml:33 web/template/expenses/edit.gohtml:38 #: web/template/products/new.gohtml:30 web/template/products/edit.gohtml:36 msgctxt "action" @@ -648,7 +648,7 @@ msgctxt "title" msgid "Edit Product “%s”" msgstr "Edició del producte «%s»" -#: pkg/login.go:37 pkg/company.go:127 pkg/profile.go:40 pkg/contacts.go:255 +#: pkg/login.go:37 pkg/company.go:127 pkg/profile.go:40 pkg/contacts.go:257 msgctxt "input" msgid "Email" msgstr "Correu-e" @@ -662,7 +662,7 @@ msgstr "Contrasenya" msgid "Email can not be empty." msgstr "No podeu deixar el correu-e en blanc." -#: pkg/login.go:71 pkg/company.go:284 pkg/profile.go:90 pkg/contacts.go:385 +#: pkg/login.go:71 pkg/company.go:284 pkg/profile.go:90 pkg/contacts.go:399 msgid "This value is not a valid email. It should be like name@domain.com." msgstr "Aquest valor no és un correu-e vàlid. Hauria de ser similar a nom@domini.cat." @@ -675,7 +675,7 @@ msgid "Invalid user or password." msgstr "Nom d’usuari o contrasenya incorrectes." #: pkg/products.go:164 pkg/products.go:263 pkg/quote.go:823 pkg/invoices.go:909 -#: pkg/contacts.go:135 pkg/contacts.go:241 +#: pkg/contacts.go:135 pkg/contacts.go:243 msgctxt "input" msgid "Name" msgstr "Nom" @@ -683,7 +683,7 @@ msgstr "Nom" #: pkg/products.go:169 pkg/products.go:290 pkg/quote.go:174 pkg/quote.go:630 #: pkg/expenses.go:188 pkg/expenses.go:347 pkg/invoices.go:174 #: pkg/invoices.go:657 pkg/invoices.go:1208 pkg/contacts.go:140 -#: pkg/contacts.go:331 +#: pkg/contacts.go:343 msgctxt "input" msgid "Tags" msgstr "Etiquetes" @@ -732,7 +732,7 @@ msgid "Taxes" msgstr "Imposts" #: pkg/products.go:309 pkg/quote.go:919 pkg/profile.go:92 pkg/invoices.go:1005 -#: pkg/contacts.go:378 +#: pkg/contacts.go:392 msgid "Name can not be empty." msgstr "No podeu deixar el nom en blanc." @@ -759,47 +759,47 @@ msgctxt "input" msgid "Trade name" msgstr "Nom comercial" -#: pkg/company.go:118 pkg/contacts.go:247 +#: pkg/company.go:118 pkg/contacts.go:249 msgctxt "input" msgid "Phone" msgstr "Telèfon" -#: pkg/company.go:136 pkg/contacts.go:263 +#: pkg/company.go:136 pkg/contacts.go:265 msgctxt "input" msgid "Web" msgstr "Web" -#: pkg/company.go:144 pkg/contacts.go:275 +#: pkg/company.go:144 pkg/contacts.go:277 msgctxt "input" msgid "Business name" msgstr "Nom i cognoms" -#: pkg/company.go:154 pkg/contacts.go:285 +#: pkg/company.go:154 pkg/contacts.go:287 msgctxt "input" msgid "VAT number" msgstr "DNI / NIF" -#: pkg/company.go:160 pkg/contacts.go:291 +#: pkg/company.go:160 pkg/contacts.go:293 msgctxt "input" msgid "Address" msgstr "Adreça" -#: pkg/company.go:169 pkg/contacts.go:300 +#: pkg/company.go:169 pkg/contacts.go:302 msgctxt "input" msgid "City" msgstr "Població" -#: pkg/company.go:175 pkg/contacts.go:306 +#: pkg/company.go:175 pkg/contacts.go:308 msgctxt "input" msgid "Province" msgstr "Província" -#: pkg/company.go:181 pkg/contacts.go:312 +#: pkg/company.go:181 pkg/contacts.go:314 msgctxt "input" msgid "Postal code" msgstr "Codi postal" -#: pkg/company.go:190 pkg/contacts.go:321 +#: pkg/company.go:190 pkg/contacts.go:323 msgctxt "input" msgid "Country" msgstr "País" @@ -834,23 +834,23 @@ msgctxt "input" msgid "Legal disclaimer" msgstr "Nota legal" -#: pkg/company.go:271 pkg/contacts.go:361 +#: pkg/company.go:271 pkg/contacts.go:375 msgid "Selected country is not valid." msgstr "Heu seleccionat un país que no és vàlid." -#: pkg/company.go:275 pkg/contacts.go:364 +#: pkg/company.go:275 pkg/contacts.go:378 msgid "Business name can not be empty." msgstr "No podeu deixar el nom i els cognoms en blanc." -#: pkg/company.go:276 pkg/contacts.go:365 +#: pkg/company.go:276 pkg/contacts.go:379 msgid "Business name must have at least two letters." msgstr "Nom i cognoms han de tenir com a mínim dues lletres." -#: pkg/company.go:277 pkg/contacts.go:366 +#: pkg/company.go:277 pkg/contacts.go:380 msgid "VAT number can not be empty." msgstr "No podeu deixar el DNI o NIF en blanc." -#: pkg/company.go:278 pkg/contacts.go:367 +#: pkg/company.go:278 pkg/contacts.go:381 msgid "This value is not a valid VAT number." msgstr "Aquest valor no és un DNI o NIF vàlid." @@ -858,31 +858,31 @@ msgstr "Aquest valor no és un DNI o NIF vàlid." msgid "Phone can not be empty." msgstr "No podeu deixar el telèfon en blanc." -#: pkg/company.go:281 pkg/contacts.go:382 +#: pkg/company.go:281 pkg/contacts.go:396 msgid "This value is not a valid phone number." msgstr "Aquest valor no és un telèfon vàlid." -#: pkg/company.go:287 pkg/contacts.go:388 +#: pkg/company.go:287 pkg/contacts.go:402 msgid "This value is not a valid web address. It should be like https://domain.com/." msgstr "Aquest valor no és una adreça web vàlida. Hauria de ser similar a https://domini.cat/." -#: pkg/company.go:289 pkg/contacts.go:369 +#: pkg/company.go:289 pkg/contacts.go:383 msgid "Address can not be empty." msgstr "No podeu deixar l’adreça en blanc." -#: pkg/company.go:290 pkg/contacts.go:370 +#: pkg/company.go:290 pkg/contacts.go:384 msgid "City can not be empty." msgstr "No podeu deixar la població en blanc." -#: pkg/company.go:291 pkg/contacts.go:371 +#: pkg/company.go:291 pkg/contacts.go:385 msgid "Province can not be empty." msgstr "No podeu deixar la província en blanc." -#: pkg/company.go:292 pkg/contacts.go:373 +#: pkg/company.go:292 pkg/contacts.go:387 msgid "Postal code can not be empty." msgstr "No podeu deixar el codi postal en blanc." -#: pkg/company.go:293 pkg/contacts.go:374 +#: pkg/company.go:293 pkg/contacts.go:388 msgid "This value is not a valid postal code." msgstr "Aquest valor no és un codi postal vàlid." @@ -1248,15 +1248,33 @@ msgstr "DD/MM/YYYY" msgid "Invoice product ID must be a number greater than zero." msgstr "L’ID del producte de factura ha de ser un número major a zero." -#: pkg/contacts.go:271 +#: pkg/contacts.go:273 msgctxt "input" msgid "Need to input tax details" msgstr "Necessito poder facturar aquest contacte" -#: pkg/contacts.go:379 +#: pkg/contacts.go:333 +msgctxt "input" +msgid "IBAN" +msgstr "IBAN" + +#: pkg/contacts.go:338 +msgctxt "bic" +msgid "BIC" +msgstr "BIC" + +#: pkg/contacts.go:393 msgid "Name must have at least two letters." msgstr "El nom ha de tenir com a mínim dues lletres." +#: pkg/contacts.go:405 +msgid "This values is not a valid IBAN." +msgstr "Aquest valor no és un IBAN vàlid." + +#: pkg/contacts.go:408 +msgid "This values is not a valid BIC." +msgstr "Aquest valor no és un BIC vàlid." + #~ msgctxt "action" #~ msgid "Update contact" #~ msgstr "Actualitza contacte" diff --git a/po/es.po b/po/es.po index 561d0cc..e4edb97 100644 --- a/po/es.po +++ b/po/es.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: numerus\n" "Report-Msgid-Bugs-To: jordi@tandem.blog\n" -"POT-Creation-Date: 2023-06-30 21:08+0200\n" +"POT-Creation-Date: 2023-07-02 01:58+0200\n" "PO-Revision-Date: 2023-01-18 17:45+0100\n" "Last-Translator: jordi fita mas \n" "Language-Team: Spanish \n" @@ -116,7 +116,7 @@ msgstr "Actualizar" #: web/template/invoices/new.gohtml:90 web/template/invoices/edit.gohtml:91 #: web/template/quotes/new.gohtml:91 web/template/quotes/edit.gohtml:92 -#: web/template/contacts/new.gohtml:44 web/template/contacts/edit.gohtml:48 +#: web/template/contacts/new.gohtml:49 web/template/contacts/edit.gohtml:53 #: web/template/expenses/new.gohtml:33 web/template/expenses/edit.gohtml:38 #: web/template/products/new.gohtml:30 web/template/products/edit.gohtml:36 msgctxt "action" @@ -648,7 +648,7 @@ msgctxt "title" msgid "Edit Product “%s”" msgstr "Edición del producto «%s»" -#: pkg/login.go:37 pkg/company.go:127 pkg/profile.go:40 pkg/contacts.go:255 +#: pkg/login.go:37 pkg/company.go:127 pkg/profile.go:40 pkg/contacts.go:257 msgctxt "input" msgid "Email" msgstr "Correo-e" @@ -662,7 +662,7 @@ msgstr "Contraseña" msgid "Email can not be empty." msgstr "No podéis dejar el correo-e en blanco." -#: pkg/login.go:71 pkg/company.go:284 pkg/profile.go:90 pkg/contacts.go:385 +#: pkg/login.go:71 pkg/company.go:284 pkg/profile.go:90 pkg/contacts.go:399 msgid "This value is not a valid email. It should be like name@domain.com." msgstr "Este valor no es un correo-e válido. Tiene que ser parecido a nombre@dominio.es." @@ -675,7 +675,7 @@ msgid "Invalid user or password." msgstr "Nombre de usuario o contraseña inválido." #: pkg/products.go:164 pkg/products.go:263 pkg/quote.go:823 pkg/invoices.go:909 -#: pkg/contacts.go:135 pkg/contacts.go:241 +#: pkg/contacts.go:135 pkg/contacts.go:243 msgctxt "input" msgid "Name" msgstr "Nombre" @@ -683,7 +683,7 @@ msgstr "Nombre" #: pkg/products.go:169 pkg/products.go:290 pkg/quote.go:174 pkg/quote.go:630 #: pkg/expenses.go:188 pkg/expenses.go:347 pkg/invoices.go:174 #: pkg/invoices.go:657 pkg/invoices.go:1208 pkg/contacts.go:140 -#: pkg/contacts.go:331 +#: pkg/contacts.go:343 msgctxt "input" msgid "Tags" msgstr "Etiquetes" @@ -732,7 +732,7 @@ msgid "Taxes" msgstr "Impuestos" #: pkg/products.go:309 pkg/quote.go:919 pkg/profile.go:92 pkg/invoices.go:1005 -#: pkg/contacts.go:378 +#: pkg/contacts.go:392 msgid "Name can not be empty." msgstr "No podéis dejar el nombre en blanco." @@ -759,47 +759,47 @@ msgctxt "input" msgid "Trade name" msgstr "Nombre comercial" -#: pkg/company.go:118 pkg/contacts.go:247 +#: pkg/company.go:118 pkg/contacts.go:249 msgctxt "input" msgid "Phone" msgstr "Teléfono" -#: pkg/company.go:136 pkg/contacts.go:263 +#: pkg/company.go:136 pkg/contacts.go:265 msgctxt "input" msgid "Web" msgstr "Web" -#: pkg/company.go:144 pkg/contacts.go:275 +#: pkg/company.go:144 pkg/contacts.go:277 msgctxt "input" msgid "Business name" msgstr "Nombre y apellidos" -#: pkg/company.go:154 pkg/contacts.go:285 +#: pkg/company.go:154 pkg/contacts.go:287 msgctxt "input" msgid "VAT number" msgstr "DNI / NIF" -#: pkg/company.go:160 pkg/contacts.go:291 +#: pkg/company.go:160 pkg/contacts.go:293 msgctxt "input" msgid "Address" msgstr "Dirección" -#: pkg/company.go:169 pkg/contacts.go:300 +#: pkg/company.go:169 pkg/contacts.go:302 msgctxt "input" msgid "City" msgstr "Población" -#: pkg/company.go:175 pkg/contacts.go:306 +#: pkg/company.go:175 pkg/contacts.go:308 msgctxt "input" msgid "Province" msgstr "Provincia" -#: pkg/company.go:181 pkg/contacts.go:312 +#: pkg/company.go:181 pkg/contacts.go:314 msgctxt "input" msgid "Postal code" msgstr "Código postal" -#: pkg/company.go:190 pkg/contacts.go:321 +#: pkg/company.go:190 pkg/contacts.go:323 msgctxt "input" msgid "Country" msgstr "País" @@ -834,23 +834,23 @@ msgctxt "input" msgid "Legal disclaimer" msgstr "Nota legal" -#: pkg/company.go:271 pkg/contacts.go:361 +#: pkg/company.go:271 pkg/contacts.go:375 msgid "Selected country is not valid." msgstr "Habéis escogido un país que no es válido." -#: pkg/company.go:275 pkg/contacts.go:364 +#: pkg/company.go:275 pkg/contacts.go:378 msgid "Business name can not be empty." msgstr "No podéis dejar el nombre y los apellidos en blanco." -#: pkg/company.go:276 pkg/contacts.go:365 +#: pkg/company.go:276 pkg/contacts.go:379 msgid "Business name must have at least two letters." msgstr "El nombre y los apellidos deben contener como mínimo dos letras." -#: pkg/company.go:277 pkg/contacts.go:366 +#: pkg/company.go:277 pkg/contacts.go:380 msgid "VAT number can not be empty." msgstr "No podéis dejar el DNI o NIF en blanco." -#: pkg/company.go:278 pkg/contacts.go:367 +#: pkg/company.go:278 pkg/contacts.go:381 msgid "This value is not a valid VAT number." msgstr "Este valor no es un DNI o NIF válido." @@ -858,31 +858,31 @@ msgstr "Este valor no es un DNI o NIF válido." msgid "Phone can not be empty." msgstr "No podéis dejar el teléfono en blanco." -#: pkg/company.go:281 pkg/contacts.go:382 +#: pkg/company.go:281 pkg/contacts.go:396 msgid "This value is not a valid phone number." msgstr "Este valor no es un teléfono válido." -#: pkg/company.go:287 pkg/contacts.go:388 +#: pkg/company.go:287 pkg/contacts.go:402 msgid "This value is not a valid web address. It should be like https://domain.com/." msgstr "Este valor no es una dirección web válida. Tiene que ser parecida a https://dominio.es/." -#: pkg/company.go:289 pkg/contacts.go:369 +#: pkg/company.go:289 pkg/contacts.go:383 msgid "Address can not be empty." msgstr "No podéis dejar la dirección en blanco." -#: pkg/company.go:290 pkg/contacts.go:370 +#: pkg/company.go:290 pkg/contacts.go:384 msgid "City can not be empty." msgstr "No podéis dejar la población en blanco." -#: pkg/company.go:291 pkg/contacts.go:371 +#: pkg/company.go:291 pkg/contacts.go:385 msgid "Province can not be empty." msgstr "No podéis dejar la provincia en blanco." -#: pkg/company.go:292 pkg/contacts.go:373 +#: pkg/company.go:292 pkg/contacts.go:387 msgid "Postal code can not be empty." msgstr "No podéis dejar el código postal en blanco." -#: pkg/company.go:293 pkg/contacts.go:374 +#: pkg/company.go:293 pkg/contacts.go:388 msgid "This value is not a valid postal code." msgstr "Este valor no es un código postal válido válido." @@ -1248,15 +1248,33 @@ msgstr "DD/MM/YYYY" msgid "Invoice product ID must be a number greater than zero." msgstr "El ID de producto de factura tiene que ser un número mayor a cero." -#: pkg/contacts.go:271 +#: pkg/contacts.go:273 msgctxt "input" msgid "Need to input tax details" msgstr "Necesito facturar este contacto" -#: pkg/contacts.go:379 +#: pkg/contacts.go:333 +msgctxt "input" +msgid "IBAN" +msgstr "IBAN" + +#: pkg/contacts.go:338 +msgctxt "bic" +msgid "BIC" +msgstr "BIC" + +#: pkg/contacts.go:393 msgid "Name must have at least two letters." msgstr "El nombre debe contener como mínimo dos letras." +#: pkg/contacts.go:405 +msgid "This values is not a valid IBAN." +msgstr "Este valor no es un IBAN válido." + +#: pkg/contacts.go:408 +msgid "This values is not a valid BIC." +msgstr "Este valor no es un BIC válido." + #~ msgctxt "action" #~ msgid "Update contact" #~ msgstr "Actualizar contacto" diff --git a/revert/add_contact.sql b/revert/add_contact.sql index 6a48fca..56af075 100644 --- a/revert/add_contact.sql +++ b/revert/add_contact.sql @@ -34,6 +34,6 @@ grant execute on function add_contact(integer, text, text, text, text, email, ur grant execute on function add_contact(integer, text, text, text, text, email, uri, text, text, text, text, country_code, tag_name[]) to admin; -drop function if exists add_contact(integer, text, text, email, uri, tax_details, tag_name[]); +drop function if exists add_contact(integer, text, text, text, text, tax_details, text, text, tag_name[]); commit; diff --git a/revert/bic.sql b/revert/bic.sql new file mode 100644 index 0000000..c4b26f5 --- /dev/null +++ b/revert/bic.sql @@ -0,0 +1,7 @@ +-- Revert numerus:bic from pg + +begin; + +drop domain if exists numerus.bic; + +commit; diff --git a/revert/contact_iban.sql b/revert/contact_iban.sql new file mode 100644 index 0000000..6dce87d --- /dev/null +++ b/revert/contact_iban.sql @@ -0,0 +1,7 @@ +-- Revert numerus:contact_iban from pg + +begin; + +drop table if exists numerus.contact_iban; + +commit; diff --git a/revert/contact_swift.sql b/revert/contact_swift.sql new file mode 100644 index 0000000..b3c88a2 --- /dev/null +++ b/revert/contact_swift.sql @@ -0,0 +1,7 @@ +-- Revert numerus:contact_swift from pg + +begin; + +drop table if exists numerus.contact_swift; + +commit; diff --git a/revert/edit_contact.sql b/revert/edit_contact.sql index 6b6b9dc..14c7bab 100644 --- a/revert/edit_contact.sql +++ b/revert/edit_contact.sql @@ -50,6 +50,6 @@ revoke execute on function edit_contact(uuid, text, text, text, text, email, uri grant execute on function edit_contact(uuid, text, text, text, text, email, uri, text, text, text, text, country_code, tag_name[]) to invoicer; grant execute on function edit_contact(uuid, text, text, text, text, email, uri, text, text, text, text, country_code, tag_name[]) to admin; -drop function if exists edit_contact(uuid, text, text, email, uri, tax_details, tag_name[]); +drop function if exists edit_contact(uuid, text, text, text, text, tax_details, text, text, tag_name[]); commit; diff --git a/revert/extension_iban.sql b/revert/extension_iban.sql new file mode 100644 index 0000000..08d7f88 --- /dev/null +++ b/revert/extension_iban.sql @@ -0,0 +1,7 @@ +-- Revert numerus:extension_iban from pg + +begin; + +drop extension if exists iban; + +commit; diff --git a/sqitch.plan b/sqitch.plan index 1b31987..b7c0852 100644 --- a/sqitch.plan +++ b/sqitch.plan @@ -104,6 +104,10 @@ contact_email [roles schema_numerus email contact] 2023-06-28T11:47:19Z jordi fi contact_web [roles schema_numerus extension_uri contact] 2023-06-28T12:01:07Z jordi fita mas # Add relation to keep contacts’ websites contact_tax_details [roles schema_numerus contact extension_vat country_code country] 2023-06-23T09:14:03Z jordi fita mas # Add relation of contact’s tax details tax_details [schema_numerus extension_vat country_code] 2023-06-29T10:57:57Z jordi fita mas # Add composite type for contacts’ tax details -add_contact [add_contact@v0 tax_details] 2023-06-29T11:10:15Z jordi fita mas # Change add contact to accept a tax_detail parameter and use the new relations -edit_contact [edit_contact@v0 tax_details] 2023-06-29T11:50:41Z jordi fita mas # Change edit_contact to require tax_details parameter and to use new relations for web, email, and phone invoice_contact_id_fkey [schema_numerus invoice contact_tax_details] 2023-06-30T16:50:45Z jordi fita mas # Update invoice’s contact_id foreign key to point to tax sales +extension_iban [schema_numerus] 2023-07-01T22:20:38Z jordi fita mas # Add IBAN extension +contact_iban [schema_numerus roles contact extension_iban] 2023-07-01T22:27:38Z jordi fita mas # Add relation for contact iban +bic [schema_numerus] 2023-07-01T22:46:30Z jordi fita mas # Add BIC domain +contact_swift [schema_numerus roles contact bic] 2023-07-01T23:03:13Z jordi fita mas # Add relation for contact’s SWIFT-BIC +add_contact [add_contact@v0 tax_details contact_web contact_email contact_phone contact_iban contact_swift] 2023-06-29T11:10:15Z jordi fita mas # Change add contact to accept a tax_detail parameter and use the new relations for web, email, phone, iban, and swift +edit_contact [edit_contact@v0 tax_details contact_web contact_email contact_phone contact_iban contact_swift] 2023-06-29T11:50:41Z jordi fita mas # Change edit_contact to require tax_details parameter and to use new relations for web, email, phone, iban, and swift diff --git a/test/add_contact.sql b/test/add_contact.sql index 88a9fa3..55ea5b8 100644 --- a/test/add_contact.sql +++ b/test/add_contact.sql @@ -5,22 +5,28 @@ reset client_min_messages; begin; -select plan(18); +select plan(20); set search_path to auth, numerus, public; -select has_function('numerus', 'add_contact', array ['integer', 'text', 'text', 'email', 'uri', 'tax_details', 'tag_name[]']); -select function_lang_is('numerus', 'add_contact', array ['integer', 'text', 'text', 'email', 'uri', 'tax_details', 'tag_name[]'], 'plpgsql'); -select function_returns('numerus', 'add_contact', array ['integer', 'text', 'text', 'email', 'uri', 'tax_details', 'tag_name[]'], 'uuid'); -select isnt_definer('numerus', 'add_contact', array ['integer', 'text', 'text', 'email', 'uri', 'tax_details', 'tag_name[]']); -select volatility_is('numerus', 'add_contact', array ['integer', 'text', 'text', 'email', 'uri', 'tax_details', 'tag_name[]'], 'volatile'); -select function_privs_are('numerus', 'add_contact', array ['integer', 'text', 'text', 'email', 'uri', 'tax_details', 'tag_name[]'], 'guest', array []::text[]); -select function_privs_are('numerus', 'add_contact', array ['integer', 'text', 'text', 'email', 'uri', 'tax_details', 'tag_name[]'], 'invoicer', array ['EXECUTE']); -select function_privs_are('numerus', 'add_contact', array ['integer', 'text', 'text', 'email', 'uri', 'tax_details', 'tag_name[]'], 'admin', array ['EXECUTE']); -select function_privs_are('numerus', 'add_contact', array ['integer', 'text', 'text', 'email', 'uri', 'tax_details', 'tag_name[]'], 'authenticator', array []::text[]); +select has_function('numerus', 'add_contact', array ['integer', 'text', 'text', 'text', 'text', 'tax_details', 'text', 'text', 'tag_name[]']); +select function_lang_is('numerus', 'add_contact', array ['integer', 'text', 'text', 'text', 'text', 'tax_details', 'text', 'text', 'tag_name[]'], 'plpgsql'); +select function_returns('numerus', 'add_contact', array ['integer', 'text', 'text', 'text', 'text', 'tax_details', 'text', 'text', 'tag_name[]'], 'uuid'); +select isnt_definer('numerus', 'add_contact', array ['integer', 'text', 'text', 'text', 'text', 'tax_details', 'text', 'text', 'tag_name[]']); +select volatility_is('numerus', 'add_contact', array ['integer', 'text', 'text', 'text', 'text', 'tax_details', 'text', 'text', 'tag_name[]'], 'volatile'); +select function_privs_are('numerus', 'add_contact', array ['integer', 'text', 'text', 'text', 'text', 'tax_details', 'text', 'text', 'tag_name[]'], 'guest', array []::text[]); +select function_privs_are('numerus', 'add_contact', array ['integer', 'text', 'text', 'text', 'text', 'tax_details', 'text', 'text', 'tag_name[]'], 'invoicer', array ['EXECUTE']); +select function_privs_are('numerus', 'add_contact', array ['integer', 'text', 'text', 'text', 'text', 'tax_details', 'text', 'text', 'tag_name[]'], 'admin', array ['EXECUTE']); +select function_privs_are('numerus', 'add_contact', array ['integer', 'text', 'text', 'text', 'text', 'tax_details', 'text', 'text', 'tag_name[]'], 'authenticator', array []::text[]); set client_min_messages to warning; +truncate contact_swift cascade; +truncate contact_iban cascade; +truncate contact_web cascade; +truncate contact_email cascade; +truncate contact_phone cascade; +truncate contact_tax_details cascade; truncate contact cascade; truncate payment_method cascade; truncate company cascade; @@ -41,22 +47,22 @@ values (111, 1, 'cash', 'cash') set constraints "company_default_payment_method_id_fkey" immediate; select lives_ok( - $$ select add_contact(1, 'Contact 2.1', '777-777-777', null, 'https://c', null, '{tag1,tag2}') $$, + $$ select add_contact(1, 'Contact 2.1', '777-777-777', '', 'https://c', null, 'NL35INGB5262865534', '', '{tag1,tag2}') $$, 'Should be able to insert a contact for the first company with two tags, no email, and no tax details' ); select lives_ok( - $$ select add_contact(1, 'Contact 2.2', null, 'd@d', null, '(Contact 2.2 Ltd,41414141L,"Fake St., 123",City 2.2,Province 2.2,17487,ES)', '{}') $$, + $$ select add_contact(1, 'Contact 2.2', '', 'd@d', '', '(Contact 2.2 Ltd,41414141L,"Fake St., 123",City 2.2,Province 2.2,17487,ES)', '', 'HSBCSKBA', '{}') $$, 'Should be able to insert a second contact for the first company with no tag, no phone, and not website' ); select lives_ok( - $$ select add_contact(2, 'Contact 4.1', '999-999-999', 'e@e', 'http://e', '(Contact 4.1 Ltd,42424242Y,"Another Fake St., 123",City 4.1,Province 4.1,17488,ES)', '{tag2}') $$, + $$ select add_contact(2, 'Contact 4.1', '999-999-999', 'e@e', 'http://e', '(Contact 4.1 Ltd,42424242Y,"Another Fake St., 123",City 4.1,Province 4.1,17488,ES)', 'MT47JQRS54557143744629565915326', 'ARBNNL22', '{tag2}') $$, 'Should be able to insert a contact for the second company with a tag and everything else' ); select lives_ok( - $$ select add_contact(1, 'Contact 2.3', null, null, null, null, '{tag2}') $$, + $$ select add_contact(1, 'Contact 2.3', '', '', '', null, '', '', '{tag2}') $$, 'Should be able to insert another contact with a repeated tag' ); @@ -102,6 +108,22 @@ select bag_eq( 'Should have created all contacts’ web' ); +select bag_eq( + $$ select name, iban::text from contact join contact_iban using (contact_id) $$, + $$ values ('Contact 2.1', 'NL35INGB5262865534') + , ('Contact 4.1', 'MT47JQRS54557143744629565915326') + $$, + 'Should have created all contacts’ IBAN' +); + +select bag_eq( + $$ select name, bic::text from contact join contact_swift using (contact_id) $$, + $$ values ('Contact 2.2', 'HSBCSKBA') + , ('Contact 4.1', 'ARBNNL22') + $$, + 'Should have created all contacts’ BIC' +); + select * from finish(); diff --git a/test/bic.sql b/test/bic.sql new file mode 100644 index 0000000..495c1bb --- /dev/null +++ b/test/bic.sql @@ -0,0 +1,106 @@ +-- Test bic +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(18); + +set search_path to numerus, public; + +select has_domain('bic'); +select domain_type_is('bic', 'text'); + +select lives_ok($$ select 'BBVAESMM'::bic $$, 'Should be able to cast strings without department to bic'); +select lives_ok($$ select 'LOYDCHGGZCH'::bic $$, 'Should be able to cast strings with department to bic'); + +select throws_ok( + $$ SELECT 'loydchggzch'::bic $$, + 23514, null, + 'Should reject BIC with lowercase' +); + +select throws_ok( + $$ SELECT ' LOYDCHGGZCH'::bic $$, + 23514, null, + 'Should reject BIC with heading spaces' +); + +select throws_ok( + $$ SELECT 'LOYDCHGGZCH '::bic $$, + 23514, null, + 'Should reject BIC with trailing spaces' +); + +select throws_ok( + $$ SELECT 'LOYD CH GG ZCH'::bic $$, + 23514, null, + 'Should reject BIC with middle spaces' +); + +select throws_ok( + $$ SELECT '1OYDCHGGZCH'::bic $$, + 23514, null, + 'Should reject BIC with numbers at first position' +); + +select throws_ok( + $$ SELECT 'L2YDCHGGZCH'::bic $$, + 23514, null, + 'Should reject BIC with numbers at second position' +); + +select throws_ok( + $$ SELECT 'LO3DCHGGZCH'::bic $$, + 23514, null, + 'Should reject BIC with numbers at third position' +); + +select throws_ok( + $$ SELECT 'LOY4CHGGZCH'::bic $$, + 23514, null, + 'Should reject BIC with numbers at fourth position' +); + +select throws_ok( + $$ SELECT 'LOYD5HGGZCH'::bic $$, + 23514, null, + 'Should reject BIC with numbers at fifth position' +); + +select throws_ok( + $$ SELECT 'LOYDC6GGZCH'::bic $$, + 23514, null, + 'Should reject BIC with numbers at sixth position' +); + +select throws_ok( + $$ SELECT 'LOYDCHG'::bic $$, + 23514, null, + 'Should reject BIC with 7 characters' +); + +select throws_ok( + $$ SELECT 'LOYDCHGGZ'::bic $$, + 23514, null, + 'Should reject BIC with 9 characters' +); + +select throws_ok( + $$ SELECT 'LOYDCHGGZC'::bic $$, + 23514, null, + 'Should reject BIC with 10 characters' +); + +select throws_ok( + $$ SELECT 'LOYDCHGGZCHH'::bic $$, + 23514, null, + 'Should reject BIC with 12 characters' +); + + +select * +from finish(); + +rollback; diff --git a/test/contact_iban.sql b/test/contact_iban.sql new file mode 100644 index 0000000..220e220 --- /dev/null +++ b/test/contact_iban.sql @@ -0,0 +1,117 @@ +-- Test contact_iban +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(21); + +set search_path to numerus, auth, public; + +select has_table('contact_iban'); +select has_pk('contact_iban' ); +select table_privs_are('contact_iban', 'guest', array []::text[]); +select table_privs_are('contact_iban', 'invoicer', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']); +select table_privs_are('contact_iban', 'admin', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']); +select table_privs_are('contact_iban', 'authenticator', array []::text[]); + +select has_column('contact_iban', 'contact_id'); +select col_is_pk('contact_iban', 'contact_id'); +select col_is_fk('contact_iban', 'contact_id'); +select fk_ok('contact_iban', 'contact_id', 'contact', 'contact_id'); +select col_type_is('contact_iban', 'contact_id', 'integer'); +select col_not_null('contact_iban', 'contact_id'); +select col_hasnt_default('contact_iban', 'contact_id'); + +select has_column('contact_iban', 'iban'); +select col_type_is('contact_iban', 'iban', 'iban'); +select col_not_null('contact_iban', 'iban'); +select col_hasnt_default('contact_iban', 'iban'); + + +set client_min_messages to warning; +truncate contact_iban cascade; +truncate contact cascade; +truncate company_user cascade; +truncate payment_method cascade; +truncate company cascade; +truncate auth."user" cascade; +reset client_min_messages; + +set constraints "company_default_payment_method_id_fkey" deferred; + +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_code, currency_code, default_payment_method_id) +values (2, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 222) + , (4, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', 'FR', 'USD', 444) +; + +insert into payment_method (payment_method_id, company_id, name, instructions) +values (444, 4, 'cash', 'cash') + , (222, 2, 'cash', 'cash') +; + +set constraints "company_default_payment_method_id_fkey" immediate; + +insert into company_user (company_id, user_id) +values (2, 1) + , (4, 5) +; + +insert into contact (contact_id, company_id, name) +values (6, 2, 'C1') + , (8, 4, 'C2') + , (9, 4, 'C3') +; + +insert into contact_iban (contact_id, iban) +values (6, 'NL35INGB5262865534') + , (8, 'MT47JQRS54557143744629565915326') +; + +prepare contact_data as +select company_id, iban::text +from contact +join contact_iban using (contact_id) +order by company_id, iban; + +set role invoicer; +select is_empty('contact_data', 'Should show no data when cookie is not set yet'); +reset role; + +select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog'); +select bag_eq( + 'contact_data', + $$ values (2, 'NL35INGB5262865534') + $$, + 'Should only list contacts of the companies where demo@tandem.blog is user of' +); +reset role; + +select set_cookie('12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524/admin@tandem.blog'); +select bag_eq( + 'contact_data', + $$ values (4, 'MT47JQRS54557143744629565915326') + $$, + 'Should only list contacts of the companies where admin@tandem.blog is user of' +); +reset role; + +select set_cookie('not-a-cookie'); +select throws_ok( + 'contact_data', + '42501', 'permission denied for table contact', + 'Should not allow select to guest users' +); +reset role; + +select * +from finish(); + +rollback; + diff --git a/test/contact_swift.sql b/test/contact_swift.sql new file mode 100644 index 0000000..dcecb61 --- /dev/null +++ b/test/contact_swift.sql @@ -0,0 +1,118 @@ +-- Test contact_swift +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(21); + +set search_path to numerus, auth, public; + +select has_table('contact_swift'); +select has_pk('contact_swift' ); +select table_privs_are('contact_swift', 'guest', array []::text[]); +select table_privs_are('contact_swift', 'invoicer', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']); +select table_privs_are('contact_swift', 'admin', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']); +select table_privs_are('contact_swift', 'authenticator', array []::text[]); + +select has_column('contact_swift', 'contact_id'); +select col_is_pk('contact_swift', 'contact_id'); +select col_is_fk('contact_swift', 'contact_id'); +select fk_ok('contact_swift', 'contact_id', 'contact', 'contact_id'); +select col_type_is('contact_swift', 'contact_id', 'integer'); +select col_not_null('contact_swift', 'contact_id'); +select col_hasnt_default('contact_swift', 'contact_id'); + +select has_column('contact_swift', 'bic'); +select col_type_is('contact_swift', 'bic', 'bic'); +select col_not_null('contact_swift', 'bic'); +select col_hasnt_default('contact_swift', 'bic'); + + +set client_min_messages to warning; +truncate contact_swift cascade; +truncate contact cascade; +truncate company_user cascade; +truncate payment_method cascade; +truncate company cascade; +truncate auth."user" cascade; +reset client_min_messages; + +set constraints "company_default_payment_method_id_fkey" deferred; + +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_code, currency_code, default_payment_method_id) +values (2, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 222) + , (4, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', 'FR', 'USD', 444) +; + +insert into payment_method (payment_method_id, company_id, name, instructions) +values (444, 4, 'cash', 'cash') + , (222, 2, 'cash', 'cash') +; + +set constraints "company_default_payment_method_id_fkey" immediate; + +insert into company_user (company_id, user_id) +values (2, 1) + , (4, 5) +; + +insert into contact (contact_id, company_id, name) +values (6, 2, 'C1') + , (8, 4, 'C2') + , (9, 4, 'C3') +; + +insert into contact_swift (contact_id, bic) +values (6, 'HSBCSKBA') + , (8, 'ARBNNL22') +; + +prepare contact_data as +select company_id, bic::text +from contact +join contact_swift using (contact_id) +order by company_id, bic; + +set role invoicer; +select is_empty('contact_data', 'Should show no data when cookie is not set yet'); +reset role; + +select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog'); +select bag_eq( + 'contact_data', + $$ values (2, 'HSBCSKBA') + $$, + 'Should only list contacts of the companies where demo@tandem.blog is user of' +); +reset role; + +select set_cookie('12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524/admin@tandem.blog'); +select bag_eq( + 'contact_data', + $$ values (4, 'ARBNNL22') + $$, + 'Should only list contacts of the companies where admin@tandem.blog is user of' +); +reset role; + +select set_cookie('not-a-cookie'); +select throws_ok( + 'contact_data', + '42501', 'permission denied for table contact', + 'Should not allow select to guest users' +); +reset role; + + +select * +from finish(); + +rollback; + diff --git a/test/edit_contact.sql b/test/edit_contact.sql index 73d61cd..13007e3 100644 --- a/test/edit_contact.sql +++ b/test/edit_contact.sql @@ -5,22 +5,28 @@ reset client_min_messages; begin; -select plan(17); +select plan(19); set search_path to auth, numerus, public; -select has_function('numerus', 'edit_contact', array ['uuid', 'text', 'text', 'email', 'uri', 'tax_details', 'tag_name[]']); -select function_lang_is('numerus', 'edit_contact', array ['uuid', 'text', 'text', 'email', 'uri', 'tax_details', 'tag_name[]'], 'plpgsql'); -select function_returns('numerus', 'edit_contact', array ['uuid', 'text', 'text', 'email', 'uri', 'tax_details', 'tag_name[]'], 'uuid'); -select isnt_definer('numerus', 'edit_contact', array ['uuid', 'text', 'text', 'email', 'uri', 'tax_details', 'tag_name[]']); -select volatility_is('numerus', 'edit_contact', array ['uuid', 'text', 'text', 'email', 'uri', 'tax_details', 'tag_name[]'], 'volatile'); -select function_privs_are('numerus', 'edit_contact', array ['uuid', 'text', 'text', 'email', 'uri', 'tax_details', 'tag_name[]'], 'guest', array []::text[]); -select function_privs_are('numerus', 'edit_contact', array ['uuid', 'text', 'text', 'email', 'uri', 'tax_details', 'tag_name[]'], 'invoicer', array ['EXECUTE']); -select function_privs_are('numerus', 'edit_contact', array ['uuid', 'text', 'text', 'email', 'uri', 'tax_details', 'tag_name[]'], 'admin', array ['EXECUTE']); -select function_privs_are('numerus', 'edit_contact', array ['uuid', 'text', 'text', 'email', 'uri', 'tax_details', 'tag_name[]'], 'authenticator', array []::text[]); +select has_function('numerus', 'edit_contact', array ['uuid', 'text', 'text', 'text', 'text', 'tax_details', 'text', 'text', 'tag_name[]']); +select function_lang_is('numerus', 'edit_contact', array ['uuid', 'text', 'text', 'text', 'text', 'tax_details', 'text', 'text', 'tag_name[]'], 'plpgsql'); +select function_returns('numerus', 'edit_contact', array ['uuid', 'text', 'text', 'text', 'text', 'tax_details', 'text', 'text', 'tag_name[]'], 'uuid'); +select isnt_definer('numerus', 'edit_contact', array ['uuid', 'text', 'text', 'text', 'text', 'tax_details', 'text', 'text', 'tag_name[]']); +select volatility_is('numerus', 'edit_contact', array ['uuid', 'text', 'text', 'text', 'text', 'tax_details', 'text', 'text', 'tag_name[]'], 'volatile'); +select function_privs_are('numerus', 'edit_contact', array ['uuid', 'text', 'text', 'text', 'text', 'tax_details', 'text', 'text', 'tag_name[]'], 'guest', array []::text[]); +select function_privs_are('numerus', 'edit_contact', array ['uuid', 'text', 'text', 'text', 'text', 'tax_details', 'text', 'text', 'tag_name[]'], 'invoicer', array ['EXECUTE']); +select function_privs_are('numerus', 'edit_contact', array ['uuid', 'text', 'text', 'text', 'text', 'tax_details', 'text', 'text', 'tag_name[]'], 'admin', array ['EXECUTE']); +select function_privs_are('numerus', 'edit_contact', array ['uuid', 'text', 'text', 'text', 'text', 'tax_details', 'text', 'text', 'tag_name[]'], 'authenticator', array []::text[]); set client_min_messages to warning; +truncate contact_swift cascade; +truncate contact_iban cascade; +truncate contact_web cascade; +truncate contact_email cascade; +truncate contact_phone cascade; +truncate contact_tax_details cascade; truncate contact cascade; truncate payment_method cascade; truncate company cascade; @@ -66,19 +72,29 @@ values (12, 'https://1/') , (13, 'https://2/') ; +insert into contact_iban (contact_id, iban) +values (12, 'NL04RABO9373475770') + , (13, 'NL17RABO4416709382') +; + +insert into contact_swift (contact_id, bic) +values (12, 'ABNANL2A') + , (13, 'ARBNNL22') +; + select lives_ok( - $$ select edit_contact('7ac3ae0e-b0c1-4206-a19b-0be20835edd4', 'Contact 2.1', '999-999-999', 'c1@c1', 'https://c', '(Contact 2.1 Ltd,40404040D,"Fake St., 123",City 2.1,Province 2.1,19486,ES)', array['tag1']) $$, + $$ select edit_contact('7ac3ae0e-b0c1-4206-a19b-0be20835edd4', 'Contact 2.1', '999-999-999', 'c1@c1', 'https://c', '(Contact 2.1 Ltd,40404040D,"Fake St., 123",City 2.1,Province 2.1,19486,ES)', 'NL68RABO7792285766', '', array['tag1']) $$, 'Should be able to edit the first contact' ); select lives_ok( - $$ select edit_contact('b57b980b-247b-4be4-a0b7-03a7819c53ae', 'Contact 2.2', null, null, null, null, array['tag1', 'tag3']) $$, + $$ select edit_contact('b57b980b-247b-4be4-a0b7-03a7819c53ae', 'Contact 2.2', '', '', '', null, '', 'ASNBNL21', array['tag1', 'tag3']) $$, 'Should be able to edit the second contact' ); select lives_ok( - $$ select edit_contact('12fd031b-8f4d-4ac1-9dde-7df336dc6d52', 'Contact 2.3', '111-111-111', 'd2@d2', 'https://d', '(Contact 2.3 Ltd,41414141L,"Another Fake St., 123",City 2.2,Province 2.2,17417,ES)', array['tag2']) $$, + $$ select edit_contact('12fd031b-8f4d-4ac1-9dde-7df336dc6d52', 'Contact 2.3', '111-111-111', 'd2@d2', 'https://d', '(Contact 2.3 Ltd,41414141L,"Another Fake St., 123",City 2.2,Province 2.2,17417,ES)', 'NL48RABO6482008283', 'BOFSNL21002', array['tag2']) $$, 'Should be able to edit the third contact' ); @@ -123,6 +139,22 @@ select bag_eq( 'Should have updated all contacts’ web' ); +select bag_eq( + $$ select name, iban::text from contact join contact_iban using (contact_id) $$, + $$ values ('Contact 2.1', 'NL68RABO7792285766') + , ('Contact 2.3', 'NL48RABO6482008283') + $$, + 'Should have updated all contacts’ IBAN' +); + +select bag_eq( + $$ select name, bic::text from contact join contact_swift using (contact_id) $$, + $$ values ('Contact 2.2', 'ASNBNL21') + , ('Contact 2.3', 'BOFSNL21002') + $$, + 'Should have updated all contacts’ BIC' +); + select * from finish(); diff --git a/test/extensions.sql b/test/extensions.sql index 99d5fd7..b73cf67 100644 --- a/test/extensions.sql +++ b/test/extensions.sql @@ -9,9 +9,10 @@ select plan(1); select extensions_are(array[ 'citext' + , 'iban' + , 'pgcrypto' , 'pg_libphonenumber' , 'pgtap' - , 'pgcrypto' , 'plpgsql' , 'uri' , 'vat' diff --git a/verify/add_contact.sql b/verify/add_contact.sql index 55c87af..e644f0f 100644 --- a/verify/add_contact.sql +++ b/verify/add_contact.sql @@ -2,6 +2,6 @@ begin; -select has_function_privilege('numerus.add_contact(integer, text, text, numerus.email, uri, numerus.tax_details, numerus.tag_name[])', 'execute'); +select has_function_privilege('numerus.add_contact(integer, text, text, text, text, numerus.tax_details, text, text, numerus.tag_name[])', 'execute'); rollback; diff --git a/verify/bic.sql b/verify/bic.sql new file mode 100644 index 0000000..9b7a8d3 --- /dev/null +++ b/verify/bic.sql @@ -0,0 +1,7 @@ +-- Verify numerus:bic on pg + +begin; + +select pg_catalog.has_type_privilege('numerus.bic', 'usage'); + +rollback; diff --git a/verify/contact_iban.sql b/verify/contact_iban.sql new file mode 100644 index 0000000..f221bcb --- /dev/null +++ b/verify/contact_iban.sql @@ -0,0 +1,13 @@ +-- Verify numerus:contact_iban on pg + +begin; + +select contact_id + , iban +from numerus.contact_iban +where false; + +select 1 / count(*) from pg_class where oid = 'numerus.contact_iban'::regclass and relrowsecurity; +select 1 / count(*) from pg_policy where polname = 'company_policy' and polrelid = 'numerus.contact_iban'::regclass; + +rollback; diff --git a/verify/contact_swift.sql b/verify/contact_swift.sql new file mode 100644 index 0000000..0182e74 --- /dev/null +++ b/verify/contact_swift.sql @@ -0,0 +1,13 @@ +-- Verify numerus:contact_swift on pg + +begin; + +select contact_id + , bic +from numerus.contact_swift +where false; + +select 1 / count(*) from pg_class where oid = 'numerus.contact_swift'::regclass and relrowsecurity; +select 1 / count(*) from pg_policy where polname = 'company_policy' and polrelid = 'numerus.contact_swift'::regclass; + +rollback; diff --git a/verify/edit_contact.sql b/verify/edit_contact.sql index 1fa9360..b1fc016 100644 --- a/verify/edit_contact.sql +++ b/verify/edit_contact.sql @@ -2,6 +2,6 @@ begin; -select has_function_privilege('numerus.edit_contact(uuid, text, text, numerus.email, uri, numerus.tax_details, numerus.tag_name[])', 'execute'); +select has_function_privilege('numerus.edit_contact(uuid, text, text, text, text, numerus.tax_details, text, text, numerus.tag_name[])', 'execute'); rollback; diff --git a/verify/extension_iban.sql b/verify/extension_iban.sql new file mode 100644 index 0000000..4a77e98 --- /dev/null +++ b/verify/extension_iban.sql @@ -0,0 +1,7 @@ +-- Verify numerus:extension_iban on pg + +begin; + +select 1/count(*) from pg_extension where extname = 'iban'; + +rollback; diff --git a/web/template/contacts/edit.gohtml b/web/template/contacts/edit.gohtml index 0f4a7ed..c4cf677 100644 --- a/web/template/contacts/edit.gohtml +++ b/web/template/contacts/edit.gohtml @@ -23,25 +23,30 @@ {{ putMethod }} {{ with .Form }} -
- {{ template "input-field" .Name }} - {{ template "input-field" .Phone }} - {{ template "input-field" .Email }} - {{ template "input-field" .Web }} - {{ template "tags-field" .Tags }} -
+
+ {{ template "input-field" .Name }} + {{ template "input-field" .Phone }} + {{ template "input-field" .Email }} + {{ template "input-field" .Web }} + {{ template "tags-field" .Tags }} +
- {{ template "check-field" .HasTaxDetails }} +
+ {{ template "input-field" .IBAN }} + {{ template "input-field" .BIC }} +
-
- {{ template "input-field" .BusinessName }} - {{ template "input-field" .VATIN }} - {{ template "input-field" .Address }} - {{ template "input-field" .City }} - {{ template "input-field" .Province }} - {{ template "input-field" .PostalCode }} - {{ template "select-field" .Country }} -
+ {{ template "check-field" .HasTaxDetails }} + +
+ {{ template "input-field" .BusinessName }} + {{ template "input-field" .VATIN }} + {{ template "input-field" .Address }} + {{ template "input-field" .City }} + {{ template "input-field" .Province }} + {{ template "input-field" .PostalCode }} + {{ template "select-field" .Country }} +
{{ end }}
diff --git a/web/template/contacts/new.gohtml b/web/template/contacts/new.gohtml index 0bdf05d..81b0bf6 100644 --- a/web/template/contacts/new.gohtml +++ b/web/template/contacts/new.gohtml @@ -20,17 +20,22 @@
{{ csrfToken }} -
+
{{ template "input-field" .Name | addInputAttr "autofocus" }} {{ template "input-field" .Phone }} {{ template "input-field" .Email }} {{ template "input-field" .Web }} {{ template "tags-field" .Tags }} -
- +
+ +
+ {{ template "input-field" .IBAN }} + {{ template "input-field" .BIC }} +
+ {{ template "check-field" .HasTaxDetails }} - -
+ +
{{ template "input-field" .BusinessName }} {{ template "input-field" .VATIN }} {{ template "input-field" .Address }} @@ -38,7 +43,7 @@ {{ template "input-field" .Province }} {{ template "input-field" .PostalCode }} {{ template "select-field" .Country }} -
+