mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
client and some admin new
Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
'use strict';
|
||||
|
||||
// Tier Plans v2: bundle items (courses/units/lessons) may now belong to more
|
||||
// than one plan simultaneously — the old single-column unique constraints
|
||||
// enforced "one item = at most one plan ever", which blocks that. Replace
|
||||
// each with a composite unique on (plan_id, item_id), which still prevents
|
||||
// the same item being added twice to the *same* plan.
|
||||
module.exports = {
|
||||
async up(queryInterface) {
|
||||
await queryInterface.removeConstraint('plan_courses', 'plan_courses_course_id_key').catch(() => {});
|
||||
await queryInterface.removeIndex('plan_courses', 'plan_courses_course_id_key').catch(() => {});
|
||||
await queryInterface.addIndex('plan_courses', ['plan_id', 'course_id'], {
|
||||
unique: true,
|
||||
name: 'plan_courses_plan_id_course_id_unique',
|
||||
});
|
||||
|
||||
await queryInterface.removeConstraint('plan_units', 'plan_units_unit_id_key').catch(() => {});
|
||||
await queryInterface.removeIndex('plan_units', 'plan_units_unit_id_key').catch(() => {});
|
||||
await queryInterface.addIndex('plan_units', ['plan_id', 'unit_id'], {
|
||||
unique: true,
|
||||
name: 'plan_units_plan_id_unit_id_unique',
|
||||
});
|
||||
|
||||
await queryInterface.removeConstraint('plan_lessons', 'plan_lessons_lesson_id_key').catch(() => {});
|
||||
await queryInterface.removeIndex('plan_lessons', 'plan_lessons_lesson_id_key').catch(() => {});
|
||||
await queryInterface.addIndex('plan_lessons', ['plan_id', 'lesson_id'], {
|
||||
unique: true,
|
||||
name: 'plan_lessons_plan_id_lesson_id_unique',
|
||||
});
|
||||
},
|
||||
|
||||
async down(queryInterface, Sequelize) {
|
||||
await queryInterface.removeIndex('plan_courses', 'plan_courses_plan_id_course_id_unique');
|
||||
await queryInterface.changeColumn('plan_courses', 'course_id', { type: Sequelize.BIGINT, allowNull: false, unique: true });
|
||||
|
||||
await queryInterface.removeIndex('plan_units', 'plan_units_plan_id_unit_id_unique');
|
||||
await queryInterface.changeColumn('plan_units', 'unit_id', { type: Sequelize.BIGINT, allowNull: false, unique: true });
|
||||
|
||||
await queryInterface.removeIndex('plan_lessons', 'plan_lessons_plan_id_lesson_id_unique');
|
||||
await queryInterface.changeColumn('plan_lessons', 'lesson_id', { type: Sequelize.BIGINT, allowNull: false, unique: true });
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
'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');
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
'use strict';
|
||||
|
||||
// Tier Plans v2: item-specific entitlement means two DIFFERENT plans at the
|
||||
// SAME tier slug (e.g. two separate Premium bundles) must be able to stay
|
||||
// concurrently active for one user, each governing its own distinct
|
||||
// user_tier_grants snapshot — buying a second Premium bundle must not just
|
||||
// extend the first one's expiry (see captureOrder in
|
||||
// controllers/client/tiers.controller.js).
|
||||
//
|
||||
// The old partial unique index (20270101000075) enforced "one active row per
|
||||
// (user_id, tier)", which blocks that. Replace it with "one active row per
|
||||
// (user_id, plan_id)" — still prevents accidentally double-purchasing/
|
||||
// double-granting the exact same plan concurrently, but allows multiple
|
||||
// plans at the same tier to coexist. NULL plan_id rows (admin-granted
|
||||
// tiers with no backing plan) are never considered equal to one another by
|
||||
// a unique index, so they're unaffected.
|
||||
//
|
||||
// NOT auto-run — do not execute against the shared dev/prod database without
|
||||
// explicit confirmation.
|
||||
module.exports = {
|
||||
async up(queryInterface) {
|
||||
await queryInterface.removeIndex('user_tiers', 'user_tiers_one_active_per_user_tier');
|
||||
await queryInterface.addIndex('user_tiers', ['user_id', 'plan_id'], {
|
||||
unique: true,
|
||||
where: { status: 'active' },
|
||||
name: 'user_tiers_one_active_per_user_plan',
|
||||
});
|
||||
},
|
||||
|
||||
async down(queryInterface) {
|
||||
await queryInterface.removeIndex('user_tiers', 'user_tiers_one_active_per_user_plan');
|
||||
await queryInterface.addIndex('user_tiers', ['user_id', 'tier'], {
|
||||
unique: true,
|
||||
where: { status: 'active' },
|
||||
name: 'user_tiers_one_active_per_user_tier',
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
'use strict';
|
||||
|
||||
// The "Access Rules" feature (rule-based access on top of/instead of item-
|
||||
// specific entitlement — course_subscription_access/required_active_tier/
|
||||
// group_restriction/item_allowlist rule types, plus the "starter-set purchase
|
||||
// eligibility" gate derived from item_allowlist) has been removed entirely,
|
||||
// admin UI and runtime enforcement both — item-specific `user_tier_grants` is
|
||||
// the only entitlement mechanism now (see hasItemGrant() in
|
||||
// controllers/client/courses.controller.js). No other table has an FK into
|
||||
// plan_policies (only plan_policies.plan_id -> tier_plans.plan_id), so this
|
||||
// is a clean, isolated drop.
|
||||
//
|
||||
// NOT auto-run — do not execute against the shared dev/prod database without
|
||||
// explicit confirmation.
|
||||
module.exports = {
|
||||
async up(queryInterface) {
|
||||
await queryInterface.dropTable('plan_policies');
|
||||
},
|
||||
|
||||
async down(queryInterface, Sequelize) {
|
||||
await queryInterface.createTable('plan_policies', {
|
||||
policy_id: { type: Sequelize.BIGINT, primaryKey: true, autoIncrement: true },
|
||||
plan_id: { type: Sequelize.BIGINT, allowNull: false, unique: true,
|
||||
references: { model: 'tier_plans', key: 'plan_id' },
|
||||
onUpdate: 'CASCADE', onDelete: 'CASCADE' },
|
||||
access_rules: { type: Sequelize.JSONB, allowNull: false, defaultValue: [] },
|
||||
createdAt: { type: Sequelize.DATE, allowNull: false },
|
||||
updatedAt: { type: Sequelize.DATE, allowNull: false },
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
await queryInterface.addColumn('tier_plans', 'status', {
|
||||
type: Sequelize.ENUM('draft', 'published'),
|
||||
allowNull: false,
|
||||
defaultValue: 'draft',
|
||||
});
|
||||
|
||||
// Preserve current live behavior — every plan that already exists is
|
||||
// visible on the client today (only is_active governs purchasability),
|
||||
// so backfill all of them to 'published'. The 'draft' default only
|
||||
// takes effect for plans created after this migration.
|
||||
await queryInterface.sequelize.query(`UPDATE "tier_plans" SET "status" = 'published'`);
|
||||
},
|
||||
|
||||
async down(queryInterface) {
|
||||
await queryInterface.removeColumn('tier_plans', 'status');
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user