@xuda.io/drive_module 1.1.1499 → 1.1.1500
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 +173 -0
- package/index_ms.mjs +8 -0
- package/index_msa.mjs +8 -0
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -29,6 +29,10 @@ const { get_account_default_project_id } = await import(path.join(process.env.XU
|
|
|
29
29
|
const module_path = path.join(process.env.XUDA_HOME, 'cpi') + (!_conf.is_debug ? '/node_modules/@xuda.io' : '');
|
|
30
30
|
const fs_module = await import(`${module_path}/fs_module/index.mjs`);
|
|
31
31
|
const db_module = await import(`${module_path}/db_module/index.mjs`);
|
|
32
|
+
// UI-113: the Drive storage ladder is one line item on the consolidated
|
|
33
|
+
// subscription, so drive_set_plan drives Stripe the same way every other module
|
|
34
|
+
// tier does (add_subscription_item to buy or reprice, remove on the free tier).
|
|
35
|
+
const stripe_ms = await import(`${module_path}/stripe_module/index_ms.mjs`);
|
|
32
36
|
|
|
33
37
|
// const account_msa = await import(`${module_path}/account_module/index_msa.mjs`);
|
|
34
38
|
|
|
@@ -4470,3 +4474,172 @@ export const file_upload_validator = function (file_name) {
|
|
|
4470
4474
|
return { valid: false, error: `Unsupported file extension: .${ext}` };
|
|
4471
4475
|
}
|
|
4472
4476
|
};
|
|
4477
|
+
|
|
4478
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
4479
|
+
// DRIVE PLANS (UI-113)
|
|
4480
|
+
//
|
|
4481
|
+
// Drive is sold on its own drive_* ladder and what the ladder sells is SPACE.
|
|
4482
|
+
// A tier's `flags.extra_gb` is added on top of the storage the account already
|
|
4483
|
+
// has from its membership plan and its AI workspace plan, it does not replace
|
|
4484
|
+
// either, so an account that never picks a tier is on drive_free and nothing
|
|
4485
|
+
// about its Drive changes.
|
|
4486
|
+
//
|
|
4487
|
+
// The money is one line item on the consolidated subscription, the same shape
|
|
4488
|
+
// every other module tier uses: a paid tier creates or reprices the line, free
|
|
4489
|
+
// removes it. Billing moves FIRST and the plan is only written once Stripe
|
|
4490
|
+
// agreed, so a failed charge can never leave an account holding space it is
|
|
4491
|
+
// not paying for.
|
|
4492
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
4493
|
+
|
|
4494
|
+
const DRIVE_CATEGORY = 'drive';
|
|
4495
|
+
const GB = 1073741824;
|
|
4496
|
+
|
|
4497
|
+
const _drive_tiers = () =>
|
|
4498
|
+
Object.values(_conf.PLAN_OBJ || {})
|
|
4499
|
+
.filter((p) => p.category === DRIVE_CATEGORY)
|
|
4500
|
+
.sort((a, b) => (Number(a.rank) || 0) - (Number(b.rank) || 0));
|
|
4501
|
+
|
|
4502
|
+
const _drive_plan = (plan_id) => {
|
|
4503
|
+
const plan = _conf.PLAN_OBJ?.[plan_id];
|
|
4504
|
+
return plan && plan.category === DRIVE_CATEGORY ? plan : null;
|
|
4505
|
+
};
|
|
4506
|
+
|
|
4507
|
+
const _drive_account_doc = async (uid) => {
|
|
4508
|
+
const ret = await db_module.get_couch_doc('xuda_accounts', uid);
|
|
4509
|
+
return ret.code < 0 || !ret.data ? {} : ret.data;
|
|
4510
|
+
};
|
|
4511
|
+
|
|
4512
|
+
// An account that never touched the ladder is on free. Same default the billing
|
|
4513
|
+
// Modules card applies, kept identical on purpose so the two never disagree.
|
|
4514
|
+
const _drive_account_plan = (account) => (_drive_plan(account?.drive_plan) ? account.drive_plan : 'drive_free');
|
|
4515
|
+
|
|
4516
|
+
// What the account is storing, using the SAME sum the overage metering charges
|
|
4517
|
+
// on (account_module increment_account_usage). project_data_size is deliberately
|
|
4518
|
+
// out of it there, so it is out of it here: a guard that counted bytes the
|
|
4519
|
+
// metering ignores would refuse a downgrade the customer is not billed for.
|
|
4520
|
+
const _drive_used_bytes = (account) =>
|
|
4521
|
+
(account?.user_drive_size || 0) +
|
|
4522
|
+
(account?.studio_drive_size || 0) +
|
|
4523
|
+
(account?.workspace_drive_size || 0) +
|
|
4524
|
+
(account?.builds_drive_size || 0) +
|
|
4525
|
+
(account?.plugins_drive_size || 0);
|
|
4526
|
+
|
|
4527
|
+
// The quota the account WOULD have on a given tier, in bytes. Delegates to
|
|
4528
|
+
// account_module's get_effective_entitlements rather than re-deriving the stack,
|
|
4529
|
+
// because that function is what the metering and every storage bar already use.
|
|
4530
|
+
// Imported lazily: this is the only place in the drive module that needs it and
|
|
4531
|
+
// account_module pulls in a large tree of its own.
|
|
4532
|
+
const _drive_quota_bytes = async (account, plan_id) => {
|
|
4533
|
+
const { get_effective_entitlements } = await import(`${module_path}/account_module/index.mjs`);
|
|
4534
|
+
const ent = get_effective_entitlements({ ...account, drive_plan: plan_id ?? account?.drive_plan });
|
|
4535
|
+
return {
|
|
4536
|
+
unlimited: !Number.isFinite(ent.drive_gb),
|
|
4537
|
+
bytes: Number.isFinite(ent.drive_gb) ? Math.round(ent.drive_gb * GB) : 0,
|
|
4538
|
+
breakdown: ent.drive_breakdown || {},
|
|
4539
|
+
};
|
|
4540
|
+
};
|
|
4541
|
+
|
|
4542
|
+
const _drive_pretty = (bytes) => {
|
|
4543
|
+
if (bytes >= 1099511627776) return `${Math.round((bytes / 1099511627776) * 10) / 10} TB`;
|
|
4544
|
+
if (bytes >= GB) return `${Math.round((bytes / GB) * 10) / 10} GB`;
|
|
4545
|
+
return `${Math.round(bytes / 1048576)} MB`;
|
|
4546
|
+
};
|
|
4547
|
+
|
|
4548
|
+
export const get_drive_plans = async function (req) {
|
|
4549
|
+
try {
|
|
4550
|
+
const uid = req?.uid;
|
|
4551
|
+
if (!uid) return { code: -401, data: 'not authenticated' };
|
|
4552
|
+
const account = await _drive_account_doc(uid);
|
|
4553
|
+
if (!account._id) return { code: -404, data: 'account not found' };
|
|
4554
|
+
|
|
4555
|
+
const plan_id = _drive_account_plan(account);
|
|
4556
|
+
const used = _drive_used_bytes(account);
|
|
4557
|
+
const quota = await _drive_quota_bytes(account, plan_id);
|
|
4558
|
+
|
|
4559
|
+
// Every tier carries the quota it WOULD give and whether the account already
|
|
4560
|
+
// stores more than that, so the screen can mark a tier as out of reach
|
|
4561
|
+
// without doing the entitlement arithmetic itself.
|
|
4562
|
+
const plans = [];
|
|
4563
|
+
for (const p of _drive_tiers()) {
|
|
4564
|
+
const q = await _drive_quota_bytes(account, p.id);
|
|
4565
|
+
plans.push({
|
|
4566
|
+
...p,
|
|
4567
|
+
quota_bytes: q.bytes,
|
|
4568
|
+
quota_unlimited: q.unlimited,
|
|
4569
|
+
fits: q.unlimited || used <= q.bytes,
|
|
4570
|
+
});
|
|
4571
|
+
}
|
|
4572
|
+
|
|
4573
|
+
return {
|
|
4574
|
+
code: 1,
|
|
4575
|
+
data: {
|
|
4576
|
+
plan_id,
|
|
4577
|
+
plan_name: _drive_plan(plan_id)?.name || '',
|
|
4578
|
+
changed_ts: account.drive_plan_changed || null,
|
|
4579
|
+
// The line on the invoice. Free holds no item at all.
|
|
4580
|
+
subscribed: !!account?.stripe_subscription_items?.[DRIVE_CATEGORY],
|
|
4581
|
+
used_bytes: used,
|
|
4582
|
+
quota_bytes: quota.bytes,
|
|
4583
|
+
quota_unlimited: quota.unlimited,
|
|
4584
|
+
// Where the space comes from, so a full bar can point at the plan that
|
|
4585
|
+
// would actually fix it rather than at this one by default.
|
|
4586
|
+
quota_from: {
|
|
4587
|
+
membership_bytes: Math.round((quota.breakdown.membership_gb === Infinity ? 0 : quota.breakdown.membership_gb || 0) * GB),
|
|
4588
|
+
workspace_bytes: Math.round((quota.breakdown.workspace_gb || 0) * GB),
|
|
4589
|
+
drive_plan_bytes: Math.round((quota.breakdown.drive_plan_gb || 0) * GB),
|
|
4590
|
+
},
|
|
4591
|
+
membership_plan: account.membership_plan || 'free',
|
|
4592
|
+
ai_workspace_plan: account.ai_workspace_plan || '',
|
|
4593
|
+
plans,
|
|
4594
|
+
},
|
|
4595
|
+
};
|
|
4596
|
+
} catch (err) {
|
|
4597
|
+
return { code: -1, data: err.message || String(err) };
|
|
4598
|
+
}
|
|
4599
|
+
};
|
|
4600
|
+
|
|
4601
|
+
export const drive_set_plan = async function (req) {
|
|
4602
|
+
try {
|
|
4603
|
+
const uid = req?.uid;
|
|
4604
|
+
if (!uid) return { code: -401, data: 'not authenticated' };
|
|
4605
|
+
const plan_id = req?.plan_id || '';
|
|
4606
|
+
const plan = _drive_plan(plan_id);
|
|
4607
|
+
if (!plan) return { code: -1, data: 'unknown drive plan' };
|
|
4608
|
+
|
|
4609
|
+
const account = await _drive_account_doc(uid);
|
|
4610
|
+
if (!account._id) return { code: -404, data: 'account not found' };
|
|
4611
|
+
const current = _drive_account_plan(account);
|
|
4612
|
+
if (current === plan_id) return await get_drive_plans({ uid });
|
|
4613
|
+
|
|
4614
|
+
// Downgrade guard. Taking space away from an account that is already using it
|
|
4615
|
+
// would drop it straight into metered overage on files it cannot see a way to
|
|
4616
|
+
// remove, so name the gap instead and let the customer clear it first.
|
|
4617
|
+
const target = await _drive_quota_bytes(account, plan_id);
|
|
4618
|
+
const used = _drive_used_bytes(account);
|
|
4619
|
+
if (!target.unlimited && used > target.bytes) {
|
|
4620
|
+
return {
|
|
4621
|
+
code: -1,
|
|
4622
|
+
data: `You are storing ${_drive_pretty(used)} and the ${plan.name} plan would leave you with ${_drive_pretty(target.bytes)}. Free up ${_drive_pretty(used - target.bytes)} in Drive first.`,
|
|
4623
|
+
};
|
|
4624
|
+
}
|
|
4625
|
+
|
|
4626
|
+
const paid = Number(plan.price) > 0;
|
|
4627
|
+
if (paid) {
|
|
4628
|
+
const ret = await stripe_ms.add_subscription_item({ uid, plan_id });
|
|
4629
|
+
if (!ret || ret.code < 0) return { code: ret?.code === -402 || ret?.code === -3 ? -402 : -1, data: ret?.data || 'could not change the plan' };
|
|
4630
|
+
} else if (account.stripe_subscription_items?.[DRIVE_CATEGORY]) {
|
|
4631
|
+
const ret = await stripe_ms.remove_subscription_item({ uid, category: DRIVE_CATEGORY });
|
|
4632
|
+
if (!ret || ret.code < 0) return { code: -1, data: ret?.data || 'could not remove the subscription line' };
|
|
4633
|
+
}
|
|
4634
|
+
|
|
4635
|
+
// Re-read: the stripe call just wrote stripe_subscription_items on this doc.
|
|
4636
|
+
const fresh = await _drive_account_doc(uid);
|
|
4637
|
+
fresh.drive_plan = plan_id;
|
|
4638
|
+
fresh.drive_plan_changed = Date.now();
|
|
4639
|
+
await db_module.save_couch_doc('xuda_accounts', fresh);
|
|
4640
|
+
|
|
4641
|
+
return await get_drive_plans({ uid });
|
|
4642
|
+
} catch (err) {
|
|
4643
|
+
return { code: -1, data: err.message || String(err) };
|
|
4644
|
+
}
|
|
4645
|
+
};
|
package/index_ms.mjs
CHANGED
|
@@ -421,6 +421,14 @@ export const file_upload_validator = async function (...args) {
|
|
|
421
421
|
return await broker.send_to_queue("file_upload_validator", ...args);
|
|
422
422
|
};
|
|
423
423
|
|
|
424
|
+
export const get_drive_plans = async function (...args) {
|
|
425
|
+
return await broker.send_to_queue("get_drive_plans", ...args);
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
export const drive_set_plan = async function (...args) {
|
|
429
|
+
return await broker.send_to_queue("drive_set_plan", ...args);
|
|
430
|
+
};
|
|
431
|
+
|
|
424
432
|
export const get_drive_files_studio = async function (...args) {
|
|
425
433
|
return await broker.send_to_queue("get_drive_files_studio", ...args);
|
|
426
434
|
};
|
package/index_msa.mjs
CHANGED
|
@@ -421,6 +421,14 @@ export const file_upload_validator = function (...args) {
|
|
|
421
421
|
broker.send_to_queue_async("file_upload_validator", ...args);
|
|
422
422
|
};
|
|
423
423
|
|
|
424
|
+
export const get_drive_plans = function (...args) {
|
|
425
|
+
broker.send_to_queue_async("get_drive_plans", ...args);
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
export const drive_set_plan = function (...args) {
|
|
429
|
+
broker.send_to_queue_async("drive_set_plan", ...args);
|
|
430
|
+
};
|
|
431
|
+
|
|
424
432
|
export const get_drive_files_studio = function (...args) {
|
|
425
433
|
broker.send_to_queue_async("get_drive_files_studio", ...args);
|
|
426
434
|
};
|