OpenConcerto

Dépôt officiel du code source de l'ERP OpenConcerto
sonarqube

svn://code.openconcerto.org/openconcerto

Rev

Rev 151 | Blame | Compare with Previous | Last modification | View Log | RSS feed

/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 * 
 * Copyright 2011 OpenConcerto, by ILM Informatique. All rights reserved.
 * 
 * The contents of this file are subject to the terms of the GNU General Public License Version 3
 * only ("GPL"). You may not use this file except in compliance with the License. You can obtain a
 * copy of the License at http://www.gnu.org/licenses/gpl-3.0.html See the License for the specific
 * language governing permissions and limitations under the License.
 * 
 * When distributing the software, include this License Header Notice in each file.
 */
 
 package org.openconcerto.erp.core.sales.pos.model;

import org.openconcerto.erp.config.ComptaPropsConfiguration;
import org.openconcerto.erp.core.sales.pos.POSConfiguration;
import org.openconcerto.erp.core.sales.pos.model.RegisterLog.EventType;
import org.openconcerto.erp.core.sales.pos.model.RegisterLogEntry.ReceiptEntry;
import org.openconcerto.erp.core.sales.pos.model.RegisterLogEntry.RegisterEntry;
import org.openconcerto.erp.core.sales.pos.model.RegisterState.Status;
import org.openconcerto.sql.model.Order;
import org.openconcerto.sql.model.SQLRow;
import org.openconcerto.sql.model.SQLRowListRSH;
import org.openconcerto.sql.model.SQLRowValues;
import org.openconcerto.sql.model.SQLSelect;
import org.openconcerto.sql.model.SQLTable;
import org.openconcerto.sql.model.TableRef;
import org.openconcerto.sql.model.Where;
import org.openconcerto.utils.CompareUtils;
import org.openconcerto.utils.Value;

import java.io.IOException;
import java.nio.file.Path;
import java.sql.SQLException;
import java.text.ParseException;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;

import org.jdom2.JDOMException;

public class CheckIntegrity {

    public static void main(String[] args) throws JDOMException, IOException {
        final POSConfiguration posConf = POSConfiguration.setInstance();
        final ComptaPropsConfiguration conf = posConf.createConnexion();
        try {
            for (final RegisterFiles files : RegisterFiles.scan(posConf.getRootDir())) {
                final RegisterDB registerDB = new RegisterDB(conf.getDirectory(), conf.getProductInfo(), files.getPosID());
                checkRegisterFiles(registerDB, files);
                checkRegisterRow(registerDB, files);
            }
            System.out.println("\n\nAll done");
        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            posConf.closeConnexion();
        }
    }

    private static void checkRegisterRow(final RegisterDB registerDB, final RegisterFiles files) throws IOException, JDOMException, ParseException, SQLException {
        final RegisterState localRegisterState = files.getLastLog().getRegisterState();
        final DBState dbState = registerDB.fetchRegisterState();
        final RegisterState remoteRegisterState = dbState.getRegisterState();
        if (!localRegisterState.equals(remoteRegisterState))
            System.out.println("WARNING FS and DB state not equal (this may be fixed by launching the application) :\n" + localRegisterState + "\n" + remoteRegisterState);

        final SQLTable logT = registerDB.getLogElement().getTable();
        final SQLSelect sel = new SQLSelect();
        sel.addSelect(logT.getKey());
        setWhereAndOrder(sel, registerDB, logT);
        final Number lastLogID = (Number) logT.getDBSystemRoot().getDataSource().executeScalar(sel.asString());
        final SQLRowValues registerLastEntry = dbState.getLastEntry();
        final Number registerLastEntryID = registerLastEntry == null ? null : registerLastEntry.getIDNumber();
        if (!Objects.equals(registerLastEntryID, lastLogID))
            throw new IllegalStateException("Last log entry referenced by the register " + registerLastEntryID + " isn't the last in the log table " + lastLogID);

        final SQLTable closureT = registerDB.getClosureElement().getTable();
        final SQLSelect selClosure = new SQLSelect();
        selClosure.addSelect(closureT.getKey());
        final TableRef entryRef = selClosure.addJoin("INNER", closureT.getField("ID_ENTREE_JOURNAL")).getJoinedTable();
        setWhereAndOrder(selClosure, registerDB, entryRef);
        final Number lastClosureID = (Number) logT.getDBSystemRoot().getDataSource().executeScalar(selClosure.asString());
        final SQLRowValues registerLastClosure = dbState.getLastClosure();
        final Number registerLastClosureID = registerLastClosure == null ? null : registerLastClosure.getIDNumber();
        if (!Objects.equals(registerLastClosureID, lastClosureID))
            throw new IllegalStateException("Last closure referenced by the register " + registerLastClosureID + " isn't the last in its table " + lastClosureID);
    }

    private static void setWhereAndOrder(final SQLSelect sel, final RegisterDB registerDB, final TableRef entryRef) {
        sel.setWhere(new Where(entryRef.getField("ID_CAISSE"), "=", registerDB.getPosID()));
        sel.addFieldOrder(entryRef.getField("DATE"), Order.desc());
        sel.setLimit(1);
    }

