File size: 6,799 Bytes
1f10f31
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
package subscriptions

import (
	"context"
	"errors"
	"fmt"
	"net/http"

	"github.com/samber/lo"

	api "github.com/openmeterio/openmeter/api/v3"
	"github.com/openmeterio/openmeter/api/v3/apierrors"
	"github.com/openmeterio/openmeter/api/v3/request"
	"github.com/openmeterio/openmeter/openmeter/customer"
	"github.com/openmeterio/openmeter/openmeter/productcatalog"
	"github.com/openmeterio/openmeter/openmeter/productcatalog/plan"
	plansubscription "github.com/openmeterio/openmeter/openmeter/productcatalog/subscription"
	"github.com/openmeterio/openmeter/pkg/framework/commonhttp"
	"github.com/openmeterio/openmeter/pkg/framework/transport/httptransport"
	models "github.com/openmeterio/openmeter/pkg/models"
)

type (
	CreateSubscriptionRequest  = plansubscription.CreateSubscriptionRequest
	CreateSubscriptionResponse = api.BillingSubscription
	CreateSubscriptionHandler  = httptransport.Handler[CreateSubscriptionRequest, CreateSubscriptionResponse]
)

// CreateSubscription returns a new httptransport.Handler for creating a subscription.
func (h *handler) CreateSubscription() CreateSubscriptionHandler {
	return httptransport.NewHandler(
		func(ctx context.Context, r *http.Request) (plansubscription.CreateSubscriptionRequest, error) {
			// Parse the request body
			body := api.BillingSubscriptionCreate{}
			if err := request.ParseBody(r, &body); err != nil {
				return CreateSubscriptionRequest{}, err
			}

			// Resolve the namespace
			ns, err := h.resolveNamespace(ctx)
			if err != nil {
				return CreateSubscriptionRequest{}, err
			}

			var settlementMode *productcatalog.SettlementMode
			if body.SettlementMode != nil {
				settlementMode = lo.ToPtr(productcatalog.SettlementMode(*body.SettlementMode))
			}

			// Validate that either customer ID or customer key is provided
			if body.Customer.Id == nil && body.Customer.Key == nil {
				reason := "one of customer.id or customer.key is required"
				return CreateSubscriptionRequest{}, apierrors.NewBadRequestError(
					ctx,
					errors.New(reason),
					[]apierrors.InvalidParameter{
						{
							Field:  "customer.id",
							Reason: reason,
							Source: apierrors.InvalidParamSourceBody,
							Rule:   "required",
						},
						{
							Field:  "customer.key",
							Reason: reason,
							Source: apierrors.InvalidParamSourceBody,
							Rule:   "required",
						},
					},
				)
			}

			// Get the customer to validate it exists
			customerEntity, err := h.getCustomerByIDOrKey(ctx, ns, body.Customer.Id, body.Customer.Key)
			if err != nil {
				return CreateSubscriptionRequest{}, fmt.Errorf("failed to get customer: %w", err)
			}

			// TODO: implement custom subscription creation
			if body.Plan.Id == nil && body.Plan.Key == nil {
				reason := "one of plan.id or plan.key is required"
				// We use bad request error because not implemented does not provide the error context
				return CreateSubscriptionRequest{}, apierrors.NewBadRequestError(
					ctx,
					errors.New(reason),
					[]apierrors.InvalidParameter{
						{
							Field:  "plan.id",
							Reason: reason,
							Source: apierrors.InvalidParamSourceBody,
							Rule:   "required",
						},
						{
							Field:  "plan.key",
							Reason: reason,
							Source: apierrors.InvalidParamSourceBody,
							Rule:   "required",
						},
					},
				)
			}

			// Get the plan entity by ID or key to validate it exists
			planEntity, err := h.getPlanByIDOrKey(ctx, ns, body.Plan.Id, body.Plan.Key, body.Plan.Version)
			if err != nil {
				return CreateSubscriptionRequest{}, fmt.Errorf("failed to get plan: %w", err)
			}

			// Convert the plan entity to a plan input
			planInput := plansubscription.PlanInput{}
			planInput.FromRef(&plansubscription.PlanRefInput{
				Key:     planEntity.Key,
				Version: &planEntity.Version,
			})

			// Convert the request to a create subscription workflow input
			subscriptionName := fmt.Sprintf("%s v%d", planEntity.Key, planEntity.Version)
			workflowInput, err := FromAPIBillingSubscriptionCreate(
				ns,
				customerEntity.GetID(),
				subscriptionName,
				body,
			)
			if err != nil {
				return CreateSubscriptionRequest{}, err
			}

			return plansubscription.CreateSubscriptionRequest{
				WorkflowInput:  workflowInput,
				PlanInput:      planInput,
				SettlementMode: settlementMode,
			}, nil
		},
		func(ctx context.Context, request plansubscription.CreateSubscriptionRequest) (CreateSubscriptionResponse, error) {
			// Create the subscription from a plan
			m, err := h.planSubscriptionService.Create(ctx, request)
			if err != nil {
				return CreateSubscriptionResponse{}, err
			}

			// Convert the subscription to an API subscription
			return ToAPIBillingSubscription(m), nil
		},
		commonhttp.JSONResponseEncoderWithStatus[CreateSubscriptionResponse](http.StatusCreated),
		httptransport.AppendOptions(
			h.options,
			httptransport.WithOperationName("create-subscription"),
			httptransport.WithErrorEncoder(apierrors.GenericErrorEncoder()),
		)...,
	)
}

// getCustomerByIDOrKey gets a customer by ID or key helper function
// TODO: move this to the customer service
func (h *handler) getCustomerByIDOrKey(ctx context.Context, namespace string, customerID *string, customerKey *string) (*customer.Customer, error) {
	var getCustomerInput customer.GetCustomerInput

	if customerID != nil {
		getCustomerInput = customer.GetCustomerInput{
			CustomerID: &customer.CustomerID{
				Namespace: namespace,
				ID:        *customerID,
			},
		}
	} else if customerKey != nil {
		getCustomerInput = customer.GetCustomerInput{
			CustomerKey: &customer.CustomerKey{
				Namespace: namespace,
				Key:       *customerKey,
			},
		}
	} else {
		return nil, fmt.Errorf("customer id or customer key is required")
	}

	return h.customerService.GetCustomer(ctx, getCustomerInput)
}

// getPlanByIDOrKey gets a plan by ID or key helper function
// TODO: move this to the plan service
func (h *handler) getPlanByIDOrKey(ctx context.Context, namespace string, planID *string, planKey *string, planVersion *int) (*plan.Plan, error) {
	// Get the plan entity, to validate it exists
	var getPlanInput plan.GetPlanInput

	if planID != nil {
		getPlanInput = plan.GetPlanInput{
			NamespacedID: models.NamespacedID{
				Namespace: namespace,
				ID:        *planID,
			},
		}
	} else if planKey != nil {
		getPlanInput = plan.GetPlanInput{}
		// We use setters because namespace only exists on namespaced ID
		// But here we don't have a namespaced ID
		getPlanInput.Namespace = namespace
		getPlanInput.Key = *planKey

		if planVersion != nil {
			getPlanInput.Version = *planVersion
		} else {
			getPlanInput.IncludeLatest = true
		}
	} else {
		return nil, errors.New("plan id or plan key must be set")
	}

	// Get the plan entity
	return h.planService.GetPlan(ctx, getPlanInput)
}