@vivoa/partner-sdk 0.2.0 → 0.3.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.
@@ -155,7 +155,7 @@ class FakeVivoa {
155
155
  async route(op, method, url, rawBody) {
156
156
  const path = url.pathname.replace(/^\/partner\/v1/, '');
157
157
  const body = rawBody ? JSON.parse(rawBody) : {};
158
- const merchantRoute = path.match(/^\/merchants\/([^/]+)\/(status|activate|suspend|reactivate|admin-ticket)$/);
158
+ const merchantRoute = path.match(/^\/merchants\/([^/]+)\/(status|activate|suspend|reactivate|admin-ticket|plugins|products)$/);
159
159
  if (method === 'GET' && path === '/me')
160
160
  return this.profile(op);
161
161
  if (method === 'POST' && path === '/me/rotate-secret') {
@@ -202,6 +202,20 @@ class FakeVivoa {
202
202
  redirectUrl: `${base}/auth/admin-redeem?token=fake-${merchant.id}${returnPath === '/admin' ? '' : `&rp=${encodeURIComponent(returnPath)}`}`,
203
203
  };
204
204
  }
205
+ if (action === 'plugins') {
206
+ const plugins = Array.isArray(body.plugins) ? body.plugins : [];
207
+ return {
208
+ plugins: plugins.map((p) => ({
209
+ code: p.code,
210
+ installed: true,
211
+ configured: true,
212
+ })),
213
+ };
214
+ }
215
+ if (action === 'products') {
216
+ const products = Array.isArray(body.products) ? body.products : [];
217
+ return { created: products.length, updated: 0, skipped: 0, failed: 0 };
218
+ }
205
219
  }
206
220
  }
207
221
  if (method === 'GET' && path === '/fleet/analytics')
@@ -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 ImportProductsResult, type ListMerchantsQuery, type Merchant, type MerchantAdminTicket, type MerchantStatus, type Page, type PluginProvisionInput, type PluginProvisionResult, 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 {
@@ -39,5 +39,19 @@ export declare class MerchantsResource {
39
39
  * not store it. 403 if the merchant is not in your fleet or is not active.
40
40
  */
41
41
  adminTicket(merchantId: string, options?: AdminTicketOptions): Promise<MerchantAdminTicket>;
42
+ /**
43
+ * Installs + configures plugins on the merchant's store (credentials, pickup
44
+ * origin, activation). Idempotent — config is merged, not replaced — so it
45
+ * doubles as a re-sync. 403 if the merchant is not in your fleet.
46
+ */
47
+ configurePlugins(merchantId: string, plugins: PluginProvisionInput[]): Promise<{
48
+ plugins: PluginProvisionResult[];
49
+ }>;
50
+ /**
51
+ * Bulk-imports products into the merchant's catalog (URL images, per-country
52
+ * inventory). Idempotent on `handle`, so re-running updates rather than
53
+ * duplicates. 403 if the merchant is not in your fleet.
54
+ */
55
+ importProducts(merchantId: string, products: ProvisionProduct[]): Promise<ImportProductsResult>;
42
56
  }
43
57
  export declare function validateCreate(input: CreateMerchantInput): void;
@@ -72,6 +72,30 @@ class MerchantsResource {
72
72
  body: options.returnPath ? { returnPath: options.returnPath } : {},
73
73
  });
74
74
  }
75
+ /**
76
+ * Installs + configures plugins on the merchant's store (credentials, pickup
77
+ * origin, activation). Idempotent — config is merged, not replaced — so it
78
+ * doubles as a re-sync. 403 if the merchant is not in your fleet.
79
+ */
80
+ async configurePlugins(merchantId, plugins) {
81
+ if (plugins.length === 0)
82
+ throw new errors_ts_1.VivoaValidationError('plugins must not be empty');
83
+ return this.http.request('POST', `/partner/v1/merchants/${encodeId(merchantId)}/plugins`, {
84
+ body: { plugins },
85
+ });
86
+ }
87
+ /**
88
+ * Bulk-imports products into the merchant's catalog (URL images, per-country
89
+ * inventory). Idempotent on `handle`, so re-running updates rather than
90
+ * duplicates. 403 if the merchant is not in your fleet.
91
+ */
92
+ async importProducts(merchantId, products) {
93
+ if (products.length === 0)
94
+ throw new errors_ts_1.VivoaValidationError('products must not be empty');
95
+ return this.http.request('POST', `/partner/v1/merchants/${encodeId(merchantId)}/products`, {
96
+ body: { products },
97
+ });
98
+ }
75
99
  }
