@xuda.io/account_module 1.2.2304 → 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.
- package/index.mjs +105 -27
- 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
|
-
//
|
|
592
|
-
// (
|
|
593
|
-
|
|
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 = {
|
|
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
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
const
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
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:
|
|
622
|
-
plan_name:
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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);
|
|
@@ -8208,6 +8262,30 @@ const _nl_one = async (db, docType) => {
|
|
|
8208
8262
|
}
|
|
8209
8263
|
})();
|
|
8210
8264
|
|
|
8265
|
+
// The banner reconcilers (_ensure_billing_hold_banner, _ensure_bounce_suspension_banner,
|
|
8266
|
+
// _clear_bounce_suspension_banner, and the confirm_email cleanup in verify_account) all
|
|
8267
|
+
// select xuda_notification on {uid, topic, read}. With no index Couch full-scans the whole
|
|
8268
|
+
// notification DB on EVERY get_account_data: "No matching index found" plus a full-DB
|
|
8269
|
+
// examine per dashboard load. uid leads (most selective), then topic, then read; the extra
|
|
8270
|
+
// docType / delivery_method clauses those callers add are cheap in-memory filters once
|
|
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.
|
|
8275
|
+
(async () => {
|
|
8276
|
+
try {
|
|
8277
|
+
const ret = await db_module.create_couch_index('xuda_notification', {
|
|
8278
|
+
index: { fields: ['uid', 'topic', 'read'] },
|
|
8279
|
+
name: 'idx_notif_uid_topic_read',
|
|
8280
|
+
ddoc: 'idx_notif_uid_topic_read',
|
|
8281
|
+
type: 'json',
|
|
8282
|
+
});
|
|
8283
|
+
if (ret?.error) console.error('[account] xuda_notification uid/topic/read index create failed:', ret.error);
|
|
8284
|
+
} catch (err) {
|
|
8285
|
+
console.error('[account] xuda_notification uid/topic/read index init failed:', err?.message || err);
|
|
8286
|
+
}
|
|
8287
|
+
})();
|
|
8288
|
+
|
|
8211
8289
|
const _nl_fashion_url = (p) => {
|
|
8212
8290
|
const rel = p.custom_url || (p.slug ? '/' + p.slug : '');
|
|
8213
8291
|
return `https://xuda.fashion${rel}`;
|