package lulutest import ( crand "crypto/rand" "fmt" mrand "math/rand/v2" "strings" "sync" "time" "github.com/shopspring/decimal" "git.samanthony.xyz/lulu" ) type mockClient struct { mu sync.Mutex ints []lulu.InteriorValidation covs []lulu.CoverValidation jobs []job printables map[lulu.PrintableId]lulu.Printable } func NewMockClient() lulu.Client { return &mockClient{ printables: make(map[lulu.PrintableId]lulu.Printable), } } func (c *mockClient) StartInteriorValidation(srcUrl string, mfg lulu.PkgId) (uint, error) { c.mu.Lock() defer c.mu.Unlock() id := uint(len(c.ints)) val := lulu.InteriorValidation{Id: id, SrcUrl: srcUrl, NPages: 123, Status: lulu.InteriorStatusNormalized} c.ints = append(c.ints, val) return id, nil } func (c *mockClient) StartInteriorValidationBasic(srcUrl string) (uint, error) { return c.StartInteriorValidation(srcUrl, lulu.PkgId{}) } func (c *mockClient) GetInteriorValidation(id uint) (lulu.InteriorValidation, error) { c.mu.Lock() defer c.mu.Unlock() if id >= uint(len(c.ints)) { return lulu.InteriorValidation{}, fmt.Errorf("no interior validation job with id %d", id) } return c.ints[id], nil } func (c *mockClient) CoverDimensions(mfg lulu.PkgId, npages uint, unit lulu.Unit) (lulu.CoverDimensions, error) { return lulu.CoverDimensions{123.4, 56.78, unit}, nil } func (c *mockClient) StartCoverValidation(srcUrl string, mfg lulu.PkgId, npages uint) (uint, error) { c.mu.Lock() defer c.mu.Unlock() id := uint(len(c.covs)) val := lulu.CoverValidation{Id: id, SrcUrl: srcUrl, NPages: npages, Status: lulu.CoverStatusNormalized} c.covs = append(c.covs, val) return id, nil } func (c *mockClient) GetCoverValidation(id uint) (lulu.CoverValidation, error) { c.mu.Lock() defer c.mu.Unlock() if id >= uint(len(c.covs)) { return lulu.CoverValidation{}, fmt.Errorf("no cover validation job with id %d", id) } return c.covs[id], nil } func (c *mockClient) Cost(items []lulu.CostLineItem, addr lulu.ShippingAddress, opt lulu.ShippingLevel) (lulu.Cost, lulu.ShippingAddressValidation, error) { var cost lulu.Cost var subtotal decimal.Decimal var tax decimal.Decimal taxRate := decimal.NewFromFloat(0.15) cost.LineItemCosts = make([]lulu.LineItemCost, len(items)) for i, item := range items { itemCost := decimal.NewFromInt(5 + mrand.Int64N(100)) itemTax := itemCost.Mul(taxRate).RoundCash(5) itemTotal := itemCost.Add(itemTax) cost.LineItemCosts[i] = lulu.LineItemCost{ CostExclDiscounts: itemCost, TotalTax: itemTax, TaxRate: taxRate, Quantity: item.Quantity, TotalCostExclTax: itemCost, TotalCostExclDiscounts: itemTotal, TotalCostInclTax: itemTotal, } subtotal = subtotal.Add(itemCost) tax = tax.Add(itemTax) } shipCost := decimal.NewFromInt(2 + mrand.Int64N(50)) cost.ShipCost = lulu.FulfillmentCost{ TotalCostExclTax: shipCost, TotalCostInclTax: shipCost, } subtotal = subtotal.Add(shipCost) cost.TotalTax = tax cost.TotalCostExclTax = subtotal cost.TotalCostInclTax = subtotal.Add(tax) cost.Currency = "USD" sugAddr := addr repl := strings.NewReplacer( "st", "Rd", "rd", "St", "St", "rd", "Rd", "st", ) sugAddr.Street1 = repl.Replace(addr.Street1) return cost, lulu.ShippingAddressValidation{addr, sugAddr, nil}, nil } type job struct { j <-chan lulu.PrintJob cancel chan chan bool } func print(id uint64, eid string, contact lulu.EmailAddress, delay time.Duration, addr lulu.ShippingAddress, opt lulu.ShippingLevel, addrVal lulu.ShippingAddressValidation, items []lulu.LineItem, cost lulu.Cost) job { jc := make(chan lulu.PrintJob) cancel := make(chan chan bool) go func() { j := lulu.PrintJob{ Contact: contact, Cost: cost, Created: time.Now(), Modified: time.Now(), EstimatedShippingDates: lulu.EstimatedShippingDates{ DispatchMin: lulu.Date(time.Now().Add(delay + 5*time.Minute)), DispatchMax: lulu.Date(time.Now().Add(delay + 24*time.Hour)), ArrivalMin: lulu.Date(time.Now().Add(delay + 48*time.Hour)), ArrivalMax: lulu.Date(time.Now().Add(delay + 72*time.Hour)), }, ExternalId: eid, Id: id, Items: items, OrderId: fmt.Sprintf("o%d", id), ProductionDelay: delay, ProductionDue: time.Now().Add(delay), AddressValidation: addrVal, ShipOpt: opt, } setStatus := func(s lulu.OrderStatus) { now := time.Now() j.Modified = now j.Status = lulu.PrintJobStatus{ Changed: now, Status: s, } } setStatus(lulu.OrderProductionDelayed) prod := time.After(delay) ship := time.After(delay + time.Hour) arrive := time.After(delay + time.Hour + 56*time.Hour) for { select { case jc <- j: case c := <-cancel: if j.Status.Status == lulu.OrderProductionDelayed { setStatus(lulu.OrderCanceled) c <- true } else { c <- false } case <-prod: if j.Status.Status == lulu.OrderProductionDelayed { setStatus(lulu.OrderInProduction) } case <-ship: if j.Status.Status == lulu.OrderInProduction { setStatus(lulu.OrderShipped) } case <-arrive: if j.Status.Status == lulu.OrderShipped { setStatus(lulu.OrderDelivered) } } } }() return job{jc, cancel} } func (c *mockClient) Print(contact lulu.EmailAddress, eid string, delay time.Duration, addr lulu.ShippingAddress, opt lulu.ShippingLevel, items []lulu.Printable) (lulu.PrintJob, error) { c.mu.Lock() defer c.mu.Unlock() id := uint64(len(c.jobs)) lineItems := make([]lulu.LineItem, len(items)) costItems := make([]lulu.CostLineItem, len(items)) for i, it := range items { pid := lulu.PrintableId(crand.Text()) lineItems[i] = lulu.LineItem{ ExternalId: it.ExternalId, Mfg: it.Mfg, PrintableId: pid, Quantity: it.Quantity, Title: it.Title, } c.printables[pid] = it costItems[i] = lulu.CostLineItem{ Mfg: it.Mfg, Quantity: it.Quantity, } } return c.print(id, eid, contact, delay, addr, opt, lineItems, costItems) } func (c *mockClient) Reprint(contact lulu.EmailAddress, eid string, delay time.Duration, addr lulu.ShippingAddress, opt lulu.ShippingLevel, items []lulu.Reprintable) (lulu.PrintJob, error) { c.mu.Lock() defer c.mu.Unlock() id := uint64(len(c.jobs)) lineItems := make([]lulu.LineItem, len(items)) costItems := make([]lulu.CostLineItem, len(items)) for i, it := range items { pid := it.PrintableId p, ok := c.printables[pid] if !ok { return lulu.PrintJob{}, fmt.Errorf("printable %q does not exist", pid) } lineItems[i] = lulu.LineItem{ ExternalId: it.ExternalId, Mfg: p.Mfg, PrintableId: pid, Quantity: it.Quantity, Title: it.Title, } costItems[i] = lulu.CostLineItem{ Mfg: p.Mfg, Quantity: it.Quantity, } } return c.print(id, eid, contact, delay, addr, opt, lineItems, costItems) } func (c *mockClient) print(id uint64, eid string, contact lulu.EmailAddress, delay time.Duration, addr lulu.ShippingAddress, opt lulu.ShippingLevel, lineItems []lulu.LineItem, costItems []lulu.CostLineItem) (lulu.PrintJob, error) { cost, addrVal, err := c.Cost(costItems, addr, opt) if err != nil { return lulu.PrintJob{}, err } j := print(id, eid, contact, delay, addr, opt, addrVal, lineItems, cost) c.jobs = append(c.jobs, j) return <-j.j, nil } func (c *mockClient) Job(id uint64) (lulu.PrintJob, error) { c.mu.Lock() defer c.mu.Unlock() if id < uint64(len(c.jobs)) { return <-c.jobs[id].j, nil } return lulu.PrintJob{}, fmt.Errorf("job %d does not exist", id) } // Ignores queries func (c *mockClient) Jobs(qs ...lulu.PrintJobQuery) ([]lulu.PrintJob, error) { c.mu.Lock() defer c.mu.Unlock() js := make([]lulu.PrintJob, len(c.jobs)) for i := range c.jobs { js[i] = <-c.jobs[i].j } return js, nil } func (c *mockClient) Cancel(id uint64) error { c.mu.Lock() defer c.mu.Unlock() if id < uint64(len(c.jobs)) { cc := make(chan bool) defer close(cc) c.jobs[id].cancel <- cc if <-cc { return nil } else { return fmt.Errorf("failed to cancel job %d", id) } } else { return fmt.Errorf("job %d does not exist", id) } }