Upload folder using huggingface_hub
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- apiconverter/cursor.go +18 -0
- apiconverter/filter.gen.go +266 -0
- apiconverter/filter.go +43 -0
- app/adapter.go +37 -0
- app/adapter/adapter.go +68 -0
- app/adapter/app.go +283 -0
- app/adapter/customer.go +178 -0
- app/adapter/marketplace.go +132 -0
- app/app.go +218 -0
- app/appbase.go +161 -0
- app/customer.go +10 -0
- app/custominvoicing/adapter.go +27 -0
- app/custominvoicing/adapter/adapter.go +72 -0
- app/custominvoicing/adapter/appconfig.go +70 -0
- app/custominvoicing/adapter/customerdata.go +89 -0
- app/custominvoicing/app.go +170 -0
- app/custominvoicing/customerdata.go +100 -0
- app/custominvoicing/factory.go +159 -0
- app/custominvoicing/httpdriver/custominvoicing.go +172 -0
- app/custominvoicing/httpdriver/errors.go +21 -0
- app/custominvoicing/httpdriver/handler.go +52 -0
- app/custominvoicing/httpdriver/mapper.go +85 -0
- app/custominvoicing/service.go +34 -0
- app/custominvoicing/service/customerdata.go +26 -0
- app/custominvoicing/service/factory.go +57 -0
- app/custominvoicing/service/service.go +62 -0
- app/custominvoicing/service/sync.go +117 -0
- app/custominvoicing/sync.go +66 -0
- app/defaults.go +6 -0
- app/errors.go +226 -0
- app/event.go +214 -0
- app/events.go +70 -0
- app/httpdriver/app.go +261 -0
- app/httpdriver/customer.go +421 -0
- app/httpdriver/handler.go +80 -0
- app/httpdriver/mapper.go +177 -0
- app/httpdriver/marketplace.go +245 -0
- app/input.go +91 -0
- app/marketplace.go +230 -0
- app/registry.go +78 -0
- app/sandbox/app.go +255 -0
- app/sandbox/config.go +7 -0
- app/sandbox/errors.go +5 -0
- app/sandbox/helpers.go +70 -0
- app/sandbox/marketplace.go +42 -0
- app/sandbox/mock.go +284 -0
- app/service.go +34 -0
- app/service/app.go +151 -0
- app/service/customer.go +22 -0
- 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 |
+
}
|