@mgsoftwarebv/mcp-server-bridge 3.5.7 → 3.5.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -103433,10 +103433,28 @@ var customerRecurringServices = pgTable(
103433
103433
  // Frozen RecurringContractTerms (notice period, minimum term, renewal
103434
103434
  // policy, cancellation effect, billing after cancellation, free notes).
103435
103435
  contractTerms: jsonb("contract_terms").$type(),
103436
+ // Frozen ProductClause — customer-specific scope/limits snapshot.
103437
+ clause: jsonb().$type(),
103438
+ // Structured discount applied on top of catalog price.
103439
+ discountType: text("discount_type"),
103440
+ discountValue: numericCasted("discount_value", { precision: 10, scale: 2 }),
103441
+ // Compact invoice line text (overrides name on draft invoices when set).
103442
+ lineDescription: text("line_description"),
103443
+ // Umbrella contract label for grouped billable lines on invoices.
103444
+ umbrellaLabel: text("umbrella_label"),
103445
+ // Pro-rata first invoice for yearly variants starting mid-period.
103446
+ proRataFirstInvoice: boolean3("pro_rata_first_invoice").default(false).notNull(),
103447
+ // End of billing/contract period for pro-rata (defaults to end of start year).
103448
+ contractPeriodEnd: timestamp("contract_period_end", {
103449
+ withTimezone: true,
103450
+ mode: "string"
103451
+ }),
103436
103452
  // --- Lifecycle ---
103437
103453
  // "active" | "paused" | "cancelled" | "ended"
103438
103454
  status: text().$type().default("active").notNull(),
103439
103455
  startDate: timestamp("start_date", { withTimezone: true, mode: "string" }),
103456
+ // Planned end of the agreement (before cancellation/ended).
103457
+ endDate: timestamp("end_date", { withTimezone: true, mode: "string" }),
103440
103458
  // Next renewal boundary (end of current covered term).
103441
103459
  renewalDate: timestamp("renewal_date", {
103442
103460
  withTimezone: true,
@@ -107383,6 +107401,90 @@ var TOOLS = [
107383
107401
  },
107384
107402
  required: ["projectId", "directoryPath"]
107385
107403
  }
107404
+ },
107405
+ {
107406
+ name: "get-customer-agreements",
107407
+ description: "List per-customer product agreements (customer_recurring_services): custom price, discount, clause snapshot, umbrella label, billing cadence, start/end dates.",
107408
+ inputSchema: {
107409
+ type: "object",
107410
+ properties: {
107411
+ teamId: teamIdProp,
107412
+ customerId: { type: "string", description: "Filter by customer UUID" },
107413
+ status: {
107414
+ type: "string",
107415
+ enum: ["active", "paused", "cancelled", "ended"],
107416
+ description: "Filter by agreement status"
107417
+ }
107418
+ }
107419
+ }
107420
+ },
107421
+ {
107422
+ name: "create-customer-agreement",
107423
+ description: "Create a customer-specific product agreement between catalog and billing. Snapshots price, discount, clause and invoice line text without mutating the catalog product.",
107424
+ inputSchema: {
107425
+ type: "object",
107426
+ properties: {
107427
+ teamId: teamIdProp,
107428
+ customerId: { type: "string", description: "Customer UUID" },
107429
+ productId: { type: "string", description: "Optional catalog product UUID" },
107430
+ name: { type: "string", description: "Agreement name" },
107431
+ description: { type: "string" },
107432
+ price: { type: "number" },
107433
+ billingInterval: { type: "string", enum: ["month", "year"] },
107434
+ discountType: { type: "string", enum: ["percentage", "amount"] },
107435
+ discountValue: { type: "number" },
107436
+ discountDescription: { type: "string" },
107437
+ lineDescription: {
107438
+ type: "string",
107439
+ description: "Compact invoice line text override"
107440
+ },
107441
+ umbrellaLabel: {
107442
+ type: "string",
107443
+ description: "Umbrella contract label for grouped invoice lines"
107444
+ },
107445
+ clause: productClauseProp,
107446
+ startDate: { type: "string", description: "ISO start date" },
107447
+ endDate: { type: "string", description: "ISO planned end date" },
107448
+ contractPeriodEnd: {
107449
+ type: "string",
107450
+ description: "ISO period end for pro-rata calculations"
107451
+ },
107452
+ proRataFirstInvoice: { type: "boolean" }
107453
+ },
107454
+ required: ["customerId", "name"]
107455
+ }
107456
+ },
107457
+ {
107458
+ name: "update-customer-agreement",
107459
+ description: "Update an existing customer product agreement by id.",
107460
+ inputSchema: {
107461
+ type: "object",
107462
+ properties: {
107463
+ teamId: teamIdProp,
107464
+ id: { type: "string", description: "Agreement UUID" },
107465
+ customerId: { type: "string" },
107466
+ productId: { type: "string" },
107467
+ name: { type: "string" },
107468
+ description: { type: "string" },
107469
+ price: { type: "number" },
107470
+ billingInterval: { type: "string", enum: ["month", "year"] },
107471
+ discountType: { type: "string", enum: ["percentage", "amount"] },
107472
+ discountValue: { type: "number" },
107473
+ discountDescription: { type: "string" },
107474
+ lineDescription: { type: "string" },
107475
+ umbrellaLabel: { type: "string" },
107476
+ clause: productClauseProp,
107477
+ startDate: { type: "string" },
107478
+ endDate: { type: "string" },
107479
+ contractPeriodEnd: { type: "string" },
107480
+ proRataFirstInvoice: { type: "boolean" },
107481
+ status: {
107482
+ type: "string",
107483
+ enum: ["active", "paused", "cancelled", "ended"]
107484
+ }
107485
+ },
107486
+ required: ["id"]
107487
+ }
107386
107488
  }
107387
107489
  ];
107388
107490
  var RESOURCES = [
@@ -108477,6 +108579,250 @@ The customer had no projects, tickets, invoices, quotations, documents, time ent
108477
108579
  );
108478
108580
  }
108479
108581
 
108582
+ // ../invoice/src/utils/product-clause.ts
108583
+ function trimToNull(value) {
108584
+ if (typeof value !== "string") return null;
108585
+ const trimmed = value.trim();
108586
+ return trimmed.length > 0 ? trimmed : null;
108587
+ }
108588
+ function parseStringList(value) {
108589
+ if (!Array.isArray(value)) return [];
108590
+ const out = [];
108591
+ for (const entry of value) {
108592
+ const label = trimToNull(entry);
108593
+ if (label) out.push(label);
108594
+ }
108595
+ return out;
108596
+ }
108597
+ function parseLimits(value) {
108598
+ if (!value || typeof value !== "object") return null;
108599
+ const raw = value;
108600
+ const hoursRaw = raw.includedHoursPerMonth;
108601
+ let includedHoursPerMonth = null;
108602
+ if (typeof hoursRaw === "number" && Number.isFinite(hoursRaw)) {
108603
+ includedHoursPerMonth = hoursRaw;
108604
+ } else if (typeof hoursRaw === "string" && hoursRaw.trim() !== "") {
108605
+ const parsed = Number(hoursRaw);
108606
+ if (Number.isFinite(parsed)) includedHoursPerMonth = parsed;
108607
+ }
108608
+ const rollover = typeof raw.rollover === "boolean" ? raw.rollover : null;
108609
+ const limits = {
108610
+ includedHoursPerMonth,
108611
+ rollover,
108612
+ minimumBillingUnitExtraWork: trimToNull(raw.minimumBillingUnitExtraWork),
108613
+ responseTime: trimToNull(raw.responseTime),
108614
+ supportLevel: trimToNull(raw.supportLevel)
108615
+ };
108616
+ return isLimitsEmpty(limits) ? null : limits;
108617
+ }
108618
+ function isLimitsEmpty(limits) {
108619
+ if (!limits) return true;
108620
+ return limits.includedHoursPerMonth == null && limits.rollover == null && !limits.minimumBillingUnitExtraWork && !limits.responseTime && !limits.supportLevel;
108621
+ }
108622
+ function parseProductClause(value) {
108623
+ if (value == null) return null;
108624
+ if (typeof value !== "object" || Array.isArray(value)) return null;
108625
+ const raw = value;
108626
+ const clause = {
108627
+ commercialDescription: trimToNull(raw.commercialDescription),
108628
+ includedScope: parseStringList(raw.includedScope),
108629
+ excludedScope: parseStringList(raw.excludedScope),
108630
+ limits: parseLimits(raw.limits),
108631
+ customerValue: trimToNull(raw.customerValue),
108632
+ extraWorkConditions: trimToNull(raw.extraWorkConditions)
108633
+ };
108634
+ return isClauseEmpty(clause) ? null : clause;
108635
+ }
108636
+ function isClauseEmpty(clause) {
108637
+ if (!clause) return true;
108638
+ return !clause.commercialDescription && (clause.includedScope?.length ?? 0) === 0 && (clause.excludedScope?.length ?? 0) === 0 && isLimitsEmpty(clause.limits) && !clause.customerValue && !clause.extraWorkConditions;
108639
+ }
108640
+ function serializeProductClause(clause) {
108641
+ if (clause == null) return null;
108642
+ const parsed = parseProductClause(clause);
108643
+ if (!parsed) return null;
108644
+ const out = {};
108645
+ if (parsed.commercialDescription) {
108646
+ out.commercialDescription = parsed.commercialDescription;
108647
+ }
108648
+ if (parsed.includedScope && parsed.includedScope.length > 0) {
108649
+ out.includedScope = parsed.includedScope;
108650
+ }
108651
+ if (parsed.excludedScope && parsed.excludedScope.length > 0) {
108652
+ out.excludedScope = parsed.excludedScope;
108653
+ }
108654
+ if (!isLimitsEmpty(parsed.limits)) {
108655
+ const limits = parsed.limits;
108656
+ const cleanedLimits = {};
108657
+ if (limits.includedHoursPerMonth != null) {
108658
+ cleanedLimits.includedHoursPerMonth = limits.includedHoursPerMonth;
108659
+ }
108660
+ if (limits.rollover != null) cleanedLimits.rollover = limits.rollover;
108661
+ if (limits.minimumBillingUnitExtraWork) {
108662
+ cleanedLimits.minimumBillingUnitExtraWork = limits.minimumBillingUnitExtraWork;
108663
+ }
108664
+ if (limits.responseTime) cleanedLimits.responseTime = limits.responseTime;
108665
+ if (limits.supportLevel) cleanedLimits.supportLevel = limits.supportLevel;
108666
+ out.limits = cleanedLimits;
108667
+ }
108668
+ if (parsed.customerValue) out.customerValue = parsed.customerValue;
108669
+ if (parsed.extraWorkConditions) {
108670
+ out.extraWorkConditions = parsed.extraWorkConditions;
108671
+ }
108672
+ return Object.keys(out).length > 0 ? out : null;
108673
+ }
108674
+ function clauseLimitLines(limits) {
108675
+ if (isLimitsEmpty(limits) || !limits) return [];
108676
+ const lines = [];
108677
+ if (limits.includedHoursPerMonth != null) {
108678
+ lines.push(`Inbegrepen uren per maand: ${limits.includedHoursPerMonth}`);
108679
+ }
108680
+ if (limits.rollover != null) {
108681
+ lines.push(
108682
+ limits.rollover ? "Niet-gebruikte uren schuiven door naar de volgende maand" : "Niet-gebruikte uren vervallen aan het einde van de maand"
108683
+ );
108684
+ }
108685
+ if (limits.minimumBillingUnitExtraWork) {
108686
+ lines.push(
108687
+ `Minimale facturatie-eenheid meerwerk: ${limits.minimumBillingUnitExtraWork}`
108688
+ );
108689
+ }
108690
+ if (limits.supportLevel) lines.push(`Supportniveau: ${limits.supportLevel}`);
108691
+ if (limits.responseTime) lines.push(`Responstijd: ${limits.responseTime}`);
108692
+ return lines;
108693
+ }
108694
+ function clauseSummaryLines(clause) {
108695
+ const parsed = parseProductClause(clause);
108696
+ if (!parsed) return [];
108697
+ const lines = [];
108698
+ if (parsed.commercialDescription) {
108699
+ lines.push(parsed.commercialDescription);
108700
+ }
108701
+ if (parsed.includedScope?.length) {
108702
+ lines.push(`Inbegrepen: ${parsed.includedScope.join("; ")}`);
108703
+ }
108704
+ if (parsed.excludedScope?.length) {
108705
+ lines.push(`Niet inbegrepen: ${parsed.excludedScope.join("; ")}`);
108706
+ }
108707
+ lines.push(...clauseLimitLines(parsed.limits));
108708
+ if (parsed.customerValue) {
108709
+ lines.push(`Klantwaarde: ${parsed.customerValue}`);
108710
+ }
108711
+ if (parsed.extraWorkConditions) {
108712
+ lines.push(`Meerwerk: ${parsed.extraWorkConditions}`);
108713
+ }
108714
+ return lines;
108715
+ }
108716
+
108717
+ // src/tools/customer-agreements.ts
108718
+ function textResponse2(text3) {
108719
+ return { content: [{ type: "text", text: text3 }] };
108720
+ }
108721
+ function formatAgreement(row) {
108722
+ const parts = [
108723
+ `**${row.name}** (${row.status})`,
108724
+ `id: ${row.id}`,
108725
+ `customerId: ${row.customerId}`,
108726
+ row.productId ? `productId: ${row.productId}` : null,
108727
+ row.price != null ? `price: ${row.price} ${row.currency ?? "EUR"}` : null,
108728
+ row.billingInterval ? `billing: ${row.billingInterval}` : null,
108729
+ row.discountType && row.discountValue != null ? `discount: ${row.discountType} ${row.discountValue}` : null,
108730
+ row.lineDescription ? `invoice line: ${row.lineDescription}` : null,
108731
+ row.umbrellaLabel ? `umbrella: ${row.umbrellaLabel}` : null,
108732
+ row.startDate ? `start: ${row.startDate}` : null,
108733
+ row.endDate ? `end: ${row.endDate}` : null,
108734
+ row.proRataFirstInvoice ? "pro-rata first invoice: yes" : null
108735
+ ].filter(Boolean);
108736
+ return parts.join("\n");
108737
+ }
108738
+ async function handleGetCustomerAgreements(input) {
108739
+ const scope = await resolveTeamScope(input.teamId);
108740
+ if (!scope.ok) return scope.response;
108741
+ const conditions = [inArray(schema_exports.customerRecurringServices.teamId, scope.teamIds)];
108742
+ if (input.customerId) {
108743
+ conditions.push(
108744
+ eq(schema_exports.customerRecurringServices.customerId, input.customerId)
108745
+ );
108746
+ }
108747
+ if (input.status) {
108748
+ conditions.push(eq(schema_exports.customerRecurringServices.status, input.status));
108749
+ }
108750
+ const rows = await db.select().from(schema_exports.customerRecurringServices).where(and(...conditions)).orderBy(desc(schema_exports.customerRecurringServices.createdAt)).limit(50);
108751
+ if (rows.length === 0) {
108752
+ return textResponse2("No customer agreements found.");
108753
+ }
108754
+ return textResponse2(rows.map(formatAgreement).join("\n\n---\n\n"));
108755
+ }
108756
+ async function handleCreateCustomerAgreement(input) {
108757
+ const scope = await resolveTeamScope(input.teamId);
108758
+ if (!scope.ok) return scope.response;
108759
+ const teamId = scope.teamIds[0];
108760
+ const [row] = await db.insert(schema_exports.customerRecurringServices).values({
108761
+ teamId,
108762
+ customerId: input.customerId,
108763
+ productId: input.productId ?? null,
108764
+ name: input.name,
108765
+ description: input.description ?? null,
108766
+ price: input.price ?? null,
108767
+ billingInterval: input.billingInterval ?? null,
108768
+ discountType: input.discountType ?? null,
108769
+ discountValue: input.discountValue ?? null,
108770
+ discountDescription: input.discountDescription ?? null,
108771
+ lineDescription: input.lineDescription ?? null,
108772
+ umbrellaLabel: input.umbrellaLabel ?? null,
108773
+ clause: serializeProductClause(input.clause ?? null),
108774
+ startDate: input.startDate ?? null,
108775
+ endDate: input.endDate ?? null,
108776
+ contractPeriodEnd: input.contractPeriodEnd ?? null,
108777
+ proRataFirstInvoice: input.proRataFirstInvoice ?? false,
108778
+ status: "active"
108779
+ }).returning();
108780
+ return textResponse2(`Created customer agreement:
108781
+
108782
+ ${formatAgreement(row)}`);
108783
+ }
108784
+ async function handleUpdateCustomerAgreement(input) {
108785
+ const scope = await resolveTeamScope(input.teamId);
108786
+ if (!scope.ok) return scope.response;
108787
+ const teamId = scope.teamIds[0];
108788
+ const patch = {
108789
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
108790
+ };
108791
+ if (input.name !== void 0) patch.name = input.name;
108792
+ if (input.description !== void 0) patch.description = input.description;
108793
+ if (input.price !== void 0) patch.price = input.price;
108794
+ if (input.billingInterval !== void 0)
108795
+ patch.billingInterval = input.billingInterval;
108796
+ if (input.discountType !== void 0) patch.discountType = input.discountType;
108797
+ if (input.discountValue !== void 0) patch.discountValue = input.discountValue;
108798
+ if (input.discountDescription !== void 0)
108799
+ patch.discountDescription = input.discountDescription;
108800
+ if (input.lineDescription !== void 0)
108801
+ patch.lineDescription = input.lineDescription;
108802
+ if (input.umbrellaLabel !== void 0) patch.umbrellaLabel = input.umbrellaLabel;
108803
+ if (input.clause !== void 0)
108804
+ patch.clause = serializeProductClause(input.clause ?? null);
108805
+ if (input.startDate !== void 0) patch.startDate = input.startDate;
108806
+ if (input.endDate !== void 0) patch.endDate = input.endDate;
108807
+ if (input.contractPeriodEnd !== void 0)
108808
+ patch.contractPeriodEnd = input.contractPeriodEnd;
108809
+ if (input.proRataFirstInvoice !== void 0)
108810
+ patch.proRataFirstInvoice = input.proRataFirstInvoice;
108811
+ if (input.status !== void 0) patch.status = input.status;
108812
+ const [row] = await db.update(schema_exports.customerRecurringServices).set(patch).where(
108813
+ and(
108814
+ eq(schema_exports.customerRecurringServices.id, input.id),
108815
+ eq(schema_exports.customerRecurringServices.teamId, teamId)
108816
+ )
108817
+ ).returning();
108818
+ if (!row) {
108819
+ return textResponse2("Agreement not found.");
108820
+ }
108821
+ return textResponse2(`Updated customer agreement:
108822
+
108823
+ ${formatAgreement(row)}`);
108824
+ }
108825
+
108480
108826
  // ../document/src/humanizer/rules.ts
