diff --git a/app/address.go b/app/address.go index 17ee4d6..3f5083e 100644 --- a/app/address.go +++ b/app/address.go @@ -1,6 +1,8 @@ package app import ( + "encoding/json" + "code.tnxs.net/taxnexus/lib/api/crm/crm_models" "code.tnxs.net/taxnexus/lib/api/ops/ops_models" ) @@ -16,6 +18,21 @@ type Address struct { Street string } +// ToString returns the full address in a string +func (obj *Address) ToString() string { + return obj.Street + " " + + obj.City + " " + + obj.State + " " + + obj.PostalCode +} + +// MarshalToJSON returns a JSON string representation of the address +func (obj *Address) MarshalToJSON() *string { + bytes, _ := json.Marshal(obj) + str := string(bytes) + return &str +} + // MarshalToCrm converts a first class object to swagger func (obj *Address) MarshalToCrm() *crm_models.Address { if obj == nil { diff --git a/app/contact-cache.go b/app/contact-cache.go new file mode 100644 index 0000000..7f9390c --- /dev/null +++ b/app/contact-cache.go @@ -0,0 +1,25 @@ +package app + +import "sync" + +var contactCache = contactCacheType{ + obj: map[string]*Contact{}, +} + +type contactCacheType struct { + sync.RWMutex + obj map[string]*Contact +} + +func (m *contactCacheType) get(recordID string) (*Contact, bool) { + m.RLock() + defer m.RUnlock() + r, ok := m.obj[recordID] + return r, ok +} + +func (m *contactCacheType) put(recordID string, itm *Contact) { + m.Lock() + defer m.Unlock() + m.obj[recordID] = itm +} diff --git a/app/contact-services.go b/app/contact-services.go new file mode 100644 index 0000000..278be2a --- /dev/null +++ b/app/contact-services.go @@ -0,0 +1,49 @@ +package app + +import ( + "fmt" + + "code.tnxs.net/taxnexus/lib/api/crm/crm_client/contacts" +) + +// GetContact is a first class object retrieval function +func GetContact(id string, principal *User) Contact { + if id == "" { + return Contact{} + } + c, ok := contactCache.get(id) + if ok { + return *c + } + c, err := GetContactByID(id, principal) + if err != nil { + return Contact{} + } + return *c +} + +// GetContactByID is a first class object retrieval function +func GetContactByID(key string, principal *User) (*Contact, error) { + sugar.Debugf("render.getContactByID: 📥") + if key == "" { + return nil, fmt.Errorf("render.getContactByID: 💣 ⛔ key is blank") + } + obj, ok := contactCache.get(key) + if ok { + sugar.Debugf("render.getContactByID: 👍 🎯 📤") + return obj, nil + } + params := contacts.NewGetContactsParams() + params.ContactID = &key + response, err := crmClient.Contacts.GetContacts(params, principal.Auth) + if err != nil { + return nil, err + } + var newObj *Contact + for _, itm := range response.Payload.Data { // single iteration execution + newObj = UnMarshalContact(itm) + } + contactCache.put(key, newObj) + sugar.Debugf("render.getContactByID: 👍 🆕 📤") + return newObj, nil +} diff --git a/app/coordinate-helpers.go b/app/coordinate-helpers.go index 09d76fa..d48e53d 100644 --- a/app/coordinate-helpers.go +++ b/app/coordinate-helpers.go @@ -1,8 +1,58 @@ package app -import "code.tnxs.net/taxnexus/lib/api/geo/geo_models" +import ( + "encoding/base64" + "html/template" -// Coordinate is never ingested, hence no UnMarshal method + "code.tnxs.net/taxnexus/lib/api/geo/geo_models" +) + +func UnMarshalSwaggerCoordinate(s *geo_models.Coordinate, principal *User) *Coordinate { + taxTypes := []*TaxType{} + for _, itm := range s.TaxTypes { + if itm.ID != "" { + taxTypes = append(taxTypes, UnMarshalTaxType(itm)) + } + } + var situs string + if s.IsDistrict { + situs = s.State + " / " + s.County + " / " + s.Place + } else { + situs = s.State + " / " + s.County + } + return &Coordinate{ + ID: s.ID, + BusinessTaxTypes: GetBusinessTaxTypes(taxTypes), + CannabisTypes: GetCannabisTaxTypes(taxTypes), + Country: s.Country, + CountryID: s.CountryID, + County: s.County, + CountyID: s.CountyID, + ExciseTaxTypes: GetExciseTaxTypes(taxTypes), + Focus: s.Focus, + FormattedAddress: s.FormattedAddress, + IsDistrict: s.IsDistrict, + Latitude: s.Latitude, + Longitude: s.Longitude, + Map: template.URL(base64.RawStdEncoding.EncodeToString(s.Map)), //nolint:gosec // it definitely is + MerchTaxTypes: GetMerchTaxTypes(taxTypes), + Name: s.Name, + Neighborhood: s.Neighborhood, + Place: s.Place, + Geocode: s.PlaceGeocode, + PlaceID: s.PlaceID, + PostalCode: s.PostalCode, + Ref: s.Ref, + Situs: situs, + State: s.State, + StateID: s.StateID, + Status: s.Status, + Street: s.Street, + StreetNumber: s.StreetNumber, + StreetView: template.URL(base64.RawStdEncoding.EncodeToString(s.StreetView)), //nolint:gosec,lll // it definitely is + TelecomTaxTypes: GetTelecomTaxTypes(taxTypes), + } +} // MarshalToSwagger encodes a first class object to swagger func (obj *Coordinate) MarshalToSwagger() *geo_models.Coordinate { @@ -33,21 +83,21 @@ func (obj *Coordinate) MarshalToSwagger() *geo_models.Coordinate { IsDistrict: obj.IsDistrict, Latitude: obj.Latitude, Longitude: obj.Longitude, - Map: obj.Map, - Name: obj.Name, - Neighborhood: obj.Neighborhood, - Place: obj.Place, - PlaceGeocode: obj.Geocode, - PlaceID: obj.PlaceID, - PostalCode: obj.PostalCode, - Ref: obj.Ref, - State: obj.State, - StateID: obj.StateID, - Status: obj.Status, - Street: obj.Street, - StreetNumber: obj.StreetNumber, - StreetView: obj.StreetView, - TaxTypes: theTaxTypes, - TaxRate: &taxRate, + // Map: obj.Map, + Name: obj.Name, + Neighborhood: obj.Neighborhood, + Place: obj.Place, + PlaceGeocode: obj.Geocode, + PlaceID: obj.PlaceID, + PostalCode: obj.PostalCode, + Ref: obj.Ref, + State: obj.State, + StateID: obj.StateID, + Status: obj.Status, + Street: obj.Street, + StreetNumber: obj.StreetNumber, + // StreetView: obj.StreetView, + TaxTypes: theTaxTypes, + TaxRate: &taxRate, } } diff --git a/app/coordinate.go b/app/coordinate.go index 2b34ab6..2754a07 100644 --- a/app/coordinate.go +++ b/app/coordinate.go @@ -2,6 +2,7 @@ package app import ( "database/sql" + "html/template" "code.tnxs.net/taxnexus/lib/api/geo/geo_models" ) @@ -40,12 +41,15 @@ type CoordinateActivityWrapper struct { // Coordinate is a first class object type type Coordinate struct { ID string + BusinessTaxTypes []*TaxType + CannabisTypes []*TaxType Country string CountryID string County string CountyID string CreatedByID string CreatedDate sql.NullTime + ExciseTaxTypes []*TaxType Focus string FormattedAddress string Geocode string @@ -54,21 +58,24 @@ type Coordinate struct { LastModifiedDate sql.NullTime Latitude float64 Longitude float64 + Map template.URL + MerchTaxTypes []*TaxType Name string Neighborhood string - Map []byte OwnerID string Place string PlaceID string PostalCode string Ref string + Situs string State string StateID string Status string Street string StreetNumber string - StreetView []byte + StreetView template.URL TaxRate *TaxRate - TaxTypes []*TaxType TaxTypeIDs []*string + TaxTypes []*TaxType + TelecomTaxTypes []*TaxType } diff --git a/app/document.go b/app/document.go new file mode 100644 index 0000000..bd8ff01 --- /dev/null +++ b/app/document.go @@ -0,0 +1,13 @@ +package app + +// Document is an object type used for rendering +type Document struct { + ID string + Filename string + HTML string + ParentID string + PDF string + SagaType string + Title string + URI string +} diff --git a/app/render-float.go b/app/render-float.go new file mode 100644 index 0000000..3869a29 --- /dev/null +++ b/app/render-float.go @@ -0,0 +1,195 @@ +package app + +/* + +Author: https://github.com/gorhill +Source: https://gist.github.com/gorhill/5285193 + +A Go function to render a number to a string based on +the following user-specified criteria: + +* thousands separator +* decimal separator +* decimal precision + +Usage: s := RenderFloat(format, n) + +The format parameter tells how to render the number n. + +http://play.golang.org/p/LXc1Ddm1lJ + +Examples of format strings, given n = 12345.6789: + +"#,###.##" => "12,345.67" +"#,###." => "12,345" +"#,###" => "12345,678" +"#\u202F###,##" => "12 345,67" +"#.###,###### => 12.345,678900 +"" (aka default format) => 12,345.67 + +The highest precision allowed is 9 digits after the decimal symbol. +There is also a version for integer number, RenderInteger(), +which is convenient for calls within template. + +I didn't feel it was worth to publish a library just for this piece +of code, hence the snippet. Feel free to reuse as you wish. +const rPattern = "#,###.##" +*/ + +import ( + "math" + "strconv" +) + +var renderFloatPrecisionMultipliers = [10]float64{ + 1, + 10, + 100, + 1000, + 10000, + 100000, + 1000000, + 10000000, + 100000000, + 1000000000, +} + +var renderFloatPrecisionRounders = [10]float64{ + 0.5, + 0.05, + 0.005, + 0.0005, + 0.00005, + 0.000005, + 0.0000005, + 0.00000005, + 0.000000005, + 0.0000000005, +} + +// func renderInteger(format string, n int) string { +// return renderFloat(format, float64(n)) +// } + +// RenderFloat renders a number to a string +func RenderFloat(format string, n float64) string { //nolint:funlen,gocyclo // lots of comments + // Special cases: + // NaN = "NaN" + // +Inf = "+Infinity" + // -Inf = "-Infinity" + if math.IsNaN(n) { + return "NaN" + } + if n > math.MaxFloat64 { + return "Infinity" + } + if n < -math.MaxFloat64 { + return "-Infinity" + } + + // default format + precision := 2 + decimalStr := "." + thousandStr := "," + positiveStr := "" + negativeStr := "-" + + if len(format) > 0 { + // If there is an explicit format directive, + // then default values are these: + precision = 9 + thousandStr = "" + + // collect indices of meaningful formatting directives + formatDirectiveChars := []rune(format) + formatDirectiveIndices := make([]int, 0) + for i, char := range formatDirectiveChars { + if char != '#' && char != '0' { + formatDirectiveIndices = append(formatDirectiveIndices, i) + } + } + + if len(formatDirectiveIndices) > 0 { + // Directive at index 0: + // Must be a '+' + // Raise an error if not the case + // index: 0123456789 + // +0.000,000 + // +000,000.0 + // +0000.00 + // +0000 + if formatDirectiveIndices[0] == 0 { + if formatDirectiveChars[formatDirectiveIndices[0]] != '+' { + panic("RenderFloat(): invalid positive sign directive") + } + positiveStr = "+" + formatDirectiveIndices = formatDirectiveIndices[1:] + } + + // Two directives: + // First is thousands separator + // Raise an error if not followed by 3-digit + // 0123456789 + // 0.000,000 + // 000,000.00 + if len(formatDirectiveIndices) == 2 { //nolint:gomnd // not my code + if (formatDirectiveIndices[1] - formatDirectiveIndices[0]) != 4 { //nolint:gomnd // not my code + panic("RenderFloat(): thousands separator directive must be followed by 3 digit-specifiers") + } + thousandStr = string(formatDirectiveChars[formatDirectiveIndices[0]]) + formatDirectiveIndices = formatDirectiveIndices[1:] + } + + // One directive: + // Directive is decimal separator + // The number of digit-specifier following the separator indicates wanted precision + // 0123456789 + // 0.00 + // 000,0000 + if len(formatDirectiveIndices) == 1 { + decimalStr = string(formatDirectiveChars[formatDirectiveIndices[0]]) + precision = len(formatDirectiveChars) - formatDirectiveIndices[0] - 1 + } + } + } + + // generate sign part + var signStr string + if n >= 0.000000001 { //nolint:gomnd,gocritic // not my code! + signStr = positiveStr + } else if n <= -0.000000001 { + signStr = negativeStr + n = -n + } else { + signStr = "" + n = 0.0 + } + + // split number into integer and fractional parts + intf, fracf := math.Modf(n + renderFloatPrecisionRounders[precision]) + + // generate integer part string + intStr := strconv.Itoa(int(intf)) + + // add thousand separator if required + if len(thousandStr) > 0 { + for i := len(intStr); i > 3; { + i -= 3 + intStr = intStr[:i] + thousandStr + intStr[i:] + } + } + + // no fractional part, we can leave now + if precision == 0 { + return signStr + intStr + } + + // generate fractional part + fracStr := strconv.Itoa(int(fracf * renderFloatPrecisionMultipliers[precision])) + // may need padding + if len(fracStr) < precision { + fracStr = "000000000000000"[:precision-len(fracStr)] + fracStr + } + + return signStr + intStr + decimalStr + fracStr +} diff --git a/app/root.go b/app/root.go index af7d85c..d0448b9 100644 --- a/app/root.go +++ b/app/root.go @@ -8,6 +8,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/stash/stash_client" "code.tnxs.net/taxnexus/lib/app/logger" httptransport "github.com/go-openapi/runtime/client" "github.com/spf13/viper" @@ -21,6 +22,7 @@ var appViper = viper.New() var auth0Client = auth0_client.Default var geoClient = geo_client.Default var crmClient = crm_client.Default +var stashClient = stash_client.Default var configured = false var apiUsers map[string]User diff --git a/app/stash-services.go b/app/stash-services.go new file mode 100644 index 0000000..ed5d17a --- /dev/null +++ b/app/stash-services.go @@ -0,0 +1,71 @@ +package app + +import ( + "code.tnxs.net/taxnexus/lib/api/stash/stash_client/stash_pdf" + "code.tnxs.net/taxnexus/lib/api/stash/stash_models" +) + +type StashPDFParams struct { + document *Document + account *Account + objectType string + principal *User +} + +func StashPDF(params StashPDFParams) (*Document, error) { + sugar.Debugf("render.stashPDF: 📥") + var title string + var fileName string + var description string + var ref string + switch params.objectType { + case "account": + title = "Taxnexus Report for " + params.account.Name + fileName = "Taxnexus Report for " + params.account.Name + ".pdf" + description = "Account Report generated by render" + ref = "render.getAccounts" + case "tax_summary": + title = "Taxnexus Tax Report for " + params.account.Name + fileName = params.account.Name + " Tax Report.pdf" + description = "CDTFA Quarterly District Sales and Use Tax for " + ref = "render.getTaxes" + } + + todoString := "todo" // todo #5 + stashParams := stash_pdf.NewPostPdfsParamsWithTimeout(getTimeout) + stashParams.PDFRequest = &stash_models.PDFRequest{ + Meta: &stash_models.RequestMeta{ + TaxnexusAccount: &todoString, + }, + Data: []*stash_models.NewPDF{ + { + Description: description, + Filename: fileName, + HTML: params.document.HTML, + ObjectType: params.document.SagaType, + OwnerID: "fabric", // todo #6 make OwnerID in new PDF real + ParentID: params.document.ParentID, + Ref: ref, + Title: title, + }, + }, + } + response, err := stashClient.StashPdf.PostPdfs(stashParams, params.principal.Auth) + if err != nil { + sugar.Errorf("render.stashPDF: 💣 ⛔ post PDF to stash error %w", err) + return nil, err + } + var newObj *stash_models.Document + for _, itm := range response.Payload.Data { // single iteration execution + newObj = itm + } + sugar.Debugf("render.stashPDF: 👍 📤") + return &Document{ + ID: newObj.ID, + Filename: params.account.Name + fileName, + SagaType: params.document.SagaType, + ParentID: params.document.ParentID, + Title: title + params.account.Name, + URI: newObj.URI, + }, nil +}