diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea9c6c2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +cmd +.idea diff --git a/direct_billing.go b/direct_billing.go new file mode 100644 index 0000000..3abe8b5 --- /dev/null +++ b/direct_billing.go @@ -0,0 +1,98 @@ +package simpay + +import ( + "crypto" + "encoding/json" + "fmt" + "strings" +) + +type DirectBilling struct { + restClient +} + +func NewDirectBilling(apiKey, simPassword string) DirectBilling { + return DirectBilling{ + restClient: newRestClient(apiKey, simPassword), + } +} + +func (d DirectBilling) GetServiceList(page, limit uint) (DirectBillingServiceListResponse, error) { + endpoint := fmt.Sprintf("/directbilling?page=%v&limit=%v", page, limit) + response, err := d.restClient.sendGetRequest(endpoint) + if err != nil { + return DirectBillingServiceListResponse{}, err + } + var serviceList DirectBillingServiceListResponse + return serviceList, json.Unmarshal(response, &serviceList) +} +func (d DirectBilling) GetServiceDetails(serviceId uint) (DirectBillingServiceDetailsResponse, error) { + endpoint := fmt.Sprintf("/directbilling/%v", serviceId) + response, err := d.restClient.sendGetRequest(endpoint) + if err != nil { + return DirectBillingServiceDetailsResponse{}, err + } + var serviceDetails DirectBillingServiceDetailsResponse + return serviceDetails, json.Unmarshal(response, &serviceDetails) +} +func (d DirectBilling) CalculateCommission(serviceId uint, amount float32) (CalculateCommissionResponse, error) { + endpoint := fmt.Sprintf("/directbilling/%v/calculate?amount=%f", serviceId, amount) + response, err := d.restClient.sendGetRequest(endpoint) + if err != nil { + return CalculateCommissionResponse{}, err + } + var commissionResponse CalculateCommissionResponse + return commissionResponse, json.Unmarshal(response, &commissionResponse) +} + +func (d DirectBilling) GetTransactionList(serviceId, page, limit uint) (DirectBillingTransactionListResponse, error) { + endpoint := fmt.Sprintf("/directbilling/%v/transactions?page=%v&limit=%v", serviceId, page, limit) + response, err := d.restClient.sendGetRequest(endpoint) + if err != nil { + return DirectBillingTransactionListResponse{}, err + } + var transactionList DirectBillingTransactionListResponse + return transactionList, json.Unmarshal(response, &transactionList) +} +func (d DirectBilling) GetTransactionDetails(serviceId, transactionId uint) (DirectBillingTransactionDetailsResponse, error) { + endpoint := fmt.Sprintf("/directbilling/%v/transactions/%v", serviceId, transactionId) + response, err := d.restClient.sendGetRequest(endpoint) + if err != nil { + return DirectBillingTransactionDetailsResponse{}, err + } + var transactionDetails DirectBillingTransactionDetailsResponse + return transactionDetails, json.Unmarshal(response, &transactionDetails) +} +func (d DirectBilling) GenerateTransaction(serviceId uint, request GenerateTransactionRequest) (DirectBillingGenerateTransactionResponse, error) { + endpoint := fmt.Sprintf("/directbilling/%d/transactions", serviceId) + response, err := d.restClient.sendPostRequest(endpoint, request) + if err != nil { + return DirectBillingGenerateTransactionResponse{}, err + } + var transactionResponse DirectBillingGenerateTransactionResponse + return transactionResponse, json.Unmarshal(response, &transactionResponse) +} + +func CheckSignature(key, transactionJson string) bool { + var n DirectBillingTransactionNotification + err := json.Unmarshal([]byte(transactionJson), &n) + if err != nil { + return false + } + + fields := []string{ + fmt.Sprintf("%v", n.Id), + n.Status, + fmt.Sprintf("%f", n.Values.Net), + fmt.Sprintf("%f", n.Values.Gross), + fmt.Sprintf("%f", n.Values.Partner), + n.Returns.Complete, + n.Returns.Failure, + n.Number, + fmt.Sprintf("%v", n.Provider), + n.Signature, + key, + } + return string(crypto.SHA256.New().Sum([]byte(strings.Join(fields, "|")))) == n.Signature + +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7b31ed9 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/SimPaypl + +go 1.16 diff --git a/model.go b/model.go new file mode 100644 index 0000000..f6a0baf --- /dev/null +++ b/model.go @@ -0,0 +1,286 @@ +package simpay + +import ( + "crypto" + "fmt" + "strings" + "time" +) + +const dateTimeFormat = "2006-01-02T15:04:05-07:00" + +type Time struct { + time.Time +} + +func (t *Time) UnmarshalJSON(b []byte) error { + s := strings.Trim(string(b), "\"") + var err error + t.Time, err = time.Parse(dateTimeFormat, s) + return err +} + +func (t Time) MarshalJSON() ([]byte, error) { + return []byte(`"` + t.Format(dateTimeFormat) + `"`), nil +} + +type Response struct { + Success bool `json:"success"` + Errors map[string][]string `json:"errors"` +} + +type PaginatedResponse struct { + Response + Pagination struct { + Total int `json:"total"` + Count int `json:"count"` + PerPage int `json:"per_page"` + CurrentPage int `json:"current_page"` + TotalPages int `json:"total_pages"` + Links struct { + NextPage string `json:"next_page"` + PrevPage string `json:"prev_page"` + } `json:"links"` + } `json:"pagination"` +} + +type SmsServiceListResponse struct { + PaginatedResponse + ServiceList []struct { + Id int `json:"id"` + Type string `json:"type"` + Status string `json:"status"` + Name string `json:"name"` + Prefix string `json:"prefix"` + Suffix string `json:"suffix"` + Adult bool `json:"adult"` + CreatedAt Time `json:"created_at"` + } `json:"data"` +} + +type SmsServiceDetailsResponse struct { + Response + ServiceDetails struct { + Id int `json:"id"` + Type string `json:"type"` + Status string `json:"status"` + Name string `json:"name"` + Prefix string `json:"prefix"` + Suffix string `json:"suffix"` + Description string `json:"description"` + Adult bool `json:"adult"` + Numbers []string `json:"numbers"` + CreatedAt Time `json:"created_at"` + } `json:"data"` +} + +type SmsTransactionListResponse struct { + PaginatedResponse + TransactionList []struct { + Id int `json:"id"` + From int64 `json:"from"` + Code string `json:"code"` + Used bool `json:"used"` + SendAt Time `json:"send_at"` + } `json:"data"` +} + +type SmsTransactionDetailsResponse struct { + Response + TransactionDetails struct { + Id int `json:"id"` + From int64 `json:"from"` + Code string `json:"code"` + Used bool `json:"used"` + SendNumber int `json:"send_number"` + Value float64 `json:"value"` + SendAt Time `json:"send_at"` + } `json:"data"` +} + +type NumberDetails struct { + Number int `json:"number"` + Value float64 `json:"value"` + ValueGross float64 `json:"value_gross"` + Adult bool `json:"adult"` +} + +type ServiceNumberListResponse struct { + PaginatedResponse + NumberList []NumberDetails `json:"data"` +} + +type NumberDetailsResponse struct { + Response + NumberDetails NumberDetails `json:"data"` +} + +type NumberListResponse struct { + PaginatedResponse + NumberList []NumberDetails `json:"data"` +} + +type CodeVerifyRequest struct { + Code string + Number int64 +} + +type CodeVerificationResponse struct { + Success bool `json:"success"` + CodeVerification struct { + Used bool `json:"used"` + Code string `json:"code"` + Test bool `json:"test"` + From string `json:"from"` + Number int `json:"number"` + Value float64 `json:"value"` + UsedAt Time `json:"used_at"` + } `json:"data"` +} + +type DirectBillingServiceListResponse struct { + PaginatedResponse + ServiceList []struct { + Id int `json:"id"` + Name string `json:"name"` + Suffix string `json:"suffix"` + Status string `json:"status"` + CreatedAt Time `json:"created_at"` + } `json:"data"` +} + +type CommissionPercent struct { + Commission0 string `json:"commission_0"` + Commission9 string `json:"commission_9"` + Commission25 string `json:"commission_25"` +} + +type DirectBillingServiceDetailsResponse struct { + Response + ServiceDetails struct { + Id int `json:"id"` + Name string `json:"name"` + Suffix string `json:"suffix"` + Status string `json:"status"` + Api struct { + Complete string `json:"complete"` + Failure string `json:"failure"` + } `json:"api"` + Providers struct { + TMobile bool `json:"t-mobile"` + Orange bool `json:"orange"` + Play bool `json:"play"` + Plus bool `json:"plus"` + } `json:"providers"` + Commissions struct { + TMobile CommissionPercent `json:"t-mobile"` + Orange CommissionPercent `json:"orange"` + Play CommissionPercent `json:"play"` + Plus CommissionPercent `json:"plus"` + } `json:"commissions"` + MaxValues struct { + TMobile string `json:"t-mobile"` + Orange string `json:"orange"` + Play string `json:"play"` + Plus string `json:"plus"` + } `json:"maxValues"` + CreatedAt Time `json:"created_at"` + } `json:"data"` +} + +type CommissionValue struct { + Net float64 `json:"net"` + Gross float64 `json:"gross"` +} + +type CalculateCommissionResponse struct { + Response + CalculateCommission struct { + Orange CommissionValue `json:"orange"` + Play CommissionValue `json:"play"` + TMobile CommissionValue `json:"t-mobile"` + Plus CommissionValue `json:"plus"` + } `json:"data"` +} + +type DirectBillingTransactionListResponse struct { + PaginatedResponse + TransactionList []struct { + Id int `json:"id"` + Status string `json:"status"` + Value float64 `json:"value"` + ValueNetto float64 `json:"value_netto"` + Operator string `json:"operator"` + CreatedAt Time `json:"created_at"` + UpdatedAt Time `json:"updated_at"` + } `json:"data"` +} + +type DirectBillingTransactionDetailsResponse struct { + Response + TransactionDetails struct { + Id int `json:"id"` + Status string `json:"status"` + PhoneNumber interface{} `json:"phoneNumber"` + Control string `json:"control"` + Value float64 `json:"value"` + ValueNetto float64 `json:"value_netto"` + Operator string `json:"operator"` + Notify struct { + IsSend bool `json:"is_send"` + LastSendAt Time `json:"last_send_at"` + Count int `json:"count"` + } `json:"notify"` + CreatedAt Time `json:"created_at"` + UpdatedAt Time `json:"updated_at"` + } `json:"data"` +} + +type GenerateTransactionRequest struct { + Amount float64 `json:"amount"` + AmountType string `json:"amountType"` + Description string `json:"description"` + Control string `json:"control"` + Returns struct { + Success string `json:"success"` + Failure string `json:"failure"` + } `json:"returns"` + PhoneNumber string `json:"phoneNumber"` + Signature string `json:"signature"` +} + +func (r GenerateTransactionRequest) Sign(key string) { + fields := []string{fmt.Sprintf("%f", r.Amount), r.AmountType, r.Description, r.Control, r.Returns.Success, r.Returns.Failure, r.PhoneNumber, key} + r.Signature = string(crypto.SHA256.New().Sum([]byte(strings.Join(fields, "|")))) +} + +func (r GenerateTransactionRequest) SignWithAmountAndControl(key string) { + r.Signature = string(crypto.SHA256.New().Sum([]byte(strings.Join([]string{fmt.Sprintf("%f", r.Amount), r.Control, key}, "|")))) +} + +type DirectBillingGenerateTransactionResponse struct { + Response + Data struct { + TransactionId string `json:"transactionId"` + RedirectUrl string `json:"redirectUrl"` + } `json:"data"` +} + +type DirectBillingTransactionNotification struct { + Id int `json:"id"` + ServiceId int `json:"service_id"` + Status string `json:"status"` + Values struct { + Net float64 `json:"net"` + Gross float64 `json:"gross"` + Partner float64 `json:"partner"` + } `json:"values"` + Returns struct { + Complete string `json:"complete"` + Failure string `json:"failure"` + } `json:"returns"` + Control string `json:"control"` + Number string `json:"number"` + Provider int `json:"provider"` + Signature string `json:"Signature"` +} diff --git a/rest.go b/rest.go new file mode 100644 index 0000000..de2a1ff --- /dev/null +++ b/rest.go @@ -0,0 +1,65 @@ +package simpay + +import ( + "bytes" + "encoding/json" + "io/ioutil" + "net/http" +) + +const ( + SimKeyHeader = "X-SIM-KEY" + SimPasswordHeader = "X-SIM-PASSWORD" + BaseUrl = "https://api.simpay.pl" + ContentType = "application/json" +) + +type restClient struct { + http.Client +} + +func newRestClient(apiKey, simPassword string) restClient { + return restClient{http.Client{Transport: interceptor{apikey: apiKey, simKey: simPassword, core: http.DefaultTransport}}} +} +func (r restClient) sendGetRequest(endpoint string) ([]byte, error) { + resp, err := r.Get(BaseUrl + endpoint) + return extractBody(err, resp) +} + +func (r restClient) sendPostRequest(endpoint string, body interface{}) ([]byte, error) { + marshaledBody, err := json.Marshal(body) + + if err != nil { + return nil, err + } + + resp, err := r.Post(BaseUrl+endpoint, ContentType, bytes.NewBuffer(marshaledBody)) + return extractBody(err, resp) +} + +func extractBody(err error, resp *http.Response) ([]byte, error) { + if err != nil { + return nil, err + } + + defer resp.Body.Close() + byteResponse, err := ioutil.ReadAll(resp.Body) + + if err != nil { + return nil, err + } + + return byteResponse, nil +} + +type interceptor struct { + apikey string + simKey string + core http.RoundTripper +} + +func (i interceptor) RoundTrip(r *http.Request) (*http.Response, error) { + r.Header.Set(SimKeyHeader, i.apikey) + r.Header.Set(SimPasswordHeader, i.simKey) + return i.core.RoundTrip(r) +} diff --git a/sms.go b/sms.go new file mode 100644 index 0000000..3a27c1a --- /dev/null +++ b/sms.go @@ -0,0 +1,106 @@ +package simpay + +import ( + "encoding/json" + "fmt" +) + +type Sms struct { + restClient restClient +} + +func NewSms(apiKey, simPassword string) *Sms { + return &Sms{ + restClient: newRestClient(apiKey, simPassword), + } +} + +func (s Sms) GetServiceList(page, limit uint) (SmsServiceListResponse, error) { + endpoint := fmt.Sprintf("/sms?page=%v&limit=%v", page, limit) + response, err := s.restClient.sendGetRequest(endpoint) + if err != nil { + return SmsServiceListResponse{}, err + } + var serviceList SmsServiceListResponse + return serviceList, json.Unmarshal(response, &serviceList) +} + +func (s Sms) GetServiceDetails(serviceId uint) (SmsServiceDetailsResponse, error) { + endpoint := fmt.Sprintf("/sms/%v", serviceId) + response, err := s.restClient.sendGetRequest(endpoint) + if err != nil { + return SmsServiceDetailsResponse{}, err + } + var serviceDetails SmsServiceDetailsResponse + return serviceDetails, json.Unmarshal(response, &serviceDetails) +} + +func (s Sms) GetTransactions(serviceId, page, limit uint) (SmsTransactionListResponse, error) { + endpoint := fmt.Sprintf("/sms/%v/transactions?page=%v&limit=%v", serviceId, page, limit) + response, err := s.restClient.sendGetRequest(endpoint) + if err != nil { + return SmsTransactionListResponse{}, err + } + var transactionList SmsTransactionListResponse + return transactionList, json.Unmarshal(response, &transactionList) +} + +func (s Sms) GetTransactionDetails(serviceId, transactionId uint) (SmsTransactionDetailsResponse, error) { + endpoint := fmt.Sprintf("/sms/%v/transactions/%v", serviceId, transactionId) + response, err := s.restClient.sendGetRequest(endpoint) + if err != nil { + return SmsTransactionDetailsResponse{}, err + } + var transactionDetails SmsTransactionDetailsResponse + return transactionDetails, json.Unmarshal(response, &transactionDetails) +} + +func (s Sms) GetServiceNumbers(serviceId, page, limit uint) (NumberListResponse, error) { + endpoint := fmt.Sprintf("/sms/%v/numbers?page=%v&limit=%v", serviceId, page, limit) + response, err := s.restClient.sendGetRequest(endpoint) + if err != nil { + return NumberListResponse{}, err + } + var numberListResponse NumberListResponse + return numberListResponse, json.Unmarshal(response, &numberListResponse) +} + +func (s Sms) GetServiceNumberDetails(serviceId uint, number int64) (NumberDetailsResponse, error) { + endpoint := fmt.Sprintf("/sms/%v/numbers/%v", serviceId, number) + response, err := s.restClient.sendGetRequest(endpoint) + if err != nil { + return NumberDetailsResponse{}, err + } + var numberDetails NumberDetailsResponse + return numberDetails, json.Unmarshal(response, &numberDetails) +} + +func (s Sms) GetNumbers(page, limit uint) (NumberListResponse, error) { + endpoint := fmt.Sprintf("/sms/numbers?page=%v&limit=%v", page, limit) + response, err := s.restClient.sendGetRequest(endpoint) + if err != nil { + return NumberListResponse{}, err + } + var numberListResponse NumberListResponse + return numberListResponse, json.Unmarshal(response, &numberListResponse) +} + +func (s Sms) GetNumberDetails(number int64) (NumberDetailsResponse, error) { + endpoint := fmt.Sprintf("/sms/numbers/%v", number) + response, err := s.restClient.sendGetRequest(endpoint) + if err != nil { + return NumberDetailsResponse{}, err + } + var numberDetails NumberDetailsResponse + return numberDetails, json.Unmarshal(response, &numberDetails) +} + +func (s Sms) VerifyCode(serviceId uint, code string, number int64) (CodeVerificationResponse, error) { + endpoint := fmt.Sprintf("/sms/%v", serviceId) + response, err := s.restClient.sendPostRequest(endpoint, CodeVerifyRequest{Code: code, Number: number}) + if err != nil { + return CodeVerificationResponse{}, err + } + var verificationResponse CodeVerificationResponse + return verificationResponse, json.Unmarshal(response, &verificationResponse) +} diff --git a/sms_xml.go b/sms_xml.go new file mode 100644 index 0000000..04ea8cd --- /dev/null +++ b/sms_xml.go @@ -0,0 +1,85 @@ +package simpay + +import ( + "crypto" + "fmt" + "math/rand" + "strings" + "time" +) + +const charset = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789" + +var codes map[string]float64 + +func init() { + codes = make(map[string]float64) + codes["7055"] = 0.25 + codes["7136"] = 0.5 + codes["7255"] = 1.0 + codes["7355"] = 1.5 + codes["7455"] = 2.0 + codes["7555"] = 2.5 + codes["7636"] = 3.0 + codes["77464"] = 3.5 + codes["78464"] = 4.0 + codes["7936"] = 4.5 + codes["91055"] = 5.0 + codes["91155"] = 5.5 + codes["91455"] = 7.0 + codes["91664"] = 8.0 + codes["91955"] = 9.5 + codes["92055"] = 10.0 + codes["92555"] = 12.5 +} + +type SmsXml struct { + hashingKey string +} + +func NewSmsXml(hashingKey string) SmsXml { + return SmsXml{hashingKey: hashingKey} +} + +func (SmsXml) GenerateCode() string { + var sb strings.Builder + rand.Seed(time.Now().UnixNano()) + for i := 0; i < 6; i++ { + sb.WriteString(string(charset[rand.Intn(len(charset))])) + } + return sb.String() +} + +func (SmsXml) GenerateXml(code string) string { + return "" + code + "" +} + +func (s SmsXml) CheckParameters(m map[string]interface{}) bool { + params := []string{"send_number", "sms_text", "sms_from", "sms_id", "sign"} + + for _, param := range params { + if !contains(param, m) { + return false + } + } + + return m["sign"] == sign(m, s.hashingKey) +} + +func (SmsXml) GetNumberValue(number string) float64 { + return codes[number] +} + +func contains(v string, m map[string]interface{}) bool { + for s := range m { + if v == s { + return true + } + } + return false +} + +func sign(m map[string]interface{}, hashingKey string) string { + values := fmt.Sprintf("%v%v%v%v%v%v", m["sms_id"], m["sms_text"], m["sms_from"], m["send_number"], m["send_time"], hashingKey) + return string(crypto.SHA256.New().Sum([]byte(values))) +}