@xuda.io/account_module 1.2.2313 → 1.2.2314
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.mjs +205 -4
- package/index_ms.mjs +8 -0
- package/index_msa.mjs +8 -0
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -670,8 +670,20 @@ export const get_effective_entitlements = function (account_doc) {
|
|
|
670
670
|
// to where it is set.
|
|
671
671
|
// scope 'metered' → no plan ladder at all, usage is metered per call. Listed
|
|
672
672
|
// so the customer can see it costs no subscription.
|
|
673
|
+
//
|
|
674
|
+
// `is_active` → the module is being PAID for. Defaults to price > 0.
|
|
675
|
+
// `is_activated`→ the customer switched it ON, Free included. Defaults to
|
|
676
|
+
// `is_active` or a `<field>_changed` stamp, and a module that
|
|
677
|
+
// keeps its own answer to that question says so here rather
|
|
678
|
+
// than leaving the UI to guess from a price of zero.
|
|
673
679
|
const _MODULE_SUBSCRIPTIONS = [
|
|
674
|
-
|
|
680
|
+
// UI-192: `commerce_plan` being SET at all is Commerce's own on/off switch
|
|
681
|
+
// (`chosen` in commerce_module _cm_plan), which is why activation is read off
|
|
682
|
+
// the field rather than off the stamp: commerce_set_plan wrote the plan and
|
|
683
|
+
// stamped nothing until this same change, so every account that switched
|
|
684
|
+
// Commerce on at its free tier reported "not switched on" here while its own
|
|
685
|
+
// screen showed the stores.
|
|
686
|
+
{ key: 'commerce', scope: 'account', field: 'commerce_plan', default_plan: 'commerce_free', is_activated: (a) => !!a.commerce_plan },
|
|
675
687
|
{ key: 'shipping', scope: 'account', field: 'shipping_plan', default_plan: 'shipping_free' },
|
|
676
688
|
{ key: 'finance', scope: 'account', field: 'finance_plan', default_plan: 'finance_free' },
|
|
677
689
|
// The desk screen has its own copy of this ladder (UI-82), because Tickets is
|
|
@@ -681,12 +693,17 @@ const _MODULE_SUBSCRIPTIONS = [
|
|
|
681
693
|
// UI-104: email is its own product, billed per mailbox tier. Its plan lives on the
|
|
682
694
|
// account as an OBJECT (`email_plan.tier`), which is why it resolves rather than
|
|
683
695
|
// reading a field, and `set_email_plan` is what moves it.
|
|
696
|
+
// UI-192: and its "the customer chose this" marker is `email_plan.activated_ts`, not
|
|
697
|
+
// an `email_plan_changed` field, which is email_policy's own `chosen`. The generic
|
|
698
|
+
// stamp lookup never found anything, so an account on a deliberately free mailbox
|
|
699
|
+
// tier read as switched off.
|
|
684
700
|
{
|
|
685
701
|
key: 'email',
|
|
686
702
|
scope: 'account',
|
|
687
703
|
field: 'email_plan',
|
|
688
704
|
default_plan: 'email_free',
|
|
689
705
|
resolve: (a) => (a && a.email_plan && a.email_plan.tier ? `email_${a.email_plan.tier}` : ''),
|
|
706
|
+
changed_of: (a) => (a && a.email_plan && a.email_plan.activated_ts) || null,
|
|
690
707
|
},
|
|
691
708
|
// Auto response is the single gate for whether the AI answers, on every channel
|
|
692
709
|
// (chat, the public chat widget, email and the phone). It is account scoped and
|
|
@@ -952,9 +969,14 @@ export const get_module_subscriptions = async function (req = {}) {
|
|
|
952
969
|
// `plan_of` keeps reporting no plan, exactly as before.
|
|
953
970
|
const plan_id = m.plan_of ? m.plan_of(account) : '';
|
|
954
971
|
const plan = plans[plan_id] || {};
|
|
972
|
+
const active = m.is_active ? !!m.is_active(account, base.items) : m.scope === 'metered' || base.items.length > 0;
|
|
955
973
|
return {
|
|
956
974
|
...base,
|
|
957
|
-
active
|
|
975
|
+
active,
|
|
976
|
+
// UI-192: a per-resource module has no free tier to be switched on at, so
|
|
977
|
+
// paying for one of the things IS being switched on. Sent all the same, so
|
|
978
|
+
// every surface can ask the one question rather than two.
|
|
979
|
+
activated: m.is_activated ? !!m.is_activated(account, base.items) : active,
|
|
958
980
|
included: m.scope === 'metered',
|
|
959
981
|
plan_id,
|
|
960
982
|
plan_name: plan.name || '',
|
|
@@ -977,6 +999,11 @@ export const get_module_subscriptions = async function (req = {}) {
|
|
|
977
999
|
? { to_plan: pc.to_plan, to_name: plans[pc.to_plan]?.name || pc.to_plan, effective: pc.effective || null }
|
|
978
1000
|
: null;
|
|
979
1001
|
const price = Number(plan.price) || 0;
|
|
1002
|
+
// Every *_set_plan method stamps `<field>_changed` when the customer picks a
|
|
1003
|
+
// tier, Free included, so this is the general answer to "have they been through
|
|
1004
|
+
// the picker". A module whose marker is somewhere else brings its own resolver.
|
|
1005
|
+
const changed_ts = (m.changed_of ? m.changed_of(account) : account[`${m.field}_changed`]) || null;
|
|
1006
|
+
const active = m.is_active ? !!m.is_active(account, base.items) : price > 0;
|
|
980
1007
|
return {
|
|
981
1008
|
...base,
|
|
982
1009
|
plan_id,
|
|
@@ -985,10 +1012,16 @@ export const get_module_subscriptions = async function (req = {}) {
|
|
|
985
1012
|
// UI-110: paying is the default proof that a module is on, but a module that
|
|
986
1013
|
// records the customer CHOOSING a tier says so itself, because a deliberate
|
|
987
1014
|
// free tier is switched on and a price of zero cannot tell the two apart.
|
|
988
|
-
active
|
|
1015
|
+
active,
|
|
1016
|
+
// UI-192 (Boaz, on the create sheet's switches): "switched on" as its own
|
|
1017
|
+
// answer, so no surface has to reassemble it out of `active` and `changed_ts`
|
|
1018
|
+
// and get it differently from the next one. Picking a FREE tier is switching
|
|
1019
|
+
// the module on: it is a real tier, it opens the module's screen and it puts
|
|
1020
|
+
// the module in the menu, and the switch that did it has to stay flipped.
|
|
1021
|
+
activated: m.is_activated ? !!m.is_activated(account, base.items) : !!(active || changed_ts),
|
|
989
1022
|
included: false,
|
|
990
1023
|
pending,
|
|
991
|
-
changed_ts
|
|
1024
|
+
changed_ts,
|
|
992
1025
|
};
|
|
993
1026
|
});
|
|
994
1027
|
return { code: 1, data: { modules, membership_plan: account.membership_plan || 'free' } };
|
|
@@ -7124,6 +7157,9 @@ export const archive_account_profile = async function (req) {
|
|
|
7124
7157
|
await db_module.save_couch_doc('xuda_accounts', account_doc);
|
|
7125
7158
|
}
|
|
7126
7159
|
|
|
7160
|
+
// UI-202
|
|
7161
|
+
log_profile_activity(uid, profile_id, 'archived', { by: 'user', reason: account_profile_doc.stat_reason }, app_id);
|
|
7162
|
+
|
|
7127
7163
|
return account_profile_save_ret;
|
|
7128
7164
|
} catch (err) {
|
|
7129
7165
|
return {
|
|
@@ -7157,6 +7193,10 @@ export const delete_account_profile = async function (req, job_id, headers) {
|
|
|
7157
7193
|
}
|
|
7158
7194
|
|
|
7159
7195
|
ai_msa.delete_depended_chats(uid, profile_id);
|
|
7196
|
+
|
|
7197
|
+
// UI-202: the last row this profile gets.
|
|
7198
|
+
log_profile_activity(uid, profile_id, 'deleted', { by: 'user', reason: account_profile_doc.stat_reason, note: 'the chats that hung off this profile were deleted with it' }, app_id);
|
|
7199
|
+
|
|
7160
7200
|
return account_profile_save_ret;
|
|
7161
7201
|
} catch (err) {
|
|
7162
7202
|
return {
|
|
@@ -7184,6 +7224,9 @@ export const unarchive_account_profile = async function (req) {
|
|
|
7184
7224
|
|
|
7185
7225
|
const account_profile_save_ret = await db_module.save_app_couch_doc(app_id, account_profile_doc);
|
|
7186
7226
|
|
|
7227
|
+
// UI-202
|
|
7228
|
+
log_profile_activity(uid, profile_id, 'unarchived', { by: 'user' }, app_id);
|
|
7229
|
+
|
|
7187
7230
|
return account_profile_save_ret;
|
|
7188
7231
|
}
|
|
7189
7232
|
} catch (err) {
|
|
@@ -7221,6 +7264,9 @@ export const create_account_profile = async function (req, job_id, headers) {
|
|
|
7221
7264
|
const save_ret = await db_module.save_app_couch_doc(app_id, doc);
|
|
7222
7265
|
// acc_obj.active_profile_id = save_ret.data.id;
|
|
7223
7266
|
|
|
7267
|
+
// UI-202: the first row of this profile's trail.
|
|
7268
|
+
log_profile_activity(uid, doc._id, 'created', { by: 'user', name: profile_name, type: account_type, main: !!main, mailbox: !!email_account_id }, app_id);
|
|
7269
|
+
|
|
7224
7270
|
// return { code: 55, data: save_ret };
|
|
7225
7271
|
return save_ret;
|
|
7226
7272
|
} catch (err) {
|
|
@@ -7257,12 +7303,17 @@ export const update_account_profile = async function (req, job_id, headers) {
|
|
|
7257
7303
|
}
|
|
7258
7304
|
|
|
7259
7305
|
let changes_arr = [];
|
|
7306
|
+
// UI-202: the previous values of the fields this edit touches, so the trail can say what
|
|
7307
|
+
// it changed FROM as well as to. Taken inside the same loop that detects the change, so
|
|
7308
|
+
// it costs nothing and cannot drift from changes_arr.
|
|
7309
|
+
const previous_values = {};
|
|
7260
7310
|
|
|
7261
7311
|
for (const key of account_profile_properties) {
|
|
7262
7312
|
let val = req[key];
|
|
7263
7313
|
if (typeof val === 'undefined') continue;
|
|
7264
7314
|
if (account_profile_doc[key] !== val) {
|
|
7265
7315
|
changes_arr.push(key);
|
|
7316
|
+
previous_values[key] = account_profile_doc[key];
|
|
7266
7317
|
account_profile_doc[key] = val;
|
|
7267
7318
|
}
|
|
7268
7319
|
}
|
|
@@ -7276,6 +7327,27 @@ export const update_account_profile = async function (req, job_id, headers) {
|
|
|
7276
7327
|
|
|
7277
7328
|
const save_ret = await db_module.save_app_couch_doc(app_id, account_profile_doc);
|
|
7278
7329
|
|
|
7330
|
+
// UI-202: one row naming the fields that moved, with the values of the ones that read
|
|
7331
|
+
// as values (a signature and an avatar url do not). Auto response gets its OWN row on
|
|
7332
|
+
// top: it is the switch that decides whether this profile answers mail by itself, so
|
|
7333
|
+
// "auto response" buried in a list of six field names is not good enough.
|
|
7334
|
+
const changed = changes_arr.map((key) => PROFILE_FIELD_LABELS[key] || key.replace(/_/g, ' '));
|
|
7335
|
+
const values = {};
|
|
7336
|
+
for (const key of changes_arr) {
|
|
7337
|
+
if (PROFILE_FIELDS_WITH_VALUES.has(key)) values[PROFILE_FIELD_LABELS[key] || key] = String(account_profile_doc[key] ?? '').slice(0, 120);
|
|
7338
|
+
}
|
|
7339
|
+
log_profile_activity(uid, _id, 'updated', { by: 'user', changed: [...new Set(changed)], values, previous_name: changes_arr.includes('profile_name') ? previous_values.profile_name : undefined }, app_id);
|
|
7340
|
+
|
|
7341
|
+
if (changes_arr.includes('auto_respond')) {
|
|
7342
|
+
log_profile_activity(
|
|
7343
|
+
uid,
|
|
7344
|
+
_id,
|
|
7345
|
+
account_profile_doc.auto_respond ? 'auto_respond_on' : 'auto_respond_off',
|
|
7346
|
+
{ by: 'user', mode: account_profile_doc.auto_respond_mode, agents: (account_profile_doc.auto_respond_agents || []).length },
|
|
7347
|
+
app_id
|
|
7348
|
+
);
|
|
7349
|
+
}
|
|
7350
|
+
|
|
7279
7351
|
return { code: 56, data: save_ret };
|
|
7280
7352
|
} catch (err) {
|
|
7281
7353
|
return { code: -56, data: err.message };
|
|
@@ -7371,6 +7443,135 @@ const log_contact_activity = async function (uid, contact_id, event, detail = {}
|
|
|
7371
7443
|
}
|
|
7372
7444
|
};
|
|
7373
7445
|
|
|
7446
|
+
// ─── UI-202: the account profile activity trail ───────────────────────────────────────
|
|
7447
|
+
// The fourth of the same family (contacts UI-134, agents and chats UI-194/195), same shape
|
|
7448
|
+
// because one panel renders all of them. A profile doc keeps the current answer to every
|
|
7449
|
+
// question and nothing about when it changed, and a profile is the thing that answers mail
|
|
7450
|
+
// on its own: "when was auto respond switched on", "which agents were attached", "who
|
|
7451
|
+
// changed the signature" had no trace at all.
|
|
7452
|
+
//
|
|
7453
|
+
// Append-only, never awaited, and a failure to write it can never fail the action it
|
|
7454
|
+
// records. Lives in the same app db as the profile.
|
|
7455
|
+
const log_profile_activity = async function (uid, profile_id, event, detail = {}, app_id) {
|
|
7456
|
+
try {
|
|
7457
|
+
if (!uid || !profile_id || !event) return null;
|
|
7458
|
+
const app = app_id || (await get_account_default_project_id(uid));
|
|
7459
|
+
if (!app) return null;
|
|
7460
|
+
|
|
7461
|
+
return await db_module.save_app_couch_doc_native(app, {
|
|
7462
|
+
_id: await _common.xuda_get_uuid('profile_activity'),
|
|
7463
|
+
docType: 'profile_activity',
|
|
7464
|
+
profile_id,
|
|
7465
|
+
uid,
|
|
7466
|
+
event,
|
|
7467
|
+
detail,
|
|
7468
|
+
ts: Date.now(),
|
|
7469
|
+
stat: 3,
|
|
7470
|
+
});
|
|
7471
|
+
} catch (err) {
|
|
7472
|
+
console.error('[account_module] profile activity not recorded:', event, profile_id, err?.message || err);
|
|
7473
|
+
return null;
|
|
7474
|
+
}
|
|
7475
|
+
};
|
|
7476
|
+
|
|
7477
|
+
// The cross-module door: ai_module regenerates a profile's avatar, team_module shares one.
|
|
7478
|
+
export const log_account_profile_activity = async function (req) {
|
|
7479
|
+
const { uid, profile_id, event, detail, app_id } = req || {};
|
|
7480
|
+
const ret = await log_profile_activity(uid, profile_id, event, detail || {}, app_id);
|
|
7481
|
+
return { code: ret ? 1 : 0, data: ret ? { profile_id, event } : 'not recorded' };
|
|
7482
|
+
};
|
|
7483
|
+
|
|
7484
|
+
// Field names the edit row prints, and the ones whose VALUE is safe and useful to print
|
|
7485
|
+
// with them. A signature is free text and an avatar is a url, so those travel as the fact
|
|
7486
|
+
// that they changed; a name, a mode or a model is the whole point of the row.
|
|
7487
|
+
const PROFILE_FIELD_LABELS = {
|
|
7488
|
+
profile_name: 'name',
|
|
7489
|
+
profile_signature: 'signature',
|
|
7490
|
+
email_account_id: 'mailbox',
|
|
7491
|
+
profile_picture: 'picture',
|
|
7492
|
+
profile_avatar: 'avatar',
|
|
7493
|
+
profile_picture_obj: 'picture',
|
|
7494
|
+
profile_avatar_obj: 'avatar',
|
|
7495
|
+
auto_respond: 'auto response',
|
|
7496
|
+
auto_respond_mode: 'auto response mode',
|
|
7497
|
+
auto_respond_agents: 'auto response agents',
|
|
7498
|
+
account_type: 'type',
|
|
7499
|
+
active_ai_model: 'model',
|
|
7500
|
+
ai_models: 'models',
|
|
7501
|
+
active_agents: 'agents',
|
|
7502
|
+
email_template: 'email template',
|
|
7503
|
+
};
|
|
7504
|
+
const PROFILE_FIELDS_WITH_VALUES = new Set(['profile_name', 'auto_respond_mode', 'account_type', 'active_ai_model']);
|
|
7505
|
+
|
|
7506
|
+
export const get_account_profile_activity = async function (req) {
|
|
7507
|
+
const { uid, profile_id } = req;
|
|
7508
|
+
try {
|
|
7509
|
+
if (!profile_id) throw new Error('profile_id is missing');
|
|
7510
|
+
const app_id = await get_account_default_project_id(uid);
|
|
7511
|
+
|
|
7512
|
+
let profile_doc;
|
|
7513
|
+
try {
|
|
7514
|
+
profile_doc = await db_module.get_app_couch_doc_native(app_id, profile_id);
|
|
7515
|
+
} catch (_) {
|
|
7516
|
+
profile_doc = null;
|
|
7517
|
+
}
|
|
7518
|
+
if (!profile_doc || profile_doc.docType !== 'account_profile') throw new Error(`profile ${profile_id} not found`);
|
|
7519
|
+
if (profile_doc.uid !== uid) throw new Error('Operation not allowed');
|
|
7520
|
+
|
|
7521
|
+
const recorded_ret = await db_module.find_app_couch_query(app_id, {
|
|
7522
|
+
selector: { docType: 'profile_activity', profile_id },
|
|
7523
|
+
limit: 500,
|
|
7524
|
+
});
|
|
7525
|
+
const recorded = (recorded_ret?.docs || []).map((d) => ({ event: d.event, detail: d.detail || {}, ts: d.ts, derived: false }));
|
|
7526
|
+
|
|
7527
|
+
const derived = [];
|
|
7528
|
+
const add = (event, ts, detail) => {
|
|
7529
|
+
if (!ts) return;
|
|
7530
|
+
if (recorded.some((r) => r.event === event)) return;
|
|
7531
|
+
derived.push({ event, detail, ts, derived: true });
|
|
7532
|
+
};
|
|
7533
|
+
|
|
7534
|
+
const created_ts = profile_doc.date_created_ts || profile_doc.ts;
|
|
7535
|
+
add('created', created_ts, { name: profile_doc.profile_name, type: profile_doc.account_type, main: !!profile_doc.main });
|
|
7536
|
+
if (profile_doc.email_account_id) add('mailbox_bound', profile_doc.ts || created_ts, {});
|
|
7537
|
+
if (profile_doc.auto_respond) add('auto_respond_on', profile_doc.ts || created_ts, { mode: profile_doc.auto_respond_mode, agents: (profile_doc.auto_respond_agents || []).length });
|
|
7538
|
+
if (profile_doc.profile_avatar) add('avatar_ready', profile_doc.profile_avatar_stat_ts || profile_doc.ts || created_ts, {});
|
|
7539
|
+
// UI-204: the two public surfaces, reconstructed from the flags they are stored as, so a
|
|
7540
|
+
// profile that has been taking messages from strangers for months says so today rather
|
|
7541
|
+
// than only from the next time somebody toggles it.
|
|
7542
|
+
if (profile_doc.widget_enabled) add('widget_on', profile_doc.ts || created_ts, {});
|
|
7543
|
+
if (profile_doc.contact_form_enabled) add('contact_form_on', profile_doc.ts || created_ts, {});
|
|
7544
|
+
if (profile_doc.shared_from_uid) add('shared_with_you', profile_doc.shared_ts || created_ts, { from_uid: profile_doc.shared_from_uid });
|
|
7545
|
+
if (profile_doc.stat === 5) add('archived', profile_doc.stat_ts, { reason: profile_doc.stat_reason });
|
|
7546
|
+
if (profile_doc.stat === 4) add('deleted', profile_doc.stat_ts, { reason: profile_doc.stat_reason });
|
|
7547
|
+
|
|
7548
|
+
const rows = [...recorded, ...derived].sort((a, b) => (b.ts || 0) - (a.ts || 0));
|
|
7549
|
+
|
|
7550
|
+
return {
|
|
7551
|
+
code: 1,
|
|
7552
|
+
data: {
|
|
7553
|
+
profile_id,
|
|
7554
|
+
current: {
|
|
7555
|
+
name: profile_doc.profile_name || null,
|
|
7556
|
+
stat: profile_doc.stat,
|
|
7557
|
+
main: !!profile_doc.main,
|
|
7558
|
+
type: profile_doc.account_type || null,
|
|
7559
|
+
auto_respond: !!profile_doc.auto_respond,
|
|
7560
|
+
auto_respond_mode: profile_doc.auto_respond_mode || null,
|
|
7561
|
+
mailbox: !!profile_doc.email_account_id,
|
|
7562
|
+
model: profile_doc.active_ai_model || null,
|
|
7563
|
+
agents: (profile_doc.active_agents || []).length,
|
|
7564
|
+
widget: !!profile_doc.widget_enabled,
|
|
7565
|
+
contact_form: !!profile_doc.contact_form_enabled,
|
|
7566
|
+
},
|
|
7567
|
+
rows,
|
|
7568
|
+
},
|
|
7569
|
+
};
|
|
7570
|
+
} catch (err) {
|
|
7571
|
+
return { code: -25, data: err.message };
|
|
7572
|
+
}
|
|
7573
|
+
};
|
|
7574
|
+
|
|
7374
7575
|
export const save_contact = async function (uid, contact_doc) {
|
|
7375
7576
|
const account_profile_info = await get_active_account_profile_info(uid);
|
|
7376
7577
|
const contact_ret = await db_module.save_app_couch_doc(account_profile_info.app_id, contact_doc);
|
package/index_ms.mjs
CHANGED
|
@@ -493,6 +493,14 @@ export const get_contact = async function (...args) {
|
|
|
493
493
|
return await broker.send_to_queue("get_contact", ...args);
|
|
494
494
|
};
|
|
495
495
|
|
|
496
|
+
export const log_account_profile_activity = async function (...args) {
|
|
497
|
+
return await broker.send_to_queue("log_account_profile_activity", ...args);
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
export const get_account_profile_activity = async function (...args) {
|
|
501
|
+
return await broker.send_to_queue("get_account_profile_activity", ...args);
|
|
502
|
+
};
|
|
503
|
+
|
|
496
504
|
export const save_contact = async function (...args) {
|
|
497
505
|
return await broker.send_to_queue("save_contact", ...args);
|
|
498
506
|
};
|
package/index_msa.mjs
CHANGED
|
@@ -493,6 +493,14 @@ export const get_contact = function (...args) {
|
|
|
493
493
|
broker.send_to_queue_async("get_contact", ...args);
|
|
494
494
|
};
|
|
495
495
|
|
|
496
|
+
export const log_account_profile_activity = function (...args) {
|
|
497
|
+
broker.send_to_queue_async("log_account_profile_activity", ...args);
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
export const get_account_profile_activity = function (...args) {
|
|
501
|
+
broker.send_to_queue_async("get_account_profile_activity", ...args);
|
|
502
|
+
};
|
|
503
|
+
|
|
496
504
|
export const save_contact = function (...args) {
|
|
497
505
|
broker.send_to_queue_async("save_contact", ...args);
|
|
498
506
|
};
|