From aa12ecd5a26ddf686dbfd24ea4d249089bb0226c Mon Sep 17 00:00:00 2001 From: giancarlo Date: Sun, 31 Mar 2024 15:13:44 +0800 Subject: [PATCH] Refactor API code and simplify billing display The code in the webhook API has been refactored to move the DatabaseWebhookHandlerService instance out of the POST function scope. Furthermore, the display of renewal plan details on the billing page has been simplified and certain parts deemed superfluous have been removed. Numerous types and interfaces in the database.types.ts file have also been corrected and formatted for consistency and improved readability. --- apps/web/.env.development | 2 +- apps/web/app/api/database/webhook/route.ts | 4 +- .../src/components/current-plan-card.tsx | 13 +- .../billing-event-handler.service.ts | 26 +- .../billing-webhook-handler.service.ts | 10 +- .../database-webhook-handler.service.ts | 27 +- .../stripe-webhook-handler.service.ts | 98 +- packages/supabase/src/database.types.ts | 1665 +++++++++-------- supabase/migrations/20221215192558_schema.sql | 292 +-- supabase/seed.sql | 22 +- 10 files changed, 1133 insertions(+), 1026 deletions(-) diff --git a/apps/web/.env.development b/apps/web/.env.development index 8ee84a2c1..f67182512 100644 --- a/apps/web/.env.development +++ b/apps/web/.env.development @@ -20,7 +20,7 @@ NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzd NEXT_PUBLIC_REQUIRE_EMAIL_CONFIRMATION=true ## THIS IS FOR DEVELOPMENT ONLY - DO NOT USE IN PRODUCTION -SUPABASE_WEBHOOK_SECRET=WEBHOOKSECRET +SUPABASE_DB_WEBHOOK_SECRET=WEBHOOKSECRET EMAIL_SENDER=test@makerkit.dev EMAIL_PORT=54325 diff --git a/apps/web/app/api/database/webhook/route.ts b/apps/web/app/api/database/webhook/route.ts index 0144f3e73..b5ae3145e 100644 --- a/apps/web/app/api/database/webhook/route.ts +++ b/apps/web/app/api/database/webhook/route.ts @@ -9,9 +9,9 @@ const webhooksSecret = z .min(1) .parse(process.env.SUPABASE_DB_WEBHOOK_SECRET); -export async function POST(request: Request) { - const service = new DatabaseWebhookHandlerService(); +const service = new DatabaseWebhookHandlerService(); +export async function POST(request: Request) { await service.handleWebhook(request, webhooksSecret); return new Response(null, { diff --git a/packages/billing-gateway/src/components/current-plan-card.tsx b/packages/billing-gateway/src/components/current-plan-card.tsx index a76560430..2e2739512 100644 --- a/packages/billing-gateway/src/components/current-plan-card.tsx +++ b/packages/billing-gateway/src/components/current-plan-card.tsx @@ -34,8 +34,7 @@ export function CurrentPlanCard({ subscription: Database['public']['Tables']['subscriptions']['Row']; config: BillingConfig; }>) { - const { plan, product } = getProductPlanPair(config, subscription.variant_id); - const baseLineItem = getBaseLineItem(config, plan.id); + const { plan, product } = getProductPlanPair(config, subscription); return ( @@ -61,16 +60,6 @@ export function CurrentPlanCard({ {product.name} - -
- -
diff --git a/packages/billing-gateway/src/server/services/billing-event-handler/billing-event-handler.service.ts b/packages/billing-gateway/src/server/services/billing-event-handler/billing-event-handler.service.ts index 24df7cedc..0a9d03a51 100644 --- a/packages/billing-gateway/src/server/services/billing-event-handler/billing-event-handler.service.ts +++ b/packages/billing-gateway/src/server/services/billing-event-handler/billing-event-handler.service.ts @@ -53,19 +53,21 @@ export class BillingEventHandlerService { const ctx = { namespace: 'billing', - subscriptionId: subscription.id, + subscriptionId: subscription.subscription_id, provider: subscription.billing_provider, accountId: subscription.account_id, + customerId: subscription.customer_id, }; Logger.info(ctx, 'Processing subscription updated event'); // Handle the subscription updated event // here we update the subscription in the database - const { error } = await client - .from('subscriptions') - .update(subscription) - .match({ id: subscription.id }); + const { error } = await client.rpc('upsert_subscription', { + ...subscription, + customer_id: subscription.customer_id, + account_id: subscription.account_id, + }); if (error) { Logger.error( @@ -88,24 +90,16 @@ export class BillingEventHandlerService { const ctx = { namespace: 'billing', - subscriptionId: subscription.id, + subscriptionId: subscription.subscription_id, provider: subscription.billing_provider, accountId: subscription.account_id, }; Logger.info(ctx, 'Processing checkout session completed event...'); - const { id: _, ...data } = subscription; - - const { error } = await client.rpc('add_subscription', { - ...data, - subscription_id: subscription.id, + const { error } = await client.rpc('upsert_subscription', { + ...subscription, customer_id: customerId, - price_amount: subscription.price_amount ?? 0, - period_starts_at: subscription.period_starts_at!, - period_ends_at: subscription.period_ends_at!, - trial_starts_at: subscription.trial_starts_at!, - trial_ends_at: subscription.trial_ends_at!, }); if (error) { diff --git a/packages/billing/src/services/billing-webhook-handler.service.ts b/packages/billing/src/services/billing-webhook-handler.service.ts index b91f15a89..c14e284ae 100644 --- a/packages/billing/src/services/billing-webhook-handler.service.ts +++ b/packages/billing/src/services/billing-webhook-handler.service.ts @@ -1,8 +1,7 @@ import { Database } from '@kit/supabase/database'; -type SubscriptionObject = Database['public']['Tables']['subscriptions']; - -type SubscriptionUpdateParams = SubscriptionObject['Update']; +type UpsertSubscriptionParams = + Database['public']['Functions']['upsert_subscription']['Args']; /** * Represents an abstract class for handling billing webhook events. @@ -14,12 +13,13 @@ export abstract class BillingWebhookHandlerService { event: unknown, params: { onCheckoutSessionCompleted: ( - subscription: SubscriptionObject['Row'], + subscription: UpsertSubscriptionParams, customerId: string, ) => Promise; onSubscriptionUpdated: ( - subscription: SubscriptionUpdateParams, + subscription: UpsertSubscriptionParams, + customerId: string, ) => Promise; onSubscriptionDeleted: (subscriptionId: string) => Promise; diff --git a/packages/database-webhooks/src/server/services/database-webhook-handler.service.ts b/packages/database-webhooks/src/server/services/database-webhook-handler.service.ts index 06677811e..37474c1e7 100644 --- a/packages/database-webhooks/src/server/services/database-webhook-handler.service.ts +++ b/packages/database-webhooks/src/server/services/database-webhook-handler.service.ts @@ -10,9 +10,14 @@ export class DatabaseWebhookHandlerService { private readonly namespace = 'database-webhook-handler'; async handleWebhook(request: Request, webhooksSecret: string) { + const json = await request.clone().json(); + const { table, type } = json as RecordChange; + Logger.info( { name: this.namespace, + table, + type, }, 'Received webhook from DB. Processing...', ); @@ -21,11 +26,17 @@ export class DatabaseWebhookHandlerService { this.assertSignatureIsAuthentic(request, webhooksSecret); // all good, handle the webhook - const json = await request.json(); - await this.handleWebhookBody(json); + // create a client with admin access since we are handling webhooks + // and no user is authenticated + const client = getSupabaseRouteHandlerClient({ + admin: true, + }); - const { table, type } = json as RecordChange; + // handle the webhook + const service = new DatabaseWebhookRouterService(client); + + await service.handleWebhook(json); Logger.info( { @@ -37,16 +48,6 @@ export class DatabaseWebhookHandlerService { ); } - private handleWebhookBody(body: RecordChange) { - const client = getSupabaseRouteHandlerClient({ - admin: true, - }); - - const service = new DatabaseWebhookRouterService(client); - - return service.handleWebhook(body); - } - private assertSignatureIsAuthentic(request: Request, webhooksSecret: string) { const header = request.headers.get('X-Supabase-Event-Signature'); diff --git a/packages/stripe/src/services/stripe-webhook-handler.service.ts b/packages/stripe/src/services/stripe-webhook-handler.service.ts index b0d55e8a4..e9eb36dec 100644 --- a/packages/stripe/src/services/stripe-webhook-handler.service.ts +++ b/packages/stripe/src/services/stripe-webhook-handler.service.ts @@ -7,12 +7,8 @@ import { Database } from '@kit/supabase/database'; import { StripeServerEnvSchema } from '../schema/stripe-server-env.schema'; import { createStripeClient } from './stripe-sdk'; -type Subscription = Database['public']['Tables']['subscriptions']; - -type InsertSubscriptionParams = Omit< - Subscription['Insert'], - 'billing_customer_id' ->; +type UpsertSubscriptionParams = + Database['public']['Functions']['upsert_subscription']['Args']; export class StripeWebhookHandlerService implements BillingWebhookHandlerService @@ -64,11 +60,12 @@ export class StripeWebhookHandlerService event: Stripe.Event, params: { onCheckoutSessionCompleted: ( - data: InsertSubscriptionParams, - customerId: string, + data: UpsertSubscriptionParams, ) => Promise; - onSubscriptionUpdated: (data: Subscription['Update']) => Promise; + onSubscriptionUpdated: ( + data: UpsertSubscriptionParams, + ) => Promise; onSubscriptionDeleted: (subscriptionId: string) => Promise; }, ) { @@ -111,8 +108,7 @@ export class StripeWebhookHandlerService private async handleCheckoutSessionCompleted( event: Stripe.CheckoutSessionCompletedEvent, onCheckoutCompletedCallback: ( - data: Omit, - customerId: string, + data: UpsertSubscriptionParams, ) => Promise, ) { const stripe = await this.loadStripe(); @@ -134,20 +130,23 @@ export class StripeWebhookHandlerService const payload = this.buildSubscriptionPayload({ subscription, - accountId, amount, - }) as InsertSubscriptionParams; + accountId, + customerId, + }); - return onCheckoutCompletedCallback(payload, customerId); + return onCheckoutCompletedCallback(payload); } private async handleSubscriptionUpdatedEvent( event: Stripe.CustomerSubscriptionUpdatedEvent, onSubscriptionUpdatedCallback: ( - data: Subscription['Update'], + data: UpsertSubscriptionParams, ) => Promise, ) { const subscription = event.data.object; + const accountId = subscription.metadata.account_id as string; + const customerId = subscription.customer as string; const amount = subscription.items.data.reduce((acc, item) => { return (acc + (item.plan.amount ?? 0)) * (item.quantity ?? 1); @@ -156,6 +155,8 @@ export class StripeWebhookHandlerService const payload = this.buildSubscriptionPayload({ subscription, amount, + accountId, + customerId, }); return onSubscriptionUpdatedCallback(payload); @@ -173,52 +174,53 @@ export class StripeWebhookHandlerService private buildSubscriptionPayload(params: { subscription: Stripe.Subscription; amount: number; - // we only need the account id if we - // are creating a subscription for an account - accountId?: string; - }) { + accountId: string; + customerId: string; + }): UpsertSubscriptionParams { const { subscription } = params; - const lineItem = subscription.items.data[0]; - const price = lineItem?.price; - const priceId = price?.id as string; - const interval = price?.recurring?.interval ?? null; + const currency = subscription.currency; const active = subscription.status === 'active' || subscription.status === 'trialing'; - const data = { - billing_provider: this.provider, - id: subscription.id, - status: subscription.status, - active, - price_amount: params.amount, - cancel_at_period_end: subscription.cancel_at_period_end ?? false, - interval: interval as string, - currency: (price as Stripe.Price).currency, - product_id: (price as Stripe.Price).product as string, - variant_id: priceId, - interval_count: price?.recurring?.interval_count ?? 1, - period_starts_at: getISOString(subscription.current_period_start), - period_ends_at: getISOString(subscription.current_period_end), - trial_starts_at: getISOString(subscription.trial_start), - trial_ends_at: getISOString(subscription.trial_end), - } satisfies Omit; + const lineItems = subscription.items.data.map((item) => { + const quantity = item.quantity ?? 1; - // when we are creating a subscription for an account - // we need to include the account id - if (params.accountId !== undefined) { return { - ...data, - account_id: params.accountId, + id: item.id, + subscription_id: subscription.id, + product_id: item.price.product as string, + variant_id: item.price.id, + price_amount: item.price.unit_amount, + quantity, + interval: item.price.recurring?.interval as string, + interval_count: item.price.recurring?.interval_count as number, }; - } + }); // otherwise we are updating a subscription // and we only need to return the update payload - return data; + return { + line_items: lineItems, + billing_provider: this.provider, + subscription_id: subscription.id, + status: subscription.status, + total_amount: params.amount, + active, + currency, + cancel_at_period_end: subscription.cancel_at_period_end ?? false, + period_starts_at: getISOString( + subscription.current_period_start, + ) as string, + period_ends_at: getISOString(subscription.current_period_end) as string, + trial_starts_at: getISOString(subscription.trial_start), + trial_ends_at: getISOString(subscription.trial_end), + account_id: params.accountId, + customer_id: params.customerId, + }; } } function getISOString(date: number | null) { - return date ? new Date(date * 1000).toISOString() : null; + return date ? new Date(date * 1000).toISOString() : undefined; } diff --git a/packages/supabase/src/database.types.ts b/packages/supabase/src/database.types.ts index 4ffac81e7..f2f1a449a 100644 --- a/packages/supabase/src/database.types.ts +++ b/packages/supabase/src/database.types.ts @@ -4,1069 +4,1104 @@ export type Json = | boolean | null | { [key: string]: Json | undefined } - | Json[]; + | Json[] export type Database = { graphql_public: { Tables: { - [_ in never]: never; - }; + [_ in never]: never + } Views: { - [_ in never]: never; - }; + [_ in never]: never + } Functions: { graphql: { Args: { - operationName?: string; - query?: string; - variables?: Json; - extensions?: Json; - }; - Returns: Json; - }; - }; + operationName?: string + query?: string + variables?: Json + extensions?: Json + } + Returns: Json + } + } Enums: { - [_ in never]: never; - }; + [_ in never]: never + } CompositeTypes: { - [_ in never]: never; - }; - }; + [_ in never]: never + } + } public: { Tables: { account_roles: { Row: { - account_id: string; - id: number; - role: string; - }; + account_id: string + id: number + role: string + } Insert: { - account_id: string; - id?: number; - role: string; - }; + account_id: string + id?: number + role: string + } Update: { - account_id?: string; - id?: number; - role?: string; - }; + account_id?: string + id?: number + role?: string + } Relationships: [ { - foreignKeyName: 'account_roles_account_id_fkey'; - columns: ['account_id']; - isOneToOne: false; - referencedRelation: 'accounts'; - referencedColumns: ['id']; + foreignKeyName: "account_roles_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "accounts" + referencedColumns: ["id"] }, { - foreignKeyName: 'account_roles_account_id_fkey'; - columns: ['account_id']; - isOneToOne: false; - referencedRelation: 'user_account_workspace'; - referencedColumns: ['id']; + foreignKeyName: "account_roles_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_account_workspace" + referencedColumns: ["id"] }, { - foreignKeyName: 'account_roles_account_id_fkey'; - columns: ['account_id']; - isOneToOne: false; - referencedRelation: 'user_accounts'; - referencedColumns: ['id']; + foreignKeyName: "account_roles_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_accounts" + referencedColumns: ["id"] }, { - foreignKeyName: 'account_roles_role_fkey'; - columns: ['role']; - isOneToOne: false; - referencedRelation: 'roles'; - referencedColumns: ['name']; + foreignKeyName: "account_roles_role_fkey" + columns: ["role"] + isOneToOne: false + referencedRelation: "roles" + referencedColumns: ["name"] }, - ]; - }; + ] + } accounts: { Row: { - created_at: string | null; - created_by: string | null; - email: string | null; - id: string; - is_personal_account: boolean; - name: string; - picture_url: string | null; - primary_owner_user_id: string; - slug: string | null; - updated_at: string | null; - updated_by: string | null; - }; + created_at: string | null + created_by: string | null + email: string | null + id: string + is_personal_account: boolean + name: string + picture_url: string | null + primary_owner_user_id: string + slug: string | null + updated_at: string | null + updated_by: string | null + } Insert: { - created_at?: string | null; - created_by?: string | null; - email?: string | null; - id?: string; - is_personal_account?: boolean; - name: string; - picture_url?: string | null; - primary_owner_user_id?: string; - slug?: string | null; - updated_at?: string | null; - updated_by?: string | null; - }; + created_at?: string | null + created_by?: string | null + email?: string | null + id?: string + is_personal_account?: boolean + name: string + picture_url?: string | null + primary_owner_user_id?: string + slug?: string | null + updated_at?: string | null + updated_by?: string | null + } Update: { - created_at?: string | null; - created_by?: string | null; - email?: string | null; - id?: string; - is_personal_account?: boolean; - name?: string; - picture_url?: string | null; - primary_owner_user_id?: string; - slug?: string | null; - updated_at?: string | null; - updated_by?: string | null; - }; + created_at?: string | null + created_by?: string | null + email?: string | null + id?: string + is_personal_account?: boolean + name?: string + picture_url?: string | null + primary_owner_user_id?: string + slug?: string | null + updated_at?: string | null + updated_by?: string | null + } Relationships: [ { - foreignKeyName: 'accounts_created_by_fkey'; - columns: ['created_by']; - isOneToOne: false; - referencedRelation: 'users'; - referencedColumns: ['id']; + foreignKeyName: "accounts_created_by_fkey" + columns: ["created_by"] + isOneToOne: false + referencedRelation: "users" + referencedColumns: ["id"] }, { - foreignKeyName: 'accounts_primary_owner_user_id_fkey'; - columns: ['primary_owner_user_id']; - isOneToOne: false; - referencedRelation: 'users'; - referencedColumns: ['id']; + foreignKeyName: "accounts_primary_owner_user_id_fkey" + columns: ["primary_owner_user_id"] + isOneToOne: false + referencedRelation: "users" + referencedColumns: ["id"] }, { - foreignKeyName: 'accounts_updated_by_fkey'; - columns: ['updated_by']; - isOneToOne: false; - referencedRelation: 'users'; - referencedColumns: ['id']; + foreignKeyName: "accounts_updated_by_fkey" + columns: ["updated_by"] + isOneToOne: false + referencedRelation: "users" + referencedColumns: ["id"] }, - ]; - }; + ] + } accounts_memberships: { Row: { - account_id: string; - account_role: string; - created_at: string; - created_by: string | null; - updated_at: string; - updated_by: string | null; - user_id: string; - }; + account_id: string + account_role: string + created_at: string + created_by: string | null + updated_at: string + updated_by: string | null + user_id: string + } Insert: { - account_id: string; - account_role: string; - created_at?: string; - created_by?: string | null; - updated_at?: string; - updated_by?: string | null; - user_id: string; - }; + account_id: string + account_role: string + created_at?: string + created_by?: string | null + updated_at?: string + updated_by?: string | null + user_id: string + } Update: { - account_id?: string; - account_role?: string; - created_at?: string; - created_by?: string | null; - updated_at?: string; - updated_by?: string | null; - user_id?: string; - }; + account_id?: string + account_role?: string + created_at?: string + created_by?: string | null + updated_at?: string + updated_by?: string | null + user_id?: string + } Relationships: [ { - foreignKeyName: 'accounts_memberships_account_id_fkey'; - columns: ['account_id']; - isOneToOne: false; - referencedRelation: 'accounts'; - referencedColumns: ['id']; + foreignKeyName: "accounts_memberships_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "accounts" + referencedColumns: ["id"] }, { - foreignKeyName: 'accounts_memberships_account_id_fkey'; - columns: ['account_id']; - isOneToOne: false; - referencedRelation: 'user_account_workspace'; - referencedColumns: ['id']; + foreignKeyName: "accounts_memberships_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_account_workspace" + referencedColumns: ["id"] }, { - foreignKeyName: 'accounts_memberships_account_id_fkey'; - columns: ['account_id']; - isOneToOne: false; - referencedRelation: 'user_accounts'; - referencedColumns: ['id']; + foreignKeyName: "accounts_memberships_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_accounts" + referencedColumns: ["id"] }, { - foreignKeyName: 'accounts_memberships_account_role_fkey'; - columns: ['account_role']; - isOneToOne: false; - referencedRelation: 'roles'; - referencedColumns: ['name']; + foreignKeyName: "accounts_memberships_account_role_fkey" + columns: ["account_role"] + isOneToOne: false + referencedRelation: "roles" + referencedColumns: ["name"] }, { - foreignKeyName: 'accounts_memberships_created_by_fkey'; - columns: ['created_by']; - isOneToOne: false; - referencedRelation: 'users'; - referencedColumns: ['id']; + foreignKeyName: "accounts_memberships_created_by_fkey" + columns: ["created_by"] + isOneToOne: false + referencedRelation: "users" + referencedColumns: ["id"] }, { - foreignKeyName: 'accounts_memberships_updated_by_fkey'; - columns: ['updated_by']; - isOneToOne: false; - referencedRelation: 'users'; - referencedColumns: ['id']; + foreignKeyName: "accounts_memberships_updated_by_fkey" + columns: ["updated_by"] + isOneToOne: false + referencedRelation: "users" + referencedColumns: ["id"] }, { - foreignKeyName: 'accounts_memberships_user_id_fkey'; - columns: ['user_id']; - isOneToOne: false; - referencedRelation: 'users'; - referencedColumns: ['id']; + foreignKeyName: "accounts_memberships_user_id_fkey" + columns: ["user_id"] + isOneToOne: false + referencedRelation: "users" + referencedColumns: ["id"] }, - ]; - }; + ] + } billing_customers: { Row: { - account_id: string; - customer_id: string; - email: string | null; - id: number; - provider: Database['public']['Enums']['billing_provider']; - }; + account_id: string + customer_id: string + email: string | null + id: number + provider: Database["public"]["Enums"]["billing_provider"] + } Insert: { - account_id: string; - customer_id: string; - email?: string | null; - id?: number; - provider: Database['public']['Enums']['billing_provider']; - }; + account_id: string + customer_id: string + email?: string | null + id?: number + provider: Database["public"]["Enums"]["billing_provider"] + } Update: { - account_id?: string; - customer_id?: string; - email?: string | null; - id?: number; - provider?: Database['public']['Enums']['billing_provider']; - }; + account_id?: string + customer_id?: string + email?: string | null + id?: number + provider?: Database["public"]["Enums"]["billing_provider"] + } Relationships: [ { - foreignKeyName: 'billing_customers_account_id_fkey'; - columns: ['account_id']; - isOneToOne: false; - referencedRelation: 'accounts'; - referencedColumns: ['id']; + foreignKeyName: "billing_customers_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "accounts" + referencedColumns: ["id"] }, { - foreignKeyName: 'billing_customers_account_id_fkey'; - columns: ['account_id']; - isOneToOne: false; - referencedRelation: 'user_account_workspace'; - referencedColumns: ['id']; + foreignKeyName: "billing_customers_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_account_workspace" + referencedColumns: ["id"] }, { - foreignKeyName: 'billing_customers_account_id_fkey'; - columns: ['account_id']; - isOneToOne: false; - referencedRelation: 'user_accounts'; - referencedColumns: ['id']; + foreignKeyName: "billing_customers_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_accounts" + referencedColumns: ["id"] }, - ]; - }; + ] + } config: { Row: { - billing_provider: Database['public']['Enums']['billing_provider']; - enable_account_billing: boolean; - enable_organization_accounts: boolean; - enable_organization_billing: boolean; - }; + billing_provider: Database["public"]["Enums"]["billing_provider"] + enable_account_billing: boolean + enable_organization_accounts: boolean + enable_organization_billing: boolean + } Insert: { - billing_provider?: Database['public']['Enums']['billing_provider']; - enable_account_billing?: boolean; - enable_organization_accounts?: boolean; - enable_organization_billing?: boolean; - }; + billing_provider?: Database["public"]["Enums"]["billing_provider"] + enable_account_billing?: boolean + enable_organization_accounts?: boolean + enable_organization_billing?: boolean + } Update: { - billing_provider?: Database['public']['Enums']['billing_provider']; - enable_account_billing?: boolean; - enable_organization_accounts?: boolean; - enable_organization_billing?: boolean; - }; - Relationships: []; - }; + billing_provider?: Database["public"]["Enums"]["billing_provider"] + enable_account_billing?: boolean + enable_organization_accounts?: boolean + enable_organization_billing?: boolean + } + Relationships: [] + } invitations: { Row: { - account_id: string; - created_at: string; - email: string; - expires_at: string; - id: number; - invite_token: string; - invited_by: string; - role: string; - updated_at: string; - }; + account_id: string + created_at: string + email: string + expires_at: string + id: number + invite_token: string + invited_by: string + role: string + updated_at: string + } Insert: { - account_id: string; - created_at?: string; - email: string; - expires_at?: string; - id?: number; - invite_token: string; - invited_by: string; - role: string; - updated_at?: string; - }; + account_id: string + created_at?: string + email: string + expires_at?: string + id?: number + invite_token: string + invited_by: string + role: string + updated_at?: string + } Update: { - account_id?: string; - created_at?: string; - email?: string; - expires_at?: string; - id?: number; - invite_token?: string; - invited_by?: string; - role?: string; - updated_at?: string; - }; + account_id?: string + created_at?: string + email?: string + expires_at?: string + id?: number + invite_token?: string + invited_by?: string + role?: string + updated_at?: string + } Relationships: [ { - foreignKeyName: 'invitations_account_id_fkey'; - columns: ['account_id']; - isOneToOne: false; - referencedRelation: 'accounts'; - referencedColumns: ['id']; + foreignKeyName: "invitations_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "accounts" + referencedColumns: ["id"] }, { - foreignKeyName: 'invitations_account_id_fkey'; - columns: ['account_id']; - isOneToOne: false; - referencedRelation: 'user_account_workspace'; - referencedColumns: ['id']; + foreignKeyName: "invitations_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_account_workspace" + referencedColumns: ["id"] }, { - foreignKeyName: 'invitations_account_id_fkey'; - columns: ['account_id']; - isOneToOne: false; - referencedRelation: 'user_accounts'; - referencedColumns: ['id']; + foreignKeyName: "invitations_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_accounts" + referencedColumns: ["id"] }, { - foreignKeyName: 'invitations_invited_by_fkey'; - columns: ['invited_by']; - isOneToOne: false; - referencedRelation: 'users'; - referencedColumns: ['id']; + foreignKeyName: "invitations_invited_by_fkey" + columns: ["invited_by"] + isOneToOne: false + referencedRelation: "users" + referencedColumns: ["id"] }, { - foreignKeyName: 'invitations_role_fkey'; - columns: ['role']; - isOneToOne: false; - referencedRelation: 'roles'; - referencedColumns: ['name']; + foreignKeyName: "invitations_role_fkey" + columns: ["role"] + isOneToOne: false + referencedRelation: "roles" + referencedColumns: ["name"] }, - ]; - }; + ] + } role_permissions: { Row: { - id: number; - permission: Database['public']['Enums']['app_permissions']; - role: string; - }; + id: number + permission: Database["public"]["Enums"]["app_permissions"] + role: string + } Insert: { - id?: number; - permission: Database['public']['Enums']['app_permissions']; - role: string; - }; + id?: number + permission: Database["public"]["Enums"]["app_permissions"] + role: string + } Update: { - id?: number; - permission?: Database['public']['Enums']['app_permissions']; - role?: string; - }; + id?: number + permission?: Database["public"]["Enums"]["app_permissions"] + role?: string + } Relationships: [ { - foreignKeyName: 'role_permissions_role_fkey'; - columns: ['role']; - isOneToOne: false; - referencedRelation: 'roles'; - referencedColumns: ['name']; + foreignKeyName: "role_permissions_role_fkey" + columns: ["role"] + isOneToOne: false + referencedRelation: "roles" + referencedColumns: ["name"] }, - ]; - }; + ] + } roles: { Row: { - account_id: string | null; - hierarchy_level: number; - is_custom: boolean; - name: string; - }; + account_id: string | null + hierarchy_level: number + is_custom: boolean + name: string + } Insert: { - account_id?: string | null; - hierarchy_level: number; - is_custom?: boolean; - name: string; - }; + account_id?: string | null + hierarchy_level: number + is_custom?: boolean + name: string + } Update: { - account_id?: string | null; - hierarchy_level?: number; - is_custom?: boolean; - name?: string; - }; + account_id?: string | null + hierarchy_level?: number + is_custom?: boolean + name?: string + } Relationships: [ { - foreignKeyName: 'roles_account_id_fkey'; - columns: ['account_id']; - isOneToOne: false; - referencedRelation: 'accounts'; - referencedColumns: ['id']; + foreignKeyName: "roles_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "accounts" + referencedColumns: ["id"] }, { - foreignKeyName: 'roles_account_id_fkey'; - columns: ['account_id']; - isOneToOne: false; - referencedRelation: 'user_account_workspace'; - referencedColumns: ['id']; + foreignKeyName: "roles_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_account_workspace" + referencedColumns: ["id"] }, { - foreignKeyName: 'roles_account_id_fkey'; - columns: ['account_id']; - isOneToOne: false; - referencedRelation: 'user_accounts'; - referencedColumns: ['id']; + foreignKeyName: "roles_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_accounts" + referencedColumns: ["id"] }, - ]; - }; + ] + } + subscription_items: { + Row: { + created_at: string + id: string + interval: string + interval_count: number + price_amount: number | null + product_id: string + quantity: number + subscription_id: string + updated_at: string + variant_id: string + } + Insert: { + created_at?: string + id: string + interval: string + interval_count: number + price_amount?: number | null + product_id: string + quantity?: number + subscription_id: string + updated_at?: string + variant_id: string + } + Update: { + created_at?: string + id?: string + interval?: string + interval_count?: number + price_amount?: number | null + product_id?: string + quantity?: number + subscription_id?: string + updated_at?: string + variant_id?: string + } + Relationships: [ + { + foreignKeyName: "subscription_items_subscription_id_fkey" + columns: ["subscription_id"] + isOneToOne: false + referencedRelation: "subscriptions" + referencedColumns: ["id"] + }, + ] + } subscriptions: { Row: { - account_id: string; - active: boolean; - billing_customer_id: number; - billing_provider: Database['public']['Enums']['billing_provider']; - cancel_at_period_end: boolean; - created_at: string; - currency: string; - id: string; - interval: string; - interval_count: number; - period_ends_at: string | null; - period_starts_at: string | null; - price_amount: number | null; - product_id: string; - status: Database['public']['Enums']['subscription_status']; - trial_ends_at: string | null; - trial_starts_at: string | null; - updated_at: string; - variant_id: string; - }; + account_id: string + active: boolean + billing_customer_id: number + billing_provider: Database["public"]["Enums"]["billing_provider"] + cancel_at_period_end: boolean + created_at: string + currency: string + id: string + period_ends_at: string + period_starts_at: string + status: Database["public"]["Enums"]["subscription_status"] + total_amount: number + trial_ends_at: string | null + trial_starts_at: string | null + type: Database["public"]["Enums"]["subscription_type"] + updated_at: string + } Insert: { - account_id: string; - active: boolean; - billing_customer_id: number; - billing_provider: Database['public']['Enums']['billing_provider']; - cancel_at_period_end: boolean; - created_at?: string; - currency: string; - id: string; - interval: string; - interval_count: number; - period_ends_at?: string | null; - period_starts_at?: string | null; - price_amount?: number | null; - product_id: string; - status: Database['public']['Enums']['subscription_status']; - trial_ends_at?: string | null; - trial_starts_at?: string | null; - updated_at?: string; - variant_id: string; - }; + account_id: string + active: boolean + billing_customer_id: number + billing_provider: Database["public"]["Enums"]["billing_provider"] + cancel_at_period_end: boolean + created_at?: string + currency: string + id: string + period_ends_at: string + period_starts_at: string + status: Database["public"]["Enums"]["subscription_status"] + total_amount: number + trial_ends_at?: string | null + trial_starts_at?: string | null + type?: Database["public"]["Enums"]["subscription_type"] + updated_at?: string + } Update: { - account_id?: string; - active?: boolean; - billing_customer_id?: number; - billing_provider?: Database['public']['Enums']['billing_provider']; - cancel_at_period_end?: boolean; - created_at?: string; - currency?: string; - id?: string; - interval?: string; - interval_count?: number; - period_ends_at?: string | null; - period_starts_at?: string | null; - price_amount?: number | null; - product_id?: string; - status?: Database['public']['Enums']['subscription_status']; - trial_ends_at?: string | null; - trial_starts_at?: string | null; - updated_at?: string; - variant_id?: string; - }; + account_id?: string + active?: boolean + billing_customer_id?: number + billing_provider?: Database["public"]["Enums"]["billing_provider"] + cancel_at_period_end?: boolean + created_at?: string + currency?: string + id?: string + period_ends_at?: string + period_starts_at?: string + status?: Database["public"]["Enums"]["subscription_status"] + total_amount?: number + trial_ends_at?: string | null + trial_starts_at?: string | null + type?: Database["public"]["Enums"]["subscription_type"] + updated_at?: string + } Relationships: [ { - foreignKeyName: 'subscriptions_account_id_fkey'; - columns: ['account_id']; - isOneToOne: false; - referencedRelation: 'accounts'; - referencedColumns: ['id']; + foreignKeyName: "subscriptions_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "accounts" + referencedColumns: ["id"] }, { - foreignKeyName: 'subscriptions_account_id_fkey'; - columns: ['account_id']; - isOneToOne: false; - referencedRelation: 'user_account_workspace'; - referencedColumns: ['id']; + foreignKeyName: "subscriptions_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_account_workspace" + referencedColumns: ["id"] }, { - foreignKeyName: 'subscriptions_account_id_fkey'; - columns: ['account_id']; - isOneToOne: false; - referencedRelation: 'user_accounts'; - referencedColumns: ['id']; + foreignKeyName: "subscriptions_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_accounts" + referencedColumns: ["id"] }, { - foreignKeyName: 'subscriptions_billing_customer_id_fkey'; - columns: ['billing_customer_id']; - isOneToOne: false; - referencedRelation: 'billing_customers'; - referencedColumns: ['id']; + foreignKeyName: "subscriptions_billing_customer_id_fkey" + columns: ["billing_customer_id"] + isOneToOne: false + referencedRelation: "billing_customers" + referencedColumns: ["id"] }, - ]; - }; - }; + ] + } + } Views: { user_account_workspace: { Row: { - id: string | null; - name: string | null; - picture_url: string | null; + id: string | null + name: string | null + picture_url: string | null subscription_status: - | Database['public']['Enums']['subscription_status'] - | null; - }; - Relationships: []; - }; + | Database["public"]["Enums"]["subscription_status"] + | null + } + Relationships: [] + } user_accounts: { Row: { - id: string | null; - name: string | null; - picture_url: string | null; - role: string | null; - slug: string | null; - }; + id: string | null + name: string | null + picture_url: string | null + role: string | null + slug: string | null + } Relationships: [ { - foreignKeyName: 'accounts_memberships_account_role_fkey'; - columns: ['role']; - isOneToOne: false; - referencedRelation: 'roles'; - referencedColumns: ['name']; + foreignKeyName: "accounts_memberships_account_role_fkey" + columns: ["role"] + isOneToOne: false + referencedRelation: "roles" + referencedColumns: ["name"] }, - ]; - }; - }; + ] + } + } Functions: { accept_invitation: { Args: { - token: string; - user_id: string; - }; - Returns: undefined; - }; + token: string + user_id: string + } + Returns: undefined + } add_invitations_to_account: { Args: { - account_slug: string; - invitations: unknown[]; - }; - Returns: Database['public']['Tables']['invitations']['Row'][]; - }; - add_subscription: { - Args: { - account_id: string; - subscription_id: string; - active: boolean; - status: Database['public']['Enums']['subscription_status']; - billing_provider: Database['public']['Enums']['billing_provider']; - product_id: string; - variant_id: string; - price_amount: number; - cancel_at_period_end: boolean; - currency: string; - interval: string; - interval_count: number; - period_starts_at: string; - period_ends_at: string; - trial_starts_at: string; - trial_ends_at: string; - customer_id: string; - }; - Returns: { - account_id: string; - active: boolean; - billing_customer_id: number; - billing_provider: Database['public']['Enums']['billing_provider']; - cancel_at_period_end: boolean; - created_at: string; - currency: string; - id: string; - interval: string; - interval_count: number; - period_ends_at: string | null; - period_starts_at: string | null; - price_amount: number | null; - product_id: string; - status: Database['public']['Enums']['subscription_status']; - trial_ends_at: string | null; - trial_starts_at: string | null; - updated_at: string; - variant_id: string; - }; - }; + account_slug: string + invitations: unknown[] + } + Returns: Database["public"]["Tables"]["invitations"]["Row"][] + } create_account: { Args: { - account_name: string; - }; + account_name: string + } Returns: { - created_at: string | null; - created_by: string | null; - email: string | null; - id: string; - is_personal_account: boolean; - name: string; - picture_url: string | null; - primary_owner_user_id: string; - slug: string | null; - updated_at: string | null; - updated_by: string | null; - }; - }; + created_at: string | null + created_by: string | null + email: string | null + id: string + is_personal_account: boolean + name: string + picture_url: string | null + primary_owner_user_id: string + slug: string | null + updated_at: string | null + updated_by: string | null + } + } create_invitation: { Args: { - account_id: string; - email: string; - role: string; - }; + account_id: string + email: string + role: string + } Returns: { - account_id: string; - created_at: string; - email: string; - expires_at: string; - id: number; - invite_token: string; - invited_by: string; - role: string; - updated_at: string; - }; - }; + account_id: string + created_at: string + email: string + expires_at: string + id: number + invite_token: string + invited_by: string + role: string + updated_at: string + } + } get_account_invitations: { Args: { - account_slug: string; - }; + account_slug: string + } Returns: { - id: number; - email: string; - account_id: string; - invited_by: string; - role: string; - created_at: string; - updated_at: string; - expires_at: string; - inviter_name: string; - inviter_email: string; - }[]; - }; + id: number + email: string + account_id: string + invited_by: string + role: string + created_at: string + updated_at: string + expires_at: string + inviter_name: string + inviter_email: string + }[] + } get_account_members: { Args: { - account_slug: string; - }; + account_slug: string + } Returns: { - id: string; - user_id: string; - account_id: string; - role: string; - role_hierarchy_level: number; - primary_owner_user_id: string; - name: string; - email: string; - picture_url: string; - created_at: string; - updated_at: string; - }[]; - }; + id: string + user_id: string + account_id: string + role: string + role_hierarchy_level: number + primary_owner_user_id: string + name: string + email: string + picture_url: string + created_at: string + updated_at: string + }[] + } get_config: { - Args: Record; - Returns: Json; - }; + Args: Record + Returns: Json + } get_user_accounts: { - Args: Record; + Args: Record Returns: { - created_at: string | null; - created_by: string | null; - email: string | null; - id: string; - is_personal_account: boolean; - name: string; - picture_url: string | null; - primary_owner_user_id: string; - slug: string | null; - updated_at: string | null; - updated_by: string | null; - }[]; - }; + created_at: string | null + created_by: string | null + email: string | null + id: string + is_personal_account: boolean + name: string + picture_url: string | null + primary_owner_user_id: string + slug: string | null + updated_at: string | null + updated_by: string | null + }[] + } has_more_elevated_role: { Args: { - target_user_id: string; - target_account_id: string; - role_name: string; - }; - Returns: boolean; - }; + target_user_id: string + target_account_id: string + role_name: string + } + Returns: boolean + } has_permission: { Args: { - user_id: string; - account_id: string; - permission_name: Database['public']['Enums']['app_permissions']; - }; - Returns: boolean; - }; + user_id: string + account_id: string + permission_name: Database["public"]["Enums"]["app_permissions"] + } + Returns: boolean + } has_role_on_account: { Args: { - account_id: string; - account_role?: string; - }; - Returns: boolean; - }; + account_id: string + account_role?: string + } + Returns: boolean + } is_account_owner: { Args: { - account_id: string; - }; - Returns: boolean; - }; + account_id: string + } + Returns: boolean + } is_set: { Args: { - field_name: string; - }; - Returns: boolean; - }; + field_name: string + } + Returns: boolean + } is_team_member: { Args: { - account_id: string; - user_id: string; - }; - Returns: boolean; - }; + account_id: string + user_id: string + } + Returns: boolean + } organization_account_workspace: { Args: { - account_slug: string; - }; + account_slug: string + } Returns: { - id: string; - name: string; - picture_url: string; - slug: string; - role: string; - role_hierarchy_level: number; - primary_owner_user_id: string; - subscription_status: Database['public']['Enums']['subscription_status']; - permissions: Database['public']['Enums']['app_permissions'][]; - }[]; - }; + id: string + name: string + picture_url: string + slug: string + role: string + role_hierarchy_level: number + primary_owner_user_id: string + subscription_status: Database["public"]["Enums"]["subscription_status"] + permissions: Database["public"]["Enums"]["app_permissions"][] + }[] + } transfer_team_account_ownership: { Args: { - target_account_id: string; - new_owner_id: string; - }; - Returns: undefined; - }; + target_account_id: string + new_owner_id: string + } + Returns: undefined + } unaccent: { Args: { - '': string; - }; - Returns: string; - }; + "": string + } + Returns: string + } unaccent_init: { Args: { - '': unknown; - }; - Returns: unknown; - }; - }; + "": unknown + } + Returns: unknown + } + upsert_subscription: { + Args: { + account_id: string + subscription_id: string + active: boolean + total_amount: number + status: Database["public"]["Enums"]["subscription_status"] + billing_provider: Database["public"]["Enums"]["billing_provider"] + cancel_at_period_end: boolean + currency: string + period_starts_at: string + period_ends_at: string + customer_id: string + line_items: Json + trial_starts_at?: string + trial_ends_at?: string + type?: Database["public"]["Enums"]["subscription_type"] + } + Returns: { + account_id: string + active: boolean + billing_customer_id: number + billing_provider: Database["public"]["Enums"]["billing_provider"] + cancel_at_period_end: boolean + created_at: string + currency: string + id: string + period_ends_at: string + period_starts_at: string + status: Database["public"]["Enums"]["subscription_status"] + total_amount: number + trial_ends_at: string | null + trial_starts_at: string | null + type: Database["public"]["Enums"]["subscription_type"] + updated_at: string + } + } + } Enums: { app_permissions: - | 'roles.manage' - | 'billing.manage' - | 'settings.manage' - | 'members.manage' - | 'invites.manage'; - billing_provider: 'stripe' | 'lemon-squeezy' | 'paddle'; + | "roles.manage" + | "billing.manage" + | "settings.manage" + | "members.manage" + | "invites.manage" + billing_provider: "stripe" | "lemon-squeezy" | "paddle" subscription_status: - | 'active' - | 'trialing' - | 'past_due' - | 'canceled' - | 'unpaid' - | 'incomplete' - | 'incomplete_expired' - | 'paused'; - }; + | "active" + | "trialing" + | "past_due" + | "canceled" + | "unpaid" + | "incomplete" + | "incomplete_expired" + | "paused" + subscription_type: "one-off" | "recurring" + } CompositeTypes: { - [_ in never]: never; - }; - }; + [_ in never]: never + } + } storage: { Tables: { buckets: { Row: { - allowed_mime_types: string[] | null; - avif_autodetection: boolean | null; - created_at: string | null; - file_size_limit: number | null; - id: string; - name: string; - owner: string | null; - owner_id: string | null; - public: boolean | null; - updated_at: string | null; - }; + allowed_mime_types: string[] | null + avif_autodetection: boolean | null + created_at: string | null + file_size_limit: number | null + id: string + name: string + owner: string | null + owner_id: string | null + public: boolean | null + updated_at: string | null + } Insert: { - allowed_mime_types?: string[] | null; - avif_autodetection?: boolean | null; - created_at?: string | null; - file_size_limit?: number | null; - id: string; - name: string; - owner?: string | null; - owner_id?: string | null; - public?: boolean | null; - updated_at?: string | null; - }; + allowed_mime_types?: string[] | null + avif_autodetection?: boolean | null + created_at?: string | null + file_size_limit?: number | null + id: string + name: string + owner?: string | null + owner_id?: string | null + public?: boolean | null + updated_at?: string | null + } Update: { - allowed_mime_types?: string[] | null; - avif_autodetection?: boolean | null; - created_at?: string | null; - file_size_limit?: number | null; - id?: string; - name?: string; - owner?: string | null; - owner_id?: string | null; - public?: boolean | null; - updated_at?: string | null; - }; - Relationships: []; - }; + allowed_mime_types?: string[] | null + avif_autodetection?: boolean | null + created_at?: string | null + file_size_limit?: number | null + id?: string + name?: string + owner?: string | null + owner_id?: string | null + public?: boolean | null + updated_at?: string | null + } + Relationships: [] + } migrations: { Row: { - executed_at: string | null; - hash: string; - id: number; - name: string; - }; + executed_at: string | null + hash: string + id: number + name: string + } Insert: { - executed_at?: string | null; - hash: string; - id: number; - name: string; - }; + executed_at?: string | null + hash: string + id: number + name: string + } Update: { - executed_at?: string | null; - hash?: string; - id?: number; - name?: string; - }; - Relationships: []; - }; + executed_at?: string | null + hash?: string + id?: number + name?: string + } + Relationships: [] + } objects: { Row: { - bucket_id: string | null; - created_at: string | null; - id: string; - last_accessed_at: string | null; - metadata: Json | null; - name: string | null; - owner: string | null; - owner_id: string | null; - path_tokens: string[] | null; - updated_at: string | null; - version: string | null; - }; + bucket_id: string | null + created_at: string | null + id: string + last_accessed_at: string | null + metadata: Json | null + name: string | null + owner: string | null + owner_id: string | null + path_tokens: string[] | null + updated_at: string | null + version: string | null + } Insert: { - bucket_id?: string | null; - created_at?: string | null; - id?: string; - last_accessed_at?: string | null; - metadata?: Json | null; - name?: string | null; - owner?: string | null; - owner_id?: string | null; - path_tokens?: string[] | null; - updated_at?: string | null; - version?: string | null; - }; + bucket_id?: string | null + created_at?: string | null + id?: string + last_accessed_at?: string | null + metadata?: Json | null + name?: string | null + owner?: string | null + owner_id?: string | null + path_tokens?: string[] | null + updated_at?: string | null + version?: string | null + } Update: { - bucket_id?: string | null; - created_at?: string | null; - id?: string; - last_accessed_at?: string | null; - metadata?: Json | null; - name?: string | null; - owner?: string | null; - owner_id?: string | null; - path_tokens?: string[] | null; - updated_at?: string | null; - version?: string | null; - }; + bucket_id?: string | null + created_at?: string | null + id?: string + last_accessed_at?: string | null + metadata?: Json | null + name?: string | null + owner?: string | null + owner_id?: string | null + path_tokens?: string[] | null + updated_at?: string | null + version?: string | null + } Relationships: [ { - foreignKeyName: 'objects_bucketId_fkey'; - columns: ['bucket_id']; - isOneToOne: false; - referencedRelation: 'buckets'; - referencedColumns: ['id']; + foreignKeyName: "objects_bucketId_fkey" + columns: ["bucket_id"] + isOneToOne: false + referencedRelation: "buckets" + referencedColumns: ["id"] }, - ]; - }; - }; + ] + } + } Views: { - [_ in never]: never; - }; + [_ in never]: never + } Functions: { can_insert_object: { Args: { - bucketid: string; - name: string; - owner: string; - metadata: Json; - }; - Returns: undefined; - }; + bucketid: string + name: string + owner: string + metadata: Json + } + Returns: undefined + } extension: { Args: { - name: string; - }; - Returns: string; - }; + name: string + } + Returns: string + } filename: { Args: { - name: string; - }; - Returns: string; - }; + name: string + } + Returns: string + } foldername: { Args: { - name: string; - }; - Returns: string[]; - }; + name: string + } + Returns: string[] + } get_size_by_bucket: { - Args: Record; + Args: Record Returns: { - size: number; - bucket_id: string; - }[]; - }; + size: number + bucket_id: string + }[] + } search: { Args: { - prefix: string; - bucketname: string; - limits?: number; - levels?: number; - offsets?: number; - search?: string; - sortcolumn?: string; - sortorder?: string; - }; + prefix: string + bucketname: string + limits?: number + levels?: number + offsets?: number + search?: string + sortcolumn?: string + sortorder?: string + } Returns: { - name: string; - id: string; - updated_at: string; - created_at: string; - last_accessed_at: string; - metadata: Json; - }[]; - }; - }; + name: string + id: string + updated_at: string + created_at: string + last_accessed_at: string + metadata: Json + }[] + } + } Enums: { - [_ in never]: never; - }; + [_ in never]: never + } CompositeTypes: { - [_ in never]: never; - }; - }; -}; + [_ in never]: never + } + } +} -type PublicSchema = Database[Extract]; +type PublicSchema = Database[Extract] export type Tables< PublicTableNameOrOptions extends - | keyof (PublicSchema['Tables'] & PublicSchema['Views']) + | keyof (PublicSchema["Tables"] & PublicSchema["Views"]) | { schema: keyof Database }, TableName extends PublicTableNameOrOptions extends { schema: keyof Database } - ? keyof (Database[PublicTableNameOrOptions['schema']]['Tables'] & - Database[PublicTableNameOrOptions['schema']]['Views']) + ? keyof (Database[PublicTableNameOrOptions["schema"]]["Tables"] & + Database[PublicTableNameOrOptions["schema"]]["Views"]) : never = never, > = PublicTableNameOrOptions extends { schema: keyof Database } - ? (Database[PublicTableNameOrOptions['schema']]['Tables'] & - Database[PublicTableNameOrOptions['schema']]['Views'])[TableName] extends { - Row: infer R; + ? (Database[PublicTableNameOrOptions["schema"]]["Tables"] & + Database[PublicTableNameOrOptions["schema"]]["Views"])[TableName] extends { + Row: infer R } ? R : never - : PublicTableNameOrOptions extends keyof (PublicSchema['Tables'] & - PublicSchema['Views']) - ? (PublicSchema['Tables'] & - PublicSchema['Views'])[PublicTableNameOrOptions] extends { - Row: infer R; + : PublicTableNameOrOptions extends keyof (PublicSchema["Tables"] & + PublicSchema["Views"]) + ? (PublicSchema["Tables"] & + PublicSchema["Views"])[PublicTableNameOrOptions] extends { + Row: infer R } ? R : never - : never; + : never export type TablesInsert< PublicTableNameOrOptions extends - | keyof PublicSchema['Tables'] + | keyof PublicSchema["Tables"] | { schema: keyof Database }, TableName extends PublicTableNameOrOptions extends { schema: keyof Database } - ? keyof Database[PublicTableNameOrOptions['schema']]['Tables'] + ? keyof Database[PublicTableNameOrOptions["schema"]]["Tables"] : never = never, > = PublicTableNameOrOptions extends { schema: keyof Database } - ? Database[PublicTableNameOrOptions['schema']]['Tables'][TableName] extends { - Insert: infer I; + ? Database[PublicTableNameOrOptions["schema"]]["Tables"][TableName] extends { + Insert: infer I } ? I : never - : PublicTableNameOrOptions extends keyof PublicSchema['Tables'] - ? PublicSchema['Tables'][PublicTableNameOrOptions] extends { - Insert: infer I; + : PublicTableNameOrOptions extends keyof PublicSchema["Tables"] + ? PublicSchema["Tables"][PublicTableNameOrOptions] extends { + Insert: infer I } ? I : never - : never; + : never export type TablesUpdate< PublicTableNameOrOptions extends - | keyof PublicSchema['Tables'] + | keyof PublicSchema["Tables"] | { schema: keyof Database }, TableName extends PublicTableNameOrOptions extends { schema: keyof Database } - ? keyof Database[PublicTableNameOrOptions['schema']]['Tables'] + ? keyof Database[PublicTableNameOrOptions["schema"]]["Tables"] : never = never, > = PublicTableNameOrOptions extends { schema: keyof Database } - ? Database[PublicTableNameOrOptions['schema']]['Tables'][TableName] extends { - Update: infer U; + ? Database[PublicTableNameOrOptions["schema"]]["Tables"][TableName] extends { + Update: infer U } ? U : never - : PublicTableNameOrOptions extends keyof PublicSchema['Tables'] - ? PublicSchema['Tables'][PublicTableNameOrOptions] extends { - Update: infer U; + : PublicTableNameOrOptions extends keyof PublicSchema["Tables"] + ? PublicSchema["Tables"][PublicTableNameOrOptions] extends { + Update: infer U } ? U : never - : never; + : never export type Enums< PublicEnumNameOrOptions extends - | keyof PublicSchema['Enums'] + | keyof PublicSchema["Enums"] | { schema: keyof Database }, EnumName extends PublicEnumNameOrOptions extends { schema: keyof Database } - ? keyof Database[PublicEnumNameOrOptions['schema']]['Enums'] + ? keyof Database[PublicEnumNameOrOptions["schema"]]["Enums"] : never = never, > = PublicEnumNameOrOptions extends { schema: keyof Database } - ? Database[PublicEnumNameOrOptions['schema']]['Enums'][EnumName] - : PublicEnumNameOrOptions extends keyof PublicSchema['Enums'] - ? PublicSchema['Enums'][PublicEnumNameOrOptions] - : never; + ? Database[PublicEnumNameOrOptions["schema"]]["Enums"][EnumName] + : PublicEnumNameOrOptions extends keyof PublicSchema["Enums"] + ? PublicSchema["Enums"][PublicEnumNameOrOptions] + : never + diff --git a/supabase/migrations/20221215192558_schema.sql b/supabase/migrations/20221215192558_schema.sql index d012c9626..f95847e41 100644 --- a/supabase/migrations/20221215192558_schema.sql +++ b/supabase/migrations/20221215192558_schema.sql @@ -118,6 +118,16 @@ create type public.subscription_status as ENUM( 'paused' ); +/* Subscription Type +- We create the subscription type for the Supabase MakerKit. These types are used to manage the type of the subscriptions +- The types are 'ONE_OFF' and 'RECURRING'. +- You can add more types as needed. +*/ +create type public.subscription_type as enum ( + 'one-off', + 'recurring' +); + /* * Billing Provider - We create the billing provider for the Supabase MakerKit. These providers are used to manage the billing provider for the accounts and organizations @@ -944,28 +954,24 @@ select * ------------------------------------------------------- */ -- Subscriptions table -create table if not exists - public.subscriptions ( - account_id uuid references public.accounts (id) on delete cascade not null, - billing_customer_id int references public.billing_customers on delete cascade not null, - id text not null primary key, - status public.subscription_status not null, - active bool not null, - billing_provider public.billing_provider not null, - product_id varchar(255) not null, - variant_id varchar(255) not null, - price_amount numeric, - cancel_at_period_end bool not null, - currency varchar(3) not null, - interval varchar(255) not null, - interval_count integer not null check (interval_count > 0), - created_at timestamptz not null default current_timestamp, - updated_at timestamptz not null default current_timestamp, - period_starts_at timestamptz, - period_ends_at timestamptz, - trial_starts_at timestamptz, - trial_ends_at timestamptz - ); +create table if not exists public.subscriptions ( + id text not null primary key, + account_id uuid references public.accounts (id) on delete cascade not null, + billing_customer_id int references public.billing_customers on delete cascade not null, + status public.subscription_status not null, + type public.subscription_type not null default 'recurring', + total_amount numeric not null, + active bool not null, + billing_provider public.billing_provider not null, + cancel_at_period_end bool not null, + currency varchar(3) not null, + created_at timestamptz not null default current_timestamp, + updated_at timestamptz not null default current_timestamp, + period_starts_at timestamptz not null, + period_ends_at timestamptz not null, + trial_starts_at timestamptz, + trial_ends_at timestamptz +); comment on table public.subscriptions is 'The subscriptions for an account'; @@ -973,22 +979,16 @@ comment on column public.subscriptions.account_id is 'The account the subscripti comment on column public.subscriptions.billing_provider is 'The provider of the subscription'; -comment on column public.subscriptions.product_id is 'The product ID for the subscription'; - -comment on column public.subscriptions.variant_id is 'The variant ID for the subscription'; - -comment on column public.subscriptions.price_amount is 'The price amount for the subscription'; +comment on column public.subscriptions.total_amount is 'The total price amount for the subscription'; comment on column public.subscriptions.cancel_at_period_end is 'Whether the subscription will be canceled at the end of the period'; comment on column public.subscriptions.currency is 'The currency for the subscription'; -comment on column public.subscriptions.interval is 'The interval for the subscription'; - -comment on column public.subscriptions.interval_count is 'The interval count for the subscription'; - comment on column public.subscriptions.status is 'The status of the subscription'; +comment on column public.subscriptions.type is 'The type of the subscription, either one-off or recurring'; + comment on column public.subscriptions.period_starts_at is 'The start of the current period for the subscription'; comment on column public.subscriptions.period_ends_at is 'The end of the current period for the subscription'; @@ -1017,103 +1017,177 @@ select to authenticated using (has_role_on_account (account_id) or account_id = auth.uid ()); -- Functions - -create or replace function public.add_subscription ( +create or replace function public.upsert_subscription ( account_id uuid, subscription_id text, active bool, + total_amount numeric, status public.subscription_status, billing_provider public.billing_provider, - product_id varchar(255), - variant_id varchar(255), - price_amount numeric, cancel_at_period_end bool, currency varchar(3), - "interval" varchar(255), - interval_count integer, period_starts_at timestamptz, period_ends_at timestamptz, - trial_starts_at timestamptz, - trial_ends_at timestamptz, - customer_id varchar(255) + customer_id varchar(255), + line_items jsonb, + trial_starts_at timestamptz default null, + trial_ends_at timestamptz default null, + type public.subscription_type default 'recurring' ) returns public.subscriptions as $$ declare new_subscription public.subscriptions; new_billing_customer_id int; begin - insert into public.billing_customers( - account_id, - provider, - customer_id) - values ( - account_id, - billing_provider, - customer_id) - returning - id into new_billing_customer_id; + insert into public.billing_customers(account_id, provider, customer_id) + values (account_id, billing_provider, customer_id) + on conflict (account_id, provider, customer_id) do update + set provider = excluded.provider + returning id into new_billing_customer_id; - insert into public.subscriptions( - account_id, - billing_customer_id, - id, - active, - status, - billing_provider, - product_id, - variant_id, - price_amount, - cancel_at_period_end, - currency, - interval, - interval_count, - period_starts_at, - period_ends_at, - trial_starts_at, - trial_ends_at) - values ( - account_id, - new_billing_customer_id, - subscription_id, - active, - status, - billing_provider, - product_id, - variant_id, - price_amount, - cancel_at_period_end, - currency, - interval, - interval_count, - period_starts_at, - period_ends_at, - trial_starts_at, - trial_ends_at) - returning - * into new_subscription; - return new_subscription; - end; + insert into public.subscriptions( + account_id, + billing_customer_id, + id, + active, + total_amount, + status, + type, + billing_provider, + cancel_at_period_end, + currency, + period_starts_at, + period_ends_at, + trial_starts_at, + trial_ends_at) + values ( + account_id, + new_billing_customer_id, + subscription_id, + active, + total_amount, + status, + type, + billing_provider, + cancel_at_period_end, + currency, + period_starts_at, + period_ends_at, + trial_starts_at, + trial_ends_at) + on conflict (id) do update + set active = excluded.active, + status = excluded.status, + cancel_at_period_end = excluded.cancel_at_period_end, + currency = excluded.currency, + period_starts_at = excluded.period_starts_at, + period_ends_at = excluded.period_ends_at, + trial_starts_at = excluded.trial_starts_at, + trial_ends_at = excluded.trial_ends_at + returning * into new_subscription; + + -- Upsert subscription items + with item_data as ( + select + (line_item ->> 'product_id')::varchar as prod_id, + (line_item ->> 'variant_id')::varchar as var_id, + (line_item ->> 'price_amount')::numeric as price_amt, + (line_item ->> 'quantity')::integer as qty, + (line_item ->> 'interval')::varchar as intv, + (line_item ->> 'interval_count')::integer as intv_count + from jsonb_array_elements(line_items) as line_item + ) + insert into public.subscription_items( + subscription_id, + product_id, + variant_id, + price_amount, + quantity, + interval, + interval_count) + select + subscription_id, + prod_id, + var_id, + price_amt, + qty, + intv, + intv_count + from item_data + on conflict (subscription_id, product_id, variant_id) do update + set price_amount = excluded.price_amount, + quantity = excluded.quantity, + interval = excluded.interval, + interval_count = excluded.interval_count; + + return new_subscription; +end; $$ language plpgsql; -grant execute on function public.add_subscription ( - uuid, - text, - boolean, - public.subscription_status, - public.billing_provider, - varchar, - varchar, - numeric, - boolean, - varchar, - varchar, - integer, - timestamptz, - timestamptz, - timestamptz, - timestamptz, - varchar +grant execute on function public.upsert_subscription ( + uuid, + text, + bool, + numeric, + public.subscription_status, + public.billing_provider, + bool, + varchar, + timestamptz, + timestamptz, + varchar, + jsonb, + timestamptz, + timestamptz, + public.subscription_type ) to service_role; + +/* ------------------------------------------------------- + * Section: Subscription Items + * We create the schema for the subscription items. Subscription items are the items in a subscription. + * For example, a subscription might have a subscription item with the product ID 'prod_123' and the variant ID 'var_123'. + * ------------------------------------------------------- + */ +create table if not exists public.subscription_items ( + id text not null primary key, + subscription_id text references public.subscriptions (id) on delete cascade not null, + product_id varchar(255) not null, + variant_id varchar(255) not null, + price_amount numeric, + quantity integer not null default 1, + interval varchar(255) not null, + interval_count integer not null check (interval_count > 0), + created_at timestamptz not null default current_timestamp, + updated_at timestamptz not null default current_timestamp +); + +comment on table public.subscription_items is 'The items in a subscription'; +comment on column public.subscription_items.subscription_id is 'The subscription the item is for'; +comment on column public.subscription_items.product_id is 'The product ID for the item'; +comment on column public.subscription_items.variant_id is 'The variant ID for the item'; +comment on column public.subscription_items.price_amount is 'The price amount for the item'; +comment on column public.subscription_items.quantity is 'The quantity of the item'; +comment on column public.subscription_items.interval is 'The interval for the item'; +comment on column public.subscription_items.interval_count is 'The interval count for the item'; +comment on column public.subscription_items.created_at is 'The creation date of the item'; +comment on column public.subscription_items.updated_at is 'The last update date of the item'; + +-- Open up access to subscription_items table for authenticated users and service_role +grant select on table public.subscription_items to authenticated, service_role; +grant insert, update, delete on table public.subscription_items to service_role; + +-- RLS +alter table public.subscription_items enable row level security; + +-- SELECT: Users can read subscription items on a subscription they are a member of +create policy subscription_items_read_self on public.subscription_items for +select + to authenticated using ( + exists ( + select 1 from public.subscriptions where id = subscription_id and (account_id = auth.uid () or has_role_on_account (account_id)) + ) + ); + /* * ------------------------------------------------------- * Section: Functions diff --git a/supabase/seed.sql b/supabase/seed.sql index b44a50283..6270defb4 100644 --- a/supabase/seed.sql +++ b/supabase/seed.sql @@ -6,18 +6,18 @@ create trigger "accounts_memberships_insert" after insert on "public"."accounts_memberships" for each row execute function "supabase_functions"."http_request"( - 'http://localhost:3000/api/database/webhook', + 'http://host.docker.internal:3000/api/database/webhook', 'POST', '{"Content-Type":"application/json", "X-Supabase-Event-Signature":"WEBHOOKSECRET"}', '{}', '1000' ); --- this webhook will be triggered after every insert on the accounts_memberships table -create trigger "account_membership_delete" after insert +-- this webhook will be triggered after every delete on the accounts_memberships table +create trigger "account_membership_delete" after delete on "public"."accounts_memberships" for each row execute function "supabase_functions"."http_request"( - 'http://localhost:3000/api/database/webhook', + 'http://host.docker.internal:3000/api/database/webhook', 'POST', '{"Content-Type":"application/json", "X-Supabase-Event-Signature":"WEBHOOKSECRET"}', '{}', @@ -29,7 +29,19 @@ execute function "supabase_functions"."http_request"( create trigger "account_delete" after delete on "public"."subscriptions" for each row execute function "supabase_functions"."http_request"( - 'http://localhost:3000/api/database/webhook', + 'http://host.docker.internal:3000/api/database/webhook', + 'POST', + '{"Content-Type":"application/json", "X-Supabase-Event-Signature":"WEBHOOKSECRET"}', + '{}', + '1000' +); + +-- this webhook will be triggered after every insert on the invitations table +-- which should happen when a user invites someone to their account +create trigger "invitations_insert" after insert +on "public"."invitations" for each row +execute function "supabase_functions"."http_request"( + 'http://host.docker.internal:3000/api/database/webhook', 'POST', '{"Content-Type":"application/json", "X-Supabase-Event-Signature":"WEBHOOKSECRET"}', '{}',