Back to .md Directory

Data Models

Defines TypeScript interfaces, PostgreSQL tables, and API request/response models for a promotional campaign planning system.

May 2, 2026
0 downloads
1 views
ai rag
View source

What this file does

Defines TypeScript interfaces, PostgreSQL tables, and API request/response models for a promotional campaign planning system.

When to use it

  • Building a promo planning or campaign management system
  • Designing data models for retail promotion workflows
  • Implementing scenario analysis and KPI tracking
  • Creating validation and post-mortem reporting for promotions

Assumes this stack

TypeScriptPostgreSQLPythonPydanticFastAPIZod

Data Models

This document describes the core data structures used throughout the system.

Core Models

PromoContext

Represents the contextual environment for promotional planning.

interface PromoContext {
  geo: string;  // e.g., "DE", "UA"
  date_range: {
    start: string;  // ISO date
    end: string;
  };
  events: Event[];
  seasonality_factors: SeasonalityFactor[];
  weekend_pattern: {
    weekend_days: string[];  // ["SAT", "SUN"]
  };
  weather_profile: {
    location: string;  // Geographic code (e.g., "DE", "UA")
    coordinates?: {
      latitude: number;
      longitude: number;
      name: string;
    };
    summary: string;  // "rainy_weekend, with 2 rainy day(s), avg 12.5°C"
    daily: DailyWeather[];
  };
}

interface Event {
  name: string;
  date: string;  // ISO date
  type: "global_sale" | "public_holiday" | "local_event" | "payday";
  impact?: "high" | "medium" | "low";
}

interface SeasonalityFactor {
  department: string;
  month: number;
  factor: number;  // Multiplier (1.1 = 10% above average)
}

interface DailyWeather {
  date: string;
  condition: "sun" | "cloud" | "rain" | "snow" | "storm";
  temp_max: number;  // Celsius
  temp_min: number;  // Celsius
  temp_avg: number;  // Celsius
  rain_prob: number;  // 0-100
  rain_sum: number;  // mm
  cloud_cover?: number;  // 0-100
}

PromoBrief

Structured brief extracted from user input.

interface PromoBrief {
  month: string;  // "2024-10"
  promo_date_range: {
    start: string;
    end: string;
  };
  gap_to_target: {
    sales_value: number;  // Negative = below target
    margin_points: number;
  };
  objectives: {
    sales_value_target: number;
    margin_floor_pct: number;
    ebit_target?: number;
  };
  focus_departments: string[];  // ["TV", "Gaming"]
  channels: ("online" | "offline")[];
  risk_appetite: "low" | "medium" | "high";
  loyalty_focus: boolean;
  constraints: {
    max_discount_pct: Record<string, number>;  // By department
    min_margin_pct: number;
    excluded_brands?: string[];
  };
}

PromoOpportunity

Identified opportunity for a promotional campaign.

interface PromoOpportunity {
  id: string;
  title: string;
  promo_date_range: {
    start: string;
    end: string;
  };
  description: string;
  focus_departments: string[];
  estimated_potential: {
    sales_value: number;
    margin_impact: number;  // Percentage points
  };
  context_snapshot: PromoContext;
  priority: "high" | "medium" | "low";
}

PromoScenario

A specific promotional scenario with mechanics.

interface PromoScenario {
  id: string;
  label: string;  // "Conservative", "Balanced", "Aggressive"
  source_opportunity_id?: string;
  date_range: {
    start: string;
    end: string;
  };
  mechanics: PromoMechanic[];
  scenario_type?: "flash" | "best_deal" | "coupon" | "member_only";
}

interface PromoMechanic {
  department: string;
  channel: "online" | "offline" | "both";
  discount_pct: number;
  segments: string[];  // ["ALL"] or ["LOYAL_HIGH_VALUE"]
  notes?: string;
  product_focus?: string[];  // Specific products/brands
}

ScenarioKPI

Key performance indicators for a scenario.

interface ScenarioKPI {
  scenario_id: string;
  period: string;  // "2024-10-22..2024-10-27"
  total: {
    sales_value: number;
    margin_value: number;
    margin_pct: number;
    ebit: number;
    units: number;
  };
  vs_baseline: {
    sales_value_delta: number;
    margin_value_delta: number;
    ebit_delta: number;
    units_delta: number;
  };
  by_channel: ChannelKPI[];
  by_department: DepartmentKPI[];
  by_segment: SegmentKPI[];
}

interface ChannelKPI {
  channel: "online" | "offline";
  sales_value: number;
  margin_pct: number;
  units: number;
}

interface DepartmentKPI {
  department: string;
  sales_value: number;
  margin_pct: number;
  units: number;
}

interface SegmentKPI {
  segment_id: string;
  sales_value: number;
  margin_pct: number;
  units: number;
  comment?: string;
}

ValidationReport

Validation results for a scenario.

interface ValidationReport {
  scenario_id: string;
  status: "PASS" | "WARN" | "BLOCK";
  issues: ValidationIssue[];
  overall_score?: number;  // 0-100
}

