@mitumbadotafrica/logistics 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,64 @@
1
+ # @mitumbadotafrica/logistics
2
+
3
+ The headless, carrier-neutral client for Mitumba Logistics. It speaks the HTTP contract and
4
+ nothing else: no carrier code, no routing rules, no secrets. Re-exports
5
+ `@mitumbadotafrica/logistics-contract`, so one import is enough.
6
+
7
+ ```bash
8
+ npm install @mitumbadotafrica/logistics
9
+ ```
10
+
11
+ ```ts
12
+ import { MitumbaLogistics } from '@mitumbadotafrica/logistics'
13
+
14
+ const logistics = new MitumbaLogistics({ token: () => session.accessToken })
15
+ ```
16
+
17
+ `token` is a string or a function returning one (sync or async). `baseUrl`, `fetch`, `maxRetries`
18
+ and `debug` are optional. A non-2xx response throws a `LogisticsError` with `status`, `code` and,
19
+ where the backend gives one, `reason`.
20
+
21
+ ## Checkout delivery: basket quotes
22
+
23
+ Price each shop's basket before any order exists. The backend derives the origin from the shop.
24
+ You send the destination: a saved address, or a "Deliver to" point with its region.
25
+
26
+ ```ts
27
+ const { baskets } = await logistics.quoteBaskets({
28
+ market_code: 'KE',
29
+ destination: { type: 'address', address_id },
30
+ baskets: [{ store_id, items: [{ listing_id, quantity: 2 }] }],
31
+ })
32
+ ```
33
+
34
+ Each basket is `available` with options, or `unavailable` with a `reason`. An available basket
35
+ has one option per door method and one per nearby pickup hub (`option.hub`). The helpers below
36
+ arrange what came back. They never choose a carrier or invent a price:
37
+
38
+ | Helper | Use |
39
+ |---|---|
40
+ | `methodChoices(basket)` | Options grouped by method, with the cheapest and whether a hub is picked |
41
+ | `methodLabel(method)`, `unavailableLabel(reason)`, `etaLabel(eta)` | Buyer-facing copy, overridable, safe for unknown values |
42
+ | `isQuoteExpired(option)` | Re-quote once `expires_at` has passed |
43
+ | `checkoutDeliveries(baskets, picks)` | The `deliveries: [{ store_id, quote_id }]` checkout takes, or which shops are missing or undeliverable |
44
+
45
+ A quote is bound to the buyer, shop, market, destination and the basket's exact items. Re-quote
46
+ after any of them changes.
47
+
48
+ ## Seller delivery data
49
+
50
+ ```ts
51
+ await logistics.setStoreOrigin(storeId, { latitude, longitude, address_line, dropoff_node_id: null })
52
+ await logistics.setListingPackage(listingId, 'medium') // small | medium | large | oversize
53
+ ```
54
+
55
+ `getStoreOrigin` and `getListingPackage` read them back. A listing without a size reports its
56
+ category default (`source: 'category_default'`).
57
+
58
+ ## Orders and parcels
59
+
60
+ `getMarkets()`, `quote()` (an existing order), `createParcel()`, `getParcel()`, and
61
+ `pollParcel(client, parcelId, options)`. `pollParcel` stops at a terminal status, or at
62
+ `action_required` so you can surface the next action.
63
+
64
+ MIT
package/dist/index.cjs CHANGED
@@ -25,7 +25,13 @@ __export(index_exports, {
25
25
  HttpClient: () => HttpClient,
26
26
  LogisticsError: () => LogisticsError,
27
27
  MitumbaLogistics: () => MitumbaLogistics,
28
- pollParcel: () => pollParcel
28
+ checkoutDeliveries: () => checkoutDeliveries,
29
+ etaLabel: () => etaLabel,
30
+ isQuoteExpired: () => isQuoteExpired,
31
+ methodChoices: () => methodChoices,
32
+ methodLabel: () => methodLabel,
33
+ pollParcel: () => pollParcel,
34
+ unavailableLabel: () => unavailableLabel
29
35
  });
30
36
  module.exports = __toCommonJS(index_exports);
31
37
 
