Skip to content

Commit 12d7ae6

Browse files
authored
Create crm_utils.py
1 parent 9ff8eeb commit 12d7ae6

1 file changed

Lines changed: 257 additions & 0 deletions

File tree

crm/crm_utils.py

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
"""CRM Utility Functions for Trinity Strategy & Marketing
2+
3+
This module provides in-memory CRUD utilities for contacts, leads, and business
4+
records. It is designed to be easily swappable with a persistent backend later
5+
(e.g., SQLite, Supabase, MongoDB) while offering a clean API now.
6+
"""
7+
from __future__ import annotations
8+
9+
from typing import Dict, List, Optional, Any, Callable
10+
from dataclasses import asdict
11+
from datetime import datetime
12+
import uuid
13+
14+
from .crm_models import (
15+
Contact,
16+
Lead,
17+
Business,
18+
ContactStatus,
19+
LeadStatus,
20+
PipelineStage,
21+
BusinessType,
22+
)
23+
24+
25+
class InMemoryStore:
26+
"""Simple in-memory store to scaffold data operations.
27+
28+
Replace with a real repository later. Methods mirror common CRUD patterns.
29+
"""
30+
31+
def __init__(self):
32+
self.contacts: Dict[str, Contact] = {}
33+
self.leads: Dict[str, Lead] = {}
34+
self.businesses: Dict[str, Business] = {}
35+
36+
# ---------------------- CONTACTS ----------------------
37+
def add_contact(self, contact: Contact) -> Contact:
38+
contact_id = contact.contact_id or str(uuid.uuid4())
39+
contact.contact_id = contact_id
40+
contact.created_at = contact.created_at or datetime.now()
41+
contact.updated_at = datetime.now()
42+
self.contacts[contact_id] = contact
43+
return contact
44+
45+
def update_contact(self, contact_id: str, **updates) -> Optional[Contact]:
46+
c = self.contacts.get(contact_id)
47+
if not c:
48+
return None
49+
for k, v in updates.items():
50+
if hasattr(c, k):
51+
setattr(c, k, v)
52+
c.updated_at = datetime.now()
53+
return c
54+
55+
def get_contact(self, contact_id: str) -> Optional[Contact]:
56+
return self.contacts.get(contact_id)
57+
58+
def search_contacts(self, query: str = "", filters: Dict[str, Any] | None = None) -> List[Contact]:
59+
filters = filters or {}
60+
q = query.lower().strip()
61+
results: List[Contact] = []
62+
for c in self.contacts.values():
63+
blob = " ".join(
64+
[
65+
c.email or "",
66+
c.phone or "",
67+
c.first_name or "",
68+
c.last_name or "",
69+
c.company or "",
70+
c.source or "",
71+
" ".join(c.tags or []),
72+
c.notes or "",
73+
]
74+
).lower()
75+
if q and q not in blob:
76+
continue
77+
# filter matching
78+
ok = True
79+
for fk, fv in filters.items():
80+
val = getattr(c, fk, None)
81+
if isinstance(val, list):
82+
ok = fv in val
83+
else:
84+
ok = val == fv
85+
if not ok:
86+
break
87+
if ok:
88+
results.append(c)
89+
return results
90+
91+
# ---------------------- LEADS ----------------------
92+
def add_lead(self, lead: Lead) -> Lead:
93+
lead_id = lead.lead_id or str(uuid.uuid4())
94+
lead.lead_id = lead_id
95+
lead.created_at = lead.created_at or datetime.now()
96+
lead.updated_at = datetime.now()
97+
self.leads[lead_id] = lead
98+
return lead
99+
100+
def update_lead(self, lead_id: str, **updates) -> Optional[Lead]:
101+
l = self.leads.get(lead_id)
102+
if not l:
103+
return None
104+
for k, v in updates.items():
105+
if hasattr(l, k):
106+
setattr(l, k, v)
107+
l.updated_at = datetime.now()
108+
return l
109+
110+
def get_lead(self, lead_id: str) -> Optional[Lead]:
111+
return self.leads.get(lead_id)
112+
113+
def search_leads(self, query: str = "", filters: Dict[str, Any] | None = None) -> List[Lead]:
114+
filters = filters or {}
115+
q = query.lower().strip()
116+
results: List[Lead] = []
117+
for l in self.leads.values():
118+
blob = " ".join(
119+
[
120+
l.title or "",
121+
l.description or "",
122+
l.source or "",
123+
l.campaign or "",
124+
l.status.value if isinstance(l.status, LeadStatus) else str(l.status),
125+
l.pipeline_stage.value if isinstance(l.pipeline_stage, PipelineStage) else str(l.pipeline_stage),
126+
]
127+
).lower()
128+
if q and q not in blob:
129+
continue
130+
ok = True
131+
for fk, fv in filters.items():
132+
val = getattr(l, fk, None)
133+
if isinstance(val, Enum):
134+
val = val.value
135+
ok = val == fv
136+
if not ok:
137+
break
138+
if ok:
139+
results.append(l)
140+
return results
141+
142+
# ---------------------- BUSINESSES ----------------------
143+
def add_business(self, business: Business) -> Business:
144+
business_id = business.business_id or str(uuid.uuid4())
145+
business.business_id = business_id
146+
business.created_at = business.created_at or datetime.now()
147+
business.updated_at = datetime.now()
148+
self.businesses[business_id] = business
149+
return business
150+
151+
def update_business(self, business_id: str, **updates) -> Optional[Business]:
152+
b = self.businesses.get(business_id)
153+
if not b:
154+
return None
155+
for k, v in updates.items():
156+
if hasattr(b, k):
157+
setattr(b, k, v)
158+
b.updated_at = datetime.now()
159+
return b
160+
161+
def get_business(self, business_id: str) -> Optional[Business]:
162+
return self.businesses.get(business_id)
163+
164+
def search_businesses(self, query: str = "", filters: Dict[str, Any] | None = None) -> List[Business]:
165+
filters = filters or {}
166+
q = query.lower().strip()
167+
results: List[Business] = []
168+
for b in self.businesses.values():
169+
blob = " ".join(
170+
[
171+
b.name or "",
172+
b.website or "",
173+
b.industry or "",
174+
" ".join(b.marketing_segments or []),
175+
]
176+
).lower()
177+
if q and q not in blob:
178+
continue
179+
ok = True
180+
for fk, fv in filters.items():
181+
val = getattr(b, fk, None)
182+
if isinstance(val, list):
183+
ok = fv in val
184+
else:
185+
ok = val == fv
186+
if not ok:
187+
break
188+
if ok:
189+
results.append(b)
190+
return results
191+
192+
193+
# Convenience factory
194+
_store: Optional[InMemoryStore] = None
195+
196+
def get_store() -> InMemoryStore:
197+
global _store
198+
if _store is None:
199+
_store = InMemoryStore()
200+
return _store
201+
202+
203+
# High-level helper functions (thin wrappers)
204+
205+
def add_contact(**kwargs) -> Contact:
206+
c = Contact(**kwargs)
207+
return get_store().add_contact(c)
208+
209+
def update_contact(contact_id: str, **updates) -> Optional[Contact]:
210+
return get_store().update_contact(contact_id, **updates)
211+
212+
def find_contacts(query: str = "", **filters) -> List[Contact]:
213+
return get_store().search_contacts(query=query, filters=filters or None)
214+
215+
216+
def add_lead(**kwargs) -> Lead:
217+
l = Lead(**kwargs)
218+
return get_store().add_lead(l)
219+
220+
def update_lead(lead_id: str, **updates) -> Optional[Lead]:
221+
return get_store().update_lead(lead_id, **updates)
222+
223+
def find_leads(query: str = "", **filters) -> List[Lead]:
224+
return get_store().search_leads(query=query, filters=filters or None)
225+
226+
227+
def add_business(**kwargs) -> Business:
228+
b = Business(**kwargs)
229+
return get_store().add_business(b)
230+
231+
def update_business(business_id: str, **updates) -> Optional[Business]:
232+
return get_store().update_business(business_id, **updates)
233+
234+
def find_businesses(query: str = "", **filters) -> List[Business]:
235+
return get_store().search_businesses(query=query, filters=filters or None)
236+
237+
238+
# Seed helpers for plug-and-play demos
239+
240+
def seed_demo_data() -> Dict[str, Any]:
241+
"""Seed a minimal dataset suitable for marketing automation demos."""
242+
store = get_store()
243+
# Business
244+
acme = store.add_business(
245+
Business(name="Acme Co.", website="https://acme.example", industry="SaaS", business_type=BusinessType.SAAS)
246+
)
247+
# Contacts
248+
alice = store.add_contact(Contact(email="alice@acme.example", first_name="Alice", last_name="Ng", company="Acme", source="website", tags=["newsletter"]))
249+
bob = store.add_contact(Contact(email="bob@acme.example", first_name="Bob", last_name="Lee", company="Acme", source="referral", tags=["paid_ads"]))
250+
# Leads
251+
l1 = store.add_lead(Lead(title="Website Revamp", contact_id=alice.contact_id, value=15000, source="inbound", pipeline_stage=PipelineStage.INTEREST))
252+
l2 = store.add_lead(Lead(title="SEO Retainer", contact_id=bob.contact_id, value=3000, source="outbound", pipeline_stage=PipelineStage.CONSIDERATION))
253+
return {
254+
"business": acme,
255+
"contacts": [alice, bob],
256+
"leads": [l1, l2],
257+
}

0 commit comments

Comments
 (0)