mirror of
https://github.com/luxfi/captable.git
synced 2026-10-30 17:01:02 +00:00
246 lines
5.9 KiB
Go
246 lines
5.9 KiB
Go
package tax
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
type memRepo struct {
|
|
mu sync.RWMutex
|
|
divs map[string]*Form1099DIV
|
|
bs map[string]*Form1099B
|
|
k1s map[string]*ScheduleK1
|
|
}
|
|
|
|
func newMemRepo() *memRepo {
|
|
return &memRepo{
|
|
divs: make(map[string]*Form1099DIV),
|
|
bs: make(map[string]*Form1099B),
|
|
k1s: make(map[string]*ScheduleK1),
|
|
}
|
|
}
|
|
|
|
func (r *memRepo) CreateForm1099DIV(_ context.Context, f *Form1099DIV) error {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
cp := *f
|
|
r.divs[f.ID] = &cp
|
|
return nil
|
|
}
|
|
|
|
func (r *memRepo) GetForm1099DIV(_ context.Context, id string) (*Form1099DIV, error) {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
f, ok := r.divs[id]
|
|
if !ok {
|
|
return nil, fmt.Errorf("1099-DIV %s not found", id)
|
|
}
|
|
cp := *f
|
|
return &cp, nil
|
|
}
|
|
|
|
func (r *memRepo) ListForms1099DIV(_ context.Context, _ int, _ string) ([]*Form1099DIV, error) {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
var out []*Form1099DIV
|
|
for _, f := range r.divs {
|
|
cp := *f
|
|
out = append(out, &cp)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (r *memRepo) CreateForm1099B(_ context.Context, f *Form1099B) error {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
cp := *f
|
|
r.bs[f.ID] = &cp
|
|
return nil
|
|
}
|
|
|
|
func (r *memRepo) GetForm1099B(_ context.Context, id string) (*Form1099B, error) {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
f, ok := r.bs[id]
|
|
if !ok {
|
|
return nil, fmt.Errorf("1099-B %s not found", id)
|
|
}
|
|
cp := *f
|
|
return &cp, nil
|
|
}
|
|
|
|
func (r *memRepo) ListForms1099B(_ context.Context, _ int, _ string) ([]*Form1099B, error) {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
var out []*Form1099B
|
|
for _, f := range r.bs {
|
|
cp := *f
|
|
out = append(out, &cp)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (r *memRepo) CreateScheduleK1(_ context.Context, k *ScheduleK1) error {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
cp := *k
|
|
r.k1s[k.ID] = &cp
|
|
return nil
|
|
}
|
|
|
|
func (r *memRepo) GetScheduleK1(_ context.Context, id string) (*ScheduleK1, error) {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
k, ok := r.k1s[id]
|
|
if !ok {
|
|
return nil, fmt.Errorf("K-1 %s not found", id)
|
|
}
|
|
cp := *k
|
|
return &cp, nil
|
|
}
|
|
|
|
func (r *memRepo) ListScheduleK1s(_ context.Context, _ int, _ string) ([]*ScheduleK1, error) {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
var out []*ScheduleK1
|
|
for _, k := range r.k1s {
|
|
cp := *k
|
|
out = append(out, &cp)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// stubDividends implements DividendProvider for tests.
|
|
type stubDividends struct {
|
|
data map[string]float64
|
|
}
|
|
|
|
func (d *stubDividends) GetDividendsByYear(_ context.Context, _ string, _ int) (map[string]float64, error) {
|
|
return d.data, nil
|
|
}
|
|
|
|
func TestGenerate1099DIV(t *testing.T) {
|
|
repo := newMemRepo()
|
|
divProv := &stubDividends{data: map[string]float64{
|
|
"sh1": 5000.00,
|
|
"sh2": 250.00,
|
|
"sh3": 5.00, // below $10 threshold
|
|
}}
|
|
svc := NewService(repo, divProv)
|
|
|
|
recipients := map[string]RecipientInfo{
|
|
"sh1": {Name: "Alice", TIN: "123-45-6789", Address: "123 Main St"},
|
|
"sh2": {Name: "Bob", TIN: "987-65-4321", Address: "456 Oak Ave"},
|
|
"sh3": {Name: "Charlie", TIN: "111-22-3333", Address: "789 Pine Rd"},
|
|
}
|
|
|
|
result, err := svc.Generate1099DIV(context.Background(), &GenerationRequest{
|
|
CompanyID: "c1",
|
|
TaxYear: 2025,
|
|
}, "Acme Inc.", "12-3456789", recipients)
|
|
if err != nil {
|
|
t.Fatalf("Generate1099DIV: %v", err)
|
|
}
|
|
if result.FormsCreated != 2 { // sh3 below threshold
|
|
t.Fatalf("forms_created = %d, want 2", result.FormsCreated)
|
|
}
|
|
if result.Errors != 0 {
|
|
t.Fatalf("errors = %d, want 0", result.Errors)
|
|
}
|
|
}
|
|
|
|
func TestGenerate1099DIVMissingRecipient(t *testing.T) {
|
|
repo := newMemRepo()
|
|
divProv := &stubDividends{data: map[string]float64{
|
|
"sh1": 500.00,
|
|
"sh_missing": 200.00, // no recipient info
|
|
}}
|
|
svc := NewService(repo, divProv)
|
|
|
|
recipients := map[string]RecipientInfo{
|
|
"sh1": {Name: "Alice", TIN: "123-45-6789"},
|
|
}
|
|
|
|
result, err := svc.Generate1099DIV(context.Background(), &GenerationRequest{
|
|
CompanyID: "c1", TaxYear: 2025,
|
|
}, "Acme", "12-3456789", recipients)
|
|
if err != nil {
|
|
t.Fatalf("Generate1099DIV: %v", err)
|
|
}
|
|
if result.FormsCreated != 1 {
|
|
t.Fatalf("forms_created = %d, want 1", result.FormsCreated)
|
|
}
|
|
if result.Errors != 1 {
|
|
t.Fatalf("errors = %d, want 1 (missing recipient)", result.Errors)
|
|
}
|
|
}
|
|
|
|
func TestGenerate1099DIVValidation(t *testing.T) {
|
|
svc := NewService(newMemRepo(), nil)
|
|
|
|
_, err := svc.Generate1099DIV(context.Background(), &GenerationRequest{
|
|
TaxYear: 2025,
|
|
}, "Acme", "12-3456789", nil)
|
|
if err == nil {
|
|
t.Fatal("expected error for missing company_id")
|
|
}
|
|
|
|
_, err = svc.Generate1099DIV(context.Background(), &GenerationRequest{
|
|
CompanyID: "c1",
|
|
}, "Acme", "12-3456789", nil)
|
|
if err == nil {
|
|
t.Fatal("expected error for missing tax_year")
|
|
}
|
|
}
|
|
|
|
func TestCompute1099BShortTerm(t *testing.T) {
|
|
svc := NewService(newMemRepo(), nil)
|
|
|
|
acquired := time.Date(2025, 6, 1, 0, 0, 0, 0, time.UTC)
|
|
sold := time.Date(2025, 11, 1, 0, 0, 0, 0, time.UTC) // ~5 months
|
|
form := svc.Compute1099B(15000, 10000, acquired, sold)
|
|
|
|
if form.GainLoss != 5000 {
|
|
t.Fatalf("gain_loss = %f, want 5000", form.GainLoss)
|
|
}
|
|
if form.ShortTermLongTerm != "short" {
|
|
t.Fatalf("term = %q, want short", form.ShortTermLongTerm)
|
|
}
|
|
if form.Proceeds != 15000 {
|
|
t.Fatalf("proceeds = %f, want 15000", form.Proceeds)
|
|
}
|
|
if form.CostBasis != 10000 {
|
|
t.Fatalf("cost_basis = %f, want 10000", form.CostBasis)
|
|
}
|
|
}
|
|
|
|
func TestCompute1099BLongTerm(t *testing.T) {
|
|
svc := NewService(newMemRepo(), nil)
|
|
|
|
acquired := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
|
|
sold := time.Date(2025, 6, 1, 0, 0, 0, 0, time.UTC) // ~17 months
|
|
form := svc.Compute1099B(8000, 10000, acquired, sold)
|
|
|
|
if form.GainLoss != -2000 {
|
|
t.Fatalf("gain_loss = %f, want -2000", form.GainLoss)
|
|
}
|
|
if form.ShortTermLongTerm != "long" {
|
|
t.Fatalf("term = %q, want long", form.ShortTermLongTerm)
|
|
}
|
|
}
|
|
|
|
func TestCompute1099BBreakeven(t *testing.T) {
|
|
svc := NewService(newMemRepo(), nil)
|
|
|
|
acquired := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
|
|
sold := time.Date(2025, 3, 1, 0, 0, 0, 0, time.UTC)
|
|
form := svc.Compute1099B(5000, 5000, acquired, sold)
|
|
|
|
if form.GainLoss != 0 {
|
|
t.Fatalf("gain_loss = %f, want 0", form.GainLoss)
|
|
}
|
|
}
|