@evinvest/sites-contract 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/dist/booking-config.d.ts +22 -0
- package/dist/booking-config.js +101 -0
- package/dist/booking.d.ts +2 -23
- package/dist/booking.js +5 -100
- package/dist/experiments.js +1 -1
- package/dist/generated/booking-codes.d.ts +2 -0
- package/dist/generated/booking-codes.js +2 -0
- package/dist/generated/booking-limits.d.ts +6 -0
- package/dist/generated/booking-limits.js +7 -0
- package/dist/generated/booking-providers.d.ts +1 -0
- package/dist/generated/booking-providers.js +2 -0
- package/dist/generated/booking-statuses.d.ts +2 -0
- package/dist/generated/booking-statuses.js +2 -0
- package/dist/generated/day-parts.d.ts +2 -0
- package/dist/generated/day-parts.js +2 -0
- package/dist/generated/days.d.ts +1 -0
- package/dist/generated/days.js +2 -0
- package/dist/generated/index.d.ts +24 -0
- package/dist/generated/index.js +25 -0
- package/dist/generated/input-kinds.d.ts +2 -0
- package/dist/generated/input-kinds.js +2 -0
- package/dist/generated/lead-channels.d.ts +2 -0
- package/dist/generated/lead-channels.js +2 -0
- package/dist/generated/lead-flows.d.ts +2 -0
- package/dist/generated/lead-flows.js +2 -0
- package/dist/generated/lead-limits.d.ts +5 -0
- package/dist/generated/lead-limits.js +6 -0
- package/dist/generated/lead-suspects.d.ts +2 -0
- package/dist/generated/lead-suspects.js +2 -0
- package/dist/generated/messengers.d.ts +1 -0
- package/dist/generated/messengers.js +2 -0
- package/dist/generated/page-providers.d.ts +1 -0
- package/dist/generated/page-providers.js +2 -0
- package/dist/generated/place-codes.d.ts +2 -0
- package/dist/generated/place-codes.js +2 -0
- package/dist/generated/place-fields.d.ts +2 -0
- package/dist/generated/place-fields.js +2 -0
- package/dist/generated/place-limits.d.ts +11 -0
- package/dist/generated/{place.js → place-limits.js} +0 -3
- package/dist/generated/pricing-codes.d.ts +2 -0
- package/dist/generated/pricing-codes.js +2 -0
- package/dist/generated/pricing-currency.d.ts +1 -0
- package/dist/generated/pricing-currency.js +2 -0
- package/dist/generated/pricing-format.d.ts +1 -0
- package/dist/generated/pricing-format.js +2 -0
- package/dist/generated/{pricing.d.ts → pricing-limits.d.ts} +0 -4
- package/dist/generated/{pricing.js → pricing-limits.js} +0 -3
- package/dist/generated/site-channels.d.ts +2 -0
- package/dist/generated/site-channels.js +2 -0
- package/dist/index.d.ts +5 -10
- package/dist/index.js +5 -9
- package/dist/ingest.d.ts +3 -1
- package/dist/place.d.ts +2 -1
- package/dist/place.js +4 -2
- package/dist/price.d.ts +57 -0
- package/dist/price.js +78 -0
- package/dist/pricing.d.ts +2 -53
- package/dist/pricing.js +5 -79
- package/package.json +1 -1
- package/dist/generated/booking.d.ts +0 -12
- package/dist/generated/booking.js +0 -11
- package/dist/generated/codes.d.ts +0 -6
- package/dist/generated/codes.js +0 -4
- package/dist/generated/lead.d.ts +0 -13
- package/dist/generated/lead.js +0 -10
- package/dist/generated/place.d.ts +0 -15
- /package/dist/generated/{experiments.d.ts → experiment-limits.d.ts} +0 -0
- /package/dist/generated/{experiments.js → experiment-limits.js} +0 -0
package/README.md
CHANGED
|
@@ -7,18 +7,20 @@ only TS form. No runtime dependencies, no Node imports: it is bundled into landi
|
|
|
7
7
|
the panel's front end.
|
|
8
8
|
|
|
9
9
|
```text
|
|
10
|
-
src/generated
|
|
10
|
+
src/generated/*.ts panel_core's shapes, vocabularies and limits (`nix run .#gen`), a
|
|
11
|
+
module per constant
|
|
11
12
|
src/generated/wire.ts the protos as protojson: the envelope, every type's properties
|
|
12
13
|
src/*.ts the one TS implementation of what both sides run, held to
|
|
13
14
|
contracts/vectors with panel_core:
|
|
14
|
-
pricing
|
|
15
|
-
booking checkBookingConfig, checkBookingUrl
|
|
15
|
+
price priceOf: what a page runs pricing checkPricingModel
|
|
16
|
+
booking bookingRequested, isLeadRef booking-config checkBookingConfig, checkBookingUrl
|
|
16
17
|
place checkPlaceLive: a place's live settings, field by field
|
|
17
18
|
phone normalizePhone, phoneProblem
|
|
18
19
|
ingest siteEvent, ingestBody, uuidV7, isOpaqueId: the only way a site builds an event
|
|
19
20
|
experiments parseLiveExperiments, declarationProblem
|
|
20
21
|
```
|
|
21
22
|
|
|
23
|
+
A page bundles what it runs and not the panel's checks: they sit in modules of their own.
|
|
22
24
|
Checks answer `{ path, code }` problems, never words: a site logs them, the panel's editor says
|
|
23
25
|
each code in its own copy.
|
|
24
26
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { BookingCode } from "./generated/booking-codes.js";
|
|
2
|
+
import type { BookingConfig, PageProvider } from "./generated/types.js";
|
|
3
|
+
/** One thing wrong with a place's `booking`: where below it (`""`, `default`, `providers.cal_com.url`) and why. */
|
|
4
|
+
export interface BookingProblem {
|
|
5
|
+
path: string;
|
|
6
|
+
code: BookingCode;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Why `url` may not be `provider`'s booking page, or `null` (`contracts/vectors/booking/README.md`,
|
|
10
|
+
* normative): at most 2048 printable ASCII; the literal `https://`; no `#` and no `\`; an
|
|
11
|
+
* authority without `@`, `:` or `[`; a dotted DNS name whose last label is not a number; then
|
|
12
|
+
* the provider's own rule.
|
|
13
|
+
*/
|
|
14
|
+
export declare function checkBookingUrl(provider: PageProvider, url: string): BookingCode | null;
|
|
15
|
+
/**
|
|
16
|
+
* A place's `booking` checked whole: every problem, and the config when there is none. `manual`
|
|
17
|
+
* is always available, so never a key of `providers`; the default is `manual` or one of them.
|
|
18
|
+
*/
|
|
19
|
+
export declare function checkBookingConfig(v: unknown): {
|
|
20
|
+
problems: BookingProblem[];
|
|
21
|
+
config: BookingConfig | null;
|
|
22
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { isBookingProvider, isPageProvider } from "./booking.js";
|
|
2
|
+
import { BOOKING_LIMITS as LIMIT } from "./generated/booking-limits.js";
|
|
3
|
+
import { at, isObject } from "./text.js";
|
|
4
|
+
const CAL_SEGMENT = /* @__PURE__ */ new RegExp(LIMIT.calComSegment);
|
|
5
|
+
const LABEL = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$/;
|
|
6
|
+
const APPOINTMENTS = "/calendar/appointments/";
|
|
7
|
+
/**
|
|
8
|
+
* Why `url` may not be `provider`'s booking page, or `null` (`contracts/vectors/booking/README.md`,
|
|
9
|
+
* normative): at most 2048 printable ASCII; the literal `https://`; no `#` and no `\`; an
|
|
10
|
+
* authority without `@`, `:` or `[`; a dotted DNS name whose last label is not a number; then
|
|
11
|
+
* the provider's own rule.
|
|
12
|
+
*/
|
|
13
|
+
export function checkBookingUrl(provider, url) {
|
|
14
|
+
if (url.length === 0 || url.length > LIMIT.maxUrl || !/^[\x21-\x7e]+$/.test(url))
|
|
15
|
+
return "format";
|
|
16
|
+
if (!url.startsWith("https://"))
|
|
17
|
+
return "scheme";
|
|
18
|
+
if (url.includes("#"))
|
|
19
|
+
return "fragment";
|
|
20
|
+
if (url.includes("\\"))
|
|
21
|
+
return "backslash";
|
|
22
|
+
const rest = url.slice("https://".length);
|
|
23
|
+
const split = rest.search(/[/?]/);
|
|
24
|
+
const host = split < 0 ? rest : rest.slice(0, split);
|
|
25
|
+
if (host.includes("@"))
|
|
26
|
+
return "userinfo";
|
|
27
|
+
if (host.startsWith("["))
|
|
28
|
+
return "ip";
|
|
29
|
+
if (host.includes(":"))
|
|
30
|
+
return "port";
|
|
31
|
+
const labels = host.split(".");
|
|
32
|
+
if (host.length > 253 || labels.length < 2 || !labels.every(l => LABEL.test(l)))
|
|
33
|
+
return "host";
|
|
34
|
+
const last = (labels.at(-1) ?? "").toLowerCase();
|
|
35
|
+
// A WHATWG parser reads a host whose last label is a number as an IPv4 address.
|
|
36
|
+
if (/^\d+$/.test(last) || last.startsWith("0x"))
|
|
37
|
+
return "ip";
|
|
38
|
+
const name = host.toLowerCase();
|
|
39
|
+
const path = (split < 0 ? "" : rest.slice(split)).split("?")[0] ?? "";
|
|
40
|
+
switch (provider) {
|
|
41
|
+
case "link":
|
|
42
|
+
return null;
|
|
43
|
+
case "google_calendar":
|
|
44
|
+
return (name === "calendar.app.google" && path.length > 1) || (name === "calendar.google.com" && path.length > APPOINTMENTS.length && path.startsWith(APPOINTMENTS)) ? null : "google";
|
|
45
|
+
case "cal_com": {
|
|
46
|
+
if (!LIMIT.calComHosts.some(h => h === name))
|
|
47
|
+
return "cal_host";
|
|
48
|
+
const segments = path.replace(/^\//, "").split("/");
|
|
49
|
+
return path.startsWith("/") && segments.length === 2 && segments.every(s => CAL_SEGMENT.test(s)) ? null : "cal_path";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* A place's `booking` checked whole: every problem, and the config when there is none. `manual`
|
|
55
|
+
* is always available, so never a key of `providers`; the default is `manual` or one of them.
|
|
56
|
+
*/
|
|
57
|
+
export function checkBookingConfig(v) {
|
|
58
|
+
const problems = [];
|
|
59
|
+
const fail = (path, code) => problems.push({ path, code });
|
|
60
|
+
if (!isObject(v))
|
|
61
|
+
return { problems: [{ path: "", code: "shape" }], config: null };
|
|
62
|
+
if (Object.keys(v).some(k => k !== "default" && k !== "providers"))
|
|
63
|
+
fail("", "unknown_key");
|
|
64
|
+
const def = isBookingProvider(v.default) ? v.default : null;
|
|
65
|
+
if (!Object.hasOwn(v, "default"))
|
|
66
|
+
fail("default", "required");
|
|
67
|
+
else if (def === null)
|
|
68
|
+
fail("default", "default_unknown");
|
|
69
|
+
const providers = {};
|
|
70
|
+
if (!Object.hasOwn(v, "providers"))
|
|
71
|
+
fail("providers", "required");
|
|
72
|
+
else if (!isObject(v.providers))
|
|
73
|
+
fail("providers", "shape");
|
|
74
|
+
else
|
|
75
|
+
for (const [name, conf] of Object.entries(v.providers)) {
|
|
76
|
+
const path = at("providers", name);
|
|
77
|
+
if (!isBookingProvider(name))
|
|
78
|
+
fail(path, "provider_unknown");
|
|
79
|
+
else if (!isObject(conf))
|
|
80
|
+
fail(path, "provider_shape");
|
|
81
|
+
else if (Object.keys(conf).some(k => k !== "url"))
|
|
82
|
+
fail(path, "provider_extra");
|
|
83
|
+
else if (!isPageProvider(name))
|
|
84
|
+
fail(path, "manual");
|
|
85
|
+
else if (!Object.hasOwn(conf, "url"))
|
|
86
|
+
fail(at(path, "url"), "required");
|
|
87
|
+
else if (typeof conf.url !== "string")
|
|
88
|
+
fail(at(path, "url"), "url_type");
|
|
89
|
+
else {
|
|
90
|
+
const bad = checkBookingUrl(name, conf.url);
|
|
91
|
+
if (bad)
|
|
92
|
+
fail(at(path, "url"), bad);
|
|
93
|
+
else
|
|
94
|
+
providers[name] = { url: conf.url };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// A default whose own entry is refused is blamed there, not twice.
|
|
98
|
+
if (isPageProvider(def) && !Object.hasOwn(providers, def) && !problems.some(p => p.path.startsWith(at("providers", def))))
|
|
99
|
+
fail("default", "default_unset");
|
|
100
|
+
return problems.length > 0 || def === null ? { problems, config: null } : { problems, config: { default: def, providers } };
|
|
101
|
+
}
|
package/dist/booking.d.ts
CHANGED
|
@@ -1,31 +1,10 @@
|
|
|
1
|
-
import { type DayPart } from "./generated/
|
|
2
|
-
import type {
|
|
3
|
-
import type { BookingConfig, BookingProvider, PageProvider } from "./generated/types.js";
|
|
1
|
+
import { type DayPart } from "./generated/day-parts.js";
|
|
2
|
+
import type { BookingProvider, PageProvider } from "./generated/types.js";
|
|
4
3
|
import type { BookingRequestedV1 } from "./generated/wire.js";
|
|
5
|
-
/** One thing wrong with a place's `booking`: where below it (`""`, `default`, `providers.cal_com.url`) and why. */
|
|
6
|
-
export interface BookingProblem {
|
|
7
|
-
path: string;
|
|
8
|
-
code: BookingCode;
|
|
9
|
-
}
|
|
10
4
|
export declare const isBookingProvider: (v: unknown) => v is BookingProvider;
|
|
11
5
|
export declare const isPageProvider: (v: unknown) => v is PageProvider;
|
|
12
6
|
/** `lead-<row>-<8 lowercase hex>`, the row 1 or more: the panel's lead id for a site's lead. */
|
|
13
7
|
export declare const isLeadRef: (v: unknown) => v is string;
|
|
14
|
-
/**
|
|
15
|
-
* Why `url` may not be `provider`'s booking page, or `null` (`contracts/vectors/booking/README.md`,
|
|
16
|
-
* normative): at most 2048 printable ASCII; the literal `https://`; no `#` and no `\`; an
|
|
17
|
-
* authority without `@`, `:` or `[`; a dotted DNS name whose last label is not a number; then
|
|
18
|
-
* the provider's own rule.
|
|
19
|
-
*/
|
|
20
|
-
export declare function checkBookingUrl(provider: PageProvider, url: string): BookingCode | null;
|
|
21
|
-
/**
|
|
22
|
-
* A place's `booking` checked whole: every problem, and the config when there is none. `manual`
|
|
23
|
-
* is always available, so never a key of `providers`; the default is `manual` or one of them.
|
|
24
|
-
*/
|
|
25
|
-
export declare function checkBookingConfig(v: unknown): {
|
|
26
|
-
problems: BookingProblem[];
|
|
27
|
-
config: BookingConfig | null;
|
|
28
|
-
};
|
|
29
8
|
/** `booking.requested@1`'s properties, their vocabularies as the panel checks them. */
|
|
30
9
|
export type BookingRequested = BookingRequestedV1 & {
|
|
31
10
|
provider: BookingProvider;
|
package/dist/booking.js
CHANGED
|
@@ -1,108 +1,13 @@
|
|
|
1
|
-
import { BOOKING_LIMITS as LIMIT
|
|
2
|
-
import {
|
|
1
|
+
import { BOOKING_LIMITS as LIMIT } from "./generated/booking-limits.js";
|
|
2
|
+
import { BOOKING_PROVIDERS } from "./generated/booking-providers.js";
|
|
3
|
+
import { DAY_PARTS } from "./generated/day-parts.js";
|
|
4
|
+
import { PAGE_PROVIDERS } from "./generated/page-providers.js";
|
|
5
|
+
import { isDate, isObject } from "./text.js";
|
|
3
6
|
export const isBookingProvider = (v) => BOOKING_PROVIDERS.some(p => p === v);
|
|
4
7
|
export const isPageProvider = (v) => PAGE_PROVIDERS.some(p => p === v);
|
|
5
8
|
const LEAD_REF = /* @__PURE__ */ new RegExp(LIMIT.leadRef);
|
|
6
|
-
const CAL_SEGMENT = /* @__PURE__ */ new RegExp(LIMIT.calComSegment);
|
|
7
|
-
const LABEL = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$/;
|
|
8
|
-
const APPOINTMENTS = "/calendar/appointments/";
|
|
9
9
|
/** `lead-<row>-<8 lowercase hex>`, the row 1 or more: the panel's lead id for a site's lead. */
|
|
10
10
|
export const isLeadRef = (v) => typeof v === "string" && LEAD_REF.test(v);
|
|
11
|
-
/**
|
|
12
|
-
* Why `url` may not be `provider`'s booking page, or `null` (`contracts/vectors/booking/README.md`,
|
|
13
|
-
* normative): at most 2048 printable ASCII; the literal `https://`; no `#` and no `\`; an
|
|
14
|
-
* authority without `@`, `:` or `[`; a dotted DNS name whose last label is not a number; then
|
|
15
|
-
* the provider's own rule.
|
|
16
|
-
*/
|
|
17
|
-
export function checkBookingUrl(provider, url) {
|
|
18
|
-
if (url.length === 0 || url.length > LIMIT.maxUrl || !/^[\x21-\x7e]+$/.test(url))
|
|
19
|
-
return "format";
|
|
20
|
-
if (!url.startsWith("https://"))
|
|
21
|
-
return "scheme";
|
|
22
|
-
if (url.includes("#"))
|
|
23
|
-
return "fragment";
|
|
24
|
-
if (url.includes("\\"))
|
|
25
|
-
return "backslash";
|
|
26
|
-
const rest = url.slice("https://".length);
|
|
27
|
-
const split = rest.search(/[/?]/);
|
|
28
|
-
const host = split < 0 ? rest : rest.slice(0, split);
|
|
29
|
-
if (host.includes("@"))
|
|
30
|
-
return "userinfo";
|
|
31
|
-
if (host.startsWith("["))
|
|
32
|
-
return "ip";
|
|
33
|
-
if (host.includes(":"))
|
|
34
|
-
return "port";
|
|
35
|
-
const labels = host.split(".");
|
|
36
|
-
if (host.length > 253 || labels.length < 2 || !labels.every(l => LABEL.test(l)))
|
|
37
|
-
return "host";
|
|
38
|
-
const last = (labels.at(-1) ?? "").toLowerCase();
|
|
39
|
-
// A WHATWG parser reads a host whose last label is a number as an IPv4 address.
|
|
40
|
-
if (/^\d+$/.test(last) || last.startsWith("0x"))
|
|
41
|
-
return "ip";
|
|
42
|
-
const name = host.toLowerCase();
|
|
43
|
-
const path = (split < 0 ? "" : rest.slice(split)).split("?")[0] ?? "";
|
|
44
|
-
switch (provider) {
|
|
45
|
-
case "link":
|
|
46
|
-
return null;
|
|
47
|
-
case "google_calendar":
|
|
48
|
-
return (name === "calendar.app.google" && path.length > 1) || (name === "calendar.google.com" && path.length > APPOINTMENTS.length && path.startsWith(APPOINTMENTS)) ? null : "google";
|
|
49
|
-
case "cal_com": {
|
|
50
|
-
if (!LIMIT.calComHosts.some(h => h === name))
|
|
51
|
-
return "cal_host";
|
|
52
|
-
const segments = path.replace(/^\//, "").split("/");
|
|
53
|
-
return path.startsWith("/") && segments.length === 2 && segments.every(s => CAL_SEGMENT.test(s)) ? null : "cal_path";
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* A place's `booking` checked whole: every problem, and the config when there is none. `manual`
|
|
59
|
-
* is always available, so never a key of `providers`; the default is `manual` or one of them.
|
|
60
|
-
*/
|
|
61
|
-
export function checkBookingConfig(v) {
|
|
62
|
-
const problems = [];
|
|
63
|
-
const fail = (path, code) => problems.push({ path, code });
|
|
64
|
-
if (!isObject(v))
|
|
65
|
-
return { problems: [{ path: "", code: "shape" }], config: null };
|
|
66
|
-
if (Object.keys(v).some(k => k !== "default" && k !== "providers"))
|
|
67
|
-
fail("", "unknown_key");
|
|
68
|
-
const def = isBookingProvider(v.default) ? v.default : null;
|
|
69
|
-
if (!Object.hasOwn(v, "default"))
|
|
70
|
-
fail("default", "required");
|
|
71
|
-
else if (def === null)
|
|
72
|
-
fail("default", "default_unknown");
|
|
73
|
-
const providers = {};
|
|
74
|
-
if (!Object.hasOwn(v, "providers"))
|
|
75
|
-
fail("providers", "required");
|
|
76
|
-
else if (!isObject(v.providers))
|
|
77
|
-
fail("providers", "shape");
|
|
78
|
-
else
|
|
79
|
-
for (const [name, conf] of Object.entries(v.providers)) {
|
|
80
|
-
const path = at("providers", name);
|
|
81
|
-
if (!isBookingProvider(name))
|
|
82
|
-
fail(path, "provider_unknown");
|
|
83
|
-
else if (!isObject(conf))
|
|
84
|
-
fail(path, "provider_shape");
|
|
85
|
-
else if (Object.keys(conf).some(k => k !== "url"))
|
|
86
|
-
fail(path, "provider_extra");
|
|
87
|
-
else if (!isPageProvider(name))
|
|
88
|
-
fail(path, "manual");
|
|
89
|
-
else if (!Object.hasOwn(conf, "url"))
|
|
90
|
-
fail(at(path, "url"), "required");
|
|
91
|
-
else if (typeof conf.url !== "string")
|
|
92
|
-
fail(at(path, "url"), "url_type");
|
|
93
|
-
else {
|
|
94
|
-
const bad = checkBookingUrl(name, conf.url);
|
|
95
|
-
if (bad)
|
|
96
|
-
fail(at(path, "url"), bad);
|
|
97
|
-
else
|
|
98
|
-
providers[name] = { url: conf.url };
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
// A default whose own entry is refused is blamed there, not twice.
|
|
102
|
-
if (isPageProvider(def) && !Object.hasOwn(providers, def) && !problems.some(p => p.path.startsWith(at("providers", def))))
|
|
103
|
-
fail("default", "default_unset");
|
|
104
|
-
return problems.length > 0 || def === null ? { problems, config: null } : { problems, config: { default: def, providers } };
|
|
105
|
-
}
|
|
106
11
|
const isDayPart = (v) => DAY_PARTS.some(p => p === v);
|
|
107
12
|
/**
|
|
108
13
|
* `booking.requested@1`'s properties, or `null` when the panel would refuse them: a lead ref, a
|
package/dist/experiments.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EXPERIMENT_LIMITS } from "./generated/
|
|
1
|
+
import { EXPERIMENT_LIMITS } from "./generated/experiment-limits.js";
|
|
2
2
|
import { charCount, isObject, trim } from "./text.js";
|
|
3
3
|
/**
|
|
4
4
|
* The panel's answer to `GET …/brands/{brand}/experiments`, or `null` when it is not one: an
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export declare const BOOKING_CODES: readonly ["shape", "unknown_key", "required", "provider_unknown", "provider_shape", "provider_extra", "manual", "url_type", "default_unknown", "default_unset", "format", "scheme", "fragment", "backslash", "userinfo", "ip", "port", "host", "google", "cal_host", "cal_path"];
|
|
2
|
+
export type BookingCode = (typeof BOOKING_CODES)[number];
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
// AUTO-GENERATED by `nix run .#gen` — do not edit.
|
|
2
|
+
export const BOOKING_CODES = ["shape", "unknown_key", "required", "provider_unknown", "provider_shape", "provider_extra", "manual", "url_type", "default_unknown", "default_unset", "format", "scheme", "fragment", "backslash", "userinfo", "ip", "port", "host", "google", "cal_host", "cal_path"];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const BOOKING_PROVIDERS: readonly ["manual", "link", "google_calendar", "cal_com"];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const DAYS: readonly ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export * from "./booking-codes.js";
|
|
2
|
+
export * from "./booking-limits.js";
|
|
3
|
+
export * from "./booking-providers.js";
|
|
4
|
+
export * from "./booking-statuses.js";
|
|
5
|
+
export * from "./day-parts.js";
|
|
6
|
+
export * from "./days.js";
|
|
7
|
+
export * from "./experiment-limits.js";
|
|
8
|
+
export * from "./input-kinds.js";
|
|
9
|
+
export * from "./lead-channels.js";
|
|
10
|
+
export * from "./lead-flows.js";
|
|
11
|
+
export * from "./lead-limits.js";
|
|
12
|
+
export * from "./lead-suspects.js";
|
|
13
|
+
export * from "./messengers.js";
|
|
14
|
+
export * from "./page-providers.js";
|
|
15
|
+
export * from "./place-codes.js";
|
|
16
|
+
export * from "./place-fields.js";
|
|
17
|
+
export * from "./place-limits.js";
|
|
18
|
+
export * from "./pricing-codes.js";
|
|
19
|
+
export * from "./pricing-currency.js";
|
|
20
|
+
export * from "./pricing-format.js";
|
|
21
|
+
export * from "./pricing-limits.js";
|
|
22
|
+
export * from "./site-channels.js";
|
|
23
|
+
export * from "./types.js";
|
|
24
|
+
export * from "./wire.js";
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// AUTO-GENERATED by `nix run .#gen` — do not edit.
|
|
2
|
+
export * from "./booking-codes.js";
|
|
3
|
+
export * from "./booking-limits.js";
|
|
4
|
+
export * from "./booking-providers.js";
|
|
5
|
+
export * from "./booking-statuses.js";
|
|
6
|
+
export * from "./day-parts.js";
|
|
7
|
+
export * from "./days.js";
|
|
8
|
+
export * from "./experiment-limits.js";
|
|
9
|
+
export * from "./input-kinds.js";
|
|
10
|
+
export * from "./lead-channels.js";
|
|
11
|
+
export * from "./lead-flows.js";
|
|
12
|
+
export * from "./lead-limits.js";
|
|
13
|
+
export * from "./lead-suspects.js";
|
|
14
|
+
export * from "./messengers.js";
|
|
15
|
+
export * from "./page-providers.js";
|
|
16
|
+
export * from "./place-codes.js";
|
|
17
|
+
export * from "./place-fields.js";
|
|
18
|
+
export * from "./place-limits.js";
|
|
19
|
+
export * from "./pricing-codes.js";
|
|
20
|
+
export * from "./pricing-currency.js";
|
|
21
|
+
export * from "./pricing-format.js";
|
|
22
|
+
export * from "./pricing-limits.js";
|
|
23
|
+
export * from "./site-channels.js";
|
|
24
|
+
export * from "./types.js";
|
|
25
|
+
export * from "./wire.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const MESSENGERS: readonly ["whatsapp", "telegram"];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const PAGE_PROVIDERS: readonly ["link", "google_calendar", "cal_com"];
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export declare const PLACE_CODES: readonly ["object", "unknown_field", "required", "type", "blank", "too_long", "control", "e164", "telegram_bot", "messenger_unknown", "boolean", "shape", "empty", "too_many", "day", "duplicate", "time", "closes_before_opens", "postal_code", "coordinate", "https_url", "review_url", "locale", "rating_value", "rating_count", "instant"];
|
|
2
|
+
export type PlaceCode = (typeof PLACE_CODES)[number];
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
// AUTO-GENERATED by `nix run .#gen` — do not edit.
|
|
2
|
+
export const PLACE_CODES = ["object", "unknown_field", "required", "type", "blank", "too_long", "control", "e164", "telegram_bot", "messenger_unknown", "boolean", "shape", "empty", "too_many", "day", "duplicate", "time", "closes_before_opens", "postal_code", "coordinate", "https_url", "review_url", "locale", "rating_value", "rating_count", "instant"];
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare const PLACE_LIMITS: {
|
|
2
|
+
readonly e164: "^\\+[1-9][0-9]{6,14}$";
|
|
3
|
+
readonly maxArea: 200;
|
|
4
|
+
readonly maxHours: 14;
|
|
5
|
+
readonly maxLocales: 8;
|
|
6
|
+
readonly maxText: 200;
|
|
7
|
+
readonly maxUrl: 2048;
|
|
8
|
+
readonly reviewHosts: readonly ["g.page", "search.google.com"];
|
|
9
|
+
readonly telegramBot: "^[A-Za-z][A-Za-z0-9_]{1,28}[Bb][Oo][Tt]$";
|
|
10
|
+
readonly time: "^([01][0-9]|2[0-3]):[0-5][0-9]$";
|
|
11
|
+
};
|
|
@@ -1,7 +1,4 @@
|
|
|
1
1
|
// AUTO-GENERATED by `nix run .#gen` — do not edit.
|
|
2
|
-
export const DAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
|
|
3
|
-
export const MESSENGERS = ["whatsapp", "telegram"];
|
|
4
|
-
export const PLACE_FIELDS = ["phone", "whatsapp", "telegram", "messengers", "hours", "serviceArea", "address", "geo", "storefrontPhoto", "landmark", "rating", "booking", "reviewUrl"];
|
|
5
2
|
export const PLACE_LIMITS = {
|
|
6
3
|
"e164": "^\\+[1-9][0-9]{6,14}$",
|
|
7
4
|
"maxArea": 200,
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export declare const PRICING_CODES: readonly ["object", "missing", "unknownField", "format", "currency", "date", "int", "slug", "labels", "locale", "label", "list", "duplicate", "kind", "needKind", "noInput", "dearest", "labelMissing"];
|
|
2
|
+
export type PricingCode = (typeof PRICING_CODES)[number];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const PRICING_CURRENCY = "EUR";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const PRICING_FORMAT = 1;
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
export declare const PRICING_FORMAT = 1;
|
|
2
|
-
export declare const PRICING_CURRENCY = "EUR";
|
|
3
1
|
export declare const PRICING_LIMITS: {
|
|
4
2
|
readonly locale: "^[a-z]{2}(-[A-Z]{2})?$";
|
|
5
3
|
readonly maxDiscountBp: 10000;
|
|
@@ -12,5 +10,3 @@ export declare const PRICING_LIMITS: {
|
|
|
12
10
|
readonly maxPriceCents: 100000000;
|
|
13
11
|
readonly slug: "^[a-z0-9_-]{1,40}$";
|
|
14
12
|
};
|
|
15
|
-
export declare const INPUT_KINDS: readonly ["add", "multiply", "discount"];
|
|
16
|
-
export type PricingInputKind = (typeof INPUT_KINDS)[number];
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
// AUTO-GENERATED by `nix run .#gen` — do not edit.
|
|
2
|
-
export const PRICING_FORMAT = 1;
|
|
3
|
-
export const PRICING_CURRENCY = "EUR";
|
|
4
2
|
export const PRICING_LIMITS = {
|
|
5
3
|
"locale": "^[a-z]{2}(-[A-Z]{2})?$",
|
|
6
4
|
"maxDiscountBp": 10000,
|
|
@@ -13,4 +11,3 @@ export const PRICING_LIMITS = {
|
|
|
13
11
|
"maxPriceCents": 100000000,
|
|
14
12
|
"slug": "^[a-z0-9_-]{1,40}$",
|
|
15
13
|
};
|
|
16
|
-
export const INPUT_KINDS = ["add", "multiply", "discount"];
|
package/dist/index.d.ts
CHANGED
|
@@ -1,14 +1,9 @@
|
|
|
1
|
-
export * from "./generated/
|
|
2
|
-
export
|
|
3
|
-
export
|
|
4
|
-
export * from "./generated/lead.js";
|
|
5
|
-
export * from "./generated/place.js";
|
|
6
|
-
export * from "./generated/pricing.js";
|
|
7
|
-
export type * from "./generated/types.js";
|
|
8
|
-
export * from "./generated/wire.js";
|
|
9
|
-
export { checkBookingConfig, checkBookingUrl, bookingRequested, isBookingProvider, isLeadRef, isPageProvider, preferredDateInWindow, type BookingProblem, type BookingRequested } from "./booking.js";
|
|
1
|
+
export * from "./generated/index.js";
|
|
2
|
+
export { bookingRequested, isBookingProvider, isLeadRef, isPageProvider, preferredDateInWindow, type BookingRequested } from "./booking.js";
|
|
3
|
+
export { checkBookingConfig, checkBookingUrl, type BookingProblem } from "./booking-config.js";
|
|
10
4
|
export { declarationProblem, parseLiveExperiments, type DeclarationProblem } from "./experiments.js";
|
|
11
5
|
export { ingestBody, isOpaqueId, siteEvent, uuidV7, type LeadCreated, type SiteEvent, type SiteEventHead, type SiteProperties } from "./ingest.js";
|
|
12
6
|
export { normalizePhone, phoneProblem } from "./phone.js";
|
|
13
7
|
export { checkPlaceLive, isInstant, type PlaceProblem } from "./place.js";
|
|
14
|
-
export {
|
|
8
|
+
export { mulBp, priceOf, roundTo, type Price, type PriceLine, type PricingInputs, type PricingLabels } from "./price.js";
|
|
9
|
+
export { checkPricingModel, type PricingProblem } from "./pricing.js";
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,9 @@
|
|
|
1
|
-
export * from "./generated/
|
|
2
|
-
export
|
|
3
|
-
export
|
|
4
|
-
export * from "./generated/lead.js";
|
|
5
|
-
export * from "./generated/place.js";
|
|
6
|
-
export * from "./generated/pricing.js";
|
|
7
|
-
export * from "./generated/wire.js";
|
|
8
|
-
export { checkBookingConfig, checkBookingUrl, bookingRequested, isBookingProvider, isLeadRef, isPageProvider, preferredDateInWindow } from "./booking.js";
|
|
1
|
+
export * from "./generated/index.js";
|
|
2
|
+
export { bookingRequested, isBookingProvider, isLeadRef, isPageProvider, preferredDateInWindow } from "./booking.js";
|
|
3
|
+
export { checkBookingConfig, checkBookingUrl } from "./booking-config.js";
|
|
9
4
|
export { declarationProblem, parseLiveExperiments } from "./experiments.js";
|
|
10
5
|
export { ingestBody, isOpaqueId, siteEvent, uuidV7 } from "./ingest.js";
|
|
11
6
|
export { normalizePhone, phoneProblem } from "./phone.js";
|
|
12
7
|
export { checkPlaceLive, isInstant } from "./place.js";
|
|
13
|
-
export {
|
|
8
|
+
export { mulBp, priceOf, roundTo } from "./price.js";
|
|
9
|
+
export { checkPricingModel } from "./pricing.js";
|
package/dist/ingest.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { BookingRequested } from "./booking.js";
|
|
2
|
-
import type { LeadFlow
|
|
2
|
+
import type { LeadFlow } from "./generated/lead-flows.js";
|
|
3
|
+
import type { LeadSuspect } from "./generated/lead-suspects.js";
|
|
4
|
+
import type { SiteChannel } from "./generated/site-channels.js";
|
|
3
5
|
import { type Event, type ExperimentsDeclaredV1, type IngestRequest, type LeadCreatedV1, type Registered, type SiteEventKey, type Subject } from "./generated/wire.js";
|
|
4
6
|
/** `lead.created@1`'s properties, their vocabularies as the panel checks them. */
|
|
5
7
|
export type LeadCreated = LeadCreatedV1 & {
|
package/dist/place.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { BookingCode
|
|
1
|
+
import type { BookingCode } from "./generated/booking-codes.js";
|
|
2
|
+
import type { PlaceCode } from "./generated/place-codes.js";
|
|
2
3
|
import type { PlaceLive } from "./generated/types.js";
|
|
3
4
|
/** One thing wrong with a place's settings: its path (`phone`, `hours[0].opens`, `booking.providers.cal_com.url`) and why. */
|
|
4
5
|
export interface PlaceProblem {
|
package/dist/place.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { checkBookingConfig } from "./booking.js";
|
|
2
|
-
import { DAYS
|
|
1
|
+
import { checkBookingConfig } from "./booking-config.js";
|
|
2
|
+
import { DAYS } from "./generated/days.js";
|
|
3
|
+
import { MESSENGERS } from "./generated/messengers.js";
|
|
4
|
+
import { PLACE_LIMITS as LIMIT } from "./generated/place-limits.js";
|
|
3
5
|
import { at, charCount, hasControl, hasWhitespace, isDate, isObject, trim } from "./text.js";
|
|
4
6
|
const E164 = /* @__PURE__ */ new RegExp(LIMIT.e164);
|
|
5
7
|
const TELEGRAM_BOT = /* @__PURE__ */ new RegExp(LIMIT.telegramBot);
|
package/dist/price.d.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { PricingInputKind } from "./generated/input-kinds.js";
|
|
2
|
+
import type { PricingInput, PricingModel } from "./generated/types.js";
|
|
3
|
+
/** The answers to an estimate's inputs: input id → option id. */
|
|
4
|
+
export type PricingInputs = Readonly<Record<string, string>>;
|
|
5
|
+
/** The labels of an input or an option, by locale. */
|
|
6
|
+
export type PricingLabels = PricingInput["labels"];
|
|
7
|
+
/** The effect of each option of an input, in the unit its kind says. */
|
|
8
|
+
export declare function effects(input: PricingInput): {
|
|
9
|
+
id: string;
|
|
10
|
+
effect: number;
|
|
11
|
+
}[];
|
|
12
|
+
/**
|
|
13
|
+
* `cents × bp / 10 000`, rounded half up to the cent. Integers throughout: the product is exact
|
|
14
|
+
* (the model's limits keep it under 2^53), and the division is done on the remainder.
|
|
15
|
+
*/
|
|
16
|
+
export declare function mulBp(cents: number, bp: number): number;
|
|
17
|
+
/** Half up to a multiple of `step` (≥ 1). */
|
|
18
|
+
export declare function roundTo(cents: number, step: number): number;
|
|
19
|
+
/**
|
|
20
|
+
* One line of how a price was reached; `cents` is what the line changed, so the lines add up to
|
|
21
|
+
* the total. `base` is the need's base (or the fixed price); `rounding` and `minimum` appear only
|
|
22
|
+
* when they changed something.
|
|
23
|
+
*/
|
|
24
|
+
export type PriceLine = {
|
|
25
|
+
kind: "base";
|
|
26
|
+
cents: number;
|
|
27
|
+
} | {
|
|
28
|
+
kind: PricingInputKind;
|
|
29
|
+
input: string;
|
|
30
|
+
option: string;
|
|
31
|
+
cents: number;
|
|
32
|
+
} | {
|
|
33
|
+
kind: "rounding";
|
|
34
|
+
cents: number;
|
|
35
|
+
} | {
|
|
36
|
+
kind: "minimum";
|
|
37
|
+
cents: number;
|
|
38
|
+
};
|
|
39
|
+
export interface Price {
|
|
40
|
+
cents: number;
|
|
41
|
+
breakdown: readonly PriceLine[];
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* What a need costs for these answers (`contracts/vectors/pricing/README.md`, normative):
|
|
45
|
+
*
|
|
46
|
+
* 1. the need's base, plus every `add` input, in the need's input order;
|
|
47
|
+
* 2. times every `multiply` input, in order, each product rounded half up to the cent at once;
|
|
48
|
+
* 3. every `discount` input, in order, as a multiplication by `10 000 − discountBp`, rounded the
|
|
49
|
+
* same way;
|
|
50
|
+
* 4. the total rounded half up to a multiple of `roundToCents`;
|
|
51
|
+
* 5. raised to `minimumCents`.
|
|
52
|
+
*
|
|
53
|
+
* A `fixed` need is its price, as is. `null`: the need is not priced, or an input is unanswered
|
|
54
|
+
* or answered with an option it does not have — never a guess. Answers to inputs the need does
|
|
55
|
+
* not ask are ignored.
|
|
56
|
+
*/
|
|
57
|
+
export declare function priceOf(model: PricingModel, need: string, inputs: PricingInputs): Price | null;
|
package/dist/price.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/** The effect of each option of an input, in the unit its kind says. */
|
|
2
|
+
export function effects(input) {
|
|
3
|
+
switch (input.kind) {
|
|
4
|
+
case "add":
|
|
5
|
+
return input.options.map(o => ({ id: o.id, effect: o.addCents }));
|
|
6
|
+
case "multiply":
|
|
7
|
+
return input.options.map(o => ({ id: o.id, effect: o.multiplyBp }));
|
|
8
|
+
case "discount":
|
|
9
|
+
return input.options.map(o => ({ id: o.id, effect: o.discountBp }));
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
const BP = 10_000;
|
|
13
|
+
/**
|
|
14
|
+
* `cents × bp / 10 000`, rounded half up to the cent. Integers throughout: the product is exact
|
|
15
|
+
* (the model's limits keep it under 2^53), and the division is done on the remainder.
|
|
16
|
+
*/
|
|
17
|
+
export function mulBp(cents, bp) {
|
|
18
|
+
const n = cents * bp + BP / 2;
|
|
19
|
+
return (n - (n % BP)) / BP;
|
|
20
|
+
}
|
|
21
|
+
/** Half up to a multiple of `step` (≥ 1). */
|
|
22
|
+
export function roundTo(cents, step) {
|
|
23
|
+
const rest = cents % step;
|
|
24
|
+
return rest * 2 >= step ? cents - rest + step : cents - rest;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* What a need costs for these answers (`contracts/vectors/pricing/README.md`, normative):
|
|
28
|
+
*
|
|
29
|
+
* 1. the need's base, plus every `add` input, in the need's input order;
|
|
30
|
+
* 2. times every `multiply` input, in order, each product rounded half up to the cent at once;
|
|
31
|
+
* 3. every `discount` input, in order, as a multiplication by `10 000 − discountBp`, rounded the
|
|
32
|
+
* same way;
|
|
33
|
+
* 4. the total rounded half up to a multiple of `roundToCents`;
|
|
34
|
+
* 5. raised to `minimumCents`.
|
|
35
|
+
*
|
|
36
|
+
* A `fixed` need is its price, as is. `null`: the need is not priced, or an input is unanswered
|
|
37
|
+
* or answered with an option it does not have — never a guess. Answers to inputs the need does
|
|
38
|
+
* not ask are ignored.
|
|
39
|
+
*/
|
|
40
|
+
export function priceOf(model, need, inputs) {
|
|
41
|
+
const pricing = Object.hasOwn(model.needs, need) ? model.needs[need] : undefined;
|
|
42
|
+
if (!pricing)
|
|
43
|
+
return null;
|
|
44
|
+
if (pricing.kind === "fixed")
|
|
45
|
+
return { cents: pricing.cents, breakdown: [{ kind: "base", cents: pricing.cents }] };
|
|
46
|
+
const answers = [];
|
|
47
|
+
for (const id of pricing.inputs) {
|
|
48
|
+
const input = model.inputs.find(i => i.id === id);
|
|
49
|
+
const answer = Object.hasOwn(inputs, id) ? inputs[id] : undefined;
|
|
50
|
+
const option = input && effects(input).find(o => o.id === answer);
|
|
51
|
+
if (!input || !option)
|
|
52
|
+
return null;
|
|
53
|
+
answers.push({ input, option: option.id, effect: option.effect });
|
|
54
|
+
}
|
|
55
|
+
const lines = [{ kind: "base", cents: pricing.baseCents }];
|
|
56
|
+
let total = pricing.baseCents;
|
|
57
|
+
const step = (kind, apply) => {
|
|
58
|
+
for (const a of answers) {
|
|
59
|
+
if (a.input.kind !== kind)
|
|
60
|
+
continue;
|
|
61
|
+
const next = apply(total, a.effect);
|
|
62
|
+
lines.push({ kind, input: a.input.id, option: a.option, cents: next - total });
|
|
63
|
+
total = next;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
step("add", (t, cents) => t + cents);
|
|
67
|
+
step("multiply", (t, bp) => mulBp(t, bp));
|
|
68
|
+
step("discount", (t, bp) => mulBp(t, BP - bp));
|
|
69
|
+
const rounded = roundTo(total, model.roundToCents);
|
|
70
|
+
if (rounded !== total)
|
|
71
|
+
lines.push({ kind: "rounding", cents: rounded - total });
|
|
72
|
+
total = rounded;
|
|
73
|
+
if (total < model.minimumCents) {
|
|
74
|
+
lines.push({ kind: "minimum", cents: model.minimumCents - total });
|
|
75
|
+
total = model.minimumCents;
|
|
76
|
+
}
|
|
77
|
+
return { cents: total, breakdown: lines };
|
|
78
|
+
}
|
package/dist/pricing.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import type { PricingCode } from "./generated/codes.js";
|
|
2
|
-
import {
|
|
3
|
-
import type { PricingInput, PricingModel } from "./generated/types.js";
|
|
1
|
+
import type { PricingCode } from "./generated/pricing-codes.js";
|
|
2
|
+
import type { PricingModel } from "./generated/types.js";
|
|
4
3
|
/**
|
|
5
4
|
* One thing wrong with a model: its path from the model's root (`inputs[0].options[2].addCents`,
|
|
6
5
|
* `""` for the model itself), the code the panel names it by too, and what the code's words
|
|
@@ -11,10 +10,6 @@ export interface PricingProblem {
|
|
|
11
10
|
code: PricingCode;
|
|
12
11
|
vars: Readonly<Record<string, string | number>>;
|
|
13
12
|
}
|
|
14
|
-
/** The answers to an estimate's inputs: input id → option id. */
|
|
15
|
-
export type PricingInputs = Readonly<Record<string, string>>;
|
|
16
|
-
/** The labels of an input or an option, by locale. */
|
|
17
|
-
export type PricingLabels = PricingInput["labels"];
|
|
18
13
|
/**
|
|
19
14
|
* Every reason `value` is not a pricing model, and the model rebuilt from what was checked when
|
|
20
15
|
* there is none: taken whole or not at all, unknown fields refused rather than ignored. With
|
|
@@ -24,49 +19,3 @@ export declare function checkPricingModel(value: unknown, locales?: readonly str
|
|
|
24
19
|
problems: PricingProblem[];
|
|
25
20
|
model: PricingModel | null;
|
|
26
21
|
};
|
|
27
|
-
/**
|
|
28
|
-
* `cents × bp / 10 000`, rounded half up to the cent. Integers throughout: the product is exact
|
|
29
|
-
* (the model's limits keep it under 2^53), and the division is done on the remainder.
|
|
30
|
-
*/
|
|
31
|
-
export declare function mulBp(cents: number, bp: number): number;
|
|
32
|
-
/** Half up to a multiple of `step` (≥ 1). */
|
|
33
|
-
export declare function roundTo(cents: number, step: number): number;
|
|
34
|
-
/**
|
|
35
|
-
* One line of how a price was reached; `cents` is what the line changed, so the lines add up to
|
|
36
|
-
* the total. `base` is the need's base (or the fixed price); `rounding` and `minimum` appear only
|
|
37
|
-
* when they changed something.
|
|
38
|
-
*/
|
|
39
|
-
export type PriceLine = {
|
|
40
|
-
kind: "base";
|
|
41
|
-
cents: number;
|
|
42
|
-
} | {
|
|
43
|
-
kind: PricingInputKind;
|
|
44
|
-
input: string;
|
|
45
|
-
option: string;
|
|
46
|
-
cents: number;
|
|
47
|
-
} | {
|
|
48
|
-
kind: "rounding";
|
|
49
|
-
cents: number;
|
|
50
|
-
} | {
|
|
51
|
-
kind: "minimum";
|
|
52
|
-
cents: number;
|
|
53
|
-
};
|
|
54
|
-
export interface Price {
|
|
55
|
-
cents: number;
|
|
56
|
-
breakdown: readonly PriceLine[];
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* What a need costs for these answers (`contracts/vectors/pricing/README.md`, normative):
|
|
60
|
-
*
|
|
61
|
-
* 1. the need's base, plus every `add` input, in the need's input order;
|
|
62
|
-
* 2. times every `multiply` input, in order, each product rounded half up to the cent at once;
|
|
63
|
-
* 3. every `discount` input, in order, as a multiplication by `10 000 − discountBp`, rounded the
|
|
64
|
-
* same way;
|
|
65
|
-
* 4. the total rounded half up to a multiple of `roundToCents`;
|
|
66
|
-
* 5. raised to `minimumCents`.
|
|
67
|
-
*
|
|
68
|
-
* A `fixed` need is its price, as is. `null`: the need is not priced, or an input is unanswered
|
|
69
|
-
* or answered with an option it does not have — never a guess. Answers to inputs the need does
|
|
70
|
-
* not ask are ignored.
|
|
71
|
-
*/
|
|
72
|
-
export declare function priceOf(model: PricingModel, need: string, inputs: PricingInputs): Price | null;
|
package/dist/pricing.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import { INPUT_KINDS
|
|
1
|
+
import { INPUT_KINDS } from "./generated/input-kinds.js";
|
|
2
|
+
import { PRICING_CURRENCY } from "./generated/pricing-currency.js";
|
|
3
|
+
import { PRICING_FORMAT } from "./generated/pricing-format.js";
|
|
4
|
+
import { PRICING_LIMITS as LIMIT } from "./generated/pricing-limits.js";
|
|
5
|
+
import { effects, mulBp } from "./price.js";
|
|
2
6
|
import { at, isDate, isObject, trim } from "./text.js";
|
|
3
7
|
const SLUG = /* @__PURE__ */ new RegExp(LIMIT.slug);
|
|
4
8
|
const LOCALE = /* @__PURE__ */ new RegExp(LIMIT.locale);
|
|
@@ -116,17 +120,6 @@ function need(c, path, v, inputs) {
|
|
|
116
120
|
c.fail(at(at(path, "inputs"), i), "noInput", { id });
|
|
117
121
|
return { kind: "estimate", baseCents, inputs: ids };
|
|
118
122
|
}
|
|
119
|
-
/** The effect of each option of an input, in the unit its kind says. */
|
|
120
|
-
function effects(input) {
|
|
121
|
-
switch (input.kind) {
|
|
122
|
-
case "add":
|
|
123
|
-
return input.options.map(o => ({ id: o.id, effect: o.addCents }));
|
|
124
|
-
case "multiply":
|
|
125
|
-
return input.options.map(o => ({ id: o.id, effect: o.multiplyBp }));
|
|
126
|
-
case "discount":
|
|
127
|
-
return input.options.map(o => ({ id: o.id, effect: o.discountBp }));
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
123
|
/**
|
|
131
124
|
* The dearest answer to every input — each effect only raises the price with its value, and
|
|
132
125
|
* rounding half up is monotonic — walked step by step: if no step of it passes the cap, no
|
|
@@ -204,70 +197,3 @@ export function checkPricingModel(value, locales = []) {
|
|
|
204
197
|
return { problems: c.problems, model: null };
|
|
205
198
|
return { problems: [], model: { format: PRICING_FORMAT, currency: PRICING_CURRENCY, validFrom: String(value.validFrom), roundToCents, minimumCents, inputs, needs } };
|
|
206
199
|
}
|
|
207
|
-
const BP = 10_000;
|
|
208
|
-
/**
|
|
209
|
-
* `cents × bp / 10 000`, rounded half up to the cent. Integers throughout: the product is exact
|
|
210
|
-
* (the model's limits keep it under 2^53), and the division is done on the remainder.
|
|
211
|
-
*/
|
|
212
|
-
export function mulBp(cents, bp) {
|
|
213
|
-
const n = cents * bp + BP / 2;
|
|
214
|
-
return (n - (n % BP)) / BP;
|
|
215
|
-
}
|
|
216
|
-
/** Half up to a multiple of `step` (≥ 1). */
|
|
217
|
-
export function roundTo(cents, step) {
|
|
218
|
-
const rest = cents % step;
|
|
219
|
-
return rest * 2 >= step ? cents - rest + step : cents - rest;
|
|
220
|
-
}
|
|
221
|
-
/**
|
|
222
|
-
* What a need costs for these answers (`contracts/vectors/pricing/README.md`, normative):
|
|
223
|
-
*
|
|
224
|
-
* 1. the need's base, plus every `add` input, in the need's input order;
|
|
225
|
-
* 2. times every `multiply` input, in order, each product rounded half up to the cent at once;
|
|
226
|
-
* 3. every `discount` input, in order, as a multiplication by `10 000 − discountBp`, rounded the
|
|
227
|
-
* same way;
|
|
228
|
-
* 4. the total rounded half up to a multiple of `roundToCents`;
|
|
229
|
-
* 5. raised to `minimumCents`.
|
|
230
|
-
*
|
|
231
|
-
* A `fixed` need is its price, as is. `null`: the need is not priced, or an input is unanswered
|
|
232
|
-
* or answered with an option it does not have — never a guess. Answers to inputs the need does
|
|
233
|
-
* not ask are ignored.
|
|
234
|
-
*/
|
|
235
|
-
export function priceOf(model, need, inputs) {
|
|
236
|
-
const pricing = Object.hasOwn(model.needs, need) ? model.needs[need] : undefined;
|
|
237
|
-
if (!pricing)
|
|
238
|
-
return null;
|
|
239
|
-
if (pricing.kind === "fixed")
|
|
240
|
-
return { cents: pricing.cents, breakdown: [{ kind: "base", cents: pricing.cents }] };
|
|
241
|
-
const answers = [];
|
|
242
|
-
for (const id of pricing.inputs) {
|
|
243
|
-
const input = model.inputs.find(i => i.id === id);
|
|
244
|
-
const answer = Object.hasOwn(inputs, id) ? inputs[id] : undefined;
|
|
245
|
-
const option = input && effects(input).find(o => o.id === answer);
|
|
246
|
-
if (!input || !option)
|
|
247
|
-
return null;
|
|
248
|
-
answers.push({ input, option: option.id, effect: option.effect });
|
|
249
|
-
}
|
|
250
|
-
const lines = [{ kind: "base", cents: pricing.baseCents }];
|
|
251
|
-
let total = pricing.baseCents;
|
|
252
|
-
const step = (kind, apply) => {
|
|
253
|
-
for (const a of answers) {
|
|
254
|
-
if (a.input.kind !== kind)
|
|
255
|
-
continue;
|
|
256
|
-
const next = apply(total, a.effect);
|
|
257
|
-
lines.push({ kind, input: a.input.id, option: a.option, cents: next - total });
|
|
258
|
-
total = next;
|
|
259
|
-
}
|
|
260
|
-
};
|
|
261
|
-
step("add", (t, cents) => t + cents);
|
|
262
|
-
step("multiply", (t, bp) => mulBp(t, bp));
|
|
263
|
-
step("discount", (t, bp) => mulBp(t, BP - bp));
|
|
264
|
-
const rounded = roundTo(total, model.roundToCents);
|
|
265
|
-
if (rounded !== total)
|
|
266
|
-
lines.push({ kind: "rounding", cents: rounded - total });
|
|
267
|
-
total = rounded;
|
|
268
|
-
if (total < model.minimumCents) {
|
|
269
|
-
lines.push({ kind: "minimum", cents: model.minimumCents - total });
|
|
270
|
-
total = model.minimumCents;
|
|
271
|
-
}
|
|
272
|
-
return { cents: total, breakdown: lines };
|
|
273
|
-
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evinvest/sites-contract",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "The Service-Arb sites ↔ panel contract in TS: the shapes and vocabularies generated from the panel's Rust and protos, and the one TS implementation of what both sides run — pricing, booking and place checks, phone numbers, the ingest events a site sends.",
|
|
5
5
|
"license": "BlueOak-1.0.0",
|
|
6
6
|
"publishConfig": {
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
export declare const BOOKING_PROVIDERS: readonly ["manual", "link", "google_calendar", "cal_com"];
|
|
2
|
-
export declare const PAGE_PROVIDERS: readonly ["link", "google_calendar", "cal_com"];
|
|
3
|
-
export declare const BOOKING_LIMITS: {
|
|
4
|
-
readonly calComHosts: readonly ["cal.evinvest.ltd", "cal.com"];
|
|
5
|
-
readonly calComSegment: "^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$";
|
|
6
|
-
readonly leadRef: "^lead-[1-9][0-9]{0,18}-[0-9a-f]{8}$";
|
|
7
|
-
readonly maxUrl: 2048;
|
|
8
|
-
};
|
|
9
|
-
export declare const BOOKING_STATUSES: readonly ["none", "requested", "booked", "canceled", "done", "no_show"];
|
|
10
|
-
export type BookingStatus = (typeof BOOKING_STATUSES)[number];
|
|
11
|
-
export declare const DAY_PARTS: readonly ["morning", "afternoon", "evening"];
|
|
12
|
-
export type DayPart = (typeof DAY_PARTS)[number];
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
// AUTO-GENERATED by `nix run .#gen` — do not edit.
|
|
2
|
-
export const BOOKING_PROVIDERS = ["manual", "link", "google_calendar", "cal_com"];
|
|
3
|
-
export const PAGE_PROVIDERS = ["link", "google_calendar", "cal_com"];
|
|
4
|
-
export const BOOKING_LIMITS = {
|
|
5
|
-
"calComHosts": ["cal.evinvest.ltd", "cal.com"],
|
|
6
|
-
"calComSegment": "^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$",
|
|
7
|
-
"leadRef": "^lead-[1-9][0-9]{0,18}-[0-9a-f]{8}$",
|
|
8
|
-
"maxUrl": 2048,
|
|
9
|
-
};
|
|
10
|
-
export const BOOKING_STATUSES = ["none", "requested", "booked", "canceled", "done", "no_show"];
|
|
11
|
-
export const DAY_PARTS = ["morning", "afternoon", "evening"];
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
export declare const PRICING_CODES: readonly ["object", "missing", "unknownField", "format", "currency", "date", "int", "slug", "labels", "locale", "label", "list", "duplicate", "kind", "needKind", "noInput", "dearest", "labelMissing"];
|
|
2
|
-
export type PricingCode = (typeof PRICING_CODES)[number];
|
|
3
|
-
export declare const BOOKING_CODES: readonly ["shape", "unknown_key", "required", "provider_unknown", "provider_shape", "provider_extra", "manual", "url_type", "default_unknown", "default_unset", "format", "scheme", "fragment", "backslash", "userinfo", "ip", "port", "host", "google", "cal_host", "cal_path"];
|
|
4
|
-
export type BookingCode = (typeof BOOKING_CODES)[number];
|
|
5
|
-
export declare const PLACE_CODES: readonly ["object", "unknown_field", "required", "type", "blank", "too_long", "control", "e164", "telegram_bot", "messenger_unknown", "boolean", "shape", "empty", "too_many", "day", "duplicate", "time", "closes_before_opens", "postal_code", "coordinate", "https_url", "review_url", "locale", "rating_value", "rating_count", "instant"];
|
|
6
|
-
export type PlaceCode = (typeof PLACE_CODES)[number];
|
package/dist/generated/codes.js
DELETED
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
// AUTO-GENERATED by `nix run .#gen` — do not edit.
|
|
2
|
-
export const PRICING_CODES = ["object", "missing", "unknownField", "format", "currency", "date", "int", "slug", "labels", "locale", "label", "list", "duplicate", "kind", "needKind", "noInput", "dearest", "labelMissing"];
|
|
3
|
-
export const BOOKING_CODES = ["shape", "unknown_key", "required", "provider_unknown", "provider_shape", "provider_extra", "manual", "url_type", "default_unknown", "default_unset", "format", "scheme", "fragment", "backslash", "userinfo", "ip", "port", "host", "google", "cal_host", "cal_path"];
|
|
4
|
-
export const PLACE_CODES = ["object", "unknown_field", "required", "type", "blank", "too_long", "control", "e164", "telegram_bot", "messenger_unknown", "boolean", "shape", "empty", "too_many", "day", "duplicate", "time", "closes_before_opens", "postal_code", "coordinate", "https_url", "review_url", "locale", "rating_value", "rating_count", "instant"];
|
package/dist/generated/lead.d.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
export declare const LEAD_LIMITS: {
|
|
2
|
-
readonly analyticsId: "^[A-Za-z0-9._:-]{1,128}$";
|
|
3
|
-
readonly maxEstimateInputs: 12;
|
|
4
|
-
readonly messageRef: "^[A-Z]{2,4}-[0-9A-HJKMNP-TV-Z]{4,8}$";
|
|
5
|
-
};
|
|
6
|
-
export declare const LEAD_CHANNELS: readonly ["form", "phone_inbound", "callback", "whatsapp", "telegram"];
|
|
7
|
-
export type LeadChannel = (typeof LEAD_CHANNELS)[number];
|
|
8
|
-
export declare const SITE_CHANNELS: readonly ["form", "callback", "whatsapp", "telegram"];
|
|
9
|
-
export type SiteChannel = (typeof SITE_CHANNELS)[number];
|
|
10
|
-
export declare const LEAD_SUSPECTS: readonly ["rate_limited", "too_fast"];
|
|
11
|
-
export type LeadSuspect = (typeof LEAD_SUSPECTS)[number];
|
|
12
|
-
export declare const LEAD_FLOWS: readonly ["quote", "estimate", "fixed"];
|
|
13
|
-
export type LeadFlow = (typeof LEAD_FLOWS)[number];
|
package/dist/generated/lead.js
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
// AUTO-GENERATED by `nix run .#gen` — do not edit.
|
|
2
|
-
export const LEAD_LIMITS = {
|
|
3
|
-
"analyticsId": "^[A-Za-z0-9._:-]{1,128}$",
|
|
4
|
-
"maxEstimateInputs": 12,
|
|
5
|
-
"messageRef": "^[A-Z]{2,4}-[0-9A-HJKMNP-TV-Z]{4,8}$",
|
|
6
|
-
};
|
|
7
|
-
export const LEAD_CHANNELS = ["form", "phone_inbound", "callback", "whatsapp", "telegram"];
|
|
8
|
-
export const SITE_CHANNELS = ["form", "callback", "whatsapp", "telegram"];
|
|
9
|
-
export const LEAD_SUSPECTS = ["rate_limited", "too_fast"];
|
|
10
|
-
export const LEAD_FLOWS = ["quote", "estimate", "fixed"];
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
export declare const DAYS: readonly ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
|
|
2
|
-
export declare const MESSENGERS: readonly ["whatsapp", "telegram"];
|
|
3
|
-
export declare const PLACE_FIELDS: readonly ["phone", "whatsapp", "telegram", "messengers", "hours", "serviceArea", "address", "geo", "storefrontPhoto", "landmark", "rating", "booking", "reviewUrl"];
|
|
4
|
-
export type PlaceField = (typeof PLACE_FIELDS)[number];
|
|
5
|
-
export declare const PLACE_LIMITS: {
|
|
6
|
-
readonly e164: "^\\+[1-9][0-9]{6,14}$";
|
|
7
|
-
readonly maxArea: 200;
|
|
8
|
-
readonly maxHours: 14;
|
|
9
|
-
readonly maxLocales: 8;
|
|
10
|
-
readonly maxText: 200;
|
|
11
|
-
readonly maxUrl: 2048;
|
|
12
|
-
readonly reviewHosts: readonly ["g.page", "search.google.com"];
|
|
13
|
-
readonly telegramBot: "^[A-Za-z][A-Za-z0-9_]{1,28}[Bb][Oo][Tt]$";
|
|
14
|
-
readonly time: "^([01][0-9]|2[0-3]):[0-5][0-9]$";
|
|
15
|
-
};
|
|
File without changes
|
|
File without changes
|