universal-parcel-scraper 0.82.0-main.949 → 0.83.0-main.951

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/README.md CHANGED
@@ -13,7 +13,7 @@ The engine behind [Peek](https://peektracker.com) ([GitHub](https://github.com/p
13
13
  [![License: Apache-2.0](https://img.shields.io/badge/core-Apache--2.0-blue.svg)](LICENSE)
14
14
 
15
15
  <!-- GENERATED:summary -->
16
- **3,500+ carriers** through **140 dedicated adapters** and **5 universal fallbacks**
16
+ **3,500+ carriers** through **141 dedicated adapters** and **5 universal fallbacks**
17
17
 
18
18
  <sub>207 catalog entries · 66 countries represented</sub>
19
19
  <!-- /GENERATED:summary -->
@@ -155,7 +155,7 @@ createTracker({ providers: ['ParcelsApp', 'Ship24', '17TRACK', 'Postal Ninja', '
155
155
  <!-- GENERATED:stages -->
156
156
  <img src="docs/assets/stages.svg" alt="DHL: Die Sendung wurde in das Zustellfahrzeug geladen.; Mondial Relay: En cours de livraison; Correios Brazil: Objeto saiu para entrega ao destinatário; Correos Express: EN REPARTO; Yamato Transport: 配達中; La Poste / Colissimo: DISTOU. All are filed under out_for_delivery." width="760">
157
157
 
158
- The carrier folders record 3,936 such statuses, each filed under one stage.
158
+ The carrier folders record 3,952 such statuses, each filed under one stage.
159
159
  <!-- /GENERATED:stages -->
160
160
 
161
161
  Wording nobody recorded yet goes through a classifier that reads seven European languages.
package/data/catalog.json CHANGED
@@ -8105,7 +8105,8 @@
8105
8105
  "timezone": "Europe/Paris",
8106
8106
  "tracking": {
8107
8107
  "mode": "automatic",
8108
- "adapter": "universal"
8108
+ "adapter": "vinted-go",
8109
+ "recognitionRank": 72
8109
8110
  },
8110
8111
  "canaryUrl": "https://vintedgo.com/en/tracking",
8111
8112
  "trackingUrlTemplate": "https://vintedgo.com/en/tracking/{trackingNumber}",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "catalog": 207,
3
- "activeAdapters": 140,
3
+ "activeAdapters": 141,
4
4
  "countries": 66,
5
5
  "referenceCarriers": 100,
6
6
  "comparison": {
@@ -7748,6 +7748,7 @@
7748
7748
  "vinted-go"
7749
7749
  ],
7750
7750
  "asked": [
7751
+ "vinted-go",
7751
7752
  "tnt",
7752
7753
  "dhl-ecommerce"
7753
7754
  ]
@@ -7927,6 +7928,7 @@
7927
7928
  "vinted-go"
7928
7929
  ],
7929
7930
  "asked": [
7931
+ "vinted-go",
7930
7932
  "tnt",
7931
7933
  "dhl-ecommerce"
7932
7934
  ]
@@ -7942,6 +7944,7 @@
7942
7944
  "vinted-go"
7943
7945
  ],
7944
7946
  "asked": [
7947
+ "vinted-go",
7945
7948
  "tnt",
7946
7949
  "dhl-ecommerce"
7947
7950
  ]
@@ -7957,6 +7960,7 @@
7957
7960
  "vinted-go"
7958
7961
  ],
7959
7962
  "asked": [
7963
+ "vinted-go",
7960
7964
  "tnt",
7961
7965
  "dhl-ecommerce"
7962
7966
  ]
@@ -7972,6 +7976,7 @@
7972
7976
  "vinted-go"
7973
7977
  ],
7974
7978
  "asked": [
7979
+ "vinted-go",
7975
7980
  "tnt",
7976
7981
  "dhl-ecommerce"
7977
7982
  ]
@@ -7987,6 +7992,7 @@
7987
7992
  "vinted-go"
7988
7993
  ],
7989
7994
  "asked": [
7995
+ "vinted-go",
7990
7996
  "tnt",
7991
7997
  "dhl-ecommerce"
7992
7998
  ]
@@ -6398,7 +6398,8 @@ var ye = {
6398
6398
  timezone: "Europe/Paris",
6399
6399
  tracking: {
6400
6400
  mode: "automatic",
6401
- adapter: "universal"
6401
+ adapter: "vinted-go",
6402
+ recognitionRank: 72
6402
6403
  },
6403
6404
  canaryUrl: "https://vintedgo.com/en/tracking",
6404
6405
  trackingUrlTemplate: "https://vintedgo.com/en/tracking/{trackingNumber}",
@@ -11810,6 +11811,7 @@ var _c = {
11810
11811
  ukrposhta: 5,
11811
11812
  uniuni: 33,
11812
11813
  venipak: 63,
11814
+ "vinted-go": 72,
11813
11815
  "yurtici-kargo": 66
11814
11816
  }, vc = 5;
11815
11817
  function yc(e) {
@@ -0,0 +1,15 @@
1
+ import { type AdapterFactory, type TrackingContext } from '../../core/adapter/index.js';
2
+ import type { CarrierResult } from '../../core/result/index.js';
3
+ import { type StepRecorder } from '../../core/telemetry/index.js';
4
+ export declare const VINTED_GO_ENDPOINT = "https://carrier.vintedgo.com/public/v3/tracking_events/";
5
+ export declare function vintedGoTrackingUrl(raw: string): string;
6
+ export declare class VintedGoTracker {
7
+ private readonly options;
8
+ constructor(options?: {
9
+ fetcher?: typeof fetch;
10
+ recorder?: StepRecorder;
11
+ userAgent?: string;
12
+ });
13
+ fetch(raw: string, context?: TrackingContext): Promise<CarrierResult>;
14
+ }
15
+ export declare const adapter: AdapterFactory;
@@ -0,0 +1,62 @@
1
+ import { accepted, recognizeFromLookup } from '../../core/adapter/index.js';
2
+ import { ChallengeError, IndeterminateError, SchemaError } from '../../core/errors/index.js';
3
+ import { runSteps } from '../../core/runner/index.js';
4
+ import { firstAttemptMs, networkRetryStep } from '../../core/runner/networkRetry.js';
5
+ import { NOOP_RECORDER } from '../../core/telemetry/index.js';
6
+ import { decodeText, fetchBounded, userAgentOf } from '../../core/transport/index.js';
7
+ import { normalizeVintedGoNumber, parseVintedGo, parseVintedGoMissing, PROVIDER, VINTED_GO_MAX_BYTES } from './parser.js';
8
+ // The service the tracker's search box asks before it opens a parcel's page.
9
+ export const VINTED_GO_ENDPOINT = 'https://carrier.vintedgo.com/public/v3/tracking_events/';
10
+ const REDIRECTS = [301, 302, 303, 307, 308];
11
+ // Cloudflare's challenge page, which can come instead of the reply.
12
+ const CHALLENGE = /_cf_chl_opt|cf-turnstile|<title>Just a moment/i;
13
+ export function vintedGoTrackingUrl(raw) {
14
+ return `${VINTED_GO_ENDPOINT}${normalizeVintedGoNumber(raw)}`;
15
+ }
16
+ export class VintedGoTracker {
17
+ options;
18
+ constructor(options = {}) {
19
+ this.options = options;
20
+ }
21
+ async fetch(raw, context = {}) {
22
+ const url = vintedGoTrackingUrl(raw);
23
+ const read = async (signal, timeoutMs) => {
24
+ // The scans are worded in the language asked for; English is the one the map and the place rules read.
25
+ const { response, bytes } = await fetchBounded(url, { signal, headers: {
26
+ Accept: 'application/json', 'Accept-Language': 'en', 'User-Agent': userAgentOf(this.options.userAgent),
27
+ } }, { provider: PROVIDER, timeoutMs: Math.max(1, Math.floor(timeoutMs)), maxBytes: VINTED_GO_MAX_BYTES, redirect: 'manual',
28
+ allowHttpStatuses: [...REDIRECTS, 404], fetcher: this.options.fetcher });
29
+ const text = decodeText(bytes);
30
+ if (response.status === 404)
31
+ return parseVintedGoMissing(text, response.headers.get('content-type'));
32
+ // A moved service says nothing about the parcel.
33
+ if (response.status !== 200)
34
+ throw new IndeterminateError(PROVIDER, 'Vinted Go tracking service moved');
35
+ let reply;
36
+ try {
37
+ reply = JSON.parse(text);
38
+ }
39
+ catch (error) {
40
+ if (CHALLENGE.test(text))
41
+ throw new ChallengeError(PROVIDER, 'Vinted Go answered with a browser challenge', { cause: error });
42
+ throw new SchemaError(PROVIDER, 'Vinted Go returned a reply that is not JSON', { cause: error });
43
+ }
44
+ return parseVintedGo(reply, raw);
45
+ };
46
+ // A request that fails to reach Vinted Go, or hangs, gets one more try: the
47
+ // first gets half of the budget, so the retry fits in the rest.
48
+ return runSteps({ carrier: 'vinted-go', budgetMs: context.budgetMs ?? 15_000, signal: context.signal,
49
+ recorder: this.options.recorder ?? NOOP_RECORDER }, [
50
+ { id: 'direct', run: ({ signal, remainingMs }) => read(signal, firstAttemptMs(remainingMs)) },
51
+ networkRetryStep(({ signal, remainingMs }) => read(signal, remainingMs)),
52
+ ]);
53
+ }
54
+ }
55
+ export const adapter = (environment) => {
56
+ const tracker = new VintedGoTracker({ fetcher: environment.fetcher, recorder: environment.recorder, userAgent: environment.userAgent });
57
+ return {
58
+ id: 'vinted-go', recordsSteps: true, steps: ['direct', 'retry'],
59
+ track: (input, context) => tracker.fetch(input.number, context),
60
+ recognize: (number, context) => recognizeFromLookup(() => tracker.fetch(number, context), () => accepted(() => normalizeVintedGoNumber(number))),
61
+ };
62
+ };
@@ -21,18 +21,65 @@
21
21
  "timezone": "Europe/Paris",
22
22
  "tracking": {
23
23
  "mode": "automatic",
24
- "adapter": "universal",
25
- "steps": []
24
+ "adapter": "vinted-go",
25
+ "steps": [
26
+ "direct",
27
+ "retry"
28
+ ],
29
+ "recognition": {
30
+ "rank": 72
31
+ }
26
32
  },
27
- "capabilities": [],
33
+ "capabilities": [
34
+ "history",
35
+ "location",
36
+ "delivered_at",
37
+ "provider_code",
38
+ "pickup_point",
39
+ "delivery_partner",
40
+ "delivery_tracking_number"
41
+ ],
28
42
  "portal": {
29
43
  "url": "https://vintedgo.com/en/tracking/{trackingNumber}",
30
44
  "searchUrl": "https://vintedgo.com/en/tracking",
31
45
  "canaryUrl": "https://vintedgo.com/en/tracking",
32
- "shows": [],
33
- "scraped": [],
34
- "discarded": [],
35
- "unavailable": []
46
+ "shows": [
47
+ "status",
48
+ "history",
49
+ "location",
50
+ "delivered_at",
51
+ "pickup_point",
52
+ "pickup_point_address",
53
+ "office_opening_hours",
54
+ "pickup_deadline",
55
+ "delivery_partner",
56
+ "delivery_tracking_number",
57
+ "notices"
58
+ ],
59
+ "scraped": [
60
+ "status",
61
+ "history",
62
+ "location",
63
+ "delivered_at",
64
+ "provider_code",
65
+ "offset_timestamps",
66
+ "pickup_point",
67
+ "delivery_partner",
68
+ "delivery_tracking_number"
69
+ ],
70
+ "discarded": [
71
+ "pickup_point_address",
72
+ "office_opening_hours",
73
+ "pickup_deadline",
74
+ "notices"
75
+ ],
76
+ "unavailable": [
77
+ "eta",
78
+ "weight",
79
+ "service_name",
80
+ "sender_name",
81
+ "recipient_name"
82
+ ]
36
83
  },
37
84
  "links": [
38
85
  {
@@ -112,7 +112,7 @@
112
112
  "access": "indexed_excerpt"
113
113
  },
114
114
  "context": {
115
- "assessment": "candidate"
115
+ "assessment": "confirmed"
116
116
  },
117
117
  "expect": {
118
118
  "carrier": "unknown",
@@ -137,7 +137,7 @@
137
137
  "access": "indexed_excerpt"
138
138
  },
139
139
  "context": {
140
- "assessment": "candidate"
140
+ "assessment": "confirmed"
141
141
  },
142
142
  "expect": {
143
143
  "carrier": "unknown",
@@ -0,0 +1,19 @@
1
+ import type { CarrierResult } from '../../core/result/index.js';
2
+ export declare const PROVIDER = "Vinted Go";
3
+ export declare const VINTED_GO_MAX_BYTES: number;
4
+ /** A Vinted Go parcel number; anything else is a number Vinted Go does not issue. */
5
+ export declare function normalizeVintedGoNumber(raw: string): string;
6
+ /**
7
+ * The reply of `GET /public/v3/tracking_events/{number}`. It does not echo the
8
+ * number: the scans belong to the one in the request's path. The pickup
9
+ * point's address, its id, the pickup deadline, banners and questions in the
10
+ * same reply are never read.
11
+ */
12
+ export declare function parseVintedGo(payload: unknown, raw: string): CarrierResult;
13
+ /**
14
+ * A 404 from the service. An unknown number gets an empty body in the type
15
+ * asked for; a number whose label is over six months old gets
16
+ * `shipment_expired`, whether the parcel existed or not. A missing route
17
+ * answers "Not found" in plain text and says nothing about the parcel.
18
+ */
19
+ export declare function parseVintedGoMissing(body: string, contentType: string | null): never;
@@ -0,0 +1,187 @@
1
+ import { carrierIdFromPartner } from '../../core/catalog/hints.js';
2
+ import { normalizeTrackingNumber } from '../../core/detection/index.js';
3
+ import { hasLuhnCheckDigit } from '../../core/detection/numericChecksums.js';
4
+ import { IndeterminateError, InvalidInputError, NotFoundError, SchemaError } from '../../core/errors/index.js';
5
+ import { lastMovement } from '../../core/result/pickup.js';
6
+ import { classifyWording, languageStageStatus } from '../../core/status/index.js';
7
+ import { explicitOffsetTime } from '../../core/time/index.js';
8
+ import { clean, cleanScalar } from '../../core/transport/index.js';
9
+ import { isRecord } from '../../core/types.js';
10
+ import { vintedGoCode, vintedGoStage } from './status.js';
11
+ export const PROVIDER = 'Vinted Go';
12
+ // A parcel's reply is a few kilobytes; its scans and a few questions and answers.
13
+ export const VINTED_GO_MAX_BYTES = 512 * 1024;
14
+ // Sixteen digits: the label's creation in Unix milliseconds, two more digits
15
+ // and a Luhn check digit. The service answers a number whose check fails as if
16
+ // it did not exist, whatever its age.
17
+ const NUMBER = /^1\d{15}$/;
18
+ // A sorting centre's town and country, the only place a scan's wording names
19
+ // as the parcel's own. "In transit to" names where it is going.
20
+ const SORTING_CENTER = /^(?:Return: )?At sorting center in ([^,]{2,80}), ([A-Z]{2})$/;
21
+ // The point a parcel is on its way to. Only lockers and parcel shops are named:
22
+ // a neighbourhood point can be someone's home.
23
+ const ON_ITS_WAY = /^The parcel is on its way to the (locker|parcel shop) \((.{1,120})\)$/;
24
+ const READY_AT = /^Ready to collect at the (locker|parcel shop)$/;
25
+ // The milestones whose scan can carry a new tracking code on another network.
26
+ const HANDOVER = new Set(['redirected', 'left_our_network']);
27
+ const MAX_ROWS = 500;
28
+ const MAX_EVENTS = 100;
29
+ /** A Vinted Go parcel number; anything else is a number Vinted Go does not issue. */
30
+ export function normalizeVintedGoNumber(raw) {
31
+ const number = raw.length <= 64 ? normalizeTrackingNumber(raw) : '';
32
+ if (!NUMBER.test(number) || !hasLuhnCheckDigit(number)) {
33
+ throw new InvalidInputError(PROVIDER, 'Vinted Go numbers are sixteen digits starting 1 whose last digit passes the Luhn check');
34
+ }
35
+ return number;
36
+ }
37
+ /** An optional text field; an object, array or other non-scalar is a changed reply. */
38
+ function text(row, key, maxLength = 500) {
39
+ const value = row[key];
40
+ if (value === null || value === undefined)
41
+ return '';
42
+ if (typeof value !== 'string' && typeof value !== 'number')
43
+ throw new SchemaError(PROVIDER, `Vinted Go returned an invalid ${key}`);
44
+ return typeof value === 'number' ? cleanScalar(value, maxLength) : clean(value, maxLength);
45
+ }
46
+ function projectScan(row, index) {
47
+ if (!isRecord(row))
48
+ throw new SchemaError(PROVIDER, 'Vinted Go returned an invalid scan');
49
+ const stamp = text(row, 'timestamp', 64);
50
+ const time = explicitOffsetTime(stamp);
51
+ if (!time)
52
+ throw new SchemaError(PROVIDER, 'Vinted Go returned a scan without an offset time');
53
+ const description = text(row, 'message');
54
+ if (!description)
55
+ throw new SchemaError(PROVIDER, 'Vinted Go returned a scan without wording');
56
+ const group = text(row, 'group', 64);
57
+ const state = text(row, 'state', 64);
58
+ const stage = vintedGoStage(state, group);
59
+ const place = SORTING_CENTER.exec(description);
60
+ const event = {
61
+ time: time.iso,
62
+ description,
63
+ ...(place ? { location: `${place[1]}, ${place[2]}` } : {}),
64
+ ...(group && state ? { provider_code: vintedGoCode(state, group) } : {}),
65
+ ...(stage ? { stage, stage_source: 'carrier_map' } : {}),
66
+ };
67
+ const metadata = isRecord(row.metadata) ? row.metadata : {};
68
+ return { id: text(row, 'id', 64), scan: { event, at: time.timestamp, index, group, state, metadata } };
69
+ }
70
+ /**
71
+ * The locker or parcel shop a parcel waits at: the point the scan before its
72
+ * newest ready-to-collect scan sent it on to, when no redirection came between.
73
+ * On a delivered parcel, the point it was collected from.
74
+ */
75
+ function pickupPoint(scans, stage) {
76
+ const events = scans.map((scan) => scan.event);
77
+ const ready = scans.findIndex((scan) => scan.event.stage === 'ready_for_pickup');
78
+ if (ready < 0)
79
+ return '';
80
+ if (stage === 'delivered') {
81
+ if (lastMovement(events.slice(1)) !== 'ready_for_pickup')
82
+ return '';
83
+ }
84
+ else if (stage !== 'ready_for_pickup' || ready !== 0) {
85
+ return '';
86
+ }
87
+ const kind = READY_AT.exec(scans[ready].event.description)?.[1];
88
+ if (!kind)
89
+ return '';
90
+ for (const scan of scans.slice(ready + 1)) {
91
+ if (scan.group === 'redirected')
92
+ return '';
93
+ if (scan.group !== 'in_transit')
94
+ continue;
95
+ const match = ON_ITS_WAY.exec(scan.event.description);
96
+ if (match)
97
+ return match[1] === kind ? match[2] : '';
98
+ }
99
+ return '';
100
+ }
101
+ /** The newest new tracking code the parcel was handed over under, with its network when its link names one. */
102
+ function handover(scans, number) {
103
+ for (const scan of scans) {
104
+ if (scan.state !== 'delivery' || !HANDOVER.has(scan.group))
105
+ continue;
106
+ const code = normalizeTrackingNumber(text(scan.metadata, 'tracking_code', 64));
107
+ if (!/^[A-Z0-9]{4,40}$/.test(code) || code === number)
108
+ continue;
109
+ const partner = carrierIdFromPartner('', text(scan.metadata, 'external_link', 500));
110
+ return { delivery_tracking_number: code, ...(partner && partner !== 'vinted-go' ? { delivery_carrier: partner } : {}) };
111
+ }
112
+ return {};
113
+ }
114
+ /**
115
+ * The reply of `GET /public/v3/tracking_events/{number}`. It does not echo the
116
+ * number: the scans belong to the one in the request's path. The pickup
117
+ * point's address, its id, the pickup deadline, banners and questions in the
118
+ * same reply are never read.
119
+ */
120
+ export function parseVintedGo(payload, raw) {
121
+ const number = normalizeVintedGoNumber(raw);
122
+ if (!isRecord(payload) || !Array.isArray(payload.tracking_events))
123
+ throw new SchemaError(PROVIDER, 'Vinted Go returned an unexpected reply');
124
+ const rows = payload.tracking_events;
125
+ if (rows.length > MAX_ROWS)
126
+ throw new SchemaError(PROVIDER, 'Vinted Go returned an oversized history');
127
+ // The tracker treats an empty list like an unknown number, but no reply showed one.
128
+ if (!rows.length)
129
+ throw new IndeterminateError(PROVIDER, 'Vinted Go returned no scans for the parcel');
130
+ const seen = new Set();
131
+ const scans = [];
132
+ rows.forEach((row, index) => {
133
+ const { id, scan } = projectScan(row, index);
134
+ const key = id || JSON.stringify(scan.event);
135
+ if (seen.has(key))
136
+ return;
137
+ seen.add(key);
138
+ scans.push(scan);
139
+ });
140
+ // The reply lists scans newest first. Instants order them; equal ones keep its order.
141
+ scans.sort((left, right) => right.at - left.at || left.index - right.index);
142
+ const newest = scans[0].event;
143
+ const current = newest.stage
144
+ ? { stage: newest.stage, source: 'carrier_map' }
145
+ : classifyWording(newest.description, 'in_transit');
146
+ const known = current.source !== 'none';
147
+ const stage = known ? current.stage : undefined;
148
+ const point = pickupPoint(scans, stage);
149
+ return {
150
+ status: stage ? languageStageStatus(stage) : 'unknown',
151
+ ...(stage ? { current_stage: stage, current_stage_source: current.source } : {}),
152
+ last_status_text: newest.description,
153
+ last_update: newest.time,
154
+ expected_delivery: null,
155
+ ...(stage === 'delivered' ? { delivered_at: newest.time } : {}),
156
+ ...(point ? { pickup_point: point } : {}),
157
+ ...handover(scans, number),
158
+ events: scans.slice(0, MAX_EVENTS).map((scan) => scan.event),
159
+ ...(scans.length > MAX_EVENTS ? { history_truncated: true } : {}),
160
+ };
161
+ }
162
+ /**
163
+ * A 404 from the service. An unknown number gets an empty body in the type
164
+ * asked for; a number whose label is over six months old gets
165
+ * `shipment_expired`, whether the parcel existed or not. A missing route
166
+ * answers "Not found" in plain text and says nothing about the parcel.
167
+ */
168
+ export function parseVintedGoMissing(body, contentType) {
169
+ const type = (contentType ?? '').toLowerCase();
170
+ if (!body.trim()) {
171
+ if (type.includes('application/json'))
172
+ throw new NotFoundError(PROVIDER);
173
+ throw new IndeterminateError(PROVIDER, 'Vinted Go tracking service is unavailable');
174
+ }
175
+ let reply;
176
+ try {
177
+ reply = JSON.parse(body);
178
+ }
179
+ catch {
180
+ throw new IndeterminateError(PROVIDER, 'Vinted Go tracking service is unavailable');
181
+ }
182
+ const code = isRecord(reply) && typeof reply.error_code === 'string' ? reply.error_code : '';
183
+ if (code === 'shipment_expired') {
184
+ throw new IndeterminateError(PROVIDER, 'Vinted Go no longer keeps the history of parcels over six months old', { reason: 'history_expired' });
185
+ }
186
+ throw new IndeterminateError(PROVIDER, 'Vinted Go refused the lookup');
187
+ }
@@ -0,0 +1,13 @@
1
+ import type { Stage } from '../../core/status/index.js';
2
+ import type { CarrierStatusMap } from '../../core/status/statusMap.js';
3
+ /** The provider code of a scan: its leg, a slash and its milestone ("return/in_transit"). */
4
+ export declare function vintedGoCode(state: string, group: string): string;
5
+ /**
6
+ * A scan's stage from its leg and milestone. Every movement on the return leg
7
+ * takes the parcel back to its sender, so it is `returned`; a loss or a
8
+ * cancellation stays an exception on any leg. A leg or milestone the map does
9
+ * not know is left to the shared wording rules.
10
+ */
11
+ export declare function vintedGoStage(state: string, group: string): Stage | undefined;
12
+ /** What the map says about one scan, by its provider code alone. */
13
+ export declare const statusMap: CarrierStatusMap;
@@ -0,0 +1,47 @@
1
+ // Each scan names its milestone (`group`) and the leg it belongs to (`state`):
2
+ // `delivery` on the way to the recipient, `return` back to the sender and
3
+ // `dispose` for a parcel that will not arrive. The tracker's own labels name
4
+ // the groups; a redirection sends the parcel on to another point or network.
5
+ const GROUPS = {
6
+ created: 'registered',
7
+ shipped: 'accepted',
8
+ in_transit: 'in_transit',
9
+ redirected: 'in_transit',
10
+ left_our_network: 'in_transit',
11
+ ready_for_pickup: 'ready_for_pickup',
12
+ delivered: 'delivered',
13
+ return: 'returned',
14
+ returned: 'returned',
15
+ lost: 'exception',
16
+ disposed: 'exception',
17
+ cancelled: 'exception',
18
+ empty_locker_found: 'exception',
19
+ };
20
+ const LEGS = new Set(['delivery', 'return', 'dispose']);
21
+ /** The provider code of a scan: its leg, a slash and its milestone ("return/in_transit"). */
22
+ export function vintedGoCode(state, group) {
23
+ return `${state}/${group}`;
24
+ }
25
+ /**
26
+ * A scan's stage from its leg and milestone. Every movement on the return leg
27
+ * takes the parcel back to its sender, so it is `returned`; a loss or a
28
+ * cancellation stays an exception on any leg. A leg or milestone the map does
29
+ * not know is left to the shared wording rules.
30
+ */
31
+ export function vintedGoStage(state, group) {
32
+ if (!LEGS.has(state) || !Object.hasOwn(GROUPS, group))
33
+ return undefined;
34
+ const stage = GROUPS[group];
35
+ return state === 'return' && stage !== 'exception' ? 'returned' : stage;
36
+ }
37
+ /** What the map says about one scan, by its provider code alone. */
38
+ export const statusMap = {
39
+ stage: (code) => {
40
+ const [state, group, ...rest] = (code ?? '').split('/');
41
+ return state && group && !rest.length ? vintedGoStage(state, group) : undefined;
42
+ },
43
+ gaps: [{
44
+ code: 'delivery/oversized',
45
+ note: 'The tracker names an oversized parcel, which it then redirects to a larger point. No reply showed one, so it is left to the wording rules.',
46
+ }],
47
+ };
@@ -1,4 +1,21 @@
1
1
  {
2
2
  "carrier": "vinted-go",
3
- "entries": []
3
+ "entries": [
4
+ { "code": "delivery/created", "wording": "Shipment created", "language": "en", "stage": "registered", "confirmedBy": "live", "note": "The label, before the sender drops the parcel off." },
5
+ { "code": "delivery/shipped", "wording": "Parcel was dropped off at the locker", "language": "en", "stage": "accepted", "confirmedBy": "live", "note": "Also worded \"Parcel was dropped off at the parcel shop\"." },
6
+ { "code": "delivery/in_transit", "wording": "At sorting center in <town>, <country>", "language": "en", "stage": "in_transit", "confirmedBy": "live", "note": "Also \"In transit to <town>, <country>\" and \"The parcel is on its way to the locker (<point>)\". Only a sorting centre's town becomes the scan's location." },
7
+ { "code": "delivery/redirected", "wording": "Parcel redirected to the parcel shop", "language": "en", "stage": "in_transit", "confirmedBy": "live", "note": "The selected point could not take the parcel, which goes on to another point, a hub (\"to the hub\", \"to the locker\") or another network (\"Change: Parcel redirected to Colissimo\")." },
8
+ { "code": "delivery/left_our_network", "wording": "Parcel left our network", "language": "en", "stage": "in_transit", "confirmedBy": "bundle", "note": "The tracker shows this milestone with the new tracking code and the other network's link; no reply showed it, so the wording is a placeholder." },
9
+ { "code": "delivery/ready_for_pickup", "wording": "Ready to collect at the locker", "language": "en", "stage": "ready_for_pickup", "confirmedBy": "live", "note": "Also \"Ready to collect at the parcel shop\"." },
10
+ { "code": "delivery/delivered", "wording": "Parcel was collected by the recipient", "language": "en", "stage": "delivered", "confirmedBy": "live", "note": "A second scan worded \"Delivered\" follows within a second." },
11
+ { "code": "delivery/empty_locker_found", "wording": "Missing from locker", "language": "en", "stage": "exception", "confirmedBy": "bundle", "note": "The tracker's label for the milestone." },
12
+ { "code": "return/return", "wording": "Return: Parcel will be returned. Reason: pickup time has expired", "language": "en", "stage": "returned", "confirmedBy": "live", "note": "The return leg starts." },
13
+ { "code": "return/in_transit", "wording": "Return: At sorting center in <town>, <country>", "language": "en", "stage": "returned", "confirmedBy": "live", "note": "Every movement on the return leg takes the parcel back to its sender." },
14
+ { "code": "return/ready_for_pickup", "wording": "Return ready to collect", "language": "en", "stage": "returned", "confirmedBy": "bundle", "note": "The tracker's label: the returned parcel waits for its sender at a point." },
15
+ { "code": "return/returned", "wording": "Return delivered", "language": "en", "stage": "returned", "confirmedBy": "bundle", "note": "The tracker's label." },
16
+ { "code": "dispose/lost", "wording": "Parcel lost", "language": "en", "stage": "exception", "confirmedBy": "live", "note": "Later scans can show the parcel found again." },
17
+ { "code": "dispose/disposed", "wording": "Couldn’t be delivered", "language": "en", "stage": "exception", "confirmedBy": "bundle", "note": "The tracker's label." },
18
+ { "code": "dispose/cancelled", "wording": "Shipment was cancelled", "language": "en", "stage": "exception", "confirmedBy": "bundle", "note": "The tracker's label." },
19
+ { "code": "delivery/oversized", "wording": "Parcel is oversized", "language": "en", "confirmedBy": "bundle", "note": "Left without a stage: the tracker's code names the milestone without a label, and no reply showed it." }
20
+ ]
4
21
  }
@@ -55,6 +55,7 @@ import { statusMap as thailandPost } from '../../carriers/thailand-post/status.j
55
55
  import { statusMap as tnt } from '../../carriers/tnt/status.js';
56
56
  import { statusMap as ups } from '../../carriers/ups/status.js';
57
57
  import { statusMap as venipak } from '../../carriers/venipak/status.js';
58
+ import { statusMap as vintedGo } from '../../carriers/vinted-go/status.js';
58
59
  import { statusMap as yunexpress } from '../../carriers/yunexpress/status.js';
59
60
  import { statusMap as yurticiKargo } from '../../carriers/yurtici-kargo/status.js';
60
61
  import { statusMap as universal } from '../../providers/status.js';
@@ -67,7 +68,7 @@ export const STATUS_MAPS = {
67
68
  emile, 'fan-courier': fanCourier, foxpost, 'geniki-taxydromiki': geniki, 'gls-it': glsIt, 'gofo-fr': gofoFr, 'gofo-it': gofoIt, helthjem,
68
69
  'j-and-t': jAndT, 'j-and-t-cargo': jntCargo, 'la-poste': laPoste, matkahuolto, 'old-dominion': oldDominion, omgo, omniva,
69
70
  'orlen-paczka': orlenPaczka, postlogistics, ppl, ptt, sagawa, sameday, speedx, speedy, sto, 'swiss-post': swissPost, 'thailand-post': thailandPost,
70
- tnt, ups, venipak, yunexpress, 'yurtici-kargo': yurticiKargo,
71
+ tnt, ups, venipak, 'vinted-go': vintedGo, yunexpress, 'yurtici-kargo': yurticiKargo,
71
72
  };
72
73
  /** The carrier the app files the universal providers' scans under. */
73
74
  const UNIVERSAL_CARRIER = 'unknown';
@@ -8105,7 +8105,8 @@
8105
8105
  "timezone": "Europe/Paris",
8106
8106
  "tracking": {
8107
8107
  "mode": "automatic",
8108
- "adapter": "universal"
8108
+ "adapter": "vinted-go",
8109
+ "recognitionRank": 72
8109
8110
  },
8110
8111
  "canaryUrl": "https://vintedgo.com/en/tracking",
8111
8112
  "trackingUrlTemplate": "https://vintedgo.com/en/tracking/{trackingNumber}",
@@ -5438,7 +5438,8 @@ export declare const CARRIER_CATALOG: {
5438
5438
  readonly timezone: "Europe/Paris";
5439
5439
  readonly tracking: {
5440
5440
  readonly mode: "automatic";
5441
- readonly adapter: "universal";
5441
+ readonly adapter: "vinted-go";
5442
+ readonly recognitionRank: 72;
5442
5443
  };
5443
5444
  readonly canaryUrl: "https://vintedgo.com/en/tracking";
5444
5445
  readonly trackingUrlTemplate: "https://vintedgo.com/en/tracking/{trackingNumber}";
@@ -8106,7 +8106,8 @@ export const CARRIER_CATALOG = {
8106
8106
  "timezone": "Europe/Paris",
8107
8107
  "tracking": {
8108
8108
  "mode": "automatic",
8109
- "adapter": "universal"
8109
+ "adapter": "vinted-go",
8110
+ "recognitionRank": 72
8110
8111
  },
8111
8112
  "canaryUrl": "https://vintedgo.com/en/tracking",
8112
8113
  "trackingUrlTemplate": "https://vintedgo.com/en/tracking/{trackingNumber}",
@@ -73,5 +73,6 @@ export const CARRIER_RECOGNITION_RANKS = {
73
73
  "ukrposhta": 5,
74
74
  "uniuni": 33,
75
75
  "venipak": 63,
76
+ "vinted-go": 72,
76
77
  "yurtici-kargo": 66,
77
78
  };
@@ -132,6 +132,7 @@ import { adapter as uniuni } from '../carriers/uniuni/adapter.js';
132
132
  import { adapter as ups } from '../carriers/ups/adapter.js';
133
133
  import { adapter as usps } from '../carriers/usps/adapter.js';
134
134
  import { adapter as venipak } from '../carriers/venipak/adapter.js';
135
+ import { adapter as vintedGo } from '../carriers/vinted-go/adapter.js';
135
136
  import { adapter as xpressbees } from '../carriers/xpressbees/adapter.js';
136
137
  import { adapter as yamato } from '../carriers/yamato/adapter.js';
137
138
  import { adapter as yanwen } from '../carriers/yanwen/adapter.js';
@@ -274,6 +275,7 @@ export const REGISTRY = {
274
275
  "ups": ups,
275
276
  "usps": usps,
276
277
  "venipak": venipak,
278
+ "vinted-go": vintedGo,
277
279
  "xpressbees": xpressbees,
278
280
  "yamato": yamato,
279
281
  "yanwen": yanwen,
@@ -476,7 +478,7 @@ export const REGISTRY = {
476
478
  "ups": "ups",
477
479
  "usps": "usps",
478
480
  "venipak": "venipak",
479
- "vinted-go": "universal",
481
+ "vinted-go": "vinted-go",
480
482
  "wanbexpress": "universal",
481
483
  "whistl": "universal",
482
484
  "xdp": "universal",
@@ -2,7 +2,7 @@
2
2
  "openapi": "3.1.0",
3
3
  "info": {
4
4
  "title": "Universal Parcel Scraper",
5
- "version": "0.82.0"
5
+ "version": "0.83.0"
6
6
  },
7
7
  "paths": {
8
8
  "/health": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "universal-parcel-scraper",
3
- "version": "0.82.0-main.949",
3
+ "version": "0.83.0-main.951",
4
4
  "description": "Self-hosted parcel tracking: carrier detection, dedicated scrapers and optional universal providers.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",