feat(finance): support weighted rent split

This commit is contained in:
2026-03-10 02:47:58 +04:00
parent 9c4fe5cb52
commit 6a04b9d7f5
25 changed files with 2639 additions and 11 deletions

View File

@@ -159,6 +159,7 @@ function createHouseholdConfigurationRepository(): HouseholdConfigurationReposit
displayName: input.displayName,
preferredLocale: input.preferredLocale ?? null,
householdDefaultLocale: 'ru',
rentShareWeight: 1,
isAdmin: input.isAdmin === true
}),
getHouseholdMember: async () => null,
@@ -171,6 +172,7 @@ function createHouseholdConfigurationRepository(): HouseholdConfigurationReposit
displayName: 'Stan',
preferredLocale: null,
householdDefaultLocale: 'ru',
rentShareWeight: 1,
isAdmin: false
}
],
@@ -191,6 +193,7 @@ function createHouseholdConfigurationRepository(): HouseholdConfigurationReposit
displayName: 'Stan',
preferredLocale: locale,
householdDefaultLocale: 'ru',
rentShareWeight: 1,
isAdmin: false
}),
getHouseholdBillingSettings: async (householdId) => ({
@@ -222,7 +225,8 @@ function createHouseholdConfigurationRepository(): HouseholdConfigurationReposit
sortOrder: input.sortOrder,
isActive: input.isActive
}),
promoteHouseholdAdmin: async () => null
promoteHouseholdAdmin: async () => null,
updateHouseholdMemberRentShareWeight: async () => null
}
}

View File

@@ -28,6 +28,7 @@ function onboardingRepository(): HouseholdConfigurationRepository {
displayName: string
preferredLocale: 'en' | 'ru' | null
householdDefaultLocale: 'en' | 'ru'
rentShareWeight: number
isAdmin: boolean
}
>()
@@ -97,6 +98,7 @@ function onboardingRepository(): HouseholdConfigurationRepository {
displayName: input.displayName,
preferredLocale: input.preferredLocale ?? null,
householdDefaultLocale: household.defaultLocale,
rentShareWeight: 1,
isAdmin: input.isAdmin === true
}
members.set(input.telegramUserId, member)
@@ -122,6 +124,7 @@ function onboardingRepository(): HouseholdConfigurationRepository {
displayName: pending.displayName,
preferredLocale: null,
householdDefaultLocale: household.defaultLocale,
rentShareWeight: 1,
isAdmin: input.isAdmin === true
}
members.set(pending.telegramUserId, member)
@@ -141,6 +144,15 @@ function onboardingRepository(): HouseholdConfigurationRepository {
}
: null
},
updateHouseholdMemberRentShareWeight: async (_householdId, memberId, rentShareWeight) => {
const member = [...members.values()].find((entry) => entry.id === memberId)
return member
? {
...member,
rentShareWeight
}
: null
},
getHouseholdBillingSettings: async (householdId) => ({
householdId,
rentAmountMinor: null,

View File

@@ -70,6 +70,7 @@ function onboardingRepository(): HouseholdConfigurationRepository {
displayName: input.displayName,
preferredLocale: input.preferredLocale ?? null,
householdDefaultLocale: household.defaultLocale,
rentShareWeight: 1,
isAdmin: input.isAdmin === true
}),
getHouseholdMember: async () => null,
@@ -111,6 +112,7 @@ function onboardingRepository(): HouseholdConfigurationRepository {
displayName: 'Stan',
preferredLocale: null,
householdDefaultLocale: 'ru',
rentShareWeight: 1,
isAdmin: true
}
],
@@ -121,7 +123,8 @@ function onboardingRepository(): HouseholdConfigurationRepository {
defaultLocale: locale
}),
updateMemberPreferredLocale: async () => null,
promoteHouseholdAdmin: async () => null
promoteHouseholdAdmin: async () => null,
updateHouseholdMemberRentShareWeight: async () => null
}
}

