-
Notifications
You must be signed in to change notification settings - Fork 5
Express Checkout PR 3: Shipping & Totals Endpoint #94
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dl260302/express-checkout-pr2-checkout-object
Are you sure you want to change the base?
Changes from all commits
59f635c
60c57bb
ef16222
3f41914
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -335,6 +335,232 @@ server.get('ExpressCheckout', function (req, res, next) { | |
| return next(); | ||
| }); | ||
|
|
||
| /** | ||
| * Shipping & Totals HTTP Endpoint for Express Checkout. | ||
| * Called server-to-server by Affirm's backend (no browser session). | ||
| * Validates HMAC, looks up cart via Custom Object, calculates shipping options. | ||
| */ | ||
| server.post('ShippingTotals', function (req, res, next) { | ||
| res.setContentType('application/json'); | ||
|
|
||
| if (!affirm.data.getExpressCheckoutEnabled()) { | ||
| res.setStatusCode(404); | ||
| res.json({ error: true }); | ||
| return next(); | ||
| } | ||
|
|
||
| // Verify HMAC signature | ||
| var hmacResult = affirmUtils.verifyHMAC(request); | ||
| if (!hmacResult.valid) { | ||
| Logger.error('Affirm Express Checkout: HMAC verification failed - {0}', hmacResult.error); | ||
| res.setStatusCode(401); | ||
| res.json({ error: true, message: 'Unauthorized' }); | ||
| return next(); | ||
| } | ||
|
|
||
| // Parse request body | ||
| var requestBody; | ||
| try { | ||
| requestBody = JSON.parse(request.httpParameterMap.requestBodyAsString); | ||
| } catch (e) { | ||
| res.setStatusCode(400); | ||
| res.json({ error: true, message: 'Invalid JSON' }); | ||
| return next(); | ||
| } | ||
|
|
||
| var orderId = requestBody.order_id; | ||
| var currency = requestBody.currency; | ||
| var shippingAddress = requestBody.shipping; | ||
|
|
||
| // Look up AffirmExpressCart Custom Object | ||
| var expressCart = CustomObjectMgr.getCustomObject('AffirmExpressCart', orderId); | ||
| if (!expressCart) { | ||
| res.setStatusCode(422); | ||
| res.json({ | ||
| errors: [{ | ||
| error_code: 'ORDER_NOT_FOUND', | ||
| message: 'Cart session not found or expired.' | ||
| }] | ||
| }); | ||
| return next(); | ||
| } | ||
|
|
||
| // Validate currency | ||
| if (currency !== 'USD') { | ||
| res.setStatusCode(422); | ||
| res.json({ | ||
| errors: [{ | ||
| error_code: 'CURRENCY_MISMATCH', | ||
| message: 'Only USD transactions are supported.' | ||
| }] | ||
| }); | ||
| return next(); | ||
| } | ||
|
|
||
| // Validate address via hook or default validation | ||
| if (HookMgr.hasHook('app.affirm.express.validateAddress')) { | ||
| var cartData = JSON.parse(expressCart.custom.cartData); | ||
| var addressValidation = HookMgr.callHook('app.affirm.express.validateAddress', 'validateAddress', shippingAddress, cartData); | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Custom merchant-defined validation hook called here, otherwise default to simple validation on line 415. |
||
| if (addressValidation && !addressValidation.valid) { | ||
| res.setStatusCode(422); | ||
| res.json({ | ||
| errors: [{ | ||
| error_code: addressValidation.error_code || 'INVALID_SHIPPING_ADDRESS', | ||
| message: addressValidation.message || 'The provided address is not valid.', | ||
| fields: addressValidation.fields || [] | ||
| }] | ||
| }); | ||
| return next(); | ||
| } | ||
| } else { | ||
| // Default validation: US addresses only | ||
| if (shippingAddress && shippingAddress.country && shippingAddress.country !== 'US') { | ||
| res.setStatusCode(422); | ||
| res.json({ | ||
| errors: [{ | ||
| error_code: 'UNSUPPORTED_SHIPPING_ZONE', | ||
| message: 'Only US shipping addresses are supported.', | ||
| fields: ['shipping_address.country'] | ||
| }] | ||
| }); | ||
| return next(); | ||
| } | ||
| } | ||
|
|
||
| // Build SFCC address object for shipping method lookup | ||
| var shippingAddressForLookup = { | ||
| countryCode: shippingAddress.country || 'US', | ||
| stateCode: shippingAddress.state || '', | ||
| postalCode: shippingAddress.zipcode || '', | ||
| city: shippingAddress.city || '', | ||
| address1: shippingAddress.line1 || '', | ||
| address2: shippingAddress.line2 || '' | ||
| }; | ||
|
|
||
| // We need a basket to calculate shipping. Look up by basketUUID via the Custom Object. | ||
| // Since this is a sessionless call, we use a temporary basket approach: | ||
| // calculate from the stored cart data + SFCC shipping method lookup. | ||
| var expressCartData = JSON.parse(expressCart.custom.cartData); | ||
|
|
||
| // Build a temporary basket from stored cart data for accurate shipping/tax calculation | ||
| var shippingOptions = []; | ||
|
|
||
| try { | ||
| var tempBasket = BasketMgr.getCurrentOrNewBasket(); | ||
|
|
||
| // Populate basket with products from the cart snapshot | ||
| Transaction.wrap(function () { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we want 1 transaction block between this line and the |
||
| var tempShipment = tempBasket.getDefaultShipment(); | ||
|
|
||
| // Clear any pre-existing line items | ||
| var existingItems = tempBasket.getAllProductLineItems().iterator(); | ||
| while (existingItems.hasNext()) { | ||
| tempBasket.removeProductLineItem(existingItems.next()); | ||
| } | ||
|
|
||
| // Recreate product line items from stored cart data | ||
| var items = expressCartData.items || []; | ||
| for (var i = 0; i < items.length; i++) { | ||
| var item = items[i]; | ||
| if (item.sku) { | ||
| var lineItem = tempBasket.createProductLineItem(item.sku, tempShipment); | ||
| lineItem.setQuantityValue(item.qty || 1); | ||
| } | ||
| } | ||
|
|
||
| // Set shipping address (needed for applicable-method lookup and tax calc) | ||
| var shippingAddressFromTempBasket = tempShipment.createShippingAddress(); | ||
| shippingAddressFromTempBasket.setCountryCode(shippingAddressForLookup.countryCode); | ||
| shippingAddressFromTempBasket.setStateCode(shippingAddressForLookup.stateCode); | ||
| shippingAddressFromTempBasket.setPostalCode(shippingAddressForLookup.postalCode); | ||
| shippingAddressFromTempBasket.setCity(shippingAddressForLookup.city); | ||
| shippingAddressFromTempBasket.setAddress1(shippingAddressForLookup.address1); | ||
| shippingAddressFromTempBasket.setAddress2(shippingAddressForLookup.address2); | ||
|
|
||
| HookMgr.callHook('dw.order.calculate', 'calculate', tempBasket); | ||
| }); | ||
|
|
||
| // Get shipping methods applicable to this address | ||
| // Note: Even thoughtempShipment address is set above for basket calculation, method lookup still needs shippingAddressForLookup because getApplicableShippingMethods expects a normal JS object with address fields, not OrderAddress. | ||
| var tempShipment = tempBasket.getDefaultShipment(); | ||
| var applicableShippingMethods = ShippingMgr.getShipmentShippingModel(tempShipment) | ||
| .getApplicableShippingMethods(shippingAddressForLookup); | ||
|
|
||
| // Cycle each shipping method: set it, recalculate, capture totals, then roll back | ||
| Transaction.begin(); | ||
|
|
||
| for (var j = 0; j < applicableShippingMethods.length; j++) { | ||
| var method = applicableShippingMethods[j]; | ||
|
|
||
| affirmUtils.updateShipmentShippingMethod( | ||
| tempShipment.getID(), method.getID(), method, applicableShippingMethods | ||
| ); | ||
| HookMgr.callHook('dw.order.calculate', 'calculate', tempBasket); | ||
|
|
||
| var shippingAmount = Math.round(tempBasket.getAdjustedShippingTotalPrice().getValue() * 100); | ||
| var taxAmount = Math.round(tempBasket.getTotalTax().getValue() * 100); | ||
| var totalAmount = Math.round(tempBasket.getTotalGrossPrice().getValue() * 100); | ||
|
|
||
| // Allow custom hook to override calculated totals | ||
| if (HookMgr.hasHook('app.affirm.express.calculateTotals')) { | ||
| var totalsResult = HookMgr.callHook( | ||
| 'app.affirm.express.calculateTotals', 'calculateTotals', | ||
| method, shippingAddress, expressCartData | ||
| ); | ||
| if (totalsResult) { | ||
| shippingAmount = totalsResult.shipping_amount !== undefined ? totalsResult.shipping_amount : shippingAmount; | ||
| taxAmount = totalsResult.tax_amount !== undefined ? totalsResult.tax_amount : taxAmount; | ||
| totalAmount = totalsResult.total !== undefined ? totalsResult.total : totalAmount; | ||
| } | ||
| } | ||
|
|
||
| shippingOptions.push({ | ||
| shipping_type: method.getID(), | ||
| shipping_label: method.getDisplayName(), | ||
| shipping_amount: shippingAmount, | ||
| tax_amount: taxAmount, | ||
| total: totalAmount | ||
| }); | ||
| } | ||
|
|
||
| Transaction.rollback(); | ||
| } catch (e) { | ||
| Logger.error('Affirm Express: Error calculating shipping options - {0}', e); | ||
| res.setStatusCode(422); | ||
| res.json({ | ||
| errors: [{ | ||
| error_code: 'INTERNAL_SERVER_ERROR', | ||
| message: 'An unexpected error occurred. Please try again.' | ||
| }] | ||
| }); | ||
| return next(); | ||
| } | ||
|
|
||
| // Apply hook filter if available | ||
| if (HookMgr.hasHook('app.affirm.express.filterShippingMethods')) { | ||
| shippingOptions = HookMgr.callHook('app.affirm.express.filterShippingMethods', 'filterShippingMethods', shippingOptions, shippingAddress, expressCartData); | ||
| } | ||
|
|
||
| if (!shippingOptions || shippingOptions.length === 0) { | ||
| res.setStatusCode(422); | ||
| res.json({ | ||
| errors: [{ | ||
| error_code: 'SHIPPING_METHOD_UNAVAILABLE', | ||
| message: 'No shipping options are available for this address.' | ||
| }] | ||
| }); | ||
| return next(); | ||
| } | ||
|
|
||
| res.json({ | ||
| order_id: orderId, | ||
| currency: 'USD', | ||
| subtotal: expressCartData.subtotal, | ||
| shipping_options: shippingOptions | ||
| }); | ||
| return next(); | ||
| }); | ||
|
|
||
| /** | ||
| * Adds Affirm discount coupon | ||
| */ | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Express checkout only supports the United States and USD for initial launch.