Commit 198d33dc by Awais Committed by Awais Qureshi

New AB Testing URL for checkout page.

ECOM-2866
parent 068b439a
...@@ -23,6 +23,16 @@ urlpatterns = patterns( ...@@ -23,6 +23,16 @@ urlpatterns = patterns(
} }
), ),
# This is for A/B testing.
url(
r'^begin-flow/{course}/$'.format(course=settings.COURSE_ID_PATTERN),
views.PayAndVerifyView.as_view(),
name="verify_student_begin_flow",
kwargs={
'message': views.PayAndVerifyView.FIRST_TIME_VERIFY_MSG
}
),
# The user is enrolled in a non-paid mode and wants to upgrade. # The user is enrolled in a non-paid mode and wants to upgrade.
# This is the same as the "start verification" flow, # This is the same as the "start verification" flow,
# except with slight messaging changes. # except with slight messaging changes.
......
...@@ -423,7 +423,9 @@ class PayAndVerifyView(View): ...@@ -423,7 +423,9 @@ class PayAndVerifyView(View):
'verification_good_until': verification_good_until, 'verification_good_until': verification_good_until,
'capture_sound': staticfiles_storage.url("audio/camera_capture.wav"), 'capture_sound': staticfiles_storage.url("audio/camera_capture.wav"),
'nav_hidden': True, 'nav_hidden': True,
'is_ab_testing': 'begin-flow' in request.path,
} }
return render_to_response("verify_student/pay_and_verify.html", context) return render_to_response("verify_student/pay_and_verify.html", context)
def _redirect_if_necessary( def _redirect_if_necessary(
......
...@@ -674,6 +674,7 @@ ...@@ -674,6 +674,7 @@
'lms/include/js/spec/verify_student/image_input_spec.js', 'lms/include/js/spec/verify_student/image_input_spec.js',
'lms/include/js/spec/verify_student/review_photos_step_view_spec.js', 'lms/include/js/spec/verify_student/review_photos_step_view_spec.js',
'lms/include/js/spec/verify_student/make_payment_step_view_spec.js', 'lms/include/js/spec/verify_student/make_payment_step_view_spec.js',
'lms/include/js/spec/verify_student/make_payment_step_view_ab_testing_spec.js',
'lms/include/js/spec/edxnotes/utils/logger_spec.js', 'lms/include/js/spec/edxnotes/utils/logger_spec.js',
'lms/include/js/spec/edxnotes/views/notes_factory_spec.js', 'lms/include/js/spec/edxnotes/views/notes_factory_spec.js',
'lms/include/js/spec/edxnotes/views/shim_spec.js', 'lms/include/js/spec/edxnotes/views/shim_spec.js',
......
define([
'jquery',
'underscore',
'backbone',
'common/js/spec_helpers/ajax_helpers',
'common/js/spec_helpers/template_helpers',
'js/verify_student/views/make_payment_step_view'
],
function( $, _, Backbone, AjaxHelpers, TemplateHelpers, MakePaymentStepView ) {
'use strict';
var checkPaymentButtons,
expectPaymentSubmitted,
goToPayment,
expectPaymentDisabledBecauseInactive,
expectPaymentButtonEnabled,
expectPriceSelected,
createView,
SERVER_ERROR_MSG = 'An error occurred!';
describe( 'edx.verify_student.MakePaymentStepView', function() {
var STEP_DATA = {
minPrice: '12',
currency: 'usd',
processors: ['test-payment-processor'],
courseKey: 'edx/test/test',
courseModeSlug: 'verified',
isABTesting: true
};
createView = function( stepDataOverrides ) {
var view = new MakePaymentStepView({
el: $( '#current-step-container' ),
stepData: _.extend( _.clone( STEP_DATA ), stepDataOverrides ),
errorModel: new ( Backbone.Model.extend({}) )()
}).render();
// Stub the payment form submission
spyOn( view, 'submitForm' ).andCallFake( function() {} );
return view;
};
expectPriceSelected = function( price ) {
var sel = $( 'input[name="contribution"]' );
// check that contribution value is same as price given
expect( sel.length ).toEqual(1);
expect( sel.val() ).toEqual(price);
};
expectPaymentButtonEnabled = function( isEnabled ) {
var el = $( '.payment-button'),
appearsDisabled = el.hasClass( 'is-disabled' ),
isDisabled = el.prop( 'disabled' );
expect( appearsDisabled ).not.toEqual( isEnabled );
expect( isDisabled ).not.toEqual( isEnabled );
};
expectPaymentDisabledBecauseInactive = function() {
var payButton = $( '.payment-button' );
// Payment button should be hidden
expect( payButton.length ).toEqual(0);
};
goToPayment = function( requests, kwargs ) {
var params = {
contribution: kwargs.amount || '',
course_id: kwargs.courseId || '',
processor: kwargs.processor || '',
sku: kwargs.sku || ''
};
// Click the "go to payment" button
$( '.payment-button' ).click();
// Verify that the request was made to the server
AjaxHelpers.expectPostRequest(
requests, '/verify_student/create_order/', $.param( params )
);
// Simulate the server response
if ( kwargs.succeeds ) {
// TODO put fixture responses in the right place
AjaxHelpers.respondWithJson(
requests, {payment_page_url: 'http://payment-page-url/', payment_form_data: {foo: 'bar'}}
);
} else {
AjaxHelpers.respondWithTextError( requests, 400, SERVER_ERROR_MSG);
}
};
expectPaymentSubmitted = function( view, params ) {
var form;
expect(view.submitForm).toHaveBeenCalled();
form = view.submitForm.mostRecentCall.args[0];
expect(form.serialize()).toEqual($.param(params));
expect(form.attr('method')).toEqual('POST');
expect(form.attr('action')).toEqual('http://payment-page-url/');
};
checkPaymentButtons = function( requests, buttons ) {
var $el = $( '.payment-button' );
expect($el.length).toEqual(_.size(buttons));
_.each(buttons, function( expectedText, expectedId ) {
var buttonEl = $( '#' + expectedId),
request;
buttonEl.removeAttr('disabled');
expect( buttonEl.length ).toEqual( 1 );
expect( buttonEl[0] ).toHaveClass( 'payment-button' );
expect( buttonEl[0] ).toHaveText( expectedText );
expect( buttonEl[0] ).toHaveClass( 'action-primary-blue' );
buttonEl[0].click();
expect( buttonEl[0] ).toHaveClass( 'is-selected' );
expectPaymentButtonEnabled( false );
request = AjaxHelpers.currentRequest(requests);
expect(request.requestBody.split('&')).toContain('processor=' + expectedId);
AjaxHelpers.respondWithJson(requests, {});
});
};
beforeEach(function() {
window.analytics = jasmine.createSpyObj('analytics', ['track', 'page', 'trackLink']);
setFixtures( '<div id="current-step-container"></div>' );
TemplateHelpers.installTemplate( 'templates/verify_student/make_payment_step_ab_testing' );
});
it( 'A/B Testing: check Initialize method with AB testing enable ', function() {
var view = createView();
expect( view.templateName ).toEqual('make_payment_step_ab_testing');
expect( view.btnClass ).toEqual('action-primary-blue');
});
it( 'shows users only minimum price', function() {
var view = createView(),
requests = AjaxHelpers.requests(this);
expectPriceSelected( STEP_DATA.minPrice );
expectPaymentButtonEnabled( true );
goToPayment( requests, {
amount: STEP_DATA.minPrice,
courseId: STEP_DATA.courseKey,
processor: STEP_DATA.processors[0],
succeeds: true
});
expectPaymentSubmitted( view, {foo: 'bar'} );
});
it( 'A/B Testing: provides working payment buttons for a single processor', function() {
createView({processors: ['cybersource']});
checkPaymentButtons( AjaxHelpers.requests(this), {cybersource: 'Checkout'});
});
it( 'A/B Testing: provides working payment buttons for multiple processors', function() {
createView({processors: ['cybersource', 'paypal', 'other']});
checkPaymentButtons( AjaxHelpers.requests(this), {
cybersource: 'Checkout',
paypal: 'Checkout with PayPal',
other: 'Checkout with other'
});
});
it( 'A/B Testing: by default minimum price is selected if no suggested prices are given', function() {
var view = createView(),
requests = AjaxHelpers.requests( this );
expectPriceSelected( STEP_DATA.minPrice);
expectPaymentButtonEnabled( true );
goToPayment( requests, {
amount: STEP_DATA.minPrice,
courseId: STEP_DATA.courseKey,
processor: STEP_DATA.processors[0],
succeeds: true
});
expectPaymentSubmitted( view, {foo: 'bar'} );
});
it( 'A/B Testing: min price is always selected even if contribution amount is provided', function() {
// Pre-select a price NOT in the suggestions
createView({
contributionAmount: '99.99'
});
// Expect that the price is filled in
expectPriceSelected( STEP_DATA.minPrice );
});
it( 'A/B Testing: disables payment for inactive users', function() {
createView({ isActive: false });
expectPaymentDisabledBecauseInactive();
});
it( 'A/B Testing: displays an error if the order could not be created', function() {
var requests = AjaxHelpers.requests( this ),
view = createView();
goToPayment( requests, {
amount: STEP_DATA.minPrice,
courseId: STEP_DATA.courseKey,
processor: STEP_DATA.processors[0],
succeeds: false
});
// Expect that an error is displayed
expect( view.errorModel.get('shown') ).toBe( true );
expect( view.errorModel.get('errorTitle') ).toEqual( 'Could not submit order' );
expect( view.errorModel.get('errorMsg') ).toEqual( SERVER_ERROR_MSG );
// Expect that the payment button is re-enabled
expectPaymentButtonEnabled( true );
});
});
}
);
...@@ -108,6 +108,7 @@ define([ ...@@ -108,6 +108,7 @@ define([
buttonEl.removeAttr('disabled'); buttonEl.removeAttr('disabled');
expect( buttonEl.length ).toEqual( 1 ); expect( buttonEl.length ).toEqual( 1 );
expect( buttonEl[0] ).toHaveClass( 'payment-button' ); expect( buttonEl[0] ).toHaveClass( 'payment-button' );
expect( buttonEl[0] ).toHaveClass( 'action-primary' );
expect( buttonEl[0] ).toHaveText( expectedText ); expect( buttonEl[0] ).toHaveText( expectedText );
buttonEl[0].click(); buttonEl[0].click();
...@@ -216,6 +217,12 @@ define([ ...@@ -216,6 +217,12 @@ define([
'Try the transaction again in a few minutes.' 'Try the transaction again in a few minutes.'
); );
}); });
it( 'check Initialize method without AB testing ', function() {
var view = createView();
expect( view.templateName ).toEqual('make_payment_step');
expect( view.btnClass ).toEqual('action-primary');
});
}); });
} }
); );
...@@ -66,7 +66,8 @@ var edx = edx || {}; ...@@ -66,7 +66,8 @@ var edx = edx || {};
verificationDeadline: el.data('verification-deadline'), verificationDeadline: el.data('verification-deadline'),
courseModeSlug: el.data('course-mode-slug'), courseModeSlug: el.data('course-mode-slug'),
alreadyVerified: el.data('already-verified'), alreadyVerified: el.data('already-verified'),
verificationGoodUntil: el.data('verification-good-until') verificationGoodUntil: el.data('verification-good-until'),
isABTesting: el.data('is-ab-testing')
}, },
'payment-confirmation-step': { 'payment-confirmation-step': {
courseKey: el.data('course-key'), courseKey: el.data('course-key'),
......
...@@ -11,6 +11,15 @@ var edx = edx || {}; ...@@ -11,6 +11,15 @@ var edx = edx || {};
edx.verify_student.MakePaymentStepView = edx.verify_student.StepView.extend({ edx.verify_student.MakePaymentStepView = edx.verify_student.StepView.extend({
templateName: "make_payment_step", templateName: "make_payment_step",
btnClass: 'action-primary',
initialize: function( obj ) {
_.extend( this, obj );
if (this.templateContext().isABTesting) {
this.templateName = 'make_payment_step_ab_testing';
this.btnClass = 'action-primary-blue';
}
},
defaultContext: function() { defaultContext: function() {
return { return {
...@@ -27,7 +36,8 @@ var edx = edx || {}; ...@@ -27,7 +36,8 @@ var edx = edx || {};
platformName: '', platformName: '',
alreadyVerified: false, alreadyVerified: false,
courseModeSlug: 'audit', courseModeSlug: 'audit',
verificationGoodUntil: '' verificationGoodUntil: '',
isABTesting: false
}; };
}, },
...@@ -61,8 +71,8 @@ var edx = edx || {}; ...@@ -61,8 +71,8 @@ var edx = edx || {};
_getPaymentButtonHtml: function(processorName) { _getPaymentButtonHtml: function(processorName) {
var self = this; var self = this;
return _.template( return _.template(
'<button class="next action-primary payment-button" id="<%- name %>" ><%- text %></button> ' '<button class="next <%- btnClass %> payment-button" id="<%- name %>" ><%- text %></button> '
)({name: processorName, text: self._getPaymentButtonText(processorName)}); )({name: processorName, text: self._getPaymentButtonText(processorName), btnClass: this.btnClass});
}, },
postRender: function() { postRender: function() {
......
...@@ -175,6 +175,14 @@ ...@@ -175,6 +175,14 @@
color: $white !important; color: $white !important;
} }
// elements - controls
.action-primary-blue {
@extend %btn-primary-blue;
// needed for override due to .register a:link styling
border: 0 !important;
color: $white !important;
}
.action-confirm { .action-confirm {
@extend %btn-verify-primary; @extend %btn-verify-primary;
// needed for override due to .register a:link styling // needed for override due to .register a:link styling
...@@ -821,6 +829,109 @@ ...@@ -821,6 +829,109 @@
// indiv slides - review // indiv slides - review
#wrapper-review { #wrapper-review {
color: $black;
.page-title {
@extend %t-strong;
border-bottom: 2px solid $m-gray-d3;
padding-bottom: ($baseline*0.75);
margin-bottom: $baseline;
text-transform: inherit;
}
.review {
.certificate {
@include font-size(18);
background-repeat: no-repeat;
padding-left: ($baseline*2.5);
overflow: hidden;
min-height: 32px;
p {
@include line-height(22);
@extend %t-strong;
margin-top: 0;
color: $black;
}
.purchase {
@include float(right);
@include margin-left($baseline*0.75);
text-align: right;
.product-info {
@include font-size(22);
@extend %t-strong;
color: $blue;
}
}
&.verified_icon {
background-image: url('#{$static-path}/images/icon-sm-verified.png');
}
&.no-id-professional_icon,
&.professional_icon {
background-image: url('#{$static-path}/images/icon-sm-professional.png');
}
}
.payment-buttons {
overflow: auto;
padding-bottom: ($baseline/4);
margin: {
top: ($baseline / 2);
bottom: ($baseline * 0.75);
};
.payment-button {
padding: ($baseline*0.4) $baseline;
min-width: 200px;
}
.action-primary-blue {
&.is-selected {
background: $blue !important;
}
}
}
.border-gray {
border-bottom: 2px solid $gray;
margin: ($baseline*1.12) 0;
}
}
.container {
padding: ($baseline*0.75) 0;
p {
@include line-height(22);
color: $black;
}
.photo-requirement {
@include font-size(12);
position: relative;
padding-left: ($baseline*2);
margin-top: ($baseline*0.75);
background-repeat: no-repeat;
background-position: left top;
.fa {
position: absolute;
left:0;
color: $mediumGrey;
}
h6 {
font-weight: bold;
color: $extraDarkGrey;
}
}
}
.review-task { .review-task {
margin-bottom: ($baseline*1.5); margin-bottom: ($baseline*1.5);
......
<div id="wrapper-review" tab-index="0" class="wrapper-view make-payment-step">
<div class="review view">
<% if (!isActive ) { %>
<h2 class="page-title">
<%- gettext("Account Not Activated")%>
</h2>
<% } else if ( !upgrade ) { %>
<h2 class="page-title">
<%= _.sprintf(
gettext( "You are enrolling in %(courseName)s"),
{ courseName: '<span class="course-title">' + courseName + '</span>' }
) %>
</h2>
<% } else { %>
<h2 class="page-title">
<%= _.sprintf(
gettext( "Upgrade to a Verified Certificate for %(courseName)s"),
{ courseName: '<span class="course-title">' + courseName + '</span>' }
) %>
</h2>
<% } %>
<% if ( !isActive ) { %>
<p>
<%- gettext("Before you upgrade to a certificate track, you must activate your account.") %>
<%- gettext("Check your email for an activation message.") %>
</p>
<% } else { %>
<div class="certificate <%- courseModeSlug %>_icon">
<div class="purchase">
<p class="product-info"><span class="product-name"></span> <%- gettext( "Total" ) %>: <span class="price">$<%- minPrice %> USD</span></p>
</div>
<p>
<% if ( courseModeSlug === 'no-id-professional' || courseModeSlug === 'professional') { %>
<%= _.sprintf(
gettext( "Professional Certificate for %(courseName)s"),{ courseName: courseName }
)%>
<% } else { %>
<%= _.sprintf(
gettext( "Verified Certificate for %(courseName)s"),{ courseName: courseName }
)%>
<% } %>
</p>
</div>
<% } %>
<% if ( isActive ) { %>
<div class="payment-buttons is-ready center">
<input type="hidden" name="contribution" value="<%- minPrice %>" />
<input type="hidden" name="sku" value="<%- sku %>" />
<div class="pay-options">
<%
// payment buttons will go here
%>
</div>
</div>
<div class="border-gray"></div>
<% } %>
</div>
<% if ( isActive ) { %>
<div class="container">
<% if ( _.some( requirements, function( isVisible ) { return isVisible; } ) ) { %>
<p>
<% if ( verificationDeadline ) { %>
<%- _.sprintf(
gettext( "To receive a certificate, you must also verify your identity before %(date)s." ),
{ date: verificationDeadline }
) %>
<% } else { %>
<%- gettext( "To receive a certificate, you must also verify your identity." ) %>
<% } %>
<%- gettext("To verify your identity, you need a webcam and a government-issued photo ID.") %>
</p>
<% if ( requirements['photo-id-required'] ) { %>
<div class="photo-requirement user_icon">
<i class="fa fa-user fa-2x" aria-hidden="true"></i>
<h6>
<%- gettext("Photo ID") %>
</h6>
<p>
<%- gettext("Your ID must be a government-issued photo ID that clearly shows your face.") %>
</p>
</div>
<% } %>
<% if ( requirements['webcam-required'] ) { %>
<div class="photo-requirement cam_icon">
<i class="fa fa-video-camera fa-2x" aria-hidden="true"></i>
<h6>
<%- gettext("Webcam") %>
</h6>
<p>
<%- gettext("You will use your webcam to take a picture of your face and of your government-issued photo ID.") %>
</p>
</div>
<% } %>
<% } %>
</div>
<% } %>
<form id="payment-processor-form"></form>
</div>
...@@ -24,9 +24,15 @@ from lms.djangoapps.verify_student.views import PayAndVerifyView ...@@ -24,9 +24,15 @@ from lms.djangoapps.verify_student.views import PayAndVerifyView
<% <%
template_names = ( template_names = (
["webcam_photo", "image_input", "error"] + ["webcam_photo", "image_input", "error"] +
["intro_step", "make_payment_step", "payment_confirmation_step"] + ["intro_step", "payment_confirmation_step"] +
["face_photo_step", "id_photo_step", "review_photos_step", "enrollment_confirmation_step"] ["face_photo_step", "id_photo_step", "review_photos_step", "enrollment_confirmation_step"]
) )
if not is_ab_testing:
template_names.append("make_payment_step")
else:
template_names.append("make_payment_step_ab_testing")
%> %>
% for template_name in template_names: % for template_name in template_names:
<script type="text/template" id="${template_name}-tpl"> <script type="text/template" id="${template_name}-tpl">
...@@ -76,6 +82,7 @@ from lms.djangoapps.verify_student.views import PayAndVerifyView ...@@ -76,6 +82,7 @@ from lms.djangoapps.verify_student.views import PayAndVerifyView
data-already-verified='${already_verified}' data-already-verified='${already_verified}'
data-verification-good-until='${verification_good_until}' data-verification-good-until='${verification_good_until}'
data-capture-sound='${capture_sound}' data-capture-sound='${capture_sound}'
data-is-ab-testing='${json.dumps(is_ab_testing)}'
## If we reached the verification flow from an in-course checkpoint, ## If we reached the verification flow from an in-course checkpoint,
## then pass the checkpoint location in so that we can associate ## then pass the checkpoint location in so that we can associate
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment