package httpdriver import ( "context" "fmt" "net/http" "github.com/samber/lo" "github.com/openmeterio/openmeter/api" "github.com/openmeterio/openmeter/openmeter/app" appcustominvoicing "github.com/openmeterio/openmeter/openmeter/app/custominvoicing" appsandbox "github.com/openmeterio/openmeter/openmeter/app/sandbox" appstripe "github.com/openmeterio/openmeter/openmeter/app/stripe" "github.com/openmeterio/openmeter/openmeter/billing" "github.com/openmeterio/openmeter/openmeter/customer" "github.com/openmeterio/openmeter/pkg/framework/commonhttp" "github.com/openmeterio/openmeter/pkg/framework/transport/httptransport" "github.com/openmeterio/openmeter/pkg/models" "github.com/openmeterio/openmeter/pkg/pagination" ) type ( ListCustomerDataRequest = app.ListCustomerInput ListCustomerDataResponse = api.CustomerAppDataPaginatedResponse ListCustomerDataHandler httptransport.HandlerWithArgs[ListCustomerDataRequest, ListCustomerDataResponse, ListCustomerDataParams] ) type ListCustomerDataParams struct { api.ListCustomerAppDataParams CustomerIdOrKey string } // ListCustomerData returns a handler for listing customers app data. func (h *handler) ListCustomerData() ListCustomerDataHandler { return httptransport.NewHandlerWithArgs( func(ctx context.Context, r *http.Request, params ListCustomerDataParams) (ListCustomerDataRequest, error) { ns, err := h.resolveNamespace(ctx) if err != nil { return ListCustomerDataRequest{}, err } // Get the customer cus, err := h.customerService.GetCustomer(ctx, customer.GetCustomerInput{ CustomerIDOrKey: &customer.CustomerIDOrKey{ IDOrKey: params.CustomerIdOrKey, Namespace: ns, }, }) if err != nil { return ListCustomerDataRequest{}, err } if cus != nil && cus.IsDeleted() { return ListCustomerDataRequest{}, models.NewGenericPreConditionFailedError( fmt.Errorf("customer is deleted [namespace=%s customer.id=%s]", cus.Namespace, cus.ID), ) } req := ListCustomerDataRequest{ CustomerID: cus.GetID(), // Pagination Page: pagination.Page{ PageSize: lo.FromPtrOr(params.PageSize, customer.DefaultPageSize), PageNumber: lo.FromPtrOr(params.Page, customer.DefaultPageNumber), }, } if params.Type != nil { req.Type = lo.ToPtr(app.AppType(*params.Type)) } return req, nil }, func(ctx context.Context, request ListCustomerDataRequest) (ListCustomerDataResponse, error) { resp, err := h.service.ListCustomerData(ctx, request) if err != nil { return ListCustomerDataResponse{}, fmt.Errorf("failed to list customers: %w", err) } items := make([]api.CustomerAppData, 0, len(resp.Items)) for _, customerApp := range resp.Items { item, err := h.toAPICustomerAppData(customerApp) if err != nil { return ListCustomerDataResponse{}, fmt.Errorf("failed to cast app customer data: %w", err) } items = append(items, item) } return ListCustomerDataResponse{ Items: items, Page: resp.Page.PageNumber, PageSize: resp.Page.PageSize, TotalCount: resp.TotalCount, }, nil }, commonhttp.JSONResponseEncoderWithStatus[ListCustomerDataResponse](http.StatusOK), httptransport.AppendOptions( h.options, httptransport.WithOperationName("listCustomerData"), )..., ) } type UpsertCustomerDataRequest struct { CustomerId customer.CustomerID Data []api.CustomerAppData } type UpsertCustomerDataParams struct { CustomerIdOrKey string } type ( UpsertCustomerDataResponse = interface{} UpsertCustomerDataHandler httptransport.HandlerWithArgs[UpsertCustomerDataRequest, UpsertCustomerDataResponse, UpsertCustomerDataParams] ) // UpsertCustomerData returns a new httptransport.Handler for creating a customer. func (h *handler) UpsertCustomerData() UpsertCustomerDataHandler { return httptransport.NewHandlerWithArgs( func(ctx context.Context, r *http.Request, params UpsertCustomerDataParams) (UpsertCustomerDataRequest, error) { ns, err := h.resolveNamespace(ctx) if err != nil { return UpsertCustomerDataRequest{}, err } // Get the customer and ensure we do not update customer data if the customer is already deleted cus, err := h.customerService.GetCustomer(ctx, customer.GetCustomerInput{ CustomerIDOrKey: &customer.CustomerIDOrKey{ IDOrKey: params.CustomerIdOrKey, Namespace: ns, }, }) if err != nil { return UpsertCustomerDataRequest{}, err } if cus != nil && cus.IsDeleted() { return UpsertCustomerDataRequest{}, models.NewGenericPreConditionFailedError( fmt.Errorf("customer is deleted [namespace=%s customer.id=%s]", cus.Namespace, cus.ID), ) } var body []api.CustomerAppData if err := commonhttp.JSONRequestBodyDecoder(r, &body); err != nil { return UpsertCustomerDataRequest{}, fmt.Errorf("field to decode upsert customer data request: %w", err) } return UpsertCustomerDataRequest{ CustomerId: cus.GetID(), Data: body, }, nil }, func(ctx context.Context, req UpsertCustomerDataRequest) (UpsertCustomerDataResponse, error) { for _, apiCustomerData := range req.Data { customerApp, customerData, err := h.toCustomerData(ctx, req.CustomerId, apiCustomerData) if err != nil { return nil, err } err = customerApp.UpsertCustomerData(ctx, app.UpsertAppInstanceCustomerDataInput{ CustomerID: req.CustomerId, Data: customerData, }) if err != nil { return nil, err } } return nil, nil }, commonhttp.EmptyResponseEncoder[UpsertCustomerDataResponse](http.StatusOK), httptransport.AppendOptions( h.options, httptransport.WithOperationName("upsertCustomerData"), )..., ) } type DeleteCustomerDataParams struct { CustomerIdOrKey string AppId string } type DeleteCustomerDataRequest struct { AppID app.AppID CustomerID customer.CustomerID } type ( DeleteCustomerDataResponse = interface{} DeleteCustomerDataHandler httptransport.HandlerWithArgs[DeleteCustomerDataRequest, DeleteCustomerDataResponse, DeleteCustomerDataParams] ) // DeleteCustomerData returns a handler for deleting a customer data. func (h *handler) DeleteCustomerData() DeleteCustomerDataHandler { return httptransport.NewHandlerWithArgs( func(ctx context.Context, r *http.Request, params DeleteCustomerDataParams) (DeleteCustomerDataRequest, error) { ns, err := h.resolveNamespace(ctx) if err != nil { return DeleteCustomerDataRequest{}, err } // Get the customer cus, err := h.customerService.GetCustomer(ctx, customer.GetCustomerInput{ CustomerIDOrKey: &customer.CustomerIDOrKey{ IDOrKey: params.CustomerIdOrKey, Namespace: ns, }, }) if err != nil { return DeleteCustomerDataRequest{}, err } if cus != nil && cus.IsDeleted() { return DeleteCustomerDataRequest{}, models.NewGenericPreConditionFailedError( fmt.Errorf("customer is deleted [namespace=%s customer.id=%s]", cus.Namespace, cus.ID), ) } return DeleteCustomerDataRequest{ CustomerID: cus.GetID(), AppID: app.AppID{ Namespace: ns, ID: params.AppId, }, }, nil }, func(ctx context.Context, request DeleteCustomerDataRequest) (DeleteCustomerDataResponse, error) { // Get app existingApp, err := h.service.GetApp(ctx, request.AppID) if err != nil { return nil, err } // Delete customer data err = existingApp.DeleteCustomerData(ctx, app.DeleteAppInstanceCustomerDataInput{ CustomerID: request.CustomerID, }) if err != nil { return nil, err } return nil, nil }, commonhttp.EmptyResponseEncoder[DeleteCustomerDataResponse](http.StatusNoContent), httptransport.AppendOptions( h.options, httptransport.WithOperationName("deleteCustomerData"), )..., ) } // toCustomerData converts an API CustomerAppData to a CustomerData model func (h *handler) toCustomerData(ctx context.Context, customerID customer.CustomerID, apiApp api.CustomerAppData) (app.App, app.CustomerData, error) { // Get app type appType, err := apiApp.Discriminator() if err != nil { return nil, nil, fmt.Errorf("error getting app type: %w", err) } switch appType { // Sandbox app case string(app.AppTypeSandbox): // Parse as sandbox app apiSandboxCustomerData, err := apiApp.AsSandboxCustomerAppData() if err != nil { return nil, nil, fmt.Errorf("error converting to stripe app: %w", err) } // Resolve app resolvedApp, err := h.resolveCustomerApp(ctx, customerID, app.AppTypeSandbox, apiSandboxCustomerData.Id) if err != nil { return nil, nil, fmt.Errorf("error resolving sandbox app: %w", err) } // Create customer data sandboxCustomerData := appsandbox.CustomerData{} return resolvedApp, sandboxCustomerData, nil // Stripe app case string(app.AppTypeStripe): // Parse as stripe app apiStripeCustomerData, err := apiApp.AsStripeCustomerAppData() if err != nil { return nil, nil, fmt.Errorf("error converting to stripe app: %w", err) } // Resolve app resolvedApp, err := h.resolveCustomerApp(ctx, customerID, app.AppTypeStripe, apiStripeCustomerData.Id) if err != nil { return nil, nil, fmt.Errorf("error resolving stripe app: %w", err) } // Create customer data stripeCustomerData := fromAPIAppStripeCustomerData(apiStripeCustomerData) return resolvedApp, stripeCustomerData, nil case string(app.AppTypeCustomInvoicing): // Parse as custom invoicing app apiCustomInvoicingCustomerData, err := apiApp.AsCustomInvoicingCustomerAppData() if err != nil { return nil, nil, fmt.Errorf("error converting to custom invoicing app: %w", err) } // Resolve app resolvedApp, err := h.resolveCustomerApp(ctx, customerID, app.AppTypeCustomInvoicing, apiCustomInvoicingCustomerData.Id) if err != nil { return nil, nil, fmt.Errorf("error resolving custom invoicing app: %w", err) } // Create customer data customInvoicingCustomerData := appcustominvoicing.CustomerData{ Metadata: lo.FromPtrOr(apiCustomInvoicingCustomerData.Metadata, map[string]string{}), } return resolvedApp, customInvoicingCustomerData, nil } return nil, nil, fmt.Errorf("unsupported app type: %s", appType) } // resolveCustomerApp resolves a customer app based on the app type or app ID. func (h *handler) resolveCustomerApp(ctx context.Context, customerID customer.CustomerID, appType app.AppType, appID *string) (app.App, error) { var resolvedApp app.App var err error // Get app ID from API data or get default app for billing profile if appID != nil { return h.service.GetApp(ctx, app.GetAppInput{ Namespace: customerID.Namespace, ID: *appID, }) } // Get the customer app by type resolvedApp, err = h.billingService.GetCustomerApp(ctx, billing.GetCustomerAppInput{ CustomerID: customerID, AppType: appType, }) if err != nil { return nil, fmt.Errorf("error getting customer app: %w", err) } return resolvedApp, nil } // toAPICustomerAppData converts a CustomerApp to an API CustomerAppData func (h *handler) toAPICustomerAppData(a app.CustomerApp) (api.CustomerAppData, error) { apiCustomerAppData := api.CustomerAppData{} appId := a.App.GetID().ID switch customerAppData := a.CustomerData.(type) { case appstripe.CustomerData: stripeApp, ok := a.App.(appstripe.App) if !ok { return apiCustomerAppData, fmt.Errorf("error casting app to stripe app") } // Convert to API stripe customer app data apiStripeCustomerAppData := ToAPIStripeCustomerAppData(customerAppData, stripeApp) // Convert to API customer app data err := apiCustomerAppData.FromStripeCustomerAppData(apiStripeCustomerAppData) if err != nil { return apiCustomerAppData, fmt.Errorf("error converting to stripe customer app: %w", err) } case appsandbox.CustomerData: sandboxApp, ok := a.App.(appsandbox.App) if !ok { return apiCustomerAppData, fmt.Errorf("error casting app to sandbox app") } apiApp := mapSandboxAppToAPI(sandboxApp.Meta) apiSandboxCustomerAppData := api.SandboxCustomerAppData{ Id: &appId, Type: api.SandboxCustomerAppDataTypeSandbox, App: &apiApp, } err := apiCustomerAppData.FromSandboxCustomerAppData(apiSandboxCustomerAppData) if err != nil { return apiCustomerAppData, fmt.Errorf("error converting to sandbox customer app: %w", err) } case appcustominvoicing.CustomerData: customInvoicingApp, ok := a.App.(appcustominvoicing.App) if !ok { return apiCustomerAppData, fmt.Errorf("error casting app to custom invoicing app") } apiApp := mapCustomInvoicingAppToAPI(customInvoicingApp.Meta) apiCustomInvoicingCustomerAppData := api.CustomInvoicingCustomerAppData{ Id: &appId, Type: api.CustomInvoicingCustomerAppDataTypeCustomInvoicing, App: &apiApp, } err := apiCustomerAppData.FromCustomInvoicingCustomerAppData(apiCustomInvoicingCustomerAppData) if err != nil { return apiCustomerAppData, fmt.Errorf("error converting to custom invoicing customer app: %w", err) } default: return apiCustomerAppData, fmt.Errorf("unsupported customer data for app: %s", appId) } return apiCustomerAppData, nil }