We Built a Custom CRM for a 90-Agent Travel Aggregator During Their Diwali Slow Week — Here is the 3-Week Sprint Plan
A Jaipur travel aggregator (90 agents, 28k yearly bookings) shipped their CRM during the post-Diwali Oct 22 to Nov 14 slow window. The 4 modules we built first, the data-migration script, and the phased rollout.
Hrishikesh Baidya
October 25, 202514 min read
0%
A Jaipur-based travel aggregator running 90 agents across Rajasthan and Delhi NCR — 28,000 yearly bookings, ₹84 cr GMV — had been waiting 18 months to replace their patchwork CRM. The window opened on October 22, 2025 (the day after Bhai Dooj). Travel demand collapses in the post-Diwali, pre-winter-honeymoon slow window — Oct 22 to about Nov 14 is the deadest 3 weeks of the Indian travel calendar. Their CEO called us: "ship it before the December rush." We did. 4 core modules in 3 weeks, full data migration on day 19, soft launch on day 21. Here is the sprint plan, the 4-module phased rollout, and the data-migration script we use on every CRM project.
90
Agents (Jaipur HQ + 4 satellite offices)
28k
Annual bookings
3 weeks
Sprint duration (Oct 22 to Nov 12, 2025)
₹14.4L
Phase 1 fixed-price build
## TL;DR (60 Words)
3-week sprint, 4 modules shipped first: lead capture, itinerary builder, booking + payment, customer 360. Phased rollout: pilot at the Jaipur HQ on day 16, satellite offices on day 21. Data migration: a Node script that pulled 67,000 historical bookings from their old MySQL CRM, normalised to our Postgres schema, ran nightly for the 6-day overlap window. Year-1 ROI: ₹38 lakh in saved Zoho + manual rework.
## Why The Post-Diwali Window Is The Right Time
For Indian travel, Oct 22 to Nov 14 is the deadest 3 weeks of the calendar. Diwali ends. Winter honeymoon and December tour bookings start in earnest from Nov 15. The 3-week window between is when agents have time to learn a new tool. We have shipped 4 travel CRMs in this exact window across 3 years; the adoption curve is dramatically gentler than during peak season. If you can plan for it, the post-Diwali calendar is the ideal CRM cutover window for any Indian travel business.
## The Client (Specific Details)
- Sector: Mid-tier domestic + inbound travel aggregator (Rajasthan circuits, Golden Triangle, Khajuraho-Varanasi-Bodhgaya, custom honeymoon)
- Location: Jaipur HQ + satellite offices in Delhi (NCR), Udaipur, Jodhpur, Pushkar
- Size: 90 agents (62 sales + 18 ops + 10 finance), 28,000 yearly bookings, ₹84 cr GMV
- Stack on day 0: a 9-year-old PHP CRM (custom-built, original developer long gone), Tally for accounts, WhatsApp for everything urgent
- Trigger: The PHP CRM ran on PHP 5.6 on a single Windows VM. The CEO got tired of "the system is down again". The CFO got tired of agents losing customer history when the VM rebooted.
## The 4 Modules We Built First (Phase 1)
L
Module 1: Lead Capture (Days 1–4)
WhatsApp inbound form, website form, and a phone-call logger. Auto-assigns to the next available agent based on round-robin + agent specialisation (Rajasthan circuits vs Golden Triangle). De-dup by phone number across the 4 sources.
I
Module 2: Itinerary Builder (Days 5–10)
Drag-and-drop day-by-day builder with a hotel + experience catalogue. Auto-calculates margins per supplier. PDF export with the company branding for customer sign-off. Most-used module by week 3.
B
Module 3: Booking + Payment (Days 11–14)
Razorpay UPI Intent + bank-transfer flows. Generates supplier purchase orders automatically. Handles partial payments and the 50/50 advance-balance pattern Indian travel uses.
C
Module 4: Customer 360 (Days 15–18)
Single-page view of every customer: past bookings, payment history, WhatsApp thread, complaints, repeat-customer flag. The agent's "what does this customer remember" screen.
WhatsApp Business API via Gupshup (lead capture + itinerary share)
PMT
Razorpay UPI Intent + bank transfer reconciliation
## The 3-Week Sprint Plan (Day-by-Day)
1
Days 1–2: Discovery sprint
Two of us flew to Jaipur. Spent a full day each at the HQ + Delhi office. Shadowed 4 agents through their full booking cycle. Catalogued 18 must-have workflows. Got CEO + CFO + ops manager signed off on the 4-module Phase 1 scope on a single A4 page.
2
Days 3–4: Schema + lead capture
Postgres schema with explicit row-level RBAC (an agent sees only their leads + their team's leads + the unassigned pool). Lead capture from 4 sources (WhatsApp, website, phone, walk-in). Round-robin assignment with a Redis lock to prevent dual-assignment.
3
Days 5–10: Itinerary builder
Drag-and-drop with day-by-day cards. Hotel + experience catalogue (1,840 entries seeded from the existing CRM). Auto-margin calculation per supplier. PDF export via Puppeteer. Tested with 3 senior agents on day 10 — feedback led to a one-day catalogue-search overhaul.
4
Days 11–14: Booking + payment
Razorpay integration with UPI Intent + bank transfer + Razorpay Routes for vendor payouts. Supplier PO auto-generated on booking confirmation. Partial-payment ledger that tracks the 50/50 advance-balance pattern.
5
Days 15–18: Customer 360 + data migration
Customer 360 view with the WhatsApp thread embedded. Data migration script ran nightly for 6 nights (Days 13-18) on a 6-day overlap window. 67,000 historical bookings, 38,000 customers, 1.2 million WhatsApp messages migrated.
6
Days 19–20: Pilot at Jaipur HQ
62 Jaipur agents on the new system. Old system in read-only mode. Three of us on-site for 2 days. Day 19 logged 4 bugs (one critical: round-robin skipping the last agent). Day 20: zero bugs. Sign-off.
7
Day 21: Satellite office cutover
28 satellite agents (Delhi, Udaipur, Jodhpur, Pushkar) onboarded via 90-min Zoom training each. Old system fully decommissioned. 14-day support window starts.
## The Data-Migration Script (The Reusable Part)
This is the script we run on every CRM data migration. It is the most-asked-for snippet from our CRM clients.
javascript
// migrate-bookings.js - the nightly migration during the 6-day overlap window
const PROD_DELTA_HOURS = 24; // pull yesterday's changes only
const BATCH_SIZE = 5000;
async function migrateBookings() {
// Capture watermark BEFORE pulling
const lastSyncRaw = await fs.readFile('.last-sync', 'utf-8').catch(() => null);
const lastSync = lastSyncRaw ? new Date(lastSyncRaw) : new Date('2010-01-01');
const newSyncMark = new Date();
console.log(Pulling bookings updated since ${lastSync.toISOString()});
// Pull from old MySQL CRM in batches (LIMIT/OFFSET on a sortable key)
let offset = 0;
let total = 0;
while (true) {
const oldBookings = await mysqlPool.query(
SELECT b.*, c.phone, c.email, c.name as customer_name
FROM bookings b
LEFT JOIN customers c ON b.customer_id = c.id
WHERE b.updated_at > ?
ORDER BY b.id
LIMIT ? OFFSET ?
, [lastSync, BATCH_SIZE, offset]);
if (oldBookings.length === 0) break;
// Idempotent upsert into the new Postgres CRM
for (const b of oldBookings) {
await pgPool.query(
INSERT INTO bookings (
legacy_id, customer_phone, itinerary_json, total_amount,
payment_status, created_at, updated_at, agent_id, source
) VALUES ($1, $2, $3, $4, $5, $6, $7,
(SELECT id FROM agents WHERE legacy_id = $8),
$9
)
ON CONFLICT (legacy_id) DO UPDATE SET
itinerary_json = EXCLUDED.itinerary_json,
total_amount = EXCLUDED.total_amount,
payment_status = EXCLUDED.payment_status,
updated_at = EXCLUDED.updated_at
, [
b.id, b.phone,
JSON.stringify(parseLegacyItinerary(b.itinerary_blob)),
b.total_amount, b.payment_status,
b.created_at, b.updated_at,
b.agent_id, b.source || 'phone',
]);
}
total += oldBookings.length;
offset += BATCH_SIZE;
console.log(Migrated ${total} so far);
}
// Persist the watermark so next nightly run picks up correctly
await fs.writeFile('.last-sync', newSyncMark.toISOString());
console.log(Done. Total: ${total}. Next sync watermark: ${newSyncMark.toISOString()});
}
Three things to notice. The watermark is captured before pulling so we never miss rows updated during the migration. The upsert uses a legacy_id column so re-runs are idempotent. The itinerary blob — stored as a custom binary format in the old CRM — gets parsed into structured JSON we can query in Postgres. We have run variants of this script on 14 client migrations.
## What Happened in Week 1 of the New System
Outcome (Days 21–28, post-cutover): 1,420 leads captured, 184 itineraries built, 38 bookings closed, ₹74 lakh GMV processed. Agent adoption at Day 7: 87 of 90 agents using the new CRM as primary tool. 3 holdouts converted by Day 14 after a 1:1 walkthrough each. One bug caught (a Razorpay webhook retry duplicating a payment record), patched in 4 hours.
## The 14-Day Support Window (What We Build In)
Every CRM cutover comes with a 14-day support window where one of us is on WhatsApp full-time during business hours and a second engineer is on-call evening + weekend. Volume curve over 14 days, from this client and 9 others:
## Pre-Cutover Decision Checklist
Discovery sprint (2 days on-site at HQ + at least one satellite office)
4-module Phase 1 scope signed off on a single A4 page by CEO + CFO + ops manager
Postgres schema with row-level RBAC matrix signed off in writing
Data-migration script tested on a staging copy of the old CRM
6-day overlap window planned in the operational calendar (no festival, no month-end)
Round-robin lead-assignment Redis lock load-tested at 6x projected peak
WhatsApp Business API templates approved by Meta 2 weeks before cutover
Razorpay UPI Intent + bank transfer reconciliation tested with sample bookings
Pilot day-1 + day-2 schedule with all 3 engineers on-site
14-day support window staffed with one engineer on WhatsApp during business hours
## Common Mistakes (Each One Hurts)
Symptom: "Migration runs forever and breaks the old CRM." Cause: pulling all rows in a single query without batching. Fix: batch by primary key, run nightly during the overlap window, capture watermarks.
Symptom: "Round-robin assignment skips agents." Cause: race condition in the assignment counter. Fix: Redis SETNX lock per-team, with a 30-second TTL.
Symptom: "Agents revert to the old CRM after week 2." Cause: the new CRM is technically better but slower for the most-used workflow. Fix: instrument every workflow with timing in week 1; optimise the top-3 to be at least as fast as the old one.
Symptom: "Razorpay webhook fires twice and creates duplicate payments." Cause: webhook retry without idempotency key. Fix: every webhook handler is idempotent on the Razorpay payment_id.
Symptom: "Itinerary PDF generation crashes the server." Cause: Puppeteer instance not pooled, every PDF spins a new Chromium. Fix: pool of 4 Puppeteer instances; queue if all busy.
## When 3 Weeks Is The Wrong Timeline
Skip the 3-week sprint if (a) you have more than 8 must-have modules — split into 2 phased sprints, (b) your old CRM has more than 200,000 bookings — migration alone needs more time, or (c) your team has not done a CRM cutover before — the support burden is higher than expected. We have done 3-week sprints on 4 travel CRMs of this size; below 60 agents it shrinks to 2 weeks, above 120 agents it stretches to 5 weeks.
## A Detail That Saved Us On Day 19
On day 19 (the pilot day at Jaipur HQ) the round-robin lead-assignment skipped agent #62 every time. We had tested round-robin extensively. The bug was a 1-off in the modulo calculation when the agent count was a power of 2 plus one. Found it in 18 minutes by adding logging to the assignment function and watching live. Fixed in 4 minutes. The lesson: the agent who feels skipped will tell you in the first 30 minutes of pilot. Listen to the human signal before trusting the dashboard.
## FAQ
### Why did you pick the post-Diwali window?
Travel demand drops to 25% of normal between Bhai Dooj (Oct 22) and the start of December tour bookings (Nov 14-18). Agents have 3 weeks of bandwidth to learn a new tool. We have used this window 4 times on travel CRM cutovers; the adoption curve is gentler than during peak.
### How does the data-migration script handle deleted records?
The old CRM had a soft-delete pattern (deleted_at column). We migrate soft-deleted records into a separate "archive" schema rather than discard. Useful for audit and the rare "the customer says they booked with us in 2019" scenario.
### What about supplier integrations (hotels, transport)?
Phase 1 did not integrate with supplier APIs. The aggregator works mostly with regional hotel chains that confirm bookings via WhatsApp + email. Phase 2 (planned for Q1 2026) integrates with HotelHub and TBO. We deferred to keep the Phase 1 scope tight.
### How did you handle the Hindi/English UI?
Single i18n setup with next-intl. Every UI string has both Hindi and English. Agents toggle their preference per-account. Form labels and help text are translated; data fields (customer name, hotel name) are stored in their original language without translation.
### What is the row-level RBAC pattern?
Postgres views per agent that filter rows by ownership. Senior agents have an additional "team_lead" claim that widens the filter. CEO and CFO have a "global_read" claim. Every claim is auditable and the audit log captures every elevated read.
### How did you handle the existing WhatsApp thread history?
The old CRM stored WhatsApp messages in a JSON blob per booking. We parsed and migrated 1.2 million messages into a thread-per-customer schema in Postgres. This took 4 hours in the migration window. The customer 360 view shows the full thread with the new threads appended.
### What was the team for this build?
Three full-stack engineers (one lead on schema + lead-assignment, one on itinerary builder, one on payment + reconciliation), our designer at 0.4 FTE for the Hindi UI strings and the itinerary PDF template, our QA lead Manvi at 0.5 FTE for the pilot day. Our CTO Hrishikesh at 0.3 FTE for architecture review.
## Want a 3-Week CRM Sprint For Your Travel Business?
Need a 3-Week CRM Sprint for Your Travel/Tour Business?
We run 3-week CRM sprints for Indian travel agencies and tour operators in the 30-150 agent range. Phase 1 covers the 4 most-used modules. Fixed-price ₹8L-₹18L. We do the data migration, the cutover, and the 14-day support window. First call is with the engineer who would lead your build.