Update billing system to support single and recurring payments

This update modifies the billing system to properly handle both single and recurring payment plans. Logic is introduced to determine whether the selected plan is recurring or a one-time payment and adjust the interface accordingly. The naming of some components and variables has been changed to more accurately reflect their purpose. Additionally, a
This commit is contained in:
giancarlo
2024-04-01 20:58:26 +08:00
parent 6b72206b00
commit 84a4b45bcd
22 changed files with 291 additions and 119 deletions

View File

@@ -25,7 +25,10 @@ export function PersonalAccountCheckoutForm(props: {
}) { }) {
const [pending, startTransition] = useTransition(); const [pending, startTransition] = useTransition();
const [error, setError] = useState(false); const [error, setError] = useState(false);
const [checkoutToken, setCheckoutToken] = useState<string>();
const [checkoutToken, setCheckoutToken] = useState<string | undefined>(
undefined,
);
// only allow trial if the user is not already a customer // only allow trial if the user is not already a customer
const canStartTrial = !props.customerId; const canStartTrial = !props.customerId;
@@ -36,6 +39,7 @@ export function PersonalAccountCheckoutForm(props: {
<EmbeddedCheckout <EmbeddedCheckout
checkoutToken={checkoutToken} checkoutToken={checkoutToken}
provider={billingConfig.provider} provider={billingConfig.provider}
onClose={() => setCheckoutToken(undefined)}
/> />
); );
} }

View File

