N NezamDocumentation

Configuration resolver patterns#

Configuration-driven ERP behavior needs a repeatable shape. Put variable policy in tables, resolve the exact row in a function or service, and fail closed when required configuration is missing or ambiguous.

This pattern applies to status transitions, approval routing, posting controls, thresholds, reason codes, workflow outcomes, integration routing, retry settings, and other behavior that can vary by company, country, tenant, process, or time.

File split#

FileResponsibility
.fields.blReusable identifiers, codes, dates, and constrained scalar types
.tables.blPolicy/configuration rows and effective dating
.messages.blDeclared user-facing failures for missing or invalid configuration
.functions.blResolver functions and commands that call them
.validations.blReusable validation rules when separated from commands

Keep these responsibilities separate when the policy is shared across modules. A command can be in .functions.bl, but the configurable outcome it depends on should stay in .tables.bl.

Configuration table#

bl
field CompanyCode: string {
  max_length: 10;
}

field DocumentType: string {
  max_length: 20;
}

table PostingPolicy {
  field company_code: CompanyCode key;
  field document_type: DocumentType key;
  field valid_from: date key;
  field valid_to: date optional;
  field active: bool required default(true);
  field requires_approval: bool required default(false);
  field approval_limit: decimal optional min(0);

  constraint primary key (company_code, document_type, valid_from);

  index active_policy on (company_code, document_type) {
    unique: false;
  }

  validation {
    active_has_start: valid_from is not null;
  }
}

Use table fields for all policy values that are not true invariants. Do not model tenant-owned statuses, reasons, thresholds, or routes as enums.

Declared failures#

bl
message posting_policy_required {
  code: "FI-ERR-1004";
  severity: error;
  category: custom("config");
  params: {
    company_code: CompanyCode;
    document_type: DocumentType;
  };
  message: {
    en: "Posting policy is required for the company and document type.";
  };
}

message posting_policy_ambiguous {
  code: "FI-ERR-1005";
  severity: error;
  category: custom("config");
  params: {
    company_code: CompanyCode;
    document_type: DocumentType;
  };
  message: {
    en: "More than one active posting policy matches the company, document type, and date.";
  };
}

error PostingPolicyError;

User-facing failures should be message-backed. Inline string errors make configuration problems harder to localize, route, and operate.

Resolver function#

bl
private function resolve_posting_policy(
  company_code_param: CompanyCode,
  document_type_param: DocumentType,
  posting_date_param: date
): PostingPolicy {
  select var policy: PostingPolicy
    where PostingPolicy.company_code = company_code_param
      and PostingPolicy.document_type = document_type_param
      and PostingPolicy.active = true
      and PostingPolicy.valid_from <= posting_date_param
      and (
        PostingPolicy.valid_to is null
        || PostingPolicy.valid_to >= posting_date_param
      );

  if (policy is null) {
    raise message posting_policy_required(company_code_param, document_type_param) with {
      company_code: company_code_param;
      document_type: document_type_param;
    };
  }

  return policy;
}

A resolver should make every scope dimension explicit. Scope usually includes company or tenant, document or process type, effective date, active state, and sometimes country, business unit, channel, or source system.

Command usage#

bl
public function post_invoice(
  invoice_id_param: uuid,
  posting_date_param: date
): Invoice {
  let invoice = load_invoice(invoice_id_param);
  let policy = resolve_posting_policy(
    invoice.company_code,
    invoice.document_type,
    posting_date_param
  );

  if (policy.requires_approval && invoice.amount > policy.approval_limit) {
    raise message invoice_approval_required(invoice.invoice_id) with {
      invoice_id: invoice.invoice_id;
    };
  }

  invoice.status = "posted";
  save invoice;
  return invoice;
}

Resolve policy before changing persisted state. Failed resolution should leave existing runtime state intact.

Ordered override policy#

If broader and narrower policies are both valid, encode precedence as data. Do not hide fallback order in a string-literal branch tree.

bl
table PostingPolicyOverride {
  field company_code: CompanyCode key;
  field document_type: DocumentType key;
  field country_code: string optional;
  field scope_rank: int required min(0);
  field active: bool required default(true);
  field valid_from: date key;
  field valid_to: date optional;
  field requires_approval: bool required default(false);

  constraint primary key (company_code, document_type, scope_rank, valid_from);
}

In this pattern, a company-only row and a company-plus-country row are both configuration. The resolver orders by scope_rank because the precedence is part of the policy table, not an implicit code fallback.

Configured policy denial#

Separate configuration failures from configured denials.

CaseCategory
Required row is missing, inactive, or ambiguouscustom("config")
Required row exists and blocks the operationcustom("policy")
bl
message invoice_approval_required {
  code: "FI-ERR-1010";
  severity: error;
  category: custom("policy");
  params: {
    invoice_id: uuid;
  };
  message: {
    en: "Invoice approval is required before posting.";
  };
}

This distinction helps operations teams fix the right thing: configuration data for custom("config"), or business process state for custom("policy").

Test coverage#

Every new configuration-driven rule should cover success and failure paths.

bl
test PostingPolicyResolution {
  context invoice: Invoice = sample_invoice;

  setup {
    seed_posting_policy(invoice.company_code, invoice.document_type);
  }

  expect post_invoice(invoice.invoice_id, today()) to be invoice;
}

test MissingPostingPolicyFailsClosed {
  context invoice: Invoice = sample_invoice_without_policy;

  expect post_invoice(invoice.invoice_id, today()) to throw PostingPolicyError;
}

At minimum, cover:

PathCoverage
SuccessOne active row matches all scope and date filters
Missing configNo row matches and the command raises a declared failure
Ambiguous configMore than one row matches and the command raises a declared failure
Inactive configInactive rows are ignored
Effective dateRows outside the valid date range are ignored

Closed-failure checklist#

  • Required configuration is never optional at the command boundary.
  • Missing or ambiguous configuration raises a declared message.
  • No string-literal branch tree encodes configurable policy.
  • Enums are used only for true invariants.
  • The resolver reads from tables and filters every required scope dimension.
  • Tests cover success and failure paths.
Source: packages/business/language/configuration-resolver-patterns.md