openvibers/openvibe.billing Commits

Publish dated public offers for Compare (#263)

OP OpenVibers committed 2 days ago · d0f211dbd3 · 4 files, +36 −0

Changes

ddiff --git a/deploy/nginx/billing.openvibe.network.conf b/deploy/nginx/billing.openvibe.network.conf
iindex f6ac33f..82cab6b 100644
---- a/deploy/nginx/billing.openvibe.network.conf
++++ b/deploy/nginx/billing.openvibe.network.conf
@@@ -59,6 +59,13 @@ server {
proxy_pass http://127.0.0.1:4600;
}
++ # Dated public offers for Compare; all other service API routes retain their restrictions.
++ location = /api/v1/plans {
++ limit_except GET { deny all; }
++ limit_req zone=ovbilling_console burst=20 nodelay;
++ proxy_pass http://127.0.0.1:4600;
++ }
++
location = /api/health { proxy_pass http://127.0.0.1:4600; }
# A signed-in person's own Vibes, read by the shared navbar on every OpenVibe site (server/api/me.js: a user token,
# CORS for OpenVibe's own origins).
ddiff --git a/server/api/v1.js b/server/api/v1.js
iindex e0846b8..ab29e48 100644
---- a/server/api/v1.js
++++ b/server/api/v1.js
@@@ -70,6 +70,12 @@ function v1Router({ ctx, auth, adapters, limits, subjectAssertion }) {
const txnOut = (t, extra = {}) => ({ transaction: presentTxn(t), ...extra });
const pathSubject = (v) => userSubject(String(v), 'subject');
++ // Public offers only: no customer, creator or ledger records.
++ r.get('/plans', (req, res) => {
++ res.set('Cache-Control', 'public, max-age=600');
++ res.json(require('../public-plans').publicPlans(ctx.rates, ctx.now()));
++ });
++
// ── Rates (public: prices the UI shows) ──────────────────
r.get('/rates', (req, res) => {
const rt = ctx.rates;
ddiff --git a/server/public-plans.js b/server/public-plans.js
nnew file mode 100644
iindex 0000000..0e150a3
---- /dev/null
++++ b/server/public-plans.js
@@@ -0,0 +1,13 @@
++'use strict';
++/** Public offers are derived from the same rate functions checkout uses; never a second price list. */
++function publicPlans(rates, now = Date.now()) {
++ const as_of = new Date(now).toISOString();
++ return { as_of, date_kind: 'observed', plans: [
++ ...[100,500,1000,2500,5000,10000,25000].filter(v=>v>=rates.minPurchaseBits && v<=rates.maxBits).map(vibes=>({
++ id:`vibes-${vibes}`, name:`${vibes.toLocaleString('en-US')} Vibes`, kind:'package', vibes,
++ price_cents:rates.priceCents(vibes), currency:'USD', as_of, url:'https://openvibe.network/vibes',
++ })),
++ {id:'channel-membership-default',name:'Default channel membership',kind:'membership',price_cents:rates.subPriceCents,period_days:rates.subPeriodDays,currency:'USD',as_of,url:'https://openvibe.vip',note:'Default tier only. Creators may set their own prices; this is not a network-wide subscription.'},
++ ]};
++}
++module.exports={publicPlans};
ddiff --git a/test/public-plans.test.js b/test/public-plans.test.js
nnew file mode 100644
iindex 0000000..169cee0
---- /dev/null
++++ b/test/public-plans.test.js
@@@ -0,0 +1,10 @@
++'use strict';
++const assert=require('node:assert/strict');
++const {publicPlans}=require('../server/public-plans');
++const rates={minPurchaseBits:500,maxBits:5000,priceCents:n=>n*2,subPriceCents:499,subPeriodDays:30};
++const out=publicPlans(rates,Date.UTC(2026,10,11));
++assert.deepEqual(out.plans.filter(p=>p.kind==='package').map(p=>p.vibes),[500,1000,2500,5000]);
++for(const p of out.plans){assert.equal(p.as_of,'2026-11-11T00:00:00.000Z');assert.equal(p.currency,'USD');if(p.kind==='package')assert.equal(p.price_cents,p.vibes*2);}
++assert.equal(out.plans.at(-1).price_cents,499);
++assert.equal(out.plans.at(-1).period_days,30);
++console.log('public plans: checkout prices, limits, periods and provenance pass');