Extra </cfif> at line 243 had no matching opener. Was hidden by
Lucee's template cache until the Tomcat restart exposed it.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
All servers (db, dev, biz) migrated to UTC:
- MySQL default-time-zone changed from America/Los_Angeles to +00:00
- All existing datetime data converted from Pacific to UTC
- JVM timezone set to UTC on dev and biz servers
- OS timezone set to UTC on all three servers
Removed CONVERT_TZ hack from listForKDS.cfm — data is natively UTC.
Simplified toISO8601() — no dateConvert needed since data is already UTC.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
MySQL is in America/Los_Angeles, not UTC. Use CONVERT_TZ to properly
convert to UTC before appending the Z suffix.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
dateTimeFormat() converts to JVM local time before formatting, so
appending 'Z' was wrong. Use DATE_FORMAT in SQL instead since the DB
values are already UTC.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
DB stores timestamps in UTC but the API returned them without timezone
info, causing JS to parse them as local time. Append 'Z' suffix so
new Date() correctly interprets them as UTC.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Combines allocate_business_namespace + allocate_servicepoint_minor into one
endpoint that returns UUID, Major, Minor, MeasuredPower, AdvInterval, TxPower,
ServicePointName, and BusinessName. No more hardcoded defaults on the client.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Restaurant info is in ROOT_QUERY.restaurantV2(...) keys, not
Restaurant:* top-level keys (Apollo cache format)
- Prices are in item.prices array [4.50], not item.price scalar
- Added null checks for imageUrls (can be null, not missing)
- Fallback to title tag for business name
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Instead of sending 450KB of HTML to Claude (which truncates to 100K
and only extracts ~60 items), parse the structured __OO_STATE__ data
directly on the server. This captures all menus, groups, items, prices,
and images from Toast pages - 169 items for Jus Family Cafe vs 60 before.
Falls back to Claude analysis if __OO_STATE__ parsing fails.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Shows subcategory count as a separate indented row beneath categories
when subcategories are present.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Changed from fixed 120px height to aspect-ratio 3:1 to match the
recommended 1200x400 dimensions, preventing heavy cropping.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The Claude API sometimes returns explanatory text before the JSON
response even when instructed to return only JSON. Added extraction
logic to find the first { character and strip any leading text.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Parse HTML heading structure to detect h2 parents with h3 subcategories
- Append detected hierarchy to Claude prompt as explicit hint
- Post-process Claude response to enforce hierarchy even if Claude returns flat
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- analyzeMenuUrl.cfm: Detect subcategories from Toast subgroups and
Claude API responses, preserve hierarchy with parentCategoryName
- setup-wizard.html: Display subcategories indented under parents
throughout wizard flow (categories step, items review, summary, preview)
- menu-builder.html: Show subcategories nested in outline modal view
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- saveCategory.cfm: Accept ParentCategoryID, enforce max 2-level nesting
- items.cfm: Include ParentCategoryID on virtual category rows for Android
- getForBuilder.cfm: Return ParentCategoryID in builder API response
- saveFromBuilder.cfm: Persist ParentCategoryID on save, track JS-to-DB id mapping
- saveWizard.cfm: Two-pass category creation (parents first, then subcategories)
- menu-builder.html: Parent category dropdown in properties, visual nesting in canvas,
add subcategory button, renderItemCard() extracted for reuse
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Large menus (20+ categories) were getting truncated JSON responses
at 8192 tokens, causing parse failures.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Staff cash_debit goes to the worker (they keep the cash).
Admin/Manager cash_debit goes to the business owner (restaurant has it).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Create tt_StaffRoles lookup table (Staff, Manager, Admin)
- Add RoleID column to Employees table (default: Staff)
- Wire portal role dropdown to addTeamMember API
- Return RoleName in team list and RoleID to Android
- Skip worker payout ledger and cash_debit for Manager/Admin roles
on cash task completion (they collect on behalf of the restaurant)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Return HasChildren boolean so Android can route food court parents
directly to business selector. Exclude child businesses from top-level
restaurant list.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- open.cfm: accept optional ApprovalMode param (0=auto, 1=manual, NULL=business default)
- addOrder.cfm: check tab ApprovalMode first, fall back to business TabApprovalRequired
- getActive.cfm: return resolved ApprovalRequired in tab response
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The cron was marking tabs as 'captured' without checking if the Stripe
capture actually succeeded. Also, it never loaded the Stripe config
(api/config/stripe.cfm), so the API key was empty and all captures
were silently failing. Now includes the Stripe config, checks every
response status, and reverts the tab to open on capture failure.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- New api/tabs/ directory with 13 endpoints: open, close, cancel, get, getActive,
addOrder, increaseAuth, addMember, removeMember, getPresence, approveOrder,
rejectOrder, pendingOrders
- New api/presence/heartbeat.cfm for beacon-based user presence tracking
- New cron/expireTabs.cfm for idle tab expiry and presence cleanup
- Modified submit.cfm for tab-aware order submission (skip payment, update running total)
- Modified getOrCreateCart.cfm to auto-detect active tab and set TabID on new carts
- Modified webhook.cfm to handle tab capture events (metadata type=tab_close)
- Modified businesses/get.cfm and updateTabs.cfm with new tab config columns
- Updated portal tab settings UI with auth amounts, max members, approval toggle
- Added tab and presence endpoints to Application.cfm public allowlist
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The addEventListener wasn't firing (likely cached JS). Added inline
onclick as reliable fallback and flexbox centering for the SVG icon.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
b.Minor is 0 for the first beacon which is falsy in JS.
Changed to null check so Minor: 0 displays correctly.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The refresh button in the top bar had no click handler attached.
Now it reloads the current page data when clicked.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Changed tt_TaskTypeID to ID in task type CRUD operations to match
the migrated dev database schema.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The tt_TaskTypes table uses 'ID' not 'tt_TaskTypeID'.
This was causing "Failed to load services" in the portal.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The escaped backticks (\`) were causing "invalid escape sequence" error,
breaking the entire script and making the Upload Files button non-functional.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add getTimeInZone() and getDayInZone() helpers in Application.cfm
- Update items.cfm and getForBuilder.cfm to use business timezone
- Menu availability now correctly respects each business's timezone
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Both values MUST be set per business. If not configured, the menu
API will return an error instead of silently using defaults.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
If PayfritFee is not configured for a business, the payment intent
creation now errors instead of silently using 5%. This ensures fees
are always explicitly set per business.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add SubmittedOn = COALESCE(SubmittedOn, NOW()) to webhook for KDS timer
- Add test mode webhook secret for dev.payfrit.com
- Keep live mode webhook secret for biz.payfrit.com
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Moved fee calculation before PI check so we can compare amounts.
If existing PaymentIntent has different amount than current cart,
update it via Stripe API before returning.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When user abandons checkout and retries, retrieve the existing
PaymentIntent from Stripe. If still usable (requires_payment_method,
requires_confirmation, requires_action), return its client_secret.
If already succeeded, block with clear error. If terminal/canceled,
clear and create new one.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Returns customer ID, ephemeral key, and publishable key needed for
Stripe Payment Sheet to display saved payment methods on iOS/Android.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Get user info (StripeCustomerId, email, name) when creating PaymentIntent
- Create Stripe Customer if user doesn't have one
- Add customer and setup_future_usage=off_session to PaymentIntent
- Cards are automatically saved after successful payment
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The ## escape sequence was missing the closing # for variable interpolation,
causing 500 errors on all webhook requests. Fixed ##metaTipID# -> ###metaTipID#
and similar patterns.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Cancel Task now leaves order untouched (customer can pay another way)
- Standardized SMS text to "Your Payfrit code is:" across all endpoints
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Check for existing active (uncompleted) cash task before creating
a new one. Prevents duplicate "Pay With Cash" tasks if order status
changes are triggered multiple times.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When worker passes CancelOrder: true to complete.cfm:
- Skips all cash validation and financial processing
- Sets order status to 6 (cancelled)
- Completes the task normally
- Returns OrderCancelled: true in response
For when customer changes their mind about paying with cash.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Fixed complete.cfm, getDetails.cfm, listMine.cfm, listPending.cfm to pull
PayfritFee from Businesses table with 0.05 fallback if not set.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Matches the lookup.cfm approach:
- UUID from BeaconShards
- Major from Businesses.BeaconMajor
- Minor from ServicePoints.BeaconMinor
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Looks up actual ShardUUID, Major, Minor from BeaconHardware
instead of deriving from Business/ServicePoint tables.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- UUID from BeaconShards via Business.BeaconShardID
- Major from Business.BeaconMajor
- Minor = ServicePointID
Replaces old beacon lookup via ServicePoints.BeaconID
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>