In questo articolo vedremo come gestire il flusso del checkout di un e-commerce in ExpressJS.

Il processo di checkout si suddivide in quattro fasi:

  1. L' utente si autentica o si registra sul sito.
  2. L' utente inserisce i suoi dati di fatturazione e di spedizione.
  3. L' utente effettua il pagamento.
  4. L' utente viene reindirizzato sulla pagina di conclusione dell'ordine.

Le nostre route necessiteranno della persistenza dei dati in sessione, quindi per il nostro scopo utilizzeremo il modulo NPM cookie-session. Dobbiamo anche validare i dati inseriti dall'utente, quindi useremo il modulo NPM validator.

Il file principale della nostra applicazione sarà il seguente:

'use strict';

const path = require('path');
const express = require('express');
const app = express();
const port = process.env.PORT || 3000;
const helmet = require('helmet');
const bodyParser = require('body-parser');
const cookieSession = require('cookie-session');
const routes = require('./routes');

app.set('view engine', 'ejs');

app.disable('x-powered-by');

app.use('/public', express.static(path.join(__dirname, 'public')));
app.use(helmet());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(cookieSession({
  name: 'name',
  keys: ['key1', 'key2']
}));
app.use('/', routes);


app.listen(port);

Il primo step fornisce come view due form, uno per il login e l'altro per la registrazione. La variabile section viene usata nella view per impostare la voce corrente del menu di navigazione.

'use strict';

const express = require('express');
const router = express.Router();
const validator = require('validator');


router.get('/checkout', (req, res, next) => {
    res.render('index', {
       title: 'Checkout',
       section: 'info'
    });
});

module.exports = router;

Qui dobbiamo gestire le due richieste POST dei form.

router.post('/login', (req, res, next) => {
    const { email, password } = req.body;
    const errors = [];

    if(!validator.isEmail(email)) {
        errors.push({
            param: 'email',
            msg: 'Invalid e-mail address.'
        });
    }

    if(validator.isEmpty(password)) {
        errors.push({
            param: 'password',
            msg: 'Invalid password.'
        });
    }

    if(errors.length) {
        res.json({ errors });
    } else {
        if(!req.session.user) {
            req.session.user = { email };
        }
        res.json({ loggedIn: true });
    }
});

router.post('/register', (req, res, next) => {
    const { name, email, password } = req.body;
    const errors = [];

    if(validator.isEmpty(name)) {
        errors.push({
            param: 'name',
            msg: 'Invalid name.'
        });
    }

    if(!validator.isEmail(email)) {
        errors.push({
            param: 'email',
            msg: 'Invalid e-mail address.'
        });
    }

    if(validator.isEmpty(password)) {
        errors.push({
            param: 'password',
            msg: 'Invalid password.'
        });
    }

    if(errors.length) {
        res.json({ errors });
    } else {
        if(!req.session.user) {
            req.session.user = { name, email };
        }
        res.json({ registered: true });
    }
});

Se non ci sono errori di validazione, viene creato un oggetto user nella sessione corrente. Altrimenti gli errori vengono passati come array di oggetti JSON al codice JavaScript lato client.

Ciascun oggetto errore contiene il messaggio da visualizzare e un riferimento all'elemento del form corrispondente (attributi id o name).

Il secondo step riguarda l'inserimento dei dati di fatturazione e di spedizione. In questa route verifichiamo che l'utente abbia di fatto effettuato il primo passaggio. In caso contrario lo reindirizziamo alla pagina iniziale.

router.get('/billing-shipping', (req, res, next) => {
    if(req.session.user) {
        res.render('billing-shipping', {
            title: 'Billing and shipping',
            section: 'billing',
            user: req.session.user
        });
    } else {
        res.redirect('/checkout');
    }
});

A questo punto dobbiamo elaborare la richiesta POST del form di questa sezione.

