openvibers/openvibe.billing Commits
Publish dated public offers for Compare (#263)
Changes
@@@ -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).@@@ -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;nnew file mode 100644@@@ -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};nnew file mode 100644@@@ -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');
@@ -59,6 +59,13 @@ server {
@@ -59,6 +59,13 @@ server {
proxy_pass http://127.0.0.1:4600;
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; }
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,
# 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).
# CORS for OpenVibe's own origins).
@@ -70,6 +70,12 @@ function v1Router({ ctx, auth, adapters, limits, subjectAssertion }) {
@@ -70,6 +70,12 @@ function v1Router({ ctx, auth, adapters, limits, subjectAssertion }) {
const txnOut = (t, extra = {}) => ({ transaction: presentTxn(t), ...extra });
const txnOut = (t, extra = {}) => ({ transaction: presentTxn(t), ...extra });
const pathSubject = (v) => userSubject(String(v), 'subject');
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) ──────────────────
// ── Rates (public: prices the UI shows) ──────────────────
r.get('/rates', (req, res) => {
r.get('/rates', (req, res) => {
const rt = ctx.rates;
const rt = ctx.rates;
new file mode 100644
new file mode 100644
@@ -0,0 +1,13 @@
@@ -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};
new file mode 100644
new file mode 100644
@@ -0,0 +1,10 @@
@@ -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');