Vendix SFA
Product · Pipeline Engine

Customer change requests become DB rows. Not forks.

Every SFA vendor eventually faces the same fork: customer A wants OTP on cash payments, customer B doesn't. Customer C wants a manager-approval step on every invoice over a threshold. Most respond by maintaining per-customer branches. Vendix solves it once: every transaction is an ordered list of steps, and tenants compose the list from a shared catalog.

19
Transaction types
Tenant overrides
1
Codebase

CreateIndirectInvoice · Tenant Demo Pharma

  1. 1 ValidateOutlet default
  2. 2 ResolvePriceList default
  3. 3 CalculateDiscounts default
  4. 4 EnforceCreditLimit tenant override
  5. 5 EvaluateFreeIssues default
  6. 6 AllocateInventoryFEFO default
  7. 7 CashCollectionOtp tenant override
  8. 8 PersistInvoice default
  9. 9 EmitForecastSignal tenant override

3 of 9 steps are tenant-specific. The other 6 ship with the platform.

Same module, different journeys

Tenant CRs without tenant forks.

The traditional way to honor “customer X needs an extra approval step”: a feature flag that rots, or a code branch that bit-rots faster. Vendix takes a third path - TenantTransactionSteps in VendixDb is a per-tenant ordered list of step keys. Want OTP on cash collection? Insert CashCollectionOtp at position 7. Want it gone? Delete the row. No deploy needed.

  • Default pipeline shared by every tenant - the “happy path” lives in one place.
  • Per-tenant overrides via DB rows - insert, replace, or remove steps without code change.
  • Step classes deploy globally - once a step exists, every tenant can opt in.
  • Web and Android share the pipeline - same steps run regardless of input surface.

Two tenants · same module · different journeys

Tenant A - Pharma

  1. 1. ValidateOutlet
  2. 2. ResolvePriceList
  3. 3. EnforceColdChain
  4. 4. CalculateDiscounts
  5. 5. EnforceCreditLimit
  6. 6. PersistInvoice

Tenant B - FMCG

  1. 1. ValidateOutlet
  2. 2. ResolvePriceList
  3. 3. CalculateDiscounts
  4. 4. EvaluateFreeIssues
  5. 5. CashCollectionOtp
  6. 6. PersistInvoice

Same CreateIndirectInvoiceCommand. Same codebase. Different rows in TenantTransactionSteps.

A step is just a small reusable rule

Validate outlet

Ships with the platform. Used by every tenant. Checks the outlet exists, is active, and is on the rep's route.

Enforce credit limit

Ships with the platform. Stops invoices that would put the customer over their limit.

Enforce cold chain

Custom step for a pharma tenant. Blocks an invoice if a temperature-sensitive product is in the load and the cold-truck check hasn't been logged.

Cash collection OTP

Custom step for tenants who want one-time-password confirmation on cash payments above a threshold.

Steps you can add without forking

Platform steps and custom steps look identical.

Whether a step ships with the platform (validate outlet, enforce credit limit) or was built for a specific tenant (enforce cold chain, cash collection OTP), the engine treats them the same way. Each one runs in order, each one can stop the transaction with a clear reason, and each one is recorded in the audit trail.

  • Reusable across transactions - the same "enforce credit limit" step can guard invoices, sales orders, and direct invoices.
  • Clear failure reasons - when a step blocks a transaction, the user sees a business-language explanation, not an error code.
  • Time-aware - long-running pipelines respect timeouts so a slow step never freezes the rest of the system.
  • DI-resolved - steps are registered in DependencyInjection.cs and resolved per request.
Audit trace per step

When something goes wrong, you can see exactly where.

The pipeline executor records every step run - start, finish, duration, success or failure reason - against a correlation ID that ties it to the originating Mediator command. When a tenant says “the invoice didn't go through” you don't dig through logs; you read the step trace.

  • CorrelationIdBehavior stamps every Mediator command, propagated through the whole step chain.
  • Per-step result row in the audit trail with field-level diffs (old → new).
  • Sensitive props auto-redacted - passwords, tokens, connection strings never hit the audit DB.
# pipeline trace · CORR-2026-04-29-09382
[ok] ValidateOutlet · 12ms
[ok] ResolvePriceList · 8ms · DPL-NEG-12 v17
[ok] CalculateDiscounts · 31ms · 2 schemas evaluated
[!] EnforceCreditLimit · 4ms · limit=150K, outstanding=148K → soft warn
[ok] EvaluateFreeIssues · 18ms · 1 rule fired (BXGY)
[ok] AllocateInventoryFEFO · 22ms · 3 batches
[X] CashCollectionOtp · 1ms · aborted: invalid OTP
# total 96ms · pipeline rolled back · audit row written

Designed to absorb requirement churn.

19 transaction types

PO, GRN, Invoice, Sales Order, Sales Return, Direct Invoice Return, Stock Transfer, Customer Payment, Credit Approval - every transaction is pipelined.

Hot-config by row

Override a step by inserting a row in TenantTransactionSteps. Active on next request - no deploy, no restart.

Web + Android equivalent

Same pipeline runs whether the trigger is a Blazor click, a mobile push, a chatbot tool call, or a system-to-system API.

Source-generator CQRS

Pipeline plays nicely with Mediator v3. Validation, audit, correlation behaviours wrap every step run.

Default pipeline first

New tenants get the platform default. Customisation is opt-in, not opt-out - onboarding is one row.

Idempotent on ClientTxnId

Mobile retries are detected and short-circuited inside the pipeline. No duplicate invoices, no manual reconciliation.

Show us the CR your last vendor refused.

Bring a customisation that previous SFA tools said was a six-month rewrite. We'll wire it as a step in front of you and show you the audit trace before the demo ends.