353 lines
13 KiB
JavaScript
353 lines
13 KiB
JavaScript
const pool = require('../../database/database');
|
|
const Abonemment = require('../../models/Abonemment');
|
|
|
|
class AbonemmentRepository {
|
|
async createAbonement(referredBy, snapshot) {
|
|
const conn = await pool.getConnection();
|
|
try {
|
|
await conn.beginTransaction();
|
|
const [res] = await conn.query(
|
|
`INSERT INTO coffee_abonements
|
|
(pack_group, status, started_at, ended_at, next_billing_at, billing_interval, interval_count, price, currency, is_auto_renew, notes, pack_breakdown, first_name, last_name, email, street, postal_code, city, country, frequency, referred_by, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())`,
|
|
[
|
|
snapshot.pack_group || '',
|
|
snapshot.status || 'active',
|
|
snapshot.started_at,
|
|
snapshot.ended_at || null,
|
|
snapshot.next_billing_at || null,
|
|
snapshot.billing_interval,
|
|
snapshot.interval_count,
|
|
snapshot.price,
|
|
snapshot.currency,
|
|
snapshot.is_auto_renew ? 1 : 0,
|
|
snapshot.notes || null,
|
|
snapshot.pack_breakdown ? JSON.stringify(snapshot.pack_breakdown) : null,
|
|
snapshot.first_name || null,
|
|
snapshot.last_name || null,
|
|
snapshot.email || null,
|
|
snapshot.street || null,
|
|
snapshot.postal_code || null,
|
|
snapshot.city || null,
|
|
snapshot.country || null,
|
|
snapshot.frequency || null,
|
|
referredBy || null, // Ensure referred_by is stored
|
|
],
|
|
);
|
|
const abonementId = res.insertId;
|
|
|
|
const historyDetails = { ...(snapshot.details || {}), pack_group: snapshot.pack_group };
|
|
await conn.query(
|
|
`INSERT INTO coffee_abonement_history
|
|
(abonement_id, event_type, event_at, actor_user_id, details, created_at)
|
|
VALUES (?, ?, ?, ?, ?, NOW())`,
|
|
[
|
|
abonementId,
|
|
'created',
|
|
snapshot.started_at,
|
|
referredBy || snapshot.actor_user_id || null, // Use referredBy as actor_user_id if available
|
|
JSON.stringify(historyDetails),
|
|
],
|
|
);
|
|
|
|
await conn.commit();
|
|
return this.getAbonementById(abonementId);
|
|
} catch (err) {
|
|
await conn.rollback();
|
|
throw err;
|
|
} finally {
|
|
conn.release();
|
|
}
|
|
}
|
|
|
|
async getAbonementById(id) {
|
|
const [rows] = await pool.query(`SELECT * FROM coffee_abonements WHERE id = ?`, [id]);
|
|
return rows[0] ? new Abonemment(rows[0]) : null;
|
|
}
|
|
|
|
async listByUser(userId, { status, limit = 50, offset = 0 } = {}) {
|
|
const params = [userId];
|
|
let sql = `SELECT * FROM coffee_abonements WHERE user_id = ?`;
|
|
if (status) {
|
|
sql += ` AND status = ?`;
|
|
params.push(status);
|
|
}
|
|
sql += ` ORDER BY created_at DESC LIMIT ? OFFSET ?`;
|
|
params.push(Number(limit), Number(offset));
|
|
const [rows] = await pool.query(sql, params);
|
|
return rows.map((r) => new Abonemment(r));
|
|
}
|
|
|
|
async updateStatus(id, newStatus, { ended_at, updated_at = new Date() } = {}) {
|
|
await pool.query(
|
|
`UPDATE coffee_abonements SET status = ?, ended_at = ?, updated_at = ? WHERE id = ?`,
|
|
[newStatus, ended_at || null, updated_at, id],
|
|
);
|
|
return this.getAbonementById(id);
|
|
}
|
|
|
|
async updateBilling(id, next_billing_at, updated_at = new Date()) {
|
|
await pool.query(
|
|
`UPDATE coffee_abonements SET next_billing_at = ?, updated_at = ? WHERE id = ?`,
|
|
[next_billing_at, updated_at, id],
|
|
);
|
|
return this.getAbonementById(id);
|
|
}
|
|
|
|
async appendHistory(abonementId, eventType, actorUserId, details = {}, eventAt = new Date()) {
|
|
await pool.query(
|
|
`INSERT INTO coffee_abonement_history
|
|
(abonement_id, event_type, event_at, actor_user_id, details, created_at)
|
|
VALUES (?, ?, ?, ?, ?, NOW())`,
|
|
[abonementId, eventType, eventAt, actorUserId || null, JSON.stringify(details || {})],
|
|
);
|
|
}
|
|
|
|
async transitionStatus(id, newStatus, historyPayload = {}) {
|
|
const conn = await pool.getConnection();
|
|
try {
|
|
await conn.beginTransaction();
|
|
await conn.query(
|
|
`UPDATE coffee_abonements SET status = ?, ended_at = ?, updated_at = ? WHERE id = ?`,
|
|
[
|
|
newStatus,
|
|
historyPayload.ended_at || null,
|
|
historyPayload.updated_at || new Date(),
|
|
id,
|
|
],
|
|
);
|
|
await conn.query(
|
|
`INSERT INTO coffee_abonement_history
|
|
(abonement_id, event_type, event_at, actor_user_id, details, created_at)
|
|
VALUES (?, ?, ?, ?, ?, NOW())`,
|
|
[
|
|
id,
|
|
historyPayload.event_type || 'updated',
|
|
historyPayload.event_at || new Date(),
|
|
historyPayload.actor_user_id || null,
|
|
JSON.stringify(historyPayload.details || {}),
|
|
],
|
|
);
|
|
await conn.commit();
|
|
} catch (err) {
|
|
await conn.rollback();
|
|
throw err;
|
|
} finally {
|
|
conn.release();
|
|
}
|
|
return this.getAbonementById(id);
|
|
}
|
|
|
|
async transitionBilling(id, nextBillingAt, historyPayload = {}) {
|
|
const conn = await pool.getConnection();
|
|
try {
|
|
await conn.beginTransaction();
|
|
await conn.query(
|
|
`UPDATE coffee_abonements SET next_billing_at = ?, updated_at = ? WHERE id = ?`,
|
|
[nextBillingAt, historyPayload.updated_at || new Date(), id],
|
|
);
|
|
await conn.query(
|
|
`INSERT INTO coffee_abonement_history
|
|
(abonement_id, event_type, event_at, actor_user_id, details, created_at)
|
|
VALUES (?, ?, ?, ?, ?, NOW())`,
|
|
[
|
|
id,
|
|
historyPayload.event_type || 'renewed',
|
|
historyPayload.event_at || new Date(),
|
|
historyPayload.actor_user_id || null,
|
|
JSON.stringify(historyPayload.details || {}),
|
|
],
|
|
);
|
|
await conn.commit();
|
|
} catch (err) {
|
|
await conn.rollback();
|
|
throw err;
|
|
} finally {
|
|
conn.release();
|
|
}
|
|
return this.getAbonementById(id);
|
|
}
|
|
|
|
async listDueForBilling(now) {
|
|
const [rows] = await pool.query(
|
|
`SELECT * FROM coffee_abonements
|
|
WHERE status = 'active' AND is_auto_renew = 1 AND next_billing_at IS NOT NULL AND next_billing_at <= ?
|
|
ORDER BY next_billing_at ASC`,
|
|
[now],
|
|
);
|
|
return rows.map((r) => new Abonemment(r));
|
|
}
|
|
|
|
async listActiveByProduct(productId) {
|
|
const [rows] = await pool.query(
|
|
`SELECT * FROM coffee_abonements WHERE coffee_table_id = ? AND status = 'active'`,
|
|
[productId],
|
|
);
|
|
return rows.map((r) => new Abonemment(r));
|
|
}
|
|
|
|
async listHistory(abonementId) {
|
|
const [rows] = await pool.query(
|
|
`SELECT * FROM coffee_abonement_history WHERE abonement_id = ? ORDER BY event_at ASC, id ASC`,
|
|
[abonementId],
|
|
);
|
|
return rows.map((r) => ({
|
|
id: r.id,
|
|
abonement_id: r.abonement_id,
|
|
event_type: r.event_type,
|
|
event_at: r.event_at,
|
|
actor_user_id: r.actor_user_id,
|
|
details: r.details ? JSON.parse(r.details) : {},
|
|
created_at: r.created_at,
|
|
}));
|
|
}
|
|
|
|
async findActiveOrPausedByUserAndProduct(userId, packGroup) {
|
|
const normalizedUserId = userId ?? null;
|
|
const normalizedPackGroup = packGroup || '';
|
|
const [rows] = await pool.query(
|
|
`SELECT * FROM coffee_abonements
|
|
WHERE pack_group = ? AND user_id <=> ? AND status IN ('active','paused')
|
|
ORDER BY created_at DESC LIMIT 1`,
|
|
[normalizedPackGroup, normalizedUserId],
|
|
);
|
|
return rows[0] ? new Abonemment(rows[0]) : null;
|
|
}
|
|
|
|
async updateExistingAbonementForSubscribe(id, snapshot) {
|
|
const conn = await pool.getConnection();
|
|
try {
|
|
await conn.beginTransaction();
|
|
await conn.query(
|
|
`UPDATE coffee_abonements
|
|
SET status = 'active',
|
|
pack_group = ?,
|
|
started_at = IFNULL(started_at, ?),
|
|
next_billing_at = ?,
|
|
billing_interval = ?,
|
|
interval_count = ?,
|
|
price = ?,
|
|
currency = ?,
|
|
is_auto_renew = ?,
|
|
notes = ?,
|
|
recipient_email = ?,
|
|
purchaser_user_id = IFNULL(purchaser_user_id, ?),
|
|
pack_breakdown = ?, -- NEW
|
|
updated_at = NOW()
|
|
WHERE id = ?`,
|
|
[
|
|
snapshot.pack_group || '',
|
|
snapshot.started_at,
|
|
snapshot.next_billing_at,
|
|
snapshot.billing_interval,
|
|
snapshot.interval_count,
|
|
snapshot.price,
|
|
snapshot.currency,
|
|
snapshot.is_auto_renew ? 1 : 0,
|
|
snapshot.notes || null,
|
|
snapshot.recipient_email || null,
|
|
snapshot.purchaser_user_id ?? null,
|
|
snapshot.pack_breakdown ? JSON.stringify(snapshot.pack_breakdown) : null,
|
|
id,
|
|
],
|
|
);
|
|
const historyDetails = { ...(snapshot.details || {}), reused_existing: true, pack_group: snapshot.pack_group };
|
|
if (snapshot.recipient) historyDetails.recipient = snapshot.recipient;
|
|
await conn.query(
|
|
`INSERT INTO coffee_abonement_history
|
|
(abonement_id, event_type, event_at, actor_user_id, details, created_at)
|
|
VALUES (?, ?, ?, ?, ?, NOW())`,
|
|
[id, 'merged_subscribe', snapshot.started_at, snapshot.actor_user_id || null, JSON.stringify(historyDetails)],
|
|
);
|
|
await conn.commit();
|
|
return this.getAbonementById(id);
|
|
} catch (err) {
|
|
await conn.rollback();
|
|
throw err;
|
|
} finally {
|
|
conn.release();
|
|
}
|
|
}
|
|
|
|
// Helper: attempt insert into alias table if it exists
|
|
async tryAliasInsert(email, abonementId, createdByUserId) {
|
|
try {
|
|
await pool.query(
|
|
`INSERT INTO no_user_aboo_mails (email, abonement_id, status, source, created_by_user_id, created_at, updated_at)
|
|
VALUES (?, ?, 'pending', 'subscribe', ?, NOW(), NOW())
|
|
ON DUPLICATE KEY UPDATE status = 'pending', source = 'subscribe', created_by_user_id = VALUES(created_by_user_id), updated_at = NOW()`,
|
|
[email, abonementId, createdByUserId || null],
|
|
);
|
|
} catch (e) {
|
|
if (!(e && e.code === 'ER_NO_SUCH_TABLE')) throw e;
|
|
}
|
|
}
|
|
|
|
// Insert/update pending ownership by email (primary table), with alias fallback
|
|
async upsertNoUserAboMail(email, abonementId, createdByUserId) {
|
|
const normalized = typeof email === 'string' ? email.trim().toLowerCase() : null;
|
|
console.log('[UPSERT NO USER ABO MAIL] Normalized email:', normalized); // NEW
|
|
console.log('[UPSERT NO USER ABO MAIL] Abonement ID:', abonementId); // NEW
|
|
console.log('[UPSERT NO USER ABO MAIL] Created by user ID:', createdByUserId); // NEW
|
|
|
|
if (!normalized) {
|
|
console.log('[UPSERT NO USER ABO MAIL] Skipping due to invalid email'); // NEW
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await pool.query(
|
|
`INSERT INTO no_user_abo_mails (email, abonement_id, status, source, created_by_user_id, created_at, updated_at)
|
|
VALUES (?, ?, 'pending', 'subscribe', ?, NOW(), NOW())
|
|
ON DUPLICATE KEY UPDATE status = 'pending', source = 'subscribe', created_by_user_id = VALUES(created_by_user_id), updated_at = NOW()`,
|
|
[normalized, abonementId, createdByUserId || null],
|
|
);
|
|
console.log('[UPSERT NO USER ABO MAIL] Successfully inserted/updated record'); // NEW
|
|
} catch (err) {
|
|
console.error('[UPSERT NO USER ABO MAIL] Error inserting/updating record:', err); // NEW
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
// Fetch pending ownership rows for an email
|
|
async findPendingNoUserAboMailsByEmail(email) {
|
|
const normalized = typeof email === 'string' ? email.trim().toLowerCase() : null;
|
|
console.log('[FIND PENDING NO USER ABO MAILS] Normalized email:', normalized); // NEW
|
|
|
|
if (!normalized) {
|
|
console.log('[FIND PENDING NO USER ABO MAILS] Skipping due to invalid email'); // NEW
|
|
return [];
|
|
}
|
|
|
|
const [rows] = await pool.query(
|
|
`SELECT * FROM no_user_abo_mails WHERE email = ? AND status = 'pending'`,
|
|
[normalized],
|
|
);
|
|
console.log('[FIND PENDING NO USER ABO MAILS] Found rows:', rows); // NEW
|
|
return rows || [];
|
|
}
|
|
|
|
// Mark a pending ownership record as linked
|
|
async markNoUserAboMailLinked(id) {
|
|
console.log('[MARK NO USER ABO MAIL LINKED] Marking record as linked for ID:', id); // NEW
|
|
await pool.query(`UPDATE no_user_abo_mails SET status = 'linked', updated_at = NOW() WHERE id = ?`, [id]);
|
|
console.log('[MARK NO USER ABO MAIL LINKED] Successfully marked record as linked'); // NEW
|
|
}
|
|
|
|
// Link abonement to a user (on registration)
|
|
async linkAbonementToUser(abonementId, userId) {
|
|
await pool.query(`UPDATE coffee_abonements SET user_id = ?, updated_at = NOW() WHERE id = ?`, [userId, abonementId]);
|
|
}
|
|
|
|
async findByReferredByAndEmail(referredBy, email) {
|
|
const [rows] = await pool.query(
|
|
`SELECT * FROM coffee_abonements
|
|
WHERE referred_by = ? AND email = ?
|
|
ORDER BY created_at DESC`,
|
|
[referredBy, email],
|
|
);
|
|
return rows.map((row) => new Abonemment(row));
|
|
}
|
|
}
|
|
|
|
module.exports = AbonemmentRepository; |