From 471121c56810878b8d03d199fc6f05850d48137d Mon Sep 17 00:00:00 2001 From: gioelecerati Date: Fri, 18 Aug 2023 21:08:35 +0200 Subject: [PATCH 1/5] billing: enterprise products --- packages/api/src/config.js | 69 ++++++++++++++ packages/api/src/controllers/stripe.ts | 119 +++++++++++++++++++++++++ packages/api/src/schema/db-schema.yaml | 6 ++ packages/www/public/sitemap-0.xml | 41 +++++---- 4 files changed, 218 insertions(+), 17 deletions(-) diff --git a/packages/api/src/config.js b/packages/api/src/config.js index 4d8089b742..6586f50503 100644 --- a/packages/api/src/config.js +++ b/packages/api/src/config.js @@ -298,4 +298,73 @@ export const products = { ], monthlyPrice: 0, }, + prod_OTTwpzjA4U8B2P: { + order: 12, + name: "Enterprise", + lookupKeys: ["enterprise_plan_1"], + usage: [ + { + name: "Transcoding", + description: "Transcoding (minutes)", + price: 0, + }, + { + name: "Delivery", + description: "Delivery (minutes)", + price: 0, + }, + { + name: "Storage", + description: "Storage (minutes)", + price: 0, + }, + ], + monthlyPrice: 0, + }, + prod_OTTwpzjA4U8B2P: { + order: 12, + name: "Enterprise", + lookupKeys: ["enterprise_plan_1"], + usage: [ + { + name: "Transcoding", + description: "Transcoding (minutes)", + price: 0, + }, + { + name: "Delivery", + description: "Delivery (minutes)", + price: 0, + }, + { + name: "Storage", + description: "Storage (minutes)", + price: 0, + }, + ], + monthlyPrice: 0, + }, + prod_OTTbwpzxNLMNSh: { + order: 12, + name: "Enterprise", + lookupKeys: ["enterprise_plan_1"], + usage: [ + { + name: "Transcoding", + description: "Transcoding (minutes)", + price: 0, + }, + { + name: "Delivery", + description: "Delivery (minutes)", + price: 0, + }, + { + name: "Storage", + description: "Storage (minutes)", + price: 0, + }, + ], + monthlyPrice: 0, + }, }; diff --git a/packages/api/src/controllers/stripe.ts b/packages/api/src/controllers/stripe.ts index da06f229b9..0edf3cf497 100644 --- a/packages/api/src/controllers/stripe.ts +++ b/packages/api/src/controllers/stripe.ts @@ -8,6 +8,7 @@ import qs from "qs"; import { sendgridEmailPaymentFailed } from "./helpers"; import { WithID } from "../store/types"; import { User } from "../schema/types"; +import { authorizer } from "../middleware"; const app = Router(); @@ -699,4 +700,122 @@ app.post("/migrate-pro-user", async (req, res) => { }); }); +app.post( + "/subscribe-to-enterprise", + authorizer({ anyAdmin: true }), + async (req, res) => { + let enterprisePlan = "prod_OTTbwpzxNLMNSh"; + + if (req.body.staging === true) { + enterprisePlan = "prod_OTTwpzjA4U8B2P"; + } + + const userId = req.body.userId; + + const [users] = await db.user.find([sql`users.id = ${userId}`], { + limit: 1, + useReplica: false, + }); + + if (users.length == 0) { + res.json({ + errors: ["no users found"], + }); + return; + } + + let user = users[0]; + + const { data } = await req.stripe.customers.list({ + email: user.email, + }); + + if (data.length > 0) { + const items = await req.stripe.prices.list({ + lookup_keys: products[enterprisePlan].lookupKeys, + }); + + let subscription; + + try { + subscription = await req.stripe.subscriptions.retrieve( + user.stripeCustomerSubscriptionId + ); + } catch (e) { + console.log(` + error- subscription not found for user=${user.id} email=${user.email} subscriptionId=${user.stripeCustomerSubscriptionId} + `); + await db.user.update(user.id, { + isActiveSubscription: false, + }); + res.json({ + errors: [ + `error - subscription not found for user=${user.id} email=${user.email} subscriptionId=${user.stripeCustomerSubscriptionId}`, + ], + }); + return; + } + + const subscriptionItems = await req.stripe.subscriptionItems.list({ + subscription: user.stripeCustomerSubscriptionId, + }); + + if (!req.body.actually_migrate) { + res.json({ + migrating_user: { + email: user.email, + stripe_customer_id: user.stripeCustomerId, + id: user.id, + }, + from_product: user.stripeProductId, + pay_as_you_go_items_to_apply: [], + subscription_items_to_apply: items, + }); + return; + } + + subscription = await req.stripe.subscriptions.update( + user.stripeCustomerSubscriptionId, + { + proration_behavior: "none", + cancel_at_period_end: false, + items: [ + ...subscriptionItems.data.map((item) => { + const isMetered = item.price.recurring.usage_type === "metered"; + return { + id: item.id, + deleted: true, + clear_usage: isMetered ? true : undefined, + price: item.price.id, + }; + }), + ...items.data.map((item) => ({ + price: item.id, + })), + ], + } + ); + + await db.user.update(user.id, { + stripeCustomerId: user.stripeCustomerId, + stripeProductId: + req.body.staging === true + ? "prod_OTTwpzjA4U8B2P" + : "prod_OTTbwpzxNLMNSh", + stripeCustomerSubscriptionId: subscription.id, + }); + } else { + res.json({ + errors: [ + `Unable to migrate user - customer not found for user=${user.id} email=${user.email} subscriptionId=${user.stripeCustomerSubscriptionId}`, + ], + }); + return; + } + res.json({ + result: "Migrated user with email " + user.email + " to enterprise plan", + }); + } +); + export default app; diff --git a/packages/api/src/schema/db-schema.yaml b/packages/api/src/schema/db-schema.yaml index 6dabf18eb1..4aa86f5c5f 100644 --- a/packages/api/src/schema/db-schema.yaml +++ b/packages/api/src/schema/db-schema.yaml @@ -229,6 +229,8 @@ components: - prod_O9XtHhI6rbTT1B - prod_O9XtcfOSMjSD5L - prod_O9XuWMU1Up6QKf + - prod_OTTbwpzxNLMNSh + - prod_OTTwpzjA4U8B2P update-subscription: type: object required: @@ -264,6 +266,8 @@ components: - prod_O9XtHhI6rbTT1B - prod_O9XtcfOSMjSD5L - prod_O9XuWMU1Up6QKf + - prod_OTTbwpzxNLMNSh + - prod_OTTwpzjA4U8B2P update-customer-payment-method: type: object required: @@ -995,6 +999,8 @@ components: - prod_O9XtHhI6rbTT1B - prod_O9XtcfOSMjSD5L - prod_O9XuWMU1Up6QKf + - prod_OTTbwpzxNLMNSh + - prod_OTTwpzjA4U8B2P toMigrate: type: boolean default: true diff --git a/packages/www/public/sitemap-0.xml b/packages/www/public/sitemap-0.xml index 91c9097e16..3b378f15c2 100644 --- a/packages/www/public/sitemap-0.xml +++ b/packages/www/public/sitemap-0.xml @@ -1,20 +1,27 @@ -https://livepeer.studiodaily0.72023-07-07T16:37:14.836Z -https://livepeer.studio/contactdaily0.72023-07-07T16:37:14.836Z -https://livepeer.studio/dashboarddaily0.72023-07-07T16:37:14.836Z -https://livepeer.studio/dashboard/assetsdaily0.72023-07-07T16:37:14.836Z -https://livepeer.studio/dashboard/billingdaily0.72023-07-07T16:37:14.836Z -https://livepeer.studio/dashboard/billing/plansdaily0.72023-07-07T16:37:14.836Z -https://livepeer.studio/dashboard/developers/api-keysdaily0.72023-07-07T16:37:14.836Z -https://livepeer.studio/dashboard/developers/signing-keysdaily0.72023-07-07T16:37:14.836Z -https://livepeer.studio/dashboard/developers/webhooksdaily0.72023-07-07T16:37:14.836Z -https://livepeer.studio/dashboard/sessionsdaily0.72023-07-07T16:37:14.836Z -https://livepeer.studio/dashboard/stream-healthdaily0.72023-07-07T16:37:14.836Z -https://livepeer.studio/dashboard/streamsdaily0.72023-07-07T16:37:14.836Z -https://livepeer.studio/dashboard/usagedaily0.72023-07-07T16:37:14.836Z -https://livepeer.studio/forgot-passworddaily0.72023-07-07T16:37:14.836Z -https://livepeer.studio/registerdaily0.72023-07-07T16:37:14.836Z -https://livepeer.studio/reset-passworddaily0.72023-07-07T16:37:14.836Z -https://livepeer.studio/verifydaily0.72023-07-07T16:37:14.836Z +https://livepeer.studiodaily0.72023-08-18T18:12:16.653Z +https://livepeer.studio/contactdaily0.72023-08-18T18:12:16.653Z +https://livepeer.studio/dashboarddaily0.72023-08-18T18:12:16.653Z +https://livepeer.studio/dashboard/assetsdaily0.72023-08-18T18:12:16.653Z +https://livepeer.studio/dashboard/billingdaily0.72023-08-18T18:12:16.653Z +https://livepeer.studio/dashboard/billing/plansdaily0.72023-08-18T18:12:16.653Z +https://livepeer.studio/dashboard/developers/api-keysdaily0.72023-08-18T18:12:16.653Z +https://livepeer.studio/dashboard/developers/signing-keysdaily0.72023-08-18T18:12:16.653Z +https://livepeer.studio/dashboard/developers/webhooksdaily0.72023-08-18T18:12:16.653Z +https://livepeer.studio/dashboard/sessionsdaily0.72023-08-18T18:12:16.653Z +https://livepeer.studio/dashboard/stream-healthdaily0.72023-08-18T18:12:16.653Z +https://livepeer.studio/dashboard/streamsdaily0.72023-08-18T18:12:16.653Z +https://livepeer.studio/dashboard/usagedaily0.72023-08-18T18:12:16.653Z +https://livepeer.studio/forgot-passworddaily0.72023-08-18T18:12:16.653Z +https://livepeer.studio/registerdaily0.72023-08-18T18:12:16.653Z +https://livepeer.studio/reset-passworddaily0.72023-08-18T18:12:16.653Z +https://livepeer.studio/verifydaily0.72023-08-18T18:12:16.653Z +https://livepeer.studio/privacy-policydaily0.72023-08-18T18:12:16.653Z +https://livepeer.studio/decentralized-storage-arweavedaily0.72023-08-18T18:12:16.653Z +https://livepeer.studio/terms-of-servicedaily0.72023-08-18T18:12:16.653Z +https://livepeer.studio/pricing-faqdaily0.72023-08-18T18:12:16.653Z +https://livepeer.studio/decentralized-storage-ipfsdaily0.72023-08-18T18:12:16.653Z +https://livepeer.studio/dstoragedaily0.72023-08-18T18:12:16.653Z +https://livepeer.studio/bundlrdaily0.72023-08-18T18:12:16.653Z \ No newline at end of file From d9b086a55b253f1b4ebca790f330e3980b203b1c Mon Sep 17 00:00:00 2001 From: gioelecerati Date: Fri, 18 Aug 2023 21:11:02 +0200 Subject: [PATCH 2/5] fix: remove duplicated config --- packages/api/src/config.js | 25 +------------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/packages/api/src/config.js b/packages/api/src/config.js index 6586f50503..9e24879067 100644 --- a/packages/api/src/config.js +++ b/packages/api/src/config.js @@ -321,31 +321,8 @@ export const products = { ], monthlyPrice: 0, }, - prod_OTTwpzjA4U8B2P: { - order: 12, - name: "Enterprise", - lookupKeys: ["enterprise_plan_1"], - usage: [ - { - name: "Transcoding", - description: "Transcoding (minutes)", - price: 0, - }, - { - name: "Delivery", - description: "Delivery (minutes)", - price: 0, - }, - { - name: "Storage", - description: "Storage (minutes)", - price: 0, - }, - ], - monthlyPrice: 0, - }, prod_OTTbwpzxNLMNSh: { - order: 12, + order: 13, name: "Enterprise", lookupKeys: ["enterprise_plan_1"], usage: [ From 819f23ea6c183485ea718599918de7b9d696915e Mon Sep 17 00:00:00 2001 From: gioelecerati Date: Fri, 18 Aug 2023 22:55:56 +0200 Subject: [PATCH 3/5] billing: change text for custom plans --- packages/www/pages/dashboard/billing/index.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/www/pages/dashboard/billing/index.tsx b/packages/www/pages/dashboard/billing/index.tsx index f47ca460dc..58a5eac207 100644 --- a/packages/www/pages/dashboard/billing/index.tsx +++ b/packages/www/pages/dashboard/billing/index.tsx @@ -51,6 +51,8 @@ const Billing = () => { null ); + const standardProducts = ["Hacker", "Growth", "Scale"]; + const fetcher = useCallback(async () => { if (user?.stripeCustomerPaymentMethodId) { const [_res, paymentMethod] = await getPaymentMethod( @@ -375,10 +377,11 @@ const Billing = () => { - {!products[user.stripeProductId]?.order ? ( + {!standardProducts.includes(products[user.stripeProductId]?.name) ? ( - The Hacker plan is free of charge up to 1000 minutes per month and - limited to 10 concurrent viewers per account. + You are subscribed to a custom plan, and you will be invoiced + according to the terms of that agreement. Please reach out to + contact@livepeer.org with any questions. ) : ( subscription && ( From e8611483ac9f4ecd78b62d3148fa8122255a1205 Mon Sep 17 00:00:00 2001 From: gioelecerati Date: Fri, 18 Aug 2023 23:05:07 +0200 Subject: [PATCH 4/5] billing: fix typo --- packages/api/src/config.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/api/src/config.js b/packages/api/src/config.js index 9e24879067..9ad90c8e0e 100644 --- a/packages/api/src/config.js +++ b/packages/api/src/config.js @@ -210,13 +210,13 @@ export const products = { name: "Delivery", description: "Delivery (minutes)", price: 0.0005, - limit: 1_000, + limit: 10_000, }, { name: "Storage", description: "Storage (minutes)", price: 0.0035, - limit: 10_000, + limit: 1_000, }, ], monthlyPrice: 0, From 5ec0d7803b47b6e589926b9f628cd0688c889777 Mon Sep 17 00:00:00 2001 From: gioelecerati Date: Mon, 21 Aug 2023 20:33:49 +0200 Subject: [PATCH 5/5] address comment --- packages/api/src/config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/api/src/config.js b/packages/api/src/config.js index 9ad90c8e0e..dc8d1df3df 100644 --- a/packages/api/src/config.js +++ b/packages/api/src/config.js @@ -300,7 +300,7 @@ export const products = { }, prod_OTTwpzjA4U8B2P: { order: 12, - name: "Enterprise", + name: "Enterprise (Staging)", lookupKeys: ["enterprise_plan_1"], usage: [ {