Skip to content

Commit

Permalink
feat: update billing customer API
Browse files Browse the repository at this point in the history
- fixes product update api call failing with provider id missing

Signed-off-by: Kush Sharma <[email protected]>
  • Loading branch information
kushsharma committed Jan 9, 2024
1 parent 7e90e07 commit 139c38d
Show file tree
Hide file tree
Showing 7 changed files with 313 additions and 24 deletions.
42 changes: 42 additions & 0 deletions billing/customer/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type Repository interface {
GetByID(ctx context.Context, id string) (Customer, error)
List(ctx context.Context, filter Filter) ([]Customer, error)
Create(ctx context.Context, customer Customer) (Customer, error)
UpdateByID(ctx context.Context, customer Customer) (Customer, error)
Delete(ctx context.Context, id string) error
}

Expand Down Expand Up @@ -73,6 +74,47 @@ func (s Service) Create(ctx context.Context, customer Customer) (Customer, error
return s.repository.Create(ctx, customer)
}

func (s Service) Update(ctx context.Context, customer Customer) (Customer, error) {
existingCustomer, err := s.repository.GetByID(ctx, customer.ID)
if err != nil {
return Customer{}, err
}

// update a customer in stripe
stripeCustomer, err := s.stripeClient.Customers.Update(existingCustomer.ProviderID, &stripe.CustomerParams{
Params: stripe.Params{
Context: ctx,
},
Address: &stripe.AddressParams{
City: &customer.Address.City,
Country: &customer.Address.Country,
Line1: &customer.Address.Line1,
Line2: &customer.Address.Line2,
PostalCode: &customer.Address.PostalCode,
State: &customer.Address.State,
},
Email: &customer.Email,
Name: &customer.Name,
Phone: &customer.Phone,
Metadata: map[string]string{
"org_id": customer.OrgID,
"managed_by": "frontier",
},
})
if err != nil {
if stripeErr, ok := err.(*stripe.Error); ok {
switch stripeErr.Code {
case stripe.ErrorCodeParameterMissing:
// stripe error
return Customer{}, fmt.Errorf("missing parameter while registering to biller: %s", stripeErr.Error())
}
}
return Customer{}, fmt.Errorf("failed to register in billing provider: %w", err)
}
customer.ProviderID = stripeCustomer.ID
return s.repository.UpdateByID(ctx, customer)
}

func (s Service) GetByID(ctx context.Context, id string) (Customer, error) {
return s.repository.GetByID(ctx, id)
}
Expand Down
135 changes: 124 additions & 11 deletions billing/product/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,20 +129,37 @@ func (s *Service) GetByID(ctx context.Context, id string) (Product, error) {
}
}

if fetchedProduct.Prices, err = s.GetPriceByProductID(ctx, fetchedProduct.ID); err != nil {
return Product{}, fmt.Errorf("failed to fetch prices for product %s: %w", fetchedProduct.ID, err)
}
if fetchedProduct.Features, err = s.GetFeatureByProductID(ctx, fetchedProduct.ID); err != nil {
return Product{}, fmt.Errorf("failed to fetch features for product %s: %w", fetchedProduct.ID, err)
fetchedProduct, err = s.populateProduct(ctx, fetchedProduct)
if err != nil {
return Product{}, err
}
return fetchedProduct, nil
}

// populate product with price and features
func (s *Service) populateProduct(ctx context.Context, product Product) (Product, error) {
var err error
product.Prices, err = s.GetPriceByProductID(ctx, product.ID)
if err != nil {
return Product{}, fmt.Errorf("failed to fetch prices for product %s: %w", product.ID, err)
}
product.Features, err = s.GetFeatureByProductID(ctx, product.ID)
if err != nil {
return Product{}, fmt.Errorf("failed to fetch features for product %s: %w", product.ID, err)
}
return product, nil
}

