(function() { // Copyright 2018 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Beancount transaction exporter for Patreon // Usage: // On your credit card / Paypal exports, bill everything to the SourceAccount. // The exports from this script will move the money to the corect sub-accounts. // // Open your https://patreon.com/pledges page, go to "Billing History", and // select the correct year. Open the developer tools, paste in this script, and // then type: // // exportPage() // // This will create one console log per transaction on the page (which may be // more than one per month in some circumstances). let AccountPrefix = 'Expenses:Entertainment:Patreon:'; let SourceAccount = 'Expenses:Entertainment:Patreon:Pledges'; // turns "/username" into "Username" function toAccountName(href) { const u = new URL(href); let p = u.pathname; if (p === '/user') { return 'U' + u.search.slice(3); } if (/_/.test(p)) { p = p.replace(/_/g, ''); } return p.slice(1, 2).toUpperCase() + p.slice(2); } // turns "$5.00" into "5.00 USD" function toCurrency(amount) { if (amount.startsWith('$')) { return amount.slice(1) + ' USD'; } else if (amount.startsWith('£')) { return amount.slice(1) + ' GBP'; } else if (amount.startsWith('€')) { return amount.slice(1) + ' EUR'; } else { throw 'unimplemented currency code: ' + amount; } } var monthNames = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ]; // TODO - check other locales. function textToISOTextDate(text) { const split = text.split(' '); const monthIdx = monthNames.indexOf(split[0]); if (monthIdx === -1) { throw 'Failed to parse date: ' + text; } const dayText = split[1].replace(/,/g, ''); const yearText = split[2]; const monthText = (monthIdx + 1).toLocaleString('en-US', {minimumIntegerDigits: 2, style: 'decimal'}); // Sanity check result const result = `${yearText}-${monthText}-${dayText}`; if (isNaN(Date.parse(result))) { throw 'Failed to parse date: \'' + text + '\' got \'' + result + '\''; } return result; } // @param [pledges-historical-desktop-layout-row] function exportRow(el) { if (el.childElementCount !== 5) { throw 'row layout has changed'; } // Get username const userLink = el.children[0].querySelector('a').href; const acct = toAccountName(userLink); // Get amount const successEl = el.children[3].querySelector('[color="success"]'); if (!successEl) { return ';' + acct + ' ; Transaction not success'; } const amountContainerEl = successEl.parentElement.parentElement.previousElementSibling, amountText = amountContainerEl.firstElementChild.innerText; const currency = toCurrency(amountText); return [acct, currency]; } // @param [data-tag="pledges-historical-group"] function exportTransaction(el) { const headerEl = el.children[0], listEl = el.children[1]; const listContainer = listEl.querySelector( '[data-tag="pledges-historical-desktop-layout-rows-container"]'); if (!listContainer) { throw 'layout has changed'; } // header > left section > color dark > text const pageDateText = headerEl.firstElementChild.firstElementChild.innerText; const ourDateText = textToISOTextDate(pageDateText); // header > right section > color dark const totalEl = headerEl.lastElementChild.firstElementChild; // split into two text elements const totalText = totalEl.childNodes[1].data; const totalCurrency = '-' + toCurrency(totalText); let resultHeader = `${ourDateText} * "Patreon Pledges for ${pageDateText}" ""`; let rows = []; for (let i = 0; i < listContainer.childElementCount; i++) { let rowEl = listContainer.childNodes[i]; let rowContainer = rowEl.querySelector( '[data-tag="pledges-historical-desktop-layout-row"]'); rows.push(exportRow(rowContainer)); } rows = rows.map((it) => ('\t' + AccountPrefix + it[0].padEnd(18 - 1) + ' ' + it[1])); const finalText = [ resultHeader, '\t' + SourceAccount + ' ' + totalCurrency ].concat(rows).join('\n'); return finalText; } window.exportTransaction = () => console.log(exportTransaction()); function exportPage() { let transactions = []; document.querySelectorAll('[data-tag="pledges-historical-group"]') .forEach((el) => { transactions.push(exportTransaction(el)); }); return transactions.reverse().join('\n\n'); } window.exportPage = () => console.log(exportPage()); })();