fix: send memberid on checkout

This commit is contained in:
Rotimi Best
2026-05-27 20:44:56 +01:00
parent 607377f191
commit 730bcee553
5 changed files with 118 additions and 5 deletions
+65 -3
View File
@@ -15,6 +15,9 @@ import {
getOrganizationAudienceMember, getOrganizationAudienceMember,
getOrganizationById, getOrganizationById,
getOrganizationBySiteName, getOrganizationBySiteName,
getOrganizationMemberByIdAndOrg,
getOrganizationMemberIdByOrgAndProfile,
getOrganizationPlanBySubscriptionId,
getOrganizationTeam, getOrganizationTeam,
getOrganizations, getOrganizations,
revokeActiveOrganizationInvitesByEmails, revokeActiveOrganizationInvitesByEmails,
@@ -41,7 +44,7 @@ import { PLAN } from '@cio/utils/plans';
import { ROLE } from '@cio/utils/constants'; import { ROLE } from '@cio/utils/constants';
import { createOrganizationWithOwner } from '@api/services/onboarding'; import { createOrganizationWithOwner } from '@api/services/onboarding';
import { deriveAudienceMemberStatus } from '@api/utils/audience-member-status'; import { deriveAudienceMemberStatus } from '@api/utils/audience-member-status';
import { getProfileById } from '@cio/db/queries/auth'; import { getProfileById, getProfileByEmail } from '@cio/db/queries/auth';
import { inviteTeamMembers as inviteTeamMembersSecure } from './organization/invite'; import { inviteTeamMembers as inviteTeamMembersSecure } from './organization/invite';
import { trustCustomDomainHostname, untrustCustomDomainHostname } from '@cio/db/utils'; import { trustCustomDomainHostname, untrustCustomDomainHostname } from '@cio/db/utils';
@@ -520,6 +523,45 @@ export async function getOrgSetupData(siteName: string) {
} }
} }
async function resolveOrgPlanTriggeredBy(params: {
checkoutOrgId: string;
targetOrgId: string;
triggeredBy: number;
payload: Record<string, unknown> | null | undefined;
}): Promise<number> {
const { checkoutOrgId, targetOrgId, triggeredBy, payload } = params;
const checkoutMember = await getOrganizationMemberByIdAndOrg(triggeredBy, checkoutOrgId);
if (checkoutMember?.profileId) {
if (targetOrgId === checkoutOrgId) {
return triggeredBy;
}
const primaryMemberId = await getOrganizationMemberIdByOrgAndProfile(targetOrgId, checkoutMember.profileId);
if (primaryMemberId) {
return primaryMemberId;
}
}
const customer = payload?.customer;
if (customer && typeof customer === 'object' && 'email' in customer && typeof customer.email === 'string') {
const profile = await getProfileByEmail(customer.email);
const memberId = profile ? await getOrganizationMemberIdByOrgAndProfile(targetOrgId, profile.id) : null;
if (memberId) {
return memberId;
}
}
throw new AppError(
'Could not resolve organization member for plan',
ErrorCodes.ORG_PLAN_CREATE_FAILED,
400
);
}
/** /**
* Creates a new organization plan * Creates a new organization plan
* @param data - Organization plan creation data * @param data - Organization plan creation data
@@ -527,22 +569,42 @@ export async function getOrgSetupData(siteName: string) {
*/ */
export async function createOrgPlan(data: TNewOrganizationPlan) { export async function createOrgPlan(data: TNewOrganizationPlan) {
try { try {
if (!data.orgId || data.triggeredBy == null || !data.subscriptionId) {
throw new AppError('Missing organization plan fields', ErrorCodes.ORG_PLAN_CREATE_FAILED, 400);
}
const existingPlan = await getOrganizationPlanBySubscriptionId(data.subscriptionId);
if (existingPlan) {
return existingPlan;
}
// Subscriptions always attach to the primary workspace, even if a // Subscriptions always attach to the primary workspace, even if a
// secondary org id was supplied at checkout. // secondary org id was supplied at checkout.
const primary = data.orgId ? await getAccountPrimary(data.orgId) : null; const primary = await getAccountPrimary(data.orgId);
const targetOrgId = primary?.id ?? data.orgId; const targetOrgId = primary?.id ?? data.orgId;
const triggeredBy = await resolveOrgPlanTriggeredBy({
checkoutOrgId: data.orgId,
targetOrgId,
triggeredBy: data.triggeredBy,
payload: data.payload as Record<string, unknown> | null | undefined
});
const plan = await createOrganizationPlan({ const plan = await createOrganizationPlan({
orgId: targetOrgId, orgId: targetOrgId,
planName: data.planName, planName: data.planName,
subscriptionId: data.subscriptionId, subscriptionId: data.subscriptionId,
triggeredBy: data.triggeredBy, triggeredBy,
payload: data.payload, payload: data.payload,
isActive: true, isActive: true,
provider: 'polar' provider: 'polar'
}); });
return plan; return plan;
} catch (error) { } catch (error) {
if (error instanceof AppError) {
throw error;
}
throw new AppError( throw new AppError(
error instanceof Error ? error.message : 'Failed to create organization plan', error instanceof Error ? error.message : 'Failed to create organization plan',
ErrorCodes.ORG_PLAN_CREATE_FAILED, ErrorCodes.ORG_PLAN_CREATE_FAILED,
@@ -32,7 +32,7 @@
}); });
async function handleClick(plan, planName: string) { async function handleClick(plan, planName: string) {
if (plan.CTA.IS_DISABLED || !$profile.id || !$profile.email) { if (plan.CTA.IS_DISABLED || !$profile.id || !$profile.email || !$currentOrg.memberId) {
return; return;
} }
@@ -52,7 +52,7 @@
checkoutURL.searchParams.set( checkoutURL.searchParams.set(
'metadata', 'metadata',
JSON.stringify({ JSON.stringify({
triggeredBy: $currentOrg.roleId, triggeredBy: $currentOrg.memberId,
orgId: $currentOrg.id, orgId: $currentOrg.id,
orgSlug: $currentOrg.siteName orgSlug: $currentOrg.siteName
}) })
@@ -48,6 +48,7 @@ export const currentOrg: Writable<AccountOrg> = writable({
parentOrganizationId: null, parentOrganizationId: null,
plans: [], plans: [],
readOnlyUntil: null, readOnlyUntil: null,
memberId: 0,
roleId: 0, roleId: 0,
settings: {}, settings: {},
siteName: '', siteName: '',
@@ -42,6 +42,7 @@ export const getOrganizationByProfileId = async (profileId: string): Promise<Org
const result = await db const result = await db
.select({ .select({
organization: schema.organization, organization: schema.organization,
memberId: schema.organizationmember.id,
roleId: schema.organizationmember.roleId, roleId: schema.organizationmember.roleId,
plan: { plan: {
planName: schema.organizationPlan.planName, planName: schema.organizationPlan.planName,
@@ -61,6 +62,7 @@ export const getOrganizationByProfileId = async (profileId: string): Promise<Org
string, string,
{ {
organization: typeof schema.organization.$inferSelect; organization: typeof schema.organization.$inferSelect;
memberId: number | undefined;
roleId: number | undefined; roleId: number | undefined;
plans: Array<OrganizationPlan>; plans: Array<OrganizationPlan>;
} }
@@ -72,6 +74,7 @@ export const getOrganizationByProfileId = async (profileId: string): Promise<Org
if (!organizationMap.has(orgId)) { if (!organizationMap.has(orgId)) {
organizationMap.set(orgId, { organizationMap.set(orgId, {
organization: row.organization, organization: row.organization,
memberId: row.memberId ?? undefined,
roleId: row.roleId ?? undefined, roleId: row.roleId ?? undefined,
plans: [] plans: []
}); });
@@ -115,6 +118,32 @@ export const createOrganizationMember = async (data: TNewOrganizationmember, dbC
return member; return member;
}; };
export async function getOrganizationMemberByIdAndOrg(
memberId: number,
organizationId: string,
dbClient: DbOrTxClient = db
): Promise<{ id: number; profileId: string | null } | null> {
try {
const [row] = await dbClient
.select({
id: schema.organizationmember.id,
profileId: schema.organizationmember.profileId
})
.from(schema.organizationmember)
.where(
and(eq(schema.organizationmember.id, memberId), eq(schema.organizationmember.organizationId, organizationId))
)
.limit(1);
return row ?? null;
} catch (error) {
console.error('getOrganizationMemberByIdAndOrg error:', error);
throw new Error(
`Failed to resolve organization membership: ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
}
export async function getOrganizationMemberIdByOrgAndProfile( export async function getOrganizationMemberIdByOrgAndProfile(
organizationId: string, organizationId: string,
profileId: string, profileId: string,
@@ -870,6 +899,26 @@ export const getOrganizationCount = async (): Promise<number> => {
* @param dbClient Optional DB or transaction client for use within transactions * @param dbClient Optional DB or transaction client for use within transactions
* @returns Created organization plan record * @returns Created organization plan record
*/ */
export const getOrganizationPlanBySubscriptionId = async (
subscriptionId: string,
dbClient: DbOrTxClient = db
): Promise<TOrganizationPlan | null> => {
try {
const [plan] = await dbClient
.select()
.from(schema.organizationPlan)
.where(eq(schema.organizationPlan.subscriptionId, subscriptionId))
.limit(1);
return plan ?? null;
} catch (error) {
console.error('getOrganizationPlanBySubscriptionId error:', error);
throw new Error(
`Failed to fetch organization plan: ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
};
export const createOrganizationPlan = async ( export const createOrganizationPlan = async (
data: TNewOrganizationPlan, data: TNewOrganizationPlan,
dbClient: DbOrTxClient = db dbClient: DbOrTxClient = db
@@ -5,6 +5,7 @@ export type OrganizationPlan = Pick<TOrganizationPlan, 'planName' | 'isActive' |
}; };
export type OrganizationWithMemberAndPlans = TOrganization & { export type OrganizationWithMemberAndPlans = TOrganization & {
memberId?: number;
roleId?: number; roleId?: number;
plans: OrganizationPlan[]; plans: OrganizationPlan[];
}; };