@@ -73,6 +79,10 @@ var HttpClient = class {
73
79
  async post(path, body, options) {
74
80
  return this.request("POST", path, body, options);
75
81
  }
82
+ /** PUT replaces a whole resource, so it is idempotent; it is still only retried on network errors. */
83
+ async put(path, body, options) {
84
+ return this.request("PUT", path, body, options);
85
+ }
76
86
  async request(method, path, body, options) {
77
87
  const url = `${this.baseUrl}${path.startsWith("/") ? path : `/${path}`}`;
78
88
  const token = options?.token ?? await this.resolveToken();
@@ -161,6 +171,37 @@ var MitumbaLogistics = class {
161
171
  async getParcel(parcelId, options) {
162
172
  return this.http.get(`/logistics/parcels/${encodeURIComponent(parcelId)}`, options);
163
173
  }
174
+ /**
175
+ * Price each shop basket before any order exists. Origin is the shop (derived
176
+ * by the backend); destination is the buyer's saved address or a "Deliver to"
177
+ * point. Each available basket has one option per door method and one per
178
+ * nearby pickup hub; pass the picked option's `quote_id` per shop to checkout.
179
+ * An unservable basket comes back `unavailable` with a reason, not as an error.
180
+ * Requires the buyer's token.
181
+ */
182
+ async quoteBaskets(input, options) {
183
+ return this.http.post("/logistics/basket-quotes", input, options);
184
+ }
185
+ /** Seller: where a shop's parcels start. Fields are null until set. */
186
+ async getStoreOrigin(storeId, options) {
187
+ return this.http.get(`/logistics/stores/${encodeURIComponent(storeId)}/origin`, options);
188
+ }
189
+ /** Seller: replace a shop's dispatch point. */
190
+ async setStoreOrigin(storeId, input, options) {
191
+ return this.http.put(`/logistics/stores/${encodeURIComponent(storeId)}/origin`, input, options);
192
+ }
193
+ /** Seller: a listing's package size, or its category default when unset. */
194
+ async getListingPackage(listingId, options) {
195
+ return this.http.get(`/logistics/listings/${encodeURIComponent(listingId)}/package`, options);
196
+ }
197
+ /** Seller: set a listing's package size. */
198
+ async setListingPackage(listingId, packageSize, options) {
199
+ return this.http.put(
200
+ `/logistics/listings/${encodeURIComponent(listingId)}/package`,
201
+ { package_size: packageSize },
202
+ options
203
+ );
204
+ }
164
205
  };
165
206
 
166
207
  // src/polling.ts
@@ -208,6 +249,66 @@ function sleep(ms, signal) {
208
249
  });
209
250
  }
210
251
 
252
+ // src/delivery.ts
253
+ function methodChoices(basket) {
254
+ const byMethod = /* @__PURE__ */ new Map();
255
+ for (const option of basket.options) {
256
+ const options = byMethod.get(option.method) ?? [];
257
+ options.push(option);
258
+ byMethod.set(option.method, options);
259
+ }
260
+ return [...byMethod].map(([method, options]) => ({
261
+ method,
262
+ options,
263
+ cheapest: options.reduce((best, option) => option.price.minor_units < best.price.minor_units ? option : best),
264
+ needsHub: options.some((option) => option.hub !== void 0)
265
+ }));
266
+ }
267
+ var METHOD_LABELS = {
268
+ pickup_point: "Pickup point",
269
+ doorstep: "Door delivery",
270
+ same_day: "Same-day delivery",
271
+ branch_collection: "Branch collection"
272
+ };
273
+ function methodLabel(method, overrides = {}) {
274
+ return overrides[method] ?? METHOD_LABELS[method] ?? method.replace(/_/g, " ").replace(/^\w/, (c) => c.toUpperCase());
275
+ }
276
+ function unavailableLabel(reason, overrides = {}) {
277
+ const defaults = {
278
+ no_carrier_for_lane: "No delivery is available from this shop to your location yet.",
279
+ outside_market: "This shop does not deliver to that location.",
280
+ listing_unavailable: "An item from this shop is no longer available."
281
+ };
282
+ const key = reason ?? "";
283
+ return overrides[key] ?? defaults[key] ?? "Delivery is unavailable for this shop.";
284
+ }
285
+ function etaLabel(eta) {
286
+ if (eta.max_days <= 1 && eta.min_days === 0) return "Same day";
287
+ if (eta.min_days === eta.max_days) return `${eta.max_days} ${eta.max_days === 1 ? "day" : "days"}`;
288
+ return `${eta.min_days}\u2013${eta.max_days} days`;
289
+ }
290
+ function isQuoteExpired(option, now = /* @__PURE__ */ new Date()) {
291
+ return Date.parse(option.expires_at) <= now.getTime();
292
+ }
293
+ function checkoutDeliveries(baskets, picks) {
294
+ const missing = [];
295
+ const unavailable = [];
296
+ const deliveries = [];
297
+ for (const basket of baskets) {
298
+ if (basket.status !== "available" || basket.options.length === 0) {
299
+ unavailable.push(basket.store_id);
300
+ continue;
301
+ }
302
+ const quoteId = picks[basket.store_id];
303
+ if (!quoteId || !basket.options.some((option) => option.quote_id === quoteId)) {
304
+ missing.push(basket.store_id);
305
+ continue;
306
+ }
307
+ deliveries.push({ store_id: basket.store_id, quote_id: quoteId });
308
+ }
309
+ return missing.length === 0 && unavailable.length === 0 ? { ok: true, deliveries } : { ok: false, missing, unavailable };
310
+ }
311
+
211
312
  // src/index.ts
