@reblu/site-contracts 1.13.0 → 1.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,61 @@
1
1
  # @reblu/site-contracts
2
2
 
3
+ ## 1.15.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 614e826: Shop checkout: full-invoice opt-in and the buyer's identifier (REB-73).
8
+
9
+ `ShopCheckoutInput` gains the optional `wantsFullInvoice` flag, and
10
+ `ShopBillingInput` the optional `billingIdType` (`NIF` for a Spanish
11
+ NIF/NIE/CIF; `02` EU VAT id, `03` passport, `04` ID document of the country
12
+ of residence, `05` residence certificate, `06` other document) and
13
+ `billingIdCountry` (ISO 3166-1 alpha-2, required for `02`–`06`). A full
14
+ invoice needs the identifier, the billing name and a billing address; without
15
+ the identifier the invoice is simplified.
16
+
17
+ `ShopErrorCode` gains two checkout codes, both answered before any payment:
18
+ `BILLING_DATA_REQUIRED` (HTTP 422, `details.field: 'billing'`) when the
19
+ opt-in, or a total over the tenant's simplified-invoice cap, comes without
20
+ those three; and `INVOICING_NOT_READY` (HTTP 503) when the shop cannot issue
21
+ invoices yet. Existing clients that send only `billing.billingNif` keep
22
+ working: its type is derived from the billing address country.
23
+
24
+ - 614e826: Shop checkout: no full-invoice opt-in on a shop that issues no invoices (REB-73).
25
+ - `ShopErrorCode` gains `FULL_INVOICE_UNAVAILABLE` (HTTP 422,
26
+ `details.field: 'wantsFullInvoice'`): `wantsFullInvoice: true` sent to a shop
27
+ whose invoicing is off (`ShopConfig.simplifiedInvoiceCapCents` is `null`) is
28
+ refused before any payment instead of succeeding without an invoice. Clients
29
+ that never send the opt-in are unaffected. The union is additive: a SITE
30
+ with an exhaustive `switch` over `ShopErrorCode` (or a
31
+ `Record<ShopErrorCode, …>`) must add the new member when it upgrades.
32
+ - `@reblu/site-blocks`: new `shopIssuesInvoices(config)`; `FullInvoiceFields`
33
+ takes `issuesInvoices` (default `true`) and renders nothing when it is
34
+ `false`, and `toCheckoutInvoiceFields(value, overCap, issuesInvoices)`
35
+ returns no opt-in and no billing block then. Default Spanish copy for the
36
+ new code in `getShopErrorMessage`.
37
+
38
+ - 614e826: Shop: the order's invoice and the simplified-invoice cap (REB-73).
39
+ - `ShopOrder` gains the optional `invoice: { number, url } | null`
40
+ (`ShopOrderInvoice`): once the sale invoice is issued, its number and the
41
+ shop's authenticated client-portal route that serves it — never a signed
42
+ URL, so the link does not expire; the buyer signs in to open it.
43
+ - `ShopConfig` gains `simplifiedInvoiceCapCents` (cents, never above the legal
44
+ 300000; `null` while the shop issues no invoices): above it, checkout needs
45
+ the buyer's identifier, name and billing address.
46
+ - `@reblu/site-blocks`: new drop-in `OrderInvoiceLink`, which
47
+ `OrderConfirmation` now renders under a confirmed order (labels
48
+ `invoiceLinkLabel`/`invoiceSignInHint`), and default Spanish copy for the
49
+ `BILLING_DATA_REQUIRED`/`INVOICING_NOT_READY` checkout codes in
50
+ `getShopErrorMessage`.
51
+
52
+ ## 1.14.0
53
+
54
+ ### Minor Changes
55
+
56
+ - 92e0f6f: Add the `header` slot to `Cta['slot']` (`'header' | 'hero' | 'blog-end' | 'services' | 'contact' | 'sticky-mobile' | 'footer'`), so a SITE can drive its persistent header CTA from nexus via `GET /api/site/ctas?slot=header`.
57
+ - b75f066: Deprecate `ShopCheckEmailInput` and `ShopEmailCheck` (REB-834). `POST /api/site/shop/check-email` was an account-enumeration oracle on the tenant's customer list; it now always answers `{ exists: false }`. Never branch UI on `exists`. Both types are removed in the next major.
58
+
3
59
  ## 1.13.0
