Files
docker-finance/container/src/hledger-flow/accounts/btcpayserver/btcpayserver-shared.bash
Aaron Fiore 5920d87f86 container: hledger-flow: btcpayserver: support "Legacy Invoice Export" plugin, update "Wallets" impl
BTCPay Server v2.2.0 has a new "Invoices" export format that is more
complex than the prior "Legacy Invoice Export" format.

To help facilitate the transition to the new format, upstream has
provided a plugin for backwards compatibility. However, this plugin
happens to provide additional columns that must be supported.

Additionaly, the "Wallets" format also has new columns with fee
information, so the impl now supports tx fees (prior, fees required
manual input).

The "Wallets" impl also now defaults to a refund expense account for
outgoing txs and adds tax rules & documentation for rationale.
2025-08-08 12:33:24 -07:00

275 lines
8.9 KiB
Bash
Executable File

#!/usr/bin/env bash
# docker-finance | modern accounting for the power-user
#
# Copyright (C) 2021-2025 Aaron Fiore (Founder, Evergreen Crypto LLC)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
# "Libraries"
#
[ -z "$DOCKER_FINANCE_CONTAINER_REPO" ] && exit 1
source "${DOCKER_FINANCE_CONTAINER_REPO}/src/hledger-flow/lib/lib_preprocess.bash" "$1" "$2"
source "${DOCKER_FINANCE_CONTAINER_REPO}/src/hledger-flow/lib/lib_utils.bash" || exit 1
#
# Implementation
#
[ -z "$global_year" ] && exit 1
[ -z "$global_subaccount" ] && exit 1
[ -z "$global_in_path" ] && exit 1
[ -z "$global_out_path" ] && exit 1
function btcpayserver::print_warning()
{
[ -z "$global_account" ] && lib_utils::die_fatal
lib_utils::print_warning "${global_account}: '$1' report is not supported"
}
#
# BTCPay Server v2
#
# NOTE/TODO:
#
# TL;DR:
#
# - As of v2.2.0, *MUST* install the "Legacy Invoice Export" plugin. TODO: use v2.2.0 format w/out plugin
#
# - *MUST* export both "Legacy Invoice" report and "Wallets" report for an accurate balance.
# * The "Legacy Invoice" report currently does *not* include any outgoing txs (refunds or transfers).
#
# WARNING:
#
# - If using a watch-ony wallet, consider accounting entirely here (via btcpayserver) or
# entirely via that other wallet (e.g., electrum). Mixing the two may cause tax accounting issues.
#
# CAUTION:
#
# - The "Wallets" report contains the txid that the "Payments" report *should* include (but doesn't).
# * The "Legacy Invoice" report provides both.
#
# - The "Wallets" report does *not* contain a description for refunds versus transfers.
# * This is done in "Payouts" report or "Refunds" report.
#
# - The "Payouts" report contains the destination address whereas "Refunds" does not.
# * "Payouts" places the Invoice ID in the 'Source' field.
# * "Refunds" places the Invoice ID in the 'InvoiceId' field.
# "Legacy Invoice" report
function btcpayserver::legacy()
{
gawk -v global_year="$global_year" -v global_subaccount="$global_subaccount" \
'{
if (NR<2 || $1 !~ global_year)
next
# ReceivedDate (w/ local timezone added)
sub(/ /, "T", $1) # HACK: makes arg-friendly by removing space
cmd = "date \"+%F %T %z\" --date="$1 | getline date
printf date OFS
printf $2 OFS # StoreId
printf $3 OFS # OrderId
printf $4 OFS # InvoiceId
# InvoiceCreatedDate (w/ local timezone added)
sub(/ /, "T", $5)
cmd = "date \"+%F %T %z\" --date="$5 | getline date
printf date OFS
# InvoiceExpirationDate (w/ local timezone added)
sub(/ /, "T", $6)
cmd = "date \"+%F %T %z\" --date="$6 | getline date
printf date OFS
# InvoiceMonitoringDate (w/ local timezone added)
sub(/ /, "T", $7)
cmd = "date \"+%F %T %z\" --date="$7 | getline date
printf date OFS
# PaymentId
# NOTE: BTCPay Server will append the block index as "-N" to the txid
txid=substr($8, 1, 64); printf txid OFS
ind=substr($8, 66, 2); printf ind OFS
printf $9 OFS # Destination
printf $10 OFS # PaymentType
printf $11 OFS # CryptoCode
# Paid
printf("%.8f", $12); printf OFS
printf $13 OFS # NetworkFee
printf $14 OFS # ConversionRate
printf $15 OFS # PaidCurrency
printf $16 OFS # InvoiceCurrency
printf $17 OFS # InvoiceDue
printf $18 OFS # InvoicePrice
printf $19 OFS # InvoiceTaxIncluded
printf $20 OFS # InvoiceTip
printf $21 OFS # InvoiceSubtotal
printf $22 OFS # InvoiceItemCode
# TODO: if description contains comma(s)?
printf $23 OFS # InvoiceItemDesc
printf $24 OFS # InvoiceFullStatus
printf $25 OFS # InvoiceStatus
printf $26 OFS # InvoiceExceptionStatus
printf $27 OFS # BuyerEmail
printf $28 OFS # Accounted
# WARNING: appears to be always IN (see notes regarding "Wallets" report)
printf "IN" OFS # Direction
printf global_subaccount # Subaccount
printf "\n"
}' FS=, OFS=, "$global_in_path"
}
# "Payments" report
function btcpayserver::payments()
{
btcpayserver::print_warning "Payments"
}
# "Payouts" report
function btcpayserver::payouts()
{
btcpayserver::print_warning "Payouts"
}
# "Refunds" report
function btcpayserver::refunds()
{
btcpayserver::print_warning "Refunds"
}
# "Sales" report
function btcpayserver::sales()
{
btcpayserver::print_warning "Sales"
# TODO: Accounting for this needs more consideration,
# as these are individual products sold w/ only fiat value given.
}
# "Wallets" report
function btcpayserver::wallets()
{
gawk -v global_year="$global_year" -v global_subaccount="$global_subaccount" \
'{
if (NR<2 || $1 !~ global_year)
next
# All "IN"s are removed and handled by legacy invoice
if ($6 !~ /^-/)
next
# Strip sign from amount (using direction instead)
sub(/^-/, "", $6)
# Date (ReceivedDate w/ local timezone added)
sub(/ /, "T", $1) # HACK: makes arg-friendly by removing space
cmd = "date \"+%F %T %z\" --date="$1 | getline date
printf date OFS
printf OFS # (StoreId)
printf OFS # (OrderId)
printf $4 OFS # InvoiceId (InvoiceId)
printf OFS # (InvoiceCreatedDate)
printf OFS # (InvoiceExpirationDate)
printf OFS # (InvoiceMonitoringDate)
# (PaymentId)
printf $3 OFS # TransactionId (txid)
printf OFS # (Block Index)
printf OFS # (Destination)
printf OFS # (PaymentType)
printf $2 OFS # Crypto (CryptoCode)
# BalanceChange (Paid)
# NOTE: must provide as subtotal, so subtract fee
printf("%.8f", $6 - $7); printf OFS
# Fee (NetworkFee)
printf("%.8f", $7); printf OFS
printf OFS # (ConversionRate)
# FeeRate (PaidCurrency)
printf("%.3f", $8); printf OFS
# Rate (XXX) (InvoiceCurrency)
printf("%.3f", $9); printf OFS
printf OFS # (InvoiceDue)
printf OFS # (InvoicePrice)
printf OFS # (InvoiceTaxIncluded)
printf OFS # (InvoiceTip)
printf OFS # (InvoiceSubtotal)
printf OFS # (InvoiceItemCode)
printf OFS # (InvoiceItemDesc)
printf OFS # (InvoiceFullStatus)
printf OFS # (InvoiceStatus)
printf OFS # (InvoiceExceptionStatus)
printf OFS # (BuyerEmail)
printf $5 OFS # Confirmed (Accounted)
# All "IN"s are removed and handled by legacy invoice
printf "OUT" OFS # Direction
printf global_subaccount # Subaccount
printf "\n"
}' FS=, OFS=, "$global_in_path"
}
function btcpayserver::parse()
{
lib_preprocess::test_header "ReceivedDate,StoreId,OrderId,InvoiceId,InvoiceCreatedDate,InvoiceExpirationDate,InvoiceMonitoringDate,PaymentId,Destination,PaymentType,CryptoCode,Paid,NetworkFee,ConversionRate,PaidCurrency,InvoiceCurrency,InvoiceDue,InvoicePrice,InvoiceTaxIncluded,InvoiceTip,InvoiceSubtotal,InvoiceItemCode,InvoiceItemDesc,InvoiceFullStatus,InvoiceStatus,InvoiceExceptionStatus,BuyerEmail,Accounted" \
&& btcpayserver::legacy
lib_preprocess::test_header "Date,InvoiceId,OrderId,Category,PaymentMethodId,Confirmed,Address,PaymentCurrency,PaymentAmount,PaymentMethodFee,LightningAddress,InvoiceCurrency,InvoiceCurrencyAmount,Rate" \
&& btcpayserver::payments
lib_preprocess::test_header "Date,Source,State,PaymentType,Currency,Amount,OriginalCurrency,OriginalAmount,Destination" \
&& btcpayserver::payouts
lib_preprocess::test_header "Date,InvoiceId,Currency,Completed,Awaiting,Limit,FullyPaid" \
&& btcpayserver::refunds
lib_preprocess::test_header "Date,InvoiceId,State,AppId,Product,Quantity,CurrencyAmount,Currency" \
&& btcpayserver::sales
# TODO: don't hardcode USD (upstream should make the currency in "Rate (XXX)" into a separate column).
#lib_preprocess::test_header "Date,Crypto,TransactionId,InvoiceId,Confirmed,BalanceChange,Fee,FeeRate,Rate (USD)" \
lib_preprocess::test_header "Date,Crypto,TransactionId,InvoiceId,Confirmed,BalanceChange,Fee,FeeRate" \
&& btcpayserver::wallets
}
function main()
{
# WARNING: upstream produces carriage return all EOL
btcpayserver::parse | sed -e 's:\x0d::g' >"$global_out_path" || lib_utils::catch $?
}
main "$@"
# vim: sw=2 sts=2 si ai et