Metadata-Version: 2.4
Name: vanty-crm
Version: 0.3.1
Summary: Vanty App: multi-tenant CRM (people, companies, activities, feedback).
Project-URL: Homepage, https://github.com/advantch/vanty-crm
Project-URL: Repository, https://github.com/advantch/vanty-crm
Project-URL: Issues, https://github.com/advantch/vanty-crm/issues
License-Expression: MIT
License-File: LICENSE
Keywords: contacts,crm,fastapi,feedback,leads
Classifier: Development Status :: 4 - Beta
Classifier: Framework :: FastAPI
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Office/Business
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.12
Requires-Dist: fastapi>=0.135.1
Requires-Dist: pydantic-settings>=2.13.1
Requires-Dist: python-multipart>=0.0.20
Requires-Dist: taskiq>=0.11
Requires-Dist: tortoise-orm>=1.1.6
Requires-Dist: vanty-core>=0.3.0
Description-Content-Type: text/markdown

# vanty-crm

Lean, multi-tenant CRM toolkit for FastAPI + Tortoise ORM. Part of the
[Vanty](https://github.com/advantch) ecosystem.

- People (leads/contacts/customers), companies, activities, feedback
- Tortoise models on top of `OrganizationScopedModel` for tenant isolation
- FastAPI routers (public + admin) with cursor pagination
- Domain events on the shared `vanty-core` event bus — subscribe from any
  package to react to CRM lifecycle moments
- CSV import (preview + apply)

CRM `Person` is an **external contact**, never a platform user. An optional
`linked_user_id` records the user account a Person was converted into.

## Install

```bash
uv add vanty-crm
```

## Quickstart

```python
from fastapi import FastAPI

from vanty_crm import CrmApp, CrmSettings, mount_crm_router

app = FastAPI()
kit = mount_crm_router(app, CrmSettings(database_url="sqlite://./crm.db"))


@app.on_event("startup")
async def _startup() -> None:
    await kit.init_orm(generate_schemas=True)


@app.on_event("shutdown")
async def _shutdown() -> None:
    await kit.close_orm()


# Optional: admin router (mount behind your own RBAC)
from vanty_crm.fastapi.dependencies import require_superuser

def my_superuser_check() -> None:
    ...  # raise HTTPException if caller is not a superuser

app.dependency_overrides[require_superuser] = my_superuser_check
app.include_router(kit.admin_router)
```

## Subscribe to `vanty_auth.user.signed_up`

The headline cross-package recipe — when a new user signs up via `vanty-auth`,
upsert a CRM `Person` for them so marketing/CS workflows can pick them up:

```python
from vanty_auth.events import UserSignedUp
from vanty_core.events import on

from vanty_crm.services.person_service import PersonService

person_service = PersonService()


@on(UserSignedUp)
async def upsert_person_on_signup(evt: UserSignedUp) -> None:
    if evt.organization_id is None:
        return
    await person_service.upsert_by_email(
        organization_id=evt.organization_id,
        email=evt.email,
        defaults={"linked_user_id": evt.user_id, "source": evt.source or "signup"},
    )
```

That handler is the contract proven by `tests/test_signup_subscriber.py`.

## Public API

Mounted at `/crm` by default. All routes (except `POST /crm/feedback`) require
the `X-Organization-ID` header (or an active `vanty_auth` AuthContext).

| Method | Path                                          | Notes                              |
| -----: | --------------------------------------------- | ---------------------------------- |
|    GET | `/crm/people`                                 | Filter by `search`, `owner_id`, `stage`, `company_id`. Cursor pagination via `cursor`/`limit`. |
|   POST | `/crm/people`                                 | Create person                      |
|    GET | `/crm/people/{id}`                            | Get person                         |
|  PATCH | `/crm/people/{id}`                            | Partial update                     |
| DELETE | `/crm/people/{id}`                            | Hard delete                        |
|   POST | `/crm/people/merge`                           | Merge two people                   |
|    GET | `/crm/people/{id}/activities`                 | Activity timeline                  |
|   POST | `/crm/people/{id}/activities`                 | Log a note/email/call/event        |
|    GET | `/crm/companies`                              | Filter by `search`, `owner_id`     |
|   POST | `/crm/companies`                              | Create company                     |
|    GET | `/crm/companies/{id}`                         |                                    |
|  PATCH | `/crm/companies/{id}`                         |                                    |
| DELETE | `/crm/companies/{id}`                         |                                    |
|   POST | `/crm/companies/{id}/people`                  | Attach person                      |
| DELETE | `/crm/companies/{id}/people/{person_id}`      | Detach person                      |
|   POST | `/crm/feedback`                               | Public — accepts unauthenticated submissions |
|    GET | `/crm/feedback`                               | List (auth required)               |
|  PATCH | `/crm/feedback/{id}`                          | Update / mark resolved             |

Pagination uses an opaque `cursor` (the last id returned). Responses include
`next_cursor` while more rows are available.

## Admin API

Mounted by host at `/admin/crm` behind `require_superuser`:

- `GET /admin/crm/people` — cross-organization list (filter by `organization_id`)
- `GET /admin/crm/companies`
- `GET /admin/crm/feedback`
- `GET /admin/crm/activities`
- `POST /admin/crm/imports/people` — upload a CSV file, returns preview
- `POST /admin/crm/imports/people/{import_id}/apply` — apply a previewed import

CSV imports are held in process memory between preview and apply (v1).

## Events

Every event is a frozen dataclass registered with `@event(...)`:

| Event name                       | Class             |
| -------------------------------- | ----------------- |
| `vanty_crm.person.created`       | `PersonCreated`   |
| `vanty_crm.person.updated`       | `PersonUpdated`   |
| `vanty_crm.person.merged`        | `PersonMerged`    |
| `vanty_crm.person.deleted`       | `PersonDeleted`   |
| `vanty_crm.company.created`      | `CompanyCreated`  |
| `vanty_crm.company.updated`      | `CompanyUpdated`  |
| `vanty_crm.feedback.received`    | `FeedbackReceived`|
| `vanty_crm.feedback.resolved`    | `FeedbackResolved`|
| `vanty_crm.activity.logged`      | `ActivityLogged`  |

## Develop

```bash
uv sync
uv run pytest --no-cov -p no:cacheprovider
```

## Publish to its own GitHub repo

This package lives inside the Vanty monorepo but ships as a standalone
PyPI package + GitHub repo. The `publish-github` Make target uses
[`git-filter-repo`](https://github.com/newren/git-filter-repo) to extract
the `vanty-crm/` subdirectory with full history and push it to a fresh repo:

```bash
brew install git-filter-repo  # one-time
make publish-github REPO=git@github.com:advantch/vanty-crm.git
```

## License

MIT — © 2026 Themba <themba@advantch.com>
