@holaboss/client 0.2.0-beta.3 → 0.2.0-beta.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import * as react_jsx_runtime from 'react/jsx-runtime';
1
+ import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
3
 
4
4
  /** A file the employee produced (image/video/audio/other). In-turn artifacts (a turn's
@@ -61,6 +61,9 @@ type HolaMessage = {
61
61
  type HolaSubscribeHandlers = {
62
62
  onMessage: (message: HolaMessage) => void;
63
63
  onError?: (error: HolaError) => void;
64
+ /** A poll completed cleanly. Fires every successful cycle (even with no new messages) so a
65
+ * consumer can clear a stale error — the receive channel retries, so one blip is not an outage. */
66
+ onHealthy?: () => void;
64
67
  };
65
68
  type HolaSubscribeOptions = {
66
69
  /** Resume from this cursor; omit to read the thread from the start. */
@@ -97,9 +100,6 @@ type HolaTransport = {
97
100
  * async artifact. Returns an unsubscribe. Optional: a transport with no receive channel omits
98
101
  * it (callers guard on its presence). */
99
102
  subscribe?(handlers: HolaSubscribeHandlers, options?: HolaSubscribeOptions): () => void;
100
- /** "Talk to a person": ask for a human to take over this conversation. The AI pauses until a
101
- * teammate replies or hands back; the person's messages arrive via `subscribe`. Optional. */
102
- requestHuman?(): Promise<void>;
103
103
  };
104
104
 
105
105
  type ApiTransportConfig = {
@@ -131,7 +131,7 @@ declare const useArtifactUrl: (dataBase64: string, mimeType: string) => string;
131
131
 
132
132
  /** A minimal, dependency-free chat surface for the employee. Drop it inside a
133
133
  * `<HolaProvider>`. For custom UI, use `useChat(client)` directly. */
134
- declare const Chat: () => react_jsx_runtime.JSX.Element;
134
+ declare const Chat: () => react.JSX.Element;
135
135
 
136
136
  type WidgetTransportConfig = {
137
137
  /** API origin that fronts the Holaboss employee endpoint, e.g. "https://api.holaos.ai". */
@@ -158,13 +158,6 @@ type CreateHolaClientOptions = WidgetTransportConfig | {
158
158
  * agnostic — the current transport is an internal detail, not the SDK's identity. */
159
159
  declare const createHolaClient: (options: CreateHolaClientOptions) => HolaClient;
160
160
 
161
- /** Extract a streamed text delta from a raw pi turn event, or null. */
162
- declare const textDeltaOf: (event: unknown) => string | null;
163
- /** Extract a streamed reasoning delta from a raw pi turn event, or null. */
164
- declare const thinkingDeltaOf: (event: unknown) => string | null;
165
- /** Extract a tool-execution start/end from a raw pi turn event, or null. */
166
- declare const toolEventOf: (event: unknown) => HolaToolEvent | null;
167
-
168
161
  /** A column, as the company defined it. */
169
162
  type PortalField = {
170
163
  key: string;
@@ -183,11 +176,22 @@ type PortalTable = {
183
176
  writable_fields: string[];
184
177
  row_count: number;
185
178
  };
179
+ /** Who last wrote a row. `employee` is the company's AI employee, `org_member` its
180
+ * staff, `end_user` this customer. Null on rows written before the platform
181
+ * recorded it — unknown rather than assumed. */
182
+ type PortalRowAuthor = {
183
+ kind: "org_member" | "employee" | "end_user";
184
+ id: string | null;
185
+ };
186
186
  type PortalRow = {
187
187
  rowId: string;
188
188
  data: Record<string, unknown>;
189
189
  createdAt: string;
190
190
  updatedAt: string;
191
+ /** Bumped on every write. Quote it back on a patch (`If-Match`) and a save made
192
+ * against a value that has since moved is refused instead of overwriting it. */
193
+ revision: number;
194
+ updatedBy: PortalRowAuthor | null;
191
195
  };
192
196
  type PortalRowPage = {
193
197
  rows: PortalRow[];
@@ -244,8 +248,14 @@ type PortalData = {
244
248
  insertRow(slug: string, row: Record<string, unknown>): Promise<string>;
245
249
  /** Pass an empty string to clear a field. */
246
250
  patchRow(slug: string, rowId: string, patch: Record<string, unknown>): Promise<number>;
247
- /** Only what the company allows a customer to delete, and only when nothing
248
- * depends on it. Rejects with the reason otherwise. */
251
+ /**
252
+ * Only what the company allows a customer to delete, and only when nothing
253
+ * depends on it. Rejects with the reason otherwise.
254
+ *
255
+ * Genuinely any table: which ones are deletable, and what holds a row back, is
256
+ * declared in the company's own schema rather than known to the platform. This
257
+ * used to look general and answer for two hard-coded slugs.
258
+ */
249
259
  deleteRow(slug: string, rowId: string): Promise<number>;
250
260
  /** Everything exchanged with this customer, newest first. */
251
261
  listFiles(): Promise<PortalFile[]>;
@@ -288,14 +298,1033 @@ type PortalData = {
288
298
  };
289
299
  declare const createPortalData: (config: ApiTransportConfig) => PortalData;
290
300
 
301
+ /** Build a `PortalField[]` from a slice's rows, typed by the dominant JSON value type per key.
302
+ * `select` is never inferred (it needs an option set only a template carries) — such columns fall
303
+ * back to `text`. Returns [] for an empty slice. */
304
+ declare function deriveFields(rows: readonly {
305
+ data: Record<string, unknown>;
306
+ }[]): PortalField[];
307
+
308
+ /** How a metric column is reduced to a single number. */
309
+ type Agg = "sum" | "avg" | "count" | "min" | "max" | "last";
310
+ type WidgetType = "stat" | "line" | "bar" | "donut" | "table" | "text";
311
+ /** A headline number: one metric reduced by `agg` (omit `metric` to count the rows). */
312
+ interface StatWidget {
313
+ type: "stat";
314
+ /** Which table to read. Absent = the dashboard's primary table. See `Widget.source`. */
315
+ source?: string | undefined;
316
+ label?: string | undefined;
317
+ metric?: string | undefined;
318
+ agg?: Agg | undefined;
319
+ w?: number | undefined;
320
+ h?: number | undefined;
321
+ gx?: number | undefined;
322
+ gy?: number | undefined;
323
+ }
324
+ /** A time series: `y` (a numeric column) aggregated per day of `x` (a date column); omit `y` to count
325
+ * rows per day. `line` and `bar` differ only in how the renderer draws the same points. */
326
+ interface SeriesWidget {
327
+ type: "line" | "bar";
328
+ /** Which table to read. Absent = the dashboard's primary table. See `Widget.source`. */
329
+ source?: string | undefined;
330
+ title?: string | undefined;
331
+ x: string;
332
+ y?: string | undefined;
333
+ agg?: Agg | undefined;
334
+ w?: number | undefined;
335
+ h?: number | undefined;
336
+ gx?: number | undefined;
337
+ gy?: number | undefined;
338
+ }
339
+ /** A breakdown per distinct value of `groupBy`.
340
+ *
341
+ * `metric` + `agg` say WHAT is measured for each group; omit `metric` and it counts rows, which was
342
+ * the only thing this could do and stays the default — every donut authored before this renders
343
+ * identically. With one, "how many newsletters per status" becomes "how many RECIPIENTS per
344
+ * status", which is usually the number the dashboard exists to show. */
345
+ interface DonutWidget {
346
+ type: "donut";
347
+ /** Which table to read. Absent = the dashboard's primary table. See `Widget.source`. */
348
+ source?: string | undefined;
349
+ title?: string | undefined;
350
+ groupBy: string;
351
+ metric?: string | undefined;
352
+ agg?: Agg | undefined;
353
+ w?: number | undefined;
354
+ h?: number | undefined;
355
+ gx?: number | undefined;
356
+ gy?: number | undefined;
357
+ }
358
+ /** A row table: `columns` (by key, in order) or every non-plumbing column when omitted. */
359
+ interface TableWidget {
360
+ type: "table";
361
+ /** Which table to read. Absent = the dashboard's primary table. See `Widget.source`. */
362
+ source?: string | undefined;
363
+ title?: string | undefined;
364
+ columns?: string[] | undefined;
365
+ w?: number | undefined;
366
+ h?: number | undefined;
367
+ gx?: number | undefined;
368
+ gy?: number | undefined;
369
+ }
370
+ /** Static prose — a note above or between the data widgets. */
371
+ interface TextWidget {
372
+ type: "text";
373
+ title?: string | undefined;
374
+ body: string;
375
+ w?: number | undefined;
376
+ h?: number | undefined;
377
+ gx?: number | undefined;
378
+ gy?: number | undefined;
379
+ }
380
+ /**
381
+ * `source` — which table a widget reads.
382
+ *
383
+ * One dashboard, many tables: a service's work items, its spend, a channel breakdown. Absent means
384
+ * the primary table, which is what every spec authored before this said implicitly, so an old spec
385
+ * keeps rendering unchanged. The renderer supplies the named tables; a widget naming one it was not
386
+ * given is dropped rather than silently redrawn against the primary — a chart of the wrong table is
387
+ * worse than no chart, because it looks right.
388
+ */
389
+ type Widget = StatWidget | SeriesWidget | DonutWidget | TableWidget | TextWidget;
390
+ interface DashboardSpec {
391
+ version: number;
392
+ title?: string | undefined;
393
+ widgets: Widget[];
394
+ }
395
+ /** Parse a service's stored dashboard spec (a JSON string or an already-parsed object). Returns null
396
+ * for anything that isn't a usable spec — empty, absent, garbled, or with no valid widgets — so the
397
+ * caller falls back to inference instead of showing a blank dashboard. */
398
+ declare function parseDashboardSpec(raw: unknown): DashboardSpec | null;
399
+
400
+ declare function inferSpec(fields: PortalField[], rows: PortalRow[]): DashboardSpec;
401
+
402
+ /** A point on a line/bar chart or a slice of a donut. */
403
+ interface Point {
404
+ label: string;
405
+ value: number;
406
+ }
407
+ /** A widget resolved against the data — everything a renderer needs, already computed. */
408
+ type ResolvedWidget = {
409
+ kind: "stat";
410
+ label: string;
411
+ value: number;
412
+ w?: number | undefined;
413
+ h?: number | undefined;
414
+ gx?: number | undefined;
415
+ gy?: number | undefined;
416
+ } | {
417
+ kind: "series";
418
+ draw: "line" | "bar";
419
+ title: string;
420
+ points: Point[];
421
+ w?: number | undefined;
422
+ h?: number | undefined;
423
+ gx?: number | undefined;
424
+ gy?: number | undefined;
425
+ } | {
426
+ kind: "donut";
427
+ title: string;
428
+ points: Point[];
429
+ w?: number | undefined;
430
+ h?: number | undefined;
431
+ gx?: number | undefined;
432
+ gy?: number | undefined;
433
+ } | {
434
+ kind: "table";
435
+ title?: string | undefined;
436
+ columns: PortalField[];
437
+ rows: PortalRow[];
438
+ w?: number | undefined;
439
+ h?: number | undefined;
440
+ gx?: number | undefined;
441
+ gy?: number | undefined;
442
+ } | {
443
+ kind: "text";
444
+ title?: string | undefined;
445
+ body: string;
446
+ w?: number | undefined;
447
+ h?: number | undefined;
448
+ gx?: number | undefined;
449
+ gy?: number | undefined;
450
+ };
451
+ /** One table a dashboard can draw from. */
452
+ interface DataSource {
453
+ fields: PortalField[];
454
+ rows: PortalRow[];
455
+ }
456
+ /**
457
+ * Resolve a whole spec against the data. The result is render-ready and free of widgets that would
458
+ * draw nothing.
459
+ *
460
+ * `fields`/`rows` are the PRIMARY source — what a widget binds to when it names none, and what every
461
+ * dashboard used to be limited to. `others` adds further tables by name, which is what lets one
462
+ * dashboard mix them: a widget's `source` picks which table it reads, so the posts chart and the
463
+ * spend stat can sit on the same canvas over different data.
464
+ *
465
+ * A widget naming a source we were not given is DROPPED, like any other widget that would draw
466
+ * nothing. Falling back to the primary would be worse than silence: it would draw a real chart, with
467
+ * a plausible shape, of the wrong table.
468
+ */
469
+ declare function renderModel(spec: DashboardSpec, fields: PortalField[], rows: PortalRow[], others?: Record<string, DataSource>): ResolvedWidget[];
470
+
471
+ type Band = "stat" | "chart" | "block";
472
+ type StatView = Extract<ResolvedWidget, {
473
+ kind: "stat";
474
+ }>;
475
+ type ChartView = Extract<ResolvedWidget, {
476
+ kind: "series" | "donut";
477
+ }>;
478
+ /** The dashboard grid is a fixed 6 columns wide, at every width. */
479
+ declare const GRID_COLS = 6;
480
+ declare const GRID_MAX_COLS = 6;
481
+ declare const GRID_MAX_ROWS = 4;
482
+ /** The band a resolved widget belongs to. */
483
+ declare function bandOfKind(kind: ResolvedWidget["kind"]): Band;
484
+ /** The band a spec widget TYPE belongs to — for the editor, which lays out widgets before they resolve
485
+ * (a half-configured widget still needs a cell). Mirrors bandOfKind. */
486
+ declare function bandOfType(type: WidgetType): Band;
487
+ /** A band's default cell size. */
488
+ declare function footprintOf(band: Band): {
489
+ w: number;
490
+ h: number;
491
+ };
492
+ /** A widget's effective cell size: its stored `w`×`h` (clamped to the grid), else the band default.
493
+ * The renderers and the editor both size cells through this, so a widget looks the same everywhere. */
494
+ declare function footprintWith(band: Band, w: number | undefined, h: number | undefined): {
495
+ w: number;
496
+ h: number;
497
+ };
498
+ /** A widget placed on the grid — the view-model plus its top-left cell and size in cells. */
499
+ interface PlacedWidget {
500
+ widget: ResolvedWidget;
501
+ gx: number;
502
+ gy: number;
503
+ w: number;
504
+ h: number;
505
+ }
506
+ /** Place every widget on the fixed grid: honor its stored `gx`/`gy` when set (and free), else auto-
507
+ * place it first-fit (top-to-bottom, left-to-right) at its footprint. Deterministic + collision-free,
508
+ * so a legacy spec (no positions) and a positioned one both render stably, and a stale position that
509
+ * now overlaps (e.g. after a neighbour grew) falls back to a free slot instead of stacking. */
510
+ declare function placeWidgets(resolved: ResolvedWidget[]): PlacedWidget[];
511
+ /** A dashboard split into the two things that lay out differently: square TILES (stats + charts) that
512
+ * pack into the fixed grid, and full-width BLOCKS (tables + notes) that stack UNDER the grid at their
513
+ * content height. A table's height depends on its rows, not the column width, so it can't share the
514
+ * square-cell grid without leaving slack — it flows below instead, exactly as tall as its content. */
515
+ interface DashboardLayout {
516
+ /** Stats + charts, placed on the fixed grid. */
517
+ tiles: PlacedWidget[];
518
+ /** Tables + notes, full-width and content-height, in spec order (top to bottom). */
519
+ blocks: ResolvedWidget[];
520
+ }
521
+ /** Split resolved widgets into grid tiles and stacked full-width blocks (see {@link DashboardLayout}).
522
+ * Every surface (portal, console) renders these the same way, so a dashboard reads identically. */
523
+ declare function splitLayout(resolved: ResolvedWidget[]): DashboardLayout;
524
+
525
+ type DeliverableItemKind = "file" | "link" | "text" | "table";
526
+ type DeliverableItem = {
527
+ kind: DeliverableItemKind;
528
+ value: string;
529
+ /** Version this item was delivered in, numbered from 1 (absent = v1). */
530
+ v?: number;
531
+ };
532
+ /** One delivered version — a bundle of items sent together, numbered from 1. */
533
+ type DeliverableVersion = {
534
+ v: number;
535
+ items: DeliverableItem[];
536
+ };
537
+ declare function normalizeDeliverableItem(x: unknown): DeliverableItem | null;
538
+ declare function groupDeliverableVersions(items: DeliverableItem[]): DeliverableVersion[];
539
+ /** Parse a deliverable row's items from its `data`. Tolerant of the legacy single `file` field and of
540
+ * bare-string refs; returns [] when there is nothing. */
541
+ declare function parseDeliverableItems(data: Record<string, unknown>): DeliverableItem[];
542
+ type DeliverableReviewState = "awaiting" | "accepted" | "revise";
543
+ declare function deliverableStatus(items: DeliverableItem[], publishedVersion: number, verdictById: Map<string, string>,
544
+ /** The client's verdict on the delivered TABLE (from the deliverable's response), if any. */
545
+ tableVerdict?: "" | "accepted" | "revise"): DeliverableReviewState;
546
+ /** The version the client currently sees — the EXPLICIT `published_version` pointer the operator set
547
+ * (0 = nothing published). No `done → latest` fallback: that followed the latest version, so a new
548
+ * version the team delivered would auto-appear to the client before it was published. */
549
+ declare function publishedVersionOf(data: Record<string, unknown>): number;
550
+
551
+ /** A column type. `select` is an open enum edited as text; the rest drive how a cell reads. */
552
+ type DeliverableColType = "text" | "number" | "url" | "date" | "boolean" | "select";
553
+ type DeliverableColumn = {
554
+ key: string;
555
+ label: string;
556
+ type: DeliverableColType;
557
+ /** The customer may fill this column in from their portal — their values live in the deliverable's
558
+ * `responses`, layered over the operator's cells. Absent = read-only (the default). */
559
+ clientEditable?: boolean;
560
+ };
561
+ type DeliverableRow = {
562
+ id: string;
563
+ cells: Record<string, unknown>;
564
+ };
565
+ type DeliverableTable = {
566
+ /** The operator's name for this table (e.g. "Twitter influencers"). Optional for older payloads. */
567
+ name?: string;
568
+ columns: DeliverableColumn[];
569
+ rows: DeliverableRow[];
570
+ };
571
+ /** Legacy tables used `link`/`checkbox`; the current vocabulary is `url`/`boolean`. */
572
+ declare function normalizeDeliverableColType(t: unknown): DeliverableColType;
573
+ /** Parse a table item's JSON `value` into the grid model, tolerating the legacy id-keyed shape (its
574
+ * columns carried `id` not `key`, so a legacy column keeps its cells by keying on that id). Returns
575
+ * null when `value` is not a table payload. */
576
+ declare function parseDeliverableTable(value: string): DeliverableTable | null;
577
+ /** A cell value as display text (a stored null/undefined reads as empty). */
578
+ declare function deliverableCellText(v: unknown): string;
579
+ /** Whether a `boolean` cell is checked — tolerant of a real boolean or the string forms it was stored
580
+ * as across versions. */
581
+ declare function isDeliverableChecked(v: unknown): boolean;
582
+ type TableReviewVerdict = "" | "accepted" | "revise";
583
+ type TableResponses = {
584
+ /** The client's edits to client-editable columns: rowId → columnKey → value. */
585
+ cells: Record<string, Record<string, unknown>>;
586
+ /** The client's review of the table — empty until they submit. */
587
+ verdict: TableReviewVerdict;
588
+ /** The client's comment when sending back (or a note on approve). */
589
+ comment: string;
590
+ };
591
+ declare function emptyTableResponses(): TableResponses;
592
+ /** Parse a deliverable's `responses` field (client_tasks.responses). Tolerant of the legacy bare
593
+ * rowId→colKey map (read as edits with no review yet). */
594
+ declare function parseTableResponses(value: unknown): TableResponses;
595
+ /** The value to SHOW for a cell: the customer's edit on a client-editable column, else the operator's
596
+ * authored value. */
597
+ declare function cellValue(col: DeliverableColumn, row: DeliverableRow, responses: TableResponses): unknown;
598
+ /** Merge one client cell edit into a response (immutably) — for the client's local draft. */
599
+ declare function setResponse(responses: TableResponses, rowId: string, colKey: string, value: unknown): TableResponses;
600
+ /** Set the review verdict + comment (immutably) — for the submit (approve / send back) action. */
601
+ declare function setTableReview(responses: TableResponses, verdict: TableReviewVerdict, comment: string): TableResponses;
602
+ /** A `date` cell stores YYYY-MM-DD; format it as e.g. "Aug 18, 2026" (raw string if unparseable). */
603
+ declare function formatDeliverableDate(v: string): string;
604
+
605
+ /** Extract a streamed text delta from a raw pi turn event, or null. */
606
+ declare const textDeltaOf: (event: unknown) => string | null;
607
+ /** Extract a streamed reasoning delta from a raw pi turn event, or null. */
608
+ declare const thinkingDeltaOf: (event: unknown) => string | null;
609
+ /** Extract a tool-execution start/end from a raw pi turn event, or null. */
610
+ declare const toolEventOf: (event: unknown) => HolaToolEvent | null;
611
+
612
+ /**
613
+ * A refusal this surface makes on purpose, carrying both halves of it.
614
+ *
615
+ * `toHolaError` reads only `.error` and `.retryable`, so routing these through it would keep the
616
+ * code and drop the sentence — and the sentence is the part worth showing. `insufficient_credits`
617
+ * NAMES THE SHORTFALL ("that costs 100 credits and you have 0; top up 100 more"), which is the whole
618
+ * of what the customer needs and strictly more than a portal could say for itself.
619
+ *
620
+ * The code is what a caller branches on, and the branches are not cosmetic: `insufficient_credits`
621
+ * is fixed by topping up, `stripe_not_connected` is the company's own setup and no amount of trying
622
+ * again will move it, `rate_limited` wants a pause, and a `pack_*` refusal means the list on screen
623
+ * is stale. Answering all of them with "please try again" is wrong for every one but the last.
624
+ */
625
+ declare class CreditRefused extends Error {
626
+ readonly code: string;
627
+ readonly status: number;
628
+ constructor(code: string, message: string, status: number);
629
+ }
630
+ /** One pack on sale: what it grants, and what it costs in real money.
631
+ *
632
+ * `credits` and `price` are different units and the pair is the point — 500 credits for $99 is a
633
+ * rate, not a discount. `currency` belongs to the PRICE; the balance those credits land in is
634
+ * denominated in credits and nothing else. */
635
+ type CreditPack = {
636
+ packId: string;
637
+ name: string;
638
+ credits: number;
639
+ price: number;
640
+ currency: string;
641
+ };
642
+ /** A top-up started and not finished. */
643
+ type PendingTopup = {
644
+ txnId: string;
645
+ credits: number;
646
+ packId: string | null;
647
+ /** The money is known to have arrived and only the grant is outstanding. The difference between
648
+ * "we are waiting for you" and "we are waiting for us", which a customer should never have to
649
+ * guess at. */
650
+ paid: boolean;
651
+ /** Where this top-up stands, as one value rather than two booleans a reader has to combine:
652
+ * `waiting` on the customer, `paid` on the operator, `failed` on nobody — the payment was
653
+ * declined and this is terminal. Absent from a backend older than the field, where the two
654
+ * live states are all there were; treat a missing value as `paid ? "paid" : "waiting"`. */
655
+ state?: "waiting" | "paid" | "failed";
656
+ createdAt: string;
657
+ /** The server's own sentence for the state above. Rendered rather than re-derived: a second
658
+ * account of it written portal-side is one that can disagree with the first. */
659
+ note: string;
660
+ /** Where to go to finish paying, when there is somewhere.
661
+ *
662
+ * Stripe payment links neither expire nor refuse a second visit, so the one this top-up was
663
+ * minted with is still good. Null once `paid` — the money is in, and a link back to a payment
664
+ * page invites paying twice — and null for a top-up started before the link was recorded. */
665
+ resumeUrl?: string | null;
666
+ };
667
+ /** What the customer has, what they can buy, and what they have started. */
668
+ type CreditWallet = {
669
+ balance: number;
670
+ packs: CreditPack[];
671
+ pending: PendingTopup[];
672
+ /** What the BALANCE is in, which is `credits` however the shelf happens to be priced. */
673
+ currency: string;
674
+ /** What one unit of currency is worth in credits on this company's books — 1 means 1 credit = $1.
675
+ *
676
+ * Nominal by design: it is what values a credit-priced order on the operator's money surfaces,
677
+ * NOT what any particular customer paid, because two customers can legitimately pay different
678
+ * money for the same credit — that is what a volume bonus IS. So a portal may show the balance
679
+ * in money beside it, and should not present that figure as what they spent. */
680
+ bookRate?: number;
681
+ /** Whether this company prices in credits AT ALL.
682
+ *
683
+ * Ask this, never `packs.length`. An empty list is what a company selling for money gets (it is
684
+ * never asked) and equally what a credits company that has not priced a pack yet gets — and a
685
+ * portal reading the two as one hides its credits surface from the operator who has just turned
686
+ * credits on and gone looking for it. Optional, so a portal pointed at a backend older than the
687
+ * field still works; absent, the count is the only signal left. */
688
+ sellsCredits?: boolean;
689
+ };
690
+ /** One movement of the balance, with what it left the customer on. */
691
+ type CreditEntry = {
692
+ id: string;
693
+ /** Signed: negative spent it, positive added to it. The sign IS the direction, so a portal never
694
+ * has to decide which way a row goes from its reason. */
695
+ delta: number;
696
+ /** What they were on immediately AFTER this one — computed server-side by working backwards from
697
+ * the current balance, so it cannot disagree with the number at the top of the page. */
698
+ balance: number;
699
+ /** Why: `topup`, `purchase`, or whatever an operator recorded by hand. */
700
+ reason: string;
701
+ /** The order this bought, when it bought one. Null for a top-up or an adjustment. */
702
+ orderId: string | null;
703
+ /** That order's name, when it still exists. Absent for a top-up, and absent for an order deleted
704
+ * since — a ledger row for a purchase that happened is not made wrong by that, so it falls back
705
+ * to a generic label rather than disappearing or showing a row id. */
706
+ orderName?: string;
707
+ createdAt: string;
708
+ };
709
+ /** What a balance has taken in and paid out over its whole life. Both POSITIVE — a customer reads
710
+ * "spent 1,275", not "spent −1,275". */
711
+ type CreditTotals = {
712
+ purchased: number;
713
+ spent: number;
714
+ };
715
+ type CreditHistory = {
716
+ entries: CreditEntry[];
717
+ totals: CreditTotals;
718
+ /** The same figure `wallet()` answers, returned here so a page drawing both cannot show two. */
719
+ balance: number;
720
+ };
721
+ /** A started top-up: where to pay, and what it will grant. */
722
+ type TopupStarted = {
723
+ /** The Stripe payment link. It ends on Stripe's own confirmation page and never returns here, so
724
+ * open it in a NEW TAB and leave the portal standing — and re-read the wallet when the customer
725
+ * comes back, because the credits land on the webhook and not on their return. */
726
+ url: string;
727
+ txnId: string;
728
+ credits: number;
729
+ amount: number;
730
+ currency: string;
731
+ /** This handed back a link the customer already had rather than minting another. Not an error and
732
+ * usually not worth saying out loud — the same tab opens either way. */
733
+ resumed?: boolean;
734
+ };
735
+ /** What a purchase comes back as. Every figure is the server's own: a quantity in the request is a
736
+ * request and never a price, so the amount is recomputed from the product row. */
737
+ type Purchased = {
738
+ /** The order the credits bought, ready to open at `/orders/:orderId`. */
739
+ orderId: string;
740
+ /** Credits actually taken, at the price the server recomputed. */
741
+ credits: number;
742
+ /** What is left afterwards. */
743
+ balance: number;
744
+ /** What the order is worth in money at the company's book rate — the figure that lands on the
745
+ * operator's money surfaces. NOT what the customer paid: they paid credits. */
746
+ value: number;
747
+ };
748
+ /**
749
+ * What the customer agrees to pay, and when.
750
+ *
751
+ * Carried BY THE QUOTE rather than left to the order, because accepting a total without a schedule
752
+ * is half an agreement — nobody should discover "50% up front" after saying yes — and because it is
753
+ * what lets the order's payment rows be raised from something they have actually seen.
754
+ *
755
+ * MONEY, in the same unit `total` is. Not the shelf unit the LINES are priced in: the two travel
756
+ * together in one payload and telling them apart is the whole of what `QuotedLine.price` warns
757
+ * about below.
758
+ */
759
+ type QuoteTerms = {
760
+ /** Taken before the work starts. The deposit IS the starting gun — nothing runs until it lands. */
761
+ upfront?: number;
762
+ /** Taken on delivery. ADVISORY: see `quoteSchedule`, which derives the balance as the remainder
763
+ * instead, so what a customer is shown and what they are billed cannot drift apart. */
764
+ onDelivery?: number;
765
+ /** Anything the operator wants said about the schedule, in their own words. */
766
+ note?: string;
767
+ };
768
+ /**
769
+ * One service being quoted, as the customer may see it.
770
+ *
771
+ * An ALLOWLIST assembled server-side, field by field, and not a projection of the stored item. The
772
+ * durable shape behind it carries the INTERNAL checklist, the skill row ids and the dashboard spec
773
+ * in the same object as the customer-facing `deliverables` — so the rule is that a new field
774
+ * upstream stays invisible here until somebody adds it on purpose.
775
+ */
776
+ type QuotedLine = {
777
+ name: string;
778
+ /** A line of scope in the operator's words. */
779
+ notes?: string;
780
+ /** Longer scope — what this service actually covers, when it needs a paragraph. */
781
+ content?: string;
782
+ /**
783
+ * MONEY, in the same unit `total` is — safe to print with a currency symbol.
784
+ *
785
+ * IT DID NOT USED TO BE. This was handed over in the org's SHELF unit (credits on a credits org)
786
+ * beside a `total` that was money, in a payload naming no unit at all, and no portal could fix
787
+ * that from where it stood: `5,000` next to `$8,000` reads as dollars and is wrong by the whole
788
+ * book rate. It is converted server-side now, where the rate lives, so one payload is in one
789
+ * unit.
790
+ *
791
+ * Absent is still a real answer and is NOT a price of 0 — work thrown in is an agreement like
792
+ * any other. The lines also need not add up to `total`: `total` wins, exactly as `orders.value`
793
+ * beats the sum of its services, because a bundle priced below its parts is a real deal.
794
+ */
795
+ price?: number;
796
+ /** "per month", "one-off" — how often this recurs, when it recurs. */
797
+ period?: string;
798
+ /** What they get: the client-visible deliverables this line buys. */
799
+ deliverables: string[];
800
+ };
801
+ /**
802
+ * The live quote on a 询单, or the fact that there isn't one.
803
+ *
804
+ * A UNION rather than a nullable object so "not quoted yet" cannot be read as "quoted at nothing".
805
+ * They are opposite states — one is the team still working, the other is an offer of an empty
806
+ * basket — and a portal that conflated them would put an Accept button under a quote nobody sent.
807
+ */
808
+ type InquiryQuote = {
809
+ quoted: false;
810
+ } | {
811
+ quoted: true;
812
+ /** THE VERSION IN THIS PAYLOAD, and the number `acceptInquiry` must be given. Once they have
813
+ * accepted one it is that one — what somebody signed does not change because the operator
814
+ * sent another afterwards — and until then it is the newest sent. */
815
+ version: number;
816
+ /** The newest version that EXISTS. Equal to `version` in the ordinary case; greater when they
817
+ * accepted one and the operator has sent another since, which is the only way a portal can
818
+ * know to say so. Never a thing to accept: `version` is what is on screen, and accepting
819
+ * something nobody has read is the failure the whole scheme exists to prevent. */
820
+ latest: number;
821
+ sentAt: string;
822
+ lines: QuotedLine[];
823
+ /** MONEY, whatever the lines are priced in. The one figure to print as a price. */
824
+ total: number;
825
+ terms?: QuoteTerms;
826
+ /** Which version they have ALREADY accepted; 0 for none. So a reload after accepting shows
827
+ * the agreement rather than the button again. `accepted === version` is the ordinary way a
828
+ * portal knows it is looking at the contract rather than at an offer. */
829
+ accepted: number;
830
+ };
831
+ /** One line of the payment schedule: what is owed, and what it is called. */
832
+ type QuoteScheduleLine = {
833
+ label: string;
834
+ amount: number;
835
+ };
836
+ /** The two labels the platform raises payment rows under. Stated here so a portal can show a
837
+ * customer the SAME words before they accept that they will read on the bill afterwards — a
838
+ * schedule that says "Deposit" and a bill that says "Pre-payment" is two documents about one
839
+ * agreement, and the customer is the one left reconciling them. */
840
+ declare const QUOTE_DEPOSIT_LABEL = "Pre-payment";
841
+ declare const QUOTE_BALANCE_LABEL = "After-payment";
842
+ /**
843
+ * The schedule a quote's terms come to, as lines. MONEY in, money out.
844
+ *
845
+ * THE BALANCE IS THE REMAINDER, never a second stated figure, and this is the reason the function
846
+ * exists rather than a portal reading `terms.upfront` and `terms.onDelivery` straight out. Those
847
+ * two are typed by a human into two boxes and do not have to add up to the contract; the server
848
+ * bills the remainder regardless. A portal printing both would show a customer a schedule that
849
+ * silently forgives or double-charges the difference against the bill they actually receive.
850
+ *
851
+ * It mirrors the server's own `scheduleFrom` deliberately — same clamp, same rounding, same labels
852
+ * — because the point is that the customer sees, before they say yes, exactly the lines that will
853
+ * be raised when they do. In the SDK for the reason `orderBilling` is: the second portal to write
854
+ * this by hand would inherit nothing the first one learned, and being subtly wrong here is money.
855
+ *
856
+ * No terms at all means the whole thing up front — the honest reading of "they agreed a price and
857
+ * said nothing about instalments", and it never bills more than the contract.
858
+ */
859
+ declare function quoteSchedule(total: number, terms: QuoteTerms | undefined): QuoteScheduleLine[];
860
+ type PortalCredits = {
861
+ /** Balance, packs and anything in flight — one call, because they are one question. A balance
862
+ * without the packs cannot be acted on, and a balance without the pending top-ups looks wrong to
863
+ * anyone who has just paid. */
864
+ wallet(): Promise<CreditWallet>;
865
+ /** Start buying a pack, by ID alone — the price is read from the pack row server-side, so there
866
+ * is no amount here for a client to choose. */
867
+ topup(packId: string): Promise<TopupStarted>;
868
+ /** The ledger behind the balance. Its OWN call, not part of `wallet()`: the wallet is read on
869
+ * every page that draws a balance, and hanging a fifty-row scan off it would make the cheapest
870
+ * read on this surface the most expensive one. */
871
+ history(): Promise<CreditHistory>;
872
+ /**
873
+ * Buy something from the shelf.
874
+ *
875
+ * NOT a redirect. On a credits shelf the money moved at the top-up, so this is a synchronous
876
+ * debit: it takes the credits, writes the order and its services, and answers `Purchased`.
877
+ *
878
+ * Refuses with `insufficient_credits` when they cannot cover it, and that refusal names the
879
+ * shortfall — it is the one message worth showing verbatim, because "not enough credits" is all a
880
+ * portal could say for itself.
881
+ */
882
+ checkout(purchase: {
883
+ productId: string;
884
+ variant?: string;
885
+ qty?: number;
886
+ }): Promise<Purchased>;
887
+ /**
888
+ * Ask for a quote on an `ask` product — the other way a shelf turns into an order.
889
+ *
890
+ * Writes a real order in `ask` status carrying these answers, with no services, no value and
891
+ * nothing running: the brief, waiting to be priced. Send the KEYS the product's `form` declares;
892
+ * the labels are read from the product row server-side, so a client cannot restate the question
893
+ * it is answering.
894
+ *
895
+ * On this surface rather than the shelf's for the same reason `checkout` is: an order belongs to
896
+ * somebody, so it takes the customer's bearer. (The public shelf's `apply` is the stranger's
897
+ * door, and it leaves an inquiry instead — there is no customer to write an order against.)
898
+ */
899
+ /** Raise a 询单 on an `ask` product. Returns the inquiry it created, so the caller can open it.
900
+ *
901
+ * It used to return `orderId`: an enquiry wrote an ORDER in `ask` status, which was the wrong
902
+ * object — an order with no money, no services and no agreed price, and a dead row in the
903
+ * customer's order list for every quote that went nowhere. */
904
+ enquire(
905
+ /** The `ask` product this is about. EMPTY for a 询单 raised about something that is not on the
906
+ * shelf — "can you also do X", which is how most of them start. */
907
+ productId: string, answers: {
908
+ key: string;
909
+ value: string;
910
+ }[],
911
+ /** What THEY say they need, in their words. Required when `productId` is empty and ignored when
912
+ * it is not: a product's own name is snapshotted server-side so a later rename cannot rewrite
913
+ * what somebody asked for. */
914
+ name?: string): Promise<{
915
+ inquiryId: string;
916
+ name: string;
917
+ }>;
918
+ /**
919
+ * The quote on a 询单: the services being offered, at the prices being offered.
920
+ *
921
+ * ITS OWN ROUTE, not a field on the inquiry row, and that is load-bearing rather than tidy. The
922
+ * spine's read permission is per TABLE and never per column, so a basket parked on the
923
+ * externally-visible `inquiries` row would hand the customer the internal checklist, the skill
924
+ * row ids and the dashboard spec along with their price. The server reads it as the org and
925
+ * projects an allowlist; there is one door, and `QuotedLine` is the whole of what fits through it.
926
+ *
927
+ * `{quoted: false}` is a REAL answer and not an error — nobody has sent anything yet, which is
928
+ * what a portal draws "we're working on your proposal" from. It is also what an inquiry quoted
929
+ * the old way answers, so a portal reading this keeps working against one.
930
+ *
931
+ * WHAT THEY SIGNED WINS over what has been sent since: once a version is accepted this returns
932
+ * THAT one, with `latest` naming the newest that exists. Before, it always returned the newest
933
+ * and left `accepted` as a bare number — so a customer whose operator revised after they said
934
+ * yes had a screen that knew which version was the contract and displayed a different one.
935
+ */
936
+ inquiryQuote(inquiryId: string): Promise<InquiryQuote>;
937
+ /**
938
+ * Accept the quote on a 询单 — and, when there is one, get the order written from it.
939
+ *
940
+ * THE VERSION IS REQUIRED, and deliberately not defaulted to the latest. Defaulting would make
941
+ * the stale case unrepresentable: a tab left open on v1 would silently agree to whatever price is
942
+ * current, which is the exact failure versioning exists to prevent. Pass the `version` off the
943
+ * quote that was actually on screen; pass 0 for an inquiry with no quote, which keeps today's
944
+ * behaviour (it moves to `accepted` and the operator writes the order by hand).
945
+ *
946
+ * Idempotent — `changed: false` means they had already accepted, which is what a double-tap on a
947
+ * slow connection looks like and is not an error.
948
+ *
949
+ * Refuses with `CreditRefused`, and the code says which of three things happened. Each wants a
950
+ * different sentence, which is why they are three:
951
+ *
952
+ * - `quote_revised` — the version is no longer the current one. The ONLY correct response is to
953
+ * re-read `inquiryQuote` and show what came back: retrying with the version named in the
954
+ * refusal is the stale accept again, one round trip later. (Which is why that number is not
955
+ * surfaced here — the re-read answers it, and answers it as of now rather than as of then.)
956
+ * - `not_quoted` — nobody has sent anything. There is nothing to accept, and there is nothing
957
+ * for the customer to do but wait.
958
+ * - `inquiry_closed` — it is already closed, or already an order. Someone else moved it, so the
959
+ * page should re-read rather than argue.
960
+ */
961
+ acceptInquiry(inquiryId: string, version: number): Promise<{
962
+ status: string;
963
+ changed: boolean;
964
+ }>;
965
+ /** Every payment on this customer's orders, due and paid both — what they owe, on what, and by
966
+ * when. Join to an order on `order`.
967
+ *
968
+ * On this surface rather than portal-data because it is NOT a table read: `payments` is not
969
+ * externally visible, and the server projects an allowlist of bill fields off the row. */
970
+ payments(): Promise<{
971
+ payments: CustomerPayment[];
972
+ }>;
973
+ /** How this company takes a bank transfer. `null` when they publish no details — Stripe is not
974
+ * the only way money moves, and for an operator banking with Mercury it is not a way at all. */
975
+ paymentMethods(): Promise<{
976
+ transfer: TransferDetails | null;
977
+ }>;
978
+ /** "I've sent the transfer." Records the customer's claim against one payment and puts it on the
979
+ * operator's desk to confirm against the account.
980
+ *
981
+ * It does NOT settle the payment, and a portal must not present it as though it does: work
982
+ * starts on payment, and a claim is not a receipt. `claimed: false` with `status: "paid"` means
983
+ * the operator had already confirmed it. */
984
+ markPaymentSent(paymentId: string, reference?: string): Promise<{
985
+ claimed: boolean;
986
+ status: string;
987
+ }>;
988
+ };
989
+ /** One line on a customer's bill. Mirrors the projection holaapp-backend's `customerPayment`
990
+ * emits — an allowlist, not the stored row. */
991
+ type CustomerPayment = {
992
+ id: string;
993
+ /** The order this runs against. */
994
+ order: string;
995
+ /** "Pre-payment", "After-payment", "Paid in full" — whatever the operator labelled it. */
996
+ label: string;
997
+ amount: number;
998
+ /** Anything the server could not read as `paid` comes back as `due`. */
999
+ status: "due" | "paid";
1000
+ method: string;
1001
+ dueDate: string;
1002
+ paidAt: string;
1003
+ /** Where to pay this online, when the money can be taken that way. Empty for a bank transfer,
1004
+ * which is not a link but a set of details plus a person confirming it arrived. */
1005
+ payUrl: string;
1006
+ /** When the customer said they had sent it — their claim, not a receipt. Set by `markPaymentSent`
1007
+ * and cleared by nothing: it is the record of what was said, and the operator confirming the
1008
+ * money is what changes `status`. */
1009
+ claimedAt: string;
1010
+ reference: string;
1011
+ };
1012
+ /** How to pay this company by transfer. Null when the operator has published nothing you could
1013
+ * actually send money to — no details is not an empty form, it is no transfer option. */
1014
+ type TransferDetails = {
1015
+ bank: string;
1016
+ accountName: string;
1017
+ accountNumber: string;
1018
+ routing: string;
1019
+ swift: string;
1020
+ note: string;
1021
+ };
1022
+ /** Where one order stands financially, and therefore whether it has started. */
1023
+ type OrderBilling = {
1024
+ /** The agreed contract value. 0 when nobody priced it — not the same as free, which is what
1025
+ * `priced` is for. */
1026
+ value: number;
1027
+ priced: boolean;
1028
+ /** Raised against this order, settled or not. Can exceed `value` on a renegotiated one: it is a
1029
+ * record of what was billed, not a derivation of the contract. */
1030
+ billed: number;
1031
+ paid: number;
1032
+ /** Still owed. Falls back to the whole contract value when the order is priced but no line has
1033
+ * been raised yet — the ordinary case the moment an order is written, where the customer is owed
1034
+ * a figure rather than a shrug. Never negative. */
1035
+ due: number;
1036
+ /** The lines, what is owed first. */
1037
+ lines: CustomerPayment[];
1038
+ /**
1039
+ * Whether the work is waiting on money — and so has not started.
1040
+ *
1041
+ * PRICED AND NOTHING PAID. Not "billed and unsettled", which is wrong in the ordinary case: an
1042
+ * order is written with a value and no payment lines until somebody raises one, so a customer
1043
+ * looking at a fresh contract would be told it was under way while the operator's console badged
1044
+ * it Unpaid. This matches the operator side's rule exactly — any PAID line means started — which
1045
+ * is what stops the two describing one order in opposite terms.
1046
+ */
1047
+ awaitingPayment: boolean;
1048
+ };
1049
+ /**
1050
+ * Fold an order's payment lines into where it stands.
1051
+ *
1052
+ * In the SDK rather than in each portal for the reason the whole of this file is: every portal that
1053
+ * shows a bill needs exactly this, and the second one to write it by hand would inherit nothing the
1054
+ * first one learned. Pure, so the arithmetic that decides whether somebody is told "this hasn't
1055
+ * started" is testable without a server.
1056
+ */
1057
+ declare const orderBilling: (order: {
1058
+ value: number;
1059
+ }, all: CustomerPayment[], orderId: string) => OrderBilling;
1060
+ declare const createPortalCredits: (config: ApiTransportConfig) => PortalCredits;
1061
+
1062
+ type PortalShopConfig = {
1063
+ /** API origin, e.g. "https://api.holaos.ai" — or empty for a portal whose own worker answers
1064
+ * `/api/v1/end-user/…` same-origin, which is how the iMerch template is wired. */
1065
+ baseUrl: string;
1066
+ /**
1067
+ * The tenant's publishable key, which is the whole of the authentication here.
1068
+ *
1069
+ * A plain string rather than the getter `ApiTransportConfig.token` takes: an end-user token
1070
+ * refreshes and a publishable key does not — it is baked into the page source a visitor can read,
1071
+ * which is what makes it publishable. There is deliberately no token field on this config at all,
1072
+ * so nothing in a storefront can come to depend on being signed in to browse.
1073
+ */
1074
+ publishableKey: string;
1075
+ };
1076
+ /** How a product is taken: `buy` charges for it, `ask` opens the conversation the customer already
1077
+ * has with the company, `apply` posts the form `apply()` sends. */
1078
+ type ShopMode = "buy" | "ask";
1079
+ /**
1080
+ * What a product costs — exactly one of four shapes, never two.
1081
+ *
1082
+ * The number is in whatever unit the OPERATOR prices in: credits for an org that sells them, money
1083
+ * for one that does not — and the wire says which, on `ShopProduct.currency` beside this. It used
1084
+ * to say otherwise, that the figure travelled bare and the unit "belongs to the template that knows
1085
+ * the tenant", which was how this contract was written before `pricing_unit` existed and was false
1086
+ * the day the server started sending the field. A template built on that sentence prints a
1087
+ * hardcoded `$` in front of a credit price — wrong by the whole book rate. Nothing here should ever
1088
+ * print a `$` it did not read off `currency`.
1089
+ *
1090
+ * `quoted` is not a price. It is the honest answer for everything in a real catalogue that reads
1091
+ * `5k – 10k`, `20k+` or 待定 — a statement that a human has to quote this. Render it as words, and
1092
+ * never behind a Buy button that cannot compute a total.
1093
+ */
1094
+ type ShopPricing = {
1095
+ kind: "flat";
1096
+ price: number;
1097
+ } | {
1098
+ kind: "per_unit";
1099
+ unit: string;
1100
+ unitPrice: number;
1101
+ unitStep: number;
1102
+ } | {
1103
+ kind: "variants";
1104
+ variants: {
1105
+ name: string;
1106
+ price: number | null;
1107
+ }[];
1108
+ } | {
1109
+ kind: "quoted";
1110
+ };
1111
+ /** One part of a bundle, as the shelf is allowed to describe it: what it is called, what it covers,
1112
+ * how long it runs — and no price, for the reason at the top of this file. */
1113
+ type ShopInclusion = {
1114
+ name: string;
1115
+ content?: string;
1116
+ period?: string;
1117
+ };
1118
+ /** A named tier — 基础 / 标准 / 高阶 — and what it covers. Its PRICE is not here: it is in
1119
+ * `pricing` (`kind: "variants"`), matched to this by name, so that there is one place the money
1120
+ * comes from and no way for the two to disagree. */
1121
+ /** One question an `ask` product asks before it can be quoted. Authored by the operator on the
1122
+ * product row; the storefront draws it and sends back answers keyed to it. */
1123
+ type ShopFormField = {
1124
+ key: string;
1125
+ label: string;
1126
+ type: "text" | "textarea" | "number" | "date" | "select";
1127
+ required?: boolean;
1128
+ options?: string[];
1129
+ help?: string;
1130
+ };
1131
+ type ShopVariant = {
1132
+ name: string;
1133
+ includes: ShopInclusion[];
1134
+ };
1135
+ /** One product as a visitor sees it. */
1136
+ type ShopProduct = {
1137
+ id: string;
1138
+ name: string;
1139
+ summary: string;
1140
+ description: string;
1141
+ /**
1142
+ * One of `ShopMode` for anything authored in the console — but typed as the string it is on the
1143
+ * wire, because the server sends the row's own value and a product from before the field existed
1144
+ * answers `""`. A storefront that switched over the union would be handed a value the type swore
1145
+ * could not occur, so match the three and keep a default arm.
1146
+ */
1147
+ mode: string;
1148
+ category: string;
1149
+ platform: string;
1150
+ cadence: string;
1151
+ period: string;
1152
+ /** Image URLs for the card and the product page; empty when the operator set none. */
1153
+ media: string[];
1154
+ pricing: ShopPricing;
1155
+ /** What THIS product's numbers are in — an ISO code, or the literal "credits". It sits on the
1156
+ * product rather than on the shelf because a shelf can be mixed: once an org sells credits, its
1157
+ * buyable products are priced in them while a 20k engagement it merely quotes stays in money.
1158
+ * Empty from a backend older than the field, which is a shelf that has not SAID — and is not the
1159
+ * same as one that said dollars, so print the bare figure rather than guessing a symbol. */
1160
+ currency: string;
1161
+ /** Bounds on a per-unit purchase, when the product states them. Public because the quantity
1162
+ * picker has to honour them — a customer who meets a limit they were never shown reads it as a
1163
+ * broken shop. */
1164
+ minQty?: number;
1165
+ maxQty?: number;
1166
+ includes: ShopInclusion[];
1167
+ /** The enquiry form, on an `ask` product only. Never empty when present — a product that asks
1168
+ * nothing still gets one box, because a Send button over nothing collects an enquiry that says
1169
+ * only that somebody was interested. Absent on `buy` and `apply`, which are not asked. */
1170
+ form?: ShopFormField[];
1171
+ /** Present only where the tiers differ in scope; a product without them has one implicit tier,
1172
+ * which is its own `pricing` and its own `includes`. */
1173
+ variants?: ShopVariant[];
1174
+ };
1175
+ type ShopShelf = {
1176
+ products: ShopProduct[];
1177
+ /**
1178
+ * The catalogue is bigger than what came back.
1179
+ *
1180
+ * Kept rather than dropped for the reason `listRows` returns a page: the server caps a shelf read,
1181
+ * and a shop that quietly ends at the cap is a customer who cannot see half of it and nobody who
1182
+ * can tell. The same argument the server makes when it bothers to send the flag.
1183
+ */
1184
+ truncated: boolean;
1185
+ /** What an enquiry with NO product asks — "can you also do X", which has no product row to carry
1186
+ * questions. The operator's own defaults; empty when they have set none, which a storefront
1187
+ * reads as one box ("What do you need?"). A product's `form` is already resolved against these
1188
+ * server-side, so a card never has to merge the two itself. */
1189
+ enquiryForm: ShopFormField[];
1190
+ };
1191
+ /** An application against a product, or against nothing in particular — a general enquiry that
1192
+ * names no product is a real thing to receive, so `productId` is optional. */
1193
+ type ShopApplication = {
1194
+ name: string;
1195
+ /**
1196
+ * Where the operator answers. Required, and checked by the server — but a malformed one is
1197
+ * dropped SILENTLY (see `apply` below), so a form that wants to tell someone their address is
1198
+ * wrong has to say so itself, before it sends.
1199
+ */
1200
+ email: string;
1201
+ phone?: string;
1202
+ message?: string;
1203
+ productId?: string;
1204
+ };
1205
+ /** The one answer every application gets. See `apply` for why it never varies. */
1206
+ type ShopApplyResult = {
1207
+ /**
1208
+ * Always `true` — including for a submission that was dropped. It is a receipt that the request
1209
+ * was accepted, never evidence that a row exists.
1210
+ */
1211
+ ok: boolean;
1212
+ /** The server's own thank-you line. Shown as sent rather than reworded, so the operator's copy is
1213
+ * the copy the applicant reads. */
1214
+ message: string;
1215
+ };
1216
+ type PortalShop = {
1217
+ /** Everything on sale, in the operator's own order. Listed products only — a draft is something
1218
+ * still being written and an archived one is off sale but kept because orders point at it. */
1219
+ listProducts(): Promise<ShopShelf>;
1220
+ /**
1221
+ * Send an application. No account, no session — this is the one write on the platform a total
1222
+ * stranger can make.
1223
+ *
1224
+ * It answers the SAME "thanks, we have your details" to an unknown key, a body that did not
1225
+ * parse and a write that failed as it does to an application that landed, and that is deliberate:
1226
+ * anything else would turn this into a way to ask which publishable keys reach a real company —
1227
+ * and, once signups are invite-only, which addresses one already has. The cost is that a
1228
+ * miswired form gets a cheerful reply and files nothing, so when applications are not arriving,
1229
+ * the answer is in the operator's server logs and cannot be here.
1230
+ */
1231
+ apply(application: ShopApplication): Promise<ShopApplyResult>;
1232
+ };
1233
+ declare const createPortalShop: (config: PortalShopConfig) => PortalShop;
1234
+
291
1235
  /** Provides a `HolaClient` (from `createHolaClient`) to `useChat` / `<Chat/>`.
292
1236
  * Create the client once (e.g. `useMemo`) so its session persists across renders. */
293
1237
  declare const HolaProvider: ({ client, children, }: {
294
1238
  client: HolaClient;
295
1239
  children: ReactNode;
296
- }) => react_jsx_runtime.JSX.Element;
1240
+ }) => react.JSX.Element;
297
1241
  declare const useHolaClient: () => HolaClient;
298
1242
 
1243
+ /**
1244
+ * How this product is taken.
1245
+ *
1246
+ * The wire carries the row's own value rather than the union, because a product authored before
1247
+ * the field existed answers `""`. A shelf that switched on the three names and fell through would
1248
+ * render a product with no action at all, so an unrecognised mode becomes `ask`: it is the one
1249
+ * path that needs no money, writes nothing, and always has somebody at the other end of it.
1250
+ */
1251
+ declare function modeOf(product: ShopProduct): ShopMode;
1252
+ /** The price that applies once a tier has been chosen. A tiered product is not a price until then;
1253
+ * every other shape already was one. */
1254
+ type ChosenPrice = {
1255
+ kind: "flat";
1256
+ price: number;
1257
+ } | {
1258
+ kind: "per_unit";
1259
+ unit: string;
1260
+ unitPrice: number;
1261
+ unitStep: number;
1262
+ } | {
1263
+ kind: "quoted";
1264
+ };
1265
+ /**
1266
+ * The figure the shelf carries, printed in the unit the ORG prices in.
1267
+ *
1268
+ * `currency` comes off the shelf payload and from nowhere else. It used to be a line of portal
1269
+ * config on the portal itself, which is a portal guessing at somebody else's rate card: the
1270
+ * numbers belong to the org and so does what they MEAN, and those two travelling separately is how
1271
+ * a catalogue ends up wrong by an exchange rate. An ISO code prints money; the literal "credits"
1272
+ * prints credits, which is the other unit an operator can price in.
1273
+ *
1274
+ * A shelf that names no unit prints the bare figure. That is a backend older than the field, and an
1275
+ * ambiguous number is a far smaller lie than a confident "$" in front of a price in RMB.
1276
+ *
1277
+ * An unusable code is caught rather than left to throw: one bad value would otherwise raise a
1278
+ * RangeError from inside a price label and take the whole shelf down with it.
1279
+ */
1280
+ declare function money(amount: number, currency: string): string;
1281
+ /** What a customer is quoted, in words. `quoted` is deliberately not a number: it is the honest
1282
+ * answer for a catalogue line that reads `20k+` or 待定, and it must never sit behind a button
1283
+ * that cannot compute a total. */
1284
+ declare function priceLabel(price: ChosenPrice, currency: string): string;
1285
+ /** The one line a card shows about money. A tiered product is a RANGE until a tier is picked, so
1286
+ * the card says "From" and the product page says which — a card that printed one tier's figure
1287
+ * would be quoting a price that half the buyers do not pay. */
1288
+ declare function shelfPriceLabel(product: ShopProduct): string;
1289
+ /** The chosen tier's price, matched to it BY NAME — the tier's scope and the tier's money are two
1290
+ * fields on the wire, and this is the join. A tier nobody priced is quoted, which is a real state:
1291
+ * 基础 / 标准 published, 高阶 still being costed. */
1292
+ declare function priceFor(pricing: ShopPricing, variant: string | null): ChosenPrice;
1293
+ /** What the customer would pay for `qty` units, or null where there is no total to compute. Steps,
1294
+ * not units: 50 upvotes at a time is what is actually sold, so a request for 60 is charged as two
1295
+ * steps — the same arithmetic the server does when it recomputes this from the row. */
1296
+ declare function totalFor(price: ChosenPrice, qty: number): number | null;
1297
+ /** How much of a per-unit product may be bought, and in what increments. The bounds are the
1298
+ * product's own, and they are shown as well as enforced: a customer who hits a limit they were
1299
+ * never told about reads it as a broken shop rather than a rule. */
1300
+ declare function quantityRange(product: ShopProduct, price: ChosenPrice): {
1301
+ min: number;
1302
+ max: number | null;
1303
+ step: number;
1304
+ };
1305
+ /**
1306
+ * What the chosen tier covers.
1307
+ *
1308
+ * A tier with nothing listed falls back to the product's own inclusions rather than showing an
1309
+ * empty list. That is not a guess: `variants` is authored only where the tiers DIFFER in scope, so
1310
+ * a tier that lists nothing is one that adds nothing to what the product already says it includes —
1311
+ * and an empty "What's included" reads as a bundle containing nothing at all.
1312
+ */
1313
+ declare function includesFor(product: ShopProduct, variant: string | null): ShopInclusion[];
1314
+ /** The values a facet actually has on this shelf, in the operator's own order. Not sorted: `sort`
1315
+ * on the product rows is a decision somebody made about what to show first, and re-ordering these
1316
+ * alphabetically throws it away. Products with the facet unset are simply not counted. */
1317
+ declare function facetValues(products: ShopProduct[], of: (product: ShopProduct) => string): string[];
1318
+ /** The message a "buy this" enquiry sends when a portal cannot take the money itself.
1319
+ *
1320
+ * Named rather than described by id: an operator reads this in the same thread as everything else,
1321
+ * and a row id is not a thing anyone can answer. It is a SENTENCE and not a product because the
1322
+ * same text opens the apply form for a signed-out visitor — a request that read differently
1323
+ * depending on which door it came through would be two requests to reconcile.
1324
+ */
1325
+ declare function buyRequestText(product: ShopProduct, variant: string | null, price: ChosenPrice, qty: number): string;
1326
+ declare function shelfProblem(error: unknown): string | null;
1327
+
299
1328
  type ChatMessage = {
300
1329
  id: number;
301
1330
  role: "user" | "assistant";
@@ -314,6 +1343,45 @@ type UseChat = {
314
1343
  /** Chat state for one employee client: greeting, message list, streaming send. */
315
1344
  declare const useChat: (client: HolaClient) => UseChat;
316
1345
 
1346
+ type UseCredits = {
1347
+ wallet: CreditWallet | null;
1348
+ /** The FIRST read only. A refresh keeps the numbers on screen and swaps them when the answer
1349
+ * lands: blanking a balance somebody is reading, to show a spinner, to show the same balance
1350
+ * again, is a flicker that reads as a fault. */
1351
+ loading: boolean;
1352
+ failure: string | null;
1353
+ refresh: () => void;
1354
+ };
1355
+ declare const useCredits: (credits: PortalCredits) => UseCredits;
1356
+
1357
+ type UseShop = {
1358
+ products: ShopProduct[];
1359
+ /** What an enquiry asks when the product has authored nothing of its own — and the WHOLE form
1360
+ * for somebody asking about something not on the shelf, which has no product to carry questions
1361
+ * at all. The org's, set in the console. Empty is a real answer: it means one box. */
1362
+ enquiryForm: ShopFormField[];
1363
+ /** In flight — the first read, and every `reload`. The products from the previous read stay put
1364
+ * underneath it, so a refresh does not blank a shop that is already on screen. */
1365
+ loading: boolean;
1366
+ /** The catalogue is bigger than `products`; the server capped the read. */
1367
+ truncated: boolean;
1368
+ /** Why the last read failed, as the SDK's own code — `rate_limited` is the one worth wording
1369
+ * differently, since it is the only failure that fixes itself. */
1370
+ error: string | null;
1371
+ reload: () => void;
1372
+ };
1373
+ /**
1374
+ * The public shelf, loaded on mount.
1375
+ *
1376
+ * Hold `shop` still: build it once at module scope, the way a portal builds its client (`export
1377
+ * const holaShop = createPortalShop(config)`), and hand the same object in on every render. A
1378
+ * `createPortalShop({...})` written inside the component body is a new object each time, and since
1379
+ * that identity is what this re-reads on, the storefront would fetch its own shelf in a loop —
1380
+ * against a route that is rate-limited per visitor, so the shop would end up empty for the person
1381
+ * least at fault.
1382
+ */
1383
+ declare const useShop: (shop: PortalShop) => UseShop;
1384
+
317
1385
  /** A thread message; `pending` marks an optimistic echo of the visitor's own send, shown
318
1386
  * instantly and replaced by the authoritative message when the receive channel delivers it. */
319
1387
  type ThreadMessage = HolaMessage & {
@@ -327,9 +1395,6 @@ type UseThread = {
327
1395
  sending: boolean;
328
1396
  error: string | null;
329
1397
  };
330
- /** Merge one incoming thread message: dedupe by id, and replace a pending optimistic echo of the
331
- * visitor's own send (same text) with the authoritative one. Pure — the hook's core, so the
332
- * merge rule is unit-checkable without React. */
333
1398
  declare function mergeMessage(list: ThreadMessage[], incoming: HolaMessage): ThreadMessage[];
334
1399
  /** A poll-based UNIFIED thread: the whole conversation — visitor, agent, and HUMAN turns — as one
335
1400
  * live list via the receive channel (`client.subscribe`). Unlike `useChat` (streaming, one turn
@@ -340,4 +1405,4 @@ declare const useThread: (client: HolaClient, options?: {
340
1405
  pollMs?: number;
341
1406
  }) => UseThread;
342
1407
 
343
- export { type ApiTransportConfig, Chat, type ChatMessage, type CreateHolaClientOptions, type HolaArtifact, type HolaAuthor, type HolaClient, type HolaCompletion, type HolaEmployeeInfo, type HolaError, type HolaIdentity, type HolaMessage, HolaProvider, type HolaStreamHandlers, type HolaSubscribeHandlers, type HolaSubscribeOptions, type HolaToolEvent, type HolaTransport, PortalConflict, type PortalData, type PortalDocument, type PortalDocumentSummary, type PortalField, type PortalFile, type PortalRow, type PortalRowPage, type PortalTable, type ThreadMessage, type UseChat, type UseThread, type WidgetTransportConfig, createApiTransport, createHolaClient, createPortalData, createWidgetTransport, mergeMessage, textDeltaOf, thinkingDeltaOf, toolEventOf, useArtifactUrl, useChat, useHolaClient, useThread };
1408
+ export { type Agg, type ApiTransportConfig, type Band, type ChartView, Chat, type ChatMessage, type ChosenPrice, type CreateHolaClientOptions, type CreditEntry, type CreditHistory, type CreditPack, CreditRefused, type CreditTotals, type CreditWallet, type CustomerPayment, type DashboardLayout, type DashboardSpec, type DataSource, type DeliverableColType, type DeliverableColumn, type DeliverableItem, type DeliverableItemKind, type DeliverableReviewState, type DeliverableRow, type DeliverableTable, type DeliverableVersion, type DonutWidget, GRID_COLS, GRID_MAX_COLS, GRID_MAX_ROWS, type HolaArtifact, type HolaAuthor, type HolaClient, type HolaCompletion, type HolaEmployeeInfo, type HolaError, type HolaIdentity, type HolaMessage, HolaProvider, type HolaStreamHandlers, type HolaSubscribeHandlers, type HolaSubscribeOptions, type HolaToolEvent, type HolaTransport, type InquiryQuote, type OrderBilling, type PendingTopup, type PlacedWidget, type Point, PortalConflict, type PortalCredits, type PortalData, type PortalDocument, type PortalDocumentSummary, type PortalField, type PortalFile, type PortalRow, type PortalRowAuthor, type PortalRowPage, type PortalShop, type PortalShopConfig, type PortalTable, type Purchased, QUOTE_BALANCE_LABEL, QUOTE_DEPOSIT_LABEL, type QuoteScheduleLine, type QuoteTerms, type QuotedLine, type ResolvedWidget, type SeriesWidget, type ShopApplication, type ShopApplyResult, type ShopFormField, type ShopInclusion, type ShopMode, type ShopPricing, type ShopProduct, type ShopShelf, type ShopVariant, type StatView, type StatWidget, type TableResponses, type TableReviewVerdict, type TableWidget, type TextWidget, type ThreadMessage, type TopupStarted, type TransferDetails, type UseChat, type UseCredits, type UseShop, type UseThread, type Widget, type WidgetTransportConfig, type WidgetType, bandOfKind, bandOfType, buyRequestText, cellValue, createApiTransport, createHolaClient, createPortalCredits, createPortalData, createPortalShop, createWidgetTransport, deliverableCellText, deliverableStatus, deriveFields, emptyTableResponses, facetValues, footprintOf, footprintWith, formatDeliverableDate, groupDeliverableVersions, includesFor, inferSpec, isDeliverableChecked, mergeMessage, modeOf, money, normalizeDeliverableColType, normalizeDeliverableItem, orderBilling, parseDashboardSpec, parseDeliverableItems, parseDeliverableTable, parseTableResponses, placeWidgets, priceFor, priceLabel, publishedVersionOf, quantityRange, quoteSchedule, renderModel, setResponse, setTableReview, shelfPriceLabel, shelfProblem, splitLayout, textDeltaOf, thinkingDeltaOf, toolEventOf, totalFor, useArtifactUrl, useChat, useCredits, useHolaClient, useShop, useThread };