Practical Example - Read CREMUL payments file and update database

This is a complete example of how to parse and import data from a CREMUL (Credit Transfer Initiation) payments file into a database using ACF (Advanced Custom Functions for FileMaker) functions. Here's a breakdown of what this code does:

  1. It defines several functions for creating SQL insert statements for different types of data, such as message headers, sequences, and transactions.

  2. It reads the contents of a CREMUL file and splits it into segments, where each segment is separated by a single apostrophe ('').

  3. It then iterates through the segments and processes each one based on its type, extracting relevant information and populating variables.

  4. The code organizes the data into three arrays: dataMH for message headers, dataSQ for sequences, and dataTR for transactions.

  5. It inserts the data from these arrays into the corresponding database tables using SQL insert statements.

  6. The code also performs some data validation and error checking to ensure that the CREMUL file is well-formed.

  7. It includes a function import_cremul_select_file that allows the user to select a CREMUL file interactively and then import its data into the database.

This code is intended to be used as part of a system for processing and storing payment data from CREMUL files, making it easier to generate reports for the financial department. The code appears to be well-documented and includes error handling to ensure that data is imported correctly.

Please note that to use this code effectively, you would need to integrate it with your FileMaker structure. Additionally, you may need to customize the code to match the specific database schema and requirements of your application.

The Cremul File Format

