Jiffoo Docs

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

On this page