@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 +54 -2
- package/dist/index.cjs +327 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6492 -2381
- package/dist/index.d.ts +6492 -2381
- package/dist/index.js +319 -7
- package/dist/index.js.map +1 -1
- package/openapi/erp-v1.yaml +3090 -0
- package/openapi/openapi.yaml +1099 -105
- package/package.json +3 -2
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
|
-
| `
|
|
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`,
|
|
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.
|
|
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
|
-
|
|
780
|
-
|
|
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
|