Files
zeekay 7b09f3d3d0 feat(pretrade): G-37 synchronous pre-trade compliance gate
New pkg/pretrade implementing Gap G-37 (Stage 6.5): synchronous gate
at order-entry that fetches state from cap-table, transfer, AML, KYC,
and on-chain ERC-3643 compliance providers and decides allow / deny /
escalate. Each predicate is a composable internal function; the
Decision carries every actionable reason in a single round.

Wired into the broker order path via Wrap[Req, Resp] generic
middleware so existing router signatures stay untouched (additive,
forwards-only). GateError exposes the Decision to callers that surface
required actions to the investor.

Audit is delegated through AuditSink; canonical AuditLogSink bridges
to broker/pkg/audit. PII (ClientIP) is masked to /24 / /48 in the
audit row.

Test coverage: happy path, every hard-deny predicate (kyc_not_verified,
kyc_stale, not_accredited, accreditation_stale_{income,third_party},
suitability_{missing,stale}, aml_not_clean, open_sar,
ofac_screen_stale, lockup_active, rule144_holding_period,
country_restricted, max_holders, transfer_restricted,
chain_compliance_refused), every escalate predicate
(suitability_ambiguous, kyc_near_expiry, accreditation_near_expiry),
upstream-provider error propagation, input validation, max-holder
sell-side exemption, max-holder existing-holder exemption, PII
redaction. 41 tests passing under -race.

Source-of-design: Lux-Prior-IP
Source-ref: f7a8d7a
Clean-Team-Member: dev-agent (P0 G-37 implementation)
Signed-off-by: dev <[email protected]>
2026-05-26 18:11:50 -07:00

62 lines
2.8 KiB
Go

package pretrade
import "context"
// CapTableProvider returns accreditation, suitability, and offering-level
// restriction state for an investor / offering pair. Backed in production
// by github.com/luxfi/captable. Defined here so the gate can be
// constructed with mocks in tests and so the consumed surface is
// minimal and stable.
type CapTableProvider interface {
// Accreditation returns the investor's accreditation snapshot, or an
// error if the investor is unknown.
Accreditation(ctx context.Context, investorID string) (AccreditationStatus, error)
// Suitability returns the investor's suitability snapshot for the
// offering, or an error if the investor is unknown.
Suitability(ctx context.Context, investorID, offeringID string) (SuitabilityStatus, error)
// Restrictions returns the offering-level restriction snapshot for
// the investor.
Restrictions(ctx context.Context, investorID, offeringID string) (RestrictionStatus, error)
}
// TransferProvider exposes the canonical transfer-restriction Check for
// the cap-table-of-record. Backed in production by github.com/luxfi/
// transfer/pkg/restrictions. The gate uses this as a cross-check against
// the offering-level RestrictionStatus: any disagreement is a hard deny.
type TransferProvider interface {
// AllowTransfer answers whether the proposed quantity of an offering
// can be transferred to or from the investor (depending on side).
AllowTransfer(ctx context.Context, investorID, offeringID, side string, qty int64) (bool, []string, error)
}
// AMLProvider returns the AML / OFAC snapshot for an investor.
type AMLProvider interface {
// Status returns the AML snapshot or an error if the investor is
// unknown to the AML service.
Status(ctx context.Context, investorID string) (AMLStatus, error)
}
// KYCProvider returns the KYC snapshot for an investor.
type KYCProvider interface {
// Status returns the KYC snapshot or an error if the investor is
// unknown.
Status(ctx context.Context, investorID string) (KYCStatus, error)
}
// ChainComplianceProvider delegates the on-chain ERC-3643 modular
// compliance check. Backed in production by github.com/luxfi/securities/
// erc3643/compliance.go. The provider is queried only when the order
// carries a non-empty TokenAddress.
type ChainComplianceProvider interface {
// CanTransfer asks the on-chain compliance module whether the
// transfer is permitted under the token's attached modules.
CanTransfer(ctx context.Context, tokenAddr, fromInvestorID, toInvestorID string, qty int64) (ChainComplianceStatus, error)
}
// AuditSink receives a Decision for every Check evaluation. Implementations
// must be non-blocking and concurrency-safe. The broker wires this to the
// canonical broker/pkg/audit log; tests pass a slice-collector.
type AuditSink interface {
RecordPreTrade(decision *Decision, order *Order)
}