@armor/zuora-mcp 1.0.1 → 1.2.0

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/tools.js CHANGED
@@ -3,6 +3,7 @@
3
3
  * Provides account, invoice, subscription, payment, and ZOQL query tools
4
4
  */
5
5
  import { z } from "zod";
6
+ import { queryAll, escapeZoql, todayString, addDays, subtractDays, daysBetween, getString, getNumber, collectIds, queryWithBatchedIds, } from "./zoql-helpers.js";
6
7
  // ==================== Helpers ====================
7
8
  function isValidCalendarDate(dateStr) {
8
9
  const [year, month, day] = dateStr.split("-").map(Number);
@@ -217,6 +218,73 @@ export const schemas = {
217
218
  .default(20)
218
219
  .describe("Records per page (default: 20, max: 40)"),
219
220
  }),
221
+ // User Management Tools
222
+ listUsers: z.object({
223
+ startIndex: z
224
+ .number()
225
+ .int()
226
+ .min(1)
227
+ .default(1)
228
+ .describe("1-based start index for SCIM pagination (default: 1)"),
229
+ count: z
230
+ .number()
231
+ .int()
232
+ .min(1)
233
+ .max(100)
234
+ .default(100)
235
+ .describe("Number of users to return per page (default: 100, max: 100)"),
236
+ filter: z
237
+ .string()
238
+ .max(500)
239
+ .optional()
240
+ .describe("SCIM filter string (optional). " +
241
+ "Examples: status eq \"Active\", userName eq \"user@example.com\""),
242
+ }),
243
+ getUser: z.object({
244
+ userId: z
245
+ .string()
246
+ .uuid("User ID must be a valid UUID")
247
+ .describe("Zuora user ID (UUID format)"),
248
+ }),
249
+ // Bill Run Read Tools
250
+ getBillRun: z.object({
251
+ billRunId: z
252
+ .string()
253
+ .min(1)
254
+ .describe("Bill run ID (e.g., 2c92c0f8...)"),
255
+ }),
256
+ listBillRuns: z.object({
257
+ page: z
258
+ .number()
259
+ .int()
260
+ .min(1)
261
+ .default(1)
262
+ .describe("Page number (default: 1)"),
263
+ pageSize: z
264
+ .number()
265
+ .int()
266
+ .min(1)
267
+ .max(40)
268
+ .default(20)
269
+ .describe("Records per page (default: 20, max: 40)"),
270
+ }),
271
+ // Contact Read Tools
272
+ getContact: z.object({
273
+ contactId: z
274
+ .string()
275
+ .min(1)
276
+ .describe("Contact ID (e.g., 2c92c0f8...)"),
277
+ }),
278
+ // Describe API
279
+ describeObject: z.object({
280
+ objectType: z
281
+ .string()
282
+ .min(1)
283
+ .max(100)
284
+ .regex(/^[A-Z][a-zA-Z]*$/, "Object type must be PascalCase (e.g., Account, Invoice, Subscription)")
285
+ .describe("Zuora object type name in PascalCase (e.g., Account, Invoice, Subscription, " +
286
+ "Payment, RatePlan, RatePlanCharge, Product, ProductRatePlan, BillRun, Contact)"),
287
+ }),
220
288
  // Phase 3: Write Operations
221
289
  createPayment: z.object({
222
290
  accountId: z
@@ -596,6 +664,255 @@ export const schemas = {
596
664
  .describe("UUID to prevent duplicate refunds on retries. " +
597
665
  "Generate a new UUID v4 for each distinct refund intent."),
598
666
  }),
667
+ // Bill Run Write Tools
668
+ createBillRun: z.object({
669
+ targetDate: z
670
+ .string()
671
+ .regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be YYYY-MM-DD format")
672
+ .refine(isValidCalendarDate, "Must be a valid calendar date")
673
+ .describe("Target date for the bill run in YYYY-MM-DD format. Invoices are generated for charges through this date."),
674
+ invoiceDate: z
675
+ .string()
676
+ .regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be YYYY-MM-DD format")
677
+ .refine(isValidCalendarDate, "Must be a valid calendar date")
678
+ .optional()
679
+ .describe("Invoice date in YYYY-MM-DD format (optional, defaults to targetDate)"),
680
+ autoPost: z
681
+ .boolean()
682
+ .default(false)
683
+ .describe("Automatically post invoices after generation (default: false)"),
684
+ autoEmail: z
685
+ .boolean()
686
+ .default(false)
687
+ .describe("Automatically email invoices after posting (default: false). Requires autoPost to be true."),
688
+ name: z
689
+ .string()
690
+ .max(255)
691
+ .optional()
692
+ .describe("Optional name/label for the bill run"),
693
+ idempotencyKey: z
694
+ .string()
695
+ .uuid("Idempotency key must be a valid UUID")
696
+ .describe("UUID to prevent duplicate bill runs on retries. " +
697
+ "Generate a new UUID v4 for each distinct bill run intent."),
698
+ }).superRefine((data, ctx) => {
699
+ if (data.autoEmail && !data.autoPost) {
700
+ ctx.addIssue({
701
+ code: z.ZodIssueCode.custom,
702
+ path: ["autoEmail"],
703
+ message: "autoEmail requires autoPost to be true — invoices must be posted before they can be emailed",
704
+ });
705
+ }
706
+ }),
707
+ // Contact Write Tools
708
+ createContact: z.object({
709
+ accountId: z
710
+ .string()
711
+ .min(1)
712
+ .describe("Zuora account UUID to create the contact for"),
713
+ firstName: z
714
+ .string()
715
+ .min(1)
716
+ .max(100)
717
+ .describe("Contact first name"),
718
+ lastName: z
719
+ .string()
720
+ .min(1)
721
+ .max(100)
722
+ .describe("Contact last name"),
723
+ workEmail: z
724
+ .string()
725
+ .email("Must be a valid email address")
726
+ .optional()
727
+ .describe("Work email address"),
728
+ personalEmail: z
729
+ .string()
730
+ .email("Must be a valid email address")
731
+ .optional()
732
+ .describe("Personal email address"),
733
+ workPhone: z.string().max(40).optional().describe("Work phone number"),
734
+ homePhone: z.string().max(40).optional().describe("Home phone number"),
735
+ mobilePhone: z.string().max(40).optional().describe("Mobile phone number"),
736
+ fax: z.string().max(40).optional().describe("Fax number"),
737
+ address1: z.string().max(255).optional().describe("Street address line 1"),
738
+ address2: z.string().max(255).optional().describe("Street address line 2"),
739
+ city: z.string().max(100).optional().describe("City"),
740
+ state: z.string().max(100).optional().describe("State or province"),
741
+ country: z.string().max(100).optional().describe("Country (ISO 3166 name or code)"),
742
+ postalCode: z.string().max(20).optional().describe("Postal/ZIP code"),
743
+ county: z.string().max(100).optional().describe("County"),
744
+ taxRegion: z.string().max(100).optional().describe("Tax region"),
745
+ description: z.string().max(2000).optional().describe("Contact description/notes"),
746
+ nickname: z.string().max(100).optional().describe("Contact nickname"),
747
+ idempotencyKey: z
748
+ .string()
749
+ .uuid("Idempotency key must be a valid UUID")
750
+ .describe("UUID to prevent duplicate contact creation on retries. " +
751
+ "Generate a new UUID v4 for each distinct contact intent."),
752
+ }),
753
+ updateContact: z.object({
754
+ contactId: z
755
+ .string()
756
+ .min(1)
757
+ .describe("Contact ID to update"),
758
+ firstName: z.string().min(1).max(100).optional().describe("Updated first name"),
759
+ lastName: z.string().min(1).max(100).optional().describe("Updated last name"),
760
+ workEmail: z.string().email("Must be a valid email address").optional().describe("Updated work email"),
761
+ personalEmail: z.string().email("Must be a valid email address").optional().describe("Updated personal email"),
762
+ workPhone: z.string().max(40).optional().describe("Updated work phone"),
763
+ homePhone: z.string().max(40).optional().describe("Updated home phone"),
764
+ mobilePhone: z.string().max(40).optional().describe("Updated mobile phone"),
765
+ fax: z.string().max(40).optional().describe("Updated fax number"),
766
+ address1: z.string().max(255).optional().describe("Updated street address line 1"),
767
+ address2: z.string().max(255).optional().describe("Updated street address line 2"),
768
+ city: z.string().max(100).optional().describe("Updated city"),
769
+ state: z.string().max(100).optional().describe("Updated state or province"),
770
+ country: z.string().max(100).optional().describe("Updated country"),
771
+ postalCode: z.string().max(20).optional().describe("Updated postal/ZIP code"),
772
+ county: z.string().max(100).optional().describe("Updated county"),
773
+ taxRegion: z.string().max(100).optional().describe("Updated tax region"),
774
+ description: z.string().max(2000).optional().describe("Updated description/notes"),
775
+ nickname: z.string().max(100).optional().describe("Updated nickname"),
776
+ }).superRefine((data, ctx) => {
777
+ const { contactId: _, ...updateFields } = data;
778
+ const hasUpdate = Object.values(updateFields).some((v) => v !== undefined);
779
+ if (!hasUpdate) {
780
+ ctx.addIssue({
781
+ code: z.ZodIssueCode.custom,
782
+ path: [],
783
+ message: "At least one field to update must be provided",
784
+ });
785
+ }
786
+ }),
787
+ // ==================== Composite Tool Schemas ====================
788
+ findAccountsByProduct: z.object({
789
+ productName: z
790
+ .string()
791
+ .min(1)
792
+ .max(255)
793
+ .describe("Product name to search for (partial match supported). " +
794
+ "Example: 'Security Analytics Log Retention 13 Months'"),
795
+ limit: z
796
+ .number()
797
+ .int()
798
+ .min(1)
799
+ .max(100)
800
+ .default(25)
801
+ .describe("Maximum accounts to return (default: 25, max: 100)"),
802
+ }),
803
+ getOverdueInvoices: z.object({
804
+ minBalance: z
805
+ .number()
806
+ .min(0)
807
+ .default(0)
808
+ .describe("Minimum invoice balance to include (default: 0)"),
809
+ limit: z
810
+ .number()
811
+ .int()
812
+ .min(1)
813
+ .max(200)
814
+ .default(50)
815
+ .describe("Maximum invoices to return (default: 50, max: 200)"),
816
+ }),
817
+ getExpiringSubscriptions: z.object({
818
+ daysAhead: z
819
+ .number()
820
+ .int()
821
+ .min(1)
822
+ .max(365)
823
+ .default(30)
824
+ .describe("Days to look ahead for expiring subscriptions (default: 30, max: 365)"),
825
+ limit: z
826
+ .number()
827
+ .int()
828
+ .min(1)
829
+ .max(200)
830
+ .default(50)
831
+ .describe("Maximum subscriptions to return (default: 50, max: 200)"),
832
+ }),
833
+ getAccountBillingOverview: z.object({
834
+ accountKey: z
835
+ .string()
836
+ .min(1)
837
+ .describe("Account number or ID (e.g., A00012345)"),
838
+ }),
839
+ getRevenueByProduct: z.object({
840
+ limit: z
841
+ .number()
842
+ .int()
843
+ .min(1)
844
+ .max(100)
845
+ .default(50)
846
+ .describe("Maximum products to return (default: 50, max: 100)"),
847
+ }),
848
+ getPaymentReconciliation: z.object({
849
+ startDate: z
850
+ .string()
851
+ .regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be YYYY-MM-DD format")
852
+ .refine(isValidCalendarDate, "Must be a valid calendar date")
853
+ .describe("Start date for payment period (YYYY-MM-DD)"),
854
+ endDate: z
855
+ .string()
856
+ .regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be YYYY-MM-DD format")
857
+ .refine(isValidCalendarDate, "Must be a valid calendar date")
858
+ .describe("End date for payment period (YYYY-MM-DD)"),
859
+ limit: z
860
+ .number()
861
+ .int()
862
+ .min(1)
863
+ .max(500)
864
+ .default(100)
865
+ .describe("Maximum payments to return (default: 100, max: 500)"),
866
+ }),
867
+ getRecentlyCancelledSubscriptions: z.object({
868
+ daysBack: z
869
+ .number()
870
+ .int()
871
+ .min(1)
872
+ .max(365)
873
+ .default(30)
874
+ .describe("Days to look back for cancellations (default: 30, max: 365)"),
875
+ limit: z
876
+ .number()
877
+ .int()
878
+ .min(1)
879
+ .max(200)
880
+ .default(50)
881
+ .describe("Maximum subscriptions to return (default: 50, max: 200)"),
882
+ }),
883
+ getInvoiceAgingReport: z.object({
884
+ limit: z
885
+ .number()
886
+ .int()
887
+ .min(1)
888
+ .max(500)
889
+ .default(200)
890
+ .describe("Maximum invoices to analyze (default: 200, max: 500)"),
891
+ }),
892
+ getAccountHealthScorecard: z.object({
893
+ limit: z
894
+ .number()
895
+ .int()
896
+ .min(1)
897
+ .max(100)
898
+ .default(20)
899
+ .describe("Maximum at-risk accounts to return (default: 20, max: 100)"),
900
+ }),
901
+ findInvoicesByProduct: z.object({
902
+ productName: z
903
+ .string()
904
+ .min(1)
905
+ .max(255)
906
+ .describe("Product name to search for (partial match supported). " +
907
+ "Example: 'Security Analytics'"),
908
+ limit: z
909
+ .number()
910
+ .int()
911
+ .min(1)
912
+ .max(200)
913
+ .default(50)
914
+ .describe("Maximum invoice items to return (default: 50, max: 200)"),
915
+ }),
599
916
  };
