Glacier Tax Prep Stock Transactions Helper

Help nonresident alien fill in the 1099-B stock transactions by importing transactions from local CSV file

// ==UserScript==
// @name         Glacier Tax Prep Stock Transactions Helper
// @namespace    http://tampermonkey.net/
// @version      1.3
// @description  Help nonresident alien fill in the 1099-B stock transactions by importing transactions from local CSV file
// @author       Tony Chen, JiahaoShan
// @match        https://www.glaciertax.com/IRSForm/StockTransaction*
// @icon         data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyNpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNS1jMDE0IDc5LjE1MTQ4MSwgMjAxMy8wMy8xMy0xMjowOToxNSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIChNYWNpbnRvc2gpIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOkZERjUzOEI4RkFCNTExRTY5N0ZCQTY1Rjg3QjYyODA1IiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOkZERjUzOEI5RkFCNTExRTY5N0ZCQTY1Rjg3QjYyODA1Ij4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6RTVBMUY1NzVGQUI1MTFFNjk3RkJBNjVGODdCNjI4MDUiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6RTVBMUY1NzZGQUI1MTFFNjk3RkJBNjVGODdCNjI4MDUiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz5M9rAoAAACDklEQVR42qSTMW/TQBTH37s7n89OUpwoaapUohQhSiUEDAyoAxILH4SBL8InYGJkYAGJjREkRBdmJAaKioAWSlKHpE7SxHbsO56dtlDcTLX85LOt93+//3t3aIyB81zieDHt7oOWdpNJS4GBgioVQmGrCBhrI70j4mmB7WdP7uLa+nO33vQo3wAyulmWCdlaTyNMo9Cv39q4bdvKV0rleexYIG42Vlob9z3VaDlR/7erqnVXli+4yqu7lltyGzfuOKJUqSWTiSTFogX6lCRxmNheDZx6E4Y72xD2fCi3VmD0awdq124SDSTEbs7swcwoIQkJXLlQqXhQXl6FKOiBd3kdpoejzPj8JuYU5Dns+/lap2n+lCSEjOeM+Jd2DgFVSA4HEHY7gJyfEs6sZOySIbB/SNj/SJh1n5JPgs0iT+bcfAyngxedznwBrVMaWVyIrGqcTJ2nm28et4Pg3pkWjNZQWlwGe6GaV2XCOjKMwIWAsQErYPzBUndvCHD9bYGASwk/37+G4OsWOCQUDfow9tsw6e3n4gyZiStVMIhRcR8g/bak1HGUzz+fCCWm4xEgkeDFK1SBi5RoDOOiIBAG3ehg68MnyyktET6Odr+AkIrQrXw6o/au1v6evdg/cERqfysIVFfXNn+8e/WIti7PTs7w++fZ/I8mZlJNRyStPIwneuHS1Zcn5Oc9zn8EGADf1crUyv7gZAAAAABJRU5ErkJggg==
// @require      https://greasyfork.org/scripts/370520-super-gm-set-and-get/code/Super%20GM%20set%20and%20get.js?version=614650
// @grant        GM_getValue
// @grant        GM_setValue
// @license      MIT
// ==/UserScript==

var transactions = null;
var pid = null;


(function () {
    'use strict';

    loadLayout();

    var fileInput = document.getElementById('fileInput');
    var payorId = document.getElementById("payorId");
    var button = document.getElementById("process");

    payorId.addEventListener("change", _ => {
        if (!validatePayorId()) {
            fileInput.disabled = true;
            button.disabled = true;
        } else {
            fileInput.disabled = false;
            pid = payorId.value;
        }
    });

    fileInput.addEventListener("change", event => {
        if (fileInput.files.length != 0) {
            button.disabled = false;
        } else {
            button.disabled = true;
        }
    });

    button.addEventListener("click", _ => {
        importFile(fileInput.files[0]);
    });

    if (GM_SuperValue.get("status", "") == "importing") {
        fillTransaction();
    } else {
        GM_SuperValue.set("transactions", 0);
        GM_SuperValue.set("payorId", 0);
        GM_SuperValue.set("index", 0);
    }
})();

