pay

package module
v0.0.2-beta Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 21, 2023 License: MIT Imports: 18 Imported by: 4

README

Pay

Pay serves as an intermediary between your application and third party payment providers such as stripe, allowing you to query all plan, customer and subscription data without directly going to the provider. It also allows you to create checkout sessions so that your customers can purchase your plans.

Pay synchronizes with the provider and stores data in a way that is provider-agnostic. Data can be kept in sync via  a Webhook() http.Handler that receives events from the provider, or manually via the Sync method. This approach boosts your application's robustness, speeds up data retrieval, and allows you to support multiple providers.

To see an example of a micro service that uses this package check out https://github.com/cristosal/cent

Installation

go get -u github.com/cristosal/pay

Usage

Below is the example of how to use the stripe provider. Note that error handling has been omitted for brevity.

Initialization

Open an sql database

db, err := sql.Open("pgx", os.Getenv("CONNECTION_STRING"))

Create the provider

provider := pay.NewStripeProvider(&pay.StripeConfig{
	Repo:          pay.NewEntityRepo(db),
	Key:           os.Getenv("STRIPE_API_KEY"),
	WebhookSecret: os.Getenv("STRIPE_WEBHOOK_SECRET"),
})

Initialize the provider. This will create payment tables by running the migrations

err := provider.Init(context.TODO())
Syncing Data

To have a local copy of data that exists in our stripe account, it is good practice to run the Sync method any time the application starts so as to always have up-to-date data.

// Adds or updates our database with the newest data available
err := provider.Sync()
Receive updates from the provider

In order to keep the data in sync during the lifetime of our application we need to receive updates from the provider to our Webhook.

// Register the webhook
http.HandleFunc("/webhook/stripe", provider.Webhook())

// Start the http server
http.ListenAndServe(":8080", nil)

Checkout

When our customers want to purchase a plan at a specific pricing we can give them a url to visit to checkout.

IMPORTANT: All Add, Remove and Update methods in the provider object, do not go directly to the database. Instead, they issue a request to the provider (Stripe) to perform the action. Stripe will then send a POST request to the webhook. Upon receiving the message, entities in the database will be updated. In practice, doing a ListAllCustomers right after an AddCustomer is not guaranteed to return the customer that was added. You have to wait until stripe acknowledges the request and sends the event to your webhook. This approach ensures that your data is always in sync with stripe, and allows changes within the stripe dashboard itself to be reflected in your database.

Add Plan

Note that you do not need to specify the Provider as we have already created the provider as a StripeProvider so this field will get populated automatically. Both ID and ProviderID don't exist yet so those fields are blank as well.

err := provider.AddPlan(&pay.Plan{
	Name:        "Basic Plan",
	Description: "Access all basic features",
	Active:      true,
})
Add Price

After the plan is saved we will add a price to it

err := provider.AddPrice(&pay.Price{
	PlanID:    1,    // replace with your plan id
	Amount:    1000, // this is in cents. The equivalent would be $10.00
	Currency: "USD",
	Schedule: PricingMonthly,
})
Add Customer

Next let's add the customer

err := provider.AddCustomer(&pay.Customer{
	Name: "Test Customer",
	Email: "test@example.com",
})
Redirect to checkout

With all our entities in place we can now perform the checkout. The RedirectURL property is the url a user will redirect to once they have completed the checkout

url, err := provider.Checkout(&pay.CheckoutRequest{
	CustomerID:  1,    // id of our customer
	PriceID:     1,    // id of our price attached to a plan
	RedirectURL: "http://myapp.com/success",
})

The url return variable contains the url that a user can go to actually perform the checkout. If you are using pay within the context of a web app you can redirect the user as follows

func HandleCheckout(w http.ResponseWriter, r *http.Request) {
	// ... your checkout logic here
	
	// assuming everything is correct and we have url variable available
	http.Redirect(w, r, url, http.StatusSeeOther)
}

Events

You can hook into events using any of the various On methods.

provider.OnSubscriptionCreated(func (s *Subscription) {
	log.Printf("subscription with id %s created", s.ProviderID)
})

provider.OnSubscriptionUpdated(func (prev, current *Subscription) {
	if prev.Active != s.Active {
		log.Printf("subscription %s status changed to %v", 
			current.ProviderID, current.Active)
	}
})

provider.OnSubscriptionRemoved(func (s *Subscription) {
	log.Printf("subscription %s deleted", s.ProviderID)
})

These events are available for Plans, Customers, and Prices as well.

Associating multiple users with a subscription

