flamingo.me/flamingo-commerce/v3@v3.11.0/checkout/application/placeorder/validate_payment_test.go (about)

     1  package placeorder_test
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"net/url"
     7  	"testing"
     8  
     9  	cartDomain "flamingo.me/flamingo-commerce/v3/cart/domain/cart"
    10  	"flamingo.me/flamingo-commerce/v3/checkout/application/placeorder"
    11  	"flamingo.me/flamingo-commerce/v3/checkout/domain/placeorder/process"
    12  	"flamingo.me/flamingo-commerce/v3/checkout/domain/placeorder/states"
    13  	"flamingo.me/flamingo-commerce/v3/payment/application"
    14  	"flamingo.me/flamingo-commerce/v3/payment/domain"
    15  	"flamingo.me/flamingo-commerce/v3/payment/interfaces"
    16  	"flamingo.me/flamingo-commerce/v3/payment/interfaces/mocks"
    17  	price "flamingo.me/flamingo-commerce/v3/price/domain"
    18  	"github.com/google/go-cmp/cmp"
    19  	"github.com/stretchr/testify/assert"
    20  	"github.com/stretchr/testify/mock"
    21  	"github.com/stretchr/testify/require"
    22  )
    23  
    24  func provideProcessFactory(t *testing.T) *process.Factory {
    25  	t.Helper()
    26  	factory := &process.Factory{}
    27  	factory.Inject(
    28  		func() *process.Process {
    29  			return &process.Process{}
    30  		},
    31  		&struct {
    32  			StartState  process.State `inject:"startState"`
    33  			FailedState process.State `inject:"failedState"`
    34  		}{
    35  			StartState:  &states.New{},
    36  			FailedState: &states.Failed{},
    37  		},
    38  	)
    39  	return factory
    40  }
    41  
    42  func provideCartWithPaymentSelection(t *testing.T) cartDomain.Cart {
    43  	t.Helper()
    44  	cart := cartDomain.Cart{}
    45  	paymentSelection, err := cartDomain.NewDefaultPaymentSelection("test", map[string]string{price.ChargeTypeMain: "main"}, cart)
    46  	require.NoError(t, err)
    47  	cart.PaymentSelection = paymentSelection
    48  	return cart
    49  }
    50  
    51  func paymentServiceHelper(t *testing.T, gateway interfaces.WebCartPaymentGateway) *application.PaymentService {
    52  	t.Helper()
    53  	paymentService := &application.PaymentService{}
    54  
    55  	paymentService.Inject(func() map[string]interfaces.WebCartPaymentGateway {
    56  		return map[string]interfaces.WebCartPaymentGateway{
    57  			"test": gateway,
    58  		}
    59  	})
    60  	return paymentService
    61  }
    62  
    63  func TestPaymentValidator(t *testing.T) {
    64  	type flowStatusResult struct {
    65  		flowStatus *domain.FlowStatus
    66  		err        error
    67  	}
    68  
    69  	type want struct {
    70  		runResult process.RunResult
    71  		state     string
    72  		stateData process.StateData
    73  	}
    74  
    75  	tests := []struct {
    76  		name       string
    77  		flowStatus flowStatusResult
    78  		want       want
    79  	}{
    80  		{
    81  			name: "generic payment error during FlowStatus request",
    82  			flowStatus: flowStatusResult{
    83  				err: errors.New("generic_error"),
    84  			},
    85  			want: want{
    86  				runResult: process.RunResult{Failed: process.ErrorOccurredReason{Error: "generic_error"}},
    87  				state:     states.New{}.Name(),
    88  			},
    89  		},
    90  		{
    91  			name: "status: unapproved, action: show iframe",
    92  			flowStatus: flowStatusResult{
    93  				flowStatus: &domain.FlowStatus{
    94  					Status: domain.PaymentFlowStatusUnapproved,
    95  					Action: domain.PaymentFlowActionShowIframe,
    96  					ActionData: domain.FlowActionData{
    97  						URL: &url.URL{Scheme: "https", Host: "iframe-url.com"},
    98  					},
    99  				},
   100  			},
   101  			want: want{
   102  				runResult: process.RunResult{Failed: nil},
   103  				state:     states.ShowIframe{}.Name(),
   104  				stateData: process.StateData(&url.URL{Scheme: "https", Host: "iframe-url.com"}),
   105  			},
   106  		},
   107  		{
   108  			name: "status: unapproved, action: show iframe - URL missing",
   109  			flowStatus: flowStatusResult{
   110  				flowStatus: &domain.FlowStatus{
   111  					Status: domain.PaymentFlowStatusUnapproved,
   112  					Action: domain.PaymentFlowActionShowIframe,
   113  				},
   114  			},
   115  			want: want{
   116  				runResult: process.RunResult{Failed: process.PaymentErrorOccurredReason{Error: placeorder.ValidatePaymentErrorNoActionURL}},
   117  				state:     states.New{}.Name(),
   118  			},
   119  		},
   120  		{
   121  			name: "status: unapproved, action: show html",
   122  			flowStatus: flowStatusResult{
   123  				flowStatus: &domain.FlowStatus{
   124  					Status: domain.PaymentFlowStatusUnapproved,
   125  					Action: domain.PaymentFlowActionShowHTML,
   126  					ActionData: domain.FlowActionData{
   127  						DisplayData: "<h2>Payment Form<h2><form><input type=\"hidden\" /></form>",
   128  					},
   129  				},
   130  			},
   131  			want: want{
   132  				runResult: process.RunResult{Failed: nil},
   133  				state:     states.ShowHTML{}.Name(),
   134  				stateData: process.StateData("<h2>Payment Form<h2><form><input type=\"hidden\" /></form>"),
   135  			},
   136  		},
   137  		{
   138  			name: "status: unapproved, action: show html - html missing",
   139  			flowStatus: flowStatusResult{
   140  				flowStatus: &domain.FlowStatus{
   141  					Status: domain.PaymentFlowStatusUnapproved,
   142  					Action: domain.PaymentFlowActionShowHTML,
   143  				},
   144  			},
   145  			want: want{
   146  				runResult: process.RunResult{Failed: process.PaymentErrorOccurredReason{Error: placeorder.ValidatePaymentErrorNoActionDisplayData}},
   147  				state:     states.New{}.Name(),
   148  			},
   149  		},
   150  		{
   151  			name: "status: unapproved, action: redirect",
   152  			flowStatus: flowStatusResult{
   153  				flowStatus: &domain.FlowStatus{
   154  					Status: domain.PaymentFlowStatusUnapproved,
   155  					Action: domain.PaymentFlowActionRedirect,
   156  					ActionData: domain.FlowActionData{
   157  						URL: &url.URL{Scheme: "https", Host: "redirect-url.com"},
   158  					},
   159  				},
   160  			},
   161  			want: want{
   162  				runResult: process.RunResult{Failed: nil},
   163  				state:     states.Redirect{}.Name(),
   164  				stateData: process.StateData(&url.URL{Scheme: "https", Host: "redirect-url.com"}),
   165  			},
   166  		},
   167  		{
   168  			name: "status: unapproved, action: redirect - URL missing",
   169  			flowStatus: flowStatusResult{
   170  				flowStatus: &domain.FlowStatus{
   171  					Status: domain.PaymentFlowStatusUnapproved,
   172  					Action: domain.PaymentFlowActionRedirect,
   173  				},
   174  			},
   175  			want: want{
   176  				runResult: process.RunResult{Failed: process.PaymentErrorOccurredReason{Error: placeorder.ValidatePaymentErrorNoActionURL}},
   177  				state:     states.New{}.Name(),
   178  			},
   179  		},
   180  		{
   181  			name: "status: unapproved, action: trigger_client_sdk",
   182  			flowStatus: flowStatusResult{
   183  				flowStatus: &domain.FlowStatus{
   184  					Status: domain.PaymentFlowStatusUnapproved,
   185  					Action: domain.PaymentFlowActionTriggerClientSDK,
   186  					ActionData: domain.FlowActionData{
   187  						URL:         &url.URL{Scheme: "https", Host: "redirect-url.com"},
   188  						DisplayData: `{"foo": "bar"}`,
   189  					},
   190  				},
   191  			},
   192  			want: want{
   193  				runResult: process.RunResult{Failed: nil},
   194  				state:     states.TriggerClientSDK{}.Name(),
   195  				stateData: process.StateData(states.TriggerClientSDKData{
   196  					URL:  &url.URL{Scheme: "https", Host: "redirect-url.com"},
   197  					Data: `{"foo": "bar"}`,
   198  				}),
   199  			},
   200  		},
   201  		{
   202  			name: "status: unapproved, action: trigger_client_sdk - URL missing",
   203  			flowStatus: flowStatusResult{
   204  				flowStatus: &domain.FlowStatus{
   205  					Status: domain.PaymentFlowStatusUnapproved,
   206  					Action: domain.PaymentFlowActionTriggerClientSDK,
   207  				},
   208  			},
   209  			want: want{
   210  				runResult: process.RunResult{Failed: process.PaymentErrorOccurredReason{Error: placeorder.ValidatePaymentErrorNoActionURL}},
   211  				state:     states.New{}.Name(),
   212  			},
   213  		},
   214  		{
   215  			name: "status: unapproved, action: post redirect",
   216  			flowStatus: flowStatusResult{
   217  				flowStatus: &domain.FlowStatus{
   218  					Status: domain.PaymentFlowStatusUnapproved,
   219  					Action: domain.PaymentFlowActionPostRedirect,
   220  					ActionData: domain.FlowActionData{
   221  						URL: &url.URL{Scheme: "https", Host: "post-redirect-url.com"},
   222  						FormParameter: map[string]domain.FormField{
   223  							"form-field-0": {
   224  								Value: []string{"value0", "value1"},
   225  							},
   226  							"form-field-1": {
   227  								Value: []string{"value0"},
   228  							},
   229  						},
   230  					},
   231  				},
   232  			},
   233  			want: want{
   234  				runResult: process.RunResult{Failed: nil},
   235  				state:     states.PostRedirect{}.Name(),
   236  				stateData: process.StateData(states.PostRedirectData{
   237  					FormFields: map[string]states.FormField{
   238  						"form-field-0": {
   239  							Value: []string{"value0", "value1"},
   240  						},
   241  						"form-field-1": {
   242  							Value: []string{"value0"},
   243  						},
   244  					},
   245  					URL: &url.URL{Scheme: "https", Host: "post-redirect-url.com"},
   246  				}),
   247  			},
   248  		},
   249  		{
   250  			name: "status: unapproved, action: show wallet payment",
   251  			flowStatus: flowStatusResult{
   252  				flowStatus: &domain.FlowStatus{
   253  					Status: domain.PaymentFlowStatusUnapproved,
   254  					Action: domain.PaymentFlowActionShowWalletPayment,
   255  					ActionData: domain.FlowActionData{
   256  						WalletDetails: &domain.WalletDetails{
   257  							UsedPaymentMethod: "ApplePay",
   258  							PaymentRequestAPI: domain.PaymentRequestAPI{
   259  								Methods: `{
   260  									"supportedMethods": "https://apple.com/apple-pay",
   261  									"data": {
   262  										"version": 3,
   263  										"merchantIdentifier": "merchant.com.aoe.om3.kso",
   264  										"merchantCapabilities": ["supports3DS"],
   265  										"supportedNetworks": ["asterCard", "visa", "discover", "amex"],
   266  										"countryCode": "DE"
   267  									}
   268  								}
   269  								`,
   270  								MerchantValidationURL: &url.URL{Scheme: "https", Host: "validate.example.com"},
   271  							},
   272  						},
   273  					},
   274  				},
   275  			},
   276  			want: want{
   277  				runResult: process.RunResult{Failed: nil},
   278  				state:     states.ShowWalletPayment{}.Name(),
   279  				stateData: process.StateData(states.ShowWalletPaymentData{
   280  					UsedPaymentMethod: "ApplePay",
   281  					PaymentRequestAPI: domain.PaymentRequestAPI{
   282  						Methods: `{
   283  									"supportedMethods": "https://apple.com/apple-pay",
   284  									"data": {
   285  										"version": 3,
   286  										"merchantIdentifier": "merchant.com.aoe.om3.kso",
   287  										"merchantCapabilities": ["supports3DS"],
   288  										"supportedNetworks": ["asterCard", "visa", "discover", "amex"],
   289  										"countryCode": "DE"
   290  									}
   291  								}
   292  								`,
   293  						MerchantValidationURL: &url.URL{Scheme: "https", Host: "validate.example.com"},
   294  					},
   295  				}),
   296  			},
   297  		},
   298  		{
   299  			name: "status: unapproved, action: post redirect - URL missing",
   300  			flowStatus: flowStatusResult{
   301  				flowStatus: &domain.FlowStatus{
   302  					Status: domain.PaymentFlowStatusUnapproved,
   303  					Action: domain.PaymentFlowActionPostRedirect,
   304  				},
   305  			},
   306  			want: want{
   307  				runResult: process.RunResult{Failed: process.PaymentErrorOccurredReason{Error: placeorder.ValidatePaymentErrorNoActionURL}},
   308  				state:     states.New{}.Name(),
   309  			},
   310  		},
   311  		{
   312  			name: "status: unapproved, action: not supported",
   313  			flowStatus: flowStatusResult{
   314  				flowStatus: &domain.FlowStatus{
   315  					Status: domain.PaymentFlowStatusUnapproved,
   316  					Action: "unknown",
   317  				},
   318  			},
   319  			want: want{
   320  				runResult: process.RunResult{Failed: process.PaymentErrorOccurredReason{Error: "Payment action not supported: \"unknown\""}},
   321  				state:     states.New{}.Name(),
   322  			},
   323  		},
   324  		{
   325  			name: "status: approved",
   326  			flowStatus: flowStatusResult{
   327  				flowStatus: &domain.FlowStatus{
   328  					Status: domain.PaymentFlowStatusApproved,
   329  				},
   330  			},
   331  			want: want{
   332  				runResult: process.RunResult{},
   333  				state:     states.CompletePayment{}.Name(),
   334  			},
   335  		},
   336  		{
   337  			name: "status: completed",
   338  			flowStatus: flowStatusResult{
   339  				flowStatus: &domain.FlowStatus{
   340  					Status: domain.PaymentFlowStatusCompleted,
   341  				},
   342  			},
   343  			want: want{
   344  				runResult: process.RunResult{},
   345  				state:     states.Success{}.Name(),
   346  			},
   347  		},
   348  		{
   349  			name: "status: completed",
   350  			flowStatus: flowStatusResult{
   351  				flowStatus: &domain.FlowStatus{
   352  					Status: domain.PaymentFlowStatusCompleted,
   353  				},
   354  			},
   355  			want: want{
   356  				runResult: process.RunResult{},
   357  				state:     states.Success{}.Name(),
   358  			},
   359  		},
   360  		{
   361  			name: "status: aborted",
   362  			flowStatus: flowStatusResult{
   363  				flowStatus: &domain.FlowStatus{
   364  					Status: domain.PaymentFlowStatusAborted,
   365  				},
   366  			},
   367  			want: want{
   368  				runResult: process.RunResult{Failed: process.PaymentCanceledByCustomerReason{}},
   369  				state:     states.New{}.Name(),
   370  			},
   371  		},
   372  		{
   373  			name: "status: cancelled",
   374  			flowStatus: flowStatusResult{
   375  				flowStatus: &domain.FlowStatus{
   376  					Status: domain.PaymentFlowStatusCancelled,
   377  				},
   378  			},
   379  			want: want{
   380  				runResult: process.RunResult{Failed: process.PaymentErrorOccurredReason{}},
   381  				state:     states.New{}.Name(),
   382  			},
   383  		},
   384  		{
   385  			name: "status: failed",
   386  			flowStatus: flowStatusResult{
   387  				flowStatus: &domain.FlowStatus{
   388  					Status: domain.PaymentFlowStatusFailed,
   389  				},
   390  			},
   391  			want: want{
   392  				runResult: process.RunResult{Failed: process.PaymentErrorOccurredReason{}},
   393  				state:     states.New{}.Name(),
   394  			},
   395  		},
   396  		{
   397  			name: "status: wait for customer",
   398  			flowStatus: flowStatusResult{
   399  				flowStatus: &domain.FlowStatus{
   400  					Status: domain.PaymentFlowWaitingForCustomer,
   401  				},
   402  			},
   403  			want: want{
   404  				runResult: process.RunResult{},
   405  				state:     states.WaitForCustomer{}.Name(),
   406  			},
   407  		},
   408  		{
   409  			name: "status: unknown",
   410  			flowStatus: flowStatusResult{
   411  				flowStatus: &domain.FlowStatus{
   412  					Status: "unknown",
   413  				},
   414  			},
   415  			want: want{
   416  				runResult: process.RunResult{Failed: process.PaymentErrorOccurredReason{Error: "Payment status not supported: \"unknown\""}},
   417  				state:     states.New{}.Name(),
   418  			},
   419  		},
   420  	}
   421  	for _, tt := range tests {
   422  		t.Run(tt.name, func(t *testing.T) {
   423  			factory := provideProcessFactory(t)
   424  			p, _ := factory.New(&url.URL{}, provideCartWithPaymentSelection(t))
   425  			gateway := mocks.NewWebCartPaymentGateway(t)
   426  			gateway.EXPECT().FlowStatus(mock.Anything, mock.Anything, p.Context().UUID).Return(tt.flowStatus.flowStatus, tt.flowStatus.err).Once()
   427  
   428  			paymentService := paymentServiceHelper(t, gateway)
   429  			got := placeorder.PaymentValidator(context.Background(), p, paymentService)
   430  			if diff := cmp.Diff(got, tt.want.runResult); diff != "" {
   431  				t.Error("PaymentValidator() = -got +want", diff)
   432  			}
   433  
   434  			assert.Equal(t, p.Context().CurrentStateName, tt.want.state)
   435  
   436  			if diff := cmp.Diff(p.Context().CurrentStateData, tt.want.stateData); diff != "" {
   437  				t.Error("CurrentStateData = -got +want", diff)
   438  			}
   439  		})
   440  	}
   441  }