feat: Implement unsubscribe link for marketing automation [31188f42]

This commit introduces a new unsubscribe feature to allow contacts to opt-out
from marketing automation.

Key changes include:
- Database schema migration: Added  (UUID) to the  model.
- Data population: Implemented a script to assign unique tokens to existing contacts.
- API endpoint: Created a public GET  endpoint to handle opt-out requests.
- Automation: New contacts automatically receive an unsubscribe token upon creation.
- Integration: The full unsubscribe link is now returned via the provisioning API
  for storage in SuperOffice UDFs (ProgID: SuperOffice:9).
- Documentation: Updated  and
  to reflect the new feature and its integration requirements.
- Added  for quick overview and next steps.
This commit is contained in:
2026-02-24 12:18:13 +00:00
parent 0c7088e5fd
commit 7c82e4b5d7
10 changed files with 185 additions and 4 deletions

View File

@@ -0,0 +1,44 @@
import uuid
import os
import sys
# This is the crucial part to fix the import error.
# We add the 'company-explorer' directory to the path, so imports can be absolute
# from the 'backend' module.
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../..')))
from backend.database import Contact, SessionLocal
def migrate_existing_contacts():
"""
Generates and adds an unsubscribe_token for all existing contacts
that do not have one yet.
"""
db = SessionLocal()
try:
contacts_to_update = db.query(Contact).filter(Contact.unsubscribe_token == None).all()
if not contacts_to_update:
print("All contacts already have an unsubscribe token. No migration needed.")
return
print(f"Found {len(contacts_to_update)} contacts without an unsubscribe token. Generating tokens...")
for contact in contacts_to_update:
token = str(uuid.uuid4())
contact.unsubscribe_token = token
print(f" - Generated token for contact ID {contact.id} ({contact.email})")
db.commit()
print("\nSuccessfully updated all contacts with new unsubscribe tokens.")
except Exception as e:
print(f"An error occurred: {e}")
db.rollback()
finally:
db.close()
if __name__ == "__main__":
print("Starting migration: Populating unsubscribe_token for existing contacts.")
migrate_existing_contacts()
print("Migration finished.")

View File

@@ -89,6 +89,17 @@ def migrate_tables():
""")
logger.info("Table 'reported_mistakes' ensured to exist.")
# 4. Update CONTACTS Table (Two-step for SQLite compatibility)
logger.info("Checking 'contacts' table schema for unsubscribe_token...")
contacts_columns = get_table_columns(cursor, "contacts")
if 'unsubscribe_token' not in contacts_columns:
logger.info("Adding column 'unsubscribe_token' to 'contacts' table...")
cursor.execute("ALTER TABLE contacts ADD COLUMN unsubscribe_token TEXT")
logger.info("Creating UNIQUE index on 'unsubscribe_token' column...")
cursor.execute("CREATE UNIQUE INDEX IF NOT EXISTS idx_contacts_unsubscribe_token ON contacts (unsubscribe_token)")
conn.commit()
logger.info("All migrations completed successfully.")