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
33 changes: 33 additions & 0 deletions alembic/versions/149ad85edb6f_add_variant_name_and_description_.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""add variant name and description template fields

Revision ID: 149ad85edb6f
Revises: c608bc822a13
Create Date: 2025-11-08 05:29:06.834972

"""

from collections.abc import Sequence

import sqlalchemy as sa

from alembic import op

# revision identifiers, used by Alembic.
revision: str = "149ad85edb6f"
down_revision: str | Sequence[str] | None = "c608bc822a13"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None


def upgrade() -> None:
"""Upgrade schema."""
# Add variant name and description template fields to products table
op.add_column("products", sa.Column("variant_name_template", sa.String(length=500), nullable=True))
op.add_column("products", sa.Column("variant_description_template", sa.Text(), nullable=True))


def downgrade() -> None:
"""Downgrade schema."""
# Remove variant name and description template fields from products table
op.drop_column("products", "variant_description_template")
op.drop_column("products", "variant_name_template")
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""add_measurement_providers_to_tenants

Revision ID: a79158b4a1ae
Revises: e40d739b89c6
Create Date: 2025-11-07 12:44:40.932528

"""

from collections.abc import Sequence

import sqlalchemy as sa

from alembic import op

# revision identifiers, used by Alembic.
revision: str = "a79158b4a1ae"
down_revision: str | Sequence[str] | None = "e40d739b89c6"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None


def upgrade() -> None:
"""Upgrade schema."""
# Add measurement_providers JSONB column to tenants table
# Structure: {"providers": ["Provider 1", "Provider 2"], "default": "Provider 1"}
op.add_column("tenants", sa.Column("measurement_providers", sa.dialects.postgresql.JSONB(), nullable=True))


def downgrade() -> None:
"""Downgrade schema."""
op.drop_column("tenants", "measurement_providers")
81 changes: 81 additions & 0 deletions alembic/versions/c608bc822a13_add_dynamic_product_fields.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"""add_dynamic_product_fields

Revision ID: c608bc822a13
Revises: a79158b4a1ae
Create Date: 2025-11-07 18:06:33.079580

"""

from collections.abc import Sequence

import sqlalchemy as sa

from alembic import op

# revision identifiers, used by Alembic.
revision: str = "c608bc822a13"
down_revision: str | Sequence[str] | None = "a79158b4a1ae"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None


def upgrade() -> None:
"""Upgrade schema."""
# Add dynamic product fields
op.add_column("products", sa.Column("is_dynamic", sa.Boolean(), nullable=False, server_default="false"))
op.add_column("products", sa.Column("is_dynamic_variant", sa.Boolean(), nullable=False, server_default="false"))
op.add_column("products", sa.Column("parent_product_id", sa.String(100), nullable=True))
op.add_column("products", sa.Column("signals_agent_ids", sa.dialects.postgresql.JSONB(), nullable=True))
op.add_column("products", sa.Column("max_signals", sa.Integer(), nullable=False, server_default="5"))
op.add_column("products", sa.Column("activation_key", sa.dialects.postgresql.JSONB(), nullable=True))
op.add_column("products", sa.Column("signal_metadata", sa.dialects.postgresql.JSONB(), nullable=True))
op.add_column("products", sa.Column("last_synced_at", sa.DateTime(), nullable=True))
op.add_column("products", sa.Column("archived_at", sa.DateTime(), nullable=True))
op.add_column("products", sa.Column("variant_ttl_days", sa.Integer(), nullable=True))

# Add foreign key constraint for parent_product_id
op.create_foreign_key(
"fk_products_parent_product",
"products",
"products",
["tenant_id", "parent_product_id"],
["tenant_id", "product_id"],
ondelete="CASCADE",
)

# Add indexes for performance
op.create_index(
"idx_products_dynamic", "products", ["tenant_id", "is_dynamic"], postgresql_where=sa.text("is_dynamic = true")
)
op.create_index(
"idx_products_variants",
"products",
["tenant_id", "parent_product_id"],
postgresql_where=sa.text("parent_product_id IS NOT NULL"),
)
op.create_index(
"idx_products_archived", "products", ["archived_at"], postgresql_where=sa.text("archived_at IS NOT NULL")
)


def downgrade() -> None:
"""Downgrade schema."""
# Drop indexes
op.drop_index("idx_products_archived", "products")
op.drop_index("idx_products_variants", "products")
op.drop_index("idx_products_dynamic", "products")

# Drop foreign key
op.drop_constraint("fk_products_parent_product", "products", type_="foreignkey")

# Drop columns
op.drop_column("products", "variant_ttl_days")
op.drop_column("products", "archived_at")
op.drop_column("products", "last_synced_at")
op.drop_column("products", "signal_metadata")
op.drop_column("products", "activation_key")
op.drop_column("products", "max_signals")
op.drop_column("products", "signals_agent_ids")
op.drop_column("products", "parent_product_id")
op.drop_column("products", "is_dynamic_variant")
op.drop_column("products", "is_dynamic")
45 changes: 45 additions & 0 deletions alembic/versions/e40d739b89c6_add_product_detail_fields.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""add_product_detail_fields

Revision ID: e40d739b89c6
Revises: d169f2e66919
Create Date: 2025-11-07 08:01:39.496342