interface ValidationIssue {
  type: 
    | "margin_threshold"
    | "discount_limit"
    | "kpi_plausibility"
    | "brand_compliance"
    | "cannibalization_risk";
  severity: "low" | "medium" | "high" | "critical";
  message: string;
  suggested_fix?: string;
  affected_department?: string;
}

PostMortemReport

Post-campaign analysis report.

interface PostMortemReport {
  scenario_id: string;
  period: string;
  forecast_kpi: ScenarioKPI;
  actual_kpi: {
    total: {
      sales_value: number;
      margin_value: number;
      margin_pct: number;
      ebit: number;
      units: number;
    };
  };
  vs_forecast: {
    sales_value_error_pct: number;  // Negative = under-forecast
    margin_value_error_pct: number;
    ebit_error_pct: number;
    units_error_pct: number;
  };
  post_promo_dip?: {
    period: string;
    sales_vs_baseline_delta: number;
  };
  cannibalization_signals: CannibalizationSignal[];
  insights: string[];
  learning_points: string[];
}

interface CannibalizationSignal {
  department: string;
  effect: "negative" | "positive" | "neutral";
  sales_vs_baseline_delta_pct: number;
  comment: string;
}

CreativeBrief

Creative brief for campaign assets.

interface CreativeBrief {
  scenario_id: string;
  objectives: {
    primary: string;
    secondary: string[];
  };
  target_audience: {
    segments: string[];
    demographics?: string;
    psychographics?: string;
  };
  key_messages: string[];
  tone: string;  // "energetic", "trustworthy", "urgent"
  mandatory_elements: {
    legal: string[];
    brand: string[];
  };
  assets: AssetSpec[];
}

interface AssetSpec {
  type: 
    | "homepage_hero"
    | "category_banner"
    | "in_store_sheet"
    | "email_hero"
    | "video_storyboard";
  title: string;
  headline: string;
  subheadline?: string;
  cta: string;
  product_focus: string[];
  layout_hints?: string;
  segment_specific?: boolean;
  target_segment?: string;
}

Segment

Customer segment from CDP.

interface Segment {
  segment_id: string;
  name: string;
  share_of_customers: number;  // 0-1
  share_of_revenue: number;  // 0-1
  avg_basket_value: number;
  fav_categories: string[];
  discount_sensitivity: "low" | "medium" | "high";
  purchase_frequency?: number;
  last_purchase_days_ago?: number;
}

UpliftModel

Uplift coefficients for forecasting.

interface UpliftModel {
  coefficients: UpliftCoefficient[];
  last_updated: string;
  version: string;
}

interface UpliftCoefficient {
  department: string;
  channel: "online" | "offline";
  discount_band: string;  // "0-10", "10-20", "20-30", "30+"
  uplift_sales_pct: number;  // Percentage uplift
  uplift_units_pct: number;
  margin_impact_pct: number;  // Change in margin percentage points
  confidence?: number;  // 0-1
  sample_size?: number;
}

BaselineForecast

Baseline forecast without promotions.

interface BaselineForecast {
  period: {
    start: string;
    end: string;
  };
  daily: DailyForecast[];
  totals: {
    sales_value: number;
    margin_value: number;
    margin_pct: number;
    units: number;
  };
  by_channel: ChannelForecast[];
  by_department: DepartmentForecast[];
}

interface DailyForecast {
  date: string;
  sales_value: number;
  margin_value: number;
  units: number;
  day_of_week: string;
}

interface ChannelForecast {
  channel: "online" | "offline";
  sales_value: number;
  margin_pct: number;
}

interface DepartmentForecast {
  department: string;
  sales_value: number;
  margin_pct: number;
}

Database Schema (PostgreSQL)

Tables

-- Promotional scenarios
CREATE TABLE promo_scenarios (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    label VARCHAR(100) NOT NULL,
    source_opportunity_id UUID,
    date_range_start DATE NOT NULL,
    date_range_end DATE NOT NULL,
    scenario_type VARCHAR(50),
    mechanics JSONB NOT NULL,
    created_at TIMESTAMP DEFAULT NOW(),
    updated_at TIMESTAMP DEFAULT NOW()
);

-- Scenario KPIs
CREATE TABLE scenario_kpis (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    scenario_id UUID REFERENCES promo_scenarios(id),
    period_start DATE NOT NULL,
    period_end DATE NOT NULL,
    total_sales_value NUMERIC(15,2),
    total_margin_value NUMERIC(15,2),
    total_margin_pct NUMERIC(5,2),
    total_ebit NUMERIC(15,2),
    total_units INTEGER,
    kpi_breakdown JSONB,  -- by_channel, by_department, by_segment
    created_at TIMESTAMP DEFAULT NOW()
);

-- Validation reports
CREATE TABLE validation_reports (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    scenario_id UUID REFERENCES promo_scenarios(id),
    status VARCHAR(20) NOT NULL,
    issues JSONB,
    overall_score INTEGER,
    created_at TIMESTAMP DEFAULT NOW()
);

