From 4fb7fd209ccd2e8192bf81de7f31ebd4ffc08c02 Mon Sep 17 00:00:00 2001 From: Steve Dogiakos Date: Fri, 13 Mar 2026 08:43:34 -0600 Subject: [PATCH] Add deposit slip and report generation - New Deposits tab with ledger: date, checks total, cash, deposit total, item count, status - Slide-in deposit panel: date, currency, coin, cash back, dynamic check entry rows, live totals - Save deposit, then generate Deposit Slip or Deposit Report PDF - Deposit slip: 3.375" x 8.5" portrait with Style A background drawn server-side, digit-column amounts, GnuMICR routing/account line rotated 90 deg, rotated deposit total and check count in left margin - Deposit report: plain Courier ledger with depositor/bank info, check grid, totals - deposits and deposit_items tables in schema; ON DELETE CASCADE for items - Routes: GET/POST/PUT/DELETE /api/deposits, POST /api/deposit-pdf - Generating a slip marks deposit as printed; date range and status filters - README updated to describe deposit slip feature --- README.md | 22 +- public/css/style.css | 160 +++++++++ public/index.html | 121 +++++++ public/js/app.js | 300 +++++++++++++++++ src/app.js | 6 +- src/db/schema.sql | 25 ++ src/routes/deposit-pdf.js | 47 +++ src/routes/deposits.js | 144 ++++++++ src/services/depositPdfService.js | 523 ++++++++++++++++++++++++++++++ 9 files changed, 1345 insertions(+), 3 deletions(-) create mode 100644 src/routes/deposit-pdf.js create mode 100644 src/routes/deposits.js create mode 100644 src/services/depositPdfService.js diff --git a/README.md b/README.md index da8cf6b..5eebb81 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # check-printing -Self-hosted web app for printing checks on blank check stock. A Dockerized Node.js web app accessible on the local network. +Self-hosted web app for printing checks and bank deposit slips. A containerized Node.js web app accessible on the local network. ## Stack @@ -58,6 +58,26 @@ The script imports account config (T100), logo (Settings), check layout (T200), Use the **Reprint** button on printed checks to regenerate without re-marking them. +## Deposit slips + +Switch to the **Deposits** tab in the toolbar to manage bank deposits. + +1. Click **+ New Deposit** to open the deposit entry panel +2. Enter the deposit date, currency, coin, and cash back amounts +3. Add each check being deposited (check number, payee, memo, amount) — totals update live +4. Click **Save Deposit**, then **Deposit Slip** or **Report** to generate a PDF + +**Deposit Slip** generates a precisely positioned 3.375" × 8.5" PDF matching physical bank deposit slip stock, including: + +- Style A background (form lines and labels drawn server-side — no preprinted stock required) +- Digit-column amount formatting +- Routing/account line in E-13B magnetic ink character recognition font, rotated 90° +- Rotated deposit total and check count in the left margin + +**Deposit Report** generates a plain formatted ledger document listing all checks, cash totals, and the final deposit amount — suitable for filing. + +Generating a deposit slip marks the deposit as printed in the ledger. + ## Check layout - Page: 8.5" × 11", zero margins diff --git a/public/css/style.css b/public/css/style.css index 4ec67d1..1554cb1 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -535,3 +535,163 @@ input[type="file"] { max-height: calc(100vh - 160px); overflow-y: auto; } + +/* ── View nav tabs ── */ +.view-nav { + display: flex; + gap: 0; + background: var(--surface); + border-bottom: 2px solid var(--border); + flex-shrink: 0; + padding: 0 1rem; +} +.view-tab { + background: transparent; + border: none; + border-bottom: 2px solid transparent; + border-radius: 0; + padding: 8px 16px; + font-size: 13px; + font-weight: 500; + color: var(--text-muted); + cursor: pointer; + margin-bottom: -2px; +} +.view-tab:hover { color: var(--text); } +.view-tab.active { color: var(--primary); border-bottom-color: var(--primary); } + +/* ── View panes ── */ +.view-pane { + display: flex; + flex-direction: column; + flex: 1; + overflow: hidden; +} +.view-pane[hidden] { display: none; } + +/* ── Deposit panel ── */ +#dep-panel-overlay { + position: fixed; + inset: 0; + background: rgba(0,0,0,0.3); + z-index: 100; + opacity: 0; + pointer-events: none; + transition: opacity 0.2s; +} +#dep-panel-overlay.open { opacity: 1; pointer-events: auto; } + +#deposit-panel { + position: fixed; + top: 0; + right: 0; + width: 560px; + max-width: 98vw; + height: 100vh; + background: var(--surface); + z-index: 101; + box-shadow: -4px 0 24px rgba(0,0,0,0.15); + transform: translateX(100%); + transition: transform 0.2s ease; + display: flex; + flex-direction: column; + overflow-y: auto; +} +#deposit-panel.open { transform: translateX(0); } + +#deposit-panel-body { + padding: 16px; + display: flex; + flex-direction: column; + gap: 14px; + flex: 1; +} + +.dep-summary { + display: flex; + flex-direction: column; + gap: 10px; +} + +.dep-totals { + background: var(--bg); + border: 1px solid var(--border); + border-radius: 4px; + padding: 8px 12px; + display: flex; + flex-direction: column; + gap: 3px; +} +.dep-total-row { + display: flex; + justify-content: space-between; + font-size: 12px; + color: var(--text-muted); +} +.dep-total-grand { + font-size: 13px; + font-weight: 600; + color: var(--text); + margin-top: 4px; + padding-top: 4px; + border-top: 1px solid var(--border); +} + +/* Deposit check items table */ +.dep-checks-section { display: flex; flex-direction: column; gap: 6px; flex: 1; } +.dep-checks-header { + display: flex; + align-items: center; + justify-content: space-between; + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--text-muted); +} +.dep-items-table { + width: 100%; + border-collapse: collapse; + font-size: 12px; +} +.dep-items-table thead th { + background: #f8f8f8; + border-bottom: 1px solid var(--border); + padding: 4px 6px; + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + color: var(--text-muted); + white-space: nowrap; + position: static; +} +.dep-items-table td { + padding: 2px 4px; + vertical-align: middle; + border-bottom: 1px solid #f0f0f0; +} +.dep-item-input { + border: 1px solid var(--border); + border-radius: 3px; + padding: 3px 6px; + font-size: 12px; + font-family: var(--font); + background: var(--surface); + color: var(--text); +} +.dep-item-input:focus { + outline: none; + border-color: var(--primary); +} + +.dep-form-actions { + display: flex; + justify-content: space-between; + align-items: center; + gap: 8px; + padding-top: 4px; + border-top: 1px solid var(--border); + margin-top: auto; + flex-wrap: wrap; +} +.dep-pdf-btns { display: flex; gap: 6px; } diff --git a/public/index.html b/public/index.html index 9a7cc99..91ab953 100644 --- a/public/index.html +++ b/public/index.html @@ -16,6 +16,14 @@ Next check: + + + + +
@@ -56,6 +64,45 @@
+
+ + +
@@ -334,6 +381,80 @@
+ +
+ + diff --git a/public/js/app.js b/public/js/app.js index 7487990..ae31f82 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -607,6 +607,271 @@ async function saveAccountSettings() { } } +// ── Deposits ───────────────────────────────────────────────────────────────── + +const depState = { + deposits: [], + editingId: null, + items: [], // working list of check rows in the panel +}; + +async function loadDeposits() { + if (!state.activeAccountId) return; + const tbody = document.getElementById('deposits-tbody'); + tbody.innerHTML = 'Loading…'; + try { + depState.deposits = await apiFetch('GET', `/api/deposits?account_id=${state.activeAccountId}`); + renderDepositsTable(); + } catch (err) { + tbody.innerHTML = `Error: ${escHtml(err.message)}`; + } +} + +function renderDepositsTable() { + const tbody = document.getElementById('deposits-tbody'); + const from = document.getElementById('dep-filter-from').value; + const to = document.getElementById('dep-filter-to').value; + const status = document.getElementById('dep-filter-status').value; + + const fmt = n => new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(n || 0); + const fmtDate = d => d ? new Date(d + 'T12:00:00').toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) : '—'; + + let list = depState.deposits.filter(d => { + if (from && d.deposit_date < from) return false; + if (to && d.deposit_date > to) return false; + if (status === '0' && d.printed) return false; + if (status === '1' && !d.printed) return false; + return true; + }); + + if (list.length === 0) { + tbody.innerHTML = 'No deposits found.'; + return; + } + + tbody.innerHTML = list.map(d => { + const cashTotal = (d.currency || 0) + (d.coin || 0); + const checksTotal = d.checks_total || 0; + const depositTotal = cashTotal + checksTotal - (d.cash_back || 0); + const printed = !!d.printed; + const badge = printed + ? 'Printed' + : 'Unprinted'; + return ` + ${fmtDate(d.deposit_date)} + ${fmt(checksTotal)} + ${fmt(cashTotal)} + ${fmt(d.cash_back)} + ${fmt(depositTotal)} + ${d.item_count || 0} + ${badge} + + + + + `; + }).join(''); + + tbody.querySelectorAll('.dep-btn-edit').forEach(btn => + btn.addEventListener('click', () => openDepositPanel(parseInt(btn.dataset.id, 10)))); + tbody.querySelectorAll('.dep-btn-delete').forEach(btn => + btn.addEventListener('click', () => deleteDeposit(parseInt(btn.dataset.id, 10)))); +} + +async function openDepositPanel(id = null) { + depState.editingId = id; + depState.items = []; + + document.getElementById('dep-panel-error').hidden = true; + document.getElementById('dep-panel-title').textContent = id ? 'Edit Deposit' : 'New Deposit'; + document.getElementById('dep-date').value = new Date().toISOString().slice(0, 10); + document.getElementById('dep-currency').value = ''; + document.getElementById('dep-coin').value = ''; + document.getElementById('dep-cashback').value = ''; + + if (id !== null) { + try { + const dep = await apiFetch('GET', `/api/deposits/${id}`); + document.getElementById('dep-date').value = dep.deposit_date || ''; + document.getElementById('dep-currency').value = dep.currency || ''; + document.getElementById('dep-coin').value = dep.coin || ''; + document.getElementById('dep-cashback').value = dep.cash_back || ''; + depState.items = (dep.items || []).map(it => ({ ...it })); + } catch (err) { + alert('Error loading deposit: ' + err.message); + return; + } + } else { + depState.items = [newDepItem()]; + } + + renderDepItems(); + recalcDepTotals(); + + const slipBtn = document.getElementById('btn-dep-slip'); + const reportBtn = document.getElementById('btn-dep-report'); + slipBtn.disabled = id === null; + reportBtn.disabled = id === null; + + document.getElementById('dep-panel-overlay').classList.add('open'); + document.getElementById('deposit-panel').classList.add('open'); + document.getElementById('dep-date').focus(); +} + +function closeDepositPanel() { + document.getElementById('dep-panel-overlay').classList.remove('open'); + document.getElementById('deposit-panel').classList.remove('open'); + depState.editingId = null; + depState.items = []; +} + +function newDepItem() { + return { _key: Math.random(), check_no: '', bank_no: '', payee: '', memo: '', amount: '' }; +} + +function renderDepItems() { + const tbody = document.getElementById('dep-items-tbody'); + tbody.innerHTML = depState.items.map((item, i) => ` + + + + + + + + `).join(''); + + tbody.querySelectorAll('.dep-item-input').forEach(inp => { + inp.addEventListener('input', e => { + const row = e.target.closest('tr'); + const idx = parseInt(row.dataset.idx, 10); + depState.items[idx][e.target.dataset.field] = e.target.value; + if (e.target.dataset.field === 'amount') recalcDepTotals(); + }); + }); + tbody.querySelectorAll('.dep-item-remove').forEach(btn => { + btn.addEventListener('click', () => { + depState.items.splice(parseInt(btn.dataset.idx, 10), 1); + renderDepItems(); + recalcDepTotals(); + }); + }); +} + +function recalcDepTotals() { + const fmt = n => new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(n); + const currency = parseFloat(document.getElementById('dep-currency').value) || 0; + const coin = parseFloat(document.getElementById('dep-coin').value) || 0; + const cashBack = parseFloat(document.getElementById('dep-cashback').value) || 0; + const cashTotal = currency + coin; + const checksTotal = depState.items.reduce((s, it) => s + (parseFloat(it.amount) || 0), 0); + const subTotal = cashTotal + checksTotal; + const grand = subTotal - cashBack; + + document.getElementById('dep-cash-total').textContent = fmt(cashTotal); + document.getElementById('dep-checks-total').textContent = fmt(checksTotal); + document.getElementById('dep-subtotal').textContent = fmt(subTotal); + document.getElementById('dep-cashback-display').textContent = fmt(cashBack); + document.getElementById('dep-grand-total').textContent = fmt(grand); +} + +async function saveDeposit() { + const errEl = document.getElementById('dep-panel-error'); + errEl.hidden = true; + + const deposit_date = document.getElementById('dep-date').value; + if (!deposit_date) { + errEl.textContent = 'Deposit date is required.'; + errEl.hidden = false; + return; + } + + const payload = { + account_id: state.activeAccountId, + deposit_date, + currency: parseFloat(document.getElementById('dep-currency').value) || 0, + coin: parseFloat(document.getElementById('dep-coin').value) || 0, + cash_back: parseFloat(document.getElementById('dep-cashback').value) || 0, + items: depState.items + .filter(it => parseFloat(it.amount) > 0 || it.check_no || it.payee) + .map((it, i) => ({ + sort_order: i, + check_no: it.check_no || null, + bank_no: it.bank_no || null, + payee: it.payee || null, + memo: it.memo || null, + amount: parseFloat(it.amount) || 0, + })), + }; + + const btn = document.getElementById('btn-save-deposit'); + btn.disabled = true; + btn.textContent = 'Saving…'; + + try { + let saved; + if (depState.editingId !== null) { + saved = await apiFetch('PUT', `/api/deposits/${depState.editingId}`, payload); + } else { + saved = await apiFetch('POST', '/api/deposits', payload); + } + depState.editingId = saved.id; + // Enable PDF buttons now that deposit is saved + document.getElementById('btn-dep-slip').disabled = false; + document.getElementById('btn-dep-report').disabled = false; + document.getElementById('dep-panel-title').textContent = 'Edit Deposit'; + await loadDeposits(); + btn.disabled = false; + btn.textContent = 'Save Deposit'; + } catch (err) { + errEl.textContent = err.message; + errEl.hidden = false; + btn.disabled = false; + btn.textContent = 'Save Deposit'; + } +} + +async function deleteDeposit(id) { + const dep = depState.deposits.find(d => d.id === id); + const label = dep ? dep.deposit_date : `#${id}`; + if (!confirm(`Delete deposit from ${label}?`)) return; + try { + await apiFetch('DELETE', `/api/deposits/${id}`); + await loadDeposits(); + } catch (err) { + alert('Error: ' + err.message); + } +} + +async function generateDepositPdf(type) { + if (!depState.editingId) return; + const btn = type === 'slip' + ? document.getElementById('btn-dep-slip') + : document.getElementById('btn-dep-report'); + btn.disabled = true; + const orig = btn.textContent; + btn.textContent = '…'; + try { + const res = await fetch('/api/deposit-pdf', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ depositId: depState.editingId, type, mark_printed: type === 'slip' }), + }); + if (!res.ok) { + const err = await res.json().catch(() => ({ error: res.statusText })); + throw new Error(err.error || res.statusText); + } + const blob = await res.blob(); + window.open(URL.createObjectURL(blob), '_blank'); + if (type === 'slip') await loadDeposits(); + } catch (err) { + alert('PDF error: ' + err.message); + } finally { + btn.disabled = false; + btn.textContent = orig; + } +} + // ── Utilities ──────────────────────────────────────────────────────────────── function escHtml(str) { @@ -714,6 +979,41 @@ function init() { reader.readAsDataURL(file); }); + // View tabs (Checks / Deposits) + document.querySelectorAll('.view-tab').forEach(tab => { + tab.addEventListener('click', () => { + document.querySelectorAll('.view-tab').forEach(t => t.classList.remove('active')); + tab.classList.add('active'); + const view = tab.dataset.view; + document.getElementById('view-checks').hidden = view !== 'checks'; + document.getElementById('view-deposits').hidden = view !== 'deposits'; + if (view === 'deposits') loadDeposits(); + }); + }); + + // Deposit filters + document.getElementById('dep-filter-from').addEventListener('change', renderDepositsTable); + document.getElementById('dep-filter-to').addEventListener('change', renderDepositsTable); + document.getElementById('dep-filter-status').addEventListener('change', renderDepositsTable); + + // Deposit panel + document.getElementById('btn-new-deposit').addEventListener('click', () => openDepositPanel()); + document.getElementById('btn-close-dep-panel').addEventListener('click', closeDepositPanel); + document.getElementById('btn-cancel-deposit').addEventListener('click', closeDepositPanel); + document.getElementById('dep-panel-overlay').addEventListener('click', closeDepositPanel); + document.getElementById('btn-save-deposit').addEventListener('click', saveDeposit); + document.getElementById('btn-add-dep-item').addEventListener('click', () => { + depState.items.push(newDepItem()); + renderDepItems(); + }); + document.getElementById('btn-dep-slip').addEventListener('click', () => generateDepositPdf('slip')); + document.getElementById('btn-dep-report').addEventListener('click', () => generateDepositPdf('report')); + + // Deposit panel live recalc + ['dep-currency', 'dep-coin', 'dep-cashback'].forEach(id => { + document.getElementById(id).addEventListener('input', recalcDepTotals); + }); + // Initial data load loadAccounts(); } diff --git a/src/app.js b/src/app.js index 708c91c..650d15f 100644 --- a/src/app.js +++ b/src/app.js @@ -14,8 +14,10 @@ app.use(express.json()); app.use(express.static(path.join(__dirname, '../public'))); // Routes -app.use('/api/checks', require('./routes/checks')); -app.use('/api/pdf', require('./routes/pdf')); +app.use('/api/checks', require('./routes/checks')); +app.use('/api/pdf', require('./routes/pdf')); +app.use('/api/deposits', require('./routes/deposits')); +app.use('/api/deposit-pdf', require('./routes/deposit-pdf')); // GET /api/accounts - list all accounts (id + display name) app.get('/api/accounts', (req, res) => { diff --git a/src/db/schema.sql b/src/db/schema.sql index a73e024..3da463b 100644 --- a/src/db/schema.sql +++ b/src/db/schema.sql @@ -71,3 +71,28 @@ CREATE TABLE IF NOT EXISTS layout_fields ( CREATE INDEX IF NOT EXISTS idx_checks_date ON checks(check_date); CREATE INDEX IF NOT EXISTS idx_checks_printed ON checks(printed); CREATE INDEX IF NOT EXISTS idx_checks_check_no ON checks(check_no); + +CREATE TABLE IF NOT EXISTS deposits ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + account_id INTEGER NOT NULL REFERENCES account(id), + deposit_date TEXT NOT NULL, + currency REAL NOT NULL DEFAULT 0, + coin REAL NOT NULL DEFAULT 0, + cash_back REAL NOT NULL DEFAULT 0, + printed INTEGER NOT NULL DEFAULT 0, + add_date TEXT NOT NULL DEFAULT (datetime('now')) +); + +CREATE TABLE IF NOT EXISTS deposit_items ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + deposit_id INTEGER NOT NULL REFERENCES deposits(id) ON DELETE CASCADE, + sort_order INTEGER NOT NULL DEFAULT 0, + check_no TEXT, + bank_no TEXT, + payee TEXT, + memo TEXT, + amount REAL NOT NULL DEFAULT 0 +); + +CREATE INDEX IF NOT EXISTS idx_deposits_account ON deposits(account_id); +CREATE INDEX IF NOT EXISTS idx_deposit_items ON deposit_items(deposit_id); diff --git a/src/routes/deposit-pdf.js b/src/routes/deposit-pdf.js new file mode 100644 index 0000000..f9c60c5 --- /dev/null +++ b/src/routes/deposit-pdf.js @@ -0,0 +1,47 @@ +'use strict'; + +const express = require('express'); +const router = express.Router(); +const db = require('../db/database'); +const { generateDepositPdf } = require('../services/depositPdfService'); + +// POST /api/deposit-pdf +// Body: { depositId, type: 'slip' | 'report', mark_printed: true } +router.post('/', async (req, res) => { + const { depositId, type = 'slip', mark_printed = true } = req.body; + + if (!depositId) return res.status(400).json({ error: 'depositId is required.' }); + if (!['slip', 'report'].includes(type)) { + return res.status(400).json({ error: 'type must be "slip" or "report".' }); + } + + const deposit = db.prepare('SELECT * FROM deposits WHERE id = ?').get(depositId); + if (!deposit) return res.status(404).json({ error: 'Deposit not found.' }); + + const account = db.prepare('SELECT * FROM account WHERE id = ?').get(deposit.account_id); + if (!account) return res.status(404).json({ error: 'Account not found.' }); + + const items = db.prepare( + 'SELECT * FROM deposit_items WHERE deposit_id = ? ORDER BY sort_order ASC, id ASC' + ).all(depositId); + + try { + const pdfBuffer = await generateDepositPdf(account, deposit, items, type); + + if (mark_printed && type === 'slip') { + db.prepare('UPDATE deposits SET printed = 1 WHERE id = ?').run(depositId); + } + + res.set({ + 'Content-Type': 'application/pdf', + 'Content-Disposition': `inline; filename="deposit-${type}-${deposit.deposit_date}.pdf"`, + 'Content-Length': pdfBuffer.length, + }); + res.send(pdfBuffer); + } catch (err) { + console.error('Deposit PDF generation error:', err); + res.status(500).json({ error: err.message }); + } +}); + +module.exports = router; diff --git a/src/routes/deposits.js b/src/routes/deposits.js new file mode 100644 index 0000000..1ceb120 --- /dev/null +++ b/src/routes/deposits.js @@ -0,0 +1,144 @@ +'use strict'; + +const express = require('express'); +const router = express.Router(); +const db = require('../db/database'); + +// Helper: fetch deposit with items +function getDepositWithItems(id) { + const deposit = db.prepare('SELECT * FROM deposits WHERE id = ?').get(id); + if (!deposit) return null; + deposit.items = db.prepare( + 'SELECT * FROM deposit_items WHERE deposit_id = ? ORDER BY sort_order ASC, id ASC' + ).all(id); + return deposit; +} + +// GET /api/deposits?account_id=X +router.get('/', (req, res) => { + const { account_id } = req.query; + if (!account_id) return res.status(400).json({ error: 'account_id is required.' }); + + const deposits = db.prepare(` + SELECT d.*, COUNT(di.id) AS item_count, + COALESCE(SUM(di.amount), 0) AS checks_total + FROM deposits d + LEFT JOIN deposit_items di ON di.deposit_id = d.id + WHERE d.account_id = ? + GROUP BY d.id + ORDER BY d.deposit_date DESC, d.id DESC + `).all(account_id); + + res.json(deposits); +}); + +// GET /api/deposits/:id +router.get('/:id', (req, res) => { + const deposit = getDepositWithItems(req.params.id); + if (!deposit) return res.status(404).json({ error: 'Deposit not found.' }); + res.json(deposit); +}); + +// POST /api/deposits +router.post('/', (req, res) => { + const { account_id, deposit_date, currency, coin, cash_back, items } = req.body; + if (!account_id) return res.status(400).json({ error: 'account_id is required.' }); + if (!deposit_date) return res.status(400).json({ error: 'deposit_date is required.' }); + + const insert = db.transaction(() => { + const result = db.prepare(` + INSERT INTO deposits (account_id, deposit_date, currency, coin, cash_back) + VALUES (?, ?, ?, ?, ?) + `).run( + account_id, + deposit_date, + parseFloat(currency) || 0, + parseFloat(coin) || 0, + parseFloat(cash_back) || 0, + ); + + const depositId = result.lastInsertRowid; + + if (Array.isArray(items)) { + const stmt = db.prepare(` + INSERT INTO deposit_items (deposit_id, sort_order, check_no, bank_no, payee, memo, amount) + VALUES (?, ?, ?, ?, ?, ?, ?) + `); + items.forEach((item, i) => { + stmt.run( + depositId, i, + item.check_no || null, + item.bank_no || null, + item.payee || null, + item.memo || null, + parseFloat(item.amount) || 0, + ); + }); + } + + return depositId; + }); + + const depositId = insert(); + res.status(201).json(getDepositWithItems(depositId)); +}); + +// PUT /api/deposits/:id +router.put('/:id', (req, res) => { + const existing = db.prepare('SELECT id FROM deposits WHERE id = ?').get(req.params.id); + if (!existing) return res.status(404).json({ error: 'Deposit not found.' }); + + const { deposit_date, currency, coin, cash_back, items } = req.body; + if (!deposit_date) return res.status(400).json({ error: 'deposit_date is required.' }); + + const update = db.transaction(() => { + db.prepare(` + UPDATE deposits SET deposit_date = ?, currency = ?, coin = ?, cash_back = ? + WHERE id = ? + `).run( + deposit_date, + parseFloat(currency) || 0, + parseFloat(coin) || 0, + parseFloat(cash_back) || 0, + req.params.id, + ); + + if (Array.isArray(items)) { + db.prepare('DELETE FROM deposit_items WHERE deposit_id = ?').run(req.params.id); + const stmt = db.prepare(` + INSERT INTO deposit_items (deposit_id, sort_order, check_no, bank_no, payee, memo, amount) + VALUES (?, ?, ?, ?, ?, ?, ?) + `); + items.forEach((item, i) => { + stmt.run( + req.params.id, i, + item.check_no || null, + item.bank_no || null, + item.payee || null, + item.memo || null, + parseFloat(item.amount) || 0, + ); + }); + } + }); + + update(); + res.json(getDepositWithItems(req.params.id)); +}); + +// DELETE /api/deposits/:id +router.delete('/:id', (req, res) => { + const existing = db.prepare('SELECT id FROM deposits WHERE id = ?').get(req.params.id); + if (!existing) return res.status(404).json({ error: 'Deposit not found.' }); + // deposit_items deleted via ON DELETE CASCADE + db.prepare('DELETE FROM deposits WHERE id = ?').run(req.params.id); + res.status(204).end(); +}); + +// PATCH /api/deposits/:id/mark-printed +router.patch('/:id/mark-printed', (req, res) => { + db.prepare('UPDATE deposits SET printed = 1 WHERE id = ?').run(req.params.id); + res.json({ ok: true }); +}); + +module.exports = router; diff --git a/src/services/depositPdfService.js b/src/services/depositPdfService.js new file mode 100644 index 0000000..beb5839 --- /dev/null +++ b/src/services/depositPdfService.js @@ -0,0 +1,523 @@ +'use strict'; + +/** + * depositPdfService.js + * + * Generates two PDF types for a deposit: + * - Deposit Report: plain formatted document (Courier, monospaced) + * - Deposit Slip: precisely positioned 3.375" × 8.5" slip with Style A background, + * digit-column amounts, and a GnuMICR line rotated 90°. + * + * All measurements in inches; converted to points (× 72) for PDFKit. + * + * TMDC slip layout is hardcoded. Tune the LAYOUT constants below if fields + * print slightly off on physical stock. + */ + +const PDFDocument = require('pdfkit'); +const path = require('path'); +const fs = require('fs'); + +const PT = 72; // points per inch +const MICR_FONT_PATH = path.join(__dirname, '../../fonts/GnuMICR.ttf'); + +// ── Deposit Slip Layout Constants (inches) ──────────────────────────────────── +// Page is 3.375" wide × 8.5" tall (portrait). +// A 0.625" strip on the LEFT holds all rotated elements (MICR, deposit total, +// check count). The remaining 2.75" is the main form content. +const SL = { + W: 3.375, + H: 8.5, + + // Left rotated strip — right edge of reserved area + stripX: 0.625, + + // Content X start (inside the strip) + cX: 0.65, + + // ── Depositor / Bank block ──────────────────────────────────────────────── + depositorY: 0.28, // Y of first depositor line + bankX: 1.9, // X of bank name (right column) + + // ── Date ───────────────────────────────────────────────────────────────── + dateY: 1.38, // Y of DATE label + dateValueX: 0.92, // X where date value prints + + // ── Disclaimer ──────────────────────────────────────────────────────────── + disclaimerY: 1.56, + + // ── Amount grid ─────────────────────────────────────────────────────────── + gridTop: 1.72, // top border of grid + rowH: 0.175, // height of each row + + // Column positions (right edges, in inches from left of page) + colCentsR: 3.26, // right edge of cents column + colCentsW: 0.42, // width of cents column + colDollarSep: 0.08, // gap between dollars and cents columns + // dollars column right edge = colCentsR - colCentsW - colDollarSep + // dollars column width = 7 digit slots × digitW + + digitW: 0.115, // width of each digit slot (Courier 8pt ≈ 4.8pt + spacing) + centDigitW: 0.115, + + // Row Y offsets from gridTop (label baseline) + currencyRow: 1, // grid row index + coinRow: 2, + checksRow: 3, // "CHECKS:" label row (no amount on this row) + firstCheckRow: 4, // first numbered check row + maxChecks: 30, + + // Check number column X + checkNoX: 0.67, + checkNoW: 0.55, // max width for check number text + + // ── Footer ──────────────────────────────────────────────────────────────── + // "TOTAL $" row sits just below the last check row + // (computed dynamically from firstCheckRow + maxChecks) + + // ── Rotated left strip ──────────────────────────────────────────────────── + // Rotated text is drawn with doc.rotate(-90) centred in the strip. + // X positions below are distance from LEFT edge of page (strip area). + micrY: 8.3, // Y (on page) where rotated MICR baseline sits + micrX: 0.12, // X anchor for rotated MICR (left side of text after rotation) + + depTotalLabelY: 6.8, // Y where "DEPOSIT TOTAL $" rotated label baseline sits + depTotalAmtY: 5.6, // Y where deposit total digits start (reading upward) + depTotalX: 0.44, // X of rotated deposit total elements + + checkCountLabelY: 3.5, // Y of rotated "TOTAL ITEMS" label + checkCountValY: 2.8, // Y of rotated check count value + checkCountX: 0.44, + + // ── Style A background colours ─────────────────────────────────────────── + bgStripColor: '#d4c9a8', // beige shaded strip (left margin) + bgLineColor: '#888888', // grid lines + bgLabelColor: '#444444', // row labels (CURRENCY, COIN, etc.) + bgHeaderColor: '#000000', // DEPOSIT TICKET header +}; + +// Grid row Y baseline (from top of page, in inches) +function rowY(rowIndex) { + return SL.gridTop + rowIndex * SL.rowH + SL.rowH * 0.7; // text baseline within row +} + +function rowTopY(rowIndex) { + return SL.gridTop + rowIndex * SL.rowH; +} + +// ── Report PDF ──────────────────────────────────────────────────────────────── + +function generateDepositReport(account, deposit, items) { + return new Promise((resolve, reject) => { + const doc = new PDFDocument({ + size: 'LETTER', + margins: { top: 36, bottom: 36, left: 54, right: 54 }, + autoFirstPage: true, + }); + + const buffers = []; + doc.on('data', c => buffers.push(c)); + doc.on('end', () => resolve(Buffer.concat(buffers))); + doc.on('error', reject); + + const cashTotal = (deposit.currency || 0) + (deposit.coin || 0); + const checksTotal = items.reduce((s, i) => s + (i.amount || 0), 0); + const subTotal = cashTotal + checksTotal; + const depositTotal = subTotal - (deposit.cash_back || 0); + + const fmt = n => n.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ','); + const W = 504; // usable width in points + + // Header + doc.font('Helvetica-Bold').fontSize(14) + .text('Deposit Report', { align: 'center' }); + doc.font('Helvetica').fontSize(10) + .text(deposit.deposit_date, { align: 'right' }); + doc.moveDown(0.5); + + // Two-column depositor / bank block + const colW = W / 2; + const startY = doc.y; + doc.font('Helvetica-Bold').fontSize(9) + .text(account.company1 || '', 54, startY, { width: colW }); + if (account.company2) doc.font('Helvetica').fontSize(9).text(account.company2, 54, doc.y, { width: colW }); + if (account.company3) doc.font('Helvetica').fontSize(9).text(account.company3, 54, doc.y, { width: colW }); + + const bankX = 54 + colW; + doc.font('Helvetica-Bold').fontSize(9) + .text(account.bank_name || '', bankX, startY, { width: colW }); + if (account.bank_info1) doc.font('Helvetica').fontSize(9).text(account.bank_info1, bankX, doc.y, { width: colW }); + if (account.bank_info2) doc.font('Helvetica').fontSize(9).text(account.bank_info2, bankX, doc.y, { width: colW }); + + doc.moveDown(1); + + // Cash summary + doc.font('Courier').fontSize(9); + const lw = 200; + const rx = 54 + W - 80; + function reportLine(label, value) { + doc.font('Courier').fontSize(9) + .text(label + ':', 54, doc.y, { width: lw, continued: false }); + doc.text(fmt(value), rx, doc.y - doc.currentLineHeight(), { width: 80, align: 'right' }); + } + reportLine('Currency', deposit.currency || 0); + reportLine('Coin', deposit.coin || 0); + reportLine('Cash Total', cashTotal); + doc.moveDown(0.3); + + // Check grid header + doc.moveTo(54, doc.y).lineTo(54 + W, doc.y).lineWidth(0.5).stroke(); + doc.moveDown(0.2); + doc.font('Courier-Bold').fontSize(8) + .text('#', 54, doc.y, { width: 20 }) + .text('Check#', 74, doc.y - doc.currentLineHeight(), { width: 55 }) + .text('Bank#', 134, doc.y - doc.currentLineHeight(), { width: 55 }) + .text('Received From', 194, doc.y - doc.currentLineHeight(), { width: 140 }) + .text('Memo', 340, doc.y - doc.currentLineHeight(), { width: 120 }) + .text('Amount', 460, doc.y - doc.currentLineHeight(), { width: 98, align: 'right' }); + doc.moveDown(0.2); + doc.moveTo(54, doc.y).lineTo(54 + W, doc.y).lineWidth(0.5).stroke(); + doc.moveDown(0.2); + + // Check rows + doc.font('Courier').fontSize(8); + items.forEach((item, i) => { + const y = doc.y; + doc.text(String(i + 1) + '.', 54, y, { width: 20 }); + doc.text(item.check_no || '', 74, y, { width: 55 }); + doc.text(item.bank_no || '', 134, y, { width: 55 }); + doc.text(item.payee || '', 194, y, { width: 140, ellipsis: true }); + doc.text(item.memo || '', 340, y, { width: 120, ellipsis: true }); + doc.text(fmt(item.amount || 0), 460, y, { width: 98, align: 'right' }); + }); + + doc.moveDown(0.3); + doc.moveTo(54, doc.y).lineTo(54 + W, doc.y).lineWidth(0.5).stroke(); + doc.moveDown(0.3); + + // Totals block + function totalLine(label, value, bold) { + const y = doc.y; + doc.font(bold ? 'Courier-Bold' : 'Courier').fontSize(9) + .text(label + ':', 54, y, { width: lw }) + .text(fmt(value), rx, y, { width: 80, align: 'right' }); + } + totalLine('Checks Total', checksTotal); + totalLine('Subtotal', subTotal); + totalLine('Cash Back', deposit.cash_back || 0); + doc.moveDown(0.2); + doc.moveTo(rx - 10, doc.y).lineTo(54 + W, doc.y).lineWidth(0.5).stroke(); + doc.moveDown(0.2); + totalLine('Deposit Total', depositTotal, true); + + doc.end(); + }); +} + +// ── Slip PDF ────────────────────────────────────────────────────────────────── + +function generateDepositSlip(account, deposit, items) { + return new Promise((resolve, reject) => { + const hasMicrFont = fs.existsSync(MICR_FONT_PATH); + + const doc = new PDFDocument({ + size: [SL.W * PT, SL.H * PT], + margins: { top: 0, bottom: 0, left: 0, right: 0 }, + autoFirstPage: true, + }); + + if (hasMicrFont) doc.registerFont('MICR', MICR_FONT_PATH); + + const buffers = []; + doc.on('data', c => buffers.push(c)); + doc.on('end', () => resolve(Buffer.concat(buffers))); + doc.on('error', reject); + + const cashTotal = (deposit.currency || 0) + (deposit.coin || 0); + const checksTotal = items.reduce((s, i) => s + (i.amount || 0), 0); + const subTotal = cashTotal + checksTotal; + const depositTotal = subTotal - (deposit.cash_back || 0); + const checkCount = items.length; + + const totalRows = SL.firstCheckRow + SL.maxChecks; // last row index + const totalRowY_ = rowTopY(totalRows); // top of TOTAL $ row + const gridBottom = totalRowY_ + SL.rowH; + + // ── Style A background ────────────────────────────────────────────────── + + // Left beige strip + doc.rect(0, 0, SL.stripX * PT, SL.H * PT) + .fill(SL.bgStripColor); + + // Outer border + doc.rect(SL.stripX * PT, 0, (SL.W - SL.stripX) * PT, SL.H * PT) + .lineWidth(1).stroke('#000000'); + + // Vertical divider between check# and amount columns + const dividerX = (SL.colCentsR - SL.colCentsW - SL.colDollarSep - 7 * SL.digitW) * PT; + const gridTopPt = SL.gridTop * PT; + const gridBotPt = gridBottom * PT; + doc.moveTo(dividerX, gridTopPt).lineTo(dividerX, gridBotPt).lineWidth(0.5).stroke(SL.bgLineColor); + + // Vertical divider between dollars and cents + const dollarsCentsX = (SL.colCentsR - SL.colCentsW - SL.colDollarSep) * PT; + doc.moveTo(dollarsCentsX, gridTopPt).lineTo(dollarsCentsX, gridBotPt).lineWidth(0.5).stroke(SL.bgLineColor); + + // Right border of cents column + doc.moveTo(SL.colCentsR * PT, gridTopPt).lineTo(SL.colCentsR * PT, gridBotPt).lineWidth(0.5).stroke(SL.bgLineColor); + + // Column header labels + doc.font('Helvetica').fontSize(6).fillColor(SL.bgLabelColor); + const hdrY = (SL.gridTop - 0.1) * PT; + doc.text('DOLLARS', dollarsCentsX - 7 * SL.digitW * PT, hdrY, + { width: 7 * SL.digitW * PT, align: 'center', lineBreak: false }); + doc.text('CENTS', (SL.colCentsR - SL.colCentsW) * PT, hdrY, + { width: SL.colCentsW * PT, align: 'center', lineBreak: false }); + + // Horizontal grid lines for all rows + for (let r = 0; r <= totalRows + 1; r++) { + const y = rowTopY(r) * PT; + doc.moveTo(SL.stripX * PT, y).lineTo(SL.colCentsR * PT, y) + .lineWidth(r === 0 || r === totalRows + 1 ? 0.75 : 0.3) + .stroke(SL.bgLineColor); + } + + // Row labels + doc.font('Courier').fontSize(7).fillColor(SL.bgLabelColor); + function rowLabel(label, rowIdx) { + doc.text(label, SL.cX * PT, rowY(rowIdx) * PT - 5, { lineBreak: false }); + } + rowLabel('CURRENCY', SL.currencyRow); + rowLabel('COIN', SL.coinRow); + rowLabel('CHECKS:', SL.checksRow); + + // Numbered check rows + doc.font('Courier').fontSize(6).fillColor(SL.bgLabelColor); + for (let i = 0; i < SL.maxChecks; i++) { + const r = SL.firstCheckRow + i; + doc.text(String(i + 1), SL.cX * PT, rowY(r) * PT - 4, + { width: 14, align: 'right', lineBreak: false }); + } + + // TOTAL $ row label + doc.font('Courier-Bold').fontSize(7).fillColor('#000000'); + doc.text('TOTAL $', SL.cX * PT, rowY(totalRows) * PT - 5, { lineBreak: false }); + + // Disclaimer text (below date, above grid) + doc.font('Helvetica').fontSize(5).fillColor('#666666') + .text( + 'DEPOSITS MAY NOT BE AVAILABLE FOR IMMEDIATE WITHDRAWAL', + SL.cX * PT, SL.disclaimerY * PT, + { width: (SL.W - SL.cX - 0.05) * PT, lineBreak: false } + ); + + // Bottom disclaimer (inside form, near total row) + doc.font('Helvetica').fontSize(5).fillColor('#666666') + .text( + 'Checks and other items are received for deposit subject to the provisions of the Uniform Commercial Code or any applicable collection agreements.', + SL.cX * PT, (gridBottom + 0.05) * PT, + { width: (SL.W - SL.cX - 0.1) * PT } + ); + + // DEPOSIT TICKET header + doc.font('Helvetica-Bold').fontSize(9).fillColor(SL.bgHeaderColor); + const headerText = 'D E P O S I T T I C K E T'; + doc.text(headerText, SL.cX * PT, 0.08 * PT, + { width: (SL.W - SL.cX - 0.05) * PT, align: 'center', lineBreak: false }); + + // Vertical separator between depositor and bank columns + const midX = ((SL.cX + SL.bankX) / 2) * PT; + doc.moveTo(midX, 0.18 * PT).lineTo(midX, (SL.dateY - 0.05) * PT) + .lineWidth(0.5).stroke('#aaaaaa'); + + // ── Depositor / Bank block ────────────────────────────────────────────── + doc.font('Helvetica-Bold').fontSize(8).fillColor('#000000') + .text(account.company1 || '', SL.cX * PT, SL.depositorY * PT, + { lineBreak: false }); + let depY = SL.depositorY + 0.12; + doc.font('Helvetica').fontSize(7); + [account.company2, account.company3, account.company4].forEach(line => { + if (line) { + doc.text(line, SL.cX * PT, depY * PT, { lineBreak: false }); + depY += 0.10; + } + }); + + doc.font('Helvetica-Bold').fontSize(8).fillColor('#000000') + .text(account.bank_name || '', SL.bankX * PT, SL.depositorY * PT, + { lineBreak: false }); + let bnkY = SL.depositorY + 0.12; + doc.font('Helvetica').fontSize(7); + [account.bank_info1, account.bank_info2].forEach(line => { + if (line) { + doc.text(line, SL.bankX * PT, bnkY * PT, { lineBreak: false }); + bnkY += 0.10; + } + }); + + // ── Date ─────────────────────────────────────────────────────────────── + doc.font('Helvetica').fontSize(7).fillColor(SL.bgLabelColor) + .text('DATE', SL.cX * PT, SL.dateY * PT, { lineBreak: false }); + // Underline + const dateUnderX1 = (SL.cX + 0.33) * PT; + const dateUnderX2 = (SL.dateValueX + 0.85) * PT; + const dateUnderY = (SL.dateY + 0.01) * PT; + doc.moveTo(dateUnderX1, dateUnderY).lineTo(dateUnderX2, dateUnderY) + .lineWidth(0.5).stroke('#000000'); + doc.font('Courier').fontSize(8).fillColor('#000000') + .text(deposit.deposit_date || '', (SL.cX + 0.36) * PT, (SL.dateY - 0.03) * PT, + { lineBreak: false }); + + // ── Amount data ───────────────────────────────────────────────────────── + // Draw digits in fixed-width slots, right-aligned in the dollars column + const dollarsRightX = (SL.colCentsR - SL.colCentsW - SL.colDollarSep); + + function drawAmountRow(amount, rowIdx) { + const y = (rowY(rowIdx) - 0.015) * PT; + doc.font('Courier').fontSize(8).fillColor('#000000'); + drawDigitAmount(doc, amount, dollarsRightX, y); + } + + drawAmountRow(deposit.currency || 0, SL.currencyRow); + drawAmountRow(deposit.coin || 0, SL.coinRow); + + // Check items + items.slice(0, SL.maxChecks).forEach((item, i) => { + const r = SL.firstCheckRow + i; + const y = (rowY(r) - 0.015) * PT; + + // Check number + if (item.check_no) { + doc.font('Courier').fontSize(7).fillColor('#000000') + .text(String(item.check_no).slice(0, 8), + (SL.cX + 0.16) * PT, y, + { width: SL.checkNoW * PT, lineBreak: false }); + } + drawAmountRow(item.amount || 0, r); + }); + + // Total $ row + drawAmountRow(checksTotal, totalRows); + + // ── Rotated left strip elements ───────────────────────────────────────── + + // MICR line (routing + account, no check number for deposits) + const routing = (account.routing_number || '').replace(/\D/g, ''); + const acctNo = (account.account_number || '').replace(/[^0-9]/g, ''); + const micrStr = `A${routing}A ${acctNo}C`; + + doc.save(); + doc.translate(SL.micrX * PT, SL.micrY * PT); + doc.rotate(-90); + if (hasMicrFont) { + doc.font('MICR').fontSize(11).fillColor('#000000') + .text(micrStr, 0, 0, { lineBreak: false }); + } else { + doc.font('Courier').fontSize(8).fillColor('#000000') + .text(micrStr, 0, 0, { lineBreak: false }); + } + doc.restore(); + + // Rotated "DEPOSIT TOTAL $" label + amount + doc.save(); + doc.translate(SL.depTotalX * PT, SL.depTotalLabelY * PT); + doc.rotate(-90); + doc.font('Helvetica').fontSize(6).fillColor(SL.bgLabelColor) + .text('DEPOSIT TOTAL $', 0, 0, { lineBreak: false }); + doc.restore(); + + // Deposit total digits (rotated, spaced) + drawRotatedDigitAmount(doc, depositTotal, SL.depTotalX, SL.depTotalAmtY); + + // Rotated "TOTAL ITEMS" label + count + doc.save(); + doc.translate(SL.checkCountX * PT, SL.checkCountLabelY * PT); + doc.rotate(-90); + doc.font('Helvetica').fontSize(6).fillColor(SL.bgLabelColor) + .text('TOTAL ITEMS', 0, 0, { lineBreak: false }); + doc.restore(); + + doc.save(); + doc.translate(SL.checkCountX * PT, SL.checkCountValY * PT); + doc.rotate(-90); + doc.font('Courier').fontSize(9).fillColor('#000000') + .text(String(checkCount), 0, 0, { lineBreak: false }); + doc.restore(); + + doc.end(); + }); +} + +// ── Amount rendering helpers ────────────────────────────────────────────────── + +/** + * Draw a dollar amount in digit-column format (each digit in its own fixed slot). + * @param {PDFDocument} doc + * @param {number} amount e.g. 9224.45 + * @param {number} dollarsRightX right edge of dollars column (inches) + * @param {number} y PDFKit Y in points (absolute) + */ +function drawDigitAmount(doc, amount, dollarsRightX, y) { + const totalCents = Math.round(Math.abs(amount) * 100); + const dollars = Math.floor(totalCents / 100); + const cents = totalCents % 100; + + const NSLOTS = 7; // dollar digit slots + const dolStr = dollars === 0 ? '' : String(dollars); + const ctStr = String(cents).padStart(2, '0'); + + // Dollars: place each digit right-to-left + const dW = SL.digitW * PT; + const rightPt = dollarsRightX * PT; + for (let i = 0; i < NSLOTS; i++) { + const digitIdx = dolStr.length - 1 - i; + if (digitIdx < 0) break; + const x = rightPt - (i + 1) * dW + (dW - 4.8) / 2; // centre 4.8pt char in slot + doc.text(dolStr[digitIdx], x, y, { lineBreak: false }); + } + + // Cents: two digits in cents column + const SL_colCentsR = SL.colCentsR * PT; + const cW = SL.centDigitW * PT; + for (let i = 0; i < 2; i++) { + const x = SL_colCentsR - (2 - i) * cW + (cW - 4.8) / 2; + doc.text(ctStr[i], x, y, { lineBreak: false }); + } +} + +/** + * Draw deposit total digits rotated 90° in the left strip. + * Each digit is stacked vertically (reading downward when viewed in portrait). + */ +function drawRotatedDigitAmount(doc, amount, stripX, startY) { + const totalCents = Math.round(Math.abs(amount) * 100); + const dollars = Math.floor(totalCents / 100); + const cents = totalCents % 100; + const fullStr = String(dollars) + String(cents).padStart(2, '0'); + const spacing = 0.16; // inches between each digit + + doc.font('Courier').fontSize(9).fillColor('#000000'); + fullStr.split('').forEach((ch, i) => { + doc.save(); + doc.translate(stripX * PT, (startY - i * spacing) * PT); + doc.rotate(-90); + doc.text(ch, 0, 0, { lineBreak: false }); + doc.restore(); + }); +} + +// ── Main export ─────────────────────────────────────────────────────────────── + +/** + * @param {Object} account - account row + * @param {Object} deposit - deposit row + * @param {Array} items - deposit_items rows + * @param {string} type - 'slip' | 'report' + * @returns {Promise} + */ +function generateDepositPdf(account, deposit, items, type) { + if (type === 'report') return generateDepositReport(account, deposit, items); + return generateDepositSlip(account, deposit, items); +} + +module.exports = { generateDepositPdf };