includio-cms 0.34.1 → 0.36.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/API.md +6 -2
- package/CHANGELOG.md +29 -0
- package/DOCS.md +1 -1
- package/dist/admin/client/shop/coupon-edit-page.svelte +1 -0
- package/dist/admin/client/shop/coupon-form.svelte +62 -2
- package/dist/admin/client/shop/coupon-schema.d.ts +5 -0
- package/dist/admin/client/shop/coupon-schema.js +2 -0
- package/dist/admin/client/shop/shop-order-detail-page.svelte +72 -2
- package/dist/admin/components/fields/date-field.svelte +81 -27
- package/dist/admin/components/fields/date-field.svelte.d.ts +3 -0
- package/dist/admin/components/fields/datetime-field.svelte +142 -29
- package/dist/admin/components/fields/datetime-field.svelte.d.ts +3 -0
- package/dist/admin/remote/shop.remote.d.ts +6 -0
- package/dist/admin/remote/shop.remote.js +4 -0
- package/dist/core/server/generator/generator.js +3 -2
- package/dist/db-postgres/schema/shop/coupons.d.ts +20 -0
- package/dist/db-postgres/schema/shop/coupons.js +3 -0
- package/dist/paraglide/messages/_index.d.ts +36 -3
- package/dist/paraglide/messages/_index.js +71 -3
- package/dist/paraglide/messages/en.d.ts +5 -0
- package/dist/paraglide/messages/en.js +14 -0
- package/dist/paraglide/messages/pl.d.ts +5 -0
- package/dist/paraglide/messages/pl.js +14 -0
- package/dist/shop/cart/types.d.ts +7 -0
- package/dist/shop/client/index.d.ts +1 -0
- package/dist/shop/http/order-handler.js +2 -1
- package/dist/shop/index.d.ts +2 -1
- package/dist/shop/index.js +4 -2
- package/dist/shop/pricing.d.ts +18 -6
- package/dist/shop/pricing.js +33 -8
- package/dist/shop/server/cart-hydrate.js +30 -3
- package/dist/shop/server/coupons.js +3 -2
- package/dist/shop/server/email.js +41 -7
- 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/templates/_partials/items.en.html +10 -0
- package/dist/shop/templates/_partials/items.pl.html +10 -0
- package/dist/shop/types.d.ts +25 -1
- package/dist/updates/0.35.0/index.d.ts +2 -0
- package/dist/updates/0.35.0/index.js +16 -0
- package/dist/updates/0.36.0/index.d.ts +2 -0
- package/dist/updates/0.36.0/index.js +13 -0
- package/dist/updates/index.js +5 -1
- package/package.json +1 -1
- package/dist/paraglide/messages/hello_world.d.ts +0 -5
- package/dist/paraglide/messages/hello_world.js +0 -33
- package/dist/paraglide/messages/login_hello.d.ts +0 -16
- package/dist/paraglide/messages/login_hello.js +0 -34
- package/dist/paraglide/messages/login_please_login.d.ts +0 -16
- package/dist/paraglide/messages/login_please_login.js +0 -34
|
@@ -2,6 +2,9 @@ import type { DateTimeField } from '../../../types/fields.js';
|
|
|
2
2
|
type Props = {
|
|
3
3
|
field: DateTimeField;
|
|
4
4
|
value: string | undefined;
|
|
5
|
+
id?: string;
|
|
6
|
+
'aria-invalid'?: string | boolean | undefined;
|
|
7
|
+
'aria-describedby'?: string | undefined;
|
|
5
8
|
};
|
|
6
9
|
declare const DatetimeField: import("svelte").Component<Props, {}, "value">;
|
|
7
10
|
type DatetimeField = ReturnType<typeof DatetimeField>;
|
|
@@ -413,6 +413,7 @@ export declare const listCouponsAdmin: import("@sveltejs/kit").RemoteQueryFuncti
|
|
|
413
413
|
code: string;
|
|
414
414
|
type: import("../../db-postgres/schema/shop/index.js").ShopCouponType;
|
|
415
415
|
value: string;
|
|
416
|
+
appliesTo: import("../../db-postgres/schema/shop/index.js").ShopCouponAppliesTo;
|
|
416
417
|
minOrderAmount: number | null;
|
|
417
418
|
maxUses: number | null;
|
|
418
419
|
usedCount: number;
|
|
@@ -426,6 +427,7 @@ export declare const getCouponAdmin: import("@sveltejs/kit").RemoteQueryFunction
|
|
|
426
427
|
code: string;
|
|
427
428
|
type: import("../../db-postgres/schema/shop/index.js").ShopCouponType;
|
|
428
429
|
value: string;
|
|
430
|
+
appliesTo: import("../../db-postgres/schema/shop/index.js").ShopCouponAppliesTo;
|
|
429
431
|
minOrderAmount: number | null;
|
|
430
432
|
maxUses: number | null;
|
|
431
433
|
usedCount: number;
|
|
@@ -438,6 +440,7 @@ export declare const createCouponCmd: import("@sveltejs/kit").RemoteCommand<{
|
|
|
438
440
|
code: string;
|
|
439
441
|
type: "fixed" | "percent";
|
|
440
442
|
value: number;
|
|
443
|
+
appliesTo?: "net" | "gross" | undefined;
|
|
441
444
|
minOrderAmount?: number | null | undefined;
|
|
442
445
|
maxUses?: number | null | undefined;
|
|
443
446
|
expiresAt?: string | null | undefined;
|
|
@@ -451,6 +454,7 @@ export declare const createCouponCmd: import("@sveltejs/kit").RemoteCommand<{
|
|
|
451
454
|
expiresAt: Date | null;
|
|
452
455
|
value: string;
|
|
453
456
|
type: import("../../db-postgres/schema/shop/index.js").ShopCouponType;
|
|
457
|
+
appliesTo: import("../../db-postgres/schema/shop/index.js").ShopCouponAppliesTo;
|
|
454
458
|
minOrderAmount: number | null;
|
|
455
459
|
maxUses: number | null;
|
|
456
460
|
usedCount: number;
|
|
@@ -460,6 +464,7 @@ export declare const updateCouponCmd: import("@sveltejs/kit").RemoteCommand<{
|
|
|
460
464
|
input: {
|
|
461
465
|
code?: string | undefined;
|
|
462
466
|
type?: "fixed" | "percent" | undefined;
|
|
467
|
+
appliesTo?: "net" | "gross" | undefined;
|
|
463
468
|
value?: number | undefined;
|
|
464
469
|
minOrderAmount?: number | null | undefined;
|
|
465
470
|
maxUses?: number | null | undefined;
|
|
@@ -471,6 +476,7 @@ export declare const updateCouponCmd: import("@sveltejs/kit").RemoteCommand<{
|
|
|
471
476
|
code: string;
|
|
472
477
|
type: import("../../db-postgres/schema/shop/index.js").ShopCouponType;
|
|
473
478
|
value: string;
|
|
479
|
+
appliesTo: import("../../db-postgres/schema/shop/index.js").ShopCouponAppliesTo;
|
|
474
480
|
minOrderAmount: number | null;
|
|
475
481
|
maxUses: number | null;
|
|
476
482
|
usedCount: number;
|
|
@@ -346,6 +346,7 @@ export const generateBalanceLinkForOrder = command(z.string(), async (orderId) =
|
|
|
346
346
|
const couponInputSchema = z.object({
|
|
347
347
|
code: z.string().min(1).max(64),
|
|
348
348
|
type: z.enum(['percent', 'fixed']),
|
|
349
|
+
appliesTo: z.enum(['net', 'gross']).optional(),
|
|
349
350
|
value: z.number().nonnegative().max(1e9),
|
|
350
351
|
minOrderAmount: z.number().int().nonnegative().nullable().optional(),
|
|
351
352
|
maxUses: z.number().int().positive().nullable().optional(),
|
|
@@ -372,6 +373,7 @@ export const createCouponCmd = command(couponInputSchema, async (input) => {
|
|
|
372
373
|
.values({
|
|
373
374
|
code,
|
|
374
375
|
type: input.type,
|
|
376
|
+
appliesTo: input.appliesTo ?? 'net',
|
|
375
377
|
value: String(input.value),
|
|
376
378
|
minOrderAmount: input.minOrderAmount ?? null,
|
|
377
379
|
maxUses: input.maxUses ?? null,
|
|
@@ -389,6 +391,8 @@ export const updateCouponCmd = command(z.object({ id: z.string(), input: couponI
|
|
|
389
391
|
patch.code = input.code.trim().toUpperCase();
|
|
390
392
|
if (input.type !== undefined)
|
|
391
393
|
patch.type = input.type;
|
|
394
|
+
if (input.appliesTo !== undefined)
|
|
395
|
+
patch.appliesTo = input.appliesTo;
|
|
392
396
|
if (input.value !== undefined)
|
|
393
397
|
patch.value = String(input.value);
|
|
394
398
|
if (input.minOrderAmount !== undefined)
|
|
@@ -178,10 +178,11 @@ function generateAPI(config) {
|
|
|
178
178
|
const cmsDir = join(process.cwd(), 'src/lib/cms/runtime');
|
|
179
179
|
const filePath = join(cmsDir, 'api.ts');
|
|
180
180
|
let code = `// This file is auto-generated. Do not edit directly.\n\n`;
|
|
181
|
+
const hasForms = !!(config.forms && config.forms.length > 0);
|
|
181
182
|
code += `
|
|
182
183
|
|
|
183
|
-
import type { SingleEntryMap, SingleSlug, CollectionEntryMap, CollectionSlug
|
|
184
|
-
import { resolveEntry, resolveEntries, countEntries
|
|
184
|
+
import type { SingleEntryMap, SingleSlug, CollectionEntryMap, CollectionSlug,${hasForms ? ' FormEntryMap,' : ''} SiteLanguage } from './types';
|
|
185
|
+
import { resolveEntry, resolveEntries, countEntries,${hasForms ? ' createFormSubmission,' : ''} type PopulateConfig } from 'includio-cms/sveltekit/server';
|
|
185
186
|
|
|
186
187
|
`;
|
|
187
188
|
code += `
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export type ShopCouponType = 'percent' | 'fixed';
|
|
2
|
+
export type ShopCouponAppliesTo = 'net' | 'gross';
|
|
2
3
|
export declare const shopCouponsTable: import("drizzle-orm/pg-core/table", { with: { "resolution-mode": "require" } }).PgTableWithColumns<{
|
|
3
4
|
name: "shop_coupons";
|
|
4
5
|
schema: undefined;
|
|
@@ -73,6 +74,25 @@ export declare const shopCouponsTable: import("drizzle-orm/pg-core/table", { wit
|
|
|
73
74
|
identity: undefined;
|
|
74
75
|
generated: undefined;
|
|
75
76
|
}, {}, {}>;
|
|
77
|
+
appliesTo: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
|
|
78
|
+
name: "applies_to";
|
|
79
|
+
tableName: "shop_coupons";
|
|
80
|
+
dataType: "string";
|
|
81
|
+
columnType: "PgText";
|
|
82
|
+
data: ShopCouponAppliesTo;
|
|
83
|
+
driverParam: string;
|
|
84
|
+
notNull: true;
|
|
85
|
+
hasDefault: true;
|
|
86
|
+
isPrimaryKey: false;
|
|
87
|
+
isAutoincrement: false;
|
|
88
|
+
hasRuntimeDefault: false;
|
|
89
|
+
enumValues: [string, ...string[]];
|
|
90
|
+
baseColumn: never;
|
|
91
|
+
identity: undefined;
|
|
92
|
+
generated: undefined;
|
|
93
|
+
}, {}, {
|
|
94
|
+
$type: ShopCouponAppliesTo;
|
|
95
|
+
}>;
|
|
76
96
|
minOrderAmount: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
|
|
77
97
|
name: "min_order_amount";
|
|
78
98
|
tableName: "shop_coupons";
|
|
@@ -6,6 +6,9 @@ export const shopCouponsTable = pgTable('shop_coupons', {
|
|
|
6
6
|
type: text('type').$type().notNull(),
|
|
7
7
|
// percent → integer 0-100 stored as numeric for symmetry; fixed → PLN value (precision 20,6 like product.basePrice)
|
|
8
8
|
value: numeric('value', { precision: 20, scale: 6 }).notNull(),
|
|
9
|
+
// Whether `value` applies to the net or gross subtotal. Storage of the
|
|
10
|
+
// resulting discount is always net (canonical) — see calculateCouponDiscountNet.
|
|
11
|
+
appliesTo: text('applies_to').$type().notNull().default('net'),
|
|
9
12
|
minOrderAmount: integer('min_order_amount'),
|
|
10
13
|
maxUses: integer('max_uses'),
|
|
11
14
|
usedCount: integer('used_count').notNull().default(0),
|
|
@@ -1,3 +1,36 @@
|
|
|
1
|
-
export
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
export function hello_world(inputs: {
|
|
2
|
+
name: NonNullable<unknown>;
|
|
3
|
+
}, options?: {
|
|
4
|
+
locale?: "en" | "pl";
|
|
5
|
+
}): string;
|
|
6
|
+
/**
|
|
7
|
+
* This function has been compiled by [Paraglide JS](https://inlang.com/m/gerre34r).
|
|
8
|
+
*
|
|
9
|
+
* - Changing this function will be over-written by the next build.
|
|
10
|
+
*
|
|
11
|
+
* - If you want to change the translations, you can either edit the source files e.g. `en.json`, or
|
|
12
|
+
* use another inlang app like [Fink](https://inlang.com/m/tdozzpar) or the [VSCode extension Sherlock](https://inlang.com/m/r7kp499g).
|
|
13
|
+
*
|
|
14
|
+
* @param {{}} inputs
|
|
15
|
+
* @param {{ locale?: "en" | "pl" }} options
|
|
16
|
+
* @returns {string}
|
|
17
|
+
*/
|
|
18
|
+
declare function login_hello(inputs?: {}, options?: {
|
|
19
|
+
locale?: "en" | "pl";
|
|
20
|
+
}): string;
|
|
21
|
+
/**
|
|
22
|
+
* This function has been compiled by [Paraglide JS](https://inlang.com/m/gerre34r).
|
|
23
|
+
*
|
|
24
|
+
* - Changing this function will be over-written by the next build.
|
|
25
|
+
*
|
|
26
|
+
* - If you want to change the translations, you can either edit the source files e.g. `en.json`, or
|
|
27
|
+
* use another inlang app like [Fink](https://inlang.com/m/tdozzpar) or the [VSCode extension Sherlock](https://inlang.com/m/r7kp499g).
|
|
28
|
+
*
|
|
29
|
+
* @param {{}} inputs
|
|
30
|
+
* @param {{ locale?: "en" | "pl" }} options
|
|
31
|
+
* @returns {string}
|
|
32
|
+
*/
|
|
33
|
+
declare function login_please_login(inputs?: {}, options?: {
|
|
34
|
+
locale?: "en" | "pl";
|
|
35
|
+
}): string;
|
|
36
|
+
export { login_hello as login.hello, login_please_login as login.please_login };
|
|
@@ -1,4 +1,72 @@
|
|
|
1
1
|
/* eslint-disable */
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
import { getLocale, trackMessageCall, experimentalMiddlewareLocaleSplitting, isServer } from "../runtime.js"
|
|
3
|
+
import * as en from "./en.js"
|
|
4
|
+
import * as pl from "./pl.js"
|
|
5
|
+
/**
|
|
6
|
+
* This function has been compiled by [Paraglide JS](https://inlang.com/m/gerre34r).
|
|
7
|
+
*
|
|
8
|
+
* - Changing this function will be over-written by the next build.
|
|
9
|
+
*
|
|
10
|
+
* - If you want to change the translations, you can either edit the source files e.g. `en.json`, or
|
|
11
|
+
* use another inlang app like [Fink](https://inlang.com/m/tdozzpar) or the [VSCode extension Sherlock](https://inlang.com/m/r7kp499g).
|
|
12
|
+
*
|
|
13
|
+
* @param {{ name: NonNullable<unknown> }} inputs
|
|
14
|
+
* @param {{ locale?: "en" | "pl" }} options
|
|
15
|
+
* @returns {string}
|
|
16
|
+
*/
|
|
17
|
+
/* @__NO_SIDE_EFFECTS__ */
|
|
18
|
+
export const hello_world = (inputs, options = {}) => {
|
|
19
|
+
if (experimentalMiddlewareLocaleSplitting && isServer === false) {
|
|
20
|
+
return /** @type {any} */ (globalThis).__paraglide_ssr.hello_world(inputs)
|
|
21
|
+
}
|
|
22
|
+
const locale = options.locale ?? getLocale()
|
|
23
|
+
trackMessageCall("hello_world", locale)
|
|
24
|
+
if (locale === "en") return en.hello_world(inputs)
|
|
25
|
+
return pl.hello_world(inputs)
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* This function has been compiled by [Paraglide JS](https://inlang.com/m/gerre34r).
|
|
29
|
+
*
|
|
30
|
+
* - Changing this function will be over-written by the next build.
|
|
31
|
+
*
|
|
32
|
+
* - If you want to change the translations, you can either edit the source files e.g. `en.json`, or
|
|
33
|
+
* use another inlang app like [Fink](https://inlang.com/m/tdozzpar) or the [VSCode extension Sherlock](https://inlang.com/m/r7kp499g).
|
|
34
|
+
*
|
|
35
|
+
* @param {{}} inputs
|
|
36
|
+
* @param {{ locale?: "en" | "pl" }} options
|
|
37
|
+
* @returns {string}
|
|
38
|
+
*/
|
|
39
|
+
/* @__NO_SIDE_EFFECTS__ */
|
|
40
|
+
const login_hello = (inputs = {}, options = {}) => {
|
|
41
|
+
if (experimentalMiddlewareLocaleSplitting && isServer === false) {
|
|
42
|
+
return /** @type {any} */ (globalThis).__paraglide_ssr.login_hello(inputs)
|
|
43
|
+
}
|
|
44
|
+
const locale = options.locale ?? getLocale()
|
|
45
|
+
trackMessageCall("login_hello", locale)
|
|
46
|
+
if (locale === "en") return en.login_hello(inputs)
|
|
47
|
+
return pl.login_hello(inputs)
|
|
48
|
+
};
|
|
49
|
+
export { login_hello as "login.hello" }
|
|
50
|
+
/**
|
|
51
|
+
* This function has been compiled by [Paraglide JS](https://inlang.com/m/gerre34r).
|
|
52
|
+
*
|
|
53
|
+
* - Changing this function will be over-written by the next build.
|
|
54
|
+
*
|
|
55
|
+
* - If you want to change the translations, you can either edit the source files e.g. `en.json`, or
|
|
56
|
+
* use another inlang app like [Fink](https://inlang.com/m/tdozzpar) or the [VSCode extension Sherlock](https://inlang.com/m/r7kp499g).
|
|
57
|
+
*
|
|
58
|
+
* @param {{}} inputs
|
|
59
|
+
* @param {{ locale?: "en" | "pl" }} options
|
|
60
|
+
* @returns {string}
|
|
61
|
+
*/
|
|
62
|
+
/* @__NO_SIDE_EFFECTS__ */
|
|
63
|
+
const login_please_login = (inputs = {}, options = {}) => {
|
|
64
|
+
if (experimentalMiddlewareLocaleSplitting && isServer === false) {
|
|
65
|
+
return /** @type {any} */ (globalThis).__paraglide_ssr.login_please_login(inputs)
|
|
66
|
+
}
|
|
67
|
+
const locale = options.locale ?? getLocale()
|
|
68
|
+
trackMessageCall("login_please_login", locale)
|
|
69
|
+
if (locale === "en") return en.login_please_login(inputs)
|
|
70
|
+
return pl.login_please_login(inputs)
|
|
71
|
+
};
|
|
72
|
+
export { login_please_login as "login.please_login" }
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
export const hello_world = /** @type {(inputs: { name: NonNullable<unknown> }) => string} */ (i) => {
|
|
5
|
+
return `Hello, ${i.name} from en!`
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const login_hello = /** @type {(inputs: {}) => string} */ () => {
|
|
9
|
+
return `Welcome back`
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const login_please_login = /** @type {(inputs: {}) => string} */ () => {
|
|
13
|
+
return `Login to your account`
|
|
14
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
export const hello_world = /** @type {(inputs: { name: NonNullable<unknown> }) => string} */ (i) => {
|
|
5
|
+
return `Hello, ${i.name} from pl!`
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const login_hello = /** @type {(inputs: {}) => string} */ () => {
|
|
9
|
+
return `Witaj ponownie`
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const login_please_login = /** @type {(inputs: {}) => string} */ () => {
|
|
13
|
+
return `Zaloguj się na swoje konto`
|
|
14
|
+
};
|
|
@@ -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;
|
|
@@ -76,7 +76,8 @@ export function createOrderHandler() {
|
|
|
76
76
|
trackingNumber: order.trackingNumber,
|
|
77
77
|
trackingUrl,
|
|
78
78
|
language: order.language,
|
|
79
|
-
createdAt: order.createdAt
|
|
79
|
+
createdAt: order.createdAt,
|
|
80
|
+
notes: order.notes ?? null
|
|
80
81
|
},
|
|
81
82
|
items: items.map((i) => ({
|
|
82
83
|
id: i.id,
|
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';
|
package/dist/shop/pricing.d.ts
CHANGED
|
@@ -21,14 +21,26 @@ export interface CouponDiscountInput {
|
|
|
21
21
|
type: 'percent' | 'fixed';
|
|
22
22
|
/** percent: 0-100; fixed: PLN value (precision 20,6). */
|
|
23
23
|
value: number;
|
|
24
|
+
/** Whether `value` is applied to the net or gross subtotal. Defaults to 'net'. */
|
|
25
|
+
appliesTo?: 'net' | 'gross';
|
|
24
26
|
}
|
|
25
27
|
/**
|
|
26
28
|
* Compute discount in cents (minor units) applied to a net subtotal.
|
|
27
|
-
* - `percent`: subtotalNet × (value / 100), rounded to nearest cent.
|
|
28
|
-
* - `fixed`: PLN value converted to cents, capped at subtotalNet (never negative).
|
|
29
29
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
30
|
+
* Behavior by mode:
|
|
31
|
+
* - `appliesTo='net'` (default):
|
|
32
|
+
* - `percent`: subtotalNet × (value / 100), rounded to nearest cent.
|
|
33
|
+
* - `fixed`: PLN value converted to cents, capped at subtotalNet.
|
|
34
|
+
* - `appliesTo='gross'` (requires `subtotalGross`):
|
|
35
|
+
* - `percent`: discount expressed against the gross subtotal, then converted
|
|
36
|
+
* to net via the aggregate net/gross ratio (handles mixed VAT rates).
|
|
37
|
+
* Mathematically yields the same proportional factor as net-percent —
|
|
38
|
+
* kept explicit for clarity and rounding parity with the fixed-gross path.
|
|
39
|
+
* - `fixed`: PLN value interpreted as a gross amount, capped at subtotalGross,
|
|
40
|
+
* then converted to net via `× subtotalNet / subtotalGross`.
|
|
41
|
+
*
|
|
42
|
+
* Discount storage is always net (canonical) — VAT is applied to lines after
|
|
43
|
+
* the discount factor, so the rebate appears proportionally on each line.
|
|
44
|
+
* Result is bounded to `[0, subtotalNet]`.
|
|
33
45
|
*/
|
|
34
|
-
export declare function calculateCouponDiscountNet(subtotalNet: number, coupon: CouponDiscountInput): number;
|
|
46
|
+
export declare function calculateCouponDiscountNet(subtotalNet: number, coupon: CouponDiscountInput, subtotalGross?: number): number;
|
package/dist/shop/pricing.js
CHANGED
|
@@ -49,23 +49,48 @@ export function resolveI18n(value, language, fallback) {
|
|
|
49
49
|
}
|
|
50
50
|
/**
|
|
51
51
|
* Compute discount in cents (minor units) applied to a net subtotal.
|
|
52
|
-
* - `percent`: subtotalNet × (value / 100), rounded to nearest cent.
|
|
53
|
-
* - `fixed`: PLN value converted to cents, capped at subtotalNet (never negative).
|
|
54
52
|
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
53
|
+
* Behavior by mode:
|
|
54
|
+
* - `appliesTo='net'` (default):
|
|
55
|
+
* - `percent`: subtotalNet × (value / 100), rounded to nearest cent.
|
|
56
|
+
* - `fixed`: PLN value converted to cents, capped at subtotalNet.
|
|
57
|
+
* - `appliesTo='gross'` (requires `subtotalGross`):
|
|
58
|
+
* - `percent`: discount expressed against the gross subtotal, then converted
|
|
59
|
+
* to net via the aggregate net/gross ratio (handles mixed VAT rates).
|
|
60
|
+
* Mathematically yields the same proportional factor as net-percent —
|
|
61
|
+
* kept explicit for clarity and rounding parity with the fixed-gross path.
|
|
62
|
+
* - `fixed`: PLN value interpreted as a gross amount, capped at subtotalGross,
|
|
63
|
+
* then converted to net via `× subtotalNet / subtotalGross`.
|
|
64
|
+
*
|
|
65
|
+
* Discount storage is always net (canonical) — VAT is applied to lines after
|
|
66
|
+
* the discount factor, so the rebate appears proportionally on each line.
|
|
67
|
+
* Result is bounded to `[0, subtotalNet]`.
|
|
58
68
|
*/
|
|
59
|
-
export function calculateCouponDiscountNet(subtotalNet, coupon) {
|
|
69
|
+
export function calculateCouponDiscountNet(subtotalNet, coupon, subtotalGross) {
|
|
60
70
|
if (subtotalNet <= 0)
|
|
61
71
|
return 0;
|
|
72
|
+
const appliesTo = coupon.appliesTo ?? 'net';
|
|
73
|
+
const useGross = appliesTo === 'gross' && typeof subtotalGross === 'number' && subtotalGross > 0;
|
|
62
74
|
let raw = 0;
|
|
63
75
|
if (coupon.type === 'percent') {
|
|
64
76
|
const pct = Math.max(0, Math.min(100, coupon.value));
|
|
65
|
-
|
|
77
|
+
if (useGross) {
|
|
78
|
+
const discountGross = Math.round(subtotalGross * (pct / 100));
|
|
79
|
+
raw = Math.round((discountGross * subtotalNet) / subtotalGross);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
raw = Math.round(subtotalNet * (pct / 100));
|
|
83
|
+
}
|
|
66
84
|
}
|
|
67
85
|
else if (coupon.type === 'fixed') {
|
|
68
|
-
|
|
86
|
+
const fixedCents = toCents(Math.max(0, coupon.value));
|
|
87
|
+
if (useGross) {
|
|
88
|
+
const cappedGross = Math.min(fixedCents, subtotalGross);
|
|
89
|
+
raw = Math.round((cappedGross * subtotalNet) / subtotalGross);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
raw = fixedCents;
|
|
93
|
+
}
|
|
69
94
|
}
|
|
70
95
|
return Math.max(0, Math.min(raw, subtotalNet));
|
|
71
96
|
}
|
|
@@ -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,
|
|
@@ -40,8 +40,9 @@ export async function validateCoupon(input) {
|
|
|
40
40
|
}
|
|
41
41
|
const discountNet = calculateCouponDiscountNet(input.subtotalNet, {
|
|
42
42
|
type: row.type,
|
|
43
|
-
value: Number(row.value)
|
|
44
|
-
|
|
43
|
+
value: Number(row.value),
|
|
44
|
+
appliesTo: (row.appliesTo ?? 'net')
|
|
45
|
+
}, input.subtotalGross);
|
|
45
46
|
return { row, discountNet };
|
|
46
47
|
}
|
|
47
48
|
/**
|
|
@@ -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';
|
|
@@ -70,6 +69,30 @@ export async function sendOrderStatusEmail(orderId, status) {
|
|
|
70
69
|
catch (err) {
|
|
71
70
|
console.error('[shop] Failed to load order coupon for email context:', err);
|
|
72
71
|
}
|
|
72
|
+
let participants = [];
|
|
73
|
+
if (order.notes) {
|
|
74
|
+
try {
|
|
75
|
+
const parsed = JSON.parse(order.notes);
|
|
76
|
+
if (Array.isArray(parsed?.participants)) {
|
|
77
|
+
participants = parsed.participants
|
|
78
|
+
.map((p) => {
|
|
79
|
+
const fn = typeof p?.firstName === 'string'
|
|
80
|
+
? p.firstName.trim()
|
|
81
|
+
: '';
|
|
82
|
+
const ln = typeof p?.lastName === 'string'
|
|
83
|
+
? p.lastName.trim()
|
|
84
|
+
: '';
|
|
85
|
+
if (!fn && !ln)
|
|
86
|
+
return null;
|
|
87
|
+
return { firstName: fn, lastName: ln };
|
|
88
|
+
})
|
|
89
|
+
.filter((p) => p !== null);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// notes nie jest JSON-em (legacy free-form) — pomijamy
|
|
94
|
+
}
|
|
95
|
+
}
|
|
73
96
|
const lang = (order.language || cms.languages[0] || 'pl');
|
|
74
97
|
const subjectKey = (lang in STATUS_SUBJECTS[status] ? lang : 'pl');
|
|
75
98
|
const viewUrl = /^https?:\/\//i.test(shop.orderViewUrl)
|
|
@@ -121,13 +144,24 @@ export async function sendOrderStatusEmail(orderId, status) {
|
|
|
121
144
|
discountAmount: coupon ? formatPrice(coupon.discountAmount, order.currency) : null,
|
|
122
145
|
hasDiscount: Boolean(coupon && coupon.discountAmount > 0)
|
|
123
146
|
},
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
qty: i.qty,
|
|
129
|
-
lineGross: formatPrice(i.priceGrossSnapshot * i.qty, order.currency)
|
|
147
|
+
participants: participants.map((p, idx) => ({
|
|
148
|
+
number: idx + 1,
|
|
149
|
+
firstName: p.firstName,
|
|
150
|
+
lastName: p.lastName
|
|
130
151
|
})),
|
|
152
|
+
hasParticipants: participants.length > 0,
|
|
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
|
+
}),
|
|
131
165
|
shop: {
|
|
132
166
|
currency: order.currency,
|
|
133
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,
|