From 4c5fb21750a817b937798a64ec87156df0124782 Mon Sep 17 00:00:00 2001 From: b1ackd0t Date: Sun, 17 Jul 2022 16:58:55 +0000 Subject: [PATCH] Add webhooks support and mpesa Signed-off-by: GitHub --- README.md | 1 + pkg/auth.go | 11 +- pkg/kopokopo.go | 59 ++++++++- pkg/mpesa.go | 57 +++++++-- pkg/requests.go | 317 +++++++++++++++++++++++++++--------------------- pkg/webhooks.go | 192 ++++++++++++++++++++++++----- 6 files changed, 456 insertions(+), 181 deletions(-) diff --git a/README.md b/README.md index 9e255ae..d7fb562 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,3 @@ # kopokopo-go + Unofficial Golang SDK to connect to the Kopo Kopo API https://developers.kopokopo.com/ diff --git a/pkg/auth.go b/pkg/auth.go index 542e8f2..aa725e7 100644 --- a/pkg/auth.go +++ b/pkg/auth.go @@ -10,6 +10,7 @@ import ( var tokenEndpoint = "oauth" +// https://sandbox.kopokopo.com/oauth/token?grant_type=client_credentials&client_id={{CLIENT-ID}}&client_secret={{CLIENT-SECRET}} func (sdk kSDK) GetToken() (string, error) { q := url.Values{} q.Add("client_id", sdk.credentials.AppID) @@ -21,7 +22,7 @@ func (sdk kSDK) GetToken() (string, error) { if err != nil { return "", err } - resp, err := sdk.makeRequest(req, "") + resp, err := sdk.getBodyParams(req, "") if err != nil { return "", err } @@ -47,7 +48,7 @@ func (sdk kSDK) RevokeToken(token string) error { if err != nil { return err } - _, err = sdk.makeRequest(req, "") + _, err = sdk.getBodyParams(req, "") if err != nil { return err } @@ -62,13 +63,13 @@ func (sdk kSDK) TokenIntrospection(token string) (tokenIntrospectionResp, error) q.Add("client_id", sdk.credentials.AppID) q.Add("client_secret", sdk.credentials.Secret) q.Add("token", token) - endpoint := fmt.Sprintf("%s/%s/token/info?%s", sdk.baseURL, tokenEndpoint, q.Encode()) + endpoint := fmt.Sprintf("%s/%s/token/introspect?%s", sdk.baseURL, tokenEndpoint, q.Encode()) req, err := http.NewRequest(http.MethodPost, endpoint, nil) if err != nil { return tokenIntrospectionResp{}, err } - resp, err := sdk.makeRequest(req, "") + resp, err := sdk.getBodyParams(req, "") if err != nil { return tokenIntrospectionResp{}, err } @@ -90,7 +91,7 @@ func (sdk kSDK) TokenInformation(token string) (tokenInfo, error) { if err != nil { return tokenInfo{}, err } - resp, err := sdk.makeRequest(req, token) + resp, err := sdk.getBodyParams(req, token) if err != nil { return tokenInfo{}, err } diff --git a/pkg/kopokopo.go b/pkg/kopokopo.go index b2aaeb8..3c493a8 100644 --- a/pkg/kopokopo.go +++ b/pkg/kopokopo.go @@ -1,6 +1,7 @@ package kopokopo import ( + "errors" "io/ioutil" "net/http" "time" @@ -31,6 +32,40 @@ type SDK interface { // Shows details about the token used for authentication. TokenInformation(token string) (tokenInfo, error) + + // Create a webhook subscription + CreateWebhook(token string, webookReq CreateWebhookReq) (string, error) + + // Before processing webhook events, make sure that they originated from Kopo Kopo. + // Each request is signed with the api_key you got when creating an oauth application on the platform. + ValidateWebhook(webhookURL string) (BuyGoodsTrans, error) + + // Notifies your application when a Buygoods Transaction has been received. + C2BSubscription(webhookURL string) (BuyGoodsTrans, error) + + // Notifies your application when a B2b (External Till to Till transaction) has been received. + // These are payments recieved from other tills and not subscribers. + B2BSubscription(webhookURL string) (BuyGoodsTrans, error) + + // Notifies your application when another Kopo Kopo merchant transfers funds + // to your Kopo Kopo merchant account (Merchant to Merchant) + M2MSubscription(webhookURL string) (BuyGoodsTrans, error) + + // Notifies your application when a Buygoods Transaction has been reversed + C2BReversalSubscription(webhookURL string) (BuyGoodsTrans, error) + + // Settlement Transfer Completed + SettlementSub(webhookURL string) (BuyGoodsTrans, error) + + // Customer Created + CusomerCreationSub(webhookURL string) (CustomerReq, error) + + // Receive payments from M-PESA users via STK Push. + ReceiveMpesaPayment(token string, receiveMpesaReq ReceiveMpesaReq) (string, error) + + // With an Incoming Payment location url, you can query what the status of the Incoming Payment is. + // If a corresponding Incoming Payment Result exists, it will be bundled in the payload of the result. + QueryIncommingMpesaPayment(token, id string) (IncomingPaymentEvent, error) } // Credentials contains the credentials @@ -68,15 +103,23 @@ func NewSDK(conf Config) SDK { } } -func (sdk kSDK) makeRequest(req *http.Request, token string) ([]byte, error) { +func (sdk kSDK) makeRequest(req *http.Request, token string) (*http.Response, error) { if token != "" { - req.Header.Add("Authorization", "Basic "+token) + req.Header.Add("Authorization", "Bearer "+token) } req.Header.Add("Content-Type", "application/json") resp, err := sdk.client.Do(req) if err != nil { return nil, err } + return resp, nil +} + +func (sdk kSDK) getBodyParams(req *http.Request, token string) ([]byte, error) { + resp, err := sdk.makeRequest(req, token) + if err != nil { + return nil, err + } body, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, err @@ -84,3 +127,15 @@ func (sdk kSDK) makeRequest(req *http.Request, token string) ([]byte, error) { defer resp.Body.Close() return body, nil } + +func (sdk kSDK) getHeaderParams(req *http.Request, token string) (string, error) { + resp, err := sdk.makeRequest(req, token) + if err != nil { + return "", err + } + if resp.StatusCode != http.StatusCreated { + return "", errors.New("failed to created") + } + id := resp.Header.Get("Location") + return id, nil +} diff --git a/pkg/mpesa.go b/pkg/mpesa.go index e610de2..5a461f9 100644 --- a/pkg/mpesa.go +++ b/pkg/mpesa.go @@ -1,14 +1,57 @@ package kopokopo -// Receive payments from M-PESA users via STK Push. -// func (sdk kSDK) ReceiveMpesaPayment(grantType string) (string, error) { -// panic("Not implemented") -// } +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "net/http" + "strings" +) + +func (sdk kSDK) ReceiveMpesaPayment(token string, receiveMpesaReq ReceiveMpesaReq) (string, error) { + if err := receiveMpesaReq.Validate(); err != nil { + return "", err + } + data, err := json.Marshal(receiveMpesaReq) + if err != nil { + return "", err + } + endpoint := fmt.Sprintf("%s/api/v1/incoming_payments", sdk.baseURL) + req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewReader(data)) + if err != nil { + return "", err + } + url, err := sdk.getHeaderParams(req, token) + if err != nil { + return "", err + } + id := strings.TrimPrefix(url, fmt.Sprintf("%s/api/v1/incoming_payments/", sdk.baseURL)) + return id, nil +} // func (sdk kSDK) ProcessIncommingMpesaPayment(grantType string) (string, error) { // panic("Not implemented") // } -// func (sdk kSDK) QueryIncommingMpesaPayment(grantType string) (string, error) { -// panic("Not implemented") -// } +func (sdk kSDK) QueryIncommingMpesaPayment(token, id string) (IncomingPaymentEvent, error) { + if id == "" { + return IncomingPaymentEvent{}, errors.New("empty id") + } + + endpoint := fmt.Sprintf("%s/api/v1/incoming_payments/", sdk.baseURL) + + req, err := http.NewRequest(http.MethodGet, endpoint, nil) + if err != nil { + return IncomingPaymentEvent{}, err + } + resp, err := sdk.getBodyParams(req, token) + if err != nil { + return IncomingPaymentEvent{}, err + } + var ipe IncomingPaymentEvent + if err := json.Unmarshal(resp, &ipe); err != nil { + return ipe, err + } + return ipe, nil +} diff --git a/pkg/requests.go b/pkg/requests.go index 22f251e..48f8ffa 100644 --- a/pkg/requests.go +++ b/pkg/requests.go @@ -1,161 +1,206 @@ package kopokopo -// type CreateWebhookReq struct { -// EventType string `json:"event_type,omitempty"` //The type of event you are subscribing to -// URL string `json:"url,omitempty"` // The http end point to send the webhook. -// Scope string `json:"scope,omitempty"` // The scope of the webhook subscription. -// ScopeRef string `json:"scope_reference,omitempty"` -// } +import ( + "errors" + "strings" +) -// func (cwr CreateWebhookReq) Validate() error { -// if cwr.EventType != "buygoods_transaction_received" && -// cwr.EventType != "buygoods_transaction_reversed" && -// cwr.EventType != "b2b_transaction_received" && -// cwr.EventType != "m2m_transaction_received" && -// cwr.EventType != "settlement_transfer_completed" && -// cwr.EventType != "customer_created" { -// return errors.New("invalid event type") -// } -// if !strings.HasPrefix(cwr.URL, "https") { -// return errors.New("URL is not secured with TLS") -// } -// if cwr.Scope != "company" && cwr.Scope != "till" { -// return errors.New("invalid scope") -// } -// if cwr.Scope == "till" && cwr.ScopeRef == "" { -// return errors.New("scope reference is required") -// } -// return nil -// } +// CreateWebhookReq struct +type CreateWebhookReq struct { + EventType string `json:"event_type,omitempty"` //The type of event you are subscribing to + URL string `json:"url,omitempty"` // The http end point to send the webhook. + Scope string `json:"scope,omitempty"` // The scope of the webhook subscription. + ScopeRef string `json:"scope_reference,omitempty"` +} -// type Destination struct { -// Type string `json:"type,omitempty"` -// Resource DestinationResource `json:"resource,omitempty"` -// } +// Validate returns nil if the struct is valid +func (cwr CreateWebhookReq) Validate() error { + if cwr.EventType != "buygoods_transaction_received" && + cwr.EventType != "buygoods_transaction_reversed" && + cwr.EventType != "b2b_transaction_received" && + cwr.EventType != "m2m_transaction_received" && + cwr.EventType != "settlement_transfer_completed" && + cwr.EventType != "customer_created" { + return errors.New("invalid event type") + } + if !strings.HasPrefix(cwr.URL, "https") { + return errors.New("URL is not secured with TLS") + } + if cwr.Scope != "company" && cwr.Scope != "till" { + return errors.New("invalid scope") + } + if cwr.Scope == "till" && cwr.ScopeRef == "" { + return errors.New("scope reference is required") + } + return nil +} -// type DestinationResource struct { -// Reference string `json:"reference,omitempty"` // The destination reference -// AccountName string `json:"account_name,omitempty"` // The name as indicated on the bank account -// AccountNumber string `json:"account_number,omitempty"` // The bank account number -// BankBranchReference string `json:"bank_branch_ref,omitempty"` // An identifier identifying the destination bank branch -// SettlementMethod string `json:"settlement_method,omitempty"` // EFT or RTS -// FirstName string `json:"first_name,omitempty"` // String First name of the recipient -// LastName string `json:"last_name,omitempty"` // Last name of recipient -// Email string `json:"email,omitempty"` // Email of recipient -// PhoneNumber string `json:"phone_number,omitempty"` // Phone number -// Network string `json:"network,omitempty"` // The mobile network to which the phone number belongs -// } +// Destination struct +type Destination struct { + Type string `json:"type,omitempty"` + Resource DestinationResource `json:"resource,omitempty"` +} -// type Disbursements struct { -// Status string `json:"status,omitempty"` // The status of the disbursement -// Amount string `json:"amount,omitempty"` // The amount of the disbursement -// OriginationTime string `json:"origination_time,omitempty"` // The Timestamp of when the transaction took place -// TransactionalReference string `json:"transactional_reference,omitempty"` // The reference from the transaction. i.e mpesa reference It is null for eft transactions -// } +// DestinationResource struct +type DestinationResource struct { + Reference string `json:"reference,omitempty"` // The destination reference + AccountName string `json:"account_name,omitempty"` // The name as indicated on the bank account + AccountNumber string `json:"account_number,omitempty"` // The bank account number + BankBranchReference string `json:"bank_branch_ref,omitempty"` // An identifier identifying the destination bank branch + SettlementMethod string `json:"settlement_method,omitempty"` // EFT or RTS + FirstName string `json:"first_name,omitempty"` // String First name of the recipient + LastName string `json:"last_name,omitempty"` // Last name of recipient + Email string `json:"email,omitempty"` // Email of recipient + PhoneNumber string `json:"phone_number,omitempty"` // Phone number + Network string `json:"network,omitempty"` // The mobile network to which the phone number belongs +} -// type Resource struct { -// ID string `json:"id,omitempty"` // The api reference of the transaction -// Amount float64 `json:"amount,omitempty"` // The amount of the transaction -// Status string `json:"status,omitempty"` // The status of the transaction -// System string `json:"system,omitempty"` // The mobile money system -// Currency string `json:"currency,omitempty"` // Currency -// Reference string `json:"reference,omitempty"` // The mpesa reference -// TransactionalReference string `json:"transactional_reference,omitempty"` -// TillNumber string `json:"till_number,omitempty"` // The till number to which the payment was made -// SendingTill string `json:"sending_till,omitempty"` // The till number of the sender -// AccountName string `json:"account_name,omitempty"` -// AccountNumber string `json:"account_number,omitempty"` -// BankBranchReference string `json:"bank_branch_ref,omitempty"` -// SettlementMethod string `json:"settlement_method,omitempty"` -// SendingMerchant string `json:"sending_merchant,omitempty"` // Name of merchant -// SenderPhoneNumber string `json:"sender_phone_number,omitempty"` // The phone number that sent the payment -// OriginationTime string `json:"origination_time,omitempty"` // The transaction timestamp -// SenderLastName string `json:"sender_last_name,omitempty"` // Last name of payer -// SenderFirstName string `json:"sender_first_name,omitempty"` // First name of payer -// SenderMiddleName string `json:"sender_middle_name,omitempty"` // Middle name of payer -// Destination Destination `json:"destination,omitempty"` // The destination of the settlement transfer -// Disbursements []Disbursements `json:"disbursements,omitempty"` // These are the disbursements in that particular transfer batch -// } +// Disbursements struct +type Disbursements struct { + Status string `json:"status,omitempty"` // The status of the disbursement + Amount string `json:"amount,omitempty"` // The amount of the disbursement + OriginationTime string `json:"origination_time,omitempty"` // The Timestamp of when the transaction took place + TransactionalReference string `json:"transactional_reference,omitempty"` // The reference from the transaction. i.e mpesa reference It is null for eft transactions +} -// type Event struct { -// Type string `json:"type,omitempty"` // The type of transaction -// Resource Resource `json:"resource,omitempty"` // The resource corresponding to the event. -// } +// Resource struct +type Resource struct { + ID string `json:"id,omitempty"` // The api reference of the transaction + Amount float64 `json:"amount,omitempty"` // The amount of the transaction + Status string `json:"status,omitempty"` // The status of the transaction + System string `json:"system,omitempty"` // The mobile money system + Currency string `json:"currency,omitempty"` // Currency + Reference string `json:"reference,omitempty"` // The mpesa reference + TransactionalReference string `json:"transactional_reference,omitempty"` + TillNumber string `json:"till_number,omitempty"` // The till number to which the payment was made + SendingTill string `json:"sending_till,omitempty"` // The till number of the sender + AccountName string `json:"account_name,omitempty"` + AccountNumber string `json:"account_number,omitempty"` + BankBranchReference string `json:"bank_branch_ref,omitempty"` + SettlementMethod string `json:"settlement_method,omitempty"` + SendingMerchant string `json:"sending_merchant,omitempty"` // Name of merchant + SenderPhoneNumber string `json:"sender_phone_number,omitempty"` // The phone number that sent the payment + OriginationTime string `json:"origination_time,omitempty"` // The transaction timestamp + SenderLastName string `json:"sender_last_name,omitempty"` // Last name of payer + SenderFirstName string `json:"sender_first_name,omitempty"` // First name of payer + SenderMiddleName string `json:"sender_middle_name,omitempty"` // Middle name of payer + Destination Destination `json:"destination,omitempty"` // The destination of the settlement transfer + Disbursements []Disbursements `json:"disbursements,omitempty"` // These are the disbursements in that particular transfer batch +} -// type Links struct { -// Self string `json:"self,omitempty"` -// Resource string `json:"resource,omitempty"` -// Callback string `json:"callback_url,omitempty"` -// } +// Event struct +type Event struct { + Type string `json:"type,omitempty"` // The type of transaction + Resource Resource `json:"resource,omitempty"` // The resource corresponding to the event. +} -// type BuyGoodsTrans struct { -// Topic string `json:"topic,omitempty"` // The ID of the Webhook Event -// ID string `json:"id,omitempty"` // The topic of the webhook. -// CreatedAt string `json:"created_at,omitempty"` // The timestamp of when the webhook event was created. -// Event Event `json:"event,omitempty"` -// Links Links `json:"_links,omitempty"` // A JSON object containing links to the Webhook Event and the corresponding Buygoods Transaction resource -// } +// Links struct +type Links struct { + Self string `json:"self,omitempty"` + Resource string `json:"resource,omitempty"` + Callback string `json:"callback_url,omitempty"` +} -// func (bgt BuyGoodsTrans) Validate() error { -// return nil -// } +// BuyGoodsTrans struct +type BuyGoodsTrans struct { + Topic string `json:"topic,omitempty"` // The ID of the Webhook Event + ID string `json:"id,omitempty"` // The topic of the webhook. + CreatedAt string `json:"created_at,omitempty"` // The timestamp of when the webhook event was created. + Event Event `json:"event,omitempty"` + Links Links `json:"_links,omitempty"` // A JSON object containing links to the Webhook Event and the corresponding Buygoods Transaction resource +} -// type CustomerResource struct { -// LastName string `json:"last_name,omitempty"` // Last name of payer -// FirstName string `json:"first_name,omitempty"` // First name of payer -// MiddleName string `json:"middle_name,omitempty"` // Middle name of payer -// PhoneNumber string `json:"phone_number,omitempty"` // The phone number that sent the payment -// } -// type CustomerEvent struct { -// Type string `json:"type,omitempty"` // The type of record (Mobile Money User) -// Resource CustomerResource `json:"resource,omitempty"` // The resource corresponding to the event. -// } +// Validate returns nil if the struct is valid +func (bgt BuyGoodsTrans) Validate() error { + return nil +} -// type CustomerReq struct { -// Topic string `json:"topic,omitempty"` // The ID of the Webhook Event -// ID string `json:"id,omitempty"` // The topic of the webhook. -// CreatedAt string `json:"created_at,omitempty"` // The timestamp of when the webhook event was created. -// Event CustomerEvent `json:"event,omitempty"` -// Links Links `json:"_links,omitempty"` // A JSON object containing links to the Webhook Event and the corresponding Buygoods Transaction resource -// } +// CustomerResource struct +type CustomerResource struct { + LastName string `json:"last_name,omitempty"` // Last name of payer + FirstName string `json:"first_name,omitempty"` // First name of payer + MiddleName string `json:"middle_name,omitempty"` // Middle name of payer + PhoneNumber string `json:"phone_number,omitempty"` // The phone number that sent the payment +} -// type Subscriber struct { -// LastName string `json:"last_name,omitempty"` // Last name of the subscriber -// FirstName string `json:"first_name,omitempty"` // First name of the subscriber -// MiddleName string `json:"middle_name,omitempty"` // Middle name of the subscriber -// PhoneNumber string `json:"phone_number,omitempty"` // The phone number of the subscriber from which the payment will be made -// Email string `json:"email,omitempty"` // E-mail address of the subscriber - optional -// } +// CustomerEvent struct +type CustomerEvent struct { + Type string `json:"type,omitempty"` // The type of record (Mobile Money User) + Resource CustomerResource `json:"resource,omitempty"` // The resource corresponding to the event. +} -// type Amount struct { -// Value string `json:"value,omitempty"` // The amount of the transaction -// Currency string `json:"currency,omitempty"` // Currency -// } -// type ReceiveMpesaReq struct { -// PaymentChannel string `json:"payment_channel,omitempty"` // The payment channel to be used eg. M-PESA -// TillNumber string `json:"till_number,omitempty"` // The online payments till number from the Kopo Kopo dashboard to which the payment will be made -// Subscriber Subscriber `json:"subscriber,omitempty"` // A Subscriber JSON object see below -// Amount Amount `json:"amount,omitempty"` // An Amount JSON object containing currency and amount -// Metadata map[string]interface{} `json:"metadata,omitempty"` // An optional JSON object containing a maximum of 5 key value pairs -// Links Links `json:"_links,omitempty"` // A JOSN object containing the call back URL where the result of the Incoming Payment will be posted -// } +// CustomerReq struct +type CustomerReq struct { + Topic string `json:"topic,omitempty"` // The ID of the Webhook Event + ID string `json:"id,omitempty"` // The topic of the webhook. + CreatedAt string `json:"created_at,omitempty"` // The timestamp of when the webhook event was created. + Event CustomerEvent `json:"event,omitempty"` + Links Links `json:"_links,omitempty"` // A JSON object containing links to the Webhook Event and the corresponding Buygoods Transaction resource +} + +// Subscriber struct +type Subscriber struct { + LastName string `json:"last_name,omitempty"` // Last name of the subscriber + FirstName string `json:"first_name,omitempty"` // First name of the subscriber + MiddleName string `json:"middle_name,omitempty"` // Middle name of the subscriber + PhoneNumber string `json:"phone_number,omitempty"` // The phone number of the subscriber from which the payment will be made + Email string `json:"email,omitempty"` // E-mail address of the subscriber - optional +} + +// Amount struct +type Amount struct { + Value string `json:"value,omitempty"` // The amount of the transaction + Currency string `json:"currency,omitempty"` // Currency +} + +// ReceiveMpesaReq struct +type ReceiveMpesaReq struct { + PaymentChannel string `json:"payment_channel,omitempty"` // The payment channel to be used eg. M-PESA + TillNumber string `json:"till_number,omitempty"` // The online payments till number from the Kopo Kopo dashboard to which the payment will be made + Subscriber Subscriber `json:"subscriber,omitempty"` // A Subscriber JSON object see below + Amount Amount `json:"amount,omitempty"` // An Amount JSON object containing currency and amount + Metadata map[string]interface{} `json:"metadata,omitempty"` // An optional JSON object containing a maximum of 5 key value pairs + Links Links `json:"_links,omitempty"` // A JOSN object containing the call back URL where the result of the Incoming Payment will be posted +} + +// Validate returns nil if the struct is valid +func (rmr ReceiveMpesaReq) Validate() error { + return nil +} + +// IncomingPaymentEvent struct +type IncomingPaymentEvent struct { + Type string `json:"type,omitempty"` // The ID of the Webhook Event + ID string `json:"id,omitempty"` // The topic of the webhook. + Attributes Attribute `json:"attributes,omitempty"` +} + +// Attribute struct +type Attribute struct { + InitiationTime string `json:"initiation_time,omitempty"` // The timestamp of when the webhook event was created. + Status string `json:"status,omitempty"` // A status string denoting the status of the Incoming Payment + Resource Resource `json:"resource,omitempty"` // A JSON Object encapsulating the event of the request + Metadata map[string]interface{} `json:"metadata,omitempty"` // An optional JSON object containing a maximum of 5 key value pairs + Links Links `json:"_links,omitempty"` // A JSON object containing links to the Webhook Event and the corresponding Buygoods Transaction resource + +} // type IncomingPaymentEvent struct { // Type string `json:"type,omitempty"` // The type of record (Mobile Money User) // Resource Resource `json:"resource,omitempty"` // The resource corresponding to the event. // Errors string `json:"errors,omitempty"` // A string containing information on the error than occured - // } -// type ProcessIncommingPaymentReq struct { -// Topic string `json:"topic,omitempty"` // The topic of the request. -// ID string `json:"id,omitempty"` // The ID of the Incoming Payment -// InitiationTime string `json:"initiation_time,omitempty"` // The timestamp of when the webhook event was created. -// Status string `json:"status,omitempty"` // A status string denoting the status of the Incoming Payment -// Event IncomingPaymentEvent `json:"event,omitempty"` // A JSON Object encapsulating the event of the request -// Metadata map[string]interface{} `json:"metadata,omitempty"` // An optional JSON object containing a maximum of 5 key value pairs -// Links Links `json:"_links,omitempty"` // A JSON object containing links to the Webhook Event and the corresponding Buygoods Transaction resource -// } +// ProcessIncommingPaymentReq struct +type ProcessIncommingPaymentReq struct { + Topic string `json:"topic,omitempty"` // The topic of the request. + ID string `json:"id,omitempty"` // The ID of the Incoming Payment + InitiationTime string `json:"initiation_time,omitempty"` // The timestamp of when the webhook event was created. + Status string `json:"status,omitempty"` // A status string denoting the status of the Incoming Payment + Event IncomingPaymentEvent `json:"event,omitempty"` // A JSON Object encapsulating the event of the request + Metadata map[string]interface{} `json:"metadata,omitempty"` // An optional JSON object containing a maximum of 5 key value pairs + Links Links `json:"_links,omitempty"` // A JSON object containing links to the Webhook Event and the corresponding Buygoods Transaction resource +} // type PaymentRecipient struct { // LastName string `json:"last_name,omitempty"` // Last name of the recipient diff --git a/pkg/webhooks.go b/pkg/webhooks.go index 57e54e3..f1325b0 100644 --- a/pkg/webhooks.go +++ b/pkg/webhooks.go @@ -1,40 +1,170 @@ +// Package kopokopo Webhooks are a means of getting notified of events in the Kopo Kopo application. +// To receive webhooks, you need to create a webhook subscription. package kopokopo -// func (sdk kSDK) CreateWebhook(webookReq CreateWebhookReq) (string, error) { -// panic("Not implemented") -// } +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "strings" +) -// // Before processing webhook events, make sure that they originated from Kopo Kopo -// func (sdk kSDK) ValidateWebhook() error { -// panic("Not implemented") -// } +var webhookSubsEndpoint = "api/v1/webhook_subscriptions" -// // Notifies your application when a Buygoods Transaction has been received. -// func (sdk kSDK) C2BSubscription(buyGoodsReq BuyGoodsTrans) (string, error) { -// panic("Not implemented") -// } +func (sdk kSDK) CreateWebhook(token string, webookReq CreateWebhookReq) (string, error) { + if err := webookReq.Validate(); err != nil { + return "", err + } + endpoint := fmt.Sprintf("%s/%s", sdk.baseURL, webhookSubsEndpoint) + req, err := http.NewRequest(http.MethodPost, endpoint, nil) + if err != nil { + return "", err + } + url, err := sdk.getHeaderParams(req, token) + if err != nil { + return "", err + } + id := strings.TrimPrefix(url, fmt.Sprintf("%s/%s/", sdk.baseURL, webhookSubsEndpoint)) + return id, nil +} -// // Notifies your application when a B2b (External Till to Till transaction) has been received. -// // These are payments recieved from other tills and not subscribers. -// func (sdk kSDK) B2BSubscription(b2bReq BuyGoodsTrans) (string, error) { -// panic("Not implemented") -// } +func (sdk kSDK) ValidateWebhook(webhookURL string) (BuyGoodsTrans, error) { + if webhookURL == "" { + return BuyGoodsTrans{}, errors.New("empty webhook url") + } + req, err := http.NewRequest(http.MethodPost, webhookURL, nil) + if err != nil { + return BuyGoodsTrans{}, err + } + req.Header.Add("X-KopoKopo-Signature", sdk.credentials.APIKey) + resp, err := sdk.getBodyParams(req, "") + if err != nil { + return BuyGoodsTrans{}, err + } + var bgt BuyGoodsTrans + if err := json.Unmarshal(resp, &bgt); err != nil { + return BuyGoodsTrans{}, err + } + return bgt, nil +} -// // Notifies your application when another Kopo Kopo merchant transfers funds to your Kopo Kopo merchant account (Merchant to Merchant) -// func (sdk kSDK) M2MSubscription(m2mReq BuyGoodsTrans) (string, error) { -// panic("Not implemented") -// } +func (sdk kSDK) C2BSubscription(webhookURL string) (BuyGoodsTrans, error) { + if webhookURL == "" { + return BuyGoodsTrans{}, errors.New("empty webhook url") + } + req, err := http.NewRequest(http.MethodPost, webhookURL, nil) + if err != nil { + return BuyGoodsTrans{}, err + } + req.Header.Add("X-KopoKopo-Signature", sdk.credentials.APIKey) + resp, err := sdk.getBodyParams(req, "") + if err != nil { + return BuyGoodsTrans{}, err + } + var bgt BuyGoodsTrans + if err := json.Unmarshal(resp, &bgt); err != nil { + return BuyGoodsTrans{}, err + } + return bgt, nil +} -// // Notifies your application when a Buygoods Transaction has been reversed -// func (sdk kSDK) C2BReversalSubscription(c2bReq BuyGoodsTrans) error { -// panic("Not implemented") -// } +func (sdk kSDK) B2BSubscription(webhookURL string) (BuyGoodsTrans, error) { + if webhookURL == "" { + return BuyGoodsTrans{}, errors.New("empty webhook url") + } + req, err := http.NewRequest(http.MethodPost, webhookURL, nil) + if err != nil { + return BuyGoodsTrans{}, err + } + req.Header.Add("X-KopoKopo-Signature", sdk.credentials.APIKey) + resp, err := sdk.getBodyParams(req, "") + if err != nil { + return BuyGoodsTrans{}, err + } + var bgt BuyGoodsTrans + if err := json.Unmarshal(resp, &bgt); err != nil { + return BuyGoodsTrans{}, err + } + return bgt, nil +} -// // Parameters contained in a settlement_transfer_completed webhook -// func (sdk kSDK) SettlementSub(setReq BuyGoodsTrans) error { -// panic("Not implemented") -// } +func (sdk kSDK) M2MSubscription(webhookURL string) (BuyGoodsTrans, error) { + if webhookURL == "" { + return BuyGoodsTrans{}, errors.New("empty webhook url") + } + req, err := http.NewRequest(http.MethodPost, webhookURL, nil) + if err != nil { + return BuyGoodsTrans{}, err + } + req.Header.Add("X-KopoKopo-Signature", sdk.credentials.APIKey) + resp, err := sdk.getBodyParams(req, "") + if err != nil { + return BuyGoodsTrans{}, err + } + var bgt BuyGoodsTrans + if err := json.Unmarshal(resp, &bgt); err != nil { + return BuyGoodsTrans{}, err + } + return bgt, nil +} -// func (sdk kSDK) CusomerCreationSub(cReq CustomerReq) error { -// panic("Not implemented") -// } +func (sdk kSDK) C2BReversalSubscription(webhookURL string) (BuyGoodsTrans, error) { + if webhookURL == "" { + return BuyGoodsTrans{}, errors.New("empty webhook url") + } + req, err := http.NewRequest(http.MethodPost, webhookURL, nil) + if err != nil { + return BuyGoodsTrans{}, err + } + req.Header.Add("X-KopoKopo-Signature", sdk.credentials.APIKey) + resp, err := sdk.getBodyParams(req, "") + if err != nil { + return BuyGoodsTrans{}, err + } + var bgt BuyGoodsTrans + if err := json.Unmarshal(resp, &bgt); err != nil { + return BuyGoodsTrans{}, err + } + return bgt, nil +} + +func (sdk kSDK) SettlementSub(webhookURL string) (BuyGoodsTrans, error) { + if webhookURL == "" { + return BuyGoodsTrans{}, errors.New("empty webhook url") + } + req, err := http.NewRequest(http.MethodPost, webhookURL, nil) + if err != nil { + return BuyGoodsTrans{}, err + } + req.Header.Add("X-KopoKopo-Signature", sdk.credentials.APIKey) + resp, err := sdk.getBodyParams(req, "") + if err != nil { + return BuyGoodsTrans{}, err + } + var bgt BuyGoodsTrans + if err := json.Unmarshal(resp, &bgt); err != nil { + return BuyGoodsTrans{}, err + } + return bgt, nil +} + +func (sdk kSDK) CusomerCreationSub(webhookURL string) (CustomerReq, error) { + if webhookURL == "" { + return CustomerReq{}, errors.New("empty webhook url") + } + req, err := http.NewRequest(http.MethodPost, webhookURL, nil) + if err != nil { + return CustomerReq{}, err + } + req.Header.Add("X-KopoKopo-Signature", sdk.credentials.APIKey) + resp, err := sdk.getBodyParams(req, "") + if err != nil { + return CustomerReq{}, err + } + var cr CustomerReq + if err := json.Unmarshal(resp, &cr); err != nil { + return CustomerReq{}, err + } + return cr, nil +}