router.post('/billing-shipping', (req, res, next) => {
    const post = req.body;
    const errors = [];

    if(validator.isEmpty(post.billing_first_name)) {
        errors.push({
            param: 'billing_first_name',
            msg: 'Required field.'
        });
    }
    if(validator.isEmpty(post.billing_last_name)) {
        errors.push({
            param: 'billing_last_name',
            msg: 'Required field.'
        });
    }
    if(!validator.isEmail(post.billing_email)) {
        errors.push({
            param: 'billing_email',
            msg: 'Invalid e-mail address.'
        });
    }

    if(validator.isEmpty(post.billing_address)) {
        errors.push({
            param: 'billing_address',
            msg: 'Required field.'
        });
    }

    if(validator.isEmpty(post.billing_city)) {
        errors.push({
            param: 'billing_city',
            msg: 'Required field.'
        });
    }

    if(!validator.isNumeric(post.billing_zip)) {
        errors.push({
            param: 'billing_zip',
            msg: 'Invalid postal code.'
        });
    }

    if(!post.same_as) {
        if(validator.isEmpty(post.shipping_first_name)) {
            errors.push({
                param: 'shipping_first_name',
                msg: 'Required field.'
            });
        }
        if(validator.isEmpty(post.shipping_last_name)) {
            errors.push({
                param: 'shipping_last_name',
                msg: 'Required field.'
            });
        }
        if(!validator.isEmail(post.shipping_email)) {
            errors.push({
                param: 'shipping_email',
                msg: 'Invalid e-mail address.'
            });
        }
    
        if(validator.isEmpty(post.shipping_address)) {
            errors.push({
                param: 'shipping_address',
                msg: 'Required field.'
            });
        }
    
        if(validator.isEmpty(post.shipping_city)) {
            errors.push({
                param: 'shipping_city',
                msg: 'Required field.'
            });
        }
    
        if(!validator.isNumeric(post.shipping_zip)) {
            errors.push({
                param: 'shipping_zip',
                msg: 'Invalid postal code.'
            });
        }
    }

    if(errors.length > 0) {
        res.json({ errors });
    } else {
        const billing = {};
        

        for(let prop in post) {
            if(prop.startsWith('billing')) {
                let key = prop.replace('billing', '').replace(/_/g, '');
                billing[key] = post[prop];
            }
        }

        req.session.user.billing = billing;

        if(!post.same_as) {
            const shipping = {};

            for(let prop in post) {
                if(prop.startsWith('shipping')) {
                    let key = prop.replace('shipping', '').replace(/_/g, '');
                    shipping[key] = post[prop];
                }
            }

            req.session.user.shipping = shipping;
        }

        res.json({ saved: true });
    }
});

Se non ci sono errori di validazione, condizionati dal fatto se l'utente ha indicato o meno che i dati di fatturazione coincidono con quelli di spedizione, i dati di fatturazione vengono aggiunti come proprietà dell'oggetto user presente in sessione.

Il terzo step è il pagamento. Qui può essere inserito il form che crea la transazione con il gateway remoto (nel nostro esempio è PayPal). In questa route dobbiamo sempre verificare che l'utente abbia completato gli step precedenti.

router.get('/payment', (req, res, next) => {
    if(!req.session.user) {
        res.redirect('/checkout');
        return;
    }

    const { user } = req.session;

    if(!user.billing) {
        res.redirect('/billing-shipping');
        return;
    }

    res.render('payment', {
        title: 'Payment',
        section: 'payment',
        user
    });
});

Lo step finale ha luogo quando l'utente viene reindirizzato sulla pagina di conclusione dell'ordine dopo aver effettuato il pagamento.

router.get('/thank-you', (req, res, next) => {
    if(req.session.user && req.session.user.billing) {
        res.render('thank-you', {
            title: 'Order complete',
            section: 'thank-you',
            user: req.session.user
        });
    } else {
        res.redirect('/checkout');
    }
});

A questo punto in un e-commerce live andrebbe azzerata la sessione corrente ed inviata un'e-mail di conferma all'utente con il riepilogo dell'ordine.

Demo

Heroku

Codice sorgente

GitHub