// models/advertisements/advertisements.mdl.js const { DataTypes } = require("sequelize"); const sequelize = require("../../config/db.config"); const mdl_Users = require("../users/users.mdl"); const mdl_Assets = require("../assets/assets.mdl"); const Advertisement = sequelize.define("Advertisement", { // ─── Identity ───────────────────────────────────────────────────────────── advertisement_id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true, label: "Advertisement ID", order: 0, hidden: true }, uuid: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, allowNull: false, unique: true, label: "UUID", order: 0, hidden: true }, // ─── Identification / placement ────────────────────────────────────────── // placement is the source of truth for "where" (a registry key, see // advertisements.placements.js); type is denormalized from it on every // write (applyAdvertisementFields) and describes "what it looks like". placement: { type: DataTypes.STRING(100), allowNull: true, filterable: true, label: "Placement", order: 1 }, type: { type: DataTypes.ENUM("hero", "banner", "popup", "sidebar"), allowNull: false, filterable: true, label: "Format", order: 2 }, status: { type: DataTypes.ENUM("draft", "active", "scheduled", "expired", "archived"), allowNull: false, defaultValue: "draft", label: "Status", order: 3 }, // ─── Content ────────────────────────────────────────────────────────────── // Every ad is now created/edited as "content" (badge/headline/description/ // image/link all mandatory) — applyAdvertisementFields on the backend fixes // this server-side rather than accepting it from the client. "image" is // legacy-only, left on rows created before the Image Only / Text with Image // toggle was removed, until they're next edited. content_mode: { type: DataTypes.ENUM("image", "content"), allowNull: false, defaultValue: "image", label: "Content Mode", order: 3.5 }, // Up to 2 outline-badge chips shown alongside the headline — see MAX_BADGE_LABELS. badge_labels: { type: DataTypes.JSONB, allowNull: false, defaultValue: [], label: "Badge Labels", order: 4 }, headline: { type: DataTypes.STRING(255), label: "Headline", order: 5 }, description: { type: DataTypes.TEXT, label: "Description", order: 6 }, // ─── Media ──────────────────────────────────────────────────────────────── image_url: { type: DataTypes.STRING(512), label: "Image URL", order: 0, hidden: true }, image_asset_id: { type: DataTypes.BIGINT, allowNull: true, label: "Image Asset", order: 0, hidden: true }, // ─── Calls-to-action ────────────────────────────────────────────────────── // [{ label, link }, ...] — 0-2 entries depending on type ctas: { type: DataTypes.JSONB, allowNull: false, defaultValue: [], label: "CTAs", order: 7 }, // Where the ad as a whole links to when clicked with no CTA of its own // (banners/full-image ads have no CTA row) — takes priority over landing_page. redirect_link: { type: DataTypes.STRING(512), allowNull: true, label: "Redirect Link", order: 7.5 }, // Internally-authored landing page { title, description, body, links: [{label, link}] } // used as the click-through destination when redirect_link is empty — see // GET /api/client/advertisements/uuid/:uuid and the /ads/:uuid client route. landing_page: { type: DataTypes.JSONB, allowNull: true, label: "Landing Page", order: 0, hidden: true }, // ─── Scheduling ─────────────────────────────────────────────────────────── start_date: { type: DataTypes.DATE, label: "Start Date", order: 8 }, end_date: { type: DataTypes.DATE, label: "End Date", order: 9 }, // ─── Display behavior ───────────────────────────────────────────────────── order: { type: DataTypes.INTEGER, allowNull: false, defaultValue: 0, field: "order", label: "Order", order: 10 }, is_active: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: true, label: "Active", order: 11 }, size: { type: DataTypes.ENUM("sm", "md", "lg"), allowNull: true, label: "Size", order: 12 }, // banner-only, ignored by other types // ─── Metrics ────────────────────────────────────────────────────────────── click_count: { type: DataTypes.INTEGER, allowNull: false, defaultValue: 0, label: "Clicks", order: 13, hidden: true }, // ─── Audit trails ───────────────────────────────────────────────────────── createdBy: { type: DataTypes.BIGINT, allowNull: true, label: "Created By" }, updatedBy: { type: DataTypes.BIGINT, allowNull: true, label: "Modified By" }, deletedBy: { type: DataTypes.BIGINT, allowNull: true, label: "Deleted By" }, }, { tableName: "advertisements", timestamps: true, // createdAt, updatedAt paranoid: true, indexes: [ { fields: ["uuid"] }, { fields: ["type"] }, { fields: ["placement"] }, { fields: ["status"] }, { fields: ["is_active"] }, { fields: ["deletedAt"] }, ], }); Advertisement.belongsTo(mdl_Assets, { as: "image", foreignKey: "image_asset_id" }); Advertisement.belongsTo(mdl_Users, { as: "creator", foreignKey: "createdBy" }); Advertisement.belongsTo(mdl_Users, { as: "updater", foreignKey: "updatedBy" }); module.exports = Advertisement;