function loadLayout() {
    var css = document.createElement("style");
    css.innerHTML = ".myInput {margin-left: 20px;}";
    document.body.appendChild(css);

    var availablePayorIds = getPayorIds();
    var options = `<option value='' disabled selected>Select Payor ID (EIN)</option>\n`;

    for (let id of availablePayorIds) {
        options += `<option value='${id}'>${id}</option>\n`;
    }

    var importerHTML = `
            <div id='helper'>
                <h2>Glacier Tax Prep Form 1099-B Stock Transactions Importer</h2>
                <fieldset>
                    <legend>Instruction</legend>
                    <ol>
                        <li>
                            Add 1099-B forms in the previous page.
                            If you get Consolidated Form 1099 PDF from Robinhood, <a href='https://github.com/joshfraser/robinhood-to-csv' target='_blank'>you may export it to .csv file</a>.
                        </li>
                        <li>Select a desired Payor ID (EIN) in the dropdown list.</li>
                        <li>
                            Choose the corresponding local .csv file. Make sure your .csv file follow the format strictly as below: </br>
                            <code>
                            name,acquired,sold,proceeds,cost</br>
                            ALPHABET INC CLASS A COMMON STOCK,10/19/2016,11/03/2016,779.07,824.52</br>
                            TESLA MOTORS INC,12/02/2016,12/06/2016,1468.19,1460.64
                            </code>
                        </li>
                        <li>The "Start" button will be enabled once valid EIN has been selected and csv file has been provided.</li>
                    </ol>
                    <ul>
                        <li>
                            Disclaimer: This greasyfork user script is NOT an official tool from Glacier Tax Prep.
                        </li>
                        <li>
                            Please submit an <a href='https://github.com/TonyCSB/Glacier-tax-1099-B-Stock-Transactions-Helper/issues/new' target="_blank">issue</a> on <a href='https://github.com/TonyCSB/Glacier-tax-1099-B-Stock-Transactions-Helper' target="_blank">GitHub</a> for bug reports and feature requests.
                        </li>
                    </ul>
                </fieldset>
                <fieldset>
                    <legend>Select Payor ID (EIN) and Choose .csv File</legend>
                    <span class='myInput'>
                        <label for="payorId">Payor ID (EIN): </label>
                        <select name="payorId" id="payorId">
                            ${options}
                        </select>
                    </span>
                    <span class='myInput'><input id='fileInput' type='file' id='input' accept='text/*,.csv' disabled></span>
                    <span class='myInput'><button id='process' disabled>Start</button></span>
                    <span class='myInput' style='display:none;color:white;font-size:1.2em;background-color:red;' id='errorMessage'></span>
                </fieldset>
            </div>
        `;
    $("#main").prepend(importerHTML);
}

function getPayorIds() {
    var payorIds = [];
    var trs = $('tbody').find('tr');
    for (var i = 0; i < trs.length; ++i) {
        if (trs[i].childElementCount == 4) {
            if ($(trs[i]).find("td:nth-child(2)")[0].innerText == "Total Proceeds (Box 2) 1099-B") {
                payorIds.push($(trs[i]).find("td:nth-child(1)")[0].innerText);
            }
        }
    }
    return payorIds;
}

function validatePayorId() {
    return getPayorIds().includes(payorId.value);
}

function importFile(file) {
    var textType = /.*\.csv/;
    if (file.name.match(textType)) {
        var reader = new FileReader();
        reader.onload = _ => {
            var lines = processData(reader.result);
            if (!lines) {
                fileInput.value = "";
                return;
            }

            transactions = lines;
            hideErrorMessage();

            GM_SuperValue.set("transactions", transactions);
            GM_SuperValue.set("payorId", pid);
            GM_SuperValue.set("index", 0);
            GM_SuperValue.set("status", "importing")

            fillTransaction();
        }

        reader.readAsText(file);
    } else {
        $("#process").hide();
        showErrorMessage("ERROR: only csv with header 'name,acquired,sold,proceeds,cost' is supported!");
    }
}

function getTotalProceeds() {
    if (!validatePayorId()) return -1;
    var trs = $('tbody').find('tr');
    for (var i = 0; i < trs.length; ++i) {
        var isTarget = $(trs[i]).find("td")[0].innerText == payorId.value;
        if (isTarget) {
            return $(trs[i]).find("td:nth-child(3)").text().trim();
        }
    }
}

function validateDate(dateString) {
    return dateString.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/) ? true : false;
}

