Document ledger reprints, optional dueDate, Limmud FSU invoice data

Add --print to regenerate PDFs from invoices.json without ledger writes.
Support optional dueDate on ledger rows (default remains invoice date + 14 days)
and persist dueDate when saving new production rows.

Data: limmud_fsu_canada client with invoiceFileName, cruise_event_av_it
product, 2026-LFSU01 sample invoice, Levkin contact fields in sender.json.

README and project.md describe CLI, schema, and May 2026 changelog.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
ilia 2026-05-09 12:07:54 -04:00
parent 0bec2d8e6f
commit 0cb8ad3d11
7 changed files with 626 additions and 73 deletions

164
README.md
View File

@ -1,120 +1,150 @@
# Invoice Generator # Invoice Generator
A powerful and flexible command-line tool for generating PDF invoices from simple JSON data files. It features an interactive mode for ease of use and a robust, non-interactive mode for scripting and automation. A command-line tool for generating professional PDF invoices from JSON data files. Features interactive and non-interactive modes with preview-first workflow.
## Features ## Quick Start
- **Interactive & Non-Interactive Modes**: Use the simple interactive wizard (`npm start`) or pass arguments directly for scripted use. 1. **Install dependencies:**
- **Safe Preview-First Workflow**: Always generates a development preview first. You must confirm before the official invoice is created and recorded, preventing accidental invoice generation.
- **Data-Driven**: Senders, clients, and products are managed in simple `.json` files, making them easy to update without changing code.
- **Automatic Invoice Numbering**: Invoice numbers are automatically incremented based on the year and existing records. The sequence resets annually.
- **Organized Output**: Generated PDFs are neatly stored in folders organized by sender (`invoice/<sender_name>/`).
- **Unit Tested**: Core logic is verified with unit tests using Jest.
## Prerequisites
- [Node.js](https://nodejs.org/) (v16 or higher recommended)
- [npm](https://www.npmjs.com/) (comes with Node.js)
## Setup
1. Clone the repository or download the source code.
2. Open your terminal in the project directory.
3. Install the dependencies:
```bash ```bash
npm install npm install
``` ```
## Usage 2. **Generate an invoice:**
```bash
npm start
```
There are two primary ways to generate an invoice: ## Usage Options
### 1. Interactive Mode (Recommended for most users) ### Interactive Mode (Recommended)
Run the interactive wizard. The tool will guide you through selecting a sender, client, and one or more products.
```bash ```bash
npm start npm start
``` ```
The script will first generate a **development preview** PDF. After you review it, you will be prompted to confirm whether you want to create the final **production** invoice. Guided wizard to select sender, client, and products.
### 2. Non-Interactive / Scripted Mode ### Non-Interactive Mode
You can also generate an invoice by passing command-line arguments. This is useful for automation and scripting.
The basic command is:
`npm run generate -- --sender <sender_key> --client <client_key> --products <product_key_1> <product_key_2>`
**Example:**
```bash ```bash
npm run generate -- --sender company1 --client company4 --products linkedin_scraper web_design npm run generate -- --sender levkin --client levitin --products linkedin_scraper web_design
``` ```
Like the interactive mode, this will also generate a preview and prompt for confirmation before creating the production invoice. ### Test Mode
### Running Tests
To run the automated tests for the project:
```bash ```bash
npm test npm run test-invoice
``` ```
## How the Data Files Work Uses first available options for testing.
All data is managed in the `data/` directory. ### Reprint an existing invoice (from ledger)
### `sender.json` Generates a PDF from `data/invoices.json` only — **does not** change the ledger or run the preview prompt:
Stores your company or sender profiles. Each key represents a unique sender. ```bash
node index.js --print 2026-LFSU01
```
Output path: `invoice/<senderKey>/<filenamePrefix>_<number>.pdf`. The filename prefix is the clients optional `invoiceFileName` in `client.json`, otherwise the client key.
**Due date on the PDF:** Each ledger row can include optional `"dueDate": "YYYY-MM-DD"`. If omitted, the tool uses invoice `date` plus 14 days.
All data is stored in the `data/` directory:
### `data/sender.json` - Your Company Profiles
```json ```json
{ {
"company1": { "company": "Your Company", "address": ... }, "levkin": {
"company2": { "company": "Another Profile", "address": ... } "company": "Levkin Inc.",
"contactName": "Ilia Dobkin",
"phone": "647 987 2792",
"email": "idobkin@gmail.com",
"address": "6 Keefer Court",
"zip": "L4J 5Y4",
"city": "Thornhill, ON",
"country": "Canada",
"taxName": "HST",
"taxNumber": "79438 2895 RT0001",
"paymentInfo": "Bank transfer details..."
}
} }
``` ```
### `client.json` ### `data/client.json` - Client Information
Stores your client profiles. The `invoiceFileName` key is optional; if provided, it will be used for the generated PDF's filename.
```json ```json
{ {
"company3": { "company": "First Client", "address": ... }, "levitin": {
"company4": { "company": "Second Client", "invoiceFileName": "second_client_invoices", "address": ... } "company": "LEVITIN EMPLOYMENT LAWYERS",
"address": "#2809 - 330 Richmond Street West",
"zip": "M5V 0M4",
"city": "Toronto, ON",
"country": "Canada"
}
} }
``` ```
### `products.json` ### `data/products.json` - Services/Products Catalog
A catalog of the services or products you offer.
```json ```json
{ {
"linkedin_scraper": { "linkedin_scraper": {
"description": "LinkedIn Scraper Bot", "description": "LinkedIn Scraper Bot",
"price": 250, "price": 250,
"taxRate": 0 "taxRate": 13
}, },
"web_design": { "description": "Website Design", "price": 1200, "taxRate": 0 } "web_design": {
"description": "Website Design",
"price": 1200,
"taxRate": 13
}
} }
``` ```
### `invoices.json` ## Workflow
This file is your ledger. It's **automatically updated** when you create a production invoice. It tracks all generated invoices to determine the next sequential number. You should not need to edit this file manually. 1. **Preview Generation**: Creates a development preview PDF first
2. **Review**: Check the preview in `invoice/<sender>/<client>_<year>-DEV-PREVIEW.pdf`
3. **Confirmation**: Choose to create the production invoice or keep only the preview
4. **Production**: Generates final invoice with sequential numbering and saves to ledger
## Output Structure
```json
{
"invoices": [
{
"number": "2024-0001",
"date": "2024-07-31",
...
}
]
}
``` ```
invoice/
├── levkin/
│ ├── levitin_2025-0007.pdf # Production invoice
│ └── levitin_2025-DEV-PREVIEW.pdf # Preview
└── dobit/
└── ...
```
## Testing
```bash
npm test
```
Tests cover invoice numbering logic including year resets and sequential ordering.
## Key Features
- **Safe Preview-First**: Always generates preview before production
- **Automatic Numbering**: Sequential invoice numbers (YYYY-0001, YYYY-0002...)
- **Year Reset**: Invoice numbers reset annually
- **Organized Output**: PDFs stored by sender in separate folders
- **Tax Support**: Configurable tax rates per product
- **Payment Info**: Custom payment instructions in footer
- **Ledger Tracking**: All invoices automatically recorded in `data/invoices.json`
- **Reprint mode** (`--print`): PDF from ledger without editing it
- **Optional `dueDate`** on ledger rows for custom payment deadlines when reprinting
See [project.md](project.md) for full schema, CLI details, and changelog.
## Requirements
- Node.js v16+
- npm

View File

@ -12,5 +12,27 @@
"zip": "M5V 0M4", "zip": "M5V 0M4",
"city": "Toronto, ON", "city": "Toronto, ON",
"country": "Canada" "country": "Canada"
},
"jrcc": {
"company": "JRCC - Jewish Russian Community Centre",
"address": "5987 Bathurst St #3",
"zip": "M2R 1Z3",
"city": "North York, ON",
"country": "Canada"
},
"innosphere": {
"company": "Innosphere SDG Ltd.",
"address": "49 Norfolk Street - Suite 300",
"zip": "N1H 4J1",
"city": "Guelph, Ontario",
"country": "Canada"
},
"limmud_fsu_canada": {
"company": "Limmud FSU Canada",
"address": "9600 Bathurst St",
"zip": "L6A 3Z8",
"city": "Maple, Ontario",
"country": "Canada",
"invoiceFileName": "limmud_fsuc"
} }
} }

