File size: 3,364 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 subscription

import (
	"context"
	"fmt"

	"github.com/qmuntal/stateless"

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

type SubscriptionStatus string

const (
	// Active means the subscription is active and the customer is being billed
	SubscriptionStatusActive SubscriptionStatus = "active"
	// Canceled means the subscription has already been canceled but is still active
	SubscriptionStatusCanceled SubscriptionStatus = "canceled"
	// Inactive means the subscription is inactive (might have been previously active) and the customer is not being billed
	SubscriptionStatusInactive SubscriptionStatus = "inactive"
	// Scheduled means the subscription is scheduled to be active in the future
	SubscriptionStatusScheduled SubscriptionStatus = "scheduled"
)

func (s SubscriptionStatus) Values() []SubscriptionStatus {
	return []SubscriptionStatus{
		SubscriptionStatusActive,
		SubscriptionStatusCanceled,
		SubscriptionStatusInactive,
		SubscriptionStatusScheduled,
	}
}

func (s SubscriptionStatus) Validate() error {
	switch s {
	case SubscriptionStatusActive, SubscriptionStatusCanceled, SubscriptionStatusInactive, SubscriptionStatusScheduled:
		return nil
	default:
		return fmt.Errorf("invalid subscription status: %s", s)
	}
}

type SubscriptionAction string

const (
	SubscriptionActionCreate       SubscriptionAction = "create"
	SubscriptionActionUpdate       SubscriptionAction = "update"
	SubscriptionActionCancel       SubscriptionAction = "cancel"
	SubscriptionActionContinue     SubscriptionAction = "continue"
	SubscriptionActionDelete       SubscriptionAction = "delete"
	SubscriptionActionChangeAddons SubscriptionAction = "change_addons"
)

// SubscriptionStateMachine is a very simple state machine that determines what actions can be taken on a Subscription
type SubscriptionStateMachine struct {
	sm *stateless.StateMachine
}

func (sm SubscriptionStateMachine) CanTransitionOrErr(ctx context.Context, action SubscriptionAction) error {
	can, err := sm.sm.CanFireCtx(ctx, action)
	// If there was an error, let's just log it and return false
	if err != nil {
		return fmt.Errorf("failed to check if transition is possible: %w", err)
	}

	state, err := sm.sm.State(ctx)
	if err != nil {
		return fmt.Errorf("failed to get current state: %w", err)
	}

	status, ok := state.(SubscriptionStatus)
	if !ok {
		return fmt.Errorf("failed to cast state to SubscriptionStatus, got %T %v", state, state)
	}

	if err := status.Validate(); err != nil {
		return fmt.Errorf("invalid state: %w", err)
	}

	if !can {
		return models.NewGenericForbiddenError(
			fmt.Errorf("transition %s in state %s not allowed", action, state),
		)
	}

	return nil
}

func NewStateMachine(status SubscriptionStatus) SubscriptionStateMachine {
	sm := stateless.NewStateMachine(status)

	sm.Configure(SubscriptionStatusInactive).
		Permit(SubscriptionActionCreate, SubscriptionStatusActive)

	sm.Configure(SubscriptionStatusActive).
		PermitReentry(SubscriptionActionUpdate).
		PermitReentry(SubscriptionActionChangeAddons).
		Permit(SubscriptionActionCancel, SubscriptionStatusCanceled)

	sm.Configure(SubscriptionStatusCanceled).
		Permit(SubscriptionActionContinue, SubscriptionStatusActive)

	sm.Configure(SubscriptionStatusScheduled).
		Permit(SubscriptionActionDelete, nil) // Delete deletes the state too

	return SubscriptionStateMachine{
		sm: sm,
	}
}