@majikah/majik-invoice
v0.0.10
Published
Domain model for post-quantum secured invoices — base GeneralInvoice with double-entry accounting projections, and MajikInvoice with hybrid Ed25519 + ML-DSA-87 signing and optional ML-KEM-768 encryption.
Maintainers
Readme
Majik Invoice
Domain model for post-quantum secured invoices — structured accounting primitives with optional hybrid digital signatures and encryption.
Part of the Majikah ecosystem.
- Majik Invoice
- Overview
- Features
- Installation
- Usage
- GeneralInvoice — basic
- GeneralInvoice — with* mutation
- GeneralInvoice — updating a line item
- GeneralInvoice — tax operations
- GeneralInvoice — accounting projections
- GeneralInvoice — lifecycle
- MajikInvoice — signed-only
- MajikInvoice — encrypted-and-signed
- MajikInvoice — multi-sig with allowlist
- MajikInvoice — reissue after changes
- Error types
- Accounting branch support
- Serialization
- Related Projects
- Contributing
- License
- Author
- Contact
Overview
@majikah/majik-invoice provides two invoice domain objects designed to work together:
GeneralInvoice is a pure, accounting-neutral invoice model. It handles line items, tax calculation, discounts, totals, and can project itself into double-entry journal entries and sub-ledger entries. It has no cryptographic dependencies and can be used standalone in any accounting context.
MajikInvoice wraps GeneralInvoice with cryptographic security — hybrid Ed25519 + ML-DSA-87 digital signatures via @majikah/majik-signature, and optional ML-KEM-768 encryption via @majikah/majik-envelope. It supports two modes: signed-only (plaintext invoice with integrity seal) and encrypted-and-signed (invoice encrypted for specific recipients, with a public summary always visible).
Both classes are immutable by design. All mutation methods follow the with* pattern and return new instances — originals are never modified.
Features
- Immutable domain objects —
with*pattern throughout; originals are never mutated - Auto-computed totals — subtotal, tax, discount, and grand total derived from line items; never stale
- Per-line and invoice-level tax — supports VAT, GST, withholding tax, inclusive and exclusive rates, multi-jurisdiction
- BIR-compatible
Partymodel — TIN, registered address, branch code, RDO district, trade name, nature of business; globally compatible via ISO 3166-1 and ISO 4217 - Accounting projections —
toJournalEntry()produces balanced double-entry journal entries;toSubLedgerEntry()produces AR/AP sub-ledger entries - Multi-branch accounting support — commercial, tax, government, project, forensic, environmental, and more via
InvoiceTypediscriminant - Lifecycle state machine — explicit status transitions with guards (
draft → issued → paid → void) - Post-quantum signing — hybrid Ed25519 + ML-DSA-87 via
@majikah/majik-signature - Post-quantum encryption — ML-KEM-768 + AES-256-GCM via
@majikah/majik-envelope - Multi-sig with allowlists — restrict which keys may sign; seal when complete
- Deterministic canonical serialization —
toCanonicalBytes()is stable for signing and hashing - Full
MajikMoneyintegration — arbitrary-precision, currency-aware arithmetic via@thezelijah/majik-money - Round-trip stable JSON —
toJSON()/fromJSON()with fullMajikMoneyserialization and rehydration
Installation
npm install @majikah/majik-invoicePeer dependencies
npm install @thezelijah/majik-moneyFor MajikInvoice (signing and encryption):
npm install @majikah/majik-key @majikah/majik-signature @majikah/majik-envelopeUsage
GeneralInvoice — basic
import { GeneralInvoice } from "@majikah/majik-invoice";
const invoice = GeneralInvoice.create({
issuer: {
legalName: "Acme Corp",
tin: "123-456-789-000",
address: {
line1: "123 Ayala Avenue",
city: "Makati",
stateOrProvince: "Metro Manila",
postalCode: "1226",
country: "PH",
branchCode: "000",
},
},
recipient: {
legalName: "Beta Inc",
tin: "987-654-321-000",
},
currency: "PHP",
defaultTax: { taxType: "VAT", rate: 0.12 },
lineItems: [
{ description: "Web Development", quantity: 1, unitPrice: 50000 },
{ description: "UI Design", quantity: 3, unitPrice: 8000 },
],
});
console.log(invoice.totals.grandTotal.format()); // "₱73,024.00"
console.log(invoice.hasTax); // true
console.log(invoice.taxBreakdown());
// [{ taxType: "VAT", rate: 0.12, taxableBase: 74000, taxAmount: 8880, ... }]GeneralInvoice — with* mutation
All mutations return new instances. The original is always untouched.
const draft = GeneralInvoice.create({ ... });
const updated = draft
.withLineItem({ description: "Hosting", quantity: 1, unitPrice: 5000 })
.withInvoiceNumber("INV-2025-001")
.withDueDate("2025-05-22")
.withTag("q2")
.withMetadata({ projectId: "proj-abc" })
.withStatus("issued");
console.log(updated.lineItemCount); // 3
console.log(updated.formattedTotal); // "₱78,624.00"
console.log(updated.isDraft); // falseGeneralInvoice — updating a line item
const lineItemId = invoice.lineItems[0].id;
const corrected = invoice.withUpdatedLineItem(lineItemId, {
quantity: 2,
unitPrice: 45000,
});GeneralInvoice — tax operations
// Apply a specific tax to one line
const withWht = invoice.withTaxOnLineItem(lineItemId, {
taxType: "WHT",
rate: 0.02,
label: "Withholding Tax (2%)",
});
// Change the invoice-level default tax
const withGst = invoice.withDefaultTax({ taxType: "GST", rate: 0.10 });
// Remove tax from all lines
const taxFree = invoice.withoutTaxOnAllLineItems();
// Breakdown by type (useful for BIR returns)
const breakdown = invoice.taxBreakdown();
// [{ taxType: "VAT", taxableBase: 74000, taxAmount: 8880 }, ...]
// Tax total for a specific type
const vatOnly = invoice.taxTotalByType("VAT"); // 8880GeneralInvoice — accounting projections
// Journal entry (double-entry, always "draft")
const entry = invoice.toJournalEntry();
// DR Accounts Receivable 78,624.00
// CR Revenue 74,000.00 (or per account code)
// CR Tax Payable 8,880.00 (if tax applies)
// With a custom Chart of Accounts
const entry = invoice.toJournalEntry({
accounts: {
receivable: "1100",
revenue: "4100",
tax: "2200",
},
});
// AR sub-ledger entry
const arEntry = invoice.toSubLedgerEntry();
// { type: "AR", partyName: "Beta Inc", balance: 78624, status: "open" }
// FX conversion (read-only — does not modify invoice)
const usdTotals = invoice.computeWithFxRate(0.0175, "USD");
// { grandTotal: 1375.92, formatted: { grandTotal: "US$1,375.92" } }
// Amount due after partial payment
const paid = MajikMoney.fromMajor(30000, "PHP");
const due = invoice.amountDue(paid);
const isFullyPaid = invoice.isFullyPaid(paid); // falseGeneralInvoice — lifecycle
console.log(invoice.allowedTransitions()); // ["issued", "void"]
console.log(invoice.canTransitionTo("paid")); // false
const issued = invoice.withStatus("issued");
const paid = issued.withStatus("paid");
// Voided invoices are terminal
const voided = paid.withStatus("void");
voided.withNotes("test"); // throws InvoiceMutationErrorMajikInvoice — signed-only
import { MajikInvoice } from "@majikah/majik-invoice";
// aliceKey must be unlocked and have signing keys
const invoice = await MajikInvoice.create({
mode: "signed-only",
signerKey: aliceKey,
issuer: { legalName: "Alice Corporation", tin: "123-456-789-000" },
recipient: { legalName: "Bob Inc" },
currency: "PHP",
defaultTax: { taxType: "VAT", rate: 0.12 },
lineItems: [
{ description: "Design Services", quantity: 1, unitPrice: 50000 },
],
});
console.log(invoice.status); // "sealed"
console.log(invoice.public.formattedTotal); // "₱56,000.00"
console.log(invoice.isSigned); // true
// Verify signatures
const results = await invoice.verifySignatures();
// [{ valid: true, signerId: "alice-fingerprint", ... }]
// Access the inner GeneralInvoice directly
const general = invoice.invoice;
const entry = general.toJournalEntry();MajikInvoice — encrypted-and-signed
const invoice = await MajikInvoice.create({
mode: "encrypted-and-signed",
signerKey: aliceKey,
recipientKeys: [bobKey],
issuer: { legalName: "Alice Corporation" },
recipient: { legalName: "Bob Inc" },
currency: "PHP",
lineItems: [
{ description: "Confidential Services", quantity: 1, unitPrice: 100000 },
],
});
// Public summary is always visible without decryption
console.log(invoice.public.issuerName); // "Alice Co"
console.log(invoice.public.totalAmount); // 100000
console.log(invoice.canDecrypt(bobKey)); // true
// Decrypt with Bob's key
const general = await invoice.decrypt(bobKey);
console.log(general.totals.grandTotal.format()); // "₱100,000.00"
// Decrypted invoice cached on the instance
const general2 = invoice.invoice; // uses cache — no re-decryptMajikInvoice — multi-sig with allowlist
const invoice = await MajikInvoice.create({
mode: "signed-only",
signerKey: aliceKey,
expectedSigners: [
{ signerId: aliceKey.fingerprint, label: "Issuer" },
{ signerId: bobKey.fingerprint, label: "Approver" },
],
...invoiceInput,
});
// Alice has signed; Bob still pending
console.log(invoice.pendingSigners); // [{ signerId: "bob-fingerprint", label: "Approver" }]
console.log(invoice.isFullySigned); // false
// Bob co-signs
const cosigned = await invoice.sign(bobKey);
console.log(cosigned.isFullySigned); // true
// Alice seals (as the allowlist issuer)
const sealed = await cosigned.seal(aliceKey);
console.log(sealed.isSealed); // true
sealed.sign(bobKey); // throws MajikInvoiceSealError — no further signaturesMajikInvoice — reissue after changes
// Modify the inner invoice (returns a new GeneralInvoice)
const updated = invoice.invoice
.withLineItem({ description: "Extra Work", quantity: 2, unitPrice: 5000 })
.withNotes("Revised scope");
// Reissue as a new MajikInvoice — all signatures cleared, re-sign
const reissued = await invoice.reissue(updated, { signerKey: aliceKey });
console.log(reissued.status); // "sealed"Error types
| Error | When thrown |
| -------------------------------- | -------------------------------------------------- |
| InvoiceValidationError | Invalid GeneralInvoice input |
| InvoiceLifecycleError | Illegal status transition |
| InvoiceMutationError | Structural mutation on non-draft or voided invoice |
| InvoiceProjectionError | Unbalanced or empty invoice projected to journal |
| LineItemValidationError | Invalid line item input |
| MajikInvoiceError | General MajikInvoice error (base class) |
| MajikInvoiceValidationError | Invalid MajikInvoice structure or input |
| MajikInvoiceKeyError | Locked key, missing signing/ML-KEM keys |
| MajikInvoiceEncryptionError | Encryption or decryption failure |
| MajikInvoiceSignatureError | Signing or verification failure |
| MajikInvoiceSealError | Illegal seal operation |
| MajikInvoiceSerializationError | Malformed JSON on fromJSON() |
Accounting branch support
InvoiceType controls which accounting branch the invoice belongs to. The same GeneralInvoice structure supports all branches — no parallel class hierarchies.
| Type | Branch |
| --------------- | ------------------------------------------------- |
| commercial | Financial Accounting — standard B2B |
| proforma | Pre-invoice, not a legal document |
| credit | Credit note / reversal (journal entries reversed) |
| debit | Debit note |
| tax | Tax Accounting — VAT / GST invoice |
| government | Government / Public procurement |
| intercompany | Managerial — internal transfer pricing |
| project | Project Accounting — milestone billing |
| recurring | Subscription / periodic billing |
| forensic | Forensic / Audit — flagged for investigation |
| environmental | Social & Environmental Accounting |
Serialization
Both classes are round-trip stable. MajikMoney instances are serialized via serializeMoney() and rehydrated via deserializeMoney() on fromJSON(). Totals are always re-derived from line items on deserialization — they can never be stale or tampered with independently.
// Serialize
const json = invoice.toJSON();
const str = JSON.stringify(json);
// Rehydrate
const restored = GeneralInvoice.fromJSON(JSON.parse(str));
// Canonical bytes — deterministic, for signing
const bytes = invoice.toCanonicalBytes();Related Projects
Majik Key
Seed phrase account library — required peer dependency for signing.
Majik Signature
A hybrid post-quantum content signing and verification library for the Majikah ecosystem. Built on top of Majik Key, it provides tamper-proof, forgery-resistant digital signatures for any content format — plaintext, JSON, PDF, audio, video, binary — using a dual-algorithm architecture that combines classical Ed25519 with post-quantum ML-DSA-87 (FIPS-204).
Majik Envelope
Majik Envelope is the core cryptographic engine of the Majik Message platform. It provides a post-quantum secure "envelope" format that handles message encryption, multi-recipient key encapsulation, and transparent compression using NIST-standardized algorithms.
Contributing
If you want to contribute or help extend support to more platforms, reach out via email. All contributions are welcome!
License
Apache-2.0 — free for personal and commercial use.
Author
Made with 💙 by @thezelijah
Developer: Josef Elijah Fabian
GitHub: https://github.com/jedlsf
Project Repository: https://github.com/Majikah/majik-invoice
Contact
- Business Email: [email protected]
- Official Website: https://www.thezelijah.world
