From e569613c7c7e79c1c2df7f7967b7b36d26acd345 Mon Sep 17 00:00:00 2001 From: Altay Date: Sat, 16 Dec 2023 15:17:17 +0200 Subject: [PATCH] feat: display orders in the `credits` page (#78) https://github.com/experiment-station/beecast/assets/9790196/5669b227-7efe-4916-9d05-14c505f23f47 --- app/api/webhooks/stripe/route.ts | 2 +- app/credits/components/credit-list-item.tsx | 19 +++++++-- app/credits/components/order-list-item.tsx | 46 ++++++++++++++++++++ app/credits/page.tsx | 36 +++++++++++++--- app/credits/utils/get-currency-symbol.ts | 14 ++++++ lib/services/stripe/{order.ts => orders.ts} | 35 ++++++++++++--- types/supabase/database.ts | 47 +++++++++++++++++++++ 7 files changed, 183 insertions(+), 16 deletions(-) create mode 100644 app/credits/components/order-list-item.tsx create mode 100644 app/credits/utils/get-currency-symbol.ts rename lib/services/stripe/{order.ts => orders.ts} (57%) diff --git a/app/api/webhooks/stripe/route.ts b/app/api/webhooks/stripe/route.ts index c9a0b6b..b47669e 100644 --- a/app/api/webhooks/stripe/route.ts +++ b/app/api/webhooks/stripe/route.ts @@ -4,7 +4,7 @@ import type Stripe from 'stripe'; import { env } from '@/env.mjs'; import { HttpBadRequestError, HttpInternalServerError } from '@/lib/errors'; import { stripe } from '@/lib/services/stripe/client'; -import { fulfillOrder } from '@/lib/services/stripe/order'; +import { fulfillOrder } from '@/lib/services/stripe/orders'; export async function POST(req: NextRequest) { const signature = req.headers.get('stripe-signature'); diff --git a/app/credits/components/credit-list-item.tsx b/app/credits/components/credit-list-item.tsx index 7330c1e..2236d6c 100644 --- a/app/credits/components/credit-list-item.tsx +++ b/app/credits/components/credit-list-item.tsx @@ -4,8 +4,11 @@ import { createCheckoutSession } from '@/lib/services/stripe/checkout'; import { Badge, Button, Flex, Text } from '@radix-ui/themes'; import { atom, useAtom } from 'jotai'; +import { getCurrencySymbol } from '../utils/get-currency-symbol'; + type Props = { amount: number; + currency: string; id: string; popular?: boolean; quantity: number; @@ -17,7 +20,16 @@ const checkoutStatusAtom = atom< type: 'idle', }); -export function CreditListItem({ amount, id, popular, quantity }: Props) { +export function CreditListItem({ + amount, + currency, + id, + popular, + quantity, +}: Props) { + const fixedAmount = amount / 100; + const symbolizedCurrency = getCurrencySymbol(currency); + const [checkoutStatus, setCheckoutStatus] = useAtom(checkoutStatusAtom); const handleClick = async () => { @@ -52,7 +64,8 @@ export function CreditListItem({ amount, id, popular, quantity }: Props) { - ${(amount / quantity).toFixed(2)} per episode summarization. + {symbolizedCurrency} + {(fixedAmount / quantity).toFixed(2)} per episode summarization. @@ -66,7 +79,7 @@ export function CreditListItem({ amount, id, popular, quantity }: Props) { > {checkoutStatus.type === 'loading' && checkoutStatus.id === id ? 'Loading...' - : `$${amount}`} + : `${symbolizedCurrency}${fixedAmount}`} ); diff --git a/app/credits/components/order-list-item.tsx b/app/credits/components/order-list-item.tsx new file mode 100644 index 0000000..2912694 --- /dev/null +++ b/app/credits/components/order-list-item.tsx @@ -0,0 +1,46 @@ +import type { Tables } from '@/types/supabase/database'; + +import { Button, Flex, Text } from '@radix-ui/themes'; +import { format } from 'date-fns'; +import Link from 'next/link'; + +import { getCurrencySymbol } from '../utils/get-currency-symbol'; + +type Props = Tables<'order'>; + +export function OrderListItem(props: Props) { + const fixedAmount = props.amount / 100; + const symbolizedCurrency = getCurrencySymbol(props.currency); + + return ( + + + + + {props.credits} credits for{' '} + + {symbolizedCurrency} + {fixedAmount} + + + + + + {format(new Date(props.created_at), 'HH:mm - MMM d, yyyy')} + + + + + + ); +} diff --git a/app/credits/page.tsx b/app/credits/page.tsx index 9861758..394e440 100644 --- a/app/credits/page.tsx +++ b/app/credits/page.tsx @@ -1,5 +1,6 @@ import { fetchAccountAICredits } from '@/lib/services/account'; import { getPrices } from '@/lib/services/stripe/prices'; +import { createSupabaseServerClient } from '@/lib/services/supabase/server'; import { CalloutIcon, CalloutRoot, @@ -11,9 +12,11 @@ import { Text, } from '@radix-ui/themes'; import { Provider } from 'jotai'; +import { cookies } from 'next/headers'; import { FaCircleCheck } from 'react-icons/fa6'; import { CreditListItem } from './components/credit-list-item'; +import { OrderListItem } from './components/order-list-item'; type Props = { searchParams: { @@ -22,16 +25,16 @@ type Props = { }; export default async function Page(props: Props) { + const supabase = createSupabaseServerClient(cookies()); const credits = await fetchAccountAICredits(); const prices = await getPrices(); + const ordersQuery = await supabase + .from('order') + .select('*') + .order('created_at', { ascending: false }); return ( - + {props.searchParams.status === 'success' ? ( @@ -61,7 +64,8 @@ export default async function Page(props: Props) { {prices.map((price, index) => ( + + {(ordersQuery.data || []).length > 0 ? ( + + + + Orders + + + + + + {(ordersQuery.data || []).map((order) => ( + + ))} + + + + ) : null} ); } diff --git a/app/credits/utils/get-currency-symbol.ts b/app/credits/utils/get-currency-symbol.ts new file mode 100644 index 0000000..8a0bd60 --- /dev/null +++ b/app/credits/utils/get-currency-symbol.ts @@ -0,0 +1,14 @@ +export function getCurrencySymbol(currency: string): string { + const normalizedCurrency = currency.toUpperCase(); + + switch (normalizedCurrency) { + case 'USD': + return '$'; + case 'EUR': + return '€'; + case 'GBP': + return '£'; + default: + return normalizedCurrency; + } +} diff --git a/lib/services/stripe/order.ts b/lib/services/stripe/orders.ts similarity index 57% rename from lib/services/stripe/order.ts rename to lib/services/stripe/orders.ts index 339054a..336bba1 100644 --- a/lib/services/stripe/order.ts +++ b/lib/services/stripe/orders.ts @@ -4,8 +4,11 @@ import { DatabaseError } from '@/lib/errors'; import { z } from 'zod'; import { createSupabaseServiceClient } from '../supabase/service'; +import { stripe } from './client'; const checkoutSessionSchema = z.object({ + amount_total: z.number(), + currency: z.string(), id: z.string(), line_items: z.object({ data: z.array( @@ -22,9 +25,10 @@ const checkoutSessionSchema = z.object({ accountId: z.string().min(1).transform(Number), userId: z.string().min(1), }), + payment_intent: z.string(), }); -export const fulfillOrder = async (session: Stripe.Checkout.Session) => { +export async function fulfillOrder(session: Stripe.Checkout.Session) { const validatedSession = checkoutSessionSchema.safeParse(session); if (!validatedSession.success) { @@ -43,17 +47,38 @@ export const fulfillOrder = async (session: Stripe.Checkout.Session) => { throw new DatabaseError(currentAccountCreditsQuery.error); } + const purchasedCredits = + validatedSession.data.line_items.data[0].price.transform_quantity.divide_by; + + const { data: charges } = await stripe.charges.list({ + payment_intent: validatedSession.data.payment_intent, + }); + + const charge = charges[0]; + + const createOrderQuery = await supabase.from('order').insert({ + account: validatedSession.data.metadata.accountId, + amount: validatedSession.data.amount_total, + credits: purchasedCredits, + currency: validatedSession.data.currency, + invoice_url: charge.receipt_url ?? '', + reference_id: validatedSession.data.id, + status: 'paid', + }); + + if (createOrderQuery.error) { + throw new DatabaseError(createOrderQuery.error); + } + const updateAccountQuery = await supabase .from('account') .update({ ai_credit: - (currentAccountCreditsQuery.data.ai_credit ?? 0) + - validatedSession.data.line_items.data[0].price.transform_quantity - .divide_by, + (currentAccountCreditsQuery.data.ai_credit ?? 0) + purchasedCredits, }) .eq('id', validatedSession.data.metadata.accountId); if (updateAccountQuery.error) { throw new DatabaseError(updateAccountQuery.error); } -}; +} diff --git a/types/supabase/database.ts b/types/supabase/database.ts index 5c04438..e411d47 100644 --- a/types/supabase/database.ts +++ b/types/supabase/database.ts @@ -240,18 +240,21 @@ export interface Database { Row: { created_at: string; id: number; + podcast_index_guid: string; show: number; spotify_id: string; }; Insert: { created_at?: string; id?: number; + podcast_index_guid: string; show: number; spotify_id: string; }; Update: { created_at?: string; id?: number; + podcast_index_guid?: string; show?: number; spotify_id?: string; }; @@ -265,6 +268,50 @@ export interface Database { }, ]; }; + order: { + Row: { + account: number; + amount: number; + created_at: string; + credits: number; + currency: string; + id: number; + invoice_url: string; + reference_id: string; + status: string; + }; + Insert: { + account: number; + amount: number; + created_at?: string; + credits: number; + currency: string; + id?: number; + invoice_url?: string; + reference_id: string; + status: string; + }; + Update: { + account?: number; + amount?: number; + created_at?: string; + credits?: number; + currency?: string; + id?: number; + invoice_url?: string; + reference_id?: string; + status?: string; + }; + Relationships: [ + { + foreignKeyName: 'order_account_fkey'; + columns: ['account']; + isOneToOne: false; + referencedRelation: 'account'; + referencedColumns: ['id']; + }, + ]; + }; show: { Row: { created_at: string;