POS Integration

The Membership Module integrates seamlessly with BigLedger’s POS General Applet, enabling comprehensive membership functionality at physical checkout locations. This integration provides real-time member identification, automatic points accrual, redemption processing, and tier-based benefits application during point-of-sale transactions.

Integration Overview

POS integration enables cashiers to identify members, view their membership status, apply tier-based discounts, award points based on purchase amounts, process points redemptions, and complete transactions with full membership functionality without interrupting the checkout flow.

The integration operates through a combination of local POS terminal capabilities and cloud-based membership services, ensuring rapid response times while maintaining data accuracy and synchronization with the central membership system.

Member Identification Methods

Membership Card Scanning

Members can present physical membership cards with barcodes or QR codes for instant identification. The POS terminal scans the card, extracts the member ID, and retrieves the member profile from the membership system.

Barcode Format: Member IDs are encoded in Code 128 format, providing reliable scanning across standard barcode readers. The encoded value matches the member ID format MEM-{numeric-id}.

QR Code Format: QR codes contain JSON payloads with member ID and verification checksum:

{
  "memberId": "MEM-789012",
  "type": "membership",
  "checksum": "a7b3c9d2"
}

The POS system validates the checksum before accepting the member ID, preventing fraudulent card usage.

Phone Number Lookup

When members don’t have their cards, cashiers can look up membership accounts by phone number. The system searches for exact phone number matches and displays member information for cashier verification.

async function lookupMemberByPhone(phoneNumber) {
  const result = await api.request('GET', `/members?search=${phoneNumber}&limit=5`);

  if (result.data.length === 0) {
    return null;
  }

  if (result.data.length === 1) {
    return result.data[0];
  }

  // Multiple matches - cashier selects correct member
  return displayMemberSelection(result.data);
}

Email Address Lookup

Similar to phone lookup, members can provide email addresses for account identification. Email lookups perform case-insensitive matching and display results for cashier confirmation.

Mobile App Integration

Members with the BigLedger mobile app can present dynamic QR codes generated within the app. These codes expire after 60 seconds, providing enhanced security compared to static card codes.

The dynamic QR payload includes timestamp and signature:

{
  "memberId": "MEM-789012",
  "timestamp": 1705329600,
  "signature": "e8f4a2c7d9b1"
}

The POS terminal verifies the signature and timestamp before accepting the identification.

Points Earning at Checkout

Automatic Points Calculation

Points are calculated automatically based on purchase amount, tier multipliers, and active promotional rules. The POS terminal sends transaction details to the membership API, which applies all relevant rules and returns the calculated points.

async function calculatePoints(memberId, purchaseAmount, transactionItems) {
  const member = await getMemberDetails(memberId);

  // Base points calculation
  const baseEarnRate = 0.5; // 1 point per 2 MYR
  let basePoints = Math.floor(purchaseAmount * baseEarnRate);

  // Apply tier multiplier
  const tierMultipliers = {
    'BRONZE': 1.0,
    'SILVER': 1.25,
    'GOLD': 1.5,
    'PLATINUM': 2.0
  };

  const multiplier = tierMultipliers[member.tier] || 1.0;
  const totalPoints = Math.floor(basePoints * multiplier);

  return {
    basePoints: basePoints,
    tierMultiplier: multiplier,
    totalPoints: totalPoints,
    tier: member.tier
  };
}

Transaction Processing

When the transaction is completed, the POS terminal awards points to the member account:

async function completePurchaseWithPoints(memberId, transactionData) {
  // Calculate points
  const pointsCalc = await calculatePoints(
    memberId,
    transactionData.totalAmount,
    transactionData.items
  );

  // Award points
  const pointsResult = await api.request('POST', '/points/earn', {
    memberId: memberId,
    points: pointsCalc.totalPoints,
    reason: 'Purchase transaction',
    referenceId: transactionData.transactionId,
    metadata: {
      transactionAmount: transactionData.totalAmount,
      location: transactionData.storeLocation,
      earnRate: 0.5,
      tierMultiplier: pointsCalc.tierMultiplier
    }
  });

  // Print points summary on receipt
  return {
    success: true,
    pointsEarned: pointsCalc.totalPoints,
    previousBalance: pointsResult.data.previousBalance,
    newBalance: pointsResult.data.newBalance,
    transactionId: pointsResult.data.transactionId
  };
}

Category-Based Multipliers

Different product categories may have different earning rates. The POS system applies category multipliers when calculating points:

function calculateCategoryPoints(transactionItems) {
  const categoryMultipliers = {
    'electronics': 1.0,
    'fashion': 1.5,
    'grocery': 0.5,
    'beauty': 1.25
  };

  let totalPoints = 0;

  transactionItems.forEach(item => {
    const basePoints = item.amount * 0.5;
    const multiplier = categoryMultipliers[item.category] || 1.0;
    const itemPoints = Math.floor(basePoints * multiplier);
    totalPoints += itemPoints;
  });

  return totalPoints;
}

