mirror of
https://github.com/luxfi/compliance.git
synced 2026-10-30 16:59:39 +00:00
134 lines
4.1 KiB
Go
134 lines
4.1 KiB
Go
package reporting
|
|
|
|
import "time"
|
|
|
|
// CTRData is a Currency Transaction Report per FinCEN (31 CFR 1010.311).
|
|
// Auto-generated when a person's daily aggregate cash transactions exceed $10,000.
|
|
type CTRData struct {
|
|
Person CTRPerson `json:"person"`
|
|
Transactions []CTRTxDetail `json:"transactions"`
|
|
Institution CTRInstitution `json:"financial_institution"`
|
|
Date time.Time `json:"date"`
|
|
TotalCashIn float64 `json:"total_cash_in"`
|
|
TotalCashOut float64 `json:"total_cash_out"`
|
|
}
|
|
|
|
// CTRPerson is the individual conducting or on whose behalf the transactions were made.
|
|
type CTRPerson struct {
|
|
Name string `json:"name"`
|
|
FirstName string `json:"first_name,omitempty"`
|
|
LastName string `json:"last_name,omitempty"`
|
|
SSN string `json:"-"` // never serialized
|
|
SSNLast4 string `json:"ssn_last4,omitempty"`
|
|
DOB string `json:"dob,omitempty"`
|
|
Address string `json:"address,omitempty"`
|
|
City string `json:"city,omitempty"`
|
|
State string `json:"state,omitempty"`
|
|
ZipCode string `json:"zip_code,omitempty"`
|
|
Country string `json:"country,omitempty"`
|
|
IDType string `json:"id_type,omitempty"` // drivers_license, passport
|
|
IDNumber string `json:"-"` // never serialized
|
|
IDState string `json:"id_state,omitempty"`
|
|
AccountID string `json:"account_id,omitempty"`
|
|
Occupation string `json:"occupation,omitempty"`
|
|
}
|
|
|
|
// CTRTxDetail is a single cash transaction within the CTR.
|
|
type CTRTxDetail struct {
|
|
Amount float64 `json:"amount"`
|
|
Direction string `json:"direction"` // cash_in, cash_out
|
|
Type string `json:"type"` // deposit, withdrawal, exchange, payment
|
|
Timestamp time.Time `json:"timestamp"`
|
|
TxID string `json:"tx_id,omitempty"`
|
|
}
|
|
|
|
// CTRInstitution identifies the filing financial institution.
|
|
type CTRInstitution struct {
|
|
Name string `json:"name"`
|
|
RSSD string `json:"rssd,omitempty"`
|
|
EIN string `json:"-"` // never serialized
|
|
Address string `json:"address,omitempty"`
|
|
City string `json:"city,omitempty"`
|
|
State string `json:"state,omitempty"`
|
|
ZipCode string `json:"zip_code,omitempty"`
|
|
}
|
|
|
|
// CTRThreshold is the FinCEN threshold for mandatory CTR filing.
|
|
const CTRThreshold = 10000.0
|
|
|
|
// BuildCTR constructs a CTR from a user and their transactions for a given date.
|
|
// Only call this when daily aggregate exceeds CTRThreshold.
|
|
func BuildCTR(user Entity, transactions []Transaction, date time.Time) *CTRData {
|
|
ctr := &CTRData{
|
|
Person: CTRPerson{
|
|
Name: user.Name,
|
|
FirstName: user.FirstName,
|
|
LastName: user.LastName,
|
|
SSNLast4: user.SSNLast4,
|
|
DOB: user.DOB,
|
|
Address: user.Address,
|
|
City: user.City,
|
|
State: user.State,
|
|
ZipCode: user.ZipCode,
|
|
Country: user.Country,
|
|
AccountID: user.AccountID,
|
|
},
|
|
Date: date,
|
|
}
|
|
|
|
for _, tx := range transactions {
|
|
if tx.Date.Year() != date.Year() || tx.Date.YearDay() != date.YearDay() {
|
|
continue
|
|
}
|
|
|
|
direction := "cash_in"
|
|
if tx.Type == "withdrawal" || tx.Type == "payout" {
|
|
direction = "cash_out"
|
|
}
|
|
|
|
detail := CTRTxDetail{
|
|
Amount: tx.Amount,
|
|
Direction: direction,
|
|
Type: tx.Type,
|
|
Timestamp: tx.Date,
|
|
TxID: tx.ID,
|
|
}
|
|
ctr.Transactions = append(ctr.Transactions, detail)
|
|
|
|
if direction == "cash_in" {
|
|
ctr.TotalCashIn += tx.Amount
|
|
} else {
|
|
ctr.TotalCashOut += tx.Amount
|
|
}
|
|
}
|
|
|
|
return ctr
|
|
}
|
|
|
|
// ValidateCTR returns validation errors for a CTR before filing.
|
|
func ValidateCTR(ctr *CTRData) []string {
|
|
var errs []string
|
|
|
|
if ctr.Person.Name == "" && (ctr.Person.FirstName == "" || ctr.Person.LastName == "") {
|
|
errs = append(errs, "person name is required")
|
|
}
|
|
if ctr.Date.IsZero() {
|
|
errs = append(errs, "date is required")
|
|
}
|
|
if len(ctr.Transactions) == 0 {
|
|
errs = append(errs, "at least one transaction is required")
|
|
}
|
|
|
|
total := ctr.TotalCashIn + ctr.TotalCashOut
|
|
if total < CTRThreshold {
|
|
errs = append(errs, "aggregate total below $10,000 CTR threshold")
|
|
}
|
|
|
|
return errs
|
|
}
|
|
|
|
// RequiresCTR returns true if the daily aggregate for an account exceeds the threshold.
|
|
func RequiresCTR(dailyAggregate float64) bool {
|
|
return dailyAggregate > CTRThreshold
|
|
}
|