View File

@ -118,6 +118,75 @@
"subtotal": 250, "subtotal": 250,
"taxTotal": 32.5, "taxTotal": 32.5,
"total": 282.5 "total": 282.5
},
{
"number": "2026-JRCC10",
"date": "2026-04-30",
"sender": "Levkin Inc.",
"client": "JRCC - Jewish Russian Community Centre",
"products": [
{
"description": "Design and development of PunimTag Web: full-stack facial recognition photo management platform (FastAPI backend, React frontend) with DeepFace-based face detection and identification, PostgreSQL/SQLite data storage, Redis/RQ background processing, advanced search and tagging, and deployment-ready configuration and documentation.",
"price": 10000,
"quantity": 1,
"taxRate": 13
}
],
"subtotal": 10000,
"taxTotal": 1300,
"total": 11300
},
{
"number": "2026-INNO16",
"date": "2026-03-31",
"sender": "Levkin Inc.",
"client": "Innosphere SDG Ltd.",
"products": [
{
"description": "Javascript Developer - Mar 1 - Mar 31, 2026",
"price": 85,
"quantity": 136,
"taxRate": 13
}
],
"subtotal": 11560,
"taxTotal": 1502.8,
"total": 13062.8
},
{
"number": "2026-INNO17",
"date": "2026-04-30",
"sender": "Levkin Inc.",
"client": "Innosphere SDG Ltd.",
"products": [
{
"description": "Javascript Developer - Apr 1 - Apr 30, 2026",
"price": 85,
"quantity": 176,
"taxRate": 13
}
],
"subtotal": 14960,
"taxTotal": 1944.8,
"total": 16904.8
},
{
"number": "2026-LFSU01",
"date": "2026-03-22",
"dueDate": "2026-05-31",
"sender": "Levkin Inc.",
"client": "Limmud FSU Canada",
"products": [
{
"description": "March 1522, 2026 — Cruise event IT — audio/video and stage tech: microphones, speakers, video projection and signage, presenter feeds, cabling and rack setup, sound checks and on-site troubleshooting for onboard Limmud FSU programming.",
"price": 500,
"quantity": 1,
"taxRate": 13
}
],
"subtotal": 500,
"taxTotal": 65,
"total": 565
} }
] ]
} }

