qvo

package module
v0.0.0-...-3a6bcf8 Latest Latest
Warning

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

Go to latest
Published: Jul 26, 2018 License: MIT Imports: 12 Imported by: 0

README

qvo-go-client

Description

Unofficial Go client for the QVO payment service.
All objects and calls from their REST API are implemented.

Documentation

The package implements everything as described on QVO's docs.
Please refer to their offical REST API documentation for information about expected parameters.
Also check the package's godocs for details about the implementation.

Requirements

This project depends on 3 Go packages:

github.com/pkg/errors for better error handling.
github.com/smartystreets/goconvey/convey for testing.
github.com/sirupsen/logrus for logging.

You may install them easilly by running this:

make requirements

Get the package with:

go get github.com/iegomez/qvo-go-client

Tests

In order to test the package, you need to set the QVO_TEST_TOKEN env var with your sandbox api token. Just export the var in the terminal before running the tests, or add the export to your .profile, .bash_profile, .bash_rc, etc., depending on your system, and then source the file before running tests.

Only customer and plan tests (and events listing in customer test) are available, as transaction, subscription, payment, withdrawal and webpay process need real card data or user actions to be tested.
So please file an issue for any bug you may encounter using them and I'll fix it as soon as possible.

You may run the tests like this:

make test

If you want tests to stop at first fail, run them like this:

mate test-fast

They run with debug log level. Just delete the line setting the level at the test files to run with info level, or set your level of preference.

Usage

After importing it, the package qvo is exposed:

import "github.com/iegomez/qvo-go-client"

The Client expects a JWT authorization token for the API, and a sandbox/production mode bool (true for sandbox). So, using your token, you may intialize a pointer to a Client and then call any exported function passing the pointer:

c := qvo.NewClient("your-api-token", true) //NewClient returns a pointer to a qvo client.

var where = make(map[string]map[string]interface{}) //Create map for the filters
where["name"] = make(map[string]interface{})
where["name"]["like"] = "%Test%"

plans, err := qvo.ListPlans(c, 0, 0, where, "") //To omit pages, perPage or order parameters, just pass Go's zero values for ints and string.

Client's default log level is Info, but youy may change it with the method SetLogLevel:

c := qvo.NewClient("your-api-token", true) //NewClient returns a pointer to a qvo client.
c.SetLogLevel(log.DebugLevel)

Example

Here´s a stripped example used in a real project showing a function to start a webpay transaction and another to check the transaction's status:


import "github.com/iegomez/qvo-go-client"

//StartPayment receives a contract id and calls to QVO for a webpay request. It returns the qvo redirect url or an error.
//It looks for a customer at QVO's end with the client's email. If there's none, it creates it and then makes the request.
func StartPayment(db *sqlx.DB, qc *qvo.Client, rp *redis.Pool, contractID int64) (string, error) {

	//Get redis connection.
	conn := rp.Get()
	defer conn.Close()

	contract, err := GetContract(db, contractID)
	if err != nil {
		return "", err
	}

	user, err := GetInternalUser(db, contract.ClientID)
	if err != nil {
		return "", err
	}

	client, err := GetUserClient(db, user.ID)
	if err != nil {
		return "", err
	}

	var customer qvo.Customer
	log.Debugf("Paying contract with user %v", user)
	//Check that the user has a qvo customer id. If not, try to create a customer.
	if user.QvoID == "" {
		log.Debugf("trying to create customer for user %d", user.ID)
		customer, err = qvo.CreateCustomer(qc, fmt.Sprintf("%s %s", client.Name, client.Lastname), user.Email)
		if err != nil {
			return "", err
		}
		//Set the customer id for the user.
		err = SetQvoID(db, user.ID, customer.ID)
		if err != nil {
			return "", err
		}
	} else {
		log.Debugf("trying to retrieve customer for user %d with qvo_id %s", user.ID, user.QvoID)
		customer, err = qvo.GetCustomer(qc, user.QvoID)
		if err != nil {
			return "", err
		}
	}

	//Make the transaction request.
	resp, err := qvo.WebpayTransaction(qc, customer.ID, fmt.Sprintf("%s#/webpay_return", common.Host), fmt.Sprintf("user %d (%s) attempts to pay contract %d", user.ID, user.Email, contract.ID), contract.Price)
	if err != nil {
		return "", err
	}

	//Now we should store the transaction id in redis so it may expire. It should contain the contract id to mark it as paid if everything goes ok.
	reply, err := conn.Do("SETEX", resp.TransactionID, int(time.Until(resp.ExpirationDate).Seconds()), contract.ID)
	if err != nil {
		log.Errorf("couldn't set transaction for contract id %d: %s", contract.ID, err)
		return "", err
	}

	log.Debugf("transaction was set with reply: %v", reply)

	return resp.RedirectURL, nil
}



