Advanced Plugin Examples
Real-world examples and patterns for building production-ready Jiffoo Mall plugins
Advanced Plugin Examples
This guide provides comprehensive, real-world examples for building production-ready plugins for Jiffoo Mall.
Multi-Tenant Payment Gateway
A complete payment gateway plugin with multi-tenant support, webhook handling, and error recovery.
import { Plugin, PluginContext, createCache, createLogger } from '@jiffoo/plugin-sdk';
import Stripe from 'stripe';
const logger = createLogger({ name: 'stripe-gateway' });
const cache = createCache({ type: 'redis', ttl: 3600 });
const plugin = new Plugin({
manifest: {
slug: 'stripe-gateway',
name: 'Stripe Payment Gateway',
version: '2.0.0',
category: 'payment',
capabilities: [
'payment.process',
'payment.refund',
'payment.capture',
'webhook.handle',
],
configSchema: {
apiKey: {
type: 'secret',
label: 'Stripe Secret Key',
required: true,
description: 'Your Stripe secret API key',
},
webhookSecret: {
type: 'secret',
label: 'Webhook Signing Secret',
required: true,
},
environment: {
type: 'select',
label: 'Environment',
options: ['test', 'live'],
default: 'test',
},
autoCapture: {
type: 'boolean',
label: 'Auto-capture payments',
default: true,
},
},
},
});
// Installation handler
plugin.onInstall(async (ctx: PluginContext) => {
const { tenantId, config } = ctx;
try {
const stripe = new Stripe(config.apiKey, { apiVersion: '2023-10-16' });
// Create webhook endpoint
const webhook = await stripe.webhookEndpoints.create({
url: `https://plugins.jiffoo.com/${ctx.installationId}/webhook`,
enabled_events: [
'payment_intent.succeeded',
'payment_intent.payment_failed',
'charge.refunded',
],
});
// Store webhook ID in cache
await cache.set(`webhook:${tenantId}`, webhook.id);
logger.info('Plugin installed', { tenantId, webhookId: webhook.id });
return {
success: true,
metadata: {
webhookId: webhook.id,
webhookUrl: webhook.url,
},
};
} catch (error) {
logger.error('Installation failed', { tenantId, error });
throw new Error(`Failed to install plugin: ${error.message}`);
}
});
// Uninstallation handler
plugin.onUninstall(async (ctx: PluginContext) => {
const { tenantId, config } = ctx;
try {
const stripe = new Stripe(config.apiKey, { apiVersion: '2023-10-16' });
const webhookId = await cache.get(`webhook:${tenantId}`);
if (webhookId) {
await stripe.webhookEndpoints.del(webhookId);
await cache.del(`webhook:${tenantId}`);
}
logger.info('Plugin uninstalled', { tenantId });
return { success: true };
} catch (error) {
logger.error('Uninstallation failed', { tenantId, error });
throw error;
}
});
// Process payment
plugin.handle('POST', '/api/process-payment', async (ctx, req) => {
const { tenantId, config } = ctx;
const { amount, currency, orderId, metadata } = req.body;
try {
const stripe = new Stripe(config.apiKey, { apiVersion: '2023-10-16' });
const paymentIntent = await stripe.paymentIntents.create({
amount,
currency,
capture_method: config.autoCapture ? 'automatic' : 'manual',
metadata: {
tenantId,
orderId,
...metadata,
},
});
// Cache payment intent for webhook correlation
await cache.set(`payment:${paymentIntent.id}`, {
tenantId,
orderId,
amount,
currency,
}, 3600);
logger.info('Payment created', {
tenantId,
paymentIntentId: paymentIntent.id,
amount,
currency,
});
return {
success: true,
paymentIntentId: paymentIntent.id,
clientSecret: paymentIntent.client_secret,
status: paymentIntent.status,
};
} catch (error) {
logger.error('Payment processing failed', { tenantId, error });
return {
success: false,
error: error.message,
};
}
});
// Capture payment
plugin.handle('POST', '/api/capture-payment', async (ctx, req) => {
const { config } = ctx;
const { paymentIntentId, amountToCapture } = req.body;
try {
const stripe = new Stripe(config.apiKey, { apiVersion: '2023-10-16' });
const paymentIntent = await stripe.paymentIntents.capture(
paymentIntentId,
amountToCapture ? { amount_to_capture: amountToCapture } : {}
);
logger.info('Payment captured', { paymentIntentId });
return {
success: true,
status: paymentIntent.status,
amountCaptured: paymentIntent.amount_received,
};
} catch (error) {
logger.error('Capture failed', { paymentIntentId, error });
return {
success: false,
error: error.message,
};
}
});
// Refund payment
plugin.handle('POST', '/api/refund-payment', async (ctx, req) => {
const { config } = ctx;
const { paymentIntentId, amount, reason } = req.body;
try {
const stripe = new Stripe(config.apiKey, { apiVersion: '2023-10-16' });
const refund = await stripe.refunds.create({
payment_intent: paymentIntentId,
amount,
reason,
});
logger.info('Refund created', { paymentIntentId, refundId: refund.id });
return {
success: true,
refundId: refund.id,
amount: refund.amount,
status: refund.status,
};
} catch (error) {
logger.error('Refund failed', { paymentIntentId, error });
return {
success: false,
error: error.message,
};
}
});
// Webhook handler
plugin.handle('POST', '/webhook', async (ctx, req) => {
const { config } = ctx;
const sig = req.headers['stripe-signature'];
try {
const stripe = new Stripe(config.apiKey, { apiVersion: '2023-10-16' });
const event = stripe.webhooks.constructEvent(
req.rawBody,
sig,
config.webhookSecret
);
// Get cached payment data
const paymentData = await cache.get(`payment:${event.data.object.id}`);
switch (event.type) {
case 'payment_intent.succeeded':
await ctx.client.post('/webhooks/payment-success', {
tenantId: paymentData?.tenantId,
orderId: paymentData?.orderId,
paymentIntentId: event.data.object.id,
amount: event.data.object.amount,
});
break;
case 'payment_intent.payment_failed':
await ctx.client.post('/webhooks/payment-failed', {
tenantId: paymentData?.tenantId,
orderId: paymentData?.orderId,
paymentIntentId: event.data.object.id,
error: event.data.object.last_payment_error?.message,
});
break;
case 'charge.refunded':
await ctx.client.post('/webhooks/payment-refunded', {
tenantId: paymentData?.tenantId,
chargeId: event.data.object.id,
amount: event.data.object.amount_refunded,
});
break;
}
logger.info('Webhook processed', { type: event.type });
return { received: true };
} catch (error) {
logger.error('Webhook processing failed', { error });
return { received: false, error: error.message };
}
});
plugin.start(3000);Email Marketing Integration
An email marketing plugin with template management, scheduling, and analytics.
import { Plugin, createJobQueue, createDatabase } from '@jiffoo/plugin-sdk';
import { Resend } from 'resend';
const db = createDatabase({
type: 'postgres',
url: process.env.DATABASE_URL!,
});
const queue = createJobQueue({
name: 'email-queue',
redis: {
host: process.env.REDIS_HOST,
port: parseInt(process.env.REDIS_PORT || '6379'),
},
});
const plugin = new Plugin({
manifest: {
slug: 'email-marketing',
name: 'Email Marketing Suite',
version: '1.0.0',
category: 'marketing',
capabilities: ['email.send', 'email.schedule', 'analytics.track'],
},
});
// Send email
plugin.handle('POST', '/api/send-email', async (ctx, req) => {
const { to, templateId, variables, scheduled } = req.body;
try {
// Get template
const template = await db.query(
'SELECT * FROM email_templates WHERE id = $1 AND tenant_id = $2',
[templateId, ctx.tenantId]
);
if (!template.rows[0]) {
return { success: false, error: 'Template not found' };
}
// Schedule or send immediately
if (scheduled) {
await queue.add(
'send-scheduled-email',
{
tenantId: ctx.tenantId,
to,
template: template.rows[0],
variables,
},
{ delay: new Date(scheduled).getTime() - Date.now() }
);
return {
success: true,
scheduled: true,
scheduledFor: scheduled,
};
} else {
const result = await sendEmail(ctx, to, template.rows[0], variables);
return result;
}
} catch (error) {
ctx.logger.error('Send email failed', { error });
return { success: false, error: error.message };
}
});
// Create template
plugin.handle('POST', '/api/templates', async (ctx, req) => {
const { name, subject, html, variables } = req.body;
try {
const result = await db.query(
`INSERT INTO email_templates (tenant_id, name, subject, html, variables)
VALUES ($1, $2, $3, $4, $5) RETURNING id`,
[ctx.tenantId, name, subject, html, variables]
);
return {
success: true,
templateId: result.rows[0].id,
};
} catch (error) {
return { success: false, error: error.message };
}
});
// Track email opens
plugin.handle('GET', '/api/track/:emailId', async (ctx, req) => {
const { emailId } = req.params;
await db.query(
`INSERT INTO email_analytics (email_id, event_type, timestamp)
VALUES ($1, 'open', NOW())`,
[emailId]
);
// Return 1x1 transparent pixel
return {
headers: { 'Content-Type': 'image/png' },
body: Buffer.from(
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==',
'base64'
),
};
});
// Get analytics
plugin.handle('GET', '/api/analytics', async (ctx, req) => {
const { startDate, endDate } = req.query;
const result = await db.query(
`SELECT
COUNT(DISTINCT email_id) as total_sent,
COUNT(DISTINCT CASE WHEN event_type = 'open' THEN email_id END) as total_opened,
COUNT(DISTINCT CASE WHEN event_type = 'click' THEN email_id END) as total_clicked
FROM email_analytics
WHERE tenant_id = $1
AND timestamp BETWEEN $2 AND $3`,
[ctx.tenantId, startDate, endDate]
);
const stats = result.rows[0];
return {
success: true,
analytics: {
totalSent: parseInt(stats.total_sent),
totalOpened: parseInt(stats.total_opened),
totalClicked: parseInt(stats.total_clicked),
openRate:
stats.total_sent > 0
? (stats.total_opened / stats.total_sent) * 100
: 0,
clickRate:
stats.total_sent > 0
? (stats.total_clicked / stats.total_sent) * 100
: 0,
},
};
});
// Background job processor
queue.process('send-scheduled-email', async (job) => {
const { tenantId, to, template, variables } = job.data;
const ctx = await plugin.getContext(tenantId);
await sendEmail(ctx, to, template, variables);
});
// Helper function
async function sendEmail(ctx, to, template, variables) {
const resend = new Resend(ctx.config.apiKey);
// Replace variables in template
let html = template.html;
for (const [key, value] of Object.entries(variables)) {
html = html.replace(new RegExp(`{{${key}}}`, 'g'), value);
}
const result = await resend.emails.send({
from: ctx.config.fromEmail,
to,
subject: template.subject,
html,
});
// Store in database
await db.query(
`INSERT INTO sent_emails (tenant_id, template_id, recipient, email_id)
VALUES ($1, $2, $3, $4)`,
[ctx.tenantId, template.id, to, result.id]
);
ctx.logger.info('Email sent', { emailId: result.id, to });
return {
success: true,
emailId: result.id,
};
}
plugin.start(3000);Inventory Sync Plugin
Synchronize inventory across multiple sales channels with conflict resolution.
import { Plugin, createCache, createJobQueue } from '@jiffoo/plugin-sdk';
const cache = createCache({ type: 'redis' });
const queue = createJobQueue({ name: 'inventory-sync' });
const plugin = new Plugin({
manifest: {
slug: 'inventory-sync',
name: 'Multi-Channel Inventory Sync',
version: '1.0.0',
category: 'inventory',
capabilities: ['inventory.sync', 'inventory.reserve'],
},
});
// Sync inventory from external source
plugin.handle('POST', '/api/sync', async (ctx, req) => {
const { source, products } = req.body;
try {
const results = [];
for (const product of products) {
const result = await syncProduct(ctx, source, product);
results.push(result);
}
// Queue background job for full sync
await queue.add('full-inventory-sync', {
tenantId: ctx.tenantId,
source,
});
return {
success: true,
synced: results.length,
results,
};
} catch (error) {
ctx.logger.error('Inventory sync failed', { error });
return { success: false, error: error.message };
}
});
// Reserve inventory with optimistic locking
plugin.handle('POST', '/api/reserve', async (ctx, req) => {
const { productId, quantity, orderId } = req.body;
const lockKey = `lock:${ctx.tenantId}:${productId}`;
const inventoryKey = `inventory:${ctx.tenantId}:${productId}`;
try {
// Acquire lock with retry
const lock = await acquireLock(lockKey, 5000);
if (!lock) {
return {
success: false,
error: 'Unable to acquire inventory lock',
};
}
// Get current inventory
const current = await cache.get(inventoryKey);
const available = current?.available || 0;
if (available < quantity) {
await releaseLock(lockKey);
return {
success: false,
error: 'Insufficient inventory',
available,
};
}
// Update inventory
await cache.set(inventoryKey, {
available: available - quantity,
reserved: (current?.reserved || 0) + quantity,
lastUpdated: Date.now(),
});
// Create reservation record
await ctx.client.post('/inventory/reservations', {
tenantId: ctx.tenantId,
productId,
orderId,
quantity,
expiresAt: Date.now() + 15 * 60 * 1000, // 15 minutes
});
await releaseLock(lockKey);
ctx.logger.info('Inventory reserved', {
productId,
quantity,
orderId,
});
return {
success: true,
reserved: quantity,
available: available - quantity,
};
} catch (error) {
await releaseLock(lockKey);
ctx.logger.error('Reservation failed', { error });
return { success: false, error: error.message };
}
});
// Release expired reservations
queue.process('release-expired-reservations', async (job) => {
const { tenantId } = job.data;
const expired = await plugin.client.get(
`/inventory/reservations/expired?tenantId=${tenantId}`
);
for (const reservation of expired.data) {
const inventoryKey = `inventory:${tenantId}:${reservation.productId}`;
const lockKey = `lock:${tenantId}:${reservation.productId}`;
await acquireLock(lockKey, 5000);
const current = await cache.get(inventoryKey);
await cache.set(inventoryKey, {
available: current.available + reservation.quantity,
reserved: current.reserved - reservation.quantity,
lastUpdated: Date.now(),
});
await releaseLock(lockKey);
await plugin.client.delete(
`/inventory/reservations/${reservation.id}`
);
}
});
// Helper functions
async function acquireLock(key: string, timeout: number): Promise<boolean> {
const end = Date.now() + timeout;
while (Date.now() < end) {
const acquired = await cache.set(key, '1', 5, 'NX');
if (acquired) return true;
await new Promise((resolve) => setTimeout(resolve, 50));
}
return false;
}
async function releaseLock(key: string): Promise<void> {
await cache.del(key);
}
async function syncProduct(ctx, source, product) {
const inventoryKey = `inventory:${ctx.tenantId}:${product.id}`;
// Get current inventory
const current = await cache.get(inventoryKey);
// Detect conflicts
if (
current &&
current.lastUpdated > product.lastUpdated &&
current.source !== source
) {
ctx.logger.warn('Inventory conflict detected', {
productId: product.id,
source,
});
// Apply conflict resolution strategy (last-write-wins, source-priority, etc.)
// For this example, we use source priority
if (ctx.config.prioritySources?.includes(source)) {
await cache.set(inventoryKey, {
available: product.quantity,
reserved: current.reserved || 0,
source,
lastUpdated: Date.now(),
});
return { productId: product.id, updated: true, conflictResolved: true };
}
return { productId: product.id, updated: false, conflict: true };
}
// Update inventory
await cache.set(inventoryKey, {
available: product.quantity,
reserved: current?.reserved || 0,
source,
lastUpdated: Date.now(),
});
return { productId: product.id, updated: true };
}
plugin.start(3000);Analytics Dashboard Plugin
Real-time analytics dashboard with custom metrics and reporting.
import { Plugin, createDatabase } from '@jiffoo/plugin-sdk';
import { Parser } from 'json2csv';
const db = createDatabase({
type: 'postgres',
url: process.env.DATABASE_URL!,
});
const plugin = new Plugin({
manifest: {
slug: 'analytics-dashboard',
name: 'Advanced Analytics',
version: '1.0.0',
category: 'analytics',
capabilities: ['analytics.track', 'analytics.report'],
},
});
// Track custom event
plugin.handle('POST', '/api/track', async (ctx, req) => {
const { event, properties, userId, timestamp } = req.body;
try {
await db.query(
`INSERT INTO analytics_events
(tenant_id, event, properties, user_id, timestamp)
VALUES ($1, $2, $3, $4, $5)`,
[
ctx.tenantId,
event,
JSON.stringify(properties),
userId,
timestamp || new Date(),
]
);
return { success: true };
} catch (error) {
return { success: false, error: error.message };
}
});
// Get dashboard metrics
plugin.handle('GET', '/api/dashboard', async (ctx, req) => {
const { startDate, endDate } = req.query;
try {
// Revenue metrics
const revenue = await db.query(
`SELECT
SUM(properties->>'amount')::numeric as total_revenue,
COUNT(*) as order_count,
AVG(properties->>'amount')::numeric as average_order_value
FROM analytics_events
WHERE tenant_id = $1
AND event = 'order.completed'
AND timestamp BETWEEN $2 AND $3`,
[ctx.tenantId, startDate, endDate]
);
// User metrics
const users = await db.query(
`SELECT
COUNT(DISTINCT user_id) as active_users,
COUNT(DISTINCT CASE WHEN properties->>'isNewCustomer' = 'true'
THEN user_id END) as new_customers
FROM analytics_events
WHERE tenant_id = $1
AND timestamp BETWEEN $2 AND $3`,
[ctx.tenantId, startDate, endDate]
);
// Top products
const products = await db.query(
`SELECT
properties->>'productId' as product_id,
properties->>'productName' as product_name,
COUNT(*) as purchase_count,
SUM((properties->>'quantity')::integer) as total_quantity
FROM analytics_events
WHERE tenant_id = $1
AND event = 'product.purchased'
AND timestamp BETWEEN $2 AND $3
GROUP BY properties->>'productId', properties->>'productName'
ORDER BY purchase_count DESC
LIMIT 10`,
[ctx.tenantId, startDate, endDate]
);
return {
success: true,
metrics: {
revenue: {
total: parseFloat(revenue.rows[0].total_revenue || 0),
orderCount: parseInt(revenue.rows[0].order_count || 0),
averageOrderValue: parseFloat(
revenue.rows[0].average_order_value || 0
),
},
users: {
active: parseInt(users.rows[0].active_users || 0),
new: parseInt(users.rows[0].new_customers || 0),
},
topProducts: products.rows,
},
};
} catch (error) {
ctx.logger.error('Dashboard metrics failed', { error });
return { success: false, error: error.message };
}
});
// Export report as CSV
plugin.handle('GET', '/api/export', async (ctx, req) => {
const { reportType, startDate, endDate } = req.query;
try {
let data;
switch (reportType) {
case 'revenue':
const result = await db.query(
`SELECT
DATE(timestamp) as date,
SUM(properties->>'amount')::numeric as revenue,
COUNT(*) as orders
FROM analytics_events
WHERE tenant_id = $1
AND event = 'order.completed'
AND timestamp BETWEEN $2 AND $3
GROUP BY DATE(timestamp)
ORDER BY date`,
[ctx.tenantId, startDate, endDate]
);
data = result.rows;
break;
default:
return { success: false, error: 'Invalid report type' };
}
const parser = new Parser();
const csv = parser.parse(data);
return {
success: true,
headers: {
'Content-Type': 'text/csv',
'Content-Disposition': `attachment; filename="${reportType}-report.csv"`,
},
body: csv,
};
} catch (error) {
return { success: false, error: error.message };
}
});
plugin.start(3000);Best Practices
Error Recovery
// Implement exponential backoff for retries
async function withRetry<T>(
fn: () => Promise<T>,
maxRetries: number = 3
): Promise<T> {
let lastError;
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
lastError = error;
const delay = Math.pow(2, i) * 1000; // Exponential backoff
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
throw lastError;
}Health Monitoring
plugin.handle('GET', '/health', async (ctx, req) => {
const checks = {
database: false,
cache: false,
externalApi: false,
};
// Check database
try {
await db.query('SELECT 1');
checks.database = true;
} catch (error) {
ctx.logger.error('Database health check failed', { error });
}
// Check cache
try {
await cache.set('health-check', '1', 5);
checks.cache = true;
} catch (error) {
ctx.logger.error('Cache health check failed', { error });
}
// Check external API
try {
await externalApi.get('/health');
checks.externalApi = true;
} catch (error) {
ctx.logger.error('External API health check failed', { error });
}
const healthy = Object.values(checks).every((check) => check);
return {
status: healthy ? 'healthy' : 'unhealthy',
checks,
timestamp: new Date().toISOString(),
};
});Request Validation
import Joi from 'joi';
const processPaymentSchema = Joi.object({
amount: Joi.number().positive().required(),
currency: Joi.string().length(3).uppercase().required(),
orderId: Joi.string().uuid().required(),
metadata: Joi.object().optional(),
});
plugin.handle('POST', '/api/process-payment', async (ctx, req) => {
// Validate request body
const { error, value } = processPaymentSchema.validate(req.body);
if (error) {
return {
success: false,
error: 'Validation failed',
details: error.details,
};
}
// Process with validated data
return await processPayment(ctx, value);
});Next Steps
- Explore Troubleshooting Guide
- Read Plugin SDK Reference
- Join our Discord Community