212
313
  __reExport(index_exports, require("@mitumbadotafrica/logistics-contract"), module.exports);
213
314
  // Annotate the CommonJS export names for ESM import in node:
@@ -216,6 +317,12 @@ __reExport(index_exports, require("@mitumbadotafrica/logistics-contract"), modul
216
317
  HttpClient,
217
318
  LogisticsError,
218
319
  MitumbaLogistics,
320
+ checkoutDeliveries,
321
+ etaLabel,
322
+ isQuoteExpired,
323
+ methodChoices,
324
+ methodLabel,
219
325
  pollParcel,
326
+ unavailableLabel,
220
327
  ...require("@mitumbadotafrica/logistics-contract")
221
328
  });
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { MarketsResponse, QuoteRequest, QuoteResponse, CreateParcelResponse, ParcelView, LogisticsErrorBody } from '@mitumbadotafrica/logistics-contract';
1
+ import { MarketsResponse, QuoteRequest, QuoteResponse, CreateParcelResponse, ParcelView, BasketQuoteRequest, BasketQuoteResponse, StoreOrigin, StoreOriginInput, ListingPackage, PackageSize, LogisticsErrorBody, FulfilmentMethod, BasketQuoteOption, BasketQuote } from '@mitumbadotafrica/logistics-contract';
2
2
  export * from '@mitumbadotafrica/logistics-contract';
3
3
 
4
4
  /** Canonical public API base. Override for staging/self-hosting. */
@@ -76,6 +76,23 @@ declare class MitumbaLogistics {
76
76
  * whether one exists.
77
77
  */
78
78
  getParcel(parcelId: string, options?: RequestOptions): Promise<ParcelView>;
79
+ /**
80
+ * Price each shop basket before any order exists. Origin is the shop (derived
81
+ * by the backend); destination is the buyer's saved address or a "Deliver to"
82
+ * point. Each available basket has one option per door method and one per
83
+ * nearby pickup hub; pass the picked option's `quote_id` per shop to checkout.
84
+ * An unservable basket comes back `unavailable` with a reason, not as an error.
85
+ * Requires the buyer's token.
86
+ */
87
+ quoteBaskets(input: BasketQuoteRequest, options?: RequestOptions): Promise<BasketQuoteResponse>;
88
+ /** Seller: where a shop's parcels start. Fields are null until set. */
89
+ getStoreOrigin(storeId: string, options?: RequestOptions): Promise<StoreOrigin>;
90
+ /** Seller: replace a shop's dispatch point. */
91
+ setStoreOrigin(storeId: string, input: StoreOriginInput, options?: RequestOptions): Promise<StoreOrigin>;
92
+ /** Seller: a listing's package size, or its category default when unset. */
93
+ getListingPackage(listingId: string, options?: RequestOptions): Promise<ListingPackage>;
94
+ /** Seller: set a listing's package size. */
95
+ setListingPackage(listingId: string, packageSize: PackageSize, options?: RequestOptions): Promise<ListingPackage>;
79
96
  }
80
97
 
