core/analytics/schemas/index.tstypescript
// Event schema types and definitions for analytics v2

// Canonical dl_* event vocabulary (the full firable superset). `eventSchemas`
// below defines field-level validation for the subset that carries one.
export * from './events';

/**
 * Product payload carried by commerce analytics events. Field names follow the GA4 item schema (`item_id`, `item_name`, `price`).
 *
 * @category Analytics
 */
export interface ProductSchema {
  item_id: string;
  item_name: string;
  affiliation?: string;
  coupon?: string;
  currency?: string;
  discount?: number;
  index?: number;
  item_brand?: string;
  item_category?: string;
  item_category2?: string;
  item_category3?: string;
  item_category4?: string;
  item_category5?: string;
  item_list_id?: string;
  item_list_name?: string;
  item_variant?: string;
  location_id?: string;
  price?: number;
  quantity?: number;
}

/**
 * Identical to {@link ProductSchema}; retained as a named alias so list
 * impressions read clearly at call sites.
 * @category Analytics
 */
export type ImpressionSchema = ProductSchema;

/**
 * Visitor and customer identity fields attached to analytics events as `user_properties`.
 *
 * @category Analytics
 */
export interface UserPropertiesSchema {
  visitor_type?: string;
  customer_id?: string;
  customer_email?: string;
  customer_phone?: string;
  customer_first_name?: string;
  customer_last_name?: string;
  customer_address_city?: string;
  customer_address_province?: string;
  customer_address_province_code?: string;
  customer_address_country?: string;
  customer_address_country_code?: string;
  customer_address_zip?: string;
  customer_order_count?: number;
  customer_total_spent?: number;
  customer_tags?: string;
  [key: string]: any;
}

/**
 * Fields shared by every analytics event: the `dl_*` event name plus optional session and customer context.
 *
 * @category Analytics
 */
export interface BaseEventSchema {
  event: string;
  event_id?: string;
  timestamp?: number;
  user_properties?: UserPropertiesSchema;
  ecommerce?: {
    currency?: string;
    value?: number;
    coupon?: string;
    items?: ProductSchema[];
    impressions?: ImpressionSchema[];
    [key: string]: any;
  };
  [key: string]: any;
}

export interface FieldDefinition {
  type: 'string' | 'number' | 'boolean' | 'object' | 'array';
  required?: boolean;
  properties?: Record<string, FieldDefinition>;
  items?: FieldDefinition;
  enum?: any[];
}

export interface EventSchema {
  name: string;
  fields: Record<string, FieldDefinition>;
}

// Common field definitions
const userPropertiesFields: Record<string, FieldDefinition> = {
  visitor_type: { type: 'string' },
  customer_id: { type: 'string' },
  customer_email: { type: 'string' },
  customer_phone: { type: 'string' },
  customer_first_name: { type: 'string' },
  customer_last_name: { type: 'string' },
  customer_address_city: { type: 'string' },
  customer_address_province: { type: 'string' },
  customer_address_province_code: { type: 'string' },
  customer_address_country: { type: 'string' },
  customer_address_country_code: { type: 'string' },
  customer_address_zip: { type: 'string' },
  customer_order_count: { type: 'number' },
  customer_total_spent: { type: 'number' },
  customer_tags: { type: 'string' }
};

const productFields: Record<string, FieldDefinition> = {
  item_id: { type: 'string', required: true },
  item_name: { type: 'string', required: true },
  affiliation: { type: 'string' },
  coupon: { type: 'string' },
  currency: { type: 'string' },
  discount: { type: 'number' },
  index: { type: 'number' },
  item_brand: { type: 'string' },
  item_category: { type: 'string' },
  item_category2: { type: 'string' },
  item_category3: { type: 'string' },
  item_category4: { type: 'string' },
  item_category5: { type: 'string' },
  item_list_id: { type: 'string' },
  item_list_name: { type: 'string' },
  item_variant: { type: 'string' },
  item_image: { type: 'string' },
  location_id: { type: 'string' },
  price: { type: 'number' },
  quantity: { type: 'number' }
};

const ecommerceWithItemsFields: Record<string, FieldDefinition> = {
  currency: { type: 'string' },
  value: { type: 'number' },
  coupon: { type: 'string' },
  items: {
    type: 'array',
    items: {
      type: 'object',
      properties: productFields
    }
  }
};