600
917
  // ==================== Tool Handlers ====================
601
918
  export class ToolHandlers {
@@ -902,6 +1219,115 @@ export class ToolHandlers {
902
1219
  };
903
1220
  }
904
1221
  }
1222
+ // --- User Management Handlers ---
1223
+ async listUsers(input) {
1224
+ try {
1225
+ const { startIndex, count, filter } = schemas.listUsers.parse(input);
1226
+ const result = await this.client.listUsers(startIndex, count, filter);
1227
+ const userCount = result.Resources?.length ?? 0;
1228
+ const filterNote = filter ? ` (filter: ${filter})` : "";
1229
+ return {
1230
+ success: true,
1231
+ message: `Found ${userCount} user(s) of ${result.totalResults} total${filterNote}`,
1232
+ data: result,
1233
+ };
1234
+ }
1235
+ catch (error) {
1236
+ return {
1237
+ success: false,
1238
+ message: `Failed to list users: ${error instanceof Error ? error.message : String(error)}`,
1239
+ };
1240
+ }
1241
+ }
1242
+ async getUser(input) {
1243
+ try {
1244
+ const { userId } = schemas.getUser.parse(input);
1245
+ const user = await this.client.getUser(userId);
1246
+ return {
1247
+ success: true,
1248
+ message: `Retrieved user ${user.userName ?? userId}`,
1249
+ data: user,
1250
+ };
1251
+ }
1252
+ catch (error) {
1253
+ return {
1254
+ success: false,
1255
+ message: `Failed to get user: ${error instanceof Error ? error.message : String(error)}`,
1256
+ };
1257
+ }
1258
+ }
1259
+ // --- Bill Run Handlers ---
1260
+ async getBillRun(input) {
1261
+ try {
1262
+ const { billRunId } = schemas.getBillRun.parse(input);
1263
+ const billRun = await this.client.getBillRun(billRunId);
1264
+ return {
1265
+ success: true,
1266
+ message: `Retrieved bill run ${billRun.billRunNumber ?? billRunId} (status: ${billRun.status})`,
1267
+ data: billRun,
1268
+ };
1269
+ }
1270
+ catch (error) {
1271
+ return {
1272
+ success: false,
1273
+ message: `Failed to get bill run: ${error instanceof Error ? error.message : String(error)}`,
1274
+ };
1275
+ }
1276
+ }
1277
+ async listBillRuns(input) {
1278
+ try {
1279
+ const { page, pageSize } = schemas.listBillRuns.parse(input);
1280
+ const result = await this.client.listBillRuns(page, pageSize);
1281
+ const count = result.billRuns?.length ?? 0;
1282
+ return {
1283
+ success: true,
1284
+ message: `Found ${count} bill run(s)`,
1285
+ data: result,
1286
+ };
1287
+ }
1288
+ catch (error) {
1289
+ return {
1290
+ success: false,
1291
+ message: `Failed to list bill runs: ${error instanceof Error ? error.message : String(error)}`,
1292
+ };
1293
+ }
1294
+ }
1295
+ // --- Contact Handlers ---
1296
+ async getContact(input) {
1297
+ try {
1298
+ const { contactId } = schemas.getContact.parse(input);
1299
+ const contact = await this.client.getContact(contactId);
1300
+ return {
1301
+ success: true,
1302
+ message: `Retrieved contact ${contact.firstName} ${contact.lastName} (${contactId})`,
1303
+ data: contact,
1304
+ };
1305
+ }
1306
+ catch (error) {
1307
+ return {
1308
+ success: false,
1309
+ message: `Failed to get contact: ${error instanceof Error ? error.message : String(error)}`,
1310
+ };
1311
+ }
1312
+ }
1313
+ // --- Describe API Handlers ---
1314
+ async describeObject(input) {
1315
+ try {
1316
+ const { objectType } = schemas.describeObject.parse(input);
1317
+ const result = await this.client.describeObject(objectType);
1318
+ return {
1319
+ success: true,
1320
+ message: `Described ${result.objectName}: ${result.fieldCount} field(s)`,
1321
+ data: result,
1322
+ };
1323
+ }
1324
+ catch (error) {
1325
+ return {
1326
+ success: false,
1327
+ message: `Failed to describe object: ${error instanceof Error ? error.message : String(error)}`,
1328
+ };
1329
+ }
1330
+ }
905
1331
  // --- Phase 3: Write Operation Handlers ---
906
1332
  async createPayment(input) {
907
1333
  try {
@@ -1077,6 +1503,909 @@ export class ToolHandlers {
1077
1503
  };
1078
1504
  }
1079
1505
  }
1506
+ // --- Bill Run Write Handlers ---
1507
+ async createBillRun(input) {
1508
+ try {
1509
+ const { idempotencyKey, ...billRunData } = schemas.createBillRun.parse(input);
1510
+ const result = await this.client.createBillRun(billRunData, idempotencyKey);
1511
+ return {
1512
+ success: true,
1513
+ message: `Created bill run ${result.billRunNumber} (status: ${result.status})`,
1514
+ data: result,
1515
+ };
1516
+ }
1517
+ catch (error) {
1518
+ return {
1519
+ success: false,
1520
+ message: `Failed to create bill run: ${error instanceof Error ? error.message : String(error)}`,
1521
+ };
1522
+ }
1523
+ }
1524
+ // --- Contact Write Handlers ---
1525
+ async createContact(input) {
1526
+ try {
1527
+ const { idempotencyKey, ...contactData } = schemas.createContact.parse(input);
1528
+ const result = await this.client.createContact(contactData, idempotencyKey);
1529
+ return {
1530
+ success: true,
1531
+ message: `Created contact (id: ${result.id}) for account ${contactData.accountId}`,
1532
+ data: result,
1533
+ };
1534
+ }
1535
+ catch (error) {
1536
+ return {
1537
+ success: false,
1538
+ message: `Failed to create contact: ${error instanceof Error ? error.message : String(error)}`,
1539
+ };
1540
+ }
1541
+ }
1542
+ async updateContact(input) {
1543
+ try {
1544
+ const { contactId, ...updateData } = schemas.updateContact.parse(input);
1545
+ const result = await this.client.updateContact(contactId, updateData);
1546
+ return {
1547
+ success: true,
1548
+ message: `Updated contact ${contactId} successfully`,
1549
+ data: result,
1550
+ };
1551
+ }
1552
+ catch (error) {
1553
+ return {
1554
+ success: false,
1555
+ message: `Failed to update contact: ${error instanceof Error ? error.message : String(error)}`,
1556
+ };
1557
+ }
1558
+ }
1559
+ // --- Composite Tool Handlers ---
1560
+ async findAccountsByProduct(input) {
1561
+ try {
1562
+ const { productName, limit } = schemas.findAccountsByProduct.parse(input);
1563
+ const safeName = escapeZoql(productName);
1564
+ // Step 1: Find ProductRatePlan IDs matching the product name
1565
+ const productRatePlans = await queryAll(this.client, `SELECT Id, Name FROM ProductRatePlan WHERE Name LIKE '%${safeName}%'`);
1566
+ if (productRatePlans.length === 0) {
1567
+ // Try searching by Product.Name instead
1568
+ const products = await queryAll(this.client, `SELECT Id, Name FROM Product WHERE Name LIKE '%${safeName}%'`);
1569
+ if (products.length === 0) {
1570
+ return {
1571
+ success: true,
1572
+ message: `No products found matching "${productName}"`,
1573
+ data: { accounts: [], totalFound: 0 },
1574
+ };
1575
+ }
1576
+ const productIds = collectIds(products, "Id");
1577
+ const prpFromProduct = await queryWithBatchedIds(this.client, "Id, Name", "ProductRatePlan", "ProductId", productIds);
1578
+ if (prpFromProduct.length === 0) {
1579
+ return {
1580
+ success: true,
1581
+ message: `Product found but no rate plans configured for "${productName}"`,
1582
+ data: { accounts: [], totalFound: 0 },
1583
+ };
1584
+ }
1585
+ productRatePlans.push(...prpFromProduct);
1586
+ }
1587
+ const prpIds = collectIds(productRatePlans, "Id");
1588
+ // Step 2: Find RatePlans using those ProductRatePlanIds → get SubscriptionIds
1589
+ const ratePlans = await queryWithBatchedIds(this.client, "Id, SubscriptionId, Name", "RatePlan", "ProductRatePlanId", prpIds);
1590
+ if (ratePlans.length === 0) {
1591
+ return {
1592
+ success: true,
1593
+ message: `No active subscriptions found for product "${productName}"`,
1594
+ data: { accounts: [], totalFound: 0 },
1595
+ };
1596
+ }
1597
+ const subscriptionIds = collectIds(ratePlans, "SubscriptionId");
1598
+ // Step 3: Get Subscriptions → AccountIds
1599
+ const subscriptions = await queryWithBatchedIds(this.client, "Id, AccountId, SubscriptionNumber, Status, TermEndDate, ContractedMrr", "Subscription", "Id", subscriptionIds, "Status = 'Active'");
1600
+ if (subscriptions.length === 0) {
1601
+ return {
1602
+ success: true,
1603
+ message: `No active subscriptions found for product "${productName}"`,
1604
+ data: { accounts: [], totalFound: 0 },
1605
+ };
1606
+ }
1607
+ const accountIds = collectIds(subscriptions, "AccountId");
1608
+ // Step 4: Get Account details
1609
+ const accounts = await queryWithBatchedIds(this.client, "Id, AccountNumber, Name, Status", "Account", "Id", accountIds);
1610
+ // Build lookup maps
1611
+ const accountMap = new Map(accounts.map((a) => [getString(a, "Id"), a]));
1612
+ const subsByAccount = new Map();
1613
+ for (const sub of subscriptions) {
1614
+ const acctId = getString(sub, "AccountId") ?? "";
1615
+ const existing = subsByAccount.get(acctId) ?? [];
1616
+ existing.push(sub);
1617
+ subsByAccount.set(acctId, existing);
1618
+ }
1619
+ // Build rate plan name lookup by subscription
1620
+ const rpNameBySub = new Map();
1621
+ for (const rp of ratePlans) {
1622
+ const subId = getString(rp, "SubscriptionId") ?? "";
1623
+ const existing = rpNameBySub.get(subId) ?? [];
1624
+ const name = getString(rp, "Name");
1625
+ if (name)
1626
+ existing.push(name);
1627
+ rpNameBySub.set(subId, existing);
1628
+ }
1629
+ // Assemble results
1630
+ const results = [];
1631
+ for (const [acctId, acct] of accountMap) {
1632
+ if (!acctId)
1633
+ continue;
1634
+ const acctSubs = subsByAccount.get(acctId) ?? [];
1635
+ results.push({
1636
+ accountId: acctId,
1637
+ accountNumber: getString(acct, "AccountNumber") ?? "",
1638
+ accountName: getString(acct, "Name") ?? "",
1639
+ accountStatus: getString(acct, "Status") ?? "",
1640
+ subscriptions: acctSubs.map((sub) => ({
1641
+ subscriptionId: getString(sub, "Id") ?? "",
1642
+ subscriptionNumber: getString(sub, "SubscriptionNumber") ?? "",
1643
+ status: getString(sub, "Status") ?? "",
1644
+ termEndDate: getString(sub, "TermEndDate"),
1645
+ ratePlanName: (rpNameBySub.get(getString(sub, "Id") ?? "") ?? []).join(", "),
1646
+ })),
1647
+ });
1648
+ if (results.length >= limit)
1649
+ break;
1650
+ }
1651
+ return {
1652
+ success: true,
1653
+ message: `Found ${results.length} account(s) with active subscriptions for "${productName}"`,
1654
+ data: { accounts: results, totalFound: results.length },
1655
+ };
1656
+ }
1657
+ catch (error) {
1658
+ return {
1659
+ success: false,
1660
+ message: `Failed to find accounts by product: ${error instanceof Error ? error.message : String(error)}`,
1661
+ };
1662
+ }
1663
+ }
1664
+ async getOverdueInvoices(input) {
1665
+ try {
1666
+ const { minBalance, limit } = schemas.getOverdueInvoices.parse(input);
1667
+ const today = todayString();
1668
+ // Query overdue invoices: Posted, has balance, past due date
1669
+ const invoices = await queryAll(this.client, `SELECT Id, InvoiceNumber, AccountId, InvoiceDate, DueDate, Amount, Balance ` +
1670
+ `FROM Invoice ` +
1671
+ `WHERE Status = 'Posted' AND Balance > ${minBalance} AND DueDate < '${today}'`, limit);
1672
+ if (invoices.length === 0) {
1673
+ return {
1674
+ success: true,
1675
+ message: "No overdue invoices found",
1676
+ data: { invoices: [], totalFound: 0, totalOverdueBalance: 0 },
1677
+ };
1678
+ }
1679
+ // Get account details for the overdue invoices
1680
+ const accountIds = collectIds(invoices, "AccountId");
1681
+ const accounts = await queryWithBatchedIds(this.client, "Id, AccountNumber, Name", "Account", "Id", accountIds);
1682
+ const accountMap = new Map(accounts.map((a) => [getString(a, "Id"), a]));
1683
+ // Build results
1684
+ const results = invoices.map((inv) => {
1685
+ const acctId = getString(inv, "AccountId") ?? "";
1686
+ const acct = accountMap.get(acctId);
1687
+ const dueDate = getString(inv, "DueDate") ?? today;
1688
+ return {
1689
+ invoiceId: getString(inv, "Id") ?? "",
1690
+ invoiceNumber: getString(inv, "InvoiceNumber") ?? "",
1691
+ accountId: acctId,
1692
+ accountNumber: getString(acct ?? {}, "AccountNumber") ?? "",
1693
+ accountName: getString(acct ?? {}, "Name") ?? "",
1694
+ invoiceDate: getString(inv, "InvoiceDate") ?? "",
1695
+ dueDate,
1696
+ amount: getNumber(inv, "Amount") ?? 0,
1697
+ balance: getNumber(inv, "Balance") ?? 0,
1698
+ daysPastDue: daysBetween(dueDate, today),
1699
+ };
1700
+ });
1701
+ // Sort by days past due descending
1702
+ results.sort((a, b) => b.daysPastDue - a.daysPastDue);
1703
+ const totalOverdueBalance = results.reduce((sum, r) => sum + r.balance, 0);
1704
+ return {
1705
+ success: true,
1706
+ message: `Found ${results.length} overdue invoice(s) totaling $${totalOverdueBalance.toFixed(2)}`,
1707
+ data: {
1708
+ invoices: results.slice(0, limit),
1709
+ totalFound: results.length,
1710
+ totalOverdueBalance,
1711
+ },
1712
+ };
1713
+ }
1714
+ catch (error) {
1715
+ return {
1716
+ success: false,
1717
+ message: `Failed to get overdue invoices: ${error instanceof Error ? error.message : String(error)}`,
1718
+ };
1719
+ }
1720
+ }
1721
+ async getExpiringSubscriptions(input) {
1722
+ try {
1723
+ const { daysAhead, limit } = schemas.getExpiringSubscriptions.parse(input);
1724
+ const today = todayString();
1725
+ const futureDate = addDays(new Date(), daysAhead);
1726
+ // Query active subscriptions expiring within the window
1727
+ const subscriptions = await queryAll(this.client, `SELECT Id, SubscriptionNumber, AccountId, Status, TermEndDate, AutoRenew, ContractedMrr ` +
1728
+ `FROM Subscription ` +
1729
+ `WHERE Status = 'Active' AND TermEndDate >= '${today}' AND TermEndDate <= '${futureDate}'`, limit * 2 // fetch extra to account for filtering
1730
+ );
1731
+ if (subscriptions.length === 0) {
1732
+ return {
1733
+ success: true,
1734
+ message: `No subscriptions expiring in the next ${daysAhead} days`,
1735
+ data: { subscriptions: [], totalFound: 0, totalMrrAtRisk: 0 },
1736
+ };
1737
+ }
1738
+ // Get account details
1739
+ const accountIds = collectIds(subscriptions, "AccountId");
1740
+ const accounts = await queryWithBatchedIds(this.client, "Id, AccountNumber, Name", "Account", "Id", accountIds);
1741
+ const accountMap = new Map(accounts.map((a) => [getString(a, "Id"), a]));
1742
+ // Get rate plan names
1743
+ const subIds = collectIds(subscriptions, "Id");
1744
+ const ratePlans = await queryWithBatchedIds(this.client, "Id, SubscriptionId, Name", "RatePlan", "SubscriptionId", subIds);
1745
+ const rpNamesBySub = new Map();
1746
+ for (const rp of ratePlans) {
1747
+ const subId = getString(rp, "SubscriptionId") ?? "";
1748
+ const existing = rpNamesBySub.get(subId) ?? [];
1749
+ const name = getString(rp, "Name");
1750
+ if (name)
1751
+ existing.push(name);
1752
+ rpNamesBySub.set(subId, existing);
1753
+ }
1754
+ // Build results
1755
+ const results = subscriptions.map((sub) => {
1756
+ const acctId = getString(sub, "AccountId") ?? "";
1757
+ const acct = accountMap.get(acctId);
1758
+ const termEnd = getString(sub, "TermEndDate") ?? futureDate;
1759
+ const subId = getString(sub, "Id") ?? "";
1760
+ return {
1761
+ subscriptionId: subId,
1762
+ subscriptionNumber: getString(sub, "SubscriptionNumber") ?? "",
1763
+ accountId: acctId,
1764
+ accountNumber: getString(acct ?? {}, "AccountNumber") ?? "",
1765
+ accountName: getString(acct ?? {}, "Name") ?? "",
1766
+ status: getString(sub, "Status") ?? "",
1767
+ termEndDate: termEnd,
1768
+ autoRenew: sub.AutoRenew === true,
1769
+ contractedMrr: getNumber(sub, "ContractedMrr") ?? 0,
1770
+ ratePlanNames: rpNamesBySub.get(subId) ?? [],
1771
+ daysUntilExpiry: daysBetween(today, termEnd),
1772
+ };
1773
+ });
1774
+ results.sort((a, b) => a.daysUntilExpiry - b.daysUntilExpiry);
1775
+ const totalMrrAtRisk = results
1776
+ .filter((r) => !r.autoRenew)
1777
+ .reduce((sum, r) => sum + r.contractedMrr, 0);
1778
+ return {
1779
+ success: true,
1780
+ message: `Found ${results.length} subscription(s) expiring in the next ${daysAhead} days (MRR at risk: $${totalMrrAtRisk.toFixed(2)})`,
1781
+ data: {
1782
+ subscriptions: results.slice(0, limit),
1783
+ totalFound: results.length,
1784
+ totalMrrAtRisk,
1785
+ },
1786
+ };
1787
+ }
1788
+ catch (error) {
1789
+ return {
1790
+ success: false,
1791
+ message: `Failed to get expiring subscriptions: ${error instanceof Error ? error.message : String(error)}`,
1792
+ };
1793
+ }
1794
+ }
1795
+ async getAccountBillingOverview(input) {
1796
+ try {
1797
+ const { accountKey } = schemas.getAccountBillingOverview.parse(input);
1798
+ // Step 1: Get account via REST API
1799
+ const account = await this.client.getAccount(accountKey);
1800
+ // Step 2: Get recent invoices via ZOQL
1801
+ const invoices = await queryAll(this.client, `SELECT Id, InvoiceNumber, InvoiceDate, DueDate, Amount, Balance, Status ` +
1802
+ `FROM Invoice WHERE AccountId = '${escapeZoql(account.id)}' ` +
1803
+ `ORDER BY InvoiceDate DESC`, 20);
1804
+ // Step 3: Get recent payments via ZOQL
1805
+ const payments = await queryAll(this.client, `SELECT Id, PaymentNumber, Amount, EffectiveDate, Status ` +
1806
+ `FROM Payment WHERE AccountId = '${escapeZoql(account.id)}' ` +
1807
+ `ORDER BY EffectiveDate DESC`, 10);
1808
+ // Step 4: Get subscriptions via REST API
1809
+ const subResult = await this.client.listSubscriptionsByAccount(accountKey);
1810
+ const subscriptions = subResult.subscriptions ?? [];
1811
+ // Compute invoice summary
1812
+ const today = todayString();
1813
+ let totalOutstanding = 0;
1814
+ let overdueCount = 0;
1815
+ let overdueAmount = 0;
1816
+ for (const inv of invoices) {
1817
+ const balance = getNumber(inv, "Balance") ?? 0;
1818
+ const status = getString(inv, "Status");
1819
+ if (status === "Posted" && balance > 0) {
1820
+ totalOutstanding += balance;
1821
+ const dueDate = getString(inv, "DueDate") ?? "";
1822
+ if (dueDate && dueDate < today) {
1823
+ overdueCount++;
1824
+ overdueAmount += balance;
1825
+ }
1826
+ }
1827
+ }
1828
+ const activeSubscriptions = subscriptions.filter((s) => s.status === "Active");
1829
+ const totalMrr = activeSubscriptions.reduce((sum, s) => sum + (s.contractedMrr ?? 0), 0);
1830
+ const overview = {
1831
+ account: {
1832
+ id: account.id,
1833
+ accountNumber: account.accountNumber,
1834
+ name: account.name,
1835
+ status: account.status,
1836
+ balance: account.balance,
1837
+ currency: account.currency,
1838
+ autoPay: account.autoPay,
1839
+ paymentTerm: account.paymentTerm,
1840
+ },
1841
+ invoiceSummary: {
1842
+ totalOutstanding,
1843
+ overdueCount,
1844
+ overdueAmount,
1845
+ recentInvoices: invoices.slice(0, 10).map((inv) => ({
1846
+ invoiceNumber: getString(inv, "InvoiceNumber") ?? "",
1847
+ invoiceDate: getString(inv, "InvoiceDate") ?? "",
1848
+ dueDate: getString(inv, "DueDate") ?? "",
1849
+ amount: getNumber(inv, "Amount") ?? 0,
1850
+ balance: getNumber(inv, "Balance") ?? 0,
1851
+ status: getString(inv, "Status") ?? "",
1852
+ })),
1853
+ },
1854
+ paymentSummary: {
1855
+ recentPayments: payments.slice(0, 5).map((p) => ({
1856
+ paymentNumber: getString(p, "PaymentNumber") ?? "",
1857
+ amount: getNumber(p, "Amount") ?? 0,
1858
+ effectiveDate: getString(p, "EffectiveDate") ?? "",
1859
+ status: getString(p, "Status") ?? "",
1860
+ })),
1861
+ },
1862
+ subscriptionSummary: {
1863
+ activeCount: activeSubscriptions.length,
1864
+ totalMrr,
1865
+ subscriptions: activeSubscriptions.slice(0, 10).map((s) => ({
1866
+ subscriptionNumber: s.subscriptionNumber,
1867
+ status: s.status,
1868
+ termEndDate: s.termEndDate,
1869
+ contractedMrr: s.contractedMrr,
1870
+ ratePlanNames: s.ratePlans?.map((rp) => rp.ratePlanName) ?? [],
1871
+ })),
1872
+ },
1873
+ };
1874
+ return {
1875
+ success: true,
1876
+ message: `Billing overview for ${account.name} (${account.accountNumber}): balance $${account.balance}, ${overdueCount} overdue, ${activeSubscriptions.length} active subscriptions`,
1877
+ data: overview,
1878
+ };
1879
+ }
1880
+ catch (error) {
1881
+ return {
1882
+ success: false,
1883
+ message: `Failed to get billing overview: ${error instanceof Error ? error.message : String(error)}`,
1884
+ };
1885
+ }
1886
+ }
1887
+ async getRevenueByProduct(input) {
1888
+ try {
1889
+ const { limit } = schemas.getRevenueByProduct.parse(input);
1890
+ // Step 1: Get all active subscriptions with MRR
1891
+ const subscriptions = await queryAll(this.client, `SELECT Id, AccountId, SubscriptionNumber, ContractedMrr, Status ` +
1892
+ `FROM Subscription WHERE Status = 'Active'`, 5000);
1893
+ if (subscriptions.length === 0) {
1894
+ return {
1895
+ success: true,
1896
+ message: "No active subscriptions found",
1897
+ data: { products: [], totalMrr: 0 },
1898
+ };
1899
+ }
1900
+ // Step 2: Get rate plans for subscriptions to resolve product names
1901
+ const subIds = collectIds(subscriptions, "Id");
1902
+ const ratePlans = await queryWithBatchedIds(this.client, "Id, SubscriptionId, ProductRatePlanId, Name", "RatePlan", "SubscriptionId", subIds);
1903
+ // Step 3: Get ProductRatePlan → Product mapping
1904
+ const prpIds = collectIds(ratePlans, "ProductRatePlanId");
1905
+ const productRatePlans = await queryWithBatchedIds(this.client, "Id, ProductId, Name", "ProductRatePlan", "Id", prpIds);
1906
+ const productIds = collectIds(productRatePlans, "ProductId");
1907
+ const products = await queryWithBatchedIds(this.client, "Id, Name", "Product", "Id", productIds);
1908
+ // Build lookup maps
1909
+ const productNameMap = new Map(products.map((p) => [getString(p, "Id"), getString(p, "Name") ?? "Unknown"]));
1910
+ const prpToProduct = new Map(productRatePlans.map((prp) => [
1911
+ getString(prp, "Id"),
1912
+ productNameMap.get(getString(prp, "ProductId") ?? "") ?? getString(prp, "Name") ?? "Unknown",
1913
+ ]));
1914
+ // Map subscriptions to their rate plans and product names
1915
+ const subMap = new Map(subscriptions.map((s) => [getString(s, "Id"), s]));
1916
+ // Get account details for all subscriptions
1917
+ const accountIds = collectIds(subscriptions, "AccountId");
1918
+ const accounts = await queryWithBatchedIds(this.client, "Id, AccountNumber, Name", "Account", "Id", accountIds);
1919
+ const accountMap = new Map(accounts.map((a) => [getString(a, "Id"), a]));
1920
+ // Group by product name
1921
+ const productData = new Map();
1922
+ for (const rp of ratePlans) {
1923
+ const prpId = getString(rp, "ProductRatePlanId") ?? "";
1924
+ const productName = prpToProduct.get(prpId) ?? getString(rp, "Name") ?? "Unknown";
1925
+ const subId = getString(rp, "SubscriptionId") ?? "";
1926
+ const sub = subMap.get(subId);
1927
+ if (!sub)
1928
+ continue;
1929
+ const acctId = getString(sub, "AccountId") ?? "";
1930
+ const acct = accountMap.get(acctId);
1931
+ const mrr = getNumber(sub, "ContractedMrr") ?? 0;
1932
+ const existing = productData.get(productName) ?? {
1933
+ subscriptionCount: 0,
1934
+ totalMrr: 0,
1935
+ totalTcv: 0,
1936
+ subscriptions: [],
1937
+ };
1938
+ existing.subscriptionCount++;
1939
+ existing.totalMrr += mrr;
1940
+ existing.subscriptions.push({
1941
+ subscriptionNumber: getString(sub, "SubscriptionNumber") ?? "",
1942
+ accountNumber: getString(acct ?? {}, "AccountNumber") ?? "",
1943
+ accountName: getString(acct ?? {}, "Name") ?? "",
1944
+ mrr,
1945
+ status: getString(sub, "Status") ?? "",
1946
+ });
1947
+ productData.set(productName, existing);
1948
+ }
1949
+ // Convert to sorted array
1950
+ const results = [...productData.entries()]
1951
+ .map(([name, data]) => ({
1952
+ productName: name,
1953
+ subscriptionCount: data.subscriptionCount,
1954
+ totalMrr: data.totalMrr,
1955
+ totalTcv: data.totalTcv,
1956
+ subscriptions: data.subscriptions,
1957
+ }))
1958
+ .sort((a, b) => b.totalMrr - a.totalMrr)
1959
+ .slice(0, limit);
1960
+ const totalMrr = results.reduce((sum, r) => sum + r.totalMrr, 0);
1961
+ return {
1962
+ success: true,
1963
+ message: `Revenue breakdown across ${results.length} product(s), total MRR: $${totalMrr.toFixed(2)}`,
1964
+ data: { products: results, totalMrr },
1965
+ };
1966
+ }
1967
+ catch (error) {
1968
+ return {
1969
+ success: false,
1970
+ message: `Failed to get revenue by product: ${error instanceof Error ? error.message : String(error)}`,
1971
+ };
1972
+ }
1973
+ }
1974
+ async getPaymentReconciliation(input) {
1975
+ try {
1976
+ const { startDate, endDate, limit } = schemas.getPaymentReconciliation.parse(input);
1977
+ // Query payments in the date range
1978
+ const payments = await queryAll(this.client, `SELECT Id, PaymentNumber, AccountId, Amount, EffectiveDate, Status, PaymentMethodType ` +
1979
+ `FROM Payment ` +
1980
+ `WHERE EffectiveDate >= '${escapeZoql(startDate)}' AND EffectiveDate <= '${escapeZoql(endDate)}'`, limit);
1981
+ if (payments.length === 0) {
1982
+ return {
1983
+ success: true,
1984
+ message: `No payments found between ${startDate} and ${endDate}`,
1985
+ data: {
1986
+ period: { startDate, endDate },
1987
+ summary: { totalPayments: 0, totalAmount: 0, byStatus: {} },
1988
+ payments: [],
1989
+ },
1990
+ };
1991
+ }
1992
+ // Get account details
1993
+ const accountIds = collectIds(payments, "AccountId");
1994
+ const accounts = await queryWithBatchedIds(this.client, "Id, AccountNumber, Name", "Account", "Id", accountIds);
1995
+ const accountMap = new Map(accounts.map((a) => [getString(a, "Id"), a]));
1996
+ // Build summary
1997
+ const byStatus = {};
1998
+ let totalAmount = 0;
1999
+ const paymentResults = payments.map((p) => {
2000
+ const amount = getNumber(p, "Amount") ?? 0;
2001
+ const status = getString(p, "Status") ?? "Unknown";
2002
+ const acctId = getString(p, "AccountId") ?? "";
2003
+ const acct = accountMap.get(acctId);
2004
+ totalAmount += amount;
2005
+ const statusBucket = byStatus[status] ?? { count: 0, amount: 0 };
2006
+ statusBucket.count++;
2007
+ statusBucket.amount += amount;
2008
+ byStatus[status] = statusBucket;
2009
+ return {
2010
+ paymentNumber: getString(p, "PaymentNumber") ?? "",
2011
+ accountNumber: getString(acct ?? {}, "AccountNumber") ?? "",
2012
+ accountName: getString(acct ?? {}, "Name") ?? "",
2013
+ amount,
2014
+ effectiveDate: getString(p, "EffectiveDate") ?? "",
2015
+ status,
2016
+ paymentMethodType: getString(p, "PaymentMethodType") ?? "",
2017
+ };
2018
+ });
2019
+ const result = {
2020
+ period: { startDate, endDate },
2021
+ summary: {
2022
+ totalPayments: payments.length,
2023
+ totalAmount,
2024
+ byStatus,
2025
+ },
2026
+ payments: paymentResults,
2027
+ };
2028
+ return {
2029
+ success: true,
2030
+ message: `${payments.length} payment(s) totaling $${totalAmount.toFixed(2)} between ${startDate} and ${endDate}`,
2031
+ data: result,
2032
+ };
2033
+ }
2034
+ catch (error) {
2035
+ return {
2036
+ success: false,
2037
+ message: `Failed to get payment reconciliation: ${error instanceof Error ? error.message : String(error)}`,
2038
+ };
2039
+ }
2040
+ }
2041
+ async getRecentlyCancelledSubscriptions(input) {
2042
+ try {
2043
+ const { daysBack, limit } = schemas.getRecentlyCancelledSubscriptions.parse(input);
2044
+ const sinceDate = subtractDays(new Date(), daysBack);
2045
+ const subscriptions = await queryAll(this.client, `SELECT Id, SubscriptionNumber, AccountId, Status, TermEndDate, UpdatedDate, ContractedMrr ` +
2046
+ `FROM Subscription ` +
2047
+ `WHERE Status = 'Cancelled' AND UpdatedDate >= '${sinceDate}'`, limit * 2);
2048
+ if (subscriptions.length === 0) {
2049
+ return {
2050
+ success: true,
2051
+ message: `No subscriptions cancelled in the last ${daysBack} days`,
2052
+ data: { subscriptions: [], totalFound: 0, totalLostMrr: 0 },
2053
+ };
2054
+ }
2055
+ // Get account details
2056
+ const accountIds = collectIds(subscriptions, "AccountId");
2057
+ const accounts = await queryWithBatchedIds(this.client, "Id, AccountNumber, Name", "Account", "Id", accountIds);
2058
+ const accountMap = new Map(accounts.map((a) => [getString(a, "Id"), a]));
2059
+ // Get rate plan names
2060
+ const subIds = collectIds(subscriptions, "Id");
2061
+ const ratePlans = await queryWithBatchedIds(this.client, "Id, SubscriptionId, Name", "RatePlan", "SubscriptionId", subIds);
2062
+ const rpNamesBySub = new Map();
2063
+ for (const rp of ratePlans) {
2064
+ const subId = getString(rp, "SubscriptionId") ?? "";
2065
+ const existing = rpNamesBySub.get(subId) ?? [];
2066
+ const name = getString(rp, "Name");
2067
+ if (name)
2068
+ existing.push(name);
2069
+ rpNamesBySub.set(subId, existing);
2070
+ }
2071
+ const results = subscriptions.map((sub) => {
2072
+ const acctId = getString(sub, "AccountId") ?? "";
2073
+ const acct = accountMap.get(acctId);
2074
+ const subId = getString(sub, "Id") ?? "";
2075
+ return {
2076
+ subscriptionId: subId,
2077
+ subscriptionNumber: getString(sub, "SubscriptionNumber") ?? "",
2078
+ accountId: acctId,
2079
+ accountNumber: getString(acct ?? {}, "AccountNumber") ?? "",
2080
+ accountName: getString(acct ?? {}, "Name") ?? "",
2081
+ status: getString(sub, "Status") ?? "",
2082
+ cancelledDate: getString(sub, "UpdatedDate") ?? "",
2083
+ termEndDate: getString(sub, "TermEndDate") ?? "",
2084
+ contractedMrr: getNumber(sub, "ContractedMrr") ?? 0,
2085
+ ratePlanNames: rpNamesBySub.get(subId) ?? [],
2086
+ };
2087
+ });
2088
+ const totalLostMrr = results.reduce((sum, r) => sum + r.contractedMrr, 0);
2089
+ return {
2090
+ success: true,
2091
+ message: `Found ${results.length} cancelled subscription(s) in the last ${daysBack} days (lost MRR: $${totalLostMrr.toFixed(2)})`,
2092
+ data: {
2093
+ subscriptions: results.slice(0, limit),
2094
+ totalFound: results.length,
2095
+ totalLostMrr,
2096
+ },
2097
+ };
2098
+ }
2099
+ catch (error) {
2100
+ return {
2101
+ success: false,
2102
+ message: `Failed to get cancelled subscriptions: ${error instanceof Error ? error.message : String(error)}`,
2103
+ };
2104
+ }
2105
+ }
2106
+ async getInvoiceAgingReport(input) {
2107
+ try {
2108
+ const { limit } = schemas.getInvoiceAgingReport.parse(input);
2109
+ const today = todayString();
2110
+ // Query all posted invoices with outstanding balance
2111
+ const invoices = await queryAll(this.client, `SELECT Id, InvoiceNumber, AccountId, DueDate, Amount, Balance ` +
2112
+ `FROM Invoice ` +
2113
+ `WHERE Status = 'Posted' AND Balance > 0`, limit);
2114
+ if (invoices.length === 0) {
2115
+ return {
2116
+ success: true,
2117
+ message: "No outstanding invoices found",
2118
+ data: { buckets: [], totalOutstanding: 0 },
2119
+ };
2120
+ }
2121
+ // Get account details
2122
+ const accountIds = collectIds(invoices, "AccountId");
2123
+ const accounts = await queryWithBatchedIds(this.client, "Id, AccountNumber, Name", "Account", "Id", accountIds);
2124
+ const accountMap = new Map(accounts.map((a) => [getString(a, "Id"), a]));
2125
+ // Define aging buckets
2126
+ const bucketDefs = [
2127
+ { label: "Current", minDays: -999999, maxDays: 0 },
2128
+ { label: "1-30 Days", minDays: 1, maxDays: 30 },
2129
+ { label: "31-60 Days", minDays: 31, maxDays: 60 },
2130
+ { label: "61-90 Days", minDays: 61, maxDays: 90 },
2131
+ { label: "90+ Days", minDays: 91, maxDays: null },
2132
+ ];
2133
+ const buckets = bucketDefs.map((def) => ({
2134
+ ...def,
2135
+ invoiceCount: 0,
2136
+ totalBalance: 0,
2137
+ invoices: [],
2138
+ }));
2139
+ let totalOutstanding = 0;
2140
+ for (const inv of invoices) {
2141
+ const dueDate = getString(inv, "DueDate") ?? today;
2142
+ const balance = getNumber(inv, "Balance") ?? 0;
2143
+ const daysPast = daysBetween(dueDate, today);
2144
+ const acctId = getString(inv, "AccountId") ?? "";
2145
+ const acct = accountMap.get(acctId);
2146
+ totalOutstanding += balance;
2147
+ const invoiceEntry = {
2148
+ invoiceId: getString(inv, "Id") ?? "",
2149
+ invoiceNumber: getString(inv, "InvoiceNumber") ?? "",
2150
+ accountName: getString(acct ?? {}, "Name") ?? "",
2151
+ accountNumber: getString(acct ?? {}, "AccountNumber") ?? "",
2152
+ balance,
2153
+ dueDate,
2154
+ daysPastDue: Math.max(0, daysPast),
2155
+ };
2156
+ for (const bucket of buckets) {
2157
+ const inMin = daysPast >= bucket.minDays;
2158
+ const inMax = bucket.maxDays === null || daysPast <= bucket.maxDays;
2159
+ if (inMin && inMax) {
2160
+ bucket.invoiceCount++;
2161
+ bucket.totalBalance += balance;
2162
+ bucket.invoices.push(invoiceEntry);
2163
+ break;
2164
+ }
2165
+ }
2166
+ }
2167
+ // Sort invoices within each bucket by balance descending
2168
+ for (const bucket of buckets) {
2169
+ bucket.invoices.sort((a, b) => b.balance - a.balance);
2170
+ }
2171
+ return {
2172
+ success: true,
2173
+ message: `AR aging report: ${invoices.length} outstanding invoice(s), total $${totalOutstanding.toFixed(2)}`,
2174
+ data: { buckets, totalOutstanding },
2175
+ };
2176
+ }
2177
+ catch (error) {
2178
+ return {
2179
+ success: false,
2180
+ message: `Failed to get invoice aging report: ${error instanceof Error ? error.message : String(error)}`,
2181
+ };
2182
+ }
2183
+ }
2184
+ async getAccountHealthScorecard(input) {
2185
+ try {
2186
+ const { limit } = schemas.getAccountHealthScorecard.parse(input);
2187
+ const today = todayString();
2188
+ const thirtyDaysAhead = addDays(new Date(), 30);
2189
+ const thirtyDaysBack = subtractDays(new Date(), 30);
2190
+ // Signal 1: Overdue invoices
2191
+ const overdueInvoices = await queryAll(this.client, `SELECT Id, AccountId, Balance, DueDate ` +
2192
+ `FROM Invoice ` +
2193
+ `WHERE Status = 'Posted' AND Balance > 0 AND DueDate < '${today}'`, 2000);
2194
+ // Signal 2: Expiring subscriptions (next 30 days, not auto-renewing)
2195
+ const expiringSubscriptions = await queryAll(this.client, `SELECT Id, AccountId, ContractedMrr, TermEndDate ` +
2196
+ `FROM Subscription ` +
2197
+ `WHERE Status = 'Active' AND TermEndDate >= '${today}' AND TermEndDate <= '${thirtyDaysAhead}' AND AutoRenew = false`, 2000);
2198
+ // Signal 3: Recent payment failures
2199
+ const failedPayments = await queryAll(this.client, `SELECT Id, AccountId ` +
2200
+ `FROM Payment ` +
2201
+ `WHERE Status = 'Error' AND EffectiveDate >= '${thirtyDaysBack}'`, 2000);
2202
+ // Aggregate by account
2203
+ const accountScores = new Map();
2204
+ const ensureAccount = (acctId) => {
2205
+ if (!accountScores.has(acctId)) {
2206
+ accountScores.set(acctId, {
2207
+ overdueCount: 0,
2208
+ overdueBalance: 0,
2209
+ expiringCount: 0,
2210
+ expiringMrr: 0,
2211
+ paymentFailures: 0,
2212
+ });
2213
+ }
2214
+ return accountScores.get(acctId);
2215
+ };
2216
+ for (const inv of overdueInvoices) {
2217
+ const acctId = getString(inv, "AccountId");
2218
+ if (!acctId)
2219
+ continue;
2220
+ const score = ensureAccount(acctId);
2221
+ score.overdueCount++;
2222
+ score.overdueBalance += getNumber(inv, "Balance") ?? 0;
2223
+ }
2224
+ for (const sub of expiringSubscriptions) {
2225
+ const acctId = getString(sub, "AccountId");
2226
+ if (!acctId)
2227
+ continue;
2228
+ const score = ensureAccount(acctId);
2229
+ score.expiringCount++;
2230
+ score.expiringMrr += getNumber(sub, "ContractedMrr") ?? 0;
2231
+ }
2232
+ for (const pmt of failedPayments) {
2233
+ const acctId = getString(pmt, "AccountId");
2234
+ if (!acctId)
2235
+ continue;
2236
+ const score = ensureAccount(acctId);
2237
+ score.paymentFailures++;
2238
+ }
2239
+ // Calculate health scores (100 = healthy, 0 = critical)
2240
+ const accountIds = [...accountScores.keys()];
2241
+ if (accountIds.length === 0) {
2242
+ return {
2243
+ success: true,
2244
+ message: "No at-risk accounts found",
2245
+ data: { accounts: [], totalAtRisk: 0 },
2246
+ };
2247
+ }
2248
+ const accounts = await queryWithBatchedIds(this.client, "Id, AccountNumber, Name", "Account", "Id", accountIds);
2249
+ const accountMap = new Map(accounts.map((a) => [getString(a, "Id"), a]));
2250
+ const results = [];
2251
+ for (const [acctId, scores] of accountScores) {
2252
+ const acct = accountMap.get(acctId);
2253
+ const riskFactors = [];
2254
+ let healthScore = 100;
2255
+ if (scores.overdueCount > 0) {
2256
+ healthScore -= Math.min(40, scores.overdueCount * 15);
2257
+ riskFactors.push(`${scores.overdueCount} overdue invoice(s), $${scores.overdueBalance.toFixed(2)} outstanding`);
2258
+ }
2259
+ if (scores.expiringCount > 0) {
2260
+ healthScore -= Math.min(30, scores.expiringCount * 10);
2261
+ riskFactors.push(`${scores.expiringCount} subscription(s) expiring soon, $${scores.expiringMrr.toFixed(2)} MRR at risk`);
2262
+ }
2263
+ if (scores.paymentFailures > 0) {
2264
+ healthScore -= Math.min(30, scores.paymentFailures * 15);
2265
+ riskFactors.push(`${scores.paymentFailures} recent payment failure(s)`);
2266
+ }
2267
+ results.push({
2268
+ accountId: acctId,
2269
+ accountNumber: getString(acct ?? {}, "AccountNumber") ?? "",
2270
+ accountName: getString(acct ?? {}, "Name") ?? "",
2271
+ healthScore: Math.max(0, healthScore),
2272
+ riskFactors,
2273
+ overdueInvoices: {
2274
+ count: scores.overdueCount,
2275
+ totalBalance: scores.overdueBalance,
2276
+ },
2277
+ expiringSubscriptions: {
2278
+ count: scores.expiringCount,
2279
+ totalMrr: scores.expiringMrr,
2280
+ },
2281
+ recentPaymentFailures: scores.paymentFailures,
2282
+ });
2283
+ }
2284
+ // Sort by health score ascending (worst first)
2285
+ results.sort((a, b) => a.healthScore - b.healthScore);
2286
+ return {
2287
+ success: true,
2288
+ message: `Found ${results.length} account(s) with risk signals`,
2289
+ data: {
2290
+ accounts: results.slice(0, limit),
2291
+ totalAtRisk: results.length,
2292
+ },
2293
+ };
2294
+ }
2295
+ catch (error) {
2296
+ return {
2297
+ success: false,
2298
+ message: `Failed to get account health scorecard: ${error instanceof Error ? error.message : String(error)}`,
2299
+ };
2300
+ }
2301
+ }
2302
+ async findInvoicesByProduct(input) {
2303
+ try {
2304
+ const { productName, limit } = schemas.findInvoicesByProduct.parse(input);
2305
+ const safeName = escapeZoql(productName);
2306
+ // Step 1: Find ProductRatePlan IDs matching the product name
2307
+ let productRatePlans = await queryAll(this.client, `SELECT Id, Name FROM ProductRatePlan WHERE Name LIKE '%${safeName}%'`);
2308
+ if (productRatePlans.length === 0) {
2309
+ // Try Product name
2310
+ const products = await queryAll(this.client, `SELECT Id, Name FROM Product WHERE Name LIKE '%${safeName}%'`);
2311
+ if (products.length === 0) {
2312
+ return {
2313
+ success: true,
2314
+ message: `No products found matching "${productName}"`,
2315
+ data: { invoiceItems: [], totalFound: 0 },
2316
+ };
2317
+ }
2318
+ const productIds = collectIds(products, "Id");
2319
+ productRatePlans = await queryWithBatchedIds(this.client, "Id, Name", "ProductRatePlan", "ProductId", productIds);
2320
+ if (productRatePlans.length === 0) {
2321
+ return {
2322
+ success: true,
2323
+ message: `No rate plans found for product "${productName}"`,
2324
+ data: { invoiceItems: [], totalFound: 0 },
2325
+ };
2326
+ }
2327
+ }
2328
+ const prpIds = collectIds(productRatePlans, "Id");
2329
+ // Step 2: Find RatePlans → RatePlanCharges
2330
+ const ratePlans = await queryWithBatchedIds(this.client, "Id, SubscriptionId, Name", "RatePlan", "ProductRatePlanId", prpIds);
2331
+ if (ratePlans.length === 0) {
2332
+ return {
2333
+ success: true,
2334
+ message: `No subscriptions found with product "${productName}"`,
2335
+ data: { invoiceItems: [], totalFound: 0 },
2336
+ };
2337
+ }
2338
+ const rpIds = collectIds(ratePlans, "Id");
2339
+ const ratePlanCharges = await queryWithBatchedIds(this.client, "Id, RatePlanId, Name", "RatePlanCharge", "RatePlanId", rpIds);
2340
+ if (ratePlanCharges.length === 0) {
2341
+ return {
2342
+ success: true,
2343
+ message: `No charges found for product "${productName}"`,
2344
+ data: { invoiceItems: [], totalFound: 0 },
2345
+ };
2346
+ }
2347
+ const rpcIds = collectIds(ratePlanCharges, "Id");
2348
+ // Step 3: Find InvoiceItems by RatePlanChargeId
2349
+ const invoiceItems = await queryWithBatchedIds(this.client, "Id, InvoiceId, ChargeAmount, ChargeName, ServiceStartDate, ServiceEndDate, SubscriptionId", "InvoiceItem", "RatePlanChargeId", rpcIds, undefined, limit);
2350
+ if (invoiceItems.length === 0) {
2351
+ return {
2352
+ success: true,
2353
+ message: `No invoice items found for product "${productName}"`,
2354
+ data: { invoiceItems: [], totalFound: 0 },
2355
+ };
2356
+ }
2357
+ // Step 4: Get Invoice details
2358
+ const invoiceIds = collectIds(invoiceItems, "InvoiceId");
2359
+ const invoices = await queryWithBatchedIds(this.client, "Id, InvoiceNumber, InvoiceDate, AccountId", "Invoice", "Id", invoiceIds);
2360
+ const invoiceMap = new Map(invoices.map((i) => [getString(i, "Id"), i]));
2361
+ // Step 5: Get Account details
2362
+ const accountIds = collectIds(invoices, "AccountId");
2363
+ const accounts = await queryWithBatchedIds(this.client, "Id, AccountNumber, Name", "Account", "Id", accountIds);
2364
+ const accountMap = new Map(accounts.map((a) => [getString(a, "Id"), a]));
2365
+ // Build rate plan name lookup
2366
+ const rpNameMap = new Map(ratePlans.map((rp) => [getString(rp, "Id"), getString(rp, "Name") ?? ""]));
2367
+ const rpcToRp = new Map(ratePlanCharges.map((rpc) => [getString(rpc, "Id"), getString(rpc, "RatePlanId") ?? ""]));
2368
+ // Get subscription number lookup
2369
+ const subIds = collectIds(invoiceItems, "SubscriptionId");
2370
+ const subs = await queryWithBatchedIds(this.client, "Id, SubscriptionNumber", "Subscription", "Id", subIds);
2371
+ const subNumberMap = new Map(subs.map((s) => [getString(s, "Id"), getString(s, "SubscriptionNumber") ?? ""]));
2372
+ // Assemble results
2373
+ const results = invoiceItems.map((item) => {
2374
+ const invId = getString(item, "InvoiceId") ?? "";
2375
+ const inv = invoiceMap.get(invId);
2376
+ const acctId = getString(inv ?? {}, "AccountId") ?? "";
2377
+ const acct = accountMap.get(acctId);
2378
+ const subId = getString(item, "SubscriptionId") ?? "";
2379
+ return {
2380
+ invoiceId: invId,
2381
+ invoiceNumber: getString(inv ?? {}, "InvoiceNumber") ?? "",
2382
+ invoiceDate: getString(inv ?? {}, "InvoiceDate") ?? "",
2383
+ accountNumber: getString(acct ?? {}, "AccountNumber") ?? "",
2384
+ accountName: getString(acct ?? {}, "Name") ?? "",
2385
+ chargeAmount: getNumber(item, "ChargeAmount") ?? 0,
2386
+ chargeName: getString(item, "ChargeName") ?? "",
2387
+ ratePlanName: rpNameMap.get(rpcToRp.get(getString(item, "RatePlanChargeId") ?? "") ?? "") ?? "",
2388
+ subscriptionNumber: subNumberMap.get(subId) ?? "",
2389
+ };
2390
+ });
2391
+ const totalChargeAmount = results.reduce((sum, r) => sum + r.chargeAmount, 0);
2392
+ return {
2393
+ success: true,
2394
+ message: `Found ${results.length} invoice item(s) for "${productName}", total charges: $${totalChargeAmount.toFixed(2)}`,
2395
+ data: {
2396
+ invoiceItems: results.slice(0, limit),
2397
+ totalFound: results.length,
2398
+ totalChargeAmount,
2399
+ },
2400
+ };
2401
+ }
2402
+ catch (error) {
2403
+ return {
2404
+ success: false,
2405
+ message: `Failed to find invoices by product: ${error instanceof Error ? error.message : String(error)}`,
2406
+ };
2407
+ }
2408
+ }
1080
2409
  }
