UNPKG

@seneca/refer

Version:

User Referral business logic plugin for the Seneca platform.

404 lines 12.8 kB
"use strict"; /* Copyright © 2022 Seneca Project Contributors, MIT License. */ Object.defineProperty(exports, "__esModule", { value: true }); function refer(options) { const seneca = this; const genToken = this.util.Nid(options.token); const genCode = this.util.Nid(options.code); seneca .fix('biz:refer') .message('create:entry', msgCreateEntry) .message('accept:entry', msgAcceptEntry) .message('update:occur', msgUpdateOccur) .message('update:entry', msgUpdateEntry) .message('load:entry', msgLoadEntry) .message('ensure:entry', msgEnsureEntry) .message('lost:entry', msgLostEntry) .message('give:award', msgRewardEntry) .message('load:rules', msgLoadRules); // TODO: seneca.prepare should not be affected by seneca.fix seneca .prepare(prepare); async function msgCreateEntry(msg) { const seneca = this; // Sending user, not required let user_id = msg.user_id; let method = msg.method || 'email'; // 'email' | 'code' let email = msg.email; // required if method=email and mode=single let code = msg.code; // explicit code, otherwise generated let token = msg.token; // explicit code, otherwise generated let mode = msg.mode || 'single'; // 'single' | 'multi' | 'limit' let limit = msg.limit || 1; // usage limit; -1 = unlimited let kind = msg.kind || 'standard'; let peg = msg.peg || 'none'; // app specific entry type let active = null == msg.active ? true : !!msg.active; let EntryEnt = seneca.entity('refer/entry'); let OccurEnt = seneca.entity('refer/occur'); // Check single use email referral used only once if ('email' === method && 'single' === mode) { if (null == email || '' === email) { return { ok: false, why: 'email-required', }; } let occur = await OccurEnt.load$({ email, kind: 'accept', }); if (occur) { return { ok: false, why: 'entry-exists', details: { email } }; } } const entry = await EntryEnt.save$({ user_id, kind, email, method, mode, limit, peg, // unique token for this referral, used for link validation token: token || genToken(), // unique code for this referral, used for human validation code: code || genCode(), // usage count count: 0, active, }); let occur; // REVIEW: is this 'create' entry needed? if ('single' === mode) { occur = await OccurEnt.data$({ id: null, id$: null, user_id: msg.user_id, entry_kind: msg.kind, entry_mode: msg.mode, entry_peg: msg.peg, email: msg.email, entry_id: entry.id, kind: 'create', code: entry.code, token: entry.token, }).save$(); } return { ok: true, entry, occur, }; } async function msgAcceptEntry(msg) { const seneca = this; // If check=true, do not update occur let check = true === msg.check ? true : false; // User using the referral, if known at creation let user_id = msg.user_id; let token = msg.token; let code = msg.code; let q = {}; if (msg.token) { q.token = msg.token; } else if (msg.code) { q.code = msg.code; } else { return { ok: false, why: 'no-token-or-code' }; } const entry = await seneca.entity('refer/entry').load$(q); if (!entry) { return { ok: false, why: 'entry-unknown', details: { token, code, } }; } if (!entry.active) { return { ok: false, why: 'entry-not-active', }; } let occurs = await this.entity('refer/occur').list$({ entry_id: entry.id, fields$: ['kind'] }); let isLost = occurs.find((occur) => 'lost' === occur.kind); if (isLost) { return { ok: false, why: 'entry-lost', }; } let accepts = occurs.filter((occur) => 'accept' === occur.kind); if (('single' === entry.mode || 1 === entry.limit) && (1 <= accepts)) { return { ok: false, why: 'entry-used', }; } else if (0 < entry.limit && entry.limit <= accepts.length) { return { ok: false, why: 'entry-limit', details: { limit: entry.limit, accepts: accepts.length, } }; } let occur; if (!check) { occur = await seneca.entity('refer/occur').save$({ user_id, entry_kind: entry.kind, email: entry.email, entry_id: entry.id, kind: 'accept', code: entry.code, token: entry.token, }); entry.count = accepts.length + 1; await entry.save$(); } return { ok: true, entry, occur, // NOTE: will be undef if check=true }; } async function msgUpdateOccur(msg) { const seneca = this; let occur_id = msg.occur_id; let code = msg.code; let token = msg.token; let occurUpdate = msg.occur; let q = {}; if (occur_id) { q.id = occur_id; } let occur = await seneca.entity('refer/occur').load$(q); if (!occur) { return { ok: false, why: 'not-found' }; } occur.data$(occurUpdate); await occur.save$(); return { ok: true, occur, }; } async function msgUpdateEntry(msg) { const seneca = this; let entry_id = msg.entry_id; let active = msg.active; let entry = seneca.entity('refer/entry').load$(entry_id); if (!entry) { return { ok: false, why: 'not-found' }; } if (null != active) { entry.active = !!active; await entry.save$(); } return { ok: true, entry, }; } async function msgLoadEntry(msg) { const seneca = this; let entry_id = msg.entry_id; let entry = seneca.entity('refer/entry').load$(entry_id); if (!entry) { return { ok: false, why: 'not-found' }; } let occurs = seneca.entity('refer/occur').list$({ entry_id: entry.id }); return { ok: true, entry, occurs, }; } // Create if not exists, otherwise return match // Most useful for mode=multi async function msgEnsureEntry(msg) { const seneca = this; let user_id = msg.user_id; let kind = msg.kind || 'standard'; let peg = msg.peg; let entry = await seneca.entity('refer/entry').load$({ user_id, kind, peg, }); let out; if (null == entry) { let createMsg = { ...msg, create: 'entry' }; delete createMsg.ensure; out = await seneca.post(createMsg); } else { out = { ok: true, entry, occur: [], }; } return out; } async function msgLostEntry(msg) { const seneca = this; const occurList = await seneca.entity('refer/occur').list$({ email: msg.email, kind: 'create', }); const unacceptedReferrals = occurList.filter((occur) => occur.user_id !== msg.userWinner); for (let i = 0; i < unacceptedReferrals.length; i++) { await seneca.entity('refer/occur').save$({ user_id: unacceptedReferrals[i].user_id, entry_kind: unacceptedReferrals[i].entry_kind, email: msg.email, entry_id: unacceptedReferrals[i].entry_id, kind: 'lost', }); } } async function msgRewardEntry(msg) { const seneca = this; const entry = await seneca.entity('refer/occur').load$({ entry_id: msg.entry_id, }); if (!entry) { return { ok: false, why: 'unknown-entry' }; } let reward = await this.entity('refer/reward').load$({ entry_id: entry.id, }); if (!reward) { reward = seneca.make('refer/reward', { entry_id: msg.entry_id, entry_kind: msg.entry_kind, kind: msg.kind, award: msg.award, }); reward[msg.field] = 0; } reward[msg.field] = reward[msg.field] + 1; await reward.save$(); } async function msgLoadRules(msg) { const seneca = this; const rules = await seneca.entity('refer/rule').list$(); // TODO: handle rule updates? // TODO: create a @seneca/rule plugin? later! for (let rule of rules) { if (rule.ent) { const subpat = generateSubPat(seneca, rule); seneca.sub(subpat, function (msg) { if (rule.where.kind === 'create') { rule.call.forEach((callmsg) => { // TODO: use https://github.com/rjrodger/inks callmsg.toaddr = msg.ent.email; callmsg.fromaddr = 'invite@example.com'; this.act(callmsg); }); } }); seneca.sub(subpat, function (msg) { if (rule.where.kind === 'accept') { rule.call.forEach((callmsg) => { callmsg.ent = seneca.entity(rule.ent); callmsg.entry_id = msg.q.entry_id; callmsg.entry_kind = msg.q.entry_kind; this.act(callmsg); }); } }); seneca.sub(subpat, function (msg) { if (rule.where.kind === 'lost' && msg.q.kind === 'accept') { rule.call.forEach((callmsg) => { callmsg.ent = seneca.entity(rule.ent); callmsg.email = msg.q.email; callmsg.userWinner = msg.q.user_id; this.act(callmsg); }); } }); } // else ignore as not yet implemented } } async function prepare() { const seneca = this; await seneca.post('biz:refer,load:rules'); } function generateSubPat(seneca, rule) { const ent = seneca.entity(rule.ent); const canon = ent.canon$({ object: true }); Object.keys(canon).forEach((key) => { if (!canon[key]) { delete canon[key]; } }); return { role: 'entity', cmd: rule.cmd, q: rule.where, ...canon, out$: true, }; } return { exports: { genToken, genCode, } }; } // Default options. const defaults = { // TODO: Enable debug logging debug: false, token: { len: 16, alphabet: undefined, }, code: { len: 6, alphabet: 'BCDFGHJKLMNPQRSTVWXYZ2456789' } }; Object.assign(refer, { defaults }); exports.default = refer; if ('undefined' !== typeof module) { module.exports = refer; } //# sourceMappingURL=refer.js.map