76
100
  exports.MerchantsResource = MerchantsResource;
77
101
  function encodeId(id) {
@@ -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 ImportProductsResult, type Merchant, type MerchantRuntimeStatus, type MerchantStatus, type PluginProvisionInput, type PluginProvisionResult, 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,12 +18,24 @@ 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
+ /** Products to seed into the catalog once the store is live. Best-effort. */
28
+ products?: ProvisionProduct[];
21
29
  }
22
30
  export interface ProvisionStoreResult {
23
31
  merchant: Merchant;
24
32
  status: MerchantStatus;
25
33
  /** false when a merchant with this externalId already existed in your fleet. */
26
34
  created: boolean;
35
+ /** Per-plugin seed outcome, when `options.plugins` was supplied. */
36
+ plugins?: PluginProvisionResult[];
37
+ /** Catalog seed outcome, when `options.products` was supplied. */
38
+ products?: ImportProductsResult;
27
39
  }
28
40
  /**
29
41
  * "Give me a live store" — the one call most partners need.
@@ -45,11 +45,43 @@ class ProvisionStoreUseCase {
45
45
  pollIntervalMs: options.pollIntervalMs,
46
46
  });
47
47
  }
48
- progress('settled', merchant, status.deployStatus);
49
48
  if (status.deployStatus === 'failed' || status.deployStatus === 'unknown') {
49
+ progress('settled', merchant, status.deployStatus);
50
50
  throw new errors_ts_1.VivoaProvisioningFailedError(merchant.id, `the platform reported runtime status "${status.deployStatus}"`);
51
51
  }
52
- return { merchant: { ...merchant, runtime: status.deployStatus, domain: status.domain }, status, created };
52
+ // Seed plugins + catalog once the store is live. Best-effort by design: a
53
+ // failed seed leaves the store live and reports the error, so the caller can
54
+ // re-run (`configurePlugins`/`importProducts` are idempotent) rather than
55
+ // losing a provisioned store to a transient catalog/credential hiccup.
56
+ let plugins;
57
+ let products;
58
+ if (options.plugins?.length || options.products?.length) {
59
+ progress('seeding', merchant, status.deployStatus);
60
+ if (options.plugins?.length) {
61
+ plugins = await this.merchants
62
+ .configurePlugins(merchant.id, options.plugins)
63
+ .then((r) => r.plugins)
64
+ .catch((err) => options.plugins.map((p) => ({
65
+ code: p.code,
66
+ installed: false,
67
+ configured: false,
68
+ error: err instanceof Error ? err.message : String(err),
69
+ })));
70
+ }
71
+ if (options.products?.length) {
72
+ products = await this.merchants
73
+ .importProducts(merchant.id, options.products)
74
+ .catch(() => ({ created: 0, updated: 0, skipped: 0, failed: options.products.length }));
75
+ }
76
+ }
77
+ progress('settled', merchant, status.deployStatus);
78
+ return {
79
+ merchant: { ...merchant, runtime: status.deployStatus, domain: status.domain },
80
+ status,
81
+ created,
82
+ ...(plugins ? { plugins } : {}),
83
+ ...(products ? { products } : {}),
84
+ };
53
85
  }
54
86
  /** Polls `GET /status` until the runtime leaves `creating`. */
