Skip to content
Open
20 changes: 20 additions & 0 deletions cartridges/int_affirm/cartridge/scripts/api/affirmAPI.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,26 @@
};
}
};
/**
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrelated to this change, but the auth() function should take a new orderId field so that the order id can be updated during auth call

* Read checkout details by checkout ID (Express Checkout)
*
* @param {string} checkoutId checkout ID from Express Checkout token
* @returns {Object} checkout details including shipping, address, totals
*/
self.readCheckout = function (checkoutId) {
try {
var affirmService = require('*/cartridge/scripts/init/initAffirmServices').initService('affirm.checkout.read');
affirmService.URL = affirmData.getURLPath() + '/v2/checkout/' + checkoutId;
var data = { reqMethod: 'GET' };
var response = affirmService.call(data).object;
return response;
} catch (e) {
logger.error('Affirm. File - affirmAPI. readCheckout Error - {0}', e);
return {
error: true
};
}
};
};
module.exports = new Api();
}());
13 changes: 6 additions & 7 deletions cartridges/int_affirm/cartridge/scripts/utils/affirmHelper.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,20 +71,19 @@ function checkCart(cart, sfraFlag) {
};
}
var affirmResponse = affirm.order.authOrder(token);
session.privacy.affirmResponseID = affirmResponse.response.id;
session.privacy.affirmFirstEventID = affirmResponse.response.events[0].id;
session.privacy.affirmFirstEventCreatedAt = affirmResponse.response.events[0].created;
session.privacy.affirmAmount = affirmResponse.response.amount;
session.privacy.affirmCurrency = affirmResponse.response.currency;

