feat(finance): ajouter le socle métier bancaire canonique

This commit is contained in:
fy59 2026-08-02 11:17:12 +02:00
parent 116b08f2f4
commit e97b35a4df
13 changed files with 704 additions and 0 deletions

3
.gitignore vendored
View file

@ -20,6 +20,9 @@ compile_commands.json
/AGENTS.md
/prompt.md
# Copies locales des tickets Forgejo
/ticket[0-9]*.md
# OpenCode local
/opencode.json
/.opencode/

View file

@ -175,6 +175,7 @@ TEST_PERSON_CREATION_COORDINATOR := tests/test_person_creation_coordinator
TEST_EML_ANALYZER := tests/test_eml_analyzer
TEST_EML_INTEGRATION := tests/test_eml_integration
TEST_IBAN_ANALYZER := tests/test_iban_analyzer
TEST_FINANCIAL_FOUNDATION := tests/test_financial_foundation
TEST_EXIFTOOL_METADATA := tests/test_exiftool_metadata
TEST_PDF_PASSWORD_RECOVERY := tests/test_pdf_password_recovery
TEST_EXTRACTION_DROP_SERVICE := tests/test_extraction_drop_service
@ -1130,6 +1131,12 @@ $(TEST_EML_INTEGRATION): tests/test_eml_integration.c \
$(TEST_IBAN_ANALYZER): tests/test_iban_analyzer.c src/core/iban_analyzer.c
$(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS)
$(TEST_FINANCIAL_FOUNDATION): tests/test_financial_foundation.c \
src/models/financial_foundation.c src/core/bank_text_extractor.c \
src/core/bic_validator.c src/core/french_bban.c \
src/core/iban_analyzer.c src/core/bank_proposal.c src/core/controlled_vocab.c
$(CC) $(TEST_CFLAGS) -Wpedantic $^ -o $@ $(TEST_LDFLAGS)
$(TEST_EXIFTOOL_METADATA): tests/test_exiftool_metadata.c \
src/core/exiftool_metadata.c
$(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS)
@ -1238,6 +1245,7 @@ test: \
$(TEST_EML_ANALYZER) \
$(TEST_EML_INTEGRATION) \
$(TEST_IBAN_ANALYZER) \
$(TEST_FINANCIAL_FOUNDATION) \
$(TEST_EXIFTOOL_METADATA) \
$(TEST_PDF_PASSWORD_RECOVERY) \
$(TEST_EXTRACTION_DROP_SERVICE) \
@ -1339,6 +1347,7 @@ test: \
@$(TEST_EML_ANALYZER)
@$(TEST_EML_INTEGRATION)
@$(TEST_IBAN_ANALYZER)
@$(TEST_FINANCIAL_FOUNDATION)
@$(TEST_EXIFTOOL_METADATA)
@$(TEST_PDF_PASSWORD_RECOVERY)
@$(TEST_EXTRACTION_DROP_SERVICE)
@ -1451,6 +1460,7 @@ clean:
$(TEST_RELATION_TYPE_SERVICE) \
$(TEST_CONTROLLED_VOCAB) \
$(TEST_BANK_PROPOSAL) \
$(TEST_FINANCIAL_FOUNDATION) \
$(TEST_EML_PIPELINE_TASK) \
$(TEST_EML_MIME_EXTRACTOR) \
$(TEST_EXIFTOOL_ANALYSIS) \

View file

@ -0,0 +1,8 @@
#ifndef LABFY_INVESTIGATION_BANK_TEXT_EXTRACTOR_H
#define LABFY_INVESTIGATION_BANK_TEXT_EXTRACTOR_H
#include "models/financial_foundation.h"
G_BEGIN_DECLS
/** Extrait des BankCandidate possédés. Les offsets sont des octets UTF-8. */
GPtrArray *bank_text_extractor_extract(const char *text, GError **error);
G_END_DECLS
#endif

View file

@ -0,0 +1,18 @@
#ifndef LABFY_INVESTIGATION_BIC_VALIDATOR_H
#define LABFY_INVESTIGATION_BIC_VALIDATOR_H
#include <glib.h>
G_BEGIN_DECLS
typedef enum
{
BIC_VALIDATOR_ABSENT,
BIC_VALIDATOR_VALID,
BIC_VALIDATOR_INVALID
} BicValidatorResult;
/** Retourne une copie majuscule, ou NULL pour une valeur absente/invalide. */
char *bic_validator_normalize(const char *bic);
/** Distingue explicitement absence, validité syntaxique et invalidité. */
BicValidatorResult bic_validator_check(const char *bic);
/** Validation syntaxique ISO 9362 uniquement ; une absence retourne FALSE. */
gboolean bic_validator_validate(const char *bic);
G_END_DECLS
#endif

View file

@ -0,0 +1,7 @@
#ifndef LABFY_INVESTIGATION_FRENCH_BBAN_H
#define LABFY_INVESTIGATION_FRENCH_BBAN_H
#include <glib.h>
typedef struct { char *bank_code; char *branch_code; char *account_number; char *rib_key; } FrenchBban;
FrenchBban *french_bban_derive(const char *normalized_iban);
void french_bban_free(FrenchBban *bban);
#endif

View file

