Monthly Capping System — Technical Documentation
Zoho CRM Automation
Table of Contents
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 = 0 → 0 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 |
|---|---|
| Monthly capping is enforced for all retailers |
| Capping is disabled; unlimited points are applied |
To enable capping: Go to Zoho CRM → Settings → CRM Variables and set
Retailer_CappingtoON.
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 |
|---|---|
| Total points the retailer is allowed this month |
| Remaining limit for Provisional point bucket |
| Remaining limit for Actual point bucket |
| 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_Invoicerecord by IDExtract
Retailer,Retailer_State, andDealerExit 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 tonnesExtract colour and thickness from
Match_ResultsCalculate 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 |
|
Scan source | Contains |
Item status |
|
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 |
|
|---|---|
No approved items |
|
All approved items hit the cap |
|
Some approved items hit the cap |
|
No approved items were 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 = Approvedare 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 |
|---|---|---|
| Retailer Credit |
|
| Retailer Debit |
|
| Dealer Credit |
|
| Dealer Debit |
|
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 | Purpose |
|---|---|
| The retailer's total monthly cap at time of first processing |
| The remaining balance at time of first processing |
| Timestamp of when the snapshot was taken |
Snapshot Rules
If
Monthly_LimitandRemaining_Limitare both already set on the invoice → snapshot is reused, no API call madeIf either is
null→ fresh snapshot is fetched fromgetRetailerCappingDetailsand savedThe 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 |
|---|---|
| Treated as |
| Function exits with log |
No | Function exits — no points processed |
Item unit is not | Item skipped — not eligible for points |
|
|
Remaining limit is 0 at start | All items get |
Invoice re-processed after items approved/rejected | Delta-only transactions raised — no double counting |
| Flag is preserved as-is (not overwritten) |
Dealer is missing from invoice | Function exits before any processing |
| Capping balance not decremented; row stays in queue as |
10. Field Reference
Synced_Invoices — Capping Fields
Field | Type | Description |
|---|---|---|
| Number | Retailer's total monthly cap (snapshot) |
| Number | Remaining balance at snapshot time |
| DateTime | When the snapshot was captured |
| Picklist |
|
Invoice_Information Subform — Per-Item Fields
Field | Type | Description |
|---|---|---|
| Decimal | Retailer points actually allocated (after capping) |
| Decimal | Dealer points actually allocated (after capping) |
| Decimal | Raw calculated retailer points (before capping) |
| Decimal | Raw calculated dealer points (before capping) |
| Boolean |
|
| Boolean |
|
| Boolean |
|
| Boolean |
|
| Boolean |
|
| Boolean |
|
Points_Transaction — Created Fields
Field | Value Set |
|---|---|
| Retailer record ID |
| Retailer email address |
| Dealer record ID (dealer TXNs only) |
| Computed delta amount |
|
|
|
|
|
|
| Invoice record ID (retailer TXNs only) |
Monthly Capping System — Internal Technical Documentation — v1.0