+
{/* Invoice Details */}
@@ -89,12 +113,12 @@ export function InvoiceGeneralInfo({ invoice }: InvoiceGeneralInfoProps) {
)}
{invoice.status && (
- {invoice.status.charAt(0).toUpperCase() + invoice.status.slice(1)}
+ {formatEnum(invoice.status)}
)}
-
+
-
- {/* Relations */}
-
-
-
-
- Related Information
-
-
-
-
-
-
-
-
+ {/* Customer & Vehicle Information */}
+
+ {/* Customer Details */}
+
+
+
+
+ Customer Information
+
+
+
+
+
+
+
+
+
+ {customer.address_line_1 && (
+ <>
+
+
+
Address
+
+ {customer.address_line_1}
+ {customer.address_line_2 ? `, ${customer.address_line_2}` : ""}
+
+ {customer.city ? `${customer.city}` : ""}
+ {customer.zip_code ? `, ${customer.zip_code}` : ""}
+
+
+ >
+ )}
+
+
+
+ {/* Vehicle Details */}
+
+
+
+
+ Vehicle Information
+
+
+
+
+
+
+
+
+
+ {vehicle.mileage && (
+ <>
+
+
+ >
+ )}
+
+
+
+
+ {/* Payment & Insurance Information */}
+
+ {/* Payment Information */}
+
+
+
+
+ Payment Information
+
+
+
+
+
+
+
+
+
+
+
+ {/* Insurance & Additional Info */}
+
+
+
+
+ Additional Information
+
+
+
+
+
+
+ {invoice.has_insurance && insurer.id && (
+
+ )}
+ {invoice.kms_in && (
+
+ )}
+
+
+
+
+
+ {/* Notes & Terms */}
+ {(invoice.notes || invoice.terms_and_conditions) && (
+
{invoice.notes && (
- <>
-
-
-
Notes
-
{invoice.notes}
-
- >
+
+
+ Notes
+
+
+ {invoice.notes}
+
+
)}
-
-
+ {invoice.terms_and_conditions && (
+
+
+ Terms & Conditions
+
+
+ {invoice.terms_and_conditions}
+
+
+ )}
+
+ )}
+
)
}
diff --git a/apps/dashboard/modules/invoices/invoice-parts-section.tsx b/apps/dashboard/modules/invoices/invoice-parts-section.tsx
new file mode 100644
index 0000000..1f32cbe
--- /dev/null
+++ b/apps/dashboard/modules/invoices/invoice-parts-section.tsx
@@ -0,0 +1,103 @@
+"use client"
+
+import { Wrench } from "lucide-react"
+import {
+ Card,
+ CardContent,
+ CardHeader,
+ CardTitle,
+} from "@/shared/components/ui/card"
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/shared/components/ui/table"
+import { formatCurrency, formatNumber } from "@/shared/utils/formatters"
+
+type InvoicePart = {
+ id: number
+ invoice_id: number
+ part_id: number
+ quantity: string | number
+ rate: string | number
+ description?: string
+ chart_of_account?: string
+ department_id?: number
+ created_at?: string
+ updated_at?: string
+}
+
+type InvoicePartsSectionProps = {
+ parts?: InvoicePart[]
+}
+
+export function InvoicePartsSection({ parts = [] }: InvoicePartsSectionProps) {
+ if (!parts || parts.length === 0) {
+ return null
+ }
+
+ const subtotal = parts.reduce((sum, part) => {
+ const qty = parseFloat(String(part.quantity))
+ const rate = parseFloat(String(part.rate))
+ return sum + (qty * rate)
+ }, 0)
+
+ return (
+
+
+
+
+ Parts
+
+
+
+
+
+
+
+ Description
+ Quantity
+ Rate
+ Amount
+
+
+
+ {parts.map((part) => {
+ const qty = parseFloat(String(part.quantity))
+ const rate = parseFloat(String(part.rate))
+ const amount = qty * rate
+ return (
+
+
+ {part.description || `Part #${part.part_id}`}
+
+
+ {formatNumber(qty)}
+
+
+ {formatCurrency(rate)}
+
+
+ {formatCurrency(amount)}
+
+
+ )
+ })}
+
+
+ Subtotal
+
+
+ {formatCurrency(subtotal)}
+
+
+
+
+
+
+
+ )
+}
diff --git a/apps/dashboard/modules/invoices/invoice-services-section.tsx b/apps/dashboard/modules/invoices/invoice-services-section.tsx
new file mode 100644
index 0000000..cd1c3fc
--- /dev/null
+++ b/apps/dashboard/modules/invoices/invoice-services-section.tsx
@@ -0,0 +1,103 @@
+"use client"
+
+import { Briefcase } from "lucide-react"
+import {
+ Card,
+ CardContent,
+ CardHeader,
+ CardTitle,
+} from "@/shared/components/ui/card"
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/shared/components/ui/table"
+import { formatCurrency, formatNumber } from "@/shared/utils/formatters"
+
+type InvoiceService = {
+ id: number
+ invoice_id: number
+ service_id: number
+ quantity: string | number
+ rate: string | number
+ description?: string
+ chart_of_account?: string
+ department_id?: number
+ created_at?: string
+ updated_at?: string
+}
+
+type InvoiceServicesSectionProps = {
+ services?: InvoiceService[]
+}
+
+export function InvoiceServicesSection({ services = [] }: InvoiceServicesSectionProps) {
+ if (!services || services.length === 0) {
+ return null
+ }
+
+ const subtotal = services.reduce((sum, service) => {
+ const qty = parseFloat(String(service.quantity))
+ const rate = parseFloat(String(service.rate))
+ return sum + (qty * rate)
+ }, 0)
+
+ return (
+
+
+
+
+ Services
+
+
+
+
+
+
+
+ Description
+ Quantity
+ Rate
+ Amount
+
+
+
+ {services.map((service) => {
+ const qty = parseFloat(String(service.quantity))
+ const rate = parseFloat(String(service.rate))
+ const amount = qty * rate
+ return (
+
+
+ {service.description || `Service #${service.service_id}`}
+
+
+ {formatNumber(qty)}
+
+
+ {formatCurrency(rate)}
+
+
+ {formatCurrency(amount)}
+
+
+ )
+ })}
+
+
+ Subtotal
+
+
+ {formatCurrency(subtotal)}
+
+
+
+
+
+
+
+ )
+}
diff --git a/apps/dashboard/modules/invoices/invoice.schema.ts b/apps/dashboard/modules/invoices/invoice.schema.ts
index 5d3da94..d7fbda6 100644
--- a/apps/dashboard/modules/invoices/invoice.schema.ts
+++ b/apps/dashboard/modules/invoices/invoice.schema.ts
@@ -4,6 +4,30 @@ const relationFieldSchema = z
.object({ value: z.string(), label: z.string() })
.nullable()
+const invoicePartItemSchema = z.object({
+ part_id: z.number(),
+ title: z.string(),
+ quantity: z.number().min(1),
+ rate: z.number().min(0),
+ description: z.string().optional(),
+})
+
+const invoiceServiceItemSchema = z.object({
+ service_id: z.number(),
+ title: z.string(),
+ quantity: z.number().min(1),
+ rate: z.number().min(0),
+ description: z.string().optional(),
+})
+
+const invoiceExpenseItemSchema = z.object({
+ expense_id: z.number(),
+ title: z.string(),
+ quantity: z.number().min(1),
+ rate: z.number().min(0),
+ description: z.string().optional(),
+})
+
const invoiceFormSchema = z.object({
// ── Required fields ──
subject: z.string().min(1, "Subject is required"),
@@ -12,16 +36,36 @@ const invoiceFormSchema = z.object({
customer: relationFieldSchema,
vehicle: relationFieldSchema,
department: relationFieldSchema,
+ estimate: relationFieldSchema,
+ payment_terms: relationFieldSchema,
+ invoice_sequence: relationFieldSchema,
+ payment_mode: relationFieldSchema,
+ insurer: relationFieldSchema,
+ invoice_to: relationFieldSchema,
// ── Optional fields ──
invoice_number: z.string().optional(),
+ invoice_title: z.string().optional(),
invoice_date: z.string().optional(),
due_date: z.string().optional(),
status: z.string().optional(),
+ kms_in: z.coerce.number().optional(),
+ has_insurance: z.boolean().default(false),
+ discount: z.string().optional(),
+ deposit_to: z.string().optional(),
notes: z.string().optional(),
+ terms_and_conditions: z.string().optional(),
+
+ // ── Line items ──
+ parts: z.array(invoicePartItemSchema).optional(),
+ services: z.array(invoiceServiceItemSchema).optional(),
+ expense_items: z.array(invoiceExpenseItemSchema).optional(),
})
type InvoiceFormValues = z.infer
+type InvoicePartItem = z.infer
+type InvoiceServiceItem = z.infer
+type InvoiceExpenseItem = z.infer
-export { invoiceFormSchema, relationFieldSchema }
-export type { InvoiceFormValues }
+export { invoiceFormSchema, relationFieldSchema, invoicePartItemSchema, invoiceServiceItemSchema, invoiceExpenseItemSchema }
+export type { InvoiceFormValues, InvoicePartItem, InvoiceServiceItem, InvoiceExpenseItem }
diff --git a/packages/api/open-api/schema.json b/packages/api/open-api/schema.json
index bab8e16..149b234 100644
--- a/packages/api/open-api/schema.json
+++ b/packages/api/open-api/schema.json
@@ -462,8 +462,8 @@
],
"summary": "GET /api/profile",
"responses": {
- "200": {
- "description": "OK",
+ "201": {
+ "description": "Created",
"content": {
"application/json": {
"schema": {
@@ -21668,6 +21668,290 @@
}
},
"/api/estimates/{id}": {
+ "get": {
+ "tags": [
+ "Estimates"
+ ],
+ "summary": "Display the specified estimate.",
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "title": {
+ "type": "string"
+ },
+ "customer_id": {
+ "type": "integer"
+ },
+ "vehicle_id": {
+ "type": "integer"
+ },
+ "department_id": {
+ "type": "integer"
+ },
+ "estimate_number": {
+ "type": "string"
+ },
+ "date": {
+ "type": "string"
+ },
+ "has_insurance": {
+ "type": "boolean"
+ },
+ "enable_digital_authorisation": {
+ "type": "boolean"
+ },
+ "insurance_type_id": {
+ "type": "integer"
+ },
+ "insurer_id": {
+ "type": "integer"
+ },
+ "service_writer_id": {
+ "type": "integer"
+ },
+ "footer": {
+ "type": "string"
+ },
+ "created_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "updated_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "labels": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "title": {
+ "type": "string"
+ },
+ "color_code": {
+ "type": "string"
+ },
+ "pivot": {
+ "type": "object",
+ "properties": {
+ "estimate_id": {
+ "type": "integer"
+ },
+ "label_id": {
+ "type": "integer"
+ }
+ }
+ }
+ }
+ }
+ },
+ "customer_remarks": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "estimate_id": {
+ "type": "integer"
+ },
+ "remark": {
+ "type": "string"
+ },
+ "created_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "updated_at": {
+ "type": "string",
+ "format": "date-time"
+ }
+ }
+ }
+ },
+ "customer": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "first_name": {
+ "type": "string"
+ },
+ "last_name": {
+ "type": "string"
+ }
+ }
+ },
+ "vehicle": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "registration_number": {
+ "type": "string"
+ },
+ "make": {
+ "type": "string"
+ },
+ "model": {
+ "type": "string"
+ }
+ }
+ },
+ "department": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "name": {
+ "type": "string"
+ }
+ }
+ },
+ "insurance_type": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "title": {
+ "type": "string"
+ }
+ }
+ },
+ "insurer": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "first_name": {
+ "type": "string"
+ },
+ "last_name": {
+ "type": "string"
+ }
+ }
+ },
+ "service_writer": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "first_name": {
+ "type": "string"
+ },
+ "last_name": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "example": {
+ "data": {
+ "id": 1,
+ "title": "Estimate for Toyota Camry",
+ "customer_id": 1,
+ "vehicle_id": 1,
+ "department_id": 1,
+ "estimate_number": "EST-001",
+ "date": "2026-03-31",
+ "has_insurance": false,
+ "enable_digital_authorisation": false,
+ "insurance_type_id": 1,
+ "insurer_id": 2,
+ "service_writer_id": 1,
+ "footer": "Thank you for your business.",
+ "created_at": "2026-03-31T10:00:00.000000Z",
+ "updated_at": "2026-03-31T10:00:00.000000Z",
+ "labels": [
+ {
+ "id": 1,
+ "title": "Urgent",
+ "color_code": "#FF0000",
+ "pivot": {
+ "estimate_id": 1,
+ "label_id": 1
+ }
+ }
+ ],
+ "customer_remarks": [
+ {
+ "id": 1,
+ "estimate_id": 1,
+ "remark": "Oil change recommended.",
+ "created_at": "2026-03-31T10:00:00.000000Z",
+ "updated_at": "2026-03-31T10:00:00.000000Z"
+ }
+ ],
+ "customer": {
+ "id": 1,
+ "first_name": "John",
+ "last_name": "Doe"
+ },
+ "vehicle": {
+ "id": 1,
+ "registration_number": "ABC-1234",
+ "make": "Toyota",
+ "model": "Camry"
+ },
+ "department": {
+ "id": 1,
+ "name": "Service"
+ },
+ "insurance_type": {
+ "id": 1,
+ "title": "Comprehensive"
+ },
+ "insurer": {
+ "id": 2,
+ "first_name": "Alex",
+ "last_name": "Insurer"
+ },
+ "service_writer": {
+ "id": 1,
+ "first_name": "Sam",
+ "last_name": "Writer"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
"put": {
"tags": [
"Estimates"
@@ -42973,6 +43257,24 @@
"estimate_id": {
"type": "integer"
},
+ "kms_in": {
+ "type": "integer"
+ },
+ "has_insurance": {
+ "type": "boolean"
+ },
+ "insurer_id": {
+ "type": "integer"
+ },
+ "invoice_to_id": {
+ "type": "integer"
+ },
+ "billing_address_id": {
+ "type": "integer"
+ },
+ "delivery_address_id": {
+ "type": "integer"
+ },
"invoice_date": {
"type": "string"
},
@@ -42988,17 +43290,195 @@
"invoice_number": {
"type": "string"
},
+ "invoice_title": {
+ "type": "string"
+ },
"department_id": {
"type": "integer"
},
"notes": {
"type": "string"
},
+ "terms_and_conditions": {
+ "type": "string"
+ },
"status": {
"type": "string"
},
+ "received_payment": {
+ "type": "boolean"
+ },
+ "payment_mode_id": {
+ "type": "integer"
+ },
+ "deposit_to": {
+ "type": "string"
+ },
+ "amount": {
+ "type": "integer"
+ },
"discount": {
"type": "string"
+ },
+ "inspection_categories": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "inspection_category_id": {
+ "type": "integer"
+ },
+ "rate_type": {
+ "type": "string"
+ },
+ "labor_rate": {
+ "type": "integer"
+ },
+ "working_hours": {
+ "type": "integer"
+ },
+ "labor_hours": {
+ "type": "integer"
+ },
+ "rate": {
+ "type": "integer"
+ },
+ "chart_of_account": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "department_id": {
+ "type": "integer"
+ }
+ }
+ }
+ },
+ "parts": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "part_id": {
+ "type": "integer"
+ },
+ "quantity": {
+ "type": "integer"
+ },
+ "rate": {
+ "type": "integer"
+ },
+ "chart_of_account": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "department_id": {
+ "type": "integer"
+ }
+ }
+ }
+ },
+ "expenses": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "expense_id": {
+ "type": "integer"
+ },
+ "quantity": {
+ "type": "integer"
+ },
+ "rate": {
+ "type": "integer"
+ },
+ "chart_of_account": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "department_id": {
+ "type": "integer"
+ }
+ }
+ }
+ },
+ "services": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "service_id": {
+ "type": "integer"
+ },
+ "rate_type": {
+ "type": "string"
+ },
+ "labor_rate_id": {
+ "type": "integer"
+ },
+ "working_hours": {
+ "type": "integer"
+ },
+ "labor_hours": {
+ "type": "integer"
+ },
+ "quantity": {
+ "type": "integer"
+ },
+ "rate": {
+ "type": "integer"
+ },
+ "chart_of_account": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "department_id": {
+ "type": "integer"
+ }
+ }
+ }
+ },
+ "service_groups": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "service_group_id": {
+ "type": "integer"
+ },
+ "rate_type": {
+ "type": "string"
+ },
+ "labor_rate_id": {
+ "type": "integer"
+ },
+ "working_hours": {
+ "type": "integer"
+ },
+ "labor_hours": {
+ "type": "integer"
+ },
+ "rate": {
+ "type": "integer"
+ },
+ "chart_of_account": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "department_id": {
+ "type": "integer"
+ }
+ }
+ }
}
}
},
@@ -43007,22 +43487,94 @@
"customer_id": 1,
"vehicle_id": 1,
"estimate_id": 1,
+ "kms_in": 50000,
+ "has_insurance": false,
+ "insurer_id": 2,
+ "invoice_to_id": 1,
+ "billing_address_id": 10,
+ "delivery_address_id": 11,
"invoice_date": "2026-03-31",
"due_date": "2026-04-14",
"payment_terms_id": 1,
"invoice_sequence_id": 1,
"invoice_number": "INV-001",
+ "invoice_title": "Tax Invoice",
"department_id": 1,
- "notes": "string",
+ "notes": "Vehicle service and parts",
+ "terms_and_conditions": "Payment due in 14 days.",
"status": "draft",
- "discount": "no"
+ "received_payment": false,
+ "payment_mode_id": 1,
+ "deposit_to": "Main Account",
+ "amount": 2500,
+ "discount": "no",
+ "inspection_categories": [
+ {
+ "inspection_category_id": 1,
+ "rate_type": "flat_rate",
+ "labor_rate": 500,
+ "working_hours": 1,
+ "labor_hours": 1,
+ "rate": 500,
+ "chart_of_account": "4000",
+ "description": "General inspection",
+ "department_id": 1
+ }
+ ],
+ "parts": [
+ {
+ "part_id": 1,
+ "quantity": 2,
+ "rate": 150,
+ "chart_of_account": "4100",
+ "description": "Oil filter",
+ "department_id": 1
+ }
+ ],
+ "expenses": [
+ {
+ "expense_id": 1,
+ "quantity": 1,
+ "rate": 100,
+ "chart_of_account": "4200",
+ "description": "Shop supplies",
+ "department_id": 1
+ }
+ ],
+ "services": [
+ {
+ "service_id": 1,
+ "rate_type": "hourly",
+ "labor_rate_id": 1,
+ "working_hours": 2,
+ "labor_hours": 2,
+ "quantity": 1,
+ "rate": 800,
+ "chart_of_account": "4300",
+ "description": "Engine service",
+ "department_id": 1
+ }
+ ],
+ "service_groups": [
+ {
+ "service_group_id": 1,
+ "rate_type": "flat_rate",
+ "labor_rate_id": 1,
+ "working_hours": 1,
+ "labor_hours": 1,
+ "rate": 600,
+ "chart_of_account": "4400",
+ "description": "Major service package",
+ "department_id": 1
+ }
+ ]
}
}
}
},
"responses": {
- "200": {
- "description": "OK",
+ "201": {
+ "description": "Created",
"content": {
"application/json": {
"schema": {
@@ -43098,7 +43650,7 @@
"type": "string"
},
"received_payment": {
- "type": "integer"
+ "type": "boolean"
},
"payment_mode_id": {
"type": "integer"
@@ -43119,25 +43671,253 @@
"updated_at": {
"type": "string",
"format": "date-time"
+ },
+ "customer": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ }
+ }
+ },
+ "vehicle": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ }
+ }
+ },
+ "invoice_sequence": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "title": {
+ "type": "string"
+ }
+ }
+ },
+ "department": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "name": {
+ "type": "string"
+ }
+ }
+ },
+ "invoice_inspection_categories": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "invoice_id": {
+ "type": "integer"
+ },
+ "inspection_category_id": {
+ "type": "integer"
+ },
+ "rate_type": {
+ "type": "string"
+ },
+ "labor_rate": {
+ "type": "integer"
+ },
+ "working_hours": {
+ "type": "integer"
+ },
+ "labor_hours": {
+ "type": "integer"
+ },
+ "rate": {
+ "type": "integer"
+ },
+ "chart_of_account": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "department_id": {
+ "type": "integer"
+ }
+ }
+ }
+ },
+ "invoice_parts": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "invoice_id": {
+ "type": "integer"
+ },
+ "part_id": {
+ "type": "integer"
+ },
+ "quantity": {
+ "type": "integer"
+ },
+ "rate": {
+ "type": "integer"
+ },
+ "chart_of_account": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "department_id": {
+ "type": "integer"
+ }
+ }
+ }
+ },
+ "invoice_expenses": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "invoice_id": {
+ "type": "integer"
+ },
+ "expense_id": {
+ "type": "integer"
+ },
+ "quantity": {
+ "type": "integer"
+ },
+ "rate": {
+ "type": "integer"
+ },
+ "chart_of_account": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "department_id": {
+ "type": "integer"
+ }
+ }
+ }
+ },
+ "invoice_services": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "invoice_id": {
+ "type": "integer"
+ },
+ "service_id": {
+ "type": "integer"
+ },
+ "rate_type": {
+ "type": "string"
+ },
+ "labor_rate_id": {
+ "type": "integer"
+ },
+ "working_hours": {
+ "type": "integer"
+ },
+ "labor_hours": {
+ "type": "integer"
+ },
+ "quantity": {
+ "type": "integer"
+ },
+ "rate": {
+ "type": "integer"
+ },
+ "chart_of_account": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "department_id": {
+ "type": "integer"
+ }
+ }
+ }
+ },
+ "invoice_service_groups": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "invoice_id": {
+ "type": "integer"
+ },
+ "service_group_id": {
+ "type": "integer"
+ },
+ "rate_type": {
+ "type": "string"
+ },
+ "labor_rate_id": {
+ "type": "integer"
+ },
+ "working_hours": {
+ "type": "integer"
+ },
+ "labor_hours": {
+ "type": "integer"
+ },
+ "rate": {
+ "type": "integer"
+ },
+ "chart_of_account": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "department_id": {
+ "type": "integer"
+ }
+ }
+ }
}
}
}
}
},
"example": {
- "message": "Operation completed successfully.",
+ "message": "Invoice created successfully.",
"data": {
"id": 1,
- "subject": "Invoice for Job Card 001",
+ "subject": "Invoice for Service",
"customer_id": 1,
"vehicle_id": 1,
"estimate_id": 1,
"kms_in": 50000,
"has_insurance": false,
- "insurer_id": 1,
+ "insurer_id": 2,
"invoice_to_id": 1,
- "billing_address_id": 1,
- "delivery_address_id": 1,
+ "billing_address_id": 10,
+ "delivery_address_id": 11,
"invoice_date": "2026-03-31",
"due_date": "2026-04-14",
"payment_terms_id": 1,
@@ -43145,16 +43925,100 @@
"invoice_number": "INV-001",
"invoice_title": "Tax Invoice",
"department_id": 1,
- "notes": "string",
- "terms_and_conditions": "string",
+ "notes": "Vehicle service and parts",
+ "terms_and_conditions": "Payment due in 14 days.",
"status": "draft",
- "received_payment": 0,
+ "received_payment": false,
"payment_mode_id": 1,
- "deposit_to": "string",
- "amount": 0,
+ "deposit_to": "Main Account",
+ "amount": 2500,
"discount": "no",
"created_at": "2026-03-31T10:00:00.000000Z",
- "updated_at": "2026-03-31T10:00:00.000000Z"
+ "updated_at": "2026-03-31T10:00:00.000000Z",
+ "customer": {
+ "id": 1
+ },
+ "vehicle": {
+ "id": 1
+ },
+ "invoice_sequence": {
+ "id": 1,
+ "title": "INV"
+ },
+ "department": {
+ "id": 1,
+ "name": "Service"
+ },
+ "invoice_inspection_categories": [
+ {
+ "id": 1,
+ "invoice_id": 1,
+ "inspection_category_id": 1,
+ "rate_type": "flat_rate",
+ "labor_rate": 500,
+ "working_hours": 1,
+ "labor_hours": 1,
+ "rate": 500,
+ "chart_of_account": "4000",
+ "description": "General inspection",
+ "department_id": 1
+ }
+ ],
+ "invoice_parts": [
+ {
+ "id": 1,
+ "invoice_id": 1,
+ "part_id": 1,
+ "quantity": 2,
+ "rate": 150,
+ "chart_of_account": "4100",
+ "description": "Oil filter",
+ "department_id": 1
+ }
+ ],
+ "invoice_expenses": [
+ {
+ "id": 1,
+ "invoice_id": 1,
+ "expense_id": 1,
+ "quantity": 1,
+ "rate": 100,
+ "chart_of_account": "4200",
+ "description": "Shop supplies",
+ "department_id": 1
+ }
+ ],
+ "invoice_services": [
+ {
+ "id": 1,
+ "invoice_id": 1,
+ "service_id": 1,
+ "rate_type": "hourly",
+ "labor_rate_id": 1,
+ "working_hours": 2,
+ "labor_hours": 2,
+ "quantity": 1,
+ "rate": 800,
+ "chart_of_account": "4300",
+ "description": "Engine service",
+ "department_id": 1
+ }
+ ],
+ "invoice_service_groups": [
+ {
+ "id": 1,
+ "invoice_id": 1,
+ "service_group_id": 1,
+ "rate_type": "flat_rate",
+ "labor_rate_id": 1,
+ "working_hours": 1,
+ "labor_hours": 1,
+ "rate": 600,
+ "chart_of_account": "4400",
+ "description": "Major service package",
+ "department_id": 1
+ }
+ ]
}
}
}
diff --git a/packages/api/package.json b/packages/api/package.json
index 515590a..0d5dcf2 100644
--- a/packages/api/package.json
+++ b/packages/api/package.json
@@ -23,7 +23,8 @@
"check-types": "echo \"No typecheck configured for @garage/api\""
},
"dependencies": {
- "openapi-fetch": "^0.14.0"
+ "openapi-fetch": "^0.14.0",
+ "pino": "^10.3.1"
},
"devDependencies": {
"openapi-typescript": "^7.10.1"
@@ -33,7 +34,11 @@
"server-only": "*"
},
"peerDependenciesMeta": {
- "next": { "optional": true },
- "server-only": { "optional": true }
+ "next": {
+ "optional": true
+ },
+ "server-only": {
+ "optional": true
+ }
}
}
diff --git a/packages/api/postman/collection.json b/packages/api/postman/collection.json
index aec8e44..d2329f8 100644
--- a/packages/api/postman/collection.json
+++ b/packages/api/postman/collection.json
@@ -1,6 +1,6 @@
{
"info": {
- "_postman_id": "351b2d30-cb82-4904-9a47-0d356181a8ea",
+ "_postman_id": "38ac2be3-7ab6-4dcc-ad76-6112212395af",
"name": "Reparee Collection",
"description": "Auto-generated from OpenAPI spec. Import storage/app/openapi-default.json for the full schema.",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
@@ -135,7 +135,7 @@
},
"response": [
{
- "name": "200 OK",
+ "name": "201 Created",
"originalRequest": {
"auth": {
"type": "bearer",
@@ -169,8 +169,8 @@
]
}
},
- "status": "OK",
- "code": 200,
+ "status": "Created",
+ "code": 201,
"_postman_previewlanguage": "json",
"header": [
{
@@ -14953,6 +14953,93 @@
}
]
},
+ {
+ "name": "Display the specified estimate.",
+ "request": {
+ "auth": {
+ "type": "bearer",
+ "bearer": [
+ {
+ "key": "token",
+ "value": "{{auth_token}}",
+ "type": "string"
+ }
+ ]
+ },
+ "method": "GET",
+ "header": [
+ {
+ "key": "Accept",
+ "value": "application/json"
+ },
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "url": {
+ "raw": "{{base_url}}/api/estimates/{{id}}",
+ "host": [
+ "{{base_url}}"
+ ],
+ "path": [
+ "api",
+ "estimates",
+ "{{id}}"
+ ]
+ }
+ },
+ "response": [
+ {
+ "name": "200 OK",
+ "originalRequest": {
+ "auth": {
+ "type": "bearer",
+ "bearer": [
+ {
+ "key": "token",
+ "value": "{{auth_token}}",
+ "type": "string"
+ }
+ ]
+ },
+ "method": "GET",
+ "header": [
+ {
+ "key": "Accept",
+ "value": "application/json"
+ },
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "url": {
+ "raw": "{{base_url}}/api/estimates/{{id}}",
+ "host": [
+ "{{base_url}}"
+ ],
+ "path": [
+ "api",
+ "estimates",
+ "{{id}}"
+ ]
+ }
+ },
+ "status": "OK",
+ "code": 200,
+ "_postman_previewlanguage": "json",
+ "header": [
+ {
+ "key": "Content-Type",
+ "value": "application/json"
+ }
+ ],
+ "cookie": [],
+ "body": "{\n \"data\": {\n \"id\": 1,\n \"title\": \"Estimate for Toyota Camry\",\n \"customer_id\": 1,\n \"vehicle_id\": 1,\n \"department_id\": 1,\n \"estimate_number\": \"EST-001\",\n \"date\": \"2026-03-31\",\n \"has_insurance\": false,\n \"enable_digital_authorisation\": false,\n \"insurance_type_id\": 1,\n \"insurer_id\": 2,\n \"service_writer_id\": 1,\n \"footer\": \"Thank you for your business.\",\n \"created_at\": \"2026-03-31T10:00:00.000000Z\",\n \"updated_at\": \"2026-03-31T10:00:00.000000Z\",\n \"labels\": [\n {\n \"id\": 1,\n \"title\": \"Urgent\",\n \"color_code\": \"#FF0000\",\n \"pivot\": {\n \"estimate_id\": 1,\n \"label_id\": 1\n }\n }\n ],\n \"customer_remarks\": [\n {\n \"id\": 1,\n \"estimate_id\": 1,\n \"remark\": \"Oil change recommended.\",\n \"created_at\": \"2026-03-31T10:00:00.000000Z\",\n \"updated_at\": \"2026-03-31T10:00:00.000000Z\"\n }\n ],\n \"customer\": {\n \"id\": 1,\n \"first_name\": \"John\",\n \"last_name\": \"Doe\"\n },\n \"vehicle\": {\n \"id\": 1,\n \"registration_number\": \"ABC-1234\",\n \"make\": \"Toyota\",\n \"model\": \"Camry\"\n },\n \"department\": {\n \"id\": 1,\n \"name\": \"Service\"\n },\n \"insurance_type\": {\n \"id\": 1,\n \"title\": \"Comprehensive\"\n },\n \"insurer\": {\n \"id\": 2,\n \"first_name\": \"Alex\",\n \"last_name\": \"Insurer\"\n },\n \"service_writer\": {\n \"id\": 1,\n \"first_name\": \"Sam\",\n \"last_name\": \"Writer\"\n }\n }\n}"
+ }
+ ]
+ },
{
"name": "Store a newly created estimate.",
"request": {
@@ -33569,7 +33656,7 @@
],
"body": {
"mode": "raw",
- "raw": "{\n \"subject\": \"Invoice for Service\",\n \"customer_id\": 1,\n \"vehicle_id\": 1,\n \"estimate_id\": 1,\n \"invoice_date\": \"2026-03-31\",\n \"due_date\": \"2026-04-14\",\n \"payment_terms_id\": 1,\n \"invoice_sequence_id\": 1,\n \"invoice_number\": \"INV-001\",\n \"department_id\": 1,\n \"notes\": \"string\",\n \"status\": \"draft\",\n \"discount\": \"no\"\n}",
+ "raw": "{\n \"subject\": \"Invoice for Service\",\n \"customer_id\": 1,\n \"vehicle_id\": 1,\n \"estimate_id\": 1,\n \"kms_in\": 50000,\n \"has_insurance\": false,\n \"insurer_id\": 2,\n \"invoice_to_id\": 1,\n \"billing_address_id\": 10,\n \"delivery_address_id\": 11,\n \"invoice_date\": \"2026-03-31\",\n \"due_date\": \"2026-04-14\",\n \"payment_terms_id\": 1,\n \"invoice_sequence_id\": 1,\n \"invoice_number\": \"INV-001\",\n \"invoice_title\": \"Tax Invoice\",\n \"department_id\": 1,\n \"notes\": \"Vehicle service and parts\",\n \"terms_and_conditions\": \"Payment due in 14 days.\",\n \"status\": \"draft\",\n \"received_payment\": false,\n \"payment_mode_id\": 1,\n \"deposit_to\": \"Main Account\",\n \"amount\": 2500,\n \"discount\": \"no\",\n \"inspection_categories\": [\n {\n \"inspection_category_id\": 1,\n \"rate_type\": \"flat_rate\",\n \"labor_rate\": 500,\n \"working_hours\": 1,\n \"labor_hours\": 1,\n \"rate\": 500,\n \"chart_of_account\": \"4000\",\n \"description\": \"General inspection\",\n \"department_id\": 1\n }\n ],\n \"parts\": [\n {\n \"part_id\": 1,\n \"quantity\": 2,\n \"rate\": 150,\n \"chart_of_account\": \"4100\",\n \"description\": \"Oil filter\",\n \"department_id\": 1\n }\n ],\n \"expenses\": [\n {\n \"expense_id\": 1,\n \"quantity\": 1,\n \"rate\": 100,\n \"chart_of_account\": \"4200\",\n \"description\": \"Shop supplies\",\n \"department_id\": 1\n }\n ],\n \"services\": [\n {\n \"service_id\": 1,\n \"rate_type\": \"hourly\",\n \"labor_rate_id\": 1,\n \"working_hours\": 2,\n \"labor_hours\": 2,\n \"quantity\": 1,\n \"rate\": 800,\n \"chart_of_account\": \"4300\",\n \"description\": \"Engine service\",\n \"department_id\": 1\n }\n ],\n \"service_groups\": [\n {\n \"service_group_id\": 1,\n \"rate_type\": \"flat_rate\",\n \"labor_rate_id\": 1,\n \"working_hours\": 1,\n \"labor_hours\": 1,\n \"rate\": 600,\n \"chart_of_account\": \"4400\",\n \"description\": \"Major service package\",\n \"department_id\": 1\n }\n ]\n}",
"options": {
"raw": {
"language": "json"
@@ -33589,7 +33676,7 @@
},
"response": [
{
- "name": "200 OK",
+ "name": "201 Created",
"originalRequest": {
"auth": {
"type": "bearer",
@@ -33614,7 +33701,7 @@
],
"body": {
"mode": "raw",
- "raw": "{\n \"subject\": \"Invoice for Service\",\n \"customer_id\": 1,\n \"vehicle_id\": 1,\n \"estimate_id\": 1,\n \"invoice_date\": \"2026-03-31\",\n \"due_date\": \"2026-04-14\",\n \"payment_terms_id\": 1,\n \"invoice_sequence_id\": 1,\n \"invoice_number\": \"INV-001\",\n \"department_id\": 1,\n \"notes\": \"string\",\n \"status\": \"draft\",\n \"discount\": \"no\"\n}",
+ "raw": "{\n \"subject\": \"Invoice for Service\",\n \"customer_id\": 1,\n \"vehicle_id\": 1,\n \"estimate_id\": 1,\n \"kms_in\": 50000,\n \"has_insurance\": false,\n \"insurer_id\": 2,\n \"invoice_to_id\": 1,\n \"billing_address_id\": 10,\n \"delivery_address_id\": 11,\n \"invoice_date\": \"2026-03-31\",\n \"due_date\": \"2026-04-14\",\n \"payment_terms_id\": 1,\n \"invoice_sequence_id\": 1,\n \"invoice_number\": \"INV-001\",\n \"invoice_title\": \"Tax Invoice\",\n \"department_id\": 1,\n \"notes\": \"Vehicle service and parts\",\n \"terms_and_conditions\": \"Payment due in 14 days.\",\n \"status\": \"draft\",\n \"received_payment\": false,\n \"payment_mode_id\": 1,\n \"deposit_to\": \"Main Account\",\n \"amount\": 2500,\n \"discount\": \"no\",\n \"inspection_categories\": [\n {\n \"inspection_category_id\": 1,\n \"rate_type\": \"flat_rate\",\n \"labor_rate\": 500,\n \"working_hours\": 1,\n \"labor_hours\": 1,\n \"rate\": 500,\n \"chart_of_account\": \"4000\",\n \"description\": \"General inspection\",\n \"department_id\": 1\n }\n ],\n \"parts\": [\n {\n \"part_id\": 1,\n \"quantity\": 2,\n \"rate\": 150,\n \"chart_of_account\": \"4100\",\n \"description\": \"Oil filter\",\n \"department_id\": 1\n }\n ],\n \"expenses\": [\n {\n \"expense_id\": 1,\n \"quantity\": 1,\n \"rate\": 100,\n \"chart_of_account\": \"4200\",\n \"description\": \"Shop supplies\",\n \"department_id\": 1\n }\n ],\n \"services\": [\n {\n \"service_id\": 1,\n \"rate_type\": \"hourly\",\n \"labor_rate_id\": 1,\n \"working_hours\": 2,\n \"labor_hours\": 2,\n \"quantity\": 1,\n \"rate\": 800,\n \"chart_of_account\": \"4300\",\n \"description\": \"Engine service\",\n \"department_id\": 1\n }\n ],\n \"service_groups\": [\n {\n \"service_group_id\": 1,\n \"rate_type\": \"flat_rate\",\n \"labor_rate_id\": 1,\n \"working_hours\": 1,\n \"labor_hours\": 1,\n \"rate\": 600,\n \"chart_of_account\": \"4400\",\n \"description\": \"Major service package\",\n \"department_id\": 1\n }\n ]\n}",
"options": {
"raw": {
"language": "json"
@@ -33632,8 +33719,8 @@
]
}
},
- "status": "OK",
- "code": 200,
+ "status": "Created",
+ "code": 201,
"_postman_previewlanguage": "json",
"header": [
{
@@ -33642,7 +33729,7 @@
}
],
"cookie": [],
- "body": "{\n \"message\": \"Operation completed successfully.\",\n \"data\": {\n \"id\": 1,\n \"subject\": \"Invoice for Job Card 001\",\n \"customer_id\": 1,\n \"vehicle_id\": 1,\n \"estimate_id\": 1,\n \"kms_in\": 50000,\n \"has_insurance\": false,\n \"insurer_id\": 1,\n \"invoice_to_id\": 1,\n \"billing_address_id\": 1,\n \"delivery_address_id\": 1,\n \"invoice_date\": \"2026-03-31\",\n \"due_date\": \"2026-04-14\",\n \"payment_terms_id\": 1,\n \"invoice_sequence_id\": 1,\n \"invoice_number\": \"INV-001\",\n \"invoice_title\": \"Tax Invoice\",\n \"department_id\": 1,\n \"notes\": \"string\",\n \"terms_and_conditions\": \"string\",\n \"status\": \"draft\",\n \"received_payment\": 0,\n \"payment_mode_id\": 1,\n \"deposit_to\": \"string\",\n \"amount\": 0,\n \"discount\": \"no\",\n \"created_at\": \"2026-03-31T10:00:00.000000Z\",\n \"updated_at\": \"2026-03-31T10:00:00.000000Z\"\n }\n}"
+ "body": "{\n \"message\": \"Invoice created successfully.\",\n \"data\": {\n \"id\": 1,\n \"subject\": \"Invoice for Service\",\n \"customer_id\": 1,\n \"vehicle_id\": 1,\n \"estimate_id\": 1,\n \"kms_in\": 50000,\n \"has_insurance\": false,\n \"insurer_id\": 2,\n \"invoice_to_id\": 1,\n \"billing_address_id\": 10,\n \"delivery_address_id\": 11,\n \"invoice_date\": \"2026-03-31\",\n \"due_date\": \"2026-04-14\",\n \"payment_terms_id\": 1,\n \"invoice_sequence_id\": 1,\n \"invoice_number\": \"INV-001\",\n \"invoice_title\": \"Tax Invoice\",\n \"department_id\": 1,\n \"notes\": \"Vehicle service and parts\",\n \"terms_and_conditions\": \"Payment due in 14 days.\",\n \"status\": \"draft\",\n \"received_payment\": false,\n \"payment_mode_id\": 1,\n \"deposit_to\": \"Main Account\",\n \"amount\": 2500,\n \"discount\": \"no\",\n \"created_at\": \"2026-03-31T10:00:00.000000Z\",\n \"updated_at\": \"2026-03-31T10:00:00.000000Z\",\n \"customer\": {\n \"id\": 1\n },\n \"vehicle\": {\n \"id\": 1\n },\n \"invoice_sequence\": {\n \"id\": 1,\n \"title\": \"INV\"\n },\n \"department\": {\n \"id\": 1,\n \"name\": \"Service\"\n },\n \"invoice_inspection_categories\": [\n {\n \"id\": 1,\n \"invoice_id\": 1,\n \"inspection_category_id\": 1,\n \"rate_type\": \"flat_rate\",\n \"labor_rate\": 500,\n \"working_hours\": 1,\n \"labor_hours\": 1,\n \"rate\": 500,\n \"chart_of_account\": \"4000\",\n \"description\": \"General inspection\",\n \"department_id\": 1\n }\n ],\n \"invoice_parts\": [\n {\n \"id\": 1,\n \"invoice_id\": 1,\n \"part_id\": 1,\n \"quantity\": 2,\n \"rate\": 150,\n \"chart_of_account\": \"4100\",\n \"description\": \"Oil filter\",\n \"department_id\": 1\n }\n ],\n \"invoice_expenses\": [\n {\n \"id\": 1,\n \"invoice_id\": 1,\n \"expense_id\": 1,\n \"quantity\": 1,\n \"rate\": 100,\n \"chart_of_account\": \"4200\",\n \"description\": \"Shop supplies\",\n \"department_id\": 1\n }\n ],\n \"invoice_services\": [\n {\n \"id\": 1,\n \"invoice_id\": 1,\n \"service_id\": 1,\n \"rate_type\": \"hourly\",\n \"labor_rate_id\": 1,\n \"working_hours\": 2,\n \"labor_hours\": 2,\n \"quantity\": 1,\n \"rate\": 800,\n \"chart_of_account\": \"4300\",\n \"description\": \"Engine service\",\n \"department_id\": 1\n }\n ],\n \"invoice_service_groups\": [\n {\n \"id\": 1,\n \"invoice_id\": 1,\n \"service_group_id\": 1,\n \"rate_type\": \"flat_rate\",\n \"labor_rate_id\": 1,\n \"working_hours\": 1,\n \"labor_hours\": 1,\n \"rate\": 600,\n \"chart_of_account\": \"4400\",\n \"description\": \"Major service package\",\n \"department_id\": 1\n }\n ]\n }\n}"
}
]
},
diff --git a/packages/api/src/clients/estimates.ts b/packages/api/src/clients/estimates.ts
index d8f7b09..02c7750 100644
--- a/packages/api/src/clients/estimates.ts
+++ b/packages/api/src/clients/estimates.ts
@@ -1,6 +1,6 @@
import { CrudClient } from "../infra/crud-client"
import type { ApiClientOptions } from "../infra/client"
-import type { ApiPath } from "../infra/types"
+import type { ApiPath, ApiResponse } from "../infra/types"
export const ESTIMATE_ROUTES = {
INDEX: "/api/estimates",
@@ -25,8 +25,8 @@ export class EstimatesClient extends CrudClient<
// Note: GET /api/estimates/{id} is not in the OpenAPI schema.
// This method uses a type cast and relies on the backend supporting the route.
async getById(id: string) {
- const data = await this.get(ESTIMATE_ROUTES.INDEX, { query: { id } } as never)
- return {...data, data: (data as any)?.data?.[0] ?? null }
+ const data = await this.get(ESTIMATE_ROUTES.BY_ID, { params: { id } })
+ return data;
}
// ── Estimate Services ──
@@ -48,7 +48,7 @@ export class EstimatesClient extends CrudClient<
// ── Estimate Parts ──
async listParts(estimateId: string) {
- return this.get(ESTIMATE_ROUTES.PARTS, { params: { id: estimateId } } as never)
+ return this.get(ESTIMATE_ROUTES.PARTS, { params: { id: estimateId } }) as Promise>
}
async addPart(estimateId: string, payload: { part_id?: number; quantity?: number; rate?: string; description?: string }) {
diff --git a/packages/api/src/infra/client.ts b/packages/api/src/infra/client.ts
index c6fba47..4f8a948 100644
--- a/packages/api/src/infra/client.ts
+++ b/packages/api/src/infra/client.ts
@@ -8,6 +8,7 @@ import type {
} from "./types"
import createClient from "openapi-fetch"
import type { paths } from "../../types/index"
+import { logger } from "./logger"
type HttpMethod = "get" | "post" | "put" | "delete" | "patch"
@@ -73,6 +74,8 @@ export class ApiClient {
const opts = options as never
const body = (options as Record).body as never
+
+
switch (method) {
case "get":
return this.get(ep, opts) as Promise>
@@ -96,12 +99,15 @@ export class ApiClient {
options: ApiRequestOptions = {} as ApiRequestOptions,
): Promise> {
const requestOptions = this.toFetchOptions(options)
+ const startTime = Date.now()
+ logger.debug({ method: "GET", endpoint }, "API request")
try {
const { data, error, response } = await this.client.GET(endpoint, requestOptions as never)
- return this.resolveResult(endpoint, "get", data, error, response)
+ return this.resolveResult(endpoint, "get", data, error, response, startTime)
} catch (err) {
if (err instanceof ApiError) throw err
+ logger.error({ method: "GET", endpoint, duration: Date.now() - startTime }, "Network error")
throw this.createNetworkError(endpoint, "get")
}
}
@@ -112,12 +118,15 @@ export class ApiClient {
options: Omit, "body"> = {} as Omit, "body">,
): Promise> {
const requestOptions = this.toFetchOptions({ ...options, body })
+ const startTime = Date.now()
+ logger.debug({ method: "POST", endpoint }, "API request")
try {
const { data, error, response } = await this.client.POST(endpoint, requestOptions as never)
- return this.resolveResult(endpoint, "post", data, error, response)
+ return this.resolveResult(endpoint, "post", data, error, response, startTime)
} catch (err) {
if (err instanceof ApiError) throw err
+ logger.error({ method: "POST", endpoint, duration: Date.now() - startTime }, "Network error")
throw this.createNetworkError(endpoint, "post")
}
}
@@ -128,12 +137,15 @@ export class ApiClient {
options: Omit, "body"> = {} as Omit, "body">,
): Promise> {
const requestOptions = this.toFetchOptions({ ...options, body })
+ const startTime = Date.now()
+ logger.debug({ method: "PUT", endpoint }, "API request")
try {
const { data, error, response } = await this.client.PUT(endpoint, requestOptions as never)
- return this.resolveResult(endpoint, "put", data, error, response)
+ return this.resolveResult(endpoint, "put", data, error, response, startTime)
} catch (err) {
if (err instanceof ApiError) throw err
+ logger.error({ method: "PUT", endpoint, duration: Date.now() - startTime }, "Network error")
throw this.createNetworkError(endpoint, "put")
}
}
@@ -143,12 +155,15 @@ export class ApiClient {
options: ApiRequestOptions = {} as ApiRequestOptions,
): Promise> {
const requestOptions = this.toFetchOptions(options)
+ const startTime = Date.now()
+ logger.debug({ method: "DELETE", endpoint }, "API request")
try {
const { data, error, response } = await this.client.DELETE(endpoint, requestOptions as never)
- return this.resolveResult(endpoint, "delete", data, error, response)
+ return this.resolveResult(endpoint, "delete", data, error, response, startTime)
} catch (err) {
if (err instanceof ApiError) throw err
+ logger.error({ method: "DELETE", endpoint, duration: Date.now() - startTime }, "Network error")
throw this.createNetworkError(endpoint, "delete")
}
}
@@ -159,12 +174,15 @@ export class ApiClient {
options: Omit, "body"> = {} as Omit, "body">,
): Promise> {
const requestOptions = this.toFetchOptions({ ...options, body })
+ const startTime = Date.now()
+ logger.debug({ method: "PATCH", endpoint }, "API request")
try {
const { data, error, response } = await this.client.PATCH(endpoint, requestOptions as never)
- return this.resolveResult(endpoint, "patch", data, error, response)
+ return this.resolveResult(endpoint, "patch", data, error, response, startTime)
} catch (err) {
if (err instanceof ApiError) throw err
+ logger.error({ method: "PATCH", endpoint, duration: Date.now() - startTime }, "Network error")
throw this.createNetworkError(endpoint, "patch")
}
}
@@ -179,14 +197,21 @@ export class ApiClient {
headers.set("Accept", "application/json")
// Content-Type is intentionally omitted — fetch sets multipart/form-data + boundary automatically
+ const startTime = Date.now()
+ logger.debug({ method: "POST", endpoint }, "API request (form-data)")
+
const response = await fetch(url, { method: "POST", headers, body: formData })
const text = await response.text()
const data = text ? JSON.parse(text) : null
+ const duration = Date.now() - startTime
if (!response.ok) {
+ const level = response.status >= 500 ? "error" : "warn"
+ logger[level]({ method: "POST", endpoint, status: response.status, duration }, data?.message ?? "API request failed")
throw new ApiError(response.status, response.statusText, endpoint, "post", data)
}
+ logger.debug({ method: "POST", endpoint, status: response.status, duration }, "API request completed (form-data)")
return data
}
@@ -201,14 +226,21 @@ export class ApiClient {
init.body = JSON.stringify(options.body)
}
+ const startTime = Date.now()
+ logger.debug({ method, endpoint }, "API request (blob)")
+
const response = await fetch(url, init)
+ const duration = Date.now() - startTime
if (!response.ok) {
const text = await response.text()
const data = text ? JSON.parse(text) : null
+ const level = response.status >= 500 ? "error" : "warn"
+ logger[level]({ method, endpoint, status: response.status, duration }, data?.message ?? "API request failed")
throw new ApiError(response.status, response.statusText, endpoint, method.toLowerCase(), data)
}
+ logger.debug({ method, endpoint, status: response.status, duration }, "API request completed (blob)")
return response.blob()
}
@@ -250,17 +282,28 @@ export class ApiClient {
data: unknown,
error: unknown,
response: Response,
+ startTime: number,
): ApiResponse {
+ const duration = Date.now() - startTime
+
if (error !== undefined) {
+ const payload = this.normalizeErrorPayload(error)
+ if (response.status >= 500) {
+ logger.error({ method: method.toUpperCase(), endpoint, status: response.status, duration }, payload?.message ?? "API request failed")
+ } else {
+ logger.warn({ method: method.toUpperCase(), endpoint, status: response.status, duration }, payload?.message ?? "API request failed")
+ }
throw new ApiError(
response.status,
response.statusText,
endpoint,
method,
- this.normalizeErrorPayload(error),
+ payload,
)
}
+ logger.debug({ method: method.toUpperCase(), endpoint, status: response.status, duration, data }, "API request completed")
+
return data as ApiResponse
}
diff --git a/packages/api/src/infra/logger.ts b/packages/api/src/infra/logger.ts
new file mode 100644
index 0000000..6e168c6
--- /dev/null
+++ b/packages/api/src/infra/logger.ts
@@ -0,0 +1,32 @@
+import pino from "pino"
+
+/**
+ * Isomorphic logger for the API client.
+ *
+ * - In Node.js (server / Next.js RSC / server actions): pino writes newline-delimited
+ * JSON to stdout — structured and machine-parseable.
+ * - In the browser (Next.js client components): pino switches to the browser
+ * transport automatically and delegates to the appropriate `console.*` method
+ * (console.debug, console.warn, console.error, …).
+ *
+ * Log level defaults:
+ * development → debug (all messages)
+ * production → warn (warnings + errors only)
+ *
+ * Override with the NEXT_PUBLIC_LOG_LEVEL env var (e.g. "info").
+ */
+const level =
+ (process.env.NEXT_PUBLIC_LOG_LEVEL as pino.Level | undefined) ??
+ (process.env.NODE_ENV === "production" ? "warn" : "debug")
+
+export const logger = pino({
+ name: "garage-api",
+ level,
+ browser: {
+ /**
+ * Pass a plain object to `console.*` so DevTools display
+ * log fields (method, endpoint, status, duration) as expandable props.
+ */
+ asObject: true,
+ },
+})
diff --git a/packages/api/types/index.ts b/packages/api/types/index.ts
index 059fc63..7bb6d0f 100644
--- a/packages/api/types/index.ts
+++ b/packages/api/types/index.ts
@@ -95,8 +95,8 @@ export interface paths {
};
requestBody?: never;
responses: {
- /** @description OK */
- 200: {
+ /** @description Created */
+ 201: {
headers: {
[name: string]: unknown;
};
@@ -14538,7 +14538,166 @@ export interface paths {
path?: never;
cookie?: never;
};
- get?: never;
+ /** Display the specified estimate. */
+ get: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ /**
+ * @example {
+ * "data": {
+ * "id": 1,
+ * "title": "Estimate for Toyota Camry",
+ * "customer_id": 1,
+ * "vehicle_id": 1,
+ * "department_id": 1,
+ * "estimate_number": "EST-001",
+ * "date": "2026-03-31",
+ * "has_insurance": false,
+ * "enable_digital_authorisation": false,
+ * "insurance_type_id": 1,
+ * "insurer_id": 2,
+ * "service_writer_id": 1,
+ * "footer": "Thank you for your business.",
+ * "created_at": "2026-03-31T10:00:00.000000Z",
+ * "updated_at": "2026-03-31T10:00:00.000000Z",
+ * "labels": [
+ * {
+ * "id": 1,
+ * "title": "Urgent",
+ * "color_code": "#FF0000",
+ * "pivot": {
+ * "estimate_id": 1,
+ * "label_id": 1
+ * }
+ * }
+ * ],
+ * "customer_remarks": [
+ * {
+ * "id": 1,
+ * "estimate_id": 1,
+ * "remark": "Oil change recommended.",
+ * "created_at": "2026-03-31T10:00:00.000000Z",
+ * "updated_at": "2026-03-31T10:00:00.000000Z"
+ * }
+ * ],
+ * "customer": {
+ * "id": 1,
+ * "first_name": "John",
+ * "last_name": "Doe"
+ * },
+ * "vehicle": {
+ * "id": 1,
+ * "registration_number": "ABC-1234",
+ * "make": "Toyota",
+ * "model": "Camry"
+ * },
+ * "department": {
+ * "id": 1,
+ * "name": "Service"
+ * },
+ * "insurance_type": {
+ * "id": 1,
+ * "title": "Comprehensive"
+ * },
+ * "insurer": {
+ * "id": 2,
+ * "first_name": "Alex",
+ * "last_name": "Insurer"
+ * },
+ * "service_writer": {
+ * "id": 1,
+ * "first_name": "Sam",
+ * "last_name": "Writer"
+ * }
+ * }
+ * }
+ */
+ "application/json": {
+ data?: {
+ id?: number;
+ title?: string;
+ customer_id?: number;
+ vehicle_id?: number;
+ department_id?: number;
+ estimate_number?: string;
+ date?: string;
+ has_insurance?: boolean;
+ enable_digital_authorisation?: boolean;
+ insurance_type_id?: number;
+ insurer_id?: number;
+ service_writer_id?: number;
+ footer?: string;
+ /** Format: date-time */
+ created_at?: string;
+ /** Format: date-time */
+ updated_at?: string;
+ labels?: {
+ id?: number;
+ title?: string;
+ color_code?: string;
+ pivot?: {
+ estimate_id?: number;
+ label_id?: number;
+ };
+ }[];
+ customer_remarks?: {
+ id?: number;
+ estimate_id?: number;
+ remark?: string;
+ /** Format: date-time */
+ created_at?: string;
+ /** Format: date-time */
+ updated_at?: string;
+ }[];
+ customer?: {
+ id?: number;
+ first_name?: string;
+ last_name?: string;
+ };
+ vehicle?: {
+ id?: number;
+ registration_number?: string;
+ make?: string;
+ model?: string;
+ };
+ department?: {
+ id?: number;
+ name?: string;
+ };
+ insurance_type?: {
+ id?: number;
+ title?: string;
+ };
+ insurer?: {
+ id?: number;
+ first_name?: string;
+ last_name?: string;
+ };
+ service_writer?: {
+ id?: number;
+ first_name?: string;
+ last_name?: string;
+ };
+ };
+ };
+ };
+ };
+ };
+ };
/** Update the specified estimate. */
put: {
parameters: {
@@ -29708,15 +29867,87 @@ export interface paths {
* "customer_id": 1,
* "vehicle_id": 1,
* "estimate_id": 1,
+ * "kms_in": 50000,
+ * "has_insurance": false,
+ * "insurer_id": 2,
+ * "invoice_to_id": 1,
+ * "billing_address_id": 10,
+ * "delivery_address_id": 11,
* "invoice_date": "2026-03-31",
* "due_date": "2026-04-14",
* "payment_terms_id": 1,
* "invoice_sequence_id": 1,
* "invoice_number": "INV-001",
+ * "invoice_title": "Tax Invoice",
* "department_id": 1,
- * "notes": "string",
+ * "notes": "Vehicle service and parts",
+ * "terms_and_conditions": "Payment due in 14 days.",
* "status": "draft",
- * "discount": "no"
+ * "received_payment": false,
+ * "payment_mode_id": 1,
+ * "deposit_to": "Main Account",
+ * "amount": 2500,
+ * "discount": "no",
+ * "inspection_categories": [
+ * {
+ * "inspection_category_id": 1,
+ * "rate_type": "flat_rate",
+ * "labor_rate": 500,
+ * "working_hours": 1,
+ * "labor_hours": 1,
+ * "rate": 500,
+ * "chart_of_account": "4000",
+ * "description": "General inspection",
+ * "department_id": 1
+ * }
+ * ],
+ * "parts": [
+ * {
+ * "part_id": 1,
+ * "quantity": 2,
+ * "rate": 150,
+ * "chart_of_account": "4100",
+ * "description": "Oil filter",
+ * "department_id": 1
+ * }
+ * ],
+ * "expenses": [
+ * {
+ * "expense_id": 1,
+ * "quantity": 1,
+ * "rate": 100,
+ * "chart_of_account": "4200",
+ * "description": "Shop supplies",
+ * "department_id": 1
+ * }
+ * ],
+ * "services": [
+ * {
+ * "service_id": 1,
+ * "rate_type": "hourly",
+ * "labor_rate_id": 1,
+ * "working_hours": 2,
+ * "labor_hours": 2,
+ * "quantity": 1,
+ * "rate": 800,
+ * "chart_of_account": "4300",
+ * "description": "Engine service",
+ * "department_id": 1
+ * }
+ * ],
+ * "service_groups": [
+ * {
+ * "service_group_id": 1,
+ * "rate_type": "flat_rate",
+ * "labor_rate_id": 1,
+ * "working_hours": 1,
+ * "labor_hours": 1,
+ * "rate": 600,
+ * "chart_of_account": "4400",
+ * "description": "Major service package",
+ * "department_id": 1
+ * }
+ * ]
* }
*/
"application/json": {
@@ -29724,40 +29955,102 @@ export interface paths {
customer_id?: number;
vehicle_id?: number;
estimate_id?: number;
+ kms_in?: number;
+ has_insurance?: boolean;
+ insurer_id?: number;
+ invoice_to_id?: number;
+ billing_address_id?: number;
+ delivery_address_id?: number;
invoice_date?: string;
due_date?: string;
payment_terms_id?: number;
invoice_sequence_id?: number;
invoice_number?: string;
+ invoice_title?: string;
department_id?: number;
notes?: string;
+ terms_and_conditions?: string;
status?: string;
+ received_payment?: boolean;
+ payment_mode_id?: number;
+ deposit_to?: string;
+ amount?: number;
discount?: string;
+ inspection_categories?: {
+ inspection_category_id?: number;
+ rate_type?: string;
+ labor_rate?: number;
+ working_hours?: number;
+ labor_hours?: number;
+ rate?: number;
+ chart_of_account?: string;
+ description?: string;
+ department_id?: number;
+ }[];
+ parts?: {
+ part_id?: number;
+ quantity?: number;
+ rate?: number;
+ chart_of_account?: string;
+ description?: string;
+ department_id?: number;
+ }[];
+ expenses?: {
+ expense_id?: number;
+ quantity?: number;
+ rate?: number;
+ chart_of_account?: string;
+ description?: string;
+ department_id?: number;
+ }[];
+ services?: {
+ service_id?: number;
+ rate_type?: string;
+ labor_rate_id?: number;
+ working_hours?: number;
+ labor_hours?: number;
+ quantity?: number;
+ rate?: number;
+ chart_of_account?: string;
+ description?: string;
+ department_id?: number;
+ }[];
+ service_groups?: {
+ service_group_id?: number;
+ rate_type?: string;
+ labor_rate_id?: number;
+ working_hours?: number;
+ labor_hours?: number;
+ rate?: number;
+ chart_of_account?: string;
+ description?: string;
+ department_id?: number;
+ }[];
};
};
};
responses: {
- /** @description OK */
- 200: {
+ /** @description Created */
+ 201: {
headers: {
[name: string]: unknown;
};
content: {
/**
* @example {
- * "message": "Operation completed successfully.",
+ * "message": "Invoice created successfully.",
* "data": {
* "id": 1,
- * "subject": "Invoice for Job Card 001",
+ * "subject": "Invoice for Service",
* "customer_id": 1,
* "vehicle_id": 1,
* "estimate_id": 1,
* "kms_in": 50000,
* "has_insurance": false,
- * "insurer_id": 1,
+ * "insurer_id": 2,
* "invoice_to_id": 1,
- * "billing_address_id": 1,
- * "delivery_address_id": 1,
+ * "billing_address_id": 10,
+ * "delivery_address_id": 11,
* "invoice_date": "2026-03-31",
* "due_date": "2026-04-14",
* "payment_terms_id": 1,
@@ -29765,16 +30058,100 @@ export interface paths {
* "invoice_number": "INV-001",
* "invoice_title": "Tax Invoice",
* "department_id": 1,
- * "notes": "string",
- * "terms_and_conditions": "string",
+ * "notes": "Vehicle service and parts",
+ * "terms_and_conditions": "Payment due in 14 days.",
* "status": "draft",
- * "received_payment": 0,
+ * "received_payment": false,
* "payment_mode_id": 1,
- * "deposit_to": "string",
- * "amount": 0,
+ * "deposit_to": "Main Account",
+ * "amount": 2500,
* "discount": "no",
* "created_at": "2026-03-31T10:00:00.000000Z",
- * "updated_at": "2026-03-31T10:00:00.000000Z"
+ * "updated_at": "2026-03-31T10:00:00.000000Z",
+ * "customer": {
+ * "id": 1
+ * },
+ * "vehicle": {
+ * "id": 1
+ * },
+ * "invoice_sequence": {
+ * "id": 1,
+ * "title": "INV"
+ * },
+ * "department": {
+ * "id": 1,
+ * "name": "Service"
+ * },
+ * "invoice_inspection_categories": [
+ * {
+ * "id": 1,
+ * "invoice_id": 1,
+ * "inspection_category_id": 1,
+ * "rate_type": "flat_rate",
+ * "labor_rate": 500,
+ * "working_hours": 1,
+ * "labor_hours": 1,
+ * "rate": 500,
+ * "chart_of_account": "4000",
+ * "description": "General inspection",
+ * "department_id": 1
+ * }
+ * ],
+ * "invoice_parts": [
+ * {
+ * "id": 1,
+ * "invoice_id": 1,
+ * "part_id": 1,
+ * "quantity": 2,
+ * "rate": 150,
+ * "chart_of_account": "4100",
+ * "description": "Oil filter",
+ * "department_id": 1
+ * }
+ * ],
+ * "invoice_expenses": [
+ * {
+ * "id": 1,
+ * "invoice_id": 1,
+ * "expense_id": 1,
+ * "quantity": 1,
+ * "rate": 100,
+ * "chart_of_account": "4200",
+ * "description": "Shop supplies",
+ * "department_id": 1
+ * }
+ * ],
+ * "invoice_services": [
+ * {
+ * "id": 1,
+ * "invoice_id": 1,
+ * "service_id": 1,
+ * "rate_type": "hourly",
+ * "labor_rate_id": 1,
+ * "working_hours": 2,
+ * "labor_hours": 2,
+ * "quantity": 1,
+ * "rate": 800,
+ * "chart_of_account": "4300",
+ * "description": "Engine service",
+ * "department_id": 1
+ * }
+ * ],
+ * "invoice_service_groups": [
+ * {
+ * "id": 1,
+ * "invoice_id": 1,
+ * "service_group_id": 1,
+ * "rate_type": "flat_rate",
+ * "labor_rate_id": 1,
+ * "working_hours": 1,
+ * "labor_hours": 1,
+ * "rate": 600,
+ * "chart_of_account": "4400",
+ * "description": "Major service package",
+ * "department_id": 1
+ * }
+ * ]
* }
* }
*/
@@ -29802,7 +30179,7 @@ export interface paths {
notes?: string;
terms_and_conditions?: string;
status?: string;
- received_payment?: number;
+ received_payment?: boolean;
payment_mode_id?: number;
deposit_to?: string;
amount?: number;
@@ -29811,6 +30188,80 @@ export interface paths {
created_at?: string;
/** Format: date-time */
updated_at?: string;
+ customer?: {
+ id?: number;
+ };
+ vehicle?: {
+ id?: number;
+ };
+ invoice_sequence?: {
+ id?: number;
+ title?: string;
+ };
+ department?: {
+ id?: number;
+ name?: string;
+ };
+ invoice_inspection_categories?: {
+ id?: number;
+ invoice_id?: number;
+ inspection_category_id?: number;
+ rate_type?: string;
+ labor_rate?: number;
+ working_hours?: number;
+ labor_hours?: number;
+ rate?: number;
+ chart_of_account?: string;
+ description?: string;
+ department_id?: number;
+ }[];
+ invoice_parts?: {
+ id?: number;
+ invoice_id?: number;
+ part_id?: number;
+ quantity?: number;
+ rate?: number;
+ chart_of_account?: string;
+ description?: string;
+ department_id?: number;
+ }[];
+ invoice_expenses?: {
+ id?: number;
+ invoice_id?: number;
+ expense_id?: number;
+ quantity?: number;
+ rate?: number;
+ chart_of_account?: string;
+ description?: string;
+ department_id?: number;
+ }[];
+ invoice_services?: {
+ id?: number;
+ invoice_id?: number;
+ service_id?: number;
+ rate_type?: string;
+ labor_rate_id?: number;
+ working_hours?: number;
+ labor_hours?: number;
+ quantity?: number;
+ rate?: number;
+ chart_of_account?: string;
+ description?: string;
+ department_id?: number;
+ }[];
+ invoice_service_groups?: {
+ id?: number;
+ invoice_id?: number;
+ service_group_id?: number;
+ rate_type?: string;
+ labor_rate_id?: number;
+ working_hours?: number;
+ labor_hours?: number;
+ rate?: number;
+ chart_of_account?: string;
+ description?: string;
+ department_id?: number;
+ }[];
};
};
};
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 6319557..c78c6ed 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -159,6 +159,9 @@ importers:
openapi-fetch:
specifier: ^0.14.0
version: 0.14.1
+ pino:
+ specifier: ^10.3.1
+ version: 10.3.1
server-only:
specifier: '*'
version: 0.0.1
@@ -816,6 +819,9 @@ packages:
'@open-draft/until@2.1.0':
resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==}
+ '@pinojs/redact@0.4.0':
+ resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==}
+
'@radix-ui/number@1.1.1':
resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==}
@@ -2063,6 +2069,10 @@ packages:
resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==}
engines: {node: '>= 4.0.0'}
+ atomic-sleep@1.0.0:
+ resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==}
+ engines: {node: '>=8.0.0'}
+
available-typed-arrays@1.0.7:
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
engines: {node: '>= 0.4'}
@@ -3777,6 +3787,10 @@ packages:
resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==}
engines: {node: '>= 0.4'}
+ on-exit-leak-free@2.1.2:
+ resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==}
+ engines: {node: '>=14.0.0'}
+
on-finished@2.4.1:
resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
engines: {node: '>= 0.8'}
@@ -3903,6 +3917,16 @@ packages:
resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
engines: {node: '>=0.10.0'}
+ pino-abstract-transport@3.0.0:
+ resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==}
+
+ pino-std-serializers@7.1.0:
+ resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==}
+
+ pino@10.3.1:
+ resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==}
+ hasBin: true
+
pkce-challenge@5.0.1:
resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==}
engines: {node: '>=16.20.0'}
@@ -4008,6 +4032,9 @@ packages:
resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==}
engines: {node: '>=18'}
+ process-warning@5.0.0:
+ resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==}
+
process@0.11.10:
resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==}
engines: {node: '>= 0.6.0'}
@@ -4044,6 +4071,9 @@ packages:
queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
+ quick-format-unescaped@4.0.4:
+ resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==}
+
radix-ui@1.4.3:
resolution: {integrity: sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA==}
peerDependencies:
@@ -4146,6 +4176,10 @@ packages:
resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==}
engines: {node: '>=0.10.0'}
+ real-require@0.2.0:
+ resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==}
+ engines: {node: '>= 12.13.0'}
+
recast@0.23.11:
resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==}
engines: {node: '>= 4'}
@@ -4251,6 +4285,10 @@ packages:
resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==}
engines: {node: '>= 0.4'}
+ safe-stable-stringify@2.5.0:
+ resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==}
+ engines: {node: '>=10'}
+
safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
@@ -4342,6 +4380,9 @@ packages:
resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==}
engines: {node: '>=10'}
+ sonic-boom@4.2.1:
+ resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==}
+
sonner@2.0.7:
resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==}
peerDependencies:
@@ -4356,6 +4397,10 @@ packages:
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
engines: {node: '>=0.10.0'}
+ split2@4.2.0:
+ resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
+ engines: {node: '>= 10.x'}
+
sshpk@1.18.0:
resolution: {integrity: sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==}
engines: {node: '>=0.10.0'}
@@ -4490,6 +4535,10 @@ packages:
resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==}
engines: {node: '>=6'}
+ thread-stream@4.0.0:
+ resolution: {integrity: sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==}
+ engines: {node: '>=20'}
+
throttleit@1.0.1:
resolution: {integrity: sha512-vDZpf9Chs9mAdfY046mcPt8fg5QSZr37hEH4TXYBnDF+izxgrbRGUAAaBvIk/fJm9aOFCGFd1EsNg5AZCbnQCQ==}
@@ -4872,7 +4921,7 @@ snapshots:
'@babel/types': 7.29.0
'@jridgewell/remapping': 2.3.5
convert-source-map: 2.0.0
- debug: 4.4.3(supports-color@10.2.2)
+ debug: 4.4.3(supports-color@8.1.1)
gensync: 1.0.0-beta.2
json5: 2.2.3
semver: 6.3.1
@@ -5030,7 +5079,7 @@ snapshots:
'@babel/parser': 7.29.2
'@babel/template': 7.28.6
'@babel/types': 7.29.0
- debug: 4.4.3(supports-color@10.2.2)
+ debug: 4.4.3(supports-color@8.1.1)
transitivePeerDependencies:
- supports-color
@@ -5140,7 +5189,7 @@ snapshots:
'@eslint/config-array@0.21.1':
dependencies:
'@eslint/object-schema': 2.1.7
- debug: 4.4.3(supports-color@10.2.2)
+ debug: 4.4.3(supports-color@8.1.1)
minimatch: 3.1.2
transitivePeerDependencies:
- supports-color
@@ -5148,7 +5197,7 @@ snapshots:
'@eslint/config-array@0.21.2':
dependencies:
'@eslint/object-schema': 2.1.7
- debug: 4.4.3(supports-color@10.2.2)
+ debug: 4.4.3(supports-color@8.1.1)
minimatch: 3.1.5
transitivePeerDependencies:
- supports-color
@@ -5164,7 +5213,7 @@ snapshots:
'@eslint/eslintrc@3.3.1':
dependencies:
ajv: 6.12.6
- debug: 4.4.3(supports-color@10.2.2)
+ debug: 4.4.3(supports-color@8.1.1)
espree: 10.4.0
globals: 14.0.0
ignore: 5.3.2
@@ -5178,7 +5227,7 @@ snapshots:
'@eslint/eslintrc@3.3.5':
dependencies:
ajv: 6.14.0
- debug: 4.4.3(supports-color@10.2.2)
+ debug: 4.4.3(supports-color@8.1.1)
espree: 10.4.0
globals: 14.0.0
ignore: 5.3.2
@@ -5484,6 +5533,8 @@ snapshots:
'@open-draft/until@2.1.0': {}
+ '@pinojs/redact@0.4.0': {}
+
'@radix-ui/number@1.1.1': {}
'@radix-ui/primitive@1.1.3': {}
@@ -6506,7 +6557,7 @@ snapshots:
'@typescript-eslint/types': 8.50.0
'@typescript-eslint/typescript-estree': 8.50.0(typescript@5.9.2)
'@typescript-eslint/visitor-keys': 8.50.0
- debug: 4.4.3(supports-color@10.2.2)
+ debug: 4.4.3(supports-color@8.1.1)
eslint: 9.39.1(jiti@2.6.1)
typescript: 5.9.2
transitivePeerDependencies:
@@ -6518,7 +6569,7 @@ snapshots:
'@typescript-eslint/types': 8.50.0
'@typescript-eslint/typescript-estree': 8.50.0(typescript@5.9.3)
'@typescript-eslint/visitor-keys': 8.50.0
- debug: 4.4.3(supports-color@10.2.2)
+ debug: 4.4.3(supports-color@8.1.1)
eslint: 9.39.4(jiti@2.6.1)
typescript: 5.9.3
transitivePeerDependencies:
@@ -6528,7 +6579,7 @@ snapshots:
dependencies:
'@typescript-eslint/tsconfig-utils': 8.50.0(typescript@5.9.2)
'@typescript-eslint/types': 8.50.0
- debug: 4.4.3(supports-color@10.2.2)
+ debug: 4.4.3(supports-color@8.1.1)
typescript: 5.9.2
transitivePeerDependencies:
- supports-color
@@ -6537,7 +6588,7 @@ snapshots:
dependencies:
'@typescript-eslint/tsconfig-utils': 8.50.0(typescript@5.9.3)
'@typescript-eslint/types': 8.50.0
- debug: 4.4.3(supports-color@10.2.2)
+ debug: 4.4.3(supports-color@8.1.1)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
@@ -6560,7 +6611,7 @@ snapshots:
'@typescript-eslint/types': 8.50.0
'@typescript-eslint/typescript-estree': 8.50.0(typescript@5.9.2)
'@typescript-eslint/utils': 8.50.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.2)
- debug: 4.4.3(supports-color@10.2.2)
+ debug: 4.4.3(supports-color@8.1.1)
eslint: 9.39.1(jiti@2.6.1)
ts-api-utils: 2.1.0(typescript@5.9.2)
typescript: 5.9.2
@@ -6572,7 +6623,7 @@ snapshots:
'@typescript-eslint/types': 8.50.0
'@typescript-eslint/typescript-estree': 8.50.0(typescript@5.9.3)
'@typescript-eslint/utils': 8.50.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
- debug: 4.4.3(supports-color@10.2.2)
+ debug: 4.4.3(supports-color@8.1.1)
eslint: 9.39.4(jiti@2.6.1)
ts-api-utils: 2.1.0(typescript@5.9.3)
typescript: 5.9.3
@@ -6587,7 +6638,7 @@ snapshots:
'@typescript-eslint/tsconfig-utils': 8.50.0(typescript@5.9.2)
'@typescript-eslint/types': 8.50.0
'@typescript-eslint/visitor-keys': 8.50.0
- debug: 4.4.3(supports-color@10.2.2)
+ debug: 4.4.3(supports-color@8.1.1)
minimatch: 9.0.5
semver: 7.7.3
tinyglobby: 0.2.15
@@ -6602,7 +6653,7 @@ snapshots:
'@typescript-eslint/tsconfig-utils': 8.50.0(typescript@5.9.3)
'@typescript-eslint/types': 8.50.0
'@typescript-eslint/visitor-keys': 8.50.0
- debug: 4.4.3(supports-color@10.2.2)
+ debug: 4.4.3(supports-color@8.1.1)
minimatch: 9.0.5
semver: 7.7.3
tinyglobby: 0.2.15
@@ -6851,6 +6902,8 @@ snapshots:
at-least-node@1.0.0: {}
+ atomic-sleep@1.0.0: {}
+
available-typed-arrays@1.0.7:
dependencies:
possible-typed-array-names: 1.1.0
@@ -6883,7 +6936,7 @@ snapshots:
dependencies:
bytes: 3.1.2
content-type: 1.0.5
- debug: 4.4.3(supports-color@10.2.2)
+ debug: 4.4.3(supports-color@8.1.1)
http-errors: 2.0.1
iconv-lite: 0.7.2
on-finished: 2.4.1
@@ -7476,7 +7529,7 @@ snapshots:
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.6.1)):
dependencies:
'@nolyfill/is-core-module': 1.0.39
- debug: 4.4.3(supports-color@10.2.2)
+ debug: 4.4.3(supports-color@8.1.1)
eslint: 9.39.4(jiti@2.6.1)
get-tsconfig: 4.13.7
is-bun-module: 2.0.0
@@ -7640,7 +7693,7 @@ snapshots:
ajv: 6.12.6
chalk: 4.1.2
cross-spawn: 7.0.6
- debug: 4.4.3(supports-color@10.2.2)
+ debug: 4.4.3(supports-color@8.1.1)
escape-string-regexp: 4.0.0
eslint-scope: 8.4.0
eslint-visitor-keys: 4.2.1
@@ -7681,7 +7734,7 @@ snapshots:
ajv: 6.14.0
chalk: 4.1.2
cross-spawn: 7.0.6
- debug: 4.4.3(supports-color@10.2.2)
+ debug: 4.4.3(supports-color@8.1.1)
escape-string-regexp: 4.0.0
eslint-scope: 8.4.0
eslint-visitor-keys: 4.2.1
@@ -7793,7 +7846,7 @@ snapshots:
content-type: 1.0.5
cookie: 0.7.2
cookie-signature: 1.2.2
- debug: 4.4.3(supports-color@10.2.2)
+ debug: 4.4.3(supports-color@8.1.1)
depd: 2.0.0
encodeurl: 2.0.0
escape-html: 1.0.3
@@ -7891,7 +7944,7 @@ snapshots:
finalhandler@2.1.1:
dependencies:
- debug: 4.4.3(supports-color@10.2.2)
+ debug: 4.4.3(supports-color@8.1.1)
encodeurl: 2.0.0
escape-html: 1.0.3
on-finished: 2.4.1
@@ -8095,6 +8148,13 @@ snapshots:
jsprim: 2.0.2
sshpk: 1.18.0
+ https-proxy-agent@7.0.6:
+ dependencies:
+ agent-base: 7.1.4
+ debug: 4.4.3(supports-color@8.1.1)
+ transitivePeerDependencies:
+ - supports-color
+
https-proxy-agent@7.0.6(supports-color@10.2.2):
dependencies:
agent-base: 7.1.4
@@ -8708,6 +8768,8 @@ snapshots:
define-properties: 1.2.1
es-object-atoms: 1.1.1
+ on-exit-leak-free@2.1.2: {}
+
on-finished@2.4.1:
dependencies:
ee-first: 1.1.1
@@ -8839,6 +8901,26 @@ snapshots:
pify@2.3.0: {}
+ pino-abstract-transport@3.0.0:
+ dependencies:
+ split2: 4.2.0
+
+ pino-std-serializers@7.1.0: {}
+
+ pino@10.3.1:
+ dependencies:
+ '@pinojs/redact': 0.4.0
+ atomic-sleep: 1.0.0
+ on-exit-leak-free: 2.1.2
+ pino-abstract-transport: 3.0.0
+ pino-std-serializers: 7.1.0
+ process-warning: 5.0.0
+ quick-format-unescaped: 4.0.4
+ real-require: 0.2.0
+ safe-stable-stringify: 2.5.0
+ sonic-boom: 4.2.1
+ thread-stream: 4.0.0
+
pkce-challenge@5.0.1: {}
pluralize@8.0.0: {}
@@ -8880,6 +8962,8 @@ snapshots:
dependencies:
parse-ms: 4.0.0
+ process-warning@5.0.0: {}
+
process@0.11.10: {}
prompts@2.4.2:
@@ -8917,6 +9001,8 @@ snapshots:
queue-microtask@1.2.3: {}
+ quick-format-unescaped@4.0.4: {}
+
radix-ui@1.4.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
dependencies:
'@radix-ui/primitive': 1.1.3
@@ -9058,6 +9144,8 @@ snapshots:
react@19.2.4: {}
+ real-require@0.2.0: {}
+
recast@0.23.11:
dependencies:
ast-types: 0.16.1
@@ -9156,7 +9244,7 @@ snapshots:
router@2.2.0:
dependencies:
- debug: 4.4.3(supports-color@10.2.2)
+ debug: 4.4.3(supports-color@8.1.1)
depd: 2.0.0
is-promise: 4.0.0
parseurl: 1.3.3
@@ -9195,6 +9283,8 @@ snapshots:
es-errors: 1.3.0
is-regex: 1.2.1
+ safe-stable-stringify@2.5.0: {}
+
safer-buffer@2.1.2: {}
scheduler@0.27.0: {}
@@ -9205,7 +9295,7 @@ snapshots:
send@1.2.1:
dependencies:
- debug: 4.4.3(supports-color@10.2.2)
+ debug: 4.4.3(supports-color@8.1.1)
encodeurl: 2.0.0
escape-html: 1.0.3
etag: 1.8.1
@@ -9273,7 +9363,7 @@ snapshots:
fast-glob: 3.3.3
fs-extra: 11.3.4
fuzzysort: 3.1.0
- https-proxy-agent: 7.0.6(supports-color@10.2.2)
+ https-proxy-agent: 7.0.6
kleur: 4.1.5
msw: 2.12.14(@types/node@25.5.0)(typescript@5.9.3)
node-fetch: 3.3.2
@@ -9381,6 +9471,10 @@ snapshots:
astral-regex: 2.0.0
is-fullwidth-code-point: 3.0.0
+ sonic-boom@4.2.1:
+ dependencies:
+ atomic-sleep: 1.0.0
+
sonner@2.0.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
dependencies:
react: 19.2.4
@@ -9390,6 +9484,8 @@ snapshots:
source-map@0.6.1: {}
+ split2@4.2.0: {}
+
sshpk@1.18.0:
dependencies:
asn1: 0.2.6
@@ -9530,6 +9626,10 @@ snapshots:
tapable@2.3.0: {}
+ thread-stream@4.0.0:
+ dependencies:
+ real-require: 0.2.0
+
throttleit@1.0.1: {}
through@2.3.8: {}