//CheckPayment checks the status of a given transaction. If it's ok, it sets the contract id as paid. If not, it deletes it.
func CheckPayment(db *sqlx.DB, qc *qvo.Client, rp *redis.Pool, transactionID string) (bool, error) {
	//Get the contract id from redis.
	conn := rp.Get()
	defer conn.Close()

	contractID, redisErr := redis.String(conn.Do("GET", transactionID))
	if redisErr != nil {
		log.Errorf("couldn't get contract for transaction %s: %s", transactionID, redisErr)
		return false, redisErr
	}

	cID, err := strconv.ParseInt(contractID, 10, 64)
	if err != nil {
		log.Errorf("strconv error: %s", err)
		return false, err
	}

	//Now check against QVO that the transaction is ok.
	transaction, err := qvo.GetTransaction(qc, transactionID)
	if err != nil {
		log.Errorf("get transaction error: %s", err)
		return false, err
	}

	//We have a transaction, let's check the status.
	//If successful
	if transaction.Status == qvo.Successful {

		payErr := PayContract(db, cID, transaction)
		if payErr != nil {
			log.Errorf("pay contract error: %s", err)
			return false, payErr
		}
		//Everything's ok, delete the transaction ID from redis.
		resp, err := conn.Do("DEL", transactionID)
		if err != nil {
			log.Errorf("Couldn't delete key %d, err: %s", transactionID, err)
		}
		log.Debugf("redis delete key: %s", resp)
		return true, nil
	} else if transaction.Status == qvo.Waiting {
		//If transaction isn't ready, the frontend should keep asking, so signal that.
		log.Errorf("sending retry: %s", err)
		return false, errors.New("retry")
	}
	//On any other status, return false but with a new error to signal the client why it failed.
	//Also, delete the redis key and the contract.
	resp, err := conn.Do("DEL", transactionID)
	if err != nil {
		log.Errorf("Couldn't delete key %d, err: %s", transactionID, err)
	}
	log.Debugf("redis delete key: %s", resp)

	err = DeleteContract(db, cID)
	if err != nil {
		log.Errorf("Couldn't delete contract %d, err: %s", contractID, err)
	}

	return false, errors.Errorf("transaction error: %s", transaction.Status)
}

Caveats

You should be careful about some caveats with the original API:

For list filters and order strings you should really check the offical docs for their syntax, as qvo errors won't mention any issue in the param field. For example, if you pass a random field name as filter, or you mistype the order (e.g., created ASC instead of the correct created_at ASC), QVO will respond with a 500 status code.

The API is somewhat inconsistent with int an decimal fields. First, it allows to pass an int or a string which contains an int as the price field of a plan with CLP currency, but won't allow a float nor a string containing a float. Oddly enough, on creation or retrieval, it'll return a float string for the same field. So you may create a plan with price 19000 or "19000" if the currency is CLP (UF allows both ints and floats), but not 19000.0 or "19000.0", and the API will return it with "19000.0" (always a string, never 19000.0) as the price. I could deal with this at the client implementation, but it seems messy and I've already reported it, so hopefully it'll be addressed soon.

I'll update this section if there's any change on the API.

Contributing

Report any bug by filing an issue.

There's not much to be added client wise, as it is faithful to the public docs from QVO. Nevertheless, please file issues with the enhancement or feature tag for anything that's not included and you'd like to see implemented (stats, comparisons between customer, or any other thing you can think of).

Of course, feel free to submit a PR for any of the above.

License

The QVO Go client is distributed under the MIT license. See also LICENSE.

Documentation

Index

Constants

View Source
const (
	Succeeded string = "succeeded"
	Failed    string = "failed"
)

Status constants

View Source
const (
	CustomerCreated             string = "customer.created"
	CustomerUpdated             string = "customer.updated"
	CustomerDeleted             string = "customer.deleted"
	PlanCreated                 string = "plan.created"
	PlanUpdated                 string = "plan.updated"
	PlanDeleted                 string = "plan.deleted"
	CustomerCardCreated         string = "customer.card.created"
	CustomerCardDeleted         string = "customer.card.deleted"
	CustomerSubscriptionCreated string = "customer.subscription.created"
	CustomerSubscriptionUpdated string = "customer.subscription.updated"
	CustomerSubscriptionDeleted string = "customer.subscription.deleted"
	TransactionPaymentSucceeded string = "transaction.payment_succeeded"
	TransactionPaymentFailed    string = "transaction.payment_failed"
	TransactionPRefunded        string = "transaction.refunded"
	TransactionResponseTimeout  string = "transaction.response_timeout"
)