Here is a little sample of a CREMUL file we are going to read into the database. Usually there is several segments on each line only separated by a single apostrophe (').


UNH+4+CREMUL:D:96A:UN' 
BGM+435+20010629170120128' 
DTM+137:20010629:102' 

LIN+1' 
DTM+209:20010418:102' 
BUS++DO++230:25:124' 
MOA+349:124113,53' 
RFF+ACK:01918100001' 
DTM+171:20010418:102' 
FII+BF+97600224105'

SEQ++1'
DTM+203:20010418:102'
FII+OR+83801915348'
RFF+ACD:101089152'
RFF+AEK:91521461'
MOA+143:3808,11'
NAD+PL+++HANS ESPEN'
INP+BF+2:SI'
FTX+AAG+++FROM?:HANS ESPEN PAID ON?: 18.04.01' 
PRC+8'
DOC+999+0000773500050154208143212' 
MOA+12:3808,11'
GIS+37'

SEQ++2'
DTM+203:20010418:102'
FII+OR+36251403893'
RFF+ACD:362589343'
RFF+AEK:53191521504'
MOA+143:4843,44'
NAD+PL+++Erik Jensen'
INP+BF+2:SI'
FTX+AAG+++FROM?: Erik Jensen PAID ON?: 18.04.01' 
PRC+8'
DOC+999+0000773500050209408106755' 
MOA+12:4843,44'
GIS+37'

SEQ++3'
DTM+203:20010418:102'
FII+OR+36260900980'
RFF+ACD:362689451'
RFF+AEK:94510009'
MOA+143:115461,98'
NAD+PL+++Øystein Gudmonson'
INP+BF+2:SI'
FTX+AAG+++FROM?: 3626.09.00980 PAID ON?: 18.04.01' 
PRC+8'
DOC+999+0000773500050211808028787' 
MOA+12:115461,98'
GIS+37'

UNT+50+4'

ACF Functions Example:

Here is the import function to parse this format. Then update the three database tables,

This makes it easy to generate a report for the Financial department...


package cremul_import "Functions to read and import CREMUL data into database tables. ";

/*

The functions below are used to create SQL insert VALUES part that is put into arrays in the main function. 
The arrays are then merged with a comma separator to be all inserted in one single INSERT INTO SQL command. One for each file. 

*/
function createMessageHeader ( string MessageRef, date MessageDate, string FilePath )
    string FileName = regex_replace("^(.+)/(.+\..+)$", FilePath, "\2");
    string ImpBy = @get(AccountName)@;
    string TimeStampNow = string ( now()+3600, "%Y-%m-%d %H:%M:%S"); 
    string data = format ( "('%s',DATE '%s', '%s', '%s', TIMESTAMP '%s', '%s')", 
        MessageRef, string (MessageDate, "%Y-%m-%d" ), FileName, FilePath, TimeStampNow, ImpBy); 
    return data; 
end

function createSequenceLIN ( string MessageRef, int LIN, date ProcDate, 
                string BUSCat, float SeqAm, string SEQBankRef, date valDate, string RecAccNo)
    string data = format ( "('%s',%d, DATE '%s', '%s', %.4f,'%s',DATE '%s','%s')", 
        MessageRef, LIN, string (ProcDate, "%Y-%m-%d" ),BUSCat, SeqAm, SEQBankRef, 
        string (valDate, "%Y-%m-%d" ), RecAccNo); 
    return data; 
end

function createTransaction ( string MessageRef, int LIN, int seq, date recDate, string ArcRef, string OppdragRef, 
        float transfAm, string PayerAccNo,  string PayerName, string Freetext, string INP, int Proc, string DocRef, 
        float ProcAm, int genInd, string NADPL, string KID)
        
    if ( PayerName == "" ) then
        PayerName = NADPL; 
    end if
    
    string data = format ( "('%s',%d, %d, DATE '%s', '%s', '%s', %.4f, '%s', '%s', '%s', '%s', %d, '%s', %.4f, %d, '%s')",  
        MessageRef, LIN, seq, string (recDate, "%Y-%m-%d" ), ArcRef, OppdragRef, 
        transfAm, PayerAccNo,  PayerName, Freetext, INP, Proc, DocRef, 
        ProcAm, genInd, KID); 
    return data; 
end

/*

Helper functions to facilitate some operations in the main function. 

*/

function assert ( string value, string v1, string v2, string message)
    if (value == v1 || value == v2 ) then
        return true; 
    else
        throw "ERROR Assert " + message + " value: " + value; 
    end if
    return false; 
end

function getDate (string datevalue, string formatcode ) 
    date dateval;  
    bool b1 = assert (formatcode, "102", "203", "Invalid formatcode in DTM, should be 102 or 203, is: " ); 
    if ( formatcode == "102") then
        dateval = date (datevalue, "%Y%m%d" ); 
    elseif ( formatcode == "203" ) then
        dateval = date (left ( datevalue, 8 ) , "%Y%m%d" );
    else
    end if

    return dateval; 
end

function getAmounth ( string number ) 
    double xx = double ( substitute (number, ",", ".")); 
    return xx; 
end

/*

Here is the main function. It can be called with a file-path to the CREMUL file, OR use the 
import_cremul_select_file () in the bottom to get a select dialogue to pick the file. 

*/

function import_cremul ( string filename )

    int x = open ( filename, "r"); // Plugins Exception handler handles any error and abort 
                                    // the functions returning a proper error message back to FileMaker.  
    string cremul_data = read ( x ); 
    close ( x ) ; 

/*
Each segment is terminated by a single aphostrophe ('). The file can contain several segments on one line. 
Remove all the line-separator (CR / LF) to get a long string of segments each terminated by the ('). 
Then - split in a array of segments to parce each one on a loop. 
    
*/ 
    // Clean and split the file in segments. 
    cremul_data = substitute ( cremul_data, "\r", ""); 
    cremul_data = substitute ( cremul_data, "\n", ""); 
    cremul_data = substitute ( cremul_data, "\t", ""); 
    
    array string cremul_segments = explode ( "'", cremul_data); 
    
    array string subsegs, subsub; 
    
    array string dataMH, dataSQ, dataTR; // The data arrays for the table data in SQL VALUES format. To be inserted in tables at the end. 
    
    int noSegments = sizeof (cremul_segments ); 
    bool b1; 
    
    string MessageIdentifier, UniqueMessageNumber, BUScat, refPostingACK, refCT, refArchive, refOrderNumber,
    receivingAccountNumber, payersAccountNumber, PayersName,
    NAD_MR, NAD_PL, INP, FriTekst, KID; 
    
    date DocumentDate, ReceivedDate, ProcessedDate, ValueDate, NETSProcessingDate; 
    
    float totalAmount, transactionAmount; // float are doubles in ACF - really. 
    
    int segNo, noSubSegs, currentMessageStart, LevB_LIN, sequence; 
    
    string subSegStart; 
    
    for ( segNo = 1, noSegments )
        if ( cremul_segments[segNo] != "") then
        print "\n" + segNo + ": "+ cremul_segments[segNo]; 
            subsegs = explode ( "+", cremul_segments[segNo]); 
            noSubSegs = sizeof ( subsegs ); 
            subSegStart = subsegs[1]; 
            subSegStart = regex_replace ( "^(\\s|\\n|\\r)*(.*)(\\s|\\n|\\r)*$",subSegStart, "\2"); 
            
            if ( subSegStart == "UNH" ) then  // Start of Message.
                // UNH+1+CREMUL:D:96A:UN
                MessageIdentifier = subsegs[2]; 
                currentMessageStart = segNo; 
                if ( subsegs[3] != "CREMUL:D:96A:UN") then
                    throw "Cremul document is not according to standard: CREMUL:D:96A:UN, but instead: " 
                        + subsegs[3] + " Contact your software vendor to update CREMUL Import to this standard."; 
                end if
                
            elseif (MessageIdentifier == "" ) then
                throw "ERROR cremul file lacks Message identifier"; 
                
            elseif (subSegStart == "BGM") then  // Beginning of message
                // BGM+435+1999090813150001
                UniqueMessageNumber = subsegs[3];
                b1 = assert ( subsegs[2], "435", "455", "BGM Header invalid code, should be 435 or 455, is: "); 
                
            elseif (subSegStart == "DTM") then
                // DTM+137:19970303:102
                // DTM+137:200003031208:203
                subsub = explode ( ":", subsegs[2]); 
                if (subsub[1]== "137" ) then
                    DocumentDate = getDate (subsub[2], subsub[3] ); 
                elseif ( subsub[1]== "171" ) then 
                    NETSProcessingDate = getDate (subsub[2], subsub[3] ); 
                elseif ( subsub[1]== "203" ) then  
                    ReceivedDate = getDate (subsub[2], subsub[3] );
                elseif ( subsub[1]== "193" ) then  
                    ProcessedDate = getDate (subsub[2], subsub[3] );
                elseif ( subsub[1]== "209" ) then  
                    ValueDate = getDate (subsub[2], subsub[3] );
                else
                    throw "Invalud DTM Date Qualifier: " + subsub[1]; 
                end if
                
            elseif (subSegStart == "LIN") then
                // Starts level B ( Sequence numbers unique inside this level. )
                // If sequence number set, flush transactions as we start on a new B segment. 
                // LIN+1
                
                if ( LevB_LIN != 0) then
                    dataTR[] = createTransaction ( UniqueMessageNumber, LevB_LIN, sequence, ReceivedDate, 
                        refArchive, refOrderNumber, 
                        transactionAmount, payersAccountNumber,  PayersName, FriTekst, INP, 0, "", 
                        0.0, 0, NAD_PL, KID); 
                    sequence = 0;   
                    
                    transactionAmount = 0.0; 
                    refArchive = "";        
                    refOrderNumber = ""; 
                    payersAccountNumber = ""; 
                    PayersName = ""; 
                    NAD_PL = ""; 
                    FriTekst = ""; 
                    KID = ""; 
                    INP = ""; 
                    
                end if
                LevB_LIN = subsegs[2]; 
                
                
                
            elseif (subSegStart == "BUS") then
                // BUS++DO++230:25:124
                subsub = explode ( ":", subsegs[5]); 
                BUScat = subsegs[3]+"."+subsub[1]; 
            elseif (subSegStart == "MOA") then
                // MOA+349:124113,53
                subsub = explode ( ":", subsegs[2]); 
                if ( subsub[1] == "60" || subsub[1] == "346" || subsub[1] == "349" || subsub[1] == "362") then
                    totalAmount = getAmounth (subsub[2]); 
                elseif ( subsub[1] == "143" ) then
                    transactionAmount = getAmounth (subsub[2]);
                else
                end if 
/* TODO: Find the use of those codes...
36 (Converted amount) 
60 (Final (posted) amount)
98 (Original amount – the original amount to be paid)
119 (Received amount)
143 (Transfer amount) */
                
            elseif (subSegStart == "RFF") then
                // RFF+ACK:00430409720 - (Bank reference – posted amount)
                // RFF+CT:001005001 - (AutoGiro agreement ID)
                // RFF+ACD:*94011106 - (Bank reference = Archive reference. The customer should tell the 
                    // archive reference to the bank if the customer has questions about a specific transaction)
                // RFF+AEK:6011489652 - (Payment order number. Bankgiro note number is unique. The
                    // number does not have to be unique when other payment instruments are used)
                subsub = explode ( ":", subsegs[2]);    
                if ( subsub[1] == "ACK") then
                    refPostingACK = subsub[2]; 
                elseif ( subsub[1] == "CT" ) then
                    refCT = subsub[2];
                elseif ( subsub[1] == "ACD" ) then
                    refArchive = subsub[2];
                elseif ( subsub[1] == "AEK" ) then
                    refOrderNumber = subsub[2];
                else
                    // Other refs: ABO, ACK, AFO, AGN, AHK, RA, TBR
                    // Investigate the use of those. 
                end if
            elseif (subSegStart == "FII") then
                // FII+BF+12345678901 - Kontnummer til mottaker
                // FII+OR+36251403893 - Betalt fra konto
                // FII+OR+23510524047:Jenny Petterson
                subsub = explode ( ":", subsegs[3]);
                if ( subsegs[2] == "BF") then
                    receivingAccountNumber = subsub[1]; 
                elseif  ( subsegs[2] == "OR") then
                    payersAccountNumber = subsub[1]; 
                    if ( sizeof ( subsub ) > 1) then
                        PayersName = subsub[2]; 
                    else
                        PayersName = ""; 
                    end if
                else
                    // no other known codes....
                end if

                    
                
            elseif (subSegStart == "SEQ") then
                // Start a new sequence
                // Flush old values to payment table, if sequence is different than 0. 
                // SEQ++1
                // Within same level B (starts with the LIN-segment), the first underlaying transaction on level C
                // starts with SEQ+1, the second transaction starts with SEQ+2, the third transaction starts with SEQ+3 etc.
                // (Data element 1050 Sequence number)
                
                
                
                if ( sequence != 0 ) then
                    // flush previous transaction, empty trans data. 
                    dataTR[] = createTransaction ( UniqueMessageNumber, LevB_LIN, sequence, ReceivedDate, 
                            refArchive, refOrderNumber, 
                            transactionAmount, payersAccountNumber,  PayersName, FriTekst, INP, 0, "", 
                            0.0, 0, NAD_PL, KID);
                            
                    transactionAmount = 0.0; 
                    refArchive = "";        
                    refOrderNumber = ""; 
                    payersAccountNumber = ""; 
                    PayersName = ""; 
                    NAD_PL = ""; 
                    FriTekst = ""; 
                    KID = ""; 
                    INP = ""; 
        
                end if
                
                sequence = int( subsegs[3] ); 
                
                if (sequence == 1 ) then
                    if ( LevB_LIN == 1) then 
                        // Flush the message header
                        dataMH[] = createMessageHeader ( UniqueMessageNumber, DocumentDate, filename ); 
                    end if

                   // Flush the LIN header
                   dataSQ[] = createSequenceLIN ( UniqueMessageNumber, LevB_LIN, NETSProcessingDate, 
                       BUScat, totalAmount, refPostingACK, ValueDate, receivingAccountNumber); 
                
                end if
                
            
            elseif (subSegStart == "NAD") then
                // Name and address (M1)
                // NAD+MR+00123456789-001234567 - Mottakers ORG nummer
                // NAD+PL+++Bjarne Frogner AS++Asker++1370 - Beatlers detaljer: 
                // NAD+PL+++Erik Jensen
                // NAD_MR, NAD_PL
                if ( subsegs[2] == "MR" ) then
                    // Mottaker
                    NAD_MR = subsegs[3]; 
                elseif ( subsegs[2] == "PL" ) then
                    // Betaler
                    NAD_PL = subsegs[5];
                end if
                
                
            elseif (subSegStart == "INP") then
                // Instruction to parties (M 1)
                // INP+BF+2:SI
                subsub = explode ( ":", subsegs[3]);
                INP = subsub[2]; 
            
            elseif (subSegStart == "FTX") then
                // Free text (C 1)
                // FTX+REG++14+Import of Toyota-cars
                // FTX+AAG+++Tandberg Data
                FriTekst = subsegs[5]; 
                
            elseif (subSegStart == "PRC") then
                // PRC Process identification
                // Not in NETS documentation
                
            elseif (subSegStart == "DOC") then
                // Document/message details 
                // Not in NETS documentation Den Danske Bank har ref på dette. her ligger KID
                // DOC+999+0000773500050154208143212
                if ( subsegs[2] == "999") then
                    KID = subsegs[3]; 
                end if
                
                
            elseif (subSegStart == "GIS") then
                // General indicator (M 1)
                // GIS+10 -  (Declaration is nessecary)
            elseif (subSegStart == "UNT") then
                // Message trailer (M 1)
                // UNT+25+1
                if ( int ( subsegs[2] ) != segNo - currentMessageStart + 1 ) then
                    throw "Number of lines in Message does not comform with the message trailer: " + subsegs[2] 
                        + "/" + (segNo - currentMessageStart + 1) + " / MessageID:" + UniqueMessageNumber; 
                end if
                
                // TODO: Complete the update of the payments table and related tables. 
                dataTR[] = createTransaction ( UniqueMessageNumber, LevB_LIN, sequence, ReceivedDate, 
                        refArchive, refOrderNumber, 
                        transactionAmount, payersAccountNumber,  PayersName, FriTekst, INP, 0, "", 
                        0.0, 0, NAD_PL, KID); 
                        
                //
                transactionAmount = 0.0; 
                refArchive = "";        
                refOrderNumber = ""; 
                payersAccountNumber = ""; 
                PayersName = ""; 
                NAD_PL = ""; 
                FriTekst = ""; 
                KID = ""; 
                INP = ""; 
                
                 
                // Verify the sum of the amounths and the total. 
                
                currentMessageStart = 0; 
                MessageIdentifier = ""; 
            else
                throw "Unidentified code in CREMUL file: " + subSegStart; 
            end if
        end if
    end for
    
    if ( MessageIdentifier != "") then
        throw "CREMUL File is not complete. Last segment not terminated with Message Trailer (UNT)"; 
    end if
    
/*

If we arrive here, the parsing is successfull, but we haven't done anything to the database yet. Lets update the database. 

Use the data arrays for header, sequences and transactions to insert into tables. 
The data is allready in SQL VALUES format in the three arrays dataMH, dataSQ, and dataTR. 

Use the "implode" function to merge the arrays into comma-separated lists and insert all records for that table using one INSERT statement. 
    INSERT INTO TABLE (field list) VALUES ( record 1 data ),(record 2 data),(record 3 data),(record 4 data)...

*/
    
    // Header
    string res = ExecuteSQL ( "INSERT INTO PaymentMessage (MessageRef, MessageDate, FileName,FilePath, ImportedTimeStamp, ImportedBy) VALUES " + 
    implode ( ",", dataMH)); 
    
    // sequences
    string res2 = ExecuteSQL ( "INSERT INTO PaymentSequences (MessageRef, LIN, NETSProcessingDate, BusCategory,
        SequenceAmounth, SequenceBankreference, ValDate, ReceiversAccountNumber) VALUES " + 
    implode ( ",", dataSQ)); 
    
    // Transactions
    string SQL3 =  "INSERT INTO PaymentTransaction (MessageRef, LIN_ID, SequneceNo, ReceivedDate, ArkivRef, 
        OppdragRef, TransfAmounth, PayerAccountNumber, PayerName, Freetext, INP, Proc, DocRef, ProcAmounth, GeneralIndicator, KID) VALUES " + 
    implode ( ",", dataTR); 
    string res3 = ExecuteSQL (SQL3); 
    
    
    if ( res == "" && res2 == "" && res3 == "") then
        return "OK"; 
    end if
    
    
    return format ( "ERROR SQL Insert res (MH): %s, (SQ): %s, (TR):%s\n", res, res2, res3); 
end

function import_cremul_select_file ()
    string FileName = select_file ("Select a CREMUL FILE?", desktop_directory()); 
    if ( FileName != "") then
        return import_cremul ( FileName );
    end if
    return "OK"; // user cancelled the File Selcet Dialogue. 
end

After we run this function on our test Database with the above example CREMUL file. We got this data

References:

Cremul Doc at NETS (Norway)