CREATE TABLE reports (
    id bigserial primary key not null,
    report_id int NOT NULL,
    account text not null,
    scn text not null, -- SCN is the check #
    their_balance numeric,
    our_balance numeric,
    errorcode INT,
    user int references entity(id) not null, -- why ois this not an entity reference?
    corrections INT NOT NULL DEFAULT 0
    clear_time TIMESTAMP NOT NULL,
    insert_time TIMESTAMPTZ NOT NULL DEFAULT now(),
    ledger_id int REFERENCES acc_trans(entry_id),
    overlook boolean not null default 'f',
    approved boolean not null default 'f'
);

CREATE TABLE coa_to_account (
    chart_id int not null references chart(id),
    account text not null CHECK (account ~ '[0-9]{7}(xxx)')
);


CREATE TABLE report_corrections (
    id serial primary key not null,
    correction_id int not null default 1,
    entry references reports(id) not null,
    user references entity(id) not null, -- why is this not an entity reference?
    reason text not null,
    insert_time timestamptz not null default now()
);

-- to correct OUR wrong amount.
CREATE OR REPLACE FUNCTION reconciliation__correct_ledger (in_report_id INT, in_id int, in_new_amount NUMERIC, reason TEXT) returns INT AS $$

    DECLARE
        new_code INT;
        current_row RECORD;
        l_row acc_trans;
        in_user TEXT;
        full_reason TEXT;
    BEGIN
        select into in_user from current_user;
        
        select into current_row from reports where reports.id = in_report_id and reports.id = in_id;
        select into l_row from acc_trans where entry_id = current_row.lid;
        
        IF NOT FOUND THEN
            RAISE EXCEPTION 'No such id % in this report.', in_scn;
        END IF;
        
        IF user <> current_row.user THEN
        
            IF current_row.our_balance <> in_new_amount AND in_new_amount = current_row.their_balance THEN
                update reports pr
                set pr.corrections = reports.corrections + 1, 
                pr.new_balance = in_new_amount,
                error_code = 0
                where id = in_report_id and scn = in_scn;
                return 0;
                
                -- After that, it is required to update the general ledger.
                full_reason := "User % is filing a reconciliation correction on the general ledger, changing amount % to amount %. 
                Their reason given is: %", in_user, current_row.our_balance, in_new_amount, reason;
                perform select reconciliation__update_ledger(current_row.lid, in_new_amount, full_reason)
            ELSE IF current_row.our_balance = in_new_amount THEN
                -- This should be something, does it equal the original 
                -- balance? If so, there's no change.
                return current_row.error_code;
            END IF;
        END IF;
        
        return current_row.error_code;            
                    
    END;
$$ language 'plpgsql';

-- to correct an incorrect bank statement value.
CREATE OR REPLACE FUNCTION reconciliation__correct_bank_statement (in_report_id INT, in_id int, in_new_amount NUMERIC) returns INT AS $$

    DECLARE
        new_code INT;
        current_row RECORD;
        in_user TEXT;
    BEGIN
        select into in_user from current_user;
        select into current_row from reports where reports.id = in_id and reports.report_id = in_report_id;
        IF NOT FOUND THEN
            RAISE EXCEPTION 'No such SCN % in this report.', in_scn;
        END IF;
        
        IF user <> current_row.user THEN
        
            IF current_row.their_balance <> in_new_amount AND in_new_amount = current_row.our_balance THEN
                update reports pr
                set pr.corrections = reports.corrections + 1, 
                pr.new_balance = in_new_amount,
                error_code = 0
                where id = in_report_id and scn = in_scn;
                return 0;
            
            ELSE IF current_row.their_balance = in_new_amount THEN
                -- This should be something, does it equal the original 
                -- balance? If so, there's no change.
                return current_row.error_code;         
            END IF;
        END IF;
        
        return current_row.error_code;            
                    
    END;
$$ language 'plpgsql';


