From 4b153e7626fc952a4e5a2d820431c355f9368cdd Mon Sep 17 00:00:00 2001 From: HugoCasa Date: Mon, 19 Feb 2024 21:09:59 +0100 Subject: [PATCH] feat: usage-based billing (#3247) * feat: usage-based billing * fix: missing field * fix: sqlx build --- ...c61296a3ff7489ae12f52a19f9543173ac597.json | 8 +- ...2d3881fa44468ed5330a501daa0d61c84cb71.json | 14 +++ ...2fa84fefbb3df8bb1bc9996c40a86d464b8a1.json | 14 +++ ...dd02a128f571039714e61525e2bb86591d93.json} | 12 +- ...7630a1d7b9deb8d33e9f909061f351f96844e.json | 8 +- ...40219101340_add_automatic_billing.down.sql | 1 + ...0240219101340_add_automatic_billing.up.sql | 3 + backend/windmill-api/openapi.yaml | 38 ++++++- backend/windmill-api/src/workspaces.rs | 1 + .../components/settings/PremiumInfo.svelte | 106 ++++++++++++++---- 10 files changed, 175 insertions(+), 30 deletions(-) create mode 100644 backend/.sqlx/query-1e659916501e668913e591eb7282d3881fa44468ed5330a501daa0d61c84cb71.json create mode 100644 backend/.sqlx/query-2d4d4564108376ab310cb4a50fa2fa84fefbb3df8bb1bc9996c40a86d464b8a1.json rename backend/.sqlx/{query-4999995f4459da32a0e2fc7115eae97fb49b0080075fb01e0863d164ba9a5f13.json => query-49a8fe5c538d8fc26f6d8ece5b30dd02a128f571039714e61525e2bb86591d93.json} (54%) create mode 100644 backend/migrations/20240219101340_add_automatic_billing.down.sql create mode 100644 backend/migrations/20240219101340_add_automatic_billing.up.sql diff --git a/backend/.sqlx/query-1730f39fd1793d45fbb41b21389c61296a3ff7489ae12f52a19f9543173ac597.json b/backend/.sqlx/query-1730f39fd1793d45fbb41b21389c61296a3ff7489ae12f52a19f9543173ac597.json index 24546ff4ec..d5266eabae 100644 --- a/backend/.sqlx/query-1730f39fd1793d45fbb41b21389c61296a3ff7489ae12f52a19f9543173ac597.json +++ b/backend/.sqlx/query-1730f39fd1793d45fbb41b21389c61296a3ff7489ae12f52a19f9543173ac597.json @@ -102,6 +102,11 @@ "ordinal": 19, "name": "auto_add", "type_info": "Bool" + }, + { + "ordinal": 20, + "name": "automatic_billing", + "type_info": "Bool" } ], "parameters": { @@ -129,7 +134,8 @@ true, true, true, - true + true, + false ] }, "hash": "1730f39fd1793d45fbb41b21389c61296a3ff7489ae12f52a19f9543173ac597" diff --git a/backend/.sqlx/query-1e659916501e668913e591eb7282d3881fa44468ed5330a501daa0d61c84cb71.json b/backend/.sqlx/query-1e659916501e668913e591eb7282d3881fa44468ed5330a501daa0d61c84cb71.json new file mode 100644 index 0000000000..2cb0d351da --- /dev/null +++ b/backend/.sqlx/query-1e659916501e668913e591eb7282d3881fa44468ed5330a501daa0d61c84cb71.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE workspace_settings SET automatic_billing = false WHERE workspace_id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [] + }, + "hash": "1e659916501e668913e591eb7282d3881fa44468ed5330a501daa0d61c84cb71" +} diff --git a/backend/.sqlx/query-2d4d4564108376ab310cb4a50fa2fa84fefbb3df8bb1bc9996c40a86d464b8a1.json b/backend/.sqlx/query-2d4d4564108376ab310cb4a50fa2fa84fefbb3df8bb1bc9996c40a86d464b8a1.json new file mode 100644 index 0000000000..8c26257ef8 --- /dev/null +++ b/backend/.sqlx/query-2d4d4564108376ab310cb4a50fa2fa84fefbb3df8bb1bc9996c40a86d464b8a1.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE workspace_settings SET automatic_billing = TRUE WHERE workspace_id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [] + }, + "hash": "2d4d4564108376ab310cb4a50fa2fa84fefbb3df8bb1bc9996c40a86d464b8a1" +} diff --git a/backend/.sqlx/query-4999995f4459da32a0e2fc7115eae97fb49b0080075fb01e0863d164ba9a5f13.json b/backend/.sqlx/query-49a8fe5c538d8fc26f6d8ece5b30dd02a128f571039714e61525e2bb86591d93.json similarity index 54% rename from backend/.sqlx/query-4999995f4459da32a0e2fc7115eae97fb49b0080075fb01e0863d164ba9a5f13.json rename to backend/.sqlx/query-49a8fe5c538d8fc26f6d8ece5b30dd02a128f571039714e61525e2bb86591d93.json index 6829a3d690..b67bf27a83 100644 --- a/backend/.sqlx/query-4999995f4459da32a0e2fc7115eae97fb49b0080075fb01e0863d164ba9a5f13.json +++ b/backend/.sqlx/query-49a8fe5c538d8fc26f6d8ece5b30dd02a128f571039714e61525e2bb86591d93.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT premium, usage.usage as \"usage?\", workspace_settings.customer_id, workspace_settings.plan FROM workspace LEFT JOIN workspace_settings ON workspace_settings.workspace_id = $1 LEFT JOIN usage ON usage.id = $1 AND month_ = EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date) AND usage.is_workspace IS true WHERE workspace.id = $1", + "query": "SELECT premium, usage.usage as \"usage?\", workspace_settings.customer_id, workspace_settings.plan, workspace_settings.automatic_billing FROM workspace LEFT JOIN workspace_settings ON workspace_settings.workspace_id = $1 LEFT JOIN usage ON usage.id = $1 AND month_ = EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date) AND usage.is_workspace IS true WHERE workspace.id = $1", "describe": { "columns": [ { @@ -22,6 +22,11 @@ "ordinal": 3, "name": "plan", "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "automatic_billing", + "type_info": "Bool" } ], "parameters": { @@ -33,8 +38,9 @@ false, false, true, - true + true, + false ] }, - "hash": "4999995f4459da32a0e2fc7115eae97fb49b0080075fb01e0863d164ba9a5f13" + "hash": "49a8fe5c538d8fc26f6d8ece5b30dd02a128f571039714e61525e2bb86591d93" } diff --git a/backend/.sqlx/query-5445083864b2b092b012e894bff7630a1d7b9deb8d33e9f909061f351f96844e.json b/backend/.sqlx/query-5445083864b2b092b012e894bff7630a1d7b9deb8d33e9f909061f351f96844e.json index 047bdb82af..a055459035 100644 --- a/backend/.sqlx/query-5445083864b2b092b012e894bff7630a1d7b9deb8d33e9f909061f351f96844e.json +++ b/backend/.sqlx/query-5445083864b2b092b012e894bff7630a1d7b9deb8d33e9f909061f351f96844e.json @@ -102,6 +102,11 @@ "ordinal": 19, "name": "auto_add", "type_info": "Bool" + }, + { + "ordinal": 20, + "name": "automatic_billing", + "type_info": "Bool" } ], "parameters": { @@ -129,7 +134,8 @@ true, true, true, - true + true, + false ] }, "hash": "5445083864b2b092b012e894bff7630a1d7b9deb8d33e9f909061f351f96844e" diff --git a/backend/migrations/20240219101340_add_automatic_billing.down.sql b/backend/migrations/20240219101340_add_automatic_billing.down.sql new file mode 100644 index 0000000000..d2f607c5b8 --- /dev/null +++ b/backend/migrations/20240219101340_add_automatic_billing.down.sql @@ -0,0 +1 @@ +-- Add down migration script here diff --git a/backend/migrations/20240219101340_add_automatic_billing.up.sql b/backend/migrations/20240219101340_add_automatic_billing.up.sql new file mode 100644 index 0000000000..02de9b5931 --- /dev/null +++ b/backend/migrations/20240219101340_add_automatic_billing.up.sql @@ -0,0 +1,3 @@ +-- Add up migration script here +ALTER TABLE workspace_settings +ADD COLUMN automatic_billing BOOLEAN NOT NULL DEFAULT FALSE; \ No newline at end of file diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 6640129433..7a98108bef 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -1197,6 +1197,8 @@ paths: type: boolean plan: type: string + automatic_billing: + type: boolean customer_id: type: string webhook: @@ -1221,6 +1223,7 @@ paths: type: string required: - code_completion_enabled + - automatic_billing /w/{workspace}/workspaces/get_deploy_to: get: @@ -1282,8 +1285,42 @@ paths: type: number seats: type: number + automatic_billing: + type: boolean required: - premium + - automatic_billing + + /w/{workspace}/workspaces/set_automatic_billing: + post: + summary: set automatic billing + operationId: setAutomaticBilling + tags: + - workspace + parameters: + - $ref: "#/components/parameters/WorkspaceId" + requestBody: + description: automatic billing + required: true + content: + application/json: + schema: + type: object + properties: + automatic_billing: + type: boolean + seats: + type: number + required: + - automatic_billing + + responses: + "200": + description: status + content: + text/plain: + schema: + type: string /w/{workspace}/workspaces/edit_slack_command: post: @@ -5175,7 +5212,6 @@ paths: application/json: schema: {} - /w/{workspace}/jobs_u/completed/get/{id}: get: summary: get completed job diff --git a/backend/windmill-api/src/workspaces.rs b/backend/windmill-api/src/workspaces.rs index bf9429b705..0a7b74a6d9 100644 --- a/backend/windmill-api/src/workspaces.rs +++ b/backend/windmill-api/src/workspaces.rs @@ -158,6 +158,7 @@ pub struct WorkspaceSettings { pub large_file_storage: Option, // effectively: DatasetsStorage pub git_sync: Option, // effectively: WorkspaceGitSyncSettings pub default_app: Option, + pub automatic_billing: bool, } #[derive(FromRow, Serialize, Debug)] diff --git a/frontend/src/lib/components/settings/PremiumInfo.svelte b/frontend/src/lib/components/settings/PremiumInfo.svelte index 191f4b5e59..315ae810ec 100644 --- a/frontend/src/lib/components/settings/PremiumInfo.svelte +++ b/frontend/src/lib/components/settings/PremiumInfo.svelte @@ -1,13 +1,14 @@
@@ -98,10 +120,29 @@
{#if plan}
- Current plan: {capitalize(plan)} plan{plan === 'team' + Current plan: {capitalize(plan)} plan{plan === 'team' && premiumInfo.automatic_billing + ? ' (usage-based)' + : plan === 'team' ? ` (${premiumInfo.seats} seat${premiumInfo.seats > 1 ? 's' : ''})` : ''}
+ {#if plan === 'team'} +
+ + {#if billingModeLoading} + + {/if} +
+ {/if} {:else}
Current plan: Free plan
{/if} @@ -172,13 +213,17 @@ >
premiumInfo.seats + plan === 'team' && + premiumInfo.usedSeats > premiumInfo.seats && + !premiumInfo.automatic_billing ? 'text-red-500' : '' )} >Used seats Highest between seats from developers + operators and seats from computations - {plan === 'team' && premiumInfo.usedSeats > premiumInfo.seats + {plan === 'team' && + premiumInfo.usedSeats > premiumInfo.seats && + !premiumInfo.automatic_billing ? ' > Paid seats' : ''}
premiumInfo.seats + plan === 'team' && + premiumInfo.usedSeats > premiumInfo.seats && + !premiumInfo.automatic_billing ? 'text-red-500' : '' )} > - max(u, c) = max({premiumInfo.seatsFromUsers}, {premiumInfo.seatsFromComps}) = {premiumInfo.usedSeats}{premiumInfo.usedSeats > - premiumInfo.seats + max(u, c) = max({premiumInfo.seatsFromUsers}, {premiumInfo.seatsFromComps}) = {premiumInfo.usedSeats}{plan === + 'team' && + premiumInfo.usedSeats > premiumInfo.seats && + !premiumInfo.automatic_billing ? ` > ${premiumInfo.seats}` : ''}
@@ -202,7 +251,7 @@ - {#if plan === 'team' && premiumInfo.usedSeats > premiumInfo.seats} + {#if plan === 'team' && premiumInfo.usedSeats > premiumInfo.seats && !premiumInfo.automatic_billing}

You have exceeded your allowed number of seats, please update your plan in the customer portal. @@ -220,16 +269,22 @@

All plans
- {#each Object.entries(plans) as [planTitle, planDesc]} -
-

- {planTitle} -

-
    - {#each planDesc as item} -
  • {@html item}
  • - {/each} -
+ {#each Object.entries(plans) as [planTitle, planDesc]} +
+

+ {planTitle} +

+
    + {#each planDesc as item} +
  • {@html item}
  • + {/each} +
{#if planTitle == 'Team'} @@ -257,7 +312,12 @@ {:else if planTitle == 'Enterprise'} {#if plan != 'enterprise'}
-
@@ -270,16 +330,14 @@ Cancel your plan in the customer portal to downgrade to the free plan
{:else} -
- Workspace is on the free plan -
+
Workspace is on the free plan
{/if} {/if}
{/each}
-
Frequently asked questions

+
Frequently asked questions

What is an execution?