Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
3651475
test: Add @pytest.mark.requires_db to test_mcp_protocol.py
bokelley Oct 30, 2025
134891f
fix: Remove test_main.py which violated PostgreSQL-only architecture
bokelley Oct 30, 2025
5d14fe3
fix: Add sample_tenant and sample_principal fixtures to workflow appr…
bokelley Oct 30, 2025
6005d5d
fix: Add required NOT NULL fields to test fixtures
bokelley Oct 31, 2025
31801e3
fix: Use valid platform_mappings structure in tenant isolation tests
bokelley Oct 31, 2025
d0d0a16
fix: Add session.flush() before creative assignment to satisfy FK con…
bokelley Oct 31, 2025
6451794
fix: Change 'organization_name' to 'name' in Tenant model usage
bokelley Oct 31, 2025
298e195
fix: Add sample_tenant and sample_principal fixtures to test_workflow…
bokelley Oct 31, 2025
aff4d25
fix: Change 'comment' to 'text' key in workflow architecture test
bokelley Oct 31, 2025
bc726b4
fix: Update Principal instantiation in creative assignment tests
bokelley Oct 31, 2025
4b66235
fix: Fix test_media_buy_readiness cleanup and test logic
bokelley Oct 31, 2025
7e838ad
fix: Update GAM lifecycle + tenant setup tests to AdCP 2.4 conventions
bokelley Oct 31, 2025
7637893
fix: Update GAM pricing tests to AdCP 2.4 conventions
bokelley Oct 31, 2025
f6fa35d
fix: Fix generative creatives integration tests
bokelley Oct 31, 2025
b5e5e7c
fix: Resolve test failures from AdCP 2.4 migration and schema changes
bokelley Oct 31, 2025
0a6445d
fix: Resolve remaining test failures - constraint violations, schema …
bokelley Oct 31, 2025
025b986
fix: Correct AdCP 2.2.0 schema compliance - field names and required …
bokelley Oct 31, 2025
eb911a8
fix: Remove deprecated Tenant fields and update test fixtures
bokelley Oct 31, 2025
dd964de
refactor: Convert SQLAlchemy 1.x query patterns to 2.0 in test cleanup
bokelley Oct 31, 2025
e119f46
fix: Critical test failures - authentication, field names, and async …
bokelley Oct 31, 2025
e31a370
fix: Comprehensive test fixes - authentication, model fields, and OAu…
bokelley Oct 31, 2025
74c5198
fix: Test fixes and FormatId string-to-object conversion
bokelley Oct 31, 2025
f19564d
fix: Final test fixes - package validation, imports, and AdCP 2.4 err…
bokelley Oct 31, 2025
5a8d0c8
fix: Final 19 test failures - GAM validation, pricing, creative assig…
bokelley Oct 31, 2025
9d817be
fix: Final 4 test failures - pricing_options, sync_creatives field, p…
bokelley Oct 31, 2025
2b34a83
fix: Final 2 test failures - UpdateMediaBuy status field and multi-pa…
bokelley Oct 31, 2025
0660dc8
fix: Handle both product_id and products fields in Package
bokelley Oct 31, 2025
4a859f6
fix: Correct cleanup order in pricing models integration tests
bokelley Oct 31, 2025
c93eeed
fix: Correct MediaPackage cleanup to use media_buy_id filter
bokelley Oct 31, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions src/adapters/google_ad_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,12 @@ def __init__(

# advertiser_id is only required for order/campaign operations, not inventory sync

if not self.key_file and not self.service_account_json and not self.refresh_token:
raise ValueError(
"GAM config requires either 'service_account_key_file', 'service_account_json', or 'refresh_token'"
)
# Skip auth validation in dry_run mode (for testing)
if not self.dry_run:
if not self.key_file and not self.service_account_json and not self.refresh_token:
raise ValueError(
"GAM config requires either 'service_account_key_file', 'service_account_json', or 'refresh_token'"
)

# Initialize modular components
if not self.dry_run:
Expand Down Expand Up @@ -1046,6 +1048,7 @@ def update_media_buy(
media_buy_id=media_buy_id,
buyer_ref=buyer_ref,
implementation_date=today,
workflow_step_id=step_id,
errors=[],
)
else:
Expand Down
31 changes: 28 additions & 3 deletions src/adapters/mock_ad_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,12 @@ def _send_completion_webhook(self, step_id: str, approved: bool, rejection_reaso
self.log(f"⚠️ Webhook failed for {step_id}: {e}")

def _validate_media_buy_request(
self, request: CreateMediaBuyRequest, packages: list[MediaPackage], start_time: datetime, end_time: datetime
self,
request: CreateMediaBuyRequest,
packages: list[MediaPackage],
start_time: datetime,
end_time: datetime,
package_pricing_info: dict[str, dict] | None = None,
):
"""Validate media buy request with GAM-like validation rules."""
errors = []
Expand All @@ -310,8 +315,18 @@ def _validate_media_buy_request(
# This allows test scenarios to run without configuring ad unit IDs

# Goal validation (like GAM limits)
# Note: For CPCV/CPV pricing, impressions are calculated as if CPM which inflates the number
# Mock adapter allows higher limits for these pricing models
for package in packages:
if package.impressions > 1000000: # Mock limit
# Get pricing model from package_pricing_info if available
pricing_model = None
if package_pricing_info and package.package_id in package_pricing_info:
pricing_model = package_pricing_info[package.package_id].get("pricing_model")

# Apply higher limit for video-based pricing models (CPCV, CPV)
limit = 100000000 if pricing_model in ["cpcv", "cpv"] else 1000000

if package.impressions > limit: # Mock limit
errors.append(
f"ReservationDetailsError.PERCENTAGE_UNITS_BOUGHT_TOO_HIGH @ lineItem[0].primaryGoal.units; trigger:'{package.impressions}'"
)
Expand Down Expand Up @@ -471,7 +486,7 @@ def create_media_buy(
)

# GAM-like validation (based on real GAM behavior)
self._validate_media_buy_request(request, packages, start_time, end_time)
self._validate_media_buy_request(request, packages, start_time, end_time, package_pricing_info)

# If no AI scenario or scenario accepts, proceed with normal flow
# HITL Mode Processing
Expand Down Expand Up @@ -754,6 +769,16 @@ def _create_media_buy_immediate(
pkg_dict = pkg.model_dump(mode="python", exclude_none=False)
self.log(f"[DEBUG] MockAdapter: Package {idx} model_dump() = {pkg_dict}")
self.log(f"[DEBUG] MockAdapter: Package {idx} has package_id = {pkg_dict.get('package_id')}")

# CRITICAL: Ensure package_id is set (required for AdCP response)
# If package doesn't have package_id yet, generate one
if not pkg_dict.get("package_id"):
import uuid

generated_package_id = f"pkg_{idx}_{uuid.uuid4().hex[:8]}"
pkg_dict["package_id"] = generated_package_id
self.log(f"[DEBUG] MockAdapter: Generated package_id for package {idx}: {generated_package_id}")

# Add buyer_ref from original request package if available
if request.packages and idx < len(request.packages):
pkg_dict["buyer_ref"] = request.packages[idx].buyer_ref
Expand Down
6 changes: 6 additions & 0 deletions src/admin/blueprints/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,12 @@ def google_callback():
session["user_name"] = user.get("name", email)
session["user_picture"] = user.get("picture", "")

# Check if this is a signup flow
if session.get("signup_flow"):
# Redirect to onboarding wizard for new tenant signup
flash(f"Welcome {user.get('name', email)}!", "success")
return redirect(url_for("public.signup_onboarding"))

# Unified flow: Always show tenant selector (with option to create new tenant)
# No distinction between signup and login - keeps UX simple and consistent
from src.admin.domain_access import get_user_tenant_access
Expand Down
7 changes: 3 additions & 4 deletions src/admin/tenant_management_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ def create_tenant():
is_active=data.get("is_active", True),
billing_plan=data.get("billing_plan", "standard"),
billing_contact=data.get("billing_contact"),
max_daily_budget=data.get("max_daily_budget", 10000),
# Note: max_daily_budget moved to currency_limits table (per models.py line 55)
enable_axe_signals=data.get("enable_axe_signals", True),
authorized_emails=json.dumps(email_list),
authorized_domains=json.dumps(domain_list),
Expand Down Expand Up @@ -321,7 +321,7 @@ def get_tenant(tenant_id):
"created_at": tenant.created_at.isoformat() if tenant.created_at else None,
"updated_at": tenant.updated_at.isoformat() if tenant.updated_at else None,
"settings": {
"max_daily_budget": tenant.max_daily_budget,
# Note: max_daily_budget moved to currency_limits table (per models.py line 55)
"enable_axe_signals": bool(tenant.enable_axe_signals),
"authorized_emails": tenant.authorized_emails if tenant.authorized_emails else [],
"authorized_domains": tenant.authorized_domains if tenant.authorized_domains else [],
Expand Down Expand Up @@ -419,8 +419,7 @@ def update_tenant(tenant_id):
tenant.billing_plan = data["billing_plan"]
if "billing_contact" in data:
tenant.billing_contact = data["billing_contact"]
if "max_daily_budget" in data:
tenant.max_daily_budget = data["max_daily_budget"]
# Note: max_daily_budget moved to currency_limits table (per models.py line 55)
if "enable_axe_signals" in data:
tenant.enable_axe_signals = data["enable_axe_signals"]
if "authorized_emails" in data:
Expand Down
6 changes: 6 additions & 0 deletions src/core/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ def get_principal_from_token(token: str, tenant_id: str | None = None) -> str |
# Also check if it's the admin token for this specific tenant
tenant_stmt = select(Tenant).filter_by(tenant_id=tenant_id, is_active=True)
tenant = session.scalars(tenant_stmt).first()

if tenant and tenant.admin_token == token:
console.print(f"[green]Token matches admin token for tenant '{tenant_id}'[/green]")
# Return a special admin principal ID
return f"{tenant_id}_admin"

console.print(f"[red]Token not found in tenant '{tenant_id}'[/red]")
return None
else:
Expand Down
4 changes: 3 additions & 1 deletion src/core/helpers/context_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ def get_principal_id_from_context(context: Context | ToolContext | None) -> str
Returns:
Principal ID string, or None if not authenticated
"""
# Handle ToolContext (from A2A server) - it already has principal_id
# Handle ToolContext (from A2A server) - it already has principal_id and tenant_id
if isinstance(context, ToolContext):
# Set tenant context from ToolContext
set_current_tenant({"tenant_id": context.tenant_id})
return context.principal_id

# Handle FastMCP Context (from MCP protocol)
Expand Down
Loading