| package service |
|
|
| import ( |
| "context" |
| "fmt" |
|
|
| "github.com/samber/lo" |
|
|
| "github.com/openmeterio/openmeter/openmeter/customer" |
| "github.com/openmeterio/openmeter/openmeter/subscription" |
| "github.com/openmeterio/openmeter/pkg/clock" |
| "github.com/openmeterio/openmeter/pkg/models" |
| ) |
|
|
| func (s *service) validateCreate(ctx context.Context, cust customer.Customer, spec subscription.SubscriptionSpec) error { |
| |
| if spec.CustomerId != cust.ID { |
| return fmt.Errorf("customer ID mismatch: %s != %s", spec.CustomerId, cust.ID) |
| } |
|
|
| |
|
|
| |
| if err := spec.Validate(); err != nil { |
| return fmt.Errorf("spec is invalid: %w", err) |
| } |
|
|
| |
| if err := subscription.NewStateMachine(subscription.SubscriptionStatusInactive).CanTransitionOrErr(ctx, subscription.SubscriptionActionCreate); err != nil { |
| return err |
| } |
|
|
| |
| if spec.HasBillables() { |
| if cust.Currency != nil && (string(*cust.Currency) != string(spec.Currency)) { |
| return models.NewGenericValidationError(fmt.Errorf("currency mismatch: customer currency is %s, but subscription currency is %s", *cust.Currency, spec.Currency)) |
| } |
| } |
|
|
| return nil |
| } |
|
|
| func (s *service) validateUpdate(ctx context.Context, currentView subscription.SubscriptionView, newSpec subscription.SubscriptionSpec) error { |
| |
| if err := subscription.NewStateMachine( |
| currentView.Subscription.GetStatusAt(clock.Now()), |
| ).CanTransitionOrErr(ctx, subscription.SubscriptionActionUpdate); err != nil { |
| return err |
| } |
|
|
| |
| cus, err := s.CustomerService.GetCustomer(ctx, customer.GetCustomerInput{ |
| CustomerID: &customer.CustomerID{ |
| Namespace: currentView.Subscription.Namespace, |
| ID: currentView.Subscription.CustomerId, |
| }, |
| }) |
| if err != nil { |
| return err |
| } |
|
|
| if cus != nil && cus.IsDeleted() { |
| return models.NewGenericPreConditionFailedError( |
| fmt.Errorf("customer is deleted [namespace=%s customer.id=%s]", cus.Namespace, cus.ID), |
| ) |
| } |
|
|
| if cus == nil { |
| return fmt.Errorf("customer is nil") |
| } |
|
|
| if newSpec.HasBillables() { |
| if cus.Currency != nil { |
| if string(*cus.Currency) != string(newSpec.Currency) { |
| return models.NewGenericValidationError(fmt.Errorf("currency mismatch: customer currency is %s, but subscription currency is %s", *cus.Currency, newSpec.Currency)) |
| } |
| } |
| } |
|
|
| return nil |
| } |
|
|
| func (s *service) validateCancel(ctx context.Context, view subscription.SubscriptionView, timing subscription.Timing) error { |
| |
| if err := subscription.NewStateMachine( |
| view.Subscription.GetStatusAt(clock.Now()), |
| ).CanTransitionOrErr(ctx, subscription.SubscriptionActionCancel); err != nil { |
| return err |
| } |
|
|
| spec := view.AsSpec() |
|
|
| |
| if err := timing.ValidateForAction(subscription.SubscriptionActionCancel, &view); err != nil { |
| return fmt.Errorf("invalid cancelation timing: %w", err) |
| } |
|
|
| cancelTime, err := timing.ResolveForSpec(view.Spec) |
| if err != nil { |
| return fmt.Errorf("failed to get cancelation time: %w", err) |
| } |
|
|
| |
| |
|
|
| spec.ActiveTo = lo.ToPtr(cancelTime) |
|
|
| if err := spec.Validate(); err != nil { |
| return fmt.Errorf("spec is invalid after setting cancelation time: %w", err) |
| } |
|
|
| return nil |
| } |
|
|
| func (s *service) validateContinue(ctx context.Context, view subscription.SubscriptionView) error { |
| |
| if err := subscription.NewStateMachine( |
| view.Subscription.GetStatusAt(clock.Now()), |
| ).CanTransitionOrErr(ctx, subscription.SubscriptionActionContinue); err != nil { |
| return err |
| } |
|
|
| return nil |
| } |
|
|
| func numNotGrouped[T any, K comparable](source []T, grouped map[K][]T) int { |
| count := len(source) |
| for _, group := range grouped { |
| count -= len(group) |
| } |
|
|
| return count |
| } |
|
|