@ic-labs/invoice-kit
v0.1.7
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 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
- 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
- Optional multiline footer rendered at the extreme bottom of the page
- Delivery 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"
},
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
} 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.
Important behavior for delivery 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 delivery notes
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?: BankInfolocale?: "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
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, but Arabic PDFs require Arabic-capable font files when calling toPDF().
await quote.toPDF({
outputPath: "./devis-ar.pdf",
fonts: {
regular: "./fonts/NotoSansArabic-Regular.ttf",
bold: "./fonts/NotoSansArabic-Bold.ttf"
}
});Without custom fonts, Arabic glyph rendering is not reliable in PDF output.
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
Currently supported remote image formats:
PNGJPEG/JPG
Currently rejected remote image formats:
SVGWEBP
Why:
pdfkitdoes not natively embedSVGorWEBP- this package intentionally keeps dependencies light and does not bundle image conversion tooling
Validation Rules
The library validates document input before building the document model.
Examples of enforced rules:
numbermust be present- provided issuer, seller, and client names must be non-empty
- provided address lines must be non-empty
itemsmust contain at least one line item- 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
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
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"
});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
- multi-page table layout is not implemented yet
- remote logos currently support PNG/JPEG only
- Arabic PDF output requires custom fonts
- delivery notes intentionally do not display financial values
License
MIT