CREATE OR REPLACE reconciliation__correct_passthrough ( in_report_id int, in_id int ) returns INT AS $$

    DECLARE
        in_user TEXT;
        pending_entry reports;
    BEGIN
        select into in_user from current_user;
        
        select into pending_entry from reports where report_id = in_report_id and id = in_id;
        
        IF NOT FOUND THEN
            -- Raise an exception.
            RAISE EXCEPTION "Cannot find entry.";
        ELSE IF pending_entry.errorcode <> 4 THEN 
            -- Only error codes of 4 may be "passed through" safely.
            RAISE EXCEPTION "Selected entry not permitted to be passed through.";
            
        ELSE
            -- Then we mark it passthroughable, and "approve" will overlook it.
            update reports set overlook = 't', errorcode = 0 where report_id = in_report_id and id = in_id;
            return 0;
        END IF;
    END;

$$ language 'plpgsql';

CREATE OR REPLACE FUNCTION reconciliation__correct_bank_charge (in_report_id int, in_id int) returns INT AS $$

    DECLARE
        in_user TEXT;
        pending_entry reports;
    BEGIN
    
        IF NOT FOUND THEN
             -- Raise an exception.
             RAISE EXCEPTION "Cannot find entry with ID % in report %.", in_id, in_report_id;
         ELSE IF pending_entry.errorcode <> 2 THEN 
             -- Only error codes of 2 may be "passed through" safely.
             RAISE EXCEPTION "Attempt to retroactively add a non-bank-charge entry to the ledger.";
         
         ELSE
             -- Then we mark it passthroughable, and "approve" will overlook it.
             
             select create_entry (pending_entry.their_balance, 'payable', pending_entry.clear_date, 'Bank charge');
             
             update reports set errorcode = 0 where report_id = in_report_id and id = in_id;
             return 0;
         END IF;
    END;

$$ LANGUAGE 'plpgsql';

CREATE OR REPLACE FUNCTION reconciliation__correct_unaccounted_charge (in_report_id int, in_id int, reason TEXT) RETURNS INT AS $$

    DECLARE
        in_user TEXT;
        pending_entry reports;
        note TEXT;
    BEGIN
    
        IF NOT FOUND THEN
             -- Raise an exception.
             RAISE EXCEPTION "Cannot find entry with ID % in report %.", in_id, in_report_id;
         ELSE IF pending_entry.errorcode <> 3 THEN 
             -- Only error codes of 3 may be "passed through" safely.
             RAISE EXCEPTION "Not an unaccounted charge; cannot be retroactively added to the ledger.";
         
         ELSE
             -- Then we mark it passthroughable, and "approve" will overlook it.
             
             note := 'Retroactive addition of an unaccounted entry, of value %. 
             Being added by user % with the following explanation: %', pending_entry.their_balance, in_user, in_reason;
             
             select create_entry (pending_entry.their_balance, 'payable', pending_entry.clear_date,note);
             
             update reports set errorcode = 0 where report_id = in_report_id and id = in_id;
             return in_id;
         END IF;
    END;
$$ language 'plpgsql';

CREATE OR REPLACE FUNCTION reconciliation__report_approve (in_report_id INT) returns INT as $$
    
    -- Does some basic checks before allowing the approval to go through; 
    -- moves the approval to "reports", I guess, or some other "final" table.
    --
    -- Pending may just be a single flag in the database to mark that it is
    -- not finalized. Will need to discuss with Chris.
    
    DECLARE
        current_row RECORD;
        completed reports;
        total_errors INT;
        in_user TEXT;
    BEGIN
        
        select into in_user current_user;
        select into current_row distinct on user * from reports where report_id = in_report_id;
        IF NOT FOUND THEN
            RAISE EXCEPTION "Fatal Error: Pending report % not found", in_report_id;
        END IF;
        
        IF current_row.user = in_user THEN
            RAISE EXCEPTION "Fatal Error: User % cannot self-approve report!", in_user;
        END IF;
        
        SELECT INTO total_errors count(*) from reports where report_id = in_report_id and error_code <> 0;
        
        IF total_errors <> 0 THEN
            RAISE EXCEPTION "Fatal Error: Cannot approve while % uncorrected errors remain.", total_errors;
        END IF;
        
        -- so far, so good. Different user, and no errors remain. Therefore, we can move it to completed reports.
        --
        -- User may not be necessary - I would think it better to use the 
        -- in_user, to note who approved the report, than the user who
        -- filed it. This may require clunkier syntax..
        
        -- 
        
        update reports set approved = 't', clear_time = now() where report_id = in_report_id;
        
        return 1;        
    END;

