Skip to main content

Job Invoice Syncing System

Introduction

The Wrkbelt Job Invoice Syncing System provides automated, continuous synchronization of invoice data from vendor systems (ServiceTitan, etc.) to ensure accurate revenue tracking for service bookings. Unlike traditional one-time invoice syncing that only captures initial billing, our system implements a 3-month rolling window that continuously syncs all invoices for a job, capturing additional charges, adjustments, and final billing as work progresses.

The system transforms the booking lifecycle into reliable financial intelligence by automatically fetching invoice updates every 12 hours within a 3-month window following the first scheduled appointment. This approach ensures businesses have accurate, up-to-date revenue data without manual intervention or missed invoice updates.

Why This Matters

Imagine a service business books a plumbing job through Wrkbelt:

  • Day 1: Customer books appointment for "leaky faucet" - Initial estimate: $200
  • Day 2: Technician arrives and discovers additional pipe damage - Updated invoice: $800
  • Day 5: Final work completed with parts markup adjustments - Final invoice: $950

Traditional systems capture only the $200 initial estimate, underreporting revenue by $750.

Wrkbelt's system automatically syncs invoice updates twice daily for 3 months, ensuring the final $950 is accurately tracked without any manual intervention.

Architecture Overview

Design Principles

Our invoice syncing system is built on established software engineering patterns:

  • Strategy Pattern: Vendor-specific implementations with consistent interface
  • SOLID Principles: Single responsibility, dependency inversion, open/closed
  • DRY: Shared batch processing logic through composition
  • Rate Limiting: Vendor-specific throttling (ServiceTitan: 5 requests/second)
  • Dual Sync Flows: Separate initial and ongoing sync with distinct logic
  • Appointment-Based Decisions: Only syncs when no future appointments exist (ongoing flow)
  • 3-Month Rolling Window: Time-based syncing with automatic expiration
  • 12-Hour Cooldown: Prevents over-syncing while maintaining data freshness

System Architecture

Core Components

1. CRON Scheduler (BookingJobsService)

Responsibility: System-level orchestration

Key Behaviors:

  • Runs every 10 minutes to discover organizations with eligible bookings
  • Processes organizations sequentially to prevent resource contention
  • Delegates vendor-specific logic to strategy implementations
  • No business logic - pure orchestration layer

Critical Decision: 10-minute interval balances system responsiveness with infrastructure load. More frequent runs would not improve sync freshness due to the 12-hour cooldown.

2. Strategy Pattern (BookingVendorStrategy)

Responsibility: Vendor-specific invoice syncing with rate limiting control and appointment management

Key Design Principle: Full delegation of batch processing to strategy implementations. The strategy owns:

  • Rate limiting logic (vendor-specific constraints)
  • Batch size optimization
  • Error handling and partial failure recovery
  • API call pacing and throttling
  • Appointment checking and decision data return

Critical Decision: Strategy receives ALL bookings for an organization, not individual bookings. This enables vendor-optimized batch processing that can't be achieved with single-booking interfaces.

Interface Contracts:

Invoice Syncing:

syncJobInvoices(organizationId: string, bookings: Booking[]): Promise<JobInvoiceSyncResult[]>

Appointment Checking (Returns Decision Data):

updateBookingAppointments(
organizationId: string,
bookings: Booking[],
bookingService: BookingService
): Promise<BookingAppointmentUpdateResult[]>

Key Insight: The strategy returns hasFutureAppointment flag for each booking. The orchestrator uses this to make the decision:

  • Initial Sync: Skips appointment check entirely, always syncs invoices
  • Ongoing Sync: Calls appointment check first, only syncs bookings where hasFutureAppointment === false

This separation ensures initial dispatch fees are captured immediately while ongoing syncs respect appointment schedules.

The strategy returns results for ALL bookings (success or failure), ensuring the system can track partial failures without halting the entire batch.

3. ServiceTitan Implementation

Vendor Constraint: ServiceTitan allows 60 API requests per second per client (organization)

Strategic Throttling Decision: We intentionally limit invoice syncing to 5 bookings per batch to prioritize other critical API consumption:

  • Scheduler capacity checks: Real-time availability queries need immediate response
  • Booking flow operations: Customer-facing booking creation cannot be delayed
  • Invoice syncing: Non-urgent background task that can afford slower processing

