Generates advertising invoices as PDFs, styled to match poly.rpi.edu: the masthead logo, Raleway throughout, a Minion Pro headline, horizontal hairlines only, and red reserved for the logo.
Invoices are payable by cash or check. You email the PDF; they mail payment with the detached remittance slip; you record it here.
It's a terminal program — there's no window or app to click. Open Terminal, go to this folder once, and stay there:
cd ~/Code/businessThen run the commands below. Everything writes into out/.
Requirements are Node and either Google Chrome or Xcode. Nothing to install — no
npm install, no dependencies. Chrome is used headlessly to print the PDF; if yours
lives somewhere unusual, set CHROME_PATH. With no Chrome-family browser installed,
the PDF is printed through macOS's own WebKit instead, via a small Swift helper
(lib/html2pdf.swift) that is compiled into .build/ the first time it's needed.
config.json holds everything about you. The check details are set:
defaultNotes— the note copied into each new invoice; carries "All rates subject to change"paymentMethods—Cash or checkpayableTo—Rensselaer Polytechnic Institute(checks only; cash has no payee)checkMemoTemplate—The Polytechnic ({number});{number}is replaced per invoice so a payment can be matched to what it settlesremitTo— the mail-to block, ATTN: Nicole HollisterfromAddress— the shorter address in the "From" column
Still unverified: billingEmail (ads@poly.rpi.edu), billingPhone, taxId, and
taxRate, which is 0. New York generally does not tax advertising services, but
confirm before assuming it for print placements.
defaultTermsDays is 30, which prints as "Net 30" — the balance is due 30 days
after the invoice date. Set it to 0 for "Due on receipt", or override per invoice
with termsDays.
node bin/invoice.js new eighth-street-books # scaffold data/invoices/1002.json
node bin/invoice.js render 1002 # write out/Poly-Invoice-1002-….pdf
node bin/invoice.js render 1002 --open # …and open it
node bin/invoice.js render --all # re-render everything
node bin/invoice.js list # who still owes you money
node bin/invoice.js paid 1002 --check 8871 # record a checkrender --html writes HTML instead of PDF, which is faster when you're
adjusting the design.
config.json your details — the from-address, who checks are payable to
data/clients.json advertisers, keyed by a short slug
data/rates.json the rate card — ad sizes and per-square-inch rates
data/invoices/<n>.json one file per invoice; the number is the filename
lib/template.js the design (CSS + HTML)
lib/build.js totals, dates, formatting
lib/pdf.js headless Chrome, or the WebKit fallback
lib/html2pdf.swift the WebKit fallback's helper
out/ generated PDFs (gitignored)
assets/ logo + fonts, copied from the poly repo
{
"number": "1001",
"date": "2026-09-09",
"client": "eighth-street-books",
"termsDays": 30,
"ad": {
"size": "full",
"category": "local",
"run": "Vol. CXLIV Ed. IV · October 9, 2026"
},
"notes": "Ad artwork due five business days before each issue date.",
"paid": null
}An invoice bills exactly one ad, priced off the rate card in data/rates.json:
area in square inches × the category's rate per square inch. size is one of full,
half, quarter, eighth, sixteenth, variable; category is one of national,
local, institute, clubs. For an odd size, add height and width (inches) to
override the card's dimensions. run is the issue and date.
The bold heading is the category and size ("Local Business Half Page"). Under it sit
the dimensions and the issue. The table row reads, left to right: the category's unit
("Local Business square inch"), the unit price, the quantity in square inches, and
the amount. Optional lines add further rows (aspect, detail, amount): a
negative amount is a discount, 0 prints as "Included". Leave out size and the
invoice is priced from lines alone. name overrides the heading.
Set "client" to a key from data/clients.json, or inline the whole object.
notes is the paragraph under the payment block. new seeds it from defaultNotes
in config.json, so the rates-subject-to-change line is there by default; edit it per
invoice as needed.
Once paid is filled in, the same file renders as a receipt instead: the kicker
becomes "Advertising Receipt", the total reads "Paid in full" with the check number,
and the mail-a-check block disappears. So you can send the same PDF back as confirmation.
new takes the highest existing number and adds one, starting at 1001. Invoice numbers
should stay sequential and should never be reused — keep the JSON files even after
an invoice is paid, and don't renumber.
One page: the invoice, then a payment slip below a dashed tear line that bleeds to both edges of the sheet, marked with scissors. The advertiser cuts it off and mails it with the cash or check so the payment can be matched to an invoice — it repeats the advertiser, the invoice number, the amount due, the mailing address, and gives blanks for the amount enclosed and the check number.
For the tear line to reach the paper's edge, @page carries no margin and the page
margins sit on body instead. That is only correct while the invoice is one page —
a second page would start hard against the top edge. The page guard below is what
keeps that honest, so don't ignore it.
A receipt (an invoice with paid filled in) drops the slip.
The page is budgeted to about 780px of its 944px content area. render counts pages
and warns if it spills, usually from a long notes line or many ad lines.
To see where the space goes, temporarily add this to a rendered HTML file and open it:
document.querySelectorAll("header,.parties,table,.totals,.payment,.stub")
.forEach(e => console.log(e.className, e.getBoundingClientRect().height));The content area is 944px tall at 96dpi.
The page carries horizontal rules only — no vertical ones, no boxes, no fills. The
slip's tear line is the one dashed rule; the fill-in blanks are underlines. There is
no page footer and no terms block.
Hierarchy comes from two weights; all text is black.
Red (#D6001C) appears only in the masthead logo.
Fonts and the logo are copied into assets/ from ../poly/public/. If the site's
brand changes, re-copy them.
The invoice carries no terms. This wording was drafted for it and is kept here in case it belongs somewhere else later — a rate card, an insertion order, or the ad page on the site. It is boilerplate modelled on how newspaper rate cards are normally written, not legal advice, and it asserts protections on behalf of RPI and the Union, so it should be reviewed before being used anywhere binding.
All rates are subject to change. Advertisements are printed only in the issues listed above; no additional insertions are made without a new order. The Polytechnic reserves the right to refuse, reject, cancel, or omit any advertisement at any time, and to label any advertisement as such. Position is not guaranteed unless separately purchased. The advertiser represents that the submitted material is accurate, lawful, and does not infringe the rights of any third party, and agrees to hold The Polytechnic, the Rensselaer Union, and Rensselaer Polytechnic Institute harmless from any claim arising out of its publication. Liability for any error is limited to the cost of the space occupied by the error, and no allowance is made for errors that do not materially affect the value of the advertisement. Accounts not paid within the stated terms may be referred for collection. Cash payments are made at the sender’s own risk; The Polytechnic is not responsible for cash lost in transit and can credit an account only for payment actually received.