// Event schemas definitions
export const eventSchemas: Record<string, EventSchema> = {
  dl_user_data: {
    name: 'dl_user_data',
    fields: {
      event: { type: 'string', required: true },
      user_properties: {
        type: 'object',
        required: true,
        properties: userPropertiesFields
      },
      ecommerce: {
        type: 'object',
        properties: {
          ...ecommerceWithItemsFields,
          // cart_contents is deprecated but still supported for backward compatibility
          cart_contents: {
            type: 'array',
            items: {
              type: 'object',
              properties: productFields
            }
          }
        }
      }
    }
  },

  dl_sign_up: {
    name: 'dl_sign_up',
    fields: {
      event: { type: 'string', required: true },
      user_properties: {
        type: 'object',
        properties: userPropertiesFields
      },
      method: { type: 'string' }
    }
  },

  dl_login: {
    name: 'dl_login',
    fields: {
      event: { type: 'string', required: true },
      user_properties: {
        type: 'object',
        properties: userPropertiesFields
      },
      method: { type: 'string' }
    }
  },

  dl_view_item_list: {
    name: 'dl_view_item_list',
    fields: {
      event: { type: 'string', required: true },
      ecommerce: {
        type: 'object',
        required: true,
        properties: {
          ...ecommerceWithItemsFields,
          item_list_id: { type: 'string' },
          item_list_name: { type: 'string' },
          // impressions is deprecated but still supported for backward compatibility
          impressions: {
            type: 'array',
            items: {
              type: 'object',
              properties: productFields
            }
          }
        }
      },
      user_properties: {
        type: 'object',
        properties: userPropertiesFields
      }
    }
  },

  dl_view_search_results: {
    name: 'dl_view_search_results',
    fields: {
      event: { type: 'string', required: true },
      search_term: { type: 'string', required: true },
      ecommerce: {
        type: 'object',
        properties: {
          ...ecommerceWithItemsFields,
          item_list_name: { type: 'string' },
          // impressions is deprecated but still supported for backward compatibility
          impressions: {
            type: 'array',
            items: {
              type: 'object',
              properties: productFields
            }
          }
        }
      },
      user_properties: {
        type: 'object',
        properties: userPropertiesFields
      }
    }
  },

  dl_select_item: {
    name: 'dl_select_item',
    fields: {
      event: { type: 'string', required: true },
      ecommerce: {
        type: 'object',
        required: true,
        properties: {
          ...ecommerceWithItemsFields,
          item_list_id: { type: 'string' },
          item_list_name: { type: 'string' }
        }
      },
      user_properties: {
        type: 'object',
        properties: userPropertiesFields
      }
    }
  },

  dl_view_item: {
    name: 'dl_view_item',
    fields: {
      event: { type: 'string', required: true },
      ecommerce: {
        type: 'object',
        required: true,
        properties: ecommerceWithItemsFields
      },
      user_properties: {
        type: 'object',
        properties: userPropertiesFields
      }
    }
  },

  dl_add_to_cart: {
    name: 'dl_add_to_cart',
    fields: {
      event: { type: 'string', required: true },
      ecommerce: {
        type: 'object',
        required: true,
        properties: ecommerceWithItemsFields
      },
      user_properties: {
        type: 'object',
        properties: userPropertiesFields
      }
    }
  },

  dl_remove_from_cart: {
    name: 'dl_remove_from_cart',
    fields: {
      event: { type: 'string', required: true },
      ecommerce: {
        type: 'object',
        required: true,
        properties: ecommerceWithItemsFields
      },
      user_properties: {
        type: 'object',
        properties: userPropertiesFields
      }
    }
  },

  dl_view_cart: {
    name: 'dl_view_cart',
    fields: {
      event: { type: 'string', required: true },
      ecommerce: {
        type: 'object',
        required: true,
        properties: ecommerceWithItemsFields
      },
      user_properties: {
        type: 'object',
        properties: userPropertiesFields
      }
    }
  },

  dl_begin_checkout: {
    name: 'dl_begin_checkout',
    fields: {
      event: { type: 'string', required: true },
      ecommerce: {
        type: 'object',
        required: true,
        properties: {
          ...ecommerceWithItemsFields,
          checkout_id: { type: 'string' },
          checkout_step: { type: 'number' }
        }
      },
      user_properties: {
        type: 'object',
        properties: userPropertiesFields
      }
    }
  },

  dl_add_shipping_info: {
    name: 'dl_add_shipping_info',
    fields: {
      event: { type: 'string', required: true },
      ecommerce: {
        type: 'object',
        required: true,
        properties: {
          ...ecommerceWithItemsFields,
          shipping_tier: { type: 'string' }
        }
      },
      shipping_tier: { type: 'string' },
      user_properties: {
        type: 'object',
        properties: userPropertiesFields
      }
    }
  },

  dl_add_payment_info: {
    name: 'dl_add_payment_info',
    fields: {
      event: { type: 'string', required: true },
      ecommerce: {
        type: 'object',
        required: true,
        properties: {
          ...ecommerceWithItemsFields,
          payment_type: { type: 'string' }
        }
      },
      payment_type: { type: 'string' },
      user_properties: {
        type: 'object',
        properties: userPropertiesFields
      }
    }
  },

  dl_purchase: {
    name: 'dl_purchase',
    fields: {
      event: { type: 'string', required: true },
      ecommerce: {
        type: 'object',
        required: true,
        properties: {
          ...ecommerceWithItemsFields,
          transaction_id: { type: 'string', required: true },
          affiliation: { type: 'string' },
          tax: { type: 'number' },
          shipping: { type: 'number' },
          discount: { type: 'number' }
        }
      },
      user_properties: {
        type: 'object',
        properties: userPropertiesFields
      }
    }
  },

  dl_subscribe: {
    name: 'dl_subscribe',
    fields: {
      event: { type: 'string', required: true },
      ecommerce: {
        type: 'object',
        properties: {
          ...ecommerceWithItemsFields,
          subscription_id: { type: 'string' },
          subscription_status: { type: 'string' }
        }
      },
      user_properties: {
        type: 'object',
        properties: userPropertiesFields
      }
    }
  },

  // Upsell events
  dl_viewed_upsell: {
    name: 'dl_viewed_upsell',
    fields: {
      event: { type: 'string', required: true },
      order_id: { type: 'string', required: true },
      upsell: {
        type: 'object',
        required: true,
        properties: {
          package_id: { type: 'string', required: true },
          package_name: { type: 'string', required: true },
          price: { type: 'number' },
          currency: { type: 'string' }
        }
      }
    }
  },

  dl_accepted_upsell: {
    name: 'dl_accepted_upsell',
    fields: {
      event: { type: 'string', required: true },
      order_id: { type: 'string', required: true },
      upsell: {
        type: 'object',
        required: true,
        properties: {
          package_id: { type: 'string', required: true },
          package_name: { type: 'string' },
          quantity: { type: 'number' },
          value: { type: 'number', required: true },
          currency: { type: 'string' }
        }
      }
    }
  },

  dl_skipped_upsell: {
    name: 'dl_skipped_upsell',
    fields: {
      event: { type: 'string', required: true },
      order_id: { type: 'string', required: true },
      upsell: {
        type: 'object',
        required: true,
        properties: {
          package_id: { type: 'string' },
          package_name: { type: 'string' }
        }
      }
    }
  },

  // Accepted upsell in GA4 purchase format (counterpart to dl_accepted_upsell).
  dl_upsell_purchase: {
    name: 'dl_upsell_purchase',
    fields: {
      event: { type: 'string', required: true },
      ecommerce: {
        type: 'object',
        required: true,
        properties: {
          ...ecommerceWithItemsFields,
          transaction_id: { type: 'string', required: true },
          affiliation: { type: 'string' },
          tax: { type: 'number' },
          shipping: { type: 'number' }
        }
      },
      upsell_metadata: {
        type: 'object',
        properties: {
          original_order_id: { type: 'string' },
          upsell_number: { type: 'number' },
          package_id: { type: 'string' },
          package_name: { type: 'string' }
        }
      },
      user_properties: {
        type: 'object',
        properties: userPropertiesFields
      }
    }
  }
};