Points Redemption

Redemption Workflow

Members can redeem points for discounts or rewards during checkout. The POS system validates available points, processes the redemption, and applies the discount to the current transaction.

Step 1: Display Available Redemptions

The POS terminal queries available rewards based on the member’s points balance and tier:

async function getAvailableRedemptions(memberId) {
  const balance = await api.request('GET', `/points/balance/${memberId}`);
  const rewards = await api.request('GET', '/config/rewards?available=true');

  // Filter rewards member can afford
  const availableRewards = rewards.data.filter(reward => {
    return reward.pointsCost <= balance.data.currentPoints &&
           isEligibleForReward(balance.data.tier, reward.minimumTier);
  });

  return availableRewards;
}

function isEligibleForReward(memberTier, requiredTier) {
  const tierHierarchy = ['BRONZE', 'SILVER', 'GOLD', 'PLATINUM'];
  const memberLevel = tierHierarchy.indexOf(memberTier);
  const requiredLevel = tierHierarchy.indexOf(requiredTier);

  return memberLevel >= requiredLevel;
}

Step 2: Process Redemption

When the member selects a reward, the POS system processes the redemption:

async function processRedemption(memberId, rewardId, transactionId) {
  // Get reward details
  const rewards = await api.request('GET', '/config/rewards');
  const reward = rewards.data.find(r => r.rewardId === rewardId);

  if (!reward) {
    throw new Error('Reward not found');
  }

  // Process redemption
  const redemptionResult = await api.request('POST', '/points/redeem', {
    memberId: memberId,
    points: reward.pointsCost,
    rewardId: rewardId,
    reason: reward.name,
    referenceId: transactionId
  });

  return {
    success: true,
    pointsRedeemed: reward.pointsCost,
    newBalance: redemptionResult.data.newBalance,
    rewardName: reward.name,
    discountAmount: reward.value,
    voucherCode: redemptionResult.data.voucherCode
  };
}

Step 3: Apply Discount

The POS system applies the redemption discount to the current transaction:

function applyRedemptionDiscount(transaction, redemption) {
  transaction.discounts.push({
    type: 'points_redemption',
    description: redemption.rewardName,
    amount: redemption.discountAmount,
    voucherCode: redemption.voucherCode
  });

  transaction.totalAmount -= redemption.discountAmount;

  // Ensure total doesn't go negative
  if (transaction.totalAmount < 0) {
    transaction.totalAmount = 0;
  }

  return transaction;
}

Tier-Based Benefits

Automatic Discount Application

Members receive tier-based discounts automatically when identified at checkout:

async function applyTierDiscount(memberId, purchaseAmount) {
  const member = await getMemberDetails(memberId);

  const tierDiscounts = {
    'BRONZE': 0,
    'SILVER': 5,
    'GOLD': 10,
    'PLATINUM': 15
  };

  const discountPercentage = tierDiscounts[member.tier] || 0;
  const discountAmount = purchaseAmount * (discountPercentage / 100);

  return {
    tier: member.tier,
    discountPercentage: discountPercentage,
    discountAmount: discountAmount,
    finalAmount: purchaseAmount - discountAmount
  };
}

Exclusive Offers

Certain products or promotions may be exclusive to specific tiers. The POS system validates tier eligibility and applies exclusive offers:

function validateTierExclusiveOffer(memberTier, offerRequirement) {
  const tierHierarchy = ['BRONZE', 'SILVER', 'GOLD', 'PLATINUM'];
  const memberLevel = tierHierarchy.indexOf(memberTier);
  const requiredLevel = tierHierarchy.indexOf(offerRequirement);

  if (memberLevel >= requiredLevel) {
    return {
      eligible: true,
      message: 'Tier-exclusive offer applied'
    };
  }

  return {
    eligible: false,
    message: `This offer requires ${offerRequirement} tier or higher`
  };
}

Offline Capability

POS terminals may experience network connectivity issues. The integration includes offline capability to ensure uninterrupted service.

Local Caching

Member profiles and points balances are cached locally on the POS terminal:

class OfflineCache {
  constructor() {
    this.cache = new Map();
    this.pendingSync = [];
  }

  cacheMember(member) {
    this.cache.set(member.memberId, {
      data: member,
      cachedAt: Date.now(),
      synced: true
    });
  }

  getMember(memberId) {
    const cached = this.cache.get(memberId);
    if (!cached) return null;

    // Cache expires after 1 hour
    if (Date.now() - cached.cachedAt > 3600000) {
      return null;
    }

    return cached.data;
  }

  queueTransaction(transaction) {
    this.pendingSync.push({
      ...transaction,
      timestamp: Date.now(),
      synced: false
    });
  }

