// ==UserScript==
// @name Amazon QFX Statement Exporter
// @namespace Violentmonkey Scripts
// @license MIT
// @version 1.1.12
// @description Automatically download QFX statements from Amazon
// @match https://*.amazon.com/*
// @grant GM_download
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @require https://cdn.jsdelivr.net/npm/[email protected]/build/md5.min.js
// ==/UserScript==
(function () {
'use strict';
// src/common.ts
async function getElement(selector, POLL_INTERVAL) {
return new Promise((resolve) => {
const check = setInterval(() => {
let el;
if (typeof selector === "string") {
el = document.querySelector(selector);
} else {
el = Array.from(selector).map((s) => document.querySelector(s)).find(Boolean);
}
if (el) {
clearInterval(check);
resolve(el);
}
}, POLL_INTERVAL);
});
}
async function easyDownload({ content, name, saveAs = true }) {
let type;
if (name.endsWith(".qfx"))
type = "application/x-qfx";
else if (name.endsWith(".csv"))
type = "text/csv";
else
type = "application/octet-stream";
await GM_download_promise({
url: URL.createObjectURL(new Blob([content.trim()], { type })),
name,
saveAs
});
}
function GM_download_promise(option) {
const { url, name, saveAs } = option;
return new Promise((resolve, reject) => {
GM_download({ url, name, saveAs, onload: resolve, onerror: reject });
});
}
function easySetValue(key, value) {
GM_setValue(key, value);
}
function easyGetValue(key) {
return GM_getValue(key);
}
// src/amazon/lib.ts
var BANK_ID = "amazon";
var LOGGER_prefix = `[${BANK_ID} Downloader]`;
var origLog = console.log;
var origError = console.error;
console.log = (...args) => origLog(LOGGER_prefix, ...args);
console.error = (...args) => origError(LOGGER_prefix, ...args);
var POLL_INTERVAL = 500;
class SingleTransaction {
id;
hint;
date;
amount;
url;
description;
constructor(id, hint, date, amount, url, description) {
this.id = id;
this.hint = hint;
this.date = date;
this.amount = amount;
this.url = url;
this.description = description;
}
}
var matchedTransactions = {};
var cachedCardTextAreas = {};
var shouldCollectMode = false;
var history_length = 3;
async function addGotoTransactionButton() {
const container = await getElement("#nav-al-your-account", POLL_INTERVAL);
const CLASS = "my-download-btn";
if (container?.querySelector(`.${CLASS}`))
return;
const btn = document.createElement("button");
btn.textContent = "Download CSV";
btn.className = CLASS;
btn.style.cssText = `
padding: 4px 4px;
margin: 0px 0px 4px 0px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
width: fit-content;
`;
btn.addEventListener("click", async () => {
window.location.href = "https://www.amazon.com/cpe/yourpayments/transactions";
});
container?.insertBefore(btn, container.firstChild);
}
async function collectTransactionData() {
if (!shouldCollectMode)
return;
const form = document.querySelector("form.a-spacing-none");
if (!form)
return;
const transactionElements = form.querySelectorAll(".a-section.a-spacing-base");
let currentDate = undefined;
for (const transactionElement of transactionElements) {
if (transactionElement.querySelector(".a-row") === null) {
currentDate = transactionElement.querySelector("span")?.textContent;
continue;
}
const hint_amount = Array.from(transactionElement.querySelectorAll(".a-column span")).map((el) => el.textContent);
const url_element = transactionElement.querySelector(".a-column a");
if (hint_amount.length < 2 || !hint_amount[0] || !hint_amount[1]) {
console.error("Failed to extract hint and amount:", {
hint_amount,
url_element
});
continue;
}
if (hint_amount.length == 4) {
const [hint, amount, order_text, description] = hint_amount;
let id = order_text.trim() || "";
if (id.startsWith("Order #")) {
id = id.slice("Order #".length).trim();
}
matchedTransactions[`${id}---${hint}`] = new SingleTransaction(id, hint, currentDate ? new Date(currentDate) : new Date("2000-01-01"), parseFloat(hint_amount[1].replace(/[$,]/g, "")) || 0, "", description || "");
} else {
const [hint, amount, description] = hint_amount;
if (!url_element) {
console.error("Failed to extract URL element:", {
hint_amount
});
continue;
}
let id = url_element.textContent?.trim() || "";
if (id.startsWith("Order #")) {
id = id.slice("Order #".length).trim();
}
matchedTransactions[`${id}---${hint}`] = new SingleTransaction(id, hint, currentDate ? new Date(currentDate) : new Date("2000-01-01"), parseFloat(hint_amount[1].replace(/[$,]/g, "")) || 0, url_element.href || "", description || "");
}
}
if (checkForOldTransactions(history_length)) {
console.log("Found old transactions, stopped");
shouldCollectMode = false;
attachCardTextAreas();
} else {
console.log("No old transactions found");
pressNextButton();
}
}
async function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function pressNextButton() {
const nextButton = Array.from(document.querySelectorAll("input[type='submit']")).find((input) => {
const span = input.nextElementSibling;
return span && span.textContent === "Next Page";
});
if (nextButton) {
await sleep(1000);
nextButton.click();
await sleep(1000);
collectTransactionData();
}
}
function checkForOldTransactions(ago) {
const oldDate = new Date;
oldDate.setMonth(oldDate.getMonth() - ago);
for (const [_, t] of Object.entries(matchedTransactions)) {
if (t.date < oldDate) {
return true;
}
}
return false;
}
function generateQFX(cardnumber, matchedTransactions2, mapping) {
const earliest = Object.values(matchedTransactions2).reduce((min, t) => t.date < min ? t.date : min, new Date);
const latest = Object.values(matchedTransactions2).reduce((max, t) => t.date > max ? t.date : max, new Date(0));
const transactionCount = {};
for (const [_, t] of Object.entries(matchedTransactions2)) {
transactionCount[t.id] = (transactionCount[t.id] || 0) + 1;
}
const formatQFXDate = (date) => {
const yyyy = date.getFullYear();
const mm = String(date.getMonth() + 1).padStart(2, "0");
const dd = String(date.getDate()).padStart(2, "0");
return `${yyyy}${mm}${dd}120000`;
};
const qfxHeader = `
OFXHEADER:100
DATA:OFXSGML
VERSION:102
SECURITY:NONE
ENCODING:USASCII
CHARSET:1252
COMPRESSION:NONE
OLDFILEUID:NONE
NEWFILEUID:NONE
<OFX>
<SIGNONMSGSRSV1>
<SONRS>
<STATUS>
<CODE>0
<SEVERITY>INFO
</STATUS>
<DTSERVER>20250420120000[0:GMT]
<LANGUAGE>ENG
<FI>
<ORG>B1
<FID>10898
</FI>
<INTU.BID>10898
</SONRS>
</SIGNONMSGSRSV1>
<CREDITCARDMSGSRSV1>
<CCSTMTTRNRS>
<TRNUID>1
<STATUS>
<CODE>0
<SEVERITY>INFO
<MESSAGE>Success
</STATUS>
<CCSTMTRS>
<CURDEF>USD
<CCACCTFROM>
<ACCTID>${cardnumber}
</CCACCTFROM>
<BANKTRANLIST>
<DTSTART>${formatQFXDate(earliest)}[0:GMT]
<DTEND>${formatQFXDate(latest)}[0:GMT]
`;
const qfxTransactions = Object.values(matchedTransactions2).map((t) => {
if (cardnumber != mapping[t.hint])
return "";
const hash = md5(t.id).substring(0, 6);
return `
<STMTTRN>
<TRNTYPE>DEBIT
<DTPOSTED>${formatQFXDate(t.date)}[0:GMT]
<TRNAMT>${t.amount}
<FITID>${formatQFXDate(t.date)}${hash}
<NAME>${t.description}
<MEMO>ID${t.id}-${(transactionCount[t.id] ?? 0) > 1 ? "mixed" : ""}
</STMTTRN>
`;
});
const qfxFooter = `
</BANKTRANLIST>
<LEDGERBAL>
<BALAMT>0
<DTASOF>${formatQFXDate(latest)}[0:GMT]
</LEDGERBAL>
<AVAILBAL>
<BALAMT>0
<DTASOF>${formatQFXDate(latest)}[0:GMT]
</AVAILBAL>
</CCSTMTRS>
</CCSTMTTRNRS>
</CREDITCARDMSGSRSV1>
</OFX>
`;
return qfxHeader + qfxTransactions.join(`
`) + qfxFooter;
}
async function attachCardTextAreas() {
const cardListArea = await getElement("#my-card-list-area", POLL_INTERVAL);
if (cardListArea) {
cardListArea.innerHTML = "";
}
for (const [_, t] of Object.entries(matchedTransactions)) {
if (!cachedCardTextAreas[t.hint]) {
const cardTextArea = document.createElement("textarea");
cardTextArea.textContent = easyGetValue(`amazon-card-details-${t.hint}`) || "";
cardTextArea.className = "my-card-text-area";
cardTextArea.placeholder = `Enter card details to ${t.hint} here...`;
cardTextArea.onchange = (e) => {
const value = e.target.value;
easySetValue(`amazon-card-details-${t.hint}`, value);
};
cachedCardTextAreas[t.hint] = cardTextArea;
}
}
for (const [_, t] of Object.entries(matchedTransactions)) {
const cardTextArea = cachedCardTextAreas[t.hint];
if (cardTextArea) {
cardListArea?.appendChild(cardTextArea);
}
}
const downloadButton = document.createElement("button");
downloadButton.textContent = "Download QFX";
downloadButton.onclick = async () => {
const mapping = {};
for (const [_, t] of Object.entries(matchedTransactions)) {
const cardTextArea = cachedCardTextAreas[t.hint];
if (cardTextArea) {
mapping[t.hint] = cardTextArea.value;
}
}
const uniqueCardNumbers = Array.from(new Set(Object.values(mapping).filter(Boolean)));
for (const cardnumber of uniqueCardNumbers) {
let strip = function(s) {
return s.replace(/[^\w\s]/gi, "").replace(/\s+/g, "_").replace("*", "").replace(" ", "");
};
const qfx_content = generateQFX(cardnumber, matchedTransactions, mapping);
await easyDownload({
content: qfx_content,
name: `${BANK_ID}_${strip(cardnumber)}_${new Date().toISOString().slice(0, 10)}_${history_length}mo.qfx`,
saveAs: true
});
}
const widgetArea = await getElement(".my-widget-area", POLL_INTERVAL);
widgetArea.appendChild(downloadButton);
};
cardListArea?.appendChild(downloadButton);
}
async function addWidget() {
const container = await getElement(".amazonPayWalletBreadcrumbsContainer ", POLL_INTERVAL);
if (!container)
return;
const widgetArea = document.createElement("div");
widgetArea.className = "my-widget-area a-container payWalletContentContainer";
widgetArea.style.border = "1px solid #333";
container?.parentNode?.insertBefore(widgetArea, container.nextSibling);
const title = document.createElement("h3");
title.textContent = "QFX Download Plugin";
widgetArea.appendChild(title);
const collectButton = document.createElement("button");
collectButton.textContent = "Collect Transactions";
collectButton.onclick = () => {
shouldCollectMode = true;
collectButton.style.display = "none";
collectTransactionData();
};
widgetArea.appendChild(collectButton);
const cardArea = document.createElement("div");
const cardLabel = document.createElement("label");
cardLabel.textContent = "Card Details";
cardArea.id = "my-card-list-area";
widgetArea.appendChild(cardLabel);
widgetArea.appendChild(cardArea);
}
// src/amazon/index.ts
try {
addGotoTransactionButton();
addWidget();
} catch (err) {
console.error("[Amazon Downloader] Error:", err);
}
})();