package model import ( "fmt" "strings" "time" validation "github.com/docker/licensing/lib/go-validation" "github.com/docker/licensing/types" ) // PricingComponents represents a collection of pricing components type PricingComponents []*SubscriptionPricingComponent func (comps PricingComponents) Len() int { return len(comps) } func (comps PricingComponents) Swap(i, j int) { comps[i], comps[j] = comps[j], comps[i] } // always sorting by name func (comps PricingComponents) Less(i, j int) bool { return comps[i].Name < comps[j].Name } // Subscription includes the base fields that will be meaningful to most of the clients consuming this api type Subscription struct { Name string `json:"name"` ID string `json:"subscription_id"` DockerID string `json:"docker_id"` ProductID string `json:"product_id"` ProductRatePlan string `json:"product_rate_plan"` ProductRatePlanID string `json:"product_rate_plan_id"` Start *time.Time `json:"current_period_start,omitempty"` Expires *time.Time `json:"current_period_end,omitempty"` State string `json:"state"` Eusa *EusaState `json:"eusa,omitempty"` PricingComponents PricingComponents `json:"pricing_components"` } func (s *Subscription) String() string { storeURL := "https://docker.com/licensing" var nameMsg, expirationMsg, statusMsg string switch types.State(s.State) { case types.Cancelled: statusMsg = fmt.Sprintf("\tCancelled! You will no longer receive updates. To purchase go to %s", storeURL) expirationMsg = fmt.Sprintf("Expiration date: %s", s.Expires.Format("2006-01-02")) case types.Expired: statusMsg = fmt.Sprintf("\tExpired! You will no longer receive updates. Please renew at %s", storeURL) expirationMsg = fmt.Sprintf("Expiration date: %s", s.Expires.Format("2006-01-02")) case types.Preparing: statusMsg = "\tYour subscription has not yet begun" expirationMsg = fmt.Sprintf("Activation date: %s", s.Start.Format("2006-01-02")) case types.Failed: statusMsg = "\tOops, this subscription did not get setup properly!" expirationMsg = "" case types.Active: statusMsg = "\tLicense is currently active" expirationMsg = fmt.Sprintf("Expiration date: %s", s.Expires.Format("2006-01-02")) default: expirationMsg = fmt.Sprintf("Expiration date: %s", s.Expires.Format("2006-01-02")) } pcStrs := make([]string, len(s.PricingComponents)) for i, pc := range s.PricingComponents { pcStrs[i] = fmt.Sprintf("%d %s", pc.Value, pc.Name) } quantityMsg := "Quantity: " + strings.Join(pcStrs, ", ") if s.Name != "" { nameMsg = fmt.Sprintf("License Name: %s\t", s.Name) } else if s.ProductRatePlan == "free-trial" { // TODO - consider a humanized formatting for expiration time on trials (e.g., "10 days remaining") nameMsg = "Free trial\t" statusMsg = fmt.Sprintf("\tTo purchase go to %s", storeURL) } return fmt.Sprintf("%s%s\t%s%s", nameMsg, quantityMsg, expirationMsg, statusMsg) } // SubscriptionDetail presents Subscription information to billing service clients. type SubscriptionDetail struct { Subscription Origin string `json:"origin,omitempty"` OrderID string `json:"order_id,omitempty"` OrderItemID string `json:"order_item_id,omitempty"` InitialPeriodStart time.Time `json:"initial_period_start"` CreatedByID string `json:"created_by_docker_id"` // If true, the product for this subscription uses product keys. To // obtain the keys, the frontend or billing client will need to // make additional calls to the fulfillment service. UsesProductKeys bool `json:"uses_product_keys,omitempty"` // If non-empty, this is a managed subscription, and this identifier is // known to the fulfillment service as a means to uniquely identify the // partner that manages this subscription. // // Different permissions checking will be used to authorize changes and // cancellation; the entity entitled to this subscription (represented // by DockerID) may not change or cancel it directly. ManagingPartnerID string `json:"managing_partner_id,omitempty"` // If non-empty, this is a managed subscription, and this ID belongs to the // account of a user within a partner's account system. PartnerAccountID string `json:"partner_account_id,omitempty"` // Marketing opt-in for the subscription. This means customer agrees to receive additional marketing emails MarketingOptIn bool `json:"marketing_opt_in"` } // SubscriptionPricingComponent captures pricing component values that have been selected by the user. type SubscriptionPricingComponent struct { Name string `json:"name"` Value int `json:"value"` } // SubscriptionCreationRequest represents a subscription creation request type SubscriptionCreationRequest struct { Name string `json:"name"` DockerID string `json:"docker_id"` ProductID string `json:"product_id"` ProductRatePlan string `json:"product_rate_plan"` Eusa *EusaState `json:"eusa,omitempty"` Origin string `json:"origin,omitempty"` OrderID string `json:"order_id,omitempty"` OrderItemID string `json:"order_item_id,omitempty"` End *time.Time `json:"end,omitempty"` Start *time.Time `json:"start,omitempty"` CouponCodes []string `json:"coupon_codes"` PricingComponents PricingComponents `json:"pricing_components"` // If true, the product for this subscription uses product keys. To // obtain the keys, the frontend or billing client will need to // make additional calls to the fulfillment service. UsesProductKeys bool `json:"uses_product_keys,omitempty"` // Should be non-empty only if creating a managed subscription that will // be controlled by a partner or publisher. This identifier matches // whatever the fulfillment service uses as guid's for partners. ManagingPartnerID string `json:"managing_partner_id,omitempty"` // Should be non-empty only if creating a managed subscription on behalf // of a partner, and this ID represent's a partner's user's account id. PartnerAccountID string `json:"partner_account_id,omitempty"` // Marketing opt-in for the subscription. This means customer agrees to receive additional marketing emails MarketingOptIn bool `json:"marketing_opt_in"` } // Validate returns true if the subscription request is valid, false otherwise. // If invalid, one or more validation Errors will be returned. func (s *SubscriptionCreationRequest) Validate() (bool, validation.Errors) { var errs validation.Errors if validation.IsEmpty(s.Name) { errs = append(errs, validation.InvalidEmpty("name")) } if validation.IsEmpty(s.DockerID) { errs = append(errs, validation.InvalidEmpty("docker_id")) } if validation.IsEmpty(s.ProductID) { errs = append(errs, validation.InvalidEmpty("product_id")) } if validation.IsEmpty(s.ProductRatePlan) { errs = append(errs, validation.InvalidEmpty("product_rate_plan")) } for i, component := range s.PricingComponents { if validation.IsEmpty(component.Name) { name := fmt.Sprintf("pricing_component[%v]/name", i) errs = append(errs, validation.InvalidEmpty(name)) } } valid := len(errs) == 0 return valid, errs } // EusaState encodes whether the subscription's EUSA has been accepted, // and if so, by whom and when. // See json marshal & unmarshal below. type EusaState struct { Accepted bool `json:"accepted"` AcceptedBy string `json:"accepted_by,omitempty"` AcceptedOn string `json:"accepted_on,omitempty"` }