mirror of
https://github.com/luxfi/broker.git
synced 2026-09-24 00:25:18 +00:00
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]>
84 lines
2.0 KiB
Go
84 lines
2.0 KiB
Go
package pretrade
|
|
|
|
import (
|
|
"encoding/json"
|
|
|
|
"github.com/luxfi/broker/pkg/audit"
|
|
)
|
|
|
|
// AuditLogSink adapts a *audit.Log into the local AuditSink. Use this
|
|
// in production wiring; in tests pass a SliceSink.
|
|
type AuditLogSink struct {
|
|
Log *audit.Log
|
|
}
|
|
|
|
// RecordPreTrade writes a single audit entry per evaluation. The action
|
|
// is the existing risk-decision channel ("route.decision") so existing
|
|
// SIEM consumers see pre-trade decisions through one stream; the
|
|
// Metadata map carries the structured decision payload.
|
|
func (s *AuditLogSink) RecordPreTrade(d *Decision, o *Order) {
|
|
if s == nil || s.Log == nil || d == nil {
|
|
return
|
|
}
|
|
entry := audit.Entry{
|
|
Action: audit.ActionRouteDecision,
|
|
AccountID: o.AccountID,
|
|
Symbol: o.Symbol,
|
|
Side: o.Side,
|
|
Qty: o.Qty,
|
|
Price: o.Price,
|
|
RequestID: o.RequestID,
|
|
ClientIP: o.ClientIP,
|
|
Status: statusFor(d),
|
|
Metadata: map[string]interface{}{
|
|
"investor_id": o.InvestorID,
|
|
"offering_id": o.OfferingID,
|
|
"token_address": o.TokenAddress,
|
|
"country_code": o.CountryCode,
|
|
"decision": marshal(d),
|
|
"required_actions": d.RequiredActions,
|
|
"latency_ns": d.LatencyNs,
|
|
},
|
|
}
|
|
s.Log.Record(entry)
|
|
}
|
|
|
|
func statusFor(d *Decision) string {
|
|
switch {
|
|
case d.Allow:
|
|
return "allow"
|
|
case d.Deny:
|
|
return "deny"
|
|
case d.Escalate:
|
|
return "escalate"
|
|
default:
|
|
return "unknown"
|
|
}
|
|
}
|
|
|
|
func marshal(v interface{}) string {
|
|
b, err := json.Marshal(v)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return string(b)
|
|
}
|
|
|
|
// SliceSink collects decisions into a slice for unit tests. Not safe
|
|
// for concurrent producers — use a Mutex if a concurrent test ever
|
|
// needs it.
|
|
type SliceSink struct {
|
|
Entries []SliceEntry
|
|
}
|
|
|
|
// SliceEntry is one captured pre-trade decision.
|
|
type SliceEntry struct {
|
|
Decision *Decision
|
|
Order *Order
|
|
}
|
|
|
|
// RecordPreTrade satisfies the AuditSink interface.
|
|
func (s *SliceSink) RecordPreTrade(d *Decision, o *Order) {
|
|
s.Entries = append(s.Entries, SliceEntry{Decision: d, Order: o})
|
|
}
|