Event types

View Source
const (
	Successful         string = "successful"
	Rejected           string = "rejected"
	Unable             string = "unable_to_charge"
	Refunded           string = "refunded"
	WaitingForResponse string = "waiting_for_response"
	WaitingResponse    string = "waiting_response"
	Timeout            string = "response?timeout"
)

Status constants

View Source
const (
	WebpayPlus     string = "webpay_plus"
	WebpayOneclick string = "webpay_oneclick"
	Olpays         string = "olpays"
)

Gateway constants

Variables

This section is empty.

Functions

func CancelSubscription

func CancelSubscription(c *Client, subscriptionID string, cancelAtePeriodEnd bool) error

CancelSubscription cancels a subscription. Depending on cancelAtPeriodEnd, it'll be canceled when the current period end is reached (if true), or immediately (if false). If subscription was ianctive, it'll be canceled immediately anyway.

func DeleteCard

func DeleteCard(c *Client, customerID, cardID string) error

DeleteCard deletes a card for a given customer.

func DeleteCustomer

func DeleteCustomer(c *Client, id string) error

DeleteCustomer deletes a customer given its id.

func DeletePlan

func DeletePlan(c *Client, id string) error

DeletePlan deletes a plan given its id.

Types

type Card

type Card struct {
	ID           string    `json:"id"`
	Lats4Digits  string    `json:"last_4_digits"`
	CardType     string    `json:"card_type"`    //VISA or MASTERCARD
	PaymentType  string    `json:"payment_type"` //CD (credit) or DB (debit)
	FailureCount int32     `json:"failure_count"`
	CreatedAt    time.Time `json:"created_at"`
}

Card struct to represent a qvo card object.

func GetCard

func GetCard(c *Client, customerID, cardID string) (Card, error)

GetCard returns a card given a customer id and a card id.

func ListCards

func ListCards(c *Client, customerID string) ([]Card, error)

ListCards retrieves cards for a given customer.

type CardInscriptionResponse

type CardInscriptionResponse struct {
	InscriptionUID string    `json:"inscription_uid"`
	RedirectURL    string    `json:"redirect_url"`
	ExpirationDate time.Time `json:"expiration_date"`
}

CardInscriptionResponse struct holds the answer from qvo for a card inscription response.

func CreateCardInscription

func CreateCardInscription(c *Client, customerID, returnURL string) (CardInscriptionResponse, error)

CreateCardInscription begins a card inscription request. If everything's ok, it'll return an inscription uid, the redirect url to send the customer to, and the expiration date for this transaction.

type CardInscriptionState

type CardInscriptionState struct {
	UID       string        `json:"uid"`
	Status    string        `json:"status"`
	Card      *Card         `json:"card"`
	Error     *errorWrapper `json:"error"`
	CreatedAt time.Time     `json:"created_at"`
	UpdatedAt time.Time     `json:"updated_at"`
}

CardInscriptionState holds the state of a card inscription request.

func GetCardInscription

func GetCardInscription(c *Client, customerID, inscriptionUID string) (CardInscriptionState, error)

GetCardInscription returns the inscription's state and a card (if successful).

type Client

type Client struct {
	Token     string
	IsSandbox bool
}

Client represent the qvo api client. It holds the auth token and sandbox/production mode and offers request methods.

func NewClient

func NewClient(token string, isSandbox bool) *Client

NewClient initializes the api client with a token and a sandbox/production mode. Default log level is info.

func (*Client) SetLogLevel

func (c *Client) SetLogLevel(logLevel log.Level)

SetLogLevel allows to set the log level.

type Customer

type Customer struct {
	ID                   string         `json:"id"`
	DefaultPaymentMethod Card           `json:"default_payment_method"`
	Name                 string         `json:"name"`
	Email                string         `json:"email"`
	Subscriptions        []Subscription `json:"subscriptions"`
	Cards                []Card         `json:"cards"`
	Transactions         []Transaction  `json:"transactions"`
	CreatedAt            time.Time      `json:"created_at"`
	UpdatedAt            time.Time      `json:"updated_at"`
}

Customer struct to represent a qvo customer object.

func CreateCustomer

func CreateCustomer(c *Client, name, email string) (Customer, error)

