@xuda.io/account_module 1.2.2305 → 1.2.2307
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 +147 -24
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -588,43 +588,151 @@ 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' },
|
|
611
|
+
// UI-97: a registered domain renews yearly and a dedicated IPv4 is a monthly line,
|
|
612
|
+
// so both carry their own `cycle` on each item rather than being assumed monthly.
|
|
613
|
+
{ key: 'domain', scope: 'resource' },
|
|
614
|
+
{ key: 'ipv4', scope: 'resource' },
|
|
596
615
|
// Xuda Verify (verify_module) has no PLAN_OBJ category: every check is metered
|
|
597
616
|
// per call into verify_meter and nothing is charged to the account today.
|
|
598
617
|
{ key: 'trust_center', scope: 'metered' },
|
|
599
618
|
];
|
|
600
619
|
|
|
620
|
+
// app_type -> the row it belongs under. An app type that is not here is not a
|
|
621
|
+
// recurring hosted charge (or is not something the customer thinks of as one), so it
|
|
622
|
+
// simply does not appear rather than landing in a catch-all.
|
|
623
|
+
const _HOSTING_GROUP = {
|
|
624
|
+
vps: 'vps',
|
|
625
|
+
datacenter: 'datacenter',
|
|
626
|
+
master: 'project',
|
|
627
|
+
preview: 'preview',
|
|
628
|
+
instance: 'instance',
|
|
629
|
+
backup: 'backup',
|
|
630
|
+
static_website: 'static_website',
|
|
631
|
+
};
|
|
632
|
+
|
|
601
633
|
// The individual paid things inside a per-resource module, so the billing
|
|
602
634
|
// screen can be the ONE place that shows everything the account pays for.
|
|
603
635
|
// A site or a number on a free tier is not a subscription and is left out.
|
|
604
636
|
const _module_resource_items = async (uid) => {
|
|
605
|
-
const items = {
|
|
637
|
+
const items = {};
|
|
638
|
+
for (const m of _MODULE_SUBSCRIPTIONS) if (m.scope === 'resource') items[m.key] = [];
|
|
639
|
+
|
|
640
|
+
// UI-96: every hosted resource is a monthly charge, so the card walks the account's
|
|
641
|
+
// apps the way BILLING itself does (the `user_apps` view plus `is_billable` from
|
|
642
|
+
// deploy_module/cost.mjs, which is what get_billing_metrics uses) and groups the live
|
|
643
|
+
// ones by kind. Reading `app_cost.monthly_total` rather than re-deriving a price from
|
|
644
|
+
// the plan catalog is what keeps this card and the invoice from disagreeing.
|
|
645
|
+
try {
|
|
646
|
+
const { is_billable, format_app_cost_summary } = await import(`${module_path}/deploy_module/cost.mjs`);
|
|
647
|
+
const apps_ret = await db_module.get_couch_view('xuda_master', 'user_apps', {
|
|
648
|
+
startkey: [uid, ''],
|
|
649
|
+
endkey: [uid, 'ZZZZZ'],
|
|
650
|
+
include_docs: true,
|
|
651
|
+
});
|
|
652
|
+
for (const row of apps_ret?.data?.rows || []) {
|
|
653
|
+
const app = row.doc;
|
|
654
|
+
if (!app || !is_billable(app)) continue;
|
|
655
|
+
// Billing's own definition of live: status 4 and up is gone, and a terminated
|
|
656
|
+
// deployment stops costing. A deleted website was being counted before this.
|
|
657
|
+
if (Number(app.app_status_code) >= 4) continue;
|
|
658
|
+
if (app.app_cost?.terminated_ts) continue;
|
|
659
|
+
const key = _HOSTING_GROUP[app.app_type];
|
|
660
|
+
if (!key || !items[key]) continue;
|
|
661
|
+
let summary = '';
|
|
662
|
+
try {
|
|
663
|
+
summary = format_app_cost_summary(app) || '';
|
|
664
|
+
} catch (e) {
|
|
665
|
+
summary = '';
|
|
666
|
+
}
|
|
667
|
+
items[key].push({
|
|
668
|
+
id: app._id,
|
|
669
|
+
label: app.app_name || app.name || app.deploy_data?.domain || app._id,
|
|
670
|
+
plan_id: app.app_cost?.app_server_type || app.app_type,
|
|
671
|
+
plan_name: summary || app.app_cost?.app_server_type || '',
|
|
672
|
+
price: Number(app.app_cost?.monthly_total) || 0,
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
} catch (e) {
|
|
676
|
+
console.error('[get_module_subscriptions hosting]', e.message);
|
|
677
|
+
}
|
|
606
678
|
|
|
679
|
+
// UI-97: domains registered THROUGH Xuda. A domain the customer merely connected
|
|
680
|
+
// (registered elsewhere, `connection_type: 'connected'`) costs nothing here, so the
|
|
681
|
+
// Stripe yearly price is what decides whether it is a subscription at all.
|
|
607
682
|
try {
|
|
608
683
|
const ret = await db_module.find_couch_query(
|
|
609
684
|
'xuda_master',
|
|
610
|
-
{ selector: { docType: '
|
|
685
|
+
{ selector: { docType: 'domain_registration', owner_uid: uid }, limit: 500 },
|
|
611
686
|
true,
|
|
612
687
|
);
|
|
613
|
-
for (const
|
|
614
|
-
const
|
|
615
|
-
const plan = _conf.PLAN_OBJ?.[`static_website_${tier}`];
|
|
616
|
-
const price = Number(plan?.price) || 0;
|
|
688
|
+
for (const dom of ret?.docs || []) {
|
|
689
|
+
const price = Number(dom.stripe_price_yr) || 0;
|
|
617
690
|
if (!price) continue;
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
691
|
+
const renews = dom.expires_at ? new Date(dom.expires_at) : null;
|
|
692
|
+
items.domain.push({
|
|
693
|
+
id: dom._id,
|
|
694
|
+
label: dom.name || dom._id,
|
|
695
|
+
// Auto renew off means it lapses at expiry, which is the one thing worth
|
|
696
|
+
// saying on a billing screen about a domain.
|
|
697
|
+
plan_name: dom.auto_renew === false
|
|
698
|
+
? `Renews ${renews ? renews.toISOString().slice(0, 10) : 'at expiry'}, auto renew off`
|
|
699
|
+
: `Renews ${renews ? renews.toISOString().slice(0, 10) : 'yearly'}`,
|
|
623
700
|
price,
|
|
701
|
+
cycle: 'yr',
|
|
624
702
|
});
|
|
625
703
|
}
|
|
626
704
|
} catch (e) {
|
|
627
|
-
console.error('[get_module_subscriptions
|
|
705
|
+
console.error('[get_module_subscriptions domains]', e.message);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// UI-97: dedicated IPv4. The per-address price depends on what it is doing
|
|
709
|
+
// (internal / external / parked), which is exactly how ipv4_module prices it, so the
|
|
710
|
+
// rate is read from PRICE_OBJ the same way rather than assumed.
|
|
711
|
+
try {
|
|
712
|
+
const prices = {
|
|
713
|
+
internal: Number(_conf.PRICE_OBJ?.ipv4?.internal ?? 2),
|
|
714
|
+
external: Number(_conf.PRICE_OBJ?.ipv4?.external ?? 10),
|
|
715
|
+
parked: Number(_conf.PRICE_OBJ?.ipv4?.parked ?? 5),
|
|
716
|
+
};
|
|
717
|
+
const ret = await db_module.find_couch_query(
|
|
718
|
+
'xuda_ipv4',
|
|
719
|
+
{ selector: { docType: 'ipv4_addr', account_id: uid }, limit: 1000 },
|
|
720
|
+
true,
|
|
721
|
+
);
|
|
722
|
+
for (const ip of ret?.docs || []) {
|
|
723
|
+
// A suspended address keeps billing at the rate it held, same as ipv4_module.
|
|
724
|
+
const bill = ip.state === 'suspended' ? ip.bill_as : ip.state;
|
|
725
|
+
const price = Number(prices[bill]) || 0;
|
|
726
|
+
if (!price) continue;
|
|
727
|
+
items.ipv4.push({
|
|
728
|
+
id: ip._id,
|
|
729
|
+
label: ip.address || ip._id,
|
|
730
|
+
plan_name: [bill, ip.target ? `on ${ip.target}` : ''].filter(Boolean).join(', '),
|
|
731
|
+
price,
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
} catch (e) {
|
|
735
|
+
console.error('[get_module_subscriptions ipv4]', e.message);
|
|
628
736
|
}
|
|
629
737
|
|
|
630
738
|
try {
|
|
@@ -674,7 +782,6 @@ export const get_module_subscriptions = async function (req = {}) {
|
|
|
674
782
|
if (ar.code < 0 || !ar.data) return { code: -404, data: 'account not found' };
|
|
675
783
|
const account = ar.data;
|
|
676
784
|
const plans = _conf.PLAN_OBJ || {};
|
|
677
|
-
const is_member = !!(account.membership_plan && account.membership_plan !== 'free');
|
|
678
785
|
// A category is only offerable once its paid tiers carry a real Stripe
|
|
679
786
|
// price, otherwise "activate" would set a plan nobody is billed for.
|
|
680
787
|
const billable = (key) =>
|
|
@@ -684,12 +791,11 @@ export const get_module_subscriptions = async function (req = {}) {
|
|
|
684
791
|
|
|
685
792
|
const modules = _MODULE_SUBSCRIPTIONS.map((m) => {
|
|
686
793
|
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
794
|
if (m.scope === 'resource' || m.scope === 'metered') {
|
|
692
795
|
const price = base.items.reduce((sum, i) => sum + Number(i.price || 0), 0);
|
|
796
|
+
// UI-97: a row is only yearly when everything in it is (domains). Anything
|
|
797
|
+
// else stays monthly, so a total is never a sum of two different cycles.
|
|
798
|
+
const cycle = base.items.length && base.items.every((i) => i.cycle === 'yr') ? 'yr' : 'mo';
|
|
693
799
|
return {
|
|
694
800
|
...base,
|
|
695
801
|
active: m.scope === 'metered' || base.items.length > 0,
|
|
@@ -697,6 +803,7 @@ export const get_module_subscriptions = async function (req = {}) {
|
|
|
697
803
|
plan_id: '',
|
|
698
804
|
plan_name: '',
|
|
699
805
|
price,
|
|
806
|
+
cycle,
|
|
700
807
|
};
|
|
701
808
|
}
|
|
702
809
|
const plan_id = account[m.field] || m.default_plan;
|
|
@@ -4052,8 +4159,20 @@ const get_contact_background = function (doc) {
|
|
|
4052
4159
|
return ret;
|
|
4053
4160
|
};
|
|
4054
4161
|
|
|
4162
|
+
// The card's background pattern doubles as a verification signal: L0..L4 each
|
|
4163
|
+
// have their own tile, distinguishable by density and brightness as well as by
|
|
4164
|
+
// the level token, so the level reads at a glance without another badge. Level
|
|
4165
|
+
// comes from account_info.verify_level, the projection verify_module writes on
|
|
4166
|
+
// every recompute; anything missing or out of range reads as L0, which is the
|
|
4167
|
+
// honest default for an account we have not scored.
|
|
4168
|
+
const level_pattern = function (doc) {
|
|
4169
|
+
const raw = doc?.verify_level ?? doc?.account_info?.verify_level ?? doc?.user_contact?.verify_level;
|
|
4170
|
+
const level = Number.isFinite(Number(raw)) ? Math.max(0, Math.min(4, Math.trunc(Number(raw)))) : 0;
|
|
4171
|
+
return `level-${level}-pattern.png`;
|
|
4172
|
+
};
|
|
4173
|
+
|
|
4055
4174
|
const get_contact_pattern = async function (doc) {
|
|
4056
|
-
let ret =
|
|
4175
|
+
let ret = level_pattern(doc);
|
|
4057
4176
|
|
|
4058
4177
|
if (doc?.shared_from_uid) {
|
|
4059
4178
|
const shared_from_uid_ret = await get_account_name({ uid_query: doc.shared_from_uid });
|
|
@@ -6293,7 +6412,9 @@ export const get_account_profile_info = async function (uid, contact_profile_doc
|
|
|
6293
6412
|
}
|
|
6294
6413
|
|
|
6295
6414
|
const get_pattern = async function (doc) {
|
|
6296
|
-
|
|
6415
|
+
// Same level-based default as get_contact_pattern, so a card shows the same
|
|
6416
|
+
// verification tile whichever path built it.
|
|
6417
|
+
let ret = level_pattern(doc);
|
|
6297
6418
|
|
|
6298
6419
|
if (doc.shared_from_uid) {
|
|
6299
6420
|
// const account_profile_doc = await db_module.get_couch_doc_native('xuda_accounts', doc.share_item_id);
|
|
@@ -8214,8 +8335,10 @@ const _nl_one = async (db, docType) => {
|
|
|
8214
8335
|
// notification DB on EVERY get_account_data: "No matching index found" plus a full-DB
|
|
8215
8336
|
// examine per dashboard load. uid leads (most selective), then topic, then read; the extra
|
|
8216
8337
|
// docType / delivery_method clauses those callers add are cheap in-memory filters once
|
|
8217
|
-
// uid + topic have narrowed the set to a handful.
|
|
8218
|
-
// xuda_notification
|
|
8338
|
+
// uid + topic have narrowed the set to a handful. Not host-gated like the content DBs
|
|
8339
|
+
// above: xuda_notification exists on every box, so every node should ensure the index.
|
|
8340
|
+
// The fleet boxes replicate it through the master hub (so the PUT no-ops once one node
|
|
8341
|
+
// has it) but dev is outside that hub and only gets the index from its own run.
|
|
8219
8342
|
(async () => {
|
|
8220
8343
|
try {
|
|
8221
8344
|
const ret = await db_module.create_couch_index('xuda_notification', {
|