Skip to content

Instantly share code, notes, and snippets.

@riking
Last active August 15, 2019 23:23
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save riking/0f0dab2b7761d2f6895c5d58c0b62a66 to your computer and use it in GitHub Desktop.
Save riking/0f0dab2b7761d2f6895c5d58c0b62a66 to your computer and use it in GitHub Desktop.
Beancount exporter for Patreon transactions.
(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());
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment