feat(member): add away billing policies

This commit is contained in:
2026-03-11 14:05:52 +04:00
parent 773abf2531
commit 98988159eb
34 changed files with 4218 additions and 39 deletions

View File

@@ -253,7 +253,9 @@ function createHouseholdConfigurationRepository(): HouseholdConfigurationReposit
}),
promoteHouseholdAdmin: async () => null,
updateHouseholdMemberRentShareWeight: async () => null,
updateHouseholdMemberStatus: async () => null
updateHouseholdMemberStatus: async () => null,
listHouseholdMemberAbsencePolicies: async () => [],
upsertHouseholdMemberAbsencePolicy: async () => null
}
}

View File

@@ -133,7 +133,9 @@ function createRepository(isAdmin = false): HouseholdConfigurationRepository {
updateMemberPreferredLocale: async () => null,
promoteHouseholdAdmin: async () => null,
updateHouseholdMemberRentShareWeight: async () => null,
updateHouseholdMemberStatus: async () => null
updateHouseholdMemberStatus: async () => null,
listHouseholdMemberAbsencePolicies: async () => [],
upsertHouseholdMemberAbsencePolicy: async () => null
}
}

View File

@@ -194,7 +194,9 @@ function createHouseholdRepository(): HouseholdConfigurationRepository {
updateMemberPreferredLocale: async () => null,
promoteHouseholdAdmin: async () => null,
updateHouseholdMemberRentShareWeight: async () => null,
updateHouseholdMemberStatus: async () => null
updateHouseholdMemberStatus: async () => null,
listHouseholdMemberAbsencePolicies: async () => [],
upsertHouseholdMemberAbsencePolicy: async () => null
}
}

View File

@@ -111,7 +111,9 @@ function createRepository(): HouseholdConfigurationRepository {
updateMemberPreferredLocale: async () => null,
promoteHouseholdAdmin: async () => null,
updateHouseholdMemberRentShareWeight: async () => null,
updateHouseholdMemberStatus: async () => null
updateHouseholdMemberStatus: async () => null,
listHouseholdMemberAbsencePolicies: async () => [],
upsertHouseholdMemberAbsencePolicy: async () => null
}
}

View File

@@ -497,6 +497,12 @@ function createHouseholdConfigurationRepository(): HouseholdConfigurationReposit
},
async updateHouseholdMemberStatus() {
return null
},
async listHouseholdMemberAbsencePolicies() {
return []
},
async upsertHouseholdMemberAbsencePolicy() {
return null
}
}
}

View File

@@ -49,6 +49,7 @@ import {
createMiniAppPendingMembersHandler,
createMiniAppPromoteMemberHandler,
createMiniAppSettingsHandler,
createMiniAppUpdateMemberAbsencePolicyHandler,
createMiniAppUpdateMemberStatusHandler,
createMiniAppUpdateMemberRentWeightHandler,
createMiniAppUpdateSettingsHandler,
@@ -546,6 +547,15 @@ const server = createBotWebhookServer({
logger: getLogger('miniapp-admin')
})
: undefined,
miniAppUpdateMemberAbsencePolicy: householdOnboardingService
? createMiniAppUpdateMemberAbsencePolicyHandler({
allowedOrigins: runtime.miniAppAllowedOrigins,
botToken: runtime.telegramBotToken,
onboardingService: householdOnboardingService,
miniAppAdminService: miniAppAdminService!,
logger: getLogger('miniapp-admin')
})
: undefined,
miniAppBillingCycle: householdOnboardingService
? createMiniAppBillingCycleHandler({
allowedOrigins: runtime.miniAppAllowedOrigins,

View File

@@ -11,6 +11,7 @@ import {
createMiniAppPendingMembersHandler,
createMiniAppPromoteMemberHandler,
createMiniAppSettingsHandler,
createMiniAppUpdateMemberAbsencePolicyHandler,
createMiniAppUpdateMemberStatusHandler,
createMiniAppUpdateSettingsHandler
} from './miniapp-admin'
@@ -25,6 +26,12 @@ function onboardingRepository(): HouseholdConfigurationRepository {
title: 'Kojori House',
defaultLocale: 'ru' as const
}
let memberAbsencePolicies: {
householdId: string
memberId: string
effectiveFromPeriod: string
policy: 'resident' | 'away_rent_and_utilities' | 'away_rent_only' | 'inactive'
}[] = []
return {
registerTelegramHouseholdChat: async () => ({
@@ -83,7 +90,19 @@ function onboardingRepository(): HouseholdConfigurationRepository {
isAdmin: input.isAdmin === true
}),
getHouseholdMember: async () => null,
listHouseholdMembers: async () => [],
listHouseholdMembers: async () => [
{
id: 'member-123456',
householdId: household.householdId,
telegramUserId: '123456',
displayName: 'Stan',
status: 'active',
preferredLocale: null,
householdDefaultLocale: household.defaultLocale,
rentShareWeight: 1,
isAdmin: true
}
],
listHouseholdMembersByTelegramUserId: async () => [],
listPendingHouseholdMembers: async () => [
{
@@ -208,7 +227,28 @@ function onboardingRepository(): HouseholdConfigurationRepository {
rentShareWeight: 1,
isAdmin: false
}
: null
: null,
listHouseholdMemberAbsencePolicies: async () => memberAbsencePolicies,
upsertHouseholdMemberAbsencePolicy: async (input) => {
const next = {
householdId: input.householdId,
memberId: input.memberId,
effectiveFromPeriod: input.effectiveFromPeriod,
policy: input.policy
}
memberAbsencePolicies = [
...memberAbsencePolicies.filter(
(entry) =>
!(
entry.householdId === input.householdId &&
entry.memberId === input.memberId &&
entry.effectiveFromPeriod === input.effectiveFromPeriod
)
),
next
]
return next
}
}
}
@@ -423,6 +463,7 @@ describe('createMiniAppSettingsHandler', () => {
}
],
categories: [],
memberAbsencePolicies: [],
assistantUsage: [],
members: [
{
@@ -641,4 +682,63 @@ describe('createMiniAppUpdateMemberStatusHandler', () => {
}
})
})
test('updates a household member absence policy for an authenticated admin', async () => {
const authDate = Math.floor(Date.now() / 1000)
const repository = onboardingRepository()
repository.listHouseholdMembersByTelegramUserId = async () => [
{
id: 'member-123456',
householdId: 'household-1',
telegramUserId: '123456',
displayName: 'Stan',
status: 'active',
preferredLocale: null,
householdDefaultLocale: 'ru',
rentShareWeight: 1,
isAdmin: true
}
]
const handler = createMiniAppUpdateMemberAbsencePolicyHandler({
allowedOrigins: ['http://localhost:5173'],
botToken: 'test-bot-token',
onboardingService: createHouseholdOnboardingService({
repository
}),
miniAppAdminService: createMiniAppAdminService(repository)
})
const response = await handler.handler(
new Request('http://localhost/api/miniapp/admin/members/absence-policy', {
method: 'POST',
headers: {
origin: 'http://localhost:5173',
'content-type': 'application/json'
},
body: JSON.stringify({
initData: buildMiniAppInitData('test-bot-token', authDate, {
id: 123456,
first_name: 'Stan',
username: 'stanislav',
language_code: 'ru'
}),
memberId: 'member-123456',
policy: 'away_rent_only'
})
})
)
expect(response.status).toBe(200)
expect(await response.json()).toEqual({
ok: true,
authorized: true,
policy: {
householdId: 'household-1',
memberId: 'member-123456',
effectiveFromPeriod: '2026-03',
policy: 'away_rent_only'
}
})
})
})

View File

@@ -1,8 +1,10 @@
import type { HouseholdOnboardingService, MiniAppAdminService } from '@household/application'
import type { Logger } from '@household/observability'
import {
HOUSEHOLD_MEMBER_ABSENCE_POLICIES,
HOUSEHOLD_MEMBER_LIFECYCLE_STATUSES,
type HouseholdBillingSettingsRecord,
type HouseholdMemberAbsencePolicy,
type HouseholdMemberLifecycleStatus
} from '@household/ports'
import type { MiniAppSessionResult } from './miniapp-auth'
@@ -257,6 +259,42 @@ async function readMemberStatusPayload(request: Request): Promise<{
}
}
async function readMemberAbsencePolicyPayload(request: Request): Promise<{
initData: string
memberId: string
policy: HouseholdMemberAbsencePolicy
}> {
const clonedRequest = request.clone()
const payload = await readMiniAppRequestPayload(request)
if (!payload.initData) {
throw new Error('Missing initData')
}
const text = await clonedRequest.text()
let parsed: { memberId?: string; policy?: string }
try {
parsed = JSON.parse(text)
} catch {
throw new Error('Invalid JSON body')
}
const memberId = parsed.memberId?.trim()
const policy = parsed.policy?.trim().toLowerCase()
if (!memberId || !policy) {
throw new Error('Missing member absence policy fields')
}
if (!(HOUSEHOLD_MEMBER_ABSENCE_POLICIES as readonly string[]).includes(policy)) {
throw new Error('Invalid member absence policy')
}
return {
initData: payload.initData,
memberId,
policy: policy as HouseholdMemberAbsencePolicy
}
}
function serializeBillingSettings(settings: HouseholdBillingSettingsRecord) {
return {
householdId: settings.householdId,
@@ -442,6 +480,7 @@ export function createMiniAppSettingsHandler(options: {
topics: result.topics,
categories: result.categories,
members: result.members,
memberAbsencePolicies: result.memberAbsencePolicies,
assistantUsage:
options.assistantUsageTracker?.listHouseholdUsage(member.householdId) ?? []
},
@@ -923,6 +962,87 @@ export function createMiniAppUpdateMemberStatusHandler(options: {
}
}
export function createMiniAppUpdateMemberAbsencePolicyHandler(options: {
allowedOrigins: readonly string[]
botToken: string
onboardingService: HouseholdOnboardingService
miniAppAdminService: MiniAppAdminService
logger?: Logger
}): {
handler: (request: Request) => Promise<Response>
} {
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 readMemberAbsencePolicyPayload(request)
const session = await sessionService.authenticate({
initData: payload.initData
})
if (
!session ||
!session.authorized ||
!session.member ||
session.member.status !== 'active' ||
!session.member.isAdmin
) {
return miniAppJsonResponse(
{ ok: false, error: 'Admin access required for active household members' },
session ? 403 : 401,
origin
)
}
const result = await options.miniAppAdminService.updateMemberAbsencePolicy({
householdId: session.member.householdId,
actorIsAdmin: session.member.isAdmin,
memberId: payload.memberId,
policy: payload.policy
})
if (result.status === 'rejected') {
return miniAppJsonResponse(
{
ok: false,
error:
result.reason === 'member_not_found' ? 'Member not found' : 'Admin access required'
},
result.reason === 'member_not_found' ? 404 : 403,
origin
)
}
return miniAppJsonResponse(
{
ok: true,
authorized: true,
policy: result.policy
},
200,
origin
)
} catch (error) {
return miniAppErrorResponse(error, origin, options.logger)
}
}
}
}
export function createMiniAppApproveMemberHandler(options: {
allowedOrigins: readonly string[]
botToken: string

View File

@@ -154,6 +154,8 @@ function onboardingRepository(): HouseholdConfigurationRepository {
}
: null
},
listHouseholdMemberAbsencePolicies: async () => [],
upsertHouseholdMemberAbsencePolicy: async () => null,
getHouseholdBillingSettings: async (householdId) => ({
householdId,
settlementCurrency: 'GEL',

View File

@@ -132,7 +132,9 @@ function onboardingRepository(): HouseholdConfigurationRepository {
updateMemberPreferredLocale: async () => null,
promoteHouseholdAdmin: async () => null,
updateHouseholdMemberRentShareWeight: async () => null,
updateHouseholdMemberStatus: async () => null
updateHouseholdMemberStatus: async () => null,
listHouseholdMemberAbsencePolicies: async () => [],
upsertHouseholdMemberAbsencePolicy: async () => null
}
}

View File

@@ -185,7 +185,19 @@ function onboardingRepository(): HouseholdConfigurationRepository {
isAdmin: input.isAdmin === true
}),
getHouseholdMember: async () => null,
listHouseholdMembers: async () => [],
listHouseholdMembers: async () => [
{
id: 'member-1',
householdId: household.householdId,
telegramUserId: '123456',
displayName: 'Stan',
status: 'active',
preferredLocale: null,
householdDefaultLocale: household.defaultLocale,
rentShareWeight: 1,
isAdmin: true
}
],
listHouseholdMembersByTelegramUserId: async () => [],
listPendingHouseholdMembers: async () => [],
approvePendingHouseholdMember: async () => null,
@@ -227,7 +239,9 @@ function onboardingRepository(): HouseholdConfigurationRepository {
}),
promoteHouseholdAdmin: async () => null,
updateHouseholdMemberRentShareWeight: async () => null,
updateHouseholdMemberStatus: async () => null
updateHouseholdMemberStatus: async () => null,
listHouseholdMemberAbsencePolicies: async () => [],
upsertHouseholdMemberAbsencePolicy: async () => null
}
}