function validateInputFile(lines) {
    var inputTotalProceeds = 0;
    for (var i = 0; i < lines.length; i++) {
        if (!lines[i][0] || lines[i][0] == "") {
            showErrorMessage(getErrorMessage(i, "empty name"));
            return false;
        }
        if (!lines[i][1] || !validateDate(lines[i][1])) {
            showErrorMessage(getErrorMessage(i, "invalid acquired date"));
            return false;
        }
        if (!lines[i][2] || !validateDate(lines[i][2])) {
            showErrorMessage(getErrorMessage(i, "invalid sold date"));
            return false;
        }
        if (!lines[i][3] || isNaN(lines[i][3]) || parseFloat(lines[i][3]) < 0) {
            showErrorMessage(getErrorMessage(i, "invalid proceeds number"));
            return false;
        }
        if (!lines[i][4] || isNaN(lines[i][4]) || parseFloat(lines[i][4]) < 0) {
            showErrorMessage(getErrorMessage(i, "invalid cost number"));
            return false;
        }
        var acquiredDate = new Date(lines[i][1]);
        var soldDate = new Date(lines[i][2]);
        if (acquiredDate.getTime() > soldDate.getTime()) {
            showErrorMessage(getErrorMessage(i, "invalid acquired date or sold date"));
            return false;
        }
        inputTotalProceeds += parseFloat(lines[i][3]);
    }
    var inputTotal = inputTotalProceeds.toFixed(2);
    var totalProceeds = parseFloat(getTotalProceeds()).toFixed(2);
    if (totalProceeds == -1.00) {
        return false;
    } else if (inputTotal != totalProceeds) {
        alert("Total proceeds in the input file (" + inputTotal + ") doesn't match the total proceeds in the 1099 (" + totalProceeds + "). You may proceed, but it may cause errors.");
        return true;
    }
    return true;
}

function processData(allText) {
    var allTextLines = allText.split(/\r\n|\n/);
    var headers = allTextLines[0].split(',');
    if (headers.length != 5) {
        showErrorMessage('ERROR: Invalid number of headers');
        return false;
    }
    if (headers[0] != 'name' || headers[1] != 'acquired' || headers[2] != 'sold' || headers[3] != 'proceeds' || headers['4'] != 'cost') {
        showErrorMessage("ERROR: only csv with header 'name,acquired,sold,proceeds,cost' is supported!");
        return false;
    }
    var lines = [];
    for (var i = 1; i < allTextLines.length; i++) {
        var data = allTextLines[i].split(',');
        if (data.length == headers.length) {
            var tarr = [];
            for (var j = 0; j < headers.length; j++) {
                if (headers[j] == 'acquired') {
                    var acquiredData = data[j].split(' ');
                    if (acquiredData && acquiredData.length > 1) {
                        data[j] = acquiredData[acquiredData.length - 1];
                    }
                }
                tarr.push(data[j]);
            }
            lines.push(tarr);
        }
    }

    return validateInputFile(lines) ? lines : false;
}

function hideErrorMessage() {
    $("#errorMessage").hide();
}

function showErrorMessage(message) {
    $("#errorMessage").css("background-color", "red");
    $("#errorMessage").show().html(message);
}

function getErrorMessage(lineNumber, message) {
    return "ERROR: The " + (lineNumber + 1) + getOrdinalIndicator(lineNumber + 1) + " line has " + message;
}

// GM_SuperValue.set("transactions", transactions);
// GM_SuperValue.set("payorId", pid);
// GM_SuperValue.set("index", 0);
// GM_SuperValue.set("status", "importing")

function fillTransaction() {
    var transactions = GM_SuperValue.get("transactions");
    var pid = GM_SuperValue.get("payorId");
    var index = GM_SuperValue.get("index");
    if (index === undefined) {
        index = 0;
    }

    var filling = $("#Name").length ? true : false;
    if (filling) {
        var transaction = transactions[index];
        GM_SuperValue.set("index", index + 1);
        console.log(index);
        console.log(transaction);
        if (index + 1 >= transactions.length) {
            GM_SuperValue.set("status", "done");
            location.reload(false);
        }
        $("#Name").val(transaction[0]);
        if ($("#PurchasedDateString").length != 0) {
            $("#PurchasedDateString").val(transaction[1]);
        }
        $("#SoldDateString").val(transaction[2]);
        $("#SalesPrice").val(transaction[3]);
        $("#PurchasePrice").val(transaction[4]);
        document.forms[0].submit();
    } else {
        var trs = $('tbody').find('tr');
        for (var i = 0; i < trs.length; ++i) {
            var isTarget = $(trs[i]).find("td")[0].innerText == pid;
            if (isTarget && index < transactions.length) {
                $(trs[i]).find("td:nth-child(4) a:nth-child(1)")[0].click()
            }
        }
    }
}