576 lines
21 KiB
JavaScript
576 lines
21 KiB
JavaScript
const ReferralTokenRepository = require('../../repositories/referral/ReferralTokenRepository');
|
|
const PersonalUserRepository = require('../../repositories/user/personal/PersonalUserRepository');
|
|
const CompanyUserRepository = require('../../repositories/user/company/CompanyUserRepository');
|
|
const UserStatusService = require('../status/UserStatusService');
|
|
const UserSettingsRepository = require('../../repositories/settings/UserSettingsRepository');
|
|
const MailService = require('../email/MailService');
|
|
const { logger } = require('../../middleware/logger');
|
|
|
|
class ReferralService {
|
|
// --- Token creation and management ---
|
|
static async createReferralToken({ userId, expiresInDays, maxUses, unitOfWork }) {
|
|
logger.info('ReferralService:createReferralToken:start', { userId, expiresInDays, maxUses });
|
|
|
|
const normalizeUnlimited = (val, label) => {
|
|
if (val === undefined || val === null || val === '') return -1;
|
|
if (typeof val === 'string') {
|
|
const trimmed = val.trim().toLowerCase();
|
|
if (['unlimited','∞','-1'].includes(trimmed)) return -1;
|
|
const num = Number(val);
|
|
if (!Number.isNaN(num)) val = num;
|
|
}
|
|
// Treat 0 as unlimited (frontend currently sends 0 for unlimited)
|
|
if (val === 0) {
|
|
logger.debug(`ReferralService:createReferralToken:interpreting_0_as_unlimited`, { field: label });
|
|
return -1;
|
|
}
|
|
return val;
|
|
};
|
|
|
|
expiresInDays = normalizeUnlimited(expiresInDays, 'expiresInDays');
|
|
maxUses = normalizeUnlimited(maxUses, 'maxUses');
|
|
|
|
if (!(expiresInDays === -1 || (Number.isInteger(expiresInDays) && expiresInDays >= 1 && expiresInDays <= 7))) {
|
|
throw new Error('Expiry must be between 1 and 7 days or unlimited');
|
|
}
|
|
if (!(maxUses === -1 || (Number.isInteger(maxUses) && maxUses >= 1))) {
|
|
throw new Error('maxUses must be a positive integer or unlimited');
|
|
}
|
|
|
|
const unlimited = maxUses === -1;
|
|
if (unlimited) logger.info('ReferralService:createReferralToken:unlimited', { userId });
|
|
|
|
const UNLIMITED_EXPIRY_DATE = new Date('2999-12-31T23:59:59Z');
|
|
const expiresAt = (expiresInDays === -1)
|
|
? UNLIMITED_EXPIRY_DATE
|
|
: new Date(Date.now() + expiresInDays * 24 * 60 * 60 * 1000);
|
|
|
|
const repo = new ReferralTokenRepository(unitOfWork);
|
|
const token = await repo.createToken({
|
|
createdByUserId: userId,
|
|
expiresAt,
|
|
maxUses
|
|
});
|
|
|
|
logger.info('ReferralService:createReferralToken:success', {
|
|
token: token.token,
|
|
unlimited,
|
|
storedMaxUses: token.maxUses,
|
|
storedUsesRemaining: token.usesRemaining
|
|
});
|
|
|
|
return this.presentToken({
|
|
id: token.id,
|
|
token: token.token,
|
|
created_by_user_id: token.createdByUserId,
|
|
expires_at: token.expiresAt,
|
|
status: token.status,
|
|
max_uses: token.maxUses,
|
|
uses_remaining: token.usesRemaining
|
|
});
|
|
}
|
|
|
|
static async getUserReferralTokens(userId, unitOfWork) {
|
|
logger.info('ReferralService:getUserReferralTokens:start', { userId });
|
|
const repo = new ReferralTokenRepository(unitOfWork);
|
|
const rows = await repo.getTokensByUser(userId);
|
|
|
|
const base = process.env.REFERRAL_PUBLIC_BASE_URL || 'https://profit-planet.partners/register?ref=';
|
|
|
|
const tokens = rows.map(r => {
|
|
const max = r.max_uses;
|
|
const remaining = r.uses_remaining;
|
|
const isUnlimited = (max === -1) || (remaining === -1);
|
|
let used;
|
|
|
|
if (isUnlimited) {
|
|
used = 0;
|
|
} else if (typeof r.used_count === 'number') {
|
|
used = r.used_count;
|
|
} else if (Number.isFinite(max) && Number.isFinite(remaining)) {
|
|
used = max - remaining;
|
|
} else {
|
|
used = 0;
|
|
}
|
|
|
|
if (!Number.isFinite(used) || used < 0) used = 0;
|
|
if (!isUnlimited && used > max) used = max;
|
|
|
|
const maxUsesCanonical = isUnlimited ? null : max;
|
|
const usage = isUnlimited ? 'unlimited' : `${used}/${maxUsesCanonical}`;
|
|
|
|
return {
|
|
id: r.id,
|
|
token: r.token,
|
|
link: `${base}${r.token}`,
|
|
status: r.status,
|
|
created: r.created_at ? new Date(r.created_at).toISOString() : null,
|
|
expires: isUnlimited ? null : (r.expires_at ? new Date(r.expires_at).toISOString() : null),
|
|
used,
|
|
maxUses: maxUsesCanonical,
|
|
isUnlimited,
|
|
usage,
|
|
progress: usage,
|
|
// Aliases (defensive)
|
|
useCount: used,
|
|
usedCount: used
|
|
};
|
|
});
|
|
|
|
tokens.slice(0, 5).forEach(t => {
|
|
logger.debug('ReferralService:getUserReferralTokens:canonical', {
|
|
id: t.id, usage: t.usage, used: t.used, maxUses: t.maxUses, isUnlimited: t.isUnlimited
|
|
});
|
|
});
|
|
|
|
logger.info('ReferralService:getUserReferralTokens:success', { count: tokens.length });
|
|
return tokens;
|
|
}
|
|
|
|
static async getUserReferralStats(userId, unitOfWork) {
|
|
logger.info('ReferralService:getUserReferralStats:start', { userId });
|
|
const repo = new ReferralTokenRepository(unitOfWork);
|
|
const stats = await repo.getStatsByUser(userId);
|
|
logger.info('ReferralService:getUserReferralStats:success', { stats });
|
|
// Pass through all fields including new ones
|
|
return stats;
|
|
}
|
|
|
|
static async deactivateReferralToken(userId, tokenId, unitOfWork) {
|
|
logger.info('ReferralService:deactivateReferralToken:start', { userId, tokenId });
|
|
const repo = new ReferralTokenRepository(unitOfWork);
|
|
await repo.deactivateToken(tokenId, userId);
|
|
logger.info('ReferralService:deactivateReferralToken:success', { userId, tokenId });
|
|
}
|
|
|
|
static async countActiveReferralTokens(userId, unitOfWork) {
|
|
logger.info('ReferralService:countActiveReferralTokens:start', { userId });
|
|
const repo = new ReferralTokenRepository(unitOfWork);
|
|
const count = await repo.countActiveTokensByUser(userId);
|
|
logger.info('ReferralService:countActiveReferralTokens:success', { userId, count });
|
|
return count;
|
|
}
|
|
|
|
static presentToken(row) {
|
|
if (!row) return row;
|
|
|
|
// Raw DB fields
|
|
const maxNum = (row.max_uses !== undefined ? row.max_uses : row.maxUses);
|
|
const remNum = (row.uses_remaining !== undefined ? row.uses_remaining : row.usesRemaining);
|
|
const createdRaw = row.created_at || row.createdAt;
|
|
const expiresRaw = row.expires_at || row.expiresAt;
|
|
|
|
const createdDate = createdRaw ? new Date(createdRaw) : null;
|
|
const createdValid = createdDate && !isNaN(createdDate.getTime());
|
|
const createdISO = createdValid ? createdDate.toISOString() : null;
|
|
|
|
const expiresDate = expiresRaw ? new Date(expiresRaw) : null;
|
|
const expiresValid = expiresDate && !isNaN(expiresDate.getTime());
|
|
|
|
const unlimited = (maxNum === -1) || (remNum === -1);
|
|
const maxDisplay = row.max_uses_display || row.max_uses_label || (unlimited ? 'unlimited' : String(maxNum));
|
|
const remDisplay = row.uses_remaining_display || row.uses_remaining_label || (unlimited ? 'unlimited' : String(remNum));
|
|
|
|
// Numeric total & used
|
|
let totalCount = unlimited ? -1 : (
|
|
typeof row.total_count === 'number'
|
|
? row.total_count
|
|
: (Number.isFinite(maxNum) ? maxNum : 0)
|
|
);
|
|
|
|
let usedCount = unlimited ? 0 : (
|
|
typeof row.used_count === 'number'
|
|
? row.used_count
|
|
: (Number.isFinite(maxNum) && Number.isFinite(remNum) ? (maxNum - remNum) : 0)
|
|
);
|
|
|
|
if (!unlimited) {
|
|
if (!Number.isFinite(usedCount) || usedCount < 0) usedCount = 0;
|
|
if (Number.isFinite(totalCount) && usedCount > totalCount) usedCount = totalCount;
|
|
}
|
|
|
|
const remainingCount = unlimited
|
|
? -1
|
|
: (Number.isFinite(remNum)
|
|
? remNum
|
|
: (Number.isFinite(totalCount) ? (totalCount - usedCount) : 0));
|
|
|
|
// Build usage string deterministically (do NOT trust usage_display if present)
|
|
const usageString = unlimited
|
|
? 'unlimited'
|
|
: (totalCount > 0 ? `${usedCount}/${totalCount}` : '0/0');
|
|
|
|
const infinitySymbol = '∞';
|
|
const denominatorDisplay = unlimited ? infinitySymbol : String(totalCount);
|
|
const numeratorDisplay = unlimited ? '0' : String(usedCount);
|
|
|
|
// Expiry display (Never for unlimited OR far future year >= 2999)
|
|
const expiresAtISO = unlimited
|
|
? null
|
|
: (expiresValid ? expiresDate.toISOString() : null);
|
|
const expiresAtDisplay = unlimited
|
|
? 'Never'
|
|
: (expiresValid ? expiresDate.toISOString() : 'Unknown');
|
|
|
|
// Numeric maxUses for FE (for unlimited keep -1 sentinel)
|
|
const maxUsesNumber = unlimited ? -1 : (Number.isFinite(totalCount) ? totalCount : 0);
|
|
|
|
// NEW: derive referrer identity (email & name) early so it can be inserted into tokenObj
|
|
const referrerEmailDerived =
|
|
row.referrer_email ||
|
|
row.referrerEmail ||
|
|
row.email ||
|
|
row.created_by_email ||
|
|
row.createdByEmail ||
|
|
null;
|
|
|
|
// Try to build a display name if first/last name fields exist (silent if not)
|
|
const refFirst = row.referrer_first_name || row.first_name || row.firstName;
|
|
const refLast = row.referrer_last_name || row.last_name || row.lastName;
|
|
let referrerNameDerived = row.referrer_name || row.referrerName || null;
|
|
if (!referrerNameDerived && (refFirst || refLast)) {
|
|
referrerNameDerived = [refFirst, refLast].filter(Boolean).join(' ').trim() || null;
|
|
}
|
|
if (!referrerNameDerived) {
|
|
// fallback to email for display if no name
|
|
referrerNameDerived = referrerEmailDerived || null;
|
|
}
|
|
|
|
const tokenObj = {
|
|
id: row.id,
|
|
token: row.token,
|
|
status: row.status,
|
|
|
|
// Core time fields
|
|
createdAt: createdISO,
|
|
created_at: createdISO,
|
|
createdAtISO: createdISO,
|
|
createdAtEpoch: createdValid ? createdDate.getTime() : null,
|
|
|
|
expiresAt: expiresAtISO,
|
|
expires_at: expiresAtISO,
|
|
expiresAtISO,
|
|
expiresAtEpoch: (expiresAtISO ? expiresDate.getTime() : null),
|
|
expiresAtDisplay,
|
|
|
|
// Display & numeric usage
|
|
usage: usageString,
|
|
usageDisplay: usageString,
|
|
progress: usageString,
|
|
|
|
// Display values
|
|
maxUsesDisplay: unlimited ? 'unlimited' : String(maxUsesNumber),
|
|
usesRemainingDisplay: unlimited ? 'unlimited' : String(remainingCount),
|
|
|
|
// Primary numeric values (camelCase)
|
|
maxUses: maxUsesNumber,
|
|
usesRemaining: remainingCount,
|
|
used: usedCount,
|
|
isUnlimited: unlimited,
|
|
|
|
// Raw helpers
|
|
maxUsesRaw: maxUsesNumber,
|
|
usesRemainingRaw: remainingCount,
|
|
usesUsedRaw: usedCount,
|
|
|
|
// Additional alias set to satisfy unknown FE expectations
|
|
limit: maxUsesNumber,
|
|
total: maxUsesNumber,
|
|
totalCount: maxUsesNumber,
|
|
max: maxUsesNumber,
|
|
capacity: maxUsesNumber,
|
|
remaining: remainingCount,
|
|
remainingCount,
|
|
left: remainingCount,
|
|
usedCount,
|
|
currentUses: usedCount,
|
|
progressUsed: usedCount,
|
|
totalUses: maxUsesNumber,
|
|
remainingUses: remainingCount,
|
|
usedUses: usedCount,
|
|
|
|
// Snake_case (legacy)
|
|
max_uses: maxUsesNumber,
|
|
uses_remaining: remainingCount,
|
|
|
|
// Displays for numerator/denominator parts
|
|
numeratorDisplay,
|
|
denominatorDisplay,
|
|
infinitySymbol,
|
|
|
|
// Structured usage object
|
|
usageData: {
|
|
used: usedCount,
|
|
remaining: remainingCount,
|
|
max: maxUsesNumber,
|
|
limit: maxUsesNumber,
|
|
total: maxUsesNumber,
|
|
isUnlimited: unlimited,
|
|
display: usageString,
|
|
numerator: numeratorDisplay,
|
|
denominator: denominatorDisplay
|
|
},
|
|
|
|
// Keep original creation if present (for FE that lists columns generically)
|
|
created_by_user_id: row.created_by_user_id || row.createdByUserId,
|
|
createdByUserId: row.created_by_user_id || row.createdByUserId,
|
|
|
|
// Count of individual usage rows
|
|
usageCount: row.usage_count,
|
|
|
|
// NEW: referrer identity fields (added near end to avoid interfering with existing logic)
|
|
referrerEmail: referrerEmailDerived,
|
|
referrerName: referrerNameDerived,
|
|
// legacy / alias fields some FE might probe
|
|
ownerEmail: referrerEmailDerived,
|
|
createdByEmail: referrerEmailDerived,
|
|
userEmail: referrerEmailDerived
|
|
};
|
|
|
|
if (!tokenObj.referrerEmail) {
|
|
logger.debug('ReferralService:presentToken:referrer_email_missing', { tokenId: row.id, hasRawField: !!row.referrer_email });
|
|
}
|
|
|
|
return tokenObj;
|
|
}
|
|
|
|
// --- Registration via referral ---
|
|
static evaluateTokenRecord(rec) {
|
|
if (!rec) return { valid: false, reason: 'not_found' };
|
|
|
|
// Defensive normalization
|
|
const rawMax = (rec.max_uses === null || rec.max_uses === undefined) ? -1 : rec.max_uses;
|
|
const rawRemaining = (rec.uses_remaining === null || rec.uses_remaining === undefined) ? -1 : rec.uses_remaining;
|
|
|
|
const isUnlimited = (rawMax === -1) || (rawRemaining === -1);
|
|
|
|
// Diagnostics
|
|
logger.debug('ReferralService:evaluateTokenRecord:raw', {
|
|
status: rec.status,
|
|
expires_at: rec.expires_at,
|
|
max_uses: rawMax,
|
|
uses_remaining: rawRemaining,
|
|
isUnlimited
|
|
});
|
|
|
|
if (rec.status !== 'active') return { valid: false, reason: 'inactive' };
|
|
|
|
const now = Date.now();
|
|
const expMs = rec.expires_at ? new Date(rec.expires_at).getTime() : NaN;
|
|
|
|
if (!isUnlimited) {
|
|
if (isNaN(expMs) || expMs <= now) return { valid: false, reason: 'expired' };
|
|
if (rawRemaining === 0) return { valid: false, reason: 'exhausted' };
|
|
if (rawRemaining < 0) {
|
|
// Inconsistent negative (not -1 sentinel) -> treat as internal issue; allow but log
|
|
logger.warn('ReferralService:evaluateTokenRecord:negative_remaining_non_unlimited', {
|
|
uses_remaining: rawRemaining, max_uses: rawMax
|
|
});
|
|
}
|
|
}
|
|
|
|
return {
|
|
valid: true,
|
|
reason: null,
|
|
isUnlimited,
|
|
usesRemaining: isUnlimited ? null : rawRemaining
|
|
};
|
|
}
|
|
|
|
static async getReferrerInfo(token, unitOfWork) {
|
|
logger.info('ReferralService:getReferrerInfo:start', { token });
|
|
const repo = new ReferralTokenRepository(unitOfWork);
|
|
const raw = await repo.getReferrerInfoByToken(token);
|
|
if (!raw) {
|
|
logger.warn('ReferralService:getReferrerInfo:not_found', { token });
|
|
return { valid: false, reason: 'not_found' };
|
|
}
|
|
const evalResult = this.evaluateTokenRecord(raw);
|
|
if (!evalResult.valid) {
|
|
logger.warn('ReferralService:getReferrerInfo:invalid', {
|
|
token, reason: evalResult.reason, max_uses: raw.max_uses, uses_remaining: raw.uses_remaining
|
|
});
|
|
return { valid: false, reason: evalResult.reason };
|
|
}
|
|
|
|
const normalized = this.presentToken(raw);
|
|
|
|
const refEmail = normalized.referrerEmail || raw.referrer_email || raw.email || null;
|
|
const refName = normalized.referrerName || refEmail;
|
|
|
|
if (!refEmail) {
|
|
logger.warn('ReferralService:getReferrerInfo:referrer_email_unresolved', { token, tokenId: raw.id });
|
|
} else {
|
|
logger.debug('ReferralService:getReferrerInfo:referrer_email_resolved', { token, refEmail });
|
|
}
|
|
|
|
return {
|
|
valid: true,
|
|
referrerId: raw.referrer_id || normalized.referrerId || null,
|
|
referrerName: refName,
|
|
referrerEmail: refEmail,
|
|
isUnlimited: normalized.isUnlimited,
|
|
usesRemaining: normalized.isUnlimited ? -1 : normalized.usesRemaining,
|
|
maxUses: normalized.maxUses,
|
|
usage: normalized.usage
|
|
};
|
|
}
|
|
|
|
static async registerPersonalWithReferral(registrationData, refToken, unitOfWork) {
|
|
logger.info('ReferralService:registerPersonalWithReferral:start', { refToken });
|
|
const repo = new ReferralTokenRepository(unitOfWork);
|
|
const raw = await repo.getReferrerInfoByToken(refToken);
|
|
const evalResult = this.evaluateTokenRecord(raw);
|
|
if (!evalResult.valid) {
|
|
logger.warn('ReferralService:registerPersonalWithReferral:token_invalid', {
|
|
refToken, reason: evalResult.reason, max_uses: raw && raw.max_uses, uses_remaining: raw && raw.uses_remaining
|
|
});
|
|
throw new Error(evalResult.reason || 'invalid_token');
|
|
}
|
|
|
|
const personalRepo = new PersonalUserRepository(unitOfWork);
|
|
const user = await personalRepo.create(registrationData);
|
|
|
|
await UserStatusService.initializeUserStatus(user.id, 'personal', unitOfWork, 'inactive');
|
|
|
|
if (UserSettingsRepository) {
|
|
const settingsRepo = new UserSettingsRepository(unitOfWork);
|
|
await settingsRepo.createDefaultSettings(user.id, unitOfWork);
|
|
} else if (unitOfWork.connection) {
|
|
await unitOfWork.connection.query(`
|
|
INSERT INTO user_settings (user_id) VALUES (?)
|
|
ON DUPLICATE KEY UPDATE user_id = user_id
|
|
`, [user.id]);
|
|
}
|
|
|
|
await repo.markReferralTokenUsed(raw.token_id, user.id, unitOfWork);
|
|
|
|
await MailService.sendRegistrationEmail({
|
|
email: registrationData.email,
|
|
firstName: registrationData.firstName,
|
|
lastName: registrationData.lastName,
|
|
userType: 'personal'
|
|
});
|
|
|
|
logger.info('ReferralService:registerPersonalWithReferral:success', { userId: user.id, email: user.email });
|
|
return user;
|
|
}
|
|
|
|
static async registerCompanyWithReferral(registrationData, refToken, unitOfWork) {
|
|
logger.info('ReferralService:registerCompanyWithReferral:start', { refToken });
|
|
const repo = new ReferralTokenRepository(unitOfWork);
|
|
const raw = await repo.getReferrerInfoByToken(refToken);
|
|
const evalResult = this.evaluateTokenRecord(raw);
|
|
if (!evalResult.valid) {
|
|
logger.warn('ReferralService:registerCompanyWithReferral:token_invalid', {
|
|
refToken, reason: evalResult.reason, max_uses: raw && raw.max_uses, uses_remaining: raw && raw.uses_remaining
|
|
});
|
|
throw new Error(evalResult.reason || 'invalid_token');
|
|
}
|
|
|
|
const companyRepo = new CompanyUserRepository(unitOfWork);
|
|
const { companyEmail, password, companyName, companyPhone, contactPersonName, contactPersonPhone } = registrationData;
|
|
const user = await companyRepo.create({
|
|
companyEmail,
|
|
password,
|
|
companyName,
|
|
companyPhone,
|
|
contactPersonName,
|
|
contactPersonPhone
|
|
});
|
|
|
|
await UserStatusService.initializeUserStatus(user.id, 'company', unitOfWork, 'inactive');
|
|
|
|
if (UserSettingsRepository) {
|
|
const settingsRepo = new UserSettingsRepository(unitOfWork);
|
|
await settingsRepo.createDefaultSettings(user.id, unitOfWork);
|
|
} else if (unitOfWork.connection) {
|
|
await unitOfWork.connection.query(`
|
|
INSERT INTO user_settings (user_id) VALUES (?)
|
|
ON DUPLICATE KEY UPDATE user_id = user_id
|
|
`, [user.id]);
|
|
}
|
|
|
|
await repo.markReferralTokenUsed(raw.token_id, user.id, unitOfWork);
|
|
|
|
await MailService.sendRegistrationEmail({
|
|
email: registrationData.companyEmail,
|
|
companyName: registrationData.companyName,
|
|
userType: 'company'
|
|
});
|
|
|
|
logger.info('ReferralService:registerCompanyWithReferral:success', { userId: user.id, email: user.email });
|
|
return user;
|
|
}
|
|
|
|
// --- Referral usage processing ---
|
|
static async processReferral(userId, referralEmail, unitOfWork) {
|
|
logger.info('ReferralService:processReferral:start', { userId, referralEmail });
|
|
try {
|
|
const referrer = await this.findReferrerByEmail(referralEmail, unitOfWork);
|
|
if (!referrer) {
|
|
logger.warn('ReferralService:processReferral:referrer_not_found', { referralEmail });
|
|
return false;
|
|
}
|
|
|
|
const referralToken = await this.findActiveReferralToken(referrer.id, unitOfWork);
|
|
if (!referralToken) {
|
|
logger.warn('ReferralService:processReferral:no_active_token', { referrerId: referrer.id });
|
|
return false;
|
|
}
|
|
|
|
await this.createReferralUsage(referralToken.id, userId, unitOfWork);
|
|
await this.updateTokenUsage(referralToken.id, unitOfWork);
|
|
|
|
logger.info('ReferralService:processReferral:success', { userId, referralTokenId: referralToken.id });
|
|
return true;
|
|
} catch (error) {
|
|
logger.error('ReferralService:processReferral:error', { userId, referralEmail, error });
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static async findReferrerByEmail(email, unitOfWork) {
|
|
logger.debug('ReferralService:findReferrerByEmail', { email });
|
|
const conn = unitOfWork.connection;
|
|
const query = `SELECT id FROM users WHERE email = ?`;
|
|
const [rows] = await conn.query(query, [email]);
|
|
return rows.length > 0 ? rows[0] : null;
|
|
}
|
|
|
|
static async findActiveReferralToken(userId, unitOfWork) {
|
|
logger.debug('ReferralService:findActiveReferralToken', { userId });
|
|
const conn = unitOfWork.connection;
|
|
const query = `
|
|
SELECT * FROM referral_tokens
|
|
WHERE created_by_user_id = ? AND status = 'active' AND expires_at > NOW()
|
|
LIMIT 1
|
|
`;
|
|
const [rows] = await conn.query(query, [userId]);
|
|
return rows.length > 0 ? rows[0] : null;
|
|
}
|
|
|
|
static async createReferralUsage(tokenId, userId, unitOfWork) {
|
|
logger.debug('ReferralService:createReferralUsage', { tokenId, userId });
|
|
const conn = unitOfWork.connection;
|
|
const query = `
|
|
INSERT INTO referral_token_usage (referral_token_id, used_by_user_id)
|
|
VALUES (?, ?)
|
|
`;
|
|
await conn.query(query, [tokenId, userId]);
|
|
}
|
|
|
|
static async updateTokenUsage(tokenId, unitOfWork) {
|
|
logger.debug('ReferralService:updateTokenUsage', { tokenId });
|
|
const conn = unitOfWork.connection;
|
|
const query = `
|
|
UPDATE referral_tokens
|
|
SET uses_remaining = uses_remaining - 1
|
|
WHERE id = ? AND uses_remaining > 0
|
|
`;
|
|
await conn.query(query, [tokenId]);
|
|
}
|
|
}
|
|
|
|
module.exports = ReferralService;
|