Skip to content

Commit

Permalink
Merge pull request #8 from rorycl/bugfix-decimal-pricing
Browse files Browse the repository at this point in the history
Bugfix decimal pricing. Fixes #7.
  • Loading branch information
rorycl authored Jun 26, 2024
2 parents 7d6b28e + 595b52f commit ffef172
Show file tree
Hide file tree
Showing 19 changed files with 12,324 additions and 53 deletions.
21 changes: 10 additions & 11 deletions CHANGELOG.txt
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
Changelog for release v0.2.5
Changelog for release v0.2.6

Update to use decimal.Decimal rather than int to support cheaper items
on Cex which have decimal prices.

* docs: update README for v0.2.4
* cex: replace long store names and truncate store listings; update tests
* cex: update results to show cash/exchange prices and stores
* console: use both title and description for items to show prices, stores
also regenerate console.gif from console.vhs
* console: tweak colours
* cli: update to provide verbose option to show pricing, stores
also regenerate cli.gif from cli.vhs
* go: update modules
* docker: minor "add" tweaks to Dockerfile
* web: update htmx to v2.0
* web: update int pricing to use decimal.Decimal
* console: update int pricing to use decimal.Decimal
* cli: update int pricing to use decimal.Decimal
* query: bubble up json unmarshall errors properly
* cex: change int pricing to decimal.Decimal, update tests accordingly
* testdata: add example data json file with decimal results
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# cexfind

v0.2.4 : 21 June 2024 : add buy/exchange and store info; htmx 2.0
v0.2.6 : 26 June 2024 : change pricing from int to decimal.Decimal

## Find kit on Cex, fast

Expand Down
10 changes: 6 additions & 4 deletions cex.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ import (
"fmt"
"slices"
"strings"

"github.com/shopspring/decimal"
)

// Box is a very simplified representation of a Cex/Webuy json entry,
Expand All @@ -46,9 +48,9 @@ type Box struct {
Model string
Name string
ID string
Price int
PriceCash int
PriceExchange int
Price decimal.Decimal
PriceCash decimal.Decimal
PriceExchange decimal.Decimal
Stores []string
}

Expand Down Expand Up @@ -119,7 +121,7 @@ func (b *boxes) sort() {
if c != 0 {
return c
}
c = cmp.Compare(i.Price, j.Price)
c = i.Price.Compare(j.Price)
if c != 0 {
return c
}
Expand Down
50 changes: 43 additions & 7 deletions cex_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"testing"

"github.com/google/go-cmp/cmp"
"github.com/shopspring/decimal"
)

// TestBoxInQuery tests strict query/Box.Name matches
Expand Down Expand Up @@ -84,7 +85,42 @@ func TestSearch(t *testing.T) {
if err == nil || err.Error() != "no results" {
t.Fatalf("expected no results error, got %v", err)
}
}

// Search for terminator search string
func TestSearchTerminator(t *testing.T) {

f, err := os.Open("testdata/terminator.json")
if err != nil {
t.Fatal(err)
}
contents, err := io.ReadAll(f)
if err != nil {
t.Fatal(err)
}

ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, string(contents))
}))
defer ts.Close()

// overwrite global URL with test URL
URL = ts.URL

// non-strict search
results, err := Search([]string{"terminator"}, false)
if err != nil {
t.Fatal(err)
}

if got, want := len(results), 17; got != want {
t.Fatalf("expected %d box results, got %d", want, got)
}

// verbose output (use test -v)
for _, v := range results {
t.Log("\t", v)
}
}

