mirror of
https://github.com/luxfi/captable.git
synced 2026-10-30 17:01:02 +00:00
G-03 — SEC EDGAR Form D adapter (pkg/edgar): - FormDFiling typed model covering primary issuer, related persons, industry, federal exemptions, sales/offering amounts, investor count, sales commissions, use of proceeds, and signature blocks - marshalFormD emits the SEC Form D XML primaryDocument shape; buildSGMLSubmission wraps it in the EDGARLink Online SUBMISSION SGML envelope (TYPE / CIK / CCC / CONTACT / DOCUMENT) - Filer with FileFormD / AmendFormD / GetFilingStatus, CIK normalization, exponential-backoff + jitter retry with Retry-After honoring, REJECTED-status → ErrSubmissionRejected, 4xx → *APIError - 32 conformance tests against httptest.Server fixtures G-04 — Blue-Sky state notice adapter (pkg/bluesky): - Registrar dispatches to per-state StateAdapter implementations - EFDAdapter (NASAA Electronic Filing Depository) covers 49 states + DC with bearer-token auth, ACH-payment metadata, and renewal support - Real state-portal adapters for FL, TX, NY, CA, MA — five highest- volume non-EFD / hybrid-portal states - Per-state fee calculator with flat + scaled fee schedules (CA / NY / WA scale with offering size); EFD system fee layered on - 30 conformance tests against httptest.Server fixtures compliance bridge (pkg/compliance/filings.go): - FormDFiler / BlueSkyFiler interfaces let the compliance Service delegate filings without importing the wire packages directly - compliance.FileFormD / AmendFormD / FileBlueSkyNotice update the in-domain record with the regulator-assigned identifier, status, filing date, and fee - 6 bridge tests with in-memory fakes Coverage: go build / vet / -race clean across the full repo. 62 tests in the two new packages + 6 in the compliance bridge, all passing. Source-of-design: Public-Spec Source-ref: https://www.sec.gov/info/edgar/specifications/formdxml.pdf Source-ref: https://www.sec.gov/edgar/filer-information/current-edgar-technical-specifications Source-ref: http://nasaaefd.org Source-ref: http://nasaaefd.org/About/FormDStates Clean-Team-Member: dev-agent (P0 G-03 / G-04 implementation) Signed-off-by: dev <[email protected]>
123 lines
3.6 KiB
Go
123 lines
3.6 KiB
Go
package compliance
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
// Service handles securities compliance — Form D, blue sky filings, and accreditation checks.
|
|
type Service struct {
|
|
repo Repository
|
|
formDFiler FormDFiler
|
|
blueSkyFiler BlueSkyFiler
|
|
}
|
|
|
|
// NewService creates a compliance service.
|
|
func NewService(repo Repository) *Service {
|
|
return &Service{repo: repo}
|
|
}
|
|
|
|
// CreateFormD creates a new Form D filing record.
|
|
func (s *Service) CreateFormD(ctx context.Context, f *FormD) error {
|
|
if f.CompanyID == "" {
|
|
return fmt.Errorf("company_id is required")
|
|
}
|
|
if f.Exemption == "" {
|
|
return fmt.Errorf("exemption is required")
|
|
}
|
|
if f.TotalOffering <= 0 {
|
|
return fmt.Errorf("total_offering must be positive")
|
|
}
|
|
now := time.Now().UTC()
|
|
f.CreatedAt = now
|
|
f.UpdatedAt = now
|
|
f.TotalRemaining = f.TotalOffering - f.TotalSold
|
|
if f.Status == "" {
|
|
f.Status = "draft"
|
|
}
|
|
return s.repo.CreateFormD(ctx, f)
|
|
}
|
|
|
|
// CheckRegDCompliance validates a Reg D offering against SEC rules.
|
|
func (s *Service) CheckRegDCompliance(f *FormD) *ComplianceCheckResult {
|
|
result := &ComplianceCheckResult{Compliant: true}
|
|
|
|
switch f.Exemption {
|
|
case "506b":
|
|
// Rule 506(b): max 35 non-accredited investors.
|
|
if f.NumNonAccredited > 35 {
|
|
result.Compliant = false
|
|
result.Violations = append(result.Violations,
|
|
fmt.Sprintf("506(b) allows max 35 non-accredited investors, have %d", f.NumNonAccredited))
|
|
}
|
|
// Warning at 30+.
|
|
if f.NumNonAccredited >= 30 && f.NumNonAccredited <= 35 {
|
|
result.Warnings = append(result.Warnings,
|
|
fmt.Sprintf("approaching 506(b) non-accredited limit: %d/35", f.NumNonAccredited))
|
|
}
|
|
|
|
case "506c":
|
|
// Rule 506(c): ALL investors must be accredited.
|
|
if f.NumNonAccredited > 0 {
|
|
result.Compliant = false
|
|
result.Violations = append(result.Violations,
|
|
fmt.Sprintf("506(c) requires all investors to be accredited, have %d non-accredited", f.NumNonAccredited))
|
|
}
|
|
|
|
case "504":
|
|
// Rule 504: max $10M in 12 months.
|
|
if f.TotalOffering > 10_000_000 {
|
|
result.Compliant = false
|
|
result.Violations = append(result.Violations,
|
|
fmt.Sprintf("Rule 504 max offering is $10M, offering is $%.2f", f.TotalOffering))
|
|
}
|
|
}
|
|
|
|
// Form D must be filed within 15 days of first sale.
|
|
if f.FirstSaleDate != nil && f.FilingDate == nil {
|
|
deadline := f.FirstSaleDate.Add(15 * 24 * time.Hour)
|
|
if time.Now().After(deadline) {
|
|
result.Compliant = false
|
|
result.Violations = append(result.Violations,
|
|
"Form D not filed within 15 days of first sale")
|
|
} else {
|
|
daysLeft := int(time.Until(deadline).Hours() / 24)
|
|
result.Warnings = append(result.Warnings,
|
|
fmt.Sprintf("Form D filing deadline in %d days", daysLeft))
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// CreateBlueSkyFiling creates a new state filing record.
|
|
func (s *Service) CreateBlueSkyFiling(ctx context.Context, b *BlueSkyFiling) error {
|
|
if b.CompanyID == "" {
|
|
return fmt.Errorf("company_id is required")
|
|
}
|
|
if b.State == "" {
|
|
return fmt.Errorf("state is required")
|
|
}
|
|
if b.FilingType == "" {
|
|
return fmt.Errorf("filing_type is required")
|
|
}
|
|
if b.Status == "" {
|
|
b.Status = "pending"
|
|
}
|
|
return s.repo.CreateBlueSkyFiling(ctx, b)
|
|
}
|
|
|
|
// GetRequiredStates returns the list of US states that typically require blue sky notice filings.
|
|
// This is a simplified reference — actual requirements vary by exemption and offering type.
|
|
func (s *Service) GetRequiredStates() []string {
|
|
return []string{
|
|
"AL", "AK", "AZ", "AR", "CA", "CO", "CT", "DE", "FL", "GA",
|
|
"HI", "ID", "IL", "IN", "IA", "KS", "KY", "LA", "ME", "MD",
|
|
"MA", "MI", "MN", "MS", "MO", "MT", "NE", "NV", "NH", "NJ",
|
|
"NM", "NY", "NC", "ND", "OH", "OK", "OR", "PA", "RI", "SC",
|
|
"SD", "TN", "TX", "UT", "VT", "VA", "WA", "WV", "WI", "WY",
|
|
"DC",
|
|
}
|
|
}
|