@xuda.io/account_module 1.2.2306 → 1.2.2308

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 +79 -1
  2. package/package.json +1 -1
package/index.mjs CHANGED
@@ -596,7 +596,9 @@ const _MODULE_SUBSCRIPTIONS = [
596
596
  // Four-tier ladder since 2026-08-06 (free / external / api / custom rules),
597
597
  // chosen on the Bot Protection Manager's Plans tab, which calls
598
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' },
599
+ // No default_plan on purpose: Free is a tier the customer activates, so an
600
+ // 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: '' },
600
602
  { key: 'static_website', scope: 'resource' },
601
603
  { key: 'profile_phone', scope: 'resource' },
602
604
  // UI-96: the hosted resources. Not PLAN_OBJ categories, they are billed per resource
@@ -608,6 +610,10 @@ const _MODULE_SUBSCRIPTIONS = [
608
610
  { key: 'preview', scope: 'resource' },
609
611
  { key: 'instance', scope: 'resource' },
610
612
  { key: 'backup', scope: 'resource' },
613
+ // UI-97: a registered domain renews yearly and a dedicated IPv4 is a monthly line,
614
+ // so both carry their own `cycle` on each item rather than being assumed monthly.
615
+ { key: 'domain', scope: 'resource' },
616
+ { key: 'ipv4', scope: 'resource' },
611
617
  // Xuda Verify (verify_module) has no PLAN_OBJ category: every check is metered
612
618
  // per call into verify_meter and nothing is charged to the account today.
613
619
  { key: 'trust_center', scope: 'metered' },
@@ -672,6 +678,65 @@ const _module_resource_items = async (uid) => {
672
678
  console.error('[get_module_subscriptions hosting]', e.message);
673
679
  }
674
680
 
681
+ // UI-97: domains registered THROUGH Xuda. A domain the customer merely connected
682
+ // (registered elsewhere, `connection_type: 'connected'`) costs nothing here, so the
683
+ // Stripe yearly price is what decides whether it is a subscription at all.
684
+ try {
685
+ const ret = await db_module.find_couch_query(
686
+ 'xuda_master',
687
+ { selector: { docType: 'domain_registration', owner_uid: uid }, limit: 500 },
688
+ true,
689
+ );
690
+ for (const dom of ret?.docs || []) {
691
+ const price = Number(dom.stripe_price_yr) || 0;
692
+ if (!price) continue;
693
+ const renews = dom.expires_at ? new Date(dom.expires_at) : null;
694
+ items.domain.push({
695
+ id: dom._id,
696
+ label: dom.name || dom._id,
697
+ // Auto renew off means it lapses at expiry, which is the one thing worth
698
+ // saying on a billing screen about a domain.
699
+ plan_name: dom.auto_renew === false
700
+ ? `Renews ${renews ? renews.toISOString().slice(0, 10) : 'at expiry'}, auto renew off`
701
+ : `Renews ${renews ? renews.toISOString().slice(0, 10) : 'yearly'}`,
702
+ price,
703
+ cycle: 'yr',
704
+ });
705
+ }
706
+ } catch (e) {
707
+ console.error('[get_module_subscriptions domains]', e.message);
708
+ }
709
+
710
+ // UI-97: dedicated IPv4. The per-address price depends on what it is doing
711
+ // (internal / external / parked), which is exactly how ipv4_module prices it, so the
712
+ // rate is read from PRICE_OBJ the same way rather than assumed.
713
+ try {
714
+ const prices = {
715
+ internal: Number(_conf.PRICE_OBJ?.ipv4?.internal ?? 2),
716
+ external: Number(_conf.PRICE_OBJ?.ipv4?.external ?? 10),
717
+ parked: Number(_conf.PRICE_OBJ?.ipv4?.parked ?? 5),
718
+ };
719
+ const ret = await db_module.find_couch_query(
720
+ 'xuda_ipv4',
721
+ { selector: { docType: 'ipv4_addr', account_id: uid }, limit: 1000 },
722
+ true,
723
+ );
724
+ for (const ip of ret?.docs || []) {
725
+ // A suspended address keeps billing at the rate it held, same as ipv4_module.
726
+ const bill = ip.state === 'suspended' ? ip.bill_as : ip.state;
727
+ const price = Number(prices[bill]) || 0;
728
+ if (!price) continue;
729
+ items.ipv4.push({
730
+ id: ip._id,
731
+ label: ip.address || ip._id,
732
+ plan_name: [bill, ip.target ? `on ${ip.target}` : ''].filter(Boolean).join(', '),
733
+ price,
734
+ });
735
+ }
736
+ } catch (e) {
737
+ console.error('[get_module_subscriptions ipv4]', e.message);
738
+ }
739
+
675
740
  try {
676
741
  const ret = await db_module.find_couch_query(
677
742
  'xuda_master',
@@ -730,6 +795,9 @@ export const get_module_subscriptions = async function (req = {}) {
730
795
  const base = { key: m.key, scope: m.scope, billable: billable(m.key), items: resource_items[m.key] || [] };
731
796
  if (m.scope === 'resource' || m.scope === 'metered') {
732
797
  const price = base.items.reduce((sum, i) => sum + Number(i.price || 0), 0);
798
+ // UI-97: a row is only yearly when everything in it is (domains). Anything
799
+ // else stays monthly, so a total is never a sum of two different cycles.
800
+ const cycle = base.items.length && base.items.every((i) => i.cycle === 'yr') ? 'yr' : 'mo';
733
801
  return {
734
802
  ...base,
735
803
  active: m.scope === 'metered' || base.items.length > 0,
@@ -737,10 +805,19 @@ export const get_module_subscriptions = async function (req = {}) {
737
805
  plan_id: '',
738
806
  plan_name: '',
739
807
  price,
808
+ cycle,
740
809
  };
741
810
  }
742
811
  const plan_id = account[m.field] || m.default_plan;
743
812
  const plan = plans[plan_id] || {};
813
+ // UI-103: a switch-off is a DOWNGRADE, so Stripe schedules it for the end of the
814
+ // period the customer already paid for and the plan field does not move until the
815
+ // webhook transitions it. Surfacing the pending change is what stops the UI from
816
+ // looking like the click did nothing.
817
+ const pc = account.pending_plan_change;
818
+ const pending = pc && pc.category === m.key
819
+ ? { to_plan: pc.to_plan, to_name: plans[pc.to_plan]?.name || pc.to_plan, effective: pc.effective || null }
820
+ : null;
744
821
  const price = Number(plan.price) || 0;
745
822
  return {
746
823
  ...base,
@@ -749,6 +826,7 @@ export const get_module_subscriptions = async function (req = {}) {
749
826
  price,
750
827
  active: price > 0,
751
828
  included: false,
829
+ pending,
752
830
  changed_ts: account[`${m.field}_changed`] || null,
753
831
  };
754
832
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xuda.io/account_module",
3
- "version": "1.2.2306",
3
+ "version": "1.2.2308",
4
4
  "description": "Xuda Account Server Module",
5
5
  "main": "index.mjs",
6
6
  "dependencies": {