@solumflow-app/crm-client 0.1.0 → 0.3.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.
@@ -1,47 +1,27 @@
1
1
  /** What a key is allowed to reach. A key carries one or more of these. */
2
- export type ApiScope = 'catalog:read' | 'events:read' | 'orders:write' | 'contacts:write' | 'forms:write';
2
+ export type ApiScope = 'catalog:read' | 'catalog:write' | 'events:read' | 'orders:write' | 'contacts:write' | 'forms:write' | 'website:manage';
3
3
  export declare const ALL_API_SCOPES: readonly ApiScope[];
4
4
  /** Everything an endpoint can be told about. A delivery names one. */
5
- export type ApiWebhookEvent = 'product.changed' | 'product.deleted' | 'event.changed' | 'order.status_changed';
5
+ export type ApiWebhookEvent = 'product.changed' | 'product.deleted' | 'category.changed' | 'event.changed' | 'order.status_changed';
6
6
  export declare const ALL_API_WEBHOOK_EVENTS: readonly ApiWebhookEvent[];
7
7
  /** The `error.code` of a refusal. Match on this, never on the message -- the message is written for a person reading a log and may be reworded. */
8
- export type ApiErrorCode = 'unauthorized' | 'forbidden' | 'feature_unavailable' | 'not_found' | 'invalid_request' | 'idempotency_key_reused' | 'request_in_progress' | 'rate_limited' | 'internal_error';
8
+ export type ApiErrorCode = 'unauthorized' | 'forbidden' | 'feature_unavailable' | 'not_found' | 'invalid_request' | 'conflict' | 'idempotency_key_reused' | 'request_in_progress' | 'rate_limited' | 'internal_error';
9
9
  export declare const ALL_API_ERROR_CODES: readonly ApiErrorCode[];
10
- /**
11
- * One product as a stranger's website sees it.
12
- *
13
- * These interfaces are the public API's contract, not internal convenience
14
- * types. They are served cross-origin to pages nobody here controls, and a
15
- * customer's shop reads these exact keys — so a field removed or renamed breaks
16
- * a site that will not be redeployed. Add fields; do not repurpose them.
17
- *
18
- * What is deliberately absent is as much the contract as what is here.
19
- *
20
- * - **`sku`** is the number this business uses to find the thing in its own
21
- * stockroom. It says how many suppliers there are, which one a line came
22
- * from, and often what was paid. `gtin` is the number printed on the box and
23
- * is offered instead, in the detail: that one is meant to be public, and a
24
- * shop needs it for a product feed.
25
- * - **The stock count** never leaves. `inStock` answers the only question a
26
- * visitor has, and the number itself is a business fact a competitor would
27
- * like and a cached answer would get wrong within the minute.
28
- * - **`unit_price_cents` raw** is not it either; see `priceFromCents`.
29
- * - **Non-public custom fields.** `fields` carries only attributes a member
30
- * marked `is_public`, which defaults to off. A field called "purchase price"
31
- * stays home unless somebody says otherwise, once, per field.
32
- *
33
- * And one field that an event has and a product does not: **`url`**. An event
34
- * is sold on a page this system hosts, so its listing can hand out a link. A
35
- * product is sold on the customer's own site — that is the entire reason this
36
- * API exists — and this system does not know what they called their product
37
- * page. A guessed link is worse than none.
38
- */
10
+ /** What a product is, for the one question the answer changes: does anything get shipped. Whether it repeats is a property of its price, not of this. */
11
+ export type ProductType = 'digital' | 'physical' | 'service';
12
+ export declare const ALL_PRODUCT_TYPES: readonly ProductType[];
39
13
  export interface PublicProductListItem {
40
14
  id: string;
15
+ /**
16
+ * Which catalogue the product is in, by the slug the seller gave it:
17
+ * `product` for the catalogue every account has, something like `courses`
18
+ * for one of its own. The listing takes the same word as `?object=`.
19
+ */
20
+ object: string;
41
21
  /** Always present: a product without an address cannot be published. */
42
22
  slug: string;
43
23
  name: string;
44
- productType: 'digital' | 'physical';
24
+ productType: ProductType;
45
25
  /**
46
26
  * The storage path, not a URL. The route turns it into a public URL, because
47
27
  * only the route knows the origin its own storage is served from.
@@ -73,8 +53,15 @@ export interface PublicProductListItem {
73
53
  */
74
54
  inStock: boolean;
75
55
  /**
76
- * The custom fields this account invented, keyed by the slug they chose, and
77
- * only the ones marked public. An empty object when none are.
56
+ * The custom fields this account invented for this product's catalogue,
57
+ * keyed by the slug they chose, and only the ones marked public there. An
58
+ * empty object when none are.
59
+ *
60
+ * A content block (`content_text`, `repeater`, `cta` -- see
61
+ * `PublicContentText`, `PublicRepeater` and `PublicCta`) that is public but
62
+ * has nothing in it is `null`, not absent: the site can tell "no content
63
+ * yet" from "not published". A block's shape is described by the field's own
64
+ * entry in `GET /attributes` (`PublicProductField`).
78
65
  */
79
66
  fields: Record<string, unknown>;
80
67
  }
