Keyboard shortcuts

Press โ† or โ†’ to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Rullst Capital ๐Ÿ’ฐ

โ€œEnterprise Multi-Gateway Billing, SaaS Analytics & Fiscal Engineโ€

rullst-capital provides a unified financial foundation for SaaS, digital commerce, and marketplace platforms written in Rust. It includes multi-provider adapter surfaces, recurring-subscription models, international payout helpers, and a bounded Brazilian National NFS-e preparation pipeline. Live provider and fiscal production readiness must be established per adapter and environment.


โšก Capability & Lifecycle Matrix

SubsystemLifecycle StatusDescription
Direct Gateways๐ŸŸ  [Partial]11 payment/payout adapter surfaces with pooled HTTP clients and deterministic mocks. Live method coverage, provider acceptance tests, retry semantics, and reconciliation are not uniform yet.
Outbound Failure Boundary๐ŸŸข [Implemented / Bounded]Reviewed live methods share finite timeouts, disabled redirects/ambient proxies, one-MiB JSON parsing, HTTPS checkout-location validation, and redacted permanent/transient/rate-limited failures. Rullst performs no automatic mutation retry.
Subscription Lifecycle๐ŸŸ  [Partial]Checkout, portal, cancellation, pause, usage, coupon, trial, status, and webhook APIs exist, but not every provider implements and verifies every method end-to-end.
Webhook Processing๐ŸŸข [Implemented / Bounded]Axum and opt-in Actix middleware call one canonical bounded verifier; named adapters implement signature verification and freshness checks. The opt-in webhook-sql ledger shares bounded payload or semantic-event claims across SQLite, PostgreSQL, MySQL, and MariaDB processes. Relational handlers can claim a stable provider event ID with one domain mutation in a caller transaction. Cross-system exactly-once and reconciliation remain application work; Alipay RSA2 remains fail-closed.
Metered Billing๐ŸŸข [Implemented / Bounded]Current Stripe Meter Events and Lemon Squeezy Usage Records shapes with provider-specific identity/action, bounded response binding and deterministic non-live mocks. Durable application-outbox claiming and provider-account evidence remain explicit.
Paid Invoice Rendering๐ŸŸข [Implemented / Feature-gated]Exact validated minor units, escaped HTML, bounded paginated A4 PDF and a final-success e-mail/amount/currency binding. The downstream Mail bridge sends the attachment but durable outbox claiming and exactly-once delivery remain application work.
SaaS MRR/ARR Analytics๐ŸŸข [Implemented / Bounded]In-memory revenue metrics and churn calculations for supplied records; this is not an accounting ledger or provider reconciliation engine.
NFS-e 1.01 Local Pipeline๐ŸŸข [Implemented / Bounded]Strict ordinary-service DPS builder, checksum-pinned closed-catalog validation of official XSD sources with one exact documented production regex-anchor compatibility normalization, protected PKCS#12 RSA-SHA256/inclusive-C14N XMLDSig, signed-tpAmb binding, independent local signature verification, deterministic dpsXmlGZipB64 request JSON, bounded signed-authorization and structured-rejection parsing, and bounded rustls mTLS client construction.
NFS-e Local Command Journal๐ŸŸข [Implemented / Bounded]Single-active-writer HMAC-chained prepared/terminal evidence, exact replay/conflict handling, restart recovery of minimized pending descriptors, hard record/byte quotas and externally retainable exact-tip checkpoints. It stores no XML, access key, response messages or certificate data and does not transmit or retry.
NFS-e Offline Sandbox๐ŸŸก [Offline Mock]Deterministic offline mock fixtures (NfseEnvironment::Mock) for local development and CI testing.
SEFIN Live NFS-e Homologation๐Ÿ”ต [Roadmap / External Evidence]Full emitter/ICP-Brasil certificate policy, deployment-owned request/outbox and reconciliation storage, retained official protocol fixtures, real A1 restricted-environment tests, independent review, and official homologation. Transmission is disabled.

๐Ÿ“ฆ Supported Payment & Payout Providers

