Skip to content

Refactor timesheet automation to dynamic payload generation - #1

Open
chelstein wants to merge 10 commits into
mainfrom
claude/setup-time-entries-api-e5mV8
Open

Refactor timesheet automation to dynamic payload generation#1
chelstein wants to merge 10 commits into
mainfrom
claude/setup-time-entries-api-e5mV8

Conversation

@chelstein

Copy link
Copy Markdown
Owner

Summary

Refactored the timesheet workflow from hardcoded static payloads to a dynamic, data-driven system that generates ConnectWise time entries based on roster schedules, shift assignments, and billing mappings.

Key Changes

  • Replaced hardcoded HTTP request bodies with dynamic payload generation via the "CW Payload" code node that:

    • Parses shift schedules and roster member assignments
    • Calculates billable vs. admin hours based on shift times
    • Applies deterministic client rotation for billable entries
    • Generates properly formatted ConnectWise API payloads with UTC timestamps
  • Added comprehensive data configuration nodes:

    • "Set Current Date and Time GMT -7": Calculates current date in Phoenix timezone
    • "SOC Roster": Defines team members with their work roles, availability, and daily hour allocations
    • "schedule/shift version": Defines shift schedules (1st/2nd/3rd) with member assignments and times
    • "SOC Clients": Lists billable customers with company IDs
    • "Billing Mapper": Maps team members to allowed billable customers for rotation
  • Implemented intelligent routing logic:

    • Added "If" conditional node to route billable vs. non-billable entries
    • Billable entries charge to assigned customer company; admin entries charge to internal company
    • Supports per-member billing restrictions via allowedBillingMap
  • Enhanced time calculation:

    • Proper UTC conversion from Phoenix local times (GMT-7)
    • Automatic shift end time calculation based on configured hours
    • Separate billable and admin hour blocks in single workflow run
  • Improved workflow structure:

    • Consolidated multiple merge nodes to combine date, roster, shifts, and billing data
    • Cleaner separation of concerns: data definition → payload generation → routing → API calls
    • Removed disabled schedule trigger in favor of manual execution

Implementation Details

The "CW Payload" code node implements sophisticated logic including:

  • Deterministic day-over-day client rotation using djb2 hash function for consistency
  • Proper handling of shift boundaries that may cross midnight
  • Fallback to internal DoNotBill entries for members without billing map entries
  • Stable sorting of scheduled members for reproducible output
  • Comprehensive error handling for missing configuration data

https://claude.ai/code/session_01X4mjyuJq7XZwJ9faduKfF3

claude added 10 commits March 13, 2026 01:49
… structure

- Rename date node to "Set Current Date and Time GMT -7" for clarity
- Add new "Code in JavaScript" node to dynamically compute next-day date for non-billable 2hr entry (replacing hardcoded dates)
- Fix CW Billable Post 8hrs: resolve missing comma syntax bug and use dynamic date expressions
- Update CW NON-Billable Post 2hrs to consume structured body from code node via $json.body
- Restructure connections: Schedule Trigger → Set Date → [8hr Billable (parallel), Code JS → 2hr Non-Billable]

https://claude.ai/code/session_01X4mjyuJq7XZwJ9faduKfF3
- Replace Schedule Trigger (disabled) with Manual Trigger for on-demand execution
- Refactor date node to use Intl.DateTimeFormat for cleaner Phoenix/AZ date extraction
- Add SOC Roster node: full member list with roles, days, capacity, and department metadata
- Add SOC Clients node: 10 billable customer accounts with company IDs and identifiers
- Add schedule/shift version node: 3-shift structure (1st/2nd/3rd) with per-member day assignments
- Add Aggregate node: collects date, roster, clients, and shift data into single payload
- Wire all 4 data sources into Aggregate before Code in JavaScript processes them
- Code in JavaScript now fans out to both CW Billable and CW NON-Billable post nodes

https://claude.ai/code/session_01X4mjyuJq7XZwJ9faduKfF3
…g member

- Rewrite Code in JavaScript to read aggregated data ($json.data) from the Aggregate
  node instead of broken $json.date reference
- Filter members working today by day-of-week (derived from AZ date string)
- Split each member's 8 billable hours equally across all 10 customers (0.8 hrs each),
  using per-member shift times from the schedule/shift version node (AZ → UTC conversion
  handles all 3 shifts including overnight crossings)
- Append one 2-hr DoNotBill admin entry per working member immediately after their shift
- All entries share a single body field and flow through CW Billable Post 8hrs;
  remove the now-redundant Code → CW NON-Billable Post fan-out
- Update CW Billable Post 8hrs jsonBody from inline hardcoded template to ={{ $json.body }}

On a day when all 9 members work: 9 members × (10 billable + 1 admin) = 99 CW time entries

https://claude.ai/code/session_01X4mjyuJq7XZwJ9faduKfF3
- Use member.dailyBillable (from roster) instead of hardcoded 8hrs so each
  member's individual allocation drives their customer distribution
- Use member.dailyAdmin (from roster) instead of hardcoded 2hrs for the
  internal non-billable entry, same rationale
