Files
starr-philproperties/database/migrations/20270101000089-create-user-tier-grants.js
T
2026-08-05 04:32:43 +08:00

49 lines
2.4 KiB
JavaScript

'use strict';
// Tier Plans v2: item-specific entitlement. Until now a Tier Plan purchase
// only recorded `user_tiers.plan_id` + tier rank; the actual courses/units/
// lessons granted were re-resolved LIVE off plan_courses/plan_units/
// plan_lessons on every access check, so editing a plan's bundle later
// silently changed what past purchasers could access, and a Unit-only bundle
// couldn't be distinguished from its parent Course's bundle.
//
// This table is the purchase-time snapshot: one row per exact item granted
// by a given user_tiers purchase. Access checks now look up rows here
// instead of comparing tier rank against item subscription (see hasItemGrant()
// in controllers/client/courses.controller.js).
//
// item_type is STRING + CHECK, not a real ENUM, mirroring
// products.purchasable_type (20270101000078) — CockroachDB can't create a
// new enum type via addColumn the way createTable can.
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('user_tier_grants', {
id: { type: Sequelize.BIGINT, primaryKey: true, autoIncrement: true },
user_tier_id: { type: Sequelize.BIGINT, allowNull: false, references: { model: 'user_tiers', key: 'tier_id' }, onDelete: 'CASCADE' },
user_id: { type: Sequelize.BIGINT, allowNull: false, references: { model: 'users', key: 'user_id' }, onDelete: 'CASCADE' },
plan_id: { type: Sequelize.BIGINT, allowNull: true, references: { model: 'tier_plans', key: 'plan_id' }, onUpdate: 'CASCADE', onDelete: 'SET NULL' },
item_type: { type: Sequelize.STRING(10), allowNull: false },
item_id: { type: Sequelize.BIGINT, allowNull: false },
granted_at: { type: Sequelize.DATE, allowNull: false, defaultValue: Sequelize.NOW },
createdAt: { type: Sequelize.DATE, allowNull: false },
updatedAt: { type: Sequelize.DATE, allowNull: false },
});
await queryInterface.addConstraint('user_tier_grants', {
fields: ['item_type'],
type: 'check',
name: 'check_user_tier_grants_item_type',
where: { item_type: { [Sequelize.Op.in]: ['course', 'unit', 'lesson'] } },
});
await queryInterface.addIndex('user_tier_grants', ['user_id', 'item_type', 'item_id'], {
name: 'user_tier_grants_user_item_idx',
});
await queryInterface.addIndex('user_tier_grants', ['user_tier_id']);
},
async down(queryInterface) {
await queryInterface.dropTable('user_tier_grants');
},
};