Skip to main content
LedgerItem.date is the effective civil business day of a ledger line. Postgres stores it as DATE (DateTime @db.Date). The API accepts YYYY-MM-DD; inserts use ${date}::date. It is not an insert Instant, not a call clock (Event.startTime / endTime), and not created_at. Product behavior: Gig payments. Domain rules: .agents/skills/finance-domain/SKILL.md.

Field meanings

Do not derive date at read time from Event.eventDate. Stored date is the source of truth.

Field → Temporal type map (ENG-582)

Rules:
  • Parse a bare YYYY-MM-DD with parseCivilDay (in each package’s temporalDate module) — never new Date('YYYY-MM-DD'), which parses as UTC midnight and shifts a day west of UTC.
  • Prisma still hydrates @db.Date as a JS Date (UTC midnight); convert at the mapping edge with the bare-date:interop markers, never re-parse the wire string with new Date.
  • The check:bare-date gate (in api-events npm run checks) keeps new bare Date out of production code.

Writers

Invitation-linked PAYABLEs (invitation_id set) and crew-offer PAYABLEs (notes starting source=crew_offer;) are stamped with Event.eventDate at create time. createPayableLedgerItem takes the same event-date: advisory lock as the reschedule snap, re-reads Event.eventDate, and inserts that civil day. A concurrent date change cannot insert on the old day. Recompute / amount upserts do not rewrite stored date on existing payables. New supplement creates go through createPayableLedgerItem and use the current gig day. Manual PAYABLE / INCOME / EXPENSE / RECEIVABLE lines keep the date the caller sent (createLedgerItem is unlocked).

Gig reschedule

When updateEventWithDateCascade changes Event.eventDate’s UTC civil day, it takes the event-date: advisory lock and edits matching PAYABLE date in place in the same transaction (snapOpenGigPayableDatesWithTx in api-events/src/db/eventCrud.ts). Invitation and crew-offer payable creates take that same lock before they insert. The UPDATE moves a row only when all of these hold:
  • type = PAYABLE and status = UNPAID
  • invitation_id IS NOT NULL or notes LIKE 'source=crew\_offer;%' ESCAPE '\'
  • no transactions row exists (any status: PENDING, SUCCEEDED, FAILED, REVERSED, REFUNDED)
  • stored UTC day is distinct from the new event day
Leave these on their stored day:
  • PAID, PROCESSING, FAILED, REFUNDED, VOIDED
  • any item with payment activity
  • manual lines (no invitation id and no crew-offer notes)
Same-day timestamp-only eventDate edits and endDate-only edits do not snap. Unlocked multi-day start-date changes still snap open payables; staffing cascade stays single-day-only. Do not void-and-copy on reschedule. invitation_id is unique on ledger items, and void means cancellation, not a moved gig.

Schedule and Run of Show follow the start day (ENG-1721)

EventScheduleItem, run_of_show_items and run_of_show_sections store absolute instants. When the same update slides the gig (both ends move the same way, which includes moving a single-day gig), shiftGigScheduleWithTx moves every one of their start and end times by the eventDate day delta, in the same transaction and under lockRunOfShowTx. Each row keeps its wall clock in the gig’s zone (the one the update sends, else the stored one, as the staffing cascade does), so a move across a DST change keeps 7:00 PM at 7:00 PM. A shift that touches Run of Show rows bumps event_run_of_show_meta.version.
  • A multi-day slide moves every day by the same amount. Section and key time day_offset already count from eventDate, so they are not changed.
  • Growing or shrinking the range (extend, reduce in classifyEventRangeChange) shifts nothing: the remaining days keep their schedule on their calendar day. endDate-only and same-day edits shift nothing either.
  • Known gap: when the front day is added or removed, section and key time day_offset still count from the new eventDate, so they point one day off. That needs its own fix.
  • An import that skips a user-claimed schedule (importedScheduleSkipped) leaves eventDate alone, so it shifts nothing either.
  • Gigs moved before this change keep their old-day rows. Repairing them is a separate, approved data change.

Applied backfills (do not re-edit)

Prisma migrate deploy records a checksum and does not re-run an applied migration. Editing the SQL file after qa/prod applied it changes the repo only. 20260316 is the rewrite that moved the original createdAt backfill cohort. Do not edit 20260307 again to “fix” leftovers. If a classified leftover rewrite is needed, add a new migration. Studio historically stored gig civil days as UTC noon so a DateTime column would not flip the calendar day in US timezones, and ledger backfills copied that Instant onto ledger_items.date. This branch midnight-normalizes ledger dates (::time <> 00:00:00, including the one prod leftover that is neither noon nor midnight), then converts the column to DATE. Event.eventDate encoding stays with ENG-580. Transaction.date stays TIMESTAMP(3).

Leftover inventory (read-only)

Invitation PAYABLEs whose UTC civil day is not Event.eventDate can remain after 20260316. Causes include later reschedules (before the runtime snap), Flex eventDate Instant encoding (see ENG-580), and non-midnight createdAt that midnight-equality missed before 20260316. Do not blindly SET date = eventDate for every leftover. Classify first. A future backfill, if any, should be UNPAID-only, skip items with transactions, and treat Flex ±1-day non-midnight eventDate as a separate Instant-encoding question. Session TZ is assumed UTC; the queries still name UTC explicitly.
A matches_invitation_createdAt_day hit after 20260316 means date and eventDate differ as civil days but share the invite-create UTC day — unexpected if that rewrite copied eventDate. More likely leftovers are matches_respondedAt_day, matches_ledger_created_at_day, or other. For other rows, drill into day offset and whether the event was edited after the ledger day: