From ad5bc271b69ee6350a1ca328043552f442e6d421 Mon Sep 17 00:00:00 2001 From: jordi fita mas Date: Sat, 10 Aug 2024 04:34:07 +0200 Subject: [PATCH] Add the payments section MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This actually should be the “payments and receivables” section, however this is quite a mouthful; a “receivable” is a payment made **to** you, therefore “payments” is ok. In fact, there is still no receivables in there, as they should be in a separate relation, to constraint them to invoices instead of expenses. It will be done in a separate commit. Since this section will be, in a sense, sort of simplified accounting, i needed to introduce the “payment account” concept. There is no way, yet, for users to add them, because i have to revamp the “tax details” section, but this commit started to grow too big already. The same reasoning for the attachment payment slips as PDF to payment: something i have to add, but not yet in this commit. --- deploy/add_payment.sql | 67 ++++ deploy/add_payment_account_bank.sql | 35 ++ deploy/add_payment_account_card.sql | 34 ++ deploy/add_payment_account_cash.sql | 23 ++ deploy/add_payment_account_other.sql | 23 ++ deploy/available_expense_status.sql | 5 + deploy/available_expense_status@v2.sql | 22 ++ deploy/available_payment_account_types.sql | 28 ++ deploy/available_payment_status.sql | 22 ++ deploy/edit_payment.sql | 53 +++ deploy/edit_payment_account_bank.sql | 44 +++ deploy/edit_payment_account_card.sql | 44 +++ deploy/edit_payment_account_cash.sql | 27 ++ deploy/edit_payment_account_other.sql | 27 ++ deploy/expense_payment.sql | 32 ++ deploy/payment.sql | 47 +++ deploy/payment_account.sql | 38 +++ deploy/payment_account_bank.sql | 33 ++ deploy/payment_account_card.sql | 33 ++ deploy/payment_account_type.sql | 17 + deploy/payment_account_type_i18n.sql | 21 ++ deploy/payment_status.sql | 17 + deploy/payment_status_i18n.sql | 21 ++ deploy/update_expense_payment_status.sql | 43 +++ pkg/accounts.go | 340 ++++++++++++++++++++ pkg/form.go | 43 +++ pkg/payments.go | 261 +++++++++++++++ pkg/router.go | 8 + po/ca.po | 270 ++++++++++++---- po/es.po | 270 ++++++++++++---- revert/add_payment.sql | 7 + revert/add_payment_account_bank.sql | 7 + revert/add_payment_account_card.sql | 7 + revert/add_payment_account_cash.sql | 7 + revert/add_payment_account_other.sql | 7 + revert/available_expense_status.sql | 9 +- revert/available_expense_status@v2.sql | 10 + revert/available_payment_account_types.sql | 10 + revert/available_payment_status.sql | 10 + revert/edit_payment.sql | 7 + revert/edit_payment_account_bank.sql | 7 + revert/edit_payment_account_card.sql | 7 + revert/edit_payment_account_cash.sql | 7 + revert/edit_payment_account_other.sql | 7 + revert/expense_payment.sql | 7 + revert/payment.sql | 7 + revert/payment_account.sql | 7 + revert/payment_account_bank.sql | 7 + revert/payment_account_card.sql | 7 + revert/payment_account_type.sql | 7 + revert/payment_account_type_i18n.sql | 7 + revert/payment_status.sql | 7 + revert/payment_status_i18n.sql | 7 + revert/update_expense_payment_status.sql | 7 + sqitch.plan | 25 ++ test/add_payment.sql | 125 +++++++ test/add_payment_account_bank.sql | 67 ++++ test/add_payment_account_card.sql | 66 ++++ test/add_payment_account_cash.sql | 65 ++++ test/add_payment_account_other.sql | 65 ++++ test/edit_payment.sql | 115 +++++++ test/edit_payment_account_bank.sql | 84 +++++ test/edit_payment_account_card.sql | 84 +++++ test/edit_payment_account_cash.sql | 77 +++++ test/edit_payment_account_other.sql | 77 +++++ test/expense_payment.sql | 136 ++++++++ test/payment.sql | 173 ++++++++++ test/payment_account.sql | 139 ++++++++ test/payment_account_bank.sql | 150 +++++++++ test/payment_account_card.sql | 192 +++++++++++ test/payment_account_type.sql | 33 ++ test/payment_account_type_i18n.sql | 44 +++ test/payment_status.sql | 35 ++ test/payment_status_i18n.sql | 44 +++ test/update_expense_payment_status.sql | 26 ++ verify/add_payment.sql | 7 + verify/add_payment_account_bank.sql | 7 + verify/add_payment_account_card.sql | 7 + verify/add_payment_account_cash.sql | 7 + verify/add_payment_account_other.sql | 7 + verify/available_expense_status.sql | 3 + verify/available_expense_status@v2.sql | 15 + verify/available_payment_account_types.sql | 21 ++ verify/available_payment_status.sql | 15 + verify/edit_payment.sql | 7 + verify/edit_payment_account_bank.sql | 7 + verify/edit_payment_account_card.sql | 7 + verify/edit_payment_account_cash.sql | 7 + verify/edit_payment_account_other.sql | 7 + verify/expense_payment.sql | 13 + verify/payment.sql | 22 ++ verify/payment_account.sql | 16 + verify/payment_account_bank.sql | 14 + verify/payment_account_card.sql | 15 + verify/payment_account_type.sql | 10 + verify/payment_account_type_i18n.sql | 11 + verify/payment_status.sql | 10 + verify/payment_status_i18n.sql | 11 + verify/update_expense_payment_status.sql | 7 + web/template/app.gohtml | 1 + web/template/form.gohtml | 4 +- web/template/payments/accounts/edit.gohtml | 42 +++ web/template/payments/accounts/index.gohtml | 56 ++++ web/template/payments/accounts/new.gohtml | 45 +++ web/template/payments/edit.gohtml | 39 +++ web/template/payments/index.gohtml | 59 ++++ web/template/payments/new.gohtml | 37 +++ 107 files changed, 4373 insertions(+), 112 deletions(-) create mode 100644 deploy/add_payment.sql create mode 100644 deploy/add_payment_account_bank.sql create mode 100644 deploy/add_payment_account_card.sql create mode 100644 deploy/add_payment_account_cash.sql create mode 100644 deploy/add_payment_account_other.sql create mode 100644 deploy/available_expense_status@v2.sql create mode 100644 deploy/available_payment_account_types.sql create mode 100644 deploy/available_payment_status.sql create mode 100644 deploy/edit_payment.sql create mode 100644 deploy/edit_payment_account_bank.sql create mode 100644 deploy/edit_payment_account_card.sql create mode 100644 deploy/edit_payment_account_cash.sql create mode 100644 deploy/edit_payment_account_other.sql create mode 100644 deploy/expense_payment.sql create mode 100644 deploy/payment.sql create mode 100644 deploy/payment_account.sql create mode 100644 deploy/payment_account_bank.sql create mode 100644 deploy/payment_account_card.sql create mode 100644 deploy/payment_account_type.sql create mode 100644 deploy/payment_account_type_i18n.sql create mode 100644 deploy/payment_status.sql create mode 100644 deploy/payment_status_i18n.sql create mode 100644 deploy/update_expense_payment_status.sql create mode 100644 pkg/accounts.go create mode 100644 pkg/payments.go create mode 100644 revert/add_payment.sql create mode 100644 revert/add_payment_account_bank.sql create mode 100644 revert/add_payment_account_card.sql create mode 100644 revert/add_payment_account_cash.sql create mode 100644 revert/add_payment_account_other.sql create mode 100644 revert/available_expense_status@v2.sql create mode 100644 revert/available_payment_account_types.sql create mode 100644 revert/available_payment_status.sql create mode 100644 revert/edit_payment.sql create mode 100644 revert/edit_payment_account_bank.sql create mode 100644 revert/edit_payment_account_card.sql create mode 100644 revert/edit_payment_account_cash.sql create mode 100644 revert/edit_payment_account_other.sql create mode 100644 revert/expense_payment.sql create mode 100644 revert/payment.sql create mode 100644 revert/payment_account.sql create mode 100644 revert/payment_account_bank.sql create mode 100644 revert/payment_account_card.sql create mode 100644 revert/payment_account_type.sql create mode 100644 revert/payment_account_type_i18n.sql create mode 100644 revert/payment_status.sql create mode 100644 revert/payment_status_i18n.sql create mode 100644 revert/update_expense_payment_status.sql create mode 100644 test/add_payment.sql create mode 100644 test/add_payment_account_bank.sql create mode 100644 test/add_payment_account_card.sql create mode 100644 test/add_payment_account_cash.sql create mode 100644 test/add_payment_account_other.sql create mode 100644 test/edit_payment.sql create mode 100644 test/edit_payment_account_bank.sql create mode 100644 test/edit_payment_account_card.sql create mode 100644 test/edit_payment_account_cash.sql create mode 100644 test/edit_payment_account_other.sql create mode 100644 test/expense_payment.sql create mode 100644 test/payment.sql create mode 100644 test/payment_account.sql create mode 100644 test/payment_account_bank.sql create mode 100644 test/payment_account_card.sql create mode 100644 test/payment_account_type.sql create mode 100644 test/payment_account_type_i18n.sql create mode 100644 test/payment_status.sql create mode 100644 test/payment_status_i18n.sql create mode 100644 test/update_expense_payment_status.sql create mode 100644 verify/add_payment.sql create mode 100644 verify/add_payment_account_bank.sql create mode 100644 verify/add_payment_account_card.sql create mode 100644 verify/add_payment_account_cash.sql create mode 100644 verify/add_payment_account_other.sql create mode 100644 verify/available_expense_status@v2.sql create mode 100644 verify/available_payment_account_types.sql create mode 100644 verify/available_payment_status.sql create mode 100644 verify/edit_payment.sql create mode 100644 verify/edit_payment_account_bank.sql create mode 100644 verify/edit_payment_account_card.sql create mode 100644 verify/edit_payment_account_cash.sql create mode 100644 verify/edit_payment_account_other.sql create mode 100644 verify/expense_payment.sql create mode 100644 verify/payment.sql create mode 100644 verify/payment_account.sql create mode 100644 verify/payment_account_bank.sql create mode 100644 verify/payment_account_card.sql create mode 100644 verify/payment_account_type.sql create mode 100644 verify/payment_account_type_i18n.sql create mode 100644 verify/payment_status.sql create mode 100644 verify/payment_status_i18n.sql create mode 100644 verify/update_expense_payment_status.sql create mode 100644 web/template/payments/accounts/edit.gohtml create mode 100644 web/template/payments/accounts/index.gohtml create mode 100644 web/template/payments/accounts/new.gohtml create mode 100644 web/template/payments/edit.gohtml create mode 100644 web/template/payments/index.gohtml create mode 100644 web/template/payments/new.gohtml diff --git a/deploy/add_payment.sql b/deploy/add_payment.sql new file mode 100644 index 0000000..7e8ce03 --- /dev/null +++ b/deploy/add_payment.sql @@ -0,0 +1,67 @@ +-- Deploy numerus:add_payment to pg +-- requires: roles +-- requires: schema_numerus +-- requires: payment +-- requires: expense_payment +-- requires: company +-- requires: currency +-- requires: parse_price +-- requires: tag_name +-- requires: update_expense_payment_status + +begin; + +set search_path to numerus, public; + +create or replace function add_payment(company integer, expense_id integer, payment_date date, payment_account_id integer, description text, amount text, tags tag_name[]) returns uuid as +$$ +declare + pslug uuid; + pid integer; + amount_cents integer; +begin + insert into payment + ( company_id + , payment_account_id + , description + , payment_date + , amount + , currency_code + , payment_status + , tags + ) + select company_id + , payment_account_id + , description + , payment_date + , parse_price(amount, currency.decimal_digits) + , currency_code + , 'complete' + , tags + from company + join currency using (currency_code) + where company.company_id = add_payment.company + returning payment_id, slug, payment.amount + into pid, pslug, amount_cents + ; + + if expense_id is not null then + -- must be inserted before updating statuses, so that it can see this + -- payment’s amount too. + insert into expense_payment (expense_id, payment_id) + values (expense_id, pid); + + perform update_expense_payment_status(pid, expense_id, amount_cents); + end if; + + return pslug; +end +$$ + language plpgsql +; + +revoke execute on function add_payment(integer, integer, date, integer, text, text, tag_name[]) from public; +grant execute on function add_payment(integer, integer, date, integer, text, text, tag_name[]) to invoicer; +grant execute on function add_payment(integer, integer, date, integer, text, text, tag_name[]) to admin; + +commit; diff --git a/deploy/add_payment_account_bank.sql b/deploy/add_payment_account_bank.sql new file mode 100644 index 0000000..404c7dc --- /dev/null +++ b/deploy/add_payment_account_bank.sql @@ -0,0 +1,35 @@ +-- Deploy numerus:add_payment_account_bank to pg +-- requires: roles +-- requires: schema_numerus +-- requires: payment_account +-- requires: payment_account_bank + +begin; + +set search_path to numerus, public; + +create or replace function add_payment_account_bank(company integer, name text, iban iban) returns uuid as +$$ +declare + account_id integer; + account_slug uuid; +begin + insert into payment_account (company_id, payment_account_type, name) + select company, 'bank', add_payment_account_bank.name + returning payment_account_id, slug into account_id, account_slug; + + insert into payment_account_bank (payment_account_id, iban) + values (account_id, iban) + ; + + return account_slug; +end; +$$ + language plpgsql +; + +revoke execute on function add_payment_account_bank(integer, text, iban) from public; +grant execute on function add_payment_account_bank(integer, text, iban) to invoicer; +grant execute on function add_payment_account_bank(integer, text, iban) to admin; + +commit; diff --git a/deploy/add_payment_account_card.sql b/deploy/add_payment_account_card.sql new file mode 100644 index 0000000..a64b1a5 --- /dev/null +++ b/deploy/add_payment_account_card.sql @@ -0,0 +1,34 @@ +-- Deploy numerus:add_payment_account_card to pg +-- requires: roles +-- requires: schema_numerus +-- requires: payment_account +-- requires: payment_account_card + +begin; + +set search_path to numerus, public; + +create or replace function add_payment_account_card(company integer, name text, four_digits text, exp_date date) returns uuid as +$$ +declare + account_id integer; + account_slug uuid; +begin + insert into payment_account (company_id, payment_account_type, name) + select company, 'card', add_payment_account_card.name + returning payment_account_id, slug into account_id, account_slug; + + insert into payment_account_card (payment_account_id, last_four_digits, expiration_date) + values (account_id, four_digits, exp_date); + + return account_slug; +end +$$ + language plpgsql +; + +revoke execute on function add_payment_account_card(integer, text, text, date) from public; +grant execute on function add_payment_account_card(integer, text, text, date) to invoicer; +grant execute on function add_payment_account_card(integer, text, text, date) to admin; + +commit; diff --git a/deploy/add_payment_account_cash.sql b/deploy/add_payment_account_cash.sql new file mode 100644 index 0000000..fa22e10 --- /dev/null +++ b/deploy/add_payment_account_cash.sql @@ -0,0 +1,23 @@ +-- Deploy numerus:add_payment_account_cash to pg +-- requires: roles +-- requires: schema_numerus +-- requires: payment_account + +begin; + +set search_path to numerus, public; + +create or replace function add_payment_account_cash(company integer, name text) returns uuid as +$$ + insert into payment_account (company_id, payment_account_type, name) + values (company, 'cash', name) + returning slug; +$$ + language sql +; + +revoke execute on function add_payment_account_cash(integer, text) from public; +grant execute on function add_payment_account_cash(integer, text) to invoicer; +grant execute on function add_payment_account_cash(integer, text) to admin; + +commit; diff --git a/deploy/add_payment_account_other.sql b/deploy/add_payment_account_other.sql new file mode 100644 index 0000000..0a56119 --- /dev/null +++ b/deploy/add_payment_account_other.sql @@ -0,0 +1,23 @@ +-- Deploy numerus:add_payment_account_other to pg +-- requires: roles +-- requires: schema_numerus +-- requires: payment_account + +begin; + +set search_path to numerus, public; + +create or replace function add_payment_account_other(company integer, name text) returns uuid as +$$ + insert into payment_account (company_id, payment_account_type, name) + values (company, 'other', name) + returning slug; +$$ + language sql +; + +revoke execute on function add_payment_account_other(integer, text) from public; +grant execute on function add_payment_account_other(integer, text) to invoicer; +grant execute on function add_payment_account_other(integer, text) to admin; + +commit; diff --git a/deploy/available_expense_status.sql b/deploy/available_expense_status.sql index 9348782..62c69f3 100644 --- a/deploy/available_expense_status.sql +++ b/deploy/available_expense_status.sql @@ -9,14 +9,19 @@ set search_path to numerus; insert into expense_status (expense_status, name) values ('pending', 'Pending') + , ('partial', 'Partial') , ('paid', 'Paid') +on conflict (expense_status) do nothing ; insert into expense_status_i18n (expense_status, lang_tag, name) values ('pending', 'ca', 'Pendent') + , ('partial', 'ca', 'Parcial') , ('paid', 'ca', 'Pagada') , ('pending', 'es', 'Pendiente') + , ('partial', 'es', 'Parcial') , ('paid', 'es', 'Pagada') +on conflict (expense_status, lang_tag) do nothing ; commit; diff --git a/deploy/available_expense_status@v2.sql b/deploy/available_expense_status@v2.sql new file mode 100644 index 0000000..9348782 --- /dev/null +++ b/deploy/available_expense_status@v2.sql @@ -0,0 +1,22 @@ +-- Deploy numerus:available_expense_status to pg +-- requires: schema_numerus +-- requires: expense_status +-- requires: expense_status_i18n + +begin; + +set search_path to numerus; + +insert into expense_status (expense_status, name) +values ('pending', 'Pending') + , ('paid', 'Paid') +; + +insert into expense_status_i18n (expense_status, lang_tag, name) +values ('pending', 'ca', 'Pendent') + , ('paid', 'ca', 'Pagada') + , ('pending', 'es', 'Pendiente') + , ('paid', 'es', 'Pagada') +; + +commit; diff --git a/deploy/available_payment_account_types.sql b/deploy/available_payment_account_types.sql new file mode 100644 index 0000000..327c1c9 --- /dev/null +++ b/deploy/available_payment_account_types.sql @@ -0,0 +1,28 @@ +-- Deploy numerus:available_payment_account_types to pg +-- requires: schema_numerus +-- requires: payment_account_type +-- requires: payment_account_type_i18n + +begin; + +set search_path to numerus; + +insert into payment_account_type (payment_account_type, name) +values ('bank', 'Bank') + , ('card', 'Credit Card') + , ('cash', 'Cash') + , ('other', 'Other') +; + +insert into payment_account_type_i18n (payment_account_type, lang_tag, name) +values ('bank', 'ca', 'Banc') + , ('card', 'ca', 'Targeta de crèdit') + , ('cash', 'ca', 'Efectiu') + , ('other', 'ca', 'Altres') + , ('bank', 'es', 'Banco') + , ('card', 'es', 'Tarjeta de crédito') + , ('cash', 'es', 'Efectivo') + , ('other', 'es', 'Otros') +; + +commit; diff --git a/deploy/available_payment_status.sql b/deploy/available_payment_status.sql new file mode 100644 index 0000000..4a45d68 --- /dev/null +++ b/deploy/available_payment_status.sql @@ -0,0 +1,22 @@ +-- Deploy numerus:available_payment_status to pg +-- requires: schema_numerus +-- requires: payment_status +-- requires: payment_status_i18n + +begin; + +set search_path to numerus, public; + +insert into payment_status (payment_status, name) +values ('partial', 'Partial') + , ('complete', 'Complete') +; + +insert into payment_status_i18n (payment_status, lang_tag, name) +values ('partial', 'ca', 'Parcial') + , ('partial', 'es', 'Parcial') + , ('complete', 'ca', 'Complet') + , ('complete', 'es', 'Completo') +; + +commit; diff --git a/deploy/edit_payment.sql b/deploy/edit_payment.sql new file mode 100644 index 0000000..8d706f1 --- /dev/null +++ b/deploy/edit_payment.sql @@ -0,0 +1,53 @@ +-- Deploy numerus:edit_payment to pg +-- requires: roles +-- requires: schema_numerus +-- requires: payment +-- requires: expense_payment +-- requires: currency +-- requires: parse_price +-- requires: tag_name +-- requires: update_expense_payment_status + +begin; + +set search_path to numerus, public; + +create or replace function edit_payment(payment_slug uuid, payment_date date, payment_account_id integer, description text, amount text, tags tag_name[]) returns uuid as +$$ +declare + pid integer; + eid integer; + amount_cents integer; +begin + update payment + set payment_date = edit_payment.payment_date + , payment_account_id = edit_payment.payment_account_id + , description = edit_payment.description + , amount = parse_price(edit_payment.amount, decimal_digits) + , tags = edit_payment.tags + from currency + where slug = payment_slug + and currency.currency_code = payment.currency_code + returning payment_id, payment.amount + into pid, amount_cents + ; + + select expense_id into eid + from expense_payment + where payment_id = pid; + + if eid is not null then + perform update_expense_payment_status(pid, eid, amount_cents); + end if; + + return payment_slug; +end +$$ + language plpgsql +; + +revoke execute on function edit_payment(uuid, date, integer, text, text, tag_name[]) from public; +grant execute on function edit_payment(uuid, date, integer, text, text, tag_name[]) to invoicer; +grant execute on function edit_payment(uuid, date, integer, text, text, tag_name[]) to admin; + +commit; diff --git a/deploy/edit_payment_account_bank.sql b/deploy/edit_payment_account_bank.sql new file mode 100644 index 0000000..b72a9f3 --- /dev/null +++ b/deploy/edit_payment_account_bank.sql @@ -0,0 +1,44 @@ +-- Deploy numerus:edit_payment_account_bank to pg +-- requires: roles +-- requires: schema_numerus +-- requires: payment_account +-- requires: payment_account_bank +-- requires: extension_pgcrypto +-- requires: extension_iban + +begin; + +set search_path to numerus, public; + +create or replace function edit_payment_account_bank(account_slug uuid, new_name text, new_iban iban) returns uuid as +$$ +declare + account_id int; +begin + update payment_account + set name = new_name + where slug = account_slug + and payment_account_type = 'bank' + returning payment_account_id into account_id + ; + + if account_id is null then + return null; + end if; + + update payment_account_bank + set iban = new_iban + where payment_account_id = account_id + ; + + return account_slug; +end +$$ + language plpgsql +; + +revoke execute on function edit_payment_account_bank(uuid, text, iban) from public; +grant execute on function edit_payment_account_bank(uuid, text, iban) to invoicer; +grant execute on function edit_payment_account_bank(uuid, text, iban) to admin; + +commit; diff --git a/deploy/edit_payment_account_card.sql b/deploy/edit_payment_account_card.sql new file mode 100644 index 0000000..0237296 --- /dev/null +++ b/deploy/edit_payment_account_card.sql @@ -0,0 +1,44 @@ +-- Deploy numerus:edit_payment_account_card to pg +-- requires: roles +-- requires: schema_numerus +-- requires: payment_account +-- requires: payment_account_card +-- requires: extension_pgcrypto + +begin; + +set search_path to numerus, public; + +create or replace function edit_payment_account_card(account_slug uuid, new_name text, new_last_digits text, new_exp_date date) returns uuid as +$$ +declare + account_id integer; +begin + update payment_account + set name = new_name + where slug = account_slug + and payment_account_type = 'card' + returning payment_account_id into account_id + ; + + if account_id is null then + return null; + end if; + + update payment_account_card + set last_four_digits = new_last_digits + , expiration_date = new_exp_date + where payment_account_id = account_id + ; + + return account_slug; +end +$$ + language plpgsql +; + +revoke execute on function edit_payment_account_card(uuid, text, text, date) from public; +grant execute on function edit_payment_account_card(uuid, text, text, date) to invoicer; +grant execute on function edit_payment_account_card(uuid, text, text, date) to admin; + +commit; diff --git a/deploy/edit_payment_account_cash.sql b/deploy/edit_payment_account_cash.sql new file mode 100644 index 0000000..b947cc2 --- /dev/null +++ b/deploy/edit_payment_account_cash.sql @@ -0,0 +1,27 @@ +-- Deploy numerus:edit_payment_account_cash to pg +-- requires: roles +-- requires: schema_numerus +-- requires: payment_account +-- requires: extension_pgcrypto + +begin; + +set search_path to numerus, public; + +create or replace function edit_payment_account_cash(account_slug uuid, new_name text) returns uuid as +$$ + update payment_account + set name = new_name + where slug = account_slug + and payment_account_type = 'cash' + returning slug + ; +$$ + language sql +; + +revoke execute on function edit_payment_account_cash(uuid, text) from public; +grant execute on function edit_payment_account_cash(uuid, text) to invoicer; +grant execute on function edit_payment_account_cash(uuid, text) to admin; + +commit; diff --git a/deploy/edit_payment_account_other.sql b/deploy/edit_payment_account_other.sql new file mode 100644 index 0000000..b15db25 --- /dev/null +++ b/deploy/edit_payment_account_other.sql @@ -0,0 +1,27 @@ +-- Deploy numerus:edit_payment_account_other to pg +-- requires: roles +-- requires: schema_numerus +-- requires: payment_account +-- requires: extension_pgcrypto + +begin; + +set search_path to numerus, public; + +create or replace function edit_payment_account_other(account_slug uuid, new_name text) returns uuid as +$$ + update payment_account + set name = new_name + where slug = account_slug + and payment_account_type = 'other' + returning slug + ; +$$ + language sql +; + +revoke execute on function edit_payment_account_other(uuid, text) from public; +grant execute on function edit_payment_account_other(uuid, text) to invoicer; +grant execute on function edit_payment_account_other(uuid, text) to admin; + +commit; diff --git a/deploy/expense_payment.sql b/deploy/expense_payment.sql new file mode 100644 index 0000000..079b853 --- /dev/null +++ b/deploy/expense_payment.sql @@ -0,0 +1,32 @@ +-- Deploy numerus:expense_payment to pg +-- requires: roles +-- requires: schema_numerus +-- requires: expense +-- requires: payment + +begin; + +set search_path to numerus, public; + +create table expense_payment ( + expense_id integer not null references expense, + payment_id integer not null references payment, + primary key (expense_id, payment_id) +); + +grant select, insert, update, delete on table expense_payment to invoicer; +grant select, insert, update, delete on table expense_payment to admin; + +alter table expense_payment enable row level security; + +create policy company_policy +on expense_payment +using ( + exists( + select 1 + from expense + where expense.expense_id = expense_payment.expense_id + ) +); + +commit; diff --git a/deploy/payment.sql b/deploy/payment.sql new file mode 100644 index 0000000..adb560a --- /dev/null +++ b/deploy/payment.sql @@ -0,0 +1,47 @@ +-- Deploy numerus:payment to pg +-- requires: roles +-- requires: schema_numerus +-- requires: company +-- requires: payment_account +-- requires: currency +-- requires: tag_name +-- requires: payment_status +-- requires: extension_pgcrypto + +begin; + +set search_path to numerus, public; + +create table payment ( + payment_id integer generated by default as identity primary key, + company_id integer not null references company, + slug uuid not null unique default gen_random_uuid(), + description text not null, + payment_date date not null default current_date, + payment_account_id integer not null references payment_account, + amount integer not null, + currency_code text not null references currency, + tags tag_name[] not null default '{}', + payment_status text not null default 'complete' references payment_status, + created_at timestamptz not null default current_timestamp +); + +create index on payment using gin (tags); + +grant select, insert, update, delete on table payment to invoicer; +grant select, insert, update, delete on table payment to admin; + +alter table payment enable row level security; + +create policy company_policy +on payment +using ( + exists( + select 1 + from company_user + join user_profile using (user_id) + where company_user.company_id = payment.company_id + ) +); + +commit; diff --git a/deploy/payment_account.sql b/deploy/payment_account.sql new file mode 100644 index 0000000..5bdbe5c --- /dev/null +++ b/deploy/payment_account.sql @@ -0,0 +1,38 @@ +-- Deploy numerus:payment_account to pg +-- requires: roles +-- requires: schema_numerus +-- requires: company +-- requires: payment_account_type +-- requires: extension_pgcrypto + +begin; + +set search_path to numerus, public; + +create table payment_account ( + payment_account_id integer generated by default as identity primary key, + company_id integer not null references company, + slug uuid not null unique default gen_random_uuid(), + payment_account_type text not null references payment_account_type, + name text not null constraint payment_account_name_not_empty check(length(trim(name)) > 0), + unique (payment_account_id, payment_account_type) +); + +grant select, insert, update, delete on table payment_account to invoicer; +grant select, insert, update, delete on table payment_account to admin; + +alter table payment_account enable row level security; + +create policy company_policy +on payment_account +using ( + exists( + select 1 + from company_user + join user_profile using (user_id) + where company_user.company_id = payment_account.company_id + ) +); + + +commit; diff --git a/deploy/payment_account_bank.sql b/deploy/payment_account_bank.sql new file mode 100644 index 0000000..b063dc8 --- /dev/null +++ b/deploy/payment_account_bank.sql @@ -0,0 +1,33 @@ +-- Deploy numerus:payment_account_bank to pg +-- requires: roles +-- requires: schema_numerus +-- requires: payment_account +-- requires: extension_iban + +begin; + +set search_path to numerus, public; + +create table payment_account_bank ( + payment_account_id integer primary key, + payment_account_type text not null default 'bank' constraint payment_account_type_is_bank check (payment_account_type = 'bank'), + iban iban not null, + foreign key (payment_account_id, payment_account_type) references payment_account (payment_account_id, payment_account_type) +); + +grant select, insert, update, delete on table payment_account_bank to invoicer; +grant select, insert, update, delete on table payment_account_bank to admin; + +alter table payment_account_bank enable row level security; + +create policy company_policy +on payment_account_bank +using ( + exists( + select 1 + from payment_account + where payment_account.payment_account_id = payment_account_bank.payment_account_id + ) +); + +commit; diff --git a/deploy/payment_account_card.sql b/deploy/payment_account_card.sql new file mode 100644 index 0000000..937ddbe --- /dev/null +++ b/deploy/payment_account_card.sql @@ -0,0 +1,33 @@ +-- Deploy numerus:payment_account_card to pg +-- requires: roles +-- requires: schema_numerus +-- requires: payment_account + +begin; + +set search_path to numerus, public; + +create table payment_account_card ( + payment_account_id integer primary key, + payment_account_type text not null default 'card' constraint payment_account_type_is_card check (payment_account_type = 'card'), + last_four_digits text not null constraint last_four_digits_are_digits check ( last_four_digits ~ '^\d{4}$'), + expiration_date date not null, + foreign key (payment_account_id, payment_account_type) references payment_account (payment_account_id, payment_account_type) +); + +grant select, insert, update, delete on table payment_account_card to invoicer; +grant select, insert, update, delete on table payment_account_card to admin; + +alter table payment_account_card enable row level security; + +create policy company_policy +on payment_account_card +using ( + exists( + select 1 + from payment_account + where payment_account.payment_account_id = payment_account_card.payment_account_id + ) +); + +commit; diff --git a/deploy/payment_account_type.sql b/deploy/payment_account_type.sql new file mode 100644 index 0000000..ff9b5d1 --- /dev/null +++ b/deploy/payment_account_type.sql @@ -0,0 +1,17 @@ +-- Deploy numerus:payment_account_type to pg +-- requires: roles +-- requires: schema_numerus + +begin; + +set search_path to numerus, public; + +create table payment_account_type ( + payment_account_type text primary key, + name text not null +); + +grant select on table payment_account_type to invoicer; +grant select on table payment_account_type to admin; + +commit; diff --git a/deploy/payment_account_type_i18n.sql b/deploy/payment_account_type_i18n.sql new file mode 100644 index 0000000..eff9a7e --- /dev/null +++ b/deploy/payment_account_type_i18n.sql @@ -0,0 +1,21 @@ +-- Deploy numerus:payment_account_type_i18n to pg +-- requires: roles +-- requires: schema_numerus +-- requires: payment_account_type +-- requires: language + +begin; + +set search_path to numerus, public; + +create table payment_account_type_i18n ( + payment_account_type text not null references payment_account_type, + lang_tag text not null references language, + name text not null, + primary key (payment_account_type, lang_tag) +); + +grant select on table payment_account_type_i18n to invoicer; +grant select on table payment_account_type_i18n to admin; + +commit; diff --git a/deploy/payment_status.sql b/deploy/payment_status.sql new file mode 100644 index 0000000..f8991e1 --- /dev/null +++ b/deploy/payment_status.sql @@ -0,0 +1,17 @@ +-- Deploy numerus:payment_status to pg +-- requires: roles +-- requires: schema_numerus + +begin; + +set search_path to numerus, public; + +create table payment_status ( + payment_status text primary key, + name text not null +); + +grant select on table payment_status to invoicer; +grant select on table payment_status to admin; + +commit; diff --git a/deploy/payment_status_i18n.sql b/deploy/payment_status_i18n.sql new file mode 100644 index 0000000..6bcd136 --- /dev/null +++ b/deploy/payment_status_i18n.sql @@ -0,0 +1,21 @@ +-- Deploy numerus:payment_status_i18n to pg +-- requires: roles +-- requires: schema_numerus +-- requires: payment_status +-- requires: language + +begin; + +set search_path to numerus, public; + +create table payment_status_i18n ( + payment_status text not null references payment_status, + lang_tag text not null references language, + name text not null, + primary key (payment_status, lang_tag) +); + +grant select on table payment_status_i18n to invoicer; +grant select on table payment_status_i18n to admin; + +commit; diff --git a/deploy/update_expense_payment_status.sql b/deploy/update_expense_payment_status.sql new file mode 100644 index 0000000..ecaee80 --- /dev/null +++ b/deploy/update_expense_payment_status.sql @@ -0,0 +1,43 @@ +-- Deploy numerus:update_expense_payment_status to pg +-- requires: roles +-- requires: schema_numerus +-- requires: expense +-- requires: payment +-- requires: expense_payment +-- requires: available_expense_status +-- requires: available_payment_status + +begin; + +set search_path to numerus, public; + +create or replace function update_expense_payment_status(pid integer, eid integer, amount_cents integer) returns void as +$$ + update payment + set payment_status = case when expense.amount > amount_cents or exists (select 1 from expense_payment as ep where ep.expense_id = expense.expense_id and payment_id <> pid) then 'partial' else 'complete' end + from expense + where expense.expense_id = eid + and payment_id = pid + ; + + update expense + set expense_status = case when paid_amount >= expense.amount then 'paid' else 'partial' end + from ( + select expense_payment.expense_id + , sum(payment.amount) as paid_amount + from expense_payment + join payment using (payment_id) + group by expense_payment.expense_id + ) as payment + where payment.expense_id = expense.expense_id + and expense.expense_id = eid + ; +$$ + language sql +; + +revoke execute on function update_expense_payment_status(integer, integer, integer) from public; +grant execute on function update_expense_payment_status(integer, integer, integer) to invoicer; +grant execute on function update_expense_payment_status(integer, integer, integer) to admin; + +commit; diff --git a/pkg/accounts.go b/pkg/accounts.go new file mode 100644 index 0000000..bb87d97 --- /dev/null +++ b/pkg/accounts.go @@ -0,0 +1,340 @@ +package pkg + +import ( + "context" + "github.com/jackc/pgtype" + "github.com/julienschmidt/httprouter" + "html/template" + "net/http" + "time" +) + +const ( + ExpirationDateFormat = "01/06" + AccountTypeBank = "bank" + AccountTypeCard = "card" + AccountTypeCash = "cash" + AccountTypeOther = "other" +) + +func servePaymentAccountIndex(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + conn := getConn(r) + locale := getLocale(r) + + page := NewPaymentAccountIndexPage(r.Context(), conn, locale) + page.MustRender(w, r) +} + +type PaymentAccountIndexPage struct { + Accounts []*PaymentAccountEntry +} + +func NewPaymentAccountIndexPage(ctx context.Context, conn *Conn, locale *Locale) *PaymentAccountIndexPage { + return &PaymentAccountIndexPage{ + Accounts: mustCollectPaymentAccountEntries(ctx, conn, locale), + } +} + +func (page *PaymentAccountIndexPage) MustRender(w http.ResponseWriter, r *http.Request) { + mustRenderMainTemplate(w, r, "payments/accounts/index.gohtml", page) +} + +type PaymentAccountEntry struct { + ID int + Slug string + Type string + TypeLabel string + Name string + IBAN string + LastFourDigits string + ExpirationDate string +} + +func mustCollectPaymentAccountEntries(ctx context.Context, conn *Conn, locale *Locale) []*PaymentAccountEntry { + rows := conn.MustQuery(ctx, ` + select payment_account_id + , slug + , payment_account.payment_account_type + , coalesce(i18n.name, payment_account_type.name) + , payment_account.name + , coalesce(iban::text, '') as iban + , coalesce(last_four_digits, '') as last_four_digits + , expiration_date + from payment_account + left join payment_account_bank using (payment_account_id, payment_account_type) + left join payment_account_card using (payment_account_id, payment_account_type) + join payment_account_type using (payment_account_type) + left join payment_account_type_i18n as i18n on payment_account_type.payment_account_type = i18n.payment_account_type and i18n.lang_tag = $1 + order by payment_account_id + `, locale.Language.String()) + defer rows.Close() + + var entries []*PaymentAccountEntry + for rows.Next() { + entry := &PaymentAccountEntry{} + var expirationDate pgtype.Date + if err := rows.Scan(&entry.ID, &entry.Slug, &entry.Type, &entry.TypeLabel, &entry.Name, &entry.IBAN, &entry.LastFourDigits, &expirationDate); err != nil { + panic(err) + } + if expirationDate.Status == pgtype.Present { + entry.ExpirationDate = expirationDate.Time.Format(ExpirationDateFormat) + } + entries = append(entries, entry) + } + if rows.Err() != nil { + panic(rows.Err()) + } + + return entries +} + +func servePaymentAccountForm(w http.ResponseWriter, r *http.Request, params httprouter.Params) { + locale := getLocale(r) + conn := getConn(r) + company := mustGetCompany(r) + form := newPaymentAccountForm(r.Context(), conn, locale, company) + slug := params[0].Value + if slug == "new" { + form.MustRender(w, r) + return + } + if !ValidUuid(slug) { + http.NotFound(w, r) + return + } + if !form.MustFillFromDatabase(r.Context(), conn, slug) { + http.NotFound(w, r) + return + } + form.MustRender(w, r) +} + +type PaymentAccountForm struct { + locale *Locale + company *Company + Slug string + Type *RadioField + Name *InputField + IBAN *InputField + LastFourDigits *InputField + ExpirationMonthYear *InputField +} + +func newPaymentAccountForm(ctx context.Context, conn *Conn, locale *Locale, company *Company) *PaymentAccountForm { + return &PaymentAccountForm{ + locale: locale, + company: company, + Type: &RadioField{ + Name: "payment_account_type", + Label: pgettext("input", "Type", locale), + Required: true, + Options: MustGetRadioOptions(ctx, conn, "select payment_account_type, i18n.name from payment_account_type join payment_account_type_i18n as i18n using (payment_account_type) where i18n.lang_tag = $1 order by payment_account_type", locale.Language.String()), + Attributes: []template.HTMLAttr{ + `x-model="type"`, + }, + }, + Name: &InputField{ + Name: "name", + Label: pgettext("input", "Name", locale), + Required: true, + Type: "text", + }, + IBAN: &InputField{ + Name: "iban", + Label: pgettext("input", "IBAN", locale), + Required: true, + Type: "text", + }, + LastFourDigits: &InputField{ + Name: "last_four_digits", + Label: pgettext("input", "Card’s last four digits", locale), + Required: true, + Type: "text", + Attributes: []template.HTMLAttr{ + `maxlength="4"`, + `minlength="4"`, + `pattern="[0-9]{4}"`, + }, + }, + ExpirationMonthYear: &InputField{ + Name: "expiration_date", + Label: pgettext("input", "Expiration date", locale), + Required: true, + Type: "text", + Attributes: []template.HTMLAttr{ + `maxlength="5"`, + `minlength="5"`, + `pattern="[0-9]{2}/[0-9]{2}"`, + }, + }, + } +} + +func (f *PaymentAccountForm) MustRender(w http.ResponseWriter, r *http.Request) { + if f.Slug == "" { + mustRenderMainTemplate(w, r, "payments/accounts/new.gohtml", f) + } else { + mustRenderMainTemplate(w, r, "payments/accounts/edit.gohtml", f) + } +} + +func (f *PaymentAccountForm) MustFillFromDatabase(ctx context.Context, conn *Conn, slug string) bool { + selectedType := f.Type.Selected + var expirationDate pgtype.Date + if notFoundErrorOrPanic(conn.QueryRow(ctx, ` + select payment_account_type + , name + , coalesce(iban::text, '') as iban + , coalesce(last_four_digits, '') as last_four_digits + , expiration_date + from payment_account + left join payment_account_bank using (payment_account_id, payment_account_type) + left join payment_account_card using (payment_account_id, payment_account_type) + where slug = $1 + `, slug).Scan( + f.Type, + f.Name, + f.IBAN, + f.LastFourDigits, + &expirationDate)) { + f.Type.Selected = selectedType + return false + } + f.Slug = slug + if expirationDate.Status == pgtype.Present { + f.ExpirationMonthYear.Val = expirationDate.Time.Format(ExpirationDateFormat) + } else { + f.ExpirationMonthYear.Val = "" + } + return true +} + +func (f *PaymentAccountForm) Parse(r *http.Request) error { + if err := r.ParseForm(); err != nil { + return err + } + f.Type.FillValue(r) + f.Name.FillValue(r) + f.IBAN.FillValue(r) + f.LastFourDigits.FillValue(r) + f.ExpirationMonthYear.FillValue(r) + return nil +} + +func (f *PaymentAccountForm) Validate(ctx context.Context, conn *Conn) bool { + validator := newFormValidator() + + if validator.CheckValidRadioOption(f.Type, gettext("Selected payment account type is not valid.", f.locale)) { + switch f.Type.Selected { + case AccountTypeBank: + if validator.CheckRequiredInput(f.IBAN, gettext("IBAN can not be empty.", f.locale)) { + validator.CheckValidIBANInput(ctx, conn, f.IBAN, gettext("This value is not a valid IBAN.", f.locale)) + } + case AccountTypeCard: + if validator.CheckRequiredInput(f.LastFourDigits, gettext("Last four digits can not be empty.", f.locale)) { + if validator.CheckInputLength(f.LastFourDigits, 4, gettext("You must enter the card’s last four digits", f.locale)) { + validator.CheckValidInteger(f.LastFourDigits, 0, 9999, gettext("Last four digits must be a number.", f.locale)) + } + } + if validator.CheckRequiredInput(f.ExpirationMonthYear, gettext("Expiration date can not be empty.", f.locale)) { + _, err := time.Parse(ExpirationDateFormat, f.ExpirationMonthYear.Val) + validator.checkInput(f.ExpirationMonthYear, err == nil, gettext("Expiration date should be a valid date in format MM/YY (e.g., 08/24).", f.locale)) + } + } + } + validator.CheckRequiredInput(f.Name, gettext("Payment account name can not be empty.", f.locale)) + + return validator.AllOK() +} + +func (f *PaymentAccountForm) ExpirationDate() (time.Time, error) { + date, err := time.Parse(ExpirationDateFormat, f.ExpirationMonthYear.Val) + if err != nil { + return date, err + } + return date.AddDate(0, 1, -1), nil +} + +func handleAddPaymentAccount(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + locale := getLocale(r) + conn := getConn(r) + company := mustGetCompany(r) + form := newPaymentAccountForm(r.Context(), conn, locale, company) + if err := form.Parse(r); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if err := verifyCsrfTokenValid(r); err != nil { + http.Error(w, err.Error(), http.StatusForbidden) + return + } + if !form.Validate(r.Context(), conn) { + if !IsHTMxRequest(r) { + w.WriteHeader(http.StatusUnprocessableEntity) + } + form.MustRender(w, r) + return + } + switch form.Type.Selected { + case AccountTypeBank: + conn.MustExec(r.Context(), "select add_payment_account_bank($1, $2, $3)", company.Id, form.Name, form.IBAN) + case AccountTypeCard: + date, err := form.ExpirationDate() + if err != nil { + panic(err) + } + conn.MustExec(r.Context(), "select add_payment_account_card($1, $2, $3, $4)", company.Id, form.Name, form.LastFourDigits, date) + case AccountTypeCash: + conn.MustExec(r.Context(), "select add_payment_account_cash($1, $2)", company.Id, form.Name) + case AccountTypeOther: + conn.MustExec(r.Context(), "select add_payment_account_other($1, $2)", company.Id, form.Name) + } + htmxRedirect(w, r, companyURI(company, "/payment-accounts")) +} + +func handleEditPaymentAccount(w http.ResponseWriter, r *http.Request, params httprouter.Params) { + conn := getConn(r) + locale := getLocale(r) + company := mustGetCompany(r) + form := newPaymentAccountForm(r.Context(), conn, locale, company) + form.Slug = params[0].Value + if !ValidUuid(form.Slug) { + http.NotFound(w, r) + return + } + if err := form.Parse(r); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if err := verifyCsrfTokenValid(r); err != nil { + http.Error(w, err.Error(), http.StatusForbidden) + return + } + if !form.Validate(r.Context(), conn) { + if !IsHTMxRequest(r) { + w.WriteHeader(http.StatusUnprocessableEntity) + } + form.MustRender(w, r) + return + } + var found string + switch form.Type.Selected { + case AccountTypeBank: + found = conn.MustGetText(r.Context(), "", "select edit_payment_account_bank($1, $2, $3)", form.Slug, form.Name, form.IBAN) + case AccountTypeCard: + date, err := form.ExpirationDate() + if err != nil { + panic(err) + } + found = conn.MustGetText(r.Context(), "", "select edit_payment_account_card($1, $2, $3, $4)", form.Slug, form.Name, form.LastFourDigits, date) + case AccountTypeCash: + found = conn.MustGetText(r.Context(), "", "select edit_payment_account_cash($1, $2)", form.Slug, form.Name) + case AccountTypeOther: + found = conn.MustGetText(r.Context(), "", "select edit_payment_account_other($1, $2)", form.Slug, form.Name) + } + if found == "" { + http.NotFound(w, r) + return + } + htmxRedirect(w, r, companyURI(company, "/payment-accounts")) +} diff --git a/pkg/form.go b/pkg/form.go index 797fdef..5f439ce 100644 --- a/pkg/form.go +++ b/pkg/form.go @@ -295,6 +295,33 @@ func (field *RadioField) isValidOption(selected string) bool { return field.FindOption(selected) != nil } +func (field *RadioField) HasValidOption() bool { + return field.isValidOption(field.Selected) +} + +func MustGetRadioOptions(ctx context.Context, conn *Conn, sql string, args ...interface{}) []*RadioOption { + rows, err := conn.Query(ctx, sql, args...) + if err != nil { + panic(err) + } + defer rows.Close() + + var options []*RadioOption + for rows.Next() { + option := &RadioOption{} + err = rows.Scan(&option.Value, &option.Label) + if err != nil { + panic(err) + } + options = append(options, option) + } + if rows.Err() != nil { + panic(rows.Err()) + } + + return options +} + type CheckField struct { Name string Label string @@ -452,6 +479,10 @@ func (v *FormValidator) CheckInputMinLength(field *InputField, min int, message return v.checkInput(field, len(field.Val) >= min, message) } +func (v *FormValidator) CheckInputLength(field *InputField, length int, message string) bool { + return v.checkInput(field, len(field.Val) == length, message) +} + func (v *FormValidator) CheckValidEmailInput(field *InputField, message string) bool { _, err := mail.ParseAddress(field.Val) return v.checkInput(field, err == nil, message) @@ -481,6 +512,10 @@ func (v *FormValidator) CheckValidSelectOption(field *SelectField, message strin return v.checkSelect(field, field.HasValidOptions(), message) } +func (v *FormValidator) CheckValidRadioOption(field *RadioField, message string) bool { + return v.checkRadio(field, field.HasValidOption(), message) +} + func (v *FormValidator) CheckAtMostOneOfEachGroup(field *SelectField, message string) bool { repeated := false groups := map[string]bool{} @@ -539,3 +574,11 @@ func (v *FormValidator) checkSelect(field *SelectField, ok bool, message string) } return ok } + +func (v *FormValidator) checkRadio(field *RadioField, ok bool, message string) bool { + if !ok { + field.Errors = append(field.Errors, errors.New(message)) + v.Valid = false + } + return ok +} diff --git a/pkg/payments.go b/pkg/payments.go new file mode 100644 index 0000000..7fb47b7 --- /dev/null +++ b/pkg/payments.go @@ -0,0 +1,261 @@ +package pkg + +import ( + "context" + "fmt" + "github.com/julienschmidt/httprouter" + "html/template" + "math" + "net/http" + "time" +) + +func servePaymentIndex(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + conn := getConn(r) + locale := getLocale(r) + + page := NewPaymentIndexPage(r.Context(), conn, locale) + page.MustRender(w, r) +} + +type PaymentIndexPage struct { + Payments []*PaymentEntry +} + +func NewPaymentIndexPage(ctx context.Context, conn *Conn, locale *Locale) *PaymentIndexPage { + return &PaymentIndexPage{ + Payments: mustCollectPaymentEntries(ctx, conn, locale), + } +} + +func (page *PaymentIndexPage) MustRender(w http.ResponseWriter, r *http.Request) { + mustRenderMainTemplate(w, r, "payments/index.gohtml", page) +} + +type PaymentEntry struct { + ID int + Slug string + PaymentDate time.Time + Description string + Total string + Tags []string + Status string + StatusLabel string +} + +func mustCollectPaymentEntries(ctx context.Context, conn *Conn, locale *Locale) []*PaymentEntry { + rows := conn.MustQuery(ctx, ` + select payment_id + , payment.slug + , payment_date + , description + , to_price(payment.amount, decimal_digits) as total + , payment.tags + , payment.payment_status + , psi18n.name + from payment + join payment_status_i18n psi18n on payment.payment_status = psi18n.payment_status and psi18n.lang_tag = $1 + join currency using (currency_code) + order by payment_date desc, total desc + `, locale.Language) + defer rows.Close() + + var entries []*PaymentEntry + for rows.Next() { + entry := &PaymentEntry{} + if err := rows.Scan(&entry.ID, &entry.Slug, &entry.PaymentDate, &entry.Description, &entry.Total, &entry.Tags, &entry.Status, &entry.StatusLabel); err != nil { + panic(err) + } + entries = append(entries, entry) + } + if rows.Err() != nil { + panic(rows.Err()) + } + + return entries +} + +func servePaymentForm(w http.ResponseWriter, r *http.Request, params httprouter.Params) { + locale := getLocale(r) + conn := getConn(r) + company := mustGetCompany(r) + form := newPaymentForm(r.Context(), conn, locale, company) + slug := params[0].Value + if slug == "new" { + form.PaymentDate.Val = time.Now().Format("2006-01-02") + form.MustRender(w, r) + return + } + if !ValidUuid(slug) { + http.NotFound(w, r) + return + } + if !form.MustFillFromDatabase(r.Context(), conn, slug) { + http.NotFound(w, r) + return + } + form.MustRender(w, r) +} + +type PaymentForm struct { + locale *Locale + company *Company + Slug string + Description *InputField + PaymentDate *InputField + PaymentAccount *SelectField + Amount *InputField + Tags *TagsField +} + +func newPaymentForm(ctx context.Context, conn *Conn, locale *Locale, company *Company) *PaymentForm { + return &PaymentForm{ + locale: locale, + company: company, + Description: &InputField{ + Name: "description", + Label: pgettext("input", "Description", locale), + Required: true, + Type: "text", + }, + PaymentDate: &InputField{ + Name: "payment_date", + Label: pgettext("input", "Invoice Date", locale), + Required: true, + Type: "date", + }, + PaymentAccount: &SelectField{ + Name: "payment_account", + Label: pgettext("input", "Account", locale), + Required: true, + Options: MustGetOptions(ctx, conn, "select payment_account_id::text, name from payment_account order by name"), + }, + Amount: &InputField{ + Name: "amount", + Label: pgettext("input", "Amount", locale), + Type: "number", + Required: true, + Attributes: []template.HTMLAttr{ + `min="0"`, + template.HTMLAttr(fmt.Sprintf(`step="%v"`, company.MinCents())), + }, + }, + Tags: &TagsField{ + Name: "tags", + Label: pgettext("input", "Tags", locale), + }, + } +} + +func (f *PaymentForm) MustRender(w http.ResponseWriter, r *http.Request) { + if f.Slug == "" { + f.PaymentAccount.EmptyLabel = gettext("Select an account.", f.locale) + mustRenderMainTemplate(w, r, "payments/new.gohtml", f) + } else { + mustRenderMainTemplate(w, r, "payments/edit.gohtml", f) + } +} + +func (f *PaymentForm) MustFillFromDatabase(ctx context.Context, conn *Conn, slug string) bool { + selectedPaymentAccount := f.PaymentAccount.Selected + f.PaymentAccount.Clear() + if notFoundErrorOrPanic(conn.QueryRow(ctx, ` + select description + , payment_date + , payment_account_id::text + , to_price(amount, decimal_digits) + , tags + from payment + join currency using (currency_code) + where payment.slug = $1 + `, slug).Scan( + f.Description, + f.PaymentDate, + f.PaymentAccount, + f.Amount, + f.Tags)) { + f.PaymentAccount.Selected = selectedPaymentAccount + return false + } + f.Slug = slug + return true +} + +func (f *PaymentForm) Parse(r *http.Request) error { + if err := r.ParseForm(); err != nil { + return err + } + f.Description.FillValue(r) + f.PaymentDate.FillValue(r) + f.PaymentAccount.FillValue(r) + f.Amount.FillValue(r) + f.Tags.FillValue(r) + return nil +} + +func (f *PaymentForm) Validate() bool { + validator := newFormValidator() + validator.CheckRequiredInput(f.Description, gettext("Description can not be empty.", f.locale)) + validator.CheckValidSelectOption(f.PaymentAccount, gettext("Selected payment account is not valid.", f.locale)) + validator.CheckValidDate(f.PaymentDate, gettext("Payment date must be a valid date.", f.locale)) + if validator.CheckRequiredInput(f.Amount, gettext("Amount can not be empty.", f.locale)) { + validator.CheckValidDecimal(f.Amount, f.company.MinCents(), math.MaxFloat64, gettext("Amount must be a number greater than zero.", f.locale)) + } + return validator.AllOK() +} + +func handleAddPayment(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + locale := getLocale(r) + conn := getConn(r) + company := mustGetCompany(r) + form := newPaymentForm(r.Context(), conn, locale, company) + if err := form.Parse(r); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if err := verifyCsrfTokenValid(r); err != nil { + http.Error(w, err.Error(), http.StatusForbidden) + return + } + if !form.Validate() { + if !IsHTMxRequest(r) { + w.WriteHeader(http.StatusUnprocessableEntity) + } + form.MustRender(w, r) + return + } + conn.MustExec(r.Context(), "select add_payment($1, $2, $3, $4, $5, $6, $7)", company.Id, nil, form.PaymentDate, form.PaymentAccount, form.Description, form.Amount, form.Tags) + htmxRedirect(w, r, companyURI(company, "/payments")) +} + +func handleEditPayment(w http.ResponseWriter, r *http.Request, params httprouter.Params) { + conn := getConn(r) + locale := getLocale(r) + company := mustGetCompany(r) + form := newPaymentForm(r.Context(), conn, locale, company) + form.Slug = params[0].Value + if !ValidUuid(form.Slug) { + http.NotFound(w, r) + return + } + if err := form.Parse(r); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if err := verifyCsrfTokenValid(r); err != nil { + http.Error(w, err.Error(), http.StatusForbidden) + return + } + if !form.Validate() { + if !IsHTMxRequest(r) { + w.WriteHeader(http.StatusUnprocessableEntity) + } + form.MustRender(w, r) + return + } + if found := conn.MustGetText(r.Context(), "", "select edit_payment($1, $2, $3, $4, $5, $6)", form.Slug, form.PaymentDate, form.PaymentAccount, form.Description, form.Amount, form.Tags); found == "" { + http.NotFound(w, r) + return + } + htmxRedirect(w, r, companyURI(company, "/payments")) +} diff --git a/pkg/router.go b/pkg/router.go index 191b6a6..c57d181 100644 --- a/pkg/router.go +++ b/pkg/router.go @@ -58,6 +58,14 @@ func NewRouter(db *Db, demo bool) http.Handler { companyRouter.PUT("/expenses/:slug/tags", HandleUpdateExpenseTags) companyRouter.GET("/expenses/:slug/tags/edit", ServeEditExpenseTags) companyRouter.GET("/expenses/:slug/download/:filename", ServeExpenseAttachment) + companyRouter.GET("/payments", servePaymentIndex) + companyRouter.POST("/payments", handleAddPayment) + companyRouter.GET("/payments/:slug", servePaymentForm) + companyRouter.PUT("/payments/:slug", handleEditPayment) + companyRouter.GET("/payment-accounts", servePaymentAccountIndex) + companyRouter.POST("/payment-accounts", handleAddPaymentAccount) + companyRouter.GET("/payment-accounts/:slug", servePaymentAccountForm) + companyRouter.PUT("/payment-accounts/:slug", handleEditPaymentAccount) companyRouter.GET("/", ServeDashboard) router := httprouter.New() diff --git a/po/ca.po b/po/ca.po index 37f205b..c6f2ba9 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: 2024-07-20 22:51+0200\n" +"POT-Creation-Date: 2024-08-10 04:08+0200\n" "PO-Revision-Date: 2023-01-18 17:08+0100\n" "Last-Translator: jordi fita mas \n" "Language-Team: Catalan \n" @@ -35,6 +35,11 @@ msgstr "Afegeix productes a la factura" #: web/template/expenses/index.gohtml:10 web/template/expenses/edit.gohtml:10 #: web/template/tax-details.gohtml:9 web/template/products/new.gohtml:9 #: web/template/products/index.gohtml:9 web/template/products/edit.gohtml:10 +#: web/template/payments/new.gohtml:10 web/template/payments/index.gohtml:10 +#: web/template/payments/edit.gohtml:10 +#: web/template/payments/accounts/new.gohtml:10 +#: web/template/payments/accounts/index.gohtml:10 +#: web/template/payments/accounts/edit.gohtml:10 msgctxt "title" msgid "Home" msgstr "Inici" @@ -61,6 +66,7 @@ msgstr "Tots" #: web/template/invoices/products.gohtml:49 #: web/template/switch-company.gohtml:22 web/template/quotes/products.gohtml:49 #: web/template/products/index.gohtml:45 +#: web/template/payments/accounts/index.gohtml:25 msgctxt "title" msgid "Name" msgstr "Nom" @@ -107,7 +113,7 @@ msgstr "Subtotal" #: web/template/quotes/new.gohtml:74 web/template/quotes/view.gohtml:82 #: web/template/quotes/view.gohtml:122 web/template/quotes/edit.gohtml:75 #: web/template/expenses/new.gohtml:47 web/template/expenses/index.gohtml:74 -#: web/template/expenses/edit.gohtml:49 +#: web/template/expenses/edit.gohtml:49 web/template/payments/index.gohtml:29 msgctxt "title" msgid "Total" msgstr "Total" @@ -115,6 +121,8 @@ msgstr "Total" #: web/template/invoices/new.gohtml:92 web/template/invoices/edit.gohtml:93 #: web/template/quotes/new.gohtml:92 web/template/quotes/edit.gohtml:93 #: web/template/expenses/new.gohtml:57 web/template/expenses/edit.gohtml:59 +#: web/template/payments/edit.gohtml:35 +#: web/template/payments/accounts/edit.gohtml:38 msgctxt "action" msgid "Update" msgstr "Actualitza" @@ -124,6 +132,8 @@ msgstr "Actualitza" #: web/template/contacts/new.gohtml:49 web/template/contacts/edit.gohtml:53 #: web/template/expenses/new.gohtml:60 web/template/expenses/edit.gohtml:62 #: web/template/products/new.gohtml:30 web/template/products/edit.gohtml:36 +#: web/template/payments/new.gohtml:33 +#: web/template/payments/accounts/new.gohtml:41 msgctxt "action" msgid "Save" msgstr "Desa" @@ -180,14 +190,14 @@ msgid "Customer" msgstr "Client" #: web/template/invoices/index.gohtml:70 web/template/quotes/index.gohtml:70 -#: web/template/expenses/index.gohtml:68 +#: web/template/expenses/index.gohtml:68 web/template/payments/index.gohtml:27 msgctxt "title" msgid "Status" msgstr "Estat" #: web/template/invoices/index.gohtml:71 web/template/quotes/index.gohtml:71 #: web/template/contacts/index.gohtml:50 web/template/expenses/index.gohtml:69 -#: web/template/products/index.gohtml:46 +#: web/template/products/index.gohtml:46 web/template/payments/index.gohtml:28 msgctxt "title" msgid "Tags" msgstr "Etiquetes" @@ -326,7 +336,7 @@ msgctxt "input" msgid "(Max. %s)" msgstr "(Màx. %s)" -#: web/template/form.gohtml:200 +#: web/template/form.gohtml:202 msgctxt "action" msgid "Filters" msgstr "Filtra" @@ -489,15 +499,20 @@ msgstr "Despeses" #: web/template/app.gohtml:57 msgctxt "nav" +msgid "Payments" +msgstr "Pagaments" + +#: web/template/app.gohtml:58 +msgctxt "nav" msgid "Products" msgstr "Productes" -#: web/template/app.gohtml:58 +#: web/template/app.gohtml:59 msgctxt "nav" msgid "Contacts" msgstr "Contactes" -#: web/template/app.gohtml:66 +#: web/template/app.gohtml:67 msgid "Numerus Version: %s" msgstr "Numerus versió: %s" @@ -731,6 +746,84 @@ msgctxt "title" msgid "Edit Product “%s”" msgstr "Edició del producte «%s»" +#: web/template/payments/new.gohtml:3 web/template/payments/new.gohtml:12 +msgctxt "title" +msgid "New Payment" +msgstr "Nou pagament" + +#: web/template/payments/new.gohtml:11 web/template/payments/index.gohtml:3 +#: web/template/payments/index.gohtml:11 web/template/payments/edit.gohtml:11 +msgctxt "title" +msgid "Payments" +msgstr "Pagaments" + +#: web/template/payments/index.gohtml:16 +msgctxt "action" +msgid "New payment" +msgstr "Nou pagament" + +#: web/template/payments/index.gohtml:25 +msgctxt "title" +msgid "Payment Date" +msgstr "Data del pagament" + +#: web/template/payments/index.gohtml:26 +msgctxt "title" +msgid "Description" +msgstr "Descripció" + +#: web/template/payments/index.gohtml:54 +msgid "No payments added yet." +msgstr "No hi ha cap pagament." + +#: web/template/payments/edit.gohtml:3 +msgctxt "title" +msgid "Edit Payment “%s”" +msgstr "Edició del pagament «%s»" + +#: web/template/payments/accounts/new.gohtml:3 +#: web/template/payments/accounts/new.gohtml:12 +msgctxt "title" +msgid "New Payment Account" +msgstr "Nou compte de pagament" + +#: web/template/payments/accounts/new.gohtml:11 +#: web/template/payments/accounts/index.gohtml:3 +#: web/template/payments/accounts/index.gohtml:11 +#: web/template/payments/accounts/edit.gohtml:11 +msgctxt "title" +msgid "Payment Accounts" +msgstr "Comptes de pagament" + +#: web/template/payments/accounts/index.gohtml:16 +msgctxt "action" +msgid "New payment account" +msgstr "Nou compte de pagament" + +#: web/template/payments/accounts/index.gohtml:26 +msgctxt "title" +msgid "Type" +msgstr "Tipus" + +#: web/template/payments/accounts/index.gohtml:27 +msgctxt "title" +msgid "Number" +msgstr "Número" + +#: web/template/payments/accounts/index.gohtml:28 +msgctxt "title" +msgid "Expiration Date" +msgstr "Data de caducitat" + +#: web/template/payments/accounts/index.gohtml:51 +msgid "No payment accounts added yet." +msgstr "No hi ha cap compte de pagament." + +#: web/template/payments/accounts/edit.gohtml:3 +msgctxt "title" +msgid "Edit Payment Account “%s”" +msgstr "Edició del compte de pagament «%s»" + #: pkg/ods.go:62 pkg/ods.go:106 msgctxt "title" msgid "VAT number" @@ -762,49 +855,50 @@ msgstr "No podeu deixar la contrasenya en blanc." msgid "Invalid user or password." msgstr "Nom d’usuari o contrasenya incorrectes." -#: pkg/products.go:172 pkg/products.go:276 pkg/quote.go:901 +#: pkg/products.go:172 pkg/products.go:276 pkg/quote.go:901 pkg/accounts.go:138 #: pkg/invoices.go:1147 pkg/contacts.go:149 pkg/contacts.go:262 msgctxt "input" msgid "Name" msgstr "Nom" #: pkg/products.go:177 pkg/products.go:303 pkg/quote.go:174 pkg/quote.go:708 -#: pkg/expenses.go:343 pkg/expenses.go:511 pkg/invoices.go:177 -#: pkg/invoices.go:877 pkg/invoices.go:1462 pkg/contacts.go:154 -#: pkg/contacts.go:362 +#: pkg/payments.go:145 pkg/expenses.go:343 pkg/expenses.go:510 +#: pkg/invoices.go:177 pkg/invoices.go:877 pkg/invoices.go:1462 +#: pkg/contacts.go:154 pkg/contacts.go:362 msgctxt "input" msgid "Tags" msgstr "Etiquetes" -#: pkg/products.go:181 pkg/quote.go:178 pkg/expenses.go:521 pkg/invoices.go:181 +#: pkg/products.go:181 pkg/quote.go:178 pkg/expenses.go:520 pkg/invoices.go:181 #: pkg/contacts.go:158 msgctxt "input" msgid "Tags Condition" msgstr "Condició de les etiquetes" -#: pkg/products.go:185 pkg/quote.go:182 pkg/expenses.go:525 pkg/invoices.go:185 +#: pkg/products.go:185 pkg/quote.go:182 pkg/expenses.go:524 pkg/invoices.go:185 #: pkg/contacts.go:162 msgctxt "tag condition" msgid "All" msgstr "Totes" -#: pkg/products.go:186 pkg/expenses.go:526 pkg/invoices.go:186 +#: pkg/products.go:186 pkg/expenses.go:525 pkg/invoices.go:186 #: pkg/contacts.go:163 msgid "Invoices must have all the specified labels." msgstr "Les factures han de tenir totes les etiquetes." -#: pkg/products.go:190 pkg/quote.go:187 pkg/expenses.go:530 pkg/invoices.go:190 +#: pkg/products.go:190 pkg/quote.go:187 pkg/expenses.go:529 pkg/invoices.go:190 #: pkg/contacts.go:167 msgctxt "tag condition" msgid "Any" msgstr "Qualsevol" -#: pkg/products.go:191 pkg/expenses.go:531 pkg/invoices.go:191 +#: pkg/products.go:191 pkg/expenses.go:530 pkg/invoices.go:191 #: pkg/contacts.go:168 msgid "Invoices must have at least one of the specified labels." msgstr "Les factures han de tenir com a mínim una de les etiquetes." -#: pkg/products.go:282 pkg/quote.go:915 pkg/invoices.go:1161 +#: pkg/products.go:282 pkg/quote.go:915 pkg/payments.go:117 +#: pkg/invoices.go:1161 msgctxt "input" msgid "Description" msgstr "Descripció" @@ -1062,7 +1156,7 @@ msgctxt "input" msgid "Quotation Status" msgstr "Estat del pressupost" -#: pkg/quote.go:154 pkg/expenses.go:516 pkg/invoices.go:157 +#: pkg/quote.go:154 pkg/expenses.go:515 pkg/invoices.go:157 msgid "All status" msgstr "Tots els estats" @@ -1071,12 +1165,12 @@ msgctxt "input" msgid "Quotation Number" msgstr "Número de pressupost" -#: pkg/quote.go:164 pkg/expenses.go:501 pkg/invoices.go:167 +#: pkg/quote.go:164 pkg/expenses.go:500 pkg/invoices.go:167 msgctxt "input" msgid "From Date" msgstr "A partir de la data" -#: pkg/quote.go:169 pkg/expenses.go:506 pkg/invoices.go:172 +#: pkg/quote.go:169 pkg/expenses.go:505 pkg/invoices.go:172 msgctxt "input" msgid "To Date" msgstr "Fins la data" @@ -1097,8 +1191,8 @@ msgstr "pressuposts.zip" msgid "quotations.ods" msgstr "pressuposts.ods" -#: pkg/quote.go:634 pkg/quote.go:1176 pkg/quote.go:1184 pkg/expenses.go:720 -#: pkg/expenses.go:750 pkg/invoices.go:684 pkg/invoices.go:1437 +#: pkg/quote.go:634 pkg/quote.go:1176 pkg/quote.go:1184 pkg/expenses.go:719 +#: pkg/expenses.go:749 pkg/invoices.go:684 pkg/invoices.go:1437 #: pkg/invoices.go:1445 msgid "Invalid action" msgstr "Acció invàlida." @@ -1218,6 +1312,101 @@ msgstr "La confirmació no és igual a la contrasenya." msgid "Selected language is not valid." msgstr "Heu seleccionat un idioma que no és vàlid." +#: pkg/payments.go:123 pkg/expenses.go:305 pkg/invoices.go:866 +msgctxt "input" +msgid "Invoice Date" +msgstr "Data de factura" + +#: pkg/payments.go:129 +msgctxt "input" +msgid "Account" +msgstr "Compte" + +#: pkg/payments.go:135 pkg/expenses.go:320 +msgctxt "input" +msgid "Amount" +msgstr "Import" + +#: pkg/payments.go:152 +msgid "Select an account." +msgstr "Escolliu un compte." + +#: pkg/payments.go:198 +msgid "Description can not be empty." +msgstr "No podeu deixar la descripció en blanc." + +#: pkg/payments.go:199 +msgid "Selected payment account is not valid." +msgstr "Heu seleccionat un compte de pagament que no és vàlid." + +#: pkg/payments.go:200 +msgid "Payment date must be a valid date." +msgstr "La data de pagament ha de ser vàlida." + +#: pkg/payments.go:201 pkg/expenses.go:381 +msgid "Amount can not be empty." +msgstr "No podeu deixar l’import en blanc." + +#: pkg/payments.go:202 pkg/expenses.go:382 +msgid "Amount must be a number greater than zero." +msgstr "L’import ha de ser un número major a zero." + +#: pkg/accounts.go:129 +msgctxt "input" +msgid "Type" +msgstr "Tipus" + +#: pkg/accounts.go:144 pkg/contacts.go:352 +msgctxt "input" +msgid "IBAN" +msgstr "IBAN" + +#: pkg/accounts.go:150 +msgctxt "input" +msgid "Card’s last four digits" +msgstr "Els quatre darrers dígits de la targeta" + +#: pkg/accounts.go:161 +msgctxt "input" +msgid "Expiration date" +msgstr "Data de caducitat" + +#: pkg/accounts.go:227 +msgid "Selected payment account type is not valid." +msgstr "Heu seleccionat un tipus de compte de pagament que no és vàlid." + +#: pkg/accounts.go:230 +msgid "IBAN can not be empty." +msgstr "No podeu deixar l’IBAN en blanc." + +#: pkg/accounts.go:231 +msgid "This value is not a valid IBAN." +msgstr "Aquest valor no és un IBAN vàlid." + +#: pkg/accounts.go:234 +msgid "Last four digits can not be empty." +msgstr "No podeu deixar el quatre darrers dígits en blanc." + +#: pkg/accounts.go:235 +msgid "You must enter the card’s last four digits" +msgstr "Heu d’entrar els quatre darrers dígits de la targeta" + +#: pkg/accounts.go:236 +msgid "Last four digits must be a number." +msgstr "El quatre darrera dígits han de ser un número." + +#: pkg/accounts.go:239 +msgid "Expiration date can not be empty." +msgstr "No podeu deixar la data de pagament en blanc." + +#: pkg/accounts.go:241 +msgid "Expiration date should be a valid date in format MM/YY (e.g., 08/24)." +msgstr "La data de caducitat has de ser vàlida i en format MM/AA (p. ex., 08/24)." + +#: pkg/accounts.go:245 +msgid "Payment account name can not be empty." +msgstr "No podeu deixar el nom del compte de pagament en blanc." + #: pkg/dashboard.go:138 msgctxt "input" msgid "Period" @@ -1257,7 +1446,7 @@ msgstr "Any anterior" msgid "Select a contact." msgstr "Escolliu un contacte." -#: pkg/expenses.go:294 pkg/expenses.go:490 +#: pkg/expenses.go:294 pkg/expenses.go:489 msgctxt "input" msgid "Contact" msgstr "Contacte" @@ -1267,22 +1456,12 @@ msgctxt "input" msgid "Invoice number" msgstr "Número de factura" -#: pkg/expenses.go:305 pkg/invoices.go:866 -msgctxt "input" -msgid "Invoice Date" -msgstr "Data de factura" - -#: pkg/expenses.go:320 -msgctxt "input" -msgid "Amount" -msgstr "Import" - #: pkg/expenses.go:331 pkg/invoices.go:888 msgctxt "input" msgid "File" msgstr "Fitxer" -#: pkg/expenses.go:337 pkg/expenses.go:515 +#: pkg/expenses.go:337 pkg/expenses.go:514 msgctxt "input" msgid "Expense Status" msgstr "Estat de la despesa" @@ -1295,28 +1474,20 @@ msgstr "Heu seleccionat un contacte que no és vàlid." msgid "Invoice date must be a valid date." msgstr "La data de facturació ha de ser vàlida." -#: pkg/expenses.go:381 -msgid "Amount can not be empty." -msgstr "No podeu deixar l’import en blanc." - -#: pkg/expenses.go:382 -msgid "Amount must be a number greater than zero." -msgstr "L’import ha de ser un número major a zero." - #: pkg/expenses.go:384 msgid "Selected expense status is not valid." msgstr "Heu seleccionat un estat de despesa que no és vàlid." -#: pkg/expenses.go:491 +#: pkg/expenses.go:490 msgid "All contacts" msgstr "Tots els contactes" -#: pkg/expenses.go:496 pkg/invoices.go:162 +#: pkg/expenses.go:495 pkg/invoices.go:162 msgctxt "input" msgid "Invoice Number" msgstr "Número de factura" -#: pkg/expenses.go:748 +#: pkg/expenses.go:747 msgid "expenses.ods" msgstr "despeses.ods" @@ -1364,11 +1535,6 @@ msgctxt "input" msgid "Need to input tax details" msgstr "Necessito poder facturar aquest contacte" -#: pkg/contacts.go:352 -msgctxt "input" -msgid "IBAN" -msgstr "IBAN" - #: pkg/contacts.go:357 msgctxt "bic" msgid "BIC" @@ -1445,10 +1611,6 @@ msgstr "Fitxer Excel del Holded" #~ msgid "Product ID can not be empty." #~ msgstr "No podeu deixar l’identificador del producte en blanc." -#~ msgctxt "input" -#~ msgid "Number" -#~ msgstr "Número" - #~ msgctxt "title" #~ msgid "Label" #~ msgstr "Etiqueta" diff --git a/po/es.po b/po/es.po index 6aeefd7..eccd060 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: 2024-07-20 22:51+0200\n" +"POT-Creation-Date: 2024-08-10 04:08+0200\n" "PO-Revision-Date: 2023-01-18 17:45+0100\n" "Last-Translator: jordi fita mas \n" "Language-Team: Spanish \n" @@ -35,6 +35,11 @@ msgstr "Añadir productos a la factura" #: web/template/expenses/index.gohtml:10 web/template/expenses/edit.gohtml:10 #: web/template/tax-details.gohtml:9 web/template/products/new.gohtml:9 #: web/template/products/index.gohtml:9 web/template/products/edit.gohtml:10 +#: web/template/payments/new.gohtml:10 web/template/payments/index.gohtml:10 +#: web/template/payments/edit.gohtml:10 +#: web/template/payments/accounts/new.gohtml:10 +#: web/template/payments/accounts/index.gohtml:10 +#: web/template/payments/accounts/edit.gohtml:10 msgctxt "title" msgid "Home" msgstr "Inicio" @@ -61,6 +66,7 @@ msgstr "Todos" #: web/template/invoices/products.gohtml:49 #: web/template/switch-company.gohtml:22 web/template/quotes/products.gohtml:49 #: web/template/products/index.gohtml:45 +#: web/template/payments/accounts/index.gohtml:25 msgctxt "title" msgid "Name" msgstr "Nombre" @@ -107,7 +113,7 @@ msgstr "Subtotal" #: web/template/quotes/new.gohtml:74 web/template/quotes/view.gohtml:82 #: web/template/quotes/view.gohtml:122 web/template/quotes/edit.gohtml:75 #: web/template/expenses/new.gohtml:47 web/template/expenses/index.gohtml:74 -#: web/template/expenses/edit.gohtml:49 +#: web/template/expenses/edit.gohtml:49 web/template/payments/index.gohtml:29 msgctxt "title" msgid "Total" msgstr "Total" @@ -115,6 +121,8 @@ msgstr "Total" #: web/template/invoices/new.gohtml:92 web/template/invoices/edit.gohtml:93 #: web/template/quotes/new.gohtml:92 web/template/quotes/edit.gohtml:93 #: web/template/expenses/new.gohtml:57 web/template/expenses/edit.gohtml:59 +#: web/template/payments/edit.gohtml:35 +#: web/template/payments/accounts/edit.gohtml:38 msgctxt "action" msgid "Update" msgstr "Actualizar" @@ -124,6 +132,8 @@ msgstr "Actualizar" #: web/template/contacts/new.gohtml:49 web/template/contacts/edit.gohtml:53 #: web/template/expenses/new.gohtml:60 web/template/expenses/edit.gohtml:62 #: web/template/products/new.gohtml:30 web/template/products/edit.gohtml:36 +#: web/template/payments/new.gohtml:33 +#: web/template/payments/accounts/new.gohtml:41 msgctxt "action" msgid "Save" msgstr "Guardad" @@ -180,14 +190,14 @@ msgid "Customer" msgstr "Cliente" #: web/template/invoices/index.gohtml:70 web/template/quotes/index.gohtml:70 -#: web/template/expenses/index.gohtml:68 +#: web/template/expenses/index.gohtml:68 web/template/payments/index.gohtml:27 msgctxt "title" msgid "Status" msgstr "Estado" #: web/template/invoices/index.gohtml:71 web/template/quotes/index.gohtml:71 #: web/template/contacts/index.gohtml:50 web/template/expenses/index.gohtml:69 -#: web/template/products/index.gohtml:46 +#: web/template/products/index.gohtml:46 web/template/payments/index.gohtml:28 msgctxt "title" msgid "Tags" msgstr "Etiquetes" @@ -326,7 +336,7 @@ msgctxt "input" msgid "(Max. %s)" msgstr "(Máx. %s)" -#: web/template/form.gohtml:200 +#: web/template/form.gohtml:202 msgctxt "action" msgid "Filters" msgstr "Filtrar" @@ -489,15 +499,20 @@ msgstr "Gastos" #: web/template/app.gohtml:57 msgctxt "nav" +msgid "Payments" +msgstr "Pagos" + +#: web/template/app.gohtml:58 +msgctxt "nav" msgid "Products" msgstr "Productos" -#: web/template/app.gohtml:58 +#: web/template/app.gohtml:59 msgctxt "nav" msgid "Contacts" msgstr "Contactos" -#: web/template/app.gohtml:66 +#: web/template/app.gohtml:67 msgid "Numerus Version: %s" msgstr "Numerus versión: %s" @@ -731,6 +746,84 @@ msgctxt "title" msgid "Edit Product “%s”" msgstr "Edición del producto «%s»" +#: web/template/payments/new.gohtml:3 web/template/payments/new.gohtml:12 +msgctxt "title" +msgid "New Payment" +msgstr "Nuevo pago" + +#: web/template/payments/new.gohtml:11 web/template/payments/index.gohtml:3 +#: web/template/payments/index.gohtml:11 web/template/payments/edit.gohtml:11 +msgctxt "title" +msgid "Payments" +msgstr "Pagos" + +#: web/template/payments/index.gohtml:16 +msgctxt "action" +msgid "New payment" +msgstr "Nuevo pago" + +#: web/template/payments/index.gohtml:25 +msgctxt "title" +msgid "Payment Date" +msgstr "Fecha del pago" + +#: web/template/payments/index.gohtml:26 +msgctxt "title" +msgid "Description" +msgstr "Descripción" + +#: web/template/payments/index.gohtml:54 +msgid "No payments added yet." +msgstr "No hay pagos." + +#: web/template/payments/edit.gohtml:3 +msgctxt "title" +msgid "Edit Payment “%s”" +msgstr "Edición del pago «%s»" + +#: web/template/payments/accounts/new.gohtml:3 +#: web/template/payments/accounts/new.gohtml:12 +msgctxt "title" +msgid "New Payment Account" +msgstr "Nueva cuenta de pago" + +#: web/template/payments/accounts/new.gohtml:11 +#: web/template/payments/accounts/index.gohtml:3 +#: web/template/payments/accounts/index.gohtml:11 +#: web/template/payments/accounts/edit.gohtml:11 +msgctxt "title" +msgid "Payment Accounts" +msgstr "Cuenta de pago" + +#: web/template/payments/accounts/index.gohtml:16 +msgctxt "action" +msgid "New payment account" +msgstr "Nuevo cuenta de pago" + +#: web/template/payments/accounts/index.gohtml:26 +msgctxt "title" +msgid "Type" +msgstr "Tipo" + +#: web/template/payments/accounts/index.gohtml:27 +msgctxt "title" +msgid "Number" +msgstr "Número" + +#: web/template/payments/accounts/index.gohtml:28 +msgctxt "title" +msgid "Expiration Date" +msgstr "Fecha de caducidad" + +#: web/template/payments/accounts/index.gohtml:51 +msgid "No payment accounts added yet." +msgstr "No hay cuentas de pago." + +#: web/template/payments/accounts/edit.gohtml:3 +msgctxt "title" +msgid "Edit Payment Account “%s”" +msgstr "Edición de la cuenta de pago «%s»" + #: pkg/ods.go:62 pkg/ods.go:106 msgctxt "title" msgid "VAT number" @@ -762,49 +855,50 @@ msgstr "No podéis dejar la contraseña en blanco." msgid "Invalid user or password." msgstr "Nombre de usuario o contraseña inválido." -#: pkg/products.go:172 pkg/products.go:276 pkg/quote.go:901 +#: pkg/products.go:172 pkg/products.go:276 pkg/quote.go:901 pkg/accounts.go:138 #: pkg/invoices.go:1147 pkg/contacts.go:149 pkg/contacts.go:262 msgctxt "input" msgid "Name" msgstr "Nombre" #: pkg/products.go:177 pkg/products.go:303 pkg/quote.go:174 pkg/quote.go:708 -#: pkg/expenses.go:343 pkg/expenses.go:511 pkg/invoices.go:177 -#: pkg/invoices.go:877 pkg/invoices.go:1462 pkg/contacts.go:154 -#: pkg/contacts.go:362 +#: pkg/payments.go:145 pkg/expenses.go:343 pkg/expenses.go:510 +#: pkg/invoices.go:177 pkg/invoices.go:877 pkg/invoices.go:1462 +#: pkg/contacts.go:154 pkg/contacts.go:362 msgctxt "input" msgid "Tags" msgstr "Etiquetes" -#: pkg/products.go:181 pkg/quote.go:178 pkg/expenses.go:521 pkg/invoices.go:181 +#: pkg/products.go:181 pkg/quote.go:178 pkg/expenses.go:520 pkg/invoices.go:181 #: pkg/contacts.go:158 msgctxt "input" msgid "Tags Condition" msgstr "Condición de las etiquetas" -#: pkg/products.go:185 pkg/quote.go:182 pkg/expenses.go:525 pkg/invoices.go:185 +#: pkg/products.go:185 pkg/quote.go:182 pkg/expenses.go:524 pkg/invoices.go:185 #: pkg/contacts.go:162 msgctxt "tag condition" msgid "All" msgstr "Todas" -#: pkg/products.go:186 pkg/expenses.go:526 pkg/invoices.go:186 +#: pkg/products.go:186 pkg/expenses.go:525 pkg/invoices.go:186 #: pkg/contacts.go:163 msgid "Invoices must have all the specified labels." msgstr "Las facturas deben tener todas las etiquetas." -#: pkg/products.go:190 pkg/quote.go:187 pkg/expenses.go:530 pkg/invoices.go:190 +#: pkg/products.go:190 pkg/quote.go:187 pkg/expenses.go:529 pkg/invoices.go:190 #: pkg/contacts.go:167 msgctxt "tag condition" msgid "Any" msgstr "Cualquiera" -#: pkg/products.go:191 pkg/expenses.go:531 pkg/invoices.go:191 +#: pkg/products.go:191 pkg/expenses.go:530 pkg/invoices.go:191 #: pkg/contacts.go:168 msgid "Invoices must have at least one of the specified labels." msgstr "Las facturas deben tener como mínimo una de las etiquetas." -#: pkg/products.go:282 pkg/quote.go:915 pkg/invoices.go:1161 +#: pkg/products.go:282 pkg/quote.go:915 pkg/payments.go:117 +#: pkg/invoices.go:1161 msgctxt "input" msgid "Description" msgstr "Descripción" @@ -1062,7 +1156,7 @@ msgctxt "input" msgid "Quotation Status" msgstr "Estado del presupuesto" -#: pkg/quote.go:154 pkg/expenses.go:516 pkg/invoices.go:157 +#: pkg/quote.go:154 pkg/expenses.go:515 pkg/invoices.go:157 msgid "All status" msgstr "Todos los estados" @@ -1071,12 +1165,12 @@ msgctxt "input" msgid "Quotation Number" msgstr "Número de presupuesto" -#: pkg/quote.go:164 pkg/expenses.go:501 pkg/invoices.go:167 +#: pkg/quote.go:164 pkg/expenses.go:500 pkg/invoices.go:167 msgctxt "input" msgid "From Date" msgstr "A partir de la fecha" -#: pkg/quote.go:169 pkg/expenses.go:506 pkg/invoices.go:172 +#: pkg/quote.go:169 pkg/expenses.go:505 pkg/invoices.go:172 msgctxt "input" msgid "To Date" msgstr "Hasta la fecha" @@ -1097,8 +1191,8 @@ msgstr "presupuestos.zip" msgid "quotations.ods" msgstr "presupuestos.ods" -#: pkg/quote.go:634 pkg/quote.go:1176 pkg/quote.go:1184 pkg/expenses.go:720 -#: pkg/expenses.go:750 pkg/invoices.go:684 pkg/invoices.go:1437 +#: pkg/quote.go:634 pkg/quote.go:1176 pkg/quote.go:1184 pkg/expenses.go:719 +#: pkg/expenses.go:749 pkg/invoices.go:684 pkg/invoices.go:1437 #: pkg/invoices.go:1445 msgid "Invalid action" msgstr "Acción inválida." @@ -1218,6 +1312,101 @@ msgstr "La confirmación no corresponde con la contraseña." msgid "Selected language is not valid." msgstr "Habéis escogido un idioma que no es válido." +#: pkg/payments.go:123 pkg/expenses.go:305 pkg/invoices.go:866 +msgctxt "input" +msgid "Invoice Date" +msgstr "Fecha de factura" + +#: pkg/payments.go:129 +msgctxt "input" +msgid "Account" +msgstr "Cuenta" + +#: pkg/payments.go:135 pkg/expenses.go:320 +msgctxt "input" +msgid "Amount" +msgstr "Importe" + +#: pkg/payments.go:152 +msgid "Select an account." +msgstr "Escoged una cuenta." + +#: pkg/payments.go:198 +msgid "Description can not be empty." +msgstr "No podéis dejar la descripción en blanco." + +#: pkg/payments.go:199 +msgid "Selected payment account is not valid." +msgstr "Habéis escogido una cuenta de pago que no es válida." + +#: pkg/payments.go:200 +msgid "Payment date must be a valid date." +msgstr "La fecha de pago debe ser válida." + +#: pkg/payments.go:201 pkg/expenses.go:381 +msgid "Amount can not be empty." +msgstr "No podéis dejar el importe en blanco." + +#: pkg/payments.go:202 pkg/expenses.go:382 +msgid "Amount must be a number greater than zero." +msgstr "El importe tiene que ser un número mayor a cero." + +#: pkg/accounts.go:129 +msgctxt "input" +msgid "Type" +msgstr "Tipo" + +#: pkg/accounts.go:144 pkg/contacts.go:352 +msgctxt "input" +msgid "IBAN" +msgstr "IBAN" + +#: pkg/accounts.go:150 +msgctxt "input" +msgid "Card’s last four digits" +msgstr "Últimos cuatro dígitos de la tarjeta" + +#: pkg/accounts.go:161 +msgctxt "input" +msgid "Expiration date" +msgstr "Fecha de caducidad" + +#: pkg/accounts.go:227 +msgid "Selected payment account type is not valid." +msgstr "Habéis escogido un tipo de cuenta de pago que no es válido." + +#: pkg/accounts.go:230 +msgid "IBAN can not be empty." +msgstr "No podéis dejar el IBAN en blanco." + +#: pkg/accounts.go:231 +msgid "This value is not a valid IBAN." +msgstr "Este valor no es un IBAN válido." + +#: pkg/accounts.go:234 +msgid "Last four digits can not be empty." +msgstr "No podéis dejar los cuatro últimos dígitos en blanco." + +#: pkg/accounts.go:235 +msgid "You must enter the card’s last four digits" +msgstr "Debéis entrar los cuatro últimos dígitos de la tarjeta" + +#: pkg/accounts.go:236 +msgid "Last four digits must be a number." +msgstr "Los cuatro últimos dígitos tienen que ser un número." + +#: pkg/accounts.go:239 +msgid "Expiration date can not be empty." +msgstr "No podéis dejar la fecha de caducidad en blanco." + +#: pkg/accounts.go:241 +msgid "Expiration date should be a valid date in format MM/YY (e.g., 08/24)." +msgstr "La fecha de caducidad tiene que ser válida y en formato MM/AA (p. ej., 08/24)." + +#: pkg/accounts.go:245 +msgid "Payment account name can not be empty." +msgstr "No podéis dejar el nombre de la cuenta de pago en blanco." + #: pkg/dashboard.go:138 msgctxt "input" msgid "Period" @@ -1257,7 +1446,7 @@ msgstr "Año anterior" msgid "Select a contact." msgstr "Escoged un contacto" -#: pkg/expenses.go:294 pkg/expenses.go:490 +#: pkg/expenses.go:294 pkg/expenses.go:489 msgctxt "input" msgid "Contact" msgstr "Contacto" @@ -1267,22 +1456,12 @@ msgctxt "input" msgid "Invoice number" msgstr "Número de factura" -#: pkg/expenses.go:305 pkg/invoices.go:866 -msgctxt "input" -msgid "Invoice Date" -msgstr "Fecha de factura" - -#: pkg/expenses.go:320 -msgctxt "input" -msgid "Amount" -msgstr "Importe" - #: pkg/expenses.go:331 pkg/invoices.go:888 msgctxt "input" msgid "File" msgstr "Archivo" -#: pkg/expenses.go:337 pkg/expenses.go:515 +#: pkg/expenses.go:337 pkg/expenses.go:514 msgctxt "input" msgid "Expense Status" msgstr "Estado del gasto" @@ -1295,28 +1474,20 @@ msgstr "Habéis escogido un contacto que no es válido." msgid "Invoice date must be a valid date." msgstr "La fecha de factura debe ser válida." -#: pkg/expenses.go:381 -msgid "Amount can not be empty." -msgstr "No podéis dejar el importe en blanco." - -#: pkg/expenses.go:382 -msgid "Amount must be a number greater than zero." -msgstr "El importe tiene que ser un número mayor a cero." - #: pkg/expenses.go:384 msgid "Selected expense status is not valid." msgstr "Habéis escogido un estado de gasto que no es válido." -#: pkg/expenses.go:491 +#: pkg/expenses.go:490 msgid "All contacts" msgstr "Todos los contactos" -#: pkg/expenses.go:496 pkg/invoices.go:162 +#: pkg/expenses.go:495 pkg/invoices.go:162 msgctxt "input" msgid "Invoice Number" msgstr "Número de factura" -#: pkg/expenses.go:748 +#: pkg/expenses.go:747 msgid "expenses.ods" msgstr "gastos.ods" @@ -1364,11 +1535,6 @@ msgctxt "input" msgid "Need to input tax details" msgstr "Necesito facturar este contacto" -#: pkg/contacts.go:352 -msgctxt "input" -msgid "IBAN" -msgstr "IBAN" - #: pkg/contacts.go:357 msgctxt "bic" msgid "BIC" @@ -1441,10 +1607,6 @@ msgstr "Archivo Excel de Holded" #~ msgid "Product ID can not be empty." #~ msgstr "No podéis dejar el identificador de producto en blanco." -#~ msgctxt "input" -#~ msgid "Number" -#~ msgstr "Número" - #~ msgctxt "title" #~ msgid "Label" #~ msgstr "Etiqueta" diff --git a/revert/add_payment.sql b/revert/add_payment.sql new file mode 100644 index 0000000..685198f --- /dev/null +++ b/revert/add_payment.sql @@ -0,0 +1,7 @@ +-- Revert numerus:add_payment from pg + +begin; + +drop function if exists numerus.add_payment(integer, integer, date, integer, text, text, numerus.tag_name[]); + +commit; diff --git a/revert/add_payment_account_bank.sql b/revert/add_payment_account_bank.sql new file mode 100644 index 0000000..f47eaa1 --- /dev/null +++ b/revert/add_payment_account_bank.sql @@ -0,0 +1,7 @@ +-- Revert numerus:add_payment_account_bank from pg + +begin; + +drop function if exists numerus.add_payment_account_bank(integer, text, iban); + +commit; diff --git a/revert/add_payment_account_card.sql b/revert/add_payment_account_card.sql new file mode 100644 index 0000000..9ed7b0f --- /dev/null +++ b/revert/add_payment_account_card.sql @@ -0,0 +1,7 @@ +-- Revert numerus:add_payment_account_card from pg + +begin; + +drop function if exists numerus.add_payment_account_card(integer, text, text, date); + +commit; diff --git a/revert/add_payment_account_cash.sql b/revert/add_payment_account_cash.sql new file mode 100644 index 0000000..4294ed8 --- /dev/null +++ b/revert/add_payment_account_cash.sql @@ -0,0 +1,7 @@ +-- Revert numerus:add_payment_account_cash from pg + +begin; + +drop function if exists numerus.add_payment_account_cash(integer, text); + +commit; diff --git a/revert/add_payment_account_other.sql b/revert/add_payment_account_other.sql new file mode 100644 index 0000000..17cbbbe --- /dev/null +++ b/revert/add_payment_account_other.sql @@ -0,0 +1,7 @@ +-- Revert numerus:add_payment_account_other from pg + +begin; + +drop function if exists numerus.add_payment_account_other(integer, text); + +commit; diff --git a/revert/available_expense_status.sql b/revert/available_expense_status.sql index dbd531b..36abedb 100644 --- a/revert/available_expense_status.sql +++ b/revert/available_expense_status.sql @@ -1,10 +1,13 @@ --- Revert numerus:available_expense_status from pg +-- Deploy numerus:available_expense_status to pg +-- requires: schema_numerus +-- requires: expense_status +-- requires: expense_status_i18n begin; set search_path to numerus; -delete from expense_status_i18n; -delete from expense_status; +delete from expense_status_i18n where expense_status = 'partial'; +delete from expense_status where expense_status = 'partial'; commit; diff --git a/revert/available_expense_status@v2.sql b/revert/available_expense_status@v2.sql new file mode 100644 index 0000000..dbd531b --- /dev/null +++ b/revert/available_expense_status@v2.sql @@ -0,0 +1,10 @@ +-- Revert numerus:available_expense_status from pg + +begin; + +set search_path to numerus; + +delete from expense_status_i18n; +delete from expense_status; + +commit; diff --git a/revert/available_payment_account_types.sql b/revert/available_payment_account_types.sql new file mode 100644 index 0000000..8dbf5e4 --- /dev/null +++ b/revert/available_payment_account_types.sql @@ -0,0 +1,10 @@ +-- Revert numerus:available_payment_account_types from pg + +begin; + +set search_path to numerus, public; + +delete from payment_account_type_i18n; +delete from payment_account_type; + +commit; diff --git a/revert/available_payment_status.sql b/revert/available_payment_status.sql new file mode 100644 index 0000000..7087c95 --- /dev/null +++ b/revert/available_payment_status.sql @@ -0,0 +1,10 @@ +-- Revert numerus:available_payment_status from pg + +begin; + +set search_path to numerus; + +delete from payment_status_i18n; +delete from payment_status; + +commit; diff --git a/revert/edit_payment.sql b/revert/edit_payment.sql new file mode 100644 index 0000000..93e9ae5 --- /dev/null +++ b/revert/edit_payment.sql @@ -0,0 +1,7 @@ +-- Revert numerus:edit_payment from pg + +begin; + +drop function if exists numerus.edit_payment(uuid, date, integer, text, text, numerus.tag_name[]); + +commit; diff --git a/revert/edit_payment_account_bank.sql b/revert/edit_payment_account_bank.sql new file mode 100644 index 0000000..d98e7f9 --- /dev/null +++ b/revert/edit_payment_account_bank.sql @@ -0,0 +1,7 @@ +-- Revert numerus:edit_payment_account_bank from pg + +begin; + +drop function if exists numerus.edit_payment_account_bank(uuid, text, iban); + +commit; diff --git a/revert/edit_payment_account_card.sql b/revert/edit_payment_account_card.sql new file mode 100644 index 0000000..64a4ca6 --- /dev/null +++ b/revert/edit_payment_account_card.sql @@ -0,0 +1,7 @@ +-- Revert numerus:edit_payment_account_card from pg + +begin; + +drop function if exists numerus.edit_payment_account_card(uuid, text, text, date); + +commit; diff --git a/revert/edit_payment_account_cash.sql b/revert/edit_payment_account_cash.sql new file mode 100644 index 0000000..150ef19 --- /dev/null +++ b/revert/edit_payment_account_cash.sql @@ -0,0 +1,7 @@ +-- Revert numerus:edit_payment_account_cash from pg + +begin; + +drop function if exists numerus.edit_payment_account_cash(uuid, text); + +commit; diff --git a/revert/edit_payment_account_other.sql b/revert/edit_payment_account_other.sql new file mode 100644 index 0000000..a53e0d8 --- /dev/null +++ b/revert/edit_payment_account_other.sql @@ -0,0 +1,7 @@ +-- Revert numerus:edit_payment_account_other from pg + +begin; + +drop function if exists numerus.edit_payment_account_other(uuid, text); + +commit; diff --git a/revert/expense_payment.sql b/revert/expense_payment.sql new file mode 100644 index 0000000..04eb360 --- /dev/null +++ b/revert/expense_payment.sql @@ -0,0 +1,7 @@ +-- Revert numerus:expense_payment from pg + +begin; + +drop table if exists numerus.expense_payment; + +commit; diff --git a/revert/payment.sql b/revert/payment.sql new file mode 100644 index 0000000..1d1ca9d --- /dev/null +++ b/revert/payment.sql @@ -0,0 +1,7 @@ +-- Revert numerus:payment from pg + +begin; + +drop table if exists numerus.payment; + +commit; diff --git a/revert/payment_account.sql b/revert/payment_account.sql new file mode 100644 index 0000000..51edc81 --- /dev/null +++ b/revert/payment_account.sql @@ -0,0 +1,7 @@ +-- Revert numerus:payment_account from pg + +begin; + +drop table if exists numerus.payment_account; + +commit; diff --git a/revert/payment_account_bank.sql b/revert/payment_account_bank.sql new file mode 100644 index 0000000..2dea774 --- /dev/null +++ b/revert/payment_account_bank.sql @@ -0,0 +1,7 @@ +-- Revert numerus:payment_account_bank from pg + +begin; + +drop table if exists numerus.payment_account_bank; + +commit; diff --git a/revert/payment_account_card.sql b/revert/payment_account_card.sql new file mode 100644 index 0000000..2b164e5 --- /dev/null +++ b/revert/payment_account_card.sql @@ -0,0 +1,7 @@ +-- Revert numerus:payment_account_card from pg + +begin; + +drop table if exists numerus.payment_account_card; + +commit; diff --git a/revert/payment_account_type.sql b/revert/payment_account_type.sql new file mode 100644 index 0000000..db60585 --- /dev/null +++ b/revert/payment_account_type.sql @@ -0,0 +1,7 @@ +-- Revert numerus:payment_account_type from pg + +begin; + +drop table if exists numerus.payment_account_type; + +commit; diff --git a/revert/payment_account_type_i18n.sql b/revert/payment_account_type_i18n.sql new file mode 100644 index 0000000..f666457 --- /dev/null +++ b/revert/payment_account_type_i18n.sql @@ -0,0 +1,7 @@ +-- Revert numerus:payment_account_type_i18n from pg + +begin; + +drop table if exists numerus.payment_account_type_i18n; + +commit; diff --git a/revert/payment_status.sql b/revert/payment_status.sql new file mode 100644 index 0000000..b663598 --- /dev/null +++ b/revert/payment_status.sql @@ -0,0 +1,7 @@ +-- Revert numerus:payment_status from pg + +begin; + +drop table if exists numerus.payment_status; + +commit; diff --git a/revert/payment_status_i18n.sql b/revert/payment_status_i18n.sql new file mode 100644 index 0000000..1bbbd31 --- /dev/null +++ b/revert/payment_status_i18n.sql @@ -0,0 +1,7 @@ +-- Revert numerus:payment_status_i18n from pg + +begin; + +drop table if exists numerus.payment_status_i18n; + +commit; diff --git a/revert/update_expense_payment_status.sql b/revert/update_expense_payment_status.sql new file mode 100644 index 0000000..381cf35 --- /dev/null +++ b/revert/update_expense_payment_status.sql @@ -0,0 +1,7 @@ +-- Revert numerus:update_expense_payment_status from pg + +begin; + +drop function if exists numerus.update_expense_payment_status(integer, integer, integer); + +commit; diff --git a/sqitch.plan b/sqitch.plan index bea157e..3010bed 100644 --- a/sqitch.plan +++ b/sqitch.plan @@ -127,3 +127,28 @@ attach_to_invoice [schema_numerus roles invoice invoice_attachment] 2023-07-12T1 new_expense_amount [schema_numerus] 2023-07-13T17:45:33Z jordi fita mas # Add type to return when computing new expense amounts compute_new_expense_amount [schema_numerus roles company tax new_expense_amount] 2023-07-13T17:34:12Z jordi fita mas # Add function to compute the taxes and total for a new expense parse_price [parse_price@v1] 2023-08-25T11:59:54Z jordi fita mas # Throw when subtotal is empty string +@v2 2024-08-04T05:21:18Z jordi fita mas # Tag version 2 + +payment_account_type [roles schema_numerus] 2024-08-01T23:00:18Z jordi fita mas # Add payment_account_type enum +payment_account_type_i18n [roles schema_numerus payment_account_type language] 2024-08-08T17:05:28Z jordi fita mas # Add relation for payment account type’s translations +available_payment_account_types [schema_numerus payment_account_type payment_account_type_i18n] 2024-08-08T17:09:24Z jordi fita mas # Add the list of available payment account types +payment_account [roles schema_numerus company payment_account_type extension_pgcrypto] 2024-08-01T23:11:54Z jordi fita mas # Add relation of payment account +payment_account_bank [roles schema_numerus payment_account extension_iban] 2024-08-01T23:40:34Z jordi fita mas # Add relation for payment account bank +payment_account_card [roles schema_numerus payment_account] 2024-08-02T00:10:54Z jordi fita mas # Add relation for payment account card +add_payment_account_bank [roles schema_numerus payment_account payment_account_bank] 2024-08-03T00:24:18Z jordi fita mas # Add function to insert bank payment accounts +edit_payment_account_bank [roles schema_numerus payment_account payment_account_bank extension_pgcrypto extension_iban] 2024-08-10T00:04:03Z jordi fita mas # Add function to update bank payment accounts +add_payment_account_card [roles schema_numerus payment_account payment_account_card] 2024-08-03T00:41:18Z jordi fita mas # Add function to insert card payment accounts +edit_payment_account_card [roles schema_numerus payment_account payment_account_card extension_pgcrypto] 2024-08-10T00:22:37Z jordi fita mas # Add function to update card payment accounts +add_payment_account_cash [roles schema_numerus payment_account] 2024-08-03T00:49:07Z jordi fita mas # Add function to insert cash payment accounts +edit_payment_account_cash [roles schema_numerus payment_account extension_pgcrypto] 2024-08-10T00:34:19Z jordi fita mas # Add function to update cash payment accounts +add_payment_account_other [roles schema_numerus payment_account] 2024-08-03T00:49:15Z jordi fita mas # Add function to insert other payment accounts +edit_payment_account_other [roles schema_numerus payment_account extension_pgcrypto] 2024-08-10T00:41:38Z jordi fita mas # Add function to update other payment accounts +payment_status [roles schema_numerus] 2024-08-04T03:02:06Z jordi fita mas # Add relation of payment status +payment_status_i18n [roles schema_numerus payment_status language] 2024-08-04T03:05:41Z jordi fita mas # Add relation of payment status’ translated name +available_payment_status [schema_numerus payment_status payment_status_i18n] 2024-08-04T03:08:42Z jordi fita mas # Add the list of available payment status +payment [roles schema_numerus company payment_account currency tag_name payment_status extension_pgcrypto] 2024-08-01T01:28:59Z jordi fita mas # Add relation for accounts payable +expense_payment [roles schema_numerus expense payment] 2024-08-04T03:44:30Z jordi fita mas # Add relation of expense payments +available_expense_status [available_expense_status@v2] 2024-08-04T05:24:08Z jordi fita mas # Add “partial” expense status +update_expense_payment_status [roles schema_numerus expense payment expense_payment available_expense_status available_payment_status] 2024-08-04T06:36:00Z jordi fita mas # Add function to update payment and expense status +add_payment [roles schema_numerus payment expense_payment company currency parse_price tag_name update_expense_payment_status] 2024-08-04T03:16:55Z jordi fita mas # Add function to insert new payments +edit_payment [roles schema_numerus payment expense_payment currency parse_price tag_name update_expense_payment_status] 2024-08-04T03:31:45Z jordi fita mas # Add function to update payments diff --git a/test/add_payment.sql b/test/add_payment.sql new file mode 100644 index 0000000..6575b5a --- /dev/null +++ b/test/add_payment.sql @@ -0,0 +1,125 @@ +-- Test add_payment +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(17); + +set search_path to numerus, auth, public; + +select has_function('numerus', 'add_payment', array['integer', 'integer', 'date', 'integer', 'text', 'text', 'tag_name[]']); +select function_lang_is('numerus', 'add_payment', array['integer', 'integer', 'date', 'integer', 'text', 'text', 'tag_name[]'], 'plpgsql'); +select function_returns('numerus', 'add_payment', array['integer', 'integer', 'date', 'integer', 'text', 'text', 'tag_name[]'], 'uuid'); +select isnt_definer('numerus', 'add_payment', array['integer', 'integer', 'date', 'integer', 'text', 'text', 'tag_name[]']); +select volatility_is('numerus', 'add_payment', array['integer', 'integer', 'date', 'integer', 'text', 'text', 'tag_name[]'], 'volatile'); + +select function_privs_are('numerus', 'add_payment', array ['integer', 'integer', 'date', 'integer', 'text', 'text', 'tag_name[]'], 'guest', array []::text[]); +select function_privs_are('numerus', 'add_payment', array ['integer', 'integer', 'date', 'integer', 'text', 'text', 'tag_name[]'], 'invoicer', array ['EXECUTE']); +select function_privs_are('numerus', 'add_payment', array ['integer', 'integer', 'date', 'integer', 'text', 'text', 'tag_name[]'], 'admin', array ['EXECUTE']); +select function_privs_are('numerus', 'add_payment', array ['integer', 'integer', 'date', 'integer', 'text', 'text', 'tag_name[]'], 'authenticator', array []::text[]); + + +set client_min_messages to warning; +truncate expense_payment cascade; +truncate payment cascade; +truncate payment_account cascade; +truncate expense cascade; +truncate contact cascade; +truncate payment_method cascade; +truncate company cascade; +reset client_min_messages; + +set constraints "company_default_payment_method_id_fkey" deferred; + +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 (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 111) + , (2, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', 'FR', 'USD', 222) +; + +insert into payment_method (payment_method_id, company_id, name, instructions) +values (111, 1, 'cash', 'cash') + , (222, 2, 'cash', 'cash') +; + +set constraints "company_default_payment_method_id_fkey" immediate; + +insert into contact (contact_id, company_id, name) +values ( 9, 1, 'Customer 1') + , (10, 2, 'Customer 2') +; + +insert into expense (expense_id, company_id, invoice_number, contact_id, invoice_date, amount, currency_code) +values (12, 1, 'REF123', 9, '2011-01-11', 111, 'EUR') + , (13, 2, 'INV001', 10, '2011-01-11', 111, 'USD') + , (14, 2, 'INV002', 10, '2022-02-22', 222, 'USD') + , (15, 2, 'INV003', 10, '2022-02-22', 222, 'USD') +; + +insert into payment_account (payment_account_id, company_id, payment_account_type, name) +values (11, 1, 'other', 'Other 1') + , (22, 2, 'cash', 'Cash 2') +; + +select lives_ok( + $$ select add_payment(1, null, '2023-05-02', 11, '“Protection”', '11.11', array['tag1', 'tag2']) $$, + 'Should be able to insert a payment, unrelated to any expense, for the first company' +); + +select lives_ok( + $$ select add_payment(2, 13, '2023-05-03', 22, 'Payment of INV001', '1.11', array[]::tag_name[]) $$, + 'Should be able to insert a complete payment for the first expense' +); + +select lives_ok( + $$ select add_payment(2, 14, '2023-05-04', 22, 'First payment of INV002', '1.00', array[]::tag_name[]) $$, + 'Should be able to insert a partial payment for the second expense' +); + +select lives_ok( + $$ select add_payment(2, 14, '2023-05-05', 22, 'Second payment of INV002', '1.22', array[]::tag_name[]) $$, + 'Should be able to insert a partial, and final, payment for the second expense' +); + +select lives_ok( + $$ select add_payment(2, 15, '2023-05-06', 22, 'Partial payment of INV003', '1.11', array[]::tag_name[]) $$, + 'Should be able to insert a partial payment for the third expense' +); + +select bag_eq( + $$ select company_id, description, payment_date::text, payment_account_id, amount, currency_code, payment_status, tags::text, created_at from payment $$, + $$ values (1, '“Protection”', '2023-05-02', 11, 1111, 'EUR', 'complete', '{tag1,tag2}', current_timestamp) + , (2, 'Payment of INV001', '2023-05-03', 22, 111, 'USD', 'complete', '{}', current_timestamp) + , (2, 'First payment of INV002', '2023-05-04', 22, 100, 'USD', 'partial', '{}', current_timestamp) + , (2, 'Second payment of INV002', '2023-05-05', 22, 122, 'USD', 'partial', '{}', current_timestamp) + , (2, 'Partial payment of INV003', '2023-05-06', 22, 111, 'USD', 'partial', '{}', current_timestamp) + $$, + 'Should have created all payments' +); + +select bag_eq( + $$ select expense_id, description from expense_payment join payment using (payment_id) $$, + $$ values (13, 'Payment of INV001') + , (14, 'First payment of INV002') + , (14, 'Second payment of INV002') + , (15, 'Partial payment of INV003') + $$, + 'Should have linked all expenses to payments' +); + +select bag_eq( + $$ select expense_id, expense_status from expense $$, + $$ values (12, 'pending') + , (13, 'paid') + , (14, 'paid') + , (15, 'partial') + $$, + 'Should have updated the status of expenses' +); + + +select * +from finish(); + +rollback; diff --git a/test/add_payment_account_bank.sql b/test/add_payment_account_bank.sql new file mode 100644 index 0000000..3f3c741 --- /dev/null +++ b/test/add_payment_account_bank.sql @@ -0,0 +1,67 @@ +-- Test add_payment_account_bank +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(12); + +set search_path to numerus, auth, public; + +select has_function('numerus', 'add_payment_account_bank', array['integer', 'text', 'iban']); +select function_lang_is('numerus', 'add_payment_account_bank', array['integer', 'text', 'iban'], 'plpgsql'); +select function_returns('numerus', 'add_payment_account_bank', array['integer', 'text', 'iban'], 'uuid'); +select isnt_definer('numerus', 'add_payment_account_bank', array['integer', 'text', 'iban']); +select volatility_is('numerus', 'add_payment_account_bank', array['integer', 'text', 'iban'], 'volatile'); + +select function_privs_are('numerus', 'add_payment_account_bank', array['integer', 'text', 'iban'], 'guest', array []::text[]); +select function_privs_are('numerus', 'add_payment_account_bank', array['integer', 'text', 'iban'], 'invoicer', array ['EXECUTE']); +select function_privs_are('numerus', 'add_payment_account_bank', array['integer', 'text', 'iban'], 'admin', array ['EXECUTE']); +select function_privs_are('numerus', 'add_payment_account_bank', array['integer', 'text', 'iban'], 'authenticator', array []::text[]); + + +set client_min_messages to warning; +truncate payment_account_bank cascade; +truncate payment_account cascade; +truncate payment_method cascade; +truncate company cascade; +reset client_min_messages; + +set constraints "company_default_payment_method_id_fkey" deferred; + +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 (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 111) + , (2, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', 'FR', 'USD', 222) +; + +insert into payment_method (payment_method_id, company_id, name, instructions) +values (111, 1, 'cash', 'cash') + , (222, 2, 'cash', 'cash') +; + +set constraints "company_default_payment_method_id_fkey" immediate; + +select lives_ok( + $$ select add_payment_account_bank(1, 'Bank A', 'ES2820958297603648596978') $$, + 'Should be able to insert a first bank payment account' +); + +select lives_ok ( + $$ select add_payment_account_bank(1, 'Bank 2', 'DE68500105178297336485') $$, + 'Should be able to insert a second bank payment account' +); + +select bag_eq( + $$ select company_id, payment_account_type::text, name, iban::text from payment_account join payment_account_bank using (payment_account_id, payment_account_type) $$, + $$ values (1, 'bank', 'Bank A', 'ES2820958297603648596978') + , (1, 'bank', 'Bank 2', 'DE68500105178297336485') + $$, + 'Should have created all payment accounts' +); + + +select * +from finish(); + +rollback; diff --git a/test/add_payment_account_card.sql b/test/add_payment_account_card.sql new file mode 100644 index 0000000..25cb423 --- /dev/null +++ b/test/add_payment_account_card.sql @@ -0,0 +1,66 @@ +-- Test add_payment_account_card +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(12); + +set search_path to numerus, public; + +select has_function('numerus', 'add_payment_account_card', array['integer', 'text', 'text', 'date']); +select function_lang_is('numerus', 'add_payment_account_card', array['integer', 'text', 'text', 'date'], 'plpgsql'); +select function_returns('numerus', 'add_payment_account_card', array['integer', 'text', 'text', 'date'], 'uuid'); +select isnt_definer('numerus', 'add_payment_account_card', array['integer', 'text', 'text', 'date']); +select volatility_is('numerus', 'add_payment_account_card', array['integer', 'text', 'text', 'date'], 'volatile'); + +select function_privs_are('numerus', 'add_payment_account_card', array['integer', 'text', 'text', 'date'], 'guest', array []::text[]); +select function_privs_are('numerus', 'add_payment_account_card', array['integer', 'text', 'text', 'date'], 'invoicer', array ['EXECUTE']); +select function_privs_are('numerus', 'add_payment_account_card', array['integer', 'text', 'text', 'date'], 'admin', array ['EXECUTE']); +select function_privs_are('numerus', 'add_payment_account_card', array['integer', 'text', 'text', 'date'], 'authenticator', array []::text[]); + + +set client_min_messages to warning; +truncate payment_account_card cascade; +truncate payment_account cascade; +truncate payment_method cascade; +truncate company cascade; +reset client_min_messages; + +set constraints "company_default_payment_method_id_fkey" deferred; + +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 (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 111) + , (2, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', 'FR', 'USD', 222) +; + +insert into payment_method (payment_method_id, company_id, name, instructions) +values (111, 1, 'cash', 'cash') + , (222, 2, 'cash', 'cash') +; + +set constraints "company_default_payment_method_id_fkey" immediate; + +select lives_ok( + $$ select add_payment_account_card(1, 'Card A', '1234', '2024-04-04') $$, + 'Should be able to insert a first card payment account' +); + +select lives_ok ( + $$ select add_payment_account_card(1, 'Card 2', '2345', '2025-05-05') $$, + 'Should be able to insert a second card payment account' +); + +select bag_eq( + $$ select company_id, payment_account_type::text, name, last_four_digits, expiration_date::text from payment_account join payment_account_card using (payment_account_id, payment_account_type) $$, + $$ values (1, 'card', 'Card A', '1234', '2024-04-04') + , (1, 'card', 'Card 2', '2345', '2025-05-05') + $$, + 'Should have created all payment accounts' +); + +select * +from finish(); + +rollback; diff --git a/test/add_payment_account_cash.sql b/test/add_payment_account_cash.sql new file mode 100644 index 0000000..2470d9b --- /dev/null +++ b/test/add_payment_account_cash.sql @@ -0,0 +1,65 @@ +-- Test add_payment_account_cash +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(12); + +set search_path to numerus, public; + +select has_function('numerus', 'add_payment_account_cash', array['integer', 'text']); +select function_lang_is('numerus', 'add_payment_account_cash', array['integer', 'text'], 'sql'); +select function_returns('numerus', 'add_payment_account_cash', array['integer', 'text'], 'uuid'); +select isnt_definer('numerus', 'add_payment_account_cash', array['integer', 'text']); +select volatility_is('numerus', 'add_payment_account_cash', array['integer', 'text'], 'volatile'); + +select function_privs_are('numerus', 'add_payment_account_cash', array['integer', 'text'], 'guest', array []::text[]); +select function_privs_are('numerus', 'add_payment_account_cash', array['integer', 'text'], 'invoicer', array ['EXECUTE']); +select function_privs_are('numerus', 'add_payment_account_cash', array['integer', 'text'], 'admin', array ['EXECUTE']); +select function_privs_are('numerus', 'add_payment_account_cash', array['integer', 'text'], 'authenticator', array []::text[]); + + +set client_min_messages to warning; +truncate payment_account cascade; +truncate payment_method cascade; +truncate company cascade; +reset client_min_messages; + +set constraints "company_default_payment_method_id_fkey" deferred; + +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 (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 111) + , (2, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', 'FR', 'USD', 222) +; + +insert into payment_method (payment_method_id, company_id, name, instructions) +values (111, 1, 'cash', 'cash') + , (222, 2, 'cash', 'cash') +; + +set constraints "company_default_payment_method_id_fkey" immediate; + +select lives_ok( + $$ select add_payment_account_cash(1, 'Cash A') $$, + 'Should be able to insert a first cash payment account' +); + +select lives_ok ( + $$ select add_payment_account_cash(1, 'Cash 2') $$, + 'Should be able to insert a second cash payment account' +); + +select bag_eq( + $$ select company_id, payment_account_type::text, name from payment_account $$, + $$ values (1, 'cash', 'Cash A') + , (1, 'cash', 'Cash 2') + $$, + 'Should have created all payment accounts' +); + +select * +from finish(); + +rollback; diff --git a/test/add_payment_account_other.sql b/test/add_payment_account_other.sql new file mode 100644 index 0000000..8b1df49 --- /dev/null +++ b/test/add_payment_account_other.sql @@ -0,0 +1,65 @@ +-- Test add_payment_account_other +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(12); + +set search_path to numerus, public; + +select has_function('numerus', 'add_payment_account_other', array['integer', 'text']); +select function_lang_is('numerus', 'add_payment_account_other', array['integer', 'text'], 'sql'); +select function_returns('numerus', 'add_payment_account_other', array['integer', 'text'], 'uuid'); +select isnt_definer('numerus', 'add_payment_account_other', array['integer', 'text']); +select volatility_is('numerus', 'add_payment_account_other', array['integer', 'text'], 'volatile'); + +select function_privs_are('numerus', 'add_payment_account_other', array['integer', 'text'], 'guest', array []::text[]); +select function_privs_are('numerus', 'add_payment_account_other', array['integer', 'text'], 'invoicer', array ['EXECUTE']); +select function_privs_are('numerus', 'add_payment_account_other', array['integer', 'text'], 'admin', array ['EXECUTE']); +select function_privs_are('numerus', 'add_payment_account_other', array['integer', 'text'], 'authenticator', array []::text[]); + + +set client_min_messages to warning; +truncate payment_account cascade; +truncate payment_method cascade; +truncate company cascade; +reset client_min_messages; + +set constraints "company_default_payment_method_id_fkey" deferred; + +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 (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 111) + , (2, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', 'FR', 'USD', 222) +; + +insert into payment_method (payment_method_id, company_id, name, instructions) +values (111, 1, 'cash', 'cash') + , (222, 2, 'cash', 'cash') +; + +set constraints "company_default_payment_method_id_fkey" immediate; + +select lives_ok( + $$ select add_payment_account_other(1, 'Other A') $$, + 'Should be able to insert a first payment account' +); + +select lives_ok ( + $$ select add_payment_account_other(1, 'Other 2') $$, + 'Should be able to insert a second payment account' +); + +select bag_eq( + $$ select company_id, payment_account_type::text, name from payment_account $$, + $$ values (1, 'other', 'Other A') + , (1, 'other', 'Other 2') + $$, + 'Should have created all payment accounts' +); + +select * +from finish(); + +rollback; diff --git a/test/edit_payment.sql b/test/edit_payment.sql new file mode 100644 index 0000000..a7d6835 --- /dev/null +++ b/test/edit_payment.sql @@ -0,0 +1,115 @@ +-- Test edit_payment +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(14); + +set search_path to numerus, public; + +select has_function('numerus', 'edit_payment', array['uuid', 'date', 'integer', 'text', 'text', 'tag_name[]']); +select function_lang_is('numerus', 'edit_payment', array['uuid', 'date', 'integer', 'text', 'text', 'tag_name[]'], 'plpgsql'); +select function_returns('numerus', 'edit_payment', array['uuid', 'date', 'integer', 'text', 'text', 'tag_name[]'], 'uuid'); +select isnt_definer('numerus', 'edit_payment', array['uuid', 'date', 'integer', 'text', 'text', 'tag_name[]']); +select volatility_is('numerus', 'edit_payment', array['uuid', 'date', 'integer', 'text', 'text', 'tag_name[]'], 'volatile'); + +select function_privs_are('numerus', 'edit_payment', array ['uuid', 'date', 'integer', 'text', 'text', 'tag_name[]'], 'guest', array []::text[]); +select function_privs_are('numerus', 'edit_payment', array ['uuid', 'date', 'integer', 'text', 'text', 'tag_name[]'], 'invoicer', array ['EXECUTE']); +select function_privs_are('numerus', 'edit_payment', array ['uuid', 'date', 'integer', 'text', 'text', 'tag_name[]'], 'admin', array ['EXECUTE']); +select function_privs_are('numerus', 'edit_payment', array ['uuid', 'date', 'integer', 'text', 'text', 'tag_name[]'], 'authenticator', array []::text[]); + + +set client_min_messages to warning; +truncate expense_payment cascade; +truncate payment cascade; +truncate expense cascade; +truncate contact cascade; +truncate payment_account cascade; +truncate payment_method cascade; +truncate company cascade; +reset client_min_messages; + + +set constraints "company_default_payment_method_id_fkey" deferred; + +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 (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 111) +; + +insert into payment_method (payment_method_id, company_id, name, instructions) +values (111, 1, 'cash', 'cash') +; + +set constraints "company_default_payment_method_id_fkey" immediate; + +insert into contact (contact_id, company_id, name) +values ( 9, 1, 'Customer 1') +; + +insert into expense (expense_id, company_id, invoice_number, contact_id, invoice_date, amount, currency_code, expense_status) +values (13, 1, 'INV001', 9, '2011-01-11', 111, 'EUR', 'paid') + , (14, 1, 'INV002', 9, '2022-02-22', 222, 'EUR', 'paid') + , (15, 1, 'INV003', 9, '2022-02-22', 333, 'EUR', 'partial') +; + +insert into payment_account (payment_account_id, company_id, payment_account_type, name) +values (11, 1, 'cash', 'Cash 1') + , (12, 1, 'cash', 'Cash 2') + , (13, 1, 'other', 'Other') +; + +insert into payment (payment_id, company_id, slug, description, payment_date, payment_account_id, amount, currency_code, payment_status, tags) +values (16, 1, '7ac3ae0e-b0c1-4206-a19b-0be20835edd4', 'Payment INV001', '2023-05-04', 12, 111, 'EUR', 'complete', '{tag1}') + , (17, 1, 'b57b980b-247b-4be4-a0b7-03a7819c53ae', 'First INV002', '2023-05-05', 13, 100, 'EUR', 'partial', '{tag2}') + , (18, 1, '3bdad7a8-4a1e-4ae0-b5c6-015e51ee0502', 'Second INV002', '2023-05-06', 13, 122, 'EUR', 'partial', '{tag1,tag3}') + , (19, 1, '5a524bee-8311-4d13-9adf-ef6310b26990', 'Partial INV003', '2023-05-07', 11, 123, 'EUR', 'partial', '{}') +; + +insert into expense_payment (expense_id, payment_id) +values (13, 16) + , (14, 17) + , (14, 18) + , (15, 19) +; + +select lives_ok( + $$ select edit_payment('7ac3ae0e-b0c1-4206-a19b-0be20835edd4', '2023-05-06', 13, 'Partial INV001', '1.00', array['tag1']) $$, + 'Should be able to change a complete payment to partial' +); + +select lives_ok( + $$ select edit_payment('b57b980b-247b-4be4-a0b7-03a7819c53ae', '2023-05-07', 12, 'First INV002', '0.50', array['tag1', 'tag3']) $$, + 'Should be able to adjust a partial payment, that is still partial, and the expense now becomes partial' +); + +select lives_ok( + $$ select edit_payment('5a524bee-8311-4d13-9adf-ef6310b26990', '2023-05-01', 11, 'Complete INV003', '3.33', array[]::tag_name[]) $$, + 'Should be able to complete a previously partial payment' +); + +select bag_eq( + $$ select description, payment_date::text, payment_account_id, amount, payment_status, tags::text from payment $$, + $$ values ('Partial INV001', '2023-05-06', 13, 100, 'partial', '{tag1}') + , ('First INV002', '2023-05-07', 12, 50, 'partial', '{tag1,tag3}') + , ('Second INV002', '2023-05-06', 13, 122, 'partial', '{tag1,tag3}') + , ('Complete INV003', '2023-05-01', 11, 333, 'complete', '{}') + $$, + 'Should have updated all payments' +); + +select bag_eq( + $$ select expense_id, expense_status from expense $$, + $$ values (13, 'partial') + , (14, 'partial') + , (15, 'paid') + $$, + 'Should have updated expenses too' +); + + +select * +from finish(); + +rollback; diff --git a/test/edit_payment_account_bank.sql b/test/edit_payment_account_bank.sql new file mode 100644 index 0000000..b698d03 --- /dev/null +++ b/test/edit_payment_account_bank.sql @@ -0,0 +1,84 @@ +-- Test edit_payment_account_bank +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(13); + +set search_path to numerus, public; + +select has_function('numerus', 'edit_payment_account_bank', array['uuid', 'text', 'iban']); +select function_lang_is('numerus', 'edit_payment_account_bank', array['uuid', 'text', 'iban'], 'plpgsql'); +select function_returns('numerus', 'edit_payment_account_bank', array['uuid', 'text', 'iban'], 'uuid'); +select isnt_definer('numerus', 'edit_payment_account_bank', array['uuid', 'text', 'iban']); +select volatility_is('numerus', 'edit_payment_account_bank', array['uuid', 'text', 'iban'], 'volatile'); + +select function_privs_are('numerus', 'edit_payment_account_bank', array['uuid', 'text', 'iban'], 'guest', array []::text[]); +select function_privs_are('numerus', 'edit_payment_account_bank', array['uuid', 'text', 'iban'], 'invoicer', array ['EXECUTE']); +select function_privs_are('numerus', 'edit_payment_account_bank', array['uuid', 'text', 'iban'], 'admin', array ['EXECUTE']); +select function_privs_are('numerus', 'edit_payment_account_bank', array['uuid', 'text', 'iban'], 'authenticator', array []::text[]); + + +set client_min_messages to warning; +truncate payment_account_bank cascade; +truncate payment_account cascade; +truncate payment_method cascade; +truncate company cascade; +reset client_min_messages; + +set constraints "company_default_payment_method_id_fkey" deferred; + +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 (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 111) +; + +insert into payment_method (payment_method_id, company_id, name, instructions) +values (111, 1, 'cash', 'cash') +; + +set constraints "company_default_payment_method_id_fkey" immediate; + +insert into payment_account (payment_account_id, company_id, slug, payment_account_type, name) +values (11, 1, '81c478ab-395c-44aa-a5b4-3489e5349a0b', 'bank', 'Bank 1') + , (12, 1, '84e84788-54f1-4796-b93d-dfa2c9adc072', 'bank', 'Bank 2') + , (13, 1, 'afb774a5-0c38-480c-bef2-fe03465071cd', 'other', 'Other') +; + +insert into payment_account_bank (payment_account_id, iban) +values (11, 'MR1765838885255712383711884') + , (12, 'UA709147419869457296562612646') +; + + +select lives_ok( + $$ select edit_payment_account_bank('81c478ab-395c-44aa-a5b4-3489e5349a0b', 'Piggy Bank', 'ES2820958297603648596978') $$, + 'Should be able to edit the first bank payment account' +); + +select lives_ok ( + $$ select edit_payment_account_bank('84e84788-54f1-4796-b93d-dfa2c9adc072', 'Totally Secure', 'DE68500105178297336485') $$, + 'Should be able to edit the second bank payment account' +); + +select results_eq ( + $$ select edit_payment_account_bank('afb774a5-0c38-480c-bef2-fe03465071cd', 'Huh?', 'ES3720951423184941926287')::text $$, + $$ values (null::text) $$, + 'Should do nothing to an account that is not of type bank' +); + +select bag_eq( + $$ select payment_account_id, payment_account_type::text, name, iban::text from payment_account left join payment_account_bank using (payment_account_id, payment_account_type) $$, + $$ values (11, 'bank', 'Piggy Bank', 'ES2820958297603648596978') + , (12, 'bank', 'Totally Secure', 'DE68500105178297336485') + , (13, 'other', 'Other', null) + $$, + 'Should have update all bank payment accounts' +); + + +select * +from finish(); + +rollback; diff --git a/test/edit_payment_account_card.sql b/test/edit_payment_account_card.sql new file mode 100644 index 0000000..5134687 --- /dev/null +++ b/test/edit_payment_account_card.sql @@ -0,0 +1,84 @@ +-- Test edit_payment_account_card +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(13); + +set search_path to numerus, public; + +select has_function('numerus', 'edit_payment_account_card', array['uuid', 'text', 'text', 'date']); +select function_lang_is('numerus', 'edit_payment_account_card', array['uuid', 'text', 'text', 'date'], 'plpgsql'); +select function_returns('numerus', 'edit_payment_account_card', array['uuid', 'text', 'text', 'date'], 'uuid'); +select isnt_definer('numerus', 'edit_payment_account_card', array['uuid', 'text', 'text', 'date']); +select volatility_is('numerus', 'edit_payment_account_card', array['uuid', 'text', 'text', 'date'], 'volatile'); + +select function_privs_are('numerus', 'edit_payment_account_card', array['uuid', 'text', 'text', 'date'], 'guest', array []::text[]); +select function_privs_are('numerus', 'edit_payment_account_card', array['uuid', 'text', 'text', 'date'], 'invoicer', array ['EXECUTE']); +select function_privs_are('numerus', 'edit_payment_account_card', array['uuid', 'text', 'text', 'date'], 'admin', array ['EXECUTE']); +select function_privs_are('numerus', 'edit_payment_account_card', array['uuid', 'text', 'text', 'date'], 'authenticator', array []::text[]); + + +set client_min_messages to warning; +truncate payment_account_card cascade; +truncate payment_account cascade; +truncate payment_method cascade; +truncate company cascade; +reset client_min_messages; + +set constraints "company_default_payment_method_id_fkey" deferred; + +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 (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 111) +; + +insert into payment_method (payment_method_id, company_id, name, instructions) +values (111, 1, 'cash', 'cash') +; + +set constraints "company_default_payment_method_id_fkey" immediate; + +insert into payment_account (payment_account_id, company_id, slug, payment_account_type, name) +values (11, 1, '81c478ab-395c-44aa-a5b4-3489e5349a0b', 'card', 'Card 1') + , (12, 1, '84e84788-54f1-4796-b93d-dfa2c9adc072', 'card', 'Card 2') + , (13, 1, 'afb774a5-0c38-480c-bef2-fe03465071cd', 'other', 'Other') +; + +insert into payment_account_card (payment_account_id, last_four_digits, expiration_date) +values (11, '1234', '2022-04-02') + , (12, '2345', '2024-05-02') +; + + +select lives_ok( + $$ select edit_payment_account_card('81c478ab-395c-44aa-a5b4-3489e5349a0b', 'Gold Card', '9999', '2022-12-22') $$, + 'Should be able to edit the first card payment account' +); + +select lives_ok ( + $$ select edit_payment_account_card('84e84788-54f1-4796-b93d-dfa2c9adc072', 'Scurvy Card', '7777', '2025-08-25') $$, + 'Should be able to edit the second card payment account' +); + +select results_eq ( + $$ select edit_payment_account_card('afb774a5-0c38-480c-bef2-fe03465071cd', 'Huh?', '1234', '2006-01-02')::text $$, + $$ values (null::text) $$, + 'Should do nothing to an account that is not of type card' +); + +select bag_eq( + $$ select payment_account_id, payment_account_type::text, name, last_four_digits, expiration_date::text from payment_account left join payment_account_card using (payment_account_id, payment_account_type) $$, + $$ values (11, 'card', 'Gold Card', '9999', '2022-12-22') + , (12, 'card', 'Scurvy Card', '7777', '2025-08-25') + , (13, 'other', 'Other', null::text, null::text) + $$, + 'Should have update all card payment accounts' +); + + +select * +from finish(); + +rollback; diff --git a/test/edit_payment_account_cash.sql b/test/edit_payment_account_cash.sql new file mode 100644 index 0000000..303b5b2 --- /dev/null +++ b/test/edit_payment_account_cash.sql @@ -0,0 +1,77 @@ +-- Test edit_payment_account_cash +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(13); + +set search_path to numerus, public; + +select has_function('numerus', 'edit_payment_account_cash', array['uuid', 'text']); +select function_lang_is('numerus', 'edit_payment_account_cash', array['uuid', 'text'], 'sql'); +select function_returns('numerus', 'edit_payment_account_cash', array['uuid', 'text'], 'uuid'); +select isnt_definer('numerus', 'edit_payment_account_cash', array['uuid', 'text']); +select volatility_is('numerus', 'edit_payment_account_cash', array['uuid', 'text'], 'volatile'); + +select function_privs_are('numerus', 'edit_payment_account_cash', array['uuid', 'text'], 'guest', array []::text[]); +select function_privs_are('numerus', 'edit_payment_account_cash', array['uuid', 'text'], 'invoicer', array ['EXECUTE']); +select function_privs_are('numerus', 'edit_payment_account_cash', array['uuid', 'text'], 'admin', array ['EXECUTE']); +select function_privs_are('numerus', 'edit_payment_account_cash', array['uuid', 'text'], 'authenticator', array []::text[]); + + +set client_min_messages to warning; +truncate payment_account cascade; +truncate payment_method cascade; +truncate company cascade; +reset client_min_messages; + +set constraints "company_default_payment_method_id_fkey" deferred; + +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 (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 111) +; + +insert into payment_method (payment_method_id, company_id, name, instructions) +values (111, 1, 'cash', 'cash') +; + +set constraints "company_default_payment_method_id_fkey" immediate; + +insert into payment_account (payment_account_id, company_id, slug, payment_account_type, name) +values (11, 1, '81c478ab-395c-44aa-a5b4-3489e5349a0b', 'cash', 'Cash 1') + , (12, 1, '84e84788-54f1-4796-b93d-dfa2c9adc072', 'cash', 'Cash 2') + , (13, 1, 'afb774a5-0c38-480c-bef2-fe03465071cd', 'other', 'Other') +; + + +select lives_ok( + $$ select edit_payment_account_cash('81c478ab-395c-44aa-a5b4-3489e5349a0b', 'My stash') $$, + 'Should be able to edit the first cash payment account' +); + +select lives_ok ( + $$ select edit_payment_account_cash('84e84788-54f1-4796-b93d-dfa2c9adc072', 'Under mattress') $$, + 'Should be able to edit the second cash payment account' +); + +select results_eq ( + $$ select edit_payment_account_cash('afb774a5-0c38-480c-bef2-fe03465071cd', 'Huh?')::text $$, + $$ values (null::text) $$, + 'Should do nothing to an account that is not of type cash' +); + +select bag_eq( + $$ select payment_account_id, payment_account_type::text, name from payment_account $$, + $$ values (11, 'cash', 'My stash') + , (12, 'cash', 'Under mattress') + , (13, 'other', 'Other') + $$, + 'Should have update all cash payment accounts' +); + +select * +from finish(); + +rollback; diff --git a/test/edit_payment_account_other.sql b/test/edit_payment_account_other.sql new file mode 100644 index 0000000..b72d069 --- /dev/null +++ b/test/edit_payment_account_other.sql @@ -0,0 +1,77 @@ +-- Test edit_payment_account_other +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(13); + +set search_path to numerus, public; + +select has_function('numerus', 'edit_payment_account_other', array['uuid', 'text']); +select function_lang_is('numerus', 'edit_payment_account_other', array['uuid', 'text'], 'sql'); +select function_returns('numerus', 'edit_payment_account_other', array['uuid', 'text'], 'uuid'); +select isnt_definer('numerus', 'edit_payment_account_other', array['uuid', 'text']); +select volatility_is('numerus', 'edit_payment_account_other', array['uuid', 'text'], 'volatile'); + +select function_privs_are('numerus', 'edit_payment_account_other', array['uuid', 'text'], 'guest', array []::text[]); +select function_privs_are('numerus', 'edit_payment_account_other', array['uuid', 'text'], 'invoicer', array ['EXECUTE']); +select function_privs_are('numerus', 'edit_payment_account_other', array['uuid', 'text'], 'admin', array ['EXECUTE']); +select function_privs_are('numerus', 'edit_payment_account_other', array['uuid', 'text'], 'authenticator', array []::text[]); + + +set client_min_messages to warning; +truncate payment_account cascade; +truncate payment_method cascade; +truncate company cascade; +reset client_min_messages; + +set constraints "company_default_payment_method_id_fkey" deferred; + +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 (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', 'ES', 'EUR', 111) +; + +insert into payment_method (payment_method_id, company_id, name, instructions) +values (111, 1, 'cash', 'cash') +; + +set constraints "company_default_payment_method_id_fkey" immediate; + +insert into payment_account (payment_account_id, company_id, slug, payment_account_type, name) +values (11, 1, '81c478ab-395c-44aa-a5b4-3489e5349a0b', 'other', 'Other 1') + , (12, 1, '84e84788-54f1-4796-b93d-dfa2c9adc072', 'other', 'Other 2') + , (13, 1, 'afb774a5-0c38-480c-bef2-fe03465071cd', 'cash', 'Cash') +; + + +select lives_ok( + $$ select edit_payment_account_other('81c478ab-395c-44aa-a5b4-3489e5349a0b', 'Something') $$, + 'Should be able to edit the first other payment account' +); + +select lives_ok ( + $$ select edit_payment_account_other('84e84788-54f1-4796-b93d-dfa2c9adc072', 'And something else') $$, + 'Should be able to edit the second other payment account' +); + +select results_eq ( + $$ select edit_payment_account_other('afb774a5-0c38-480c-bef2-fe03465071cd', 'Huh?')::text $$, + $$ values (null::text) $$, + 'Should do nothing to an account that is not of type other' +); + +select bag_eq( + $$ select payment_account_id, payment_account_type::text, name from payment_account $$, + $$ values (11, 'other', 'Something') + , (12, 'other', 'And something else') + , (13, 'cash', 'Cash') + $$, + 'Should have update all other payment accounts' +); + +select * +from finish(); + +rollback; diff --git a/test/expense_payment.sql b/test/expense_payment.sql new file mode 100644 index 0000000..dcdf492 --- /dev/null +++ b/test/expense_payment.sql @@ -0,0 +1,136 @@ +-- Test expense_payment +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(23); + +set search_path to numerus, auth, public; + +select has_table('expense_payment'); +select has_pk('expense_payment'); +select col_is_pk('expense_payment', array['expense_id', 'payment_id']); +select table_privs_are('expense_payment', 'guest', array []::text[]); +select table_privs_are('expense_payment', 'invoicer', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']); +select table_privs_are('expense_payment', 'admin', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']); +select table_privs_are('expense_payment', 'authenticator', array []::text[]); + +select has_column('expense_payment', 'expense_id'); +select col_is_fk('expense_payment', 'expense_id'); +select fk_ok('expense_payment', 'expense_id', 'expense', 'expense_id'); +select col_type_is('expense_payment', 'expense_id', 'integer'); +select col_not_null('expense_payment', 'expense_id'); +select col_hasnt_default('expense_payment', 'expense_id'); + +select has_column('expense_payment', 'payment_id'); +select col_is_fk('expense_payment', 'payment_id'); +select fk_ok('expense_payment', 'payment_id', 'payment', 'payment_id'); +select col_type_is('expense_payment', 'payment_id', 'integer'); +select col_not_null('expense_payment', 'payment_id'); +select col_hasnt_default('expense_payment', 'payment_id'); + + +set client_min_messages to warning; +truncate expense_payment cascade; +truncate payment cascade; +truncate payment_account cascade; +truncate expense cascade; +truncate contact cascade; +truncate company_user cascade; +truncate payment_method 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') +; + +set constraints "company_default_payment_method_id_fkey" deferred; + +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 ( 9, 2, 'Customer 1') + , (10, 4, 'Customer 2') +; + +insert into expense (expense_id, company_id, invoice_number, contact_id, invoice_date, amount, currency_code) +values (13, 2, 'INV001', 9, '2011-01-11', 111, 'EUR') + , (14, 4, 'INV002', 10, '2022-02-22', 222, 'EUR') +; + +insert into payment_account (payment_account_id, company_id, payment_account_type, name) +values (17, 2, 'cash', 'Cash 2') + , (18, 4, 'cash', 'Cash 4') +; + +insert into payment (payment_id, company_id, description, payment_date, payment_account_id, amount, currency_code) +values (21, 2, 'Payment INV001', '2022-01-11', 17, 111, 'EUR') + , (22, 4, 'Payment INV002', '2022-02-23', 18, 222, 'EUR') +; + +insert into expense_payment (expense_id, payment_id) +values (13, 21) + , (14, 22) +; + +prepare expense_payment_data as +select expense_id, payment_id +from expense_payment +order by expense_id, payment_id; + +set role invoicer; +select is_empty('expense_payment_data', 'Should show no data when cookie is not set yet'); +reset role; + +select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog'); +select bag_eq( + 'expense_payment_data', + $$ values (13, 21) + $$, + 'Should only list tax of products of the companies where demo@tandem.blog is user of' +); +reset role; + +select set_cookie('12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524/admin@tandem.blog'); +select bag_eq( + 'expense_payment_data', + $$ values (14, 22) + $$, + 'Should only list tax of products of the companies where admin@tandem.blog is user of' +); +reset role; + +select set_cookie('not-a-cookie'); +select throws_ok( + 'expense_payment_data', + '42501', 'permission denied for table expense_payment', + 'Should not allow select to guest users' +); +reset role; + + +select * +from finish(); + +rollback; + diff --git a/test/payment.sql b/test/payment.sql new file mode 100644 index 0000000..c471d2a --- /dev/null +++ b/test/payment.sql @@ -0,0 +1,173 @@ +-- Test payment +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(68); + +set search_path to numerus, auth, public; + +select has_table('payment'); +select has_pk('payment'); +select table_privs_are('payment', 'guest', array []::text[]); +select table_privs_are('payment', 'invoicer', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']); +select table_privs_are('payment', 'admin', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']); +select table_privs_are('payment', 'authenticator', array []::text[]); + +select has_column('payment', 'payment_id'); +select col_type_is('payment', 'payment_id', 'integer'); +select col_not_null('payment', 'payment_id'); +select col_hasnt_default('payment', 'payment_id'); + +select has_column('payment', 'company_id'); +select col_is_fk('payment', 'company_id'); +select fk_ok('payment', 'company_id', 'company', 'company_id'); +select col_type_is('payment', 'company_id', 'integer'); +select col_not_null('payment', 'company_id'); +select col_hasnt_default('payment', 'company_id'); + +select has_column('payment', 'slug'); +select col_is_unique('payment', 'slug'); +select col_type_is('payment', 'slug', 'uuid'); +select col_not_null('payment', 'slug'); +select col_has_default('payment', 'slug'); +select col_default_is('payment', 'slug', 'gen_random_uuid()'); + +select has_column('payment', 'description'); +select col_type_is('payment', 'description', 'text'); +select col_not_null('payment', 'description'); +select col_hasnt_default('payment', 'description'); + +select has_column('payment', 'payment_date'); +select col_type_is('payment', 'payment_date', 'date'); +select col_not_null('payment', 'payment_date'); +select col_has_default('payment', 'payment_date'); +select col_default_is('payment', 'payment_date', 'CURRENT_DATE'); + +select has_column('payment', 'payment_account_id'); +select col_is_fk('payment', 'payment_account_id'); +select fk_ok('payment', 'payment_account_id', 'payment_account', 'payment_account_id'); +select col_type_is('payment', 'payment_account_id', 'integer'); +select col_not_null('payment', 'payment_account_id'); +select col_hasnt_default('payment', 'payment_account_id'); + +select has_column('payment', 'amount'); +select col_type_is('payment', 'amount', 'integer'); +select col_not_null('payment', 'amount'); +select col_hasnt_default('payment', 'amount'); + +select has_column('payment', 'currency_code'); +select col_is_fk('payment', 'currency_code'); +select fk_ok('payment', 'currency_code', 'currency', 'currency_code'); +select col_type_is('payment', 'currency_code', 'text'); +select col_not_null('payment', 'currency_code'); +select col_hasnt_default('payment', 'currency_code'); + +select has_column('payment', 'tags'); +select col_type_is('payment', 'tags', 'tag_name[]'); +select col_not_null('payment', 'tags'); +select col_has_default('payment', 'tags'); +select col_default_is('payment', 'tags', '{}'); + +select has_column('payment', 'payment_status'); +select col_is_fk('payment', 'payment_status'); +select fk_ok('payment', 'payment_status', 'payment_status', 'payment_status'); +select col_type_is('payment', 'payment_status', 'text'); +select col_not_null('payment', 'payment_status'); +select col_has_default('payment', 'payment_status'); +select col_default_is('payment', 'payment_status', 'complete'); + +select has_column('payment', 'created_at'); +select col_type_is('payment', 'created_at', 'timestamp with time zone'); +select col_not_null('payment', 'created_at'); +select col_has_default('payment', 'created_at'); +select col_default_is('payment', 'created_at', 'CURRENT_TIMESTAMP'); + + +set client_min_messages to warning; +truncate payment cascade; +truncate payment_account cascade; +truncate company_user cascade; +truncate company cascade; +truncate payment_method 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') +; + +set constraints "company_default_payment_method_id_fkey" deferred; + +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 payment_account (payment_account_id, company_id, payment_account_type, name) +values (221, 2, 'other', 'Other 2') + , (441, 4, 'other', 'Other 4') +; + +insert into payment (company_id, description, payment_account_id, amount, currency_code) +values (2, 'Payment 20001', 221, 333, 'EUR') + , (4, 'Payment 40001', 441, 555, 'EUR') +; + + +prepare payment_data as +select company_id, description +from payment +order by company_id, description; + +set role invoicer; +select is_empty('payment_data', 'Should show no data when cookie is not set yet'); +reset role; + +select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog'); +select bag_eq( + 'payment_data', + $$ values (2, 'Payment 20001') + $$, + 'Should only list payments from the companies where demo@tandem.blog is user of' +); +reset role; + +select set_cookie('12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524/admin@tandem.blog'); +select bag_eq( + 'payment_data', + $$ values (4, 'Payment 40001') + $$, + 'Should only list payments from the companies where admin@tandem.blog is user of' +); +reset role; + +select set_cookie('not-a-cookie'); +select throws_ok( + 'payment_data', + '42501', 'permission denied for table payment', + 'Should not allow select to guest users' +); +reset role; + + +select * +from finish(); + +rollback; + diff --git a/test/payment_account.sql b/test/payment_account.sql new file mode 100644 index 0000000..56594bc --- /dev/null +++ b/test/payment_account.sql @@ -0,0 +1,139 @@ +-- Test payment_account +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(38); + +set search_path to numerus, auth, public; + +select has_table('payment_account'); +select has_pk('payment_account'); +select table_privs_are('payment_account', 'guest', array []::text[]); +select table_privs_are('payment_account', 'invoicer', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']); +select table_privs_are('payment_account', 'admin', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']); +select table_privs_are('payment_account', 'authenticator', array []::text[]); + + +select has_column('payment_account', 'payment_account_id'); +select col_is_pk('payment_account', 'payment_account_id'); +select col_type_is('payment_account', 'payment_account_id', 'integer'); +select col_not_null('payment_account', 'payment_account_id'); +select col_hasnt_default('payment_account', 'payment_account_id'); + +select has_column('payment_account', 'company_id'); +select col_is_fk('payment_account', 'company_id'); +select fk_ok('payment_account', 'company_id', 'company', 'company_id'); +select col_type_is('payment_account', 'company_id', 'integer'); +select col_not_null('payment_account', 'company_id'); +select col_hasnt_default('payment_account', 'company_id'); + +select has_column('payment_account', 'slug'); +select col_is_unique('payment_account', 'slug'); +select col_type_is('payment_account', 'slug', 'uuid'); +select col_not_null('payment_account', 'slug'); +select col_has_default('payment_account', 'slug'); +select col_default_is('payment_account', 'slug', 'gen_random_uuid()'); + +select has_column('payment_account', 'payment_account_type'); +select col_type_is('payment_account', 'payment_account_type', 'text'); +select col_is_fk('payment_account', 'payment_account_type'); +select fk_ok('payment_account', 'payment_account_type', 'payment_account_type', 'payment_account_type'); +select col_not_null('payment_account', 'payment_account_type'); +select col_hasnt_default('payment_account', 'payment_account_type'); + +select has_column('payment_account', 'name'); +select col_type_is('payment_account', 'name', 'text'); +select col_not_null('payment_account', 'name'); +select col_hasnt_default('payment_account', 'name'); + + +set client_min_messages to warning; +truncate payment_account cascade; +truncate company_user cascade; +truncate company cascade; +truncate payment_method 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') +; + +set constraints "company_default_payment_method_id_fkey" deferred; + +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 payment_account (company_id, payment_account_type, name) +values (2, 'bank', 'Bank A') + , (4, 'cash', 'Cash') +; + + +prepare payment_account_data as +select company_id, name +from payment_account +order by company_id, name; + +set role invoicer; +select is_empty('payment_account_data', 'Should show no data when cookie is not set yet'); +reset role; + +select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog'); +select bag_eq( + 'payment_account_data', + $$ values (2, 'Bank A') + $$, + 'Should only list payment_accounts from the companies where demo@tandem.blog is user of' +); +reset role; + +select set_cookie('12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524/admin@tandem.blog'); +select bag_eq( + 'payment_account_data', + $$ values (4, 'Cash') + $$, + 'Should only list payment_accounts from the companies where admin@tandem.blog is user of' +); +reset role; + +select set_cookie('not-a-cookie'); +select throws_ok( + 'payment_account_data', + '42501', 'permission denied for table payment_account', + 'Should not allow select to guest users' +); +reset role; + +select throws_ok( $$ + insert into payment_account (company_id, payment_account_type, name) + values (2, 'cash', ' ') + $$, + '23514', 'new row for relation "payment_account" violates check constraint "payment_account_name_not_empty"', + 'Should not allow payment accounts with a blank name' +); + + +select * +from finish(); + +rollback; + diff --git a/test/payment_account_bank.sql b/test/payment_account_bank.sql new file mode 100644 index 0000000..85377d0 --- /dev/null +++ b/test/payment_account_bank.sql @@ -0,0 +1,150 @@ +-- Test payment_account_bank +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(29); + +set search_path to numerus, auth, public; + +select has_table('payment_account_bank'); +select has_pk('payment_account_bank'); +select table_privs_are('payment_account_bank', 'guest', array []::text[]); +select table_privs_are('payment_account_bank', 'invoicer', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']); +select table_privs_are('payment_account_bank', 'admin', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']); +select table_privs_are('payment_account_bank', 'authenticator', array []::text[]); + +select col_is_fk('payment_account_bank', array['payment_account_id', 'payment_account_type']); +select fk_ok('payment_account_bank', array['payment_account_id', 'payment_account_type'], 'payment_account', array['payment_account_id', 'payment_account_type']); + +select has_column('payment_account_bank', 'payment_account_id'); +select col_is_pk('payment_account_bank', 'payment_account_id'); +select col_type_is('payment_account_bank', 'payment_account_id', 'integer'); +select col_not_null('payment_account_bank', 'payment_account_id'); +select col_hasnt_default('payment_account_bank', 'payment_account_id'); + +select has_column('payment_account_bank', 'payment_account_type'); +select col_type_is('payment_account_bank', 'payment_account_type', 'text'); +select col_not_null('payment_account_bank', 'payment_account_type'); +select col_has_default('payment_account_bank', 'payment_account_type'); +select col_default_is('payment_account_bank', 'payment_account_type', 'bank'); + +select has_column('payment_account_bank', 'iban'); +select col_type_is('payment_account_bank', 'iban', 'iban'); +select col_not_null('payment_account_bank', 'iban'); +select col_hasnt_default('payment_account_bank', 'iban'); + + +set client_min_messages to warning; +truncate payment_account_bank cascade; +truncate payment_account 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 payment_account (payment_account_id, company_id, payment_account_type, name) +values ( 6, 2, 'bank', 'Bank 2') + , ( 7, 2, 'card', 'Credit card 2') + , ( 8, 2, 'cash', 'Cash 2') + , ( 9, 2, 'other', 'Other 2') + , (10, 4, 'bank', 'Bank 4') +; + +insert into payment_account_bank (payment_account_id, iban) +values ( 6, 'NL35INGB5262865534') + , (10, 'MT47JQRS54557143744629565915326') +; + +prepare payment_account_data as +select company_id, iban::text +from payment_account +join payment_account_bank using (payment_account_id) +order by payment_account_id, iban; + +set role invoicer; +select is_empty('payment_account_data', 'Should show no data when cookie is not set yet'); +reset role; + +select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog'); +select bag_eq( + 'payment_account_data', + $$ values (2, 'NL35INGB5262865534') + $$, + 'Should only list payment_accounts of the companies where demo@tandem.blog is user of' +); +reset role; + +select set_cookie('12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524/admin@tandem.blog'); +select bag_eq( + 'payment_account_data', + $$ values (4, 'MT47JQRS54557143744629565915326') + $$, + 'Should only list payment_accounts of the companies where admin@tandem.blog is user of' +); +reset role; + +select set_cookie('not-a-cookie'); +select throws_ok( + 'payment_account_data', + '42501', 'permission denied for table payment_account', + 'Should not allow select to guest users' +); +reset role; + +select throws_ok( $$ + insert into payment_account_bank (payment_account_id, payment_account_type, iban) + values (7, 'card', 'ES2820958297603648596978') + $$, + '23514', 'new row for relation "payment_account_bank" violates check constraint "payment_account_type_is_bank"', + 'Should not allow payment accounts of type card' +); + +select throws_ok( $$ + insert into payment_account_bank (payment_account_id, payment_account_type, iban) + values (8, 'cash', 'ES2820958297603648596978') + $$, + '23514', 'new row for relation "payment_account_bank" violates check constraint "payment_account_type_is_bank"', + 'Should not allow payment accounts of type cash' +); + +select throws_ok( $$ + insert into payment_account_bank (payment_account_id, payment_account_type, iban) + values (9, 'other', 'ES2820958297603648596978') + $$, + '23514', 'new row for relation "payment_account_bank" violates check constraint "payment_account_type_is_bank"', + 'Should not allow payment accounts of type other' +); + +select * +from finish(); + +rollback; + diff --git a/test/payment_account_card.sql b/test/payment_account_card.sql new file mode 100644 index 0000000..404738b --- /dev/null +++ b/test/payment_account_card.sql @@ -0,0 +1,192 @@ +-- Test payment_account_card +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(39); + +set search_path to numerus, auth, public; + +select has_table('payment_account_card'); +select has_pk('payment_account_card'); +select table_privs_are('payment_account_card', 'guest', array []::text[]); +select table_privs_are('payment_account_card', 'invoicer', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']); +select table_privs_are('payment_account_card', 'admin', array ['SELECT', 'INSERT', 'UPDATE', 'DELETE']); +select table_privs_are('payment_account_card', 'authenticator', array []::text[]); + +select col_is_fk('payment_account_card', array['payment_account_id', 'payment_account_type']); +select fk_ok('payment_account_card', array['payment_account_id', 'payment_account_type'], 'payment_account', array['payment_account_id', 'payment_account_type']); + +select has_column('payment_account_card', 'payment_account_id'); +select col_is_pk('payment_account_card', 'payment_account_id'); +select col_type_is('payment_account_card', 'payment_account_id', 'integer'); +select col_not_null('payment_account_card', 'payment_account_id'); +select col_hasnt_default('payment_account_card', 'payment_account_id'); + +select has_column('payment_account_card', 'payment_account_type'); +select col_type_is('payment_account_card', 'payment_account_type', 'text'); +select col_not_null('payment_account_card', 'payment_account_type'); +select col_has_default('payment_account_card', 'payment_account_type'); +select col_default_is('payment_account_card', 'payment_account_type', 'card'); + +select has_column('payment_account_card', 'last_four_digits'); +select col_type_is('payment_account_card', 'last_four_digits', 'text'); +select col_not_null('payment_account_card', 'last_four_digits'); +select col_hasnt_default('payment_account_card', 'last_four_digits'); + +select has_column('payment_account_card', 'expiration_date'); +select col_type_is('payment_account_card', 'expiration_date', 'date'); +select col_not_null('payment_account_card', 'expiration_date'); +select col_hasnt_default('payment_account_card', 'expiration_date'); + + +set client_min_messages to warning; +truncate payment_account_card cascade; +truncate payment_account 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 payment_account (payment_account_id, company_id, payment_account_type, name) +values ( 6, 2, 'bank', 'Bank 2') + , ( 7, 2, 'card', 'Credit card 2') + , ( 8, 2, 'cash', 'Cash 2') + , ( 9, 2, 'other', 'Other 2') + , (10, 4, 'card', 'Card 4') +; + +insert into payment_account_card (payment_account_id, last_four_digits, expiration_date) +values ( 7, '1234', '2024-07-09') + , (10, '4321', '2025-09-07') +; + +prepare payment_account_data as +select company_id, last_four_digits +from payment_account +join payment_account_card using (payment_account_id) +order by payment_account_id, last_four_digits; + +set role invoicer; +select is_empty('payment_account_data', 'Should show no data when cookie is not set yet'); +reset role; + +select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog'); +select bag_eq( + 'payment_account_data', + $$ values (2, '1234') + $$, + 'Should only list payment_accounts of the companies where demo@tandem.blog is user of' +); +reset role; + +select set_cookie('12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524/admin@tandem.blog'); +select bag_eq( + 'payment_account_data', + $$ values (4, '4321') + $$, + 'Should only list payment_accounts of the companies where admin@tandem.blog is user of' +); +reset role; + +select set_cookie('not-a-cookie'); +select throws_ok( + 'payment_account_data', + '42501', 'permission denied for table payment_account', + 'Should not allow select to guest users' +); +reset role; + +select throws_ok( $$ + insert into payment_account_card (payment_account_id, payment_account_type, last_four_digits, expiration_date) + values (6, 'bank', '1111', '2025-05-05') + $$, + '23514', 'new row for relation "payment_account_card" violates check constraint "payment_account_type_is_card"', + 'Should not allow payment accounts of type bank' +); + +select throws_ok( $$ + insert into payment_account_card (payment_account_id, payment_account_type, last_four_digits, expiration_date) + values (8, 'cash', '1111', '2025-05-05') + $$, + '23514', 'new row for relation "payment_account_card" violates check constraint "payment_account_type_is_card"', + 'Should not allow payment accounts of type cash' +); + +select throws_ok( $$ + insert into payment_account_card (payment_account_id, payment_account_type, last_four_digits, expiration_date) + values (9, 'other', '1111', '2025-05-05') + $$, + '23514', 'new row for relation "payment_account_card" violates check constraint "payment_account_type_is_card"', + 'Should not allow payment accounts of type other' +); + +select throws_ok ( + $$ update payment_account_card set last_four_digits = 'a234' where payment_account_id = 7 $$, + '23514', 'new row for relation "payment_account_card" violates check constraint "last_four_digits_are_digits"', + 'Last four digits of a credit card number should be all digits' +); + +select throws_ok ( + $$ update payment_account_card set last_four_digits = '1a34' where payment_account_id = 7 $$, + '23514', 'new row for relation "payment_account_card" violates check constraint "last_four_digits_are_digits"', + 'Last four digits of a credit card number should be all digits' +); + +select throws_ok ( + $$ update payment_account_card set last_four_digits = '12a4' where payment_account_id = 7 $$, + '23514', 'new row for relation "payment_account_card" violates check constraint "last_four_digits_are_digits"', + 'Last four digits of a credit card number should be all digits' +); + +select throws_ok ( + $$ update payment_account_card set last_four_digits = '123a' where payment_account_id = 7 $$, + '23514', 'new row for relation "payment_account_card" violates check constraint "last_four_digits_are_digits"', + 'Last four digits of a credit card number should be all digits' +); + +select throws_ok ( + $$ update payment_account_card set last_four_digits = '12345' where payment_account_id = 7 $$, + '23514', 'new row for relation "payment_account_card" violates check constraint "last_four_digits_are_digits"', + 'Last four digits of a credit card number should be not have more than four digits' +); + +select throws_ok ( + $$ update payment_account_card set last_four_digits = '123' where payment_account_id = 7 $$, + '23514', 'new row for relation "payment_account_card" violates check constraint "last_four_digits_are_digits"', + 'Last four digits of a credit card number should be not have less than four digits' +); + + +select * +from finish(); + +rollback; + diff --git a/test/payment_account_type.sql b/test/payment_account_type.sql new file mode 100644 index 0000000..1b2ad80 --- /dev/null +++ b/test/payment_account_type.sql @@ -0,0 +1,33 @@ +-- Test payment_account_type +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(15); + +set search_path to numerus, public; + +select has_table('payment_account_type'); +select has_pk('payment_account_type' ); +select table_privs_are('payment_account_type', 'guest', array []::text[]); +select table_privs_are('payment_account_type', 'invoicer', array ['SELECT']); +select table_privs_are('payment_account_type', 'admin', array ['SELECT']); +select table_privs_are('payment_account_type', 'authenticator', array []::text[]); + +select has_column('payment_account_type', 'payment_account_type'); +select col_is_pk('payment_account_type', 'payment_account_type'); +select col_type_is('payment_account_type', 'payment_account_type', 'text'); +select col_not_null('payment_account_type', 'payment_account_type'); +select col_hasnt_default('payment_account_type', 'payment_account_type'); + +select has_column('payment_account_type', 'name'); +select col_type_is('payment_account_type', 'name', 'text'); +select col_not_null('payment_account_type', 'name'); +select col_hasnt_default('payment_account_type', 'name'); + +select * +from finish(); + +rollback; diff --git a/test/payment_account_type_i18n.sql b/test/payment_account_type_i18n.sql new file mode 100644 index 0000000..9bebabc --- /dev/null +++ b/test/payment_account_type_i18n.sql @@ -0,0 +1,44 @@ +-- Test payment_account_type_i18n +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(23); + +set search_path to numerus, public; + +select has_table('payment_account_type_i18n'); +select has_pk('payment_account_type_i18n'); +select col_is_pk('payment_account_type_i18n', array['payment_account_type', 'lang_tag']); +select table_privs_are('payment_account_type_i18n', 'guest', array []::text[]); +select table_privs_are('payment_account_type_i18n', 'invoicer', array ['SELECT']); +select table_privs_are('payment_account_type_i18n', 'admin', array ['SELECT']); +select table_privs_are('payment_account_type_i18n', 'authenticator', array []::text[]); + +select has_column('payment_account_type_i18n', 'payment_account_type'); +select col_is_fk('payment_account_type_i18n', 'payment_account_type'); +select fk_ok('payment_account_type_i18n', 'payment_account_type', 'payment_account_type', 'payment_account_type'); +select col_type_is('payment_account_type_i18n', 'payment_account_type', 'text'); +select col_not_null('payment_account_type_i18n', 'payment_account_type'); +select col_hasnt_default('payment_account_type_i18n', 'payment_account_type'); + +select has_column('payment_account_type_i18n', 'lang_tag'); +select col_is_fk('payment_account_type_i18n', 'lang_tag'); +select fk_ok('payment_account_type_i18n', 'lang_tag', 'language', 'lang_tag'); +select col_type_is('payment_account_type_i18n', 'lang_tag', 'text'); +select col_not_null('payment_account_type_i18n', 'lang_tag'); +select col_hasnt_default('payment_account_type_i18n', 'lang_tag'); + +select has_column('payment_account_type_i18n', 'name'); +select col_type_is('payment_account_type_i18n', 'name', 'text'); +select col_not_null('payment_account_type_i18n', 'name'); +select col_hasnt_default('payment_account_type_i18n', 'name'); + + +select * +from finish(); + +rollback; + diff --git a/test/payment_status.sql b/test/payment_status.sql new file mode 100644 index 0000000..45c9877 --- /dev/null +++ b/test/payment_status.sql @@ -0,0 +1,35 @@ +-- Test payment_status +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(15); + +set search_path to numerus, public; + +select has_table('payment_status'); +select has_pk('payment_status'); +select table_privs_are('payment_status', 'guest', array []::text[]); +select table_privs_are('payment_status', 'invoicer', array ['SELECT']); +select table_privs_are('payment_status', 'admin', array ['SELECT']); +select table_privs_are('payment_status', 'authenticator', array []::text[]); + +select has_column('payment_status', 'payment_status'); +select col_is_pk('payment_status', 'payment_status'); +select col_type_is('payment_status', 'payment_status', 'text'); +select col_not_null('payment_status', 'payment_status'); +select col_hasnt_default('payment_status', 'payment_status'); + +select has_column('payment_status', 'name'); +select col_type_is('payment_status', 'name', 'text'); +select col_not_null('payment_status', 'name'); +select col_hasnt_default('payment_status', 'name'); + + +select * +from finish(); + +rollback; + diff --git a/test/payment_status_i18n.sql b/test/payment_status_i18n.sql new file mode 100644 index 0000000..bb7bc03 --- /dev/null +++ b/test/payment_status_i18n.sql @@ -0,0 +1,44 @@ +-- Test payment_status_i18n +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(23); + +set search_path to numerus, public; + +select has_table('payment_status_i18n'); +select has_pk('payment_status_i18n'); +select col_is_pk('payment_status_i18n', array['payment_status', 'lang_tag']); +select table_privs_are('payment_status_i18n', 'guest', array []::text[]); +select table_privs_are('payment_status_i18n', 'invoicer', array ['SELECT']); +select table_privs_are('payment_status_i18n', 'admin', array ['SELECT']); +select table_privs_are('payment_status_i18n', 'authenticator', array []::text[]); + +select has_column('payment_status_i18n', 'payment_status'); +select col_is_fk('payment_status_i18n', 'payment_status'); +select fk_ok('payment_status_i18n', 'payment_status', 'payment_status', 'payment_status'); +select col_type_is('payment_status_i18n', 'payment_status', 'text'); +select col_not_null('payment_status_i18n', 'payment_status'); +select col_hasnt_default('payment_status_i18n', 'payment_status'); + +select has_column('payment_status_i18n', 'lang_tag'); +select col_is_fk('payment_status_i18n', 'lang_tag'); +select fk_ok('payment_status_i18n', 'lang_tag', 'language', 'lang_tag'); +select col_type_is('payment_status_i18n', 'lang_tag', 'text'); +select col_not_null('payment_status_i18n', 'lang_tag'); +select col_hasnt_default('payment_status_i18n', 'lang_tag'); + +select has_column('payment_status_i18n', 'name'); +select col_type_is('payment_status_i18n', 'name', 'text'); +select col_not_null('payment_status_i18n', 'name'); +select col_hasnt_default('payment_status_i18n', 'name'); + + +select * +from finish(); + +rollback; + diff --git a/test/update_expense_payment_status.sql b/test/update_expense_payment_status.sql new file mode 100644 index 0000000..4fb34a1 --- /dev/null +++ b/test/update_expense_payment_status.sql @@ -0,0 +1,26 @@ +-- Test update_expense_payment_status +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(9); + +set search_path to numerus, public; + +select has_function('numerus', 'update_expense_payment_status', array['integer', 'integer', 'integer']); +select function_lang_is('numerus', 'update_expense_payment_status', array['integer', 'integer', 'integer'], 'sql'); +select function_returns('numerus', 'update_expense_payment_status', array['integer', 'integer', 'integer'], 'void'); +select isnt_definer('numerus', 'update_expense_payment_status', array['integer', 'integer', 'integer']); +select volatility_is('numerus', 'update_expense_payment_status', array['integer', 'integer', 'integer'], 'volatile'); + +select function_privs_are('numerus', 'update_expense_payment_status', array ['integer', 'integer', 'integer'], 'guest', array []::text[]); +select function_privs_are('numerus', 'update_expense_payment_status', array ['integer', 'integer', 'integer'], 'invoicer', array ['EXECUTE']); +select function_privs_are('numerus', 'update_expense_payment_status', array ['integer', 'integer', 'integer'], 'admin', array ['EXECUTE']); +select function_privs_are('numerus', 'update_expense_payment_status', array ['integer', 'integer', 'integer'], 'authenticator', array []::text[]); + +select * +from finish(); + +rollback; diff --git a/verify/add_payment.sql b/verify/add_payment.sql new file mode 100644 index 0000000..4758fca --- /dev/null +++ b/verify/add_payment.sql @@ -0,0 +1,7 @@ +-- Verify numerus:add_payment on pg + +begin; + +select has_function_privilege('numerus.add_payment(integer, integer, date, integer, text, text, numerus.tag_name[])', 'execute'); + +rollback; diff --git a/verify/add_payment_account_bank.sql b/verify/add_payment_account_bank.sql new file mode 100644 index 0000000..a53f0b0 --- /dev/null +++ b/verify/add_payment_account_bank.sql @@ -0,0 +1,7 @@ +-- Verify numerus:add_payment_account_bank on pg + +begin; + +select has_function_privilege('numerus.add_payment_account_bank(integer, text, iban)', 'execute'); + +rollback; diff --git a/verify/add_payment_account_card.sql b/verify/add_payment_account_card.sql new file mode 100644 index 0000000..7c6b59b --- /dev/null +++ b/verify/add_payment_account_card.sql @@ -0,0 +1,7 @@ +-- Verify numerus:add_payment_account_card on pg + +begin; + +select has_function_privilege('numerus.add_payment_account_card(integer, text, text, date)', 'execute'); + +rollback; diff --git a/verify/add_payment_account_cash.sql b/verify/add_payment_account_cash.sql new file mode 100644 index 0000000..6706012 --- /dev/null +++ b/verify/add_payment_account_cash.sql @@ -0,0 +1,7 @@ +-- Verify numerus:add_payment_account_cash on pg + +begin; + +select has_function_privilege('numerus.add_payment_account_cash(integer, text)', 'execute'); + +rollback; diff --git a/verify/add_payment_account_other.sql b/verify/add_payment_account_other.sql new file mode 100644 index 0000000..ff93daa --- /dev/null +++ b/verify/add_payment_account_other.sql @@ -0,0 +1,7 @@ +-- Verify numerus:add_payment_account_other on pg + +begin; + +select has_function_privilege('numerus.add_payment_account_other(integer, text)', 'execute'); + +rollback; diff --git a/verify/available_expense_status.sql b/verify/available_expense_status.sql index b4dab7e..a92f58e 100644 --- a/verify/available_expense_status.sql +++ b/verify/available_expense_status.sql @@ -5,10 +5,13 @@ begin; set search_path to numerus; select 1 / count(*) from expense_status where expense_status = 'pending' and name ='Pending'; +select 1 / count(*) from expense_status where expense_status = 'partial' and name ='Partial'; select 1 / count(*) from expense_status where expense_status = 'paid' and name ='Paid'; select 1 / count(*) from expense_status_i18n where expense_status = 'pending' and name ='Pendent' and lang_tag = 'ca'; select 1 / count(*) from expense_status_i18n where expense_status = 'pending' and name ='Pendiente' and lang_tag = 'es'; +select 1 / count(*) from expense_status_i18n where expense_status = 'partial' and name ='Parcial' and lang_tag = 'ca'; +select 1 / count(*) from expense_status_i18n where expense_status = 'partial' and name ='Parcial' and lang_tag = 'es'; select 1 / count(*) from expense_status_i18n where expense_status = 'paid' and name ='Pagada' and lang_tag= 'ca'; select 1 / count(*) from expense_status_i18n where expense_status = 'paid' and name ='Pagada' and lang_tag= 'es'; diff --git a/verify/available_expense_status@v2.sql b/verify/available_expense_status@v2.sql new file mode 100644 index 0000000..b4dab7e --- /dev/null +++ b/verify/available_expense_status@v2.sql @@ -0,0 +1,15 @@ +-- Verify numerus:available_expense_status on pg + +begin; + +set search_path to numerus; + +select 1 / count(*) from expense_status where expense_status = 'pending' and name ='Pending'; +select 1 / count(*) from expense_status where expense_status = 'paid' and name ='Paid'; + +select 1 / count(*) from expense_status_i18n where expense_status = 'pending' and name ='Pendent' and lang_tag = 'ca'; +select 1 / count(*) from expense_status_i18n where expense_status = 'pending' and name ='Pendiente' and lang_tag = 'es'; +select 1 / count(*) from expense_status_i18n where expense_status = 'paid' and name ='Pagada' and lang_tag= 'ca'; +select 1 / count(*) from expense_status_i18n where expense_status = 'paid' and name ='Pagada' and lang_tag= 'es'; + +rollback; diff --git a/verify/available_payment_account_types.sql b/verify/available_payment_account_types.sql new file mode 100644 index 0000000..671eaad --- /dev/null +++ b/verify/available_payment_account_types.sql @@ -0,0 +1,21 @@ +-- Verify numerus:available_payment_account_types on pg + +begin; + +set search_path to numerus; + +select 1 / count(*) from payment_account_type where payment_account_type = 'bank' and name ='Bank'; +select 1 / count(*) from payment_account_type where payment_account_type = 'card' and name ='Credit Card'; +select 1 / count(*) from payment_account_type where payment_account_type = 'cash' and name ='Cash'; +select 1 / count(*) from payment_account_type where payment_account_type = 'other' and name ='Other'; + +select 1 / count(*) from payment_account_type_i18n where payment_account_type = 'bank' and name ='Banc' and lang_tag = 'ca'; +select 1 / count(*) from payment_account_type_i18n where payment_account_type = 'bank' and name ='Banco' and lang_tag = 'es'; +select 1 / count(*) from payment_account_type_i18n where payment_account_type = 'card' and name ='Targeta de crèdit' and lang_tag= 'ca'; +select 1 / count(*) from payment_account_type_i18n where payment_account_type = 'card' and name ='Tarjeta de crédito' and lang_tag= 'es'; +select 1 / count(*) from payment_account_type_i18n where payment_account_type = 'cash' and name ='Efectiu' and lang_tag= 'ca'; +select 1 / count(*) from payment_account_type_i18n where payment_account_type = 'cash' and name ='Efectivo' and lang_tag= 'es'; +select 1 / count(*) from payment_account_type_i18n where payment_account_type = 'other' and name ='Altres' and lang_tag= 'ca'; +select 1 / count(*) from payment_account_type_i18n where payment_account_type = 'other' and name ='Otros' and lang_tag= 'es'; + +rollback; diff --git a/verify/available_payment_status.sql b/verify/available_payment_status.sql new file mode 100644 index 0000000..55ca632 --- /dev/null +++ b/verify/available_payment_status.sql @@ -0,0 +1,15 @@ +-- Verify numerus:available_payment_status on pg + +BEGIN; + +set search_path to numerus; + +select 1 / count(*) from payment_status where payment_status = 'partial' and name ='Partial'; +select 1 / count(*) from payment_status where payment_status = 'complete' and name ='Complete'; + +select 1 / count(*) from payment_status_i18n where payment_status = 'partial' and name ='Parcial' and lang_tag = 'ca'; +select 1 / count(*) from payment_status_i18n where payment_status = 'partial' and name ='Parcial' and lang_tag = 'es'; +select 1 / count(*) from payment_status_i18n where payment_status = 'complete' and name ='Complet' and lang_tag= 'ca'; +select 1 / count(*) from payment_status_i18n where payment_status = 'complete' and name ='Completo' and lang_tag= 'es'; + +ROLLBACK; diff --git a/verify/edit_payment.sql b/verify/edit_payment.sql new file mode 100644 index 0000000..1a5d49f --- /dev/null +++ b/verify/edit_payment.sql @@ -0,0 +1,7 @@ +-- Verify numerus:edit_payment on pg + +begin; + +select has_function_privilege('numerus.edit_payment(uuid, date, integer, text, text, numerus.tag_name[])', 'execute'); + +rollback; diff --git a/verify/edit_payment_account_bank.sql b/verify/edit_payment_account_bank.sql new file mode 100644 index 0000000..156a09c --- /dev/null +++ b/verify/edit_payment_account_bank.sql @@ -0,0 +1,7 @@ +-- Verify numerus:edit_payment_account_bank on pg + +begin; + +select has_function_privilege('numerus.edit_payment_account_bank(uuid, text, iban)', 'execute'); + +rollback; diff --git a/verify/edit_payment_account_card.sql b/verify/edit_payment_account_card.sql new file mode 100644 index 0000000..d65a5c6 --- /dev/null +++ b/verify/edit_payment_account_card.sql @@ -0,0 +1,7 @@ +-- Verify numerus:edit_payment_account_card on pg + +begin; + +select has_function_privilege('numerus.edit_payment_account_card(uuid, text, text, date)', 'execute'); + +rollback; diff --git a/verify/edit_payment_account_cash.sql b/verify/edit_payment_account_cash.sql new file mode 100644 index 0000000..0e3c6dd --- /dev/null +++ b/verify/edit_payment_account_cash.sql @@ -0,0 +1,7 @@ +-- Verify numerus:edit_payment_account_cash on pg + +begin; + +select has_function_privilege('numerus.edit_payment_account_cash(uuid, text)', 'execute'); + +rollback; diff --git a/verify/edit_payment_account_other.sql b/verify/edit_payment_account_other.sql new file mode 100644 index 0000000..fab65cc --- /dev/null +++ b/verify/edit_payment_account_other.sql @@ -0,0 +1,7 @@ +-- Verify numerus:edit_payment_account_other on pg + +begin; + +select has_function_privilege('numerus.edit_payment_account_other(uuid, text)', 'execute'); + +rollback; diff --git a/verify/expense_payment.sql b/verify/expense_payment.sql new file mode 100644 index 0000000..50918b1 --- /dev/null +++ b/verify/expense_payment.sql @@ -0,0 +1,13 @@ +-- Verify numerus:expense_payment on pg + +begin; + +select expense_id + , payment_id +from numerus.expense_payment +where false; + +select 1 / count(*) from pg_class where oid = 'numerus.expense_payment'::regclass and relrowsecurity; +select 1 / count(*) from pg_policy where polname = 'company_policy' and polrelid = 'numerus.expense_payment'::regclass; + +rollback; diff --git a/verify/payment.sql b/verify/payment.sql new file mode 100644 index 0000000..1ce6514 --- /dev/null +++ b/verify/payment.sql @@ -0,0 +1,22 @@ +-- Verify numerus:payment on pg + +begin; + +select payment_id + , company_id + , slug + , description + , payment_date + , payment_account_id + , amount + , currency_code + , tags + , payment_status + , created_at +from numerus.payment +where false; + +select 1 / count(*) from pg_class where oid = 'numerus.payment'::regclass and relrowsecurity; +select 1 / count(*) from pg_policy where polname = 'company_policy' and polrelid = 'numerus.payment'::regclass; + +rollback; diff --git a/verify/payment_account.sql b/verify/payment_account.sql new file mode 100644 index 0000000..a305944 --- /dev/null +++ b/verify/payment_account.sql @@ -0,0 +1,16 @@ +-- Verify numerus:payment_account on pg + +begin; + +select payment_account_id + , company_id + , slug + , payment_account_type + , name +from numerus.payment_account +where false; + +select 1 / count(*) from pg_class where oid = 'numerus.payment_account'::regclass and relrowsecurity; +select 1 / count(*) from pg_policy where polname = 'company_policy' and polrelid = 'numerus.payment_account'::regclass; + +rollback; diff --git a/verify/payment_account_bank.sql b/verify/payment_account_bank.sql new file mode 100644 index 0000000..802729a --- /dev/null +++ b/verify/payment_account_bank.sql @@ -0,0 +1,14 @@ +-- Verify numerus:payment_account_bank on pg + +begin; + +select payment_account_id + , payment_account_type + , iban +from numerus.payment_account_bank +where false; + +select 1 / count(*) from pg_class where oid = 'numerus.payment_account_bank'::regclass and relrowsecurity; +select 1 / count(*) from pg_policy where polname = 'company_policy' and polrelid = 'numerus.payment_account_bank'::regclass; + +rollback; diff --git a/verify/payment_account_card.sql b/verify/payment_account_card.sql new file mode 100644 index 0000000..a89a6ca --- /dev/null +++ b/verify/payment_account_card.sql @@ -0,0 +1,15 @@ +-- Verify numerus:payment_account_card on pg + +begin; + +select payment_account_id + , payment_account_type + , last_four_digits + , expiration_date +from numerus.payment_account_card +where false; + +select 1 / count(*) from pg_class where oid = 'numerus.payment_account_card'::regclass and relrowsecurity; +select 1 / count(*) from pg_policy where polname = 'company_policy' and polrelid = 'numerus.payment_account_card'::regclass; + +rollback; diff --git a/verify/payment_account_type.sql b/verify/payment_account_type.sql new file mode 100644 index 0000000..928aeb4 --- /dev/null +++ b/verify/payment_account_type.sql @@ -0,0 +1,10 @@ +-- Verify numerus:payment_account_type on pg + +begin; + +select payment_account_type + , name +from numerus.payment_account_type +where false; + +rollback; diff --git a/verify/payment_account_type_i18n.sql b/verify/payment_account_type_i18n.sql new file mode 100644 index 0000000..7be72c2 --- /dev/null +++ b/verify/payment_account_type_i18n.sql @@ -0,0 +1,11 @@ +-- Verify numerus:payment_account_type_i18n on pg + +begin; + +select payment_account_type + , lang_tag + , name +from numerus.payment_account_type_i18n +where false; + +rollback; diff --git a/verify/payment_status.sql b/verify/payment_status.sql new file mode 100644 index 0000000..ea97dd3 --- /dev/null +++ b/verify/payment_status.sql @@ -0,0 +1,10 @@ +-- Verify numerus:payment_status on pg + +begin; + +select payment_status + , name +from numerus.payment_status +where false; + +rollback; diff --git a/verify/payment_status_i18n.sql b/verify/payment_status_i18n.sql new file mode 100644 index 0000000..358cfbf --- /dev/null +++ b/verify/payment_status_i18n.sql @@ -0,0 +1,11 @@ +-- Verify numerus:payment_status_i18n on pg + +begin; + +select payment_status + , lang_tag + , name +from numerus.payment_status_i18n +where false; + +rollback; diff --git a/verify/update_expense_payment_status.sql b/verify/update_expense_payment_status.sql new file mode 100644 index 0000000..9dd16e4 --- /dev/null +++ b/verify/update_expense_payment_status.sql @@ -0,0 +1,7 @@ +-- Verify numerus:update_expense_payment_status on pg + +begin; + +select has_function_privilege('numerus.update_expense_payment_status(integer, integer, integer)', 'execute'); + +rollback; diff --git a/web/template/app.gohtml b/web/template/app.gohtml index c9b6131..1fc1634 100644 --- a/web/template/app.gohtml +++ b/web/template/app.gohtml @@ -54,6 +54,7 @@
  • {{( pgettext "Quotations" "nav" )}}
  • {{( pgettext "Invoices" "nav" )}}
  • {{( pgettext "Expenses" "nav" )}}
  • +
  • {{( pgettext "Payments" "nav" )}}
  • {{( pgettext "Products" "nav" )}}
  • {{( pgettext "Contacts" "nav" )}}
  • diff --git a/web/template/form.gohtml b/web/template/form.gohtml index 578be3a..699d8d0 100644 --- a/web/template/form.gohtml +++ b/web/template/form.gohtml @@ -130,7 +130,9 @@
    {{ .Label }} {{- range $option := .Options }} - + {{- end }} {{- if .Errors }}
      diff --git a/web/template/payments/accounts/edit.gohtml b/web/template/payments/accounts/edit.gohtml new file mode 100644 index 0000000..eff346f --- /dev/null +++ b/web/template/payments/accounts/edit.gohtml @@ -0,0 +1,42 @@ +{{ define "title" -}} + {{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.PaymentAccountForm*/ -}} + {{ printf ( pgettext "Edit Payment Account “%s”" "title" ) .Name }} +{{- end }} + +{{ define "breadcrumbs" -}} + {{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.PaymentAccountForm*/ -}} + +{{- end }} + +{{ define "content" }} + {{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.PaymentAccountForm*/ -}} +
      +

      {{ template "title" . }}

      +
      + {{ csrfToken }} + {{ putMethod }} + + + {{ template "input-field" .Name }} + {{ if eq .Type.Selected "bank" }} + {{ template "input-field" .IBAN }} + {{ else if eq .Type.Selected "card" }} + {{ template "input-field" .LastFourDigits }} + {{ template "input-field" .ExpirationMonthYear }} + {{ end }} + +
      + +
      +
      +
      +{{- end }} diff --git a/web/template/payments/accounts/index.gohtml b/web/template/payments/accounts/index.gohtml new file mode 100644 index 0000000..0103ee6 --- /dev/null +++ b/web/template/payments/accounts/index.gohtml @@ -0,0 +1,56 @@ +{{ define "title" -}} + {{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.PaymentAccountIndexPage*/ -}} + {{( pgettext "Payment Accounts" "title" )}} +{{- end }} + +{{ define "breadcrumbs" -}} + {{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.PaymentAccountIndexPage*/ -}} + +{{- end }} + +{{ define "content" }} + {{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.PaymentAccountIndexPage*/ -}} + + + + + + + + + + + {{ with .Accounts }} + {{- range . }} + + + + + + + {{- end }} + {{ else }} + + + + {{ end }} + +
      {{( pgettext "Name" "title" )}}{{( pgettext "Type" "title" )}}{{( pgettext "Number" "title" )}}{{( pgettext "Expiration Date" "title" )}}
      {{ .Name }}{{ .TypeLabel }} + {{- if eq .Type "bank" -}} + {{ .IBAN }} + {{- else if eq .Type "card" -}} + •••• •••• •••• {{ .LastFourDigits }} + {{- end -}} + + {{ .ExpirationDate }} +
      {{( gettext "No payment accounts added yet." )}}
      +{{- end }} diff --git a/web/template/payments/accounts/new.gohtml b/web/template/payments/accounts/new.gohtml new file mode 100644 index 0000000..58bee8e --- /dev/null +++ b/web/template/payments/accounts/new.gohtml @@ -0,0 +1,45 @@ +{{ define "title" -}} + {{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.PaymentAccountForm*/ -}} + {{( pgettext "New Payment Account" "title" )}} +{{- end }} + +{{ define "breadcrumbs" -}} + {{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.PaymentAccountForm*/ -}} + +{{- end }} + +{{ define "content" }} + {{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.PaymentAccountForm*/ -}} +
      +

      {{ template "title" . }}

      +
      + {{ csrfToken }} + + {{ template "radio-field" .Type }} + {{ template "input-field" .Name }} + + + + +
      + +
      +
      +
      +{{- end }} diff --git a/web/template/payments/edit.gohtml b/web/template/payments/edit.gohtml new file mode 100644 index 0000000..c3ea09d --- /dev/null +++ b/web/template/payments/edit.gohtml @@ -0,0 +1,39 @@ +{{ define "title" -}} + {{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.PaymentForm*/ -}} + {{ printf ( pgettext "Edit Payment “%s”" "title" ) .Description }} +{{- end }} + +{{ define "breadcrumbs" -}} + {{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.PaymentForm*/ -}} + +{{- end }} + +{{ define "content" }} + {{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.PaymentForm*/ -}} +
      +

      {{ template "title" . }}

      +
      + {{ csrfToken }} + {{ putMethod }} + + {{ template "select-field" .PaymentAccount }} + {{ template "input-field" .Description }} + {{ template "input-field" .PaymentDate }} + {{ template "input-field" .Amount }} + {{ template "tags-field" .Tags }} + +
      + +
      +
      +
      +{{- end }} diff --git a/web/template/payments/index.gohtml b/web/template/payments/index.gohtml new file mode 100644 index 0000000..42cbd8e --- /dev/null +++ b/web/template/payments/index.gohtml @@ -0,0 +1,59 @@ +{{ define "title" -}} + {{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.PaymentIndexPage*/ -}} + {{( pgettext "Payments" "title" )}} +{{- end }} + +{{ define "breadcrumbs" -}} + {{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.PaymentIndexPage*/ -}} + +{{- end }} + +{{ define "content" }} + {{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.PaymentIndexPage*/ -}} + + + + + + + + + + + + {{ with .Payments }} + {{- range $payment := . }} + + + + + + + + {{- end }} + {{ else }} + + + + {{ end }} + +
      {{( pgettext "Payment Date" "title" )}}{{( pgettext "Description" "title" )}}{{( pgettext "Status" "title" )}}{{( pgettext "Tags" "title" )}}{{( pgettext "Total" "title" )}}
      {{ .PaymentDate|formatDate }}{{ .Description }}{{ .StatusLabel }} + {{- range $index, $tag := .Tags }} + {{- if gt $index 0 }}, {{ end -}} + {{ . }} + {{- end }} + {{ .Total | formatPrice }}
      {{( gettext "No payments added yet." )}}
      +{{- end }} diff --git a/web/template/payments/new.gohtml b/web/template/payments/new.gohtml new file mode 100644 index 0000000..a8a0e5d --- /dev/null +++ b/web/template/payments/new.gohtml @@ -0,0 +1,37 @@ +{{ define "title" -}} + {{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.PaymentForm*/ -}} + {{( pgettext "New Payment" "title" )}} +{{- end }} + +{{ define "breadcrumbs" -}} + {{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.PaymentForm*/ -}} + +{{- end }} + +{{ define "content" }} + {{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.PaymentForm*/ -}} +
      +

      {{ template "title" . }}

      +
      + {{ csrfToken }} + + {{ template "select-field" .PaymentAccount }} + {{ template "input-field" .Description }} + {{ template "input-field" .PaymentDate }} + {{ template "input-field" .Amount }} + {{ template "tags-field" .Tags }} + + +
      +
      +{{- end }}