Merge lane/abrechnung in feature/craftvia-mvp
Konflikte gelöst (additiv): Benachrichtigungstexte (Planung + Meilensteine), Event-Typen, Navigation, Job-Queues/Prozessoren (geocode-site, planning-watch, billing-pdf), OpenAPI-Tags, Smoke-Prüfungen (zweiter Mandant: Planung + Abrechnung). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
import { requireApiContext } from "@/server/api/context";
|
||||
import { json, readJsonObject, withApi } from "@/server/api/respond";
|
||||
import { markBilled } from "@/server/services/billing/records";
|
||||
|
||||
/**
|
||||
* POST /api/v1/billing/:id/billed — mark as billed (freeze statement, assign positions, PDF job).
|
||||
* Body (optional): { invoiceNumber?: string, confirmPendingExcluded?: boolean }.
|
||||
*/
|
||||
export const POST = withApi(async (req: Request, { params }: { params: Promise<{ id: string }> }) => {
|
||||
const ctx = await requireApiContext("billing", "billing:write");
|
||||
const { id } = await params;
|
||||
const body = await readJsonObject(req, { allowEmpty: true });
|
||||
const record = await markBilled(ctx, {
|
||||
recordId: id,
|
||||
invoiceNumber: typeof body.invoiceNumber === "string" ? body.invoiceNumber : null,
|
||||
confirmPendingExcluded: body.confirmPendingExcluded === true,
|
||||
});
|
||||
return json({ id: record.id, status: record.status, invoiceNumber: record.invoiceNumber, billedAt: record.billedAt });
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
import { requireApiContext } from "@/server/api/context";
|
||||
import { json, withApi } from "@/server/api/respond";
|
||||
import { getBillingRecordDetail } from "@/server/services/billing/queries";
|
||||
|
||||
/** GET /api/v1/billing/:id — billing record with its statement (frozen snapshot once billed). */
|
||||
export const GET = withApi(async (_req: Request, { params }: { params: Promise<{ id: string }> }) => {
|
||||
const ctx = await requireApiContext("billing", "billing:read");
|
||||
const { id } = await params;
|
||||
return json(await getBillingRecordDetail(ctx, id));
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
import { requireApiContext } from "@/server/api/context";
|
||||
import { json, readJsonObject, withApi } from "@/server/api/respond";
|
||||
import { voidBilling } from "@/server/services/billing/records";
|
||||
|
||||
/** POST /api/v1/billing/:id/void — void a billed record. Body: { reason: string } (required). */
|
||||
export const POST = withApi(async (req: Request, { params }: { params: Promise<{ id: string }> }) => {
|
||||
const ctx = await requireApiContext("billing", "billing:write");
|
||||
const { id } = await params;
|
||||
const body = await readJsonObject(req);
|
||||
const res = await voidBilling(ctx, { recordId: id, reason: typeof body.reason === "string" ? body.reason : "" });
|
||||
return json({ id: res.voided.id, status: res.voided.status, reopenedId: res.reopened?.id ?? null });
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
import { requireApiContext } from "@/server/api/context";
|
||||
import { paginated, parsePagination, withApi } from "@/server/api/respond";
|
||||
import { listBillingRecords } from "@/server/services/billing/queries";
|
||||
|
||||
/**
|
||||
* GET /api/v1/billing — billing overview (L14). Query: status (open|billed|voided, default open), kind,
|
||||
* customerId, teamId, from/to (YYYY-MM-DD), q, page, pageSize. No prices or amounts.
|
||||
*/
|
||||
export const GET = withApi(async (req: Request) => {
|
||||
const ctx = await requireApiContext("billing", "billing:read");
|
||||
const url = new URL(req.url);
|
||||
const { page, pageSize } = parsePagination(url);
|
||||
const sp = url.searchParams;
|
||||
const res = await listBillingRecords(ctx, {
|
||||
status: (sp.get("status") ?? undefined) as never,
|
||||
kind: (sp.get("kind") ?? undefined) as never,
|
||||
customerId: sp.get("customerId") ?? undefined,
|
||||
teamId: sp.get("teamId") ?? undefined,
|
||||
from: sp.get("from") ?? undefined,
|
||||
to: sp.get("to") ?? undefined,
|
||||
q: sp.get("q") ?? undefined,
|
||||
page,
|
||||
pageSize,
|
||||
});
|
||||
return paginated(res.items, res.total, res.page, res.pageSize);
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
import { requireApiContext } from "@/server/api/context";
|
||||
import { json, withApi } from "@/server/api/respond";
|
||||
import { confirmMilestone } from "@/server/services/billing/milestones";
|
||||
|
||||
/** POST /api/v1/milestones/:id/confirm — confirm a reported milestone → open billing record. */
|
||||
export const POST = withApi(async (_req: Request, { params }: { params: Promise<{ id: string }> }) => {
|
||||
const ctx = await requireApiContext("billing", "billing:write");
|
||||
const { id } = await params;
|
||||
return json(await confirmMilestone(ctx, id));
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import { requireApiContext } from "@/server/api/context";
|
||||
import { json, readJsonObject, withApi } from "@/server/api/respond";
|
||||
import { markMilestoneReached } from "@/server/services/billing/milestones";
|
||||
|
||||
/**
|
||||
* POST /api/v1/milestones/:id/reach — report a milestone as reached (field:execute + order in scope,
|
||||
* or work_order:write). Idempotent. Body (optional): { note?: string }. Offline: sync op `milestone.reach`.
|
||||
*/
|
||||
export const POST = withApi(async (req: Request, { params }: { params: Promise<{ id: string }> }) => {
|
||||
const ctx = await requireApiContext("billing");
|
||||
const { id } = await params;
|
||||
const body = await readJsonObject(req, { allowEmpty: true });
|
||||
const res = await markMilestoneReached(ctx, { milestoneId: id, note: typeof body.note === "string" ? body.note : null });
|
||||
return json({ milestone: res.milestone, changed: res.changed });
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { requireApiContext } from "@/server/api/context";
|
||||
import { json, readJsonObject, withApi } from "@/server/api/respond";
|
||||
import { rejectMilestone } from "@/server/services/billing/milestones";
|
||||
|
||||
/** POST /api/v1/milestones/:id/reject — reject a reported milestone. Body: { reason: string } (required). */
|
||||
export const POST = withApi(async (req: Request, { params }: { params: Promise<{ id: string }> }) => {
|
||||
const ctx = await requireApiContext("billing", "billing:write");
|
||||
const { id } = await params;
|
||||
const body = await readJsonObject(req);
|
||||
return json(await rejectMilestone(ctx, { milestoneId: id, reason: typeof body.reason === "string" ? body.reason : "" }));
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
import { requireApiContext } from "@/server/api/context";
|
||||
import { json, readJsonObject, withApi } from "@/server/api/respond";
|
||||
import { createMilestone, listMilestones } from "@/server/services/billing/milestones";
|
||||
|
||||
/** GET /api/v1/work-orders/:id/milestones — milestones of a visible order (field roles: scope). */
|
||||
export const GET = withApi(async (_req: Request, { params }: { params: Promise<{ id: string }> }) => {
|
||||
const ctx = await requireApiContext("billing");
|
||||
const { id } = await params;
|
||||
return json({ data: await listMilestones(ctx, id) });
|
||||
});
|
||||
|
||||
/** POST /api/v1/work-orders/:id/milestones — define a milestone. Body: { title, description? }. */
|
||||
export const POST = withApi(async (req: Request, { params }: { params: Promise<{ id: string }> }) => {
|
||||
const ctx = await requireApiContext("billing", "work_order:write");
|
||||
const { id } = await params;
|
||||
const body = await readJsonObject(req);
|
||||
const m = await createMilestone(ctx, { workOrderId: id, title: typeof body.title === "string" ? body.title : "", description: typeof body.description === "string" ? body.description : null });
|
||||
return json(m, { status: 201 });
|
||||
});
|
||||
Reference in New Issue
Block a user