mirror of
https://github.com/whekin/household-bot.git
synced 2026-03-31 12:04:02 +00:00
feat(finance): support weighted rent split
This commit is contained in:
@@ -159,6 +159,7 @@ function createHouseholdConfigurationRepository(): HouseholdConfigurationReposit
|
|||||||
displayName: input.displayName,
|
displayName: input.displayName,
|
||||||
preferredLocale: input.preferredLocale ?? null,
|
preferredLocale: input.preferredLocale ?? null,
|
||||||
householdDefaultLocale: 'ru',
|
householdDefaultLocale: 'ru',
|
||||||
|
rentShareWeight: 1,
|
||||||
isAdmin: input.isAdmin === true
|
isAdmin: input.isAdmin === true
|
||||||
}),
|
}),
|
||||||
getHouseholdMember: async () => null,
|
getHouseholdMember: async () => null,
|
||||||
@@ -171,6 +172,7 @@ function createHouseholdConfigurationRepository(): HouseholdConfigurationReposit
|
|||||||
displayName: 'Stan',
|
displayName: 'Stan',
|
||||||
preferredLocale: null,
|
preferredLocale: null,
|
||||||
householdDefaultLocale: 'ru',
|
householdDefaultLocale: 'ru',
|
||||||
|
rentShareWeight: 1,
|
||||||
isAdmin: false
|
isAdmin: false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -191,6 +193,7 @@ function createHouseholdConfigurationRepository(): HouseholdConfigurationReposit
|
|||||||
displayName: 'Stan',
|
displayName: 'Stan',
|
||||||
preferredLocale: locale,
|
preferredLocale: locale,
|
||||||
householdDefaultLocale: 'ru',
|
householdDefaultLocale: 'ru',
|
||||||
|
rentShareWeight: 1,
|
||||||
isAdmin: false
|
isAdmin: false
|
||||||
}),
|
}),
|
||||||
getHouseholdBillingSettings: async (householdId) => ({
|
getHouseholdBillingSettings: async (householdId) => ({
|
||||||
@@ -222,7 +225,8 @@ function createHouseholdConfigurationRepository(): HouseholdConfigurationReposit
|
|||||||
sortOrder: input.sortOrder,
|
sortOrder: input.sortOrder,
|
||||||
isActive: input.isActive
|
isActive: input.isActive
|
||||||
}),
|
}),
|
||||||
promoteHouseholdAdmin: async () => null
|
promoteHouseholdAdmin: async () => null,
|
||||||
|
updateHouseholdMemberRentShareWeight: async () => null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ function onboardingRepository(): HouseholdConfigurationRepository {
|
|||||||
displayName: string
|
displayName: string
|
||||||
preferredLocale: 'en' | 'ru' | null
|
preferredLocale: 'en' | 'ru' | null
|
||||||
householdDefaultLocale: 'en' | 'ru'
|
householdDefaultLocale: 'en' | 'ru'
|
||||||
|
rentShareWeight: number
|
||||||
isAdmin: boolean
|
isAdmin: boolean
|
||||||
}
|
}
|
||||||
>()
|
>()
|
||||||
@@ -97,6 +98,7 @@ function onboardingRepository(): HouseholdConfigurationRepository {
|
|||||||
displayName: input.displayName,
|
displayName: input.displayName,
|
||||||
preferredLocale: input.preferredLocale ?? null,
|
preferredLocale: input.preferredLocale ?? null,
|
||||||
householdDefaultLocale: household.defaultLocale,
|
householdDefaultLocale: household.defaultLocale,
|
||||||
|
rentShareWeight: 1,
|
||||||
isAdmin: input.isAdmin === true
|
isAdmin: input.isAdmin === true
|
||||||
}
|
}
|
||||||
members.set(input.telegramUserId, member)
|
members.set(input.telegramUserId, member)
|
||||||
@@ -122,6 +124,7 @@ function onboardingRepository(): HouseholdConfigurationRepository {
|
|||||||
displayName: pending.displayName,
|
displayName: pending.displayName,
|
||||||
preferredLocale: null,
|
preferredLocale: null,
|
||||||
householdDefaultLocale: household.defaultLocale,
|
householdDefaultLocale: household.defaultLocale,
|
||||||
|
rentShareWeight: 1,
|
||||||
isAdmin: input.isAdmin === true
|
isAdmin: input.isAdmin === true
|
||||||
}
|
}
|
||||||
members.set(pending.telegramUserId, member)
|
members.set(pending.telegramUserId, member)
|
||||||
@@ -141,6 +144,15 @@ function onboardingRepository(): HouseholdConfigurationRepository {
|
|||||||
}
|
}
|
||||||
: null
|
: null
|
||||||
},
|
},
|
||||||
|
updateHouseholdMemberRentShareWeight: async (_householdId, memberId, rentShareWeight) => {
|
||||||
|
const member = [...members.values()].find((entry) => entry.id === memberId)
|
||||||
|
return member
|
||||||
|
? {
|
||||||
|
...member,
|
||||||
|
rentShareWeight
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
},
|
||||||
getHouseholdBillingSettings: async (householdId) => ({
|
getHouseholdBillingSettings: async (householdId) => ({
|
||||||
householdId,
|
householdId,
|
||||||
rentAmountMinor: null,
|
rentAmountMinor: null,
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ function onboardingRepository(): HouseholdConfigurationRepository {
|
|||||||
displayName: input.displayName,
|
displayName: input.displayName,
|
||||||
preferredLocale: input.preferredLocale ?? null,
|
preferredLocale: input.preferredLocale ?? null,
|
||||||
householdDefaultLocale: household.defaultLocale,
|
householdDefaultLocale: household.defaultLocale,
|
||||||
|
rentShareWeight: 1,
|
||||||
isAdmin: input.isAdmin === true
|
isAdmin: input.isAdmin === true
|
||||||
}),
|
}),
|
||||||
getHouseholdMember: async () => null,
|
getHouseholdMember: async () => null,
|
||||||
@@ -111,6 +112,7 @@ function onboardingRepository(): HouseholdConfigurationRepository {
|
|||||||
displayName: 'Stan',
|
displayName: 'Stan',
|
||||||
preferredLocale: null,
|
preferredLocale: null,
|
||||||
householdDefaultLocale: 'ru',
|
householdDefaultLocale: 'ru',
|
||||||
|
rentShareWeight: 1,
|
||||||
isAdmin: true
|
isAdmin: true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -121,7 +123,8 @@ function onboardingRepository(): HouseholdConfigurationRepository {
|
|||||||
defaultLocale: locale
|
defaultLocale: locale
|
||||||
}),
|
}),
|
||||||
updateMemberPreferredLocale: async () => null,
|
updateMemberPreferredLocale: async () => null,
|
||||||
promoteHouseholdAdmin: async () => null
|
promoteHouseholdAdmin: async () => null,
|
||||||
|
updateHouseholdMemberRentShareWeight: async () => null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ function repository(
|
|||||||
id: 'member-1',
|
id: 'member-1',
|
||||||
telegramUserId: '123456',
|
telegramUserId: '123456',
|
||||||
displayName: 'Stan',
|
displayName: 'Stan',
|
||||||
|
rentShareWeight: 1,
|
||||||
isAdmin: true
|
isAdmin: true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -124,6 +125,7 @@ function onboardingRepository(): HouseholdConfigurationRepository {
|
|||||||
displayName: input.displayName,
|
displayName: input.displayName,
|
||||||
preferredLocale: input.preferredLocale ?? null,
|
preferredLocale: input.preferredLocale ?? null,
|
||||||
householdDefaultLocale: household.defaultLocale,
|
householdDefaultLocale: household.defaultLocale,
|
||||||
|
rentShareWeight: 1,
|
||||||
isAdmin: input.isAdmin === true
|
isAdmin: input.isAdmin === true
|
||||||
}),
|
}),
|
||||||
getHouseholdMember: async () => null,
|
getHouseholdMember: async () => null,
|
||||||
@@ -165,7 +167,8 @@ function onboardingRepository(): HouseholdConfigurationRepository {
|
|||||||
sortOrder: input.sortOrder,
|
sortOrder: input.sortOrder,
|
||||||
isActive: input.isActive
|
isActive: input.isActive
|
||||||
}),
|
}),
|
||||||
promoteHouseholdAdmin: async () => null
|
promoteHouseholdAdmin: async () => null,
|
||||||
|
updateHouseholdMemberRentShareWeight: async () => null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,6 +180,7 @@ describe('createMiniAppDashboardHandler', () => {
|
|||||||
id: 'member-1',
|
id: 'member-1',
|
||||||
telegramUserId: '123456',
|
telegramUserId: '123456',
|
||||||
displayName: 'Stan',
|
displayName: 'Stan',
|
||||||
|
rentShareWeight: 1,
|
||||||
isAdmin: true
|
isAdmin: true
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
@@ -189,6 +193,7 @@ describe('createMiniAppDashboardHandler', () => {
|
|||||||
displayName: 'Stan',
|
displayName: 'Stan',
|
||||||
preferredLocale: null,
|
preferredLocale: null,
|
||||||
householdDefaultLocale: 'ru',
|
householdDefaultLocale: 'ru',
|
||||||
|
rentShareWeight: 1,
|
||||||
isAdmin: true
|
isAdmin: true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -255,6 +260,7 @@ describe('createMiniAppDashboardHandler', () => {
|
|||||||
id: 'member-1',
|
id: 'member-1',
|
||||||
telegramUserId: '123456',
|
telegramUserId: '123456',
|
||||||
displayName: 'Stan',
|
displayName: 'Stan',
|
||||||
|
rentShareWeight: 1,
|
||||||
isAdmin: true
|
isAdmin: true
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
@@ -267,6 +273,7 @@ describe('createMiniAppDashboardHandler', () => {
|
|||||||
displayName: 'Stan',
|
displayName: 'Stan',
|
||||||
preferredLocale: null,
|
preferredLocale: null,
|
||||||
householdDefaultLocale: 'ru',
|
householdDefaultLocale: 'ru',
|
||||||
|
rentShareWeight: 1,
|
||||||
isAdmin: true
|
isAdmin: true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ function repository(): HouseholdConfigurationRepository {
|
|||||||
displayName: 'Stan',
|
displayName: 'Stan',
|
||||||
preferredLocale: null,
|
preferredLocale: null,
|
||||||
householdDefaultLocale: household.defaultLocale,
|
householdDefaultLocale: household.defaultLocale,
|
||||||
|
rentShareWeight: 1,
|
||||||
isAdmin: true
|
isAdmin: true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -45,6 +46,7 @@ function repository(): HouseholdConfigurationRepository {
|
|||||||
displayName: 'Mia',
|
displayName: 'Mia',
|
||||||
preferredLocale: null,
|
preferredLocale: null,
|
||||||
householdDefaultLocale: household.defaultLocale,
|
householdDefaultLocale: household.defaultLocale,
|
||||||
|
rentShareWeight: 1,
|
||||||
isAdmin: false
|
isAdmin: false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -92,6 +94,7 @@ function repository(): HouseholdConfigurationRepository {
|
|||||||
displayName: input.displayName,
|
displayName: input.displayName,
|
||||||
preferredLocale: input.preferredLocale ?? null,
|
preferredLocale: input.preferredLocale ?? null,
|
||||||
householdDefaultLocale: household.defaultLocale,
|
householdDefaultLocale: household.defaultLocale,
|
||||||
|
rentShareWeight: 1,
|
||||||
isAdmin: input.isAdmin === true
|
isAdmin: input.isAdmin === true
|
||||||
},
|
},
|
||||||
getHouseholdMember: async (_householdId, telegramUserId) => members.get(telegramUserId) ?? null,
|
getHouseholdMember: async (_householdId, telegramUserId) => members.get(telegramUserId) ?? null,
|
||||||
@@ -154,7 +157,8 @@ function repository(): HouseholdConfigurationRepository {
|
|||||||
sortOrder: input.sortOrder,
|
sortOrder: input.sortOrder,
|
||||||
isActive: input.isActive
|
isActive: input.isActive
|
||||||
}),
|
}),
|
||||||
promoteHouseholdAdmin: async () => null
|
promoteHouseholdAdmin: async () => null,
|
||||||
|
updateHouseholdMemberRentShareWeight: async () => null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
48
docs/specs/HOUSEBOT-077-custom-rent-split-weights.md
Normal file
48
docs/specs/HOUSEBOT-077-custom-rent-split-weights.md
Normal 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.
|
||||||
@@ -38,6 +38,7 @@ export function createDbFinanceRepository(
|
|||||||
id: schema.members.id,
|
id: schema.members.id,
|
||||||
telegramUserId: schema.members.telegramUserId,
|
telegramUserId: schema.members.telegramUserId,
|
||||||
displayName: schema.members.displayName,
|
displayName: schema.members.displayName,
|
||||||
|
rentShareWeight: schema.members.rentShareWeight,
|
||||||
isAdmin: schema.members.isAdmin
|
isAdmin: schema.members.isAdmin
|
||||||
})
|
})
|
||||||
.from(schema.members)
|
.from(schema.members)
|
||||||
@@ -66,6 +67,7 @@ export function createDbFinanceRepository(
|
|||||||
id: schema.members.id,
|
id: schema.members.id,
|
||||||
telegramUserId: schema.members.telegramUserId,
|
telegramUserId: schema.members.telegramUserId,
|
||||||
displayName: schema.members.displayName,
|
displayName: schema.members.displayName,
|
||||||
|
rentShareWeight: schema.members.rentShareWeight,
|
||||||
isAdmin: schema.members.isAdmin
|
isAdmin: schema.members.isAdmin
|
||||||
})
|
})
|
||||||
.from(schema.members)
|
.from(schema.members)
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ function toHouseholdMemberRecord(row: {
|
|||||||
displayName: string
|
displayName: string
|
||||||
preferredLocale: string | null
|
preferredLocale: string | null
|
||||||
defaultLocale: string
|
defaultLocale: string
|
||||||
|
rentShareWeight: number
|
||||||
isAdmin: number
|
isAdmin: number
|
||||||
}): HouseholdMemberRecord {
|
}): HouseholdMemberRecord {
|
||||||
const householdDefaultLocale = normalizeSupportedLocale(row.defaultLocale)
|
const householdDefaultLocale = normalizeSupportedLocale(row.defaultLocale)
|
||||||
@@ -129,6 +130,7 @@ function toHouseholdMemberRecord(row: {
|
|||||||
displayName: row.displayName,
|
displayName: row.displayName,
|
||||||
preferredLocale: normalizeSupportedLocale(row.preferredLocale),
|
preferredLocale: normalizeSupportedLocale(row.preferredLocale),
|
||||||
householdDefaultLocale,
|
householdDefaultLocale,
|
||||||
|
rentShareWeight: row.rentShareWeight,
|
||||||
isAdmin: row.isAdmin === 1
|
isAdmin: row.isAdmin === 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -766,6 +768,7 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): {
|
|||||||
telegramUserId: input.telegramUserId,
|
telegramUserId: input.telegramUserId,
|
||||||
displayName: input.displayName,
|
displayName: input.displayName,
|
||||||
preferredLocale: input.preferredLocale ?? null,
|
preferredLocale: input.preferredLocale ?? null,
|
||||||
|
rentShareWeight: input.rentShareWeight ?? 1,
|
||||||
isAdmin: input.isAdmin ? 1 : 0
|
isAdmin: input.isAdmin ? 1 : 0
|
||||||
})
|
})
|
||||||
.onConflictDoUpdate({
|
.onConflictDoUpdate({
|
||||||
@@ -773,6 +776,7 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): {
|
|||||||
set: {
|
set: {
|
||||||
displayName: input.displayName,
|
displayName: input.displayName,
|
||||||
preferredLocale: input.preferredLocale ?? schema.members.preferredLocale,
|
preferredLocale: input.preferredLocale ?? schema.members.preferredLocale,
|
||||||
|
rentShareWeight: input.rentShareWeight ?? schema.members.rentShareWeight,
|
||||||
...(input.isAdmin
|
...(input.isAdmin
|
||||||
? {
|
? {
|
||||||
isAdmin: 1
|
isAdmin: 1
|
||||||
@@ -786,6 +790,7 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): {
|
|||||||
telegramUserId: schema.members.telegramUserId,
|
telegramUserId: schema.members.telegramUserId,
|
||||||
displayName: schema.members.displayName,
|
displayName: schema.members.displayName,
|
||||||
preferredLocale: schema.members.preferredLocale,
|
preferredLocale: schema.members.preferredLocale,
|
||||||
|
rentShareWeight: schema.members.rentShareWeight,
|
||||||
isAdmin: schema.members.isAdmin
|
isAdmin: schema.members.isAdmin
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -813,6 +818,7 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): {
|
|||||||
telegramUserId: schema.members.telegramUserId,
|
telegramUserId: schema.members.telegramUserId,
|
||||||
displayName: schema.members.displayName,
|
displayName: schema.members.displayName,
|
||||||
preferredLocale: schema.members.preferredLocale,
|
preferredLocale: schema.members.preferredLocale,
|
||||||
|
rentShareWeight: schema.members.rentShareWeight,
|
||||||
defaultLocale: schema.households.defaultLocale,
|
defaultLocale: schema.households.defaultLocale,
|
||||||
isAdmin: schema.members.isAdmin
|
isAdmin: schema.members.isAdmin
|
||||||
})
|
})
|
||||||
@@ -838,6 +844,7 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): {
|
|||||||
telegramUserId: schema.members.telegramUserId,
|
telegramUserId: schema.members.telegramUserId,
|
||||||
displayName: schema.members.displayName,
|
displayName: schema.members.displayName,
|
||||||
preferredLocale: schema.members.preferredLocale,
|
preferredLocale: schema.members.preferredLocale,
|
||||||
|
rentShareWeight: schema.members.rentShareWeight,
|
||||||
defaultLocale: schema.households.defaultLocale,
|
defaultLocale: schema.households.defaultLocale,
|
||||||
isAdmin: schema.members.isAdmin
|
isAdmin: schema.members.isAdmin
|
||||||
})
|
})
|
||||||
@@ -1012,6 +1019,7 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): {
|
|||||||
telegramUserId: schema.members.telegramUserId,
|
telegramUserId: schema.members.telegramUserId,
|
||||||
displayName: schema.members.displayName,
|
displayName: schema.members.displayName,
|
||||||
preferredLocale: schema.members.preferredLocale,
|
preferredLocale: schema.members.preferredLocale,
|
||||||
|
rentShareWeight: schema.members.rentShareWeight,
|
||||||
defaultLocale: schema.households.defaultLocale,
|
defaultLocale: schema.households.defaultLocale,
|
||||||
isAdmin: schema.members.isAdmin
|
isAdmin: schema.members.isAdmin
|
||||||
})
|
})
|
||||||
@@ -1082,6 +1090,7 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): {
|
|||||||
telegramUserId: pending.telegramUserId,
|
telegramUserId: pending.telegramUserId,
|
||||||
displayName: pending.displayName,
|
displayName: pending.displayName,
|
||||||
preferredLocale: normalizeSupportedLocale(pending.languageCode),
|
preferredLocale: normalizeSupportedLocale(pending.languageCode),
|
||||||
|
rentShareWeight: 1,
|
||||||
isAdmin: input.isAdmin ? 1 : 0
|
isAdmin: input.isAdmin ? 1 : 0
|
||||||
})
|
})
|
||||||
.onConflictDoUpdate({
|
.onConflictDoUpdate({
|
||||||
@@ -1103,6 +1112,7 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): {
|
|||||||
telegramUserId: schema.members.telegramUserId,
|
telegramUserId: schema.members.telegramUserId,
|
||||||
displayName: schema.members.displayName,
|
displayName: schema.members.displayName,
|
||||||
preferredLocale: schema.members.preferredLocale,
|
preferredLocale: schema.members.preferredLocale,
|
||||||
|
rentShareWeight: schema.members.rentShareWeight,
|
||||||
isAdmin: schema.members.isAdmin
|
isAdmin: schema.members.isAdmin
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1174,6 +1184,7 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): {
|
|||||||
telegramUserId: schema.members.telegramUserId,
|
telegramUserId: schema.members.telegramUserId,
|
||||||
displayName: schema.members.displayName,
|
displayName: schema.members.displayName,
|
||||||
preferredLocale: schema.members.preferredLocale,
|
preferredLocale: schema.members.preferredLocale,
|
||||||
|
rentShareWeight: schema.members.rentShareWeight,
|
||||||
isAdmin: schema.members.isAdmin
|
isAdmin: schema.members.isAdmin
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1206,6 +1217,7 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): {
|
|||||||
telegramUserId: schema.members.telegramUserId,
|
telegramUserId: schema.members.telegramUserId,
|
||||||
displayName: schema.members.displayName,
|
displayName: schema.members.displayName,
|
||||||
preferredLocale: schema.members.preferredLocale,
|
preferredLocale: schema.members.preferredLocale,
|
||||||
|
rentShareWeight: schema.members.rentShareWeight,
|
||||||
isAdmin: schema.members.isAdmin
|
isAdmin: schema.members.isAdmin
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1219,6 +1231,39 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): {
|
|||||||
throw new Error('Failed to resolve household chat after admin promotion')
|
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({
|
return toHouseholdMemberRecord({
|
||||||
...row,
|
...row,
|
||||||
defaultLocale: household.defaultLocale
|
defaultLocale: household.defaultLocale
|
||||||
|
|||||||
@@ -216,12 +216,14 @@ describe('createFinanceCommandService', () => {
|
|||||||
id: 'alice',
|
id: 'alice',
|
||||||
telegramUserId: '100',
|
telegramUserId: '100',
|
||||||
displayName: 'Alice',
|
displayName: 'Alice',
|
||||||
|
rentShareWeight: 1,
|
||||||
isAdmin: true
|
isAdmin: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'bob',
|
id: 'bob',
|
||||||
telegramUserId: '200',
|
telegramUserId: '200',
|
||||||
displayName: 'Bob',
|
displayName: 'Bob',
|
||||||
|
rentShareWeight: 1,
|
||||||
isAdmin: false
|
isAdmin: false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -130,7 +130,8 @@ async function buildFinanceDashboard(
|
|||||||
utilitySplitMode: 'equal',
|
utilitySplitMode: 'equal',
|
||||||
members: members.map((member) => ({
|
members: members.map((member) => ({
|
||||||
memberId: MemberId.from(member.id),
|
memberId: MemberId.from(member.id),
|
||||||
active: true
|
active: true,
|
||||||
|
rentWeight: member.rentShareWeight
|
||||||
})),
|
})),
|
||||||
purchases: purchases.map((purchase) => ({
|
purchases: purchases.map((purchase) => ({
|
||||||
purchaseId: PurchaseEntryId.from(purchase.id),
|
purchaseId: PurchaseEntryId.from(purchase.id),
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ function createRepositoryStub() {
|
|||||||
displayName: 'Stan',
|
displayName: 'Stan',
|
||||||
preferredLocale: null,
|
preferredLocale: null,
|
||||||
householdDefaultLocale: household.defaultLocale,
|
householdDefaultLocale: household.defaultLocale,
|
||||||
|
rentShareWeight: 1,
|
||||||
isAdmin: true
|
isAdmin: true
|
||||||
})
|
})
|
||||||
pendingMembers.set('2', {
|
pendingMembers.set('2', {
|
||||||
@@ -94,6 +95,7 @@ function createRepositoryStub() {
|
|||||||
displayName: input.displayName,
|
displayName: input.displayName,
|
||||||
preferredLocale: input.preferredLocale ?? null,
|
preferredLocale: input.preferredLocale ?? null,
|
||||||
householdDefaultLocale: household.defaultLocale,
|
householdDefaultLocale: household.defaultLocale,
|
||||||
|
rentShareWeight: 1,
|
||||||
isAdmin: input.isAdmin === true
|
isAdmin: input.isAdmin === true
|
||||||
}
|
}
|
||||||
members.set(input.telegramUserId, record)
|
members.set(input.telegramUserId, record)
|
||||||
@@ -120,6 +122,7 @@ function createRepositoryStub() {
|
|||||||
displayName: pending.displayName,
|
displayName: pending.displayName,
|
||||||
preferredLocale: null,
|
preferredLocale: null,
|
||||||
householdDefaultLocale: household.defaultLocale,
|
householdDefaultLocale: household.defaultLocale,
|
||||||
|
rentShareWeight: 1,
|
||||||
isAdmin: input.isAdmin === true
|
isAdmin: input.isAdmin === true
|
||||||
}
|
}
|
||||||
members.set(member.telegramUserId, member)
|
members.set(member.telegramUserId, member)
|
||||||
@@ -167,7 +170,8 @@ function createRepositoryStub() {
|
|||||||
sortOrder: input.sortOrder,
|
sortOrder: input.sortOrder,
|
||||||
isActive: input.isActive
|
isActive: input.isActive
|
||||||
}),
|
}),
|
||||||
promoteHouseholdAdmin: async () => null
|
promoteHouseholdAdmin: async () => null,
|
||||||
|
updateHouseholdMemberRentShareWeight: async () => null
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -238,6 +242,7 @@ describe('createHouseholdAdminService', () => {
|
|||||||
displayName: 'Alice',
|
displayName: 'Alice',
|
||||||
preferredLocale: null,
|
preferredLocale: null,
|
||||||
householdDefaultLocale: 'ru',
|
householdDefaultLocale: 'ru',
|
||||||
|
rentShareWeight: 1,
|
||||||
isAdmin: false
|
isAdmin: false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ function createRepositoryStub() {
|
|||||||
displayName: input.displayName,
|
displayName: input.displayName,
|
||||||
preferredLocale: input.preferredLocale ?? null,
|
preferredLocale: input.preferredLocale ?? null,
|
||||||
householdDefaultLocale: household.defaultLocale,
|
householdDefaultLocale: household.defaultLocale,
|
||||||
|
rentShareWeight: 1,
|
||||||
isAdmin: input.isAdmin === true
|
isAdmin: input.isAdmin === true
|
||||||
}
|
}
|
||||||
members.set(input.telegramUserId, member)
|
members.set(input.telegramUserId, member)
|
||||||
@@ -133,6 +134,7 @@ function createRepositoryStub() {
|
|||||||
displayName: pending.displayName,
|
displayName: pending.displayName,
|
||||||
preferredLocale: null,
|
preferredLocale: null,
|
||||||
householdDefaultLocale: household.defaultLocale,
|
householdDefaultLocale: household.defaultLocale,
|
||||||
|
rentShareWeight: 1,
|
||||||
isAdmin: input.isAdmin === true
|
isAdmin: input.isAdmin === true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -190,6 +192,9 @@ function createRepositoryStub() {
|
|||||||
},
|
},
|
||||||
async promoteHouseholdAdmin() {
|
async promoteHouseholdAdmin() {
|
||||||
return null
|
return null
|
||||||
|
},
|
||||||
|
async updateHouseholdMemberRentShareWeight() {
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -319,6 +324,7 @@ describe('createHouseholdOnboardingService', () => {
|
|||||||
displayName: 'Stan',
|
displayName: 'Stan',
|
||||||
preferredLocale: null,
|
preferredLocale: null,
|
||||||
householdDefaultLocale: 'ru',
|
householdDefaultLocale: 'ru',
|
||||||
|
rentShareWeight: 1,
|
||||||
isAdmin: true
|
isAdmin: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -333,13 +339,14 @@ describe('createHouseholdOnboardingService', () => {
|
|||||||
displayName: 'Stan',
|
displayName: 'Stan',
|
||||||
preferredLocale: null,
|
preferredLocale: null,
|
||||||
householdDefaultLocale: 'ru',
|
householdDefaultLocale: 'ru',
|
||||||
|
rentShareWeight: 1,
|
||||||
isAdmin: true
|
isAdmin: true
|
||||||
}
|
}
|
||||||
const service = createHouseholdOnboardingService({ repository })
|
const service = createHouseholdOnboardingService({ repository })
|
||||||
const duplicateRepository = repository as HouseholdConfigurationRepository & {
|
const duplicateRepository = repository as HouseholdConfigurationRepository & {
|
||||||
listHouseholdMembersByTelegramUserId: (
|
listHouseholdMembersByTelegramUserId: (
|
||||||
telegramUserId: string
|
telegramUserId: string
|
||||||
) => Promise<readonly HouseholdMemberRecord[]>
|
) => Promise<readonly (HouseholdMemberRecord & { rentShareWeight: number })[]>
|
||||||
}
|
}
|
||||||
duplicateRepository.listHouseholdMembersByTelegramUserId = async () => [
|
duplicateRepository.listHouseholdMembersByTelegramUserId = async () => [
|
||||||
member,
|
member,
|
||||||
@@ -350,6 +357,7 @@ describe('createHouseholdOnboardingService', () => {
|
|||||||
displayName: 'Stan elsewhere',
|
displayName: 'Stan elsewhere',
|
||||||
preferredLocale: null,
|
preferredLocale: null,
|
||||||
householdDefaultLocale: 'ru',
|
householdDefaultLocale: 'ru',
|
||||||
|
rentShareWeight: 1,
|
||||||
isAdmin: false
|
isAdmin: false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export type HouseholdMiniAppAccess =
|
|||||||
isAdmin: boolean
|
isAdmin: boolean
|
||||||
preferredLocale: SupportedLocale | null
|
preferredLocale: SupportedLocale | null
|
||||||
householdDefaultLocale: SupportedLocale
|
householdDefaultLocale: SupportedLocale
|
||||||
|
rentShareWeight: number
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
@@ -70,6 +71,7 @@ export interface HouseholdOnboardingService {
|
|||||||
isAdmin: boolean
|
isAdmin: boolean
|
||||||
preferredLocale: SupportedLocale | null
|
preferredLocale: SupportedLocale | null
|
||||||
householdDefaultLocale: SupportedLocale
|
householdDefaultLocale: SupportedLocale
|
||||||
|
rentShareWeight: number
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
@@ -85,6 +87,7 @@ function toMember(member: HouseholdMemberRecord): {
|
|||||||
isAdmin: boolean
|
isAdmin: boolean
|
||||||
preferredLocale: SupportedLocale | null
|
preferredLocale: SupportedLocale | null
|
||||||
householdDefaultLocale: SupportedLocale
|
householdDefaultLocale: SupportedLocale
|
||||||
|
rentShareWeight: number
|
||||||
} {
|
} {
|
||||||
return {
|
return {
|
||||||
id: member.id,
|
id: member.id,
|
||||||
@@ -92,7 +95,8 @@ function toMember(member: HouseholdMemberRecord): {
|
|||||||
displayName: member.displayName,
|
displayName: member.displayName,
|
||||||
isAdmin: member.isAdmin,
|
isAdmin: member.isAdmin,
|
||||||
preferredLocale: member.preferredLocale,
|
preferredLocale: member.preferredLocale,
|
||||||
householdDefaultLocale: member.householdDefaultLocale
|
householdDefaultLocale: member.householdDefaultLocale,
|
||||||
|
rentShareWeight: member.rentShareWeight
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -175,6 +175,7 @@ function createRepositoryStub() {
|
|||||||
householdDefaultLocale:
|
householdDefaultLocale:
|
||||||
[...households.values()].find((household) => household.householdId === input.householdId)
|
[...households.values()].find((household) => household.householdId === input.householdId)
|
||||||
?.defaultLocale ?? 'ru',
|
?.defaultLocale ?? 'ru',
|
||||||
|
rentShareWeight: input.rentShareWeight ?? existing?.rentShareWeight ?? 1,
|
||||||
isAdmin: input.isAdmin === true || existing?.isAdmin === true
|
isAdmin: input.isAdmin === true || existing?.isAdmin === true
|
||||||
}
|
}
|
||||||
members.set(key, next)
|
members.set(key, next)
|
||||||
@@ -216,6 +217,7 @@ function createRepositoryStub() {
|
|||||||
[...households.values()].find(
|
[...households.values()].find(
|
||||||
(household) => household.householdId === pending.householdId
|
(household) => household.householdId === pending.householdId
|
||||||
)?.defaultLocale ?? 'ru',
|
)?.defaultLocale ?? 'ru',
|
||||||
|
rentShareWeight: 1,
|
||||||
isAdmin: input.isAdmin === true
|
isAdmin: input.isAdmin === true
|
||||||
}
|
}
|
||||||
members.set(key, member)
|
members.set(key, member)
|
||||||
@@ -296,6 +298,22 @@ function createRepositoryStub() {
|
|||||||
}
|
}
|
||||||
members.set(`${householdId}:${member.telegramUserId}`, next)
|
members.set(`${householdId}:${member.telegramUserId}`, next)
|
||||||
return 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',
|
displayName: 'Stan',
|
||||||
preferredLocale: null,
|
preferredLocale: null,
|
||||||
householdDefaultLocale: 'ru',
|
householdDefaultLocale: 'ru',
|
||||||
|
rentShareWeight: 1,
|
||||||
isAdmin: true
|
isAdmin: true
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -369,6 +388,7 @@ describe('createHouseholdSetupService', () => {
|
|||||||
displayName: 'Mia',
|
displayName: 'Mia',
|
||||||
preferredLocale: null,
|
preferredLocale: null,
|
||||||
householdDefaultLocale: 'ru',
|
householdDefaultLocale: 'ru',
|
||||||
|
rentShareWeight: 1,
|
||||||
isAdmin: true
|
isAdmin: true
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ function createRepository(): HouseholdConfigurationRepository {
|
|||||||
displayName: 'Stan',
|
displayName: 'Stan',
|
||||||
preferredLocale: null,
|
preferredLocale: null,
|
||||||
householdDefaultLocale: 'ru',
|
householdDefaultLocale: 'ru',
|
||||||
|
rentShareWeight: 1,
|
||||||
isAdmin: true
|
isAdmin: true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,7 +105,8 @@ function createRepository(): HouseholdConfigurationRepository {
|
|||||||
sortOrder: input.sortOrder,
|
sortOrder: input.sortOrder,
|
||||||
isActive: input.isActive
|
isActive: input.isActive
|
||||||
}),
|
}),
|
||||||
promoteHouseholdAdmin: async () => null
|
promoteHouseholdAdmin: async () => null,
|
||||||
|
updateHouseholdMemberRentShareWeight: async () => null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -83,6 +83,25 @@ describe('calculateMonthlySettlement', () => {
|
|||||||
expect(result.totalDue.amountMinor).toBe(82000n)
|
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', () => {
|
test('5-member scenario with two purchases remains deterministic', () => {
|
||||||
const input = {
|
const input = {
|
||||||
...fixtureBase(),
|
...fixtureBase(),
|
||||||
@@ -150,4 +169,18 @@ describe('calculateMonthlySettlement', () => {
|
|||||||
|
|
||||||
expect(() => calculateMonthlySettlement(input)).toThrow(DomainError)
|
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)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -82,6 +82,31 @@ function validateWeightedUtilityDays(members: readonly SettlementMemberInput[]):
|
|||||||
return weights
|
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 {
|
function validateCurrencyConsistency(input: SettlementInput): void {
|
||||||
const currency = input.rent.currency
|
const currency = input.rent.currency
|
||||||
|
|
||||||
@@ -114,7 +139,7 @@ export function calculateMonthlySettlement(input: SettlementInput): SettlementRe
|
|||||||
activeMembers.map((member) => [member.memberId.toString(), createMemberState(member, currency)])
|
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()) {
|
for (const [index, member] of activeMembers.entries()) {
|
||||||
const state = membersById.get(member.memberId.toString())
|
const state = membersById.get(member.memberId.toString())
|
||||||
if (!state) {
|
if (!state) {
|
||||||
|
|||||||
@@ -10,6 +10,8 @@
|
|||||||
"0006_marvelous_nehzno.sql": "369a862f68dede5568116e29865aa3c8a7a0ff494487af1b202b62932ffe83f4",
|
"0006_marvelous_nehzno.sql": "369a862f68dede5568116e29865aa3c8a7a0ff494487af1b202b62932ffe83f4",
|
||||||
"0007_sudden_murmur.sql": "eedf265c46705c20be4ddb3e2bd7d9670108756915b911e580e19a00f8118104",
|
"0007_sudden_murmur.sql": "eedf265c46705c20be4ddb3e2bd7d9670108756915b911e580e19a00f8118104",
|
||||||
"0008_lowly_spiral.sql": "4f016332d60f7ef1fef0311210a0fa1a0bfc1d9b6da1da4380a60c14d54a9681",
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1
packages/db/drizzle/0011_previous_ezekiel_stane.sql
Normal file
1
packages/db/drizzle/0011_previous_ezekiel_stane.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "members" ADD COLUMN "rent_share_weight" integer DEFAULT 1 NOT NULL;
|
||||||
2383
packages/db/drizzle/meta/0011_snapshot.json
Normal file
2383
packages/db/drizzle/meta/0011_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -78,6 +78,13 @@
|
|||||||
"when": 1773092080214,
|
"when": 1773092080214,
|
||||||
"tag": "0010_wild_molecule_man",
|
"tag": "0010_wild_molecule_man",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 11,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1773096404367,
|
||||||
|
"tag": "0011_previous_ezekiel_stane",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -194,6 +194,7 @@ export const members = pgTable(
|
|||||||
telegramUserId: text('telegram_user_id').notNull(),
|
telegramUserId: text('telegram_user_id').notNull(),
|
||||||
displayName: text('display_name').notNull(),
|
displayName: text('display_name').notNull(),
|
||||||
preferredLocale: text('preferred_locale'),
|
preferredLocale: text('preferred_locale'),
|
||||||
|
rentShareWeight: integer('rent_share_weight').default(1).notNull(),
|
||||||
isAdmin: integer('is_admin').default(0).notNull(),
|
isAdmin: integer('is_admin').default(0).notNull(),
|
||||||
joinedAt: timestamp('joined_at', { withTimezone: true }).defaultNow().notNull()
|
joinedAt: timestamp('joined_at', { withTimezone: true }).defaultNow().notNull()
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export type UtilitySplitMode = 'equal' | 'weighted_by_days'
|
|||||||
export interface SettlementMemberInput {
|
export interface SettlementMemberInput {
|
||||||
memberId: MemberId
|
memberId: MemberId
|
||||||
active: boolean
|
active: boolean
|
||||||
|
rentWeight?: number
|
||||||
utilityDays?: number
|
utilityDays?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export interface FinanceMemberRecord {
|
|||||||
id: string
|
id: string
|
||||||
telegramUserId: string
|
telegramUserId: string
|
||||||
displayName: string
|
displayName: string
|
||||||
|
rentShareWeight: number
|
||||||
isAdmin: boolean
|
isAdmin: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ export interface HouseholdMemberRecord {
|
|||||||
displayName: string
|
displayName: string
|
||||||
preferredLocale: SupportedLocale | null
|
preferredLocale: SupportedLocale | null
|
||||||
householdDefaultLocale: SupportedLocale
|
householdDefaultLocale: SupportedLocale
|
||||||
|
rentShareWeight: number
|
||||||
isAdmin: boolean
|
isAdmin: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,6 +129,7 @@ export interface HouseholdConfigurationRepository {
|
|||||||
telegramUserId: string
|
telegramUserId: string
|
||||||
displayName: string
|
displayName: string
|
||||||
preferredLocale?: SupportedLocale | null
|
preferredLocale?: SupportedLocale | null
|
||||||
|
rentShareWeight?: number
|
||||||
isAdmin?: boolean
|
isAdmin?: boolean
|
||||||
}): Promise<HouseholdMemberRecord>
|
}): Promise<HouseholdMemberRecord>
|
||||||
getHouseholdMember(
|
getHouseholdMember(
|
||||||
@@ -178,4 +180,9 @@ export interface HouseholdConfigurationRepository {
|
|||||||
householdId: string,
|
householdId: string,
|
||||||
memberId: string
|
memberId: string
|
||||||
): Promise<HouseholdMemberRecord | null>
|
): Promise<HouseholdMemberRecord | null>
|
||||||
|
updateHouseholdMemberRentShareWeight(
|
||||||
|
householdId: string,
|
||||||
|
memberId: string,
|
||||||
|
rentShareWeight: number
|
||||||
|
): Promise<HouseholdMemberRecord | null>
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user