81
98
  /**
@@ -94,6 +111,8 @@ declare class HttpClient {
94
111
  private resolveToken;
95
112
  get<T>(path: string, options?: RequestOptions): Promise<T>;
96
113
  post<T>(path: string, body: unknown, options?: RequestOptions): Promise<T>;
114
+ /** PUT replaces a whole resource, so it is idempotent; it is still only retried on network errors. */
115
+ put<T>(path: string, body: unknown, options?: RequestOptions): Promise<T>;
97
116
  private request;
98
117
  }
99
118
 
@@ -146,4 +165,54 @@ interface PollParcelOptions {
146
165
  */
147
166
  declare function pollParcel(client: MitumbaLogistics, parcelId: string, options?: PollParcelOptions): Promise<ParcelView | null>;
148
167
 
149
- export { type CreateParcelInput, DEFAULT_BASE_URL, HttpClient, LogisticsError, MitumbaLogistics, type MitumbaLogisticsConfig, type PollParcelOptions, type RequestOptions, type ShipmentPartyInput, pollParcel };
168
+ /**
169
+ * Framework-free helpers for a checkout's delivery step, shared by the React and
170
+ * React Native pickers. They only arrange what the backend returned: the client
171
+ * never chooses a carrier, never prices, and never invents an option.
172
+ */
173
+ /** The options of one method for a basket. A pickup method lists one option per hub. */
174
+ interface MethodChoice {
175
+ method: FulfilmentMethod;
176
+ options: BasketQuoteOption[];
177
+ /** The cheapest option of the method, for a "from KES X" line. */
178
+ cheapest: BasketQuoteOption;
179
+ /** True when the buyer picks a hub (the options carry `hub`). */
180
+ needsHub: boolean;
181
+ }
182
+ /** Group a basket's options by method, in the order the backend returned them. */
183
+ declare function methodChoices(basket: BasketQuote): MethodChoice[];
184
+ /** Buyer-facing name for a method. An unknown method is humanised, never an error. */
185
+ declare function methodLabel(method: FulfilmentMethod, overrides?: Partial<Record<string, string>>): string;
186
+ /** Buyer-facing copy for an unavailable basket. An unknown reason gets the generic line. */
187
+ declare function unavailableLabel(reason: string | undefined, overrides?: Partial<Record<string, string>>): string;
188
+ /** "Same day", "1 day", "2–4 days". */
189
+ declare function etaLabel(eta: {
190
+ min_days: number;
191
+ max_days: number;
192
+ }): string;
193
+ /** Has an option's quote stopped being claimable? Re-quote before checkout if so. */
194
+ declare function isQuoteExpired(option: Pick<BasketQuoteOption, 'expires_at'>, now?: Date): boolean;
195
+ /** One shop's pick, as checkout takes it: `deliveries: [{ store_id, quote_id }]`. */
196
+ interface CheckoutDelivery {
197
+ store_id: string;
198
+ quote_id: string;
199
+ }
200
+ type CheckoutDeliveriesResult = {
201
+ ok: true;
202
+ deliveries: CheckoutDelivery[];
203
+ }
204
+ /** Shops the buyer has not picked a valid option for, and shops that cannot be delivered. */
205
+ | {
206
+ ok: false;
207
+ missing: string[];
208
+ unavailable: string[];
209
+ };
210
+ /**
211
+ * Turn the buyer's picks (store id → quote id) into checkout's `deliveries`.
212
+ * Every basket needs exactly one pick among its own options; an unavailable
213
+ * basket blocks checkout (the shop's items must be removed or the destination
214
+ * changed). A pick that is not one of the basket's options counts as missing.
215
+ */
216
+ declare function checkoutDeliveries(baskets: readonly BasketQuote[], picks: Readonly<Record<string, string | undefined>>): CheckoutDeliveriesResult;
217
+
218
+ export { type CheckoutDeliveriesResult, type CheckoutDelivery, type CreateParcelInput, DEFAULT_BASE_URL, HttpClient, LogisticsError, type MethodChoice, MitumbaLogistics, type MitumbaLogisticsConfig, type PollParcelOptions, type RequestOptions, type ShipmentPartyInput, checkoutDeliveries, etaLabel, isQuoteExpired, methodChoices, methodLabel, pollParcel, unavailableLabel };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { MarketsResponse, QuoteRequest, QuoteResponse, CreateParcelResponse, ParcelView, LogisticsErrorBody } from '@mitumbadotafrica/logistics-contract';
1
+ import { MarketsResponse, QuoteRequest, QuoteResponse, CreateParcelResponse, ParcelView, BasketQuoteRequest, BasketQuoteResponse, StoreOrigin, StoreOriginInput, ListingPackage, PackageSize, LogisticsErrorBody, FulfilmentMethod, BasketQuoteOption, BasketQuote } from '@mitumbadotafrica/logistics-contract';
2
2
  export * from '@mitumbadotafrica/logistics-contract';
3
3
 
4
4
  /** Canonical public API base. Override for staging/self-hosting. */
@@ -76,6 +76,23 @@ declare class MitumbaLogistics {
76
76
  * whether one exists.
77
77
  */
78
78
  getParcel(parcelId: string, options?: RequestOptions): Promise<ParcelView>;
79
+ /**
80
+ * Price each shop basket before any order exists. Origin is the shop (derived
81
+ * by the backend); destination is the buyer's saved address or a "Deliver to"
82
+ * point. Each available basket has one option per door method and one per
83
+ * nearby pickup hub; pass the picked option's `quote_id` per shop to checkout.
84
+ * An unservable basket comes back `unavailable` with a reason, not as an error.
85
+ * Requires the buyer's token.
86
+ */
87
+ quoteBaskets(input: BasketQuoteRequest, options?: RequestOptions): Promise<BasketQuoteResponse>;
88
+ /** Seller: where a shop's parcels start. Fields are null until set. */
89
+ getStoreOrigin(storeId: string, options?: RequestOptions): Promise<StoreOrigin>;
90
+ /** Seller: replace a shop's dispatch point. */
91
+ setStoreOrigin(storeId: string, input: StoreOriginInput, options?: RequestOptions): Promise<StoreOrigin>;
92
+ /** Seller: a listing's package size, or its category default when unset. */
93
+ getListingPackage(listingId: string, options?: RequestOptions): Promise<ListingPackage>;
94
+ /** Seller: set a listing's package size. */
95
+ setListingPackage(listingId: string, packageSize: PackageSize, options?: RequestOptions): Promise<ListingPackage>;
79
96
  }
80
97
 
81
98
  /**
@@ -94,6 +111,8 @@ declare class HttpClient {
94
111
  private resolveToken;
95
112
  get<T>(path: string, options?: RequestOptions): Promise<T>;
96
113
  post<T>(path: string, body: unknown, options?: RequestOptions): Promise<T>;
114
+ /** PUT replaces a whole resource, so it is idempotent; it is still only retried on network errors. */
115
+ put<T>(path: string, body: unknown, options?: RequestOptions): Promise<T>;
97
116
  private request;
98
117
  }