rullst-capital includes decoupled adapter surfaces for 11 global and regional gateways. The list preserves the intended product reach; it does not mean every provider product, fee, payment method, tax promise, or live API path has been independently homologated by Rullst:

  1. ๐Ÿ’ณ Stripe: Global card checkouts, Customer Portal, and recurring subscriptions.
  2. ๐Ÿ‹ Lemon Squeezy: Merchant of Record (MoR) with automated global tax compliance.
  3. ๐ŸŒŽ Mercado Pago: LATAM subscriptions, Pix, and credit card checkouts.
  4. โšก InfinitePay: Ultra-low-fee domestic Brazilian Pix and installment credit cards.
  5. ๐Ÿ“ฑ PicPay: Brazilian digital wallet and QR-code checkout flows.
  6. ๐Ÿปโ€โ„๏ธ Polar: Developer-first MoR for monetizing GitHub repositories and SaaS software.
  7. ๐Ÿ›ถ Paddle: Global B2B SaaS quote-to-cash with EU VAT handling.
  8. ๐Ÿ‡ฎ๐Ÿ‡ณ Razorpay: Recurring UPI Autopay and credit card orders in India & APAC.
  9. ๐Ÿ’ธ Wise: High-speed, multi-currency international contractor payouts (40+ currencies).
  10. ๐Ÿช™ Coinbase Commerce: On-chain cryptocurrency payments (Bitcoin, Ethereum, Solana, USDC).
  11. ๐ŸŒ Alipay: Cross-border Chinese digital wallet checkouts (ๆ”ฏไป˜ๅฎ).

Provider conformance ladder

Validate every provider operation independently. A successful checkout does not validate a portal, refund, usage report, cancellation or webhook contract, and evidence from one provider cannot be transferred to another.

LevelRequired evidence
1. Deterministic offlineBounds, redaction, failure classification, idempotency material and explicit mock behavior without network access.
2. Protocol fixturesExact signed payloads, negative signature/freshness/replay cases and bounded provider response parsing.
3. Official test environmentProvider sandbox/test-mode checkout, webhook, lifecycle and reconciliation exercises.
4. Controlled live acceptanceThe smallest provider-permitted real transaction only after account, legal, secret, refund, observability and reconciliation controls are ready; retain redacted evidence.

The generated SaaS blueprint exercises an application boundary for Stripe and Lemon Squeezy. It is not a conformance app for all eleven adapters. Record the exact provider, operation, environment and observed result; never summarize partial evidence as โ€œall payments work.โ€ Refer to the official Stripe testing, Stripe sandbox, Lemon Squeezy test-mode, and Lemon Squeezy webhook simulation guides when constructing acceptance cases.


๐Ÿš€ Usage Examples

Shared outbound failure contract

CapitalError::Provider carries a redacted ProviderFailure for request construction, transport, non-success HTTP status, oversized/malformed JSON, or semantic response mismatch. Its provider and operation labels are static and safe for low-cardinality telemetry; the value deliberately omits URLs, credentials, bodies, and raw transport errors.

#![allow(unused)]
fn main() {
use rullst_capital::{CapitalError, ProviderFailureClass};

fn record_disposition(error: &CapitalError) -> &'static str {
    match error {
        CapitalError::Provider(failure) => match failure.class() {
            ProviderFailureClass::Permanent => "permanent",
            ProviderFailureClass::Transient => "transient",
            ProviderFailureClass::RateLimited => "rate_limited",
            _ => "unknown",
        },
        _ => "not_provider_transport",
    }
}
}

HTTP 429 is rate-limited; transport failures and HTTP 408, 425, and 5xx are transient; request-build, response-shape, and other HTTP failures are permanent. Only numeric Retry-After delta seconds are retained and they are capped at 24 hours. These are scheduling hints, not a generic retry engine: non-idempotent operations must not be repeated without a durable, provider-forwarded idempotency key and reconciliation.

1. Initializing a Provider and Creating a Checkout Session

