diff --git a/server/src/dao/binding.dao.js b/server/src/dao/binding.dao.js index b99e56a..d93be62 100644 --- a/server/src/dao/binding.dao.js +++ b/server/src/dao/binding.dao.js @@ -95,23 +95,8 @@ async function confirmBind(userId, deviceId, bindToken) { 'UPDATE bindings SET bind_status = 1, bind_time = NOW() WHERE binding_id = ?', [rows[0].binding_id] ) - // Grant trial only if user has never had one before - const [activeSubs] = await conn.execute( - 'SELECT subscription_id FROM subscriptions WHERE user_id = ? AND status = 1 AND expire_time > NOW() LIMIT 1', - [userId] - ) - if (activeSubs.length === 0) { - const [trialHistory] = await conn.execute( - "SELECT subscription_id FROM subscriptions WHERE user_id = ? AND plan = 'trial' LIMIT 1", - [userId] - ) - if (trialHistory.length === 0) { - await conn.execute( - "INSERT INTO subscriptions (user_id, plan, status, amount, start_time, expire_time) VALUES (?, 'trial', 1, 0, NOW(), DATE_ADD(NOW(), INTERVAL 7 DAY))", - [userId] - ) - } - } + const subscriptionDao = require('./subscription.dao') + await subscriptionDao.grantTrialIfEligible(userId, conn) return true }) } @@ -142,23 +127,8 @@ async function mockBind(userId, deviceId) { "INSERT INTO bindings (user_id, device_id, bind_token, bind_expires, bind_status, bind_time) VALUES (?, ?, 'mock', NOW(), 1, NOW())", [userId, deviceId] ) - // Grant trial only if user has never had one before - const [activeSubs] = await conn.execute( - 'SELECT subscription_id FROM subscriptions WHERE user_id = ? AND status = 1 AND expire_time > NOW() LIMIT 1', - [userId] - ) - if (activeSubs.length === 0) { - const [trialHistory] = await conn.execute( - "SELECT subscription_id FROM subscriptions WHERE user_id = ? AND plan = 'trial' LIMIT 1", - [userId] - ) - if (trialHistory.length === 0) { - await conn.execute( - "INSERT INTO subscriptions (user_id, plan, status, amount, start_time, expire_time) VALUES (?, 'trial', 1, 0, NOW(), DATE_ADD(NOW(), INTERVAL 7 DAY))", - [userId] - ) - } - } + const subscriptionDao = require('./subscription.dao') + await subscriptionDao.grantTrialIfEligible(userId, conn) return { success: true } }) } diff --git a/server/src/dao/subscription.dao.js b/server/src/dao/subscription.dao.js index 1c6335c..817347e 100644 --- a/server/src/dao/subscription.dao.js +++ b/server/src/dao/subscription.dao.js @@ -63,6 +63,34 @@ async function createTrial(userId, orderId, days) { ) } +/** + * Grant trial if user is eligible (never had trial before, no active sub). + * Designed to be called within an existing transaction with a connection. + * Uses SELECT FOR UPDATE to prevent race conditions. + * @param {number} userId + * @param {Object} conn - transaction connection + * @param {number} [days] - trial days (default 7) + * @returns {Promise} true if trial was granted + */ +async function grantTrialIfEligible(userId, conn, days) { + const trialDays = Number(days) || 7 + const [activeSubs] = await conn.execute( + 'SELECT subscription_id FROM subscriptions WHERE user_id = ? AND status = 1 AND expire_time > NOW() LIMIT 1 FOR UPDATE', + [userId] + ) + if (activeSubs.length > 0) return false + const [trialHistory] = await conn.execute( + "SELECT subscription_id FROM subscriptions WHERE user_id = ? AND plan = 'trial' LIMIT 1 FOR UPDATE", + [userId] + ) + if (trialHistory.length > 0) return false + await conn.execute( + "INSERT INTO subscriptions (user_id, plan, status, amount, start_time, expire_time) VALUES (?, 'trial', 1, 0, NOW(), DATE_ADD(NOW(), INTERVAL ? DAY))", + [userId, trialDays] + ) + return true +} + /** * Purchase / activate a subscription: extend existing or create new * If user has an active subscription, add days to current expire_time. @@ -221,6 +249,7 @@ module.exports = { findTrial, findAnyActive, createTrial, + grantTrialIfEligible, purchase, adminCreate, cancel,