diff --git a/client_test.go b/client_test.go index a75e341..be5ce29 100644 --- a/client_test.go +++ b/client_test.go @@ -22,7 +22,7 @@ func TestClient_Token(t *testing.T) { // Arrange requests := make([]http.Request, 0) - server := helpers.MakeRequestCapturingTestServer(http.StatusOK, [][]byte{stubs.TokenResponse()}, &requests) + server := helpers.MakeRequestCapturingTestServer([]int{http.StatusOK}, [][]byte{stubs.TokenResponse()}, &requests) client := New( WithBaseURL(server.URL), diff --git a/internal/helpers/test_helper.go b/internal/helpers/test_helper.go index 6a45bab..2d26b51 100644 --- a/internal/helpers/test_helper.go +++ b/internal/helpers/test_helper.go @@ -20,7 +20,7 @@ func MakeTestServer(responseCode int, body []byte) *httptest.Server { } // MakeRequestCapturingTestServer creates an api server that captures the request object -func MakeRequestCapturingTestServer(responseCode int, responses [][]byte, requests *[]http.Request) *httptest.Server { +func MakeRequestCapturingTestServer(responseCodes []int, responses [][]byte, requests *[]http.Request) *httptest.Server { index := 0 return httptest.NewServer(http.HandlerFunc(func(responseWriter http.ResponseWriter, req *http.Request) { clonedRequest := req.Clone(context.Background()) @@ -35,7 +35,7 @@ func MakeRequestCapturingTestServer(responseCode int, responses [][]byte, reques *requests = append(*requests, *clonedRequest) - responseWriter.WriteHeader(responseCode) + responseWriter.WriteHeader(responseCodes[index]) _, err = responseWriter.Write(responses[index]) index++ if err != nil { diff --git a/internal/stubs/merchant_payment.go b/internal/stubs/merchant_payment.go index 7734eba..9584eb7 100644 --- a/internal/stubs/merchant_payment.go +++ b/internal/stubs/merchant_payment.go @@ -11,3 +11,133 @@ func MerchantPaymentInitResponse() []byte { } `) } + +// MerchantPaymentPayResponseWithInsufficientFunds is the response when the user has insufficient funds +func MerchantPaymentPayResponseWithInsufficientFunds() []byte { + return []byte(` +{ + "message":"60019 :: Le solde du compte du payeur est insuffisant", + "data":{ + "id":48462449, + "createtime":"1670442106", + "subscriberMsisdn":"69XXXXXXX", + "amount":100, + "payToken":"MP22120771FEB7B21FD2381C3786", + "txnid":null, + "txnmode":"12345", + "inittxnmessage":"Le solde du compte du payeur est insuffisant", + "inittxnstatus":"60019", + "confirmtxnstatus":null, + "confirmtxnmessage":null, + "status":"FAILED", + "notifUrl":"https://example.com/payment-notification", + "description":"Payment Description", + "channelUserMsisdn":"69XXXXXXX" + } +} +`) +} + +// MerchantPaymentPayResponse is the response after executing a payment +func MerchantPaymentPayResponse() []byte { + return []byte(` +{ + "message":"Merchant payment successfully initiated", + "data":{ + "id":48463325, + "createtime":"1670442691", + "subscriberMsisdn":"69XXXXXXX", + "amount":100, + "payToken":"MP22120771FEB7B21FD2381C3786", + "txnid":"MP221207.2051.B56929", + "txnmode":"12345", + "inittxnmessage":"Paiement e la clientele done.The devrez confirmer le paiement en saisissant son code PIN et vous recevrez alors un SMS. Merci dutiliser des services Orange Money.", + "inittxnstatus":"200", + "confirmtxnstatus":null, + "confirmtxnmessage":null, + "status":"PENDING", + "notifUrl":"https://example.com/payment-notification", + "description":"Payment Description", + "channelUserMsisdn":"69XXXXXXX" + } +} +`) +} + +// MerchantPaymentPushResponse is the response after sending a push notification +func MerchantPaymentPushResponse() []byte { + return []byte(` +{ + "message":"Push sent to customer", + "data":{ + "id":48463325, + "createtime":"1670442691", + "subscriberMsisdn":"69XXXXXXX", + "amount":100, + "payToken":"MP22120771FEB7B21FD2381C3786", + "txnid":"MP221207.2051.B56929", + "txnmode":"12345", + "inittxnmessage":"Paiement e la clientele done.The devrez confirmer le paiement en saisissant son code PIN et vous recevrez alors un SMS. Merci dutiliser des services Orange Money.", + "inittxnstatus":"200", + "confirmtxnstatus":null, + "confirmtxnmessage":null, + "status":"PENDING", + "notifUrl":"https://example.com/payment-notification", + "description":"Payment Description", + "channelUserMsisdn":"69XXXXXXX" + } +} +`) +} + +// MerchantPaymentTransactionStatusResponse is the transaction status response for a confirmed payment +func MerchantPaymentTransactionStatusResponse() []byte { + return []byte(` +{ + "message":"Transaction retrieved successfully", + "data":{ + "id":48463325, + "createtime":"1670442691", + "subscriberMsisdn":"69XXXXXXX", + "amount":100, + "payToken":"MP22120771FEB7B21FD2381C3786", + "txnid":"MP221207.2051.B56929", + "txnmode":"12345", + "inittxnmessage":"Paiement e la clientele done.The devrez confirmer le paiement en saisissant son code PIN et vous recevrez alors un SMS. Merci dutiliser des services Orange Money.", + "inittxnstatus":"200", + "confirmtxnstatus":"200", + "confirmtxnmessage":"Successful Payment of COMPANY_NAME from 69XXXXXXX CUSTOMER_NAME. Transaction ID:MP221207.2051.B56929, Amount:100, New balance:1103.5.", + "status":"SUCCESSFULL", + "notifUrl":"https://example.com/payment-notification", + "description":"Payment Description", + "channelUserMsisdn":"69XXXXXXX" + } +} +`) +} + +// MerchantPaymentTransactionStatusResponseWithExpired is the transaction status response for an expired payment +func MerchantPaymentTransactionStatusResponseWithExpired() []byte { + return []byte(` +{ + "message":"Transaction retrieved successfully", + "data":{ + "id":48445436, + "createtime":"1670436067", + "subscriberMsisdn":null, + "amount":null, + "payToken":"MP22120771FEB7B21FD2381C3786", + "txnid":null, + "txnmode":null, + "inittxnmessage":null, + "inittxnstatus":null, + "confirmtxnstatus":null, + "confirmtxnmessage":null, + "status":"EXPIRED", + "notifUrl":null, + "description":null, + "channelUserMsisdn":null + } +} +`) +} diff --git a/merchant_payment.go b/merchant_payment.go index b548c78..dbf1239 100644 --- a/merchant_payment.go +++ b/merchant_payment.go @@ -30,3 +30,20 @@ type MerchantPaymentTransaction struct { Description string `json:"description"` ChannelUserMSISDN string `json:"channelUserMsisdn"` } + +// IsExpired checks if a transaction is expired +func (transaction *MerchantPaymentTransaction) IsExpired() bool { + return transaction.Status == "EXPIRED" +} + +// IsPending checks if a transaction is pending +func (transaction *MerchantPaymentTransaction) IsPending() bool { + return transaction.Status == "PENDING" +} + +// IsConfirmed checks if a transaction is confirmed by the user +func (transaction *MerchantPaymentTransaction) IsConfirmed() bool { + return transaction.Status == "SUCCESSFULL" && + transaction.ConfirmTransactionStatus != nil && + *transaction.ConfirmTransactionStatus == "200" +} diff --git a/merchant_payment_service_test.go b/merchant_payment_service_test.go index 7bab0d3..d1e8a37 100644 --- a/merchant_payment_service_test.go +++ b/merchant_payment_service_test.go @@ -3,6 +3,7 @@ package orangemoney import ( "context" "net/http" + "strings" "testing" "github.com/NdoleStudio/orangemoney-go/internal/helpers" @@ -17,7 +18,7 @@ func TestMerchantPaymentService_Init(t *testing.T) { // Arrange requests := make([]http.Request, 0) responses := [][]byte{stubs.TokenResponse(), stubs.MerchantPaymentInitResponse()} - server := helpers.MakeRequestCapturingTestServer(http.StatusOK, responses, &requests) + server := helpers.MakeRequestCapturingTestServer([]int{http.StatusOK, http.StatusOK}, responses, &requests) client := New( WithBaseURL(server.URL), WithUsername(testUsername), @@ -50,3 +51,270 @@ func TestMerchantPaymentService_Init(t *testing.T) { // Teardown server.Close() } + +func TestMerchantPaymentService_Pay(t *testing.T) { + // Setup + t.Parallel() + + // Arrange + requests := make([]http.Request, 0) + responses := [][]byte{stubs.TokenResponse(), stubs.MerchantPaymentPayResponse()} + server := helpers.MakeRequestCapturingTestServer([]int{http.StatusOK, http.StatusOK}, responses, &requests) + client := New( + WithBaseURL(server.URL), + WithUsername(testUsername), + WithPassword(testPassword), + WithAuthToken(testAuthToken), + ) + + // Act + transaction, response, err := client.MerchantPayment.Pay(context.Background(), &MerchantPaymentPayPrams{ + SubscriberMSISDN: "69XXXXXXX", + ChannelUserMSISDN: "69XXXXXXX", + Amount: "100", + Description: "Payment Description", + OrderID: "abcdef", + Pin: "123456", + PayToken: "MP22120771FEB7B21FD2381C3786", + NotificationURL: "https://example.com/payment-notification", + }) + + // Assert + assert.Nil(t, err) + + assert.GreaterOrEqual(t, len(requests), 2) + request := requests[len(requests)-1] + + assert.Equal(t, "/omcoreapis/1.0.2/mp/pay", request.URL.Path) + assert.Equal(t, testAuthToken, request.Header.Get("X-AUTH-TOKEN")) + assert.Equal(t, "Bearer 19077204-9d0a-31fa-85cf-xxxxxxxxxx", request.Header.Get("Authorization")) + + assert.Equal(t, http.StatusOK, response.HTTPResponse.StatusCode) + + assert.Equal(t, &OrangeResponse[MerchantPaymentTransaction]{ + Message: "Merchant payment successfully initiated", + Data: MerchantPaymentTransaction{ + ID: 48463325, + CreatedTime: "1670442691", + SubscriberMSISDN: "69XXXXXXX", + Amount: 100, + PayToken: "MP22120771FEB7B21FD2381C3786", + TransactionID: "MP221207.2051.B56929", + TransactionMode: "12345", + InitTransactionMessage: "Paiement e la clientele done.The devrez confirmer le paiement en saisissant son code PIN et vous recevrez alors un SMS. Merci dutiliser des services Orange Money.", + InitTransactionStatus: "200", + ConfirmTransactionStatus: nil, + ConfirmTransactionMessage: nil, + Status: "PENDING", + NotificationURL: "https://example.com/payment-notification", + Description: "Payment Description", + ChannelUserMSISDN: "69XXXXXXX", + }, + }, transaction) + + assert.True(t, transaction.Data.IsPending()) + assert.False(t, transaction.Data.IsConfirmed()) + assert.False(t, transaction.Data.IsExpired()) + + // Teardown + server.Close() +} + +func TestMerchantPaymentService_PayWithInsufficientFunds(t *testing.T) { + // Setup + t.Parallel() + + // Arrange + requests := make([]http.Request, 0) + responses := [][]byte{stubs.TokenResponse(), stubs.MerchantPaymentPayResponseWithInsufficientFunds()} + server := helpers.MakeRequestCapturingTestServer([]int{http.StatusOK, http.StatusExpectationFailed}, responses, &requests) + client := New( + WithBaseURL(server.URL), + WithUsername(testUsername), + WithPassword(testPassword), + WithAuthToken(testAuthToken), + ) + + // Act + _, response, err := client.MerchantPayment.Pay(context.Background(), &MerchantPaymentPayPrams{ + SubscriberMSISDN: "69XXXXXXX", + ChannelUserMSISDN: "69XXXXXXX", + Amount: "100", + Description: "Payment Description", + OrderID: "abcdef", + Pin: "123456", + PayToken: "MP22120771FEB7B21FD2381C3786", + NotificationURL: "https://example.com/payment-notification", + }) + + // Assert + assert.NotNil(t, err) + assert.Equal(t, http.StatusExpectationFailed, response.HTTPResponse.StatusCode) + assert.True(t, strings.Contains(string(*response.Body), "60019 :: Le solde du compte du payeur est insuffisant")) + + // Teardown + server.Close() +} + +func TestMerchantPaymentService_Push(t *testing.T) { + // Setup + t.Parallel() + + // Arrange + requests := make([]http.Request, 0) + responses := [][]byte{stubs.TokenResponse(), stubs.MerchantPaymentPushResponse()} + server := helpers.MakeRequestCapturingTestServer([]int{http.StatusOK, http.StatusOK}, responses, &requests) + client := New( + WithBaseURL(server.URL), + WithUsername(testUsername), + WithPassword(testPassword), + WithAuthToken(testAuthToken), + ) + payToken := "MP22120771FEB7B21FD2381C3786" + + // Act + transaction, response, err := client.MerchantPayment.Push(context.Background(), &payToken) + + // Assert + assert.Nil(t, err) + + assert.GreaterOrEqual(t, len(requests), 2) + request := requests[len(requests)-1] + + assert.Equal(t, "/omcoreapis/1.0.2/mp/push/"+payToken, request.URL.Path) + assert.Equal(t, testAuthToken, request.Header.Get("X-AUTH-TOKEN")) + assert.Equal(t, "Bearer 19077204-9d0a-31fa-85cf-xxxxxxxxxx", request.Header.Get("Authorization")) + + assert.Equal(t, http.StatusOK, response.HTTPResponse.StatusCode) + + assert.Equal(t, &OrangeResponse[MerchantPaymentTransaction]{ + Message: "Push sent to customer", + Data: MerchantPaymentTransaction{ + ID: 48463325, + CreatedTime: "1670442691", + SubscriberMSISDN: "69XXXXXXX", + Amount: 100, + PayToken: "MP22120771FEB7B21FD2381C3786", + TransactionID: "MP221207.2051.B56929", + TransactionMode: "12345", + InitTransactionMessage: "Paiement e la clientele done.The devrez confirmer le paiement en saisissant son code PIN et vous recevrez alors un SMS. Merci dutiliser des services Orange Money.", + InitTransactionStatus: "200", + ConfirmTransactionStatus: nil, + ConfirmTransactionMessage: nil, + Status: "PENDING", + NotificationURL: "https://example.com/payment-notification", + Description: "Payment Description", + ChannelUserMSISDN: "69XXXXXXX", + }, + }, transaction) + + assert.True(t, transaction.Data.IsPending()) + assert.False(t, transaction.Data.IsConfirmed()) + assert.False(t, transaction.Data.IsExpired()) + + // Teardown + server.Close() +} + +func TestMerchantPaymentService_TransactionStatus(t *testing.T) { + // Setup + t.Parallel() + + // Arrange + requests := make([]http.Request, 0) + responses := [][]byte{stubs.TokenResponse(), stubs.MerchantPaymentTransactionStatusResponse()} + server := helpers.MakeRequestCapturingTestServer([]int{http.StatusOK, http.StatusOK}, responses, &requests) + client := New( + WithBaseURL(server.URL), + WithUsername(testUsername), + WithPassword(testPassword), + WithAuthToken(testAuthToken), + ) + payToken := "MP22120771FEB7B21FD2381C3786" + + // Act + transaction, response, err := client.MerchantPayment.TransactionStatus(context.Background(), &payToken) + + // Assert + assert.Nil(t, err) + + assert.GreaterOrEqual(t, len(requests), 2) + request := requests[len(requests)-1] + + assert.Equal(t, "/omcoreapis/1.0.2/mp/paymentstatus/"+payToken, request.URL.Path) + assert.Equal(t, testAuthToken, request.Header.Get("X-AUTH-TOKEN")) + assert.Equal(t, "Bearer 19077204-9d0a-31fa-85cf-xxxxxxxxxx", request.Header.Get("Authorization")) + + assert.Equal(t, http.StatusOK, response.HTTPResponse.StatusCode) + + strPtr := func(val string) *string { + return &val + } + + assert.Equal(t, &OrangeResponse[MerchantPaymentTransaction]{ + Message: "Transaction retrieved successfully", + Data: MerchantPaymentTransaction{ + ID: 48463325, + CreatedTime: "1670442691", + SubscriberMSISDN: "69XXXXXXX", + Amount: 100, + PayToken: "MP22120771FEB7B21FD2381C3786", + TransactionID: "MP221207.2051.B56929", + TransactionMode: "12345", + InitTransactionMessage: "Paiement e la clientele done.The devrez confirmer le paiement en saisissant son code PIN et vous recevrez alors un SMS. Merci dutiliser des services Orange Money.", + InitTransactionStatus: "200", + ConfirmTransactionStatus: strPtr("200"), + ConfirmTransactionMessage: strPtr("Successful Payment of COMPANY_NAME from 69XXXXXXX CUSTOMER_NAME. Transaction ID:MP221207.2051.B56929, Amount:100, New balance:1103.5."), + Status: "SUCCESSFULL", + NotificationURL: "https://example.com/payment-notification", + Description: "Payment Description", + ChannelUserMSISDN: "69XXXXXXX", + }, + }, transaction) + + assert.False(t, transaction.Data.IsPending()) + assert.True(t, transaction.Data.IsConfirmed()) + assert.False(t, transaction.Data.IsExpired()) + + // Teardown + server.Close() +} + +func TestMerchantPaymentService_TransactionStatusWithExpired(t *testing.T) { + // Setup + t.Parallel() + + // Arrange + requests := make([]http.Request, 0) + responses := [][]byte{stubs.TokenResponse(), stubs.MerchantPaymentTransactionStatusResponseWithExpired()} + server := helpers.MakeRequestCapturingTestServer([]int{http.StatusOK, http.StatusOK}, responses, &requests) + client := New( + WithBaseURL(server.URL), + WithUsername(testUsername), + WithPassword(testPassword), + WithAuthToken(testAuthToken), + ) + payToken := "MP22120771FEB7B21FD2381C3786" + + // Act + transaction, response, err := client.MerchantPayment.TransactionStatus(context.Background(), &payToken) + + // Assert + assert.Nil(t, err) + + assert.GreaterOrEqual(t, len(requests), 2) + request := requests[len(requests)-1] + + assert.Equal(t, "/omcoreapis/1.0.2/mp/paymentstatus/"+payToken, request.URL.Path) + assert.Equal(t, testAuthToken, request.Header.Get("X-AUTH-TOKEN")) + assert.Equal(t, "Bearer 19077204-9d0a-31fa-85cf-xxxxxxxxxx", request.Header.Get("Authorization")) + + assert.Equal(t, http.StatusOK, response.HTTPResponse.StatusCode) + + assert.False(t, transaction.Data.IsPending()) + assert.False(t, transaction.Data.IsConfirmed()) + assert.True(t, transaction.Data.IsExpired()) + + // Teardown + server.Close() +}