use rullst_capital::providers::stripe::StripeProvider;
use rullst_capital::BillingProvider;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let stripe = StripeProvider::new(
        "sk_live_your_stripe_api_key",
        "whsec_your_webhook_signing_secret",
    );

    let session = stripe
        .create_checkout_session(
            "[email protected]",
            "price_pro_monthly",
            "https://example.com/billing/complete",
        )
        .await?;

    println!("Checkout URL: {session}");
    Ok(())
}

2. Provider-Specific Metered Usage

MeteredBillingProvider uses an associated request type so the framework does not confuse Stripe customer/meter identity with Lemon Squeezy subscription-item identity. StripeMeterEvent implements the current form-encoded Meter Events contract and forwards its identifier as both event identity and idempotency header. LemonSqueezyUsageRecord implements the current JSON:API relationship and requires Increment or Set to match provider aggregation.

Both paths validate positive bounded quantities, bind accepted responses, cap response JSON to one MiB and return visibly non-live deterministic mocks. A Stripe identifier has rolling provider deduplication. Lemonโ€™s application event key is not accepted by the provider request, so claim it in a durable outbox before sending. Live-account acceptance, retry/reconciliation and entitlements remain application/release evidence.

3. Payment-Bound Invoice PDF and Mail

Enable rullst/capital-mail or the separate rullst-capital/invoice-pdf and rullst-mail/capital-invoice features. A PaidInvoice can be constructed only from final Succeeded evidence matching the invoice recipient, exact minor-unit total and currency. PaidInvoiceDelivery::prepare generates escaped HTML and a bounded PDF attachment and runs Mailโ€™s mandatory pre-flight.

The stable delivery key is an application outbox identity, not a distributed lock. The application must reconcile webhooks, claim that key atomically and own at-least-once retries/provider attachment policy.

4. Verified Webhook Signature Handling

The low-level provider contract below illustrates exact-byte verification. HTTP applications should normally mount verify_webhook on Axum or verify_webhook_actix_with_state on Actix so body limits, normalized event insertion, and replay rejection are applied before the handler. The default store is process-local; the opt-in webhook-sql feature accepts an Arc<SqlWebhookReplayStore> in WebhookMiddlewareState for cross-process admission. Active claims are never evicted to admit new work. Webhooks use constant-time cryptographic verification where applicable:

#![allow(unused)]
fn main() {
use axum::{body::Bytes, http::HeaderMap, response::IntoResponse};
use rullst_capital::providers::stripe::StripeProvider;
use rullst_capital::BillingProvider;
use std::collections::HashMap;

pub async fn handle_stripe_webhook(
    headers: HeaderMap,
    body: Bytes,
) -> Result<impl IntoResponse, axum::http::StatusCode> {
    let stripe = StripeProvider::new(
        "sk_live_api_key",
        "whsec_your_webhook_signing_secret",
    );

    let signature = headers
        .get("Stripe-Signature")
        .and_then(|v| v.to_str().ok())
        .ok_or(axum::http::StatusCode::BAD_REQUEST)?;

    let provider_headers = HashMap::from([(
        "stripe-signature".to_string(),
        signature.to_string(),
    )]);

    // Verifies the provider signature and timestamp before parsing the event.
    let event = stripe
        .handle_webhook(&body, &provider_headers)
        .map_err(|_| axum::http::StatusCode::UNAUTHORIZED)?;

    println!(
        "Verified subscription {} with status {:?}",
        event.subscription_id,
        event.status,
    );
    Ok(axum::http::StatusCode::OK)
}
}

SQL-backed middleware claims the payload before dispatch. Treat it as a fail-closed replay firewall, not an exactly-once delivery guarantee. For an atomic relational state change, verify the exact payload through the selected provider, obtain its stable event identifier, then call check_and_record_event_key_with_transaction inside the same transaction as the domain mutation. Provider API calls, e-mail, queues, and other systems still need an outbox, idempotent consumers, and reconciliation.


๐Ÿ›๏ธ Brazilian Digital Invoicing (NFS-e Nacional)

rullst-capital includes a dedicated fiscal module (rullst_capital::fiscal) shaped around the National NFS-e domain. Its local schema, signature, bounded issuance-codec, and mTLS preparation contracts are implemented and tested; it also supplies a bounded authenticated local command journal, but it is not yet an officially homologated issuer.

