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.
Files changed (62) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/DOCS.md +137 -2
  3. package/ROADMAP.md +7 -2
  4. package/dist/admin/client/shop/shipping-method-form.svelte +66 -1
  5. package/dist/admin/client/shop/shipping-method-form.svelte.d.ts +8 -0
  6. package/dist/admin/client/shop/shop-order-detail-page.svelte +101 -0
  7. package/dist/admin/remote/shop.remote.d.ts +44 -0
  8. package/dist/admin/remote/shop.remote.js +35 -0
  9. package/dist/cli/index.js +49 -4
  10. package/dist/cli/scaffold/admin.d.ts +9 -2
  11. package/dist/cli/scaffold/admin.js +32 -3
  12. package/dist/db-postgres/schema/shop/order.d.ts +68 -0
  13. package/dist/db-postgres/schema/shop/order.js +4 -0
  14. package/dist/db-postgres/schema/shop/shippingMethod.d.ts +25 -0
  15. package/dist/db-postgres/schema/shop/shippingMethod.js +1 -0
  16. package/dist/shop/adapters/inpost/geowidget.d.ts +27 -0
  17. package/dist/shop/adapters/inpost/geowidget.js +31 -0
  18. package/dist/shop/adapters/inpost/index.d.ts +89 -0
  19. package/dist/shop/adapters/inpost/index.js +156 -0
  20. package/dist/shop/adapters/inpost/payload.d.ts +18 -0
  21. package/dist/shop/adapters/inpost/payload.js +85 -0
  22. package/dist/shop/adapters/inpost/points-api.d.ts +17 -0
  23. package/dist/shop/adapters/inpost/points-api.js +55 -0
  24. package/dist/shop/adapters/inpost/shipx-client.d.ts +56 -0
  25. package/dist/shop/adapters/inpost/shipx-client.js +95 -0
  26. package/dist/shop/adapters/inpost/status-map.d.ts +9 -0
  27. package/dist/shop/adapters/inpost/status-map.js +46 -0
  28. package/dist/shop/adapters/inpost/webhook.d.ts +16 -0
  29. package/dist/shop/adapters/inpost/webhook.js +55 -0
  30. package/dist/shop/client/index.d.ts +5 -0
  31. package/dist/shop/http/carrier-handler.d.ts +12 -0
  32. package/dist/shop/http/carrier-handler.js +45 -0
  33. package/dist/shop/http/carrier-webhook-handler.d.ts +13 -0
  34. package/dist/shop/http/carrier-webhook-handler.js +66 -0
  35. package/dist/shop/http/checkout-handler.js +23 -1
  36. package/dist/shop/http/index.d.ts +3 -0
  37. package/dist/shop/http/index.js +3 -0
  38. package/dist/shop/http/order-handler.js +14 -0
  39. package/dist/shop/http/shipment-label-handler.d.ts +10 -0
  40. package/dist/shop/http/shipment-label-handler.js +53 -0
  41. package/dist/shop/http/shipping-handler.js +3 -0
  42. package/dist/shop/index.d.ts +3 -1
  43. package/dist/shop/index.js +1 -0
  44. package/dist/shop/server/email.js +37 -0
  45. package/dist/shop/server/orders.d.ts +9 -0
  46. package/dist/shop/server/orders.js +48 -0
  47. package/dist/shop/server/shipments.d.ts +33 -0
  48. package/dist/shop/server/shipments.js +145 -0
  49. package/dist/shop/server/shipping.d.ts +2 -1
  50. package/dist/shop/server/shipping.js +9 -0
  51. package/dist/shop/svelte/InpostPicker.svelte +270 -0
  52. package/dist/shop/svelte/InpostPicker.svelte.d.ts +51 -0
  53. package/dist/shop/svelte/OrderStatus.svelte +53 -1
  54. package/dist/shop/svelte/index.d.ts +1 -0
  55. package/dist/shop/svelte/index.js +1 -0
  56. package/dist/shop/svelte/labels.d.ts +5 -0
  57. package/dist/shop/svelte/labels.js +6 -1
  58. package/dist/shop/types.d.ts +49 -1
  59. package/dist/updates/0.15.3/index.d.ts +2 -0
  60. package/dist/updates/0.15.3/index.js +19 -0
  61. package/dist/updates/index.js +2 -1
  62. 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 routesDirIdx = args.indexOf('--routes-dir');
26
- const routesDir = routesDirIdx !== -1 ? args[routesDirIdx + 1] : path.join(process.cwd(), 'src', 'routes');
27
- console.log('Scaffolding admin routes...\n');
28
- scaffoldAdmin({ routesDir, force });
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 declare function scaffoldAdmin(options: {
1
+ export interface ScaffoldAdminOptions {
2
2
  routesDir: string;
3
3
  force?: boolean;
4
- }): void;
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 files = getAdminFiles();
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>;