Order Fulfillment
Automatically process and fulfill orders when payment is received.
Overview
This pattern shows how to build an automated order fulfillment system that:
- Receives payment notifications via webhook
- Delivers items to players
- Marks orders as fulfilled
Architecture
Pixlpay → Webhook → Your Server → Game Server
↓
Mark FulfilledComplete 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);
});