bottleneckcrm/docs/api/modules.md

261 lines
14 KiB
Markdown

# 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](../getting-started/modules.md).
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.
```json
{
"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:
```json
{"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`:
```json
{"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](audit-log.md) 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`):
```json
{"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`:
```json
{"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.