dodil/erp-brick-activity

README.md

brick-activity

Contacts, what was said, and what happens next. People at business partners, and one record for every call, email, meeting, note and task. A brick on erp-platform, rebuilt from a general CRM activity model and held to the CRM spec (modules/crm). Every action is both an API route and an MCP tool.

What it does

StoryHow
CRM-02 log a call or email against a contact in one stepname the contact and the company comes from them. If the rep has exactly one open deal there, the activity goes on that deal too; with several it says so instead of guessing. It shows on the contact, the company (everything logged against its people included) and the deal, newest first
CRM-12 my tasks as overdue, today and this weeka task is an activity with a due date; the board groups the caller's open ones (this week = the next seven days)
CRM-16, for the pipelinea logged call, email or meeting on a deal — and a completed task on one — is reported to brick-pipeline (touch_opportunity), so a deal being worked is not flagged as gone quiet

A task is an activity with a due date — one record, so a scheduled call and the call that happened are the same row, and completing a task keeps what it said. Something that happened cannot be in the future.

One email is one contact. DataK3 enforces nothing unique but a primary key, so the email is the key of activity_contact_emails and a claim is a plain INSERT: a second contact with the same address fails at the database (23505, verified live), even when two requests race. A contact who left the company is kept — history points at them — and unsubscribed.

0.2.0 — the CRM audit's wave 1 (2026-10-17)

FindingNow
H5 a rep read another rep's deal through its timelinea timeline filtered by opportunity_id — and a call logged onto a named deal — asks the pipeline as the caller whether they may see that deal: 404 when not, the pipeline's own answer. A contact or company that does not exist is 404, not an empty list
H12 six adds of one email at once: five 500sthe loser of the email claim (at the INSERT or at COMMIT) answers as if it came second — the same contact (or the "already … at partner …" 409). A NUL, a 300-character subject, due_on 2026-02-30, today=not-a-date, limit=-1 and a 23-digit bp_id are 422
C3 two edits of one contact or task: one lostcontacts and tasks carry a version (GET /contacts/{id} sends it as the ETag); every change takes version or If-Match and answers 409 "changed since you opened it" when it is stale. Without one, a lost race is re-run on the latest
C4bp_id leaves as a string of digits; comes in as digits or a number
H13 a retried "log this call" made twolog_activity takes an Idempotency-Key header (MCP: idempotency_key): a repeat returns the first answer

Who

Every signed-in user reads contacts and the activity record — the point of CRM-02 is that the next person knows what was said. (Whether rows are owned at all is a customer_varies question in the spec; this brick answers "shared".) Writing needs activity:contact:write / activity:activity:write; changing someone else's contact or task needs to be its owner or activity:contact:manage / activity:activity:manage (0.3.0 — until then the manager role name). A DEAL's activities are the exception (0.2.0): they are shown to whoever may see the deal in the pipeline, and nobody else.

A brick decides by permission, never by role name (erp-platform 0.9.2). From 0.3.0 nothing here reads a role name. The roles are the customer's: src/brick_activity/roles.yaml ships the defaults, from the CRM spec's actors; a composition maps them to its pool. The process (application/process.yaml) is served as BPMN 2.0 at GET /process/bpmn.

Neighbours

BrickForIf not installedIf down
business-partnera contact's company must exist; a company's timeline or contact list is of one that existstaken as giventaken as given
pipelineattach to the open deal; report activity; may the caller see a deal they named (asked as the caller)no deal is attached unless named; a named one is taken as giventhe activity is saved; the reply says the deal was not told. A deal's timeline, or logging onto a named deal, is 503 until it answers

The activity brick's service identity needs pipeline:opportunity:touch. Across apps (tier 2) the pipeline answers as that service and sees no rep's deals, so nothing is auto-attached there until the platform can carry the acting person (the on-behalf-of gap in erp_platform.seam).

Tables

activity_contacts, activity_contact_emails (one email → one contact), activity_log (activities and tasks), and (0.2.0) activity_contacts_versions / activity_log_versions, the optimistic-version claims. See src/brick_activity/schema.sql. 0.2.0 migration (additive): a version column at the end of activity_contacts and activity_log (ALTERs, DISRUPTIVE: reads stall for minutes) and the two claim tables — migrate before rolling 0.2.0.

Run

pip install -e '.[test]'                     # needs erp-platform; brick-pipeline + brick-business-partner
pytest -q                                    #   installed too run the tier-1 tests
BUCKET=<scratch> DODIL_ORG=<org> python scripts/live_check.py

Not in 0.2

Importing contacts from a spreadsheet (CRM-13); syncing a mailbox or calendar; @mentions; private notes; merging duplicate contacts.

License: Apache-2.0, see LICENSE