Add stripe integration
This commit is contained in:
Generated
+18
@@ -17,6 +17,7 @@
|
||||
"morgan": "1.10.0",
|
||||
"nodemailer": "^8.0.5",
|
||||
"pg": "8.13.1",
|
||||
"stripe": "^22.0.2",
|
||||
"zod": "3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -1767,6 +1768,23 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/stripe": {
|
||||
"version": "22.0.2",
|
||||
"resolved": "https://registry.npmjs.org/stripe/-/stripe-22.0.2.tgz",
|
||||
"integrity": "sha512-2/BLrQ3oB1zlNfeL/LfHFjTGx6EQn0j+ztrrTJHuDjV5VVIpk92oSDaxyKLUr3pG3dnee2LZqhFUv2Bf0G1/3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/toidentifier": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"morgan": "1.10.0",
|
||||
"nodemailer": "^8.0.5",
|
||||
"pg": "8.13.1",
|
||||
"stripe": "^22.0.2",
|
||||
"zod": "3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -6,6 +6,7 @@ import rateLimit from 'express-rate-limit';
|
||||
import helmet from 'helmet';
|
||||
import morgan from 'morgan';
|
||||
import nodemailer from 'nodemailer';
|
||||
import Stripe from 'stripe';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ensureSchema } from './db/schema.js';
|
||||
@@ -60,6 +61,9 @@ import {
|
||||
listRescueWorkspacesForAdmin,
|
||||
listMembershipsForUser,
|
||||
listWorkspaceMembers,
|
||||
setWorkspaceStripeCustomerId,
|
||||
setWorkspaceStripeSubscription,
|
||||
setWorkspaceSubscriptionStatusByStripeSubscriptionId,
|
||||
updateRescueVerificationStatus,
|
||||
updateWorkspace,
|
||||
upsertWorkspaceMember,
|
||||
@@ -72,6 +76,7 @@ import type {
|
||||
IntegrationTokenRow,
|
||||
ProviderKey,
|
||||
RescueVerificationStatus,
|
||||
SubscriptionStatus,
|
||||
UserRow,
|
||||
VetVisitRow,
|
||||
WeightRow,
|
||||
@@ -231,6 +236,17 @@ const smtpPass = process.env.SMTP_PASS?.trim() ?? '';
|
||||
const smtpFromEmail = process.env.SMTP_FROM_EMAIL?.trim() ?? '';
|
||||
const smtpFromName = process.env.SMTP_FROM_NAME?.trim() || 'FlockPal';
|
||||
const rescueStatusNotificationEmail = process.env.RESCUE_STATUS_NOTIFICATION_EMAIL?.trim() || 'appadmin@flockpal.app';
|
||||
const stripeSecretKey = process.env.STRIPE_SECRET_KEY?.trim() ?? '';
|
||||
const stripeWebhookSecret = process.env.STRIPE_WEBHOOK_SECRET?.trim() ?? '';
|
||||
const stripeCheckoutSuccessUrl = process.env.STRIPE_CHECKOUT_SUCCESS_URL?.trim() || `${frontendBaseUrl}/?billing=success`;
|
||||
const stripeCheckoutCancelUrl = process.env.STRIPE_CHECKOUT_CANCEL_URL?.trim() || `${frontendBaseUrl}/?billing=cancelled`;
|
||||
const stripePortalReturnUrl = process.env.STRIPE_PORTAL_RETURN_URL?.trim() || frontendBaseUrl;
|
||||
const stripePriceByBillingPlan: Partial<Record<BillingPlan, string>> = {
|
||||
household_basic: process.env.STRIPE_PRICE_HOUSEHOLD_CONURE?.trim() ?? '',
|
||||
household_plus: process.env.STRIPE_PRICE_HOUSEHOLD_INDIANRINGNECK?.trim() ?? '',
|
||||
household_macaw: process.env.STRIPE_PRICE_HOUSEHOLD_MACAW?.trim() ?? '',
|
||||
};
|
||||
const stripe = stripeSecretKey ? new Stripe(stripeSecretKey) : null;
|
||||
const adminEmails = new Set(
|
||||
(process.env.ADMIN_EMAILS ?? '')
|
||||
.split(',')
|
||||
@@ -271,6 +287,8 @@ const normalizeWorkspace = (row: WorkspaceRow) => ({
|
||||
billingEmail: row.billing_email,
|
||||
billingPlan: row.billing_plan,
|
||||
subscriptionStatus: row.subscription_status,
|
||||
stripeCustomerId: row.stripe_customer_id,
|
||||
stripeSubscriptionId: row.stripe_subscription_id,
|
||||
rescueVerificationStatus: row.rescue_verification_status,
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at,
|
||||
@@ -402,6 +420,74 @@ app.use(
|
||||
legacyHeaders: false,
|
||||
}),
|
||||
);
|
||||
app.post('/api/billing/stripe/webhook', express.raw({ type: 'application/json' }), async (req: Request, res: Response) => {
|
||||
if (!stripeWebhookSecret) {
|
||||
res.status(503).json({ error: 'Stripe webhook is not configured.' });
|
||||
return;
|
||||
}
|
||||
|
||||
const signature = req.headers['stripe-signature'];
|
||||
|
||||
if (!signature) {
|
||||
res.status(400).json({ error: 'Missing Stripe signature.' });
|
||||
return;
|
||||
}
|
||||
|
||||
let event: Stripe.Event;
|
||||
|
||||
try {
|
||||
event = getStripeClient().webhooks.constructEvent(req.body, signature, stripeWebhookSecret);
|
||||
} catch (error) {
|
||||
res.status(400).json({ error: error instanceof Error ? error.message : 'Invalid Stripe webhook signature.' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (event.type === 'checkout.session.completed') {
|
||||
const session = event.data.object as Stripe.Checkout.Session;
|
||||
const workspaceId = Number(session.metadata?.workspaceId ?? session.client_reference_id ?? 0);
|
||||
const subscriptionId = typeof session.subscription === 'string' ? session.subscription : session.subscription?.id;
|
||||
const customerId = typeof session.customer === 'string' ? session.customer : session.customer?.id;
|
||||
|
||||
if (workspaceId && subscriptionId) {
|
||||
const subscription = await getStripeClient().subscriptions.retrieve(subscriptionId);
|
||||
await setWorkspaceStripeSubscription({
|
||||
workspaceId,
|
||||
stripeCustomerId: customerId ?? (typeof subscription.customer === 'string' ? subscription.customer : subscription.customer.id),
|
||||
stripeSubscriptionId: subscription.id,
|
||||
subscriptionStatus: mapStripeSubscriptionStatus(subscription.status),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
event.type === 'customer.subscription.created' ||
|
||||
event.type === 'customer.subscription.updated' ||
|
||||
event.type === 'customer.subscription.deleted'
|
||||
) {
|
||||
const subscription = event.data.object as Stripe.Subscription;
|
||||
const workspaceId = Number(subscription.metadata?.workspaceId ?? 0);
|
||||
const customerId = typeof subscription.customer === 'string' ? subscription.customer : subscription.customer.id;
|
||||
const subscriptionStatus = mapStripeSubscriptionStatus(subscription.status);
|
||||
|
||||
if (workspaceId) {
|
||||
await setWorkspaceStripeSubscription({
|
||||
workspaceId,
|
||||
stripeCustomerId: customerId,
|
||||
stripeSubscriptionId: subscription.id,
|
||||
subscriptionStatus,
|
||||
});
|
||||
} else {
|
||||
await setWorkspaceSubscriptionStatusByStripeSubscriptionId(subscription.id, subscriptionStatus);
|
||||
}
|
||||
}
|
||||
|
||||
res.json({ received: true });
|
||||
} catch (error) {
|
||||
console.error('Stripe webhook handling failed', error);
|
||||
res.status(500).json({ error: 'Unable to process Stripe webhook.' });
|
||||
}
|
||||
});
|
||||
app.use(express.json({ limit: '2mb' }));
|
||||
app.use(express.urlencoded({ extended: false }));
|
||||
app.use(morgan(process.env.NODE_ENV === 'production' ? 'combined' : 'dev'));
|
||||
@@ -416,6 +502,8 @@ const normalizeWorkspaceMembershipList = async (userId: string) =>
|
||||
billing_email: row.billing_email,
|
||||
billing_plan: row.billing_plan,
|
||||
subscription_status: row.subscription_status,
|
||||
stripe_customer_id: row.stripe_customer_id,
|
||||
stripe_subscription_id: row.stripe_subscription_id,
|
||||
rescue_verification_status: row.rescue_verification_status,
|
||||
created_at: row.workspace_created_at,
|
||||
updated_at: row.workspace_updated_at,
|
||||
@@ -432,6 +520,32 @@ const subscriptionAllowsWrite = (workspace: WorkspaceRow) => {
|
||||
return workspace.subscription_status === 'active' || workspace.subscription_status === 'trialing';
|
||||
};
|
||||
|
||||
const mapStripeSubscriptionStatus = (status: Stripe.Subscription.Status): SubscriptionStatus => {
|
||||
if (status === 'active' || status === 'trialing' || status === 'past_due' || status === 'canceled' || status === 'unpaid') {
|
||||
return status;
|
||||
}
|
||||
|
||||
return 'none';
|
||||
};
|
||||
|
||||
const getStripeClient = () => {
|
||||
if (!stripe) {
|
||||
throw new Error('Stripe is not configured.');
|
||||
}
|
||||
|
||||
return stripe;
|
||||
};
|
||||
|
||||
const getStripePriceIdForBillingPlan = (billingPlan: BillingPlan) => {
|
||||
const priceId = stripePriceByBillingPlan[billingPlan]?.trim() ?? '';
|
||||
|
||||
if (!priceId) {
|
||||
throw new Error(`Stripe price is not configured for ${billingPlan}.`);
|
||||
}
|
||||
|
||||
return priceId;
|
||||
};
|
||||
|
||||
const createAuthSession = async (userId: string, activeWorkspaceId: number) => {
|
||||
const token = createSessionToken();
|
||||
const tokenHash = hashToken(token);
|
||||
@@ -1113,6 +1227,106 @@ app.patch('/api/admin/rescue-workspaces/:workspaceId', requireAuth, requireSessi
|
||||
}
|
||||
});
|
||||
|
||||
app.post(
|
||||
'/api/billing/checkout-session',
|
||||
requireAuth,
|
||||
requireSessionAuth,
|
||||
requireWorkspaceRole(['owner', 'assistant']),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const parsed = z.object({ billingPlan: billingPlanSchema.optional() }).safeParse(req.body ?? {});
|
||||
|
||||
if (!parsed.success) {
|
||||
res.status(400).json({ error: 'Invalid billing payload', details: parsed.error.flatten() });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const workspace = req.auth!.workspace;
|
||||
|
||||
if (workspace.workspace_type === 'rescue') {
|
||||
res.status(400).json({ error: 'Rescue flocks do not use Stripe billing.' });
|
||||
return;
|
||||
}
|
||||
|
||||
const billingPlan = parsed.data.billingPlan ?? workspace.billing_plan;
|
||||
const priceId = getStripePriceIdForBillingPlan(billingPlan);
|
||||
let stripeCustomerId = workspace.stripe_customer_id;
|
||||
|
||||
if (!stripeCustomerId) {
|
||||
const customer = await getStripeClient().customers.create({
|
||||
email: workspace.billing_email ?? req.auth!.user.email,
|
||||
name: workspace.name,
|
||||
metadata: {
|
||||
workspaceId: String(workspace.id),
|
||||
userId: req.auth!.user.id,
|
||||
},
|
||||
});
|
||||
stripeCustomerId = customer.id;
|
||||
await setWorkspaceStripeCustomerId(workspace.id, stripeCustomerId);
|
||||
}
|
||||
|
||||
const checkoutSession = await getStripeClient().checkout.sessions.create({
|
||||
mode: 'subscription',
|
||||
customer: stripeCustomerId,
|
||||
client_reference_id: String(workspace.id),
|
||||
line_items: [{ price: priceId, quantity: 1 }],
|
||||
success_url: stripeCheckoutSuccessUrl,
|
||||
cancel_url: stripeCheckoutCancelUrl,
|
||||
allow_promotion_codes: true,
|
||||
metadata: {
|
||||
workspaceId: String(workspace.id),
|
||||
billingPlan,
|
||||
},
|
||||
subscription_data: {
|
||||
metadata: {
|
||||
workspaceId: String(workspace.id),
|
||||
billingPlan,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!checkoutSession.url) {
|
||||
throw new Error('Stripe did not return a checkout URL.');
|
||||
}
|
||||
|
||||
res.json({ url: checkoutSession.url });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
app.post(
|
||||
'/api/billing/portal-session',
|
||||
requireAuth,
|
||||
requireSessionAuth,
|
||||
requireWorkspaceRole(['owner', 'assistant']),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const workspace = req.auth!.workspace;
|
||||
|
||||
if (workspace.workspace_type === 'rescue') {
|
||||
res.status(400).json({ error: 'Rescue flocks do not use Stripe billing.' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!workspace.stripe_customer_id) {
|
||||
res.status(409).json({ error: 'Start a subscription before opening the billing portal.' });
|
||||
return;
|
||||
}
|
||||
|
||||
const portalSession = await getStripeClient().billingPortal.sessions.create({
|
||||
customer: workspace.stripe_customer_id,
|
||||
return_url: stripePortalReturnUrl,
|
||||
});
|
||||
|
||||
res.json({ url: portalSession.url });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
app.get('/api/integration-tokens', requireAuth, requireSessionAuth, async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const tokens = await listIntegrationTokens(req.auth!.user.id, req.auth!.workspace.id);
|
||||
|
||||
@@ -19,6 +19,8 @@ export const ensureSchema = async (database: DatabaseClient = db) => {
|
||||
billing_email VARCHAR(255),
|
||||
billing_plan VARCHAR(32) NOT NULL DEFAULT 'household_basic',
|
||||
subscription_status VARCHAR(32) NOT NULL DEFAULT 'active',
|
||||
stripe_customer_id VARCHAR(255),
|
||||
stripe_subscription_id VARCHAR(255),
|
||||
rescue_verification_status VARCHAR(32) NOT NULL DEFAULT 'not_required',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
@@ -31,8 +33,18 @@ export const ensureSchema = async (database: DatabaseClient = db) => {
|
||||
ADD COLUMN IF NOT EXISTS billing_email VARCHAR(255),
|
||||
ADD COLUMN IF NOT EXISTS billing_plan VARCHAR(32) NOT NULL DEFAULT 'household_basic',
|
||||
ADD COLUMN IF NOT EXISTS subscription_status VARCHAR(32) NOT NULL DEFAULT 'active',
|
||||
ADD COLUMN IF NOT EXISTS stripe_customer_id VARCHAR(255),
|
||||
ADD COLUMN IF NOT EXISTS stripe_subscription_id VARCHAR(255),
|
||||
ADD COLUMN IF NOT EXISTS rescue_verification_status VARCHAR(32) NOT NULL DEFAULT 'not_required';
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_workspaces_stripe_subscription_id
|
||||
ON workspaces (stripe_subscription_id)
|
||||
WHERE stripe_subscription_id IS NOT NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_workspaces_stripe_customer_id
|
||||
ON workspaces (stripe_customer_id)
|
||||
WHERE stripe_customer_id IS NOT NULL;
|
||||
|
||||
UPDATE workspaces
|
||||
SET rescue_verification_status = 'pending'
|
||||
WHERE workspace_type = 'rescue'
|
||||
|
||||
@@ -39,6 +39,8 @@ const mapSessionAuthRow = (
|
||||
workspace_billing_email: string | null;
|
||||
workspace_billing_plan: BillingPlan;
|
||||
workspace_subscription_status: SubscriptionStatus;
|
||||
workspace_stripe_customer_id: string | null;
|
||||
workspace_stripe_subscription_id: string | null;
|
||||
workspace_rescue_verification_status: RescueVerificationStatus;
|
||||
workspace_created_at: string;
|
||||
workspace_updated_at: string;
|
||||
@@ -75,6 +77,8 @@ const mapSessionAuthRow = (
|
||||
billing_email: row.workspace_billing_email,
|
||||
billing_plan: row.workspace_billing_plan,
|
||||
subscription_status: row.workspace_subscription_status,
|
||||
stripe_customer_id: row.workspace_stripe_customer_id,
|
||||
stripe_subscription_id: row.workspace_stripe_subscription_id,
|
||||
rescue_verification_status: row.workspace_rescue_verification_status,
|
||||
created_at: row.workspace_created_at,
|
||||
updated_at: row.workspace_updated_at,
|
||||
@@ -120,6 +124,8 @@ const mapIntegrationTokenAuthRow = (
|
||||
workspace_billing_email: string | null;
|
||||
workspace_billing_plan: BillingPlan;
|
||||
workspace_subscription_status: SubscriptionStatus;
|
||||
workspace_stripe_customer_id: string | null;
|
||||
workspace_stripe_subscription_id: string | null;
|
||||
workspace_rescue_verification_status: RescueVerificationStatus;
|
||||
workspace_created_at: string;
|
||||
workspace_updated_at: string;
|
||||
@@ -156,6 +162,8 @@ const mapIntegrationTokenAuthRow = (
|
||||
billing_email: row.workspace_billing_email,
|
||||
billing_plan: row.workspace_billing_plan,
|
||||
subscription_status: row.workspace_subscription_status,
|
||||
stripe_customer_id: row.workspace_stripe_customer_id,
|
||||
stripe_subscription_id: row.workspace_stripe_subscription_id,
|
||||
rescue_verification_status: row.workspace_rescue_verification_status,
|
||||
created_at: row.workspace_created_at,
|
||||
updated_at: row.workspace_updated_at,
|
||||
@@ -338,6 +346,8 @@ export const resolveAuth = async (tokenHash: string, token: string) => {
|
||||
workspace_billing_email: string | null;
|
||||
workspace_billing_plan: BillingPlan;
|
||||
workspace_subscription_status: SubscriptionStatus;
|
||||
workspace_stripe_customer_id: string | null;
|
||||
workspace_stripe_subscription_id: string | null;
|
||||
workspace_rescue_verification_status: RescueVerificationStatus;
|
||||
workspace_created_at: string;
|
||||
workspace_updated_at: string;
|
||||
@@ -369,6 +379,8 @@ export const resolveAuth = async (tokenHash: string, token: string) => {
|
||||
workspaces.billing_email AS workspace_billing_email,
|
||||
workspaces.billing_plan AS workspace_billing_plan,
|
||||
workspaces.subscription_status AS workspace_subscription_status,
|
||||
workspaces.stripe_customer_id AS workspace_stripe_customer_id,
|
||||
workspaces.stripe_subscription_id AS workspace_stripe_subscription_id,
|
||||
workspaces.rescue_verification_status AS workspace_rescue_verification_status,
|
||||
workspaces.created_at AS workspace_created_at,
|
||||
workspaces.updated_at AS workspace_updated_at,
|
||||
@@ -422,6 +434,8 @@ export const resolveIntegrationTokenAuth = async (tokenHash: string, token: stri
|
||||
workspace_billing_email: string | null;
|
||||
workspace_billing_plan: BillingPlan;
|
||||
workspace_subscription_status: SubscriptionStatus;
|
||||
workspace_stripe_customer_id: string | null;
|
||||
workspace_stripe_subscription_id: string | null;
|
||||
workspace_rescue_verification_status: RescueVerificationStatus;
|
||||
workspace_created_at: string;
|
||||
workspace_updated_at: string;
|
||||
@@ -458,6 +472,8 @@ export const resolveIntegrationTokenAuth = async (tokenHash: string, token: stri
|
||||
workspaces.billing_email AS workspace_billing_email,
|
||||
workspaces.billing_plan AS workspace_billing_plan,
|
||||
workspaces.subscription_status AS workspace_subscription_status,
|
||||
workspaces.stripe_customer_id AS workspace_stripe_customer_id,
|
||||
workspaces.stripe_subscription_id AS workspace_stripe_subscription_id,
|
||||
workspaces.rescue_verification_status AS workspace_rescue_verification_status,
|
||||
workspaces.created_at AS workspace_created_at,
|
||||
workspaces.updated_at AS workspace_updated_at,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { db } from '../db/client.js';
|
||||
import type { BillingPlan, RescueVerificationStatus, UserRow, WorkspaceMemberRow, WorkspaceRow, WorkspaceType } from '../types.js';
|
||||
import type { BillingPlan, RescueVerificationStatus, SubscriptionStatus, UserRow, WorkspaceMemberRow, WorkspaceRow, WorkspaceType } from '../types.js';
|
||||
|
||||
export const getNextWorkspaceId = async () => {
|
||||
const result = await db.query<{ next_id: number }>('SELECT COALESCE(MAX(id), 0) + 1 AS next_id FROM workspaces');
|
||||
@@ -8,7 +8,7 @@ export const getNextWorkspaceId = async () => {
|
||||
|
||||
export const getWorkspaceById = async (workspaceId: number) => {
|
||||
const result = await db.query<WorkspaceRow>(
|
||||
`SELECT id, name, workspace_type, billing_email, billing_plan, subscription_status, rescue_verification_status, created_at, updated_at
|
||||
`SELECT id, name, workspace_type, billing_email, billing_plan, subscription_status, stripe_customer_id, stripe_subscription_id, rescue_verification_status, created_at, updated_at
|
||||
FROM workspaces
|
||||
WHERE id = $1`,
|
||||
[workspaceId],
|
||||
@@ -37,6 +37,8 @@ export const listMembershipsForUser = async (userId: string) => {
|
||||
billing_email: string | null;
|
||||
billing_plan: BillingPlan;
|
||||
subscription_status: WorkspaceRow['subscription_status'];
|
||||
stripe_customer_id: string | null;
|
||||
stripe_subscription_id: string | null;
|
||||
rescue_verification_status: RescueVerificationStatus;
|
||||
workspace_created_at: string;
|
||||
workspace_updated_at: string;
|
||||
@@ -56,6 +58,8 @@ export const listMembershipsForUser = async (userId: string) => {
|
||||
workspaces.billing_email,
|
||||
workspaces.billing_plan,
|
||||
workspaces.subscription_status,
|
||||
workspaces.stripe_customer_id,
|
||||
workspaces.stripe_subscription_id,
|
||||
workspaces.rescue_verification_status,
|
||||
workspaces.created_at AS workspace_created_at,
|
||||
workspaces.updated_at AS workspace_updated_at
|
||||
@@ -217,7 +221,7 @@ export const updateWorkspace = async ({
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
FROM input
|
||||
WHERE workspaces.id = input.workspace_id
|
||||
RETURNING workspaces.id, workspaces.name, workspaces.workspace_type, workspaces.billing_email, workspaces.billing_plan, workspaces.subscription_status, workspaces.rescue_verification_status, workspaces.created_at, workspaces.updated_at`,
|
||||
RETURNING workspaces.id, workspaces.name, workspaces.workspace_type, workspaces.billing_email, workspaces.billing_plan, workspaces.subscription_status, workspaces.stripe_customer_id, workspaces.stripe_subscription_id, workspaces.rescue_verification_status, workspaces.created_at, workspaces.updated_at`,
|
||||
[workspaceId, name, workspaceType, billingEmail, billingPlan],
|
||||
);
|
||||
|
||||
@@ -252,7 +256,7 @@ export const findAlternateWorkspaceForUser = async (userId: string, excludeWorks
|
||||
|
||||
export const listOwnedWorkspacesByOwnerEmail = async (ownerEmail: string, excludeWorkspaceId: number) => {
|
||||
const result = await db.query<WorkspaceRow>(
|
||||
`SELECT workspaces.id, workspaces.name, workspaces.workspace_type, workspaces.billing_email, workspaces.billing_plan, workspaces.subscription_status, workspaces.rescue_verification_status, workspaces.created_at, workspaces.updated_at
|
||||
`SELECT workspaces.id, workspaces.name, workspaces.workspace_type, workspaces.billing_email, workspaces.billing_plan, workspaces.subscription_status, workspaces.stripe_customer_id, workspaces.stripe_subscription_id, workspaces.rescue_verification_status, workspaces.created_at, workspaces.updated_at
|
||||
FROM workspace_members
|
||||
INNER JOIN workspaces ON workspaces.id = workspace_members.workspace_id
|
||||
WHERE LOWER(COALESCE(workspace_members.invite_email, workspace_members.email)) = LOWER($1)
|
||||
@@ -353,6 +357,8 @@ export const listRescueWorkspacesForAdmin = async () => {
|
||||
workspaces.billing_email,
|
||||
workspaces.billing_plan,
|
||||
workspaces.subscription_status,
|
||||
workspaces.stripe_customer_id,
|
||||
workspaces.stripe_subscription_id,
|
||||
workspaces.rescue_verification_status,
|
||||
workspaces.created_at,
|
||||
workspaces.updated_at,
|
||||
@@ -398,7 +404,7 @@ export const updateRescueVerificationStatus = async (workspaceId: number, status
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $1
|
||||
AND workspace_type = 'rescue'
|
||||
RETURNING id, name, workspace_type, billing_email, billing_plan, subscription_status, rescue_verification_status, created_at, updated_at`,
|
||||
RETURNING id, name, workspace_type, billing_email, billing_plan, subscription_status, stripe_customer_id, stripe_subscription_id, rescue_verification_status, created_at, updated_at`,
|
||||
[workspaceId, status],
|
||||
);
|
||||
|
||||
@@ -415,13 +421,67 @@ export const cancelRescueVerificationRequest = async (workspaceId: number) => {
|
||||
WHERE id = $1
|
||||
AND workspace_type = 'rescue'
|
||||
AND rescue_verification_status = 'pending'
|
||||
RETURNING id, name, workspace_type, billing_email, billing_plan, subscription_status, rescue_verification_status, created_at, updated_at`,
|
||||
RETURNING id, name, workspace_type, billing_email, billing_plan, subscription_status, stripe_customer_id, stripe_subscription_id, rescue_verification_status, created_at, updated_at`,
|
||||
[workspaceId],
|
||||
);
|
||||
|
||||
return result.rows[0] ?? null;
|
||||
};
|
||||
|
||||
export const setWorkspaceStripeCustomerId = async (workspaceId: number, stripeCustomerId: string) => {
|
||||
const result = await db.query<WorkspaceRow>(
|
||||
`UPDATE workspaces
|
||||
SET stripe_customer_id = $2,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $1
|
||||
RETURNING id, name, workspace_type, billing_email, billing_plan, subscription_status, stripe_customer_id, stripe_subscription_id, rescue_verification_status, created_at, updated_at`,
|
||||
[workspaceId, stripeCustomerId],
|
||||
);
|
||||
|
||||
return result.rows[0] ?? null;
|
||||
};
|
||||
|
||||
export const setWorkspaceStripeSubscription = async ({
|
||||
workspaceId,
|
||||
stripeCustomerId,
|
||||
stripeSubscriptionId,
|
||||
subscriptionStatus,
|
||||
}: {
|
||||
workspaceId: number;
|
||||
stripeCustomerId: string | null;
|
||||
stripeSubscriptionId: string;
|
||||
subscriptionStatus: SubscriptionStatus;
|
||||
}) => {
|
||||
const result = await db.query<WorkspaceRow>(
|
||||
`UPDATE workspaces
|
||||
SET stripe_customer_id = COALESCE($2, stripe_customer_id),
|
||||
stripe_subscription_id = $3,
|
||||
subscription_status = $4,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $1
|
||||
RETURNING id, name, workspace_type, billing_email, billing_plan, subscription_status, stripe_customer_id, stripe_subscription_id, rescue_verification_status, created_at, updated_at`,
|
||||
[workspaceId, stripeCustomerId, stripeSubscriptionId, subscriptionStatus],
|
||||
);
|
||||
|
||||
return result.rows[0] ?? null;
|
||||
};
|
||||
|
||||
export const setWorkspaceSubscriptionStatusByStripeSubscriptionId = async (
|
||||
stripeSubscriptionId: string,
|
||||
subscriptionStatus: SubscriptionStatus,
|
||||
) => {
|
||||
const result = await db.query<WorkspaceRow>(
|
||||
`UPDATE workspaces
|
||||
SET subscription_status = $2,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE stripe_subscription_id = $1
|
||||
RETURNING id, name, workspace_type, billing_email, billing_plan, subscription_status, stripe_customer_id, stripe_subscription_id, rescue_verification_status, created_at, updated_at`,
|
||||
[stripeSubscriptionId, subscriptionStatus],
|
||||
);
|
||||
|
||||
return result.rows[0] ?? null;
|
||||
};
|
||||
|
||||
export const getPlatformAdminSummary = async () => {
|
||||
const result = await db.query<{
|
||||
total_birds: number;
|
||||
|
||||
@@ -22,6 +22,8 @@ export type WorkspaceRow = {
|
||||
billing_email: string | null;
|
||||
billing_plan: BillingPlan;
|
||||
subscription_status: SubscriptionStatus;
|
||||
stripe_customer_id: string | null;
|
||||
stripe_subscription_id: string | null;
|
||||
rescue_verification_status: RescueVerificationStatus;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
|
||||
Reference in New Issue
Block a user