System pluginów
Ksefnik ma modularną architekturę opartą na pluginach. Core zawiera 6 passów reconciliation (order 100-500), a pluginy dodają kolejne (order 600+). Architektura “plugin by absence” oznacza, że brak pakietu = darmowa wersja, bez żadnego DRM.
Architektura “plugin by absence”
Dział zatytułowany „Architektura “plugin by absence””Ksefnik nie sprawdza licencji, nie dzwoni do żadnego serwera, nie blokuje funkcji. Zamiast tego:
// Wewnątrz ksefnik — ładowanie pluginutry { const proPlugin = await import('@ksefnik/pro-reconciliation'); registry.register(proPlugin.default);} catch { // Pakiet nie zainstalowany — brak dodatkowych passów // Działa darmowa wersja z 6 core passami}Interfejs KsefPlugin
Dział zatytułowany „Interfejs KsefPlugin”Każdy plugin implementuje interfejs KsefPlugin:
interface KsefPlugin { /** Unikalna nazwa pluginu */ name: string;
/** Wersja pluginu (semver) */ version: string;
/** Dodatkowe passy reconciliation */ reconciliationPasses?(): ReconciliationPass[];
/** Hook wywoływany po zakończeniu reconciliation */ onReconciliationComplete?(report: ReconciliationReport): Promise<void>;
/** Dodatkowe narzędzia MCP */ mcpTools?(): McpTool[];
/** Dodatkowe komendy CLI */ cliCommands?(): CliCommand[];}Interfejs ReconciliationPass
Dział zatytułowany „Interfejs ReconciliationPass”Każdy pass reconciliation implementuje:
interface ReconciliationPass { /** Unikalna nazwa passa */ name: string;
/** Kolejność wykonania (core: 100-500, pluginy: 600+) */ order: number;
/** Wykonaj dopasowanie */ execute( unmatchedInvoices: Invoice[], unmatchedTransactions: BankTransaction[], ): Promise<Match[]>;}Rejestracja pluginów
Dział zatytułowany „Rejestracja pluginów”Ręczna rejestracja
Dział zatytułowany „Ręczna rejestracja”import { createKsefnik } from '@ksefnik/core';import { ecommercePlugin } from './my-ecommerce-plugin';
const ksef = createKsefnik({ config: { nip: '5213456784' },});
ksef.plugins.register(ecommercePlugin);Ładowanie z pakietu npm
Dział zatytułowany „Ładowanie z pakietu npm”const ksef = createKsefnik({ config: { nip: '5213456784' },});
// Załaduj plugin z pakietu npm — jeśli nie jest zainstalowany, loguje warningawait ksef.plugins.load('@ksefnik/pro-reconciliation');PluginRegistry — kolejność passów
Dział zatytułowany „PluginRegistry — kolejność passów”Registry scala passy z core i pluginów, sortując je po order:
| Źródło | Pass | Order |
|---|---|---|
| Core | ksefRef | 100 |
| Core | exactNip | 200 |
| Core | invoiceRef | 300 |
| Core | fuzzy | 400 |
| Core | partial | 450 |
| Core | proximity | 500 |
| Plugin | ecommerceOrderId | 600 |
| Plugin | recurringPayment | 650 |
| Plugin | multiCurrency | 700 |
// Sprawdź zarejestrowane passyconst passes = ksef.reconciliation.getPasses();for (const pass of passes) { console.log(`${pass.name} (order: ${pass.order})`);}Tworzenie własnego pluginu
Dział zatytułowany „Tworzenie własnego pluginu”Minimalny plugin z jednym passem
Dział zatytułowany „Minimalny plugin z jednym passem”import type { KsefPlugin, ReconciliationPass, Match } from '@ksefnik/core';
/** Pass dopasowujący po numerze zamówienia e-commerce w tytule przelewu */const ecommerceOrderPass: ReconciliationPass = { name: 'ecommerceOrderId', order: 600,
async execute(invoices, transactions): Promise<Match[]> { const matches: Match[] = [];
for (const inv of invoices) { // Szukaj numeru zamówienia w metadanych faktury const orderId = inv.metadata?.orderId; if (!orderId) continue;
const tx = transactions.find(t => t.title.includes(orderId) );
if (tx && Math.abs(tx.amount) === inv.grossAmount) { matches.push({ invoice: inv, transaction: tx, pass: 'ecommerceOrderId', confidence: 0.92, }); } }
return matches; },};
export const ecommercePlugin: KsefPlugin = { name: 'ecommerce-reconciliation', version: '1.0.0',
reconciliationPasses() { return [ecommerceOrderPass]; },
async onReconciliationComplete(report) { const ecommerceMatches = report.matched.filter( m => m.pass === 'ecommerceOrderId' ); console.log( `E-commerce plugin dopasował ${ecommerceMatches.length} faktur po numerze zamówienia` ); },};Rejestracja i użycie
Dział zatytułowany „Rejestracja i użycie”import { createKsefnik } from '@ksefnik/core';import { ecommercePlugin } from './ecommerce-plugin';
const ksef = createKsefnik({ config: { nip: '8971234561' },});
ksef.plugins.register(ecommercePlugin);
const invoices = await ksef.invoices.fetch({ from: '2026-03-01', to: '2026-03-31',});const transactions = await ksef.bank.importStatement('./wyciag.csv');
const report = await ksef.reconciliation.run(invoices, transactions);
// passBreakdown zawiera też passy z pluginówfor (const pb of report.passBreakdown) { console.log(`${pb.pass}: ${pb.matchCount} dopasowań`);}Plugin z narzędziami MCP i komendami CLI
Dział zatytułowany „Plugin z narzędziami MCP i komendami CLI”export const advancedPlugin: KsefPlugin = { name: 'advanced-analytics', version: '2.0.0',
mcpTools() { return [ { name: 'analyze_trends', description: 'Analiza trendów dopasowań w czasie', parameters: { months: { type: 'number', description: 'Liczba miesięcy do analizy' }, }, handler: async (params) => { // logika analizy trendów return { trend: 'improving', matchRateChange: '+5%' }; }, }, ]; },
cliCommands() { return [ { name: 'trends', description: 'Pokaż trendy dopasowań', handler: async (args) => { // logika komendy CLI }, }, ]; },};Przykład: firma tworzy plugin dla e-commerce
Dział zatytułowany „Przykład: firma tworzy plugin dla e-commerce”- Firma sprzedaje online i klienci podają numer zamówienia w tytule przelewu (np.
Zamówienie #ORD-2026-1234). - Core passes nie dopasowują tych przelewów, bo tytuł nie zawiera NIP ani numeru faktury.
- Firma tworzy plugin z passem
ecommerceOrderId(order 600), który szuka numerów zamówień. - Po zainstalowaniu pluginu match rate rośnie z 72% do 91%.
- Plugin działa obok core passes — nie zastępuje ich, tylko uzupełnia pipeline.