99
118
 
@@ -146,4 +165,54 @@ interface PollParcelOptions {
146
165
  */
147
166
  declare function pollParcel(client: MitumbaLogistics, parcelId: string, options?: PollParcelOptions): Promise<ParcelView | null>;
148
167
 
149
- export { type CreateParcelInput, DEFAULT_BASE_URL, HttpClient, LogisticsError, MitumbaLogistics, type MitumbaLogisticsConfig, type PollParcelOptions, type RequestOptions, type ShipmentPartyInput, pollParcel };
168
+ /**
169
+ * Framework-free helpers for a checkout's delivery step, shared by the React and
170
+ * React Native pickers. They only arrange what the backend returned: the client
171
+ * never chooses a carrier, never prices, and never invents an option.
172
+ */
173
+ /** The options of one method for a basket. A pickup method lists one option per hub. */
174
+ interface MethodChoice {
175
+ method: FulfilmentMethod;
176
+ options: BasketQuoteOption[];
177
+ /** The cheapest option of the method, for a "from KES X" line. */
178
+ cheapest: BasketQuoteOption;
179
+ /** True when the buyer picks a hub (the options carry `hub`). */
180
+ needsHub: boolean;
181
+ }
182
+ /** Group a basket's options by method, in the order the backend returned them. */
183
+ declare function methodChoices(basket: BasketQuote): MethodChoice[];
184
+ /** Buyer-facing name for a method. An unknown method is humanised, never an error. */
185
+ declare function methodLabel(method: FulfilmentMethod, overrides?: Partial<Record<string, string>>): string;
186
+ /** Buyer-facing copy for an unavailable basket. An unknown reason gets the generic line. */
187
+ declare function unavailableLabel(reason: string | undefined, overrides?: Partial<Record<string, string>>): string;
188
+ /** "Same day", "1 day", "2–4 days". */
189
+ declare function etaLabel(eta: {
190
+ min_days: number;
191
+ max_days: number;
192
+ }): string;
193
+ /** Has an option's quote stopped being claimable? Re-quote before checkout if so. */
194
+ declare function isQuoteExpired(option: Pick<BasketQuoteOption, 'expires_at'>, now?: Date): boolean;
195
+ /** One shop's pick, as checkout takes it: `deliveries: [{ store_id, quote_id }]`. */
196
+ interface CheckoutDelivery {
197
+ store_id: string;
198
+ quote_id: string;
199
+ }
200
+ type CheckoutDeliveriesResult = {
201
+ ok: true;
202
+ deliveries: CheckoutDelivery[];
203
+ }
204
+ /** Shops the buyer has not picked a valid option for, and shops that cannot be delivered. */
205
+ | {
206
+ ok: false;
207
+ missing: string[];
208
+ unavailable: string[];
209
+ };
210
+ /**
211
+ * Turn the buyer's picks (store id → quote id) into checkout's `deliveries`.
212
+ * Every basket needs exactly one pick among its own options; an unavailable
213
+ * basket blocks checkout (the shop's items must be removed or the destination
214
+ * changed). A pick that is not one of the basket's options counts as missing.
215
+ */
216
+ declare function checkoutDeliveries(baskets: readonly BasketQuote[], picks: Readonly<Record<string, string | undefined>>): CheckoutDeliveriesResult;
217
+
218
+ export { type CheckoutDeliveriesResult, type CheckoutDelivery, type CreateParcelInput, DEFAULT_BASE_URL, HttpClient, LogisticsError, type MethodChoice, MitumbaLogistics, type MitumbaLogisticsConfig, type PollParcelOptions, type RequestOptions, type ShipmentPartyInput, checkoutDeliveries, etaLabel, isQuoteExpired, methodChoices, methodLabel, pollParcel, unavailableLabel };
package/dist/index.js CHANGED
@@ -42,6 +42,10 @@ var HttpClient = class {
42
42
  async post(path, body, options) {
43
43
  return this.request("POST", path, body, options);
44
44
  }
45
+ /** PUT replaces a whole resource, so it is idempotent; it is still only retried on network errors. */
46
+ async put(path, body, options) {
47
+ return this.request("PUT", path, body, options);
48
+ }
45
49
  async request(method, path, body, options) {
46
50
  const url = `${this.baseUrl}${path.startsWith("/") ? path : `/${path}`}`;
47
51
  const token = options?.token ?? await this.resolveToken();
@@ -130,6 +134,37 @@ var MitumbaLogistics = class {
130
134
  async getParcel(parcelId, options) {
131
135
  return this.http.get(`/logistics/parcels/${encodeURIComponent(parcelId)}`, options);
132
136
  }
137
+ /**
138
+ * Price each shop basket before any order exists. Origin is the shop (derived
139
+ * by the backend); destination is the buyer's saved address or a "Deliver to"
140
+ * point. Each available basket has one option per door method and one per
141
+ * nearby pickup hub; pass the picked option's `quote_id` per shop to checkout.
142
+ * An unservable basket comes back `unavailable` with a reason, not as an error.
143
+ * Requires the buyer's token.
144
+ */
145
+ async quoteBaskets(input, options) {
146
+ return this.http.post("/logistics/basket-quotes", input, options);
147
+ }
148
+ /** Seller: where a shop's parcels start. Fields are null until set. */
149
+ async getStoreOrigin(storeId, options) {
150
+ return this.http.get(`/logistics/stores/${encodeURIComponent(storeId)}/origin`, options);
151
+ }
152
+ /** Seller: replace a shop's dispatch point. */
153
+ async setStoreOrigin(storeId, input, options) {
154
+ return this.http.put(`/logistics/stores/${encodeURIComponent(storeId)}/origin`, input, options);
155
+ }
156
+ /** Seller: a listing's package size, or its category default when unset. */
157
+ async getListingPackage(listingId, options) {
158
+ return this.http.get(`/logistics/listings/${encodeURIComponent(listingId)}/package`, options);
159
+ }
160
+ /** Seller: set a listing's package size. */
161
+ async setListingPackage(listingId, packageSize, options) {
162
+ return this.http.put(
163
+ `/logistics/listings/${encodeURIComponent(listingId)}/package`,
164
+ { package_size: packageSize },
165
+ options
166
+ );
167
+ }
133
168
  };
134
169
 
135
170
  // src/polling.ts
@@ -177,6 +212,66 @@ function sleep(ms, signal) {
177
212
  });
178
213
  }
179
214
 
215
+ // src/delivery.ts
216
+ function methodChoices(basket) {
217
+ const byMethod = /* @__PURE__ */ new Map();
218
+ for (const option of basket.options) {
219
+ const options = byMethod.get(option.method) ?? [];
220
+ options.push(option);
221
+ byMethod.set(option.method, options);
222
+ }
223
+ return [...byMethod].map(([method, options]) => ({
224
+ method,
225
+ options,
226
+ cheapest: options.reduce((best, option) => option.price.minor_units < best.price.minor_units ? option : best),
227
+ needsHub: options.some((option) => option.hub !== void 0)
228
+ }));
229
+ }
230
+ var METHOD_LABELS = {
231
+ pickup_point: "Pickup point",
232
+ doorstep: "Door delivery",
233
+ same_day: "Same-day delivery",
234
+ branch_collection: "Branch collection"
235
+ };
236
+ function methodLabel(method, overrides = {}) {
237
+ return overrides[method] ?? METHOD_LABELS[method] ?? method.replace(/_/g, " ").replace(/^\w/, (c) => c.toUpperCase());
238
+ }
239
+ function unavailableLabel(reason, overrides = {}) {
240
+ const defaults = {
241
+ no_carrier_for_lane: "No delivery is available from this shop to your location yet.",
242
+ outside_market: "This shop does not deliver to that location.",
243
+ listing_unavailable: "An item from this shop is no longer available."
244
+ };
245
+ const key = reason ?? "";
246
+ return overrides[key] ?? defaults[key] ?? "Delivery is unavailable for this shop.";
247
+ }
248
+ function etaLabel(eta) {
249
+ if (eta.max_days <= 1 && eta.min_days === 0) return "Same day";
250
+ if (eta.min_days === eta.max_days) return `${eta.max_days} ${eta.max_days === 1 ? "day" : "days"}`;
251
+ return `${eta.min_days}\u2013${eta.max_days} days`;
252
+ }
253
+ function isQuoteExpired(option, now = /* @__PURE__ */ new Date()) {
254
+ return Date.parse(option.expires_at) <= now.getTime();
255
+ }
256
+ function checkoutDeliveries(baskets, picks) {
257
+ const missing = [];
258
+ const unavailable = [];
259
+ const deliveries = [];
260
+ for (const basket of baskets) {
261
+ if (basket.status !== "available" || basket.options.length === 0) {
262
+ unavailable.push(basket.store_id);
263
+ continue;
264
+ }
265
+ const quoteId = picks[basket.store_id];
266
+ if (!quoteId || !basket.options.some((option) => option.quote_id === quoteId)) {
267
+ missing.push(basket.store_id);
268
+ continue;
269
+ }
270
+ deliveries.push({ store_id: basket.store_id, quote_id: quoteId });
271
+ }
272
+ return missing.length === 0 && unavailable.length === 0 ? { ok: true, deliveries } : { ok: false, missing, unavailable };
273
+ }
274
+
180
275
  // src/index.ts
181
276
  export * from "@mitumbadotafrica/logistics-contract";
182
277
  export {
@@ -184,5 +279,11 @@ export {
184
279
  HttpClient,
185
280
  LogisticsError,
186
281
  MitumbaLogistics,
187
- pollParcel
282
+ checkoutDeliveries,
283
+ etaLabel,
284
+ isQuoteExpired,
285
+ methodChoices,
286
+ methodLabel,
287
+ pollParcel,
288
+ unavailableLabel
188
289
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mitumbadotafrica/logistics",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Asset-light carrier-orchestration SDK — carrier-neutral headless client for Kenyan couriers. Zero UI, zero carrier knowledge.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -49,7 +49,7 @@
49
49
  "clean": "rimraf dist .turbo"
50
50
  },
51
51
  "dependencies": {
52
- "@mitumbadotafrica/logistics-contract": "0.1.0"
52
+ "@mitumbadotafrica/logistics-contract": "0.2.0"
53
53
  },
54
54
  "devDependencies": {
55
55
  "rimraf": "^6.0.1",