View File

@@ -24,6 +24,7 @@ function repository(
id: 'member-1',
telegramUserId: '123456',
displayName: 'Stan',
rentShareWeight: 1,
isAdmin: true
}
],
@@ -124,6 +125,7 @@ function onboardingRepository(): HouseholdConfigurationRepository {
displayName: input.displayName,
preferredLocale: input.preferredLocale ?? null,
householdDefaultLocale: household.defaultLocale,
rentShareWeight: 1,
isAdmin: input.isAdmin === true
}),
getHouseholdMember: async () => null,
@@ -165,7 +167,8 @@ function onboardingRepository(): HouseholdConfigurationRepository {
sortOrder: input.sortOrder,
isActive: input.isActive
}),
promoteHouseholdAdmin: async () => null
promoteHouseholdAdmin: async () => null,
updateHouseholdMemberRentShareWeight: async () => null
}
}
@@ -177,6 +180,7 @@ describe('createMiniAppDashboardHandler', () => {
id: 'member-1',
telegramUserId: '123456',
displayName: 'Stan',
rentShareWeight: 1,
isAdmin: true
})
)
@@ -189,6 +193,7 @@ describe('createMiniAppDashboardHandler', () => {
displayName: 'Stan',
preferredLocale: null,
householdDefaultLocale: 'ru',
rentShareWeight: 1,
isAdmin: true
}
]
@@ -255,6 +260,7 @@ describe('createMiniAppDashboardHandler', () => {
id: 'member-1',
telegramUserId: '123456',
displayName: 'Stan',
rentShareWeight: 1,
isAdmin: true
})
)
@@ -267,6 +273,7 @@ describe('createMiniAppDashboardHandler', () => {
displayName: 'Stan',
preferredLocale: null,
householdDefaultLocale: 'ru',
rentShareWeight: 1,
isAdmin: true
}
]

View File

@@ -33,6 +33,7 @@ function repository(): HouseholdConfigurationRepository {
displayName: 'Stan',
preferredLocale: null,
householdDefaultLocale: household.defaultLocale,
rentShareWeight: 1,
isAdmin: true
}
],
@@ -45,6 +46,7 @@ function repository(): HouseholdConfigurationRepository {
displayName: 'Mia',
preferredLocale: null,
householdDefaultLocale: household.defaultLocale,
rentShareWeight: 1,
isAdmin: false
}
]
@@ -92,6 +94,7 @@ function repository(): HouseholdConfigurationRepository {
displayName: input.displayName,
preferredLocale: input.preferredLocale ?? null,
householdDefaultLocale: household.defaultLocale,
rentShareWeight: 1,
isAdmin: input.isAdmin === true
},
getHouseholdMember: async (_householdId, telegramUserId) => members.get(telegramUserId) ?? null,
@@ -154,7 +157,8 @@ function repository(): HouseholdConfigurationRepository {
sortOrder: input.sortOrder,
isActive: input.isActive
}),
promoteHouseholdAdmin: async () => null
promoteHouseholdAdmin: async () => null,
updateHouseholdMemberRentShareWeight: async () => null
}
}

View File

@@ -0,0 +1,48 @@
# HOUSEBOT-077 Custom Rent Split Weights
## Summary
Support unequal room rents by storing a deterministic rent-share weight per active household member and using those weights in monthly settlement calculations.
## Goals
- Preserve equal split as the default when all member weights are `1`.
- Allow household admins to edit per-member rent weights from the mini app.
- Keep settlement math deterministic and money-safe.
- Reflect weighted rent shares consistently in statements and dashboard views.
## Non-goals
- Per-cycle rent weights.
- Free-form percentage editing.
- Automatic square-meter or room-type calculations.
## Scope
- In: member-level `rentShareWeight`, settlement-engine support, admin API/UI, tests.
- Out: historical backfill UI, move-in/move-out proration logic, rent history analytics.
## Data Model Changes
- Add `members.rent_share_weight integer not null default 1`.
- Existing members migrate to `1`.
## Domain Rules
- Rent weights must be positive integers.
- Active members participate in rent splitting according to their weight.
- Utility splitting remains independent from rent splitting.
- The same input must always produce the same minor-unit allocation.
## Interfaces
- Household admin mini app payload includes member `rentShareWeight`.
- Admin write endpoint updates one member rent weight at a time.
## Acceptance Criteria
- [ ] Settlement engine uses weighted rent shares.
- [ ] Equal split still holds when all weights are `1`.
- [ ] Admins can edit member rent weights in the mini app.
- [ ] Dashboard and statements reflect the new rent shares.
- [ ] Validation rejects zero or negative weights.