$$ language 'plpgsql';

CREATE OR REPLACE FUNCTION reconciliation__new_report_id () returns INT as $$

    SELECT nextval('pending_report_report_id_seq');

$$ language 'sql';

create or replace function reconciliation__add_entry(
    in_report_id INT, 
    in_scn INT, 
    in_amount numeric, 
    in_account INT, 
    in_user TEXT, 
    in_date TIMESTAMP
) RETURNS INT AS $$
    
    DELCARE
        la RECORD;
        errorcode INT;
        our_value NUMERIC;
        lid INT;
    BEGIN
    
        SELECT INTO la FROM acc_trans gl 
        JOIN chart c on gl.chart_id = c.id
        JOIN ap ON gl.trans_id = ap.id
        JOIN coa_to_account cta on cta.chart_id = gl.chart_id
        WHERE gl.source ~ in_scn -- does it look like it?
        and cta.account = in_account 
        and gl.amount = in_amount
        AND gl.transdate = in_date;
        
        lid := NULL;
        IF NOT FOUND THEN
            -- they have it, we don't. This is Bad, and implies either a bank
            -- charge or an unaccounted cheque.
            
            if in_scn <> '' and in_scn IS NOT NULL THEN
            
                -- It's a bank charge. Approval action will probably be 
                -- adding it as an entry to the general ledger.
                errorcode := 2; 
                our_value := 0;
            ELSE
                -- Okay, now this is bad.
                -- They have a cheque/sourced charge that we don't. 
                -- REsolution action is going to be
                errorcode := 3;
                our_value := 0;
            END IF;
            
        ELSE if la.amount <> in_amount THEN
        
            errorcode := 1;
            our_value := la.amount;
            lid := la.entry_id;
            
        ELSE
            -- it reconciles. No problem.
            
            errorcode := 0;
            our_value := la.amount;
            lid := la.entry_id;
            
        END IF;
        
        INSERT INTO reports (
                report_id,
                scn,
                their_balance,
                our_balance,
                error_code, 
                user,
                clear_time,
                ledger_id
            ) 
            VALUES (
                in_report_id,
                in_scn,
                in_amount,
                gl.amount,
                errorcode,
                in_user,
                in_date,
                lid
            );
            
        -- success, basically. This could very likely be collapsed to
        -- do the compare check here, instead of in the Perl app. Save us a DB
        -- call.
        return 1; 
        
    END;    
$$ language 'plpgsql';

create or replace function reconciliation__pending_transactions (in_month DATE) RETURNS setof acc_trans as $$
    
    DECLARE
        gl_row acc_trans;
    BEGIN
        FOR gl_row IN
            select gl.* from acc_trans gl, reports pr 
            where gl.cleared = 'f' 
            and date_trunc('month',gl.transdate) <= date_trunc('month', in_month)
            and gl.entry_id <> pr.ledger_id -- there's no entries in the reports for this
        LOOP
            RETURN NEXT gl_row;
        END LOOP;
    END;
$$ LANGUAGE plpgsql;

CREATE OR REPLACE FUNCTION reconciliation__report (in_report_id INT) RETURNS setof reports as $$

    DECLARE
        row reports;
    BEGIN    
        FOR row IN select * from reports where report_id = in_report_id LOOP
        
            RETURN NEXT row;
        
        END LOOP;    
    END;

$$ language 'plpgsql';

