feat(miniapp): carry overdue billing and admin role flows

This commit is contained in:
2026-03-23 15:44:55 +04:00
parent ee8c53d89b
commit 5af14e101e
44 changed files with 2965 additions and 329 deletions

View File

@@ -153,6 +153,23 @@ export function createDbFinanceRepository(
}
},
async listCycles() {
const rows = await db
.select({
id: schema.billingCycles.id,
period: schema.billingCycles.period,
currency: schema.billingCycles.currency
})
.from(schema.billingCycles)
.where(eq(schema.billingCycles.householdId, householdId))
.orderBy(schema.billingCycles.period)
return rows.map((row) => ({
...row,
currency: toCurrencyCode(row.currency)
}))
},
async getCycleByPeriod(period) {
const rows = await db
.select({
@@ -354,6 +371,7 @@ export function createDbFinanceRepository(
await db.insert(schema.purchaseMessages).values({
id: purchaseId,
householdId,
cycleId: input.cycleId,
senderMemberId: input.payerMemberId,
payerMemberId: input.payerMemberId,
senderTelegramUserId: 'miniapp',
@@ -415,11 +433,13 @@ export function createDbFinanceRepository(
return {
id: row.id,
cycleId: input.cycleId,
payerMemberId: row.payerMemberId,
amountMinor: row.amountMinor,
currency: toCurrencyCode(row.currency),
description: row.description,
occurredAt: row.occurredAt ? instantFromDatabaseValue(row.occurredAt) : null,
cyclePeriod: null,
splitMode: row.splitMode as 'equal' | 'custom_amounts',
participants: participantRows.map((p) => ({
memberId: p.memberId,
@@ -502,11 +522,13 @@ export function createDbFinanceRepository(
return {
id: row.id,
cycleId: null,
payerMemberId: row.payerMemberId,
amountMinor: row.amountMinor,
currency: toCurrencyCode(row.currency),
description: row.description,
occurredAt: instantFromDatabaseValue(row.occurredAt),
cyclePeriod: null,
splitMode: row.splitMode === 'custom_amounts' ? 'custom_amounts' : 'equal',
participants: participants.map((participant) => ({
id: participant.id,
@@ -596,6 +618,7 @@ export function createDbFinanceRepository(
})
.returning({
id: schema.paymentRecords.id,
cycleId: schema.paymentRecords.cycleId,
memberId: schema.paymentRecords.memberId,
kind: schema.paymentRecords.kind,
amountMinor: schema.paymentRecords.amountMinor,
@@ -610,6 +633,8 @@ export function createDbFinanceRepository(
return {
id: row.id,
cycleId: row.cycleId,
cyclePeriod: null,
memberId: row.memberId,
kind: row.kind === 'utilities' ? 'utilities' : 'rent',
amountMinor: row.amountMinor,
@@ -618,6 +643,66 @@ export function createDbFinanceRepository(
}
},
async getPaymentRecord(paymentId) {
const rows = await db
.select({
id: schema.paymentRecords.id,
cycleId: schema.paymentRecords.cycleId,
cyclePeriod: schema.billingCycles.period,
memberId: schema.paymentRecords.memberId,
kind: schema.paymentRecords.kind,
amountMinor: schema.paymentRecords.amountMinor,
currency: schema.paymentRecords.currency,
recordedAt: schema.paymentRecords.recordedAt
})
.from(schema.paymentRecords)
.innerJoin(schema.billingCycles, eq(schema.paymentRecords.cycleId, schema.billingCycles.id))
.where(
and(
eq(schema.paymentRecords.householdId, householdId),
eq(schema.paymentRecords.id, paymentId)
)
)
.limit(1)
const row = rows[0]
if (!row) {
return null
}
return {
id: row.id,
cycleId: row.cycleId,
cyclePeriod: row.cyclePeriod,
memberId: row.memberId,
kind: row.kind === 'utilities' ? 'utilities' : 'rent',
amountMinor: row.amountMinor,
currency: toCurrencyCode(row.currency),
recordedAt: instantFromDatabaseValue(row.recordedAt)!
}
},
async replacePaymentPurchaseAllocations(input) {
await db.transaction(async (tx) => {
await tx
.delete(schema.paymentPurchaseAllocations)
.where(eq(schema.paymentPurchaseAllocations.paymentRecordId, input.paymentRecordId))
if (input.allocations.length === 0) {
return
}
await tx.insert(schema.paymentPurchaseAllocations).values(
input.allocations.map((allocation) => ({
paymentRecordId: input.paymentRecordId,
purchaseId: allocation.purchaseId,
memberId: allocation.memberId,
amountMinor: allocation.amountMinor
}))
)
})
},
async updatePaymentRecord(input) {
const rows = await db
.update(schema.paymentRecords)
@@ -635,6 +720,7 @@ export function createDbFinanceRepository(
)
.returning({
id: schema.paymentRecords.id,
cycleId: schema.paymentRecords.cycleId,
memberId: schema.paymentRecords.memberId,
kind: schema.paymentRecords.kind,
amountMinor: schema.paymentRecords.amountMinor,
@@ -649,6 +735,8 @@ export function createDbFinanceRepository(
return {
id: row.id,
cycleId: row.cycleId,
cyclePeriod: null,
memberId: row.memberId,
kind: row.kind === 'utilities' ? 'utilities' : 'rent',
amountMinor: row.amountMinor,
@@ -741,6 +829,8 @@ export function createDbFinanceRepository(
const rows = await db
.select({
id: schema.paymentRecords.id,
cycleId: schema.paymentRecords.cycleId,
cyclePeriod: schema.billingCycles.period,
memberId: schema.paymentRecords.memberId,
kind: schema.paymentRecords.kind,
amountMinor: schema.paymentRecords.amountMinor,
@@ -748,11 +838,14 @@ export function createDbFinanceRepository(
recordedAt: schema.paymentRecords.recordedAt
})
.from(schema.paymentRecords)
.innerJoin(schema.billingCycles, eq(schema.paymentRecords.cycleId, schema.billingCycles.id))
.where(eq(schema.paymentRecords.cycleId, cycleId))
.orderBy(schema.paymentRecords.recordedAt)
return rows.map((row) => ({
id: row.id,
cycleId: row.cycleId,
cyclePeriod: row.cyclePeriod,
memberId: row.memberId,
kind: row.kind === 'utilities' ? 'utilities' : 'rent',
amountMinor: row.amountMinor,
@@ -765,6 +858,8 @@ export function createDbFinanceRepository(
const rows = await db
.select({
id: schema.purchaseMessages.id,
cycleId: schema.purchaseMessages.cycleId,
cyclePeriod: schema.billingCycles.period,
payerMemberId: schema.purchaseMessages.payerMemberId,
amountMinor: schema.purchaseMessages.parsedAmountMinor,
currency: schema.purchaseMessages.parsedCurrency,
@@ -773,6 +868,10 @@ export function createDbFinanceRepository(
splitMode: schema.purchaseMessages.participantSplitMode
})
.from(schema.purchaseMessages)
.leftJoin(
schema.billingCycles,
eq(schema.purchaseMessages.cycleId, schema.billingCycles.id)
)
.where(
and(
eq(schema.purchaseMessages.householdId, householdId),
@@ -792,6 +891,8 @@ export function createDbFinanceRepository(
return rows.map((row) => ({
id: row.id,
cycleId: row.cycleId,
cyclePeriod: row.cyclePeriod,
payerMemberId: row.payerMemberId!,
amountMinor: row.amountMinor!,
currency: toCurrencyCode(row.currency!),
@@ -802,6 +903,82 @@ export function createDbFinanceRepository(
}))
},
async listParsedPurchases() {
const rows = await db
.select({
id: schema.purchaseMessages.id,
cycleId: schema.purchaseMessages.cycleId,
cyclePeriod: schema.billingCycles.period,
payerMemberId: schema.purchaseMessages.payerMemberId,
amountMinor: schema.purchaseMessages.parsedAmountMinor,
currency: schema.purchaseMessages.parsedCurrency,
description: schema.purchaseMessages.parsedItemDescription,
occurredAt: schema.purchaseMessages.messageSentAt,
splitMode: schema.purchaseMessages.participantSplitMode
})
.from(schema.purchaseMessages)
.leftJoin(
schema.billingCycles,
eq(schema.purchaseMessages.cycleId, schema.billingCycles.id)
)
.where(
and(
eq(schema.purchaseMessages.householdId, householdId),
isNotNull(schema.purchaseMessages.payerMemberId),
isNotNull(schema.purchaseMessages.parsedAmountMinor),
isNotNull(schema.purchaseMessages.parsedCurrency),
or(
eq(schema.purchaseMessages.processingStatus, 'parsed'),
eq(schema.purchaseMessages.processingStatus, 'confirmed')
)
)
)
.orderBy(schema.purchaseMessages.messageSentAt, schema.purchaseMessages.id)
const participantsByPurchaseId = await loadPurchaseParticipants(rows.map((row) => row.id))
return rows.map((row) => ({
id: row.id,
cycleId: row.cycleId,
cyclePeriod: row.cyclePeriod,
payerMemberId: row.payerMemberId!,
amountMinor: row.amountMinor!,
currency: toCurrencyCode(row.currency!),
description: row.description,
occurredAt: instantFromDatabaseValue(row.occurredAt),
splitMode: row.splitMode === 'custom_amounts' ? 'custom_amounts' : 'equal',
participants: participantsByPurchaseId.get(row.id) ?? []
}))
},
async listPaymentPurchaseAllocations() {
const rows = await db
.select({
id: schema.paymentPurchaseAllocations.id,
paymentRecordId: schema.paymentPurchaseAllocations.paymentRecordId,
purchaseId: schema.paymentPurchaseAllocations.purchaseId,
memberId: schema.paymentPurchaseAllocations.memberId,
amountMinor: schema.paymentPurchaseAllocations.amountMinor,
recordedAt: schema.paymentRecords.recordedAt
})
.from(schema.paymentPurchaseAllocations)
.innerJoin(
schema.paymentRecords,
eq(schema.paymentPurchaseAllocations.paymentRecordId, schema.paymentRecords.id)
)
.where(eq(schema.paymentRecords.householdId, householdId))
.orderBy(
schema.paymentPurchaseAllocations.purchaseId,
schema.paymentPurchaseAllocations.memberId,
schema.paymentPurchaseAllocations.createdAt
)
return rows.map((row) => ({
...row,
recordedAt: instantFromDatabaseValue(row.recordedAt)!
}))
},
async getSettlementSnapshotLines(cycleId) {
const rows = await db
.select({
@@ -907,6 +1084,8 @@ export function createDbFinanceRepository(
status: 'recorded' as const,
paymentRecord: {
id: paymentRow.id,
cycleId: input.cycleId,
cyclePeriod: null,
memberId: paymentRow.memberId,
kind: paymentRow.kind === 'utilities' ? 'utilities' : 'rent',
amountMinor: paymentRow.amountMinor,

View File

@@ -1512,6 +1512,40 @@ export function createDbHouseholdConfigurationRepository(databaseUrl: string): {
})
},
async demoteHouseholdAdmin(householdId, memberId) {
const rows = await db
.update(schema.members)
.set({
isAdmin: 0
})
.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 household admin demotion')
}
return toHouseholdMemberRecord({
...row,
defaultLocale: household.defaultLocale
})
},
async updateHouseholdMemberRentShareWeight(householdId, memberId, rentShareWeight) {
const rows = await db
.update(schema.members)