Monthly Capping System — Technical Documentation

Zoho CRM Automation


Table of Contents

  1. System Overview

  2. Global Capping Toggle

  3. Capping Data Structure

  4. How Capping is Applied on Invoice Sync

  5. FIFO Point Allocation Logic

  6. Capping Status on Invoice

  7. How Retailer Capping is Updated (Point Transaction Flow)

  8. Capping Snapshot

  9. Edge Cases & Rules

  10. Field Reference


1. System Overview

The Monthly Capping System limits how many loyalty points a Retailer can earn in a single calendar month. It works as a soft cap — points are still calculated in full, but only allocated up to the retailer's remaining monthly limit using a FIFO (First In, First Out) strategy across invoice line items.

The system is designed to be:

  • Globally switchable — a single Org Variable turns capping ON or OFF for all retailers

  • Snapshot-based — the limit is captured at invoice processing time, not re-read on every run

  • Non-destructive — rejected or uncapped items are cleanly reset without disturbing already-applied point debits

Invoice Synced
      │
      ▼
Global Capping ON?
   ├── NO  → Unlimited points applied to all approved items
   └── YES → Read retailer's remaining monthly limit
                  │
                  ▼
            Apply FIFO across approved line items
                  │
                  ├── Within limit  → Full points applied
                  ├── Partial limit → Partial points applied (Capped = true)
                  └── Limit = 00 points applied (Fully Capped)
                  │
                  ▼
         Create Points_Transaction records (Credit / Debit)
                  │
                  ▼
         When TXN marked Done → Update Retailer Monthly Capping record


2. Global Capping Toggle

Capping is controlled by a Zoho CRM Org Variable named Retailer_Capping.

capping_enabled = zoho.crm.getOrgVariable("Retailer_Capping");

if(capping_enabled == null) {
    capping_enabled = "OFF";
} else {
    capping_enabled = capping_enabled.toString().trim();
}

Variable Value

Behaviour

ON

Monthly capping is enforced for all retailers

OFF (or not set)

Capping is disabled; unlimited points are applied

To enable capping: Go to Zoho CRM → Settings → CRM Variables and set Retailer_Capping to ON.

When capping is OFF, the remaining limit is set to a virtually unlimited value internally so the rest of the FIFO logic runs unchanged:

// Capping OFF → effectively unlimited
remainingRetailer = 999999999999;
remainingDealer   = 999999999999;


3. Capping Data Structure

Capping details per retailer per month are stored and fetched via the getRetailerCappingDetails function, which returns:

Key

Description

monthlyLimit

Total points the retailer is allowed this month

remainingProvisionalLimit

Remaining limit for Provisional point bucket

remainingActualLimit

Remaining limit for Actual point bucket

remainingBonusLimit

Remaining limit for Bonus point bucket

The effective remaining limit used for FIFO is the minimum across all three buckets:

minRemainingLimit = remainingProvisionalLimit;

if(remainingActualLimit < minRemainingLimit) {
    minRemainingLimit = remainingActualLimit;
}
if(remainingBonusLimit < minRemainingLimit) {
    minRemainingLimit = remainingBonusLimit;
}

remainingLimit = minRemainingLimit;

Why the minimum? A retailer must have headroom in all three buckets simultaneously. The tightest bucket governs how many points can be safely allocated.


4. How Capping is Applied on Invoice Sync

The function SyncInvoiceUpdatePointRetailer runs when a Synced_Invoice record is updated. Here is the end-to-end flow:

Step 1 — Fetch Invoice & Validate

  • Load the Synced_Invoice record by ID

  • Extract Retailer, Retailer_State, and Dealer

  • Exit early if any of these are missing

Step 2 — Read Retailer Type

retailer_data = zoho.crm.getRecordById("Contacts", retailer_id);
Retailer_Type = retailer_data.get("Retailer_Type");  // e.g. "Gold", "Silver", "Platinum"

Retailer_Type determines which Product_Point master record is used to look up per-colour point rates.

Step 3 — Load or Reuse Capping Snapshot

monthlyLimit    = invoice.get("Monthly_Limit");
remainingLimit  = invoice.get("Remaining_Limit");

if(monthlyLimit == null || remainingLimit == null) {
    // Fresh snapshot — call capping details function
    cappingData    = automation.getRetailerCappingDetails(retailer_id, Retailer_Type);
    remainingLimit = min(remainingProvisional, remainingActual, remainingBonus);

    // Persist snapshot to invoice
    zoho.crm.updateRecord("Synced_Invoices", id, {
        "Monthly_Limit":        monthlyLimit,
        "Remaining_Limit":      remainingLimit,
        "Capping_Snapshot_Time": zoho.currenttime
    });
} else {
    // Reuse previously stored snapshot — no re-fetch needed
}

See Section 8 — Capping Snapshot for why snapshots are used.

Step 4 — Load Point Rate Master

The function looks up the Product_Point module filtered by Retailer_Type and State. It builds two lookup maps:

base_point_map  →  { "SABRANG": 2.5, "GPGC": 3.0, ... }   // retailer points per MT
dealer_map      →  { "SABRANG": 1.0, "GPGC": 1.5, ... }   // dealer points per MT

Step 5 — Process Each Line Item

For every item in the Invoice_Information subform:

  • Determine the unit (kg, mt, sqft, sqmt) and convert quantity to metric tonnes

  • Extract colour and thickness from Match_Results

  • Calculate raw retailer and dealer points using the rate map

  • Only items that meet all three conditions are approved for point allocation:

Condition

Check

Valid unit

kg, mt, sqft, or sqmt

Scan source

Contains gemini or zoho

Item status

Approved


5. FIFO Point Allocation Logic

Approved items are allocated points in the order they appear (FIFO). The remaining limit acts as a running budget that decreases with each allocation.

remaining = remainingLimit  (from snapshot, or 999...9 if capping OFF)

for each approved item (in order):

    if remaining >= item_points:
        applied = item_points        ← full points, not capped
        remaining -= item_points

    elif remaining > 0:
        applied = remaining          ← partial points, item IS capped
        remaining = 0

    else:
        applied = 0                  ← fully capped, no points

Delta Calculation (Idempotent Updates)

Because the same invoice may be re-processed (e.g. when an item is approved or rejected after the first run), the system computes a delta against previously applied values:

retailer_delta = new_applied - old_applied   // positive = more points to credit
dealer_delta   = new_dealer_applied - old_dealer_applied

// Accumulate into totals:
if(retailer_delta > 0)  → total_add_points    += retailer_delta   // Credit TXN
if(retailer_delta < 0)  → total_deduct_points += abs(delta)       // Debit TXN

This means the function only raises transactions for the change, not the full amount — preventing double-crediting on re-runs.


6. Capping Status on Invoice

After FIFO allocation, the invoice's Capping_Status field is set based on the approved items:

Condition

Capping_Status

No approved items

No Capped

All approved items hit the cap

Fully Capped

Some approved items hit the cap

Partial Capped

No approved items were capped

No Capped

if(total_items == 0)                        → "No Capped"
else if(capped_items == total_items)        → "Fully Capped"
else if(capped_items > 0)                   → "Partial Capped"
else"No Capped"

Only items with Status = Approved are counted — rejected and pending items are excluded from this calculation.


7. How Retailer Capping is Updated (Point Transaction Flow)

The capping balance is not deducted at invoice processing time. Instead, it is maintained through the Points_Transaction lifecycle:

Invoice Sync runs
      │
      ▼
Points_Transaction record created
  (TXN_Type = Credit/Debit, Points_Type = Provisional)
      │
      ▼
Transaction marked as Done (Processing_Status = Done)
      │
      ▼
Capping record for current month updated:
  → Used_Points  += credited amount
  → Remaining_*  -= credited amount
      │
      ▼
Notification sent to Retailer

Transaction Records Created

At the end of SyncInvoiceUpdatePointRetailer, up to four Points_Transaction records may be created:

Condition

Record Created

Fields

total_add_points > 0

Retailer Credit

TXN_Type = Credit, Points_Type = Provisional

total_deduct_points > 0

Retailer Debit

TXN_Type = Debit, Points_Type = Provisional

