resumeCampaign only updated status/pause_reason, so a campaign auto-paused
for many days and then resumed could immediately satisfy
escalateWeeklyLimits' 7-day check and double its daily limit right after
coming back from an unhealthy state -- the opposite of the intended
safety behavior. Now resumeCampaign also stamps semana_iniciada_em to
datetime('now'), resetting the weekly clock on every resume.
116 lines
4.7 KiB
JavaScript
116 lines
4.7 KiB
JavaScript
import { transaction } from '../db.mjs';
|
|
|
|
export function createCampaign(db, {
|
|
nome, produto, segmentos, variantes, limiteDiarioInicial = 10, limiteDiarioTeto = 80
|
|
}) {
|
|
return transaction(db, () => {
|
|
const result = db.prepare(
|
|
`INSERT INTO outreach_campaigns
|
|
(nome, produto, segmentos_json, limite_diario_atual, limite_diario_teto)
|
|
VALUES (?, ?, ?, ?, ?)`
|
|
).run(nome, produto, JSON.stringify(segmentos || []), limiteDiarioInicial, limiteDiarioTeto);
|
|
const campaignId = Number(result.lastInsertRowid);
|
|
const insertVariant = db.prepare(
|
|
`INSERT INTO outreach_campaign_variants (campaign_id, code, mensagem) VALUES (?, ?, ?)`
|
|
);
|
|
for (const variante of variantes) {
|
|
insertVariant.run(campaignId, variante.code, variante.mensagem);
|
|
}
|
|
return getCampaign(db, campaignId);
|
|
});
|
|
}
|
|
|
|
export function getCampaign(db, id) {
|
|
const row = db.prepare('SELECT * FROM outreach_campaigns WHERE id = ?').get(id);
|
|
if (!row) return null;
|
|
return {
|
|
...row,
|
|
segmentos: JSON.parse(row.segmentos_json || '[]'),
|
|
variants: listVariants(db, id)
|
|
};
|
|
}
|
|
|
|
export function listCampaigns(db) {
|
|
return db.prepare('SELECT * FROM outreach_campaigns ORDER BY id DESC').all()
|
|
.map((row) => ({ ...row, segmentos: JSON.parse(row.segmentos_json || '[]') }));
|
|
}
|
|
|
|
export function listVariants(db, campaignId) {
|
|
return db.prepare(
|
|
'SELECT * FROM outreach_campaign_variants WHERE campaign_id = ? AND ativo = 1 ORDER BY code'
|
|
).all(campaignId);
|
|
}
|
|
|
|
// status: 'rascunho' -> 'rodando' -> 'pausado' <-> 'rodando' -> 'concluido'.
|
|
// pause_reason so fica setado quando o novo status e 'pausado'.
|
|
export function setCampaignStatus(db, campaignId, status, pauseReason = null) {
|
|
if (status === 'rodando') {
|
|
// When transitioning to 'rodando', stamp semana_iniciada_em only on first run (if currently NULL)
|
|
db.prepare(
|
|
`UPDATE outreach_campaigns
|
|
SET status = ?, pause_reason = ?, semana_iniciada_em = COALESCE(semana_iniciada_em, datetime('now')), updated_at = datetime('now')
|
|
WHERE id = ?`
|
|
).run(status, status === 'pausado' ? pauseReason : null, campaignId);
|
|
} else {
|
|
db.prepare(
|
|
`UPDATE outreach_campaigns SET status = ?, pause_reason = ?, updated_at = datetime('now') WHERE id = ?`
|
|
).run(status, status === 'pausado' ? pauseReason : null, campaignId);
|
|
}
|
|
return getCampaign(db, campaignId);
|
|
}
|
|
|
|
// So avanca campanha que estiver de fato pausada — retomar uma campanha em
|
|
// rascunho, por exemplo, deveria usar setCampaignStatus(db, id, 'rodando').
|
|
export function resumeCampaign(db, campaignId) {
|
|
// Reseta semana_iniciada_em pra agora: uma campanha que ficou pausada (por
|
|
// saude ruim ou motivo manual) por varios dias nao pode, ao ser retomada,
|
|
// já satisfazer o "rodando ha 7+ dias" do escalateWeeklyLimits e dobrar o
|
|
// limite diario logo de cara — isso inverteria a logica de seguranca.
|
|
db.prepare(
|
|
`UPDATE outreach_campaigns
|
|
SET status = 'rodando', pause_reason = NULL, semana_iniciada_em = datetime('now'), updated_at = datetime('now')
|
|
WHERE id = ? AND status = 'pausado'`
|
|
).run(campaignId);
|
|
return getCampaign(db, campaignId);
|
|
}
|
|
|
|
// Escalonamento semanal: dobra o limite diario a cada 7 dias corridos rodando
|
|
// sem pausa desde semana_iniciada_em, ate o teto da campanha. So campanhas com
|
|
// status='rodando' avancam — uma pausada fica congelada ate o operador retomar.
|
|
export function escalateWeeklyLimits(db, now = new Date()) {
|
|
const campaigns = db.prepare(
|
|
`SELECT id, limite_diario_atual, limite_diario_teto, semana_iniciada_em
|
|
FROM outreach_campaigns WHERE status = 'rodando'`
|
|
).all();
|
|
let escaladas = 0;
|
|
for (const campaign of campaigns) {
|
|
if (!campaign.semana_iniciada_em) continue;
|
|
const dias = (now.getTime() - new Date(campaign.semana_iniciada_em.replace(' ', 'T') + 'Z').getTime()) / 86400000;
|
|
if (dias < 7) continue;
|
|
const novoLimite = Math.min(campaign.limite_diario_teto, campaign.limite_diario_atual * 2);
|
|
if (novoLimite === campaign.limite_diario_atual) continue;
|
|
db.prepare(
|
|
`UPDATE outreach_campaigns
|
|
SET limite_diario_atual = ?, semana_iniciada_em = datetime('now'), updated_at = datetime('now')
|
|
WHERE id = ?`
|
|
).run(novoLimite, campaign.id);
|
|
escaladas += 1;
|
|
}
|
|
return escaladas;
|
|
}
|
|
|
|
export function isOptedOut(db, numero) {
|
|
return Boolean(db.prepare('SELECT 1 FROM outreach_opt_out WHERE numero = ?').get(numero));
|
|
}
|
|
|
|
export function addOptOut(db, numero, motivo = null) {
|
|
db.prepare(
|
|
`INSERT INTO outreach_opt_out (numero, motivo) VALUES (?, ?)
|
|
ON CONFLICT(numero) DO UPDATE SET motivo = excluded.motivo`
|
|
).run(numero, motivo);
|
|
}
|
|
|
|
export function removeOptOut(db, numero) {
|
|
db.prepare('DELETE FROM outreach_opt_out WHERE numero = ?').run(numero);
|
|
}
|