@ -6,8 +6,27 @@
#define LABFY_INVESTIGATION_IBAN_ANALYZER_H
#include <glib.h>
G_BEGIN_DECLS
typedef enum
{
IBAN_ANALYZER_RESULT_VALID,
IBAN_ANALYZER_RESULT_INVALID,
IBAN_ANALYZER_RESULT_UNSUPPORTED_COUNTRY
} IbanAnalyzerResult;
/** @brief Normalise un IBAN en majuscules sans séparateurs. */
char *iban_analyzer_normalize(const char *text);
/** Normalise uniquement espaces ASCII, tabulations, CR/LF et tirets. */
char *iban_analyzer_normalize_canonical(const char *text);
/**
* Vérifie format, longueur nationale prise en charge et MOD-97.
*
* La table volontairement partielle de cette tranche couvre BE, DE, ES, FR,
* GB, IT, LU, NL et PT. Un code pays absent ne peut jamais être déclaré
* valide : iban_analyzer_validate_canonical_result() retourne alors
* IBAN_ANALYZER_RESULT_UNSUPPORTED_COUNTRY.
*/
gboolean iban_analyzer_validate_canonical(const char *iban);
IbanAnalyzerResult iban_analyzer_validate_canonical_result(const char *iban);
/** @brief Vérifie la structure et la clé de contrôle modulo 97. */
gboolean iban_analyzer_validate(const char *iban);
/**

View file

@ -0,0 +1,122 @@
#ifndef LABFY_INVESTIGATION_FINANCIAL_FOUNDATION_H
#define LABFY_INVESTIGATION_FINANCIAL_FOUNDATION_H
#include <glib.h>
G_BEGIN_DECLS
#define FINANCIAL_TEXT_MAX_BYTES (1024U * 1024U)
#define FINANCIAL_CANDIDATE_MAX_RESULTS 4096U
typedef enum { BANK_VALIDATION_VALID, BANK_VALIDATION_INVALID,
BANK_VALIDATION_AMBIGUOUS, BANK_VALIDATION_UNVERIFIABLE,
BANK_VALIDATION_COUNT } BankValidationState;
typedef enum { BANK_DECISION_PROPOSED, BANK_DECISION_CONFIRMED,
BANK_DECISION_REJECTED, BANK_DECISION_COUNT } BankHumanDecision;
typedef enum { BANK_ORIGIN_AUTOMATIC, BANK_ORIGIN_HUMAN,
BANK_ORIGIN_LEGACY, BANK_ORIGIN_COUNT } BankOrigin;
typedef enum { BANK_ACCOUNT_STATE_UNKNOWN, BANK_ACCOUNT_STATE_ACTIVE,
BANK_ACCOUNT_STATE_CLOSED } BankAccountState;
/* Contrat d'ownership commun : tous les constructeurs copient les chaînes
* reçues. Tous les champs pointeurs des structures retournées sont possédés
* par la structure et libérés par sa fonction *_free(), qui accepte NULL.
* Les champs bruts d'une observation ou d'un candidat sont immuables par
* contrat ; une correction appartient à BankObservationRevision. */
/* Les conversions retournent "unknown" pour toute valeur hors enum. */
const char *bank_validation_state_to_code(BankValidationState state);
const char *bank_human_decision_to_code(BankHumanDecision decision);
const char *bank_origin_to_code(BankOrigin origin);
typedef struct {
char *source_text; char *raw_iban; char *normalized_iban;
char *raw_bic; char *normalized_bic; char *declared_holder_raw;
char *declared_institution; char *declared_address;
gsize start_offset; gsize end_offset; BankValidationState validation;
GPtrArray *warnings; BankOrigin origin; BankHumanDecision decision;
gboolean proposed_grouping; char *proposed_group_identifier;
} BankCandidate;
BankCandidate *bank_candidate_new(const char *source_text, const char *raw_iban,
gsize start_offset, gsize end_offset, GError **error);
void bank_candidate_free(BankCandidate *candidate);
typedef struct {
char *identifier; char *evidence_identifier; char *evidence_sha256;
char *raw_value; char *source_type; char *extraction_method;
char *source_page_or_image; char *ocr_run_identifier;
gboolean has_region; double region_x; double region_y;
double region_width; double region_height; BankOrigin origin;
char *created_at_utc; char *factual_notes;
} BankObservation;
BankObservation *bank_observation_new(const char *identifier,
const char *evidence_identifier, const char *evidence_sha256,
const char *raw_value, const char *source_type,
const char *extraction_method, BankOrigin origin,
const char *created_at_utc, GError **error);
void bank_observation_free(BankObservation *observation);
typedef struct {
char *observation_identifier; char *corrected_iban; char *normalized_iban;
char *corrected_bic; char *normalized_bic; char *corrected_holder;
BankValidationState validation; BankOrigin origin;
char *created_at_utc; char *factual_notes;
} BankObservationRevision;
BankObservationRevision *bank_observation_revision_new(
const char *observation_identifier, const char *corrected_iban,
const char *normalized_iban, BankValidationState validation,
const char *created_at_utc, GError **error);
void bank_observation_revision_free(BankObservationRevision *revision);
typedef struct {
char *entity_identifier; char *normalized_iban; char *country_code;
char *bic; char *declared_institution; BankAccountState state;
char *factual_notes;
} BankAccount;
BankAccount *bank_account_new(const char *entity_identifier,
const char *normalized_iban, const char *bic,
const char *declared_institution, BankAccountState state, GError **error);
void bank_account_free(BankAccount *account);
typedef struct {
char *declared_name; char *evidence_identifier;
char *observation_identifier; BankOrigin origin; char *factual_notes;
} DeclaredBankHolder;
DeclaredBankHolder *declared_bank_holder_new(const char *declared_name,
const char *evidence_identifier, const char *observation_identifier,
BankOrigin origin, GError **error);
void declared_bank_holder_free(DeclaredBankHolder *holder);
typedef struct {
char *transaction_type; char *raw_date; char *normalized_date;
char *amount_decimal; char *currency; char *observed_status;
char *bank_reference; char *reference_type; char *uetr;
char *debtor_account_identifier; char *creditor_account_identifier;
char *declared_payer; char *declared_beneficiary;
char *source_institution; char *evidence_identifier;
char *observation_identifier; BankOrigin origin;
char *factual_notes; char *created_at_utc;
} FinancialTransaction;
/* amount_decimal est une chaîne ASCII validée : signe facultatif, chiffres,
* point décimal facultatif. Elle préserve une valeur exacte sans flottant. */
FinancialTransaction *financial_transaction_new(const char *transaction_type,
const char *raw_date, const char *amount_decimal, const char *currency,
const char *observed_status, const char *evidence_identifier,
const char *created_at_utc, GError **error);
void financial_transaction_free(FinancialTransaction *transaction);
typedef struct {
GPtrArray *candidates; BankHumanDecision decision;
gboolean create_account; gboolean reuse_account; gboolean create_person;
gboolean create_relation; gboolean create_transaction;
} FinancialReviewModel;
/* candidates est possédé et détruit avec ses BankCandidate. Les options de
* création restent FALSE jusqu'à une action humaine explicite. */
FinancialReviewModel *financial_review_model_new(void);
void financial_review_model_free(FinancialReviewModel *model);
G_END_DECLS
#endif

View file

