@xuda.io/account_module 1.2.2313 → 1.2.2315

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,10 +670,33 @@ 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
- { key: 'finance', scope: 'account', field: 'finance_plan', default_plan: 'finance_free' },
688
+ // UI-192: an account can be USING Finance without ever having been through the picker —
689
+ // finance accounts predate the ladder, and info@xuda.ai holds two of them with no
690
+ // `finance_plan` at all. Reporting that as "not switched on" put the module behind a
691
+ // pitch for the customer already invoicing through it. Both markers sit on the account
692
+ // document, so this costs no extra read.
693
+ {
694
+ key: 'finance',
695
+ scope: 'account',
696
+ field: 'finance_plan',
697
+ default_plan: 'finance_free',
698
+ is_activated: (a) => !!(a.finance_plan || a.finance_plan_changed || a.finance_active || a.finance_account_id),
699
+ },
677
700
  // The desk screen has its own copy of this ladder (UI-82), because Tickets is
678
701
  // switched on there as well as here. Both call tickets_set_plan, so the plan
679
702
  // is one line item on the consolidated subscription either way.
@@ -681,12 +704,17 @@ const _MODULE_SUBSCRIPTIONS = [
681
704
  // UI-104: email is its own product, billed per mailbox tier. Its plan lives on the
682
705
  // account as an OBJECT (`email_plan.tier`), which is why it resolves rather than
683
706
  // reading a field, and `set_email_plan` is what moves it.
707
+ // UI-192: and its "the customer chose this" marker is `email_plan.activated_ts`, not
708
+ // an `email_plan_changed` field, which is email_policy's own `chosen`. The generic
709
+ // stamp lookup never found anything, so an account on a deliberately free mailbox
710
+ // tier read as switched off.
684
711
  {
685
712
  key: 'email',
686
713
  scope: 'account',
687
714
  field: 'email_plan',
688
715
  default_plan: 'email_free',
689
716
  resolve: (a) => (a && a.email_plan && a.email_plan.tier ? `email_${a.email_plan.tier}` : ''),
717
+ changed_of: (a) => (a && a.email_plan && a.email_plan.activated_ts) || null,
690
718
  },
691
719
  // Auto response is the single gate for whether the AI answers, on every channel
692
720
  // (chat, the public chat widget, email and the phone). It is account scoped and
@@ -930,13 +958,14 @@ export const get_module_subscriptions = async function (req = {}) {
930
958
  if (ar.code < 0 || !ar.data) return { code: -404, data: 'account not found' };
931
959
  const account = ar.data;
932
960
  const plans = _conf.PLAN_OBJ || {};
933
- // A category is only offerable once its paid tiers carry a real Stripe
934
- // price, otherwise "activate" would set a plan nobody is billed for. A module
935
- // whose money is NOT a plan line item says so itself (phone), because for those
936
- // an absent price_id is the design, not a gap.
937
- const billable = (m) =>
938
- m.billable === true ||
939
- Object.values(plans).some((p) => p.category === m.key && Number(p.price) > 0 && p.price_id && !/TODO/.test(p.price_id));
961
+ // A category is offerable once it has a paid tier with a real amount on it. It used to
962
+ // also demand a hand-made Stripe price behind that tier, which is what left Commerce,
963
+ // Shipping and Finance reading "Coming soon" on the create screen while their products
964
+ // were finished and shipped: stripe_module now mints the price from the catalog the
965
+ // first time a tier is bought (_resolve_plan_price), so a placeholder price id is no
966
+ // longer a reason to take a module off the shelf. A module whose money is not a plan
967
+ // line item at all still says so itself (phone charges the number).
968
+ const billable = (m) => m.billable === true || Object.values(plans).some((p) => p.category === m.key && Number(p.price) > 0);
940
969
 
941
970
  const resource_items = await _module_resource_items(uid);
942
971
 
@@ -952,9 +981,14 @@ export const get_module_subscriptions = async function (req = {}) {
952
981
  // `plan_of` keeps reporting no plan, exactly as before.
953
982
  const plan_id = m.plan_of ? m.plan_of(account) : '';
954
983
  const plan = plans[plan_id] || {};
984
+ const active = m.is_active ? !!m.is_active(account, base.items) : m.scope === 'metered' || base.items.length > 0;
955
985
  return {
956
986
  ...base,
957
- active: m.is_active ? !!m.is_active(account, base.items) : m.scope === 'metered' || base.items.length > 0,
987
+ active,
988
+ // UI-192: a per-resource module has no free tier to be switched on at, so
989
+ // paying for one of the things IS being switched on. Sent all the same, so
990
+ // every surface can ask the one question rather than two.
991
+ activated: m.is_activated ? !!m.is_activated(account, base.items) : active,
958
992
  included: m.scope === 'metered',
959
993
  plan_id,
960
994
  plan_name: plan.name || '',
@@ -977,6 +1011,11 @@ export const get_module_subscriptions = async function (req = {}) {
977
1011
  ? { to_plan: pc.to_plan, to_name: plans[pc.to_plan]?.name || pc.to_plan, effective: pc.effective || null }
978
1012
  : null;
979
1013
  const price = Number(plan.price) || 0;
1014
+ // Every *_set_plan method stamps `<field>_changed` when the customer picks a
1015
+ // tier, Free included, so this is the general answer to "have they been through
1016
+ // the picker". A module whose marker is somewhere else brings its own resolver.
1017
+ const changed_ts = (m.changed_of ? m.changed_of(account) : account[`${m.field}_changed`]) || null;
1018
+ const active = m.is_active ? !!m.is_active(account, base.items) : price > 0;
980
1019
  return {
981
1020
  ...base,
982
1021
  plan_id,
@@ -985,10 +1024,16 @@ export const get_module_subscriptions = async function (req = {}) {
985
1024
  // UI-110: paying is the default proof that a module is on, but a module that
986
1025
  // records the customer CHOOSING a tier says so itself, because a deliberate
987
1026
  // 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,
1027
+ active,
1028
+ // UI-192 (Boaz, on the create sheet's switches): "switched on" as its own
1029
+ // answer, so no surface has to reassemble it out of `active` and `changed_ts`
1030
+ // and get it differently from the next one. Picking a FREE tier is switching
1031
+ // the module on: it is a real tier, it opens the module's screen and it puts
1032
+ // the module in the menu, and the switch that did it has to stay flipped.
1033
+ activated: m.is_activated ? !!m.is_activated(account, base.items) : !!(active || changed_ts),
989
1034
  included: false,
990
1035
  pending,
991
- changed_ts: account[`${m.field}_changed`] || null,
1036
+ changed_ts,
992
1037
  };
993
1038
  });
994
1039
  return { code: 1, data: { modules, membership_plan: account.membership_plan || 'free' } };
@@ -7124,6 +7169,9 @@ export const archive_account_profile = async function (req) {
7124
7169
  await db_module.save_couch_doc('xuda_accounts', account_doc);
7125
7170
  }
7126
7171
 
7172
+ // UI-202
7173
+ log_profile_activity(uid, profile_id, 'archived', { by: 'user', reason: account_profile_doc.stat_reason }, app_id);
7174
+
7127
7175
  return account_profile_save_ret;
7128
7176
  } catch (err) {
7129
7177
  return {
@@ -7157,6 +7205,10 @@ export const delete_account_profile = async function (req, job_id, headers) {
7157
7205
  }
7158
7206
 
7159
7207
  ai_msa.delete_depended_chats(uid, profile_id);
7208
+
7209
+ // UI-202: the last row this profile gets.
7210
+ 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);
7211
+
7160
7212
  return account_profile_save_ret;
7161
7213
  } catch (err) {
7162
7214
  return {
@@ -7184,6 +7236,9 @@ export const unarchive_account_profile = async function (req) {
7184
7236
 
7185
7237
  const account_profile_save_ret = await db_module.save_app_couch_doc(app_id, account_profile_doc);
7186
7238
 
7239
+ // UI-202
7240
+ log_profile_activity(uid, profile_id, 'unarchived', { by: 'user' }, app_id);
7241
+
7187
7242
  return account_profile_save_ret;
7188
7243
  }
7189
7244
  } catch (err) {
@@ -7221,6 +7276,9 @@ export const create_account_profile = async function (req, job_id, headers) {
7221
7276
  const save_ret = await db_module.save_app_couch_doc(app_id, doc);
7222
7277
  // acc_obj.active_profile_id = save_ret.data.id;
7223
7278
 
7279
+ // UI-202: the first row of this profile's trail.
7280
+ log_profile_activity(uid, doc._id, 'created', { by: 'user', name: profile_name, type: account_type, main: !!main, mailbox: !!email_account_id }, app_id);
7281
+
7224
7282
  // return { code: 55, data: save_ret };
7225
7283
  return save_ret;
7226
7284
  } catch (err) {
@@ -7257,12 +7315,17 @@ export const update_account_profile = async function (req, job_id, headers) {
7257
7315
  }
7258
7316
 
7259
7317
  let changes_arr = [];
7318
+ // UI-202: the previous values of the fields this edit touches, so the trail can say what
7319
+ // it changed FROM as well as to. Taken inside the same loop that detects the change, so
7320
+ // it costs nothing and cannot drift from changes_arr.
7321
+ const previous_values = {};
7260
7322
 
7261
7323
  for (const key of account_profile_properties) {
7262
7324
  let val = req[key];
7263
7325
  if (typeof val === 'undefined') continue;
7264
7326
  if (account_profile_doc[key] !== val) {
7265
7327
  changes_arr.push(key);
7328
+ previous_values[key] = account_profile_doc[key];
7266
7329
  account_profile_doc[key] = val;
7267
7330
  }
7268
7331
  }
@@ -7276,6 +7339,27 @@ export const update_account_profile = async function (req, job_id, headers) {
7276
7339
 
7277
7340
  const save_ret = await db_module.save_app_couch_doc(app_id, account_profile_doc);
7278
7341
 
7342
+ // UI-202: one row naming the fields that moved, with the values of the ones that read
7343
+ // as values (a signature and an avatar url do not). Auto response gets its OWN row on
7344
+ // top: it is the switch that decides whether this profile answers mail by itself, so
7345
+ // "auto response" buried in a list of six field names is not good enough.
7346
+ const changed = changes_arr.map((key) => PROFILE_FIELD_LABELS[key] || key.replace(/_/g, ' '));
7347
+ const values = {};
7348
+ for (const key of changes_arr) {
7349
+ if (PROFILE_FIELDS_WITH_VALUES.has(key)) values[PROFILE_FIELD_LABELS[key] || key] = String(account_profile_doc[key] ?? '').slice(0, 120);
7350
+ }
7351
+ 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);
7352
+
7353
+ if (changes_arr.includes('auto_respond')) {
7354
+ log_profile_activity(
7355
+ uid,
7356
+ _id,
7357
+ account_profile_doc.auto_respond ? 'auto_respond_on' : 'auto_respond_off',
7358
+ { by: 'user', mode: account_profile_doc.auto_respond_mode, agents: (account_profile_doc.auto_respond_agents || []).length },
7359
+ app_id
7360
+ );
7361
+ }
7362
+
7279
7363
  return { code: 56, data: save_ret };
7280
7364
  } catch (err) {
7281
7365
  return { code: -56, data: err.message };
@@ -7371,6 +7455,135 @@ const log_contact_activity = async function (uid, contact_id, event, detail = {}
7371
7455
  }
7372
7456
  };
7373
7457
 
7458
+ // ─── UI-202: the account profile activity trail ───────────────────────────────────────
7459
+ // The fourth of the same family (contacts UI-134, agents and chats UI-194/195), same shape
7460
+ // because one panel renders all of them. A profile doc keeps the current answer to every
7461
+ // question and nothing about when it changed, and a profile is the thing that answers mail
7462
+ // on its own: "when was auto respond switched on", "which agents were attached", "who
7463
+ // changed the signature" had no trace at all.
7464
+ //
7465
+ // Append-only, never awaited, and a failure to write it can never fail the action it
7466
+ // records. Lives in the same app db as the profile.
7467
+ const log_profile_activity = async function (uid, profile_id, event, detail = {}, app_id) {
7468
+ try {
7469
+ if (!uid || !profile_id || !event) return null;
7470
+ const app = app_id || (await get_account_default_project_id(uid));
7471
+ if (!app) return null;
7472
+
7473
+ return await db_module.save_app_couch_doc_native(app, {
7474
+ _id: await _common.xuda_get_uuid('profile_activity'),
7475
+ docType: 'profile_activity',
7476
+ profile_id,
7477
+ uid,
7478
+ event,
7479
+ detail,
7480
+ ts: Date.now(),
7481
+ stat: 3,
7482
+ });
7483
+ } catch (err) {
7484
+ console.error('[account_module] profile activity not recorded:', event, profile_id, err?.message || err);
7485
+ return null;
7486
+ }
7487
+ };
7488
+
7489
+ // The cross-module door: ai_module regenerates a profile's avatar, team_module shares one.
7490
+ export const log_account_profile_activity = async function (req) {
7491
+ const { uid, profile_id, event, detail, app_id } = req || {};
7492
+ const ret = await log_profile_activity(uid, profile_id, event, detail || {}, app_id);
7493
+ return { code: ret ? 1 : 0, data: ret ? { profile_id, event } : 'not recorded' };
7494
+ };
7495
+
7496
+ // Field names the edit row prints, and the ones whose VALUE is safe and useful to print
7497
+ // with them. A signature is free text and an avatar is a url, so those travel as the fact
7498
+ // that they changed; a name, a mode or a model is the whole point of the row.
7499
+ const PROFILE_FIELD_LABELS = {
7500
+ profile_name: 'name',
7501
+ profile_signature: 'signature',
7502
+ email_account_id: 'mailbox',
7503
+ profile_picture: 'picture',
7504
+ profile_avatar: 'avatar',
7505
+ profile_picture_obj: 'picture',
7506
+ profile_avatar_obj: 'avatar',
7507
+ auto_respond: 'auto response',
7508
+ auto_respond_mode: 'auto response mode',
7509
+ auto_respond_agents: 'auto response agents',
7510
+ account_type: 'type',
7511
+ active_ai_model: 'model',
7512
+ ai_models: 'models',
7513
+ active_agents: 'agents',
7514
+ email_template: 'email template',
7515
+ };
7516
+ const PROFILE_FIELDS_WITH_VALUES = new Set(['profile_name', 'auto_respond_mode', 'account_type', 'active_ai_model']);
7517
+
7518
+ export const get_account_profile_activity = async function (req) {
7519
+ const { uid, profile_id } = req;
7520
+ try {
7521
+ if (!profile_id) throw new Error('profile_id is missing');
7522
+ const app_id = await get_account_default_project_id(uid);
7523
+
7524
+ let profile_doc;
7525
+ try {
7526
+ profile_doc = await db_module.get_app_couch_doc_native(app_id, profile_id);
7527
+ } catch (_) {
7528
+ profile_doc = null;
7529
+ }
7530
+ if (!profile_doc || profile_doc.docType !== 'account_profile') throw new Error(`profile ${profile_id} not found`);
7531
+ if (profile_doc.uid !== uid) throw new Error('Operation not allowed');
7532
+
7533
+ const recorded_ret = await db_module.find_app_couch_query(app_id, {
7534
+ selector: { docType: 'profile_activity', profile_id },
7535
+ limit: 500,
7536
+ });
7537
+ const recorded = (recorded_ret?.docs || []).map((d) => ({ event: d.event, detail: d.detail || {}, ts: d.ts, derived: false }));
7538
+
7539
+ const derived = [];
7540
+ const add = (event, ts, detail) => {
7541
+ if (!ts) return;
7542
+ if (recorded.some((r) => r.event === event)) return;
7543
+ derived.push({ event, detail, ts, derived: true });
7544
+ };
7545
+
7546
+ const created_ts = profile_doc.date_created_ts || profile_doc.ts;
7547
+ add('created', created_ts, { name: profile_doc.profile_name, type: profile_doc.account_type, main: !!profile_doc.main });
7548
+ if (profile_doc.email_account_id) add('mailbox_bound', profile_doc.ts || created_ts, {});
7549
+ 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 });
7550
+ if (profile_doc.profile_avatar) add('avatar_ready', profile_doc.profile_avatar_stat_ts || profile_doc.ts || created_ts, {});
7551
+ // UI-204: the two public surfaces, reconstructed from the flags they are stored as, so a
7552
+ // profile that has been taking messages from strangers for months says so today rather
7553
+ // than only from the next time somebody toggles it.
7554
+ if (profile_doc.widget_enabled) add('widget_on', profile_doc.ts || created_ts, {});
7555
+ if (profile_doc.contact_form_enabled) add('contact_form_on', profile_doc.ts || created_ts, {});
7556
+ if (profile_doc.shared_from_uid) add('shared_with_you', profile_doc.shared_ts || created_ts, { from_uid: profile_doc.shared_from_uid });
7557
+ if (profile_doc.stat === 5) add('archived', profile_doc.stat_ts, { reason: profile_doc.stat_reason });
7558
+ if (profile_doc.stat === 4) add('deleted', profile_doc.stat_ts, { reason: profile_doc.stat_reason });
7559
+
7560
+ const rows = [...recorded, ...derived].sort((a, b) => (b.ts || 0) - (a.ts || 0));
7561
+
7562
+ return {
7563
+ code: 1,
7564
+ data: {
7565
+ profile_id,
7566
+ current: {
7567
+ name: profile_doc.profile_name || null,
7568
+ stat: profile_doc.stat,
7569
+ main: !!profile_doc.main,
7570
+ type: profile_doc.account_type || null,
7571
+ auto_respond: !!profile_doc.auto_respond,
7572
+ auto_respond_mode: profile_doc.auto_respond_mode || null,
7573
+ mailbox: !!profile_doc.email_account_id,
7574
+ model: profile_doc.active_ai_model || null,
7575
+ agents: (profile_doc.active_agents || []).length,
7576
+ widget: !!profile_doc.widget_enabled,
7577
+ contact_form: !!profile_doc.contact_form_enabled,
7578
+ },
7579
+ rows,
7580
+ },
7581
+ };
7582
+ } catch (err) {
7583
+ return { code: -25, data: err.message };
7584
+ }
7585
+ };
7586
+
7374
7587
  export const save_contact = async function (uid, contact_doc) {
7375
7588
  const account_profile_info = await get_active_account_profile_info(uid);
7376
7589
  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.2315",
4
4
  "description": "Xuda Account Server Module",
5
5
  "main": "index.mjs",
6
6
  "dependencies": {