feat(t239): persist freight item metadata
This commit is contained in:
@@ -156,21 +156,109 @@ func normalizeItem(value map[string]any) (domain.FreightSourceItem, error) {
|
||||
if title == "" {
|
||||
title = text(value["detailProductName"])
|
||||
}
|
||||
sku := text(value["sku"])
|
||||
if sku == "" {
|
||||
sku = text(value["variationSku"])
|
||||
productSpec := text(value["productSpec"])
|
||||
productThumbRef, err := optionalExternalID(value["productThumb"])
|
||||
if err != nil {
|
||||
return domain.FreightSourceItem{}, err
|
||||
}
|
||||
originalUnitPriceMinor, err := priceMinor(value["productPrice"])
|
||||
if err != nil {
|
||||
return domain.FreightSourceItem{}, err
|
||||
}
|
||||
return domain.FreightSourceItem{
|
||||
ExternalItemID: externalItemID,
|
||||
Title: title,
|
||||
ProductSpec: text(value["productSpec"]),
|
||||
SKU: sku,
|
||||
Quantity: positiveInt(value["productQty"]),
|
||||
ProductThumbRef: optionalScalar(value["productThumb"]),
|
||||
PurchaseStatus: optionalScalar(value["purchaseStatus"]),
|
||||
ExternalItemID: externalItemID,
|
||||
Title: title,
|
||||
ProductSpec: productSpec,
|
||||
SKU: productSpec,
|
||||
Quantity: positiveInt(value["productQty"]),
|
||||
ProductThumbRef: productThumbRef,
|
||||
OriginalUnitPriceMinor: originalUnitPriceMinor,
|
||||
OriginalCurrency: domain.FreightCurrencyTWD,
|
||||
PurchaseStatus: optionalScalar(value["purchaseStatus"]),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func optionalExternalID(value any) (*string, error) {
|
||||
if value == nil {
|
||||
return nil, nil
|
||||
}
|
||||
if typed, ok := value.(string); ok && strings.TrimSpace(typed) == "" {
|
||||
return nil, nil
|
||||
}
|
||||
normalized, err := externalID(value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &normalized, nil
|
||||
}
|
||||
|
||||
func priceMinor(value any) (*int64, error) {
|
||||
if value == nil {
|
||||
return nil, nil
|
||||
}
|
||||
var raw string
|
||||
switch typed := value.(type) {
|
||||
case string:
|
||||
raw = strings.TrimSpace(typed)
|
||||
case json.Number:
|
||||
raw = string(typed)
|
||||
case float64:
|
||||
if math.IsNaN(typed) || math.IsInf(typed, 0) || typed < 0 {
|
||||
return nil, errInvalidProtocolInput
|
||||
}
|
||||
raw = strconv.FormatFloat(typed, 'f', -1, 64)
|
||||
case int:
|
||||
raw = strconv.Itoa(typed)
|
||||
case int64:
|
||||
raw = strconv.FormatInt(typed, 10)
|
||||
case uint64:
|
||||
raw = strconv.FormatUint(typed, 10)
|
||||
default:
|
||||
return nil, errInvalidProtocolInput
|
||||
}
|
||||
if raw == "" {
|
||||
return nil, nil
|
||||
}
|
||||
whole, fraction, hasFraction := strings.Cut(raw, ".")
|
||||
if whole == "" || strings.Contains(fraction, ".") ||
|
||||
!decimalDigits(whole) || (hasFraction && !decimalDigits(fraction)) ||
|
||||
len(fraction) > 2 {
|
||||
return nil, errInvalidProtocolInput
|
||||
}
|
||||
wholeValue, err := strconv.ParseUint(whole, 10, 64)
|
||||
if err != nil || wholeValue > uint64(math.MaxInt64)/100 {
|
||||
return nil, errInvalidProtocolInput
|
||||
}
|
||||
minor := wholeValue * 100
|
||||
if hasFraction {
|
||||
fractionValue, err := strconv.ParseUint(fraction, 10, 8)
|
||||
if err != nil {
|
||||
return nil, errInvalidProtocolInput
|
||||
}
|
||||
if len(fraction) == 1 {
|
||||
fractionValue *= 10
|
||||
}
|
||||
minor += fractionValue
|
||||
}
|
||||
if minor > uint64(math.MaxInt64) {
|
||||
return nil, errInvalidProtocolInput
|
||||
}
|
||||
result := int64(minor)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func decimalDigits(value string) bool {
|
||||
if value == "" {
|
||||
return false
|
||||
}
|
||||
for _, char := range value {
|
||||
if char < '0' || char > '9' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func externalID(value any) (string, error) {
|
||||
var normalized uint64
|
||||
switch typed := value.(type) {
|
||||
|
||||
Reference in New Issue
Block a user