4
60
 
5
61
  ### Minor Changes
package/README.md CHANGED
@@ -30,6 +30,13 @@ async function getPosts(): Promise<{ data: BlogPostListItem[] }> {
30
30
  Pair it with `@reblu/site-client` for a typed client that already knows these
31
31
  shapes.
32
32
 
33
+ ### Editable CTAs — slots
34
+
35
+ `Cta['slot']` is the union of placements a CTA can be configured for in nexus:
36
+ `'header' | 'hero' | 'blog-end' | 'services' | 'contact' | 'sticky-mobile' |
37
+ 'footer'`. `GET /api/site/ctas?slot=header` returns the active CTAs of the
38
+ persistent header; `@reblu/site-next`'s `EditableCta` renders them drop-in.
39
+
33
40
  ### Page blocks — canonical shape (REB-849 / REB-851 / REB-852 / REB-854)
34
41
 
35
42
  `GET /api/site/pages/[slug]` (`PageDetail`) returns BOTH `sections[]` (legacy,
package/dist/index.d.cts CHANGED
@@ -71,7 +71,7 @@ export type ContactFormSubmission = { id: string; createdAt: string;}
71
71
 
72
72
  export type CookieBanner = { enabled: boolean; title: string | null; body: string | null; acceptLabel: string | null; rejectLabel: string | null; manageLabel: string | null; policyUrl: string | null;}
73
73
 
74
- export type Cta = { id: string; slot: "hero" | "blog-end" | "services" | "contact" | "sticky-mobile" | "footer"; label: string; href: string; sortOrder: number; abVariant: string | null; analyticsId: string;}
74
+ export type Cta = { id: string; slot: "header" | "hero" | "blog-end" | "services" | "contact" | "sticky-mobile" | "footer"; label: string; href: string; sortOrder: number; abVariant: string | null; analyticsId: string;}
75
75
 
76
76
  export type CustomFieldDefinition = { id: string; entityType: "page" | "product"; key: string; label: string; valueType: "boolean" | "text" | "select" | "multiselect"; options: { value: string; label: string; }[]; isFilterable: boolean; showInDetail: boolean; unit: string | null; sortOrder: number;}
77
77
 
@@ -145,7 +145,7 @@ export type ShopAddressInput = { firstName: string; lastName: string; s
145
145
 
146
146
  export type ShopApplyDiscountInput = { code: string; orderSubtotal: number; customerEmail?: string | undefined;}
147
147
 
148
- export type ShopBillingInput = { billingName: string; billingNif?: string | undefined; billingEmail?: string | undefined; billingAddress?: { firstName: string; lastName: string; street: string; city: string; postalCode: string; region?: string | undefined; country?: string | undefined; phone?: string | null | undefined; } | undefined;}
148
+ export type ShopBillingInput = { billingName: string; billingNif?: string | undefined; billingIdType?: "NIF" | "02" | "03" | "04" | "05" | "06" | undefined; billingIdCountry?: string | undefined; billingEmail?: string | undefined; billingAddress?: { firstName: string; lastName: string; street: string; city: string; postalCode: string; region?: string | undefined; country?: string | undefined; phone?: string | null | undefined; } | undefined;}
149
149
 
150
150
  export type ShopBundleComponent = { quantity: number; component: { id: string; name: string; slug: string; price: number; };}
151
151
 
@@ -159,25 +159,37 @@ export type ShopCartValidationItem = { productId: string; name: string;
159
159
 
160
160
  export type ShopCategory = { id: string; name: string; slug: string; description: string | null; imageUrl: string | null; parentId: string | null; sortOrder: number;}
161
161
 
162
+ /**
163
+ * @deprecated REB-834 — `POST /api/site/shop/check-email` no longer reveals
164
+ * whether an account exists. Do not call it; removed in the next major.
165
+ */
162
166
  export type ShopCheckEmailInput = { email: string;}
163
167
 
164
- export type ShopCheckoutInput = { items: { productId: string; quantity: number; variantId?: string | null | undefined; }[]; customerEmail: string; customerName: string; successUrl: string; cancelUrl: string; customerPhone?: string | null | undefined; discountCode?: string | undefined; shippingAddress?: { firstName: string; lastName: string; street: string; city: string; postalCode: string; region?: string | undefined; country?: string | undefined; phone?: string | null | undefined; } | undefined; billing?: { billingName: string; billingNif?: string | undefined; billingEmail?: string | undefined; billingAddress?: { firstName: string; lastName: string; street: string; city: string; postalCode: string; region?: string | undefined; country?: string | undefined; phone?: string | null | undefined; } | undefined; } | undefined;}
168
+ export type ShopCheckoutInput = { items: { productId: string; quantity: number; variantId?: string | null | undefined; }[]; customerEmail: string; customerName: string; successUrl: string; cancelUrl: string; customerPhone?: string | null | undefined; discountCode?: string | undefined; shippingAddress?: { firstName: string; lastName: string; street: string; city: string; postalCode: string; region?: string | undefined; country?: string | undefined; phone?: string | null | undefined; } | undefined; billing?: { billingName: string; billingNif?: string | undefined; billingIdType?: "NIF" | "02" | "03" | "04" | "05" | "06" | undefined; billingIdCountry?: string | undefined; billingEmail?: string | undefined; billingAddress?: { firstName: string; lastName: string; street: string; city: string; postalCode: string; region?: string | undefined; country?: string | undefined; phone?: string | null | undefined; } | undefined; } | undefined; wantsFullInvoice?: boolean | undefined;}
165
169
 
166
170
  export type ShopCheckoutSession = { url: string; sessionId: string;}
167
171
 
168
- export type ShopConfig = { shopName: string | null; defaultLowStockThreshold: number; currencyCode: string; stripePublishableKey: string | null; stripeTestPublishableKey: string | null; stripeTestMode: boolean; shippingEnabled: boolean; pickupEnabled: boolean; taxEnabled: boolean; taxIncludedInPrice: boolean; productTrustItems?: { [x: string]: unknown; icon: (string & {}) | "shipping" | "returns" | "secure-payment" | "support" | "gift" | "pickup" | "warranty" | "quality"; text: string; }[] | null | undefined;}
172
+ export type ShopConfig = { shopName: string | null; defaultLowStockThreshold: number; currencyCode: string; stripePublishableKey: string | null; stripeTestPublishableKey: string | null; stripeTestMode: boolean; shippingEnabled: boolean; pickupEnabled: boolean; taxEnabled: boolean; taxIncludedInPrice: boolean; simplifiedInvoiceCapCents?: number | null | undefined; productTrustItems?: { [x: string]: unknown; icon: (string & {}) | "shipping" | "returns" | "secure-payment" | "support" | "gift" | "pickup" | "warranty" | "quality"; text: string; }[] | null | undefined;}
169
173
 
170
174
  export type ShopDiscountApplication = { valid: boolean; discount: number; type: "PERCENTAGE" | "FIXED" | null; code: string | null; error?: string | undefined;}
171
175
 
176
+ /**
177
+ * @deprecated REB-834 — `exists` is always `false`. Never branch UI on it:
178
+ * the checkout always offers «Iniciar sesión o continuar como invitado» and
179
+ * the server links the order to an existing account after payment. Removed
180
+ * in the next major.
181
+ */
172
182
  export type ShopEmailCheck = { exists: boolean;}
173
183
 
174
- export type ShopErrorCode = "STOCK_CHANGED" | "PRICE_CHANGED" | "PRODUCT_UNAVAILABLE" | "DISCOUNT_INVALID" | "SHIPPING_UNAVAILABLE" | "INVALID_INPUT" | "STORE_DISABLED" | "PAYMENT_PROVIDER_ERROR" | "INTERNAL_ERROR"
184
+ export type ShopErrorCode = "STOCK_CHANGED" | "PRICE_CHANGED" | "PRODUCT_UNAVAILABLE" | "DISCOUNT_INVALID" | "SHIPPING_UNAVAILABLE" | "INVALID_INPUT" | "STORE_DISABLED" | "PAYMENT_PROVIDER_ERROR" | "INTERNAL_ERROR" | "BILLING_DATA_REQUIRED" | "INVOICING_NOT_READY" | "FULL_INVOICE_UNAVAILABLE"
175
185
 
176
186
  export type ShopFacets = { taxonomies: { key: string; label: string; showAsBadge: boolean; selectionMode: "single" | "multiple"; terms: { slug: string; name: string; count: number; }[]; }[]; attributes: { key: string; label: string; valueType: "boolean" | "text" | "select" | "multiselect"; options: { value: string; label: string; count: number; }[]; }[]; price: { min: number; max: number; }; onSale: { count: number; };}
177
187
 
178
188
  export type ShopInventoryRef = { stockQuantity: number; reservedQuantity: number; lowStockThreshold: number | null;}
179
189
 
180
- export type ShopOrder = { id: string; orderNumber: string; status: "PENDING" | "CONFIRMED" | "PROCESSING" | "SHIPPED" | "DELIVERED" | "CANCELLED" | "REFUNDED"; paymentStatus: "PENDING" | "REFUNDED" | "PAID" | "FAILED" | "PARTIALLY_REFUNDED"; customerEmail: string; total: number; items: { product: { id: string; name: string; slug: string; }; quantity: number; unitPrice: number; total: number; variant?: { id: string; name: string; } | null | undefined; }[];}
190
+ export type ShopOrder = { id: string; orderNumber: string; status: "PENDING" | "CONFIRMED" | "PROCESSING" | "SHIPPED" | "DELIVERED" | "CANCELLED" | "REFUNDED"; paymentStatus: "PENDING" | "REFUNDED" | "PAID" | "FAILED" | "PARTIALLY_REFUNDED"; customerEmail: string; total: number; items: { product: { id: string; name: string; slug: string; }; quantity: number; unitPrice: number; total: number; variant?: { id: string; name: string; } | null | undefined; }[]; invoice?: { number: string; url: string; } | null | undefined;}
191
+
192
+ export type ShopOrderInvoice = { number: string; url: string;}
181
193
 
182
194
  export type ShopOrderItem = { product: { id: string; name: string; slug: string; }; quantity: number; unitPrice: number; total: number; variant?: { id: string; name: string; } | null | undefined;}
183
195
 
package/dist/index.d.ts CHANGED
@@ -71,7 +71,7 @@ export type ContactFormSubmission = { id: string; createdAt: string;}
71
71
 
72
72
  export type CookieBanner = { enabled: boolean; title: string | null; body: string | null; acceptLabel: string | null; rejectLabel: string | null; manageLabel: string | null; policyUrl: string | null;}
73
73
 
74
- export type Cta = { id: string; slot: "hero" | "blog-end" | "services" | "contact" | "sticky-mobile" | "footer"; label: string; href: string; sortOrder: number; abVariant: string | null; analyticsId: string;}
74
+ export type Cta = { id: string; slot: "header" | "hero" | "blog-end" | "services" | "contact" | "sticky-mobile" | "footer"; label: string; href: string; sortOrder: number; abVariant: string | null; analyticsId: string;}
75
75
 
76
76
  export type CustomFieldDefinition = { id: string; entityType: "page" | "product"; key: string; label: string; valueType: "boolean" | "text" | "select" | "multiselect"; options: { value: string; label: string; }[]; isFilterable: boolean; showInDetail: boolean; unit: string | null; sortOrder: number;}
77
77
 
@@ -145,7 +145,7 @@ export type ShopAddressInput = { firstName: string; lastName: string; s
145
145
 
146
146
  export type ShopApplyDiscountInput = { code: string; orderSubtotal: number; customerEmail?: string | undefined;}
147
147
 
148
- export type ShopBillingInput = { billingName: string; billingNif?: string | undefined; billingEmail?: string | undefined; billingAddress?: { firstName: string; lastName: string; street: string; city: string; postalCode: string; region?: string | undefined; country?: string | undefined; phone?: string | null | undefined; } | undefined;}
148
+ export type ShopBillingInput = { billingName: string; billingNif?: string | undefined; billingIdType?: "NIF" | "02" | "03" | "04" | "05" | "06" | undefined; billingIdCountry?: string | undefined; billingEmail?: string | undefined; billingAddress?: { firstName: string; lastName: string; street: string; city: string; postalCode: string; region?: string | undefined; country?: string | undefined; phone?: string | null | undefined; } | undefined;}
149
149
 
150
150
  export type ShopBundleComponent = { quantity: number; component: { id: string; name: string; slug: string; price: number; };}
151
151
 
@@ -159,25 +159,37 @@ export type ShopCartValidationItem = { productId: string; name: string;
159
159
 
160
160
  export type ShopCategory = { id: string; name: string; slug: string; description: string | null; imageUrl: string | null; parentId: string | null; sortOrder: number;}
161
161
 
162
+ /**
163
+ * @deprecated REB-834 — `POST /api/site/shop/check-email` no longer reveals
164
+ * whether an account exists. Do not call it; removed in the next major.
165
+ */
162
166
  export type ShopCheckEmailInput = { email: string;}
163
167
 
164
- export type ShopCheckoutInput = { items: { productId: string; quantity: number; variantId?: string | null | undefined; }[]; customerEmail: string; customerName: string; successUrl: string; cancelUrl: string; customerPhone?: string | null | undefined; discountCode?: string | undefined; shippingAddress?: { firstName: string; lastName: string; street: string; city: string; postalCode: string; region?: string | undefined; country?: string | undefined; phone?: string | null | undefined; } | undefined; billing?: { billingName: string; billingNif?: string | undefined; billingEmail?: string | undefined; billingAddress?: { firstName: string; lastName: string; street: string; city: string; postalCode: string; region?: string | undefined; country?: string | undefined; phone?: string | null | undefined; } | undefined; } | undefined;}
168
+ export type ShopCheckoutInput = { items: { productId: string; quantity: number; variantId?: string | null | undefined; }[]; customerEmail: string; customerName: string; successUrl: string; cancelUrl: string; customerPhone?: string | null | undefined; discountCode?: string | undefined; shippingAddress?: { firstName: string; lastName: string; street: string; city: string; postalCode: string; region?: string | undefined; country?: string | undefined; phone?: string | null | undefined; } | undefined; billing?: { billingName: string; billingNif?: string | undefined; billingIdType?: "NIF" | "02" | "03" | "04" | "05" | "06" | undefined; billingIdCountry?: string | undefined; billingEmail?: string | undefined; billingAddress?: { firstName: string; lastName: string; street: string; city: string; postalCode: string; region?: string | undefined; country?: string | undefined; phone?: string | null | undefined; } | undefined; } | undefined; wantsFullInvoice?: boolean | undefined;}
165
169
 
166
170
  export type ShopCheckoutSession = { url: string; sessionId: string;}
167
171
 
168
- export type ShopConfig = { shopName: string | null; defaultLowStockThreshold: number; currencyCode: string; stripePublishableKey: string | null; stripeTestPublishableKey: string | null; stripeTestMode: boolean; shippingEnabled: boolean; pickupEnabled: boolean; taxEnabled: boolean; taxIncludedInPrice: boolean; productTrustItems?: { [x: string]: unknown; icon: (string & {}) | "shipping" | "returns" | "secure-payment" | "support" | "gift" | "pickup" | "warranty" | "quality"; text: string; }[] | null | undefined;}
172
+ export type ShopConfig = { shopName: string | null; defaultLowStockThreshold: number; currencyCode: string; stripePublishableKey: string | null; stripeTestPublishableKey: string | null; stripeTestMode: boolean; shippingEnabled: boolean; pickupEnabled: boolean; taxEnabled: boolean; taxIncludedInPrice: boolean; simplifiedInvoiceCapCents?: number | null | undefined; productTrustItems?: { [x: string]: unknown; icon: (string & {}) | "shipping" | "returns" | "secure-payment" | "support" | "gift" | "pickup" | "warranty" | "quality"; text: string; }[] | null | undefined;}
169
173
 
170
174
  export type ShopDiscountApplication = { valid: boolean; discount: number; type: "PERCENTAGE" | "FIXED" | null; code: string | null; error?: string | undefined;}
171
175
 
176
+ /**
177
+ * @deprecated REB-834 — `exists` is always `false`. Never branch UI on it:
178
+ * the checkout always offers «Iniciar sesión o continuar como invitado» and
179
+ * the server links the order to an existing account after payment. Removed
180
+ * in the next major.
181
+ */
172
182
  export type ShopEmailCheck = { exists: boolean;}
173
183
 
174
- export type ShopErrorCode = "STOCK_CHANGED" | "PRICE_CHANGED" | "PRODUCT_UNAVAILABLE" | "DISCOUNT_INVALID" | "SHIPPING_UNAVAILABLE" | "INVALID_INPUT" | "STORE_DISABLED" | "PAYMENT_PROVIDER_ERROR" | "INTERNAL_ERROR"
184
+ export type ShopErrorCode = "STOCK_CHANGED" | "PRICE_CHANGED" | "PRODUCT_UNAVAILABLE" | "DISCOUNT_INVALID" | "SHIPPING_UNAVAILABLE" | "INVALID_INPUT" | "STORE_DISABLED" | "PAYMENT_PROVIDER_ERROR" | "INTERNAL_ERROR" | "BILLING_DATA_REQUIRED" | "INVOICING_NOT_READY" | "FULL_INVOICE_UNAVAILABLE"
175
185
 
176
186
  export type ShopFacets = { taxonomies: { key: string; label: string; showAsBadge: boolean; selectionMode: "single" | "multiple"; terms: { slug: string; name: string; count: number; }[]; }[]; attributes: { key: string; label: string; valueType: "boolean" | "text" | "select" | "multiselect"; options: { value: string; label: string; count: number; }[]; }[]; price: { min: number; max: number; }; onSale: { count: number; };}
177
187
 
178
188
  export type ShopInventoryRef = { stockQuantity: number; reservedQuantity: number; lowStockThreshold: number | null;}
179
189
 
180
- export type ShopOrder = { id: string; orderNumber: string; status: "PENDING" | "CONFIRMED" | "PROCESSING" | "SHIPPED" | "DELIVERED" | "CANCELLED" | "REFUNDED"; paymentStatus: "PENDING" | "REFUNDED" | "PAID" | "FAILED" | "PARTIALLY_REFUNDED"; customerEmail: string; total: number; items: { product: { id: string; name: string; slug: string; }; quantity: number; unitPrice: number; total: number; variant?: { id: string; name: string; } | null | undefined; }[];}
190
+ export type ShopOrder = { id: string; orderNumber: string; status: "PENDING" | "CONFIRMED" | "PROCESSING" | "SHIPPED" | "DELIVERED" | "CANCELLED" | "REFUNDED"; paymentStatus: "PENDING" | "REFUNDED" | "PAID" | "FAILED" | "PARTIALLY_REFUNDED"; customerEmail: string; total: number; items: { product: { id: string; name: string; slug: string; }; quantity: number; unitPrice: number; total: number; variant?: { id: string; name: string; } | null | undefined; }[]; invoice?: { number: string; url: string; } | null | undefined;}
191
+
192
+ export type ShopOrderInvoice = { number: string; url: string;}
181
193
 
182
194
  export type ShopOrderItem = { product: { id: string; name: string; slug: string; }; quantity: number; unitPrice: number; total: number; variant?: { id: string; name: string; } | null | undefined;}
183
195
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reblu/site-contracts",
3
- "version": "1.13.0",
3
+ "version": "1.15.0",
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",