// Update updates a product, but it doesn't update all fields
// ideally we should keep it immutable and create a new product
func (s *Service) Update(ctx context.Context, product Product) (Product, error) {
existingProduct, err := s.productRepository.GetByID(ctx, product.ID)
if err != nil {
return Product{}, err
}

// update product in stripe
_, err := s.stripeClient.Products.Update(product.ProviderID, &stripe.ProductParams{
_, err = s.stripeClient.Products.Update(existingProduct.ProviderID, &stripe.ProductParams{
Params: stripe.Params{
Context: ctx,
},
Expand All @@ -157,7 +174,54 @@ func (s *Service) Update(ctx context.Context, product Product) (Product, error)
if err != nil {
return Product{}, err
}
return s.productRepository.UpdateByName(ctx, product)

// only following fields will be updated
existingProduct.Title = product.Title
existingProduct.Description = product.Description
existingProduct.PlanIDs = product.PlanIDs
existingProduct.CreditAmount = product.CreditAmount
existingProduct.Metadata = product.Metadata

// check feature updates in product
var featureErr error
existingFeatures, err := s.ListFeatures(ctx, Filter{
ProductID: existingProduct.ID,
})
if err != nil {
return Product{}, err
}
for _, existingFeature := range existingFeatures {
_, found := utils.FindFirst(product.Features, func(f Feature) bool {
return f.ID == existingFeature.ID
})
if !found {
if err := s.RemoveFeatureFromProduct(ctx, existingFeature.ID, existingProduct.ID); err != nil {
featureErr = errors.Join(featureErr, err)
}
}
}
for _, feature := range product.Features {
if err := s.AddFeatureToProduct(ctx, feature, existingProduct.ID); err != nil {
featureErr = errors.Join(featureErr, err)
}
}
if featureErr != nil {
return Product{}, fmt.Errorf("failed to update features for product %s: %w", existingProduct.ID, featureErr)
}

// update in db
updatedProduct, err := s.productRepository.UpdateByName(ctx, existingProduct)
if err != nil {
return Product{}, err
}

// populate product with price and features
updatedProduct, err = s.populateProduct(ctx, updatedProduct)
if err != nil {
return Product{}, err
}

return updatedProduct, nil
}

func (s *Service) AddPlan(ctx context.Context, productOb Product, planID string) error {
Expand Down Expand Up @@ -235,7 +299,12 @@ func (s *Service) GetPriceByProductID(ctx context.Context, id string) ([]Price,
// UpdatePrice updates a price, but it doesn't update all fields
// ideally we should keep it immutable and create a new price
func (s *Service) UpdatePrice(ctx context.Context, price Price) (Price, error) {
_, err := s.stripeClient.Prices.Update(price.ProviderID, &stripe.PriceParams{
existingPrice, err := s.priceRepository.GetByID(ctx, price.ID)
if err != nil {
return Price{}, err
}

_, err = s.stripeClient.Prices.Update(existingPrice.ProviderID, &stripe.PriceParams{
Params: stripe.Params{
Context: ctx,
},
Expand All @@ -248,7 +317,11 @@ func (s *Service) UpdatePrice(ctx context.Context, price Price) (Price, error) {
if err != nil {
return Price{}, err
}
return s.priceRepository.UpdateByID(ctx, price)

// only following fields will be updated
existingPrice.Name = price.Name
existingPrice.Metadata = price.Metadata
return s.priceRepository.UpdateByID(ctx, existingPrice)
}

func (s *Service) List(ctx context.Context, flt Filter) ([]Product, error) {
Expand All @@ -260,11 +333,10 @@ func (s *Service) List(ctx context.Context, flt Filter) ([]Product, error) {
// enrich with prices
for i, listedProduct := range listedProducts {
// TODO(kushsharma): we can do this in one query
price, err := s.GetPriceByProductID(ctx, listedProduct.ID)
listedProducts[i], err = s.populateProduct(ctx, listedProduct)
if err != nil {
return nil, err
}
listedProducts[i].Prices = price
}
return listedProducts, nil
}
Expand All @@ -287,6 +359,47 @@ func (s *Service) UpsertFeature(ctx context.Context, feature Feature) (Feature,
return s.featureRepository.UpdateByName(ctx, existingFeature)
}

func (s *Service) AddFeatureToProduct(ctx context.Context, feature Feature, productID string) error {
existingFeature, err := s.GetFeatureByID(ctx, feature.Name)
if err != nil {
if !errors.Is(err, ErrFeatureNotFound) {
return err
}
// create a new feature if not found
feature.ProductIDs = append(feature.ProductIDs, productID)
existingFeature, err = s.UpsertFeature(ctx, feature)
if err != nil {
return err
}
}

if !slices.Contains(existingFeature.ProductIDs, productID) {
existingFeature.ProductIDs = append(existingFeature.ProductIDs, productID)
_, err = s.featureRepository.UpdateByName(ctx, existingFeature)
if err != nil {
return err
}
}
return nil
}

func (s *Service) RemoveFeatureFromProduct(ctx context.Context, featureID, productID string) error {
feature, err := s.GetFeatureByID(ctx, featureID)
if err != nil {
return err
}
if slices.Contains(feature.ProductIDs, productID) {
feature.ProductIDs = slices.DeleteFunc(feature.ProductIDs, func(id string) bool {
return id == productID
})
_, err = s.featureRepository.UpdateByName(ctx, feature)
if err != nil {
return err
}
}
return nil
}

func (s *Service) GetFeatureByID(ctx context.Context, id string) (Feature, error) {
if utils.IsValidUUID(id) {
return s.featureRepository.GetByID(ctx, id)
Expand Down
38 changes: 38 additions & 0 deletions internal/api/v1beta1/billing_customer.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type CustomerService interface {
List(ctx context.Context, filter customer.Filter) ([]customer.Customer, error)
Delete(ctx context.Context, id string) error
ListPaymentMethods(ctx context.Context, id string) ([]customer.PaymentMethod, error)
Update(ctx context.Context, customer customer.Customer) (customer.Customer, error)
}

func (h Handler) CreateBillingAccount(ctx context.Context, request *frontierv1beta1.CreateBillingAccountRequest) (*frontierv1beta1.CreateBillingAccountResponse, error) {
Expand Down Expand Up @@ -158,6 +159,43 @@ func (h Handler) GetBillingBalance(ctx context.Context, request *frontierv1beta1
}, nil
}

func (h Handler) UpdateBillingAccount(ctx context.Context, request *frontierv1beta1.UpdateBillingAccountRequest) (*frontierv1beta1.UpdateBillingAccountResponse, error) {
logger := grpczap.Extract(ctx)

metaDataMap := metadata.Build(request.GetBody().GetMetadata().AsMap())
updatedCustomer, err := h.customerService.Update(ctx, customer.Customer{
ID: request.GetId(),
OrgID: request.GetOrgId(),
Name: request.GetBody().GetName(),
Email: request.GetBody().GetEmail(),
Phone: request.GetBody().GetPhone(),
Currency: request.GetBody().GetCurrency(),
Address: customer.Address{
City: request.GetBody().GetAddress().GetCity(),
Country: request.GetBody().GetAddress().GetCountry(),
Line1: request.GetBody().GetAddress().GetLine1(),
Line2: request.GetBody().GetAddress().GetLine2(),
PostalCode: request.GetBody().GetAddress().GetPostalCode(),
State: request.GetBody().GetAddress().GetState(),
},
Metadata: metaDataMap,
})
if err != nil {
logger.Error(err.Error())
return nil, grpcInternalServerError
}

customerPB, err := transformCustomerToPB(updatedCustomer)
if err != nil {
logger.Error(err.Error())
return nil, grpcInternalServerError
}

return &frontierv1beta1.UpdateBillingAccountResponse{
BillingAccount: customerPB,
}, nil
}

func transformCustomerToPB(customer customer.Customer) (*frontierv1beta1.BillingAccount, error) {
metaData, err := customer.Metadata.ToStructPB()
if err != nil {
Expand Down
19 changes: 11 additions & 8 deletions internal/api/v1beta1/billing_product.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ func (h Handler) UpdateProduct(ctx context.Context, request *frontierv1beta1.Upd
var productPrices []product.Price
for _, v := range request.GetBody().GetPrices() {
productPrices = append(productPrices, product.Price{
ID: v.GetId(),
Name: v.GetName(),
Metadata: metadata.Build(v.GetMetadata().AsMap()),
})
Expand All @@ -88,20 +89,22 @@ func (h Handler) UpdateProduct(ctx context.Context, request *frontierv1beta1.Upd
var productFeatures []product.Feature
for _, v := range request.GetBody().GetFeatures() {
productFeatures = append(productFeatures, product.Feature{
ID: v.GetId(),
Name: v.GetName(),
ProductIDs: v.GetProductIds(),
Metadata: metadata.Build(v.GetMetadata().AsMap()),
})
}
updatedProduct, err := h.productService.Update(ctx, product.Product{
ID: request.GetId(),
Name: request.GetBody().GetName(),
Title: request.GetBody().GetTitle(),
Description: request.GetBody().GetDescription(),
Behavior: product.Behavior(request.GetBody().GetBehavior()),
Prices: productPrices,
Features: productFeatures,
Metadata: metaDataMap,
ID: request.GetId(),
Name: request.GetBody().GetName(),
Title: request.GetBody().GetTitle(),
Description: request.GetBody().GetDescription(),
Behavior: product.Behavior(request.GetBody().GetBehavior()),
CreditAmount: request.GetBody().GetCreditAmount(),
Prices: productPrices,
Features: productFeatures,
Metadata: metaDataMap,
})
if err != nil {
logger.Error(err.Error())
Expand Down
1 change: 1 addition & 0 deletions internal/store/postgres/billing_customer_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ func (r BillingCustomerRepository) UpdateByID(ctx context.Context, toUpdate cust
updateRecord := goqu.Record{
"email": toUpdate.Email,
"phone": toUpdate.Phone,
"currency": toUpdate.Currency,
"address": marshaledAddress,
"metadata": marshaledMetadata,
"updated_at": goqu.L("now()"),
Expand Down
9 changes: 5 additions & 4 deletions internal/store/postgres/billing_product_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,10 +206,11 @@ func (r BillingProductRepository) UpdateByName(ctx context.Context, toUpdate pro
return product.Product{}, fmt.Errorf("%w: %s", parseErr, err)
}
updateRecord := goqu.Record{
"title": toUpdate.Title,
"description": toUpdate.Description,
"metadata": marshaledMetadata,
"updated_at": goqu.L("now()"),
"title": toUpdate.Title,
"description": toUpdate.Description,
"credit_amount": toUpdate.CreditAmount,
"metadata": marshaledMetadata,
"updated_at": goqu.L("now()"),
}
if toUpdate.State != "" {
updateRecord["state"] = toUpdate.State
Expand Down
Loading

0 comments on commit 139c38d

Please sign in to comment.