Files
craftvia/src/lib/billing/schemas.ts
T
msolarczekandClaude Opus 5 ac2d857e26 L14 Abrechnungsübersicht: Services für Einträge, Aufstellung, Abrechnen/Stornieren, Meilensteine und PDF
syncBillingCandidates (idempotent, Event-Hook für work_order.released_for_billing und report.approved), Aufstellung ohne Preise (nur freigegebene Zeiten, Fahrzeit getrennt, Pausen informativ, Anfahrten-Zählung, Material), markBilled mit eingefrorenem Snapshot und Positionszuordnung, voidBilling mit Grund und neuem offenen Eintrag (billed → released_for_billing nur über applyTransition-Option billingVoid), Meilenstein-Flow mit Events, Job billing-pdf (Dokument other/backoffice_only), Sync-Op milestone.reach.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 10:35:38 +02:00

78 lines
3.0 KiB
TypeScript

import { z } from "zod";
/**
* L14 Abrechnungsübersicht — input schemas and constants (client-safe).
* Scope: overview for the external accounting. No prices, amounts, taxes, invoices or payments.
*/
export const BILLING_KINDS = ["order_completion", "milestone", "daily_report"] as const;
export type BillingKind = (typeof BILLING_KINDS)[number];
export const BILLING_STATUSES = ["open", "billed", "voided"] as const;
export type BillingStatus = (typeof BILLING_STATUSES)[number];
export const MILESTONE_STATUSES = ["open", "reached", "confirmed", "billed"] as const;
export type MilestoneStatus = (typeof MILESTONE_STATUSES)[number];
const id = z.string().trim().min(1).max(64);
const optionalText = (max: number) =>
z
.string()
.trim()
.max(max)
.nullish()
.transform((v) => (v ? v : null));
export const invoiceNumberField = optionalText(100);
export const markBilledSchema = z.object({
recordId: id,
invoiceNumber: invoiceNumberField,
/** required when approved-pending time entries exist: they are NOT part of the billed section */
confirmPendingExcluded: z.boolean().optional(),
});
export type MarkBilledInput = z.input<typeof markBilledSchema>;
export const voidBillingSchema = z.object({ recordId: id, reason: z.string().trim().min(1).max(2000) });
export type VoidBillingInput = z.input<typeof voidBillingSchema>;
export const updateInvoiceNumberSchema = z.object({ recordId: id, invoiceNumber: invoiceNumberField });
export const createMilestoneSchema = z.object({
workOrderId: id,
title: z.string().trim().min(1).max(200),
description: optionalText(2000),
});
export type CreateMilestoneInput = z.input<typeof createMilestoneSchema>;
export const updateMilestoneSchema = z.object({
milestoneId: id,
title: z.string().trim().min(1).max(200).optional(),
description: optionalText(2000).optional(),
});
export const moveMilestoneSchema = z.object({ milestoneId: id, direction: z.enum(["up", "down"]) });
export const rejectMilestoneSchema = z.object({ milestoneId: id, reason: z.string().trim().min(1).max(2000) });
/** Sync op `milestone.reach` (offline-capable, idempotent: an already reached milestone stays unchanged). */
export const milestoneReachPayload = z.object({
workOrderId: id,
milestoneId: id,
note: optionalText(2000),
});
export const billingListFilterSchema = z.object({
status: z.enum(BILLING_STATUSES).catch("open").default("open"),
kind: z.enum(BILLING_KINDS).optional().catch(undefined),
customerId: id.optional().catch(undefined),
teamId: id.optional().catch(undefined),
/** period overlap (YYYY-MM-DD, tenant time zone) */
from: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().catch(undefined),
to: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().catch(undefined),
q: z.string().trim().max(100).optional().catch(undefined),
page: z.coerce.number().int().min(1).catch(1).default(1),
pageSize: z.coerce.number().int().min(1).max(100).catch(25).default(25),
});
export type BillingListFilter = z.input<typeof billingListFilterSchema>;