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"}', '{}',