The path towards a good-enough software for invoices was quite thorny, I would say. I have been using from self-hosted Craterapp instance for a year and a half. Crater worked almost fine. It had no major issue, I would say. But it had a lot of small-ish issues that bugged me a lot. I mean, it was livable but, hey, this is administrative work. It is not very fun, usually boring and tedious. Using software that makes this even more painful, even though just a little, meant a death by a thousand cuts for me.
I was looking at a solution to this problem for some time. Tried Revolut Business invoicing platform. That was very limited in features. The only upside it had was an automatic payment pairing, which is nice, but for low volume of invoices it was not a deal-breaker. Then I found Paypal had quite a mature invoicing platform built-in as well and fiddled with it for a bit. I would have almost settled there, not for the recommendation from a friend.
Enter faktury-online.com#
One year ago I have started using https://faktury-online.com to handle my invoices and a price offers. At first, I was skeptical, I do not even remember why. But since it is made by a Slovak team, I instantly fell in love, because it had all the bureaucratic hurdles of our country solved, unlike all the aforementioned options, which were either very general or even optimized the most for US.
As a year passed, I have found no major obstacle in that software. There was just a one catch: I had not backup of the data, apart from PDFs that I keep in electronic form and the accountant keeps in their archive. Disclaimer: I am not associated in any way with faktury-online.com and I have never received any sponsorship from them. If you like them, just go using and pay them, the first year is free.
Back to the backup problem. They offer a backup service, which costs around twice the amount of base fee for the usage, if I am not mistaken. It is still in the low tenths for a whole year, so not expensive at all, but they also offer an API. After considering its abilities, I decided, I'll do backup myself via API.
Backup via API#
This recipe needed these ingredients:
- script, either in PHP or JS that will call the API and grab all the data
- way to store the data, preferably as a JSON file
- way to call the script either when I do a change or at least periodically
Start by installing required dependencies:
npm i @actions/core @actions/github @vercel/ncc
Now update package.json
to include the build:
"scripts": {
"build": "ncc build index.js --license licenses.txt"
},
Now create the index.js
which will actually do the API polling:
const fs = require("fs").promises
const path = require("path")
const key = process.env.API_KEY
const email = process.env.EMAIL
const baseUrl = process.env.BASE_URL
let phpSessionId
async function fetchData(endpoint, code = "") {
const data = JSON.stringify({ key, email, code })
const url = `${baseUrl}/${endpoint}?data=${encodeURIComponent(data)}`
const options = {
method: "GET",
credentials: "include",
headers: phpSessionId ? { Cookie: `PHPSESSID=${phpSessionId}` } : {},
}
try {
const response = await fetch(url, options)
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`)
}
const setCookieHeader = response.headers.get("set-cookie")
if (setCookieHeader) {
const matches = setCookieHeader.match(/PHPSESSID=([^;]+)/)
if (matches) {
phpSessionId = matches[1]
}
}
return await response.json()
} catch (error) {
console.error(error)
}
}
async function writeOutputToFile(data, filePath, sortBy) {
try {
data.sort((a, b) => b[sortBy].localeCompare(a[sortBy]))
await fs.writeFile(filePath, JSON.stringify(data, null, 2))
console.log("Output written to:", path.resolve(filePath))
} catch (error) {
console.error("Error writing to file:", error)
}
}
;(async () => {
await fetchData("init")
const invoicesList = await fetchData("list/created")
const invoicesOutput = await Promise.all(
invoicesList.invoices.map(async invoice =>
fetchData("status", invoice.code)
)
)
await writeOutputToFile(
invoicesOutput,
"invoices.json",
"invoice_number"
)
const offersList = await fetchData("cp-list/created")
const offersOutput = await Promise.all(
offersList.offers.map(async offer =>
fetchData("cp-status", offer.code)
)
)
await writeOutputToFile(offersOutput, "offers.json", "offer_number")
})()
Continue with .github/workflows/main.yml
so the whole process can be
automated:
name: Faktury-online.com backup
on:
workflow_dispatch:
schedule:
- cron: "15 0 1 * *"
jobs:
run:
runs-on: ubuntu-latest
permissions:
contents: write
env:
BASE_URL: ${{ secrets.BASE_URL }}
API_KEY: ${{ secrets.API_KEY }}
EMAIL: ${{ secrets.EMAIL }}
steps:
- uses: actions/checkout@v4
- run: node dist/index.js
- uses: stefanzweifel/git-auto-commit-action@v5
And finalize with configuring GitHub Action secrets for API_KEY
, EMAIL
and BASE_URL
, take these from <faktury-online.com>. Also you need to
enable GitHub actions write permissions, as this creates automatic commits.
A few additional notes:
- if you make any changes to
index.js
runnpm run build
first - runs at the beginning of the month or manually
- it is possible to run this somewhat manually via
act workflow_dispatch
but requiresGITHUB_TOKEN
This was a perfect opportunity to seize the power of Github Actions. I have a lot of free GitHub Actions minutes just sitting there, and they could be doing some useful automation. But I had not learned how to use it so far, so I pushed myself over the weekend to do it. Enjoy!
Links#
- https://docs.github.com/en/actions/creating-actions/creating-a-javascript-action
- https://docs.github.com/en/actions/quickstart
- https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions#accessing-your-secrets
- https://docs.github.com/en/actions/using-workflows/manually-running-a-workflow
- https://jasonet.co/posts/scheduled-actions/
- https://joht.github.io/johtizen/build/2022/01/20/github-actions-push-into-repository.html
- https://www.faktury-online.com/faktury-online-api/manual
- https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows