@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 +589 -330
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
-
|
|
113708
|
-
|
|
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/
|
|
113789
|
-
function
|
|
113790
|
-
|
|
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
|
|
113795
|
-
|
|
113796
|
-
const
|
|
113797
|
-
|
|
113798
|
-
|
|
113799
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
114590
|
+
return textResponse3("No accessible teams found.");
|
|
114341
114591
|
}
|
|
114342
114592
|
const invoice = await loadInvoiceByIdentifier(invoiceId, scope.teamIds);
|
|
114343
114593
|
if (!invoice) {
|
|
114344
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
114435
|
-
return
|
|
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
|
|
114691
|
+
if (!invoiceId) return textResponse3("Error: `invoiceId` is required.");
|
|
114442
114692
|
if (!patches || patches.length === 0) {
|
|
114443
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
114507
|
-
if (!productId) return
|
|
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
|
|
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
|
|
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
|
|
114802
|
+
return textResponse3(`Failed to add product to invoice ${invoiceId}.`);
|
|
114553
114803
|
}
|
|
114554
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 :
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
115678
|
+
return textResponse5(
|
|
115429
115679
|
`No products found${status !== "all" ? ` (status: ${status})` : ""}.`
|
|
115430
115680
|
);
|
|
115431
115681
|
}
|
|
115432
|
-
return
|
|
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
|
|
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
|
|
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
|
|
115703
|
+
return textResponse5(
|
|
115454
115704
|
`Product ${productId} not found or you don't have access to it.`
|
|
115455
115705
|
);
|
|
115456
115706
|
}
|
|
115457
|
-
return
|
|
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
|
|
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
|
|
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
|
|
115496
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
115546
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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
|
|
115571
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
115969
|
-
return
|
|
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
|
|
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
|
|
116240
|
+
if (!id) return textResponse6("Error: `id` is required.");
|
|
115994
116241
|
if (input.status !== void 0 && !SAFE_DRAFT_STATUSES.has(input.status)) {
|
|
115995
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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
|
|
116039
|
-
return
|
|
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
|
|
116046
|
-
if (!productId) return
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
122229
|
+
return textResponse7("Error: Not authenticated.");
|
|
121983
122230
|
}
|
|
121984
122231
|
const inputError = validateDeleteAttachmentInput(input.attachmentId);
|
|
121985
122232
|
if (inputError) {
|
|
121986
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
122537
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
123540
|
+
if (!input.date) return textResponse9("Error: `date` (YYYY-MM-DD) is required.");
|
|
123294
123541
|
if (!input.startLocation || !input.endLocation) {
|
|
123295
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
123627
|
+
if (!input.id) return textResponse9("Error: `id` is required.");
|
|
123381
123628
|
if (input.tripType && !TRIP_TYPES.includes(input.tripType)) {
|
|
123382
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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":
|