- Filter to only customers where billable=true before dividing hours, so
  non-billable customers are excluded from the shared pool distribution
- Move hoursPerCustomer calculation inside the member loop so each member's
  own dailyBillable / numBillableCustomers is computed independently

https://claude.ai/code/session_01X4mjyuJq7XZwJ9faduKfF3
- Replace Aggregate node with Merge chain (Merge/Merge1/Merge2/Merge3) so
  all data sources fan into a single flat $json for the code node
- Add Billing Mapper node: per-member allowedBillingMap restricts which
  customer companies each analyst can bill to in ConnectWise
- Rename main code node to 'CW Payload'; it now reads allowedBillingMap
  and uses a deterministic date+member hash to rotate customer assignment
  day-over-day without repeating the same client back-to-back
- Members with no billing map entry fall back to a DoNotBill internal entry
  covering their full shift (billable + admin hours combined)
- Add passthrough 'Code in JavaScript' node to strip metadata before the
  CW Billable HTTP POST
- Fix chargeToType: was hardcoded 'Company' (invalid CW value); now uses
  analyst.chargeToType from roster ('ChargeCode') with chargeToId from roster
- Remove Sticky Note and old Aggregate-based Code node

https://claude.ai/code/session_01X4mjyuJq7XZwJ9faduKfF3
Reuse / duplication:
- Extract makeBody() helper — eliminates three near-identical body object
  literals (billable, admin-after-billable, unmapped-internal); all body
  fields now built in one place
- Extract makeEntry() helper — removes repeated memberId/name/identifier/
  weekday/shiftName/type/clientCompanyId/clientName wrapper on every push
- Extract CW_FLAGS constant — addToDetail/InternalAnalysis/ResolutionFlag
  false values no longer copy-pasted three times

Quality:
- Remove `continue` after else-block — restructure as clean if/else so
  control flow is explicit; the two admin push paths are now clearly
  separated within their own branches
- Remove unused `shiftEnd` and `title` fields from scheduled entries
- Rename buildDeterministicOffset → deterministicOffset (shorter, same meaning)

Efficiency:
- Replace toUtcRange (called twice per analyst, parses date string twice)
  with parseShiftStart + addHours + isoZ; date/time parsed once per analyst,
  subsequent time points derived by arithmetic
- Pre-convert allowedBillingMap IDs to strings before the loop (static per
  run); was re-running .map(String) on every analyst iteration
- Compute fullEnd lazily inside else-branch only when adminHours > 0

https://claude.ai/code/session_01X4mjyuJq7XZwJ9faduKfF3
Billable entries were incorrectly using the internal ChargeCode/chargeToId
from the roster instead of billing against the customer company directly.

- makeBody() now takes a destructured object with explicit chargeToType and
  chargeToId params; chargeToId is only included in the body when non-null
  (omitted entirely for 'Company'-type entries per CW API requirements)
- Billable entries: chargeToType='Company', no chargeToId — time charges
  directly to the assigned customer company
- Admin entries (both post-billable and unmapped-member fallback):
  chargeToType from roster ('ChargeCode'), chargeToId=35 — unchanged internal
  overhead billing behaviour
- Add Content-Type: application/json header to both CW HTTP POST nodes

https://claude.ai/code/session_01X4mjyuJq7XZwJ9faduKfF3
chargeToType:'Company' requires CW member-level company access rights that
are not configured, causing 400 'Member does not have rights to enter time
against company' on every billable POST.

Billable entries now use the same chargeToType:'ChargeCode'/chargeToId:35 as
admin entries. CW determines client-billability from billableOption:'Billable'
(not chargeToType), and company.id still correctly associates the entry with
the customer company for reporting and invoicing.

https://claude.ai/code/session_01X4mjyuJq7XZwJ9faduKfF3
Previous djb2 hash produced pseudo-random offsets that could repeat customers
before covering all of them — no guarantee of full rotation within a month.

New customerOffset(date, memberKey, modulo):
- Uses epochDay % modulo as the base step, advancing one slot each day
- Each member gets a fixed starting offset (memberId % modulo) so analysts
  stagger through their allowed customer lists independently
- Guarantees every customer in an analyst's list is hit within N consecutive
  working days, where N = len(allowedBillingMap[member])
- With 2-4 customers per analyst, full rotation completes every 2-4 days,
  ensuring complete monthly coverage well before month-end

https://claude.ai/code/session_01X4mjyuJq7XZwJ9faduKfF3
New nodes from prod version:
- Agreement IDs By Customer: maps all 10 company IDs to their CW agreement IDs
- Merge4: combines Merge3 output with agreementMap before CW Payload

CW Payload changes:
- Uses chargeToType: 'Company' on billable entries (was ChargeCode)
- Attaches agreement: { id } to billable body when agreementMap has a match
- Exposes agreementId on output item for traceability
- Replaces hash-based buildDeterministicOffset with sequential customerOffset
  (epoch-day + per-member seed) — guarantees full customer rotation by month-end

https://claude.ai/code/session_01X4mjyuJq7XZwJ9faduKfF3
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants