File size: 2,884 Bytes
16cdcb7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
package patch

import (
	"fmt"

	"github.com/openmeterio/openmeter/openmeter/subscription"
	"github.com/openmeterio/openmeter/pkg/datetime"
)

type PatchStretchPhase struct {
	PhaseKey string
	// Signed duration
	Duration datetime.ISODuration
}

func (p PatchStretchPhase) Op() subscription.PatchOperation {
	return subscription.PatchOperationStretch
}

func (p PatchStretchPhase) Path() subscription.SpecPath {
	return subscription.NewPhasePath(p.PhaseKey)
}

func (p PatchStretchPhase) Value() datetime.ISODuration {
	return p.Duration
}

func (p PatchStretchPhase) ValueAsAny() any {
	return p.Duration
}

func (p PatchStretchPhase) Validate() error {
	if err := p.Path().Validate(); err != nil {
		return err
	}

	if err := p.Op().Validate(); err != nil {
		return err
	}

	if p.Duration.IsZero() {
		return fmt.Errorf("duration cannot be zero")
	}

	return nil
}

var _ subscription.ValuePatch[datetime.ISODuration] = PatchStretchPhase{}

func (p PatchStretchPhase) ApplyTo(spec *subscription.SubscriptionSpec, actx subscription.ApplyContext) error {
	phase, ok := spec.Phases[p.PhaseKey]
	if !ok {
		return fmt.Errorf("phase %s not found", p.PhaseKey)
	}

	sortedPhases := spec.GetSortedPhases()

	// Checks we need:
	pST, _ := phase.StartAfter.AddTo(spec.ActiveFrom)
	// 2. You cannot extend past phases, only current or future ones
	current, exists := spec.GetCurrentPhaseAt(actx.CurrentTime)
	if exists {
		cPST, _ := current.StartAfter.AddTo(spec.ActiveFrom)

		if pST.Before(cPST) {
			return &subscription.PatchForbiddenError{Msg: "cannot extend past phase"}
		}
	} else {
		// If current phase doesn't exist then all phases are either in the past or in the future
		// If they're all in the past then the by checking any we can see if it should fail or not
		if pST.Before(actx.CurrentTime) {
			return &subscription.PatchForbiddenError{Msg: "cannot extend past phase"}
		}
	}

	if len(sortedPhases) < 2 {
		return &subscription.PatchConflictError{Msg: "cannot stretch a single phase"}
	}

	reachedTargetPhase := false
	for i, thisP := range sortedPhases {
		if thisP.PhaseKey == p.PhaseKey {
			reachedTargetPhase = true
			continue
		}

		if reachedTargetPhase {
			// Adding durtions in the semantic way (using ISO8601 format)
			sa, err := thisP.StartAfter.Add(p.Duration)
			if err != nil {
				return &subscription.PatchValidationError{Msg: fmt.Sprintf("failed to extend phase %s: %s", thisP.PhaseKey, err)}
			}

			// before changing lets make sure the previous phase doesn't disappear
			if i > 0 {
				prev := sortedPhases[i-1]
				prevStart, _ := prev.StartAfter.AddTo(spec.ActiveFrom)
				newStart, _ := sa.AddTo(spec.ActiveFrom)
				if !newStart.After(prevStart) {
					return &subscription.PatchConflictError{Msg: fmt.Sprintf("phase %s would disappear due to stretching", prev.PhaseKey)}
				}
			}

			sortedPhases[i].StartAfter = sa
		}
	}

	return nil
}