feat(finance): ajouter le socle métier bancaire canonique
This commit is contained in:
parent
116b08f2f4
commit
e97b35a4df
13 changed files with 704 additions and 0 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -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/
|
||||
|
|
|
|||
10
Makefile
10
Makefile
|
|
@ -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) \
|
||||
|
|
|
|||
8
include/core/bank_text_extractor.h
Normal file
8
include/core/bank_text_extractor.h
Normal 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
|
||||
18
include/core/bic_validator.h
Normal file
18
include/core/bic_validator.h
Normal 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
|
||||
7
include/core/french_bban.h
Normal file
7
include/core/french_bban.h
Normal 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
|
||||
|
|
@ -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);
|
||||
/**
|
||||
|
|
|
|||
122
include/models/financial_foundation.h
Normal file
122
include/models/financial_foundation.h
Normal 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
|
||||
103
src/core/bank_text_extractor.c
Normal file
103
src/core/bank_text_extractor.c
Normal 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
14
src/core/bic_validator.c
Normal 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
11
src/core/french_bban.c
Normal 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);}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
149
src/models/financial_foundation.c
Normal file
149
src/models/financial_foundation.c
Normal 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,"L’observation 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);}
|
||||
202
tests/test_financial_foundation.c
Normal file
202
tests/test_financial_foundation.c
Normal 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("FR4830002005500000000000052"));
|
||||
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 3000 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();}
|
||||
Loading…
Reference in a new issue