includio-cms 0.35.0 → 0.36.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/API.md +4 -2
- package/CHANGELOG.md +17 -0
- package/DOCS.md +1 -1
- package/dist/shop/adapters/fakturownia/payload.d.ts +2 -0
- package/dist/shop/adapters/fakturownia/payload.js +5 -0
- package/dist/shop/cart/types.d.ts +7 -0
- package/dist/shop/index.d.ts +2 -1
- package/dist/shop/index.js +4 -2
- package/dist/shop/server/cart-hydrate.js +30 -3
- package/dist/shop/server/email.js +12 -8
- package/dist/shop/server/invoices.js +9 -3
- package/dist/shop/server/orders.js +2 -1
- package/dist/shop/template.d.ts +20 -0
- package/dist/shop/template.js +82 -5
- package/dist/shop/types.d.ts +25 -1
- package/dist/updates/0.36.0/index.d.ts +2 -0
- package/dist/updates/0.36.0/index.js +13 -0
- package/dist/updates/0.36.1/index.d.ts +2 -0
- package/dist/updates/0.36.1/index.js +10 -0
- package/dist/updates/index.js +5 -1
- package/package.json +1 -1
package/API.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
# includio-cms — Public API v0.
|
|
1
|
+
# includio-cms — Public API v0.36.1
|
|
2
2
|
|
|
3
3
|
> Auto-generated by `scripts/generate-api-md.ts`. Do not edit by hand.
|
|
4
4
|
|
|
5
|
-
Entry points: **19** · Stable: **
|
|
5
|
+
Entry points: **19** · Stable: **492** · Experimental: **4**
|
|
6
6
|
|
|
7
7
|
Tags:
|
|
8
8
|
- `@public` — frozen for v1.0; semver-protected.
|
|
@@ -450,6 +450,7 @@ Tags:
|
|
|
450
450
|
- `interface FakturowniaAdapterOptions`
|
|
451
451
|
- `filterUpcoming(variants: T[], config: VariantExpiryConfig | null, now?: Date): T[]` — Return the subset of `variants` that have not yet expired. Order is
|
|
452
452
|
- `type GeowidgetConfigPreset = 'parcelcollect' | 'parcelsend' | 'parcelcollect247' | string`
|
|
453
|
+
- `type I18nTemplate = Record<string, string>`
|
|
453
454
|
- `type I18nText = { [lang: string]: string; }`
|
|
454
455
|
- `inpostAdapter(opts: InpostAdapterOptions): CarrierAdapter`
|
|
455
456
|
- `interface InpostAdapterOptions`
|
|
@@ -480,6 +481,7 @@ Tags:
|
|
|
480
481
|
- `interface PaymentRefundResult`
|
|
481
482
|
- `payuAdapter(opts: PayuAdapterOptions): PaymentAdapter`
|
|
482
483
|
- `interface PayuAdapterOptions`
|
|
484
|
+
- `renderShopName(template: I18nTemplate | null | undefined, ctx: { entryData: Record<string, unknown> | null | undefi..., locale: string): string | null` — Render a multilingual shop-name template (`invoiceName` / `cartName`)
|
|
483
485
|
- `interface ResolvedShopConfig`
|
|
484
486
|
- `interface ShipmentCreateInput`
|
|
485
487
|
- `interface ShipmentCreateResult`
|
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,23 @@
|
|
|
3
3
|
All notable changes to includio-cms are documented here.
|
|
4
4
|
Generated from `src/lib/updates/` — do not edit manually.
|
|
5
5
|
|
|
6
|
+
## 0.36.1 — 2026-06-08
|
|
7
|
+
|
|
8
|
+
Fakturownia adapter — fix B2C: gdy brak `buyer.nip` i `buyer.companyName`, payload zawiera `buyer_first_name` + `buyer_last_name` (rozdzielone przez `splitFullName` — spójnie z PayU/InPost). Bez tej zmiany Fakturownia API odrzuca fakturę dla osoby fizycznej z 422 `buyer_tax_no — nie może być puste`.
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- `buildFakturowniaInvoice` — dla B2C (brak NIP i companyName) payload zawiera teraz `buyer_first_name` i `buyer_last_name` (rozdzielone z `buyer.name` przez `splitFullName` z adaptera PayU). B2B (z NIP lub companyName) zachowuje obecne zachowanie — bez first/last name. Dla `splitFullName("Madonna")` (1 słowo) → tylko `buyer_first_name`. Naprawia 422 z Fakturownia dla osób fizycznych.
|
|
12
|
+
|
|
13
|
+
## 0.36.0 — 2026-06-05
|
|
14
|
+
|
|
15
|
+
`defineShop({ invoiceName, cartName })` — opcjonalne multilingual template overriding produktowej nazwy w koszyku, podsumowaniu zamówienia i na fakturze. Reusuje engine od `variantLabel.template` (`{slug}`, `{slug|filter}`), z nowymi możliwościami: dot-path (`{hero.title}`) i auto-unwrap pól i18n. Reserved `{variant}` = wyrenderowany variantLabel. Order item `nameSnapshot` zyskuje opcjonalne pole `invoice` (zamrażane przy checkout). Additive — brak zmian default behavior.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- `ShopConfig.invoiceName?: I18nText` i `ShopConfig.cartName?: I18nText` — template (per język) renderowany przy hydracji koszyka i tworzeniu zamówienia. Fallback chain: `cartName` (koszyk) → `invoiceName` (koszyk + faktura) → legacy `productTitle` / `product — variant`. `invoiceName` jest **full override** na fakturze — wariant NIE jest doklejany automatycznie, użyj `{variant}` w template.
|
|
19
|
+
- `interpolateTemplate()` (`$lib/shop/template.ts`) zyskuje dot-path resolver (`{hero.title}`) oraz auto-unwrap pól i18n `{ pl: …, en: … }` do aktualnego języka (z fallbackiem `lang.split("-")[0]` → pierwszy klucz). Backwards-compatible: istniejące `{key}` (płaskie) działają bez zmian.
|
|
20
|
+
- `renderShopName(template, ctx, locale)` (`$lib/shop/template.ts`) — public helper łączący resolverę I18nText template + budowę vars `{ ...entryData, variant }`. Używany wewnętrznie przez cart-hydrate; eksportowany dla customowych use case (np. own checkout flows).
|
|
21
|
+
- `CartLine` zyskuje pole `invoiceTitle: string | null` — frozen invoice copy obok `productTitle` (cart copy). `orders.createOrderFromCart` snapshot-uje to do `nameSnapshot.invoice`. `buildInvoicePayload()` honoruje `nameSnapshot.invoice` — fallback do `product — variant` gdy brak.
|
|
22
|
+
|
|
6
23
|
## 0.35.0 — 2026-06-05
|
|
7
24
|
|
|
8
25
|
Kupon `appliesTo` (netto/brutto), uczestnicy w `notes` (admin + email), Calendar+Popover w polach `date`/`datetime` (admin), `api.ts` bez `FormEntryMap` gdy projekt nie ma form. Additive only — domyślne zachowania bez zmian.
|
package/DOCS.md
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { splitFullName } from '../payu/payload.js';
|
|
1
2
|
/** Minor units (grosze) → major units (PLN) with 2-decimal precision. */
|
|
2
3
|
function toMajor(minor) {
|
|
3
4
|
return Math.round(minor) / 100;
|
|
@@ -18,6 +19,8 @@ function pick(addr, ...keys) {
|
|
|
18
19
|
export function buildFakturowniaInvoice(payload, opts = {}) {
|
|
19
20
|
const date = payload.paidAt.slice(0, 10);
|
|
20
21
|
const { buyer } = payload;
|
|
22
|
+
const isB2C = !buyer.nip && !buyer.companyName;
|
|
23
|
+
const names = isB2C ? splitFullName(buyer.name) : {};
|
|
21
24
|
return {
|
|
22
25
|
kind: opts.kind ?? 'vat',
|
|
23
26
|
status: 'paid',
|
|
@@ -28,6 +31,8 @@ export function buildFakturowniaInvoice(payload, opts = {}) {
|
|
|
28
31
|
buyer_name: buyer.companyName || buyer.name,
|
|
29
32
|
buyer_email: buyer.email,
|
|
30
33
|
...(buyer.nip ? { buyer_tax_no: buyer.nip } : {}),
|
|
34
|
+
...(names.firstName ? { buyer_first_name: names.firstName } : {}),
|
|
35
|
+
...(names.lastName ? { buyer_last_name: names.lastName } : {}),
|
|
31
36
|
...(pick(buyer.address, 'street') ? { buyer_street: pick(buyer.address, 'street') } : {}),
|
|
32
37
|
...(pick(buyer.address, 'city') ? { buyer_city: pick(buyer.address, 'city') } : {}),
|
|
33
38
|
...(pick(buyer.address, 'postCode', 'zip', 'postalCode')
|
|
@@ -9,6 +9,13 @@ export interface CartLine extends CartItemRef {
|
|
|
9
9
|
variantName: Record<string, string> | null;
|
|
10
10
|
variantSku: string | null;
|
|
11
11
|
productTitle: string | null;
|
|
12
|
+
/**
|
|
13
|
+
* Frozen invoice-line name rendered from `ShopConfig.invoiceName` at hydrate
|
|
14
|
+
* time. Null when `invoiceName` is not configured. Carried separately from
|
|
15
|
+
* `productTitle` so order creation can snapshot the cart copy and the
|
|
16
|
+
* invoice copy independently.
|
|
17
|
+
*/
|
|
18
|
+
invoiceTitle: string | null;
|
|
12
19
|
productSlug: string | null;
|
|
13
20
|
priceNet: number;
|
|
14
21
|
priceGross: number;
|
package/dist/shop/index.d.ts
CHANGED
|
@@ -13,4 +13,5 @@ export { fakturowniaAdapter } from './adapters/fakturownia/index.js';
|
|
|
13
13
|
export type { FakturowniaAdapterOptions } from './adapters/fakturownia/index.js';
|
|
14
14
|
export { isValidNip } from './nip.js';
|
|
15
15
|
export type { ShopConfig, ResolvedShopConfig, Currency, Order, OrderStatus, PaymentAdapter, PaymentCreateContext, PaymentRefundInput, PaymentRefundResult, CarrierAdapter, CarrierEvent, ShipmentCreateInput, ShipmentCreateResult, ShipmentLabel, ConsentConfig, ShopFeatures, PaymentCreateResult, PaymentEvent, OrderRef, CouponRef, I18nText, VariantAttribute, VariantAttributeText, VariantAttributeNumber, VariantAttributeDatetime, VariantAttributeSelect, VariantAttributeBoolean, VariantAttributeImage, VariantAttributeEntry, VariantAttributeSlug, VariantLabelConfig, VariantExpiryConfig, PaymentPolicy, DepositAmount, PartialPayment, InvoicingAdapter, InvoiceIssuePolicy, InvoiceBuyer, InvoiceLineItem, InvoicePayload, InvoiceCreateResult, InvoiceContext } from './types.js';
|
|
16
|
-
export { interpolateTemplate } from './template.js';
|
|
16
|
+
export { interpolateTemplate, renderShopName } from './template.js';
|
|
17
|
+
export type { I18nTemplate } from './template.js';
|
package/dist/shop/index.js
CHANGED
|
@@ -17,7 +17,9 @@ export function defineShop(config) {
|
|
|
17
17
|
orderViewUrl: config.orderViewUrl ?? '/shop/order/{orderNumber}?token={accessToken}',
|
|
18
18
|
variantAttributes: config.variantAttributes ?? {},
|
|
19
19
|
variantLabel: config.variantLabel ?? null,
|
|
20
|
-
variantExpiry: config.variantExpiry ?? null
|
|
20
|
+
variantExpiry: config.variantExpiry ?? null,
|
|
21
|
+
invoiceName: config.invoiceName ?? null,
|
|
22
|
+
cartName: config.cartName ?? null
|
|
21
23
|
};
|
|
22
24
|
}
|
|
23
25
|
export { InvalidVariantAttributesError } from './variant-attributes.js';
|
|
@@ -28,4 +30,4 @@ export { stripeAdapter } from './adapters/stripe/index.js';
|
|
|
28
30
|
export { inpostAdapter } from './adapters/inpost/index.js';
|
|
29
31
|
export { fakturowniaAdapter } from './adapters/fakturownia/index.js';
|
|
30
32
|
export { isValidNip } from './nip.js';
|
|
31
|
-
export { interpolateTemplate } from './template.js';
|
|
33
|
+
export { interpolateTemplate, renderShopName } from './template.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { desc, inArray } from 'drizzle-orm';
|
|
2
2
|
import { shopProductsTable, shopProductVariantsTable } from '../../db-postgres/schema/shop/index.js';
|
|
3
3
|
import { entriesTable } from '../../db-postgres/schema/entry.js';
|
|
4
4
|
import { entryVersionsTable } from '../../db-postgres/schema/entryVersion.js';
|
|
@@ -6,6 +6,7 @@ import { getCMS } from '../../core/cms.js';
|
|
|
6
6
|
import { getShopDb, requireShopConfig } from './db.js';
|
|
7
7
|
import { grossFromNet, grossPlnFromNetPln, toCents } from '../pricing.js';
|
|
8
8
|
import { buildAppliedCoupon, validateCoupon, CouponError } from './coupons.js';
|
|
9
|
+
import { renderShopName } from '../template.js';
|
|
9
10
|
export async function hydrateCart(items, opts = {}) {
|
|
10
11
|
const shop = requireShopConfig();
|
|
11
12
|
const cms = getCMS();
|
|
@@ -42,10 +43,16 @@ export async function hydrateCart(items, opts = {}) {
|
|
|
42
43
|
.select()
|
|
43
44
|
.from(entryVersionsTable)
|
|
44
45
|
.where(inArray(entryVersionsTable.entryId, entryIds))
|
|
46
|
+
.orderBy(desc(entryVersionsTable.versionNumber))
|
|
45
47
|
: [];
|
|
46
48
|
const publishedByEntry = new Map();
|
|
49
|
+
const now = new Date();
|
|
47
50
|
for (const v of versions) {
|
|
48
|
-
if (v.
|
|
51
|
+
if (v.lang !== language)
|
|
52
|
+
continue;
|
|
53
|
+
if (v.publishedAt == null || v.publishedAt > now)
|
|
54
|
+
continue;
|
|
55
|
+
if (!publishedByEntry.has(v.entryId))
|
|
49
56
|
publishedByEntry.set(v.entryId, v);
|
|
50
57
|
}
|
|
51
58
|
const productById = new Map(products.map((p) => [p.id, p]));
|
|
@@ -62,6 +69,19 @@ export async function hydrateCart(items, opts = {}) {
|
|
|
62
69
|
}
|
|
63
70
|
return null;
|
|
64
71
|
}
|
|
72
|
+
function readVariantLabel(name, lang) {
|
|
73
|
+
if (!name || typeof name !== 'object')
|
|
74
|
+
return '';
|
|
75
|
+
const map = name;
|
|
76
|
+
const direct = map[lang];
|
|
77
|
+
if (typeof direct === 'string' && direct.length > 0)
|
|
78
|
+
return direct;
|
|
79
|
+
const base = map[lang.split('-')[0]];
|
|
80
|
+
if (typeof base === 'string' && base.length > 0)
|
|
81
|
+
return base;
|
|
82
|
+
const first = Object.values(map).find((v) => typeof v === 'string' && v.length > 0);
|
|
83
|
+
return first ?? '';
|
|
84
|
+
}
|
|
65
85
|
function readSlug(data) {
|
|
66
86
|
if (!data || typeof data !== 'object')
|
|
67
87
|
return null;
|
|
@@ -95,6 +115,11 @@ export async function hydrateCart(items, opts = {}) {
|
|
|
95
115
|
const version = publishedByEntry.get(product.entryId);
|
|
96
116
|
const title = readTitle(version?.data);
|
|
97
117
|
const slug = readSlug(version?.data);
|
|
118
|
+
const entryData = (version?.data ?? null);
|
|
119
|
+
const variantLabel = readVariantLabel(variant.name, language);
|
|
120
|
+
const renderedInvoice = renderShopName(shop.invoiceName, { entryData, variant: variantLabel, language }, language);
|
|
121
|
+
const renderedCart = renderShopName(shop.cartName, { entryData, variant: variantLabel, language }, language) ?? renderedInvoice;
|
|
122
|
+
const cartTitle = renderedCart ?? title;
|
|
98
123
|
// basePrice/priceDelta są numeric(20,6) → drizzle zwraca string. Konwertujemy do PLN (number).
|
|
99
124
|
const priceNetPln = Number(product.basePrice) + Number(variant.priceDelta ?? 0);
|
|
100
125
|
const priceGrossPln = grossPlnFromNetPln(priceNetPln, product.vatRate);
|
|
@@ -130,7 +155,8 @@ export async function hydrateCart(items, opts = {}) {
|
|
|
130
155
|
productId: product.id,
|
|
131
156
|
variantName: variant.name ?? null,
|
|
132
157
|
variantSku: variant.sku,
|
|
133
|
-
productTitle:
|
|
158
|
+
productTitle: cartTitle,
|
|
159
|
+
invoiceTitle: renderedInvoice,
|
|
134
160
|
productSlug: slug,
|
|
135
161
|
priceNet,
|
|
136
162
|
priceGross,
|
|
@@ -210,6 +236,7 @@ export async function hydrateCart(items, opts = {}) {
|
|
|
210
236
|
variantName: null,
|
|
211
237
|
variantSku: null,
|
|
212
238
|
productTitle: null,
|
|
239
|
+
invoiceTitle: null,
|
|
213
240
|
productSlug: null,
|
|
214
241
|
priceNet: 0,
|
|
215
242
|
priceGross: 0,
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { getCMS } from '../../core/cms.js';
|
|
2
|
-
import { resolveI18n } from '../pricing.js';
|
|
3
2
|
import { getOrderById, getOrderItems } from './orders.js';
|
|
4
3
|
import { getOrderCoupon } from './coupons.js';
|
|
5
4
|
import { requireShopConfig } from './db.js';
|
|
@@ -151,13 +150,18 @@ export async function sendOrderStatusEmail(orderId, status) {
|
|
|
151
150
|
lastName: p.lastName
|
|
152
151
|
})),
|
|
153
152
|
hasParticipants: participants.length > 0,
|
|
154
|
-
items: items.map((i) =>
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
153
|
+
items: items.map((i) => {
|
|
154
|
+
const snap = (i.nameSnapshot ?? {});
|
|
155
|
+
const composed = snap.invoice ||
|
|
156
|
+
(snap.product && snap.variant
|
|
157
|
+
? `${snap.product} — ${snap.variant}`
|
|
158
|
+
: snap.product || snap.variant || '');
|
|
159
|
+
return {
|
|
160
|
+
name: composed || '—',
|
|
161
|
+
qty: i.qty,
|
|
162
|
+
lineGross: formatPrice(i.priceGrossSnapshot * i.qty, order.currency)
|
|
163
|
+
};
|
|
164
|
+
}),
|
|
161
165
|
shop: {
|
|
162
166
|
currency: order.currency,
|
|
163
167
|
adminEmail: shop.adminEmail ?? null
|
|
@@ -43,10 +43,16 @@ export function decideInvoiceAction(order, existing, policy, opts = {}) {
|
|
|
43
43
|
return 'create';
|
|
44
44
|
return 'skip';
|
|
45
45
|
}
|
|
46
|
-
// Order-item `nameSnapshot` is `{ product, variant }` (see
|
|
47
|
-
//
|
|
48
|
-
//
|
|
46
|
+
// Order-item `nameSnapshot` is `{ product, variant, invoice? }` (see
|
|
47
|
+
// createOrderFromCart). When `invoice` is present it was rendered from
|
|
48
|
+
// `ShopConfig.invoiceName` at order time and is treated as a full override —
|
|
49
|
+
// the variant suffix is NOT appended (template author controls whether to
|
|
50
|
+
// include `{variant}`). Otherwise the legacy `product — variant` format is
|
|
51
|
+
// used (e.g. "Odporność psychiczna — Poznań • 20 września 2026").
|
|
49
52
|
function lineName(name) {
|
|
53
|
+
const invoice = name.invoice ?? '';
|
|
54
|
+
if (invoice)
|
|
55
|
+
return invoice;
|
|
50
56
|
const product = name.product ?? '';
|
|
51
57
|
const variant = name.variant ?? '';
|
|
52
58
|
if (product && variant)
|
|
@@ -327,7 +327,8 @@ export async function createOrderFromCart(input) {
|
|
|
327
327
|
product: line.productTitle ?? '',
|
|
328
328
|
variant: line.variantName && typeof line.variantName === 'object'
|
|
329
329
|
? (Object.values(line.variantName)[0] ?? '')
|
|
330
|
-
: ''
|
|
330
|
+
: '',
|
|
331
|
+
...(line.invoiceTitle ? { invoice: line.invoiceTitle } : {})
|
|
331
332
|
},
|
|
332
333
|
skuSnapshot: line.variantSku ?? null,
|
|
333
334
|
priceNetSnapshot: line.priceNet,
|
package/dist/shop/template.d.ts
CHANGED
|
@@ -11,3 +11,23 @@
|
|
|
11
11
|
* @public
|
|
12
12
|
*/
|
|
13
13
|
export declare function interpolateTemplate(template: string, vars: Record<string, unknown>, locale: string): string;
|
|
14
|
+
/**
|
|
15
|
+
* Render a multilingual shop-name template (`invoiceName` / `cartName`)
|
|
16
|
+
* against an entry's data and a precomputed variant label. Returns `null`
|
|
17
|
+
* when the template is absent or yields an empty string after trimming —
|
|
18
|
+
* callers fall back to legacy behavior.
|
|
19
|
+
*
|
|
20
|
+
* Variables: `{<slug>}` resolves to any top-level (dot-path) field on the
|
|
21
|
+
* entry's published data. The reserved `{variant}` returns the rendered
|
|
22
|
+
* variant label. I18n fields stored as `{ pl: '…', en: '…' }` are auto
|
|
23
|
+
* unwrapped to the active language.
|
|
24
|
+
*
|
|
25
|
+
* @public
|
|
26
|
+
*/
|
|
27
|
+
export declare function renderShopName(template: I18nTemplate | null | undefined, ctx: {
|
|
28
|
+
entryData: Record<string, unknown> | null | undefined;
|
|
29
|
+
variant: string;
|
|
30
|
+
language: string;
|
|
31
|
+
}, locale: string): string | null;
|
|
32
|
+
/** @public */
|
|
33
|
+
export type I18nTemplate = Record<string, string>;
|
package/dist/shop/template.js
CHANGED
|
@@ -17,18 +17,95 @@ export function interpolateTemplate(template, vars, locale) {
|
|
|
17
17
|
}
|
|
18
18
|
return template.replace(/\{([^}]+)\}/g, (_match, body) => {
|
|
19
19
|
const { key, filter, arg } = parsePlaceholder(body);
|
|
20
|
-
|
|
20
|
+
const lookup = resolveKey(vars, key, locale);
|
|
21
|
+
if (lookup === undefined) {
|
|
21
22
|
console.warn(`[interpolateTemplate] Unknown key "${key}" in template: ${template}`);
|
|
22
23
|
return '';
|
|
23
24
|
}
|
|
24
|
-
|
|
25
|
-
if (raw === null || raw === undefined)
|
|
25
|
+
if (lookup === null)
|
|
26
26
|
return '';
|
|
27
27
|
if (!filter)
|
|
28
|
-
return String(
|
|
29
|
-
return applyFilter(
|
|
28
|
+
return String(lookup);
|
|
29
|
+
return applyFilter(lookup, filter, arg, locale);
|
|
30
30
|
});
|
|
31
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Resolve a placeholder key against `vars`. Supports dot-path (`hero.title`)
|
|
34
|
+
* and auto-unwraps an `I18nText`-shaped record (`{ pl: '…', en: '…' }`) to
|
|
35
|
+
* the active locale (falls back to the first key when the exact locale is
|
|
36
|
+
* missing). Returns `undefined` when the path does not exist — caller treats
|
|
37
|
+
* that as "unknown key" and warns. Returns `null` when the path exists but
|
|
38
|
+
* the value is nullish; caller emits empty string.
|
|
39
|
+
*
|
|
40
|
+
* @internal
|
|
41
|
+
*/
|
|
42
|
+
function resolveKey(vars, key, locale) {
|
|
43
|
+
const parts = key.split('.');
|
|
44
|
+
let current = vars;
|
|
45
|
+
for (const part of parts) {
|
|
46
|
+
if (current === null || current === undefined)
|
|
47
|
+
return undefined;
|
|
48
|
+
if (typeof current !== 'object')
|
|
49
|
+
return undefined;
|
|
50
|
+
const obj = current;
|
|
51
|
+
if (!(part in obj))
|
|
52
|
+
return undefined;
|
|
53
|
+
current = obj[part];
|
|
54
|
+
}
|
|
55
|
+
return unwrapI18n(current, locale);
|
|
56
|
+
}
|
|
57
|
+
/** @internal */
|
|
58
|
+
function unwrapI18n(value, locale) {
|
|
59
|
+
if (value === null || value === undefined)
|
|
60
|
+
return value;
|
|
61
|
+
if (typeof value !== 'object' || Array.isArray(value))
|
|
62
|
+
return value;
|
|
63
|
+
const obj = value;
|
|
64
|
+
const keys = Object.keys(obj);
|
|
65
|
+
if (keys.length === 0)
|
|
66
|
+
return value;
|
|
67
|
+
const allStrings = keys.every((k) => typeof obj[k] === 'string');
|
|
68
|
+
if (!allStrings)
|
|
69
|
+
return value;
|
|
70
|
+
if (locale in obj)
|
|
71
|
+
return obj[locale];
|
|
72
|
+
const baseLocale = locale.split('-')[0];
|
|
73
|
+
if (baseLocale in obj)
|
|
74
|
+
return obj[baseLocale];
|
|
75
|
+
return obj[keys[0]];
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Render a multilingual shop-name template (`invoiceName` / `cartName`)
|
|
79
|
+
* against an entry's data and a precomputed variant label. Returns `null`
|
|
80
|
+
* when the template is absent or yields an empty string after trimming —
|
|
81
|
+
* callers fall back to legacy behavior.
|
|
82
|
+
*
|
|
83
|
+
* Variables: `{<slug>}` resolves to any top-level (dot-path) field on the
|
|
84
|
+
* entry's published data. The reserved `{variant}` returns the rendered
|
|
85
|
+
* variant label. I18n fields stored as `{ pl: '…', en: '…' }` are auto
|
|
86
|
+
* unwrapped to the active language.
|
|
87
|
+
*
|
|
88
|
+
* @public
|
|
89
|
+
*/
|
|
90
|
+
export function renderShopName(template, ctx, locale) {
|
|
91
|
+
if (!template)
|
|
92
|
+
return null;
|
|
93
|
+
const tpl = template[ctx.language] ?? template[ctx.language.split('-')[0]] ?? null;
|
|
94
|
+
if (!tpl)
|
|
95
|
+
return null;
|
|
96
|
+
const vars = {
|
|
97
|
+
...(ctx.entryData ?? {}),
|
|
98
|
+
variant: ctx.variant
|
|
99
|
+
};
|
|
100
|
+
for (const match of tpl.matchAll(/\{([^}]+)\}/g)) {
|
|
101
|
+
const key = match[1].split('|')[0].trim();
|
|
102
|
+
const value = resolveKey(vars, key, locale);
|
|
103
|
+
if (value === undefined || value === null || value === '')
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
const rendered = interpolateTemplate(tpl, vars, locale).trim();
|
|
107
|
+
return rendered.length > 0 ? rendered : null;
|
|
108
|
+
}
|
|
32
109
|
/** @internal */
|
|
33
110
|
function parsePlaceholder(body) {
|
|
34
111
|
const [keyPart, filterPart] = body.split('|', 2);
|
package/dist/shop/types.d.ts
CHANGED
|
@@ -394,6 +394,28 @@ export interface ShopConfig {
|
|
|
394
394
|
* @public
|
|
395
395
|
*/
|
|
396
396
|
variantLabel?: VariantLabelConfig;
|
|
397
|
+
/**
|
|
398
|
+
* Multilingual template overriding the product name shown on invoices
|
|
399
|
+
* (Fakturownia etc.) and — unless `cartName` is set — also in cart and
|
|
400
|
+
* order views. Keys are language codes (`pl`, `en`, …). Placeholders use
|
|
401
|
+
* the same engine as `variantLabel`: `{slug}` resolves to a top-level
|
|
402
|
+
* field on the entry (dot-path supported, i18n-aware); the reserved
|
|
403
|
+
* `{variant}` resolves to the rendered `variantLabel`. Filters from
|
|
404
|
+
* {@link interpolateTemplate} (`|date`, `|currency`, `|uppercase`) apply.
|
|
405
|
+
* Omitted = legacy behavior (`product — variant`).
|
|
406
|
+
* @public
|
|
407
|
+
*/
|
|
408
|
+
invoiceName?: I18nText;
|
|
409
|
+
/**
|
|
410
|
+
* Multilingual template overriding the product name shown in cart and
|
|
411
|
+
* storefront order views ONLY (invoice still uses `invoiceName` or
|
|
412
|
+
* legacy). Set this in addition to `invoiceName` when the cart needs a
|
|
413
|
+
* shorter / different copy than the invoice line. Same syntax as
|
|
414
|
+
* {@link ShopConfig.invoiceName}. Omitted = falls back to `invoiceName`,
|
|
415
|
+
* then to legacy `productTitle`.
|
|
416
|
+
* @public
|
|
417
|
+
*/
|
|
418
|
+
cartName?: I18nText;
|
|
397
419
|
/**
|
|
398
420
|
* Opt-in storefront filter for time-bound variants (events, courses).
|
|
399
421
|
* Omitted = no filtering (legacy behavior — every variant always listed).
|
|
@@ -434,7 +456,7 @@ export interface ShopConfig {
|
|
|
434
456
|
*/
|
|
435
457
|
onOrderPaid?: (order: Order) => Promise<void> | void;
|
|
436
458
|
}
|
|
437
|
-
export interface ResolvedShopConfig extends Omit<ShopConfig, 'variantLabel' | 'variantExpiry' | 'invoicing'> {
|
|
459
|
+
export interface ResolvedShopConfig extends Omit<ShopConfig, 'variantLabel' | 'variantExpiry' | 'invoicing' | 'invoiceName' | 'cartName'> {
|
|
438
460
|
features: Required<ShopFeatures>;
|
|
439
461
|
rateLimit: Required<ShopRateLimit>;
|
|
440
462
|
carriers: CarrierAdapter[];
|
|
@@ -444,5 +466,7 @@ export interface ResolvedShopConfig extends Omit<ShopConfig, 'variantLabel' | 'v
|
|
|
444
466
|
variantAttributes: Record<string, VariantAttribute>;
|
|
445
467
|
variantLabel: VariantLabelConfig | null;
|
|
446
468
|
variantExpiry: VariantExpiryConfig | null;
|
|
469
|
+
invoiceName: I18nText | null;
|
|
470
|
+
cartName: I18nText | null;
|
|
447
471
|
}
|
|
448
472
|
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export const update = {
|
|
2
|
+
version: '0.36.0',
|
|
3
|
+
date: '2026-06-05',
|
|
4
|
+
description: '`defineShop({ invoiceName, cartName })` — opcjonalne multilingual template overriding produktowej nazwy w koszyku, podsumowaniu zamówienia i na fakturze. Reusuje engine od `variantLabel.template` (`{slug}`, `{slug|filter}`), z nowymi możliwościami: dot-path (`{hero.title}`) i auto-unwrap pól i18n. Reserved `{variant}` = wyrenderowany variantLabel. Order item `nameSnapshot` zyskuje opcjonalne pole `invoice` (zamrażane przy checkout). Additive — brak zmian default behavior.',
|
|
5
|
+
features: [
|
|
6
|
+
'`ShopConfig.invoiceName?: I18nText` i `ShopConfig.cartName?: I18nText` — template (per język) renderowany przy hydracji koszyka i tworzeniu zamówienia. Fallback chain: `cartName` (koszyk) → `invoiceName` (koszyk + faktura) → legacy `productTitle` / `product — variant`. `invoiceName` jest **full override** na fakturze — wariant NIE jest doklejany automatycznie, użyj `{variant}` w template.',
|
|
7
|
+
'`interpolateTemplate()` (`$lib/shop/template.ts`) zyskuje dot-path resolver (`{hero.title}`) oraz auto-unwrap pól i18n `{ pl: …, en: … }` do aktualnego języka (z fallbackiem `lang.split("-")[0]` → pierwszy klucz). Backwards-compatible: istniejące `{key}` (płaskie) działają bez zmian.',
|
|
8
|
+
'`renderShopName(template, ctx, locale)` (`$lib/shop/template.ts`) — public helper łączący resolverę I18nText template + budowę vars `{ ...entryData, variant }`. Używany wewnętrznie przez cart-hydrate; eksportowany dla customowych use case (np. own checkout flows).',
|
|
9
|
+
'`CartLine` zyskuje pole `invoiceTitle: string | null` — frozen invoice copy obok `productTitle` (cart copy). `orders.createOrderFromCart` snapshot-uje to do `nameSnapshot.invoice`. `buildInvoicePayload()` honoruje `nameSnapshot.invoice` — fallback do `product — variant` gdy brak.'
|
|
10
|
+
],
|
|
11
|
+
fixes: [],
|
|
12
|
+
breakingChanges: []
|
|
13
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const update = {
|
|
2
|
+
version: '0.36.1',
|
|
3
|
+
date: '2026-06-08',
|
|
4
|
+
description: 'Fakturownia adapter — fix B2C: gdy brak `buyer.nip` i `buyer.companyName`, payload zawiera `buyer_first_name` + `buyer_last_name` (rozdzielone przez `splitFullName` — spójnie z PayU/InPost). Bez tej zmiany Fakturownia API odrzuca fakturę dla osoby fizycznej z 422 `buyer_tax_no — nie może być puste`.',
|
|
5
|
+
features: [],
|
|
6
|
+
fixes: [
|
|
7
|
+
'`buildFakturowniaInvoice` — dla B2C (brak NIP i companyName) payload zawiera teraz `buyer_first_name` i `buyer_last_name` (rozdzielone z `buyer.name` przez `splitFullName` z adaptera PayU). B2B (z NIP lub companyName) zachowuje obecne zachowanie — bez first/last name. Dla `splitFullName("Madonna")` (1 słowo) → tylko `buyer_first_name`. Naprawia 422 z Fakturownia dla osób fizycznych.'
|
|
8
|
+
],
|
|
9
|
+
breakingChanges: []
|
|
10
|
+
};
|
package/dist/updates/index.js
CHANGED
|
@@ -66,6 +66,8 @@ import { update as update0280 } from './0.28.0/index.js';
|
|
|
66
66
|
import { update as update0340 } from './0.34.0/index.js';
|
|
67
67
|
import { update as update0341 } from './0.34.1/index.js';
|
|
68
68
|
import { update as update0350 } from './0.35.0/index.js';
|
|
69
|
+
import { update as update0360 } from './0.36.0/index.js';
|
|
70
|
+
import { update as update0361 } from './0.36.1/index.js';
|
|
69
71
|
export const updates = [
|
|
70
72
|
update0065,
|
|
71
73
|
update0066,
|
|
@@ -134,7 +136,9 @@ export const updates = [
|
|
|
134
136
|
update0280,
|
|
135
137
|
update0340,
|
|
136
138
|
update0341,
|
|
137
|
-
update0350
|
|
139
|
+
update0350,
|
|
140
|
+
update0360,
|
|
141
|
+
update0361
|
|
138
142
|
];
|
|
139
143
|
export const getUpdatesFrom = (fromVersion) => {
|
|
140
144
|
const fromParts = fromVersion.split('.').map(Number);
|