Ad Code

How to: Build a Whatsapp E-commerce Flow - Part 6

 How to: Build a Whatsapp E-commerce Flow - Netlify function create-order-proxy Part 6

File netlify/functions/create-order-proxy.js — lightweight proxy that accepts a minimal payload from the webhook function and forwards to your main backend or runs the order creation logic inline. Drop this file into netlify/functions/. Configure environment variables in Netlify UI as shown in the repo README.

// netlify/functions/create-order-proxy.js
const fetch = require('node-fetch');
const Stripe = require('stripe');
const { Pool } = require('pg');
const Redis = require('ioredis');

const {
  BACKEND_MODE = 'proxy', // 'proxy' or 'inline'
  BACKEND_URL,            // used when BACKEND_MODE=proxy
  DATABASE_URL,
  REDIS_URL,
  STRIPE_SECRET,
  BASE_URL,
  WHATSAPP_PHONE_NUMBER_ID,
  WHATSAPP_ACCESS_TOKEN
} = process.env;

const stripe = STRIPE_SECRET ? Stripe(STRIPE_SECRET) : null;
const pg = DATABASE_URL ? new Pool({ connectionString: DATABASE_URL }) : null;
const redis = REDIS_URL ? new Redis(REDIS_URL) : null;

// Helper: send WhatsApp message via Cloud API
async function sendWhatsAppMessage(toPhone, payload) {
  const url = `https://graph.facebook.com/v18.0/${WHATSAPP_PHONE_NUMBER_ID}/messages`;
  const res = await fetch(url, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${WHATSAPP_ACCESS_TOKEN}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(Object.assign({ messaging_product: 'whatsapp', to: toPhone }, payload))
  });
  return res.json();
}

// Minimal reservation helper using Redis
async function reserveSku(sku, qty, orderId, ttlSeconds = 10 * 60) {
  if (!redis) return true;
  const key = `reservation:${sku}:${orderId}`;
  const exists = await redis.get(key);
  if (exists) return false;
  await redis.set(key, qty.toString(), 'EX', ttlSeconds);
  await redis.sadd(`reservations_by_sku:${sku}`, key);
  return true;
}

// Inline order creation logic (simple)
async function createOrderInline({ phone, sku, wa_message_id }) {
  if (!pg) throw new Error('DATABASE_URL not configured for inline mode');

  const client = await pg.connect();
  try {
    // lookup product and variant
    const q = `
      SELECT p.id, p.sku, p.title, p.price_cents, p.currency, v.id AS variant_id, v.stock, v.price_delta_cents
      FROM products p
      LEFT JOIN variants v ON v.product_id = p.id
      WHERE p.sku = $1
      LIMIT 1
    `;
    const r = await client.query(q, [sku]);
    if (!r.rows.length) {
      return { status: 'not_found', message: `SKU ${sku} not found` };
    }
    const row = r.rows[0];
    const unitPrice = row.price_cents + (row.price_delta_cents || 0);
    const items = [{ sku: row.sku, variant_id: row.variant_id, qty: 1, unit_price_cents: unitPrice }];
    const subtotal = unitPrice;
    const total = subtotal;

    const externalId = `wa_${Date.now()}_${Math.random().toString(36).slice(2,8)}`;
    const insert = await client.query(
      `INSERT INTO orders (external_id, customer_phone, items, subtotal_cents, total_cents, payment_status, metadata)
       VALUES ($1,$2,$3,$4,$5,$6,$7) RETURNING id`,
      [externalId, phone, JSON.stringify(items), subtotal, total, 'pending', JSON.stringify({ wa_message_id })]
    );
    const orderId = insert.rows[0].id;

    // reserve stock
    const reserved = await reserveSku(row.sku, 1, orderId, 10 * 60);
    if (!reserved) {
      // mark order as expired or delete
      await client.query('UPDATE orders SET fulfillment_status=$1 WHERE id=$2', ['expired', orderId]);
      return { status: 'unavailable', message: 'Item temporarily unavailable' };
    }

    // create Stripe Checkout session if configured
    let checkoutUrl = null;
    if (stripe) {
      const session = await stripe.checkout.sessions.create({
        payment_method_types: ['card'],
        mode: 'payment',
        line_items: [{
          price_data: {
            currency: row.currency.toLowerCase(),
            product_data: { name: row.title },
            unit_amount: unitPrice
          },
          quantity: 1
        }],
        metadata: { order_id: orderId },
        success_url: `${BASE_URL}/checkout/success?session_id={CHECKOUT_SESSION_ID}`,
        cancel_url: `${BASE_URL}/checkout/cancel`
      });
      checkoutUrl = session.url;
    }

    // send WhatsApp message with checkout link
    const messageBody = checkoutUrl
      ? `Order created for ${row.title}\nTotal: ${(total/100).toFixed(2)} ${row.currency}\nPay here: ${checkoutUrl}`
      : `Order created for ${row.title}\nTotal: ${(total/100).toFixed(2)} ${row.currency}\nPlease complete payment via your preferred method.`;

    await sendWhatsAppMessage(phone, { type: 'text', text: { body: messageBody } });

    return { status: 'created', order_id: orderId, checkout_url: checkoutUrl };
  } finally {
    client.release();
  }
}