  async syncPendingTransactions() {
    const pending = this.pendingSync.filter(t => !t.synced);

    for (const transaction of pending) {
      try {
        await syncTransactionToCloud(transaction);
        transaction.synced = true;
      } catch (error) {
        console.error('Sync failed for transaction:', transaction.transactionId);
      }
    }

    // Remove synced transactions older than 24 hours
    this.pendingSync = this.pendingSync.filter(t =>
      !t.synced || (Date.now() - t.timestamp < 86400000)
    );
  }
}

const offlineCache = new OfflineCache();

Offline Transaction Processing

When offline, the POS terminal processes transactions using cached data and queues updates for synchronization:

async function processTransactionOffline(memberId, transactionData) {
  const cachedMember = offlineCache.getMember(memberId);

  if (!cachedMember) {
    throw new Error('Member profile not available offline');
  }

  // Calculate points using cached tier information
  const pointsCalc = calculatePoints(
    memberId,
    transactionData.totalAmount,
    transactionData.items,
    cachedMember.tier
  );

  // Queue transaction for sync
  offlineCache.queueTransaction({
    type: 'points_earn',
    memberId: memberId,
    points: pointsCalc.totalPoints,
    transactionId: transactionData.transactionId,
    amount: transactionData.totalAmount
  });

  return {
    success: true,
    offline: true,
    pointsEarned: pointsCalc.totalPoints,
    message: 'Transaction will be synchronized when connection is restored'
  };
}

Synchronization

When connectivity is restored, pending transactions are synchronized with the cloud:

async function syncTransactionToCloud(transaction) {
  if (transaction.type === 'points_earn') {
    await api.request('POST', '/points/earn', {
      memberId: transaction.memberId,
      points: transaction.points,
      reason: 'Purchase transaction (offline sync)',
      referenceId: transaction.transactionId,
      metadata: {
        offlineSync: true,
        originalTimestamp: transaction.timestamp
      }
    });
  }
}

// Periodic sync check
setInterval(async () => {
  if (isOnline()) {
    await offlineCache.syncPendingTransactions();
  }
}, 30000); // Every 30 seconds

Receipt Integration

Membership information is printed on customer receipts including points earned, current balance, tier status, and expiring points notifications.

function generateMembershipReceiptSection(member, transaction) {
  const receiptLines = [];

  receiptLines.push('MEMBERSHIP DETAILS');
  receiptLines.push(`Member: ${member.firstName} ${member.lastName}`);
  receiptLines.push(`Tier: ${member.tierName}`);
  receiptLines.push('');

  if (transaction.tierDiscount) {
    receiptLines.push(`Tier Discount (${transaction.tierDiscount.percentage}%): -RM${transaction.tierDiscount.amount.toFixed(2)}`);
  }

  if (transaction.pointsEarned) {
    receiptLines.push(`Points Earned: ${transaction.pointsEarned}`);
  }

  if (transaction.pointsRedeemed) {
    receiptLines.push(`Points Redeemed: -${transaction.pointsRedeemed}`);
  }

  receiptLines.push(`Points Balance: ${transaction.newBalance}`);

  if (member.expiringPoints > 0) {
    receiptLines.push('');
    receiptLines.push(`NOTICE: ${member.expiringPoints} points expiring on ${member.nextExpirationDate}`);
  }

  return receiptLines.join('\n');
}

Performance Considerations

POS integrations require minimal latency to avoid slowing checkout processes. Optimize performance through local caching of member data, asynchronous API calls for non-critical operations, connection pooling for API requests, and progressive loading of member details.

The member lookup should complete within 500 milliseconds, points calculation within 200 milliseconds, and redemption processing within 1 second to maintain smooth checkout flow.

Security Measures

POS terminals implement security measures including encrypted communication using TLS 1.2 or higher, secure credential storage for API keys, cashier authentication for sensitive operations, and audit logging of all membership transactions.

API keys used by POS terminals have restricted permissions limited to member lookup, points operations, and redemption processing. Administrative functions are not accessible from POS terminals.

Testing and Validation

Before deploying POS integration to production, thoroughly test member identification using all supported methods, points calculation across different scenarios, redemption processing including edge cases, offline capability and synchronization, receipt generation with membership details, and error handling for network failures.

Use the sandbox environment for integration testing with synthetic member data that won’t affect production records. The sandbox provides realistic response times and error conditions for comprehensive testing.

Troubleshooting

Common issues and resolutions include member not found errors (verify member ID format and check account status), points calculation discrepancies (confirm tier multiplier and category rules), redemption failures (check points balance and reward availability), offline sync failures (review transaction queue and connectivity logs), and slow response times (optimize caching strategy and check network performance).

POS integration logs should capture all API interactions, including request IDs returned by the membership API. These request IDs enable support teams to trace issues through the entire system for rapid resolution.