chartmogul/chartmogul-go

View on GitHub
chartmogul.go

Summary

Maintainability
A
35 mins
Test Coverage
C
70%
// Package chartmogul is a simple Go API library for Chartmogul public API.
//
// HTTP 2
//
// ChartMogul's current stable version of nginx is incompatible with HTTP 2 implementation of Go.
// For this reason the application must run with the following (or otherwise prohibit HTTP 2):
//  export GODEBUG=http2client=0
//
// Uses the library gorequest, which allows simple struct->query, body->struct,
// struct->body.
package chartmogul

import (
    "fmt"
    "net/http"
    "time"

    "github.com/parnurzeal/gorequest"
)

const (
    // ErrKeyExternalID is key in Errors map indicating there's a problem with External ID of the resource.
    ErrKeyExternalID = "external_id"
    // ErrKeyTransactionExternalID is key in Errors map indicating there's a problem with External ID of the transaction.
    ErrKeyTransactionExternalID = "transactions.external_id"
    // ErrKeyLineItemsExternalID indicates problem with one/any of line items' external IDs
    ErrKeyLineItemsExternalID = "line_items.external_id"
    // ErrKeyName - data source name
    ErrKeyName = "name"
    // ErrValCustomerExternalIDExists = can't import new customer with the same external ID
    ErrValCustomerExternalIDExists = "The external ID for this customer already exists in our system."
    // ErrValLineItemExternalIDExists = can't import invoice, b'c line item external ID exists
    ErrValLineItemExternalIDExists = "The external ID for this line item already exists in our system."
    // ErrValExternalIDExists = can't save Transaction, because it exists already.
    ErrValExternalIDExists = "has already been taken"
    // ErrValInvoiceExternalIDExists = invoice already exists
    ErrValInvoiceExternalIDExists = "The external ID for this invoice already exists in our system."
    // ErrValPlanExternalIDExists = plan already exists
    ErrValPlanExternalIDExists = "A plan with this identifier already exists in our system."
    // ErrValHasAlreadyBeenTaken = data source name taken
    ErrValHasAlreadyBeenTaken = "Has already been taken."
)

var (
    url     = "https://api.chartmogul.com/v1/%v"
    timeout = 30 * time.Second
)

