import type { CalculatedDiscountLine, DiscountProposal } from "./discount-calculator"; /** * Discount authority ceilings: how far one set of grant envelopes reaches on * one proposed document (docs/planning/discount-authority.md §4, §7 in * MainAPI). * * Shared by the API, which enforces, and the Angular client, which previews, * so both resolve a ceiling to the same minor unit. The rule: * * 1. Each envelope is one grant's `Constraint`, tested as a **complete * envelope**: its own line ceiling covers every line and its own document * ceiling covers the total. The document is allowed when at least one * envelope does. Ceilings are never combined across envelopes -- no * per-dimension maxima, no sums (§4.1). * 2. A grant without a ceiling, or with a malformed one, refuses the whole * decision, even when another envelope would cover: an unconstrained grant * must never read as "No Limit" (§9). * 3. An AMOUNT in another currency cannot authorize, and neither covers nor * blocks the envelopes that can (§5.4). * * Ceilings resolve to money against the same basis before any comparison * (§4.2): `PERCENT p` → `floor(B × p / 100)`, `AMOUNT a` → `min(B, a)`, * `UNLIMITED` → `B`. Exact, in integer minor units, with no tolerance. * * Pure: no I/O, no clock, no authorization model. Which grants apply -- scope, * expiry, SA/EA -- is the caller's decision; `ApplicableCeilingEnvelopes` * makes it from the client login `Authz` for the UI, and MainAPI makes it from * the session for enforcement. The API always decides; a UI result is a guide. */ export type MonetaryCeiling = { readonly Mode: "UNLIMITED"; } | { readonly Mode: "PERCENT"; readonly Value: number; } | { readonly Mode: "AMOUNT"; readonly Value: number; readonly Currency: string; }; /** One grant's narrowing of a discount capability: the `Constraint` in login `Authz` rules. */ export interface MonetaryCeilingConstraint { readonly Type: "MONETARY_CEILING"; readonly LineCeiling: MonetaryCeiling; readonly DocumentCeiling: MonetaryCeiling; } export declare const UnlimitedMonetaryCeiling: MonetaryCeilingConstraint; /** Every amount is a non-negative fixed-point string at the currency's precision, e.g. "145.00". */ export interface MonetaryCeilingLine { /** Where the line sits, e.g. `Items[2]`. Reported on refusal. */ readonly Location: string; readonly LineId: string | null; /** B_i: the line's eligible basis. */ readonly Basis: string; /** D_i: every manual concession on the line, counted once. */ readonly ManualDiscount: string; } export interface MonetaryCeilingDocument { /** B: Σ B_i over the lines supplied. Checked, not trusted. */ readonly Basis: string; /** D: the document's total manual concession. Checked, not trusted. */ readonly ManualDiscount: string; /** * The part of D that belongs to the document rather than to any line -- a * negative `Adjust`, normalized into concession. Defaults to zero. It lets * the total be verified: `D = Σ D_i + this`. */ readonly AdjustmentConcession?: string; } /** The document side of the evaluation: what `MonetaryCeilingProposalOf` builds from a proposal. */ export interface MonetaryCeilingProposal { readonly Currency: { readonly Code: string; readonly MinorUnitExponent: number; }; /** SALE lines only: returns, non-discountable and non-goods rows carry no basis. */ readonly Lines: readonly MonetaryCeilingLine[]; readonly Document: MonetaryCeilingDocument; } export interface MonetaryCeilingInput extends MonetaryCeilingProposal { /** * One entry per applicable grant, in a stable order: its `Constraint`, or * null/undefined for a grant that carries none. SA and EA pass one * `UnlimitedMonetaryCeiling`. Empty means no grant applies. */ readonly Envelopes: readonly (MonetaryCeilingConstraint | null | undefined)[]; } export declare const MonetaryCeilingIssueCodes: Readonly<{ readonly INPUT_MALFORMED: "INPUT_MALFORMED"; /** No envelope was supplied: the user holds no applicable grant. */ readonly NO_APPLICABLE_GRANT: "NO_APPLICABLE_GRANT"; /** An applicable grant carries no ceiling. Never read as "No Limit". */ readonly CEILING_MISSING: "CEILING_MISSING"; readonly CEILING_MALFORMED: "CEILING_MALFORMED"; /** Every envelope is an AMOUNT in another currency; none can authorize. */ readonly CEILING_CURRENCY_MISMATCH: "CEILING_CURRENCY_MISMATCH"; /** No single envelope covers every line and the total. */ readonly AUTHORITY_LIMIT_EXCEEDED: "AUTHORITY_LIMIT_EXCEEDED"; }>; export type MonetaryCeilingIssueCode = (typeof MonetaryCeilingIssueCodes)[keyof typeof MonetaryCeilingIssueCodes]; export interface MonetaryCeilingLineView { readonly Location: string; readonly LineId: string | null; readonly Basis: string; readonly Proposed: string; /** This envelope's ceiling on this line, in money. */ readonly Ceiling: string; } export interface MonetaryCeilingDocumentView { readonly Basis: string; readonly Proposed: string; readonly Ceiling: string; } /** One envelope resolved to money against this document. */ export interface MonetaryCeilingEnvelopeView { /** Position of the envelope in `Envelopes`, so the caller can name the grant behind it. */ readonly EnvelopeIndex: number; readonly Lines: readonly MonetaryCeilingLineView[]; readonly Document: MonetaryCeilingDocumentView; } /** * The most permissive envelope, for showing what this document allows. * Envelopes are only partially ordered (§4.1): `Dominant` is true when this one * is at least as permissive as every other on every line and the total. When * false, another grant allows some shape this one does not. */ export interface MonetaryCeilingPreview extends MonetaryCeilingEnvelopeView { readonly Dominant: boolean; } export interface MonetaryCeilingFailure { readonly Kind: "LINE" | "DOCUMENT"; /** `DOCUMENT` for the total. */ readonly Location: string; readonly LineId: string | null; readonly Basis: string; readonly Proposed: string; readonly Ceiling: string; } export interface MonetaryCeilingAllowed { readonly Allowed: true; readonly Currency: string; /** The witness: the complete envelope that covers the proposal. The preview when it covers. */ readonly Evaluated: MonetaryCeilingEnvelopeView; readonly Preview: MonetaryCeilingPreview; } export interface MonetaryCeilingRefused { readonly Allowed: false; readonly Reason: MonetaryCeilingIssueCode; /** Limit refusals only: the first line, else the total, that exceeds the preview envelope. */ readonly Failure?: MonetaryCeilingFailure; /** Present whenever the envelopes could be resolved, so the UI can still guide. */ readonly Preview?: MonetaryCeilingPreview; } export type MonetaryCeilingResult = MonetaryCeilingAllowed | MonetaryCeilingRefused; /** The document field a calculated line came from, e.g. `Items[2]` or `Ops[0]`. */ export declare function DiscountLineLocation(line: Pick): string; /** The document side of the evaluation, from an accepted `CalculateDiscountProposal` result. */ export declare function MonetaryCeilingProposalOf(proposal: DiscountProposal): MonetaryCeilingProposal; /** One rule of a capability in the client login `Authz.Caps`. */ export interface ClientCeilingRule { readonly Scope?: { readonly Type: string; readonly Ids: readonly (string | number)[]; }; readonly ExpiresAt?: string; readonly Constraint?: MonetaryCeilingConstraint; } /** The part of the client login `Authz` this reads. */ export interface ClientCeilingAuthz { readonly SA?: boolean; readonly EA?: boolean; readonly Caps?: Readonly>; } /** * The envelopes that apply to a document in `entityId` at `nowEpochMs`, from * the client login `Authz` -- the same filter the API applies to the session: * SA/EA are unlimited; a rule without `Scope` is the session's own entity; a * scoped rule applies only when it is ENTITY scope naming `entityId`; an * expired rule never applies. A capability held as `true` has no ceiling, so it * yields one missing envelope and the evaluation refuses (§9). */ export declare function ApplicableCeilingEnvelopes(authz: ClientCeilingAuthz | null | undefined, capabilityKey: string, entityId: number, nowEpochMs: number): (MonetaryCeilingConstraint | undefined)[]; /** * Decides whether a set of grant envelopes covers one proposed document's * manual concessions. Pure and deterministic; never throws, and never clamps -- * a refusal carries where and by how much. */ export declare function EvaluateMonetaryCeiling(input: MonetaryCeilingInput): MonetaryCeilingResult;