"""

from collections.abc import Sequence

import sqlalchemy as sa

from alembic import op

# revision identifiers, used by Alembic.
revision: str = "e40d739b89c6"
down_revision: str | Sequence[str] | None = "d169f2e66919"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None


def upgrade() -> None:
"""Upgrade schema."""
# Add delivery_measurement column (REQUIRED per AdCP spec)
op.add_column("products", sa.Column("delivery_measurement", sa.dialects.postgresql.JSONB(), nullable=True))

# Add optional product card columns
op.add_column("products", sa.Column("product_card", sa.dialects.postgresql.JSONB(), nullable=True))
op.add_column("products", sa.Column("product_card_detailed", sa.dialects.postgresql.JSONB(), nullable=True))

# Add optional placements column (array of placement objects)
op.add_column("products", sa.Column("placements", sa.dialects.postgresql.JSONB(), nullable=True))

# Add optional reporting_capabilities column
op.add_column("products", sa.Column("reporting_capabilities", sa.dialects.postgresql.JSONB(), nullable=True))


def downgrade() -> None:
"""Downgrade schema."""
# Remove columns in reverse order
op.drop_column("products", "reporting_capabilities")
op.drop_column("products", "placements")
op.drop_column("products", "product_card_detailed")
op.drop_column("products", "product_card")
op.drop_column("products", "delivery_measurement")
5 changes: 5 additions & 0 deletions product_catalog_providers/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,11 @@ async def _create_product_from_signals(
property_tags=["all_inventory"], # Required per AdCP spec (using property_tags instead of properties)
properties=None, # Using property_tags instead
estimated_exposures=None, # Optional - signals products don't have exposure estimates
delivery_measurement=None, # Optional - new field from product details
product_card=None, # Optional - new field from product details
product_card_detailed=None, # Optional - new field from product details
placements=None, # Optional - new field from product details
reporting_capabilities=None, # Optional - new field from product details
pricing_options=[
PricingOption(
pricing_option_id="cpm_usd_auction",
Expand Down
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ dependencies = [
"jinja2>=3.1.0",
"prometheus-client>=0.23.1",
"types-pytz>=2025.2.0.20250809",
"markdown>=3.4.0",
"types-Markdown>=3.4.2.10",
]


Expand Down
129 changes: 86 additions & 43 deletions schemas/v1/_schemas_v1_media-buy_create-media-buy-response_json.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,54 +2,97 @@
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "/schemas/v1/media-buy/create-media-buy-response.json",
"title": "Create Media Buy Response",
"description": "Response payload for create_media_buy task",
"description": "Response payload for create_media_buy task. Returns either complete success data OR error information, never both. This enforces atomic operation semantics - the media buy is either fully created or not created at all.",
"type": "object",
"properties": {
"media_buy_id": {
"type": "string",
"description": "Publisher's unique identifier for the created media buy"
},
"buyer_ref": {
"type": "string",
"description": "Buyer's reference identifier for this media buy"
},
"creative_deadline": {
"type": "string",
"format": "date-time",
"description": "ISO 8601 timestamp for creative upload deadline"
},
"packages": {
"type": "array",
"description": "Array of created packages",
"items": {
"type": "object",
"properties": {
"package_id": {
"type": "string",
"description": "Publisher's unique identifier for the package"
},
"buyer_ref": {
"type": "string",
"description": "Buyer's reference identifier for the package"
}
"oneOf": [
{
"description": "Success response - media buy created successfully",
"type": "object",
"properties": {
"media_buy_id": {
"type": "string",
"description": "Publisher's unique identifier for the created media buy"
},
"buyer_ref": {
"type": "string",
"description": "Buyer's reference identifier for this media buy"
},
"creative_deadline": {
"type": "string",
"format": "date-time",
"description": "ISO 8601 timestamp for creative upload deadline"
},
"packages": {
"type": "array",
"description": "Array of created packages",
"items": {
"type": "object",
"properties": {
"package_id": {
"type": "string",
"description": "Publisher's unique identifier for the package"
},
"buyer_ref": {
"type": "string",
"description": "Buyer's reference identifier for the package"
}
},
"required": [
"package_id",
"buyer_ref"
],
"additionalProperties": false
}
}
},
"required": [
"media_buy_id",
"buyer_ref",
"packages"
],
"additionalProperties": false,
"not": {
"required": [
"package_id",
"buyer_ref"
],
"additionalProperties": false
"errors"
]
}
},
"errors": {
"type": "array",
"description": "Task-specific errors and warnings (e.g., partial package creation failures)",
"items": {
"$ref": "/schemas/v1/core/error.json"
{
"description": "Error response - operation failed, no media buy created",
"type": "object",
"properties": {
"errors": {
"type": "array",
"description": "Array of errors explaining why the operation failed",
"items": {
"$ref": "/schemas/v1/core/error.json"
},
"minItems": 1
}
},
"required": [
"errors"
],
"additionalProperties": false,
"not": {
"anyOf": [
{
"required": [
"media_buy_id"
]
},
{
"required": [
"buyer_ref"
]
},
{
"required": [
"packages"
]
}
]
}
}
},
"required": [
"buyer_ref"
],
"additionalProperties": false
]
}
Loading