108481
108827
  var REPLACEMENTS = [
108482
108828
  // --- Dutch clichés ---
@@ -113692,9 +114038,15 @@ function parseIncludedItems(value) {
113692
114038
  const raw = entry;
113693
114039
  const label = String(raw.label ?? "").trim();
113694
114040
  if (!label) continue;
114041
+ const billable = raw.billable === false ? false : raw.billable === true ? true : void 0;
114042
+ const rawPrice = raw.price;
114043
+ const price = typeof rawPrice === "number" && Number.isFinite(rawPrice) ? rawPrice : typeof rawPrice === "string" && rawPrice.trim() !== "" ? Number(rawPrice) : null;
113695
114044
  items.push({
113696
114045
  label,
113697
- productId: raw.productId ?? null
114046
+ productId: typeof raw.productId === "string" ? raw.productId : null,
114047
+ ...billable !== void 0 ? { billable } : {},
114048
+ ...price != null && Number.isFinite(price) ? { price } : {},
114049
+ ...typeof raw.pricingOptionId === "string" ? { pricingOptionId: raw.pricingOptionId } : {}
113698
114050
  });
113699
114051
  }
113700
114052
  }
@@ -113702,10 +114054,15 @@ function parseIncludedItems(value) {
113702
114054
  }
113703
114055
  function serializeIncludedItems(items) {
113704
114056
  if (items == null) return null;
113705
- const cleaned = items.map((item) => ({
113706
- label: item.label.trim(),
113707
- ...item.productId ? { productId: item.productId } : {}
113708
- })).filter((item) => item.label.length > 0);
114057
+ const cleaned = items.map((item) => {
114058
+ const out = { label: item.label.trim() };
114059
+ if (item.productId) out.productId = item.productId;
114060
+ if (item.billable === false) out.billable = false;
114061
+ else if (item.billable === true) out.billable = true;
114062
+ if (item.price != null && Number.isFinite(item.price)) out.price = item.price;
114063
+ if (item.pricingOptionId) out.pricingOptionId = item.pricingOptionId;
114064
+ return out;
114065
+ }).filter((item) => item.label.length > 0);
113709
114066
  return cleaned.length > 0 ? cleaned : null;
113710
114067
  }
