3 Commits
Author SHA1 Message Date
AFFAANhandClaude Opus 5 9d6aa64ad6 feat(properties): add custom fields without a migration
Modelled on HubSpot's custom properties: the definition lives in a table,
the values live in each record's existing JSON attributes. Adding a field
is an INSERT, so it can happen mid-conversation through MCP and the next
record can use it immediately.

The cost of that is that the database enforces nothing about the values,
so data_type is enforced in application code and every write path has to
come through it — otherwise the type on a definition is decoration.
Values are coerced rather than merely checked, because callers are agents
and HTTP clients and "42" is a number expressed loosely; what is rejected
is genuinely ambiguous, like "quite large" for a number.

A value whose property has no definition is rejected rather than stored.
A typo sitting in the database looks exactly like data, and a registry
whose set of fields is not actually the set of fields is worse than none.
Workspaces with nothing defined keep the old free-form behaviour, so this
does not break attributes already in use.

name and data_type are not updatable: renaming orphans every value stored
under the old key, and retyping leaves values that no longer satisfy the
type. Deleting a definition leaves existing values alone rather than
rewriting every record, so undoing a mistaken delete is just recreating
the property.

Two ways in, sharing one validator: a logged-in person, and an integration
key for MaskanX. An agent gets no shortcut around the rules a person is
held to.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 11:07:50 +05:30
AFFAANhandClaude Opus 5 55d5af849b feat(campaigns): report advertising spend and cost per lead
MaskanX runs the campaigns; this stores what they cost and what they
produced so the question "what did this campaign cost us per lead" is
answerable next to the leads themselves.

POST /integrations/campaigns is an upsert keyed on (provider,
external_id), not an idempotent create like /leads. A lead is an event
that happened once; a campaign's figures change every time they are read,
and MaskanX re-pushes the same campaign as its spend grows. An
Idempotency-Key here would pin the CRM to the first numbers it ever saw.

Money is stored as integers in minor currency units, matching what
MaskanX sends and what Meta uses. A Numeric would add a second convention
and a rounding step between systems that currently agree exactly.

Ad attribution is promoted out of crm_leads.attributes into indexed
columns, so counting leads per campaign is a join rather than a JSON scan
— which also keeps it working on both SQLite and PostgreSQL.

Meta's lead count and the CRM's own are both kept. They routinely differ,
since Meta attributes late and leads can be entered by hand, and the gap
is worth seeing rather than hiding behind one number.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 10:55:46 +05:30
AFFAANhandClaude Opus 5 f6a54d6f93 Initial commit: Maskan CRM backend
Independent FastAPI backend for Maskan CRM.

Owns contacts, organizations, leads, pipelines, activities, products,
quotes, users, permissions, audit records and first-party integration
credentials, with Alembic migrations against PostgreSQL.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 10:28:41 +05:30