133 lines
4.2 KiB
Go
133 lines
4.2 KiB
Go
package shopify
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"encoding/json"
|
||
|
|
"strings"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
const (
|
||
|
|
defaultBatchSize = 25
|
||
|
|
defaultSyncLimit = 200
|
||
|
|
maxBatchSize = 100
|
||
|
|
maxSyncLimit = 1000
|
||
|
|
maxOrdersSyncLimit = 5000
|
||
|
|
maxProductIDMap = 5000
|
||
|
|
maxScheduleIntervalH = 168 // 7 days
|
||
|
|
)
|
||
|
|
|
||
|
|
// SyncOptions is stored as JSON on shopify_configs.sync_options.
|
||
|
|
type SyncOptions struct {
|
||
|
|
ProductIDs map[string]int64 `json:"product_ids"`
|
||
|
|
MatchStrategy string `json:"match_strategy"`
|
||
|
|
BatchSize int `json:"batch_size"`
|
||
|
|
SyncLimit int `json:"sync_limit"`
|
||
|
|
PendingSync bool `json:"pending_sync"`
|
||
|
|
PendingOrdersSync bool `json:"pending_orders_sync"`
|
||
|
|
LastSyncStatus string `json:"last_sync_status"`
|
||
|
|
LastSyncError string `json:"last_sync_error"`
|
||
|
|
LastSyncSummary *SyncSummary `json:"last_sync_summary,omitempty"`
|
||
|
|
LastOrdersSyncStatus string `json:"last_orders_sync_status"`
|
||
|
|
LastOrdersSyncError string `json:"last_orders_sync_error"`
|
||
|
|
LastOrdersSyncedAt *time.Time `json:"last_orders_synced_at,omitempty"`
|
||
|
|
OrdersSyncLimit int `json:"orders_sync_limit"`
|
||
|
|
DryRun bool `json:"dry_run"`
|
||
|
|
ScheduleIntervalHours int `json:"schedule_interval_hours"`
|
||
|
|
SchedulePaused bool `json:"schedule_paused"`
|
||
|
|
// One-shot selection for the next product sync (cleared when sync finishes).
|
||
|
|
SyncFilterStatus string `json:"sync_filter_status,omitempty"`
|
||
|
|
SyncFilterCategory string `json:"sync_filter_category,omitempty"`
|
||
|
|
SyncOnlyIDs []string `json:"sync_only_ids,omitempty"`
|
||
|
|
|
||
|
|
// Auth (Dev Dashboard client_credentials). Legacy permanent tokens leave these empty.
|
||
|
|
AuthMode string `json:"auth_mode,omitempty"`
|
||
|
|
ClientID string `json:"client_id,omitempty"`
|
||
|
|
ClientSecretEnc string `json:"client_secret_enc,omitempty"`
|
||
|
|
TokenExpiresAt *time.Time `json:"token_expires_at,omitempty"`
|
||
|
|
}
|
||
|
|
|
||
|
|
func parseSyncOptions(raw []byte) SyncOptions {
|
||
|
|
opt := SyncOptions{
|
||
|
|
ProductIDs: map[string]int64{},
|
||
|
|
MatchStrategy: "sku",
|
||
|
|
BatchSize: defaultBatchSize,
|
||
|
|
SyncLimit: defaultSyncLimit,
|
||
|
|
}
|
||
|
|
if len(raw) == 0 {
|
||
|
|
return opt
|
||
|
|
}
|
||
|
|
_ = json.Unmarshal(raw, &opt)
|
||
|
|
if opt.ProductIDs == nil {
|
||
|
|
opt.ProductIDs = map[string]int64{}
|
||
|
|
}
|
||
|
|
pruneStringInt64Map(opt.ProductIDs, maxProductIDMap)
|
||
|
|
if opt.MatchStrategy == "" {
|
||
|
|
opt.MatchStrategy = "sku"
|
||
|
|
}
|
||
|
|
if opt.BatchSize <= 0 || opt.BatchSize > maxBatchSize {
|
||
|
|
opt.BatchSize = defaultBatchSize
|
||
|
|
}
|
||
|
|
if opt.SyncLimit <= 0 || opt.SyncLimit > maxSyncLimit {
|
||
|
|
opt.SyncLimit = defaultSyncLimit
|
||
|
|
}
|
||
|
|
if opt.OrdersSyncLimit < 0 || opt.OrdersSyncLimit > maxOrdersSyncLimit {
|
||
|
|
opt.OrdersSyncLimit = 0
|
||
|
|
}
|
||
|
|
if opt.ScheduleIntervalHours < 0 || opt.ScheduleIntervalHours > maxScheduleIntervalH {
|
||
|
|
opt.ScheduleIntervalHours = 0
|
||
|
|
}
|
||
|
|
opt.SyncOnlyIDs = pruneSyncOnlyIDs(opt.SyncOnlyIDs, maxSyncOnlyIDs)
|
||
|
|
return opt
|
||
|
|
}
|
||
|
|
|
||
|
|
// resolveScheduleInterval maps schedule_interval_hours to a duration (default when hours<=0).
|
||
|
|
func resolveScheduleInterval(hours int, defaultInterval time.Duration) time.Duration {
|
||
|
|
if defaultInterval <= 0 {
|
||
|
|
defaultInterval = 6 * time.Hour
|
||
|
|
}
|
||
|
|
if hours > 0 {
|
||
|
|
return time.Duration(hours) * time.Hour
|
||
|
|
}
|
||
|
|
return defaultInterval
|
||
|
|
}
|
||
|
|
|
||
|
|
// isDueForSchedule reports whether last sync is missing or older than interval.
|
||
|
|
func isDueForSchedule(last *time.Time, now time.Time, interval time.Duration) bool {
|
||
|
|
if last == nil {
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
return now.Sub(last.UTC()) >= interval
|
||
|
|
}
|
||
|
|
|
||
|
|
// shouldEnqueueScheduled reports whether auto product sync should run now.
|
||
|
|
// Paused schedules never enqueue; interval 0 uses defaultInterval (worker default 6h).
|
||
|
|
func shouldEnqueueScheduled(paused bool, hours int, last *time.Time, now time.Time, defaultInterval time.Duration) bool {
|
||
|
|
if paused {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
return isDueForSchedule(last, now, resolveScheduleInterval(hours, defaultInterval))
|
||
|
|
}
|
||
|
|
|
||
|
|
func firstNonEmpty(vals ...string) string {
|
||
|
|
for _, v := range vals {
|
||
|
|
if strings.TrimSpace(v) != "" {
|
||
|
|
return strings.TrimSpace(v)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return ""
|
||
|
|
}
|
||
|
|
|
||
|
|
func pruneStringInt64Map(m map[string]int64, max int) {
|
||
|
|
if max <= 0 || len(m) <= max {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
n := len(m) - max
|
||
|
|
for k := range m {
|
||
|
|
delete(m, k)
|
||
|
|
n--
|
||
|
|
if n <= 0 {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|