bottleneckcrm/docs/api/modules.md

14 KiB

Modules

An org admin can switch whole parts of BottleCRM off for the org. This page is the API side: reading and changing the setting, what an off module answers, and what keeps running or stops while it is off. The admin's view of the same feature is Modules.

The catalogue is backend/common/modules.py; the endpoints are OrgModulesView (backend/common/views/module_views.py) and change_modules (backend/common/module_settings.py).

Switching a module off never deletes or changes a record. Everything is still stored, and it all comes back when the module is switched on.

The modules

Key Label Covers Needs
deals Pipeline Deals, deal pipelines, line items, the deal board and stage aging.
leads Leads Leads, lead pipelines, and lead import and export.
goals Goals Sales goals, the leaderboard and goal history. deals
tasks Tasks Tasks, boards, the task calendar and its calendar feed.
tickets Tickets Tickets, approvals, ticket analytics, routing, escalation, the reopen policy, business hours, saved replies, inbound email, CSAT and portal tickets.
knowledge_base Knowledge base Articles, the public help center and portal articles.
documents Documents Documents and their downloads.
invoices Invoices Invoices, estimates, recurring invoices, invoice templates, invoice reports, account statements, and public invoice and estimate links.
timesheet Timesheet Time entries, the timesheet and the time report. tickets

The groups, in the order they are served, are sell (deals, leads, goals), serve (tasks, tickets, knowledge base, documents) and bill (invoices, timesheet). Within a group the modules follow the org's saved order.

A label follows the org's terminology: an org that calls deals "Opportunities" sees that name for deals, and likewise for leads and invoices. Every module message uses the org's label.

Needs. A module that needs another is available only while that one is. Goals needs Pipeline and Timesheet needs Tickets. Switching the needed module off makes the dependent one unavailable without changing its own setting, so switching the needed one back on restores it.

Read the setting

GET /api/org/modules/, any member of the org.

{
  "modules": [
    {
      "key": "deals",
      "label": "Pipeline",
      "group": "sell",
      "covers": "Deals, deal pipelines, line items, the deal board and stage aging.",
      "stops": "Everything in Pipeline leaves every menu, search and Today, and stale-deal emails stop. Converting a lead no longer creates a deal, and Goals switches off too.",
      "enabled": false,
      "available": false,
      "needs": null
    },
    {
      "key": "goals",
      "label": "Goals",
      "group": "sell",
      "covers": "Sales goals, the leaderboard and goal history.",
      "stops": "Goals leave every menu, and goal milestone emails stop.",
      "enabled": true,
      "available": false,
      "needs": "deals"
    }
  ],
  "can_edit": true
}

The response has all nine rows (two are shown). enabled is the module's own setting; available is enabled and its need available. stops is the confirmation text an admin reads before switching the module off, already carrying the org's label. can_edit is true for admins and false for members.

A request with no session answers 403 (every unauthenticated API request does), and so does a session with no organization.

Change the setting

PUT /api/org/modules/, admins only. Both fields are required, and the body replaces the setting:

{"off": ["tickets", "timesheet"], "order": ["deals", "leads", "goals", "tasks", "tickets", "knowledge_base", "documents", "invoices", "timesheet"]}

off lists the modules to switch off; every module not named is on. order lists module keys in the order the org wants them; modules are only ordered within their group, and a module left out of order follows the named ones. A successful PUT answers 200 with the same body as the GET. PATCH is 405.

A member is refused with 403:

{"error": true, "errors": "Only admins can switch modules on or off."}

The body is checked before anything is saved, and an invalid request changes nothing. A 400 carries the problems under off and order:

Problem Field Message
Field missing off or order This field is required.
Not a list of strings off or order Send a list of module keys.
A key that is not a module off or order Unknown module: "crm". (one entry per key)
A key named twice order Name each module once.
A module switched on while its need stays off off Goals needs Pipeline. Switch Pipeline on first.
A web form still takes submissions off Leads still receives submissions from the published web form "Contact us". Unpublish it first, so no submission is lost.
Several such forms off Tickets still receives submissions from 2 published web forms: "Billing help", "Zed help". Unpublish them first, so no submission is lost.
An active mailbox off Tickets still receives email at help@example.com. Switch that mailbox off first, so no email is lost.
Several active mailboxes off Tickets still receives email at 2 mailboxes: a@example.com, b@example.com. Switch them off first, so no email is lost.

off can carry several messages at once. Lists of forms or mailboxes name at most five and then say and N more. The form and mailbox checks apply only to a module the change makes unavailable, so an admin can still reorder while a module is already off. Forms for leads count against Leads, and forms for tickets and active inbound mailboxes count against Tickets; because Timesheet needs Tickets, switching Tickets off is refused for the same reasons.

A body that is not valid JSON answers DRF's own 400, {"detail": "JSON parse error ..."}.

A change that alters the off list or the order is written to the security audit log as MODULES_CHANGED.

What an off module answers

A signed-in request to a route of an unavailable module answers 404 for every method (GET, HEAD, OPTIONS, POST, PUT, PATCH, DELETE):

{"detail": "Tickets is switched off for this organization.", "code": "module_off", "module": "tickets"}

This applies to a session, a personal access token and the organization API key alike. The module named is the one that is actually off: with Goals on and Pipeline off, a goals route names deals. A personal access token's own scope refusal (403) still comes first, and the organization API key is read-only, so a write with it is refused as it always was.

Some routes need more than one module. The time entry routes need Tickets and Timesheet; invoice creation from a deal needs Invoices and Pipeline; invoice creation from time entries needs Invoices, Tickets and Timesheet. Products belong to Pipeline or Invoices and stay reachable while either is on. GET /api/org/help-center/ is the Knowledge base's settings route and answers the same 404 for every method while it is off.

