Package PGSQL extension with Spanish VAT check

This commit is contained in:
jordi fita mas 2023-01-21 19:31:50 +01:00
commit a2069e102c
9 changed files with 497 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
*.o
*.so
regression.*
results/

12
Makefile Normal file
View File

@ -0,0 +1,12 @@
MODULE_big = vat
OBJS = vatin.o es.o
EXTENSION = vat
DATA = vat--0.0.sql
PGFILEDESC = "vat - data type for VAT identification numbers"
REGRESS = vat
PG_CONFIG := pg_config
PGXS := $(shell $(PG_CONFIG) --pgxs)
include $(PGXS)

148
es.c Normal file
View File

@ -0,0 +1,148 @@
/*
* SPDX-FileCopyrightText: 2023 jordi fita mas <jfita@peritasoft.com>
*
* SPDX-License-Identifier: GPL-3.0-only
*/
#include "es.h"
#include <stdbool.h>
#include <ctype.h>
#include <postgres.h>
#include <utils/palloc.h>
static char
compute_dni_check_digit(const char *start, const char *end)
{
unsigned long int n = 0;
const char *p = start;
for(; p != end; p++) {
n = n * 10 + *p - 48;
}
return "TRWAGMYFPDXBNJZSQVHLCKE"[n % 23];
}
static bool
valid_short_dni(const char *dni) {
return dni[8] == compute_dni_check_digit(dni + 1, dni + 8);
}
static bool
valid_dni(const char *dni) {
return dni[8] == compute_dni_check_digit(dni, dni + 8);
}
static bool
valid_nie(char *nie) {
bool valid = false;
nie[0] -= 'X' - 48;
valid = nie[8] == compute_dni_check_digit(nie, nie + 8);
nie[0] += 'X' - 48;
return valid;
}
static bool
valid_cif(const char *cif) {
int odds = 0;
int evens = 0;
int n = 0;
int checksum;
for (n = 1; n < 8; n++) {
if (n % 2 == 0) {
evens += cif[n] - 48;
} else {
int d = cif[n] * 2;
int a = d / 10;
int b = d % 10;
odds += a + b;
}
}
checksum = 10 - (odds + evens) % 10;
return cif[8] == checksum + 48 || cif[8] == "JABCDEFGHI"[checksum];
}
char *
check_es_vat(const char *str, size_t len) {
const char *p = str;
const char *end = p + len;
char buf[10] = { 0 };
int n = 0;
if (len < 9 ) {
return NULL;
}
for (; p != end && n < 10; p++) {
char c = *p;
if (c == ' ' || c == '-') continue;
if (n > 0 && n < 8) {
if (!isdigit(c)) {
return NULL;
}
buf[n] = c;
} else {
buf[n] = toupper(c);
}
n++;
}
if (n == 10) {
return NULL;
}
buf[9] = 0;
switch (buf[0]) {
case 'K':
case 'L':
case 'M':
if (!valid_short_dni(buf)) {
return NULL;
}
break;
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
if (!valid_dni(buf)) {
return NULL;
}
break;
case 'X':
case 'Y':
case 'Z':
if (!valid_nie(buf)) {
return NULL;
}
break;
case 'A':
case 'B':
case 'C':
case 'D':
case 'E':
case 'F':
case 'G':
case 'H':
case 'J':
case 'N':
case 'P':
case 'Q':
case 'R':
case 'S':
case 'U':
case 'V':
case 'W':
if (!valid_cif(buf)) {
return NULL;
}
break;
default:
return NULL;
}
return pstrdup(buf);
}

21
es.h Normal file
View File

@ -0,0 +1,21 @@
/*
* SPDX-FileCopyrightText: 2023 jordi fita mas <jfita@peritasoft.com>
*
* SPDX-License-Identifier: GPL-3.0-only
*/
#ifndef VAT_ES_H
#define VAT_ES_H 1
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
char * check_es_vat(const char *str, size_t len);
#ifdef __cplusplus
}
#endif
#endif /* VAT_ES_H */

31
expected/vat.out Normal file
View File

