f920ca8920
- Added models for support messages, SLA policies, tickets, ticket events, and ticket links. - Created routes for help, help admin, newsletter, recommendations, and support for both customer and admin. - Developed services for help, marketing (newsletter), and recommendations. - Introduced support service for ticket management, including creation, replies, transitions, and attachments. - Added validation schemas for support recommendations and ticket management. - Implemented a cron job for support reconciliation and recommendation cleanup. - Created migration for new support and recommendation database tables. - Added unit tests for validation and policy checks related to Phase 10 features.
17 lines
6.1 KiB
JavaScript
17 lines
6.1 KiB
JavaScript
"use strict";module.exports={async up(q,S){const ID={type:S.STRING,primaryKey:true,allowNull:false},REQ={type:S.STRING,allowNull:false},STR={type:S.STRING},DATE={type:S.DATE},INT={type:S.INTEGER,allowNull:false,defaultValue:0},TS={createdAt:{type:S.DATE,allowNull:false},updatedAt:{type:S.DATE,allowNull:false}};
|
|
await q.createTable("support_categories",{id:ID,code:{type:S.STRING(60),allowNull:false,unique:true},status:REQ,translations:{type:S.JSON,allowNull:false},sort_order:INT,...TS});
|
|
await q.createTable("support_sla_policies",{id:ID,name:REQ,status:REQ,priority:{type:S.STRING,allowNull:false,unique:true},first_response_minutes:{type:S.INTEGER,allowNull:false},resolution_minutes:{type:S.INTEGER,allowNull:false},business_hours_mode:REQ,...TS});
|
|
await q.createTable("support_tickets",{id:ID,ticket_number:{type:S.STRING(80),allowNull:false,unique:true},customer_user_id:REQ,business_customer_id:STR,subject:{type:S.STRING(200),allowNull:false},category_id:STR,priority:REQ,status:REQ,assigned_agent_id:STR,source:REQ,sla_policy_id:STR,first_response_due_at:DATE,resolution_due_at:DATE,first_response_at:DATE,resolved_at:DATE,closed_at:DATE,last_customer_message_at:DATE,last_agent_message_at:DATE,...TS});await q.addIndex("support_tickets",["customer_user_id","createdAt"]);await q.addIndex("support_tickets",["business_customer_id","createdAt"]);await q.addIndex("support_tickets",["status","priority"]);await q.addIndex("support_tickets",["assigned_agent_id","status"]);await q.addIndex("support_tickets",["first_response_due_at","resolution_due_at"]);
|
|
await q.createTable("support_messages",{id:ID,ticket_id:{...REQ,references:{model:"support_tickets",key:"id"}},sender_user_id:STR,sender_type:REQ,message_type:REQ,body:{type:S.TEXT,allowNull:false},visibility:REQ,createdAt:{type:S.DATE,allowNull:false}});await q.addIndex("support_messages",["ticket_id","createdAt"]);
|
|
await q.createTable("support_ticket_events",{id:ID,event_id:{type:S.STRING(180),allowNull:false,unique:true},ticket_id:{...REQ,references:{model:"support_tickets",key:"id"}},actor_user_id:STR,type:REQ,from_value:STR,to_value:STR,metadata:{type:S.JSON},occurred_at:{type:S.DATE,allowNull:false},createdAt:{type:S.DATE,allowNull:false}});await q.addIndex("support_ticket_events",["ticket_id","occurred_at"]);
|
|
await q.createTable("support_ticket_links",{id:ID,ticket_id:{...REQ,references:{model:"support_tickets",key:"id"}},resource_type:REQ,resource_id:REQ,createdAt:{type:S.DATE,allowNull:false}});await q.addConstraint("support_ticket_links",{fields:["ticket_id","resource_type","resource_id"],type:"unique",name:"uq_support_ticket_resource"});await q.addIndex("support_ticket_links",["resource_type","resource_id"]);
|
|
await q.createTable("support_attachments",{id:ID,ticket_id:{...REQ,references:{model:"support_tickets",key:"id"}},message_id:STR,upload_id:{type:S.INTEGER,allowNull:false,references:{model:"uploads",key:"id"}},uploaded_by:REQ,visibility:REQ,createdAt:{type:S.DATE,allowNull:false}});await q.addIndex("support_attachments",["ticket_id","visibility"]);
|
|
await q.createTable("support_escalations",{id:ID,event_id:{type:S.STRING(180),allowNull:false,unique:true},ticket_id:{...REQ,references:{model:"support_tickets",key:"id"}},type:REQ,level:{type:S.INTEGER,allowNull:false},reason:{type:S.STRING(300),allowNull:false},status:REQ,acknowledged_at:DATE,...TS});await q.addIndex("support_escalations",["ticket_id","status"]);
|
|
await q.createTable("help_categories",{id:ID,slug:{type:S.STRING(160),allowNull:false,unique:true},status:REQ,sort_order:INT,icon_key:{type:S.STRING(80)},...TS});await q.addIndex("help_categories",["status","sort_order"]);
|
|
await q.createTable("help_category_translations",{id:ID,category_id:{...REQ,references:{model:"help_categories",key:"id"}},locale:{type:S.STRING(5),allowNull:false},name:{type:S.STRING(160),allowNull:false},description:{type:S.STRING(500)},...TS});await q.addConstraint("help_category_translations",{fields:["category_id","locale"],type:"unique",name:"uq_help_category_locale"});await q.addIndex("help_category_translations",["locale"]);
|
|
await q.createTable("help_articles",{id:ID,slug:{type:S.STRING(180),allowNull:false,unique:true},category_id:{...REQ,references:{model:"help_categories",key:"id"}},article_type:REQ,status:REQ,sort_order:INT,is_featured:{type:S.BOOLEAN,allowNull:false,defaultValue:false},published_at:DATE,created_by:REQ,updated_by:STR,...TS});await q.addIndex("help_articles",["category_id","status","published_at"]);
|
|
await q.createTable("help_article_translations",{id:ID,article_id:{...REQ,references:{model:"help_articles",key:"id"}},locale:{type:S.STRING(5),allowNull:false},title:{type:S.STRING(220),allowNull:false},summary:{type:S.STRING(500)},body:{type:S.TEXT("long"),allowNull:false},seo_title:{type:S.STRING(220)},seo_description:{type:S.STRING(500)},...TS});await q.addConstraint("help_article_translations",{fields:["article_id","locale"],type:"unique",name:"uq_help_article_locale"});await q.addIndex("help_article_translations",["locale"]);
|
|
await q.createTable("newsletter_subscriptions",{id:ID,email_normalized:{type:S.STRING(254),allowNull:false,unique:true},user_id:STR,status:REQ,locale:{type:S.STRING(5),allowNull:false},source:REQ,consented_at:DATE,confirmed_at:DATE,unsubscribed_at:DATE,unsubscribe_token_hash:{type:S.STRING(64),allowNull:false},...TS});await q.addIndex("newsletter_subscriptions",["status","email_normalized"]);
|
|
await q.createTable("recommendation_events",{id:ID,event_id:{type:S.STRING(180),allowNull:false,unique:true},user_id:STR,session_key_hash:{type:S.STRING(64)},product_id:{...REQ,references:{model:"products",key:"id"}},variant_id:STR,event_type:REQ,source:{type:S.STRING(40),allowNull:false},occurred_at:{type:S.DATE,allowNull:false},createdAt:{type:S.DATE,allowNull:false}});await q.addIndex("recommendation_events",["user_id","occurred_at"]);await q.addIndex("recommendation_events",["product_id","occurred_at"]);await q.addIndex("recommendation_events",["event_type","occurred_at"]);await q.addIndex("recommendation_events",["product_id","event_type","occurred_at"]);
|
|
},async down(){throw new Error("Phase 10 migration is forward-only; restore from a verified backup instead");}};
|