@@ -2,7 +2,7 @@ import { SupabaseClient } from '@supabase/supabase-js';
import { import {
BillingPortalCard, BillingPortalCard,
CurrentPlanCard, CurrentSubscriptionCard,
} from '@kit/billing-gateway/components'; } from '@kit/billing-gateway/components';
import { Database } from '@kit/supabase/database'; import { Database } from '@kit/supabase/database';
import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client'; import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client';
@@ -51,9 +51,9 @@ async function PersonalAccountBillingPage() {
<If condition={subscription}> <If condition={subscription}>
{(subscription) => ( {(subscription) => (
<div <div
className={'mx-auto flex w-full max-w-2xl flex-col space-y-4'} className={'mx-auto flex w-full max-w-2xl flex-col space-y-6'}
> >
<CurrentPlanCard <CurrentSubscriptionCard
subscription={subscription} subscription={subscription}
config={billingConfig} config={billingConfig}
/> />

View File

@@ -24,7 +24,10 @@ export function TeamAccountCheckoutForm(params: {
}) { }) {
const routeParams = useParams(); const routeParams = useParams();
const [pending, startTransition] = useTransition(); const [pending, startTransition] = useTransition();
const [checkoutToken, setCheckoutToken] = useState<string | null>(null);
const [checkoutToken, setCheckoutToken] = useState<string | undefined>(
undefined,
);
// If the checkout token is set, render the embedded checkout component // If the checkout token is set, render the embedded checkout component
if (checkoutToken) { if (checkoutToken) {
@@ -32,6 +35,7 @@ export function TeamAccountCheckoutForm(params: {
<EmbeddedCheckout <EmbeddedCheckout
checkoutToken={checkoutToken} checkoutToken={checkoutToken}
provider={billingConfig.provider} provider={billingConfig.provider}
onClose={() => setCheckoutToken(undefined)}
/> />
); );
} }

View File

@@ -1,6 +1,6 @@
import { import {
BillingPortalCard, BillingPortalCard,
CurrentPlanCard, CurrentSubscriptionCard,
} from '@kit/billing-gateway/components'; } from '@kit/billing-gateway/components';
import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client'; import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client';
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert'; import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
@@ -53,7 +53,7 @@ async function TeamAccountBillingPage({ params }: Params) {
</If> </If>
<div> <div>
<div className={'flex flex-col space-y-2'}> <div className={'flex flex-col space-y-6'}>
<If <If
condition={subscription} condition={subscription}
fallback={ fallback={
@@ -66,7 +66,10 @@ async function TeamAccountBillingPage({ params }: Params) {
} }
> >
{(data) => ( {(data) => (
<CurrentPlanCard subscription={data} config={billingConfig} /> <CurrentSubscriptionCard
subscription={data}
config={billingConfig}
/>
)} )}
</If> </If>

View File

@@ -114,7 +114,7 @@ async function TeamAccountMembersPage({ params }: Params) {
<PageBody> <PageBody>
<div <div
className={'mx-auto flex w-full max-w-3xl flex-col space-y-4 pb-32'} className={'mx-auto flex w-full max-w-3xl flex-col space-y-6 pb-32'}
> >
<Card> <Card>
<CardHeader className={'flex flex-row justify-between'}> <CardHeader className={'flex flex-row justify-between'}>

View File

@@ -7,6 +7,29 @@ const provider = BillingProviderSchema.parse(
export default createBillingSchema({ export default createBillingSchema({
provider, provider,
products: [ products: [
{
id: 'lifetime',
name: 'Lifetime',
description: 'The perfect plan for a lifetime',
currency: 'USD',
features: ['Feature 1', 'Feature 2', 'Feature 3'],
plans: [
{
name: 'Lifetime',
id: 'lifetime',
paymentType: 'one-time',
lineItems: [
{
id: 'price_1P0jgcI1i3VnbZTqXVXaZkMP',
name: 'Base',
description: 'Base plan',
cost: 999.99,
type: 'base',
},
],
},
],
},
{ {
id: 'starter', id: 'starter',
name: 'Starter', name: 'Starter',

View File

@@ -20,12 +20,13 @@
"cannotManageBillingAlertDescription": "You do not have permissions to manage billing. Please contact your organization owner.", "cannotManageBillingAlertDescription": "You do not have permissions to manage billing. Please contact your organization owner.",
"manageTeamPlan": "Manage your Team Plan", "manageTeamPlan": "Manage your Team Plan",
"manageTeamPlanDescription": "Choose a plan that fits your team's needs. You can upgrade or downgrade your plan at any time.", "manageTeamPlanDescription": "Choose a plan that fits your team's needs. You can upgrade or downgrade your plan at any time.",
"flatSubscription": "Flat Subscription", "basePlan": "Base Plan",
"billingInterval": { "billingInterval": {
"label": "Choose your billing interval", "label": "Choose your billing interval",
"month": "Billed monthly", "month": "Billed monthly",
"year": "Billed yearly" "year": "Billed yearly"
}, },
"lifetime": "Lifetime",
"trialPeriod": "{{period}} day trial", "trialPeriod": "{{period}} day trial",
"perPeriod": "per {{period}}", "perPeriod": "per {{period}}",
"processing": "Processing...", "processing": "Processing...",
@@ -85,6 +86,21 @@
"badge": "Paused", "badge": "Paused",
"heading": "Your subscription is paused", "heading": "Your subscription is paused",
"description": "Your subscription is paused. You can resume it at any time." "description": "Your subscription is paused. You can resume it at any time."
},
"succeeded": {
"badge": "Succeeded",
"heading": "Your payment was successful",
"description": "Your payment was successful. Thank you for subscribing!"
},
"pending": {
"badge": "Pending",
"heading": "Your payment is pending",
"description": "Your payment is pending. Please bear with us."
},
"failed": {
"badge": "Failed",
"heading": "Your payment failed",
"description": "Your payment failed. Please update your payment method."
} }
} }
} }

View File

@@ -0,0 +1,94 @@
import { BadgeCheck } from 'lucide-react';
import { BillingConfig, getProductPlanPairByVariantId } from '@kit/billing';
import { Database } from '@kit/supabase/database';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@kit/ui/card';
import { Trans } from '@kit/ui/trans';
import { CurrentPlanBadge } from './current-plan-badge';
import { LineItemDetails } from './line-item-details';
type Order = Database['public']['Tables']['orders']['Row'];
type LineItem = Database['public']['Tables']['order_items']['Row'];
interface Props {
order: Order & {
items: LineItem[];
};
config: BillingConfig;
}
export function CurrentLifetimeOrderCard({
order,
config,
}: React.PropsWithChildren<Props>) {
const lineItems = order.items;
const firstLineItem = lineItems[0];
if (!firstLineItem) {
throw new Error('No line items found in subscription');
}
const { product, plan } = getProductPlanPairByVariantId(
config,
firstLineItem.variant_id,
);
if (!product || !plan) {
throw new Error(
'Product or plan not found. Did you forget to add it to the billing config?',
);
}
const productLineItems = plan.lineItems;
return (
<Card>
<CardHeader>
<CardTitle>
<Trans i18nKey="billing:planCardTitle" />
</CardTitle>
<CardDescription>
<Trans i18nKey="billing:planCardDescription" />
</CardDescription>
</CardHeader>
<CardContent className={'space-y-3 text-sm'}>
<div className={'flex flex-col space-y-1'}>
<div className={'flex items-center space-x-2 text-lg font-semibold'}>
<BadgeCheck
className={
's-6 fill-green-500 text-white dark:fill-white dark:text-black'
}
/>
<span>{product.name}</span>
<CurrentPlanBadge status={order.status} />
</div>
</div>
<div>
<div className="flex flex-col space-y-0.5">
<span className="font-semibold">
<Trans i18nKey="billing:detailsLabel" />
</span>
<LineItemDetails
lineItems={productLineItems}
currency={order.currency}
/>
</div>
</div>
</CardContent>
</Card>
);
}

View File

@@ -2,9 +2,13 @@ import { Database } from '@kit/supabase/database';
import { Badge } from '@kit/ui/badge'; import { Badge } from '@kit/ui/badge';
import { Trans } from '@kit/ui/trans'; import { Trans } from '@kit/ui/trans';
type Status =
| Database['public']['Enums']['subscription_status']
| Database['public']['Enums']['payment_status'];
export function CurrentPlanBadge( export function CurrentPlanBadge(
props: React.PropsWithoutRef<{ props: React.PropsWithoutRef<{
status: Database['public']['Enums']['subscription_status']; status: Status;
}>, }>,
) { ) {
let variant: 'success' | 'warning' | 'destructive'; let variant: 'success' | 'warning' | 'destructive';
@@ -12,12 +16,14 @@ export function CurrentPlanBadge(
switch (props.status) { switch (props.status) {
case 'active': case 'active':
case 'succeeded':
variant = 'success'; variant = 'success';
break; break;
case 'trialing': case 'trialing':
variant = 'success'; variant = 'success';
break; break;
case 'past_due': case 'past_due':
case 'failed':
variant = 'destructive'; variant = 'destructive';
break; break;
case 'canceled': case 'canceled':
@@ -27,6 +33,7 @@ export function CurrentPlanBadge(
variant = 'destructive'; variant = 'destructive';
break; break;
case 'incomplete': case 'incomplete':
case 'pending':
variant = 'warning'; variant = 'warning';
break; break;
case 'incomplete_expired': case 'incomplete_expired':

View File

@@ -1,14 +1,10 @@
import { formatDate } from 'date-fns'; import { formatDate } from 'date-fns';
import { BadgeCheck, CheckCircle2 } from 'lucide-react'; import { BadgeCheck } from 'lucide-react';
import { BillingConfig, getProductPlanPairByVariantId } from '@kit/billing'; import { BillingConfig, getProductPlanPairByVariantId } from '@kit/billing';
import { Database } from '@kit/supabase/database'; import { Database } from '@kit/supabase/database';
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from '@kit/ui/accordion';
import { import {
Card, Card,
CardContent, CardContent,
@@ -34,7 +30,7 @@ interface Props {
config: BillingConfig; config: BillingConfig;
} }
export function CurrentPlanCard({ export function CurrentSubscriptionCard({
subscription, subscription,
config, config,
}: React.PropsWithChildren<Props>) { }: React.PropsWithChildren<Props>) {

View File

@@ -1,5 +1,6 @@
export * from './plan-picker'; export * from './plan-picker';
export * from './current-plan-card'; export * from './current-subscription-card';
export * from './current-lifetime-order-card';
export * from './embedded-checkout'; export * from './embedded-checkout';
export * from './billing-session-status'; export * from './billing-session-status';
export * from './billing-portal-card'; export * from './billing-portal-card';

View File

@@ -2,13 +2,14 @@ import { z } from 'zod';
import { LineItemSchema } from '@kit/billing'; import { LineItemSchema } from '@kit/billing';
import { formatCurrency } from '@kit/shared/utils'; import { formatCurrency } from '@kit/shared/utils';
import { If } from '@kit/ui/if';
import { Trans } from '@kit/ui/trans'; import { Trans } from '@kit/ui/trans';
export function LineItemDetails( export function LineItemDetails(
props: React.PropsWithChildren<{ props: React.PropsWithChildren<{
lineItems: z.infer<typeof LineItemSchema>[]; lineItems: z.infer<typeof LineItemSchema>[];
currency: string; currency: string;
selectedInterval: string; selectedInterval?: string | undefined;
}>, }>,
) { ) {
return ( return (
@@ -23,15 +24,20 @@ export function LineItemDetails(
> >
<span className={'flex space-x-2'}> <span className={'flex space-x-2'}>
<span> <span>
<Trans i18nKey={'billing:flatSubscription'} /> <Trans i18nKey={'billing:basePlan'} />
</span> </span>
<span>/</span> <span>/</span>
<span> <span>
<Trans <If
i18nKey={`billing:billingInterval.${props.selectedInterval}`} condition={props.selectedInterval}
/> fallback={<Trans i18nKey={'billing:lifetime'} />}
>
<Trans
i18nKey={`billing:billingInterval.${props.selectedInterval}`}
/>
</If>
</span> </span>
</span> </span>

View File

@@ -101,6 +101,10 @@ export function PlanPicker(
const { t } = useTranslation(`billing`); const { t } = useTranslation(`billing`);
// display the period picker if the selected plan is recurring or if no plan is selected
const isRecurringPlan =
selectedPlan?.paymentType === 'recurring' || !selectedPlan;
return ( return (
<Form {...form}> <Form {...form}>
<div <div
@@ -112,67 +116,74 @@ export function PlanPicker(
className={'flex w-full max-w-xl flex-col space-y-4'} className={'flex w-full max-w-xl flex-col space-y-4'}
onSubmit={form.handleSubmit(props.onSubmit)} onSubmit={form.handleSubmit(props.onSubmit)}
> >
<FormField <div
name={'interval'} className={cn('transition-all', {
render={({ field }) => { ['pointer-events-none opacity-50']: !isRecurringPlan,
return ( })}
<FormItem className={'rounded-md border p-4'}> >
<FormLabel htmlFor={'plan-picker-id'}> <FormField
<Trans i18nKey={'common:billingInterval.label'} /> name={'interval'}
</FormLabel> render={({ field }) => {
return (
<FormItem className={'rounded-md border p-4'}>
<FormLabel htmlFor={'plan-picker-id'}>
<Trans i18nKey={'common:billingInterval.label'} />
</FormLabel>
<FormControl id={'plan-picker-id'}> <FormControl id={'plan-picker-id'}>
<RadioGroup name={field.name} value={field.value}> <RadioGroup name={field.name} value={field.value}>
<div className={'flex space-x-2.5'}> <div className={'flex space-x-2.5'}>
{intervals.map((interval) => { {intervals.map((interval) => {
const selected = field.value === interval; const selected = field.value === interval;
return ( return (
<label <label
htmlFor={interval} htmlFor={interval}
key={interval} key={interval}
className={cn( className={cn(
'hover:bg-muted flex items-center space-x-2 rounded-md border border-transparent px-4 py-2', 'hover:bg-muted flex items-center space-x-2 rounded-md border border-transparent px-4 py-2',
{ {
['border-border']: selected, ['border-border']: selected,
['hover:bg-muted']: !selected, ['hover:bg-muted']: !selected,
}, },
)} )}
> >
<RadioGroupItem <RadioGroupItem
id={interval} id={interval}
value={interval} value={interval}
onClick={() => { onClick={() => {
form.setValue('planId', '', { form.setValue('planId', '', {
shouldValidate: true, shouldValidate: true,
}); });
form.setValue('productId', '', { form.setValue('productId', '', {
shouldValidate: true, shouldValidate: true,
}); });
form.setValue('interval', interval, { form.setValue('interval', interval, {
shouldValidate: true, shouldValidate: true,
}); });
}} }}
/>
<span className={'text-sm font-bold'}>
<Trans
i18nKey={`billing:billingInterval.${interval}`}
/> />
</span>
</label> <span className={'text-sm font-bold'}>
); <Trans
})} i18nKey={`billing:billingInterval.${interval}`}
</div> />
</RadioGroup> </span>
</FormControl> </label>
<FormMessage /> );
</FormItem> })}
); </div>
}} </RadioGroup>
/> </FormControl>
<FormMessage />
</FormItem>
);
}}
/>
</div>
<FormField <FormField
name={'planId'} name={'planId'}
@@ -185,9 +196,13 @@ export function PlanPicker(
<FormControl> <FormControl>
<RadioGroup name={field.name}> <RadioGroup name={field.name}>
{props.config.products.map((product) => { {props.config.products.map((product) => {
const plan = product.plans.find( const plan = product.plans.find((item) => {
(item) => item.interval === selectedInterval, if (item.paymentType === 'one-time') {
); return true;
}
return item.interval === selectedInterval;
});
if (!plan) { if (!plan) {
return null; return null;
@@ -277,12 +292,21 @@ export function PlanPicker(
<div> <div>
<span className={'text-muted-foreground'}> <span className={'text-muted-foreground'}>
<Trans <If
i18nKey={`billing:perPeriod`} condition={
values={{ plan.paymentType === 'recurring'
period: selectedInterval, }
}} fallback={
/> <Trans i18nKey={`billing:lifetime`} />
}
>
<Trans
i18nKey={`billing:perPeriod`}
values={{
period: selectedInterval,
}}
/>
</If>
</span> </span>
</div> </div>
</div> </div>
@@ -348,8 +372,11 @@ function PlanDetails({
selectedPlan: { selectedPlan: {
lineItems: z.infer<typeof LineItemSchema>[]; lineItems: z.infer<typeof LineItemSchema>[];
paymentType: string;
}; };
}) { }) {
const isRecurring = selectedPlan.paymentType === 'recurring';
return ( return (
<div <div
className={ className={
@@ -364,7 +391,9 @@ function PlanDetails({
defaults={selectedProduct.name} defaults={selectedProduct.name}
/> />
</b>{' '} </b>{' '}
/ <Trans i18nKey={`billing:billingInterval.${selectedInterval}`} /> <If condition={isRecurring}>
/ <Trans i18nKey={`billing:billingInterval.${selectedInterval}`} />
</If>
</Heading> </Heading>
<p> <p>
@@ -384,7 +413,7 @@ function PlanDetails({
<LineItemDetails <LineItemDetails
lineItems={selectedPlan.lineItems ?? []} lineItems={selectedPlan.lineItems ?? []}
selectedInterval={selectedInterval} selectedInterval={isRecurring ? selectedInterval : undefined}
currency={selectedProduct.currency} currency={selectedProduct.currency}
/> />
</div> </div>

View File

@@ -81,20 +81,20 @@ export class BillingEventHandlerService {
Logger.info(ctx, 'Successfully updated subscription'); Logger.info(ctx, 'Successfully updated subscription');
}, },
onCheckoutSessionCompleted: async (payload, customerId) => { onCheckoutSessionCompleted: async (payload) => {
// Handle the checkout session completed event // Handle the checkout session completed event
// here we add the subscription to the database // here we add the subscription to the database
const client = this.clientProvider(); const client = this.clientProvider();
// Check if the payload contains an order_id // Check if the payload contains an order_id
// if it does, we add an order, otherwise we add a subscription // if it does, we add an order, otherwise we add a subscription
if ('order_id' in payload) { if ('target_order_id' in payload) {
const ctx = { const ctx = {
namespace: this.namespace, namespace: this.namespace,
orderId: payload.order_id, orderId: payload.target_order_id,
provider: payload.billing_provider, provider: payload.billing_provider,
accountId: payload.target_account_id, accountId: payload.target_account_id,
customerId, customerId: payload.target_customer_id,
}; };
Logger.info(ctx, 'Processing order completed event...'); Logger.info(ctx, 'Processing order completed event...');
@@ -114,7 +114,7 @@ export class BillingEventHandlerService {
subscriptionId: payload.target_subscription_id, subscriptionId: payload.target_subscription_id,
provider: payload.billing_provider, provider: payload.billing_provider,
accountId: payload.target_account_id, accountId: payload.target_account_id,
customerId, customerId: payload.target_customer_id,
}; };
Logger.info(ctx, 'Processing checkout session completed event...'); Logger.info(ctx, 'Processing checkout session completed event...');

View File

@@ -186,9 +186,9 @@ export type BillingConfig = z.infer<typeof BillingSchema>;
export type ProductSchema = z.infer<typeof ProductSchema>; export type ProductSchema = z.infer<typeof ProductSchema>;
export function getPlanIntervals(config: z.infer<typeof BillingSchema>) { export function getPlanIntervals(config: z.infer<typeof BillingSchema>) {
const intervals = config.products.flatMap((product) => const intervals = config.products
product.plans.map((plan) => plan.interval), .flatMap((product) => product.plans.map((plan) => plan.interval))
); .filter(Boolean);
return Array.from(new Set(intervals)); return Array.from(new Set(intervals));
} }

View File

@@ -29,7 +29,7 @@ export function PersonalAccountSettingsContainer(
}>, }>,
) { ) {
return ( return (
<div className={'flex w-full flex-col space-y-8 pb-32'}> <div className={'flex w-full flex-col space-y-6 pb-32'}>
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle> <CardTitle>

View File

@@ -27,7 +27,7 @@ export function TeamAccountSettingsContainer(props: {
}; };
}) { }) {
return ( return (
<div className={'flex w-full flex-col space-y-8'}> <div className={'flex w-full flex-col space-y-6'}>
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle> <CardTitle>

View File

@@ -63,10 +63,6 @@ function EmbeddedCheckoutPopup({
setOpen(open); setOpen(open);
}} }}
> >
<DialogHeader>
<DialogTitle>Complete your purchase</DialogTitle>
</DialogHeader>
<DialogContent <DialogContent
className={className} className={className}
onOpenAutoFocus={(e) => e.preventDefault()} onOpenAutoFocus={(e) => e.preventDefault()}

View File

@@ -73,6 +73,7 @@ export async function createStripeCheckout(
line_items: lineItems, line_items: lineItems,
client_reference_id: clientReferenceId, client_reference_id: clientReferenceId,
subscription_data: subscriptionData, subscription_data: subscriptionData,
customer_creation: 'always',
...customerData, ...customerData,
...urls, ...urls,
}); });

View File

@@ -171,7 +171,7 @@ export class StripeWebhookHandlerService
const payload: UpsertOrderParams = { const payload: UpsertOrderParams = {
target_account_id: accountId, target_account_id: accountId,
target_customer_id: customerId, target_customer_id: customerId,
order_id: sessionId, target_order_id: sessionId,
billing_provider: this.provider, billing_provider: this.provider,
status: status, status: status,
currency: currency, currency: currency,

View File

@@ -410,11 +410,9 @@ export type Database = {
created_at: string created_at: string
currency: string currency: string
id: string id: string
product_id: string
status: Database["public"]["Enums"]["payment_status"] status: Database["public"]["Enums"]["payment_status"]
total_amount: number total_amount: number
updated_at: string updated_at: string
variant_id: string
} }
Insert: { Insert: {
account_id: string account_id: string
@@ -423,11 +421,9 @@ export type Database = {
created_at?: string created_at?: string
currency: string currency: string
id: string id: string
product_id: string
status: Database["public"]["Enums"]["payment_status"] status: Database["public"]["Enums"]["payment_status"]
total_amount: number total_amount: number
updated_at?: string updated_at?: string
variant_id: string
} }
Update: { Update: {
account_id?: string account_id?: string
@@ -436,11 +432,9 @@ export type Database = {
created_at?: string created_at?: string
currency?: string currency?: string
id?: string id?: string
product_id?: string
status?: Database["public"]["Enums"]["payment_status"] status?: Database["public"]["Enums"]["payment_status"]
total_amount?: number total_amount?: number
updated_at?: string updated_at?: string
variant_id?: string
} }
Relationships: [ Relationships: [
{ {
@@ -891,7 +885,7 @@ export type Database = {
Args: { Args: {
target_account_id: string target_account_id: string
target_customer_id: string target_customer_id: string
order_id: string target_order_id: string
status: Database["public"]["Enums"]["payment_status"] status: Database["public"]["Enums"]["payment_status"]
billing_provider: Database["public"]["Enums"]["billing_provider"] billing_provider: Database["public"]["Enums"]["billing_provider"]
total_amount: number total_amount: number
@@ -905,11 +899,9 @@ export type Database = {
created_at: string created_at: string
currency: string currency: string
id: string id: string
product_id: string
status: Database["public"]["Enums"]["payment_status"] status: Database["public"]["Enums"]["payment_status"]
total_amount: number total_amount: number
updated_at: string updated_at: string
variant_id: string
} }
} }
upsert_subscription: { upsert_subscription: {

View File

@@ -1249,7 +1249,7 @@ select
create or replace function public.upsert_order( create or replace function public.upsert_order(
target_account_id uuid, target_account_id uuid,
target_customer_id varchar(255), target_customer_id varchar(255),
order_id text, target_order_id text,
status public.payment_status, status public.payment_status,
billing_provider public.billing_provider, billing_provider public.billing_provider,
total_amount numeric, total_amount numeric,
@@ -1261,7 +1261,7 @@ declare
new_billing_customer_id int; new_billing_customer_id int;
begin begin
insert into public.billing_customers(account_id, provider, customer_id) insert into public.billing_customers(account_id, provider, customer_id)
values (target_account_id, target_billing_provider, target_customer_id) values (target_account_id, billing_provider, target_customer_id)
on conflict (account_id, provider, customer_id) do update on conflict (account_id, provider, customer_id) do update
set provider = excluded.provider set provider = excluded.provider
returning id into new_billing_customer_id; returning id into new_billing_customer_id;
@@ -1277,7 +1277,7 @@ begin
values ( values (
target_account_id, target_account_id,
new_billing_customer_id, new_billing_customer_id,
order_id, target_order_id,
status, status,
billing_provider, billing_provider,
total_amount, total_amount,