@ -0,0 +1,31 @@
--
-- Test VATIN extensions
--
CREATE EXTENSION vat;
-- Test valid conversions
SELECT 'ES40404040D'::VATIN,
'ES40404040-D'::VATIN,
' esx0523821l '::VATIN,
'ESM0243487d'::VATIN,
' ESb17616756 '::VATIN
;
vatin | vatin | vatin | vatin | vatin
-------------+-------------+-------------+-------------+-------------
ES40404040D | ES40404040D | ESX0523821L | ESM0243487D | ESB17616756
(1 row)
-- Test invalid checksums
SELECT 'ES40414040D'::VATIN;
ERROR: invalid input syntax for VAT number: "ES40414040D"
LINE 1: SELECT 'ES40414040D'::VATIN;
^
SELECT 'ESM0243487F'::VATIN;
ERROR: invalid input syntax for VAT number: "ESM0243487F"
LINE 1: SELECT 'ESM0243487F'::VATIN;
^
SELECT 'ESB17616757'::VATIN;
ERROR: invalid input syntax for VAT number: "ESB17616757"
LINE 1: SELECT 'ESB17616757'::VATIN;
^
-- Cleanup
DROP EXTENSION vat;

21
sql/vat.sql Normal file
View File

@ -0,0 +1,21 @@
--
-- Test VATIN extensions
--
CREATE EXTENSION vat;
-- Test valid conversions
SELECT 'ES40404040D'::VATIN,
'ES40404040-D'::VATIN,
' esx0523821l '::VATIN,
'ESM0243487d'::VATIN,
' ESb17616756 '::VATIN
;
-- Test invalid checksums
SELECT 'ES40414040D'::VATIN;
SELECT 'ESM0243487F'::VATIN;
SELECT 'ESB17616757'::VATIN;
-- Cleanup
DROP EXTENSION vat;

187
vat--0.0.sql Normal file
View File

@ -0,0 +1,187 @@
/* vatin--0.0.sql */
-- complain if script is sources in psql, rather than via CREATE EXTENSION
\echo Use "CREATE EXTENSION vat" to load this file. \quit
-- Example:
-- create table test ( id vatin );
-- insert into tast values('ES40404040D');
--
-- select vatin('ES40404040-D');
--
--
-- Input and output functions and data types:
--
---------------------------------------------
CREATE FUNCTION vatin_in(cstring)
RETURNS vatin
AS 'MODULE_PATHNAME'
LANGUAGE C
IMMUTABLE STRICT
PARALLEL SAFE
;
CREATE FUNCTION vatin_out(vatin)
RETURNS cstring
AS 'textout'
LANGUAGE 'internal'
IMMUTABLE STRICT
PARALLEL SAFE
;
CREATE TYPE vatin (
INPUT = vatin_in,
OUTPUT = vatin_out,
LIKE = pg_catalog.text
);
COMMENT ON TYPE vatin
IS 'Value added tax identification number (VATIN)'
;
--
-- Operator functions:
--
---------------------------------------------
CREATE FUNCTION vatinlt(vatin, vatin)
RETURNS boolean
AS 'text_lt'
LANGUAGE 'internal'
IMMUTABLE STRICT
PARALLEL SAFE
;
CREATE FUNCTION vatinle(vatin, vatin)
RETURNS boolean
AS 'text_le'
LANGUAGE 'internal'
IMMUTABLE STRICT
PARALLEL SAFE
;
CREATE FUNCTION vatineq(vatin, vatin)
RETURNS boolean
AS 'texteq'
LANGUAGE 'internal'
IMMUTABLE STRICT
PARALLEL SAFE;
CREATE FUNCTION vatinge(vatin, vatin)
RETURNS boolean
AS 'text_ge'
LANGUAGE 'internal'
IMMUTABLE STRICT
PARALLEL SAFE
;
CREATE FUNCTION vatingt(vatin, vatin)
RETURNS boolean
AS 'text_gt'
LANGUAGE 'internal'
IMMUTABLE STRICT
PARALLEL SAFE
;
CREATE FUNCTION vatinne(vatin, vatin)
RETURNS boolean
AS 'textne'
LANGUAGE 'internal'
IMMUTABLE STRICT
PARALLEL SAFE
;
--
-- Operators:
--
---------------------------------------------
CREATE OPERATOR < (
PROCEDURE = vatinlt,
LEFTARG = vatin,
RIGHTARG = vatin,
COMMUTATOR = >,
NEGATOR = >=,
RESTRICT = scalarltsel,
JOIN = scalarltjoinsel
);
CREATE OPERATOR <= (
PROCEDURE = vatinle,
LEFTARG = vatin,
RIGHTARG = vatin,
COMMUTATOR = >=,
NEGATOR = >,
RESTRICT = scalarltsel,
JOIN = scalarltjoinsel
);
CREATE OPERATOR = (
PROCEDURE = vatineq,
LEFTARG = vatin,
RIGHTARG = vatin,
COMMUTATOR = =,
NEGATOR = <>,
RESTRICT = eqsel,
JOIN = eqjoinsel,
MERGES,
HASHES
);
CREATE OPERATOR >= (
PROCEDURE = vatinge,
LEFTARG = vatin,
RIGHTARG = vatin,
COMMUTATOR = <=,
NEGATOR = <,
RESTRICT = scalargtsel,
JOIN = scalargtjoinsel
);
CREATE OPERATOR > (
PROCEDURE = vatingt,
LEFTARG = vatin,
RIGHTARG = vatin,
COMMUTATOR = <,
NEGATOR = <=,
RESTRICT = scalargtsel,
JOIN = scalargtjoinsel
);
CREATE OPERATOR <> (
PROCEDURE = vatinne,
LEFTARG = vatin,
RIGHTARG = vatin,
COMMUTATOR = <>,
NEGATOR = =,
RESTRICT = neqsel,
JOIN = neqjoinsel
);
--
-- Operator families for the various operator classes:
--
---------------------------------------------
CREATE OPERATOR FAMILY vatin_ops USING btree;
CREATE OPERATOR FAMILY vatin_ops USING hash;
--
-- Operator classes:
--
---------------------------------------------
CREATE FUNCTION btvatincmp(vatin, vatin)
RETURNS int4
AS 'bttextcmp'
LANGUAGE 'internal'
IMMUTABLE STRICT
PARALLEL SAFE
;
CREATE OPERATOR CLASS vatin_ops DEFAULT
FOR TYPE vatin USING btree FAMILY vatin_ops AS
OPERATOR 1 <,
OPERATOR 2 <=,
OPERATOR 3 =,
OPERATOR 4 >=,
OPERATOR 5 >,
FUNCTION 1 btvatincmp(vatin, vatin)
;
CREATE FUNCTION hashvatin(vatin)
RETURNS int4
AS 'hashtext'
LANGUAGE 'internal' IMMUTABLE STRICT
PARALLEL SAFE
;
CREATE OPERATOR CLASS vatin_ops DEFAULT
FOR TYPE vatin USING hash FAMILY vatin_ops AS
OPERATOR 1 =,
FUNCTION 1 hashvatin(vatin)
;

