feat(#29): Add ability to record that a user has purchased a product - part 2

This commit is contained in:
Josh Creek
2024-09-21 20:26:36 +01:00
parent d4f7f5041d
commit 05891ad510
5 changed files with 448 additions and 634 deletions
+4 -4
View File
@@ -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
+300 -548
View File
@@ -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;
};
[_ 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: {
customers: {
Row: {
id: string;
stripe_customer_id: string | null;
};
id: string
stripe_customer_id: string | null
}
Insert: {
id: string;
stripe_customer_id?: string | null;
};
id: string
stripe_customer_id?: string | null
}
Update: {
id?: string;
stripe_customer_id?: string | null;
};
id?: string
stripe_customer_id?: string | null
}
Relationships: [
{
foreignKeyName: 'customers_id_fkey';
columns: ['id'];
isOneToOne: true;
referencedRelation: 'users';
referencedColumns: ['id'];
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;
};
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;
};
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;
};
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'];
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;
};
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;
};
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: [];
};
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;
};
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;
};
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;
};
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_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'];
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'];
id: string
name: string | null
}
];
};
};
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'];
id: string
name?: string | null
}
];
};
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'];
id?: string
name?: string | null
}
];
};
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: "users_id_fkey"
columns: ["id"]
isOneToOne: true
referencedRelation: "users"
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;
};
[_ 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<PropertyKey, never>;
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<PropertyKey, never>;
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;
}[];
};
};
[_ in never]: never
}
Enums: {
[_ in never]: never;
};
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;
};
};
};
[_ in never]: never
}
}
}
type PublicSchema = Database[Extract<keyof Database, 'public'>];
type PublicSchema = Database[Extract<keyof Database, "public">]
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'])
: never = never
? 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'] | { schema: keyof Database },
PublicTableNameOrOptions extends
| keyof PublicSchema["Tables"]
| { schema: keyof Database },
TableName extends PublicTableNameOrOptions extends { schema: keyof Database }
? keyof Database[PublicTableNameOrOptions['schema']]['Tables']
: never = never
? 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'] | { schema: keyof Database },
PublicTableNameOrOptions extends
| keyof PublicSchema["Tables"]
| { schema: keyof Database },
TableName extends PublicTableNameOrOptions extends { schema: keyof Database }
? keyof Database[PublicTableNameOrOptions['schema']]['Tables']
: never = never
? 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'] | { schema: keyof Database },
PublicEnumNameOrOptions extends
| keyof PublicSchema["Enums"]
| { schema: keyof Database },
EnumName extends PublicEnumNameOrOptions extends { schema: keyof Database }
? keyof Database[PublicEnumNameOrOptions['schema']]['Enums']
: never = never
? 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
+22 -2
View File
@@ -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
};
+30 -4
View File
@@ -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;
}
@@ -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);