stuff
This commit is contained in:
1 parent
5807132ac6
commit
ccb43808fa
4 files changed
+177
-8
No files matched your search
+51
-2
@@ -143,6 +143,9 @@ func Apply(s Schema, source map[string]any) (Projection, error) {
|
|||||||
TaxClass: tax,
|
TaxClass: tax,
|
||||||
InventoryTracked: boolOf(source, s.InventoryTrackedF, truthy),
|
InventoryTracked: boolOf(source, s.InventoryTrackedF, truthy),
|
||||||
DropShip: boolOf(source, s.DropShipF, truthy),
|
DropShip: boolOf(source, s.DropShipF, truthy),
|
||||||
|
SellerID: uint32Of(source, s.SellerIdField),
|
||||||
|
SellerName: stringOf(source, s.SellerNameField),
|
||||||
|
LeadTimeDays: intOf(source, s.LeadTimeField),
|
||||||
Category: stringOf(source, s.CategoryField),
|
Category: stringOf(source, s.CategoryField),
|
||||||
Category2: stringOf(source, s.Category2Field),
|
Category2: stringOf(source, s.Category2Field),
|
||||||
Category3: stringOf(source, s.Category3Field),
|
Category3: stringOf(source, s.Category3Field),
|
||||||
@@ -260,11 +263,57 @@ func uint32Of(m map[string]any, key string) uint32 {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// intOf returns the value at key as an int (floor for fractional floats).
|
||||||
|
// Missing / non-numeric / negative values return 0 — Projection.LeadTimeDays
|
||||||
|
// = 0 is the documented "unset" signal, mirroring uint32Of's contract.
|
||||||
|
func intOf(m map[string]any, key string) int {
|
||||||
|
if key == "" {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
v, ok := m[key]
|
||||||
|
if !ok || v == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
switch x := v.(type) {
|
||||||
|
case int:
|
||||||
|
if x < 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return x
|
||||||
|
case int64:
|
||||||
|
if x < 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return int(x)
|
||||||
|
case float64:
|
||||||
|
if x < 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return int(x)
|
||||||
|
case json.Number:
|
||||||
|
n, err := x.Int64()
|
||||||
|
if err != nil || n < 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return int(n)
|
||||||
|
case string:
|
||||||
|
s := strings.TrimSpace(x)
|
||||||
|
if s == "" {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
n, err := strconv.ParseFloat(s, 64)
|
||||||
|
if err != nil || n < 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return int(n)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
// boolOf returns the value at key coerced through truthy. nil/missing keys
|
// boolOf returns the value at key coerced through truthy. nil/missing keys
|
||||||
// are false; strings lower-cased and matched against truthy; bools passed
|
// are false; strings lower-cased and matched against truthy; bools passed
|
||||||
// through; numerics "!= 0"; json.Number goes through Float64.
|
// through; numerics "!= 0"; json.Number goes through Float64.
|
||||||
func boolOf(m map[string]any, key string, truthy []string) bool {
|
func boolOf(m map[string]any, key string, truthy []string) bool { if key == "" {
|
||||||
if key == "" {
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
v, ok := m[key]
|
v, ok := m[key]
|
||||||
|
|||||||
+102
-6
@@ -15,12 +15,12 @@ import (
|
|||||||
func TestBuilderJSONNumber(t *testing.T) {
|
func TestBuilderJSONNumber(t *testing.T) {
|
||||||
// json.Number for SKU
|
// json.Number for SKU
|
||||||
src := map[string]any{
|
src := map[string]any{
|
||||||
"sku": json.Number("123456789012345"), // float64-unsafe
|
"sku": json.Number("123456789012345"), // float64-unsafe
|
||||||
"title": "BigNumber SKU",
|
"title": "BigNumber SKU",
|
||||||
"price_inc_vat": json.Number("14995"), // already øre
|
"price_inc_vat": json.Number("14995"), // already øre
|
||||||
"currency": "SEK",
|
"currency": "SEK",
|
||||||
"tax_class": "standard",
|
"tax_class": "standard",
|
||||||
"updated_at": json.Number("1700000000000"),
|
"updated_at": json.Number("1700000000000"),
|
||||||
}
|
}
|
||||||
p, err := Apply(DefaultSchema(), src)
|
p, err := Apply(DefaultSchema(), src)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -193,3 +193,99 @@ func TestBuilderFractionalStringPriceCorrectScale(t *testing.T) {
|
|||||||
t.Errorf("PriceIncVat: got %d want %d", p.PriceIncVat, 14995)
|
t.Errorf("PriceIncVat: got %d want %d", p.PriceIncVat, 14995)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Drop-ship routing carriers (docs/plan-dropship.md Phase 1): seller id /
|
||||||
|
// name / lead time coerce from per-tenant source keys via Schema overrides,
|
||||||
|
// mirroring how the live feed emits supplierName-style keys. Zero values
|
||||||
|
// (missing keys, negative numbers, junk strings) must stay at the documented
|
||||||
|
// "unset" signals rather than erroring — one bad item can't poison a batch.
|
||||||
|
func TestBuilderDropshipCarriers(t *testing.T) {
|
||||||
|
s := DefaultSchema()
|
||||||
|
s.SellerIdField = "supplierId"
|
||||||
|
s.SellerNameField = "supplierName"
|
||||||
|
s.LeadTimeField = "deliveryTime"
|
||||||
|
|
||||||
|
p, err := Apply(s, map[string]any{
|
||||||
|
"sku": "X-1",
|
||||||
|
"price_inc_vat": int64(100_00),
|
||||||
|
"updated_at": int64(1700000000000),
|
||||||
|
"drop_ship": true,
|
||||||
|
"supplierId": 4711,
|
||||||
|
"supplierName": "Bygg Import AB",
|
||||||
|
"deliveryTime": 3,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("apply with dropship carriers: %v", err)
|
||||||
|
}
|
||||||
|
if !p.DropShip {
|
||||||
|
t.Error("DropShip: got false, want true")
|
||||||
|
}
|
||||||
|
if p.SellerID != 4711 {
|
||||||
|
t.Errorf("SellerID: got %d, want 4711", p.SellerID)
|
||||||
|
}
|
||||||
|
if p.SellerName != "Bygg Import AB" {
|
||||||
|
t.Errorf("SellerName: got %q", p.SellerName)
|
||||||
|
}
|
||||||
|
if p.LeadTimeDays != 3 {
|
||||||
|
t.Errorf("LeadTimeDays: got %d, want 3", p.LeadTimeDays)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Missing keys → unset signals, no error.
|
||||||
|
p, err = Apply(s, map[string]any{
|
||||||
|
"sku": "X-2",
|
||||||
|
"price_inc_vat": int64(100_00),
|
||||||
|
"updated_at": int64(1700000000000),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("apply without dropship carriers: %v", err)
|
||||||
|
}
|
||||||
|
if p.DropShip || p.SellerID != 0 || p.SellerName != "" || p.LeadTimeDays != 0 {
|
||||||
|
t.Errorf("unset carriers should zero: %+v", p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Junk shapes floor to unset, never fail the build.
|
||||||
|
for name, bad := range map[string]any{
|
||||||
|
"negative id": -5,
|
||||||
|
"string id": "not-a-number",
|
||||||
|
"float id": 1.5,
|
||||||
|
} {
|
||||||
|
p, err = Apply(s, map[string]any{
|
||||||
|
"sku": "X-3", "price_inc_vat": int64(100_00),
|
||||||
|
"updated_at": int64(1700000000000),
|
||||||
|
"supplierId": bad,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%s: apply should not fail on junk carrier: %v", name, err)
|
||||||
|
}
|
||||||
|
if p.SellerID != 0 {
|
||||||
|
t.Errorf("%s: SellerID should be 0, got %d", name, p.SellerID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wire round-trip: new fields survive Encode/Decode for consumers that
|
||||||
|
// never rebuilt.
|
||||||
|
b, err := p.Encode()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("encode: %v", err)
|
||||||
|
}
|
||||||
|
rt, err := Decode(b)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("decode: %v", err)
|
||||||
|
}
|
||||||
|
if rt.SKU != p.SKU || rt.DropShip || rt.SellerID != 0 {
|
||||||
|
t.Errorf("round-trip mismatch: %+v vs %+v", rt, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// json.Number lead time (UseNumber producers).
|
||||||
|
p, err = Apply(s, map[string]any{
|
||||||
|
"sku": "X-4", "price_inc_vat": int64(100_00),
|
||||||
|
"updated_at": int64(1700000000000),
|
||||||
|
"deliveryTime": json.Number("7"),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("json.Number lead time: %v", err)
|
||||||
|
}
|
||||||
|
if p.LeadTimeDays != 7 {
|
||||||
|
t.Errorf("LeadTimeDays from json.Number: got %d, want 7", p.LeadTimeDays)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,9 @@
|
|||||||
// tax_class ← source[TaxClassField] (string identifier)
|
// tax_class ← source[TaxClassField] (string identifier)
|
||||||
// inventory_tracked ← bool coercion of source[InventoryTrackedField]
|
// inventory_tracked ← bool coercion of source[InventoryTrackedField]
|
||||||
// drop_ship ← bool coercion of source[DropShipField]
|
// drop_ship ← bool coercion of source[DropShipField]
|
||||||
|
// seller_id ← uint32 coercion of source[SellerIdField] (0 = unset)
|
||||||
|
// seller_name ← source[SellerNameField] (omitempty)
|
||||||
|
// lead_time_days ← int coercion of source[LeadTimeField] (0 = unset)
|
||||||
// category ← source[CategoryField] (omitempty)
|
// category ← source[CategoryField] (omitempty)
|
||||||
// category2 ← source[Category2Field] (omitempty)
|
// category2 ← source[Category2Field] (omitempty)
|
||||||
// category3 ← source[Category3Field] (omitempty)
|
// category3 ← source[Category3Field] (omitempty)
|
||||||
@@ -103,6 +106,15 @@ type Projection struct {
|
|||||||
TaxClass string `json:"tax_class"`
|
TaxClass string `json:"tax_class"`
|
||||||
InventoryTracked bool `json:"inventory_tracked"`
|
InventoryTracked bool `json:"inventory_tracked"`
|
||||||
DropShip bool `json:"drop_ship"`
|
DropShip bool `json:"drop_ship"`
|
||||||
|
// Drop-ship routing carriers (Phase 1 of docs/plan-dropship.md).
|
||||||
|
// SellerID mirrors AddItem.ItemId coercion rules: 0 = "producer didn't
|
||||||
|
// publish" and downstream keeps whatever the HTTP fallback fetched.
|
||||||
|
// SellerName is what checkout writes into order-line
|
||||||
|
// Properties["supplier"] (the dropship-fulfill flow groups on it).
|
||||||
|
// LeadTimeDays is supplier lead time in days, 0 = unset.
|
||||||
|
SellerID uint32 `json:"seller_id,omitempty"`
|
||||||
|
SellerName string `json:"seller_name,omitempty"`
|
||||||
|
LeadTimeDays int `json:"lead_time_days,omitempty"`
|
||||||
Category string `json:"category,omitempty"`
|
Category string `json:"category,omitempty"`
|
||||||
Category2 string `json:"category2,omitempty"`
|
Category2 string `json:"category2,omitempty"`
|
||||||
Category3 string `json:"category3,omitempty"`
|
Category3 string `json:"category3,omitempty"`
|
||||||
|
|||||||
@@ -58,6 +58,15 @@ type Schema struct {
|
|||||||
TaxClassField string `json:"tax_class_field,omitempty"`
|
TaxClassField string `json:"tax_class_field,omitempty"`
|
||||||
InventoryTrackedF string `json:"inventory_tracked_field,omitempty"`
|
InventoryTrackedF string `json:"inventory_tracked_field,omitempty"`
|
||||||
DropShipF string `json:"drop_ship_field,omitempty"`
|
DropShipF string `json:"drop_ship_field,omitempty"`
|
||||||
|
// SellerIdField / SellerNameField name the source keys carrying the
|
||||||
|
// drop-ship supplier identity. SellerId coerces to uint32 (0 = unset);
|
||||||
|
// SellerName is the human-facing name checkout writes into order-line
|
||||||
|
// Properties["supplier"]. Empty field = carrier not published.
|
||||||
|
SellerIdField string `json:"seller_id_field,omitempty"`
|
||||||
|
SellerNameField string `json:"seller_name_field,omitempty"`
|
||||||
|
// LeadTimeField names the source key carrying supplier lead time in
|
||||||
|
// days (numeric; fractional values floor). Empty = not published.
|
||||||
|
LeadTimeField string `json:"lead_time_field,omitempty"`
|
||||||
CategoryField string `json:"category_field,omitempty"`
|
CategoryField string `json:"category_field,omitempty"`
|
||||||
Category2Field string `json:"category2_field,omitempty"`
|
Category2Field string `json:"category2_field,omitempty"`
|
||||||
Category3Field string `json:"category3_field,omitempty"`
|
Category3Field string `json:"category3_field,omitempty"`
|
||||||
@@ -128,6 +137,9 @@ func DefaultSchema() Schema {
|
|||||||
TaxClassField: "tax_class",
|
TaxClassField: "tax_class",
|
||||||
InventoryTrackedF: "inventory_tracked",
|
InventoryTrackedF: "inventory_tracked",
|
||||||
DropShipF: "drop_ship",
|
DropShipF: "drop_ship",
|
||||||
|
SellerIdField: "seller_id",
|
||||||
|
SellerNameField: "seller_name",
|
||||||
|
LeadTimeField: "lead_time_days",
|
||||||
CategoryField: "category",
|
CategoryField: "category",
|
||||||
Category2Field: "category2",
|
Category2Field: "category2",
|
||||||
Category3Field: "category3",
|
Category3Field: "category3",
|
||||||
|
|||||||
Reference in new issue
Block a user