@holaboss/client 0.2.0-beta.3 → 0.2.0-beta.31
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.cjs +5 -5
- package/dist/index.d.cts +1240 -19
- package/dist/index.d.ts +1240 -19
- package/dist/index.js +5 -5
- package/package.json +6 -4
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as
|
|
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: () =>
|
|
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
|
-
/**
|
|
248
|
-
*
|
|
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,1189 @@ 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
|
+
/**
|
|
631
|
+
* "You cannot cover this" — carrying the three numbers that make it a QUOTE rather than a refusal.
|
|
632
|
+
*
|
|
633
|
+
* A SUBCLASS, not a second class, and that is load-bearing: every caller in every portal already
|
|
634
|
+
* branches on `instanceof CreditRefused` and reads `.code`, and a sibling class would fall out of
|
|
635
|
+
* all of them at once — the exact failure the comment above `CreditRefused` warns about, arriving
|
|
636
|
+
* from the other direction. Anything that handled the refusal before goes on handling it; the three
|
|
637
|
+
* numbers are additive.
|
|
638
|
+
*
|
|
639
|
+
* `short` is `credits - balance`, computed server-side, because the two figures it is derived from
|
|
640
|
+
* are read there under the same lock the debit takes. A portal recomputing it from a balance it
|
|
641
|
+
* fetched a minute ago would show a shortfall that has already been topped up.
|
|
642
|
+
*
|
|
643
|
+
* Minted ONLY when the server actually sent the numbers. Two surfaces answer 402
|
|
644
|
+
* `insufficient_credits` — settling a bill and buying from the shelf — and only the first sends
|
|
645
|
+
* them, so a shop refusal stays a plain `CreditRefused` rather than one carrying three zeros
|
|
646
|
+
* dressed as facts.
|
|
647
|
+
*/
|
|
648
|
+
declare class InsufficientCredits extends CreditRefused {
|
|
649
|
+
/** What the line costs, in credits. ROUNDED UP: see `creditsOfMoney`. */
|
|
650
|
+
readonly credits: number;
|
|
651
|
+
/** What they have right now. */
|
|
652
|
+
readonly balance: number;
|
|
653
|
+
/** The difference, and the size of the top-up that would fix it. */
|
|
654
|
+
readonly short: number;
|
|
655
|
+
constructor(message: string, figures: {
|
|
656
|
+
credits: number;
|
|
657
|
+
balance: number;
|
|
658
|
+
short: number;
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
/** One pack on sale: what it grants, and what it costs in real money.
|
|
662
|
+
*
|
|
663
|
+
* `credits` and `price` are different units and the pair is the point — 500 credits for $99 is a
|
|
664
|
+
* rate, not a discount. `currency` belongs to the PRICE; the balance those credits land in is
|
|
665
|
+
* denominated in credits and nothing else. */
|
|
666
|
+
type CreditPack = {
|
|
667
|
+
packId: string;
|
|
668
|
+
name: string;
|
|
669
|
+
credits: number;
|
|
670
|
+
price: number;
|
|
671
|
+
currency: string;
|
|
672
|
+
};
|
|
673
|
+
/** A top-up started and not finished. */
|
|
674
|
+
type PendingTopup = {
|
|
675
|
+
txnId: string;
|
|
676
|
+
credits: number;
|
|
677
|
+
packId: string | null;
|
|
678
|
+
/** The money is known to have arrived and only the grant is outstanding. The difference between
|
|
679
|
+
* "we are waiting for you" and "we are waiting for us", which a customer should never have to
|
|
680
|
+
* guess at. */
|
|
681
|
+
paid: boolean;
|
|
682
|
+
/** Where this top-up stands, as one value rather than two booleans a reader has to combine:
|
|
683
|
+
* `waiting` on the customer, `paid` on the operator, `failed` on nobody — the payment was
|
|
684
|
+
* declined and this is terminal. Absent from a backend older than the field, where the two
|
|
685
|
+
* live states are all there were; treat a missing value as `paid ? "paid" : "waiting"`. */
|
|
686
|
+
state?: "waiting" | "paid" | "failed";
|
|
687
|
+
createdAt: string;
|
|
688
|
+
/** The server's own sentence for the state above. Rendered rather than re-derived: a second
|
|
689
|
+
* account of it written portal-side is one that can disagree with the first. */
|
|
690
|
+
note: string;
|
|
691
|
+
/** Where to go to finish paying, when there is somewhere.
|
|
692
|
+
*
|
|
693
|
+
* Stripe payment links neither expire nor refuse a second visit, so the one this top-up was
|
|
694
|
+
* minted with is still good. Null once `paid` — the money is in, and a link back to a payment
|
|
695
|
+
* page invites paying twice — and null for a top-up started before the link was recorded. */
|
|
696
|
+
resumeUrl?: string | null;
|
|
697
|
+
};
|
|
698
|
+
/** What the customer has, what they can buy, and what they have started. */
|
|
699
|
+
type CreditWallet = {
|
|
700
|
+
balance: number;
|
|
701
|
+
packs: CreditPack[];
|
|
702
|
+
pending: PendingTopup[];
|
|
703
|
+
/** What the BALANCE is in, which is `credits` however the shelf happens to be priced. */
|
|
704
|
+
currency: string;
|
|
705
|
+
/** What one unit of currency is worth in credits on this company's books — 1 means 1 credit = $1.
|
|
706
|
+
*
|
|
707
|
+
* Nominal by design: it is what values a credit-priced order on the operator's money surfaces,
|
|
708
|
+
* NOT what any particular customer paid, because two customers can legitimately pay different
|
|
709
|
+
* money for the same credit — that is what a volume bonus IS. So a portal may show the balance
|
|
710
|
+
* in money beside it, and should not present that figure as what they spent. */
|
|
711
|
+
bookRate?: number;
|
|
712
|
+
/** Whether this company prices in credits AT ALL.
|
|
713
|
+
*
|
|
714
|
+
* Ask this, never `packs.length`. An empty list is what a company selling for money gets (it is
|
|
715
|
+
* never asked) and equally what a credits company that has not priced a pack yet gets — and a
|
|
716
|
+
* portal reading the two as one hides its credits surface from the operator who has just turned
|
|
717
|
+
* credits on and gone looking for it. Optional, so a portal pointed at a backend older than the
|
|
718
|
+
* field still works; absent, the count is the only signal left. */
|
|
719
|
+
sellsCredits?: boolean;
|
|
720
|
+
};
|
|
721
|
+
/** One movement of the balance, with what it left the customer on. */
|
|
722
|
+
type CreditEntry = {
|
|
723
|
+
id: string;
|
|
724
|
+
/** Signed: negative spent it, positive added to it. The sign IS the direction, so a portal never
|
|
725
|
+
* has to decide which way a row goes from its reason. */
|
|
726
|
+
delta: number;
|
|
727
|
+
/** What they were on immediately AFTER this one — computed server-side by working backwards from
|
|
728
|
+
* the current balance, so it cannot disagree with the number at the top of the page. */
|
|
729
|
+
balance: number;
|
|
730
|
+
/** Why: `topup`, `purchase`, or whatever an operator recorded by hand. */
|
|
731
|
+
reason: string;
|
|
732
|
+
/** The order this bought, when it bought one. Null for a top-up or an adjustment. */
|
|
733
|
+
orderId: string | null;
|
|
734
|
+
/** That order's name, when it still exists. Absent for a top-up, and absent for an order deleted
|
|
735
|
+
* since — a ledger row for a purchase that happened is not made wrong by that, so it falls back
|
|
736
|
+
* to a generic label rather than disappearing or showing a row id. */
|
|
737
|
+
orderName?: string;
|
|
738
|
+
createdAt: string;
|
|
739
|
+
};
|
|
740
|
+
/** What a balance has taken in and paid out over its whole life. Both POSITIVE — a customer reads
|
|
741
|
+
* "spent 1,275", not "spent −1,275". */
|
|
742
|
+
type CreditTotals = {
|
|
743
|
+
purchased: number;
|
|
744
|
+
spent: number;
|
|
745
|
+
};
|
|
746
|
+
type CreditHistory = {
|
|
747
|
+
entries: CreditEntry[];
|
|
748
|
+
totals: CreditTotals;
|
|
749
|
+
/** The same figure `wallet()` answers, returned here so a page drawing both cannot show two. */
|
|
750
|
+
balance: number;
|
|
751
|
+
};
|
|
752
|
+
/** A started top-up: where to pay, and what it will grant. */
|
|
753
|
+
type TopupStarted = {
|
|
754
|
+
/** The Stripe payment link. It ends on Stripe's own confirmation page and never returns here, so
|
|
755
|
+
* open it in a NEW TAB and leave the portal standing — and re-read the wallet when the customer
|
|
756
|
+
* comes back, because the credits land on the webhook and not on their return. */
|
|
757
|
+
url: string;
|
|
758
|
+
txnId: string;
|
|
759
|
+
credits: number;
|
|
760
|
+
amount: number;
|
|
761
|
+
currency: string;
|
|
762
|
+
/** This handed back a link the customer already had rather than minting another. Not an error and
|
|
763
|
+
* usually not worth saying out loud — the same tab opens either way. */
|
|
764
|
+
resumed?: boolean;
|
|
765
|
+
};
|
|
766
|
+
/** What a purchase comes back as. Every figure is the server's own: a quantity in the request is a
|
|
767
|
+
* request and never a price, so the amount is recomputed from the product row. */
|
|
768
|
+
type Purchased = {
|
|
769
|
+
/** The order the credits bought, ready to open at `/orders/:orderId`. */
|
|
770
|
+
orderId: string;
|
|
771
|
+
/** Credits actually taken, at the price the server recomputed. */
|
|
772
|
+
credits: number;
|
|
773
|
+
/** What is left afterwards. */
|
|
774
|
+
balance: number;
|
|
775
|
+
/** What the order is worth in money at the company's book rate — the figure that lands on the
|
|
776
|
+
* operator's money surfaces. NOT what the customer paid: they paid credits. */
|
|
777
|
+
value: number;
|
|
778
|
+
};
|
|
779
|
+
/**
|
|
780
|
+
* What the customer agrees to pay, and when.
|
|
781
|
+
*
|
|
782
|
+
* Carried BY THE QUOTE rather than left to the order, because accepting a total without a schedule
|
|
783
|
+
* is half an agreement — nobody should discover "50% up front" after saying yes — and because it is
|
|
784
|
+
* what lets the order's payment rows be raised from something they have actually seen.
|
|
785
|
+
*
|
|
786
|
+
* MONEY, in the same unit `total` is. Not the shelf unit the LINES are priced in: the two travel
|
|
787
|
+
* together in one payload and telling them apart is the whole of what `QuotedLine.price` warns
|
|
788
|
+
* about below.
|
|
789
|
+
*/
|
|
790
|
+
type QuoteTerms = {
|
|
791
|
+
/** Taken before the work starts. The deposit IS the starting gun — nothing runs until it lands. */
|
|
792
|
+
upfront?: number;
|
|
793
|
+
/** Taken on delivery. ADVISORY: see `quoteSchedule`, which derives the balance as the remainder
|
|
794
|
+
* instead, so what a customer is shown and what they are billed cannot drift apart. */
|
|
795
|
+
onDelivery?: number;
|
|
796
|
+
/** Anything the operator wants said about the schedule, in their own words. */
|
|
797
|
+
note?: string;
|
|
798
|
+
};
|
|
799
|
+
/**
|
|
800
|
+
* One service being quoted, as the customer may see it.
|
|
801
|
+
*
|
|
802
|
+
* An ALLOWLIST assembled server-side, field by field, and not a projection of the stored item. The
|
|
803
|
+
* durable shape behind it carries the INTERNAL checklist, the skill row ids and the dashboard spec
|
|
804
|
+
* in the same object as the customer-facing `deliverables` — so the rule is that a new field
|
|
805
|
+
* upstream stays invisible here until somebody adds it on purpose.
|
|
806
|
+
*/
|
|
807
|
+
type QuotedLine = {
|
|
808
|
+
name: string;
|
|
809
|
+
/** A line of scope in the operator's words. */
|
|
810
|
+
notes?: string;
|
|
811
|
+
/** Longer scope — what this service actually covers, when it needs a paragraph. */
|
|
812
|
+
content?: string;
|
|
813
|
+
/**
|
|
814
|
+
* MONEY, in the same unit `total` is — safe to print with a currency symbol.
|
|
815
|
+
*
|
|
816
|
+
* IT DID NOT USED TO BE. This was handed over in the org's SHELF unit (credits on a credits org)
|
|
817
|
+
* beside a `total` that was money, in a payload naming no unit at all, and no portal could fix
|
|
818
|
+
* that from where it stood: `5,000` next to `$8,000` reads as dollars and is wrong by the whole
|
|
819
|
+
* book rate. It is converted server-side now, where the rate lives, so one payload is in one
|
|
820
|
+
* unit.
|
|
821
|
+
*
|
|
822
|
+
* Absent is still a real answer and is NOT a price of 0 — work thrown in is an agreement like
|
|
823
|
+
* any other. The lines also need not add up to `total`: `total` wins, exactly as `orders.value`
|
|
824
|
+
* beats the sum of its services, because a bundle priced below its parts is a real deal.
|
|
825
|
+
*/
|
|
826
|
+
price?: number;
|
|
827
|
+
/** "per month", "one-off" — how often this recurs, when it recurs. */
|
|
828
|
+
period?: string;
|
|
829
|
+
/** What they get: the client-visible deliverables this line buys. */
|
|
830
|
+
deliverables: string[];
|
|
831
|
+
};
|
|
832
|
+
/**
|
|
833
|
+
* The live quote on a 询单, or the fact that there isn't one.
|
|
834
|
+
*
|
|
835
|
+
* A UNION rather than a nullable object so "not quoted yet" cannot be read as "quoted at nothing".
|
|
836
|
+
* They are opposite states — one is the team still working, the other is an offer of an empty
|
|
837
|
+
* basket — and a portal that conflated them would put an Accept button under a quote nobody sent.
|
|
838
|
+
*/
|
|
839
|
+
type InquiryQuote = {
|
|
840
|
+
quoted: false;
|
|
841
|
+
} | {
|
|
842
|
+
quoted: true;
|
|
843
|
+
/** THE VERSION IN THIS PAYLOAD, and the number `acceptInquiry` must be given. Once they have
|
|
844
|
+
* accepted one it is that one — what somebody signed does not change because the operator
|
|
845
|
+
* sent another afterwards — and until then it is the newest sent. */
|
|
846
|
+
version: number;
|
|
847
|
+
/** The newest version that EXISTS. Equal to `version` in the ordinary case; greater when they
|
|
848
|
+
* accepted one and the operator has sent another since, which is the only way a portal can
|
|
849
|
+
* know to say so. Never a thing to accept: `version` is what is on screen, and accepting
|
|
850
|
+
* something nobody has read is the failure the whole scheme exists to prevent. */
|
|
851
|
+
latest: number;
|
|
852
|
+
sentAt: string;
|
|
853
|
+
lines: QuotedLine[];
|
|
854
|
+
/** MONEY, whatever the lines are priced in. The one figure to print as a price. */
|
|
855
|
+
total: number;
|
|
856
|
+
terms?: QuoteTerms;
|
|
857
|
+
/** Which version they have ALREADY accepted; 0 for none. So a reload after accepting shows
|
|
858
|
+
* the agreement rather than the button again. `accepted === version` is the ordinary way a
|
|
859
|
+
* portal knows it is looking at the contract rather than at an offer. */
|
|
860
|
+
accepted: number;
|
|
861
|
+
};
|
|
862
|
+
/** One line of the payment schedule: what is owed, and what it is called. */
|
|
863
|
+
type QuoteScheduleLine = {
|
|
864
|
+
label: string;
|
|
865
|
+
amount: number;
|
|
866
|
+
};
|
|
867
|
+
/** The two labels the platform raises payment rows under. Stated here so a portal can show a
|
|
868
|
+
* customer the SAME words before they accept that they will read on the bill afterwards — a
|
|
869
|
+
* schedule that says "Deposit" and a bill that says "Pre-payment" is two documents about one
|
|
870
|
+
* agreement, and the customer is the one left reconciling them. */
|
|
871
|
+
declare const QUOTE_DEPOSIT_LABEL = "Pre-payment";
|
|
872
|
+
declare const QUOTE_BALANCE_LABEL = "After-payment";
|
|
873
|
+
/**
|
|
874
|
+
* The schedule a quote's terms come to, as lines. MONEY in, money out.
|
|
875
|
+
*
|
|
876
|
+
* THE BALANCE IS THE REMAINDER, never a second stated figure, and this is the reason the function
|
|
877
|
+
* exists rather than a portal reading `terms.upfront` and `terms.onDelivery` straight out. Those
|
|
878
|
+
* two are typed by a human into two boxes and do not have to add up to the contract; the server
|
|
879
|
+
* bills the remainder regardless. A portal printing both would show a customer a schedule that
|
|
880
|
+
* silently forgives or double-charges the difference against the bill they actually receive.
|
|
881
|
+
*
|
|
882
|
+
* It mirrors the server's own `scheduleFrom` deliberately — same clamp, same rounding, same labels
|
|
883
|
+
* — because the point is that the customer sees, before they say yes, exactly the lines that will
|
|
884
|
+
* be raised when they do. In the SDK for the reason `orderBilling` is: the second portal to write
|
|
885
|
+
* this by hand would inherit nothing the first one learned, and being subtly wrong here is money.
|
|
886
|
+
*
|
|
887
|
+
* No terms at all means the whole thing up front — the honest reading of "they agreed a price and
|
|
888
|
+
* said nothing about instalments", and it never bills more than the contract.
|
|
889
|
+
*/
|
|
890
|
+
declare function quoteSchedule(total: number, terms: QuoteTerms | undefined): QuoteScheduleLine[];
|
|
891
|
+
type PortalCredits = {
|
|
892
|
+
/** Balance, packs and anything in flight — one call, because they are one question. A balance
|
|
893
|
+
* without the packs cannot be acted on, and a balance without the pending top-ups looks wrong to
|
|
894
|
+
* anyone who has just paid. */
|
|
895
|
+
wallet(): Promise<CreditWallet>;
|
|
896
|
+
/** Start buying a pack, by ID alone — the price is read from the pack row server-side, so there
|
|
897
|
+
* is no amount here for a client to choose. */
|
|
898
|
+
topup(packId: string): Promise<TopupStarted>;
|
|
899
|
+
/** The ledger behind the balance. Its OWN call, not part of `wallet()`: the wallet is read on
|
|
900
|
+
* every page that draws a balance, and hanging a fifty-row scan off it would make the cheapest
|
|
901
|
+
* read on this surface the most expensive one. */
|
|
902
|
+
history(): Promise<CreditHistory>;
|
|
903
|
+
/**
|
|
904
|
+
* Buy something from the shelf.
|
|
905
|
+
*
|
|
906
|
+
* NOT a redirect. On a credits shelf the money moved at the top-up, so this is a synchronous
|
|
907
|
+
* debit: it takes the credits, writes the order and its services, and answers `Purchased`.
|
|
908
|
+
*
|
|
909
|
+
* Refuses with `insufficient_credits` when they cannot cover it, and that refusal names the
|
|
910
|
+
* shortfall — it is the one message worth showing verbatim, because "not enough credits" is all a
|
|
911
|
+
* portal could say for itself.
|
|
912
|
+
*/
|
|
913
|
+
checkout(purchase: {
|
|
914
|
+
productId: string;
|
|
915
|
+
variant?: string;
|
|
916
|
+
qty?: number;
|
|
917
|
+
}): Promise<Purchased>;
|
|
918
|
+
/**
|
|
919
|
+
* Ask for a quote on an `ask` product — the other way a shelf turns into an order.
|
|
920
|
+
*
|
|
921
|
+
* Writes a real order in `ask` status carrying these answers, with no services, no value and
|
|
922
|
+
* nothing running: the brief, waiting to be priced. Send the KEYS the product's `form` declares;
|
|
923
|
+
* the labels are read from the product row server-side, so a client cannot restate the question
|
|
924
|
+
* it is answering.
|
|
925
|
+
*
|
|
926
|
+
* On this surface rather than the shelf's for the same reason `checkout` is: an order belongs to
|
|
927
|
+
* somebody, so it takes the customer's bearer. (The public shelf's `apply` is the stranger's
|
|
928
|
+
* door, and it leaves an inquiry instead — there is no customer to write an order against.)
|
|
929
|
+
*/
|
|
930
|
+
/** Raise a 询单 on an `ask` product. Returns the inquiry it created, so the caller can open it.
|
|
931
|
+
*
|
|
932
|
+
* It used to return `orderId`: an enquiry wrote an ORDER in `ask` status, which was the wrong
|
|
933
|
+
* object — an order with no money, no services and no agreed price, and a dead row in the
|
|
934
|
+
* customer's order list for every quote that went nowhere. */
|
|
935
|
+
enquire(
|
|
936
|
+
/** The `ask` product this is about. EMPTY for a 询单 raised about something that is not on the
|
|
937
|
+
* shelf — "can you also do X", which is how most of them start. */
|
|
938
|
+
productId: string, answers: {
|
|
939
|
+
key: string;
|
|
940
|
+
value: string;
|
|
941
|
+
}[],
|
|
942
|
+
/** What THEY say they need, in their words. Required when `productId` is empty and ignored when
|
|
943
|
+
* it is not: a product's own name is snapshotted server-side so a later rename cannot rewrite
|
|
944
|
+
* what somebody asked for. */
|
|
945
|
+
name?: string): Promise<{
|
|
946
|
+
inquiryId: string;
|
|
947
|
+
name: string;
|
|
948
|
+
}>;
|
|
949
|
+
/**
|
|
950
|
+
* The quote on a 询单: the services being offered, at the prices being offered.
|
|
951
|
+
*
|
|
952
|
+
* ITS OWN ROUTE, not a field on the inquiry row, and that is load-bearing rather than tidy. The
|
|
953
|
+
* spine's read permission is per TABLE and never per column, so a basket parked on the
|
|
954
|
+
* externally-visible `inquiries` row would hand the customer the internal checklist, the skill
|
|
955
|
+
* row ids and the dashboard spec along with their price. The server reads it as the org and
|
|
956
|
+
* projects an allowlist; there is one door, and `QuotedLine` is the whole of what fits through it.
|
|
957
|
+
*
|
|
958
|
+
* `{quoted: false}` is a REAL answer and not an error — nobody has sent anything yet, which is
|
|
959
|
+
* what a portal draws "we're working on your proposal" from. It is also what an inquiry quoted
|
|
960
|
+
* the old way answers, so a portal reading this keeps working against one.
|
|
961
|
+
*
|
|
962
|
+
* WHAT THEY SIGNED WINS over what has been sent since: once a version is accepted this returns
|
|
963
|
+
* THAT one, with `latest` naming the newest that exists. Before, it always returned the newest
|
|
964
|
+
* and left `accepted` as a bare number — so a customer whose operator revised after they said
|
|
965
|
+
* yes had a screen that knew which version was the contract and displayed a different one.
|
|
966
|
+
*/
|
|
967
|
+
inquiryQuote(inquiryId: string): Promise<InquiryQuote>;
|
|
968
|
+
/**
|
|
969
|
+
* Accept the quote on a 询单 — and, when there is one, get the order written from it.
|
|
970
|
+
*
|
|
971
|
+
* THE VERSION IS REQUIRED, and deliberately not defaulted to the latest. Defaulting would make
|
|
972
|
+
* the stale case unrepresentable: a tab left open on v1 would silently agree to whatever price is
|
|
973
|
+
* current, which is the exact failure versioning exists to prevent. Pass the `version` off the
|
|
974
|
+
* quote that was actually on screen; pass 0 for an inquiry with no quote, which keeps today's
|
|
975
|
+
* behaviour (it moves to `accepted` and the operator writes the order by hand).
|
|
976
|
+
*
|
|
977
|
+
* Idempotent — `changed: false` means they had already accepted, which is what a double-tap on a
|
|
978
|
+
* slow connection looks like and is not an error.
|
|
979
|
+
*
|
|
980
|
+
* Refuses with `CreditRefused`, and the code says which of three things happened. Each wants a
|
|
981
|
+
* different sentence, which is why they are three:
|
|
982
|
+
*
|
|
983
|
+
* - `quote_revised` — the version is no longer the current one. The ONLY correct response is to
|
|
984
|
+
* re-read `inquiryQuote` and show what came back: retrying with the version named in the
|
|
985
|
+
* refusal is the stale accept again, one round trip later. (Which is why that number is not
|
|
986
|
+
* surfaced here — the re-read answers it, and answers it as of now rather than as of then.)
|
|
987
|
+
* - `not_quoted` — nobody has sent anything. There is nothing to accept, and there is nothing
|
|
988
|
+
* for the customer to do but wait.
|
|
989
|
+
* - `inquiry_closed` — it is already closed, or already an order. Someone else moved it, so the
|
|
990
|
+
* page should re-read rather than argue.
|
|
991
|
+
*/
|
|
992
|
+
acceptInquiry(inquiryId: string, version: number): Promise<{
|
|
993
|
+
status: string;
|
|
994
|
+
changed: boolean;
|
|
995
|
+
}>;
|
|
996
|
+
/** Every payment on this customer's orders, due and paid both — what they owe, on what, and by
|
|
997
|
+
* when. Join to an order on `order`.
|
|
998
|
+
*
|
|
999
|
+
* On this surface rather than portal-data because it is NOT a table read: `payments` is not
|
|
1000
|
+
* externally visible, and the server projects an allowlist of bill fields off the row. */
|
|
1001
|
+
payments(): Promise<{
|
|
1002
|
+
payments: CustomerPayment[];
|
|
1003
|
+
}>;
|
|
1004
|
+
/** How this company takes a bank transfer. `null` when they publish no details — Stripe is not
|
|
1005
|
+
* the only way money moves, and for an operator banking with Mercury it is not a way at all. */
|
|
1006
|
+
paymentMethods(): Promise<{
|
|
1007
|
+
transfer: TransferDetails | null;
|
|
1008
|
+
}>;
|
|
1009
|
+
/** "I've sent the transfer." Records the customer's claim against one payment and puts it on the
|
|
1010
|
+
* operator's desk to confirm against the account.
|
|
1011
|
+
*
|
|
1012
|
+
* It does NOT settle the payment, and a portal must not present it as though it does: work
|
|
1013
|
+
* starts on payment, and a claim is not a receipt. `claimed: false` with `status: "paid"` means
|
|
1014
|
+
* the operator had already confirmed it. */
|
|
1015
|
+
markPaymentSent(paymentId: string, reference?: string): Promise<{
|
|
1016
|
+
claimed: boolean;
|
|
1017
|
+
status: string;
|
|
1018
|
+
}>;
|
|
1019
|
+
/**
|
|
1020
|
+
* Settle one payment line out of the customer's own prepaid balance.
|
|
1021
|
+
*
|
|
1022
|
+
* THE THIRD DOOR ONTO ONE TRANSITION, and the only self-service one that works. A transfer
|
|
1023
|
+
* settles in a bank this product cannot see and needs an operator to confirm it; `pay_url` has no
|
|
1024
|
+
* writer anywhere, so the card button never renders. This one closes itself — the balance moves,
|
|
1025
|
+
* the line flips to paid, the work starts — and it does NOT invent a second notion of paid:
|
|
1026
|
+
* `payments.status` stays the one definition, which is what keeps the console's badge, the
|
|
1027
|
+
* worklist and this page telling one story about the same order.
|
|
1028
|
+
*
|
|
1029
|
+
* `paymentId` names a LINE, not an order. That is what lets somebody pay the deposit today and
|
|
1030
|
+
* the balance on delivery, and it is what the server's idempotency key is cut from: a double tap
|
|
1031
|
+
* on a slow connection takes the credits once.
|
|
1032
|
+
*
|
|
1033
|
+
* Show the customer `creditsOfMoney(line.amount, wallet.bookRate)` before they press it. The
|
|
1034
|
+
* rounding is up, it is applied server-side, and a page that only ever showed the money figure
|
|
1035
|
+
* would take a credit more than the number on screen.
|
|
1036
|
+
*
|
|
1037
|
+
* REFUSALS, and each wants a different sentence:
|
|
1038
|
+
*
|
|
1039
|
+
* - `InsufficientCredits` (402) — they cannot cover it. Not a fault: it is a quote for a top-up,
|
|
1040
|
+
* carrying what the line costs, what they hold and the difference. The other ways to pay are
|
|
1041
|
+
* still on screen and must stay there.
|
|
1042
|
+
* - `CreditRefused` with `credits_not_sold` (409) — this company does not price in credits at
|
|
1043
|
+
* all. A portal should never have offered the option; treat it as the wallet being stale.
|
|
1044
|
+
* - `CreditRefused` with `rate_limited` (429) — too many in a row. Wait.
|
|
1045
|
+
* - Anything else is ours and the credits are safe; the sentence is the server's.
|
|
1046
|
+
*/
|
|
1047
|
+
payWithCredits(paymentId: string): Promise<PaidWithCredits>;
|
|
1048
|
+
};
|
|
1049
|
+
/** One line on a customer's bill. Mirrors the projection holaapp-backend's `customerPayment`
|
|
1050
|
+
* emits — an allowlist, not the stored row. */
|
|
1051
|
+
type CustomerPayment = {
|
|
1052
|
+
id: string;
|
|
1053
|
+
/** The order this runs against. */
|
|
1054
|
+
order: string;
|
|
1055
|
+
/** "Pre-payment", "After-payment", "Paid in full" — whatever the operator labelled it. */
|
|
1056
|
+
label: string;
|
|
1057
|
+
amount: number;
|
|
1058
|
+
/** Anything the server could not read as `paid` comes back as `due`. */
|
|
1059
|
+
status: "due" | "paid";
|
|
1060
|
+
method: string;
|
|
1061
|
+
dueDate: string;
|
|
1062
|
+
paidAt: string;
|
|
1063
|
+
/** Where to pay this online, when the money can be taken that way. Empty for a bank transfer,
|
|
1064
|
+
* which is not a link but a set of details plus a person confirming it arrived. */
|
|
1065
|
+
payUrl: string;
|
|
1066
|
+
/** When the customer said they had sent it — their claim, not a receipt. Set by `markPaymentSent`
|
|
1067
|
+
* and cleared by nothing: it is the record of what was said, and the operator confirming the
|
|
1068
|
+
* money is what changes `status`. */
|
|
1069
|
+
claimedAt: string;
|
|
1070
|
+
reference: string;
|
|
1071
|
+
/**
|
|
1072
|
+
* Whether the customer is being ASKED for this line right now, as opposed to merely shown it as
|
|
1073
|
+
* part of the schedule they agreed to.
|
|
1074
|
+
*
|
|
1075
|
+
* RAISED IS NOT OWED. The whole schedule is written the moment the order is — that is what makes
|
|
1076
|
+
* the contract complete and the terms visible — but a balance falling due ON DELIVERY is not owed
|
|
1077
|
+
* while the work is still being done. With only `status` to go on, a portal drew a bank-transfer
|
|
1078
|
+
* form for the balance the instant the deposit landed: account numbers and an "I've sent the
|
|
1079
|
+
* transfer" button for money nobody was owed yet.
|
|
1080
|
+
*
|
|
1081
|
+
* DERIVED SERVER-SIDE, and the rule stays there. It is structural (which label, and whether the
|
|
1082
|
+
* operator has requested it) rather than a stamp, and a second copy of it here is a second copy
|
|
1083
|
+
* that can disagree with the operator's console about what a customer owes. Read it, never
|
|
1084
|
+
* recompute it — `paymentAsked` below is the whole of the reading.
|
|
1085
|
+
*
|
|
1086
|
+
* OPTIONAL because a portal is pinned to a published SDK and talks to whatever backend the
|
|
1087
|
+
* operator is on. Absent means the backend predates the field; see `paymentAsked` for why that
|
|
1088
|
+
* has to read as asked rather than as scheduled.
|
|
1089
|
+
*/
|
|
1090
|
+
asked?: boolean;
|
|
1091
|
+
};
|
|
1092
|
+
/**
|
|
1093
|
+
* Whether this line has a pay surface — the transfer details, the credits button, the claim form.
|
|
1094
|
+
*
|
|
1095
|
+
* ABSENT READS AS ASKED, and the direction is not a coin toss. A backend older than the field sends
|
|
1096
|
+
* nothing, and reading that as "scheduled" would take the pay surface off every line in the portal
|
|
1097
|
+
* at once — a customer who owes a deposit with no way to pay it, which is worse than the bug
|
|
1098
|
+
* `asked` was added to fix. Reading it as asked reproduces exactly the behaviour that backend
|
|
1099
|
+
* already has.
|
|
1100
|
+
*
|
|
1101
|
+
* A settled line is never asked for, whatever the flag says: the server agrees, and stating it here
|
|
1102
|
+
* as well means a portal cannot draw a payment form over a receipt because one stale row disagreed.
|
|
1103
|
+
*/
|
|
1104
|
+
declare const paymentAsked: (p: CustomerPayment) => boolean;
|
|
1105
|
+
/** How to pay this company by transfer. Null when the operator has published nothing you could
|
|
1106
|
+
* actually send money to — no details is not an empty form, it is no transfer option. */
|
|
1107
|
+
type TransferDetails = {
|
|
1108
|
+
bank: string;
|
|
1109
|
+
accountName: string;
|
|
1110
|
+
accountNumber: string;
|
|
1111
|
+
routing: string;
|
|
1112
|
+
swift: string;
|
|
1113
|
+
note: string;
|
|
1114
|
+
};
|
|
1115
|
+
/**
|
|
1116
|
+
* What settling a bill from the balance came back as.
|
|
1117
|
+
*
|
|
1118
|
+
* BOTH ARMS SAY `paid: true`, and that is the whole shape of this type. The second one is the line
|
|
1119
|
+
* having already been settled — by an earlier tap of the same button on a slow connection, by the
|
|
1120
|
+
* operator marking a transfer while the customer was looking at the page, or by a retry whose first
|
|
1121
|
+
* attempt got further than the caller saw. From the customer's side the thing they wanted is TRUE,
|
|
1122
|
+
* so it is not an error and must not be drawn as one; a portal that showed "couldn't pay that" over
|
|
1123
|
+
* a bill that is paid would send somebody to pay it a second time.
|
|
1124
|
+
*
|
|
1125
|
+
* The zeros on that arm are LITERAL because they are not readings. Nothing was taken (`credits: 0`)
|
|
1126
|
+
* and nobody looked at the balance (`balance: 0`) — the row was already settled before a debit was
|
|
1127
|
+
* attempted. Re-read the wallet rather than printing that 0 at anybody.
|
|
1128
|
+
*/
|
|
1129
|
+
type PaidWithCredits =
|
|
1130
|
+
/** Settled just now. `balance` is what is LEFT, after this line. */
|
|
1131
|
+
{
|
|
1132
|
+
paid: true;
|
|
1133
|
+
alreadyPaid?: false;
|
|
1134
|
+
credits: number;
|
|
1135
|
+
balance: number;
|
|
1136
|
+
}
|
|
1137
|
+
/** It was already settled. Neither number is a reading — see above. */
|
|
1138
|
+
| {
|
|
1139
|
+
paid: true;
|
|
1140
|
+
alreadyPaid: true;
|
|
1141
|
+
credits: 0;
|
|
1142
|
+
balance: 0;
|
|
1143
|
+
};
|
|
1144
|
+
/**
|
|
1145
|
+
* What a MONEY figure costs in credits, at the company's book rate.
|
|
1146
|
+
*
|
|
1147
|
+
* A MIRROR of the server's `creditsOfMoney`, deliberately, and here for the same reason
|
|
1148
|
+
* `quoteSchedule` is: the customer has to be shown the credit figure BEFORE they press the button,
|
|
1149
|
+
* and the only figure worth showing is the one that will actually be taken.
|
|
1150
|
+
*
|
|
1151
|
+
* IT ROUNDS UP, and that is why showing it matters rather than being a nicety. The ledger moves in
|
|
1152
|
+
* whole credits, so a $10.50 line at rate 1 costs 11 — more than the exact conversion of the money
|
|
1153
|
+
* on the row. Up costs the customer at most one credit and never leaves the operator short, which
|
|
1154
|
+
* is the only direction that is safe to apply without asking; what makes it HONEST is that they
|
|
1155
|
+
* read the number first.
|
|
1156
|
+
*
|
|
1157
|
+
* Only good for a line that is still due. A settled one carries what was actually taken, stamped on
|
|
1158
|
+
* the row at the moment it was paid — the book rate is a setting an operator can change, and a
|
|
1159
|
+
* receipt re-derived through today's rate is a receipt that rewrites itself.
|
|
1160
|
+
*/
|
|
1161
|
+
declare const creditsOfMoney: (amount: number,
|
|
1162
|
+
/** Credits per one unit of currency — `CreditWallet.bookRate`. Absent or nonsense reads as 1,
|
|
1163
|
+
* which is what `normaliseBookRate` does server-side. */
|
|
1164
|
+
bookRate?: number) => number;
|
|
1165
|
+
/** Where one order stands financially, and therefore whether it has started. */
|
|
1166
|
+
type OrderBilling = {
|
|
1167
|
+
/** The agreed contract value. 0 when nobody priced it — not the same as free, which is what
|
|
1168
|
+
* `priced` is for. */
|
|
1169
|
+
value: number;
|
|
1170
|
+
priced: boolean;
|
|
1171
|
+
/** Raised against this order, settled or not. Can exceed `value` on a renegotiated one: it is a
|
|
1172
|
+
* record of what was billed, not a derivation of the contract. */
|
|
1173
|
+
billed: number;
|
|
1174
|
+
paid: number;
|
|
1175
|
+
/**
|
|
1176
|
+
* What the customer is being ASKED FOR right now. Never negative.
|
|
1177
|
+
*
|
|
1178
|
+
* NOT "everything raised and unsettled", which is the figure this used to be and is a bill the
|
|
1179
|
+
* customer does not owe: a deposit paid and a balance falling due on delivery came to "$4,000
|
|
1180
|
+
* due" over an order whose work was under way and whose next payment nobody had requested. Only
|
|
1181
|
+
* `asked` lines are counted — see `CustomerPayment.asked`.
|
|
1182
|
+
*
|
|
1183
|
+
* Falls back to the whole contract value when the order is priced and NOTHING has been raised
|
|
1184
|
+
* yet — the ordinary case the moment an order is written, where the customer is owed a figure
|
|
1185
|
+
* rather than a shrug. Deliberately not applied when lines exist but none is asked for: that is
|
|
1186
|
+
* a real state (all settled, or the rest merely scheduled) and answering it with the contract
|
|
1187
|
+
* value would re-bill a paid order.
|
|
1188
|
+
*/
|
|
1189
|
+
due: number;
|
|
1190
|
+
/** Raised, unsettled, and NOT being asked for yet — the rest of the schedule. Listed so a
|
|
1191
|
+
* customer can see the terms they agreed to; never given a payment form. */
|
|
1192
|
+
scheduled: number;
|
|
1193
|
+
/** Every line, in SCHEDULE order: the deposit, then any milestones, then the balance — settled
|
|
1194
|
+
* or not. A bill's history reads forwards; what is owed *now* is `asked`, not a sort. */
|
|
1195
|
+
lines: CustomerPayment[];
|
|
1196
|
+
/** The lines with a pay surface: unsettled and asked for, in the order above. What a portal draws
|
|
1197
|
+
* a transfer form, a credits button or a claim under — and nothing else. */
|
|
1198
|
+
asked: CustomerPayment[];
|
|
1199
|
+
/** Raised, unsettled, and merely COMING. Listed with their dates and no way to pay them: asking
|
|
1200
|
+
* for money against work nobody has handed over is the bug `asked` exists to prevent. */
|
|
1201
|
+
upcoming: CustomerPayment[];
|
|
1202
|
+
/**
|
|
1203
|
+
* Whether the work is waiting on money — and so has not started.
|
|
1204
|
+
*
|
|
1205
|
+
* PRICED AND NOTHING PAID. Not "billed and unsettled", which is wrong in the ordinary case: an
|
|
1206
|
+
* order is written with a value and no payment lines until somebody raises one, so a customer
|
|
1207
|
+
* looking at a fresh contract would be told it was under way while the operator's console badged
|
|
1208
|
+
* it Unpaid. This matches the operator side's rule exactly — any PAID line means started — which
|
|
1209
|
+
* is what stops the two describing one order in opposite terms.
|
|
1210
|
+
*/
|
|
1211
|
+
awaitingPayment: boolean;
|
|
1212
|
+
};
|
|
1213
|
+
declare const orderBilling: (order: {
|
|
1214
|
+
value: number;
|
|
1215
|
+
}, all: CustomerPayment[], orderId: string) => OrderBilling;
|
|
1216
|
+
declare const createPortalCredits: (config: ApiTransportConfig) => PortalCredits;
|
|
1217
|
+
|
|
1218
|
+
type PortalShopConfig = {
|
|
1219
|
+
/** API origin, e.g. "https://api.holaos.ai" — or empty for a portal whose own worker answers
|
|
1220
|
+
* `/api/v1/end-user/…` same-origin, which is how the iMerch template is wired. */
|
|
1221
|
+
baseUrl: string;
|
|
1222
|
+
/**
|
|
1223
|
+
* The tenant's publishable key, which is the whole of the authentication here.
|
|
1224
|
+
*
|
|
1225
|
+
* A plain string rather than the getter `ApiTransportConfig.token` takes: an end-user token
|
|
1226
|
+
* refreshes and a publishable key does not — it is baked into the page source a visitor can read,
|
|
1227
|
+
* which is what makes it publishable. There is deliberately no token field on this config at all,
|
|
1228
|
+
* so nothing in a storefront can come to depend on being signed in to browse.
|
|
1229
|
+
*/
|
|
1230
|
+
publishableKey: string;
|
|
1231
|
+
};
|
|
1232
|
+
/** How a product is taken: `buy` charges for it, `ask` opens the conversation the customer already
|
|
1233
|
+
* has with the company, `apply` posts the form `apply()` sends. */
|
|
1234
|
+
type ShopMode = "buy" | "ask";
|
|
1235
|
+
/**
|
|
1236
|
+
* What a product costs — exactly one of four shapes, never two.
|
|
1237
|
+
*
|
|
1238
|
+
* The number is in whatever unit the OPERATOR prices in: credits for an org that sells them, money
|
|
1239
|
+
* for one that does not — and the wire says which, on `ShopProduct.currency` beside this. It used
|
|
1240
|
+
* to say otherwise, that the figure travelled bare and the unit "belongs to the template that knows
|
|
1241
|
+
* the tenant", which was how this contract was written before `pricing_unit` existed and was false
|
|
1242
|
+
* the day the server started sending the field. A template built on that sentence prints a
|
|
1243
|
+
* hardcoded `$` in front of a credit price — wrong by the whole book rate. Nothing here should ever
|
|
1244
|
+
* print a `$` it did not read off `currency`.
|
|
1245
|
+
*
|
|
1246
|
+
* `quoted` is not a price. It is the honest answer for everything in a real catalogue that reads
|
|
1247
|
+
* `5k – 10k`, `20k+` or 待定 — a statement that a human has to quote this. Render it as words, and
|
|
1248
|
+
* never behind a Buy button that cannot compute a total.
|
|
1249
|
+
*/
|
|
1250
|
+
type ShopPricing = {
|
|
1251
|
+
kind: "flat";
|
|
1252
|
+
price: number;
|
|
1253
|
+
} | {
|
|
1254
|
+
kind: "per_unit";
|
|
1255
|
+
unit: string;
|
|
1256
|
+
unitPrice: number;
|
|
1257
|
+
unitStep: number;
|
|
1258
|
+
} | {
|
|
1259
|
+
kind: "variants";
|
|
1260
|
+
variants: {
|
|
1261
|
+
name: string;
|
|
1262
|
+
price: number | null;
|
|
1263
|
+
}[];
|
|
1264
|
+
} | {
|
|
1265
|
+
kind: "quoted";
|
|
1266
|
+
};
|
|
1267
|
+
/** One part of a bundle, as the shelf is allowed to describe it: what it is called, what it covers,
|
|
1268
|
+
* how long it runs — and no price, for the reason at the top of this file. */
|
|
1269
|
+
type ShopInclusion = {
|
|
1270
|
+
name: string;
|
|
1271
|
+
content?: string;
|
|
1272
|
+
period?: string;
|
|
1273
|
+
};
|
|
1274
|
+
/** A named tier — 基础 / 标准 / 高阶 — and what it covers. Its PRICE is not here: it is in
|
|
1275
|
+
* `pricing` (`kind: "variants"`), matched to this by name, so that there is one place the money
|
|
1276
|
+
* comes from and no way for the two to disagree. */
|
|
1277
|
+
/** One question an `ask` product asks before it can be quoted. Authored by the operator on the
|
|
1278
|
+
* product row; the storefront draws it and sends back answers keyed to it. */
|
|
1279
|
+
type ShopFormField = {
|
|
1280
|
+
key: string;
|
|
1281
|
+
label: string;
|
|
1282
|
+
type: "text" | "textarea" | "number" | "date" | "select";
|
|
1283
|
+
required?: boolean;
|
|
1284
|
+
options?: string[];
|
|
1285
|
+
help?: string;
|
|
1286
|
+
};
|
|
1287
|
+
type ShopVariant = {
|
|
1288
|
+
name: string;
|
|
1289
|
+
includes: ShopInclusion[];
|
|
1290
|
+
};
|
|
1291
|
+
/** One product as a visitor sees it. */
|
|
1292
|
+
type ShopProduct = {
|
|
1293
|
+
id: string;
|
|
1294
|
+
name: string;
|
|
1295
|
+
summary: string;
|
|
1296
|
+
description: string;
|
|
1297
|
+
/**
|
|
1298
|
+
* One of `ShopMode` for anything authored in the console — but typed as the string it is on the
|
|
1299
|
+
* wire, because the server sends the row's own value and a product from before the field existed
|
|
1300
|
+
* answers `""`. A storefront that switched over the union would be handed a value the type swore
|
|
1301
|
+
* could not occur, so match the three and keep a default arm.
|
|
1302
|
+
*/
|
|
1303
|
+
mode: string;
|
|
1304
|
+
category: string;
|
|
1305
|
+
platform: string;
|
|
1306
|
+
cadence: string;
|
|
1307
|
+
period: string;
|
|
1308
|
+
/** Image URLs for the card and the product page; empty when the operator set none. */
|
|
1309
|
+
media: string[];
|
|
1310
|
+
pricing: ShopPricing;
|
|
1311
|
+
/** What THIS product's numbers are in — an ISO code, or the literal "credits". It sits on the
|
|
1312
|
+
* product rather than on the shelf because a shelf can be mixed: once an org sells credits, its
|
|
1313
|
+
* buyable products are priced in them while a 20k engagement it merely quotes stays in money.
|
|
1314
|
+
* Empty from a backend older than the field, which is a shelf that has not SAID — and is not the
|
|
1315
|
+
* same as one that said dollars, so print the bare figure rather than guessing a symbol. */
|
|
1316
|
+
currency: string;
|
|
1317
|
+
/** Bounds on a per-unit purchase, when the product states them. Public because the quantity
|
|
1318
|
+
* picker has to honour them — a customer who meets a limit they were never shown reads it as a
|
|
1319
|
+
* broken shop. */
|
|
1320
|
+
minQty?: number;
|
|
1321
|
+
maxQty?: number;
|
|
1322
|
+
includes: ShopInclusion[];
|
|
1323
|
+
/** The enquiry form, on an `ask` product only. Never empty when present — a product that asks
|
|
1324
|
+
* nothing still gets one box, because a Send button over nothing collects an enquiry that says
|
|
1325
|
+
* only that somebody was interested. Absent on `buy` and `apply`, which are not asked. */
|
|
1326
|
+
form?: ShopFormField[];
|
|
1327
|
+
/** Present only where the tiers differ in scope; a product without them has one implicit tier,
|
|
1328
|
+
* which is its own `pricing` and its own `includes`. */
|
|
1329
|
+
variants?: ShopVariant[];
|
|
1330
|
+
};
|
|
1331
|
+
type ShopShelf = {
|
|
1332
|
+
products: ShopProduct[];
|
|
1333
|
+
/**
|
|
1334
|
+
* The catalogue is bigger than what came back.
|
|
1335
|
+
*
|
|
1336
|
+
* Kept rather than dropped for the reason `listRows` returns a page: the server caps a shelf read,
|
|
1337
|
+
* and a shop that quietly ends at the cap is a customer who cannot see half of it and nobody who
|
|
1338
|
+
* can tell. The same argument the server makes when it bothers to send the flag.
|
|
1339
|
+
*/
|
|
1340
|
+
truncated: boolean;
|
|
1341
|
+
/** What an enquiry with NO product asks — "can you also do X", which has no product row to carry
|
|
1342
|
+
* questions. The operator's own defaults; empty when they have set none, which a storefront
|
|
1343
|
+
* reads as one box ("What do you need?"). A product's `form` is already resolved against these
|
|
1344
|
+
* server-side, so a card never has to merge the two itself. */
|
|
1345
|
+
enquiryForm: ShopFormField[];
|
|
1346
|
+
};
|
|
1347
|
+
/** An application against a product, or against nothing in particular — a general enquiry that
|
|
1348
|
+
* names no product is a real thing to receive, so `productId` is optional. */
|
|
1349
|
+
type ShopApplication = {
|
|
1350
|
+
name: string;
|
|
1351
|
+
/**
|
|
1352
|
+
* Where the operator answers. Required, and checked by the server — but a malformed one is
|
|
1353
|
+
* dropped SILENTLY (see `apply` below), so a form that wants to tell someone their address is
|
|
1354
|
+
* wrong has to say so itself, before it sends.
|
|
1355
|
+
*/
|
|
1356
|
+
email: string;
|
|
1357
|
+
phone?: string;
|
|
1358
|
+
message?: string;
|
|
1359
|
+
productId?: string;
|
|
1360
|
+
};
|
|
1361
|
+
/** The one answer every application gets. See `apply` for why it never varies. */
|
|
1362
|
+
type ShopApplyResult = {
|
|
1363
|
+
/**
|
|
1364
|
+
* Always `true` — including for a submission that was dropped. It is a receipt that the request
|
|
1365
|
+
* was accepted, never evidence that a row exists.
|
|
1366
|
+
*/
|
|
1367
|
+
ok: boolean;
|
|
1368
|
+
/** The server's own thank-you line. Shown as sent rather than reworded, so the operator's copy is
|
|
1369
|
+
* the copy the applicant reads. */
|
|
1370
|
+
message: string;
|
|
1371
|
+
};
|
|
1372
|
+
type PortalShop = {
|
|
1373
|
+
/** Everything on sale, in the operator's own order. Listed products only — a draft is something
|
|
1374
|
+
* still being written and an archived one is off sale but kept because orders point at it. */
|
|
1375
|
+
listProducts(): Promise<ShopShelf>;
|
|
1376
|
+
/**
|
|
1377
|
+
* Send an application. No account, no session — this is the one write on the platform a total
|
|
1378
|
+
* stranger can make.
|
|
1379
|
+
*
|
|
1380
|
+
* It answers the SAME "thanks, we have your details" to an unknown key, a body that did not
|
|
1381
|
+
* parse and a write that failed as it does to an application that landed, and that is deliberate:
|
|
1382
|
+
* anything else would turn this into a way to ask which publishable keys reach a real company —
|
|
1383
|
+
* and, once signups are invite-only, which addresses one already has. The cost is that a
|
|
1384
|
+
* miswired form gets a cheerful reply and files nothing, so when applications are not arriving,
|
|
1385
|
+
* the answer is in the operator's server logs and cannot be here.
|
|
1386
|
+
*/
|
|
1387
|
+
apply(application: ShopApplication): Promise<ShopApplyResult>;
|
|
1388
|
+
};
|
|
1389
|
+
declare const createPortalShop: (config: PortalShopConfig) => PortalShop;
|
|
1390
|
+
|
|
291
1391
|
/** Provides a `HolaClient` (from `createHolaClient`) to `useChat` / `<Chat/>`.
|
|
292
1392
|
* Create the client once (e.g. `useMemo`) so its session persists across renders. */
|
|
293
1393
|
declare const HolaProvider: ({ client, children, }: {
|
|
294
1394
|
client: HolaClient;
|
|
295
1395
|
children: ReactNode;
|
|
296
|
-
}) =>
|
|
1396
|
+
}) => react.JSX.Element;
|
|
297
1397
|
declare const useHolaClient: () => HolaClient;
|
|
298
1398
|
|
|
1399
|
+
/**
|
|
1400
|
+
* How this product is taken.
|
|
1401
|
+
*
|
|
1402
|
+
* The wire carries the row's own value rather than the union, because a product authored before
|
|
1403
|
+
* the field existed answers `""`. A shelf that switched on the three names and fell through would
|
|
1404
|
+
* render a product with no action at all, so an unrecognised mode becomes `ask`: it is the one
|
|
1405
|
+
* path that needs no money, writes nothing, and always has somebody at the other end of it.
|
|
1406
|
+
*/
|
|
1407
|
+
declare function modeOf(product: ShopProduct): ShopMode;
|
|
1408
|
+
/** The price that applies once a tier has been chosen. A tiered product is not a price until then;
|
|
1409
|
+
* every other shape already was one. */
|
|
1410
|
+
type ChosenPrice = {
|
|
1411
|
+
kind: "flat";
|
|
1412
|
+
price: number;
|
|
1413
|
+
} | {
|
|
1414
|
+
kind: "per_unit";
|
|
1415
|
+
unit: string;
|
|
1416
|
+
unitPrice: number;
|
|
1417
|
+
unitStep: number;
|
|
1418
|
+
} | {
|
|
1419
|
+
kind: "quoted";
|
|
1420
|
+
};
|
|
1421
|
+
/**
|
|
1422
|
+
* The figure the shelf carries, printed in the unit the ORG prices in.
|
|
1423
|
+
*
|
|
1424
|
+
* `currency` comes off the shelf payload and from nowhere else. It used to be a line of portal
|
|
1425
|
+
* config on the portal itself, which is a portal guessing at somebody else's rate card: the
|
|
1426
|
+
* numbers belong to the org and so does what they MEAN, and those two travelling separately is how
|
|
1427
|
+
* a catalogue ends up wrong by an exchange rate. An ISO code prints money; the literal "credits"
|
|
1428
|
+
* prints credits, which is the other unit an operator can price in.
|
|
1429
|
+
*
|
|
1430
|
+
* A shelf that names no unit prints the bare figure. That is a backend older than the field, and an
|
|
1431
|
+
* ambiguous number is a far smaller lie than a confident "$" in front of a price in RMB.
|
|
1432
|
+
*
|
|
1433
|
+
* An unusable code is caught rather than left to throw: one bad value would otherwise raise a
|
|
1434
|
+
* RangeError from inside a price label and take the whole shelf down with it.
|
|
1435
|
+
*/
|
|
1436
|
+
declare function money(amount: number, currency: string): string;
|
|
1437
|
+
/** What a customer is quoted, in words. `quoted` is deliberately not a number: it is the honest
|
|
1438
|
+
* answer for a catalogue line that reads `20k+` or 待定, and it must never sit behind a button
|
|
1439
|
+
* that cannot compute a total. */
|
|
1440
|
+
declare function priceLabel(price: ChosenPrice, currency: string): string;
|
|
1441
|
+
/** The one line a card shows about money. A tiered product is a RANGE until a tier is picked, so
|
|
1442
|
+
* the card says "From" and the product page says which — a card that printed one tier's figure
|
|
1443
|
+
* would be quoting a price that half the buyers do not pay. */
|
|
1444
|
+
declare function shelfPriceLabel(product: ShopProduct): string;
|
|
1445
|
+
/** The chosen tier's price, matched to it BY NAME — the tier's scope and the tier's money are two
|
|
1446
|
+
* fields on the wire, and this is the join. A tier nobody priced is quoted, which is a real state:
|
|
1447
|
+
* 基础 / 标准 published, 高阶 still being costed. */
|
|
1448
|
+
declare function priceFor(pricing: ShopPricing, variant: string | null): ChosenPrice;
|
|
1449
|
+
/** What the customer would pay for `qty` units, or null where there is no total to compute. Steps,
|
|
1450
|
+
* not units: 50 upvotes at a time is what is actually sold, so a request for 60 is charged as two
|
|
1451
|
+
* steps — the same arithmetic the server does when it recomputes this from the row. */
|
|
1452
|
+
declare function totalFor(price: ChosenPrice, qty: number): number | null;
|
|
1453
|
+
/** How much of a per-unit product may be bought, and in what increments. The bounds are the
|
|
1454
|
+
* product's own, and they are shown as well as enforced: a customer who hits a limit they were
|
|
1455
|
+
* never told about reads it as a broken shop rather than a rule. */
|
|
1456
|
+
declare function quantityRange(product: ShopProduct, price: ChosenPrice): {
|
|
1457
|
+
min: number;
|
|
1458
|
+
max: number | null;
|
|
1459
|
+
step: number;
|
|
1460
|
+
};
|
|
1461
|
+
/**
|
|
1462
|
+
* What the chosen tier covers.
|
|
1463
|
+
*
|
|
1464
|
+
* A tier with nothing listed falls back to the product's own inclusions rather than showing an
|
|
1465
|
+
* empty list. That is not a guess: `variants` is authored only where the tiers DIFFER in scope, so
|
|
1466
|
+
* a tier that lists nothing is one that adds nothing to what the product already says it includes —
|
|
1467
|
+
* and an empty "What's included" reads as a bundle containing nothing at all.
|
|
1468
|
+
*/
|
|
1469
|
+
declare function includesFor(product: ShopProduct, variant: string | null): ShopInclusion[];
|
|
1470
|
+
/** The values a facet actually has on this shelf, in the operator's own order. Not sorted: `sort`
|
|
1471
|
+
* on the product rows is a decision somebody made about what to show first, and re-ordering these
|
|
1472
|
+
* alphabetically throws it away. Products with the facet unset are simply not counted. */
|
|
1473
|
+
declare function facetValues(products: ShopProduct[], of: (product: ShopProduct) => string): string[];
|
|
1474
|
+
/** The message a "buy this" enquiry sends when a portal cannot take the money itself.
|
|
1475
|
+
*
|
|
1476
|
+
* Named rather than described by id: an operator reads this in the same thread as everything else,
|
|
1477
|
+
* and a row id is not a thing anyone can answer. It is a SENTENCE and not a product because the
|
|
1478
|
+
* same text opens the apply form for a signed-out visitor — a request that read differently
|
|
1479
|
+
* depending on which door it came through would be two requests to reconcile.
|
|
1480
|
+
*/
|
|
1481
|
+
declare function buyRequestText(product: ShopProduct, variant: string | null, price: ChosenPrice, qty: number): string;
|
|
1482
|
+
declare function shelfProblem(error: unknown): string | null;
|
|
1483
|
+
|
|
299
1484
|
type ChatMessage = {
|
|
300
1485
|
id: number;
|
|
301
1486
|
role: "user" | "assistant";
|
|
@@ -314,6 +1499,45 @@ type UseChat = {
|
|
|
314
1499
|
/** Chat state for one employee client: greeting, message list, streaming send. */
|
|
315
1500
|
declare const useChat: (client: HolaClient) => UseChat;
|
|
316
1501
|
|
|
1502
|
+
type UseCredits = {
|
|
1503
|
+
wallet: CreditWallet | null;
|
|
1504
|
+
/** The FIRST read only. A refresh keeps the numbers on screen and swaps them when the answer
|
|
1505
|
+
* lands: blanking a balance somebody is reading, to show a spinner, to show the same balance
|
|
1506
|
+
* again, is a flicker that reads as a fault. */
|
|
1507
|
+
loading: boolean;
|
|
1508
|
+
failure: string | null;
|
|
1509
|
+
refresh: () => void;
|
|
1510
|
+
};
|
|
1511
|
+
declare const useCredits: (credits: PortalCredits) => UseCredits;
|
|
1512
|
+
|
|
1513
|
+
type UseShop = {
|
|
1514
|
+
products: ShopProduct[];
|
|
1515
|
+
/** What an enquiry asks when the product has authored nothing of its own — and the WHOLE form
|
|
1516
|
+
* for somebody asking about something not on the shelf, which has no product to carry questions
|
|
1517
|
+
* at all. The org's, set in the console. Empty is a real answer: it means one box. */
|
|
1518
|
+
enquiryForm: ShopFormField[];
|
|
1519
|
+
/** In flight — the first read, and every `reload`. The products from the previous read stay put
|
|
1520
|
+
* underneath it, so a refresh does not blank a shop that is already on screen. */
|
|
1521
|
+
loading: boolean;
|
|
1522
|
+
/** The catalogue is bigger than `products`; the server capped the read. */
|
|
1523
|
+
truncated: boolean;
|
|
1524
|
+
/** Why the last read failed, as the SDK's own code — `rate_limited` is the one worth wording
|
|
1525
|
+
* differently, since it is the only failure that fixes itself. */
|
|
1526
|
+
error: string | null;
|
|
1527
|
+
reload: () => void;
|
|
1528
|
+
};
|
|
1529
|
+
/**
|
|
1530
|
+
* The public shelf, loaded on mount.
|
|
1531
|
+
*
|
|
1532
|
+
* Hold `shop` still: build it once at module scope, the way a portal builds its client (`export
|
|
1533
|
+
* const holaShop = createPortalShop(config)`), and hand the same object in on every render. A
|
|
1534
|
+
* `createPortalShop({...})` written inside the component body is a new object each time, and since
|
|
1535
|
+
* that identity is what this re-reads on, the storefront would fetch its own shelf in a loop —
|
|
1536
|
+
* against a route that is rate-limited per visitor, so the shop would end up empty for the person
|
|
1537
|
+
* least at fault.
|
|
1538
|
+
*/
|
|
1539
|
+
declare const useShop: (shop: PortalShop) => UseShop;
|
|
1540
|
+
|
|
317
1541
|
/** A thread message; `pending` marks an optimistic echo of the visitor's own send, shown
|
|
318
1542
|
* instantly and replaced by the authoritative message when the receive channel delivers it. */
|
|
319
1543
|
type ThreadMessage = HolaMessage & {
|
|
@@ -327,9 +1551,6 @@ type UseThread = {
|
|
|
327
1551
|
sending: boolean;
|
|
328
1552
|
error: string | null;
|
|
329
1553
|
};
|
|
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
1554
|
declare function mergeMessage(list: ThreadMessage[], incoming: HolaMessage): ThreadMessage[];
|
|
334
1555
|
/** A poll-based UNIFIED thread: the whole conversation — visitor, agent, and HUMAN turns — as one
|
|
335
1556
|
* live list via the receive channel (`client.subscribe`). Unlike `useChat` (streaming, one turn
|
|
@@ -340,4 +1561,4 @@ declare const useThread: (client: HolaClient, options?: {
|
|
|
340
1561
|
pollMs?: number;
|
|
341
1562
|
}) => UseThread;
|
|
342
1563
|
|
|
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 };
|
|
1564
|
+
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, InsufficientCredits, type OrderBilling, type PaidWithCredits, 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, creditsOfMoney, deliverableCellText, deliverableStatus, deriveFields, emptyTableResponses, facetValues, footprintOf, footprintWith, formatDeliverableDate, groupDeliverableVersions, includesFor, inferSpec, isDeliverableChecked, mergeMessage, modeOf, money, normalizeDeliverableColType, normalizeDeliverableItem, orderBilling, parseDashboardSpec, parseDeliverableItems, parseDeliverableTable, parseTableResponses, paymentAsked, placeWidgets, priceFor, priceLabel, publishedVersionOf, quantityRange, quoteSchedule, renderModel, setResponse, setTableReview, shelfPriceLabel, shelfProblem, splitLayout, textDeltaOf, thinkingDeltaOf, toolEventOf, totalFor, useArtifactUrl, useChat, useCredits, useHolaClient, useShop, useThread };
|