openvibers/openvibe.vip Commits
Checkout stays closed while VIP_CHECKOUT_PROVIDERS is empty (the setting until Billing's cutover): plan pages say so, the API refuses; README: before and after the Billing cutover
Changes
@@@ -28,8 +28,9 @@ VIP_STAFF_ROLES=adminBILLING_URL=http://127.0.0.1:4600# BILLING_AUDIENCE=openvibe.billing# VIP_BILLING_TIMEOUT_MS=5000--# Providers offered on plan pages (must be enabled on Billing); `credit` pays from Vibes credit--VIP_CHECKOUT_PROVIDERS=powerchat,credit++# Providers offered on plan pages (must be enabled on Billing); `credit` pays from Vibes credit.++# Leave EMPTY until Billing is authoritative (Wave 8 cutover): checkout is then closed.++VIP_CHECKOUT_PROVIDERS=# Turns Billing's PowerChat checkout_ref into a link: {ref} and {cents}VIP_POWERCHAT_LINK_TEMPLATE=# VIP_BILLING_RATES_TTL_MS=300000@@@ -232,6 +232,21 @@ Idempotent (keys in `vip_migration_maps`), never overwrites a creator's later edon a later run; `--dry-run` rolls everything back; exit 1 when Billing could not be read. Live's`sub_price_usd`/share/fee are reported for reference only — Billing prices.++## Before and after the Billing cutover++++Billing runs in shadow mode until the Wave 8 cutover (Live stays authoritative for money,++`BILLING_AUTHORITY=live`). Until then:++++- keep **checkout closed** (`VIP_CHECKOUT_PROVIDERS=` empty): a subscription sold through Billing++ now would be one Live does not know about. Plan pages say joining is not open yet; the API answers++ `422 vip.checkout.provider_unavailable`;++- VIP can run read-only against Billing's shadow entitlements (imported from Live), which are only as++ current as Billing's last import; new Live subscriptions do not reach Billing (or VIP) before the cutover;++- Billing publishes its events only when its own `EVENTS_URL` is set.++++After the cutover: run `scripts/import-live.js` against the final Live snapshot (after Billing's++final import), `scripts/subscribe.js`, then open checkout (`VIP_CHECKOUT_PROVIDERS=powerchat,credit`).++## Security notes- Every API route checks one capability (services) or ownership (users); a presented token is judged@@@ -58,7 +58,8 @@ function loadConfig(env = process.env) {timeoutMs: int(env.VIP_BILLING_TIMEOUT_MS, 5000),// Checkout providers offered on the plan pages (each must be enabled on Billing too).// `credit` pays from the member's Vibes credit through Billing's subscription API.-- providers: list(env.VIP_CHECKOUT_PROVIDERS || 'powerchat,credit'),++ // Empty (set but blank) closes checkout — the setting until Billing is authoritative.++ providers: list(env.VIP_CHECKOUT_PROVIDERS != null ? env.VIP_CHECKOUT_PROVIDERS : 'powerchat,credit'),// Billing answers a PowerChat intent with a checkout_ref (pcsub:…) instead of a URL. When// set, this template turns it into a link: {ref} and {cents} are filled in.powerchatLinkTemplate: env.VIP_POWERCHAT_LINK_TEMPLATE || '',@@@ -71,6 +71,7 @@ ${member.membership && member.membership.plan_version ? `<p class="muted">You jo? '<p class="muted">This plan is not sold through Billing yet.</p>': isOwner ? '<p class="muted">This is your plan.</p>': member && member.status === 'active' ? ''++ : !providers.length ? '<p class="muted">Joining through OpenVibe.Billing is not open yet.</p>': viewer? `<form method="post" action="/${encodeURIComponent(creator.username)}/join" class="join">${csrf(token)}<input type="hidden" name="plan_id" value="${esc(p.id)}"><label>Pay with <select name="provider">${providers.map((x) => `<option value="${esc(x)}">${esc(x === 'credit' ? 'your Vibes credit' : x === 'powerchat' ? 'PowerChat' : x)}</option>`).join('')}</select></label>@@@ -227,5 +227,19 @@ const { test, run } = harness('api');assert.strictEqual(bad.status, 401);});++ test('checkout closed (VIP_CHECKOUT_PROVIDERS empty, before the Billing cutover): pages say so, the API refuses', async () => {++ const t2 = await boot({ env: { VIP_CHECKOUT_PROVIDERS: '' } });++ try {++ const c = t2.network.newUser('uma', { role: 'streamer' });++ const p = (await t2.call('POST', '/api/v1/plans', { user: c, body: { name: 'Club', publish: true } })).json.plan;++ const page = await t2.page('/uma', { user: t2.network.newUser('vic') });++ assert.match(page.text, /Joining through OpenVibe\.Billing is not open yet/);++ assert.doesNotMatch(page.text, /<select name="provider">/);++ const r = await t2.call('POST', '/api/v1/checkout', { user: t2.network.newUser('wes'), body: { plan_id: p.id, provider: 'powerchat' } });++ assert.strictEqual(r.status, 422);++ assert.strictEqual(r.json.code, 'vip.checkout.provider_unavailable');++ } finally { await t2.close(); }++ });++await run().finally(() => t.close());})();
@@ -28,8 +28,9 @@ VIP_STAFF_ROLES=admin
@@ -28,8 +28,9 @@ VIP_STAFF_ROLES=admin
BILLING_URL=http://127.0.0.1:4600
BILLING_URL=http://127.0.0.1:4600
# BILLING_AUDIENCE=openvibe.billing
# BILLING_AUDIENCE=openvibe.billing
# VIP_BILLING_TIMEOUT_MS=5000
# VIP_BILLING_TIMEOUT_MS=5000
-# Providers offered on plan pages (must be enabled on Billing); `credit` pays from Vibes credit
-VIP_CHECKOUT_PROVIDERS=powerchat,credit
+# Providers offered on plan pages (must be enabled on Billing); `credit` pays from Vibes credit.
+# Leave EMPTY until Billing is authoritative (Wave 8 cutover): checkout is then closed.
+VIP_CHECKOUT_PROVIDERS=
# Turns Billing's PowerChat checkout_ref into a link: {ref} and {cents}
# Turns Billing's PowerChat checkout_ref into a link: {ref} and {cents}
VIP_POWERCHAT_LINK_TEMPLATE=
VIP_POWERCHAT_LINK_TEMPLATE=
# VIP_BILLING_RATES_TTL_MS=300000
# VIP_BILLING_RATES_TTL_MS=300000
@@ -232,6 +232,21 @@ Idempotent (keys in `vip_migration_maps`), never overwrites a creator's later ed
@@ -232,6 +232,21 @@ Idempotent (keys in `vip_migration_maps`), never overwrites a creator's later ed
on a later run; `--dry-run` rolls everything back; exit 1 when Billing could not be read. Live's
on a later run; `--dry-run` rolls everything back; exit 1 when Billing could not be read. Live's
`sub_price_usd`/share/fee are reported for reference only — Billing prices.
`sub_price_usd`/share/fee are reported for reference only — Billing prices.
+## Before and after the Billing cutover
+
+Billing runs in shadow mode until the Wave 8 cutover (Live stays authoritative for money,
+`BILLING_AUTHORITY=live`). Until then:
+
+- keep **checkout closed** (`VIP_CHECKOUT_PROVIDERS=` empty): a subscription sold through Billing
+ now would be one Live does not know about. Plan pages say joining is not open yet; the API answers
+ `422 vip.checkout.provider_unavailable`;
+- VIP can run read-only against Billing's shadow entitlements (imported from Live), which are only as
+ current as Billing's last import; new Live subscriptions do not reach Billing (or VIP) before the cutover;
+- Billing publishes its events only when its own `EVENTS_URL` is set.
+
+After the cutover: run `scripts/import-live.js` against the final Live snapshot (after Billing's
+final import), `scripts/subscribe.js`, then open checkout (`VIP_CHECKOUT_PROVIDERS=powerchat,credit`).
+
## Security notes
## Security notes
- Every API route checks one capability (services) or ownership (users); a presented token is judged
- Every API route checks one capability (services) or ownership (users); a presented token is judged
@@ -58,7 +58,8 @@ function loadConfig(env = process.env) {
@@ -58,7 +58,8 @@ function loadConfig(env = process.env) {
timeoutMs: int(env.VIP_BILLING_TIMEOUT_MS, 5000),
timeoutMs: int(env.VIP_BILLING_TIMEOUT_MS, 5000),
// Checkout providers offered on the plan pages (each must be enabled on Billing too).
// Checkout providers offered on the plan pages (each must be enabled on Billing too).
// `credit` pays from the member's Vibes credit through Billing's subscription API.
// `credit` pays from the member's Vibes credit through Billing's subscription API.
- providers: list(env.VIP_CHECKOUT_PROVIDERS || 'powerchat,credit'),
+ // Empty (set but blank) closes checkout — the setting until Billing is authoritative.
+ providers: list(env.VIP_CHECKOUT_PROVIDERS != null ? env.VIP_CHECKOUT_PROVIDERS : 'powerchat,credit'),
// Billing answers a PowerChat intent with a checkout_ref (pcsub:…) instead of a URL. When
// Billing answers a PowerChat intent with a checkout_ref (pcsub:…) instead of a URL. When
// set, this template turns it into a link: {ref} and {cents} are filled in.
// set, this template turns it into a link: {ref} and {cents} are filled in.
powerchatLinkTemplate: env.VIP_POWERCHAT_LINK_TEMPLATE || '',
powerchatLinkTemplate: env.VIP_POWERCHAT_LINK_TEMPLATE || '',
@@ -71,6 +71,7 @@ ${member.membership && member.membership.plan_version ? `<p class="muted">You jo
@@ -71,6 +71,7 @@ ${member.membership && member.membership.plan_version ? `<p class="muted">You jo
? '<p class="muted">This plan is not sold through Billing yet.</p>'
? '<p class="muted">This plan is not sold through Billing yet.</p>'
: isOwner ? '<p class="muted">This is your plan.</p>'
: isOwner ? '<p class="muted">This is your plan.</p>'
: member && member.status === 'active' ? ''
: member && member.status === 'active' ? ''
+ : !providers.length ? '<p class="muted">Joining through OpenVibe.Billing is not open yet.</p>'
: viewer
: viewer
? `<form method="post" action="/${encodeURIComponent(creator.username)}/join" class="join">${csrf(token)}<input type="hidden" name="plan_id" value="${esc(p.id)}">
? `<form method="post" action="/${encodeURIComponent(creator.username)}/join" class="join">${csrf(token)}<input type="hidden" name="plan_id" value="${esc(p.id)}">
<label>Pay with <select name="provider">${providers.map((x) => `<option value="${esc(x)}">${esc(x === 'credit' ? 'your Vibes credit' : x === 'powerchat' ? 'PowerChat' : x)}</option>`).join('')}</select></label>
<label>Pay with <select name="provider">${providers.map((x) => `<option value="${esc(x)}">${esc(x === 'credit' ? 'your Vibes credit' : x === 'powerchat' ? 'PowerChat' : x)}</option>`).join('')}</select></label>
@@ -227,5 +227,19 @@ const { test, run } = harness('api');
@@ -227,5 +227,19 @@ const { test, run } = harness('api');
assert.strictEqual(bad.status, 401);
assert.strictEqual(bad.status, 401);
});
});
+ test('checkout closed (VIP_CHECKOUT_PROVIDERS empty, before the Billing cutover): pages say so, the API refuses', async () => {
+ const t2 = await boot({ env: { VIP_CHECKOUT_PROVIDERS: '' } });
+ try {
+ const c = t2.network.newUser('uma', { role: 'streamer' });
+ const p = (await t2.call('POST', '/api/v1/plans', { user: c, body: { name: 'Club', publish: true } })).json.plan;
+ const page = await t2.page('/uma', { user: t2.network.newUser('vic') });
+ assert.match(page.text, /Joining through OpenVibe\.Billing is not open yet/);
+ assert.doesNotMatch(page.text, /<select name="provider">/);
+ const r = await t2.call('POST', '/api/v1/checkout', { user: t2.network.newUser('wes'), body: { plan_id: p.id, provider: 'powerchat' } });
+ assert.strictEqual(r.status, 422);
+ assert.strictEqual(r.json.code, 'vip.checkout.provider_unavailable');
+ } finally { await t2.close(); }
+ });
+
await run().finally(() => t.close());
await run().finally(() => t.close());
})();
})();