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>
78 lines
3.0 KiB
TypeScript
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>;
|