@vivoa/partner-sdk 0.1.1 → 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.
- package/dist/cjs/adapters/in-memory/fake-vivoa.js +25 -1
- package/dist/cjs/application/resources/merchants.d.ts +21 -1
- package/dist/cjs/application/resources/merchants.js +34 -0
- package/dist/cjs/application/use-cases/provision-store.d.ts +14 -2
- package/dist/cjs/application/use-cases/provision-store.js +34 -2
- package/dist/cjs/domain/merchant.d.ts +66 -0
- package/dist/cjs/version.d.ts +1 -1
- package/dist/cjs/version.js +1 -1
- package/dist/esm/adapters/in-memory/fake-vivoa.js +25 -1
- package/dist/esm/application/resources/merchants.d.ts +21 -1
- package/dist/esm/application/resources/merchants.js +34 -0
- package/dist/esm/application/use-cases/provision-store.d.ts +14 -2
- package/dist/esm/application/use-cases/provision-store.js +34 -2
- package/dist/esm/cli.js +6 -0
- package/dist/esm/domain/merchant.d.ts +66 -0
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/package.json +1 -1
|
@@ -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)$/);
|
|
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') {
|
|
@@ -192,6 +192,30 @@ class FakeVivoa {
|
|
|
192
192
|
this.emit(op, 'merchant.reactivated', merchant);
|
|
193
193
|
return { storeId: merchant.id, status: 'active' };
|
|
194
194
|
}
|
|
195
|
+
if (action === 'admin-ticket') {
|
|
196
|
+
if (merchant.status !== 'active')
|
|
197
|
+
throw new HttpError(403, 'Store is not active');
|
|
198
|
+
const rawPath = typeof body.returnPath === 'string' ? body.returnPath : '/admin';
|
|
199
|
+
const returnPath = rawPath.startsWith('/') && !rawPath.startsWith('//') ? rawPath : '/admin';
|
|
200
|
+
const base = merchant.domain ? `https://${merchant.domain}` : `https://${merchant.handle}.vivoa.test`;
|
|
201
|
+
return {
|
|
202
|
+
redirectUrl: `${base}/auth/admin-redeem?token=fake-${merchant.id}${returnPath === '/admin' ? '' : `&rp=${encodeURIComponent(returnPath)}`}`,
|
|
203
|
+
};
|
|
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
|
+
}
|
|
195
219
|
}
|
|
196
220
|
}
|
|
197
221
|
if (method === 'GET' && path === '/fleet/analytics')
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SignedHttpClient } from '../signed-http.ts';
|
|
2
|
-
import { type CreateMerchantInput, type ListMerchantsQuery, type Merchant, 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 {
|
|
@@ -33,5 +33,25 @@ export declare class MerchantsResource {
|
|
|
33
33
|
storeId: string;
|
|
34
34
|
status: 'active';
|
|
35
35
|
}>;
|
|
36
|
+
/**
|
|
37
|
+
* Mints a single-use SSO URL into this merchant's store admin, signed in AS
|
|
38
|
+
* the store owner. The link expires in ~60s — redirect to it right away, do
|
|
39
|
+
* not store it. 403 if the merchant is not in your fleet or is not active.
|
|
40
|
+
*/
|
|
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>;
|
|
36
56
|
}
|
|
37
57
|
export declare function validateCreate(input: CreateMerchantInput): void;
|
|
@@ -62,6 +62,40 @@ class MerchantsResource {
|
|
|
62
62
|
async reactivate(merchantId) {
|
|
63
63
|
return this.http.request('POST', `/partner/v1/merchants/${encodeId(merchantId)}/reactivate`);
|
|
64
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* Mints a single-use SSO URL into this merchant's store admin, signed in AS
|
|
67
|
+
* the store owner. The link expires in ~60s — redirect to it right away, do
|
|
68
|
+
* not store it. 403 if the merchant is not in your fleet or is not active.
|
|
69
|
+
*/
|
|
70
|
+
async adminTicket(merchantId, options = {}) {
|
|
71
|
+
return this.http.request('POST', `/partner/v1/merchants/${encodeId(merchantId)}/admin-ticket`, {
|
|
72
|
+
body: options.returnPath ? { returnPath: options.returnPath } : {},
|
|
73
|
+
});
|
|
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
|
+
}
|
|
65
99
|
}
|
|
66
100
|
exports.MerchantsResource = MerchantsResource;
|
|
67
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
|
-
|
|
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 = {}) {
|
|
@@ -49,6 +49,72 @@ export interface CreateMerchantInput {
|
|
|
49
49
|
*/
|
|
50
50
|
externalId?: string;
|
|
51
51
|
}
|
|
52
|
+
/** Options for `merchants.adminTicket()`. */
|
|
53
|
+
export interface AdminTicketOptions {
|
|
54
|
+
/**
|
|
55
|
+
* Same-origin absolute path to land on inside the store admin (defaults to
|
|
56
|
+
* `/admin`). Must start with a single `/` — the platform re-validates and
|
|
57
|
+
* falls back to `/admin` on anything scheme-relative or malformed.
|
|
58
|
+
*/
|
|
59
|
+
returnPath?: string;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Single-use SSO handoff into a merchant's store admin. `redirectUrl` is signed
|
|
63
|
+
* in AS the store owner and expires in ~60s — open it immediately, never cache.
|
|
64
|
+
*/
|
|
65
|
+
export interface MerchantAdminTicket {
|
|
66
|
+
redirectUrl: string;
|
|
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
|
+
}
|
|
52
118
|
export interface ListMerchantsQuery {
|
|
53
119
|
page?: number;
|
|
54
120
|
/** Max 100. */
|
package/dist/cjs/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const SDK_VERSION = "0.
|
|
1
|
+
export declare const SDK_VERSION = "0.3.0";
|
package/dist/cjs/version.js
CHANGED
|
@@ -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)$/);
|
|
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') {
|
|
@@ -189,6 +189,30 @@ export class FakeVivoa {
|
|
|
189
189
|
this.emit(op, 'merchant.reactivated', merchant);
|
|
190
190
|
return { storeId: merchant.id, status: 'active' };
|
|
191
191
|
}
|
|
192
|
+
if (action === 'admin-ticket') {
|
|
193
|
+
if (merchant.status !== 'active')
|
|
194
|
+
throw new HttpError(403, 'Store is not active');
|
|
195
|
+
const rawPath = typeof body.returnPath === 'string' ? body.returnPath : '/admin';
|
|
196
|
+
const returnPath = rawPath.startsWith('/') && !rawPath.startsWith('//') ? rawPath : '/admin';
|
|
197
|
+
const base = merchant.domain ? `https://${merchant.domain}` : `https://${merchant.handle}.vivoa.test`;
|
|
198
|
+
return {
|
|
199
|
+
redirectUrl: `${base}/auth/admin-redeem?token=fake-${merchant.id}${returnPath === '/admin' ? '' : `&rp=${encodeURIComponent(returnPath)}`}`,
|
|
200
|
+
};
|
|
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
|
+
}
|
|
192
216
|
}
|
|
193
217
|
}
|
|
194
218
|
if (method === 'GET' && path === '/fleet/analytics')
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SignedHttpClient } from '../signed-http.ts';
|
|
2
|
-
import { type CreateMerchantInput, type ListMerchantsQuery, type Merchant, 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 {
|
|
@@ -33,5 +33,25 @@ export declare class MerchantsResource {
|
|
|
33
33
|
storeId: string;
|
|
34
34
|
status: 'active';
|
|
35
35
|
}>;
|
|
36
|
+
/**
|
|
37
|
+
* Mints a single-use SSO URL into this merchant's store admin, signed in AS
|
|
38
|
+
* the store owner. The link expires in ~60s — redirect to it right away, do
|
|
39
|
+
* not store it. 403 if the merchant is not in your fleet or is not active.
|
|
40
|
+
*/
|
|
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>;
|
|
36
56
|
}
|
|
37
57
|
export declare function validateCreate(input: CreateMerchantInput): void;
|
|
@@ -58,6 +58,40 @@ export class MerchantsResource {
|
|
|
58
58
|
async reactivate(merchantId) {
|
|
59
59
|
return this.http.request('POST', `/partner/v1/merchants/${encodeId(merchantId)}/reactivate`);
|
|
60
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Mints a single-use SSO URL into this merchant's store admin, signed in AS
|
|
63
|
+
* the store owner. The link expires in ~60s — redirect to it right away, do
|
|
64
|
+
* not store it. 403 if the merchant is not in your fleet or is not active.
|
|
65
|
+
*/
|
|
66
|
+
async adminTicket(merchantId, options = {}) {
|
|
67
|
+
return this.http.request('POST', `/partner/v1/merchants/${encodeId(merchantId)}/admin-ticket`, {
|
|
68
|
+
body: options.returnPath ? { returnPath: options.returnPath } : {},
|
|
69
|
+
});
|
|
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
|
+
}
|
|
61
95
|
}
|
|
62
96
|
function encodeId(id) {
|
|
63
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
|
-
|
|
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 = {}) {
|
package/dist/esm/cli.js
CHANGED
|
@@ -13,6 +13,7 @@ const USAGE = `vivoa-partner <command> [options]
|
|
|
13
13
|
merchants list [--search s] [--external-id id] [--page n] [--limit n]
|
|
14
14
|
merchants status <id>
|
|
15
15
|
merchants suspend <id> | reactivate <id>
|
|
16
|
+
merchants admin-ticket <id> [--return-path /admin/team]
|
|
16
17
|
store create --external-id id --name n --email e --countries SV,GT [--industry i] --yes
|
|
17
18
|
Create (or resume) + activate + wait until live.
|
|
18
19
|
The platform assigns the handle; --external-id is
|
|
@@ -36,6 +37,7 @@ async function main(argv) {
|
|
|
36
37
|
industry: { type: 'string' },
|
|
37
38
|
url: { type: 'string' },
|
|
38
39
|
events: { type: 'string' },
|
|
40
|
+
'return-path': { type: 'string' },
|
|
39
41
|
yes: { type: 'boolean', default: false },
|
|
40
42
|
help: { type: 'boolean', short: 'h', default: false },
|
|
41
43
|
},
|
|
@@ -69,6 +71,10 @@ async function main(argv) {
|
|
|
69
71
|
return client.merchants.suspend(required(arg, '<id>'));
|
|
70
72
|
case 'merchants reactivate':
|
|
71
73
|
return client.merchants.reactivate(required(arg, '<id>'));
|
|
74
|
+
case 'merchants admin-ticket':
|
|
75
|
+
return client.merchants.adminTicket(required(arg, '<id>'), {
|
|
76
|
+
returnPath: values['return-path'],
|
|
77
|
+
});
|
|
72
78
|
case 'store create': {
|
|
73
79
|
if (!values.yes)
|
|
74
80
|
throw new VivoaError('store create provisions REAL infrastructure — re-run with --yes');
|
|
@@ -49,6 +49,72 @@ export interface CreateMerchantInput {
|
|
|
49
49
|
*/
|
|
50
50
|
externalId?: string;
|
|
51
51
|
}
|
|
52
|
+
/** Options for `merchants.adminTicket()`. */
|
|
53
|
+
export interface AdminTicketOptions {
|
|
54
|
+
/**
|
|
55
|
+
* Same-origin absolute path to land on inside the store admin (defaults to
|
|
56
|
+
* `/admin`). Must start with a single `/` — the platform re-validates and
|
|
57
|
+
* falls back to `/admin` on anything scheme-relative or malformed.
|
|
58
|
+
*/
|
|
59
|
+
returnPath?: string;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Single-use SSO handoff into a merchant's store admin. `redirectUrl` is signed
|
|
63
|
+
* in AS the store owner and expires in ~60s — open it immediately, never cache.
|
|
64
|
+
*/
|
|
65
|
+
export interface MerchantAdminTicket {
|
|
66
|
+
redirectUrl: string;
|
|
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
|
+
}
|
|
52
118
|
export interface ListMerchantsQuery {
|
|
53
119
|
page?: number;
|
|
54
120
|
/** Max 100. */
|
package/dist/esm/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const SDK_VERSION = "0.
|
|
1
|
+
export declare const SDK_VERSION = "0.3.0";
|
package/dist/esm/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const SDK_VERSION = '0.
|
|
1
|
+
export const SDK_VERSION = '0.3.0';
|