Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(customer): add optional key #2172

Merged
merged 2 commits into from
Jan 30, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,651 changes: 857 additions & 794 deletions api/api.gen.go

Large diffs are not rendered by default.

45 changes: 43 additions & 2 deletions api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1910,6 +1910,7 @@ paths:
- $ref: '#/components/parameters/CustomerOrderByOrdering.order'
- $ref: '#/components/parameters/CustomerOrderByOrdering.orderBy'
- $ref: '#/components/parameters/queryCustomerList.includeDeleted'
- $ref: '#/components/parameters/queryCustomerList.key'
- $ref: '#/components/parameters/queryCustomerList.name'
- $ref: '#/components/parameters/queryCustomerList.primaryEmail'
- $ref: '#/components/parameters/queryCustomerList.subject'
Expand Down Expand Up @@ -7452,6 +7453,16 @@ components:
type: boolean
default: false
explode: false
queryCustomerList.key:
name: key
in: query
required: false
description: |-
Filter customers by key.
Case-sensitive exact match.
schema:
type: string
explode: false
queryCustomerList.name:
name: name
in: query
Expand Down Expand Up @@ -8631,9 +8642,10 @@ components:
customer:
anyOf:
- $ref: '#/components/schemas/CustomerId'
- $ref: '#/components/schemas/CustomerKey'
- $ref: '#/components/schemas/CustomerCreate'
description: |-
Provide a customer ID to use an existing OpenMeter customer.
Provide a customer ID or key to use an existing OpenMeter customer.
or provide a customer object to create a new customer.
stripeCustomerId:
type: string
Expand Down Expand Up @@ -8980,6 +8992,13 @@ components:
description: Timestamp of when the resource was permanently deleted.
title: Deletion Time
readOnly: true
key:
type: string
description: |-
The optional key of the customer.
Useful to reference the customer in external systems.
For example, your database ID.
title: Key
usageAttribution:
allOf:
- $ref: '#/components/schemas/CustomerUsageAttribution'
Expand Down Expand Up @@ -9082,6 +9101,13 @@ components:
nullable: true
description: Additional metadata for the resource.
title: Metadata
key:
type: string
description: |-
The optional key of the customer.
Useful to reference the customer in external systems.
For example, your database ID.
title: Key
usageAttribution:
allOf:
- $ref: '#/components/schemas/CustomerUsageAttribution'
Expand Down Expand Up @@ -9116,7 +9142,15 @@ components:
example: 01G65Z755AFWAKHE12NY0CQ9FH
pattern: ^[0-7][0-9A-HJKMNP-TV-Za-hjkmnp-tv-z]{25}$
description: ULID (Universally Unique Lexicographically Sortable Identifier).
description: Create Stripe checkout session customer ID.
description: Create Stripe checkout session with customer ID.
CustomerKey:
type: object
required:
- key
properties:
key:
type: string
description: Create Stripe checkout session with customer key.
CustomerOrderBy:
type: string
enum:
Expand Down Expand Up @@ -9174,6 +9208,13 @@ components:
nullable: true
description: Additional metadata for the resource.
title: Metadata
key:
type: string
description: |-
The optional key of the customer.
Useful to reference the customer in external systems.
For example, your database ID.
title: Key
usageAttribution:
allOf:
- $ref: '#/components/schemas/CustomerUsageAttribution'
Expand Down
26 changes: 22 additions & 4 deletions api/spec/src/app/stripe.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,21 @@ model StripeWebhookResponse {
}

/**
* Create Stripe checkout session customer ID.
* Create Stripe checkout session with customer ID.
*/
@friendlyName("CustomerId")
model CustomerId {
id: ULID;
}

/**
* Create Stripe checkout session with customer key.
*/
@friendlyName("CustomerKey")
model CustomerKey {
key: string;
}