55
87
  async waitUntilSettled(merchantId, options = {}) {
@@ -65,6 +65,56 @@ export interface AdminTicketOptions {
65
65
  export interface MerchantAdminTicket {
66
66
  redirectUrl: string;
67
67
  }
68
+ /** One plugin to install + configure on a merchant's store. */
69
+ export interface PluginProvisionInput {
70
+ /** Plugin code (e.g. `boxful`). */
71
+ code: string;
72
+ /** Global-scope config, merged into the plugin's existing config. */
73
+ global?: Record<string, unknown>;
74
+ /** Per-country config slices (credentials, pickup origin…), merged per country. */
75
+ countries?: Array<{
76
+ countryCode: string;
77
+ config: Record<string, unknown>;
78
+ }>;
79
+ /** Activate the plugin (and its countries) so the store is ready to use. */
80
+ activate?: boolean;
81
+ }
82
+ /** Outcome of `merchants.configurePlugins()` for one plugin. */
83
+ export interface PluginProvisionResult {
84
+ code: string;
85
+ installed: boolean;
86
+ configured: boolean;
87
+ error?: string;
88
+ }
89
+ /** One product to seed into a merchant's catalog. Mirrors the store import item. */
90
+ export interface ProvisionProduct {
91
+ /** Slug, kebab-case `[a-z0-9-]`. */
92
+ handle: string;
93
+ name: string;
94
+ /** Product type slug the store catalog knows (e.g. `default`). */
95
+ productTypeSlug: string;
96
+ price: number;
97
+ description?: string;
98
+ vendor?: string;
99
+ tags?: string[];
100
+ costPrice?: number;
101
+ barcode?: string;
102
+ weight?: number;
103
+ /** Remote image URLs — validated + ingested store-side. */
104
+ images?: string[];
105
+ /** Per-country stock. */
106
+ inventory?: Array<{
107
+ country: string;
108
+ quantity: number;
109
+ }>;
110
+ }
111
+ /** Aggregate result of `merchants.importProducts()`. */
112
+ export interface ImportProductsResult {
113
+ created: number;
114
+ updated: number;
115
+ skipped: number;
116
+ failed: number;
117
+ }
68
118
  export interface ListMerchantsQuery {
69
119
  page?: number;
70
120
  /** Max 100. */
@@ -1 +1 @@
1
- export declare const SDK_VERSION = "0.2.0";
1
+ export declare const SDK_VERSION = "0.3.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.3.0';
@@ -152,7 +152,7 @@ export class FakeVivoa {
152
152
  async route(op, method, url, rawBody) {
153
153
  const path = url.pathname.replace(/^\/partner\/v1/, '');
154
154
  const body = rawBody ? JSON.parse(rawBody) : {};
155
- const merchantRoute = path.match(/^\/merchants\/([^/]+)\/(status|activate|suspend|reactivate|admin-ticket)$/);
155
+ const merchantRoute = path.match(/^\/merchants\/([^/]+)\/(status|activate|suspend|reactivate|admin-ticket|plugins|products)$/);
156
156
  if (method === 'GET' && path === '/me')
157
157
  return this.profile(op);
158
158
  if (method === 'POST' && path === '/me/rotate-secret') {
@@ -199,6 +199,20 @@ export class FakeVivoa {
199
199
  redirectUrl: `${base}/auth/admin-redeem?token=fake-${merchant.id}${returnPath === '/admin' ? '' : `&rp=${encodeURIComponent(returnPath)}`}`,
200
200
  };
201
201
  }
202
+ if (action === 'plugins') {
203
+ const plugins = Array.isArray(body.plugins) ? body.plugins : [];
204
+ return {
205
+ plugins: plugins.map((p) => ({
206
+ code: p.code,
207
+ installed: true,
208
+ configured: true,
209
+ })),
210
+ };
211
+ }
212
+ if (action === 'products') {
213
+ const products = Array.isArray(body.products) ? body.products : [];
214
+ return { created: products.length, updated: 0, skipped: 0, failed: 0 };
215
+ }
202
216
  }
203
217
  }
204
218
  if (method === 'GET' && path === '/fleet/analytics')
@@ -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 ImportProductsResult, type ListMerchantsQuery, type Merchant, type MerchantAdminTicket, type MerchantStatus, type Page, type PluginProvisionInput, type PluginProvisionResult, 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 {
@@ -39,5 +39,19 @@ export declare class MerchantsResource {
39
39
  * not store it. 403 if the merchant is not in your fleet or is not active.
40
40
  */
41
41
  adminTicket(merchantId: string, options?: AdminTicketOptions): Promise<MerchantAdminTicket>;
42
+ /**
43
+ * Installs + configures plugins on the merchant's store (credentials, pickup
44
+ * origin, activation). Idempotent — config is merged, not replaced — so it
45
+ * doubles as a re-sync. 403 if the merchant is not in your fleet.
46
+ */
47
+ configurePlugins(merchantId: string, plugins: PluginProvisionInput[]): Promise<{
48
+ plugins: PluginProvisionResult[];
49
+ }>;
50
+ /**
51
+ * Bulk-imports products into the merchant's catalog (URL images, per-country
52
+ * inventory). Idempotent on `handle`, so re-running updates rather than
53
+ * duplicates. 403 if the merchant is not in your fleet.
54
+ */
55
+ importProducts(merchantId: string, products: ProvisionProduct[]): Promise<ImportProductsResult>;
42
56
  }
43
57
  export declare function validateCreate(input: CreateMerchantInput): void;
@@ -68,6 +68,30 @@ export class MerchantsResource {
68
68
  body: options.returnPath ? { returnPath: options.returnPath } : {},
69
69
  });
70
70
  }
71
+ /**
72
+ * Installs + configures plugins on the merchant's store (credentials, pickup
73
+ * origin, activation). Idempotent — config is merged, not replaced — so it
74
+ * doubles as a re-sync. 403 if the merchant is not in your fleet.
75
+ */
76
+ async configurePlugins(merchantId, plugins) {
77
+ if (plugins.length === 0)
78
+ throw new VivoaValidationError('plugins must not be empty');
79
+ return this.http.request('POST', `/partner/v1/merchants/${encodeId(merchantId)}/plugins`, {
80
+ body: { plugins },
81
+ });
82
+ }
83
+ /**
84
+ * Bulk-imports products into the merchant's catalog (URL images, per-country
85
+ * inventory). Idempotent on `handle`, so re-running updates rather than
86
+ * duplicates. 403 if the merchant is not in your fleet.
87
+ */
88
+ async importProducts(merchantId, products) {
89
+ if (products.length === 0)
90
+ throw new VivoaValidationError('products must not be empty');
91
+ return this.http.request('POST', `/partner/v1/merchants/${encodeId(merchantId)}/products`, {
92
+ body: { products },
93
+ });
94
+ }
71
95
  }
72
96
  function encodeId(id) {
73
97
  if (!id)
@@ -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 ImportProductsResult, type Merchant, type MerchantRuntimeStatus, type MerchantStatus, type PluginProvisionInput, type PluginProvisionResult, 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,12 +18,24 @@ 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
+ /** Products to seed into the catalog once the store is live. Best-effort. */
28
+ products?: ProvisionProduct[];
21
29
  }
22
30
  export interface ProvisionStoreResult {
23
31
  merchant: Merchant;
24
32
  status: MerchantStatus;
25
33
  /** false when a merchant with this externalId already existed in your fleet. */
26
34
  created: boolean;
35
+ /** Per-plugin seed outcome, when `options.plugins` was supplied. */
36
+ plugins?: PluginProvisionResult[];
37
+ /** Catalog seed outcome, when `options.products` was supplied. */
38
+ products?: ImportProductsResult;
27
39
  }
28
40
  /**
29
41
  * "Give me a live store" — the one call most partners need.
@@ -42,11 +42,43 @@ export class ProvisionStoreUseCase {
42
42
  pollIntervalMs: options.pollIntervalMs,
43
43
  });
44
44
  }
45
- progress('settled', merchant, status.deployStatus);
46
45
  if (status.deployStatus === 'failed' || status.deployStatus === 'unknown') {
46
+ progress('settled', merchant, status.deployStatus);
47
47
  throw new VivoaProvisioningFailedError(merchant.id, `the platform reported runtime status "${status.deployStatus}"`);
48
48
  }
49
- return { merchant: { ...merchant, runtime: status.deployStatus, domain: status.domain }, status, created };
49
+ // Seed plugins + catalog once the store is live. Best-effort by design: a
50
+ // failed seed leaves the store live and reports the error, so the caller can
51
+ // re-run (`configurePlugins`/`importProducts` are idempotent) rather than
52
+ // losing a provisioned store to a transient catalog/credential hiccup.
53
+ let plugins;
54
+ let products;
55
+ if (options.plugins?.length || options.products?.length) {
56
+ progress('seeding', merchant, status.deployStatus);
57
+ if (options.plugins?.length) {
58
+ plugins = await this.merchants
59
+ .configurePlugins(merchant.id, options.plugins)
60
+ .then((r) => r.plugins)
61
+ .catch((err) => options.plugins.map((p) => ({
62
+ code: p.code,
63
+ installed: false,
64
+ configured: false,
65
+ error: err instanceof Error ? err.message : String(err),
66
+ })));
67
+ }
68
+ if (options.products?.length) {
69
+ products = await this.merchants
70
+ .importProducts(merchant.id, options.products)
71
+ .catch(() => ({ created: 0, updated: 0, skipped: 0, failed: options.products.length }));
72
+ }
73
+ }
74
+ progress('settled', merchant, status.deployStatus);
75
+ return {
76
+ merchant: { ...merchant, runtime: status.deployStatus, domain: status.domain },
77
+ status,
78
+ created,
79
+ ...(plugins ? { plugins } : {}),
80
+ ...(products ? { products } : {}),
81
+ };
50
82
  }
51
83
  /** Polls `GET /status` until the runtime leaves `creating`. */
52
84
  async waitUntilSettled(merchantId, options = {}) {
@@ -65,6 +65,56 @@ export interface AdminTicketOptions {
65
65
  export interface MerchantAdminTicket {
66
66
  redirectUrl: string;
67
67
  }
68
+ /** One plugin to install + configure on a merchant's store. */
69
+ export interface PluginProvisionInput {
70
+ /** Plugin code (e.g. `boxful`). */
71
+ code: string;
72
+ /** Global-scope config, merged into the plugin's existing config. */
73
+ global?: Record<string, unknown>;
74
+ /** Per-country config slices (credentials, pickup origin…), merged per country. */
75
+ countries?: Array<{
76
+ countryCode: string;
77
+ config: Record<string, unknown>;
78
+ }>;
79
+ /** Activate the plugin (and its countries) so the store is ready to use. */
80
+ activate?: boolean;
81
+ }
82
+ /** Outcome of `merchants.configurePlugins()` for one plugin. */
83
+ export interface PluginProvisionResult {
84
+ code: string;
85
+ installed: boolean;
86
+ configured: boolean;
87
+ error?: string;
88
+ }
89
+ /** One product to seed into a merchant's catalog. Mirrors the store import item. */
90
+ export interface ProvisionProduct {
91
+ /** Slug, kebab-case `[a-z0-9-]`. */
92
+ handle: string;
93
+ name: string;
94
+ /** Product type slug the store catalog knows (e.g. `default`). */
95
+ productTypeSlug: string;
96
+ price: number;
97
+ description?: string;
98
+ vendor?: string;
99
+ tags?: string[];
100
+ costPrice?: number;
101
+ barcode?: string;
102
+ weight?: number;
103
+ /** Remote image URLs — validated + ingested store-side. */
104
+ images?: string[];
105
+ /** Per-country stock. */
106
+ inventory?: Array<{
107
+ country: string;
108
+ quantity: number;
109
+ }>;
110
+ }
111
+ /** Aggregate result of `merchants.importProducts()`. */
112
+ export interface ImportProductsResult {
113
+ created: number;
114
+ updated: number;
115
+ skipped: number;
116
+ failed: number;
117
+ }
68
118
  export interface ListMerchantsQuery {
69
119
  page?: number;
70
120
  /** Max 100. */
@@ -1 +1 @@
1
- export declare const SDK_VERSION = "0.2.0";
1
+ export declare const SDK_VERSION = "0.3.0";
@@ -1 +1 @@
1
- export const SDK_VERSION = '0.2.0';
1
+ export const SDK_VERSION = '0.3.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vivoa/partner-sdk",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Official SDK for the Vivoa Partner API — create and operate stores for your merchants",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",