From 773abf25316c04d74cef6d633787358d5a93330a Mon Sep 17 00:00:00 2001 From: whekin Date: Wed, 11 Mar 2026 13:44:38 +0400 Subject: [PATCH] feat(member): add household lifecycle states --- apps/bot/src/anonymous-feedback.test.ts | 7 +- apps/bot/src/bot-i18n.test.ts | 4 +- apps/bot/src/dm-assistant.test.ts | 5 +- apps/bot/src/finance-commands.test.ts | 4 +- apps/bot/src/household-setup.test.ts | 5 + apps/bot/src/index.ts | 10 + apps/bot/src/miniapp-admin.test.ts | 95 + apps/bot/src/miniapp-admin.ts | 187 +- apps/bot/src/miniapp-auth.test.ts | 29 +- apps/bot/src/miniapp-auth.ts | 1 + apps/bot/src/miniapp-billing.test.ts | 5 +- apps/bot/src/miniapp-dashboard.test.ts | 6 +- apps/bot/src/miniapp-locale.test.ts | 6 +- apps/bot/src/server.test.ts | 25 + apps/bot/src/server.ts | 12 + apps/miniapp/src/App.tsx | 97 +- apps/miniapp/src/i18n.ts | 14 + apps/miniapp/src/miniapp-api.ts | 33 + .../src/household-config-repository.ts | 60 + .../src/household-admin-service.test.ts | 7 +- .../src/household-onboarding-service.test.ts | 8 + .../src/household-onboarding-service.ts | 4 + .../src/household-setup-service.test.ts | 20 + .../src/locale-preference-service.test.ts | 4 +- .../src/miniapp-admin-service.test.ts | 47 + .../application/src/miniapp-admin-service.ts | 42 + packages/db/drizzle/0014_empty_risque.sql | 1 + packages/db/drizzle/meta/0014_snapshot.json | 2952 +++++++++++++++++ packages/db/drizzle/meta/_journal.json | 7 + packages/db/src/schema.ts | 1 + packages/ports/src/household-config.ts | 9 + packages/ports/src/index.ts | 2 + 32 files changed, 3671 insertions(+), 38 deletions(-) create mode 100644 packages/db/drizzle/0014_empty_risque.sql create mode 100644 packages/db/drizzle/meta/0014_snapshot.json diff --git a/apps/bot/src/anonymous-feedback.test.ts b/apps/bot/src/anonymous-feedback.test.ts index ff3c87b..ff0401f 100644 --- a/apps/bot/src/anonymous-feedback.test.ts +++ b/apps/bot/src/anonymous-feedback.test.ts @@ -178,6 +178,7 @@ function createHouseholdConfigurationRepository(): HouseholdConfigurationReposit householdId: input.householdId, telegramUserId: input.telegramUserId, displayName: input.displayName, + status: input.status ?? 'active', preferredLocale: input.preferredLocale ?? null, householdDefaultLocale: 'ru', rentShareWeight: 1, @@ -191,6 +192,7 @@ function createHouseholdConfigurationRepository(): HouseholdConfigurationReposit householdId: 'household-1', telegramUserId: '123456', displayName: 'Stan', + status: 'active', preferredLocale: null, householdDefaultLocale: 'ru', rentShareWeight: 1, @@ -212,6 +214,7 @@ function createHouseholdConfigurationRepository(): HouseholdConfigurationReposit householdId: 'household-1', telegramUserId, displayName: 'Stan', + status: 'active', preferredLocale: locale, householdDefaultLocale: 'ru', rentShareWeight: 1, @@ -249,7 +252,8 @@ function createHouseholdConfigurationRepository(): HouseholdConfigurationReposit isActive: input.isActive }), promoteHouseholdAdmin: async () => null, - updateHouseholdMemberRentShareWeight: async () => null + updateHouseholdMemberRentShareWeight: async () => null, + updateHouseholdMemberStatus: async () => null } } @@ -351,6 +355,7 @@ describe('registerAnonymousFeedback', () => { householdId: 'household-1', telegramUserId: '123456', displayName: 'Stan', + status: 'active', preferredLocale: 'en', householdDefaultLocale: 'ru', rentShareWeight: 1, diff --git a/apps/bot/src/bot-i18n.test.ts b/apps/bot/src/bot-i18n.test.ts index b616fb1..1ae548d 100644 --- a/apps/bot/src/bot-i18n.test.ts +++ b/apps/bot/src/bot-i18n.test.ts @@ -117,6 +117,7 @@ function createRepository(isAdmin = false): HouseholdConfigurationRepository { householdId: 'household-1', telegramUserId: '123456', displayName: 'Stan', + status: 'active', preferredLocale: 'ru', householdDefaultLocale: 'ru', rentShareWeight: 1, @@ -131,7 +132,8 @@ function createRepository(isAdmin = false): HouseholdConfigurationRepository { }, updateMemberPreferredLocale: async () => null, promoteHouseholdAdmin: async () => null, - updateHouseholdMemberRentShareWeight: async () => null + updateHouseholdMemberRentShareWeight: async () => null, + updateHouseholdMemberStatus: async () => null } } diff --git a/apps/bot/src/dm-assistant.test.ts b/apps/bot/src/dm-assistant.test.ts index 5c8a5ff..6579166 100644 --- a/apps/bot/src/dm-assistant.test.ts +++ b/apps/bot/src/dm-assistant.test.ts @@ -150,6 +150,7 @@ function createHouseholdRepository(): HouseholdConfigurationRepository { householdId: 'household-1', telegramUserId: '123456', displayName: 'Stan', + status: 'active', preferredLocale: null, householdDefaultLocale: 'en', rentShareWeight: 1, @@ -180,6 +181,7 @@ function createHouseholdRepository(): HouseholdConfigurationRepository { householdId: 'household-1', telegramUserId: '123456', displayName: 'Stan', + status: 'active', preferredLocale: null, householdDefaultLocale: 'en', rentShareWeight: 1, @@ -191,7 +193,8 @@ function createHouseholdRepository(): HouseholdConfigurationRepository { updateHouseholdDefaultLocale: async () => household, updateMemberPreferredLocale: async () => null, promoteHouseholdAdmin: async () => null, - updateHouseholdMemberRentShareWeight: async () => null + updateHouseholdMemberRentShareWeight: async () => null, + updateHouseholdMemberStatus: async () => null } } diff --git a/apps/bot/src/finance-commands.test.ts b/apps/bot/src/finance-commands.test.ts index 91bd5c6..ca01ae4 100644 --- a/apps/bot/src/finance-commands.test.ts +++ b/apps/bot/src/finance-commands.test.ts @@ -78,6 +78,7 @@ function createRepository(): HouseholdConfigurationRepository { householdId: 'household-1', telegramUserId: '123456', displayName: 'Stan', + status: 'active', preferredLocale: 'ru', householdDefaultLocale: 'ru', rentShareWeight: 1, @@ -109,7 +110,8 @@ function createRepository(): HouseholdConfigurationRepository { }, updateMemberPreferredLocale: async () => null, promoteHouseholdAdmin: async () => null, - updateHouseholdMemberRentShareWeight: async () => null + updateHouseholdMemberRentShareWeight: async () => null, + updateHouseholdMemberStatus: async () => null } } diff --git a/apps/bot/src/household-setup.test.ts b/apps/bot/src/household-setup.test.ts index 1a1bbf7..0e7a2bf 100644 --- a/apps/bot/src/household-setup.test.ts +++ b/apps/bot/src/household-setup.test.ts @@ -384,6 +384,7 @@ function createHouseholdConfigurationRepository(): HouseholdConfigurationReposit householdId: input.householdId, telegramUserId: input.telegramUserId, displayName: input.displayName, + status: input.status ?? existing?.status ?? 'active', preferredLocale: input.preferredLocale ?? existing?.preferredLocale ?? null, householdDefaultLocale: household.defaultLocale, rentShareWeight: input.rentShareWeight ?? existing?.rentShareWeight ?? 1, @@ -456,6 +457,7 @@ function createHouseholdConfigurationRepository(): HouseholdConfigurationReposit householdId: pending.householdId, telegramUserId: pending.telegramUserId, displayName: pending.displayName, + status: 'active', preferredLocale: null, householdDefaultLocale: pending.householdDefaultLocale, rentShareWeight: 1, @@ -492,6 +494,9 @@ function createHouseholdConfigurationRepository(): HouseholdConfigurationReposit }, async updateHouseholdMemberRentShareWeight() { return null + }, + async updateHouseholdMemberStatus() { + return null } } } diff --git a/apps/bot/src/index.ts b/apps/bot/src/index.ts index 92737c0..d9e42a1 100644 --- a/apps/bot/src/index.ts +++ b/apps/bot/src/index.ts @@ -49,6 +49,7 @@ import { createMiniAppPendingMembersHandler, createMiniAppPromoteMemberHandler, createMiniAppSettingsHandler, + createMiniAppUpdateMemberStatusHandler, createMiniAppUpdateMemberRentWeightHandler, createMiniAppUpdateSettingsHandler, createMiniAppUpsertUtilityCategoryHandler @@ -536,6 +537,15 @@ const server = createBotWebhookServer({ logger: getLogger('miniapp-admin') }) : undefined, + miniAppUpdateMemberStatus: householdOnboardingService + ? createMiniAppUpdateMemberStatusHandler({ + allowedOrigins: runtime.miniAppAllowedOrigins, + botToken: runtime.telegramBotToken, + onboardingService: householdOnboardingService, + miniAppAdminService: miniAppAdminService!, + logger: getLogger('miniapp-admin') + }) + : undefined, miniAppBillingCycle: householdOnboardingService ? createMiniAppBillingCycleHandler({ allowedOrigins: runtime.miniAppAllowedOrigins, diff --git a/apps/bot/src/miniapp-admin.test.ts b/apps/bot/src/miniapp-admin.test.ts index 26a353d..78d129e 100644 --- a/apps/bot/src/miniapp-admin.test.ts +++ b/apps/bot/src/miniapp-admin.test.ts @@ -11,6 +11,7 @@ import { createMiniAppPendingMembersHandler, createMiniAppPromoteMemberHandler, createMiniAppSettingsHandler, + createMiniAppUpdateMemberStatusHandler, createMiniAppUpdateSettingsHandler } from './miniapp-admin' import { buildMiniAppInitData } from './telegram-miniapp-test-helpers' @@ -75,6 +76,7 @@ function onboardingRepository(): HouseholdConfigurationRepository { householdId: household.householdId, telegramUserId: input.telegramUserId, displayName: input.displayName, + status: input.status ?? 'active', preferredLocale: input.preferredLocale ?? null, householdDefaultLocale: household.defaultLocale, rentShareWeight: 1, @@ -101,6 +103,7 @@ function onboardingRepository(): HouseholdConfigurationRepository { householdId: household.householdId, telegramUserId: '555777', displayName: 'Mia', + status: 'active', preferredLocale: null, householdDefaultLocale: household.defaultLocale, rentShareWeight: 1, @@ -118,6 +121,7 @@ function onboardingRepository(): HouseholdConfigurationRepository { householdId: household.householdId, telegramUserId, displayName: 'Mia', + status: 'active', preferredLocale: locale, householdDefaultLocale: household.defaultLocale, rentShareWeight: 1, @@ -162,6 +166,7 @@ function onboardingRepository(): HouseholdConfigurationRepository { householdId, telegramUserId: '123456', displayName: 'Stan', + status: 'active' as const, preferredLocale: null, householdDefaultLocale: household.defaultLocale, rentShareWeight: 1, @@ -183,11 +188,26 @@ function onboardingRepository(): HouseholdConfigurationRepository { householdId: household.householdId, telegramUserId: '123456', displayName: 'Stan', + status: 'active', preferredLocale: null, householdDefaultLocale: household.defaultLocale, rentShareWeight, isAdmin: false } + : null, + updateHouseholdMemberStatus: async (_householdId, memberId, status) => + memberId === 'member-123456' + ? { + id: memberId, + householdId: household.householdId, + telegramUserId: '123456', + displayName: 'Stan', + status, + preferredLocale: null, + householdDefaultLocale: household.defaultLocale, + rentShareWeight: 1, + isAdmin: false + } : null } } @@ -202,6 +222,7 @@ describe('createMiniAppPendingMembersHandler', () => { householdId: 'household-1', telegramUserId: '123456', displayName: 'Stan', + status: 'active', preferredLocale: null, householdDefaultLocale: 'ru', rentShareWeight: 1, @@ -265,6 +286,7 @@ describe('createMiniAppApproveMemberHandler', () => { householdId: 'household-1', telegramUserId: '123456', displayName: 'Stan', + status: 'active', preferredLocale: null, householdDefaultLocale: 'ru', rentShareWeight: 1, @@ -309,6 +331,7 @@ describe('createMiniAppApproveMemberHandler', () => { householdId: 'household-1', telegramUserId: '555777', displayName: 'Mia', + status: 'active', preferredLocale: null, householdDefaultLocale: 'ru', rentShareWeight: 1, @@ -328,6 +351,7 @@ describe('createMiniAppSettingsHandler', () => { householdId: 'household-1', telegramUserId: '123456', displayName: 'Stan', + status: 'active', preferredLocale: null, householdDefaultLocale: 'ru', rentShareWeight: 1, @@ -340,6 +364,7 @@ describe('createMiniAppSettingsHandler', () => { householdId: 'household-1', telegramUserId: '123456', displayName: 'Stan', + status: 'active', preferredLocale: null, householdDefaultLocale: 'ru', rentShareWeight: 1, @@ -405,6 +430,7 @@ describe('createMiniAppSettingsHandler', () => { householdId: 'household-1', telegramUserId: '123456', displayName: 'Stan', + status: 'active', preferredLocale: null, householdDefaultLocale: 'ru', rentShareWeight: 1, @@ -425,6 +451,7 @@ describe('createMiniAppUpdateSettingsHandler', () => { householdId: 'household-1', telegramUserId: '123456', displayName: 'Stan', + status: 'active', preferredLocale: null, householdDefaultLocale: 'ru', rentShareWeight: 1, @@ -495,6 +522,7 @@ describe('createMiniAppPromoteMemberHandler', () => { householdId: 'household-1', telegramUserId: '123456', displayName: 'Stan', + status: 'active', preferredLocale: null, householdDefaultLocale: 'ru', rentShareWeight: 1, @@ -539,6 +567,7 @@ describe('createMiniAppPromoteMemberHandler', () => { householdId: 'household-1', telegramUserId: '123456', displayName: 'Stan', + status: 'active', preferredLocale: null, householdDefaultLocale: 'ru', rentShareWeight: 1, @@ -547,3 +576,69 @@ describe('createMiniAppPromoteMemberHandler', () => { }) }) }) + +describe('createMiniAppUpdateMemberStatusHandler', () => { + test('updates a household member status for an authenticated admin', async () => { + const authDate = Math.floor(Date.now() / 1000) + const repository = onboardingRepository() + repository.listHouseholdMembersByTelegramUserId = async () => [ + { + id: 'member-123456', + householdId: 'household-1', + telegramUserId: '123456', + displayName: 'Stan', + status: 'active', + preferredLocale: null, + householdDefaultLocale: 'ru', + rentShareWeight: 1, + isAdmin: true + } + ] + + const handler = createMiniAppUpdateMemberStatusHandler({ + allowedOrigins: ['http://localhost:5173'], + botToken: 'test-bot-token', + onboardingService: createHouseholdOnboardingService({ + repository + }), + miniAppAdminService: createMiniAppAdminService(repository) + }) + + const response = await handler.handler( + new Request('http://localhost/api/miniapp/admin/members/status', { + method: 'POST', + headers: { + origin: 'http://localhost:5173', + 'content-type': 'application/json' + }, + body: JSON.stringify({ + initData: buildMiniAppInitData('test-bot-token', authDate, { + id: 123456, + first_name: 'Stan', + username: 'stanislav', + language_code: 'ru' + }), + memberId: 'member-123456', + status: 'away' + }) + }) + ) + + expect(response.status).toBe(200) + expect(await response.json()).toEqual({ + ok: true, + authorized: true, + member: { + id: 'member-123456', + householdId: 'household-1', + telegramUserId: '123456', + displayName: 'Stan', + status: 'away', + preferredLocale: null, + householdDefaultLocale: 'ru', + rentShareWeight: 1, + isAdmin: false + } + }) + }) +}) diff --git a/apps/bot/src/miniapp-admin.ts b/apps/bot/src/miniapp-admin.ts index 218a040..4d1a42b 100644 --- a/apps/bot/src/miniapp-admin.ts +++ b/apps/bot/src/miniapp-admin.ts @@ -1,6 +1,10 @@ import type { HouseholdOnboardingService, MiniAppAdminService } from '@household/application' import type { Logger } from '@household/observability' -import type { HouseholdBillingSettingsRecord } from '@household/ports' +import { + HOUSEHOLD_MEMBER_LIFECYCLE_STATUSES, + type HouseholdBillingSettingsRecord, + type HouseholdMemberLifecycleStatus +} from '@household/ports' import type { MiniAppSessionResult } from './miniapp-auth' import type { AssistantUsageTracker } from './dm-assistant' @@ -217,6 +221,42 @@ async function readRentWeightPayload(request: Request): Promise<{ } } +async function readMemberStatusPayload(request: Request): Promise<{ + initData: string + memberId: string + status: HouseholdMemberLifecycleStatus +}> { + const clonedRequest = request.clone() + const payload = await readMiniAppRequestPayload(request) + if (!payload.initData) { + throw new Error('Missing initData') + } + + const text = await clonedRequest.text() + let parsed: { memberId?: string; status?: string } + try { + parsed = JSON.parse(text) + } catch { + throw new Error('Invalid JSON body') + } + + const memberId = parsed.memberId?.trim() + const status = parsed.status?.trim().toLowerCase() + if (!memberId || !status) { + throw new Error('Missing member status fields') + } + + if (!(HOUSEHOLD_MEMBER_LIFECYCLE_STATUSES as readonly string[]).includes(status)) { + throw new Error('Invalid member status') + } + + return { + initData: payload.initData, + memberId, + status: status as HouseholdMemberLifecycleStatus + } +} + function serializeBillingSettings(settings: HouseholdBillingSettingsRecord) { return { householdId: settings.householdId, @@ -253,7 +293,15 @@ async function authenticateAdminSession( if (!session.authorized || !session.member) { return miniAppJsonResponse( - { ok: false, error: 'Access limited to active household members' }, + { ok: false, error: 'Admin access required for active household members' }, + 403, + origin + ) + } + + if (session.member.status !== 'active' || !session.member.isAdmin) { + return miniAppJsonResponse( + { ok: false, error: 'Admin access required for active household members' }, 403, origin ) @@ -305,9 +353,14 @@ export function createMiniAppPendingMembersHandler(options: { ) } - if (!session.authorized || !session.member) { + if ( + !session.authorized || + !session.member || + session.member.status !== 'active' || + !session.member.isAdmin + ) { return miniAppJsonResponse( - { ok: false, error: 'Access limited to active household members' }, + { ok: false, error: 'Admin access required for active household members' }, 403, origin ) @@ -442,9 +495,14 @@ export function createMiniAppUpdateSettingsHandler(options: { ) } - if (!session.authorized || !session.member) { + if ( + !session.authorized || + !session.member || + session.member.status !== 'active' || + !session.member.isAdmin + ) { return miniAppJsonResponse( - { ok: false, error: 'Access limited to active household members' }, + { ok: false, error: 'Admin access required for active household members' }, 403, origin ) @@ -545,9 +603,14 @@ export function createMiniAppUpsertUtilityCategoryHandler(options: { ) } - if (!session.authorized || !session.member) { + if ( + !session.authorized || + !session.member || + session.member.status !== 'active' || + !session.member.isAdmin + ) { return miniAppJsonResponse( - { ok: false, error: 'Access limited to active household members' }, + { ok: false, error: 'Admin access required for active household members' }, 403, origin ) @@ -636,9 +699,14 @@ export function createMiniAppPromoteMemberHandler(options: { ) } - if (!session.authorized || !session.member) { + if ( + !session.authorized || + !session.member || + session.member.status !== 'active' || + !session.member.isAdmin + ) { return miniAppJsonResponse( - { ok: false, error: 'Access limited to active household members' }, + { ok: false, error: 'Admin access required for active household members' }, 403, origin ) @@ -718,9 +786,14 @@ export function createMiniAppUpdateMemberRentWeightHandler(options: { ) } - if (!session.authorized || !session.member) { + if ( + !session.authorized || + !session.member || + session.member.status !== 'active' || + !session.member.isAdmin + ) { return miniAppJsonResponse( - { ok: false, error: 'Access limited to active household members' }, + { ok: false, error: 'Admin access required for active household members' }, 403, origin ) @@ -769,6 +842,87 @@ export function createMiniAppUpdateMemberRentWeightHandler(options: { } } +export function createMiniAppUpdateMemberStatusHandler(options: { + allowedOrigins: readonly string[] + botToken: string + onboardingService: HouseholdOnboardingService + miniAppAdminService: MiniAppAdminService + logger?: Logger +}): { + handler: (request: Request) => Promise +} { + const sessionService = createMiniAppSessionService({ + botToken: options.botToken, + onboardingService: options.onboardingService + }) + + return { + handler: async (request) => { + const origin = allowedMiniAppOrigin(request, options.allowedOrigins) + + if (request.method === 'OPTIONS') { + return miniAppJsonResponse({ ok: true }, 204, origin) + } + + if (request.method !== 'POST') { + return miniAppJsonResponse({ ok: false, error: 'Method Not Allowed' }, 405, origin) + } + + try { + const payload = await readMemberStatusPayload(request) + const session = await sessionService.authenticate({ + initData: payload.initData + }) + + if ( + !session || + !session.authorized || + !session.member || + session.member.status !== 'active' || + !session.member.isAdmin + ) { + return miniAppJsonResponse( + { ok: false, error: 'Admin access required for active household members' }, + session ? 403 : 401, + origin + ) + } + + const result = await options.miniAppAdminService.updateMemberStatus({ + householdId: session.member.householdId, + actorIsAdmin: session.member.isAdmin, + memberId: payload.memberId, + status: payload.status + }) + + if (result.status === 'rejected') { + return miniAppJsonResponse( + { + ok: false, + error: + result.reason === 'member_not_found' ? 'Member not found' : 'Admin access required' + }, + result.reason === 'member_not_found' ? 404 : 403, + origin + ) + } + + return miniAppJsonResponse( + { + ok: true, + authorized: true, + member: result.member + }, + 200, + origin + ) + } catch (error) { + return miniAppErrorResponse(error, origin, options.logger) + } + } + } +} + export function createMiniAppApproveMemberHandler(options: { allowedOrigins: readonly string[] botToken: string @@ -809,9 +963,14 @@ export function createMiniAppApproveMemberHandler(options: { ) } - if (!session.authorized || !session.member) { + if ( + !session.authorized || + !session.member || + session.member.status !== 'active' || + !session.member.isAdmin + ) { return miniAppJsonResponse( - { ok: false, error: 'Access limited to active household members' }, + { ok: false, error: 'Admin access required for active household members' }, 403, origin ) diff --git a/apps/bot/src/miniapp-auth.test.ts b/apps/bot/src/miniapp-auth.test.ts index 6c02458..d84df97 100644 --- a/apps/bot/src/miniapp-auth.test.ts +++ b/apps/bot/src/miniapp-auth.test.ts @@ -3,6 +3,7 @@ import { describe, expect, test } from 'bun:test' import { createHouseholdOnboardingService } from '@household/application' import type { HouseholdConfigurationRepository, + HouseholdMemberRecord, HouseholdTopicBindingRecord } from '@household/ports' @@ -19,19 +20,7 @@ function onboardingRepository(): HouseholdConfigurationRepository { defaultLocale: 'ru' as const } let joinToken: string | null = 'join-token' - const members = new Map< - string, - { - id: string - householdId: string - telegramUserId: string - displayName: string - preferredLocale: 'en' | 'ru' | null - householdDefaultLocale: 'en' | 'ru' - rentShareWeight: number - isAdmin: boolean - } - >() + const members = new Map() let pending: { householdId: string householdName: string @@ -97,6 +86,7 @@ function onboardingRepository(): HouseholdConfigurationRepository { householdId: household.householdId, telegramUserId: input.telegramUserId, displayName: input.displayName, + status: input.status ?? 'active', preferredLocale: input.preferredLocale ?? null, householdDefaultLocale: household.defaultLocale, rentShareWeight: 1, @@ -118,11 +108,12 @@ function onboardingRepository(): HouseholdConfigurationRepository { return null } - const member = { + const member: HouseholdMemberRecord = { id: `member-${pending.telegramUserId}`, householdId: household.householdId, telegramUserId: pending.telegramUserId, displayName: pending.displayName, + status: 'active', preferredLocale: null, householdDefaultLocale: household.defaultLocale, rentShareWeight: 1, @@ -154,6 +145,15 @@ function onboardingRepository(): HouseholdConfigurationRepository { } : null }, + updateHouseholdMemberStatus: async (_householdId, memberId, status) => { + const member = [...members.values()].find((entry) => entry.id === memberId) + return member + ? { + ...member, + status + } + : null + }, getHouseholdBillingSettings: async (householdId) => ({ householdId, settlementCurrency: 'GEL', @@ -232,6 +232,7 @@ describe('createMiniAppAuthHandler', () => { authorized: true, member: { displayName: 'Stan', + status: 'active', isAdmin: true, preferredLocale: null, householdDefaultLocale: 'ru' diff --git a/apps/bot/src/miniapp-auth.ts b/apps/bot/src/miniapp-auth.ts index e11002e..3f13c30 100644 --- a/apps/bot/src/miniapp-auth.ts +++ b/apps/bot/src/miniapp-auth.ts @@ -101,6 +101,7 @@ export interface MiniAppSessionResult { id: string householdId: string displayName: string + status: 'active' | 'away' | 'left' isAdmin: boolean preferredLocale: SupportedLocale | null householdDefaultLocale: SupportedLocale diff --git a/apps/bot/src/miniapp-billing.test.ts b/apps/bot/src/miniapp-billing.test.ts index 77b137b..a948bb9 100644 --- a/apps/bot/src/miniapp-billing.test.ts +++ b/apps/bot/src/miniapp-billing.test.ts @@ -71,6 +71,7 @@ function onboardingRepository(): HouseholdConfigurationRepository { householdId: household.householdId, telegramUserId: input.telegramUserId, displayName: input.displayName, + status: input.status ?? 'active', preferredLocale: input.preferredLocale ?? null, householdDefaultLocale: household.defaultLocale, rentShareWeight: 1, @@ -115,6 +116,7 @@ function onboardingRepository(): HouseholdConfigurationRepository { householdId: 'household-1', telegramUserId: '123456', displayName: 'Stan', + status: 'active', preferredLocale: null, householdDefaultLocale: 'ru', rentShareWeight: 1, @@ -129,7 +131,8 @@ function onboardingRepository(): HouseholdConfigurationRepository { }), updateMemberPreferredLocale: async () => null, promoteHouseholdAdmin: async () => null, - updateHouseholdMemberRentShareWeight: async () => null + updateHouseholdMemberRentShareWeight: async () => null, + updateHouseholdMemberStatus: async () => null } } diff --git a/apps/bot/src/miniapp-dashboard.test.ts b/apps/bot/src/miniapp-dashboard.test.ts index a8a67c2..ffa59fd 100644 --- a/apps/bot/src/miniapp-dashboard.test.ts +++ b/apps/bot/src/miniapp-dashboard.test.ts @@ -178,6 +178,7 @@ function onboardingRepository(): HouseholdConfigurationRepository { householdId: household.householdId, telegramUserId: input.telegramUserId, displayName: input.displayName, + status: input.status ?? 'active', preferredLocale: input.preferredLocale ?? null, householdDefaultLocale: household.defaultLocale, rentShareWeight: 1, @@ -225,7 +226,8 @@ function onboardingRepository(): HouseholdConfigurationRepository { isActive: input.isActive }), promoteHouseholdAdmin: async () => null, - updateHouseholdMemberRentShareWeight: async () => null + updateHouseholdMemberRentShareWeight: async () => null, + updateHouseholdMemberStatus: async () => null } } @@ -252,6 +254,7 @@ describe('createMiniAppDashboardHandler', () => { householdId: 'household-1', telegramUserId: '123456', displayName: 'Stan', + status: 'active', preferredLocale: null, householdDefaultLocale: 'ru', rentShareWeight: 1, @@ -354,6 +357,7 @@ describe('createMiniAppDashboardHandler', () => { householdId: 'household-1', telegramUserId: '123456', displayName: 'Stan', + status: 'active', preferredLocale: null, householdDefaultLocale: 'ru', rentShareWeight: 1, diff --git a/apps/bot/src/miniapp-locale.test.ts b/apps/bot/src/miniapp-locale.test.ts index 3d2075e..aeffd94 100644 --- a/apps/bot/src/miniapp-locale.test.ts +++ b/apps/bot/src/miniapp-locale.test.ts @@ -31,6 +31,7 @@ function repository(): HouseholdConfigurationRepository { householdId: household.householdId, telegramUserId: '123456', displayName: 'Stan', + status: 'active', preferredLocale: null, householdDefaultLocale: household.defaultLocale, rentShareWeight: 1, @@ -44,6 +45,7 @@ function repository(): HouseholdConfigurationRepository { householdId: household.householdId, telegramUserId: '222222', displayName: 'Mia', + status: 'active', preferredLocale: null, householdDefaultLocale: household.defaultLocale, rentShareWeight: 1, @@ -93,6 +95,7 @@ function repository(): HouseholdConfigurationRepository { householdId: input.householdId, telegramUserId: input.telegramUserId, displayName: input.displayName, + status: input.status ?? 'active', preferredLocale: input.preferredLocale ?? null, householdDefaultLocale: household.defaultLocale, rentShareWeight: 1, @@ -161,7 +164,8 @@ function repository(): HouseholdConfigurationRepository { isActive: input.isActive }), promoteHouseholdAdmin: async () => null, - updateHouseholdMemberRentShareWeight: async () => null + updateHouseholdMemberRentShareWeight: async () => null, + updateHouseholdMemberStatus: async () => null } } diff --git a/apps/bot/src/server.test.ts b/apps/bot/src/server.test.ts index 40d6600..e7f1807 100644 --- a/apps/bot/src/server.test.ts +++ b/apps/bot/src/server.test.ts @@ -82,6 +82,15 @@ describe('createBotWebhookServer', () => { } }) }, + miniAppUpdateMemberStatus: { + handler: async () => + new Response(JSON.stringify({ ok: true, authorized: true, member: {} }), { + status: 200, + headers: { + 'content-type': 'application/json; charset=utf-8' + } + }) + }, miniAppBillingCycle: { handler: async () => new Response(JSON.stringify({ ok: true, authorized: true, cycleState: {} }), { @@ -347,6 +356,22 @@ describe('createBotWebhookServer', () => { }) }) + test('accepts mini app member status update request', async () => { + const response = await server.fetch( + new Request('http://localhost/api/miniapp/admin/members/status', { + method: 'POST', + body: JSON.stringify({ initData: 'payload' }) + }) + ) + + expect(response.status).toBe(200) + expect(await response.json()).toEqual({ + ok: true, + authorized: true, + member: {} + }) + }) + test('accepts mini app billing cycle request', async () => { const response = await server.fetch( new Request('http://localhost/api/miniapp/admin/billing-cycle', { diff --git a/apps/bot/src/server.ts b/apps/bot/src/server.ts index 2c50056..6cefea1 100644 --- a/apps/bot/src/server.ts +++ b/apps/bot/src/server.ts @@ -62,6 +62,12 @@ export interface BotWebhookServerOptions { handler: (request: Request) => Promise } | undefined + miniAppUpdateMemberStatus?: + | { + path?: string + handler: (request: Request) => Promise + } + | undefined miniAppBillingCycle?: | { path?: string @@ -186,6 +192,8 @@ export function createBotWebhookServer(options: BotWebhookServerOptions): { options.miniAppPromoteMember?.path ?? '/api/miniapp/admin/members/promote' const miniAppUpdateMemberRentWeightPath = options.miniAppUpdateMemberRentWeight?.path ?? '/api/miniapp/admin/members/rent-weight' + const miniAppUpdateMemberStatusPath = + options.miniAppUpdateMemberStatus?.path ?? '/api/miniapp/admin/members/status' const miniAppBillingCyclePath = options.miniAppBillingCycle?.path ?? '/api/miniapp/admin/billing-cycle' const miniAppOpenCyclePath = @@ -268,6 +276,10 @@ export function createBotWebhookServer(options: BotWebhookServerOptions): { return await options.miniAppUpdateMemberRentWeight.handler(request) } + if (options.miniAppUpdateMemberStatus && url.pathname === miniAppUpdateMemberStatusPath) { + return await options.miniAppUpdateMemberStatus.handler(request) + } + if (options.miniAppBillingCycle && url.pathname === miniAppBillingCyclePath) { return await options.miniAppBillingCycle.handler(request) } diff --git a/apps/miniapp/src/App.tsx b/apps/miniapp/src/App.tsx index 4892187..f07ac09 100644 --- a/apps/miniapp/src/App.tsx +++ b/apps/miniapp/src/App.tsx @@ -17,6 +17,7 @@ import { joinMiniAppHousehold, openMiniAppBillingCycle, promoteMiniAppMember, + updateMiniAppMemberStatus, updateMiniAppMemberRentWeight, type MiniAppAdminCycleState, type MiniAppAdminSettingsPayload, @@ -56,6 +57,7 @@ type SessionState = member: { id: string displayName: string + status: 'active' | 'away' | 'left' isAdmin: boolean preferredLocale: Locale | null householdDefaultLocale: Locale @@ -95,6 +97,7 @@ const demoSession: Extract = { member: { id: 'demo-member', displayName: 'Demo Resident', + status: 'active', isAdmin: false, preferredLocale: 'en', householdDefaultLocale: 'en' @@ -278,7 +281,11 @@ function App() { const [approvingTelegramUserId, setApprovingTelegramUserId] = createSignal(null) const [promotingMemberId, setPromotingMemberId] = createSignal(null) const [savingRentWeightMemberId, setSavingRentWeightMemberId] = createSignal(null) + const [savingMemberStatusId, setSavingMemberStatusId] = createSignal(null) const [rentWeightDrafts, setRentWeightDrafts] = createSignal>({}) + const [memberStatusDrafts, setMemberStatusDrafts] = createSignal< + Record + >({}) const [savingMemberLocale, setSavingMemberLocale] = createSignal(false) const [savingHouseholdLocale, setSavingHouseholdLocale] = createSignal(false) const [savingBillingSettings, setSavingBillingSettings] = createSignal(false) @@ -382,6 +389,17 @@ function App() { } } + function memberStatusLabel(status: 'active' | 'away' | 'left'): string { + switch (status) { + case 'active': + return copy().memberStatusActive + case 'away': + return copy().memberStatusAway + case 'left': + return copy().memberStatusLeft + } + } + async function loadDashboard(initData: string) { try { const nextDashboard = await fetchMiniAppDashboard(initData) @@ -420,6 +438,9 @@ function App() { payload.members.map((member) => [member.id, String(member.rentShareWeight)]) ) ) + setMemberStatusDrafts( + Object.fromEntries(payload.members.map((member) => [member.id, member.status])) + ) setCycleForm((current) => ({ ...current, rentCurrency: payload.settings.rentCurrency, @@ -1231,6 +1252,35 @@ function App() { } } + async function handleSaveMemberStatus(memberId: string) { + const initData = webApp?.initData?.trim() + const currentReady = readySession() + const nextStatus = memberStatusDrafts()[memberId] + if (!initData || currentReady?.mode !== 'live' || !currentReady.member.isAdmin || !nextStatus) { + return + } + + setSavingMemberStatusId(memberId) + + try { + const member = await updateMiniAppMemberStatus(initData, memberId, nextStatus) + setAdminSettings((current) => + current + ? { + ...current, + members: current.members.map((item) => (item.id === member.id ? member : item)) + } + : current + ) + setMemberStatusDrafts((current) => ({ + ...current, + [member.id]: member.status + })) + } finally { + setSavingMemberStatusId(null) + } + } + const renderPanel = () => { switch (activeNav()) { case 'balances': @@ -2372,9 +2422,31 @@ function App() {
{member.displayName} - {member.isAdmin ? copy().adminTag : copy().residentTag} + + {member.isAdmin ? copy().adminTag : copy().residentTag} + {` · ${memberStatusLabel(member.status)}`} +
+
+

@@ -2802,6 +2889,14 @@ function App() {

{copy().overviewTitle}

{readySession()?.member.displayName}

+

+ {copy().memberStatusSummary.replace( + '{status}', + readySession()?.member.status + ? memberStatusLabel(readySession()!.member.status) + : copy().memberStatusActive + )} +

{renderPanel()}
diff --git a/apps/miniapp/src/i18n.ts b/apps/miniapp/src/i18n.ts index 393091c..41c3452 100644 --- a/apps/miniapp/src/i18n.ts +++ b/apps/miniapp/src/i18n.ts @@ -138,6 +138,13 @@ export const dictionary = { savingCategory: 'Saving…', adminsTitle: 'Admins', adminsBody: 'Promote trusted household members so they can manage billing and approvals.', + memberStatusLabel: 'Member status', + saveMemberStatusAction: 'Save status', + savingMemberStatus: 'Saving status…', + memberStatusActive: 'Active', + memberStatusAway: 'Away', + memberStatusLeft: 'Left', + memberStatusSummary: 'Your household status: {status}.', rentWeightLabel: 'Rent weight', saveRentWeightAction: 'Save rent weight', savingRentWeight: 'Saving weight…', @@ -296,6 +303,13 @@ export const dictionary = { adminsTitle: 'Админы', adminsBody: 'Повышай доверенных участников, чтобы они могли управлять биллингом и подтверждениями.', + memberStatusLabel: 'Статус участника', + saveMemberStatusAction: 'Сохранить статус', + savingMemberStatus: 'Сохраняем статус…', + memberStatusActive: 'Активный', + memberStatusAway: 'В отъезде', + memberStatusLeft: 'Выехал', + memberStatusSummary: 'Твой статус в household: {status}.', rentWeightLabel: 'Вес аренды', saveRentWeightAction: 'Сохранить вес аренды', savingRentWeight: 'Сохраняем вес…', diff --git a/apps/miniapp/src/miniapp-api.ts b/apps/miniapp/src/miniapp-api.ts index ec7d20e..35d37bd 100644 --- a/apps/miniapp/src/miniapp-api.ts +++ b/apps/miniapp/src/miniapp-api.ts @@ -6,6 +6,7 @@ export interface MiniAppSession { id: string householdId: string displayName: string + status: 'active' | 'away' | 'left' isAdmin: boolean preferredLocale: 'en' | 'ru' | null householdDefaultLocale: 'en' | 'ru' @@ -39,6 +40,7 @@ export interface MiniAppPendingMember { export interface MiniAppMember { id: string displayName: string + status: 'active' | 'away' | 'left' rentShareWeight: number isAdmin: boolean } @@ -514,6 +516,37 @@ export async function updateMiniAppMemberRentWeight( return payload.member } +export async function updateMiniAppMemberStatus( + initData: string, + memberId: string, + status: 'active' | 'away' | 'left' +): Promise { + const response = await fetch(`${apiBaseUrl()}/api/miniapp/admin/members/status`, { + method: 'POST', + headers: { + 'content-type': 'application/json' + }, + body: JSON.stringify({ + initData, + memberId, + status + }) + }) + + const payload = (await response.json()) as { + ok: boolean + authorized?: boolean + member?: MiniAppMember + error?: string + } + + if (!response.ok || !payload.member) { + throw new Error(payload.error ?? 'Failed to update member status') + } + + return payload.member +} + export async function fetchMiniAppBillingCycle(initData: string): Promise { const response = await fetch(`${apiBaseUrl()}/api/miniapp/admin/billing-cycle`, { method: 'POST', diff --git a/packages/adapters-db/src/household-config-repository.ts b/packages/adapters-db/src/household-config-repository.ts index 233d650..f874851 100644 --- a/packages/adapters-db/src/household-config-repository.ts +++ b/packages/adapters-db/src/household-config-repository.ts @@ -8,10 +8,12 @@ import { type CurrencyCode } from '@household/domain' import { + HOUSEHOLD_MEMBER_LIFECYCLE_STATUSES, HOUSEHOLD_TOPIC_ROLES, type HouseholdBillingSettingsRecord, type HouseholdConfigurationRepository, type HouseholdJoinTokenRecord, + type HouseholdMemberLifecycleStatus, type HouseholdMemberRecord, type HouseholdPendingMemberRecord, type HouseholdTelegramChatRecord, @@ -32,6 +34,16 @@ function normalizeTopicRole(role: string): HouseholdTopicRole { throw new Error(`Unsupported household topic role: ${role}`) } +function normalizeMemberLifecycleStatus(raw: string): HouseholdMemberLifecycleStatus { + const normalized = raw.trim().toLowerCase() + + if ((HOUSEHOLD_MEMBER_LIFECYCLE_STATUSES as readonly string[]).includes(normalized)) { + return normalized as HouseholdMemberLifecycleStatus + } + + throw new Error(`Unsupported household member lifecycle status: ${raw}`) +} + function toHouseholdTelegramChatRecord(row: { householdId: string householdName: string @@ -113,6 +125,7 @@ function toHouseholdMemberRecord(row: { householdId: string telegramUserId: string displayName: string + lifecycleStatus: string preferredLocale: string | null defaultLocale: string rentShareWeight: number @@ -128,6 +141,7 @@ function toHouseholdMemberRecord(row: { householdId: row.householdId, telegramUserId: row.telegramUserId, displayName: row.displayName, + status: normalizeMemberLifecycleStatus(row.lifecycleStatus), preferredLocale: normalizeSupportedLocale(row.preferredLocale), householdDefaultLocale, rentShareWeight: row.rentShareWeight, @@ -775,6 +789,7 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): { householdId: input.householdId, telegramUserId: input.telegramUserId, displayName: input.displayName, + lifecycleStatus: input.status ?? 'active', preferredLocale: input.preferredLocale ?? null, rentShareWeight: input.rentShareWeight ?? 1, isAdmin: input.isAdmin ? 1 : 0 @@ -783,6 +798,7 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): { target: [schema.members.householdId, schema.members.telegramUserId], set: { displayName: input.displayName, + lifecycleStatus: input.status ?? schema.members.lifecycleStatus, preferredLocale: input.preferredLocale ?? schema.members.preferredLocale, rentShareWeight: input.rentShareWeight ?? schema.members.rentShareWeight, ...(input.isAdmin @@ -797,6 +813,7 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): { householdId: schema.members.householdId, telegramUserId: schema.members.telegramUserId, displayName: schema.members.displayName, + lifecycleStatus: schema.members.lifecycleStatus, preferredLocale: schema.members.preferredLocale, rentShareWeight: schema.members.rentShareWeight, isAdmin: schema.members.isAdmin @@ -825,6 +842,7 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): { householdId: schema.members.householdId, telegramUserId: schema.members.telegramUserId, displayName: schema.members.displayName, + lifecycleStatus: schema.members.lifecycleStatus, preferredLocale: schema.members.preferredLocale, rentShareWeight: schema.members.rentShareWeight, defaultLocale: schema.households.defaultLocale, @@ -851,6 +869,7 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): { householdId: schema.members.householdId, telegramUserId: schema.members.telegramUserId, displayName: schema.members.displayName, + lifecycleStatus: schema.members.lifecycleStatus, preferredLocale: schema.members.preferredLocale, rentShareWeight: schema.members.rentShareWeight, defaultLocale: schema.households.defaultLocale, @@ -1033,6 +1052,7 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): { householdId: schema.members.householdId, telegramUserId: schema.members.telegramUserId, displayName: schema.members.displayName, + lifecycleStatus: schema.members.lifecycleStatus, preferredLocale: schema.members.preferredLocale, rentShareWeight: schema.members.rentShareWeight, defaultLocale: schema.households.defaultLocale, @@ -1104,6 +1124,7 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): { householdId: pending.householdId, telegramUserId: pending.telegramUserId, displayName: pending.displayName, + lifecycleStatus: 'active', preferredLocale: normalizeSupportedLocale(pending.languageCode), rentShareWeight: 1, isAdmin: input.isAdmin ? 1 : 0 @@ -1112,6 +1133,7 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): { target: [schema.members.householdId, schema.members.telegramUserId], set: { displayName: pending.displayName, + lifecycleStatus: 'active', preferredLocale: normalizeSupportedLocale(pending.languageCode) ?? schema.members.preferredLocale, ...(input.isAdmin @@ -1126,6 +1148,7 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): { householdId: schema.members.householdId, telegramUserId: schema.members.telegramUserId, displayName: schema.members.displayName, + lifecycleStatus: schema.members.lifecycleStatus, preferredLocale: schema.members.preferredLocale, rentShareWeight: schema.members.rentShareWeight, isAdmin: schema.members.isAdmin @@ -1198,6 +1221,7 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): { householdId: schema.members.householdId, telegramUserId: schema.members.telegramUserId, displayName: schema.members.displayName, + lifecycleStatus: schema.members.lifecycleStatus, preferredLocale: schema.members.preferredLocale, rentShareWeight: schema.members.rentShareWeight, isAdmin: schema.members.isAdmin @@ -1231,6 +1255,7 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): { householdId: schema.members.householdId, telegramUserId: schema.members.telegramUserId, displayName: schema.members.displayName, + lifecycleStatus: schema.members.lifecycleStatus, preferredLocale: schema.members.preferredLocale, rentShareWeight: schema.members.rentShareWeight, isAdmin: schema.members.isAdmin @@ -1264,6 +1289,7 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): { householdId: schema.members.householdId, telegramUserId: schema.members.telegramUserId, displayName: schema.members.displayName, + lifecycleStatus: schema.members.lifecycleStatus, preferredLocale: schema.members.preferredLocale, rentShareWeight: schema.members.rentShareWeight, isAdmin: schema.members.isAdmin @@ -1279,6 +1305,40 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): { throw new Error('Failed to resolve household chat after rent weight update') } + return toHouseholdMemberRecord({ + ...row, + defaultLocale: household.defaultLocale + }) + }, + + async updateHouseholdMemberStatus(householdId, memberId, status) { + const rows = await db + .update(schema.members) + .set({ + lifecycleStatus: status + }) + .where(and(eq(schema.members.householdId, householdId), eq(schema.members.id, memberId))) + .returning({ + id: schema.members.id, + householdId: schema.members.householdId, + telegramUserId: schema.members.telegramUserId, + displayName: schema.members.displayName, + lifecycleStatus: schema.members.lifecycleStatus, + preferredLocale: schema.members.preferredLocale, + rentShareWeight: schema.members.rentShareWeight, + isAdmin: schema.members.isAdmin + }) + + const row = rows[0] + if (!row) { + return null + } + + const household = await this.getHouseholdChatByHouseholdId(householdId) + if (!household) { + throw new Error('Failed to resolve household chat after member status update') + } + return toHouseholdMemberRecord({ ...row, defaultLocale: household.defaultLocale diff --git a/packages/application/src/household-admin-service.test.ts b/packages/application/src/household-admin-service.test.ts index 120397a..6d7157b 100644 --- a/packages/application/src/household-admin-service.test.ts +++ b/packages/application/src/household-admin-service.test.ts @@ -28,6 +28,7 @@ function createRepositoryStub() { householdId: household.householdId, telegramUserId: '1', displayName: 'Stan', + status: 'active', preferredLocale: null, householdDefaultLocale: household.defaultLocale, rentShareWeight: 1, @@ -94,6 +95,7 @@ function createRepositoryStub() { householdId: input.householdId, telegramUserId: input.telegramUserId, displayName: input.displayName, + status: input.status ?? 'active', preferredLocale: input.preferredLocale ?? null, householdDefaultLocale: household.defaultLocale, rentShareWeight: 1, @@ -121,6 +123,7 @@ function createRepositoryStub() { householdId: pending.householdId, telegramUserId: pending.telegramUserId, displayName: pending.displayName, + status: 'active', preferredLocale: null, householdDefaultLocale: household.defaultLocale, rentShareWeight: 1, @@ -174,7 +177,8 @@ function createRepositoryStub() { isActive: input.isActive }), promoteHouseholdAdmin: async () => null, - updateHouseholdMemberRentShareWeight: async () => null + updateHouseholdMemberRentShareWeight: async () => null, + updateHouseholdMemberStatus: async () => null } return { @@ -243,6 +247,7 @@ describe('createHouseholdAdminService', () => { householdId: 'household-1', telegramUserId: '2', displayName: 'Alice', + status: 'active', preferredLocale: null, householdDefaultLocale: 'ru', rentShareWeight: 1, diff --git a/packages/application/src/household-onboarding-service.test.ts b/packages/application/src/household-onboarding-service.test.ts index 140a8fd..9965026 100644 --- a/packages/application/src/household-onboarding-service.test.ts +++ b/packages/application/src/household-onboarding-service.test.ts @@ -99,6 +99,7 @@ function createRepositoryStub() { householdId: input.householdId, telegramUserId: input.telegramUserId, displayName: input.displayName, + status: input.status ?? 'active', preferredLocale: input.preferredLocale ?? null, householdDefaultLocale: household.defaultLocale, rentShareWeight: 1, @@ -133,6 +134,7 @@ function createRepositoryStub() { householdId: pending.householdId, telegramUserId: pending.telegramUserId, displayName: pending.displayName, + status: 'active', preferredLocale: null, householdDefaultLocale: household.defaultLocale, rentShareWeight: 1, @@ -198,6 +200,9 @@ function createRepositoryStub() { }, async updateHouseholdMemberRentShareWeight() { return null + }, + async updateHouseholdMemberStatus() { + return null } } @@ -325,6 +330,7 @@ describe('createHouseholdOnboardingService', () => { id: 'member-42', householdId: 'household-1', displayName: 'Stan', + status: 'active', preferredLocale: null, householdDefaultLocale: 'ru', rentShareWeight: 1, @@ -340,6 +346,7 @@ describe('createHouseholdOnboardingService', () => { householdId: 'household-1', telegramUserId: '42', displayName: 'Stan', + status: 'active', preferredLocale: null, householdDefaultLocale: 'ru', rentShareWeight: 1, @@ -358,6 +365,7 @@ describe('createHouseholdOnboardingService', () => { householdId: 'household-2', telegramUserId: '42', displayName: 'Stan elsewhere', + status: 'active', preferredLocale: null, householdDefaultLocale: 'ru', rentShareWeight: 1, diff --git a/packages/application/src/household-onboarding-service.ts b/packages/application/src/household-onboarding-service.ts index 6ae0f7e..cf23053 100644 --- a/packages/application/src/household-onboarding-service.ts +++ b/packages/application/src/household-onboarding-service.ts @@ -17,6 +17,7 @@ export type HouseholdMiniAppAccess = id: string householdId: string displayName: string + status: HouseholdMemberRecord['status'] isAdmin: boolean preferredLocale: SupportedLocale | null householdDefaultLocale: SupportedLocale @@ -68,6 +69,7 @@ export interface HouseholdOnboardingService { id: string householdId: string displayName: string + status: HouseholdMemberRecord['status'] isAdmin: boolean preferredLocale: SupportedLocale | null householdDefaultLocale: SupportedLocale @@ -84,6 +86,7 @@ function toMember(member: HouseholdMemberRecord): { id: string householdId: string displayName: string + status: HouseholdMemberRecord['status'] isAdmin: boolean preferredLocale: SupportedLocale | null householdDefaultLocale: SupportedLocale @@ -93,6 +96,7 @@ function toMember(member: HouseholdMemberRecord): { id: member.id, householdId: member.householdId, displayName: member.displayName, + status: member.status, isAdmin: member.isAdmin, preferredLocale: member.preferredLocale, householdDefaultLocale: member.householdDefaultLocale, diff --git a/packages/application/src/household-setup-service.test.ts b/packages/application/src/household-setup-service.test.ts index ec3d874..38fba90 100644 --- a/packages/application/src/household-setup-service.test.ts +++ b/packages/application/src/household-setup-service.test.ts @@ -174,6 +174,7 @@ function createRepositoryStub() { householdId: input.householdId, telegramUserId: input.telegramUserId, displayName: input.displayName, + status: input.status ?? existing?.status ?? 'active', preferredLocale: input.preferredLocale ?? existing?.preferredLocale ?? null, householdDefaultLocale: [...households.values()].find((household) => household.householdId === input.householdId) @@ -215,6 +216,7 @@ function createRepositoryStub() { householdId: pending.householdId, telegramUserId: pending.telegramUserId, displayName: pending.displayName, + status: 'active', preferredLocale: null, householdDefaultLocale: [...households.values()].find( @@ -319,6 +321,22 @@ function createRepositoryStub() { } members.set(`${householdId}:${member.telegramUserId}`, next) return next + }, + + async updateHouseholdMemberStatus(householdId, memberId, status) { + const member = [...members.values()].find( + (entry) => entry.householdId === householdId && entry.id === memberId + ) + if (!member) { + return null + } + + const next = { + ...member, + status + } + members.set(`${householdId}:${member.telegramUserId}`, next) + return next } } @@ -353,6 +371,7 @@ describe('createHouseholdSetupService', () => { householdId: result.household.householdId, telegramUserId: '42', displayName: 'Stan', + status: 'active', preferredLocale: null, householdDefaultLocale: 'ru', rentShareWeight: 1, @@ -391,6 +410,7 @@ describe('createHouseholdSetupService', () => { householdId: result.household.householdId, telegramUserId: '77', displayName: 'Mia', + status: 'active', preferredLocale: null, householdDefaultLocale: 'ru', rentShareWeight: 1, diff --git a/packages/application/src/locale-preference-service.test.ts b/packages/application/src/locale-preference-service.test.ts index 6ebdd64..7353bd8 100644 --- a/packages/application/src/locale-preference-service.test.ts +++ b/packages/application/src/locale-preference-service.test.ts @@ -19,6 +19,7 @@ function createRepository(): HouseholdConfigurationRepository { householdId: 'household-1', telegramUserId: '123456', displayName: 'Stan', + status: 'active', preferredLocale: null, householdDefaultLocale: 'ru', rentShareWeight: 1, @@ -109,7 +110,8 @@ function createRepository(): HouseholdConfigurationRepository { isActive: input.isActive }), promoteHouseholdAdmin: async () => null, - updateHouseholdMemberRentShareWeight: async () => null + updateHouseholdMemberRentShareWeight: async () => null, + updateHouseholdMemberStatus: async () => null } } diff --git a/packages/application/src/miniapp-admin-service.test.ts b/packages/application/src/miniapp-admin-service.test.ts index 4d5b308..1d994ce 100644 --- a/packages/application/src/miniapp-admin-service.test.ts +++ b/packages/application/src/miniapp-admin-service.test.ts @@ -61,6 +61,7 @@ function repository(): HouseholdConfigurationRepository { householdId: input.householdId, telegramUserId: input.telegramUserId, displayName: input.displayName, + status: input.status ?? 'active', preferredLocale: input.preferredLocale ?? null, householdDefaultLocale: 'ru', rentShareWeight: 1, @@ -87,6 +88,7 @@ function repository(): HouseholdConfigurationRepository { householdId: input.householdId, telegramUserId: input.telegramUserId, displayName: 'Stan', + status: 'active', preferredLocale: null, householdDefaultLocale: 'ru', rentShareWeight: 1, @@ -108,6 +110,7 @@ function repository(): HouseholdConfigurationRepository { householdId: 'household-1', telegramUserId, displayName: 'Stan', + status: 'active', preferredLocale: locale, householdDefaultLocale: 'ru', rentShareWeight: 1, @@ -152,6 +155,7 @@ function repository(): HouseholdConfigurationRepository { householdId, telegramUserId: '123456', displayName: 'Stan', + status: 'active', preferredLocale: null, householdDefaultLocale: 'ru', rentShareWeight: 1, @@ -165,11 +169,26 @@ function repository(): HouseholdConfigurationRepository { householdId: 'household-1', telegramUserId: '123456', displayName: 'Stan', + status: 'active', preferredLocale: null, householdDefaultLocale: 'ru', rentShareWeight, isAdmin: false } + : null, + updateHouseholdMemberStatus: async (_householdId, memberId, status) => + memberId === 'member-123456' + ? { + id: memberId, + householdId: 'household-1', + telegramUserId: '123456', + displayName: 'Stan', + status, + preferredLocale: null, + householdDefaultLocale: 'ru', + rentShareWeight: 1, + isAdmin: false + } : null } } @@ -318,6 +337,7 @@ describe('createMiniAppAdminService', () => { householdId: 'household-1', telegramUserId: '123456', displayName: 'Stan', + status: 'active', preferredLocale: null, householdDefaultLocale: 'ru', rentShareWeight: 1, @@ -342,6 +362,7 @@ describe('createMiniAppAdminService', () => { householdId: 'household-1', telegramUserId: '123456', displayName: 'Stan', + status: 'active', preferredLocale: null, householdDefaultLocale: 'ru', rentShareWeight: 1, @@ -349,4 +370,30 @@ describe('createMiniAppAdminService', () => { } }) }) + + test('updates a household member lifecycle status for admins', async () => { + const service = createMiniAppAdminService(repository()) + + const result = await service.updateMemberStatus({ + householdId: 'household-1', + actorIsAdmin: true, + memberId: 'member-123456', + status: 'away' + }) + + expect(result).toEqual({ + status: 'ok', + member: { + id: 'member-123456', + householdId: 'household-1', + telegramUserId: '123456', + displayName: 'Stan', + status: 'away', + preferredLocale: null, + householdDefaultLocale: 'ru', + rentShareWeight: 1, + isAdmin: false + } + }) + }) }) diff --git a/packages/application/src/miniapp-admin-service.ts b/packages/application/src/miniapp-admin-service.ts index bfeea97..73ae6cf 100644 --- a/packages/application/src/miniapp-admin-service.ts +++ b/packages/application/src/miniapp-admin-service.ts @@ -1,6 +1,7 @@ import type { HouseholdBillingSettingsRecord, HouseholdConfigurationRepository, + HouseholdMemberLifecycleStatus, HouseholdMemberRecord, HouseholdPendingMemberRecord, HouseholdTopicBindingRecord, @@ -126,6 +127,21 @@ export interface MiniAppAdminService { reason: 'not_admin' | 'invalid_weight' | 'member_not_found' } > + updateMemberStatus(input: { + householdId: string + actorIsAdmin: boolean + memberId: string + status: HouseholdMemberLifecycleStatus + }): Promise< + | { + status: 'ok' + member: HouseholdMemberRecord + } + | { + status: 'rejected' + reason: 'not_admin' | 'member_not_found' + } + > } export function createMiniAppAdminService( @@ -349,6 +365,32 @@ export function createMiniAppAdminService( } } + return { + status: 'ok', + member + } + }, + + async updateMemberStatus(input) { + if (!input.actorIsAdmin) { + return { + status: 'rejected', + reason: 'not_admin' + } + } + + const member = await repository.updateHouseholdMemberStatus( + input.householdId, + input.memberId, + input.status + ) + if (!member) { + return { + status: 'rejected', + reason: 'member_not_found' + } + } + return { status: 'ok', member diff --git a/packages/db/drizzle/0014_empty_risque.sql b/packages/db/drizzle/0014_empty_risque.sql new file mode 100644 index 0000000..ed01359 --- /dev/null +++ b/packages/db/drizzle/0014_empty_risque.sql @@ -0,0 +1 @@ +ALTER TABLE "members" ADD COLUMN "lifecycle_status" text DEFAULT 'active' NOT NULL; \ No newline at end of file diff --git a/packages/db/drizzle/meta/0014_snapshot.json b/packages/db/drizzle/meta/0014_snapshot.json new file mode 100644 index 0000000..3c87a17 --- /dev/null +++ b/packages/db/drizzle/meta/0014_snapshot.json @@ -0,0 +1,2952 @@ +{ + "id": "f6ad6e48-597d-4451-b3c4-16eebcbe1e33", + "prevId": "d21fa4de-b88e-45fd-baa7-6b731edc2f9e", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.anonymous_messages": { + "name": "anonymous_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "household_id": { + "name": "household_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "submitted_by_member_id": { + "name": "submitted_by_member_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "raw_text": { + "name": "raw_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sanitized_text": { + "name": "sanitized_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "moderation_status": { + "name": "moderation_status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "moderation_reason": { + "name": "moderation_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telegram_chat_id": { + "name": "telegram_chat_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "telegram_message_id": { + "name": "telegram_message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "telegram_update_id": { + "name": "telegram_update_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "posted_chat_id": { + "name": "posted_chat_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "posted_thread_id": { + "name": "posted_thread_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "posted_message_id": { + "name": "posted_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "posted_at": { + "name": "posted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "anonymous_messages_household_tg_update_unique": { + "name": "anonymous_messages_household_tg_update_unique", + "columns": [ + { + "expression": "household_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "telegram_update_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "anonymous_messages_member_created_idx": { + "name": "anonymous_messages_member_created_idx", + "columns": [ + { + "expression": "submitted_by_member_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "anonymous_messages_status_created_idx": { + "name": "anonymous_messages_status_created_idx", + "columns": [ + { + "expression": "moderation_status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "anonymous_messages_household_id_households_id_fk": { + "name": "anonymous_messages_household_id_households_id_fk", + "tableFrom": "anonymous_messages", + "tableTo": "households", + "columnsFrom": ["household_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "anonymous_messages_submitted_by_member_id_members_id_fk": { + "name": "anonymous_messages_submitted_by_member_id_members_id_fk", + "tableFrom": "anonymous_messages", + "tableTo": "members", + "columnsFrom": ["submitted_by_member_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.billing_cycle_exchange_rates": { + "name": "billing_cycle_exchange_rates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "cycle_id": { + "name": "cycle_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_currency": { + "name": "source_currency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_currency": { + "name": "target_currency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rate_micros": { + "name": "rate_micros", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "effective_date": { + "name": "effective_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'nbg'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "billing_cycle_exchange_rates_cycle_pair_unique": { + "name": "billing_cycle_exchange_rates_cycle_pair_unique", + "columns": [ + { + "expression": "cycle_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_currency", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_currency", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_cycle_exchange_rates_cycle_idx": { + "name": "billing_cycle_exchange_rates_cycle_idx", + "columns": [ + { + "expression": "cycle_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "billing_cycle_exchange_rates_cycle_id_billing_cycles_id_fk": { + "name": "billing_cycle_exchange_rates_cycle_id_billing_cycles_id_fk", + "tableFrom": "billing_cycle_exchange_rates", + "tableTo": "billing_cycles", + "columnsFrom": ["cycle_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.billing_cycles": { + "name": "billing_cycles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "household_id": { + "name": "household_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "period": { + "name": "period", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "closed_at": { + "name": "closed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "billing_cycles_household_period_unique": { + "name": "billing_cycles_household_period_unique", + "columns": [ + { + "expression": "household_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "period", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_cycles_household_period_idx": { + "name": "billing_cycles_household_period_idx", + "columns": [ + { + "expression": "household_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "period", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "billing_cycles_household_id_households_id_fk": { + "name": "billing_cycles_household_id_households_id_fk", + "tableFrom": "billing_cycles", + "tableTo": "households", + "columnsFrom": ["household_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.household_billing_settings": { + "name": "household_billing_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "household_id": { + "name": "household_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "settlement_currency": { + "name": "settlement_currency", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'GEL'" + }, + "rent_amount_minor": { + "name": "rent_amount_minor", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "rent_currency": { + "name": "rent_currency", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'USD'" + }, + "rent_due_day": { + "name": "rent_due_day", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 20 + }, + "rent_warning_day": { + "name": "rent_warning_day", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 17 + }, + "utilities_due_day": { + "name": "utilities_due_day", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 4 + }, + "utilities_reminder_day": { + "name": "utilities_reminder_day", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'Asia/Tbilisi'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "household_billing_settings_household_unique": { + "name": "household_billing_settings_household_unique", + "columns": [ + { + "expression": "household_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "household_billing_settings_household_id_households_id_fk": { + "name": "household_billing_settings_household_id_households_id_fk", + "tableFrom": "household_billing_settings", + "tableTo": "households", + "columnsFrom": ["household_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.household_join_tokens": { + "name": "household_join_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "household_id": { + "name": "household_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_telegram_user_id": { + "name": "created_by_telegram_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "household_join_tokens_household_unique": { + "name": "household_join_tokens_household_unique", + "columns": [ + { + "expression": "household_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "household_join_tokens_token_unique": { + "name": "household_join_tokens_token_unique", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "household_join_tokens_household_id_households_id_fk": { + "name": "household_join_tokens_household_id_households_id_fk", + "tableFrom": "household_join_tokens", + "tableTo": "households", + "columnsFrom": ["household_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.household_pending_members": { + "name": "household_pending_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "household_id": { + "name": "household_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "telegram_user_id": { + "name": "telegram_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "language_code": { + "name": "language_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "household_pending_members_household_user_unique": { + "name": "household_pending_members_household_user_unique", + "columns": [ + { + "expression": "household_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "telegram_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "household_pending_members_telegram_user_idx": { + "name": "household_pending_members_telegram_user_idx", + "columns": [ + { + "expression": "telegram_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "household_pending_members_household_id_households_id_fk": { + "name": "household_pending_members_household_id_households_id_fk", + "tableFrom": "household_pending_members", + "tableTo": "households", + "columnsFrom": ["household_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.household_telegram_chats": { + "name": "household_telegram_chats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "household_id": { + "name": "household_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "telegram_chat_id": { + "name": "telegram_chat_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "telegram_chat_type": { + "name": "telegram_chat_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "household_telegram_chats_household_unique": { + "name": "household_telegram_chats_household_unique", + "columns": [ + { + "expression": "household_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "household_telegram_chats_chat_unique": { + "name": "household_telegram_chats_chat_unique", + "columns": [ + { + "expression": "telegram_chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "household_telegram_chats_household_id_households_id_fk": { + "name": "household_telegram_chats_household_id_households_id_fk", + "tableFrom": "household_telegram_chats", + "tableTo": "households", + "columnsFrom": ["household_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.household_topic_bindings": { + "name": "household_topic_bindings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "household_id": { + "name": "household_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "telegram_thread_id": { + "name": "telegram_thread_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "topic_name": { + "name": "topic_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "household_topic_bindings_household_role_unique": { + "name": "household_topic_bindings_household_role_unique", + "columns": [ + { + "expression": "household_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "household_topic_bindings_household_thread_unique": { + "name": "household_topic_bindings_household_thread_unique", + "columns": [ + { + "expression": "household_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "telegram_thread_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "household_topic_bindings_household_role_idx": { + "name": "household_topic_bindings_household_role_idx", + "columns": [ + { + "expression": "household_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "household_topic_bindings_household_id_households_id_fk": { + "name": "household_topic_bindings_household_id_households_id_fk", + "tableFrom": "household_topic_bindings", + "tableTo": "households", + "columnsFrom": ["household_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.household_utility_categories": { + "name": "household_utility_categories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "household_id": { + "name": "household_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "household_utility_categories_household_slug_unique": { + "name": "household_utility_categories_household_slug_unique", + "columns": [ + { + "expression": "household_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "household_utility_categories_household_sort_idx": { + "name": "household_utility_categories_household_sort_idx", + "columns": [ + { + "expression": "household_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "household_utility_categories_household_id_households_id_fk": { + "name": "household_utility_categories_household_id_households_id_fk", + "tableFrom": "household_utility_categories", + "tableTo": "households", + "columnsFrom": ["household_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.households": { + "name": "households", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "default_locale": { + "name": "default_locale", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'ru'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.members": { + "name": "members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "household_id": { + "name": "household_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "telegram_user_id": { + "name": "telegram_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lifecycle_status": { + "name": "lifecycle_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "preferred_locale": { + "name": "preferred_locale", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rent_share_weight": { + "name": "rent_share_weight", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "is_admin": { + "name": "is_admin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "members_household_idx": { + "name": "members_household_idx", + "columns": [ + { + "expression": "household_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "members_household_tg_user_unique": { + "name": "members_household_tg_user_unique", + "columns": [ + { + "expression": "household_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "telegram_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "members_household_id_households_id_fk": { + "name": "members_household_id_households_id_fk", + "tableFrom": "members", + "tableTo": "households", + "columnsFrom": ["household_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payment_confirmations": { + "name": "payment_confirmations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "household_id": { + "name": "household_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "cycle_id": { + "name": "cycle_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "member_id": { + "name": "member_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "sender_telegram_user_id": { + "name": "sender_telegram_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "raw_text": { + "name": "raw_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "normalized_text": { + "name": "normalized_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "detected_kind": { + "name": "detected_kind", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "explicit_amount_minor": { + "name": "explicit_amount_minor", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "explicit_currency": { + "name": "explicit_currency", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolved_amount_minor": { + "name": "resolved_amount_minor", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "resolved_currency": { + "name": "resolved_currency", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "review_reason": { + "name": "review_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attachment_count": { + "name": "attachment_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "telegram_chat_id": { + "name": "telegram_chat_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "telegram_message_id": { + "name": "telegram_message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "telegram_thread_id": { + "name": "telegram_thread_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "telegram_update_id": { + "name": "telegram_update_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message_sent_at": { + "name": "message_sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "payment_confirmations_household_tg_message_unique": { + "name": "payment_confirmations_household_tg_message_unique", + "columns": [ + { + "expression": "household_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "telegram_chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "telegram_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payment_confirmations_household_tg_update_unique": { + "name": "payment_confirmations_household_tg_update_unique", + "columns": [ + { + "expression": "household_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "telegram_update_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payment_confirmations_household_status_idx": { + "name": "payment_confirmations_household_status_idx", + "columns": [ + { + "expression": "household_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payment_confirmations_member_created_idx": { + "name": "payment_confirmations_member_created_idx", + "columns": [ + { + "expression": "member_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "payment_confirmations_household_id_households_id_fk": { + "name": "payment_confirmations_household_id_households_id_fk", + "tableFrom": "payment_confirmations", + "tableTo": "households", + "columnsFrom": ["household_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payment_confirmations_cycle_id_billing_cycles_id_fk": { + "name": "payment_confirmations_cycle_id_billing_cycles_id_fk", + "tableFrom": "payment_confirmations", + "tableTo": "billing_cycles", + "columnsFrom": ["cycle_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "payment_confirmations_member_id_members_id_fk": { + "name": "payment_confirmations_member_id_members_id_fk", + "tableFrom": "payment_confirmations", + "tableTo": "members", + "columnsFrom": ["member_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payment_records": { + "name": "payment_records", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "household_id": { + "name": "household_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "cycle_id": { + "name": "cycle_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "member_id": { + "name": "member_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount_minor": { + "name": "amount_minor", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "confirmation_id": { + "name": "confirmation_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "recorded_at": { + "name": "recorded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "payment_records_cycle_member_idx": { + "name": "payment_records_cycle_member_idx", + "columns": [ + { + "expression": "cycle_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "member_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payment_records_cycle_kind_idx": { + "name": "payment_records_cycle_kind_idx", + "columns": [ + { + "expression": "cycle_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payment_records_confirmation_unique": { + "name": "payment_records_confirmation_unique", + "columns": [ + { + "expression": "confirmation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "payment_records_household_id_households_id_fk": { + "name": "payment_records_household_id_households_id_fk", + "tableFrom": "payment_records", + "tableTo": "households", + "columnsFrom": ["household_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payment_records_cycle_id_billing_cycles_id_fk": { + "name": "payment_records_cycle_id_billing_cycles_id_fk", + "tableFrom": "payment_records", + "tableTo": "billing_cycles", + "columnsFrom": ["cycle_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payment_records_member_id_members_id_fk": { + "name": "payment_records_member_id_members_id_fk", + "tableFrom": "payment_records", + "tableTo": "members", + "columnsFrom": ["member_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "payment_records_confirmation_id_payment_confirmations_id_fk": { + "name": "payment_records_confirmation_id_payment_confirmations_id_fk", + "tableFrom": "payment_records", + "tableTo": "payment_confirmations", + "columnsFrom": ["confirmation_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.presence_overrides": { + "name": "presence_overrides", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "cycle_id": { + "name": "cycle_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "member_id": { + "name": "member_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "utility_days": { + "name": "utility_days", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "presence_overrides_cycle_member_unique": { + "name": "presence_overrides_cycle_member_unique", + "columns": [ + { + "expression": "cycle_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "member_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "presence_overrides_cycle_idx": { + "name": "presence_overrides_cycle_idx", + "columns": [ + { + "expression": "cycle_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "presence_overrides_cycle_id_billing_cycles_id_fk": { + "name": "presence_overrides_cycle_id_billing_cycles_id_fk", + "tableFrom": "presence_overrides", + "tableTo": "billing_cycles", + "columnsFrom": ["cycle_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "presence_overrides_member_id_members_id_fk": { + "name": "presence_overrides_member_id_members_id_fk", + "tableFrom": "presence_overrides", + "tableTo": "members", + "columnsFrom": ["member_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.processed_bot_messages": { + "name": "processed_bot_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "household_id": { + "name": "household_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_message_key": { + "name": "source_message_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload_hash": { + "name": "payload_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "processed_bot_messages_source_message_unique": { + "name": "processed_bot_messages_source_message_unique", + "columns": [ + { + "expression": "household_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_message_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "processed_bot_messages_household_id_households_id_fk": { + "name": "processed_bot_messages_household_id_households_id_fk", + "tableFrom": "processed_bot_messages", + "tableTo": "households", + "columnsFrom": ["household_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.purchase_entries": { + "name": "purchase_entries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "household_id": { + "name": "household_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "cycle_id": { + "name": "cycle_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "payer_member_id": { + "name": "payer_member_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "amount_minor": { + "name": "amount_minor", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "raw_text": { + "name": "raw_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "normalized_text": { + "name": "normalized_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parser_mode": { + "name": "parser_mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parser_confidence": { + "name": "parser_confidence", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "telegram_chat_id": { + "name": "telegram_chat_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telegram_message_id": { + "name": "telegram_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "telegram_thread_id": { + "name": "telegram_thread_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "message_sent_at": { + "name": "message_sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "purchase_entries_household_cycle_idx": { + "name": "purchase_entries_household_cycle_idx", + "columns": [ + { + "expression": "household_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cycle_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "purchase_entries_payer_idx": { + "name": "purchase_entries_payer_idx", + "columns": [ + { + "expression": "payer_member_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "purchase_entries_household_tg_message_unique": { + "name": "purchase_entries_household_tg_message_unique", + "columns": [ + { + "expression": "household_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "telegram_chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "telegram_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "purchase_entries_household_id_households_id_fk": { + "name": "purchase_entries_household_id_households_id_fk", + "tableFrom": "purchase_entries", + "tableTo": "households", + "columnsFrom": ["household_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "purchase_entries_cycle_id_billing_cycles_id_fk": { + "name": "purchase_entries_cycle_id_billing_cycles_id_fk", + "tableFrom": "purchase_entries", + "tableTo": "billing_cycles", + "columnsFrom": ["cycle_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "purchase_entries_payer_member_id_members_id_fk": { + "name": "purchase_entries_payer_member_id_members_id_fk", + "tableFrom": "purchase_entries", + "tableTo": "members", + "columnsFrom": ["payer_member_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.purchase_messages": { + "name": "purchase_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "household_id": { + "name": "household_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sender_member_id": { + "name": "sender_member_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "sender_telegram_user_id": { + "name": "sender_telegram_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sender_display_name": { + "name": "sender_display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "raw_text": { + "name": "raw_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "telegram_chat_id": { + "name": "telegram_chat_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "telegram_message_id": { + "name": "telegram_message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "telegram_thread_id": { + "name": "telegram_thread_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "telegram_update_id": { + "name": "telegram_update_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message_sent_at": { + "name": "message_sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "parsed_amount_minor": { + "name": "parsed_amount_minor", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "parsed_currency": { + "name": "parsed_currency", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parsed_item_description": { + "name": "parsed_item_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parser_mode": { + "name": "parser_mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parser_confidence": { + "name": "parser_confidence", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "needs_review": { + "name": "needs_review", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "parser_error": { + "name": "parser_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "processing_status": { + "name": "processing_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "ingested_at": { + "name": "ingested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "purchase_messages_household_thread_idx": { + "name": "purchase_messages_household_thread_idx", + "columns": [ + { + "expression": "household_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "telegram_thread_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "purchase_messages_sender_idx": { + "name": "purchase_messages_sender_idx", + "columns": [ + { + "expression": "sender_telegram_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "purchase_messages_household_tg_message_unique": { + "name": "purchase_messages_household_tg_message_unique", + "columns": [ + { + "expression": "household_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "telegram_chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "telegram_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "purchase_messages_household_tg_update_unique": { + "name": "purchase_messages_household_tg_update_unique", + "columns": [ + { + "expression": "household_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "telegram_update_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "purchase_messages_household_id_households_id_fk": { + "name": "purchase_messages_household_id_households_id_fk", + "tableFrom": "purchase_messages", + "tableTo": "households", + "columnsFrom": ["household_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "purchase_messages_sender_member_id_members_id_fk": { + "name": "purchase_messages_sender_member_id_members_id_fk", + "tableFrom": "purchase_messages", + "tableTo": "members", + "columnsFrom": ["sender_member_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rent_rules": { + "name": "rent_rules", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "household_id": { + "name": "household_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "amount_minor": { + "name": "amount_minor", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "effective_from_period": { + "name": "effective_from_period", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "effective_to_period": { + "name": "effective_to_period", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "rent_rules_household_from_period_unique": { + "name": "rent_rules_household_from_period_unique", + "columns": [ + { + "expression": "household_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "effective_from_period", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "rent_rules_household_from_period_idx": { + "name": "rent_rules_household_from_period_idx", + "columns": [ + { + "expression": "household_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "effective_from_period", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "rent_rules_household_id_households_id_fk": { + "name": "rent_rules_household_id_households_id_fk", + "tableFrom": "rent_rules", + "tableTo": "households", + "columnsFrom": ["household_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settlement_lines": { + "name": "settlement_lines", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "settlement_id": { + "name": "settlement_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "member_id": { + "name": "member_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "rent_share_minor": { + "name": "rent_share_minor", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "utility_share_minor": { + "name": "utility_share_minor", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "purchase_offset_minor": { + "name": "purchase_offset_minor", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "net_due_minor": { + "name": "net_due_minor", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "explanations": { + "name": "explanations", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "settlement_lines_settlement_member_unique": { + "name": "settlement_lines_settlement_member_unique", + "columns": [ + { + "expression": "settlement_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "member_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "settlement_lines_settlement_idx": { + "name": "settlement_lines_settlement_idx", + "columns": [ + { + "expression": "settlement_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "settlement_lines_settlement_id_settlements_id_fk": { + "name": "settlement_lines_settlement_id_settlements_id_fk", + "tableFrom": "settlement_lines", + "tableTo": "settlements", + "columnsFrom": ["settlement_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "settlement_lines_member_id_members_id_fk": { + "name": "settlement_lines_member_id_members_id_fk", + "tableFrom": "settlement_lines", + "tableTo": "members", + "columnsFrom": ["member_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settlements": { + "name": "settlements", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "household_id": { + "name": "household_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "cycle_id": { + "name": "cycle_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "input_hash": { + "name": "input_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "total_due_minor": { + "name": "total_due_minor", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "computed_at": { + "name": "computed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + } + }, + "indexes": { + "settlements_cycle_unique": { + "name": "settlements_cycle_unique", + "columns": [ + { + "expression": "cycle_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "settlements_household_computed_idx": { + "name": "settlements_household_computed_idx", + "columns": [ + { + "expression": "household_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "computed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "settlements_household_id_households_id_fk": { + "name": "settlements_household_id_households_id_fk", + "tableFrom": "settlements", + "tableTo": "households", + "columnsFrom": ["household_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "settlements_cycle_id_billing_cycles_id_fk": { + "name": "settlements_cycle_id_billing_cycles_id_fk", + "tableFrom": "settlements", + "tableTo": "billing_cycles", + "columnsFrom": ["cycle_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.telegram_pending_actions": { + "name": "telegram_pending_actions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "telegram_user_id": { + "name": "telegram_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "telegram_chat_id": { + "name": "telegram_chat_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "telegram_pending_actions_chat_user_unique": { + "name": "telegram_pending_actions_chat_user_unique", + "columns": [ + { + "expression": "telegram_chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "telegram_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "telegram_pending_actions_user_action_idx": { + "name": "telegram_pending_actions_user_action_idx", + "columns": [ + { + "expression": "telegram_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.utility_bills": { + "name": "utility_bills", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "household_id": { + "name": "household_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "cycle_id": { + "name": "cycle_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "bill_name": { + "name": "bill_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount_minor": { + "name": "amount_minor", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "due_date": { + "name": "due_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'manual'" + }, + "created_by_member_id": { + "name": "created_by_member_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "utility_bills_cycle_idx": { + "name": "utility_bills_cycle_idx", + "columns": [ + { + "expression": "cycle_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "utility_bills_household_cycle_idx": { + "name": "utility_bills_household_cycle_idx", + "columns": [ + { + "expression": "household_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cycle_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "utility_bills_household_id_households_id_fk": { + "name": "utility_bills_household_id_households_id_fk", + "tableFrom": "utility_bills", + "tableTo": "households", + "columnsFrom": ["household_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "utility_bills_cycle_id_billing_cycles_id_fk": { + "name": "utility_bills_cycle_id_billing_cycles_id_fk", + "tableFrom": "utility_bills", + "tableTo": "billing_cycles", + "columnsFrom": ["cycle_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "utility_bills_created_by_member_id_members_id_fk": { + "name": "utility_bills_created_by_member_id_members_id_fk", + "tableFrom": "utility_bills", + "tableTo": "members", + "columnsFrom": ["created_by_member_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/db/drizzle/meta/_journal.json b/packages/db/drizzle/meta/_journal.json index f9af1e4..fb66c84 100644 --- a/packages/db/drizzle/meta/_journal.json +++ b/packages/db/drizzle/meta/_journal.json @@ -99,6 +99,13 @@ "when": 1773147481265, "tag": "0013_wild_avengers", "breakpoints": true + }, + { + "idx": 14, + "version": "7", + "when": 1773222186943, + "tag": "0014_empty_risque", + "breakpoints": true } ] } diff --git a/packages/db/src/schema.ts b/packages/db/src/schema.ts index ac7775d..2c3d906 100644 --- a/packages/db/src/schema.ts +++ b/packages/db/src/schema.ts @@ -194,6 +194,7 @@ export const members = pgTable( .references(() => households.id, { onDelete: 'cascade' }), telegramUserId: text('telegram_user_id').notNull(), displayName: text('display_name').notNull(), + lifecycleStatus: text('lifecycle_status').default('active').notNull(), preferredLocale: text('preferred_locale'), rentShareWeight: integer('rent_share_weight').default(1).notNull(), isAdmin: integer('is_admin').default(0).notNull(), diff --git a/packages/ports/src/household-config.ts b/packages/ports/src/household-config.ts index 5fe86d1..4e09b03 100644 --- a/packages/ports/src/household-config.ts +++ b/packages/ports/src/household-config.ts @@ -2,8 +2,10 @@ import type { CurrencyCode, SupportedLocale } from '@household/domain' import type { ReminderTarget } from './reminders' export const HOUSEHOLD_TOPIC_ROLES = ['purchase', 'feedback', 'reminders', 'payments'] as const +export const HOUSEHOLD_MEMBER_LIFECYCLE_STATUSES = ['active', 'away', 'left'] as const export type HouseholdTopicRole = (typeof HOUSEHOLD_TOPIC_ROLES)[number] +export type HouseholdMemberLifecycleStatus = (typeof HOUSEHOLD_MEMBER_LIFECYCLE_STATUSES)[number] export interface HouseholdTelegramChatRecord { householdId: string @@ -43,6 +45,7 @@ export interface HouseholdMemberRecord { householdId: string telegramUserId: string displayName: string + status: HouseholdMemberLifecycleStatus preferredLocale: SupportedLocale | null householdDefaultLocale: SupportedLocale rentShareWeight: number @@ -130,6 +133,7 @@ export interface HouseholdConfigurationRepository { householdId: string telegramUserId: string displayName: string + status?: HouseholdMemberLifecycleStatus preferredLocale?: SupportedLocale | null rentShareWeight?: number isAdmin?: boolean @@ -188,4 +192,9 @@ export interface HouseholdConfigurationRepository { memberId: string, rentShareWeight: number ): Promise + updateHouseholdMemberStatus( + householdId: string, + memberId: string, + status: HouseholdMemberLifecycleStatus + ): Promise } diff --git a/packages/ports/src/index.ts b/packages/ports/src/index.ts index eeb4341..9589203 100644 --- a/packages/ports/src/index.ts +++ b/packages/ports/src/index.ts @@ -13,10 +13,12 @@ export type { ReleaseProcessedBotMessageInput } from './processed-bot-messages' export { + HOUSEHOLD_MEMBER_LIFECYCLE_STATUSES, HOUSEHOLD_TOPIC_ROLES, type HouseholdConfigurationRepository, type HouseholdBillingSettingsRecord, type HouseholdJoinTokenRecord, + type HouseholdMemberLifecycleStatus, type HouseholdMemberRecord, type HouseholdPendingMemberRecord, type HouseholdTelegramChatRecord,