Strategy Design:

  • Batch Size: 5 bookings per batch (intentionally conservative)
  • Parallel Execution: Process each batch concurrently (5 simultaneous API calls)
  • Inter-Batch Delay: 1-second wait between batches
  • Rate Limiting: Built into BaseServiceTitanClient (58 req/sec with safety margin)
  • Failure Isolation: Promise.allSettled ensures individual failures don't crash the batch

Performance Characteristics:

  • 100 bookings = 20 batches × 1 second = ~20 seconds total
  • Leaves 53+ requests/sec available for urgent operations
  • Graceful degradation under partial failures

Dual Sync Flow Architecture

The Business Problem

Service businesses face a common challenge: invoice amounts change as work progresses. Consider these scenarios:

  1. HVAC Repair: Initial diagnostic fee ($150) → Parts needed ($600) → Additional labor ($300) → Final: $1,050
  2. Plumbing Emergency: Quote for leak ($250) → Pipe replacement required ($1,200) → Final: $1,450
  3. Electrical Work: Inspection fee ($100) → Full rewiring needed ($3,500) → Permit adjustments ($200) → Final: $3,800

If we only capture the initial invoice, businesses lose visibility into thousands of dollars in actual revenue.

Our Solution: Dual Sync Flows

Two completely separate sync flows with distinct logic and decision-making:

Flow 1: Initial Sync (5 minutes after booking)

Purpose: Capture dispatch fees and initial estimates immediately

Characteristics:

  • Runs once per booking
  • No appointment checking - always syncs invoices
  • Triggered by scheduled_initial_sync_at field
  • Provides immediate revenue visibility
  • Query excludes bookings already synced (last_sync_at is null)

Why No Appointment Check? Dispatch fees and initial estimates exist immediately, even if appointments aren't scheduled yet. We want this data captured ASAP.

Flow 2: Ongoing Sync (3-month rolling window)

Purpose: Capture invoice updates throughout job lifecycle

Characteristics:

  • Runs every 12 hours for 90 days
  • Checks appointments FIRST - only syncs if no future appointments exist
  • Triggered by start_sync_at field (24h after first appointment)
  • Query requires last_sync_at to exist (initial sync already ran)
  • Automatically updates start_sync_at when new appointments detected

Decision Logic:

// STEP 1: Check all bookings for future appointments
const appointmentResults = await strategy.updateBookingAppointments(...);

// STEP 2: Filter - only sync bookings WITHOUT future appointments
const bookingsToSync = appointmentResults
.filter(result => !result.hasFutureAppointment)
.map(result => findBooking(result.bookingId));

// STEP 3: Sync invoices only for filtered bookings
if (bookingsToSync.length > 0) {
await strategy.syncJobInvoices(organizationId, bookingsToSync);
}

Why Check Appointments? If a booking has future appointments, work isn't complete yet. Invoice amounts may still change. We skip syncing to avoid capturing partial/interim invoices, then sync after the last appointment completes.

Window Boundaries

Initial Sync: scheduled_initial_sync_at = NOW + 5 minutes

  • Purpose: Capture immediate charges (dispatch fees, initial estimates)
  • Timing: Runs once shortly after booking creation
  • Business Value: Immediate revenue tracking for dispatched jobs

3-Month Window Start: start_sync_at = 24 hours after latest scheduled appointment

  • Dynamic Adjustment: Automatically updates if additional appointments are added
  • Rationale: Ensures all appointments complete before beginning ongoing syncs
  • Business Alignment: Captures actual work completion, not booking creation

Window End: start_sync_at + 90 days (3 months)

  • Rationale: Aligns with typical warranty periods and payment cycles
  • Automatic Expiration: No manual cleanup required; bookings age out naturally

Eligibility Criteria

Two Separate Repository Queries - Clean separation with no overlap:

Query 1: findBookingsForInitialJobInvoiceSync()

Returns bookings ready for their first sync (5 minutes after creation):

{
scheduled_initial_sync_at: { $lte: NOW },
last_sync_at: { $exists: false } OR last_sync_at === null,
processing_started_at: not locked
}

Key Point: Once synced, last_sync_at is set, so booking never appears in this query again. Initial sync runs exactly once.

Query 2: findBookingsForOngoingJobInvoiceSync()

Returns bookings in the 3-month rolling window for ongoing syncs:

