augmenttoolkit / shared /src /budget /budget.schema.ts
Leon4gr45's picture
Upload folder using huggingface_hub (part 7)
98fbc6d verified
Raw
History Blame Contribute Delete
7.22 kB
import { z } from 'zod';
/**
* Budget API contract — single source of truth for the /api/trips/:tripId/budget
* endpoints (expense items, per-member splits, paid toggles, settlement).
*
* Trip-scoped: every endpoint verifies trip access (404 "Trip not found") and
* mutations check the 'budget_edit' permission (403 "No permission"). The legacy
* route (server/src/routes/budget.ts) wraps services/budgetService.ts; rows are
* DB-shaped and kept open. Mutations broadcast over WebSocket with the forwarded
* X-Socket-Id. Updating a linked item's total_price also syncs the price into the
* linked reservation's metadata (and broadcasts reservation:updated).
*/
/**
* Budget item member as embedded on a budget item
* (server/src/services/budgetService.ts -> loadItemMembers). `paid` is the raw
* SQLite INTEGER (0/1); `avatar_url` is the resolved avatar (avatarUrl()).
*/
export const budgetItemMemberSchema = z.object({
user_id: z.number(),
paid: z.number(),
username: z.string(),
avatar_url: z.string().nullable().optional(),
avatar: z.string().nullable().optional(),
budget_item_id: z.number().optional(),
});
export type BudgetItemMember = z.infer<typeof budgetItemMemberSchema>;
/**
* The fixed "Costs" expense categories. Unlike the old budget, users cannot
* create their own categories — every expense maps to one of these keys. The
* label/icon/colour per key live in the client; the server only stores the key.
* Pre-rework rows used free-text categories; those are shown as `other`.
*/
export const COST_CATEGORIES = [
'accommodation',
'food',
'groceries',
'transport',
'flights',
'activities',
'sightseeing',
'shopping',
'fees',
'health',
'tips',
'other',
] as const;
export type CostCategory = (typeof COST_CATEGORIES)[number];
/**
* One payer of an expense — a row of budget_item_payers. `amount` is in the
* expense's own currency (budget_items.currency). Several payers can split who
* actually paid one bill. Username/avatar are joined for display.
*/
export const budgetItemPayerSchema = z.object({
user_id: z.number(),
amount: z.number(),
username: z.string().optional(),
avatar_url: z.string().nullable().optional(),
avatar: z.string().nullable().optional(),
budget_item_id: z.number().optional(),
});
export type BudgetItemPayer = z.infer<typeof budgetItemPayerSchema>;
/**
* Budget item entity as returned by the budget list/create/update endpoints
* (server/src/services/budgetService.ts). Columns of the `budget_items` table
* plus the embedded `members` (equal-split participants) and `payers` arrays.
* total_price is the sum of payer amounts in `currency`; `exchange_rate` converts
* that to the trip base currency (NULL currency + rate 1 = base currency).
*/
export const budgetItemSchema = z.object({
id: z.number(),
trip_id: z.number(),
category: z.string(),
name: z.string(),
total_price: z.number(),
currency: z.string().nullable().optional(),
exchange_rate: z.number().optional(),
persons: z.number().nullable().optional(),
days: z.number().nullable().optional(),
note: z.string().nullable().optional(),
reservation_id: z.number().nullable().optional(),
paid_by_user_id: z.number().nullable().optional(),
expense_date: z.string().nullable().optional(),
sort_order: z.number().optional(),
created_at: z.string().optional(),
members: z.array(budgetItemMemberSchema).optional(),
payers: z.array(budgetItemPayerSchema).optional(),
});
export type BudgetItem = z.infer<typeof budgetItemSchema>;
const payerInputSchema = z.object({
user_id: z.number(),
amount: z.number(),
});
export const budgetCreateItemRequestSchema = z.object({
name: z.string().min(1),
category: z.string().optional(),
total_price: z.number().optional(),
currency: z.string().nullable().optional(),
exchange_rate: z.number().optional(),
// Multi-payer: who paid how much (in the expense currency). When omitted, the
// server falls back to total_price with no explicit payer.
payers: z.array(payerInputSchema).optional(),
// Equal-split participants. When omitted, the item has no split (planning-only).
member_ids: z.array(z.number()).optional(),
persons: z.number().nullable().optional(),
days: z.number().nullable().optional(),
note: z.string().nullable().optional(),
expense_date: z.string().nullable().optional(),
});
export type BudgetCreateItemRequest = z.infer<
typeof budgetCreateItemRequestSchema
>;
/** Update accepts the same fields plus total_price changes; all optional. */
export const budgetUpdateItemRequestSchema = z.object({
name: z.string().optional(),
category: z.string().optional(),
total_price: z.number().optional(),
currency: z.string().nullable().optional(),
exchange_rate: z.number().optional(),
payers: z.array(payerInputSchema).optional(),
member_ids: z.array(z.number()).optional(),
persons: z.number().nullable().optional(),
days: z.number().nullable().optional(),
note: z.string().nullable().optional(),
expense_date: z.string().nullable().optional(),
});
export type BudgetUpdateItemRequest = z.infer<
typeof budgetUpdateItemRequestSchema
>;
/** Replace the explicit payers of an expense (amounts in expense currency). */
export const budgetUpdatePayersRequestSchema = z.object({
payers: z.array(payerInputSchema),
});
export type BudgetUpdatePayersRequest = z.infer<
typeof budgetUpdatePayersRequestSchema
>;
/**
* A persisted settle-up transfer (budget_settlements row): "from paid to" a
* given amount in the trip base currency. Creating one marks a suggested flow as
* paid; deleting it (undo) brings the flow back. Names joined for display.
*/
export const budgetSettlementSchema = z.object({
id: z.number(),
trip_id: z.number(),
from_user_id: z.number(),
to_user_id: z.number(),
amount: z.number(),
created_at: z.string().optional(),
created_by_user_id: z.number().nullable().optional(),
from_username: z.string().optional(),
from_avatar_url: z.string().nullable().optional(),
to_username: z.string().optional(),
to_avatar_url: z.string().nullable().optional(),
});
export type BudgetSettlement = z.infer<typeof budgetSettlementSchema>;
export const budgetCreateSettlementRequestSchema = z.object({
from_user_id: z.number(),
to_user_id: z.number(),
amount: z.number(),
});
export type BudgetCreateSettlementRequest = z.infer<
typeof budgetCreateSettlementRequestSchema
>;
export const budgetUpdateMembersRequestSchema = z.object({
user_ids: z.array(z.number()),
});
export type BudgetUpdateMembersRequest = z.infer<
typeof budgetUpdateMembersRequestSchema
>;
export const budgetToggleMemberPaidRequestSchema = z.object({
paid: z.boolean(),
});
export type BudgetToggleMemberPaidRequest = z.infer<
typeof budgetToggleMemberPaidRequestSchema
>;
export const budgetReorderItemsRequestSchema = z.object({
orderedIds: z.array(z.number()),
});
export type BudgetReorderItemsRequest = z.infer<
typeof budgetReorderItemsRequestSchema
>;
export const budgetReorderCategoriesRequestSchema = z.object({
orderedCategories: z.array(z.string()),
});
export type BudgetReorderCategoriesRequest = z.infer<
typeof budgetReorderCategoriesRequestSchema
>;