@ic-labs/invoice-kit
v0.1.14
Published
Lightweight TypeScript library for generating Moroccan business documents as PDFs.
Maintainers
Readme
@ic-labs/invoice-kit
TypeScript library for generating Moroccan business documents as PDFs.
It is designed for real business usage, not a demo. The package provides typed document factories, business calculations, French and Arabic labels, Moroccan invoice defaults, and PDF generation for:
Facture/ invoiceDevis/ quoteBon de commande/ purchase orderBon de livraison/ delivery noteBon de retour/ return note
Features
- Strict TypeScript API
- Node.js 18+
- Lightweight runtime based on
pdfkit - Clean factories for all document types
- Automatic subtotal, discount, VAT, taxable base, and total calculations
- Multi-page item tables for longer documents
- Automated
node:testcoverage for totals, validation, sparse documents, remote logo formats, and pagination - Moroccan defaults:
- currency:
MAD - VAT label:
TVA - date format:
dd/MM/yyyy - company identifiers:
ICE,IF,RC
- currency:
- French labels by default
- Arabic labels with
locale: "ar-MA" - Bank details support:
- local transfer: bank name, holder name, RIB
- international transfer: bank name, holder name, SWIFT, IBAN
- Remote logo URL support in PDF headers, including SVG and WEBP
- Optional document color palette customization
- Optional multiline footer rendered at the extreme bottom of the page
- Delivery and return notes rendered without pricing information
Installation
npm install @ic-labs/invoice-kitQuick Start
import { createInvoice } from "@ic-labs/invoice-kit";
const invoice = createInvoice({
title: "Facture",
issuer: {
name: "IC Labs SARL",
logo: "https://example.com/assets/logo.png"
},
seller: {
name: "IC Distribution SARL"
},
client: {
name: "Client Demo"
},
items: [
{
name: "Service de conseil",
quantity: 1,
price: 1000
}
],
vatRate: 0.2,
paymentTerms: {
label: "Paiement sous 30 jours"
},
colors: {
primary: "#7c3aed",
mutedText: "#8b5cf6",
border: "#ddd6fe"
},
footer: "IC Labs SARL\[email protected]\n+212600000000",
bankInfo: {
type: "local",
bankName: "Attijariwafa Bank",
holderName: "IC Labs SARL",
rib: "007 810 000123456789012345"
}
});
await invoice.toPDF("./invoice.pdf");Public API
import {
createDeliveryNote,
createInvoice,
createPurchaseOrder,
createQuote,
createReturnNote
} from "@ic-labs/invoice-kit";Each factory returns a BusinessDocument instance.
BusinessDocument
toJSON(): BusinessDocumentDatatoPDF(outputPathOrOptions?): Promise<Uint8Array>
Examples:
const bytes = await invoice.toPDF();await invoice.toPDF("./invoice.pdf");await invoice.toPDF({
outputPath: "./invoice.pdf"
});Document Factories
createInvoice(input)
Creates a financial invoice with totals, discounts, VAT, bank details, and payment terms.
createQuote(input)
Creates a quote/devis with the same financial model as invoices.
createPurchaseOrder(input)
Creates a purchase order with pricing and totals.
createDeliveryNote(input)
Creates a delivery note without pricing display in the PDF.
createReturnNote(input)
Creates a Moroccan-style return note without pricing display in the PDF.
Important behavior for delivery and return notes:
- item prices are not required
- pricing columns are hidden in the PDF
- totals are hidden in the PDF
discounts,vatRate,paymentTerms, andbankInfomay exist in input but are not rendered as financial output- internal totals are normalized to zero for stock-movement documents
Input Reference
All document factories accept a typed object shaped like DocumentInput.
Modular fields
- Every top-level field is optional.
- If a field is omitted, that module is not rendered into the PDF.
- The library does not inject missing modules into the output.
Available modules:
number?: stringissuer?: Issuerseller?: Partyclient?: Partyitems?: LineItemInput[]issueDate?: DatedueDate?: Datecurrency?: stringvatRate?: numberdiscounts?: DiscountInput[]notes?: stringfooter?: stringpaymentTerms?: PaymentTermsbankInfo?: BankInfocolors?: DocumentColorslocale?: "fr-MA" | "ar-MA"title?: string
Examples:
createInvoice({
title: "Facture",
footer: "IC Labs SARL"
});createQuote({
issuer: { name: "IC Labs SARL", logo: "https://example.com/logo.png" },
client: { name: "Client Demo" }
});footer
footeris an optional string rendered centered at the extreme bottom of the PDF- multiline strings are supported with
\n - when multiple lines are provided, rendering starts higher so the last line ends at the bottom edge
- if
footeris omitted, the PDF falls back to the document title and number only when one of them exists
colors
interface DocumentColors {
primary?: string;
onPrimary?: string;
text?: string;
metaText?: string;
mutedText?: string;
border?: string;
footerText?: string;
}colorsis an optional top-level module for customizing the PDF palette- all color values are optional and must use hex format like
#173d73 - omitted color slots fall back to the default library palette
primaryis used for the header accent, item table header, and final total rowonPrimaryis used for text rendered on top ofprimarytext,metaText, andmutedTextcover body text, metadata, and section labelsbordercontrols table and totals bordersfooterTextcontrols the footer color
Issuer
interface Issuer extends Party {
logo?: string;
}issuer is the document emitter and the source of header branding.
issuer.logois the logo rendered in the PDF headerissuer.nameis rendered in the header brand block- issuer address and business identifiers render directly below the issuer name in the header
seller
seller is the commercial party shown in the document body.
sellerremains separate fromissuer- use
issuerfor brand/emitter identity and header logo - use
sellerfor the legal or operational selling entity shown in the document sections
Party
interface Party {
name: string;
addressLines: string[];
email?: string;
phone?: string;
city?: string;
country?: string;
taxId?: string;
ice?: string;
if?: string;
rc?: string;
}Notes:
- all party fields are optional modules too
- only provided party fields are rendered
- issuer identifiers
ICE,IF, andRCare rendered directly inside issuer details when provided - seller identifiers
ICE,IF, andRCare rendered in the seller section when provided
LineItemInput
interface LineItemInput {
name: string;
description?: string;
quantity: number;
unitPrice?: number;
price?: number;
unit?: string;
discountRate?: number;
}Notes:
- financial documents only need
unitPriceorpricewhen you actually include priced items - delivery notes do not require pricing
discountRateis a decimal ratio, for example:0.1for 10%0.05for 5%
DiscountInput
interface DiscountInput {
type: "percentage" | "fixed";
value: number;
label?: string;
}Examples:
{ type: "fixed", value: 150, label: "Remise commerciale" }{ type: "percentage", value: 0.02, label: "Escompte" }PaymentTerms
interface PaymentTerms {
label: string;
dueDate?: Date;
notes?: string;
}BankInfo
Local transfer
{
type: "local",
bankName: "Attijariwafa Bank",
holderName: "IC Labs SARL",
rib: "007 810 000123456789012345"
}International transfer
{
type: "international",
bankName: "BMCI Corporate",
holderName: "IC Labs SARL",
swiftCode: "BMCIMAMC",
iban: "MA64001122000001234567890123"
}Financial Model
For invoices, quotes, and purchase orders, the package calculates:
- line subtotal
- line discount amount
- line total
- subtotal
- document discount total
- taxable base
- VAT amount
- grand total
VAT
- default VAT rate is
0.2 - VAT is displayed as
TVA - rate input uses decimal form:
0.2= 20%0.1= 10%0= 0%
Currency
- default currency is
MAD - amounts are formatted with two decimals
Moroccan Business Context
The package is tailored for Morocco:
MADdefault currencyTVAterminology- issuer company fields:
- seller company fields:
ICEIFRC
- French-first labels
- Arabic labels available via locale
- date formatting in
dd/MM/yyyy
Localization
Supported locales:
fr-MAar-MA
Example:
const quote = createQuote({
number: "DEV-2026-0012",
locale: "ar-MA",
issuer: {
name: "IC Labs SARL",
addressLines: ["Casablanca"]
},
seller: {
name: "IC Distribution SARL",
addressLines: ["Casablanca"]
},
client: {
name: "Client",
addressLines: ["Rabat"]
},
items: [{ name: "Audit", quantity: 1, price: 1200 }]
});Arabic PDF rendering
Arabic labels are supported out of the box. The renderer first tries your explicit toPDF({ fonts }) values, then falls back to DejaVu Sans font bytes embedded in the package. The fallback does not require font files from node_modules at runtime.
await quote.toPDF({
outputPath: "./devis-ar.pdf",
fonts: {
regular: "./fonts/NotoSansArabic-Regular.ttf",
bold: "./fonts/NotoSansArabic-Bold.ttf"
}
});You can still provide your own Arabic-capable fonts when you want a different visual style. Custom font values may be filesystem paths, Uint8Array instances, or ArrayBuffer instances.
Testing
npm testbuilds the library and runs the automatednode:testsuite- automated tests cover totals, VAT edge cases, validation, sparse document modules, delivery and return note normalization, bundled Arabic fallback, SVG/WEBP remote logos, and table pagination
npm run smokestill generates end-to-end sample PDFs for manual inspection
Remote Logo Support
You can pass a remote image URL through issuer.logo.
issuer: {
name: "IC Labs SARL",
logo: "https://example.com/assets/logo.png",
addressLines: ["Casablanca"]
}Behavior:
- the library fetches
issuer.logoitself duringtoPDF() - the user only provides the URL
- the logo is rendered in the document header
Supported remote image formats include:
PNGJPEG/JPGSVGWEBP
Behavior details:
- the renderer converts non-native PDF image formats like
SVGandWEBPinto a PDF-safe PNG buffer internally - unsupported or unreadable remote images still fail with a clear runtime error
Validation Rules
The library validates document input before building the document model.
Examples of enforced rules:
- provided issuer, seller, and client names must be non-empty
- provided address lines must be non-empty
- items are validated only when you provide them
- provided item quantity must be greater than zero
- priced financial items require
unitPriceorprice - negative prices are rejected
- negative discounts are rejected
vatRatemust be between0and1issuer.logomust be a validhttporhttpsURL- bank fields must be present according to bank mode
- color overrides must use hex values like
#173d73
Rendering Behavior
Financial documents
Invoices, quotes, and purchase orders render:
- only the modules you provide
- for example: issuer block, seller block, client block, metadata, item table, totals, payment terms, bank details, notes, footer
Delivery notes
Delivery notes render:
- only the modules you provide
- item tables remain quantity-only when items are present
Delivery notes do not render:
- unit price
- amount
- subtotal
- discount
- VAT
- total
- bank details
- payment terms block
Return notes
Return notes render:
- only the modules you provide
- item tables remain quantity-only when items are present
Return notes do not render:
- unit price
- amount
- subtotal
- discount
- VAT
- total
- bank details
- payment terms block
Examples
Invoice with international transfer info
import { createInvoice } from "@ic-labs/invoice-kit";
const invoice = createInvoice({
issuer: {
name: "IC Labs SARL",
addressLines: ["Casablanca"]
},
seller: {
name: "IC Distribution SARL",
addressLines: ["Casablanca"]
},
client: {
name: "Noxel SAS",
addressLines: ["Paris"]
},
items: [
{
name: "ERP Deployment",
quantity: 3,
price: 1800,
unit: "jour"
}
],
vatRate: 0.2,
footer: "IC Labs SARL\[email protected]",
bankInfo: {
type: "international",
bankName: "BMCI Corporate",
holderName: "IC Labs SARL",
swiftCode: "BMCIMAMC",
iban: "MA64001122000001234567890123"
}
});Quote with local bank details
import { createQuote } from "@ic-labs/invoice-kit";
const quote = createQuote({
issuer: {
name: "IC Labs SARL",
addressLines: ["Casablanca"]
},
seller: {
name: "IC Distribution SARL",
addressLines: ["Casablanca"]
},
client: {
name: "Client",
addressLines: ["Rabat"]
},
items: [
{
name: "Audit",
quantity: 1,
price: 1200
}
],
discounts: [{ type: "percentage", value: 0.05, label: "Remise devis" }],
footer: "Validite 15 jours\nwww.iclabs.ma",
bankInfo: {
type: "local",
bankName: "Attijariwafa Bank",
holderName: "IC Labs SARL",
rib: "007 810 000123456789012345"
}
});Delivery note without pricing
import { createDeliveryNote } from "@ic-labs/invoice-kit";
const deliveryNote = createDeliveryNote({
issuer: {
name: "IC Labs SARL",
addressLines: ["Casablanca"]
},
seller: {
name: "IC Distribution SARL",
addressLines: ["Casablanca"]
},
client: {
name: "Client",
addressLines: ["Rabat"]
},
items: [
{ name: "Laptop", quantity: 2, unit: "piece" },
{ name: "Mouse", quantity: 3, unit: "piece" }
],
notes: "Marchandise remise au responsable du site.",
footer: "Reception marchandise\nSignature et cachet"
});Return note without pricing
import { createReturnNote } from "@ic-labs/invoice-kit";
const returnNote = createReturnNote({
issuer: {
name: "IC Labs SARL",
addressLines: ["Casablanca"]
},
seller: {
name: "IC Distribution SARL",
addressLines: ["Casablanca"]
},
client: {
name: "Client",
addressLines: ["Rabat"]
},
items: [
{ name: "Laptop", quantity: 1, unit: "piece" },
{ name: "Dock USB-C", quantity: 2, unit: "piece" }
],
notes: "Retour marchandise suite a non-conformite.",
footer: "Bon pour retour\nSignature et cachet"
});Development
Available scripts:
npm run typechecknpm run buildnpm run smoke
Publishing
The repository is configured for GitHub Actions based publishing.
Current release model:
- CI runs on pushes and pull requests
- publish workflow runs on pushes to
main - npm publish only happens if the current
package.jsonversion does not already exist on npm
That means a release requires a version bump before pushing to main.
Limitations
- delivery and return notes intentionally do not display financial values
License
MIT
