// file: model/value.go package model import ( "time" "github.com/mitchellh/mapstructure" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" "github.com/tech/sendico/pkg/merrors" ) // ---------------------------- // Assignment model (domain) // ---------------------------- type Value struct { PermissionBound `bson:",inline" json:",inline"` Target ObjectRef `bson:"target" json:"target"` Type PropertyType `bson:"type" json:"type"` Cardinality Cardinality `bson:"cardinality" json:"cardinality"` PropertySchemaRef primitive.ObjectID `bson:"propertySchemaRef" json:"propertySchemaRef"` // Small typed shape via keys like: "string"/"strings", "integer"/"integers", etc. Values SettingsT `bson:"data" json:"data" yaml:"data"` } type Money struct { Amount primitive.Decimal128 `bson:"amount" json:"amount"` Currency Currency `bson:"currency" json:"currency"` } type Object = map[string]Value // ---------------------------- // SINGLE getters // ---------------------------- func (v *Value) AsString() (string, error) { if v.Type != PTString { return "", invalidType(PTString, v.Type) } if v.Cardinality != One { return "", merrors.InvalidArgument("invalid cardinality: expected one") } type payload struct { Value string `mapstructure:"string" bson:"string" json:"string" yaml:"string"` } var p payload if err := mapstructure.Decode(v.Values, &p); err != nil { return "", err } return p.Value, nil } func (v *Value) AsColor() (string, error) { if v.Type != PTColor { return "", invalidType(PTColor, v.Type) } if v.Cardinality != One { return "", merrors.InvalidArgument("invalid cardinality: expected one") } type payload struct { Value string `mapstructure:"color" bson:"color" json:"color" yaml:"color"` } var p payload if err := mapstructure.Decode(v.Values, &p); err != nil { return "", err } return p.Value, nil } func (v *Value) AsInteger() (int64, error) { if v.Type != PTInteger { return 0, invalidType(PTInteger, v.Type) } if v.Cardinality != One { return 0, merrors.InvalidArgument("invalid cardinality: expected one") } type payload struct { Value int64 `mapstructure:"integer" bson:"integer" json:"integer" yaml:"integer"` } var p payload if err := mapstructure.Decode(v.Values, &p); err != nil { return 0, err } return p.Value, nil } func (v *Value) AsFloat() (float64, error) { if v.Type != PTFloat { return 0, invalidType(PTFloat, v.Type) } if v.Cardinality != One { return 0, merrors.InvalidArgument("invalid cardinality: expected one") } type payload struct { Value float64 `mapstructure:"float" bson:"float" json:"float" yaml:"float"` } var p payload if err := mapstructure.Decode(v.Values, &p); err != nil { return 0, err } return p.Value, nil } func (v *Value) AsDateTime() (time.Time, error) { if v.Type != PTDateTime { return time.Time{}, invalidType(PTDateTime, v.Type) } if v.Cardinality != One { return time.Time{}, merrors.InvalidArgument("invalid cardinality: expected one") } type payload struct { Value time.Time `mapstructure:"date_time" bson:"date_time" json:"date_time" yaml:"date_time"` } var p payload if err := mapstructure.Decode(v.Values, &p); err != nil { return time.Time{}, err } return p.Value, nil } func (v *Value) AsMonetary() (Money, error) { if v.Type != PTMonetary { return Money{}, invalidType(PTMonetary, v.Type) } if v.Cardinality != One { return Money{}, merrors.InvalidArgument("invalid cardinality: expected one") } type payload struct { Value Money `mapstructure:"monetary" bson:"monetary" json:"monetary" yaml:"monetary"` } var p payload if err := mapstructure.Decode(v.Values, &p); err != nil { return Money{}, err } return p.Value, nil } func (v *Value) AsReference() (primitive.ObjectID, error) { if v.Type != PTReference { return primitive.NilObjectID, invalidType(PTReference, v.Type) } if v.Cardinality != One { return primitive.NilObjectID, merrors.InvalidArgument("invalid cardinality: expected one") } type payload struct { Value primitive.ObjectID `mapstructure:"reference" bson:"reference" json:"reference" yaml:"reference"` } var p payload if err := mapstructure.Decode(v.Values, &p); err != nil { return primitive.NilObjectID, err } return p.Value, nil } func (v *Value) AsObject() (Object, error) { if v.Type != PTObject { return nil, invalidType(PTObject, v.Type) } if v.Cardinality != One { return nil, merrors.InvalidArgument("invalid cardinality: expected one") } type payload struct { Value Object `mapstructure:"object" bson:"object" json:"object" yaml:"object"` } var p payload if err := mapstructure.Decode(v.Values, &p); err != nil { return nil, err } return p.Value, nil } // ---------------------------- // ARRAY getters // ---------------------------- func (v *Value) AsStrings() ([]string, error) { if v.Type != PTString { return nil, invalidType(PTString, v.Type) } if v.Cardinality != Many { return nil, merrors.InvalidArgument("invalid cardinality: expected many") } type payload struct { Values []string `mapstructure:"strings" bson:"strings" json:"strings" yaml:"strings"` } var p payload if err := mapstructure.Decode(v.Values, &p); err != nil { return nil, err } return p.Values, nil } func (v *Value) AsColors() ([]string, error) { if v.Type != PTColor { return nil, invalidType(PTColor, v.Type) } if v.Cardinality != Many { return nil, merrors.InvalidArgument("invalid cardinality: expected many") } type payload struct { Values []string `mapstructure:"colors" bson:"colors" json:"colors" yaml:"colors"` } var p payload if err := mapstructure.Decode(v.Values, &p); err != nil { return nil, err } return p.Values, nil } func (v *Value) AsIntegers() ([]int64, error) { if v.Type != PTInteger { return nil, invalidType(PTInteger, v.Type) } if v.Cardinality != Many { return nil, merrors.InvalidArgument("invalid cardinality: expected many") } type payload struct { Values []int64 `mapstructure:"integers" bson:"integers" json:"integers" yaml:"integers"` } var p payload if err := mapstructure.Decode(v.Values, &p); err != nil { return nil, err } return p.Values, nil } func (v *Value) AsFloats() ([]float64, error) { if v.Type != PTFloat { return nil, invalidType(PTFloat, v.Type) } if v.Cardinality != Many { return nil, merrors.InvalidArgument("invalid cardinality: expected many") } type payload struct { Values []float64 `mapstructure:"floats" bson:"floats" json:"floats" yaml:"floats"` } var p payload if err := mapstructure.Decode(v.Values, &p); err != nil { return nil, err } return p.Values, nil } func (v *Value) AsDateTimes() ([]time.Time, error) { if v.Type != PTDateTime { return nil, invalidType(PTDateTime, v.Type) } if v.Cardinality != Many { return nil, merrors.InvalidArgument("invalid cardinality: expected many") } type payload struct { Values []time.Time `mapstructure:"date_times" bson:"date_times" json:"date_times" yaml:"date_times"` } var p payload if err := mapstructure.Decode(v.Values, &p); err != nil { return nil, err } return p.Values, nil } func (v *Value) AsMonetaries() ([]Money, error) { if v.Type != PTMonetary { return nil, invalidType(PTMonetary, v.Type) } if v.Cardinality != Many { return nil, merrors.InvalidArgument("invalid cardinality: expected many") } type payload struct { Values []Money `mapstructure:"monetaries" bson:"monetaries" json:"monetaries" yaml:"monetaries"` } var p payload if err := mapstructure.Decode(v.Values, &p); err != nil { return nil, err } return p.Values, nil } func (v *Value) AsReferences() ([]primitive.ObjectID, error) { if v.Type != PTReference { return nil, invalidType(PTReference, v.Type) } if v.Cardinality != Many { return nil, merrors.InvalidArgument("invalid cardinality: expected many") } type payload struct { Values []primitive.ObjectID `mapstructure:"references" bson:"references" json:"references" yaml:"references"` } var p payload if err := mapstructure.Decode(v.Values, &p); err != nil { return nil, err } return p.Values, nil } func (v *Value) AsObjects() ([]Object, error) { if v.Type != PTObject { return nil, invalidType(PTObject, v.Type) } if v.Cardinality != Many { return nil, merrors.InvalidArgument("invalid cardinality: expected many") } type payload struct { Values []Object `mapstructure:"objects" bson:"objects" json:"objects" yaml:"objects"` } var p payload if err := mapstructure.Decode(v.Values, &p); err != nil { return nil, err } return p.Values, nil } // ---------------------------- // FACTORIES (scheme + value) // ---------------------------- // Strings func NewStringValue(scope PermissionBound, target ObjectRef, scheme PropertySchema, v string) (Value, error) { if scheme.Type != PTString { return Value{}, invalidType(PTString, scheme.Type) } if err := scheme.ValidateStrings([]string{v}); err != nil { return Value{}, err } return Value{ PermissionBound: scope, Target: target, Type: PTString, Cardinality: One, PropertySchemaRef: scheme.ID, Values: SettingsT{VKString: v}, }, nil } func NewStringsValue(scope PermissionBound, target ObjectRef, scheme PropertySchema, vv []string) (Value, error) { if scheme.Type != PTString { return Value{}, invalidType(PTString, scheme.Type) } if err := scheme.ValidateStrings(vv); err != nil { return Value{}, err } return Value{ PermissionBound: scope, Target: target, Type: PTString, Cardinality: Many, PropertySchemaRef: scheme.ID, Values: SettingsT{VKStrings: vv}, }, nil } // Colors func NewColorValue(scope PermissionBound, target ObjectRef, scheme PropertySchema, v string) (Value, error) { if scheme.Type != PTColor { return Value{}, invalidType(PTColor, scheme.Type) } if err := scheme.ValidateColors([]string{v}); err != nil { return Value{}, err } return Value{scope, target, PTColor, One, scheme.ID, SettingsT{VKColor: v}}, nil } func NewColorsValue(scope PermissionBound, target ObjectRef, scheme PropertySchema, vv []string) (Value, error) { if scheme.Type != PTColor { return Value{}, invalidType(PTColor, scheme.Type) } if err := scheme.ValidateColors(vv); err != nil { return Value{}, err } return Value{scope, target, PTColor, Many, scheme.ID, SettingsT{VKColors: vv}}, nil } // Integers func NewIntegerValue(scope PermissionBound, target ObjectRef, scheme PropertySchema, v int64) (Value, error) { if scheme.Type != PTInteger { return Value{}, invalidType(PTInteger, scheme.Type) } if err := scheme.ValidateIntegers([]int64{v}); err != nil { return Value{}, err } return Value{scope, target, PTInteger, One, scheme.ID, SettingsT{VKInteger: v}}, nil } func NewIntegersValue(scope PermissionBound, target ObjectRef, scheme PropertySchema, vv []int64) (Value, error) { if scheme.Type != PTInteger { return Value{}, invalidType(PTInteger, scheme.Type) } if err := scheme.ValidateIntegers(vv); err != nil { return Value{}, err } return Value{scope, target, PTInteger, Many, scheme.ID, SettingsT{VKIntegers: vv}}, nil } // Floats func NewFloatValue(scope PermissionBound, target ObjectRef, scheme PropertySchema, v float64) (Value, error) { if scheme.Type != PTFloat { return Value{}, invalidType(PTFloat, scheme.Type) } if err := scheme.ValidateFloats([]float64{v}); err != nil { return Value{}, err } return Value{scope, target, PTFloat, One, scheme.ID, SettingsT{VKFloat: v}}, nil } func NewFloatsValue(scope PermissionBound, target ObjectRef, scheme PropertySchema, vv []float64) (Value, error) { if scheme.Type != PTFloat { return Value{}, invalidType(PTFloat, scheme.Type) } if err := scheme.ValidateFloats(vv); err != nil { return Value{}, err } return Value{scope, target, PTFloat, Many, scheme.ID, SettingsT{VKFloats: vv}}, nil } // DateTimes func NewDateTimeValue(scope PermissionBound, target ObjectRef, scheme PropertySchema, v time.Time) (Value, error) { if scheme.Type != PTDateTime { return Value{}, invalidType(PTDateTime, scheme.Type) } if err := scheme.ValidateDateTimes([]time.Time{v}); err != nil { return Value{}, err } return Value{scope, target, PTDateTime, One, scheme.ID, SettingsT{VKDateTime: v}}, nil } func NewDateTimesValue(scope PermissionBound, target ObjectRef, scheme PropertySchema, vv []time.Time) (Value, error) { if scheme.Type != PTDateTime { return Value{}, invalidType(PTDateTime, scheme.Type) } if err := scheme.ValidateDateTimes(vv); err != nil { return Value{}, err } return Value{scope, target, PTDateTime, Many, scheme.ID, SettingsT{VKDateTimes: vv}}, nil } // Monetary (needs org currency for validation if required by scheme) func NewMonetaryValue(scope PermissionBound, target ObjectRef, scheme PropertySchema, v Money, orgCurrency Currency) (Value, error) { if scheme.Type != PTMonetary { return Value{}, invalidType(PTMonetary, scheme.Type) } if err := scheme.ValidateMonetaries([]Money{v}, orgCurrency); err != nil { return Value{}, err } return Value{scope, target, PTMonetary, One, scheme.ID, SettingsT{VKMonetary: v}}, nil } func NewMonetariesValue(scope PermissionBound, target ObjectRef, scheme PropertySchema, vv []Money, orgCurrency Currency) (Value, error) { if scheme.Type != PTMonetary { return Value{}, invalidType(PTMonetary, scheme.Type) } if err := scheme.ValidateMonetaries(vv, orgCurrency); err != nil { return Value{}, err } return Value{scope, target, PTMonetary, Many, scheme.ID, SettingsT{VKMonetaries: vv}}, nil } // References (needs exist-fn) func NewReferenceValue(scope PermissionBound, target ObjectRef, scheme PropertySchema, v primitive.ObjectID, exist ExistFn) (Value, error) { if scheme.Type != PTReference { return Value{}, invalidType(PTReference, scheme.Type) } if err := scheme.ValidateReferences([]primitive.ObjectID{v}, exist); err != nil { return Value{}, err } return Value{scope, target, PTReference, One, scheme.ID, SettingsT{VKReference: v}}, nil } func NewReferencesValue(scope PermissionBound, target ObjectRef, scheme PropertySchema, vv []primitive.ObjectID, exist ExistFn) (Value, error) { if scheme.Type != PTReference { return Value{}, invalidType(PTReference, scheme.Type) } if err := scheme.ValidateReferences(vv, exist); err != nil { return Value{}, err } return Value{scope, target, PTReference, Many, scheme.ID, SettingsT{VKReferences: vv}}, nil } // Objects (opaque maps) func NewObjectValue(scope PermissionBound, target ObjectRef, scheme PropertySchema, v Object) (Value, error) { if scheme.Type != PTObject { return Value{}, invalidType(PTObject, scheme.Type) } // Add your own ValidateObject if needed return Value{scope, target, PTObject, One, scheme.ID, SettingsT{VKObject: v}}, nil } func NewObjectsValue(scope PermissionBound, target ObjectRef, scheme PropertySchema, vv []Object) (Value, error) { if scheme.Type != PTObject { return Value{}, invalidType(PTObject, scheme.Type) } return Value{scope, target, PTObject, Many, scheme.ID, SettingsT{VKObjects: vv}}, nil } // ---------------------------- // Custom BSON Marshalers/Unmarshalers // ---------------------------- // MarshalBSON implements bson.Marshaler to ensure proper serialization func (v Value) MarshalBSON() ([]byte, error) { // Create a temporary struct that preserves the exact structure temp := struct { PermissionBound `bson:",inline"` Target ObjectRef `bson:"target"` Type PropertyType `bson:"type"` Cardinality Cardinality `bson:"cardinality"` PropertySchemaRef primitive.ObjectID `bson:"propertySchemaRef"` Values SettingsTWrapper `bson:"data"` }{ PermissionBound: v.PermissionBound, Target: v.Target, Type: v.Type, Cardinality: v.Cardinality, PropertySchemaRef: v.PropertySchemaRef, Values: SettingsTWrapper(v.Values), } return bson.Marshal(temp) } // UnmarshalBSON implements bson.Unmarshaler to ensure proper deserialization func (v *Value) UnmarshalBSON(data []byte) error { // Create a temporary struct that matches the BSON structure temp := struct { PermissionBound `bson:",inline"` Target ObjectRef `bson:"target"` Type PropertyType `bson:"type"` Cardinality Cardinality `bson:"cardinality"` PropertySchemaRef primitive.ObjectID `bson:"propertySchemaRef"` Values SettingsTWrapper `bson:"data"` }{} if err := bson.Unmarshal(data, &temp); err != nil { return err } // Copy the values back to the original struct v.PermissionBound = temp.PermissionBound v.Target = temp.Target v.Type = temp.Type v.Cardinality = temp.Cardinality v.PropertySchemaRef = temp.PropertySchemaRef v.Values = SettingsT(temp.Values) return nil } // ---------------------------- // Custom BSON Marshalers for SettingsT // ---------------------------- // SettingsT is a type alias, so we need to define a wrapper type for methods type SettingsTWrapper SettingsT // MarshalBSON implements bson.Marshaler for SettingsT to preserve exact types func (s SettingsTWrapper) MarshalBSON() ([]byte, error) { // Convert SettingsT to bson.M to preserve exact types doc := bson.M{} for key, value := range s { doc[key] = value } return bson.Marshal(doc) } // UnmarshalBSON implements bson.Unmarshaler for SettingsT to preserve exact types func (s *SettingsTWrapper) UnmarshalBSON(data []byte) error { // Unmarshal into a generic map first var doc bson.M if err := bson.Unmarshal(data, &doc); err != nil { return err } // Convert back to SettingsT, preserving types *s = make(SettingsT) for key, value := range doc { // Handle special cases where BSON converts types switch v := value.(type) { case primitive.A: // Convert primitive.A back to appropriate slice type if len(v) > 0 { switch v[0].(type) { case string: strings := make([]string, len(v)) for i, item := range v { strings[i] = item.(string) } (*s)[key] = strings case int32, int64: ints := make([]int64, len(v)) for i, item := range v { switch val := item.(type) { case int32: ints[i] = int64(val) case int64: ints[i] = val } } (*s)[key] = ints case float32, float64: floats := make([]float64, len(v)) for i, item := range v { switch val := item.(type) { case float32: floats[i] = float64(val) case float64: floats[i] = val } } (*s)[key] = floats case primitive.DateTime: times := make([]time.Time, len(v)) for i, item := range v { times[i] = item.(primitive.DateTime).Time().Truncate(time.Millisecond) } (*s)[key] = times case primitive.ObjectID: refs := make([]primitive.ObjectID, len(v)) for i, item := range v { refs[i] = item.(primitive.ObjectID) } (*s)[key] = refs case bson.M: // Handle nested objects (Money, Object, etc.) if key == VKMonetaries { // Handle Money slice moneys := make([]Money, len(v)) for i, item := range v { if itemMap, ok := item.(bson.M); ok { var money Money if amount, ok := itemMap[MKAmount].(primitive.Decimal128); ok { money.Amount = amount } if currency, ok := itemMap[MKCurrency].(string); ok { money.Currency = Currency(currency) } moneys[i] = money } } (*s)[key] = moneys } else { // Handle Object slice objects := make([]Object, len(v)) for i, item := range v { obj := make(Object) for k, val := range item.(bson.M) { // Recursively handle nested Values if valMap, ok := val.(bson.M); ok { var nestedValue Value if data, err := bson.Marshal(valMap); err == nil { if err := bson.Unmarshal(data, &nestedValue); err == nil { obj[k] = nestedValue } } } } objects[i] = obj } (*s)[key] = objects } default: // Fallback: keep as primitive.A (*s)[key] = v } } else { // Empty array - determine type from key name switch key { case VKStrings, VKColors: (*s)[key] = []string{} case VKIntegers: (*s)[key] = []int64{} case VKFloats: (*s)[key] = []float64{} case VKDateTimes: (*s)[key] = []time.Time{} case VKReferences: (*s)[key] = []primitive.ObjectID{} case VKMonetaries: (*s)[key] = []Money{} case VKObjects: (*s)[key] = []Object{} default: (*s)[key] = []interface{}{} } } case primitive.DateTime: // Convert primitive.DateTime back to time.Time and truncate to millisecond precision (*s)[key] = v.Time().Truncate(time.Millisecond) case int64: // Handle time.Time that gets converted to int64 (Unix timestamp) if key == VKDateTime { (*s)[key] = time.Unix(v, 0).UTC().Truncate(time.Millisecond) } else { (*s)[key] = v } case bson.M: // Handle nested objects if key == VKMonetary { // Handle Money struct var money Money if amount, ok := v[MKAmount].(primitive.Decimal128); ok { money.Amount = amount } if currency, ok := v[MKCurrency].(string); ok { money.Currency = Currency(currency) } (*s)[key] = money } else if key == VKMonetaries { // Handle Money slice - this shouldn't happen in single values (*s)[key] = v } else if key == VKObject { // Handle Object type obj := make(Object) for k, val := range v { if valMap, ok := val.(bson.M); ok { var nestedValue Value if data, err := bson.Marshal(valMap); err == nil { if err := bson.Unmarshal(data, &nestedValue); err == nil { obj[k] = nestedValue } } } } (*s)[key] = obj } else { // Generic map (*s)[key] = v } case nil: // Handle nil values - determine type from key name switch key { case VKStrings, VKColors: (*s)[key] = []string(nil) case VKIntegers: (*s)[key] = []int64(nil) case VKFloats: (*s)[key] = []float64(nil) case VKDateTimes: (*s)[key] = []time.Time(nil) case VKReferences: (*s)[key] = []primitive.ObjectID(nil) case VKMonetaries: (*s)[key] = []Money(nil) case VKObjects: (*s)[key] = []Object(nil) default: (*s)[key] = nil } default: // Keep as-is for primitive types (*s)[key] = value } } return nil }