@@ -95,11 +82,40 @@ export interface PublicProductDetail extends PublicProductListItem {
95
82
  gtin: string | null;
96
83
  metaTitle: string | null;
97
84
  metaDescription: string | null;
85
+ /**
86
+ * What a link to this product should show when somebody shares it, with
87
+ * every fallback already applied.
88
+ *
89
+ * Resolved here rather than left to the site, and `metaTitle` beside it is
90
+ * why: a site given both would have to decide which wins when one is empty,
91
+ * and every site would decide slightly differently. The screen that edits
92
+ * these shows a preview, and that preview and this field are the same
93
+ * function -- so what a seller saw is what a stranger gets.
94
+ */
95
+ social: {
96
+ title: string;
97
+ description: string | null;
98
+ /** A path, turned into a URL by the route. Same reason as `imagePath`. */
99
+ imagePath: string | null;
100
+ };
98
101
  images: PublicProductImage[];
99
102
  prices: PublicProductPrice[];
100
103
  categories: PublicProductCategory[];
101
104
  /** What the seller linked this to — a companion, a refill, a bigger model. */
102
105
  related: PublicProductSummary[];
106
+ /**
107
+ * The fields this product's variants differ along — a width, a colour —
108
+ * each with the values somebody can choose, in the field's own order.
109
+ * Empty for a product sold as one thing.
110
+ */
111
+ variantAxes: PublicProductVariantAxis[];
112
+ /**
113
+ * One entry per variant still on sale, in the seller's order. A shop
114
+ * orders a variant by its `id` or by the `id` of one of its prices.
115
+ * Pictures are the variant's own; a variant without any shows the
116
+ * product's.
117
+ */
118
+ variants: PublicProductVariant[];
103
119
  }