/**
* Create Stripe checkout session request.
*/
Expand All @@ -87,7 +95,17 @@ model CustomerId {
options: #{ currency: "USD", successURL: "http://example.com" },
},
#{
title: "With existing OpenMeter customer",
title: "With existing OpenMeter customer by id",
description: "Create a checkout session with existing customer.",
}
)
@example(
#{
customer: #{ key: "my-internal-id" },
options: #{ currency: "USD", successURL: "http://example.com" },
},
#{
title: "With existing OpenMeter customer by key",
description: "Create a checkout session with existing customer.",
}
)
Expand Down Expand Up @@ -150,10 +168,10 @@ model CreateStripeCheckoutSessionRequest {
appId?: ULID;

/**
* Provide a customer ID to use an existing OpenMeter customer.
* Provide a customer ID or key to use an existing OpenMeter customer.
* or provide a customer object to create a new customer.
*/
customer: CustomerId | Rest.Resource.ResourceCreateModel<Customer>;
customer: CustomerId | CustomerKey | Rest.Resource.ResourceCreateModel<Customer>;

/**
* Stripe customer ID.
Expand Down
15 changes: 15 additions & 0 deletions api/spec/src/customer.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,13 @@ model ListCustomersParams {
@query
includeDeleted?: boolean = false;

/**
* Filter customers by key.
* Case-sensitive exact match.
*/
@query
key?: string;

/**
* Filter customers by name.
* Case-insensitive partial match.
Expand Down Expand Up @@ -208,6 +215,14 @@ enum CustomerOrderBy {
model Customer {
...Resource;

/**
* The optional key of the customer.
* Useful to reference the customer in external systems.
* For example, your database ID.
*/
@summary("Key")
key?: string;

/**
* Mapping to attribute metered usage to the customer
*/
Expand Down
28 changes: 28 additions & 0 deletions openmeter/app/stripe/adapter/stripe.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
secretentity "github.com/openmeterio/openmeter/openmeter/secret/entity"
"github.com/openmeterio/openmeter/pkg/framework/entutils"
"github.com/openmeterio/openmeter/pkg/models"
"github.com/openmeterio/openmeter/pkg/pagination"
)

var _ appstripe.AppStripeAdapter = (*adapter)(nil)
Expand Down Expand Up @@ -382,6 +383,33 @@ func (a adapter) CreateCheckoutSession(ctx context.Context, input appstripeentit
}
}

// Find a customer by key
if input.CustomerKey != nil {
customers, err := repo.customerService.ListCustomers(ctx, customerentity.ListCustomersInput{
Namespace: input.Namespace,
Key: input.CustomerKey,
Page: pagination.NewPage(1, 1),
})
if err != nil {
return appstripeentity.CreateCheckoutSessionOutput{}, fmt.Errorf("failed to list customers: %w", err)
}

// Customer not found with key
if customers.TotalCount == 0 {
return appstripeentity.CreateCheckoutSessionOutput{}, app.ValidationError{
Err: fmt.Errorf("customer not found with key: %s", *input.CustomerKey),
}
}

// Should not happen
if customers.TotalCount > 1 {
return appstripeentity.CreateCheckoutSessionOutput{}, fmt.Errorf("multiple customers found with key: %s", *input.CustomerKey)
}

customer = &customers.Items[0]
}