View File

@@ -38,6 +38,7 @@ export function createDbFinanceRepository(
id: schema.members.id,
telegramUserId: schema.members.telegramUserId,
displayName: schema.members.displayName,
rentShareWeight: schema.members.rentShareWeight,
isAdmin: schema.members.isAdmin
})
.from(schema.members)
@@ -66,6 +67,7 @@ export function createDbFinanceRepository(
id: schema.members.id,
telegramUserId: schema.members.telegramUserId,
displayName: schema.members.displayName,
rentShareWeight: schema.members.rentShareWeight,
isAdmin: schema.members.isAdmin
})
.from(schema.members)

View File

@@ -115,6 +115,7 @@ function toHouseholdMemberRecord(row: {
displayName: string
preferredLocale: string | null
defaultLocale: string
rentShareWeight: number
isAdmin: number
}): HouseholdMemberRecord {
const householdDefaultLocale = normalizeSupportedLocale(row.defaultLocale)
@@ -129,6 +130,7 @@ function toHouseholdMemberRecord(row: {
displayName: row.displayName,
preferredLocale: normalizeSupportedLocale(row.preferredLocale),
householdDefaultLocale,
rentShareWeight: row.rentShareWeight,
isAdmin: row.isAdmin === 1
}
}
@@ -766,6 +768,7 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): {
telegramUserId: input.telegramUserId,
displayName: input.displayName,
preferredLocale: input.preferredLocale ?? null,
rentShareWeight: input.rentShareWeight ?? 1,
isAdmin: input.isAdmin ? 1 : 0
})
.onConflictDoUpdate({
@@ -773,6 +776,7 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): {
set: {
displayName: input.displayName,
preferredLocale: input.preferredLocale ?? schema.members.preferredLocale,
rentShareWeight: input.rentShareWeight ?? schema.members.rentShareWeight,
...(input.isAdmin
? {
isAdmin: 1
@@ -786,6 +790,7 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): {
telegramUserId: schema.members.telegramUserId,
displayName: schema.members.displayName,
preferredLocale: schema.members.preferredLocale,
rentShareWeight: schema.members.rentShareWeight,
isAdmin: schema.members.isAdmin
})
@@ -813,6 +818,7 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): {
telegramUserId: schema.members.telegramUserId,
displayName: schema.members.displayName,
preferredLocale: schema.members.preferredLocale,
rentShareWeight: schema.members.rentShareWeight,
defaultLocale: schema.households.defaultLocale,
isAdmin: schema.members.isAdmin
})
@@ -838,6 +844,7 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): {
telegramUserId: schema.members.telegramUserId,
displayName: schema.members.displayName,
preferredLocale: schema.members.preferredLocale,
rentShareWeight: schema.members.rentShareWeight,
defaultLocale: schema.households.defaultLocale,
isAdmin: schema.members.isAdmin
})
@@ -1012,6 +1019,7 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): {
telegramUserId: schema.members.telegramUserId,
displayName: schema.members.displayName,
preferredLocale: schema.members.preferredLocale,
rentShareWeight: schema.members.rentShareWeight,
defaultLocale: schema.households.defaultLocale,
isAdmin: schema.members.isAdmin
})
@@ -1082,6 +1090,7 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): {
telegramUserId: pending.telegramUserId,
displayName: pending.displayName,
preferredLocale: normalizeSupportedLocale(pending.languageCode),
rentShareWeight: 1,
isAdmin: input.isAdmin ? 1 : 0
})
.onConflictDoUpdate({
@@ -1103,6 +1112,7 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): {
telegramUserId: schema.members.telegramUserId,
displayName: schema.members.displayName,
preferredLocale: schema.members.preferredLocale,
rentShareWeight: schema.members.rentShareWeight,
isAdmin: schema.members.isAdmin
})
@@ -1174,6 +1184,7 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): {
telegramUserId: schema.members.telegramUserId,
displayName: schema.members.displayName,
preferredLocale: schema.members.preferredLocale,
rentShareWeight: schema.members.rentShareWeight,
isAdmin: schema.members.isAdmin
})
@@ -1206,6 +1217,7 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): {
telegramUserId: schema.members.telegramUserId,
displayName: schema.members.displayName,
preferredLocale: schema.members.preferredLocale,
rentShareWeight: schema.members.rentShareWeight,
isAdmin: schema.members.isAdmin
})
@@ -1219,6 +1231,39 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): {
throw new Error('Failed to resolve household chat after admin promotion')
}
return toHouseholdMemberRecord({
...row,
defaultLocale: household.defaultLocale
})
},
async updateHouseholdMemberRentShareWeight(householdId, memberId, rentShareWeight) {
const rows = await db
.update(schema.members)
.set({
rentShareWeight
})
.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,
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 rent weight update')
}
return toHouseholdMemberRecord({
...row,
defaultLocale: household.defaultLocale

View File

@@ -216,12 +216,14 @@ describe('createFinanceCommandService', () => {
id: 'alice',
telegramUserId: '100',
displayName: 'Alice',
rentShareWeight: 1,
isAdmin: true
},
{
id: 'bob',
telegramUserId: '200',
displayName: 'Bob',
rentShareWeight: 1,
isAdmin: false
}
]

View File

@@ -130,7 +130,8 @@ async function buildFinanceDashboard(
utilitySplitMode: 'equal',
members: members.map((member) => ({
memberId: MemberId.from(member.id),
active: true
active: true,
rentWeight: member.rentShareWeight
})),
purchases: purchases.map((purchase) => ({
purchaseId: PurchaseEntryId.from(purchase.id),

View File

@@ -30,6 +30,7 @@ function createRepositoryStub() {
displayName: 'Stan',
preferredLocale: null,
householdDefaultLocale: household.defaultLocale,
rentShareWeight: 1,
isAdmin: true
})
pendingMembers.set('2', {
@@ -94,6 +95,7 @@ function createRepositoryStub() {
displayName: input.displayName,
preferredLocale: input.preferredLocale ?? null,
householdDefaultLocale: household.defaultLocale,
rentShareWeight: 1,
isAdmin: input.isAdmin === true
}
members.set(input.telegramUserId, record)
@@ -120,6 +122,7 @@ function createRepositoryStub() {
displayName: pending.displayName,
preferredLocale: null,
householdDefaultLocale: household.defaultLocale,
rentShareWeight: 1,
isAdmin: input.isAdmin === true
}
members.set(member.telegramUserId, member)
@@ -167,7 +170,8 @@ function createRepositoryStub() {
sortOrder: input.sortOrder,
isActive: input.isActive
}),
promoteHouseholdAdmin: async () => null
promoteHouseholdAdmin: async () => null,
updateHouseholdMemberRentShareWeight: async () => null
}
return {
@@ -238,6 +242,7 @@ describe('createHouseholdAdminService', () => {
displayName: 'Alice',
preferredLocale: null,
householdDefaultLocale: 'ru',
rentShareWeight: 1,
isAdmin: false
}
})

View File

@@ -100,6 +100,7 @@ function createRepositoryStub() {
displayName: input.displayName,
preferredLocale: input.preferredLocale ?? null,
householdDefaultLocale: household.defaultLocale,
rentShareWeight: 1,
isAdmin: input.isAdmin === true
}
members.set(input.telegramUserId, member)
@@ -133,6 +134,7 @@ function createRepositoryStub() {
displayName: pending.displayName,
preferredLocale: null,
householdDefaultLocale: household.defaultLocale,
rentShareWeight: 1,
isAdmin: input.isAdmin === true
}
},
@@ -190,6 +192,9 @@ function createRepositoryStub() {
},
async promoteHouseholdAdmin() {
return null
},
async updateHouseholdMemberRentShareWeight() {
return null
}
}
@@ -319,6 +324,7 @@ describe('createHouseholdOnboardingService', () => {
displayName: 'Stan',
preferredLocale: null,
householdDefaultLocale: 'ru',
rentShareWeight: 1,
isAdmin: true
}
})
@@ -333,13 +339,14 @@ describe('createHouseholdOnboardingService', () => {
displayName: 'Stan',
preferredLocale: null,
householdDefaultLocale: 'ru',
rentShareWeight: 1,
isAdmin: true
}
const service = createHouseholdOnboardingService({ repository })
const duplicateRepository = repository as HouseholdConfigurationRepository & {
listHouseholdMembersByTelegramUserId: (
telegramUserId: string
) => Promise<readonly HouseholdMemberRecord[]>
) => Promise<readonly (HouseholdMemberRecord & { rentShareWeight: number })[]>
}
duplicateRepository.listHouseholdMembersByTelegramUserId = async () => [
member,
@@ -350,6 +357,7 @@ describe('createHouseholdOnboardingService', () => {
displayName: 'Stan elsewhere',
preferredLocale: null,
householdDefaultLocale: 'ru',
rentShareWeight: 1,
isAdmin: false
}
]

View File

@@ -20,6 +20,7 @@ export type HouseholdMiniAppAccess =
isAdmin: boolean
preferredLocale: SupportedLocale | null
householdDefaultLocale: SupportedLocale
rentShareWeight: number
}
}
| {
@@ -70,6 +71,7 @@ export interface HouseholdOnboardingService {
isAdmin: boolean
preferredLocale: SupportedLocale | null
householdDefaultLocale: SupportedLocale
rentShareWeight: number
}
}
| {
@@ -85,6 +87,7 @@ function toMember(member: HouseholdMemberRecord): {
isAdmin: boolean
preferredLocale: SupportedLocale | null
householdDefaultLocale: SupportedLocale
rentShareWeight: number
} {
return {
id: member.id,
@@ -92,7 +95,8 @@ function toMember(member: HouseholdMemberRecord): {
displayName: member.displayName,
isAdmin: member.isAdmin,
preferredLocale: member.preferredLocale,
householdDefaultLocale: member.householdDefaultLocale
householdDefaultLocale: member.householdDefaultLocale,
rentShareWeight: member.rentShareWeight
}
}

View File

@@ -175,6 +175,7 @@ function createRepositoryStub() {
householdDefaultLocale:
[...households.values()].find((household) => household.householdId === input.householdId)
?.defaultLocale ?? 'ru',
rentShareWeight: input.rentShareWeight ?? existing?.rentShareWeight ?? 1,
isAdmin: input.isAdmin === true || existing?.isAdmin === true
}
members.set(key, next)
@@ -216,6 +217,7 @@ function createRepositoryStub() {
[...households.values()].find(
(household) => household.householdId === pending.householdId
)?.defaultLocale ?? 'ru',
rentShareWeight: 1,
isAdmin: input.isAdmin === true
}
members.set(key, member)
@@ -296,6 +298,22 @@ function createRepositoryStub() {
}
members.set(`${householdId}:${member.telegramUserId}`, next)
return next
},
async updateHouseholdMemberRentShareWeight(householdId, memberId, rentShareWeight) {
const member = [...members.values()].find(
(entry) => entry.householdId === householdId && entry.id === memberId
)
if (!member) {
return null
}
const next = {
...member,
rentShareWeight
}
members.set(`${householdId}:${member.telegramUserId}`, next)
return next
}
}
@@ -332,6 +350,7 @@ describe('createHouseholdSetupService', () => {
displayName: 'Stan',
preferredLocale: null,
householdDefaultLocale: 'ru',
rentShareWeight: 1,
isAdmin: true
})
})
@@ -369,6 +388,7 @@ describe('createHouseholdSetupService', () => {
displayName: 'Mia',
preferredLocale: null,
householdDefaultLocale: 'ru',
rentShareWeight: 1,
isAdmin: true
})
})