exports.handler = async function (event) {
  try {
    if (event.httpMethod !== 'POST') return { statusCode: 405, body: 'Method Not Allowed' };

    const payload = JSON.parse(event.body || '{}');
    const phone = payload.phone || payload.from;
    const sku = payload.sku;
    const wa_message_id = payload.wa_message_id || null;

    if (!phone || !sku) return { statusCode: 400, body: JSON.stringify({ error: 'missing phone or sku' }) };

    if (BACKEND_MODE === 'proxy') {
      if (!BACKEND_URL) return { statusCode: 500, body: JSON.stringify({ error: 'BACKEND_URL not configured' }) };
      // forward to main backend
      const res = await fetch(`${BACKEND_URL}/create-order`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ customer_phone: phone, items: [{ sku, qty: 1 }], source: 'whatsapp', metadata: { wa_message_id } })
      });
      const json = await res.json();
      return { statusCode: res.status, body: JSON.stringify(json) };
    } else {
      // inline mode: create order directly in Netlify function
      const result = await createOrderInline({ phone, sku, wa_message_id });
      return { statusCode: 200, body: JSON.stringify(result) };
    }
  } catch (err) {
    console.error('create-order-proxy error', err);
    return { statusCode: 500, body: JSON.stringify({ error: 'server_error', message: err.message }) };
  }
};

Netlify functions package ready to drop in

Place these files under netlify/functions/:

  • webhook.js (from earlier)
  • payment-callback.js (from earlier)
  • create-order-proxy.js (above)

Environment variables to set in Netlify Site settings
Required: DATABASE_URL, REDIS_URL, WHATSAPP_PHONE_NUMBER_ID, WHATSAPP_ACCESS_TOKEN, WHATSAPP_APP_SECRET, VERIFY_TOKEN, STRIPE_SECRET, STRIPE_WEBHOOK_SECRET, BASE_URL, BACKEND_MODE, BACKEND_URL (if using proxy mode).

Notes

  • Use BACKEND_MODE=proxy to forward requests to your main backend (recommended for heavy DB work).
  • Use BACKEND_MODE=inline only if you configured DATABASE_URL and REDIS_URL in Netlify and accept function execution time limits.
  • For Stripe webhooks, register the endpoint at https://<site>/.netlify/functions/payment-callback and copy the webhook secret into STRIPE_WEBHOOK_SECRET.

Blog ready README snippet for publishing

Title Build a WhatsApp Commerce Flow with Netlify Functions Docker and cPanel

TLDR Copy the repo, set environment variables, run docker compose up --build, import the Postman collection, and test the flow: message → product selection → create order → checkout → payment webhook → WhatsApp confirmation.

Quick copy paste commands

git clone https://github.com/your-org/whatsapp-commerce-starter.git
cd whatsapp-commerce-starter
cp .env.example .env
# edit .env with your secrets
docker compose up --build
# apply DB migration
docker compose exec db psql -U postgres -d wa -f /app/migrations/migration.sql

Netlify drop in

  1. Copy netlify/functions/ into your repo.
  2. Set environment variables in Netlify Site settings.
  3. Deploy site.
  4. Configure Stripe webhook to https://<site>/.netlify/functions/payment-callback.

Files included

  • netlify/functions/webhook.js — WhatsApp webhook receiver.
  • netlify/functions/create-order-proxy.js — order creation proxy or inline order creator.
  • netlify/functions/payment-callback.js — Stripe webhook handler.
  • src/index.js — Express app for traditional hosting.
  • Dockerfile and docker-compose.yml — local test environment.
  • migrations/migration.sql — minimal schema.
  • scripts/cleanup-reservations.js and scripts/reserve.lua — reservation cleanup and atomic reserve.

Image placeholders for blog

  • Screenshot 1: Postman flow showing Send Product List request.
  • Screenshot 2: Netlify environment variables panel.
  • Screenshot 3: Docker Compose logs showing services up.

Short FAQ

  • Why a proxy function — keep heavy DB work off serverless functions to avoid timeouts; proxy to a dedicated backend.
  • How to avoid oversell — use Redis reservations with TTL and atomic Lua script for stock decrement.
  • WhatsApp templates — submit order_confirmation and shipping_update templates early for approval.

Final notes and next steps

  • I packaged the create-order-proxy function so you can run order creation either inline or forwarded to your backend.
  • Next,  I will now generate a ready .env.example file tailored for Netlify and a small README_BLOG.md formatted for your blog with copy‑paste code blocks and image markdown placeholders.

Post a Comment

0 Comments