View File

@@ -165,7 +165,9 @@ function repository(): HouseholdConfigurationRepository {
}),
promoteHouseholdAdmin: async () => null,
updateHouseholdMemberRentShareWeight: async () => null,
updateHouseholdMemberStatus: async () => null
updateHouseholdMemberStatus: async () => null,
listHouseholdMemberAbsencePolicies: async () => [],
upsertHouseholdMemberAbsencePolicy: async () => null
}
}

View File

@@ -68,6 +68,12 @@ export interface BotWebhookServerOptions {
handler: (request: Request) => Promise<Response>
}
| undefined
miniAppUpdateMemberAbsencePolicy?:
| {
path?: string
handler: (request: Request) => Promise<Response>
}
| undefined
miniAppBillingCycle?:
| {
path?: string
@@ -194,6 +200,8 @@ export function createBotWebhookServer(options: BotWebhookServerOptions): {
options.miniAppUpdateMemberRentWeight?.path ?? '/api/miniapp/admin/members/rent-weight'
const miniAppUpdateMemberStatusPath =
options.miniAppUpdateMemberStatus?.path ?? '/api/miniapp/admin/members/status'
const miniAppUpdateMemberAbsencePolicyPath =
options.miniAppUpdateMemberAbsencePolicy?.path ?? '/api/miniapp/admin/members/absence-policy'
const miniAppBillingCyclePath =
options.miniAppBillingCycle?.path ?? '/api/miniapp/admin/billing-cycle'
const miniAppOpenCyclePath =
@@ -280,6 +288,13 @@ export function createBotWebhookServer(options: BotWebhookServerOptions): {
return await options.miniAppUpdateMemberStatus.handler(request)
}
if (
options.miniAppUpdateMemberAbsencePolicy &&
url.pathname === miniAppUpdateMemberAbsencePolicyPath
) {
return await options.miniAppUpdateMemberAbsencePolicy.handler(request)
}
if (options.miniAppBillingCycle && url.pathname === miniAppBillingCyclePath) {
return await options.miniAppBillingCycle.handler(request)
}

View File

@@ -17,10 +17,12 @@ import {
joinMiniAppHousehold,
openMiniAppBillingCycle,
promoteMiniAppMember,
updateMiniAppMemberAbsencePolicy,
updateMiniAppMemberStatus,
updateMiniAppMemberRentWeight,
type MiniAppAdminCycleState,
type MiniAppAdminSettingsPayload,
type MiniAppMemberAbsencePolicy,
updateMiniAppLocalePreference,
updateMiniAppBillingSettings,
updateMiniAppCycleRent,
@@ -282,10 +284,16 @@ function App() {
const [promotingMemberId, setPromotingMemberId] = createSignal<string | null>(null)
const [savingRentWeightMemberId, setSavingRentWeightMemberId] = createSignal<string | null>(null)
const [savingMemberStatusId, setSavingMemberStatusId] = createSignal<string | null>(null)
const [savingMemberAbsencePolicyId, setSavingMemberAbsencePolicyId] = createSignal<string | null>(
null
)
const [rentWeightDrafts, setRentWeightDrafts] = createSignal<Record<string, string>>({})
const [memberStatusDrafts, setMemberStatusDrafts] = createSignal<
Record<string, 'active' | 'away' | 'left'>
>({})
const [memberAbsencePolicyDrafts, setMemberAbsencePolicyDrafts] = createSignal<
Record<string, MiniAppMemberAbsencePolicy>
>({})
const [savingMemberLocale, setSavingMemberLocale] = createSignal(false)
const [savingHouseholdLocale, setSavingHouseholdLocale] = createSignal(false)
const [savingBillingSettings, setSavingBillingSettings] = createSignal(false)
@@ -400,6 +408,39 @@ function App() {
}
}
function defaultAbsencePolicyForStatus(
status: 'active' | 'away' | 'left'
): MiniAppMemberAbsencePolicy {
if (status === 'away') {
return 'away_rent_and_utilities'
}
if (status === 'left') {
return 'inactive'
}
return 'resident'
}
function resolvedMemberAbsencePolicy(
memberId: string,
status: 'active' | 'away' | 'left',
settings = adminSettings()
) {
const current = settings?.memberAbsencePolicies
.filter((policy) => policy.memberId === memberId)
.sort((left, right) => left.effectiveFromPeriod.localeCompare(right.effectiveFromPeriod))
.at(-1)
return (
current ?? {
memberId,
effectiveFromPeriod: '',
policy: defaultAbsencePolicyForStatus(status)
}
)
}
async function loadDashboard(initData: string) {
try {
const nextDashboard = await fetchMiniAppDashboard(initData)
@@ -441,6 +482,14 @@ function App() {
setMemberStatusDrafts(
Object.fromEntries(payload.members.map((member) => [member.id, member.status]))
)
setMemberAbsencePolicyDrafts(
Object.fromEntries(
payload.members.map((member) => [
member.id,
resolvedMemberAbsencePolicy(member.id, member.status, payload).policy
])
)
)
setCycleForm((current) => ({
...current,
rentCurrency: payload.settings.rentCurrency,
@@ -1276,11 +1325,66 @@ function App() {
...current,
[member.id]: member.status
}))
setMemberAbsencePolicyDrafts((current) => ({
...current,
[member.id]:
current[member.id] ??
resolvedMemberAbsencePolicy(member.id, member.status).policy ??
defaultAbsencePolicyForStatus(member.status)
}))
} finally {
setSavingMemberStatusId(null)
}
}
async function handleSaveMemberAbsencePolicy(memberId: string) {
const initData = webApp?.initData?.trim()
const currentReady = readySession()
const member = adminSettings()?.members.find((entry) => entry.id === memberId)
const nextPolicy = memberAbsencePolicyDrafts()[memberId]
const effectiveStatus = memberStatusDrafts()[memberId] ?? member?.status
if (
!initData ||
currentReady?.mode !== 'live' ||
!currentReady.member.isAdmin ||
!member ||
!nextPolicy ||
effectiveStatus !== 'away'
) {
return
}
setSavingMemberAbsencePolicyId(memberId)
try {
const savedPolicy = await updateMiniAppMemberAbsencePolicy(initData, memberId, nextPolicy)
setAdminSettings((current) =>
current
? {
...current,
memberAbsencePolicies: [
...current.memberAbsencePolicies.filter(
(policy) =>
!(
policy.memberId === savedPolicy.memberId &&
policy.effectiveFromPeriod === savedPolicy.effectiveFromPeriod
)
),
savedPolicy
]
}
: current
)
setMemberAbsencePolicyDrafts((current) => ({
...current,
[memberId]: savedPolicy.policy
}))
} finally {
setSavingMemberAbsencePolicyId(null)
}
}
const renderPanel = () => {
switch (activeNav()) {
case 'balances':
@@ -2447,6 +2551,44 @@ function App() {
<option value="left">{copy().memberStatusLeft}</option>
</select>
</label>
<label class="settings-field settings-field--wide">
<span>{copy().absencePolicyLabel}</span>
<select
value={
memberAbsencePolicyDrafts()[member.id] ??
resolvedMemberAbsencePolicy(member.id, member.status).policy
}
disabled={
(memberStatusDrafts()[member.id] ?? member.status) !== 'away'
}
onChange={(event) =>
setMemberAbsencePolicyDrafts((current) => ({
...current,
[member.id]: event.currentTarget
.value as MiniAppMemberAbsencePolicy
}))
}
>
<option value="away_rent_and_utilities">
{copy().absencePolicyAwayRentAndUtilities}
</option>
<option value="away_rent_only">
{copy().absencePolicyAwayRentOnly}
</option>
<option value="inactive">{copy().absencePolicyInactive}</option>
<option value="resident">{copy().absencePolicyResident}</option>
</select>
<small>
{resolvedMemberAbsencePolicy(member.id, member.status)
.effectiveFromPeriod
? copy().absencePolicyEffectiveFrom.replace(
'{period}',
resolvedMemberAbsencePolicy(member.id, member.status)
.effectiveFromPeriod
)
: copy().absencePolicyHint}
</small>
</label>
<label class="settings-field settings-field--wide">
<span>{copy().rentWeightLabel}</span>
<input
@@ -2474,6 +2616,19 @@ function App() {
? copy().savingMemberStatus
: copy().saveMemberStatusAction}
</button>
<button
class="ghost-button"
type="button"
disabled={
savingMemberAbsencePolicyId() === member.id ||
(memberStatusDrafts()[member.id] ?? member.status) !== 'away'
}
onClick={() => void handleSaveMemberAbsencePolicy(member.id)}
>
{savingMemberAbsencePolicyId() === member.id
? copy().savingAbsencePolicy
: copy().saveAbsencePolicyAction}
</button>
<button
class="ghost-button"
type="button"

View File

@@ -144,6 +144,15 @@ export const dictionary = {
memberStatusActive: 'Active',
memberStatusAway: 'Away',
memberStatusLeft: 'Left',
absencePolicyLabel: 'Away billing policy',
absencePolicyResident: 'Resident billing',
absencePolicyAwayRentAndUtilities: 'Away: rent and utilities',
absencePolicyAwayRentOnly: 'Away: rent only',
absencePolicyInactive: 'Inactive / moved out',
absencePolicyHint: 'Applies to future cycle calculations for away members.',
absencePolicyEffectiveFrom: 'Effective from {period}',
saveAbsencePolicyAction: 'Save away policy',
savingAbsencePolicy: 'Saving policy…',
memberStatusSummary: 'Your household status: {status}.',
rentWeightLabel: 'Rent weight',
saveRentWeightAction: 'Save rent weight',
@@ -309,6 +318,15 @@ export const dictionary = {
memberStatusActive: 'Активный',
memberStatusAway: 'В отъезде',
memberStatusLeft: 'Выехал',
absencePolicyLabel: 'Политика начислений в отъезде',
absencePolicyResident: 'Как у проживающего',
absencePolicyAwayRentAndUtilities: 'В отъезде: аренда и коммуналка',
absencePolicyAwayRentOnly: 'В отъезде: только аренда',
absencePolicyInactive: 'Неактивен / выехал',
absencePolicyHint: 'Применяется к будущим расчётам для участников со статусом «В отъезде».',
absencePolicyEffectiveFrom: 'Действует с {period}',
saveAbsencePolicyAction: 'Сохранить политику',
savingAbsencePolicy: 'Сохраняем политику…',
memberStatusSummary: 'Твой статус в household: {status}.',
rentWeightLabel: 'Вес аренды',
saveRentWeightAction: 'Сохранить вес аренды',

View File

@@ -37,6 +37,18 @@ export interface MiniAppPendingMember {
languageCode: string | null
}
export type MiniAppMemberAbsencePolicy =
| 'resident'
| 'away_rent_and_utilities'
| 'away_rent_only'
| 'inactive'
export interface MiniAppMemberAbsencePolicyRecord {
memberId: string
effectiveFromPeriod: string
policy: MiniAppMemberAbsencePolicy
}
export interface MiniAppMember {
id: string
displayName: string
@@ -116,6 +128,7 @@ export interface MiniAppAdminSettingsPayload {
topics: readonly MiniAppTopicBinding[]
categories: readonly MiniAppUtilityCategory[]
members: readonly MiniAppMember[]
memberAbsencePolicies: readonly MiniAppMemberAbsencePolicyRecord[]
}
export interface MiniAppAdminCycleState {
@@ -362,6 +375,7 @@ export async function fetchMiniAppAdminSettings(
topics?: MiniAppTopicBinding[]
categories?: MiniAppUtilityCategory[]
members?: MiniAppMember[]
memberAbsencePolicies?: MiniAppMemberAbsencePolicyRecord[]
error?: string
}
@@ -371,7 +385,8 @@ export async function fetchMiniAppAdminSettings(
!payload.settings ||
!payload.topics ||
!payload.categories ||
!payload.members
!payload.members ||
!payload.memberAbsencePolicies
) {
throw new Error(payload.error ?? 'Failed to load admin settings')
}
@@ -380,7 +395,8 @@ export async function fetchMiniAppAdminSettings(
settings: payload.settings,
topics: payload.topics,
categories: payload.categories,
members: payload.members
members: payload.members,
memberAbsencePolicies: payload.memberAbsencePolicies
}
}
@@ -547,6 +563,37 @@ export async function updateMiniAppMemberStatus(
return payload.member
}
export async function updateMiniAppMemberAbsencePolicy(
initData: string,
memberId: string,
policy: MiniAppMemberAbsencePolicy
): Promise<MiniAppMemberAbsencePolicyRecord> {
const response = await fetch(`${apiBaseUrl()}/api/miniapp/admin/members/absence-policy`, {
method: 'POST',
headers: {
'content-type': 'application/json'
},
body: JSON.stringify({
initData,
memberId,
policy
})
})
const payload = (await response.json()) as {
ok: boolean
authorized?: boolean
policy?: MiniAppMemberAbsencePolicyRecord
error?: string
}
if (!response.ok || !payload.policy) {
throw new Error(payload.error ?? 'Failed to update member absence policy')
}
return payload.policy
}
export async function fetchMiniAppBillingCycle(initData: string): Promise<MiniAppAdminCycleState> {
const response = await fetch(`${apiBaseUrl()}/api/miniapp/admin/billing-cycle`, {
method: 'POST',