dealer_total_add_points > 0

Dealer Credit

TXN_Type = Credit, Points_Type = Provisional

dealer_total_deduct_points > 0

Dealer Debit

TXN_Type = Debit, Points_Type = Provisional

All transactions are created with:

optionalMap = {"trigger": {"workflow", "blueprint", "approval"}};

This ensures downstream workflows (including the capping balance update) fire automatically.

Rejected Item Handling

When an item's status changes to Rejected and it previously had points applied, a Debit transaction is raised for the previously applied amount:

if(current_status == "Rejected" && old_applied > 0) {
    total_deduct_points += old_applied;   // raises a Debit TXN
    row.put("Point_Debit", true);         // flags the line item
    row.put("Applied_Points", "0");       // resets the line item
}


8. Capping Snapshot

Why Snapshots Exist

The retailer's remaining limit changes every time a transaction is processed. If the function re-fetched live capping data on every invoice re-run, it could read a stale or already-decremented balance, causing incorrect point allocation.

Instead, the limit is snapshotted at first processing and stored on the invoice itself:

Field on Synced_Invoices

Purpose

Monthly_Limit

The retailer's total monthly cap at time of first processing

Remaining_Limit

The remaining balance at time of first processing

Capping_Snapshot_Time

Timestamp of when the snapshot was taken

Snapshot Rules

  • If Monthly_Limit and Remaining_Limit are both already set on the invoice → snapshot is reused, no API call made

  • If either is nullfresh snapshot is fetched from getRetailerCappingDetails and saved

  • The snapshot is never automatically refreshed after being stored — manual intervention or a new invoice record is required to reset it


9. Edge Cases & Rules

Scenario

Behaviour

Retailer_Capping org variable not set

Treated as OFF — unlimited points

Retailer_Type is blank

Function exits with log "Retailer Type null" — no points processed

No Product_Point found for Retailer_Type + State

Function exits — no points processed

Item unit is not kg, mt, sqft, or sqmt

Item skipped — not eligible for points

Thickness not found in thicknessWeightMap

wtPerSqFt = 0 → item earns 0 points

Remaining limit is 0 at start

All items get Applied_Points = 0, Capped = true

Invoice re-processed after items approved/rejected

Delta-only transactions raised — no double counting

Point_Debit flag already set on a non-rejected item

Flag is preserved as-is (not overwritten)

Dealer is missing from invoice

Function exits before any processing

Points_Transaction record created but TXN update fails

Capping balance not decremented; row stays in queue as Failed


10. Field Reference

Synced_Invoices — Capping Fields

Field

Type

Description

Monthly_Limit

Number

Retailer's total monthly cap (snapshot)

Remaining_Limit

Number

Remaining balance at snapshot time

Capping_Snapshot_Time

DateTime

When the snapshot was captured

Capping_Status

Picklist

No Capped / Partial Capped / Fully Capped

Invoice_Information Subform — Per-Item Fields

Field

Type

Description

Applied_Points

Decimal

Retailer points actually allocated (after capping)

Dealer_Applied_Points

Decimal

Dealer points actually allocated (after capping)

Points

Decimal

Raw calculated retailer points (before capping)

Dealer_Points

Decimal

Raw calculated dealer points (before capping)

Point_Added

Boolean

true if retailer points were allocated

Dealer_Point_Added

Boolean

true if dealer points were allocated

Capped

Boolean

true if retailer allocation was less than calculated

Dealer_Capped

Boolean

true if dealer allocation was less than calculated

Point_Debit

Boolean

true if a debit was raised for this item (rejected)

Dealer_Point_Debit

Boolean

true if a dealer debit was raised for this item

Points_Transaction — Created Fields

Field

Value Set

Retailer

Retailer record ID

Retailer_Email

Retailer email address

Dealer

Dealer record ID (dealer TXNs only)

Point

Computed delta amount

TXN_Type

Credit or Debit

Points_Type

Provisional

Order_Type

Invoices

Synced_Invoices

Invoice record ID (retailer TXNs only)


Monthly Capping System — Internal Technical Documentation — v1.0


On this page