// IApi defines the interface of the library.
// Necessary eg. for mocks in testing.
type IApi interface {
    Ping() (res bool, err error)
    // Data sources
    CreateDataSource(name string) (*DataSource, error)
    CreateDataSourceWithSystem(dataSource *DataSource) (*DataSource, error)
    RetrieveDataSource(dataSourceUUID string) (*DataSource, error)
    ListDataSources() (*DataSources, error)
    ListDataSourcesWithFilters(listDataSourcesParams *ListDataSourcesParams) (*DataSources, error)
    PurgeDataSource(dataSourceUUID string) error
    EmptyDataSource(dataSourceUUID string) error
    DeleteDataSource(dataSourceUUID string) error
    // Invoices
    CreateInvoices(invoices []*Invoice, customerUUID string) (*Invoices, error)
    ListInvoices(cursor *Cursor, customerUUID string) (*Invoices, error)
    ListAllInvoices(listAllInvoicesParams *ListAllInvoicesParams) (*Invoices, error)
    RetrieveInvoice(invoiceUUID string) (*Invoice, error)
    DeleteInvoice(invoiceUUID string) error
    // Plans
    CreatePlan(plan *Plan) (result *Plan, err error)
    RetrievePlan(planUUID string) (*Plan, error)
    ListPlans(listPlansParams *ListPlansParams) (*Plans, error)
    UpdatePlan(plan *Plan, planUUID string) (*Plan, error)
    DeletePlan(planUUID string) error
    // Plan Groups
    CreatePlanGroup(planGroup *PlanGroup) (result *PlanGroup, err error)
    RetrievePlanGroup(planGroupUUID string) (*PlanGroup, error)
    ListPlanGroups(cursor *Cursor) (*PlanGroups, error)
    UpdatePlanGroup(plan *PlanGroup, planGroupUUID string) (*PlanGroup, error)
    DeletePlanGroup(planGroupUUID string) error
    ListPlanGroupPlans(cursor *Cursor, planGroupUUID string) (*PlanGroupPlans, error)
    // Subscriptions
    CancelSubscription(subscriptionUUID string, cancelSubscriptionParams *CancelSubscriptionParams) (*Subscription, error)
    ListSubscriptions(cursor *Cursor, customerUUID string) (*Subscriptions, error)
    // Transactions
    CreateTransaction(transaction *Transaction, invoiceUUID string) (*Transaction, error)

    // Customers
    CreateCustomer(newCustomer *NewCustomer) (*Customer, error)
    RetrieveCustomer(customerUUID string) (*Customer, error)
    UpdateCustomer(Customer *Customer, customerUUID string) (*Customer, error)
    UpdateCustomerV2(Customer *UpdateCustomer, customerUUID string) (*Customer, error)
    ListCustomers(ListCustomersParams *ListCustomersParams) (*Customers, error)
    SearchCustomers(SearchCustomersParams *SearchCustomersParams) (*Customers, error)
    MergeCustomers(MergeCustomersParams *MergeCustomersParams) error
    DeleteCustomer(customerUUID string) error
    DeleteCustomerInvoices(dataSourceUUID, customerUUID string) error
    DeleteCustomerInvoicesV2(dataSourceUUID, customerUUID string, DeleteCustomerInvoicesParams *DeleteCustomerInvoicesParams) error

    //  - Cusomer Attributes
    RetrieveCustomersAttributes(customerUUID string) (*Attributes, error)

    //  Tags
    AddTagsToCustomer(customerUUID string, tags []string) (*TagsResult, error)
    AddTagsToCustomersWithEmail(email string, tags []string) (*Customers, error)
    RemoveTagsFromCustomer(customerUUID string, tags []string) (*TagsResult, error)

    // Custom Attributes
    AddCustomAttributesToCustomer(customerUUID string, customAttributes []*CustomAttribute) (*CustomAttributes, error)
    AddCustomAttributesWithEmail(email string, customAttributes []*CustomAttribute) (*Customers, error)
    UpdateCustomAttributesOfCustomer(customerUUID string, customAttributes map[string]interface{}) (*CustomAttributes, error)
    RemoveCustomAttributes(customerUUID string, customAttributes []string) (*CustomAttributes, error)

    // Metrics
    MetricsRetrieveAll(metricsFilter *MetricsFilter) (*MetricsResult, error)
    MetricsRetrieveMRR(metricsFilter *MetricsFilter) (*MRRResult, error)
    MetricsRetrieveARR(metricsFilter *MetricsFilter) (*ARRResult, error)
    MetricsRetrieveARPA(metricsFilter *MetricsFilter) (*ARPAResult, error)
    MetricsRetrieveASP(metricsFilter *MetricsFilter) (*ASPResult, error)
    MetricsRetrieveCustomerCount(metricsFilter *MetricsFilter) (*CustomerCountResult, error)
    MetricsRetrieveCustomerChurnRate(metricsFilter *MetricsFilter) (*CustomerChurnRateResult, error)
    MetricsRetrieveMRRChurnRate(metricsFilter *MetricsFilter) (*MRRChurnRateResult, error)
    MetricsRetrieveLTV(metricsFilter *MetricsFilter) (*LTVResult, error)

    // Metrics - Subscriptions & Activities
    MetricsListSubscriptions(cursor *Cursor, customerUUID string) (*MetricsSubscriptions, error)
    MetricsListActivities(cursor *Cursor, customerUUID string) (*MetricsActivities, error)
    MetricsCreateActivitiesExport(CreateMetricsActivitiesExportParam *CreateMetricsActivitiesExportParam) (*MetricsActivitiesExport, error)
    MetricsRetrieveActivitiesExport(activitiesExportUUID string) (*MetricsActivitiesExport, error)

    // Account
    RetrieveAccount() (*Account, error)
}