@ -0,0 +1,103 @@
#include "core/bank_text_extractor.h"
#include "core/bic_validator.h"
#include "core/iban_analyzer.h"
#include <string.h>
static char *extract_label(const char *block,const char *labels)
{
char *pattern=g_strdup_printf("(?im)^(?:%s)[ \\t]*:[ \\t]*(.+)$",labels);
GRegex *regex=g_regex_new(pattern,0,0,NULL);GMatchInfo *match=NULL;char *value=NULL;
g_regex_match(regex,block,0,&match);if(g_match_info_matches(match))value=g_match_info_fetch(match,1);
if(value!=NULL)
g_strstrip(value);
g_match_info_free(match);g_regex_unref(regex);g_free(pattern);return value;
}
static char *candidate_block(const char *text,gsize start,gsize end)
{
gsize left=start,right=end,length=strlen(text);
while(left>0&&start-left<4096U&&!(text[left-1]=='\n'&&left>1&&text[left-2]=='\n'))left--;
while(right<length&&right-end<4096U&&!(text[right]=='\n'&&right+1<length&&text[right+1]=='\n'))right++;
return g_strndup(text+left,right-left);
}
static gboolean has_ocr_confusion(const char *raw)
{
char *compact=iban_analyzer_normalize(raw);gboolean ambiguous=compact!=NULL&&
strlen(compact)>=4&&(!g_ascii_isdigit(compact[2])||!g_ascii_isdigit(compact[3]));
g_free(compact);return ambiguous;
}
static void trim_at_first_valid_iban(char **raw,gint *end)
{
gsize length=strlen(*raw);
for(gsize size=15;size<=length;size++) {
unsigned char next=(unsigned char)(*raw)[size];
if(next!='\0'&&g_ascii_isalnum(next))continue;
char *prefix=g_strndup(*raw,size);
if(iban_analyzer_validate_canonical(prefix)) {
g_free(*raw);*raw=prefix;*end-=((gint)length-(gint)size);return;
}
g_free(prefix);
}
}
static void enrich_candidate(BankCandidate *candidate,const char *text)
{
char *block=candidate_block(text,candidate->start_offset,candidate->end_offset);
g_free(candidate->source_text);candidate->source_text=g_strdup(block);
candidate->declared_holder_raw=extract_label(block,"Titulaire|Holder|Payeur|Bénéficiaire");
candidate->declared_institution=extract_label(block,"Banque|Bank|Établissement");
candidate->declared_address=extract_label(block,"Adresse(?: de la banque)?|Bank address");
candidate->raw_bic=extract_label(block,"BIC|SWIFT");
candidate->normalized_bic=bic_validator_normalize(candidate->raw_bic);
if(candidate->raw_bic!=NULL&&candidate->normalized_bic==NULL)
g_ptr_array_add(candidate->warnings,g_strdup("BIC syntaxiquement invalide."));
g_free(block);
}
GPtrArray *bank_text_extractor_extract(const char *text,GError **error)
{
GPtrArray *results=g_ptr_array_new_with_free_func((GDestroyNotify)bank_candidate_free);
if(text==NULL||text[0]=='\0')return results;
if(strlen(text)>FINANCIAL_TEXT_MAX_BYTES){g_set_error_literal(error,
g_quark_from_static_string("bank-text-extractor-error"),1,"Le texte bancaire dépasse la limite autorisée.");return results;}
GRegex *regex=g_regex_new("(?i)\\b[A-Z]{2}[0-9OIl]{2}(?:[ \\t\\r\\n-]*[A-Z0-9OIl]){3,30}\\b",
G_REGEX_OPTIMIZE,0,NULL);GMatchInfo *matches=NULL;g_regex_match(regex,text,0,&matches);
while(g_match_info_matches(matches)&&results->len<FINANCIAL_CANDIDATE_MAX_RESULTS) {
gint start=0,end=0;g_match_info_fetch_pos(matches,0,&start,&end);
char *raw=g_match_info_fetch(matches,0);trim_at_first_valid_iban(&raw,&end);BankCandidate *candidate=
bank_candidate_new(text,raw,(gsize)start,(gsize)end,NULL);
candidate->normalized_iban=iban_analyzer_normalize_canonical(raw);
if(has_ocr_confusion(raw)) {
candidate->validation=BANK_VALIDATION_AMBIGUOUS;
g_clear_pointer(&candidate->normalized_iban,g_free);
g_ptr_array_add(candidate->warnings,g_strdup("Confusion OCR possible, sans correction automatique."));
} else if(candidate->normalized_iban==NULL)
candidate->validation=BANK_VALIDATION_UNVERIFIABLE;
else {
IbanAnalyzerResult validation=iban_analyzer_validate_canonical_result(
candidate->normalized_iban);
if(validation==IBAN_ANALYZER_RESULT_VALID)
candidate->validation=BANK_VALIDATION_VALID;
else if(validation==IBAN_ANALYZER_RESULT_UNSUPPORTED_COUNTRY) {
candidate->validation=BANK_VALIDATION_UNVERIFIABLE;
g_ptr_array_add(candidate->warnings,g_strdup(
"Pays IBAN non pris en charge."));
} else candidate->validation=BANK_VALIDATION_INVALID;
}
enrich_candidate(candidate,text);g_ptr_array_add(results,candidate);g_free(raw);
if(!g_match_info_next(matches,NULL))break;
}
g_match_info_free(matches);g_regex_unref(regex);
regex=g_regex_new("(?im)^IBAN[ \\t]*:[ \\t]*([^\\r\\n]{8,80})",0,0,NULL);
g_regex_match(regex,text,0,&matches);
while(g_match_info_matches(matches)&&results->len<FINANCIAL_CANDIDATE_MAX_RESULTS) {
gint start=0,end=0;char *raw=g_match_info_fetch(matches,1);
g_match_info_fetch_pos(matches,1,&start,&end);gboolean non_ascii=FALSE;
for(const unsigned char *p=(const unsigned char *)raw;*p;p++)if(*p>=128)non_ascii=TRUE;
if(non_ascii) {
BankCandidate *candidate=bank_candidate_new(text,raw,(gsize)start,(gsize)end,NULL);
candidate->validation=BANK_VALIDATION_AMBIGUOUS;
g_ptr_array_add(candidate->warnings,g_strdup("Caractère Unicode non normalisé."));
enrich_candidate(candidate,text);g_ptr_array_add(results,candidate);
}
g_free(raw);if(!g_match_info_next(matches,NULL))break;
}
g_match_info_free(matches);g_regex_unref(regex);return results;
}

14
src/core/bic_validator.c Normal file
View file