View File

@@ -21,6 +21,7 @@ function createRepository(): HouseholdConfigurationRepository {
displayName: 'Stan',
preferredLocale: null,
householdDefaultLocale: 'ru',
rentShareWeight: 1,
isAdmin: true
}
@@ -104,7 +105,8 @@ function createRepository(): HouseholdConfigurationRepository {
sortOrder: input.sortOrder,
isActive: input.isActive
}),
promoteHouseholdAdmin: async () => null
promoteHouseholdAdmin: async () => null,
updateHouseholdMemberRentShareWeight: async () => null
}
}

View File

@@ -83,6 +83,25 @@ describe('calculateMonthlySettlement', () => {
expect(result.totalDue.amountMinor).toBe(82000n)
})
test('supports weighted rent split by member weights', () => {
const input = {
...fixtureBase(),
utilitySplitMode: 'equal' as const,
members: [
{ memberId: MemberId.from('a'), active: true, rentWeight: 3 },
{ memberId: MemberId.from('b'), active: true, rentWeight: 2 },
{ memberId: MemberId.from('c'), active: true, rentWeight: 1 }
],
purchases: []
}
const result = calculateMonthlySettlement(input)
expect(result.lines.map((line) => line.rentShare.amountMinor)).toEqual([35000n, 23333n, 11667n])
expect(result.lines.map((line) => line.utilityShare.amountMinor)).toEqual([4000n, 4000n, 4000n])
expect(result.totalDue.amountMinor).toBe(82000n)
})
test('5-member scenario with two purchases remains deterministic', () => {
const input = {
...fixtureBase(),
@@ -150,4 +169,18 @@ describe('calculateMonthlySettlement', () => {
expect(() => calculateMonthlySettlement(input)).toThrow(DomainError)
})
test('throws if rent split is selected with invalid rent weights', () => {
const input = {
...fixtureBase(),
utilitySplitMode: 'equal' as const,
members: [
{ memberId: MemberId.from('a'), active: true, rentWeight: 1 },
{ memberId: MemberId.from('b'), active: true, rentWeight: 0 }
],
purchases: []
}
expect(() => calculateMonthlySettlement(input)).toThrow(DomainError)
})
})

View File

@@ -82,6 +82,31 @@ function validateWeightedUtilityDays(members: readonly SettlementMemberInput[]):
return weights
}
function validateRentWeights(members: readonly SettlementMemberInput[]): readonly bigint[] {
const weights = members.map((member) => {
const raw = member.rentWeight ?? 1
if (!Number.isInteger(raw) || raw <= 0) {
throw new DomainError(
DOMAIN_ERROR_CODE.INVALID_SETTLEMENT_INPUT,
`rentWeight must be a positive integer for member ${member.memberId.toString()}`
)
}
return BigInt(raw)
})
const total = weights.reduce((sum, current) => sum + current, 0n)
if (total <= 0n) {
throw new DomainError(
DOMAIN_ERROR_CODE.INVALID_SETTLEMENT_INPUT,
'Total rent weights must be positive'
)
}
return weights
}
function validateCurrencyConsistency(input: SettlementInput): void {
const currency = input.rent.currency
@@ -114,7 +139,7 @@ export function calculateMonthlySettlement(input: SettlementInput): SettlementRe
activeMembers.map((member) => [member.memberId.toString(), createMemberState(member, currency)])
)
const rentShares = input.rent.splitEvenly(activeMembers.length)
const rentShares = input.rent.splitByWeights(validateRentWeights(activeMembers))
for (const [index, member] of activeMembers.entries()) {
const state = membersById.get(member.memberId.toString())
if (!state) {

View File

@@ -10,6 +10,8 @@
"0006_marvelous_nehzno.sql": "369a862f68dede5568116e29865aa3c8a7a0ff494487af1b202b62932ffe83f4",
"0007_sudden_murmur.sql": "eedf265c46705c20be4ddb3e2bd7d9670108756915b911e580e19a00f8118104",
"0008_lowly_spiral.sql": "4f016332d60f7ef1fef0311210a0fa1a0bfc1d9b6da1da4380a60c14d54a9681",
"0009_quiet_wallflower.sql": "c5bcb6a01b6f22a9e64866ac0d11468105aad8b2afb248296370f15b462e3087"
"0009_quiet_wallflower.sql": "c5bcb6a01b6f22a9e64866ac0d11468105aad8b2afb248296370f15b462e3087",
"0010_wild_molecule_man.sql": "46027a6ac770cdc2efd4c3eb5bb53f09d1b852c70fdc46a2434e5a7064587245",
"0011_previous_ezekiel_stane.sql": "d996e64d3854de22e36dedeaa94e46774399163d90263bbb05e0b9199af79b70"
}
}