1081
2410
  export const toolRegistrations = [
1082
2411
  // Account Tools
@@ -1211,6 +2540,59 @@ export const toolRegistrations = [
1211
2540
  inputSchema: schemas.listUsage,
1212
2541
  invoke: (handlers, args) => handlers.listUsage(args),
1213
2542
  },
2543
+ // User Management
2544
+ {
2545
+ name: "list_users",
2546
+ description: "List Zuora platform users with optional SCIM filter (e.g., status eq 'Active'). " +
2547
+ "Uses REST API, not ZOQL — the User object is not ZOQL-queryable. " +
2548
+ "Supports pagination via startIndex/count. " +
2549
+ "Returns user names, emails, statuses, roles, and last login times.",
2550
+ inputSchema: schemas.listUsers,
2551
+ invoke: (handlers, args) => handlers.listUsers(args),
2552
+ },
2553
+ {
2554
+ name: "get_user",
2555
+ description: "Get details of a specific Zuora platform user by their user ID (UUID). " +
2556
+ "Returns user name, email, status, role, profile, and last login time.",
2557
+ inputSchema: schemas.getUser,
2558
+ invoke: (handlers, args) => handlers.getUser(args),
2559
+ },
2560
+ // Bill Run Tools
2561
+ {
2562
+ name: "get_bill_run",
2563
+ description: "Get details and status of a Zuora bill run by bill run ID. " +
2564
+ "Returns bill run number, status (Pending/Processing/Completed/Error/Canceled/Posted), " +
2565
+ "target date, invoice date, and auto-post/email settings.",
2566
+ inputSchema: schemas.getBillRun,
2567
+ invoke: (handlers, args) => handlers.getBillRun(args),
2568
+ },
2569
+ {
2570
+ name: "list_bill_runs",
2571
+ description: "List bill runs in Zuora with pagination. " +
2572
+ "Returns bill run numbers, statuses, target dates, and settings.",
2573
+ inputSchema: schemas.listBillRuns,
2574
+ invoke: (handlers, args) => handlers.listBillRuns(args),
2575
+ },
2576
+ // Contact Tools
2577
+ {
2578
+ name: "get_contact",
2579
+ description: "Get full details of a Zuora contact by contact ID. " +
2580
+ "Returns name, emails, phone numbers, address, tax region, and timestamps. " +
2581
+ "Use ZOQL to find contact IDs: SELECT Id, FirstName, LastName FROM Contact WHERE AccountId = '...'",
2582
+ inputSchema: schemas.getContact,
2583
+ invoke: (handlers, args) => handlers.getContact(args),
2584
+ },
2585
+ // Describe API
2586
+ {
2587
+ name: "describe_object",
2588
+ description: "Get field metadata for any Zuora object type. Returns all fields with names, types, " +
2589
+ "and properties (selectable, createable, updateable, filterable, required). " +
2590
+ "Essential for building correct ZOQL queries — use this to discover available fields " +
2591
+ "before writing SELECT statements. Object type must be PascalCase (e.g., Account, " +
2592
+ "Invoice, Subscription, Payment, RatePlan, RatePlanCharge, BillRun, Contact).",
2593
+ inputSchema: schemas.describeObject,
2594
+ invoke: (handlers, args) => handlers.describeObject(args),
2595
+ },
1214
2596
  // Phase 3: Write Operations (require confirmation before calling)
1215
2597
  // Payment Creation (HIGH risk)
1216
2598
  {
@@ -1332,5 +2714,127 @@ export const toolRegistrations = [
1332
2714
  inputSchema: schemas.createRefund,
1333
2715
  invoke: (handlers, args) => handlers.createRefund(args),
1334
2716
  },
2717
+ // Bill Run Creation (MEDIUM risk)
2718
+ {
2719
+ name: "create_bill_run",
2720
+ description: "FINANCIAL WRITE OPERATION (MEDIUM RISK): Create a bill run to generate invoices for a billing cycle. " +
2721
+ "Bill runs process all eligible charges through the target date and generate draft invoices. " +
2722
+ "Confirm the target date with the user before calling. " +
2723
+ "Set autoPost=true to automatically post generated invoices. " +
2724
+ "Set autoEmail=true (requires autoPost) to email invoices to customers. " +
2725
+ "Requires a UUID idempotency key to prevent duplicate bill runs. " +
2726
+ "The bill run starts in Pending status and progresses through Processing to Completed.",
2727
+ inputSchema: schemas.createBillRun,
2728
+ invoke: (handlers, args) => handlers.createBillRun(args),
2729
+ },
2730
+ // Contact Creation (LOW risk)
2731
+ {
2732
+ name: "create_contact",
2733
+ description: "WRITE OPERATION (LOW RISK): Create a new contact for a Zuora account. " +
2734
+ "Requires account ID, first name, and last name. " +
2735
+ "Contacts can be used as bill-to or sold-to addresses via update_account. " +
2736
+ "Requires a UUID idempotency key to prevent duplicate contacts.",
2737
+ inputSchema: schemas.createContact,
2738
+ invoke: (handlers, args) => handlers.createContact(args),
2739
+ },
2740
+ // Contact Update (LOW risk)
2741
+ {
2742
+ name: "update_contact",
2743
+ description: "WRITE OPERATION (LOW RISK): Update an existing Zuora contact's information. " +
2744
+ "Only include fields that need to change. " +
2745
+ "ALWAYS use get_contact to review current details before updating.",
2746
+ inputSchema: schemas.updateContact,
2747
+ invoke: (handlers, args) => handlers.updateContact(args),
2748
+ },
2749
+ // ==================== Composite Tools (Read-Only) ====================
2750
+ {
2751
+ name: "find_accounts_by_product",
2752
+ description: "Find accounts with active subscriptions for a specific product. " +
2753
+ "Answers questions like 'Which accounts have Security Analytics?' " +
2754
+ "Internally chains: ProductRatePlan → RatePlan → Subscription → Account. " +
2755
+ "Returns account details with subscription info and rate plan names.",
2756
+ inputSchema: schemas.findAccountsByProduct,
2757
+ invoke: (handlers, args) => handlers.findAccountsByProduct(args),
2758
+ },
2759
+ {
2760
+ name: "get_overdue_invoices",
2761
+ description: "List all overdue invoices across all accounts, sorted by days past due. " +
2762
+ "Answers 'Show overdue invoices' or 'What invoices are past due?' " +
2763
+ "Filters: Posted invoices with balance > 0 and due date in the past. " +
2764
+ "Returns invoice details with account info and days past due.",
2765
+ inputSchema: schemas.getOverdueInvoices,
2766
+ invoke: (handlers, args) => handlers.getOverdueInvoices(args),
2767
+ },
2768
+ {
2769
+ name: "get_expiring_subscriptions",
2770
+ description: "Find subscriptions expiring within a time window. " +
2771
+ "Answers 'What subscriptions expire this month?' or 'Renewal pipeline for Q1.' " +
2772
+ "Shows MRR at risk (non-auto-renewing subscriptions). " +
2773
+ "Returns subscription details with account info and days until expiry.",
2774
+ inputSchema: schemas.getExpiringSubscriptions,
2775
+ invoke: (handlers, args) => handlers.getExpiringSubscriptions(args),
2776
+ },
2777
+ {
2778
+ name: "get_account_billing_overview",
2779
+ description: "Comprehensive billing summary for a single account in one call. " +
2780
+ "Answers 'What's the billing status for account X?' " +
2781
+ "Combines: account details + recent invoices + payments + active subscriptions. " +
2782
+ "Shows outstanding balance, overdue amounts, MRR, and recent activity.",
2783
+ inputSchema: schemas.getAccountBillingOverview,
2784
+ invoke: (handlers, args) => handlers.getAccountBillingOverview(args),
2785
+ },
2786
+ {
2787
+ name: "get_revenue_by_product",
2788
+ description: "MRR breakdown by product across all active subscriptions. " +
2789
+ "Answers 'What's our revenue by product?' or 'MRR breakdown.' " +
2790
+ "Chains: Subscription → RatePlan → ProductRatePlan → Product. " +
2791
+ "Returns products sorted by MRR with subscription details.",
2792
+ inputSchema: schemas.getRevenueByProduct,
2793
+ invoke: (handlers, args) => handlers.getRevenueByProduct(args),
2794
+ },
2795
+ {
2796
+ name: "get_payment_reconciliation",
2797
+ description: "Payment reconciliation report for a date range. " +
2798
+ "Answers 'What payments were received this month?' " +
2799
+ "Returns payment list with account details, summary stats, and status breakdown.",
2800
+ inputSchema: schemas.getPaymentReconciliation,
2801
+ invoke: (handlers, args) => handlers.getPaymentReconciliation(args),
2802
+ },
2803
+ {
2804
+ name: "get_recently_cancelled_subscriptions",
2805
+ description: "Find subscriptions cancelled within a recent time window. " +
2806
+ "Answers 'What churned last month?' or 'Recent cancellations.' " +
2807
+ "Shows lost MRR and affected products. " +
2808
+ "Returns cancelled subscription details with account info.",
2809
+ inputSchema: schemas.getRecentlyCancelledSubscriptions,
2810
+ invoke: (handlers, args) => handlers.getRecentlyCancelledSubscriptions(args),
2811
+ },
2812
+ {
2813
+ name: "get_invoice_aging_report",
2814
+ description: "AR aging report: all outstanding invoices bucketed by days past due. " +
2815
+ "Answers 'Give me an AR aging report' or 'What does our accounts receivable look like?' " +
2816
+ "Buckets: Current, 1-30, 31-60, 61-90, 90+ days. " +
2817
+ "Returns aging buckets with invoice and account details.",
2818
+ inputSchema: schemas.getInvoiceAgingReport,
2819
+ invoke: (handlers, args) => handlers.getInvoiceAgingReport(args),
2820
+ },
2821
+ {
2822
+ name: "get_account_health_scorecard",
2823
+ description: "Identify at-risk accounts using multiple signals: overdue invoices, " +
2824
+ "expiring subscriptions (non-auto-renewing), and recent payment failures. " +
2825
+ "Answers 'Which accounts are at risk?' " +
2826
+ "Returns accounts sorted by health score (0=critical, 100=healthy) with risk factors.",
2827
+ inputSchema: schemas.getAccountHealthScorecard,
2828
+ invoke: (handlers, args) => handlers.getAccountHealthScorecard(args),
2829
+ },
2830
+ {
2831
+ name: "find_invoices_by_product",
2832
+ description: "Find invoice line items for a specific product. " +
2833
+ "Answers 'Show invoices for Security Analytics' or 'What was billed for product X?' " +
2834
+ "Chains: ProductRatePlan → RatePlan → RatePlanCharge → InvoiceItem → Invoice → Account. " +
2835
+ "Returns invoice items with charge amounts, dates, and account details.",
2836
+ inputSchema: schemas.findInvoicesByProduct,
2837
+ invoke: (handlers, args) => handlers.findInvoicesByProduct(args),
2838
+ },
1335
2839
  ];
1336
2840
  //# sourceMappingURL=tools.js.map