openvibers/openvibe.live Commits

Subscribe shows what Billing charges: the creator's tier-1 price when they set one on OpenVibe.VIP (Billing tiers, contracts 0.201.0), else Billing's default (#180)

OP OpenVibers committed 27 days ago · 1f8894d3b2 · 5 files, +28 −6

Changes

ddiff --git a/server/monetization/billing-actions.js b/server/monetization/billing-actions.js
iindex 2219b7d7..5da1ec8a 100644
---- a/server/monetization/billing-actions.js
++++ b/server/monetization/billing-actions.js
@@@ -405,17 +405,26 @@ async function mySubscriptions(liveUserId) {
}
/** { subscribed, subscriberCount } for the channel page. */
++/** What Billing charges for Live's subscription (tier 1): the creator's own price when set on VIP, else the default. */
++function tierOnePrice(t) {
++ if (!t) return null;
++ const one = (t.tiers || []).find((x) => Number(x.tier) === 1 && x.status === 'active');
++ const cents = one ? Number(one.price_cents) : Number(t.default_price_cents);
++ return cents > 0 ? cents : null;
++}
++
async function channelState(streamer, viewer) {
const to = await viewerOrNull(() => subjectFor(streamer.id, { role: 'recipient' }));
if (!to) return { subscribed: false, subscriberCount: 0 };
const me = viewer ? await viewerOrNull(() => subjectFor(viewer.id)) : null;
-- const [ent, list] = await Promise.all([
++ const [ent, list, tiers] = await Promise.all([
me ? await entitlementOf(me, to) : Promise.resolve(null),
api.listSubscriptions({ streamer: to, status: 'active' }),
++ api.tiers(to).catch(() => null),
]);
const now = Date.now();
const count = (list.subscriptions || []).filter((s) => !s.current_period_end || Date.parse(s.current_period_end) > now).length;
-- return { subscribed: !!(ent && ent.active), subscriberCount: count };
++ return { subscribed: !!(ent && ent.active), subscriberCount: count, priceCents: tierOnePrice(tiers) };
}
async function cancelSubscription(req, { id, streamerId }) {
ddiff --git a/server/monetization/billing-client.js b/server/monetization/billing-client.js
iindex ba49380f..354c755a 100644
---- a/server/monetization/billing-client.js
++++ b/server/monetization/billing-client.js
@@@ -191,6 +191,8 @@ const api = {
/** billing.entitlement.check */
listSubscriptions: async (q, o) => await request('GET', `/subscriptions?${new URLSearchParams(Object.entries(q).filter(([, v]) => v != null))}`, o),
entitlement: async (subject, streamer, o) => await request('GET', `/entitlements/${enc(subject)}?streamer=${enc(streamer)}`, o),
++ // A creator's tier prices (contracts 0.201.0): Live sells tier 1, at the creator's price when they set one on VIP.
++ tiers: async (streamer, o) => await request('GET', `/tiers/${enc(streamer)}`, o),
/** billing.balance.read */
balance: async (subject, o) => await request('GET', `/balances/${enc(subject)}`, o),
transactions: async (subject, { limit = 50, cursor } = {}, o) => await request('GET', `/transactions?${new URLSearchParams({ subject, limit: String(limit), ...(cursor ? { cursor } : {}) })}`, o),
ddiff --git a/server/monetization/payments-routes.js b/server/monetization/payments-routes.js
iindex 82267a53..cc4994ac 100644
---- a/server/monetization/payments-routes.js
++++ b/server/monetization/payments-routes.js
@@@ -243,15 +243,16 @@ router.get('/subscriptions/mine', requireAuth, async (req, res) => {
router.get('/channel/:username', optionalAuth, async (req, res) => {
const streamer = await db.getUserByUsername(req.params.username);
if (!streamer) return res.status(404).json({ error: 'Not found' });
-- let subscribed = false, subscriberCount = null, billingUnavailable = false;
++ let subscribed = false, subscriberCount = null, billingUnavailable = false, billingPriceCents = null;
if (money.onBilling()) {
-- try { ({ subscribed, subscriberCount } = await billingActions.channelState(streamer, req.user || null)); }
++ try { ({ subscribed, subscriberCount, priceCents: billingPriceCents } = await billingActions.channelState(streamer, req.user || null)); }
catch { billingUnavailable = true; }
} else {
subscribed = req.user ? await db.isActiveSubscriber(req.user.id, streamer.id) : false;
subscriberCount = await db.getActiveSubscriberCount(streamer.id);
}
-- const priceUsd = await pay._num('sub_price_usd', 4.99);
++ // Under Billing the price is the creator's tier 1 (VIP tiers), which Billing charges; else the site setting.
++ const priceUsd = billingPriceCents ? billingPriceCents / 100 : await pay._num('sub_price_usd', 4.99);
let powerchat = { direct: false, site: false };
try { powerchat = await require('../integrations/powerchat-checkout').subscribeRoutes(streamer.id); } catch { /* */ }
const feePct = await pay._num('sub_site_route_fee_pct', 10);
ddiff --git a/test/billing-authority.test.js b/test/billing-authority.test.js
iindex aba05101..e86030a9 100644
---- a/test/billing-authority.test.js
++++ b/test/billing-authority.test.js
@@@ -233,6 +233,12 @@ async function check(name, fn) {
const ch = await call('GET', '/api/payments/channel/bob', 2);
assert.strictEqual(ch.status, 200, ch.text);
assert.strictEqual(ch.json.subscribed, true); assert.strictEqual(ch.json.subscriberCount, 1);
++ assert.strictEqual(ch.json.priceUsd, 4.99, 'Billing\'s default tier-1 price');
++ billing.state.tierOne = 300;
++ const priced = await call('GET', '/api/payments/channel/bob', 2);
++ assert.strictEqual(priced.json.priceUsd, 3, 'the creator\'s own tier-1 price (set on VIP), which Billing charges');
++ assert.strictEqual(priced.json.powerchat.siteTotalUsd, 3.3);
++ billing.state.tierOne = 0;
const mine = await call('GET', '/api/payments/subscriptions/mine', 2);
assert.strictEqual(mine.json.subscriptions[0].streamer_username, 'bob');
const sub = billing.subs[0];
ddiff --git a/test/billing-stub.js b/test/billing-stub.js
iindex 29163f2f..eb1f5368 100644
---- a/test/billing-stub.js
++++ b/test/billing-stub.js
@@@ -49,7 +49,7 @@ async function startNetwork() {
}
const CAP = [
-- ['POST', /^\/intents$/, 'billing.intent.create'], ['GET', /^\/intents\/[^/]+$/, 'billing.intent.create'], ['POST', /^\/intents\/[^/]+\/capture$/, 'billing.intent.create'],
++ ['POST', /^\/intents$/, 'billing.intent.create'], ['GET', /^\/intents\/[^/]+$/, 'billing.intent.create'], ['GET', /^\/tiers\/[^/]+$/, 'billing.intent.create'], ['POST', /^\/intents\/[^/]+\/capture$/, 'billing.intent.create'],
['POST', /^\/transfers$/, 'billing.transfer.create'], ['POST', /^\/transfers\/[^/]+\/refund$/, 'billing.transfer.create'],
['POST', /^\/recycle$/, 'billing.cashout.request'], ['POST', /^\/cashouts$/, 'billing.cashout.request'],
['POST', /^\/subscriptions$/, 'billing.subscription.manage'], ['POST', /^\/subscriptions\/[^/]+\/cancel$/, 'billing.subscription.manage'],
@@@ -148,6 +148,10 @@ async function startBilling({ network }) {
const s = subs.find((x) => x.subscriber.id === m[1] && x.streamer.id === q.get('streamer') && x.status === 'active' && Date.parse(x.current_period_end) > Date.now());
return send(res, 200, { subject: { type: 'user', id: m[1] }, streamer: { type: 'user', id: q.get('streamer') }, active: !!s, expires_at: s ? s.current_period_end : null });
}
++ if (method === 'GET' && (m = path.match(/^\/tiers\/([^/]+)$/))) {
++ // A creator's tier prices (contracts 0.201.0): state.tierOne sets every creator's tier-1 price.
++ return send(res, 200, { streamer: { type: 'user', id: m[1] }, tiers: state.tierOne ? [{ tier: 1, price_cents: state.tierOne, status: 'active' }] : [], default_price_cents: 499, min_price_cents: 100, max_price_cents: 50000, max_tiers: 10 });
++ }
if (method === 'GET' && (m = path.match(/^\/balances\/([^/]+)$/))) {
return send(res, 200, { subject: { type: 'user', id: m[1] }, currency: 'vibes-bits', credit: bal(credit, m[1]), payable: bal(payable, m[1]), pending_payouts: 0, payable_value_cents: bal(payable, m[1]), bits_per_usd: 100 });
}