Sometimes you want to associate multiple users with one subscription. This can be the case in seat-based plans where a customer can give x amount of users access to an account.

When a subscription is first added, a SubscriptionUser is added with username being the email of the customer that purchased the subscription.

To help facilitate this we have the SubscriptionUser entity

type SubscriptionUser struct {
	SubscriptionID int64
	Username       string
}

Username is the unique identifier for the user.

We can manage seats by using the following methods

func (r *Repo) AddSubscriptionUser(su *SubscriptionUser) error

func (r *Repo) RemoveSubscriptionUser(su *SubscriptionUser) error

func (r *Repo) CountSubscriptionUsers(subID int64) (int64, error)

If we want to get the underlying subscription or plan for the user...

func (r *Repo) GetSubscriptionByUsername(username string) (*Subscription, error)

func (r *Repo) GetPlanByUsername(username string) (*Plan, error)

Documentation

Index

Constants

View Source
const DefaultSchema = "pay"

DefaultSchema where tables will be stored can be overriden using

View Source
const ProviderStripe = "stripe"

Variables

View Source
var (
	ErrSubscriptionNotFound  = errors.New("subscription not found")
	ErrSubscriptionNotActive = errors.New("subscription not active")
)
View Source
var ErrCheckoutFailed = errors.New("checkout failed")

Functions

This section is empty.

Types

type CheckoutRequest

type CheckoutRequest struct {
	CustomerID  int64
	PriceID     int64
	RedirectURL string
}

CheckoutRequest

type Customer

type Customer struct {
	ID         int64  // internal (to this service)
	ProviderID string // external providers id
	Provider   string // the provider for this customer
	Name       string // customers name
	Email      string // customers email
}

Customer from a provider like stripe or paypal

func (*Customer) TableName

func (c *Customer) TableName() string

type Migration

type Migration = orm.Migration

type Plan

type Plan struct {
	ID          int64
	Name        string
	Description string
	Provider    string
	ProviderID  string
	Active      bool
}

Plan that customers will subscribe to

func (*Plan) TableName

func (p *Plan) TableName() string

type Price

type Price struct {
	ID         int64
	PlanID     int64
	Provider   string
	ProviderID string
	Amount     int64
	Currency   string
	Schedule   PricingSchedule
	TrialDays  int
}

func (*Price) HasTrial

func (p *Price) HasTrial() bool

func (*Price) TableName

func (p *Price) TableName() string

func (*Price) TrialEnd

func (p *Price) TrialEnd() time.Time

TrialEnd returns the time at which the trial would end if it started now

type PricingSchedule

type PricingSchedule = string
const (
	PricingAnnual  PricingSchedule = "annual"
	PricingMonthly PricingSchedule = "monthly"
	PricingOnce    PricingSchedule = "once"
)

type Repo

type Repo struct {
	// contains filtered or unexported fields
}

Repo contains methods for storing entities within an sql database

func NewEntityRepo

func NewEntityRepo(db *sql.DB) *Repo

NewEntityRepo is a constructor for *Repo

func (*Repo) AddMigrations

func (r *Repo) AddMigrations(migrations []Migration)

AddMigrations adds migrations to the execute after the base migrations

func (*Repo) AddSubscriptionUser

func (r *Repo) AddSubscriptionUser(su *SubscriptionUser) error

func (*Repo) CountSubscriptionUsers

func (r *Repo) CountSubscriptionUsers(subID int64) (int64, error)

func (*Repo) Destroy

func (r *Repo) Destroy(ctx context.Context) error

Destroy removes all tables and relationships

func (*Repo) GetCustomerByEmail

func (r *Repo) GetCustomerByEmail(email string) (*Customer, error)

GetCustomerByEmail returns the customer with a given email

func (*Repo) GetCustomerByID

func (r *Repo) GetCustomerByID(id int64) (*Customer, error)

GetCustomerByID returns the customer by its id field

func (*Repo) GetCustomerByProvider

func (r *Repo) GetCustomerByProvider(provider, providerID string) (*Customer, error)

GetCustomerByProvider returns the customer with provider id. Provider id refers to the id given to the customer by an external provider such as stripe or paypal.

func (*Repo) GetPlanByID

func (r *Repo) GetPlanByID(id int64) (*Plan, error)

GetPlanByID returns the plan matching the internal id

func (*Repo) GetPlanByName

func (r *Repo) GetPlanByName(name string) (*Plan, error)

GetPlanByName returns the plan with given name

func (*Repo) GetPlanByPriceID