Enable rullst-capital/nfse (or umbrella rullst/capital-nfse) for the pinned XSD, XMLDSig, GZip/Base64 protocol codec, and mTLS preparation dependencies. Selecting the feature does not enable SEFIN transmission.

Architecture & Pipeline

[SaaS Sale] โ”€โ–บ [NfseDpsV101] โ”€โ–บ [Pinned XSD] โ”€โ–บ [PKCS#12 XMLDSig] โ”€โ–บ [Bounded JSON codec]
                    โ”‚                                                    โ”‚
                    โ–ผ                                                    โ–ผ
          [Offline deterministic fixture]       [HMAC journal; mTLS prepared; transmission disabled]

Emitting an Invoicing Document (DPS)

use rullst_capital::fiscal::{
    build_dps_xml_v1_01, FiscalCustomer, FiscalEmitter, IssRetention,
    IssTaxation, NfseDpsV101, NfseEnvironment, TaxRegime,
};
use chrono::{NaiveDate, Utc};

fn main() -> Result<(), Box<dyn std::error::Error>> {
let emitter = FiscalEmitter {
    cnpj: "12.345.678/0001-90".to_string(),
    inscricao_municipal: "1234567".to_string(),
    legal_name: "Rullst SaaS & Software Ltda".to_string(),
    trade_name: Some("Rullst".to_string()),
    ibge_code: "3550308".to_string(), // Sรฃo Paulo
    tax_regime: TaxRegime::SimplesNacional,
};

let customer = FiscalCustomer {
    doc_number: "123.456.789-00".to_string(),
    name: "Joรฃo Silva".to_string(),
    email: "[email protected]".to_string(),
    zip_code: Some("01310-100".to_string()),
    address: Some("Av Paulista, 1000".to_string()),
    ibge_code: Some("3550308".to_string()),
};

let dps = NfseDpsV101 {
    id: "DPS355030821122233300018100001000000000000101".to_string(),
    series: "1".to_string(),
    number: 101,
    issued_at: Utc::now(),
    competence_date: NaiveDate::from_ymd_opt(2026, 8, 30).ok_or("invalid date")?,
    service_code: "010301".to_string(),
    description: "Assinatura Mensal SaaS Rullst Pro".to_string(),
    amount_cents: 9_900,
    iss_rate_basis_points: Some(200),
    iss_taxation: IssTaxation::Taxable,
    iss_retention: IssRetention::NotRetained,
    service_city_ibge: "3550308".to_string(),
};

let unsigned_xml = build_dps_xml_v1_01(
    &emitter,
    &customer,
    &dps,
    NfseEnvironment::Homologation,
)?;
let _ = unsigned_xml;
Ok(())
}

See Preparing a National NFS-e 1.01 homologation candidate for pinned artifact validation, local signing, and the external gates that still prevent live transmission.

The opt-in journal records a caller-owned opaque command before transport and then one parsed terminal result. Exact replays are read-only; a reused command ID with different request/result material fails closed. pending() returns only the command ID, environment, signed-request digest, local observation time, and sequence needed to reconcile application-owned request storage after a restart. The host must keep the 32-byte HMAC key in a secret manager, use one active writer in a trusted directory, persist checkpoint() independently, and own retention, backup, request/outbox storage, retries, and authority reconciliation.


๐Ÿ”’ Security Invariants

  1. Constant-Time Verification: Webhook signatures use subtle::ConstantTimeEq to prevent side-channel timing attacks.
  2. Fail-Closed Live Modes: Local XMLDSig/XSD/codec/mTLS preparation and command evidence do not enable a request. Homologation and Production return a typed FiscalError::Unsupported without network I/O until the external trust and homologation gates pass.
  3. Bounded Egress: Reviewed live provider methods use a pooled client with finite connect/request timeouts, disabled redirects and ambient proxy discovery, bounded JSON, and redacted typed failure evidence. Returned checkout URLs must be absolute credential-free HTTPS without fragments.