    private static void checkRegisterFiles(final RegisterDB registerDB, final RegisterFiles files) throws IOException, JDOMException, ParseException {
        Value<RegisterLog> lastLog = Value.getNone();
        for (final Path logFile : files.findLogFiles()) {
            System.out.println("Checking " + logFile);
            final RegisterLog log = new RegisterLog(logFile).parse();
            try {
                checkOneLog(files, lastLog, log, registerDB);
                System.out.println("OK for " + logFile);
            } catch (Exception e) {
                // keep checking other files
                System.err.println("Error for " + logFile);
                e.printStackTrace();
            }
            lastLog = Value.getSome(log);
        }
    }

    private static void checkOneLog(final RegisterFiles files, final Value<RegisterLog> previousLog, final RegisterLog log, final RegisterDB registerDB)
            throws IOException, JDOMException, ParseException {
        if (log.getFirstRegisterEvent().getRegisterID() != files.getPosID())
            throw new IllegalStateException("Opening register ID mismatch");
        final SQLTable logT = registerDB.getLogElement().getTable();

        Date cal;
        // this checks this log is chained to the previous
        if (previousLog.hasValue()) {
            final RegisterLogEntry lastEntry = previousLog.getValue().getLastEvent();
            if (lastEntry.getType() != EventType.REGISTER_CLOSURE)
                throw new IllegalStateException("Previous log isn't closed");
            final RegisterEntry previousLogClosure = (RegisterEntry) lastEntry;
            if (!Objects.equals(previousLogClosure.getLastReceiptHash(), log.getFirstRegisterEvent().getLastReceiptHash()))
                throw new IllegalStateException("Register opening hash mismatch, chain broken");
            if (RegisterFiles.isNotChronological(previousLogClosure.getDate(), log.getFirstRegisterEvent().getDate()))
                throw new IllegalStateException("Register opening before previous closure");
            cal = previousLogClosure.getDate();
        } else {
            cal = null;
        }
        final Date expectedPreviousDate;
        // getPreviousDate() was added in version 2
        if (log.getVersion() < 2 || !previousLog.hasValue()) {
            expectedPreviousDate = null;
        } else {
            expectedPreviousDate = previousLog.getValue().getFirstRegisterEvent().getDate();
        }
        if (!Objects.equals(expectedPreviousDate, log.getFirstRegisterEvent().getPreviousDate()))
            throw new IllegalStateException("Previous opening date of this log (" + log.getFirstRegisterEvent() + ") doesn't match the previous log " + expectedPreviousDate);

        final List<ReceiptEntry> receiptEvents = log.getReceiptEvents();

        // check all event dates are chronological
        final List<RegisterLogEntry> allEvents = log.getAllEvents();
        for (final RegisterLogEntry e : allEvents) {
            final Date newDate = e.getDate();
            if (cal != null && RegisterFiles.isNotChronological(cal, newDate))
                throw new IllegalStateException("Later event before last one");
            cal = newDate;
        }

        // check log opening was stored in the DB
        checkLogTable(logT, log.getFirstRegisterEvent());
        // this checks the hash chain and the coherence of the log and the receipts
        final List<Ticket> receipts = log.parseReceipts();
        // this checks the closure
        if (log.getRegisterState().getStatus() == Status.CLOSED) {
            final ReceiptEntry lastReceiptCreationEvent = log.getLastReceiptCreationEvent();
            assert receipts.isEmpty() == (lastReceiptCreationEvent == null);
            // parseReceipts() has already checked that lastReceiptCreationEvent.getFileHash()
            // matches the receipt
            // if there's no receipt in this log, opening and closure should match
            // the opening has already been checked above to match the previous log
            final String lastReceiptHash = receipts.isEmpty() ? log.getFirstRegisterEvent().getLastReceiptHash() : lastReceiptCreationEvent.getFileHash();
            final RegisterEntry lastRegisterEvent = log.getLastRegisterEvent();
            if (!CompareUtils.equals(lastRegisterEvent.getLastReceiptHash(), lastReceiptHash))
                throw new IllegalStateException("Closure receipt hash mismatch, recorded " + lastRegisterEvent.getLastReceiptHash() + " but was " + lastReceiptHash);
            if (lastRegisterEvent.getRegisterID() != files.getPosID())
                throw new IllegalStateException("Closure register ID mismatch");

            // check log closure was stored in the DB
            final Number closureEntryID = checkLogTable(logT, lastRegisterEvent);

            final SQLRow closureRow = getClosureRow(registerDB.getClosureElement().getTable(), lastRegisterEvent, closureEntryID);
            // TODO this doesn't check dates
            final SQLRowValues expected = RegisterDB.fillRow(new SQLRowValues(closureRow.getTable()), receiptEvents, receipts);
            for (final String expectedField : expected.getFields()) {
                final Object expectedVal = expected.getObject(expectedField);
                final Object dbVal = closureRow.getObject(expectedField);
                final boolean equals;
                // needed for BigDecimal
                if (expectedVal instanceof Comparable) {
                    equals = CompareUtils.compare(expectedVal, dbVal) == 0;
                } else {
                    equals = Objects.equals(expectedVal, dbVal);
                }
                if (!equals)
                    throw new IllegalStateException("Closure row data doesn't match log for " + expectedField + " : " + expectedVal + " " + dbVal);
            }
        }

        final SQLTable registerT = registerDB.getReceiptElement().getTable();
        final SQLSelect selReceipts = new SQLSelect();
        selReceipts.addSelectStar(registerT);
        selReceipts.setWhere(new Where(registerT.getField("ID_CAISSE"), "=", files.getPosID()));
        final Date lowerBound = log.getFirstRegisterEvent().getDate();
        final Where dateWhere;
        final int expectedRowsCount;
        if (log.getRegisterState().getStatus() == Status.CLOSED) {
            final Date upperBound = log.getLastEvent().getDate();
            dateWhere = new Where(registerT.getField("DATE"), lowerBound, upperBound);
            expectedRowsCount = receipts.size();
        } else {
            dateWhere = new Where(registerT.getField("DATE"), ">=", lowerBound);
            // not yet in the DB
            expectedRowsCount = 0;
        }
        selReceipts.andWhere(dateWhere);
        selReceipts.addFieldOrder(registerT.getField("DATE"));
        final List<SQLRow> receiptRows = SQLRowListRSH.execute(selReceipts);
        if (receiptRows.size() != expectedRowsCount)
            throw new IllegalStateException("Receipts count in the DB (" + receiptRows.size() + ") doesn't match log (" + expectedRowsCount + ")");
        if (expectedRowsCount > 0) {
            final Iterator<SQLRow> iter = receiptRows.iterator();
            final Iterator<ReceiptEntry> receiptEventsIter = receiptEvents.iterator();
            for (final Ticket receipt : receipts) {
                final SQLRow row = iter.next();
                final ReceiptEntry receiptEvent = receiptEventsIter.next();

                try {
                    if (!row.getString("NUMERO").equals(receipt.getCode()))
                        throw new IllegalStateException("Code in the DB doesn't match log");
                    if (row.getDate("DATE").compareTo(receipt.getCreationCal()) != 0)
                        throw new IllegalStateException("Date in the DB doesn't match log");
                    if (!row.getString("FILE_HASH").equals(receiptEvent.getFileHash()))
                        throw new IllegalStateException("File hash in the DB doesn't match log : " + row.getString("FILE_HASH") + " != " + receiptEvent.getFileHash());
                    if (!Objects.equals(row.getString("FILE_HASH_PREVIOUS"), receipt.getPreviousHash()))
                        throw new IllegalStateException("Previous file hash in the DB doesn't match log");
                    if (row.getLong("TOTAL_TTC") != receipt.getTotalInCents())
                        throw new IllegalStateException("TTC in the DB " + row.getLong("TOTAL_TTC") + " doesn't match log " + receipt.getTotalInCents());
                    // ATTN the paid amount isn't stored in the DB by
                    // POSConfiguration.importReceipts() so we can't check it exactly
                    if (row.getLong("TOTAL_TTC") > receipt.getPaidTotal())
                        throw new IllegalStateException("Paid amount in the log (" + receipt.getPaidTotal() + ") is less than total in the DB " + row.getLong("TOTAL_TTC"));
                } catch (Exception exn) {
                    throw new IllegalStateException("Error while checking " + row + " against " + receipt + " in " + log, exn);
                }
            }
            assert !iter.hasNext() && !receiptEventsIter.hasNext();
        }
    }

