const db = require('../../config/db'); const ensureSchema = async () => { await db.execute(` CREATE TABLE IF NOT EXISTS invitation_flows ( id INT AUTO_INCREMENT PRIMARY KEY, document_id INT NOT NULL, owner_user_id INT NOT NULL, base_signature_id INT NOT NULL, status VARCHAR(32) NOT NULL DEFAULT 'active', current_signed_filename VARCHAR(255) NOT NULL, current_signed_path VARCHAR(500) NOT NULL, signature_style VARCHAR(32) NOT NULL DEFAULT 'qr', qr_position VARCHAR(16) NULL, qr_background VARCHAR(16) NULL, qr_border VARCHAR(16) NULL, signature_image_base64 MEDIUMTEXT NULL, total_invites INT NOT NULL DEFAULT 0, completed_invites INT NOT NULL DEFAULT 0, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_invitation_flows_document_owner (document_id, owner_user_id), INDEX idx_invitation_flows_status (status) ) `); await db.execute(` CREATE TABLE IF NOT EXISTS invitation_signers ( id INT AUTO_INCREMENT PRIMARY KEY, flow_id INT NOT NULL, owner_user_id INT NOT NULL, document_id INT NOT NULL, order_index INT NOT NULL, invitee_name VARCHAR(160) NOT NULL, invitee_email VARCHAR(255) NOT NULL, token VARCHAR(255) NOT NULL, token_expires DATETIME NOT NULL, status VARCHAR(32) NOT NULL DEFAULT 'queued', legal_accepted_at DATETIME NULL, viewed_at DATETIME NULL, sent_at DATETIME NULL, signed_at DATETIME NULL, signed_filename VARCHAR(255) NULL, signed_path VARCHAR(500) NULL, certificate_subject_cn VARCHAR(255) NULL, certificate_issuer_cn VARCHAR(255) NULL, verification_code VARCHAR(100) NULL, verification_url VARCHAR(500) NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uq_invitation_signers_token (token), UNIQUE KEY uq_invitation_signers_flow_order (flow_id, order_index), INDEX idx_invitation_signers_document_owner (document_id, owner_user_id), INDEX idx_invitation_signers_status (status) ) `); }; const createFlow = async ({ documentId, ownerUserId, baseSignatureId, currentSignedFilename, currentSignedPath, signatureStyle, qrPosition, qrBackground, qrBorder, signatureImageBase64, totalInvites, }) => { const [result] = await db.execute( `INSERT INTO invitation_flows (document_id, owner_user_id, base_signature_id, current_signed_filename, current_signed_path, signature_style, qr_position, qr_background, qr_border, signature_image_base64, total_invites) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [ documentId, ownerUserId, baseSignatureId, currentSignedFilename, currentSignedPath, signatureStyle, qrPosition || null, qrBackground || null, qrBorder || null, signatureImageBase64 || null, totalInvites, ] ); return result.insertId; }; const createInvitation = async ({ flowId, ownerUserId, documentId, orderIndex, inviteeName, inviteeEmail, token, tokenExpires, status, }) => { const [result] = await db.execute( `INSERT INTO invitation_signers (flow_id, owner_user_id, document_id, order_index, invitee_name, invitee_email, token, token_expires, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [flowId, ownerUserId, documentId, orderIndex, inviteeName, inviteeEmail, token, tokenExpires, status] ); return result.insertId; }; const findFlowByDocumentOwner = async (documentId, ownerUserId) => { const [rows] = await db.execute( 'SELECT * FROM invitation_flows WHERE document_id = ? AND owner_user_id = ? ORDER BY id DESC LIMIT 1', [documentId, ownerUserId] ); return rows[0] || null; }; const findFlowById = async (id) => { const [rows] = await db.execute('SELECT * FROM invitation_flows WHERE id = ?', [id]); return rows[0] || null; }; const listInvitationsByFlow = async (flowId) => { const [rows] = await db.execute( 'SELECT * FROM invitation_signers WHERE flow_id = ? ORDER BY order_index ASC', [flowId] ); return rows; }; const getMaxOrderIndex = async (flowId) => { const [rows] = await db.execute( `SELECT COALESCE(MAX(order_index), 0) AS maxOrder FROM invitation_signers WHERE flow_id = ?`, [flowId] ); return Number(rows[0]?.maxOrder || 0); }; const findInvitationByIdForOwner = async (id, ownerUserId) => { const [rows] = await db.execute( `SELECT i.*, f.status as flow_status, f.current_signed_filename, f.current_signed_path, f.signature_style, f.qr_position, f.qr_background, f.qr_border, f.signature_image_base64, f.base_signature_id, f.total_invites, f.completed_invites, d.title as document_title, d.filename as document_filename, u.name as owner_name, u.email as owner_email FROM invitation_signers i JOIN invitation_flows f ON f.id = i.flow_id JOIN documents d ON d.id = i.document_id JOIN users u ON u.id = i.owner_user_id WHERE i.id = ? AND i.owner_user_id = ? LIMIT 1`, [id, ownerUserId] ); return rows[0] || null; }; const findInvitationByToken = async (token) => { const [rows] = await db.execute( `SELECT i.*, f.status as flow_status, f.current_signed_filename, f.current_signed_path, f.signature_style, f.qr_position, f.qr_background, f.qr_border, f.signature_image_base64, f.base_signature_id, f.total_invites, f.completed_invites, d.title as document_title, d.filename as document_filename, u.name as owner_name, u.email as owner_email FROM invitation_signers i JOIN invitation_flows f ON f.id = i.flow_id JOIN documents d ON d.id = i.document_id JOIN users u ON u.id = i.owner_user_id WHERE i.token = ? LIMIT 1`, [token] ); return rows[0] || null; }; const markInvitationSent = async (id) => { await db.execute( `UPDATE invitation_signers SET status = 'sent', sent_at = COALESCE(sent_at, NOW()) WHERE id = ?`, [id] ); }; const markInvitationViewed = async (id) => { await db.execute( `UPDATE invitation_signers SET status = CASE WHEN status = 'sent' THEN 'opened' ELSE status END, viewed_at = COALESCE(viewed_at, NOW()) WHERE id = ?`, [id] ); }; const markInvitationTermsAccepted = async (id) => { await db.execute( `UPDATE invitation_signers SET legal_accepted_at = COALESCE(legal_accepted_at, NOW()) WHERE id = ?`, [id] ); }; const markInvitationSigned = async ({ id, signedFilename, signedPath, certificateSubjectCn, certificateIssuerCn, verificationCode, verificationUrl, }) => { await db.execute( `UPDATE invitation_signers SET status = 'signed', signed_at = NOW(), signed_filename = ?, signed_path = ?, certificate_subject_cn = ?, certificate_issuer_cn = ?, verification_code = ?, verification_url = ? WHERE id = ?`, [signedFilename, signedPath, certificateSubjectCn || null, certificateIssuerCn || null, verificationCode || null, verificationUrl || null, id] ); }; const invalidateInvitationToken = async (id, replacementToken) => { await db.execute( `UPDATE invitation_signers SET token = ?, token_expires = NOW() WHERE id = ?`, [replacementToken, id] ); }; const activateInvitation = async (id) => { await db.execute( `UPDATE invitation_signers SET status = 'sent', sent_at = COALESCE(sent_at, NOW()) WHERE id = ?`, [id] ); }; const updateFlowProgress = async ({ flowId, currentSignedFilename, currentSignedPath, completedInvites, status }) => { await db.execute( `UPDATE invitation_flows SET current_signed_filename = ?, current_signed_path = ?, completed_invites = ?, status = ? WHERE id = ?`, [currentSignedFilename, currentSignedPath, completedInvites, status, flowId] ); }; const findNextInvitation = async (flowId) => { const [rows] = await db.execute( `SELECT * FROM invitation_signers WHERE flow_id = ? AND status = 'queued' ORDER BY order_index ASC LIMIT 1`, [flowId] ); return rows[0] || null; }; const countSignedInvitations = async (flowId) => { const [rows] = await db.execute( `SELECT COUNT(*) AS total FROM invitation_signers WHERE flow_id = ? AND status = 'signed'`, [flowId] ); return Number(rows[0]?.total || 0); }; const countInvitations = async (flowId) => { const [rows] = await db.execute( `SELECT COUNT(*) AS total FROM invitation_signers WHERE flow_id = ?`, [flowId] ); return Number(rows[0]?.total || 0); }; const updateFlowCounts = async ({ flowId, totalInvites, completedInvites, status }) => { await db.execute( `UPDATE invitation_flows SET total_invites = ?, completed_invites = ?, status = ? WHERE id = ?`, [totalInvites, completedInvites, status, flowId] ); }; const removeInvitation = async (id) => { await db.execute('DELETE FROM invitation_signers WHERE id = ?', [id]); }; module.exports = { ensureSchema, createFlow, createInvitation, findFlowByDocumentOwner, findFlowById, listInvitationsByFlow, getMaxOrderIndex, findInvitationByIdForOwner, findInvitationByToken, markInvitationSent, markInvitationViewed, markInvitationTermsAccepted, markInvitationSigned, invalidateInvitationToken, activateInvitation, updateFlowProgress, findNextInvitation, countSignedInvitations, countInvitations, updateFlowCounts, removeInvitation, };