diff --git a/app/account-cache.go b/app/account-cache.go new file mode 100644 index 0000000..9e24ec9 --- /dev/null +++ b/app/account-cache.go @@ -0,0 +1,25 @@ +package app + +import "sync" + +var accountCache = accountCacheType{ + obj: map[string]*Account{}, +} + +type accountCacheType struct { + sync.RWMutex + obj map[string]*Account +} + +func (m *accountCacheType) get(recordID string) (*Account, bool) { + m.RLock() + defer m.RUnlock() + r, ok := m.obj[recordID] + return r, ok +} + +func (m *accountCacheType) put(recordID string, itm *Account) { + m.Lock() + defer m.Unlock() + m.obj[recordID] = itm +} diff --git a/app/account-services.go b/app/account-services.go new file mode 100644 index 0000000..27e9964 --- /dev/null +++ b/app/account-services.go @@ -0,0 +1,47 @@ +package app + +import ( + "fmt" + + "code.tnxs.net/taxnexus/lib/api/crm/crm_client/accounts" +) + +func GetAccount(key string, principal *User) Account { + if key == "" { + return Account{} + } + a, ok := accountCache.get(key) + if ok { + return *a + } + acct, err := GetAccountByID(key, principal) + if err != nil { + return Account{} + } + return acct +} + +func GetAccountByID(recordID string, principal *User) (Account, error) { + sugar.Debug("app.GetAccountByID: 📥") + if recordID == "" { + return Account{}, fmt.Errorf("app.getAccountByID: 💣 ⛔ key is blank") + } + obj, ok := accountCache.get(recordID) + if ok { + sugar.Debug("app.getAccountByID: 👍 🎯 📤") + return *obj, nil + } + crmParams := accounts.NewGetAccountsParamsWithTimeout(getTimeout) + crmParams.AccountID = &recordID + response, err := crmClient.Accounts.GetAccounts(crmParams, principal.Auth) + if err != nil { + return Account{}, err + } + var newObj *Account + for _, itm := range response.Payload.Data { // single iteration execution + newObj = UnMarshalAccount(itm) + } + accountCache.put(recordID, newObj) + sugar.Debug("app.getAccountByID: 👍 🆕 📤") + return *newObj, nil +} diff --git a/app/company-services.go b/app/company-services.go new file mode 100644 index 0000000..e69de29 diff --git a/app/county-cache.go b/app/county-cache.go new file mode 100644 index 0000000..a311a42 --- /dev/null +++ b/app/county-cache.go @@ -0,0 +1,29 @@ +package app + +import ( + "sync" + + "code.tnxs.net/taxnexus/lib/api/geo/geo_models" +) + +var countyCache = countyCacheType{ + obj: map[string]*geo_models.County{}, +} + +type countyCacheType struct { + sync.RWMutex + obj map[string]*geo_models.County +} + +func (m *countyCacheType) get(key string) (*geo_models.County, bool) { + m.RLock() + defer m.RUnlock() + r, ok := m.obj[key] + return r, ok +} + +func (m *countyCacheType) put(key string, obj *geo_models.County) { + m.Lock() + defer m.Unlock() + m.obj[key] = obj +} diff --git a/app/county-services.go b/app/county-services.go new file mode 100644 index 0000000..52a091d --- /dev/null +++ b/app/county-services.go @@ -0,0 +1,58 @@ +package app + +import ( + "fmt" + + "code.tnxs.net/taxnexus/lib/api/geo/geo_client/county" + "code.tnxs.net/taxnexus/lib/api/geo/geo_models" +) + +func GetCountyNameByGeocode(key string, principal *User) string { + if key == "" { + return "" + } + if len(key) == countyGeocodeLength { + c, ok := countyCache.get(key) + if ok { + return c.Name + } + obj, err := GetCountyByGeocode(key, principal) + if err != nil { + return "" + } + return obj.Name + } + obj, ok := placeCache.get(key) + if ok { + return obj.SalesTaxRate.County + } + place, err := getPlaceByGeocode(key, principal) + if err != nil { + return "" + } + return place.SalesTaxRate.County +} +func GetCountyByGeocode(recordID string, principal *User) (geo_models.County, error) { + sugar.Debug("plex.getCountyByGeocode: 📥") + if recordID == "" { + return geo_models.County{}, fmt.Errorf("plex.getCountyByID: 💣 ⛔ key is blank") + } + obj, ok := countyCache.get(recordID) + if ok { + sugar.Debug("plex.getCountyByGeocode: 👍 🎯 📤") + return *obj, nil + } + geoParams := county.NewGetCountiesParamsWithTimeout(getTimeout) + geoParams.Geocode = &recordID + response, err := geoClient.County.GetCounties(geoParams, principal.Auth) + if err != nil { + return geo_models.County{}, err + } + var newObj *geo_models.County + for _, itm := range response.Payload.Data { // single iteration execution + newObj = itm + } + countyCache.put(recordID, newObj) + sugar.Debug("plex.getCountyByGeocode: 👍 🆕 📤") + return *newObj, nil +} diff --git a/app/place-cache.go b/app/place-cache.go new file mode 100644 index 0000000..8f07b5f --- /dev/null +++ b/app/place-cache.go @@ -0,0 +1,29 @@ +package app + +import ( + "sync" + + "code.tnxs.net/taxnexus/lib/api/geo/geo_models" +) + +var placeCache = placeCacheType{ + obj: map[string]*geo_models.Place{}, +} + +type placeCacheType struct { + sync.RWMutex + obj map[string]*geo_models.Place +} + +func (m *placeCacheType) get(key string) (*geo_models.Place, bool) { + m.RLock() + defer m.RUnlock() + r, ok := m.obj[key] + return r, ok +} + +func (m *placeCacheType) put(key string, obj *geo_models.Place) { + m.Lock() + defer m.Unlock() + m.obj[key] = obj +} diff --git a/app/place-services.go b/app/place-services.go new file mode 100644 index 0000000..7b2aacd --- /dev/null +++ b/app/place-services.go @@ -0,0 +1,62 @@ +package app + +import ( + "fmt" + + "code.tnxs.net/taxnexus/lib/api/geo/geo_client/place" + "code.tnxs.net/taxnexus/lib/api/geo/geo_models" +) + +func getPlaceNameByGeocode(key string, principal *User) string { + if key == "" || len(key) == 7 { + return "" + } + p, ok := placeCache.get(key) + if ok { + return p.Name + } + thePlace, err := getPlaceByGeocode(key, principal) + if err != nil { + return "" + } + return thePlace.Name +} +func getPlaceHasDistrictTaxesByGeocode(key string, principal *User) bool { + if key == "" || len(key) == 7 { + return false + } + p, ok := placeCache.get(key) + if ok { + return p.HasDistrictTaxes + } + thePlace, err := getPlaceByGeocode(key, principal) + if err != nil { + return false + } + return thePlace.HasDistrictTaxes +} + +func getPlaceByGeocode(recordID string, principal *User) (geo_models.Place, error) { + sugar.Debug("plex.getPlaceByGeocode: 📥") + if recordID == "" { + return geo_models.Place{}, fmt.Errorf("plex.getPlaceByID: 💣 ⛔ key is blank") + } + obj, ok := placeCache.get(recordID) + if ok { + sugar.Debug("plex.getPlaceByGeocode: 👍 🎯 📤") + return *obj, nil + } + geoParams := place.NewGetPlacesParamsWithTimeout(getTimeout) + geoParams.Geocode = &recordID + response, err := geoClient.Place.GetPlaces(geoParams, principal.Auth) + if err != nil { + return geo_models.Place{}, err + } + var newObj *geo_models.Place + for _, itm := range response.Payload.Data { // single iteration execution + newObj = itm + } + placeCache.put(recordID, newObj) + sugar.Debug("plex.getPlaceByGeocode: 👍 🆕 📤") + return *newObj, nil +} diff --git a/app/root.go b/app/root.go index 81ae203..af7d85c 100644 --- a/app/root.go +++ b/app/root.go @@ -6,6 +6,8 @@ import ( "time" "code.tnxs.net/taxnexus/lib/api/auth0/auth0_client" + "code.tnxs.net/taxnexus/lib/api/crm/crm_client" + "code.tnxs.net/taxnexus/lib/api/geo/geo_client" "code.tnxs.net/taxnexus/lib/app/logger" httptransport "github.com/go-openapi/runtime/client" "github.com/spf13/viper" @@ -17,6 +19,8 @@ var sugar *zap.SugaredLogger var config = Configuration{} var appViper = viper.New() var auth0Client = auth0_client.Default +var geoClient = geo_client.Default +var crmClient = crm_client.Default var configured = false var apiUsers map[string]User @@ -27,6 +31,8 @@ const dateFormat = "2006-01-02" const dateTimeFormat = "2006-01-02T15:04:05-0800" const dateTimeFormatAlt = "2006-01-02 15:04:05" const dateFormatAlt = "1/2/2006" +const countyGeocodeLength = 7 +const cityGeocodeLength = 12 // InitConfig exports the config initialization func func InitConfig(systemName string, initalLogLevel zapcore.Level) { diff --git a/app/taxtype-cache.go b/app/taxtype-cache.go new file mode 100644 index 0000000..dc8c42c --- /dev/null +++ b/app/taxtype-cache.go @@ -0,0 +1,25 @@ +package app + +import "sync" + +var taxTypeCache = taxTypeCacheType{ + obj: map[string]*TaxType{}, +} + +type taxTypeCacheType struct { + sync.RWMutex + obj map[string]*TaxType +} + +func (m *taxTypeCacheType) get(recordID string) (*TaxType, bool) { + m.RLock() + defer m.RUnlock() + r, ok := m.obj[recordID] + return r, ok +} + +func (m *taxTypeCacheType) put(recordID string, itm *TaxType) { + m.Lock() + defer m.Unlock() + m.obj[recordID] = itm +} diff --git a/app/taxtype-services.go b/app/taxtype-services.go new file mode 100644 index 0000000..7ea114a --- /dev/null +++ b/app/taxtype-services.go @@ -0,0 +1,162 @@ +package app + +import ( + "fmt" + "time" + + "code.tnxs.net/taxnexus/lib/api/geo/geo_client/tax_type" +) + +var exciseCategories = map[string]string{ + "cannabis-excise": "cannabis-excise", + "telecom-excise": "telecom-excise", +} + +var cannabisCategories = map[string]string{ + "cannabis-ag": "cannabis-ag", + "cannabis-trade": "cannabis-trade", +} + +var merchCategories = map[string]string{ + "cannabis-retail": "cannabis-retail", + "sales-district": "sales-district", + "sales-state": "sales-state", +} + +var telecomCategories = map[string]string{ + "broadband": "broadband", + "voip-interstate": "voip-interstate", + "voip-international": "voip-international", + "voip-intrastate": "voip-intrastate", + "voip": "voip", + "wireless": "wireless", +} + +func GetTaxType(recordID string, principal *User) *TaxType { + obj, ok := taxTypeCache.get(recordID) + if ok { + return obj + } + obj, err := GetTaxTypeByID(recordID, principal) + if err != nil { + return &TaxType{} + } + return obj +} +func GetTaxTypeByID(recordID string, principal *User) (*TaxType, error) { + sugar.Debugf("render.GetTaxTypesByID: 📥") + if recordID == "" { + return nil, fmt.Errorf("render.getTaxTypeByID: 💣 ⛔ key is blank") + } + obj, ok := taxTypeCache.get(recordID) + if ok { + sugar.Debugf("render.getTaxTypeByID: 👍 🎯 📤") + return obj, nil + } + geoParams := tax_type.NewGetTaxTypesParamsWithTimeout(getTimeout) + geoParams.TaxTypeID = &recordID + response, err := geoClient.TaxType.GetTaxTypes(geoParams, principal.Auth) + if err != nil { + return nil, err + } + var newObj *TaxType + for _, itm := range response.Payload.Data { // single iteration execution + newObj = UnMarshalTaxType(itm) + } + taxTypeCache.put(recordID, newObj) + sugar.Debugf("render.getTaxTypeByID: 👍 🆕 📤") + return newObj, nil +} + +func GetCannabisTaxTypes(taxTypes []*TaxType) []*TaxType { + objList := []*TaxType{} + for _, tt := range taxTypes { + if tt != nil { + if inMap(cannabisCategories, tt.Category) && isActive(tt) { + sugar.Debugf("render.getCannabisTaxTypes: ➡ ✅ cannabis %s", tt.Name) + objList = append(objList, tt) + } + } + } + if len(objList) == 0 { + return nil + } + return objList +} + +func GetExciseTaxTypes(taxTypes []*TaxType) []*TaxType { + objList := []*TaxType{} + for _, tt := range taxTypes { + if tt != nil { + if inMap(exciseCategories, tt.Category) && isActive(tt) { + sugar.Debugf("render.getExciseTaxTypes: ➡ ✅ excise %s", tt.Name) + objList = append(objList, tt) + } + } + } + if len(objList) == 0 { + return nil + } + return objList +} + +func GetBusinessTaxTypes(taxTypes []*TaxType) []*TaxType { + objList := []*TaxType{} + for _, tt := range taxTypes { + if tt != nil { + if tt.Category == "business" && isActive(tt) { + sugar.Debugf("render.getBusinessTaxTypes: ➡ ✅ business %s", tt.Name) + objList = append(objList, tt) + } + } + } + if len(objList) == 0 { + return nil + } + return objList +} + +func GetMerchTaxTypes(taxTypes []*TaxType) []*TaxType { + objList := []*TaxType{} + for _, tt := range taxTypes { + if tt != nil { + if inMap(merchCategories, tt.Category) && isActive(tt) { + sugar.Debugf("render.getMerchTaxTypes: ➡ ✅ merch %s", tt.Name) + objList = append(objList, tt) + } + } + } + if len(objList) == 0 { + return nil + } + return objList +} +func GetTelecomTaxTypes(taxTypes []*TaxType) []*TaxType { + objList := []*TaxType{} + for _, tt := range taxTypes { + if tt != nil { + if inMap(telecomCategories, tt.Category) && isActive(tt) { + sugar.Debugf("render.getTelecomTaxTypes: ➡ ✅ telecom %s", tt.Name) + objList = append(objList, tt) + } + } + } + if len(objList) == 0 { + return nil + } + return objList +} + +func inMap(values map[string]string, str string) bool { + _, ok := values[str] + return ok +} +func isActive(tt *TaxType) bool { + if tt.EndDate.Time.IsZero() && tt.EffectiveDate.Time.Before(time.Now()) { + return true + } + if tt.EndDate.Time.After(time.Now()) && tt.EffectiveDate.Time.Before(time.Now()) { + return true + } + return false +} diff --git a/app/taxtype.go b/app/taxtype.go index 3a21462..eab6df4 100644 --- a/app/taxtype.go +++ b/app/taxtype.go @@ -17,6 +17,7 @@ type TaxTypeActivityWrapper struct { // TaxType is a first class object type type TaxType struct { ID string + Account Account AccountID string AccountingRuleCode string Active bool @@ -26,6 +27,7 @@ type TaxType struct { Category string CollectorDomainID string CompanyID string + Contact Contact ContactID string CreatedByID string CreatedDate sql.NullTime @@ -40,6 +42,7 @@ type TaxType struct { FilingPostalcode string FilingState string FilingStreet string + Formatted taxTypeFormatted Fractional bool Frequency string GeocodeString string @@ -67,3 +70,18 @@ type TaxType struct { UnitBase float64 Units string } +type taxTypeFormatted struct { + CreatedDate string + EffectiveDate string + EndDate string + Fractional string + InterestRate string + IsMedicinal string + IsRecreational string + LastModifiedDate string + MarkupRate string + PassThrough string + PenaltyDays string + PenaltyRate string + Rate string +}