{
start_sync_at: { $lte: NOW },
start_sync_at: { $gte: NOW - 90 days },
last_sync_at: { $exists: true, $ne: null, $lt: NOW - 12 hours },
processing_started_at: not locked
}

Key Points:

  • Requires last_sync_at to exist (initial sync already completed)
  • 12-hour cooldown prevents over-syncing
  • No status filter - syncs regardless of PENDING/SYNCED/FAILED status
  • Automatically ages out after 90 days

Critical Design Decision: These queries have zero overlap. A booking can never appear in both queries simultaneously:

  • Initial query requires last_sync_at === null
  • Ongoing query requires last_sync_at !== null

This clean separation eliminates any possibility of duplicate processing.

Timeline Visualization

Booking Lifecycle Example

Day 1 (Jan 1): Booking created

  • start_sync_at = null (not set yet)
  • last_sync_at = null
  • last_sync_status = PENDING

Day 2 (Jan 2): First appointment at 10:00 AM

  • System sets start_sync_at = Jan 3, 10:00 AM (24 hours after first appointment)

Day 3 (Jan 3, 10:00 AM): Window starts

  • CRON picks up booking (within 3-month window, never synced)
  • Fetches invoices from ServiceTitan
  • Updates: last_sync_at = Jan 3, 10:00 AM, last_sync_status = SYNCED

Day 3 (10:00 PM): 12 hours later

  • CRON picks up booking again (12-hour cooldown passed)
  • Syncs again to capture any new invoices
  • Updates: last_sync_at = Jan 3, 10:00 PM

Continues every 12 hours for 3 months...

Day 93 (April 3): Window closes

  • Booking NO LONGER appears in query (outside 3-month window)
  • Syncing stops automatically

Complete Sync Flow

CRON Orchestration (Every 10 Minutes)

The CRON job runs both sync flows sequentially for each organization:

@Cron(JOB_INVOICE_PROCESS_CRON)
async processPendingJobInitialInvoices() {
const organizationIds = await getOrganizationsWithPendingJobInvoice();

for (const organizationId of organizationIds) {
// FLOW 1: Initial sync (5 min after booking, no appointment check)
await this.processInitialJobInvoiceSyncForOrganization(organizationId);

// FLOW 2: Ongoing sync (3-month window, with appointment check)
await this.processOngoingJobInvoiceSyncForOrganization(organizationId);
}
}

Flow 1: Initial Sync Sequence

Flow 2: Ongoing Sync Sequence (With Appointment Check)

Rate Limiting & Performance

ServiceTitan Rate Limiting Strategy

API Constraint: 5 requests per second

Implementation:

// Batch size: 5 bookings per batch
const batches = this.createBatches(bookings, 5);

for (let i = 0; i < batches.length; i++) {
const batch = batches[i];

// Process batch in parallel (5 concurrent requests)
const batchResults = await Promise.allSettled(
batch.map((booking) => this.syncSingleJobInvoice(organizationId, booking))
);

// Wait 1 second before next batch
if (i < batches.length - 1) {
await delay(1000);
}
}

Performance Example:

  • 100 bookings = 20 batches
  • Time: ~20 seconds (1 second per batch)
  • API calls: 100 calls at intentionally throttled 5/batch rate
  • API capacity reserved for urgent operations: 53+ requests/sec available
  • No rate limit violations: ✅

Architectural Evolution

Previous Design (Sequential)

Flow: Job Service → Individual API calls → Individual DB updates

Limitations:

  • No rate limiting control
  • Job service owned vendor-specific timing logic
  • Single point of failure (one error stops all processing)
  • Inefficient for high-volume syncing

Current Design (Strategy-Driven Batching)

Flow: Job Service → Strategy (batch) → Parallel API calls → Bulk persistence

Key Improvements:

  • Separation of Concerns: Vendor logic encapsulated in strategies
  • Performance: 80% faster through parallelization
  • Resilience: Partial failures don't halt batch processing
  • Extensibility: New vendors = new strategy, zero changes to job service

Data Model

JobInvoice Type

