| package taxcode |
|
|
| import ( |
| "context" |
| "errors" |
| "fmt" |
| "log/slog" |
|
|
| "github.com/samber/lo" |
|
|
| "github.com/openmeterio/openmeter/openmeter/namespace" |
| "github.com/openmeterio/openmeter/pkg/framework/transaction" |
| "github.com/openmeterio/openmeter/pkg/models" |
| ) |
|
|
| |
| type SeedEntry struct { |
| Key string |
| Name string |
| Description *string |
| AppMappings TaxCodeAppMappings |
| DefaultInvoicing bool |
| DefaultCreditGrant bool |
| } |
|
|
| |
| type NamespaceHandlerConfig struct { |
| Logger *slog.Logger |
| Service Service |
| Seeds []SeedEntry |
| TransactionManager transaction.Creator |
| } |
|
|
| func (c NamespaceHandlerConfig) validate() error { |
| var errs []error |
|
|
| if c.Logger == nil { |
| errs = append(errs, errors.New("logger is required")) |
| } |
|
|
| if c.Service == nil { |
| errs = append(errs, errors.New("service is required")) |
| } |
|
|
| if c.TransactionManager == nil { |
| errs = append(errs, errors.New("transaction manager is required")) |
| } |
|
|
| if len(c.Seeds) == 0 { |
| errs = append(errs, errors.New("at least one seed entry is required")) |
| } else { |
| invoicingCount := lo.CountBy(c.Seeds, func(s SeedEntry) bool { return s.DefaultInvoicing }) |
| creditGrantCount := lo.CountBy(c.Seeds, func(s SeedEntry) bool { return s.DefaultCreditGrant }) |
| if invoicingCount != 1 { |
| errs = append(errs, fmt.Errorf("exactly one seed must have DefaultInvoicing=true, got %d", invoicingCount)) |
| } |
| if creditGrantCount != 1 { |
| errs = append(errs, fmt.Errorf("exactly one seed must have DefaultCreditGrant=true, got %d", creditGrantCount)) |
| } |
| } |
|
|
| return errors.Join(errs...) |
| } |
|
|
| |
| type NamespaceHandler struct { |
| logger *slog.Logger |
| service Service |
| seeds []SeedEntry |
| transactionManager transaction.Creator |
| } |
|
|
| var _ namespace.Handler = (*NamespaceHandler)(nil) |
|
|
| |
| |
| func NewNamespaceHandler(cfg NamespaceHandlerConfig) (*NamespaceHandler, error) { |
| if err := cfg.validate(); err != nil { |
| return nil, fmt.Errorf("invalid namespace handler config: %w", err) |
| } |
|
|
| return &NamespaceHandler{ |
| logger: cfg.Logger, |
| service: cfg.Service, |
| seeds: cfg.Seeds, |
| transactionManager: cfg.TransactionManager, |
| }, nil |
| } |
|
|
| |
| |
| |
| |
| |
| func (h *NamespaceHandler) CreateNamespace(ctx context.Context, ns string) error { |
| return transaction.RunWithNoValue(ctx, h.transactionManager, func(ctx context.Context) error { |
| |
| |
| listed, err := h.service.ListTaxCodes(ctx, ListTaxCodesInput{Namespace: ns}) |
| if err != nil { |
| return fmt.Errorf("list tax codes: %w", err) |
| } |
| existingByKey := lo.SliceToMap(listed.Items, func(tc TaxCode) (string, TaxCode) { |
| return tc.Key, tc |
| }) |
|
|
| var invoicingID, creditGrantID string |
|
|
| for _, seed := range h.seeds { |
| id, err := h.ensureTaxCode(ctx, ns, seed, existingByKey) |
| if err != nil { |
| return fmt.Errorf("seed tax code %q: %w", seed.Key, err) |
| } |
|
|
| if seed.DefaultInvoicing { |
| invoicingID = id |
| } |
|
|
| if seed.DefaultCreditGrant { |
| creditGrantID = id |
| } |
| } |
|
|
| |
| _, err = h.service.GetOrganizationDefaultTaxCodes(ctx, GetOrganizationDefaultTaxCodesInput{ |
| Namespace: ns, |
| }) |
| if err != nil && !IsOrganizationDefaultTaxCodesNotFoundError(err) { |
| return fmt.Errorf("get organization default tax codes: %w", err) |
| } |
|
|
| if err == nil { |
| |
| return nil |
| } |
|
|
| if _, err := h.service.UpsertOrganizationDefaultTaxCodes(ctx, UpsertOrganizationDefaultTaxCodesInput{ |
| Namespace: ns, |
| InvoicingTaxCodeID: invoicingID, |
| CreditGrantTaxCodeID: creditGrantID, |
| }); err != nil { |
| return fmt.Errorf("upsert organization default tax codes: %w", err) |
| } |
|
|
| return nil |
| }) |
| } |
|
|
| |
| |
| func (h *NamespaceHandler) DeleteNamespace(_ context.Context, _ string) error { |
| return nil |
| } |
|
|
| |
| |
| |
| |
| func (h *NamespaceHandler) ensureTaxCode(ctx context.Context, ns string, seed SeedEntry, existingByKey map[string]TaxCode) (string, error) { |
| if existing, found := existingByKey[seed.Key]; found { |
| return existing.ID, nil |
| } |
|
|
| |
| created, createErr := h.service.CreateTaxCode(ctx, CreateTaxCodeInput{ |
| Namespace: ns, |
| Key: seed.Key, |
| Name: seed.Name, |
| Description: seed.Description, |
| AppMappings: seed.AppMappings, |
| Annotations: models.Annotations{ |
| AnnotationKeyManagedBy: AnnotationValueManagedBySystem, |
| }, |
| }) |
| if createErr != nil { |
| |
| if models.IsGenericConflictError(createErr) { |
| result2, listErr := h.service.ListTaxCodes(ctx, ListTaxCodesInput{ |
| Namespace: ns, |
| }) |
| if listErr != nil { |
| return "", fmt.Errorf("list tax codes after conflict: %w", listErr) |
| } |
|
|
| concurrent, ok := lo.Find(result2.Items, func(tc TaxCode) bool { |
| return tc.Key == seed.Key |
| }) |
| if ok { |
| return concurrent.ID, nil |
| } |
|
|
| return "", fmt.Errorf("tax code with key %q: conflict reported by create but key not found after re-list: %w", seed.Key, createErr) |
| } |
|
|
| return "", fmt.Errorf("create tax code: %w", createErr) |
| } |
|
|
| return created.ID, nil |
| } |
|
|