Initial commit of Descrybe v2 without local scratch artifacts.

Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
This commit is contained in:
2026-08-09 22:47:43 +02:00
commit 8580c996c3
1285 changed files with 325780 additions and 0 deletions
+132
View File
@@ -0,0 +1,132 @@
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
}
}
}