@xuda.io/account_module 1.2.2305 → 1.2.2306

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.
Files changed (2) hide show
  1. package/index.mjs +85 -29
  2. package/package.json +1 -1
package/index.mjs CHANGED
@@ -588,43 +588,88 @@ const _MODULE_SUBSCRIPTIONS = [
588
588
  // switched on there as well as here. Both call tickets_set_plan, so the plan
589
589
  // is one line item on the consolidated subscription either way.
590
590
  { key: 'tickets', scope: 'account', field: 'tickets_plan', default_plan: 'tickets_free' },
591
- // No free tier and no account field: the entitlement IS the subscription item
592
- // (or a paid membership, which includes the standard tier at no charge).
593
- { key: 'bot_protection', scope: 'account' },
591
+ // Auto response is the single gate for whether the AI answers, on every channel
592
+ // (chat, the public chat widget, email and the phone). It is account scoped and
593
+ // billed once however many scenarios are written, so it is a plain line item
594
+ // here rather than a per-resource charge like phone or email.
595
+ { key: 'auto_response', scope: 'account', field: 'auto_response_plan', default_plan: 'auto_response_free' },
596
+ // Four-tier ladder since 2026-08-06 (free / external / api / custom rules),
597
+ // chosen on the Bot Protection Manager's Plans tab, which calls
598
+ // bot_protection_set_plan. Free holds no line item at all.
599
+ { key: 'bot_protection', scope: 'account', field: 'bot_protection_plan', default_plan: 'bot_protection_free' },
594
600
  { key: 'static_website', scope: 'resource' },
595
601
  { key: 'profile_phone', scope: 'resource' },
602
+ // UI-96: the hosted resources. Not PLAN_OBJ categories, they are billed per resource
603
+ // from `app_cost` on the app doc, but they are money on the same invoice, so the card
604
+ // that answers "what am I paying for" has to show them.
605
+ { key: 'vps', scope: 'resource' },
606
+ { key: 'datacenter', scope: 'resource' },
607
+ { key: 'project', scope: 'resource' },
608
+ { key: 'preview', scope: 'resource' },
609
+ { key: 'instance', scope: 'resource' },
610
+ { key: 'backup', scope: 'resource' },
596
611
  // Xuda Verify (verify_module) has no PLAN_OBJ category: every check is metered
597
612
  // per call into verify_meter and nothing is charged to the account today.
598
613
  { key: 'trust_center', scope: 'metered' },
599
614
  ];
600
615
 
616
+ // app_type -> the row it belongs under. An app type that is not here is not a
617
+ // recurring hosted charge (or is not something the customer thinks of as one), so it
618
+ // simply does not appear rather than landing in a catch-all.
619
+ const _HOSTING_GROUP = {
620
+ vps: 'vps',
621
+ datacenter: 'datacenter',
622
+ master: 'project',
623
+ preview: 'preview',
624
+ instance: 'instance',
625
+ backup: 'backup',
626
+ static_website: 'static_website',
627
+ };
628
+
601
629
  // The individual paid things inside a per-resource module, so the billing
602
630
  // screen can be the ONE place that shows everything the account pays for.
603
631
  // A site or a number on a free tier is not a subscription and is left out.
