{ config, pkgs, ... }: let domain = "firefly.hoyer.world"; importDomain = "firefly-import.hoyer.world"; importerHome = "/var/lib/firefly-iii-data-importer"; inbox = "${importerHome}/inbox"; configFile = "${importerHome}/sparda-config.json"; bankCode = "55090500"; userId = "5987838198"; giroAccountId = "3"; # aqbanking 6.8.2 ships only an "import" profile and a "full" export # profile that renders amounts as fractions ("-499/100"). Firefly's CSV # importer needs decimal amounts and benefits from localIban/remoteIban # columns, so derive a profile that combines "full"'s columns with # decimal value formatting. fireflyCsvProfile = pkgs.runCommand "aqbanking-csv-firefly-profile" { } '' sed 's/name="full"/name="firefly"/; s/valueFormat="rational"/valueFormat="float"/' \ ${pkgs.aqbanking}/share/aqbanking/imexporters/csv/profiles/full.conf > $out ''; vhostBase = { enableACME = false; useACMEHost = "internal.hoyer.world"; forceSSL = true; }; in { sops.secrets = { "firefly/app_key" = { sopsFile = ../../../.secrets/sgx/firefly.yaml; owner = "firefly-iii"; }; "firefly/sparda_pin" = { sopsFile = ../../../.secrets/sgx/firefly.yaml; owner = "firefly-iii-data-importer"; }; "firefly/auto_import_secret" = { sopsFile = ../../../.secrets/sgx/firefly.yaml; owner = "firefly-iii-data-importer"; }; }; environment.systemPackages = [ pkgs.aqbanking ]; systemd = { tmpfiles.rules = [ "d ${inbox} 0700 firefly-iii-data-importer nginx -" "d ${importerHome}/.aqbanking/imexporters/csv/profiles 0700 firefly-iii-data-importer nginx -" "L+ ${importerHome}/.aqbanking/imexporters/csv/profiles/firefly.conf - - - - ${fireflyCsvProfile}" ]; services.firefly-sparda-fetch = { description = "Fetch Sparda transactions via FinTS and trigger Firefly auto-import"; after = [ "network-online.target" "phpfpm-firefly-iii-data-importer.service" ]; wants = [ "network-online.target" ]; path = with pkgs; [ aqbanking curl coreutils ]; serviceConfig = { Type = "oneshot"; User = "firefly-iii-data-importer"; Group = "nginx"; RuntimeDirectory = "firefly-sparda-fetch"; LoadCredential = [ "pin:${config.sops.secrets."firefly/sparda_pin".path}" "secret:${config.sops.secrets."firefly/auto_import_secret".path}" ]; ProtectSystem = "strict"; ReadWritePaths = [ importerHome ]; ProtectHome = true; PrivateTmp = true; NoNewPrivileges = true; TimeoutStartSec = "3min"; }; script = '' set -euo pipefail pinfile=$RUNTIME_DIRECTORY/pinfile umask 077 printf 'PIN_%s_%s = "%s"\n' "${bankCode}" "${userId}" \ "$(<"$CREDENTIALS_DIRECTORY/pin")" >"$pinfile" ts=$(date +%Y%m%d-%H%M%S) ctx=$RUNTIME_DIRECTORY/ctx-$ts.aqb out=${inbox}/sparda-$ts.csv # Refresh SEPA account list — Atruvia/Sparda rejects HKCAZ # ("Mussfeld 9160") if this metadata isn't fresh in the dialog. aqhbci-tool4 -n -A -P "$pinfile" getaccsepa -u ${giroAccountId} fromdate=$(date --date='35 days ago' +%Y%m%d) aqbanking-cli -n -A -P "$pinfile" request \ --transactions --fromdate="$fromdate" \ --aid=${giroAccountId} -c "$ctx" aqbanking-cli export \ --exporter=csv --profile=firefly \ -c "$ctx" -o "$out" secret=$(<"$CREDENTIALS_DIRECTORY/secret") curl -fsS -X POST \ "https://${importDomain}/autoupload?secret=$secret" \ -F "json=@${configFile}" \ -F "importable=@$out" ''; }; # Timer disabled while we work around aqbanking 6.8.2's broken # `-P pinfile` handling. The fetch service authenticates with a wrong # PIN against the bank — three runs locked the access at Sparda. Do # not re-enable until the auth path is replaced (likely python-fints). timers.firefly-sparda-fetch = { wantedBy = [ ]; timerConfig = { OnCalendar = "daily"; Persistent = true; RandomizedDelaySec = "1h"; }; }; }; services = { firefly-iii = { enable = true; enableNginx = true; virtualHost = domain; settings = { APP_ENV = "production"; APP_KEY_FILE = config.sops.secrets."firefly/app_key".path; SITE_OWNER = "harald.hoyer@gmail.com"; TZ = "Europe/Berlin"; DEFAULT_LANGUAGE = "de_DE"; DEFAULT_LOCALE = "de_DE"; TRUSTED_PROXIES = "**"; LOG_CHANNEL = "stack"; }; }; firefly-iii-data-importer = { enable = true; enableNginx = true; virtualHost = importDomain; settings = { FIREFLY_III_URL = "https://${domain}"; VANITY_URL = "https://${importDomain}"; TZ = "Europe/Berlin"; CAN_POST_FILES = "true"; CAN_POST_AUTOIMPORT = "true"; IMPORT_DIR_ALLOWLIST = inbox; AUTO_IMPORT_SECRET_FILE = config.sops.secrets."firefly/auto_import_secret".path; }; }; nginx.virtualHosts = { ${domain} = vhostBase; ${importDomain} = vhostBase; }; }; }