@xuda.io/account_module 1.2.2308 → 1.2.2309
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 +143 -22
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -542,11 +542,18 @@ export const save_admin_presets = async function (req) {
|
|
|
542
542
|
};
|
|
543
543
|
|
|
544
544
|
// Effective entitlements = the membership plan's base quota PLUS the AI-workspace
|
|
545
|
-
// add-on's credits + storage stacked on top
|
|
546
|
-
//
|
|
547
|
-
//
|
|
548
|
-
//
|
|
549
|
-
//
|
|
545
|
+
// add-on's credits + storage stacked on top, PLUS the Drive plan's own storage.
|
|
546
|
+
// The Free membership tier does NOT stack the workspace add-on (it keeps its base
|
|
547
|
+
// only); every paid tier adds whatever ai_workspace plan the account carries.
|
|
548
|
+
// String quotas ("unlimited", e.g. Enterprise T3) short-circuit to Infinity so
|
|
549
|
+
// downstream numeric comparisons never cap. Single source of truth, use it for
|
|
550
|
+
// quota checks and any effective-quota display or API.
|
|
551
|
+
//
|
|
552
|
+
// UI-113: the Drive plan (drive_* category) is a storage add-on the customer buys
|
|
553
|
+
// on its own, so unlike the workspace it stacks on EVERY membership tier including
|
|
554
|
+
// free. Withholding space that has its own line on the invoice would be selling
|
|
555
|
+
// nothing. The breakdown comes back with it, because a customer looking at a full
|
|
556
|
+
// bar needs to see which of the three plans to move to get more room.
|
|
550
557
|
export const get_effective_entitlements = function (account_doc) {
|
|
551
558
|
const membership = _conf.PLAN_OBJ?.[account_doc?.membership_plan];
|
|
552
559
|
const mf = membership?.features || {};
|
|
@@ -554,15 +561,34 @@ export const get_effective_entitlements = function (account_doc) {
|
|
|
554
561
|
const numOrInf = (v) => (typeof v === 'string' ? Infinity : Number(v) || 0);
|
|
555
562
|
let ai_credits = numOrInf(mf.ai_credits);
|
|
556
563
|
let drive_gb = numOrInf(mf.drive);
|
|
564
|
+
const membership_drive_gb = drive_gb;
|
|
565
|
+
let workspace_drive_gb = 0;
|
|
566
|
+
let plan_drive_gb = 0;
|
|
557
567
|
|
|
558
568
|
if (account_doc?.membership_plan && account_doc.membership_plan !== 'free') {
|
|
559
569
|
const ws = _conf.PLAN_OBJ?.[account_doc?.ai_workspace_plan];
|
|
560
570
|
if (ws && ws.category === 'ai_workspace') {
|
|
561
571
|
if (Number.isFinite(ai_credits)) ai_credits += Number(ws.ai_credits) || 0;
|
|
562
|
-
|
|
572
|
+
workspace_drive_gb = (Number(ws.size) || 0) / 1073741824;
|
|
573
|
+
if (Number.isFinite(drive_gb)) drive_gb += workspace_drive_gb;
|
|
563
574
|
}
|
|
564
575
|
}
|
|
565
|
-
|
|
576
|
+
|
|
577
|
+
const drive_plan = _conf.PLAN_OBJ?.[account_doc?.drive_plan];
|
|
578
|
+
if (drive_plan && drive_plan.category === 'drive') {
|
|
579
|
+
plan_drive_gb = Number(drive_plan.flags?.extra_gb) || 0;
|
|
580
|
+
if (Number.isFinite(drive_gb)) drive_gb += plan_drive_gb;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
return {
|
|
584
|
+
ai_credits,
|
|
585
|
+
drive_gb,
|
|
586
|
+
drive_breakdown: {
|
|
587
|
+
membership_gb: membership_drive_gb,
|
|
588
|
+
workspace_gb: workspace_drive_gb,
|
|
589
|
+
drive_plan_gb: plan_drive_gb,
|
|
590
|
+
},
|
|
591
|
+
};
|
|
566
592
|
};
|
|
567
593
|
|
|
568
594
|
// ===================================================================
|
|
@@ -588,19 +614,58 @@ const _MODULE_SUBSCRIPTIONS = [
|
|
|
588
614
|
// switched on there as well as here. Both call tickets_set_plan, so the plan
|
|
589
615
|
// is one line item on the consolidated subscription either way.
|
|
590
616
|
{ key: 'tickets', scope: 'account', field: 'tickets_plan', default_plan: 'tickets_free' },
|
|
617
|
+
// UI-104: email is its own product, billed per mailbox tier. Its plan lives on the
|
|
618
|
+
// account as an OBJECT (`email_plan.tier`), which is why it resolves rather than
|
|
619
|
+
// reading a field, and `set_email_plan` is what moves it.
|
|
620
|
+
{
|
|
621
|
+
key: 'email',
|
|
622
|
+
scope: 'account',
|
|
623
|
+
field: 'email_plan',
|
|
624
|
+
default_plan: 'email_free',
|
|
625
|
+
resolve: (a) => (a && a.email_plan && a.email_plan.tier ? `email_${a.email_plan.tier}` : ''),
|
|
626
|
+
},
|
|
591
627
|
// Auto response is the single gate for whether the AI answers, on every channel
|
|
592
628
|
// (chat, the public chat widget, email and the phone). It is account scoped and
|
|
593
629
|
// billed once however many scenarios are written, so it is a plain line item
|
|
594
630
|
// here rather than a per-resource charge like phone or email.
|
|
595
|
-
|
|
631
|
+
//
|
|
632
|
+
// UI-110: "nothing auto-responds before a plan is picked" is the module's own rule
|
|
633
|
+
// (auto_response_module assert_active), so PICKED is what active means here, Free
|
|
634
|
+
// included. Judging it on price alone would report an account that deliberately runs
|
|
635
|
+
// the free tier as switched off, and the menu gate would then hide the screen it uses.
|
|
636
|
+
{
|
|
637
|
+
key: 'auto_response',
|
|
638
|
+
scope: 'account',
|
|
639
|
+
field: 'auto_response_plan',
|
|
640
|
+
default_plan: 'auto_response_free',
|
|
641
|
+
is_active: (a) => !!a.auto_response_plan,
|
|
642
|
+
},
|
|
596
643
|
// Four-tier ladder since 2026-08-06 (free / external / api / custom rules),
|
|
597
644
|
// chosen on the Bot Protection Manager's Plans tab, which calls
|
|
598
645
|
// bot_protection_set_plan. Free holds no line item at all.
|
|
599
646
|
// No default_plan on purpose: Free is a tier the customer activates, so an
|
|
600
647
|
// account that never chose one reads as not active rather than as Free.
|
|
601
|
-
{ key: 'bot_protection', scope: 'account', field: 'bot_protection_plan', default_plan: '' },
|
|
648
|
+
{ key: 'bot_protection', scope: 'account', field: 'bot_protection_plan', default_plan: '', is_active: (a) => !!a.bot_protection_plan },
|
|
602
649
|
{ key: 'static_website', scope: 'resource' },
|
|
603
|
-
|
|
650
|
+
// Phone is the one module that is BOTH: an account-wide tier (set_voice_plan, held in
|
|
651
|
+
// communications.voice_plan) and a per-number charge, because the plan fee rides the
|
|
652
|
+
// oldest live number's own Stripe subscription rather than the consolidated one. So the
|
|
653
|
+
// row carries the tier for the switch AND one item per number for the billing card.
|
|
654
|
+
//
|
|
655
|
+
// billable is forced true for the same reason: the generic test looks for a Stripe
|
|
656
|
+
// price_id on a paid tier, and the phone ladder has none by design, its money is on the
|
|
657
|
+
// number. Left to the generic test the module would read "coming soon" while customers
|
|
658
|
+
// are being billed for numbers.
|
|
659
|
+
{
|
|
660
|
+
key: 'profile_phone',
|
|
661
|
+
scope: 'resource',
|
|
662
|
+
billable: true,
|
|
663
|
+
plan_of: (a) => `profile_phone_${a?.communications?.voice_plan?.tier || 'free'}`,
|
|
664
|
+
// get_voice_plans' own definition, kept identical on purpose: a number bought before
|
|
665
|
+
// UI-84 counts as active, so an existing customer is not asked to activate a plan
|
|
666
|
+
// they already pay for.
|
|
667
|
+
is_active: (a, items) => !!a?.communications?.voice_plan?.activated_ts || items.length > 0,
|
|
668
|
+
},
|
|
604
669
|
// UI-96: the hosted resources. Not PLAN_OBJ categories, they are billed per resource
|
|
605
670
|
// from `app_cost` on the app doc, but they are money on the same invoice, so the card
|
|
606
671
|
// that answers "what am I paying for" has to show them.
|
|
@@ -614,9 +679,26 @@ const _MODULE_SUBSCRIPTIONS = [
|
|
|
614
679
|
// so both carry their own `cycle` on each item rather than being assumed monthly.
|
|
615
680
|
{ key: 'domain', scope: 'resource' },
|
|
616
681
|
{ key: 'ipv4', scope: 'resource' },
|
|
617
|
-
//
|
|
618
|
-
//
|
|
619
|
-
|
|
682
|
+
// Trust Center (verify_module) is sold on the trust_center_* ladder: the tier
|
|
683
|
+
// buys a capability ceiling and an included volume of live verifications for
|
|
684
|
+
// the customer's OWN users, and traffic past the allowance still meters per
|
|
685
|
+
// check into verify_meter. Account scoped since 2026-08-06; it was 'metered'
|
|
686
|
+
// while the service had no ladder behind it. Xuda's own verification is never
|
|
687
|
+
// priced by this plan, deliberately: see the note in verify_module.
|
|
688
|
+
{ key: 'trust_center', scope: 'account', field: 'trust_center_plan', default_plan: 'trust_center_free' },
|
|
689
|
+
// The Marketplace seller ladder: the tier buys live listings, the listing types the
|
|
690
|
+
// account may sell, and above all the cut Xuda keeps on each sale (20% down to 5%).
|
|
691
|
+
// is_active on the FIELD rather than on price, like auto response: Free is a tier a
|
|
692
|
+
// seller picks, and it is the tier that decides their commission, so an account that
|
|
693
|
+
// deliberately runs Free is switched on and must not read as off.
|
|
694
|
+
{ key: 'marketplace', scope: 'account', field: 'marketplace_plan', default_plan: 'marketplace_free', is_active: (a) => !!a.marketplace_plan },
|
|
695
|
+
// UI-113: Drive is sold on its own drive_* ladder, storage only. The tier buys space
|
|
696
|
+
// STACKED on top of what the membership and the AI workspace plan already include
|
|
697
|
+
// (get_effective_entitlements), which is why the free tier is 0 extra GB rather than
|
|
698
|
+
// a number: an account that never touched this ladder keeps exactly the space it has
|
|
699
|
+
// today and owes nothing new. Judged on price, so Free reads as not active, because
|
|
700
|
+
// here that is the truth: Free adds nothing and holds no line on the invoice.
|
|
701
|
+
{ key: 'drive', scope: 'account', field: 'drive_plan', default_plan: 'drive_free' },
|
|
620
702
|
];
|
|
621
703
|
|
|
622
704
|
// app_type -> the row it belongs under. An app type that is not here is not a
|
|
@@ -785,30 +867,42 @@ export const get_module_subscriptions = async function (req = {}) {
|
|
|
785
867
|
const account = ar.data;
|
|
786
868
|
const plans = _conf.PLAN_OBJ || {};
|
|
787
869
|
// A category is only offerable once its paid tiers carry a real Stripe
|
|
788
|
-
// price, otherwise "activate" would set a plan nobody is billed for.
|
|
789
|
-
|
|
790
|
-
|
|
870
|
+
// price, otherwise "activate" would set a plan nobody is billed for. A module
|
|
871
|
+
// whose money is NOT a plan line item says so itself (phone), because for those
|
|
872
|
+
// an absent price_id is the design, not a gap.
|
|
873
|
+
const billable = (m) =>
|
|
874
|
+
m.billable === true ||
|
|
875
|
+
Object.values(plans).some((p) => p.category === m.key && Number(p.price) > 0 && p.price_id && !/TODO/.test(p.price_id));
|
|
791
876
|
|
|
792
877
|
const resource_items = await _module_resource_items(uid);
|
|
793
878
|
|
|
794
879
|
const modules = _MODULE_SUBSCRIPTIONS.map((m) => {
|
|
795
|
-
const base = { key: m.key, scope: m.scope, billable: billable(m
|
|
880
|
+
const base = { key: m.key, scope: m.scope, billable: billable(m), items: resource_items[m.key] || [] };
|
|
796
881
|
if (m.scope === 'resource' || m.scope === 'metered') {
|
|
797
882
|
const price = base.items.reduce((sum, i) => sum + Number(i.price || 0), 0);
|
|
798
883
|
// UI-97: a row is only yearly when everything in it is (domains). Anything
|
|
799
884
|
// else stays monthly, so a total is never a sum of two different cycles.
|
|
800
885
|
const cycle = base.items.length && base.items.every((i) => i.cycle === 'yr') ? 'yr' : 'mo';
|
|
886
|
+
// UI-110: a resource row may still sit under an account-wide tier (phone), and
|
|
887
|
+
// the switch needs that tier, not the sum of the numbers. Everything without a
|
|
888
|
+
// `plan_of` keeps reporting no plan, exactly as before.
|
|
889
|
+
const plan_id = m.plan_of ? m.plan_of(account) : '';
|
|
890
|
+
const plan = plans[plan_id] || {};
|
|
801
891
|
return {
|
|
802
892
|
...base,
|
|
803
|
-
active: m.scope === 'metered' || base.items.length > 0,
|
|
893
|
+
active: m.is_active ? !!m.is_active(account, base.items) : m.scope === 'metered' || base.items.length > 0,
|
|
804
894
|
included: m.scope === 'metered',
|
|
805
|
-
plan_id
|
|
806
|
-
plan_name: '',
|
|
895
|
+
plan_id,
|
|
896
|
+
plan_name: plan.name || '',
|
|
897
|
+
plan_price: Number(plan.price) || 0,
|
|
807
898
|
price,
|
|
808
899
|
cycle,
|
|
809
900
|
};
|
|
810
901
|
}
|
|
811
|
-
|
|
902
|
+
// UI-104: most modules keep their plan id directly on the account, but email
|
|
903
|
+
// keeps `{ tier, activated_ts }`, so a module may bring its own resolver rather
|
|
904
|
+
// than forcing every field into the same shape.
|
|
905
|
+
const plan_id = (m.resolve ? m.resolve(account) : account[m.field]) || m.default_plan;
|
|
812
906
|
const plan = plans[plan_id] || {};
|
|
813
907
|
// UI-103: a switch-off is a DOWNGRADE, so Stripe schedules it for the end of the
|
|
814
908
|
// period the customer already paid for and the plan field does not move until the
|
|
@@ -824,7 +918,10 @@ export const get_module_subscriptions = async function (req = {}) {
|
|
|
824
918
|
plan_id,
|
|
825
919
|
plan_name: plan.name || plan_id,
|
|
826
920
|
price,
|
|
827
|
-
|
|
921
|
+
// UI-110: paying is the default proof that a module is on, but a module that
|
|
922
|
+
// records the customer CHOOSING a tier says so itself, because a deliberate
|
|
923
|
+
// free tier is switched on and a price of zero cannot tell the two apart.
|
|
924
|
+
active: m.is_active ? !!m.is_active(account, base.items) : price > 0,
|
|
828
925
|
included: false,
|
|
829
926
|
pending,
|
|
830
927
|
changed_ts: account[`${m.field}_changed`] || null,
|
|
@@ -1783,7 +1880,31 @@ export const get_account_data = async function (req) {
|
|
|
1783
1880
|
builds_drive_size: acc_obj.builds_drive_size || 0,
|
|
1784
1881
|
project_data_size: acc_obj.project_data_size || 0,
|
|
1785
1882
|
total_drive_size: acc_obj.total_drive_size || 0,
|
|
1883
|
+
// UI-113: the QUOTA travels with the usage now. Every surface that drew a
|
|
1884
|
+
// storage bar (the sidenav popover, UsageBar, the Drive plans screen) was
|
|
1885
|
+
// deriving the ceiling from the ai_workspace plan's `size` alone, which
|
|
1886
|
+
// ignored the membership base and would have ignored the Drive plan too, so
|
|
1887
|
+
// a bar could read full while the account had room. This is the same
|
|
1888
|
+
// get_effective_entitlements the overage metering charges on, in bytes.
|
|
1889
|
+
...(() => {
|
|
1890
|
+
const ent = get_effective_entitlements(acc_obj);
|
|
1891
|
+
const gb = 1073741824;
|
|
1892
|
+
const b = ent.drive_breakdown || {};
|
|
1893
|
+
return {
|
|
1894
|
+
quota_bytes: Number.isFinite(ent.drive_gb) ? Math.round(ent.drive_gb * gb) : 0,
|
|
1895
|
+
quota_unlimited: !Number.isFinite(ent.drive_gb),
|
|
1896
|
+
quota_from: {
|
|
1897
|
+
membership_bytes: Math.round((b.membership_gb === Infinity ? 0 : b.membership_gb || 0) * gb),
|
|
1898
|
+
workspace_bytes: Math.round((b.workspace_gb || 0) * gb),
|
|
1899
|
+
drive_plan_bytes: Math.round((b.drive_plan_gb || 0) * gb),
|
|
1900
|
+
},
|
|
1901
|
+
};
|
|
1902
|
+
})(),
|
|
1786
1903
|
},
|
|
1904
|
+
// UI-113: the Drive storage ladder the account sits on, for the plans screen
|
|
1905
|
+
// and the Modules card. Absent on an account that never picked one, which the
|
|
1906
|
+
// registry reads as drive_free.
|
|
1907
|
+
drive_plan: acc_obj.drive_plan || 'drive_free',
|
|
1787
1908
|
|
|
1788
1909
|
stripe_connect_account_id: acc_obj?.stripe_connect_account_obj?.id,
|
|
1789
1910
|
stripe_connect_account_status: acc_obj?.stripe_connect_account_status,
|