526 lines
22 KiB
JavaScript
526 lines
22 KiB
JavaScript
'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 printed on an 8.5"×11"
|
||
* letter page for trimming. Style A background (grid lines drawn
|
||
* server-side). Digit-column amounts. GnuMICR line in left strip.
|
||
*
|
||
* All measurements in inches; converted to points (× 72) for PDFKit.
|
||
*
|
||
* Left strip elements use rotate(90) so text reads when tilting head LEFT (same
|
||
* orientation as Qslip/standard bank deposit slips).
|
||
*
|
||
* Tune the SL 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) ────────────────────────────────────
|
||
// Slip is 3.375" wide × 8.5" tall, printed on an 8.5"×11" letter page.
|
||
// slipX offsets the slip horizontally on the letter page (0 = left edge).
|
||
// A 0.625" strip on the LEFT holds all rotated strip elements.
|
||
// Strip elements use rotate(90): text reads when tilting head LEFT (Qslip orientation).
|
||
// Strip element Y positions are the TOP of each element; text flows downward.
|
||
const SL = {
|
||
W: 3.375,
|
||
H: 8.5,
|
||
|
||
// Horizontal offset of the slip on the 8.5"×11" letter page
|
||
slipX: 0,
|
||
|
||
// Left rotated strip — width of reserved area
|
||
stripX: 0.625,
|
||
|
||
// Content X start (right of strip)
|
||
cX: 0.65,
|
||
|
||
// ── Depositor block ───────────────────────────────────────────────────────
|
||
depositorY: 0.28, // Y of company name (first depositor line)
|
||
|
||
// ── 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 slip)
|
||
colCentsR: 3.26, // right edge of cents column
|
||
colCentsW: 0.42, // width of cents column
|
||
colDollarSep: 0.08, // gap between dollars and cents columns
|
||
|
||
digitW: 0.115, // width per dollar digit slot
|
||
centDigitW: 0.115,
|
||
|
||
// Row indices
|
||
currencyRow: 1,
|
||
coinRow: 2,
|
||
checksRow: 3, // "CHECKS:" label row — no amount
|
||
firstCheckRow: 4,
|
||
maxChecks: 30,
|
||
|
||
checkNoX: 0.67,
|
||
checkNoW: 0.55,
|
||
|
||
// ── Rotated left strip (rotate(90): text flows downward, reads tilt-left) ─
|
||
// All strip X positions are centered in the 0.625" strip.
|
||
// Y positions are the TOP anchor; text flows DOWNWARD from there.
|
||
stripCenterX: 0.32, // ≈ stripX/2; tune to center text in strip width
|
||
|
||
// Order top-to-bottom: DEPOSIT TOTAL label → digits → MICR → TOTAL ITEMS → count
|
||
depTotalLabelY: 1.0, // "DEPOSIT TOTAL $" label start
|
||
depTotalAmtY: 2.0, // deposit total digits start (each 0.16" apart downward)
|
||
micrY: 3.4, // MICR starts here, flows ~2.5" downward
|
||
checkCountLabelY: 6.1, // "TOTAL ITEMS" label start
|
||
checkCountValY: 6.9, // check count value start
|
||
|
||
// ── Colours ───────────────────────────────────────────────────────────────
|
||
bgLineColor: '#888888',
|
||
bgLabelColor: '#444444',
|
||
bgHeaderColor: '#000000',
|
||
};
|
||
|
||
// 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);
|
||
|
||
// Letter page — slip sits at (slipX, 0); remaining space is blank for trimming
|
||
const doc = new PDFDocument({
|
||
size: 'LETTER',
|
||
layout: 'portrait',
|
||
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;
|
||
const totalRowY_ = rowTopY(totalRows);
|
||
const gridBottom = totalRowY_ + SL.rowH;
|
||
|
||
// Offset all slip drawing to its position on the letter page
|
||
doc.save();
|
||
doc.translate(SL.slipX * PT, 0);
|
||
|
||
// ── Style A background ──────────────────────────────────────────────────
|
||
|
||
// No fill on left strip (white/transparent per user preference)
|
||
|
||
// Outer border of main content area (right of strip)
|
||
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 (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 });
|
||
|
||
// Top disclaimer (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 (below grid)
|
||
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)
|
||
.text('D E P O S I T T I C K E T', SL.cX * PT, 0.08 * PT,
|
||
{ width: (SL.W - SL.cX - 0.05) * PT, align: 'center', lineBreak: false });
|
||
|
||
// ── Depositor block — account info, then bank info stacked below ────────
|
||
doc.font('Helvetica-Bold').fontSize(8).fillColor('#000000')
|
||
.text(account.company1 || '', SL.cX * PT, SL.depositorY * PT,
|
||
{ lineBreak: false });
|
||
let blockY = 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, blockY * PT, { lineBreak: false });
|
||
blockY += 0.10;
|
||
}
|
||
});
|
||
|
||
// Bank info — stacked below depositor, small gap
|
||
blockY += 0.06;
|
||
doc.font('Helvetica-Bold').fontSize(8).fillColor('#000000')
|
||
.text(account.bank_name || '', SL.cX * PT, blockY * PT, { lineBreak: false });
|
||
blockY += 0.12;
|
||
doc.font('Helvetica').fontSize(7);
|
||
[account.bank_info1, account.bank_info2].forEach(line => {
|
||
if (line) {
|
||
doc.text(line, SL.cX * PT, blockY * PT, { lineBreak: false });
|
||
blockY += 0.10;
|
||
}
|
||
});
|
||
|
||
// ── Date ───────────────────────────────────────────────────────────────
|
||
doc.font('Helvetica').fontSize(7).fillColor(SL.bgLabelColor)
|
||
.text('DATE', SL.cX * PT, SL.dateY * PT, { lineBreak: false });
|
||
// Underline (positioned lower than the label text)
|
||
const dateUnderX1 = (SL.cX + 0.33) * PT;
|
||
const dateUnderX2 = (SL.dateValueX + 0.85) * PT;
|
||
const dateUnderY = (SL.dateY + 0.09) * 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.01) * PT,
|
||
{ lineBreak: false });
|
||
|
||
// ── Amount data ─────────────────────────────────────────────────────────
|
||
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);
|
||
|
||
items.slice(0, SL.maxChecks).forEach((item, i) => {
|
||
const r = SL.firstCheckRow + i;
|
||
const y = (rowY(r) - 0.015) * PT;
|
||
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);
|
||
});
|
||
|
||
drawAmountRow(checksTotal, totalRows);
|
||
|
||
// ── Rotated left strip elements ─────────────────────────────────────────
|
||
// All elements use rotate(90): text flows downward on the page, which reads
|
||
// correctly when you tilt your head to the LEFT (standard bank deposit orientation).
|
||
// stripCenterX centers the text baseline within the 0.625" strip.
|
||
|
||
const routing = (account.routing_number || '').replace(/\D/g, '');
|
||
const acctNo = (account.account_number || '').replace(/[^0-9]/g, '');
|
||
const micrStr = `A${routing}A ${acctNo}C`;
|
||
|
||
// MICR line — centered in strip, near bottom
|
||
doc.save();
|
||
doc.translate(SL.stripCenterX * 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();
|
||
|
||
// "DEPOSIT TOTAL $" label
|
||
doc.save();
|
||
doc.translate(SL.stripCenterX * 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 (includes decimal point)
|
||
drawRotatedDigitAmount(doc, depositTotal, SL.stripCenterX, SL.depTotalAmtY);
|
||
|
||
// "TOTAL ITEMS" label
|
||
doc.save();
|
||
doc.translate(SL.stripCenterX * PT, SL.checkCountLabelY * PT);
|
||
doc.rotate(90);
|
||
doc.font('Helvetica').fontSize(6).fillColor(SL.bgLabelColor)
|
||
.text('TOTAL ITEMS', 0, 0, { lineBreak: false });
|
||
doc.restore();
|
||
|
||
// Check count value
|
||
doc.save();
|
||
doc.translate(SL.stripCenterX * PT, SL.checkCountValY * PT);
|
||
doc.rotate(90);
|
||
doc.font('Courier').fontSize(9).fillColor('#000000')
|
||
.text(String(checkCount), 0, 0, { lineBreak: false });
|
||
doc.restore();
|
||
|
||
doc.restore(); // end slip position translate
|
||
|
||
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 in the left strip using rotate(90).
|
||
* Each character is stacked top-to-bottom on the page; reads correctly
|
||
* when tilting head left. Includes a '.' decimal separator.
|
||
*/
|
||
function drawRotatedDigitAmount(doc, amount, stripCenterX, startY) {
|
||
const totalCents = Math.round(Math.abs(amount) * 100);
|
||
const dollars = Math.floor(totalCents / 100);
|
||
const cents = totalCents % 100;
|
||
// Include decimal point between dollars and cents
|
||
const fullStr = String(dollars) + '.' + String(cents).padStart(2, '0');
|
||
const spacing = 0.16; // inches between each character
|
||
|
||
doc.font('Courier').fontSize(9).fillColor('#000000');
|
||
fullStr.split('').forEach((ch, i) => {
|
||
doc.save();
|
||
doc.translate(stripCenterX * 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<Buffer>}
|
||
*/
|
||
function generateDepositPdf(account, deposit, items, type) {
|
||
if (type === 'report') return generateDepositReport(account, deposit, items);
|
||
return generateDepositSlip(account, deposit, items);
|
||
}
|
||
|
||
module.exports = { generateDepositPdf };
|