diff --git a/deploy/compute_new_expense_amount.sql b/deploy/compute_new_expense_amount.sql index 5d70f59..b751f21 100644 --- a/deploy/compute_new_expense_amount.sql +++ b/deploy/compute_new_expense_amount.sql @@ -14,6 +14,9 @@ $$ declare result new_expense_amount; begin + if trim(subtotal) = '' then + subtotal = '0'; + end if; if array_length(taxes, 1) > 0 then with line as ( select round(parse_price(subtotal, currency.decimal_digits)) as price diff --git a/deploy/parse_price.sql b/deploy/parse_price.sql index 600635c..4e4b7ca 100644 --- a/deploy/parse_price.sql +++ b/deploy/parse_price.sql @@ -24,6 +24,9 @@ begin end if; result := parts[1]::integer; + if result is null then + raise invalid_parameter_value using message = price || ' is not a valid price representation.'; + end if; for d in 1..decimal_digits loop result := result * 10; end loop; diff --git a/deploy/parse_price@v1.sql b/deploy/parse_price@v1.sql new file mode 100644 index 0000000..600635c --- /dev/null +++ b/deploy/parse_price@v1.sql @@ -0,0 +1,53 @@ +-- Deploy numerus:parse_price to pg +-- requires: schema_public + +begin; + +set search_path to numerus, public; + +create or replace function parse_price(price text, decimal_digits integer) returns integer as +$$ +declare + parts text[]; + result int; + frac_part text; + sign int := 1; +begin + if price like '-%' Then + sign := -1; + price := substring(price from 2); + end if; + + parts := string_to_array(price, '.'); + if array_length(parts, 1) > 2 then + raise invalid_parameter_value using message = price || ' is not a valid price representation.'; + end if; + + result := parts[1]::integer; + for d in 1..decimal_digits loop + result := result * 10; + end loop; + + if array_length(parts, 1) = 2 then + frac_part := rtrim(parts[2], '0'); + if length(frac_part) > decimal_digits then + raise invalid_parameter_value using message = price || ' has too many digits in the fraction part.'; + end if; + frac_part := rpad(frac_part, decimal_digits, '0'); + result := result + frac_part::integer; + end if; + + return sign * result; +end; +$$ +language plpgsql +immutable; + +comment on function parse_price(text, integer) is +'Converts the string representation of a price in decimal form to cents, according to the number of decimal digits passed.'; + +revoke execute on function parse_price(text, integer) from public; +grant execute on function parse_price(text, integer) to invoicer; +grant execute on function parse_price(text, integer) to admin; + +commit; diff --git a/revert/parse_price.sql b/revert/parse_price.sql index 2427efa..600635c 100644 --- a/revert/parse_price.sql +++ b/revert/parse_price.sql @@ -1,7 +1,53 @@ --- Revert numerus:parse_price from pg +-- Deploy numerus:parse_price to pg +-- requires: schema_public begin; -drop function if exists numerus.parse_price(text, integer); +set search_path to numerus, public; + +create or replace function parse_price(price text, decimal_digits integer) returns integer as +$$ +declare + parts text[]; + result int; + frac_part text; + sign int := 1; +begin + if price like '-%' Then + sign := -1; + price := substring(price from 2); + end if; + + parts := string_to_array(price, '.'); + if array_length(parts, 1) > 2 then + raise invalid_parameter_value using message = price || ' is not a valid price representation.'; + end if; + + result := parts[1]::integer; + for d in 1..decimal_digits loop + result := result * 10; + end loop; + + if array_length(parts, 1) = 2 then + frac_part := rtrim(parts[2], '0'); + if length(frac_part) > decimal_digits then + raise invalid_parameter_value using message = price || ' has too many digits in the fraction part.'; + end if; + frac_part := rpad(frac_part, decimal_digits, '0'); + result := result + frac_part::integer; + end if; + + return sign * result; +end; +$$ +language plpgsql +immutable; + +comment on function parse_price(text, integer) is +'Converts the string representation of a price in decimal form to cents, according to the number of decimal digits passed.'; + +revoke execute on function parse_price(text, integer) from public; +grant execute on function parse_price(text, integer) to invoicer; +grant execute on function parse_price(text, integer) to admin; commit; diff --git a/revert/parse_price@v1.sql b/revert/parse_price@v1.sql new file mode 100644 index 0000000..2427efa --- /dev/null +++ b/revert/parse_price@v1.sql @@ -0,0 +1,7 @@ +-- Revert numerus:parse_price from pg + +begin; + +drop function if exists numerus.parse_price(text, integer); + +commit; diff --git a/sqitch.plan b/sqitch.plan index 1a4cff4..bea157e 100644 --- a/sqitch.plan +++ b/sqitch.plan @@ -126,3 +126,4 @@ invoice_attachment [schema_numerus roles invoice] 2023-07-12T17:10:58Z jordi fit attach_to_invoice [schema_numerus roles invoice invoice_attachment] 2023-07-12T17:21:19Z jordi fita mas # Add function to attachment a document to invoices 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 diff --git a/test/compute_new_expense_amount.sql b/test/compute_new_expense_amount.sql index 5a63387..a04ca07 100644 --- a/test/compute_new_expense_amount.sql +++ b/test/compute_new_expense_amount.sql @@ -5,7 +5,7 @@ reset client_min_messages; begin; -select plan(14); +select plan(15); set search_path to numerus, auth, public; @@ -54,6 +54,11 @@ select is( '("{}",0.00)'::new_expense_amount ); +select is( + compute_new_expense_amount(1, '', array[2,5,3]::integer[]), + '("{{IRPF -15 %,0.00},{IVA 4 %,0.00},{IVA 21 %,0.00}}",0.00)'::new_expense_amount +); + select is( compute_new_expense_amount(1, '4.60', array[2,5,3]), '("{{IRPF -15 %,-0.69},{IVA 4 %,0.18},{IVA 21 %,0.97}}",5.06)'::new_expense_amount diff --git a/test/parse_price.sql b/test/parse_price.sql index 9c42d98..3d8ec54 100644 --- a/test/parse_price.sql +++ b/test/parse_price.sql @@ -5,7 +5,7 @@ reset client_min_messages; begin; -select plan(42); +select plan(44); set search_path to auth, numerus, public; @@ -52,6 +52,8 @@ select is( parse_price('00000000000123456.789000000000000000000', 3), 123456789 select throws_ok( $$ select parse_price('1,1', 2) $$ ); select throws_ok( $$ select parse_price('1.1.1', 2) $$ ); select throws_ok( $$ select parse_price('a.b', 2) $$ ); +select throws_ok( $$ select parse_price('', 1) $$); +select throws_ok( $$ select parse_price(' ', 3) $$); select * from finish(); diff --git a/verify/parse_price@v1.sql b/verify/parse_price@v1.sql new file mode 100644 index 0000000..b9d9b95 --- /dev/null +++ b/verify/parse_price@v1.sql @@ -0,0 +1,7 @@ +-- Verify numerus:parse_price on pg + +begin; + +select has_function_privilege('numerus.parse_price(text, integer)', 'execute'); + +rollback;