func (r *Repo) GetPlanByPriceID(priceID int64) (*Plan, error)

func (*Repo) GetPlanByProviderID

func (r *Repo) GetPlanByProviderID(provider, providerID string) (*Plan, error)

GetPlanByProvider returns the plan which matches provider and provider id

func (*Repo) GetPlanBySubscriptionID

func (r *Repo) GetPlanBySubscriptionID(subID int64) (*Plan, error)

func (*Repo) GetPlansByUsername

func (r *Repo) GetPlansByUsername(username string) (plans []Plan, err error)

func (*Repo) GetPriceByID

func (r *Repo) GetPriceByID(priceID int64) (*Price, error)

GetPriceByID returns the price by a given id

func (*Repo) GetPriceByProvider

func (r *Repo) GetPriceByProvider(provider, providerID string) (*Price, error)

GetPriceByID returns the price by a given id

func (*Repo) GetSubscriptionByID

func (r *Repo) GetSubscriptionByID(id int64) (*Subscription, error)

func (*Repo) GetSubscriptionByProvider

func (r *Repo) GetSubscriptionByProvider(provider, providerID string) (*Subscription, error)

func (*Repo) Init

func (r *Repo) Init() error

Init creates the required tables and migrations for entities. The call to init is idempotent and can therefore be called many times acheiving the same result.

func (*Repo) ListActivePlans

func (r *Repo) ListActivePlans() ([]Plan, error)

ListActivePlans returns a list of all active plans in alphabetic order

func (*Repo) ListAllCustomers

func (r *Repo) ListAllCustomers() ([]Customer, error)

ListAllCustomers returns a list of prices

func (*Repo) ListAllPrices

func (r *Repo) ListAllPrices() ([]Price, error)

ListAllPrices returns a list of prices

func (*Repo) ListAllSubscriptions

func (r *Repo) ListAllSubscriptions() ([]Subscription, error)

func (*Repo) ListAllWebhookEvents

func (r *Repo) ListAllWebhookEvents() ([]WebhookEvent, error)

ListAllWebhookEvents returns a list of all webhook events

func (*Repo) ListPlans

func (r *Repo) ListPlans() ([]Plan, error)

Lists all plans

func (*Repo) ListPricesByPlanID

func (r *Repo) ListPricesByPlanID(planID int64) ([]Price, error)

ListPrices returns a list of prices

func (*Repo) ListSubscriptionsByCustomerID

func (r *Repo) ListSubscriptionsByCustomerID(customerID int64) ([]Subscription, error)

func (*Repo) ListSubscriptionsByPlanID

func (r *Repo) ListSubscriptionsByPlanID(planID int64) ([]Subscription, error)

func (*Repo) ListSubscriptionsByUsername

func (r *Repo) ListSubscriptionsByUsername(username string) ([]Subscription, error)

ListSubscriptionsByUsername returns all subscriptions that have a user with given username

func (*Repo) ListUsernames

func (r *Repo) ListUsernames(subID int64) ([]string, error)

ListUsername returns a list of all usernames attached to subscription

func (*Repo) OnCustomerAdded

func (e *Repo) OnCustomerAdded(cb func(*Customer))

func (*Repo) OnCustomerRemoved

func (e *Repo) OnCustomerRemoved(cb func(*Customer))

func (*Repo) OnCustomerUpdated

func (e *Repo) OnCustomerUpdated(cb func(*Customer, *Customer))

func (*Repo) OnPlanAdded

func (e *Repo) OnPlanAdded(cb func(*Plan))

func (*Repo) OnPlanRemoved

func (e *Repo) OnPlanRemoved(cb func(*Plan))

func (*Repo) OnPlanUpdated

func (e *Repo) OnPlanUpdated(cb func(*Plan, *Plan))

func (*Repo) OnPriceAdded

func (e *Repo) OnPriceAdded(cb func(*Price))

func (*Repo) OnPriceRemoved

func (e *Repo) OnPriceRemoved(cb func(*Price))

func (*Repo) OnPriceUpdated

func (e *Repo) OnPriceUpdated(cb func(*Price, *Price))

func (*Repo) OnSubscriptionAdded

func (e *Repo) OnSubscriptionAdded(cb func(*Subscription))

func (*Repo) OnSubscriptionRemoved

func (e *Repo) OnSubscriptionRemoved(cb func(*Subscription))

func (*Repo) OnSubscriptionUpdated

func (e *Repo) OnSubscriptionUpdated(cb func(*Subscription, *Subscription))

func (*Repo) RemoveSubscriptionUser