if (empty(affirmResponse) || affirmResponse.error){
if (empty(affirmResponse) || affirmResponse.error || !affirmResponse.response){
return {
status:{
error: true,
PlaceOrderError: new Status(Status.ERROR, 'confirm.error.technical')
}
};
}
session.privacy.affirmResponseID = affirmResponse.response.id;
session.privacy.affirmFirstEventID = affirmResponse.response.events[0].id;
session.privacy.affirmFirstEventCreatedAt = affirmResponse.response.events[0].created;
session.privacy.affirmAmount = affirmResponse.response.amount;
session.privacy.affirmCurrency = affirmResponse.response.currency;
var affirmStatus = affirm.basket.syncBasket(basket, affirmResponse.response);
if (affirmStatus.error){
affirm.order.voidOrder(affirmResponse.response.id);
Expand Down
212 changes: 160 additions & 52 deletions cartridges/int_affirm_sfra/cartridge/controllers/Affirm.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,15 @@ var URLUtils = require('dw/web/URLUtils');
var server = require('server');
var BasketMgr = require('dw/order/BasketMgr');
var affirm = require('*/cartridge/scripts/affirm');
var COHelpers = require('*/cartridge/scripts/checkout/checkoutHelpers');

var Transaction = require('dw/system/Transaction');
var PaymentMgr = require('dw/order/PaymentMgr');
var OrderModel = require('*/cartridge/models/order');
var csrfProtection = require('*/cartridge/scripts/middleware/csrf');
var hooksHelper = require('*/cartridge/scripts/helpers/hooks');
var Response = require('dw/system/Response');
var ShippingMgr = require('dw/order/ShippingMgr');
var HookMgr = require('dw/system/HookMgr');
var affirmUtils = require('*/cartridge/scripts/utils/affirmUtils');
var checkoutAffirm = require('*/cartridge/scripts/checkout/checkoutAffirm');
var affirmOrderFinalize = require('*/cartridge/scripts/checkout/affirmOrderFinalize');
var cartHelpers = require('*/cartridge/scripts/cart/cartHelpers');
var currentSite = require('dw/system/Site').getCurrent();
var CustomObjectMgr = require('dw/object/CustomObjectMgr');
Expand Down Expand Up @@ -180,57 +177,27 @@ server.use('Confirmation', function (req, res, next) {

try {
var basket = BasketMgr.getCurrentOrNewBasket();
if (affirm.data.getAffirmVCNStatus() != 'on') {
var affirmPaymentResult = affirm.utils.setPayment(basket, AFFIRM_PAYMENT_METHOD, true);
if (affirmPaymentResult.error) {
res.render('/error', {
message: Resource.msg('error.confirmation.error', 'confirmation', null)
});
return next();
}
}
var affirmCheck = checkoutAffirm.checkCart(basket, checkoutToken, session);
if (affirmCheck.status.error) {
res.render('/error', {
message: Resource.msg('error.confirmation.error', 'confirmation', null)
});
return next();
}

try {
var OrderMgr = require('dw/order/OrderMgr');
var order = OrderMgr.createOrder(basket);
} catch (e) {
Logger.error('Affirm: Order creation not possible for this basket. Error - {0}', e);
}

if (!order) {
res.redirect(URLUtils.url('Cart-Show').toString());
return next();
}
var handlePaymentsResult = COHelpers.handlePayments(order, order.getOrderNo());

if (handlePaymentsResult.error) {
res.render('/error', {
message: Resource.msg('error.confirmation.error', 'confirmation', null)
});
return next();
}

var fraudDetectionStatus = hooksHelper('app.fraud.detection', 'fraudDetection', basket, require('*/cartridge/scripts/hooks/fraudDetection').fraudDetection);
var finalizeResult = affirmOrderFinalize.finalizeAffirmOrder({
basket: basket,
checkoutToken: checkoutToken,
session: session,
localeId: req.locale.id,
skipSetPayment: affirm.data.getAffirmVCNStatus() == 'on',
orderCreateFailLogContext: 'Affirm'
});

var orderPlacementStatus = COHelpers.placeOrder(order, fraudDetectionStatus);
if (orderPlacementStatus.error) {
res.render('/error', {
message: Resource.msg('error.confirmation.error', 'confirmation', null)
});
if (!finalizeResult.ok) {
if (finalizeResult.mode === 'cart') {
res.redirect(URLUtils.url('Cart-Show').toString());
} else {
res.render('/error', {
message: Resource.msg('error.confirmation.error', 'confirmation', null)
});
}
return next();
}

checkoutAffirm.postProcess(order);
COHelpers.sendConfirmationEmail(order, req.locale.id);

res.redirect(URLUtils.url('Order-Confirm', 'ID', order.orderNo, 'token', order.orderToken).toString());
res.redirect(URLUtils.url('Order-Confirm', 'ID', finalizeResult.order.orderNo, 'token', finalizeResult.order.orderToken).toString());
return next();
} catch (e) {
Logger.error('APIException ' + e);
Expand Down Expand Up @@ -561,10 +528,151 @@ server.post('ShippingTotals', function (req, res, next) {
return next();
});

/**
* Handles Express Checkout confirmation.
* Reads checkout from Affirm API, applies shipping/billing to basket,
* creates order, authorizes, validates amounts, and places order.
*/
server.use('ExpressConfirmation', function (req, res, next) {
var checkoutToken = request.httpParameterMap.checkout_token.stringValue;

if (!checkoutToken) {
Logger.error('Affirm Express: Missing checkout_token on ExpressConfirmation');
res.render('/error', {
message: Resource.msg('error.confirmation.error', 'confirmation', null)
});
return next();
}

try {
var affirmAPI = require('*/cartridge/scripts/api/affirmAPI');

// Step 4a: Read checkout from Affirm API to get shipping details
var checkoutData = affirmAPI.readCheckout(checkoutToken);
if (!checkoutData || checkoutData.error) {
Logger.error('Affirm Express: Failed to read checkout - {0}', JSON.stringify(checkoutData));
res.render('/error', {
message: Resource.msg('error.confirmation.error', 'confirmation', null)
});
return next();
}

var checkoutResponse = checkoutData.response || checkoutData;

var basket = BasketMgr.getCurrentOrNewBasket();

// Step 4b: Apply shipping address, billing address, shipping method, and email from Affirm data
Transaction.wrap(function () {
// Apply shipping address
var shipment = basket.getDefaultShipment();
var shippingAddr = shipment.createShippingAddress();
var affirmShipping = checkoutResponse.shipping;

if (affirmShipping) {
shippingAddr.setFirstName(affirmShipping.name ? affirmShipping.name.first || '' : '');
shippingAddr.setLastName(affirmShipping.name ? affirmShipping.name.last || '' : '');
shippingAddr.setAddress1(affirmShipping.address ? affirmShipping.address.line1 || '' : '');
shippingAddr.setAddress2(affirmShipping.address ? affirmShipping.address.line2 || '' : '');
shippingAddr.setCity(affirmShipping.address ? affirmShipping.address.city || '' : '');
shippingAddr.setStateCode(affirmShipping.address ? affirmShipping.address.state || '' : '');
shippingAddr.setPostalCode(affirmShipping.address ? affirmShipping.address.zipcode || '' : '');
shippingAddr.setCountryCode(affirmShipping.address ? affirmShipping.address.country || 'US' : 'US');
shippingAddr.setPhone(affirmShipping.phone_number || '');
}

// Apply shipping method
if (affirmShipping && affirmShipping.shipping_type) {
var applicableShippingMethods = ShippingMgr.getShipmentShippingModel(shipment)
.getApplicableShippingMethods(shippingAddr);
affirmUtils.updateShipmentShippingMethod(
shipment.getID(),
affirmShipping.shipping_type,
null,
applicableShippingMethods
);
}

// Apply billing address
var billingAddr = basket.createBillingAddress();
var affirmBilling = checkoutResponse.billing || affirmShipping;

if (affirmBilling) {
var billingName = affirmBilling.name || (affirmShipping ? affirmShipping.name : null);
var billingAddress = affirmBilling.address || (affirmShipping ? affirmShipping.address : null);

billingAddr.setFirstName(billingName ? billingName.first || '' : '');
billingAddr.setLastName(billingName ? billingName.last || '' : '');
billingAddr.setAddress1(billingAddress ? billingAddress.line1 || '' : '');
billingAddr.setAddress2(billingAddress ? billingAddress.line2 || '' : '');
billingAddr.setCity(billingAddress ? billingAddress.city || '' : '');
billingAddr.setStateCode(billingAddress ? billingAddress.state || '' : '');
billingAddr.setPostalCode(billingAddress ? billingAddress.zipcode || '' : '');
billingAddr.setCountryCode(billingAddress ? billingAddress.country || 'US' : 'US');
billingAddr.setPhone(affirmBilling.phone_number || (affirmShipping ? affirmShipping.phone_number || '' : ''));
}

// Set customer email
var email = (affirmShipping && affirmShipping.email) || (affirmBilling && affirmBilling.email) || '';
if (email) {
basket.setCustomerEmail(email);
}

// Recalculate basket with final shipping method + address
HookMgr.callHook('dw.order.calculate', 'calculate', basket);
});

// Step 4c–4h: Affirm PI, authorize, create order, payments, place, email
var finalizeResult = affirmOrderFinalize.finalizeAffirmOrder({
basket: basket,
checkoutToken: checkoutToken,
session: session,
localeId: req.locale.id,
orderCreateFailLogContext: 'Affirm Express'
});

if (!finalizeResult.ok) {
if (finalizeResult.mode === 'cart') {
res.redirect(URLUtils.url('Cart-Show').toString());
} else {
res.render('/error', {
message: Resource.msg('error.confirmation.error', 'confirmation', null)
});
}
return next();
}

var order = finalizeResult.order;

// Clean up AffirmExpressCart Custom Object
var expressOrderId = checkoutResponse.order_id;
if (expressOrderId) {
try {
var expressCart = CustomObjectMgr.getCustomObject('AffirmExpressCart', expressOrderId);
if (expressCart) {
Transaction.wrap(function () {
CustomObjectMgr.remove(expressCart);
});
}
} catch (cleanupError) {
Logger.warn('Affirm Express: Failed to clean up AffirmExpressCart - {0}', cleanupError);
}
}

res.redirect(URLUtils.url('Order-Confirm', 'ID', order.orderNo, 'token', order.orderToken).toString());
return next();
} catch (e) {
Logger.error('Affirm Express Confirmation error: {0}', e);
res.render('/error', {
message: Resource.msg('error.confirmation.error', 'confirmation', null)
});
return next();
}
});

/**
* Adds Affirm discount coupon
*/
server.use('ApplyDiscount', function (req, res, next) {4
server.use('ApplyDiscount', function (req, res, next) {
var newCouponLi = null;
var validDiscount = false;
var discountAmount = 0;
Expand Down
Loading