QBTime Manager — Project Plan & Status¶
Single source of truth for what we set out to build and what actually got built. Consolidates the former
TODO.md(the plan),REMAINING-WORK.md(verified status), andCHANGELOG.md(build log). Last reconciled 2026-06-03.This doc stays at the plan / status level. It does not repeat the deploy, testing, backup, or user-facing guides — see the other ops docs and the public User/Administrator guides for the "how." For system design, see Architecture.
What QBTime Manager is¶
A multi-tenant web app that extends QuickBooks Time (QBT) with user/schedule management and automated attendance reporting (scheduled emails listing who clocked in and who was scheduled but didn't). It manages multiple QBT accounts (companies) under a three-tier hierarchy (Platform/DevOps → Customer → Company) and is built to be resold. Audience is admins/managers only — no employee logins; QBT access is via stored per-company service tokens.
Stack: React + Vite + TypeScript + Tailwind (client); Cloudflare Pages Functions/Workers (API); D1 (SQLite); KV (cache/sessions); a separate cron Worker (Pages can't run cron); Microsoft Graph / SendGrid email; Stripe billing.
Confirmed product decisions (locked)¶
- Audience: admins/managers only; no per-user QBT OAuth; per-company service tokens.
- Multi-tenant from day one: every record partitioned by
customer_id/company_id. - Login: local salted+hashed (PBKDF2) accounts in D1 + mandatory MFA (TOTP).
- Tiers/roles: Platform DevOps (+ Global Admin) → Customer Admin + Manager (per company).
- Impersonation: DevOps "view-as," fully audited, at a separate
/platform/*route. - Onboarding: self-service (Model 1) — admins enter their QBT client_id/secret and run the Connect wizard.
- Hosting: Cloudflare. Email: Microsoft 365 / Graph
sendMail(SendGrid fallback). Source control: GitHub. - Billing: Stripe; usage-based with a next-cycle model (see Billing section).
Build phases — planned vs. built¶
Status legend: ✅ built & verified · ⚠️ partial / read-only · ⏳ deferred · 👤 owner/infra step (not code).
| Phase | Planned | Status |
|---|---|---|
| 0 — Setup / scaffold | Monorepo, Cloudflare config, D1 schema, design system, CI, docs. | ✅ Built. (Some account/infra steps are owner tasks — see below.) |
| 1 — Auth (all tiers) | PBKDF2 login, KV sessions, mandatory MFA/TOTP + recovery, lockout, password policy + HIBP, CSRF, role/tier guards, first-run bootstrap, change-password. | ✅ Built. + email-based password reset added this round. |
| 2 — Read & display | QBT client wrapper; dashboard (today), users list, schedule view, company switcher. | ✅ Built. |
| 3 — Management (write) | Users/schedule/jobcode writes, idempotent + audited; add/edit/archive UIs. | ✅ Built. + drag-to-move shifts, bulk actions + undo added this round. |
| 4 — Reporting engine | Attendance diff, tz/DST, HTML template, Graph send, manual run + preview, snapshotting. | ✅ Built. + PDF export, combined per-company email, saved views added. |
| 5 — Scheduling | Cron orchestrator, report-config CRUD, internal cron endpoint. | ✅ Built. Combined-email + config edit/delete done. |
| 6 — Hardening & polish | Activity log + viewer + chain-verify, health/status page, dark mode, a11y, pagination. | ✅ Built. + component-test harness, a11y pass, pagination added. |
| 7 — Operations & reliability | Idempotent sends, per-company isolation, self-throttle, monitoring/alerts, token watchdog, backups, runbook. | ✅ Built. + notification delivery, daily token-expiry sweep, proactive attendance alerts, D1 backup doc, incident runbook added. |
| 8 — Onboarding & admin tooling | Add/connect/disconnect company, account invite/manage, company settings. | ✅ Built. + QBT client-secret rotation added. |
| 9 — Platform / DevOps console | /platform/* guards, customers, DevOps mgmt, audited impersonation. |
✅ Built. + cross-customer fleet-health dashboard added. |
Competitive-gap features (this round — beating QBT)¶
Built from researched QBT complaints (see Product Roadmap (QBT gaps) for the analysis):
- ✅ Proactive attendance alerts — no-show / late / missing-punch / coverage-gap to the bell, per-company prefs (migration 0006).
- ✅ Multi-level timesheet approvals — sequential 1–2 level chain; writes
approved_toback to QBT on final approval (needs QBT Approvals Add-On; graceful no-op without it). Migration 0007. - ⚠️ Time off (PTO) — read-only view shipped; field-mapping is defensive and unverified against live QBT; approve/deny + balances deferred until a QBT trial confirms the payload.
- ✅ Saved report views — name a run, re-run in one click.
- ✅ Billing usage model — next-cycle tier reconciliation + onboarding true-up (see Billing).
- ✅ Branding/chargeback — CowboyMSP disclosure on signup/billing, email sender name, Stripe statement-descriptor guidance, promo-code support.
Report catalog & QBT capabilities — built vs. planned¶
The schema defines 27 report types, all wired + emailable. ✅ = verified compute; ⚠️ = built but on assumed QBT fields (verify at the QBT trial).
| Report type | Status |
|---|---|
| Daily attendance / late-early / no-show / missing-punch / coverage-gaps | ✅ Attendance family (flagship template). Missing-punch also feeds the alert. |
| Hours (per employee) | ✅ From timesheets. |
| Overtime (>40h) | ✅ From timesheets. |
| Weekly timesheet (grid) | ✅ Employee × 7-day matrix from timesheets. |
| Detailed timesheet | ✅ Line-item entries (in/out/hours/jobcode/notes). |
| Pay-period summary | ✅ Per-user totals + entries. |
| Hours by jobcode | ⚠️ assumed jobcode_id + /jobcodes fetch. |
| Hours by group/department | ⚠️ assumed user.group_id/group_name. |
| Payroll summary (reg/OT/PTO/holiday) | ⚠️ assumed type field for PTO/holiday split. |
| Wage / gross pay (OT @1.5x) | ⚠️ assumed user.pay_rate. |
| Labor cost | ⚠️ assumed user.pay_rate. |
| Job / project costing (hours + cost) | ⚠️ assumed pay_rate + jobcode_id. |
| PTO balances | ⚠️ assumed PTO type; accrual passed in (QBT balance payload unconfirmed). |
| Schedule vs actual | ⚠️ schedule events vs timesheets; all-day = 8h assumed. |
| On-the-clock-now | ⚠️ open punches. |
| GPS anomalies | ⚠️ assumed /geolocations shape. |
| Edited timesheets | ⚠️ assumed created/last_modified. |
| Approvals status | ✅ current state from our tables. |
| Approvals history | ⚠️ from our approval tables (period/step trail). |
| Audit / edit log | ⚠️ from our audit_log table. |
| Meal/rest break compliance | ⚠️ assumed break_seconds/paid. |
| Certified payroll (WH-347) | ⚠️ assumed classification + pay_rate; no prevailing-wage feed. |
| ACA / ALE hours | ⚠️ avg weekly hours; ≥30 = full-time. |
Wiring note (updated): All catalog types now flow through
runReport, which dispatches byreport_type: the attendance family uses the rich attendance template; the rest use the generic table template (report-template.ts) viareport-dispatch.ts. Scheduled configs and manual runs both honor the selected type and email the matching report. Trial-gated types compute on ASSUMED QBT fields (flagged in code, in the config UI, and via an in-email banner) — verify the field mappings at the QBT trial before trusting their numbers.
Outcome features (beyond reports):
- ✅ Payroll export — Generic/Gusto/ADP/Paychex CSV (
payroll-export.ts+ GET/reports/payroll-export). Built from payroll-summary; rates ASSUMED. - ✅ Live ops dashboard —
/reports/ops-dashboard: on-clock-now, 7-day labor cost vs. settable weekly budget, exceptions (anomalies + break-premium $ + missing punches). Budget via/companies/labor-budget(migration 0008). - ✅ Insights engine (
insights.ts) — break-premium $ owed, anomaly detection (impossible-hours / overlapping-punch / schedule-drift / GPS-teleport), labor-vs-budget. ASSUMED fields, flagged provisional. - ✅ Daily approvals nudge —
sweepPendingApprovalsin the cron notifies admins once a day when approvals are pending. (Approvals page already mobile-friendly.)
QBT API endpoints we call today: /users, /schedule_events,
/schedule_calendars, /timesheets, and /jobcodes (new). Documented but not
yet wired: /jobcode_assignments, /geolocations (GPS), /reports
current_totals (live "on the clock now"), /notifications (push reminders to
QBT employees). These are deferred to the QBT-trial phase, where their real field
shapes can be verified — building them on guessed shapes (especially payroll-cost
and GPS) was deliberately avoided.
Billing model (as built)¶
Stripe, usage-based, next-cycle (no mid-cycle surprise charges):
- Tiers in
billing_planswithmax_companies/max_employees; onboarding fee per tier. - Over-limit → pending tier: the new monthly rate applies at the next cycle; a dashboard banner warns in advance. No proration mid-cycle.
- Onboarding true-up: charged once, to the highest tier ever reached (
highest_onboarding_tier); delta on the next invoice; never re-charged on later growth. Catches "sign up cheap, add 20 companies" whenever it happens. - Adding a company beyond the largest plan is hard-blocked ("contact us").
- "Active employees" counted over a rolling 7-day window (excludes churned staff).
- Discounts: Stripe Checkout promo codes enabled; coupons created in the Stripe Dashboard (repeating / forever / free / onboarding-waiver).
- Migrations 0003 (billing), 0005 (usage columns:
pending_tier,highest_onboarding_tier). - Coupons + tier changes: a Stripe coupon lives on the customer/subscription,
not the price, so changing a customer's tier (our price swap) never disturbs
the discount — a lifetime-free customer stays free at the new tier; a
repeating discount keeps its remaining duration applied to the new tier's price,
then reverts. The one-time onboarding-delta is skipped for any customer with
an active discount (
hasActiveDiscount), so "free/discounted" means no surprise onboarding charge.
What's still open¶
Codeable (deferred by choice):
- ⏳ PTO write-back + balances — finish once a QBT trial confirms the time-off payload.
- ⏳ True pixel drag-and-drop niceties, bulk-undo extras, deeper a11y audit, comparative reports (the larger reporting-depth items).
Owner / infra (not code — 👤 you):
- 👤 Enable the QBT Approvals Add-On on connected companies (for approval write-back).
- 👤 Create the QBT trial/sandbox account (unblocks PTO finish + live verification).
- ✅ Stripe statement descriptor set: account prefix
COWBOYMSP+ per-product suffixQBTIMEon the QBTime prices → statement readsCOWBOYMSP* QBTIME; other products keep their own labels. (Confirm the suffix is on every QBTime tier, test + live.) - ✅ Prod cron Worker + secret deployed; migrations run per env; coupons created in Stripe as needed.
Original open questions — and how they were resolved¶
From the former TODO.md §8 ("confirm before building"):
- "Scheduled" definition → a user counts as scheduled only if a published (non-draft) event exists that day. Resolved, implemented.
- Late threshold → default 15 min, configurable per report config. Resolved.
- Report delivery across companies → separate or combined email, customer's choice (delivery_mode). Resolved, built.
- Failure alerts destination → in-app bell (notification delivery), with per-company alert prefs. Resolved, built.
- Repo visibility → private. Confirmed.
- Onboarding fee vs monthly → two Stripe objects (one-time onboarding + recurring), onboarding trued-up to highest tier. Resolved, built.
- Self-serve vs assisted → converged on one customer model: everyone signs up self-serve; "we'll set it up" files a support request. Resolved.
- Customer timezone(s) / GPS retention / branding → per-company timezone setting; GPS read-only/optional; CowboyMSP branding disclosure added. Resolved / noted.
Items that still genuinely depend on the customer/QBT trial (real recipient lists, exact sender mailbox, the two real company names) are captured in the End-to-End Test Walkthrough Step 0, not here.
Condensed build log¶
- Phase 0–9 application + client built and verified (typecheck/lint/test/build green); 186+ unit/component tests.
- This round (2026-06-03): password reset, secret rotation, combined email, PDF export, fleet-health dashboard, notification delivery, drag-drop, bulk actions, pagination, a11y + component-test harness, usage-based billing rework, branding/chargeback, promo codes, proactive alerts, multi-level approvals, read-only PTO, saved views.
- Docs: split into public (User/Administrator guides) + private ops site (Deploy App/Docs/Sales, Backup & Restore, Runbook, E2E Test Walkthrough, Architecture, Product Roadmap, and this doc).
Full per-commit history lives in git; this section is the human-readable summary.