@voyantjs/plugin-smartbill
v0.119.2
Published
SmartBill e-invoicing sync adapter bundle for Voyant.
Downloads
22,479
Readme
@voyantjs/plugin-smartbill
SmartBill e-invoicing sync adapter bundle for Voyant.
Architecturally, this package is primarily:
- a SmartBill e-invoicing adapter
- a subscriber bundle for finance invoice events
- an optional packaged bundle when an app wants one installable entrypoint
It subscribes to invoice events and creates, cancels, or syncs invoices via the SmartBill REST API for Romanian tax compliance.
Install
pnpm add @voyantjs/plugin-smartbillUsage
import { smartbillPlugin } from "@voyantjs/plugin-smartbill"
import { createApp } from "@voyantjs/hono"
const smartbillSync = smartbillPlugin({
username: env.SMARTBILL_USERNAME,
apiToken: env.SMARTBILL_API_TOKEN,
companyVatCode: "RO12345678",
seriesName: (event) => (event.channel === "online" ? "WEB" : "A"),
mentions: async (event) => `Booking ${event.bookingCode ?? event.id}`,
observations: "Generated by Voyant",
art311SpecialRegimeText: "Custom Art. 311 disclosure",
// optional: language, art311SpecialRegime, events, mapEvent, logger, onError
artifacts: {
db: appDb,
documentStorage,
},
})
const app = createApp({
plugins: [smartbillSync],
})smartbillPlugin(...) is the packaged distribution helper. At runtime, the
package behaves primarily as a subscriber-driven SmartBill sync adapter. By
default it wires up 4 subscribers (invoice.issued,
invoice.proforma.issued, invoice.voided,
invoice.external.sync.requested) that create, cancel, and check payment
status on SmartBill. All error handling is fire-and-forget per the EventBus
contract.
When artifacts.db is configured, successful invoice/proforma creation also
registers the SmartBill external reference through @voyantjs/finance. When
artifacts.documentStorage is configured, the plugin downloads the generated
SmartBill PDF, uploads it to document storage, and records both a ready
invoice_renditions row and a smartbill_pdf invoice_attachments row. The
upload path defaults to invoices/<invoiceId>/smartbill/...; pass
artifacts.documentStorageKeyPrefix to customize it. Existing
smartbill_pdf attachments are reused so repeat event delivery does not upload
the same PDF again.
seriesName, mentions, and observations may be static strings or
event-specific callbacks. The packaged plugin awaits these callbacks in its
default mapper, so each invoice event can choose a SmartBill series or
compliance text based on document type, sales channel, or booking metadata.
When artifacts.db is configured, duplicate invoice.issued and
invoice.proforma.issued deliveries are idempotent by default: the plugin
checks for an existing non-error SmartBill external reference before creating a
new SmartBill document. Disable this with
idempotency: { skipExistingExternalRef: false } only when the caller owns
deduplication elsewhere. Create failures are recorded as SmartBill external refs
with status: "error" and syncError; onError(event, error) can be supplied
for application-specific reporting.
Use syncSmartbillInvoice({ db, invoiceId, pluginOptions }) to run the same
create-or-retry flow from an admin action. It loads the finance invoice,
booking, line items, and tax metadata, maps them through the configured
SmartBill options, reuses an existing non-error SmartBill ref when present, and
persists external refs/PDF artifacts through the configured artifact runtime.
Apps that use @voyantjs/hono can mount the packaged admin module:
import { createSmartbillAdminModule } from "@voyantjs/plugin-smartbill/hono"
const app = createApp({
plugins: [smartbillSync],
modules: [
createSmartbillAdminModule({
pluginOptions: {
username: env.SMARTBILL_USERNAME,
apiToken: env.SMARTBILL_API_TOKEN,
companyVatCode: "RO12345678",
seriesName: "A",
artifacts: { documentStorage },
},
}),
],
})The admin module mounts POST /v1/admin/smartbill/invoices/:id/sync. The route
uses the request database for external-ref and artifact persistence unless
pluginOptions.artifacts.db supplies a custom database resolver.
Use retrySmartbillInvoiceArtifact({ runtime, client, externalRef, documentType
}) to re-download and re-attach a SmartBill PDF from an existing external ref
without issuing a new document.
Invoice UI
The optional ./invoice-ui entry ships React helpers for invoice detail pages.
It reads SmartBill refs from /v1/finance/invoices/:id/external-refs using the
@voyantjs/finance-react provider context and can be mounted in
InvoiceDetailPage's integration slot.
import { InvoiceDetailPage } from "@voyantjs/finance-react/ui"
import { SmartbillInvoicePanel } from "@voyantjs/plugin-smartbill/invoice-ui"
export function InvoicePage({ invoiceId }: { invoiceId: string }) {
return (
<InvoiceDetailPage
id={invoiceId}
slots={{
integrationsContent: ({ invoice }) => (
<SmartbillInvoicePanel invoiceId={invoice.id} />
),
}}
/>
)
}SmartbillInvoicePanel displays the SmartBill series, number, document type,
sync status, sync errors, and document/PDF links when the external ref contains
them. By default, send and retry actions call
POST /v1/admin/smartbill/invoices/:id/sync, and proforma conversion calls the
finance POST /v1/finance/invoices/:id/convert-to-invoice endpoint. Pass
sendAction, retryAction, or convertProformaAction to override those
defaults. For custom layouts, use useSmartbillInvoiceRef(invoiceId) together with
resolveSmartbillInvoiceReferenceParts(ref) and
getSmartbillInvoiceDocumentLinks(ref).
Workflow Factories
The package also ships scheduler-agnostic workflow factories for recurring
SmartBill maintenance. They return async functions that can be called from
@voyantjs/workflows, a Cloudflare cron handler, Trigger.dev, Hatchet, or any
other job runner.
import {
createSmartbillDriftReconciler,
createSmartbillProformaConversionPoller,
} from "@voyantjs/plugin-smartbill"
const pollProformas = createSmartbillProformaConversionPoller({
db,
client: smartbillClient,
source: "invoices",
requestSpacingMs: 350,
onConverted: async (proformaRef, conversion) => {
// Record a Voyant payment or emit a domain event in the host app.
// The plugin reports the SmartBill invoice series/number and source
// proforma ref, but leaves payment semantics to the consumer.
},
})
const reconcileSmartbill = createSmartbillDriftReconciler({
db,
client: smartbillClient,
source: "invoices",
discoverRemote: true,
requestSpacingMs: 350,
onFinding: async (finding) => {
// Log, alert, or open an operator ticket.
},
onMissingLocal: async (finding) => {
const pdf = await finding.remote.accessors?.viewPdf()
// Create a local invoice or attach SmartBill evidence in the host app.
},
})createSmartbillProformaConversionPoller scans SmartBill proforma external refs
and calls client.listEstimateInvoices(...) to detect SmartBill-created final
invoices. createSmartbillDriftReconciler verifies known SmartBill refs by
default. Pass discoverRemote: true to walk SmartBill invoice/proforma series
with client.listSeries() and report documents that exist remotely without a
local external ref as missing_local; those findings include lazy PDF and
payment-status/conversion lookup accessors on finding.remote.accessors.
Both workflow factories use invoice_external_refs by default. Pass
source: "invoices" to derive candidates from finance invoice rows instead, or
pass listCandidateInvoices for a custom invoice-table/source query. Candidate
refs are materialized as SmartBill external refs when db is available; custom
sources can override that writeback with recordCandidateExternalRef. Consumers
can still pass listRemoteDocuments to provide their own remote inventory. The
reconciler only reports drift; it does not delete, void, or create finance
records. Pass requestSpacingMs to either factory to enforce a minimum interval
between SmartBill requests made by the workflow, including remote discovery and
lazy remote-document accessors returned by discovery.
Exports
| Entry | Description |
| --- | --- |
| . | Barrel re-exports |
| ./plugin | smartbillPlugin(options) — packaged adapter/subscriber bundle |
| ./client | createSmartbillClient — createInvoice, cancelInvoice, viewPdf, getPaymentStatus, etc. |
| ./hono | createSmartbillAdminModule(options) and admin sync routes |
| ./sync | syncSmartbillInvoice(...) and event-level sync helpers |
| ./invoice-ui | Optional React hooks, display helpers, and SmartbillInvoicePanel for invoice detail integrations |
| ./mock | createSmartbillMockServer — stateful local SmartBill-compatible mock for tests |
| ./workflows | Proforma conversion polling and drift reconciliation factories |
| ./types | SmartBill adapter and bundle types |
Rate limits
SmartBill can block an account after bursty traffic. createSmartbillClient
throws SmartbillRateLimitError when a response carries SmartBill's
rate-limit shape, with retryAfterMs, retryAfterAt, and blockedAt when the
response text contains enough timing data.
For cron or batch pollers, enable the process-local circuit breaker so repeated calls do not keep hitting SmartBill while the account is blocked:
import {
createSmartbillClient,
SmartbillRateLimitCircuitOpenError,
SmartbillRateLimitError,
} from "@voyantjs/plugin-smartbill/client"
const client = createSmartbillClient({
username,
apiToken,
rateLimit: {
circuitBreaker: true,
},
})
try {
await client.listEstimateInvoices(companyVatCode, seriesName, number)
} catch (err) {
if (
err instanceof SmartbillRateLimitError ||
err instanceof SmartbillRateLimitCircuitOpenError
) {
// Stop the batch and retry after `err.retryAfterMs`.
}
}Workflow factories also stop the current run after SmartbillRateLimitError or
SmartbillRateLimitCircuitOpenError, returning results processed before the
limit was reached and recording the rate-limit failure through onError.
Local SmartBill Mock
SmartBill does not provide a practical sandbox, and invoice/proforma calls can create real accounting documents. Use the packaged mock for local workflows and end-to-end tests instead of pointing development credentials at the live API.
For in-process tests, pass the mock fetch implementation and any local
apiUrl:
import { createSmartbillClient } from "@voyantjs/plugin-smartbill/client"
import { createSmartbillMockServer } from "@voyantjs/plugin-smartbill/mock"
const smartbill = createSmartbillMockServer()
const client = createSmartbillClient({
username: "local",
apiToken: "local",
apiUrl: "http://smartbill.local/SBORO/api",
fetch: smartbill.fetch,
})For full app tests, start the local HTTP listener and wire the plugin/client to the returned base URL:
const smartbill = createSmartbillMockServer()
const server = await smartbill.listen({ port: 4555 })
const plugin = smartbillPlugin({
username: "local",
apiToken: "local",
apiUrl: server.apiUrl,
companyVatCode: "RO12345678",
seriesName: "SB-TEST",
})
await server.close()The mock supports the SmartBill endpoints used by the plugin and common local billing flows:
GET /taxGET /seriesPOST /invoiceGET /invoice/pdfGET /invoice/paymentstatusPUT /invoice/cancelPUT /invoice/reversePUT /invoice/restoreDELETE /invoicePOST /estimateGET /estimate/pdfGET /estimate/invoices
Documents are stateful and deterministic per series. Generated PDF URLs use the
smartbill-mock://test-document/... scheme, and stored document mentions are
marked with TEST DOCUMENT - SmartBill local mock.
License
Apache-2.0