export type JobInvoice = {
/**
* The invoice amount (total of all invoices)
*/
amount?: number;

/**
* The current sync status
*/
last_sync_status: JobInvoiceSyncStatus;

/**
* Number of sync attempts made
*/
attempts: number;

/**
* Timestamp of the last sync attempt
*/
last_attempt_at?: Date;

/**
* Error message if sync failed
*/
error_message?: string;

/**
* Timestamp when processing started (optimistic locking)
*/
processing_started_at?: Date;

/**
* Timestamp when the invoice was last successfully synced
* Used for audit logging and 12-hour cooldown
*/
last_sync_at?: Date;

/**
* Timestamp when the 3-month syncing window starts
* Set to 24 hours after the first scheduled appointment
*/
start_sync_at?: Date;
};

export enum JobInvoiceSyncStatus {
PENDING = 'pending',
FAILED = 'failed',
SYNCED = 'synced',
}

Key Fields Explained

start_sync_at: Defines when the 3-month rolling window begins

  • Set to 24 hours after first scheduled appointment
  • Window runs from start_sync_at to start_sync_at + 90 days
  • Bookings outside this window are excluded from syncing

last_sync_at: Tracks the last successful sync timestamp

  • Used for 12-hour cooldown logic
  • Prevents over-syncing (e.g., syncing every 10 minutes)
  • Only syncs if last_sync_at is null or > 12 hours ago

last_sync_status: Current sync status

  • PENDING: Never synced or ready for next sync
  • SYNCED: Successfully synced (but will sync again after 12 hours)
  • FAILED: Last attempt failed (will retry after 12 hours)

Important: Status does NOT prevent syncing within the 3-month window. Even SYNCED bookings are re-synced every 12 hours to capture invoice updates.

Configuration Constants

All syncing parameters are centralized in configuration constants:

// File: libs/api/services-api/src/lib/scheduler/booking/booking.constants.ts

/**
* Cron expression for processing job invoices
* Runs every 10 minutes
*/
export const JOB_INVOICE_PROCESS_CRON = CronExpression.EVERY_10_MINUTES;

/**
* Minimum time in milliseconds between job invoice syncs
* Set to 12 hours to avoid unnecessary API calls
*/
export const JOB_INVOICE_MIN_SYNC_INTERVAL_MS = 12 * 60 * 60 * 1000; // 12 hours

/**
* Duration of the 3-month rolling syncing window in milliseconds
* Bookings are synced every 12 hours for 3 months after start_sync_at
*/
export const JOB_INVOICE_SYNC_WINDOW_MS = 90 * 24 * 60 * 60 * 1000; // 90 days

/**
* Maximum time that a job invoice can be locked for processing
* Used for optimistic locking to prevent race conditions
*/
export const JOB_INVOICE_LOCK_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes

/**
* Maximum number of retry attempts for failed job invoices
*/
export const JOB_INVOICE_MAX_RETRY_ATTEMPTS = 3;

Complete Syncing Flow

Booking Creation → Invoice Syncing Lifecycle

Step 1: Booking Created (booking.service.ts)

// User books appointment for Jan 15, 2024 at 10:00 AM
const now = new Date(); // Jan 10, 2024 9:00 AM
const scheduledInitialSyncAt = new Date(now.getTime() + 5 * 60 * 1000); // Jan 10 9:05 AM
const selectedStartTime = new Date('2024-01-15T10:00:00Z');
const startSyncAt = new Date(selectedStartTime.getTime() + 24 * 60 * 60 * 1000); // Jan 16 10:00 AM

booking_vendor_data: {
job_id: 12345,
job_invoice: {
last_sync_status: JobInvoiceSyncStatus.PENDING,
attempts: 0,
scheduled_initial_sync_at: scheduledInitialSyncAt, // Jan 10 9:05 AM (for initial flow)
start_sync_at: startSyncAt, // Jan 16 10:00 AM (for ongoing flow)
}
}

Step 2: Initial Sync (5 minutes after booking - FLOW 1)

  • CRON picks up booking at 9:05 AM via findBookingsForInitialJobInvoiceSync()
  • NO appointment check - always syncs immediately
  • Captures dispatch fees and initial estimates ($75)
  • Sets last_sync_at, making booking ineligible for initial sync query
  • Booking now transitions to ongoing sync eligibility

Step 3: Ongoing Sync Begins (24h after appointment - FLOW 2)

  • Jan 16, 10:00 AM: start_sync_at reached
  • CRON picks up via findBookingsForOngoingJobInvoiceSync()
  • STEP 1: Checks appointments - no future appointments found
  • STEP 2: Filters bookings (this one passes - no future appointments)
  • STEP 3: Syncs invoices, captures updated total ($875)
  • Updates last_sync_at, enters 12-hour cooldown