@ -0,0 +1,14 @@
#include "core/bic_validator.h"
#include <string.h>
char *bic_validator_normalize(const char *bic)
{ if(bic==NULL||bic[0]=='\0')return NULL; char *value=g_ascii_strup(bic,-1);
if(!bic_validator_validate(value)){g_free(value);return NULL;} return value; }
gboolean bic_validator_validate(const char *bic)
{ return bic_validator_check(bic)==BIC_VALIDATOR_VALID; }
BicValidatorResult bic_validator_check(const char *bic)
{ if(bic==NULL||bic[0]=='\0')return BIC_VALIDATOR_ABSENT;
gsize n=strlen(bic);if(n!=8&&n!=11)return BIC_VALIDATOR_INVALID;
for(gsize i=0;i<4;i++)if(!g_ascii_isalpha(bic[i]))return BIC_VALIDATOR_INVALID;
for(gsize i=4;i<6;i++)if(!g_ascii_isalpha(bic[i]))return BIC_VALIDATOR_INVALID;
for(gsize i=6;i<n;i++)if(!g_ascii_isalnum(bic[i]))return BIC_VALIDATOR_INVALID;
return BIC_VALIDATOR_VALID; }

11
src/core/french_bban.c Normal file
View file

@ -0,0 +1,11 @@
#include "core/french_bban.h"
#include "core/iban_analyzer.h"
#include <string.h>
FrenchBban *french_bban_derive(const char *iban)
{if(iban==NULL||strlen(iban)!=27||strncmp(iban,"FR",2)!=0||
!iban_analyzer_validate_canonical(iban))return NULL;
FrenchBban *b=g_new0(FrenchBban,1);b->bank_code=g_strndup(iban+4,5);
b->branch_code=g_strndup(iban+9,5);b->account_number=g_strndup(iban+14,11);
b->rib_key=g_strndup(iban+25,2);return b;}
void french_bban_free(FrenchBban *b)
{if(!b)return;g_free(b->bank_code);g_free(b->branch_code);g_free(b->account_number);g_free(b->rib_key);g_free(b);}

View file

