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]>
108 lines
2.6 KiB
Go
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)
|
|
}
|
|
}
|