includio-cms 0.15.2 → 0.15.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +40 -0
- package/DOCS.md +142 -3
- package/ROADMAP.md +13 -2
- package/dist/admin/client/shop/shipping-method-form.svelte +66 -1
- package/dist/admin/client/shop/shipping-method-form.svelte.d.ts +8 -0
- package/dist/admin/client/shop/shop-order-detail-page.svelte +101 -0
- package/dist/admin/remote/shop.remote.d.ts +44 -0
- package/dist/admin/remote/shop.remote.js +35 -0
- package/dist/cli/index.js +49 -4
- package/dist/cli/scaffold/admin.d.ts +9 -2
- package/dist/cli/scaffold/admin.js +91 -3
- package/dist/core/server/forms/submissions/operations/create.js +11 -5
- package/dist/db-postgres/schema/shop/order.d.ts +68 -0
- package/dist/db-postgres/schema/shop/order.js +4 -0
- package/dist/db-postgres/schema/shop/shippingMethod.d.ts +25 -0
- package/dist/db-postgres/schema/shop/shippingMethod.js +1 -0
- package/dist/paraglide/messages/_index.d.ts +3 -36
- package/dist/paraglide/messages/_index.js +3 -71
- package/dist/paraglide/messages/hello_world.d.ts +5 -0
- package/dist/paraglide/messages/hello_world.js +33 -0
- package/dist/paraglide/messages/login_hello.d.ts +16 -0
- package/dist/paraglide/messages/login_hello.js +34 -0
- package/dist/paraglide/messages/login_please_login.d.ts +16 -0
- package/dist/paraglide/messages/login_please_login.js +34 -0
- package/dist/shop/adapters/inpost/geowidget.d.ts +27 -0
- package/dist/shop/adapters/inpost/geowidget.js +31 -0
- package/dist/shop/adapters/inpost/index.d.ts +89 -0
- package/dist/shop/adapters/inpost/index.js +156 -0
- package/dist/shop/adapters/inpost/payload.d.ts +18 -0
- package/dist/shop/adapters/inpost/payload.js +85 -0
- package/dist/shop/adapters/inpost/points-api.d.ts +17 -0
- package/dist/shop/adapters/inpost/points-api.js +55 -0
- package/dist/shop/adapters/inpost/shipx-client.d.ts +56 -0
- package/dist/shop/adapters/inpost/shipx-client.js +95 -0
- package/dist/shop/adapters/inpost/status-map.d.ts +9 -0
- package/dist/shop/adapters/inpost/status-map.js +46 -0
- package/dist/shop/adapters/inpost/webhook.d.ts +16 -0
- package/dist/shop/adapters/inpost/webhook.js +55 -0
- package/dist/shop/client/index.d.ts +5 -0
- package/dist/shop/http/carrier-handler.d.ts +12 -0
- package/dist/shop/http/carrier-handler.js +45 -0
- package/dist/shop/http/carrier-webhook-handler.d.ts +13 -0
- package/dist/shop/http/carrier-webhook-handler.js +66 -0
- package/dist/shop/http/checkout-handler.js +23 -1
- package/dist/shop/http/index.d.ts +3 -0
- package/dist/shop/http/index.js +3 -0
- package/dist/shop/http/order-handler.js +14 -0
- package/dist/shop/http/shipment-label-handler.d.ts +10 -0
- package/dist/shop/http/shipment-label-handler.js +53 -0
- package/dist/shop/http/shipping-handler.js +3 -0
- package/dist/shop/index.d.ts +3 -1
- package/dist/shop/index.js +1 -0
- package/dist/shop/server/email.js +37 -0
- package/dist/shop/server/orders.d.ts +9 -0
- package/dist/shop/server/orders.js +48 -0
- package/dist/shop/server/shipments.d.ts +33 -0
- package/dist/shop/server/shipments.js +145 -0
- package/dist/shop/server/shipping.d.ts +2 -1
- package/dist/shop/server/shipping.js +9 -0
- package/dist/shop/svelte/InpostPicker.svelte +270 -0
- package/dist/shop/svelte/InpostPicker.svelte.d.ts +51 -0
- package/dist/shop/svelte/OrderStatus.svelte +53 -1
- package/dist/shop/svelte/index.d.ts +1 -0
- package/dist/shop/svelte/index.js +1 -0
- package/dist/shop/svelte/labels.d.ts +5 -0
- package/dist/shop/svelte/labels.js +6 -1
- package/dist/shop/types.d.ts +49 -1
- package/dist/updates/0.15.3/index.d.ts +2 -0
- package/dist/updates/0.15.3/index.js +19 -0
- package/dist/updates/0.15.4/index.d.ts +2 -0
- package/dist/updates/0.15.4/index.js +14 -0
- package/dist/updates/index.js +3 -1
- package/package.json +1 -1
- package/dist/paraglide/messages/en.d.ts +0 -5
- package/dist/paraglide/messages/en.js +0 -14
- package/dist/paraglide/messages/pl.d.ts +0 -5
- package/dist/paraglide/messages/pl.js +0 -14
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { CarrierEvent } from '../../types.js';
|
|
2
|
+
export declare class WebhookSecretMismatchError extends Error {
|
|
3
|
+
constructor();
|
|
4
|
+
}
|
|
5
|
+
export interface ParseWebhookOptions {
|
|
6
|
+
/** Expected secret. When provided, the URL must carry `?secret=...` matching it. */
|
|
7
|
+
secret?: string;
|
|
8
|
+
/** Override query parameter name (default `secret`). */
|
|
9
|
+
secretParam?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Parse a ShipX webhook request into a `CarrierEvent`. When `secret` is set the
|
|
13
|
+
* URL is checked first — mismatch throws `WebhookSecretMismatchError` so the
|
|
14
|
+
* caller can return 401 without parsing the body.
|
|
15
|
+
*/
|
|
16
|
+
export declare function parseShipxWebhook(req: Request, opts?: ParseWebhookOptions): Promise<CarrierEvent>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { mapShipxStatus } from './status-map.js';
|
|
2
|
+
/** Constant-time string compare, returns false on length mismatch. */
|
|
3
|
+
function safeEqual(a, b) {
|
|
4
|
+
if (a.length !== b.length)
|
|
5
|
+
return false;
|
|
6
|
+
let mismatch = 0;
|
|
7
|
+
for (let i = 0; i < a.length; i++) {
|
|
8
|
+
mismatch |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
9
|
+
}
|
|
10
|
+
return mismatch === 0;
|
|
11
|
+
}
|
|
12
|
+
export class WebhookSecretMismatchError extends Error {
|
|
13
|
+
constructor() {
|
|
14
|
+
super('Webhook secret mismatch.');
|
|
15
|
+
this.name = 'WebhookSecretMismatchError';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Parse a ShipX webhook request into a `CarrierEvent`. When `secret` is set the
|
|
20
|
+
* URL is checked first — mismatch throws `WebhookSecretMismatchError` so the
|
|
21
|
+
* caller can return 401 without parsing the body.
|
|
22
|
+
*/
|
|
23
|
+
export async function parseShipxWebhook(req, opts = {}) {
|
|
24
|
+
if (opts.secret) {
|
|
25
|
+
const url = new URL(req.url);
|
|
26
|
+
const provided = url.searchParams.get(opts.secretParam ?? 'secret') ?? '';
|
|
27
|
+
if (!safeEqual(provided, opts.secret)) {
|
|
28
|
+
throw new WebhookSecretMismatchError();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const text = await req.text();
|
|
32
|
+
let body;
|
|
33
|
+
try {
|
|
34
|
+
body = JSON.parse(text);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
throw new Error('Webhook body is not valid JSON.');
|
|
38
|
+
}
|
|
39
|
+
const payload = body.payload ?? {
|
|
40
|
+
shipment_id: body.shipment_id,
|
|
41
|
+
status: body.status,
|
|
42
|
+
tracking_number: body.tracking_number ?? null
|
|
43
|
+
};
|
|
44
|
+
const shipmentId = payload.shipment_id;
|
|
45
|
+
const status = payload.status ?? '';
|
|
46
|
+
if (shipmentId === undefined || shipmentId === null) {
|
|
47
|
+
throw new Error('Webhook payload missing shipment_id.');
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
shipmentId: String(shipmentId),
|
|
51
|
+
trackingNumber: payload.tracking_number ?? undefined,
|
|
52
|
+
status: mapShipxStatus(status),
|
|
53
|
+
raw: body
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -12,6 +12,9 @@ export interface ShippingMethodPublic {
|
|
|
12
12
|
price: number;
|
|
13
13
|
vatRate: number;
|
|
14
14
|
carrierType: string;
|
|
15
|
+
carrierConfig: {
|
|
16
|
+
serviceType: string | null;
|
|
17
|
+
} | null;
|
|
15
18
|
conditions: {
|
|
16
19
|
freeAbove?: number;
|
|
17
20
|
} | null;
|
|
@@ -61,6 +64,8 @@ export interface OrderDetailResponse {
|
|
|
61
64
|
paymentMethod: string | null;
|
|
62
65
|
carrierType: string | null;
|
|
63
66
|
carrierRef: string | null;
|
|
67
|
+
trackingNumber: string | null;
|
|
68
|
+
trackingUrl: string | null;
|
|
64
69
|
language: string | null;
|
|
65
70
|
createdAt: string;
|
|
66
71
|
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type RequestHandler } from '@sveltejs/kit';
|
|
2
|
+
/**
|
|
3
|
+
* Public endpoint that returns front-end facing carrier widget descriptor for
|
|
4
|
+
* a given carrier id (e.g. `inpost`). The descriptor never contains private
|
|
5
|
+
* credentials — only what the browser needs to render the picker (e.g. public
|
|
6
|
+
* geowidget token + script URL).
|
|
7
|
+
*
|
|
8
|
+
* Mounted at `/api/shop/carriers/[id]`.
|
|
9
|
+
*/
|
|
10
|
+
export declare function createCarrierConfigHandler(): {
|
|
11
|
+
GET: RequestHandler;
|
|
12
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { json } from '@sveltejs/kit';
|
|
2
|
+
import { getCMS } from '../../core/cms.js';
|
|
3
|
+
function shopEnabled() {
|
|
4
|
+
try {
|
|
5
|
+
return getCMS().shopConfig !== null;
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Public endpoint that returns front-end facing carrier widget descriptor for
|
|
13
|
+
* a given carrier id (e.g. `inpost`). The descriptor never contains private
|
|
14
|
+
* credentials — only what the browser needs to render the picker (e.g. public
|
|
15
|
+
* geowidget token + script URL).
|
|
16
|
+
*
|
|
17
|
+
* Mounted at `/api/shop/carriers/[id]`.
|
|
18
|
+
*/
|
|
19
|
+
export function createCarrierConfigHandler() {
|
|
20
|
+
return {
|
|
21
|
+
GET: async ({ params }) => {
|
|
22
|
+
if (!shopEnabled())
|
|
23
|
+
return json({ error: 'Shop not enabled' }, { status: 404 });
|
|
24
|
+
const id = params.id;
|
|
25
|
+
if (!id)
|
|
26
|
+
return json({ error: 'Carrier id required' }, { status: 400 });
|
|
27
|
+
const shop = getCMS().shopConfig;
|
|
28
|
+
const carrier = shop?.carriers.find((c) => c.id === id);
|
|
29
|
+
if (!carrier)
|
|
30
|
+
return json({ error: 'Carrier not found' }, { status: 404 });
|
|
31
|
+
if (!carrier.widget) {
|
|
32
|
+
return json({ id: carrier.id, label: carrier.label ?? null, widget: null });
|
|
33
|
+
}
|
|
34
|
+
return json({
|
|
35
|
+
id: carrier.id,
|
|
36
|
+
label: carrier.label ?? null,
|
|
37
|
+
widget: {
|
|
38
|
+
scriptUrl: carrier.widget.scriptUrl ?? null,
|
|
39
|
+
stylesheetUrl: carrier.widget.stylesheetUrl ?? null,
|
|
40
|
+
config: carrier.widget.config
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type RequestHandler } from '@sveltejs/kit';
|
|
2
|
+
/**
|
|
3
|
+
* Public webhook receiver for carrier events (e.g. ShipX `shipment_status_changed`).
|
|
4
|
+
* Carrier id (`inpost`, ...) is taken from the route param, looked up in
|
|
5
|
+
* `shop.carriers`, and the adapter's `handleWebhook` parses + verifies the
|
|
6
|
+
* request. Successful events are applied to the matching order (tracking +
|
|
7
|
+
* status). Unknown shipments still return 200 to avoid retry storms.
|
|
8
|
+
*
|
|
9
|
+
* Mounted at `POST /api/shop/carriers/[id]/webhook`.
|
|
10
|
+
*/
|
|
11
|
+
export declare function createCarrierWebhookHandler(): {
|
|
12
|
+
POST: RequestHandler;
|
|
13
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { json } from '@sveltejs/kit';
|
|
2
|
+
import { getCMS } from '../../core/cms.js';
|
|
3
|
+
import { checkRateLimit, clientKey } from '../rate-limit.js';
|
|
4
|
+
import { applyCarrierEvent } from '../server/shipments.js';
|
|
5
|
+
function shopEnabled() {
|
|
6
|
+
try {
|
|
7
|
+
return getCMS().shopConfig !== null;
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Public webhook receiver for carrier events (e.g. ShipX `shipment_status_changed`).
|
|
15
|
+
* Carrier id (`inpost`, ...) is taken from the route param, looked up in
|
|
16
|
+
* `shop.carriers`, and the adapter's `handleWebhook` parses + verifies the
|
|
17
|
+
* request. Successful events are applied to the matching order (tracking +
|
|
18
|
+
* status). Unknown shipments still return 200 to avoid retry storms.
|
|
19
|
+
*
|
|
20
|
+
* Mounted at `POST /api/shop/carriers/[id]/webhook`.
|
|
21
|
+
*/
|
|
22
|
+
export function createCarrierWebhookHandler() {
|
|
23
|
+
return {
|
|
24
|
+
POST: async ({ request, params }) => {
|
|
25
|
+
if (!shopEnabled())
|
|
26
|
+
return json({ error: 'Shop not enabled' }, { status: 404 });
|
|
27
|
+
const id = params.id;
|
|
28
|
+
if (!id)
|
|
29
|
+
return json({ error: 'Carrier id required' }, { status: 400 });
|
|
30
|
+
const shop = getCMS().shopConfig;
|
|
31
|
+
const adapter = shop?.carriers.find((c) => c.id === id);
|
|
32
|
+
if (!adapter)
|
|
33
|
+
return json({ error: 'Carrier not found' }, { status: 404 });
|
|
34
|
+
if (!adapter.handleWebhook) {
|
|
35
|
+
return json({ error: 'Carrier does not support webhooks' }, { status: 400 });
|
|
36
|
+
}
|
|
37
|
+
const rule = shop?.rateLimit.webhook ?? { limit: 60, windowSec: 60 };
|
|
38
|
+
const rl = checkRateLimit(clientKey(request, `webhook:${id}`), rule);
|
|
39
|
+
if (!rl.allowed) {
|
|
40
|
+
return json({ error: 'Rate limit exceeded' }, { status: 429 });
|
|
41
|
+
}
|
|
42
|
+
let event;
|
|
43
|
+
try {
|
|
44
|
+
event = await adapter.handleWebhook(request);
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
const name = err instanceof Error ? err.name : '';
|
|
48
|
+
if (name === 'WebhookSecretMismatchError') {
|
|
49
|
+
return json({ error: 'Unauthorized' }, { status: 401 });
|
|
50
|
+
}
|
|
51
|
+
const message = err instanceof Error ? err.message : 'Webhook parse error';
|
|
52
|
+
console.error(`[shop/carrier:${id}] webhook parse failed:`, err);
|
|
53
|
+
return json({ error: message }, { status: 400 });
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
const result = await applyCarrierEvent(event);
|
|
57
|
+
return json({ ok: true, ...result });
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
console.error(`[shop/carrier:${id}] applyCarrierEvent failed:`, err);
|
|
61
|
+
const message = err instanceof Error ? err.message : 'Apply event failed';
|
|
62
|
+
return json({ error: message }, { status: 500 });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -3,6 +3,7 @@ import { getCMS } from '../../core/cms.js';
|
|
|
3
3
|
import { readCartCookie, writeCartCookie } from '../cart/cookie.js';
|
|
4
4
|
import { writeOrderTokenCookie } from '../cart/order-token-cookie.js';
|
|
5
5
|
import { createOrderFromCart, setPaymentProviderRef } from '../server/orders.js';
|
|
6
|
+
import { getShippingMethod } from '../server/shipping.js';
|
|
6
7
|
import { checkRateLimit, clientKey } from '../rate-limit.js';
|
|
7
8
|
import { requireShopConfig } from '../server/db.js';
|
|
8
9
|
function shopEnabled() {
|
|
@@ -67,6 +68,27 @@ export function createCheckoutHandler() {
|
|
|
67
68
|
if (cartItems.length === 0) {
|
|
68
69
|
return json({ error: 'Cart is empty' }, { status: 400 });
|
|
69
70
|
}
|
|
71
|
+
const carrierRef = asString(body.carrierRef, 200);
|
|
72
|
+
// Carrier-side validation (e.g. paczkomat code → InPost Points API)
|
|
73
|
+
try {
|
|
74
|
+
const shippingMethod = await getShippingMethod(shippingMethodId);
|
|
75
|
+
if (shippingMethod && shippingMethod.carrierType !== 'none') {
|
|
76
|
+
const shop = requireShopConfig();
|
|
77
|
+
const carrier = shop.carriers.find((c) => c.id === shippingMethod.carrierType);
|
|
78
|
+
if (carrier?.validateSelection) {
|
|
79
|
+
const ok = await carrier.validateSelection(carrierRef ?? '', {
|
|
80
|
+
serviceType: shippingMethod.carrierConfig?.serviceType
|
|
81
|
+
});
|
|
82
|
+
if (!ok) {
|
|
83
|
+
return json({ error: 'Invalid carrier selection (e.g. paczkomat unavailable).' }, { status: 400 });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
console.error('[shop] carrier validateSelection failed:', err);
|
|
90
|
+
return json({ error: 'Carrier validation failed.' }, { status: 400 });
|
|
91
|
+
}
|
|
70
92
|
try {
|
|
71
93
|
const result = await createOrderFromCart({
|
|
72
94
|
cartItems,
|
|
@@ -75,7 +97,7 @@ export function createCheckoutHandler() {
|
|
|
75
97
|
customerPhone: asString(body.customerPhone, 40),
|
|
76
98
|
shippingAddress: asStringRecord(body.shippingAddress),
|
|
77
99
|
shippingMethodId,
|
|
78
|
-
carrierRef
|
|
100
|
+
carrierRef,
|
|
79
101
|
paymentMethod,
|
|
80
102
|
consents: asConsents(body.consents),
|
|
81
103
|
notes: asString(body.notes, 2000),
|
|
@@ -5,3 +5,6 @@ export { createOrderHandler } from './order-handler.js';
|
|
|
5
5
|
export { createPaymentWebhookHandler } from './webhook-handler.js';
|
|
6
6
|
export { createRefreshPaymentHandler } from './refresh-payment-handler.js';
|
|
7
7
|
export { createRetryPaymentHandler } from './retry-payment-handler.js';
|
|
8
|
+
export { createCarrierConfigHandler } from './carrier-handler.js';
|
|
9
|
+
export { createCarrierWebhookHandler } from './carrier-webhook-handler.js';
|
|
10
|
+
export { createShipmentLabelHandler } from './shipment-label-handler.js';
|
package/dist/shop/http/index.js
CHANGED
|
@@ -5,3 +5,6 @@ export { createOrderHandler } from './order-handler.js';
|
|
|
5
5
|
export { createPaymentWebhookHandler } from './webhook-handler.js';
|
|
6
6
|
export { createRefreshPaymentHandler } from './refresh-payment-handler.js';
|
|
7
7
|
export { createRetryPaymentHandler } from './retry-payment-handler.js';
|
|
8
|
+
export { createCarrierConfigHandler } from './carrier-handler.js';
|
|
9
|
+
export { createCarrierWebhookHandler } from './carrier-webhook-handler.js';
|
|
10
|
+
export { createShipmentLabelHandler } from './shipment-label-handler.js';
|
|
@@ -43,6 +43,18 @@ export function createOrderHandler() {
|
|
|
43
43
|
getOrderItems(order.id),
|
|
44
44
|
getOrderStatusHistory(order.id)
|
|
45
45
|
]);
|
|
46
|
+
let trackingUrl = null;
|
|
47
|
+
if (order.carrierType && order.trackingNumber) {
|
|
48
|
+
const adapter = getCMS().shopConfig?.carriers.find((c) => c.id === order.carrierType);
|
|
49
|
+
if (adapter?.trackingUrl) {
|
|
50
|
+
try {
|
|
51
|
+
trackingUrl = adapter.trackingUrl(order.trackingNumber);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
trackingUrl = null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
46
58
|
return json({
|
|
47
59
|
order: {
|
|
48
60
|
id: order.id,
|
|
@@ -61,6 +73,8 @@ export function createOrderHandler() {
|
|
|
61
73
|
paymentMethod: order.paymentMethod,
|
|
62
74
|
carrierType: order.carrierType,
|
|
63
75
|
carrierRef: order.carrierRef,
|
|
76
|
+
trackingNumber: order.trackingNumber,
|
|
77
|
+
trackingUrl,
|
|
64
78
|
language: order.language,
|
|
65
79
|
createdAt: order.createdAt
|
|
66
80
|
},
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type RequestHandler } from '@sveltejs/kit';
|
|
2
|
+
/**
|
|
3
|
+
* Admin-only PDF proxy that streams the carrier label without exposing the
|
|
4
|
+
* carrier's signed link or token to the browser.
|
|
5
|
+
*
|
|
6
|
+
* Mounted at `/api/shop/admin/orders/[id]/label`.
|
|
7
|
+
*/
|
|
8
|
+
export declare function createShipmentLabelHandler(): {
|
|
9
|
+
GET: RequestHandler;
|
|
10
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { error } from '@sveltejs/kit';
|
|
2
|
+
import { getCMS } from '../../core/cms.js';
|
|
3
|
+
import { requireAuth } from '../../admin/remote/middleware/auth.js';
|
|
4
|
+
import { getShipmentLabelForOrder } from '../server/shipments.js';
|
|
5
|
+
function shopEnabled() {
|
|
6
|
+
try {
|
|
7
|
+
return getCMS().shopConfig !== null;
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Admin-only PDF proxy that streams the carrier label without exposing the
|
|
15
|
+
* carrier's signed link or token to the browser.
|
|
16
|
+
*
|
|
17
|
+
* Mounted at `/api/shop/admin/orders/[id]/label`.
|
|
18
|
+
*/
|
|
19
|
+
export function createShipmentLabelHandler() {
|
|
20
|
+
return {
|
|
21
|
+
GET: async ({ params, url }) => {
|
|
22
|
+
if (!shopEnabled())
|
|
23
|
+
error(404, 'Shop not enabled');
|
|
24
|
+
try {
|
|
25
|
+
requireAuth();
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
error(401, 'Unauthorized');
|
|
29
|
+
}
|
|
30
|
+
const orderId = params.id;
|
|
31
|
+
if (!orderId)
|
|
32
|
+
error(400, 'Order id required');
|
|
33
|
+
const sizeParam = url.searchParams.get('size');
|
|
34
|
+
const size = sizeParam === 'A4' || sizeParam === 'A6' ? sizeParam : undefined;
|
|
35
|
+
let label;
|
|
36
|
+
try {
|
|
37
|
+
label = await getShipmentLabelForOrder(orderId, { size });
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
const message = err instanceof Error ? err.message : 'Label fetch failed';
|
|
41
|
+
error(400, message);
|
|
42
|
+
}
|
|
43
|
+
return new Response(new Uint8Array(label.body), {
|
|
44
|
+
status: 200,
|
|
45
|
+
headers: {
|
|
46
|
+
'Content-Type': label.contentType,
|
|
47
|
+
'Content-Disposition': `inline; filename="${label.filename ?? 'label.pdf'}"`,
|
|
48
|
+
'Cache-Control': 'no-store'
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -23,6 +23,9 @@ export function createShippingMethodsHandler() {
|
|
|
23
23
|
price: m.price,
|
|
24
24
|
vatRate: m.vatRate,
|
|
25
25
|
carrierType: m.carrierType,
|
|
26
|
+
carrierConfig: m.carrierConfig
|
|
27
|
+
? { serviceType: m.carrierConfig.serviceType ?? null }
|
|
28
|
+
: null,
|
|
26
29
|
conditions: m.conditions,
|
|
27
30
|
allowedPaymentMethods: m.allowedPaymentMethods ?? null
|
|
28
31
|
}))
|
package/dist/shop/index.d.ts
CHANGED
|
@@ -3,4 +3,6 @@ export declare function defineShop(config: ShopConfig): ResolvedShopConfig;
|
|
|
3
3
|
export { manualAdapter } from './adapters/manual/index.js';
|
|
4
4
|
export { payuAdapter } from './adapters/payu/index.js';
|
|
5
5
|
export type { PayuAdapterOptions } from './adapters/payu/index.js';
|
|
6
|
-
export
|
|
6
|
+
export { inpostAdapter } from './adapters/inpost/index.js';
|
|
7
|
+
export type { InpostAdapterOptions, InpostSenderAddress, GeowidgetConfigPreset, InpostEnvironment } from './adapters/inpost/index.js';
|
|
8
|
+
export type { ShopConfig, ResolvedShopConfig, Currency, OrderStatus, PaymentAdapter, PaymentCreateContext, CarrierAdapter, CarrierEvent, ShipmentCreateInput, ShipmentCreateResult, ShipmentLabel, ConsentConfig, ShopFeatures, PaymentCreateResult, PaymentEvent, OrderRef, I18nText } from './types.js';
|
package/dist/shop/index.js
CHANGED
|
@@ -3,6 +3,10 @@ import { resolveI18n } from '../pricing.js';
|
|
|
3
3
|
import { getOrderById, getOrderItems } from './orders.js';
|
|
4
4
|
import { requireShopConfig } from './db.js';
|
|
5
5
|
import { buildOrderViewUrl } from './order-access-url.js';
|
|
6
|
+
const TRACKING_LABEL = {
|
|
7
|
+
pl: { label: 'Numer śledzenia', linkLabel: 'Sprawdź status przesyłki ↗' },
|
|
8
|
+
en: { label: 'Tracking number', linkLabel: 'Track your shipment ↗' }
|
|
9
|
+
};
|
|
6
10
|
function formatPrice(cents, currency) {
|
|
7
11
|
return new Intl.NumberFormat('pl-PL', {
|
|
8
12
|
style: 'currency',
|
|
@@ -77,6 +81,15 @@ function renderHtml(ctx, intro) {
|
|
|
77
81
|
<div style="font-size:16px;">Razem (brutto): <strong style="color:#5B4A9E;">${ctx.order.totalGross}</strong></div>
|
|
78
82
|
<div style="color:#8888A0;font-size:12px;">netto ${ctx.order.totalNet} · VAT ${ctx.order.vatAmount}</div>
|
|
79
83
|
</div>
|
|
84
|
+
${ctx.tracking
|
|
85
|
+
? `<div style="margin-top:20px;padding:16px;background:#F4F2FA;border-radius:10px;font-size:14px;">
|
|
86
|
+
<div style="color:#555566;margin-bottom:4px;">${escapeHtml(ctx.tracking.label)}</div>
|
|
87
|
+
<div style="font-family:ui-monospace,monospace;font-weight:700;word-break:break-all;">${escapeHtml(ctx.tracking.number)}</div>
|
|
88
|
+
${ctx.tracking.url
|
|
89
|
+
? `<a href="${escapeHtml(ctx.tracking.url)}" style="display:inline-block;margin-top:8px;color:#5B4A9E;font-weight:600;text-decoration:none;">${escapeHtml(ctx.tracking.linkLabel)}</a>`
|
|
90
|
+
: ''}
|
|
91
|
+
</div>`
|
|
92
|
+
: ''}
|
|
80
93
|
${ctx.viewUrl
|
|
81
94
|
? `<div style="margin-top:24px;text-align:center;"><a href="${escapeHtml(ctx.viewUrl)}" style="display:inline-block;background:#5B4A9E;color:#fff;text-decoration:none;padding:10px 18px;border-radius:8px;font-weight:600;">${escapeHtml(ctx.viewLinkLabel)}</a></div>`
|
|
82
95
|
: ''}
|
|
@@ -115,9 +128,33 @@ export async function sendOrderStatusEmail(orderId, status) {
|
|
|
115
128
|
language: order.language
|
|
116
129
|
})
|
|
117
130
|
: null;
|
|
131
|
+
let tracking;
|
|
132
|
+
if (order.trackingNumber &&
|
|
133
|
+
(status === 'sent' || status === 'done' || status === 'preparing')) {
|
|
134
|
+
const carrier = order.carrierType
|
|
135
|
+
? shop.carriers.find((c) => c.id === order.carrierType)
|
|
136
|
+
: undefined;
|
|
137
|
+
const url = carrier?.trackingUrl
|
|
138
|
+
? (() => {
|
|
139
|
+
try {
|
|
140
|
+
return carrier.trackingUrl(order.trackingNumber);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
})()
|
|
146
|
+
: null;
|
|
147
|
+
tracking = {
|
|
148
|
+
number: order.trackingNumber,
|
|
149
|
+
url,
|
|
150
|
+
label: TRACKING_LABEL[subjectKey].label,
|
|
151
|
+
linkLabel: TRACKING_LABEL[subjectKey].linkLabel
|
|
152
|
+
};
|
|
153
|
+
}
|
|
118
154
|
const ctx = {
|
|
119
155
|
viewUrl,
|
|
120
156
|
viewLinkLabel: VIEW_LINK_LABEL[subjectKey],
|
|
157
|
+
tracking,
|
|
121
158
|
order: {
|
|
122
159
|
number: order.number,
|
|
123
160
|
status: order.status,
|
|
@@ -33,6 +33,15 @@ export declare function updateOrderStatus(orderId: string, status: OrderStatus,
|
|
|
33
33
|
changedBy?: string;
|
|
34
34
|
}): Promise<OrderRow>;
|
|
35
35
|
export declare function setPaymentProviderRef(orderId: string, ref: string | null): Promise<void>;
|
|
36
|
+
export interface ShipmentInfoInput {
|
|
37
|
+
shipmentId: string;
|
|
38
|
+
trackingNumber?: string | null;
|
|
39
|
+
labelUrl?: string | null;
|
|
40
|
+
}
|
|
41
|
+
export declare function setShipmentInfo(orderId: string, info: ShipmentInfoInput): Promise<OrderRow>;
|
|
42
|
+
export declare function updateTrackingNumber(orderId: string, trackingNumber: string): Promise<void>;
|
|
43
|
+
export declare function clearShipmentInfo(orderId: string): Promise<void>;
|
|
44
|
+
export declare function getOrderByShipmentId(shipmentId: string): Promise<OrderRow | null>;
|
|
36
45
|
export declare function getOrderById(id: string): Promise<OrderRow | null>;
|
|
37
46
|
export declare function getOrderByNumber(number: string): Promise<OrderRow | null>;
|
|
38
47
|
export declare function getOrderItems(orderId: string): Promise<OrderItemRow[]>;
|
|
@@ -256,6 +256,54 @@ export async function setPaymentProviderRef(orderId, ref) {
|
|
|
256
256
|
.set({ paymentProviderRef: ref, updatedAt: new Date() })
|
|
257
257
|
.where(eq(shopOrdersTable.id, orderId));
|
|
258
258
|
}
|
|
259
|
+
export async function setShipmentInfo(orderId, info) {
|
|
260
|
+
const db = getShopDb();
|
|
261
|
+
await db
|
|
262
|
+
.update(shopOrdersTable)
|
|
263
|
+
.set({
|
|
264
|
+
shipmentId: info.shipmentId,
|
|
265
|
+
trackingNumber: info.trackingNumber ?? null,
|
|
266
|
+
labelUrl: info.labelUrl ?? null,
|
|
267
|
+
shipmentCreatedAt: new Date(),
|
|
268
|
+
updatedAt: new Date()
|
|
269
|
+
})
|
|
270
|
+
.where(eq(shopOrdersTable.id, orderId));
|
|
271
|
+
const [row] = await db
|
|
272
|
+
.select()
|
|
273
|
+
.from(shopOrdersTable)
|
|
274
|
+
.where(eq(shopOrdersTable.id, orderId));
|
|
275
|
+
if (!row)
|
|
276
|
+
throw new Error('Order not found after shipment update.');
|
|
277
|
+
return row;
|
|
278
|
+
}
|
|
279
|
+
export async function updateTrackingNumber(orderId, trackingNumber) {
|
|
280
|
+
const db = getShopDb();
|
|
281
|
+
await db
|
|
282
|
+
.update(shopOrdersTable)
|
|
283
|
+
.set({ trackingNumber, updatedAt: new Date() })
|
|
284
|
+
.where(eq(shopOrdersTable.id, orderId));
|
|
285
|
+
}
|
|
286
|
+
export async function clearShipmentInfo(orderId) {
|
|
287
|
+
const db = getShopDb();
|
|
288
|
+
await db
|
|
289
|
+
.update(shopOrdersTable)
|
|
290
|
+
.set({
|
|
291
|
+
shipmentId: null,
|
|
292
|
+
trackingNumber: null,
|
|
293
|
+
labelUrl: null,
|
|
294
|
+
shipmentCreatedAt: null,
|
|
295
|
+
updatedAt: new Date()
|
|
296
|
+
})
|
|
297
|
+
.where(eq(shopOrdersTable.id, orderId));
|
|
298
|
+
}
|
|
299
|
+
export async function getOrderByShipmentId(shipmentId) {
|
|
300
|
+
const db = getShopDb();
|
|
301
|
+
const [row] = await db
|
|
302
|
+
.select()
|
|
303
|
+
.from(shopOrdersTable)
|
|
304
|
+
.where(eq(shopOrdersTable.shipmentId, shipmentId));
|
|
305
|
+
return row ?? null;
|
|
306
|
+
}
|
|
259
307
|
export async function getOrderById(id) {
|
|
260
308
|
const db = getShopDb();
|
|
261
309
|
const [row] = await db.select().from(shopOrdersTable).where(eq(shopOrdersTable.id, id));
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { CarrierEvent, ShipmentLabel } from '../types.js';
|
|
2
|
+
import { type OrderRow } from './orders.js';
|
|
3
|
+
/**
|
|
4
|
+
* Create a shipment for the order with the carrier configured on the order.
|
|
5
|
+
* Saves shipmentId/trackingNumber on the order and bumps status to `preparing`.
|
|
6
|
+
* Idempotent guard: throws if a shipment already exists (use `cancelShipmentForOrder` first).
|
|
7
|
+
*/
|
|
8
|
+
export declare function createShipmentForOrder(orderId: string): Promise<OrderRow>;
|
|
9
|
+
/**
|
|
10
|
+
* Cancel an existing shipment with the carrier and clear shipment data on the order.
|
|
11
|
+
* Order status is left untouched (admin decides whether to drop back to `paid`).
|
|
12
|
+
*/
|
|
13
|
+
export declare function cancelShipmentForOrder(orderId: string): Promise<OrderRow>;
|
|
14
|
+
/**
|
|
15
|
+
* Fetch the shipping label PDF (or other format) from the carrier. Used by the
|
|
16
|
+
* admin label proxy endpoint.
|
|
17
|
+
*/
|
|
18
|
+
export declare function getShipmentLabelForOrder(orderId: string, opts?: {
|
|
19
|
+
size?: 'A4' | 'A6';
|
|
20
|
+
}): Promise<ShipmentLabel>;
|
|
21
|
+
export interface ApplyCarrierEventResult {
|
|
22
|
+
matched: boolean;
|
|
23
|
+
orderId?: string;
|
|
24
|
+
statusChanged?: boolean;
|
|
25
|
+
trackingChanged?: boolean;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Apply an incoming carrier event (e.g. from a webhook) to the matching order:
|
|
29
|
+
* update the tracking number when supplied and bump the order status when the
|
|
30
|
+
* mapped status differs from the current one. Unknown carrier statuses are
|
|
31
|
+
* skipped — caller still gets `matched=true` so the webhook can return 200.
|
|
32
|
+
*/
|
|
33
|
+
export declare function applyCarrierEvent(event: CarrierEvent): Promise<ApplyCarrierEventResult>;
|