// API is the handle for communicating with Chartmogul.
type API struct {
    AccountToken string
    AccessKey    string
    Client       *http.Client
}

// Cursor contains query parameters for paging in CM.
// Attributes for query must be string, because gorequest library cannot convert anything else.
type Cursor struct {
    Page    uint32 `json:"page,omitempty"`
    PerPage uint32 `json:"per_page,omitempty"`
}

// Errors contains error feedback from ChartMogul
type Errors map[string]string

func (e Errors) Error() string {
    return fmt.Sprintf("chartmogul: %v", map[string]string(e))
}

// IsAlreadyExists is helper that returns true, if there's only one error
// and it means the uploaded resource of the same external_id already exists.
func (e Errors) IsAlreadyExists() (is bool) {
    if e == nil {
        return
    }
    if len(e) != 1 {
        return
    }
    msg, ok := e[ErrKeyExternalID]
    if !ok {
        msg, ok = e[ErrKeyTransactionExternalID]
    }
    if !ok {
        msg = e[ErrKeyName]
    }
    return msg == ErrValExternalIDExists ||
        msg == ErrValHasAlreadyBeenTaken ||
        msg == ErrValCustomerExternalIDExists ||
        msg == ErrValPlanExternalIDExists ||
        msg == ErrValInvoiceExternalIDExists
}

// IsInvoiceAndItsEntitiesAlreadyExist returns true if:
// * invoice already exists AND
// * ANY other entities (line items, transactions) already exist AND
// * no other error
//
// So, eg. if the invoice doesn't exist, but the transaction does,
// you have a different problem (duplicating txns) and this returns false.
func (e Errors) IsInvoiceAndItsEntitiesAlreadyExist() (is bool) {
    if e == nil {
        return
    }
    if msg := e[ErrKeyExternalID]; msg != ErrValInvoiceExternalIDExists {
        return
    }
    for key, val := range e {
        switch key {
        case ErrKeyTransactionExternalID:
            if val != ErrValExternalIDExists {
                return
            }
        case ErrKeyLineItemsExternalID:
            if val != ErrValLineItemExternalIDExists {
                return
            }
        case ErrKeyExternalID:
            // already checked
        default:
            return
        }
    }

    return true
}

// IsInvoiceAndTransactionAlreadyExist occurs when both invoice and tx exist already.
// Use `IsInvoiceAndItsEntitiesAlreadyExist` if you'd like to catch line items as well.
func (e Errors) IsInvoiceAndTransactionAlreadyExist() (is bool) {
    if e == nil {
        return
    }
    if len(e) != 2 {
        return
    }
    msg1, ok1 := e[ErrKeyExternalID]
    msg2, ok2 := e[ErrKeyTransactionExternalID]
    return ok1 && ok2 &&
        msg1 == ErrValInvoiceExternalIDExists && msg2 == ErrValExternalIDExists
}

// Setup configures global timeout for the library.
func Setup(timeoutConf time.Duration) {
    timeout = timeoutConf
}

// SetURL changes target URL for the module globally.
func SetURL(specialURL string) {
    url = specialURL
}

// SetClient changes the client - for VCR integration tests.
func (api *API) SetClient(newClient *http.Client) {
    api.Client = newClient
}

func prepareURL(path string) string {
    return fmt.Sprintf(url, path)
}

func (api API) req(req *gorequest.SuperAgent) *gorequest.SuperAgent {
    // defaults for client go here:
    if api.Client != nil {
        req.Client = api.Client
    }
    return req.Timeout(timeout).
        SetBasicAuth(api.AccountToken, api.AccessKey).
        Set("Content-Type", "application/json")
}