604
632
  const _module_resource_items = async (uid) => {
605
- const items = { static_website: [], profile_phone: [] };
606
-
633
+ const items = {};
634
+ for (const m of _MODULE_SUBSCRIPTIONS) if (m.scope === 'resource') items[m.key] = [];
635
+
636
+ // UI-96: every hosted resource is a monthly charge, so the card walks the account's
637
+ // apps the way BILLING itself does (the `user_apps` view plus `is_billable` from
638
+ // deploy_module/cost.mjs, which is what get_billing_metrics uses) and groups the live
639
+ // ones by kind. Reading `app_cost.monthly_total` rather than re-deriving a price from
640
+ // the plan catalog is what keeps this card and the invoice from disagreeing.
607
641
  try {
608
- const ret = await db_module.find_couch_query(
609
- 'xuda_master',
610
- { selector: { docType: 'app', app_type: 'static_website', app_uId: uid }, limit: 1000 },
611
- true,
612
- );
613
- for (const app of ret?.docs || []) {
614
- const tier = app.static_website?.plan_tier || app.deploy_data?.static_website_hosting_plan || 'free';
615
- const plan = _conf.PLAN_OBJ?.[`static_website_${tier}`];
616
- const price = Number(plan?.price) || 0;
617
- if (!price) continue;
618
- items.static_website.push({
642
+ const { is_billable, format_app_cost_summary } = await import(`${module_path}/deploy_module/cost.mjs`);
643
+ const apps_ret = await db_module.get_couch_view('xuda_master', 'user_apps', {
644
+ startkey: [uid, ''],
645
+ endkey: [uid, 'ZZZZZ'],
646
+ include_docs: true,
647
+ });
648
+ for (const row of apps_ret?.data?.rows || []) {
649
+ const app = row.doc;
650
+ if (!app || !is_billable(app)) continue;
651
+ // Billing's own definition of live: status 4 and up is gone, and a terminated
652
+ // deployment stops costing. A deleted website was being counted before this.
653
+ if (Number(app.app_status_code) >= 4) continue;
654
+ if (app.app_cost?.terminated_ts) continue;
655
+ const key = _HOSTING_GROUP[app.app_type];
656
+ if (!key || !items[key]) continue;
657
+ let summary = '';
658
+ try {
659
+ summary = format_app_cost_summary(app) || '';
660
+ } catch (e) {
661
+ summary = '';
662
+ }
663
+ items[key].push({
619
664
  id: app._id,
620
665
  label: app.app_name || app.name || app.deploy_data?.domain || app._id,
621
- plan_id: plan.id || `static_website_${tier}`,
622
- plan_name: plan.name || tier,
623
- price,
666
+ plan_id: app.app_cost?.app_server_type || app.app_type,
667
+ plan_name: summary || app.app_cost?.app_server_type || '',
668
+ price: Number(app.app_cost?.monthly_total) || 0,
624
669
  });
625
670
  }
626
671
  } catch (e) {
627
- console.error('[get_module_subscriptions static_website]', e.message);
672
+ console.error('[get_module_subscriptions hosting]', e.message);
628
673
  }
629
674
 
630
675
  try {
@@ -674,7 +719,6 @@ export const get_module_subscriptions = async function (req = {}) {
674
719
  if (ar.code < 0 || !ar.data) return { code: -404, data: 'account not found' };
675
720
  const account = ar.data;
676
721
  const plans = _conf.PLAN_OBJ || {};
677
- const is_member = !!(account.membership_plan && account.membership_plan !== 'free');
678
722
  // A category is only offerable once its paid tiers carry a real Stripe
679
723
  // price, otherwise "activate" would set a plan nobody is billed for.
680
724
  const billable = (key) =>
@@ -684,10 +728,6 @@ export const get_module_subscriptions = async function (req = {}) {
684
728
 
685
729
  const modules = _MODULE_SUBSCRIPTIONS.map((m) => {
686
730
  const base = { key: m.key, scope: m.scope, billable: billable(m.key), items: resource_items[m.key] || [] };
687
- if (m.key === 'bot_protection') {
688
- const has_item = !!account.stripe_subscription_items?.bot_protection;
689
- return { ...base, active: has_item || is_member, included: is_member && !has_item, plan_id: '', plan_name: '', price: 0 };
690
- }
691
731
  if (m.scope === 'resource' || m.scope === 'metered') {
692
732
  const price = base.items.reduce((sum, i) => sum + Number(i.price || 0), 0);
693
733
  return {
@@ -4052,8 +4092,20 @@ const get_contact_background = function (doc) {
4052
4092
  return ret;
4053
4093
  };
4054
4094
 
4095
+ // The card's background pattern doubles as a verification signal: L0..L4 each
4096
+ // have their own tile, distinguishable by density and brightness as well as by
4097
+ // the level token, so the level reads at a glance without another badge. Level
4098
+ // comes from account_info.verify_level, the projection verify_module writes on
4099
+ // every recompute; anything missing or out of range reads as L0, which is the
4100
+ // honest default for an account we have not scored.
4101
+ const level_pattern = function (doc) {
4102
+ const raw = doc?.verify_level ?? doc?.account_info?.verify_level ?? doc?.user_contact?.verify_level;
4103
+ const level = Number.isFinite(Number(raw)) ? Math.max(0, Math.min(4, Math.trunc(Number(raw)))) : 0;
4104
+ return `level-${level}-pattern.png`;
4105
+ };
4106
+
4055
4107
  const get_contact_pattern = async function (doc) {
4056
- let ret = `default-pattern.png`;
4108
+ let ret = level_pattern(doc);
4057
4109
 
4058
4110
  if (doc?.shared_from_uid) {
4059
4111
  const shared_from_uid_ret = await get_account_name({ uid_query: doc.shared_from_uid });
@@ -6293,7 +6345,9 @@ export const get_account_profile_info = async function (uid, contact_profile_doc
6293
6345
  }
6294
6346
 
6295
6347
  const get_pattern = async function (doc) {
6296
- let ret = `default-pattern.png`;
6348
+ // Same level-based default as get_contact_pattern, so a card shows the same
6349
+ // verification tile whichever path built it.
6350
+ let ret = level_pattern(doc);
6297
6351
 
6298
6352
  if (doc.shared_from_uid) {
6299
6353
  // const account_profile_doc = await db_module.get_couch_doc_native('xuda_accounts', doc.share_item_id);
@@ -8214,8 +8268,10 @@ const _nl_one = async (db, docType) => {
8214
8268
  // notification DB on EVERY get_account_data: "No matching index found" plus a full-DB
8215
8269
  // examine per dashboard load. uid leads (most selective), then topic, then read; the extra
8216
8270
  // docType / delivery_method clauses those callers add are cheap in-memory filters once
8217
- // uid + topic have narrowed the set to a handful. Unlike the content DBs above,
8218
- // xuda_notification is node-local on every box, so this runs unconditionally.
8271
+ // uid + topic have narrowed the set to a handful. Not host-gated like the content DBs
8272
+ // above: xuda_notification exists on every box, so every node should ensure the index.
8273
+ // The fleet boxes replicate it through the master hub (so the PUT no-ops once one node
8274
+ // has it) but dev is outside that hub and only gets the index from its own run.
8219
8275
  (async () => {
8220
8276
  try {
8221
8277
  const ret = await db_module.create_couch_index('xuda_notification', {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xuda.io/account_module",
3
- "version": "1.2.2305",
3
+ "version": "1.2.2306",
4
4
  "description": "Xuda Account Server Module",
5
5
  "main": "index.mjs",
6
6
  "dependencies": {