Files
zeekay 14a9f303ff feat: add SAFE, note, warrant, vesting, KYC, OmniSub domain packages
New packages (types.go + repository.go + service.go each):
- safe: YC SAFE taxonomy (pre/post money, cap, discount, MFN, pro rata)
- note: convertible notes (interest accrual, conversion triggers)
- warrant: exercise price, expiration, cash/cashless exercise
- vesting: schedules with tranches, cliff, frequency, performance conditions
- kyc: KYC/AML submissions, accreditation verification, provider hooks
- omnisub: Alpaca OmniSub accounts, sub-accounts, orders, positions, tax

Enhanced existing packages:
- securities/types.go: ContractAddress, ChainID, Exemption enum (Reg D/S/A+/CF)
- compliance/types.go: RegDCheck (506b/c), RegSCheck, full Rule144Check
2026-04-06 21:02:32 -07:00

140 lines
3.8 KiB
Go

package omnisub
import (
"context"
"fmt"
"time"
)
// Service handles omnibus account and sub-account management.
type Service struct {
repo Repository
}
// NewService creates an omnisub service backed by the given repository.
func NewService(repo Repository) *Service {
return &Service{repo: repo}
}
// CreateOmnibusAccount validates and persists a new omnibus account.
func (s *Service) CreateOmnibusAccount(ctx context.Context, a *OmnibusAccount) error {
if a.CompanyID == "" {
return fmt.Errorf("company_id is required")
}
if a.Name == "" {
return fmt.Errorf("name is required")
}
now := time.Now().UTC()
a.CreatedAt = now
a.UpdatedAt = now
if a.Status == "" {
a.Status = StatusActive
}
return s.repo.CreateOmnibusAccount(ctx, a)
}
// GetOmnibusAccount retrieves an omnibus account by ID.
func (s *Service) GetOmnibusAccount(ctx context.Context, id string) (*OmnibusAccount, error) {
if id == "" {
return nil, fmt.Errorf("omnibus account id is required")
}
return s.repo.GetOmnibusAccount(ctx, id)
}
// CreateSubAccount validates and persists a new sub-account.
func (s *Service) CreateSubAccount(ctx context.Context, a *SubAccount) error {
if a.OmnibusAccountID == "" {
return fmt.Errorf("omnibus_account_id is required")
}
if a.StakeholderID == "" {
return fmt.Errorf("stakeholder_id is required")
}
if a.AccountNumber == "" {
return fmt.Errorf("account_number is required")
}
now := time.Now().UTC()
a.CreatedAt = now
a.UpdatedAt = now
if a.Status == "" {
a.Status = StatusActive
}
return s.repo.CreateSubAccount(ctx, a)
}
// GetSubAccount retrieves a sub-account by ID.
func (s *Service) GetSubAccount(ctx context.Context, id string) (*SubAccount, error) {
if id == "" {
return nil, fmt.Errorf("sub-account id is required")
}
return s.repo.GetSubAccount(ctx, id)
}
// CloseSubAccount marks a sub-account as closed.
func (s *Service) CloseSubAccount(ctx context.Context, id string) error {
a, err := s.repo.GetSubAccount(ctx, id)
if err != nil {
return fmt.Errorf("get sub-account: %w", err)
}
if a.Status == StatusClosed {
return fmt.Errorf("sub-account %s is already closed", a.ID)
}
a.Status = StatusClosed
a.UpdatedAt = time.Now().UTC()
return s.repo.UpdateSubAccount(ctx, a)
}
// SubmitOrder validates and persists a new order.
func (s *Service) SubmitOrder(ctx context.Context, o *Order) error {
if o.SubAccountID == "" {
return fmt.Errorf("sub_account_id is required")
}
if o.SecurityID == "" {
return fmt.Errorf("security_id is required")
}
if o.Quantity <= 0 {
return fmt.Errorf("quantity must be positive")
}
if o.Price < 0 {
return fmt.Errorf("price must be non-negative")
}
if o.Side == "" {
return fmt.Errorf("side is required")
}
now := time.Now().UTC()
o.SubmittedAt = now
o.CreatedAt = now
o.UpdatedAt = now
if o.Status == "" {
o.Status = OrderPending
}
return s.repo.CreateOrder(ctx, o)
}
// ListPositions returns all positions for a sub-account.
func (s *Service) ListPositions(ctx context.Context, subAccountID string) ([]*Position, error) {
if subAccountID == "" {
return nil, fmt.Errorf("sub_account_id is required")
}
return s.repo.ListPositions(ctx, subAccountID)
}
// RecordCorporateAction persists a corporate action event.
func (s *Service) RecordCorporateAction(ctx context.Context, ca *CorporateAction) error {
if ca.SecurityID == "" {
return fmt.Errorf("security_id is required")
}
if ca.Type == "" {
return fmt.Errorf("type is required")
}
ca.CreatedAt = time.Now().UTC()
return s.repo.CreateCorporateAction(ctx, ca)
}
// ListTaxStatements returns all tax statements for a sub-account.
func (s *Service) ListTaxStatements(ctx context.Context, subAccountID string) ([]*TaxStatement, error) {
if subAccountID == "" {
return nil, fmt.Errorf("sub_account_id is required")
}
return s.repo.ListTaxStatements(ctx, subAccountID)
}