CreateCustomer creates a customer at the QVO account.

func GetCustomer

func GetCustomer(c *Client, id string) (Customer, error)

GetCustomer retrieves a customer given its id.

func ListCustomers

func ListCustomers(c *Client, page, perPage int, where map[string]map[string]interface{}, orderBy string) ([]Customer, error)

ListCustomers retrieves a list of customers with given pages, filters and order.

func UpdateCustomer

func UpdateCustomer(c *Client, id, name, email, defaultPaymentMethodID string) (Customer, error)

UpdateCustomer updates a customer given its id.

type Event

type Event struct {
	ID        string                  `json:"id"`
	Type      string                  `json:"type"`
	Data      map[string]interface{}  `json:"data"`               //API sends a "hash", so we are limited to an interfaces map.
	Previous  *map[string]interface{} `json:"previous,omitempty"` //API sends a "hash", so we are limited to an interfaces map. Also, it's nullable, so it's a pointer.
	CreatedAt time.Time               `json:"created_at"`
}

Event struct to represent a qvo event object.

func GetEvent

func GetEvent(c *Client, id string) (Event, error)

GetEvent retrieves a event given its id.

func ListEvents

func ListEvents(c *Client, page, perPage int, where map[string]map[string]interface{}, orderBy string) ([]Event, error)

ListEvents retrieves a list of events with given pages, filters and order.

type Filter

type Filter struct {
	Attribute string
	Operator  string
	Value     interface{}
}

Filter implements filter for API queries.

type GatewayResponse

type GatewayResponse struct {
	Status  string `json:"status"`
	Message string `json:"message"`
}

GatewayResponse struct to deal with gateway response from transactions.

type Payment

type Payment struct {
	Amount        int64  `json:"amount"`
	Gateway       string `json:"gateway"`      //One of: webpay_plus, webpay_oneclick, olpays.
	PaymentType   string `json:"payment_type"` //credit or debit.
	Fee           int64  `json:"fee"`
	Installments  int32  `json:"installments"`
	PaymentMethod Card   `json:"payment_method"`
}

Payment struct to represent a qvo payment object.

type Plan

type Plan struct {
	ID                string         `json:"id"`
	Name              string         `json:"name"`
	Price             string         `json:"price"`    //An int or float string.
	Currency          string         `json:"currency"` //CLP or UF.
	Interval          string         `json:"interval"` //One of: day, week, month, year.
	IntervalCount     int32          `json:"interval_count"`
	TrialPeriodDays   int32          `json:"trial_period_days"`
	DefaultCycleCount int32          `json:"default_cycle_count"`
	Status            string         `json:"status"` //active or inactive.
	Subscription      []Subscription `json:"subscriptions"`
	CreatedAt         time.Time      `json:"created_at"`
	UpdatedAt         time.Time      `json:"updated_at"`
}

Plan struct to represent a qvo plan object.

func CreatePlan

func CreatePlan(c *Client, plan Plan) (Plan, error)

CreatePlan creates a plan at QVOs end. Returns a copy of the plan if successful, and an error if not. Price is a string as it may be an int or a float string representation DEPENDING on the currency (int for CLP, float for UF).

func GetPlan

func GetPlan(c *Client, id string) (Plan, error)

GetPlan retrieves a plan by id.

func ListPlans

func ListPlans(c *Client, page, perPage int, where map[string]map[string]interface{}, orderBy string) ([]Plan, error)

ListPlans retrieves a list of plans with given pages, filters and order.

func UpdatePlan

func UpdatePlan(c *Client, planID, name string) (Plan, error)

UpdatePlan updates a plan given its id.

type Refund

type Refund struct {
	Amount    int64     `json:"amount"`
	CreatedAt time.Time `json:"created_at"`
}

Refund struct to represent a qvo refund object.

func RefundTransaction

func RefundTransaction(c *Client, id string) (Refund, error)

RefundTransaction makes a refund request for a given transaction id.

type Subscription

type Subscription struct {
	ID                 string        `json:"id"`
	Status             string        `json:"status"` //One of: active, canceled, trialing, retrying, inactive, unpaid.
	Debt               int64         `json:"debt"`
	Start              time.Time     `json:"start"`
	End                time.Time     `json:"end"`
	CycleCount         int32         `json:"cycle_count"`
	CurrentPeriodStart time.Time     `json:"current_period_start"`
	CurrentPeriodEnd   time.Time     `json:"current_period_end"`
	Customer           Customer      `json:"customer"`
	Plan               Plan          `json:"plan"`
	Transactions       []Transaction `json:"transactions"`
	TaxName            string        `json:"tax_name"`
	TaxPercent         string        `json:"tax_percent"` //A float string.
	CreatedAt          time.Time     `json:"created_at"`
	UpdatedAt          time.Time     `json:"updated_at"`
}