5
vat.control Normal file
View File

@ -0,0 +1,5 @@
comment = 'data type for VAT registration numbers'
default_version = '0.0'
module_pathname = '$libdir/vat'
relocatable = true
superuser = false

68
vatin.c Normal file
View File

@ -0,0 +1,68 @@
/*
* SPDX-FileCopyrightText: 2023 jordi fita mas <jfita@peritasoft.com>
*
* SPDX-License-Identifier: GPL-3.0-only
*/
#include <postgres.h>
#include <fmgr.h>
#include <utils/builtins.h>
#include "es.h"
PG_MODULE_MAGIC;
static bool
string2vatin(const char *str, text **result) {
const char *r = str;
char *vatin = NULL;
size_t len;
char country[2];
int i;
for (; *r == ' '; r++);
if (*r == 0) {
goto invalidvatin;
}
len = strlen(r);
for (; *(r + len - 1) == ' '; len--);
if (len < 3) {
goto invalidvatin;
}
for(i = 0; i < 2; i++, r++, len--) {
country[i] = toupper(*r);
}
if (country[0] == 'E' && country[1] == 'S') {
vatin = check_es_vat(r, len);
if (vatin == NULL) {
goto invalidvatin;
}
} else {
// For countries without validation yet assume it is correct
vatin = pstrdup(r);
}
*result = (text *)palloc(2 + len + VARHDRSZ);
SET_VARSIZE(*result, 2 + len + VARHDRSZ);
memcpy(VARDATA(*result), country, 2);
memcpy(VARDATA(*result) + 2, vatin, len);
pfree(vatin);
return true;
invalidvatin:
ereport(ERROR, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("invalid input syntax for VAT number: \"%s\"", str)));
return false;
}
PG_FUNCTION_INFO_V1(vatin_in);
Datum
vatin_in(PG_FUNCTION_ARGS)
{
const char *str = PG_GETARG_CSTRING(0);
text *result = NULL;
if (!string2vatin(str, &result)) {
PG_RETURN_NULL();
}
PG_RETURN_TEXT_P(result);
}