113711
114068
  function includedItemLabels(items) {
@@ -113714,6 +114071,14 @@ function includedItemLabels(items) {
113714
114071
  return parsed.map((item) => item.label);
113715
114072
  }
113716
114073
 
114074
+ // ../invoice/src/utils/finalize-line-items.ts
114075
+ function billableLineTotal(item) {
114076
+ if (item.billable === false) return 0;
114077
+ const price = item.price ?? 0;
114078
+ const quantity = item.quantity ?? 0;
114079
+ return price * quantity;
114080
+ }
114081
+
113717
114082
  // ../invoice/src/utils/product-options.ts
113718
114083
  function findOption(group3, optionId) {
113719
114084
  if (!optionId) return void 0;
@@ -113785,156 +114150,21 @@ function clampQuantity(quantity, group3) {
113785
114150
  return max != null ? Math.min(lower, max) : lower;
113786
114151
  }
113787
114152
 
113788
- // ../invoice/src/utils/product-clause.ts
113789
- function trimToNull(value) {
113790
- if (typeof value !== "string") return null;
113791
- const trimmed = value.trim();
113792
- return trimmed.length > 0 ? trimmed : null;
114153
+ // ../invoice/src/utils/pricing-options.ts
114154
+ function defaultMonthsCovered(interval2) {
114155
+ return interval2 === "year" ? 12 : 1;
113793
114156
  }
113794
- function parseStringList(value) {
113795
- if (!Array.isArray(value)) return [];
113796
- const out = [];
113797
- for (const entry of value) {
113798
- const label = trimToNull(entry);
113799
- if (label) out.push(label);
114157
+ function formatRecurringNote(option) {
114158
+ const period = option.billingInterval === "year" ? "per jaar" : "per maand";
114159
+ const bits = [`${option.label} (${period})`];
114160
+ if (option.discountDescription) bits.push(option.discountDescription);
114161
+ const contract = option.contract;
114162
+ const terms = [];
114163
+ if (contract?.minimumTermMonths) {
114164
+ terms.push(`min. looptijd ${contract.minimumTermMonths} mnd`);
113800
114165
  }
113801
- return out;
113802
- }
113803
- function parseLimits(value) {
113804
- if (!value || typeof value !== "object") return null;
113805
- const raw = value;
113806
- const hoursRaw = raw.includedHoursPerMonth;
113807
- let includedHoursPerMonth = null;
113808
- if (typeof hoursRaw === "number" && Number.isFinite(hoursRaw)) {
113809
- includedHoursPerMonth = hoursRaw;
113810
- } else if (typeof hoursRaw === "string" && hoursRaw.trim() !== "") {
113811
- const parsed = Number(hoursRaw);
113812
- if (Number.isFinite(parsed)) includedHoursPerMonth = parsed;
113813
- }
113814
- const rollover = typeof raw.rollover === "boolean" ? raw.rollover : null;
113815
- const limits = {
113816
- includedHoursPerMonth,
113817
- rollover,
113818
- minimumBillingUnitExtraWork: trimToNull(raw.minimumBillingUnitExtraWork),
113819
- responseTime: trimToNull(raw.responseTime),
113820
- supportLevel: trimToNull(raw.supportLevel)
113821
- };
113822
- return isLimitsEmpty(limits) ? null : limits;
113823
- }
113824
- function isLimitsEmpty(limits) {
113825
- if (!limits) return true;
113826
- return limits.includedHoursPerMonth == null && limits.rollover == null && !limits.minimumBillingUnitExtraWork && !limits.responseTime && !limits.supportLevel;
113827
- }
113828
- function parseProductClause(value) {
113829
- if (value == null) return null;
113830
- if (typeof value !== "object" || Array.isArray(value)) return null;
113831
- const raw = value;
113832
- const clause = {
113833
- commercialDescription: trimToNull(raw.commercialDescription),
113834
- includedScope: parseStringList(raw.includedScope),
113835
- excludedScope: parseStringList(raw.excludedScope),
113836
- limits: parseLimits(raw.limits),
113837
- customerValue: trimToNull(raw.customerValue),
113838
- extraWorkConditions: trimToNull(raw.extraWorkConditions)
113839
- };
113840
- return isClauseEmpty(clause) ? null : clause;
113841
- }
113842
- function isClauseEmpty(clause) {
113843
- if (!clause) return true;
113844
- return !clause.commercialDescription && (clause.includedScope?.length ?? 0) === 0 && (clause.excludedScope?.length ?? 0) === 0 && isLimitsEmpty(clause.limits) && !clause.customerValue && !clause.extraWorkConditions;
113845
- }
113846
- function serializeProductClause(clause) {
113847
- if (clause == null) return null;
113848
- const parsed = parseProductClause(clause);
113849
- if (!parsed) return null;
113850
- const out = {};
113851
- if (parsed.commercialDescription) {
113852
- out.commercialDescription = parsed.commercialDescription;
113853
- }
113854
- if (parsed.includedScope && parsed.includedScope.length > 0) {
113855
- out.includedScope = parsed.includedScope;
113856
- }
113857
- if (parsed.excludedScope && parsed.excludedScope.length > 0) {
113858
- out.excludedScope = parsed.excludedScope;
113859
- }
113860
- if (!isLimitsEmpty(parsed.limits)) {
113861
- const limits = parsed.limits;
113862
- const cleanedLimits = {};
113863
- if (limits.includedHoursPerMonth != null) {
113864
- cleanedLimits.includedHoursPerMonth = limits.includedHoursPerMonth;
113865
- }
113866
- if (limits.rollover != null) cleanedLimits.rollover = limits.rollover;
113867
- if (limits.minimumBillingUnitExtraWork) {
113868
- cleanedLimits.minimumBillingUnitExtraWork = limits.minimumBillingUnitExtraWork;
113869
- }
113870
- if (limits.responseTime) cleanedLimits.responseTime = limits.responseTime;
113871
- if (limits.supportLevel) cleanedLimits.supportLevel = limits.supportLevel;
113872
- out.limits = cleanedLimits;
113873
- }
113874
- if (parsed.customerValue) out.customerValue = parsed.customerValue;
113875
- if (parsed.extraWorkConditions) {
113876
- out.extraWorkConditions = parsed.extraWorkConditions;
113877
- }
113878
- return Object.keys(out).length > 0 ? out : null;
113879
- }
113880
- function clauseLimitLines(limits) {
113881
- if (isLimitsEmpty(limits) || !limits) return [];
113882
- const lines = [];
113883
- if (limits.includedHoursPerMonth != null) {
113884
- lines.push(`Inbegrepen uren per maand: ${limits.includedHoursPerMonth}`);
113885
- }
113886
- if (limits.rollover != null) {
113887
- lines.push(
113888
- limits.rollover ? "Niet-gebruikte uren schuiven door naar de volgende maand" : "Niet-gebruikte uren vervallen aan het einde van de maand"
113889
- );
113890
- }
113891
- if (limits.minimumBillingUnitExtraWork) {
113892
- lines.push(
113893
- `Minimale facturatie-eenheid meerwerk: ${limits.minimumBillingUnitExtraWork}`
113894
- );
113895
- }
113896
- if (limits.supportLevel) lines.push(`Supportniveau: ${limits.supportLevel}`);
113897
- if (limits.responseTime) lines.push(`Responstijd: ${limits.responseTime}`);
113898
- return lines;
113899
- }
113900
- function clauseSummaryLines(clause) {
113901
- const parsed = parseProductClause(clause);
113902
- if (!parsed) return [];
113903
- const lines = [];
113904
- if (parsed.commercialDescription) {
113905
- lines.push(parsed.commercialDescription);
113906
- }
113907
- if (parsed.includedScope?.length) {
113908
- lines.push(`Inbegrepen: ${parsed.includedScope.join("; ")}`);
113909
- }
113910
- if (parsed.excludedScope?.length) {
113911
- lines.push(`Niet inbegrepen: ${parsed.excludedScope.join("; ")}`);
113912
- }
113913
- lines.push(...clauseLimitLines(parsed.limits));
113914
- if (parsed.customerValue) {
113915
- lines.push(`Klantwaarde: ${parsed.customerValue}`);
113916
- }
113917
- if (parsed.extraWorkConditions) {
113918
- lines.push(`Meerwerk: ${parsed.extraWorkConditions}`);
113919
- }
113920
- return lines;
113921
- }
113922
-
113923
- // ../invoice/src/utils/pricing-options.ts
113924
- function defaultMonthsCovered(interval2) {
113925
- return interval2 === "year" ? 12 : 1;
113926
- }
113927
- function formatRecurringNote(option) {
113928
- const period = option.billingInterval === "year" ? "per jaar" : "per maand";
113929
- const bits = [`${option.label} (${period})`];
113930
- if (option.discountDescription) bits.push(option.discountDescription);
113931
- const contract = option.contract;
113932
- const terms = [];
113933
- if (contract?.minimumTermMonths) {
113934
- terms.push(`min. looptijd ${contract.minimumTermMonths} mnd`);
113935
- }
113936
- if (contract?.noticePeriodDays) {
113937
- terms.push(`opzegtermijn ${contract.noticePeriodDays} dagen`);
114166
+ if (contract?.noticePeriodDays) {
114167
+ terms.push(`opzegtermijn ${contract.noticePeriodDays} dagen`);
113938
114168
  }
113939
114169
  if (contract?.renewalPolicy === "auto_renew") {
113940
114170
  terms.push("stilzwijgend verlengd");
@@ -114011,7 +114241,7 @@ function lineFinancials(quantity, price, defaults) {
114011
114241
  }
114012
114242
  function computeInvoiceTotals(items, defaults, discount = 0) {
114013
114243
  const subtotal = items.reduce(
114014
- (sum, i6) => sum + (i6.quantity || 0) * (i6.price || 0),
114244
+ (sum, i6) => sum + billableLineTotal(i6),
114015
114245
  0
114016
114246
  );
114017
114247
  const vat = defaults.includeVat ? subtotal * (defaults.vatRate / 100) : 0;
@@ -114090,16 +114320,25 @@ function buildInvoiceLineFromProduct(product, opts, defaults) {
114090
114320
  if (pricingOption && !(product.isConfigurable && product.options)) {
114091
114321
  price = pricingOption.price;
114092
114322
  }
114323
+ if (opts.customPrice != null) price = opts.customPrice;
114324
+ let proRata = null;
114093
114325
  if (opts.prorate && pricingOption?.billingInterval === "year" && opts.startDate) {
114094
- const proRata = computeProRata({
114326
+ const result = computeProRata({
114095
114327
  pricePerPeriod: price,
114096
114328
  startDate: opts.startDate,
114097
114329
  periodEndDate: opts.periodEndDate,
114098
114330
  monthsInPeriod: pricingOption.monthsCovered ?? 12
114099
114331
  });
114100
- if (proRata) price = proRata.amount;
114332
+ if (result) {
114333
+ price = result.amount;
114334
+ proRata = {
114335
+ months: result.months,
114336
+ fraction: result.fraction,
114337
+ periodStart: result.periodStart,
114338
+ periodEnd: result.periodEnd
114339
+ };
114340
+ }
114101
114341
  }
114102
- if (opts.customPrice != null) price = opts.customPrice;
114103
114342
  const { vat, tax } = lineFinancials(quantity, price, defaults);
114104
114343
  return {
114105
114344
  name: compactLineDescription(product, opts.customDescription),
@@ -114111,7 +114350,11 @@ function buildInvoiceLineFromProduct(product, opts, defaults) {
114111
114350
  productId: product.id,
114112
114351
  configuration,
114113
114352
  clause,
114114
- pricingOption: pricingOption ?? null
114353
+ pricingOption: pricingOption ?? null,
114354
+ includedItems: serializeIncludedItems(parseIncludedItems(product.includedItems)),
114355
+ billable: opts.billable === false ? false : true,
114356
+ groupLabel: opts.groupLabel ?? null,
114357
+ proRata
114115
114358
  };
114116
114359
  }
114117
114360
  function formatLineItemDetail(line2, index2) {
@@ -114125,6 +114368,13 @@ function formatLineItemDetail(line2, index2) {
114125
114368
  if (line2.pricingOption) {
114126
114369
  parts.push(` variant: ${formatRecurringNote(line2.pricingOption)}`);
114127
114370
  }
114371
+ if (line2.proRata) {
114372
+ parts.push(
114373
+ ` pro-rata: ${line2.proRata.months} months (${Math.round(line2.proRata.fraction * 100)}%)`
114374
+ );
114375
+ }
114376
+ if (line2.groupLabel) parts.push(` group: ${line2.groupLabel}`);
114377
+ if (line2.billable === false) parts.push(" (scope only \u2014 not billable)");
114128
114378
  const clauseLines = clauseSummaryLines(parseProductClause(line2.clause));
114129
114379
  if (clauseLines.length > 0) {
114130
114380
  parts.push(" scope:");
@@ -114145,7 +114395,7 @@ var INVOICE_STATUSES = [
114145
114395
  "refunded"
114146
114396
  ];
114147
114397
  var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
114148
- function textResponse2(text3) {
114398
+ function textResponse3(text3) {
114149
114399
  return { content: [{ type: "text", text: text3 }] };
114150
114400
  }
114151
114401
  function tiptapNote(text3) {
@@ -114263,7 +114513,7 @@ async function resolveInvoiceLineItems(inputs, defaults, teamId) {
114263
114513
  return { items };
114264
114514
  }
114265
114515
  function notDraftResponse(invoice) {
114266
- return textResponse2(
114516
+ return textResponse3(
114267
114517
  `Invoice ${invoice.invoiceNumber ?? invoice.id} has status "${invoice.status}", not "draft". These tools only modify draft invoices \u2014 sent/paid/unpaid/overdue invoices are immutable here.`
114268
114518
  );
114269
114519
  }
@@ -114280,14 +114530,14 @@ Issue: ${invoice.issueDate ? new Date(invoice.issueDate).toLocaleDateString() :
114280
114530
  async function handleGetInvoices(input) {
114281
114531
  const { customerId, status, q: q3, pageSize = 20 } = input;
114282
114532
  if (status && !INVOICE_STATUSES.includes(status)) {
114283
- return textResponse2(
114533
+ return textResponse3(
114284
114534
  `Error: invalid status "${status}". Allowed: ${INVOICE_STATUSES.join(", ")}.`
114285
114535
  );
114286
114536
  }
114287
114537
  const scope = await resolveTeamScope(input.teamId);
114288
114538
  if (!scope.ok) return scope.response;
114289
114539
  if (scope.teamIds.length === 0) {
114290
- return textResponse2("No accessible teams found.");
114540
+ return textResponse3("No accessible teams found.");
114291
114541
  }
114292
114542
  const filters = [inArray(schema_exports.invoices.teamId, scope.teamIds)];
114293
114543
  if (customerId) filters.push(eq(schema_exports.invoices.customerId, customerId));
@@ -114314,7 +114564,7 @@ async function handleGetInvoices(input) {
114314
114564
  createdAt: schema_exports.invoices.createdAt
114315
114565
  }).from(schema_exports.invoices).where(and(...filters)).orderBy(desc(schema_exports.invoices.createdAt)).limit(Math.min(pageSize, 100));
114316
114566
  if (rows.length === 0) {
114317
- return textResponse2("No invoices found.");
114567
+ return textResponse3("No invoices found.");
114318
114568
  }
114319
114569
  const list = rows.map(
114320
114570
  (inv) => `**${inv.invoiceNumber ?? "(draft, no number)"}**
@@ -114324,7 +114574,7 @@ Customer: ${inv.customerName ?? inv.customerId ?? "(none)"}
114324
114574
  Issue date: ${inv.issueDate ? new Date(inv.issueDate).toLocaleDateString() : "-"} | Due: ${inv.dueDate ? new Date(inv.dueDate).toLocaleDateString() : "-"}
114325
114575
  `
114326
114576
  ).join("\n");
114327
- return textResponse2(
114577
+ return textResponse3(
114328
114578
  `Found ${rows.length} invoices:
114329
114579
 
114330
114580
  ${list}
@@ -114333,15 +114583,15 @@ Use \`get-invoice-by-id\` for line items and linked documents. Use \`link-docume
114333
114583
  }
114334
114584
  async function handleGetInvoiceById(input) {
114335
114585
  const { invoiceId } = input;
114336
- if (!invoiceId) return textResponse2("Error: `invoiceId` is required.");
114586
+ if (!invoiceId) return textResponse3("Error: `invoiceId` is required.");
114337
114587
  const scope = await resolveTeamScope(input.teamId);
114338
114588
  if (!scope.ok) return scope.response;
114339
114589
  if (scope.teamIds.length === 0) {
114340
- return textResponse2("No accessible teams found.");
114590
+ return textResponse3("No accessible teams found.");
114341
114591
  }
114342
114592
  const invoice = await loadInvoiceByIdentifier(invoiceId, scope.teamIds);
114343
114593
  if (!invoice) {
114344
- return textResponse2(
114594
+ return textResponse3(
114345
114595
  `Invoice ${invoiceId} not found or you don't have access to it.`
114346
114596
  );
114347
114597
  }
@@ -114358,7 +114608,7 @@ async function handleGetInvoiceById(input) {
114358
114608
  );
114359
114609
  const linesText = lineItems.length > 0 ? lineItems.map((line2, i6) => formatLineItemDetail(line2, i6)).join("\n\n") : "(no line items)";
114360
114610
  const docsText = linkedDocs.length > 0 ? linkedDocs.map((d6) => `- ${d6.title} (${d6.type ?? "document"}) \u2014 ${d6.id}`).join("\n") : "(none)";
114361
- return textResponse2(
114611
+ return textResponse3(
114362
114612
  `**Invoice ${invoice.invoiceNumber ?? invoice.id}**
114363
114613
 
114364
114614
  ID: ${invoice.id}
@@ -114380,12 +114630,12 @@ ${docsText}
114380
114630
  }
114381
114631
  async function handleUpdateInvoice(input) {
114382
114632
  const { invoiceId } = input;
114383
- if (!invoiceId) return textResponse2("Error: `invoiceId` is required.");
114633
+ if (!invoiceId) return textResponse3("Error: `invoiceId` is required.");
114384
114634
  const resolved = await resolveTeamId(input.teamId);
114385
114635
  if (!resolved.ok) return resolved.response;
114386
114636
  const invoice = await loadInvoiceInTeam(invoiceId, resolved.teamId);
114387
114637
  if (!invoice) {
114388
- return textResponse2(
114638
+ return textResponse3(
114389
114639
  `Invoice ${invoiceId} not found or not owned by this team.`
114390
114640
  );
114391
114641
  }
@@ -114412,7 +114662,7 @@ async function handleUpdateInvoice(input) {
114412
114662
  defaults,
114413
114663
  invoice.teamId
114414
114664
  );
114415
- if (error49) return textResponse2(`Error: ${error49}`);
114665
+ if (error49) return textResponse3(`Error: ${error49}`);
114416
114666
  const totals = computeInvoiceTotals(
114417
114667
  items,
114418
114668
  defaults,
@@ -114425,28 +114675,28 @@ async function handleUpdateInvoice(input) {
114425
114675
  updates.amount = totals.amount;
114426
114676
  }
114427
114677
  if (Object.keys(updates).length === 0) {
114428
- return textResponse2(
114678
+ return textResponse3(
114429
114679
  "No fields to update. Provide at least one of: title, note, internalNote, dueDate, issueDate, lineItems."
114430
114680
  );
114431
114681
  }
114432
114682
  updates.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
114433
114683
  const [updated] = await db.update(schema_exports.invoices).set(updates).where(eq(schema_exports.invoices.id, invoice.id)).returning(INVOICE_DETAIL_COLUMNS);
114434
- if (!updated) return textResponse2(`Failed to update invoice ${invoiceId}.`);
114435
- return textResponse2(`\u2705 **Draft invoice updated**
114684
+ if (!updated) return textResponse3(`Failed to update invoice ${invoiceId}.`);
114685
+ return textResponse3(`\u2705 **Draft invoice updated**
114436
114686
 
114437
114687
  ${formatInvoiceSummary(updated)}`);
114438
114688
  }
114439
114689
  async function handleUpdateInvoiceLines(input) {
114440
114690
  const { invoiceId, lineItems: patches } = input;
114441
- if (!invoiceId) return textResponse2("Error: `invoiceId` is required.");
114691
+ if (!invoiceId) return textResponse3("Error: `invoiceId` is required.");
114442
114692
  if (!patches || patches.length === 0) {
114443
- return textResponse2("Error: `lineItems` must be a non-empty array.");
114693
+ return textResponse3("Error: `lineItems` must be a non-empty array.");
114444
114694
  }
114445
114695
  const resolved = await resolveTeamId(input.teamId);
114446
114696
  if (!resolved.ok) return resolved.response;
114447
114697
  const invoice = await loadInvoiceInTeam(invoiceId, resolved.teamId);
114448
114698
  if (!invoice) {
114449
- return textResponse2(
114699
+ return textResponse3(
114450
114700
  `Invoice ${invoiceId} not found or not owned by this team.`
114451
114701
  );
114452
114702
  }
@@ -114460,7 +114710,7 @@ async function handleUpdateInvoiceLines(input) {
114460
114710
  for (const patch of patches) {
114461
114711
  const index2 = patch.index;
114462
114712
  if (index2 < 0 || index2 >= items.length) {
114463
- return textResponse2(
114713
+ return textResponse3(
114464
114714
  `Error: line index ${index2} is out of range (invoice has ${items.length} line(s)).`
114465
114715
  );
114466
114716
  }
@@ -114487,13 +114737,13 @@ async function handleUpdateInvoiceLines(input) {
114487
114737
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
114488
114738
  }).where(eq(schema_exports.invoices.id, invoice.id)).returning(INVOICE_DETAIL_COLUMNS);
114489
114739
  if (!updated) {
114490
- return textResponse2(`Failed to update invoice lines for ${invoiceId}.`);
114740
+ return textResponse3(`Failed to update invoice lines for ${invoiceId}.`);
114491
114741
  }
114492
114742
  const changedLines = patches.map((p3) => {
114493
114743
  const line2 = items[p3.index];
114494
114744
  return `[${p3.index}] ${plainTextFromLineItemName(line2.name)}`;
114495
114745
  }).join("\n");
114496
- return textResponse2(
114746
+ return textResponse3(
114497
114747
  `\u2705 **Updated ${updatedCount} line item(s) on draft invoice ${updated.invoiceNumber ?? updated.id}**
114498
114748
 
114499
114749
  ${changedLines}
@@ -114503,13 +114753,13 @@ New total: ${updated.amount} ${updated.currency} (subtotal ${updated.subtotal},
114503
114753
  }
114504
114754
  async function handleAddProductToInvoice(input) {
114505
114755
  const { invoiceId, productId } = input;
114506
- if (!invoiceId) return textResponse2("Error: `invoiceId` is required.");
114507
- if (!productId) return textResponse2("Error: `productId` is required.");
114756
+ if (!invoiceId) return textResponse3("Error: `invoiceId` is required.");
114757
+ if (!productId) return textResponse3("Error: `productId` is required.");
114508
114758
  const resolved = await resolveTeamId(input.teamId);
114509
114759
  if (!resolved.ok) return resolved.response;
114510
114760
  const invoice = await loadInvoiceInTeam(invoiceId, resolved.teamId);
114511
114761
  if (!invoice) {
114512
- return textResponse2(
114762
+ return textResponse3(
114513
114763
  `Invoice ${invoiceId} not found or not owned by this team.`
114514
114764
  );
114515
114765
  }
@@ -114517,7 +114767,7 @@ async function handleAddProductToInvoice(input) {
114517
114767
  const products = await loadProductsInTeam([productId], invoice.teamId);
114518
114768
  const product = products.get(productId);
114519
114769
  if (!product) {
114520
- return textResponse2(
114770
+ return textResponse3(
114521
114771
  `Product ${productId} not found or not owned by this team.`
114522
114772
  );
114523
114773
  }
@@ -114549,9 +114799,9 @@ async function handleAddProductToInvoice(input) {
114549
114799
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
114550
114800
  }).where(eq(schema_exports.invoices.id, invoice.id)).returning(INVOICE_DETAIL_COLUMNS);
114551
114801
  if (!updated) {
114552
- return textResponse2(`Failed to add product to invoice ${invoiceId}.`);
114802
+ return textResponse3(`Failed to add product to invoice ${invoiceId}.`);
114553
114803
  }
114554
- return textResponse2(
114804
+ return textResponse3(
114555
114805
  `\u2705 **Product added to draft invoice ${updated.invoiceNumber ?? updated.id}**
114556
114806
 
114557
114807
  ` + formatLineItemDetail(newItem, items.length - 1) + `
@@ -114563,12 +114813,12 @@ Clause and pricing variant are snapshotted on the line \u2014 later catalog edit
114563
114813
  async function handleLinkDocumentToInvoice(input) {
114564
114814
  const { documentId, invoiceId } = input;
114565
114815
  if (!documentId) {
114566
- return textResponse2("Error: `documentId` is required.");
114816
+ return textResponse3("Error: `documentId` is required.");
114567
114817
  }
114568
114818
  const scope = await resolveTeamScope(input.teamId);
114569
114819
  if (!scope.ok) return scope.response;
114570
114820
  if (scope.teamIds.length === 0) {
114571
- return textResponse2("No accessible teams found.");
114821
+ return textResponse3("No accessible teams found.");
114572
114822
  }
114573
114823
  const [doc] = await db.select({
114574
114824
  id: schema_exports.documents.id,
@@ -114583,24 +114833,24 @@ async function handleLinkDocumentToInvoice(input) {
114583
114833
  )
114584
114834
  ).limit(1);
114585
114835
  if (!doc) {
114586
- return textResponse2(
114836
+ return textResponse3(
114587
114837
  `Document ${documentId} not found or you don't have access to it.`
114588
114838
  );
114589
114839
  }
114590
114840
  if (!invoiceId) {
114591
114841
  await db.update(schema_exports.documents).set({ invoiceId: null, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq(schema_exports.documents.id, doc.id));
114592
- return textResponse2(
114842
+ return textResponse3(
114593
114843
  `\u2705 Document "${doc.title}" (${doc.id}) is unlinked from its invoice.`
114594
114844
  );
114595
114845
  }
114596
114846
  const invoice = await findAccessibleInvoice(invoiceId, [doc.teamId]);
114597
114847
  if (!invoice) {
114598
- return textResponse2(
114848
+ return textResponse3(
114599
114849
  `Error: invoice ${invoiceId} not found in team ${doc.teamId}. Use get-invoices to find a valid invoice id.`
114600
114850
  );
114601
114851
  }
114602
114852
  await db.update(schema_exports.documents).set({ invoiceId, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq(schema_exports.documents.id, doc.id));
114603
- return textResponse2(
114853
+ return textResponse3(
114604
114854
  `\u2705 **Document linked to invoice!**
114605
114855
 
114606
114856
  Document: ${doc.title} (${doc.id})
@@ -114733,7 +114983,7 @@ ${description ? `Description: ${description}
114733
114983
  ]
114734
114984
  };
114735
114985
  }
114736
- function textResponse3(text3) {
114986
+ function textResponse4(text3) {
114737
114987
  return { content: [{ type: "text", text: text3 }] };
114738
114988
  }
114739
114989
  function memberLabel(m4) {
@@ -114747,7 +114997,7 @@ async function requireTeamOwner2(teamId, userId) {
114747
114997
  eq(schema_exports.usersOnTeam.teamId, teamId)
114748
114998
  )
114749
114999
  ).limit(1);
114750
- return membership?.role === "owner" ? null : textResponse3(OWNER_REQUIRED);
115000
+ return membership?.role === "owner" ? null : textResponse4(OWNER_REQUIRED);
114751
115001
  }
114752
115002
  async function setProjectMemberAccess(params) {
114753
115003
  const { projectId, teamId, memberIds, createdBy } = params;
@@ -114851,7 +115101,7 @@ async function resolveTeamMember(teamId, opts) {
114851
115101
  if (!match) {
114852
115102
  return {
114853
115103
  ok: false,
114854
- response: textResponse3(
115104
+ response: textResponse4(
114855
115105
  `User ${opts.userId} is not a member of this team. Call get-project-members to see the team roster.`
114856
115106
  )
114857
115107
  };
@@ -114864,7 +115114,7 @@ async function resolveTeamMember(teamId, opts) {
114864
115114
  if (matches.length === 0) {
114865
115115
  return {
114866
115116
  ok: false,
114867
- response: textResponse3(
115117
+ response: textResponse4(
114868
115118
  `No team member found with email "${opts.email}". Call get-project-members to see the team roster.`
114869
115119
  )
114870
115120
  };
@@ -114872,7 +115122,7 @@ async function resolveTeamMember(teamId, opts) {
114872
115122
  if (matches.length > 1) {
114873
115123
  return {
114874
115124
  ok: false,
114875
- response: textResponse3(
115125
+ response: textResponse4(
114876
115126
  `Multiple team members match email "${opts.email}". Pass an explicit userId instead.`
114877
115127
  )
114878
115128
  };
@@ -114881,7 +115131,7 @@ async function resolveTeamMember(teamId, opts) {
114881
115131
  }
114882
115132
  return {
114883
115133
  ok: false,
114884
- response: textResponse3(
115134
+ response: textResponse4(
114885
115135
  "Provide either a userId or an email to identify the member."
114886
115136
  )
114887
115137
  };
@@ -114930,7 +115180,7 @@ async function handleUpdateProject(input) {
114930
115180
  if (!resolved.ok) return resolved.response;
114931
115181
  const existing = await loadProjectInTeam(id, resolved.teamId);
114932
115182
  if (!existing) {
114933
- return textResponse3(
115183
+ return textResponse4(
114934
115184
  `Project ${id} not found, or it is not owned by this team.`
114935
115185
  );
114936
115186
  }
@@ -114945,7 +115195,7 @@ async function handleUpdateProject(input) {
114945
115195
  )
114946
115196
  ).limit(1);
114947
115197
  if (dupe) {
114948
- return textResponse3(
115198
+ return textResponse4(
114949
115199
  `A project named "${input.name}" already exists in this team. Choose a different name.`
114950
115200
  );
114951
115201
  }
@@ -115010,7 +115260,7 @@ async function handleUpdateProject(input) {
115010
115260
  customerName: schema_exports.customers.name
115011
115261
  }).from(schema_exports.projects).leftJoin(schema_exports.customers, eq(schema_exports.projects.customerId, schema_exports.customers.id)).where(eq(schema_exports.projects.id, id)).limit(1);
115012
115262
  if (!updated) {
115013
- return textResponse3(`Failed to update project ${id}.`);
115263
+ return textResponse4(`Failed to update project ${id}.`);
115014
115264
  }
115015
115265
  const lines = [
115016
115266
  "\u2705 **Project Updated**",
@@ -115028,7 +115278,7 @@ async function handleUpdateProject(input) {
115028
115278
  if (willRename) {
115029
115279
  lines.push("", "Note: tickets for this project were renumbered.");
115030
115280
  }
115031
- return textResponse3(lines.join("\n"));
115281
+ return textResponse4(lines.join("\n"));
115032
115282
  }
115033
115283
  async function handleGetProjectMembers(input) {
115034
115284
  const { projectId } = input;
@@ -115036,7 +115286,7 @@ async function handleGetProjectMembers(input) {
115036
115286
  if (!resolved.ok) return resolved.response;
115037
115287
  const project = await loadProjectInTeam(projectId, resolved.teamId);
115038
115288
  if (!project) {
115039
- return textResponse3(
115289
+ return textResponse4(
115040
115290
  `Project ${projectId} not found, or it is not owned by this team.`
115041
115291
  );
115042
115292
  }
@@ -115065,7 +115315,7 @@ async function handleGetProjectMembers(input) {
115065
115315
  return `- ${memberLabel(m4)} (userId: ${m4.userId}, role: ${m4.role ?? "member"}) \u2014 ${access}`;
115066
115316
  }).join("\n");
115067
115317
  const note = state2.projectMemberIds.size === 0 ? "No members are explicitly assigned to this project, so every owner and every unrestricted member can see it." : `${state2.projectMemberIds.size} member(s) are explicitly assigned to this project.`;
115068
- return textResponse3(
115318
+ return textResponse4(
115069
115319
  `**Project members for "${project.name}"** (ID: ${project.id})
115070
115320
 
115071
115321
  ${note}
@@ -115086,7 +115336,7 @@ async function handleSetProjectMembers(input) {
115086
115336
  if (ownerError) return ownerError;
115087
115337
  const project = await loadProjectInTeam(projectId, resolved.teamId);
115088
115338
  if (!project) {
115089
- return textResponse3(
115339
+ return textResponse4(
115090
115340
  `Project ${projectId} not found, or it is not owned by this team.`
115091
115341
  );
115092
115342
  }
@@ -115124,7 +115374,7 @@ async function handleSetProjectMembers(input) {
115124
115374
 
115125
115375
  \u26A0\uFE0F ${names} previously had no restrictions (could see all projects). They are now restricted to only the projects explicitly assigned to them.`;
115126
115376
  }
115127
- return textResponse3(
115377
+ return textResponse4(
115128
115378
  `\u2705 **Project members updated**
115129
115379
 
115130
115380
  Members with explicit access to this project:
@@ -115140,7 +115390,7 @@ async function handleAddProjectMember(input) {
115140
115390
  if (ownerError) return ownerError;
115141
115391
  const project = await loadProjectInTeam(projectId, resolved.teamId);
115142
115392
  if (!project) {
115143
- return textResponse3(
115393
+ return textResponse4(
115144
115394
  `Project ${projectId} not found, or it is not owned by this team.`
115145
115395
  );
115146
115396
  }
@@ -115151,7 +115401,7 @@ async function handleAddProjectMember(input) {
115151
115401
  if (!member2.ok) return member2.response;
115152
115402
  const state2 = await getProjectAccessState(resolved.teamId, projectId);
115153
115403
  if (state2.projectMemberIds.has(member2.member.userId)) {
115154
- return textResponse3(
115404
+ return textResponse4(
115155
115405
  `${memberLabel(member2.member)} already has explicit access to this project.`
115156
115406
  );
115157
115407
  }
@@ -115166,7 +115416,7 @@ async function handleAddProjectMember(input) {
115166
115416
  if (wasUnrestricted) {
115167
115417
  text3 += "\n\n\u26A0\uFE0F This member previously had no access restrictions (they could see all projects). They are now restricted to ONLY the projects explicitly assigned to them. Grant any other projects they still need with add-project-member, or remove all their assignments to restore full visibility.";
115168
115418
  }
115169
- return textResponse3(text3);
115419
+ return textResponse4(text3);
115170
115420
  }
115171
115421
  async function handleRemoveProjectMember(input) {
115172
115422
  const ctx = getAuthContext();
@@ -115177,7 +115427,7 @@ async function handleRemoveProjectMember(input) {
115177
115427
  if (ownerError) return ownerError;
115178
115428
  const project = await loadProjectInTeam(projectId, resolved.teamId);
115179
115429
  if (!project) {
115180
- return textResponse3(
115430
+ return textResponse4(
115181
115431
  `Project ${projectId} not found, or it is not owned by this team.`
115182
115432
  );
115183
115433
  }
@@ -115188,7 +115438,7 @@ async function handleRemoveProjectMember(input) {
115188
115438
  if (!member2.ok) return member2.response;
115189
115439
  const state2 = await getProjectAccessState(resolved.teamId, projectId);
115190
115440
  if (!state2.projectMemberIds.has(member2.member.userId)) {
115191
- return textResponse3(
115441
+ return textResponse4(
115192
115442
  `${memberLabel(member2.member)} has no explicit assignment to this project; nothing to remove.`
115193
115443
  );
115194
115444
  }
@@ -115204,7 +115454,7 @@ async function handleRemoveProjectMember(input) {
115204
115454
  if ((state2.rowCountByUser.get(member2.member.userId) ?? 0) <= 1) {
115205
115455
  text3 += "\n\nThis was the member's last project assignment, so their access restrictions were cleared \u2014 they can see all projects in the team again (default behavior).";
115206
115456
  }
115207
- return textResponse3(text3);
115457
+ return textResponse4(text3);
115208
115458
  }
115209
115459
  async function loadProjectForCleanup(projectId, teamId) {
115210
115460
  const accessibleTeamIds = await getAccessibleTeamIds(teamId);
@@ -115232,25 +115482,25 @@ async function countProjectDependencies(projectId) {
115232
115482
  }
115233
115483
  async function handleArchiveProject(input) {
115234
115484
  const { projectId, reason } = input;
115235
- if (!projectId) return textResponse3("Error: `projectId` is required.");
115485
+ if (!projectId) return textResponse4("Error: `projectId` is required.");
115236
115486
  const resolved = await resolveTeamId(input.teamId);
115237
115487
  if (!resolved.ok) return resolved.response;
115238
115488
  const project = await loadProjectForCleanup(projectId, resolved.teamId);
115239
115489
  if (!project) {
115240
- return textResponse3(
115490
+ return textResponse4(
115241
115491
  `Project ${projectId} not found, or it is not owned by this team.`
115242
115492
  );
115243
115493
  }
115244
115494
  const state2 = getProjectArchiveState(project.settings);
115245
115495
  if (state2.archived) {
115246
- return textResponse3(
115496
+ return textResponse4(
115247
115497
  `Project "${project.name}" (${project.id}) is already archived${state2.archivedAt ? ` (since ${state2.archivedAt})` : ""}.`
115248
115498
  );
115249
115499
  }
115250
115500
  const archivedAt = (/* @__PURE__ */ new Date()).toISOString();
115251
115501
  const nextSettings = withArchiveSettings(project.settings, archivedAt, reason);
115252
115502
  await db.update(schema_exports.projects).set({ settings: nextSettings, updatedAt: sql`now()` }).where(eq(schema_exports.projects.id, project.id));
115253
- return textResponse3(
115503
+ return textResponse4(
115254
115504
  `\u2705 **Project archived**
115255
115505
 
115256
115506
  Project: ${project.name}
@@ -115268,21 +115518,21 @@ Note: the archive flag is stored in \`projects.settings.archivedAt\`; the dashbo
115268
115518
  async function handleDeleteProject(input) {
115269
115519
  const ctx = getAuthContext();
115270
115520
  const { projectId, confirmEmptyOnly } = input;
115271
- if (!projectId) return textResponse3("Error: `projectId` is required.");
115521
+ if (!projectId) return textResponse4("Error: `projectId` is required.");
115272
115522
  const resolved = await resolveTeamId(input.teamId);
115273
115523
  if (!resolved.ok) return resolved.response;
115274
115524
  const ownerError = await requireTeamOwner2(resolved.teamId, ctx.userId);
115275
115525
  if (ownerError) return ownerError;
115276
115526
  const project = await loadProjectForCleanup(projectId, resolved.teamId);
115277
115527
  if (!project) {
115278
- return textResponse3(
115528
+ return textResponse4(
115279
115529
  `Project ${projectId} not found, or it is not owned by this team.`
115280
115530
  );
115281
115531
  }
115282
115532
  const deps = await countProjectDependencies(project.id);
115283
115533
  const summary = formatProjectDependencies(deps);
115284
115534
  if (!isProjectEmpty(deps)) {
115285
- return textResponse3(
115535
+ return textResponse4(
115286
115536
  `\u{1F6AB} **Delete blocked** \u2014 project "${project.name}" (${project.id}) is not empty.
115287
115537
 
115288
115538
  Dependencies: ${summary}.
@@ -115291,12 +115541,12 @@ A hard delete would orphan these records, so it is not allowed. Use archive-proj
115291
115541
  );
115292
115542
  }
115293
115543
  if (confirmEmptyOnly !== true) {
115294
- return textResponse3(
115544
+ return textResponse4(
115295
115545
  `Project "${project.name}" (${project.id}) has no dependencies and can be safely deleted. This is a permanent hard delete. Re-run delete-project with confirmEmptyOnly: true to proceed (or use archive-project to keep the record).`
115296
115546
  );
115297
115547
  }
115298
115548
  await db.delete(schema_exports.projects).where(eq(schema_exports.projects.id, project.id));
115299
- return textResponse3(
115549
+ return textResponse4(
115300
115550
  `\u2705 **Project deleted**
115301
115551
 
115302
115552
  Project: ${project.name}
@@ -115349,7 +115599,7 @@ var PRODUCT_COLUMNS2 = {
115349
115599
  createdAt: schema_exports.invoiceProducts.createdAt,
115350
115600
  updatedAt: schema_exports.invoiceProducts.updatedAt
115351
115601
  };
115352
- function textResponse4(text3) {
115602
+ function textResponse5(text3) {
115353
115603
  return { content: [{ type: "text", text: text3 }] };
115354
115604
  }
115355
115605
  function formatPrice(p3) {
@@ -115395,14 +115645,14 @@ async function handleGetProducts(input) {
115395
115645
  const { q: q3, currency, pageSize = 20 } = input;
115396
115646
  const status = input.status ?? "active";
115397
115647
  if (!PRODUCT_STATUSES.includes(status)) {
115398
- return textResponse4(
115648
+ return textResponse5(
115399
115649
  `Error: invalid status "${status}". Allowed: ${PRODUCT_STATUSES.join(", ")}.`
115400
115650
  );
115401
115651
  }
115402
115652
  const scope = await resolveTeamScope(input.teamId);
115403
115653
  if (!scope.ok) return scope.response;
115404
115654
  if (scope.teamIds.length === 0) {
115405
- return textResponse4("No accessible teams found.");
115655
+ return textResponse5("No accessible teams found.");
115406
115656
  }
115407
115657
  const filters = [inArray(schema_exports.invoiceProducts.teamId, scope.teamIds)];
115408
115658
  if (status === "active") {
@@ -115425,11 +115675,11 @@ async function handleGetProducts(input) {
115425
115675
  asc(schema_exports.invoiceProducts.name)
115426
115676
  ).limit(Math.min(pageSize, 100));
115427
115677
  if (rows.length === 0) {
115428
- return textResponse4(
115678
+ return textResponse5(
115429
115679
  `No products found${status !== "all" ? ` (status: ${status})` : ""}.`
115430
115680
  );
115431
115681
  }
115432
- return textResponse4(
115682
+ return textResponse5(
115433
115683
  `Found ${rows.length} product(s):
115434
115684
 
115435
115685
  ${rows.map(formatProduct).join("\n")}`
@@ -115437,11 +115687,11 @@ ${rows.map(formatProduct).join("\n")}`
115437
115687
  }
115438
115688
  async function handleGetProductById(input) {
115439
115689
  const { productId } = input;
115440
- if (!productId) return textResponse4("Error: `productId` is required.");
115690
+ if (!productId) return textResponse5("Error: `productId` is required.");
115441
115691
  const scope = await resolveTeamScope(input.teamId);
115442
115692
  if (!scope.ok) return scope.response;
115443
115693
  if (scope.teamIds.length === 0) {
115444
- return textResponse4("No accessible teams found.");
115694
+ return textResponse5("No accessible teams found.");
115445
115695
  }
115446
115696
  const [row] = await db.select(PRODUCT_COLUMNS2).from(schema_exports.invoiceProducts).where(
115447
115697
  and(
@@ -115450,11 +115700,11 @@ async function handleGetProductById(input) {
115450
115700
  )
115451
115701
  ).limit(1);
115452
115702
  if (!row) {
115453
- return textResponse4(
115703
+ return textResponse5(
115454
115704
  `Product ${productId} not found or you don't have access to it.`
115455
115705
  );
115456
115706
  }
115457
- return textResponse4(formatProduct(row));
115707
+ return textResponse5(formatProduct(row));
115458
115708
  }
115459
115709
  async function loadProductInTeam(productId, teamId) {
115460
115710
  const accessibleTeamIds = await getAccessibleTeamIds(teamId);
@@ -115469,10 +115719,10 @@ async function loadProductInTeam(productId, teamId) {
115469
115719
  async function handleCreateProduct(input) {
115470
115720
  const { name: name21, description, price, currency, unit } = input;
115471
115721
  if (!name21 || name21.trim().length === 0) {
115472
- return textResponse4("Error: `name` is required.");
115722
+ return textResponse5("Error: `name` is required.");
115473
115723
  }
115474
115724
  const enumError = validateEnum("billingType", input.billingType, BILLING_TYPES) ?? validateEnum("category", input.category, CATEGORIES) ?? validateEnum("tier", input.tier, TIERS);
115475
- if (enumError) return textResponse4(enumError);
115725
+ if (enumError) return textResponse5(enumError);
115476
115726
  const resolved = await resolveTeamId(input.teamId);
115477
115727
  if (!resolved.ok) return resolved.response;
115478
115728
  const [created] = await db.insert(schema_exports.invoiceProducts).values({
@@ -115492,8 +115742,8 @@ async function handleCreateProduct(input) {
115492
115742
  isActive: true,
115493
115743
  lastUsedAt: (/* @__PURE__ */ new Date()).toISOString()
115494
115744
  }).returning(PRODUCT_COLUMNS2);
115495
- if (!created) return textResponse4("Failed to create product.");
115496
- return textResponse4(
115745
+ if (!created) return textResponse5("Failed to create product.");
115746
+ return textResponse5(
115497
115747
  `\u2705 **Product created**
115498
115748
 
115499
115749
  ${formatProduct(created)}`
@@ -115501,21 +115751,21 @@ ${formatProduct(created)}`
115501
115751
  }
115502
115752
  async function handleUpdateProduct(input) {
115503
115753
  const { productId } = input;
115504
- if (!productId) return textResponse4("Error: `productId` is required.");
115754
+ if (!productId) return textResponse5("Error: `productId` is required.");
115505
115755
  const resolved = await resolveTeamId(input.teamId);
115506
115756
  if (!resolved.ok) return resolved.response;
115507
115757
  const existing = await loadProductInTeam(productId, resolved.teamId);
115508
115758
  if (!existing) {
115509
- return textResponse4(
115759
+ return textResponse5(
115510
115760
  `Product ${productId} not found, or it is not owned by this team.`
115511
115761
  );
115512
115762
  }
115513
115763
  const enumError = validateEnum("billingType", input.billingType, BILLING_TYPES) ?? validateEnum("category", input.category, CATEGORIES) ?? validateEnum("tier", input.tier, TIERS);
115514
- if (enumError) return textResponse4(enumError);
115764
+ if (enumError) return textResponse5(enumError);
115515
115765
  const updates = {};
115516
115766
  if (input.name !== void 0) {
115517
115767
  if (!input.name || input.name.trim().length === 0) {
115518
- return textResponse4("Error: `name` cannot be empty.");
115768
+ return textResponse5("Error: `name` cannot be empty.");
115519
115769
  }
115520
115770
  updates.name = input.name.trim();
115521
115771
  }
@@ -115536,14 +115786,14 @@ async function handleUpdateProduct(input) {
115536
115786
  updates.clause = serializeProductClause(input.clause);
115537
115787
  }
115538
115788
  if (Object.keys(updates).length === 0) {
115539
- return textResponse4(
115789
+ return textResponse5(
115540
115790
  "No fields to update. Provide at least one of: name, description, price, currency, unit, isActive, billingType, category, includedItems, optional, tier, sortOrder, clause."
115541
115791
  );
115542
115792
  }
115543
115793
  updates.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
115544
115794
  const [updated] = await db.update(schema_exports.invoiceProducts).set(updates).where(eq(schema_exports.invoiceProducts.id, existing.id)).returning(PRODUCT_COLUMNS2);
115545
- if (!updated) return textResponse4(`Failed to update product ${productId}.`);
115546
- return textResponse4(
115795
+ if (!updated) return textResponse5(`Failed to update product ${productId}.`);
115796
+ return textResponse5(
115547
115797
  `\u2705 **Product updated**
115548
115798
 
115549
115799
  ${formatProduct(updated)}
@@ -115552,23 +115802,23 @@ Note: this only affects future invoices/quotes. Existing documents keep their li
115552
115802
  }
115553
115803
  async function handleArchiveProduct(input) {
115554
115804
  const { productId, reason } = input;
115555
- if (!productId) return textResponse4("Error: `productId` is required.");
115805
+ if (!productId) return textResponse5("Error: `productId` is required.");
115556
115806
  const resolved = await resolveTeamId(input.teamId);
115557
115807
  if (!resolved.ok) return resolved.response;
115558
115808
  const existing = await loadProductInTeam(productId, resolved.teamId);
115559
115809
  if (!existing) {
115560
- return textResponse4(
115810
+ return textResponse5(
115561
115811
  `Product ${productId} not found, or it is not owned by this team.`
115562
115812
  );
115563
115813
  }
115564
115814
  if (!existing.isActive) {
115565
- return textResponse4(
115815
+ return textResponse5(
115566
115816
  `Product "${existing.name}" (${existing.id}) is already archived.`
115567
115817
  );
115568
115818
  }
115569
115819
  const [archived] = await db.update(schema_exports.invoiceProducts).set({ isActive: false, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq(schema_exports.invoiceProducts.id, existing.id)).returning(PRODUCT_COLUMNS2);
115570
- if (!archived) return textResponse4(`Failed to archive product ${productId}.`);
115571
- return textResponse4(
115820
+ if (!archived) return textResponse5(`Failed to archive product ${productId}.`);
115821
+ return textResponse5(
115572
115822
  `\u2705 **Product archived** (hidden from new invoices/quotes; existing documents are untouched).
115573
115823
 
115574
115824
  ${formatProduct(archived)}${reason ? `Reason: ${reason}
@@ -115588,10 +115838,7 @@ function lineFinancials2(quantity, price, defaults) {
115588
115838
  };
115589
115839
  }
115590
115840
  function computeTotals(items, defaults) {
115591
- const subtotal = items.reduce(
115592
- (sum, i6) => sum + (i6.quantity || 0) * (i6.price || 0),
115593
- 0
115594
- );
115841
+ const subtotal = items.reduce((sum, i6) => sum + billableLineTotal(i6), 0);
115595
115842
  const vat = defaults.includeVat ? subtotal * (defaults.vatRate / 100) : 0;
115596
115843
  const tax = defaults.includeTax ? subtotal * (defaults.taxRate / 100) : 0;
115597
115844
  return {
@@ -115668,7 +115915,7 @@ var QUOTE_STATUSES = [
115668
115915
  "expired"
115669
115916
  ];
115670
115917
  var SAFE_DRAFT_STATUSES = /* @__PURE__ */ new Set(["draft"]);
115671
- function textResponse5(text3) {
115918
+ function textResponse6(text3) {
115672
115919
  return { content: [{ type: "text", text: text3 }] };
115673
115920
  }
115674
115921
  async function loadTemplateDefaults(teamId) {
@@ -115836,14 +116083,14 @@ function tiptapNote2(text3) {
115836
116083
  async function handleGetQuotes(input) {
115837
116084
  const { customerId, status, q: q3, pageSize = 20 } = input;
115838
116085
  if (status && !QUOTE_STATUSES.includes(status)) {
115839
- return textResponse5(
116086
+ return textResponse6(
115840
116087
  `Error: invalid status "${status}". Allowed: ${QUOTE_STATUSES.join(", ")}.`
115841
116088
  );
115842
116089
  }
115843
116090
  const scope = await resolveTeamScope(input.teamId);
115844
116091
  if (!scope.ok) return scope.response;
115845
116092
  if (scope.teamIds.length === 0) {
115846
- return textResponse5("No accessible teams found.");
116093
+ return textResponse6("No accessible teams found.");
115847
116094
  }
115848
116095
  const filters = [inArray(schema_exports.quotations.teamId, scope.teamIds)];
115849
116096
  if (customerId) filters.push(eq(schema_exports.quotations.customerId, customerId));
@@ -115858,10 +116105,10 @@ async function handleGetQuotes(input) {
115858
116105
  }
115859
116106
  const rows = await db.select(QUOTE_COLUMNS).from(schema_exports.quotations).where(and(...filters)).orderBy(desc(schema_exports.quotations.createdAt)).limit(Math.min(pageSize, 100));
115860
116107
  if (rows.length === 0) {
115861
- return textResponse5("No quotes found.");
116108
+ return textResponse6("No quotes found.");
115862
116109
  }
115863
116110
  const note = input.projectId ? "\nNote: `projectId` was ignored \u2014 quotations are not linked to projects." : "";
115864
- return textResponse5(
116111
+ return textResponse6(
115865
116112
  `Found ${rows.length} quote(s):
115866
116113
 
115867
116114
  ${rows.map(formatQuote).join("\n")}${note}`
@@ -115869,10 +116116,10 @@ ${rows.map(formatQuote).join("\n")}${note}`
115869
116116
  }
115870
116117
  async function handleCreateQuote(input) {
115871
116118
  const { customerId } = input;
115872
- if (!customerId) return textResponse5("Error: `customerId` is required.");
116119
+ if (!customerId) return textResponse6("Error: `customerId` is required.");
115873
116120
  const status = input.status ?? "draft";
115874
116121
  if (!SAFE_DRAFT_STATUSES.has(status)) {
115875
- return textResponse5(
116122
+ return textResponse6(
115876
116123
  `Error: this tool only creates draft quotes. Requested status "${status}" is not allowed. Sending/accepting a quote is a manual dashboard action.`
115877
116124
  );
115878
116125
  }
@@ -115895,7 +116142,7 @@ async function handleCreateQuote(input) {
115895
116142
  )
115896
116143
  ).limit(1);
115897
116144
  if (!customer) {
115898
- return textResponse5(
116145
+ return textResponse6(
115899
116146
  `Customer ${customerId} not found or not owned by this team.`
115900
116147
  );
115901
116148
  }
@@ -115905,7 +116152,7 @@ async function handleCreateQuote(input) {
115905
116152
  defaults,
115906
116153
  teamId
115907
116154
  );
115908
- if (error49) return textResponse5(`Error: ${error49}`);
116155
+ if (error49) return textResponse6(`Error: ${error49}`);
115909
116156
  const totals = computeTotals(items, defaults);
115910
116157
  const quotationNumber = await nextQuotationNumber(teamId);
115911
116158
  const template = buildQuoteTemplate(defaults, input.title);
@@ -115965,8 +116212,8 @@ async function handleCreateQuote(input) {
115965
116212
  tax: totals.tax,
115966
116213
  amount: totals.amount
115967
116214
  }).returning(QUOTE_COLUMNS);
115968
- if (!created) return textResponse5("Failed to create quote.");
115969
- return textResponse5(
116215
+ if (!created) return textResponse6("Failed to create quote.");
116216
+ return textResponse6(
115970
116217
  `\u2705 **Draft quote created**
115971
116218
 
115972
116219
  ${formatQuote(created)}
@@ -115984,15 +116231,15 @@ async function loadQuoteInTeam(id, teamId) {
115984
116231
  return row ?? null;
115985
116232
  }
115986
116233
  function notDraftResponse2(quote) {
115987
- return textResponse5(
116234
+ return textResponse6(
115988
116235
  `Quote ${quote.quotationNumber ?? quote.id} has status "${quote.status}", not "draft". These tools only modify draft quotes \u2014 sent/accepted/rejected/expired quotes are immutable here so their product snapshots stay reproducible.`
115989
116236
  );
115990
116237
  }
115991
116238
  async function handleUpdateQuote(input) {
115992
116239
  const { id } = input;
115993
- if (!id) return textResponse5("Error: `id` is required.");
116240
+ if (!id) return textResponse6("Error: `id` is required.");
115994
116241
  if (input.status !== void 0 && !SAFE_DRAFT_STATUSES.has(input.status)) {
115995
- return textResponse5(
116242
+ return textResponse6(
115996
116243
  `Error: status can only stay within {${[...SAFE_DRAFT_STATUSES].join(", ")}}. "${input.status}" (send/accept/reject/expire) must be done manually from the dashboard.`
115997
116244
  );
115998
116245
  }
@@ -116000,7 +116247,7 @@ async function handleUpdateQuote(input) {
116000
116247
  if (!resolved.ok) return resolved.response;
116001
116248
  const quote = await loadQuoteInTeam(id, resolved.teamId);
116002
116249
  if (!quote) {
116003
- return textResponse5(`Quote ${id} not found or not owned by this team.`);
116250
+ return textResponse6(`Quote ${id} not found or not owned by this team.`);
116004
116251
  }
116005
116252
  if (quote.status !== "draft") return notDraftResponse2(quote);
116006
116253
  const defaults = templateDefaultsFromStored(quote.template, quote.currency);
@@ -116020,7 +116267,7 @@ async function handleUpdateQuote(input) {
116020
116267
  defaults,
116021
116268
  quote.teamId
116022
116269
  );
116023
- if (error49) return textResponse5(`Error: ${error49}`);
116270
+ if (error49) return textResponse6(`Error: ${error49}`);
116024
116271
  const totals = computeTotals(items, defaults);
116025
116272
  updates.lineItems = items;
116026
116273
  updates.subtotal = totals.subtotal;
@@ -116029,32 +116276,32 @@ async function handleUpdateQuote(input) {
116029
116276
  updates.amount = totals.amount;
116030
116277
  }
116031
116278
  if (Object.keys(updates).length === 0) {
116032
- return textResponse5(
116279
+ return textResponse6(
116033
116280
  "No fields to update. Provide at least one of: title, description, validUntil, lineItems."
116034
116281
  );
116035
116282
  }
116036
116283
  updates.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
116037
116284
  const [updated] = await db.update(schema_exports.quotations).set(updates).where(eq(schema_exports.quotations.id, quote.id)).returning(QUOTE_COLUMNS);
116038
- if (!updated) return textResponse5(`Failed to update quote ${id}.`);
116039
- return textResponse5(`\u2705 **Draft quote updated**
116285
+ if (!updated) return textResponse6(`Failed to update quote ${id}.`);
116286
+ return textResponse6(`\u2705 **Draft quote updated**
116040
116287
 
116041
116288
  ${formatQuote(updated)}`);
116042
116289
  }
116043
116290
  async function handleAddProductToQuote(input) {
116044
116291
  const { quoteId, productId } = input;
116045
- if (!quoteId) return textResponse5("Error: `quoteId` is required.");
116046
- if (!productId) return textResponse5("Error: `productId` is required.");
116292
+ if (!quoteId) return textResponse6("Error: `quoteId` is required.");
116293
+ if (!productId) return textResponse6("Error: `productId` is required.");
116047
116294
  const resolved = await resolveTeamId(input.teamId);
116048
116295
  if (!resolved.ok) return resolved.response;
116049
116296
  const quote = await loadQuoteInTeam(quoteId, resolved.teamId);
116050
116297
  if (!quote) {
116051
- return textResponse5(`Quote ${quoteId} not found or not owned by this team.`);
116298
+ return textResponse6(`Quote ${quoteId} not found or not owned by this team.`);
116052
116299
  }
116053
116300
  if (quote.status !== "draft") return notDraftResponse2(quote);
116054
116301
  const products = await loadProductsInTeam2([productId], quote.teamId);
116055
116302
  const product = products.get(productId);
116056
116303
  if (!product) {
116057
- return textResponse5(
116304
+ return textResponse6(
116058
116305
  `Product ${productId} not found or not owned by this team.`
116059
116306
  );
116060
116307
  }
@@ -116080,7 +116327,7 @@ async function handleAddProductToQuote(input) {
116080
116327
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
116081
116328
  }).where(eq(schema_exports.quotations.id, quote.id)).returning(QUOTE_COLUMNS);
116082
116329
  if (!updated) {
116083
- return textResponse5(`Failed to add product to quote ${quoteId}.`);
116330
+ return textResponse6(`Failed to add product to quote ${quoteId}.`);
116084
116331
  }
116085
116332
  await db.update(schema_exports.invoiceProducts).set({
116086
116333
  usageCount: sql`${schema_exports.invoiceProducts.usageCount} + 1`,
@@ -116096,7 +116343,7 @@ async function handleAddProductToQuote(input) {
116096
116343
  if (meta5.includedItems && meta5.includedItems.length > 0) {
116097
116344
  metaParts.push(`included=[${meta5.includedItems.join(", ")}]`);
116098
116345
  }
116099
- return textResponse5(
116346
+ return textResponse6(
116100
116347
  `\u2705 **Product added to draft quote ${updated.quotationNumber ?? updated.id}**
116101
116348
 
116102
116349
  Line item: ${newItem.name} \xD7 ${newItem.quantity}${newItem.unit ? ` ${newItem.unit}` : ""} @ ${newItem.price} ${snap.currency}
@@ -121831,7 +122078,7 @@ function formatDeleteAttachmentRefusal(reason, context2) {
121831
122078
  }
121832
122079
 
121833
122080
  // src/tools/ticket-attachments.ts
121834
- function textResponse6(text3) {
122081
+ function textResponse7(text3) {
121835
122082
  return { content: [{ type: "text", text: text3 }] };
121836
122083
  }
121837
122084
  async function findAttachment(attachmentId) {
@@ -121904,7 +122151,7 @@ ${url3}`
121904
122151
  async function handleUploadTicketAttachment(input) {
121905
122152
  const ctx = getAuthContext() ?? authContext;
121906
122153
  if (!ctx) {
121907
- return textResponse6("Error: Not authenticated.");
122154
+ return textResponse7("Error: Not authenticated.");
121908
122155
  }
121909
122156
  const access = await loadAccessibleTicket(input.teamId, input.ticketId);
121910
122157
  if (!access.ok) return access.response;
@@ -121920,12 +122167,12 @@ async function handleUploadTicketAttachment(input) {
121920
122167
  userId: ctx.userId
121921
122168
  });
121922
122169
  if (!resolved.ok) {
121923
- return textResponse6(resolved.message);
122170
+ return textResponse7(resolved.message);
121924
122171
  }
121925
122172
  const { buffer: buffer2, fileName, mimeType, stagingStorageKey } = resolved;
121926
122173
  const validationError = validateAttachmentBuffer(buffer2, mimeType);
121927
122174
  if (validationError) {
121928
- return textResponse6(validationError.message);
122175
+ return textResponse7(validationError.message);
121929
122176
  }
121930
122177
  const storageKey = `${ticket.teamId}/tickets/${ticket.id}/${Date.now()}_${fileName}`;
121931
122178
  try {
@@ -121936,7 +122183,7 @@ async function handleUploadTicketAttachment(input) {
121936
122183
  options: { contentType: mimeType, upsert: true }
121937
122184
  });
121938
122185
  } catch (error49) {
121939
- return textResponse6(
122186
+ return textResponse7(
121940
122187
  `Upload failed: ${error49 instanceof Error ? error49.message : String(error49)}`
121941
122188
  );
121942
122189
  }
@@ -121965,7 +122212,7 @@ async function handleUploadTicketAttachment(input) {
121965
122212
  url3 = signed.url;
121966
122213
  } catch {
121967
122214
  }
121968
- return textResponse6(
122215
+ return textResponse7(
121969
122216
  `\u{1F4CE} **Attached to ${ticket.ticketNumber}**
121970
122217
  File: ${fileName}
121971
122218
  Type: ${mimeType}
@@ -121979,18 +122226,18 @@ ${url3}` : "")
121979
122226
  async function handleDeleteTicketAttachment(input) {
121980
122227
  const ctx = getAuthContext() ?? authContext;
121981
122228
  if (!ctx) {
121982
- return textResponse6("Error: Not authenticated.");
122229
+ return textResponse7("Error: Not authenticated.");
121983
122230
  }
121984
122231
  const inputError = validateDeleteAttachmentInput(input.attachmentId);
121985
122232
  if (inputError) {
121986
- return textResponse6(formatDeleteAttachmentRefusal(inputError, { ticketNumber: input.ticketId }));
122233
+ return textResponse7(formatDeleteAttachmentRefusal(inputError, { ticketNumber: input.ticketId }));
121987
122234
  }
121988
122235
  const access = await loadAccessibleTicket(input.teamId, input.ticketId);
121989
122236
  if (!access.ok) return access.response;
121990
122237
  const ticket = access.ticket;
121991
122238
  const attachment = await findAttachment(input.attachmentId);
121992
122239
  if (!attachment) {
121993
- return textResponse6(
122240
+ return textResponse7(
121994
122241
  formatDeleteAttachmentRefusal("attachment_not_found", {
121995
122242
  attachmentId: input.attachmentId,
121996
122243
  ticketNumber: ticket.ticketNumber
@@ -121998,7 +122245,7 @@ async function handleDeleteTicketAttachment(input) {
121998
122245
  );
121999
122246
  }
122000
122247
  if (!validateAttachmentBelongsToTicket(attachment.ticketId, ticket.id)) {
122001
- return textResponse6(
122248
+ return textResponse7(
122002
122249
  formatDeleteAttachmentRefusal("wrong_ticket", {
122003
122250
  attachmentId: input.attachmentId,
122004
122251
  ticketNumber: ticket.ticketNumber,
@@ -122010,7 +122257,7 @@ async function handleDeleteTicketAttachment(input) {
122010
122257
  const table = attachment.source === "ticket" ? schema_exports.ticketAttachments : schema_exports.ticketCommentAttachments;
122011
122258
  const [deletedRow] = await db.delete(table).where(eq(table.id, input.attachmentId)).returning({ id: table.id });
122012
122259
  if (!deletedRow) {
122013
- return textResponse6(
122260
+ return textResponse7(
122014
122261
  `Failed to delete attachment ${input.attachmentId}. It may have been removed already.`
122015
122262
  );
122016
122263
  }
@@ -122040,7 +122287,7 @@ async function handleDeleteTicketAttachment(input) {
122040
122287
  fileName: attachment.fileName,
122041
122288
  source: attachment.source
122042
122289
  });
122043
- return textResponse6(JSON.stringify(result, null, 2));
122290
+ return textResponse7(JSON.stringify(result, null, 2));
122044
122291
  }
122045
122292
 
122046
122293
  // src/tools/tiptap-text.ts
@@ -122457,7 +122704,7 @@ function formatTagUsage(usage) {
122457
122704
  }
122458
122705
 
122459
122706
  // src/tools/tag-management.ts
122460
- function textResponse7(text3) {
122707
+ function textResponse8(text3) {
122461
122708
  return { content: [{ type: "text", text: text3 }] };
122462
122709
  }
122463
122710
  var TAG_COLUMNS = {
@@ -122498,24 +122745,24 @@ function scopeFilter(projectId) {
122498
122745
  return projectId === null ? isNull(schema_exports.tags.projectId) : eq(schema_exports.tags.projectId, projectId);
122499
122746
  }
122500
122747
  async function handleUpdateTag(input) {
122501
- if (!input.tagId) return textResponse7("Error: `tagId` is required.");
122748
+ if (!input.tagId) return textResponse8("Error: `tagId` is required.");
122502
122749
  const resolved = await resolveTeamId(input.teamId);
122503
122750
  if (!resolved.ok) return resolved.response;
122504
122751
  const existing = await loadTagInTeam(input.tagId, resolved.teamId);
122505
122752
  if (!existing) {
122506
- return textResponse7(
122753
+ return textResponse8(
122507
122754
  `Tag ${input.tagId} not found, or it is not owned by this team.`
122508
122755
  );
122509
122756
  }
122510
122757
  const renaming = input.name !== void 0;
122511
122758
  const rescoping = input.projectId !== void 0;
122512
122759
  if (!renaming && !rescoping) {
122513
- return textResponse7(
122760
+ return textResponse8(
122514
122761
  "No changes requested. Provide `name` to rename and/or `projectId` (string, or null for a general tag) to change scope."
122515
122762
  );
122516
122763
  }
122517
122764
  if (renaming && !isValidTagName(input.name)) {
122518
- return textResponse7("Error: `name` cannot be empty.");
122765
+ return textResponse8("Error: `name` cannot be empty.");
122519
122766
  }
122520
122767
  const nextName = renaming ? input.name.trim() : existing.name;
122521
122768
  const nextProjectId = rescoping ? input.projectId ?? null : existing.projectId;
@@ -122528,13 +122775,13 @@ async function handleUpdateTag(input) {
122528
122775
  )
122529
122776
  ).limit(1);
122530
122777
  if (collision) {
122531
- return textResponse7(
122778
+ return textResponse8(
122532
122779
  `\u274C Cannot update: another tag already uses the name "${collision.name}" (id: ${collision.id}) in this scope. Use merge-tags to combine them instead of renaming.`
122533
122780
  );
122534
122781
  }
122535
122782
  const [updated] = await db.update(schema_exports.tags).set({ name: nextName, projectId: nextProjectId }).where(eq(schema_exports.tags.id, existing.id)).returning(TAG_COLUMNS);
122536
- if (!updated) return textResponse7(`Failed to update tag ${input.tagId}.`);
122537
- return textResponse7(
122783
+ if (!updated) return textResponse8(`Failed to update tag ${input.tagId}.`);
122784
+ return textResponse8(
122538
122785
  `\u2705 **Tag updated**
122539
122786
 
122540
122787
  ${describeTag(updated)}
@@ -122543,34 +122790,34 @@ Existing ticket/customer/project/transaction tag relations are preserved.`
122543
122790
  );
122544
122791
  }
122545
122792
  async function handleDeleteTag(input) {
122546
- if (!input.tagId) return textResponse7("Error: `tagId` is required.");
122793
+ if (!input.tagId) return textResponse8("Error: `tagId` is required.");
122547
122794
  const mode = input.mode ?? "delete_if_unused";
122548
122795
  const resolved = await resolveTeamId(input.teamId);
122549
122796
  if (!resolved.ok) return resolved.response;
122550
122797
  const existing = await loadTagInTeam(input.tagId, resolved.teamId);
122551
122798
  if (!existing) {
122552
- return textResponse7(
122799
+ return textResponse8(
122553
122800
  `Tag ${input.tagId} not found, or it is not owned by this team.`
122554
122801
  );
122555
122802
  }
122556
122803
  const usage = await getTagUsage(existing.id);
122557
122804
  const total = totalTagUsage(usage);
122558
122805
  if (mode === "archive") {
122559
- return textResponse7(
122806
+ return textResponse8(
122560
122807
  `\u2139\uFE0F Archiving is not supported for team tags: the \`tags\` table has no archived column. ${describeTag(existing)} is used by ${formatTagUsage(usage)}.
122561
122808
 
122562
122809
  Options: use merge-tags to fold it into another tag, or delete it once it is unused (mode: delete_if_unused).`
122563
122810
  );
122564
122811
  }
122565
122812
  if (total > 0) {
122566
- return textResponse7(
122813
+ return textResponse8(
122567
122814
  `\u274C Refusing to delete ${describeTag(existing)}: it is still used by ${formatTagUsage(usage)}. Deleting would strip the tag off those entities.
122568
122815
 
122569
122816
  Use merge-tags to move usage onto another tag first, then delete the (now-empty) tag.`
122570
122817
  );
122571
122818
  }
122572
122819
  await db.delete(schema_exports.tags).where(eq(schema_exports.tags.id, existing.id));
122573
- return textResponse7(
122820
+ return textResponse8(
122574
122821
  `\u2705 **Tag deleted** (was unused): ${describeTag(existing)}`
122575
122822
  );
122576
122823
  }
@@ -122580,7 +122827,7 @@ async function resolveMergeTarget(teamId, input) {
122580
122827
  if (!tag) {
122581
122828
  return {
122582
122829
  ok: false,
122583
- response: textResponse7(
122830
+ response: textResponse8(
122584
122831
  `Target tag ${input.targetTagId} not found, or it is not owned by this team.`
122585
122832
  )
122586
122833
  };
@@ -122590,7 +122837,7 @@ async function resolveMergeTarget(teamId, input) {
122590
122837
  if (!isValidTagName(input.targetName)) {
122591
122838
  return {
122592
122839
  ok: false,
122593
- response: textResponse7(
122840
+ response: textResponse8(
122594
122841
  "Error: provide either `targetTagId` or a non-empty `targetName`."
122595
122842
  )
122596
122843
  };
@@ -122608,14 +122855,14 @@ async function resolveMergeTarget(teamId, input) {
122608
122855
  }
122609
122856
  const [created] = await db.insert(schema_exports.tags).values({ teamId, name: input.targetName.trim(), projectId: null }).returning(TAG_COLUMNS);
122610
122857
  if (!created) {
122611
- return { ok: false, response: textResponse7("Failed to create target tag.") };
122858
+ return { ok: false, response: textResponse8("Failed to create target tag.") };
122612
122859
  }
122613
122860
  return { ok: true, tag: created, created: true };
122614
122861
  }
122615
122862
  async function handleMergeTags(input) {
122616
122863
  const rawSourceIds = [...new Set(input.sourceTagIds ?? [])].filter(Boolean);
122617
122864
  if (rawSourceIds.length === 0) {
122618
- return textResponse7("Error: `sourceTagIds` must contain at least one tag id.");
122865
+ return textResponse8("Error: `sourceTagIds` must contain at least one tag id.");
122619
122866
  }
122620
122867
  const resolved = await resolveTeamId(input.teamId);
122621
122868
  if (!resolved.ok) return resolved.response;
@@ -122629,7 +122876,7 @@ async function handleMergeTags(input) {
122629
122876
  const foundIds = new Set(sourceTags.map((t8) => t8.id));
122630
122877
  const missing = rawSourceIds.filter((id) => !foundIds.has(id));
122631
122878
  if (missing.length > 0) {
122632
- return textResponse7(
122879
+ return textResponse8(
122633
122880
  `Error: source tag(s) not found or not owned by this team: ${missing.join(", ")}.`
122634
122881
  );
122635
122882
  }
@@ -122637,7 +122884,7 @@ async function handleMergeTags(input) {
122637
122884
  if (!target.ok) return target.response;
122638
122885
  const sourcesToMerge = sourceTags.filter((t8) => t8.id !== target.tag.id);
122639
122886
  if (sourcesToMerge.length === 0) {
122640
- return textResponse7(
122887
+ return textResponse8(
122641
122888
  "Error: nothing to merge \u2014 the only source tag is the same as the target tag."
122642
122889
  );
122643
122890
  }
@@ -122734,7 +122981,7 @@ async function handleMergeTags(input) {
122734
122981
  const movedTotal = results.tickets.moved + results.customers.moved + results.projects.moved + results.transactions.moved;
122735
122982
  const skippedTotal = results.tickets.skipped + results.customers.skipped + results.projects.skipped + results.transactions.skipped;
122736
122983
  const line2 = (label, r6) => `- ${label}: ${r6.moved} moved, ${r6.skipped} skipped (duplicate)`;
122737
- return textResponse7(
122984
+ return textResponse8(
122738
122985
  `\u2705 **Tags merged** into ${describeTag(target.tag)}${target.created ? " (newly created)" : ""}
122739
122986
 
122740
122987
  Sources (${sourcesToMerge.length}): ${sourcesToMerge.map((t8) => `${t8.name} (${t8.id})`).join(", ")}
@@ -123120,7 +123367,7 @@ function attemptedLockedFields(update) {
123120
123367
  // src/tools/trips.ts
123121
123368
  var TRIP_TYPES = ["private", "business"];
123122
123369
  var BILLING_TYPES2 = TRIP_BILLING_TYPES;
123123
- function textResponse8(text3) {
123370
+ function textResponse9(text3) {
123124
123371
  return { content: [{ type: "text", text: text3 }] };
123125
123372
  }
123126
123373
  function jsonResponse(payload) {
@@ -123176,19 +123423,19 @@ var TRIP_RELATIONS = {
123176
123423
  };
123177
123424
  async function handleGetTrips(input) {
123178
123425
  if (input.tripType && !TRIP_TYPES.includes(input.tripType)) {
123179
- return textResponse8(
123426
+ return textResponse9(
123180
123427
  `Error: invalid tripType "${input.tripType}". Allowed: ${TRIP_TYPES.join(", ")}.`
123181
123428
  );
123182
123429
  }
123183
123430
  if (input.billingType && !BILLING_TYPES2.includes(input.billingType)) {
123184
- return textResponse8(
123431
+ return textResponse9(
123185
123432
  `Error: invalid billingType "${input.billingType}". Allowed: ${BILLING_TYPES2.join(", ")}.`
123186
123433
  );
123187
123434
  }
123188
123435
  const scope = await resolveTeamScope(input.teamId);
123189
123436
  if (!scope.ok) return scope.response;
123190
123437
  if (scope.teamIds.length === 0) {
123191
- return textResponse8("No accessible teams found.");
123438
+ return textResponse9("No accessible teams found.");
123192
123439
  }
123193
123440
  const filters = [inArray(schema_exports.trips.teamId, scope.teamIds)];
123194
123441
  if (input.dateFrom) filters.push(gte(schema_exports.trips.date, input.dateFrom));
@@ -123290,20 +123537,20 @@ async function validateInvoice(invoiceId, teamId) {
123290
123537
  }
123291
123538
  async function handleCreateTrip(input) {
123292
123539
  const ctx = getAuthContext();
123293
- if (!input.date) return textResponse8("Error: `date` (YYYY-MM-DD) is required.");
123540
+ if (!input.date) return textResponse9("Error: `date` (YYYY-MM-DD) is required.");
123294
123541
  if (!input.startLocation || !input.endLocation) {
123295
- return textResponse8(
123542
+ return textResponse9(
123296
123543
  "Error: `startLocation` and `endLocation` are required."
123297
123544
  );
123298
123545
  }
123299
123546
  if (!input.tripType || !TRIP_TYPES.includes(input.tripType)) {
123300
- return textResponse8(
123547
+ return textResponse9(
123301
123548
  `Error: \`tripType\` is required and must be one of: ${TRIP_TYPES.join(", ")}.`
123302
123549
  );
123303
123550
  }
123304
123551
  const billingType = input.billingType ?? "not_billable";
123305
123552
  if (!BILLING_TYPES2.includes(billingType)) {
123306
- return textResponse8(
123553
+ return textResponse9(
123307
123554
  `Error: invalid billingType "${input.billingType}". Allowed: ${BILLING_TYPES2.join(", ")}.`
123308
123555
  );
123309
123556
  }
@@ -123315,7 +123562,7 @@ async function handleCreateTrip(input) {
123315
123562
  customerId: input.customerId,
123316
123563
  vehicleId: input.vehicleId
123317
123564
  });
123318
- if (linkError) return textResponse8(`Error: ${linkError}`);
123565
+ if (linkError) return textResponse9(`Error: ${linkError}`);
123319
123566
  if (!input.allowDuplicate) {
123320
123567
  const dupFilters = [
123321
123568
  eq(schema_exports.trips.teamId, teamId),
@@ -123332,7 +123579,7 @@ async function handleCreateTrip(input) {
123332
123579
  }
123333
123580
  const [dup] = await db.select({ id: schema_exports.trips.id, distance: schema_exports.trips.distance }).from(schema_exports.trips).where(and(...dupFilters)).limit(1);
123334
123581
  if (dup) {
123335
- return textResponse8(
123582
+ return textResponse9(
123336
123583
  `\u26A0\uFE0F A matching trip already exists for ${input.date} (${input.startLocation} \u2192 ${input.endLocation}): trip ${dup.id}${dup.distance != null ? ` (${toNumber2(dup.distance)} km)` : ""}. Not creating a duplicate. Use update-trip to adjust it, or re-call create-trip with allowDuplicate: true to record a second trip anyway.`
123337
123584
  );
123338
123585
  }
@@ -123362,7 +123609,7 @@ async function handleCreateTrip(input) {
123362
123609
  vehicleId: input.vehicleId ?? null,
123363
123610
  snapshotId: input.snapshotId ?? null
123364
123611
  }).returning({ id: schema_exports.trips.id });
123365
- if (!created) return textResponse8("Failed to create trip.");
123612
+ if (!created) return textResponse9("Failed to create trip.");
123366
123613
  const trip = await loadTripInTeams(created.id, [teamId]);
123367
123614
  return {
123368
123615
  content: [
@@ -123377,14 +123624,14 @@ ${JSON.stringify(formatTrip(trip), null, 2)}`
123377
123624
  }
123378
123625
  async function handleUpdateTrip(input) {
123379
123626
  const ctx = getAuthContext();
123380
- if (!input.id) return textResponse8("Error: `id` is required.");
123627
+ if (!input.id) return textResponse9("Error: `id` is required.");
123381
123628
  if (input.tripType && !TRIP_TYPES.includes(input.tripType)) {
123382
- return textResponse8(
123629
+ return textResponse9(
123383
123630
  `Error: invalid tripType "${input.tripType}". Allowed: ${TRIP_TYPES.join(", ")}.`
123384
123631
  );
123385
123632
  }
123386
123633
  if (input.billingType && !BILLING_TYPES2.includes(input.billingType)) {
123387
- return textResponse8(
123634
+ return textResponse9(
123388
123635
  `Error: invalid billingType "${input.billingType}". Allowed: ${BILLING_TYPES2.join(", ")}.`
123389
123636
  );
123390
123637
  }
@@ -123393,7 +123640,7 @@ async function handleUpdateTrip(input) {
123393
123640
  const accessibleTeamIds = await getAccessibleTeamIds(resolved.teamId);
123394
123641
  const existing = await loadTripInTeams(input.id, accessibleTeamIds);
123395
123642
  if (!existing) {
123396
- return textResponse8(
123643
+ return textResponse9(
123397
123644
  `Trip ${input.id} not found or you don't have access to it. Call get-trips to find a valid id.`
123398
123645
  );
123399
123646
  }
@@ -123404,7 +123651,7 @@ async function handleUpdateTrip(input) {
123404
123651
  input
123405
123652
  );
123406
123653
  if (attempted.length > 0) {
123407
- return textResponse8(
123654
+ return textResponse9(
123408
123655
  `Error: trip ${input.id} is invoiced${existing.invoiceId ? ` (invoice ${existing.invoiceId})` : ""}. Financial/distance fields are locked: ${attempted.join(", ")}. Re-call with allowInvoicedOverride: true to change them anyway, or only update project/customer/notes/vehicle links.`
123409
123656
  );
123410
123657
  }
@@ -123414,10 +123661,10 @@ async function handleUpdateTrip(input) {
123414
123661
  customerId: input.customerId ?? void 0,
123415
123662
  vehicleId: input.vehicleId ?? void 0
123416
123663
  });
123417
- if (linkError) return textResponse8(`Error: ${linkError}`);
123664
+ if (linkError) return textResponse9(`Error: ${linkError}`);
123418
123665
  if (input.invoiceId) {
123419
123666
  const invoiceError = await validateInvoice(input.invoiceId, teamId);
123420
- if (invoiceError) return textResponse8(`Error: ${invoiceError}`);
123667
+ if (invoiceError) return textResponse9(`Error: ${invoiceError}`);
123421
123668
  }
123422
123669
  const updates = {};
123423
123670
  if (input.date !== void 0) updates.date = input.date;
@@ -123465,7 +123712,7 @@ async function handleUpdateTrip(input) {
123465
123712
  if (derived != null) updates.amount = derived;
123466
123713
  }
123467
123714
  if (Object.keys(updates).length === 0) {
123468
- return textResponse8(
123715
+ return textResponse9(
123469
123716
  "No fields to update. Provide at least one editable field."
123470
123717
  );
123471
123718
  }
@@ -123492,7 +123739,7 @@ async function handleGetVehicles(input) {
123492
123739
  const scope = await resolveTeamScope(input.teamId);
123493
123740
  if (!scope.ok) return scope.response;
123494
123741
  if (scope.teamIds.length === 0) {
123495
- return textResponse8("No accessible teams found.");
123742
+ return textResponse9("No accessible teams found.");
123496
123743
  }
123497
123744
  const filters = [inArray(schema_exports.vehicles.teamId, scope.teamIds)];
123498
123745
  if (input.q) filters.push(ilike(schema_exports.vehicles.name, `%${input.q}%`));
@@ -123518,7 +123765,7 @@ async function handleGetTripTemplates(input) {
123518
123765
  const scope = await resolveTeamScope(input.teamId);
123519
123766
  if (!scope.ok) return scope.response;
123520
123767
  if (scope.teamIds.length === 0) {
123521
- return textResponse8("No accessible teams found.");
123768
+ return textResponse9("No accessible teams found.");
123522
123769
  }
123523
123770
  const filters = [inArray(schema_exports.tripTemplates.teamId, scope.teamIds)];
123524
123771
  const userId = input.userId ?? ctx.userId;
@@ -123554,14 +123801,14 @@ async function handleGetTripTemplates(input) {
123554
123801
  async function handleGetFrequentTripsForProject(input) {
123555
123802
  const ctx = getAuthContext();
123556
123803
  if (!input.projectId) {
123557
- return textResponse8("Error: `projectId` is required.");
123804
+ return textResponse9("Error: `projectId` is required.");
123558
123805
  }
123559
123806
  const resolved = await resolveTeamId(input.teamId);
123560
123807
  if (!resolved.ok) return resolved.response;
123561
123808
  const teamId = resolved.teamId;
123562
123809
  const projectIds = await getAccessibleProjectIds(ctx.userId, teamId);
123563
123810
  if (!projectIds.includes(input.projectId)) {
123564
- return textResponse8(
123811
+ return textResponse9(
123565
123812
  `Project not found or no access: ${input.projectId}. Call get-projects first.`
123566
123813
  );
123567
123814
  }
@@ -124224,6 +124471,18 @@ function createMcpServer() {
124224
124471
  return await handleArchiveProduct(
124225
124472
  asToolArgs(toolArgs)
124226
124473
  );
124474
+ case "get-customer-agreements":
124475
+ return await handleGetCustomerAgreements(
124476
+ asToolArgs(toolArgs)
124477
+ );
124478
+ case "create-customer-agreement":
124479
+ return await handleCreateCustomerAgreement(
124480
+ asToolArgs(toolArgs)
124481
+ );
124482
+ case "update-customer-agreement":
124483
+ return await handleUpdateCustomerAgreement(
124484
+ asToolArgs(toolArgs)
124485
+ );
124227
124486
  case "get-trips":
124228
124487
  return await handleGetTrips(asToolArgs(toolArgs));
124229
124488
  case "create-trip":