Step 4: Dynamic Appointment Detection (ongoing sync logic)

// Jan 18: Service company adds follow-up appointment for Jan 25
// Jan 18, 10:00 PM: Next CRON run (12h after last sync)

// STEP 1: Check appointments
const appointmentResults = await strategy.updateBookingAppointments(...);
// Returns: { bookingId: '123', hasFutureAppointment: true, updatedStartSyncAt: Jan 26 }

// STEP 2: Filter bookings
const bookingsToSync = appointmentResults.filter(r => !r.hasFutureAppointment);
// Result: Empty array (this booking filtered out)

// STEP 3: Skip invoice sync
// Logged: "Skipping 1 bookings with future appointments"

Step 5: Resume After Last Appointment

  • Jan 25, 3:00 PM: Follow-up appointment completed
  • Jan 26, 3:00 PM: CRON runs again
  • STEP 1: Check appointments - no future appointments
  • STEP 2: Filter passes (no future appointments)
  • STEP 3: Syncs invoices, captures final total ($1,075)
  • Continues every 12 hours until Apr 26 (90 days)

Real-World Timeline

Jan 10, 9:00 AM: Customer books HVAC repair for Jan 15
Jan 10, 9:05 AM: ✅ Initial Sync (Flow 1) - Captures $75 dispatch fee
Jan 15, 10:00 AM: Technician arrives, diagnoses issue
Jan 15, 2:00 PM: Customer approves $800 repair
Jan 16, 10:00 AM: ✅ First Ongoing Sync (Flow 2) - Captures $875 total
Jan 16, 10:00 PM: ✅ Ongoing sync - Still $875
Jan 18, 5:00 PM: Service company schedules follow-up for Jan 25
Jan 18, 10:00 PM: ❌ Skipped - Future appointment detected
Jan 19, 10:00 AM: ❌ Skipped - Future appointment exists
...all syncs skipped until Jan 26...
Jan 25, 3:00 PM: Follow-up appointment adds $200 parts charge
Jan 26, 3:00 PM: ✅ Resume Sync - Captures final $1,075
Apr 26: Window closes, syncing stops automatically

Key Insights

  1. Dual Flow Separation: Initial and ongoing syncs have completely different logic and queries
  2. Appointment-Based Intelligence: Ongoing sync respects work schedules, doesn't sync mid-job
  3. Immediate Revenue Capture: Initial sync provides visibility within 5 minutes (no delays)
  4. Dynamic Adaptation: System pauses syncing when future work is detected, resumes automatically
  5. Zero Manual Intervention: All adjustments happen automatically based on vendor data

Error Handling & Resilience

Failure Isolation

Design Principle: Individual booking failures must not cascade to other bookings in the batch.

Implementation: Promise.allSettled instead of Promise.all

  • Each booking's sync runs independently
  • Failed bookings return error results, not thrown exceptions
  • Batch completes with mix of success/failure results

Critical Decision: Fail-fast is inappropriate for batch operations. The system prioritizes completing as many syncs as possible over stopping at the first error.

Retry Strategy

Automatic Retry Design:

  • Failed bookings remain eligible (still in 3-month window)
  • 12-hour cooldown prevents immediate retry spam
  • CRON naturally picks up failed bookings on subsequent runs
  • Maximum 3 attempts tracked to prevent infinite retries

Why This Works: No separate retry job needed. The same eligibility query that finds new bookings also finds failed ones that are ready for retry.

Optimistic Locking

Concurrency Problem: Multiple CRON instances or parallel processes could attempt to sync the same booking simultaneously.

Solution: Lock-based mutual exclusion

  • Acquire lock before processing (processing_started_at timestamp)
  • 5-minute lock timeout prevents deadlocks from crashed processes
  • Lock released on update (success or failure)