-- Post-mortem reports
CREATE TABLE post_mortem_reports (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    scenario_id UUID REFERENCES promo_scenarios(id),
    period_start DATE NOT NULL,
    period_end DATE NOT NULL,
    forecast_kpi_id UUID REFERENCES scenario_kpis(id),
    actual_kpi JSONB NOT NULL,
    vs_forecast JSONB,
    insights TEXT[],
    created_at TIMESTAMP DEFAULT NOW()
);

-- Creative briefs
CREATE TABLE creative_briefs (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    scenario_id UUID REFERENCES promo_scenarios(id),
    brief JSONB NOT NULL,
    assets JSONB,
    created_at TIMESTAMP DEFAULT NOW()
);

-- Historical sales data (aggregated)
CREATE TABLE sales_aggregated (
    date DATE NOT NULL,
    channel VARCHAR(20) NOT NULL,
    department VARCHAR(50) NOT NULL,
    promo_flag BOOLEAN DEFAULT FALSE,
    discount_pct NUMERIC(5,2),
    sales_value NUMERIC(15,2),
    margin_value NUMERIC(15,2),
    units INTEGER,
    PRIMARY KEY (date, channel, department)
);

-- Customer segments (from CDP)
CREATE TABLE segments (
    segment_id VARCHAR(50) PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    share_of_customers NUMERIC(5,4),
    share_of_revenue NUMERIC(5,4),
    avg_basket_value NUMERIC(10,2),
    fav_categories TEXT[],
    discount_sensitivity VARCHAR(20),
    updated_at TIMESTAMP DEFAULT NOW()
);

-- Uplift model coefficients
CREATE TABLE uplift_coefficients (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    department VARCHAR(50) NOT NULL,
    channel VARCHAR(20) NOT NULL,
    discount_band VARCHAR(20) NOT NULL,
    uplift_sales_pct NUMERIC(6,2),
    uplift_units_pct NUMERIC(6,2),
    margin_impact_pct NUMERIC(6,2),
    confidence NUMERIC(3,2),
    sample_size INTEGER,
    model_version VARCHAR(20),
    created_at TIMESTAMP DEFAULT NOW(),
    UNIQUE (department, channel, discount_band, model_version)
);

API Request/Response Models

Create Scenario Request

interface CreateScenarioRequest {
  brief: PromoBrief;
  scenario_type?: "conservative" | "balanced" | "aggressive";
  custom_parameters?: Record<string, any>;
}

Evaluate Scenario Response

interface EvaluateScenarioResponse {
  scenario: PromoScenario;
  kpi: ScenarioKPI;
  validation: ValidationReport;
  baseline_comparison: {
    sales_delta: number;
    margin_delta: number;
    ebit_delta: number;
  };
}

Optimize Scenarios Request

interface OptimizeScenariosRequest {
  brief: PromoBrief;
  objectives: {
    maximize: "sales" | "margin" | "ebit";
    constraints: {
      min_margin_pct?: number;
      max_discount_pct?: number;
    };
  };
  num_scenarios?: number;  // Default: 3
}

Generate Creative Request

interface GenerateCreativeRequest {
  scenario_id: string;
  asset_types?: string[];  // If omitted, generates all
  target_segments?: string[];
}

Data Validation

All models should be validated using:

  • Python: Pydantic models
  • TypeScript: Zod schemas
  • API: FastAPI automatic validation

Example Pydantic model:

from pydantic import BaseModel, Field, validator
from typing import List, Optional
from datetime import date

class PromoBrief(BaseModel):
    month: str = Field(..., regex=r"^\d{4}-\d{2}$")
    promo_date_range: dict
    gap_to_target: dict
    objectives: dict
    focus_departments: List[str]
    channels: List[str]
    risk_appetite: str = Field(..., regex="^(low|medium|high)$")
    loyalty_focus: bool
    constraints: dict
    
    @validator('channels')
    def validate_channels(cls, v):
        allowed = ['online', 'offline']
        if not all(c in allowed for c in v):
            raise ValueError('Invalid channel')
        return v

What's inside

14 TypeScript interfaces, 7 SQL table definitions, 4 API model types, 1 Pydantic validation example

Change this for your project

  • Replace Iemelianov/mms-promo-intelligence with your own repository name
  • Replace DE, UA in PromoContext.geo with your own geographic codes
  • Replace TV, Gaming in PromoBrief.focus_departments with your own departments
  • Replace LOYAL_HIGH_VALUE in PromoMechanic.segments with your own segment IDs

Where it goes

Keep it in your repository where the agent or team that needs it will read it.

Worth borrowing

  • Separating context (PromoContext) from brief (PromoBrief) keeps planning inputs modular
  • Using a validation report with severity levels and suggested fixes enables automated guardrails
  • Post-mortem reports that compare forecast vs actual with cannibalization signals close the feedback loop

Related Documents