From 1268b0d01c50ed179a1d943008c15610b6fb609b Mon Sep 17 00:00:00 2001 From: brihter Date: Sun, 16 Feb 2025 16:48:03 +0100 Subject: [PATCH 1/7] improve docs, fix bookkeeping --- bookkeeping.md | 126 ++++++++++++++++++++++----------------------- etc/bookkeeping.js | 40 ++++++++++++-- readme.md | 12 ++--- 3 files changed, 104 insertions(+), 74 deletions(-) diff --git a/bookkeeping.md b/bookkeeping.md index ab2e103..9f7742e 100644 --- a/bookkeeping.md +++ b/bookkeeping.md @@ -4,80 +4,80 @@ | Category | Amount | | --- | --- | -| Income | +$60.00 | -| Expenses | -$12.99 | -| Balance | +$47.01 | +| Income | +60.00 | +| Expenses | -10.64 | +| Balance | +49.36 | ## By Provider | By Provider | Total | | --- | --- | -| Hetzner | +$54.01 | -| Cloudflare | +$0.00 | -| Backblaze | +$0.00 | -| AWS | -$7.00 | +| Hetzner | +56.36 | +| Cloudflare | 0.00 | +| Backblaze | 0.00 | +| AWS | -7.00 | ## By Year | By Year | Total | | --- | --- | -| 2025 | +$53.45 | -| 2024 | -$3.36 | -| 2023 | -$3.08 | +| 2025 | +55.80 | +| 2024 | -3.36 | +| 2023 | -3.08 | ## Transactions | Date | Description | Amount | Provider | | --- | --- | --- | --- | -| 2025-02-12 | Hetzner Object Storage Billing | -$5.99 | Hetzner | -| 2025-02-12 | Backblaze B2 Billing | +$0.00 | Backblaze | -| 2025-02-10 | Hetzner Cloud Credits | +$60.00 | Hetzner | -| 2025-02-01 | AWS S3 Billing | -$0.28 | AWS | -| 2025-02-01 | Cloudflare R2 Billing | +$0.00 | Cloudflare | -| 2025-01-01 | AWS S3 Billing | -$0.28 | AWS | -| 2025-01-01 | Cloudflare R2 Billing | +$0.00 | Cloudflare | -| 2024-12-01 | AWS S3 Billing | -$0.28 | AWS | -| 2024-12-01 | Cloudflare R2 Billing | +$0.00 | Cloudflare | -| 2024-11-01 | AWS S3 Billing | -$0.28 | AWS | -| 2024-11-01 | Cloudflare R2 Billing | +$0.00 | Cloudflare | -| 2024-09-30 | AWS S3 Billing | -$0.28 | AWS | -| 2024-09-30 | Cloudflare R2 Billing | +$0.00 | Cloudflare | -| 2024-08-31 | AWS S3 Billing | -$0.28 | AWS | -| 2024-08-31 | Cloudflare R2 Billing | +$0.00 | Cloudflare | -| 2024-07-31 | AWS S3 Billing | -$0.28 | AWS | -| 2024-07-31 | Cloudflare R2 Billing | +$0.00 | Cloudflare | -| 2024-06-30 | AWS S3 Billing | -$0.28 | AWS | -| 2024-06-30 | Cloudflare R2 Billing | +$0.00 | Cloudflare | -| 2024-05-31 | AWS S3 Billing | -$0.28 | AWS | -| 2024-05-31 | Cloudflare R2 Billing | +$0.00 | Cloudflare | -| 2024-04-30 | AWS S3 Billing | -$0.28 | AWS | -| 2024-04-30 | Cloudflare R2 Billing | +$0.00 | Cloudflare | -| 2024-03-31 | AWS S3 Billing | -$0.28 | AWS | -| 2024-03-31 | Cloudflare R2 Billing | +$0.00 | Cloudflare | -| 2024-03-01 | AWS S3 Billing | -$0.28 | AWS | -| 2024-03-01 | Cloudflare R2 Billing | +$0.00 | Cloudflare | -| 2024-02-01 | AWS S3 Billing | -$0.28 | AWS | -| 2024-02-01 | Cloudflare R2 Billing | +$0.00 | Cloudflare | -| 2024-01-01 | AWS S3 Billing | -$0.28 | AWS | -| 2024-01-01 | Cloudflare R2 Billing | +$0.00 | Cloudflare | -| 2023-12-01 | AWS S3 Billing | -$0.28 | AWS | -| 2023-12-01 | Cloudflare R2 Billing | +$0.00 | Cloudflare | -| 2023-11-01 | AWS S3 Billing | -$0.28 | AWS | -| 2023-11-01 | Cloudflare R2 Billing | +$0.00 | Cloudflare | -| 2023-09-30 | AWS S3 Billing | -$0.28 | AWS | -| 2023-09-30 | Cloudflare R2 Billing | +$0.00 | Cloudflare | -| 2023-08-31 | AWS S3 Billing | -$0.28 | AWS | -| 2023-08-31 | Cloudflare R2 Billing | +$0.00 | Cloudflare | -| 2023-07-31 | AWS S3 Billing | -$0.28 | AWS | -| 2023-07-31 | Cloudflare R2 Billing | +$0.00 | Cloudflare | -| 2023-06-30 | AWS S3 Billing | -$0.28 | AWS | -| 2023-06-30 | Cloudflare R2 Billing | +$0.00 | Cloudflare | -| 2023-05-31 | AWS S3 Billing | -$0.28 | AWS | -| 2023-05-31 | Cloudflare R2 Billing | +$0.00 | Cloudflare | -| 2023-04-30 | AWS S3 Billing | -$0.28 | AWS | -| 2023-04-30 | Cloudflare R2 Billing | +$0.00 | Cloudflare | -| 2023-03-31 | AWS S3 Billing | -$0.28 | AWS | -| 2023-03-31 | Cloudflare R2 Billing | +$0.00 | Cloudflare | -| 2023-03-01 | AWS S3 Billing | -$0.28 | AWS | -| 2023-03-01 | Cloudflare R2 Billing | +$0.00 | Cloudflare | -| 2023-02-01 | AWS S3 Billing | -$0.28 | AWS | -| 2023-02-01 | Cloudflare R2 Billing | +$0.00 | Cloudflare | \ No newline at end of file +| 2025-02-12 | Hetzner Object Storage Billing (Prorated 12-28) | -3.64 | Hetzner | +| 2025-02-12 | Backblaze B2 Billing (Prorated 12-28) | 0.00 | Backblaze | +| 2025-02-10 | Hetzner Cloud Credits | +60.00 | Hetzner | +| 2025-02-01 | AWS S3 Billing (2025-01) | -0.28 | AWS | +| 2025-02-01 | Cloudflare R2 Billing (2025-01) | 0.00 | Cloudflare | +| 2025-01-01 | AWS S3 Billing (2024-12) | -0.28 | AWS | +| 2025-01-01 | Cloudflare R2 Billing (2024-12) | 0.00 | Cloudflare | +| 2024-12-01 | AWS S3 Billing (2024-11) | -0.28 | AWS | +| 2024-12-01 | Cloudflare R2 Billing (2024-11) | 0.00 | Cloudflare | +| 2024-11-01 | AWS S3 Billing (2024-09) | -0.28 | AWS | +| 2024-11-01 | Cloudflare R2 Billing (2024-09) | 0.00 | Cloudflare | +| 2024-09-30 | AWS S3 Billing (2024-08) | -0.28 | AWS | +| 2024-09-30 | Cloudflare R2 Billing (2024-08) | 0.00 | Cloudflare | +| 2024-08-31 | AWS S3 Billing (2024-07) | -0.28 | AWS | +| 2024-08-31 | Cloudflare R2 Billing (2024-07) | 0.00 | Cloudflare | +| 2024-07-31 | AWS S3 Billing (2024-06) | -0.28 | AWS | +| 2024-07-31 | Cloudflare R2 Billing (2024-06) | 0.00 | Cloudflare | +| 2024-06-30 | AWS S3 Billing (2024-05) | -0.28 | AWS | +| 2024-06-30 | Cloudflare R2 Billing (2024-05) | 0.00 | Cloudflare | +| 2024-05-31 | AWS S3 Billing (2024-04) | -0.28 | AWS | +| 2024-05-31 | Cloudflare R2 Billing (2024-04) | 0.00 | Cloudflare | +| 2024-04-30 | AWS S3 Billing (2024-03) | -0.28 | AWS | +| 2024-04-30 | Cloudflare R2 Billing (2024-03) | 0.00 | Cloudflare | +| 2024-03-31 | AWS S3 Billing (2024-03) | -0.28 | AWS | +| 2024-03-31 | Cloudflare R2 Billing (2024-03) | 0.00 | Cloudflare | +| 2024-03-01 | AWS S3 Billing (2024-02) | -0.28 | AWS | +| 2024-03-01 | Cloudflare R2 Billing (2024-02) | 0.00 | Cloudflare | +| 2024-02-01 | AWS S3 Billing (2024-01) | -0.28 | AWS | +| 2024-02-01 | Cloudflare R2 Billing (2024-01) | 0.00 | Cloudflare | +| 2024-01-01 | AWS S3 Billing (2023-12) | -0.28 | AWS | +| 2024-01-01 | Cloudflare R2 Billing (2023-12) | 0.00 | Cloudflare | +| 2023-12-01 | AWS S3 Billing (2023-11) | -0.28 | AWS | +| 2023-12-01 | Cloudflare R2 Billing (2023-11) | 0.00 | Cloudflare | +| 2023-11-01 | AWS S3 Billing (2023-09) | -0.28 | AWS | +| 2023-11-01 | Cloudflare R2 Billing (2023-09) | 0.00 | Cloudflare | +| 2023-09-30 | AWS S3 Billing (2023-08) | -0.28 | AWS | +| 2023-09-30 | Cloudflare R2 Billing (2023-08) | 0.00 | Cloudflare | +| 2023-08-31 | AWS S3 Billing (2023-07) | -0.28 | AWS | +| 2023-08-31 | Cloudflare R2 Billing (2023-07) | 0.00 | Cloudflare | +| 2023-07-31 | AWS S3 Billing (2023-06) | -0.28 | AWS | +| 2023-07-31 | Cloudflare R2 Billing (2023-06) | 0.00 | Cloudflare | +| 2023-06-30 | AWS S3 Billing (2023-05) | -0.28 | AWS | +| 2023-06-30 | Cloudflare R2 Billing (2023-05) | 0.00 | Cloudflare | +| 2023-05-31 | AWS S3 Billing (2023-04) | -0.28 | AWS | +| 2023-05-31 | Cloudflare R2 Billing (2023-04) | 0.00 | Cloudflare | +| 2023-04-30 | AWS S3 Billing (2023-03) | -0.28 | AWS | +| 2023-04-30 | Cloudflare R2 Billing (2023-03) | 0.00 | Cloudflare | +| 2023-03-31 | AWS S3 Billing (2023-03) | -0.28 | AWS | +| 2023-03-31 | Cloudflare R2 Billing (2023-03) | 0.00 | Cloudflare | +| 2023-03-01 | AWS S3 Billing (2023-02) | -0.28 | AWS | +| 2023-03-01 | Cloudflare R2 Billing (2023-02) | 0.00 | Cloudflare | +| 2023-02-01 | AWS S3 Billing (2023-01) | -0.28 | AWS | +| 2023-02-01 | Cloudflare R2 Billing (2023-01) | 0.00 | Cloudflare | \ No newline at end of file diff --git a/etc/bookkeeping.js b/etc/bookkeeping.js index 5810a9a..39de054 100644 --- a/etc/bookkeeping.js +++ b/etc/bookkeeping.js @@ -4,7 +4,17 @@ const formatDate = date => date.toISOString().split('T')[0] const formatAmount = (amount, isExpense = false) => { const value = Math.abs(amount).toFixed(2) - return isExpense ? `-$${value}` : `+$${value}` + if (amount === 0) return `${value}` + return isExpense ? `-${value}` : `+${value}` +} + +const calculateProration = (startDate, amount) => { + const date = new Date(startDate) + const year = date.getFullYear() + const month = date.getMonth() + const lastDayOfMonth = new Date(year, month + 1, 0).getDate() + const remainingDays = lastDayOfMonth - date.getDate() + 1 + return (amount * remainingDays) / lastDayOfMonth } const generateTransactions = (records, type = 'expense') => { @@ -22,15 +32,35 @@ const generateTransactions = (records, type = 'expense') => { }] } - let currentDate = new Date(date) + const startDate = new Date(date) + let currentDate = new Date(startDate) + + if (startDate.getDate() !== 1) { + const proratedAmount = calculateProration(startDate, amount) + transactions.push({ + date: new Date(startDate), + amount: isExpense ? -proratedAmount : proratedAmount, + desc: `${desc} (Prorated ${startDate.getDate()}-${new Date(startDate.getFullYear(), startDate.getMonth() + 1, 0).getDate()})`, + provider, + period + }) + + currentDate = new Date(startDate.getFullYear(), startDate.getMonth() + 1, 1) + } + while (currentDate <= now) { + const billingDate = new Date(currentDate) + const usagePeriod = new Date(currentDate) + usagePeriod.setMonth(usagePeriod.getMonth() - 1) + transactions.push({ - date: new Date(currentDate), + date: billingDate, amount: isExpense ? -amount : amount, - desc, + desc: `${desc} (${formatDate(usagePeriod).slice(0, 7)})`, provider, period }) + currentDate.setMonth(currentDate.getMonth() + 1) } @@ -111,4 +141,4 @@ const expenses = [ { provider: 'Backblaze', desc: 'Backblaze B2 Billing', amount: 0.0, date: new Date('2025-02-12'), period: 'monthly' }, ] -generateBookkeeping(income, expenses) +generateBookkeeping(income, expenses) \ No newline at end of file diff --git a/readme.md b/readme.md index ea228d6..1524fc1 100644 --- a/readme.md +++ b/readme.md @@ -62,14 +62,14 @@ For more information: Here's a quick API overview: ```js -await storage.read(file) -await storage.write(file, fileContents) -await storage.remove(fileOrDir) +await storage.read(file, options) +await storage.write(file, stringOrBuffer, options) +await storage.remove(fileOrDir, options) +await storage.list(dir, options) +await storage.copy(fileOrDir, fileOrDir) +await storage.presign(file, options) await storage.exists(fileOrDir) await storage.stat(file) -await storage.copy(fileOrDir, fileOrDir) -await storage.list(dir) -await storage.presign(file) ``` See [StorageInterface](src/storage/docs/StorageInterface.md) for more information. From 8ca5b381be265595e8a4d868b5ae215ad86e9360 Mon Sep 17 00:00:00 2001 From: brihter Date: Sun, 16 Feb 2025 16:58:18 +0100 Subject: [PATCH 2/7] is more fix --- bookkeeping.md | 121 ++++++++++++++++++++++----------------------- etc/bookkeeping.js | 66 +++++++++++++------------ 2 files changed, 93 insertions(+), 94 deletions(-) diff --git a/bookkeeping.md b/bookkeeping.md index 9f7742e..9bcc44b 100644 --- a/bookkeeping.md +++ b/bookkeeping.md @@ -4,80 +4,75 @@ | Category | Amount | | --- | --- | -| Income | +60.00 | -| Expenses | -10.64 | -| Balance | +49.36 | +| Income | +$60.00 | +| Expenses | -$6.72 | +| Balance | +$53.28 | ## By Provider | By Provider | Total | | --- | --- | -| Hetzner | +56.36 | -| Cloudflare | 0.00 | -| Backblaze | 0.00 | -| AWS | -7.00 | +| Hetzner | +$60.00 | +| Cloudflare | $0.00 | +| AWS | -$6.72 | ## By Year | By Year | Total | | --- | --- | -| 2025 | +55.80 | -| 2024 | -3.36 | -| 2023 | -3.08 | +| 2025 | +$59.72 | +| 2024 | -$3.36 | +| 2023 | -$3.08 | ## Transactions | Date | Description | Amount | Provider | | --- | --- | --- | --- | -| 2025-02-12 | Hetzner Object Storage Billing (Prorated 12-28) | -3.64 | Hetzner | -| 2025-02-12 | Backblaze B2 Billing (Prorated 12-28) | 0.00 | Backblaze | -| 2025-02-10 | Hetzner Cloud Credits | +60.00 | Hetzner | -| 2025-02-01 | AWS S3 Billing (2025-01) | -0.28 | AWS | -| 2025-02-01 | Cloudflare R2 Billing (2025-01) | 0.00 | Cloudflare | -| 2025-01-01 | AWS S3 Billing (2024-12) | -0.28 | AWS | -| 2025-01-01 | Cloudflare R2 Billing (2024-12) | 0.00 | Cloudflare | -| 2024-12-01 | AWS S3 Billing (2024-11) | -0.28 | AWS | -| 2024-12-01 | Cloudflare R2 Billing (2024-11) | 0.00 | Cloudflare | -| 2024-11-01 | AWS S3 Billing (2024-09) | -0.28 | AWS | -| 2024-11-01 | Cloudflare R2 Billing (2024-09) | 0.00 | Cloudflare | -| 2024-09-30 | AWS S3 Billing (2024-08) | -0.28 | AWS | -| 2024-09-30 | Cloudflare R2 Billing (2024-08) | 0.00 | Cloudflare | -| 2024-08-31 | AWS S3 Billing (2024-07) | -0.28 | AWS | -| 2024-08-31 | Cloudflare R2 Billing (2024-07) | 0.00 | Cloudflare | -| 2024-07-31 | AWS S3 Billing (2024-06) | -0.28 | AWS | -| 2024-07-31 | Cloudflare R2 Billing (2024-06) | 0.00 | Cloudflare | -| 2024-06-30 | AWS S3 Billing (2024-05) | -0.28 | AWS | -| 2024-06-30 | Cloudflare R2 Billing (2024-05) | 0.00 | Cloudflare | -| 2024-05-31 | AWS S3 Billing (2024-04) | -0.28 | AWS | -| 2024-05-31 | Cloudflare R2 Billing (2024-04) | 0.00 | Cloudflare | -| 2024-04-30 | AWS S3 Billing (2024-03) | -0.28 | AWS | -| 2024-04-30 | Cloudflare R2 Billing (2024-03) | 0.00 | Cloudflare | -| 2024-03-31 | AWS S3 Billing (2024-03) | -0.28 | AWS | -| 2024-03-31 | Cloudflare R2 Billing (2024-03) | 0.00 | Cloudflare | -| 2024-03-01 | AWS S3 Billing (2024-02) | -0.28 | AWS | -| 2024-03-01 | Cloudflare R2 Billing (2024-02) | 0.00 | Cloudflare | -| 2024-02-01 | AWS S3 Billing (2024-01) | -0.28 | AWS | -| 2024-02-01 | Cloudflare R2 Billing (2024-01) | 0.00 | Cloudflare | -| 2024-01-01 | AWS S3 Billing (2023-12) | -0.28 | AWS | -| 2024-01-01 | Cloudflare R2 Billing (2023-12) | 0.00 | Cloudflare | -| 2023-12-01 | AWS S3 Billing (2023-11) | -0.28 | AWS | -| 2023-12-01 | Cloudflare R2 Billing (2023-11) | 0.00 | Cloudflare | -| 2023-11-01 | AWS S3 Billing (2023-09) | -0.28 | AWS | -| 2023-11-01 | Cloudflare R2 Billing (2023-09) | 0.00 | Cloudflare | -| 2023-09-30 | AWS S3 Billing (2023-08) | -0.28 | AWS | -| 2023-09-30 | Cloudflare R2 Billing (2023-08) | 0.00 | Cloudflare | -| 2023-08-31 | AWS S3 Billing (2023-07) | -0.28 | AWS | -| 2023-08-31 | Cloudflare R2 Billing (2023-07) | 0.00 | Cloudflare | -| 2023-07-31 | AWS S3 Billing (2023-06) | -0.28 | AWS | -| 2023-07-31 | Cloudflare R2 Billing (2023-06) | 0.00 | Cloudflare | -| 2023-06-30 | AWS S3 Billing (2023-05) | -0.28 | AWS | -| 2023-06-30 | Cloudflare R2 Billing (2023-05) | 0.00 | Cloudflare | -| 2023-05-31 | AWS S3 Billing (2023-04) | -0.28 | AWS | -| 2023-05-31 | Cloudflare R2 Billing (2023-04) | 0.00 | Cloudflare | -| 2023-04-30 | AWS S3 Billing (2023-03) | -0.28 | AWS | -| 2023-04-30 | Cloudflare R2 Billing (2023-03) | 0.00 | Cloudflare | -| 2023-03-31 | AWS S3 Billing (2023-03) | -0.28 | AWS | -| 2023-03-31 | Cloudflare R2 Billing (2023-03) | 0.00 | Cloudflare | -| 2023-03-01 | AWS S3 Billing (2023-02) | -0.28 | AWS | -| 2023-03-01 | Cloudflare R2 Billing (2023-02) | 0.00 | Cloudflare | -| 2023-02-01 | AWS S3 Billing (2023-01) | -0.28 | AWS | -| 2023-02-01 | Cloudflare R2 Billing (2023-01) | 0.00 | Cloudflare | \ No newline at end of file +| 2025-02-10 | Hetzner Cloud Credits | +$60.00 | Hetzner | +| 2025-01-31 | AWS S3 Billing (2024-12) | -$0.28 | AWS | +| 2025-01-31 | Cloudflare R2 Billing (2024-12) | $0.00 | Cloudflare | +| 2024-12-31 | AWS S3 Billing (2024-11) | -$0.28 | AWS | +| 2024-12-31 | Cloudflare R2 Billing (2024-11) | $0.00 | Cloudflare | +| 2024-11-30 | AWS S3 Billing (2024-10) | -$0.28 | AWS | +| 2024-11-30 | Cloudflare R2 Billing (2024-10) | $0.00 | Cloudflare | +| 2024-10-31 | AWS S3 Billing (2024-09) | -$0.28 | AWS | +| 2024-10-31 | Cloudflare R2 Billing (2024-09) | $0.00 | Cloudflare | +| 2024-09-30 | AWS S3 Billing (2024-08) | -$0.28 | AWS | +| 2024-09-30 | Cloudflare R2 Billing (2024-08) | $0.00 | Cloudflare | +| 2024-08-31 | AWS S3 Billing (2024-07) | -$0.28 | AWS | +| 2024-08-31 | Cloudflare R2 Billing (2024-07) | $0.00 | Cloudflare | +| 2024-07-31 | AWS S3 Billing (2024-06) | -$0.28 | AWS | +| 2024-07-31 | Cloudflare R2 Billing (2024-06) | $0.00 | Cloudflare | +| 2024-06-30 | AWS S3 Billing (2024-05) | -$0.28 | AWS | +| 2024-06-30 | Cloudflare R2 Billing (2024-05) | $0.00 | Cloudflare | +| 2024-05-31 | AWS S3 Billing (2024-04) | -$0.28 | AWS | +| 2024-05-31 | Cloudflare R2 Billing (2024-04) | $0.00 | Cloudflare | +| 2024-04-30 | AWS S3 Billing (2024-03) | -$0.28 | AWS | +| 2024-04-30 | Cloudflare R2 Billing (2024-03) | $0.00 | Cloudflare | +| 2024-03-31 | AWS S3 Billing (2024-02) | -$0.28 | AWS | +| 2024-03-31 | Cloudflare R2 Billing (2024-02) | $0.00 | Cloudflare | +| 2024-02-29 | AWS S3 Billing (2024-01) | -$0.28 | AWS | +| 2024-02-29 | Cloudflare R2 Billing (2024-01) | $0.00 | Cloudflare | +| 2024-01-31 | AWS S3 Billing (2023-12) | -$0.28 | AWS | +| 2024-01-31 | Cloudflare R2 Billing (2023-12) | $0.00 | Cloudflare | +| 2023-12-31 | AWS S3 Billing (2023-11) | -$0.28 | AWS | +| 2023-12-31 | Cloudflare R2 Billing (2023-11) | $0.00 | Cloudflare | +| 2023-11-30 | AWS S3 Billing (2023-10) | -$0.28 | AWS | +| 2023-11-30 | Cloudflare R2 Billing (2023-10) | $0.00 | Cloudflare | +| 2023-10-31 | AWS S3 Billing (2023-09) | -$0.28 | AWS | +| 2023-10-31 | Cloudflare R2 Billing (2023-09) | $0.00 | Cloudflare | +| 2023-09-30 | AWS S3 Billing (2023-08) | -$0.28 | AWS | +| 2023-09-30 | Cloudflare R2 Billing (2023-08) | $0.00 | Cloudflare | +| 2023-08-31 | AWS S3 Billing (2023-07) | -$0.28 | AWS | +| 2023-08-31 | Cloudflare R2 Billing (2023-07) | $0.00 | Cloudflare | +| 2023-07-31 | AWS S3 Billing (2023-06) | -$0.28 | AWS | +| 2023-07-31 | Cloudflare R2 Billing (2023-06) | $0.00 | Cloudflare | +| 2023-06-30 | AWS S3 Billing (2023-05) | -$0.28 | AWS | +| 2023-06-30 | Cloudflare R2 Billing (2023-05) | $0.00 | Cloudflare | +| 2023-05-31 | AWS S3 Billing (2023-04) | -$0.28 | AWS | +| 2023-05-31 | Cloudflare R2 Billing (2023-04) | $0.00 | Cloudflare | +| 2023-04-30 | AWS S3 Billing (2023-03) | -$0.28 | AWS | +| 2023-04-30 | Cloudflare R2 Billing (2023-03) | $0.00 | Cloudflare | +| 2023-03-31 | AWS S3 Billing (2023-02) | -$0.28 | AWS | +| 2023-03-31 | Cloudflare R2 Billing (2023-02) | $0.00 | Cloudflare | +| 2023-02-28 | AWS S3 Billing (2023-01) | -$0.28 | AWS | +| 2023-02-28 | Cloudflare R2 Billing (2023-01) | $0.00 | Cloudflare | \ No newline at end of file diff --git a/etc/bookkeeping.js b/etc/bookkeeping.js index 39de054..8ae975e 100644 --- a/etc/bookkeeping.js +++ b/etc/bookkeeping.js @@ -4,8 +4,8 @@ const formatDate = date => date.toISOString().split('T')[0] const formatAmount = (amount, isExpense = false) => { const value = Math.abs(amount).toFixed(2) - if (amount === 0) return `${value}` - return isExpense ? `-${value}` : `+${value}` + if (amount === 0) return `$${value}` + return isExpense ? `-$${value}` : `+$${value}` } const calculateProration = (startDate, amount) => { @@ -20,11 +20,11 @@ const calculateProration = (startDate, amount) => { const generateTransactions = (records, type = 'expense') => { const now = new Date() const isExpense = type === 'expense' - + return records.flatMap(record => { const transactions = [] const { date, period, amount, desc, provider } = record - + if (period === 'once') { return [{ ...record, @@ -34,36 +34,40 @@ const generateTransactions = (records, type = 'expense') => { const startDate = new Date(date) let currentDate = new Date(startDate) - + if (startDate.getDate() !== 1) { - const proratedAmount = calculateProration(startDate, amount) - transactions.push({ - date: new Date(startDate), - amount: isExpense ? -proratedAmount : proratedAmount, - desc: `${desc} (Prorated ${startDate.getDate()}-${new Date(startDate.getFullYear(), startDate.getMonth() + 1, 0).getDate()})`, - provider, - period - }) - - currentDate = new Date(startDate.getFullYear(), startDate.getMonth() + 1, 1) + const firstOfNextMonth = new Date(startDate.getFullYear(), startDate.getMonth() + 1, 1); + + if (firstOfNextMonth <= now) { + const proratedAmount = calculateProration(startDate, amount) + transactions.push({ + date: firstOfNextMonth, + amount: isExpense ? -proratedAmount : proratedAmount, + desc: `${desc} (Prorated ${formatDate(startDate).slice(0, 7)})`, + provider, + period + }) + } + currentDate = new Date(startDate.getFullYear(), startDate.getMonth() + 2, 1) + } else { + currentDate = new Date(startDate.getFullYear(), startDate.getMonth() + 1, 1); } - while (currentDate <= now) { - const billingDate = new Date(currentDate) - const usagePeriod = new Date(currentDate) - usagePeriod.setMonth(usagePeriod.getMonth() - 1) - - transactions.push({ - date: billingDate, - amount: isExpense ? -amount : amount, - desc: `${desc} (${formatDate(usagePeriod).slice(0, 7)})`, - provider, - period - }) - - currentDate.setMonth(currentDate.getMonth() + 1) - } - + while (currentDate <= now) { + const billingDate = new Date(currentDate); + const usagePeriod = new Date(currentDate); + usagePeriod.setMonth(usagePeriod.getMonth() - 1); + transactions.push({ + date: billingDate, + amount: isExpense ? -amount : amount, + desc: `${desc} (${formatDate(usagePeriod).slice(0, 7)})`, + provider, + period + }); + + currentDate.setMonth(currentDate.getMonth() + 1); + } + return transactions }) } From d016d7a6c04559659477f3faed28525242d44586 Mon Sep 17 00:00:00 2001 From: brihter Date: Sun, 16 Feb 2025 16:59:53 +0100 Subject: [PATCH 3/7] styling --- bookkeeping.md | 10 ++++---- etc/bookkeeping.js | 58 +++++++++++++++++++++------------------------- 2 files changed, 32 insertions(+), 36 deletions(-) diff --git a/bookkeeping.md b/bookkeeping.md index 9bcc44b..9dc8962 100644 --- a/bookkeeping.md +++ b/bookkeeping.md @@ -4,15 +4,15 @@ | Category | Amount | | --- | --- | -| Income | +$60.00 | +| Income | $60.00 | | Expenses | -$6.72 | -| Balance | +$53.28 | +| Balance | $53.28 | ## By Provider | By Provider | Total | | --- | --- | -| Hetzner | +$60.00 | +| Hetzner | $60.00 | | Cloudflare | $0.00 | | AWS | -$6.72 | @@ -20,14 +20,14 @@ | By Year | Total | | --- | --- | -| 2025 | +$59.72 | +| 2025 | $59.72 | | 2024 | -$3.36 | | 2023 | -$3.08 | ## Transactions | Date | Description | Amount | Provider | | --- | --- | --- | --- | -| 2025-02-10 | Hetzner Cloud Credits | +$60.00 | Hetzner | +| 2025-02-10 | Hetzner Cloud Credits | $60.00 | Hetzner | | 2025-01-31 | AWS S3 Billing (2024-12) | -$0.28 | AWS | | 2025-01-31 | Cloudflare R2 Billing (2024-12) | $0.00 | Cloudflare | | 2024-12-31 | AWS S3 Billing (2024-11) | -$0.28 | AWS | diff --git a/etc/bookkeeping.js b/etc/bookkeeping.js index 8ae975e..ef96fc0 100644 --- a/etc/bookkeeping.js +++ b/etc/bookkeeping.js @@ -4,17 +4,13 @@ const formatDate = date => date.toISOString().split('T')[0] const formatAmount = (amount, isExpense = false) => { const value = Math.abs(amount).toFixed(2) - if (amount === 0) return `$${value}` - return isExpense ? `-$${value}` : `+$${value}` + return isExpense ? `-$${value}` : `$${value}` } const calculateProration = (startDate, amount) => { const date = new Date(startDate) - const year = date.getFullYear() - const month = date.getMonth() - const lastDayOfMonth = new Date(year, month + 1, 0).getDate() - const remainingDays = lastDayOfMonth - date.getDate() + 1 - return (amount * remainingDays) / lastDayOfMonth + const lastDayOfMonth = new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate() + return (amount * (lastDayOfMonth - date.getDate() + 1)) / lastDayOfMonth } const generateTransactions = (records, type = 'expense') => { @@ -22,13 +18,13 @@ const generateTransactions = (records, type = 'expense') => { const isExpense = type === 'expense' return records.flatMap(record => { - const transactions = [] const { date, period, amount, desc, provider } = record + const transactions = [] if (period === 'once') { return [{ ...record, - amount: isExpense ? -amount : amount + amount: isExpense ? -amount : amount, }] } @@ -36,7 +32,7 @@ const generateTransactions = (records, type = 'expense') => { let currentDate = new Date(startDate) if (startDate.getDate() !== 1) { - const firstOfNextMonth = new Date(startDate.getFullYear(), startDate.getMonth() + 1, 1); + const firstOfNextMonth = new Date(startDate.getFullYear(), startDate.getMonth() + 1, 1) if (firstOfNextMonth <= now) { const proratedAmount = calculateProration(startDate, amount) @@ -45,28 +41,28 @@ const generateTransactions = (records, type = 'expense') => { amount: isExpense ? -proratedAmount : proratedAmount, desc: `${desc} (Prorated ${formatDate(startDate).slice(0, 7)})`, provider, - period + period, }) } currentDate = new Date(startDate.getFullYear(), startDate.getMonth() + 2, 1) } else { - currentDate = new Date(startDate.getFullYear(), startDate.getMonth() + 1, 1); + currentDate = new Date(startDate.getFullYear(), startDate.getMonth() + 1, 1) } - while (currentDate <= now) { - const billingDate = new Date(currentDate); - const usagePeriod = new Date(currentDate); - usagePeriod.setMonth(usagePeriod.getMonth() - 1); - transactions.push({ - date: billingDate, - amount: isExpense ? -amount : amount, - desc: `${desc} (${formatDate(usagePeriod).slice(0, 7)})`, - provider, - period - }); - - currentDate.setMonth(currentDate.getMonth() + 1); - } + while (currentDate <= now) { + const billingDate = new Date(currentDate) + const usagePeriod = new Date(currentDate) + usagePeriod.setMonth(usagePeriod.getMonth() - 1) + transactions.push({ + date: billingDate, + amount: isExpense ? -amount : amount, + desc: `${desc} (${formatDate(usagePeriod).slice(0, 7)})`, + provider, + period, + }) + + currentDate.setMonth(currentDate.getMonth() + 1) + } return transactions }) @@ -97,7 +93,7 @@ const generateBreakdown = (transactions, groupingKey, title) => { .sort(([a], [b]) => b.localeCompare(a)) .map(([key, items]) => ({ key, - total: calculateTotal(items) + total: calculateTotal(items), })) const header = `\n## ${title}\n\n| ${title} | Total |\n| --- | --- |` @@ -110,7 +106,7 @@ const generateBreakdown = (transactions, groupingKey, title) => { const generateBookkeeping = async (income, expenses) => { const allTransactions = [ ...generateTransactions(income, 'income'), - ...generateTransactions(expenses, 'expense') + ...generateTransactions(expenses, 'expense'), ] const totalIncome = calculateTotal(allTransactions.filter(t => t.amount > 0)) @@ -128,14 +124,14 @@ const generateBookkeeping = async (income, expenses) => { generateBreakdown(allTransactions, 'provider', 'By Provider'), generateBreakdown(allTransactions, item => formatDate(item.date).slice(0, 4), 'By Year'), '\n## Transactions', - generateTransactionTable(allTransactions) + generateTransactionTable(allTransactions), ].join('\n') await fs.writeFile('./bookkeeping.md', content, 'utf-8') } const income = [ - { provider: 'Hetzner', desc: 'Hetzner Cloud Credits', amount: 60, date: new Date('2025-02-10'), period: 'once' } + { provider: 'Hetzner', desc: 'Hetzner Cloud Credits', amount: 60, date: new Date('2025-02-10'), period: 'once' }, ] const expenses = [ @@ -145,4 +141,4 @@ const expenses = [ { provider: 'Backblaze', desc: 'Backblaze B2 Billing', amount: 0.0, date: new Date('2025-02-12'), period: 'monthly' }, ] -generateBookkeeping(income, expenses) \ No newline at end of file +generateBookkeeping(income, expenses) From 1499fd0b9f74585ac7af35f8fa371f53a7011816 Mon Sep 17 00:00:00 2001 From: brihter Date: Sun, 16 Feb 2025 17:05:55 +0100 Subject: [PATCH 4/7] projections start --- bookkeeping.md | 19 +++++++++++ etc/bookkeeping.js | 81 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/bookkeeping.md b/bookkeeping.md index 9dc8962..6501adc 100644 --- a/bookkeeping.md +++ b/bookkeeping.md @@ -7,6 +7,25 @@ | Income | $60.00 | | Expenses | -$6.72 | | Balance | $53.28 | +| Monthly Income (Projected) | $5.00 | +| Monthly Expenses (Projected) | -$6.89 | + +## Runway Projection + +| Month | Starting Balance | Income | Expenses | Net Change | Ending Balance | +| --- | --- | --- | --- | --- | --- | +| 2025-01 | $53.28 | $5.00 | -$6.89 | $1.89 | $51.39 | +| 2025-02 | $51.39 | $5.00 | -$6.89 | $1.89 | $49.50 | +| 2025-03 | $49.50 | $5.00 | -$6.89 | $1.89 | $47.61 | +| 2025-04 | $47.61 | $5.00 | -$6.89 | $1.89 | $45.72 | +| 2025-05 | $45.72 | $5.00 | -$6.89 | $1.89 | $43.83 | +| 2025-06 | $43.83 | $5.00 | -$6.89 | $1.89 | $41.93 | +| 2025-07 | $41.93 | $5.00 | -$6.89 | $1.89 | $40.04 | +| 2025-08 | $40.04 | $5.00 | -$6.89 | $1.89 | $38.15 | +| 2025-09 | $38.15 | $5.00 | -$6.89 | $1.89 | $36.26 | +| 2025-10 | $36.26 | $5.00 | -$6.89 | $1.89 | $34.37 | +| 2025-11 | $34.37 | $5.00 | -$6.89 | $1.89 | $32.48 | +| 2025-12 | $32.48 | $5.00 | -$6.89 | $1.89 | $30.59 | ## By Provider diff --git a/etc/bookkeeping.js b/etc/bookkeeping.js index ef96fc0..06a814a 100644 --- a/etc/bookkeeping.js +++ b/etc/bookkeeping.js @@ -68,6 +68,38 @@ const generateTransactions = (records, type = 'expense') => { }) } +const generateFutureTransactions = (records, type = 'expense', untilDate) => { + const isExpense = type === 'expense' + + return records.flatMap(record => { + const { date, period, amount, desc, provider } = record + if (period === 'once') return [] + + const startDate = new Date(date) + let currentDate = new Date(startDate) + const transactions = [] + if (startDate.getDate() !== 1) { + currentDate = new Date(startDate.getFullYear(), startDate.getMonth() + 2, 1) + } else { + currentDate = new Date(startDate.getFullYear(), startDate.getMonth() + 1, 1) + } + + while (currentDate <= untilDate) { + const usagePeriod = new Date(currentDate) + usagePeriod.setMonth(usagePeriod.getMonth() - 1) + transactions.push({ + date: new Date(currentDate), + amount: isExpense ? -amount : amount, + desc: `${desc} (${formatDate(usagePeriod).slice(0, 7)})`, + provider, + period + }) + currentDate.setMonth(currentDate.getMonth() + 1) + } + return transactions + }) +} + const generateTransactionTable = transactions => { const header = '| Date | Description | Amount | Provider |\n| --- | --- | --- | --- |' const rows = transactions @@ -102,6 +134,38 @@ const generateBreakdown = (transactions, groupingKey, title) => { ) return [header, ...rows].join('\n') } +const projectRunwayTable = (initialBalance, monthlyExpenses, monthlyIncome, projectionMonths = 12) => { + if (monthlyIncome >= Math.abs(monthlyExpenses)) { + return "\n## Runway Projection\n\nThe project will not run out of money."; + } + + const netMonthlyChange = monthlyIncome + monthlyExpenses; + if (netMonthlyChange >= 0) { + return "\n## Runway Projection\n\nThe project will not run out of money."; + } + let currentBalance = initialBalance; + const header = "\n## Runway Projection\n\n| Month | Starting Balance | Income | Expenses | Net Change | Ending Balance |\n| --- | --- | --- | --- | --- | --- |"; + const rows = []; + + const currentDate = new Date(); + + for (let i = 0; i < projectionMonths; i++) { + const monthDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + i, 1); + const monthStr = formatDate(monthDate).slice(0, 7); + const startingBalance = currentBalance; + const netChange = netMonthlyChange; + currentBalance += netChange; + const endingBalance = currentBalance + + rows.push(`| ${monthStr} | ${formatAmount(startingBalance)} | ${formatAmount(monthlyIncome)} | ${formatAmount(monthlyExpenses, true)} | ${formatAmount(netChange)} | ${formatAmount(endingBalance, endingBalance<0)} |`); + + if (currentBalance < 0) { + break; // Stop projecting once balance is negative + } + + } + return [header, ...rows].join('\n'); +}; const generateBookkeeping = async (income, expenses) => { const allTransactions = [ @@ -112,6 +176,18 @@ const generateBookkeeping = async (income, expenses) => { const totalIncome = calculateTotal(allTransactions.filter(t => t.amount > 0)) const totalExpenses = calculateTotal(allTransactions.filter(t => t.amount < 0)) const balance = totalIncome + totalExpenses + const today = new Date() + const oneYearFromNow = new Date(today.getFullYear() + 1, today.getMonth(), today.getDate()) + const futureIncomeTransactions = generateFutureTransactions(income, 'income', oneYearFromNow) + const futureExpenseTransactions = generateFutureTransactions(expenses, 'expense', oneYearFromNow) + + const totalFutureIncome = calculateTotal(futureIncomeTransactions) + const totalFutureExpenses = calculateTotal(futureExpenseTransactions) + + const monthlyIncome = (totalIncome + totalFutureIncome) / 12 || 0 + const monthlyExpenses = (totalExpenses + totalFutureExpenses) / 12 || 0 + + const runwayProjectionTable = projectRunwayTable(balance, monthlyExpenses, monthlyIncome) const content = [ '# Bookkeeping', @@ -121,6 +197,9 @@ const generateBookkeeping = async (income, expenses) => { `| Income | ${formatAmount(totalIncome)} |`, `| Expenses | ${formatAmount(totalExpenses, true)} |`, `| Balance | ${formatAmount(balance, balance < 0)} |`, + `| Monthly Income (Projected) | ${formatAmount(monthlyIncome)} |`, + `| Monthly Expenses (Projected) | ${formatAmount(monthlyExpenses, true)} |`, + runwayProjectionTable, generateBreakdown(allTransactions, 'provider', 'By Provider'), generateBreakdown(allTransactions, item => formatDate(item.date).slice(0, 4), 'By Year'), '\n## Transactions', @@ -141,4 +220,4 @@ const expenses = [ { provider: 'Backblaze', desc: 'Backblaze B2 Billing', amount: 0.0, date: new Date('2025-02-12'), period: 'monthly' }, ] -generateBookkeeping(income, expenses) +generateBookkeeping(income, expenses) \ No newline at end of file From b3b39ec79ac83dc3ec0d5641d801b4516529e74c Mon Sep 17 00:00:00 2001 From: brihter Date: Sun, 16 Feb 2025 18:29:09 +0100 Subject: [PATCH 5/7] fix --- bookkeeping.md | 156 +++++++++-------- etc/bookkeeping.js | 412 ++++++++++++++++++++++++--------------------- 2 files changed, 295 insertions(+), 273 deletions(-) diff --git a/bookkeeping.md b/bookkeeping.md index 6501adc..3db6b07 100644 --- a/bookkeeping.md +++ b/bookkeeping.md @@ -2,96 +2,94 @@ ## Overview -| Category | Amount | -| --- | --- | -| Income | $60.00 | -| Expenses | -$6.72 | -| Balance | $53.28 | -| Monthly Income (Projected) | $5.00 | -| Monthly Expenses (Projected) | -$6.89 | +| income | expenses | balance | +| --- | --- | --- | +| +$60.00 | -$6.72 | +$53.28 | -## Runway Projection +## Projection (Next 12 Months) -| Month | Starting Balance | Income | Expenses | Net Change | Ending Balance | +| month | starting balance | income | expenses | net change | ending balance | | --- | --- | --- | --- | --- | --- | -| 2025-01 | $53.28 | $5.00 | -$6.89 | $1.89 | $51.39 | -| 2025-02 | $51.39 | $5.00 | -$6.89 | $1.89 | $49.50 | -| 2025-03 | $49.50 | $5.00 | -$6.89 | $1.89 | $47.61 | -| 2025-04 | $47.61 | $5.00 | -$6.89 | $1.89 | $45.72 | -| 2025-05 | $45.72 | $5.00 | -$6.89 | $1.89 | $43.83 | -| 2025-06 | $43.83 | $5.00 | -$6.89 | $1.89 | $41.93 | -| 2025-07 | $41.93 | $5.00 | -$6.89 | $1.89 | $40.04 | -| 2025-08 | $40.04 | $5.00 | -$6.89 | $1.89 | $38.15 | -| 2025-09 | $38.15 | $5.00 | -$6.89 | $1.89 | $36.26 | -| 2025-10 | $36.26 | $5.00 | -$6.89 | $1.89 | $34.37 | -| 2025-11 | $34.37 | $5.00 | -$6.89 | $1.89 | $32.48 | -| 2025-12 | $32.48 | $5.00 | -$6.89 | $1.89 | $30.59 | +| 2025-03 | +$53.28 | $0.00 | -$0.28 | -$0.28 | +$53.00 | +| 2025-04 | +$53.00 | $0.00 | -$6.27 | -$6.27 | +$46.73 | +| 2025-05 | +$46.73 | $0.00 | -$6.27 | -$6.27 | +$40.46 | +| 2025-06 | +$40.46 | $0.00 | -$6.27 | -$6.27 | +$34.19 | +| 2025-07 | +$34.19 | $0.00 | -$6.27 | -$6.27 | +$27.92 | +| 2025-08 | +$27.92 | $0.00 | -$6.27 | -$6.27 | +$21.65 | +| 2025-09 | +$21.65 | $0.00 | -$6.27 | -$6.27 | +$15.38 | +| 2025-10 | +$15.38 | $0.00 | -$6.27 | -$6.27 | +$9.11 | +| 2025-11 | +$9.11 | $0.00 | -$6.27 | -$6.27 | +$2.84 | +| 2025-12 | +$2.84 | $0.00 | -$6.27 | -$6.27 | -$3.43 | +| 2026-01 | -$3.43 | $0.00 | -$6.27 | -$6.27 | -$9.70 | +| 2026-02 | -$9.70 | $0.00 | -$6.27 | -$6.27 | -$15.97 | -## By Provider +## Breakdown by Provider -| By Provider | Total | +| provider | total | | --- | --- | -| Hetzner | $60.00 | -| Cloudflare | $0.00 | +| Hetzner | +$60.00 | | AWS | -$6.72 | +| Cloudflare | $0.00 | +| Backblaze | $0.00 | -## By Year +## Breakdown by Year -| By Year | Total | +| year | total | | --- | --- | -| 2025 | $59.72 | +| 2023 | -$2.80 | | 2024 | -$3.36 | -| 2023 | -$3.08 | +| 2025 | +$59.44 | ## Transactions -| Date | Description | Amount | Provider | + +| date | description | amount | provider | | --- | --- | --- | --- | -| 2025-02-10 | Hetzner Cloud Credits | $60.00 | Hetzner | -| 2025-01-31 | AWS S3 Billing (2024-12) | -$0.28 | AWS | -| 2025-01-31 | Cloudflare R2 Billing (2024-12) | $0.00 | Cloudflare | -| 2024-12-31 | AWS S3 Billing (2024-11) | -$0.28 | AWS | -| 2024-12-31 | Cloudflare R2 Billing (2024-11) | $0.00 | Cloudflare | -| 2024-11-30 | AWS S3 Billing (2024-10) | -$0.28 | AWS | -| 2024-11-30 | Cloudflare R2 Billing (2024-10) | $0.00 | Cloudflare | -| 2024-10-31 | AWS S3 Billing (2024-09) | -$0.28 | AWS | -| 2024-10-31 | Cloudflare R2 Billing (2024-09) | $0.00 | Cloudflare | -| 2024-09-30 | AWS S3 Billing (2024-08) | -$0.28 | AWS | -| 2024-09-30 | Cloudflare R2 Billing (2024-08) | $0.00 | Cloudflare | -| 2024-08-31 | AWS S3 Billing (2024-07) | -$0.28 | AWS | -| 2024-08-31 | Cloudflare R2 Billing (2024-07) | $0.00 | Cloudflare | -| 2024-07-31 | AWS S3 Billing (2024-06) | -$0.28 | AWS | -| 2024-07-31 | Cloudflare R2 Billing (2024-06) | $0.00 | Cloudflare | -| 2024-06-30 | AWS S3 Billing (2024-05) | -$0.28 | AWS | -| 2024-06-30 | Cloudflare R2 Billing (2024-05) | $0.00 | Cloudflare | -| 2024-05-31 | AWS S3 Billing (2024-04) | -$0.28 | AWS | -| 2024-05-31 | Cloudflare R2 Billing (2024-04) | $0.00 | Cloudflare | -| 2024-04-30 | AWS S3 Billing (2024-03) | -$0.28 | AWS | -| 2024-04-30 | Cloudflare R2 Billing (2024-03) | $0.00 | Cloudflare | -| 2024-03-31 | AWS S3 Billing (2024-02) | -$0.28 | AWS | -| 2024-03-31 | Cloudflare R2 Billing (2024-02) | $0.00 | Cloudflare | -| 2024-02-29 | AWS S3 Billing (2024-01) | -$0.28 | AWS | -| 2024-02-29 | Cloudflare R2 Billing (2024-01) | $0.00 | Cloudflare | -| 2024-01-31 | AWS S3 Billing (2023-12) | -$0.28 | AWS | -| 2024-01-31 | Cloudflare R2 Billing (2023-12) | $0.00 | Cloudflare | -| 2023-12-31 | AWS S3 Billing (2023-11) | -$0.28 | AWS | -| 2023-12-31 | Cloudflare R2 Billing (2023-11) | $0.00 | Cloudflare | -| 2023-11-30 | AWS S3 Billing (2023-10) | -$0.28 | AWS | -| 2023-11-30 | Cloudflare R2 Billing (2023-10) | $0.00 | Cloudflare | -| 2023-10-31 | AWS S3 Billing (2023-09) | -$0.28 | AWS | -| 2023-10-31 | Cloudflare R2 Billing (2023-09) | $0.00 | Cloudflare | -| 2023-09-30 | AWS S3 Billing (2023-08) | -$0.28 | AWS | -| 2023-09-30 | Cloudflare R2 Billing (2023-08) | $0.00 | Cloudflare | -| 2023-08-31 | AWS S3 Billing (2023-07) | -$0.28 | AWS | -| 2023-08-31 | Cloudflare R2 Billing (2023-07) | $0.00 | Cloudflare | -| 2023-07-31 | AWS S3 Billing (2023-06) | -$0.28 | AWS | -| 2023-07-31 | Cloudflare R2 Billing (2023-06) | $0.00 | Cloudflare | -| 2023-06-30 | AWS S3 Billing (2023-05) | -$0.28 | AWS | -| 2023-06-30 | Cloudflare R2 Billing (2023-05) | $0.00 | Cloudflare | -| 2023-05-31 | AWS S3 Billing (2023-04) | -$0.28 | AWS | -| 2023-05-31 | Cloudflare R2 Billing (2023-04) | $0.00 | Cloudflare | -| 2023-04-30 | AWS S3 Billing (2023-03) | -$0.28 | AWS | -| 2023-04-30 | Cloudflare R2 Billing (2023-03) | $0.00 | Cloudflare | -| 2023-03-31 | AWS S3 Billing (2023-02) | -$0.28 | AWS | -| 2023-03-31 | Cloudflare R2 Billing (2023-02) | $0.00 | Cloudflare | -| 2023-02-28 | AWS S3 Billing (2023-01) | -$0.28 | AWS | -| 2023-02-28 | Cloudflare R2 Billing (2023-01) | $0.00 | Cloudflare | \ No newline at end of file +| 2025-02-10 | Hetzner Cloud Credits | +$60.00 | Hetzner | +| 2025-02-01 | AWS S3 Billing | -$0.28 | AWS | +| 2025-02-01 | Cloudflare R2 Billing | $0.00 | Cloudflare | +| 2025-01-01 | AWS S3 Billing | -$0.28 | AWS | +| 2025-01-01 | Cloudflare R2 Billing | $0.00 | Cloudflare | +| 2024-12-01 | AWS S3 Billing | -$0.28 | AWS | +| 2024-12-01 | Cloudflare R2 Billing | $0.00 | Cloudflare | +| 2024-11-01 | AWS S3 Billing | -$0.28 | AWS | +| 2024-11-01 | Cloudflare R2 Billing | $0.00 | Cloudflare | +| 2024-10-01 | AWS S3 Billing | -$0.28 | AWS | +| 2024-10-01 | Cloudflare R2 Billing | $0.00 | Cloudflare | +| 2024-09-01 | AWS S3 Billing | -$0.28 | AWS | +| 2024-09-01 | Cloudflare R2 Billing | $0.00 | Cloudflare | +| 2024-08-01 | AWS S3 Billing | -$0.28 | AWS | +| 2024-08-01 | Cloudflare R2 Billing | $0.00 | Cloudflare | +| 2024-07-01 | AWS S3 Billing | -$0.28 | AWS | +| 2024-07-01 | Cloudflare R2 Billing | $0.00 | Cloudflare | +| 2024-06-01 | AWS S3 Billing | -$0.28 | AWS | +| 2024-06-01 | Cloudflare R2 Billing | $0.00 | Cloudflare | +| 2024-05-01 | AWS S3 Billing | -$0.28 | AWS | +| 2024-05-01 | Cloudflare R2 Billing | $0.00 | Cloudflare | +| 2024-04-01 | AWS S3 Billing | -$0.28 | AWS | +| 2024-04-01 | Cloudflare R2 Billing | $0.00 | Cloudflare | +| 2024-03-01 | AWS S3 Billing | -$0.28 | AWS | +| 2024-03-01 | Cloudflare R2 Billing | $0.00 | Cloudflare | +| 2024-02-01 | AWS S3 Billing | -$0.28 | AWS | +| 2024-02-01 | Cloudflare R2 Billing | $0.00 | Cloudflare | +| 2024-01-01 | AWS S3 Billing | -$0.28 | AWS | +| 2024-01-01 | Cloudflare R2 Billing | $0.00 | Cloudflare | +| 2023-12-01 | AWS S3 Billing | -$0.28 | AWS | +| 2023-12-01 | Cloudflare R2 Billing | $0.00 | Cloudflare | +| 2023-11-01 | AWS S3 Billing | -$0.28 | AWS | +| 2023-11-01 | Cloudflare R2 Billing | $0.00 | Cloudflare | +| 2023-10-01 | AWS S3 Billing | -$0.28 | AWS | +| 2023-10-01 | Cloudflare R2 Billing | $0.00 | Cloudflare | +| 2023-09-01 | AWS S3 Billing | -$0.28 | AWS | +| 2023-09-01 | Cloudflare R2 Billing | $0.00 | Cloudflare | +| 2023-08-01 | AWS S3 Billing | -$0.28 | AWS | +| 2023-08-01 | Cloudflare R2 Billing | $0.00 | Cloudflare | +| 2023-07-01 | AWS S3 Billing | -$0.28 | AWS | +| 2023-07-01 | Cloudflare R2 Billing | $0.00 | Cloudflare | +| 2023-06-01 | AWS S3 Billing | -$0.28 | AWS | +| 2023-06-01 | Cloudflare R2 Billing | $0.00 | Cloudflare | +| 2023-05-01 | AWS S3 Billing | -$0.28 | AWS | +| 2023-05-01 | Cloudflare R2 Billing | $0.00 | Cloudflare | +| 2023-04-01 | AWS S3 Billing | -$0.28 | AWS | +| 2023-04-01 | Cloudflare R2 Billing | $0.00 | Cloudflare | +| 2023-03-01 | AWS S3 Billing | -$0.28 | AWS | +| 2023-03-01 | Cloudflare R2 Billing | $0.00 | Cloudflare | \ No newline at end of file diff --git a/etc/bookkeeping.js b/etc/bookkeeping.js index 06a814a..492db90 100644 --- a/etc/bookkeeping.js +++ b/etc/bookkeeping.js @@ -1,223 +1,247 @@ import fs from 'node:fs/promises' -const formatDate = date => date.toISOString().split('T')[0] - -const formatAmount = (amount, isExpense = false) => { - const value = Math.abs(amount).toFixed(2) - return isExpense ? `-$${value}` : `$${value}` -} - -const calculateProration = (startDate, amount) => { - const date = new Date(startDate) - const lastDayOfMonth = new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate() - return (amount * (lastDayOfMonth - date.getDate() + 1)) / lastDayOfMonth -} - -const generateTransactions = (records, type = 'expense') => { - const now = new Date() - const isExpense = type === 'expense' - - return records.flatMap(record => { - const { date, period, amount, desc, provider } = record - const transactions = [] - - if (period === 'once') { - return [{ - ...record, - amount: isExpense ? -amount : amount, - }] - } - - const startDate = new Date(date) - let currentDate = new Date(startDate) - - if (startDate.getDate() !== 1) { - const firstOfNextMonth = new Date(startDate.getFullYear(), startDate.getMonth() + 1, 1) - - if (firstOfNextMonth <= now) { - const proratedAmount = calculateProration(startDate, amount) +// finish the implementation +// the output should be a markdown document +// the document should be written like so await fs.writeFile('./bookkeeping.md', content, 'utf-8') + +// the document should include: +// - overview table (columns: income, expenses, balance) +// - projection table (columns: month, starting balance, income, expenses, net change, ending balance) +// - a breakdown by provider (a table with provider, total columns, all providers should be listed even if total is 0) +// - a breakdown by year (a table with year, total columns) +// - a table of transactions (columns: date, description, amount, provider) + +// details: +// - generate transactions first and use the list as the source of truth for the calculations +// - project for the next 12 months +// - projections can be negative (can start/end with negative balance) +// - bill all services on the 1st of every month +// - if a service is billed in the middle of the month, prorate the bill on the first of the next month +// - bill services for the usage of the previous month (e.g. if a service usage started on the 20th of March, bill those 11 days on the 1st of April) +// - Rendered transactions should only be up to the current month +// - Transactions hsould be sorted by date descending +// - All reports (apart from projections) should take into account transaction up until the current date +// - Projections should be from the next month + 11 months +// - starting balance of the projections (first projected month) should be the income - expenses up to this point +// - money spent should be visualized as negative amount, income should be visualized as a positive amount (e.g. there's an error in the breakdown by provider) +// - amounts should be rendered with a dollar (e.g. +$1.2, -$2.4, zero should without a prefix) + +// coding rules: +// - no comments, no semi-colons, use modern javascript +const generateBookkeeping = (income = [], expenses = []) => { + const now = new Date('2025-02-16') + const zeroPad = v => v.toString().padStart(2, '0') + const formatDate = d => `${d.getFullYear()}-${zeroPad(d.getMonth()+1)}-${zeroPad(d.getDate())}` + const daysInMonth = (y, m) => new Date(y, m+1, 0).getDate() + const toMoney = v => { + const sign = v > 0 ? '+' : v < 0 ? '-' : '' + return sign + ? `${sign}$${Math.abs(v).toFixed(2)}` + : `$${Math.abs(v).toFixed(2)}` + } + const getMonthStart = (y, m) => new Date(y, m, 1) + const addMonths = (d, n) => { + const nd = new Date(d.getTime()) + nd.setMonth(nd.getMonth()+n) + return nd + } + const nextMonthFirst = d => { + const nd = new Date(d.getFullYear(), d.getMonth(), 1) + nd.setMonth(nd.getMonth()+1) + return nd + } + + const transactions = [] + + income.forEach(i => { + if (i.period === 'once') { + if (i.date <= now) { transactions.push({ - date: firstOfNextMonth, - amount: isExpense ? -proratedAmount : proratedAmount, - desc: `${desc} (Prorated ${formatDate(startDate).slice(0, 7)})`, - provider, - period, + date: new Date(i.date), + desc: i.desc, + amount: i.amount, + provider: i.provider }) } - currentDate = new Date(startDate.getFullYear(), startDate.getMonth() + 2, 1) - } else { - currentDate = new Date(startDate.getFullYear(), startDate.getMonth() + 1, 1) } - - while (currentDate <= now) { - const billingDate = new Date(currentDate) - const usagePeriod = new Date(currentDate) - usagePeriod.setMonth(usagePeriod.getMonth() - 1) - transactions.push({ - date: billingDate, - amount: isExpense ? -amount : amount, - desc: `${desc} (${formatDate(usagePeriod).slice(0, 7)})`, - provider, - period, - }) - - currentDate.setMonth(currentDate.getMonth() + 1) + if (i.period === 'monthly') { + let usageStart = new Date(i.date) + let billDate = nextMonthFirst(usageStart) + while (billDate <= now) { + const y = usageStart.getFullYear() + const m = usageStart.getMonth() + const dim = daysInMonth(y, m) + const day = usageStart.getDate() + const fraction = day === 1 ? 1 : (dim - day + 1) / dim + const amt = i.amount * fraction + transactions.push({ + date: new Date(billDate), + desc: i.desc, + amount: +amt.toFixed(2), + provider: i.provider + }) + usageStart = getMonthStart(billDate.getFullYear(), billDate.getMonth()) + billDate = addMonths(billDate, 1) + } } - - return transactions }) -} - -const generateFutureTransactions = (records, type = 'expense', untilDate) => { - const isExpense = type === 'expense' - - return records.flatMap(record => { - const { date, period, amount, desc, provider } = record - if (period === 'once') return [] - const startDate = new Date(date) - let currentDate = new Date(startDate) - const transactions = [] - if (startDate.getDate() !== 1) { - currentDate = new Date(startDate.getFullYear(), startDate.getMonth() + 2, 1) - } else { - currentDate = new Date(startDate.getFullYear(), startDate.getMonth() + 1, 1) + expenses.forEach(e => { + if (e.period === 'once') { + if (e.date <= now) { + transactions.push({ + date: new Date(e.date), + desc: e.desc, + amount: -Math.abs(e.amount), + provider: e.provider + }) + } } - - while (currentDate <= untilDate) { - const usagePeriod = new Date(currentDate) - usagePeriod.setMonth(usagePeriod.getMonth() - 1) - transactions.push({ - date: new Date(currentDate), - amount: isExpense ? -amount : amount, - desc: `${desc} (${formatDate(usagePeriod).slice(0, 7)})`, - provider, - period - }) - currentDate.setMonth(currentDate.getMonth() + 1) + if (e.period === 'monthly') { + let usageStart = new Date(e.date) + let billDate = nextMonthFirst(usageStart) + while (billDate <= now) { + const y = usageStart.getFullYear() + const m = usageStart.getMonth() + const dim = daysInMonth(y, m) + const day = usageStart.getDate() + const fraction = day === 1 ? 1 : (dim - day + 1) / dim + const amt = e.amount * fraction + transactions.push({ + date: new Date(billDate), + desc: e.desc, + amount: -Math.abs(+amt.toFixed(2)), + provider: e.provider + }) + usageStart = getMonthStart(billDate.getFullYear(), billDate.getMonth()) + billDate = addMonths(billDate, 1) + } } - return transactions }) -} - -const generateTransactionTable = transactions => { - const header = '| Date | Description | Amount | Provider |\n| --- | --- | --- | --- |' - const rows = transactions - .sort((a, b) => b.date - a.date) - .map(({ desc, amount, date, provider }) => - `| ${formatDate(date)} | ${desc} | ${formatAmount(amount, amount < 0)} | ${provider} |` - ) - return [header, ...rows].join('\n') -} - -const groupBy = (array, key) => - array.reduce((acc, item) => { - const groupKey = typeof key === 'function' ? key(item) : item[key] - return { ...acc, [groupKey]: [...(acc[groupKey] || []), item] } - }, {}) - -const calculateTotal = transactions => - transactions.reduce((sum, { amount }) => sum + amount, 0) - -const generateBreakdown = (transactions, groupingKey, title) => { - const grouped = groupBy(transactions, groupingKey) - const breakdown = Object.entries(grouped) - .sort(([a], [b]) => b.localeCompare(a)) - .map(([key, items]) => ({ - key, - total: calculateTotal(items), - })) - - const header = `\n## ${title}\n\n| ${title} | Total |\n| --- | --- |` - const rows = breakdown.map(({ key, total }) => - `| ${key} | ${formatAmount(total, total < 0)} |` - ) - return [header, ...rows].join('\n') -} -const projectRunwayTable = (initialBalance, monthlyExpenses, monthlyIncome, projectionMonths = 12) => { - if (monthlyIncome >= Math.abs(monthlyExpenses)) { - return "\n## Runway Projection\n\nThe project will not run out of money."; - } - const netMonthlyChange = monthlyIncome + monthlyExpenses; - if (netMonthlyChange >= 0) { - return "\n## Runway Projection\n\nThe project will not run out of money."; - } - let currentBalance = initialBalance; - const header = "\n## Runway Projection\n\n| Month | Starting Balance | Income | Expenses | Net Change | Ending Balance |\n| --- | --- | --- | --- | --- | --- |"; - const rows = []; - - const currentDate = new Date(); - - for (let i = 0; i < projectionMonths; i++) { - const monthDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + i, 1); - const monthStr = formatDate(monthDate).slice(0, 7); - const startingBalance = currentBalance; - const netChange = netMonthlyChange; - currentBalance += netChange; - const endingBalance = currentBalance - - rows.push(`| ${monthStr} | ${formatAmount(startingBalance)} | ${formatAmount(monthlyIncome)} | ${formatAmount(monthlyExpenses, true)} | ${formatAmount(netChange)} | ${formatAmount(endingBalance, endingBalance<0)} |`); - - if (currentBalance < 0) { - break; // Stop projecting once balance is negative - } + transactions.sort((a, b) => b.date - a.date) - } - return [header, ...rows].join('\n'); -}; - -const generateBookkeeping = async (income, expenses) => { - const allTransactions = [ - ...generateTransactions(income, 'income'), - ...generateTransactions(expenses, 'expense'), - ] - - const totalIncome = calculateTotal(allTransactions.filter(t => t.amount > 0)) - const totalExpenses = calculateTotal(allTransactions.filter(t => t.amount < 0)) + const totalIncome = transactions + .filter(t => t.amount > 0) + .reduce((acc, t) => acc + t.amount, 0) + const totalExpenses = transactions + .filter(t => t.amount < 0) + .reduce((acc, t) => acc + t.amount, 0) const balance = totalIncome + totalExpenses - const today = new Date() - const oneYearFromNow = new Date(today.getFullYear() + 1, today.getMonth(), today.getDate()) - const futureIncomeTransactions = generateFutureTransactions(income, 'income', oneYearFromNow) - const futureExpenseTransactions = generateFutureTransactions(expenses, 'expense', oneYearFromNow) - const totalFutureIncome = calculateTotal(futureIncomeTransactions) - const totalFutureExpenses = calculateTotal(futureExpenseTransactions) + const overviewTable = `| income | expenses | balance | +| --- | --- | --- | +| ${toMoney(totalIncome)} | ${toMoney(totalExpenses)} | ${toMoney(balance)} |` - const monthlyIncome = (totalIncome + totalFutureIncome) / 12 || 0 - const monthlyExpenses = (totalExpenses + totalFutureExpenses) / 12 || 0 - - const runwayProjectionTable = projectRunwayTable(balance, monthlyExpenses, monthlyIncome) - - const content = [ + const providers = [...new Set([...income.map(i => i.provider), ...expenses.map(e => e.provider)])] + const providerTotals = providers.map(p => { + const sum = transactions + .filter(t => t.provider === p) + .reduce((acc, t) => acc + t.amount, 0) + return { provider: p, total: sum } + }) + const breakdownByProvider = `| provider | total | +| --- | --- | +${providerTotals.map(pt => `| ${pt.provider} | ${pt.total === 0 ? `$0.00` : toMoney(pt.total)} |`).join('\n')}` + + const years = [...new Set(transactions.map(t => t.date.getFullYear()))].sort((a,b) => a-b) + const yearRows = years.map(y => { + const sum = transactions + .filter(t => t.date.getFullYear() === y) + .reduce((acc, t) => acc + t.amount, 0) + return { year: y, total: sum } + }) + const breakdownByYear = `| year | total | +| --- | --- | +${yearRows.map(r => `| ${r.year} | ${r.total === 0 ? `$0.00` : toMoney(r.total)} |`).join('\n')}` + + const transactionTable = `| date | description | amount | provider | +| --- | --- | --- | --- | +${transactions.map(t => `| ${formatDate(t.date)} | ${t.desc} | ${t.amount === 0 ? '$0.00' : toMoney(t.amount)} | ${t.provider} |`).join('\n')}` + + const currentBalance = balance + + const next12Months = [] + const startDate = new Date(now.getFullYear(), now.getMonth()+1, 1) + let runningBalance = currentBalance + for (let i=0; i<12; i++) { + const ymDate = addMonths(startDate, i) + const y = ymDate.getFullYear() + const m = ymDate.getMonth() + const monthlyIncomes = 0 + let monthlyExpenses = 0 + expenses.forEach(e => { + if (e.period === 'monthly') { + const start = new Date(e.date) + const usageYear = i === 0 ? now.getFullYear() : addMonths(startDate, i-1).getFullYear() + const usageMonth = i === 0 ? now.getMonth() : addMonths(startDate, i-1).getMonth() + if (start <= getMonthStart(usageYear, usageMonth)) { + const dim = daysInMonth(usageYear, usageMonth) + const d = start > getMonthStart(usageYear, usageMonth) ? start.getDate() : 1 + const fraction = d === 1 ? 1 : (dim - d + 1)/dim + if (i === 0) { + monthlyExpenses += (e.amount * fraction) + } else { + monthlyExpenses += e.amount + } + } + } + }) + const finalExpense = i === 0 ? +(monthlyExpenses.toFixed(2)) : monthlyExpenses + const inc = 0 + const exp = finalExpense === 0 ? 0 : -Math.abs(finalExpense) + const startBal = runningBalance + const netChange = inc + exp + const endBal = startBal + netChange + next12Months.push({ + month: `${y}-${zeroPad(m+1)}`, + startBal, + inc, + exp, + netChange, + endBal + }) + runningBalance = endBal + } + + const projectionTable = `| month | starting balance | income | expenses | net change | ending balance | +| --- | --- | --- | --- | --- | --- | +${next12Months.map(row => { + const sb = toMoney(row.startBal) + const i = row.inc === 0 ? '$0.00' : toMoney(row.inc) + const e = row.exp === 0 ? '$0.00' : toMoney(row.exp) + const n = row.netChange === 0 ? '$0.00' : toMoney(row.netChange) + const eb = toMoney(row.endBal) + return `| ${row.month} | ${sb} | ${i} | ${e} | ${n} | ${eb} |` +}).join('\n')}` + + return [ '# Bookkeeping', - '\n## Overview', - '\n| Category | Amount |', - '| --- | --- |', - `| Income | ${formatAmount(totalIncome)} |`, - `| Expenses | ${formatAmount(totalExpenses, true)} |`, - `| Balance | ${formatAmount(balance, balance < 0)} |`, - `| Monthly Income (Projected) | ${formatAmount(monthlyIncome)} |`, - `| Monthly Expenses (Projected) | ${formatAmount(monthlyExpenses, true)} |`, - runwayProjectionTable, - generateBreakdown(allTransactions, 'provider', 'By Provider'), - generateBreakdown(allTransactions, item => formatDate(item.date).slice(0, 4), 'By Year'), - '\n## Transactions', - generateTransactionTable(allTransactions), - ].join('\n') - - await fs.writeFile('./bookkeeping.md', content, 'utf-8') + '## Overview', + overviewTable, + '## Projection (Next 12 Months)', + projectionTable, + '## Breakdown by Provider', + breakdownByProvider, + '## Breakdown by Year', + breakdownByYear, + '## Transactions', + transactionTable + ].join('\n\n') } const income = [ - { provider: 'Hetzner', desc: 'Hetzner Cloud Credits', amount: 60, date: new Date('2025-02-10'), period: 'once' }, + { provider: 'Hetzner', desc: 'Hetzner Cloud Credits', amount: 60, date: new Date('2025-02-10'), period: 'once' } ] const expenses = [ { provider: 'AWS', desc: 'AWS S3 Billing', amount: 0.28, date: new Date('2023-02-01'), period: 'monthly' }, { provider: 'Cloudflare', desc: 'Cloudflare R2 Billing', amount: 0.0, date: new Date('2023-02-01'), period: 'monthly' }, { provider: 'Hetzner', desc: 'Hetzner Object Storage Billing', amount: 5.99, date: new Date('2025-02-12'), period: 'monthly' }, - { provider: 'Backblaze', desc: 'Backblaze B2 Billing', amount: 0.0, date: new Date('2025-02-12'), period: 'monthly' }, + { provider: 'Backblaze', desc: 'Backblaze B2 Billing', amount: 0.0, date: new Date('2025-02-12'), period: 'monthly' } ] -generateBookkeeping(income, expenses) \ No newline at end of file +;(async () => { + const content = generateBookkeeping(income, expenses) + await fs.writeFile('./bookkeeping.md', content, 'utf-8') +})() From c8516cdc174c80be905b87d70fc36ee1886876fd Mon Sep 17 00:00:00 2001 From: brihter Date: Sun, 16 Feb 2025 18:31:25 +0100 Subject: [PATCH 6/7] wip --- bookkeeping.md | 12 ++++++------ etc/bookkeeping.js | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/bookkeeping.md b/bookkeeping.md index 3db6b07..7fa3004 100644 --- a/bookkeeping.md +++ b/bookkeeping.md @@ -2,13 +2,13 @@ ## Overview -| income | expenses | balance | +| Income | Expenses | Balance | | --- | --- | --- | | +$60.00 | -$6.72 | +$53.28 | ## Projection (Next 12 Months) -| month | starting balance | income | expenses | net change | ending balance | +| Month | Starting balance | Income | Expenses | Net Change | Ending Balance | | --- | --- | --- | --- | --- | --- | | 2025-03 | +$53.28 | $0.00 | -$0.28 | -$0.28 | +$53.00 | | 2025-04 | +$53.00 | $0.00 | -$6.27 | -$6.27 | +$46.73 | @@ -23,16 +23,16 @@ | 2026-01 | -$3.43 | $0.00 | -$6.27 | -$6.27 | -$9.70 | | 2026-02 | -$9.70 | $0.00 | -$6.27 | -$6.27 | -$15.97 | -## Breakdown by Provider +## By Provider -| provider | total | +| Provider | Total | | --- | --- | | Hetzner | +$60.00 | | AWS | -$6.72 | | Cloudflare | $0.00 | | Backblaze | $0.00 | -## Breakdown by Year +## By Year | year | total | | --- | --- | @@ -42,7 +42,7 @@ ## Transactions -| date | description | amount | provider | +| Date | Description | Amount | Provider | | --- | --- | --- | --- | | 2025-02-10 | Hetzner Cloud Credits | +$60.00 | Hetzner | | 2025-02-01 | AWS S3 Billing | -$0.28 | AWS | diff --git a/etc/bookkeeping.js b/etc/bookkeeping.js index 492db90..90e1bd6 100644 --- a/etc/bookkeeping.js +++ b/etc/bookkeeping.js @@ -129,7 +129,7 @@ const generateBookkeeping = (income = [], expenses = []) => { .reduce((acc, t) => acc + t.amount, 0) const balance = totalIncome + totalExpenses - const overviewTable = `| income | expenses | balance | + const overviewTable = `| Income | Expenses | Balance | | --- | --- | --- | | ${toMoney(totalIncome)} | ${toMoney(totalExpenses)} | ${toMoney(balance)} |` @@ -140,7 +140,7 @@ const generateBookkeeping = (income = [], expenses = []) => { .reduce((acc, t) => acc + t.amount, 0) return { provider: p, total: sum } }) - const breakdownByProvider = `| provider | total | + const breakdownByProvider = `| Provider | Total | | --- | --- | ${providerTotals.map(pt => `| ${pt.provider} | ${pt.total === 0 ? `$0.00` : toMoney(pt.total)} |`).join('\n')}` @@ -155,7 +155,7 @@ ${providerTotals.map(pt => `| ${pt.provider} | ${pt.total === 0 ? `$0.00` : toMo | --- | --- | ${yearRows.map(r => `| ${r.year} | ${r.total === 0 ? `$0.00` : toMoney(r.total)} |`).join('\n')}` - const transactionTable = `| date | description | amount | provider | + const transactionTable = `| Date | Description | Amount | Provider | | --- | --- | --- | --- | ${transactions.map(t => `| ${formatDate(t.date)} | ${t.desc} | ${t.amount === 0 ? '$0.00' : toMoney(t.amount)} | ${t.provider} |`).join('\n')}` @@ -204,7 +204,7 @@ ${transactions.map(t => `| ${formatDate(t.date)} | ${t.desc} | ${t.amount === 0 runningBalance = endBal } - const projectionTable = `| month | starting balance | income | expenses | net change | ending balance | + const projectionTable = `| Month | Starting balance | Income | Expenses | Net Change | Ending Balance | | --- | --- | --- | --- | --- | --- | ${next12Months.map(row => { const sb = toMoney(row.startBal) @@ -221,9 +221,9 @@ ${next12Months.map(row => { overviewTable, '## Projection (Next 12 Months)', projectionTable, - '## Breakdown by Provider', + '## By Provider', breakdownByProvider, - '## Breakdown by Year', + '## By Year', breakdownByYear, '## Transactions', transactionTable From 74a108208e3a1f3593748921be950855a7087ea9 Mon Sep 17 00:00:00 2001 From: brihter Date: Sun, 16 Feb 2025 18:35:39 +0100 Subject: [PATCH 7/7] changelog --- changelog.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/changelog.md b/changelog.md index 8d92c62..6cd4164 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,12 @@ # Changelog +## v1.6.3 + +- Improved documentation. +- Added projections to bookkeeping. + +This is a maintenance release. + ## v1.6.2 - Improved documentation.