@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 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
- { key: 'commerce', scope: 'account', field: 'commerce_plan', default_plan: 'commerce_free' },
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: m.is_active ? !!m.is_active(account, base.items) : m.scope === 'metered' || base.items.length > 0,
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: m.is_active ? !!m.is_active(account, base.items) : price > 0,
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: account[`${m.field}_changed`] || null,
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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xuda.io/account_module",
3
- "version": "1.2.2313",
3
+ "version": "1.2.2314",
4
4
  "description": "Xuda Account Server Module",
5
5
  "main": "index.mjs",
6
6
  "dependencies": {