diff --git a/docs/database-setup.md b/docs/database-setup.md index e63eb22..5814ff1 100644 --- a/docs/database-setup.md +++ b/docs/database-setup.md @@ -16,11 +16,11 @@ To get the types for the tables, you can follow these instructions taken from [h `npx supabase init` -Replace `$PROJECT_REF` with your project reference: -`npx supabase gen types typescript --project-id "$PROJECT_REF" --schema public > types/supabase.ts` +Replace `$PROJECT_REF` with your project reference and replace `types/supabase.d.ts` with whatever file you want your types in: +`npx supabase gen types typescript --project-id "$PROJECT_REF" --schema public > src/lib/types/supabase.d.ts` -Replace `types/supabase.d.ts` with whatever file you want your types in: -`npx supabase gen types typescript --local > types/supabase.d.ts` +If you are using a local Supabase instance, you can use the following command to generate the types: +`npx supabase gen types typescript --local > src/lib/types/supabase.d.ts` ## Local DB Development diff --git a/src/lib/types/supabase.d.ts b/src/lib/types/supabase.d.ts index c0a26d5..42f3532 100644 --- a/src/lib/types/supabase.d.ts +++ b/src/lib/types/supabase.d.ts @@ -1,639 +1,391 @@ -export type Json = string | number | boolean | null | { [key: string]: Json | undefined } | Json[]; +export type Json = + | string + | number + | boolean + | null + | { [key: string]: Json | undefined } + | Json[] export type Database = { - graphql_public: { - Tables: { - [_ in never]: never; - }; - Views: { - [_ in never]: never; - }; - Functions: { - graphql: { - Args: { - operationName?: string; - query?: string; - variables?: Json; - extensions?: Json; - }; - Returns: Json; - }; - }; - Enums: { - [_ in never]: never; - }; - CompositeTypes: { - [_ in never]: never; - }; - }; - public: { - Tables: { - customers: { - Row: { - id: string; - stripe_customer_id: string | null; - }; - Insert: { - id: string; - stripe_customer_id?: string | null; - }; - Update: { - id?: string; - stripe_customer_id?: string | null; - }; - Relationships: [ - { - foreignKeyName: 'customers_id_fkey'; - columns: ['id']; - isOneToOne: true; - referencedRelation: 'users'; - referencedColumns: ['id']; - } - ]; - }; - prices: { - Row: { - active: boolean | null; - currency: string | null; - description: string | null; - id: string; - interval: Database['public']['Enums']['pricing_plan_interval'] | null; - interval_count: number | null; - metadata: Json | null; - product_id: string; - trial_period_days: number | null; - type: Database['public']['Enums']['pricing_type'] | null; - unit_amount: number | null; - }; - Insert: { - active?: boolean | null; - currency?: string | null; - description?: string | null; - id: string; - interval?: Database['public']['Enums']['pricing_plan_interval'] | null; - interval_count?: number | null; - metadata?: Json | null; - product_id: string; - trial_period_days?: number | null; - type?: Database['public']['Enums']['pricing_type'] | null; - unit_amount?: number | null; - }; - Update: { - active?: boolean | null; - currency?: string | null; - description?: string | null; - id?: string; - interval?: Database['public']['Enums']['pricing_plan_interval'] | null; - interval_count?: number | null; - metadata?: Json | null; - product_id?: string; - trial_period_days?: number | null; - type?: Database['public']['Enums']['pricing_type'] | null; - unit_amount?: number | null; - }; - Relationships: [ - { - foreignKeyName: 'prices_product_id_fkey'; - columns: ['product_id']; - isOneToOne: false; - referencedRelation: 'products'; - referencedColumns: ['id']; - } - ]; - }; - products: { - Row: { - active: boolean | null; - created_at: string; - description: string | null; - features: string[] | null; - id: string; - images: string[] | null; - metadata: Json | null; - name: string | null; - updated_at: string; - }; - Insert: { - active?: boolean | null; - created_at?: string; - description?: string | null; - features?: string[] | null; - id: string; - images?: string[] | null; - metadata?: Json | null; - name?: string | null; - updated_at?: string; - }; - Update: { - active?: boolean | null; - created_at?: string; - description?: string | null; - features?: string[] | null; - id?: string; - images?: string[] | null; - metadata?: Json | null; - name?: string | null; - updated_at?: string; - }; - Relationships: []; - }; - subscriptions: { - Row: { - cancel_at: string | null; - cancel_at_period_end: boolean | null; - canceled_at: string | null; - created: string; - current_period_end: string; - current_period_start: string; - ended_at: string | null; - id: string; - metadata: Json | null; - price_id: string | null; - quantity: number | null; - status: Database['public']['Enums']['subscription_status'] | null; - trial_end: string | null; - trial_start: string | null; - user_id: string; - }; - Insert: { - cancel_at?: string | null; - cancel_at_period_end?: boolean | null; - canceled_at?: string | null; - created?: string; - current_period_end?: string; - current_period_start?: string; - ended_at?: string | null; - id: string; - metadata?: Json | null; - price_id?: string | null; - quantity?: number | null; - status?: Database['public']['Enums']['subscription_status'] | null; - trial_end?: string | null; - trial_start?: string | null; - user_id: string; - }; - Update: { - cancel_at?: string | null; - cancel_at_period_end?: boolean | null; - canceled_at?: string | null; - created?: string; - current_period_end?: string; - current_period_start?: string; - ended_at?: string | null; - id?: string; - metadata?: Json | null; - price_id?: string | null; - quantity?: number | null; - status?: Database['public']['Enums']['subscription_status'] | null; - trial_end?: string | null; - trial_start?: string | null; - user_id?: string; - }; - Relationships: [ - { - foreignKeyName: 'subscriptions_price_id_fkey'; - columns: ['price_id']; - isOneToOne: false; - referencedRelation: 'prices'; - referencedColumns: ['id']; - }, - { - foreignKeyName: 'subscriptions_user_id_fkey'; - columns: ['user_id']; - isOneToOne: false; - referencedRelation: 'users'; - referencedColumns: ['id']; - } - ]; - }; - users: { - Row: { - id: string; - name: string | null; - }; - Insert: { - id: string; - name?: string | null; - }; - Update: { - id?: string; - name?: string | null; - }; - Relationships: [ - { - foreignKeyName: 'users_id_fkey'; - columns: ['id']; - isOneToOne: true; - referencedRelation: 'users'; - referencedColumns: ['id']; - } - ]; - }; - }; - Views: { - [_ in never]: never; - }; - Functions: { - [_ in never]: never; - }; - Enums: { - pricing_plan_interval: 'day' | 'week' | 'month' | 'year'; - pricing_type: 'one_time' | 'recurring'; - subscription_status: - | 'trialing' - | 'active' - | 'canceled' - | 'incomplete' - | 'incomplete_expired' - | 'past_due' - | 'unpaid' - | 'paused'; - }; - CompositeTypes: { - [_ 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; - }; - 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; - }; - 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: []; - }; - migrations: { - Row: { - executed_at: string | null; - hash: string; - id: number; - name: string; - }; - Insert: { - executed_at?: string | null; - hash: string; - id: number; - name: string; - }; - Update: { - 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; - }; - 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; - }; - 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; - }; - Relationships: [ - { - foreignKeyName: 'objects_bucketId_fkey'; - columns: ['bucket_id']; - isOneToOne: false; - referencedRelation: 'buckets'; - referencedColumns: ['id']; - } - ]; - }; - s3_multipart_uploads: { - Row: { - bucket_id: string; - created_at: string; - id: string; - in_progress_size: number; - key: string; - owner_id: string | null; - upload_signature: string; - version: string; - }; - Insert: { - bucket_id: string; - created_at?: string; - id: string; - in_progress_size?: number; - key: string; - owner_id?: string | null; - upload_signature: string; - version: string; - }; - Update: { - bucket_id?: string; - created_at?: string; - id?: string; - in_progress_size?: number; - key?: string; - owner_id?: string | null; - upload_signature?: string; - version?: string; - }; - Relationships: [ - { - foreignKeyName: 's3_multipart_uploads_bucket_id_fkey'; - columns: ['bucket_id']; - isOneToOne: false; - referencedRelation: 'buckets'; - referencedColumns: ['id']; - } - ]; - }; - s3_multipart_uploads_parts: { - Row: { - bucket_id: string; - created_at: string; - etag: string; - id: string; - key: string; - owner_id: string | null; - part_number: number; - size: number; - upload_id: string; - version: string; - }; - Insert: { - bucket_id: string; - created_at?: string; - etag: string; - id?: string; - key: string; - owner_id?: string | null; - part_number: number; - size?: number; - upload_id: string; - version: string; - }; - Update: { - bucket_id?: string; - created_at?: string; - etag?: string; - id?: string; - key?: string; - owner_id?: string | null; - part_number?: number; - size?: number; - upload_id?: string; - version?: string; - }; - Relationships: [ - { - foreignKeyName: 's3_multipart_uploads_parts_bucket_id_fkey'; - columns: ['bucket_id']; - isOneToOne: false; - referencedRelation: 'buckets'; - referencedColumns: ['id']; - }, - { - foreignKeyName: 's3_multipart_uploads_parts_upload_id_fkey'; - columns: ['upload_id']; - isOneToOne: false; - referencedRelation: 's3_multipart_uploads'; - referencedColumns: ['id']; - } - ]; - }; - }; - Views: { - [_ in never]: never; - }; - Functions: { - can_insert_object: { - Args: { - bucketid: string; - name: string; - owner: string; - metadata: Json; - }; - Returns: undefined; - }; - extension: { - Args: { - name: string; - }; - Returns: string; - }; - filename: { - Args: { - name: string; - }; - Returns: string; - }; - foldername: { - Args: { - name: string; - }; - Returns: string[]; - }; - get_size_by_bucket: { - Args: Record; - Returns: { - size: number; - bucket_id: string; - }[]; - }; - list_multipart_uploads_with_delimiter: { - Args: { - bucket_id: string; - prefix_param: string; - delimiter_param: string; - max_keys?: number; - next_key_token?: string; - next_upload_token?: string; - }; - Returns: { - key: string; - id: string; - created_at: string; - }[]; - }; - list_objects_with_delimiter: { - Args: { - bucket_id: string; - prefix_param: string; - delimiter_param: string; - max_keys?: number; - start_after?: string; - next_token?: string; - }; - Returns: { - name: string; - id: string; - metadata: Json; - updated_at: string; - }[]; - }; - operation: { - Args: Record; - Returns: string; - }; - search: { - Args: { - 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; - }[]; - }; - }; - Enums: { - [_ in never]: never; - }; - CompositeTypes: { - [_ in never]: never; - }; - }; -}; + graphql_public: { + Tables: { + [_ in never]: never + } + Views: { + [_ in never]: never + } + Functions: { + graphql: { + Args: { + operationName?: string + query?: string + variables?: Json + extensions?: Json + } + Returns: Json + } + } + Enums: { + [_ in never]: never + } + CompositeTypes: { + [_ in never]: never + } + } + public: { + Tables: { + customers: { + Row: { + id: string + stripe_customer_id: string | null + } + Insert: { + id: string + stripe_customer_id?: string | null + } + Update: { + id?: string + stripe_customer_id?: string | null + } + Relationships: [ + { + foreignKeyName: "customers_id_fkey" + columns: ["id"] + isOneToOne: true + referencedRelation: "users" + referencedColumns: ["id"] + }, + ] + } + prices: { + Row: { + active: boolean | null + currency: string | null + description: string | null + id: string + interval: Database["public"]["Enums"]["pricing_plan_interval"] | null + interval_count: number | null + metadata: Json | null + product_id: string + trial_period_days: number | null + type: Database["public"]["Enums"]["pricing_type"] | null + unit_amount: number | null + } + Insert: { + active?: boolean | null + currency?: string | null + description?: string | null + id: string + interval?: Database["public"]["Enums"]["pricing_plan_interval"] | null + interval_count?: number | null + metadata?: Json | null + product_id: string + trial_period_days?: number | null + type?: Database["public"]["Enums"]["pricing_type"] | null + unit_amount?: number | null + } + Update: { + active?: boolean | null + currency?: string | null + description?: string | null + id?: string + interval?: Database["public"]["Enums"]["pricing_plan_interval"] | null + interval_count?: number | null + metadata?: Json | null + product_id?: string + trial_period_days?: number | null + type?: Database["public"]["Enums"]["pricing_type"] | null + unit_amount?: number | null + } + Relationships: [ + { + foreignKeyName: "prices_product_id_fkey" + columns: ["product_id"] + isOneToOne: false + referencedRelation: "products" + referencedColumns: ["id"] + }, + ] + } + products: { + Row: { + active: boolean | null + created_at: string + description: string | null + features: string[] | null + id: string + images: string[] | null + metadata: Json | null + name: string | null + updated_at: string + } + Insert: { + active?: boolean | null + created_at?: string + description?: string | null + features?: string[] | null + id: string + images?: string[] | null + metadata?: Json | null + name?: string | null + updated_at?: string + } + Update: { + active?: boolean | null + created_at?: string + description?: string | null + features?: string[] | null + id?: string + images?: string[] | null + metadata?: Json | null + name?: string | null + updated_at?: string + } + Relationships: [] + } + purchases: { + Row: { + id: string + price_id: string + product_id: string + purchased_at: string + user_id: string + } + Insert: { + id?: string + price_id: string + product_id: string + purchased_at?: string + user_id: string + } + Update: { + id?: string + price_id?: string + product_id?: string + purchased_at?: string + user_id?: string + } + Relationships: [ + { + foreignKeyName: "purchases_price_id_fkey" + columns: ["price_id"] + isOneToOne: false + referencedRelation: "prices" + referencedColumns: ["id"] + }, + { + foreignKeyName: "purchases_product_id_fkey" + columns: ["product_id"] + isOneToOne: false + referencedRelation: "products" + referencedColumns: ["id"] + }, + { + foreignKeyName: "purchases_user_id_fkey" + columns: ["user_id"] + isOneToOne: false + referencedRelation: "users" + referencedColumns: ["id"] + }, + ] + } + subscriptions: { + Row: { + cancel_at: string | null + cancel_at_period_end: boolean | null + canceled_at: string | null + created: string + current_period_end: string + current_period_start: string + ended_at: string | null + id: string + metadata: Json | null + price_id: string | null + quantity: number | null + status: Database["public"]["Enums"]["subscription_status"] | null + trial_end: string | null + trial_start: string | null + user_id: string + } + Insert: { + cancel_at?: string | null + cancel_at_period_end?: boolean | null + canceled_at?: string | null + created?: string + current_period_end?: string + current_period_start?: string + ended_at?: string | null + id: string + metadata?: Json | null + price_id?: string | null + quantity?: number | null + status?: Database["public"]["Enums"]["subscription_status"] | null + trial_end?: string | null + trial_start?: string | null + user_id: string + } + Update: { + cancel_at?: string | null + cancel_at_period_end?: boolean | null + canceled_at?: string | null + created?: string + current_period_end?: string + current_period_start?: string + ended_at?: string | null + id?: string + metadata?: Json | null + price_id?: string | null + quantity?: number | null + status?: Database["public"]["Enums"]["subscription_status"] | null + trial_end?: string | null + trial_start?: string | null + user_id?: string + } + Relationships: [ + { + foreignKeyName: "subscriptions_price_id_fkey" + columns: ["price_id"] + isOneToOne: false + referencedRelation: "prices" + referencedColumns: ["id"] + }, + { + foreignKeyName: "subscriptions_user_id_fkey" + columns: ["user_id"] + isOneToOne: false + referencedRelation: "users" + referencedColumns: ["id"] + }, + ] + } + users: { + Row: { + id: string + name: string | null + } + Insert: { + id: string + name?: string | null + } + Update: { + id?: string + name?: string | null + } + Relationships: [ + { + foreignKeyName: "users_id_fkey" + columns: ["id"] + isOneToOne: true + referencedRelation: "users" + referencedColumns: ["id"] + }, + ] + } + } + Views: { + [_ in never]: never + } + Functions: { + [_ in never]: never + } + Enums: { + pricing_plan_interval: "day" | "week" | "month" | "year" + pricing_type: "one_time" | "recurring" + subscription_status: + | "trialing" + | "active" + | "canceled" + | "incomplete" + | "incomplete_expired" + | "past_due" + | "unpaid" + | "paused" + } + CompositeTypes: { + [_ in never]: never + } + } +} -type PublicSchema = Database[Extract]; +type PublicSchema = Database[Extract] export type Tables< - PublicTableNameOrOptions extends - | keyof (PublicSchema['Tables'] & PublicSchema['Views']) - | { schema: keyof Database }, - TableName extends PublicTableNameOrOptions extends { schema: keyof Database } - ? keyof (Database[PublicTableNameOrOptions['schema']]['Tables'] & - Database[PublicTableNameOrOptions['schema']]['Views']) - : never = never + PublicTableNameOrOptions extends + | keyof (PublicSchema["Tables"] & PublicSchema["Views"]) + | { schema: keyof Database }, + TableName extends PublicTableNameOrOptions extends { schema: keyof Database } + ? 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; - } - ? R - : never - : PublicTableNameOrOptions extends keyof (PublicSchema['Tables'] & PublicSchema['Views']) - ? (PublicSchema['Tables'] & PublicSchema['Views'])[PublicTableNameOrOptions] extends { - Row: infer R; - } - ? R - : never - : never; + ? (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 + } + ? R + : never + : never export type TablesInsert< - PublicTableNameOrOptions extends keyof PublicSchema['Tables'] | { schema: keyof Database }, - TableName extends PublicTableNameOrOptions extends { schema: keyof Database } - ? keyof Database[PublicTableNameOrOptions['schema']]['Tables'] - : never = never + PublicTableNameOrOptions extends + | keyof PublicSchema["Tables"] + | { schema: keyof Database }, + TableName extends PublicTableNameOrOptions extends { schema: keyof Database } + ? keyof Database[PublicTableNameOrOptions["schema"]]["Tables"] + : never = never, > = PublicTableNameOrOptions extends { schema: keyof Database } - ? Database[PublicTableNameOrOptions['schema']]['Tables'][TableName] extends { - Insert: infer I; - } - ? I - : never - : PublicTableNameOrOptions extends keyof PublicSchema['Tables'] - ? PublicSchema['Tables'][PublicTableNameOrOptions] extends { - Insert: infer I; - } - ? I - : never - : never; + ? Database[PublicTableNameOrOptions["schema"]]["Tables"][TableName] extends { + Insert: infer I + } + ? I + : never + : PublicTableNameOrOptions extends keyof PublicSchema["Tables"] + ? PublicSchema["Tables"][PublicTableNameOrOptions] extends { + Insert: infer I + } + ? I + : never + : never export type TablesUpdate< - PublicTableNameOrOptions extends keyof PublicSchema['Tables'] | { schema: keyof Database }, - TableName extends PublicTableNameOrOptions extends { schema: keyof Database } - ? keyof Database[PublicTableNameOrOptions['schema']]['Tables'] - : never = never + PublicTableNameOrOptions extends + | keyof PublicSchema["Tables"] + | { schema: keyof Database }, + TableName extends PublicTableNameOrOptions extends { schema: keyof Database } + ? keyof Database[PublicTableNameOrOptions["schema"]]["Tables"] + : never = never, > = PublicTableNameOrOptions extends { schema: keyof Database } - ? Database[PublicTableNameOrOptions['schema']]['Tables'][TableName] extends { - Update: infer U; - } - ? U - : never - : PublicTableNameOrOptions extends keyof PublicSchema['Tables'] - ? PublicSchema['Tables'][PublicTableNameOrOptions] extends { - Update: infer U; - } - ? U - : never - : never; + ? Database[PublicTableNameOrOptions["schema"]]["Tables"][TableName] extends { + Update: infer U + } + ? U + : never + : PublicTableNameOrOptions extends keyof PublicSchema["Tables"] + ? PublicSchema["Tables"][PublicTableNameOrOptions] extends { + Update: infer U + } + ? U + : never + : never export type Enums< - PublicEnumNameOrOptions extends keyof PublicSchema['Enums'] | { schema: keyof Database }, - EnumName extends PublicEnumNameOrOptions extends { schema: keyof Database } - ? keyof Database[PublicEnumNameOrOptions['schema']]['Enums'] - : never = never + PublicEnumNameOrOptions extends + | keyof PublicSchema["Enums"] + | { schema: keyof Database }, + EnumName extends PublicEnumNameOrOptions extends { schema: keyof Database } + ? 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/src/lib/utils/supabase/admin.ts b/src/lib/utils/supabase/admin.ts index 02294db..1e0bac9 100644 --- a/src/lib/utils/supabase/admin.ts +++ b/src/lib/utils/supabase/admin.ts @@ -11,7 +11,6 @@ const toDateTime = (secs: number) => { return t; }; - type Product = Tables<'products'>; type Price = Tables<'prices'>; @@ -245,11 +244,32 @@ const manageSubscriptionStatusChange = async ( // } }; +const recordProductPurchase = async (userId: string, productId: string, priceId: string) => { + console.log( + `Recording product purchase for user [${userId}] and product [${productId}] with price [${priceId}]` + ); + + try { + const { error } = await supabaseAdmin + .from('purchases') + .insert([{ user_id: userId, product_id: productId, price_id: priceId }]); + + if (error) { + throw new Error(`Failed to record product purchase: ${error.message}`); + } + } catch (error) { + console.error(error); + } + + console.log(`Product purchased recorded for user [${userId}] and product [${productId}]`); +}; + export { upsertProductRecord, upsertPriceRecord, deleteProductRecord, deletePriceRecord, createOrRetrieveCustomer, - manageSubscriptionStatusChange + manageSubscriptionStatusChange, + recordProductPurchase }; diff --git a/src/routes/api/webhook/stripe/+server.ts b/src/routes/api/webhook/stripe/+server.ts index 0e8f3a6..b05992d 100644 --- a/src/routes/api/webhook/stripe/+server.ts +++ b/src/routes/api/webhook/stripe/+server.ts @@ -6,7 +6,8 @@ import { deleteProductRecord, manageSubscriptionStatusChange, upsertPriceRecord, - upsertProductRecord + upsertProductRecord, + recordProductPurchase } from '$lib/utils/supabase/admin'; import { stripe as stripeClient } from '$lib/utils/stripe'; @@ -34,7 +35,6 @@ export const POST: RequestHandler = async ({ request }) => { } switch (event.type) { - // Product events case 'product.created': case 'product.updated': await upsertProductRecord(event.data.object as stripe.Product); @@ -64,6 +64,7 @@ export const POST: RequestHandler = async ({ request }) => { } case 'checkout.session.completed': { const checkoutSession = event.data.object as stripe.Checkout.Session; + if (checkoutSession.mode === 'subscription') { const subscriptionId = checkoutSession.subscription; await manageSubscriptionStatusChange( @@ -72,8 +73,33 @@ export const POST: RequestHandler = async ({ request }) => { true ); } else if (checkoutSession.mode === 'payment') { - // TODO Record that they purchased the product - console.log(checkoutSession); + // Fetch session with expanded line_items + const sessionWithLineItems = await stripeClient.checkout.sessions.retrieve( + checkoutSession.id, + { + expand: ['line_items'] + } + ); + + const lineItems = sessionWithLineItems.line_items?.data; + const userId = checkoutSession.metadata!.userId; + + if (!lineItems) { + throw new Error('No line items found in session'); + } + + if (!userId) { + throw new Error('No user ID found in session metadata'); + } + + for (const item of lineItems ?? []) { + const productId = item.price?.product as string; + const priceId = item.price?.id as string; + + if (productId && priceId) { + await recordProductPurchase(userId, productId, priceId); + } + } } break; } diff --git a/supabase/migrations/20240704190621_initial_migration.sql b/supabase/migrations/20240704190621_initial_migration.sql index 0166863..6da61f2 100644 --- a/supabase/migrations/20240704190621_initial_migration.sql +++ b/supabase/migrations/20240704190621_initial_migration.sql @@ -135,3 +135,19 @@ create table subscriptions ( ); alter table subscriptions enable row level security; create policy "Can only view own subs data." on subscriptions for select using (auth.uid() = user_id); + +-- Purchases table to record product purchases. +create table purchases ( + id uuid default uuid_generate_v4() primary key, + user_id uuid references auth.users not null, + product_id text references products(id) not null, + price_id text references prices(id) not null, + purchased_at timestamp with time zone not null default now() +); + +-- Enabling row-level security for purchases. +alter table purchases enable row level security; + +-- Creating a policy to ensure that users can only view their own purchase data. +create policy "Can view own purchase data." on purchases for select using (auth.uid() = user_id); +create policy "Can insert own purchase data." on purchases for insert with check (auth.uid() = user_id);