CREATE OR REPLACE FUNCTION reconciliation__get_total (in_report_id INT) returns setof reports AS $$

    DECLARE
        row reports;
    BEGIN
    
        SELECT INTO row FROM reports 
        WHERE ledger_id IS NULL 
        and report_id = in_report_id 
        AND scn = -1;
        
        IF NOT FOUND THEN -- I think this is a fairly major error condition
            RAISE EXCEPTION "No Bank Total found.";
        ELSE
            return row;
        END IF;
    END;

$$ language 'plpgsql';

CREATE OR REPLACE FUNCTION reconciliation__corrections (in_report_id INT, in_id INT) returns setof report_corrections AS $$

    DECLARE
        corr report_corrections;
    BEGIN
    
        SELECT INTO corr FROM report_corrections WHERE report_id = in_report_id AND id = in_id LIMIT 1;
        IF NOT FOUND THEN
            RAISE EXCEPTION "No corrections for selected entry.";
        ELSE
            FOR corr IN select * from report_corrections WHERE report_id = in_report_id AND id = in_id LOOP
                RETURN NEXT corr;
            END LOOP;
        END IF;
    END;

$$ language 'plplsql';

CREATE OR REPLACE FUNCTION reconciliation__single_entry (in_report_id INT, in_id INT) returns setof reports AS $$

    DECLARE
        row reports;
    BEGIN
    
        SELECT INTO row FROM reports WHERE report_id = in_report_id and id = in_id LIMIT 1; 
        -- if there's more than one, that's a Bad Thing
        
        IF NOT FOUND THEN
            RAISE EXCEPTION "Could not find selected report entry";
        ELSE
            RETURN row;
        END IF;
    END;

$$ language 'plpgsql';

CREATE OR REPLACE FUNCTION reconciliation__search (
    in_date_begin DATE, 
    in_date_end DATE, 
    in_account TEXT,
    in_status TEXT
) RETURNS setof reports AS $$

    DECLARE
        row reports;
        statement text;
        where_stmt text;
        v_status BOOLEAN;
        v_accum NUMERIC;
    BEGIN
        
        if in_status = "pending" then
            v_status = 'ft'::bool;
        ELSIF in_status = "approved" THEN
        
            v_status = 't'::bool;
        END IF;
        
        IF in_date_begin IS NOT NULL
            or in_date_end IS NOT NULL
            or in_account IS NOT NULL
            or v_status IS NOT NULL
        THEN
            statement = "select pr.* from reports pr ";
            statement = statement + "join acc_trans at on pr.ledger_id = at.entry_id ";
            
            IF in_account IS NOT NULL THEN
                
                statement = statement + "join chart c on at.chart_id = c.id ";
                where_stmt = "c.accno =~ " + quote_literal(in_account) + " AND ";
            END IF;
            
            IF in_date_begin IS NOT NULL THEN
                where_stmt = where_stmt + "insert_time >= " + quote_literal(in_date_begin) + " AND ";
            END IF;
            
            IF in_date_end IS NOT NULL THEN
                where_stmt = where_stmt + "insert_time <= " + quote_literal(in_date_end) + " AND ";
            END IF;
            
            IF in_status IS NOT NULL THEN
                
                if v_status == 't'::bool THEN
                    where_stmt = where_stmt + " approved = 't'::bool AND ";
                ELSIF v_status == 'f'::bool THEN
                    where_stmt = where_stmt + " approved = 'f'::bool AND ";
                END IF;
            
            END IF;
            
            FOR row in EXECUTE statement LOOP
                RETURN NEXT row;
            END LOOP;
        ELSE
        
            FOR row IN SELECT * FROM reports LOOP
                RETURN NEXT row;
            END LOOP;
        
        END IF;
    END;
$$ language 'plpgsql';

create type recon_accounts as (
    name text,
    id int
);

create or replace function reconciliation__account_list () returns setof recon_accounts as $$
    SELECT 
        coa.accno || ' ' || coa.description as name,
        coa.id as id
    FROM chart coa, coa_to_account cta
    WHERE cta.chart_id = coa.id;
$$ language sql;