includio-cms 0.34.1 → 0.35.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 +4 -2
- package/CHANGELOG.md +19 -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/client/index.d.ts +1 -0
- package/dist/shop/http/order-handler.js +2 -1
- package/dist/shop/pricing.d.ts +18 -6
- package/dist/shop/pricing.js +33 -8
- package/dist/shop/server/coupons.js +3 -2
- package/dist/shop/server/email.js +30 -0
- package/dist/shop/templates/_partials/items.en.html +10 -0
- package/dist/shop/templates/_partials/items.pl.html +10 -0
- package/dist/updates/0.35.0/index.d.ts +2 -0
- package/dist/updates/0.35.0/index.js +16 -0
- package/dist/updates/index.js +3 -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
|
@@ -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
|
+
};
|
|
@@ -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/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
|
}
|
|
@@ -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
|
/**
|
|
@@ -70,6 +70,30 @@ export async function sendOrderStatusEmail(orderId, status) {
|
|
|
70
70
|
catch (err) {
|
|
71
71
|
console.error('[shop] Failed to load order coupon for email context:', err);
|
|
72
72
|
}
|
|
73
|
+
let participants = [];
|
|
74
|
+
if (order.notes) {
|
|
75
|
+
try {
|
|
76
|
+
const parsed = JSON.parse(order.notes);
|
|
77
|
+
if (Array.isArray(parsed?.participants)) {
|
|
78
|
+
participants = parsed.participants
|
|
79
|
+
.map((p) => {
|
|
80
|
+
const fn = typeof p?.firstName === 'string'
|
|
81
|
+
? p.firstName.trim()
|
|
82
|
+
: '';
|
|
83
|
+
const ln = typeof p?.lastName === 'string'
|
|
84
|
+
? p.lastName.trim()
|
|
85
|
+
: '';
|
|
86
|
+
if (!fn && !ln)
|
|
87
|
+
return null;
|
|
88
|
+
return { firstName: fn, lastName: ln };
|
|
89
|
+
})
|
|
90
|
+
.filter((p) => p !== null);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// notes nie jest JSON-em (legacy free-form) — pomijamy
|
|
95
|
+
}
|
|
96
|
+
}
|
|
73
97
|
const lang = (order.language || cms.languages[0] || 'pl');
|
|
74
98
|
const subjectKey = (lang in STATUS_SUBJECTS[status] ? lang : 'pl');
|
|
75
99
|
const viewUrl = /^https?:\/\//i.test(shop.orderViewUrl)
|
|
@@ -121,6 +145,12 @@ export async function sendOrderStatusEmail(orderId, status) {
|
|
|
121
145
|
discountAmount: coupon ? formatPrice(coupon.discountAmount, order.currency) : null,
|
|
122
146
|
hasDiscount: Boolean(coupon && coupon.discountAmount > 0)
|
|
123
147
|
},
|
|
148
|
+
participants: participants.map((p, idx) => ({
|
|
149
|
+
number: idx + 1,
|
|
150
|
+
firstName: p.firstName,
|
|
151
|
+
lastName: p.lastName
|
|
152
|
+
})),
|
|
153
|
+
hasParticipants: participants.length > 0,
|
|
124
154
|
items: items.map((i) => ({
|
|
125
155
|
name: resolveI18n(i.nameSnapshot?.product
|
|
126
156
|
? { pl: i.nameSnapshot.product }
|
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
{{#if hasParticipants}}
|
|
2
|
+
<table style="width:100%;border-collapse:collapse;font-size:14px;margin-bottom:16px;">
|
|
3
|
+
<thead><tr style="background:#f4f2fa;"><th align="left" colspan="2" style="padding:8px;">Participants</th></tr></thead>
|
|
4
|
+
<tbody>
|
|
5
|
+
{{#each participants}}
|
|
6
|
+
<tr><td style="padding:6px 8px;border-bottom:1px solid #eee;width:24px;color:#5B4A9E;font-weight:700;text-align:right;">{{number}}.</td><td style="padding:6px 8px;border-bottom:1px solid #eee;font-weight:600;">{{firstName}} {{lastName}}</td></tr>
|
|
7
|
+
{{/each}}
|
|
8
|
+
</tbody>
|
|
9
|
+
</table>
|
|
10
|
+
{{/if}}
|
|
1
11
|
<table style="width:100%;border-collapse:collapse;font-size:14px;">
|
|
2
12
|
<thead><tr style="background:#f4f2fa;"><th align="left" style="padding:8px;">Item</th><th style="padding:8px;">Qty</th><th align="right" style="padding:8px;">Total</th></tr></thead>
|
|
3
13
|
<tbody>
|
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
{{#if hasParticipants}}
|
|
2
|
+
<table style="width:100%;border-collapse:collapse;font-size:14px;margin-bottom:16px;">
|
|
3
|
+
<thead><tr style="background:#f4f2fa;"><th align="left" colspan="2" style="padding:8px;">Uczestnicy</th></tr></thead>
|
|
4
|
+
<tbody>
|
|
5
|
+
{{#each participants}}
|
|
6
|
+
<tr><td style="padding:6px 8px;border-bottom:1px solid #eee;width:24px;color:#5B4A9E;font-weight:700;text-align:right;">{{number}}.</td><td style="padding:6px 8px;border-bottom:1px solid #eee;font-weight:600;">{{firstName}} {{lastName}}</td></tr>
|
|
7
|
+
{{/each}}
|
|
8
|
+
</tbody>
|
|
9
|
+
</table>
|
|
10
|
+
{{/if}}
|
|
1
11
|
<table style="width:100%;border-collapse:collapse;font-size:14px;">
|
|
2
12
|
<thead><tr style="background:#f4f2fa;"><th align="left" style="padding:8px;">Pozycja</th><th style="padding:8px;">Ilość</th><th align="right" style="padding:8px;">Suma</th></tr></thead>
|
|
3
13
|
<tbody>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const update = {
|
|
2
|
+
version: '0.35.0',
|
|
3
|
+
date: '2026-06-05',
|
|
4
|
+
description: '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.',
|
|
5
|
+
features: [
|
|
6
|
+
'`shop_coupons` dostaje kolumnę `applies_to` (enum `net`/`gross`, default `net`). `calculateCouponDiscountNet()` (`$lib/shop/pricing.ts`) liczy zniżkę od brutto (konwersja do netto przez średnią ważoną VAT) gdy `appliesTo="gross"` — działa zarówno dla `percent`, jak i `fixed`. Storage rabatu w bazie nadal w netto (canonical). Domyślnie `net` — istniejące kupony zachowują obecne zachowanie.',
|
|
7
|
+
'Admin coupon form (`coupon-form.svelte`) zyskuje toggle „Rabat dotyczy: netto / brutto" wzorowany na `inputMode` w `shop-field.svelte` — z helper textem objaśniającym wpływ na cenę zamówienia.',
|
|
8
|
+
'`order.notes` jest teraz traktowane jako opcjonalny JSON (`{ org?, orgName?, participants?: Array<{firstName, lastName}> }`) w admin order detail (`shop-order-detail-page.svelte`) oraz w kontekście emaila (`items.pl.html`/`items.en.html` przez `email.ts`). Plain-text notatki nadal renderują się raw — JSON wykrywany przez `try/catch` parse + shape guard.',
|
|
9
|
+
'Admin pola `date` i `datetime` (`date-field.svelte`, `datetime-field.svelte`) używają `bits-ui` Calendar w `Popover` zamiast natywnych `<input type="date|datetime-local">` — kalendarz z dropdownem miesiąca/roku, dwa Selecty (godzina 00–23, minuta 00/15/30/45) dla datetime, locale `pl-PL`/`en-GB` z `interfaceLanguage`. Output bez zmian: ISO datetime (`datetime`) / `YYYY-MM-DDT00:00:00.000Z` (`date`).'
|
|
10
|
+
],
|
|
11
|
+
fixes: [
|
|
12
|
+
'`generateAPI()` (`src/lib/core/server/generator/generator.ts`) nie wstrzykuje już importu `FormEntryMap` / `createFormSubmission` w wygenerowanym `api.ts`, gdy projekt nie ma zdefiniowanych form (`config.forms` puste). Wcześniej projekty bez form miały martwy import, który psuł type-check / build.'
|
|
13
|
+
],
|
|
14
|
+
breakingChanges: [],
|
|
15
|
+
sql: `ALTER TABLE shop_coupons ADD COLUMN IF NOT EXISTS applies_to text NOT NULL DEFAULT 'net';`
|
|
16
|
+
};
|
package/dist/updates/index.js
CHANGED
|
@@ -65,6 +65,7 @@ import { update as update0270 } from './0.27.0/index.js';
|
|
|
65
65
|
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
|
+
import { update as update0350 } from './0.35.0/index.js';
|
|
68
69
|
export const updates = [
|
|
69
70
|
update0065,
|
|
70
71
|
update0066,
|
|
@@ -132,7 +133,8 @@ export const updates = [
|
|
|
132
133
|
update0270,
|
|
133
134
|
update0280,
|
|
134
135
|
update0340,
|
|
135
|
-
update0341
|
|
136
|
+
update0341,
|
|
137
|
+
update0350
|
|
136
138
|
];
|
|
137
139
|
export const getUpdatesFrom = (fromVersion) => {
|
|
138
140
|
const fromParts = fromVersion.split('.').map(Number);
|
package/package.json
CHANGED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
/* eslint-disable */
|
|
2
|
-
import { getLocale, trackMessageCall, experimentalMiddlewareLocaleSplitting, isServer } from '../runtime.js';
|
|
3
|
-
|
|
4
|
-
const en_hello_world = /** @type {(inputs: { name: NonNullable<unknown> }) => string} */ (i) => {
|
|
5
|
-
return `Hello, ${i.name} from en!`
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
const pl_hello_world = /** @type {(inputs: { name: NonNullable<unknown> }) => string} */ (i) => {
|
|
9
|
-
return `Hello, ${i.name} from pl!`
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* This function has been compiled by [Paraglide JS](https://inlang.com/m/gerre34r).
|
|
14
|
-
*
|
|
15
|
-
* - Changing this function will be over-written by the next build.
|
|
16
|
-
*
|
|
17
|
-
* - If you want to change the translations, you can either edit the source files e.g. `en.json`, or
|
|
18
|
-
* use another inlang app like [Fink](https://inlang.com/m/tdozzpar) or the [VSCode extension Sherlock](https://inlang.com/m/r7kp499g).
|
|
19
|
-
*
|
|
20
|
-
* @param {{ name: NonNullable<unknown> }} inputs
|
|
21
|
-
* @param {{ locale?: "en" | "pl" }} options
|
|
22
|
-
* @returns {string}
|
|
23
|
-
*/
|
|
24
|
-
/* @__NO_SIDE_EFFECTS__ */
|
|
25
|
-
export const hello_world = (inputs, options = {}) => {
|
|
26
|
-
if (experimentalMiddlewareLocaleSplitting && isServer === false) {
|
|
27
|
-
return /** @type {any} */ (globalThis).__paraglide_ssr.hello_world(inputs)
|
|
28
|
-
}
|
|
29
|
-
const locale = options.locale ?? getLocale()
|
|
30
|
-
trackMessageCall("hello_world", locale)
|
|
31
|
-
if (locale === "en") return en_hello_world(inputs)
|
|
32
|
-
return pl_hello_world(inputs)
|
|
33
|
-
};
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
export { login_hello as login.hello };
|
|
2
|
-
/**
|
|
3
|
-
* This function has been compiled by [Paraglide JS](https://inlang.com/m/gerre34r).
|
|
4
|
-
*
|
|
5
|
-
* - Changing this function will be over-written by the next build.
|
|
6
|
-
*
|
|
7
|
-
* - If you want to change the translations, you can either edit the source files e.g. `en.json`, or
|
|
8
|
-
* use another inlang app like [Fink](https://inlang.com/m/tdozzpar) or the [VSCode extension Sherlock](https://inlang.com/m/r7kp499g).
|
|
9
|
-
*
|
|
10
|
-
* @param {{}} inputs
|
|
11
|
-
* @param {{ locale?: "en" | "pl" }} options
|
|
12
|
-
* @returns {string}
|
|
13
|
-
*/
|
|
14
|
-
declare function login_hello(inputs?: {}, options?: {
|
|
15
|
-
locale?: "en" | "pl";
|
|
16
|
-
}): string;
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
/* eslint-disable */
|
|
2
|
-
import { getLocale, trackMessageCall, experimentalMiddlewareLocaleSplitting, isServer } from '../runtime.js';
|
|
3
|
-
|
|
4
|
-
const en_login_hello = /** @type {(inputs: {}) => string} */ () => {
|
|
5
|
-
return `Welcome back`
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
const pl_login_hello = /** @type {(inputs: {}) => string} */ () => {
|
|
9
|
-
return `Witaj ponownie`
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* This function has been compiled by [Paraglide JS](https://inlang.com/m/gerre34r).
|
|
14
|
-
*
|
|
15
|
-
* - Changing this function will be over-written by the next build.
|
|
16
|
-
*
|
|
17
|
-
* - If you want to change the translations, you can either edit the source files e.g. `en.json`, or
|
|
18
|
-
* use another inlang app like [Fink](https://inlang.com/m/tdozzpar) or the [VSCode extension Sherlock](https://inlang.com/m/r7kp499g).
|
|
19
|
-
*
|
|
20
|
-
* @param {{}} inputs
|
|
21
|
-
* @param {{ locale?: "en" | "pl" }} options
|
|
22
|
-
* @returns {string}
|
|
23
|
-
*/
|
|
24
|
-
/* @__NO_SIDE_EFFECTS__ */
|
|
25
|
-
const login_hello = (inputs = {}, options = {}) => {
|
|
26
|
-
if (experimentalMiddlewareLocaleSplitting && isServer === false) {
|
|
27
|
-
return /** @type {any} */ (globalThis).__paraglide_ssr.login_hello(inputs)
|
|
28
|
-
}
|
|
29
|
-
const locale = options.locale ?? getLocale()
|
|
30
|
-
trackMessageCall("login_hello", locale)
|
|
31
|
-
if (locale === "en") return en_login_hello(inputs)
|
|
32
|
-
return pl_login_hello(inputs)
|
|
33
|
-
};
|
|
34
|
-
export { login_hello as "login.hello" }
|