View File

@@ -0,0 +1 @@
ALTER TABLE "members" ADD COLUMN "rent_share_weight" integer DEFAULT 1 NOT NULL;

File diff suppressed because it is too large Load Diff

View File

@@ -78,6 +78,13 @@
"when": 1773092080214,
"tag": "0010_wild_molecule_man",
"breakpoints": true
},
{
"idx": 11,
"version": "7",
"when": 1773096404367,
"tag": "0011_previous_ezekiel_stane",
"breakpoints": true
}
]
}

View File

@@ -194,6 +194,7 @@ export const members = pgTable(
telegramUserId: text('telegram_user_id').notNull(),
displayName: text('display_name').notNull(),
preferredLocale: text('preferred_locale'),
rentShareWeight: integer('rent_share_weight').default(1).notNull(),
isAdmin: integer('is_admin').default(0).notNull(),
joinedAt: timestamp('joined_at', { withTimezone: true }).defaultNow().notNull()
},

View File

@@ -7,6 +7,7 @@ export type UtilitySplitMode = 'equal' | 'weighted_by_days'
export interface SettlementMemberInput {
memberId: MemberId
active: boolean
rentWeight?: number
utilityDays?: number
}

View File

@@ -4,6 +4,7 @@ export interface FinanceMemberRecord {
id: string
telegramUserId: string
displayName: string
rentShareWeight: number
isAdmin: boolean
}

View File

@@ -45,6 +45,7 @@ export interface HouseholdMemberRecord {
displayName: string
preferredLocale: SupportedLocale | null
householdDefaultLocale: SupportedLocale
rentShareWeight: number
isAdmin: boolean
}
@@ -128,6 +129,7 @@ export interface HouseholdConfigurationRepository {
telegramUserId: string
displayName: string
preferredLocale?: SupportedLocale | null
rentShareWeight?: number
isAdmin?: boolean
}): Promise<HouseholdMemberRecord>
getHouseholdMember(
@@ -178,4 +180,9 @@ export interface HouseholdConfigurationRepository {
householdId: string,
memberId: string
): Promise<HouseholdMemberRecord | null>
updateHouseholdMemberRentShareWeight(
householdId: string,
memberId: string,
rentShareWeight: number
): Promise<HouseholdMemberRecord | null>
}