View File

@ -8,5 +8,10 @@
"description": "Website Design", "description": "Website Design",
"price": 1200, "price": 1200,
"taxRate": 13 "taxRate": 13
},
"cruise_event_av_it": {
"description": "March 1522, 2026 — Cruise event IT — audio/video and stage tech: microphones, mixing, amplifiers and speakers, video projection and signage, presenter feeds, cabling and rack setup, sound checks and on-site troubleshooting for onboard Limmud FSU programming.",
"price": 500,
"taxRate": 13
} }
} }

View File

@ -1,13 +1,16 @@
{ {
"levkin": { "levkin": {
"company": "Levkin Inc.", "company": "Levkin Inc.",
"contactName": "Ilia Dobkin",
"phone": "647 987 2792",
"email": "idobkin@gmail.com",
"address": "6 Keefer Court", "address": "6 Keefer Court",
"zip": "L4J 5Y4", "zip": "L4J 5Y4",
"city": "Thornhill, ON", "city": "Thornhill, ON",
"country": "Canada", "country": "Canada",
"taxName": "HST", "taxName": "HST",
"taxNumber": "79438 2895 RT0001", "taxNumber": "79438 2895 RT0001",
"paymentInfo": "<div style=\"text-align:left;\">All payments to be made by bank transfer to the following account:<br><br>- Levkin Inc. - Ilia Dobkin<br>- 6 Keefer Court, ON, L4J 5Y4<br>- Account: 5004295<br>- Transit: 31822<br>- 2600 Simcoe St. N. Unit 1, Oshawa, ON, L1L 0R1 - Inst: 004<br>- SWIFT Code: TDOMCATTTOR</div>" "paymentInfo": "<div style=\"text-align:left;\">All payments to be made by bank transfer to the following account:<br><br>- Levkin Inc. - Ilia Dobkin<br>- 6 Keefer Court, ON, L4J 5Y4<br>- Account: 5004295<br>- Transit: 31822<br>- Inst: 004<br>- 2600 Simcoe St. N. Unit 1, Oshawa, ON, L1L 0R1<br>- SWIFT Code: TDOMCATTTOR</div>"
}, },
"dobit": { "dobit": {
"company": "DOBIT Consulting", "company": "DOBIT Consulting",

104
index.js
View File

@ -78,7 +78,7 @@ function saveInvoiceRecord(invoiceData, invoiceProducts) {
return sum + (p.price * p.quantity * taxRate) / 100; return sum + (p.price * p.quantity * taxRate) / 100;
}, 0); }, 0);
invoicesData.invoices.push({ const ledgerRow = {
number: invoiceData.information.number, number: invoiceData.information.number,
date: invoiceData.information.date, date: invoiceData.information.date,
sender: invoiceData.sender.company, sender: invoiceData.sender.company,
@ -92,7 +92,11 @@ function saveInvoiceRecord(invoiceData, invoiceProducts) {
subtotal: parseFloat(subtotal.toFixed(2)), subtotal: parseFloat(subtotal.toFixed(2)),
taxTotal: parseFloat(taxTotal.toFixed(2)), taxTotal: parseFloat(taxTotal.toFixed(2)),
total: parseFloat((subtotal + taxTotal).toFixed(2)), total: parseFloat((subtotal + taxTotal).toFixed(2)),
}); };
if (invoiceData.information.dueDate) {
ledgerRow.dueDate = invoiceData.information.dueDate;
}
invoicesData.invoices.push(ledgerRow);
try { try {
fs.writeFileSync(FILES.INVOICES, JSON.stringify(invoicesData, null, 2)); fs.writeFileSync(FILES.INVOICES, JSON.stringify(invoicesData, null, 2));
console.log("✅ Invoice record saved to invoices.json."); console.log("✅ Invoice record saved to invoices.json.");
@ -108,6 +112,11 @@ async function main() {
type: "boolean", type: "boolean",
describe: "Run in interactive mode", describe: "Run in interactive mode",
}) })
.option("print", {
type: "string",
describe:
"Generate a PDF for an existing invoice number (from data/invoices.json) without modifying the ledger",
})
.option("sender", { .option("sender", {
type: "string", type: "string",
describe: "Sender key from sender.json", describe: "Sender key from sender.json",
@ -132,6 +141,97 @@ async function main() {
const clients = loadJson(FILES.CLIENTS); const clients = loadJson(FILES.CLIENTS);
const productsData = loadJson(FILES.PRODUCTS); const productsData = loadJson(FILES.PRODUCTS);
// --- Print existing invoice (no prompts, no ledger update) ---
if (argv.print) {
const invoiceNumber = argv.print;
const invoicesData = loadJson(FILES.INVOICES);
const record = invoicesData.invoices.find((inv) => inv.number === invoiceNumber);
if (!record) {
console.error(`Invoice not found in invoices.json: ${invoiceNumber}`);
process.exit(1);
}
const resolvedSenderKey =
Object.keys(senders).find((k) => senders[k]?.company === record.sender) || null;
const resolvedClientKey =
Object.keys(clients).find((k) => clients[k]?.company === record.client) || null;
if (!resolvedSenderKey) {
console.error(
`Could not match sender company "${record.sender}" to a key in sender.json.`
);
process.exit(1);
}
if (!resolvedClientKey) {
console.error(
`Could not match client company "${record.client}" to a key in client.json.`
);
process.exit(1);
}
const sender = senders[resolvedSenderKey];
const client = clients[resolvedClientKey];
const invoiceProducts = (record.products || []).map((p) => ({
quantity: p.quantity ?? 1,
description: p.description,
"tax-rate": p.taxRate ?? 0,
price: p.price,
}));
if (invoiceProducts.length === 0) {
console.error(`Invoice ${invoiceNumber} has no products.`);
process.exit(1);
}
const senderForInvoice = { ...sender };
if (sender.taxNumber) {
senderForInvoice.custom1 = `${sender.taxName || "Tax"} Number: ${
sender.taxNumber
}`;
}
let dueDateStr;
if (record.dueDate) {
dueDateStr = record.dueDate;
} else {
const due = new Date(record.date);
due.setDate(due.getDate() + 14);
dueDateStr = due.toISOString().split("T")[0];
}
const commonData = {
sender: senderForInvoice,
client,
products: invoiceProducts,
bottomNotice: sender.paymentInfo
? `Thank you for your business!<br /><br /><br />${sender.paymentInfo}`
: "Thank you for your business!",
settings: { currency: "CAD" },
translate: {
taxNotation: sender.taxName || "VAT",
number: "Invoice",
},
};
const printData = {
...commonData,
information: {
number: record.number,
date: record.date,
dueDate: dueDateStr,
},
mode: "production",
};
const fileNameKey = client.invoiceFileName || resolvedClientKey;
const outFileName = `${fileNameKey}_${record.number}.pdf`;
const outFilePath = path.join(FOLDERS.INVOICE, resolvedSenderKey, outFileName);
console.log(`\nPrinting invoice ${record.number}...`);
const ok = await createInvoice(printData, outFilePath);
process.exit(ok ? 0 : 1);
}
// Test mode: use first available options // Test mode: use first available options
if (argv.test) { if (argv.test) {
senderKey = Object.keys(senders)[0]; senderKey = Object.keys(senders)[0];

324
project.md Normal file
View File

@ -0,0 +1,324 @@
# Invoice Generator - Project Documentation
## Project Overview
A Node.js command-line application for generating professional PDF invoices. The system uses a data-driven approach where all business information (senders, clients, products) is stored in JSON files, making it easy to maintain without code changes.
## Architecture
### Core Components
```
invoice/
├── index.js # Main application entry point
├── data/ # JSON data files
│ ├── sender.json # Company/sender profiles
│ ├── client.json # Client information
│ ├── products.json # Product/service catalog
│ └── invoices.json # Invoice ledger (auto-generated)
├── invoice/ # Generated PDF output
│ └── <sender_name>/ # Organized by sender
└── test/
└── invoice.test.js # Unit tests
```
### Dependencies
**Production:**
- `easyinvoice` (v3.0.47) - PDF generation library
- `inquirer` (v8.2.4) - Interactive command-line prompts
- `yargs` (v17.7.2) - Command-line argument parsing
**Development:**
- `jest` (v29.7.0) - Testing framework
## Data Flow
### 1. Data Loading
```javascript
// Loads all JSON files from data/ directory
const senders = loadJson(FILES.SENDERS);
const clients = loadJson(FILES.CLIENTS);
const productsData = loadJson(FILES.PRODUCTS);
```
### 2. User Input Processing
- **Interactive Mode**: Uses `inquirer` prompts for sender, client, and product selection
- **Non-Interactive Mode**: Processes command-line arguments via `yargs`
- **Test Mode**: Automatically selects first available options
### 3. Invoice Generation Process
```mermaid
graph TD
A[Load Data] --> B[Validate Inputs]
B --> C[Generate Preview PDF]
C --> D[User Review]
D --> E{Proceed?}
E -->|Yes| F[Generate Production PDF]
E -->|No| G[Keep Preview Only]
F --> H[Save to Ledger]
H --> I[Update invoices.json]
```
### 4. File Output
- **Preview**: `invoice/<sender>/<client>_<year>-DEV-PREVIEW.pdf`
- **Production**: `invoice/<sender>/<client>_<year>-<sequence>.pdf`
## Key Functions
### `getNextInvoiceNumber(year, invoicesData)`
- Generates sequential invoice numbers (YYYY-0001, YYYY-0002...)
- Resets numbering annually
- Handles unordered invoice numbers correctly
### `createInvoice(invoiceData, invoiceFilePath)`
- Uses `easyinvoice` library to generate PDF
- Creates directory structure if needed
- Returns success/failure status
### `saveInvoiceRecord(invoiceData, invoiceProducts)`
- Calculates totals (subtotal, tax, grand total)
- Appends invoice record to `invoices.json`
- Persists `dueDate` from `invoiceData.information.dueDate` when set, for accurate future `--print` output
- Maintains complete invoice history
## Data Schema
### Sender Schema
```json
{
"company": "string",
"contactName": "string (optional)",
"phone": "string (optional)",
"email": "string (optional)",
"address": "string",
"zip": "string",
"city": "string",
"country": "string",
"taxName": "string (optional)",
"taxNumber": "string (optional)",
"paymentInfo": "string (optional)"
}
```
### Client Schema
```json
{
"company": "string",
"address": "string",
"zip": "string",
"city": "string",
"country": "string",
"invoiceFileName": "string (optional)"
}
```
### Product Schema
```json
{
"description": "string",
"price": "number",
"taxRate": "number (optional, default 0)"
}
```
### Invoice Record Schema
```json
{
"number": "string (YYYY-0001 or custom e.g. 2026-LFSU01)",
"date": "string (YYYY-MM-DD)",
"dueDate": "string (YYYY-MM-DD, optional — see Due dates below)",
"sender": "string (must match sender.json company name)",
"client": "string (must match client.json company name)",
"products": [
{
"description": "string",
"price": "number",
"quantity": "number",
"taxRate": "number"
}
],
"subtotal": "number",
"taxTotal": "number",
"total": "number"
}
```
**Due dates:** When you run `node index.js --print <number>`, the PDF due date is taken from `dueDate` if present. Otherwise it defaults to `date` plus 14 days. New production invoices still compute due as “today + 14” in the UI flow; `saveInvoiceRecord` stores that value on the ledger row as `dueDate` when present so reprints stay consistent. You can edit `dueDate` in `invoices.json` for any row (for example, payment terms through end of month).
## Command-Line Interface
### Available Commands
```bash
npm start # Interactive mode
npm run generate # Non-interactive mode
npm test # Run unit tests
npm run test-invoice # Test mode (dev only)
```
### Reprinting from the ledger (no prompts, no ledger changes)
```bash
node index.js --print <invoice-number>
```
Looks up `invoice-number` in `data/invoices.json`, resolves sender and client by matching `company` strings to `sender.json` / `client.json`, and writes `invoice/<senderKey>/<filename>_<number>.pdf`. The clients optional `invoiceFileName` is used as the filename prefix when set (for example `limmud_fsuc_2026-LFSU01.pdf`).
### Command-Line Options
```bash
--interactive # Force interactive mode
--print <invoice-number> # PDF only from invoices.json; does not append ledger
--sender <key> # Sender key from sender.json
--client <key> # Client key from client.json
--products <key1> <key2> # Product keys from products.json
--test # Test mode (uses first available options)
--help # Show help
```
## Testing Strategy
### Unit Tests (`test/invoice.test.js`)
- **Invoice Numbering**: Tests sequential numbering logic
- **Year Reset**: Verifies numbering resets annually
- **Unordered Numbers**: Handles gaps in invoice sequence
### Test Coverage
- `getNextInvoiceNumber()` function
- Edge cases for invoice numbering
- Year boundary scenarios
## Error Handling
### File System Errors
- Missing JSON files trigger helpful error messages
- Invalid JSON syntax is caught and reported
- Directory creation failures are handled gracefully
### Data Validation
- Invalid sender/client/product keys are rejected
- Missing required fields trigger validation errors
- Tax rate calculations handle missing values
### PDF Generation Errors
- `easyinvoice` failures are caught and logged
- File write errors are handled with user feedback
## Configuration Options
### Currency
- Default: CAD (Canadian Dollar)
- Set in `commonData.settings.currency`
### Tax Configuration
- Per-product tax rates in `products.json`
- Tax name customization in `sender.json`
- Automatic tax calculations
### Payment Information
- Custom payment instructions in `sender.paymentInfo`
- Rendered in invoice footer
- Supports HTML formatting
## File Organization
### Input Files (`data/`)
- **sender.json**: Company profiles and tax information
- **client.json**: Client contact details
- **products.json**: Service/product catalog with pricing
- **invoices.json**: Auto-generated invoice ledger
### Output Files (`invoice/`)
- Organized by sender name
- Preview files marked with `-DEV-PREVIEW`
- Production files with sequential numbering
- Automatic directory creation
## Development Workflow
### Adding New Data
1. Edit appropriate JSON file in `data/`
2. Use interactive mode to test: `npm start`
3. Verify preview generation
4. Create production invoice if satisfied
### Testing Changes
1. Run unit tests: `npm test`
2. Test invoice generation: `npm run test-invoice`
3. Verify file output structure
4. Check ledger updates
### Code Modifications
- Main logic in `index.js`
- Test cases in `test/invoice.test.js`
- Data validation in main function
- Error handling throughout
## Security Considerations
- No external API calls
- Local file system only
- No sensitive data in code
- JSON validation for data integrity
- Error boundaries prevent crashes
## Performance
- Minimal dependencies
- Synchronous file operations
- No database overhead
- Fast PDF generation
- Efficient JSON parsing
## Changelog (May 2026)
- **`--print`**: Regenerate PDFs from `data/invoices.json` without modifying the ledger.
- **Optional ledger `dueDate`**: Overrides the default “invoice date + 14 days” when printing; stored on new rows when production creates `information.dueDate`.
- **Data**: Client `limmud_fsu_canada` (Limmud FSU Canada) with `invoiceFileName` for PDF naming; product `cruise_event_av_it` ($500 + 13% HST); sample invoice `2026-LFSU01` (Levkin Inc. → Limmud FSU Canada).
- **Sender**: Optional `contactName`, `phone`, and `email` on `levkin` in `sender.json` (and schema support for other senders).
## Future Enhancements
### Potential Features
- Multiple currency support
- Invoice templates
- Email integration
- Export to accounting software
- Invoice status tracking
- Payment reminders
### Technical Improvements
- TypeScript migration
- Database integration
- Web interface
- API endpoints
- Docker containerization