@xuda.io/account_module 1.2.2302 → 1.2.2303
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 +469 -31
- package/index_ms.mjs +16 -0
- package/index_msa.mjs +16 -0
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -139,6 +139,15 @@ export const _sync_account_project_name = async function (account_obj, ref_info)
|
|
|
139
139
|
}
|
|
140
140
|
};
|
|
141
141
|
|
|
142
|
+
// The profile PICTURE is open to everyone, including accounts that have not
|
|
143
|
+
// confirmed their email; the generated AVATAR is what waits for verification.
|
|
144
|
+
// stat 1 is the unverified state (stat 2 is the password-reset temp state,
|
|
145
|
+
// stat 3 verified), matching the write-gate in http_module — which now lets
|
|
146
|
+
// update_account_info and upload_drive_file_user through at stat 1 precisely so
|
|
147
|
+
// the picture can be saved. verify_account starts any deferred generation the
|
|
148
|
+
// moment the account flips to verified, so nothing is lost by waiting here.
|
|
149
|
+
const can_generate_avatar = (account_obj) => account_obj?.stat !== 1;
|
|
150
|
+
|
|
142
151
|
export const update_account_info = async function (req, job_id, headers) {
|
|
143
152
|
const { uid } = req;
|
|
144
153
|
const data = req;
|
|
@@ -278,7 +287,7 @@ export const update_account_info = async function (req, job_id, headers) {
|
|
|
278
287
|
return { code: -1310, data: error };
|
|
279
288
|
}
|
|
280
289
|
if (!change) {
|
|
281
|
-
if (account_obj.account_info?.profile_picture && !account_obj.account_info?.profile_avatar && account_obj.account_info.profile_avatar_stat !== 2) {
|
|
290
|
+
if (can_generate_avatar(account_obj) && account_obj.account_info?.profile_picture && !account_obj.account_info?.profile_avatar && account_obj.account_info.profile_avatar_stat !== 2) {
|
|
282
291
|
set_account_profile_picture(uid, uid, account_obj.account_info, job_id, headers, account_profile_info);
|
|
283
292
|
}
|
|
284
293
|
return { code: 1300, data: 'no change' };
|
|
@@ -317,7 +326,7 @@ export const update_account_info = async function (req, job_id, headers) {
|
|
|
317
326
|
await _sync_account_project_name(account_obj, _old_name_info);
|
|
318
327
|
}
|
|
319
328
|
|
|
320
|
-
if (account_obj.account_info?.profile_picture) {
|
|
329
|
+
if (can_generate_avatar(account_obj) && account_obj.account_info?.profile_picture) {
|
|
321
330
|
if (!account_obj.account_info?.profile_avatar && account_obj.account_info.profile_avatar_stat !== 2) {
|
|
322
331
|
set_account_profile_picture(uid, uid, account_obj.account_info, job_id, headers, account_profile_info);
|
|
323
332
|
}
|
|
@@ -556,6 +565,159 @@ export const get_effective_entitlements = function (account_doc) {
|
|
|
556
565
|
return { ai_credits, drive_gb };
|
|
557
566
|
};
|
|
558
567
|
|
|
568
|
+
// ===================================================================
|
|
569
|
+
// Module subscriptions. Membership, support and AI workspace are the three
|
|
570
|
+
// core tiers the billing screen already renders off the account doc; every
|
|
571
|
+
// other suite is sold as its own PLAN_OBJ category billed as a line item on
|
|
572
|
+
// the consolidated subscription. This registry is what the billing screen
|
|
573
|
+
// lists under "Modules", so the customer can see which suites need a
|
|
574
|
+
// subscription and switch one on.
|
|
575
|
+
//
|
|
576
|
+
// scope 'account' → one plan for the whole account, held in `field`, changed
|
|
577
|
+
// with the module's own *_set_plan method.
|
|
578
|
+
// scope 'resource' → priced on the thing itself (a website, a phone number),
|
|
579
|
+
// so there is no account-wide plan to show, only a pointer
|
|
580
|
+
// to where it is set.
|
|
581
|
+
// scope 'metered' → no plan ladder at all, usage is metered per call. Listed
|
|
582
|
+
// so the customer can see it costs no subscription.
|
|
583
|
+
const _MODULE_SUBSCRIPTIONS = [
|
|
584
|
+
{ key: 'commerce', scope: 'account', field: 'commerce_plan', default_plan: 'commerce_free' },
|
|
585
|
+
{ key: 'shipping', scope: 'account', field: 'shipping_plan', default_plan: 'shipping_free' },
|
|
586
|
+
{ key: 'finance', scope: 'account', field: 'finance_plan', default_plan: 'finance_free' },
|
|
587
|
+
// The desk screen has its own copy of this ladder (UI-82), because Tickets is
|
|
588
|
+
// switched on there as well as here. Both call tickets_set_plan, so the plan
|
|
589
|
+
// is one line item on the consolidated subscription either way.
|
|
590
|
+
{ key: 'tickets', scope: 'account', field: 'tickets_plan', default_plan: 'tickets_free' },
|
|
591
|
+
// No free tier and no account field: the entitlement IS the subscription item
|
|
592
|
+
// (or a paid membership, which includes the standard tier at no charge).
|
|
593
|
+
{ key: 'bot_protection', scope: 'account' },
|
|
594
|
+
{ key: 'static_website', scope: 'resource' },
|
|
595
|
+
{ key: 'profile_phone', scope: 'resource' },
|
|
596
|
+
// Xuda Verify (verify_module) has no PLAN_OBJ category: every check is metered
|
|
597
|
+
// per call into verify_meter and nothing is charged to the account today.
|
|
598
|
+
{ key: 'trust_center', scope: 'metered' },
|
|
599
|
+
];
|
|
600
|
+
|
|
601
|
+
// The individual paid things inside a per-resource module, so the billing
|
|
602
|
+
// screen can be the ONE place that shows everything the account pays for.
|
|
603
|
+
// A site or a number on a free tier is not a subscription and is left out.
|
|
604
|
+
const _module_resource_items = async (uid) => {
|
|
605
|
+
const items = { static_website: [], profile_phone: [] };
|
|
606
|
+
|
|
607
|
+
try {
|
|
608
|
+
const ret = await db_module.find_couch_query(
|
|
609
|
+
'xuda_master',
|
|
610
|
+
{ selector: { docType: 'app', app_type: 'static_website', app_uId: uid }, limit: 1000 },
|
|
611
|
+
true,
|
|
612
|
+
);
|
|
613
|
+
for (const app of ret?.docs || []) {
|
|
614
|
+
const tier = app.static_website?.plan_tier || app.deploy_data?.static_website_hosting_plan || 'free';
|
|
615
|
+
const plan = _conf.PLAN_OBJ?.[`static_website_${tier}`];
|
|
616
|
+
const price = Number(plan?.price) || 0;
|
|
617
|
+
if (!price) continue;
|
|
618
|
+
items.static_website.push({
|
|
619
|
+
id: app._id,
|
|
620
|
+
label: app.app_name || app.name || app.deploy_data?.domain || app._id,
|
|
621
|
+
plan_id: plan.id || `static_website_${tier}`,
|
|
622
|
+
plan_name: plan.name || tier,
|
|
623
|
+
price,
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
} catch (e) {
|
|
627
|
+
console.error('[get_module_subscriptions static_website]', e.message);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
try {
|
|
631
|
+
const ret = await db_module.find_couch_query(
|
|
632
|
+
'xuda_master',
|
|
633
|
+
{ selector: { docType: 'voice_number', owner_uid: uid, status: { $ne: 'released' } }, limit: 500 },
|
|
634
|
+
true,
|
|
635
|
+
);
|
|
636
|
+
const profiles = new Map();
|
|
637
|
+
for (const number of ret?.docs || []) {
|
|
638
|
+
let profile = profiles.get(number.profile_id);
|
|
639
|
+
if (profile === undefined) {
|
|
640
|
+
const pr = await db_module.get_couch_doc('xuda_master', number.profile_id);
|
|
641
|
+
profile = pr.code > -1 ? pr.data : null;
|
|
642
|
+
profiles.set(number.profile_id, profile);
|
|
643
|
+
}
|
|
644
|
+
const tier = profile?.voice_plan?.tier || 'free';
|
|
645
|
+
const plan = _conf.PLAN_OBJ?.[`profile_phone_${tier}`];
|
|
646
|
+
// billing.monthly_total is the number's FULL monthly (rental plus the
|
|
647
|
+
// tier's service fee), so it wins when set. A number on the free tier
|
|
648
|
+
// still costs its rental, which is exactly what this row must show.
|
|
649
|
+
const rental = Number(number.billing?.monthly_total) || 0;
|
|
650
|
+
const price = rental > 0 ? rental : Number(plan?.price) || 0;
|
|
651
|
+
if (!price) continue;
|
|
652
|
+
items.profile_phone.push({
|
|
653
|
+
id: number._id,
|
|
654
|
+
label: number.friendly_name || number.phone_number || number._id,
|
|
655
|
+
plan_id: plan?.id || `profile_phone_${tier}`,
|
|
656
|
+
plan_name: plan?.name || 'Number',
|
|
657
|
+
price,
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
} catch (e) {
|
|
661
|
+
console.error('[get_module_subscriptions profile_phone]', e.message);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
return items;
|
|
665
|
+
};
|
|
666
|
+
|
|
667
|
+
// Per-module subscription state for the billing screen: what the account is on
|
|
668
|
+
// now, and whether the module is switched on at all.
|
|
669
|
+
export const get_module_subscriptions = async function (req = {}) {
|
|
670
|
+
try {
|
|
671
|
+
const uid = req?.uid;
|
|
672
|
+
if (!uid) return { code: -401, data: 'not authenticated' };
|
|
673
|
+
const ar = await db_module.get_couch_doc('xuda_accounts', uid);
|
|
674
|
+
if (ar.code < 0 || !ar.data) return { code: -404, data: 'account not found' };
|
|
675
|
+
const account = ar.data;
|
|
676
|
+
const plans = _conf.PLAN_OBJ || {};
|
|
677
|
+
const is_member = !!(account.membership_plan && account.membership_plan !== 'free');
|
|
678
|
+
// A category is only offerable once its paid tiers carry a real Stripe
|
|
679
|
+
// price, otherwise "activate" would set a plan nobody is billed for.
|
|
680
|
+
const billable = (key) =>
|
|
681
|
+
Object.values(plans).some((p) => p.category === key && Number(p.price) > 0 && p.price_id && !/TODO/.test(p.price_id));
|
|
682
|
+
|
|
683
|
+
const resource_items = await _module_resource_items(uid);
|
|
684
|
+
|
|
685
|
+
const modules = _MODULE_SUBSCRIPTIONS.map((m) => {
|
|
686
|
+
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
|
+
if (m.scope === 'resource' || m.scope === 'metered') {
|
|
692
|
+
const price = base.items.reduce((sum, i) => sum + Number(i.price || 0), 0);
|
|
693
|
+
return {
|
|
694
|
+
...base,
|
|
695
|
+
active: m.scope === 'metered' || base.items.length > 0,
|
|
696
|
+
included: m.scope === 'metered',
|
|
697
|
+
plan_id: '',
|
|
698
|
+
plan_name: '',
|
|
699
|
+
price,
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
const plan_id = account[m.field] || m.default_plan;
|
|
703
|
+
const plan = plans[plan_id] || {};
|
|
704
|
+
const price = Number(plan.price) || 0;
|
|
705
|
+
return {
|
|
706
|
+
...base,
|
|
707
|
+
plan_id,
|
|
708
|
+
plan_name: plan.name || plan_id,
|
|
709
|
+
price,
|
|
710
|
+
active: price > 0,
|
|
711
|
+
included: false,
|
|
712
|
+
changed_ts: account[`${m.field}_changed`] || null,
|
|
713
|
+
};
|
|
714
|
+
});
|
|
715
|
+
return { code: 1, data: { modules, membership_plan: account.membership_plan || 'free' } };
|
|
716
|
+
} catch (err) {
|
|
717
|
+
return { code: -1, data: err.message };
|
|
718
|
+
}
|
|
719
|
+
};
|
|
720
|
+
|
|
559
721
|
// Which website-builder image modes an account may use. SVG and stock are always
|
|
560
722
|
// available; the two gpt-image modes (gen_low = img-1, gen_high = img-2) require a
|
|
561
723
|
// paid membership AND a non-free workspace credit plan. Single source of truth for
|
|
@@ -1036,6 +1198,21 @@ export const recompute_abuse_signals = async function (req) {
|
|
|
1036
1198
|
}
|
|
1037
1199
|
};
|
|
1038
1200
|
|
|
1201
|
+
// xuda.network personas (ambassadors + mentors) are seed accounts on
|
|
1202
|
+
// @ambassadors.xuda.network / @mentors.xuda.network, not customers, so every
|
|
1203
|
+
// ops list and search hides them. Three independent markers, any one is
|
|
1204
|
+
// enough: the signup flag, the `xuda_network_*` source stamped at creation,
|
|
1205
|
+
// and the seed mail domains. They agree on all of them today, and belt-and-
|
|
1206
|
+
// braces means a persona created through a future surface still stays out.
|
|
1207
|
+
const _NETWORK_PERSONA_DOMAINS = ['ambassadors.xuda.network', 'mentors.xuda.network'];
|
|
1208
|
+
const _is_network_persona = (a = {}) => {
|
|
1209
|
+
const info = a.account_info || {};
|
|
1210
|
+
if (info.is_xuda_network_ambassador === true) return true;
|
|
1211
|
+
if (String(a.source || '').startsWith('xuda_network_')) return true;
|
|
1212
|
+
const email = String(info.email || a.email || '').toLowerCase();
|
|
1213
|
+
return _NETWORK_PERSONA_DOMAINS.some((d) => email.endsWith(`@${d}`));
|
|
1214
|
+
};
|
|
1215
|
+
|
|
1039
1216
|
// Admin-only: list every account currently flagged. The dashboard's
|
|
1040
1217
|
// /admin/abuse page reads from here.
|
|
1041
1218
|
export const get_flagged_accounts = async function (req) {
|
|
@@ -1054,7 +1231,7 @@ export const get_flagged_accounts = async function (req) {
|
|
|
1054
1231
|
limit: 500,
|
|
1055
1232
|
});
|
|
1056
1233
|
|
|
1057
|
-
const rows = (find_ret?.docs || []).map((a) => ({
|
|
1234
|
+
const rows = (find_ret?.docs || []).filter((a) => !_is_network_persona(a)).map((a) => ({
|
|
1058
1235
|
uid: a._id,
|
|
1059
1236
|
email: a.email,
|
|
1060
1237
|
first_name: a.first_name,
|
|
@@ -1973,7 +2150,10 @@ export const ops_list_terminations = async function (req = {}) {
|
|
|
1973
2150
|
db_module.find_couch_query('xuda_accounts', { selector: { docType: 'account', account_termination_status: 1 }, limit: 9999 }, true),
|
|
1974
2151
|
]);
|
|
1975
2152
|
const by_id = new Map();
|
|
1976
|
-
for (const a of [...(susp_ret?.docs || []), ...(term_ret?.docs || [])])
|
|
2153
|
+
for (const a of [...(susp_ret?.docs || []), ...(term_ret?.docs || [])]) {
|
|
2154
|
+
if (_is_network_persona(a)) continue;
|
|
2155
|
+
by_id.set(a._id, a);
|
|
2156
|
+
}
|
|
1977
2157
|
const rows = [...by_id.values()].map((a) => ({
|
|
1978
2158
|
account_uid: a._id,
|
|
1979
2159
|
email: a.account_info?.email,
|
|
@@ -2015,6 +2195,8 @@ const _ops_account_summary = (a) => ({
|
|
|
2015
2195
|
});
|
|
2016
2196
|
|
|
2017
2197
|
// Find accounts by email / name / uid (substring, case-insensitive).
|
|
2198
|
+
// xuda.network personas are dropped AFTER the query, so the fetch limit is
|
|
2199
|
+
// raised to keep a full page of real customers when personas match the term.
|
|
2018
2200
|
export const ops_find_account = async function (req = {}) {
|
|
2019
2201
|
try {
|
|
2020
2202
|
if (!_ops_is_super(req)) return { code: -403, data: 'superuser only' };
|
|
@@ -2030,9 +2212,39 @@ export const ops_find_account = async function (req = {}) {
|
|
|
2030
2212
|
{ _id: { $regex: rx } },
|
|
2031
2213
|
],
|
|
2032
2214
|
},
|
|
2033
|
-
limit:
|
|
2215
|
+
limit: 200,
|
|
2034
2216
|
}, true);
|
|
2035
|
-
|
|
2217
|
+
const rows = (ret.docs || []).filter((a) => !_is_network_persona(a)).slice(0, 25).map(_ops_account_summary);
|
|
2218
|
+
return { code: 1, data: { rows } };
|
|
2219
|
+
} catch (err) { return { code: -1, data: err.message }; }
|
|
2220
|
+
};
|
|
2221
|
+
|
|
2222
|
+
// Every customer account, newest first, paged. Backs the "All customers" tab.
|
|
2223
|
+
// Deleted accounts (stat 5) and xuda.network personas are left out, so the
|
|
2224
|
+
// total is the real customer base rather than a raw doc count. A few hundred
|
|
2225
|
+
// docs, so the page is cut in memory (Mango has no stable sort here without a
|
|
2226
|
+
// dedicated index, and skip/limit paging over an unsorted view is not stable).
|
|
2227
|
+
export const ops_list_accounts = async function (req = {}) {
|
|
2228
|
+
try {
|
|
2229
|
+
if (!_ops_is_super(req)) return { code: -403, data: 'superuser only' };
|
|
2230
|
+
const limit = Math.min(Math.max(parseInt(req.limit, 10) || 50, 1), 200);
|
|
2231
|
+
const skip = Math.max(parseInt(req.skip, 10) || 0, 0);
|
|
2232
|
+
const ret = await db_module.find_couch_query('xuda_accounts', {
|
|
2233
|
+
selector: { docType: 'account' },
|
|
2234
|
+
limit: 100000,
|
|
2235
|
+
}, true);
|
|
2236
|
+
const all = (ret.docs || [])
|
|
2237
|
+
.filter((a) => a.stat !== 5 && !_is_network_persona(a))
|
|
2238
|
+
.sort((a, b) => (b.ts || 0) - (a.ts || 0));
|
|
2239
|
+
return {
|
|
2240
|
+
code: 1,
|
|
2241
|
+
data: {
|
|
2242
|
+
rows: all.slice(skip, skip + limit).map(_ops_account_summary),
|
|
2243
|
+
total: all.length,
|
|
2244
|
+
skip,
|
|
2245
|
+
limit,
|
|
2246
|
+
},
|
|
2247
|
+
};
|
|
2036
2248
|
} catch (err) { return { code: -1, data: err.message }; }
|
|
2037
2249
|
};
|
|
2038
2250
|
|
|
@@ -2143,7 +2355,9 @@ export const ops_add_ai_credits = async function (req = {}) {
|
|
|
2143
2355
|
if (ar.code < 0 || !ar.data) return { code: -1, data: 'account not found' };
|
|
2144
2356
|
const account = ar.data;
|
|
2145
2357
|
const details = `admin grant ${credits} by ${req.uid} @ ${Date.now()}`;
|
|
2146
|
-
|
|
2358
|
+
// An ops grant is a deliberate gift on top of the plan, so it must survive
|
|
2359
|
+
// the period roll rather than evaporating at the next renewal.
|
|
2360
|
+
const cr = await record_ai_credit(req.uid, credits, 'admin_grant', details, account_uid, { kind: CREDIT_KIND.TOPUP });
|
|
2147
2361
|
if (cr.code < 0) return { code: -1, data: cr.data };
|
|
2148
2362
|
if (req.notify) {
|
|
2149
2363
|
notification_msa.submit_notification({ type: 'account', uid_arr: [account_uid], subject: `${credits} AI credits added to your account`, body: `<h2>${credits} AI credits added</h2><p>Our team has added ${credits} AI credits to your account. Enjoy!</p>`, delivery_method: ['banner', 'email'], display_type: 'success' });
|
|
@@ -3182,6 +3396,19 @@ export const verify_account = async function (req) {
|
|
|
3182
3396
|
console.warn('[verify_account] confirm_email banner cleanup failed:', e.message);
|
|
3183
3397
|
}
|
|
3184
3398
|
|
|
3399
|
+
// A picture uploaded before verification deliberately did NOT generate an
|
|
3400
|
+
// avatar (see can_generate_avatar). The account is verified as of the save
|
|
3401
|
+
// above, so start the generation that was deferred. Fire-and-forget:
|
|
3402
|
+
// generation is heavy (vision + image ops) and must not hold up the verify
|
|
3403
|
+
// response, and ensure_profile_avatar re-reads the doc and does its own
|
|
3404
|
+
// guarding — picture present, no avatar yet, not already generating — so a
|
|
3405
|
+
// user who never uploaded one is a no-op.
|
|
3406
|
+
try {
|
|
3407
|
+
ensure_profile_avatar({ uid: account_id }).catch((e) => console.warn('[verify_account] deferred avatar start failed:', e.message));
|
|
3408
|
+
} catch (e) {
|
|
3409
|
+
console.warn('[verify_account] deferred avatar start threw:', e.message);
|
|
3410
|
+
}
|
|
3411
|
+
|
|
3185
3412
|
// is_boarded rides along so the verify route can tell the client whether
|
|
3186
3413
|
// onboarding is still pending (it is, for a fresh email signup).
|
|
3187
3414
|
return { code: 1, data: { ...ret_acc.data, is_boarded: !!obj.isBoarded } };
|
|
@@ -6587,24 +6814,38 @@ export const save_cache_hit = async function (_id) {
|
|
|
6587
6814
|
///////////////////////////////
|
|
6588
6815
|
|
|
6589
6816
|
export const get_account_ai_usage = async function (req, job_id, headers) {
|
|
6590
|
-
//
|
|
6591
|
-
//
|
|
6592
|
-
//
|
|
6593
|
-
//
|
|
6594
|
-
//
|
|
6595
|
-
//
|
|
6817
|
+
// Called with NO window at all (the nav meter, the avatar ring, account boot,
|
|
6818
|
+
// the low-credit alert), this reports the CURRENT BILLING PERIOD: usage.total
|
|
6819
|
+
// is the period's spend and credits.total is the period allowance plus the
|
|
6820
|
+
// topup still available. That keeps `credits.total - usage.total` equal to the
|
|
6821
|
+
// real remaining balance, which matters because the dashboard reads those two
|
|
6822
|
+
// numbers directly and computes the difference itself — so the meter stays
|
|
6823
|
+
// correct with no frontend change.
|
|
6824
|
+
//
|
|
6825
|
+
// Callers that pass an explicit range (the WorkspaceUsage chart) still get raw
|
|
6826
|
+
// windowed spend and the full ledger total, unchanged.
|
|
6596
6827
|
const { uid, year_from = 2000, month_from = 1, day_from = 1, year_to = 2999, month_to = 12, day_to = 31 } = req;
|
|
6597
6828
|
try {
|
|
6598
6829
|
const app_id = await get_account_default_project_id(uid);
|
|
6599
6830
|
|
|
6831
|
+
// ts_from / ts_to override the Y/M/D window. Billing periods start at an
|
|
6832
|
+
// arbitrary time of day (Isaac's renews 04:00 on the 2nd), and the Y/M/D
|
|
6833
|
+
// form can only express midnight boundaries, so a day-granular window would
|
|
6834
|
+
// count a few hours of the neighbouring period's spend against this one.
|
|
6835
|
+
const has_ts = Number(req?.ts_from) > 0 || Number(req?.ts_to) > 0;
|
|
6836
|
+
const has_ymd = req?.year_from != null || req?.year_to != null || req?.month_from != null || req?.month_to != null || req?.day_from != null || req?.day_to != null;
|
|
6837
|
+
// No window requested at all -> scope to the account's current credit period.
|
|
6838
|
+
const acct_doc = !has_ts && !has_ymd ? await db_module.get_couch_doc_native('xuda_accounts', uid) : null;
|
|
6839
|
+
const period = acct_doc ? _current_credit_period(acct_doc) : null;
|
|
6840
|
+
|
|
6600
6841
|
// const timestamp_from = new Date(Number(year_from) || year, (Number(month_from) || month) - 1, Number(day_from) || day).getTime();
|
|
6601
6842
|
// const timestamp_to = new Date(Number(year_to) || year, (Number(month_to) || month) - 1, Number(day_to) || day).getTime();
|
|
6602
6843
|
|
|
6603
6844
|
// BEGINNING OF DAY (00:00:00.000)
|
|
6604
|
-
const timestamp_from = new Date(Number(year_from), Number(month_from) - 1, Number(day_from)).setHours(0, 0, 0, 0);
|
|
6845
|
+
const timestamp_from = period ? period.start : has_ts ? Number(req.ts_from) || 0 : new Date(Number(year_from), Number(month_from) - 1, Number(day_from)).setHours(0, 0, 0, 0);
|
|
6605
6846
|
|
|
6606
6847
|
// END OF DAY (23:59:59.999)
|
|
6607
|
-
const timestamp_to = new Date(Number(year_to), Number(month_to) - 1, Number(day_to)).setHours(23, 59, 59, 999);
|
|
6848
|
+
const timestamp_to = period ? Date.now() : has_ts ? Number(req.ts_to) || Date.now() : new Date(Number(year_to), Number(month_to) - 1, Number(day_to)).setHours(23, 59, 59, 999);
|
|
6608
6849
|
|
|
6609
6850
|
const query = {
|
|
6610
6851
|
// include_docs: true,
|
|
@@ -6648,7 +6889,24 @@ export const get_account_ai_usage = async function (req, job_id, headers) {
|
|
|
6648
6889
|
credits_obj[source] += credits;
|
|
6649
6890
|
}
|
|
6650
6891
|
|
|
6651
|
-
|
|
6892
|
+
// Period-scoped read: the ledger total above counts every active grant, but
|
|
6893
|
+
// topup already spent in EARLIER periods is invisible to this period's usage
|
|
6894
|
+
// figure, so it has to be deducted here. Without that, rolling the period
|
|
6895
|
+
// would hand the customer back topup credits they had already spent.
|
|
6896
|
+
let period_meta = null;
|
|
6897
|
+
if (period) {
|
|
6898
|
+
let period_grant = 0;
|
|
6899
|
+
let topup_total = 0;
|
|
6900
|
+
for (const [src, amount] of Object.entries(credits_obj)) {
|
|
6901
|
+
if (_credit_kind({ source: src }) === CREDIT_KIND.TOPUP) topup_total += amount;
|
|
6902
|
+
else period_grant += amount;
|
|
6903
|
+
}
|
|
6904
|
+
const topup_available = Math.max(0, topup_total - Math.max(0, Number(acct_doc?.credits_topup_consumed) || 0));
|
|
6905
|
+
total_credits = period_grant + topup_available;
|
|
6906
|
+
period_meta = { start_ts: period.start, end_ts: period.end, source: period.source, period_grant, topup_total, topup_available };
|
|
6907
|
+
}
|
|
6908
|
+
|
|
6909
|
+
let data = { credits: { ...credits_obj, total: total_credits }, usage: { profile, projects: [], total: total_usage }, packs: _conf.ai_credit_packs || [], period: period_meta };
|
|
6652
6910
|
|
|
6653
6911
|
if (job_id) {
|
|
6654
6912
|
data.profile_info = {};
|
|
@@ -6750,9 +7008,143 @@ const _usage_total = function (usage_data) {
|
|
|
6750
7008
|
return Object.values(by_profile).reduce((a, b) => a + (Number(b) || 0), 0);
|
|
6751
7009
|
};
|
|
6752
7010
|
|
|
6753
|
-
// Wide window so "used" reads as all-time spend
|
|
7011
|
+
// Wide window so "used" reads as all-time spend. Kept for the few places that
|
|
7012
|
+
// genuinely want all-time (ops snapshots, reporting). It is NOT the basis of the
|
|
7013
|
+
// balance any more: see get_credit_balance below for why that was broken.
|
|
6754
7014
|
const _LIFETIME_WINDOW = { year_from: 2000, month_from: 1, day_from: 1, year_to: 2999, month_to: 12, day_to: 31 };
|
|
6755
7015
|
|
|
7016
|
+
/////////////////////////////////////////////////////////////////////////
|
|
7017
|
+
// CREDIT BALANCE
|
|
7018
|
+
//
|
|
7019
|
+
// The balance used to be computed two different ways, and they disagreed:
|
|
7020
|
+
// evaluate_credit_gate did (ledger grants - LIFETIME usage) while the Credit
|
|
7021
|
+
// Management page did (plan entitlement - LIFETIME usage). Both were unstable,
|
|
7022
|
+
// because grants expire after 30 days while usage was counted forever, so the
|
|
7023
|
+
// balance could only ever drift negative. A paying Pro customer was hard-blocked
|
|
7024
|
+
// from AI while his own dashboard told him he had 539 credits left.
|
|
7025
|
+
//
|
|
7026
|
+
// The model now matches how the plans are actually sold:
|
|
7027
|
+
//
|
|
7028
|
+
// period pool — the plan's allowance for the CURRENT billing period. Granted
|
|
7029
|
+
// when the invoice is paid, and it does not roll over. The next
|
|
7030
|
+
// period starts from the plan's number again.
|
|
7031
|
+
// topup pool — separately acquired credits (bought packs, ops grants,
|
|
7032
|
+
// bonuses). Never reset; they persist until spent.
|
|
7033
|
+
//
|
|
7034
|
+
// Spend comes out of the period pool first and only overflows into topup once
|
|
7035
|
+
// the period allowance is exhausted. Overflow consumed in PAST periods is
|
|
7036
|
+
// remembered on the account as credits_topup_consumed, because usage is only
|
|
7037
|
+
// ever queried for the current period — without that counter, rolling the
|
|
7038
|
+
// period would silently hand back topup credits the customer already spent.
|
|
7039
|
+
/////////////////////////////////////////////////////////////////////////
|
|
7040
|
+
|
|
7041
|
+
// The window the current period pool is measured over. A paid account gets its
|
|
7042
|
+
// real Stripe period (stamped by the invoice.paid webhook). Anything else falls
|
|
7043
|
+
// back to the calendar month, so a free account's daily drip still resets on a
|
|
7044
|
+
// predictable boundary instead of being compared against all-time spend. A
|
|
7045
|
+
// stored period that has already elapsed is treated as absent, so a missed or
|
|
7046
|
+
// late renewal webhook degrades to the fallback instead of freezing the window.
|
|
7047
|
+
const _current_credit_period = function (account_doc) {
|
|
7048
|
+
const now = Date.now();
|
|
7049
|
+
const start = Number(account_doc?.credits_period_start_ts) || 0;
|
|
7050
|
+
const end = Number(account_doc?.credits_period_end_ts) || 0;
|
|
7051
|
+
if (start && end && now >= start && now < end) return { start, end, source: 'invoice' };
|
|
7052
|
+
const d = new Date(now);
|
|
7053
|
+
return { start: Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), 1), end: Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + 1, 1), source: 'calendar' };
|
|
7054
|
+
};
|
|
7055
|
+
|
|
7056
|
+
// Single source of truth for "how many credits does this account have left".
|
|
7057
|
+
// Everything that gates, meters or displays credits must go through here.
|
|
7058
|
+
export const get_credit_balance = async function (uid, account_doc) {
|
|
7059
|
+
const doc = account_doc || (await db_module.get_couch_doc_native('xuda_accounts', uid));
|
|
7060
|
+
const period = _current_credit_period(doc);
|
|
7061
|
+
|
|
7062
|
+
// Active (stat 3) grants, summed by source, then split into the two pools.
|
|
7063
|
+
// Expired period grants drop out of this view on their own, which is what
|
|
7064
|
+
// makes the period pool reset. Legacy docs carry no credit_kind, so the
|
|
7065
|
+
// classification falls back to the source name (see _credit_kind).
|
|
7066
|
+
const view = await db_module.get_couch_view('xuda_billing', 'ai_credits', { startkey: [uid, ''], endkey: [uid, 'zzz'], reduce: true, group_level: 999 });
|
|
7067
|
+
const by_source = {};
|
|
7068
|
+
let period_grant = 0;
|
|
7069
|
+
let topup_total = 0;
|
|
7070
|
+
for (const row of view?.data?.rows || []) {
|
|
7071
|
+
const source = row.key[1];
|
|
7072
|
+
const credits = Number(row.value) || 0;
|
|
7073
|
+
by_source[source] = (by_source[source] || 0) + credits;
|
|
7074
|
+
if (_credit_kind({ source }) === CREDIT_KIND.TOPUP) topup_total += credits;
|
|
7075
|
+
else period_grant += credits;
|
|
7076
|
+
}
|
|
7077
|
+
|
|
7078
|
+
const usage_ret = await get_account_ai_usage({ uid, ts_from: period.start, ts_to: Date.now() });
|
|
7079
|
+
const used = _usage_total(usage_ret?.data?.usage);
|
|
7080
|
+
|
|
7081
|
+
const topup_consumed = Math.max(0, Number(doc?.credits_topup_consumed) || 0);
|
|
7082
|
+
const topup_available = Math.max(0, topup_total - topup_consumed);
|
|
7083
|
+
|
|
7084
|
+
// An "unlimited" tier (Enterprise T3) resolves to Infinity and must never gate.
|
|
7085
|
+
const unlimited = !Number.isFinite(get_effective_entitlements(doc).ai_credits);
|
|
7086
|
+
const granted = period_grant + topup_available;
|
|
7087
|
+
|
|
7088
|
+
return {
|
|
7089
|
+
period_start_ts: period.start,
|
|
7090
|
+
period_end_ts: period.end,
|
|
7091
|
+
period_source: period.source,
|
|
7092
|
+
period_grant,
|
|
7093
|
+
topup_total,
|
|
7094
|
+
topup_consumed,
|
|
7095
|
+
topup_available,
|
|
7096
|
+
granted,
|
|
7097
|
+
used,
|
|
7098
|
+
remaining: unlimited ? Infinity : Math.round((granted - used) * 100) / 100,
|
|
7099
|
+
unlimited,
|
|
7100
|
+
by_source,
|
|
7101
|
+
usage_profile: usage_ret?.data?.usage?.profile,
|
|
7102
|
+
};
|
|
7103
|
+
};
|
|
7104
|
+
|
|
7105
|
+
// Close out the period that just ended and open the next one. Called from the
|
|
7106
|
+
// invoice.paid webhook right before the new grant lands.
|
|
7107
|
+
//
|
|
7108
|
+
// The overflow bookkeeping is the important half: any spend beyond the old
|
|
7109
|
+
// period's allowance came out of topup, and because the next balance read only
|
|
7110
|
+
// looks at the NEW period's usage, that spend would otherwise be forgotten and
|
|
7111
|
+
// the customer would get those topup credits back every month.
|
|
7112
|
+
export const roll_credit_period = async function (uid, next_start_ts, next_end_ts) {
|
|
7113
|
+
try {
|
|
7114
|
+
const doc = await db_module.get_couch_doc_native('xuda_accounts', uid);
|
|
7115
|
+
if (!doc) return { code: -1, data: 'account not found' };
|
|
7116
|
+
|
|
7117
|
+
const prev_start = Number(doc.credits_period_start_ts) || 0;
|
|
7118
|
+
const prev_end = Number(doc.credits_period_end_ts) || 0;
|
|
7119
|
+
if (prev_start && prev_end && Number(next_start_ts) > prev_start) {
|
|
7120
|
+
const prev = await get_credit_balance(uid, doc);
|
|
7121
|
+
const prev_overflow = Math.max(0, prev.used - prev.period_grant);
|
|
7122
|
+
if (prev_overflow > 0) doc.credits_topup_consumed = (Number(doc.credits_topup_consumed) || 0) + prev_overflow;
|
|
7123
|
+
|
|
7124
|
+
// Retire the old period's grants so they cannot be spent in the new one.
|
|
7125
|
+
// Scoped to period grants whose window has closed; topup is never touched.
|
|
7126
|
+
const stale = await db_module.find_couch_query('xuda_billing', { selector: { docType: 'ai_credit', credited_uid: uid, stat: 3 }, limit: 9999 });
|
|
7127
|
+
for (const c of stale?.docs || []) {
|
|
7128
|
+
if (_credit_kind(c) !== CREDIT_KIND.PERIOD) continue;
|
|
7129
|
+
const ended = Number(c.period_end_ts) || Number(c.date_created_ts) || 0;
|
|
7130
|
+
if (ended && ended > Number(next_start_ts)) continue; // belongs to the period we are opening
|
|
7131
|
+
c.stat = 5;
|
|
7132
|
+
c.stat_ts = Date.now();
|
|
7133
|
+
c.stat_reason = 'period closed';
|
|
7134
|
+
await db_module.save_couch_doc('xuda_billing', c);
|
|
7135
|
+
}
|
|
7136
|
+
}
|
|
7137
|
+
|
|
7138
|
+
doc.credits_period_start_ts = Number(next_start_ts) || Date.now();
|
|
7139
|
+
doc.credits_period_end_ts = Number(next_end_ts) || 0;
|
|
7140
|
+
const save = await db_module.save_couch_doc('xuda_accounts', doc);
|
|
7141
|
+
return { code: save.code > 0 ? 1 : -1, data: { period_start_ts: doc.credits_period_start_ts, period_end_ts: doc.credits_period_end_ts, topup_consumed: doc.credits_topup_consumed || 0 } };
|
|
7142
|
+
} catch (err) {
|
|
7143
|
+
console.error('[roll_credit_period]', err?.message || err);
|
|
7144
|
+
return { code: -1, data: err?.message };
|
|
7145
|
+
}
|
|
7146
|
+
};
|
|
7147
|
+
|
|
6756
7148
|
// Per-model / per-source spend is not emitted by the usage view, so re-derive it
|
|
6757
7149
|
// from the raw ai_usage docs and CALIBRATE to the view's authoritative total, so the
|
|
6758
7150
|
// numbers stay in the same credit unit (factor = view_total / raw_token_cost_total).
|
|
@@ -6764,7 +7156,9 @@ const _get_scoped_usage = async function (owner_uid, need_raw) {
|
|
|
6764
7156
|
const cached = _scoped_usage_cache[owner_uid];
|
|
6765
7157
|
if (cached && now - cached.ts < _SCOPED_USAGE_TTL && (!need_raw || cached.has_raw)) return cached.data;
|
|
6766
7158
|
|
|
6767
|
-
|
|
7159
|
+
// No window: per-profile and per-member caps are an allocation of the CURRENT
|
|
7160
|
+
// period's pool, so they have to be measured over the same period the pool is.
|
|
7161
|
+
const usage_ret = await get_account_ai_usage({ uid: owner_uid });
|
|
6768
7162
|
const { by_profile, by_user } = _fold_usage(usage_ret?.data?.usage?.profile);
|
|
6769
7163
|
const total = _usage_total(usage_ret?.data?.usage);
|
|
6770
7164
|
|
|
@@ -6895,10 +7289,15 @@ export const evaluate_credit_gate = async function (req) {
|
|
|
6895
7289
|
const account_doc = await db_module.get_couch_doc_native('xuda_accounts', owner);
|
|
6896
7290
|
const rules = account_doc?.credit_rules;
|
|
6897
7291
|
|
|
6898
|
-
//
|
|
7292
|
+
// No rules or disabled => the single account-wide pool cap, which is the
|
|
7293
|
+
// default for almost every account. This is what hard-blocks AI, so it MUST
|
|
7294
|
+
// read the same balance the dashboard shows the customer. It used to compute
|
|
7295
|
+
// its own (ledger grants - lifetime usage) while the Credit Management page
|
|
7296
|
+
// computed (plan entitlement - lifetime usage); the two drifted apart and a
|
|
7297
|
+
// paying Pro customer was blocked while his page reported 539 credits left.
|
|
6899
7298
|
if (!rules || rules.enabled === false) {
|
|
6900
|
-
const
|
|
6901
|
-
const over =
|
|
7299
|
+
const bal = await get_credit_balance(owner, account_doc);
|
|
7300
|
+
const over = bal.remaining < -0.5;
|
|
6902
7301
|
return { block: over, scope: over ? 'pool' : null, reason: over ? _SCOPE_REASON.ledger : '', hard_breached: over ? [{ scope: 'pool', key: 'ledger' }] : [], soft_breached: [], account_id: owner };
|
|
6903
7302
|
}
|
|
6904
7303
|
|
|
@@ -7009,15 +7408,17 @@ export const get_credit_management = async function (req) {
|
|
|
7009
7408
|
const plan = account_doc?.membership_plan || 'free';
|
|
7010
7409
|
const can_edit = _is_team_tier(plan);
|
|
7011
7410
|
const rules = account_doc?.credit_rules || _default_credit_rules();
|
|
7012
|
-
|
|
7013
|
-
|
|
7014
|
-
const
|
|
7411
|
+
// Report exactly what the gate enforces. These were two different formulas
|
|
7412
|
+
// and the page cheerfully showed credits to a customer the gate was blocking.
|
|
7413
|
+
const bal = await get_credit_balance(uid, account_doc);
|
|
7414
|
+
const unlimited = bal.unlimited;
|
|
7415
|
+
const entitlements = { ai_credits: unlimited ? null : bal.granted, unlimited };
|
|
7015
7416
|
|
|
7016
7417
|
const su = await _get_scoped_usage(uid, true);
|
|
7017
|
-
const remaining = unlimited ? null :
|
|
7418
|
+
const remaining = unlimited ? null : bal.remaining;
|
|
7018
7419
|
const usage_breakdown = {
|
|
7019
|
-
window: { mode:
|
|
7020
|
-
pool: { used: Math.round(
|
|
7420
|
+
window: { mode: 'period', period_start_ts: bal.period_start_ts, period_end_ts: bal.period_end_ts, period_grant: bal.period_grant, topup_available: bal.topup_available },
|
|
7421
|
+
pool: { used: Math.round(bal.used * 100) / 100, remaining },
|
|
7021
7422
|
by_profile: su.by_profile,
|
|
7022
7423
|
by_user: su.by_user,
|
|
7023
7424
|
by_model: su.by_model,
|
|
@@ -7307,7 +7708,26 @@ export const record_ai_usage = async function (uid, input_tokens, output_tokens,
|
|
|
7307
7708
|
}
|
|
7308
7709
|
};
|
|
7309
7710
|
|
|
7310
|
-
|
|
7711
|
+
// A credit belongs to exactly one of two pools, and they behave differently:
|
|
7712
|
+
//
|
|
7713
|
+
// 'period' — the allowance that comes WITH a plan (the monthly membership
|
|
7714
|
+
// grant, and the free tier's daily drip). It is spent inside one
|
|
7715
|
+
// billing period and does NOT roll over: the next period starts
|
|
7716
|
+
// from the plan's number again.
|
|
7717
|
+
// 'topup' — credits the customer separately acquired (bought packs, ops
|
|
7718
|
+
// grants, the boarding bonus, referral bonuses). These never
|
|
7719
|
+
// reset; they sit there until they are used.
|
|
7720
|
+
//
|
|
7721
|
+
// Legacy docs predate the field, so _credit_kind() infers it from `source`
|
|
7722
|
+
// rather than requiring a backfill.
|
|
7723
|
+
const CREDIT_KIND = { PERIOD: 'period', TOPUP: 'topup' };
|
|
7724
|
+
const _TOPUP_SOURCES = new Set(['purchase', 'admin_grant', 'boarding', 'contact connection']);
|
|
7725
|
+
const _credit_kind = function (doc) {
|
|
7726
|
+
if (doc?.credit_kind === CREDIT_KIND.TOPUP || doc?.credit_kind === CREDIT_KIND.PERIOD) return doc.credit_kind;
|
|
7727
|
+
return _TOPUP_SOURCES.has(doc?.source) ? CREDIT_KIND.TOPUP : CREDIT_KIND.PERIOD;
|
|
7728
|
+
};
|
|
7729
|
+
|
|
7730
|
+
export const record_ai_credit = async function (uid, credits = 0, source, details, credited_uid, opts = {}) {
|
|
7311
7731
|
try {
|
|
7312
7732
|
var dup = await db_module.find_couch_query('xuda_billing', {
|
|
7313
7733
|
selector: {
|
|
@@ -7334,7 +7754,11 @@ export const record_ai_credit = async function (uid, credits = 0, source, detail
|
|
|
7334
7754
|
stat: 3,
|
|
7335
7755
|
credited_uid,
|
|
7336
7756
|
details,
|
|
7757
|
+
credit_kind: opts.kind === CREDIT_KIND.TOPUP ? CREDIT_KIND.TOPUP : CREDIT_KIND.PERIOD,
|
|
7337
7758
|
};
|
|
7759
|
+
if (opts.period_start_ts) credit_doc.period_start_ts = opts.period_start_ts;
|
|
7760
|
+
if (opts.period_end_ts) credit_doc.period_end_ts = opts.period_end_ts;
|
|
7761
|
+
if (opts.stripe_invoice_id) credit_doc.stripe_invoice_id = opts.stripe_invoice_id;
|
|
7338
7762
|
const save_ret = await db_module.save_couch_doc('xuda_billing', credit_doc);
|
|
7339
7763
|
// console.log(save_ret);
|
|
7340
7764
|
broadcast_credits(credited_uid); // live meter: this account just gained credits
|
|
@@ -7359,7 +7783,9 @@ export const add_ai_credits_to_active_accounts = async function () {
|
|
|
7359
7783
|
const _24_hr_ms = 1000 * 60 * 60 * 24;
|
|
7360
7784
|
const active_accounts = await db_module.find_couch_query('xuda_accounts', { selector: { stat: 3, docType: 'account', last_free_daily_ai_credit_ts: { $lt: Date.now() - _24_hr_ms }, ai_workspace_plan: 'free_ai_workspace' }, limit: 99999 });
|
|
7361
7785
|
for await (let account_doc of active_accounts.docs) {
|
|
7362
|
-
|
|
7786
|
+
// The free tier's drip IS its plan allowance, so it is a period credit: it
|
|
7787
|
+
// resets with the period instead of accumulating forever.
|
|
7788
|
+
const ret = await record_ai_credit('system', 1, 'daily credit', 'free daily credit ' + date_str, account_doc._id, { kind: CREDIT_KIND.PERIOD });
|
|
7363
7789
|
if (ret.code > -1) {
|
|
7364
7790
|
account_doc.last_free_daily_ai_credit_ts = Date.now();
|
|
7365
7791
|
account_doc.last_free_daily_ai_credit_id = ret.data.id;
|
|
@@ -7371,11 +7797,23 @@ export const add_ai_credits_to_active_accounts = async function () {
|
|
|
7371
7797
|
export const archive_expire_ai_credits = async function () {
|
|
7372
7798
|
const _24_hr_ms = 1000 * 60 * 60 * 24;
|
|
7373
7799
|
const _mo_ms = _24_hr_ms * 30;
|
|
7800
|
+
const now = Date.now();
|
|
7374
7801
|
|
|
7375
|
-
const active_ai_credits = await db_module.find_couch_query('xuda_billing', { selector: { stat: 3, docType: 'ai_credit', date_created_ts: { $lt:
|
|
7802
|
+
const active_ai_credits = await db_module.find_couch_query('xuda_billing', { selector: { stat: 3, docType: 'ai_credit', date_created_ts: { $lt: now - _mo_ms } }, limit: 99999 });
|
|
7376
7803
|
for await (let ai_credit_doc of active_ai_credits.docs) {
|
|
7804
|
+
// TOPUP credits are the customer's property until they spend them: bought
|
|
7805
|
+
// packs, ops grants, the welcome bonus. This cron used to expire those too,
|
|
7806
|
+
// silently deleting credits people had paid real money for.
|
|
7807
|
+
if (_credit_kind(ai_credit_doc) === CREDIT_KIND.TOPUP) continue;
|
|
7808
|
+
|
|
7809
|
+
// A period grant expires when its PERIOD ends, not 30 days after it was
|
|
7810
|
+
// written. Those differ whenever a month is 31 days long, and the old rule
|
|
7811
|
+
// would have retired the current allowance a day early.
|
|
7812
|
+
const period_end = Number(ai_credit_doc.period_end_ts) || 0;
|
|
7813
|
+
if (period_end && period_end > now) continue;
|
|
7814
|
+
|
|
7377
7815
|
ai_credit_doc.stat = 5;
|
|
7378
|
-
ai_credit_doc.stat_ts =
|
|
7816
|
+
ai_credit_doc.stat_ts = now;
|
|
7379
7817
|
ai_credit_doc.stat_reason = 'expired';
|
|
7380
7818
|
const save_ret = await db_module.save_couch_doc('xuda_billing', ai_credit_doc);
|
|
7381
7819
|
}
|
package/index_ms.mjs
CHANGED
|
@@ -57,6 +57,10 @@ export const get_effective_entitlements = async function (...args) {
|
|
|
57
57
|
return await broker.send_to_queue("get_effective_entitlements", ...args);
|
|
58
58
|
};
|
|
59
59
|
|
|
60
|
+
export const get_module_subscriptions = async function (...args) {
|
|
61
|
+
return await broker.send_to_queue("get_module_subscriptions", ...args);
|
|
62
|
+
};
|
|
63
|
+
|
|
60
64
|
export const _site_build_image_modes = async function (...args) {
|
|
61
65
|
return await broker.send_to_queue("_site_build_image_modes", ...args);
|
|
62
66
|
};
|
|
@@ -181,6 +185,10 @@ export const ops_find_account = async function (...args) {
|
|
|
181
185
|
return await broker.send_to_queue("ops_find_account", ...args);
|
|
182
186
|
};
|
|
183
187
|
|
|
188
|
+
export const ops_list_accounts = async function (...args) {
|
|
189
|
+
return await broker.send_to_queue("ops_list_accounts", ...args);
|
|
190
|
+
};
|
|
191
|
+
|
|
184
192
|
export const ops_account_snapshot = async function (...args) {
|
|
185
193
|
return await broker.send_to_queue("ops_account_snapshot", ...args);
|
|
186
194
|
};
|
|
@@ -509,6 +517,14 @@ export const get_account_ai_usage = async function (...args) {
|
|
|
509
517
|
return await broker.send_to_queue("get_account_ai_usage", ...args);
|
|
510
518
|
};
|
|
511
519
|
|
|
520
|
+
export const get_credit_balance = async function (...args) {
|
|
521
|
+
return await broker.send_to_queue("get_credit_balance", ...args);
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
export const roll_credit_period = async function (...args) {
|
|
525
|
+
return await broker.send_to_queue("roll_credit_period", ...args);
|
|
526
|
+
};
|
|
527
|
+
|
|
512
528
|
export const emit_credit_soft_limit = async function (...args) {
|
|
513
529
|
return await broker.send_to_queue("emit_credit_soft_limit", ...args);
|
|
514
530
|
};
|
package/index_msa.mjs
CHANGED
|
@@ -57,6 +57,10 @@ export const get_effective_entitlements = function (...args) {
|
|
|
57
57
|
broker.send_to_queue_async("get_effective_entitlements", ...args);
|
|
58
58
|
};
|
|
59
59
|
|
|
60
|
+
export const get_module_subscriptions = function (...args) {
|
|
61
|
+
broker.send_to_queue_async("get_module_subscriptions", ...args);
|
|
62
|
+
};
|
|
63
|
+
|
|
60
64
|
export const _site_build_image_modes = function (...args) {
|
|
61
65
|
broker.send_to_queue_async("_site_build_image_modes", ...args);
|
|
62
66
|
};
|
|
@@ -181,6 +185,10 @@ export const ops_find_account = function (...args) {
|
|
|
181
185
|
broker.send_to_queue_async("ops_find_account", ...args);
|
|
182
186
|
};
|
|
183
187
|
|
|
188
|
+
export const ops_list_accounts = function (...args) {
|
|
189
|
+
broker.send_to_queue_async("ops_list_accounts", ...args);
|
|
190
|
+
};
|
|
191
|
+
|
|
184
192
|
export const ops_account_snapshot = function (...args) {
|
|
185
193
|
broker.send_to_queue_async("ops_account_snapshot", ...args);
|
|
186
194
|
};
|
|
@@ -509,6 +517,14 @@ export const get_account_ai_usage = function (...args) {
|
|
|
509
517
|
broker.send_to_queue_async("get_account_ai_usage", ...args);
|
|
510
518
|
};
|
|
511
519
|
|
|
520
|
+
export const get_credit_balance = function (...args) {
|
|
521
|
+
broker.send_to_queue_async("get_credit_balance", ...args);
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
export const roll_credit_period = function (...args) {
|
|
525
|
+
broker.send_to_queue_async("roll_credit_period", ...args);
|
|
526
|
+
};
|
|
527
|
+
|
|
512
528
|
export const emit_credit_soft_limit = function (...args) {
|
|
513
529
|
broker.send_to_queue_async("emit_credit_soft_limit", ...args);
|
|
514
530
|
};
|