feat(onboarding): add mini app household join flow

This commit is contained in:
2026-03-09 04:16:34 +04:00
parent e63d81cda2
commit 8109163067
22 changed files with 3702 additions and 160 deletions

View File

@@ -4,6 +4,8 @@ import { createDbClient, schema } from '@household/db'
import {
HOUSEHOLD_TOPIC_ROLES,
type HouseholdConfigurationRepository,
type HouseholdJoinTokenRecord,
type HouseholdPendingMemberRecord,
type HouseholdTelegramChatRecord,
type HouseholdTopicBindingRecord,
type HouseholdTopicRole,
@@ -50,6 +52,38 @@ function toHouseholdTopicBindingRecord(row: {
}
}
function toHouseholdJoinTokenRecord(row: {
householdId: string
householdName: string
token: string
createdByTelegramUserId: string | null
}): HouseholdJoinTokenRecord {
return {
householdId: row.householdId,
householdName: row.householdName,
token: row.token,
createdByTelegramUserId: row.createdByTelegramUserId
}
}
function toHouseholdPendingMemberRecord(row: {
householdId: string
householdName: string
telegramUserId: string
displayName: string
username: string | null
languageCode: string | null
}): HouseholdPendingMemberRecord {
return {
householdId: row.householdId,
householdName: row.householdName,
telegramUserId: row.telegramUserId,
displayName: row.displayName,
username: row.username,
languageCode: row.languageCode
}
}
export function createDbHouseholdConfigurationRepository(databaseUrl: string): {
repository: HouseholdConfigurationRepository
close: () => Promise<void>
@@ -261,6 +295,208 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): {
.orderBy(schema.householdTopicBindings.role)
return rows.map(toHouseholdTopicBindingRecord)
},
async upsertHouseholdJoinToken(input) {
const rows = await db
.insert(schema.householdJoinTokens)
.values({
householdId: input.householdId,
token: input.token,
createdByTelegramUserId: input.createdByTelegramUserId ?? null
})
.onConflictDoUpdate({
target: [schema.householdJoinTokens.householdId],
set: {
token: input.token,
createdByTelegramUserId: input.createdByTelegramUserId ?? null,
updatedAt: new Date()
}
})
.returning({
householdId: schema.householdJoinTokens.householdId,
token: schema.householdJoinTokens.token,
createdByTelegramUserId: schema.householdJoinTokens.createdByTelegramUserId
})
const row = rows[0]
if (!row) {
throw new Error('Failed to save household join token')
}
const householdRows = await db
.select({
householdId: schema.households.id,
householdName: schema.households.name
})
.from(schema.households)
.where(eq(schema.households.id, row.householdId))
.limit(1)
const household = householdRows[0]
if (!household) {
throw new Error('Failed to resolve household for join token')
}
return toHouseholdJoinTokenRecord({
householdId: row.householdId,
householdName: household.householdName,
token: row.token,
createdByTelegramUserId: row.createdByTelegramUserId
})
},
async getHouseholdJoinToken(householdId) {
const rows = await db
.select({
householdId: schema.householdJoinTokens.householdId,
householdName: schema.households.name,
token: schema.householdJoinTokens.token,
createdByTelegramUserId: schema.householdJoinTokens.createdByTelegramUserId
})
.from(schema.householdJoinTokens)
.innerJoin(
schema.households,
eq(schema.householdJoinTokens.householdId, schema.households.id)
)
.where(eq(schema.householdJoinTokens.householdId, householdId))
.limit(1)
const row = rows[0]
return row ? toHouseholdJoinTokenRecord(row) : null
},
async getHouseholdByJoinToken(token) {
const rows = await db
.select({
householdId: schema.householdJoinTokens.householdId,
householdName: schema.households.name,
telegramChatId: schema.householdTelegramChats.telegramChatId,
telegramChatType: schema.householdTelegramChats.telegramChatType,
title: schema.householdTelegramChats.title
})
.from(schema.householdJoinTokens)
.innerJoin(
schema.households,
eq(schema.householdJoinTokens.householdId, schema.households.id)
)
.innerJoin(
schema.householdTelegramChats,
eq(schema.householdJoinTokens.householdId, schema.householdTelegramChats.householdId)
)
.where(eq(schema.householdJoinTokens.token, token))
.limit(1)
const row = rows[0]
return row ? toHouseholdTelegramChatRecord(row) : null
},
async upsertPendingHouseholdMember(input) {
const rows = await db
.insert(schema.householdPendingMembers)
.values({
householdId: input.householdId,
telegramUserId: input.telegramUserId,
displayName: input.displayName,
username: input.username?.trim() || null,
languageCode: input.languageCode?.trim() || null
})
.onConflictDoUpdate({
target: [
schema.householdPendingMembers.householdId,
schema.householdPendingMembers.telegramUserId
],
set: {
displayName: input.displayName,
username: input.username?.trim() || null,
languageCode: input.languageCode?.trim() || null,
updatedAt: new Date()
}
})
.returning({
householdId: schema.householdPendingMembers.householdId,
telegramUserId: schema.householdPendingMembers.telegramUserId,
displayName: schema.householdPendingMembers.displayName,
username: schema.householdPendingMembers.username,
languageCode: schema.householdPendingMembers.languageCode
})
const row = rows[0]
if (!row) {
throw new Error('Failed to save pending household member')
}
const householdRows = await db
.select({
householdId: schema.households.id,
householdName: schema.households.name
})
.from(schema.households)
.where(eq(schema.households.id, row.householdId))
.limit(1)
const household = householdRows[0]
if (!household) {
throw new Error('Failed to resolve household for pending member')
}
return toHouseholdPendingMemberRecord({
householdId: row.householdId,
householdName: household.householdName,
telegramUserId: row.telegramUserId,
displayName: row.displayName,
username: row.username,
languageCode: row.languageCode
})
},
async getPendingHouseholdMember(householdId, telegramUserId) {
const rows = await db
.select({
householdId: schema.householdPendingMembers.householdId,
householdName: schema.households.name,
telegramUserId: schema.householdPendingMembers.telegramUserId,
displayName: schema.householdPendingMembers.displayName,
username: schema.householdPendingMembers.username,
languageCode: schema.householdPendingMembers.languageCode
})
.from(schema.householdPendingMembers)
.innerJoin(
schema.households,
eq(schema.householdPendingMembers.householdId, schema.households.id)
)
.where(
and(
eq(schema.householdPendingMembers.householdId, householdId),
eq(schema.householdPendingMembers.telegramUserId, telegramUserId)
)
)
.limit(1)
const row = rows[0]
return row ? toHouseholdPendingMemberRecord(row) : null
},
async findPendingHouseholdMemberByTelegramUserId(telegramUserId) {
const rows = await db
.select({
householdId: schema.householdPendingMembers.householdId,
householdName: schema.households.name,
telegramUserId: schema.householdPendingMembers.telegramUserId,
displayName: schema.householdPendingMembers.displayName,
username: schema.householdPendingMembers.username,
languageCode: schema.householdPendingMembers.languageCode
})
.from(schema.householdPendingMembers)
.innerJoin(
schema.households,
eq(schema.householdPendingMembers.householdId, schema.households.id)
)
.where(eq(schema.householdPendingMembers.telegramUserId, telegramUserId))
.limit(1)
const row = rows[0]
return row ? toHouseholdPendingMemberRecord(row) : null
}
}