// Validation function
export function validateEventSchema(eventData: any, schema: EventSchema): { valid: boolean; errors: string[] } {
  const errors: string[] = [];

  function validateField(value: any, fieldDef: FieldDefinition, path: string): void {
    // Check if required field is missing
    if (fieldDef.required && (value === undefined || value === null)) {
      errors.push(`Missing required field: ${path}`);
      return;
    }

    // Skip validation if field is optional and not provided
    if (value === undefined || value === null) {
      return;
    }

    // Validate type
    const actualType = Array.isArray(value) ? 'array' : typeof value;
    if (actualType !== fieldDef.type) {
      errors.push(`Invalid type for ${path}: expected ${fieldDef.type}, got ${actualType}`);
      return;
    }

    // Validate enum values
    if (fieldDef.enum && !fieldDef.enum.includes(value)) {
      errors.push(`Invalid value for ${path}: must be one of ${fieldDef.enum.join(', ')}`);
    }

    // Validate object properties
    if (fieldDef.type === 'object' && fieldDef.properties) {
      for (const [propName, propDef] of Object.entries(fieldDef.properties)) {
        validateField(value[propName], propDef, `${path}.${propName}`);
      }
    }

    // Validate array items
    if (fieldDef.type === 'array' && fieldDef.items) {
      value.forEach((item: any, index: number) => {
        validateField(item, fieldDef.items!, `${path}[${index}]`);
      });
    }
  }

  // Validate all fields defined in schema
  for (const [fieldName, fieldDef] of Object.entries(schema.fields)) {
    validateField(eventData[fieldName], fieldDef, fieldName);
  }

  return {
    valid: errors.length === 0,
    errors
  };
}

// Helper function to get schema by event name
export function getEventSchema(eventName: string): EventSchema | undefined {
  return eventSchemas[eventName];
}