Files
broker/pkg/pretrade/router_middleware_test.go
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

108 lines
2.6 KiB
Go

package pretrade
import (
"context"
"errors"
"testing"
)
type fakeReq struct {
accountID string
investorID string
offeringID string
side string
qty string
}
type fakeResp struct {
id string
}
func enrich(_ context.Context, accountID string, raw any) (*Order, error) {
r, ok := raw.(*fakeReq)
if !ok {
return nil, errors.New("bad req")
}
return &Order{
AccountID: accountID,
InvestorID: r.investorID,
OfferingID: r.offeringID,
Side: r.side,
Qty: r.qty,
Symbol: "OSAGE",
}, nil
}
func TestWrap_AllowDelegates(t *testing.T) {
c, tx, a, k, ch := happyProviders()
g, _ := newGateT(t, c, tx, a, k, ch)
called := false
inner := SubmitFunc[*fakeReq, *fakeResp](func(_ context.Context, _ string, _ *fakeReq) (*fakeResp, error) {
called = true
return &fakeResp{id: "abc"}, nil
})
w := Wrap(g, enrich, inner)
resp, err := w(context.Background(), "acct1", &fakeReq{investorID: "inv1", offeringID: "off1", side: "buy", qty: "1"})
if err != nil {
t.Fatalf("Wrap: %v", err)
}
if !called {
t.Fatal("inner was not called")
}
if resp == nil || resp.id != "abc" {
t.Fatalf("resp = %+v", resp)
}
}
func TestWrap_DenyShortCircuits(t *testing.T) {
c, tx, a, k, ch := happyProviders()
k.s.Verified = false
g, _ := newGateT(t, c, tx, a, k, ch)
called := false
inner := SubmitFunc[*fakeReq, *fakeResp](func(_ context.Context, _ string, _ *fakeReq) (*fakeResp, error) {
called = true
return &fakeResp{id: "abc"}, nil
})
w := Wrap(g, enrich, inner)
_, err := w(context.Background(), "acct1", &fakeReq{investorID: "inv1", offeringID: "off1", side: "buy", qty: "1"})
if err == nil {
t.Fatal("expected error")
}
if called {
t.Fatal("inner should not have been called")
}
ge, ok := IsGateError(err)
if !ok {
t.Fatalf("expected GateError, got %v", err)
}
if !ge.IsDeny() {
t.Fatalf("expected deny, got %+v", ge.Decision)
}
}
func TestWrap_EscalateShortCircuits(t *testing.T) {
c, tx, a, k, ch := happyProviders()
c.suit.Ambiguous = true
g, _ := newGateT(t, c, tx, a, k, ch)
called := false
inner := SubmitFunc[*fakeReq, *fakeResp](func(_ context.Context, _ string, _ *fakeReq) (*fakeResp, error) {
called = true
return &fakeResp{id: "abc"}, nil
})
w := Wrap(g, enrich, inner)
_, err := w(context.Background(), "acct1", &fakeReq{investorID: "inv1", offeringID: "off1", side: "buy", qty: "1"})
if err == nil {
t.Fatal("expected error")
}
if called {
t.Fatal("inner should not have been called")
}
ge, ok := IsGateError(err)
if !ok {
t.Fatalf("expected GateError, got %v", err)
}
if !ge.IsEscalate() {
t.Fatalf("expected escalate, got %+v", ge.Decision)
}
}