Anything outside the nine modules (accounts, contacts, web forms, webhooks, automation, search, the dashboard and the rest of /api/org/) is always reachable.

Public, portal and inbound entry points

These are not signed-in requests, so they answer what they already answer for a link that does not exist, and never name the module:

Entry point Module Answer while off
Public invoice and estimate links (view, PDF, accept, decline) Invoices 404 {"error": true, "message": "Invoice not found or link expired"} (the same body for an estimate link)
CSAT survey link Tickets 410 {"error": "Survey link is no longer valid."}
Inbound email webhook for a mailbox Tickets 404 {"error": true, "errors": "Mailbox not found"}
Portal tickets (list, create, detail, comment) Tickets 404 {"error": "Case not found"}
Portal articles (list, detail) Knowledge base 404 {"error": "Article not found"}
Portal article suggestions Knowledge base 200 {"articles": []}, as an empty query answers
Public help center Knowledge base 404 {"error": "Not found"}
Calendar feed link Tasks 404 {"error": "Not found"}
Web form page, definition and submit Leads or Tickets, by the form's target 404, as for an unknown form

A web form's CORS response headers follow the same rule: while the form's target module is off they match an unknown form's, so nothing tells an outsider which case it was. Sign-in to the portal is unaffected.

Web forms

A form belongs to Leads or Tickets by its target. While that module is off:

  • Publishing the form is refused with 400: Leads is switched off for this organization. Switch it on before publishing this form.
  • Creating a form with no target while Leads is off is 400 under target, because the default target is lead. Choosing a target whose module is off, or changing a form to one, is refused the same way, with the module message.
  • A form that was already published cannot be left behind: switching the module off is refused while it is published (see the table above).

Records that mention an off module

Responses keep their keys when they embed records of an off module; the embedded part is empty: lists are [], counts are 0, and single links and summaries are null. Specific cases:

  • A time entry keeps a read-only invoiced boolean while Invoices is off; its invoice is null.
  • A deal's next_activity is null while Tasks is off.
  • A product's used_on is 0 while Invoices is off.
  • Task webhook payloads still carry the bare ids of a task's deal, ticket or lead.

A write that omits or sends null for a link to an off module's record keeps the stored link. A write that names a different record of an off module is 400 on that field, with the module message. Choosing an off module's webhook event, saved view list, custom field target or web form target is refused the same way.

Other surfaces follow the modules too:

  • Saved views of an off module are left out of the list and answer 404; creating one is refused.
  • Webhooks. Nothing is emitted for an off module, and the event catalogue leaves it out. An endpoint keeps the events it already stored for an off module, and subscribing to a new one is 400 under events with the module message. Deliveries already queued are still sent.
  • Automation. A rule whose object belongs to an off module lists with status paused and the text Paused: Tickets is switched off, and does not run. It can still be switched on. Creating a rule on such an object, or changing a rule's object, trigger, conditions, actions or match, is 400 with the module message. While Tasks is off, the catalogue's actions leaves out create_task, and a Create a task step the stored rule does not already have is 400 under actions[N].type with the module message. A step the rule already has is kept, and skipped when the rule runs.
  • Tags leave an off module's usage out of the per-module figures, and a tag used only by an off module is never reported as unused.
  • Packs and sample data. Applying a pack reports the parts of an off module as skipped with the module message. Clearing sample data keeps the sample rows of an off module and reports them under switched_off_by_type.

Scheduled jobs

The scheduled jobs in this table skip an org whose module is off, and a module that comes back on starts fresh: they do not act on dates that passed while it was off.

Job While the module is off After it is switched on
Recurring invoices Nothing is generated or sent. Every schedule moves to its next date after today. No backlog is billed.
Overdue invoices and expired estimates Not marked. Invoices past due become Overdue and estimates past expiry become Expired, silently: no webhook, rule or email.
Payment reminders None are sent. A reminder whose day fell while off is not sent later; the next one in the cadence is sent on its day.
Stale-deal alerts (Pipeline) None are sent. A deal already stale at switch-on is never alerted.
Goal milestone emails (Goals) None are sent. Milestones already reached count as announced, so only new ones send.
SLA escalation (Tickets) The scan skips the org. A deadline that passed while off never escalates.
Stale timers (Timesheet) A running timer is not stopped. Timers that went stale while off are left running for their owner to stop.

The clean-up jobs do not check modules. They prune read notifications, automation runs and webhook delivery logs past their retention, and expired sign-in tokens, whatever an org has switched off. Webhook deliveries already queued are still sent.

Customer emails already queued (a survey, a portal notice, an invoice, an estimate, a payment reminder) are dropped if their module is off when they would be sent. Sending a draft invoice or estimate marks it Sent before the email is queued, so one whose email was dropped this way stays marked Sent; send it again once Invoices is back on.

Deleting around an off module

While a module is off its records are out of sight, so a delete that would destroy or be blocked by them is refused instead. Deleting an account, a ticket or a person answers 409:

{"error": true, "errors": "This account has 3 tickets. Tickets is switched off, so switch it on to review them before deleting.", "code": "module_off", "module": "tickets"}

The message is <Subject> has N <things>. <Label> is switched off, so switch it on to review them before deleting. (it ... it for one). The permission check runs first, so a member who may not delete the record still gets 403. What can block a delete:

Deleting Reaches
An account Its tickets, deals, invoices, estimates and recurring invoices
A ticket Its time entries
A person (a user's profile in the org) Their time entries, approvals, watched tickets, saved replies, boards and board memberships

Unlinking is unchanged: removing a contact from a ticket, or a deal from an invoice, behaves as it always did. The Django admin site is for operators and is not guarded by modules; deleting a user there removes the user's profiles and their records.