// TestBoxSort tests box sorting
Expand All @@ -93,13 +129,13 @@ func TestBoxSort(t *testing.T) {
var toSortBoxes boxes
toSortBoxes = append(toSortBoxes,
[]Box{
{"bb", "bb", "id1a", 20, 15, 17, []string{"a"}},
{"bc", "cc", "id2a", 25, 15, 17, []string{"a"}},
{"ba", "aa", "id3a", 15, 15, 17, []string{"a"}},
{"ab", "db", "id3b", 30, 15, 17, []string{"a"}},
{"ac", "dc", "id2z", 35, 15, 17, []string{"a"}},
{"aa", "da", "id1a", 35, 15, 17, []string{"a"}},
{"aa", "la", "id1b", 30, 15, 17, []string{"a"}}, // 0
{"bb", "bb", "id1a", decimal.NewFromInt(20), decimal.NewFromInt(15), decimal.NewFromInt(17), []string{"a"}},
{"bc", "cc", "id2a", decimal.NewFromInt(25), decimal.NewFromInt(15), decimal.NewFromInt(17), []string{"a"}},
{"ba", "aa", "id3a", decimal.NewFromInt(15), decimal.NewFromInt(15), decimal.NewFromInt(17), []string{"a"}},
{"ab", "db", "id3b", decimal.NewFromInt(30), decimal.NewFromInt(15), decimal.NewFromInt(17), []string{"a"}},
{"ac", "dc", "id2z", decimal.NewFromInt(35), decimal.NewFromInt(15), decimal.NewFromInt(17), []string{"a"}},
{"aa", "da", "id1a", decimal.NewFromInt(35), decimal.NewFromInt(15), decimal.NewFromInt(17), []string{"a"}},
{"aa", "la", "id1b", decimal.NewFromInt(30), decimal.NewFromInt(15), decimal.NewFromInt(17), []string{"a"}}, // 0
}...,
)

Expand Down
1 change: 1 addition & 0 deletions cmd/cli/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ require (
require (
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
golang.org/x/sys v0.14.0 // indirect
golang.org/x/text v0.16.0 // indirect
)
2 changes: 2 additions & 0 deletions cmd/cli/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
Expand Down
8 changes: 4 additions & 4 deletions cmd/cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,14 +108,14 @@ func main() {
}
if verbose {
info := fmt.Sprintf(" (%d/%d) %s",
box.PriceCash,
box.PriceExchange,
box.PriceCash.IntPart(),
box.PriceExchange.IntPart(),
box.StoresString(),
)
fmt.Printf(
"%s %-3d %s %s\n%s\n %s\n",
dotStyle("✱"),
box.Price,
box.Price.IntPart(),
box.Name,
box.ID,
infoStyle(info),
Expand All @@ -125,7 +125,7 @@ func main() {
fmt.Printf(
"%s %-3d %s %s\n %s\n",
dotStyle("✱"),
box.Price,
box.Price.IntPart(),
box.Name,
box.ID,
urlStyle(box.IDUrl()),
Expand Down
5 changes: 2 additions & 3 deletions cmd/console/delegate.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,8 @@ type CustomItemStyles struct {
func NewCustomItemStyles() (s CustomItemStyles) {

s.Heading = lipgloss.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "#d40000", Dark: "#d40000"}).
Padding(0, 0, 0, 2).
Bold(true)
Foreground(lipgloss.AdaptiveColor{Light: "#f4eee0", Dark: "#f4eee0"}).
Padding(0, 0, 0, 2)
s.SelectedHeading = lipgloss.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "#b7b7b7", Dark: "#b7b7b7"}).
BorderForeground(lipgloss.AdaptiveColor{Light: "#ff982e", Dark: "#ff982e"}).
Expand Down
4 changes: 2 additions & 2 deletions cmd/console/find.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,8 @@ func find(query string, strict bool) (items []list.Item, itemNo int, err error)
}
// add standard item
items = append(items, item{
title: fmt.Sprintf(boxTitleTpl, box.Price, box.Name),
description: fmt.Sprintf(boxDescriptionTpl, box.PriceCash, box.PriceExchange, box.StoresString()),
title: fmt.Sprintf(boxTitleTpl, box.Price.IntPart(), box.Name),
description: fmt.Sprintf(boxDescriptionTpl, box.PriceCash.IntPart(), box.PriceExchange.IntPart(), box.StoresString()),
url: box.IDUrl(),
})
}
Expand Down
1 change: 1 addition & 0 deletions cmd/console/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ require (
github.com/muesli/termenv v0.15.2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f // indirect
github.com/shopspring/decimal v1.4.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.12.0 // indirect
golang.org/x/term v0.6.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions cmd/console/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f h1:MvTmaQdww/z0Q4wrYjDSCcZ78NoftLQyHBSLW/Cx79Y=
github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
Expand Down
1 change: 1 addition & 0 deletions cmd/web/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@ require (

require (
github.com/felixge/httpsnoop v1.0.3 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
golang.org/x/text v0.16.0 // indirect
)
2 changes: 2 additions & 0 deletions cmd/web/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/schema v1.3.0 h1:rbciOzXAx3IB8stEFnfTwO3sYa6EWlQk79XdyustPDA=
github.com/gorilla/schema v1.3.0/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
7 changes: 4 additions & 3 deletions cmd/web/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"testing"

cex "github.com/rorycl/cexfind"
"github.com/shopspring/decimal"
)

// TestSetupFS sets up the FS
Expand Down Expand Up @@ -118,9 +119,9 @@ func TestResults(t *testing.T) {
// override package global searcher which indirects Search
searcher = func(queries []string, strict bool) ([]cex.Box, error) {
return []cex.Box{
cex.Box{Model: "2a", Name: "2a name", ID: "id3", Price: 3},
cex.Box{Model: "1a", Name: "1a name", ID: "id1", Price: 1},
cex.Box{Model: "1b", Name: "1b name", ID: "id2", Price: 2},
cex.Box{Model: "2a", Name: "2a name", ID: "id3", Price: decimal.NewFromInt(3)},
cex.Box{Model: "1a", Name: "1a name", ID: "id1", Price: decimal.NewFromInt(1)},
cex.Box{Model: "1b", Name: "1b name", ID: "id2", Price: decimal.NewFromInt(2)},
}, nil
}

Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ go 1.22

require (
github.com/google/go-cmp v0.6.0
github.com/shopspring/decimal v1.4.0
golang.org/x/text v0.16.0
)
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
53 changes: 35 additions & 18 deletions query.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
Expand All @@ -15,6 +16,7 @@ import (
"strings"
"sync"

"github.com/shopspring/decimal"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
Expand All @@ -23,7 +25,23 @@ var (
// URL is the Cex/Webuy search endpoint
URL = "https://search.webuy.io/1/indexes/*/queries"
// json body with placeholder MODEL; note that the availability online filter ensures only available kit is returned
jsonBody = `{"requests":[{"indexName":"prod_cex_uk","params":"clickAnalytics=true&facetFilters=%5B%5B%22availability%3AIn%20Stock%20Online%22%5D%5D&facets=%5B%22*%22%5D&filters=boxVisibilityOnWeb%3D1%20AND%20boxSaleAllowed%3D1&highlightPostTag=__%2Fais-highlight__&highlightPreTag=__ais-highlight__&hitsPerPage=17&maxValuesPerFacet=1000&page=0&query=MODEL&tagFilters=&userToken=71d182c769bd4dbc94081214a363c014"}]}`
jsonBody = strings.ReplaceAll(`{"requests": [
{
"indexName": "prod_cex_uk",
"params": "clickAnalytics=true
&facetFilters=%5B%5B%22availability%3AIn%20Stock%20Online%22%5D%5D
&facets=%5B%22*%22%5D
&filters=boxVisibilityOnWeb%3D1%20
&highlightPostTag=__%2Fais-highlight__
&highlightPreTag=__ais-highlight__
&hitsPerPage=50
&maxValuesPerFacet=1000
&page=0
&query=MODEL
&tagFilters=
&userToken=71d182c769bd4dbc94081214a363c014"
}]}`, "\n ", "")

// urlDetail is the Cex/Webuy base url for individual items
urlDetail = "https://uk.webuy.com/product-detail?id="
// save web output to temp file if DEBUG true
Expand All @@ -39,13 +57,13 @@ type jsonResults struct {
BoxName string `json:"boxName"`
BoxID string `json:"boxId"`
// Available int `json:"collectionQuantity"` // returns 0 or greater
Price int `json:"sellPrice"`
PriceCash int `json:"cashPriceCalculated"` // offer price for this kit in cash
PriceExchange int `json:"exchangePriceCalculated"` // offer price for exchange
Stores []string `json:"stores"`
Price decimal.Decimal `json:"sellPrice"`
PriceCash decimal.Decimal `json:"cashPriceCalculated"` // offer price for this kit in cash
PriceExchange decimal.Decimal `json:"exchangePriceCalculated"` // offer price for exchange
Stores []string `json:"stores"`
} `json:"hits"`
NbHits int `json:"nbHits"`
HitsPerPage int `json:"hitsPerPage"`
// NbHits int `json:"nbHits",omitempty`
// HitsPerPage int `json:"hitsPerPage",omitempty`
} `json:"results"`
}

Expand Down Expand Up @@ -111,13 +129,13 @@ func postQuery(queryBytes []byte) (jsonResults, error) {
client := &http.Client{}
response, err := client.Do(request)
if err != nil {
return r, err
return r, fmt.Errorf("http call error: %w", err)
}
defer response.Body.Close()

responseBytes, err := io.ReadAll(response.Body)
if err != nil {
return r, err
return r, fmt.Errorf("http response read error: %w", err)
}

/* save to a temporary json file for inspection */
Expand All @@ -130,18 +148,17 @@ func postQuery(queryBytes []byte) (jsonResults, error) {

err = json.Unmarshal(responseBytes, &r)
if err != nil {
var ju *json.UnmarshalTypeError
if errors.As(err, &ju) {
// no results tend to provide data that cannot be parsed,
// used for a general "home" type page
return r, NoResultsFoundError
}
// assume html page; try and extract heading
// html page might have been returned; try and extract heading
reason := headingExtract(responseBytes)
if reason == "" {
if reason != "" {
reason = "unknown or unmarshalling error"
return r, errors.New(reason)
}
var ju *json.UnmarshalTypeError
if errors.As(err, &ju) {
return r, fmt.Errorf("json unmarshalling error: %w", err)
}
return r, errors.New(reason)
return r, fmt.Errorf("json reading error %w", err)
}
if len(r.Results) < 1 || len(r.Results[0].Hits) < 1 {
return r, NoResultsFoundError
Expand Down
4 changes: 4 additions & 0 deletions testdata/jq.cmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# investigate json with jq

cat terminator.json | jq .results[0].hits[0] | jq keys | egrep "box"
cat terminator.json | jq .results[0].hits[0] | jq '{"boxId","boxName","sellPrice","cashPriceCalculated","exchangePriceCalculated"}'
Loading

0 comments on commit ffef172

Please sign in to comment.