File size: 6,454 Bytes
d6f631f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
package patch

import (
	"fmt"

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

type PatchAddItem struct {
	PhaseKey    string
	ItemKey     string
	CreateInput subscription.SubscriptionItemSpec
}

func (a PatchAddItem) Op() subscription.PatchOperation {
	return subscription.PatchOperationAdd
}

func (a PatchAddItem) Path() subscription.SpecPath {
	return subscription.NewItemPath(a.PhaseKey, a.ItemKey)
}

func (a PatchAddItem) Value() subscription.SubscriptionItemSpec {
	return a.CreateInput
}

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

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

	if err := a.CreateInput.Validate(); err != nil {
		return models.ErrorWithFieldPrefix(a.FieldDescriptor(), err)
	}

	return nil
}

func (a PatchAddItem) FieldDescriptor() *models.FieldDescriptor {
	return models.NewFieldSelectorGroup(
		models.NewFieldSelectorGroup(
			models.NewFieldSelector("phases"),
			models.NewFieldSelector(a.PhaseKey),
		).WithAttributes(models.Attributes{
			subscription.PhaseDescriptor: true,
		}),
		models.NewFieldSelector("items"),
		models.NewFieldSelector(a.ItemKey),
	)
}

func (a PatchAddItem) ValueAsAny() any {
	return a.CreateInput
}

var _ subscription.ValuePatch[subscription.SubscriptionItemSpec] = PatchAddItem{}

func (a PatchAddItem) ApplyTo(spec *subscription.SubscriptionSpec, actx subscription.ApplyContext) error {
	phase, ok := spec.Phases[a.PhaseKey]
	if !ok {
		return &subscription.PatchValidationError{Msg: fmt.Sprintf("phase %s not found", a.PhaseKey)}
	}

	phaseStartTime, _ := phase.StartAfter.AddTo(spec.ActiveFrom)

	// Checks we need:

	// 1. You cannot add items to previous phases
	currentPhase, exists := spec.GetCurrentPhaseAt(actx.CurrentTime)
	if !exists {
		// If the current phase doesn't exist then either all phases are in the past or in the future
		// If all phases are in the past then no addition is possible
		// If all phases are in the past then the selected one is also in the past
		if st, _ := phase.StartAfter.AddTo(spec.ActiveFrom); st.Before(actx.CurrentTime) {
			return &subscription.PatchForbiddenError{Msg: fmt.Sprintf("cannot add item to phase %s which starts before current phase", a.PhaseKey)}
		} else {
			// If it's added to a future phase, the matching key for the phase has to be empty
			if len(phase.ItemsByKey) > 0 {
				return &subscription.PatchForbiddenError{Msg: fmt.Sprintf("cannot add item to future phase %s which already has items", a.PhaseKey)}
			}
		}
	} else {
		currentPhaseStartTime, _ := currentPhase.StartAfter.AddTo(spec.ActiveFrom)

		// If the selected phase is before the current phase, it's forbidden
		if phaseStartTime.Before(currentPhaseStartTime) {
			return &subscription.PatchForbiddenError{Msg: fmt.Sprintf("cannot add item to phase %s which starts before current phase", a.PhaseKey)}
		} else if phase.PhaseKey == currentPhase.PhaseKey {
			// Sanity check
			if actx.CurrentTime.Before(phaseStartTime) {
				return fmt.Errorf("current time is before the current phase start which is impossible")
			}

			// 2. If it's added to the current phase, the specified start time cannot point to the past
			if a.CreateInput.ActiveFromOverrideRelativeToPhaseStart != nil {
				iST, _ := a.CreateInput.ActiveFromOverrideRelativeToPhaseStart.AddTo(phaseStartTime)
				if iST.Before(actx.CurrentTime) {
					return &subscription.PatchForbiddenError{Msg: fmt.Sprintf("cannot add item to phase %s which would become active in the past at %s", a.PhaseKey, iST)}
				}
			} else {
				// 3. If it's added to the current phase, and start time is not specified, it will be set for the current time, as you cannot change the past
				diff := datetime.ISODurationBetween(phaseStartTime, actx.CurrentTime)
				a.CreateInput.ActiveFromOverrideRelativeToPhaseStart = &diff
			}
		} else if phaseStartTime.After(currentPhaseStartTime) {
			// 4. If you're adding it to a future phase, the matching key for the phase has to be empty
			if len(phase.ItemsByKey[a.ItemKey]) > 0 {
				return &subscription.PatchForbiddenError{Msg: fmt.Sprintf("cannot add item to future phase %s which already has items", a.PhaseKey)}
			}
		} else {
			return fmt.Errorf("didn't enter any logical branch")
		}
	}

	// Finally, let's try to add it to the phase

	if phase.ItemsByKey[a.ItemKey] == nil {
		phase.ItemsByKey[a.ItemKey] = make([]*subscription.SubscriptionItemSpec, 0)
	}

	// If it's added to the current phase, we need to close the activity of any current item if present
	hasCurrentItemAndShouldCloseCurrentItemForKey := false

	if exists && currentPhase.PhaseKey == phase.PhaseKey {
		if len(phase.ItemsByKey[a.ItemKey]) > 0 {
			hasCurrentItemAndShouldCloseCurrentItemForKey = true
		}
	}

	if hasCurrentItemAndShouldCloseCurrentItemForKey {
		// Sanity check
		if len(phase.ItemsByKey[a.ItemKey]) == 0 {
			return fmt.Errorf("there should be an item to close")
		}

		itemToClose := phase.ItemsByKey[a.ItemKey][len(phase.ItemsByKey[a.ItemKey])-1]

		// If it already has a scheduled end time, which is later than the time this new item should start, we should error.
		// The user can circumvent this, by first issuing a delete for the item, and then adding a new one.
		if itemToClose.ActiveToOverrideRelativeToPhaseStart != nil {
			itemToCloseEndTime, _ := itemToClose.ActiveToOverrideRelativeToPhaseStart.AddTo(phaseStartTime)

			// Sanity check
			if a.CreateInput.ActiveFromOverrideRelativeToPhaseStart == nil {
				return fmt.Errorf("ActiveFromOverrideRelativeToPhaseStart should already be set when adding after an already existing item for the current phase")
			}

			itemToAddStartTime, _ := a.CreateInput.ActiveFromOverrideRelativeToPhaseStart.AddTo(phaseStartTime)

			if itemToCloseEndTime.After(itemToAddStartTime) {
				return &subscription.PatchForbiddenError{Msg: fmt.Sprintf("cannot add item to phase %s which would overlap with a current item, you should delete first", a.PhaseKey)}
			}
		}

		// Let's update the current item to close to actually close as the new item starts
		itemToClose.ActiveToOverrideRelativeToPhaseStart = a.CreateInput.ActiveFromOverrideRelativeToPhaseStart
	}

	// Finally, we simply add it as the last Spec for its key in the phase

	phase.ItemsByKey[a.ItemKey] = append(phase.ItemsByKey[a.ItemKey], &a.CreateInput)
	return nil
}