mirror of
https://github.com/whekin/household-bot.git
synced 2026-03-31 12:04:02 +00:00
feat: add quick payment action and improve copy button UX
Mini App Home Screen: - Add 'Record Payment' button to utilities and rent period cards - Pre-fill payment amount with member's share (rentShare/utilityShare) - Modal dialog with amount input and currency display - Toast notifications for copy and payment success/failure feedback Copy Button Improvements: - Increase spacing between icon and text (4px → 8px) - Add hover background and padding for better touch target - Green background highlight when copied (in addition to icon color change) - Toast notification appears when copying any value Backend: - Add /api/miniapp/payments/add endpoint for quick payments - Payment notifications sent to 'reminders' topic in Telegram - Include member name, payment type, amount, and period in notification Files: - New: apps/miniapp/src/components/ui/toast.tsx - Modified: apps/miniapp/src/routes/home.tsx, apps/miniapp/src/index.css, apps/miniapp/src/theme.css, apps/miniapp/src/i18n.ts, apps/bot/src/miniapp-billing.ts, apps/bot/src/server.ts Quality Gates: ✅ format, lint, typecheck, build, test Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
@@ -261,7 +261,8 @@ const householdConfigurationRepository: Pick<
|
||||
rentWarningDay: 17,
|
||||
utilitiesDueDay: 4,
|
||||
utilitiesReminderDay: 3,
|
||||
timezone: 'Asia/Tbilisi'
|
||||
timezone: 'Asia/Tbilisi',
|
||||
rentPaymentDestinations: null
|
||||
}
|
||||
},
|
||||
async listHouseholdMembers(householdId) {
|
||||
|
||||
@@ -10,7 +10,8 @@ import type {
|
||||
HouseholdConfigurationRepository,
|
||||
HouseholdMemberAbsencePolicy,
|
||||
HouseholdMemberAbsencePolicyRecord,
|
||||
HouseholdMemberRecord
|
||||
HouseholdMemberRecord,
|
||||
HouseholdRentPaymentDestination
|
||||
} from '@household/ports'
|
||||
import {
|
||||
BillingCycleId,
|
||||
@@ -144,9 +145,12 @@ export interface FinanceDashboard {
|
||||
period: string
|
||||
currency: CurrencyCode
|
||||
timezone: string
|
||||
rentWarningDay: number
|
||||
rentDueDay: number
|
||||
utilitiesReminderDay: number
|
||||
utilitiesDueDay: number
|
||||
paymentBalanceAdjustmentPolicy: 'utilities' | 'rent' | 'separate'
|
||||
rentPaymentDestinations: readonly HouseholdRentPaymentDestination[] | null
|
||||
totalDue: Money
|
||||
totalPaid: Money
|
||||
totalRemaining: Money
|
||||
@@ -577,9 +581,12 @@ async function buildFinanceDashboard(
|
||||
period: cycle.period,
|
||||
currency: cycle.currency,
|
||||
timezone: settings.timezone,
|
||||
rentWarningDay: settings.rentWarningDay,
|
||||
rentDueDay: settings.rentDueDay,
|
||||
utilitiesReminderDay: settings.utilitiesReminderDay,
|
||||
utilitiesDueDay: settings.utilitiesDueDay,
|
||||
paymentBalanceAdjustmentPolicy: settings.paymentBalanceAdjustmentPolicy ?? 'utilities',
|
||||
rentPaymentDestinations: settings.rentPaymentDestinations ?? null,
|
||||
totalDue: settlement.totalDue,
|
||||
totalPaid: paymentRecords.reduce(
|
||||
(sum, payment) => sum.add(Money.fromMinor(payment.amountMinor, payment.currency)),
|
||||
|
||||
@@ -158,7 +158,8 @@ function createRepositoryStub() {
|
||||
rentWarningDay: 17,
|
||||
utilitiesDueDay: 4,
|
||||
utilitiesReminderDay: 3,
|
||||
timezone: 'Asia/Tbilisi'
|
||||
timezone: 'Asia/Tbilisi',
|
||||
rentPaymentDestinations: null
|
||||
}),
|
||||
updateHouseholdBillingSettings: async (input) => ({
|
||||
householdId: input.householdId,
|
||||
@@ -169,7 +170,8 @@ function createRepositoryStub() {
|
||||
rentWarningDay: input.rentWarningDay ?? 17,
|
||||
utilitiesDueDay: input.utilitiesDueDay ?? 4,
|
||||
utilitiesReminderDay: input.utilitiesReminderDay ?? 3,
|
||||
timezone: input.timezone ?? 'Asia/Tbilisi'
|
||||
timezone: input.timezone ?? 'Asia/Tbilisi',
|
||||
rentPaymentDestinations: input.rentPaymentDestinations ?? null
|
||||
}),
|
||||
listHouseholdUtilityCategories: async () => [],
|
||||
upsertHouseholdUtilityCategory: async (input) => ({
|
||||
|
||||
@@ -173,7 +173,8 @@ function createRepositoryStub() {
|
||||
rentWarningDay: 17,
|
||||
utilitiesDueDay: 4,
|
||||
utilitiesReminderDay: 3,
|
||||
timezone: 'Asia/Tbilisi'
|
||||
timezone: 'Asia/Tbilisi',
|
||||
rentPaymentDestinations: null
|
||||
}
|
||||
},
|
||||
async updateHouseholdBillingSettings(input) {
|
||||
@@ -186,7 +187,8 @@ function createRepositoryStub() {
|
||||
rentWarningDay: input.rentWarningDay ?? 17,
|
||||
utilitiesDueDay: input.utilitiesDueDay ?? 4,
|
||||
utilitiesReminderDay: input.utilitiesReminderDay ?? 3,
|
||||
timezone: input.timezone ?? 'Asia/Tbilisi'
|
||||
timezone: input.timezone ?? 'Asia/Tbilisi',
|
||||
rentPaymentDestinations: input.rentPaymentDestinations ?? null
|
||||
}
|
||||
},
|
||||
async listHouseholdUtilityCategories() {
|
||||
|
||||
@@ -270,7 +270,8 @@ function createRepositoryStub() {
|
||||
rentWarningDay: 17,
|
||||
utilitiesDueDay: 4,
|
||||
utilitiesReminderDay: 3,
|
||||
timezone: 'Asia/Tbilisi'
|
||||
timezone: 'Asia/Tbilisi',
|
||||
rentPaymentDestinations: null
|
||||
}
|
||||
},
|
||||
async updateHouseholdBillingSettings(input) {
|
||||
@@ -283,7 +284,8 @@ function createRepositoryStub() {
|
||||
rentWarningDay: input.rentWarningDay ?? 17,
|
||||
utilitiesDueDay: input.utilitiesDueDay ?? 4,
|
||||
utilitiesReminderDay: input.utilitiesReminderDay ?? 3,
|
||||
timezone: input.timezone ?? 'Asia/Tbilisi'
|
||||
timezone: input.timezone ?? 'Asia/Tbilisi',
|
||||
rentPaymentDestinations: input.rentPaymentDestinations ?? null
|
||||
}
|
||||
},
|
||||
async listHouseholdUtilityCategories() {
|
||||
|
||||
@@ -92,7 +92,8 @@ function createRepository(): HouseholdConfigurationRepository {
|
||||
rentWarningDay: 17,
|
||||
utilitiesDueDay: 4,
|
||||
utilitiesReminderDay: 3,
|
||||
timezone: 'Asia/Tbilisi'
|
||||
timezone: 'Asia/Tbilisi',
|
||||
rentPaymentDestinations: null
|
||||
}),
|
||||
updateHouseholdBillingSettings: async (input) => ({
|
||||
householdId: input.householdId,
|
||||
@@ -103,7 +104,8 @@ function createRepository(): HouseholdConfigurationRepository {
|
||||
rentWarningDay: input.rentWarningDay ?? 17,
|
||||
utilitiesDueDay: input.utilitiesDueDay ?? 4,
|
||||
utilitiesReminderDay: input.utilitiesReminderDay ?? 3,
|
||||
timezone: input.timezone ?? 'Asia/Tbilisi'
|
||||
timezone: input.timezone ?? 'Asia/Tbilisi',
|
||||
rentPaymentDestinations: input.rentPaymentDestinations ?? null
|
||||
}),
|
||||
listHouseholdUtilityCategories: async () => [],
|
||||
upsertHouseholdUtilityCategory: async (input) => ({
|
||||
|
||||
@@ -167,7 +167,8 @@ function repository(): HouseholdConfigurationRepository {
|
||||
rentWarningDay: 17,
|
||||
utilitiesDueDay: 4,
|
||||
utilitiesReminderDay: 3,
|
||||
timezone: 'Asia/Tbilisi'
|
||||
timezone: 'Asia/Tbilisi',
|
||||
rentPaymentDestinations: null
|
||||
}),
|
||||
updateHouseholdBillingSettings: async (input) => ({
|
||||
householdId: input.householdId,
|
||||
@@ -178,7 +179,8 @@ function repository(): HouseholdConfigurationRepository {
|
||||
rentWarningDay: input.rentWarningDay ?? 17,
|
||||
utilitiesDueDay: input.utilitiesDueDay ?? 4,
|
||||
utilitiesReminderDay: input.utilitiesReminderDay ?? 3,
|
||||
timezone: input.timezone ?? 'Asia/Tbilisi'
|
||||
timezone: input.timezone ?? 'Asia/Tbilisi',
|
||||
rentPaymentDestinations: input.rentPaymentDestinations ?? null
|
||||
}),
|
||||
getHouseholdAssistantConfig: async (householdId) => ({
|
||||
householdId,
|
||||
@@ -286,7 +288,8 @@ describe('createMiniAppAdminService', () => {
|
||||
rentWarningDay: 17,
|
||||
utilitiesDueDay: 4,
|
||||
utilitiesReminderDay: 3,
|
||||
timezone: 'Asia/Tbilisi'
|
||||
timezone: 'Asia/Tbilisi',
|
||||
rentPaymentDestinations: null
|
||||
},
|
||||
assistantConfig: {
|
||||
householdId: 'household-1',
|
||||
@@ -346,7 +349,8 @@ describe('createMiniAppAdminService', () => {
|
||||
rentWarningDay: 18,
|
||||
utilitiesDueDay: 5,
|
||||
utilitiesReminderDay: 4,
|
||||
timezone: 'Asia/Tbilisi'
|
||||
timezone: 'Asia/Tbilisi',
|
||||
rentPaymentDestinations: null
|
||||
},
|
||||
assistantConfig: {
|
||||
householdId: 'household-1',
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
HouseholdMemberLifecycleStatus,
|
||||
HouseholdMemberRecord,
|
||||
HouseholdPendingMemberRecord,
|
||||
HouseholdRentPaymentDestination,
|
||||
HouseholdTopicBindingRecord,
|
||||
HouseholdUtilityCategoryRecord
|
||||
} from '@household/ports'
|
||||
@@ -25,6 +26,40 @@ function parseCurrency(raw: string): CurrencyCode {
|
||||
return normalized
|
||||
}
|
||||
|
||||
function normalizeOptionalString(value: unknown): string | null {
|
||||
if (typeof value !== 'string') return null
|
||||
const trimmed = value.trim()
|
||||
return trimmed.length > 0 ? trimmed : null
|
||||
}
|
||||
|
||||
function normalizeRentPaymentDestinations(
|
||||
value: unknown
|
||||
): readonly HouseholdRentPaymentDestination[] | null {
|
||||
if (value === null) return null
|
||||
if (!Array.isArray(value)) {
|
||||
throw new Error('Invalid rent payment destinations')
|
||||
}
|
||||
|
||||
return value
|
||||
.map((entry): HouseholdRentPaymentDestination | null => {
|
||||
if (!entry || typeof entry !== 'object') return null
|
||||
const record = entry as Record<string, unknown>
|
||||
const label = normalizeOptionalString(record.label)
|
||||
const account = normalizeOptionalString(record.account)
|
||||
if (!label || !account) return null
|
||||
|
||||
return {
|
||||
label,
|
||||
recipientName: normalizeOptionalString(record.recipientName),
|
||||
bankName: normalizeOptionalString(record.bankName),
|
||||
account,
|
||||
note: normalizeOptionalString(record.note),
|
||||
link: normalizeOptionalString(record.link)
|
||||
}
|
||||
})
|
||||
.filter((entry): entry is HouseholdRentPaymentDestination => Boolean(entry))
|
||||
}
|
||||
|
||||
export interface MiniAppAdminService {
|
||||
getSettings(input: { householdId: string; actorIsAdmin: boolean }): Promise<
|
||||
| {
|
||||
@@ -55,6 +90,7 @@ export interface MiniAppAdminService {
|
||||
utilitiesDueDay: number
|
||||
utilitiesReminderDay: number
|
||||
timezone: string
|
||||
rentPaymentDestinations?: unknown
|
||||
assistantContext?: string
|
||||
assistantTone?: string
|
||||
}): Promise<
|
||||
@@ -402,6 +438,18 @@ export function createMiniAppAdminService(
|
||||
rentCurrency = parseCurrency(input.rentCurrency ?? 'USD')
|
||||
}
|
||||
|
||||
let rentPaymentDestinations: readonly HouseholdRentPaymentDestination[] | null | undefined
|
||||
if (input.rentPaymentDestinations !== undefined) {
|
||||
try {
|
||||
rentPaymentDestinations = normalizeRentPaymentDestinations(input.rentPaymentDestinations)
|
||||
} catch {
|
||||
return {
|
||||
status: 'rejected',
|
||||
reason: 'invalid_settings'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const shouldUpdateAssistantConfig =
|
||||
assistantContext !== undefined || assistantTone !== undefined
|
||||
|
||||
@@ -432,7 +480,12 @@ export function createMiniAppAdminService(
|
||||
rentWarningDay: input.rentWarningDay,
|
||||
utilitiesDueDay: input.utilitiesDueDay,
|
||||
utilitiesReminderDay: input.utilitiesReminderDay,
|
||||
timezone
|
||||
timezone,
|
||||
...(rentPaymentDestinations !== undefined
|
||||
? {
|
||||
rentPaymentDestinations
|
||||
}
|
||||
: {})
|
||||
}),
|
||||
repository.updateHouseholdAssistantConfig && shouldUpdateAssistantConfig
|
||||
? repository.updateHouseholdAssistantConfig({
|
||||
|
||||
@@ -22,7 +22,8 @@ const settingsRepository: Pick<HouseholdConfigurationRepository, 'getHouseholdBi
|
||||
rentWarningDay: 17,
|
||||
utilitiesDueDay: 4,
|
||||
utilitiesReminderDay: 3,
|
||||
timezone: 'Asia/Tbilisi'
|
||||
timezone: 'Asia/Tbilisi',
|
||||
rentPaymentDestinations: null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -113,9 +114,12 @@ describe('createPaymentConfirmationService', () => {
|
||||
period: '2026-03',
|
||||
currency: 'GEL',
|
||||
timezone: 'Asia/Tbilisi',
|
||||
rentWarningDay: 17,
|
||||
rentDueDay: 20,
|
||||
utilitiesReminderDay: 3,
|
||||
utilitiesDueDay: 4,
|
||||
paymentBalanceAdjustmentPolicy: 'utilities',
|
||||
rentPaymentDestinations: null,
|
||||
totalDue: Money.fromMajor('1030', 'GEL'),
|
||||
totalPaid: Money.zero('GEL'),
|
||||
totalRemaining: Money.fromMajor('1030', 'GEL'),
|
||||
@@ -179,9 +183,12 @@ describe('createPaymentConfirmationService', () => {
|
||||
period: '2026-03',
|
||||
currency: 'GEL',
|
||||
timezone: 'Asia/Tbilisi',
|
||||
rentWarningDay: 17,
|
||||
rentDueDay: 20,
|
||||
utilitiesReminderDay: 3,
|
||||
utilitiesDueDay: 4,
|
||||
paymentBalanceAdjustmentPolicy: 'utilities',
|
||||
rentPaymentDestinations: null,
|
||||
totalDue: Money.fromMajor('1030', 'GEL'),
|
||||
totalPaid: Money.zero('GEL'),
|
||||
totalRemaining: Money.fromMajor('1030', 'GEL'),
|
||||
|
||||
Reference in New Issue
Block a user