Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
44 changes: 43 additions & 1 deletion schemas/v1/_schemas_v1_core_format_json.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"preview_image": {
"type": "string",
"format": "uri",
"description": "Optional preview image URL for format browsing/discovery UI. Should be 400x300px (4:3 aspect ratio) PNG or JPG. Used as thumbnail/card image in format browsers."
"description": "DEPRECATED: Use format_card instead. Optional preview image URL for format browsing/discovery UI. Should be 400x300px (4:3 aspect ratio) PNG or JPG. Used as thumbnail/card image in format browsers. This field is maintained for backward compatibility but format_card provides a more flexible, structured approach."
},
"example_url": {
"type": "string",
Expand Down Expand Up @@ -152,6 +152,7 @@
"vast",
"daast",
"text",
"markdown",
"html",
"css",
"javascript",
Expand Down Expand Up @@ -224,6 +225,7 @@
"vast",
"daast",
"text",
"markdown",
"html",
"css",
"javascript",
Expand Down Expand Up @@ -282,6 +284,46 @@
"items": {
"$ref": "/schemas/v1/core/format-id.json"
}
},
"format_card": {
"type": "object",
"description": "Optional standard visual card (300x400px) for displaying this format in user interfaces. Can be rendered via preview_creative or pre-generated.",
"properties": {
"format_id": {
"$ref": "/schemas/v1/core/format-id.json",
"description": "Creative format defining the card layout (typically format_card_standard)"
},
"manifest": {
"type": "object",
"description": "Asset manifest for rendering the card, structure defined by the format",
"additionalProperties": true
}
},
"required": [
"format_id",
"manifest"
],
"additionalProperties": false
},
"format_card_detailed": {
"type": "object",
"description": "Optional detailed card with carousel and full specifications. Provides rich format documentation similar to ad spec pages.",
"properties": {
"format_id": {
"$ref": "/schemas/v1/core/format-id.json",
"description": "Creative format defining the detailed card layout (typically format_card_detailed)"
},
"manifest": {
"type": "object",
"description": "Asset manifest for rendering the detailed card, structure defined by the format",
"additionalProperties": true
}
},
"required": [
"format_id",
"manifest"
],
"additionalProperties": false
}
},
"required": [
Expand Down
40 changes: 40 additions & 0 deletions schemas/v1/_schemas_v1_core_product_json.json
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,46 @@
"type": "string",
"format": "date-time",
"description": "Expiration timestamp for custom products"
},
"product_card": {
"type": "object",
"description": "Optional standard visual card (300x400px) for displaying this product in user interfaces. Can be rendered via preview_creative or pre-generated.",
"properties": {
"format_id": {
"$ref": "/schemas/v1/core/format-id.json",
"description": "Creative format defining the card layout (typically product_card_standard)"
},
"manifest": {
"type": "object",
"description": "Asset manifest for rendering the card, structure defined by the format",
"additionalProperties": true
}
},
"required": [
"format_id",
"manifest"
],
"additionalProperties": false
},
"product_card_detailed": {
"type": "object",
"description": "Optional detailed card with carousel and full specifications. Provides rich product presentation similar to media kit pages.",
"properties": {
"format_id": {
"$ref": "/schemas/v1/core/format-id.json",
"description": "Creative format defining the detailed card layout (typically product_card_detailed)"
},
"manifest": {
"type": "object",
"description": "Asset manifest for rendering the detailed card, structure defined by the format",
"additionalProperties": true
}
},
"required": [
"format_id",
"manifest"
],
"additionalProperties": false
}
},
"required": [
Expand Down
54 changes: 54 additions & 0 deletions scripts/setup/init_database_ci.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ def init_db_ci():
from src.core.database.models import (
AuthorizedProperty,
CurrencyLimit,
GAMInventory,
PricingOption,
Principal,
Product,
Expand Down Expand Up @@ -367,6 +368,59 @@ def init_db_ci():
else:
print(" ℹ️ Authorized property already exists: example.com")

# Create GAM inventory for setup checklist completion (inventory sync)
print("\nCreating GAM inventory for setup checklist...")
stmt_check_inventory = select(GAMInventory).filter_by(tenant_id=tenant_id)
existing_inventory_count = len(session.scalars(stmt_check_inventory).all())

if existing_inventory_count == 0:
# Create sample inventory items to satisfy setup checklist
inventory_items = [
GAMInventory(
tenant_id=tenant_id,
inventory_type="ad_unit",
inventory_id="ci_test_ad_unit_1",
name="CI Test Ad Unit - Homepage",
path=["root", "website", "homepage"],
status="active",
inventory_metadata={"sizes": ["300x250", "728x90"]},
),
GAMInventory(
tenant_id=tenant_id,
inventory_type="ad_unit",
inventory_id="ci_test_ad_unit_2",
name="CI Test Ad Unit - Article",
path=["root", "website", "article"],
status="active",
inventory_metadata={"sizes": ["300x600", "970x250"]},
),
GAMInventory(
tenant_id=tenant_id,
inventory_type="placement",
inventory_id="ci_test_placement_1",
name="CI Test Placement - Premium",
path=["root"],
status="active",
inventory_metadata={"description": "Premium placement for CI tests"},
),
GAMInventory(
tenant_id=tenant_id,
inventory_type="targeting_key",
inventory_id="ci_test_targeting_key_1",
name="CI Test Key - Category",
path=[],
status="active",
inventory_metadata={"type": "predefined", "values": ["news", "sports", "entertainment"]},
),
]
for item in inventory_items:
session.add(item)

session.commit()
print(f" ✓ Created {len(inventory_items)} inventory items (ad units, placements, targeting)")
else:
print(f" ℹ️ Inventory already exists: {existing_inventory_count} items")

# Verify products were actually saved
stmt_verify = select(Product).filter_by(tenant_id=tenant_id)
saved_products = session.scalars(stmt_verify).all()
Expand Down
8 changes: 4 additions & 4 deletions src/admin/blueprints/inventory.py
Original file line number Diff line number Diff line change
Expand Up @@ -552,10 +552,10 @@ def analyze_ad_server_inventory(tenant_id):
platform_mappings=mappings,
)

# Get adapter instance
from src.adapters import get_adapter

adapter = get_adapter(adapter_type, principal, config=config, dry_run=False)
# TODO: Get adapter instance and call actual discovery methods
# For now, return mock analysis data
# from src.adapters import get_adapter
# adapter = get_adapter(adapter_type, config, principal)

# Mock analysis (real adapters would implement actual discovery)
analysis = {
Expand Down
21 changes: 13 additions & 8 deletions src/services/setup_checklist_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from src.core.database.models import (
AuthorizedProperty,
CurrencyLimit,
GAMInventory,
Principal,
Product,
Tenant,
Expand Down Expand Up @@ -184,25 +185,29 @@ def _check_critical_tasks(self, session, tenant: Tenant) -> list[SetupTask]:
)

# 5. Inventory Synced
# Check if tenant has any inventory data (products with inventory mappings)
stmt = select(func.count()).select_from(Product).where(Product.tenant_id == self.tenant_id)
product_count = session.scalar(stmt) or 0

# For now, we consider inventory synced if products exist
# In future, could check for specific inventory sync timestamp
inventory_synced = product_count > 0
# Check if tenant has synced inventory from ad server
# This checks GAMInventory table which stores actual synced ad units, placements, and targeting options
stmt = select(func.count()).select_from(GAMInventory).where(GAMInventory.tenant_id == self.tenant_id)
inventory_count = session.scalar(stmt) or 0

inventory_synced = inventory_count > 0
inventory_details = (
f"{inventory_count:,} inventory items synced" if inventory_synced else "No inventory synced from ad server"
)
tasks.append(
SetupTask(
key="inventory_synced",
name="Inventory Sync",
description="Sync ad units and placements from ad server",
is_complete=inventory_synced,
action_url=f"/tenant/{self.tenant_id}/settings#inventory",
details="Inventory synced" if inventory_synced else "Inventory not synced",
details=inventory_details,
)
)

# 6. Products Created
stmt = select(func.count()).select_from(Product).where(Product.tenant_id == self.tenant_id)
product_count = session.scalar(stmt) or 0
tasks.append(
SetupTask(
key="products_created",
Expand Down
106 changes: 104 additions & 2 deletions tests/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,15 @@ def authenticated_admin_session(admin_client, integration_db):

@pytest.fixture
def test_tenant_with_data(integration_db):
"""Create a test tenant in the database with proper configuration."""
"""Create a test tenant in the database with proper configuration and all required setup data."""
from src.core.database.models import (
AuthorizedProperty,
CurrencyLimit,
GAMInventory,
Principal,
PropertyTag,
)

tenant_data = TenantFactory.create()
now = datetime.now(UTC)

Expand All @@ -284,10 +292,80 @@ def test_tenant_with_data(integration_db):
auto_approve_formats=[], # JSONType expects list, not json.dumps()
human_review_required=False,
policy_settings={}, # JSONType expects dict, not json.dumps()
authorized_emails=["test@example.com"], # Required for access control
created_at=now,
updated_at=now,
)
db_session.add(tenant)
db_session.flush()

# Add all required setup data for tests to pass setup checklist validation
tenant_id = tenant_data["tenant_id"]

# CurrencyLimit (required for budget validation)
currency_limit = CurrencyLimit(
tenant_id=tenant_id,
currency_code="USD",
min_package_budget=1.00,
max_daily_package_spend=100000.00,
)
db_session.add(currency_limit)

# PropertyTag (required for product property_tags)
property_tag = PropertyTag(
tenant_id=tenant_id,
tag_id="all_inventory",
name="All Inventory",
description="All available inventory",
)
db_session.add(property_tag)

# AuthorizedProperty (required for setup validation)
auth_property = AuthorizedProperty(
tenant_id=tenant_id,
property_id=f"{tenant_id}_property_1",
property_type="website",
name="Fixture Default Property", # Unique name to avoid conflicts with test assertions
identifiers=[{"type": "domain", "value": "fixture-default.example.com"}],
publisher_domain="fixture-default.example.com",
verification_status="verified",
)
db_session.add(auth_property)

# Principal (required for setup completion)
principal = Principal(
tenant_id=tenant_id,
principal_id=f"{tenant_id}_principal",
name="Test Principal",
access_token=f"{tenant_id}_token",
platform_mappings={"mock": {"advertiser_id": f"mock_adv_{tenant_id}"}},
)
db_session.add(principal)

# GAMInventory (required for inventory sync status)
inventory_items = [
GAMInventory(
tenant_id=tenant_id,
inventory_type="ad_unit",
inventory_id=f"{tenant_id}_ad_unit_1",
name="Test Ad Unit",
path=["root", "test"],
status="active",
inventory_metadata={"sizes": ["300x250"]},
),
GAMInventory(
tenant_id=tenant_id,
inventory_type="placement",
inventory_id=f"{tenant_id}_placement_1",
name="Test Placement",
path=["root"],
status="active",
inventory_metadata={},
),
]
for item in inventory_items:
db_session.add(item)

db_session.commit()

return tenant_data
Expand All @@ -310,7 +388,7 @@ def sample_tenant(integration_db):
from datetime import UTC, datetime

from src.core.database.database_session import get_db_session
from src.core.database.models import AuthorizedProperty, CurrencyLimit, PropertyTag, Tenant
from src.core.database.models import AuthorizedProperty, CurrencyLimit, GAMInventory, PropertyTag, Tenant

now = datetime.now(UTC)
with get_db_session() as session:
Expand Down Expand Up @@ -362,6 +440,30 @@ def sample_tenant(integration_db):
)
session.add(auth_property)

# Add GAMInventory records (required for inventory sync status in setup checklist)
inventory_items = [
GAMInventory(
tenant_id=tenant.tenant_id,
inventory_type="ad_unit",
inventory_id="test_ad_unit_1",
name="Test Ad Unit - Homepage",
path=["root", "website", "homepage"],
status="active",
inventory_metadata={"sizes": ["300x250", "728x90"]},
),
GAMInventory(
tenant_id=tenant.tenant_id,
inventory_type="placement",
inventory_id="test_placement_1",
name="Test Placement - Premium",
path=["root"],
status="active",
inventory_metadata={"description": "Premium placement"},
),
]
for item in inventory_items:
session.add(item)

session.commit()

return {
Expand Down
Loading