package resource import ( "crypto/sha256" "encoding/hex" "encoding/json" "sort" "strings" ) type ResourceGroupItem struct { GroupItemID int64 GroupID int64 ItemType string ResourceID int64 Resource Resource WalletAssetType string WalletAssetAmount int64 Quantity int64 DurationMS int64 SortOrder int32 CreatedAtMS int64 UpdatedAtMS int64 // GiftConfigs is populated only for immutable group snapshots. omitempty preserves the canonical JSON // of snapshots written before gift semantics became part of the wallet-owned reward promise. GiftConfigs []GiftConfig `json:",omitempty"` } type ResourceGroup struct { AppCode string GroupID int64 GroupCode string Name string Status string Description string SortOrder int32 Items []ResourceGroupItem CreatedByUserID int64 UpdatedByUserID int64 CreatedAtMS int64 UpdatedAtMS int64 } // ResourceGroupSnapshot is an immutable, owner-controlled reward promise. The embedded group contains // complete resource and wallet-asset facts needed both for deterministic grant execution and client display. type ResourceGroupSnapshot struct { AppCode string SnapshotID string PinKey string SourceGroupID int64 VersionNo int64 SnapshotHash string Group ResourceGroup CreatedByUserID int64 CreatedAtMS int64 SourceGroupUpdatedAtMS int64 SourceContentHash string } type PinResourceGroupSnapshotCommand struct { AppCode string PinKey string GroupID int64 OperatorUserID int64 ExpectedGroupUpdatedAtMS int64 ExpectedSourceContentHash string RequiredRegionIDs []int64 RequiredAllRegions bool } // CanonicalResourceGroupJSON is the owner-side version identity used by both inspect and pin. // It deliberately includes nested Resource bytes so an independently edited resource cannot reuse a stale pin. func CanonicalResourceGroupJSON(group ResourceGroup) ([]byte, error) { canonical := group canonical.Items = append([]ResourceGroupItem(nil), group.Items...) for index := range canonical.Items { canonical.Items[index].Resource.UsageScopes = append([]string(nil), canonical.Items[index].Resource.UsageScopes...) sort.Strings(canonical.Items[index].Resource.UsageScopes) canonical.Items[index].GiftConfigs = canonicalGiftConfigs(canonical.Items[index].GiftConfigs) } sort.SliceStable(canonical.Items, func(left, right int) bool { if canonical.Items[left].SortOrder != canonical.Items[right].SortOrder { return canonical.Items[left].SortOrder < canonical.Items[right].SortOrder } return canonical.Items[left].GroupItemID < canonical.Items[right].GroupItemID }) return json.Marshal(canonical) } func ResourceGroupContentHash(group ResourceGroup) (string, error) { body, err := CanonicalResourceGroupJSON(group) if err != nil { return "", err } sum := sha256.Sum256(body) return hex.EncodeToString(sum[:]), nil } type ResourceGroupItemInput struct { ItemType string ResourceID int64 WalletAssetType string WalletAssetAmount int64 Quantity int64 DurationMS int64 SortOrder int32 } type ListResourceGroupsQuery struct { AppCode string Status string Keyword string Page int32 PageSize int32 ActiveOnly bool } type ResourceGroupCommand struct { AppCode string GroupID int64 GroupCode string Name string Status string Description string SortOrder int32 Items []ResourceGroupItemInput OperatorUserID int64 } func ValidGrantStrategy(value string) bool { switch strings.ToLower(strings.TrimSpace(value)) { case GrantStrategyWalletCredit, GrantStrategyNewEntitlement, GrantStrategyExtendExpiry, GrantStrategyIncreaseQuantity, GrantStrategySetActiveFlag: return true default: return false } } func NormalizeGroupItemType(value string) string { value = strings.ToLower(strings.TrimSpace(value)) if value == "" { return GroupItemTypeResource } return value } func ValidGroupItemType(value string) bool { switch NormalizeGroupItemType(value) { case GroupItemTypeResource, GroupItemTypeWalletAsset: return true default: return false } }