@profullstack/coinpay 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.d.ts CHANGED
@@ -149,3 +149,4 @@ import { CoinPayClient } from './client.js';
149
149
  export default CoinPayClient;
150
150
 
151
151
  export * from './finances.js';
152
+ export * from './finances-reports.js';
package/src/index.js CHANGED
@@ -344,3 +344,6 @@ export {
344
344
  };
345
345
 
346
346
  export default CoinPayClient;
347
+
348
+ // Finance reports, sync jobs, coverage and the statement library.
349
+ export * from './finances-reports.js';
package/src/pdf.js ADDED
@@ -0,0 +1,151 @@
1
+ /**
2
+ * HTML to PDF, using whatever browser this machine already has.
3
+ *
4
+ * Deliberately not a dependency. Adding Puppeteer to a payments CLI pulls a
5
+ * ~150MB browser download into every install so that a minority of runs can
6
+ * print a document, and the machines that want the PDF usually already have a
7
+ * browser from some other tool. So: look for one, use it, and when there is
8
+ * none say exactly what to install rather than failing with a stack trace.
9
+ *
10
+ * The HTML is always written either way. A report you can open and print by
11
+ * hand is a working report; a missing browser should downgrade the output, not
12
+ * lose the work.
13
+ */
14
+
15
+ import { spawn } from 'node:child_process';
16
+ import { existsSync, mkdtempSync, readdirSync, writeFileSync } from 'node:fs';
17
+ import { homedir, tmpdir } from 'node:os';
18
+ import path from 'node:path';
19
+
20
+ /** Names a system browser might have on PATH. */
21
+ const PATH_NAMES = [
22
+ 'google-chrome',
23
+ 'google-chrome-stable',
24
+ 'chromium',
25
+ 'chromium-browser',
26
+ 'microsoft-edge',
27
+ 'brave-browser',
28
+ ];
29
+
30
+ function onPath(name) {
31
+ const dirs = (process.env.PATH ?? '').split(path.delimiter).filter(Boolean);
32
+ for (const dir of dirs) {
33
+ const file = path.join(dir, name);
34
+ if (existsSync(file)) return file;
35
+ }
36
+ return null;
37
+ }
38
+
39
+ /** Browsers that Playwright and Puppeteer download for their own use. */
40
+ function fromCaches() {
41
+ const found = [];
42
+ const roots = [
43
+ { dir: path.join(homedir(), '.cache', 'ms-playwright'), rel: ['chrome-linux', 'chrome'] },
44
+ { dir: path.join(homedir(), '.cache', 'puppeteer', 'chrome'), rel: ['chrome-linux64', 'chrome'] },
45
+ ];
46
+ for (const { dir, rel } of roots) {
47
+ if (!existsSync(dir)) continue;
48
+ let entries = [];
49
+ try {
50
+ entries = readdirSync(dir);
51
+ } catch {
52
+ continue;
53
+ }
54
+ for (const entry of entries) {
55
+ const candidate = path.join(dir, entry, ...rel);
56
+ if (existsSync(candidate)) found.push(candidate);
57
+ }
58
+ }
59
+ // Newest download first: the directory names carry versions.
60
+ return found.sort().reverse();
61
+ }
62
+
63
+ export function browserCandidates(env = process.env) {
64
+ const out = [];
65
+ if (env.COINPAY_CHROME) out.push(env.COINPAY_CHROME);
66
+ for (const name of PATH_NAMES) {
67
+ const file = onPath(name);
68
+ if (file) out.push(file);
69
+ }
70
+ out.push(...fromCaches());
71
+ return out;
72
+ }
73
+
74
+ function run(file, args, timeoutMs) {
75
+ return new Promise((resolve) => {
76
+ let child;
77
+ try {
78
+ child = spawn(file, args, { stdio: ['ignore', 'ignore', 'pipe'] });
79
+ } catch (error) {
80
+ resolve({ ok: false, stderr: String(error?.message ?? error) });
81
+ return;
82
+ }
83
+ let stderr = '';
84
+ child.stderr?.on('data', (d) => {
85
+ stderr += String(d);
86
+ });
87
+ const timer = setTimeout(() => {
88
+ try {
89
+ child.kill('SIGKILL');
90
+ } catch {
91
+ /* already gone */
92
+ }
93
+ }, timeoutMs);
94
+ child.on('error', (error) => {
95
+ clearTimeout(timer);
96
+ resolve({ ok: false, stderr: String(error?.message ?? error) });
97
+ });
98
+ child.on('exit', (code) => {
99
+ clearTimeout(timer);
100
+ resolve({ ok: code === 0, code, stderr });
101
+ });
102
+ });
103
+ }
104
+
105
+ /**
106
+ * Render `html` to `pdfPath`.
107
+ *
108
+ * Returns what actually happened rather than throwing, because "no browser" is
109
+ * an ordinary outcome here and the caller has something useful to say about it.
110
+ */
111
+ export async function htmlToPdf(html, pdfPath, { timeoutMs = 120_000, env = process.env } = {}) {
112
+ const dir = mkdtempSync(path.join(tmpdir(), 'coinpay-report-'));
113
+ const htmlPath = path.join(dir, 'report.html');
114
+ writeFileSync(htmlPath, html, 'utf8');
115
+
116
+ const candidates = browserCandidates(env);
117
+ const tried = [];
118
+
119
+ for (const browser of candidates) {
120
+ const result = await run(
121
+ browser,
122
+ [
123
+ '--headless',
124
+ '--disable-gpu',
125
+ '--no-sandbox',
126
+ '--no-pdf-header-footer',
127
+ `--print-to-pdf=${pdfPath}`,
128
+ `file://${htmlPath}`,
129
+ ],
130
+ timeoutMs,
131
+ );
132
+ if (result.ok && existsSync(pdfPath)) return { ok: true, pdfPath, htmlPath, browser };
133
+ // A cached browser with no system libraries is the common failure, and its
134
+ // message is the useful part, so it is kept rather than flattened.
135
+ tried.push({ browser, reason: (result.stderr || `exit ${result.code}`).trim().split('\n')[0] });
136
+ }
137
+
138
+ return { ok: false, htmlPath, tried };
139
+ }
140
+
141
+ /** What to tell someone whose machine could not print. */
142
+ export function installHint(tried = []) {
143
+ const missingLibs = tried.some((t) => /error while loading shared libraries|cannot open shared object/.test(t.reason ?? ''));
144
+ if (missingLibs) {
145
+ return 'A browser was found but is missing system libraries. Install them with:\n npx playwright install-deps chromium';
146
+ }
147
+ if (!tried.length) {
148
+ return 'No browser found. Install one, or point COINPAY_CHROME at a Chrome or Chromium binary:\n npx playwright install --with-deps chromium';
149
+ }
150
+ return 'No browser could print the report. Point COINPAY_CHROME at a working Chrome or Chromium binary.';
151
+ }