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)
}
|