Subscription struct to represent a qvo subscription object.

func CreateSubscription

func CreateSubscription(c *Client, customerID, planID, taxName string, taxPercent float64, cycleCount int64, start *time.Time) (Subscription, error)

CreateSubscription creates a subscription for a customer and plan. Returns a copy of the subscription if successful, and an error if not. customerID and planID are required. cycleCount <= 0 will be omitted. If taxName is "" or taxPercent isn´t in [0.0, 100.0], they'll be omitted. start is a pointer to a time.Time, so a nil pointer will be omitted.

func GetSubscription

func GetSubscription(c *Client, subscriptionID string) (Subscription, error)

GetSubscription returns the subscription or an error.

func ListSubscriptions

func ListSubscriptions(c *Client, page, perPage int, where map[string]map[string]interface{}, orderBy string) ([]Subscription, error)

ListSubscriptions retrieves a list of subscriptions with given pages, filters and order.

func UpdateSubscription

func UpdateSubscription(c *Client, subscriptionID, planID string) (Subscription, error)

UpdateSubscription updates a subscription's plan given its id.

type Transaction

type Transaction struct {
	ID              string                  `json:"id"`
	Amount          int64                   `json:"amount"`
	Currency        string                  `json:"currency"` //CLP or USD.
	Description     string                  `json:"description"`
	Gateway         string                  `json:"gateway"` //One of: webpay_plus, webpay_oneclick, olpays.
	Credits         int64                   `json:"credits"`
	Status          string                  `json:"status"` //One of: successful, rejected, unable_to_charge, refunded, waiting_for_response, response_timeout.
	Customer        Customer                `json:"customer"`
	Payment         *Payment                `json:"payment"` //Nullable, so it's a pointer.
	Refund          *Refund                 `json:"refund"`  //Nullable, so it's a pointer.
	Transable       *map[string]interface{} //API sends a "hash", so we are limited to an interfaces map. Also, it's nullable, so it's a pointer. For now, it's supposed to be a subscription.
	GatewayResponse GatewayResponse         `json:"gateway_response"`
	CreatedAt       time.Time               `json:"created_at"`
	UpdatedAt       time.Time               `json:"updated_at"`
}

Transaction struct to represent a qvo transaction object.

func ChargeCard

func ChargeCard(c *Client, customerID, cardID, description string, amount int64) (Transaction, error)

ChargeCard creates a charge for given customer and card.

func GetTransaction

func GetTransaction(c *Client, id string) (Transaction, error)

GetTransaction retrieves a transaction by id.

func ListTransactions

func ListTransactions(c *Client, page, perPage int, where map[string]map[string]interface{}, orderBy string) ([]Transaction, error)

ListTransactions retrieves a list of transactions with given pages, filters and order.

type WebpayResponse

type WebpayResponse struct {
	TransactionID  string    `json:"transaction_id"`
	RedirectURL    string    `json:"redirect_url"`
	ExpirationDate time.Time `json:"expiration_date"`
}

WebpayResponse struct holds the answer from qvo for a webpay transaction request.

func WebpayTransaction

func WebpayTransaction(c *Client, customerID, returnURL, description string, amount int64) (WebpayResponse, error)

WebpayTransaction begins a webpay transaction. If everything's ok, it'll return a transaction id (for later check), the redirect url to send the customer to, and the expiration date for this transaction.

type Withdrawal

type Withdrawal struct {
	ID        string    `json:"id"`
	Amount    int64     `json:"amount"`
	Status    string    `json:"status"` //One of: processing, rejected, transfered.
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

Withdrawal struct to represent a qvo withdraw object.

func CreateWithdrawal

func CreateWithdrawal(c *Client, amount int64) (Withdrawal, error)

CreateWithdrawal creates a withdrawal of the given amount. Return a Withdrawal object or an error.

func GetWithdrawal

func GetWithdrawal(c *Client, id string) (Withdrawal, error)

GetWithdrawal retrieves a withdrawal given its id.

func ListWithdrawals

func ListWithdrawals(c *Client, page, perPage int, where map[string]map[string]interface{}, orderBy string) ([]Withdrawal, error)

ListWithdrawals retrieves a list of withdrawals with given pages, filters and order.

Jump to

Keyboard shortcuts

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