|
(() => { |
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); |
|
|
|
const sanitize = (value) => |
|
String(value ?? "") |
|
.normalize("NFKD") |
|
.replace(/[^\w\s.-]/g, "") |
|
.replace(/\s+/g, "_") |
|
.replace(/_+/g, "_") |
|
.replace(/^_+|_+$/g, "") |
|
.slice(0, 80); |
|
|
|
const formatAmount = (amountCents, currency = "CHF") => { |
|
const sign = amountCents < 0 ? "-" : ""; |
|
const absolute = Math.abs(Number(amountCents) || 0); |
|
const whole = Math.floor(absolute / 100); |
|
const fractional = String(absolute % 100).padStart(2, "0"); |
|
return `${currency}_${sign}${whole}.${fractional}`; |
|
}; |
|
|
|
const formatDate = (dateString) => { |
|
const date = new Date(dateString); |
|
const year = date.getFullYear(); |
|
const month = String(date.getMonth() + 1).padStart(2, "0"); |
|
const day = String(date.getDate()).padStart(2, "0"); |
|
return `${year}-${month}-${day}`; |
|
}; |
|
|
|
const buildMainDocument = (purchase) => { |
|
if (purchase.online) { |
|
if (!purchase.encTransNumber || !purchase.location?.formatId) { |
|
return null; |
|
} |
|
|
|
return { |
|
kind: "bill", |
|
url: |
|
`/bin/coop/kbk/kassenzettelpoc?bc=${encodeURIComponent(purchase.encTransNumber)}` + |
|
`&pdfType=bill&format=${encodeURIComponent(purchase.location.formatId)}`, |
|
}; |
|
} |
|
|
|
if (!purchase.encBarcode) { |
|
return null; |
|
} |
|
|
|
return { |
|
kind: "receipt", |
|
url: `/bin/coop/kbk/kassenzettelpoc?bc=${encodeURIComponent(purchase.encBarcode)}&pdfType=receipt`, |
|
}; |
|
}; |
|
|
|
const buildWarrantyDocument = (purchase) => { |
|
if (!purchase.hasWarranty || purchase.online || !purchase.encBarcode) { |
|
return null; |
|
} |
|
|
|
return { |
|
kind: "warranty", |
|
url: `/bin/coop/kbk/kassenzettelpoc?bc=${encodeURIComponent(purchase.encBarcode)}&pdfType=warranty`, |
|
}; |
|
}; |
|
|
|
const buildFilename = (purchase, kind) => { |
|
const date = formatDate(purchase.date); |
|
const store = |
|
sanitize(purchase.location?.name) || |
|
sanitize(purchase.location?.specificFormatId) || |
|
sanitize(purchase.location?.formatId) || |
|
"store"; |
|
const amount = formatAmount(purchase.total?.amount, purchase.total?.currency || "CHF"); |
|
return `${date}_${store}_${amount}_${kind}.pdf`; |
|
}; |
|
|
|
async function getAllPurchases() { |
|
const allPurchases = []; |
|
let endtimestamp; |
|
|
|
while (true) { |
|
const params = new URLSearchParams({ |
|
currentPage: "0", |
|
totalAmountMin: "undefined00", |
|
totalAmountMax: "undefined00", |
|
}); |
|
|
|
if (endtimestamp) { |
|
params.set("endtimestamp", endtimestamp); |
|
} |
|
|
|
const response = await fetch( |
|
`/bin/coop/supercard/digitalReceipt/getPurchases.json?${params.toString()}`, |
|
{ credentials: "include" }, |
|
); |
|
|
|
if (!response.ok) { |
|
throw new Error(`Purchase list request failed with status ${response.status}`); |
|
} |
|
|
|
const data = await response.json(); |
|
const purchases = data.purchases || []; |
|
|
|
if (purchases.length === 0) { |
|
break; |
|
} |
|
|
|
allPurchases.push(...purchases); |
|
|
|
if (purchases.length < 20) { |
|
break; |
|
} |
|
|
|
endtimestamp = purchases[purchases.length - 1].endtimestamp; |
|
await sleep(300); |
|
} |
|
|
|
return allPurchases; |
|
} |
|
|
|
async function downloadPdf(url, filename) { |
|
const response = await fetch(url, { credentials: "include" }); |
|
|
|
if (!response.ok) { |
|
throw new Error(`PDF request failed with status ${response.status}`); |
|
} |
|
|
|
const pdfBlob = await response.blob(); |
|
const downloadBlob = new Blob([pdfBlob], { type: "application/octet-stream" }); |
|
const objectUrl = URL.createObjectURL(downloadBlob); |
|
const anchor = document.createElement("a"); |
|
anchor.href = objectUrl; |
|
anchor.download = filename; |
|
anchor.target = "_self"; |
|
anchor.rel = "noopener"; |
|
document.body.appendChild(anchor); |
|
anchor.click(); |
|
anchor.remove(); |
|
setTimeout(() => URL.revokeObjectURL(objectUrl), 10000); |
|
} |
|
|
|
async function run() { |
|
const purchases = await getAllPurchases(); |
|
const cutoff = Date.now() - 365 * 24 * 60 * 60 * 1000; |
|
const purchasesLastYear = purchases.filter((purchase) => new Date(purchase.date).getTime() >= cutoff); |
|
|
|
const jobs = []; |
|
for (const purchase of purchasesLastYear) { |
|
const mainDocument = buildMainDocument(purchase); |
|
if (mainDocument) { |
|
jobs.push({ |
|
filename: buildFilename(purchase, mainDocument.kind), |
|
url: mainDocument.url, |
|
}); |
|
} |
|
|
|
const warrantyDocument = buildWarrantyDocument(purchase); |
|
if (warrantyDocument) { |
|
jobs.push({ |
|
filename: buildFilename(purchase, warrantyDocument.kind), |
|
url: warrantyDocument.url, |
|
}); |
|
} |
|
} |
|
|
|
console.log(`Found ${purchases.length} purchases total`); |
|
console.log(`Found ${purchasesLastYear.length} purchases in the last 365 days`); |
|
console.log(`Downloading ${jobs.length} PDFs`); |
|
|
|
let successCount = 0; |
|
for (let index = 0; index < jobs.length; index += 1) { |
|
const job = jobs[index]; |
|
try { |
|
await downloadPdf(job.url, job.filename); |
|
successCount += 1; |
|
console.log(`Downloaded ${index + 1}/${jobs.length}: ${job.filename}`); |
|
} catch (error) { |
|
console.warn(`Failed ${index + 1}/${jobs.length}: ${job.filename}`, error); |
|
} |
|
await sleep(1200); |
|
} |
|
|
|
console.log(`Done. Downloaded ${successCount}/${jobs.length} PDFs.`); |
|
} |
|
|
|
window.supercardDownloadAll = run; |
|
console.log("Loaded. Run supercardDownloadAll() on the logged-in purchases page."); |
|
})(); |