{
  "schema": 1,
  "version": "69f7733",
  "updated": "2026-10-16",
  "note": "THE source of truth for the dashboard manual (D68). app/manual/index.html and app/manual/manual.md are generated from this file by scripts/build-manual.py; the build fails when a launcher tile, a config flag or a proxy action is missing here, when a flag state drifts from app/config.js, or when the generated files are stale. No secrets, no customer data, no prices.",
  "app": {
    "name": "PPTG Dashboard",
    "url": "https://dash.portapottytogo.com/app/",
    "manualUrl": "https://dash.portapottytogo.com/app/manual/",
    "jsonUrl": "https://dash.portapottytogo.com/app/manual/manual.json",
    "markdownUrl": "https://dash.portapottytogo.com/app/manual/manual.md",
    "whatItIs": "The office tool of Porta Potty To Go (PPTG): orders, invoices, collections, recurring billing, customers, routes and reports for two markets. GoHighLevel (GHL) is the accounting book (contacts, invoices, payments); Stripe holds the saved cards; the dashboard is the collector (cobrador) and the screen people work in.",
    "signIn": "Google sign-in with a company account. The role (owner, manager, office, finance, field) decides which tiles you see. Preview mode (access key) is NOT a sandbox: it talks to the real server and real GHL / Stripe data — for safe tests, mock the endpoints or use the robot contact only.",
    "views": "Phone view (tiles) everywhere; Office view (left navigation, dense tables, side panel) on desktop. The header's View switch (Auto / Phone / Office) is remembered per user and device; Auto = Office at 1200 px and wider; phones (< 640 px) see no switch.",
    "markets": [
      {
        "id": "wpb",
        "name": "West Palm — PPTG",
        "ghlLocation": "6N89N8vbtVWNcj2VpNak",
        "active": true
      },
      {
        "id": "orl",
        "name": "Orlando — partner",
        "ghlLocation": "7Ismp3jGLVSOZ2737Cfo",
        "active": true
      }
    ]
  },
  "screens": [
    {
      "id": "launcher",
      "title": "Home (Inicio)",
      "path": "#/home (Office) · the tile grid (Phone)",
      "tile": null,
      "purpose": "The start page: greeting, today's numbers (today's stops, open quotes, invoices due / overdue = open invoices at or past their due date, won this month = paid invoices issued this month), the Ask or dictate box, and the tiles your role may open.",
      "howToGetThere": "Sign in. The PPTG logo or the back arrow on any screen returns here.",
      "whatYouSee": [
        "Number chips (a source that could not be read shows '?')",
        "All / West Palm / Orlando buttons and a 'By market —' line (a market that failed says 'unavailable')",
        "'Recurring drafts awaiting activation' line when there are any (opens Invoices)",
        "Ask or dictate (the Assistant)",
        "Tiles: Sales & Billing, Operations, Tools & Reports, and the Manual link"
      ],
      "actions": [
        {
          "label": "All / West Palm / Orlando",
          "whatItDoes": "Re-filters the numbers by market.",
          "sideEffects": "none",
          "needsConfirmation": false
        },
        {
          "label": "Recurring drafts awaiting activation",
          "whatItDoes": "Opens Invoices, where they are activated.",
          "sideEffects": "none",
          "needsConfirmation": false
        }
      ],
      "gotchas": [
        "The number chips are not links (in the Office view the Home tiles are)."
      ]
    },
    {
      "id": "new-order",
      "title": "New Order (Nueva orden)",
      "path": "tile New Order · Office: #/orders",
      "tile": "new-order",
      "purpose": "Build an order for a customer and turn it into an invoice (factura) or a quote (cotización): customer, service address, lines from the catalog, delivery date, notes, Job/PO, tax.",
      "howToGetThere": "Tile 'New Order', '+ New' in Office, 'New order' on a customer profile, or a draft's 'Edit draft'.",
      "whatYouSee": [
        "1 Customer: search by phone / name / email, or create one ('Save customer')",
        "Billing market badge (decided by the service address; Override needs a reason)",
        "2 Service address (site address, ZIP prices the zone, optional place name); 'Schedule pickup' and 'Adds to an active site' options",
        "3 Order: the catalog by category, quantity, price; 'Prices from …' tells how old the prices are",
        "Payment: our pay page by default (saves the card), 'Enter card now', 'Use saved card'; 'Send GHL's link instead' is the secondary path",
        "Send as QUOTE, Preview, Send as INVOICE, Save invoice draft, Save quote draft"
      ],
      "actions": [
        {
          "label": "Send as INVOICE",
          "whatItDoes": "The confirm sheet (lines, tax, total, where it goes); Confirm creates the invoice in GHL, makes our pay page and sends it by email (and text where enabled). It also books the delivery on the Google Calendar (and a pickup when ticked), and for recurring lines drafts an inactive GHL recurring template (or enrolls the site in billing when mixedOrders is on).",
          "sideEffects": "email",
          "needsConfirmation": true
        },
        {
          "label": "Payment: Enter card now",
          "whatItDoes": "Creates the invoice and opens our pay page in staff mode to type the card; the screen waits until it is paid.",
          "sideEffects": "money",
          "needsConfirmation": true
        },
        {
          "label": "Payment: Use saved card",
          "whatItDoes": "Creates the invoice and charges the contact's saved card.",
          "sideEffects": "money",
          "needsConfirmation": true
        },
        {
          "label": "Send as QUOTE",
          "whatItDoes": "Creates the estimate (cotización) and sends it by email / text.",
          "sideEffects": "email",
          "needsConfirmation": true
        },
        {
          "label": "Preview",
          "whatItDoes": "How the customer will see it. Read only.",
          "sideEffects": "none",
          "needsConfirmation": false
        },
        {
          "label": "Save invoice draft (Guardar borrador)",
          "whatItDoes": "Creates (or updates, when editing a draft) a GHL draft invoice. Nothing is sent.",
          "sideEffects": "ghl",
          "needsConfirmation": false
        },
        {
          "label": "Save quote draft",
          "whatItDoes": "Creates a GHL estimate as a draft; send it from Quotes.",
          "sideEffects": "ghl",
          "needsConfirmation": false
        },
        {
          "label": "Save customer / Save contact",
          "whatItDoes": "Creates the GHL contact, or saves its edits from the customer strip.",
          "sideEffects": "ghl",
          "needsConfirmation": false
        },
        {
          "label": "Override (market)",
          "whatItDoes": "Bills the order in the other market. A reason is required and goes on the invoice.",
          "sideEffects": "none",
          "needsConfirmation": false
        },
        {
          "label": "Create recurring template (draft) — on the result screen",
          "whatItDoes": "Drafts an inactive GHL recurring invoice template for the recurring lines, after an in-app confirm.",
          "sideEffects": "ghl",
          "needsConfirmation": true
        }
      ],
      "gotchas": [
        "Leaving an order with unsaved lines asks 'Discard this order?' first.",
        "The market follows the SERVICE address, not the billing address.",
        "A county with no tax configured in that market blocks invoicing (fail loud) — add the tax in GHL first.",
        "Tax exempt needs a reason (and the certificate on the contact).",
        "'Prices from … (<age>)' next to 'Refresh catalogue' tells how old the prices are; older than 2 days is flagged (the owner can update them in Admin).",
        "Every customer invoice email also goes to the owner as a '[Copy]' (CONFIG.invoiceOwnerCopy; when no GHL contact has that email, CONFIG.ownerContactId). A copy that was not sent is said on the result screen and in a toast."
      ]
    },
    {
      "id": "customers",
      "title": "Customers (Clientes)",
      "path": "tile Customers · #/customers/<market>:<contactId>",
      "tile": "customers",
      "purpose": "Find any customer and see everything: contact, sites, invoices, payments, notes (History), recurring billing; edit the contact.",
      "howToGetThere": "Tile 'Customers', or click any customer name anywhere (every name is a link).",
      "whatYouSee": [
        "Search, '+ New customer'",
        "The profile: phone, email, card on file, last invoice",
        "Sites (service addresses): '+ Add site', Remove",
        "Invoices with the shared invoice menu",
        "Orders (GHL opportunities)",
        "History: invoices, payments, refunds, voids, notes, orders, recurring billing — searchable; add an internal note"
      ],
      "actions": [
        {
          "label": "+ New customer",
          "whatItDoes": "Creates the GHL contact (read back).",
          "sideEffects": "ghl",
          "needsConfirmation": false
        },
        {
          "label": "Edit",
          "whatItDoes": "Edits the GHL contact (CC emails, place reference, tax exemption); every save is read back.",
          "sideEffects": "ghl",
          "needsConfirmation": false
        },
        {
          "label": "+ Add site / Save site",
          "whatItDoes": "Saves a service address on the contact's sites (read back).",
          "sideEffects": "ghl",
          "needsConfirmation": false
        },
        {
          "label": "Remove (site)",
          "whatItDoes": "Removes a saved site; click twice (\"Confirm remove?\").",
          "sideEffects": "ghl",
          "needsConfirmation": true
        },
        {
          "label": "New order / New invoice",
          "whatItDoes": "Opens New Order with this exact contact.",
          "sideEffects": "none",
          "needsConfirmation": false
        },
        {
          "label": "Add note",
          "whatItDoes": "Writes an internal note on the contact (read back).",
          "sideEffects": "ghl",
          "needsConfirmation": false
        },
        {
          "label": "Recurring… (Sites card)",
          "whatItDoes": "Opens this customer's recurring templates (active and draft, in that market) with 'Cancel schedule' on each: an in-app confirm ('Keep it' does nothing), then GHL stops making invoices from it — re-read to confirm it shows cancelled, and a 'RECURRING CANCELLED' note goes on the customer's History. Invoices already made stay as they are. There is no Pause (no proxy action yet).",
          "sideEffects": "ghl",
          "needsConfirmation": true
        }
      ],
      "gotchas": [
        "'Back' returns to the screen the name was clicked on."
      ]
    },
    {
      "id": "collections",
      "title": "Collections (Cobranza)",
      "path": "tile Collections",
      "tile": "collections",
      "purpose": "The past-due queue for both markets, worst first, with the reminder ladder: the screen suggests the next step, a human clicks.",
      "howToGetThere": "Tile 'Collections' (its badge counts past-due invoices in both markets).",
      "whatYouSee": [
        "Per-market totals and a market filter",
        "Each invoice: days over, last reminder, the NEXT step",
        "Send reminder (the ladder dialog), Call, Promised to pay, and the shared invoice menu"
      ],
      "actions": [
        {
          "label": "Send reminder",
          "whatItDoes": "Opens the reminder dialog with the ladder's step and a preview; Send emails / texts the customer and records a note.",
          "sideEffects": "email",
          "needsConfirmation": true
        },
        {
          "label": "Promised to pay",
          "whatItDoes": "Hides the invoice from the queue until the promised date (recorded as a note). Asks the date in an in-app date box; Cancel records nothing.",
          "sideEffects": "ghl",
          "needsConfirmation": false
        }
      ],
      "gotchas": [
        "Nothing auto-sends: GHL's own reminders are switched off on purpose.",
        "A market that failed to load says 'unavailable', never 0.",
        "When the invoice has no stored pay link, the reminder dialog sends GHL's own invoice email / text instead.",
        "Call is a phone link (tel:)."
      ]
    },
    {
      "id": "billing",
      "title": "Billing (Cobro recurrente)",
      "path": "tile Billing",
      "tile": "billing",
      "purpose": "The recurring-billing engine (billing.php): which sites are enrolled, what the next cycle charges, holds that need approval, the daily run, and the activity timeline.",
      "howToGetThere": "Tile 'Billing'.",
      "whatYouSee": [
        "Recurring charging ON / OFF (the kill switch), last run, enrolled / on hold / charged so far, daily limit",
        "Enrolled sites per market with the per-cycle amount and a market filter",
        "Holds (first cycle, amount changed, over a cap)",
        "Waiting for a saved card",
        "Activity timeline by name (raw ids in the tooltip and in 'Show raw log')"
      ],
      "actions": [
        {
          "label": "Turn charging ON / OFF",
          "whatItDoes": "The kill switch of automatic charges.",
          "sideEffects": "money",
          "needsConfirmation": true
        },
        {
          "label": "Authorize charge (a hold)",
          "whatItDoes": "Charges the saved card NOW for that held cycle.",
          "sideEffects": "money",
          "needsConfirmation": true
        },
        {
          "label": "Preview today",
          "whatItDoes": "Shows what today's run would charge. Read only.",
          "sideEffects": "none",
          "needsConfirmation": false
        },
        {
          "label": "Enroll (waiting for a saved card)",
          "whatItDoes": "Enrolls the site in recurring billing once its card is saved.",
          "sideEffects": "money",
          "needsConfirmation": true
        },
        {
          "label": "Remove (waiting for a saved card) / Unenroll",
          "whatItDoes": "Stops future automatic charges for that site (the card stays saved; no money moves).",
          "sideEffects": "none",
          "needsConfirmation": true
        }
      ],
      "gotchas": [
        "The first cycle of every site is always held for approval.",
        "Charges stop at the gates (tolerance, jump vs. last cycle, per-charge cap, daily total).",
        "The engine sends no receipt of its own: GHL sends it when the payment is recorded."
      ]
    },
    {
      "id": "payments",
      "title": "Invoices (Facturas)",
      "path": "tile Invoices (data-nav payments) · Office: #/invoices",
      "tile": "payments",
      "purpose": "Every invoice in both markets: search, filter, sort, and the invoice menu (details, links, send, preview, record payment, refund, edit, void).",
      "howToGetThere": "Tile 'Invoices'.",
      "whatYouSee": [
        "Search (number, name, company, email, phone; address and notes once the invoice's Details were opened)",
        "Filters: market, past due, status, dates, amount, county, source",
        "'Recurring drafts awaiting activation' card when there are any",
        "Rows with status, market and the invoice menu"
      ],
      "actions": [
        {
          "label": "Details",
          "whatItDoes": "The invoice as GHL has it. 'Edit line items' (draft, sent, overdue, partially paid: qty, price, name, description, add / remove lines) saves to GHL and, for a sent invoice, asks whether to resend; 'Edit full order' opens a draft in New Order.",
          "sideEffects": "ghl",
          "needsConfirmation": false
        },
        {
          "label": "Open payment link / Copy link",
          "whatItDoes": "Our pay page link (refreshed when it expired), or GHL's link for invoices made in GHL.",
          "sideEffects": "none",
          "needsConfirmation": false
        },
        {
          "label": "Send invoice / Send receipt",
          "whatItDoes": "The channel dialog (email / text) with a preview; Send contacts the customer. On a draft it first asks: our pay page or GHL's standard email (GHL's is not previewed).",
          "sideEffects": "email",
          "needsConfirmation": true
        },
        {
          "label": "Preview",
          "whatItDoes": "How the customer will see it (our templates). Read only.",
          "sideEffects": "none",
          "needsConfirmation": false
        },
        {
          "label": "Send pay page (saves card)",
          "whatItDoes": "Makes our pay page (pays the invoice and saves the card), then the email / text choice. A DRAFT is finalized in GHL first (it becomes SENT) — cancelling the choice leaves it sent, nothing reaches the customer.",
          "sideEffects": "email",
          "needsConfirmation": true
        },
        {
          "label": "Charge saved card",
          "whatItDoes": "Charges the card on file for the amount due (only shown when the contact has a card).",
          "sideEffects": "money",
          "needsConfirmation": true
        },
        {
          "label": "Record payment",
          "whatItDoes": "Records a cash / check / Zelle payment in GHL (no card is charged); open invoices only; click twice.",
          "sideEffects": "ghl",
          "needsConfirmation": true,
          "flag": "recordPayments"
        },
        {
          "label": "Refund",
          "whatItDoes": "Full or partial refund to the card (owner only), also on partially paid invoices; 'Paid outside the card' records the refund only; click twice. A full refund also voids the invoice.",
          "sideEffects": "money",
          "needsConfirmation": true,
          "flag": "refunds"
        },
        {
          "label": "Edit draft",
          "whatItDoes": "Opens a draft in New Order, fully editable; 'Save changes' updates the same draft.",
          "sideEffects": "ghl",
          "needsConfirmation": false
        },
        {
          "label": "Edit invoice (sent, unpaid)",
          "whatItDoes": "Opens a sent / overdue / partially paid invoice in New Order; 'Save changes to invoice #N' updates the SAME invoice (number, dates, status kept; an 'Updated <date>: …' line in its notes and on the customer's History); never below what was already paid. Then 'Resend updated invoice' sends one message with a fresh pay link — the old link stops working. If GHL refuses the update: 'Replace invoice' (new draft, old one voided).",
          "sideEffects": "ghl",
          "needsConfirmation": false,
          "flag": "editSentInvoices"
        },
        {
          "label": "Void",
          "whatItDoes": "Two steps: confirm, then a required reason (saved on the customer). Disabled on paid invoices — refund instead.",
          "sideEffects": "ghl",
          "needsConfirmation": true
        },
        {
          "label": "Send reminder",
          "whatItDoes": "A payment reminder with the pay link (email, text where enabled), recorded as a note.",
          "sideEffects": "email",
          "needsConfirmation": true
        },
        {
          "label": "Open customer",
          "whatItDoes": "The customer's profile.",
          "sideEffects": "none",
          "needsConfirmation": false
        },
        {
          "label": "Activate (recurring drafts card)",
          "whatItDoes": "Activates a drafted GHL recurring invoice schedule, charged to the customer's card each cycle.",
          "sideEffects": "money",
          "needsConfirmation": true
        },
        {
          "label": "Recurring… (invoice menu)",
          "whatItDoes": "Opens this customer's recurring templates (active and draft, in that market) with 'Cancel schedule' on each: an in-app confirm ('Keep it' does nothing), then GHL stops making invoices from it — re-read to confirm it shows cancelled, and a 'RECURRING CANCELLED' note goes on the customer's History. Invoices already made stay as they are. There is no Pause (no proxy action yet). Shown when the customer has a live template.",
          "sideEffects": "ghl",
          "needsConfirmation": true
        }
      ],
      "gotchas": [
        "The SAME invoice menu is in Invoices, Collections, Customers, Office and the New Order result screen.",
        "The customer-facing title is 'PPTG Order — <customer> · Job/PO <po>'; internal stamps (zone, county) ride a 'Dashboard ref:' line in the terms.",
        "Paying through GHL's link does not save the card; our pay page does.",
        "Cancel an activated recurring schedule: 'Recurring…' on the customer card or the invoice menu → 'Cancel schedule'. Pause is not in the dashboard (no proxy action) — pause it in GHL."
      ]
    },
    {
      "id": "quotes",
      "title": "Quotes & Follow-Up (Cotizaciones)",
      "path": "tile Quotes & Follow-Up",
      "tile": "quotes",
      "purpose": "Open estimates (cotizaciones) per market: edit drafts, send them, and turn an accepted quote into an invoice.",
      "howToGetThere": "Tile 'Quotes & Follow-Up'.",
      "whatYouSee": [
        "Open quotes per market with status",
        "Row buttons: Edit (drafts), Send to customer / Resend, Create invoice (accepted)",
        "'+ New quote' (opens New Order)"
      ],
      "actions": [
        {
          "label": "Edit → Save draft",
          "whatItDoes": "Edits a draft quote in GHL.",
          "sideEffects": "ghl",
          "needsConfirmation": false
        },
        {
          "label": "Send to customer / Resend",
          "whatItDoes": "Sends the estimate to the customer.",
          "sideEffects": "email",
          "needsConfirmation": true
        },
        {
          "label": "Create invoice (accepted quote)",
          "whatItDoes": "Creates the real invoice (due on receipt) with the quote's lines and the county tax, then sends OUR pay page (pays and saves the card; stored on the invoice) by email + text. If the pay page cannot be made it sends GHL's link and says so. One request every 3 s, so it takes about half a minute.",
          "sideEffects": "email",
          "needsConfirmation": true
        },
        {
          "label": "+ New quote",
          "whatItDoes": "Opens a blank New Order.",
          "sideEffects": "none",
          "needsConfirmation": false
        }
      ],
      "gotchas": [
        "There is no automatic follow-up: a human resends."
      ]
    },
    {
      "id": "pickup",
      "title": "Schedule Pickup (Recoger unidades)",
      "path": "tile Schedule Pickup",
      "tile": "pickup",
      "purpose": "Request a pickup for an active rental: the request is written on the customer as a note for dispatch. It does NOT book the calendar, stop billing or remove the route stop — dispatch does those.",
      "howToGetThere": "Tile 'Schedule Pickup'.",
      "whatYouSee": [
        "Active rentals in both markets",
        "Search by name or phone",
        "'Request pickup' per customer"
      ],
      "actions": [
        {
          "label": "Request pickup",
          "whatItDoes": "One in-app dialog: the date (or 'ASAP', required) and an optional dispatch note, then 'Save pickup note' writes 'PICKUP REQUESTED — <date> — <note>' on the contact. Cancel saves nothing. It does NOT book the calendar and does NOT stop billing (the dialog says so).",
          "sideEffects": "ghl",
          "needsConfirmation": false
        }
      ],
      "gotchas": [
        "After the request: book the pickup on the calendar, stop the billing ('Recurring…' on the customer card → Cancel schedule, or Billing → Unenroll) and adjust the route."
      ]
    },
    {
      "id": "routes",
      "title": "Routes (Rutas)",
      "path": "tile Routes",
      "tile": "routes",
      "purpose": "A read-only live view of today's route from HelloTracks (no HelloTracks login needed): stops by date, address search and a map.",
      "howToGetThere": "Tile 'Routes' (the only tile the field role sees).",
      "whatYouSee": [
        "Today's Route: date picker, Today, Refresh",
        "Address Search",
        "Map"
      ],
      "actions": [],
      "gotchas": [
        "Target stops per driver and day: about 20-26 on a local route, 15-20 on a far route."
      ]
    },
    {
      "id": "calendar",
      "title": "Calendar (Calendario)",
      "path": "tile Calendar",
      "tile": "calendar",
      "purpose": "Deliveries, pickups and services on the Google calendars the routing sync reads.",
      "howToGetThere": "Tile 'Calendar'.",
      "whatYouSee": [
        "'Connect Google Calendar' (first time)",
        "WPB / ORL switch",
        "Events by calendar and day",
        "Create / edit an event (events the dashboard or you created; others open in Google Calendar)"
      ],
      "actions": [
        {
          "label": "Connect Google Calendar",
          "whatItDoes": "Connects the Google account (OAuth).",
          "sideEffects": "none",
          "needsConfirmation": false
        },
        {
          "label": "Create event / Save",
          "whatItDoes": "Writes the Google calendar right away (the routing bot syncs it the next afternoon).",
          "sideEffects": "calendar",
          "needsConfirmation": false
        },
        {
          "label": "Delete (this occurrence / the event)",
          "whatItDoes": "Deletes from the Google calendar.",
          "sideEffects": "calendar",
          "needsConfirmation": true
        }
      ],
      "gotchas": [
        "Same-day changes are manual for the dispatcher; Orlando is not synced by the bot.",
        "Only events the dashboard created, or that you created, can be edited here."
      ]
    },
    {
      "id": "week-organizer",
      "title": "Week Organizer (Organizador semanal)",
      "path": "tile Week Organizer",
      "tile": "week-organizer",
      "purpose": "A read-only weekly view from HelloTracks: seven day columns with the stop count and up to 25 delivery / pickup / service chips per day.",
      "howToGetThere": "Tile 'Week Organizer'.",
      "whatYouSee": [
        "Prev / This week / Next",
        "Seven day columns with a stop count",
        "DEL / PU / SVC chips"
      ],
      "actions": [],
      "gotchas": [
        "Moving stops between days or drivers is done in HelloTracks, not here."
      ]
    },
    {
      "id": "price",
      "title": "Price Lookup (Precios)",
      "path": "tile Price Lookup",
      "tile": "price",
      "purpose": "The rate card by ZIP and service (the same catalog New Order uses).",
      "howToGetThere": "Tile 'Price Lookup'.",
      "whatYouSee": [
        "A service address with suggestions (the ZIP is taken from it) -> zone",
        "Prices per service for that zone"
      ],
      "actions": [],
      "gotchas": [
        "Trailers cost the same in every zone.",
        "Price increases apply to new orders only."
      ]
    },
    {
      "id": "reports",
      "title": "Reports (Reportes)",
      "path": "tile Reports",
      "tile": "reports",
      "purpose": "Revenue by month: totals by market, zone and status, a separate sales-tax-by-county table, CSV exports.",
      "howToGetThere": "Tile 'Reports'.",
      "whatYouSee": [
        "Prev / This month / Next, Refresh",
        "'By market' lines (WPB / ORL), zone and status totals",
        "'Sales tax by county' table (taxable, tax, lines) with its own Export CSV",
        "Export CSV of the invoices"
      ],
      "actions": [
        {
          "label": "Export CSV",
          "whatItDoes": "Downloads exactly what the month view shows (market column WPB / ORL).",
          "sideEffects": "none",
          "needsConfirmation": false
        },
        {
          "label": "Export CSV (tax by county)",
          "whatItDoes": "Downloads the county tax table.",
          "sideEffects": "none",
          "needsConfirmation": false
        }
      ],
      "gotchas": [
        "What Orlando billed: Reports, pick the month, read the ORL line in 'By market' (or the CSV, market = ORL).",
        "Drafts and voided invoices are excluded.",
        "A past month reads until the start of THAT month (one request every 3 s, so a far month takes a few seconds). A market that could not be read fully is said in red above the totals ('Orlando: incomplete — …; these totals are LOW') — refresh, or check that market in GHL."
      ]
    },
    {
      "id": "assistant",
      "title": "Assistant (Asistente)",
      "path": "tile Assistant · the Ask or dictate box",
      "tile": "assistant",
      "purpose": "A read-only beta: ask in words (prices by ZIP, customer lookups, invoice status) or dictate; it answers, it takes no action.",
      "howToGetThere": "Tile 'Assistant' or the Ask or dictate box on Home.",
      "whatYouSee": [
        "Chat",
        "Microphone (dictation)",
        "With the assistantDrafts flag: draft cards and customer candidates that open in New Order"
      ],
      "actions": [
        {
          "label": "Open draft in New Order",
          "whatItDoes": "Fills New Order with the dictated lines; nothing is sent.",
          "sideEffects": "none",
          "needsConfirmation": false,
          "flag": "assistantDrafts"
        }
      ],
      "gotchas": [
        "The Assistant never sends or charges anything by itself.",
        "With assistantDrafts on, 'Open in New Order' asks 'Discard this order?' first when New Order has unsaved lines; Cancel keeps that order."
      ]
    },
    {
      "id": "admin",
      "title": "Admin",
      "path": "tile Admin (owner)",
      "tile": "admin",
      "purpose": "Prices (catalog) per market, config summary and live health checks (owner).",
      "howToGetThere": "Tile 'Admin' (owner only).",
      "whatYouSee": [
        "Prices (catalog): each market's last update ('West Palm: N products, M prices · from <time> (<age>)')",
        "'Update prices now' (owner)",
        "Config: host, market, service-worker build",
        "Pricing data summary",
        "Health checks: GHL proxy per market, routes proxy (HelloTracks)",
        "Diagnostics (collapsed): billing version and file hashes (click to copy)"
      ],
      "actions": [
        {
          "label": "Update prices now",
          "whatItDoes": "Re-reads every product and price from GHL for West Palm, then Orlando (3 s apart) and says the result per market (✓ / ✗ previous prices stay / ? check). Nothing is sent or charged; New Order reads the fresh prices on its next visit.",
          "sideEffects": "none",
          "needsConfirmation": true
        },
        {
          "label": "Re-run checks",
          "whatItDoes": "Runs the health checks again.",
          "sideEffects": "none",
          "needsConfirmation": false
        }
      ],
      "gotchas": []
    },
    {
      "id": "office",
      "title": "Office view (Vista oficina)",
      "path": "?view=office (only while the officeMode flag is on, and for roles with Customers / Invoices / Collections / Billing / New Order) · #/home #/customers #/invoices #/orders #/collections #/billing #/payments",
      "tile": null,
      "purpose": "The desktop layout of the same dashboard: left navigation, global search, dense Customers / Invoices tables with a side panel, + New, bookmarkable routes.",
      "howToGetThere": "View = Office in the header (desktop, when the officeMode flag is on), or ?view=office.",
      "whatYouSee": [
        "Left nav: Home, Customers, Orders / Sites (opens New Order), Invoices, Collections, Recurring billing, Payments (invoices with a payment received)",
        "Global search (name, company, phone, email, address, invoice #)",
        "Invoices title row: summary tiles Drafts / Open / Overdue / Paid (30 days) — a click filters the list to exactly that tile (search and dates cleared); again = all",
        "Market chips",
        "+ New: New order / New invoice (both open New Order), Record payment (jumps to open invoices), New customer",
        "Clickable Home KPI tiles",
        "Invoices table: row icons ✉ (send) / $ (record payment) / ⋯ (the shared menu), bulk Send reminder (N), Export CSV",
        "Invoice side panel: the shared invoice menu + tabs Summary (status, customer, dates, pay link, amounts, open balance, addresses) · Lines (the line editor) · Payments · Activity (the customer's notes about THIS invoice) · Notes (what the customer reads)",
        "Manual link at the bottom of the left nav",
        "Customer side panel: tabs Summary · Invoices · Payments · Notes · Contacts",
        "Colors (brand): Deep Blue sidebar, Brand Blue on the active item, ONE green button per screen (+ New)"
      ],
      "actions": [
        {
          "label": "+ New",
          "whatItDoes": "Starts a new order / invoice / customer (asks before discarding an unsaved order).",
          "sideEffects": "none",
          "needsConfirmation": false
        },
        {
          "label": "Send reminder (N) — Invoices table",
          "whatItDoes": "Runs the Collections reminder dialog for each selected open invoice.",
          "sideEffects": "email",
          "needsConfirmation": true
        },
        {
          "label": "Export CSV — Invoices table",
          "whatItDoes": "Downloads the table.",
          "sideEffects": "none",
          "needsConfirmation": false
        }
      ],
      "gotchas": [
        "Office spaces its server requests 3 seconds apart (the host blocks bursts).",
        "Keys: / search, n + New, Esc closes the panel.",
        "The Payments / Activity tabs show only notes that name THIS invoice (inv=, num=, 'invoice #N', the proxy's payment / refund notes) — a note that mentions the invoice some other way is on the customer's Notes tab."
      ]
    },
    {
      "id": "pay-page",
      "title": "Pay page (Página de pago)",
      "path": "app/pay.html?t=<token>",
      "tile": null,
      "purpose": "OUR customer-facing pay page (Stripe): pays one invoice and saves the card for future services (with consent).",
      "howToGetThere": "The link in the invoice email / text, 'Open payment link', 'Copy link'.",
      "whatYouSee": [
        "Invoice number and amount",
        "Card / Apple Pay / Google Pay",
        "After paying: 'Send receipt' by email or text",
        "Staff mode (opened by New Order 'Enter card now'): 'Enter the customer's card', no wallets; after a partial charge, 'Charge the remaining … to another card'"
      ],
      "actions": [
        {
          "label": "Pay now",
          "whatItDoes": "Charges the card at once (the consent text is shown above it), records the payment in GHL, saves the card.",
          "sideEffects": "money",
          "needsConfirmation": false
        },
        {
          "label": "Send receipt",
          "whatItDoes": "Emails or texts the receipt.",
          "sideEffects": "email",
          "needsConfirmation": false
        }
      ],
      "gotchas": [
        "One live link per invoice: a newer link replaces the older one ('use the link in the most recent email or text').",
        "The page re-checks the invoice before charging (paid, void, amount changed -> refused).",
        "The code keeps Stripe's Link in customer mode while the decision log says card only — architect to confirm.",
        "The invoice / reminder / receipt EMAILS and the pay page have the website look (D70): blue band with the white logo and the invoice number, the amount, a green PAY NOW button, a Deep Blue footer with the main phone line."
      ]
    }
  ],
  "flows": [
    {
      "id": "send-invoice-saves-card",
      "title": "Send an invoice that saves the card",
      "steps": [
        {
          "screen": "new-order",
          "action": "Find the customer (or create one), set the service address, add the lines",
          "note": "The market badge follows the address."
        },
        {
          "screen": "new-order",
          "action": "Leave 'Send GHL's link instead' unticked (the default is our pay page)",
          "note": ""
        },
        {
          "screen": "new-order",
          "action": "Send as INVOICE, then Confirm",
          "note": "The confirm sheet shows lines, tax, total and the channels."
        }
      ],
      "outcome": "The invoice exists in GHL as sent; the customer gets our pay page link; paying saves the card.",
      "verifyWhere": "The result screen (Pay link = our pay page) and Invoices (status sent)."
    },
    {
      "id": "send-pay-page-existing",
      "title": "Send our pay page for an invoice that already exists",
      "steps": [
        {
          "screen": "payments",
          "action": "Find the invoice",
          "note": ""
        },
        {
          "screen": "payments",
          "action": "Send pay page (saves card), choose email / text, Send",
          "note": "A draft is finalized first; nothing reaches the customer before Send."
        }
      ],
      "outcome": "The customer gets our pay page link.",
      "verifyWhere": "Invoices row (status), the customer's History (note)."
    },
    {
      "id": "record-cash-zelle",
      "title": "Record a cash / check / Zelle payment",
      "steps": [
        {
          "screen": "payments",
          "action": "Record payment on the invoice",
          "note": "Behind the recordPayments flag."
        },
        {
          "screen": "payments",
          "action": "Amount, method, date -> Record (click twice)",
          "note": ""
        }
      ],
      "outcome": "The payment is recorded in GHL (no card charged); the invoice shows paid or the new balance.",
      "verifyWhere": "Invoices row ('Paid … of …') and the customer's History."
    },
    {
      "id": "refund",
      "title": "Refund a card payment",
      "steps": [
        {
          "screen": "payments",
          "action": "Refund on a paid invoice (owner only)",
          "note": "Behind the refunds flag."
        },
        {
          "screen": "payments",
          "action": "Full or partial amount, reason -> Refund",
          "note": "A full refund also voids the invoice."
        }
      ],
      "outcome": "Stripe refunds the card; a note records it.",
      "verifyWhere": "The toast, the invoice status, Stripe."
    },
    {
      "id": "charge-saved-card",
      "title": "Charge a saved card",
      "steps": [
        {
          "screen": "payments",
          "action": "'Charge <amount> to <card>' on an open invoice",
          "note": "Only shown when the contact has a card."
        },
        {
          "screen": "payments",
          "action": "Confirm",
          "note": "The server charges the exact amount due."
        }
      ],
      "outcome": "The invoice is paid by the card on file.",
      "verifyWhere": "The toast (PI id) and the invoice status."
    },
    {
      "id": "edit-draft",
      "title": "Edit a draft invoice",
      "steps": [
        {
          "screen": "payments",
          "action": "Edit draft",
          "note": "Opens in New Order with everything loaded."
        },
        {
          "screen": "new-order",
          "action": "Change lines / notes / address -> Save changes to draft #N",
          "note": "Updates the same draft; sending it replaces the draft."
        }
      ],
      "outcome": "The same draft, updated.",
      "verifyWhere": "Invoices (draft row)."
    },
    {
      "id": "change-market",
      "title": "Bill an order in the other market",
      "steps": [
        {
          "screen": "new-order",
          "action": "Override next to the market badge, pick the market",
          "note": ""
        },
        {
          "screen": "new-order",
          "action": "Type the reason",
          "note": "Required; it goes on the invoice."
        }
      ],
      "outcome": "The invoice is created in the chosen market with 'Market override: … — <reason>' in its terms.",
      "verifyWhere": "The invoice terms and the market badge."
    },
    {
      "id": "find-customer-history",
      "title": "Find a customer and its history",
      "steps": [
        {
          "screen": "customers",
          "action": "Search by name, phone or email (or click the name anywhere)",
          "note": ""
        },
        {
          "screen": "customers",
          "action": "Read History (invoices, payments, notes, orders, recurring)",
          "note": "Searchable."
        }
      ],
      "outcome": "Everything about the customer in one place.",
      "verifyWhere": "The profile."
    },
    {
      "id": "preview-before-send",
      "title": "Preview before sending",
      "steps": [
        {
          "screen": "payments",
          "action": "Preview on the invoice",
          "note": "Read only, our templates."
        }
      ],
      "outcome": "You see the email / text the customer would get.",
      "verifyWhere": "The preview dialog."
    },
    {
      "id": "orlando-billed",
      "title": "See what Orlando billed in a month",
      "steps": [
        {
          "screen": "reports",
          "action": "Pick the month",
          "note": ""
        },
        {
          "screen": "reports",
          "action": "Read the ORL line in 'By market', or Export CSV (market = ORL)",
          "note": ""
        }
      ],
      "outcome": "Orlando's billed total for the month (see the Reports known gap for past months).",
      "verifyWhere": "Reports."
    },
    {
      "id": "collect-past-due",
      "title": "Work the past-due queue",
      "steps": [
        {
          "screen": "collections",
          "action": "Start at the top (worst first), read NEXT",
          "note": ""
        },
        {
          "screen": "collections",
          "action": "Send reminder (the ladder dialog) or Call / Promised to pay",
          "note": "Every action is recorded as a note."
        }
      ],
      "outcome": "Each past-due invoice has its next step done and recorded.",
      "verifyWhere": "Collections (last reminder) and the customer's History."
    },
    {
      "id": "edit-sent-invoice",
      "title": "Change an invoice that was already sent (add a line)",
      "steps": [
        {
          "screen": "payments",
          "action": "Edit invoice on the sent invoice",
          "note": "Behind the editSentInvoices flag; not for paid invoices."
        },
        {
          "screen": "new-order",
          "action": "Add / change lines, notes, Job/PO -> Save changes to invoice #N",
          "note": "Nothing reaches the customer yet."
        },
        {
          "screen": "new-order",
          "action": "Resend updated invoice",
          "note": "One message with a new pay link for the new amount."
        }
      ],
      "outcome": "The same invoice with the new lines and total; the customer has one link that pays the new amount.",
      "verifyWhere": "The invoice's notes ('Updated …') and the customer's History."
    },
    {
      "id": "cancel-recurring",
      "title": "Stop a customer's recurring billing (early pickup, cancelled service)",
      "steps": [
        {
          "screen": "customers",
          "action": "Open the customer → Sites card → Recurring…",
          "note": "Or 'Recurring…' on one of their invoices."
        },
        {
          "screen": "customers",
          "action": "Cancel schedule on the template → confirm",
          "note": "House rule: the period already started is charged in full — no proration, no refunds."
        }
      ],
      "outcome": "GHL makes no more invoices from that template; already-made invoices are unchanged (void or refund them separately).",
      "verifyWhere": "The dialog re-reads GHL and no longer lists it; a 'RECURRING CANCELLED' note on the customer's History."
    }
  ],
  "rules": [
    {
      "id": "market-service-address",
      "text": "The market (West Palm / Orlando) is decided by the SERVICE address: its ZIP's zone first, then the county list; overriding needs a reason that goes on the invoice.",
      "source": "docs/DESIGN-market-attribution.md"
    },
    {
      "id": "base-price",
      "text": "Base prices are the SEFL column of the master catalog (v20); zones are applied on top by the agents, never in ServiceCore.",
      "source": "DECISIONES.md · Precios (2026-09-19)"
    },
    {
      "id": "trailers-flat",
      "text": "Trailers cost the same in every zone (no zone factor).",
      "source": "DECISIONES.md · Precios (2026-08-28)"
    },
    {
      "id": "trailer-periods",
      "text": "Trailers rent by Day/Event (up to 4 days), 7 days or 28 days; one extra day is another Day/Event, two or more extra days = quote the week. No extension rate.",
      "source": "DECISIONES.md · Precios (2026-08-28)"
    },
    {
      "id": "increases-new-orders",
      "text": "Price increases apply to new orders only; existing customers keep their price until decided otherwise.",
      "source": "DECISIONES.md · Precios (2026-09-26)"
    },
    {
      "id": "deluxe",
      "text": "'Deluxe' is not a separate unit: it is a standard unit in better condition, reserved for events. VIP is only the VIP with flush.",
      "source": "DECISIONES.md · Producto y flota (2026-09-26)"
    },
    {
      "id": "trailer-fleet",
      "text": "Trailer fleet: 2, 3, 4, 6, 8 and 10 stations; 5, 7 and 9 exist on invoices but deliver 6, 8 or 10 at the same price — never a smaller one.",
      "source": "DECISIONES.md · Producto y flota (2026-09-29)"
    },
    {
      "id": "no-third-parties",
      "text": "The catalog never mentions third-party providers.",
      "source": "DECISIONES.md · Producto y flota (2026-09-18)"
    },
    {
      "id": "cycle-28",
      "text": "Recurring services bill every 28 days, no proration (event vs. construction rule); billing comparisons use billingStart / billingEnd, never the issue date.",
      "source": "DECISIONES.md · Datos y herramientas (2026-09-19, 2026-09-26)"
    },
    {
      "id": "dashboard-collects",
      "text": "The dashboard is the collector; GHL is the accounting book. GHL autopay is not used (paying one invoice there does not save the card).",
      "source": "DECISIONES.md · Datos y herramientas (2026-09-19)"
    },
    {
      "id": "first-cycle-held",
      "text": "The billing engine always holds the first cycle of every site for approval, and stops a charge at its gates (tolerance, jump vs. last cycle, per-charge cap, daily total).",
      "source": "DECISIONES.md · Datos y herramientas (2026-09-19)"
    },
    {
      "id": "card-only",
      "text": "Dashboard payments are by card only (Apple Pay and Google Pay included); Link and PayPal are off on purpose.",
      "source": "DECISIONES.md · Datos y herramientas"
    },
    {
      "id": "tax-county",
      "text": "Sales tax is the county of the service address (Florida, destination based); a county with no tax configured in the billing market blocks invoicing until it is added in GHL.",
      "source": "app/county-taxes.json"
    },
    {
      "id": "tax-exempt",
      "text": "Tax exempt needs a reason, and for a customer the Florida certificate on the contact; an expired certificate is said on screen.",
      "source": "D43 / D46"
    },
    {
      "id": "due-on-receipt",
      "text": "Invoices are due on receipt (house rule).",
      "source": "New Order / Quotes"
    },
    {
      "id": "no-discount",
      "text": "No discounts: price is price.",
      "source": "DECISIONES.md · Agentes de voz y SMS (2026-09-23)"
    },
    {
      "id": "human-click",
      "text": "Nothing reaches a customer or moves money without a human click in the dashboard; GHL's automatic reminders are off.",
      "source": "PPTG-AGENT-BRIEF.md"
    },
    {
      "id": "one-pay-link",
      "text": "One live pay link per invoice: a newer link replaces the older one, and the pay page refuses an invoice that is paid, void or whose amount changed.",
      "source": "reviews/ARCH-D61-PHP-2026-10-10.md"
    }
  ],
  "flags": [
    {
      "name": "officeMode",
      "state": true,
      "meaning": "Shows the View switch (Auto / Phone / Office) in the header and the Office layout on desktop. ON since D73 (owner, 2026-10-13).",
      "whoFlips": "Owner decision, Code changes app/config.js in a PR."
    },
    {
      "name": "recordPayments",
      "state": false,
      "meaning": "'Record payment' (cash / check / Zelle) on open invoices.",
      "whoFlips": "Owner decision, Code in a PR."
    },
    {
      "name": "refunds",
      "state": false,
      "meaning": "'Refund' on paid invoices (owner only on the server too).",
      "whoFlips": "Owner decision, Code in a PR."
    },
    {
      "name": "mixedOrders",
      "state": false,
      "meaning": "Once / Recurring per line and the billing cycle in New Order.",
      "whoFlips": "Owner decision, Code in a PR."
    },
    {
      "name": "taxExempt",
      "state": true,
      "meaning": "The 'No tax on this invoice' option with a required reason.",
      "whoFlips": "Owner decision, Code in a PR."
    },
    {
      "name": "invoiceCc",
      "state": true,
      "meaning": "CC emails on an invoice (saved back to the contact).",
      "whoFlips": "Owner decision, Code in a PR."
    },
    {
      "name": "assistantDrafts",
      "state": false,
      "meaning": "The Assistant can hand a dictated order to New Order.",
      "whoFlips": "Owner decision, Code in a PR."
    },
    {
      "name": "editSentInvoices",
      "state": true,
      "meaning": "'Edit invoice' on sent / overdue / partially paid invoices (edit in New Order, Save updates the same invoice, then 'Resend updated invoice' with a fresh pay link). ON since batch 3 (proxy.php e1821ce4 live); the architect checks it live with the robot after the deploy.",
      "whoFlips": "Owner decision (the architect runs the robot live check right after), Code in a PR."
    }
  ],
  "proxyActions": {
    "findContactByPhone": {
      "purpose": "Find a GHL contact by phone.",
      "readOnly": true,
      "moneyOrMessage": false
    },
    "searchContacts": {
      "purpose": "Search / page GHL contacts.",
      "readOnly": true,
      "moneyOrMessage": false
    },
    "getContact": {
      "purpose": "Read one contact.",
      "readOnly": true,
      "moneyOrMessage": false
    },
    "createContact": {
      "purpose": "Create a contact.",
      "readOnly": false,
      "moneyOrMessage": false
    },
    "updateContact": {
      "purpose": "Update a contact (read back by the caller).",
      "readOnly": false,
      "moneyOrMessage": false
    },
    "getOpportunities": {
      "purpose": "Read pipeline opportunities.",
      "readOnly": true,
      "moneyOrMessage": false
    },
    "createOpportunity": {
      "purpose": "Create an opportunity.",
      "readOnly": false,
      "moneyOrMessage": false
    },
    "listInvoices": {
      "purpose": "List invoices of a market (paged).",
      "readOnly": true,
      "moneyOrMessage": false
    },
    "createInvoice": {
      "purpose": "Create an invoice.",
      "readOnly": false,
      "moneyOrMessage": false
    },
    "sendInvoice": {
      "purpose": "Send / finalize an invoice through GHL.",
      "readOnly": false,
      "moneyOrMessage": true
    },
    "getInvoice": {
      "purpose": "Read one invoice.",
      "readOnly": true,
      "moneyOrMessage": false
    },
    "voidInvoice": {
      "purpose": "Void an invoice.",
      "readOnly": false,
      "moneyOrMessage": false
    },
    "updateInvoice": {
      "purpose": "Update an invoice (full object PUT).",
      "readOnly": false,
      "moneyOrMessage": false
    },
    "listUsers": {
      "purpose": "GHL users (the invoicing user).",
      "readOnly": true,
      "moneyOrMessage": false
    },
    "listProducts": {
      "purpose": "GHL products.",
      "readOnly": true,
      "moneyOrMessage": false
    },
    "getCalendars": {
      "purpose": "GHL calendars.",
      "readOnly": true,
      "moneyOrMessage": false
    },
    "createAppointment": {
      "purpose": "Book a GHL appointment.",
      "readOnly": false,
      "moneyOrMessage": false
    },
    "whoAmI": {
      "purpose": "Who is signed in (role).",
      "readOnly": true,
      "moneyOrMessage": false
    },
    "bulkUpsertProducts": {
      "purpose": "Catalog maintenance (admin).",
      "readOnly": false,
      "moneyOrMessage": false
    },
    "purgeProducts": {
      "purpose": "Catalog maintenance (admin).",
      "readOnly": false,
      "moneyOrMessage": false
    },
    "getCatalog": {
      "purpose": "The price catalog both markets use (cached server side).",
      "readOnly": true,
      "moneyOrMessage": false
    },
    "listEstimates": {
      "purpose": "List quotes (estimates).",
      "readOnly": true,
      "moneyOrMessage": false
    },
    "createEstimate": {
      "purpose": "Create a quote.",
      "readOnly": false,
      "moneyOrMessage": false
    },
    "sendEstimate": {
      "purpose": "Send a quote to the customer.",
      "readOnly": false,
      "moneyOrMessage": true
    },
    "updateEstimate": {
      "purpose": "Update a quote.",
      "readOnly": false,
      "moneyOrMessage": false
    },
    "createContactNote": {
      "purpose": "Write a note on a contact (History).",
      "readOnly": false,
      "moneyOrMessage": false
    },
    "listContactNotes": {
      "purpose": "Read a contact's notes.",
      "readOnly": true,
      "moneyOrMessage": false
    },
    "echo": {
      "purpose": "Health check.",
      "readOnly": true,
      "moneyOrMessage": false
    },
    "listSchedules": {
      "purpose": "GHL recurring invoice schedules.",
      "readOnly": true,
      "moneyOrMessage": false
    },
    "createSchedule": {
      "purpose": "Create a recurring invoice template.",
      "readOnly": false,
      "moneyOrMessage": false
    },
    "scheduleInvoiceSchedule": {
      "purpose": "Activate a recurring schedule.",
      "readOnly": false,
      "moneyOrMessage": true
    },
    "cancelInvoiceSchedule": {
      "purpose": "Cancel a recurring schedule.",
      "readOnly": false,
      "moneyOrMessage": false
    },
    "askAssistant": {
      "purpose": "The Assistant (answers; may return a draft order).",
      "readOnly": true,
      "moneyOrMessage": false
    },
    "sendEmail": {
      "purpose": "Email a contact (our templates).",
      "readOnly": false,
      "moneyOrMessage": true
    },
    "sendSms": {
      "purpose": "Text a contact (our templates).",
      "readOnly": false,
      "moneyOrMessage": true
    },
    "calAuthUrl": {
      "purpose": "Connect Google Calendar.",
      "readOnly": true,
      "moneyOrMessage": false
    },
    "calStatus": {
      "purpose": "Is Google Calendar connected.",
      "readOnly": true,
      "moneyOrMessage": false
    },
    "listCalEvents": {
      "purpose": "Read calendar events.",
      "readOnly": true,
      "moneyOrMessage": false
    },
    "createCalEvent": {
      "purpose": "Create a calendar event.",
      "readOnly": false,
      "moneyOrMessage": false
    },
    "updateCalEvent": {
      "purpose": "Update a calendar event.",
      "readOnly": false,
      "moneyOrMessage": false
    },
    "deleteCalOccurrence": {
      "purpose": "Delete one occurrence of an event.",
      "readOnly": false,
      "moneyOrMessage": false
    },
    "deleteInvoiceDraft": {
      "purpose": "Delete a draft invoice.",
      "readOnly": false,
      "moneyOrMessage": false
    },
    "signOut": {
      "purpose": "End this session.",
      "readOnly": false,
      "moneyOrMessage": false
    },
    "signOutAll": {
      "purpose": "End every session of the user.",
      "readOnly": false,
      "moneyOrMessage": false
    },
    "stripePayToken": {
      "purpose": "Make (or reuse) our pay page link for an invoice.",
      "readOnly": false,
      "moneyOrMessage": false
    },
    "stripePayExchange": {
      "purpose": "The pay page's read of its link (re-checks the invoice).",
      "readOnly": true,
      "moneyOrMessage": false
    },
    "stripePayConfirm": {
      "purpose": "The pay page reports a payment; recorded in GHL.",
      "readOnly": false,
      "moneyOrMessage": true
    },
    "stripeChargeCard": {
      "purpose": "Charge the saved card for the amount due (human click).",
      "readOnly": false,
      "moneyOrMessage": true
    },
    "stripeCardStatus": {
      "purpose": "Does the contact have a saved card.",
      "readOnly": true,
      "moneyOrMessage": false
    },
    "recordPayment": {
      "purpose": "Record a cash / check / Zelle payment in GHL.",
      "readOnly": false,
      "moneyOrMessage": true
    },
    "refundInvoice": {
      "purpose": "Refund a card payment (owner only).",
      "readOnly": false,
      "moneyOrMessage": true
    },
    "text2payInvoice": {
      "purpose": "Create an invoice as a draft (with its GHL link).",
      "readOnly": false,
      "moneyOrMessage": false
    }
  },
  "files": [
    {
      "path": "app/index.html",
      "purpose": "The dashboard (all screens, one page).",
      "owner": "code"
    },
    {
      "path": "app/config.js",
      "purpose": "Settings and feature flags.",
      "owner": "code"
    },
    {
      "path": "app/office.js",
      "purpose": "The Office view.",
      "owner": "code"
    },
    {
      "path": "app/order.js",
      "purpose": "The order builder (catalog, lines).",
      "owner": "code"
    },
    {
      "path": "app/pricing.js / app/prices.json",
      "purpose": "Zone prices from the master catalog.",
      "owner": "code"
    },
    {
      "path": "app/zones.js",
      "purpose": "Markets and the ZIP -> zone -> market rules.",
      "owner": "code"
    },
    {
      "path": "app/county-taxes.json",
      "purpose": "County sales tax per market (tax ids).",
      "owner": "code"
    },
    {
      "path": "app/customers/",
      "purpose": "The Customers hub.",
      "owner": "code"
    },
    {
      "path": "app/pay.html",
      "purpose": "Our pay page.",
      "owner": "code"
    },
    {
      "path": "app/manual/",
      "purpose": "This manual (manual.json is the source).",
      "owner": "code"
    },
    {
      "path": "app/sw.js",
      "purpose": "The service worker (one version bump per change).",
      "owner": "code"
    },
    {
      "path": "app/api/proxy.php",
      "purpose": "The server proxy to GHL and Stripe.",
      "owner": "architect"
    },
    {
      "path": "app/api/billing.php",
      "purpose": "The recurring-billing engine.",
      "owner": "architect"
    },
    {
      "path": "app/api/secrets.php",
      "purpose": "Keys (never in git, never printed).",
      "owner": "owner"
    }
  ],
  "doNot": [
    "Never send, charge, refund or void without a human click in the dashboard.",
    "Use only the robot contact for probes and tests; never a real customer.",
    "Never paste or print secrets (tokens, keys, secrets.php).",
    "Never re-enable GHL's automatic reminders.",
    "Never make more than one server request every 3 seconds in automation (the host blocks bursts).",
    "Never edit PHP from the dashboard side (proxy.php and billing.php belong to the architect; the owner uploads them).",
    "Never merge or deploy without the owner.",
    "Never promise a price or a discount that the catalog does not have.",
    "Never treat preview / access-key mode as a sandbox: it reaches the real server."
  ],
  "glossary": [
    {
      "term": "Invoice",
      "meaning": "A GHL invoice; the dashboard sends and collects it.",
      "es": "Factura"
    },
    {
      "term": "Draft",
      "meaning": "An invoice that exists but was not sent.",
      "es": "Borrador"
    },
    {
      "term": "Quote / Estimate",
      "meaning": "A price proposal; becomes an invoice when accepted.",
      "es": "Cotización"
    },
    {
      "term": "Pay page",
      "meaning": "Our Stripe page that pays an invoice and saves the card.",
      "es": "Página de pago"
    },
    {
      "term": "Market",
      "meaning": "West Palm (PPTG) or Orlando (partner), each a GHL sub-account.",
      "es": "Mercado"
    },
    {
      "term": "Service address / Site",
      "meaning": "Where the units are; decides market, zone and tax.",
      "es": "Dirección de servicio / Sitio"
    },
    {
      "term": "Zone",
      "meaning": "The price band of a ZIP (SEFL is the base).",
      "es": "Zona"
    },
    {
      "term": "Cycle",
      "meaning": "One 28-day billing period of a recurring service.",
      "es": "Ciclo"
    },
    {
      "term": "Collections",
      "meaning": "Following up past-due invoices.",
      "es": "Cobranza"
    },
    {
      "term": "Past due",
      "meaning": "An open invoice after its due date.",
      "es": "Vencida"
    },
    {
      "term": "Void",
      "meaning": "Cancel an unpaid invoice (it stays as a record).",
      "es": "Anular"
    },
    {
      "term": "Refund",
      "meaning": "Return money to the card.",
      "es": "Reembolso"
    },
    {
      "term": "Job/PO",
      "meaning": "The customer's job or purchase-order number, printed on the invoice.",
      "es": "Trabajo / Orden de compra"
    },
    {
      "term": "Robot contact",
      "meaning": "The test contact used for every probe.",
      "es": "Contacto robot"
    }
  ]
}
