Skip to content

Order Fulfillment

Automatically process and fulfill orders when payment is received.

Overview

This pattern shows how to build an automated order fulfillment system that:

  1. Receives payment notifications via webhook
  2. Delivers items to players
  3. Marks orders as fulfilled

Architecture

Pixlpay → Webhook → Your Server → Game Server

                    Mark Fulfilled

Complete Implementation

Webhook Handler (Node.js)

javascript
const express = require('express');
const crypto = require('crypto');
const Queue = require('bull');

const app = express();
const fulfillmentQueue = new Queue('order-fulfillment');

// Verify webhook signature
function verifySignature(payload, signature, secret) {
  const expected = crypto.createHmac('sha256', secret).update(payload).digest('hex');
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}

// Webhook endpoint
app.post('/webhooks/pixlpay', express.raw({ type: 'application/json' }), async (req, res) => {
  const signature = req.headers['x-webhook-signature'];

  if (!verifySignature(req.body, signature, process.env.PIXLPAY_WEBHOOK_SECRET)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  const event = JSON.parse(req.body);

  // Only process paid orders
  if (event.event_type === 'order.paid') {
    await fulfillmentQueue.add(event.data, {
      attempts: 5,
      backoff: { type: 'exponential', delay: 60000 }
    });
  }

  res.status(200).json({ status: 'queued' });
});

app.listen(3000);

Queue Processor

javascript
const { PixlpayClient } = require('./pixlpay');
const { GameServer } = require('./gameserver');

const pixlpay = new PixlpayClient(
  process.env.PIXLPAY_STORE_URL,
  process.env.PIXLPAY_API_TOKEN
);

fulfillmentQueue.process(async (job) => {
  const order = job.data;
  const playerUUID = order.metadata?.minecraft_uuid;

  if (!playerUUID) {
    console.log(`Order ${order.order_id} has no player UUID, skipping`);
    return;
  }

  // Get full order details
  const fullOrder = await pixlpay.getOrder(order.order_id);

  // Deliver each item
  for (const item of fullOrder.data.items) {
    await deliverItem(playerUUID, item);
  }

  // Mark as fulfilled
  await pixlpay.fulfillOrder(order.order_id);

  console.log(`Order ${order.order_number} fulfilled for ${order.customer_email}`);
});

async function deliverItem(playerUUID, item) {
  // Get delivery commands from product metadata
  const product = await pixlpay.getProduct(item.product_id);
  const commands = product.data.metadata?.delivery_commands || [];

  for (const command of commands) {
    const parsed = command.replace('{player}', playerUUID);
    await GameServer.executeCommand(parsed);
  }
}

Error Handling

javascript
fulfillmentQueue.on('failed', async (job, err) => {
  console.error(`Fulfillment failed for order ${job.data.order_id}:`, err);

  // Notify admin after final failure
  if (job.attemptsMade >= job.opts.attempts) {
    await notifyAdmin({
      subject: 'Order Fulfillment Failed',
      body: `Order ${job.data.order_number} failed after ${job.attemptsMade} attempts.
             Customer: ${job.data.customer_email}
             Error: ${err.message}`
    });
  }
});

Handling Offline Players

Players may be offline when their order completes:

javascript
async function deliverItem(playerUUID, item) {
  const player = await GameServer.getPlayer(playerUUID);

  if (!player || !player.isOnline) {
    // Store for later delivery
    await redis.lpush(`pending:${playerUUID}`, JSON.stringify({
      item_id: item.id,
      product_id: item.product_id,
      commands: item.metadata?.commands
    }));
    return;
  }

  // Deliver immediately
  await executeDeliveryCommands(player, item);
}

// Check for pending deliveries when player joins
GameServer.on('playerJoin', async (player) => {
  const pending = await redis.lrange(`pending:${player.uuid}`, 0, -1);

  for (const itemJson of pending) {
    const item = JSON.parse(itemJson);
    await executeDeliveryCommands(player, item);
  }

  await redis.del(`pending:${player.uuid}`);
});

Database Schema

Track deliveries for audit and support:

sql
CREATE TABLE order_deliveries (
  id SERIAL PRIMARY KEY,
  order_id INTEGER NOT NULL,
  player_uuid VARCHAR(36) NOT NULL,
  product_id INTEGER NOT NULL,
  status VARCHAR(20) DEFAULT 'pending',
  delivered_at TIMESTAMP,
  error_message TEXT,
  attempts INTEGER DEFAULT 0,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_deliveries_player ON order_deliveries(player_uuid);
CREATE INDEX idx_deliveries_status ON order_deliveries(status);

Monitoring

Track fulfillment metrics:

javascript
const metrics = {
  ordersProcessed: 0,
  ordersFailed: 0,
  averageDeliveryTime: 0,
};

fulfillmentQueue.on('completed', (job, result) => {
  metrics.ordersProcessed++;
  metrics.averageDeliveryTime = calculateAverage(job.finishedOn - job.timestamp);
});

fulfillmentQueue.on('failed', () => {
  metrics.ordersFailed++;
});

// Expose metrics endpoint
app.get('/metrics', (req, res) => {
  res.json(metrics);
});

Built for game developers, by game developers.