// Create a customer if create input is provided
if input.CreateCustomerInput != nil {
customer, err = repo.customerService.CreateCustomer(ctx, *input.CreateCustomerInput)
if err != nil {
Expand Down
17 changes: 15 additions & 2 deletions openmeter/app/stripe/entity/input.go
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ type CreateCheckoutSessionInput struct {
AppID *appentitybase.AppID
CreateCustomerInput *customerentity.CreateCustomerInput
CustomerID *customerentity.CustomerID
CustomerKey *string
StripeCustomerID *string
Options api.CreateStripeCheckoutSessionRequestOptions
}
Expand All @@ -280,15 +281,23 @@ func (i CreateCheckoutSessionInput) Validate() error {
}

// Least one of customer or customer id is required
if i.CreateCustomerInput == nil && i.CustomerID == nil {
return errors.New("create customer input or customer id is required")
if i.CreateCustomerInput == nil && i.CustomerID == nil && i.CustomerKey == nil {
return errors.New("create customer input or customer id or customer key is required")
}

// Mutually exclusive
if i.CreateCustomerInput != nil && i.CustomerID != nil {
return errors.New("create customer input and customer id cannot be provided at the same time")
}

if i.CreateCustomerInput != nil && i.CustomerKey != nil {
return errors.New("create customer input and customer key cannot be provided at the same time")
}

if i.CustomerID != nil && i.CustomerKey != nil {
return errors.New("create customer id and customer key cannot be provided at the same time")
}

if i.CreateCustomerInput != nil {
if err := i.CreateCustomerInput.Validate(); err != nil {
return fmt.Errorf("error validating create customer input: %w", err)
Expand All @@ -305,6 +314,10 @@ func (i CreateCheckoutSessionInput) Validate() error {
return errors.New("app and customer must be in the same namespace")
}

if i.CustomerKey != nil && *i.CustomerKey == "" {
return errors.New("customer key cannot be empty if provided")
}

if i.StripeCustomerID != nil && !strings.HasPrefix(*i.StripeCustomerID, "cus_") {
return errors.New("stripe customer id must start with cus_")
}
Expand Down
16 changes: 15 additions & 1 deletion openmeter/app/stripe/httpdriver/checkout_session.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ func (h *handler) CreateAppStripeCheckoutSession() CreateAppStripeCheckoutSessio

var createCustomerInput *customerentity.CreateCustomerInput
var customerId *customerentity.CustomerID
var customerKey *string

// Try to parse customer field as customer ID first
apiCustomerId, err := body.Customer.AsCustomerId()
Expand All @@ -45,7 +46,19 @@ func (h *handler) CreateAppStripeCheckoutSession() CreateAppStripeCheckoutSessio
Namespace: namespace,
ID: apiCustomerId.Id,
}
} else {
}

// If no customerId found try to parse customer field as customer key
if customerId == nil {
maybeCustomerKey, err := body.Customer.AsCustomerKey()

if err == nil && maybeCustomerKey.Key != "" {
customerKey = &maybeCustomerKey.Key
}
}

// If no customerKey found try to parse customer field as customer input
if customerKey == nil {
// If err try to parse customer field as customer input
customerCreate, err := body.Customer.AsCustomerCreate()
if err != nil {
Expand All @@ -63,6 +76,7 @@ func (h *handler) CreateAppStripeCheckoutSession() CreateAppStripeCheckoutSessio
req := CreateAppStripeCheckoutSessionRequest{
Namespace: namespace,
CustomerID: customerId,
CustomerKey: customerKey,
CreateCustomerInput: createCustomerInput,
StripeCustomerID: body.StripeCustomerId,
Options: body.Options,
Expand Down
32 changes: 27 additions & 5 deletions openmeter/customer/adapter/customer.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,14 @@ func (a *adapter) ListCustomers(ctx context.Context, input customerentity.ListCu

// Do not return deleted customers by default
if !input.IncludeDeleted {
query = query.Where(customerdb.DeletedAtIsNil())
query = query.Where(customerdb.IsDeleted(false))
}

// Filters
if input.Key != nil {
query = query.Where(customerdb.KeyEQ(*input.Key))
}

if input.Name != nil {
query = query.Where(customerdb.NameContainsFold(*input.Name))
}
Expand Down Expand Up @@ -137,6 +141,10 @@ func (a *adapter) CreateCustomer(ctx context.Context, input customerentity.Creat
SetNillablePrimaryEmail(input.PrimaryEmail).
SetNillableCurrency(input.Currency)

if input.Key != nil {
query = query.SetKey(*input.Key)
}

if input.BillingAddress != nil {
query = query.
SetNillableBillingAddressCity(input.BillingAddress.City).
Expand All @@ -150,6 +158,13 @@ func (a *adapter) CreateCustomer(ctx context.Context, input customerentity.Creat

customerEntity, err := query.Save(ctx)
if err != nil {
if entdb.IsConstraintError(err) {
return nil, customerentity.KeyConflictError{
Namespace: input.Namespace,
Key: *lo.CoalesceOrEmpty(input.Key),
}
}

return nil, fmt.Errorf("failed to create customer: %w", err)
}

Expand Down Expand Up @@ -219,7 +234,8 @@ func (a *adapter) DeleteCustomer(ctx context.Context, input customerentity.Delet
rows, err := repo.db.Customer.Update().
Where(customerdb.ID(input.ID)).
Where(customerdb.Namespace(input.Namespace)).
Where(customerdb.DeletedAtIsNil()).
Where(customerdb.IsDeleted(false)).
SetIsDeleted(true).
SetDeletedAt(deletedAt).
Save(ctx)
if err != nil {
Expand Down Expand Up @@ -321,6 +337,12 @@ func (a *adapter) UpdateCustomer(ctx context.Context, input customerentity.Updat
SetNillablePrimaryEmail(input.PrimaryEmail).
SetNillableCurrency(input.Currency)

if input.Key != nil {
query = query.SetKey(*input.Key)
} else {
query = query.ClearKey()
}

if input.BillingAddress != nil {
query = query.
SetNillableBillingAddressCity(input.BillingAddress.City).
Expand Down Expand Up @@ -351,9 +373,9 @@ func (a *adapter) UpdateCustomer(ctx context.Context, input customerentity.Updat
}

if entdb.IsConstraintError(err) {
return nil, customerentity.SubjectKeyConflictError{
Namespace: input.CustomerID.Namespace,
SubjectKeys: input.UsageAttribution.SubjectKeys,
return nil, customerentity.KeyConflictError{
Namespace: input.CustomerID.Namespace,
Key: *lo.CoalesceOrEmpty(input.Key),
}
}

Expand Down
4 changes: 4 additions & 0 deletions openmeter/customer/adapter/entitymapping.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ func CustomerFromDBEntity(e db.Customer) (*customerentity.Customer, error) {
CurrentSubscriptionID: currentSubID,
}

if e.Key != "" {
result.Key = &e.Key
}

if e.BillingAddressCity != nil || e.BillingAddressCountry != nil || e.BillingAddressLine1 != nil || e.BillingAddressLine2 != nil || e.BillingAddressPhoneNumber != nil || e.BillingAddressPostalCode != nil || e.BillingAddressState != nil {
result.BillingAddress = &models.Address{
City: e.BillingAddressCity,
Expand Down
Loading
Loading