Critical Decision: Lock timeout balances protection (prevents concurrent processing) with availability (doesn't indefinitely block bookings if a process crashes).

Business Benefits

1. Complete Revenue Tracking

Traditional Approach: One-time initial invoice sync

  • Misses additional charges added later
  • Doesn't capture final billing after work completion
  • Manual reconciliation required

Our Approach: Continuous 3-month syncing

  • ✅ Captures all invoice updates automatically
  • ✅ Reflects true job revenue over lifecycle
  • ✅ No manual intervention needed

Example Scenario:

Day 1: Initial invoice = $500 (diagnosis fee)
Day 3: Additional work = $800 (parts + labor)
Day 5: Final invoice = $1,300 (total)

Traditional System: Records $500 (initial only) ❌
Wrkbelt System: Records $1,300 (final total) ✅

Impact: Accurate financial reporting, no underreporting

2. Prevents Revenue Leakage

Revenue Delta Tracking:

  • Initial sync captures partial revenue
  • Ongoing syncs capture additions, adjustments
  • Final revenue reflects complete job value

API Efficiency with 12-Hour Cooldown:

  • Without cooldown: 100 bookings × 144 CRON runs/day = 14,400 API calls/day
  • With cooldown: 100 bookings × 2 syncs/day = 200 API calls/day
  • Savings: 98% reduction in API calls

3. Vendor Relationship Health

Rate Limiting Benefits:

  • Prevents vendor API violations
  • Maintains vendor relationship health
  • No service interruptions from rate limit bans
  • Respectful API usage patterns

4. Automated Compliance

3-Month Window Alignment:

  • Matches typical service warranty periods
  • Captures post-service adjustments
  • Automatic window closure (no manual cleanup)
  • Compliant with accounting periods

Key Design Decisions

1. Why 10-Minute CRON Interval?

Trade-offs Considered:

  • More Frequent (e.g., every minute): Wastes resources; 12-hour cooldown means no benefit
  • Less Frequent (e.g., every hour): Delays initial syncs unnecessarily

Decision: 10 minutes balances responsiveness for new bookings with infrastructure efficiency.

2. Why 12-Hour Cooldown?

The Problem: If we checked every 10 minutes (our CRON interval), we'd make 144 checks per day per booking - wasteful and unnecessary.

Trade-offs Considered:

  • No Cooldown: 14,400 API calls/day per 100 bookings (excessive waste)
  • 24-Hour Cooldown: Misses same-day invoice updates when work completes
  • 6-Hour Cooldown: Still excessive for typical invoice update patterns
  • 12-Hour Cooldown: Sweet spot that captures morning/evening updates

Decision: 12 hours = 2 syncs per day (morning and evening) captures most invoice updates while achieving 98% API call reduction.

Real-World Benefit: If a technician completes work at 2pm and updates the invoice, our evening sync (within 12 hours) captures it the same day. If they update it at 10am, our afternoon sync catches it.

3. Why 3-Month Window?

Trade-offs Considered:

  • Indefinite Syncing: Accumulates bookings, never stops, database bloat
  • 1-Month Window: Misses warranty work and payment adjustments
  • 6-Month Window: Excessive; most service work completes within 90 days

Decision: 3 months aligns with industry standards for service completion and warranty periods.

4. Why Status-Agnostic Syncing?

Alternative Considered: Only sync bookings with status = PENDING

Problem: Once a booking syncs successfully (status → SYNCED), it would never sync again, missing all subsequent invoice updates.

Decision: Ignore status field for eligibility. All bookings in the time window with cooldown expired are eligible, regardless of previous sync status.

5. Why Delegate Batching to Strategies?

Alternative Considered: Job service controls batch size, strategies just handle individual bookings

Problem: Different vendors have different rate limits (ServiceTitan: 5/sec, HouseCall Pro: 10/sec). Centralized batching can't optimize for each vendor.

Decision: Strategy owns batch processing completely. ServiceTitan batches 5 at a time; future HouseCall Pro strategy can batch 10 at a time.

Future Enhancements

1. Multi-Vendor Support

Goal: Support diverse vendor platforms with varying rate limits and API characteristics

Design Pattern: Each vendor implements BookingVendorStrategy with vendor-optimized batching

Examples:

  • ServiceTitan: 60 requests/sec available → Throttle to 5/batch (reserves capacity for urgent operations)
  • HouseCall Pro: Different rate limits → Optimized batching per their constraints
  • Jobber: Alternative platform → Custom strategy matching their API patterns

Key Benefit: Adding new vendors requires zero changes to job service or repository layers. Strategy pattern provides perfect isolation.

2. Intelligent Sync Scheduling

Planned Features:

  • Dynamic cooldown: Adjust sync frequency based on invoice update patterns
  • Priority syncing: High-value jobs synced more frequently
  • Smart window: Extend/reduce 3-month window based on job complexity
  • Predictive syncing: ML-based prediction of when invoices will change

3. Analytics & Monitoring

Planned Metrics:

  • Invoice update frequency by service type
  • Revenue delta tracking (initial vs. final invoices)
  • Sync success rates by vendor
  • API performance monitoring
  • Cost per sync analysis

4. Webhook Integration

Planned Architecture:

  • Real-time updates: Vendor webhooks trigger immediate syncs
  • Reduced polling: Only sync when vendor notifies of changes
  • Lower API usage: Further reduce from 2 syncs/day to on-demand only
  • Hybrid approach: Webhooks + periodic polling for reliability

5. Advanced Error Recovery

Planned Features:

  • Exponential backoff: Smarter retry timing for transient failures
  • Circuit breaker: Pause syncing if vendor API is down
  • Dead letter queue: Manual review queue for persistent failures
  • Automated reconciliation: Compare vendor records vs. local data

Testing Strategy

Critical Test Scenarios

Window Boundary Tests:

  • Booking with start_sync_at = 89 days ago → Should sync
  • Booking with start_sync_at = 91 days ago → Should NOT sync
  • Booking with no start_sync_at → Should NOT sync

Cooldown Tests:

  • last_sync_at = 13 hours ago → Should sync
  • last_sync_at = 11 hours ago → Should NOT sync
  • last_sync_at = null → Should sync (first sync)

Partial Failure Tests:

  • Batch of 5 bookings, 3rd fails → All 5 should have results (2 success, 1 failure, 2 success)
  • Verify other bookings complete despite individual failures

Rate Limiting Tests:

  • 10 bookings → Verify ≥1 second execution time (proves batching delay)
  • Mock vendor API → Verify exactly 5 concurrent calls per batch (not more)

Concurrency Tests:

  • Simulate two CRON runs simultaneously → Verify optimistic locking prevents double-processing
  • Verify lock timeout releases after 5 minutes

Conclusion

The Wrkbelt Job Invoice Syncing System represents a comprehensive solution for automated revenue tracking in service booking platforms. By implementing a dual sync flow architecture with initial and ongoing syncs, the system captures immediate revenue while intelligently respecting appointment schedules.

System Highlights

Dual Flow Architecture:

  • Initial Sync: Captures dispatch fees immediately (5 min after booking, no appointment check)
  • Ongoing Sync: Monitors job lifecycle (3-month window, appointment-based decision logic)

Key Technical Achievements:

  • Clean Separation: Two completely separate queries with zero overlap
  • Appointment Intelligence: Only syncs when work is complete (no future appointments)
  • Composition Pattern: Shared syncInvoicesForBookings() helper eliminates duplication
  • Strategy Pattern: Vendor-specific implementations with consistent interface
  • 98% API Reduction: Through intelligent 12-hour cooldown logic

Production-Ready Design:

  • SOLID Principles: Single responsibility, dependency inversion throughout
  • Fail-Fast: Errors thrown immediately with full context
  • Observable: Comprehensive logging at each decision point
  • Resilient: Partial failures isolated, automatic retries
  • Scalable: New vendors = new strategy, zero core changes

The architecture demonstrates enterprise-grade software engineering: clean abstractions, clear responsibilities, and composable design patterns that make the system maintainable and extensible.

Key achievements of this system:

  • 98% reduction in unnecessary API calls through intelligent cooldown logic
  • Zero duplicate processing through clean query separation (initial vs ongoing)
  • Appointment-based intelligence prevents syncing mid-job
  • Immediate revenue visibility through separate initial sync flow
  • Zero revenue leakage by capturing invoice updates throughout the job lifecycle
  • Vendor relationship preservation through respectful rate limiting
  • Automatic compliance with 3-month rolling window that self-expires
  • Production-ready resilience with partial failure handling and retry logic

As service businesses continue to rely on accurate, real-time financial data, this system provides the foundation for reliable invoice synchronization that requires no manual intervention while maintaining data integrity and vendor API health.

References

Architecture Patterns

  • Strategy Pattern: Gang of Four Design Patterns
  • SOLID Principles: Robert C. Martin - Clean Architecture
  • Domain-Driven Design: Eric Evans - Strategic design patterns

Implementation Resources

ServiceTitan API