@reblu/site-contracts 1.0.0 → 1.0.1
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/CHANGELOG.md +304 -0
- package/package.json +3 -2
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
# @reblu/site-contracts
|
|
2
|
+
|
|
3
|
+
## 1.0.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 9f54c58: REB-774 — ship `CHANGELOG.md` inside the published npm tarball for every
|
|
8
|
+
`@reblu/site-*` package (verified via `pnpm pack`: it was previously absent,
|
|
9
|
+
since `"files": ["dist"]` limited the tarball to `dist/` and npm only
|
|
10
|
+
auto-includes `README.md`, `LICENSE` and `package.json`). Consumers can now
|
|
11
|
+
read the changelog for the version they installed straight from
|
|
12
|
+
`node_modules/@reblu/<pkg>/CHANGELOG.md`.
|
|
13
|
+
|
|
14
|
+
## 1.0.0
|
|
15
|
+
|
|
16
|
+
### Major Changes
|
|
17
|
+
|
|
18
|
+
- 340db30: REB-659 — phone fields on the SITE API are now E.164, and two public boundaries
|
|
19
|
+
changed behaviour. A consuming SITE should read this before upgrading.
|
|
20
|
+
|
|
21
|
+
**Type change.** `ShopAddressInput.phone` and `ShopCheckoutInput.customerPhone`
|
|
22
|
+
widen from `string | undefined` to `string | null | undefined`. `null` is now a
|
|
23
|
+
real state meaning "clear this phone", which the server accepts and the previous
|
|
24
|
+
type could not express. Existing code that only ever assigns a string keeps
|
|
25
|
+
compiling.
|
|
26
|
+
|
|
27
|
+
**Behaviour change — `POST /api/site/contact-form` and `POST /api/site/leads`
|
|
28
|
+
(more permissive).** These used to store whatever string arrived. They now
|
|
29
|
+
resolve it to E.164 against the tenant's configured country
|
|
30
|
+
(`SiteSettings.addressCountry` in nexus). A form that posts what a person typed
|
|
31
|
+
— `922 123 456`, `(922) 123-456`, `0034 922 123 456` — keeps working and the
|
|
32
|
+
value is stored as `+34922123456`.
|
|
33
|
+
|
|
34
|
+
On `/api/site/leads` the phone stays optional: an absent key, `''` and
|
|
35
|
+
whitespace all store no phone, exactly as before.
|
|
36
|
+
|
|
37
|
+
A **non-blank** value that cannot be stored now returns an error instead of an
|
|
38
|
+
undialable string. There are two branches, and they answer differently:
|
|
39
|
+
- **shape** — shorter than 6 or longer than 32 characters, or containing
|
|
40
|
+
anything outside digits, spaces, `+`, dashes, parentheses and dots. An
|
|
41
|
+
extension always lands here, whichever way it is written (`x22`, `ext. 22`,
|
|
42
|
+
`;ext=22`, `#22`, `,22`), because every marker for one is outside that
|
|
43
|
+
allowlist — E.164 cannot represent an extension, so it is refused rather than
|
|
44
|
+
silently truncated to the switchboard. `/api/site/contact-form` answers 400;
|
|
45
|
+
`/api/site/leads` answers **422**, its long-standing status for a malformed
|
|
46
|
+
payload on any field. On that 422 the actionable message is in
|
|
47
|
+
`details.fieldErrors.phone` — the top-level `error` is the generic
|
|
48
|
+
`Validation error`.
|
|
49
|
+
- **resolution** — looks like a phone but cannot be resolved: the library does
|
|
50
|
+
not recognise it, or it lacks an international prefix while the tenant has no
|
|
51
|
+
country configured. **400** on both endpoints, with the message in `error` and
|
|
52
|
+
in `details.fieldErrors.phone`.
|
|
53
|
+
|
|
54
|
+
So: read `details.fieldErrors.phone` rather than branching on the status, and
|
|
55
|
+
do not assume the two endpoints answer with the same code for a bad shape.
|
|
56
|
+
|
|
57
|
+
On both endpoints the resolution prefers the LOCAL reading for a prefix-less
|
|
58
|
+
number over the international one, because the caller is typing (or posting)
|
|
59
|
+
into one specific tenant's form. The residual: a prefix-less number that is
|
|
60
|
+
also a valid national number of the tenant's own country is normalised into
|
|
61
|
+
that country rather than refused — for a Spanish tenant, Myanmar digits
|
|
62
|
+
`951234567` become `+34951234567`. The alternative — preferring the
|
|
63
|
+
international reading — reopens a different and worse class this file's own
|
|
64
|
+
header documents (a US number silently reading as Egyptian), so this is a
|
|
65
|
+
chosen trade, not an oversight. Quantifying how often the residual is actually
|
|
66
|
+
hit is a follow-up issue.
|
|
67
|
+
|
|
68
|
+
**Read-path change — `GET /api/site/config` (same shape, different VALUE).**
|
|
69
|
+
No field was added or removed here, but the VALUE of `address.telephone`
|
|
70
|
+
changes once the phone backfill runs against a tenant: today it can read
|
|
71
|
+
`34922123456` (the legacy shape stored before this issue); after the backfill
|
|
72
|
+
it reads `+34922123456`. Any SITE that pattern-matches or otherwise assumes the
|
|
73
|
+
prefix-less shape when rendering this field (a `tel:`/`wa.me` link builder,
|
|
74
|
+
for instance) should read this before the backfill runs against its tenant.
|
|
75
|
+
|
|
76
|
+
**Behaviour change — `POST /api/site/shop/checkout` (stricter).**
|
|
77
|
+
`customerPhone` and `shippingAddress.phone` must now be E.164 (`+34…`) or
|
|
78
|
+
absent; a bare local number is rejected. Unlike the contact form this payload is
|
|
79
|
+
assembled by the SITE from a cart rather than typed by a person at that moment,
|
|
80
|
+
so there is no "typed locally for this tenant" provenance to resolve against.
|
|
81
|
+
|
|
82
|
+
**Why the SDK does not build E.164 for you.** Every SITE would construct it
|
|
83
|
+
differently and each would be wrong in its own way — which is the defect this
|
|
84
|
+
issue fixes one layer down. The platform owns the rule because the platform is
|
|
85
|
+
the layer that knows the tenant's country. A shared formatter helper for SITEs
|
|
86
|
+
is REB-703, and it comes after this.
|
|
87
|
+
|
|
88
|
+
***
|
|
89
|
+
|
|
90
|
+
**This release is `1.0.0`, and that is deliberate — see REB-755.**
|
|
91
|
+
|
|
92
|
+
`@reblu/site-contracts` has been at `0.x` while `@reblu/site-client` reached
|
|
93
|
+
4.0.1. That ordering is hard to defend, and it had a concrete cost: consumers
|
|
94
|
+
pin `^0.5.0`, and **a caret on `0.x` does not cross the minor** (verified with
|
|
95
|
+
`semver@7`: `^0.5.0` admits `0.5.3`, not `0.6.0`). So every change — additive or
|
|
96
|
+
breaking — cost every consuming SITE repo a hand edit. The careful work of
|
|
97
|
+
deciding whether a change breaks anything bought nobody anything.
|
|
98
|
+
|
|
99
|
+
This release is breaking regardless, so the adoption is already owed. Shipping
|
|
100
|
+
`1.0.0` here means the toll and the breaking change are **one edit rather than
|
|
101
|
+
two** — a bare `1.0.0` later would make this a `2.0.0` and cost an extra
|
|
102
|
+
adoption for nothing.
|
|
103
|
+
|
|
104
|
+
From here, `^1.0.0` carries every additive change automatically. REB-651
|
|
105
|
+
(SEO fields, additive by design) lands as `1.1.0` and reaches consumers with no
|
|
106
|
+
edit. REB-665 reshapes `Config` and will be `2.0.0`.
|
|
107
|
+
|
|
108
|
+
`1.0.0` is not a claim that the contract is finished. It is a claim that
|
|
109
|
+
breakage will be signalled through the major — which we were doing anyway, into
|
|
110
|
+
a version range where it had no effect.
|
|
111
|
+
|
|
112
|
+
## 0.5.0
|
|
113
|
+
|
|
114
|
+
### Minor Changes
|
|
115
|
+
|
|
116
|
+
- 7dcfb4f: `Config` gains a `fiscal` block — the tenant's tax identifier, registered name
|
|
117
|
+
and official registry entry — plus the types `ConfigFiscal`,
|
|
118
|
+
`ConfigFiscalRegistry` and `ConfigTaxIdKind`.
|
|
119
|
+
|
|
120
|
+
A Spanish commercial site is legally required to publish its tax identifier, and
|
|
121
|
+
until now no tenant could enter one: `SiteSettings` had no `cif`, no `nif`, no
|
|
122
|
+
`taxId` and no registered name distinct from the trading name. Every SITE that
|
|
123
|
+
needed one therefore hardcoded it — the Dermamedicin footer carries
|
|
124
|
+
`B-XXXXXXXX` as a literal in its own repo, which is wrong for every other tenant
|
|
125
|
+
by construction. REB-73 (downloadable PDF invoice) was silently blocked on the
|
|
126
|
+
same gap: an invoice without the issuer's CIF is not a valid invoice.
|
|
127
|
+
|
|
128
|
+
## Shape
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
type ConfigFiscal = {
|
|
132
|
+
taxId: string | null
|
|
133
|
+
taxIdKind: 'NIF' | 'CIF' | null
|
|
134
|
+
legalName: string | null
|
|
135
|
+
registry: { name: string; number: string } | null
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// on Config:
|
|
139
|
+
fiscal: ConfigFiscal | null
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## `taxIdKind` — why the SITE is told which document it is holding
|
|
143
|
+
|
|
144
|
+
A tenant holds exactly one tax identifier, but it is not one document. In Spain
|
|
145
|
+
an autónomo files a **NIF** (8 digits + a mod-23 control letter, or a NIE for a
|
|
146
|
+
foreign resident) and every other legal form files a **CIF** (organisation
|
|
147
|
+
letter + 7 digits + a control character that is a digit for some organisation
|
|
148
|
+
letters and a letter for others). Which applies is derivable only from
|
|
149
|
+
`businessType`.
|
|
150
|
+
|
|
151
|
+
So `taxId` alone would force every SITE to re-implement that derivation, and a
|
|
152
|
+
SITE that hardcoded `CIF:` would print the wrong word next to an autónomo's
|
|
153
|
+
number. `taxIdKind` carries the platform's single derivation, and a SITE renders
|
|
154
|
+
the label verbatim:
|
|
155
|
+
|
|
156
|
+
```tsx
|
|
157
|
+
{
|
|
158
|
+
config.fiscal?.taxId && (
|
|
159
|
+
<span>
|
|
160
|
+
{config.fiscal.taxIdKind}: {config.fiscal.taxId}
|
|
161
|
+
</span>
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
`taxId` is stored normalised (uppercase, separators stripped), so it is safe to
|
|
167
|
+
render as-is.
|
|
168
|
+
|
|
169
|
+
## `legalName` carries no fallback, on purpose
|
|
170
|
+
|
|
171
|
+
`legalName` is the registered name / razón social, which routinely differs from
|
|
172
|
+
the trading name in `business.name` ("Dermamedicin" vs "Dermamedicin Canarias,
|
|
173
|
+
S.L."). It is `null` when unset and does **not** silently fall back to the
|
|
174
|
+
trading name: a legal footer and an invoice need the registered one, and quietly
|
|
175
|
+
substituting the other would publish the wrong name on a document with legal
|
|
176
|
+
weight. A SITE that wants a fallback writes it itself, seeing that it is doing
|
|
177
|
+
so.
|
|
178
|
+
|
|
179
|
+
## `registry` is sector-agnostic, and both-or-nothing
|
|
180
|
+
|
|
181
|
+
`registry` is a free-text `name` plus a `number`, not a `healthRegistryNumber`.
|
|
182
|
+
Every regulated sector is entered in one of these and only the registry's name
|
|
183
|
+
differs: Registro Sanitario for a clinic, RGSEAA for a restaurant, Registro de
|
|
184
|
+
Turismo for a hotel, a colegiado number for a self-employed professional, a
|
|
185
|
+
Registro Mercantil entry for a company. Naming the field after the first
|
|
186
|
+
tenant's sector would put medical vocabulary into a niche-agnostic contract,
|
|
187
|
+
which is the defect REB-628 exists to undo.
|
|
188
|
+
|
|
189
|
+
The object is `null` unless **both** members were filled in. A bare number with
|
|
190
|
+
no registry named is unreadable in a footer; a registry named with no number is
|
|
191
|
+
decorative.
|
|
192
|
+
|
|
193
|
+
## Absence
|
|
194
|
+
|
|
195
|
+
`fiscal` is `null` when the tenant has entered nothing at all — the same shape
|
|
196
|
+
`address` and `social` already use. That is the signal to render **no** legal
|
|
197
|
+
block, rather than a block of empty labels. The underlying columns are nullable
|
|
198
|
+
with no default and are never backfilled, so "empty" here means "the owner has
|
|
199
|
+
never filled this in", never "filled in blank".
|
|
200
|
+
|
|
201
|
+
Additive: no existing field changed and no existing consumer breaks.
|
|
202
|
+
|
|
203
|
+
## 0.4.0
|
|
204
|
+
|
|
205
|
+
### Minor Changes
|
|
206
|
+
|
|
207
|
+
- eef13bf: Expose typed professional-profile fields on `Page`/`pages.list`, widen the list filter beyond the legacy DB page type, and add the pagination/sort params `pages.list` was missing.
|
|
208
|
+
|
|
209
|
+
`pageType='team'` pages carry a structured, sector-agnostic professional
|
|
210
|
+
profile (role, structured CV, social links) — previously trapped in
|
|
211
|
+
`Page.metadata`, which never reached the wire. `pageListItemSchema` and
|
|
212
|
+
`pageDetailSchema` now expose it as a typed `professional` field (`null` for
|
|
213
|
+
every other pageType).
|
|
214
|
+
- `@reblu/site-contracts` (minor, additive-only) publishes `ProfessionalSummary`,
|
|
215
|
+
`ProfessionalProfile`, `ProfessionalCv`, `ProfessionalCvEntry`,
|
|
216
|
+
`ProfessionalLanguage`, `ProfessionalLanguageLevel`, `ProfessionalSocialLink`,
|
|
217
|
+
`ProfessionalSocialPlatform`, and `PagesListSort`.
|
|
218
|
+
- `@reblu/site-client` (major — see below) `pages.list({ type })` is widened
|
|
219
|
+
from `LegacyPageType` (SERVICE|CUSTOM) to the flexible `PageType`
|
|
220
|
+
(service|custom|treatments|specialties|team|generic) — the value space the
|
|
221
|
+
route actually filters on. A typed SITE can now request `type: 'team'`
|
|
222
|
+
without escaping its own types. `type` also accepts an array for a
|
|
223
|
+
multi-type filter (repeated `?type=`).
|
|
224
|
+
- `pages.list` also gains `page`/`pageSize`/`sort` — `pagesListParamsSchema`
|
|
225
|
+
already accepted them and the route already returns a populated `meta`
|
|
226
|
+
(total/page/limit/pages), but `PagesListParams` exposed none of it, so a
|
|
227
|
+
typed caller had no way to request page 2 or change sort. Note the param
|
|
228
|
+
is `pageSize`, not `limit` — pages and blog genuinely differ (this route
|
|
229
|
+
reads `pageSize`; blog's reads `limit`), while the response `meta.limit`
|
|
230
|
+
key is the same word for a different request param on this endpoint.
|
|
231
|
+
|
|
232
|
+
**Breaking (major, not minor):** `PagesListParams.type`'s value space changes
|
|
233
|
+
from `LegacyPageType` to `PageType` — a caller passing `'SERVICE'`/`'CUSTOM'`
|
|
234
|
+
literally will no longer typecheck. No runtime behavior changes for existing
|
|
235
|
+
callers: the route was already silently dropping those values, since it only
|
|
236
|
+
ever validated the lowercase flexible set — but the type change itself is a
|
|
237
|
+
real breaking change for compilation, so this ships as major rather than
|
|
238
|
+
minor even though nothing behaves differently at runtime.
|
|
239
|
+
|
|
240
|
+
## 0.3.0
|
|
241
|
+
|
|
242
|
+
### Minor Changes
|
|
243
|
+
|
|
244
|
+
- 495d32d: Relax the contact-form input contract and add a `NOT_CONFIGURED` contact-form mode.
|
|
245
|
+
|
|
246
|
+
`ContactFormInput` now marks `surname` and `businessName` OPTIONAL. The contract
|
|
247
|
+
had required both from every caller, which made it a B2B lead-capture shape: a
|
|
248
|
+
clinic patient, a diner or a shopper has no company name, and a payload without
|
|
249
|
+
one died with a 400 before the captcha was ever consulted. Purely a relaxation —
|
|
250
|
+
a caller that already sends both keeps typechecking unchanged.
|
|
251
|
+
|
|
252
|
+
`ContactFormMode` gains a fifth member, `NOT_CONFIGURED`. No tenant was ever
|
|
253
|
+
provisioned with a `site_integration_config` row, and every read site coalesced
|
|
254
|
+
the absent row to `DISABLED` — so a brand-new tenant's SITE was told its owner had
|
|
255
|
+
switched the contact form off, a decision nobody had made. `NOT_CONFIGURED` is
|
|
256
|
+
that missing state named honestly: nobody has chosen yet. It is never selectable
|
|
257
|
+
by an owner; only provisioning writes it.
|
|
258
|
+
|
|
259
|
+
**Consumers doing an exhaustive `switch` on `ContactFormMode` must handle the new
|
|
260
|
+
member.** Treat it as non-operational — like `DISABLED`, it accepts no
|
|
261
|
+
submissions. The two differ only in what they say about _why_, which is the point:
|
|
262
|
+
a SITE can reasonably prompt the owner to finish setup on `NOT_CONFIGURED` while
|
|
263
|
+
staying quiet on `DISABLED`.
|
|
264
|
+
|
|
265
|
+
Note this is a minor rather than a patch because of that enum widening. The
|
|
266
|
+
package is pre-1.0, so a `switch` without a default is the realistic break.
|
|
267
|
+
|
|
268
|
+
## 0.2.0
|
|
269
|
+
|
|
270
|
+
### Minor Changes
|
|
271
|
+
|
|
272
|
+
- d081f12: Expose the academy course catalogue through the SDK.
|
|
273
|
+
|
|
274
|
+
The headless academy endpoints shipped in REB-528/REB-534, but their contract
|
|
275
|
+
file was never re-exported from base-api's contracts barrel, so the types could
|
|
276
|
+
not reach `@reblu/site-contracts` and SITEs had no typed way to call them.
|
|
277
|
+
- `@reblu/site-contracts` publishes the catalogue DTOs: `AcademyCourseSummary`,
|
|
278
|
+
`AcademyCourseDetail`, `AcademyCategoryPublic`, `AcademyModulePublic`,
|
|
279
|
+
`AcademyLessonPublic`, `AcademyInstructorPublic`, and the `AcademyCourseSort`
|
|
280
|
+
query union.
|
|
281
|
+
- `@reblu/site-client` adds the `academy` namespace: `academy.courses(params?)`
|
|
282
|
+
(filtered + paginated), `academy.bySlug(slug)`, and `academy.categories()`.
|
|
283
|
+
`sort` is typed as a union because the server silently ignores an unknown key,
|
|
284
|
+
so a typo is now a compile error instead of a no-op falling back to default
|
|
285
|
+
order.
|
|
286
|
+
|
|
287
|
+
Additive only — no existing export changes shape.
|
|
288
|
+
|
|
289
|
+
## 0.1.2
|
|
290
|
+
|
|
291
|
+
### Patch Changes
|
|
292
|
+
|
|
293
|
+
- 356e04d: Republish with `dist/` included and shop surface exposed.
|
|
294
|
+
|
|
295
|
+
`0.1.0` was published broken: the bootstrap publish ran without a build, so the
|
|
296
|
+
tarball shipped only `LICENSE`, `README.md`, and `package.json` — `dist/` was
|
|
297
|
+
absent and `main`/`module`/`types` pointed at files that did not exist, making
|
|
298
|
+
both packages unusable when installed.
|
|
299
|
+
- Add a `prepack` lifecycle script to both packages so `pnpm pack` / `npm pack`
|
|
300
|
+
/ `npm publish` always run the build and emit `dist/` into the tarball. Build
|
|
301
|
+
before publish is now enforced by the package itself, not only by CI.
|
|
302
|
+
- `@reblu/site-client` ships the `shop` domain (`domains/shop.ts`) and
|
|
303
|
+
`@reblu/site-contracts` exports the matching shop types, so the SITE API shop
|
|
304
|
+
surface is available to external consumers.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reblu/site-contracts",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Public, dependency-free TypeScript types for the Reblu SITE API (types only — no Zod, no Prisma at runtime).",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -43,7 +43,8 @@
|
|
|
43
43
|
}
|
|
44
44
|
},
|
|
45
45
|
"files": [
|
|
46
|
-
"dist"
|
|
46
|
+
"dist",
|
|
47
|
+
"CHANGELOG.md"
|
|
47
48
|
],
|
|
48
49
|
"devDependencies": {
|
|
49
50
|
"@size-limit/preset-small-lib": "^11.1.6",
|