    // check log event was stored in the DB
    private static final Number checkLogTable(final SQLTable logT, final RegisterEntry entry) {
        final SQLSelect sel = new SQLSelect();
        sel.addSelect(logT.getKey());
        sel.setWhere(new Where(logT.getField("ID_CAISSE"), "=", entry.getRegisterID()));
        sel.andWhere(new Where(logT.getField("EVT"), "=", (entry.getType() == EventType.REGISTER_OPENING ? Status.OPEN : Status.CLOSED).name()));
        sel.andWhere(new Where(logT.getField("DATE"), "=", entry.getDate()));
        final List<?> ids = logT.getDBSystemRoot().getDataSource().executeCol(sel.asString());
        if (ids.size() != 1)
            throw new IllegalStateException("Not found in the DB : " + entry);
        return (Number) ids.get(0);
    }

    private static final SQLRow getClosureRow(final SQLTable closureT, final RegisterEntry entry, final Number closureEntryID) {
        final SQLSelect sel = new SQLSelect();
        sel.addSelectStar(closureT);
        sel.setWhere(new Where(closureT.getField("ID_ENTREE_JOURNAL"), "=", closureEntryID));
        final List<SQLRow> rows = SQLRowListRSH.execute(sel);
        if (rows.size() != 1)
            throw new IllegalStateException("Closure row not found for " + entry);
        return rows.get(0);
    }

}