@letsbot/sdk 0.2.0 → 0.5.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/README.md CHANGED
@@ -74,12 +74,16 @@ for await (const won of crm.iterate('crmListDeals', { status: 'won', sort: 'upda
74
74
  | `deals` | `list`, `pipelines`, `lostReasons`, `search`, `get`, `create`, `update`, `delete`, `moveStage`, `markWon`, `markLost`, `addNote` |
75
75
  | `tasks` | `list`, `get`, `create`, `update`, `delete`, `complete` |
76
76
  | `activities`, `quotes`, `products` | `list`, `get`, `create` (+ `update`, `delete`, `schema` for quotes and products) |
77
- | `hooks` | `list`, `events`, `sample`, `subscribe`, `unsubscribe` |
77
+ | `customObjects` | `list`, `schema` (custom objects — 404 until the workspace switches them on) |
78
+ | `customRecords` | `list`, `search`, `get`, `create`, `update`, `delete` — pass the object `key`, e.g. `co_projects`; values by field key |
79
+ | `hooks` | `list`, `events`, `sample`, `subscribe`, `unsubscribe` (`CRM_WEBHOOK_EVENTS` lists all 24 events) |
78
80
 
79
81
  Every method takes one object: path parameters (`id`, `contactId`) plus the query (reads) or the JSON body (writes).
80
82
  Lists take `q`, `filters` (lists-engine JSON — see `…schema()`), `view`, `sort`, `dir`, `page`, `per_page` and answer
81
83
  `{success, data, meta}`. Errors are `LetsBotApiError` with the envelope's `code` (`missing_scope`, `not_found`,
82
- `validation_failed`, `already_exists` — a duplicate phone, with the existing contact in `error.body.data`, …). Reads
84
+ `validation_failed`, `already_exists` — a duplicate phone, with the existing contact in `error.body.data`,
85
+ `possible_duplicate` — a likely duplicate, read it with `crmPossibleDuplicate(error)` or repeat with
86
+ `allow_duplicate: true`, …). Reads
83
87
  are retried on network errors and 5xx; writes are not. The client-side limit defaults to 120 requests/minute, the
84
88
  API's per-key limit.
85
89
 
@@ -90,6 +94,22 @@ Webhook deliveries (panel → CRM → Settings → API & webhooks, or `crm.hooks
90
94
  const valid = verifyCrmWebhookSignature(rawBody, req.headers['x-letsbot-signature'], subscriptionSecret);
91
95
  ```
92
96
 
97
+ `invoice.created`, `form.submitted`, `deal.assigned` and `health.at_risk` deliveries carry an extra block next to `data`:
98
+ `invoice`, `form` (source, form, submission, answers), `assignment` (new / previous owner) and `health` (score,
99
+ previous, reasons; `object` says contact or company).
100
+
101
+ ```ts
102
+ const record = await crm.customRecords.create({ key: 'co_projects', values: { name: 'Riyadh fit-out', budget: 120000 } });
103
+
104
+ try {
105
+ await crm.contacts.create({ phone: '966501234567', email: 'laila@example.com' });
106
+ } catch (error) {
107
+ const dup = crmPossibleDuplicate(error);
108
+ if (!dup) throw error;
109
+ await crm.contacts.update({ id: dup.existingId, name: 'Laila' });
110
+ }
111
+ ```
112
+
93
113
  Any operation can also be called by its `operationId` from the spec:
94
114
 
95
115
  ```ts
@@ -97,6 +117,38 @@ await letsbot.call('sendButtonsAlias', { phone: '966501234567', body: 'Confirm?'
97
117
  const { data, status, headers, replayed } = await letsbot.callWithResponse('getChatList');
98
118
  ```
99
119
 
120
+ ### Business / ERP (`LetsBotErp`)
121
+
122
+ The Business API lives on the workspace domain like the CRM API and needs a workspace key with the **erp** scope
123
+ ticked. Each area (inventory, sales, purchasing, expenses, accounting) answers 404 until the workspace switches it on.
124
+ Creates return drafts (or take the one step you call); nothing messages a customer.
125
+
126
+ ```ts
127
+ import { LetsBotErp, verifyErpWebhookSignature } from '@letsbot/sdk';
128
+
129
+ const erp = new LetsBotErp({ apiKey: process.env.LETSBOT_KEY!, workspace: 'mystore.letsbot.net' });
130
+
131
+ const { data: order } = await erp.salesOrders.create({ customer_id: 42, lines: [{ sku: 'RY-OUD-50', qty: 2 }] });
132
+ await erp.salesOrders.confirm({ id: order!.id! });
133
+
134
+ const { data: invoice } = await erp.invoices.create({ order_id: order!.id! });
135
+ await erp.invoices.issue({ id: invoice!.id! });
136
+ const { data: link } = await erp.invoices.pdfLink({ id: invoice!.id! }); // link.pdf_url once shareable
137
+
138
+ await erp.payments.record({ customer_id: 42, amount: 1150, method: 'bank_transfer', reference: 'TRX-1' });
139
+
140
+ for await (const level of erp.iterate('erpListStockLevels', { warehouse_id: 1 })) console.log(level.sku, level.available);
141
+
142
+ // Webhooks: invoice.issued, invoice.paid, invoice.cleared, invoice.rejected, credit_note.issued, payment.received,
143
+ // order.confirmed, stock.low, po.confirmed_by_supplier, expense.posted
144
+ const { data: hook } = await erp.hooks.subscribe({ url: 'https://example.com/letsbot', events: ['invoice.paid'] });
145
+ // on receipt: verifyErpWebhookSignature(rawBody, req.headers['x-letsbot-signature'], hook.secret)
146
+ ```
147
+
148
+ Every create (`salesOrders.create`, `invoices.create`, `invoices.createCreditNote`, `payments.record`,
149
+ `purchaseOrders.create`, `expenses.create`) sends an `Idempotency-Key` — pass `{ idempotencyKey: 'order-123' }` to
150
+ tie it to your own id — so a retry after a timeout never makes a second document.
151
+
100
152
  ### Templates
101
153
 
102
154
  ```ts
package/dist/index.cjs CHANGED
@@ -76,6 +76,7 @@ var operations = {
76
76
  addGroupParticipant: { method: "POST", path: "/group/add/participant", params: "body", send: false },
77
77
  archiveChat: { method: "POST", path: "/chat/archive", params: "body", send: false },
78
78
  blockUser: { method: "POST", path: "/user/block", params: "body", send: false },
79
+ cancelOrder: { method: "POST", path: "/orders/{orderId}/cancel", params: "body", send: false },
79
80
  checkPhone: { method: "POST", path: "/check/phone", params: "body", send: false },
80
81
  createGroup: { method: "POST", path: "/group/create", params: "body", send: false },
81
82
  createProduct: { method: "POST", path: "/product/create", params: "body", send: false },
@@ -110,6 +111,7 @@ var operations = {
110
111
  qr: { method: "POST", path: "/qr", params: "none", send: false },
111
112
  queryBroadcast: { method: "POST", path: "/instance/query/broadcast", params: "body", send: false },
112
113
  readChat: { method: "POST", path: "/chat/read", params: "body", send: false },
114
+ recordOrder: { method: "POST", path: "/orders", params: "body", send: false },
113
115
  removeGroupParticipant: { method: "POST", path: "/group/remove/participant", params: "body", send: false },
114
116
  renameGroup: { method: "POST", path: "/group/rename", params: "body", send: false },
115
117
  revokeInviteCode: { method: "POST", path: "/invite/revoke/code", params: "body", send: false },
@@ -240,7 +242,7 @@ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
240
242
 
241
243
  // src/client.ts
242
244
  var DEFAULT_BASE_URL = "https://letsbot.net/api/v1";
243
- var SDK_VERSION = "0.2.0";
245
+ var SDK_VERSION = "0.5.0";
244
246
  var IDEMPOTENT_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
245
247
  var RETRYABLE_5XX = /* @__PURE__ */ new Set([500, 502, 503, 504]);
246
248
  var LetsBotClient = class {
@@ -522,6 +524,10 @@ var resourceMap = {
522
524
  collections: "getCollections",
523
525
  getOrder: "getOrder"
524
526
  },
527
+ orders: {
528
+ record: "recordOrder",
529
+ cancel: "cancelOrder"
530
+ },
525
531
  session: {
526
532
  qr: "qr",
527
533
  find: "findSession",
@@ -563,13 +569,16 @@ var crmOperations = {
563
569
  crmCompleteTask: { method: "POST", path: "/tasks/{id}/complete", body: false },
564
570
  crmContactsSchema: { method: "GET", path: "/contacts/schema", body: false },
565
571
  crmCreateActivity: { method: "POST", path: "/activities", body: true },
566
- crmCreateCompany: { method: "POST", path: "/companies", body: true },
567
- crmCreateContact: { method: "POST", path: "/contacts", body: true },
572
+ crmCreateCompany: { method: "POST", path: "/companies", body: true, query: ["allow_duplicate"] },
573
+ crmCreateContact: { method: "POST", path: "/contacts", body: true, query: ["allow_duplicate"] },
574
+ crmCreateCustomRecord: { method: "POST", path: "/objects/{key}/records", body: true },
568
575
  crmCreateDeal: { method: "POST", path: "/deals", body: true },
569
576
  crmCreateProduct: { method: "POST", path: "/products", body: true },
570
577
  crmCreateQuote: { method: "POST", path: "/quotes", body: true },
571
578
  crmCreateTask: { method: "POST", path: "/tasks", body: true },
579
+ crmCustomObjectSchema: { method: "GET", path: "/objects/{key}/schema", body: false },
572
580
  crmDeleteCompany: { method: "DELETE", path: "/companies/{id}", body: false },
581
+ crmDeleteCustomRecord: { method: "DELETE", path: "/objects/{key}/records/{id}", body: false },
573
582
  crmDeleteDeal: { method: "DELETE", path: "/deals/{id}", body: false },
574
583
  crmDeleteProduct: { method: "DELETE", path: "/products/{id}", body: false },
575
584
  crmDeleteQuote: { method: "DELETE", path: "/quotes/{id}", body: false },
@@ -577,6 +586,7 @@ var crmOperations = {
577
586
  crmGetActivity: { method: "GET", path: "/activities/{id}", body: false },
578
587
  crmGetCompany: { method: "GET", path: "/companies/{id}", body: false },
579
588
  crmGetContact: { method: "GET", path: "/contacts/{id}", body: false },
589
+ crmGetCustomRecord: { method: "GET", path: "/objects/{key}/records/{id}", body: false },
580
590
  crmGetDeal: { method: "GET", path: "/deals/{id}", body: false },
581
591
  crmGetProduct: { method: "GET", path: "/products/{id}", body: false },
582
592
  crmGetQuote: { method: "GET", path: "/quotes/{id}", body: false },
@@ -586,6 +596,8 @@ var crmOperations = {
586
596
  crmListActivities: { method: "GET", path: "/activities", body: false },
587
597
  crmListCompanies: { method: "GET", path: "/companies", body: false },
588
598
  crmListContacts: { method: "GET", path: "/contacts", body: false },
599
+ crmListCustomObjects: { method: "GET", path: "/objects", body: false },
600
+ crmListCustomRecords: { method: "GET", path: "/objects/{key}/records", body: false },
589
601
  crmListDeals: { method: "GET", path: "/deals", body: false },
590
602
  crmListHookEvents: { method: "GET", path: "/hooks/events", body: false },
591
603
  crmListHooks: { method: "GET", path: "/hooks", body: false },
@@ -601,12 +613,14 @@ var crmOperations = {
601
613
  crmQuotesSchema: { method: "GET", path: "/quotes/schema", body: false },
602
614
  crmSearchCompanies: { method: "GET", path: "/companies/search", body: false },
603
615
  crmSearchContacts: { method: "GET", path: "/contacts/search", body: false },
616
+ crmSearchCustomRecords: { method: "GET", path: "/objects/{key}/records/search", body: false },
604
617
  crmSearchDeals: { method: "GET", path: "/deals/search", body: false },
605
618
  crmSubscribeHook: { method: "POST", path: "/hooks", body: true },
606
619
  crmUnlinkCompanyContact: { method: "DELETE", path: "/companies/{id}/contacts/{contactId}", body: false },
607
620
  crmUnsubscribeHook: { method: "DELETE", path: "/hooks/{id}", body: false },
608
621
  crmUpdateCompany: { method: "PATCH", path: "/companies/{id}", body: true },
609
622
  crmUpdateContact: { method: "PATCH", path: "/contacts/{id}", body: true },
623
+ crmUpdateCustomRecord: { method: "PATCH", path: "/objects/{key}/records/{id}", body: true },
610
624
  crmUpdateDeal: { method: "PATCH", path: "/deals/{id}", body: true },
611
625
  crmUpdateProduct: { method: "PATCH", path: "/products/{id}", body: true },
612
626
  crmUpdateQuote: { method: "PATCH", path: "/quotes/{id}", body: true },
@@ -614,6 +628,40 @@ var crmOperations = {
614
628
  };
615
629
 
616
630
  // src/crm.ts
631
+ var CRM_WEBHOOK_EVENTS = [
632
+ "contact.created",
633
+ "contact.updated",
634
+ "contact.deleted",
635
+ "company.created",
636
+ "company.updated",
637
+ "company.deleted",
638
+ "deal.created",
639
+ "deal.updated",
640
+ "deal.deleted",
641
+ "deal.stage_changed",
642
+ "deal.won",
643
+ "deal.lost",
644
+ "deal.assigned",
645
+ "task.created",
646
+ "task.completed",
647
+ "ticket.created",
648
+ "quote.sent",
649
+ "quote.viewed",
650
+ "quote.accepted",
651
+ "quote.signed",
652
+ "quote.paid",
653
+ "invoice.created",
654
+ "form.submitted",
655
+ "health.at_risk"
656
+ ];
657
+ function crmPossibleDuplicate(error) {
658
+ const e = error;
659
+ if (!e || e.status !== 409 || e.code !== "possible_duplicate") return null;
660
+ const body = e.body ?? {};
661
+ const duplicates = Array.isArray(body.duplicates) ? body.duplicates : [];
662
+ const existingId = Number(body.existing_id ?? duplicates[0]?.id);
663
+ return Number.isFinite(existingId) ? { existingId, duplicates } : null;
664
+ }
617
665
  var CRM_DEFAULT_RATE_LIMIT = 120;
618
666
  var CRM_SIGNATURE_HEADER = "X-LetsBot-Signature";
619
667
  var crmResourceMap = {
@@ -681,6 +729,18 @@ var crmResourceMap = {
681
729
  update: "crmUpdateProduct",
682
730
  delete: "crmDeleteProduct"
683
731
  },
732
+ customObjects: {
733
+ list: "crmListCustomObjects",
734
+ schema: "crmCustomObjectSchema"
735
+ },
736
+ customRecords: {
737
+ list: "crmListCustomRecords",
738
+ search: "crmSearchCustomRecords",
739
+ get: "crmGetCustomRecord",
740
+ create: "crmCreateCustomRecord",
741
+ update: "crmUpdateCustomRecord",
742
+ delete: "crmDeleteCustomRecord"
743
+ },
684
744
  hooks: {
685
745
  list: "crmListHooks",
686
746
  events: "crmListHookEvents",
@@ -721,6 +781,10 @@ var LetsBotCrm = class {
721
781
  activities;
722
782
  quotes;
723
783
  products;
784
+ /** Custom objects (`/objects`): 404 until the workspace has custom objects switched on. */
785
+ customObjects;
786
+ /** Records of a custom object (`/objects/{key}/records`); pass the object `key`, e.g. `co_projects`. */
787
+ customRecords;
724
788
  hooks;
725
789
  transport;
726
790
  constructor(options) {
@@ -749,6 +813,8 @@ var LetsBotCrm = class {
749
813
  this.activities = bind(crmResourceMap.activities);
750
814
  this.quotes = bind(crmResourceMap.quotes);
751
815
  this.products = bind(crmResourceMap.products);
816
+ this.customObjects = bind(crmResourceMap.customObjects);
817
+ this.customRecords = bind(crmResourceMap.customRecords);
752
818
  this.hooks = bind(crmResourceMap.hooks);
753
819
  }
754
820
  /** The resolved base URL, e.g. `https://mystore.letsbot.net/api/v1/crm`. */
@@ -774,10 +840,19 @@ var LetsBotCrm = class {
774
840
  return encodeURIComponent(String(value));
775
841
  });
776
842
  const url = new URL(this.transport.root + path);
843
+ const append = (key, value) => {
844
+ if (value === void 0 || value === null) return;
845
+ url.searchParams.append(key, typeof value === "object" ? JSON.stringify(value) : String(value));
846
+ };
777
847
  if (!meta.body) {
778
- for (const [key, value] of Object.entries(rest)) {
779
- if (value === void 0 || value === null) continue;
780
- url.searchParams.append(key, typeof value === "object" ? JSON.stringify(value) : String(value));
848
+ for (const [key, value] of Object.entries(rest)) append(key, value);
849
+ } else if ("query" in meta) {
850
+ for (const key of meta.query) {
851
+ if (key in rest) {
852
+ const value = rest[key];
853
+ append(key, typeof value === "boolean" ? value ? 1 : 0 : value);
854
+ delete rest[key];
855
+ }
781
856
  }
782
857
  }
783
858
  return this.transport.request(operationId, meta.method, url, meta.body ? rest : void 0, options);
@@ -813,6 +888,240 @@ function verifyCrmWebhookSignature(rawBody, header, secret, options = {}) {
813
888
  const given = Buffer.from(v1, "hex");
814
889
  return given.length === expected.length && crypto.timingSafeEqual(given, expected);
815
890
  }
891
+
892
+ // src/generated/erp-operations.ts
893
+ var erpOperations = {
894
+ erpConfirmSalesOrder: { method: "POST", path: "/sales/orders/{id}/confirm", body: false, idempotent: false },
895
+ erpCreateCreditNote: { method: "POST", path: "/sales/invoices/{id}/credit-notes", body: true, idempotent: true },
896
+ erpCreateExpense: { method: "POST", path: "/expenses", body: true, idempotent: true },
897
+ erpCreateInvoice: { method: "POST", path: "/sales/invoices", body: true, idempotent: true },
898
+ erpCreatePurchaseOrder: { method: "POST", path: "/purchasing/purchase-orders", body: true, idempotent: true },
899
+ erpCreateSalesOrder: { method: "POST", path: "/sales/orders", body: true, idempotent: true },
900
+ erpCustomerStatement: { method: "GET", path: "/sales/customers/{id}/statement", body: false, idempotent: false },
901
+ erpGetBill: { method: "GET", path: "/purchasing/bills/{id}", body: false, idempotent: false },
902
+ erpGetCreditNote: { method: "GET", path: "/sales/credit-notes/{id}", body: false, idempotent: false },
903
+ erpGetCustomer: { method: "GET", path: "/sales/customers/{id}", body: false, idempotent: false },
904
+ erpGetExpense: { method: "GET", path: "/expenses/{id}", body: false, idempotent: false },
905
+ erpGetInvoice: { method: "GET", path: "/sales/invoices/{id}", body: false, idempotent: false },
906
+ erpGetItem: { method: "GET", path: "/inventory/items/{id}", body: false, idempotent: false },
907
+ erpGetJournalEntry: { method: "GET", path: "/accounting/journal-entries/{id}", body: false, idempotent: false },
908
+ erpGetPayment: { method: "GET", path: "/sales/payments/{id}", body: false, idempotent: false },
909
+ erpGetPurchaseOrder: { method: "GET", path: "/purchasing/purchase-orders/{id}", body: false, idempotent: false },
910
+ erpGetSalesOrder: { method: "GET", path: "/sales/orders/{id}", body: false, idempotent: false },
911
+ erpGetSupplier: { method: "GET", path: "/purchasing/suppliers/{id}", body: false, idempotent: false },
912
+ erpHookEvents: { method: "GET", path: "/hooks/events", body: false, idempotent: false },
913
+ erpHookSample: { method: "GET", path: "/hooks/sample", body: false, idempotent: false },
914
+ erpInvoicePdfLink: { method: "GET", path: "/sales/invoices/{id}/pdf", body: false, idempotent: false },
915
+ erpIssueCreditNote: { method: "POST", path: "/sales/credit-notes/{id}/issue", body: false, idempotent: false },
916
+ erpIssueInvoice: { method: "POST", path: "/sales/invoices/{id}/issue", body: false, idempotent: false },
917
+ erpListAccounts: { method: "GET", path: "/accounting/accounts", body: false, idempotent: false },
918
+ erpListBills: { method: "GET", path: "/purchasing/bills", body: false, idempotent: false },
919
+ erpListCreditNotes: { method: "GET", path: "/sales/credit-notes", body: false, idempotent: false },
920
+ erpListCustomers: { method: "GET", path: "/sales/customers", body: false, idempotent: false },
921
+ erpListExpenseCategories: { method: "GET", path: "/expenses/categories", body: false, idempotent: false },
922
+ erpListExpenses: { method: "GET", path: "/expenses", body: false, idempotent: false },
923
+ erpListHooks: { method: "GET", path: "/hooks", body: false, idempotent: false },
924
+ erpListInvoices: { method: "GET", path: "/sales/invoices", body: false, idempotent: false },
925
+ erpListItems: { method: "GET", path: "/inventory/items", body: false, idempotent: false },
926
+ erpListJournalEntries: { method: "GET", path: "/accounting/journal-entries", body: false, idempotent: false },
927
+ erpListPayments: { method: "GET", path: "/sales/payments", body: false, idempotent: false },
928
+ erpListPurchaseOrders: { method: "GET", path: "/purchasing/purchase-orders", body: false, idempotent: false },
929
+ erpListSalesOrders: { method: "GET", path: "/sales/orders", body: false, idempotent: false },
930
+ erpListStockLevels: { method: "GET", path: "/inventory/stock-levels", body: false, idempotent: false },
931
+ erpListStockMoves: { method: "GET", path: "/inventory/stock-moves", body: false, idempotent: false },
932
+ erpListSuppliers: { method: "GET", path: "/purchasing/suppliers", body: false, idempotent: false },
933
+ erpListVariants: { method: "GET", path: "/inventory/variants", body: false, idempotent: false },
934
+ erpListWarehouses: { method: "GET", path: "/inventory/warehouses", body: false, idempotent: false },
935
+ erpRecordPayment: { method: "POST", path: "/sales/payments", body: true, idempotent: true },
936
+ erpReportBalanceSheet: { method: "GET", path: "/accounting/reports/balance-sheet", body: false, idempotent: false },
937
+ erpReportProfitAndLoss: { method: "GET", path: "/accounting/reports/profit-and-loss", body: false, idempotent: false },
938
+ erpReportVat: { method: "GET", path: "/accounting/reports/vat", body: false, idempotent: false },
939
+ erpSubscribeHook: { method: "POST", path: "/hooks", body: true, idempotent: false },
940
+ erpUnsubscribeHook: { method: "DELETE", path: "/hooks/{id}", body: false, idempotent: false }
941
+ };
942
+
943
+ // src/erp.ts
944
+ var ERP_DEFAULT_RATE_LIMIT = 120;
945
+ var ERP_WEBHOOK_EVENTS = [
946
+ "invoice.issued",
947
+ "invoice.paid",
948
+ "invoice.cleared",
949
+ "invoice.rejected",
950
+ "credit_note.issued",
951
+ "payment.received",
952
+ "order.confirmed",
953
+ "stock.low",
954
+ "po.confirmed_by_supplier",
955
+ "expense.posted"
956
+ ];
957
+ var erpResourceMap = {
958
+ items: { list: "erpListItems", get: "erpGetItem" },
959
+ variants: { list: "erpListVariants" },
960
+ warehouses: { list: "erpListWarehouses" },
961
+ stock: { levels: "erpListStockLevels", moves: "erpListStockMoves" },
962
+ customers: { list: "erpListCustomers", get: "erpGetCustomer", statement: "erpCustomerStatement" },
963
+ salesOrders: { list: "erpListSalesOrders", get: "erpGetSalesOrder", create: "erpCreateSalesOrder", confirm: "erpConfirmSalesOrder" },
964
+ invoices: {
965
+ list: "erpListInvoices",
966
+ get: "erpGetInvoice",
967
+ create: "erpCreateInvoice",
968
+ issue: "erpIssueInvoice",
969
+ pdfLink: "erpInvoicePdfLink",
970
+ createCreditNote: "erpCreateCreditNote"
971
+ },
972
+ creditNotes: { list: "erpListCreditNotes", get: "erpGetCreditNote", issue: "erpIssueCreditNote" },
973
+ payments: { list: "erpListPayments", get: "erpGetPayment", record: "erpRecordPayment" },
974
+ suppliers: { list: "erpListSuppliers", get: "erpGetSupplier" },
975
+ purchaseOrders: { list: "erpListPurchaseOrders", get: "erpGetPurchaseOrder", create: "erpCreatePurchaseOrder" },
976
+ bills: { list: "erpListBills", get: "erpGetBill" },
977
+ expenses: { list: "erpListExpenses", get: "erpGetExpense", create: "erpCreateExpense", categories: "erpListExpenseCategories" },
978
+ accounting: {
979
+ accounts: "erpListAccounts",
980
+ journalEntries: "erpListJournalEntries",
981
+ journalEntry: "erpGetJournalEntry",
982
+ profitAndLoss: "erpReportProfitAndLoss",
983
+ balanceSheet: "erpReportBalanceSheet",
984
+ vat: "erpReportVat"
985
+ },
986
+ hooks: {
987
+ list: "erpListHooks",
988
+ events: "erpHookEvents",
989
+ sample: "erpHookSample",
990
+ subscribe: "erpSubscribeHook",
991
+ unsubscribe: "erpUnsubscribeHook"
992
+ }
993
+ };
994
+ var UNSAFE2 = /* @__PURE__ */ new Set(["POST", "PATCH", "PUT", "DELETE"]);
995
+ function erpBaseUrl(workspace) {
996
+ const raw = workspace.trim();
997
+ if (raw === "") throw new TypeError("LetsBotErp: `workspace` is empty");
998
+ const host = (/^https?:\/\//i.test(raw) ? new URL(raw).host : raw.split("/")[0]).toLowerCase();
999
+ return `https://${host.includes(".") ? host : `${host}.letsbot.net`}/api/v1/erp`;
1000
+ }
1001
+ var ErpTransport = class extends LetsBotClient {
1002
+ authHeaders(apiKey) {
1003
+ return { "X-Zapier-Api-Key": apiKey };
1004
+ }
1005
+ request(operationId, method, url, json, idempotent, options) {
1006
+ const headers = this.baseHeaders(options);
1007
+ let body;
1008
+ if (json !== void 0) {
1009
+ headers["Content-Type"] = "application/json";
1010
+ body = JSON.stringify(json);
1011
+ }
1012
+ let idempotencyKey;
1013
+ if (idempotent && options.idempotencyKey !== false) {
1014
+ idempotencyKey = options.idempotencyKey ?? this.newKey();
1015
+ headers["Idempotency-Key"] = idempotencyKey;
1016
+ }
1017
+ const unsafeRetryAllowed = !UNSAFE2.has(method) || idempotencyKey !== void 0;
1018
+ return this.perform(operationId, method, url, headers, body, { idempotencyKey, unsafeRetryAllowed }, options);
1019
+ }
1020
+ get root() {
1021
+ return this.baseUrl;
1022
+ }
1023
+ };
1024
+ var LetsBotErp = class {
1025
+ items;
1026
+ variants;
1027
+ warehouses;
1028
+ stock;
1029
+ customers;
1030
+ salesOrders;
1031
+ invoices;
1032
+ creditNotes;
1033
+ payments;
1034
+ suppliers;
1035
+ purchaseOrders;
1036
+ bills;
1037
+ expenses;
1038
+ accounting;
1039
+ hooks;
1040
+ transport;
1041
+ constructor(options) {
1042
+ if (!options?.baseUrl && !options?.workspace) {
1043
+ throw new TypeError("LetsBotErp: pass `workspace` (e.g. mystore.letsbot.net) or `baseUrl`");
1044
+ }
1045
+ this.transport = new ErpTransport(
1046
+ {
1047
+ ...options,
1048
+ baseUrl: options.baseUrl ?? erpBaseUrl(options.workspace),
1049
+ rateLimit: options.rateLimit === false ? false : { requestsPerMinute: options.rateLimit?.requestsPerMinute ?? ERP_DEFAULT_RATE_LIMIT }
1050
+ },
1051
+ "LetsBotErp: `apiKey` (a workspace API key with the erp scope)"
1052
+ );
1053
+ const bind = (map) => {
1054
+ const bound = {};
1055
+ for (const [name, id] of Object.entries(map)) {
1056
+ bound[name] = (params, options2) => this.call(id, ...[params, options2]);
1057
+ }
1058
+ return bound;
1059
+ };
1060
+ this.items = bind(erpResourceMap.items);
1061
+ this.variants = bind(erpResourceMap.variants);
1062
+ this.warehouses = bind(erpResourceMap.warehouses);
1063
+ this.stock = bind(erpResourceMap.stock);
1064
+ this.customers = bind(erpResourceMap.customers);
1065
+ this.salesOrders = bind(erpResourceMap.salesOrders);
1066
+ this.invoices = bind(erpResourceMap.invoices);
1067
+ this.creditNotes = bind(erpResourceMap.creditNotes);
1068
+ this.payments = bind(erpResourceMap.payments);
1069
+ this.suppliers = bind(erpResourceMap.suppliers);
1070
+ this.purchaseOrders = bind(erpResourceMap.purchaseOrders);
1071
+ this.bills = bind(erpResourceMap.bills);
1072
+ this.expenses = bind(erpResourceMap.expenses);
1073
+ this.accounting = bind(erpResourceMap.accounting);
1074
+ this.hooks = bind(erpResourceMap.hooks);
1075
+ }
1076
+ /** The resolved base URL, e.g. `https://mystore.letsbot.net/api/v1/erp`. */
1077
+ get baseUrl() {
1078
+ return this.transport.root;
1079
+ }
1080
+ /** Call any ERP operation by its operationId and get the body. */
1081
+ async call(operationId, ...args) {
1082
+ return (await this.callWithResponse(operationId, ...args)).data;
1083
+ }
1084
+ /** Call any ERP operation and get the body plus status, headers, Idempotency-Key and replay flag. */
1085
+ async callWithResponse(operationId, ...args) {
1086
+ const [params = {}, options = {}] = args;
1087
+ const meta = erpOperations[operationId];
1088
+ if (!meta) throw new TypeError(`Unknown LetsBot ERP operation: ${String(operationId)}`);
1089
+ const rest = { ...params };
1090
+ const path = meta.path.replace(/\{(\w+)\}/g, (_, name) => {
1091
+ const value = rest[name];
1092
+ if (value === void 0 || value === null || value === "") {
1093
+ throw new TypeError(`${operationId}: \`${name}\` is required`);
1094
+ }
1095
+ delete rest[name];
1096
+ return encodeURIComponent(String(value));
1097
+ });
1098
+ const url = new URL(this.transport.root + path);
1099
+ if (!meta.body) {
1100
+ for (const [key, value] of Object.entries(rest)) {
1101
+ if (value === void 0 || value === null) continue;
1102
+ url.searchParams.append(key, typeof value === "object" ? JSON.stringify(value) : String(value));
1103
+ }
1104
+ }
1105
+ return this.transport.request(operationId, meta.method, url, meta.body ? rest : void 0, meta.idempotent, options);
1106
+ }
1107
+ /**
1108
+ * Every row of a list, page by page (`meta.last_page`).
1109
+ *
1110
+ * ```ts
1111
+ * for await (const invoice of erp.iterate('erpListInvoices', { status: 'issued,partially_paid', overdue: true })) { … }
1112
+ * ```
1113
+ */
1114
+ async *iterate(operationId, params = {}, options) {
1115
+ for (let page = 1; ; page++) {
1116
+ const body = await this.call(operationId, ...[{ per_page: 100, ...params, page }, options]);
1117
+ yield* body.data;
1118
+ if (body.data.length === 0 || page >= (body.meta?.last_page ?? page)) return;
1119
+ }
1120
+ }
1121
+ };
1122
+ function verifyErpWebhookSignature(rawBody, header, secret, options = {}) {
1123
+ return verifyCrmWebhookSignature(rawBody, header, secret, options);
1124
+ }
816
1125
  var IDENTITY_MAX_SUB_LENGTH = 191;
817
1126
  var IDENTITY_MAX_TTL_SECONDS = 7 * 24 * 60 * 60;
818
1127
  var IDENTITY_DEFAULT_TTL = "24h";
@@ -889,6 +1198,8 @@ var LetsBot = class extends LetsBotClient {
889
1198
  user;
890
1199
  instance;
891
1200
  products;
1201
+ /** Store orders: record one (order journey + invoice) or stop its journey. */
1202
+ orders;
892
1203
  session;
893
1204
  archive;
894
1205
  constructor(options) {
@@ -904,6 +1215,7 @@ var LetsBot = class extends LetsBotClient {
904
1215
  this.user = bindResource(this, resourceMap.user);
905
1216
  this.instance = bindResource(this, resourceMap.instance);
906
1217
  this.products = bindResource(this, resourceMap.products);
1218
+ this.orders = bindResource(this, resourceMap.orders);
907
1219
  this.session = bindResource(this, resourceMap.session);
908
1220
  this.archive = {
909
1221
  ...bindResource(this, resourceMap.archive),
@@ -914,7 +1226,10 @@ var LetsBot = class extends LetsBotClient {
914
1226
 
915
1227
  exports.CRM_DEFAULT_RATE_LIMIT = CRM_DEFAULT_RATE_LIMIT;
916
1228
  exports.CRM_SIGNATURE_HEADER = CRM_SIGNATURE_HEADER;
1229
+ exports.CRM_WEBHOOK_EVENTS = CRM_WEBHOOK_EVENTS;
917
1230
  exports.DEFAULT_BASE_URL = DEFAULT_BASE_URL;
1231
+ exports.ERP_DEFAULT_RATE_LIMIT = ERP_DEFAULT_RATE_LIMIT;
1232
+ exports.ERP_WEBHOOK_EVENTS = ERP_WEBHOOK_EVENTS;
918
1233
  exports.IDENTITY_DEFAULT_TTL = IDENTITY_DEFAULT_TTL;
919
1234
  exports.IDENTITY_MAX_SUB_LENGTH = IDENTITY_MAX_SUB_LENGTH;
920
1235
  exports.IDENTITY_MAX_TTL_SECONDS = IDENTITY_MAX_TTL_SECONDS;
@@ -924,16 +1239,22 @@ exports.LetsBotApiError = LetsBotApiError;
924
1239
  exports.LetsBotClient = LetsBotClient;
925
1240
  exports.LetsBotConnectionError = LetsBotConnectionError;
926
1241
  exports.LetsBotCrm = LetsBotCrm;
1242
+ exports.LetsBotErp = LetsBotErp;
927
1243
  exports.SDK_VERSION = SDK_VERSION;
928
1244
  exports.TokenBucket = TokenBucket;
929
1245
  exports.createIdentityToken = createIdentityToken;
930
1246
  exports.crmBaseUrl = crmBaseUrl;
931
1247
  exports.crmOperations = crmOperations;
1248
+ exports.crmPossibleDuplicate = crmPossibleDuplicate;
932
1249
  exports.crmResourceMap = crmResourceMap;
1250
+ exports.erpBaseUrl = erpBaseUrl;
1251
+ exports.erpOperations = erpOperations;
1252
+ exports.erpResourceMap = erpResourceMap;
933
1253
  exports.experimental_verifyWebhookSignature = experimental_verifyWebhookSignature;
934
1254
  exports.isLogicalFailure = isLogicalFailure;
935
1255
  exports.operations = operations;
936
1256
  exports.resourceMap = resourceMap;
937
1257
  exports.verifyCrmWebhookSignature = verifyCrmWebhookSignature;
1258
+ exports.verifyErpWebhookSignature = verifyErpWebhookSignature;
938
1259
  //# sourceMappingURL=index.cjs.map
939
1260
  //# sourceMappingURL=index.cjs.map