add data
This commit is contained in:
commit
b4a3274444
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
.history
|
||||
node_modules
|
||||
*.pdf
|
||||
120
README.md
Normal file
120
README.md
Normal file
@ -0,0 +1,120 @@
|
||||
# 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.
|
||||
|
||||
## Features
|
||||
|
||||
- **Interactive & Non-Interactive Modes**: Use the simple interactive wizard (`npm start`) or pass arguments directly for scripted use.
|
||||
- **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
|
||||
npm install
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
There are two primary ways to generate an invoice:
|
||||
|
||||
### 1. Interactive Mode (Recommended for most users)
|
||||
|
||||
Run the interactive wizard. The tool will guide you through selecting a sender, client, and one or more products.
|
||||
|
||||
```bash
|
||||
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.
|
||||
|
||||
### 2. Non-Interactive / Scripted 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
|
||||
npm run generate -- --sender company1 --client company4 --products linkedin_scraper web_design
|
||||
```
|
||||
|
||||
Like the interactive mode, this will also generate a preview and prompt for confirmation before creating the production invoice.
|
||||
|
||||
### Running Tests
|
||||
|
||||
To run the automated tests for the project:
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
## How the Data Files Work
|
||||
|
||||
All data is managed in the `data/` directory.
|
||||
|
||||
### `sender.json`
|
||||
|
||||
Stores your company or sender profiles. Each key represents a unique sender.
|
||||
|
||||
```json
|
||||
{
|
||||
"company1": { "company": "Your Company", "address": ... },
|
||||
"company2": { "company": "Another Profile", "address": ... }
|
||||
}
|
||||
```
|
||||
|
||||
### `client.json`
|
||||
|
||||
Stores your client profiles. The `invoiceFileName` key is optional; if provided, it will be used for the generated PDF's filename.
|
||||
|
||||
```json
|
||||
{
|
||||
"company3": { "company": "First Client", "address": ... },
|
||||
"company4": { "company": "Second Client", "invoiceFileName": "second_client_invoices", "address": ... }
|
||||
}
|
||||
```
|
||||
|
||||
### `products.json`
|
||||
|
||||
A catalog of the services or products you offer.
|
||||
|
||||
```json
|
||||
{
|
||||
"linkedin_scraper": {
|
||||
"description": "LinkedIn Scraper Bot",
|
||||
"price": 250,
|
||||
"taxRate": 0
|
||||
},
|
||||
"web_design": { "description": "Website Design", "price": 1200, "taxRate": 0 }
|
||||
}
|
||||
```
|
||||
|
||||
### `invoices.json`
|
||||
|
||||
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.
|
||||
|
||||
```json
|
||||
{
|
||||
"invoices": [
|
||||
{
|
||||
"number": "2024-0001",
|
||||
"date": "2024-07-31",
|
||||
...
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
16
data/client.json
Normal file
16
data/client.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"company3": {
|
||||
"company": "Client Company",
|
||||
"address": "456 Client Ave",
|
||||
"zip": "67890",
|
||||
"city": "Client City",
|
||||
"country": "Client Country"
|
||||
},
|
||||
"levitin": {
|
||||
"company": "LEVITIN EMPLOYMENT LAWYERS",
|
||||
"address": "#2809 - 330 Richmond Street West",
|
||||
"zip": "M5V 0M4",
|
||||
"city": "Toronto, ON",
|
||||
"country": "Canada"
|
||||
}
|
||||
}
|
||||
123
data/invoices.json
Normal file
123
data/invoices.json
Normal file
@ -0,0 +1,123 @@
|
||||
{
|
||||
"invoices": [
|
||||
{
|
||||
"number": "2025-0001",
|
||||
"date": "2025-07-04",
|
||||
"sender": "Your Company Name",
|
||||
"client": "Second Client",
|
||||
"products": [
|
||||
{
|
||||
"description": "LinkedIn Scraper Bot",
|
||||
"price": 250,
|
||||
"quantity": 1
|
||||
}
|
||||
],
|
||||
"amount": 250
|
||||
},
|
||||
{
|
||||
"number": "2025-0002",
|
||||
"date": "2025-07-04",
|
||||
"sender": "Your Company Name",
|
||||
"client": "Client Company",
|
||||
"products": [
|
||||
{
|
||||
"description": "LinkedIn Scraper Bot",
|
||||
"price": 250,
|
||||
"quantity": 1
|
||||
}
|
||||
],
|
||||
"amount": 250
|
||||
},
|
||||
{
|
||||
"number": "2025-0003",
|
||||
"date": "2025-07-04",
|
||||
"sender": "Your Company Name",
|
||||
"client": "Client Company",
|
||||
"products": [
|
||||
{
|
||||
"description": "LinkedIn Scraper Bot",
|
||||
"price": 250,
|
||||
"quantity": 1,
|
||||
"taxRate": 13
|
||||
}
|
||||
],
|
||||
"subtotal": 250,
|
||||
"taxTotal": 32.5,
|
||||
"total": 282.5
|
||||
},
|
||||
{
|
||||
"number": "2025-0004",
|
||||
"date": "2025-07-04",
|
||||
"sender": "Another Sender",
|
||||
"client": "Client Company",
|
||||
"products": [
|
||||
{
|
||||
"description": "LinkedIn Scraper Bot",
|
||||
"price": 250,
|
||||
"quantity": 1,
|
||||
"taxRate": 13
|
||||
},
|
||||
{
|
||||
"description": "Website Design",
|
||||
"price": 1200,
|
||||
"quantity": 1,
|
||||
"taxRate": 13
|
||||
}
|
||||
],
|
||||
"subtotal": 1450,
|
||||
"taxTotal": 188.5,
|
||||
"total": 1638.5
|
||||
},
|
||||
{
|
||||
"number": "2025-0005",
|
||||
"date": "2025-07-04",
|
||||
"sender": "Your Company Name",
|
||||
"client": "Client Company",
|
||||
"products": [
|
||||
{
|
||||
"description": "LinkedIn Scraper Bot",
|
||||
"price": 250,
|
||||
"quantity": 1,
|
||||
"taxRate": 13
|
||||
}
|
||||
],
|
||||
"subtotal": 250,
|
||||
"taxTotal": 32.5,
|
||||
"total": 282.5
|
||||
},
|
||||
{
|
||||
"number": "2025-0006",
|
||||
"date": "2025-07-04",
|
||||
"sender": "Your Company Name",
|
||||
"client": "Client Company",
|
||||
"products": [
|
||||
{
|
||||
"description": "LinkedIn Scraper Bot",
|
||||
"price": 250,
|
||||
"quantity": 1,
|
||||
"taxRate": 13
|
||||
}
|
||||
],
|
||||
"subtotal": 250,
|
||||
"taxTotal": 32.5,
|
||||
"total": 282.5
|
||||
},
|
||||
{
|
||||
"number": "2025-0007",
|
||||
"date": "2025-07-06",
|
||||
"sender": "Levkin Inc.",
|
||||
"client": "Client Company",
|
||||
"products": [
|
||||
{
|
||||
"description": "LinkedIn Scraper Bot",
|
||||
"price": 250,
|
||||
"quantity": 1,
|
||||
"taxRate": 13
|
||||
}
|
||||
],
|
||||
"subtotal": 250,
|
||||
"taxTotal": 32.5,
|
||||
"total": 282.5
|
||||
}
|
||||
]
|
||||
}
|
||||
12
data/products.json
Normal file
12
data/products.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"linkedin_scraper": {
|
||||
"description": "LinkedIn Scraper Bot",
|
||||
"price": 250,
|
||||
"taxRate": 13
|
||||
},
|
||||
"web_design": {
|
||||
"description": "Website Design",
|
||||
"price": 1200,
|
||||
"taxRate": 13
|
||||
}
|
||||
}
|
||||
21
data/sender.json
Normal file
21
data/sender.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"levkin": {
|
||||
"company": "Levkin Inc.",
|
||||
"address": "6 Keefer Court",
|
||||
"zip": "L4J 5Y4",
|
||||
"city": "Thornhill, ON",
|
||||
"country": "Canada",
|
||||
"taxName": "HST",
|
||||
"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>"
|
||||
},
|
||||
"dobit": {
|
||||
"company": "DOBIT Consulting",
|
||||
"address": " 6 Keefer Court",
|
||||
"zip": "L4J 5Y4",
|
||||
"city": "Thornhill, ON",
|
||||
"country": "Canada",
|
||||
"taxName": "HST",
|
||||
"taxNumber": "83602 7243 RT0002"
|
||||
}
|
||||
}
|
||||
310
index.js
Normal file
310
index.js
Normal file
@ -0,0 +1,310 @@
|
||||
// index.js
|
||||
const easyinvoice = require("easyinvoice");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const yargs = require("yargs/yargs");
|
||||
const { hideBin } = require("yargs/helpers");
|
||||
|
||||
// Late import for ESM compatibility with inquirer
|
||||
let inquirer;
|
||||
Promise.resolve().then(() => {
|
||||
inquirer = require("inquirer");
|
||||
});
|
||||
|
||||
const FOLDERS = {
|
||||
DATA: "data",
|
||||
INVOICE: "invoice",
|
||||
};
|
||||
|
||||
const FILES = {
|
||||
SENDERS: path.join(FOLDERS.DATA, "sender.json"),
|
||||
CLIENTS: path.join(FOLDERS.DATA, "client.json"),
|
||||
PRODUCTS: path.join(FOLDERS.DATA, "products.json"),
|
||||
INVOICES: path.join(FOLDERS.DATA, "invoices.json"),
|
||||
};
|
||||
|
||||
// --- UTILITY FUNCTIONS ---
|
||||
function loadJson(filePath) {
|
||||
try {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
console.error(`Error: File not found at ${filePath}. Please create it.`);
|
||||
process.exit(1);
|
||||
}
|
||||
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
||||
} catch (err) {
|
||||
console.error(`Error loading or parsing ${filePath}:`, err.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function getNextInvoiceNumber(year, invoicesData) {
|
||||
const yearInvoices = invoicesData.invoices.filter((inv) =>
|
||||
inv.number.startsWith(`${year}-`)
|
||||
);
|
||||
let lastNum = 0;
|
||||
if (yearInvoices.length > 0) {
|
||||
lastNum = Math.max(
|
||||
...yearInvoices.map((inv) => parseInt(inv.number.split("-")[1], 10))
|
||||
);
|
||||
}
|
||||
return `${year}-${String(lastNum + 1).padStart(4, "0")}`;
|
||||
}
|
||||
|
||||
async function createInvoice(invoiceData, invoiceFilePath) {
|
||||
try {
|
||||
const result = await easyinvoice.createInvoice(invoiceData);
|
||||
const invoiceFolder = path.dirname(invoiceFilePath);
|
||||
if (!fs.existsSync(invoiceFolder)) {
|
||||
fs.mkdirSync(invoiceFolder, { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(invoiceFilePath, result.pdf, "base64");
|
||||
console.log(`✅ Invoice generated: ${invoiceFilePath}`);
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error("Error creating invoice PDF:", err.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function saveInvoiceRecord(invoiceData, invoiceProducts) {
|
||||
const invoicesData = loadJson(FILES.INVOICES);
|
||||
|
||||
const subtotal = invoiceProducts.reduce(
|
||||
(sum, p) => sum + p.price * p.quantity,
|
||||
0
|
||||
);
|
||||
const taxTotal = invoiceProducts.reduce((sum, p) => {
|
||||
const taxRate = p["tax-rate"] || 0;
|
||||
return sum + (p.price * p.quantity * taxRate) / 100;
|
||||
}, 0);
|
||||
|
||||
invoicesData.invoices.push({
|
||||
number: invoiceData.information.number,
|
||||
date: invoiceData.information.date,
|
||||
sender: invoiceData.sender.company,
|
||||
client: invoiceData.client.company,
|
||||
products: invoiceProducts.map((p) => ({
|
||||
description: p.description,
|
||||
price: p.price,
|
||||
quantity: p.quantity,
|
||||
taxRate: p["tax-rate"],
|
||||
})),
|
||||
subtotal: parseFloat(subtotal.toFixed(2)),
|
||||
taxTotal: parseFloat(taxTotal.toFixed(2)),
|
||||
total: parseFloat((subtotal + taxTotal).toFixed(2)),
|
||||
});
|
||||
try {
|
||||
fs.writeFileSync(FILES.INVOICES, JSON.stringify(invoicesData, null, 2));
|
||||
console.log("✅ Invoice record saved to invoices.json.");
|
||||
} catch (err) {
|
||||
console.error("Error writing to invoices.json:", err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// --- MAIN LOGIC ---
|
||||
async function main() {
|
||||
const argv = yargs(hideBin(process.argv))
|
||||
.parserConfiguration({ "update-notifier-off": true })
|
||||
.option("interactive", {
|
||||
type: "boolean",
|
||||
describe: "Run in interactive mode",
|
||||
})
|
||||
.option("sender", {
|
||||
type: "string",
|
||||
describe: "Sender key from sender.json",
|
||||
})
|
||||
.option("client", {
|
||||
type: "string",
|
||||
describe: "Client key from client.json",
|
||||
})
|
||||
.option("products", {
|
||||
type: "array",
|
||||
describe: "Product keys from products.json",
|
||||
})
|
||||
.option("test", {
|
||||
type: "boolean",
|
||||
describe: "Run in test mode (uses first available options, dev only)",
|
||||
})
|
||||
.help().argv;
|
||||
|
||||
let senderKey, clientKey, productKeys;
|
||||
|
||||
const senders = loadJson(FILES.SENDERS);
|
||||
const clients = loadJson(FILES.CLIENTS);
|
||||
const productsData = loadJson(FILES.PRODUCTS);
|
||||
|
||||
// Test mode: use first available options
|
||||
if (argv.test) {
|
||||
senderKey = Object.keys(senders)[0];
|
||||
clientKey = Object.keys(clients)[0];
|
||||
productKeys = [Object.keys(productsData)[0]];
|
||||
console.log(
|
||||
`🧪 Test mode: Using sender="${senderKey}", client="${clientKey}", products=[${productKeys.join(
|
||||
", "
|
||||
)}]`
|
||||
);
|
||||
} else if (
|
||||
argv.interactive ||
|
||||
!(argv.sender && argv.client && argv.products)
|
||||
) {
|
||||
if (!inquirer) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100)); // wait for dynamic import
|
||||
if (!inquirer) {
|
||||
// check again
|
||||
console.error(
|
||||
"Failed to load 'inquirer'. Please ensure it is installed correctly."
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
const answers = await inquirer.prompt([
|
||||
{
|
||||
type: "list",
|
||||
name: "sender",
|
||||
message: "Select a sender:",
|
||||
choices: Object.keys(senders),
|
||||
},
|
||||
{
|
||||
type: "list",
|
||||
name: "client",
|
||||
message: "Select a client:",
|
||||
choices: Object.keys(clients),
|
||||
},
|
||||
{
|
||||
type: "checkbox",
|
||||
name: "products",
|
||||
message: "Select products:",
|
||||
choices: Object.keys(productsData),
|
||||
validate: (input) =>
|
||||
input.length > 0 ? true : "Please select at least one product.",
|
||||
},
|
||||
]);
|
||||
senderKey = answers.sender;
|
||||
clientKey = answers.client;
|
||||
productKeys = answers.products;
|
||||
} else {
|
||||
senderKey = argv.sender;
|
||||
clientKey = argv.client;
|
||||
productKeys = argv.products;
|
||||
}
|
||||
|
||||
// --- Data Validation and Preparation ---
|
||||
if (
|
||||
!senders[senderKey] ||
|
||||
!clients[clientKey] ||
|
||||
!productKeys.every((k) => productsData[k])
|
||||
) {
|
||||
console.error("Invalid sender, client, or product key provided.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const sender = senders[senderKey];
|
||||
const client = clients[clientKey];
|
||||
const invoiceProducts = productKeys.map((key) => {
|
||||
const product = productsData[key];
|
||||
return {
|
||||
quantity: 1, // Default quantity
|
||||
description: product.description,
|
||||
"tax-rate": product.taxRate || 0,
|
||||
price: product.price,
|
||||
};
|
||||
});
|
||||
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
const devInvoiceNumber = `${year}-DEV-PREVIEW`;
|
||||
|
||||
const senderForInvoice = { ...sender };
|
||||
if (sender.taxNumber) {
|
||||
senderForInvoice.custom1 = `${sender.taxName || "Tax"} Number: ${
|
||||
sender.taxNumber
|
||||
}`;
|
||||
}
|
||||
|
||||
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",
|
||||
},
|
||||
};
|
||||
|
||||
// --- Development / Preview Run ---
|
||||
const devData = {
|
||||
...commonData,
|
||||
information: {
|
||||
number: devInvoiceNumber,
|
||||
date: now.toISOString().split("T")[0],
|
||||
dueDate: new Date(new Date().setDate(now.getDate() + 14))
|
||||
.toISOString()
|
||||
.split("T")[0],
|
||||
},
|
||||
mode: "development",
|
||||
};
|
||||
const fileNameKey = client.invoiceFileName || clientKey;
|
||||
const devFileName = `${fileNameKey}_${devInvoiceNumber}.pdf`;
|
||||
const devFilePath = path.join(FOLDERS.INVOICE, senderKey, devFileName);
|
||||
|
||||
console.log("\nGenerating development preview...");
|
||||
await createInvoice(devData, devFilePath);
|
||||
|
||||
// Skip production prompt in test mode
|
||||
if (argv.test) {
|
||||
console.log("\n🧪 Test mode: Skipping production invoice creation.");
|
||||
return;
|
||||
}
|
||||
|
||||
// --- Production Prompt ---
|
||||
if (!inquirer) await new Promise((resolve) => setTimeout(resolve, 100)); // Ensure inquirer is loaded
|
||||
const { proceed } = await inquirer.prompt([
|
||||
{
|
||||
type: "list",
|
||||
name: "proceed",
|
||||
message: "Preview generated. What would you like to do?",
|
||||
choices: [
|
||||
{ name: "No - Keep only the preview", value: false },
|
||||
{ name: "Yes - Create production invoice", value: true },
|
||||
],
|
||||
default: 0, // Default to "No"
|
||||
},
|
||||
]);
|
||||
|
||||
if (proceed) {
|
||||
console.log("\nGenerating production invoice...");
|
||||
const invoicesData = loadJson(FILES.INVOICES);
|
||||
const prodInvoiceNumber = getNextInvoiceNumber(year, invoicesData);
|
||||
|
||||
const prodData = {
|
||||
...commonData,
|
||||
information: {
|
||||
number: prodInvoiceNumber,
|
||||
date: now.toISOString().split("T")[0],
|
||||
dueDate: new Date(new Date().setDate(now.getDate() + 14))
|
||||
.toISOString()
|
||||
.split("T")[0],
|
||||
},
|
||||
mode: "production",
|
||||
};
|
||||
const prodFileName = `${fileNameKey}_${prodInvoiceNumber}.pdf`;
|
||||
const prodFilePath = path.join(FOLDERS.INVOICE, senderKey, prodFileName);
|
||||
|
||||
if (await createInvoice(prodData, prodFilePath)) {
|
||||
saveInvoiceRecord(prodData, invoiceProducts);
|
||||
}
|
||||
} else {
|
||||
console.log("\nProduction invoice creation cancelled.");
|
||||
}
|
||||
}
|
||||
|
||||
// Check if running in test environment
|
||||
if (process.env.NODE_ENV !== "test") {
|
||||
main().catch((err) => console.error("An unexpected error occurred:", err));
|
||||
}
|
||||
|
||||
module.exports = { getNextInvoiceNumber, loadJson }; // For testing
|
||||
4912
package-lock.json
generated
Normal file
4912
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
24
package.json
Normal file
24
package.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "invoice",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "node index.js --interactive",
|
||||
"generate": "node index.js",
|
||||
"test": "jest",
|
||||
"test-invoice": "node index.js --test"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"type": "commonjs",
|
||||
"dependencies": {
|
||||
"easyinvoice": "^3.0.47",
|
||||
"inquirer": "^8.2.4",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jest": "^29.7.0"
|
||||
}
|
||||
}
|
||||
41
test/invoice.test.js
Normal file
41
test/invoice.test.js
Normal file
@ -0,0 +1,41 @@
|
||||
const { getNextInvoiceNumber } = require("../index");
|
||||
|
||||
describe("getNextInvoiceNumber", () => {
|
||||
test("should return -0001 for the first invoice of the year", () => {
|
||||
const year = new Date().getFullYear();
|
||||
const invoicesData = { invoices: [] };
|
||||
expect(getNextInvoiceNumber(year, invoicesData)).toBe(`${year}-0001`);
|
||||
});
|
||||
|
||||
test("should correctly increment the invoice number for the same year", () => {
|
||||
const year = new Date().getFullYear();
|
||||
const invoicesData = {
|
||||
invoices: [{ number: `${year}-0001` }, { number: `${year}-0002` }],
|
||||
};
|
||||
expect(getNextInvoiceNumber(year, invoicesData)).toBe(`${year}-0003`);
|
||||
});
|
||||
|
||||
test("should reset number for a new year", () => {
|
||||
const lastYear = new Date().getFullYear() - 1;
|
||||
const year = new Date().getFullYear();
|
||||
const invoicesData = {
|
||||
invoices: [
|
||||
{ number: `${lastYear}-0001` },
|
||||
{ number: `${lastYear}-0002` },
|
||||
],
|
||||
};
|
||||
expect(getNextInvoiceNumber(year, invoicesData)).toBe(`${year}-0001`);
|
||||
});
|
||||
|
||||
test("should handle unordered invoice numbers correctly", () => {
|
||||
const year = new Date().getFullYear();
|
||||
const invoicesData = {
|
||||
invoices: [
|
||||
{ number: `${year}-0005` },
|
||||
{ number: `${year}-0001` },
|
||||
{ number: `${year}-0003` },
|
||||
],
|
||||
};
|
||||
expect(getNextInvoiceNumber(year, invoicesData)).toBe(`${year}-0006`);
|
||||
});
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user