@@ -0,0 +1,167 @@
|
||||
package vipcardexperimentser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"91porn-server/models/v/prdcthsomod"
|
||||
"91porn-server/models/v/productmod"
|
||||
"91porn-server/models/v/rchgordmod"
|
||||
"91porn-server/models/v/vipcardexperimentmod"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
type DisableRequest struct {
|
||||
ExperimentID string `json:"experimentId" binding:"required"`
|
||||
}
|
||||
|
||||
type StatisticsResponse struct {
|
||||
Experiment *vipcardexperimentmod.Experiment `json:"experiment"`
|
||||
Events []vipcardexperimentmod.EventStat `json:"events"`
|
||||
ProductImpressions []vipcardexperimentmod.ProductImpressionStat `json:"productImpressions"`
|
||||
Orders []rchgordmod.VIPExperimentOrderStat `json:"orders"`
|
||||
}
|
||||
|
||||
func Current(now time.Time) (*vipcardexperimentmod.Experiment, error) {
|
||||
return vipcardexperimentmod.Current(now)
|
||||
}
|
||||
|
||||
func Publish(experiment *vipcardexperimentmod.Experiment, operator string) error {
|
||||
if experiment == nil {
|
||||
return fmt.Errorf("experiment is required")
|
||||
}
|
||||
experiment.Normalize()
|
||||
if err := experiment.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
now := time.Now()
|
||||
if err := validatePublicationWindow(experiment, now); err != nil {
|
||||
return err
|
||||
}
|
||||
productIDs := uniqueProductIDs(experiment.VariantA.ProductIDs, experiment.VariantB.ProductIDs)
|
||||
products, err := productmod.FindByProductIDs(productIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
enabled := make(map[primitive.ObjectID]struct{}, len(products))
|
||||
for _, product := range products {
|
||||
if product.Status {
|
||||
enabled[product.ID] = struct{}{}
|
||||
}
|
||||
}
|
||||
for _, productID := range productIDs {
|
||||
if _, ok := enabled[productID]; !ok {
|
||||
return fmt.Errorf("product is missing or disabled: %s", productID.Hex())
|
||||
}
|
||||
}
|
||||
return vipcardexperimentmod.Publish(experiment, operator)
|
||||
}
|
||||
|
||||
func validatePublicationWindow(experiment *vipcardexperimentmod.Experiment, now time.Time) error {
|
||||
if experiment.StartAt != nil && experiment.StartAt.After(now.Add(time.Minute)) {
|
||||
return fmt.Errorf("startAt cannot be in the future; publish starts the experiment immediately")
|
||||
}
|
||||
if experiment.EndAt != nil && !experiment.EndAt.After(now) {
|
||||
return fmt.Errorf("endAt must be later than the current time")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Disable(experimentID, operator string) error {
|
||||
experimentID = strings.TrimSpace(experimentID)
|
||||
if experimentID == "" {
|
||||
return fmt.Errorf("experimentId is required")
|
||||
}
|
||||
updated, err := vipcardexperimentmod.Disable(experimentID, operator)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !updated {
|
||||
return fmt.Errorf("active experiment not found")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Statistics(experimentID string) (StatisticsResponse, error) {
|
||||
response := StatisticsResponse{
|
||||
Events: make([]vipcardexperimentmod.EventStat, 0),
|
||||
ProductImpressions: make([]vipcardexperimentmod.ProductImpressionStat, 0),
|
||||
Orders: make([]rchgordmod.VIPExperimentOrderStat, 0),
|
||||
}
|
||||
experimentID = strings.TrimSpace(experimentID)
|
||||
if experimentID == "" {
|
||||
return response, fmt.Errorf("experimentId is required")
|
||||
}
|
||||
experiment, err := vipcardexperimentmod.FindByExperimentID(experimentID)
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
if experiment == nil {
|
||||
return response, fmt.Errorf("experiment not found")
|
||||
}
|
||||
response.Experiment = experiment
|
||||
response.Events, response.ProductImpressions, err = vipcardexperimentmod.EventStatistics(experimentID)
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
response.Orders, err = rchgordmod.VIPExperimentOrderStatistics(experimentID)
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
goldOrders, err := prdcthsomod.VIPExperimentGoldOrderStatistics(experimentID)
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
response.Orders = mergeOrderStatistics(response.Orders, goldOrders)
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func mergeOrderStatistics(
|
||||
cashOrders []rchgordmod.VIPExperimentOrderStat,
|
||||
goldOrders []prdcthsomod.VIPExperimentGoldOrderStat,
|
||||
) []rchgordmod.VIPExperimentOrderStat {
|
||||
result := append(make([]rchgordmod.VIPExperimentOrderStat, 0, len(cashOrders)+len(goldOrders)), cashOrders...)
|
||||
type orderKey struct {
|
||||
variant string
|
||||
productID primitive.ObjectID
|
||||
}
|
||||
positions := make(map[orderKey]int, len(result)+len(goldOrders))
|
||||
for i := range result {
|
||||
positions[orderKey{variant: result[i].Variant, productID: result[i].ProductID}] = i
|
||||
}
|
||||
for _, goldOrder := range goldOrders {
|
||||
key := orderKey{variant: goldOrder.Variant, productID: goldOrder.ProductID}
|
||||
position, exists := positions[key]
|
||||
if !exists {
|
||||
position = len(result)
|
||||
positions[key] = position
|
||||
result = append(result, rchgordmod.VIPExperimentOrderStat{
|
||||
Variant: goldOrder.Variant,
|
||||
ProductID: goldOrder.ProductID,
|
||||
})
|
||||
}
|
||||
result[position].GoldPaidOrders += goldOrder.GoldPaidOrders
|
||||
result[position].GoldPaidAmount += goldOrder.GoldPaidAmount
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func uniqueProductIDs(groups ...[]primitive.ObjectID) []primitive.ObjectID {
|
||||
seen := make(map[primitive.ObjectID]struct{})
|
||||
result := make([]primitive.ObjectID, 0)
|
||||
for _, ids := range groups {
|
||||
for _, id := range ids {
|
||||
if id.IsZero() {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[id]; ok {
|
||||
continue
|
||||
}
|
||||
seen[id] = struct{}{}
|
||||
result = append(result, id)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package vipcardexperimentser
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"91porn-server/models/v/prdcthsomod"
|
||||
"91porn-server/models/v/rchgordmod"
|
||||
"91porn-server/models/v/vipcardexperimentmod"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
func TestValidatePublicationWindow(t *testing.T) {
|
||||
now := time.Now()
|
||||
future := now.Add(2 * time.Minute)
|
||||
if err := validatePublicationWindow(
|
||||
&vipcardexperimentmod.Experiment{StartAt: &future},
|
||||
now,
|
||||
); err == nil {
|
||||
t.Fatal("expected future startAt to fail")
|
||||
}
|
||||
|
||||
past := now.Add(-time.Minute)
|
||||
if err := validatePublicationWindow(
|
||||
&vipcardexperimentmod.Experiment{EndAt: &past},
|
||||
now,
|
||||
); err == nil {
|
||||
t.Fatal("expected expired endAt to fail")
|
||||
}
|
||||
|
||||
end := now.Add(time.Hour)
|
||||
if err := validatePublicationWindow(
|
||||
&vipcardexperimentmod.Experiment{EndAt: &end},
|
||||
now,
|
||||
); err != nil {
|
||||
t.Fatalf("valid publication window failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeOrderStatisticsAddsGoldPurchases(t *testing.T) {
|
||||
sharedProductID := primitive.NewObjectID()
|
||||
goldOnlyProductID := primitive.NewObjectID()
|
||||
result := mergeOrderStatistics(
|
||||
[]rchgordmod.VIPExperimentOrderStat{{
|
||||
Variant: vipcardexperimentmod.VariantA,
|
||||
ProductID: sharedProductID,
|
||||
CreatedOrders: 2,
|
||||
PaidOrders: 1,
|
||||
PaidAmount: 1000,
|
||||
}},
|
||||
[]prdcthsomod.VIPExperimentGoldOrderStat{
|
||||
{
|
||||
Variant: vipcardexperimentmod.VariantA,
|
||||
ProductID: sharedProductID,
|
||||
GoldPaidOrders: 3,
|
||||
GoldPaidAmount: 600,
|
||||
},
|
||||
{
|
||||
Variant: vipcardexperimentmod.VariantB,
|
||||
ProductID: goldOnlyProductID,
|
||||
GoldPaidOrders: 1,
|
||||
GoldPaidAmount: 200,
|
||||
},
|
||||
},
|
||||
)
|
||||
if len(result) != 2 {
|
||||
t.Fatalf("merged order count = %d, want 2", len(result))
|
||||
}
|
||||
if result[0].PaidOrders != 1 || result[0].PaidAmount != 1000 ||
|
||||
result[0].GoldPaidOrders != 3 || result[0].GoldPaidAmount != 600 {
|
||||
t.Fatalf("merged shared product stats = %#v", result[0])
|
||||
}
|
||||
if result[1].Variant != vipcardexperimentmod.VariantB ||
|
||||
result[1].ProductID != goldOnlyProductID ||
|
||||
result[1].GoldPaidOrders != 1 || result[1].GoldPaidAmount != 200 {
|
||||
t.Fatalf("gold-only product stats = %#v", result[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublishRejectsInvalidUIConfigBeforeDatabaseAccess(t *testing.T) {
|
||||
productID := primitive.NewObjectID()
|
||||
experiment := &vipcardexperimentmod.Experiment{
|
||||
ExperimentID: "invalid-ui-config",
|
||||
TrafficA: 50,
|
||||
TrafficB: 50,
|
||||
VariantA: vipcardexperimentmod.VariantConfig{
|
||||
DefaultProductID: productID,
|
||||
ProductIDs: []primitive.ObjectID{productID},
|
||||
UIConfig: &vipcardexperimentmod.UIConfig{
|
||||
BadgeStyles: []vipcardexperimentmod.BadgeStyle{{
|
||||
BadgeType: vipcardexperimentmod.BadgeMostPopular,
|
||||
BackgroundColor: "#FFF",
|
||||
TextColor: "#FFFFFF",
|
||||
}},
|
||||
},
|
||||
},
|
||||
VariantB: vipcardexperimentmod.VariantConfig{
|
||||
DefaultProductID: productID,
|
||||
ProductIDs: []primitive.ObjectID{productID},
|
||||
},
|
||||
}
|
||||
err := Publish(experiment, "tester")
|
||||
if err == nil || !strings.Contains(err.Error(), "variantA.uiConfig") {
|
||||
t.Fatalf("Publish() error = %v, want variantA.uiConfig error", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user