Skip to content

Instantly share code, notes, and snippets.

@Jessidhia
Last active August 29, 2023 00:37
Show Gist options
  • Save Jessidhia/fbf19804e1ce2949a70aa36561930da7 to your computer and use it in GitHub Desktop.
Save Jessidhia/fbf19804e1ce2949a70aa36561930da7 to your computer and use it in GitHub Desktop.
// @ts-check
// ==UserScript==
// @name PayPay Bank CSV Export
// @match https://login.paypay-bank.co.jp/wctx/*
// @match https://login.japannetbank.co.jp/wctx/*
// @icon https://login.paypay-bank.co.jp/favicon.ico
// @grant GM.setValue
// @grant GM.getValue
// @noframes
// @updateURL https://gist.github.com/Jessidhia/fbf19804e1ce2949a70aa36561930da7/raw/jnb.user.js
// @version 2.3
// @author Jessidhia
// ==/UserScript==
;(async function () {
'use strict'
/**
* @typedef {object} GMAPI
* @prop {<T = any>(key: string, defaultValue?: T) => Promise<T>} getValue
* @prop {(key: string, value: any) => Promise<void>} setValue
*/
/** @type {GMAPI}*/
const GM = globalThis.GM
/**
* @typedef {object} CCDetail
* @prop {string} payee
*/
const pageTitle = document.querySelector('.titleL h1').textContent
if (pageTitle.endsWith('デビット ご利用明細一覧')) {
await GM.setValue('ccdetail', {
...(await GM.getValue('ccdetail', null)),
...parseDebitCardStatement(),
})
}
if (pageTitle.endsWith('普通預金取引明細照会')) {
const buttonRow = document.querySelector('.blkTitleR')
if (buttonRow) {
const text = document.createElement('span')
text.classList.add('link-icon-csv')
text.textContent = 'YNAB CSV'
const button = document.createElement('a')
button.appendChild(text)
button.href = 'javascript:void 0'
button.addEventListener('click', handleExportClick)
const li = document.createElement('li')
li.appendChild(button)
buttonRow.appendChild(li)
}
}
return
/** @param {Event} e */
async function handleExportClick(e) {
e.preventDefault()
const { parsed, missing } = parseTransactions(
await GM.getValue('ccdetail', {})
)
if (missing.length > 0) {
alert(
`Missing Debit Card transaction details for the following transactions:\n\n${missing.join(
'\n'
)}\n\nVisit the appropriate Debit Card details page to load the missing details.`
)
return
}
exportToCsv('jnb.csv', [
['Date', 'Payee', 'Memo', 'Outflow', 'Inflow'],
...parsed.map(({ date, payee, expense, income }) => [
date,
payee,
'',
expense,
income,
]),
])
}
/** @param {{[id: string]: CCDetail}} details */
function parseTransactions(details) {
const tableRows = /** @type {HTMLElement[]} */ document.querySelectorAll(
'.detail-list-wrap .detail-inner > ul'
)
const parsed = []
const missing = []
for (const row of tableRows) {
if (row.childElementCount !== 4) {
alert('Table format changed')
return
}
const rawDate = row.children[0].children[0].textContent.trim()
const payee = Array.from(row.children[0].childNodes).filter(node => node.nodeType === 3 && node.textContent.trim() !== '')[0].textContent.trim()
const isExpense = new Set(row.children[1].classList).has('colRed')
const value = Array.from(row.children[1].childNodes).filter(node => node.nodeType === 3 && node.textContent.trim() !== '')[0].textContent.trim().replace(/,/g, '')
// YNAB does not read time, only date
const date = rawDate.replace(/\//g, '-').replace(/ .*$/, '')
const tag = /^.デビット.* ([A-Z0-9]+)$/.exec(payee)?.[1]
if (tag) {
const detail = details[tag]
if (!detail) {
missing.push(tag)
} else {
parsed.push({ date, payee: detail.payee, expense: isExpense ? value : '', income: !isExpense ? value : '' })
}
} else {
parsed.push({ date, payee, expense: isExpense ? value : '', income: !isExpense ? value : '' })
}
}
return { parsed, missing }
}
function parseDebitCardStatement() {
const entries = document.querySelectorAll('.detailTbl > ul')
/** @type {{[id: string]: CCDetail}} */
const data = {}
for (const { children: [,dateAndPayeeEl, , idEl] } of entries) {
const id = idEl.childNodes[2]?.textContent
const payee = dateAndPayeeEl.querySelector('.fBold')?.textContent
if (id && payee) {
data[id] = { payee }
}
}
return data
}
// refactored from https://stackoverflow.com/a/24922761
/**
* @typedef {null|string|number|Date} CSVData
* @param {string} filename
* @param {ReadonlyArray<ReadonlyArray<CSVData>>} rows
*/
function exportToCsv(filename, rows) {
const csvFile = rows
.map(row =>
row
.map(item => {
const itemString =
item === null
? ''
: item instanceof Date
? item.toLocaleString()
: item.toString()
const escaped = itemString.replace(/\"/g, '""')
return /(\"|,|\n)/.test(escaped) ? `\"${escaped}\"` : escaped
})
.join(',')
)
.join('\n')
const blob = new Blob([csvFile], { type: 'text/csv;charset=utf-8;' })
const link = document.createElement('a')
const url = URL.createObjectURL(blob)
link.setAttribute('href', url)
link.setAttribute('download', filename)
link.style.visibility = 'hidden'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
})()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment