Leon4gr45 commited on
Commit
429334c
·
verified ·
1 Parent(s): a9a365b

Upload folder using huggingface_hub

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. apiconverter/cursor.go +18 -0
  2. apiconverter/filter.gen.go +266 -0
  3. apiconverter/filter.go +43 -0
  4. app/adapter.go +37 -0
  5. app/adapter/adapter.go +68 -0
  6. app/adapter/app.go +283 -0
  7. app/adapter/customer.go +178 -0
  8. app/adapter/marketplace.go +132 -0
  9. app/app.go +218 -0
  10. app/appbase.go +161 -0
  11. app/customer.go +10 -0
  12. app/custominvoicing/adapter.go +27 -0
  13. app/custominvoicing/adapter/adapter.go +72 -0
  14. app/custominvoicing/adapter/appconfig.go +70 -0
  15. app/custominvoicing/adapter/customerdata.go +89 -0
  16. app/custominvoicing/app.go +170 -0
  17. app/custominvoicing/customerdata.go +100 -0
  18. app/custominvoicing/factory.go +159 -0
  19. app/custominvoicing/httpdriver/custominvoicing.go +172 -0
  20. app/custominvoicing/httpdriver/errors.go +21 -0
  21. app/custominvoicing/httpdriver/handler.go +52 -0
  22. app/custominvoicing/httpdriver/mapper.go +85 -0
  23. app/custominvoicing/service.go +34 -0
  24. app/custominvoicing/service/customerdata.go +26 -0
  25. app/custominvoicing/service/factory.go +57 -0
  26. app/custominvoicing/service/service.go +62 -0
  27. app/custominvoicing/service/sync.go +117 -0
  28. app/custominvoicing/sync.go +66 -0
  29. app/defaults.go +6 -0
  30. app/errors.go +226 -0
  31. app/event.go +214 -0
  32. app/events.go +70 -0
  33. app/httpdriver/app.go +261 -0
  34. app/httpdriver/customer.go +421 -0
  35. app/httpdriver/handler.go +80 -0
  36. app/httpdriver/mapper.go +177 -0
  37. app/httpdriver/marketplace.go +245 -0
  38. app/input.go +91 -0
  39. app/marketplace.go +230 -0
  40. app/registry.go +78 -0
  41. app/sandbox/app.go +255 -0
  42. app/sandbox/config.go +7 -0
  43. app/sandbox/errors.go +5 -0
  44. app/sandbox/helpers.go +70 -0
  45. app/sandbox/marketplace.go +42 -0
  46. app/sandbox/mock.go +284 -0
  47. app/service.go +34 -0
  48. app/service/app.go +151 -0
  49. app/service/customer.go +22 -0
  50. app/service/marketplace.go +105 -0
apiconverter/cursor.go ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package apiconverter
2
+
3
+ import (
4
+ "github.com/openmeterio/openmeter/api"
5
+ "github.com/openmeterio/openmeter/pkg/pagination/v2"
6
+ )
7
+
8
+ func ConvertCursor(s api.CursorPaginationCursor) (*pagination.Cursor, error) {
9
+ return pagination.DecodeCursor(s)
10
+ }
11
+
12
+ func ConvertCursorPtr(s *api.CursorPaginationCursor) (*pagination.Cursor, error) {
13
+ if s == nil {
14
+ return nil, nil
15
+ }
16
+
17
+ return ConvertCursor(*s)
18
+ }
apiconverter/filter.gen.go ADDED
@@ -0,0 +1,266 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Code generated by github.com/jmattheis/goverter, DO NOT EDIT.
2
+ //go:build !goverter
3
+
4
+ package apiconverter
5
+
6
+ import (
7
+ api "github.com/openmeterio/openmeter/api"
8
+ filter "github.com/openmeterio/openmeter/pkg/filter"
9
+ )
10
+
11
+ func init() {
12
+ ConvertBoolean = func(source api.FilterBoolean) filter.FilterBoolean {
13
+ var filterFilterBoolean filter.FilterBoolean
14
+ filterFilterBoolean.Eq = source.Eq
15
+ return filterFilterBoolean
16
+ }
17
+ ConvertBooleanPtr = func(source *api.FilterBoolean) *filter.FilterBoolean {
18
+ var pFilterFilterBoolean *filter.FilterBoolean
19
+ if source != nil {
20
+ filterFilterBoolean := ConvertBoolean((*source))
21
+ pFilterFilterBoolean = &filterFilterBoolean
22
+ }
23
+ return pFilterFilterBoolean
24
+ }
25
+ ConvertFloat = func(source api.FilterFloat) filter.FilterFloat {
26
+ var filterFilterFloat filter.FilterFloat
27
+ filterFilterFloat.Eq = source.Eq
28
+ filterFilterFloat.Ne = source.Ne
29
+ filterFilterFloat.Gt = source.Gt
30
+ filterFilterFloat.Gte = source.Gte
31
+ filterFilterFloat.Lt = source.Lt
32
+ filterFilterFloat.Lte = source.Lte
33
+ if source.And != nil {
34
+ var filterFilterFloatList []filter.FilterFloat
35
+ if (*source.And) != nil {
36
+ filterFilterFloatList = make([]filter.FilterFloat, len((*source.And)))
37
+ for i := 0; i < len((*source.And)); i++ {
38
+ filterFilterFloatList[i] = ConvertFloat((*source.And)[i])
39
+ }
40
+ }
41
+ filterFilterFloat.And = &filterFilterFloatList
42
+ }
43
+ if source.Or != nil {
44
+ var filterFilterFloatList2 []filter.FilterFloat
45
+ if (*source.Or) != nil {
46
+ filterFilterFloatList2 = make([]filter.FilterFloat, len((*source.Or)))
47
+ for j := 0; j < len((*source.Or)); j++ {
48
+ filterFilterFloatList2[j] = ConvertFloat((*source.Or)[j])
49
+ }
50
+ }
51
+ filterFilterFloat.Or = &filterFilterFloatList2
52
+ }
53
+ return filterFilterFloat
54
+ }
55
+ ConvertFloatPtr = func(source *api.FilterFloat) *filter.FilterFloat {
56
+ var pFilterFilterFloat *filter.FilterFloat
57
+ if source != nil {
58
+ filterFilterFloat := ConvertFloat((*source))
59
+ pFilterFilterFloat = &filterFilterFloat
60
+ }
61
+ return pFilterFilterFloat
62
+ }
63
+ ConvertIDExact = func(source api.FilterIDExact) filter.FilterString {
64
+ var filterFilterString filter.FilterString
65
+ filterFilterString.In = source.In
66
+ return filterFilterString
67
+ }
68
+ ConvertIDExactPtr = func(source *api.FilterIDExact) *filter.FilterString {
69
+ var pFilterFilterString *filter.FilterString
70
+ if source != nil {
71
+ filterFilterString := ConvertIDExact((*source))
72
+ pFilterFilterString = &filterFilterString
73
+ }
74
+ return pFilterFilterString
75
+ }
76
+ ConvertInt = func(source api.FilterInteger) filter.FilterInteger {
77
+ var filterFilterInteger filter.FilterInteger
78
+ filterFilterInteger.Eq = source.Eq
79
+ filterFilterInteger.Ne = source.Ne
80
+ filterFilterInteger.Gt = source.Gt
81
+ filterFilterInteger.Gte = source.Gte
82
+ filterFilterInteger.Lt = source.Lt
83
+ filterFilterInteger.Lte = source.Lte
84
+ if source.And != nil {
85
+ var filterFilterIntegerList []filter.FilterInteger
86
+ if (*source.And) != nil {
87
+ filterFilterIntegerList = make([]filter.FilterInteger, len((*source.And)))
88
+ for i := 0; i < len((*source.And)); i++ {
89
+ filterFilterIntegerList[i] = ConvertInt((*source.And)[i])
90
+ }
91
+ }
92
+ filterFilterInteger.And = &filterFilterIntegerList
93
+ }
94
+ if source.Or != nil {
95
+ var filterFilterIntegerList2 []filter.FilterInteger
96
+ if (*source.Or) != nil {
97
+ filterFilterIntegerList2 = make([]filter.FilterInteger, len((*source.Or)))
98
+ for j := 0; j < len((*source.Or)); j++ {
99
+ filterFilterIntegerList2[j] = ConvertInt((*source.Or)[j])
100
+ }
101
+ }
102
+ filterFilterInteger.Or = &filterFilterIntegerList2
103
+ }
104
+ return filterFilterInteger
105
+ }
106
+ ConvertIntPtr = func(source *api.FilterInteger) *filter.FilterInteger {
107
+ var pFilterFilterInteger *filter.FilterInteger
108
+ if source != nil {
109
+ filterFilterInteger := ConvertInt((*source))
110
+ pFilterFilterInteger = &filterFilterInteger
111
+ }
112
+ return pFilterFilterInteger
113
+ }
114
+ ConvertString = func(source api.FilterString) filter.FilterString {
115
+ var filterFilterString filter.FilterString
116
+ filterFilterString.Eq = source.Eq
117
+ filterFilterString.Ne = source.Ne
118
+ filterFilterString.In = source.In
119
+ filterFilterString.Nin = source.Nin
120
+ filterFilterString.Like = source.Like
121
+ filterFilterString.Nlike = source.Nlike
122
+ filterFilterString.Ilike = source.Ilike
123
+ filterFilterString.Nilike = source.Nilike
124
+ filterFilterString.Gt = source.Gt
125
+ filterFilterString.Gte = source.Gte
126
+ filterFilterString.Lt = source.Lt
127
+ filterFilterString.Lte = source.Lte
128
+ if source.And != nil {
129
+ var filterFilterStringList []filter.FilterString
130
+ if (*source.And) != nil {
131
+ filterFilterStringList = make([]filter.FilterString, len((*source.And)))
132
+ for i := 0; i < len((*source.And)); i++ {
133
+ filterFilterStringList[i] = ConvertString((*source.And)[i])
134
+ }
135
+ }
136
+ filterFilterString.And = &filterFilterStringList
137
+ }
138
+ if source.Or != nil {
139
+ var filterFilterStringList2 []filter.FilterString
140
+ if (*source.Or) != nil {
141
+ filterFilterStringList2 = make([]filter.FilterString, len((*source.Or)))
142
+ for j := 0; j < len((*source.Or)); j++ {
143
+ filterFilterStringList2[j] = ConvertString((*source.Or)[j])
144
+ }
145
+ }
146
+ filterFilterString.Or = &filterFilterStringList2
147
+ }
148
+ return filterFilterString
149
+ }
150
+ ConvertStringMap = func(source map[string]api.FilterString) map[string]filter.FilterString {
151
+ var mapStringFilterFilterString map[string]filter.FilterString
152
+ if source != nil {
153
+ mapStringFilterFilterString = make(map[string]filter.FilterString, len(source))
154
+ for key, value := range source {
155
+ mapStringFilterFilterString[key] = ConvertString(value)
156
+ }
157
+ }
158
+ return mapStringFilterFilterString
159
+ }
160
+ ConvertStringMapPtr = func(source *map[string]api.FilterString) *map[string]filter.FilterString {
161
+ var pMapStringFilterFilterString *map[string]filter.FilterString
162
+ if source != nil {
163
+ mapStringFilterFilterString := ConvertStringMap((*source))
164
+ pMapStringFilterFilterString = &mapStringFilterFilterString
165
+ }
166
+ return pMapStringFilterFilterString
167
+ }
168
+ ConvertStringMapToAPIPtr = func(source map[string]filter.FilterString) map[string]api.FilterString {
169
+ var mapStringApiFilterString map[string]api.FilterString
170
+ if source != nil {
171
+ mapStringApiFilterString = make(map[string]api.FilterString, len(source))
172
+ for key, value := range source {
173
+ mapStringApiFilterString[key] = filterFilterStringToApiFilterString(value)
174
+ }
175
+ }
176
+ return mapStringApiFilterString
177
+ }
178
+ ConvertStringPtr = func(source *api.FilterString) *filter.FilterString {
179
+ var pFilterFilterString *filter.FilterString
180
+ if source != nil {
181
+ filterFilterString := ConvertString((*source))
182
+ pFilterFilterString = &filterFilterString
183
+ }
184
+ return pFilterFilterString
185
+ }
186
+ ConvertStringToAPI = func(source *filter.FilterString) *api.FilterString {
187
+ var pApiFilterString *api.FilterString
188
+ if source != nil {
189
+ apiFilterString := filterFilterStringToApiFilterString((*source))
190
+ pApiFilterString = &apiFilterString
191
+ }
192
+ return pApiFilterString
193
+ }
194
+ ConvertTime = func(source api.FilterTime) filter.FilterTime {
195
+ var filterFilterTime filter.FilterTime
196
+ filterFilterTime.Gt = source.Gt
197
+ filterFilterTime.Gte = source.Gte
198
+ filterFilterTime.Lt = source.Lt
199
+ filterFilterTime.Lte = source.Lte
200
+ if source.And != nil {
201
+ var filterFilterTimeList []filter.FilterTime
202
+ if (*source.And) != nil {
203
+ filterFilterTimeList = make([]filter.FilterTime, len((*source.And)))
204
+ for i := 0; i < len((*source.And)); i++ {
205
+ filterFilterTimeList[i] = ConvertTime((*source.And)[i])
206
+ }
207
+ }
208
+ filterFilterTime.And = &filterFilterTimeList
209
+ }
210
+ if source.Or != nil {
211
+ var filterFilterTimeList2 []filter.FilterTime
212
+ if (*source.Or) != nil {
213
+ filterFilterTimeList2 = make([]filter.FilterTime, len((*source.Or)))
214
+ for j := 0; j < len((*source.Or)); j++ {
215
+ filterFilterTimeList2[j] = ConvertTime((*source.Or)[j])
216
+ }
217
+ }
218
+ filterFilterTime.Or = &filterFilterTimeList2
219
+ }
220
+ return filterFilterTime
221
+ }
222
+ ConvertTimePtr = func(source *api.FilterTime) *filter.FilterTime {
223
+ var pFilterFilterTime *filter.FilterTime
224
+ if source != nil {
225
+ filterFilterTime := ConvertTime((*source))
226
+ pFilterFilterTime = &filterFilterTime
227
+ }
228
+ return pFilterFilterTime
229
+ }
230
+ }
231
+ func filterFilterStringToApiFilterString(source filter.FilterString) api.FilterString {
232
+ var apiFilterString api.FilterString
233
+ if source.And != nil {
234
+ var apiFilterStringList []api.FilterString
235
+ if (*source.And) != nil {
236
+ apiFilterStringList = make([]api.FilterString, len((*source.And)))
237
+ for i := 0; i < len((*source.And)); i++ {
238
+ apiFilterStringList[i] = filterFilterStringToApiFilterString((*source.And)[i])
239
+ }
240
+ }
241
+ apiFilterString.And = &apiFilterStringList
242
+ }
243
+ apiFilterString.Eq = source.Eq
244
+ apiFilterString.Gt = source.Gt
245
+ apiFilterString.Gte = source.Gte
246
+ apiFilterString.Ilike = source.Ilike
247
+ apiFilterString.In = source.In
248
+ apiFilterString.Like = source.Like
249
+ apiFilterString.Lt = source.Lt
250
+ apiFilterString.Lte = source.Lte
251
+ apiFilterString.Ne = source.Ne
252
+ apiFilterString.Nilike = source.Nilike
253
+ apiFilterString.Nin = source.Nin
254
+ apiFilterString.Nlike = source.Nlike
255
+ if source.Or != nil {
256
+ var apiFilterStringList2 []api.FilterString
257
+ if (*source.Or) != nil {
258
+ apiFilterStringList2 = make([]api.FilterString, len((*source.Or)))
259
+ for j := 0; j < len((*source.Or)); j++ {
260
+ apiFilterStringList2[j] = filterFilterStringToApiFilterString((*source.Or)[j])
261
+ }
262
+ }
263
+ apiFilterString.Or = &apiFilterStringList2
264
+ }
265
+ return apiFilterString
266
+ }
apiconverter/filter.go ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //go:generate go tool github.com/jmattheis/goverter/cmd/goverter gen ./
2
+ package apiconverter
3
+
4
+ import (
5
+ "github.com/openmeterio/openmeter/api"
6
+ "github.com/openmeterio/openmeter/pkg/filter"
7
+ )
8
+
9
+ // goverter:variables
10
+ // goverter:skipCopySameType
11
+ // goverter:output:file ./filter.gen.go
12
+ //
13
+ // The `goverter:ignore` directives below silence field-mismatch errors for
14
+ // fields that exist on the internal filter types but not on the v1 API types.
15
+ // If the v1 API (api/api.gen.go) is ever extended to expose those operators,
16
+ // remove the corresponding ignore entry so the generated converter copies
17
+ // them through — otherwise they will be silently dropped at the boundary.
18
+ var (
19
+ // Exists/Contains/Ncontains are internal-only; the v1 FilterString has no
20
+ // equivalent fields. Remove entries from the ignore list when v1 grows them.
21
+ // goverter:ignore Exists Contains Ncontains
22
+ ConvertString func(api.FilterString) filter.FilterString
23
+ ConvertStringPtr func(*api.FilterString) *filter.FilterString
24
+ ConvertStringMap func(map[string]api.FilterString) map[string]filter.FilterString
25
+ ConvertStringMapPtr func(*map[string]api.FilterString) *map[string]filter.FilterString
26
+ // goverter:ignoreMissing
27
+ ConvertIDExact func(api.FilterIDExact) filter.FilterString
28
+ ConvertIDExactPtr func(*api.FilterIDExact) *filter.FilterString
29
+ ConvertInt func(api.FilterInteger) filter.FilterInteger
30
+ ConvertIntPtr func(*api.FilterInteger) *filter.FilterInteger
31
+ ConvertFloat func(api.FilterFloat) filter.FilterFloat
32
+ ConvertFloatPtr func(*api.FilterFloat) *filter.FilterFloat
33
+ // FilterTime.Eq is new on the internal type; v1 api.FilterTime does not
34
+ // expose it. Remove this ignore when v1 grows an Eq field.
35
+ // goverter:ignore Eq
36
+ // goverter:ignore Exists
37
+ ConvertTime func(api.FilterTime) filter.FilterTime
38
+ ConvertTimePtr func(*api.FilterTime) *filter.FilterTime
39
+ ConvertBoolean func(api.FilterBoolean) filter.FilterBoolean
40
+ ConvertBooleanPtr func(*api.FilterBoolean) *filter.FilterBoolean
41
+ ConvertStringToAPI func(*filter.FilterString) *api.FilterString
42
+ ConvertStringMapToAPIPtr func(map[string]filter.FilterString) map[string]api.FilterString
43
+ )
app/adapter.go ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package app
2
+
3
+ import (
4
+ "context"
5
+
6
+ "github.com/openmeterio/openmeter/pkg/framework/entutils"
7
+ "github.com/openmeterio/openmeter/pkg/pagination"
8
+ )
9
+
10
+ type Adapter interface {
11
+ AppAdapter
12
+
13
+ entutils.TxCreator
14
+ }
15
+ type AppAdapter interface {
16
+ // Marketplace
17
+ RegisterMarketplaceListing(input RegisterMarketplaceListingInput) error
18
+ GetMarketplaceListing(ctx context.Context, input MarketplaceGetInput) (RegistryItem, error)
19
+ ListMarketplaceListings(ctx context.Context, input MarketplaceListInput) (pagination.Result[RegistryItem], error)
20
+ InstallMarketplaceListingWithAPIKey(ctx context.Context, input InstallAppWithAPIKeyInput) (App, error)
21
+ InstallMarketplaceListing(ctx context.Context, input InstallAppInput) (App, error)
22
+ GetMarketplaceListingOauth2InstallURL(ctx context.Context, input GetOauth2InstallURLInput) (GetOauth2InstallURLOutput, error)
23
+ AuthorizeMarketplaceListingOauth2Install(ctx context.Context, input AuthorizeOauth2InstallInput) error
24
+
25
+ // Installed app
26
+ CreateApp(ctx context.Context, input CreateAppInput) (AppBase, error)
27
+ GetApp(ctx context.Context, input GetAppInput) (App, error)
28
+ UpdateApp(ctx context.Context, input UpdateAppInput) (App, error)
29
+ ListApps(ctx context.Context, input ListAppInput) (pagination.Result[App], error)
30
+ UninstallApp(ctx context.Context, input UninstallAppInput) (*AppBase, error)
31
+ UpdateAppStatus(ctx context.Context, input UpdateAppStatusInput) error
32
+
33
+ // Customer data
34
+ ListCustomerData(ctx context.Context, input ListCustomerInput) (pagination.Result[CustomerApp], error)
35
+ EnsureCustomer(ctx context.Context, input EnsureCustomerInput) error
36
+ DeleteCustomer(ctx context.Context, input DeleteCustomerInput) error
37
+ }
app/adapter/adapter.go ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package appadapter
2
+
3
+ import (
4
+ "context"
5
+ "database/sql"
6
+ "errors"
7
+ "fmt"
8
+
9
+ "github.com/openmeterio/openmeter/openmeter/app"
10
+ entdb "github.com/openmeterio/openmeter/openmeter/ent/db"
11
+ "github.com/openmeterio/openmeter/pkg/framework/entutils"
12
+ "github.com/openmeterio/openmeter/pkg/framework/transaction"
13
+ )
14
+
15
+ type Config struct {
16
+ Client *entdb.Client
17
+ }
18
+
19
+ func (c Config) Validate() error {
20
+ if c.Client == nil {
21
+ return errors.New("ent client is required")
22
+ }
23
+
24
+ return nil
25
+ }
26
+
27
+ func New(config Config) (app.Adapter, error) {
28
+ if err := config.Validate(); err != nil {
29
+ return nil, err
30
+ }
31
+
32
+ adapter := &adapter{
33
+ db: config.Client,
34
+ registry: map[app.AppType]app.RegistryItem{},
35
+ }
36
+
37
+ return adapter, nil
38
+ }
39
+
40
+ var _ app.Adapter = (*adapter)(nil)
41
+
42
+ type adapter struct {
43
+ db *entdb.Client
44
+ registry map[app.AppType]app.RegistryItem
45
+ }
46
+
47
+ // Tx implements entutils.TxCreator interface
48
+ func (a *adapter) Tx(ctx context.Context) (context.Context, transaction.Driver, error) {
49
+ txCtx, rawConfig, eDriver, err := a.db.HijackTx(ctx, &sql.TxOptions{
50
+ ReadOnly: false,
51
+ })
52
+ if err != nil {
53
+ return nil, nil, fmt.Errorf("failed to hijack transaction: %w", err)
54
+ }
55
+ return txCtx, entutils.NewTxDriver(eDriver, rawConfig), nil
56
+ }
57
+
58
+ func (a *adapter) WithTx(ctx context.Context, tx *entutils.TxDriver) *adapter {
59
+ txClient := entdb.NewTxClientFromRawConfig(ctx, *tx.GetConfig())
60
+ return &adapter{
61
+ db: txClient.Client(),
62
+ registry: a.registry,
63
+ }
64
+ }
65
+
66
+ func (a *adapter) Self() *adapter {
67
+ return a
68
+ }
app/adapter/app.go ADDED
@@ -0,0 +1,283 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package appadapter
2
+
3
+ import (
4
+ "context"
5
+ "fmt"
6
+ "time"
7
+
8
+ "github.com/samber/lo"
9
+
10
+ "github.com/openmeterio/openmeter/openmeter/app"
11
+ "github.com/openmeterio/openmeter/openmeter/ent/db"
12
+ appdb "github.com/openmeterio/openmeter/openmeter/ent/db/app"
13
+ appcustomerdb "github.com/openmeterio/openmeter/openmeter/ent/db/appcustomer"
14
+ "github.com/openmeterio/openmeter/pkg/framework/entutils"
15
+ "github.com/openmeterio/openmeter/pkg/framework/transaction"
16
+ "github.com/openmeterio/openmeter/pkg/models"
17
+ "github.com/openmeterio/openmeter/pkg/pagination"
18
+ )
19
+
20
+ var _ app.AppAdapter = (*adapter)(nil)
21
+
22
+ // CreateApp creates an app
23
+ func (a *adapter) CreateApp(ctx context.Context, input app.CreateAppInput) (app.AppBase, error) {
24
+ return transaction.Run(ctx, a, func(ctx context.Context) (app.AppBase, error) {
25
+ return entutils.TransactingRepo(
26
+ ctx,
27
+ a,
28
+ func(ctx context.Context, repo *adapter) (app.AppBase, error) {
29
+ appCreateQuery := repo.db.App.Create().
30
+ SetNamespace(input.Namespace).
31
+ SetName(input.Name).
32
+ SetDescription(input.Description).
33
+ SetType(input.Type).
34
+ SetStatus(app.AppStatusReady)
35
+
36
+ // Set ID if provided by the input
37
+ if input.ID != nil {
38
+ appCreateQuery = appCreateQuery.SetID(input.ID.ID)
39
+ }
40
+
41
+ dbApp, err := appCreateQuery.Save(ctx)
42
+ if err != nil {
43
+ return app.AppBase{}, fmt.Errorf("failed to create app: %w", err)
44
+ }
45
+
46
+ // Get registry item
47
+ registryItem, err := repo.GetMarketplaceListing(ctx, app.MarketplaceGetInput{
48
+ Type: dbApp.Type,
49
+ })
50
+ if err != nil {
51
+ return app.AppBase{}, fmt.Errorf("failed to get listing for app %s: %w", dbApp.ID, err)
52
+ }
53
+
54
+ // Map app base from db
55
+ return mapAppBaseFromDB(dbApp, registryItem), nil
56
+ })
57
+ })
58
+ }
59
+
60
+ // UpdateAppStatus updates an app status
61
+ func (a *adapter) UpdateAppStatus(ctx context.Context, input app.UpdateAppStatusInput) error {
62
+ _, err := a.db.App.Update().
63
+ Where(appdb.Namespace(input.ID.Namespace)).
64
+ Where(appdb.ID(input.ID.ID)).
65
+ SetStatus(input.Status).
66
+ Save(ctx)
67
+ if err != nil {
68
+ return fmt.Errorf("failed to update app status: %w", err)
69
+ }
70
+
71
+ return nil
72
+ }
73
+
74
+ // ListApps lists apps
75
+ func (a *adapter) ListApps(ctx context.Context, params app.ListAppInput) (pagination.Result[app.App], error) {
76
+ return entutils.TransactingRepo(
77
+ ctx,
78
+ a,
79
+ func(ctx context.Context, repo *adapter) (pagination.Result[app.App], error) {
80
+ query := repo.db.App.
81
+ Query().
82
+ Where(appdb.Namespace(params.Namespace))
83
+
84
+ if params.Type != nil {
85
+ query = query.Where(appdb.Type(*params.Type))
86
+ }
87
+
88
+ // Do not return deleted apps by default
89
+ if !params.IncludeDeleted {
90
+ query = query.Where(appdb.DeletedAtIsNil())
91
+ }
92
+
93
+ // Only list apps that has customer data for the given customer
94
+ if params.CustomerID != nil {
95
+ query = query.Where(appdb.HasCustomerAppsWith(
96
+ appcustomerdb.CustomerID(params.CustomerID.ID),
97
+ appcustomerdb.DeletedAtIsNil(),
98
+ ))
99
+ }
100
+
101
+ // Only list apps that has the given app IDs
102
+ if len(params.AppIDs) > 0 {
103
+ appIDs := lo.Map(params.AppIDs, func(appID app.AppID, _ int) string {
104
+ return appID.ID
105
+ })
106
+
107
+ query = query.Where(appdb.IDIn(appIDs...))
108
+ }
109
+
110
+ response := pagination.Result[app.App]{
111
+ Page: params.Page,
112
+ }
113
+
114
+ paged, err := query.Paginate(ctx, params.Page)
115
+ if err != nil {
116
+ return response, err
117
+ }
118
+
119
+ result := make([]app.App, 0, len(paged.Items))
120
+ for _, dbApp := range paged.Items {
121
+ registryItem, err := repo.GetMarketplaceListing(ctx, app.MarketplaceGetInput{
122
+ Type: dbApp.Type,
123
+ })
124
+ if err != nil {
125
+ return response, fmt.Errorf("failed to get listing for app %s: %w", dbApp.ID, err)
126
+ }
127
+
128
+ app, err := mapAppFromDB(ctx, dbApp, registryItem)
129
+ if err != nil {
130
+ return response, fmt.Errorf("failed to map app %s from db: %w", dbApp.ID, err)
131
+ }
132
+
133
+ result = append(result, app)
134
+ }
135
+
136
+ response.TotalCount = paged.TotalCount
137
+ response.Items = result
138
+
139
+ return response, nil
140
+ },
141
+ )
142
+ }
143
+
144
+ // GetApp gets an app
145
+ func (a *adapter) GetApp(ctx context.Context, input app.GetAppInput) (app.App, error) {
146
+ return entutils.TransactingRepo(
147
+ ctx,
148
+ a,
149
+ func(ctx context.Context, repo *adapter) (app.App, error) {
150
+ dbApp, err := repo.db.App.Query().
151
+ Where(appdb.Namespace(input.Namespace)).
152
+ Where(appdb.ID(input.ID)).
153
+ First(ctx)
154
+ if err != nil {
155
+ if db.IsNotFound(err) {
156
+ return nil, app.NewAppNotFoundError(input)
157
+ }
158
+
159
+ return nil, err
160
+ }
161
+
162
+ // Get registry item
163
+ registryItem, err := repo.GetMarketplaceListing(ctx, app.MarketplaceGetInput{
164
+ Type: dbApp.Type,
165
+ })
166
+ if err != nil {
167
+ return nil, fmt.Errorf("failed to get listing for app %s: %w", dbApp.ID, err)
168
+ }
169
+
170
+ // Map app from db
171
+ app, err := mapAppFromDB(ctx, dbApp, registryItem)
172
+ if err != nil {
173
+ return nil, fmt.Errorf("failed to map app from db: %w", err)
174
+ }
175
+
176
+ return app, nil
177
+ },
178
+ )
179
+ }
180
+
181
+ // UpdateApp updates an app
182
+ func (a *adapter) UpdateApp(ctx context.Context, input app.UpdateAppInput) (app.App, error) {
183
+ return transaction.Run(ctx, a, func(ctx context.Context) (app.App, error) {
184
+ return entutils.TransactingRepo(
185
+ ctx,
186
+ a,
187
+ func(ctx context.Context, repo *adapter) (app.App, error) {
188
+ // Update the app
189
+ _, err := repo.db.App.Update().
190
+ Where(appdb.Namespace(input.AppID.Namespace)).
191
+ Where(appdb.ID(input.AppID.ID)).
192
+ SetName(input.Name).
193
+ SetOrClearDescription(input.Description).
194
+ SetOrClearMetadata(input.Metadata).
195
+ Save(ctx)
196
+ if err != nil {
197
+ return nil, fmt.Errorf("failed to update the app with id %s: %w", input.AppID.ID, err)
198
+ }
199
+
200
+ // Get the updated app
201
+ app, err := a.GetApp(ctx, input.AppID)
202
+ if err != nil {
203
+ return nil, fmt.Errorf("failed to get updated app: %s: %w", input.AppID.ID, err)
204
+ }
205
+
206
+ return app, nil
207
+ })
208
+ })
209
+ }
210
+
211
+ // UninstallApp uninstalls an app
212
+ func (a *adapter) UninstallApp(ctx context.Context, input app.UninstallAppInput) (*app.AppBase, error) {
213
+ return transaction.Run(ctx, a, func(ctx context.Context) (*app.AppBase, error) {
214
+ return entutils.TransactingRepo(ctx, a, func(ctx context.Context, repo *adapter) (*app.AppBase, error) {
215
+ installedApp, err := repo.GetApp(ctx, input)
216
+ if err != nil {
217
+ return nil, fmt.Errorf("failed to get app: %w", err)
218
+ }
219
+
220
+ // Get app factory through registry
221
+ registryItem, err := repo.GetMarketplaceListing(ctx, app.MarketplaceGetInput{
222
+ Type: installedApp.GetType(),
223
+ })
224
+ if err != nil {
225
+ return nil, fmt.Errorf("failed to get listing for app: %w", err)
226
+ }
227
+
228
+ // Uninstall app through factory
229
+ err = registryItem.Factory.UninstallApp(ctx, installedApp.GetID())
230
+ if err != nil {
231
+ return nil, fmt.Errorf("failed to uninstall app: %w", err)
232
+ }
233
+
234
+ deletedAt := time.Now()
235
+
236
+ // Delete app from database
237
+ _, err = repo.db.App.Update().
238
+ Where(appdb.Namespace(input.Namespace)).
239
+ Where(appdb.ID(input.ID)).
240
+ SetDeletedAt(time.Now()).
241
+ Save(ctx)
242
+ if err != nil {
243
+ return nil, fmt.Errorf("failed to delete app from database: %w", err)
244
+ }
245
+
246
+ appBase := installedApp.GetAppBase()
247
+ appBase.DeletedAt = &deletedAt
248
+
249
+ return &appBase, nil
250
+ })
251
+ })
252
+ }
253
+
254
+ // mapAppBaseFromDB maps an app base from the database
255
+ func mapAppBaseFromDB(dbApp *db.App, registryItem app.RegistryItem) app.AppBase {
256
+ return app.AppBase{
257
+ ManagedResource: models.NewManagedResource(models.ManagedResourceInput{
258
+ ID: dbApp.ID,
259
+ Namespace: dbApp.Namespace,
260
+ CreatedAt: dbApp.CreatedAt,
261
+ UpdatedAt: dbApp.UpdatedAt,
262
+ DeletedAt: dbApp.DeletedAt,
263
+ Name: dbApp.Name,
264
+ Description: dbApp.Description,
265
+ }),
266
+ Type: dbApp.Type,
267
+ Status: dbApp.Status,
268
+ Listing: registryItem.Listing,
269
+ Metadata: dbApp.Metadata,
270
+ }
271
+ }
272
+
273
+ // mapAppFromDB maps an app from the database
274
+ func mapAppFromDB(ctx context.Context, dbApp *db.App, registryItem app.RegistryItem) (app.App, error) {
275
+ appBase := mapAppBaseFromDB(dbApp, registryItem)
276
+
277
+ app, err := registryItem.Factory.NewApp(ctx, appBase)
278
+ if err != nil {
279
+ return app, fmt.Errorf("failed to create app with %s factory: %w", appBase.Type, err)
280
+ }
281
+
282
+ return app, nil
283
+ }
app/adapter/customer.go ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package appadapter
2
+
3
+ import (
4
+ "context"
5
+ "fmt"
6
+ "time"
7
+
8
+ "entgo.io/ent/dialect/sql"
9
+
10
+ "github.com/openmeterio/openmeter/openmeter/app"
11
+ "github.com/openmeterio/openmeter/openmeter/ent/db"
12
+ appcustomerdb "github.com/openmeterio/openmeter/openmeter/ent/db/appcustomer"
13
+ "github.com/openmeterio/openmeter/pkg/framework/entutils"
14
+ "github.com/openmeterio/openmeter/pkg/framework/transaction"
15
+ "github.com/openmeterio/openmeter/pkg/models"
16
+ "github.com/openmeterio/openmeter/pkg/pagination"
17
+ )
18
+
19
+ var _ app.AppAdapter = (*adapter)(nil)
20
+
21
+ // ListCustomerData lists app customer data
22
+ func (a *adapter) ListCustomerData(ctx context.Context, input app.ListCustomerInput) (pagination.Result[app.CustomerApp], error) {
23
+ if err := input.Validate(); err != nil {
24
+ return pagination.Result[app.CustomerApp]{}, models.NewGenericValidationError(
25
+ fmt.Errorf("error listing customer data: %w", err),
26
+ )
27
+ }
28
+
29
+ listInput := app.ListAppInput{
30
+ Page: input.Page,
31
+ Namespace: input.CustomerID.Namespace,
32
+ CustomerID: &input.CustomerID,
33
+ Type: input.Type,
34
+ }
35
+
36
+ if input.AppID != nil {
37
+ listInput.AppIDs = []app.AppID{*input.AppID}
38
+ }
39
+
40
+ apps, err := a.ListApps(ctx, listInput)
41
+ if err != nil {
42
+ return pagination.Result[app.CustomerApp]{}, fmt.Errorf("failed to list apps: %w", err)
43
+ }
44
+
45
+ response := pagination.Result[app.CustomerApp]{
46
+ Page: input.Page,
47
+ TotalCount: apps.TotalCount,
48
+ Items: make([]app.CustomerApp, 0, len(apps.Items)),
49
+ }
50
+
51
+ for _, customerApp := range apps.Items {
52
+ customerData, err := customerApp.GetCustomerData(ctx, app.GetAppInstanceCustomerDataInput{
53
+ CustomerID: input.CustomerID,
54
+ })
55
+ if err != nil {
56
+ return pagination.Result[app.CustomerApp]{}, fmt.Errorf("failed to get customer data for app %s: %w", customerApp.GetID().ID, err)
57
+ }
58
+
59
+ response.Items = append(response.Items, app.CustomerApp{
60
+ App: customerApp,
61
+ CustomerData: customerData,
62
+ })
63
+ }
64
+
65
+ return response, nil
66
+ }
67
+
68
+ // EnsureCustomer upserts app customer relationship:
69
+ // If the app or customer does not exist, an error is returned
70
+ // If the app customer relationship already exists, nothing is done
71
+ // If the app customer relationship is deleted, it is restored
72
+ func (a *adapter) EnsureCustomer(ctx context.Context, input app.EnsureCustomerInput) error {
73
+ return transaction.RunWithNoValue(ctx, a, func(ctx context.Context) error {
74
+ if err := input.Validate(); err != nil {
75
+ return models.NewGenericValidationError(
76
+ err,
77
+ )
78
+ }
79
+
80
+ _, err := entutils.TransactingRepo(
81
+ ctx,
82
+ a,
83
+ func(ctx context.Context, repo *adapter) (any, error) {
84
+ // Upsert customer data for the app
85
+ err := repo.db.AppCustomer.
86
+ Create().
87
+ SetNamespace(input.AppID.Namespace).
88
+ SetAppID(input.AppID.ID).
89
+ SetCustomerID(input.CustomerID.ID).
90
+ SetNillableDeletedAt(nil).
91
+ // Upsert
92
+ OnConflict(
93
+ sql.ConflictColumns(
94
+ appcustomerdb.FieldNamespace,
95
+ appcustomerdb.FieldAppID,
96
+ appcustomerdb.FieldCustomerID,
97
+ ),
98
+ sql.ConflictWhere(sql.IsNull(appcustomerdb.FieldDeletedAt)),
99
+ ).
100
+ UpdateDeletedAt().
101
+ Exec(ctx)
102
+ if err != nil {
103
+ // TODO: differentiate between app or customer not found
104
+ // When the constraint error is returned, it means that the app or customer does not exist.
105
+ if db.IsConstraintError(err) {
106
+ return nil, app.NewAppNotFoundError(input.AppID)
107
+ }
108
+
109
+ // TODO (pmarton): This is a workaround for the issue where DoNothing() returns an error when no rows are affected.
110
+ // See: https://github.com/ent/ent/issues/1821
111
+ if err.Error() == "sql: no rows in result set" {
112
+ return nil, nil
113
+ }
114
+
115
+ return nil, fmt.Errorf("failed to upsert app customer: %w", err)
116
+ }
117
+
118
+ return nil, nil
119
+ },
120
+ )
121
+
122
+ return err
123
+ })
124
+ }
125
+
126
+ // DeleteCustomer deletes app customer
127
+ func (a *adapter) DeleteCustomer(ctx context.Context, input app.DeleteCustomerInput) error {
128
+ return transaction.RunWithNoValue(ctx, a, func(ctx context.Context) error {
129
+ if err := input.Validate(); err != nil {
130
+ return models.NewGenericValidationError(
131
+ fmt.Errorf("error delete customer: %w", err),
132
+ )
133
+ }
134
+
135
+ // Determine namespace
136
+ var namespace string
137
+
138
+ if input.AppID != nil {
139
+ namespace = input.AppID.Namespace
140
+ }
141
+
142
+ if input.CustomerID != nil {
143
+ namespace = input.CustomerID.Namespace
144
+ }
145
+
146
+ if namespace == "" {
147
+ return models.NewGenericValidationError(
148
+ fmt.Errorf("error delete customer: namespace is empty"),
149
+ )
150
+ }
151
+
152
+ _, err := entutils.TransactingRepo(ctx, a, func(ctx context.Context, repo *adapter) (any, error) {
153
+ // Delete app customer
154
+ query := repo.db.AppCustomer.
155
+ Update().
156
+ SetDeletedAt(time.Now()).
157
+ Where(
158
+ appcustomerdb.Namespace(namespace),
159
+ )
160
+
161
+ if input.AppID != nil {
162
+ query = query.Where(appcustomerdb.AppID(input.AppID.ID))
163
+ }
164
+
165
+ if input.CustomerID != nil {
166
+ query = query.Where(appcustomerdb.CustomerID(input.CustomerID.ID))
167
+ }
168
+
169
+ _, err := query.Save(ctx)
170
+ if err != nil {
171
+ return nil, fmt.Errorf("failed to delete app customer: %w", err)
172
+ }
173
+
174
+ return nil, nil
175
+ })
176
+ return err
177
+ })
178
+ }
app/adapter/marketplace.go ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package appadapter
2
+
3
+ import (
4
+ "context"
5
+ "fmt"
6
+
7
+ "github.com/samber/lo"
8
+
9
+ "github.com/openmeterio/openmeter/openmeter/app"
10
+ "github.com/openmeterio/openmeter/pkg/framework/transaction"
11
+ "github.com/openmeterio/openmeter/pkg/models"
12
+ "github.com/openmeterio/openmeter/pkg/pagination"
13
+ )
14
+
15
+ // ListMarketplaceListings lists marketplace listings
16
+ func (a adapter) ListMarketplaceListings(ctx context.Context, input app.MarketplaceListInput) (pagination.Result[app.RegistryItem], error) {
17
+ items := lo.Values(a.registry)
18
+ items = lo.Subset(items, (input.PageNumber-1)*input.PageSize, uint(input.PageSize))
19
+
20
+ response := pagination.Result[app.RegistryItem]{
21
+ Page: input.Page,
22
+ Items: items,
23
+ TotalCount: len(a.registry),
24
+ }
25
+
26
+ return response, nil
27
+ }
28
+
29
+ // GetMarketplaceListing gets a marketplace listing
30
+ func (a adapter) GetMarketplaceListing(ctx context.Context, input app.MarketplaceGetInput) (app.RegistryItem, error) {
31
+ if _, ok := a.registry[input.Type]; !ok {
32
+ return app.RegistryItem{}, models.NewGenericNotFoundError(
33
+ fmt.Errorf("listing with type not found: %s", input.Type),
34
+ )
35
+ }
36
+
37
+ return a.registry[input.Type], nil
38
+ }
39
+
40
+ // InstallMarketplaceListingWithAPIKey installs an app with an API key
41
+ func (a *adapter) InstallMarketplaceListingWithAPIKey(ctx context.Context, input app.InstallAppWithAPIKeyInput) (app.App, error) {
42
+ return transaction.Run(ctx, a, func(ctx context.Context) (app.App, error) {
43
+ // Get registry item
44
+ registryItem, err := a.GetMarketplaceListing(ctx, app.MarketplaceGetInput{
45
+ Type: input.Type,
46
+ })
47
+ if err != nil {
48
+ return nil, fmt.Errorf("failed to get listing for app %s: %w", input.Type, err)
49
+ }
50
+
51
+ name, ok := lo.Coalesce(input.Name, registryItem.Listing.Name)
52
+ if !ok {
53
+ return nil, fmt.Errorf("name is required, listing doesn't have a name either")
54
+ }
55
+
56
+ installer, ok := registryItem.Factory.(app.AppFactoryInstallWithAPIKey)
57
+ if !ok {
58
+ return nil, models.NewGenericValidationError(fmt.Errorf("app does not support this installation method. Supported methods: %v", registryItem.Listing.InstallMethods))
59
+ }
60
+
61
+ // Install app
62
+ app, err := installer.InstallAppWithAPIKey(ctx, app.AppFactoryInstallAppWithAPIKeyInput{
63
+ Namespace: input.Namespace,
64
+ APIKey: input.APIKey,
65
+ Name: name,
66
+ })
67
+ if err != nil {
68
+ return nil, fmt.Errorf("failed to install app: %w", err)
69
+ }
70
+
71
+ return app, nil
72
+ })
73
+ }
74
+
75
+ // InstallMarketplaceListing installs an app
76
+ func (a *adapter) InstallMarketplaceListing(ctx context.Context, input app.InstallAppInput) (app.App, error) {
77
+ return transaction.Run(ctx, a, func(ctx context.Context) (app.App, error) {
78
+ // Get registry item
79
+ registryItem, err := a.GetMarketplaceListing(ctx, app.MarketplaceGetInput{
80
+ Type: input.Type,
81
+ })
82
+ if err != nil {
83
+ return nil, fmt.Errorf("failed to get listing for app %s: %w", input.Type, err)
84
+ }
85
+
86
+ name, ok := lo.Coalesce(input.Name, registryItem.Listing.Name)
87
+ if !ok {
88
+ return nil, fmt.Errorf("name is required, listing doesn't have a name either")
89
+ }
90
+
91
+ installer, ok := registryItem.Factory.(app.AppFactoryInstall)
92
+ if !ok {
93
+ return nil, models.NewGenericValidationError(fmt.Errorf("app does not support this installation method. Supported methods: %v", registryItem.Listing.InstallMethods))
94
+ }
95
+
96
+ // Install app
97
+ app, err := installer.InstallApp(ctx, app.AppFactoryInstallAppInput{
98
+ Namespace: input.Namespace,
99
+ Name: name,
100
+ })
101
+ if err != nil {
102
+ return nil, fmt.Errorf("failed to install app: %w", err)
103
+ }
104
+
105
+ return app, nil
106
+ })
107
+ }
108
+
109
+ // GetMarketplaceListingOauth2InstallURL gets an OAuth2 install URL
110
+ func (a adapter) GetMarketplaceListingOauth2InstallURL(ctx context.Context, input app.GetOauth2InstallURLInput) (app.GetOauth2InstallURLOutput, error) {
111
+ return app.GetOauth2InstallURLOutput{}, fmt.Errorf("not implemented")
112
+ }
113
+
114
+ // AuthorizeOauth2Install authorizes an OAuth2 install
115
+ func (a adapter) AuthorizeMarketplaceListingOauth2Install(ctx context.Context, input app.AuthorizeOauth2InstallInput) error {
116
+ return fmt.Errorf("not implemented")
117
+ }
118
+
119
+ // RegisterMarketplaceListing registers an app type
120
+ func (a adapter) RegisterMarketplaceListing(input app.RegisterMarketplaceListingInput) error {
121
+ if _, ok := a.registry[input.Listing.Type]; ok {
122
+ return fmt.Errorf("marketplace listing with key %s already exists", input.Listing.Type)
123
+ }
124
+
125
+ if err := input.Listing.Validate(); err != nil {
126
+ return fmt.Errorf("marketplace listing with key %s is invalid: %w", input.Listing.Type, err)
127
+ }
128
+
129
+ a.registry[input.Listing.Type] = input
130
+
131
+ return nil
132
+ }
app/app.go ADDED
@@ -0,0 +1,218 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package app
2
+
3
+ import (
4
+ "context"
5
+ "errors"
6
+ "fmt"
7
+
8
+ "github.com/openmeterio/openmeter/openmeter/customer"
9
+ "github.com/openmeterio/openmeter/pkg/models"
10
+ "github.com/openmeterio/openmeter/pkg/pagination"
11
+ )
12
+
13
+ // App represents an installed app
14
+ type App interface {
15
+ GetAppBase() AppBase
16
+ GetID() AppID
17
+ GetType() AppType
18
+ GetName() string
19
+ GetDescription() *string
20
+ GetStatus() AppStatus
21
+ GetMetadata() models.Metadata
22
+ GetListing() MarketplaceListing
23
+
24
+ GetEventAppData() (EventAppData, error)
25
+
26
+ UpdateAppConfig(ctx context.Context, input AppConfigUpdate) error
27
+
28
+ // ValidateCapabilities validates if the app can run for the given capabilities
29
+ ValidateCapabilities(capabilities ...CapabilityType) error
30
+
31
+ // Customer data
32
+ GetCustomerData(ctx context.Context, input GetAppInstanceCustomerDataInput) (CustomerData, error)
33
+ UpsertCustomerData(ctx context.Context, input UpsertAppInstanceCustomerDataInput) error
34
+ DeleteCustomerData(ctx context.Context, input DeleteAppInstanceCustomerDataInput) error
35
+ }
36
+
37
+ type GetAppInstanceCustomerDataInput struct {
38
+ CustomerID customer.CustomerID
39
+ }
40
+
41
+ func (i GetAppInstanceCustomerDataInput) Validate() error {
42
+ if err := i.CustomerID.Validate(); err != nil {
43
+ return err
44
+ }
45
+
46
+ return nil
47
+ }
48
+
49
+ type UpsertAppInstanceCustomerDataInput struct {
50
+ CustomerID customer.CustomerID
51
+ Data CustomerData
52
+ }
53
+
54
+ func (i UpsertAppInstanceCustomerDataInput) Validate() error {
55
+ if err := i.CustomerID.Validate(); err != nil {
56
+ return err
57
+ }
58
+
59
+ if err := i.Data.Validate(); err != nil {
60
+ return err
61
+ }
62
+
63
+ return nil
64
+ }
65
+
66
+ type DeleteAppInstanceCustomerDataInput struct {
67
+ CustomerID customer.CustomerID
68
+ }
69
+
70
+ func (i DeleteAppInstanceCustomerDataInput) Validate() error {
71
+ if err := i.CustomerID.Validate(); err != nil {
72
+ return err
73
+ }
74
+
75
+ return nil
76
+ }
77
+
78
+ // GetAppInput is the input for getting an installed app
79
+ type GetAppInput = AppID
80
+
81
+ type AppConfigUpdate interface {
82
+ models.Validator
83
+ }
84
+
85
+ // UpdateAppInput is the input for setting an app as default for a type
86
+ type UpdateAppInput struct {
87
+ AppID AppID
88
+ Name string
89
+ Description *string
90
+ Default bool
91
+ Metadata *map[string]string
92
+ AppConfigUpdate AppConfigUpdate
93
+ }
94
+
95
+ func (i UpdateAppInput) Validate() error {
96
+ if err := i.AppID.Validate(); err != nil {
97
+ return fmt.Errorf("error validating app ID: %w", err)
98
+ }
99
+
100
+ // Required fields
101
+ if i.Name == "" {
102
+ return errors.New("name is required")
103
+ }
104
+
105
+ if i.Metadata != nil {
106
+ for k, v := range *i.Metadata {
107
+ if k == "" {
108
+ return errors.New("metadata key is required")
109
+ }
110
+
111
+ if v == "" {
112
+ return errors.New("metadata value is required")
113
+ }
114
+ }
115
+ }
116
+
117
+ if i.AppConfigUpdate != nil {
118
+ if err := i.AppConfigUpdate.Validate(); err != nil {
119
+ return fmt.Errorf("error validating app entity update: %w", err)
120
+ }
121
+ }
122
+
123
+ return nil
124
+ }
125
+
126
+ // CreateAppInput is the input for creating an app
127
+ type CreateAppInput struct {
128
+ // AppID is optional. If not provided, a new AppID will be generated by the database
129
+ ID *AppID
130
+ Namespace string
131
+ Name string
132
+ Description string
133
+ Type AppType
134
+ }
135
+
136
+ func (i CreateAppInput) Validate() error {
137
+ if i.Namespace == "" {
138
+ return errors.New("namespace is required")
139
+ }
140
+
141
+ if i.Name == "" {
142
+ return errors.New("name is required")
143
+ }
144
+
145
+ return nil
146
+ }
147
+
148
+ // ListAppInput is the input for listing installed apps
149
+ type ListAppInput struct {
150
+ Namespace string
151
+ pagination.Page
152
+
153
+ AppIDs []AppID
154
+ Type *AppType
155
+ IncludeDeleted bool
156
+ // Only list apps that has data for the given customer
157
+ CustomerID *customer.CustomerID
158
+ }
159
+
160
+ func (i ListAppInput) Validate() error {
161
+ var errs []error
162
+
163
+ if i.Namespace == "" {
164
+ errs = append(errs, models.NewGenericValidationError(
165
+ errors.New("namespace is required"),
166
+ ))
167
+ }
168
+
169
+ if i.CustomerID != nil {
170
+ if err := i.CustomerID.Validate(); err != nil {
171
+ errs = append(errs, models.NewGenericValidationError(
172
+ fmt.Errorf("error validating customer id: %w", err),
173
+ ))
174
+ }
175
+
176
+ if i.CustomerID.Namespace != i.Namespace {
177
+ errs = append(errs, models.NewGenericValidationError(
178
+ fmt.Errorf("customer id namespace %s does not match app namespace %s", i.CustomerID.Namespace, i.Namespace),
179
+ ))
180
+ }
181
+ }
182
+
183
+ if len(i.AppIDs) > 0 {
184
+ for _, appID := range i.AppIDs {
185
+ if appID.Namespace != i.Namespace {
186
+ errs = append(errs, models.NewGenericValidationError(
187
+ fmt.Errorf("app id namespace %s does not match app namespace %s", appID.Namespace, i.Namespace),
188
+ ))
189
+ }
190
+
191
+ if err := appID.Validate(); err != nil {
192
+ errs = append(errs, models.NewGenericValidationError(
193
+ fmt.Errorf("error validating app id: %w", err),
194
+ ))
195
+ }
196
+ }
197
+ }
198
+
199
+ return errors.Join(errs...)
200
+ }
201
+
202
+ // UpdateAppStatusInput is the input for updating an app status
203
+ type UpdateAppStatusInput struct {
204
+ ID AppID
205
+ Status AppStatus
206
+ }
207
+
208
+ func (i UpdateAppStatusInput) Validate() error {
209
+ if err := i.ID.Validate(); err != nil {
210
+ return err
211
+ }
212
+
213
+ if i.Status == "" {
214
+ return errors.New("status is required")
215
+ }
216
+
217
+ return nil
218
+ }
app/appbase.go ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package app
2
+
3
+ import (
4
+ "errors"
5
+ "fmt"
6
+
7
+ "github.com/openmeterio/openmeter/pkg/models"
8
+ )
9
+
10
+ // AppType represents the type of an app
11
+ type AppType string
12
+
13
+ const (
14
+ AppTypeStripe AppType = "stripe"
15
+ AppTypeSandbox AppType = "sandbox"
16
+ AppTypeCustomInvoicing AppType = "custom_invoicing"
17
+ )
18
+
19
+ func (t AppType) Validate() error {
20
+ switch t {
21
+ case AppTypeStripe, AppTypeSandbox, AppTypeCustomInvoicing:
22
+ return nil
23
+ default:
24
+ return models.NewGenericValidationError(fmt.Errorf("invalid app type: %s", t))
25
+ }
26
+ }
27
+
28
+ // AppStatus represents the status of an app
29
+ type AppStatus string
30
+
31
+ const (
32
+ AppStatusReady AppStatus = "ready"
33
+ AppStatusUnauthorized AppStatus = "unauthorized"
34
+ )
35
+
36
+ type CapabilityType string
37
+
38
+ const (
39
+ CapabilityTypeReportUsage CapabilityType = "reportUsage"
40
+ CapabilityTypeReportEvents CapabilityType = "reportEvents"
41
+ CapabilityTypeCalculateTax CapabilityType = "calculateTax"
42
+ CapabilityTypeInvoiceCustomers CapabilityType = "invoiceCustomers"
43
+ CapabilityTypeCollectPayments CapabilityType = "collectPayments"
44
+ )
45
+
46
+ // AppBase represents an abstract with the base fields of an app
47
+ type AppBase struct {
48
+ models.ManagedResource
49
+
50
+ Type AppType `json:"type"`
51
+ Status AppStatus `json:"status"`
52
+ Listing MarketplaceListing `json:"listing"`
53
+ Metadata models.Metadata `json:"metadata,omitempty"`
54
+ }
55
+
56
+ func (a AppBase) GetAppBase() AppBase {
57
+ return a
58
+ }
59
+
60
+ func (a AppBase) GetID() AppID {
61
+ return AppID{
62
+ Namespace: a.Namespace,
63
+ ID: a.ID,
64
+ }
65
+ }
66
+
67
+ func (a AppBase) GetType() AppType {
68
+ return a.Type
69
+ }
70
+
71
+ func (a AppBase) GetName() string {
72
+ return a.Name
73
+ }
74
+
75
+ func (a AppBase) GetDescription() *string {
76
+ return a.Description
77
+ }
78
+
79
+ func (a AppBase) GetStatus() AppStatus {
80
+ return a.Status
81
+ }
82
+
83
+ func (a AppBase) GetListing() MarketplaceListing {
84
+ return a.Listing
85
+ }
86
+
87
+ func (a AppBase) GetMetadata() models.Metadata {
88
+ return a.Metadata
89
+ }
90
+
91
+ // ValidateCapabilities validates if the app can run for the given capabilities
92
+ func (a AppBase) ValidateCapabilities(capabilities ...CapabilityType) error {
93
+ for _, capability := range capabilities {
94
+ found := false
95
+
96
+ for _, c := range a.Listing.Capabilities {
97
+ if c.Type == capability {
98
+ found = true
99
+ break
100
+ }
101
+ }
102
+
103
+ if !found {
104
+ return fmt.Errorf("capability %s is not supported by %s app type", capability, a.Type)
105
+ }
106
+ }
107
+
108
+ return nil
109
+ }
110
+
111
+ // ValidateCustomer validates if the app can run for the given customer
112
+ // func (a AppBase) ValidateCustomer(c customerentity.Customer, capabilities []CapabilityType) error {
113
+ // return fmt.Errorf("each app must implement its own ValidateCustomer method")
114
+ // }
115
+
116
+ // Validate validates the app base
117
+ func (a AppBase) Validate() error {
118
+ if err := a.ManagedResource.Validate(); err != nil {
119
+ return fmt.Errorf("error validating managed resource: %w", err)
120
+ }
121
+
122
+ if a.ID == "" {
123
+ return errors.New("id is required")
124
+ }
125
+
126
+ if a.Namespace == "" {
127
+ return errors.New("namespace is required")
128
+ }
129
+
130
+ if a.Name == "" {
131
+ return errors.New("name is required")
132
+ }
133
+
134
+ if a.Status == "" {
135
+ return errors.New("status is required")
136
+ }
137
+
138
+ if err := a.Listing.Validate(); err != nil {
139
+ return fmt.Errorf("error validating listing: %w", err)
140
+ }
141
+
142
+ return nil
143
+ }
144
+
145
+ // AppID represents the unique identifier for an installed app
146
+ type AppID struct {
147
+ Namespace string
148
+ ID string
149
+ }
150
+
151
+ func (i AppID) Validate() error {
152
+ if i.Namespace == "" {
153
+ return errors.New("namespace is required")
154
+ }
155
+
156
+ if i.ID == "" {
157
+ return errors.New("id is required")
158
+ }
159
+
160
+ return nil
161
+ }
app/customer.go ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ package app
2
+
3
+ type CustomerData interface {
4
+ Validate() error
5
+ }
6
+
7
+ type CustomerApp struct {
8
+ App App
9
+ CustomerData CustomerData
10
+ }
app/custominvoicing/adapter.go ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package appcustominvoicing
2
+
3
+ import (
4
+ "context"
5
+
6
+ "github.com/openmeterio/openmeter/openmeter/app"
7
+ "github.com/openmeterio/openmeter/pkg/framework/entutils"
8
+ )
9
+
10
+ type Adapter interface {
11
+ CustomerDataAdapter
12
+ AppConfigAdapter
13
+
14
+ entutils.TxCreator
15
+ }
16
+
17
+ type CustomerDataAdapter interface {
18
+ GetCustomerData(ctx context.Context, input GetAppCustomerDataInput) (CustomerData, error)
19
+ UpsertCustomerData(ctx context.Context, input UpsertCustomerDataInput) error
20
+ DeleteCustomerData(ctx context.Context, input DeleteAppCustomerDataInput) error
21
+ }
22
+
23
+ type AppConfigAdapter interface {
24
+ GetAppConfiguration(ctx context.Context, input app.AppID) (Configuration, error)
25
+ UpsertAppConfiguration(ctx context.Context, input UpsertAppConfigurationInput) error
26
+ DeleteAppConfiguration(ctx context.Context, input app.AppID) error
27
+ }
app/custominvoicing/adapter/adapter.go ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package adapter
2
+
3
+ import (
4
+ "context"
5
+ "database/sql"
6
+ "errors"
7
+ "fmt"
8
+ "log/slog"
9
+
10
+ appcustominvoicing "github.com/openmeterio/openmeter/openmeter/app/custominvoicing"
11
+ entdb "github.com/openmeterio/openmeter/openmeter/ent/db"
12
+ "github.com/openmeterio/openmeter/pkg/framework/entutils"
13
+ "github.com/openmeterio/openmeter/pkg/framework/transaction"
14
+ )
15
+
16
+ type Config struct {
17
+ Client *entdb.Client
18
+ Logger *slog.Logger
19
+ }
20
+
21
+ func (c Config) Validate() error {
22
+ if c.Client == nil {
23
+ return errors.New("ent client is required")
24
+ }
25
+
26
+ if c.Logger == nil {
27
+ return errors.New("logger is required")
28
+ }
29
+
30
+ return nil
31
+ }
32
+
33
+ func New(config Config) (appcustominvoicing.Adapter, error) {
34
+ if err := config.Validate(); err != nil {
35
+ return nil, err
36
+ }
37
+
38
+ return &adapter{
39
+ db: config.Client,
40
+ logger: config.Logger,
41
+ }, nil
42
+ }
43
+
44
+ var _ appcustominvoicing.Adapter = (*adapter)(nil)
45
+
46
+ type adapter struct {
47
+ db *entdb.Client
48
+ logger *slog.Logger
49
+ }
50
+
51
+ func (a *adapter) Tx(ctx context.Context) (context.Context, transaction.Driver, error) {
52
+ txCtx, rawConfig, eDriver, err := a.db.HijackTx(ctx, &sql.TxOptions{
53
+ ReadOnly: false,
54
+ })
55
+ if err != nil {
56
+ return nil, nil, fmt.Errorf("failed to hijack transaction: %w", err)
57
+ }
58
+ return txCtx, entutils.NewTxDriver(eDriver, rawConfig), nil
59
+ }
60
+
61
+ func (a *adapter) WithTx(ctx context.Context, tx *entutils.TxDriver) *adapter {
62
+ txDb := entdb.NewTxClientFromRawConfig(ctx, *tx.GetConfig())
63
+
64
+ return &adapter{
65
+ db: txDb.Client(),
66
+ logger: a.logger,
67
+ }
68
+ }
69
+
70
+ func (a *adapter) Self() *adapter {
71
+ return a
72
+ }
app/custominvoicing/adapter/appconfig.go ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package adapter
2
+
3
+ import (
4
+ "context"
5
+ "time"
6
+
7
+ "github.com/openmeterio/openmeter/openmeter/app"
8
+ custominvoicing "github.com/openmeterio/openmeter/openmeter/app/custominvoicing"
9
+ "github.com/openmeterio/openmeter/openmeter/ent/db"
10
+ "github.com/openmeterio/openmeter/openmeter/ent/db/appcustominvoicing"
11
+ "github.com/openmeterio/openmeter/pkg/framework/entutils"
12
+ )
13
+
14
+ var _ custominvoicing.AppConfigAdapter = (*adapter)(nil)
15
+
16
+ func (a *adapter) GetAppConfiguration(ctx context.Context, input app.AppID) (custominvoicing.Configuration, error) {
17
+ return entutils.TransactingRepo(ctx, a, func(ctx context.Context, tx *adapter) (custominvoicing.Configuration, error) {
18
+ appConfig, err := tx.db.AppCustomInvoicing.Query().
19
+ Where(
20
+ appcustominvoicing.ID(input.ID),
21
+ appcustominvoicing.Namespace(input.Namespace),
22
+ appcustominvoicing.DeletedAtIsNil(),
23
+ ).
24
+ First(ctx)
25
+ if err != nil {
26
+ if db.IsNotFound(err) {
27
+ return custominvoicing.Configuration{}, nil
28
+ }
29
+
30
+ return custominvoicing.Configuration{}, err
31
+ }
32
+
33
+ return mapDBToAppConfiguration(appConfig), nil
34
+ })
35
+ }
36
+
37
+ func (a *adapter) UpsertAppConfiguration(ctx context.Context, input custominvoicing.UpsertAppConfigurationInput) error {
38
+ return entutils.TransactingRepoWithNoValue(ctx, a, func(ctx context.Context, tx *adapter) error {
39
+ return tx.db.AppCustomInvoicing.Create().
40
+ SetID(input.AppID.ID).
41
+ SetNamespace(input.AppID.Namespace).
42
+ SetEnableDraftSyncHook(input.Configuration.EnableDraftSyncHook).
43
+ SetEnableIssuingSyncHook(input.Configuration.EnableIssuingSyncHook).
44
+
45
+ // Upsert
46
+ OnConflictColumns(appcustominvoicing.FieldID, appcustominvoicing.FieldNamespace).
47
+ UpdateNewValues().
48
+ Exec(ctx)
49
+ })
50
+ }
51
+
52
+ func (a *adapter) DeleteAppConfiguration(ctx context.Context, input app.AppID) error {
53
+ return entutils.TransactingRepoWithNoValue(ctx, a, func(ctx context.Context, tx *adapter) error {
54
+ return tx.db.AppCustomInvoicing.Update().
55
+ Where(
56
+ appcustominvoicing.ID(input.ID),
57
+ appcustominvoicing.Namespace(input.Namespace),
58
+ appcustominvoicing.DeletedAtIsNil(),
59
+ ).
60
+ SetDeletedAt(time.Now()).
61
+ Exec(ctx)
62
+ })
63
+ }
64
+
65
+ func mapDBToAppConfiguration(appConfig *db.AppCustomInvoicing) custominvoicing.Configuration {
66
+ return custominvoicing.Configuration{
67
+ EnableDraftSyncHook: appConfig.EnableDraftSyncHook,
68
+ EnableIssuingSyncHook: appConfig.EnableIssuingSyncHook,
69
+ }
70
+ }
app/custominvoicing/adapter/customerdata.go ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package adapter
2
+
3
+ import (
4
+ "context"
5
+ "time"
6
+
7
+ "entgo.io/ent/dialect/sql"
8
+
9
+ appcustominvoicing "github.com/openmeterio/openmeter/openmeter/app/custominvoicing"
10
+ "github.com/openmeterio/openmeter/openmeter/ent/db"
11
+ "github.com/openmeterio/openmeter/openmeter/ent/db/appcustominvoicingcustomer"
12
+ "github.com/openmeterio/openmeter/pkg/framework/entutils"
13
+ )
14
+
15
+ func (a *adapter) GetCustomerData(ctx context.Context, input appcustominvoicing.GetAppCustomerDataInput) (appcustominvoicing.CustomerData, error) {
16
+ if err := input.Validate(); err != nil {
17
+ return appcustominvoicing.CustomerData{}, err
18
+ }
19
+
20
+ return entutils.TransactingRepo(ctx, a, func(ctx context.Context, tx *adapter) (appcustominvoicing.CustomerData, error) {
21
+ line, err := tx.db.AppCustomInvoicingCustomer.Query().
22
+ Where(
23
+ appcustominvoicingcustomer.CustomerID(input.CustomerID),
24
+ appcustominvoicingcustomer.Namespace(input.Namespace),
25
+ appcustominvoicingcustomer.AppID(input.AppID),
26
+ appcustominvoicingcustomer.DeletedAtIsNil(),
27
+ ).
28
+ First(ctx)
29
+ if err != nil {
30
+ if db.IsNotFound(err) {
31
+ return appcustominvoicing.CustomerData{}, nil
32
+ }
33
+
34
+ return appcustominvoicing.CustomerData{}, err
35
+ }
36
+
37
+ return mapDBToCustomerData(line), nil
38
+ })
39
+ }
40
+
41
+ func (a *adapter) UpsertCustomerData(ctx context.Context, input appcustominvoicing.UpsertCustomerDataInput) error {
42
+ if err := input.Validate(); err != nil {
43
+ return err
44
+ }
45
+
46
+ return entutils.TransactingRepoWithNoValue(ctx, a, func(ctx context.Context, tx *adapter) error {
47
+ return tx.db.AppCustomInvoicingCustomer.Create().
48
+ SetMetadata(input.Data.Metadata).
49
+ SetCustomerID(input.CustomerDataID.CustomerID).
50
+ SetNamespace(input.CustomerDataID.Namespace).
51
+ SetAppID(input.CustomerDataID.AppID).
52
+ // Upsert
53
+ OnConflict(
54
+ sql.ConflictColumns(
55
+ appcustominvoicingcustomer.FieldCustomerID,
56
+ appcustominvoicingcustomer.FieldNamespace,
57
+ appcustominvoicingcustomer.FieldAppID,
58
+ ),
59
+ sql.ConflictWhere(sql.IsNull(appcustominvoicingcustomer.FieldDeletedAt)),
60
+ ).
61
+ UpdateMetadata().
62
+ UpdateDeletedAt().
63
+ Exec(ctx)
64
+ })
65
+ }
66
+
67
+ func (a *adapter) DeleteCustomerData(ctx context.Context, input appcustominvoicing.DeleteAppCustomerDataInput) error {
68
+ if err := input.Validate(); err != nil {
69
+ return err
70
+ }
71
+
72
+ return entutils.TransactingRepoWithNoValue(ctx, a, func(ctx context.Context, tx *adapter) error {
73
+ return tx.db.AppCustomInvoicingCustomer.Update().
74
+ SetDeletedAt(time.Now()).
75
+ Where(
76
+ appcustominvoicingcustomer.CustomerID(input.CustomerID),
77
+ appcustominvoicingcustomer.Namespace(input.Namespace),
78
+ appcustominvoicingcustomer.AppID(input.AppID),
79
+ appcustominvoicingcustomer.DeletedAtIsNil(),
80
+ ).
81
+ Exec(ctx)
82
+ })
83
+ }
84
+
85
+ func mapDBToCustomerData(line *db.AppCustomInvoicingCustomer) appcustominvoicing.CustomerData {
86
+ return appcustominvoicing.CustomerData{
87
+ Metadata: line.Metadata,
88
+ }
89
+ }
app/custominvoicing/app.go ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package appcustominvoicing
2
+
3
+ import (
4
+ "context"
5
+ "fmt"
6
+
7
+ "github.com/openmeterio/openmeter/openmeter/app"
8
+ "github.com/openmeterio/openmeter/openmeter/billing"
9
+ "github.com/openmeterio/openmeter/openmeter/billing/sequence"
10
+ "github.com/openmeterio/openmeter/openmeter/customer"
11
+ customerapp "github.com/openmeterio/openmeter/openmeter/customer/app"
12
+ )
13
+
14
+ var (
15
+ _ customerapp.App = (*App)(nil)
16
+ _ billing.InvoicingApp = (*App)(nil)
17
+ _ billing.InvoicingAppAsyncSyncer = (*App)(nil)
18
+ )
19
+
20
+ var DefaultInvoiceSequenceNumber = sequence.Definition{
21
+ Prefix: "INV",
22
+ SuffixTemplate: "{{.CustomerPrefix}}-{{.NextSequenceNumber}}",
23
+ Scope: "invoices/custom-invoicing",
24
+ CommitMode: sequence.CommitModeWithCaller,
25
+ }
26
+
27
+ type Configuration struct {
28
+ EnableDraftSyncHook bool `json:"enable_draft_sync_hook"`
29
+ EnableIssuingSyncHook bool `json:"enable_issuing_sync_hook"`
30
+ }
31
+
32
+ const (
33
+ MetadataKeyDraftSyncedAt = "openmeter.io/custominvoicing/draft-synced-at"
34
+ MetadataKeyFinalizedAt = "openmeter.io/custominvoicing/finalized-at"
35
+ )
36
+
37
+ func (c Configuration) Validate() error {
38
+ return nil
39
+ }
40
+
41
+ type Meta struct {
42
+ app.AppBase
43
+ Configuration
44
+ }
45
+
46
+ var _ app.EventAppParser = (*Meta)(nil)
47
+
48
+ func (m *Meta) FromEventAppData(event app.EventApp) error {
49
+ m.AppBase = event.AppBase
50
+
51
+ if err := event.AppData.ParseInto(&m.Configuration); err != nil {
52
+ return fmt.Errorf("error parsing app data: %w", err)
53
+ }
54
+
55
+ return nil
56
+ }
57
+
58
+ type App struct {
59
+ Meta
60
+
61
+ customInvoicingService Service
62
+ sequenceService sequence.Service
63
+ }
64
+
65
+ func (a App) ValidateCustomer(ctx context.Context, customer *customer.Customer, capabilities []app.CapabilityType) error {
66
+ return nil
67
+ }
68
+
69
+ func (a App) UpdateAppConfig(ctx context.Context, input app.AppConfigUpdate) error {
70
+ cfg, ok := input.(Configuration)
71
+ if !ok {
72
+ return fmt.Errorf("invalid configuration")
73
+ }
74
+
75
+ if err := cfg.Validate(); err != nil {
76
+ return err
77
+ }
78
+
79
+ return a.customInvoicingService.UpsertAppConfiguration(ctx, UpsertAppConfigurationInput{
80
+ AppID: a.GetID(),
81
+ Configuration: cfg,
82
+ })
83
+ }
84
+
85
+ func (a App) GetEventAppData() (app.EventAppData, error) {
86
+ return app.NewEventAppData(a.Configuration)
87
+ }
88
+
89
+ // InvoicingApp
90
+ // These are no-ops as whatever is meaningful, is handled via the http driver of the custominvoicing app.
91
+
92
+ // ValidateStandardInvoice is a no-op as any validation issues are published via the draft.syncing and finalizations syncing
93
+ // flow.
94
+ func (a App) ValidateStandardInvoice(ctx context.Context, invoice billing.StandardInvoice) error {
95
+ return nil
96
+ }
97
+
98
+ func (a App) UpsertStandardInvoice(ctx context.Context, invoice billing.StandardInvoice) (*billing.UpsertStandardInvoiceResult, error) {
99
+ return nil, nil
100
+ }
101
+
102
+ func (a App) FinalizeStandardInvoice(ctx context.Context, invoice billing.StandardInvoice) (*billing.FinalizeStandardInvoiceResult, error) {
103
+ canAdvance, err := a.CanIssuingSyncAdvance(invoice)
104
+ if err != nil {
105
+ return nil, err
106
+ }
107
+
108
+ res := billing.NewFinalizeStandardInvoiceResult()
109
+
110
+ // If we are done with the hook work, let's make sure that the invoice has a non-draft invoice number
111
+ if canAdvance {
112
+ // If the invoice still has a draft invoice number, let's generate a non-draft one
113
+ if sequence.DraftInvoiceSequenceNumber.PrefixMatches(invoice.Number) {
114
+ invoiceNumber, err := a.sequenceService.GenerateInvoiceSequenceNumber(ctx,
115
+ sequence.GenerationInput{
116
+ Namespace: invoice.Namespace,
117
+ CustomerName: invoice.Customer.Name,
118
+ Currency: invoice.Currency,
119
+ },
120
+ DefaultInvoiceSequenceNumber,
121
+ )
122
+ if err != nil {
123
+ return nil, fmt.Errorf("generating invoice number: %w", err)
124
+ }
125
+
126
+ res.SetInvoiceNumber(invoiceNumber)
127
+ }
128
+ }
129
+
130
+ return res, nil
131
+ }
132
+
133
+ // DeleteStandardInvoice is a no-op as this should happen via the notifications webhook
134
+ func (a App) DeleteStandardInvoice(ctx context.Context, invoice billing.StandardInvoice) error {
135
+ return nil
136
+ }
137
+
138
+ // InvoicingAppAsyncSyncer
139
+
140
+ func (a App) CanDraftSyncAdvance(invoice billing.StandardInvoice) (bool, error) {
141
+ if !a.Configuration.EnableDraftSyncHook {
142
+ return true, nil
143
+ }
144
+
145
+ if invoice.Metadata == nil {
146
+ return false, nil
147
+ }
148
+
149
+ if _, ok := invoice.Metadata[MetadataKeyDraftSyncedAt]; ok {
150
+ return true, nil
151
+ }
152
+
153
+ return false, nil
154
+ }
155
+
156
+ func (a App) CanIssuingSyncAdvance(invoice billing.StandardInvoice) (bool, error) {
157
+ if !a.Configuration.EnableIssuingSyncHook {
158
+ return true, nil
159
+ }
160
+
161
+ if invoice.Metadata == nil {
162
+ return false, nil
163
+ }
164
+
165
+ if _, ok := invoice.Metadata[MetadataKeyFinalizedAt]; ok {
166
+ return true, nil
167
+ }
168
+
169
+ return false, nil
170
+ }
app/custominvoicing/customerdata.go ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package appcustominvoicing
2
+
3
+ import (
4
+ "context"
5
+ "errors"
6
+ "fmt"
7
+
8
+ "github.com/openmeterio/openmeter/openmeter/app"
9
+ "github.com/openmeterio/openmeter/pkg/models"
10
+ )
11
+
12
+ var _ app.CustomerData = (*CustomerData)(nil)
13
+
14
+ type CustomerData struct {
15
+ Metadata models.Metadata `json:"metadata,omitempty"`
16
+ }
17
+
18
+ func (c CustomerData) Validate() error {
19
+ return nil
20
+ }
21
+
22
+ // Customer Specific App Data Handling
23
+
24
+ func (a App) GetCustomerData(ctx context.Context, input app.GetAppInstanceCustomerDataInput) (app.CustomerData, error) {
25
+ return a.customInvoicingService.GetCustomerData(ctx, GetAppCustomerDataInput{
26
+ Namespace: a.Namespace,
27
+ AppID: a.ID,
28
+ CustomerID: input.CustomerID.ID,
29
+ })
30
+ }
31
+
32
+ func (a App) UpsertCustomerData(ctx context.Context, input app.UpsertAppInstanceCustomerDataInput) error {
33
+ data, ok := input.Data.(CustomerData)
34
+ if !ok {
35
+ return fmt.Errorf("invalid customer data: %v", input.Data)
36
+ }
37
+
38
+ return a.customInvoicingService.UpsertCustomerData(ctx, UpsertCustomerDataInput{
39
+ CustomerDataID: CustomerDataID{
40
+ Namespace: a.Namespace,
41
+ AppID: a.ID,
42
+ CustomerID: input.CustomerID.ID,
43
+ },
44
+ Data: data,
45
+ })
46
+ }
47
+
48
+ func (a App) DeleteCustomerData(ctx context.Context, input app.DeleteAppInstanceCustomerDataInput) error {
49
+ return a.customInvoicingService.DeleteCustomerData(ctx, DeleteAppCustomerDataInput{
50
+ Namespace: a.Namespace,
51
+ AppID: a.ID,
52
+ CustomerID: input.CustomerID.ID,
53
+ })
54
+ }
55
+
56
+ // Service types
57
+
58
+ type UpsertCustomerDataInput struct {
59
+ CustomerDataID
60
+ Data CustomerData
61
+ }
62
+
63
+ func (i UpsertCustomerDataInput) Validate() error {
64
+ if err := i.CustomerDataID.Validate(); err != nil {
65
+ return err
66
+ }
67
+
68
+ if err := i.Data.Validate(); err != nil {
69
+ return err
70
+ }
71
+
72
+ return nil
73
+ }
74
+
75
+ type CustomerDataID struct {
76
+ Namespace string
77
+ AppID string
78
+ CustomerID string
79
+ }
80
+
81
+ func (i CustomerDataID) Validate() error {
82
+ if i.Namespace == "" {
83
+ return errors.New("namespace is required")
84
+ }
85
+
86
+ if i.CustomerID == "" {
87
+ return errors.New("customer id is required")
88
+ }
89
+
90
+ if i.AppID == "" {
91
+ return errors.New("app id is required")
92
+ }
93
+
94
+ return nil
95
+ }
96
+
97
+ type (
98
+ GetAppCustomerDataInput = CustomerDataID
99
+ DeleteAppCustomerDataInput = CustomerDataID
100
+ )
app/custominvoicing/factory.go ADDED
@@ -0,0 +1,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package appcustominvoicing
2
+
3
+ import (
4
+ "context"
5
+ "fmt"
6
+
7
+ "github.com/openmeterio/openmeter/openmeter/app"
8
+ "github.com/openmeterio/openmeter/openmeter/billing/sequence"
9
+ )
10
+
11
+ var (
12
+ MarketplaceListing = app.MarketplaceListing{
13
+ Type: app.AppTypeCustomInvoicing,
14
+ Name: "Custom Invoicing",
15
+ Description: "Custom Invoicing can be used to interface with third party invoicing and payment systems",
16
+ Capabilities: []app.Capability{
17
+ CollectPaymentCapability,
18
+ CalculateTaxCapability,
19
+ InvoiceCustomerCapability,
20
+ },
21
+ InstallMethods: []app.InstallMethod{
22
+ app.InstallMethodNoCredentials,
23
+ },
24
+ }
25
+
26
+ CollectPaymentCapability = app.Capability{
27
+ Type: app.CapabilityTypeCollectPayments,
28
+ Key: "custom_invoicing_collect_payment",
29
+ Name: "Payment",
30
+ Description: "Process payments",
31
+ }
32
+
33
+ CalculateTaxCapability = app.Capability{
34
+ Type: app.CapabilityTypeCalculateTax,
35
+ Key: "custom_invoicing_calculate_tax",
36
+ Name: "Calculate Tax",
37
+ Description: "Calculate tax for a payment",
38
+ }
39
+
40
+ InvoiceCustomerCapability = app.Capability{
41
+ Type: app.CapabilityTypeInvoiceCustomers,
42
+ Key: "custom_invoicing_invoice_customer",
43
+ Name: "Invoice Customer",
44
+ Description: "Invoice a customer",
45
+ }
46
+ )
47
+
48
+ type Factory struct {
49
+ appService app.Service
50
+ customInvoicingService Service
51
+ sequenceService sequence.Service
52
+ }
53
+
54
+ type FactoryConfig struct {
55
+ AppService app.Service
56
+ CustomInvoicingService Service
57
+ SequenceService sequence.Service
58
+ }
59
+
60
+ func (c FactoryConfig) Validate() error {
61
+ if c.AppService == nil {
62
+ return fmt.Errorf("app service is required")
63
+ }
64
+
65
+ if c.CustomInvoicingService == nil {
66
+ return fmt.Errorf("custom invoicing service is required")
67
+ }
68
+
69
+ if c.SequenceService == nil {
70
+ return fmt.Errorf("sequence service is required")
71
+ }
72
+
73
+ return nil
74
+ }
75
+
76
+ func NewFactory(config FactoryConfig) (*Factory, error) {
77
+ if err := config.Validate(); err != nil {
78
+ return nil, fmt.Errorf("failed to validate config: %w", err)
79
+ }
80
+
81
+ fact := &Factory{
82
+ appService: config.AppService,
83
+ customInvoicingService: config.CustomInvoicingService,
84
+ sequenceService: config.SequenceService,
85
+ }
86
+
87
+ err := config.AppService.RegisterMarketplaceListing(app.RegistryItem{
88
+ Listing: MarketplaceListing,
89
+ Factory: fact,
90
+ })
91
+ if err != nil {
92
+ return nil, fmt.Errorf("failed to register marketplace listing: %w", err)
93
+ }
94
+
95
+ return fact, nil
96
+ }
97
+
98
+ // Factory
99
+ func (f *Factory) NewApp(ctx context.Context, appBase app.AppBase) (app.App, error) {
100
+ cfg, err := f.customInvoicingService.GetAppConfiguration(ctx, appBase.GetID())
101
+ if err != nil {
102
+ return nil, fmt.Errorf("failed to get app config: %w", err)
103
+ }
104
+
105
+ return App{
106
+ Meta: Meta{
107
+ AppBase: appBase,
108
+ Configuration: cfg,
109
+ },
110
+ customInvoicingService: f.customInvoicingService,
111
+ sequenceService: f.sequenceService,
112
+ }, nil
113
+ }
114
+
115
+ func (f *Factory) InstallApp(ctx context.Context, input app.AppFactoryInstallAppInput) (app.App, error) {
116
+ if err := input.Validate(); err != nil {
117
+ return nil, fmt.Errorf("invalid input: %w", err)
118
+ }
119
+
120
+ newApp, err := f.customInvoicingService.CreateApp(ctx, CreateAppInput{
121
+ Namespace: input.Namespace,
122
+ Name: input.Name,
123
+ })
124
+ if err != nil {
125
+ return nil, fmt.Errorf("failed to create app: %w", err)
126
+ }
127
+
128
+ return f.NewApp(ctx, newApp.GetAppBase())
129
+ }
130
+
131
+ func (f *Factory) UninstallApp(ctx context.Context, input app.UninstallAppInput) error {
132
+ return f.customInvoicingService.DeleteApp(ctx, input)
133
+ }
134
+
135
+ // Service types
136
+
137
+ type CreateAppInput struct {
138
+ Namespace string
139
+ Name string
140
+
141
+ Config Configuration
142
+ }
143
+
144
+ func (i CreateAppInput) Validate() error {
145
+ if i.Namespace == "" {
146
+ return fmt.Errorf("namespace is required")
147
+ }
148
+
149
+ if i.Name == "" {
150
+ return fmt.Errorf("name is required")
151
+ }
152
+
153
+ return nil
154
+ }
155
+
156
+ type UpsertAppConfigurationInput struct {
157
+ AppID app.AppID
158
+ Configuration Configuration
159
+ }
app/custominvoicing/httpdriver/custominvoicing.go ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package httpdriver
2
+
3
+ import (
4
+ "context"
5
+ "fmt"
6
+ "net/http"
7
+
8
+ "github.com/openmeterio/openmeter/api"
9
+ appcustominvoicing "github.com/openmeterio/openmeter/openmeter/app/custominvoicing"
10
+ "github.com/openmeterio/openmeter/openmeter/billing"
11
+ billinghttpdriver "github.com/openmeterio/openmeter/openmeter/billing/httpdriver"
12
+ "github.com/openmeterio/openmeter/pkg/framework/commonhttp"
13
+ "github.com/openmeterio/openmeter/pkg/framework/transport/httptransport"
14
+ )
15
+
16
+ type (
17
+ DraftSyncronizedRequest = appcustominvoicing.SyncDraftInvoiceInput
18
+ DraftSyncronizedResponse = api.Invoice
19
+ DraftSyncronizedParams = struct {
20
+ InvoiceID string `json:"invoiceId"`
21
+ }
22
+ DraftSyncronizedHandler httptransport.HandlerWithArgs[DraftSyncronizedRequest, DraftSyncronizedResponse, DraftSyncronizedParams]
23
+ )
24
+
25
+ func (h *handler) DraftSyncronized() DraftSyncronizedHandler {
26
+ return httptransport.NewHandlerWithArgs(
27
+ func(ctx context.Context, r *http.Request, params DraftSyncronizedParams) (DraftSyncronizedRequest, error) {
28
+ namespace, err := h.resolveNamespace(ctx)
29
+ if err != nil {
30
+ return DraftSyncronizedRequest{}, fmt.Errorf("failed to resolve namespace: %w", err)
31
+ }
32
+
33
+ var body api.CustomInvoicingDraftSynchronizedRequest
34
+ if err := commonhttp.JSONRequestBodyDecoder(r, &body); err != nil {
35
+ return DraftSyncronizedRequest{}, fmt.Errorf("failed to decode draft synchronized request: %w", err)
36
+ }
37
+
38
+ return DraftSyncronizedRequest{
39
+ InvoiceID: billing.InvoiceID{
40
+ ID: params.InvoiceID,
41
+ Namespace: namespace,
42
+ },
43
+ UpsertInvoiceResults: mapUpsertStandardInvoiceResultFromAPI(body.Invoicing),
44
+ }, nil
45
+ },
46
+ func(ctx context.Context, request DraftSyncronizedRequest) (DraftSyncronizedResponse, error) {
47
+ if err := request.Validate(); err != nil {
48
+ return DraftSyncronizedResponse{}, err
49
+ }
50
+
51
+ invoice, err := h.service.SyncDraftInvoice(ctx, request)
52
+ if err != nil {
53
+ return DraftSyncronizedResponse{}, err
54
+ }
55
+
56
+ return billinghttpdriver.MapStandardInvoiceToAPI(invoice)
57
+ },
58
+ commonhttp.JSONResponseEncoderWithStatus[DraftSyncronizedResponse](http.StatusOK),
59
+ httptransport.AppendOptions(
60
+ h.options,
61
+ httptransport.WithOperationName("DraftSyncronized"),
62
+ httptransport.WithErrorEncoder(errorEncoder()),
63
+ )...,
64
+ )
65
+ }
66
+
67
+ type (
68
+ IssuingSyncronizedRequest = appcustominvoicing.SyncIssuingInvoiceInput
69
+ IssuingSyncronizedResponse = api.Invoice
70
+ IssuingSyncronizedParams = struct {
71
+ InvoiceID string `json:"invoiceId"`
72
+ }
73
+ IssuingSyncronizedHandler httptransport.HandlerWithArgs[IssuingSyncronizedRequest, IssuingSyncronizedResponse, IssuingSyncronizedParams]
74
+ )
75
+
76
+ func (h *handler) IssuingSyncronized() IssuingSyncronizedHandler {
77
+ return httptransport.NewHandlerWithArgs(
78
+ func(ctx context.Context, r *http.Request, params IssuingSyncronizedParams) (IssuingSyncronizedRequest, error) {
79
+ namespace, err := h.resolveNamespace(ctx)
80
+ if err != nil {
81
+ return IssuingSyncronizedRequest{}, fmt.Errorf("failed to resolve namespace: %w", err)
82
+ }
83
+
84
+ var body api.CustomInvoicingFinalizedRequest
85
+ if err := commonhttp.JSONRequestBodyDecoder(r, &body); err != nil {
86
+ return IssuingSyncronizedRequest{}, fmt.Errorf("failed to decode issuing synchronized request: %w", err)
87
+ }
88
+
89
+ return IssuingSyncronizedRequest{
90
+ InvoiceID: billing.InvoiceID{
91
+ ID: params.InvoiceID,
92
+ Namespace: namespace,
93
+ },
94
+ FinalizeInvoiceResult: mapFinalizeStandardInvoiceResultFromAPI(body),
95
+ }, nil
96
+ },
97
+ func(ctx context.Context, request IssuingSyncronizedRequest) (IssuingSyncronizedResponse, error) {
98
+ if err := request.Validate(); err != nil {
99
+ return IssuingSyncronizedResponse{}, err
100
+ }
101
+
102
+ invoice, err := h.service.SyncIssuingInvoice(ctx, request)
103
+ if err != nil {
104
+ return IssuingSyncronizedResponse{}, err
105
+ }
106
+
107
+ return billinghttpdriver.MapStandardInvoiceToAPI(invoice)
108
+ },
109
+ commonhttp.JSONResponseEncoderWithStatus[IssuingSyncronizedResponse](http.StatusOK),
110
+ httptransport.AppendOptions(
111
+ h.options,
112
+ httptransport.WithOperationName("IssuingSyncronized"),
113
+ httptransport.WithErrorEncoder(errorEncoder()),
114
+ )...,
115
+ )
116
+ }
117
+
118
+ type (
119
+ UpdatePaymentStatusRequest = appcustominvoicing.HandlePaymentTriggerInput
120
+ UpdatePaymentStatusResponse = api.Invoice
121
+ UpdatePaymentStatusParams = struct {
122
+ InvoiceID string `json:"invoiceId"`
123
+ }
124
+ UpdatePaymentStatusHandler httptransport.HandlerWithArgs[UpdatePaymentStatusRequest, UpdatePaymentStatusResponse, UpdatePaymentStatusParams]
125
+ )
126
+
127
+ func (h *handler) UpdatePaymentStatus() UpdatePaymentStatusHandler {
128
+ return httptransport.NewHandlerWithArgs(
129
+ func(ctx context.Context, r *http.Request, params UpdatePaymentStatusParams) (UpdatePaymentStatusRequest, error) {
130
+ namespace, err := h.resolveNamespace(ctx)
131
+ if err != nil {
132
+ return UpdatePaymentStatusRequest{}, fmt.Errorf("failed to resolve namespace: %w", err)
133
+ }
134
+
135
+ var body api.CustomInvoicingUpdatePaymentStatusRequest
136
+ if err := commonhttp.JSONRequestBodyDecoder(r, &body); err != nil {
137
+ return UpdatePaymentStatusRequest{}, fmt.Errorf("failed to decode handle payment trigger request: %w", err)
138
+ }
139
+
140
+ trigger, err := mapPaymentTriggerFromAPI(body.Trigger)
141
+ if err != nil {
142
+ return UpdatePaymentStatusRequest{}, fmt.Errorf("failed to map payment trigger: %w", err)
143
+ }
144
+
145
+ return UpdatePaymentStatusRequest{
146
+ InvoiceID: billing.InvoiceID{
147
+ ID: params.InvoiceID,
148
+ Namespace: namespace,
149
+ },
150
+ Trigger: trigger,
151
+ }, nil
152
+ },
153
+ func(ctx context.Context, request UpdatePaymentStatusRequest) (UpdatePaymentStatusResponse, error) {
154
+ if err := request.Validate(); err != nil {
155
+ return UpdatePaymentStatusResponse{}, err
156
+ }
157
+
158
+ invoice, err := h.service.HandlePaymentTrigger(ctx, request)
159
+ if err != nil {
160
+ return UpdatePaymentStatusResponse{}, err
161
+ }
162
+
163
+ return billinghttpdriver.MapStandardInvoiceToAPI(invoice)
164
+ },
165
+ commonhttp.JSONResponseEncoderWithStatus[UpdatePaymentStatusResponse](http.StatusOK),
166
+ httptransport.AppendOptions(
167
+ h.options,
168
+ httptransport.WithOperationName("UpdatePaymentStatus"),
169
+ httptransport.WithErrorEncoder(errorEncoder()),
170
+ )...,
171
+ )
172
+ }
app/custominvoicing/httpdriver/errors.go ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package httpdriver
2
+
3
+ import (
4
+ "context"
5
+ "net/http"
6
+
7
+ "github.com/openmeterio/openmeter/openmeter/billing"
8
+ "github.com/openmeterio/openmeter/pkg/framework/commonhttp"
9
+ "github.com/openmeterio/openmeter/pkg/framework/transport/httptransport/encoder"
10
+ )
11
+
12
+ func errorEncoder() encoder.ErrorEncoder {
13
+ return func(ctx context.Context, err error, w http.ResponseWriter, r *http.Request) bool {
14
+ return commonhttp.HandleErrorIfTypeMatches[billing.NotFoundError](ctx, http.StatusNotFound, err, w, billing.EncodeValidationIssues) ||
15
+ commonhttp.HandleErrorIfTypeMatches[billing.ValidationError](ctx, http.StatusBadRequest, err, w, billing.EncodeValidationIssues) ||
16
+ commonhttp.HandleErrorIfTypeMatches[billing.UpdateAfterDeleteError](ctx, http.StatusConflict, err, w, billing.EncodeValidationIssues) ||
17
+ commonhttp.HandleErrorIfTypeMatches[billing.ValidationIssue](ctx, http.StatusBadRequest, err, w, billing.EncodeValidationIssues) ||
18
+ // dependency: apps
19
+ commonhttp.HandleErrorIfTypeMatches[billing.AppError](ctx, http.StatusBadRequest, err, w)
20
+ }
21
+ }
app/custominvoicing/httpdriver/handler.go ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package httpdriver
2
+
3
+ import (
4
+ "context"
5
+ "errors"
6
+ "net/http"
7
+
8
+ appcustominvoicing "github.com/openmeterio/openmeter/openmeter/app/custominvoicing"
9
+ "github.com/openmeterio/openmeter/openmeter/namespace/namespacedriver"
10
+ "github.com/openmeterio/openmeter/pkg/framework/commonhttp"
11
+ "github.com/openmeterio/openmeter/pkg/framework/transport/httptransport"
12
+ )
13
+
14
+ type Handler interface {
15
+ AppHandler
16
+ }
17
+
18
+ type AppHandler interface {
19
+ DraftSyncronized() DraftSyncronizedHandler
20
+ IssuingSyncronized() IssuingSyncronizedHandler
21
+ UpdatePaymentStatus() UpdatePaymentStatusHandler
22
+ }
23
+
24
+ var _ Handler = (*handler)(nil)
25
+
26
+ type handler struct {
27
+ service appcustominvoicing.SyncService
28
+
29
+ namespaceDecoder namespacedriver.NamespaceDecoder
30
+ options []httptransport.HandlerOption
31
+ }
32
+
33
+ func (h *handler) resolveNamespace(ctx context.Context) (string, error) {
34
+ ns, ok := h.namespaceDecoder.GetNamespace(ctx)
35
+ if !ok {
36
+ return "", commonhttp.NewHTTPError(http.StatusInternalServerError, errors.New("internal server error"))
37
+ }
38
+
39
+ return ns, nil
40
+ }
41
+
42
+ func New(
43
+ service appcustominvoicing.SyncService,
44
+ namespaceDecoder namespacedriver.NamespaceDecoder,
45
+ options ...httptransport.HandlerOption,
46
+ ) Handler {
47
+ return &handler{
48
+ service: service,
49
+ namespaceDecoder: namespaceDecoder,
50
+ options: options,
51
+ }
52
+ }
app/custominvoicing/httpdriver/mapper.go ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package httpdriver
2
+
3
+ import (
4
+ "fmt"
5
+
6
+ "github.com/openmeterio/openmeter/api"
7
+ "github.com/openmeterio/openmeter/openmeter/billing"
8
+ "github.com/openmeterio/openmeter/pkg/models"
9
+ )
10
+
11
+ func mapUpsertStandardInvoiceResultFromAPI(in *api.CustomInvoicingSyncResult) *billing.UpsertStandardInvoiceResult {
12
+ if in == nil {
13
+ return nil
14
+ }
15
+
16
+ res := billing.NewUpsertStandardInvoiceResult()
17
+
18
+ if in.InvoiceNumber != nil {
19
+ res.SetInvoiceNumber(*in.InvoiceNumber)
20
+ }
21
+
22
+ if in.ExternalId != nil {
23
+ res.SetExternalID(*in.ExternalId)
24
+ }
25
+
26
+ if in.LineExternalIds != nil {
27
+ for _, line := range *in.LineExternalIds {
28
+ res.AddLineExternalID(line.LineId, line.ExternalId)
29
+ }
30
+ }
31
+
32
+ if in.LineDiscountExternalIds != nil {
33
+ for _, lineDiscount := range *in.LineDiscountExternalIds {
34
+ res.AddLineDiscountExternalID(lineDiscount.LineDiscountId, lineDiscount.ExternalId)
35
+ }
36
+ }
37
+
38
+ return res
39
+ }
40
+
41
+ func mapFinalizeStandardInvoiceResultFromAPI(in api.CustomInvoicingFinalizedRequest) *billing.FinalizeStandardInvoiceResult {
42
+ res := billing.NewFinalizeStandardInvoiceResult()
43
+
44
+ if in.Invoicing != nil {
45
+ if in.Invoicing.InvoiceNumber != nil {
46
+ res.SetInvoiceNumber(*in.Invoicing.InvoiceNumber)
47
+ }
48
+
49
+ if in.Invoicing.SentToCustomerAt != nil {
50
+ res.SetSentToCustomerAt(*in.Invoicing.SentToCustomerAt)
51
+ }
52
+ }
53
+
54
+ if in.Payment != nil {
55
+ if in.Payment.ExternalId != nil {
56
+ res.SetPaymentExternalID(*in.Payment.ExternalId)
57
+ }
58
+ }
59
+ return res
60
+ }
61
+
62
+ func mapPaymentTriggerFromAPI(in api.CustomInvoicingPaymentTrigger) (billing.InvoiceTrigger, error) {
63
+ if in == "" {
64
+ return "", models.NewGenericValidationError(fmt.Errorf("payment trigger is required"))
65
+ }
66
+
67
+ // Map API trigger names to internal state machine triggers
68
+ switch in {
69
+ case api.CustomInvoicingPaymentTriggerPaid:
70
+ return billing.TriggerPaid, nil
71
+ case api.CustomInvoicingPaymentTriggerPaymentFailed:
72
+ // Note: API uses "payment_failed" but internal trigger is "failed"
73
+ return billing.TriggerFailed, nil
74
+ case api.CustomInvoicingPaymentTriggerPaymentUncollectible:
75
+ return billing.TriggerPaymentUncollectible, nil
76
+ case api.CustomInvoicingPaymentTriggerPaymentOverdue:
77
+ return billing.TriggerPaymentOverdue, nil
78
+ case api.CustomInvoicingPaymentTriggerActionRequired:
79
+ return billing.TriggerActionRequired, nil
80
+ case api.CustomInvoicingPaymentTriggerVoid:
81
+ return billing.TriggerVoid, nil
82
+ default:
83
+ return "", models.NewGenericValidationError(fmt.Errorf("unknown payment trigger: %s", in))
84
+ }
85
+ }
app/custominvoicing/service.go ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package appcustominvoicing
2
+
3
+ import (
4
+ "context"
5
+
6
+ "github.com/openmeterio/openmeter/openmeter/app"
7
+ "github.com/openmeterio/openmeter/openmeter/billing"
8
+ )
9
+
10
+ type Service interface {
11
+ CustomerDataService
12
+ FactoryService
13
+ SyncService
14
+ }
15
+
16
+ type CustomerDataService interface {
17
+ GetCustomerData(ctx context.Context, input GetAppCustomerDataInput) (CustomerData, error)
18
+ UpsertCustomerData(ctx context.Context, input UpsertCustomerDataInput) error
19
+ DeleteCustomerData(ctx context.Context, input DeleteAppCustomerDataInput) error
20
+ }
21
+
22
+ type FactoryService interface {
23
+ CreateApp(ctx context.Context, input CreateAppInput) (app.AppBase, error)
24
+ DeleteApp(ctx context.Context, input app.UninstallAppInput) error
25
+ UpsertAppConfiguration(ctx context.Context, input UpsertAppConfigurationInput) error
26
+ GetAppConfiguration(ctx context.Context, appID app.AppID) (Configuration, error)
27
+ }
28
+
29
+ type SyncService interface {
30
+ SyncDraftInvoice(ctx context.Context, input SyncDraftInvoiceInput) (billing.StandardInvoice, error)
31
+ SyncIssuingInvoice(ctx context.Context, input SyncIssuingInvoiceInput) (billing.StandardInvoice, error)
32
+
33
+ HandlePaymentTrigger(ctx context.Context, input HandlePaymentTriggerInput) (billing.StandardInvoice, error)
34
+ }
app/custominvoicing/service/customerdata.go ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package service
2
+
3
+ import (
4
+ "context"
5
+
6
+ appcustominvoicing "github.com/openmeterio/openmeter/openmeter/app/custominvoicing"
7
+ "github.com/openmeterio/openmeter/pkg/framework/transaction"
8
+ )
9
+
10
+ func (s *Service) GetCustomerData(ctx context.Context, input appcustominvoicing.GetAppCustomerDataInput) (appcustominvoicing.CustomerData, error) {
11
+ return transaction.Run(ctx, s.adapter, func(ctx context.Context) (appcustominvoicing.CustomerData, error) {
12
+ return s.adapter.GetCustomerData(ctx, input)
13
+ })
14
+ }
15
+
16
+ func (s *Service) UpsertCustomerData(ctx context.Context, input appcustominvoicing.UpsertCustomerDataInput) error {
17
+ return transaction.RunWithNoValue(ctx, s.adapter, func(ctx context.Context) error {
18
+ return s.adapter.UpsertCustomerData(ctx, input)
19
+ })
20
+ }
21
+
22
+ func (s *Service) DeleteCustomerData(ctx context.Context, input appcustominvoicing.DeleteAppCustomerDataInput) error {
23
+ return transaction.RunWithNoValue(ctx, s.adapter, func(ctx context.Context) error {
24
+ return s.adapter.DeleteCustomerData(ctx, input)
25
+ })
26
+ }
app/custominvoicing/service/factory.go ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package service
2
+
3
+ import (
4
+ "context"
5
+ "fmt"
6
+
7
+ "github.com/openmeterio/openmeter/openmeter/app"
8
+ appcustominvoicing "github.com/openmeterio/openmeter/openmeter/app/custominvoicing"
9
+ "github.com/openmeterio/openmeter/pkg/framework/transaction"
10
+ )
11
+
12
+ var _ appcustominvoicing.FactoryService = (*Service)(nil)
13
+
14
+ func (s *Service) CreateApp(ctx context.Context, input appcustominvoicing.CreateAppInput) (app.AppBase, error) {
15
+ if err := input.Validate(); err != nil {
16
+ return app.AppBase{}, fmt.Errorf("invalid input: %w", err)
17
+ }
18
+
19
+ return transaction.Run(ctx, s.adapter, func(ctx context.Context) (app.AppBase, error) {
20
+ // Let's create the app first
21
+ appBase, err := s.appService.CreateApp(ctx, app.CreateAppInput{
22
+ Namespace: input.Namespace,
23
+ Name: input.Name,
24
+ Type: app.AppTypeCustomInvoicing,
25
+ })
26
+ if err != nil {
27
+ return app.AppBase{}, fmt.Errorf("failed to create app: %w", err)
28
+ }
29
+
30
+ // Let's create the app settings entity
31
+ err = s.adapter.UpsertAppConfiguration(ctx, appcustominvoicing.UpsertAppConfigurationInput{
32
+ AppID: app.AppID{ID: appBase.ID, Namespace: appBase.Namespace},
33
+ Configuration: input.Config,
34
+ })
35
+ if err != nil {
36
+ return app.AppBase{}, fmt.Errorf("failed to create app settings: %w", err)
37
+ }
38
+
39
+ return appBase, nil
40
+ })
41
+ }
42
+
43
+ func (s *Service) DeleteApp(ctx context.Context, input app.UninstallAppInput) error {
44
+ return transaction.RunWithNoValue(ctx, s.adapter, func(ctx context.Context) error {
45
+ return s.adapter.DeleteAppConfiguration(ctx, input)
46
+ })
47
+ }
48
+
49
+ func (s *Service) UpsertAppConfiguration(ctx context.Context, input appcustominvoicing.UpsertAppConfigurationInput) error {
50
+ return transaction.RunWithNoValue(ctx, s.adapter, func(ctx context.Context) error {
51
+ return s.adapter.UpsertAppConfiguration(ctx, input)
52
+ })
53
+ }
54
+
55
+ func (s *Service) GetAppConfiguration(ctx context.Context, appID app.AppID) (appcustominvoicing.Configuration, error) {
56
+ return s.adapter.GetAppConfiguration(ctx, appID)
57
+ }
app/custominvoicing/service/service.go ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package service
2
+
3
+ import (
4
+ "errors"
5
+ "log/slog"
6
+
7
+ "github.com/openmeterio/openmeter/openmeter/app"
8
+ appcustominvoicing "github.com/openmeterio/openmeter/openmeter/app/custominvoicing"
9
+ "github.com/openmeterio/openmeter/openmeter/billing"
10
+ )
11
+
12
+ var _ appcustominvoicing.Service = (*Service)(nil)
13
+
14
+ type Service struct {
15
+ adapter appcustominvoicing.Adapter
16
+ logger *slog.Logger
17
+
18
+ // dependencies
19
+ appService app.Service
20
+ billingService billing.Service
21
+ }
22
+
23
+ type Config struct {
24
+ Adapter appcustominvoicing.Adapter
25
+ Logger *slog.Logger
26
+
27
+ AppService app.Service
28
+ BillingService billing.Service
29
+ }
30
+
31
+ func (c Config) Validate() error {
32
+ if c.Adapter == nil {
33
+ return errors.New("adapter cannot be nil")
34
+ }
35
+
36
+ if c.Logger == nil {
37
+ return errors.New("logger cannot be nil")
38
+ }
39
+
40
+ if c.AppService == nil {
41
+ return errors.New("app service cannot be nil")
42
+ }
43
+
44
+ if c.BillingService == nil {
45
+ return errors.New("billing service cannot be nil")
46
+ }
47
+
48
+ return nil
49
+ }
50
+
51
+ func New(config Config) (*Service, error) {
52
+ if err := config.Validate(); err != nil {
53
+ return nil, err
54
+ }
55
+
56
+ return &Service{
57
+ adapter: config.Adapter,
58
+ logger: config.Logger,
59
+ appService: config.AppService,
60
+ billingService: config.BillingService,
61
+ }, nil
62
+ }
app/custominvoicing/service/sync.go ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package service
2
+
3
+ import (
4
+ "context"
5
+ "fmt"
6
+ "time"
7
+
8
+ "github.com/samber/lo"
9
+
10
+ "github.com/openmeterio/openmeter/openmeter/app"
11
+ appcustominvoicing "github.com/openmeterio/openmeter/openmeter/app/custominvoicing"
12
+ "github.com/openmeterio/openmeter/openmeter/billing"
13
+ "github.com/openmeterio/openmeter/pkg/clock"
14
+ "github.com/openmeterio/openmeter/pkg/framework/transaction"
15
+ "github.com/openmeterio/openmeter/pkg/models"
16
+ )
17
+
18
+ var _ appcustominvoicing.SyncService = (*Service)(nil)
19
+
20
+ func (s *Service) SyncDraftInvoice(ctx context.Context, input appcustominvoicing.SyncDraftInvoiceInput) (billing.StandardInvoice, error) {
21
+ if err := input.Validate(); err != nil {
22
+ return billing.StandardInvoice{}, err
23
+ }
24
+
25
+ return s.billingService.SyncDraftInvoice(ctx, billing.SyncDraftStandardInvoiceInput{
26
+ InvoiceID: input.InvoiceID,
27
+ UpsertInvoiceResults: input.UpsertInvoiceResults,
28
+ AdditionalMetadata: map[string]string{
29
+ appcustominvoicing.MetadataKeyDraftSyncedAt: clock.Now().Format(time.RFC3339),
30
+ },
31
+ InvoiceValidator: s.ValidateInvoiceApp,
32
+ })
33
+ }
34
+
35
+ func (s *Service) SyncIssuingInvoice(ctx context.Context, input appcustominvoicing.SyncIssuingInvoiceInput) (billing.StandardInvoice, error) {
36
+ if err := input.Validate(); err != nil {
37
+ return billing.StandardInvoice{}, err
38
+ }
39
+
40
+ return s.billingService.SyncIssuingInvoice(ctx, billing.SyncIssuingStandardInvoiceInput{
41
+ InvoiceID: input.InvoiceID,
42
+ FinalizeInvoiceResult: input.FinalizeInvoiceResult,
43
+ AdditionalMetadata: map[string]string{
44
+ appcustominvoicing.MetadataKeyFinalizedAt: clock.Now().Format(time.RFC3339),
45
+ },
46
+ InvoiceValidator: s.ValidateInvoiceApp,
47
+ })
48
+ }
49
+
50
+ func (s *Service) ValidateInvoiceApp(invoice billing.StandardInvoice) error {
51
+ if invoice.Workflow.Apps == nil {
52
+ return models.NewGenericValidationError(fmt.Errorf("standard invoice %s has no apps", invoice.ID))
53
+ }
54
+
55
+ if invoice.Workflow.Apps.Invoicing == nil {
56
+ return models.NewGenericValidationError(fmt.Errorf("invoice %s has no invoicing app", invoice.ID))
57
+ }
58
+
59
+ if invoice.Workflow.Apps.Invoicing.GetType() != app.AppTypeCustomInvoicing {
60
+ return models.NewGenericValidationError(fmt.Errorf("invoice %s is not managed by the custom invoicing app", invoice.ID))
61
+ }
62
+
63
+ return nil
64
+ }
65
+
66
+ func (s *Service) HandlePaymentTrigger(ctx context.Context, input appcustominvoicing.HandlePaymentTriggerInput) (billing.StandardInvoice, error) {
67
+ if err := input.Validate(); err != nil {
68
+ return billing.StandardInvoice{}, err
69
+ }
70
+
71
+ return transaction.Run(ctx, s.adapter, func(ctx context.Context) (billing.StandardInvoice, error) {
72
+ invoice, err := s.billingService.GetStandardInvoiceById(ctx, billing.GetStandardInvoiceByIdInput{
73
+ Invoice: input.InvoiceID,
74
+ })
75
+ if err != nil {
76
+ return billing.StandardInvoice{}, err
77
+ }
78
+
79
+ if err := s.ValidateInvoiceApp(invoice); err != nil {
80
+ return billing.StandardInvoice{}, err
81
+ }
82
+
83
+ err = s.billingService.TriggerInvoice(ctx, billing.InvoiceTriggerServiceInput{
84
+ InvoiceTriggerInput: billing.InvoiceTriggerInput{
85
+ Invoice: input.InvoiceID,
86
+ Trigger: input.Trigger,
87
+ },
88
+ AppType: app.AppTypeCustomInvoicing,
89
+ Capability: app.CapabilityTypeCollectPayments,
90
+ })
91
+ if err != nil {
92
+ return billing.StandardInvoice{}, err
93
+ }
94
+
95
+ invoice, err = s.billingService.GetStandardInvoiceById(ctx, billing.GetStandardInvoiceByIdInput{
96
+ Invoice: input.InvoiceID,
97
+ })
98
+ if err != nil {
99
+ return billing.StandardInvoice{}, err
100
+ }
101
+
102
+ if len(invoice.ValidationIssues) > 0 {
103
+ criticalIssues := lo.Filter(invoice.ValidationIssues, func(issue billing.ValidationIssue, _ int) bool {
104
+ return issue.Severity == billing.ValidationIssueSeverityCritical
105
+ })
106
+
107
+ if len(criticalIssues) > 0 {
108
+ // Warning: This causes a rollback of the transaction
109
+ return billing.StandardInvoice{}, billing.ValidationError{
110
+ Err: criticalIssues.AsError(),
111
+ }
112
+ }
113
+ }
114
+
115
+ return invoice, nil
116
+ })
117
+ }
app/custominvoicing/sync.go ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package appcustominvoicing
2
+
3
+ import (
4
+ "errors"
5
+ "fmt"
6
+
7
+ "github.com/openmeterio/openmeter/openmeter/billing"
8
+ "github.com/openmeterio/openmeter/pkg/models"
9
+ )
10
+
11
+ type SyncDraftInvoiceInput struct {
12
+ InvoiceID billing.InvoiceID
13
+ UpsertInvoiceResults *billing.UpsertStandardInvoiceResult
14
+ }
15
+
16
+ func (i *SyncDraftInvoiceInput) Validate() error {
17
+ var errs []error
18
+
19
+ if err := i.InvoiceID.Validate(); err != nil {
20
+ errs = append(errs, err)
21
+ }
22
+
23
+ if i.UpsertInvoiceResults == nil {
24
+ errs = append(errs, fmt.Errorf("upsert invoice results are required"))
25
+ }
26
+
27
+ return models.NewNillableGenericValidationError(errors.Join(errs...))
28
+ }
29
+
30
+ type SyncIssuingInvoiceInput struct {
31
+ InvoiceID billing.InvoiceID
32
+ FinalizeInvoiceResult *billing.FinalizeStandardInvoiceResult
33
+ }
34
+
35
+ func (i *SyncIssuingInvoiceInput) Validate() error {
36
+ var errs []error
37
+
38
+ if err := i.InvoiceID.Validate(); err != nil {
39
+ errs = append(errs, err)
40
+ }
41
+
42
+ if i.FinalizeInvoiceResult == nil {
43
+ errs = append(errs, fmt.Errorf("finalize invoice result is required"))
44
+ }
45
+
46
+ return models.NewNillableGenericValidationError(errors.Join(errs...))
47
+ }
48
+
49
+ type HandlePaymentTriggerInput struct {
50
+ InvoiceID billing.InvoiceID
51
+ Trigger billing.InvoiceTrigger
52
+ }
53
+
54
+ func (i *HandlePaymentTriggerInput) Validate() error {
55
+ var errs []error
56
+
57
+ if err := i.InvoiceID.Validate(); err != nil {
58
+ errs = append(errs, err)
59
+ }
60
+
61
+ if i.Trigger == "" {
62
+ errs = append(errs, fmt.Errorf("trigger is required"))
63
+ }
64
+
65
+ return models.NewNillableGenericValidationError(errors.Join(errs...))
66
+ }
app/defaults.go ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ package app
2
+
3
+ const (
4
+ DefaultPageNumber = 1
5
+ DefaultPageSize = 100
6
+ )
app/errors.go ADDED
@@ -0,0 +1,226 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package app
2
+
3
+ import (
4
+ "errors"
5
+ "fmt"
6
+
7
+ "github.com/openmeterio/openmeter/openmeter/customer"
8
+ "github.com/openmeterio/openmeter/pkg/models"
9
+ )
10
+
11
+ // AppNotFoundError
12
+ func NewAppNotFoundError(appID AppID) *AppNotFoundError {
13
+ return &AppNotFoundError{
14
+ err: models.NewGenericNotFoundError(
15
+ fmt.Errorf("app with id %s not found in %s namespace", appID.ID, appID.Namespace),
16
+ ),
17
+ }
18
+ }
19
+
20
+ var _ models.GenericError = AppNotFoundError{}
21
+
22
+ type AppNotFoundError struct {
23
+ err error
24
+ }
25
+
26
+ func (e AppNotFoundError) Error() string {
27
+ return e.err.Error()
28
+ }
29
+
30
+ func (e AppNotFoundError) Unwrap() error {
31
+ return e.err
32
+ }
33
+
34
+ // IsAppNotFoundError returns true if the error is a AppNotFoundError.
35
+ func IsAppNotFoundError(err error) bool {
36
+ if err == nil {
37
+ return false
38
+ }
39
+
40
+ var e *AppNotFoundError
41
+
42
+ return errors.As(err, &e)
43
+ }
44
+
45
+ // AppDefaultNotFoundError
46
+ func NewAppDefaultNotFoundError(appType AppType, namespace string) *AppDefaultNotFoundError {
47
+ return &AppDefaultNotFoundError{
48
+ err: models.NewGenericNotFoundError(
49
+ fmt.Errorf("there is no default app for %s type in %s namespace", appType, namespace),
50
+ ),
51
+ }
52
+ }
53
+
54
+ var _ models.GenericError = AppDefaultNotFoundError{}
55
+
56
+ type AppDefaultNotFoundError struct {
57
+ err error
58
+ }
59
+
60
+ func (e AppDefaultNotFoundError) Error() string {
61
+ return e.err.Error()
62
+ }
63
+
64
+ func (e AppDefaultNotFoundError) Unwrap() error {
65
+ return e.err
66
+ }
67
+
68
+ func IsAppDefaultNotFoundError(err error) bool {
69
+ if err == nil {
70
+ return false
71
+ }
72
+
73
+ var e *AppDefaultNotFoundError
74
+
75
+ return errors.As(err, &e)
76
+ }
77
+
78
+ // AppProviderAuthenticationError
79
+ func NewAppProviderAuthenticationError(appID *AppID, namespace string, providerError error) *AppProviderAuthenticationError {
80
+ var err error
81
+
82
+ if appID == nil {
83
+ err = fmt.Errorf("provider authentication error for app in %s namespace: %w", namespace, providerError)
84
+ } else {
85
+ err = fmt.Errorf("provider authentication error for app %s: %w", appID.ID, providerError)
86
+ }
87
+
88
+ return &AppProviderAuthenticationError{
89
+ err: models.NewGenericUnauthorizedError(err),
90
+ }
91
+ }
92
+
93
+ var _ models.GenericError = (*AppProviderAuthenticationError)(nil)
94
+
95
+ type AppProviderAuthenticationError struct {
96
+ err error
97
+ }
98
+
99
+ func (e AppProviderAuthenticationError) Error() string {
100
+ return e.err.Error()
101
+ }
102
+
103
+ func (e AppProviderAuthenticationError) Unwrap() error {
104
+ return e.err
105
+ }
106
+
107
+ func IsAppProviderAuthenticationError(err error) bool {
108
+ if err == nil {
109
+ return false
110
+ }
111
+
112
+ var e *AppProviderAuthenticationError
113
+
114
+ return errors.As(err, &e)
115
+ }
116
+
117
+ // AppProviderError
118
+ func NewAppProviderError(appID *AppID, namespace string, providerError error) *AppProviderError {
119
+ var err error
120
+
121
+ if appID == nil {
122
+ err = fmt.Errorf("provider error for app in %s namespace: %w", namespace, providerError)
123
+ } else {
124
+ err = fmt.Errorf("provider error for app %s: %w", appID.ID, providerError)
125
+ }
126
+
127
+ return &AppProviderError{
128
+ err: models.NewGenericPreConditionFailedError(err),
129
+ }
130
+ }
131
+
132
+ var _ models.GenericError = (*AppProviderError)(nil)
133
+
134
+ type AppProviderError struct {
135
+ err error
136
+ }
137
+
138
+ func (e AppProviderError) Error() string {
139
+ return e.err.Error()
140
+ }
141
+
142
+ func (e AppProviderError) Unwrap() error {
143
+ return e.err
144
+ }
145
+
146
+ func IsAppProviderError(err error) bool {
147
+ if err == nil {
148
+ return false
149
+ }
150
+
151
+ var e *AppProviderError
152
+
153
+ return errors.As(err, &e)
154
+ }
155
+
156
+ // AppProviderPreConditionError
157
+ var _ models.GenericError = (*AppProviderPreConditionError)(nil)
158
+
159
+ func NewAppProviderPreConditionError(appID AppID, condition string) *AppProviderPreConditionError {
160
+ return &AppProviderPreConditionError{
161
+ err: models.NewGenericPreConditionFailedError(
162
+ fmt.Errorf("app does not meet condition for %s: %s", appID.ID, condition),
163
+ ),
164
+ }
165
+ }
166
+
167
+ type AppProviderPreConditionError struct {
168
+ err error
169
+ }
170
+
171
+ func (e AppProviderPreConditionError) Error() string {
172
+ return e.err.Error()
173
+ }
174
+
175
+ func (e AppProviderPreConditionError) Unwrap() error {
176
+ return e.err
177
+ }
178
+
179
+ func IsAppProviderPreConditionError(err error) bool {
180
+ if err == nil {
181
+ return false
182
+ }
183
+
184
+ var e *AppProviderPreConditionError
185
+
186
+ return errors.As(err, &e)
187
+ }
188
+
189
+ // AppCustomerPreConditionError
190
+ func NewAppCustomerPreConditionError(appID AppID, appType AppType, customerID *customer.CustomerID, condition string) *AppCustomerPreConditionError {
191
+ var err error
192
+
193
+ if customerID == nil {
194
+ err = fmt.Errorf("customer does not meet condition for %s app type with id %s in namespace %s: %s", appType, appID.ID, appID.Namespace, condition)
195
+ } else {
196
+ err = fmt.Errorf("customer with id %s does not meet condition %s for %s app type with id %s in namespace %s", customerID.ID, condition, appType, appID.ID, appID.Namespace)
197
+ }
198
+
199
+ return &AppCustomerPreConditionError{
200
+ err: models.NewGenericPreConditionFailedError(err),
201
+ }
202
+ }
203
+
204
+ var _ models.GenericError = (*AppCustomerPreConditionError)(nil)
205
+
206
+ type AppCustomerPreConditionError struct {
207
+ err error
208
+ }
209
+
210
+ func (e AppCustomerPreConditionError) Error() string {
211
+ return e.err.Error()
212
+ }
213
+
214
+ func (e AppCustomerPreConditionError) Unwrap() error {
215
+ return e.err
216
+ }
217
+
218
+ func IsAppCustomerPreConditionError(err error) bool {
219
+ if err == nil {
220
+ return false
221
+ }
222
+
223
+ var e *AppCustomerPreConditionError
224
+
225
+ return errors.As(err, &e)
226
+ }
app/event.go ADDED
@@ -0,0 +1,214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package app
2
+
3
+ import (
4
+ "context"
5
+ "encoding/json"
6
+ "fmt"
7
+ "reflect"
8
+
9
+ "github.com/oklog/ulid/v2"
10
+
11
+ "github.com/openmeterio/openmeter/openmeter/event/metadata"
12
+ "github.com/openmeterio/openmeter/openmeter/session"
13
+ )
14
+
15
+ // EventAppParser should be implemented by the app's meta contents to be parsable from an EventApp
16
+ type EventAppParser interface {
17
+ FromEventAppData(EventApp) error
18
+ }
19
+
20
+ type EventAppData map[string]any
21
+
22
+ // NewEventAppData creates a new EventAppData from a given value
23
+ // TODO[later]: we need to refactor apps to be able to handle serialization more gracefully, e.g. having a proper
24
+ // union type for app instead of the interface
25
+ func NewEventAppData(v any) (EventAppData, error) {
26
+ jsonBytes, err := json.Marshal(v)
27
+ if err != nil {
28
+ return nil, err
29
+ }
30
+
31
+ var data EventAppData
32
+ if err := json.Unmarshal(jsonBytes, &data); err != nil {
33
+ return nil, err
34
+ }
35
+
36
+ return data, nil
37
+ }
38
+
39
+ // ParseInto parses the EventAppData into a given value, the value must be a pointer
40
+ func (e EventAppData) ParseInto(v any) error {
41
+ if rv := reflect.ValueOf(v); rv.Kind() != reflect.Pointer || rv.IsNil() {
42
+ return fmt.Errorf("target must be a non-nil pointer")
43
+ }
44
+
45
+ jsonBytes, err := json.Marshal(e)
46
+ if err != nil {
47
+ return err
48
+ }
49
+
50
+ if err := json.Unmarshal(jsonBytes, v); err != nil {
51
+ return err
52
+ }
53
+
54
+ return nil
55
+ }
56
+
57
+ type EventApp struct {
58
+ AppBase
59
+ AppData EventAppData `json:"appData"`
60
+ }
61
+
62
+ func NewEventApp(app App) (EventApp, error) {
63
+ appBase := app.GetAppBase()
64
+
65
+ appData, err := app.GetEventAppData()
66
+ if err != nil {
67
+ return EventApp{}, err
68
+ }
69
+
70
+ return EventApp{
71
+ AppBase: appBase,
72
+ AppData: appData,
73
+ }, nil
74
+ }
75
+
76
+ const (
77
+ AppEventSubsystem metadata.EventSubsystem = "app"
78
+ AppCreateEventName metadata.EventName = "app.created"
79
+ AppUpdateEventName metadata.EventName = "app.updated"
80
+ AppDeleteEventName metadata.EventName = "app.deleted"
81
+ )
82
+
83
+ // NewAppCreateEvent creates a new app create event
84
+ // TODO[later]: We should use eventApp instead of AppBase, but the creation flow is somewhat tricky to change as the flow
85
+ // is that the app calls the AppCreate without having the configuration presisted.
86
+ func NewAppCreateEvent(ctx context.Context, appBase AppBase) AppCreateEvent {
87
+ return AppCreateEvent{
88
+ AppBase: appBase,
89
+ UserID: session.GetSessionUserID(ctx),
90
+ }
91
+ }
92
+
93
+ // AppCreateEvent is an event that is emitted when an app is created
94
+ type AppCreateEvent struct {
95
+ AppBase
96
+ UserID *string `json:"userId,omitempty"`
97
+ }
98
+
99
+ func (e AppCreateEvent) EventName() string {
100
+ return metadata.GetEventName(metadata.EventType{
101
+ Subsystem: AppEventSubsystem,
102
+ Name: AppCreateEventName,
103
+ Version: "v1",
104
+ })
105
+ }
106
+
107
+ func (e AppCreateEvent) EventMetadata() metadata.EventMetadata {
108
+ resourcePath := metadata.ComposeResourcePath(e.AppBase.Namespace, metadata.EntityApp, e.AppBase.ID)
109
+
110
+ return metadata.EventMetadata{
111
+ ID: ulid.Make().String(),
112
+ Source: resourcePath,
113
+ Subject: resourcePath,
114
+ Time: e.AppBase.CreatedAt,
115
+ }
116
+ }
117
+
118
+ func (e AppCreateEvent) Validate() error {
119
+ if e.AppBase.ID == "" {
120
+ return fmt.Errorf("app base is required")
121
+ }
122
+ return nil
123
+ }
124
+
125
+ // NewAppUpdateEvent creates a new app update event
126
+ func NewAppUpdateEvent(ctx context.Context, app App) (AppUpdateEvent, error) {
127
+ eventApp, err := NewEventApp(app)
128
+ if err != nil {
129
+ return AppUpdateEvent{}, err
130
+ }
131
+
132
+ return AppUpdateEvent{
133
+ EventApp: eventApp,
134
+ UserID: session.GetSessionUserID(ctx),
135
+ }, nil
136
+ }
137
+
138
+ // AppUpdateEvent is an event that is emitted when an app is updated
139
+ type AppUpdateEvent struct {
140
+ EventApp
141
+ UserID *string `json:"userId,omitempty"`
142
+ }
143
+
144
+ func (e AppUpdateEvent) EventName() string {
145
+ return metadata.GetEventName(metadata.EventType{
146
+ Subsystem: AppEventSubsystem,
147
+ Name: AppUpdateEventName,
148
+ Version: "v2",
149
+ })
150
+ }
151
+
152
+ func (e AppUpdateEvent) EventMetadata() metadata.EventMetadata {
153
+ appBase := e.AppBase.GetAppBase()
154
+ resourcePath := metadata.ComposeResourcePath(appBase.Namespace, metadata.EntityApp, appBase.ID)
155
+
156
+ return metadata.EventMetadata{
157
+ ID: ulid.Make().String(),
158
+ Source: resourcePath,
159
+ Subject: resourcePath,
160
+ Time: appBase.UpdatedAt,
161
+ }
162
+ }
163
+
164
+ func (e AppUpdateEvent) Validate() error {
165
+ if e.AppBase.ID == "" {
166
+ return fmt.Errorf("app base is required")
167
+ }
168
+
169
+ return nil
170
+ }
171
+
172
+ // NewAppDeleteEvent creates a new app delete event
173
+ func NewAppDeleteEvent(ctx context.Context, app AppBase, appData EventAppData) AppDeleteEvent {
174
+ return AppDeleteEvent{
175
+ EventApp: EventApp{
176
+ AppBase: app,
177
+ AppData: appData,
178
+ },
179
+ UserID: session.GetSessionUserID(ctx),
180
+ }
181
+ }
182
+
183
+ // AppDeleteEvent is an event that is emitted when an app is deleted
184
+ type AppDeleteEvent struct {
185
+ EventApp
186
+ UserID *string `json:"userId,omitempty"`
187
+ }
188
+
189
+ func (e AppDeleteEvent) EventName() string {
190
+ return metadata.GetEventName(metadata.EventType{
191
+ Subsystem: AppEventSubsystem,
192
+ Name: AppDeleteEventName,
193
+ Version: "v2",
194
+ })
195
+ }
196
+
197
+ func (e AppDeleteEvent) EventMetadata() metadata.EventMetadata {
198
+ resourcePath := metadata.ComposeResourcePath(e.AppBase.Namespace, metadata.EntityApp, e.AppBase.ID)
199
+
200
+ return metadata.EventMetadata{
201
+ ID: ulid.Make().String(),
202
+ Source: resourcePath,
203
+ Subject: resourcePath,
204
+ Time: *e.AppBase.DeletedAt,
205
+ }
206
+ }
207
+
208
+ func (e AppDeleteEvent) Validate() error {
209
+ if e.AppBase.ID == "" {
210
+ return fmt.Errorf("app base is required")
211
+ }
212
+
213
+ return nil
214
+ }
app/events.go ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package app
2
+
3
+ import (
4
+ "fmt"
5
+
6
+ "github.com/openmeterio/openmeter/openmeter/customer"
7
+ "github.com/openmeterio/openmeter/openmeter/event/metadata"
8
+ "github.com/openmeterio/openmeter/openmeter/watermill/marshaler"
9
+ )
10
+
11
+ const (
12
+ EventSubsystemAppCustomer = "app_customer"
13
+ )
14
+
15
+ type PaymentSetupAppData interface {
16
+ Validate() error
17
+ }
18
+
19
+ type CustomerPaymentSetupResult struct {
20
+ Metadata map[string]string `json:"metadata,omitempty"`
21
+ // Add additional fields here as needed. Keep in mind that this event is app neutral, so please create abstractions on top of app specific data if needed.
22
+ // The consumer can always query the specific app data. (If this does not cut it on the long run, we need to have per app event types, which is an overkill)
23
+ }
24
+
25
+ func (r CustomerPaymentSetupResult) Validate() error {
26
+ return nil
27
+ }
28
+
29
+ type CustomerPaymentSetupSucceededEvent struct {
30
+ App AppBase `json:"app"`
31
+ Customer customer.CustomerID `json:"customer"`
32
+ Result CustomerPaymentSetupResult `json:"result"`
33
+ }
34
+
35
+ var (
36
+ _ marshaler.Event = CustomerPaymentSetupSucceededEvent{}
37
+
38
+ appCustomerDefaultPaymentMethodChangedEventName = metadata.GetEventName(metadata.EventType{
39
+ Subsystem: EventSubsystemAppCustomer,
40
+ Name: "payment_setup_succeeded",
41
+ Version: "v2",
42
+ })
43
+ )
44
+
45
+ func (e CustomerPaymentSetupSucceededEvent) Validate() error {
46
+ if err := e.App.Validate(); err != nil {
47
+ return fmt.Errorf("app: %w", err)
48
+ }
49
+
50
+ if err := e.Customer.Validate(); err != nil {
51
+ return fmt.Errorf("customer: %w", err)
52
+ }
53
+
54
+ if err := e.Result.Validate(); err != nil {
55
+ return fmt.Errorf("result: %w", err)
56
+ }
57
+
58
+ return nil
59
+ }
60
+
61
+ func (e CustomerPaymentSetupSucceededEvent) EventName() string {
62
+ return appCustomerDefaultPaymentMethodChangedEventName
63
+ }
64
+
65
+ func (e CustomerPaymentSetupSucceededEvent) EventMetadata() metadata.EventMetadata {
66
+ return metadata.EventMetadata{
67
+ Source: metadata.ComposeResourcePath(e.App.Namespace, metadata.EntityApp, e.App.ID),
68
+ Subject: metadata.ComposeResourcePath(e.Customer.Namespace, metadata.EntityCustomer, e.Customer.ID),
69
+ }
70
+ }
app/httpdriver/app.go ADDED
@@ -0,0 +1,261 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package httpdriver
2
+
3
+ import (
4
+ "context"
5
+ "fmt"
6
+ "net/http"
7
+
8
+ "github.com/samber/lo"
9
+
10
+ "github.com/openmeterio/openmeter/api"
11
+ "github.com/openmeterio/openmeter/openmeter/app"
12
+ appcustominvoicing "github.com/openmeterio/openmeter/openmeter/app/custominvoicing"
13
+ appsandbox "github.com/openmeterio/openmeter/openmeter/app/sandbox"
14
+ appstripe "github.com/openmeterio/openmeter/openmeter/app/stripe"
15
+ "github.com/openmeterio/openmeter/pkg/framework/commonhttp"
16
+ "github.com/openmeterio/openmeter/pkg/framework/transport/httptransport"
17
+ "github.com/openmeterio/openmeter/pkg/models"
18
+ "github.com/openmeterio/openmeter/pkg/pagination"
19
+ )
20
+
21
+ // ListAppsHandler is a handler for listing apps
22
+ type (
23
+ ListAppsRequest = app.ListAppInput
24
+ ListAppsResponse = api.AppPaginatedResponse
25
+ ListAppsParams = api.ListAppsParams
26
+ ListAppsHandler httptransport.HandlerWithArgs[ListAppsRequest, ListAppsResponse, ListAppsParams]
27
+ )
28
+
29
+ // ListApps returns a handler for listing apps
30
+ func (h *handler) ListApps() ListAppsHandler {
31
+ return httptransport.NewHandlerWithArgs(
32
+ func(ctx context.Context, r *http.Request, params ListAppsParams) (ListAppsRequest, error) {
33
+ // Resolve namespace
34
+ namespace, err := h.resolveNamespace(ctx)
35
+ if err != nil {
36
+ return ListAppsRequest{}, fmt.Errorf("failed to resolve namespace: %w", err)
37
+ }
38
+
39
+ return ListAppsRequest{
40
+ Namespace: namespace,
41
+ Page: pagination.Page{
42
+ PageSize: lo.FromPtrOr(params.PageSize, app.DefaultPageSize),
43
+ PageNumber: lo.FromPtrOr(params.Page, app.DefaultPageNumber),
44
+ },
45
+ }, nil
46
+ },
47
+ func(ctx context.Context, request ListAppsRequest) (ListAppsResponse, error) {
48
+ result, err := h.service.ListApps(ctx, request)
49
+ if err != nil {
50
+ return ListAppsResponse{}, fmt.Errorf("failed to list apps: %w", err)
51
+ }
52
+
53
+ items := make([]api.App, 0, len(result.Items))
54
+ for _, item := range result.Items {
55
+ app, err := MapAppToAPI(item)
56
+ if err != nil {
57
+ return ListAppsResponse{}, fmt.Errorf("failed to map app to api: %w", err)
58
+ }
59
+
60
+ items = append(items, app)
61
+ }
62
+
63
+ return ListAppsResponse{
64
+ Page: result.Page.PageNumber,
65
+ PageSize: result.Page.PageSize,
66
+ TotalCount: result.TotalCount,
67
+ Items: items,
68
+ }, nil
69
+ },
70
+ commonhttp.JSONResponseEncoderWithStatus[ListAppsResponse](http.StatusOK),
71
+ httptransport.AppendOptions(
72
+ h.options,
73
+ httptransport.WithOperationName("listApps"),
74
+ )...,
75
+ )
76
+ }
77
+
78
+ // GetAppHandler is a handler to get an app by id
79
+ type (
80
+ GetAppRequest = app.GetAppInput
81
+ GetAppResponse = api.App
82
+ GetAppHandler httptransport.HandlerWithArgs[GetAppRequest, GetAppResponse, string]
83
+ )
84
+
85
+ // GetApp returns an app handler
86
+ func (h *handler) GetApp() GetAppHandler {
87
+ return httptransport.NewHandlerWithArgs(
88
+ func(ctx context.Context, r *http.Request, appId string) (GetAppRequest, error) {
89
+ // Resolve namespace
90
+ namespace, err := h.resolveNamespace(ctx)
91
+ if err != nil {
92
+ return GetAppRequest{}, fmt.Errorf("failed to resolve namespace: %w", err)
93
+ }
94
+
95
+ return GetAppRequest{
96
+ Namespace: namespace,
97
+ ID: appId,
98
+ }, nil
99
+ },
100
+ func(ctx context.Context, request GetAppRequest) (GetAppResponse, error) {
101
+ app, err := h.service.GetApp(ctx, request)
102
+ if err != nil {
103
+ return GetAppResponse{}, fmt.Errorf("failed to get app: %w", err)
104
+ }
105
+
106
+ return MapAppToAPI(app)
107
+ },
108
+ commonhttp.JSONResponseEncoderWithStatus[GetAppResponse](http.StatusOK),
109
+ httptransport.AppendOptions(
110
+ h.options,
111
+ httptransport.WithOperationName("getApp"),
112
+ )...,
113
+ )
114
+ }
115
+
116
+ // UpdateAppHandler is a handler to update an app
117
+ type (
118
+ UpdateAppRequest = app.UpdateAppInput
119
+ UpdateAppResponse = api.App
120
+ UpdateAppHandler httptransport.HandlerWithArgs[UpdateAppRequest, UpdateAppResponse, string]
121
+ )
122
+
123
+ // UpdateApp returns an app handler
124
+ func (h *handler) UpdateApp() UpdateAppHandler {
125
+ return httptransport.NewHandlerWithArgs(
126
+ func(ctx context.Context, r *http.Request, appId string) (UpdateAppRequest, error) {
127
+ // Resolve namespace
128
+ namespace, err := h.resolveNamespace(ctx)
129
+ if err != nil {
130
+ return UpdateAppRequest{}, fmt.Errorf("failed to resolve namespace: %w", err)
131
+ }
132
+
133
+ var body api.UpdateAppJSONRequestBody
134
+ if err := commonhttp.JSONRequestBodyDecoder(r, &body); err != nil {
135
+ return UpdateAppRequest{}, fmt.Errorf("field to decode upsert customer data request: %w", err)
136
+ }
137
+
138
+ updateType, err := body.Discriminator()
139
+ if err != nil {
140
+ return UpdateAppRequest{}, models.NewGenericValidationError(fmt.Errorf("failed to get update type: %w", err))
141
+ }
142
+
143
+ switch updateType {
144
+ case string(app.AppTypeStripe):
145
+ payload, err := body.AsStripeAppReplaceUpdate()
146
+ if err != nil {
147
+ return UpdateAppRequest{}, fmt.Errorf("failed to get stripe app replace update: %w", err)
148
+ }
149
+
150
+ return UpdateAppRequest{
151
+ AppID: app.AppID{
152
+ ID: appId,
153
+ Namespace: namespace,
154
+ },
155
+ Name: payload.Name,
156
+ Description: payload.Description,
157
+ Metadata: payload.Metadata,
158
+ AppConfigUpdate: appstripe.Configuration{
159
+ SecretAPIKey: payload.SecretAPIKey,
160
+ },
161
+ }, nil
162
+
163
+ case string(app.AppTypeSandbox):
164
+ payload, err := body.AsSandboxAppReplaceUpdate()
165
+ if err != nil {
166
+ return UpdateAppRequest{}, fmt.Errorf("failed to get sandbox app replace update: %w", err)
167
+ }
168
+
169
+ return UpdateAppRequest{
170
+ AppID: app.AppID{
171
+ ID: appId,
172
+ Namespace: namespace,
173
+ },
174
+ Name: payload.Name,
175
+ Description: payload.Description,
176
+ Metadata: payload.Metadata,
177
+ AppConfigUpdate: appsandbox.Configuration{},
178
+ }, nil
179
+ case string(app.AppTypeCustomInvoicing):
180
+ payload, err := body.AsCustomInvoicingAppReplaceUpdate()
181
+ if err != nil {
182
+ return UpdateAppRequest{}, fmt.Errorf("failed to get custom invoicing app replace update: %w", err)
183
+ }
184
+
185
+ return UpdateAppRequest{
186
+ AppID: app.AppID{
187
+ ID: appId,
188
+ Namespace: namespace,
189
+ },
190
+ Name: payload.Name,
191
+ Description: payload.Description,
192
+ Metadata: payload.Metadata,
193
+ AppConfigUpdate: appcustominvoicing.Configuration{
194
+ EnableDraftSyncHook: payload.EnableDraftSyncHook,
195
+ EnableIssuingSyncHook: payload.EnableIssuingSyncHook,
196
+ },
197
+ }, nil
198
+ default:
199
+ return UpdateAppRequest{}, models.NewGenericValidationError(fmt.Errorf("invalid app type: %s", updateType))
200
+ }
201
+ },
202
+ func(ctx context.Context, request UpdateAppRequest) (UpdateAppResponse, error) {
203
+ app, err := h.service.UpdateApp(ctx, request)
204
+ if err != nil {
205
+ return UpdateAppResponse{}, fmt.Errorf("failed to update app: %w", err)
206
+ }
207
+
208
+ return MapAppToAPI(app)
209
+ },
210
+ commonhttp.JSONResponseEncoderWithStatus[UpdateAppResponse](http.StatusOK),
211
+ httptransport.AppendOptions(
212
+ h.options,
213
+ httptransport.WithOperationName("updateApp"),
214
+ )...,
215
+ )
216
+ }
217
+
218
+ // UninstallAppHandler is a handler to uninstalls an app by id
219
+ type (
220
+ UninstallAppRequest = app.UninstallAppInput
221
+ UninstallAppResponse = interface{}
222
+ UninstallAppHandler httptransport.HandlerWithArgs[UninstallAppRequest, UninstallAppResponse, string]
223
+ )
224
+
225
+ // UninstallApp uninstalls an app
226
+ func (h *handler) UninstallApp() UninstallAppHandler {
227
+ return httptransport.NewHandlerWithArgs(
228
+ func(ctx context.Context, r *http.Request, appId string) (UninstallAppRequest, error) {
229
+ // Resolve namespace
230
+ namespace, err := h.resolveNamespace(ctx)
231
+ if err != nil {
232
+ return UninstallAppRequest{}, fmt.Errorf("failed to resolve namespace: %w", err)
233
+ }
234
+
235
+ return UninstallAppRequest{
236
+ Namespace: namespace,
237
+ ID: appId,
238
+ }, nil
239
+ },
240
+ func(ctx context.Context, request UninstallAppRequest) (UninstallAppResponse, error) {
241
+ // Check if the app is not used by any billing profile
242
+
243
+ if err := h.billingService.IsAppUsed(ctx, request); err != nil {
244
+ return nil, err
245
+ }
246
+
247
+ // Uninstall app
248
+ err := h.service.UninstallApp(ctx, request)
249
+ if err != nil {
250
+ return nil, fmt.Errorf("failed to uninstall app: %w", err)
251
+ }
252
+
253
+ return nil, nil
254
+ },
255
+ commonhttp.EmptyResponseEncoder[UninstallAppResponse](http.StatusNoContent),
256
+ httptransport.AppendOptions(
257
+ h.options,
258
+ httptransport.WithOperationName("uninstallApp"),
259
+ )...,
260
+ )
261
+ }
app/httpdriver/customer.go ADDED
@@ -0,0 +1,421 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package httpdriver
2
+
3
+ import (
4
+ "context"
5
+ "fmt"
6
+ "net/http"
7
+
8
+ "github.com/samber/lo"
9
+
10
+ "github.com/openmeterio/openmeter/api"
11
+ "github.com/openmeterio/openmeter/openmeter/app"
12
+ appcustominvoicing "github.com/openmeterio/openmeter/openmeter/app/custominvoicing"
13
+ appsandbox "github.com/openmeterio/openmeter/openmeter/app/sandbox"
14
+ appstripe "github.com/openmeterio/openmeter/openmeter/app/stripe"
15
+ "github.com/openmeterio/openmeter/openmeter/billing"
16
+ "github.com/openmeterio/openmeter/openmeter/customer"
17
+ "github.com/openmeterio/openmeter/pkg/framework/commonhttp"
18
+ "github.com/openmeterio/openmeter/pkg/framework/transport/httptransport"
19
+ "github.com/openmeterio/openmeter/pkg/models"
20
+ "github.com/openmeterio/openmeter/pkg/pagination"
21
+ )
22
+
23
+ type (
24
+ ListCustomerDataRequest = app.ListCustomerInput
25
+ ListCustomerDataResponse = api.CustomerAppDataPaginatedResponse
26
+ ListCustomerDataHandler httptransport.HandlerWithArgs[ListCustomerDataRequest, ListCustomerDataResponse, ListCustomerDataParams]
27
+ )
28
+
29
+ type ListCustomerDataParams struct {
30
+ api.ListCustomerAppDataParams
31
+ CustomerIdOrKey string
32
+ }
33
+
34
+ // ListCustomerData returns a handler for listing customers app data.
35
+ func (h *handler) ListCustomerData() ListCustomerDataHandler {
36
+ return httptransport.NewHandlerWithArgs(
37
+ func(ctx context.Context, r *http.Request, params ListCustomerDataParams) (ListCustomerDataRequest, error) {
38
+ ns, err := h.resolveNamespace(ctx)
39
+ if err != nil {
40
+ return ListCustomerDataRequest{}, err
41
+ }
42
+
43
+ // Get the customer
44
+ cus, err := h.customerService.GetCustomer(ctx, customer.GetCustomerInput{
45
+ CustomerIDOrKey: &customer.CustomerIDOrKey{
46
+ IDOrKey: params.CustomerIdOrKey,
47
+ Namespace: ns,
48
+ },
49
+ })
50
+ if err != nil {
51
+ return ListCustomerDataRequest{}, err
52
+ }
53
+
54
+ if cus != nil && cus.IsDeleted() {
55
+ return ListCustomerDataRequest{},
56
+ models.NewGenericPreConditionFailedError(
57
+ fmt.Errorf("customer is deleted [namespace=%s customer.id=%s]", cus.Namespace, cus.ID),
58
+ )
59
+ }
60
+
61
+ req := ListCustomerDataRequest{
62
+ CustomerID: cus.GetID(),
63
+
64
+ // Pagination
65
+ Page: pagination.Page{
66
+ PageSize: lo.FromPtrOr(params.PageSize, customer.DefaultPageSize),
67
+ PageNumber: lo.FromPtrOr(params.Page, customer.DefaultPageNumber),
68
+ },
69
+ }
70
+
71
+ if params.Type != nil {
72
+ req.Type = lo.ToPtr(app.AppType(*params.Type))
73
+ }
74
+
75
+ return req, nil
76
+ },
77
+ func(ctx context.Context, request ListCustomerDataRequest) (ListCustomerDataResponse, error) {
78
+ resp, err := h.service.ListCustomerData(ctx, request)
79
+ if err != nil {
80
+ return ListCustomerDataResponse{}, fmt.Errorf("failed to list customers: %w", err)
81
+ }
82
+
83
+ items := make([]api.CustomerAppData, 0, len(resp.Items))
84
+
85
+ for _, customerApp := range resp.Items {
86
+ item, err := h.toAPICustomerAppData(customerApp)
87
+ if err != nil {
88
+ return ListCustomerDataResponse{}, fmt.Errorf("failed to cast app customer data: %w", err)
89
+ }
90
+
91
+ items = append(items, item)
92
+ }
93
+
94
+ return ListCustomerDataResponse{
95
+ Items: items,
96
+ Page: resp.Page.PageNumber,
97
+ PageSize: resp.Page.PageSize,
98
+ TotalCount: resp.TotalCount,
99
+ }, nil
100
+ },
101
+ commonhttp.JSONResponseEncoderWithStatus[ListCustomerDataResponse](http.StatusOK),
102
+ httptransport.AppendOptions(
103
+ h.options,
104
+ httptransport.WithOperationName("listCustomerData"),
105
+ )...,
106
+ )
107
+ }
108
+
109
+ type UpsertCustomerDataRequest struct {
110
+ CustomerId customer.CustomerID
111
+ Data []api.CustomerAppData
112
+ }
113
+
114
+ type UpsertCustomerDataParams struct {
115
+ CustomerIdOrKey string
116
+ }
117
+
118
+ type (
119
+ UpsertCustomerDataResponse = interface{}
120
+ UpsertCustomerDataHandler httptransport.HandlerWithArgs[UpsertCustomerDataRequest, UpsertCustomerDataResponse, UpsertCustomerDataParams]
121
+ )
122
+
123
+ // UpsertCustomerData returns a new httptransport.Handler for creating a customer.
124
+ func (h *handler) UpsertCustomerData() UpsertCustomerDataHandler {
125
+ return httptransport.NewHandlerWithArgs(
126
+ func(ctx context.Context, r *http.Request, params UpsertCustomerDataParams) (UpsertCustomerDataRequest, error) {
127
+ ns, err := h.resolveNamespace(ctx)
128
+ if err != nil {
129
+ return UpsertCustomerDataRequest{}, err
130
+ }
131
+
132
+ // Get the customer and ensure we do not update customer data if the customer is already deleted
133
+ cus, err := h.customerService.GetCustomer(ctx, customer.GetCustomerInput{
134
+ CustomerIDOrKey: &customer.CustomerIDOrKey{
135
+ IDOrKey: params.CustomerIdOrKey,
136
+ Namespace: ns,
137
+ },
138
+ })
139
+ if err != nil {
140
+ return UpsertCustomerDataRequest{}, err
141
+ }
142
+
143
+ if cus != nil && cus.IsDeleted() {
144
+ return UpsertCustomerDataRequest{},
145
+ models.NewGenericPreConditionFailedError(
146
+ fmt.Errorf("customer is deleted [namespace=%s customer.id=%s]", cus.Namespace, cus.ID),
147
+ )
148
+ }
149
+
150
+ var body []api.CustomerAppData
151
+
152
+ if err := commonhttp.JSONRequestBodyDecoder(r, &body); err != nil {
153
+ return UpsertCustomerDataRequest{},
154
+ fmt.Errorf("field to decode upsert customer data request: %w", err)
155
+ }
156
+
157
+ return UpsertCustomerDataRequest{
158
+ CustomerId: cus.GetID(),
159
+ Data: body,
160
+ }, nil
161
+ },
162
+ func(ctx context.Context, req UpsertCustomerDataRequest) (UpsertCustomerDataResponse, error) {
163
+ for _, apiCustomerData := range req.Data {
164
+ customerApp, customerData, err := h.toCustomerData(ctx, req.CustomerId, apiCustomerData)
165
+ if err != nil {
166
+ return nil, err
167
+ }
168
+
169
+ err = customerApp.UpsertCustomerData(ctx, app.UpsertAppInstanceCustomerDataInput{
170
+ CustomerID: req.CustomerId,
171
+ Data: customerData,
172
+ })
173
+ if err != nil {
174
+ return nil, err
175
+ }
176
+ }
177
+
178
+ return nil, nil
179
+ },
180
+ commonhttp.EmptyResponseEncoder[UpsertCustomerDataResponse](http.StatusOK),
181
+ httptransport.AppendOptions(
182
+ h.options,
183
+ httptransport.WithOperationName("upsertCustomerData"),
184
+ )...,
185
+ )
186
+ }
187
+
188
+ type DeleteCustomerDataParams struct {
189
+ CustomerIdOrKey string
190
+ AppId string
191
+ }
192
+
193
+ type DeleteCustomerDataRequest struct {
194
+ AppID app.AppID
195
+ CustomerID customer.CustomerID
196
+ }
197
+
198
+ type (
199
+ DeleteCustomerDataResponse = interface{}
200
+ DeleteCustomerDataHandler httptransport.HandlerWithArgs[DeleteCustomerDataRequest, DeleteCustomerDataResponse, DeleteCustomerDataParams]
201
+ )
202
+
203
+ // DeleteCustomerData returns a handler for deleting a customer data.
204
+ func (h *handler) DeleteCustomerData() DeleteCustomerDataHandler {
205
+ return httptransport.NewHandlerWithArgs(
206
+ func(ctx context.Context, r *http.Request, params DeleteCustomerDataParams) (DeleteCustomerDataRequest, error) {
207
+ ns, err := h.resolveNamespace(ctx)
208
+ if err != nil {
209
+ return DeleteCustomerDataRequest{}, err
210
+ }
211
+
212
+ // Get the customer
213
+ cus, err := h.customerService.GetCustomer(ctx, customer.GetCustomerInput{
214
+ CustomerIDOrKey: &customer.CustomerIDOrKey{
215
+ IDOrKey: params.CustomerIdOrKey,
216
+ Namespace: ns,
217
+ },
218
+ })
219
+ if err != nil {
220
+ return DeleteCustomerDataRequest{}, err
221
+ }
222
+
223
+ if cus != nil && cus.IsDeleted() {
224
+ return DeleteCustomerDataRequest{},
225
+ models.NewGenericPreConditionFailedError(
226
+ fmt.Errorf("customer is deleted [namespace=%s customer.id=%s]", cus.Namespace, cus.ID),
227
+ )
228
+ }
229
+
230
+ return DeleteCustomerDataRequest{
231
+ CustomerID: cus.GetID(),
232
+ AppID: app.AppID{
233
+ Namespace: ns,
234
+ ID: params.AppId,
235
+ },
236
+ }, nil
237
+ },
238
+ func(ctx context.Context, request DeleteCustomerDataRequest) (DeleteCustomerDataResponse, error) {
239
+ // Get app
240
+ existingApp, err := h.service.GetApp(ctx, request.AppID)
241
+ if err != nil {
242
+ return nil, err
243
+ }
244
+
245
+ // Delete customer data
246
+ err = existingApp.DeleteCustomerData(ctx, app.DeleteAppInstanceCustomerDataInput{
247
+ CustomerID: request.CustomerID,
248
+ })
249
+ if err != nil {
250
+ return nil, err
251
+ }
252
+
253
+ return nil, nil
254
+ },
255
+ commonhttp.EmptyResponseEncoder[DeleteCustomerDataResponse](http.StatusNoContent),
256
+ httptransport.AppendOptions(
257
+ h.options,
258
+ httptransport.WithOperationName("deleteCustomerData"),
259
+ )...,
260
+ )
261
+ }
262
+
263
+ // toCustomerData converts an API CustomerAppData to a CustomerData model
264
+ func (h *handler) toCustomerData(ctx context.Context, customerID customer.CustomerID, apiApp api.CustomerAppData) (app.App, app.CustomerData, error) {
265
+ // Get app type
266
+ appType, err := apiApp.Discriminator()
267
+ if err != nil {
268
+ return nil, nil, fmt.Errorf("error getting app type: %w", err)
269
+ }
270
+
271
+ switch appType {
272
+ // Sandbox app
273
+ case string(app.AppTypeSandbox):
274
+ // Parse as sandbox app
275
+ apiSandboxCustomerData, err := apiApp.AsSandboxCustomerAppData()
276
+ if err != nil {
277
+ return nil, nil, fmt.Errorf("error converting to stripe app: %w", err)
278
+ }
279
+
280
+ // Resolve app
281
+ resolvedApp, err := h.resolveCustomerApp(ctx, customerID, app.AppTypeSandbox, apiSandboxCustomerData.Id)
282
+ if err != nil {
283
+ return nil, nil, fmt.Errorf("error resolving sandbox app: %w", err)
284
+ }
285
+
286
+ // Create customer data
287
+ sandboxCustomerData := appsandbox.CustomerData{}
288
+
289
+ return resolvedApp, sandboxCustomerData, nil
290
+
291
+ // Stripe app
292
+ case string(app.AppTypeStripe):
293
+ // Parse as stripe app
294
+ apiStripeCustomerData, err := apiApp.AsStripeCustomerAppData()
295
+ if err != nil {
296
+ return nil, nil, fmt.Errorf("error converting to stripe app: %w", err)
297
+ }
298
+
299
+ // Resolve app
300
+ resolvedApp, err := h.resolveCustomerApp(ctx, customerID, app.AppTypeStripe, apiStripeCustomerData.Id)
301
+ if err != nil {
302
+ return nil, nil, fmt.Errorf("error resolving stripe app: %w", err)
303
+ }
304
+
305
+ // Create customer data
306
+ stripeCustomerData := fromAPIAppStripeCustomerData(apiStripeCustomerData)
307
+
308
+ return resolvedApp, stripeCustomerData, nil
309
+ case string(app.AppTypeCustomInvoicing):
310
+ // Parse as custom invoicing app
311
+ apiCustomInvoicingCustomerData, err := apiApp.AsCustomInvoicingCustomerAppData()
312
+ if err != nil {
313
+ return nil, nil, fmt.Errorf("error converting to custom invoicing app: %w", err)
314
+ }
315
+
316
+ // Resolve app
317
+ resolvedApp, err := h.resolveCustomerApp(ctx, customerID, app.AppTypeCustomInvoicing, apiCustomInvoicingCustomerData.Id)
318
+ if err != nil {
319
+ return nil, nil, fmt.Errorf("error resolving custom invoicing app: %w", err)
320
+ }
321
+
322
+ // Create customer data
323
+ customInvoicingCustomerData := appcustominvoicing.CustomerData{
324
+ Metadata: lo.FromPtrOr(apiCustomInvoicingCustomerData.Metadata, map[string]string{}),
325
+ }
326
+
327
+ return resolvedApp, customInvoicingCustomerData, nil
328
+ }
329
+
330
+ return nil, nil, fmt.Errorf("unsupported app type: %s", appType)
331
+ }
332
+
333
+ // resolveCustomerApp resolves a customer app based on the app type or app ID.
334
+ func (h *handler) resolveCustomerApp(ctx context.Context, customerID customer.CustomerID, appType app.AppType, appID *string) (app.App, error) {
335
+ var resolvedApp app.App
336
+ var err error
337
+
338
+ // Get app ID from API data or get default app for billing profile
339
+ if appID != nil {
340
+ return h.service.GetApp(ctx, app.GetAppInput{
341
+ Namespace: customerID.Namespace,
342
+ ID: *appID,
343
+ })
344
+ }
345
+
346
+ // Get the customer app by type
347
+ resolvedApp, err = h.billingService.GetCustomerApp(ctx, billing.GetCustomerAppInput{
348
+ CustomerID: customerID,
349
+ AppType: appType,
350
+ })
351
+ if err != nil {
352
+ return nil, fmt.Errorf("error getting customer app: %w", err)
353
+ }
354
+
355
+ return resolvedApp, nil
356
+ }
357
+
358
+ // toAPICustomerAppData converts a CustomerApp to an API CustomerAppData
359
+ func (h *handler) toAPICustomerAppData(a app.CustomerApp) (api.CustomerAppData, error) {
360
+ apiCustomerAppData := api.CustomerAppData{}
361
+ appId := a.App.GetID().ID
362
+
363
+ switch customerAppData := a.CustomerData.(type) {
364
+ case appstripe.CustomerData:
365
+ stripeApp, ok := a.App.(appstripe.App)
366
+ if !ok {
367
+ return apiCustomerAppData, fmt.Errorf("error casting app to stripe app")
368
+ }
369
+
370
+ // Convert to API stripe customer app data
371
+ apiStripeCustomerAppData := ToAPIStripeCustomerAppData(customerAppData, stripeApp)
372
+
373
+ // Convert to API customer app data
374
+ err := apiCustomerAppData.FromStripeCustomerAppData(apiStripeCustomerAppData)
375
+ if err != nil {
376
+ return apiCustomerAppData, fmt.Errorf("error converting to stripe customer app: %w", err)
377
+ }
378
+
379
+ case appsandbox.CustomerData:
380
+ sandboxApp, ok := a.App.(appsandbox.App)
381
+ if !ok {
382
+ return apiCustomerAppData, fmt.Errorf("error casting app to sandbox app")
383
+ }
384
+
385
+ apiApp := mapSandboxAppToAPI(sandboxApp.Meta)
386
+
387
+ apiSandboxCustomerAppData := api.SandboxCustomerAppData{
388
+ Id: &appId,
389
+ Type: api.SandboxCustomerAppDataTypeSandbox,
390
+ App: &apiApp,
391
+ }
392
+
393
+ err := apiCustomerAppData.FromSandboxCustomerAppData(apiSandboxCustomerAppData)
394
+ if err != nil {
395
+ return apiCustomerAppData, fmt.Errorf("error converting to sandbox customer app: %w", err)
396
+ }
397
+
398
+ case appcustominvoicing.CustomerData:
399
+ customInvoicingApp, ok := a.App.(appcustominvoicing.App)
400
+ if !ok {
401
+ return apiCustomerAppData, fmt.Errorf("error casting app to custom invoicing app")
402
+ }
403
+
404
+ apiApp := mapCustomInvoicingAppToAPI(customInvoicingApp.Meta)
405
+
406
+ apiCustomInvoicingCustomerAppData := api.CustomInvoicingCustomerAppData{
407
+ Id: &appId,
408
+ Type: api.CustomInvoicingCustomerAppDataTypeCustomInvoicing,
409
+ App: &apiApp,
410
+ }
411
+
412
+ err := apiCustomerAppData.FromCustomInvoicingCustomerAppData(apiCustomInvoicingCustomerAppData)
413
+ if err != nil {
414
+ return apiCustomerAppData, fmt.Errorf("error converting to custom invoicing customer app: %w", err)
415
+ }
416
+ default:
417
+ return apiCustomerAppData, fmt.Errorf("unsupported customer data for app: %s", appId)
418
+ }
419
+
420
+ return apiCustomerAppData, nil
421
+ }
app/httpdriver/handler.go ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package httpdriver
2
+
3
+ import (
4
+ "context"
5
+ "errors"
6
+ "log/slog"
7
+ "net/http"
8
+
9
+ "github.com/openmeterio/openmeter/openmeter/app"
10
+ stripeapp "github.com/openmeterio/openmeter/openmeter/app/stripe"
11
+ "github.com/openmeterio/openmeter/openmeter/billing"
12
+ "github.com/openmeterio/openmeter/openmeter/customer"
13
+ "github.com/openmeterio/openmeter/openmeter/namespace/namespacedriver"
14
+ "github.com/openmeterio/openmeter/pkg/framework/commonhttp"
15
+ "github.com/openmeterio/openmeter/pkg/framework/transport/httptransport"
16
+ )
17
+
18
+ type Handler interface {
19
+ AppHandler
20
+ }
21
+
22
+ type AppHandler interface {
23
+ // App handlers
24
+ ListApps() ListAppsHandler
25
+ GetApp() GetAppHandler
26
+ UninstallApp() UninstallAppHandler
27
+ UpdateApp() UpdateAppHandler
28
+
29
+ // Customer Data handlers
30
+ ListCustomerData() ListCustomerDataHandler
31
+ UpsertCustomerData() UpsertCustomerDataHandler
32
+ DeleteCustomerData() DeleteCustomerDataHandler
33
+
34
+ // Marketplace handlers
35
+ ListMarketplaceListings() ListMarketplaceListingsHandler
36
+ GetMarketplaceListing() GetMarketplaceListingHandler
37
+ MarketplaceAppAPIKeyInstall() MarketplaceAppAPIKeyInstallHandler
38
+ MarketplaceAppInstall() MarketplaceAppInstallHandler
39
+ }
40
+
41
+ var _ Handler = (*handler)(nil)
42
+
43
+ type handler struct {
44
+ service app.Service
45
+
46
+ stripeAppService stripeapp.Service
47
+ billingService billing.Service
48
+ customerService customer.Service
49
+ namespaceDecoder namespacedriver.NamespaceDecoder
50
+ options []httptransport.HandlerOption
51
+ }
52
+
53
+ func (h *handler) resolveNamespace(ctx context.Context) (string, error) {
54
+ ns, ok := h.namespaceDecoder.GetNamespace(ctx)
55
+ if !ok {
56
+ return "", commonhttp.NewHTTPError(http.StatusInternalServerError, errors.New("internal server error"))
57
+ }
58
+
59
+ return ns, nil
60
+ }
61
+
62
+ func New(
63
+ logger *slog.Logger,
64
+ namespaceDecoder namespacedriver.NamespaceDecoder,
65
+ appService app.Service,
66
+ appStripeService stripeapp.Service,
67
+ billingService billing.Service,
68
+ customerService customer.Service,
69
+
70
+ options ...httptransport.HandlerOption,
71
+ ) Handler {
72
+ return &handler{
73
+ service: appService,
74
+ namespaceDecoder: namespaceDecoder,
75
+ stripeAppService: appStripeService,
76
+ billingService: billingService,
77
+ customerService: customerService,
78
+ options: options,
79
+ }
80
+ }
app/httpdriver/mapper.go ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package httpdriver
2
+
3
+ import (
4
+ "errors"
5
+ "fmt"
6
+
7
+ "github.com/samber/lo"
8
+
9
+ "github.com/openmeterio/openmeter/api"
10
+ "github.com/openmeterio/openmeter/openmeter/app"
11
+ appcustominvoicing "github.com/openmeterio/openmeter/openmeter/app/custominvoicing"
12
+ appsandbox "github.com/openmeterio/openmeter/openmeter/app/sandbox"
13
+ appstripe "github.com/openmeterio/openmeter/openmeter/app/stripe"
14
+ )
15
+
16
+ // MapAppToAPI maps an app to an API app
17
+ func MapAppToAPI(item app.App) (api.App, error) {
18
+ if item == nil {
19
+ return api.App{}, errors.New("invalid app: nil")
20
+ }
21
+
22
+ switch item.GetType() {
23
+ case app.AppTypeStripe:
24
+ stripeApp := item.(appstripe.App)
25
+
26
+ app := api.App{}
27
+ if err := app.FromStripeApp(mapStripeAppToAPI(stripeApp.Meta)); err != nil {
28
+ return app, err
29
+ }
30
+
31
+ return app, nil
32
+ case app.AppTypeSandbox:
33
+ sandboxApp := item.(appsandbox.App)
34
+
35
+ app := api.App{}
36
+ if err := app.FromSandboxApp(mapSandboxAppToAPI(sandboxApp.Meta)); err != nil {
37
+ return app, err
38
+ }
39
+
40
+ return app, nil
41
+ case app.AppTypeCustomInvoicing:
42
+ customInvoicingApp := item.(appcustominvoicing.App)
43
+
44
+ app := api.App{}
45
+ if err := app.FromCustomInvoicingApp(mapCustomInvoicingAppToAPI(customInvoicingApp.Meta)); err != nil {
46
+ return app, err
47
+ }
48
+
49
+ return app, nil
50
+ default:
51
+ return api.App{}, fmt.Errorf("unsupported app type: %s", item.GetType())
52
+ }
53
+ }
54
+
55
+ func mapSandboxAppToAPI(app appsandbox.Meta) api.SandboxApp {
56
+ return api.SandboxApp{
57
+ Id: app.GetID().ID,
58
+ Type: api.SandboxAppTypeSandbox,
59
+ Name: app.GetName(),
60
+ Status: api.AppStatus(app.GetStatus()),
61
+ Listing: mapMarketplaceListing(app.GetListing()),
62
+ CreatedAt: app.CreatedAt,
63
+ UpdatedAt: app.UpdatedAt,
64
+ DeletedAt: app.DeletedAt,
65
+ }
66
+ }
67
+
68
+ func mapStripeAppToAPI(
69
+ stripeApp appstripe.Meta,
70
+ ) api.StripeApp {
71
+ apiStripeApp := api.StripeApp{
72
+ Id: stripeApp.GetID().ID,
73
+ Type: api.StripeAppType(stripeApp.GetType()),
74
+ Name: stripeApp.Name,
75
+ Status: api.AppStatus(stripeApp.GetStatus()),
76
+ Listing: mapMarketplaceListing(stripeApp.GetListing()),
77
+ MaskedAPIKey: stripeApp.MaskedAPIKey,
78
+ CreatedAt: stripeApp.CreatedAt,
79
+ UpdatedAt: stripeApp.UpdatedAt,
80
+ DeletedAt: stripeApp.DeletedAt,
81
+ StripeAccountId: stripeApp.StripeAccountID,
82
+ Livemode: stripeApp.Livemode,
83
+ }
84
+
85
+ apiStripeApp.Description = stripeApp.GetDescription()
86
+
87
+ if stripeApp.GetMetadata() != nil {
88
+ apiStripeApp.Metadata = lo.ToPtr(api.Metadata(stripeApp.GetMetadata()))
89
+ }
90
+
91
+ return apiStripeApp
92
+ }
93
+
94
+ func mapCustomInvoicingAppToAPI(app appcustominvoicing.Meta) api.CustomInvoicingApp {
95
+ return api.CustomInvoicingApp{
96
+ Id: app.GetID().ID,
97
+ Type: api.CustomInvoicingAppTypeCustomInvoicing,
98
+ Name: app.GetName(),
99
+ Status: api.AppStatus(app.GetStatus()),
100
+ Listing: mapMarketplaceListing(app.GetListing()),
101
+ Metadata: lo.EmptyableToPtr(api.Metadata(app.GetMetadata())),
102
+ Description: app.GetDescription(),
103
+ CreatedAt: app.CreatedAt,
104
+ UpdatedAt: app.UpdatedAt,
105
+ DeletedAt: app.DeletedAt,
106
+
107
+ EnableDraftSyncHook: app.Configuration.EnableDraftSyncHook,
108
+ EnableIssuingSyncHook: app.Configuration.EnableIssuingSyncHook,
109
+ }
110
+ }
111
+
112
+ func MapEventAppToAPI(event app.EventApp) (api.App, error) {
113
+ switch event.GetType() {
114
+ case app.AppTypeStripe:
115
+ target := appstripe.App{}
116
+ if err := target.FromEventAppData(event); err != nil {
117
+ return api.App{}, err
118
+ }
119
+
120
+ app := api.App{}
121
+ if err := app.FromStripeApp(mapStripeAppToAPI(target.Meta)); err != nil {
122
+ return api.App{}, err
123
+ }
124
+
125
+ return app, nil
126
+ case app.AppTypeSandbox:
127
+ target := appsandbox.Meta{}
128
+ if err := target.FromEventAppData(event); err != nil {
129
+ return api.App{}, err
130
+ }
131
+
132
+ app := api.App{}
133
+ if err := app.FromSandboxApp(mapSandboxAppToAPI(target)); err != nil {
134
+ return api.App{}, err
135
+ }
136
+
137
+ return app, nil
138
+ case app.AppTypeCustomInvoicing:
139
+ target := appcustominvoicing.App{}
140
+ if err := target.FromEventAppData(event); err != nil {
141
+ return api.App{}, err
142
+ }
143
+
144
+ app := api.App{}
145
+ if err := app.FromCustomInvoicingApp(mapCustomInvoicingAppToAPI(target.Meta)); err != nil {
146
+ return api.App{}, err
147
+ }
148
+
149
+ return app, nil
150
+ default:
151
+ return api.App{}, fmt.Errorf("unsupported app type: %s", event.GetType())
152
+ }
153
+ }
154
+
155
+ // fromAPIAppStripeCustomerData maps an API stripe customer data to an app stripe customer data
156
+ func fromAPIAppStripeCustomerData(apiStripeCustomerData api.StripeCustomerAppData) appstripe.CustomerData {
157
+ return appstripe.CustomerData{
158
+ StripeCustomerID: apiStripeCustomerData.StripeCustomerId,
159
+ StripeDefaultPaymentMethodID: apiStripeCustomerData.StripeDefaultPaymentMethodId,
160
+ }
161
+ }
162
+
163
+ // customerAppToAPI converts a CustomerApp to an API CustomerAppData
164
+ func ToAPIStripeCustomerAppData(
165
+ customerAppData appstripe.CustomerData,
166
+ stripeApp appstripe.App,
167
+ ) api.StripeCustomerAppData {
168
+ apiStripeCustomerAppData := api.StripeCustomerAppData{
169
+ Id: lo.ToPtr(stripeApp.GetID().ID),
170
+ Type: api.StripeCustomerAppDataTypeStripe,
171
+ App: lo.ToPtr(mapStripeAppToAPI(stripeApp.Meta)),
172
+ StripeCustomerId: customerAppData.StripeCustomerID,
173
+ StripeDefaultPaymentMethodId: customerAppData.StripeDefaultPaymentMethodID,
174
+ }
175
+
176
+ return apiStripeCustomerAppData
177
+ }
app/httpdriver/marketplace.go ADDED
@@ -0,0 +1,245 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package httpdriver
2
+
3
+ import (
4
+ "context"
5
+ "fmt"
6
+ "net/http"
7
+
8
+ "github.com/samber/lo"
9
+
10
+ "github.com/openmeterio/openmeter/api"
11
+ "github.com/openmeterio/openmeter/openmeter/app"
12
+ "github.com/openmeterio/openmeter/pkg/framework/commonhttp"
13
+ "github.com/openmeterio/openmeter/pkg/framework/transport/httptransport"
14
+ "github.com/openmeterio/openmeter/pkg/pagination"
15
+ )
16
+
17
+ // ListMarketplaceListingsHandler is a handler for listing marketplace listings
18
+ type (
19
+ ListMarketplaceListingsRequest = app.MarketplaceListInput
20
+ ListMarketplaceListingsResponse = api.MarketplaceListingPaginatedResponse
21
+ ListMarketplaceListingsParams = api.ListMarketplaceListingsParams
22
+ ListMarketplaceListingsHandler httptransport.HandlerWithArgs[ListMarketplaceListingsRequest, ListMarketplaceListingsResponse, ListMarketplaceListingsParams]
23
+ )
24
+
25
+ // ListMarketplaceListings returns a handler for listing marketplace listings
26
+ func (h *handler) ListMarketplaceListings() ListMarketplaceListingsHandler {
27
+ return httptransport.NewHandlerWithArgs(
28
+ func(ctx context.Context, r *http.Request, params ListMarketplaceListingsParams) (ListMarketplaceListingsRequest, error) {
29
+ return ListMarketplaceListingsRequest{
30
+ Page: pagination.Page{
31
+ PageSize: lo.FromPtrOr(params.PageSize, app.DefaultPageSize),
32
+ PageNumber: lo.FromPtrOr(params.Page, app.DefaultPageNumber),
33
+ },
34
+ }, nil
35
+ },
36
+ func(ctx context.Context, request ListMarketplaceListingsRequest) (ListMarketplaceListingsResponse, error) {
37
+ result, err := h.service.ListMarketplaceListings(ctx, request)
38
+ if err != nil {
39
+ return ListMarketplaceListingsResponse{}, fmt.Errorf("failed to list marketplace listings: %w", err)
40
+ }
41
+
42
+ return ListMarketplaceListingsResponse{
43
+ Page: result.Page.PageNumber,
44
+ PageSize: result.Page.PageSize,
45
+ TotalCount: result.TotalCount,
46
+ Items: lo.Map(result.Items, func(item app.RegistryItem, _ int) api.MarketplaceListing {
47
+ return mapMarketplaceListing(item.Listing)
48
+ }),
49
+ }, nil
50
+ },
51
+ commonhttp.JSONResponseEncoderWithStatus[ListMarketplaceListingsResponse](http.StatusOK),
52
+ httptransport.AppendOptions(
53
+ h.options,
54
+ httptransport.WithOperationName("listMarketplaceListings"),
55
+ )...,
56
+ )
57
+ }
58
+
59
+ // GetMarketplaceListingHandler is a handler to get a marketplace listing
60
+ type (
61
+ GetMarketplaceListingRequest = app.MarketplaceGetInput
62
+ GetMarketplaceListingResponse = api.MarketplaceListing
63
+ GetMarketplaceListingHandler httptransport.HandlerWithArgs[GetMarketplaceListingRequest, GetMarketplaceListingResponse, api.AppType]
64
+ )
65
+
66
+ // GetMarketplaceListing returns a handler for listing marketplace listings
67
+ func (h *handler) GetMarketplaceListing() GetMarketplaceListingHandler {
68
+ return httptransport.NewHandlerWithArgs(
69
+ func(ctx context.Context, r *http.Request, appType api.AppType) (GetMarketplaceListingRequest, error) {
70
+ return GetMarketplaceListingRequest{
71
+ Type: app.AppType(appType),
72
+ }, nil
73
+ },
74
+ func(ctx context.Context, request GetMarketplaceListingRequest) (GetMarketplaceListingResponse, error) {
75
+ result, err := h.service.GetMarketplaceListing(ctx, request)
76
+ if err != nil {
77
+ return GetMarketplaceListingResponse{}, fmt.Errorf("failed to get marketplace listing: %w", err)
78
+ }
79
+
80
+ return mapMarketplaceListing(result.Listing), nil
81
+ },
82
+ commonhttp.JSONResponseEncoderWithStatus[GetMarketplaceListingResponse](http.StatusOK),
83
+ httptransport.AppendOptions(
84
+ h.options,
85
+ httptransport.WithOperationName("getMarketplaceListing"),
86
+ )...,
87
+ )
88
+ }
89
+
90
+ type (
91
+ MarketplaceAppAPIKeyInstallResponse = api.MarketplaceInstallResponse
92
+ MarketplaceAppAPIKeyInstallHandler httptransport.HandlerWithArgs[MarketplaceAppAPIKeyInstallRequest, MarketplaceAppAPIKeyInstallResponse, api.AppType]
93
+ )
94
+
95
+ type MarketplaceAppAPIKeyInstallRequest struct {
96
+ app.InstallAppV3Input
97
+ CreateBillingProfile bool
98
+ }
99
+
100
+ // MarketplaceAppAPIKeyInstall returns a handler for installing an app type with an API key
101
+ func (h *handler) MarketplaceAppAPIKeyInstall() MarketplaceAppAPIKeyInstallHandler {
102
+ return httptransport.NewHandlerWithArgs(
103
+ func(ctx context.Context, r *http.Request, appType api.AppType) (MarketplaceAppAPIKeyInstallRequest, error) {
104
+ body := api.MarketplaceAppAPIKeyInstallJSONBody{}
105
+ if err := commonhttp.JSONRequestBodyDecoder(r, &body); err != nil {
106
+ return MarketplaceAppAPIKeyInstallRequest{}, fmt.Errorf("field to decode marketplace app install request: %w", err)
107
+ }
108
+
109
+ // Resolve namespace
110
+ namespace, err := h.resolveNamespace(ctx)
111
+ if err != nil {
112
+ return MarketplaceAppAPIKeyInstallRequest{}, fmt.Errorf("failed to resolve namespace: %w", err)
113
+ }
114
+
115
+ req := MarketplaceAppAPIKeyInstallRequest{
116
+ InstallAppV3Input: app.InstallAppV3Input{
117
+ MarketplaceListingID: app.MarketplaceListingID{Type: app.AppType(appType)},
118
+ Namespace: namespace,
119
+ Name: lo.FromPtr(body.Name),
120
+ APIKey: lo.ToPtr(body.ApiKey),
121
+ },
122
+ CreateBillingProfile: lo.FromPtrOr(body.CreateBillingProfile, true),
123
+ }
124
+
125
+ return req, nil
126
+ },
127
+ func(ctx context.Context, request MarketplaceAppAPIKeyInstallRequest) (MarketplaceAppAPIKeyInstallResponse, error) {
128
+ resp := MarketplaceAppAPIKeyInstallResponse{
129
+ DefaultForCapabilityTypes: []api.AppCapabilityType{},
130
+ }
131
+
132
+ // Install app
133
+ installedApp, err := h.service.InstallApp(ctx, request.InstallAppV3Input)
134
+ if err != nil {
135
+ return resp, err
136
+ }
137
+
138
+ // Map app to API
139
+ apiApp, err := MapAppToAPI(installedApp.App)
140
+ if err != nil {
141
+ return resp, fmt.Errorf("failed to map app to API: %w", err)
142
+ }
143
+
144
+ resp.App = apiApp
145
+ resp.DefaultForCapabilityTypes = lo.Map(installedApp.DefaultCapabilies, func(c app.CapabilityType, _ int) api.AppCapabilityType {
146
+ return api.AppCapabilityType(c)
147
+ })
148
+
149
+ return resp, nil
150
+ },
151
+ commonhttp.JSONResponseEncoderWithStatus[MarketplaceAppAPIKeyInstallResponse](http.StatusOK),
152
+ httptransport.AppendOptions(
153
+ h.options,
154
+ httptransport.WithOperationName("marketplaceAppAPIKeyInstall"),
155
+ )...,
156
+ )
157
+ }
158
+
159
+ type (
160
+ MarketplaceAppInstallResponse = api.MarketplaceInstallResponse
161
+ MarketplaceAppInstallHandler httptransport.HandlerWithArgs[MarketplaceAppInstallRequest, MarketplaceAppInstallResponse, api.AppType]
162
+ )
163
+
164
+ type MarketplaceAppInstallRequest struct {
165
+ app.InstallAppV3Input
166
+ CreateBillingProfile bool
167
+ }
168
+
169
+ // MarketplaceAppInstall returns a handler for installing an app type
170
+ func (h *handler) MarketplaceAppInstall() MarketplaceAppInstallHandler {
171
+ return httptransport.NewHandlerWithArgs(
172
+ func(ctx context.Context, r *http.Request, appType api.AppType) (MarketplaceAppInstallRequest, error) {
173
+ body := api.MarketplaceInstallRequestPayload{}
174
+ if err := commonhttp.JSONRequestBodyDecoder(r, &body); err != nil {
175
+ return MarketplaceAppInstallRequest{}, fmt.Errorf("field to decode marketplace app install request: %w", err)
176
+ }
177
+
178
+ // Resolve namespace
179
+ namespace, err := h.resolveNamespace(ctx)
180
+ if err != nil {
181
+ return MarketplaceAppInstallRequest{}, fmt.Errorf("failed to resolve namespace: %w", err)
182
+ }
183
+
184
+ req := MarketplaceAppInstallRequest{
185
+ InstallAppV3Input: app.InstallAppV3Input{
186
+ MarketplaceListingID: app.MarketplaceListingID{Type: app.AppType(appType)},
187
+ Namespace: namespace,
188
+ Name: lo.FromPtr(body.Name),
189
+ },
190
+ CreateBillingProfile: lo.FromPtrOr(body.CreateBillingProfile, true),
191
+ }
192
+
193
+ return req, nil
194
+ },
195
+ func(ctx context.Context, request MarketplaceAppInstallRequest) (MarketplaceAppInstallResponse, error) {
196
+ resp := MarketplaceAppInstallResponse{
197
+ DefaultForCapabilityTypes: []api.AppCapabilityType{},
198
+ }
199
+
200
+ // Install app
201
+ installedApp, err := h.service.InstallApp(ctx, request.InstallAppV3Input)
202
+ if err != nil {
203
+ return resp, err
204
+ }
205
+
206
+ // Map app to API
207
+ apiApp, err := MapAppToAPI(installedApp.App)
208
+ if err != nil {
209
+ return resp, fmt.Errorf("failed to map app to API: %w", err)
210
+ }
211
+
212
+ resp.App = apiApp
213
+ resp.DefaultForCapabilityTypes = lo.Map(installedApp.DefaultCapabilies, func(c app.CapabilityType, _ int) api.AppCapabilityType {
214
+ return api.AppCapabilityType(c)
215
+ })
216
+
217
+ return resp, nil
218
+ },
219
+ commonhttp.JSONResponseEncoderWithStatus[MarketplaceAppInstallResponse](http.StatusOK),
220
+ httptransport.AppendOptions(
221
+ h.options,
222
+ httptransport.WithOperationName("marketplaceAppInstall"),
223
+ )...,
224
+ )
225
+ }
226
+
227
+ // Map marketplace listing to API
228
+ func mapMarketplaceListing(listing app.MarketplaceListing) api.MarketplaceListing {
229
+ return api.MarketplaceListing{
230
+ Type: api.AppType(listing.Type),
231
+ Name: listing.Name,
232
+ Description: listing.Description,
233
+ Capabilities: lo.Map(listing.Capabilities, func(v app.Capability, _ int) api.AppCapability {
234
+ return api.AppCapability{
235
+ Type: api.AppCapabilityType(v.Type),
236
+ Key: v.Key,
237
+ Name: v.Name,
238
+ Description: v.Description,
239
+ }
240
+ }),
241
+ InstallMethods: lo.Map(listing.InstallMethods, func(v app.InstallMethod, _ int) api.InstallMethod {
242
+ return api.InstallMethod(v)
243
+ }),
244
+ }
245
+ }
app/input.go ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package app
2
+
3
+ import (
4
+ "errors"
5
+ "fmt"
6
+
7
+ "github.com/openmeterio/openmeter/openmeter/customer"
8
+ "github.com/openmeterio/openmeter/pkg/models"
9
+ "github.com/openmeterio/openmeter/pkg/pagination"
10
+ )
11
+
12
+ type ListCustomerInput struct {
13
+ pagination.Page
14
+ AppID *AppID
15
+ CustomerID customer.CustomerID
16
+ Type *AppType
17
+ }
18
+
19
+ func (a ListCustomerInput) Validate() error {
20
+ var errs []error
21
+
22
+ if err := a.CustomerID.Validate(); err != nil {
23
+ errs = append(errs, err)
24
+ }
25
+
26
+ if a.AppID != nil {
27
+ if err := a.AppID.Validate(); err != nil {
28
+ errs = append(errs, err)
29
+ }
30
+ }
31
+
32
+ if a.Type != nil {
33
+ if *a.Type == "" {
34
+ errs = append(errs, models.NewGenericValidationError(
35
+ fmt.Errorf("app type cannot be empty"),
36
+ ))
37
+ }
38
+ }
39
+
40
+ return errors.Join(errs...)
41
+ }
42
+
43
+ type EnsureCustomerInput struct {
44
+ AppID AppID
45
+ CustomerID customer.CustomerID
46
+ }
47
+
48
+ func (a EnsureCustomerInput) Validate() error {
49
+ if err := a.AppID.Validate(); err != nil {
50
+ return err
51
+ }
52
+
53
+ if err := a.CustomerID.Validate(); err != nil {
54
+ return err
55
+ }
56
+
57
+ if a.AppID.Namespace != a.CustomerID.Namespace {
58
+ return fmt.Errorf("app ID namespace %s does not match customer ID namespace %s", a.AppID.Namespace, a.CustomerID.Namespace)
59
+ }
60
+
61
+ return nil
62
+ }
63
+
64
+ type DeleteCustomerInput struct {
65
+ AppID *AppID
66
+ CustomerID *customer.CustomerID
67
+ }
68
+
69
+ func (a DeleteCustomerInput) Validate() error {
70
+ if a.AppID == nil && a.CustomerID == nil {
71
+ return fmt.Errorf("app ID and customer ID cannot be nil")
72
+ }
73
+
74
+ if a.AppID != nil {
75
+ if err := a.AppID.Validate(); err != nil {
76
+ return err
77
+ }
78
+ }
79
+
80
+ if a.CustomerID != nil {
81
+ if err := a.CustomerID.Validate(); err != nil {
82
+ return err
83
+ }
84
+ }
85
+
86
+ if a.AppID != nil && a.CustomerID != nil && a.AppID.Namespace != a.CustomerID.Namespace {
87
+ return errors.New("app and customer must be in the same namespace")
88
+ }
89
+
90
+ return nil
91
+ }
app/marketplace.go ADDED
@@ -0,0 +1,230 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package app
2
+
3
+ import (
4
+ "context"
5
+ "errors"
6
+ "fmt"
7
+ "slices"
8
+
9
+ "github.com/openmeterio/openmeter/pkg/models"
10
+ "github.com/openmeterio/openmeter/pkg/pagination"
11
+ )
12
+
13
+ type InstallMethod string
14
+
15
+ const (
16
+ InstallMethodOAuth2 InstallMethod = "with_oauth2"
17
+ InstallMethodAPIKey InstallMethod = "with_api_key"
18
+ InstallMethodNoCredentials InstallMethod = "no_credentials_required"
19
+ )
20
+
21
+ func (i InstallMethod) Validate() error {
22
+ if i == "" {
23
+ return errors.New("install method is required")
24
+ }
25
+
26
+ if !slices.Contains([]InstallMethod{
27
+ InstallMethodOAuth2,
28
+ InstallMethodAPIKey,
29
+ InstallMethodNoCredentials,
30
+ }, i) {
31
+ return fmt.Errorf("invalid install method: %s", i)
32
+ }
33
+
34
+ return nil
35
+ }
36
+
37
+ type MarketplaceListing struct {
38
+ Type AppType `json:"type"`
39
+ Name string `json:"name"`
40
+ Description string `json:"description"`
41
+ Capabilities []Capability `json:"capabilities"`
42
+ InstallMethods []InstallMethod `json:"installMethods"`
43
+ }
44
+
45
+ func (p MarketplaceListing) Validate() error {
46
+ if p.Type == "" {
47
+ return errors.New("type is required")
48
+ }
49
+
50
+ if p.Name == "" {
51
+ return errors.New("name is required")
52
+ }
53
+
54
+ if p.Description == "" {
55
+ return errors.New("description is required")
56
+ }
57
+
58
+ for i, capability := range p.Capabilities {
59
+ if err := capability.Validate(); err != nil {
60
+ return fmt.Errorf("error validating capability at position %d: %w", i, err)
61
+ }
62
+ }
63
+
64
+ for i, installMethod := range p.InstallMethods {
65
+ if err := installMethod.Validate(); err != nil {
66
+ return fmt.Errorf("error validating install method at position %d: %w", i, err)
67
+ }
68
+ }
69
+
70
+ return nil
71
+ }
72
+
73
+ type Capability struct {
74
+ Type CapabilityType `json:"type"`
75
+ Key string `json:"key"`
76
+ Name string `json:"name"`
77
+ Description string `json:"description"`
78
+ }
79
+
80
+ func (c Capability) Validate() error {
81
+ if c.Key == "" {
82
+ return errors.New("key is required")
83
+ }
84
+
85
+ if c.Name == "" {
86
+ return errors.New("name is required")
87
+ }
88
+
89
+ if c.Description == "" {
90
+ return errors.New("description is required")
91
+ }
92
+
93
+ return nil
94
+ }
95
+
96
+ type MarketplaceListingID struct {
97
+ Type AppType
98
+ }
99
+
100
+ func (i MarketplaceListingID) Validate() error {
101
+ if i.Type == "" {
102
+ return errors.New("type is required")
103
+ }
104
+
105
+ return nil
106
+ }
107
+
108
+ type RegisterMarketplaceListingInput = RegistryItem
109
+
110
+ type MarketplaceGetInput = MarketplaceListingID
111
+
112
+ type MarketplaceListInput struct {
113
+ pagination.Page
114
+ }
115
+
116
+ func (i MarketplaceListInput) Validate() error {
117
+ if err := i.Page.Validate(); err != nil {
118
+ return fmt.Errorf("error validating page: %w", err)
119
+ }
120
+
121
+ return nil
122
+ }
123
+
124
+ type InstallAppWithAPIKeyInput struct {
125
+ InstallAppInput
126
+
127
+ APIKey string
128
+ }
129
+
130
+ func (i InstallAppWithAPIKeyInput) Validate() error {
131
+ if err := i.InstallAppInput.Validate(); err != nil {
132
+ return fmt.Errorf("error validating install app input: %w", err)
133
+ }
134
+
135
+ if i.APIKey == "" {
136
+ return errors.New("api key is required")
137
+ }
138
+
139
+ return nil
140
+ }
141
+
142
+ type InstallAppV3Input struct {
143
+ MarketplaceListingID
144
+
145
+ Namespace string
146
+ Name string
147
+ APIKey *string
148
+ CreateDefaultBillingProfile bool
149
+
150
+ CreateDefaultBillingProfileFn func(ctx context.Context, installedApp App) ([]CapabilityType, error)
151
+ }
152
+
153
+ func (i InstallAppV3Input) Validate() error {
154
+ var errs []error
155
+ if err := i.MarketplaceListingID.Validate(); err != nil {
156
+ errs = append(errs, fmt.Errorf("error validating marketplace listing id: %w", err))
157
+ }
158
+
159
+ if i.Namespace == "" {
160
+ errs = append(errs, errors.New("namespace is required"))
161
+ }
162
+
163
+ if i.APIKey != nil && *i.APIKey == "" {
164
+ errs = append(errs, errors.New("api key is required"))
165
+ }
166
+
167
+ return models.NewNillableGenericValidationError(errors.Join(errs...))
168
+ }
169
+
170
+ type InstallAppV3Output struct {
171
+ App App
172
+ DefaultCapabilies []CapabilityType
173
+ }
174
+
175
+ type InstallAppInput struct {
176
+ MarketplaceListingID
177
+
178
+ Namespace string
179
+ Name string
180
+ }
181
+
182
+ func (i InstallAppInput) Validate() error {
183
+ if err := i.MarketplaceListingID.Validate(); err != nil {
184
+ return models.NewGenericValidationError(
185
+ fmt.Errorf("error validating marketplace listing id: %w", err),
186
+ )
187
+ }
188
+
189
+ if i.Namespace == "" {
190
+ return errors.New("namespace is required")
191
+ }
192
+
193
+ return nil
194
+ }
195
+
196
+ type GetOauth2InstallURLInput = MarketplaceListingID
197
+
198
+ type GetOauth2InstallURLOutput struct {
199
+ URL string
200
+ }
201
+
202
+ type AuthorizeOauth2InstallInput struct {
203
+ MarketplaceListingID
204
+
205
+ Code string
206
+ // Success response fields
207
+ State string
208
+ // Error response fields
209
+ Error string
210
+ ErrorDescription string
211
+ ErrorURI string
212
+ }
213
+
214
+ func (i AuthorizeOauth2InstallInput) Validate() error {
215
+ if err := i.MarketplaceListingID.Validate(); err != nil {
216
+ return models.NewGenericValidationError(
217
+ fmt.Errorf("error validating marketplace listing id: %w", err),
218
+ )
219
+ }
220
+
221
+ if i.State == "" {
222
+ return errors.New("state is required")
223
+ }
224
+
225
+ if i.Error != "" && i.Code != "" {
226
+ return errors.New("code and error cannot be set at the same time")
227
+ }
228
+
229
+ return nil
230
+ }
app/registry.go ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package app
2
+
3
+ import (
4
+ "context"
5
+ "errors"
6
+ "fmt"
7
+ )
8
+
9
+ type AppFactory interface {
10
+ NewApp(context.Context, AppBase) (App, error)
11
+ UninstallApp(ctx context.Context, input UninstallAppInput) error
12
+ }
13
+
14
+ type AppFactoryInstallWithAPIKey interface {
15
+ InstallAppWithAPIKey(ctx context.Context, input AppFactoryInstallAppWithAPIKeyInput) (App, error)
16
+ }
17
+
18
+ type UninstallAppInput = AppID
19
+
20
+ type AppFactoryInstallAppWithAPIKeyInput struct {
21
+ Namespace string
22
+ APIKey string
23
+ Name string
24
+ }
25
+
26
+ func (i AppFactoryInstallAppWithAPIKeyInput) Validate() error {
27
+ if i.Namespace == "" {
28
+ return errors.New("namespace is required")
29
+ }
30
+
31
+ if i.APIKey == "" {
32
+ return errors.New("api key is required")
33
+ }
34
+
35
+ if i.Name == "" {
36
+ return errors.New("name is required")
37
+ }
38
+
39
+ return nil
40
+ }
41
+
42
+ type AppFactoryInstall interface {
43
+ InstallApp(ctx context.Context, input AppFactoryInstallAppInput) (App, error)
44
+ }
45
+
46
+ type AppFactoryInstallAppInput struct {
47
+ Namespace string
48
+ Name string
49
+ }
50
+
51
+ func (i AppFactoryInstallAppInput) Validate() error {
52
+ if i.Namespace == "" {
53
+ return errors.New("namespace is required")
54
+ }
55
+
56
+ if i.Name == "" {
57
+ return errors.New("name is required")
58
+ }
59
+
60
+ return nil
61
+ }
62
+
63
+ type RegistryItem struct {
64
+ Listing MarketplaceListing
65
+ Factory AppFactory
66
+ }
67
+
68
+ func (r RegistryItem) Validate() error {
69
+ if err := r.Listing.Validate(); err != nil {
70
+ return fmt.Errorf("error validating registry item: %w", err)
71
+ }
72
+
73
+ if r.Factory == nil {
74
+ return errors.New("factory is required")
75
+ }
76
+
77
+ return nil
78
+ }
app/sandbox/app.go ADDED
@@ -0,0 +1,255 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package appsandbox
2
+
3
+ import (
4
+ "context"
5
+ "fmt"
6
+
7
+ "github.com/samber/lo"
8
+
9
+ "github.com/openmeterio/openmeter/openmeter/app"
10
+ "github.com/openmeterio/openmeter/openmeter/billing"
11
+ "github.com/openmeterio/openmeter/openmeter/billing/sequence"
12
+ "github.com/openmeterio/openmeter/openmeter/customer"
13
+ customerapp "github.com/openmeterio/openmeter/openmeter/customer/app"
14
+ "github.com/openmeterio/openmeter/pkg/clock"
15
+ "github.com/openmeterio/openmeter/pkg/models"
16
+ )
17
+
18
+ const (
19
+ TargetPaymentStatusMetadataKey = "openmeter.io/sandbox/target-payment-status"
20
+
21
+ TargetPaymentStatusPaid = "paid"
22
+ TargetPaymentStatusFailed = "failed"
23
+ TargetPaymentStatusUncollectible = "uncollectible"
24
+ TargetPaymentStatusActionRequired = "action_required"
25
+ )
26
+
27
+ var (
28
+ _ customerapp.App = (*App)(nil)
29
+ _ billing.InvoicingApp = (*App)(nil)
30
+ _ billing.InvoicingAppPostAdvanceHook = (*App)(nil)
31
+ _ app.CustomerData = (*CustomerData)(nil)
32
+
33
+ InvoiceSequenceNumber = sequence.Definition{
34
+ Prefix: "OM-SANDBOX",
35
+ SuffixTemplate: "{{.CustomerPrefix}}-{{.NextSequenceNumber}}",
36
+ Scope: "invoices/app/sandbox",
37
+ CommitMode: sequence.CommitModeWithCaller,
38
+ }
39
+ )
40
+
41
+ type Meta struct {
42
+ app.AppBase
43
+ }
44
+
45
+ var _ app.EventAppParser = (*Meta)(nil)
46
+
47
+ func (m *Meta) FromEventAppData(event app.EventApp) error {
48
+ m.AppBase = event.AppBase
49
+
50
+ return nil
51
+ }
52
+
53
+ type App struct {
54
+ Meta
55
+
56
+ sequenceService sequence.Service
57
+ }
58
+
59
+ func (a App) ValidateCustomer(ctx context.Context, customer *customer.Customer, capabilities []app.CapabilityType) error {
60
+ if err := a.ValidateCapabilities(capabilities...); err != nil {
61
+ return fmt.Errorf("error validating capabilities: %w", err)
62
+ }
63
+
64
+ return nil
65
+ }
66
+
67
+ func (a App) GetCustomerData(ctx context.Context, input app.GetAppInstanceCustomerDataInput) (app.CustomerData, error) {
68
+ return CustomerData{}, nil
69
+ }
70
+
71
+ func (a App) UpsertCustomerData(ctx context.Context, input app.UpsertAppInstanceCustomerDataInput) error {
72
+ return nil
73
+ }
74
+
75
+ func (a App) DeleteCustomerData(ctx context.Context, input app.DeleteAppInstanceCustomerDataInput) error {
76
+ return nil
77
+ }
78
+
79
+ func (a App) ValidateStandardInvoice(ctx context.Context, invoice billing.StandardInvoice) error {
80
+ return nil
81
+ }
82
+
83
+ func (a App) UpdateAppConfig(ctx context.Context, input app.AppConfigUpdate) error {
84
+ return nil
85
+ }
86
+
87
+ func (a App) UpsertStandardInvoice(ctx context.Context, invoice billing.StandardInvoice) (*billing.UpsertStandardInvoiceResult, error) {
88
+ return billing.NewUpsertStandardInvoiceResult(), nil
89
+ }
90
+
91
+ func (a App) FinalizeStandardInvoice(ctx context.Context, invoice billing.StandardInvoice) (*billing.FinalizeStandardInvoiceResult, error) {
92
+ invoiceNumber, err := a.sequenceService.GenerateInvoiceSequenceNumber(
93
+ ctx,
94
+ sequence.GenerationInput{
95
+ Namespace: invoice.Namespace,
96
+ CustomerName: invoice.Customer.Name,
97
+ Currency: invoice.Currency,
98
+ },
99
+ InvoiceSequenceNumber,
100
+ )
101
+ if err != nil {
102
+ return nil, fmt.Errorf("failed to generate invoice sequence number: %w", err)
103
+ }
104
+
105
+ return billing.NewFinalizeStandardInvoiceResult().
106
+ SetInvoiceNumber(invoiceNumber).
107
+ SetSentToCustomerAt(clock.Now()), nil
108
+ }
109
+
110
+ func (a App) DeleteStandardInvoice(ctx context.Context, invoice billing.StandardInvoice) error {
111
+ return nil
112
+ }
113
+
114
+ func (a App) PostAdvanceStandardInvoiceHook(ctx context.Context, invoice billing.StandardInvoice) (*billing.PostAdvanceHookResult, error) {
115
+ if invoice.Status != billing.StandardInvoiceStatusPaymentProcessingPending {
116
+ return nil, nil
117
+ }
118
+
119
+ targetStatus := TargetPaymentStatusPaid
120
+
121
+ // Allow overriding via metadata for testing (unit, customer) purposes
122
+ override, ok := invoice.Metadata[TargetPaymentStatusMetadataKey]
123
+ if ok && override != "" {
124
+ targetStatus = override
125
+ }
126
+
127
+ out := billing.NewPostAdvanceHookResult()
128
+ // Let's simulate the payment status by invoking the right trigger
129
+ switch targetStatus {
130
+ case TargetPaymentStatusFailed:
131
+ return out.InvokeTrigger(billing.InvoiceTriggerInput{
132
+ Invoice: invoice.GetInvoiceID(),
133
+ Trigger: billing.TriggerFailed,
134
+ ValidationErrors: &billing.InvoiceTriggerValidationInput{
135
+ Operation: billing.StandardInvoiceOpInitiatePayment,
136
+ Errors: []error{ErrSimulatedPaymentFailure},
137
+ },
138
+ }), nil
139
+ case TargetPaymentStatusUncollectible:
140
+ return out.InvokeTrigger(billing.InvoiceTriggerInput{
141
+ Invoice: invoice.GetInvoiceID(),
142
+ Trigger: billing.TriggerPaymentUncollectible,
143
+ }), nil
144
+ case TargetPaymentStatusActionRequired:
145
+ return out.InvokeTrigger(billing.InvoiceTriggerInput{
146
+ Invoice: invoice.GetInvoiceID(),
147
+ Trigger: billing.TriggerActionRequired,
148
+ }), nil
149
+ case TargetPaymentStatusPaid:
150
+ fallthrough
151
+ default:
152
+ return out.InvokeTrigger(billing.InvoiceTriggerInput{
153
+ Invoice: invoice.GetInvoiceID(),
154
+ Trigger: billing.TriggerPaid,
155
+ }), nil
156
+ }
157
+ }
158
+
159
+ func (a App) GetEventAppData() (app.EventAppData, error) {
160
+ return app.EventAppData{}, nil
161
+ }
162
+
163
+ type CustomerData struct{}
164
+
165
+ func (c CustomerData) Validate() error {
166
+ return nil
167
+ }
168
+
169
+ type Factory struct {
170
+ appService app.Service
171
+ sequenceService sequence.Service
172
+ }
173
+
174
+ type Config struct {
175
+ AppService app.Service
176
+ SequenceService sequence.Service
177
+ }
178
+
179
+ func (c Config) Validate() error {
180
+ if c.AppService == nil {
181
+ return fmt.Errorf("app service is required")
182
+ }
183
+
184
+ if c.SequenceService == nil {
185
+ return fmt.Errorf("sequence service is required")
186
+ }
187
+
188
+ return nil
189
+ }
190
+
191
+ func NewFactory(config Config) (*Factory, error) {
192
+ if err := config.Validate(); err != nil {
193
+ return nil, fmt.Errorf("failed to validate config: %w", err)
194
+ }
195
+
196
+ fact := &Factory{
197
+ appService: config.AppService,
198
+ sequenceService: config.SequenceService,
199
+ }
200
+
201
+ err := config.AppService.RegisterMarketplaceListing(app.RegistryItem{
202
+ Listing: MarketplaceListing,
203
+ Factory: fact,
204
+ })
205
+ if err != nil {
206
+ return nil, fmt.Errorf("failed to register marketplace listing: %w", err)
207
+ }
208
+
209
+ return fact, nil
210
+ }
211
+
212
+ // Factory
213
+ func (a *Factory) NewApp(_ context.Context, appBase app.AppBase) (app.App, error) {
214
+ return App{
215
+ Meta: Meta{
216
+ AppBase: appBase,
217
+ },
218
+ sequenceService: a.sequenceService,
219
+ }, nil
220
+ }
221
+
222
+ func (a *Factory) InstallApp(ctx context.Context, input app.AppFactoryInstallAppInput) (app.App, error) {
223
+ if err := input.Validate(); err != nil {
224
+ return nil, fmt.Errorf("invalid input: %w", err)
225
+ }
226
+
227
+ // Sandbox is a singleton per namespace — only one instance makes sense since all
228
+ // instances are functionally identical (no credentials, no external state).
229
+ existing, err := a.appService.ListApps(ctx, app.ListAppInput{
230
+ Namespace: input.Namespace,
231
+ Type: lo.ToPtr(app.AppTypeSandbox),
232
+ })
233
+ if err != nil {
234
+ return nil, fmt.Errorf("failed to list sandbox apps: %w", err)
235
+ }
236
+
237
+ if existing.TotalCount > 0 {
238
+ return nil, models.NewGenericConflictError(fmt.Errorf("sandbox app: %s already exists", existing.Items[0].GetName()))
239
+ }
240
+
241
+ appBase, err := a.appService.CreateApp(ctx, app.CreateAppInput{
242
+ Namespace: input.Namespace,
243
+ Name: input.Name,
244
+ Type: app.AppTypeSandbox,
245
+ })
246
+ if err != nil {
247
+ return nil, fmt.Errorf("failed to create app: %w", err)
248
+ }
249
+
250
+ return a.NewApp(ctx, appBase.GetAppBase())
251
+ }
252
+
253
+ func (a *Factory) UninstallApp(ctx context.Context, input app.UninstallAppInput) error {
254
+ return nil
255
+ }
app/sandbox/config.go ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ package appsandbox
2
+
3
+ type Configuration struct{}
4
+
5
+ func (c Configuration) Validate() error {
6
+ return nil
7
+ }
app/sandbox/errors.go ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ package appsandbox
2
+
3
+ import "github.com/openmeterio/openmeter/openmeter/billing"
4
+
5
+ var ErrSimulatedPaymentFailure = billing.NewValidationError("simulated_payment_failure", "simulated payment failure")
app/sandbox/helpers.go ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package appsandbox
2
+
3
+ import (
4
+ "context"
5
+ "errors"
6
+ "fmt"
7
+
8
+ "github.com/samber/lo"
9
+
10
+ "github.com/openmeterio/openmeter/openmeter/app"
11
+ "github.com/openmeterio/openmeter/pkg/models"
12
+ )
13
+
14
+ type AutoProvisionInput struct {
15
+ Namespace string
16
+ AppService app.Service
17
+ }
18
+
19
+ func (a AutoProvisionInput) Validate() error {
20
+ if a.Namespace == "" {
21
+ return errors.New("namespace is required")
22
+ }
23
+
24
+ if a.AppService == nil {
25
+ return errors.New("app service is required")
26
+ }
27
+
28
+ return nil
29
+ }
30
+
31
+ // AutoProvision creates a new default sandbox app if it doesn't exist, otherwise returns the existing one.
32
+ //
33
+ // We install the sandbox app by default in the system, so that the user can start trying out the system
34
+ // right away.
35
+ func AutoProvision(ctx context.Context, input AutoProvisionInput) (app.App, error) {
36
+ if err := input.Validate(); err != nil {
37
+ return nil, models.NewGenericValidationError(err)
38
+ }
39
+
40
+ // Get the sandbox app list
41
+ sandboxAppList, err := input.AppService.ListApps(ctx, app.ListAppInput{
42
+ Namespace: input.Namespace,
43
+ Type: lo.ToPtr(app.AppTypeSandbox),
44
+ })
45
+ if err != nil {
46
+ return nil, fmt.Errorf("cannot list apps: %w", err)
47
+ }
48
+
49
+ // If there is no sandbox app, we need to provision a new one
50
+ if sandboxAppList.TotalCount == 0 {
51
+ // Let's provision the new app
52
+ appBase, err := input.AppService.CreateApp(ctx, app.CreateAppInput{
53
+ Namespace: input.Namespace,
54
+ Name: "Sandbox",
55
+ Description: "OpenMeter Sandbox App to be used for testing purposes.",
56
+ Type: app.AppTypeSandbox,
57
+ })
58
+ if err != nil {
59
+ return nil, fmt.Errorf("cannot create sandbox app: %w", err)
60
+ }
61
+
62
+ return input.AppService.GetApp(ctx, app.GetAppInput{
63
+ Namespace: input.Namespace,
64
+ ID: appBase.GetID().ID,
65
+ })
66
+ }
67
+
68
+ // Otherwise, we return the first one
69
+ return sandboxAppList.Items[0], nil
70
+ }
app/sandbox/marketplace.go ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package appsandbox
2
+
3
+ import (
4
+ "github.com/openmeterio/openmeter/openmeter/app"
5
+ )
6
+
7
+ var (
8
+ MarketplaceListing = app.MarketplaceListing{
9
+ Type: app.AppTypeSandbox,
10
+ Name: "Sandbox",
11
+ Description: "Sandbox can be used to test OpenMeter without external connections.",
12
+ Capabilities: []app.Capability{
13
+ CollectPaymentCapability,
14
+ CalculateTaxCapability,
15
+ InvoiceCustomerCapability,
16
+ },
17
+ InstallMethods: []app.InstallMethod{
18
+ app.InstallMethodNoCredentials,
19
+ },
20
+ }
21
+
22
+ CollectPaymentCapability = app.Capability{
23
+ Type: app.CapabilityTypeCollectPayments,
24
+ Key: "sandbox_collect_payment",
25
+ Name: "Payment",
26
+ Description: "Process payments",
27
+ }
28
+
29
+ CalculateTaxCapability = app.Capability{
30
+ Type: app.CapabilityTypeCalculateTax,
31
+ Key: "sandbox_calculate_tax",
32
+ Name: "Calculate Tax",
33
+ Description: "Calculate tax for a payment",
34
+ }
35
+
36
+ InvoiceCustomerCapability = app.Capability{
37
+ Type: app.CapabilityTypeInvoiceCustomers,
38
+ Key: "sandbox_invoice_customer",
39
+ Name: "Invoice Customer",
40
+ Description: "Invoice a customer",
41
+ }
42
+ )
app/sandbox/mock.go ADDED
@@ -0,0 +1,284 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package appsandbox
2
+
3
+ import (
4
+ "context"
5
+ "fmt"
6
+ "testing"
7
+
8
+ "github.com/samber/mo"
9
+
10
+ "github.com/openmeterio/openmeter/openmeter/app"
11
+ "github.com/openmeterio/openmeter/openmeter/billing"
12
+ "github.com/openmeterio/openmeter/openmeter/customer"
13
+ customerapp "github.com/openmeterio/openmeter/openmeter/customer/app"
14
+ )
15
+
16
+ type AppFactory interface {
17
+ NewApp(ctx context.Context, appBase app.AppBase) (app.App, error)
18
+ }
19
+
20
+ type InvoiceUpsertCallback func(billing.StandardInvoice) (*billing.UpsertStandardInvoiceResult, error)
21
+
22
+ type MockApp struct {
23
+ validateCustomerResponse mo.Option[error]
24
+ validateCustomerCalled bool
25
+
26
+ validateInvoiceResponse mo.Option[error]
27
+ validateInvoiceResponseCalled bool
28
+
29
+ upsertInvoiceCallback mo.Option[InvoiceUpsertCallback]
30
+ upsertInvoiceCalled bool
31
+
32
+ finalizeInvoiceResponse mo.Option[*billing.FinalizeStandardInvoiceResult]
33
+ finalizeInvoiceCalled bool
34
+
35
+ deleteInvoiceResponse mo.Option[error]
36
+ deleteInvoiceCalled bool
37
+ }
38
+
39
+ func NewMockApp(_ *testing.T) *MockApp {
40
+ return &MockApp{}
41
+ }
42
+
43
+ func (m *MockApp) GetCustomerData(ctx context.Context, input app.GetAppInstanceCustomerDataInput) (app.CustomerData, error) {
44
+ return nil, nil
45
+ }
46
+
47
+ func (m *MockApp) UpsertCustomerData(ctx context.Context, input app.UpsertAppInstanceCustomerDataInput) error {
48
+ return nil
49
+ }
50
+
51
+ func (m *MockApp) DeleteCustomerData(ctx context.Context, input app.DeleteAppInstanceCustomerDataInput) error {
52
+ return nil
53
+ }
54
+
55
+ func (m *MockApp) UpdateAppConfig(ctx context.Context, input app.AppConfigUpdate) error {
56
+ return nil
57
+ }
58
+
59
+ func (m *MockApp) ValidateCustomer(appID string, customer *customer.Customer, capabilities []app.CapabilityType) error {
60
+ m.validateCustomerCalled = true
61
+ return m.validateCustomerResponse.MustGet()
62
+ }
63
+
64
+ func (m *MockApp) OnValidateCustomer(err error) {
65
+ m.validateCustomerResponse = mo.Some(err)
66
+ }
67
+
68
+ // InvoicingApp
69
+
70
+ func (m *MockApp) ValidateStandardInvoice(appID string, invoice billing.StandardInvoice) error {
71
+ m.validateInvoiceResponseCalled = true
72
+ return m.validateInvoiceResponse.MustGet()
73
+ }
74
+
75
+ func (m *MockApp) OnValidateStandardInvoice(err error) {
76
+ m.validateInvoiceResponse = mo.Some(err)
77
+ }
78
+
79
+ func (m *MockApp) UpsertStandardInvoice(ctx context.Context, invoice billing.StandardInvoice) (*billing.UpsertStandardInvoiceResult, error) {
80
+ m.upsertInvoiceCalled = true
81
+
82
+ if m.upsertInvoiceCallback.IsPresent() && m.upsertInvoiceCallback.MustGet() != nil {
83
+ return m.upsertInvoiceCallback.MustGet()(invoice)
84
+ }
85
+
86
+ return billing.NewUpsertStandardInvoiceResult(), nil
87
+ }
88
+
89
+ func (m *MockApp) OnUpsertStandardInvoice(cb InvoiceUpsertCallback) {
90
+ m.upsertInvoiceCallback = mo.Some(cb)
91
+ }
92
+
93
+ func (m *MockApp) FinalizeStandardInvoice(ctx context.Context, invoice billing.StandardInvoice) (*billing.FinalizeStandardInvoiceResult, error) {
94
+ m.finalizeInvoiceCalled = true
95
+ return m.finalizeInvoiceResponse.MustGet(), nil
96
+ }
97
+
98
+ func (m *MockApp) OnFinalizeStandardInvoice(result *billing.FinalizeStandardInvoiceResult) {
99
+ m.finalizeInvoiceResponse = mo.Some(result)
100
+ }
101
+
102
+ func (m *MockApp) DeleteStandardInvoice(ctx context.Context, invoice billing.StandardInvoice) error {
103
+ m.deleteInvoiceCalled = true
104
+ return m.deleteInvoiceResponse.MustGet()
105
+ }
106
+
107
+ func (m *MockApp) OnDeleteStandardInvoice(err error) {
108
+ m.deleteInvoiceResponse = mo.Some(err)
109
+ }
110
+
111
+ func (m *MockApp) Reset(t *testing.T) {
112
+ t.Helper()
113
+
114
+ m.AssertExpectations(t)
115
+
116
+ m.validateCustomerResponse = mo.None[error]()
117
+ m.validateCustomerCalled = false
118
+
119
+ m.validateInvoiceResponse = mo.None[error]()
120
+ m.validateInvoiceResponseCalled = false
121
+
122
+ m.upsertInvoiceCallback = mo.None[InvoiceUpsertCallback]()
123
+ m.upsertInvoiceCalled = false
124
+
125
+ m.finalizeInvoiceResponse = mo.None[*billing.FinalizeStandardInvoiceResult]()
126
+ m.finalizeInvoiceCalled = false
127
+
128
+ m.deleteInvoiceResponse = mo.None[error]()
129
+ m.deleteInvoiceCalled = false
130
+ }
131
+
132
+ func (m *MockApp) AssertExpectations(t *testing.T) {
133
+ t.Helper()
134
+
135
+ if m.validateCustomerResponse.IsPresent() && !m.validateCustomerCalled {
136
+ t.Errorf("expected ValidateCustomer to be called")
137
+ }
138
+
139
+ if m.validateInvoiceResponse.IsPresent() && !m.validateInvoiceResponseCalled {
140
+ t.Errorf("expected ValidateInvoice to be called")
141
+ }
142
+
143
+ if m.upsertInvoiceCallback.IsPresent() && !m.upsertInvoiceCalled {
144
+ t.Errorf("expected UpsertInvoice to be called")
145
+ }
146
+
147
+ if m.finalizeInvoiceResponse.IsPresent() && !m.finalizeInvoiceCalled {
148
+ t.Errorf("expected FinalizeInvoice to be called")
149
+ }
150
+
151
+ if m.deleteInvoiceResponse.IsPresent() && !m.deleteInvoiceCalled {
152
+ t.Errorf("expected DeleteInvoice to be called")
153
+ }
154
+ }
155
+
156
+ func (m *MockApp) NewApp(_ context.Context, app app.AppBase) (app.App, error) {
157
+ return &mockAppInstance{
158
+ AppBase: app,
159
+ parent: m,
160
+ }, nil
161
+ }
162
+
163
+ type mockAppInstance struct {
164
+ app.AppBase
165
+
166
+ parent *MockApp
167
+ }
168
+
169
+ var (
170
+ _ billing.InvoicingApp = (*mockAppInstance)(nil)
171
+ _ customerapp.App = (*mockAppInstance)(nil)
172
+ )
173
+
174
+ func (m *mockAppInstance) GetCustomerData(ctx context.Context, input app.GetAppInstanceCustomerDataInput) (app.CustomerData, error) {
175
+ return m.parent.GetCustomerData(ctx, input)
176
+ }
177
+
178
+ func (m *mockAppInstance) UpsertCustomerData(ctx context.Context, input app.UpsertAppInstanceCustomerDataInput) error {
179
+ return m.parent.UpsertCustomerData(ctx, input)
180
+ }
181
+
182
+ func (m *mockAppInstance) DeleteCustomerData(ctx context.Context, input app.DeleteAppInstanceCustomerDataInput) error {
183
+ return m.parent.DeleteCustomerData(ctx, input)
184
+ }
185
+
186
+ func (m *mockAppInstance) UpdateAppConfig(ctx context.Context, input app.AppConfigUpdate) error {
187
+ return m.parent.UpdateAppConfig(ctx, input)
188
+ }
189
+
190
+ func (m *mockAppInstance) ValidateCustomer(ctx context.Context, customer *customer.Customer, capabilities []app.CapabilityType) error {
191
+ return m.parent.ValidateCustomer(m.GetID().ID, customer, capabilities)
192
+ }
193
+
194
+ func (m *mockAppInstance) ValidateStandardInvoice(ctx context.Context, invoice billing.StandardInvoice) error {
195
+ return m.parent.ValidateStandardInvoice(m.GetID().ID, invoice)
196
+ }
197
+
198
+ func (m *mockAppInstance) UpsertStandardInvoice(ctx context.Context, invoice billing.StandardInvoice) (*billing.UpsertStandardInvoiceResult, error) {
199
+ return m.parent.UpsertStandardInvoice(ctx, invoice)
200
+ }
201
+
202
+ func (m *mockAppInstance) FinalizeStandardInvoice(ctx context.Context, invoice billing.StandardInvoice) (*billing.FinalizeStandardInvoiceResult, error) {
203
+ return m.parent.FinalizeStandardInvoice(ctx, invoice)
204
+ }
205
+
206
+ func (m *mockAppInstance) DeleteStandardInvoice(ctx context.Context, invoice billing.StandardInvoice) error {
207
+ return m.parent.DeleteStandardInvoice(ctx, invoice)
208
+ }
209
+
210
+ func (m *mockAppInstance) GetEventAppData() (app.EventAppData, error) {
211
+ return app.EventAppData{}, nil
212
+ }
213
+
214
+ type MockableFactory struct {
215
+ *Factory
216
+
217
+ overrideFactory AppFactory
218
+ }
219
+
220
+ type mockConfig struct {
221
+ OverrideType app.AppType
222
+ }
223
+
224
+ type mockConfigOption func(*mockConfig)
225
+
226
+ func MockWithAppType(t app.AppType) mockConfigOption {
227
+ return func(c *mockConfig) {
228
+ c.OverrideType = t
229
+ }
230
+ }
231
+
232
+ func NewMockableFactory(_ *testing.T, config Config, opts ...mockConfigOption) (*MockableFactory, error) {
233
+ if err := config.Validate(); err != nil {
234
+ return nil, fmt.Errorf("failed to validate config: %w", err)
235
+ }
236
+
237
+ fact := &MockableFactory{
238
+ Factory: &Factory{
239
+ appService: config.AppService,
240
+ sequenceService: config.SequenceService,
241
+ },
242
+ }
243
+
244
+ mockConfig := &mockConfig{}
245
+ for _, opt := range opts {
246
+ opt(mockConfig)
247
+ }
248
+
249
+ listing := MarketplaceListing
250
+
251
+ if mockConfig.OverrideType != "" {
252
+ listing.Type = mockConfig.OverrideType
253
+ }
254
+
255
+ err := config.AppService.RegisterMarketplaceListing(app.RegistryItem{
256
+ Listing: listing,
257
+ Factory: fact,
258
+ })
259
+ if err != nil {
260
+ return nil, fmt.Errorf("failed to register marketplace listing: %w", err)
261
+ }
262
+
263
+ return fact, nil
264
+ }
265
+
266
+ func (m *MockableFactory) NewApp(ctx context.Context, appBase app.AppBase) (app.App, error) {
267
+ if m.overrideFactory != nil {
268
+ return m.overrideFactory.NewApp(ctx, appBase)
269
+ }
270
+
271
+ return m.Factory.NewApp(ctx, appBase)
272
+ }
273
+
274
+ func (m *MockableFactory) EnableMock(t *testing.T) *MockApp {
275
+ mock := NewMockApp(t)
276
+
277
+ m.overrideFactory = mock
278
+
279
+ return mock
280
+ }
281
+
282
+ func (m *MockableFactory) DisableMock() {
283
+ m.overrideFactory = nil
284
+ }
app/service.go ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package app
2
+
3
+ import (
4
+ "context"
5
+
6
+ "github.com/openmeterio/openmeter/pkg/pagination"
7
+ )
8
+
9
+ type Service interface {
10
+ AppService
11
+ }
12
+
13
+ type AppService interface {
14
+ // Marketplace
15
+ RegisterMarketplaceListing(input RegisterMarketplaceListingInput) error
16
+ GetMarketplaceListing(ctx context.Context, input MarketplaceGetInput) (RegistryItem, error)
17
+ ListMarketplaceListings(ctx context.Context, input MarketplaceListInput) (pagination.Result[RegistryItem], error)
18
+ InstallApp(ctx context.Context, input InstallAppV3Input) (InstallAppV3Output, error)
19
+ GetMarketplaceListingOauth2InstallURL(ctx context.Context, input GetOauth2InstallURLInput) (GetOauth2InstallURLOutput, error)
20
+ AuthorizeMarketplaceListingOauth2Install(ctx context.Context, input AuthorizeOauth2InstallInput) error
21
+
22
+ // Installed app
23
+ CreateApp(ctx context.Context, input CreateAppInput) (AppBase, error)
24
+ GetApp(ctx context.Context, input GetAppInput) (App, error)
25
+ UpdateAppStatus(ctx context.Context, input UpdateAppStatusInput) error
26
+ UpdateApp(ctx context.Context, input UpdateAppInput) (App, error)
27
+ ListApps(ctx context.Context, input ListAppInput) (pagination.Result[App], error)
28
+ UninstallApp(ctx context.Context, input UninstallAppInput) error
29
+
30
+ // Customer data
31
+ ListCustomerData(ctx context.Context, input ListCustomerInput) (pagination.Result[CustomerApp], error)
32
+ EnsureCustomer(ctx context.Context, input EnsureCustomerInput) error
33
+ DeleteCustomer(ctx context.Context, input DeleteCustomerInput) error
34
+ }
app/service/app.go ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package appservice
2
+
3
+ import (
4
+ "context"
5
+
6
+ "github.com/openmeterio/openmeter/openmeter/app"
7
+ "github.com/openmeterio/openmeter/pkg/framework/transaction"
8
+ "github.com/openmeterio/openmeter/pkg/models"
9
+ "github.com/openmeterio/openmeter/pkg/pagination"
10
+ )
11
+
12
+ var _ app.AppService = (*Service)(nil)
13
+
14
+ func (s *Service) CreateApp(ctx context.Context, input app.CreateAppInput) (app.AppBase, error) {
15
+ // Validate the input
16
+ if err := input.Validate(); err != nil {
17
+ return app.AppBase{}, models.NewGenericValidationError(err)
18
+ }
19
+
20
+ // Create the app
21
+ appBase, err := s.adapter.CreateApp(ctx, input)
22
+ if err != nil {
23
+ return app.AppBase{}, err
24
+ }
25
+
26
+ // Emit the app created event
27
+ event := app.NewAppCreateEvent(ctx, appBase)
28
+ if err := s.publisher.Publish(ctx, event); err != nil {
29
+ return app.AppBase{}, err
30
+ }
31
+
32
+ return appBase, nil
33
+ }
34
+
35
+ func (s *Service) GetApp(ctx context.Context, input app.GetAppInput) (app.App, error) {
36
+ if err := input.Validate(); err != nil {
37
+ return nil, models.NewGenericValidationError(err)
38
+ }
39
+
40
+ return s.adapter.GetApp(ctx, input)
41
+ }
42
+
43
+ func (s *Service) UpdateApp(ctx context.Context, input app.UpdateAppInput) (app.App, error) {
44
+ // Validate the input
45
+ if err := input.Validate(); err != nil {
46
+ return nil, models.NewGenericValidationError(err)
47
+ }
48
+
49
+ return transaction.Run(ctx, s.adapter, func(ctx context.Context) (app.App, error) {
50
+ // Update the app
51
+ updatedApp, err := s.adapter.UpdateApp(ctx, input)
52
+ if err != nil {
53
+ return nil, err
54
+ }
55
+
56
+ // Update the app specific entity
57
+ if input.AppConfigUpdate != nil {
58
+ err := updatedApp.UpdateAppConfig(ctx, input.AppConfigUpdate)
59
+ if err != nil {
60
+ return nil, err
61
+ }
62
+
63
+ updatedApp, err = s.adapter.GetApp(ctx, input.AppID)
64
+ if err != nil {
65
+ return nil, err
66
+ }
67
+ }
68
+
69
+ // Emit the app updated event
70
+ event, err := app.NewAppUpdateEvent(ctx, updatedApp)
71
+ if err != nil {
72
+ return nil, err
73
+ }
74
+
75
+ if err := s.publisher.Publish(ctx, event); err != nil {
76
+ return nil, err
77
+ }
78
+
79
+ return updatedApp, nil
80
+ })
81
+ }
82
+
83
+ func (s *Service) ListApps(ctx context.Context, input app.ListAppInput) (pagination.Result[app.App], error) {
84
+ if err := input.Validate(); err != nil {
85
+ return pagination.Result[app.App]{}, models.NewGenericValidationError(err)
86
+ }
87
+
88
+ return s.adapter.ListApps(ctx, input)
89
+ }
90
+
91
+ func (s *Service) UninstallApp(ctx context.Context, input app.UninstallAppInput) error {
92
+ // Validate the input
93
+ if err := input.Validate(); err != nil {
94
+ return models.NewGenericValidationError(err)
95
+ }
96
+
97
+ // Existing app
98
+ existingApp, err := s.adapter.GetApp(ctx, input)
99
+ if err != nil {
100
+ return err
101
+ }
102
+
103
+ // Delete the app
104
+ appBase, err := s.adapter.UninstallApp(ctx, input)
105
+ if err != nil {
106
+ return err
107
+ }
108
+
109
+ // Emit the app deleted event
110
+ eventAppData, err := existingApp.GetEventAppData()
111
+ if err != nil {
112
+ return err
113
+ }
114
+
115
+ event := app.NewAppDeleteEvent(ctx, *appBase, eventAppData)
116
+ if err := s.publisher.Publish(ctx, event); err != nil {
117
+ return err
118
+ }
119
+
120
+ return nil
121
+ }
122
+
123
+ func (s *Service) UpdateAppStatus(ctx context.Context, input app.UpdateAppStatusInput) error {
124
+ // Validate the input
125
+ if err := input.Validate(); err != nil {
126
+ return models.NewGenericValidationError(err)
127
+ }
128
+
129
+ // Update the app status
130
+ if err := s.adapter.UpdateAppStatus(ctx, input); err != nil {
131
+ return err
132
+ }
133
+
134
+ // Get the app after status update to include in the event
135
+ updatedApp, err := s.adapter.GetApp(ctx, input.ID)
136
+ if err != nil {
137
+ return err
138
+ }
139
+
140
+ // Emit the app updated event
141
+ event, err := app.NewAppUpdateEvent(ctx, updatedApp)
142
+ if err != nil {
143
+ return err
144
+ }
145
+
146
+ if err := s.publisher.Publish(ctx, event); err != nil {
147
+ return err
148
+ }
149
+
150
+ return nil
151
+ }
app/service/customer.go ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package appservice
2
+
3
+ import (
4
+ "context"
5
+
6
+ "github.com/openmeterio/openmeter/openmeter/app"
7
+ "github.com/openmeterio/openmeter/pkg/pagination"
8
+ )
9
+
10
+ var _ app.AppService = (*Service)(nil)
11
+
12
+ func (s *Service) ListCustomerData(ctx context.Context, input app.ListCustomerInput) (pagination.Result[app.CustomerApp], error) {
13
+ return s.adapter.ListCustomerData(ctx, input)
14
+ }
15
+
16
+ func (s *Service) EnsureCustomer(ctx context.Context, input app.EnsureCustomerInput) error {
17
+ return s.adapter.EnsureCustomer(ctx, input)
18
+ }
19
+
20
+ func (s *Service) DeleteCustomer(ctx context.Context, input app.DeleteCustomerInput) error {
21
+ return s.adapter.DeleteCustomer(ctx, input)
22
+ }
app/service/marketplace.go ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package appservice
2
+
3
+ import (
4
+ "context"
5
+ "errors"
6
+ "fmt"
7
+
8
+ "github.com/openmeterio/openmeter/openmeter/app"
9
+ "github.com/openmeterio/openmeter/pkg/framework/transaction"
10
+ "github.com/openmeterio/openmeter/pkg/models"
11
+ "github.com/openmeterio/openmeter/pkg/pagination"
12
+ )
13
+
14
+ var _ app.AppService = (*Service)(nil)
15
+
16
+ func (s *Service) RegisterMarketplaceListing(input app.RegisterMarketplaceListingInput) error {
17
+ if err := input.Validate(); err != nil {
18
+ return models.NewGenericValidationError(err)
19
+ }
20
+
21
+ return s.adapter.RegisterMarketplaceListing(input)
22
+ }
23
+
24
+ func (s *Service) GetMarketplaceListing(ctx context.Context, input app.MarketplaceGetInput) (app.RegistryItem, error) {
25
+ if err := input.Validate(); err != nil {
26
+ return app.RegistryItem{}, models.NewGenericValidationError(err)
27
+ }
28
+
29
+ return s.adapter.GetMarketplaceListing(ctx, input)
30
+ }
31
+
32
+ func (s *Service) ListMarketplaceListings(ctx context.Context, input app.MarketplaceListInput) (pagination.Result[app.RegistryItem], error) {
33
+ if err := input.Validate(); err != nil {
34
+ return pagination.Result[app.RegistryItem]{}, models.NewGenericValidationError(err)
35
+ }
36
+
37
+ return s.adapter.ListMarketplaceListings(ctx, input)
38
+ }
39
+
40
+ func (s *Service) InstallApp(ctx context.Context, input app.InstallAppV3Input) (app.InstallAppV3Output, error) {
41
+ if err := input.Validate(); err != nil {
42
+ return app.InstallAppV3Output{}, models.NewGenericValidationError(err)
43
+ }
44
+
45
+ return transaction.Run(ctx, s.adapter, func(ctx context.Context) (app.InstallAppV3Output, error) {
46
+ var installedApp app.App
47
+ var err error
48
+ if input.APIKey != nil {
49
+ installedApp, err = s.adapter.InstallMarketplaceListingWithAPIKey(ctx, app.InstallAppWithAPIKeyInput{
50
+ InstallAppInput: app.InstallAppInput{
51
+ MarketplaceListingID: app.MarketplaceListingID{
52
+ Type: input.Type,
53
+ },
54
+ Namespace: input.Namespace,
55
+ Name: input.Name,
56
+ },
57
+ APIKey: *input.APIKey,
58
+ })
59
+ } else {
60
+ installedApp, err = s.adapter.InstallMarketplaceListing(ctx, app.InstallAppInput{
61
+ MarketplaceListingID: input.MarketplaceListingID,
62
+ Namespace: input.Namespace,
63
+ Name: input.Name,
64
+ })
65
+ }
66
+
67
+ if err != nil {
68
+ return app.InstallAppV3Output{}, err
69
+ }
70
+
71
+ out := app.InstallAppV3Output{
72
+ App: installedApp,
73
+ }
74
+
75
+ if input.CreateDefaultBillingProfile {
76
+ if input.CreateDefaultBillingProfileFn == nil {
77
+ return app.InstallAppV3Output{}, errors.New("create default billing profile function is required when CreateDefaultBillingProfile is true")
78
+ }
79
+ defaultForCapabilityTypes, err := input.CreateDefaultBillingProfileFn(ctx, installedApp)
80
+ if err != nil {
81
+ return app.InstallAppV3Output{}, fmt.Errorf("create billing profile: %w", err)
82
+ }
83
+
84
+ out.DefaultCapabilies = defaultForCapabilityTypes
85
+ }
86
+
87
+ return out, nil
88
+ })
89
+ }
90
+
91
+ func (s *Service) GetMarketplaceListingOauth2InstallURL(ctx context.Context, input app.GetOauth2InstallURLInput) (app.GetOauth2InstallURLOutput, error) {
92
+ if err := input.Validate(); err != nil {
93
+ return app.GetOauth2InstallURLOutput{}, models.NewGenericValidationError(err)
94
+ }
95
+
96
+ return s.adapter.GetMarketplaceListingOauth2InstallURL(ctx, input)
97
+ }
98
+
99
+ func (s *Service) AuthorizeMarketplaceListingOauth2Install(ctx context.Context, input app.AuthorizeOauth2InstallInput) error {
100
+ if err := input.Validate(); err != nil {
101
+ return models.NewGenericValidationError(err)
102
+ }
103
+
104
+ return s.adapter.AuthorizeMarketplaceListingOauth2Install(ctx, input)
105
+ }