includio-cms 0.15.2 → 0.15.3
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 +25 -0
- package/DOCS.md +137 -2
- package/ROADMAP.md +7 -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 +32 -3
- 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/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/index.js +2 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { scaffoldAdmin } from './scaffold/admin.js';
|
|
3
3
|
import { installPeers } from './install-peers.js';
|
|
4
4
|
import { createUser } from './create-user.js';
|
|
5
|
+
import fs from 'node:fs';
|
|
5
6
|
import path from 'node:path';
|
|
6
7
|
const args = process.argv.slice(2);
|
|
7
8
|
const command = args[0];
|
|
@@ -17,15 +18,59 @@ Commands:
|
|
|
17
18
|
Options:
|
|
18
19
|
--force Overwrite existing files
|
|
19
20
|
--routes-dir Path to routes directory (default: src/routes)
|
|
21
|
+
--cms-config Path to cms config (default: src/lib/cms/cms.config.ts)
|
|
22
|
+
--shop Force shop routes ON (default: auto-detect from cms config)
|
|
23
|
+
--no-shop Force shop routes OFF
|
|
20
24
|
--dry-run Show what would be installed (install-peers)
|
|
21
25
|
`);
|
|
22
26
|
}
|
|
27
|
+
function flagValue(name) {
|
|
28
|
+
const idx = args.indexOf(name);
|
|
29
|
+
return idx !== -1 ? args[idx + 1] : undefined;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Best-effort detection: scan cms config text for an active `shop: defineShop(`
|
|
33
|
+
* or `shop: <var>` property inside `defineCMS({ ... })`. Returns null if the
|
|
34
|
+
* config can't be located so the caller can fall back to a sensible default.
|
|
35
|
+
*/
|
|
36
|
+
function detectShopUsage(cmsConfigPath) {
|
|
37
|
+
if (!fs.existsSync(cmsConfigPath))
|
|
38
|
+
return null;
|
|
39
|
+
const raw = fs.readFileSync(cmsConfigPath, 'utf-8');
|
|
40
|
+
// Strip line + block comments so a commented `// shop:` doesn't trigger.
|
|
41
|
+
const stripped = raw
|
|
42
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
43
|
+
.replace(/\/\/[^\n]*/g, '');
|
|
44
|
+
return /\bshop\s*:\s*\S/.test(stripped);
|
|
45
|
+
}
|
|
23
46
|
if (command === 'scaffold' && subcommand === 'admin') {
|
|
24
47
|
const force = args.includes('--force');
|
|
25
|
-
const
|
|
26
|
-
const routesDir =
|
|
27
|
-
|
|
28
|
-
|
|
48
|
+
const cwd = process.cwd();
|
|
49
|
+
const routesDir = flagValue('--routes-dir') ?? path.join(cwd, 'src', 'routes');
|
|
50
|
+
const cmsConfigPath = flagValue('--cms-config') ?? path.join(cwd, 'src', 'lib', 'cms', 'cms.config.ts');
|
|
51
|
+
let shop;
|
|
52
|
+
let shopSource;
|
|
53
|
+
if (args.includes('--shop')) {
|
|
54
|
+
shop = true;
|
|
55
|
+
shopSource = 'flag --shop';
|
|
56
|
+
}
|
|
57
|
+
else if (args.includes('--no-shop')) {
|
|
58
|
+
shop = false;
|
|
59
|
+
shopSource = 'flag --no-shop';
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
const detected = detectShopUsage(cmsConfigPath);
|
|
63
|
+
if (detected === null) {
|
|
64
|
+
shop = false;
|
|
65
|
+
shopSource = `${cmsConfigPath} not found → off`;
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
shop = detected;
|
|
69
|
+
shopSource = `${cmsConfigPath} → ${detected ? 'shop: detected' : 'no shop config'}`;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
console.log(`Scaffolding admin routes (shop ${shop ? 'enabled' : 'disabled'} — ${shopSource})...\n`);
|
|
73
|
+
scaffoldAdmin({ routesDir, force, shop });
|
|
29
74
|
}
|
|
30
75
|
else if (command === 'install-peers') {
|
|
31
76
|
const dryRun = args.includes('--dry-run');
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
export
|
|
1
|
+
export interface ScaffoldAdminOptions {
|
|
2
2
|
routesDir: string;
|
|
3
3
|
force?: boolean;
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Emit shop module routes (api/shop/* + admin/(afterLogin)/shop/*).
|
|
6
|
+
* Default `true`. Set to `false` for projects that don't use the shop
|
|
7
|
+
* module — keeps the route tree clean and avoids 404 stubs in your app.
|
|
8
|
+
*/
|
|
9
|
+
shop?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare function scaffoldAdmin(options: ScaffoldAdminOptions): void;
|
|
@@ -2,6 +2,10 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
const GENERATED_COMMENT = '<!-- Generated by includio-cms -->';
|
|
4
4
|
const GENERATED_COMMENT_TS = '// Generated by includio-cms';
|
|
5
|
+
function isShopFile(p) {
|
|
6
|
+
return (p.startsWith('api/shop/') ||
|
|
7
|
+
p.startsWith('admin/(afterLogin)/shop/'));
|
|
8
|
+
}
|
|
5
9
|
function getAdminFiles() {
|
|
6
10
|
return [
|
|
7
11
|
{
|
|
@@ -230,6 +234,30 @@ export const { GET } = createOrderHandler();
|
|
|
230
234
|
import { createPaymentWebhookHandler } from 'includio-cms/shop/http';
|
|
231
235
|
|
|
232
236
|
export const { POST } = createPaymentWebhookHandler();
|
|
237
|
+
`
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
path: 'api/shop/carriers/[id]/+server.ts',
|
|
241
|
+
content: `${GENERATED_COMMENT_TS}
|
|
242
|
+
import { createCarrierConfigHandler } from 'includio-cms/shop/http';
|
|
243
|
+
|
|
244
|
+
export const { GET } = createCarrierConfigHandler();
|
|
245
|
+
`
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
path: 'api/shop/carriers/[id]/webhook/+server.ts',
|
|
249
|
+
content: `${GENERATED_COMMENT_TS}
|
|
250
|
+
import { createCarrierWebhookHandler } from 'includio-cms/shop/http';
|
|
251
|
+
|
|
252
|
+
export const { POST } = createCarrierWebhookHandler();
|
|
253
|
+
`
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
path: 'api/shop/admin/orders/[id]/label/+server.ts',
|
|
257
|
+
content: `${GENERATED_COMMENT_TS}
|
|
258
|
+
import { createShipmentLabelHandler } from 'includio-cms/shop/http';
|
|
259
|
+
|
|
260
|
+
export const { GET } = createShipmentLabelHandler();
|
|
233
261
|
`
|
|
234
262
|
},
|
|
235
263
|
{
|
|
@@ -317,8 +345,9 @@ export const { GET, POST, PUT, DELETE } = createRestApiHandler();
|
|
|
317
345
|
];
|
|
318
346
|
}
|
|
319
347
|
export function scaffoldAdmin(options) {
|
|
320
|
-
const { routesDir, force = false } = options;
|
|
321
|
-
const
|
|
348
|
+
const { routesDir, force = false, shop = true } = options;
|
|
349
|
+
const allFiles = getAdminFiles();
|
|
350
|
+
const files = shop ? allFiles : allFiles.filter((f) => !isShopFile(f.path));
|
|
322
351
|
let created = 0;
|
|
323
352
|
let skipped = 0;
|
|
324
353
|
for (const file of files) {
|
|
@@ -334,7 +363,7 @@ export function scaffoldAdmin(options) {
|
|
|
334
363
|
console.log(` create ${file.path}`);
|
|
335
364
|
created++;
|
|
336
365
|
}
|
|
337
|
-
console.log(`\nDone: ${created} created, ${skipped} skipped`);
|
|
366
|
+
console.log(`\nDone: ${created} created, ${skipped} skipped${shop ? '' : ' (shop routes excluded)'}`);
|
|
338
367
|
if (skipped > 0 && !force) {
|
|
339
368
|
console.log('Use --force to overwrite existing files.');
|
|
340
369
|
}
|
|
@@ -279,6 +279,74 @@ export declare const shopOrdersTable: import("drizzle-orm/pg-core/table", { with
|
|
|
279
279
|
identity: undefined;
|
|
280
280
|
generated: undefined;
|
|
281
281
|
}, {}, {}>;
|
|
282
|
+
shipmentId: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
|
|
283
|
+
name: "shipment_id";
|
|
284
|
+
tableName: "shop_orders";
|
|
285
|
+
dataType: "string";
|
|
286
|
+
columnType: "PgText";
|
|
287
|
+
data: string;
|
|
288
|
+
driverParam: string;
|
|
289
|
+
notNull: false;
|
|
290
|
+
hasDefault: false;
|
|
291
|
+
isPrimaryKey: false;
|
|
292
|
+
isAutoincrement: false;
|
|
293
|
+
hasRuntimeDefault: false;
|
|
294
|
+
enumValues: [string, ...string[]];
|
|
295
|
+
baseColumn: never;
|
|
296
|
+
identity: undefined;
|
|
297
|
+
generated: undefined;
|
|
298
|
+
}, {}, {}>;
|
|
299
|
+
trackingNumber: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
|
|
300
|
+
name: "tracking_number";
|
|
301
|
+
tableName: "shop_orders";
|
|
302
|
+
dataType: "string";
|
|
303
|
+
columnType: "PgText";
|
|
304
|
+
data: string;
|
|
305
|
+
driverParam: string;
|
|
306
|
+
notNull: false;
|
|
307
|
+
hasDefault: false;
|
|
308
|
+
isPrimaryKey: false;
|
|
309
|
+
isAutoincrement: false;
|
|
310
|
+
hasRuntimeDefault: false;
|
|
311
|
+
enumValues: [string, ...string[]];
|
|
312
|
+
baseColumn: never;
|
|
313
|
+
identity: undefined;
|
|
314
|
+
generated: undefined;
|
|
315
|
+
}, {}, {}>;
|
|
316
|
+
labelUrl: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
|
|
317
|
+
name: "label_url";
|
|
318
|
+
tableName: "shop_orders";
|
|
319
|
+
dataType: "string";
|
|
320
|
+
columnType: "PgText";
|
|
321
|
+
data: string;
|
|
322
|
+
driverParam: string;
|
|
323
|
+
notNull: false;
|
|
324
|
+
hasDefault: false;
|
|
325
|
+
isPrimaryKey: false;
|
|
326
|
+
isAutoincrement: false;
|
|
327
|
+
hasRuntimeDefault: false;
|
|
328
|
+
enumValues: [string, ...string[]];
|
|
329
|
+
baseColumn: never;
|
|
330
|
+
identity: undefined;
|
|
331
|
+
generated: undefined;
|
|
332
|
+
}, {}, {}>;
|
|
333
|
+
shipmentCreatedAt: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
|
|
334
|
+
name: "shipment_created_at";
|
|
335
|
+
tableName: "shop_orders";
|
|
336
|
+
dataType: "date";
|
|
337
|
+
columnType: "PgTimestamp";
|
|
338
|
+
data: Date;
|
|
339
|
+
driverParam: string;
|
|
340
|
+
notNull: false;
|
|
341
|
+
hasDefault: false;
|
|
342
|
+
isPrimaryKey: false;
|
|
343
|
+
isAutoincrement: false;
|
|
344
|
+
hasRuntimeDefault: false;
|
|
345
|
+
enumValues: undefined;
|
|
346
|
+
baseColumn: never;
|
|
347
|
+
identity: undefined;
|
|
348
|
+
generated: undefined;
|
|
349
|
+
}, {}, {}>;
|
|
282
350
|
paymentMethod: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
|
|
283
351
|
name: "payment_method";
|
|
284
352
|
tableName: "shop_orders";
|
|
@@ -19,6 +19,10 @@ export const shopOrdersTable = pgTable('shop_orders', {
|
|
|
19
19
|
}),
|
|
20
20
|
carrierType: text('carrier_type'),
|
|
21
21
|
carrierRef: text('carrier_ref'),
|
|
22
|
+
shipmentId: text('shipment_id'),
|
|
23
|
+
trackingNumber: text('tracking_number'),
|
|
24
|
+
labelUrl: text('label_url'),
|
|
25
|
+
shipmentCreatedAt: timestamp('shipment_created_at', { withTimezone: true }),
|
|
22
26
|
paymentMethod: text('payment_method'),
|
|
23
27
|
paymentProviderRef: text('payment_provider_ref'),
|
|
24
28
|
consents: jsonb('consents').$type(),
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
export type ShopCarrierType = 'none' | 'inpost' | string;
|
|
2
|
+
export type InpostServiceType = 'inpost_locker_standard' | 'inpost_locker_express' | 'inpost_courier_standard' | 'inpost_courier_express';
|
|
3
|
+
export type InpostParcelSize = 'A' | 'B' | 'C';
|
|
4
|
+
export interface ShippingCarrierConfig {
|
|
5
|
+
serviceType?: InpostServiceType | string;
|
|
6
|
+
defaultSize?: InpostParcelSize | string;
|
|
7
|
+
}
|
|
2
8
|
export declare const shopShippingMethodsTable: import("drizzle-orm/pg-core/table", { with: { "resolution-mode": "require" } }).PgTableWithColumns<{
|
|
3
9
|
name: "shop_shipping_methods";
|
|
4
10
|
schema: undefined;
|
|
@@ -111,6 +117,25 @@ export declare const shopShippingMethodsTable: import("drizzle-orm/pg-core/table
|
|
|
111
117
|
}, {}, {
|
|
112
118
|
$type: string;
|
|
113
119
|
}>;
|
|
120
|
+
carrierConfig: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
|
|
121
|
+
name: "carrier_config";
|
|
122
|
+
tableName: "shop_shipping_methods";
|
|
123
|
+
dataType: "json";
|
|
124
|
+
columnType: "PgJsonb";
|
|
125
|
+
data: ShippingCarrierConfig | null;
|
|
126
|
+
driverParam: unknown;
|
|
127
|
+
notNull: false;
|
|
128
|
+
hasDefault: false;
|
|
129
|
+
isPrimaryKey: false;
|
|
130
|
+
isAutoincrement: false;
|
|
131
|
+
hasRuntimeDefault: false;
|
|
132
|
+
enumValues: undefined;
|
|
133
|
+
baseColumn: never;
|
|
134
|
+
identity: undefined;
|
|
135
|
+
generated: undefined;
|
|
136
|
+
}, {}, {
|
|
137
|
+
$type: ShippingCarrierConfig | null;
|
|
138
|
+
}>;
|
|
114
139
|
conditions: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
|
|
115
140
|
name: "conditions";
|
|
116
141
|
tableName: "shop_shipping_methods";
|
|
@@ -7,6 +7,7 @@ export const shopShippingMethodsTable = pgTable('shop_shipping_methods', {
|
|
|
7
7
|
price: numeric('price', { precision: 20, scale: 6 }).notNull(),
|
|
8
8
|
vatRate: integer('vat_rate').notNull(),
|
|
9
9
|
carrierType: text('carrier_type').$type().default('none').notNull(),
|
|
10
|
+
carrierConfig: jsonb('carrier_config').$type(),
|
|
10
11
|
conditions: jsonb('conditions').$type(),
|
|
11
12
|
allowedPaymentMethods: jsonb('allowed_payment_methods').$type(),
|
|
12
13
|
isActive: boolean('is_active').default(true).notNull(),
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Geowidget v5 (Web Component) config used by `<InpostPicker>` on the frontend.
|
|
3
|
+
* The token is a public read-only "geowidget v5" token from your InPost account
|
|
4
|
+
* (https://manager.paczkomaty.pl). It must be delivered to the browser — do not
|
|
5
|
+
* pass private ShipX tokens here.
|
|
6
|
+
*
|
|
7
|
+
* Docs: https://dokumentacja-inpost.atlassian.net/wiki/spaces/PL/pages/50069505/Geowidget+v5
|
|
8
|
+
*/
|
|
9
|
+
export type GeowidgetEnvironment = 'production' | 'sandbox';
|
|
10
|
+
export type GeowidgetConfigPreset = 'parcelcollect' | 'parcelsend' | 'parcelcollect247' | string;
|
|
11
|
+
export interface GeowidgetConfig {
|
|
12
|
+
token: string;
|
|
13
|
+
language: string;
|
|
14
|
+
config: GeowidgetConfigPreset;
|
|
15
|
+
}
|
|
16
|
+
export interface GeowidgetWidgetDescriptor {
|
|
17
|
+
scriptUrl: string;
|
|
18
|
+
stylesheetUrl: string;
|
|
19
|
+
config: GeowidgetConfig;
|
|
20
|
+
}
|
|
21
|
+
export interface GeowidgetBuildOptions {
|
|
22
|
+
token: string;
|
|
23
|
+
environment?: GeowidgetEnvironment;
|
|
24
|
+
language?: string;
|
|
25
|
+
config?: GeowidgetConfigPreset;
|
|
26
|
+
}
|
|
27
|
+
export declare function buildGeowidgetDescriptor(opts: GeowidgetBuildOptions): GeowidgetWidgetDescriptor;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Geowidget v5 (Web Component) config used by `<InpostPicker>` on the frontend.
|
|
3
|
+
* The token is a public read-only "geowidget v5" token from your InPost account
|
|
4
|
+
* (https://manager.paczkomaty.pl). It must be delivered to the browser — do not
|
|
5
|
+
* pass private ShipX tokens here.
|
|
6
|
+
*
|
|
7
|
+
* Docs: https://dokumentacja-inpost.atlassian.net/wiki/spaces/PL/pages/50069505/Geowidget+v5
|
|
8
|
+
*/
|
|
9
|
+
const GEOWIDGET_URLS = {
|
|
10
|
+
production: {
|
|
11
|
+
script: 'https://geowidget.inpost.pl/inpost-geowidget.js',
|
|
12
|
+
stylesheet: 'https://geowidget.inpost.pl/inpost-geowidget.css'
|
|
13
|
+
},
|
|
14
|
+
sandbox: {
|
|
15
|
+
script: 'https://sandbox-easy-geowidget-sdk.easypack24.net/inpost-geowidget.js',
|
|
16
|
+
stylesheet: 'https://sandbox-easy-geowidget-sdk.easypack24.net/inpost-geowidget.css'
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
export function buildGeowidgetDescriptor(opts) {
|
|
20
|
+
const env = opts.environment ?? 'production';
|
|
21
|
+
const urls = GEOWIDGET_URLS[env];
|
|
22
|
+
return {
|
|
23
|
+
scriptUrl: urls.script,
|
|
24
|
+
stylesheetUrl: urls.stylesheet,
|
|
25
|
+
config: {
|
|
26
|
+
token: opts.token,
|
|
27
|
+
language: opts.language ?? 'pl',
|
|
28
|
+
config: opts.config ?? 'parcelcollect'
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { CarrierAdapter, I18nText } from '../../types.js';
|
|
2
|
+
import { type GeowidgetConfigPreset } from './geowidget.js';
|
|
3
|
+
import { type InpostEnvironment } from './points-api.js';
|
|
4
|
+
export interface InpostSenderAddress {
|
|
5
|
+
name: string;
|
|
6
|
+
company?: string;
|
|
7
|
+
street: string;
|
|
8
|
+
buildingNumber?: string;
|
|
9
|
+
city: string;
|
|
10
|
+
postCode: string;
|
|
11
|
+
countryCode?: string;
|
|
12
|
+
email: string;
|
|
13
|
+
phone: string;
|
|
14
|
+
}
|
|
15
|
+
export interface InpostAdapterOptions {
|
|
16
|
+
/**
|
|
17
|
+
* Public Geowidget v5 token (read-only). Delivered to the browser via
|
|
18
|
+
* `GET /api/shop/carriers/inpost`.
|
|
19
|
+
*/
|
|
20
|
+
geowidgetToken: string;
|
|
21
|
+
/**
|
|
22
|
+
* Private ShipX organization-scoped token (`Bearer`). Used server-side for
|
|
23
|
+
* shipment creation, label fetching, cancellation. Optional in geowidget-only
|
|
24
|
+
* setups (Etap 2). Required for ShipX shipment operations (Etap 3).
|
|
25
|
+
*/
|
|
26
|
+
shipxToken?: string;
|
|
27
|
+
/** ShipX organization ID, e.g. `123456`. Required when shipxToken is set. */
|
|
28
|
+
organizationId?: string;
|
|
29
|
+
/** `production` (default) or `sandbox`. Controls all ShipX endpoints. */
|
|
30
|
+
environment?: InpostEnvironment;
|
|
31
|
+
/**
|
|
32
|
+
* Shared secret used to authenticate webhook calls. Pass it as `?secret=`
|
|
33
|
+
* query in the `notifyUrl` you configure in the ShipX panel.
|
|
34
|
+
*/
|
|
35
|
+
webhookSecret?: string;
|
|
36
|
+
/** Absolute URL ShipX should POST shipment events to. */
|
|
37
|
+
notifyUrl?: string;
|
|
38
|
+
/** Sender pickup address used for ShipX shipment creation. */
|
|
39
|
+
senderAddress?: InpostSenderAddress;
|
|
40
|
+
/**
|
|
41
|
+
* Tracking link template. `{trackingNumber}` is replaced. Default:
|
|
42
|
+
* `https://inpost.pl/sledzenie-przesylek?number={trackingNumber}`.
|
|
43
|
+
*/
|
|
44
|
+
trackingUrlTemplate?: string;
|
|
45
|
+
/** Default geowidget preset. Default: `parcelCollect`. */
|
|
46
|
+
geowidgetPreset?: GeowidgetConfigPreset;
|
|
47
|
+
/** Default geowidget language. Default: `pl`. */
|
|
48
|
+
geowidgetLanguage?: string;
|
|
49
|
+
/** Cache TTL for Points API validation (ms). Default 5 min. */
|
|
50
|
+
pointsCacheTtlMs?: number;
|
|
51
|
+
/** Default ShipX label format (PDF). */
|
|
52
|
+
labelFormat?: 'Pdf' | 'ZebraLP';
|
|
53
|
+
/** Default ShipX label paper size. */
|
|
54
|
+
labelSize?: 'A4' | 'A6';
|
|
55
|
+
/** Additional ShipX services attached to every shipment (e.g. ['email', 'sms']). */
|
|
56
|
+
additionalServices?: string[];
|
|
57
|
+
/**
|
|
58
|
+
* After POST /shipments, automatically call POST /shipments/:id/buy to
|
|
59
|
+
* confirm the auto-selected offer. Required for accounts (esp. sandbox)
|
|
60
|
+
* where ShipX leaves shipments in `offer_selected` instead of paying
|
|
61
|
+
* automatically. Default `true`. Set `false` if your org is configured
|
|
62
|
+
* for full auto-pay or you handle confirmation yourself.
|
|
63
|
+
*/
|
|
64
|
+
autoConfirm?: boolean;
|
|
65
|
+
/**
|
|
66
|
+
* Delay between POST /shipments and POST /shipments/:id/buy (ms). ShipX
|
|
67
|
+
* needs a brief moment to prepare the offer. Default 1500ms.
|
|
68
|
+
*/
|
|
69
|
+
autoConfirmDelayMs?: number;
|
|
70
|
+
/**
|
|
71
|
+
* After POST /shipments/:id/buy, poll GET /shipments/:id every second
|
|
72
|
+
* until the status leaves `offer_selected` (becomes `confirmed` or further).
|
|
73
|
+
* ShipX buying is asynchronous — production is fast (~1s), sandbox can
|
|
74
|
+
* take minutes. Default 8000ms. Set 0 to disable polling and rely on
|
|
75
|
+
* the webhook to reconcile.
|
|
76
|
+
*/
|
|
77
|
+
autoConfirmPollTimeoutMs?: number;
|
|
78
|
+
/**
|
|
79
|
+
* Verbose adapter logging — payload dumps, every poll tick, offer details.
|
|
80
|
+
* Off by default. Turn on when troubleshooting sandbox stalls or unexpected
|
|
81
|
+
* ShipX responses. Errors and the post-timeout stall warning always log.
|
|
82
|
+
*/
|
|
83
|
+
debug?: boolean;
|
|
84
|
+
fetch?: typeof fetch;
|
|
85
|
+
id?: string;
|
|
86
|
+
label?: I18nText;
|
|
87
|
+
}
|
|
88
|
+
export declare function inpostAdapter(opts: InpostAdapterOptions): CarrierAdapter;
|
|
89
|
+
export type { GeowidgetConfigPreset, InpostEnvironment };
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { buildGeowidgetDescriptor } from './geowidget.js';
|
|
2
|
+
import { buildShipmentPayload } from './payload.js';
|
|
3
|
+
import { createPointsValidator } from './points-api.js';
|
|
4
|
+
import { ShipxClient } from './shipx-client.js';
|
|
5
|
+
import { parseShipxWebhook } from './webhook.js';
|
|
6
|
+
const DEFAULT_LABEL = { pl: 'InPost', en: 'InPost' };
|
|
7
|
+
const DEFAULT_TRACKING_TEMPLATE = 'https://inpost.pl/sledzenie-przesylek?number={trackingNumber}';
|
|
8
|
+
export function inpostAdapter(opts) {
|
|
9
|
+
if (!opts.geowidgetToken) {
|
|
10
|
+
throw new Error('inpostAdapter: `geowidgetToken` is required.');
|
|
11
|
+
}
|
|
12
|
+
const id = opts.id ?? 'inpost';
|
|
13
|
+
const env = opts.environment ?? 'production';
|
|
14
|
+
const fetchFn = opts.fetch ?? globalThis.fetch.bind(globalThis);
|
|
15
|
+
const points = createPointsValidator({
|
|
16
|
+
environment: env,
|
|
17
|
+
fetch: fetchFn,
|
|
18
|
+
cacheTtlMs: opts.pointsCacheTtlMs
|
|
19
|
+
});
|
|
20
|
+
const widgetDesc = buildGeowidgetDescriptor({
|
|
21
|
+
token: opts.geowidgetToken,
|
|
22
|
+
environment: env,
|
|
23
|
+
language: opts.geowidgetLanguage,
|
|
24
|
+
config: opts.geowidgetPreset
|
|
25
|
+
});
|
|
26
|
+
const trackingTemplate = opts.trackingUrlTemplate ?? DEFAULT_TRACKING_TEMPLATE;
|
|
27
|
+
function requireShipx() {
|
|
28
|
+
if (!opts.shipxToken) {
|
|
29
|
+
throw new Error('inpostAdapter: `shipxToken` is required for ShipX shipment operations.');
|
|
30
|
+
}
|
|
31
|
+
if (!opts.organizationId) {
|
|
32
|
+
throw new Error('inpostAdapter: `organizationId` is required for ShipX shipment operations.');
|
|
33
|
+
}
|
|
34
|
+
return new ShipxClient({
|
|
35
|
+
token: opts.shipxToken,
|
|
36
|
+
organizationId: opts.organizationId,
|
|
37
|
+
environment: env,
|
|
38
|
+
fetch: fetchFn
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
id,
|
|
43
|
+
label: opts.label ?? DEFAULT_LABEL,
|
|
44
|
+
widget: {
|
|
45
|
+
scriptUrl: widgetDesc.scriptUrl,
|
|
46
|
+
stylesheetUrl: widgetDesc.stylesheetUrl,
|
|
47
|
+
config: { ...widgetDesc.config }
|
|
48
|
+
},
|
|
49
|
+
async validateSelection(ref, ctx) {
|
|
50
|
+
const serviceType = ctx?.serviceType;
|
|
51
|
+
// Courier service ships to a street address — no parcel locker code expected.
|
|
52
|
+
if (!serviceType || serviceType.startsWith('inpost_courier_'))
|
|
53
|
+
return true;
|
|
54
|
+
if (!ref)
|
|
55
|
+
return false;
|
|
56
|
+
return points.validate(ref);
|
|
57
|
+
},
|
|
58
|
+
async createShipment(input) {
|
|
59
|
+
const client = requireShipx();
|
|
60
|
+
const debug = opts.debug === true;
|
|
61
|
+
const log = (msg) => {
|
|
62
|
+
if (debug)
|
|
63
|
+
console.log(msg);
|
|
64
|
+
};
|
|
65
|
+
const payload = buildShipmentPayload({
|
|
66
|
+
input,
|
|
67
|
+
sender: opts.senderAddress,
|
|
68
|
+
additionalServices: opts.additionalServices
|
|
69
|
+
});
|
|
70
|
+
log(`[inpost] createShipment: order=${input.order.number} service=${input.serviceType} carrierRef=${input.carrierRef ?? '—'}`);
|
|
71
|
+
if (debug)
|
|
72
|
+
console.log('[inpost] payload →', JSON.stringify(payload, null, 2));
|
|
73
|
+
let shipment = await client.createShipment(payload);
|
|
74
|
+
log(`[inpost] POST /shipments → id=${shipment.id} status=${shipment.status} tracking=${shipment.tracking_number ?? 'null'}`);
|
|
75
|
+
if (opts.autoConfirm !== false) {
|
|
76
|
+
const delay = opts.autoConfirmDelayMs ?? 1500;
|
|
77
|
+
if (delay > 0) {
|
|
78
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
const fresh = await client.getShipment(shipment.id);
|
|
82
|
+
log(`[inpost] shipment now: status=${fresh.status} selected_offer=${fresh.selected_offer?.id ?? 'null'} offers=${fresh.offers?.length ?? 0}`);
|
|
83
|
+
const offerId = fresh.selected_offer?.id ??
|
|
84
|
+
(Array.isArray(fresh.offers) && fresh.offers.length > 0
|
|
85
|
+
? fresh.offers[0].id
|
|
86
|
+
: undefined);
|
|
87
|
+
if (offerId == null) {
|
|
88
|
+
throw new Error(`Shipment ${shipment.id} has no selected_offer / offers yet (status=${fresh.status})`);
|
|
89
|
+
}
|
|
90
|
+
log(`[inpost] POST /shipments/${shipment.id}/buy {offer_id: ${offerId}}`);
|
|
91
|
+
const buyResult = await client.buyShipment(shipment.id, offerId);
|
|
92
|
+
// Polling: ShipX buy is asynchronous. Wait until we leave the
|
|
93
|
+
// offer_selected/created state, or give up and let the webhook reconcile.
|
|
94
|
+
const pollTimeout = opts.autoConfirmPollTimeoutMs ?? 8000;
|
|
95
|
+
const pollIntervalMs = 1000;
|
|
96
|
+
const deadline = Date.now() + pollTimeout;
|
|
97
|
+
shipment = buyResult;
|
|
98
|
+
while (pollTimeout > 0 &&
|
|
99
|
+
Date.now() < deadline &&
|
|
100
|
+
(shipment.status === 'offer_selected' || shipment.status === 'created')) {
|
|
101
|
+
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
102
|
+
shipment = await client.getShipment(shipment.id);
|
|
103
|
+
log(`[inpost] poll → status=${shipment.status} tracking=${shipment.tracking_number ?? 'null'}`);
|
|
104
|
+
}
|
|
105
|
+
log(`[inpost] final shipment: status=${shipment.status} tracking=${shipment.tracking_number ?? 'null'}`);
|
|
106
|
+
if (shipment.status === 'offer_selected' || shipment.status === 'created') {
|
|
107
|
+
console.warn(`[inpost] shipment ${shipment.id} still in '${shipment.status}' after ${pollTimeout}ms — sandbox often stalls here without a wallet/payment configured. Webhook will reconcile if/when ShipX completes the purchase.`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
// Auto-confirm is best-effort. If the offer isn't ready, the
|
|
112
|
+
// account is configured for full auto-pay, or the offer was
|
|
113
|
+
// already bought, the webhook will reconcile the final status.
|
|
114
|
+
console.warn(`[inpost] auto-confirm failed for shipment ${shipment.id} — webhook will reconcile.`);
|
|
115
|
+
if (err instanceof Error) {
|
|
116
|
+
console.warn(`[inpost] ${err.name}: ${err.message}`);
|
|
117
|
+
const body = err.body;
|
|
118
|
+
if (body !== undefined) {
|
|
119
|
+
console.warn('[inpost] error body:', typeof body === 'string' ? body : JSON.stringify(body));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
console.warn('[inpost] auto-confirm error:', err);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
shipmentId: String(shipment.id),
|
|
129
|
+
trackingNumber: shipment.tracking_number ?? '',
|
|
130
|
+
raw: shipment
|
|
131
|
+
};
|
|
132
|
+
},
|
|
133
|
+
async getShipmentLabel(shipmentId, labelOpts) {
|
|
134
|
+
const client = requireShipx();
|
|
135
|
+
const { contentType, body } = await client.getLabel(shipmentId, {
|
|
136
|
+
format: opts.labelFormat ?? 'Pdf',
|
|
137
|
+
type: labelOpts?.size ?? opts.labelSize ?? 'A6'
|
|
138
|
+
});
|
|
139
|
+
return {
|
|
140
|
+
contentType,
|
|
141
|
+
body,
|
|
142
|
+
filename: `inpost-${shipmentId}.pdf`
|
|
143
|
+
};
|
|
144
|
+
},
|
|
145
|
+
async cancelShipment(shipmentId) {
|
|
146
|
+
const client = requireShipx();
|
|
147
|
+
await client.cancelShipment(shipmentId);
|
|
148
|
+
},
|
|
149
|
+
async handleWebhook(req) {
|
|
150
|
+
return parseShipxWebhook(req, { secret: opts.webhookSecret });
|
|
151
|
+
},
|
|
152
|
+
trackingUrl(trackingNumber) {
|
|
153
|
+
return trackingTemplate.replace('{trackingNumber}', encodeURIComponent(trackingNumber));
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ShipmentCreateInput } from '../../types.js';
|
|
2
|
+
import type { InpostSenderAddress } from './index.js';
|
|
3
|
+
export declare function mapParcelSize(size: string | undefined): string;
|
|
4
|
+
export declare function splitFullName(full: string | null | undefined): {
|
|
5
|
+
firstName?: string;
|
|
6
|
+
lastName?: string;
|
|
7
|
+
};
|
|
8
|
+
export interface BuildShipmentPayloadOptions {
|
|
9
|
+
input: ShipmentCreateInput;
|
|
10
|
+
sender?: InpostSenderAddress;
|
|
11
|
+
additionalServices?: string[];
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Build a ShipX `POST /shipments` body for a given order. Locker services attach
|
|
15
|
+
* `custom_attributes.target_point` (paczkomat code from `carrierRef`); courier
|
|
16
|
+
* services use the receiver address from the order's shipping_address instead.
|
|
17
|
+
*/
|
|
18
|
+
export declare function buildShipmentPayload(opts: BuildShipmentPayloadOptions): Record<string, unknown>;
|