func (r *Repo) RemoveSubscriptionUser(su *SubscriptionUser) error

func (*Repo) SetMigrationsTable

func (r *Repo) SetMigrationsTable(table string)

SetMigrationsTable for setting up migrations during init

func (*Repo) SetSchema

func (r *Repo) SetSchema(schema string)

SetSchema used for storing entity tables

type StripeConfig

type StripeConfig struct {
	Repo          *Repo
	Key           string
	WebhookSecret string
}

StripeConfig configures StripeService with necessary credentials and callbacks

type StripeProvider

type StripeProvider struct {
	*Repo
	// contains filtered or unexported fields
}

StripeProvider interfaces with stripe for customer, plan and subscription data

func NewStripeProvider

func NewStripeProvider(config *StripeConfig) *StripeProvider

NewStripeProvider creates a provider service for interacting with stripe

func (*StripeProvider) AddCustomer

func (s *StripeProvider) AddCustomer(c *Customer) error

AddCustomer directly in stripe

func (*StripeProvider) AddPlan

func (s *StripeProvider) AddPlan(p *Plan) error

AddPlan directly in stripe

func (*StripeProvider) AddPrice

func (s *StripeProvider) AddPrice(p *Price) error

AddPrice directly in stripe

func (*StripeProvider) Checkout

func (s *StripeProvider) Checkout(request *CheckoutRequest) (url string, err error)

Checkout returns the url that a user has to visit in order to complete payment it registers the customer if it was unavailable

func (StripeProvider) OnCustomerAdded

func (e StripeProvider) OnCustomerAdded(cb func(*Customer))

func (StripeProvider) OnCustomerRemoved

func (e StripeProvider) OnCustomerRemoved(cb func(*Customer))

func (StripeProvider) OnCustomerUpdated

func (e StripeProvider) OnCustomerUpdated(cb func(*Customer, *Customer))

func (StripeProvider) OnPlanAdded

func (e StripeProvider) OnPlanAdded(cb func(*Plan))

func (StripeProvider) OnPlanRemoved

func (e StripeProvider) OnPlanRemoved(cb func(*Plan))

func (StripeProvider) OnPlanUpdated

func (e StripeProvider) OnPlanUpdated(cb func(*Plan, *Plan))

func (StripeProvider) OnPriceAdded

func (e StripeProvider) OnPriceAdded(cb func(*Price))

func (StripeProvider) OnPriceRemoved

func (e StripeProvider) OnPriceRemoved(cb func(*Price))

func (StripeProvider) OnPriceUpdated

func (e StripeProvider) OnPriceUpdated(cb func(*Price, *Price))

func (StripeProvider) OnSubscriptionAdded

func (e StripeProvider) OnSubscriptionAdded(cb func(*Subscription))

func (StripeProvider) OnSubscriptionRemoved

func (e StripeProvider) OnSubscriptionRemoved(cb func(*Subscription))

func (StripeProvider) OnSubscriptionUpdated

func (e StripeProvider) OnSubscriptionUpdated(cb func(*Subscription, *Subscription))

func (*StripeProvider) RemoveCustomerByProviderID

func (s *StripeProvider) RemoveCustomerByProviderID(providerID string) error

RemoveCustomer directly in stripe

func (*StripeProvider) RemovePlanByProviderID

func (s *StripeProvider) RemovePlanByProviderID(providerID string) error

RemovePlan from stripe

func (*StripeProvider) Sync

func (s *StripeProvider) Sync() error

Sync repository data with stripe

func (*StripeProvider) VerifyCheckout

func (s *StripeProvider) VerifyCheckout(sessionID string) error

Verify that the checkout was completed

func (*StripeProvider) Webhook

func (s *StripeProvider) Webhook() http.HandlerFunc

Webhook returns the http handler that is responsible for handling any event received from stripe

type Subscription

type Subscription struct {
	ID         int64
	Provider   string
	ProviderID string
	CustomerID int64
	PriceID    int64
	Active     bool
	CreatedAt  time.Time
}

Subscription represents a customers subscription to a Plan

func (*Subscription) TableName

func (s *Subscription) TableName() string

type SubscriptionUser

type SubscriptionUser struct {
	SubscriptionID int64
	Username       string
}

func (SubscriptionUser) TableName

func (SubscriptionUser) TableName() string

type WebhookEvent

type WebhookEvent struct {
	ID         int64
	Provider   string
	ProviderID string
	EventType  string
	Payload    []byte
}

func (*WebhookEvent) TableName

func (e *WebhookEvent) TableName() string

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL