From c7d16539e652961c2e4a0cf2e5b4be8b26a169f0 Mon Sep 17 00:00:00 2001 From: George Lambert Date: Fri, 11 Sep 2026 15:18:07 -0400 Subject: [PATCH] Initial import of zappier-accounting-export from zapier monorepo --- README.md | 18 ++++++++++++++++++ package.json | 11 +++++++++++ src/server.js | 42 ++++++++++++++++++++++++++++++++++++++++++ test/health.test.js | 23 +++++++++++++++++++++++ 4 files changed, 94 insertions(+) create mode 100644 README.md create mode 100644 package.json create mode 100644 src/server.js create mode 100644 test/health.test.js diff --git a/README.md b/README.md new file mode 100644 index 0000000..ec0439a --- /dev/null +++ b/README.md @@ -0,0 +1,18 @@ +# zappier-accounting-export + +Accounting-department export of **invoices, billing, and payment history** to **QuickBooks IIF** and generic CSV (also usable in other ledgers). + +**Forgejo:** https://git.georgelambert.org/marchon/zappier-accounting-export +**Catalog:** https://zapier.georgelambert.org/packages/zappier-accounting-export/README.pdf + +Reads issued/paid invoices from zappier-edge (`ZAPPIER_ADMIN_URL`). + +```bash +PORT=3013 node src/server.js +curl 'http://127.0.0.1:3013/export/quickbooks.iif?period=2026-07' -o zappier.iif +curl 'http://127.0.0.1:3013/export/accounting.csv?period=2026-07' -o zappier.csv +``` + +Import the IIF in QuickBooks Desktop: **File → Utilities → Import → IIF**. The CSV is a flat invoice register for other systems. + +History is the invoice list on zappier-edge (draft / issued / paid) plus CS credit ledger and portal prepaid reloads. diff --git a/package.json b/package.json new file mode 100644 index 0000000..4165d1c --- /dev/null +++ b/package.json @@ -0,0 +1,11 @@ +{ + "name": "zappier-accounting-export", + "version": "0.1.0", + "private": true, + "type": "module", + "description": "Export zappier invoices to QuickBooks IIF and accounting CSV", + "scripts": { + "start": "node src/server.js", + "test": "node --test test/*.test.js" + } +} diff --git a/src/server.js b/src/server.js new file mode 100644 index 0000000..f19d025 --- /dev/null +++ b/src/server.js @@ -0,0 +1,42 @@ +#!/usr/bin/env node +/** Accounting department: QuickBooks IIF + CSV from zappier-edge invoices. */ +import http from 'node:http'; + +const PORT = Number(process.env.PORT || 3013); +const EDGE = (process.env.ZAPPIER_ADMIN_URL || 'http://127.0.0.1:3000').replace(/\/$/, ''); +const KEY = process.env.ZAPPIER_ADMIN_KEY || 'admin-dev-key'; + +async function edge(pathname) { + const r = await fetch(`${EDGE}${pathname}`, { headers: { 'x-admin-key': KEY } }); + return r; +} + +const server = http.createServer(async (req, res) => { + const url = new URL(req.url || '/', `http://127.0.0.1:${PORT}`); + const send = (code, type, body) => { + res.writeHead(code, { 'content-type': type }); + res.end(body); + }; + try { + if (req.method === 'GET' && url.pathname === '/health') { + return send(200, 'application/json', JSON.stringify({ ok: true, role: 'zappier-accounting-export' })); + } + const period = url.searchParams.get('period'); + const q = period ? `?period=${encodeURIComponent(period)}` : ''; + if (req.method === 'GET' && url.pathname === '/export/quickbooks.iif') { + const r = await edge(`/admin/api/exports/quickbooks.iif${q}`); + return send(r.status, 'text/plain', await r.text()); + } + if (req.method === 'GET' && url.pathname === '/export/accounting.csv') { + const r = await edge(`/admin/api/exports/accounting.csv${q}`); + return send(r.status, 'text/csv', await r.text()); + } + send(404, 'application/json', JSON.stringify({ error: 'not found' })); + } catch (err) { + send(502, 'application/json', JSON.stringify({ error: err.message })); + } +}); + +server.listen(PORT, '0.0.0.0', () => { + process.stdout.write(`zappier-accounting-export http://127.0.0.1:${PORT}/\n`); +}); diff --git a/test/health.test.js b/test/health.test.js new file mode 100644 index 0000000..7d3def8 --- /dev/null +++ b/test/health.test.js @@ -0,0 +1,23 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { spawn } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; +import path from 'node:path'; + +const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..'); + +test('accounting-export health', async () => { + const port = 18013; + const child = spawn(process.execPath, ['src/server.js'], { + cwd: root, + env: { ...process.env, PORT: String(port) }, + stdio: ['ignore', 'pipe', 'pipe'], + }); + await new Promise((r) => setTimeout(r, 400)); + try { + const r = await fetch(`http://127.0.0.1:${port}/health`); + assert.equal((await r.json()).role, 'zappier-accounting-export'); + } finally { + child.kill('SIGTERM'); + } +});