104
120
  export interface PublicProductImage {
105
121
  /** A path, turned into a URL by the route. Same reason as `imagePath`. */
@@ -127,14 +143,240 @@ export interface PublicProductPrice {
127
143
  trialPeriodDays: number | null;
128
144
  isDefault: boolean;
129
145
  }
146
+ /**
147
+ * One category this product sits in, as a page a visitor could land on.
148
+ *
149
+ * It used to be a record of an object the account invented, linked through
150
+ * `crm_record_links`, and it carried a name and nothing else. It is now a real
151
+ * category: a page with an address, a place in a tree, and one of them marked
152
+ * as the one this product belongs to most.
153
+ *
154
+ * `slug` is nullable even though only published categories are listed here,
155
+ * and that is not belt-and-braces: it keeps the type honest against the day a
156
+ * caller asks for the categories of something that is not on sale yet.
157
+ *
158
+ * `path` runs from the top down and **ends with this category itself**, so a
159
+ * breadcrumb is the array as given, with no element to append. A step whose
160
+ * `slug` is null is a step to print rather than to link -- an ancestor that is
161
+ * still a draft is not a page anybody can open.
162
+ */
130
163
  export interface PublicProductCategory {
131
164
  id: string;
132
165
  name: string;
166
+ slug: string | null;
167
+ /** Root first, this category last. Never empty. */
168
+ path: PublicCategoryStep[];
169
+ /** The one that carries the breadcrumb and the structured data. */
170
+ isPrimary: boolean;
171
+ }
172
+ /** One step on the way down to a category. */
173
+ export interface PublicCategoryStep {
174
+ id: string;
175
+ name: string;
176
+ /** Null when this step has no page of its own yet: print it, do not link it. */
177
+ slug: string | null;
133
178
  }
134
179
  export interface PublicProductSummary {
135
180
  id: string;
181
+ /** The catalogue it is in, the same word a listing item carries. */
182
+ object: string;
183
+ slug: string;
184
+ name: string;
185
+ }
186
+ export interface PublicProductVariantAxis {
187
+ slug: string;
188
+ label: string;
189
+ values: string[];
190
+ }
191
+ export interface PublicProductVariant {
192
+ id: string;
193
+ /** The value per axis, keyed by the axis slug. */
194
+ options: Record<string, string>;
195
+ /** The cheapest active price of this variant, as on the product. */
196
+ priceFromCents: number;
197
+ inStock: boolean;
198
+ prices: PublicProductPrice[];
199
+ images: PublicProductImage[];
200
+ }
201
+ /** A label with the address it leads to. The address is http, https, mailto or tel. */
202
+ export interface PublicContentLink {
203
+ label: string;
204
+ url: string;
205
+ }
206
+ /** A glyph of the icon library: the set it is in and its name there. */
207
+ export interface PublicContentIcon {
208
+ set: 'brands' | 'solid' | 'regular';
209
+ name: string;
210
+ }
211
+ /**
212
+ * A picture or a download inside a block, ready to use.
213
+ *
214
+ * `size` (bytes) and `contentType` are null when storage cannot say; `name` is
215
+ * the file name to show or to save the download under.
216
+ */
217
+ export interface PublicContentFile {
218
+ url: string;
219
+ name: string;
220
+ size: number | null;
221
+ contentType: string | null;
222
+ }
223
+ /** A `content_text` field: sanitised html and optional links beneath it. */
224
+ export interface PublicContentText {
225
+ html: string;
226
+ links?: PublicContentLink[];
227
+ }
228
+ /**
229
+ * One entry of a repeater. The keys besides `id` are the `itemFields` of the
230
+ * field as `GET /attributes` describes it; an entry holds a key only when it
231
+ * was filled in.
232
+ */
233
+ export interface PublicRepeaterItem {
234
+ id: string;
235
+ [key: string]: string | PublicContentLink | PublicContentIcon | PublicContentFile | undefined;
236
+ }
237
+ /**
238
+ * A `repeater` field: a list, an accordion (FAQ), downloads or testimonials,
239
+ * depending on the field's `display` and `preset`. Keys an item field no
240
+ * longer has are never in the items.
241
+ */
242
+ export interface PublicRepeater {
243
+ items: PublicRepeaterItem[];
244
+ }
245
+ /** A `cta` field: a heading, a line of text and one or two buttons. */
246
+ export interface PublicCta {
247
+ heading: string;
248
+ body: string;
249
+ primary: PublicContentLink;
250
+ /** Only present when the field offers a second button. */
251
+ secondary?: PublicContentLink | null;
252
+ }
253
+ /** One entry of a repeater's structure, as `GET /attributes` describes it. */
254
+ export interface PublicRepeaterItemField {
255
+ key: string;
256
+ label: string;
257
+ type: 'text' | 'richtext' | 'url' | 'icon' | 'image' | 'file';
258
+ }
259
+ /**
260
+ * A custom field of the catalogue as `GET /attributes` lists it.
261
+ *
262
+ * `options` is empty unless the field is a choice. The last four keys exist
263
+ * only on a `repeater`: how to draw it (`display`), the name of the built-in
264
+ * shape it started as (`preset`, may be absent), the structure of one entry
265
+ * (`itemFields`) and which entry key names it (`titleKey`, the question of a
266
+ * FAQ).
267
+ */
268
+ export interface PublicProductField {
269
+ slug: string;
270
+ label: string;
271
+ type: string;
272
+ multiple: boolean;
273
+ options: {
274
+ value: string;
275
+ label: string;
276
+ }[];
277
+ display?: 'list' | 'accordion';
278
+ preset?: string;
279
+ itemFields?: PublicRepeaterItemField[];
280
+ titleKey?: string;
281
+ }
282
+ /**
283
+ * One category as a stranger's website sees it.
284
+ *
285
+ * The same contract rules as `public-product-item.ts`: these keys are read by
286
+ * shops nobody here redeploys, so fields may be added and must not be renamed
287
+ * or repurposed.
288
+ *
289
+ * What is deliberately absent:
290
+ *
291
+ * - **`active`.** It says whether a member still files products under this
292
+ * category, which is a fact about their catalogue rather than about the
293
+ * page. A shop that read it would hide a page that is plainly published.
294
+ * - **`sortOrder`.** The order is the order of the array. A number would
295
+ * invite a shop to sort by it across pages, where it does not mean what it
296
+ * looks like -- it is unique only among siblings.
297
+ * - **Anything that counts drafts.** `productCount` is what a visitor would
298
+ * find, counted the same way the product list selects. A number larger than
299
+ * the page it heads is a promise the page then breaks.
300
+ */
301
+ export interface PublicCategoryListItem {
302
+ id: string;
303
+ /** Always present: a category without an address is not published. */
136
304
  slug: string;
137
305
  name: string;
306
+ /** Where it hangs, so a shop can build the tree from a flat page. */
307
+ parentId: string | null;
308
+ description: string | null;
309
+ /**
310
+ * The storage path, not a URL. The route turns it into a public URL, because
311
+ * only the route knows the origin its own storage is served from.
312
+ */
313
+ imagePath: string | null;
314
+ /**
315
+ * The way down from the top, ending with this category.
316
+ *
317
+ * Here as well as on the detail, and not left to a shop to assemble from
318
+ * `parentId`. `?parent=<id>` answers one level, so a menu that unfolds has
319
+ * no ancestors to build from -- and a shop that stitched a breadcrumb
320
+ * together itself here while reading `path` on the detail page would have
321
+ * two sources for one road. They agree until somebody moves a category, and
322
+ * then the visible trail and the `BreadcrumbList` beside it say different
323
+ * things, which is the one contradiction a search engine reads as a fault.
324
+ *
325
+ * A step whose `slug` is null is an ancestor with no page of its own yet --
326
+ * print it, do not link it.
327
+ */
328
+ path: PublicCategoryStep[];
329
+ /** Public products on this category's page, its whole branch included. */
330
+ productCount: number;
331
+ /**
332
+ * When this category last changed, for `lastmod` in a shop's sitemap.
333
+ *
334
+ * It is the moment the *category* was edited: its text, its address, its
335
+ * place in the tree. A product moving onto its shelf does not move it, so a
336
+ * shop that wants its category pages recrawled when the contents change has
337
+ * to take the products into account as well.
338
+ *
339
+ * **Not the same kind of fact as `publishedAt`, which is deliberately absent
340
+ * here.** A publication moment may lie in the future, and then it is a plan:
341
+ * it tells a stranger when a shop intends to launch something. This one is
342
+ * always in the past and says only that the page they can already see was
343
+ * touched -- which is exactly what a sitemap publishes anyway.
344
+ */
345
+ updatedAt: string;
346
+ }
347
+ export interface PublicCategoryDetail {
348
+ id: string;
349
+ slug: string;
350
+ name: string;
351
+ parentId: string | null;
352
+ description: string | null;
353
+ imagePath: string | null;
354
+ metaTitle: string | null;
355
+ metaDescription: string | null;
356
+ /**
357
+ * What a link to this page looks like when somebody shares it.
358
+ *
359
+ * Filled in by the same function the seller previewed it with, so what they
360
+ * saw is what a stranger gets.
361
+ */
362
+ social: {
363
+ title: string;
364
+ description: string | null;
365
+ /** A path, turned into a URL by the route. Same reason as `imagePath`. */
366
+ imagePath: string | null;
367
+ };
368
+ /**
369
+ * The way down from the top, ending with this category.
370
+ *
371
+ * The breadcrumb, as given. A step whose `slug` is null is an ancestor with
372
+ * no page of its own yet -- print it, do not link it.
373
+ */
374
+ path: PublicCategoryStep[];
375
+ productCount: number;
376
+ /** When the category itself last changed. Same meaning as on the listing. */
377
+ updatedAt: string;
378
+ /** The first page of them; `nextCursor` beside the answer carries the rest. */
379
+ products: PublicProductListItem[];
138
380
  }
139
381
  /**
140
382
  * One event as a stranger's website sees it.
@@ -263,3 +505,60 @@ export interface ApiWebhookPayload {
263
505
  ids: string[];
264
506
  truncated: boolean;
265
507
  }
508
+ /** A published form, ready to be placed on a page. */
509
+ export interface PlacementForm {
510
+ id: string;
511
+ slug: string;
512
+ name: string;
513
+ /** The form on a page of its own, for a plain link. */
514
+ url: string;
515
+ /** An empty `div` naming the form and the script that fills it. */
516
+ embed: string;
517
+ }
518
+ /** A chat widget and what it takes to put it on a site. */
519
+ export interface PlacementChatWidget {
520
+ id: string;
521
+ slug: string;
522
+ name: string;
523
+ status: 'draft' | 'published' | 'archived';
524
+ /**
525
+ * The sites the widget may run on. A site not listed here gets an empty
526
+ * corner, with the reason only in the browser's console.
527
+ */
528
+ allowedOrigins: string[];
529
+ /** The script tag, or null while the widget is not published. */
530
+ embed: string | null;
531
+ }
532
+ /** The website tracking script of the account. */
533
+ export interface PlacementSiteTracking {
534
+ /** Public by design: it is in the page source of every tracked site. */
535
+ publicKey: string;
536
+ /** The hosts the script reports from; visits from any other are dropped. */
537
+ allowedHosts: string[];
538
+ /**
539
+ * The consent the script waits for before it does anything:
540
+ * `ad_storage`, `analytics_storage`, `personalization_storage`, or
541
+ * `api_only` (it waits until the page says so itself).
542
+ */
543
+ consentSignal: 'ad_storage' | 'analytics_storage' | 'personalization_storage' | 'api_only';
544
+ lastReceivedAt: string | null;
545
+ embed: string;
546
+ }
547
+ /** A page a visitor can book on. */
548
+ export interface PlacementBookingPage {
549
+ /**
550
+ * `services`: every service the account offers online. `category` and
551
+ * `service`: narrowed to one. `calendar`: an appointment calendar of its own.
552
+ */
553
+ kind: 'services' | 'category' | 'service' | 'calendar';
554
+ /** The category, service or calendar; null for `services`. */
555
+ id: string | null;
556
+ name: string;
557
+ /** The page's address. Built on the id where there is one, so a rename does not break it. */
558
+ url: string;
559
+ /**
560
+ * An iframe plus the script that sizes it, or null for a `calendar`, which
561
+ * has no embeddable widget yet: link to `url` instead.
562
+ */
563
+ embed: string | null;
564
+ }