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>
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
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>;
|
||||
Reference in New Issue
Block a user