@ -3,6 +3,18 @@
* @brief Normalisation, validation et extraction locale d'IBAN.
******************************************************************************/
#include "core/iban_analyzer.h"
#include <string.h>
char *iban_analyzer_normalize_canonical(const char *text)
{
if(text==NULL||strlen(text)>(1024U * 1024U))return NULL;
GString *r=g_string_new(NULL);
for(const unsigned char *p=(const unsigned char *)text;*p;p++) {
if(*p==' '||*p=='\t'||*p=='\r'||*p=='\n'||*p=='-')continue;
if(!g_ascii_isalnum(*p)){g_string_free(r,TRUE);return NULL;}
g_string_append_c(r,g_ascii_toupper(*p));
}
if(r->len==0){g_string_free(r,TRUE);return NULL;}return g_string_free(r,FALSE);
}
char *iban_analyzer_normalize(const char *text)
{
GString *result = NULL;
@ -38,6 +50,32 @@ gboolean iban_analyzer_validate(const char *iban)
}
g_free(normalized); return remainder == 1;
}
IbanAnalyzerResult iban_analyzer_validate_canonical_result(const char *iban)
{
char *normalized=iban_analyzer_normalize_canonical(iban);
static const struct { const char *country; gsize length; } lengths[]={
{"BE",16},{"DE",22},{"ES",24},{"FR",27},{"GB",22},
{"IT",27},{"LU",20},{"NL",18},{"PT",25}};
if(normalized==NULL||strlen(normalized)<15||strlen(normalized)>34||
!g_ascii_isalpha(normalized[0])||!g_ascii_isalpha(normalized[1])||
!g_ascii_isdigit(normalized[2])||!g_ascii_isdigit(normalized[3])) {
g_free(normalized);return IBAN_ANALYZER_RESULT_INVALID;
}
for(guint i=0;i<G_N_ELEMENTS(lengths);i++) {
if(g_str_has_prefix(normalized,lengths[i].country)) {
gboolean valid=strlen(normalized)==lengths[i].length&&
iban_analyzer_validate(normalized);
g_free(normalized);return valid?IBAN_ANALYZER_RESULT_VALID:
IBAN_ANALYZER_RESULT_INVALID;
}
}
g_free(normalized);return IBAN_ANALYZER_RESULT_UNSUPPORTED_COUNTRY;
}
gboolean iban_analyzer_validate_canonical(const char *iban)
{
return iban_analyzer_validate_canonical_result(iban)==
IBAN_ANALYZER_RESULT_VALID;
}
GPtrArray *iban_analyzer_extract(const char *text)
{
GPtrArray *results = g_ptr_array_new_with_free_func(g_free);

View file

@ -0,0 +1,149 @@
#include "models/financial_foundation.h"
#include "core/bic_validator.h"
#include "core/iban_analyzer.h"
#include <string.h>
static GQuark financial_error_quark(void)
{ return g_quark_from_static_string("financial-foundation-error"); }
static gboolean required(const char *value)
{ return value != NULL && value[0] != '\0' && strlen(value) <= FINANCIAL_TEXT_MAX_BYTES; }
static gboolean timestamp_valid(const char *value)
{ return value != NULL && strlen(value) == 20 && value[4] == '-' &&
value[7] == '-' && value[10] == 'T' && value[13] == ':' &&
value[16] == ':' && value[19] == 'Z'; }
static void invalid(GError **error, const char *message)
{ if (error != NULL && *error == NULL) g_set_error_literal(error,
financial_error_quark(), 1, message); }
const char *bank_validation_state_to_code(BankValidationState state)
{ switch(state) { case BANK_VALIDATION_VALID:return "valid";
case BANK_VALIDATION_INVALID:return "invalid";
case BANK_VALIDATION_AMBIGUOUS:return "ambiguous";
case BANK_VALIDATION_UNVERIFIABLE:return "unverifiable";
case BANK_VALIDATION_COUNT:break; } return "unknown"; }
const char *bank_human_decision_to_code(BankHumanDecision decision)
{ switch(decision) { case BANK_DECISION_PROPOSED:return "proposed";
case BANK_DECISION_CONFIRMED:return "confirmed";
case BANK_DECISION_REJECTED:return "rejected";
case BANK_DECISION_COUNT:break; } return "unknown"; }
const char *bank_origin_to_code(BankOrigin origin)
{ switch(origin) { case BANK_ORIGIN_AUTOMATIC:return "automatic";
case BANK_ORIGIN_HUMAN:return "human";
case BANK_ORIGIN_LEGACY:return "legacy";
case BANK_ORIGIN_COUNT:break; } return "unknown"; }
BankCandidate *bank_candidate_new(const char *source_text, const char *raw_iban,
gsize start, gsize end, GError **error)
{
if (!required(source_text) || !required(raw_iban)) {
invalid(error, "Le candidat bancaire est invalide."); return NULL;
}
gsize source_length = strlen(source_text);
if (start > end || start > source_length || end > source_length) {
invalid(error, "Le candidat bancaire est invalide."); return NULL;
}
BankCandidate *item = g_new0(BankCandidate, 1);
item->source_text = g_strdup(source_text); item->raw_iban = g_strdup(raw_iban);
item->start_offset = start; item->end_offset = end;
item->validation = BANK_VALIDATION_UNVERIFIABLE;
item->warnings = g_ptr_array_new_with_free_func(g_free);
item->origin = BANK_ORIGIN_AUTOMATIC; item->decision=BANK_DECISION_PROPOSED;
item->proposed_grouping = TRUE;
return item;
}
void bank_candidate_free(BankCandidate *c)
{ if (!c) return; g_free(c->source_text); g_free(c->raw_iban);
g_free(c->normalized_iban); g_free(c->raw_bic); g_free(c->normalized_bic);
g_free(c->declared_holder_raw); g_free(c->declared_institution);
g_free(c->declared_address);g_free(c->proposed_group_identifier);
g_clear_pointer(&c->warnings,g_ptr_array_unref); g_free(c); }
BankObservation *bank_observation_new(const char *id, const char *evidence,
const char *sha256, const char *raw, const char *source,
const char *method, BankOrigin origin, const char *created, GError **error)
{
if (!required(id)||!required(evidence)||!required(sha256)||strlen(sha256)!=64||
!required(raw)||!required(source)||!required(method)||!timestamp_valid(created))
{ invalid(error,"Lobservation bancaire est invalide."); return NULL; }
BankObservation *o=g_new0(BankObservation,1); o->identifier=g_strdup(id);
o->evidence_identifier=g_strdup(evidence); o->evidence_sha256=g_strdup(sha256);
o->raw_value=g_strdup(raw); o->source_type=g_strdup(source);
o->extraction_method=g_strdup(method); o->origin=origin;
o->created_at_utc=g_strdup(created); return o;
}
void bank_observation_free(BankObservation *o)
{ if(!o)return; g_free(o->identifier);g_free(o->evidence_identifier);g_free(o->evidence_sha256);
g_free(o->raw_value);g_free(o->source_type);g_free(o->extraction_method);
g_free(o->source_page_or_image);g_free(o->ocr_run_identifier);
g_free(o->created_at_utc);g_free(o->factual_notes);g_free(o); }
BankObservationRevision *bank_observation_revision_new(const char *id,
const char *corrected, const char *normalized, BankValidationState validation,
const char *created, GError **error)
{
if(!required(id)||!required(corrected)||!required(normalized)||!timestamp_valid(created)||
(validation==BANK_VALIDATION_VALID&&!iban_analyzer_validate_canonical(normalized)))
{ invalid(error,"La révision bancaire est invalide."); return NULL; }
BankObservationRevision *r=g_new0(BankObservationRevision,1);
r->observation_identifier=g_strdup(id);r->corrected_iban=g_strdup(corrected);
r->normalized_iban=g_strdup(normalized);r->validation=validation;
r->origin=BANK_ORIGIN_HUMAN;r->created_at_utc=g_strdup(created);return r;
}
void bank_observation_revision_free(BankObservationRevision *r)
{ if(!r)return;g_free(r->observation_identifier);g_free(r->corrected_iban);
g_free(r->normalized_iban);g_free(r->corrected_bic);g_free(r->normalized_bic);
g_free(r->corrected_holder);g_free(r->created_at_utc);g_free(r->factual_notes);g_free(r); }
BankAccount *bank_account_new(const char *id,const char *iban,const char *bic,
const char *institution,BankAccountState state,GError **error)
{
char *canonical=iban_analyzer_normalize_canonical(iban);
if(!required(id)||canonical==NULL||g_strcmp0(canonical,iban)!=0||
!iban_analyzer_validate_canonical(iban)||
bic_validator_check(bic)==BIC_VALIDATOR_INVALID)
{ g_free(canonical);invalid(error,"Le compte bancaire est invalide.");return NULL; }
BankAccount *a=g_new0(BankAccount,1);a->entity_identifier=g_strdup(id);
a->normalized_iban=canonical;a->country_code=g_strndup(iban,2);
a->bic=bic_validator_normalize(bic);a->declared_institution=g_strdup(institution);
a->state=state;return a;
}
void bank_account_free(BankAccount *a)
{if(!a)return;g_free(a->entity_identifier);g_free(a->normalized_iban);g_free(a->country_code);
g_free(a->bic);g_free(a->declared_institution);g_free(a->factual_notes);g_free(a);}
DeclaredBankHolder *declared_bank_holder_new(const char *name,const char *evidence,
const char *observation,BankOrigin origin,GError **error)
{if(!required(name)||!required(evidence)){invalid(error,"Le titulaire déclaré est invalide.");return NULL;}
DeclaredBankHolder *h=g_new0(DeclaredBankHolder,1);h->declared_name=g_strdup(name);
h->evidence_identifier=g_strdup(evidence);h->observation_identifier=g_strdup(observation);
h->origin=origin;return h;}
void declared_bank_holder_free(DeclaredBankHolder *h)
{if(!h)return;g_free(h->declared_name);g_free(h->evidence_identifier);
g_free(h->observation_identifier);g_free(h->factual_notes);g_free(h);}
static gboolean decimal_valid(const char *v)
{if(!required(v))return FALSE;gsize i=v[0]=='-'?1:0;gboolean digit=FALSE,dot=FALSE;
for(;v[i];i++){if(g_ascii_isdigit(v[i]))digit=TRUE;else if(v[i]=='.'&&!dot)dot=TRUE;else return FALSE;}
return digit&&v[strlen(v)-1]!='.';}
FinancialTransaction *financial_transaction_new(const char *type,const char *raw_date,
const char *amount,const char *currency,const char *status,const char *evidence,
const char *created,GError **error)
{if(!required(type)||!required(raw_date)||!decimal_valid(amount)||!required(currency)||
!required(status)||!required(evidence)||!timestamp_valid(created))
{invalid(error,"La transaction financière est invalide.");return NULL;}
FinancialTransaction *t=g_new0(FinancialTransaction,1);t->transaction_type=g_strdup(type);
t->raw_date=g_strdup(raw_date);t->amount_decimal=g_strdup(amount);t->currency=g_ascii_strup(currency,-1);
t->observed_status=g_strdup(status);t->evidence_identifier=g_strdup(evidence);
t->origin=BANK_ORIGIN_HUMAN;t->created_at_utc=g_strdup(created);return t;}
void financial_transaction_free(FinancialTransaction *t)
{if(!t)return;g_free(t->transaction_type);g_free(t->raw_date);g_free(t->normalized_date);
g_free(t->amount_decimal);g_free(t->currency);g_free(t->observed_status);g_free(t->bank_reference);
g_free(t->reference_type);g_free(t->uetr);g_free(t->debtor_account_identifier);
g_free(t->creditor_account_identifier);g_free(t->declared_payer);g_free(t->declared_beneficiary);
g_free(t->source_institution);g_free(t->evidence_identifier);g_free(t->observation_identifier);
g_free(t->factual_notes);g_free(t->created_at_utc);g_free(t);}
FinancialReviewModel *financial_review_model_new(void)
{FinancialReviewModel *m=g_new0(FinancialReviewModel,1);m->candidates=g_ptr_array_new_with_free_func(
(GDestroyNotify)bank_candidate_free);m->decision=BANK_DECISION_PROPOSED;return m;}
void financial_review_model_free(FinancialReviewModel *m)
{if(!m)return;g_clear_pointer(&m->candidates,g_ptr_array_unref);g_free(m);}

View file

@ -0,0 +1,202 @@
#include "core/bank_proposal.h"
#include "core/bank_text_extractor.h"
#include "core/bic_validator.h"
#include "core/french_bban.h"
#include "core/iban_analyzer.h"
#include "models/financial_foundation.h"
#include <glib.h>
#include <string.h>
static const char *valid_fr="FR4830002005500000000000052";
static const char *valid_de="DE89370400440532013000";
static const char *valid_gb="GB82WEST12345698765432";
static const char *stamp="2026-01-02T03:04:05Z";
static char *test_make_mod97_iban(const char *country,gsize length)
{
char *value=g_malloc0(length+1);memcpy(value,country,2);memset(value+2,'0',length-2);
for(guint key=0;key<100;key++){value[2]=(char)('0'+key/10);value[3]=(char)('0'+key%10);
if(iban_analyzer_validate(value))return value;}g_free(value);return NULL;
}
static void test_states_independent(void)
{
g_assert_cmpstr(bank_validation_state_to_code(BANK_VALIDATION_VALID),==,"valid");
g_assert_cmpstr(bank_human_decision_to_code(BANK_DECISION_PROPOSED),==,"proposed");
g_assert_cmpstr(bank_origin_to_code(BANK_ORIGIN_HUMAN),==,"human");
FinancialReviewModel *m=financial_review_model_new();g_assert_nonnull(m);
g_assert_false(m->create_account);g_assert_false(m->reuse_account);
g_assert_false(m->create_person);g_assert_false(m->create_relation);
g_assert_false(m->create_transaction);BankCandidate *c=bank_candidate_new("RAW","RAW",0,3,NULL);
g_ptr_array_add(m->candidates,c);c->decision=BANK_DECISION_REJECTED;
g_assert_cmpstr(c->raw_iban,==,"RAW");financial_review_model_free(m);
}
static void test_enum_conversion_bounds(void)
{
const char *validation_codes[]={"valid","invalid","ambiguous","unverifiable"};
for(gint value=0;value<BANK_VALIDATION_COUNT;value++)
g_assert_cmpstr(bank_validation_state_to_code((BankValidationState)value),==,validation_codes[value]);
const char *decision_codes[]={"proposed","confirmed","rejected"};
for(gint value=0;value<BANK_DECISION_COUNT;value++)
g_assert_cmpstr(bank_human_decision_to_code((BankHumanDecision)value),==,decision_codes[value]);
const char *origin_codes[]={"automatic","human","legacy"};
for(gint value=0;value<BANK_ORIGIN_COUNT;value++)
g_assert_cmpstr(bank_origin_to_code((BankOrigin)value),==,origin_codes[value]);
const gint invalid_values[]={-1,-2,G_MAXINT};
for(guint i=0;i<G_N_ELEMENTS(invalid_values);i++) {
g_assert_cmpstr(bank_validation_state_to_code((BankValidationState)invalid_values[i]),==,"unknown");
g_assert_cmpstr(bank_human_decision_to_code((BankHumanDecision)invalid_values[i]),==,"unknown");
g_assert_cmpstr(bank_origin_to_code((BankOrigin)invalid_values[i]),==,"unknown");
}
g_assert_cmpstr(bank_validation_state_to_code(BANK_VALIDATION_COUNT),==,"unknown");
g_assert_cmpstr(bank_validation_state_to_code((BankValidationState)(BANK_VALIDATION_COUNT+1)),==,"unknown");
g_assert_cmpstr(bank_human_decision_to_code(BANK_DECISION_COUNT),==,"unknown");
g_assert_cmpstr(bank_human_decision_to_code((BankHumanDecision)(BANK_DECISION_COUNT+1)),==,"unknown");
g_assert_cmpstr(bank_origin_to_code(BANK_ORIGIN_COUNT),==,"unknown");
g_assert_cmpstr(bank_origin_to_code((BankOrigin)(BANK_ORIGIN_COUNT+1)),==,"unknown");
}
static void test_candidate_offset_bounds(void)
{
const char *source="SPECIMEN\xC3\xA9";
const gsize length=strlen(source);
BankCandidate *candidate=bank_candidate_new(source,"SPECIMEN",0,length,NULL);
g_assert_nonnull(candidate);bank_candidate_free(candidate);
candidate=bank_candidate_new(source,"SPECIMEN",length,length,NULL);
g_assert_nonnull(candidate);bank_candidate_free(candidate);
candidate=bank_candidate_new(source,"SPECIMEN",8,10,NULL);
g_assert_nonnull(candidate);g_assert_cmpuint(candidate->start_offset,==,8);
g_assert_cmpuint(candidate->end_offset,==,10);bank_candidate_free(candidate);
g_assert_null(bank_candidate_new("0123456789","SPECIMEN",100,101,NULL));
g_assert_null(bank_candidate_new("0123456789","SPECIMEN",8,7,NULL));
g_assert_null(bank_candidate_new("0123456789","SPECIMEN",11,11,NULL));
g_assert_null(bank_candidate_new("0123456789","SPECIMEN",10,11,NULL));
g_assert_null(bank_candidate_new("0123456789","SPECIMEN",G_MAXSIZE,G_MAXSIZE,NULL));
g_assert_null(bank_candidate_new("","SPECIMEN",1,1,NULL));
}
static void test_iban_canonical_and_legacy_parity(void)
{
const char *values[]={valid_fr,valid_de,valid_gb,"FR0030002005500000000000021","FR763000",NULL};
for(guint i=0;values[i]!=NULL;i++)g_assert_cmpint(iban_analyzer_validate(values[i]),==,
bank_proposal_validate_iban(values[i]));
char *n=iban_analyzer_normalize_canonical("fr48 3000-2005\n5000 0000 0000 052");
g_assert_cmpstr(n,==,valid_fr);g_free(n);
g_assert_null(iban_analyzer_normalize_canonical("FR48.30002005500000000000052"));
g_assert_null(iban_analyzer_normalize_canonical("FR480002005500000000000052"));
char *wrong_length=test_make_mod97_iban("FR",26);g_assert_nonnull(wrong_length);
g_assert_false(bank_proposal_validate_iban(wrong_length));
g_assert_cmpint(iban_analyzer_validate_canonical_result(wrong_length),==,IBAN_ANALYZER_RESULT_INVALID);
char *unsupported=test_make_mod97_iban("AD",24);g_assert_nonnull(unsupported);
g_assert_cmpint(iban_analyzer_validate_canonical_result(unsupported),==,IBAN_ANALYZER_RESULT_UNSUPPORTED_COUNTRY);
g_assert_false(iban_analyzer_validate_canonical(unsupported));
char *unknown=test_make_mod97_iban("ZZ",24);g_assert_nonnull(unknown);
g_assert_cmpint(iban_analyzer_validate_canonical_result(unknown),==,IBAN_ANALYZER_RESULT_UNSUPPORTED_COUNTRY);
g_assert_cmpint(iban_analyzer_validate_canonical_result("F100"),==,IBAN_ANALYZER_RESULT_INVALID);
g_assert_cmpint(iban_analyzer_validate_canonical_result("FR00"),==,IBAN_ANALYZER_RESULT_INVALID);
g_assert_cmpint(iban_analyzer_validate_canonical_result(valid_fr),==,IBAN_ANALYZER_RESULT_VALID);
g_assert_cmpint(iban_analyzer_validate_canonical_result("FR0030002005500000000000021"),==,IBAN_ANALYZER_RESULT_INVALID);
g_free(unknown);g_free(unsupported);g_free(wrong_length);
}
static void test_bic_and_bban(void)
{
g_assert_cmpint(bic_validator_check(NULL),==,BIC_VALIDATOR_ABSENT);
g_assert_cmpint(bic_validator_check(""),==,BIC_VALIDATOR_ABSENT);
g_assert_false(bic_validator_validate(NULL));g_assert_true(bic_validator_validate("BNPAFRPP"));
g_assert_cmpint(bic_validator_check("BNPAFRPP"),==,BIC_VALIDATOR_VALID);
g_assert_cmpint(bic_validator_check("BNPAFR"),==,BIC_VALIDATOR_INVALID);
g_assert_true(bic_validator_validate("BNPAFRPPXXX"));g_assert_false(bic_validator_validate("BNPAFR"));
g_assert_false(bic_validator_validate("BNPAFRP!"));char *bic=bic_validator_normalize("bnpafrppxxx");
g_assert_cmpstr(bic,==,"BNPAFRPPXXX");g_free(bic);FrenchBban *b=french_bban_derive(valid_fr);
g_assert_nonnull(b);g_assert_cmpstr(b->bank_code,==,"30002");g_assert_cmpstr(b->branch_code,==,"00550");
g_assert_cmpstr(b->account_number,==,"00000000000");g_assert_cmpstr(b->rib_key,==,"52");
french_bban_free(b);g_assert_null(french_bban_derive("FR0030002005500000000000021"));
}
static void test_punctuation_and_utf8_offsets(void)
{
const char *formats[]={"(%s)","IBAN: %s","%s,","[%s]",NULL};
for(guint i=0;formats[i]!=NULL;i++){char *text=g_strdup_printf(formats[i],valid_fr);
GPtrArray *items=bank_text_extractor_extract(text,NULL);g_assert_cmpuint(items->len,==,1);
BankCandidate *candidate=g_ptr_array_index(items,0);g_assert_cmpstr(candidate->raw_iban,==,valid_fr);
g_assert_cmpmem(text+candidate->start_offset,candidate->end_offset-candidate->start_offset,
valid_fr,strlen(valid_fr));g_assert_nonnull(strstr(candidate->source_text,valid_fr));
g_ptr_array_unref(items);g_free(text);}
const char *utf8="Préfixe SPECIMEN : [FR4830002005500000000000052]";
GPtrArray *items=bank_text_extractor_extract(utf8,NULL);g_assert_cmpuint(items->len,==,1);
BankCandidate *candidate=g_ptr_array_index(items,0);g_assert_cmpuint(candidate->start_offset,==,
(gsize)(strstr(utf8,valid_fr)-utf8));g_assert_cmpuint(candidate->end_offset,==,
candidate->start_offset+strlen(valid_fr));g_ptr_array_unref(items);
}
static void test_multi_occurrences_and_grouping(void)
{
const char *text="Titulaire : SPECIMEN ALPHA\nBIC : bnpafrpp\n"
"IBAN : FR48 3000 2005 5000 0000 0000 052\n"
"IBAN : DE89-3704-0044-0532-0130-00\n\n"
"Titulaire : SPECIMEN BETA\nIBAN : GB82 WEST 1234 5698 7654 32";
GPtrArray *items=bank_text_extractor_extract(text,NULL);g_assert_cmpuint(items->len,==,3);
BankCandidate *a=g_ptr_array_index(items,0),*b=g_ptr_array_index(items,1),*c=g_ptr_array_index(items,2);
g_assert_cmpint(a->validation,==,BANK_VALIDATION_VALID);g_assert_cmpint(b->validation,==,BANK_VALIDATION_VALID);
g_assert_cmpint(c->validation,==,BANK_VALIDATION_VALID);g_assert_cmpstr(a->declared_holder_raw,==,"SPECIMEN ALPHA");
g_assert_cmpstr(b->declared_holder_raw,==,"SPECIMEN ALPHA");g_assert_cmpstr(c->declared_holder_raw,==,"SPECIMEN BETA");
g_assert_cmpstr(a->normalized_bic,==,"BNPAFRPP");g_assert_true(a->proposed_grouping);
a->proposed_group_identifier=g_strdup("group-specimen");b->proposed_group_identifier=g_strdup("group-specimen");
c->proposed_group_identifier=g_strdup("separate-specimen");g_assert_cmpstr(a->proposed_group_identifier,==,b->proposed_group_identifier);
g_assert_cmpstr(a->proposed_group_identifier,!=,c->proposed_group_identifier);
g_assert_cmpmem(text+a->start_offset,a->end_offset-a->start_offset,a->raw_iban,strlen(a->raw_iban));
g_ptr_array_unref(items);
}
static void test_duplicate_offsets_and_invalid(void)
{
char *text=g_strdup_printf("%s puis %s",valid_fr,valid_fr);GPtrArray *items=bank_text_extractor_extract(text,NULL);
g_assert_cmpuint(items->len,==,2);BankCandidate *a=g_ptr_array_index(items,0),*b=g_ptr_array_index(items,1);
g_assert_cmpuint(a->start_offset,<,b->start_offset);g_assert_cmpstr(a->raw_iban,==,b->raw_iban);g_ptr_array_unref(items);g_free(text);
items=bank_text_extractor_extract("IBAN : FR00 3000 2005 5000 0000 0000 021",NULL);
g_assert_cmpuint(items->len,==,1);a=g_ptr_array_index(items,0);g_assert_cmpint(a->validation,==,BANK_VALIDATION_INVALID);
g_assert_nonnull(a->raw_iban);g_ptr_array_unref(items);
items=bank_text_extractor_extract("IBAN : FR48 3000",NULL);g_assert_cmpuint(items->len,==,1);
g_assert_cmpint(((BankCandidate*)g_ptr_array_index(items,0))->validation,==,BANK_VALIDATION_INVALID);g_ptr_array_unref(items);
}
static void test_ambiguities_no_silent_fix(void)
{
const char *texts[]={"IBAN : FRO8 3000 2005 5000 0000 0000 052",
"IBAN : FRI8 3000 2005 5000 0000 0000 052","IBAN : FR48 000 2005 5000 0000 0000 052",NULL};
for(guint i=0;texts[i];i++){GPtrArray *items=bank_text_extractor_extract(texts[i],NULL);
g_assert_cmpuint(items->len,==,1);BankCandidate *c=g_ptr_array_index(items,0);
g_assert_cmpint(c->validation,==,BANK_VALIDATION_AMBIGUOUS);g_assert_null(c->normalized_iban);
g_assert_true(strstr(texts[i],c->raw_iban)!=NULL);g_ptr_array_unref(items);}
}
static void test_models_and_transaction(void)
{
const char *sha="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";GError *error=NULL;
BankObservation *o=bank_observation_new("obs-specimen","proof-specimen",sha,"FRO8 raw","image","manual",BANK_ORIGIN_AUTOMATIC,stamp,&error);
g_assert_no_error(error);g_assert_nonnull(o);BankObservationRevision *r=bank_observation_revision_new(
"obs-specimen",valid_fr,valid_fr,BANK_VALIDATION_VALID,stamp,&error);g_assert_nonnull(r);
g_assert_cmpstr(o->raw_value,==,"FRO8 raw");g_assert_cmpint(r->origin,==,BANK_ORIGIN_HUMAN);
BankAccount *a=bank_account_new("entity-specimen",valid_fr,"BNPAFRPP",NULL,BANK_ACCOUNT_STATE_UNKNOWN,&error);g_assert_nonnull(a);
DeclaredBankHolder *h=declared_bank_holder_new("SPECIMEN HOLDER","proof-specimen","obs-specimen",BANK_ORIGIN_HUMAN,&error);g_assert_nonnull(h);
FinancialTransaction *t=financial_transaction_new("transfer","02/01/2026","1234.56","eur","envoyée","proof-specimen",stamp,&error);
g_assert_nonnull(t);g_assert_cmpstr(t->amount_decimal,==,"1234.56");g_assert_cmpstr(t->observed_status,==,"envoyée");
t->uetr=g_strdup("11111111-2222-4333-8444-555555555555");g_assert_cmpstr(t->uetr,==,"11111111-2222-4333-8444-555555555555");
financial_transaction_free(t);declared_bank_holder_free(h);bank_account_free(a);
bank_observation_revision_free(r);bank_observation_free(o);
g_assert_null(financial_transaction_new("transfer","date","1.2.3","EUR","vu","proof",stamp,&error));g_clear_error(&error);
}
static void test_null_empty_and_limits(void)
{
GPtrArray *items=bank_text_extractor_extract(NULL,NULL);g_assert_cmpuint(items->len,==,0);g_ptr_array_unref(items);
items=bank_text_extractor_extract("",NULL);g_assert_cmpuint(items->len,==,0);g_ptr_array_unref(items);
char *large=g_malloc0(FINANCIAL_TEXT_MAX_BYTES+2);memset(large,'A',FINANCIAL_TEXT_MAX_BYTES+1);
GError *error=NULL;items=bank_text_extractor_extract(large,&error);g_assert_error(error,
g_quark_from_static_string("bank-text-extractor-error"),1);g_assert_cmpuint(items->len,==,0);
g_clear_error(&error);g_ptr_array_unref(items);g_free(large);
}
int main(int argc,char **argv)
{g_test_init(&argc,&argv,NULL);g_test_add_func("/financial/states",test_states_independent);
g_test_add_func("/financial/enum-bounds",test_enum_conversion_bounds);
g_test_add_func("/financial/candidate-offset-bounds",test_candidate_offset_bounds);
g_test_add_func("/financial/iban-parity",test_iban_canonical_and_legacy_parity);
g_test_add_func("/financial/bic-bban",test_bic_and_bban);
g_test_add_func("/financial/multi-grouping",test_multi_occurrences_and_grouping);
g_test_add_func("/financial/duplicates-invalid",test_duplicate_offsets_and_invalid);
g_test_add_func("/financial/punctuation-offsets",test_punctuation_and_utf8_offsets);
g_test_add_func("/financial/ambiguities",test_ambiguities_no_silent_fix);
g_test_add_func("/financial/models-transaction",test_models_and_transaction);
g_test_add_func("/financial/null-empty-limits",test_null_empty_and_limits);return g_test_run();}