File size: 3,449 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
package config

import (
	"errors"
	"fmt"
	"strings"

	"github.com/spf13/viper"

	"github.com/openmeterio/openmeter/openmeter/taxcode"
)

type TaxCodeConfiguration struct {
	Seeds []TaxCodeSeed `mapstructure:"seeds"`
}

// TaxCodeSeed defines a single tax code entry to provision for every namespace.
// A single seed may carry both DefaultInvoicing and DefaultCreditGrant; the two flags are
// independent and the exactly-one rule is enforced per flag.
type TaxCodeSeed struct {
	Key                string              `mapstructure:"key"`
	Name               string              `mapstructure:"name"`
	Description        *string             `mapstructure:"description"`
	AppMappings        []TaxCodeAppMapping `mapstructure:"appMappings"`
	DefaultInvoicing   bool                `mapstructure:"defaultInvoicing"`
	DefaultCreditGrant bool                `mapstructure:"defaultCreditGrant"`
}

type TaxCodeAppMapping struct {
	AppType string `mapstructure:"appType"`
	TaxCode string `mapstructure:"taxCode"`
}

func (c TaxCodeConfiguration) Validate() error {
	var errs []error

	if len(c.Seeds) == 0 {
		errs = append(errs, errors.New("seeds must not be empty"))
		return errors.Join(errs...)
	}

	keys := make(map[string]struct{}, len(c.Seeds))
	defaultInvoicingCount := 0
	defaultCreditGrantCount := 0

	for i, seed := range c.Seeds {
		trimmedKey := strings.TrimSpace(seed.Key)
		if trimmedKey == "" {
			errs = append(errs, fmt.Errorf("seed[%d]: key must not be empty", i))
		}

		if strings.TrimSpace(seed.Name) == "" {
			errs = append(errs, fmt.Errorf("seed[%d]: name must not be empty", i))
		}

		if trimmedKey != "" {
			if _, exists := keys[trimmedKey]; exists {
				errs = append(errs, fmt.Errorf("seed[%d]: duplicate key %q", i, seed.Key))
			}
			keys[trimmedKey] = struct{}{}
		}

		if seed.DefaultInvoicing {
			defaultInvoicingCount++
		}

		if seed.DefaultCreditGrant {
			defaultCreditGrantCount++
		}

		for j, mapping := range seed.AppMappings {
			trimmedAppType := strings.TrimSpace(mapping.AppType)
			trimmedTaxCode := strings.TrimSpace(mapping.TaxCode)

			if trimmedAppType == "" {
				errs = append(errs, fmt.Errorf("seed[%d].appMappings[%d]: appType must not be empty", i, j))
			}

			if trimmedTaxCode == "" {
				errs = append(errs, fmt.Errorf("seed[%d].appMappings[%d]: taxCode must not be empty", i, j))
			} else if trimmedAppType == "stripe" && !taxcode.TaxCodeStripeRegexp.MatchString(trimmedTaxCode) {
				errs = append(errs, fmt.Errorf("seed[%d].appMappings[%d]: taxCode %q is not a valid Stripe tax code (must match %s)", i, j, trimmedTaxCode, taxcode.TaxCodeStripeRegexp.String()))
			}
		}
	}

	if defaultInvoicingCount != 1 {
		errs = append(errs, fmt.Errorf("exactly one seed must have defaultInvoicing=true, got %d", defaultInvoicingCount))
	}

	if defaultCreditGrantCount != 1 {
		errs = append(errs, fmt.Errorf("exactly one seed must have defaultCreditGrant=true, got %d", defaultCreditGrantCount))
	}

	return errors.Join(errs...)
}

func ConfigureTaxCode(v *viper.Viper) {
	v.SetDefault("taxcode.seeds", []map[string]any{
		{
			"key":              taxcode.ProviderDefaultTaxCodeKey,
			"name":             "Provider default",
			"defaultInvoicing": true,
		},
		{
			"key":                "nontaxable",
			"name":               "Nontaxable",
			"defaultCreditGrant": true,
			"appMappings": []map[string]any{
				{
					"appType": "stripe",
					"taxCode": "txcd_00000000",
				},
			},
		},
	})
}