@vivoa/partner-sdk 0.2.0 → 0.4.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.
@@ -32,6 +32,14 @@ export interface MerchantStatus {
32
32
  deployStatus: MerchantRuntimeStatus;
33
33
  domain: string | null;
34
34
  apiBaseUrl: string | null;
35
+ /** ISO-8601 when the store reached `live` (null before it provisions). */
36
+ provisionedAt: string | null;
37
+ /**
38
+ * Why the LAST provisioning attempt failed — surfaced only while
39
+ * `deployStatus === 'failed'`, null otherwise (a stale error never leaks once
40
+ * the store is live). The queryable "why" behind a failed activation.
41
+ */
42
+ lastError: string | null;
35
43
  }
36
44
  export interface CreateMerchantInput {
37
45
  /** Email of the store owner (receives their own merchant account). */
@@ -41,13 +49,20 @@ export interface CreateMerchantInput {
41
49
  countries: string[];
42
50
  industry?: string;
43
51
  /**
44
- * Your own reference for this merchant (customer id, order id…). Optional
45
- * here, but it is the idempotency key of `merchants.create()`: replaying the
46
- * same value returns the merchant you already created instead of a second
47
- * one. `provisionStore()` requires it for exactly that reason. Unique per
48
- * operator, max 120 chars.
52
+ * Your reference for the CLIENT who owns this store (your customer id). **Not
53
+ * unique**: one client can own several stores, all sharing this value — list
54
+ * them with `merchants.list({ externalId })`. This is NOT the idempotency key
55
+ * (see `idempotencyKey`). Max 120 chars.
49
56
  */
50
57
  externalId?: string;
58
+ /**
59
+ * Idempotency key for THIS creation (e.g. your order id). Replaying the same
60
+ * key returns the SAME merchant instead of creating a duplicate — safe
61
+ * retries. **Unique per operator**, max 120 chars. `provisionStore()`
62
+ * requires it for exactly that reason. Omit it and every call creates a new
63
+ * store.
64
+ */
65
+ idempotencyKey?: string;
51
66
  }
52
67
  /** Options for `merchants.adminTicket()`. */
53
68
  export interface AdminTicketOptions {
@@ -65,6 +80,130 @@ export interface AdminTicketOptions {
65
80
  export interface MerchantAdminTicket {
66
81
  redirectUrl: string;
67
82
  }
83
+ /** One plugin to install + configure on a merchant's store. */
84
+ export interface PluginProvisionInput {
85
+ /** Plugin code (e.g. `boxful`). */
86
+ code: string;
87
+ /** Global-scope config, merged into the plugin's existing config. */
88
+ global?: Record<string, unknown>;
89
+ /** Per-country config slices (credentials, pickup origin…), merged per country. */
90
+ countries?: Array<{
91
+ countryCode: string;
92
+ config: Record<string, unknown>;
93
+ }>;
94
+ /** Activate the plugin (and its countries) so the store is ready to use. */
95
+ activate?: boolean;
96
+ }
97
+ /** Outcome of `merchants.configurePlugins()` for one plugin. */
98
+ export interface PluginProvisionResult {
99
+ code: string;
100
+ installed: boolean;
101
+ configured: boolean;
102
+ error?: string;
103
+ }
104
+ /** Valid product type slugs (`physical` | `service` | `digital`). Unknown slugs fall back to `physical` store-side. */
105
+ export type ProductTypeSlug = 'physical' | 'service' | 'digital';
106
+ /**
107
+ * One inventory entry: stock by country (auto-creates a WH-XX location) or by an
108
+ * existing `locationCode`. `quantity` is a non-negative integer (the store
109
+ * rejects fractional stock).
110
+ */
111
+ export type InventoryEntry = {
112
+ country: string;
113
+ quantity: number;
114
+ locationCode?: never;
115
+ } | {
116
+ locationCode: string;
117
+ quantity: number;
118
+ country?: never;
119
+ };
120
+ /** One product to seed into a merchant's catalog. Mirrors the store import item. */
121
+ export interface ProvisionProduct {
122
+ /** Slug, kebab-case `[a-z0-9-]`. */
123
+ handle: string;
124
+ name: string;
125
+ /**
126
+ * Product type: `physical` (has variants, requires shipping), `service` (no
127
+ * variants, no shipping), `digital` (has variants, no shipping). Unknown
128
+ * values fall back to `physical` store-side — prefer being explicit.
129
+ */
130
+ productTypeSlug: ProductTypeSlug;
131
+ price: number;
132
+ description?: string;
133
+ vendor?: string;
134
+ tags?: string[];
135
+ costPrice?: number;
136
+ barcode?: string;
137
+ weight?: number;
138
+ /** Remote image URLs — validated + ingested store-side. Max 20. */
139
+ images?: string[];
140
+ /**
141
+ * Sales channel handles to publish this product on. Omit to publish on the
142
+ * store's default channel.
143
+ */
144
+ channels?: string[];
145
+ /** Per-country or per-location stock. Import locations before products when using `locationCode`. */
146
+ inventory?: InventoryEntry[];
147
+ }
148
+ /** One row that failed during a product import. Mirrors the store import error. */
149
+ export interface ImportProductError {
150
+ /** Zero-based index into the `products` array you sent. */
151
+ row: number;
152
+ /** Handle of the offending product, when the row carried one. */
153
+ handle?: string;
154
+ /** Machine code, e.g. `IMAGE_INVALID_URL`, `UNKNOWN_LOCATION`, `UNKNOWN_PRODUCT_TYPE`. */
155
+ code: string;
156
+ message: string;
157
+ /** Extra diagnostic payload, when the store attaches one. */
158
+ context?: Record<string, unknown>;
159
+ }
160
+ /** Aggregate result of `merchants.importProducts()`. */
161
+ export interface ImportProductsResult {
162
+ /** Rows received (== `products.length`, or the rows that passed validation). */
163
+ total: number;
164
+ created: number;
165
+ updated: number;
166
+ skipped: number;
167
+ /** Per-row failures. Empty on full success. */
168
+ errors: ImportProductError[];
169
+ }
170
+ /** One warehouse / pickup point to seed into a merchant's inventory. Upserted by `code`. */
171
+ export interface ProvisionLocation {
172
+ /** Your stable identifier — the upsert key (e.g. warehouse id). Max 60 chars. */
173
+ code: string;
174
+ /** Display name shown in the store admin. Max 120 chars. */
175
+ name: string;
176
+ /** ISO-3166-1 alpha-2 (e.g. `"SV"`, `"GT"`, `"HN"`). */
177
+ country: string;
178
+ /** Location type hint (e.g. `warehouse`, `pickup`, `shop`). */
179
+ type?: string;
180
+ address?: string;
181
+ city?: string;
182
+ postalCode?: string;
183
+ phone?: string;
184
+ email?: string;
185
+ isActive?: boolean;
186
+ /** Whether this location ships orders. Default `true`. */
187
+ handlesFulfillment?: boolean;
188
+ /** Whether customers can pick up here. Default `false`. */
189
+ pickupAvailable?: boolean;
190
+ /** Preferred carrier code for shipments dispatched from this location. */
191
+ preferredCarrier?: string;
192
+ latitude?: number;
193
+ longitude?: number;
194
+ referencePoint?: string;
195
+ }
196
+ /** Aggregate result of `merchants.importLocations()`. */
197
+ export interface ImportLocationsResult {
198
+ total: number;
199
+ created: number;
200
+ updated: number;
201
+ errors: Array<{
202
+ row: number;
203
+ code: string;
204
+ message: string;
205
+ }>;
206
+ }
68
207
  export interface ListMerchantsQuery {
69
208
  page?: number;
70
209
  /** Max 100. */
@@ -84,6 +223,8 @@ export interface Page<T> {
84
223
  }
85
224
  /** Max length the platform accepts for `CreateMerchantInput.externalId`. */
86
225
  export declare const MAX_EXTERNAL_ID_LENGTH = 120;
226
+ /** Max length the platform accepts for `CreateMerchantInput.idempotencyKey`. */
227
+ export declare const MAX_IDEMPOTENCY_KEY_LENGTH = 120;
87
228
  /** Runtime states where the platform accepts `activate` (first run or retry). */
88
229
  export declare function canActivate(runtime: MerchantRuntimeStatus): boolean;
89
230
  /** States that will not change without a new action from the partner. */
@@ -5,7 +5,7 @@
5
5
  * Pure: no I/O, no Node APIs.
6
6
  */
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
- exports.MAX_EXTERNAL_ID_LENGTH = exports.MERCHANT_RUNTIME_STATUSES = void 0;
8
+ exports.MAX_IDEMPOTENCY_KEY_LENGTH = exports.MAX_EXTERNAL_ID_LENGTH = exports.MERCHANT_RUNTIME_STATUSES = void 0;
9
9
  exports.canActivate = canActivate;
10
10
  exports.isSettled = isSettled;
11
11
  exports.MERCHANT_RUNTIME_STATUSES = [
@@ -18,6 +18,8 @@ exports.MERCHANT_RUNTIME_STATUSES = [
18
18
  ];
19
19
  /** Max length the platform accepts for `CreateMerchantInput.externalId`. */
20
20
  exports.MAX_EXTERNAL_ID_LENGTH = 120;
21
+ /** Max length the platform accepts for `CreateMerchantInput.idempotencyKey`. */
22
+ exports.MAX_IDEMPOTENCY_KEY_LENGTH = 120;
21
23
  /** Runtime states where the platform accepts `activate` (first run or retry). */
22
24
  function canActivate(runtime) {
23
25
  return runtime === 'not_activated' || runtime === 'failed';
@@ -1 +1 @@
1
- export declare const SDK_VERSION = "0.2.0";
1
+ export declare const SDK_VERSION = "0.4.0";
@@ -1,4 +1,4 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SDK_VERSION = void 0;
4
- exports.SDK_VERSION = '0.2.0';
4
+ exports.SDK_VERSION = '0.4.0';
@@ -61,6 +61,7 @@ export declare class FakeVivoa implements HttpTransport {
61
61
  private readonly operators;
62
62
  private readonly merchants;
63
63
  private readonly handles;
64
+ private readonly merchantLocations;
64
65
  constructor(options?: FakeVivoaOptions);
65
66
  addOperator(seed: FakeOperatorSeed): OperatorProfile;
66
67
  setOperatorStatus(apiKey: string, status: OperatorStatus): void;
@@ -3,7 +3,7 @@ import { nodeCrypto } from "../node-crypto.js";
3
3
  import { systemClock } from "../system-clock.js";
4
4
  import { VivoaNetworkError } from "../../domain/errors.js";
5
5
  import { canonicalRequestPayload, MAX_CLOCK_SKEW_SECONDS, PARTNER_HEADERS, } from "../../domain/signature.js";
6
- import { MAX_EXTERNAL_ID_LENGTH } from "../../domain/merchant.js";
6
+ import { MAX_EXTERNAL_ID_LENGTH, MAX_IDEMPOTENCY_KEY_LENGTH, } from "../../domain/merchant.js";
7
7
  import { MERCHANT_EVENT_TYPES } from "../../domain/webhook.js";
8
8
  class HttpError extends Error {
9
9
  status;
@@ -13,7 +13,7 @@ class HttpError extends Error {
13
13
  }
14
14
  }
15
15
  /** `POST /merchants` body — matches CreateMerchantDto's whitelist exactly (a stale `handle` 400s, like on the real platform). */
16
- const CREATE_MERCHANT_KEYS = new Set(['email', 'name', 'countries', 'industry', 'externalId']);
16
+ const CREATE_MERCHANT_KEYS = new Set(['email', 'name', 'countries', 'industry', 'externalId', 'idempotencyKey']);
17
17
  /** Mirrors `shared/utils/slugify.ts` on the platform closely enough for a fake. */
18
18
  function slugify(value) {
19
19
  return value
@@ -51,6 +51,7 @@ export class FakeVivoa {
51
51
  operators = new Map();
52
52
  merchants = new Map();
53
53
  handles = new Set();
54
+ merchantLocations = new Map();
54
55
  constructor(options = {}) {
55
56
  this.clock = options.clock ?? systemClock;
56
57
  this.crypto = options.crypto ?? nodeCrypto;
@@ -152,7 +153,7 @@ export class FakeVivoa {
152
153
  async route(op, method, url, rawBody) {
153
154
  const path = url.pathname.replace(/^\/partner\/v1/, '');
154
155
  const body = rawBody ? JSON.parse(rawBody) : {};
155
- const merchantRoute = path.match(/^\/merchants\/([^/]+)\/(status|activate|suspend|reactivate|admin-ticket)$/);
156
+ const merchantRoute = path.match(/^\/merchants\/([^/]+)\/(status|activate|suspend|reactivate|admin-ticket|plugins|locations|products)$/);
156
157
  if (method === 'GET' && path === '/me')
157
158
  return this.profile(op);
158
159
  if (method === 'POST' && path === '/me/rotate-secret') {
@@ -199,6 +200,38 @@ export class FakeVivoa {
199
200
  redirectUrl: `${base}/auth/admin-redeem?token=fake-${merchant.id}${returnPath === '/admin' ? '' : `&rp=${encodeURIComponent(returnPath)}`}`,
200
201
  };
201
202
  }
203
+ if (action === 'plugins') {
204
+ const plugins = Array.isArray(body.plugins) ? body.plugins : [];
205
+ return {
206
+ plugins: plugins.map((p) => ({
207
+ code: p.code,
208
+ installed: true,
209
+ configured: true,
210
+ })),
211
+ };
212
+ }
213
+ if (action === 'locations') {
214
+ const locations = Array.isArray(body.locations) ? body.locations : [];
215
+ if (locations.length === 0)
216
+ throw new HttpError(400, 'locations must not be empty');
217
+ const m = this.merchantLocations.get(merchant.id) ?? [];
218
+ let created = 0;
219
+ let updated = 0;
220
+ for (const loc of locations) {
221
+ if (m.some((l) => l === loc.code))
222
+ updated++;
223
+ else {
224
+ m.push(loc.code);
225
+ created++;
226
+ }
227
+ }
228
+ this.merchantLocations.set(merchant.id, m);
229
+ return { total: locations.length, created, updated, errors: [] };
230
+ }
231
+ if (action === 'products') {
232
+ const products = Array.isArray(body.products) ? body.products : [];
233
+ return { total: products.length, created: products.length, updated: 0, skipped: 0, errors: [] };
234
+ }
202
235
  }
203
236
  }
204
237
  if (method === 'GET' && path === '/fleet/analytics')
@@ -215,6 +248,7 @@ export class FakeVivoa {
215
248
  const name = String(body.name ?? '');
216
249
  const countries = Array.isArray(body.countries) ? body.countries.map(String) : [];
217
250
  const externalId = body.externalId === undefined ? undefined : String(body.externalId);
251
+ const idempotencyKey = body.idempotencyKey === undefined ? undefined : String(body.idempotencyKey);
218
252
  const problems = [];
219
253
  if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email))
220
254
  problems.push('email must be an email');
@@ -225,11 +259,17 @@ export class FakeVivoa {
225
259
  if (externalId !== undefined && externalId.length > MAX_EXTERNAL_ID_LENGTH) {
226
260
  problems.push(`externalId must be shorter than or equal to ${MAX_EXTERNAL_ID_LENGTH} characters`);
227
261
  }
262
+ if (idempotencyKey !== undefined && idempotencyKey.length > MAX_IDEMPOTENCY_KEY_LENGTH) {
263
+ problems.push(`idempotencyKey must be shorter than or equal to ${MAX_IDEMPOTENCY_KEY_LENGTH} characters`);
264
+ }
228
265
  if (problems.length)
229
266
  throw new HttpError(400, problems.join('; '));
230
- // Idempotent replay BEFORE quota/region checks — mirrors the platform.
231
- if (externalId) {
232
- const existing = this.fleet(op).find((m) => m.externalId === externalId);
267
+ // Idempotent replay BEFORE quota/region checks — mirrors the platform. The
268
+ // dedup key is idempotencyKey (unique per operator), NOT externalId: that is
269
+ // a non-unique client reference and may map to several of the operator's
270
+ // stores.
271
+ if (idempotencyKey) {
272
+ const existing = this.fleet(op).find((m) => m.idempotencyKey === idempotencyKey);
233
273
  if (existing)
234
274
  return publicMerchant(existing);
235
275
  }
@@ -254,6 +294,9 @@ export class FakeVivoa {
254
294
  createdAt: new Date(this.clock.nowMs()).toISOString(),
255
295
  operatorId: op.id,
256
296
  pollsLeft: 0,
297
+ idempotencyKey: idempotencyKey ?? null,
298
+ provisionedAt: null,
299
+ lastError: null,
257
300
  };
258
301
  this.handles.add(merchant.handle);
259
302
  this.merchants.set(merchant.id, merchant);
@@ -288,6 +331,7 @@ export class FakeVivoa {
288
331
  switch (this.activation) {
289
332
  case 'fail':
290
333
  m.runtime = 'failed';
334
+ m.lastError = this.failureReason;
291
335
  this.emit(op, 'merchant.failed', m, { reason: this.failureReason });
292
336
  throw new HttpError(500, this.failureReason);
293
337
  case 'slow':
@@ -306,6 +350,8 @@ export class FakeVivoa {
306
350
  goLive(m) {
307
351
  m.runtime = 'live';
308
352
  m.domain = `${m.handle}.vivoa.store`;
353
+ m.provisionedAt = new Date(this.clock.nowMs()).toISOString();
354
+ m.lastError = null;
309
355
  const op = [...this.operators.values()].find((o) => o.id === m.operatorId);
310
356
  if (op)
311
357
  this.emit(op, 'merchant.live', m);
@@ -324,6 +370,8 @@ export class FakeVivoa {
324
370
  deployStatus: m.runtime,
325
371
  domain: m.domain,
326
372
  apiBaseUrl: m.runtime === 'live' ? 'https://shared-api.vivoa.app' : null,
373
+ provisionedAt: m.provisionedAt,
374
+ lastError: m.runtime === 'failed' ? m.lastError : null,
327
375
  };
328
376
  }
329
377
  list(op, qs) {
@@ -453,7 +501,7 @@ export class FakeVivoa {
453
501
  }
454
502
  }
455
503
  function publicMerchant(m) {
456
- const { operatorId: _op, pollsLeft: _polls, ...view } = m;
504
+ const { operatorId: _op, pollsLeft: _polls, idempotencyKey: _key, provisionedAt: _prov, lastError: _err, ...view } = m;
457
505
  return { ...view, countries: [...view.countries] };
458
506
  }
459
507
  function webhookView(op) {
@@ -1,5 +1,5 @@
1
1
  import type { SignedHttpClient } from '../signed-http.ts';
2
- import { type AdminTicketOptions, type CreateMerchantInput, type ListMerchantsQuery, type Merchant, type MerchantAdminTicket, type MerchantStatus, type Page } from '../../domain/merchant.ts';
2
+ import { type AdminTicketOptions, type CreateMerchantInput, type ImportLocationsResult, type ImportProductsResult, type ListMerchantsQuery, type Merchant, type MerchantAdminTicket, type MerchantStatus, type Page, type PluginProvisionInput, type PluginProvisionResult, type ProvisionLocation, type ProvisionProduct } from '../../domain/merchant.ts';
3
3
  /** Provisioning is synchronous on the platform: allow it minutes, not seconds. */
4
4
  export declare const DEFAULT_ACTIVATE_TIMEOUT_MS: number;
5
5
  export declare class MerchantsResource {
@@ -7,8 +7,9 @@ export declare class MerchantsResource {
7
7
  constructor(http: SignedHttpClient);
8
8
  /**
9
9
  * Creates owner + store in your fleet. Not live until `activate`. The
10
- * platform assigns the handle. Pass `externalId` and replaying the same
10
+ * platform assigns the handle. Pass `idempotencyKey` and replaying the same
11
11
  * value returns the merchant you already created — never a second one.
12
+ * `externalId` is a non-unique client reference, not a dedup key.
12
13
  */
13
14
  create(input: CreateMerchantInput): Promise<Merchant>;
14
15
  list(query?: ListMerchantsQuery): Promise<Page<Merchant>>;
@@ -39,5 +40,26 @@ export declare class MerchantsResource {
39
40
  * not store it. 403 if the merchant is not in your fleet or is not active.
40
41
  */
41
42
  adminTicket(merchantId: string, options?: AdminTicketOptions): Promise<MerchantAdminTicket>;
43
+ /**
44
+ * Installs + configures plugins on the merchant's store (credentials, pickup
45
+ * origin, activation). Idempotent — config is merged, not replaced — so it
46
+ * doubles as a re-sync. 403 if the merchant is not in your fleet.
47
+ */
48
+ configurePlugins(merchantId: string, plugins: PluginProvisionInput[]): Promise<{
49
+ plugins: PluginProvisionResult[];
50
+ }>;
51
+ /**
52
+ * Bulk-imports inventory locations (warehouses, pickup points) into the
53
+ * merchant's store. Upserted on `code` — idempotent, re-running updates
54
+ * rather than duplicates. Import locations **before** products when you
55
+ * reference `inventory[].locationCode`. 409 if the merchant is not live.
56
+ */
57
+ importLocations(merchantId: string, locations: ProvisionLocation[]): Promise<ImportLocationsResult>;
58
+ /**
59
+ * Bulk-imports products into the merchant's catalog (URL images, per-country
60
+ * inventory). Idempotent on `handle`, so re-running updates rather than
61
+ * duplicates. 403 if the merchant is not in your fleet.
62
+ */
63
+ importProducts(merchantId: string, products: ProvisionProduct[]): Promise<ImportProductsResult>;
42
64
  }
43
65
  export declare function validateCreate(input: CreateMerchantInput): void;
@@ -1,5 +1,5 @@
1
1
  import { VivoaValidationError } from "../../domain/errors.js";
2
- import { MAX_EXTERNAL_ID_LENGTH, } from "../../domain/merchant.js";
2
+ import { MAX_EXTERNAL_ID_LENGTH, MAX_IDEMPOTENCY_KEY_LENGTH, } from "../../domain/merchant.js";
3
3
  const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
4
4
  const COUNTRY_RE = /^[A-Z]{2}$/;
5
5
  /** Provisioning is synchronous on the platform: allow it minutes, not seconds. */
@@ -11,8 +11,9 @@ export class MerchantsResource {
11
11
  }
12
12
  /**
13
13
  * Creates owner + store in your fleet. Not live until `activate`. The
14
- * platform assigns the handle. Pass `externalId` and replaying the same
14
+ * platform assigns the handle. Pass `idempotencyKey` and replaying the same
15
15
  * value returns the merchant you already created — never a second one.
16
+ * `externalId` is a non-unique client reference, not a dedup key.
16
17
  */
17
18
  async create(input) {
18
19
  validateCreate(input);
@@ -68,6 +69,43 @@ export class MerchantsResource {
68
69
  body: options.returnPath ? { returnPath: options.returnPath } : {},
69
70
  });
70
71
  }
72
+ /**
73
+ * Installs + configures plugins on the merchant's store (credentials, pickup
74
+ * origin, activation). Idempotent — config is merged, not replaced — so it
75
+ * doubles as a re-sync. 403 if the merchant is not in your fleet.
76
+ */
77
+ async configurePlugins(merchantId, plugins) {
78
+ if (plugins.length === 0)
79
+ throw new VivoaValidationError('plugins must not be empty');
80
+ return this.http.request('POST', `/partner/v1/merchants/${encodeId(merchantId)}/plugins`, {
81
+ body: { plugins },
82
+ });
83
+ }
84
+ /**
85
+ * Bulk-imports inventory locations (warehouses, pickup points) into the
86
+ * merchant's store. Upserted on `code` — idempotent, re-running updates
87
+ * rather than duplicates. Import locations **before** products when you
88
+ * reference `inventory[].locationCode`. 409 if the merchant is not live.
89
+ */
90
+ async importLocations(merchantId, locations) {
91
+ if (locations.length === 0)
92
+ throw new VivoaValidationError('locations must not be empty');
93
+ return this.http.request('POST', `/partner/v1/merchants/${encodeId(merchantId)}/locations`, {
94
+ body: { locations },
95
+ });
96
+ }
97
+ /**
98
+ * Bulk-imports products into the merchant's catalog (URL images, per-country
99
+ * inventory). Idempotent on `handle`, so re-running updates rather than
100
+ * duplicates. 403 if the merchant is not in your fleet.
101
+ */
102
+ async importProducts(merchantId, products) {
103
+ if (products.length === 0)
104
+ throw new VivoaValidationError('products must not be empty');
105
+ return this.http.request('POST', `/partner/v1/merchants/${encodeId(merchantId)}/products`, {
106
+ body: { products },
107
+ });
108
+ }
71
109
  }
72
110
  function encodeId(id) {
73
111
  if (!id)
@@ -93,6 +131,13 @@ export function validateCreate(input) {
93
131
  problems.push(`externalId must be at most ${MAX_EXTERNAL_ID_LENGTH} chars`);
94
132
  }
95
133
  }
134
+ if (input.idempotencyKey !== undefined) {
135
+ if (!input.idempotencyKey.trim())
136
+ problems.push('idempotencyKey must not be empty');
137
+ else if (input.idempotencyKey.length > MAX_IDEMPOTENCY_KEY_LENGTH) {
138
+ problems.push(`idempotencyKey must be at most ${MAX_IDEMPOTENCY_KEY_LENGTH} chars`);
139
+ }
140
+ }
96
141
  if (problems.length > 0)
97
142
  throw new VivoaValidationError(problems.join('; '));
98
143
  }
@@ -1,7 +1,7 @@
1
1
  import type { Clock } from '../../ports/clock.port.ts';
2
2
  import type { MerchantsResource } from '../resources/merchants.ts';
3
- import { type CreateMerchantInput, type Merchant, type MerchantRuntimeStatus, type MerchantStatus } from '../../domain/merchant.ts';
4
- export type ProvisionPhase = 'found' | 'created' | 'activating' | 'waiting' | 'settled';
3
+ import { type CreateMerchantInput, type ImportLocationsResult, type ImportProductsResult, type Merchant, type MerchantRuntimeStatus, type MerchantStatus, type PluginProvisionInput, type PluginProvisionResult, type ProvisionLocation, type ProvisionProduct } from '../../domain/merchant.ts';
4
+ export type ProvisionPhase = 'found' | 'created' | 'activating' | 'waiting' | 'seeding' | 'settled';
5
5
  export interface ProvisionProgress {
6
6
  phase: ProvisionPhase;
7
7
  merchantId: string;
@@ -18,22 +18,46 @@ export interface ProvisionStoreOptions {
18
18
  /** Status poll cadence while `creating`. Default 5 s. */
19
19
  pollIntervalMs?: number;
20
20
  onProgress?: (progress: ProvisionProgress) => void;
21
+ /**
22
+ * Plugins to install + configure once the store is live (credentials, pickup
23
+ * origin, activation). Best-effort: a live store with a failed seed still
24
+ * resolves — the result carries the error and the caller can re-run.
25
+ */
26
+ plugins?: PluginProvisionInput[];
27
+ /**
28
+ * Inventory locations (warehouses, pickup points) to import once the store
29
+ * is live. Seeded **before** products so `inventory[].locationCode`
30
+ * references resolve. Best-effort.
31
+ */
32
+ locations?: ProvisionLocation[];
33
+ /** Products to seed into the catalog once the store is live. Best-effort. */
34
+ products?: ProvisionProduct[];
21
35
  }
22
36
  export interface ProvisionStoreResult {
23
37
  merchant: Merchant;
24
38
  status: MerchantStatus;
25
- /** false when a merchant with this externalId already existed in your fleet. */
39
+ /**
40
+ * false when this call replayed a merchant already past `not_activated` (a
41
+ * definite prior run). A fresh create — or a replay of a merchant that never
42
+ * activated — reads `true`.
43
+ */
26
44
  created: boolean;
45
+ /** Per-plugin seed outcome, when `options.plugins` was supplied. */
46
+ plugins?: PluginProvisionResult[];
47
+ /** Location import outcome, when `options.locations` was supplied. */
48
+ locations?: ImportLocationsResult;
49
+ /** Catalog seed outcome, when `options.products` was supplied. */
50
+ products?: ImportProductsResult;
27
51
  }
28
52
  /**
29
53
  * "Give me a live store" — the one call most partners need.
30
54
  *
31
- * Requires `input.externalId`: it is what makes retrying safe. Re-running
32
- * after a crash, a timeout or a double click resumes the same merchant
33
- * instead of creating a second one — the platform is idempotent on it (see
34
- * `PARTNERS.md` §5.2), and it never runs out (unlike a handle, which the
35
- * platform now assigns and a partner never sees before creation). Never
36
- * retries a failed activation on its own.
55
+ * Requires `input.idempotencyKey`: it is what makes retrying safe. Re-running
56
+ * after a crash, a timeout or a double click resumes the same merchant instead
57
+ * of creating a second one — the platform is idempotent on it (see
58
+ * `PARTNERS.md` §5.2). `externalId` is a separate, non-unique client reference
59
+ * (one client may own several stores) and is NOT a dedup key. Never retries a
60
+ * failed activation on its own.
37
61
  */
38
62
  export declare class ProvisionStoreUseCase {
39
63
  private readonly merchants;