{"id":7705,"date":"2024-05-28T11:15:19","date_gmt":"2024-05-28T18:15:19","guid":{"rendered":"https:\/\/jeremywhittaker.com\/?p=7705"},"modified":"2024-05-28T11:35:35","modified_gmt":"2024-05-28T18:35:35","slug":"automating-fincen-beneficial-ownership-information-reporting-for-multiple-organizations","status":"publish","type":"post","link":"https:\/\/new.jeremywhittaker.com\/index.php\/2024\/05\/28\/automating-fincen-beneficial-ownership-information-reporting-for-multiple-organizations\/","title":{"rendered":"Automating FinCEN Beneficial Ownership Information Reporting for Multiple Organizations"},"content":{"rendered":"\n<p>The Financial Crimes Enforcement Network (FinCEN) now requires most businesses to report their beneficial owners, a mandate stemming from the U.S. Corporate Transparency Act. The penalties for non-compliance are exceptionally harsh. Anyone willfully violating the reporting requirements could face penalties of up to $500 for each day of continued violation, with criminal penalties including up to two years of imprisonment and fines up to $10,000. All the FAQs for BOIR reporting can be found <a href=\"https:\/\/www.fincen.gov\/boi-faqs\">here<\/a>.<\/p>\n\n\n\n<p>Fortunately, if you only have a few companies, filling out this form should take no longer than 15 minutes. You can access the online filing system <a href=\"https:\/\/boiefiling.fincen.gov\/boir\/html\">here<\/a>. However, if you have hundreds of LLCs, this process can quickly become overwhelming, potentially taking days to complete multiple forms. To address this, I&#8217;ve developed a code to automate the process. If all your companies have the same beneficial owners, this script will streamline the reporting process. However, you will still need to verify your humanity and the information filled out when you reach the CAPTCHA at the end.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Using Selenium in Python to Navigate FinCEN<\/h2>\n\n\n\n<h2 class=\"wp-block-heading\">These lines need to be modified with your information<\/h2>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">last_name_field.send_keys(\"Last Name\")\nfirst_name_field.send_keys(\"First Name\")\ndob_field.send_keys(\"01\/01\/1982\")\naddress_field.send_keys(\"Address field\")\ncity_field.send_keys(\"City\")\nfill_dropdown(driver, country_field, \"United States of America\")\nfill_dropdown(driver, state_field, \"Arizona\")\nzip_field.send_keys(\"00000\")\nid_number_field.send_keys(\"Drivers license number\")\nfill_dropdown(driver, id_country_field, \"United States of America\")\nfill_dropdown(driver, id_state_field, \"Arizona\")\nid_file_field.send_keys(\"\/full\/path\/of\/ID.jpeg\")\nlast_name_field_2.send_keys(\"Beneficial Owner #2 Last Name\")\nfirst_name_field_2.send_keys(\"Beneficial Owner #2 First Name\")\ndob_field_2.send_keys(\"Beneficial Owner #2 DOB\")\naddress_field_2.send_keys(\"Beneficial Owner #2 Address\")\ncity_field_2.send_keys(\"Beneficial Owner #2 City\")\nfill_dropdown(driver, state_field_2, \"Beneficial Owner #2 State\")\nzip_field_2.send_keys(\"Beneficial Owner #2 ZIP\")\nfill_dropdown(driver, id_type_field_2, \"State\/local\/tribe-issued ID\")\nid_number_field_2.send_keys(\"Beneficial Owner #2 ID number\")\nfill_dropdown(driver, id_country_field_2, \"United States of America\")\nfill_dropdown(driver, id_state_field_2, \"Arizona\")\nid_file_field_2.send_keys(\"beneficial\/owner2\/full\/path\/of\/ID.jpeg\")\nemail_field.send_keys(\"submitter email\")\nconfirm_email_field.send_keys(\"submitter email confirm\")\nfirst_name_field.send_keys(\"submitter first name\")\nlast_name_field.send_keys(\"submitter last name\")\nfill_field(driver, By.ID, 'rc.jurisdiction', 'reporting company country)\nfill_field(driver, By.ID, 'rc.domesticState', 'reporting company state')\nfill_field(driver, By.ID, 'rc.address.value', 'reporting company address')\nfill_field(driver, By.ID, 'rc.city.value', 'reporting company city')\nfill_field(driver, By.ID, 'rc.country.value', 'reporting company country')\nfill_field(driver, By.ID, 'rc.state.value', 'reporting company state')\nfill_field(driver, By.ID, 'rc.zip.value', 'reporting company zip')\n#should only be true if company was established before 2024    \nclick_element(driver, By.CSS_SELECTOR, 'label[for=\"rc.isExistingReportingCompany\"]', use_js=True)\n\n\n<\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Code<\/h2>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import logging\nimport time\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.service import Service\nfrom selenium.webdriver.common.by import By\nfrom selenium.webdriver.common.keys import Keys\nfrom selenium.webdriver.support.ui import WebDriverWait\nfrom selenium.webdriver.support import expected_conditions as EC\nfrom webdriver_manager.chrome import ChromeDriverManager\nfrom selenium.common.exceptions import WebDriverException, UnexpectedAlertPresentException, NoAlertPresentException\nimport os\nimport signal\n\nimport csv\nimport pandas as pd\n\n# Configure logging\nlogging.basicConfig(level=logging.INFO)\nlogger = logging.getLogger(__name__)\n\n\ndef read_companies_from_csv(file_path):\n    df = pd.read_csv(file_path, dtype={'EIN': str, 'SSN\/ITIN': str})\n    return df\n\ndef update_csv(file_path, df):\n    df.to_csv(file_path, index=False)\n\ndef mark_company_complete(df, company_name):\n    df.loc[df['LLC'] == company_name, 'complete'] = 'Yes'\n\ndef wait_for_element(driver, by, value, timeout=10):\n    try:\n        element = WebDriverWait(driver, timeout).until(\n            EC.presence_of_element_located((by, value))\n        )\n        return element\n    except Exception as e:\n        logger.error(f\"Error waiting for element {value}: {e}\")\n        return None\n\n\ndef click_element(driver, by, value, use_js=False):\n    element = wait_for_element(driver, by, value)\n    if element:\n        try:\n            if use_js:\n                driver.execute_script(\"arguments[0].click();\", element)\n            else:\n                element.click()\n            logger.info(f\"Clicked element: {value}\")\n        except Exception as e:\n            logger.error(f\"Error clicking element {value}: {e}\")\n    else:\n        logger.error(f\"Element not found to click: {value}\")\n\n\ndef fill_field(driver, by, value, text):\n    element = wait_for_element(driver, by, value)\n    if element:\n        try:\n            element.clear()\n            element.send_keys(text)\n            element.send_keys(Keys.RETURN)\n            logger.info(f\"Filled field {value} with text: {text}\")\n        except Exception as e:\n            logger.error(f\"Error filling field {value}: {e}\")\n    else:\n        logger.error(f\"Field not found to fill: {value}\")\n\n\ndef click_yes_button(driver):\n    try:\n        WebDriverWait(driver, 10).until(\n            EC.presence_of_element_located((By.CSS_SELECTOR, 'button[data-testid=\"modal-confirm-button\"]'))\n        )\n        actions = webdriver.ActionChains(driver)\n        actions.send_keys(Keys.TAB * 3)  # Adjust the number of TAB presses as needed\n        actions.send_keys(Keys.ENTER)\n        actions.perform()\n        logger.info(\"Yes button on popup clicked using TAB and ENTER keys.\")\n    except Exception as e:\n        logger.error(f\"Error clicking the Yes button: {e}\")\n\ndef fill_dropdown(driver, element, value):\n    driver.execute_script(\"arguments[0].scrollIntoView(true);\", element)\n    driver.execute_script(\"arguments[0].click();\", element)\n    element.send_keys(value)\n    element.send_keys(Keys.ENTER)\n\ndef fill_beneficial_owners(driver):\n    wait = WebDriverWait(driver, 10)\n\n    last_name_field = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '[id=\"bo[0].lastName.value\"]')))\n    last_name_field.send_keys(\"Last Name\")\n    logger.info(\"Last name field filled.\")\n\n    first_name_field = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '[id=\"bo[0].firstName.value\"]')))\n    first_name_field.send_keys(\"First Name\")\n    logger.info(\"First name field filled.\")\n\n    dob_field = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '[id=\"bo[0].dob.value\"]')))\n    dob_field.send_keys(\"01\/01\/1982\")\n    logger.info(\"Date of birth field filled.\")\n\n    address_field = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '[id=\"bo[0].address.value\"]')))\n    address_field.send_keys(\"Address field\")\n    logger.info(\"Address field filled.\")\n\n    city_field = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '[id=\"bo[0].city.value\"]')))\n    city_field.send_keys(\"City\")\n    logger.info(\"City field filled.\")\n\n    country_field = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '[id=\"bo[0].country.value\"]')))\n    fill_dropdown(driver, country_field, \"United States of America\")\n    logger.info(\"Country field filled.\")\n\n    state_field = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '[id=\"bo[0].state.value\"]')))\n    fill_dropdown(driver, state_field, \"Arizona\")\n    logger.info(\"State field filled.\")\n\n    zip_field = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '[id=\"bo[0].zip.value\"]')))\n    zip_field.send_keys(\"00000\")\n    logger.info(\"ZIP code field filled.\")\n\n    id_type_field = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '[id=\"bo[0].identification.type.value\"]')))\n    fill_dropdown(driver, id_type_field, \"State\/local\/tribe-issued ID\")\n    logger.info(\"ID type field filled.\")\n\n    id_number_field = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '[id=\"bo[0].identification.id.value\"]')))\n    id_number_field.send_keys(\"Drivers license number\")\n    logger.info(\"ID number field filled.\")\n\n    id_country_field = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '[id=\"bo[0].identification.jurisdiction.value\"]')))\n    fill_dropdown(driver, id_country_field, \"United States of America\")\n    logger.info(\"ID country field filled.\")\n\n    id_state_field = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '[id=\"bo[0].identification.state.value\"]')))\n    fill_dropdown(driver, id_state_field, \"Arizona\")\n    logger.info(\"ID state field filled.\")\n\n    id_file_field = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '[id=\"bo[0].identification.image.value\"]')))\n    id_file_field.send_keys(\"\/full\/path\/of\/ID.jpeg\")\n    logger.info(\"ID file uploaded.\")\n\n    add_bo_button = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, 'button[data-testid=\"bo-add-button\"]')))\n    driver.execute_script(\"arguments[0].scrollIntoView(true);\", add_bo_button)\n    driver.execute_script(\"arguments[0].click();\", add_bo_button)\n    logger.info(\"Add Beneficial Owner button clicked.\")\n\n    # Fill out the details for the second beneficial owner\n    last_name_field_2 = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '[id=\"bo[1].lastName.value\"]')))\n    last_name_field_2.send_keys(\"Beneficial Owner #2 Last Name\")\n    logger.info(\"Last name field for second beneficial owner filled.\")\n\n    first_name_field_2 = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '[id=\"bo[1].firstName.value\"]')))\n    first_name_field_2.send_keys(\"Beneficial Owner #2 First Name\")\n    logger.info(\"First name field for second beneficial owner filled.\")\n\n    dob_field_2 = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '[id=\"bo[1].dob.value\"]')))\n    dob_field_2.send_keys(\"Beneficial Owner #2 DOB\")\n    logger.info(\"Date of birth field for second beneficial owner filled.\")\n\n    address_field_2 = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '[id=\"bo[1].address.value\"]')))\n    address_field_2.send_keys(\"Beneficial Owner #2 Address\")\n    logger.info(\"Address field for second beneficial owner filled.\")\n\n    city_field_2 = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '[id=\"bo[1].city.value\"]')))\n    city_field_2.send_keys(\"Beneficial Owner #2 City\")\n    logger.info(\"City field for second beneficial owner filled.\")\n\n    country_field_2 = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '[id=\"bo[1].country.value\"]')))\n    fill_dropdown(driver, country_field_2, \"United States of America\")\n    logger.info(\"Country field for second beneficial owner filled.\")\n\n    state_field_2 = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '[id=\"bo[1].state.value\"]')))\n    fill_dropdown(driver, state_field_2, \"Beneficial Owner #2 State\")\n    logger.info(\"State field for second beneficial owner filled.\")\n\n    zip_field_2 = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '[id=\"bo[1].zip.value\"]')))\n    zip_field_2.send_keys(\"Beneficial Owner #2 ZIP\")\n    logger.info(\"ZIP code field for second beneficial owner filled.\")\n\n    id_type_field_2 = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '[id=\"bo[1].identification.type.value\"]')))\n    fill_dropdown(driver, id_type_field_2, \"State\/local\/tribe-issued ID\")\n    logger.info(\"ID type field for second beneficial owner filled.\")\n\n    id_number_field_2 = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '[id=\"bo[1].identification.id.value\"]')))\n    id_number_field_2.send_keys(\"Beneficial Owner #2 ID number\")\n    logger.info(\"ID number field for second beneficial owner filled.\")\n\n    id_country_field_2 = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '[id=\"bo[1].identification.jurisdiction.value\"]')))\n    fill_dropdown(driver, id_country_field_2, \"United States of America\")\n    logger.info(\"ID country field for second beneficial owner filled.\")\n\n    id_state_field_2 = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '[id=\"bo[1].identification.state.value\"]')))\n    fill_dropdown(driver, id_state_field_2, \"Arizona\")\n    logger.info(\"ID state field for second beneficial owner filled.\")\n\n    id_file_field_2 = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '[id=\"bo[1].identification.image.value\"]')))\n    id_file_field_2.send_keys(\"beneficial\/owner2\/full\/path\/of\/ID.jpeg\")\n    logger.info(\"ID file uploaded.\")\n\n    # Click the Next button to proceed to the submission page\n    next_button = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, 'button[data-testid=\"bottom-next-button\"]')))\n    driver.execute_script(\"arguments[0].scrollIntoView(true);\", next_button)\n    driver.execute_script(\"arguments[0].click();\", next_button)\n    logger.info(\"Next button clicked to proceed to submission page.\")\n\n    logger.info(\"Beneficial owners' information filled out successfully.\")\n\n\n\ndef fill_submit_page(driver):\n    wait = WebDriverWait(driver, 10)\n\n\n    # Fill out the email field\n    email_field = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '[id=\"email\"]')))\n    email_field.send_keys(\"submitter email\")\n    logger.info(\"Email field filled.\")\n\n    # Fill out the confirm email field\n    confirm_email_field = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '[id=\"confirmEmail\"]')))\n    confirm_email_field.send_keys(\"submitter email confirm\")\n    logger.info(\"Confirm email field filled.\")\n\n    # Fill out the first name field\n    first_name_field = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '[id=\"firstName\"]')))\n    first_name_field.send_keys(\"submitter first name\")\n    logger.info(\"First name field filled.\")\n\n    # Fill out the last name field\n    last_name_field = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '[id=\"lastName\"]')))\n    last_name_field.send_keys(\"submitter last name\")\n    logger.info(\"Last name field filled.\")\n\n    # Check the \"I agree\" checkbox\n    agree_checkbox_label = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, 'label[for=\"certifyCB\"]')))\n    driver.execute_script(\"arguments[0].scrollIntoView(true);\", agree_checkbox_label)\n    driver.execute_script(\"arguments[0].click();\", agree_checkbox_label)\n    logger.info(\"I agree checkbox selected.\")\n\n\n\ndef fill_filing_information(driver):\n    # Agree to the terms\n    click_element(driver, By.CSS_SELECTOR, 'button[data-testid=\"agree-button\"]')\n    time.sleep(1)\n\n    # Initial report radio button\n    click_element(driver, By.CSS_SELECTOR, 'label[for=\"fi.filingType.value1\"]')\n\n    # Next button\n    click_element(driver, By.CSS_SELECTOR, 'button[data-testid=\"bottom-next-button\"]', use_js=True)\n\n\n\ndef fill_reporting_company(driver, llc, ein, ssn_itin):\n    wait = WebDriverWait(driver, 10)\n\n    # Fill out the reporting company information\n    fill_field(driver, By.ID, 'rc.legalName', llc)\n\n    if pd.notna(ein):\n        fill_field(driver, By.ID, 'rc.taxType', 'EIN')\n        fill_field(driver, By.ID, 'rc.taxId', ein)\n    elif pd.notna(ssn_itin):\n        fill_field(driver, By.ID, 'rc.taxType', 'SSN\/ITIN')\n        fill_field(driver, By.ID, 'rc.taxId', ssn_itin)\n\n\n    fill_field(driver, By.ID, 'rc.jurisdiction', 'reporting company country)\n    fill_field(driver, By.ID, 'rc.domesticState', 'reporting company state')\n    fill_field(driver, By.ID, 'rc.address.value', 'reporting company address')\n    fill_field(driver, By.ID, 'rc.city.value', 'reporting company city')\n    fill_field(driver, By.ID, 'rc.country.value', 'reporting company country')\n    fill_field(driver, By.ID, 'rc.state.value', 'reporting company state')\n    fill_field(driver, By.ID, 'rc.zip.value', 'reporting company zip')\n\n    # Request to receive FinCEN ID\n    click_element(driver, By.CSS_SELECTOR, 'label[for=\"rc.isRequestingId\"]', use_js=True)\n    logger.info(\"Request to receive FinCEN ID checkbox selected.\")\n\n    # Final Next button\n    click_element(driver, By.CSS_SELECTOR, 'button[data-testid=\"bottom-next-button\"]', use_js=True)\n\n\ndef fill_company_applicants(driver):\n    # Select existing reporting company\n    click_element(driver, By.CSS_SELECTOR, 'label[for=\"rc.isExistingReportingCompany\"]', use_js=True)\n\n    # Click the \"Yes\" button on the popup using TAB and ENTER keys\n    click_yes_button(driver)\n\n    # Click the next button to go to the beneficial owners page\n    click_element(driver, By.CSS_SELECTOR, 'button[data-testid=\"bottom-next-button\"]', use_js=True)\n\n\ndef is_browser_open(driver):\n    try:\n        driver.title  # Attempt to get the browser's title to check if it's still open\n        return True\n    except UnexpectedAlertPresentException:\n        try:\n            alert = driver.switch_to.alert\n            alert.accept()\n            return True\n        except NoAlertPresentException:\n            return True\n    except WebDriverException:\n        return False\n\ndef close_browser(driver):\n    try:\n        driver.quit()\n    except WebDriverException:\n        # Forcefully kill the process if quit() fails\n        browser_pid = driver.service.process.pid\n        os.kill(browser_pid, signal.SIGTERM)\n\n\ndef main():\n    # Load companies from CSV\n    companies_df = read_companies_from_csv('.\/data\/companies.csv')\n\n    for index, row in companies_df.iterrows():\n        if pd.notna(row['complete']):\n            continue\n\n        llc = row['LLC']\n        ein = row['EIN']\n        ssn_itin = row['SSN\/ITIN']\n\n        # Setup Chrome driver\n        service = Service(ChromeDriverManager().install())\n        options = webdriver.ChromeOptions()\n        options.add_argument(\"--start-maximized\")  # Add this line to open Chrome in a maximized window\n        driver = webdriver.Chrome(service=service, options=options)\n\n        try:\n            # Open the website\n            driver.get(\"https:\/\/boiefiling.fincen.gov\/boir\/html\")\n            logger.info(f\"Website opened successfully for {llc}.\")\n            # Fill filing information\n            fill_filing_information(driver)\n\n            # Fill reporting company information\n            fill_reporting_company(driver, llc, ein, ssn_itin)\n\n            # Fill company applicants\n            fill_company_applicants(driver)\n\n            # Fill beneficial owners\n            fill_beneficial_owners(driver)\n\n            # Fill submission page\n            fill_submit_page(driver)\n\n            # Mark the company as complete in the DataFrame\n            companies_df.at[index, 'complete'] = 'yes'\n\n            # Save the updated CSV\n            update_csv('.\/data\/companies.csv', companies_df)\n            logger.info(f\"Form completed for {llc} and marked as complete in CSV.\")\n\n            input('Press enter for next company')\n\n        finally:\n            close_browser(driver)\n\nif __name__ == \"__main__\":\n    main()<\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Supporting CSV<\/h2>\n\n\n\n<p>You&#8217;ll need to create a subdirectory wherever you run this program labeled .\/data in this folder you can put the JPEG&#8217;s of your ID&#8217;s which must be uploaded as well as a CSV file with all of your different company names (EIN or SSN depending on how they&#8217;re setup) and a column labeled &#8216;complete&#8217;. The script will go through every single company automating your information but just changing these fields for each organization you own. Here is a sample of the CSV file. <br><\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" width=\"646\" height=\"425\" data-src=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/05\/image.png\" alt=\"\" class=\"wp-image-7724 lazyload\" data-srcset=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/05\/image.png 646w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/05\/image-300x197.png 300w, https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/05\/image-456x300.png 456w\" data-sizes=\"(max-width: 646px) 100vw, 646px\" src=\"data:image\/svg+xml;base64,PHN2ZyB3aWR0aD0iMSIgaGVpZ2h0PSIxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjwvc3ZnPg==\" style=\"--smush-placeholder-width: 646px; --smush-placeholder-aspect-ratio: 646\/425;\" \/><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">BOIR E-filing API FAILED<\/h2>\n\n\n\n<p>I originally attempted to get this code to use the API provided by FinCEN you can request access <a href=\"https:\/\/www.fincen.gov\/contact\">here<\/a>. I could not get this to format correctly based on their requirements. <br><br><strong>THIS CODE IS NOT COMPLETE<br><\/strong><\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import requests\nimport pandas as pd\nimport base64\nimport logging\nimport xml.etree.ElementTree as ET\nfrom xml.dom import minidom\nimport os\nimport re\nimport api_keys\nimport csv\nimport mimetypes\nfrom lxml import etree\nimport time\nimport xmlschema\n\n# Setup logging\nlogging.basicConfig(level=logging.INFO)\nlogger = logging.getLogger(__name__)\n\n# API endpoints\nAUTH_URL = 'https:\/\/iam.fincen.gov\/am\/oauth2\/realms\/root\/realms\/Finance\/access_token'\nSANDBOX_URL = 'https:\/\/boiefiling-api.user-test.fincen.gov\/preprod'\nPRODUCTION_URL = 'https:\/\/boiefiling-api.fincen.gov\/prod'\n\n\ndef initialize_company_status_file(csv_path):\n    with open('company_status.csv', 'w', newline='') as file:\n        writer = csv.writer(file)\n        writer.writerow(['Company Name', 'Process ID', 'Status'])\n\n    logger.info(\"Company status file initialized.\")\n\n\ndef check_file_exists(file_path):\n    if not os.path.exists(file_path):\n        logger.error(f\"File not found: {file_path}\")\n        return False\n    return True\n\n\ndef get_access_token(environment):\n    \"\"\"Retrieve an access token.\"\"\"\n    logger.info(\"Getting access token...\")\n    if environment == 'sandbox':\n        client_id = api_keys.SANDBOX_CLIENT_ID\n        client_secret = api_keys.SANDBOX_CLIENT_SECRET\n        scope = 'BOSS-EFILE-SANDBOX'\n    else:\n        client_id = api_keys.PRODUCTION_CLIENT_ID\n        client_secret = api_keys.PRODUCTION_CLIENT_SECRET\n        scope = 'BOSS-EFILE'\n\n    credentials = f\"{client_id}:{client_secret}\"\n    encoded_credentials = base64.b64encode(credentials.encode()).decode()\n\n    headers = {\n        'Authorization': f'Basic {encoded_credentials}',\n        'Content-Type': 'application\/x-www-form-urlencoded'\n    }\n    data = {\n        'grant_type': 'client_credentials',\n        'scope': scope\n    }\n\n    response = requests.post(AUTH_URL, headers=headers, data=data)\n    response.raise_for_status()\n\n    return response.json().get('access_token')\n\n\ndef validate_attachment(file_path):\n    \"\"\"Validate the identifying document image attachment.\"\"\"\n    valid_extensions = ['.jpeg', '.jpg', '.png', '.pdf']\n    file_size = os.path.getsize(file_path)\n    file_extension = os.path.splitext(file_path)[1].lower()\n\n    if file_extension not in valid_extensions:\n        raise ValueError(f\"Invalid file format: {file_extension}. Supported formats are JPEG, JPG, PNG, PDF.\")\n\n    if file_size > 4 * 1024 * 1024:\n        raise ValueError(f\"File size exceeds the 4MB limit: {file_size} bytes.\")\n\n    filename = os.path.basename(file_path)\n    if re.search(r'[^\\w!@#$%()_\\-\\.=+\\[\\]{}|;~]', filename):\n        raise ValueError(f\"Invalid characters in file name: {filename}\")\n\n    logger.info(f\"Attachment {filename} is valid.\")\n\n\n\n\ndef save_xml_file(xml_string, filename):\n    \"\"\"Save XML string to a file.\"\"\"\n    try:\n        with open(filename, 'w') as file:\n            file.write(xml_string)\n        logger.info(f\"XML saved to {filename}\")\n    except Exception as e:\n        logger.error(f\"Error saving XML to file: {str(e)}\")\n\n\ndef initiate_submission(token, base_url):\n    \"\"\"Initiate BOIR submission and get process ID.\"\"\"\n    logger.info(\"Initiating submission...\")\n    headers = {\n        'Authorization': f'Bearer {token}'\n    }\n\n    response = requests.get(f'{base_url}\/processId', headers=headers)\n    response.raise_for_status()\n\n    process_id = response.json().get('processId')\n    logger.info(f\"Obtained process ID: {process_id}\")\n    return process_id\n\n\ndef upload_attachment(token, process_id, document_path, base_url):\n    try:\n        if not check_file_exists(document_path):\n            logger.error(f\"Attachment file does not exist: {document_path}\")\n            return 'file_not_found'\n\n        validate_attachment(document_path)\n\n        headers = {\n            \"Authorization\": f\"Bearer {token}\",\n            \"Content-Type\": mimetypes.guess_type(document_path)[0]\n        }\n\n        with open(document_path, 'rb') as file_data:\n            files = {\n                'file': (os.path.basename(document_path), file_data, mimetypes.guess_type(document_path)[0])\n            }\n            url = f\"{base_url}\/attachments\/{process_id}\/{os.path.basename(document_path)}\"\n            response = requests.post(url, headers=headers, files=files)\n\n        if response.status_code != 200:\n            logger.error(f\"Failed to upload file {os.path.basename(document_path)} for process ID {process_id}. Status Code: {response.status_code}, Response: {response.text}\")\n            return 'upload_failed'\n\n        response.raise_for_status()\n        return 'upload_success'\n    except Exception as e:\n        logger.error(f\"Exception occurred while uploading attachment {os.path.basename(document_path)} for process ID {process_id}: {str(e)}\")\n        return 'exception_occurred'\n\n\ndef upload_boir_xml(token, process_id, xml_path, base_url):\n    try:\n        if not check_file_exists(xml_path):\n            logger.error(f\"BOIR XML file does not exist: {xml_path}\")\n            return 'file_not_found'\n\n        logger.info(f\"Uploading BOIR XML for process ID: {process_id}\")\n        headers = {\n            'Authorization': f'Bearer {token}',\n            'Content-Type': 'application\/xml'\n        }\n\n        xml_file_name = os.path.basename(xml_path)\n        with open(xml_path, 'rb') as xml_data:\n            response = requests.post(f'{base_url}\/upload\/BOIR\/{process_id}\/{xml_file_name}', headers=headers, data=xml_data)\n\n        if response.status_code != 200:\n            logger.error(f\"Failed to upload BOIR XML {xml_file_name} for process ID {process_id}. Status Code: {response.status_code}, Response: {response.text}\")\n            return 'upload_failed'\n\n        response.raise_for_status()\n        response_json = response.json()\n\n        if 'submissionStatus' in response_json:\n            logger.info(f\"Upload response: {response_json}\")\n            return response_json['submissionStatus']\n\n        logger.error(f\"Upload response does not contain expected keys: {response_json}\")\n        return 'unknown_status'\n    except Exception as e:\n        logger.error(f\"Exception occurred while uploading BOIR XML {xml_file_name} for process ID {process_id}: {str(e)}\")\n        return 'exception_occurred'\n\n\ndef track_submission_status(token, process_id, base_url):\n    \"\"\"Track the status of the BOIR submission.\"\"\"\n    logger.info(f\"Tracking submission status for process ID: {process_id}\")\n    headers = {\n        'Authorization': f'Bearer {token}'\n    }\n\n    response = requests.get(f'{base_url}\/submissionStatus\/{process_id}', headers=headers)\n    response.raise_for_status()\n\n    return response.json().get('submissionStatus')\n\n\ndef log_error(response_data):\n    \"\"\"Log errors based on response data.\"\"\"\n    errors = response_data.get('errors', [])\n    for error in errors:\n        error_code = error.get('ErrorCode')\n        error_text = error.get('ErrorText')\n        logger.error(f\"Error Code: {error_code}, Error Text: {error_text}\")\n\n        if error_code in [\"SBE01\", \"SBE02\", \"SBE03\", \"SBE04\", \"SBE05\", \"SBE06\"]:\n            logger.error(\"Recommended Action: \" + error.get('ErrorText', '').split('Please')[1].strip())\n\n\ndef create_boir_xml(csv_row):\n    try:\n        root = ET.Element(\"{www.fincen.gov\/base}EFilingSubmissionXML\", attrib={\n            'xmlns:fc2': 'www.fincen.gov\/base',\n            'xmlns:xsi': 'http:\/\/www.w3.org\/2001\/XMLSchema-instance',\n            'xsi:schemaLocation': 'www.fincen.gov\/base BSA_XML_2.0.xsd',\n            'SeqNum': '1'\n        })\n\n        # Submitter Information\n        ET.SubElement(root, \"{www.fincen.gov\/base}SubmitterElectronicAddressText\").text = 'EMAIL@EMAIL.com'\n        ET.SubElement(root, \"{www.fincen.gov\/base}SubmitterEntityIndivdualLastName\").text = 'LAST NAME'\n        ET.SubElement(root, \"{www.fincen.gov\/base}SubmitterIndivdualFirstName\").text = 'FIRST NAME'\n\n        activity = ET.SubElement(root, \"{www.fincen.gov\/base}Activity\", attrib={'SeqNum': '2'})\n        ET.SubElement(activity, \"{www.fincen.gov\/base}ApprovalOfficialSignatureDateText\").text = str(csv_row[\"2.date_prepared\"])\n        ET.SubElement(activity, \"{www.fincen.gov\/base}FilingDateText\")\n\n        activity_association = ET.SubElement(activity, \"{www.fincen.gov\/base}ActivityAssociation\", attrib={'SeqNum': '3'})\n        ET.SubElement(activity_association, \"{www.fincen.gov\/base}InitialReportIndicator\").text = \"Y\" if str(csv_row[\"1.a\"]).upper() == \"Y\" else \"\"\n        ET.SubElement(activity_association, \"{www.fincen.gov\/base}CorrectsAmendsPriorReportIndicator\").text = \"Y\" if str(csv_row[\"1.b\"]).upper() == \"Y\" else \"\"\n        ET.SubElement(activity_association, \"{www.fincen.gov\/base}UpdatesPriorReportIndicator\").text = \"Y\" if str(csv_row[\"1.c\"]).upper() == \"Y\" else \"\"\n        ET.SubElement(activity_association, \"{www.fincen.gov\/base}NewlyExemptEntityIndicator\").text = \"Y\" if str(csv_row[\"1.d\"]).upper() == \"Y\" else \"\"\n\n        # Reporting Company Information\n        reporting_company = ET.SubElement(activity, \"{www.fincen.gov\/base}Party\", attrib={'SeqNum': '4'})\n        ET.SubElement(reporting_company, \"{www.fincen.gov\/base}ActivityPartyTypeCode\").text = \"62\"\n        ET.SubElement(reporting_company, \"{www.fincen.gov\/base}ExistingReportingCompanyIndicator\").text = \"Y\" if str(csv_row[\"16\"]).upper() == \"Y\" else \"\"\n        ET.SubElement(reporting_company, \"{www.fincen.gov\/base}FormationCountryCodeText\").text = str(csv_row[\"10.a\"])\n        ET.SubElement(reporting_company, \"{www.fincen.gov\/base}FormationStateCodeText\").text = str(csv_row[\"10.b\"])\n        ET.SubElement(reporting_company, \"{www.fincen.gov\/base}RequestFinCENIDIndicator\").text = \"Y\"\n\n        party_name = ET.SubElement(reporting_company, \"{www.fincen.gov\/base}PartyName\", attrib={'SeqNum': '5'})\n        ET.SubElement(party_name, \"{www.fincen.gov\/base}PartyNameTypeCode\").text = \"L\"\n        ET.SubElement(party_name, \"{www.fincen.gov\/base}RawPartyFullName\").text = str(csv_row[\"5\"])\n\n        address = ET.SubElement(reporting_company, \"{www.fincen.gov\/base}Address\", attrib={'SeqNum': '6'})\n        ET.SubElement(address, \"{www.fincen.gov\/base}RawCityText\").text = str(csv_row[\"12\"])\n        ET.SubElement(address, \"{www.fincen.gov\/base}RawCountryCodeText\").text = \"US\"\n        ET.SubElement(address, \"{www.fincen.gov\/base}RawStateCodeText\").text = str(csv_row[\"14\"])\n        ET.SubElement(address, \"{www.fincen.gov\/base}RawStreetAddress1Text\").text = str(csv_row[\"11\"])\n        ET.SubElement(address, \"{www.fincen.gov\/base}RawZIPCode\").text = str(csv_row[\"15\"])\n\n        party_identification = ET.SubElement(reporting_company, \"{www.fincen.gov\/base}PartyIdentification\", attrib={'SeqNum': '7'})\n        ET.SubElement(party_identification, \"{www.fincen.gov\/base}PartyIdentificationNumberText\").text = str(csv_row[\"8\"])\n        ET.SubElement(party_identification, \"{www.fincen.gov\/base}PartyIdentificationTypeCode\").text = str(csv_row[\"7\"])\n\n        # Company Applicant Information\n        company_applicant = ET.SubElement(activity, \"{www.fincen.gov\/base}Party\", attrib={'SeqNum': '8'})\n        ET.SubElement(company_applicant, \"{www.fincen.gov\/base}ActivityPartyTypeCode\").text = \"63\"\n        ET.SubElement(company_applicant, \"{www.fincen.gov\/base}IndividualBirthDateText\").text = str(csv_row[\"23\"])\n\n        party_name = ET.SubElement(company_applicant, \"{www.fincen.gov\/base}PartyName\", attrib={'SeqNum': '9'})\n        ET.SubElement(party_name, \"{www.fincen.gov\/base}PartyNameTypeCode\").text = \"L\"\n        ET.SubElement(party_name, \"{www.fincen.gov\/base}RawEntityIndividualLastName\").text = str(csv_row[\"19\"])\n        ET.SubElement(party_name, \"{www.fincen.gov\/base}RawIndividualFirstName\").text = str(csv_row[\"20\"])\n        ET.SubElement(party_name, \"{www.fincen.gov\/base}RawIndividualMiddleName\").text = str(csv_row[\"21\"])\n        ET.SubElement(party_name, \"{www.fincen.gov\/base}RawIndividualNameSuffixText\").text = str(csv_row[\"22\"])\n\n        address = ET.SubElement(company_applicant, \"{www.fincen.gov\/base}Address\", attrib={'SeqNum': '10'})\n        ET.SubElement(address, \"{www.fincen.gov\/base}RawCityText\").text = str(csv_row[\"26\"])\n        ET.SubElement(address, \"{www.fincen.gov\/base}RawCountryCodeText\").text = str(csv_row[\"27\"])\n        ET.SubElement(address, \"{www.fincen.gov\/base}RawStateCodeText\").text = str(csv_row[\"28\"])\n        ET.SubElement(address, \"{www.fincen.gov\/base}RawStreetAddress1Text\").text = str(csv_row[\"25\"])\n        ET.SubElement(address, \"{www.fincen.gov\/base}RawZIPCode\").text = str(csv_row[\"29\"])\n        if str(csv_row[\"24\"]) == \"Business address\":\n            ET.SubElement(address, \"{www.fincen.gov\/base}BusinessAddressIndicator\").text = \"Y\"\n        else:\n            ET.SubElement(address, \"{www.fincen.gov\/base}ResidentialAddressIndicator\").text = \"Y\"\n\n        party_identification = ET.SubElement(company_applicant, \"{www.fincen.gov\/base}PartyIdentification\", attrib={'SeqNum': '11'})\n        ET.SubElement(party_identification, \"{www.fincen.gov\/base}OtherIssuerCountryText\").text = str(csv_row[\"32.a\"])\n        ET.SubElement(party_identification, \"{www.fincen.gov\/base}OtherIssuerStateText\").text = str(csv_row[\"32.b\"])\n        ET.SubElement(party_identification, \"{www.fincen.gov\/base}PartyIdentificationNumberText\").text = str(csv_row[\"31\"])\n        ET.SubElement(party_identification, \"{www.fincen.gov\/base}PartyIdentificationTypeCode\").text = str(csv_row[\"30\"])\n        ET.SubElement(party_identification, \"{www.fincen.gov\/base}OriginalAttachmentFileName\").text = str(csv_row[\"33\"])\n\n        # Beneficial Owner Information\n        seq_num = 12\n        for i in range(1, 3):\n            if f'38.{i}' in csv_row:\n                beneficial_owner = ET.SubElement(activity, \"{www.fincen.gov\/base}Party\", attrib={'SeqNum': str(seq_num)})\n                ET.SubElement(beneficial_owner, \"{www.fincen.gov\/base}ActivityPartyTypeCode\").text = \"64\"\n                ET.SubElement(beneficial_owner, \"{www.fincen.gov\/base}IndividualBirthDateText\").text = str(csv_row.get(f'42.{i}', \"\"))\n                if str(csv_row.get(f'37.{i}', \"\")).upper() == \"Y\":\n                    ET.SubElement(beneficial_owner, \"{www.fincen.gov\/base}ExemptIndicator\").text = \"Y\"\n                    ET.SubElement(beneficial_owner, \"{www.fincen.gov\/base}PartyName\", attrib={'SeqNum': str(seq_num + 1)})\n                    ET.SubElement(beneficial_owner, \"{www.fincen.gov\/base}RawEntityIndividualLastName\").text = str(csv_row.get(f'38.{i}', \"\"))\n                else:\n                    party_name = ET.SubElement(beneficial_owner, \"{www.fincen.gov\/base}PartyName\", attrib={'SeqNum': str(seq_num + 1)})\n                    ET.SubElement(party_name, \"{www.fincen.gov\/base}PartyNameTypeCode\").text = \"L\"\n                    ET.SubElement(party_name, \"{www.fincen.gov\/base}RawEntityIndividualLastName\").text = str(csv_row.get(f'38.{i}', \"\"))\n                    ET.SubElement(party_name, \"{www.fincen.gov\/base}RawIndividualFirstName\").text = str(csv_row.get(f'39.{i}', \"\"))\n                    ET.SubElement(party_name, \"{www.fincen.gov\/base}RawIndividualMiddleName\").text = str(csv_row.get(f'40.{i}', \"\"))\n                    ET.SubElement(party_name, \"{www.fincen.gov\/base}RawIndividualNameSuffixText\").text = str(csv_row.get(f'41.{i}', \"\"))\n\n                address = ET.SubElement(beneficial_owner, \"{www.fincen.gov\/base}Address\", attrib={'SeqNum': str(seq_num + 2)})\n                ET.SubElement(address, \"{www.fincen.gov\/base}RawCityText\").text = str(csv_row.get(f'44.{i}', \"\"))\n                ET.SubElement(address, \"{www.fincen.gov\/base}RawCountryCodeText\").text = str(csv_row.get(f'45.{i}', \"\"))\n                ET.SubElement(address, \"{www.fincen.gov\/base}RawStateCodeText\").text = str(csv_row.get(f'46.{i}', \"\"))\n                ET.SubElement(address, \"{www.fincen.gov\/base}RawStreetAddress1Text\").text = str(csv_row.get(f'43.{i}', \"\"))\n                ET.SubElement(address, \"{www.fincen.gov\/base}RawZIPCode\").text = str(csv_row.get(f'47.{i}', \"\"))\n\n                party_identification = ET.SubElement(beneficial_owner, \"{www.fincen.gov\/base}PartyIdentification\", attrib={'SeqNum': str(seq_num + 3)})\n                ET.SubElement(party_identification, \"{www.fincen.gov\/base}OtherIssuerCountryText\").text = str(csv_row.get(f'50.{i}.a', \"\"))\n                ET.SubElement(party_identification, \"{www.fincen.gov\/base}OtherIssuerStateText\").text = str(csv_row.get(f'50.{i}.b', \"\"))\n                ET.SubElement(party_identification, \"{www.fincen.gov\/base}IssuerLocalTribalCodeText\").text = str(csv_row.get(f'50.{i}.c', \"\"))\n                ET.SubElement(party_identification, \"{www.fincen.gov\/base}OtherIssuerLocalTribalText\").text = str(csv_row.get(f'50.{i}.d', \"\"))\n                ET.SubElement(party_identification, \"{www.fincen.gov\/base}PartyIdentificationNumberText\").text = str(csv_row.get(f'49.{i}', \"\"))\n                ET.SubElement(party_identification, \"{www.fincen.gov\/base}PartyIdentificationTypeCode\").text = str(csv_row.get(f'48.{i}', \"\"))\n                ET.SubElement(party_identification, \"{www.fincen.gov\/base}OriginalAttachmentFileName\").text = str(csv_row.get(f'51.{i}', \"\"))\n\n                seq_num += 4\n\n        xml_str = prettify_xml(root)\n        logger.debug(f\"Generated XML: {xml_str}\")\n\n        if validate_xml(xml_str):\n            return xml_str\n        else:\n            return None\n    except Exception as e:\n        logger.error(f\"Error creating BOIR XML: {str(e)}\")\n        return None\ndef prettify_xml(elem):\n    \"\"\"Return a pretty-printed XML string for the Element.\"\"\"\n    rough_string = ET.tostring(elem, 'utf-8')\n    reparsed = minidom.parseString(rough_string)\n    return reparsed.toprettyxml(indent=\"  \")\n\ndef validate_xml(xml_str):\n    \"\"\"Validate the structure of the XML string using the provided XSD schema.\"\"\"\n    try:\n        schema_bsa = xmlschema.XMLSchema(\"BSA_XML_2.0.xsd\")\n        schema_boir = xmlschema.XMLSchema(\"BOIRSchema.xsd.xml\")\n\n        schema_bsa.validate(xml_str)\n        schema_boir.validate(xml_str)\n\n        logger.info(\"XML structure is valid.\")\n        return True\n    except xmlschema.XMLSchemaException as e:\n        logger.error(f\"XML schema validation error: {e}\")\n        return False\n    except Exception as e:\n        logger.error(f\"XML parsing error: {e}\")\n        return False\n\n\ndef check_submission_status(token, process_id, base_url, retry_count=3, retry_delay=5):\n    \"\"\"Check the status of a BOIR submission.\"\"\"\n    headers = {\n        'Authorization': f'Bearer {token}'\n    }\n\n    for attempt in range(retry_count):\n        try:\n            response = requests.get(f'{base_url}\/submissionStatus\/{process_id}', headers=headers)\n\n            if response.status_code == 200:\n                submission_status = response.json().get('submissionStatus')\n                logger.info(f\"Submission status for process ID {process_id}: {submission_status}\")\n\n                if submission_status == 'submission_validation_failed':\n                    error_details = response.json().get('validationErrors', ['Unknown error'])[0]\n                    logger.debug(f'Failed with {response.json()}')\n                    log_error(response.json())\n                    return submission_status, error_details\n\n                return submission_status, None\n            else:\n                logger.error(f\"Failed to retrieve submission status for process ID {process_id}. Status code: {response.status_code}\")\n                logger.error(f\"Response content: {response.text}\")\n                if attempt &lt; retry_count - 1:\n                    logger.info(f\"Retrying in {retry_delay} seconds...\")\n                    time.sleep(retry_delay)\n        except requests.exceptions.RequestException as e:\n            logger.error(f\"Error occurred while checking submission status for process ID {process_id}: {str(e)}\")\n            if attempt &lt; retry_count - 1:\n                logger.info(f\"Retrying in {retry_delay} seconds...\")\n                time.sleep(retry_delay)\n\n    logger.error(f\"Failed to retrieve submission status for process ID {process_id} after {retry_count} attempts.\")\n    return None, None\n\n\ndef check_csv_format(filename):\n    \"\"\"\n    Check the format and values in the BOIR CSV file before proceeding.\n    \"\"\"\n    required_fields = {\n        \"1.a\": r\"^[YN]$\",\n        \"1.b\": r\"^[YN]$\",\n        \"1.c\": r\"^[YN]$\",\n        \"1.d\": r\"^[YN]$\",\n        \"2.date_prepared\": r\"^\\d{8}$\",\n        \"5\": r\"^.{1,150}$\",\n        \"8\": r\"^\\d{9}$\",\n        \"10.a\": r\"^[A-Z]{2}$\",\n        \"10.b\": r\"^[A-Z]{2}$\",\n        \"11\": r\"^.{1,100}$\",\n        \"12\": r\"^.{1,50}$\",\n        \"13\": r\"^[A-Z]{2}$\",\n        \"14\": r\"^[A-Z]{2}$\",\n        \"15\": r\"^\\d{5}$\",\n        \"23\": r\"^\\d{8}$\",\n        \"25\": r\"^.{1,100}$\",\n        \"26\": r\"^.{1,50}$\",\n        \"27\": r\"^[A-Z]{2}$\",\n        \"28\": r\"^[A-Z]{2}$\",\n        \"29\": r\"^\\d{5}$\",\n        \"31\": r\"^.{1,25}$\",\n        \"32.a\": r\"^[A-Z]{2}$\",\n        \"32.b\": r\"^[A-Z]{2}$\",\n        \"51.1\": r\"^.+\\.(jpg|jpeg|png|pdf)$\",\n        # Add patterns for any additional fields required\n    }\n\n    def check_value(field, value):\n        if field in required_fields:\n            pattern = required_fields[field]\n            if not re.match(pattern, value):\n                logger.error(f\"Field {field} has invalid value: {value}\")\n                return False\n        return True\n\n    def check_file_exists(file_path):\n        if not os.path.exists(file_path):\n            logger.error(f\"File not found: {file_path}\")\n            return False\n        return True\n\n    # Check CSV file\n    with open(filename, 'r') as csvfile:\n        reader = csv.DictReader(csvfile)\n        for row in reader:\n            for field, value in row.items():\n                if not check_value(field, value):\n                    return False\n                if field.endswith('.1') or field.endswith('.2'):  # Handle repeating fields\n                    if field.startswith('51'):  # Check if it's a document path\n                        document_path = os.path.join('.\/data', value)\n                        if not check_file_exists(document_path):\n                            return False\n    logger.info(\"CSV file format and values are correct.\")\n    return True\n\n\ndef update_company_status(company_name, process_id, submission_status, error_details=''):\n    \"\"\"Update the company status in the CSV file.\"\"\"\n    updated = False\n    if not os.path.exists('.\/data\/company_status.csv'):\n        with open('.\/data\/company_status.csv', 'w', newline='') as file:\n            writer = csv.writer(file)\n            writer.writerow(['company_name', 'process_id', 'status', 'error_details'])\n\n    with open('.\/data\/company_status.csv', 'r', newline='') as orig_file:\n        reader = csv.reader(orig_file)\n        rows = list(reader)\n\n    with open('.\/data\/company_status_temp.csv', 'w', newline='') as temp_file:\n        writer = csv.writer(temp_file)\n        for row in rows:\n            if row[0] == company_name:\n                row[1] = process_id\n                row[2] = submission_status\n                row[3] = error_details if error_details else ''\n                updated = True\n            writer.writerow(row)\n        if not updated:\n            writer.writerow([company_name, process_id, submission_status, error_details])\n\n    os.replace('.\/data\/company_status_temp.csv', '.\/data\/company_status.csv')\n\n\ndef register_organizations(csv_path, environment):\n    \"\"\"Read CSV and register organizations.\"\"\"\n    if not check_csv_format(csv_path):\n        logger.error(\"CSV file format or values are incorrect. Please check the log for details.\")\n        return\n\n    df = pd.read_csv(csv_path)\n\n    base_url = SANDBOX_URL if environment == 'sandbox' else PRODUCTION_URL\n    token = get_access_token(environment)\n\n    if not token:\n        logger.error(\"Failed to obtain access token. Exiting.\")\n        return\n\n    companies = df.to_dict('records')\n\n    if not os.path.exists('.\/data\/company_status.csv'):\n        with open('.\/data\/company_status.csv', 'w', newline='') as file:\n            writer = csv.writer(file)\n            writer.writerow(['company_name', 'process_id', 'status', 'error_details'])\n\n    for company in companies:\n        company_name = company.get('5', 'Unknown Company')\n        process_id = None\n        skip_company = False\n\n        with open('.\/data\/company_status.csv', 'r', newline='') as file:\n            reader = csv.reader(file)\n            for row in reader:\n                if row[0] == company_name:\n                    process_id = row[1]\n                    current_status, _ = check_submission_status(token, process_id, base_url)\n                    if current_status:\n                        logger.info(f\"Current status for {company_name}: {current_status}\")\n                        update_company_status(company_name, process_id, current_status)\n                        if current_status in ['submission_initiated', 'submission_validation_failed', 'submission_failed']:\n                            skip_company = False\n                        else:\n                            skip_company = True\n                    else:\n                        skip_company = True\n                    break\n\n        if skip_company:\n            continue\n\n        try:\n            if process_id is None:\n                process_id = initiate_submission(token, base_url)\n                if process_id is None:\n                    logger.error(f\"Failed to obtain process ID for {company_name}\")\n                    continue\n\n            xml_filename = f\"boir_{company_name.replace(' ', '_')}.xml\"\n            boir_xml = create_boir_xml(company)\n            if boir_xml is None:\n                logger.error(f\"Failed to create BOIR XML for {company_name}\")\n                continue\n            save_xml_file(boir_xml, f\".\/data\/{xml_filename}\")\n\n            document_paths = [f\".\/data\/{company[key]}\" for key in company if re.match(r\"^51\\.\\d+$\", key) and os.path.exists(f\".\/data\/{company[key]}\")]\n\n            if not document_paths:\n                logger.error(f\"No valid document paths found for {company_name}\")\n                continue\n\n            for document_path in document_paths:\n                upload_status = upload_attachment(token, process_id, document_path, base_url)\n                if upload_status == 'submission_exists':\n                    logger.info(f\"Submission already exists for process ID {process_id}. Skipping attachment upload.\")\n                    continue\n                elif upload_status != 'upload_success':\n                    logger.error(f\"Failed to upload attachment for {company_name}. Status: {upload_status}\")\n                    continue\n\n            boir_status = upload_boir_xml(token, process_id, f\".\/data\/{xml_filename}\", base_url)\n\n            if boir_status == 'submission_initiated':\n                update_company_status(company_name, process_id, boir_status)\n            elif boir_status == 'submission_exists':\n                logger.info(f\"Submission already exists for process ID {process_id}. Skipping BOIR XML upload.\")\n            else:\n                logger.error(f\"Failed to upload BOIR XML for {company_name}. Status: {boir_status}\")\n                continue\n\n            final_status = track_submission_status(token, process_id, base_url)\n            logger.info(f\"Final status for {company_name}: {final_status}\")\n\n            if final_status in ['submission_accepted', 'submission_rejected']:\n                retrieve_transcript(token, process_id, base_url)\n            if final_status == 'submission_rejected':\n                response_data = track_submission_status(token, process_id, base_url)\n                log_error(response_data)\n\n            time.sleep(5)  # Delay between each company registration\n\n        except requests.exceptions.RequestException as e:\n            logger.error(f\"Request error for {company_name}: {str(e)}\")\n        except Exception as e:\n            logger.error(f\"Error processing {company_name}: {str(e)}\")\n\n\ndef verify_submissions():\n    \"\"\"Verify the submission status of all companies.\"\"\"\n    if not os.path.exists('.\/data\/company_status.csv'):\n        logger.error(\"Company status CSV file not found. No submissions to verify.\")\n        return\n\n    token = get_access_token(environment)\n    base_url = SANDBOX_URL if environment == 'sandbox' else PRODUCTION_URL\n\n    if not token:\n        logger.error(\"Failed to obtain access token. Exiting.\")\n        return\n\n    with open('.\/data\/company_status.csv', 'r', newline='') as file:\n        reader = csv.reader(file)\n        next(reader)  # Skip header row\n        for row in reader:\n            company_name = row[0]\n            process_id = row[1]\n            submission_status, error_details = check_submission_status(token, process_id, base_url)\n            if submission_status:\n                logger.info(f\"Updated status for {company_name}: {submission_status}\")\n                update_company_status(company_name, process_id, submission_status, error_details if error_details else '')\n\n\ndef generate_summary_report():\n    \"\"\"Generate a summary report of the registration status.\"\"\"\n    if not os.path.exists('.\/data\/company_status.csv'):\n        logger.error(\"Company status CSV file not found. No submissions to report.\")\n        return\n\n    total_companies = 0\n    successful_submissions = 0\n    failed_submissions = 0\n\n    with open('.\/data\/company_status.csv', 'r', newline='') as file:\n        reader = csv.reader(file)\n        next(reader)  # Skip header row\n        for row in reader:\n            total_companies += 1\n            if row[2] == 'submission_accepted':\n                successful_submissions += 1\n            elif row[2] == 'submission_rejected':\n                failed_submissions += 1\n\n    logger.info(\"Registration Summary Report\")\n    logger.info(f\"Total Companies: {total_companies}\")\n    logger.info(f\"Successful Submissions: {successful_submissions}\")\n    logger.info(f\"Failed Submissions: {failed_submissions}\")\n\n    if failed_submissions > 0:\n        logger.info(\"Please check the company status CSV file for details on failed submissions.\")\n\n\ndef monitor_and_update():\n    \"\"\"Continuously monitor and update the registration status.\"\"\"\n    while True:\n        verify_submissions()\n        time.sleep(3600)  # Wait for 1 hour before checking again\n\n\nif __name__ == \"__main__\":\n    csv_path = '.\/data\/boir_data.csv'\n    environment = 'sandbox'  # or 'production'\n\n    register_organizations(csv_path, environment)\n    verify_submissions()\n    generate_summary_report()\n    monitor_and_update()\n<\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Supporting CSV<\/h2>\n\n\n\n<p>This code requires a .\/data\/boir_data.csv file which contains the following column which correspond to each section on the BOIR form.<br><br>1.a 1.b 1.c 1.d 1.e 1.f 1.g 1.h 2.date_prepared 3 4 5 6 7 8 9 10.a 10.b 10.c 10.d 10.e 10.f 10.g 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32.a 32.b 32.c 32.d 33 35 36 37 38.1 39.1 40.1 41.1 42.1 43.1 44.1 45.1 46.1 47.1 48.1 49.1 50.1.a 50.1.b 50.1.c 50.1.d 51.1 38.2 39.2 40.2 41.2 42.2 43.2 44.2 45.2 46.2 47.2 48.2 49.2 50.2.a 50.2.b 50.2.c 50.2.d 51.2<\/p>\n\n\n\n<p><\/p>\n\n\n\n<p><br><\/p>\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>The Financial Crimes Enforcement Network (FinCEN) now requires most businesses to report their beneficial owners, a mandate stemming from the U.S. Corporate Transparency Act. The penalties for non-compliance&#8230;<\/p>\n","protected":false},"author":1,"featured_media":7718,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-7705","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-uncategorized"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v27.2 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>Automating FinCEN Beneficial Ownership Information Reporting for Multiple Organizations - Jeremy Whittaker<\/title>\n<meta name=\"robots\" content=\"noindex, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Automating FinCEN Beneficial Ownership Information Reporting for Multiple Organizations - Jeremy Whittaker\" \/>\n<meta property=\"og:description\" content=\"The Financial Crimes Enforcement Network (FinCEN) now requires most businesses to report their beneficial owners, a mandate stemming from the U.S. Corporate Transparency Act. The penalties for non-compliance...\" \/>\n<meta property=\"og:url\" content=\"https:\/\/new.jeremywhittaker.com\/index.php\/2024\/05\/28\/automating-fincen-beneficial-ownership-information-reporting-for-multiple-organizations\/\" \/>\n<meta property=\"og:site_name\" content=\"Jeremy Whittaker\" \/>\n<meta property=\"article:publisher\" content=\"https:\/\/www.facebook.com\/WhittakerJeremy\" \/>\n<meta property=\"article:author\" content=\"https:\/\/www.facebook.com\/WhittakerJeremy\" \/>\n<meta property=\"article:published_time\" content=\"2024-05-28T18:15:19+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2024-05-28T18:35:35+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/05\/Screenshot-2024-05-28-094602.png\" \/>\n\t<meta property=\"og:image:width\" content=\"432\" \/>\n\t<meta property=\"og:image:height\" content=\"122\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/png\" \/>\n<meta name=\"author\" content=\"JeremyWhittaker\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"JeremyWhittaker\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"3 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\/\/new.jeremywhittaker.com\/index.php\/2024\/05\/28\/automating-fincen-beneficial-ownership-information-reporting-for-multiple-organizations\/#article\",\"isPartOf\":{\"@id\":\"https:\/\/new.jeremywhittaker.com\/index.php\/2024\/05\/28\/automating-fincen-beneficial-ownership-information-reporting-for-multiple-organizations\/\"},\"author\":{\"name\":\"JeremyWhittaker\",\"@id\":\"https:\/\/new.jeremywhittaker.com\/#\/schema\/person\/ed0edfdefb3e180693efef453372980c\"},\"headline\":\"Automating FinCEN Beneficial Ownership Information Reporting for Multiple Organizations\",\"datePublished\":\"2024-05-28T18:15:19+00:00\",\"dateModified\":\"2024-05-28T18:35:35+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/new.jeremywhittaker.com\/index.php\/2024\/05\/28\/automating-fincen-beneficial-ownership-information-reporting-for-multiple-organizations\/\"},\"wordCount\":379,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\/\/new.jeremywhittaker.com\/#\/schema\/person\/ed0edfdefb3e180693efef453372980c\"},\"image\":{\"@id\":\"https:\/\/new.jeremywhittaker.com\/index.php\/2024\/05\/28\/automating-fincen-beneficial-ownership-information-reporting-for-multiple-organizations\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/05\/Screenshot-2024-05-28-094602.png\",\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\/\/new.jeremywhittaker.com\/index.php\/2024\/05\/28\/automating-fincen-beneficial-ownership-information-reporting-for-multiple-organizations\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/new.jeremywhittaker.com\/index.php\/2024\/05\/28\/automating-fincen-beneficial-ownership-information-reporting-for-multiple-organizations\/\",\"url\":\"https:\/\/new.jeremywhittaker.com\/index.php\/2024\/05\/28\/automating-fincen-beneficial-ownership-information-reporting-for-multiple-organizations\/\",\"name\":\"Automating FinCEN Beneficial Ownership Information Reporting for Multiple Organizations - Jeremy Whittaker\",\"isPartOf\":{\"@id\":\"https:\/\/new.jeremywhittaker.com\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\/\/new.jeremywhittaker.com\/index.php\/2024\/05\/28\/automating-fincen-beneficial-ownership-information-reporting-for-multiple-organizations\/#primaryimage\"},\"image\":{\"@id\":\"https:\/\/new.jeremywhittaker.com\/index.php\/2024\/05\/28\/automating-fincen-beneficial-ownership-information-reporting-for-multiple-organizations\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/05\/Screenshot-2024-05-28-094602.png\",\"datePublished\":\"2024-05-28T18:15:19+00:00\",\"dateModified\":\"2024-05-28T18:35:35+00:00\",\"breadcrumb\":{\"@id\":\"https:\/\/new.jeremywhittaker.com\/index.php\/2024\/05\/28\/automating-fincen-beneficial-ownership-information-reporting-for-multiple-organizations\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/new.jeremywhittaker.com\/index.php\/2024\/05\/28\/automating-fincen-beneficial-ownership-information-reporting-for-multiple-organizations\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/new.jeremywhittaker.com\/index.php\/2024\/05\/28\/automating-fincen-beneficial-ownership-information-reporting-for-multiple-organizations\/#primaryimage\",\"url\":\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/05\/Screenshot-2024-05-28-094602.png\",\"contentUrl\":\"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/05\/Screenshot-2024-05-28-094602.png\",\"width\":432,\"height\":122},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/new.jeremywhittaker.com\/index.php\/2024\/05\/28\/automating-fincen-beneficial-ownership-information-reporting-for-multiple-organizations\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/new.jeremywhittaker.com\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Automating FinCEN Beneficial Ownership Information Reporting for Multiple Organizations\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/new.jeremywhittaker.com\/#website\",\"url\":\"https:\/\/new.jeremywhittaker.com\/\",\"name\":\"Jeremy Whittaker\",\"description\":\"Research, software, markets, housing, and energy\",\"publisher\":{\"@id\":\"https:\/\/new.jeremywhittaker.com\/#\/schema\/person\/ed0edfdefb3e180693efef453372980c\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/new.jeremywhittaker.com\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":[\"Person\",\"Organization\"],\"@id\":\"https:\/\/new.jeremywhittaker.com\/#\/schema\/person\/ed0edfdefb3e180693efef453372980c\",\"name\":\"JeremyWhittaker\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/secure.gravatar.com\/avatar\/c8ac20e6dfa86b5f27ce9bffee4851099770cbea5ae7338a274865bfbc8c0218?s=96&d=retro&r=g\",\"url\":\"https:\/\/secure.gravatar.com\/avatar\/c8ac20e6dfa86b5f27ce9bffee4851099770cbea5ae7338a274865bfbc8c0218?s=96&d=retro&r=g\",\"contentUrl\":\"https:\/\/secure.gravatar.com\/avatar\/c8ac20e6dfa86b5f27ce9bffee4851099770cbea5ae7338a274865bfbc8c0218?s=96&d=retro&r=g\",\"caption\":\"JeremyWhittaker\"},\"logo\":{\"@id\":\"https:\/\/secure.gravatar.com\/avatar\/c8ac20e6dfa86b5f27ce9bffee4851099770cbea5ae7338a274865bfbc8c0218?s=96&d=retro&r=g\"},\"sameAs\":[\"http:\/\/www.jeremywhittaker.com\",\"https:\/\/www.facebook.com\/WhittakerJeremy\",\"https:\/\/www.linkedin.com\/in\/jeremywhittaker\/\"],\"url\":\"https:\/\/new.jeremywhittaker.com\/index.php\/author\/jeremywhittaker\/\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Automating FinCEN Beneficial Ownership Information Reporting for Multiple Organizations - Jeremy Whittaker","robots":{"index":"noindex","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"og_locale":"en_US","og_type":"article","og_title":"Automating FinCEN Beneficial Ownership Information Reporting for Multiple Organizations - Jeremy Whittaker","og_description":"The Financial Crimes Enforcement Network (FinCEN) now requires most businesses to report their beneficial owners, a mandate stemming from the U.S. Corporate Transparency Act. The penalties for non-compliance...","og_url":"https:\/\/new.jeremywhittaker.com\/index.php\/2024\/05\/28\/automating-fincen-beneficial-ownership-information-reporting-for-multiple-organizations\/","og_site_name":"Jeremy Whittaker","article_publisher":"https:\/\/www.facebook.com\/WhittakerJeremy","article_author":"https:\/\/www.facebook.com\/WhittakerJeremy","article_published_time":"2024-05-28T18:15:19+00:00","article_modified_time":"2024-05-28T18:35:35+00:00","og_image":[{"width":432,"height":122,"url":"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/05\/Screenshot-2024-05-28-094602.png","type":"image\/png"}],"author":"JeremyWhittaker","twitter_card":"summary_large_image","twitter_misc":{"Written by":"JeremyWhittaker","Est. reading time":"3 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/new.jeremywhittaker.com\/index.php\/2024\/05\/28\/automating-fincen-beneficial-ownership-information-reporting-for-multiple-organizations\/#article","isPartOf":{"@id":"https:\/\/new.jeremywhittaker.com\/index.php\/2024\/05\/28\/automating-fincen-beneficial-ownership-information-reporting-for-multiple-organizations\/"},"author":{"name":"JeremyWhittaker","@id":"https:\/\/new.jeremywhittaker.com\/#\/schema\/person\/ed0edfdefb3e180693efef453372980c"},"headline":"Automating FinCEN Beneficial Ownership Information Reporting for Multiple Organizations","datePublished":"2024-05-28T18:15:19+00:00","dateModified":"2024-05-28T18:35:35+00:00","mainEntityOfPage":{"@id":"https:\/\/new.jeremywhittaker.com\/index.php\/2024\/05\/28\/automating-fincen-beneficial-ownership-information-reporting-for-multiple-organizations\/"},"wordCount":379,"commentCount":0,"publisher":{"@id":"https:\/\/new.jeremywhittaker.com\/#\/schema\/person\/ed0edfdefb3e180693efef453372980c"},"image":{"@id":"https:\/\/new.jeremywhittaker.com\/index.php\/2024\/05\/28\/automating-fincen-beneficial-ownership-information-reporting-for-multiple-organizations\/#primaryimage"},"thumbnailUrl":"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/05\/Screenshot-2024-05-28-094602.png","inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/new.jeremywhittaker.com\/index.php\/2024\/05\/28\/automating-fincen-beneficial-ownership-information-reporting-for-multiple-organizations\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/new.jeremywhittaker.com\/index.php\/2024\/05\/28\/automating-fincen-beneficial-ownership-information-reporting-for-multiple-organizations\/","url":"https:\/\/new.jeremywhittaker.com\/index.php\/2024\/05\/28\/automating-fincen-beneficial-ownership-information-reporting-for-multiple-organizations\/","name":"Automating FinCEN Beneficial Ownership Information Reporting for Multiple Organizations - Jeremy Whittaker","isPartOf":{"@id":"https:\/\/new.jeremywhittaker.com\/#website"},"primaryImageOfPage":{"@id":"https:\/\/new.jeremywhittaker.com\/index.php\/2024\/05\/28\/automating-fincen-beneficial-ownership-information-reporting-for-multiple-organizations\/#primaryimage"},"image":{"@id":"https:\/\/new.jeremywhittaker.com\/index.php\/2024\/05\/28\/automating-fincen-beneficial-ownership-information-reporting-for-multiple-organizations\/#primaryimage"},"thumbnailUrl":"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/05\/Screenshot-2024-05-28-094602.png","datePublished":"2024-05-28T18:15:19+00:00","dateModified":"2024-05-28T18:35:35+00:00","breadcrumb":{"@id":"https:\/\/new.jeremywhittaker.com\/index.php\/2024\/05\/28\/automating-fincen-beneficial-ownership-information-reporting-for-multiple-organizations\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/new.jeremywhittaker.com\/index.php\/2024\/05\/28\/automating-fincen-beneficial-ownership-information-reporting-for-multiple-organizations\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/new.jeremywhittaker.com\/index.php\/2024\/05\/28\/automating-fincen-beneficial-ownership-information-reporting-for-multiple-organizations\/#primaryimage","url":"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/05\/Screenshot-2024-05-28-094602.png","contentUrl":"https:\/\/new.jeremywhittaker.com\/wp-content\/uploads\/2024\/05\/Screenshot-2024-05-28-094602.png","width":432,"height":122},{"@type":"BreadcrumbList","@id":"https:\/\/new.jeremywhittaker.com\/index.php\/2024\/05\/28\/automating-fincen-beneficial-ownership-information-reporting-for-multiple-organizations\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/new.jeremywhittaker.com\/"},{"@type":"ListItem","position":2,"name":"Automating FinCEN Beneficial Ownership Information Reporting for Multiple Organizations"}]},{"@type":"WebSite","@id":"https:\/\/new.jeremywhittaker.com\/#website","url":"https:\/\/new.jeremywhittaker.com\/","name":"Jeremy Whittaker","description":"Research, software, markets, housing, and energy","publisher":{"@id":"https:\/\/new.jeremywhittaker.com\/#\/schema\/person\/ed0edfdefb3e180693efef453372980c"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/new.jeremywhittaker.com\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":["Person","Organization"],"@id":"https:\/\/new.jeremywhittaker.com\/#\/schema\/person\/ed0edfdefb3e180693efef453372980c","name":"JeremyWhittaker","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/secure.gravatar.com\/avatar\/c8ac20e6dfa86b5f27ce9bffee4851099770cbea5ae7338a274865bfbc8c0218?s=96&d=retro&r=g","url":"https:\/\/secure.gravatar.com\/avatar\/c8ac20e6dfa86b5f27ce9bffee4851099770cbea5ae7338a274865bfbc8c0218?s=96&d=retro&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/c8ac20e6dfa86b5f27ce9bffee4851099770cbea5ae7338a274865bfbc8c0218?s=96&d=retro&r=g","caption":"JeremyWhittaker"},"logo":{"@id":"https:\/\/secure.gravatar.com\/avatar\/c8ac20e6dfa86b5f27ce9bffee4851099770cbea5ae7338a274865bfbc8c0218?s=96&d=retro&r=g"},"sameAs":["http:\/\/www.jeremywhittaker.com","https:\/\/www.facebook.com\/WhittakerJeremy","https:\/\/www.linkedin.com\/in\/jeremywhittaker\/"],"url":"https:\/\/new.jeremywhittaker.com\/index.php\/author\/jeremywhittaker\/"}]}},"_links":{"self":[{"href":"https:\/\/new.jeremywhittaker.com\/index.php\/wp-json\/wp\/v2\/posts\/7705","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/new.jeremywhittaker.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/new.jeremywhittaker.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/new.jeremywhittaker.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/new.jeremywhittaker.com\/index.php\/wp-json\/wp\/v2\/comments?post=7705"}],"version-history":[{"count":14,"href":"https:\/\/new.jeremywhittaker.com\/index.php\/wp-json\/wp\/v2\/posts\/7705\/revisions"}],"predecessor-version":[{"id":7735,"href":"https:\/\/new.jeremywhittaker.com\/index.php\/wp-json\/wp\/v2\/posts\/7705\/revisions\/7735"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/new.jeremywhittaker.com\/index.php\/wp-json\/wp\/v2\/media\/7718"}],"wp:attachment":[{"href":"https:\/\/new.jeremywhittaker.com\/index.php\/wp-json\/wp\/v2\/media?parent=7705"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/new.jeremywhittaker.com\/index.php\/wp-json\/wp\/v2\/categories?post=7705"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/new.jeremywhittaker.com\/index.php\/wp-json\/wp\/v2\/tags?post=7705"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}