import type { AdHocNotificationService, FinanceCommandService, HouseholdOnboardingService } from '@household/application' import { Money } from '@household/domain' import type { Logger } from '@household/observability' import { allowedMiniAppOrigin, createMiniAppSessionService, miniAppErrorResponse, miniAppJsonResponse, readMiniAppRequestPayload } from './miniapp-auth' export function createMiniAppDashboardHandler(options: { allowedOrigins: readonly string[] botToken: string financeServiceForHousehold: (householdId: string) => FinanceCommandService adHocNotificationService: AdHocNotificationService onboardingService: HouseholdOnboardingService 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 readMiniAppRequestPayload(request) if (!payload.initData) { return miniAppJsonResponse({ ok: false, error: 'Missing initData' }, 400, origin) } const session = await sessionService.authenticate(payload) if (!session) { return miniAppJsonResponse( { ok: false, error: 'Invalid Telegram init data' }, 401, origin ) } if (!session.authorized) { return miniAppJsonResponse( { ok: true, authorized: false, onboarding: session.onboarding }, 403, origin ) } if (!session.member) { return miniAppJsonResponse( { ok: false, error: 'Authenticated session is missing member context' }, 500, origin ) } const dashboard = await options .financeServiceForHousehold(session.member.householdId) .generateDashboard() const notifications = await options.adHocNotificationService.listUpcomingNotifications({ householdId: session.member.householdId, viewerMemberId: session.member.id }) if (!dashboard) { return miniAppJsonResponse( { ok: false, error: 'No billing cycle available' }, 404, origin ) } return miniAppJsonResponse( { ok: true, authorized: true, dashboard: { period: dashboard.period, currency: dashboard.currency, timezone: dashboard.timezone, rentWarningDay: dashboard.rentWarningDay, rentDueDay: dashboard.rentDueDay, utilitiesReminderDay: dashboard.utilitiesReminderDay, utilitiesDueDay: dashboard.utilitiesDueDay, paymentBalanceAdjustmentPolicy: dashboard.paymentBalanceAdjustmentPolicy, rentPaymentDestinations: dashboard.rentPaymentDestinations, totalDueMajor: dashboard.totalDue.toMajorString(), totalPaidMajor: dashboard.totalPaid.toMajorString(), totalRemainingMajor: dashboard.totalRemaining.toMajorString(), rentSourceAmountMajor: dashboard.rentSourceAmount.toMajorString(), rentSourceCurrency: dashboard.rentSourceAmount.currency, rentDisplayAmountMajor: dashboard.rentDisplayAmount.toMajorString(), rentFxRateMicros: dashboard.rentFxRateMicros?.toString() ?? null, rentFxEffectiveDate: dashboard.rentFxEffectiveDate, members: dashboard.members.map((line) => ({ memberId: line.memberId, displayName: line.displayName, predictedUtilityShareMajor: line.predictedUtilityShare?.toMajorString() ?? null, rentShareMajor: line.rentShare.toMajorString(), utilityShareMajor: line.utilityShare.toMajorString(), purchaseOffsetMajor: line.purchaseOffset.toMajorString(), netDueMajor: line.netDue.toMajorString(), paidMajor: line.paid.toMajorString(), remainingMajor: line.remaining.toMajorString(), overduePayments: line.overduePayments.map((overdue) => ({ kind: overdue.kind, amountMajor: Money.fromMinor( overdue.amountMinor, dashboard.currency ).toMajorString(), periods: overdue.periods })), explanations: line.explanations })), paymentPeriods: (dashboard.paymentPeriods ?? []).map((period) => ({ period: period.period, utilityTotalMajor: period.utilityTotal.toMajorString(), hasOverdueBalance: period.hasOverdueBalance, isCurrentPeriod: period.isCurrentPeriod, kinds: period.kinds.map((kind) => ({ kind: kind.kind, totalDueMajor: kind.totalDue.toMajorString(), totalPaidMajor: kind.totalPaid.toMajorString(), totalRemainingMajor: kind.totalRemaining.toMajorString(), unresolvedMembers: kind.unresolvedMembers.map((member) => ({ memberId: member.memberId, displayName: member.displayName, suggestedAmountMajor: member.suggestedAmount.toMajorString(), baseDueMajor: member.baseDue.toMajorString(), paidMajor: member.paid.toMajorString(), remainingMajor: member.remaining.toMajorString(), effectivelySettled: member.effectivelySettled })) })) })), ledger: dashboard.ledger.map((entry) => ({ id: entry.id, kind: entry.kind, title: entry.title, memberId: entry.memberId, paymentKind: entry.paymentKind, amountMajor: entry.amount.toMajorString(), currency: entry.currency, displayAmountMajor: entry.displayAmount.toMajorString(), displayCurrency: entry.displayCurrency, fxRateMicros: entry.fxRateMicros?.toString() ?? null, fxEffectiveDate: entry.fxEffectiveDate, actorDisplayName: entry.actorDisplayName, occurredAt: entry.occurredAt, ...(entry.kind === 'purchase' ? { purchaseSplitMode: entry.purchaseSplitMode ?? 'equal', originPeriod: entry.originPeriod ?? null, resolutionStatus: entry.resolutionStatus ?? 'unresolved', resolvedAt: entry.resolvedAt ?? null, outstandingByMember: entry.outstandingByMember?.map((outstanding) => ({ memberId: outstanding.memberId, amountMajor: outstanding.amount.toMajorString() })) ?? [], purchaseParticipants: entry.purchaseParticipants?.map((participant) => ({ memberId: participant.memberId, included: participant.included, shareAmountMajor: participant.shareAmount?.toMajorString() ?? null })) ?? [] } : {}) })), notifications: notifications.map((notification) => ({ id: notification.id, summaryText: notification.notificationText, scheduledFor: notification.scheduledFor.toString(), status: notification.status, deliveryMode: notification.deliveryMode, dmRecipientMemberIds: notification.dmRecipientMemberIds, dmRecipientDisplayNames: notification.dmRecipientDisplayNames, creatorMemberId: notification.creatorMemberId, creatorDisplayName: notification.creatorDisplayName, assigneeMemberId: notification.assigneeMemberId, assigneeDisplayName: notification.assigneeDisplayName, canCancel: notification.canCancel, canEdit: notification.canEdit })) } }, 200, origin ) } catch (error) { return miniAppErrorResponse(error, origin, options.logger) } } } }