diff --git a/app/account-services.go b/app/account-services.go index 2fb0c4c..682e8b9 100644 --- a/app/account-services.go +++ b/app/account-services.go @@ -8,6 +8,44 @@ import ( "code.tnxs.net/taxnexus/lib/api/geo/geo_models" ) +// GetDefaultDeliveryAddress is an account helper function +func GetDefaultDeliveryAddress(obj *Account, principal *User) (*Address, error) { + var deliveryAddress Address + theContact, err := GetContactByID(obj.DefaultDeliveryContactID, principal) + if err != nil { + sugar.Errorf("ops.getDefaultAddress: 💣 ⛔ theAccount.Defaultdeliverycontactid invalid %w", err) + if obj.ShippingAddress == nil { + if obj.BillingAddress == nil { + return nil, fmt.Errorf("ops.GetDefaultDeliveryAddress: 💣 ⛔ can't find any valid addresses") + } + deliveryAddress = *obj.BillingAddress + } else { + deliveryAddress = *obj.ShippingAddress + } + } else { + if theContact.MailingAddress == nil { + sugar.Errorf( + "ops.getDefaultDeliveryAddress: 💣 ⛔ default delivery contact has no email, id = %s", + obj.DefaultBackendID, + ) + if obj.ShippingAddress == nil { + if obj.BillingAddress == nil { + return nil, fmt.Errorf("ops.GetDefaultDeliveryAddress: 💣 ⛔ can't find any valid addresses") + } + deliveryAddress = *obj.BillingAddress + } else { + deliveryAddress = *obj.ShippingAddress + } + } else { + deliveryAddress = *theContact.MailingAddress + } + } + if deliveryAddress.City == "" || deliveryAddress.State+deliveryAddress.StateCode == "" { + return nil, fmt.Errorf("ops.getDefaultDeliveryAddress: 💣 ⛔ city or state+statecode is blank: %v", deliveryAddress) + } + return &deliveryAddress, nil +} + // GetAccount is first class retrieval function func GetAccount(key string, principal *User) *Account { if key == "" { @@ -26,13 +64,13 @@ func GetAccount(key string, principal *User) *Account { // GetAccountByID retrieves and enriches an Account object instance func GetAccountByID(recordID string, principal *User) (*Account, error) { - sugar.Debug("app.GetAccountByID: 📥") + sugar.Debug("GetAccountByID: 📥") if recordID == "" { - return nil, fmt.Errorf("app.getAccountByID: 💣 ⛔ key is blank") + return nil, fmt.Errorf("getAccountByID: 💣 ⛔ key is blank") } obj, ok := accountCache.get(recordID) if ok { - sugar.Debug("app.getAccountByID: 👍 🎯 📤") + sugar.Debug("getAccountByID: 👍 🎯 📤") return obj, nil } crmParams := accounts.NewGetAccountsParamsWithTimeout(getTimeout) @@ -47,7 +85,7 @@ func GetAccountByID(recordID string, principal *User) (*Account, error) { } finalObj := theObj.Enrich(principal) accountCache.put(recordID, finalObj) - sugar.Debug("app.getAccountByID: 👍 🆕 📤") + sugar.Debug("getAccountByID: 👍 🆕 📤") return finalObj, nil } @@ -153,7 +191,7 @@ func (obj *Account) Enrich(principal *User) *Account { // GetCoordinate is a first class retrieval function func (obj *Account) GetCoordinate(principal *User) *Coordinate { - sugar.Debug("app.Account.getCoordinate: 📥") + sugar.Debug("Account.getCoordinate: 📥") if obj.CoordinateID != "" { // if CoordinateID is set, then just get it geoParams := coordinate.NewGetCoordinatesParamsWithTimeout(getTimeout) geoParams.CoordinateID = &obj.CoordinateID @@ -168,7 +206,7 @@ func (obj *Account) GetCoordinate(principal *User) *Coordinate { return obj } // else get it via addresses, shipping #1, Business #2 if obj.ShippingAddress.ToString() == "" && obj.BillingAddress.ToString() == "" { - sugar.Errorf("app.Account.getCoordinate: 💣 ⛔ billing and shipping address both blank") + sugar.Errorf("Account.getCoordinate: 💣 ⛔ billing and shipping address both blank") return nil } theAddress := obj.ShippingAddress.ToString() @@ -186,6 +224,6 @@ func (obj *Account) GetCoordinate(principal *User) *Coordinate { for _, itm := range response.Payload.Data { // single iteration execution swag = itm } - sugar.Debug("app.Account.getCoordinate: 👍 📤") + sugar.Debug("Account.getCoordinate: 👍 📤") return UnMarshalCoordinate(swag, principal) } diff --git a/app/accountingrule-cache.go b/app/accountingrule-cache.go new file mode 100644 index 0000000..bf483e2 --- /dev/null +++ b/app/accountingrule-cache.go @@ -0,0 +1,28 @@ +package app + +import ( + "sync" + + "code.tnxs.net/taxnexus/lib/api/ledger/ledger_models" +) + +var accountingRuleCache = accountingRuleCacheType{ + obj: map[string]map[string]*ledger_models.AccountingRule{}, +} + +type accountingRuleCacheType struct { + sync.RWMutex + obj map[string]map[string]*ledger_models.AccountingRule +} + +func (m *accountingRuleCacheType) get(accountID string) (map[string]*ledger_models.AccountingRule, bool) { + m.RLock() + defer m.RUnlock() + r, ok := m.obj[accountID] + return r, ok +} +func (m *accountingRuleCacheType) put(accountID string, rules map[string]*ledger_models.AccountingRule) { + m.Lock() + defer m.Unlock() + m.obj[accountID] = rules +} diff --git a/app/accountingrule-services.go b/app/accountingrule-services.go new file mode 100644 index 0000000..34448e6 --- /dev/null +++ b/app/accountingrule-services.go @@ -0,0 +1,34 @@ +package app + +import ( + "fmt" + + "code.tnxs.net/taxnexus/lib/api/ledger/ledger_client/accounting_rule" + "code.tnxs.net/taxnexus/lib/api/ledger/ledger_models" +) + +// GetAccountingRulesByAccountID is a first class object retrieval method +func GetAccountingRulesByAccountID( + accountID string, + principal *User, +) (map[string]*ledger_models.AccountingRule, error) { + sugar.Debug("ops.getAccountingRulesByAccountID: 📥") + obj, ok := accountingRuleCache.get(accountID) + if ok { + sugar.Debugf("ops.getAccountingRulesByAccountID: 👍 📤 🎯 n = %v", len(obj)) + return obj, nil + } + ledgerParams := accounting_rule.NewGetAccountingRulesParamsWithTimeout(getTimeout) + ledgerParams.AccountID = accountID + response, restErr := ledgerClient.AccountingRule.GetAccountingRules(ledgerParams, principal.Auth) + if restErr != nil { + return nil, fmt.Errorf("ops.getAccountingRulesByAccountID: 💣 ⛔ %w", restErr) + } + theRules := map[string]*ledger_models.AccountingRule{} + for _, itm := range response.Payload.Data { + theRules[itm.Code] = itm + } + sugar.Debugf("ops.getAccountingRulesByAccountID: 👍 📤 n = %v", len(theRules)) + accountingRuleCache.put(accountID, theRules) + return theRules, nil +} diff --git a/app/accountingruleset-cache.go b/app/accountingruleset-cache.go new file mode 100644 index 0000000..c399c44 --- /dev/null +++ b/app/accountingruleset-cache.go @@ -0,0 +1,29 @@ +package app + +import ( + "sync" + + "code.tnxs.net/taxnexus/lib/api/ledger/ledger_models" +) + +var accountingRulesetCache = accountingRulesetCacheType{ + obj: map[string]map[string]*ledger_models.AccountingRuleset{}, +} + +type accountingRulesetCacheType struct { + sync.RWMutex + obj map[string]map[string]*ledger_models.AccountingRuleset +} + +func (m *accountingRulesetCacheType) get(accountID string) (map[string]*ledger_models.AccountingRuleset, bool) { + m.RLock() + defer m.RUnlock() + r, ok := m.obj[accountID] + return r, ok +} + +func (m *accountingRulesetCacheType) put(accountID string, rules map[string]*ledger_models.AccountingRuleset) { + m.Lock() + defer m.Unlock() + m.obj[accountID] = rules +} diff --git a/app/accountingruleset-services.go b/app/accountingruleset-services.go new file mode 100644 index 0000000..29d7c46 --- /dev/null +++ b/app/accountingruleset-services.go @@ -0,0 +1,32 @@ +package app + +import ( + "fmt" + + "code.tnxs.net/taxnexus/lib/api/ledger/ledger_client/accounting_ruleset" + "code.tnxs.net/taxnexus/lib/api/ledger/ledger_models" +) + +func GetAccountingRulesetsByAccountID( + accountID string, + principal *User, +) (map[string]*ledger_models.AccountingRuleset, error) { + obj, ok := accountingRulesetCache.get(accountID) + if ok { + sugar.Debugf("ops.getAccountingRulesetsByAccountID: 👍 📤 🎯 n = %v", len(obj)) + return obj, nil + } + params := accounting_ruleset.NewGetAccountingRulesetsParamsWithTimeout(getTimeout) + params.AccountID = accountID + response, restErr := ledgerClient.AccountingRuleset.GetAccountingRulesets(params, principal.Auth) + if restErr != nil { + return nil, fmt.Errorf("ops.getAccountingRulesetsByAccountID: 💣 ⛔ %w", restErr) + } + theRulesets := map[string]*ledger_models.AccountingRuleset{} + for _, itm := range response.Payload.Data { + theRulesets[itm.Code] = itm + } + sugar.Debugf("ops.getAccountingRulesetsByAccountID: 👍 📤 n = %v", len(theRulesets)) + accountingRulesetCache.put(accountID, theRulesets) + return theRulesets, nil +} diff --git a/app/address-services.go b/app/address-services.go new file mode 100644 index 0000000..6c4536d --- /dev/null +++ b/app/address-services.go @@ -0,0 +1,50 @@ +package app + +import ( + "fmt" + + "code.tnxs.net/taxnexus/lib/api/geo/geo_models" +) + +type GeocodeAddressParams struct { + BusinessAddress *Address + Account *Account + Ref string +} + +func GeocodeAddress(params GeocodeAddressParams, principal *User) (*geo_models.CoordinateBasic, error) { + if params.BusinessAddress == nil { + sugar.Infof("ops.geocodeAddress: ❗ Business Address is null, ref = %s", params.Ref) + deliveryAddress, acctErr := GetDefaultDeliveryAddress(params.Account, principal) + if acctErr != nil { + return nil, fmt.Errorf("ops.geocodeAddress: 💣 ⛔ can't determine default delivery address: %w", acctErr) + } + params.BusinessAddress = deliveryAddress + } + if params.BusinessAddress.City == "" { + sugar.Infof("ops.geocodeAddress: ❗ Business Address is blank, ref = %s", params.Ref) + deliveryAddress, acctErr := GetDefaultDeliveryAddress(params.Account, principal) + if acctErr != nil { + return nil, fmt.Errorf("ops.geocodeAddress: 💣 ⛔ can't determine default delivery address: %w", acctErr) + } + params.BusinessAddress = deliveryAddress + } + var situsCoordinate *geo_models.CoordinateBasic + aCoordinate, err := GetCoordinate(params.BusinessAddress, principal) + if err == nil { + situsCoordinate = aCoordinate + } else { + sugar.Infof("ops.geocodeAddress: ❗ can't geocode address: %w", err) + deliveryAddress, acctErr := GetDefaultDeliveryAddress(params.Account, principal) + if acctErr != nil { + return nil, fmt.Errorf("ops.geocodeAddress: 💣 ⛔ can't determine default delivery address: %w", acctErr) + } + aCoordinate, coordErr := GetCoordinate(deliveryAddress, principal) + if coordErr != nil { + return nil, fmt.Errorf("ops.geocodeAddress: 💣 ⛔ can't determine default delivery coordinates: %w", acctErr) + } + sugar.Infof("ops.geocodeAddress: ❗ alternate address used for ref = %s", params.Ref) + situsCoordinate = aCoordinate + } + return situsCoordinate, nil +} diff --git a/app/coordinate-cache.go b/app/coordinate-cache.go new file mode 100644 index 0000000..934da3d --- /dev/null +++ b/app/coordinate-cache.go @@ -0,0 +1,29 @@ +package app + +import ( + "sync" + + "code.tnxs.net/taxnexus/lib/api/geo/geo_models" +) + +var coordinateCache = coordinateCacheType{ + obj: map[string]*geo_models.CoordinateBasic{}, +} + +type coordinateCacheType struct { + sync.RWMutex + obj map[string]*geo_models.CoordinateBasic +} + +func (m *coordinateCacheType) get(addrStr string) (*geo_models.CoordinateBasic, bool) { + m.RLock() + defer m.RUnlock() + r, ok := m.obj[addrStr] + return r, ok +} + +func (m *coordinateCacheType) put(accountID string, coord *geo_models.CoordinateBasic) { + m.Lock() + defer m.Unlock() + m.obj[accountID] = coord +} diff --git a/app/coordinate-services.go b/app/coordinate-services.go new file mode 100644 index 0000000..1d23ab3 --- /dev/null +++ b/app/coordinate-services.go @@ -0,0 +1,51 @@ +package app + +import ( + "fmt" + + "code.tnxs.net/taxnexus/lib/api/geo/geo_client/coordinate" + "code.tnxs.net/taxnexus/lib/api/geo/geo_models" +) + +// GetCoordinate is a first class object retrieval method +func GetCoordinate(addr *Address, principal *User) (*geo_models.CoordinateBasic, error) { + sugar.Debug("ops.getCoordinate: 📥") + if addr == nil { + return nil, fmt.Errorf("ops.getCoordinate: 💣 ⛔ Address cannot be nil") + } + if addr.ToString() == "" { + return nil, fmt.Errorf("ops.getCoordinate: 💣 ⛔ Address cannot be empty") + } + sugar.Debugf("ops.getCoordinate: 📏 address: %s", addr.ToString()) + if addr.City == "" { + return nil, fmt.Errorf("ops.getCoordinate: 💣 ⛔ City cannot be blank") + } + obj, ok := coordinateCache.get(addr.ToString()) + if ok { + sugar.Debugf("ops.GetCoordiante: 👍🏻 🎯 📤") + return obj, nil + } + placeName := addr.City + ", " + addr.StateCode + if addr.StateCode == "" { + placeName = addr.City + ", " + addr.State + } + coordParams := coordinate.NewGetCoordinateBasicParamsWithTimeout(getTimeout) + theAddress := addr.ToString() + coordParams.PlaceName = &placeName + coordParams.Address = &theAddress + response, geoErr := geoClient.Coordinate.GetCoordinateBasic(coordParams, principal.Auth) + if geoErr != nil { + return nil, fmt.Errorf("ops.getCoordinate: 💣 ⛔ failed, %s (%w)", *coordParams.Address, geoErr) + } + if len(response.Payload.Data) != 1 { + return nil, fmt.Errorf("ops.getCoordinate: 💣 ⛔ one and only one coordinate record should be returned") + } + sugar.Debugf("ops.getCoordinate: ✅ geo records retrieved = %d", len(response.Payload.Data)) + var coord geo_models.CoordinateBasic + for _, itm := range response.Payload.Data { + coord = *itm + } + coordinateCache.put(addr.ToString(), &coord) + sugar.Debug("ops.getCoordinate: 👍 📤") + return &coord, nil +} diff --git a/app/glbalance-cache.go b/app/glbalance-cache.go new file mode 100644 index 0000000..d56e733 --- /dev/null +++ b/app/glbalance-cache.go @@ -0,0 +1,38 @@ +package app + +import ( + "sync" + + "code.tnxs.net/taxnexus/lib/api/ledger/ledger_models" +) + +var glBalanceCache = glBalanceCacheType{ + obj: map[string]map[string]*ledger_models.GlBalance{}, +} + +type glBalanceCacheType struct { + sync.RWMutex + obj map[string]map[string]*ledger_models.GlBalance +} + +func (m *glBalanceCacheType) get(periodID, glAccountID string) (*ledger_models.GlBalance, bool) { + m.RLock() + defer m.RUnlock() + r, ok := m.obj[periodID][glAccountID] + return r, ok +} + +func (m *glBalanceCacheType) put(periodID, glAccountID string, glBalance *ledger_models.GlBalance) { + m.init(periodID) + m.Lock() + defer m.Unlock() + m.obj[periodID][glAccountID] = glBalance +} +func (m *glBalanceCacheType) init(periodID string) { + m.RLock() + defer m.RUnlock() + _, ok := m.obj[periodID] + if !ok { + m.obj[periodID] = map[string]*ledger_models.GlBalance{} + } +} diff --git a/app/glbalance-services.go b/app/glbalance-services.go new file mode 100644 index 0000000..edd05e1 --- /dev/null +++ b/app/glbalance-services.go @@ -0,0 +1,38 @@ +package app + +import ( + "code.tnxs.net/taxnexus/lib/api/ledger/ledger_client/gl_balance" + "code.tnxs.net/taxnexus/lib/api/ledger/ledger_models" +) + +// GlBalanceParams is a parameter struct +type GlBalanceParams struct { + glAccountID string + periodID string + principal *User +} + +// GetGlBalanceByParams is a GL balance helper function +func GetGlBalanceByParams(params GlBalanceParams) *ledger_models.GlBalance { + sugar.Debug("ops.getGlBalanceByParams: 📥") + obj, ok := glBalanceCache.get(params.periodID, params.glAccountID) + if ok { + sugar.Debug("ops.getGlBalanceByParams: 👍 📤 🎯") + return obj + } + ledgerParams := gl_balance.NewGetGlBalancesParamsWithTimeout(getTimeout) + ledgerParams.GlAccountID = ¶ms.glAccountID + ledgerParams.PeriodID = ¶ms.periodID + response, restErr := ledgerClient.GlBalance.GetGlBalances(ledgerParams, params.principal.Auth) + if restErr != nil { + sugar.Errorf("ops.getGlBalanceByParams: 💣 ⛔ %s", restErr.Error()) + return nil + } + theGlBalance := &ledger_models.GlBalance{} + for _, itm := range response.Payload.Data { + theGlBalance = itm // singleton + } + glBalanceCache.put(params.periodID, params.glAccountID, theGlBalance) + sugar.Debugf("ops.getGlBalanceByParams: 👍 📤") + return theGlBalance +} diff --git a/app/period-cache.go b/app/period-cache.go new file mode 100644 index 0000000..aaa402d --- /dev/null +++ b/app/period-cache.go @@ -0,0 +1,36 @@ +package app + +import "sync" + +var periodCache = periodCacheType{ + obj: map[string]map[string]*CalendarPeriod{}, +} + +// periodCacheType maps [accountID][dateString] +type periodCacheType struct { + sync.RWMutex + obj map[string]map[string]*CalendarPeriod +} + +func (m *periodCacheType) get(accountID, dateStr string) (*CalendarPeriod, bool) { + m.RLock() + defer m.RUnlock() + r, ok := m.obj[accountID][dateStr] + return r, ok +} + +func (m *periodCacheType) put(accountID, dateStr string, period *CalendarPeriod) { + m.init(accountID) + m.Lock() + defer m.Unlock() + m.obj[accountID][dateStr] = period +} + +func (m *periodCacheType) init(accountID string) { + m.RLock() + defer m.RUnlock() + _, ok := m.obj[accountID] + if !ok { + m.obj[accountID] = map[string]*CalendarPeriod{} + } +} diff --git a/app/period-services.go b/app/period-services.go new file mode 100644 index 0000000..25fc249 --- /dev/null +++ b/app/period-services.go @@ -0,0 +1,56 @@ +package app + +import ( + "fmt" + "time" + + "code.tnxs.net/taxnexus/lib/api/ledger/ledger_client/period" +) + +// CalendarPeriod is a parameter type +type CalendarPeriod struct { + ID string + Name string + StartDate time.Time + EndDate time.Time +} + +// GetPeriodParams is a parameter type +type GetPeriodParams struct { + Date time.Time + AccountID string + Principal *User +} + +// GetPeriodIDByDate is a period helper function +func GetPeriodIDByDate(params GetPeriodParams) (string, error) { + sugar.Debug("ops.getPeriodIDByDate: 📥") + obj, ok := periodCache.get(params.AccountID, params.Date.Format(dateFormat)) + if ok { + sugar.Debugf("ops.getPeriodIDByDate: 👍 📤 🎯") + return obj.ID, nil + } + ledgerDate := params.Date.Format(dateFormat) + ledgerParams := period.NewGetPeriodsParamsWithTimeout(getTimeout) + ledgerParams.AccountID = ¶ms.AccountID + ledgerParams.Date = &ledgerDate + response, restErr := ledgerClient.Period.GetPeriods(ledgerParams, params.Principal.Auth) + if restErr != nil { + return "", fmt.Errorf("ops.getPeriodIDByDate: 💣 ⛔ dateStr = %s (%w)", params.Date, restErr) + } + // this loop should iterate just once + thePeriod := &CalendarPeriod{} + for _, itm := range response.Payload.Data { + startDate, _ := time.Parse(dateTimeFormat, itm.StartDate) + endDate, _ := time.Parse(dateTimeFormat, itm.EndDate) + thePeriod = &CalendarPeriod{ + ID: itm.ID, + EndDate: endDate, + Name: itm.Name, + StartDate: startDate, + } + } + periodCache.put(params.AccountID, params.Date.Format(dateFormat), thePeriod) + sugar.Debugf("ops.getPeriodIDByDate: 👍 📤 %s", thePeriod.Name) + return thePeriod.ID, nil +} diff --git a/app/root.go b/app/root.go index e69f2a0..9aed395 100644 --- a/app/root.go +++ b/app/root.go @@ -9,6 +9,7 @@ import ( "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/api/ledger/ledger_client" "code.tnxs.net/taxnexus/lib/api/ops/ops_client" "code.tnxs.net/taxnexus/lib/api/regs/regs_client" "code.tnxs.net/taxnexus/lib/api/stash/stash_client" @@ -27,6 +28,7 @@ var config = Configuration{} var configured = false var crmClient = crm_client.Default var geoClient = geo_client.Default +var ledgerClient = ledger_client.Default var opsClient = ops_client.Default var regsClient = regs_client.Default var stashClient = stash_client.Default diff --git a/app/transaction-services.go b/app/transaction-services.go new file mode 100644 index 0000000..be46e1f --- /dev/null +++ b/app/transaction-services.go @@ -0,0 +1,28 @@ +package app + +import ( + "code.tnxs.net/taxnexus/lib/api/regs/regs_client/transaction" + "code.tnxs.net/taxnexus/lib/api/regs/regs_models" +) + +// PostTransactions is a first class object type storage method +func PostTransactions(objList []*TaxTransaction, principal *User) error { + swagList := []*regs_models.Transaction{} + for _, itm := range objList { + swagList = append(swagList, ®s_models.Transaction{ + AccountID: itm.AccountID, + TaxTransactionID: itm.ID, + TaxTypeID: itm.TaxTypeID, + Valid: true, + }) + } + regsParams := transaction.NewPostTransactionsParamsWithTimeout(postTimeout) + regsParams.TransactionRequest = ®s_models.TransactionRequest{ + Data: swagList, + } + _, err := regsClient.Transaction.PostTransactions(regsParams, principal.Auth) + if err != nil { + return err + } + return nil +}