diff --git a/internal/stubs/refund.go b/internal/stubs/refund.go index 4c78406..99b2403 100644 --- a/internal/stubs/refund.go +++ b/internal/stubs/refund.go @@ -4,3 +4,63 @@ package stubs func RefundInvalidClientResponse() []byte { return []byte(`{"error":"invalid_client"}`) } + +// RefundResponse is the response when refunding a transaction +func RefundResponse() []byte { + return []byte(` +{ + "MD5OfMessageBody": "4b55cf6629b5f0ee3c8ac91435a2eb35", + "MD5OfMessageAttributes": "896f665ac83c778c88943113ee0ccd55", + "MessageId": "993764f9-6b1f-41bd-a7ca-97b8b2167ed7", + "ResponseMetadata": { + "RequestId": "e7b09b6d-0d11-5111-8dc9-c4ab56e3cf7c", + "HTTPStatusCode": 200, + "HTTPHeaders": { + "x-amzn-requestid": "e7b09b6d-0d11-5111-8dc9-c4ab56e3cf7c", + "date": "Sun, 01 Dec 2024 12:42:26 GMT", + "content-type": "application/x-amz-json-1.0", + "content-length": "166", + "connection": "keep-alive" + }, + "RetryAttempts": 0 + } +} +`) +} + +// RefundStatusResponse is the response when checking the status of a refund transaction +func RefundStatusResponse() []byte { + return []byte(` +{ + "result": { + "message": "Cash in performed successfully", + "data": { + "createtime": "1733056973", + "subscriberMsisdn": "695xxxxxx", + "amount": 98, + "payToken": "CI24120168FBF65A909F588B4480", + "txnid": "CI241201.1342.C36820", + "txnmode": "rembourse", + "txnstatus": "200", + "orderId": "rembourse", + "status": "SUCCESSFULL", + "channelUserMsisdn": "695xxxxxx", + "description": "Remboursement" + } + }, + "parameters": { + "amount": "98", + "xauth": "WU5PVEVIRUFEOllOT1RFSEVBRDIwMjA=", + "channel_user_msisdn": "69xxxxxx", + "customer_key": "2fBAAq_xxxxxxx", + "customer_secret": "34nFkKxxxxxx", + "final_customer_name": "Arnold", + "final_customer_phone": "69xxxxxx", + "final_customer_name_accuracy": "0" + }, + "CreateAt": "12-01-2024 12:43:00", + "MessageId": "993764f9-6b1f-41bd-a7ca-97b8b2167ed7", + "RefundStep": "2" +} +`) +} diff --git a/refund.go b/refund.go index 894e516..709e5d3 100644 --- a/refund.go +++ b/refund.go @@ -10,6 +10,57 @@ type RefundParams struct { FinalCustomerName string `json:"final_customer_name"` RefundMethod string `json:"refund_method"` FeesIncluded bool `json:"fees_included"` - DebitPolicy string `json:"debit_policy"` FinalCustomerNameAccuracy string `json:"final_customer_name_accuracy"` } + +// RefundTransaction is the response from a refund transaction +type RefundTransaction struct { + MD5OfMessageBody string `json:"MD5OfMessageBody"` + MD5OfMessageAttributes string `json:"MD5OfMessageAttributes"` + MessageID string `json:"MessageId"` + ResponseMetadata struct { + RequestID string `json:"RequestId"` + HTTPStatusCode int `json:"HTTPStatusCode"` + HTTPHeaders struct { + XAmznRequestid string `json:"x-amzn-requestid"` + Date string `json:"date"` + ContentType string `json:"content-type"` + ContentLength string `json:"content-length"` + Connection string `json:"connection"` + } `json:"HTTPHeaders"` + RetryAttempts int `json:"RetryAttempts"` + } `json:"ResponseMetadata"` +} + +// RefundTransactionStatus is the response from a refund transaction status +type RefundTransactionStatus struct { + Result struct { + Message string `json:"message"` + Data struct { + CreatedAt string `json:"createtime"` + SubscriberMsisdn string `json:"subscriberMsisdn"` + Amount int `json:"amount"` + PayToken string `json:"payToken"` + TransactionID string `json:"txnid"` + TransactionMode string `json:"txnmode"` + TransactionStatus string `json:"txnstatus"` + OrderID string `json:"orderId"` + Status string `json:"status"` + ChannelUserMsisdn string `json:"channelUserMsisdn"` + Description string `json:"description"` + } `json:"data"` + } `json:"result"` + Parameters struct { + Amount string `json:"amount"` + Xauth string `json:"xauth"` + ChannelUserMsisdn string `json:"channel_user_msisdn"` + CustomerKey string `json:"customer_key"` + CustomerSecret string `json:"customer_secret"` + FinalCustomerName string `json:"final_customer_name"` + FinalCustomerPhone string `json:"final_customer_phone"` + FinalCustomerNameAccuracy string `json:"final_customer_name_accuracy"` + } `json:"parameters"` + CreatedAt string `json:"CreateAt"` + MessageID string `json:"MessageId"` + RefundStep string `json:"RefundStep"` +} diff --git a/refund_service.go b/refund_service.go index c633c35..824dd1e 100644 --- a/refund_service.go +++ b/refund_service.go @@ -10,13 +10,18 @@ import ( type RefundService service // Status returns the status of an initiated transaction -func (service *RefundService) Status(ctx context.Context, transactionID string) (*map[string]any, *Response, error) { +func (service *RefundService) Status(ctx context.Context, transactionID string) (*RefundTransactionStatus, *Response, error) { err := service.client.refreshToken(ctx) if err != nil { return nil, nil, err } - request, err := service.client.newRequest(ctx, http.MethodGet, "/prod/refund/status/"+transactionID, nil) + payload := map[string]any{ + "customerkey": service.client.customerKey, + "customersecret": service.client.customerSecret, + } + + request, err := service.client.newRequest(ctx, http.MethodGet, "/prod/refund/status/"+transactionID, payload) if err != nil { return nil, nil, err } @@ -26,7 +31,7 @@ func (service *RefundService) Status(ctx context.Context, transactionID string) return nil, response, err } - transaction := new(map[string]any) + transaction := new(RefundTransactionStatus) if err = json.Unmarshal(*response.Body, transaction); err != nil { return nil, response, err } @@ -35,12 +40,17 @@ func (service *RefundService) Status(ctx context.Context, transactionID string) } // Refund executes an initiated transaction -func (service *RefundService) Refund(ctx context.Context, params *RefundParams) (*map[string]any, *Response, error) { +func (service *RefundService) Refund(ctx context.Context, params *RefundParams) (*RefundTransaction, *Response, error) { err := service.client.refreshToken(ctx) if err != nil { return nil, nil, err } + feesIncluded := "No" + if params.FeesIncluded { + feesIncluded = "Yes" + } + payload := map[string]any{ "customerkey": service.client.customerKey, "customersecret": service.client.customerSecret, @@ -51,9 +61,8 @@ func (service *RefundService) Refund(ctx context.Context, params *RefundParams) "final_customer_phone": params.FinalCustomerPhone, "final_customer_name": params.FinalCustomerName, "refund_method": params.RefundMethod, - "fees_included": params.FeesIncluded, + "fees_included": feesIncluded, "final_customer_name_accuracy": params.FinalCustomerNameAccuracy, - "debit_policy": params.DebitPolicy, } request, err := service.client.newRequest(ctx, http.MethodPost, "/prod/refund", payload) @@ -66,7 +75,7 @@ func (service *RefundService) Refund(ctx context.Context, params *RefundParams) return nil, response, err } - transaction := new(map[string]any) + transaction := new(RefundTransaction) if err = json.Unmarshal(*response.Body, transaction); err != nil { return nil, response, err } diff --git a/refund_service_test.go b/refund_service_test.go index caabf89..d1bc012 100644 --- a/refund_service_test.go +++ b/refund_service_test.go @@ -16,7 +16,7 @@ func TestRefundService_Refund(t *testing.T) { // Arrange requests := make([]http.Request, 0) - responses := [][]byte{stubs.TokenResponse(), stubs.RefundInvalidClientResponse()} + responses := [][]byte{stubs.TokenResponse(), stubs.RefundResponse()} server := helpers.MakeRequestCapturingTestServer([]int{http.StatusOK, http.StatusOK}, responses, &requests) client := New( WithTokenURL(server.URL), @@ -38,7 +38,7 @@ func TestRefundService_Refund(t *testing.T) { } // Act - _, response, err := client.Refund.Refund(context.Background(), payload) + transaction, response, err := client.Refund.Refund(context.Background(), payload) // Assert assert.Nil(t, err) @@ -50,6 +50,7 @@ func TestRefundService_Refund(t *testing.T) { assert.Equal(t, "Bearer 19077204-9d0a-31fa-85cf-xxxxxxxxxx", request.Header.Get("Authorization")) assert.Equal(t, http.StatusOK, response.HTTPResponse.StatusCode) + assert.Equal(t, "993764f9-6b1f-41bd-a7ca-97b8b2167ed7", transaction.MessageID) // Teardown server.Close() @@ -95,269 +96,55 @@ func TestRefundService_RefundWithInvalidClient(t *testing.T) { 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() -//} +func TestRefundService_Status(t *testing.T) { + // Setup + t.Parallel() + + // Arrange + requests := make([]http.Request, 0) + responses := [][]byte{stubs.TokenResponse(), stubs.RefundStatusResponse()} + server := helpers.MakeRequestCapturingTestServer([]int{http.StatusOK, http.StatusOK}, responses, &requests) + client := New( + WithTokenURL(server.URL), + WithAPIURL(server.URL), + WithClientID(testClientID), + WithClientSecret(testClientSecret), + ) + + // Act + transaction, response, err := client.Refund.Status(context.Background(), "") + + // Assert + assert.Nil(t, err) + assert.Equal(t, http.StatusOK, response.HTTPResponse.StatusCode) + assert.Equal(t, "CI24120168FBF65A909F588B4480", transaction.Result.Data.PayToken) + + // Teardown + server.Close() +} + +func TestRefundService_StatusWithError(t *testing.T) { + // Setup + t.Parallel() + + // Arrange + requests := make([]http.Request, 0) + responses := [][]byte{stubs.TokenResponse(), []byte("Transactions not found")} + server := helpers.MakeRequestCapturingTestServer([]int{http.StatusOK, http.StatusOK}, responses, &requests) + client := New( + WithTokenURL(server.URL), + WithAPIURL(server.URL), + WithClientID(testClientID), + WithClientSecret(testClientSecret), + ) + + // Act + _, response, err := client.Refund.Status(context.Background(), "ddd") + + // Assert + assert.NotNil(t, err) + assert.Equal(t, http.StatusOK, response.HTTPResponse.StatusCode) + + // Teardown + server.Close() +}