Implement updateSubscription feature and refactor billing services
This commit introduces the updateSubscription method to the BillingStrategyProviderService, ensuring that subscriptions can be updated within the billing core. Additionally, a refactor has been applied to the BillingGatewayFactoryService and stripe-billing-strategy.service to improve error handling and the robustness of subscription updates. Logging in the webhook route has been adjusted for clarity and the data model has been enhanced.
This commit is contained in:
@@ -8,29 +8,31 @@ import billingConfig from '~/config/billing.config';
|
|||||||
* @description Handle the webhooks from Stripe related to checkouts
|
* @description Handle the webhooks from Stripe related to checkouts
|
||||||
*/
|
*/
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
// we can infer the provider from the billing config or the request
|
|
||||||
// for simplicity, we'll use the billing config for now
|
|
||||||
// TODO: use dynamic provider from request?
|
|
||||||
const provider = billingConfig.provider;
|
const provider = billingConfig.provider;
|
||||||
|
|
||||||
Logger.info(
|
Logger.info(
|
||||||
{
|
{
|
||||||
name: 'billing',
|
name: 'billing.webhook',
|
||||||
provider,
|
provider,
|
||||||
},
|
},
|
||||||
`Received billing webhook. Processing...`,
|
`Received billing webhook. Processing...`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const clientProvider = () => getSupabaseRouteHandlerClient({ admin: true });
|
const supabaseClientProvider = () =>
|
||||||
|
getSupabaseRouteHandlerClient({ admin: true });
|
||||||
|
|
||||||
const service = await getBillingEventHandlerService(clientProvider, provider);
|
const service = await getBillingEventHandlerService(
|
||||||
|
supabaseClientProvider,
|
||||||
|
provider,
|
||||||
|
billingConfig,
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await service.handleWebhookEvent(request);
|
await service.handleWebhookEvent(request);
|
||||||
|
|
||||||
Logger.info(
|
Logger.info(
|
||||||
{
|
{
|
||||||
name: 'billing',
|
name: 'billing.webhook',
|
||||||
},
|
},
|
||||||
`Successfully processed billing webhook`,
|
`Successfully processed billing webhook`,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,10 +11,21 @@ const webhooksSecret = z
|
|||||||
|
|
||||||
const service = new DatabaseWebhookHandlerService();
|
const service = new DatabaseWebhookHandlerService();
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
const response = (status: number) => new Response(null, { status });
|
||||||
await service.handleWebhook(request, webhooksSecret);
|
|
||||||
|
|
||||||
return new Response(null, {
|
/**
|
||||||
status: 200,
|
* @name POST
|
||||||
});
|
* @description POST handler for the webhook route that handles the webhook event
|
||||||
|
* @param request
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
try {
|
||||||
|
// handle the webhook event
|
||||||
|
await service.handleWebhook(request, webhooksSecret);
|
||||||
|
|
||||||
|
return response(200);
|
||||||
|
} catch {
|
||||||
|
return response(500);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,6 +103,34 @@ export const PlanSchema = z
|
|||||||
message: 'Line item IDs must be unique',
|
message: 'Line item IDs must be unique',
|
||||||
path: ['lineItems'],
|
path: ['lineItems'],
|
||||||
},
|
},
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.paymentType === 'one-time') {
|
||||||
|
const meteredItems = data.lineItems.filter(
|
||||||
|
(item) => item.type === 'metered',
|
||||||
|
);
|
||||||
|
|
||||||
|
return meteredItems.length === 0;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: 'One-time plans must not have metered line items',
|
||||||
|
path: ['paymentType', 'lineItems'],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.paymentType === 'one-time') {
|
||||||
|
const baseItems = data.lineItems.filter((item) => item.type !== 'base');
|
||||||
|
|
||||||
|
return baseItems.length === 0;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: 'One-time plans must not have non-base line items',
|
||||||
|
path: ['paymentType', 'lineItems'],
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const ProductSchema = z
|
const ProductSchema = z
|
||||||
@@ -259,3 +287,20 @@ export function getProductPlanPairByVariantId(
|
|||||||
|
|
||||||
throw new Error('Plan not found');
|
throw new Error('Plan not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getLineItemTypeById(
|
||||||
|
config: z.infer<typeof BillingSchema>,
|
||||||
|
id: string,
|
||||||
|
) {
|
||||||
|
for (const product of config.products) {
|
||||||
|
for (const plan of product.plans) {
|
||||||
|
for (const lineItem of plan.lineItems) {
|
||||||
|
if (lineItem.type === id) {
|
||||||
|
return lineItem.type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Line Item with ID ${id} not found`);
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,3 +3,4 @@ export * from './create-biling-portal-session.schema';
|
|||||||
export * from './retrieve-checkout-session.schema';
|
export * from './retrieve-checkout-session.schema';
|
||||||
export * from './cancel-subscription-params.schema';
|
export * from './cancel-subscription-params.schema';
|
||||||
export * from './report-billing-usage.schema';
|
export * from './report-billing-usage.schema';
|
||||||
|
export * from './update-subscription-params.schema';
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const UpdateSubscriptionParamsSchema = z.object({
|
||||||
|
subscriptionId: z.string().min(1),
|
||||||
|
subscriptionItemId: z.string().min(1),
|
||||||
|
quantity: z.number().min(1),
|
||||||
|
});
|
||||||
@@ -4,9 +4,10 @@ import {
|
|||||||
CancelSubscriptionParamsSchema,
|
CancelSubscriptionParamsSchema,
|
||||||
CreateBillingCheckoutSchema,
|
CreateBillingCheckoutSchema,
|
||||||
CreateBillingPortalSessionSchema,
|
CreateBillingPortalSessionSchema,
|
||||||
|
ReportBillingUsageSchema,
|
||||||
RetrieveCheckoutSessionSchema,
|
RetrieveCheckoutSessionSchema,
|
||||||
|
UpdateSubscriptionParamsSchema,
|
||||||
} from '../schema';
|
} from '../schema';
|
||||||
import { ReportBillingUsageSchema } from '../schema';
|
|
||||||
|
|
||||||
export abstract class BillingStrategyProviderService {
|
export abstract class BillingStrategyProviderService {
|
||||||
abstract createBillingPortalSession(
|
abstract createBillingPortalSession(
|
||||||
@@ -44,4 +45,10 @@ export abstract class BillingStrategyProviderService {
|
|||||||
): Promise<{
|
): Promise<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
abstract updateSubscription(
|
||||||
|
params: z.infer<typeof UpdateSubscriptionParamsSchema>,
|
||||||
|
): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
}>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ export class BillingEventHandlerService {
|
|||||||
|
|
||||||
Logger.info(
|
Logger.info(
|
||||||
{
|
{
|
||||||
namespace: 'billing',
|
namespace: this.namespace,
|
||||||
sessionId,
|
sessionId,
|
||||||
},
|
},
|
||||||
'Successfully updated payment status',
|
'Successfully updated payment status',
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
BillingConfig,
|
||||||
BillingProviderSchema,
|
BillingProviderSchema,
|
||||||
BillingWebhookHandlerService,
|
BillingWebhookHandlerService,
|
||||||
} from '@kit/billing';
|
} from '@kit/billing';
|
||||||
@@ -8,12 +9,13 @@ import {
|
|||||||
export class BillingEventHandlerFactoryService {
|
export class BillingEventHandlerFactoryService {
|
||||||
static async GetProviderStrategy(
|
static async GetProviderStrategy(
|
||||||
provider: z.infer<typeof BillingProviderSchema>,
|
provider: z.infer<typeof BillingProviderSchema>,
|
||||||
|
config: BillingConfig,
|
||||||
): Promise<BillingWebhookHandlerService> {
|
): Promise<BillingWebhookHandlerService> {
|
||||||
switch (provider) {
|
switch (provider) {
|
||||||
case 'stripe': {
|
case 'stripe': {
|
||||||
const { StripeWebhookHandlerService } = await import('@kit/stripe');
|
const { StripeWebhookHandlerService } = await import('@kit/stripe');
|
||||||
|
|
||||||
return new StripeWebhookHandlerService();
|
return new StripeWebhookHandlerService(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'lemon-squeezy': {
|
case 'lemon-squeezy': {
|
||||||
@@ -21,7 +23,7 @@ export class BillingEventHandlerFactoryService {
|
|||||||
'@kit/lemon-squeezy'
|
'@kit/lemon-squeezy'
|
||||||
);
|
);
|
||||||
|
|
||||||
return new LemonSqueezyWebhookHandlerService();
|
return new LemonSqueezyWebhookHandlerService(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'paddle': {
|
case 'paddle': {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { BillingConfig } from '@kit/billing';
|
||||||
import { Database } from '@kit/supabase/database';
|
import { Database } from '@kit/supabase/database';
|
||||||
import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client';
|
import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client';
|
||||||
|
|
||||||
@@ -12,9 +13,12 @@ import { BillingEventHandlerFactoryService } from './billing-gateway-factory.ser
|
|||||||
export async function getBillingEventHandlerService(
|
export async function getBillingEventHandlerService(
|
||||||
clientProvider: () => ReturnType<typeof getSupabaseServerActionClient>,
|
clientProvider: () => ReturnType<typeof getSupabaseServerActionClient>,
|
||||||
provider: Database['public']['Enums']['billing_provider'],
|
provider: Database['public']['Enums']['billing_provider'],
|
||||||
|
config: BillingConfig,
|
||||||
) {
|
) {
|
||||||
const strategy =
|
const strategy = await BillingEventHandlerFactoryService.GetProviderStrategy(
|
||||||
await BillingEventHandlerFactoryService.GetProviderStrategy(provider);
|
provider,
|
||||||
|
config,
|
||||||
|
);
|
||||||
|
|
||||||
return new BillingEventHandlerService(clientProvider, strategy);
|
return new BillingEventHandlerService(clientProvider, strategy);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import {
|
|||||||
CancelSubscriptionParamsSchema,
|
CancelSubscriptionParamsSchema,
|
||||||
CreateBillingCheckoutSchema,
|
CreateBillingCheckoutSchema,
|
||||||
CreateBillingPortalSessionSchema,
|
CreateBillingPortalSessionSchema,
|
||||||
|
ReportBillingUsageSchema,
|
||||||
RetrieveCheckoutSessionSchema,
|
RetrieveCheckoutSessionSchema,
|
||||||
|
UpdateSubscriptionParamsSchema,
|
||||||
} from '@kit/billing/schema';
|
} from '@kit/billing/schema';
|
||||||
|
|
||||||
import { BillingGatewayFactoryService } from './billing-gateway-factory.service';
|
import { BillingGatewayFactoryService } from './billing-gateway-factory.service';
|
||||||
@@ -92,4 +94,35 @@ export class BillingGatewayService {
|
|||||||
|
|
||||||
return strategy.cancelSubscription(payload);
|
return strategy.cancelSubscription(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reports the usage of the billing.
|
||||||
|
* @description This is used to report the usage of the billing to the provider.
|
||||||
|
* @param params
|
||||||
|
*/
|
||||||
|
async reportUsage(params: z.infer<typeof ReportBillingUsageSchema>) {
|
||||||
|
const strategy = await BillingGatewayFactoryService.GetProviderStrategy(
|
||||||
|
this.provider,
|
||||||
|
);
|
||||||
|
|
||||||
|
const payload = ReportBillingUsageSchema.parse(params);
|
||||||
|
|
||||||
|
return strategy.reportUsage(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a subscription with the specified parameters.
|
||||||
|
* @param params
|
||||||
|
*/
|
||||||
|
async updateSubscriptionItem(
|
||||||
|
params: z.infer<typeof UpdateSubscriptionParamsSchema>,
|
||||||
|
) {
|
||||||
|
const strategy = await BillingGatewayFactoryService.GetProviderStrategy(
|
||||||
|
this.provider,
|
||||||
|
);
|
||||||
|
|
||||||
|
const payload = UpdateSubscriptionParamsSchema.parse(params);
|
||||||
|
|
||||||
|
return strategy.updateSubscription(payload);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
cancelSubscription,
|
cancelSubscription,
|
||||||
createUsageRecord,
|
createUsageRecord,
|
||||||
getCheckout,
|
getCheckout,
|
||||||
|
updateSubscriptionItem,
|
||||||
} from '@lemonsqueezy/lemonsqueezy.js';
|
} from '@lemonsqueezy/lemonsqueezy.js';
|
||||||
import 'server-only';
|
import 'server-only';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
@@ -13,6 +14,7 @@ import {
|
|||||||
CreateBillingPortalSessionSchema,
|
CreateBillingPortalSessionSchema,
|
||||||
ReportBillingUsageSchema,
|
ReportBillingUsageSchema,
|
||||||
RetrieveCheckoutSessionSchema,
|
RetrieveCheckoutSessionSchema,
|
||||||
|
UpdateSubscriptionParamsSchema,
|
||||||
} from '@kit/billing/schema';
|
} from '@kit/billing/schema';
|
||||||
import { Logger } from '@kit/shared/logger';
|
import { Logger } from '@kit/shared/logger';
|
||||||
|
|
||||||
@@ -240,4 +242,35 @@ export class LemonSqueezyBillingStrategyService
|
|||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateSubscription(
|
||||||
|
params: z.infer<typeof UpdateSubscriptionParamsSchema>,
|
||||||
|
) {
|
||||||
|
const ctx = {
|
||||||
|
name: 'billing.lemon-squeezy',
|
||||||
|
...params,
|
||||||
|
};
|
||||||
|
|
||||||
|
Logger.info(ctx, 'Updating subscription...');
|
||||||
|
|
||||||
|
const { error } = await updateSubscriptionItem(params.subscriptionItemId, {
|
||||||
|
quantity: params.quantity,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
Logger.error(
|
||||||
|
{
|
||||||
|
...ctx,
|
||||||
|
error,
|
||||||
|
},
|
||||||
|
'Failed to update subscription',
|
||||||
|
);
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.info(ctx, 'Subscription updated successfully');
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import { getOrder, getVariant } from '@lemonsqueezy/lemonsqueezy.js';
|
import { getOrder, getVariant } from '@lemonsqueezy/lemonsqueezy.js';
|
||||||
import { createHmac, timingSafeEqual } from 'crypto';
|
import { createHmac, timingSafeEqual } from 'crypto';
|
||||||
|
|
||||||
import { BillingWebhookHandlerService } from '@kit/billing';
|
import {
|
||||||
|
BillingConfig,
|
||||||
|
BillingWebhookHandlerService,
|
||||||
|
getLineItemTypeById,
|
||||||
|
} from '@kit/billing';
|
||||||
import { Logger } from '@kit/shared/logger';
|
import { Logger } from '@kit/shared/logger';
|
||||||
import { Database } from '@kit/supabase/database';
|
import { Database } from '@kit/supabase/database';
|
||||||
|
|
||||||
@@ -35,6 +39,8 @@ export class LemonSqueezyWebhookHandlerService
|
|||||||
|
|
||||||
private readonly namespace = 'billing.lemon-squeezy';
|
private readonly namespace = 'billing.lemon-squeezy';
|
||||||
|
|
||||||
|
constructor(private readonly config: BillingConfig) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Verifies the webhook signature - should throw an error if the signature is invalid
|
* @description Verifies the webhook signature - should throw an error if the signature is invalid
|
||||||
*/
|
*/
|
||||||
@@ -307,6 +313,7 @@ export class LemonSqueezyWebhookHandlerService
|
|||||||
product_id: item.product,
|
product_id: item.product,
|
||||||
variant_id: item.variant,
|
variant_id: item.variant,
|
||||||
price_amount: item.unitAmount,
|
price_amount: item.unitAmount,
|
||||||
|
type: getLineItemTypeById(this.config, item.id),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
CreateBillingPortalSessionSchema,
|
CreateBillingPortalSessionSchema,
|
||||||
ReportBillingUsageSchema,
|
ReportBillingUsageSchema,
|
||||||
RetrieveCheckoutSessionSchema,
|
RetrieveCheckoutSessionSchema,
|
||||||
|
UpdateSubscriptionParamsSchema,
|
||||||
} from '@kit/billing/schema';
|
} from '@kit/billing/schema';
|
||||||
import { Logger } from '@kit/shared/logger';
|
import { Logger } from '@kit/shared/logger';
|
||||||
|
|
||||||
@@ -198,6 +199,52 @@ export class StripeBillingStrategyService
|
|||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateSubscription(
|
||||||
|
params: z.infer<typeof UpdateSubscriptionParamsSchema>,
|
||||||
|
) {
|
||||||
|
const stripe = await this.stripeProvider();
|
||||||
|
|
||||||
|
Logger.info(
|
||||||
|
{
|
||||||
|
name: 'billing.stripe',
|
||||||
|
...params,
|
||||||
|
},
|
||||||
|
'Updating subscription...',
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await stripe.subscriptions.update(params.subscriptionId, {
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: params.subscriptionItemId,
|
||||||
|
quantity: params.quantity,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
Logger.info(
|
||||||
|
{
|
||||||
|
name: 'billing.stripe',
|
||||||
|
...params,
|
||||||
|
},
|
||||||
|
'Subscription updated successfully',
|
||||||
|
);
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
} catch (e) {
|
||||||
|
Logger.error(
|
||||||
|
{
|
||||||
|
name: 'billing.stripe',
|
||||||
|
...params,
|
||||||
|
error: e,
|
||||||
|
},
|
||||||
|
'Failed to update subscription',
|
||||||
|
);
|
||||||
|
|
||||||
|
throw new Error('Failed to update subscription');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async stripeProvider(): Promise<Stripe> {
|
private async stripeProvider(): Promise<Stripe> {
|
||||||
return createStripeClient();
|
return createStripeClient();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import Stripe from 'stripe';
|
import Stripe from 'stripe';
|
||||||
|
|
||||||
import { BillingWebhookHandlerService } from '@kit/billing';
|
import {
|
||||||
|
BillingConfig,
|
||||||
|
BillingWebhookHandlerService,
|
||||||
|
getLineItemTypeById,
|
||||||
|
} from '@kit/billing';
|
||||||
import { Logger } from '@kit/shared/logger';
|
import { Logger } from '@kit/shared/logger';
|
||||||
import { Database } from '@kit/supabase/database';
|
import { Database } from '@kit/supabase/database';
|
||||||
|
|
||||||
@@ -18,6 +22,8 @@ export class StripeWebhookHandlerService
|
|||||||
{
|
{
|
||||||
private stripe: Stripe | undefined;
|
private stripe: Stripe | undefined;
|
||||||
|
|
||||||
|
constructor(private readonly config: BillingConfig) {}
|
||||||
|
|
||||||
private readonly provider: Database['public']['Enums']['billing_provider'] =
|
private readonly provider: Database['public']['Enums']['billing_provider'] =
|
||||||
'stripe';
|
'stripe';
|
||||||
|
|
||||||
@@ -134,6 +140,8 @@ export class StripeWebhookHandlerService
|
|||||||
const accountId = session.client_reference_id!;
|
const accountId = session.client_reference_id!;
|
||||||
const customerId = session.customer as string;
|
const customerId = session.customer as string;
|
||||||
|
|
||||||
|
// if it's a subscription, we need to retrieve the subscription
|
||||||
|
// and build the payload for the subscription
|
||||||
if (isSubscription) {
|
if (isSubscription) {
|
||||||
const subscriptionId = session.subscription as string;
|
const subscriptionId = session.subscription as string;
|
||||||
const subscription = await stripe.subscriptions.retrieve(subscriptionId);
|
const subscription = await stripe.subscriptions.retrieve(subscriptionId);
|
||||||
@@ -154,8 +162,10 @@ export class StripeWebhookHandlerService
|
|||||||
|
|
||||||
return onCheckoutCompletedCallback(payload);
|
return onCheckoutCompletedCallback(payload);
|
||||||
} else {
|
} else {
|
||||||
|
// if it's a one-time payment, we need to retrieve the session
|
||||||
const sessionId = event.data.object.id;
|
const sessionId = event.data.object.id;
|
||||||
|
|
||||||
|
// from the session, we need to retrieve the line items
|
||||||
const sessionWithLineItems = await stripe.checkout.sessions.retrieve(
|
const sessionWithLineItems = await stripe.checkout.sessions.retrieve(
|
||||||
event.data.object.id,
|
event.data.object.id,
|
||||||
{
|
{
|
||||||
@@ -280,6 +290,7 @@ export class StripeWebhookHandlerService
|
|||||||
price_amount: item.price?.unit_amount,
|
price_amount: item.price?.unit_amount,
|
||||||
interval: item.price?.recurring?.interval as string,
|
interval: item.price?.recurring?.interval as string,
|
||||||
interval_count: item.price?.recurring?.interval_count as number,
|
interval_count: item.price?.recurring?.interval_count as number,
|
||||||
|
type: getLineItemTypeById(this.config, item.id),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export class DatabaseWebhookHandlerService {
|
|||||||
const service = new DatabaseWebhookRouterService(client);
|
const service = new DatabaseWebhookRouterService(client);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// handle the webhook event based on the table
|
||||||
await service.handleWebhook(json);
|
await service.handleWebhook(json);
|
||||||
|
|
||||||
Logger.info(
|
Logger.info(
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { SupabaseClient } from '@supabase/supabase-js';
|
import { SupabaseClient } from '@supabase/supabase-js';
|
||||||
|
|
||||||
|
import { Logger } from '@kit/shared/logger';
|
||||||
import { Database } from '@kit/supabase/database';
|
import { Database } from '@kit/supabase/database';
|
||||||
|
|
||||||
import { RecordChange, Tables } from '../record-change.type';
|
import { RecordChange, Tables } from '../record-change.type';
|
||||||
@@ -21,14 +22,14 @@ export class DatabaseWebhookRouterService {
|
|||||||
return this.handleSubscriptionsWebhook(payload);
|
return this.handleSubscriptionsWebhook(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'accounts_memberships': {
|
default: {
|
||||||
const payload = body as RecordChange<typeof body.table>;
|
Logger.warn(
|
||||||
|
{
|
||||||
return this.handleAccountsMembershipsWebhook(payload);
|
table: body.table,
|
||||||
|
},
|
||||||
|
'No handler found for table',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
|
||||||
throw new Error('No handler for this table');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,12 +53,4 @@ export class DatabaseWebhookRouterService {
|
|||||||
return service.handleSubscriptionDeletedWebhook(body.old_record);
|
return service.handleSubscriptionDeletedWebhook(body.old_record);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleAccountsMembershipsWebhook(
|
|
||||||
payload: RecordChange<'accounts_memberships'>,
|
|
||||||
) {
|
|
||||||
console.log('Accounts Memberships Webhook', payload);
|
|
||||||
// no-op
|
|
||||||
return Promise.resolve(undefined);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,14 +94,13 @@ export class DeletePersonalAccountService {
|
|||||||
productName: string;
|
productName: string;
|
||||||
}) {
|
}) {
|
||||||
const { renderAccountDeleteEmail } = await import('@kit/email-templates');
|
const { renderAccountDeleteEmail } = await import('@kit/email-templates');
|
||||||
const mailer = new Mailer();
|
|
||||||
|
|
||||||
const html = renderAccountDeleteEmail({
|
const html = renderAccountDeleteEmail({
|
||||||
userDisplayName: params.userDisplayName,
|
userDisplayName: params.userDisplayName,
|
||||||
productName: params.productName,
|
productName: params.productName,
|
||||||
});
|
});
|
||||||
|
|
||||||
await mailer.sendEmail({
|
await Mailer.sendEmail({
|
||||||
to: params.userEmail,
|
to: params.userEmail,
|
||||||
from: params.fromEmail,
|
from: params.fromEmail,
|
||||||
subject: 'Account Deletion Request',
|
subject: 'Account Deletion Request',
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { InviteMembersSchema } from '../../schema/invite-members.schema';
|
|||||||
import { RenewInvitationSchema } from '../../schema/renew-invitation.schema';
|
import { RenewInvitationSchema } from '../../schema/renew-invitation.schema';
|
||||||
import { UpdateInvitationSchema } from '../../schema/update-invitation.schema';
|
import { UpdateInvitationSchema } from '../../schema/update-invitation.schema';
|
||||||
import { AccountInvitationsService } from '../services/account-invitations.service';
|
import { AccountInvitationsService } from '../services/account-invitations.service';
|
||||||
|
import { AccountPerSeatBillingService } from '../services/account-per-seat-billing.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates invitations for inviting members.
|
* Creates invitations for inviting members.
|
||||||
@@ -98,15 +99,21 @@ export async function acceptInvitationAction(data: FormData) {
|
|||||||
Object.fromEntries(data),
|
Object.fromEntries(data),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const accountPerSeatBillingService = new AccountPerSeatBillingService(client);
|
||||||
const user = await assertSession(client);
|
const user = await assertSession(client);
|
||||||
|
|
||||||
const service = new AccountInvitationsService(client);
|
const service = new AccountInvitationsService(client);
|
||||||
|
|
||||||
await service.acceptInvitationToTeam({
|
// Accept the invitation
|
||||||
adminClient: getSupabaseServerActionClient({ admin: true }),
|
const accountId = await service.acceptInvitationToTeam(
|
||||||
inviteToken,
|
getSupabaseServerActionClient({ admin: true }),
|
||||||
userId: user.id,
|
{
|
||||||
});
|
inviteToken,
|
||||||
|
userId: user.id,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await accountPerSeatBillingService.increaseSeats(accountId);
|
||||||
|
|
||||||
return redirect(nextPath);
|
return redirect(nextPath);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,8 +37,6 @@ export class AccountInvitationsWebhookService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async dispatchInvitationEmail(invitation: Invitation) {
|
private async dispatchInvitationEmail(invitation: Invitation) {
|
||||||
const mailer = new Mailer();
|
|
||||||
|
|
||||||
const inviter = await this.client
|
const inviter = await this.client
|
||||||
.from('accounts')
|
.from('accounts')
|
||||||
.select('email, name')
|
.select('email, name')
|
||||||
@@ -70,7 +68,7 @@ export class AccountInvitationsWebhookService {
|
|||||||
teamName: team.data.name,
|
teamName: team.data.name,
|
||||||
});
|
});
|
||||||
|
|
||||||
await mailer.sendEmail({
|
await Mailer.sendEmail({
|
||||||
from: env.emailSender,
|
from: env.emailSender,
|
||||||
to: invitation.email,
|
to: invitation.email,
|
||||||
subject: 'You have been invited to join a team',
|
subject: 'You have been invited to join a team',
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { InviteMembersSchema } from '../../schema/invite-members.schema';
|
|||||||
import { UpdateInvitationSchema } from '../../schema/update-invitation.schema';
|
import { UpdateInvitationSchema } from '../../schema/update-invitation.schema';
|
||||||
|
|
||||||
export class AccountInvitationsService {
|
export class AccountInvitationsService {
|
||||||
private namespace = 'accounts.invitations';
|
private readonly namespace = 'invitations';
|
||||||
|
|
||||||
constructor(private readonly client: SupabaseClient<Database>) {}
|
constructor(private readonly client: SupabaseClient<Database>) {}
|
||||||
|
|
||||||
@@ -76,7 +76,11 @@ export class AccountInvitationsService {
|
|||||||
accountSlug: string;
|
accountSlug: string;
|
||||||
}) {
|
}) {
|
||||||
Logger.info(
|
Logger.info(
|
||||||
{ account: accountSlug, invitations, name: this.namespace },
|
{
|
||||||
|
account: accountSlug,
|
||||||
|
invitations,
|
||||||
|
name: this.namespace,
|
||||||
|
},
|
||||||
'Storing invitations',
|
'Storing invitations',
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -87,6 +91,14 @@ export class AccountInvitationsService {
|
|||||||
.single();
|
.single();
|
||||||
|
|
||||||
if (!accountResponse.data) {
|
if (!accountResponse.data) {
|
||||||
|
Logger.error(
|
||||||
|
{
|
||||||
|
accountSlug,
|
||||||
|
name: this.namespace,
|
||||||
|
},
|
||||||
|
'Account not found in database. Cannot send invitations.',
|
||||||
|
);
|
||||||
|
|
||||||
throw new Error('Account not found');
|
throw new Error('Account not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,6 +108,15 @@ export class AccountInvitationsService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (response.error) {
|
if (response.error) {
|
||||||
|
Logger.error(
|
||||||
|
{
|
||||||
|
accountSlug,
|
||||||
|
error: response.error,
|
||||||
|
name: this.namespace,
|
||||||
|
},
|
||||||
|
`Failed to add invitations to account ${accountSlug}`,
|
||||||
|
);
|
||||||
|
|
||||||
throw response.error;
|
throw response.error;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,12 +137,14 @@ export class AccountInvitationsService {
|
|||||||
/**
|
/**
|
||||||
* Accepts an invitation to join a team.
|
* Accepts an invitation to join a team.
|
||||||
*/
|
*/
|
||||||
async acceptInvitationToTeam(params: {
|
async acceptInvitationToTeam(
|
||||||
userId: string;
|
adminClient: SupabaseClient<Database>,
|
||||||
inviteToken: string;
|
params: {
|
||||||
adminClient: SupabaseClient<Database>;
|
userId: string;
|
||||||
}) {
|
inviteToken: string;
|
||||||
const { error, data } = await params.adminClient.rpc('accept_invitation', {
|
},
|
||||||
|
) {
|
||||||
|
const { error, data } = await adminClient.rpc('accept_invitation', {
|
||||||
token: params.inviteToken,
|
token: params.inviteToken,
|
||||||
user_id: params.userId,
|
user_id: params.userId,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { Database } from '@kit/supabase/database';
|
|||||||
import { RemoveMemberSchema } from '../../schema/remove-member.schema';
|
import { RemoveMemberSchema } from '../../schema/remove-member.schema';
|
||||||
import { TransferOwnershipConfirmationSchema } from '../../schema/transfer-ownership-confirmation.schema';
|
import { TransferOwnershipConfirmationSchema } from '../../schema/transfer-ownership-confirmation.schema';
|
||||||
import { UpdateMemberRoleSchema } from '../../schema/update-member-role.schema';
|
import { UpdateMemberRoleSchema } from '../../schema/update-member-role.schema';
|
||||||
|
import { AccountPerSeatBillingService } from './account-per-seat-billing.service';
|
||||||
|
|
||||||
export class AccountMembersService {
|
export class AccountMembersService {
|
||||||
private readonly namespace = 'account-members';
|
private readonly namespace = 'account-members';
|
||||||
@@ -16,6 +17,13 @@ export class AccountMembersService {
|
|||||||
constructor(private readonly client: SupabaseClient<Database>) {}
|
constructor(private readonly client: SupabaseClient<Database>) {}
|
||||||
|
|
||||||
async removeMemberFromAccount(params: z.infer<typeof RemoveMemberSchema>) {
|
async removeMemberFromAccount(params: z.infer<typeof RemoveMemberSchema>) {
|
||||||
|
const ctx = {
|
||||||
|
namespace: this.namespace,
|
||||||
|
...params,
|
||||||
|
};
|
||||||
|
|
||||||
|
Logger.info(ctx, `Removing member from account...`);
|
||||||
|
|
||||||
const { data, error } = await this.client
|
const { data, error } = await this.client
|
||||||
.from('accounts_memberships')
|
.from('accounts_memberships')
|
||||||
.delete()
|
.delete()
|
||||||
@@ -25,13 +33,37 @@ export class AccountMembersService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
|
Logger.error(
|
||||||
|
{
|
||||||
|
...ctx,
|
||||||
|
error,
|
||||||
|
},
|
||||||
|
`Failed to remove member from account`,
|
||||||
|
);
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Logger.info(
|
||||||
|
ctx,
|
||||||
|
`Successfully removed member from account. Verifying seat count...`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const service = new AccountPerSeatBillingService(this.client);
|
||||||
|
|
||||||
|
await service.decreaseSeats(params.accountId);
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateMemberRole(params: z.infer<typeof UpdateMemberRoleSchema>) {
|
async updateMemberRole(params: z.infer<typeof UpdateMemberRoleSchema>) {
|
||||||
|
const ctx = {
|
||||||
|
namespace: this.namespace,
|
||||||
|
...params,
|
||||||
|
};
|
||||||
|
|
||||||
|
Logger.info(ctx, `Updating member role...`);
|
||||||
|
|
||||||
const { data, error } = await this.client
|
const { data, error } = await this.client
|
||||||
.from('accounts_memberships')
|
.from('accounts_memberships')
|
||||||
.update({
|
.update({
|
||||||
@@ -43,9 +75,19 @@ export class AccountMembersService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
|
Logger.error(
|
||||||
|
{
|
||||||
|
...ctx,
|
||||||
|
error,
|
||||||
|
},
|
||||||
|
`Failed to update member role`,
|
||||||
|
);
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Logger.info(ctx, `Successfully updated member role`);
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,7 +99,7 @@ export class AccountMembersService {
|
|||||||
...params,
|
...params,
|
||||||
};
|
};
|
||||||
|
|
||||||
Logger.info(ctx, `Transferring ownership of account`);
|
Logger.info(ctx, `Transferring ownership of account...`);
|
||||||
|
|
||||||
const { data, error } = await this.client.rpc(
|
const { data, error } = await this.client.rpc(
|
||||||
'transfer_team_account_ownership',
|
'transfer_team_account_ownership',
|
||||||
|
|||||||
@@ -0,0 +1,206 @@
|
|||||||
|
import { SupabaseClient } from '@supabase/supabase-js';
|
||||||
|
|
||||||
|
import { BillingGatewayService } from '@kit/billing-gateway';
|
||||||
|
import { Logger } from '@kit/shared/logger';
|
||||||
|
import { Database } from '@kit/supabase/database';
|
||||||
|
|
||||||
|
export class AccountPerSeatBillingService {
|
||||||
|
private readonly namespace = 'accounts.per-seat-billing';
|
||||||
|
|
||||||
|
constructor(private readonly client: SupabaseClient<Database>) {}
|
||||||
|
|
||||||
|
async getPerSeatSubscriptionItem(accountId: string) {
|
||||||
|
Logger.info(
|
||||||
|
{
|
||||||
|
name: this.namespace,
|
||||||
|
accountId,
|
||||||
|
},
|
||||||
|
`Getting per-seat subscription item for account ${accountId}...`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data, error } = await this.client
|
||||||
|
.from('subscriptions')
|
||||||
|
.select(
|
||||||
|
`
|
||||||
|
provider: billing_provider,
|
||||||
|
id,
|
||||||
|
subscription_items !inner (
|
||||||
|
quantity,
|
||||||
|
id: variant_id,
|
||||||
|
type
|
||||||
|
)
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
.eq('account_id', accountId)
|
||||||
|
.eq('subscription_items.type', 'per-seat')
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
Logger.info(
|
||||||
|
{
|
||||||
|
name: this.namespace,
|
||||||
|
accountId,
|
||||||
|
error,
|
||||||
|
},
|
||||||
|
`Failed to get per-seat subscription item for account ${accountId}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data?.subscription_items) {
|
||||||
|
Logger.info(
|
||||||
|
{ name: this.namespace, accountId },
|
||||||
|
`No per-seat subscription item found for account ${accountId}. Exiting...`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.info(
|
||||||
|
{
|
||||||
|
name: this.namespace,
|
||||||
|
accountId,
|
||||||
|
},
|
||||||
|
`Per-seat subscription item found for account ${accountId}. Will update...`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async increaseSeats(accountId: string) {
|
||||||
|
const subscription = await this.getPerSeatSubscriptionItem(accountId);
|
||||||
|
|
||||||
|
if (!subscription) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const subscriptionItems = subscription.subscription_items.filter((item) => {
|
||||||
|
return item.type === 'per_seat';
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!subscriptionItems.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const billingGateway = new BillingGatewayService(subscription.provider);
|
||||||
|
|
||||||
|
Logger.info(
|
||||||
|
{
|
||||||
|
name: this.namespace,
|
||||||
|
accountId,
|
||||||
|
subscriptionItems,
|
||||||
|
},
|
||||||
|
`Increasing seats for account ${accountId}...`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const promises = subscriptionItems.map(async (item) => {
|
||||||
|
try {
|
||||||
|
Logger.info(
|
||||||
|
{
|
||||||
|
name: this.namespace,
|
||||||
|
accountId,
|
||||||
|
subscriptionItemId: item.id,
|
||||||
|
quantity: item.quantity + 1,
|
||||||
|
},
|
||||||
|
`Updating subscription item...`,
|
||||||
|
);
|
||||||
|
|
||||||
|
await billingGateway.updateSubscriptionItem({
|
||||||
|
subscriptionId: subscription.id,
|
||||||
|
subscriptionItemId: item.id,
|
||||||
|
quantity: item.quantity + 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
Logger.info(
|
||||||
|
{
|
||||||
|
name: this.namespace,
|
||||||
|
accountId,
|
||||||
|
subscriptionItemId: item.id,
|
||||||
|
quantity: item.quantity + 1,
|
||||||
|
},
|
||||||
|
`Subscription item updated successfully`,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error(
|
||||||
|
{
|
||||||
|
name: this.namespace,
|
||||||
|
accountId,
|
||||||
|
error,
|
||||||
|
},
|
||||||
|
`Failed to increase seats for account ${accountId}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
|
}
|
||||||
|
|
||||||
|
async decreaseSeats(accountId: string) {
|
||||||
|
const subscription = await this.getPerSeatSubscriptionItem(accountId);
|
||||||
|
|
||||||
|
if (!subscription) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const subscriptionItems = subscription.subscription_items.filter((item) => {
|
||||||
|
return item.type === 'per_seat';
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!subscriptionItems.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.info(
|
||||||
|
{
|
||||||
|
name: this.namespace,
|
||||||
|
accountId,
|
||||||
|
subscriptionItems,
|
||||||
|
},
|
||||||
|
`Decreasing seats for account ${accountId}...`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const billingGateway = new BillingGatewayService(subscription.provider);
|
||||||
|
|
||||||
|
const promises = subscriptionItems.map(async (item) => {
|
||||||
|
try {
|
||||||
|
Logger.info(
|
||||||
|
{
|
||||||
|
name: this.namespace,
|
||||||
|
accountId,
|
||||||
|
subscriptionItemId: item.id,
|
||||||
|
quantity: item.quantity - 1,
|
||||||
|
},
|
||||||
|
`Updating subscription item...`,
|
||||||
|
);
|
||||||
|
|
||||||
|
await billingGateway.updateSubscriptionItem({
|
||||||
|
subscriptionId: subscription.id,
|
||||||
|
subscriptionItemId: item.id,
|
||||||
|
quantity: item.quantity - 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
Logger.info(
|
||||||
|
{
|
||||||
|
name: this.namespace,
|
||||||
|
accountId,
|
||||||
|
subscriptionItemId: item.id,
|
||||||
|
quantity: item.quantity - 1,
|
||||||
|
},
|
||||||
|
`Subscription item updated successfully`,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error(
|
||||||
|
{
|
||||||
|
name: this.namespace,
|
||||||
|
accountId,
|
||||||
|
error,
|
||||||
|
},
|
||||||
|
`Failed to decrease seats for account ${accountId}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,14 +17,20 @@ Make sure the app installs the `@kit/mailers` package before using it.
|
|||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
By default, the package uses `nodemailer`.
|
||||||
|
|
||||||
|
To use Cloudflare, please set the environment variable `MAILER_PROVIDER` to `cloudflare`.
|
||||||
|
|
||||||
|
```
|
||||||
|
MAILER_PROVIDER=cloudflare
|
||||||
|
```
|
||||||
|
|
||||||
### Send an email
|
### Send an email
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
import { Mailer } from '@kit/mailers';
|
import { Mailer } from '@kit/mailers';
|
||||||
|
|
||||||
const mailer = new Mailer();
|
Mailer.sendEmail({
|
||||||
|
|
||||||
mailer.send({
|
|
||||||
to: '',
|
to: '',
|
||||||
from: '',
|
from: '',
|
||||||
subject: 'Hello',
|
subject: 'Hello',
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
const MAILER_ENV = z
|
const MAILER_PROVIDER = z
|
||||||
.enum(['nodemailer', 'cloudflare'])
|
.enum(['nodemailer', 'cloudflare'])
|
||||||
.default('nodemailer')
|
.default('nodemailer')
|
||||||
.parse(process.env.MAILER_ENV);
|
.parse(process.env.MAILER_PROVIDER);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description A mailer interface that can be implemented by any mailer.
|
* @description A mailer interface that can be implemented by any mailer.
|
||||||
@@ -27,7 +27,7 @@ export const Mailer = await getMailer();
|
|||||||
* @description Get the mailer based on the environment variable.
|
* @description Get the mailer based on the environment variable.
|
||||||
*/
|
*/
|
||||||
async function getMailer() {
|
async function getMailer() {
|
||||||
switch (MAILER_ENV) {
|
switch (MAILER_PROVIDER) {
|
||||||
case 'nodemailer': {
|
case 'nodemailer': {
|
||||||
const { Nodemailer } = await import('./impl/nodemailer');
|
const { Nodemailer } = await import('./impl/nodemailer');
|
||||||
|
|
||||||
@@ -41,6 +41,6 @@ async function getMailer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error(`Invalid mailer environment: ${MAILER_ENV as string}`);
|
throw new Error(`Invalid mailer: ${MAILER_PROVIDER as string}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -116,6 +116,17 @@ create type public.billing_provider as ENUM(
|
|||||||
'paddle'
|
'paddle'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Subscription Item Type
|
||||||
|
- We create the subscription item type for the Supabase MakerKit. These types are used to manage the type of the subscription items
|
||||||
|
- The types are 'flat', 'per_seat', and 'metered'.
|
||||||
|
- You can add more types as needed.
|
||||||
|
*/
|
||||||
|
create type public.subscription_item_type as ENUM(
|
||||||
|
'base',
|
||||||
|
'per_seat',
|
||||||
|
'metered'
|
||||||
|
);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* -------------------------------------------------------
|
* -------------------------------------------------------
|
||||||
@@ -1023,7 +1034,7 @@ create policy invitations_delete on public.invitations
|
|||||||
-- Functions
|
-- Functions
|
||||||
-- Function to accept an invitation to an account
|
-- Function to accept an invitation to an account
|
||||||
create or replace function accept_invitation(token text, user_id uuid)
|
create or replace function accept_invitation(token text, user_id uuid)
|
||||||
returns void
|
returns uuid
|
||||||
as $$
|
as $$
|
||||||
declare
|
declare
|
||||||
target_account_id uuid;
|
target_account_id uuid;
|
||||||
@@ -1056,6 +1067,7 @@ begin
|
|||||||
delete from public.invitations
|
delete from public.invitations
|
||||||
where invite_token = token;
|
where invite_token = token;
|
||||||
|
|
||||||
|
return target_account_id;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
$$
|
$$
|
||||||
@@ -1248,17 +1260,19 @@ on conflict (
|
|||||||
with item_data as (
|
with item_data as (
|
||||||
select
|
select
|
||||||
(line_item ->> 'product_id')::varchar as prod_id,
|
(line_item ->> 'product_id')::varchar as prod_id,
|
||||||
(line_item ->> 'variant_id')::varchar as var_id,
|
(line_item ->> 'variant_id')::varchar as var_id,
|
||||||
(line_item ->> 'price_amount')::numeric as price_amt,
|
(line_item ->> 'type')::public.subscription_item_type as type,
|
||||||
(line_item ->> 'quantity')::integer as qty,
|
(line_item ->> 'price_amount')::numeric as price_amt,
|
||||||
(line_item ->> 'interval')::varchar as intv,
|
(line_item ->> 'quantity')::integer as qty,
|
||||||
(line_item ->> 'interval_count')::integer as intv_count
|
(line_item ->> 'interval')::varchar as intv,
|
||||||
|
(line_item ->> 'interval_count')::integer as intv_count
|
||||||
from
|
from
|
||||||
jsonb_array_elements(line_items) as line_item)
|
jsonb_array_elements(line_items) as line_item)
|
||||||
insert into public.subscription_items(
|
insert into public.subscription_items(
|
||||||
subscription_id,
|
subscription_id,
|
||||||
product_id,
|
product_id,
|
||||||
variant_id,
|
variant_id,
|
||||||
|
type,
|
||||||
price_amount,
|
price_amount,
|
||||||
quantity,
|
quantity,
|
||||||
interval,
|
interval,
|
||||||
@@ -1267,6 +1281,7 @@ on conflict (
|
|||||||
target_subscription_id,
|
target_subscription_id,
|
||||||
prod_id,
|
prod_id,
|
||||||
var_id,
|
var_id,
|
||||||
|
type,
|
||||||
price_amt,
|
price_amt,
|
||||||
qty,
|
qty,
|
||||||
intv,
|
intv,
|
||||||
@@ -1306,6 +1321,7 @@ create table if not exists public.subscription_items(
|
|||||||
delete cascade not null,
|
delete cascade not null,
|
||||||
product_id varchar(255) not null,
|
product_id varchar(255) not null,
|
||||||
variant_id varchar(255) not null,
|
variant_id varchar(255) not null,
|
||||||
|
type public.subscription_item_type not null,
|
||||||
price_amount numeric,
|
price_amount numeric,
|
||||||
quantity integer not null default 1,
|
quantity integer not null default 1,
|
||||||
interval varchar(255) not null,
|
interval varchar(255) not null,
|
||||||
|
|||||||
Reference in New Issue
Block a user